Android NDK开发学习笔记

最近学习了一下Android NDK开发,关于Native、反射、JNI注册相关的原理已经有足量且优秀的文章,本文仅记录一下实验过程。

JNI 参数传递

JNI会把Java中所有对象当做一个C指针传递到本地方法中,这个指针指向JVM内部数据结构,而内部的数据结构在内存中的存储方式是不可见的。只能从JNIEnv指针指向的函数表中选择合适的JNI函数来操作JVM中的数据结构。

以env->GetStringUTFChars()为例

比如native访问java.lang.String 对应的JNI类型jstring时,不能像访问基本数据类型那样使用,因为它是一个Java的引用类型,所以在本地代码中只能通过类似env->GetStringUTFChars这样的JNI函数来访问字符串的内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const char * GetStringUTFChars(JNIEnv *env, jstring string, jboolean *isCopy);

Returns a pointer to an array of bytes representing the string in modified UTF-8 encoding. This array is valid until it is released by ReleaseStringUTFChars().

If isCopy is not NULL, then *isCopy is set to JNI_TRUE if a copy is made; or it is set to JNI_FALSE if no copy is made.

LINKAGE:
Index 169 in the JNIEnv interface function table.

PARAMETERS:
env: the JNI interface pointer.
string: a Java string object.
isCopy: a pointer to a boolean.

RETURNS:
Returns a pointer to a modified UTF-8 string, or NULL if the operation fails.

函数接收一个JNIEnv指针,和目标String在Java层的引用,以及一个flag标识是拷贝字符串(JNI_TRUE)还是引用源字符串的指针(JNI_FALSE)。
如果设置为JNI_FALSE,则可以在native层修改Java层的数据。

从Native修改Java数据

最近研究了一下如何在不进行数据拷贝回传的情况下,从native层修改Java层的数据。
Oracle的JNI文档可以作为在Native层使用相关函数的参考。

通过JNIEnv操纵Java对象修改

比较常用的应该是利用JNIEnv函数来操纵Java层的对象,进而获得对象的属性并修改属性值,另外还可以实现在Native层调用Java对象的方法。
我在这里非常迂回地在Native层实现了一个base64加解密功能,用来实践这种修改思路。这种修改不需要在Java和Native之间显式地传递数据,因为通过JNIEnv函数可以实现从Native主动访问Java层数据。

主要的思路:

  1. 确定要从base64.mInputText获取输出,通过Native层的函数处理之后,赋值给base64.mOutputText.
  2. 在Native的base64decode()函数中,先通过GetClassObject(thiz)获取Java对象base64。
  3. 然后使用GetFieldID()获取base64的mInputText和mOutputText两个属性的属性ID
  4. 基于FieldID通过env-GetObjectField()获取mInputText的属性值,然后进行解码操作得到明文。
  5. 通过env-SetObjectField()把明文赋值给mOutputText。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// base64.java
public class Base64 extends AppCompatActivity {
private static final String TAG = "BASE64";

static {
System.loadLibrary("base64");
}

private Button mBase64Decode, mBase64Encode;
private EditText mInput;
private TextView mOutput;

private String mInputText, mOutputText;

@Override
protected void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
setContentView(R.layout.base64);

mInput = (EditText) findViewById(R.id.base64_input);
mOutput = (TextView) findViewById(R.id.base64_output);

mBase64Decode = (Button) findViewById(R.id.decode_button);
mBase64Encode = (Button) findViewById(R.id.encode_button);
mOut2In = (ImageButton) findViewById(R.id.out2in);
mClearText = (ImageButton) findViewById(R.id.clear_button);

mBase64Decode.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mInputText = mInput.getText().toString();
Log.i(TAG, "before call native function, mInputText = " + mInputText + " mOutputText= " + mOutputText);
base64decode();
Log.i(TAG, "after call native function, mInputText = " + mInputText + " mOutputText= " + mOutputText);
mOutput.setText(mOutputText);
}
});

