个人博客:https://blog.N0tExpectErr0r.cn
小专栏:https://xiaozhuanlan.com/N0tExpectErr0r
JNI是Java Native Interface
的简写,它可以使Java与其他语言(如C、C++)进行交互。
它是Java调用Native语言的一种特性,属于Java语言的范畴,与Android无关。
由于上述原因,此时我们就需要让Java与Native语言交互。而由于Java的特点,与Native语言的交互能力很弱。因此在此时,我们就需要用到JNI特性增强Java与Native方法的交互能力。
Java
命令执行 Java
程序,最终实现Java
调用本地代码(借助so库文件)Native是Native Development Kit
的简写,是Android的开发工具包,属于Android,与Java无关系。
它可以快速开发C/C++的动态库,自动将.so和应用一起打包为APK。因此我们可以通过NDK来在Android开发中通过JNI与Native方法交互。
在local.properties中
加入如下一行即可
ndk.dir=
在Gradle的 gradle.properties
中加入如下一行,目的是对旧版本的NDK支持
android.useDeprecatedNdk=true
在build.gradle中的defaultConfig
和android
中加入如下的externalNativeBuild
节点
apply plugin: 'com.android.application'
android {
compileSdkVersion 27
defaultConfig {
applicationId "com.n0texpecterr0r.ndkdemo"
minSdkVersion 19
targetSdkVersion 27
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
externalNativeBuild {
cmake {
cppFlags ""
}
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
externalNativeBuild {
cmake {
path "CMakeLists.txt"
}
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'com.android.support:appcompat-v7:27.1.1'
implementation 'com.android.support.constraint:constraint-layout:1.1.3'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'com.android.support.test:runner:1.0.2'
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
}
我们首先需要在Java代码的类中通过static块来加载我们的Native库。可以通过如下代码,其中loadLibrary的参数是在CMakeList.txt中定义的Native库的名称
static {
System.loadLibrary("native-lib");
}
之后,我们便可以在这个类中声明Native方法
public native String getStringFromJNI();
我们还需要在src中创建一个CMakeList.txt文件,这个文件约束了Native语言源文件的编译规则。比如下面
cmake_minimum_required(VERSION 3.4.1)
add_library(native-lib SHARED src/main/cpp/native-lib.cpp)
find_library(log-lib log)
target_link_libraries(native-lib ${log-lib})
add_library
方法中定义了一个so库,它的名称是native-lib,也就是我们在Java文件中用到的字符串,而后面则跟着这个库对应的Native文件的路径
find_library
则是定义了一个路径变量,经过了这个方法,log-lib这个变量中的值就是Android中log库的路径
target_link_libraries
则是将native-lib这个库和log库连接了起来,这样我们就能在native-lib中使用log库的方法。
在前面的CMake文件中可以看到,我们把文件放在了src/main/cpp/,因此我们创建cpp这个目录,在里面创建C++源文件native-lib.cpp。
然后, 我们便可以开始编写如下的代码:
#include
#include
extern "C"{
JNIEXPORT jstring JNICALL
Java_com_n0texpecterr0r_ndkdemo_MainActivity_getStringFromJNI(
JNIEnv* env,
jobject) {
std::string hello = "IG牛逼";
return env->NewStringUTF(hello.c_str());
}
}
此处我们使用的是C++语言,让我们来看看具体的代码。
首先我们引入了jni需要的jni.h,这个头文件中声明了各个jni需要用到的函数。同时我们引入了C++中的string.h。
然后我们看到extern “C”。为了了解这里为什么使用了extern “C”,我们首先需要知道下面的知识:
在C中,编译时的函数签名仅仅是包含了函数的名称,因此不同参数的函数都是同样的签名。这也就是为什么C不支持重载。
而C++为了支持重载,在编译的时候函数的签名除了包含函数的名称,还携带了函数的参数及返回类型等等。
试想此时我们有个C的函数库要给C++调用,会因为签名的不同而找不到对应的函数。因此,我们需要使用extern "C"
来告诉编译器使用编译C的方式来连接。
接下来我们看看JNIEXPORT和JNICALL关键字,这两个关键字是两个宏定义,他主要的作用就是说明该函数为JNI函数。
而jstring则对应了Java中的String类,JNI中有很多类似jstring的类来对应Java中的类,下面是Java中的类与JNI类型的对照表
我们继续看到函数名Java_com_n0texpecterr0r_ndkdemo_MainActivity_getStringFromJNI
。其实函数名中的_相当于Java中的 . 也就是这个函数名代表了java.com.n0texpecterr0r.ndkdemo.MainActivity.java
中的getStringFromJNI
方法,也就是我们之前定义的native方法。
格式大概如下:
Java_包名_类名_需要调用的方法名
其中,Java必须大写,包名里的.
要改成_
,_
要改成_1
接下来我们看到这个函数的两个参数:
然后可以看到后面我们创建了一个string hello,之后通过env->NewStringUTF(hello.c_str())
方法创建了一个jstring类型的变量并返回。
接着,我们便可以在MainActivty中像调用Java方法一样调用这个native方法
TextView tv = findViewById(R.id.sample_text);
tv.setText(getStringFromJNI());
我们尝试运行,可以看到,我们成功用C++构建了一个字符串并返回给Java调用:
我们在NDK开发中使用CMake的语法来编写简单的代码描述编译的过程,由于这篇文章是讲NDK的,所以关于CMake的语法就不再赘述了。。。如果想要了解CMake语法可以学习这本书《CMake Practice》
在我们JNI层调用一个方法时,需要传递一个参数——方法签名。
为什么要使用方法签名呢?因为在Java中的方法是可以重载的,两个方法可能名称相同而参数不同。为了区分调用的方法,就引入了方法签名的概念。
对于基本类型的参数,每个类型对应了一个不同的字母:
对于类,则使用 L+类名 的方式,其中(.)用(/)代替,最后加上分号
比如 java.lang.String就是 Ljava/lang/String;
对于数组,则在前面加 [ ,然后加类型的签名,几维数组就加几个。
比如 int[]
对应的就是[I
, boolean[][]
对应的则是[[Z
,而java.lang.String[]
就是[Ljava/lang/String;
我们可以通过 javap -s
命令来打印方法的签名。
比如下面的方法
public native String getMessage();
public native String getMessage(String id,long i);
对应的方法签名分别为:
()Ljava/lang/String;
(Ljava/long/String;J)Ljava/lang/String;
可以看到,前面括号中表示的是方法的参数列表,后面表示的则是返回值。
在Java中,我们调用一个方法的思路一般是先要找到对应的类,然后找到类中对应的方法,之后进行方法的调用。
在JNI中,其实也是一致的:
jclass
,对应Java中的类。jmethodID
类型的变量。jmethodID
之后,我们就可以调用JVM环境中的对应函数调用对应方法来进行函数的调用。假设我们在MainActivity定义了如下两个方法:
public static void logMessage(String msg){
Log.d("NDK", msg);
}
public native void callStaticMethod();
然后我们为native方法生成了如下的C++代码:
JNIEXPORT void JNICALL
Java_com_n0texpecterr0r_ndkdemo_MainActivity_callStaticMethod(JNIEnv *env, jobject instance) {
}
我们首先通过env的FindClass
方法找到对应的类:
jclass cls_main = env->FindClass("com/n0texpecterr0r/ndkdemo/MainActivity");
可以看到,FindClass需要的参数是这个类的全路径。获取到的jclass
我们最好判下空
接下来我们需要拿到我们要调用的方法对应的jmethodID
。
jmethodID mth_static_method = env->GetMethodID(cls_main,"logMessage","(Ljava/lang/String;)V");
由于logMessage
方法是static的,因此我们调用了env的GetStaticMethodID
方法,需要传入的参数分别是 jclass 方法名 以及方法对应的签名。
仍然判下空,之后我们可以通过env->CallXXXXXMethod方法来调用对应方法。
由于我们的logMessage方法是static,返回值为void的方法,因此我们使用CallStaticVoidMethod
方法即可
jstring str = env->NewStringUTF("这是从JNI调用的Log");
env->CallStaticVoidMethod(cls_main,mth_static_method,str);
调用完成后,我们可以通过env的DeleteLocalRef
方法来释放刚刚声明的变量。
env->DeleteLocalRef(cls_main);
env->DeleteLocalRef(str);
这样,在MainActivity中调用callStaticMethod方法后,便会打印这样一条log
D/NDK: 这是从JNI调用的Log
完整代码如下:
JNIEXPORT void JNICALL
Java_com_n0texpecterr0r_ndkdemo_MainActivity_callStaticMethod(JNIEnv *env, jobject instance) {
// 找到对应的类
jclass cls_main = env->FindClass("com/n0texpecterr0r/ndkdemo/MainActivity");
if(cls_main == NULL) return;
// 获取methodId
jmethodID mth_static_method = env->GetStaticMethodID(cls_main,"logMessage","(Ljava/lang/String;)V");
if(mth_static_method == NULL) return;
// 构建String变量
jstring str = env->NewStringUTF("这是从JNI调用的Log");
// 调用static方法
env->CallStaticVoidMethod(cls_main,mth_static_method,str);
// 释放内存
env->DeleteLocalRef(cls_main);
env->DeleteLocalRef(str);
}
有了之前的jclass之后,我们还可以通过获取jfieldID
,之后调用env的SetStaticObjectField
方法来修改static变量。与前面类似。
假设我们有了下面这样一个类
public class Adder {
private int arg1;
private int arg2;
public Adder(int arg1, int arg2) {
this.arg1 = arg1;
this.arg2 = arg2;
}
public int doAdd(){
return arg1+arg2;
}
}
调用实例方法相比调用静态方法会复杂一些
jclass
JNIEXPORT jint JNICALL
Java_com_n0texpecterr0r_ndkdemo_MainActivity_addNative(JNIEnv *env, jobject instance, jint arg1, jint arg2) {
// 找到对应类
jclass cls_adder = env->FindClass("com/n0texpecterr0r/ndkdemo/Adder");
// 获取构造方法
jmethodID mth_constructor = env->GetMethodID(cls_adder,"" ,"(II)V");
// 调用构造方法构建jobject
jobject adder = env->NewObject(cls_adder, mth_constructor,arg1,arg2);
// 获取add方法
jmethodID mth_add = env->GetMethodID(cls_adder,"doAdd","()I");
// 调用add方法获取返回值
jint result = env->CallIntMethod(adder,mth_add);
// 回收资源
env->DeleteLocalRef(cls_adder);
env->DeleteLocalRef(adder);
// 返回结果
return result;
}
可以看到,在获取构造函数的id时,指定的方法名为
。
当我们在Java中使用new创建了一个对象后,可以随意使用这个对象,不需要关注它什么时候被回收,因为这个对象的回收托管给了GC。但是当使用JNI传递给Native层的对象时该如何处理呢?
将对象传递给Native语言后,Native层会持有Java对象,如果我们不妥善处理会导致内存泄漏。 因此在Native层使用Java对象时,需要释放这个引用。
下面可以看看对于数组引用的处理
JNIEXPORT void JNICALL
Java_com_n0texpecterr0r_ndkdemo_MainActivity_useCppQSort(JNIEnv *env, jobject instance, jintArray jarray) {
jint* arrayElemts = env->GetIntArrayElements(jarray, NULL);
jsize arraySize = env->GetArrayLength(jarray);
... // 排序算法
env->ReleaseIntArrayElements(jarray, arrayElemts, JNI_COMMIT);
}
将array
对象传递给C++, C++中的变量将持有array
这个引用,因为数组和对象在java中都是引用,都会在堆内存中开辟一块空间 , 但我们使用完对象之后需要将引用释放掉,不然会导致内存泄漏 。
释放数组元素时最后一个参数可以传入两个值:
其实,只要是Java对象,在Native层中都需要释放(包括在Native层创建的对象引用)。
在Java中引用有强弱之分,在C/C++中也不例外,C/C++中也有一套全局引用
,局部引用
,弱全局引用
等等 。
局部引用指的是C/C++使用到或自行创建的Java对象,需要告知虚拟机在合适的时候回收对象。(通过DeleteLocalRef手动释放对象)
JNIEXPORT void JNICALL
Java_com_n0texpecterr0r_ndkdemo_MainActivity_localRef(JNIEnv *env, jobject instance) {
// 找到类
jclass dateClass = env->FindClass("java/util/Date");
// 得到构造方法ID
jmethodID dateConstructorId = env->GetMethodID(dateClass, "" , "()V");
// 创建Date对象
jobject dateObject = env->NewObject(dateClass, dateConstructorId);
// 创建一个局部引用
jobject dateLocalRef = env->NewLocalRef(dateObject);
...
// 不再使用对象,通知GC回收对象
env->DeleteLocalRef(dateLocalRef);
// 因为dateObject也是局部对象,可以直接回收dateObject对象
// env->DeleteLocalRef(dateObject);
}
全局引用的特点是共享(可以跨多个线程),手动控制内存使用
jstring globalStr;
/*创建全局引用*/
JNIEXPORT void JNICALL
Java_com_n0texpecterr0r_ndkdemo_MainActivity_createGlobalRef(JNIEnv *env, jobject jobj) {
jstring jStr = env->NewStringUTF("N0tExpectErr0r");
// 创建一个全局引用
globalStr = env->NewGlobalRef(jStr);
}
/*使用全局引用*/
JNIEXPORT jstring JNICALL
Java_com_n0texpecterr0r_ndkdemo_MainActivity_useGlobalRef(JNIEnv *env, jobject jobj) {
return globalStr;
}
/*释放全局引用*/
JNIEXPORT void JNICALL
Java_com_n0texpecterr0r_ndkdemo_MainActivity_deleteGlobalRef(JNIEnv *env, jobject jobj) {
// 释放全局引用
env->DeleteGlobalRef(globalStr);
}
而弱全局引用的特点是节省内存,在内存不足时可以释放所引用的对象,可以用来引用一个不常用的对象,如果为NULL,临时创建。
创建:NewWeakGlobalRef
销毁:DeleteWeakGlobalRef
下面我们先展示一个出现错误的例子:
首先,我们在MainActivity中做出了如下改变:
private String key = "123";
public native void testException();
然后编写如下的Native方法:
JNIEXPORT void JNICALL
Java_com_n0texpecterr0r_ndkdemo_MainActivity_testException(JNIEnv *env, jobject instance) {
jclass cls_this = env->GetObjectClass(instance);
// 此处属性名称故意写错了
jfieldID fid = env->GetFieldID(cls_this, "key1", "Ljava/lang/String;");
// 此处会抛出异常,Java层可以捕获Error或者Throwable。
// 但是下面的代码还是可以执行
__android_log_print(ANDROID_LOG_DEBUG, "NDKException", "flag1");
// 但是到了下面,程序crash了
jstring key = static_cast<jstring>(env->GetObjectField(instance, fid));
char* str = const_cast<char *>(env->GetStringUTFChars(key, NULL));
__android_log_print(ANDROID_LOG_DEBUG, "NDKException", "flag2");
}
运行程序,发现程序crash了。我们查看logcat:
/com.n0texpecterr0r.ndkdemo D/NDKException: flag1
可以看到虽然抛出异常但是下面的代码仍然可以执行。可是当我们尝试使用fid时,就导致了程序crash。
我们看看logcat中的错误信息:
JNI DETECTED ERROR IN APPLICATION: JNI GetObjectField called with pending exception java.lang.NoSuchFieldError: no "Ljava/lang/String;" field "key1" in class "Lcom/n0texpecterr0r/ndkdemo/MainActivity;" or its superclasses
at void com.n0texpecterr0r.ndkdemo.MainActivity.testException() (MainActivity.java:-2)
at void com.n0texpecterr0r.ndkdemo.MainActivity.onCreate(android.os.Bundle) (MainActivity.java:20)
...
可以看到,这里抛出了NoSuchFieldError
异常,信息为" no “Ljava/lang/String;” field “key1” in class “Lcom/n0texpecterr0r/ndkdemo/MainActivity;” or its superclasses",提示我们MainActivity及其父类中没有key1这个filed
Native层在出现错误时会抛出Error类型的异常,可以通过Throwable或者Error来捕获,捕获异常后Java代码可以继续执行。
为了确保Java、Native代码可以正常执行下去,我们需要:
比如下面的方式:
JNIEXPORT void JNICALL
Java_com_n0texpecterr0r_ndkdemo_MainActivity_testException(JNIEnv *env, jobject instance) {
jclass cls_this = env->GetObjectClass(instance);
// 此处属性名称故意写错了
jfieldID fid = env->GetFieldID(cls_this, "key1", "Ljava/lang/String;");
jthrowable err = env->ExceptionOccurred();
if (err != NULL){
//手动清空异常信息,保证Java代码能够继续执行
env->ExceptionClear();
//提供补救措施,例如获取另外一个属性
fid = env->GetFieldID(cls_this, "key", "Ljava/lang/String;");
}
jstring key = static_cast<jstring>(env->GetObjectField(instance, fid));
char* str = const_cast<char *>(env->GetStringUTFChars(key, NULL));
}
运行程序可以发现,我们成功地补救了这个问题,Java代码成功执行了。
我们其实也可以通过ThrowNew
方法来手动抛出异常。如下:
if (strcmp(str,"efg") != 0){
// 获取异常Class
jclass cls_err = env->FindClass("java/lang/IllegalArgumentException");
// 抛出对应异常
env->ThrowNew(cls_err, "key value is invalid!");
}
可以看到,我们成功抛出了一个IllegalArgumentException
。之后就可以在Java中捕获这个异常:
try {
testException();
}catch (IllegalArgumentException e){
Log.e("Exception", e.getMessage());
}
查看Logcat可以看到我们所捕获的异常:
E/Exception: key value is invalid!
Bitmap是Android开发中非常常用的图片操作类,它包含了图像的宽、高、格式、以及每个像素点的信息。NDK提供了bitmap.h来给我们使用Android中的Bitmap类。
NDK为我们提供了Bitmap的头文件,也就是
AndroidBitmapInfo是一个包含了Bitmap常用的信息的类,它的定义如下:
// 可以通过AndroidBitmap_getInfo()获取
typedef struct {
// Bitmap的像素宽度信息
uint32_t width;
// Bitmap的像素高度信息
uint32_t height;
// 每一行包含的像素数量
uint32_t stride;
// Bitmap像素的格式
int32_t format;
// 一个弃用的flag,恒为0
uint32_t flags;
} AndroidBitmapInfo;
AndroidBitmapFormat是一个枚举,表示了Bitmap的图片格式,与Java端一一对应。它的定义如下:
enum AndroidBitmapFormat {
ANDROID_BITMAP_FORMAT_NONE = 0,
ANDROID_BITMAP_FORMAT_RGBA_8888 = 1,
ANDROID_BITMAP_FORMAT_RGB_565 = 4,
ANDROID_BITMAP_FORMAT_RGBA_4444 = 7,
ANDROID_BITMAP_FORMAT_A_8 = 8,
};
这个地方定义了对Bitmap进行操作的结果,分别对应成功,错误参数,JNI异常,内存分配错误。可能大家会发现最后还有一个异常,其实是因为Google的程序员在写代码的时候把Result给写错了导致的。
#define ANDROID_BITMAP_RESULT_SUCCESS 0
#define ANDROID_BITMAP_RESULT_BAD_PARAMETER -1
#define ANDROID_BITMAP_RESULT_JNI_EXCEPTION -2
#define ANDROID_BITMAP_RESULT_ALLOCATION_FAILED -3
#define ANDROID_BITMAP_RESUT_SUCCESS ANDROID_BITMAP_RESULT_SUCCESS
这里声明了对Bitmap操作的函数,它们分别是
AndroidBitmap_unlockPixels
函数来解除对像素的锁定。int AndroidBitmap_getInfo(JNIEnv* env, jobject jbitmap, AndroidBitmapInfo* info);
int AndroidBitmap_lockPixels(JNIEnv* env, jobject jbitmap, void** addrPtr);
int AndroidBitmap_unlockPixels(JNIEnv* env, jobject jbitmap);
我们需要注意的是,如果我们要使用Bitmap,需要链接jnigraphics
库,否则可能出现下面的错误
E:\test\NdkBitmap\app\src\main\cpp/native-lib.cpp:52: undefined reference to
AndroidBitmap_getInfo
E:\test\NdkBitmap\app\src\main\cpp/native-lib.cpp:53: undefined reference toAndroidBitmap_getInfo
E:\test\NdkBitmap\app\src\main\cpp/native-lib.cpp:59: undefined reference toAndroidBitmap_lockPixels
E:\test\NdkBitmap\app\src\main\cpp/native-lib.cpp:64: undefined reference toAndroidBitmap_lockPixels
E:\test\NdkBitmap\app\src\main\cpp/native-lib.cpp:86: undefined reference toAndroidBitmap_unlockPixels
E:\test\NdkBitmap\app\src\main\cpp/native-lib.cpp:87: undefined reference toAndroidBitmap_unlockPixels
因此我们需要在cmake中添加**-ljnigraphics**
target_link_libraries(native-lib -ljnigraphics ${log-lib})
其实,我们之前所采用的注册Java中的native方法的方式叫做静态注册,也就是通过Java_<包名>_<类名>_<方法名>
这样的函数名来使得该native方法可以被找到。这样的函数名非常长,不便于管理,如果我们使用动态注册,就可以不受这种命名的限制。
同时我们需要清楚,Java类是通过VM来调用Native方法,调用时需要通过VM在so库中寻找Native函数,如果该函数需频繁调用,会有大量的时间消耗。因此我们可以通过动态注册,在JNI_Onload函数中把native函数注册到VM中,减少寻找花费的时间。
在介绍动态注册之前
我们在调用System.loadLibrary()
方法时,会自动在该库中查找并调用一个叫JNI_Onload的函数。它的函数原型如下:
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) {
...
}
由于这个函数是在JNI被加载时调用,所以它有如下的几个用途:
Android系统加载JNI依赖库的方式有下面的两种:
dvmResolveNativeMethod
进行动态解析。我们可以通过在JNI_Onload中调用registerNativeMethods
方法来进行Native方法的动态注册。
比如假设我们有如下的一个静态注册的Native文件
#include <string>
#include <jni.h>
extern "C"
JNIEXPORT jstring JNICALL Java_com_example_hellojni_HelloJni_stringFromJNI(JNIEnv* env, jobject thiz )
{
return env->NewStringUTF("Hello from JNI !");
}
我们可以将其改写为如下的方式,即可完成方法的动态注册
extern "C"
jstring native_hello(JNIEnv* env, jobject thiz )
{
return env->NewStringUTF("Hello from JNI !");
}
JNINativeMethod gMethods[] = {
{"stringFromJNI", "()Ljava/lang/String;", (void*)native_hello},//绑定
};
int registerNativeMethods(JNIEnv* env, const char* className, JNINativeMethod* gMethods, int numMethods) {
jclass clazz;
clazz = env->FindClass(className);
if (clazz == NULL) {
return JNI_FALSE;
}
if (env->RegisterNatives(clazz, gMethods, numMethods) < 0) {
return JNI_FALSE;
}
return JNI_TRUE;
}
/*
* 为所有类注册本地方法
*/
int registerNatives(JNIEnv* env) {
const char* kClassName = "com/example/hellojni/HelloJni";//指定要注册的类
return registerNativeMethods(env, kClassName, gMethods,
sizeof(gMethods) / sizeof(gMethods[0]));
}
/*
* System.loadLibrary("lib")时调用
* 如果成功返回JNI版本, 失败返回-1
*/
extern "C"
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) {
JNIEnv* env = NULL;
jint result = -1;
if (vm->GetEnv((void**) &env, JNI_VERSION_1_4) != JNI_OK) {
return -1;
}
assert(env != NULL);
if (!registerNatives(env)) {//注册
return -1;
}
//成功
result = JNI_VERSION_1_4;
return result;
}
可以看到,在上述方法中,我们创建了一个方法对应表,将Java中的Native方法与Native层中的函数一一对应。
其中,JNINativeMethod
是一个JNI中定义的结构体
typedef struct {
const char* name;
const char* signature;
void* fnPtr;
} JNINativeMethod;
其中,name代表了Java中的函数的名字,而signature代表了这个函数的函数签名,fnPtr则是一个函数指针,指向一个C函数,也就是native方法对应的函数。
我们都知道,在项目中使用NDK时,它会生成so文件供我们的Java代码调用。我们使用某些Java语言编写的库时,它可能内部也用到了.so文件。由此可见so文件对我们Android开发者十分重要,那么它到底是什么呢?
目前Android系统支持了七种不同的CPU架构:ARMv5、ARMv7、x86、MIPS、ARMv8、MIPS64、x86_64。而这些CPU架构每个都关联了一个ABI。
ABI,即应用程序二进制接口(Application Binary Interface)。它里面定义了二进制文件(如.so文件)如何在相应平台上运行。从使用的指令集、内存对齐到可用系统函数库都有涉及。每个CPU架构对应了一个API:armeabi,armeabi-v7a,x86,mips,arm64-v8a,mips64,x86_64
so是shared object的缩写,也就是共享对象的意思。它是一种二进制文件,里面存放的是机器可以直接运行的二进制代码。正是因为如此,所以反编译so库的难度会比反编译普通Java代码的难度更大。so主要应用在Unix和Linux操作系统中,大到操作系统,小到一个专用的软件,都离不开so。其实Windows中有和so类似的东西,也就是我们常见的dll(动态链接库),其实它们是相同的事物,只是名字不同而已。
虽然很多设备都支持多于一种的ABI(比如ARM64和x86设备也可以同时运行armeabi-v7a和armeabi的二进制包),但这种往往是要经过一种模拟层,(如x86设备模拟arm的模拟层),导致性能有所损耗。因此我们最好是针对特定的平台提供特定的so文件,从而避开模拟层,得到更好的性能。