Android开发JNI流程
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