public native void base64decode();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// base64.cpp
extern "C"
JNIEXPORT void JNICALL
Java_com_example_devndk_Base64_base64decode(JNIEnv *env, jobject thiz) {
// TODO: implement base64decode()
const char *cipherText;
char *plainText;
//先拿到Java层的对象以及对象的mInputText和mOutputText的属性ID
jclass cls = env->GetObjectClass(thiz);
jfieldID inputTextID = env->GetFieldID(cls, "mInputText", "Ljava/lang/String;");
jfieldID outputTextID = env->GetFieldID(cls, "mOutputText", "Ljava/lang/String;");
if(inputTextID == NULL){ return; }

//获取mInputText的属性值,强制转换jobject为jstring
jobject jstr = env->GetObjectField(thiz, inputTextID);
jstring jstrr = (jstring) jstr;

//copy到native变量中
cipherText = env->GetStringUTFChars(jstrr, NULL);
if(cipherText == NULL){ return; }

//开始解码,与JNI无关,可以略过
int cipher_lens = strlen(cipherText);
int plain_lens = 1 + cipher_lens * 3 / 4;
plainText = (char *)malloc(plain_lens * sizeof(char));
for(int ind=0;ind<cipher_lens/4;ind++){
int n1 = c2n(cipherText[ind*4+0]);
int n2 = c2n(cipherText[ind*4+1]);
int n3 = c2n(cipherText[ind*4+2]);
int n4 = c2n(cipherText[ind*4+3]);

plainText[ind*3+0] = (n1<<2) + (n2>>4);
plainText[ind*3+1] = (n2<<4) + (n3>>2);
plainText[ind*3+2] = (n3<<6) + (n4);
}
if(cipherText[cipher_lens-1]=='=') plainText[plain_lens-2]=0;
if(cipherText[cipher_lens-2]=='=') plainText[plain_lens-3]=0;
plainText[plain_lens-1]=0;

//获取解密后的明文plainText,然后赋值给cls.mOutputText
jstr = env->NewStringUTF(plainText);
__android_log_print(ANDROID_LOG_INFO, "BASE64", "JAVA-->C JNI:plainText=%s", plainText);
if(jstr == NULL){ return; }
env->SetObjectField(thiz, outputTextID, jstr);
}

运行结果

image-20210812095201973

通过GetArrayElements()函数直接修改

对于Java的对象,用第一种方法来操纵修改应该是比较方便的。对于一些局部变量,可以考虑向Native层传入地址引用,借助JNIEnv的一些函数来实现无原始数据传输的修改。

在这里使用GetArrayElements()这类函数来实现修改。这类函数在JNI手册中介绍如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Get<PrimitiveType>ArrayElements Routines
NativeType *Get<PrimitiveType>ArrayElements(JNIEnv *env, ArrayType array, jboolean *isCopy);

A family of functions that returns the body of the primitive array. The result is valid until the corresponding Release<PrimitiveType>ArrayElements() function is called. Since the returned array may be a copy of the Java array, changes made to the returned array will not necessarily be reflected in the original array until Release<PrimitiveType>ArrayElements() is called.

If isCopy is not NULL, then *isCopy is set to JNI_TRUE if a copy is made; or it is set to JNI_FALSE if no copy is made.

The following table describes the specific primitive array element accessors. You should make the following substitutions:

Replace Get<PrimitiveType>ArrayElements with one of the actual primitive element accessor routine names from the table.
Replace ArrayType with the corresponding array type.
Replace NativeType with the corresponding native type for that routine.
Regardless of how boolean arrays are represented in the Java VM, GetBooleanArrayElements() always returns a pointer to jbooleans, with each byte denoting an element (the unpacked representation). All arrays of other types are guaranteed to be contiguous in memory.

PARAMETERS:
env: the JNI interface pointer.
array: a Java string object.
isCopy: a pointer to a boolean.

RETURNS:
Returns a pointer to the array elements, or NULL if the operation fails.

以GetCharArrayElements()为例,该函数接收一个jcharArray的指针p和一个用于标识是否拷贝的标志位isCopy。
如果isCopy=NULL或者JNI_TRUE,则会在Native层实现一个数组p的拷贝,否则在Native层仅保留该指针而不拷贝。
此外,若要使得Native发生的修改在Java层生效,需要在修改操作完成之后使用ReleaseCharArrayElements(),并注意设置mode。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Release<PrimitiveType>ArrayElements Routines
void Release<PrimitiveType>ArrayElements(JNIEnv *env,
ArrayType array, NativeType *elems, jint mode);

