JNI入门和集成现有的C代码

本文详细讲解了在Android开发中使用JNI的全过程,从环境配置到CMake编写,再到已有C库的集成,帮助开发者掌握JNI的基本应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

JNI初始牛刀

第一步,环境配置

NDK需要下载并配置NDK路径,检测local.properties文件有没有NDK路径;
需要注意的是NDK下载已经是V20,在Android Studio下不支持armeabi架构的so文件,如果需要可以自己下载V17.

第二步,创建文件MyJNIUtils.java类

public class MyJNIUtils {
    public native String secret();
}

注意方法secret()被native修饰,此处也可以被private、static修饰;
myutil是将要生成so文件的名称,经过CMake之后最终为libmyutil.so。

第三步,生成MyJNIUtils.class类

一般build下就可以生成MyJNIUtils.class文件,位于
/app/build/intermediates/classes/debug/…/MyJNIUtils.class
也可能会在AndroidJNITest/app/build/intermediates/javac/…中
如果没有,可以使用命令:

javac MyJNIUtils.java.

注意,如果出现找不到类,可以切换到源码文件夹 XXXProject/app/src/main/java或者XXXProject/app/build/intermediates/classes/debug
中执行

第四步,生成JNI对应的头文件

在上一步生成的MyJNIUtils.class文件之后,然后使用javah命令生成头文件:

javah -jni com.xxx.xxx.xxx.MyJNIUtils

这样java和C/C++就形成了映射关系。有了MyJNIUtils.h的头文件,就要实现MyJNIUtils.c.

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_jnidemo_name_MyJNIUtil */

#ifndef _Included_com_jnidemo_name_MyJNIUtil
#define _Included_com_jnidemo_name_MyJNIUtil
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     com_jnidemo_name_MyJNIUtil
 * Method:    secret
 * Signature: ()Ljava/lang/String;
 */
JNIEXPORT jstring JNICALL Java_com_jnidemo_name_MyJNIUtil_secret
  (JNIEnv *, jobject);

#ifdef __cplusplus
}
#endif
#endif

第五步,实现MyJNIUtils.c文件

点击main文件夹,创建JNI folder:
在这里插入图片描述
把上一步生成的MyJNIUtil.h文件移到该文件夹jni下,然后创建MyJNIUtil.c文件:

//
// Created by xxx on 2019-08-09.
//

#include "com_jnidemo_name_MyJNIUtil.h"
JNIEXPORT jstring JNICALL Java_com_jnidemo_name_MyJNIUtil_secret(JNIEnv *env, jobject obj) {

    return (*env)->NewStringUTF(env, "your name");
}

第六步,引入动态库

经过上面的步骤,之后就会生成动态库了,有了动态库,我们就可以引入:

package com.jnidemo.name;

public class MyJNIUtil {
    static {
        System.loadLibrary("myutil");
    }
    public native String secret();
}

第七步,生成so文件

上一步已经介绍了怎么引入so,但是,我们并没有生成so。现在用CMake和LLDB生成(需要提前自己下载好,System Setting->Android SDK)。在项目moudle的build.gradle的android下加入:

externalNativeBuild{
        cmake {
            path file('CMakeLists.txt')
        }
    }

之后将会默认生成arm64-v8a, armeabi-v7a,x86,x86_64四种so文件,但有时候可能只需要armebai-v7a一种架构(其他架构市场上应该没有相应的手机了),所以可以在build.gradle的defaultConfig下添加:

externalNativeBuild{
            cmake {
                cppFlags ""
                abiFilters "armeabi-v7a"
            }
        }

所以最终文件:

apply plugin: 'com.android.application'

android {
    compileSdkVersion 28
    defaultConfig {
        applicationId "com.jnidemo.name"
        ……
        externalNativeBuild{
            cmake {
                cppFlags ""
                abiFilters "armeabi-v7a"
            }
        }
    }
    ……
    externalNativeBuild{
        cmake {
            path file('CMakeLists.txt')
        }
    }
}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
	····
}

第八步 CMake编写

上一步中引入了CMakeLists.txt文件,这一步将会实现它。具体在app中创建CMakeLists.txt文件,注意与build.gradle文件同目录。

