之前很多次看到网易云课程广告,里面有个熟悉的标题是这样的
性能优化之NDK高效加载GIF--NDK开发实战 三个小节如下:
- 安卓NDK开发快速入门
- giflib在安卓开发中的使用
- NDK加载GIF较传统加载方式的优势
这个广告标题已经看到过好多次了,是一个进阶课程,那么肯定是收费的,我想不花钱就能学会NDK高效加载GIF,行吗?
首先第一点,安卓NDK开发快速入门并不是很难,大多数Android开发都是属于应用层开发,很少涉及NDK,只要找一篇入门文章学习即可。
第二点,giflib在安卓开发中的使用,看到 giflib,猜想应该是一个开源库来的,在安卓中使用这个开源库,应该是跟FFmpeg类似,需要会一点NDK,用c++代码调用这个开源库api,然后通过JNI,提供给Android端调用,如果单纯是API使用,对于有JNI、NDK基础的同学来说,这一节其实难度不是很大。
最后一点,NDK加载GIF较传统加载方式的优势,是对标题的“高效加载”进行补充了,猜想这一节可能会讲giflib 加载GIF的原理,为什么高效? 应该还会对比其它的不使用NDK加载的方式,例如Glide,为什么就慢?这个涉及到加载gif原理了,能掌握的话,面试是不亏的。
会NDK,会使用NDK高效加载gif,知道其中的原理,这三点都会,吹吹牛逼应该不成问题。
有了这个思路之后,我觉得我应该可以写出这个所谓的网易云的进阶课程~
当然,写好这一篇文章,需要考虑初中级的同学可能对JNI、NDK不熟悉,cmake语法可能需要提及,流程必须清晰,必须提供可以运行的demo,对原理必须解释清楚等等~
根据之前文章的风格,这篇文章不会单单介绍NKD高效加载gif,同时会把涉及到的相关知识点都总结总结,例如:so加载原理、native方法调用原理~
直接进入正文吧~
一、JNI和NDK基础
这一块基本没太大难度的,只是大部分Android开发都是做应用层业务开发,很少涉及到动手写c++代码,所以觉得JNI、NDK是很高级的技术,曾经的我也是这么认为的。
是不是要会C++? 会肯定最好,不会的话,用到的时候学也可以。
1.1 JNI,本文只需要知道这些
Java调用c++的方法没啥好说的,写个native方法,然后通过快捷键在cpp中生成对应方法,傻瓜式操作就行。
而c++调用Java方法要了解一下,例如本文涉及到:Java层bitmap交给native层处理完,通过JNI回调Java层的Runable的run方法
// runnable 是Java传过来的参数,类型是 jobject,
//第一步获取Class对象
jclass runClass = env->GetObjectClass(runnable);
//第二部获取run方法的方法id,参数1是对象,参数2是方法名,参数3是方法签名,这里是void
jmethodID runMethod = env->GetMethodID(runClass, "run", "()V");
//通过 JNIEnv 的 CallVoidMethod函数,调用Java层的方法
env->CallVoidMethod(runnable, runMethod);
重点:
类型对应:Java 的Class 对象对应JNI 的jclass对象;
方法签名:为了区别重载方法,可以理解为就是方法参数类型;
JNIEnv: 每个JNI方法的第一个参数,提供了操作Java层的一些方法,例如调用某个方法。
如果对JNI不熟悉的话,当然最好可以找一篇入门文章看一下,例如这一篇:
Android JNI(一)——NDK与JNI基础
1.2 对于c++ ,读懂本文需要知道这些
- Java 是通过 . 来调用一个方法的,c++ 是通过 -> 来调用一个函数(方法)的;
- c++ 有指针概念,指针箭头指向的是一个内存地址,很多方法参数是指针类型,简单理解就是址传递;
- 指针如果指向的是一个数组,那么指针代表数组首地址,访问该指针就是访问数组的首地址。
二、giflib在安卓开发中的使用
giflib 是啥呢?
通过搜索引擎,发现 giflib是android源码中的一个用C语言写的加载GIF的库
把.c 和 .h 结尾的文件下载下来,放到giflib文件夹中备用
先把giflib集成到Android Studio项目中先~
三、giflib 使用
上一步 giflib 已经是下载下来了
3.1 新建一个自带JNI功能的项目
为什么要新建,主要是考虑到部分同学对cmake 语法不清楚,所以通过新建项目来熟悉它,
Android Studio 新建 Native C++ 项目
项目名就叫 GifLoaderTest
然后next,finish,
等待同步和编译完成(可能会提示安装cmake,按提示安装即可),这个是可以运行成功的带有JNI基本配置的项目。
3.2 引入giflib
看下 CMakeLists.txt 的需要配置什么
cmake_minimum_required(VERSION 3.4.1)
# 1、指定源文件目录,把 /cpp/giflib 目录的下的所有文件赋值给 GIF_LIB
file(GLOB_RECURSE GIF_LIB ${CMAKE_SOURCE_DIR}/giflib/*.*)
# 同1,cpp 目录下的文件文件放到 MAIN_SOURCE 里,不用一个一个添加
file(GLOB_RECURSE MAIN_SOURCE ${CMAKE_SOURCE_DIR}/*.*)
add_library(
# 要编译的库的名称,可以改
native-lib
# SHARED 表示要编译动态库
SHARED
${GIF_LIB} # 2、把giflib源文件添加到 native-lib 这个库中去
${MAIN_SOURCE} # 同2,我们写的cpp源文件
)
target_link_libraries(
native-lib
# 3、给native-lib 添加一些依赖
log
jnigraphics
android)
有些同学没接触过NDK开发,或者接触过,但是停留在基于Android.mk的构建方式,对cmake语法不太熟悉,没关系,本文只需3个步骤把giflib集成进去:
- 将 giflib 复制到 cpp目录下
- CMakeLists.txt 中将 giflib 目录的文件指定为源文件,对应上面的注释1和注释2
- 添加其它依赖,对应注释3
对于注释3,log、jnigraphics、android,这几个依赖在哪里呢?答案是在NDK目录下,例如我的mac是在这个目录
/Users/{用户名}/Library/Android/sdk/ndk-bundle/platforms/android-27/arch-arm/usr/lib
NDK工具包提供了一些依赖库给我们使用,log 是在控制台打印日志,jnigraphics 是图像操作相关。
3.3 定义 Java层 Gif管理类
定义一个gif管理类,叫 GifHandler
定义几个native方法
// 1.加载gif,返回 giflib中的 GifFileType对象地址,之后的操作都传这个GifFileType的地址过去
public static native long loadGif(String gifPath);
// 2. 获取gif宽高
public static native int getWidth(long nativeGifFile);
public static native int getHeight(long nativeGifFile);
// 3.更新bitmap,更新成功就回调runnable
public static native int updateBitamap(long nativeGifFile, Bitmap bitmap, Runnable runnable);
public static native void destroy(long nativeGifFile);
3.4 native层生成对应方法
鼠标放在native方法上面,按下快捷键生成native方法,mac 快捷键是 option + enter,windows应该是alt+enter
会自动在 native-lib.cpp 中生成native方法对应的JNI方法,就是这么简单
extern "C"
JNIEXPORT void JNICALL
Java_com_lanshifu_gifloadertest_GifHandler_destroy(JNIEnv *env, jclass clazz,
jlong native_gif_file) {
// TODO: implement destroy()
}
各个方法按顺序讲解:
3.4.1 loadGif
extern "C"
JNIEXPORT jlong JNICALL
Java_com_lanshifu_gifloadertest_GifHandler_loadGif(JNIEnv *env, jclass clazz, jstring path) {
const char *filePath = env->GetStringUTFChars(path, 0);
int err;
// 1.调用源码api里方法,打开gif,返回GifFileType实体
GifFileType *GifFile = DGifOpenFileName(filePath, &err);
LOGD("filePath = %s", filePath);
LOGD("loadGif,SWidth = %d", GifFile->SWidth);
LOGD("loadGif,SHeight = %d", GifFile->SHeight);
return (long long) GifFile;
}
打开一张gif,调用 DGifOpenFileName
函数,这个函数是giflib 这个库里边的,会返回一个 GifFileType
类型的指针。GifFileType 结构体如下
typedef struct GifFileType {
GifWord SWidth, SHeight; /* Size of virtual canvas */
GifWord SColorResolution; /* How many colors can we generate? */
GifWord SBackGroundColor; /* Background color for virtual canvas */
GifByteType AspectByte; /* Used to compute pixel aspect ratio */
ColorMapObject *SColorMap; /* Global colormap, NULL if nonexistent. */
int ImageCount; /* Number of current image (both APIs) */
GifImageDesc Image; /* Current image (low-level API) */
SavedImage *SavedImages; /* Image sequence (high-level API) */
int ExtensionBlockCount; /* Count extensions past last image */
ExtensionBlock *ExtensionBlocks; /* Extensions past last image */
int Error; /* Last error condition reported */
void *UserData; /* hook to attach user data (TVT) */
void *Private; /* Don't mess with this! */
} GifFileType;
通过第一步我们我们可以获取到gif的宽高 SWidth, SHeight,打印出来是
11-17 16:42:40.681 D/GIF_JNI: filePath = /storage/emulated/0/test.gif
11-17 16:42:40.682 D/GIF_JNI: loadGif,SWidth = 224
11-17 16:42:40.682 D/GIF_JNI: loadGif,SHeight = 400
通过调用 DGifOpenFileName
打开了gif文件,用一个变量GifFile保存起来,把地址返回给Java层,之后Java层可以通过传这个地址过来获取宽高,解析帧数据啥的
3.4.2 getWidth 和 getHeight
extern "C"
JNIEXPORT jint JNICALL
Java_com_lanshifu_gifloadertest_GifHandler_getWidth(JNIEnv *env, jclass clazz, jlong nativeGifFile) {
// 获取gif 宽
GifFileType *GifFile = (GifFileType *) nativeGifFile;
return GifFile->SWidth;
}
extern "C"
JNIEXPORT jint JNICALL
Java_com_lanshifu_gifloadertest_GifHandler_getHeight(JNIEnv *env, jclass clazz, jlong nativeGifFile) {
// 获取gif 高
GifFileType *GifFile = (GifFileType *) nativeGifFile;
return GifFile->SHeight;
}
2.5.1 loadGif 的时候,将native层的GifFile地址返回到Java层,那么获取宽高只要把这个地址传过来就行,强转,然后调用宽高字段SWidth/SHeight即可。
3.4.3 updateBitmap
这个方法是用来解析gif中每一帧图片,并且将图片数据更新到Bitmap上,回调到Java层。
分几个部分讲解~
屏幕缓冲区
我们需要先创建一个屏幕缓冲区,所有的图像要绘制到屏幕缓冲区上面。 首先需要创建ScreenBuffer并且分配内存,设置背景颜色为gif背景颜色
//GifRowType
GifRowType *ScreenBuffer;
//首先我们需要给屏幕分配内存:
ScreenBuffer = (GifRowType *) malloc(gifHeight * sizeof(GifRowType));
if (ScreenBuffer == NULL) {
LOGE("ScreenBuffer malloc error");
goto end;
}
//一行像素占用的内存大小
size_t rowSize;
rowSize = gifWidgh * sizeof(GifPixelType);
ScreenBuffer[0] = (GifRowType) malloc(rowSize);
/***** 给 ScreenBuffer 设置背景颜色为gif背景*/
//设置第一行背景颜色
for (int i = 0; i < gifWidgh; i++) {
ScreenBuffer[0][i] = (GifPixelType) GifFile->SBackGroundColor;
}
//其它行拷贝第一行,每一行都要申请内存
for (int i = 1; i < gifHeight; i++) {
if ((ScreenBuffer[i] = (GifRowType) malloc(rowSize)) == NULL) {
LOGE("Failed to allocate memory required, aborted.");
goto end;
}
memcpy(ScreenBuffer[i], ScreenBuffer[0], rowSize);
}
ScreenBuffer 已经初始化好了,带有gif背景颜色,接下来的操作就是将gif的每一帧数据画到ScreenBuffer上面。
解码gif数据
gif中的数据是有顺序的块,每一个块代表的是当前帧的图片数据,或者图片数据的描述,例如延时多少毫秒这些额外数据。DGifGetRecordType
函数用来获取下一块数据的类型,通过这个函数,就能把gif中每一帧图片数据取出来
/***** 循环解析gif数据,并根据不同的类型进行不同的处理*/
do {
//DGifGetRecordType函数用来获取下一块数据的类型
if (DGifGetRecordType(GifFile, &RecordType) == GIF_ERROR) {
LOGE("DGifGetRecordType Error = %d", GifFile->Error);
goto end;
}
switch (RecordType) {
//1、如果是图像数据块,需要绘制到 ScreenBuffer 中
case IMAGE_DESC_RECORD_TYPE :
...
break;
//2、额外信息块,获取帧之间间隔、透明颜色下标
case EXTENSION_RECORD_TYPE:
...
break;
} while (RecordType != TERMINATE_RECORD_TYPE);
主要是处理两种类型的块
注释1,图像数据块,这个图像数据需要绘制到前面提到的屏幕buffer上面,相应的代码如下:
case IMAGE_DESC_RECORD_TYPE :
// 1、DGifGetImageDesc 函数是 获取gif的详细信息,例如 是否是隔行扫描,每个像素点的颜色信息等等
if (DGifGetImageDesc(GifFile) == GIF_ERROR) {
LOGE("DGifGetImageDesc Error = %d", GifFile->Error);
return ERROR_CODE;
}
Row = GifFile->Image.Top; /* Image Position relative to Screen. */
Col = GifFile->Image.Left;
Width = GifFile->Image.Width;
Height = GifFile->Image.Height;
...
//隔行扫描
if (GifFile->Image.Interlace) {
//隔行扫描,要执行扫描4次才完整绘制完
for (int i = 0; i < 4; i++)
for (int j = Row + InterlacedOffset[i];
j < Row + Height; j += InterlacedJumps[i]) {
// 2、从GifFile 中获取一行数据,放到ScreenBuffer 中去
if (DGifGetLine(GifFile, &ScreenBuffer[j][Col], Width) == GIF_ERROR) {
LOGE("DGifGetLine Error = %d", GifFile->Error);
goto end;
}
}
} else {
//没有隔行扫描,顺序一行一行来
for (int i = 0; i < Height; i++) {
if (DGifGetLine(GifFile, &ScreenBuffer[Row++][Col], Width) == GIF_ERROR) {
LOGE("DGifGetLine Error = %d", GifFile->Error);
goto end;
}
}
}
//扫描完成,ScreenBuffer 中每个像素点是什么颜色就确定好了,就差绘制到Bitmap上了
ColorMap = (GifFile->Image.ColorMap
? GifFile->Image.ColorMap
: GifFile->SColorMap);
...
//delayTime 表示帧间隔时间,是从另一个数据块计算出来的,睡眠一下再画下一帧
threadSleep.msleep(delayTime * 10);
delayTime = 0;
//3、将数据绘制到Bitmap上
drawBitmap(env, bitmap, GifFile, ScreenBuffer, bitmapWidth, ColorMap,
GifFile->ImageCount - 1, pSavedImage,transparentColorIndex);
//4、Bitmap绘制好了,回调runnable的run方法,Java层刷新ImageView即可看到新的一帧图片
env->CallVoidMethod(runnable, runMethod);
break;
解码gif数据主要有4个步骤:
1.调用DGifGetImageDesc
函数获取gif的详细信息,例如是否是隔行扫描GifFile->Image.Interlace
,颜色表 Image.ColorMap
等等。
2.不管是不是隔行扫描,都会调用 DGifGetLine
函数将GifFile中一行数据填充到ScreenBuffer中,这里的隔行扫描需要遍历4次才能扫描完一张图片,扫描完成,ScreenBuffer 中每个像素点是什么颜色就确定好了,就差绘制到Bitmap上了。
关于隔行扫描和逐行扫描,举个栗子,加载一个网络图片,网络比较差,先看到图片上半部分加载出来,下半部分还是黑的,这就是从上到下逐行扫描;如果是整个图片出来了,但是很模糊,慢慢变清晰,这就属于隔行扫描。
3.drawBitmap,Java层传了一个Bitmap过来,但是是没有任何图片数据的,这里要将ScreenBuffer中的像素填充到Bitmap中去
void drawBitmap(JNIEnv *env, jobject bitmap, const GifFileType *GifFile, GifRowType *ScreenBuffer,
int bitmapWidth, ColorMapObject *ColorMap, int imageIndex,
SavedImage *pSavedImage,int transparentColorIndex) {
//1、AndroidBitmap_lockPixels 锁定Bitmap像素以确保像素的内存不会被移动
void *pixels;
AndroidBitmap_lockPixels(env, bitmap, &pixels);
//拿到Bitmap像素地址
uint32_t *sPixels = (uint32_t *) pixels;
int dataOffset = sizeof(int32_t) * DATA_OFFSET;
int dH = bitmapWidth * GifFile->Image.Top;
GifByteType colorIndex;
//从左到右,一层一层设置Bitmap像素
for (int h = GifFile->Image.Top; h < GifFile->Image.Height; h++) {
for (int w = GifFile->Image.Left; w < GifFile->Image.Width; w++) {
//2、从 ScreenBuffer 中获取像素点下标,给一个像素点设置ARGB
colorIndex = (GifByteType) ScreenBuffer[h][w];
//sPixels[dH + w] Bitmap像素地址,通过遍历给每个像素点设置argb,Bitmap就有颜色了
setColorARGB(&sPixels[dH + w],
imageIndex,
ColorMap,
colorIndex,
transparentColorIndex);
//将颜色下标保存起来,循环播放的时候需要知道这个下标
pSavedImage->RasterBits[dataOffset++] = colorIndex;
}
//遍历下一层
dH += bitmapWidth;
}
LOGD("dH 结束 = %d ", dH);
//对应解锁像素
AndroidBitmap_unlockPixels(env, bitmap);
}
native 层操作Bitmap像素之前,要先调用 AndroidBitmap_lockPixels
函数锁住Bitmap像素,并且拿到Bitmap像素内存地址,遍历之前已经填充好数据的ScreenBuffer,给Bitmap每个像素设置正确的argb即可,对应下面的 setColorARGB
方法,最后再调用 AndroidBitmap_unlockPixels
解锁Bitmap像素,到此,Bitmap就已经加载了图片数据。
setColorARGB 方法很简单,就是给像素赋值,不过要注意透明的像素点,
uint32_t gifColorToColorARGB(const GifColorType &color) {
return (uint32_t) (MAKE_COLOR_ABGR(color.Red, color.Green, color.Blue));
}
void setColorARGB(uint32_t *sPixels, int imageIndex, ColorMapObject *colorMap,
GifByteType colorIndex,int transparentColorIndex) {
if (imageIndex > 0 && colorIndex == transparentColorIndex) {
return;
}
if (colorIndex != transparentColorIndex || transparentColorIndex == NO_TRANSPARENT_COLOR) {
*sPixels = gifColorToColorARGB(colorMap->Colors[colorIndex]);
} else {
*sPixels = 0;
}
}
4.回调到Java层,通知Java层刷新UI
//Runnable 的run方法id
jclass runClass = env->GetObjectClass(runnable);
jmethodID runMethod = env->GetMethodID(runClass, "run", "()V");
//Bitmap绘制好了,回调runnable的run方法,Java层刷新ImageView即可看到新的一帧图片
env->CallVoidMethod(runnable, runMethod);
使用giflib加载gif的几个步骤简单总结一下:
- 打开gif文件,拿到native层GifFile;
- 通过GifFile 可以获取到gif宽高信息;
- 取出gif每一帧图片,进行解码操作,大概就是将图片像素信息读取到缓冲区,然后将缓冲区中的数据填充到Bitmap中去,最后将解码结果回调到应用层,更新显示图片。
核心代码已经贴出,源码放github,没有任何封装,只适合学习参考~
giflib 加载gif为什么高效
从上面的流程来看,giflib加载gif是一帧一帧解析,然后回调给Java层;对比Glide来说吧,Glide加载gif是怎么处理的呢?
//com.bumptech.glide.gifdecoder.GifHeaderParser#readContents(int)
/**
* Main file parser. Reads GIF content blocks. Stops after reading maxFrames
*/
private void readContents(int maxFrames) {
// Read GIF file content blocks.
boolean done = false;
//1、遍历所有帧
while (!(done || err() || header.frameCount > maxFrames)) {
int code = read();
switch (code) {
case IMAGE_SEPARATOR:
if (header.currentFrame == null) {
header.currentFrame = new GifFrame();
}
//2、读取每一帧的Bitmap数据
readBitmap();
...
}
/**
* Reads next frame image.
*/
private void readBitmap() {
// (sub)image position & size.
header.currentFrame.ix = readShort();
header.currentFrame.iy = readShort();
header.currentFrame.iw = readShort();
header.currentFrame.ih = readShort();
...
// 3、每一帧都缓存到list
header.frames.add(header.currentFrame);
}
从上面注释123可以看出,Glide解析Gif是先一次性将所有图片帧信息解析出来缓存到List中,
而通过giflib的方式是解析一帧就更新显示一帧。那么从首次加载速度上对比,Glide肯定是要比giflib慢,特别是当gif中图片帧比较多的时候。
看到这里,是否会有一些疑问,例如:
我们编译的so动态库,JVM是如何加载这个so的?Java层调用一个native方法,最终是如何调用到so中对应的c++方法的?
四、System.loadLibrary(...) 原理
很多Android开发即使没接触过NDK,但是对于 System.loadLibrary("native-lib");
应该不陌生,是否知道其中原理呢?
4.1 System#loadLibrary
public static void loadLibrary(String libname) {
Runtime.getRuntime().loadLibrary0(Reflection.getCallerClass(), libname);
}
4.2 Runtime#loadLibrary0
private synchronized void loadLibrary0(ClassLoader loader, Class<?> callerClass, String libname) {
if (libname.indexOf((int)File.separatorChar) != -1) {
throw new UnsatisfiedLinkError(
"Directory separator should not appear in library name: " + libname);
}
String libraryName = libname;
if (loader != null && !(loader instanceof BootClassLoader)) {
//1、先查找so是否存在
String filename = loader.findLibrary(libraryName);
if (filename == null) {
//so 不存在,抛异常
throw new UnsatisfiedLinkError(loader + " couldn't find \"" +
System.mapLibraryName(libraryName) + "\"");
}
// 2、so存在,nativeLoad方法加载so
String error = nativeLoad(filename, loader);
if (error != null) {
throw new UnsatisfiedLinkError(error);
}
return;
}
...
}
loadLibrary0 方法主要两个步骤,1、查找so是否存;2、调用native方法加载so
接下来就分析这两个部分
先看注释1,ClassLoader#findLibrary
2.1 查找so是否存在:
ClassLoader#findLibrary
protected String findLibrary(String libname) {
return null;
}
ClassLoader是抽象类,实现类是BaseDexClassLoader
BaseDexClassLoader#findLibrary
public String findLibrary(String name) {
return pathList.findLibrary(name);
}
pathList 是 BaseDexClassLoader里的一个DexPathList对象,里面存放dex数组,之前一篇关于启动优化的文章讲MultiDex原理的时候有分析过 findClass
方法,今天要分析的是 findLibrary
方法
DexPathList#findLibrary
public String findLibrary(String libraryName) {
//1、名称映射,相当于转换,例如过滤一些空格啥的
String fileName = System.mapLibraryName(libraryName);
//2、应用所有so库应该都放在nativeLibraryPathElements里了,遍历一下
for (NativeLibraryElement element : nativeLibraryPathElements) {
//3、从 NativeLibraryElement 里找
String path = element.findNativeLibrary(fileName);
if (path != null) {
return path;
}
}
return null;
}
从nativeLibraryPathElements这个列表里遍历,
nativeLibraryPathElements 存放了依赖库的所有目录信息,
NativeLibraryElement
是 DexPathList 的内部类,看下注释3的 findNativeLibrary
方法
NativeLibraryElement#findNativeLibrary
public String findNativeLibrary(String name) {
// 这个方法里会尝试创建urlHandler,如果是zip文件,urlHandler就不为空
maybeInit();
if (zipDir == null) {
//通过名字,从so目录创建这个文件
String entryPath = new File(path, name).getPath();
//这个文件可读写则直接返回路径,也就是so全路径
if (IoUtils.canOpenReadOnly(entryPath)) {
return entryPath;
}
} else if (urlHandler != null) {
//zip文件也是可以作为动态库的,把zip文件路径返回回去
// Having a urlHandler means the element has a zip file.
// In this case Android supports loading the library iff
// it is stored in the zip uncompressed.
String entryName = zipDir + '/' + name;
if (urlHandler.isEntryStored(entryName)) {
return path.getPath() + zipSeparator + entryName;
}
}
return null;
}
findNativeLibrary
方法最终是通过new File("xxx.so")
来判断so是否存在。
这里也了解到,动态库不仅指so库,zip文件也是可以作为动态库的,至于什么时候会用到zip文件作为动态库呢? 在 BaseDexClassLoader#addNativePath
方法会添加动态库搜索路径,只要传了路径带有zip分隔符 "!/"
,就满足zip库的条件,例如data/data/1.zip!/data/data/,"!/"
前面是zip文件路径,后面是zip所在的目录。然而BaseDexClassLoader#addNativePath
是一个隐藏方法,我们不能显式调用。
分析到这里,突然想到动态加载so,是否可以在so下载到指定目录之后,反射调用BaseDexClassLoader的addNativePath 方法添加一个动态库搜索目录?
当然,一般动态加载so并不需要这么复杂~
动态加载so
动态加载so一般做法是:
将so下载下来,拷贝到私有目录,/data/app/包名/lib/arm/ ,然后使用System.load("temp.so");
去加载指定目录下的so即可,实例如下:
String soPath = Environment.getExternalStorageDirectory().toString() + "/libtemp.so";
//模拟下载到指定目录(assets目录拷贝到sd卡)
File fromFile = new File(soPath);
if (!fromFile.exists()){
boolean copyAssetFileToPath = FileUtil.copyAssetFileToPath(MainActivity.this, "libtemp.so", soPath);
if (!copyAssetFileToPath){
Toast.makeText(MainActivity.this,"拷贝到sdk卡失败",Toast.LENGTH_SHORT).show();
return;
}
}
fromFile = new File(soPath);
if (!fromFile.exists()) {
Toast.makeText(MainActivity.this,"so不存在",Toast.LENGTH_SHORT).show();
return;
}
File libFile = MainActivity.this.getDir("libs", Context.MODE_PRIVATE);
String targetDir = libFile.getAbsolutePath() + "/libtemp.so";
//将下载下来的so拷贝到私有目录:/data/user/0/包名/app_libs/
FileUtil.copyFile(fromFile, libFile);
//加载so,传绝对路径
System.load(targetDir);
Toast.makeText(MainActivity.this,"动态加载so成功",Toast.LENGTH_SHORT).show();
假设so存在,则回到Runtime 类进入下一步,nativeLoad
2.2 Runtime#nativeLoad
native 方法,对应的JNI代码如下
libcore/ojluni/src/main/native/Runtime.c
JNIEXPORT jstring JNICALL
Runtime_nativeLoad(JNIEnv* env, jclass ignored, jstring javaFilename,
jobject javaLoader)
{
return JVM_NativeLoad(env, javaFilename, javaLoader);
}
直接调用JVM_NativeLoad , JVM_NativeLoad方法申明在jvm.h中,实现在OpenjdkJvm.cc中 art/openjdkjvm/OpenjdkJvm.cc
JNIEXPORT jstring JVM_NativeLoad(JNIEnv* env,
jstring javaFilename,
jobject javaLoader) {
ScopedUtfChars filename(env, javaFilename);
if (filename.c_str() == NULL) {
return NULL;
}
std::string error_msg;
{
//主要是这两句,JavaVMExt 的LoadNativeLibrary方法
art::JavaVMExt* vm = art::Runtime::Current()->GetJavaVM();
bool success = vm->LoadNativeLibrary(env,
filename.c_str(),
javaLoader,
&error_msg);
if (success) {
return nullptr;
}
}
// Don't let a pending exception from JNI_OnLoad cause a CheckJNI issue with NewStringUTF.
env->ExceptionClear();
return env->NewStringUTF(error_msg.c_str());
}
调用JavaVMExt 的LoadNativeLibrary方法,源码 /art/runtime/java_vm_ext.cc
这个方法代码太多了,我只保留要分析的重点部分,加以注释
bool JavaVMExt::LoadNativeLibrary(JNIEnv* env,
const std::string& path,
jobject class_loader,
std::string* error_msg) {
...
SharedLibrary* library;
Thread* self = Thread::Current();
{
//1、读缓存,第一次加载成功会放缓存
library = libraries_->Get(path);
}
...
//有缓存的情况下
if (library != nullptr) {
// 2、ClassLoader 不一致,一个so不能被两个ClassLoader同时加载,返回失败
if (library->GetClassLoaderAllocator() != class_loader_allocator) {
...
std::string old_class_loader = call_to_string(library->GetClassLoader());
std::string new_class_loader = call_to_string(class_loader);
StringAppendF(error_msg, "Shared library \"%s\" already opened by "
"ClassLoader %p(%s); can't open in ClassLoader %p(%s)",
path.c_str(),
library->GetClassLoader(),
old_class_loader.c_str(),
class_loader,
new_class_loader.c_str());
LOG(WARNING) << *error_msg;
return false;
}
//3、so 已经加载过,不需要再次加载了
VLOG(jni) << "[Shared library \"" << path << "\" already loaded in "
<< " ClassLoader " << class_loader << "]";
if (!library->CheckOnLoadResult()) {
StringAppendF(error_msg, "JNI_OnLoad failed on a previous attempt "
"to load \"%s\"", path.c_str());
return false;
}
return true;
}
// Below we dlopen but there is no paired dlclose, this would be necessary if we supported
// class unloading. Libraries will only be unloaded when the reference count (incremented by
// dlopen) becomes zero from dlclose.
...
//4、打开共享库,从注释看,最终是会通过 dlopen 函数打开so,返回一个handle,相当于so的内存地址吧
void* handle = android::OpenNativeLibrary(env,
runtime_->GetTargetSdkVersion(),
path_str,
class_loader,
library_path.get(),
&needs_native_bridge,
error_msg);
VLOG(jni) << "[Call to dlopen(\"" << path << "\", RTLD_NOW) returned " << handle << "]";
if (handle == nullptr) {
VLOG(jni) << "dlopen(\"" << path << "\", RTLD_NOW) failed: " << *error_msg;
return false;
}
if (env->ExceptionCheck() == JNI_TRUE) {
LOG(ERROR) << "Unexpected exception:";
env->ExceptionDescribe();
env->ExceptionClear();
}
// Create a new entry.
// TODO: move the locking (and more of this logic) into Libraries.
bool created_library = false;
{
// Create SharedLibrary ahead of taking the libraries lock to maintain lock ordering.
std::unique_ptr<SharedLibrary> new_library(
//5、打开so成功之后,创建一个 SharedLibrary,handle作为参数之一
new SharedLibrary(env,
self,
path,
handle,
needs_native_bridge,
class_loader,
class_loader_allocator));
//6、加到缓存中
library = libraries_->Get(path);
if (library == nullptr) { // We won race to get libraries_lock.
library = new_library.release();
libraries_->Put(path, library);
created_library = true;
}
}
...
bool was_successful = false;
//7、找找看是否重写了 JNI_OnLoad 方法
void* sym = library->FindSymbol("JNI_OnLoad", nullptr);
//8、没有重写,成功标志为true,就结束了
if (sym == nullptr) {
VLOG(jni) << "[No JNI_OnLoad found in \"" << path << "\"]";
was_successful = true;
} else {
// Call JNI_OnLoad. We have to override the current class
// 9、有重写 JNI_OnLoad 函数,调用它
...
//10、调用 JNI_OnLoad 函数,返回JNI版本号
int version = (*jni_on_load)(this, nullptr);
...
// 11、JNI_OnLoad 方法返回JNI_ERR,或者返回的版本号不支持,都会导致so加载失败
if (version == JNI_ERR) {
StringAppendF(error_msg, "JNI_ERR returned from JNI_OnLoad in \"%s\"", path.c_str());
} else if (JavaVMExt::IsBadJniVersion(version)) {
StringAppendF(error_msg, "Bad JNI version returned from JNI_OnLoad in \"%s\": %d",
path.c_str(), version);
//省略注释
} else {
was_successful = true;
}
VLOG(jni) << "[Returned " << (was_successful ? "successfully" : "failure")
<< " from JNI_OnLoad in \"" << path << "\"]";
}
library->SetResult(was_successful);
return was_successful;
}
整理一下 LoadNativeLibrary 的逻辑:
注释1、2、3,读缓存,如果之前已经加载过so,那么判断ClassLoader是不是一致,不一致就返回失败,一致就返回成功,不允许一个so被两个ClassLoader同时加载。
注释4、5、6:通过dlopen
函数打开so,成功则创建对应的SharedLibrary 对象,并且加到缓存中。
注释7、8、9、10:判断so中是否有 JNI_OnLoad 函数,如果没有的话就到此结束,如果有的话,要调用这个函数。
注释11: JNI_OnLoad 函数会返回一个JNI版本号,如果返回的是JNI_ERR或者返回的版本号不支持,会报错。
JNI_OnLoad 返回值一般是这样写的,要判断JVM支持哪个版本
JNIEXPORT jint JNI_OnLoad(JavaVM *vm, void *reserved) {
LOGD("JNI_OnLoad");
JNIEnv *env;
if (vm->GetEnv((void **) &env, JNI_VERSION_1_6) == JNI_OK) {
return JNI_VERSION_1_6;
} else if (vm->GetEnv((void **) &env, JNI_VERSION_1_4) == JNI_OK) {
return JNI_VERSION_1_4;
}
return JNI_ERR;
}
到此,System.LoadLibrary(...)
的源码分析就结束了(基于9.0源码分析,其它版本可能会有些差异,但原理是一样的)
简单说就是两个步骤:
- 检测so文件是否存在;
- 调用
dlopen
这个系统函数去打开so,成功则会创建 SharedLibrary 对象并且缓存起来,后面调用native方法按道理应该是从这个SharedLibrary中查找是否有某个JNI方法,进行调用。
五、native方法的调用原理
当简历上写熟悉JNI、NDK开发的时候,有很大的概率面试官会问这个问题,你熟悉JNI,到底熟悉到哪个程度,native方法调用原理知道不,如何找到对应的JNI方法的呢?
这个部分能讲清楚的文章很少了,我只能花点时间总结一下了~
5.1 native方法跟普通方法在字节码中的区别
首先做一个测试,写一个JniTest 的java类,看下native方法在字节码里面跟普通方法的区别
public class JniTest {
static {
System.loadLibrary("temp");
}
public native int nativeAdd(int x, int y);
public int add(int x, int y) {
return x + y;
}
public static void main(String[] args) {
JniTest jniTest = new JniTest();
jniTest.nativeAdd(2012, 3);
jniTest.add(2012, 3);
}
}
编译成 JniTest.class
javac JniTest.java
查看字节码
javap -c JniTest.class
通过查看字节码可以看到:
- 普通方法有Code代码块,native方法没有方法体,所以也就没有对应的Code代码块;
- 不管是普通方法还是native方法的调用,都是通过
invokevirtual
指令。
5.2 dvmInvokeMethod
不管是调用普通方法还是native方法,针对dalvik虚拟机来说,都会调用 dvmInvokeMethod
方法,
接下来就分析dalvik虚拟机是怎么调用一个native方法的,
由于4.4之后dalvik虚拟机 被 art虚拟机代替,所以这一节基于4.4源码进行分析~
dvmInvokeMethod
方法的源码位于 /dalvik/vm/interp/Stack.cpp
Object* dvmInvokeMethod(Object* obj, const Method* method,
ArrayObject* argList, ArrayObject* params, ClassObject* returnType,
bool noAccessCheck)
{
...省略其它代码
// 1、判断是native方法,则调用 *method->nativeFunc 方法
if (dvmIsNativeMethod(method)) {
TRACE_METHOD_ENTER(self, method);
/*
* Because we leave no space for local variables, "curFrame" points
* directly at the method arguments.
*/
// 2、调用 nativeFunc
(*method->nativeFunc)((u4*)self->interpSave.curFrame, &retval,
method, self);
TRACE_METHOD_EXIT(self, method);
} else {
dvmInterpret(self, method, &retval);
}
...省略其它代码
我们只看调用native方法的逻辑,如果判断是一个native方法,则调用Method对象的 nativeFunc 方法,那么理论上我们只要看 nativeFunc 是在哪里赋值的,就可以追踪到native方法跟JNI方法的对应关系~
5.3 nativeFunc函数的赋值
JNI方法的注册方式有两种,默认的和动态注册,
默认情况下,JVM加载一个类的时候,会调用native层Class对象的loadClassFromDex
方法,而这个方法内部会调用loadMethodFromDex
方法去加载Class对象内部的方法,当遇到native方法,就会对nativeFunc函数进行赋值,可以在源码中得到验证
//调用链
dvmDefineClass
findClassNoInit
loadClassFromDex
loadClassFromDex0
loadMethodFromDex
static void loadMethodFromDex(ClassObject* clazz, const DexMethod* pDexMethod,
Method* meth)
{
...
// 1、native 方法和抽象方法是没有 Code代码块的
pDexCode = dexGetCode(pDexFile, pDexMethod);
if (pDexCode != NULL) {
/* integer constants, copy over for faster access */
meth->registersSize = pDexCode->registersSize;
meth->insSize = pDexCode->insSize;
meth->outsSize = pDexCode->outsSize;
/* pointer to code area */
meth->insns = pDexCode->insns;
} else {
/*
* We don't have a DexCode block, but we still want to know how
* much space is needed for the arguments (so we don't have to
* compute it later). We also take this opportunity to compute
* JNI argument info.
*
* We do this for abstract methods as well, because we want to
* be able to substitute our exception-throwing "stub" in.
*/
int argsSize = dvmComputeMethodArgsSize(meth);
if (!dvmIsStaticMethod(meth))
argsSize++;
meth->registersSize = meth->insSize = argsSize;
assert(meth->outsSize == 0);
assert(meth->insns == NULL);
// 2.如果是native方法,nativeFunc 指向 dvmResolveNativeMethod 方法
if (dvmIsNativeMethod(meth)) {
meth->nativeFunc = dvmResolveNativeMethod;
meth->jniArgInfo = computeJniArgInfo(&meth->prototype);
}
}
}
注释1:读取当前方法的Code代码块,这个在前面通过javap 分析字节码的时候有说到,普通方法有Code 代码块,native方法和抽象方法是没有的。
注释2:判断如果是native方法,则将nativeFunc 指向 dvmResolveNativeMethod 这个方法,
5.4 dvmResolveNativeMethod
这个方法定义在Native.cpp这个类中,
void dvmResolveNativeMethod(const u4* args, JValue* pResult,
const Method* method, Thread* self)
{
ClassObject* clazz = method->clazz;
/*
* If this is a static method, it could be called before the class
* has been initialized.
*/
// 1、对于静态方法,要先确保该Class已经初始化
if (dvmIsStaticMethod(method)) {
if (!dvmIsClassInitialized(clazz) && !dvmInitClass(clazz)) {
assert(dvmCheckException(dvmThreadSelf()));
return;
}
} else {
assert(dvmIsClassInitialized(clazz) ||
dvmIsClassInitializing(clazz));
}
/* start with our internal-native methods */
//2、从内部的本地方法表查询
DalvikNativeFunc infunc = dvmLookupInternalNativeMethod(method);
if (infunc != NULL) {
/* resolution always gets the same answer, so no race here */
IF_LOGVV() {
char* desc = dexProtoCopyMethodDescriptor(&method->prototype);
LOGVV("+++ resolved native %s.%s %s, invoking",
clazz->descriptor, method->name, desc);
free(desc);
}
if (dvmIsSynchronizedMethod(method)) {
ALOGE("ERROR: internal-native can't be declared 'synchronized'");
ALOGE("Failing on %s.%s", method->clazz->descriptor, method->name);
dvmAbort(); // harsh, but this is VM-internal problem
}
//找到就调用
DalvikBridgeFunc dfunc = (DalvikBridgeFunc) infunc;
dvmSetNativeFunc((Method*) method, dfunc, NULL);
dfunc(args, pResult, method, self);
return;
}
/* now scan any DLLs we have loaded for JNI signatures */
//3、 从动态链接库中查询
void* func = lookupSharedLibMethod(method);
if (func != NULL) {
/* found it, point it at the JNI bridge and then call it */
//4、找到就调用
dvmUseJNIBridge((Method*) method, func);
(*method->nativeFunc)(args, pResult, method, self);
return;
}
IF_ALOGW() {
char* desc = dexProtoCopyMethodDescriptor(&method->prototype);
ALOGW("No implementation found for native %s.%s:%s",
clazz->descriptor, method->name, desc);
free(desc);
}
dvmThrowUnsatisfiedLinkError("Native method not found", method);
}
注释1:如果该native方法是静态方法,要确保对应的Class已经初始化;
注释2:从内部的本地方法表中查询,dvmLookupInternalNativeMethod
;
注释3:从动态链接库中查询,lookupSharedLibMethod
,要分析的重点;
注释4:不管是从内部本地方法表还是从动态库中找到native方法对应的JNI方法,都会通过JNI桥建立链接关系,然后调用这个JNI方法。
这个调用分析完了,那么如何找到native方法对应的JNI方法呢?
分两种情况
5.4.1 dvmLookupInternalNativeMethod
先看下 dvmLookupInternalNativeMethod
方法,
源码在 dalvik/vm/native/InternalNative.cpp
//1、gDvmNativeMethodSet 的定义
static DalvikNativeClass gDvmNativeMethodSet[] = {
{ "Ljava/lang/Object;", dvm_java_lang_Object, 0 },
{ "Ljava/lang/Class;", dvm_java_lang_Class, 0 },
{ "Ljava/lang/Double;", dvm_java_lang_Double, 0 },
{ "Ljava/lang/Float;", dvm_java_lang_Float, 0 },
{ "Ljava/lang/Math;", dvm_java_lang_Math, 0 },
{ "Ljava/lang/Runtime;", dvm_java_lang_Runtime, 0 },
{ "Ljava/lang/String;", dvm_java_lang_String, 0 },
{ "Ljava/lang/System;", dvm_java_lang_System, 0 },
{ "Ljava/lang/Throwable;", dvm_java_lang_Throwable, 0 },
...
};
DalvikNativeFunc dvmLookupInternalNativeMethod(const Method* method)
{
const char* classDescriptor = method->clazz->descriptor;
const DalvikNativeClass* pClass;
u4 hash;
hash = dvmComputeUtf8Hash(classDescriptor);
// 2、pClass 指针指向 gDvmNativeMethodSet 这个数组,
pClass = gDvmNativeMethodSet;
while (true) {
if (pClass->classDescriptor == NULL)
break;
if (pClass->classDescriptorHash == hash &&
strcmp(pClass->classDescriptor, classDescriptor) == 0)
{
// 注释5在这里、DalvikNativeMethod 对象指向的是 DalvikNativeClass的methodInfo字段
const DalvikNativeMethod* pMeth = pClass->methodInfo;
while (true) {
if (pMeth->name == NULL)
break;
// 3、匹配到对应方法,返回
if (dvmCompareNameDescriptorAndMethod(pMeth->name,
pMeth->signature, method) == 0)
{
/* match */
//ALOGV("+++ match on %s.%s %s at %p",
// className, methodName, methodSignature, pMeth->fnPtr);
//4、返回这个方法
return pMeth->fnPtr;
}
pMeth++;
}
}
// 5、pClass 是一个指针,指向的是gDvmNativeMethodSet 这个数组首地址,pClass++表示数组遍历
pClass++;
}
return NULL;
}
从内部的本地方法表查找,这里有几个知识点:
注释1:内部本地方法表gDvmNativeMethodSet(数组)定义了很多Java类对应的native类;
注释2:pClass是DalvikNativeClass类型的指针,指向数组,pClass就代表数组的首地址,
pClass = gDvmNativeMethodSet
等同于 pClass = gDvmNativeMethodSet[0]
,
注释5 的第一次pClass++
,结果是pClass = gDvmNativeMethodSet[1]
,也就是数组的遍历;
注释3:通过对比方法名、描述符,从内部本地方法表找到native方法的实现了;
注释4:返回 pMeth->fnPtr,也就是返回DalvikNativeMethod 对象的fnPtr函数
临时增加了注释5,DalvikNativeMethod 对象的fnPtr函数其实是 DalvikNativeClass的methodInfo字段,直接看 DalvikNativeClass
是怎么给methodInfo 赋值的,
这里有必要看一下 DalvikNativeClass
的定义
源码位于 dalvik/vm/Native.h
struct DalvikNativeClass {
const char* classDescriptor;
const DalvikNativeMethod* methodInfo;
u4 classDescriptorHash; /* initialized at runtime */
};
再回头看数组的初始化
//1、gDvmNativeMethodSet 的定义
static DalvikNativeClass gDvmNativeMethodSet[] = {
...
//以熟悉的String举例,这里第二个参数 dvm_java_lang_String 就是 DalvikNativeMethod 类型
{ "Ljava/lang/String;", dvm_java_lang_String, 0 },
...
对于 String类来说,DalvikNativeMethod 指向 dvm_java_lang_String,看源码
dalvik/vm/native/java_lang_String.cpp
const DalvikNativeMethod dvm_java_lang_String[] = {
{ "charAt", "(I)C", String_charAt },
{ "compareTo", "(Ljava/lang/String;)I", String_compareTo },
{ "equals", "(Ljava/lang/Object;)Z", String_equals },
{ "fastIndexOf", "(II)I", String_fastIndexOf },
{ "intern", "()Ljava/lang/String;", String_intern },
{ "isEmpty", "()Z", String_isEmpty },
{ "length", "()I", String_length },
{ NULL, NULL, NULL },
};
哦,明白了,dvm_java_lang_String 又是一个数组,DalvikNativeMethod 类型,到这里其实已经不用分析 DalvikNativeMethod 的结构了,每一行三个参数对应一个DalvikNativeMethod对象,第一个参数是Java层String的方法名,第二个参数是方法签名,第三个参数是native层String对应的函数名。
举个栗子,Java层String类的 charAt 方法是一个native方法,对应的JNI方法是 java_lang_String.cpp 的 String_charAt 函数。
分析内部本地方法表,有种抓迷藏的感觉,好像有点跑偏,我们主要还是要分析如何从动态库so中找到native方法对应的JNI方法,继续吧~
5.4.2 lookupSharedLibMethod 方法
//dalvik/vm/Native.cpp
static void* lookupSharedLibMethod(const Method* method)
{
if (gDvm.nativeLibs == NULL) {
ALOGE("Unexpected init state: nativeLibs not ready");
dvmAbort();
}
//前面判空不管,主要看这个方法
return (void*) dvmHashForeach(gDvm.nativeLibs, findMethodInLib,
(void*) method);
}
从动态库总查找,同样需要遍历,看下 dvmHashForeach 这个方法,第一个参数是动态库的集合,第二个参数是一个查找的方法,第三个参数是我们的native方法,所以,意思就是遍历动态库集合,调用 findMethodInLib 方法,传两个参数,一个是动态库,一个是native方法名。
所以,这里只要关注两个点:
- gDvm.nativeLibs 是哪里赋值的,应该跟System.loadLibrary有关;
- findMethodInLib 方法,如何从一个动态库中找到某个native方法的实现方法。
gDvm.nativeLibs 是一个动态库集合,先假设 gDvm.nativeLibs 就是 System.loadLibrary 添加进去的,直接看 findMethodInLib
方法
// dalvik/vm/Native.cpp
static int findMethodInLib(void* vlib, void* vmethod)
{
// 1、动态库集合里面放的就是 SharedLib 对象
const SharedLib* pLib = (const SharedLib*) vlib;
const Method* meth = (const Method*) vmethod;
char* preMangleCM = NULL;
char* mangleCM = NULL;
char* mangleSig = NULL;
char* mangleCMSig = NULL;
void* func = NULL;
int len;
...
/*
* First, we try it without the signature.
*/
// 2、通过native方法名,获取对应的JNI方法名
preMangleCM =
createJniNameString(meth->clazz->descriptor, meth->name, &len);
if (preMangleCM == NULL)
goto bail;
//3、对JNI方法进行处理,转换成so中对应的格式
mangleCM = mangleString(preMangleCM, len);
if (mangleCM == NULL)
goto bail;
ALOGV("+++ calling dlsym(%s)", mangleCM);
// 4、通过 dlsym 这个系统函数,查找so中的方法,找到就返回一个指针
func = dlsym(pLib->handle, mangleCM);
...
bail:
free(preMangleCM);
free(mangleCM);
free(mangleSig);
free(mangleCMSig);
return (int) func;
}
这个方法分析完就到尾声了,好激动
注释1:从动态库集合里遍历,拿到的是 SharedLib 对象(动态库的一个封装对象,里面有动态库的句柄)
注释2:通过native方法名,获取对应的JNI方法名,createJniNameString 方法看一下
static char* createJniNameString(const char* classDescriptor,
const char* methodName, int* pLen)
{
char* result;
size_t descriptorLength = strlen(classDescriptor);
// JNI方法名有三个部分组成,先计算需要的内存大小,申请内存
*pLen = 4 + descriptorLength + strlen(methodName);
result = (char*)malloc(*pLen +1);
/*
* Add one to classDescriptor to skip the "L", and then replace
* the final ";" with a "/" after the sprintf() call.
*/
// sprintf 函数,将后面的字符串赋值给result,
sprintf(result, "Java/%s%s", classDescriptor + 1, methodName);
result[5 + (descriptorLength - 2)] = '/';
return result;
}
这里只是获取native方法对应的JNI方法名,比如:
com.lanshifu.gifloadertest.GifHandler
这个类的一个native方法方法 ,对应的JNI方法如下:
native方法 | 对应的JNI方法 |
---|---|
public static native int getWidth(long nativeGifFile) | Java_com_lanshifu_gifloadertest_GifHandler_getWidth(...) |
这个JNI方法我们是可以通过Android Studio 快捷键自动生成,为什么生成这样格式,原理就在上面了~
回到上面注释3:将获取的JNI方法名转换一下,转换成动态库中的编码格式;
注释4:通过dlsym
函数查找动态库中的方法并返回,结束~
由于分析System.loadLibrary
的时候是基于9.0 源码,
而分析 dalvik 虚拟机的方法调用流程,是基于4.4 的源码(4.4之后dalvik被art代替),导致上面的动态库集合好像跟System.loadLibrary
有点脱钩,但其实动态库集合中的数据,就是在System.loadLibrary 的时候添加进去的,大家可以基于 4.4 源码分析 System.loadLibrary,肯定是这样的, 我就不再重复分析了~
native 方法调用原理小结
native方法的调用原理小结如下:
- 通过javap 命令查看字节码发现普通方法和native方法的调用都是通过
invokevirtual
指令; - dalvik 虚拟机调用一个方法的时候,如果判断是native方法,则会通过Method类的nativeFunc函数进行调用;
- 就JNI默认的静态注册流程分析,当虚拟机加载一个类的时候,会去加载里面的方法,当遇到native方法,则会给nativeFunc赋值(没有调用),指向一个dvmResolveNativeMethod 方法;
- dvmResolveNativeMethod 方法,会先从内部的本地方法表查询是否有对应的JNI方法,找到就通过JNI桥建立关系并调用;如果没找到,就遍历动态库(so)查找,先组装native方法名对应的JNI方法名,然后转换成动态库的编码格式,再通过
dlsym
函数查找动态库中是否有该方法。
如果是在面试中问到native方法调用原理,那么最好能先说System.loadLibrary原理:找到so,再通过dlopen
函数去打开so,返回一个句柄,保存到动态库集合中;native 方法的调用会先从内部本地方法表查找,找不到再遍历这个动态库集合,通过dlsym
函数查找动态库中是否有对应的JNI方法,有的话就将native方法跟JNI方法建立链接并调用。
全文总结
这篇文章从网易云的进阶课程广告入手:
- 通过代码示例介绍了如何使用giflib实现NDK高效加载gif;
- 介绍System.loadLibrary的原理,两个步骤,第一步是先查找动态库文件是否存在,第二步是通过
dlopen
函数打开动态库,返回handle句柄,添加到动态库集合中,还会调用JNI_OnLoad 函数(如果有的话);引申了动态加载so的方式。 - 介绍native方法调用的底层原理,由于Android 4.4 之后采用art虚拟机代替dalvik虚拟机,所以基于4.4源码基础上分析dalvik虚拟机调用一个native方法的流程。
相关参考链接:
Android 使用系统库giflib实现高效gif动画加载
android app动图优化:源码giflib加载gif动图,性能秒杀glide
图像解码之三——giflib解码gif图片
结尾
由于换了新工作,这篇文章应该是2019年最后一篇啦~
接下来我的计划是系统学习下Flutter,掘金没有像简书那样的归档功能,所以Flutter文章会先在简书发布。
面试官系列文章暂时告一段落吧,下一篇文章是什么内容还不知道呢,当我发现一个知识点是自己不会的,可能就会花时间去深入研究它,作为下一篇文章的主题~
大家有任何问题欢迎在评论区留言~
我在掘金发布的其它5篇文章:
面试官:简历上最好不要写Glide,不是问源码那么简单
总结UI原理和高级的UI优化方式
面试官:说说多线程并发问题
面试官又来了:你的app卡顿过吗?
面试官:今日头条启动很快,你觉得可能是做了哪些优化?