A family of functions that informs the VM that the native code no longer needs access to elems. The elems argument is a pointer derived from array using the corresponding Get<PrimitiveType>ArrayElements() function. If necessary, this function copies back all changes made to elems to the original array.

The mode argument provides information on how the array buffer should be released. mode has no effect if elems is not a copy of the elements in array. Otherwise, mode has the following impact, as shown in the following table:

Table 4-10 Primitive Array Release Modes
mode actions
0 copy back the content and free the elems buffer
JNI_COMMIT copy back the content but do not free the elems buffer
JNI_ABORT free the buffer without copying back the possible changes

In most cases, programmers pass “0” to the mode argument to ensure consistent behavior for both pinned and copied arrays. The other options give the programmer more control over memory management and should be used with extreme care.

PARAMETERS:
env: the JNI interface pointer.
array: a Java array object.
elems: a pointer to array elements.
mode: the release mode.

实验代码如下:

  1. 在Java层初始化char[] s = “a1234567890”
  2. 将s传入到Native层,使用env->GetCharArrayElements()进行修改,调用env->ReleaseCharArrayElements()提交修改,并适量打印中间结果。
1
2
3
4
5
6
//Java
String ss= "a1234567890";
char[] s = ss.toCharArray();
Log.i(TAG, "before test2change() called, s = " + (new String(s)));
test2change(s);//传入s
Log.i(TAG, "after test2change() called, s = " + (new String(s)));
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//Native C++
Java_com_example_devndk_Base64_test2change(JNIEnv *env, jobject thiz, jcharArray str) {
// TODO: implement test2change()
jchar * s;
jint lens;
s = env->GetCharArrayElements(str, JNI_FALSE);
//获取str在Java层的指针,
lens = (int) env->GetArrayLength(str);

__android_log_print(ANDROID_LOG_INFO, "BASE64", "s=%x &s=%x lens=%x &lens=%x **s=%c", s, &s, lens, &lens, *s);
if(s==NULL){
__android_log_print(ANDROID_LOG_INFO, "BASE64", "s is NULL");
}

for(int ind=0;ind<lens/2;ind++){
s[ind] = 97;
}
env->ReleaseCharArrayElements(str, s, 0);
}

运行结果

image-20210812210613220

从输出可以看到,Java层s数组最终发生变化,Native的变量s为指向Java层s数组的指针。

在Java层打印变量的物理地址(待完善)

事实上,考虑到Java Heap实际上使用的是虚拟内存的缘故,我对s=b64b2ac0是否真的为Java层s数组的物理地址持有一定的怀疑。
所以试图在Java层打印s的物理地址,这是一个非常别扭的想法。
找了半天,发现sun.misc.Unsafe这个类可能会有所帮助,于是尝试了一个第三方库(因为Android不支持Unsafe)和据说能打印物理地址的脚本
但是发现Java和Native的地址并不相同,此处有待探讨,可能是脚本并不能满足要求。

1
2
3
4
5
6
7
//测试语句
String ss= "a1234567890";
char[] s = ss.toCharArray();
printAddresses("Address of s", s);
Log.i(TAG, "before test2change() called, s = " + (new String(s)));
test2change(s);
Log.i(TAG, "after test2change() called, s = " + (new String(s)));
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
//需要导入预编译的第三方库文件
public static void printAddresses(String label, Object... objects) {
System.out.print(label + ": 0x");
long last = 0;
final UnsafeAndroid unsafe = new UnsafeAndroid();
int offset = unsafe.arrayBaseOffset(objects.getClass());
int scale = unsafe.arrayIndexScale(objects.getClass());
switch (scale) {
case 4:
long factor = 1;
// long factor = is64bit ? 8 : 1;
final long i1 = (unsafe.getInt(objects, offset) & 0xFFFFFFFFL) * factor;
System.out.print(Long.toHexString(i1));
last = i1;
for (int i = 1; i < objects.length; i++) {
final long i2 = (unsafe.getInt(objects, offset + i * 4) & 0xFFFFFFFFL) * factor;
if (i2 > last)
System.out.print(", +" + Long.toHexString(i2 - last));
else
System.out.print(", -" + Long.toHexString( last - i2));
last = i2;
}
break;
case 8:
throw new AssertionError("Not supported");
}
System.out.println();
}

