01.NDK开发详解
NDK简述
使用NDK开发的so不再具有跨平台特性,需要编译提供不同平台支持ABl:ApplicationBinary Interface。
官方文档:NDK
现在很多x86也可以通过二进制翻译器去加载arm32框架的so文件。
Android Studio里演示
新建一个native C++
#include <jni.h>
#include <string>
extern "C" JNIEXPORT jstring JNICALL
Java_com_example_ndk01_MainActivity_stringFromJNI(
JNIEnv* env,
jobject /* this */) {
std::string hello = "Hello from C++";
return env->NewStringUTF(hello.c_str());
}
int add(int a,int b){
return a+b;
}
extern "C" int add1(int a,int b){
return a+b;
}
两者IDA解析出的就完全不一样
添加extern “C” 保留了函数符号,未添加的则按C++一样name mangling。
如果是C语言编写的,没有添加extern “C”,虽然能编译,但是不能通过链接,会报错。
MainActivity
package com.example.ndk01;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.TextView;
import com.example.ndk01.databinding.ActivityMainBinding;
public class MainActivity extends AppCompatActivity {
// Used to load the 'ndk01' library on application startup.
static {
System.loadLibrary("ndk01");
}
private ActivityMainBinding binding;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ActivityMainBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
// Example of a call to a native method
TextView tv = binding.sampleText;
tv.setText(stringFromJNI());
MainActivity.staticfunc();
}
/**
* A native method that is implemented by the 'ndk01' native library,
* which is packaged with this application.
*/
public native String stringFromJNI();
public native void myfirstjni();
public static native void staticfunc();
}
native-lib.cpp
#include <jni.h>
#include <string>
#include <android/log.h>
extern "C"{//注意一定要添加extern "C"
#include "testc.h"
}
int add(int a,int b){
return a+b;
}
extern "C" int add1(int a,int b){
return a+b;
}
extern "C" JNIEXPORT jstring JNICALL
Java_com_example_ndk01_MainActivity_stringFromJNI(
JNIEnv* env,
jobject /* this */) {
std::string hello = "Hello from C++";
return env->NewStringUTF(hello.c_str());
}
extern "C" JNIEXPORT void JNICALL Java_com_example_ndk01_MainActivity_myfirstjni(JNIEnv* env,jobject /* this */) {
}
extern "C" JNIEXPORT void JNICALL Java_com_example_ndk01_MainActivity_staticfunc(JNIEnv* env,jclass /* this */) {
int result=add_c(4,6);
__android_log_print(ANDROID_LOG_INFO,"JNI","JNI->%d",result);
}
testc.c &&testc.h 以及修改在CMakeLists.txt中添加testc.c
testc.c
//
// Created by MESSI on 2023-01-11.
//
#include "testc.h"
int add_c(int a,int b){
return a+b;
}
testc.h
//
// Created by MESSI on 2023-01-11.
//
#ifndef NDK01_TESTC_H
#define NDK01_TESTC_H
#endif //NDK01_TESTC_H
#include<stdio.h>
int add_c(int a,int b);
output
JNI类型映射
02.NDK开发提升性能
比较JNI 与 JAVA速度之间的差异
Android4.4环境下,使其运行在纯解释模式下。
JNI的速度是Java的4倍多。
Android6.0环境下,在quick模式下运行dex2oat的汇编代码
鉴于代码不是特别复杂,速度也是快了接近一倍。
java 引用数据类型
java中数组类型和JNI的数组类型
JNIEnv
Jni中字符串操作
String returnstring=testjstringapis("hello from jni");
Log.i("Fup1p1","result"+returnstring);
extern "C" JNIEXPORT jstring JNICALL Java_com_example_ndk01_MainActivity_testjstringapis(JNIEnv* env,jobject /* this */,jstring content) {
const char * a=env->GetStringUTFChars(content, nullptr);
int jstring_size=env->GetStringUTFLength(content);
if(a!= nullptr){
__android_log_print( ANDROID_LOG_INFO,"Fup1p1","char content:%s size:%d",a,jstring_size);
}
env->ReleaseStringUTFChars(content,a);
jstring result=env->NewStringUTF("Hello from jni");
return result;
}
03.Java反射思维和NDK开发
java反射:java反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意类的静态属性和方法,都能够完成对静态属性的获取和设置以及静态方法的调用;对于任意一个对象,都能够调用它的任意方法和属性;这种动态获取信息以及动态调用对象方法的功能称为java语言的反射机制。在日常的第三方应用开发过程中,经常会遇到某个类的某个成员变量、方法或是属性是私有的或是只对系统应用开放,这时候就可以利用Java的反射机制通过反射来获取所需的私有成员或是方法。而对于public类型的成员变量和方法和属性都可以使用反射来进行访问。
JAVA反射的相关类
Class类
获得类中属性相关的方法
获得类中构造器相关的方法
获得类中方法相关的方法
类中其他重要的方法
Field、Method、Constructor类
代码实践
反射实现
Test类
package com.example.reflecttest;
import android.util.Log;
public class Test {
public String flag=null;
public Test(){
flag="Test()";
}
public Test(String arg){
flag="Test(String arg)";
}
public Test(String arg,int arg2){
flag="Test(){String arg,int arg2}";
}
public static String publicstaticfield="i am a public staticField";
public String publicNonstaticfield="i am a public non staticField";
private static String privatestaticField="i am a private staticField";
private String privatenonstaticField="i am a private non staticField";
public static void publicStaticfunc(){
Log.i("Fup1p1","i am from publicStaticfunc");
}
public void PublicNonstaticfunc(){
Log.i("Fup1p1","i am from publicnonstaticfunc");
}
private static void privateStaticfunc(){
Log.i("Fup1p1","i am from privateStaticfunc");
}
private void privateNonstaticfunc(){
Log.i("Fup1p1","i am from privatenonstaticfunc");
}
}
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ActivityMainBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
// Example of a call to a native method
TextView tv = binding.sampleText;
tv.setText(stringFromJNI());
test();
}
public void test(){
Test a_obj=new Test();
Test b_obj=new Test("test");
Test c_obj=new Test("test",2);
//方法一,通过classloader来获取。先获得classloader,再通过loadClass,对它进行加载
try {
Class testclazz=MainActivity.class.getClassLoader().loadClass("com.example.reflecttest.Test");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
//方法二,通过两个API,通过Class.forName获取。
Class testclazz2;
try {
testclazz2=Class.forName("com.example.reflecttest.Test");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
//方法三,本例特殊,Test类和MainActivity类在一起,可以直接获取。
Class testclazz3;
testclazz3=Test.class;
try {
//指定域
Field publicstaticfield=testclazz3.getDeclaredField(("publicstaticfield"));
} catch (NoSuchFieldException e) {
e.printStackTrace();
}
//遍历域
Field[] fields=testclazz3.getDeclaredFields();
for (Field i :fields){
Log.i("Fup1p1","getDeclaredFields->"+i);
}
Field[] fields1=testclazz3.getFields();
for (Field i :fields1){
Log.i("Fup1p1","getFields->"+i);
}
}
我们可以发现getDeclaredFields()与getFields()的区别。后者不能访问私有属性。
访问Public属性
Field publicstaticfield=testclazz3.getDeclaredField(("publicstaticfield"));
Log.i("Fup1p1","getDeclaredfield "+publicstaticfield);
String content = (String)publicstaticfield.get(null);
Log.i("Fup1p1","getDeclaredfield "+content);
content=(String) publicstaticfield.get(a_obj);
Log.i("Fup1p1","getDeclaredfield "+content);
访问私有属性
那么对于私有属性,也能get到吗?
我们换一个私有属性进行测试。
因为进行了权限检查,导致私有域不能直接访问,所以程序抛出异常,难道没有其他办法了吗?
真有!!!
Field privatestaticField=testclazz3.getDeclaredField(("privatestaticField"));
Log.i("Fup1p1","getDeclaredfield "+privatestaticField);
privatestaticField.setAccessible(true);
String content = (String)privatestaticField.get(null);
Log.i("Fup1p1","getDeclaredfield "+content);
content=(String) privatestaticField.get(a_obj);
Log.i("Fup1p1","getDeclaredfield "+content);
privatestaticField.setAccessible(true);
通过上面的API,我们可以取消权限检查,使得访问成功!
修改属性
对于修改私有属性,我们只需要
privatestaticField.setAccessible(true);
privatestaticField.set(null,"Fup1p1's field");//static属性,所以obj填null
访问方法
public void testMethod(){
Class testclazz=Test.class;
Method[] methods=testclazz.getDeclaredMethods();
for(Method i:methods){
Log.i("Fup1p1","getDeclaredMethods()-> "+i);
}
Method[] methods1=testclazz.getMethods();
for(Method i:methods1){
Log.i("Fup1p1","getMethods()-> "+i);
}
}
咦,getmethod()获得了更多方法,但是没有private方法。至于多出来的方法,是因为Test类继承object(虽然我们没有写),而getmethod()将继承的公有方法也一并输出了。
调用方法
对于公有方法,直接使用invoke()调用方法
Method publicStaticfunc_method= null;
try {
publicStaticfunc_method = testclazz.getDeclaredMethod("publicStaticfunc");
publicStaticfunc_method.invoke(null);
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
Log.i("Fup1p1","getDeclaredMethods()-> "+publicStaticfunc_method);
对于私有方法,同之前访问Field一样,只需添加setAccessible(true)即可。
Method privateStaticfunc_method= null;
try {
privateStaticfunc_method = testclazz.getDeclaredMethod("privateStaticfunc");
privateStaticfunc_method.setAccessible(true);
privateStaticfunc_method.invoke(null);//static方法,所以参数填null。
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
Log.i("Fup1p1","getDeclaredMethods()-> "+privateStaticfunc_method);
访问构造器
Constructor[] constructors=testclazz.getDeclaredConstructors();
for (Constructor i :constructors){
Log.i("Fup1p1","getDeclaredConstructors()-> "+i);
}
通过反射构造器创建实例
try {
Constructor constructor=testclazz.getConstructor(String.class,int.class);
Object testobj=constructor.newInstance("test",888);
Field privatefield=testclazz.getDeclaredField("privatestaticField");
privatefield.setAccessible(true);
String privatefiled_string= (String) privatefield.get(testobj);
Log.i("Fup1p1","privatestaticField-> "+privatefiled_string);
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (NoSuchFieldException e) {
e.printStackTrace();
}
jni层实现java反射访问公有属性
Java_com_example_reflecttest_MainActivity_stringFromJNI(
JNIEnv* env,
jobject /* this */) {
std::string hello = "Hello from C++";
jclass TestJclass=env->FindClass("com/example/reflecttest/Test");
jfieldID publicstaticfield_jfieldID=env->GetStaticFieldID(TestJclass,"publicstaticfield", "Ljava/lang/String;");
jobject publicstaticfield_content=env->GetStaticObjectField(TestJclass,publicstaticfield_jfieldID);
const char* content_ptr=env->GetStringUTFChars(static_cast<jstring>(publicstaticfield_content), nullptr);
__android_log_print(4,"Fup1p1->jni","jni->%s",content_ptr);
return env->NewStringUTF(hello.c_str());
}
小补充
注意:一个进程JVM中只有一个JavaVM对象。VM是多执行绪(Multi-threading) ,每个JNIEnv都是不同的!特别是在不同线程,都是独自维护各自独立的JNIEnv。
04.JavaVM与JNIEnv
JavaVM
其中,JavaVM虚拟机在JNI层的代表,在一个虚拟机进程中只有一个JavaVM,该进程的所有线程都可以使用此JavaVM。
JavaEnv
JNIEnv指针是Native世界中Java环境的代表,通过该指针Native世界就可以访问Java世界的代码进行操作,其具有以下主要特点及作用:
1.JNIEnv *只对创建它的线程有效,不能跨线程传递,不同线程的JNIEnv是相互独立的;
2.通过JNIEnv *可调用Java的方法;
3.通过JNIEnv *可操作Java中的变量和对象
通过JavaVM的AttachCurrentThread函数可以获取这个线程的JNIEnv,这样就能在不同的线程中调用Java方法了,采用这种方式的要退出线程时调用DetachCurrentThread函数来释放资源。也许你有疑问,JavaVM不是有接口 GetEnv,线程内不可以用GetEnv去获得属于自己的env呢?
只有先AttachCurrentThread到JavaVM,分配到了独立的JNIEnv之后,GetEnv第二个参数二级指针返回的env才有值。就是说JavaVM->GetEnv获取的是,此线程有效的env。JavaVM->AttachCurrentThread是向虚拟机分配线程独立的env。 所以一般在线程执行函数第一句是AttachCurrentThread,随后就能用GetEnv了。
例子
JNI_OnLoad
JNIEXPORT jint JNI_OnLoad(JavaVM* vm,void* reserved){
globalVM=vm;
__android_log_print(4,"Fup1p1->jni"," JNI_OnLoad(JavaVM* vm,void* reserved) vm-> %p",vm);
__android_log_print(4,"Fup1p1->jni","jni->%s","JNI_Onload is called");
jint result=0;
JNIEnv *env= nullptr;
if(vm->GetEnv((void**)&env,JNI_VERSION_1_6)==JNI_OK){
__android_log_print(4,"Fup1p1->jni","jni->%s","vm->GetEnv((void**)&env,JNI_VERSION_1_6) success");
}
__android_log_print(4,"Fup1p1->jni","JNI env-> %p",env);
pthread_t thread;
pthread_create(&thread, nullptr,threadtest, nullptr);
pthread_join(thread, nullptr);
result=JNI_VERSION_1_6;
return result;
}
线程内
void *threadtest(void *args){
for(int i=0;i<10;i++){
__android_log_print(4,"Fup1p1->jni","jni->%s,%d","i am from threadtest",i);
}
JNIEnv *threadenv= nullptr;
int result = globalVM->AttachCurrentThread(&threadenv, nullptr);//这样threadenv就能调用丰富的Api了。
if(result==JNI_OK){
__android_log_print(4,"Fup1p1->jni","jni->%s","GlobalVm->AttachCurrentThread");
jstring jstring1=threadenv->NewStringUTF("threadtest jstring");
const char* content=threadenv->GetStringUTFChars(jstring1, nullptr);
__android_log_print(4,"Fup1p1->jni","jni->%s",content);
threadenv->ReleaseStringUTFChars(jstring1,content);
}
}
05.JNI新建对象、访问Java中属性、访问java方法
JNI新建对象的两种方式
extern "C" JNIEXPORT void JNICALL
Java_com_example_reflecttest_MainActivity_newObject(
JNIEnv* env,
jobject /* this */) {
jclass Testjclass=env->FindClass("com/example/reflecttest/Test");
jmethodID con_mid=env->GetMethodID(Testjclass,"<init>","(Ljava/lang/String;)V");//对于构造函数来说,都是固定的,为“<init>”
//NewObject
jstring arg=env->NewStringUTF("i am from jni");
jobject testobj=env->NewObject(Testjclass,con_mid,arg);
if(testobj!= nullptr){
__android_log_print(4,"Fup1p1->jni","jni->%s","New Object success");
}
//AllocObject
jobject testobj2 =env->AllocObject(Testjclass);
jstring arg1=env->NewStringUTF("I am from jni->AllocObject");
env->CallNonvirtualVoidMethod(testobj2,Testjclass,con_mid,arg1);
if(testobj2!= nullptr){
__android_log_print(4,"Fup1p1->jni","jni->%s","AllocObject success");
}
}
访问Java中的属性
访问静态属性
访问静态Public属性
extern "C" JNIEXPORT void JNICALL
Java_com_example_reflecttest_MainActivity_getStaticFiled(
JNIEnv* env,
jobject /* this */) {
//private static
jclass Testjclass=env->FindClass("com/example/reflecttest/Test");
jfieldID publicStaticField_fid=env->GetStaticFieldID(Testjclass,"publicstaticfield", "Ljava/lang/String;");
jstring publicStaticField_obj= static_cast<jstring>(env->GetStaticObjectField(Testjclass,
publicStaticField_fid));
const char* punlicStaticField_content=env->GetStringUTFChars(publicStaticField_obj, nullptr);
__android_log_print(4,"Fup1p1->jni","getStaicField_obj->%s",punlicStaticField_content);
}
访问静态私有属性
发现只把上面的public改成private,也能正常输出。这和java反射有所不同,不需要取消权限检查。
string 类型
jfieldID privateStaticField_fid=env->GetStaticFieldID(Testjclass,"privatestaticField", "Ljava/lang/String;");
jstring privateStaticField_obj= static_cast<jstring>(env->GetStaticObjectField(Testjclass,
privateStaticField_fid));
const char* privateStaticField_content=env->GetStringUTFChars(privateStaticField_obj, nullptr);
__android_log_print(4,"Fup1p1->jni","getStaicField_obj->%s",privateStaticField_content);
int 类型
相比较string,少一步类型转换而已。
jfieldID publicStaticField_int_fid=env->GetStaticFieldID(Testjclass,"pubicStaticField_int","I");
jint publicStaticField_int_value=env->GetStaticIntField(Testjclass,publicStaticField_int_fid);
__android_log_print(4,"Fup1p1->jni","getStaicField_int_value->%d",publicStaticField_int_value);
修改静态属性
jstring setjstring=env->NewStringUTF("modified by jni");
env->SetStaticObjectField(Testjclass,publicStaticField_fid,setjstring);
jstring publicStaticField_obj= static_cast<jstring>(env->GetStaticObjectField(Testjclass,
publicStaticField_fid));
const char* punlicStaticField_content=env->GetStringUTFChars(publicStaticField_obj, nullptr);
__android_log_print(4,"Fup1p1->jni","getStaicField_obj->%s",punlicStaticField_content);
访问非静态属性
访问非静态公开属性
由于不需要与私有一样,这里省略。
访问非静态私有属性
由于是非静态,所以需要带上参数。
Test testobj=new Test("i am from java");
getNonStaticField(testobj);
extern "C" JNIEXPORT void JNICALL
Java_com_example_reflecttest_MainActivity_getNonStaticField(
JNIEnv* env,
jobject/**/ ,jobject testobj) {
//private String
jclass Testjclass=env->FindClass("com/example/reflecttest/Test");
jfieldID privateField_fid=env->GetFieldID(Testjclass,"privatenonstaticField","Ljava/lang/String;");
jstring privateFieldobj= static_cast<jstring>(env->GetObjectField(testobj, privateField_fid));
const char *privateField_obj_content=env->GetStringUTFChars(privateFieldobj, nullptr);
__android_log_print(4,"Fup1p1->jni","privateField_obj->%s",privateField_obj_content);
}
修改非静态属性
小问一手ChatGPT-4:
在JNI层,访问权限检查主要在Java层进行。当你在Java层通过反射访问私有属性时,需要显式关闭访问权限检查,以避免安全性异常(如 IllegalAccessException)。然而,在JNI层,访问权限检查并不会发生,因此你可以直接通过GetFieldID访问私有属性。
所以我们在编写代码的时候不需要取消权限检查。
修改private string
jstring newString=env->NewStringUTF("Modified by jni");
env->SetObjectField(testobj,privateField_fid,newString);//修改值
privateFieldobj= static_cast<jstring>(env->GetObjectField(testobj, privateField_fid));
privateField_obj_content=env->GetStringUTFChars(privateFieldobj, nullptr);//重新获取一下值
__android_log_print(4,"Fup1p1->jni","privateField_obj_->%s",privateField_obj_content);
修改private int
jfieldID publicField_fid=env->GetFieldID(Testjclass,"pubicField_int","I");
jint public_int_value=env->GetIntField(testobj,publicField_fid);
__android_log_print(4,"Fup1p1->jni","publicField_int_obj->%d",public_int_value);
env->SetIntField(testobj,publicField_fid,114514);
public_int_value=env->GetIntField(testobj,publicField_fid);
__android_log_print(4,"Fup1p1->jni","publicField_int_obj->%d",public_int_value);
访问&&修改array
在test类中先创建一个数组
构造函数中初始化数组
public class Test {
public String flag=null;
public int[] intarray=null;
public Test(){
flag="Test()";
intarray=new int[10];
for(int i=0;i<10;i++){
intarray[i]=i;
}
}
......
}
访问array
jfieldID intarray_fid=env->GetFieldID(Testjclass,"intarray","[I");
jintArray intarray_obj= static_cast<jintArray>(env->GetObjectField(testobj, intarray_fid));
int arraylength=env->GetArrayLength(intarray_obj);//获取数组长度
__android_log_print(4,"Fup1p1->jni","array_length->%d",arraylength);
int* array=env->GetIntArrayElements(intarray_obj, nullptr);//获取数组的指针
for (int i=0;i<arraylength;i++){
__android_log_print(4,"Fup1p1->jni","array[%d]->%d",i,array[i]);
}
修改array
jint jint_array[arraylength];
for (int i=0;i<arraylength;i++){
jint_array[i]=10-i;
}
const jint* ptr=jint_array;
env->SetIntArrayRegion(intarray_obj,0,arraylength,ptr);
array=env->GetIntArrayElements(intarray_obj, nullptr);
for (int i=0;i<arraylength;i++){
__android_log_print(4,"Fup1p1->jni","array[%d]->%d",i,array[i]);
}
效果(如果在java层输出,发现数组也已经被改变)
访问java方法
访问静态方法
jclass clazz=env->FindClass("com/example/reflecttest/Test");
jmethodID publicfuncid=env->GetStaticMethodID(clazz,"publicStaticfunc","()V");
env->CallStaticVoidMethod(clazz,publicfuncid);
访问非静态方法
jclass clazz=env->FindClass("com/example/reflecttest/Test");
jmethodID methodid=env->GetMethodID(clazz,"<init>","()V");
jmethodID privateNonstaticfuncid=env->GetMethodID(clazz,"privateNonstaticfunc","()V");
jobject reflectobj=env->NewObject(clazz,methodid);
env->CallVoidMethod(reflectobj,privateNonstaticfuncid);
思考:调用一个参数和返回值都是数组的方法?
public static int[] privateStaticFunc(String[] str){
StringBuilder retval=new StringBuilder();
for(String i:str){
retval.append(i);
}
Log.i("Fup1p1","this is privateStaticFunc"+retval.toString());
return new int[]{0,1,2,3,4,5,6,7,8,9};
}
jclass clazz = env->FindClass("com/example/reflecttest/Test");
jclass strclazz = env->FindClass("java/lang/String");
jobjectArray strarray = env->NewObjectArray(3, strclazz, nullptr);
//相当于 java 中 String strarray[] =new String[3];
for (int i = 0; i < 3; i++) {
jstring str3 = env->NewStringUTF("114514");
env->SetObjectArrayElement(strarray, i, str3);
}
/* 等于for(int i=0;i<3;i++){
strarray[i]="114514";
}
*/
jmethodID privateStaticFuncid=env->GetStaticMethodID(clazz,"privateStaticFunc","([Ljava/lang/String;)[I");
jintArray intArray= static_cast<jintArray>(env->CallStaticObjectMethod(clazz,privateStaticFuncid,
strarray));
int *array=env->GetIntArrayElements(intArray, nullptr);
int arraylength=env->GetArrayLength(intArray);
for (int i=0;i<arraylength;i++){
__android_log_print(4,"Fup1p1->jni","array[%d]->%d",i,array[i]);
}
env->ReleaseIntArrayElements(intArray,array,JNI_ABORT);//释放一个整型数组在本地代码中申请的内存
06.静态注册与动调注册
JNI函数的注册主要有两种方式:静态注册和动态注册。这两者有很大的不同,也各自具有优缺点。
静态注册:
静态注册是通过在本地代码中定义一个固定格式的函数名,并与Java方法名一一对应。在Java代码中,使用native关键字声明需要本地实现的方法,同时在本地代码中实现对应的方法。
优点:
简单:静态注册的实现比较简单,只需要遵循JNI命名规范。
性能:静态注册的函数查找时间较短,性能较好。
缺点:
命名限制:静态注册需要遵循JNI命名规范,可能导致本地代码的函数名较长且可读性较差。
扩展性:如果有大量的JNI函数,静态注册会导致代码臃肿,不利于维护。
动态注册:
动态注册是在本地代码中通过一个映射表将Java方法名与本地代码中的函数名进行关联,然后在JNI_OnLoad函数中调用RegisterNatives方法进行注册。
优点:
灵活性:动态注册可以自定义函数名,不受JNI命名规范的限制。
扩展性:可以更容易地管理和维护大量的JNI函数,有利于代码组织。
缺点:
实现复杂度:动态注册的实现比静态注册复杂,需要维护映射表并在JNI_OnLoad中进行注册。
性能:动态注册的函数查找时间可能较长,相对静态注册性能较差。
总结:
静态注册和动态注册各有优缺点。静态注册适用于简单的本地方法调用,易于实现,性能较好。而动态注册适用于复杂的JNI场景,具有更好的灵活性和扩展性,但实现相对复杂,性能可能较差。在实际应用中,可以根据项目的实际需求选择合适的JNI函数注册方式。
动调注册样例
jstring test(JNIEnv *env,jobject obj,jstring a,jint b,jbyteArray c){//JNI函数
__android_log_print(4,"Fup1p1->jni","jni->%s","Register Success!");
return env->NewStringUTF("Register Success!");
}
JNIEXPORT jint JNI_OnLoad(JavaVM *vm,void *reserved){
globalVM=vm;
__android_log_print(4,"Fup1p1->jni","JavaVM1->%p",vm);
JNIEnv *Env=nullptr;
if(vm->GetEnv((void**)&Env,JNI_VERSION_1_6)==JNI_OK){
__android_log_print(4,"Fup1p1->jni","jni->%s","Get Version Success!");
}
jclass mainactivityclass=Env->FindClass("com/example/ndkmultithread/MainActivity");
JNINativeMethod methods[]={
{"stringFromJNI2", "(Ljava/lang/String;I[B)Ljava/lang/String;",(void *)test}}//映射表
;
Env->RegisterNatives(mainactivityclass,methods,sizeof(methods)/sizeof(JNINativeMethod));
return JNI_VERSION_1_6;//JNI_OnLoad要求最后一定要返回版本号
}
如果映射表中有多个重名的JNI函数,以最后一个为准,例如:
...
{"stringFromJNI2", "(Ljava/lang/String;I[B)Ljava/lang/String;",(void *)test},
{"stringFromJNI2", "(Ljava/lang/String;I[B)Ljava/lang/String;",(void *)test1}
...
那么最后还是以test1为准。
07.so之间的相互调用
方法一. CMakeList添加参数
编译到同一个so文件下的不同cpp文件之间相互调用其函数,只需要申明一下,就可以调用。
那么对于不同so文件下的,该怎么办呢?
很简单,在CMakeList中,将两个so库link一下即可。(用IDA反编译A.so,可以在导入表中找到libBtest)
target_link_libraries( # Specifies the target library.
ndkmultithreadA
ndkmultithreadB
# Links the target library to the log library
# included in the NDK.
${log-lib})
甚至还能看到其调用的so库
方法二. 通过dlopen,dlsym,dlclose去加载其他so文件(当然可以打开一些其他目录下的,甚至系统so文件)
首先,先学会怎么获取so库的地址
获取app的so库存放地址
public String getlibpath(Context ml){
PackageManager pm=ml.getPackageManager();
List<PackageInfo>pkglist=pm.getInstalledPackages(0);
Log.i("Fup1p1","pkglist"+pkglist);
if(pkglist==null||pkglist.size()==0)return null;
for(PackageInfo pi:pkglist){
if(pi.applicationInfo.nativeLibraryDir.startsWith("/data/app/")&&pi.packageName.startsWith("com.example.ndkmultithread")){ //data/app为apk安装的目录
return pi.applicationInfo.nativeLibraryDir;
}
}
return null;
}
如果需要调用
Log.i("Fup1p1",getlibpath(getApplicationContext())); //通过getApplicationContext()来获取当前的context
so库之间的调用
创建两个so文件),然后在CMakeList里面添加进去
新建一个cpp,在CMakeList中将它添加到到xxxB.so中去。
XXXXB.so
#include<xxx>
...
extern "C" void libBtest(){
__android_log_print(4,"Fup1p1->jni","hello from demo.cpp");
}
//我们之后需要用到符号表,由于C++存在name mangling,编译后符号会变。所以最好添加extern "C",如果不加的话,之后需要用到它的符号表时,就需要使用IDA反编译,寻找它编译后的符号名称。
stringFromJNI函数
比如我们想在xxxA.so的stringFromJNI函数中调用xxxB.so中的函数libBtest
extern "C" JNIEXPORT jstring JNICALL
Java_com_example_ndkmultithread_MainActivity_stringFromJNI(
JNIEnv* env,
jobject /* this */,jstring path) {//注意添加参数path传入so库路径
std::string hello = "Hello from C++";
pthread_t thread;
pthread_create(&thread, nullptr, reinterpret_cast<void *(*)(void *)>(Mthread), nullptr);
pthread_join(thread,nullptr);
const char* cpath=env->GetStringUTFChars(path, nullptr);//dlopen需要传入的类型是const char *,所以我们需要调用函数来讲jstring转成char *
__android_log_print(4,"Fup1p1->jni","cpath->%s",cpath);
void *soinfo=dlopen(cpath,RTLD_NOW);//
void (*ref)(); //无类型函数指针,可以自己写一两个demo玩一下。
ref=reinterpret_cast<void(*)()>(dlsym(soinfo,"libBtest"));//C++是强类型语言,void* 与char *之间的强制类型转换则需要通过reinterpret_cast等函数
//对于"libBtest",如果前面是extern “C”,那么直接写原函数名即可。否则要写经过name mangling之后的符号名。
if(ref== nullptr){
__android_log_print(4,"Fup1p1->jni","jni->%s","ref is null!");
}
ref();//调用libBtest
dlclose(soinfo);//卸载
return env->NewStringUTF(hello.c_str());
}
MainActivity
package com.example.ndkmultithread;
import androidx.appcompat.app.AppCompatActivity;
import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.util.Log;
import android.widget.TextView;
import com.example.ndkmultithread.databinding.ActivityMainBinding;
import java.util.List;
public class MainActivity extends AppCompatActivity {
// Used to load the 'ndkmultithread' library on application startup.
static {
System.loadLibrary("ndkmultithreadA");//只加载xxxxA.so,然后之后通过A.so中的函数去调用B.so中的函数。
}
private ActivityMainBinding binding;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ActivityMainBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
// Example of a call to a native method
TextView tv = binding.sampleText;
String path=getlibpath(getApplicationContext())+"/libndkmultithreadB.so";//传入xxxB.so的名称!!!(注意前缀是有lib的,一开始没加,看了半天才发现...)
tv.setText(stringFromJNI(path));
}
/**
* A native method that is implemented by the 'ndkmultithread' native library,
* which is packaged with this application.
*/
public String getlibpath(Context ml){
PackageManager pm=ml.getPackageManager();
List<PackageInfo>pkglist=pm.getInstalledPackages(0);
Log.i("Fup1p1","pkglist"+pkglist);
if(pkglist==null||pkglist.size()==0)return null;
for(PackageInfo pi:pkglist){
if(pi.applicationInfo.nativeLibraryDir.startsWith("/data/app/")&&pi.packageName.startsWith("com.example.ndkmultithread")){
return pi.applicationInfo.nativeLibraryDir;
}
}
return null;
}
public native String stringFromJNI(String path);
}
小补充
• dlopen
对于dlopen,经常用到的两个参数:
RTLD_LAZY:在dlopen返回前,对于动态库中存在的未定义的变量(如外部变量extern,也可以是函数)不执行解析,就是不解析这个变量的地址。
RTLD_NOW:与上面不同,他需要在dlopen返回前,解析出每个未定义变量的地址,如果解析不出来,在dlopen会返回NULL,错误
• 对于无类型函数指针
#include<iostream>
using namespace std;
void s(){
cout<<"114514";
}
int main(void)
{
void(*sptr)()=&s;
sptr();
}
• 关于GetStringUTFChars函数
一般调用完最好都再加上一局 env->ReleaseStringUTFChars(…);来释放本地缓冲区,不然可能会导致内存泄漏,最终导致应用程序崩溃或者耗尽系统内存。
08.JNI中的内存管理
局部引用
大多数的jni函数,调用以后返回的结果都是局部引用
因此,env->NewLocalRef 基本不用
一个函数内的局部引用数量是有限制的,在早期的安卓系统中,体现的更为明显
当函数体内需要大量使用局部引用时,比如大循环中,最好及时删除不用的局部引用
可以使用 env->DeleteLocalRef 来删除局部引用
for (int i = 0; i < 3; i++) {
jstring str3 = env->NewStringUTF("114514");
env->SetObjectArrayElement(strarray, i, str3);
env->DeleteLocalRef(str3);//这样每次都将局部引用的str3给删去。程序依然能正常运行。
}
局部引用相关的其他函数
env->EnsureLocalCapacity(num) 判断是否有足够的局部引用可以使用,足够则返回0
if(env->EnsureLocalCapacity(100)==0){
for (int i = 0; i < 3; i++) {
jstring str3 = env->NewStringUTF("114514");
env->SetObjectArrayElement(strarray, i, str3);
env->DeleteLocalRef(str3);
sleep(1);//日志输出速度跟不上代码执行速度,不加sleep(1),log只会输出两次。而实际上代码循环了三次。
__android_log_print(4,"Fup1p1->jni","EnsureLocalCapacity");
}
}
需大量使用局部引用时,手动删除太过麻烦,可使用以下两个函数来批量管理局部引用
env->PushLocalFrame(num)
env->PopLocalFrame(nullptr)
env->PushLocalFrame(100);
if(env->EnsureLocalCapacity(100)==0){
for (int i = 0; i < 3; i++) {
jstring str3 = env->NewStringUTF("114514");
env->SetObjectArrayElement(strarray, i, str3);
sleep(1);
__android_log_print(4,"Fup1p1->jni","EnsureLocalCapacity");
}
}
env->PopLocalFrame(nullptr);
全局引用
在jni开发中,需要跨函数使用变量时,直接定义全局变量是没用的
需要使用以下两个方法,来创建和删除全局引用
env->NewGlobalRef
env->DeleteGlobalRef
比如我们之前的例子中经常用到jclass clazz = env->FindClass(“com/example/reflecttest/Test”);
我们能否把它定义成全局引用呢?
直接定义为全局变量,然后直接引用,会发现程序能通过编译,但是运行就会崩溃。
这时候我们就需要用到NewGlobalRef函数
jclass clazz1;//先定义一个全局变量
JNIEXPORT jint JNI_OnLoad(JavaVM* vm,void* reserved){
jint result=0;
JNIEnv *env= nullptr;
if(vm->GetEnv((void**)&env,JNI_VERSION_1_6)==JNI_OK){
__android_log_print(4,"Fup1p1->jni","jni->%s","vm->GetEnv((void**)&env,JNI_VERSION_1_6) success");
}
jclass tempclazz=env->FindClass("com/example/reflecttest/Test");
clazz1= static_cast<jclass>(env->NewGlobalRef(tempclazz));//创建全局引用
result=JNI_VERSION_1_6;
return result;
}
弱全局引用
与全局引用基本相同,区别是弱全局引用有可能会被回收
env->NewWeakGlobalRef
env->DeleteWeakGlobalRef
总结
全局引用通常用于跨越多个本地方法调用来访问Java对象。当你需要在不同的本地方法中共享Java对象时,可以使用全局引用。
弱全局引用适用于那些希望引用Java对象但不阻止垃圾回收的场景,比如缓存。通过使用弱全局引用,你可以确保在内存紧张时,Java虚拟机可以回收不再需要的对象。
09.子线程中加载类
方法一. 在子线程中,findClass可以直接获取系统类
前面有例子,就不举例了,要注意的就是要先通过AttachCurrentThread获取env。
方法二.在主线程中获取类,使用全局引用来传递到子线程中
jobject classloader;//定义一个全局变量
JNIEXPORT jint JNI_OnLoad(JavaVM *vm,void *reserved){
globalVM=vm;
__android_log_print(4,"Fup1p1->jni","JavaVM1->%p",vm);
JNIEnv *Env=nullptr;
if(vm->GetEnv((void**)&Env,JNI_VERSION_1_6)==JNI_OK){
__android_log_print(4,"Fup1p1->jni","jni->%s","Get Version Success!");
}
//下面一段相当于MainActivity.class.getClassLoader()
jclass mainactivityclass=Env->FindClass("com/example/ndkmultithread/MainActivity");
jclass classclazz=Env->FindClass("java/lang/Class");
jmethodID getClassLoaderid= Env->GetMethodID(classclazz,"getClassLoader", "()Ljava/lang/ClassLoader;");
jobject tempclassloaderobj=Env->CallObjectMethod(mainactivityclass,getClassLoaderid);
classloader=Env->NewGlobalRef(tempclassloaderobj);//设置为全局引用
return JNI_VERSION_1_6;
}
方法三.在主线程中获取正确的ClassLoader,在子线程中去加载类
3.1 在Java中,可以先获取类字节码,然后使用getClassLoader()来获取
Demo.class.getClassLoader()
new Demo().getClass().getClassLoader()
Class.forName(…).getClassLoader()
3.2 在jni的主线程中获取ClassLoader
3.3 在jni的子线程中使用loadClass
//MainActivity中
public static void func(){
Log.i("Fup1p1","You have got it!");
}
void Mthread() {
JNIEnv *env= nullptr;
int result = globalVM->AttachCurrentThread(&env, nullptr);
jclass loadclass=env->FindClass("java/lang/ClassLoader");
jmethodID loadclassmethodid=env->GetMethodID(loadclass,"loadClass", "(Ljava/lang/String;)Ljava/lang/Class;");
jclass MainActivityclazz=static_cast<jclass>(env->CallObjectMethod(classloader,loadclassmethodid,env->NewStringUTF("com.example.ndkmultithread.MainActivity")));
//MainActivity.class.getClassLoader().loadClass();
jmethodID fun=env->GetStaticMethodID(MainActivityclazz,"func","()V");
env->CallStaticVoidMethod(MainActivityclazz,fun);
pthread_exit(0);
}
10.init与initarray以及onCreatre Native化
so在执行JNI_Onload之前,还会执行两个构造函数init、initarray
so加固、so中字符串加密等等,一般会把相关代码放到这里
init的使用
extern “C” void _init(){ //函数名必须为_init
......
}
IDA中怎么找到这个函数呢?查看导出表,搜_init,搜不到?搜一搜.init_proc。为什么,不是加了extern “C”吗?
问了一手ChatGPT4:编译器为了避免潜在的命名冲突,可能会给函数名加上特定的前缀或后缀。
┓( ´∀` )┏
initarray的使用
__attribute__ ((constructor)) void initArrayTest1(){ ... }
__attribute__ ((constructor(200))) void initArrayTest2(){ ... }
__attribute__ ((constructor(101))) void initArrayTest3(){ ... }
__attribute__ ((constructor, visibility("hidden"))) void initArrayTest4(){ ... }//visibility("hidden"),这样函数名就隐藏了(符号表被去除了),IDA的导出表就看不见这个函数了。
constructor后面的值,较小的先执行,最好从100以后开始用
如果constructor后面没有跟值,那么按定义的顺序,从上往下执行
onCreate Native化
有些安全开发的喜欢这样搞。
自己改写了一遍,快累死了…
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ActivityMainBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
// Example of a call to a native method
TextView tv = binding.sampleText;
String path=getlibpath(getApplicationContext())+"/libndkmultithreadB.so";
Log.i("Fup1p1",stringFromJNI2("114514",100,"114514".getBytes()));
tv.setText(stringFromJNI(path));
}
Native化,前方高能
extern "C"
JNIEXPORT void JNICALL
Java_com_example_ndkmultithread_MainActivity_onCreate(JNIEnv *env, jobject thiz,
jobject saved_instance_state) {
// super.onCreate(savedInstanceState);
jclass clazz=env->FindClass("androidx/fragment/app/FragmentActivity");
jmethodID oncreateid=env->GetMethodID(clazz,"onCreate", "(Landroid/os/Bundle;)V");
env->CallNonvirtualVoidMethod(thiz,clazz,oncreateid,saved_instance_state);
//-----------------------------------------------------------------------------------------------------------------------------------------------------------
jclass Activityclazz=env->FindClass("android/app/Activity");
jmethodID getLayoutInflaterid =env->GetMethodID(Activityclazz,"getLayoutInflater", "()Landroid/view/LayoutInflater;");
jobject LayoutInflater=env->CallObjectMethod(thiz,getLayoutInflaterid);
jclass ActivityMainBindingclazz= env->FindClass("com/example/ndkmultithread/databinding/ActivityMainBinding");//这个类是编译后产生的,与页面绑定的
jmethodID inflateid =env->GetStaticMethodID(ActivityMainBindingclazz,"inflate","(Landroid/view/LayoutInflater;)Lcom/example/ndkmultithread/databinding/ActivityMainBinding;");
jobject ActivityMainBindingobj= env->CallStaticObjectMethod(ActivityMainBindingclazz,inflateid,LayoutInflater);
// binding = ActivityMainBinding.inflate(getLayoutInflater());
//-----------------------------------------------------------------------------------------------------------------------------------------------------------
jmethodID getrootid=env->GetMethodID(ActivityMainBindingclazz,"getRoot","()Landroidx/constraintlayout/widget/ConstraintLayout;");
jobject ConstraintLayoutobj = env->CallObjectMethod(ActivityMainBindingobj,getrootid);
jclass AppCompatActivityclazz=env->FindClass("androidx/appcompat/app/AppCompatActivity");
jmethodID setContentViewid=env->GetMethodID(AppCompatActivityclazz,"setContentView", "(Landroid/view/View;)V");
env->CallVoidMethod(thiz,setContentViewid,ConstraintLayoutobj);
// setContentView(binding.getRoot());
//-----------------------------------------------------------------------------------------------------------------------------------------------------------
// TextView tv = binding.sampleText;
jfieldID sampleTextid=env->GetFieldID(ActivityMainBindingclazz,"sampleText","Landroid/widget/TextView;");
jobject tv=env->GetObjectField(ActivityMainBindingobj,sampleTextid);
//-----------------------------------------------------------------------------------------------------------------------------------------------------------
// String path=getlibpath(getApplicationContext())+"/libndkmultithreadB.so";
jclass getApplicationContextclazz=env->FindClass("android/content/ContextWrapper");
jmethodID getApplicationContextid=env->GetMethodID(getApplicationContextclazz,"getApplicationContext","()Landroid/content/Context;");
jobject getApplicationContextobj=env->CallObjectMethod(thiz,getApplicationContextid);
jclass getlibpathclazz=env->FindClass("com/example/ndkmultithread/MainActivity");
const char* path_0= "/libndkmultithreadB.so";
jmethodID getlibpathid=env->GetMethodID(getlibpathclazz,"getlibpath", "(Landroid/content/Context;)Ljava/lang/String;");
const char * path_1= env->GetStringUTFChars(static_cast<jstring>(env->CallObjectMethod(thiz, getlibpathid,
getApplicationContextobj)),
nullptr);
char *path;
strcat(path, path_1);
strcat(path,path_0);
__android_log_print(4,"Fup1p1","fullgetlibpath result is %s",path);
//-----------------------------------------------------------------------------------------------------------------------------------------------------------
// Log.i("Fup1p1",stringFromJNI2("114514",100,"114514".getBytes()));
jclass mainactivityclass=env->FindClass("com/example/ndkmultithread/MainActivity");
// jclass mainactivityclass=env->GetObjectClass(thiz); 两个都行
jmethodID stringFromJNI2id=env->GetMethodID(mainactivityclass,"stringFromJNI2", "(Ljava/lang/String;I[B)Ljava/lang/String;");
jstring arg1=env->NewStringUTF("114514");
jint arg2=100;
jbyteArray arg3=env->NewByteArray(7);
env->SetByteArrayRegion(arg3, 0, 7, reinterpret_cast<const jbyte *>("114514"));
const char * log_i= env->GetStringUTFChars(static_cast<jstring>(env->CallObjectMethod(thiz, stringFromJNI2id, arg1, arg2,
arg3)),
nullptr);
__android_log_print(4,"Fup1p1","stringFromJNI2 result is %s",log_i);
//-----------------------------------------------------------------------------------------------------------------------------------------------------------
jmethodID stringFromJNIid=env->GetMethodID(mainactivityclass,"stringFromJNI", "(Ljava/lang/String;)Ljava/lang/String;");
jobject stringFromJNIobj= env->CallObjectMethod(thiz,stringFromJNIid,env->NewStringUTF(path));
jclass setTextclazz=env->FindClass("android/widget/TextView");
jmethodID setTextid=env->GetMethodID(setTextclazz,"setText", "(Ljava/lang/CharSequence;)V");
env->CallVoidMethod(tv,setTextid,stringFromJNIobj);
// tv.setText(stringFromJNI(path));
}
评论区