//12.1 获取字节码 const char *person_class_str = “com/devyk/ndk_sample/Person”; //12.2 转 jni jclass jclass person_class = env->FindClass(person_class_str); //12.3 方法签名 javap -a const char *sig = “()Ljava/lang/String;”; jmethodID jmethodID1 = env->GetMethodID(person_class, “getName”, sig);
jobject obj_string = env->CallObjectMethod(person, jmethodID1); jstring perStr = static_cast(obj_string); const char *itemStr2 = env->GetStringUTFChars(perStr, NULL); LOGD(“Person: %s”, itemStr2); env->DeleteLocalRef(person_class); // 回收 env->DeleteLocalRef(person); // 回收
//13. 打印 Java 传递过来的 booleanArray jsize booArrayLength = env->GetArrayLength(bArray_); jboolean *bArray = env->GetBooleanArrayElements(bArray_, NULL); for (int i = 0; i < booArrayLength; i) { bool b = bArray[i]; jboolean b2 = bArray[i]; LOGD(“boolean:%d”,b) LOGD(“jboolean:%d”,b2) } //回收 env->ReleaseBooleanArrayElements(bArray_, bArray, 0);
}
输出:
native-lib: boolean-> 1 native-lib: jbyte-> 1 native-lib: char-> 44 native-lib: short-> 3 native-lib: long-> 4 native-lib: float-> 3.300000 native-lib: double-> 2.200000 native-lib: string-> DevYK native-lib: int:28 native-lib: intArray->1: native-lib: intArray->2: native-lib: intArray->3: native-lib: intArray->4: native-lib: intArray->5: native-lib: intArray->6: native-lib: intArray->7: native-lib: String[0]: 1 native-lib: String[1]: 2 native-lib: String[2]: 4 native-lib: Person: 阳坤 native-lib: boolean:0 native-lib: jboolean:0 native-lib: boolean:1 native-lib: jboolean:1
- 定义一个 Java 对象
public class Person { private String name; private int age;
public int getAge() { return age; }
public void setAge(int age) { this.age = age; }
public Person(String name, int age) { this.name = name; this.age = age; }
public void setName(String name){ this.name = name; }
public String getName(){ return name; }
@Override public String toString() { return “Person{” “name=’” name ‘’’ “, age=” age ‘}’; } }
- 定义 native 接口
public class MainActivity extends AppCompatActivity {
private String TAG = this.getClass().getSimpleName();
/**
- 1. 加载 native 库 */ static { System.loadLibrary(“native-lib”); }
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); TextView text = findViewById(R.id.sample_text); /*处理 Java 对象/ String str = getPerson().toString(); text.setText(str); } public native Person getPerson(); }
根据上述代码,我们知道,如果成功,数据肯定会打印在手机屏幕上。
- JNI 的处理
extern “C” JNIEXPORT jobject JNICALL Java_com_devyk_ndk_1sample_MainActivity_getPerson(JNIEnv *env, jobject instance) {
//1. 拿到 Java 类的全路径 const char *person_java = “com/devyk/ndk_sample/Person”; const char *method = “”; // Java标识结构方法
//2. 找到需要处理的 Java 对象 class jclass j_person_class = env->FindClass(person_java);
//3. 获取空参结构方法 jmethodID person_constructor = env->GetMethodID(j_person_class, method, “()V”);
//4. 创建对象 jobject person_obj = env->NewObject(j_person_class, person_constructor);
//5. 拿到 setName 签名方法,并获得相应的方法 setName 方法 const char *nameSig = “(Ljava/lang/String;)V”; jmethodID nameMethodId = env->GetMethodID(j_person_class, “setName”, nameSig);
//6. 拿到 setAge 并获得方法签名 setAge 方法 const char *ageSig = “(I)V”; jmethodID ageMethodId = env->GetMethodID(j_person_class, “setAge”, ageSig);
//7. 正在调用 Java 对象函数 const char *name = “DevYK”; jstring newStringName = env->NewStringUTF(name); env->CallVoidMethod(person_obj, nameMethodId, newStringName); env->CallVoidMethod(person_obj, ageMethodId, 28);
const char *sig = “()Ljava/lang/String;”; jmethodID jtoStrin = env->GetMethodID(j_person_class, “toString”, sig); jobject obj_string = env->CallObjectMethod(person_obj, jtoString); jstring perStr = static_cast(obj_string); const char *itemStr2 = env->GetStringUTFChars(perStr, NULL); LOGD(“Person: %s”, itemStr2); return person_obj; }
输出:
可以看到 native 返回数据给 Java 了。
5. JNI 动态注册
前面咱们学习的都是静态注册,静态注册虽然简单方便,但是也面临一个较大的问题,如果当前类定义的 native 方法名称改变或者包名改变,那么这一改也就面临在 cpp 中实现的也将改动,如果将要面临这种情况你可以试试 JNI 动态注册,如下代码所示:
public class MainActivity extends AppCompatActivity {
private String TAG = this.getClass().getSimpleName();
/**
- 1. 加载 native 库 */ static { System.loadLibrary(“native-lib”); }
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); TextView text = findViewById(R.id.sample_text);
/**动态注册的 native */ dynamicRegister(“我是动态注册的”); }
/**
- 动态注册 */ public native void dynamicRegister(String name); } 复制代码
cpp:
#include <jni.h> #include #include <android/log.h>
#include
#define TAG “native-lib” // 代表 …的可变参数 #define LOGD(…) __android_log_print(ANDROID_LOG_DEBUG, TAG, ); #define LOGE(…) __android_log_print(ANDROID_LOG_ERROR, TAG, ); #define LOGI(…) __android_log_print(ANDROID_LOG_INFO, TAG, );
/**
- TODO 动态注册 */
/**
- 对应java类的全路径名,.用/代替 */ const char *classPathName = “com/devyk/ndk_sample/MainActivity”;
extern “C” //支持 C 语言 JNIEXPORT void JNICALL //告诉虚拟机,这是jni函数 native_dynamicRegister(JNIEnv *env, jobject instance, jstring name) { const char *j_name = env->GetStringUTFChars(name, NULL); LOGD(“动态注册: %s”, j_name) //释放 env->ReleaseStringUTFChars(name, j_name); }
/* 源码结构体
- typedef struct { const char* name; const char* signature; void* fnPtr; } JNINativeMethod; */ static const JNINativeMethod jniNativeMethod[] = { {“dynamicRegister”, “(Ljava/lang/String;)V”, (void *) (native_dynamicRegister)} };
/**
- 该函数定义在jni.h头文件中,System.loadLibrary()时会调用JNI_OnLoad()函数 */ JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *javaVm, void *pVoid) { //通过虚拟机 创建爱你全新的 evn JNIEnv *jniEnv = nullptr; jint result = javaVm->GetEnv(reinterpret_cast<void **>(&jniEnv), JNI_VERSION_1_6); if (result != JNI_OK) { return JNI_ERR; // 主动报错 } jclass mainActivityClass = jniEnv->FindClass(classPathName); jniEnv->RegisterNatives(mainActivityClass, jniNativeMethod, sizeof(jniNativeMethod) / sizeof(JNINativeMethod));//动态注册的数量
return JNI_VERSION_1_6; } 复制代码
动态注册: 我是动态注册的
6. 异常处理
异常处理是 Java 程序设计语言的重要功能, JNI 中的异常行为与 Java 中的有所不同,在 Java 中,当抛出一个异常时,虚拟机停止执行代码块并进入调用栈反向检查能处理特定类型异常的异常处理程序代码块,这也叫捕获异常。虚拟机清除异常并将控制权交给异常处理程序。相比之下, JNI 要求开发人员在异常发生后显式地实现异常处理流。
JNIEvn 接口提供了一组与异常相关的函数集,在运行过程中可以使用 Java 类查看这些函数,比如代码如下:
public native void dynamicRegister2(String name);
/**
- 测试抛出异常
- @throws NullPointerException */ private void testException() throws NullPointerException { throw new NullPointerException(“MainActivity testException NullPointerException”); }
当调用 testException 方法时,dynamicRegister2 该原生方法需要显式的处理异常信息,JNI 提供了 ExceptionOccurred 函数查询虚拟机中是否有挂起的异常。在使用完之后,异常处理程序需要用 ExceptionClear 函数显式的清除异常,如下代码:
jthrowable exc = env->ExceptionOccurred(); // 检测是否发生异常 if (exc) {//如果发生异常 env->ExceptionDescribe(); // 打印异常信息 env->ExceptionClear(); // 清除掉发生的异常 }
JNI 也允许原生代码抛出异常。因为异常是 Java 类,应该先用 FindClass 函数找到异常类,用 ThrowNew 函数可以使用化且抛出新的异常,如下代码所示:
jthrowable exc = env->ExceptionOccurred(); // 检测是否发生异常 if (exc) {//如果发生异常 jclass newExcCls = env->FindClass(“java/lang/IllegalArgumentException”); env->ThrowNew(newExcCls, “JNI 中发生了一个异常信息”); // 返回一个新的异常到 Java }
因为原生函数的代码执行不受虚拟机的控制,因此抛出异常并不会停止原生函数的执行并把控制权交给异常处理程序。到抛出异常时,原生函数应该释放所有已分配的原生资源,例如内存及合适的返回值等。通过 JNIEvn 接口获得的引用是局部引用且一旦返回原生函数,它们自动地被虚拟机释放。
public class MainActivity extends AppCompatActivity {
private String TAG = this.getClass().getSimpleName();
/**
- 1. 加载 native 库 */ static { System.loadLibrary(“native-lib”); }
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main);
dynamicRegister2(“测试异常处理”); }
public native void dynamicRegister2(String name);
/**
- 测试抛出异常
- @throws NullPointerException */ private void testException() throws NullPointerException { throw new NullPointerException(“MainActivity testException NullPointerException”); }
}
native-lib.cpp 文件
#include <jni.h> #include #include <android/log.h>
#include
#define TAG “native-lib” // 代表 …的可变参数 #define LOGD(…) __android_log_print(ANDROID_LOG_DEBUG, TAG, ); #define LOGE(…) __android_log_print(ANDROID_LOG_ERROR, TAG, ); #define LOGI(…) __android_log_print(ANDROID_LOG_INFO, TAG, );
/**
- TODO 动态注册 */
…
…
extern “C” //支持 C 语言 JNIEXPORT void JNICALL //告诉虚拟机,这是jni函数 native_dynamicRegister2(JNIEnv *env, jobject instance, jstring name) { const char *j_name = env->GetStringUTFChars(name, NULL); LOGD(“动态注册: %s”, j_name)
jclass clazz = env->GetObjectClass(instance);//拿到当前类的class jmethodID mid =env->GetMethodID(clazz, “testException”, “()V”);//执行 Java 测试抛出异常的代码 env->CallVoidMethod(instance, mid); // 执行会抛出一个异常 jthrowable exc = env->ExceptionOccurred(); // 检测是否发生异常 if (exc) {//如果发生异常 env->ExceptionDescribe(); // 打印异常信息 env->ExceptionClear(); // 清除掉发生的异常 jclass newExcCls = env->FindClass(“java/lang/IllegalArgumentException”); env->ThrowNew(newExcCls, “JNI 中发生了一个异常信息”); // 返回一个新的异常到 Java }
//释放 env->ReleaseStringUTFChars(name, j_name); }
…
这里还是使用的动态注册。
最后效果如下:
可以看见这里即捕获到了 Java 抛出的异常,也抛出了一个 JNI 新的异常信息。
7. 局部与全局引用
引用在 Java 程序设计中扮演非常重要的角色。虚拟机通过追踪类实例的引用并收回不在引用的垃圾来管理类实例的使用期限。因为原生代码不是一个管理环境,因此 JNI 提供了一组函数允许原生代码显式地管理对象引用及使用期间原生代码。 JNI 支持三种引用: 局部引用、全局引用和弱全局引用。下面将介绍这几类引用。
大多数 JNI 函数返回局部引用。局部应用不能在后续的调用中被缓存及重用,主要是因为它们的使用期限仅限于原生方法,一旦原生方法返回,局部引用即被释放。例如: FindClass 函数返回一个局部引用,当原生方法返回时,它被自动释放,也可以用 DeleteLocalRef 函数显式的释放原生代码。如下代码所示:
jclass personClass; extern “C” //支持 C 语言 JNIEXPORT void JNICALL //告诉虚拟机,这是jni函数 native_test4(JNIEnv *env, jobject instance) { LOGD(“测试局部引用”) if (personClass == NULL) { const char *person_class = “com/devyk/ndk_sample/Person”; personClass = env->FindClass(person_class); LOGD(“personClass == null 执行了。”) } //Java Person 构造方法实例化 const char *sig = “()V”; const char *method = “”;//Java 构造方法标识 jmethodID init = env->GetMethodID(personClass, method, sig); //创建出来 env->NewObject(personClass, init); }
效果:
跟介绍的一样的吧。局部引用不能再后续的调用中重复使用,那么怎么解决这个问题勒,其实只要把局部引用提升为全局引用或者调用 DeleteLocalRef 显式释放就行了。这个我们在全局引用中演示。
全局引用在原生方法的后续调用过程中依然有效,除非它们被原生代码显式释放。
- 创建全局引用
可以使用 NewGlobalRef 函数将局部引用初始化为全局引用,如下代码所示:
jclass personClass; extern “C” //支持 C 语言 JNIEXPORT void JNICALL //告诉虚拟机,这是jni函数 native_test4(JNIEnv *env, jobject instance) { LOGD(“测试局部引用”)
if (personClass == NULL) { //1. 提升全局解决不能重复使用问题 const char *person_class = “com/devyk/ndk_sample/Person”; jclass jclass1 = env->FindClass(person_class); personClass = static_cast(env->NewGlobalRef(jclass1)); LOGD(“personClass == null 执行了。”) }
//Java Person 构造方法实例化 const char *sig = “()V”; const char *method = “”;//Java 构造方法标识 jmethodID init = env->GetMethodID(personClass, method, sig); //创建出来 env->NewObject(personClass, init);
//2. 显式释放主动删除全局引用 env->DeleteLocalRef(personClass); personClass = NULL; }
- 删除全局引用
当原生代码不再需要一个全局引用时,可以随时用 DeleteGlobalRef 函数释放它,如下代码所示:
env->DeleteLocalRef(personClass); personClass = NULL;
全局引用的另一种类型是弱全局引用。与全局引用一样,弱全局引用在原生方法的后续调用依然有效。与全局引用不同,弱全局引用并不阻止潜在的对象被垃圾收回。
- 创建弱全局引用
可以使用 NewWeakGlobalRef 函数对弱全局引用进行初始化,如下所示:
jclass personClass; extern “C” //支持 C 语言 JNIEXPORT void JNICALL //告诉虚拟机,这是jni函数 native_test4(JNIEnv *env, jobject instance) { LOGD(“测试局部引用”)
if (personClass == NULL) { //1. 提升全局解决不能重复使用问题 const char *person_class = “com/devyk/ndk_sample/Person”; jclass jclass1 = env->FindClass(person_class); // personClass = static_cast(env->NewGlobalRef(jclass1)); personClass = static_cast(env->NewWeakGlobalRef(jclass1)); LOGD(“personClass == null 执行了。”) }
//Java Person 构造方法实例化 const char *sig = “()V”; const char *method = “”;//Java 构造方法标识 jmethodID init = env->GetMethodID(personClass, method, sig); //创建出来 env->NewObject(personClass, init);
//2. 显式释放主动删除局部引用 // env->DeleteLocalRef(personClass); env->DeleteWeakGlobalRef(personClass); personClass = NULL;
}
- 弱全局引用的有效性检验
可以用 IsSameObject 函数来检验一个弱全局引用是否仍然指向活动的类实例.
- 删除弱全局引用
env->DeleteWeakGlobalRef(personClass);
全局引用显示释放前一直有效,它们可以被其它原生函数及原生线程使用。
8. JNI 线程操作
作为多线程环境的一部分,虚拟机支持运行的原生代码。在开发构件时要记住 JNI 技术的一些约束:
- 只有再原生方法执行期间及正在执行原生方法的线程环境下局部引用是有效的,局部引用不能再多线程间共享,只有全局可以被多个线程共享。
- 被传递给每个原生方法的 JNIEvn 接口指针在与方法调用相关的线程中也是有效的,它不能被其它线程缓存或使用。
同步是多线程程序设计最终的特征。与 Java 同步类似, JNI 的监视器允许原生代码利用 Java 对象同步,虚拟机保证存取监视器的线程能够安全执行,而其他线程等待监视器对象变成可用状态。
jint MonitorEnter(jobject obj)
对 MonitorEnter 函数的调用应该与对 MonitorExit 的调用相匹配,从而避免代码出现死锁。
例子:
public void test4(View view) { for (int i = 0; i < 10; i++) { new Thread(new Runnable() { @Override public void run() { count(); nativeCount(); } }).start(); } }
private void count() { synchronized (this) { count++; Log.d(“Java”, “count=” + count); } }
public native void nativeCount();
native 代码:
extern “C” //支持 C 语言 JNIEXPORT void JNICALL //告诉虚拟机,这是jni函数 native_count(JNIEnv *env, jobject instance) {
jclass cls = env->GetObjectClass(instance); jfieldID fieldID = env->GetFieldID(cls, “count”, “I”);
/if (env->MonitorEnter(instance) != JNI_OK) { LOGE("%s: MonitorEnter() failed", ); }/
/* synchronized block */ int val = env->GetIntField(instance, fieldID); val++; LOGI(“count=%d”, val); env->SetIntField(instance, fieldID, val);
/*if (env->ExceptionOccurred()) { LOGE(“ExceptionOccurred()…”); if (env->MonitorExit(instance) != JNI_OK) { LOGE("%s: MonitorExit() failed", ); }; }
if (env->MonitorExit(instance) != JNI_OK) { LOGE("%s: MonitorExit() failed", ); };*/
}
在 native 中没有进行同步,打印如下:
com.devyk.ndk_sample D/Java: count=1 com.devyk.ndk_sample I/native-lib: count=2 com.devyk.ndk_sample D/Java: count=3 com.devyk.ndk_sample I/native-lib: count=4 com.devyk.ndk_sample D/Java: count=5 com.devyk.ndk_sample I/native-lib: count=6 com.devyk.ndk_sample D/Java: count=7 com.devyk.ndk_sample I/native-lib: count=8 com.devyk.ndk_sample D/Java: count=9 com.devyk.ndk_sample I/native-lib: count=10 com.devyk.ndk_sample D/Java: count=11 com.devyk.ndk_sample I/native-lib: count=12 com.devyk.ndk_sample D/Java: count=13 com.devyk.ndk_sample I/native-lib: count=15 com.devyk.ndk_sample D/Java: count=15 com.devyk.ndk_sample I/native-lib: count=16 com.devyk.ndk_sample D/Java: count=17 com.devyk.ndk_sample I/native-lib: count=18 com.devyk.ndk_sample D/Java: count=19 com.devyk.ndk_sample I/native-lib: count=20
通过多线程对 count 字段操作,可以看见已经无法保证 count 的可见性了。这就需要 JNI 本地实现也要同步。
我们把注释放开:
打印如下:
com.devyk.ndk_sample D/Java: count=1 com.devyk.ndk_sample I/native-lib: count=2 com.devyk.ndk_sample D/Java: count=3 com.devyk.ndk_sample I/native-lib: count=4 com.devyk.ndk_sample D/Java: count=5 com.devyk.ndk_sample I/native-lib: count=6 com.devyk.ndk_sample D/Java: count=7 com.devyk.ndk_sample I/native-lib: count=8 com.devyk.ndk_sample D/Java: count=9 com.devyk.ndk_sample I/native-lib: count=10 com.devyk.ndk_sample D/Java: count=11 com.devyk.ndk_sample I/native-lib: count=12 com.devyk.ndk_sample D/Java: count=13 com.devyk.ndk_sample D/Java: count=14 com.devyk.ndk_sample I/native-lib: count=15 com.devyk.ndk_sample I/native-lib: count=16 com.devyk.ndk_sample D/Java: count=17 com.devyk.ndk_sample I/native-lib: count=18 com.devyk.ndk_sample D/Java: count=19 com.devyk.ndk_sample I/native-lib: count=20
现在保证了count 的可见性了。
为了执行特定任务,这些原生构建可以并行使用原生线程。因为虚拟机不知道原生线程,因此它们不能与 Java 构建直接通信。为了与应用的依然活跃部分交互,原生线程应该先附着在虚拟机上。
JNI 通过 JavaVM 接口指针提供了 AttachCurrentThread 函数以便于让原生代码将原生线程附着到虚拟机上,如下代码所示, JavaVM 接口指针应该尽早被缓存,否则的话它不能被获取。
JavaVM* jvm; … JNIEnv* env = NULL; … jvm->AttachCurrentThread(&env,0);//把 native 线程附着到 JVM 上 … jvm->DetachCurrentThread();//解除 附着 到 JVM 的 native 线程
对 AttachCurrentThread 函数的调用允许应用程序获得对当前线程有效的 JNIEnv 接口指针。将一个已经附着的原生线程再次附着不会有任何副作用。当原生线程完成时,可以用 DetachCurrentThread 函数将原生线程与虚拟机分离。
例子:
最后
都说三年是程序员的一个坎,能否晋升或者提高自己的核心竞争力,这几年就十分关键。
我搜集整理过这几年阿里,以及腾讯,字节跳动,华为,小米等公司的面试题,把面试的要求和技术点梳理成一份大而全的“ Android架构师”面试 PDF(实际上比预期多花了不少精力),包含知识脉络 + 分支细节。
大厂,小厂。Android面试先看你熟不熟悉Java语言
自定义view; 自定义view,Android开发的基本功。
数据结构算法,设计模式。都是这里面的关键基础和重点需要熟练的。
未来的方向,高薪必会。
组件化,热升级,热修复,框架设计
网上学习 Android的资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。希望这份系统化的技术体系对大家有一个方向参考。
我在搭建这些技术框架的时候,还整理了系统的高级进阶教程,会比自己碎片化学习效果强太多,GitHub可见;
当然,想要深入学习并掌握这些能力,并不简单。关于如何学习,做程序员这一行什么工作强度大家都懂,但是不管工作多忙,每周也要雷打不动的抽出 2 小时用来学习。
图片转存中…(img-Kag0qFWD-1644030112631)]
数据结构算法,设计模式。都是这里面的关键基础和重点需要熟练的。
[外链图片转存中…(img-Jeu8RdsC-1644030112632)]
未来的方向,高薪必会。
[外链图片转存中…(img-Qv3fcMCJ-1644030112632)]
组件化,热升级,热修复,框架设计
[外链图片转存中…(img-EdCzKyh4-1644030112633)]
网上学习 Android的资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。希望这份系统化的技术体系对大家有一个方向参考。
我在搭建这些技术框架的时候,还整理了系统的高级进阶教程,会比自己碎片化学习效果强太多,GitHub可见;
当然,想要深入学习并掌握这些能力,并不简单。关于如何学习,做程序员这一行什么工作强度大家都懂,但是不管工作多忙,每周也要雷打不动的抽出 2 小时用来学习。