image-20210812212750699

通过Direct ByteBuffer修改

有位老哥说他用GetCharArrayElements()方法没有成功修改,阅读他的代码之后感觉是因为没有调用ReleaseCharArrayElements()提交修改导致的。不过他顺便提到了第三种修改的方法–ByteBuffer。

ByteBuffer在内存中预留指定大小的存储空间作为IO数据的临时存储,主要还是用于优化内存性能,同样可以避免在Native拷贝数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// java
try{
byte[] d = {97,97,97,97};
Log.i(TAG, "d=" + (new String(d)));
ByteBuffer input_buffer = ByteBuffer.allocateDirect(d.length);
input_buffer.put(d);

printAddresses("Address input_buffer:", input_buffer);//见上文

test(input_buffer);

input_buffer.flip(); //改变缓存区的读写状态
input_buffer.get(d);

Log.i(TAG, "d=" + (new String(d)));
} catch (Exception e){
e.printStackTrace();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// c++
Java_com_example_devndk_Base64_test(JNIEnv *env, jobject thiz, jobject i) {
int iBufferSize = env->GetDirectBufferCapacity(i);
__android_log_print(ANDROID_LOG_INFO, "BASE64", "iBufferSize = %d", iBufferSize);
char *iBuffer = (char *)env->GetDirectBufferAddress(i);

if(iBuffer != NULL){
__android_log_print(ANDROID_LOG_INFO, "BASE64", "iBuffer=%x *iBuffer=%c", iBuffer, *iBuffer);
for(int i=0;i<iBufferSize;i++){
iBuffer[i]=100;
}
} else{
__android_log_print(ANDROID_LOG_INFO, "BASE64", "iBuffer = NULL");
}
}

运行结果

image-20210813102723917

反射

学习反射

笔者含泪推荐廖雪峰的教程,他从开发的角度出发,讲得非常详细且全面。
在这里简单记录一下我看了其他文章没有理解到却在廖雪峰处学会了的知识点,然后是实验代码

setAccessible(true)并不是修改目标字段的访问权限,而是试图不进行安全检查访问目标字段。如果此时存在安全检查,则setAccessible可能失败。

关于几个获取Method和Field的函数的区别。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
- Field getField(name):根据字段名获取某个public的field(包括父类)
- Field getDeclaredField(name):根据字段名获取当前类的某个field(不包括父类)
- Field[] getFields():获取所有public的field(包括父类)
- Field[] getDeclaredFields():获取当前类的所有field(不包括父类)

- Method getMethod(name, Class...):获取某个public的Method(包括父类)
- Method getDeclaredMethod(name, Class...):获取当前类的某个Method(不包括父类)
- Method[] getMethods():获取所有public的Method(包括父类)
- Method[] getDeclaredMethods():获取当前类的所有Method(不包括父类)

- Constructor getConstructor(Class...):获取某个public的Constructor;
- Constructor getDeclaredConstructor(Class...):获取某个Constructor;
- Constructor[] getConstructors():获取所有public的Constructor;
- Constructor[] getDeclaredConstructors():获取所有Constructor

使用上述函数编写一个函数用于打印目标类的相关信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public void ClassInfo(String ClassName){
try{
Class cls = Class.forName(ClassName);
Object obj = cls.newInstance();

//打印目标类的所有field method constructor
Field[] allClassFields = cls.getFields();
for(Field f: allClassFields){
System.out.println(ClassName + ": PublicField - " + f.getName() + "=" + f.get(obj) + "(" + f.getType() + ")");
}
Field[] allClassDeclaredFields = cls.getDeclaredFields();
for(Field f: allClassDeclaredFields){
f.setAccessible(true);
System.out.println(ClassName + ": DeclaredField - " + f.getName() + "=" + f.get(obj) + "(" + f.getType() + ")");
}

Constructor[] allClassConstructors = cls.getConstructors();
for(Constructor c: allClassConstructors){
System.out.println(ClassName + ": PublicConstructor - " + c.getName() + "(" + c.getParameterTypes().toString() + ")");
}
Constructor[] allClassDeclaredConstructors = cls.getDeclaredConstructors();
for(Constructor c: allClassDeclaredConstructors){
c.setAccessible(true);
System.out.println(ClassName + ": DeclaredConstructor - " + c.getName() + "(" + c.getParameterTypes().toString() + ")");
}

Method[] allClassMethods = cls.getMethods();
for(Method m: allClassMethods){
System.out.println(ClassName + ": PublicMethod - " + m.getName() + "(" + m.getParameterTypes().toString() + ")");
}
Method[] allClassDeclaredMethods = cls.getDeclaredMethods();
for(Method m: allClassDeclaredMethods){
m.setAccessible(true);
System.out.println(ClassName + ": DeclaredMethod - " + m.getName() + "(" + m.getParameterTypes().toString() + ")");
}
} catch (ClassNotFoundException e){
Log.i(TAG, "Error : ClassNotFoundException");
} catch (Exception e){
Log.i(TAG, "Unknown Exception");
}
}

因为需要使用方法签名确定具体调用哪个方法,所以便捷而准确地或者各个方法的签名会有利工作推进。
可以使用Java命令来获取一个类的所有方法的签名,步骤如下:

  1. 打开工作目录APPNAME\app\build\intermediates\javac\debug\classes,同时make project。(Android Studio Ctrl+F9即可)
  2. 命令行 javac -s -p com.example.xxx

运行结果

image-20210814172949102

常用类型的对照表如下

image-20210814213352336

同时,javap的参数规范如下

image-20210814173029942

用于测试的目标类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
public class ReflectionClass extends SuperReflectionClass{
private String TAG = "ReflectionClass";
private String subStr = "ReflectionStr";
private String priStr = "private string";
private int priInt = 123456;
public String pubStr = "public string";
private void priMtd(){
System.out.println("priMtd() called");
}
private void priMtd(String p){
System.out.println("p = " + p + ", priStr=" + this.priStr);
System.out.println("priMtd(String p) called");
}
public void pubMtd(){
System.out.println("pubMtd() called");
}
public ReflectionClass(){
this.subStr = "this is the " + this.subStr;
System.out.println("ReflectionClass() called");
}
public ReflectionClass(String priStr){
this.priStr = priStr;
System.out.println("ReflectionClass(String priStr) called");
}
}

public class SuperReflectionClass {
private String TAG = "superReflectionClass";
private String priStr = "super private string";
private int priInt = 654321;
public String pubStr = "super public string";
public String superStr = "superString";

private void priMtd(){
Log.i(TAG, "superPriMtd() called");
}
public void pubMtd(){
Log.i(TAG, "superPubMtd() called");
}
public void superMtd(){
Log.i(TAG, "superMtd() called");
}
public SuperReflectionClass(){
this.superStr = "This is the " + this.superStr;
Log.i(TAG, "SuperReflectionClass() called");
}
}

运行结果

image-20210814153719963

使用反射

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public void optObjByReflection(){
try{
Class cls = Class.forName("com.example.devndk.ReflectionClass");
//调用无参的构造方法
Object obj = cls.newInstance();

Field priStr = cls.getDeclaredField("priStr");
priStr.setAccessible(true);//设置不进行安全检查访问私有属性
System.out.println("fetch the field priStr = " + priStr.get(obj));

Method priMtd = cls.getDeclaredMethod("priMtd");
priMtd.setAccessible(true);
priMtd.invoke(obj);
//调用有参的构造方法
Constructor c = cls.getDeclaredConstructor(String.class);//通过ParameterTypes确定对应的构造方法
Object obj1 = c.newInstance("constructor with parameters");//传入参数
Field priStr1 = cls.getDeclaredField("priStr");
priStr1.setAccessible(true);
System.out.println("fetch the field priStr = " + priStr1.get(obj));

//调用带参的priMtd方法
Method priMtd1= cls.getDeclaredMethod("priMtd", String.class);//方法名称+对应的paramemterTypes
priMtd1.setAccessible(true);
priMtd1.invoke(obj1, "PriMtd with parameter");//调用方法并传入对应的参数

} catch (Exception e){
e.printStackTrace();
}
}

运行结果

image-20210814154031695

从Native反射

除了在Java层,从Native同样也能完成反射的工作。
所有的流程类似于Java,只是通过JNIEnv函数来实现。同时,获取属性值或者调用方法的时候不需要区分private和public。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
extern "C"
JNIEXPORT void JNICALL
Java_com_example_devndk_MainActivity_reflectByNative(JNIEnv *env, jobject thiz) {
jclass cls = env->FindClass("com/example/devndk/ReflectionClass");
if(cls == NULL){
__android_log_print(ANDROID_LOG_INFO,"JNIMsg", "cls = NULL");
return;
}
jmethodID constructorID = env->GetMethodID(cls, "<init>", "(Ljava/lang/String;)V");
if(constructorID == NULL){
__android_log_print(ANDROID_LOG_INFO,"JNIMsg", "constructorID = NULL");
return;
}
char s[] = "reflection from native";
jstring ss = env->NewStringUTF(s);
jobject obj = env->NewObject(cls, constructorID, ss);

jmethodID priMtdID = env->GetMethodID(cls, "priMtd", "(Ljava/lang/String;)V");
if(priMtdID == NULL){
__android_log_print(ANDROID_LOG_INFO,"JNIMsg", "priMtdID = NULL");
}
env->CallVoidMethod(obj, priMtdID, ss);//这里不需要区分private和public

jfieldID priIntID = env->GetFieldID(cls,"priInt", "I");
if(priIntID == NULL){
__android_log_print(ANDROID_LOG_INFO,"JNIMsg", "priIntID = NULL");
}
__android_log_print(ANDROID_LOG_INFO,"JNIMsg", "ReflectionClass.priInt = %d", env->GetIntField(obj, priIntID));
}

运行结果

image-20210814172116714

JNIEnv调用

在Native函数中直接通过传入的thiz对象来控制Java层调用对象会更加简单,直接借助JNIEnv结构体而不必使用反射技术。
当然,这里thiz代表的是调用该Native方法的Java层实例,所以只能用于控制对应的上层实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
//MainActivity.java
public class MainActivity extends AppCompatActivity {
public static final String TAG = "MainActivity";

static {
System.loadLibrary("native-lib");
}

private String priStr="private string";
private int priInt = 123;
public String pubStr = "public string";
public int pubInt = 456;
private void priMethod(){
Log.i(TAG, "private Method() called");
}
public void pubMethod(){
Log.i(TAG, "public Method() called");
}

@Override
protected void onCreate(Bundle savedInstanceState) {
......
mThiz = (Button) findViewById(R.id.thiz);
mThiz.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
optObjByThiz();
}
});
}