# For more information about using CMake with Android Studio, read the
# documentation: https://d.android.com/studio/projects/add-native-code.html
# Sets the minimum version of CMake required to build the native library.
#CMakeLists.txt
cmake_minimum_required(VERSION 3.4.1)
# Creates and names a library, sets it as either STATIC
# or SHARED, and provides the relative paths to its source code.
# You can define multiple libraries, and CMake builds them for you.
# Gradle automatically packages shared libraries with your APK.
add_library( # Sets the name of the library.
        # 设置so文件名称. 
        myutil
        # Sets the library as a shared library.
        SHARED
        # 设置这个so文件为共享.
        # Provides a relative path to your source file(s).
        # 设置这个so文件为共享.
        src/main/jni/com_jnidemo_MyJNIUtil.c)
# Searches for a specified prebuilt library and stores the path as a
# variable. Because CMake includes system libraries in the search path by
# default, you only need to specify the name of the public NDK library
# you want to add. CMake verifies that the library exists before
# completing its build.
find_library( # Sets the name of the path variable.
        log-lib
        # Specifies the name of the NDK library that
        # you want CMake to locate.
        log )
# Specifies libraries CMake should link to your target library. You
# can link multiple libraries, such as libraries you define in this
# build script, prebuilt third-party libraries, or system libraries.
target_link_libraries( # Specifies the target library.
        # 制定目标库.
        myutil
        # Links the target library to the log library
        # included in the NDK.
        ${log-lib} )

有个小技巧,写完CMake之后,上一步也可以自动生成,具体为右击app,选择Link C++ project with gradle也可以生成上面的配置。

最后一步运行

点击make project就可以生成so文件了。MyJNIUtil.java的native方法具体调用和普通调用一样,将会返回“your name”。
如果是按照上面的步骤,JNI基本上都能跑通。到此,JNI的开发就算入门了。

开发已有的c库

我们有时候也会把密钥保存在so文件中,在一定程度上保证了密钥的安全性:

return (*env)->NewStringUTF(env, (char *)"your key");

也有时候公司有些核心业务是c文件写的,此时需要把该库集成至现有的Android Studio项目中,其实也很简单,只需要修改几个地方就可以了,第一个CMakeLists.txt修改:
在这里插入图片描述

把头文件在add_library方法中都声明一下即可。上面是我实现的日志库使用mmap存储的代码。
可以知道store_core是c语言对外提供的接口,所以现在我们的目标就是实现一个对应的java类StoreProtocol,该方法和store_core对应的方法对齐即可:

//
//  store_core.h
//  mmap
//
//  Created xxx
//  Copyright © 2019 xxx. All rights reserved.
//

#ifndef store_core_h
#define store_core_h

#include <stdio.h>
#ifdef __cplusplus
extern "C"
{
#endif//__cplusplus
    int store_init(const char*dirs,size_t max_file);
    int write(const char *string ,const char *key);
    char * read(const char *key,int rows);
    char * read_end(const char *key/*,int *row_count*/);
    int delete(const char *key,int rows);
   
    int rows(const char *key);
#ifdef __cplusplus
}
#endif//__cplusplus
#endif /* store_core_h */

与之对应的java类:

package com.jnidemo.name;

/**
 * @author xxx
 */
public class StoreProtocol {
    private static final String LIBRARY_NAME = "clang";

    static {
        System.loadLibrary(LIBRARY_NAME);
    }
    public native int init(String dirs, long max_file);

    public native int write(String content, String file_name);
    
    public native String read(String file_name, int rows);

    public native String readEnd(String file_name);
  
    public native int delete(String file_name, int rows);

    public native int rows(String file_name);
}

StoreProtocol.java和store_core.c就有对应的关系了,但互相不能调用,中介需要一个JNI接口层,怎么实现呢,其实和上面的步骤一样,首先生成StoreProtocol.java的class文件:StoreProtocol.class,然后根据这个class文件使用javah命令生成JNI头文件:com_jnidemo_name_StoreProtocol.h,然后再实现给头文件的声明:com_jnidemo_name_StoreProtocol.c。头文件在此不贴出了,都是有IDE自动生成的,现在看看c文件内容:

#include "com_jnidemo_name_StoreProtocol.h"
#include "jrstore_core.h"
#include "jrlog.h"
/* Header for class Java_com_jnidemo_name_StoreProtocol */

JNIEXPORT jint JNICALL Java_com_jnidemo_name_StoreProtocol_init
  (JNIEnv *env, jobject instance, jstring dirs_, jlong max_file){
        const char *dirs = (*env)->GetStringUTFChars(env, dirs_, 0);

        LOGI("rows-dubeg reporter");

        jint code = (jint) jrstore_init(dirs, max_file);

        (*env)->ReleaseStringUTFChars(env, dirs_, dirs);
        return code;
  }