public native void optObjByThiz();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//native-lib.cpp
extern "C"
JNIEXPORT void JNICALL
Java_com_example_devndk_MainActivity_optObjByThiz(JNIEnv *env, jobject thiz) {
// TODO: implement optObjByThiz()

jclass clz = env->GetObjectClass(thiz);
jfieldID priStrID = env->GetFieldID(clz, "priStr", "Ljava/lang/String;");
jfieldID priIntID = env->GetFieldID(clz, "priInt", "I");
jmethodID priMtdID = env->GetMethodID(clz, "priMethod", "()V");

jobject jPriStrObj = env->GetObjectField(thiz, priStrID);
jstring jPriStr = (jstring) jPriStrObj;
const char *priStr = env->GetStringUTFChars(jPriStr, NULL);
__android_log_print(ANDROID_LOG_INFO,"JNIMsg", "private string = %s", priStr);

jint priInt = env->GetIntField(thiz, priIntID);
__android_log_print(ANDROID_LOG_INFO,"JNIMsg", "private int = %d", priInt);

env->CallVoidMethod(thiz, priMtdID);
}

运行结果

image-20210813193006366

JNI动态注册

参考了邓凡平《深入理解Android 卷Ⅰ》第2章 深入理解JNI,推荐阅读。

在前文的JNI函数编写过程中,都是使用的静态方法来注册JNI函数。
既然Java Native函数和JNI函数都是一一对应的,那么应该会有一种数据结构来保存这种对应关系。
在JNI技术中,记录这种对应关系的是一种叫做JNINativeMethod的结构,其定义如下:

1
2
3
4
5
typedef struct{
const char* name; //Java中native函数的名字,不用携带包的路径,如"native_init"
const char* signature; // Java函数的签名信息,用字符串表示,时参数类型和返回值的组合,如"(Ljava/lang/String;)I",(接收一个String参数,返回int)
void* fnPtr; // JNI层对应函数的函数指针,注意他是void*类型
}JNINativeMethod;

于是乎,JNI的动态注册的目标便是在Native层修改JNINativeMethod这个结构。
对于该结构的修改,可以通过env->RegisterNatives()来注册关联关系。

在这里,我们通过动态注册,从Native向Java对象MainActivity注册DynamicJNI()和DynamicJNI_2()两个方法,方法的细节如gMethods数组和它们对应的Native函数所示。
为了把方法注册到MainActivity上,先使用env->FindClass(aimClass)找到目标,然后使用env->RegisterNatives(clazz, gMethods, MethodsCnt)函数,完成gMethods描述的函数注册。该步骤被封装为registerMethods函数,具体实现时记得处理异常情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
void native_DynamicJNI(JNIEnv *env, jclass clazz){
__android_log_print(ANDROID_LOG_INFO,"JNIMsg", "native_DynamicJIN() called");
}

int native_DynamicJNI_2(JNIEnv *env, jclass clazz, jint a, jint b, jstring s){
const char *ss;
ss = env->GetStringUTFChars(s, JNI_FALSE);
__android_log_print(ANDROID_LOG_INFO, "JNIMsg", "native_DynamicNJI_2() called, received parameter from Java: s = %s", ss);
return a+b;
}
static JNINativeMethod gMethods[] = {
{
"DynamicJNI", //供Java引用的方法名
"()V", //描述参数类型和返回值类型,即方法签名
(void *) native_DynamicJNI //Native层定义的函数名,()包裹返回值类型
},
{
"DynamicJNI_2",
"(IILjava/lang/String;)I",
(jint *) native_DynamicJNI_2
}
};