/*
 * Method:    write
 * Signature: (Ljava/lang/String;Ljava/lang/String;)I
 */
JNIEXPORT jint JNICALL Java_com_jnidemo_name_StoreProtocol_write
  (JNIEnv *env, jobject instance, jstring content_, jstring file_name_){
          const char *content = (*env)->GetStringUTFChars(env, content_, 0);
          const char *file_name = (*env)->GetStringUTFChars(env, file_name_, 0);

          jint code = (jint) jrwrite(content, file_name);

          (*env)->ReleaseStringUTFChars(env, content_, content);
          (*env)->ReleaseStringUTFChars(env, file_name_, file_name);

          return code;
  }

/*
 * Method:    read
 * Signature: (Ljava/lang/String;I)Ljava/lang/String;
 */
JNIEXPORT jstring JNICALL Java_com_jnidemo_name_StoreProtocol_read
  (JNIEnv *env, jobject instance, jstring file_name_, jint rows_){
            const char *file_name = (*env)->GetStringUTFChars(env, file_name_, 0);
            char * code = jrread(file_name, rows_);
            jstring result = (*env)->NewStringUTF(env, code);
            (*env)->ReleaseStringUTFChars(env, file_name_, file_name);
            return result;
  }

/*
 * Method:    read_end
 * Signature: (Ljava/lang/String;)Ljava/lang/String;
 */
JNIEXPORT jstring JNICALL Java_com_jnidemo_name_StoreProtocol_readEnd
  (JNIEnv *env, jobject instance, jstring file_name_){
            const char *file_name = (*env)->GetStringUTFChars(env, file_name_, 0);
            char * code = jrread_end(file_name);
            jstring result = (*env)->NewStringUTF(env, code);
            (*env)->ReleaseStringUTFChars(env, file_name_, file_name);
            return result;
}

/*
 * Method:    delete
 * Signature: (Ljava/lang/String;I)I
 */
JNIEXPORT jint JNICALL Java_com_jnidemo_name_CStoreProtocol_delete
  (JNIEnv *env, jobject instance, jstring file_name_, jint rows_){
           const char *file_name = (*env)->GetStringUTFChars(env, file_name_, 0);
            jint code = (jint) jrdelete(file_name, rows_);

            (*env)->ReleaseStringUTFChars(env, file_name_, file_name);
            return code;
  }

/*
 * Method:    rows
 * Signature: (Ljava/lang/String;)I
 */
JNIEXPORT jint JNICALL Java_com_jnidemo_name_StoreProtocol_rows
  (JNIEnv *env, jobject instance, jstring file_name_) {
        const char *file_name = (*env)->GetStringUTFChars(env, file_name_, 0);
        jint code = (jint) jrrows(file_name);
        (*env)->ReleaseStringUTFChars(env, file_name_, file_name);
        return code;
  }

至此就完成了java调用现有的c层库业务。点击:
build->Refresh Linked C++ Project. 每次c层代码有更新,都需要该操作。
然后运行即可。
有时候需要像Java层一样打印Log,怎么实现呢,只需要声明一个头文件log.h:

#include <android/log.h>
// 宏定义类似java 层的定义,不同级别的Log LOGI, LOGD, LOGW, LOGE, LOGF。 对就Java中的 Log.i log.d
#define LOG_TAG    "JNILOG" // 这个是自定义的LOG的标识
//#undef LOG // 取消默认的LOG
#define LOGI(...)  __android_log_print(ANDROID_LOG_INFO,LOG_TAG, __VA_ARGS__)
#define LOGD(...)  __android_log_print(ANDROID_LOG_DEBUG,LOG_TAG, __VA_ARGS__)
#define LOGW(...)  __android_log_print(ANDROID_LOG_WARN,LOG_TAG, __VA_ARGS__)
#define LOGE(...)  __android_log_print(ANDROID_LOG_ERROR,LOG_TAG, __VA_ARGS__)
#define LOGF(...)  __android_log_print(ANDROID_LOG_FATAL,LOG_TAG, __VA_ARGS__)

然后在需要打印Log的文件中添加该头文件的声明即可,比如:
LogI(“print logi”);
LogI(“print name : %s”, name);
。。。

其他问题总结:
1、size_t在c中对应java的long或者int
2、c文件对头文件的引用,不能是尖括号,只能是双引号
3、SIMPLE:Error configure
classpath版本号3.1.3修改为3.2.1

学习资料:
1、Android Studio CMake加载整个目录下的C/C++源代码
https://www.jianshu.com/p/c4488cb844fb

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值