#define METHODS_CNT(x) ((int) (sizeof(x) / sizeof((x)[0])))
#define AIM_CLASS "com/example/devndk/MainActivity"

static int registerMethods(JNIEnv *env, const char *className, JNINativeMethod *gMethods, int MethodsCnt){
jclass clazz = env->FindClass(className);
if(clazz == NULL){
__android_log_print(ANDROID_LOG_INFO, "JNIMsg","FindClass() Error");
return JNI_FALSE;
}
if(env->RegisterNatives(clazz, gMethods, MethodsCnt) < 0){
__android_log_print(ANDROID_LOG_INFO, "JNIMsg","RegisterNatives() Error");
return JNI_FALSE;
}
return JNI_TRUE;
}

在MainActivity.java中,也需要完成相应的声明。(此时忽略Android Studio的错误提示)

1
2
3
4
5
6
7
8
9
10
11
12
public class MainActivity extends AppCompatActivity {
......
public static native void DynamicJNI();
public static native int DynamicJNI_2(int a, int b, String s);
public void main(){
......
DynamicJNI();
int ret = DynamicJNI_2(7,7, "DynamicJNI_2");
Log.i(TAG, "DynamicJNI_2() return " + ret);
.....
}
}

值得讨论的是,动态注册工作在什么时候发生?即registerMethods()函数在什么时候被调用?
答案是:当Java层通过System.loadLibrary()加载完JNI动态库之后,会查找库中名为JNI_OnLoad()的函数,如果存在便调用
因此,动态注册发生在**JNI_OnLoad()**中,也可以说是通过实现JNI_OnLoad函数并在其中调用registerMethods()函数,来完成动态注册。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
jint JNI_OnLoad(JavaVM* vm, void* reserved){
//注意,JavaVM和JNIEnv不同,JavaVM是虚拟机在JNI的代表,每个进程只有一个这样的JavaVM
JNIEnv* env = NULL;
jint result = -1;
if(vm->GetEnv((void**) &env, JNI_VERSION_1_6) != JNI_OK){
return JNI_ERR;
}
assert(env != NULL);

if(!registerMethods(env, AIM_CLASS, gMethods, METHODS_CNT(gMethods))){
return JNI_ERR;
}
return JNI_VERSION_1_6;//必须返回
}

运行结果

image-20210814211854054

Welcome to my other publishing channels