阅读 634

手撸网易云进阶课程-性能优化之NDK高效加载GIF

之前很多次看到网易云课程广告,里面有个熟悉的标题是这样的

性能优化之NDK高效加载GIF--NDK开发实战 三个小节如下:

  1. 安卓NDK开发快速入门
  2. giflib在安卓开发中的使用
  3. 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的库

xref/external/giflib

giflib

把.c 和 .h 结尾的文件下载下来,放到giflib文件夹中备用

giflib

先把giflib集成到Android Studio项目中先~

三、giflib 使用

上一步 giflib 已经是下载下来了

3.1 新建一个自带JNI功能的项目

为什么要新建,主要是考虑到部分同学对cmake 语法不清楚,所以通过新建项目来熟悉它,

Android Studio 新建 Native C++ 项目

create new project

项目名就叫 GifLoaderTest

新建带有c++代码的项目

然后next,finish,

等待同步和编译完成(可能会提示安装cmake,按提示安装即可),这个是可以运行成功的带有JNI基本配置的项目。

3.2 引入giflib

看下 CMakeLists.txt 的需要配置什么

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集成进去:

  1. 将 giflib 复制到 cpp目录下
  2. CMakeLists.txt 中将 giflib 目录的文件指定为源文件,对应上面的注释1和注释2
  3. 添加其它依赖,对应注释3

对于注释3,log、jnigraphics、android,这几个依赖在哪里呢?答案是在NDK目录下,例如我的mac是在这个目录

/Users/{用户名}/Library/Android/sdk/ndk-bundle/platforms/android-27/arch-arm/usr/lib

ndk-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

生成JNI方法

会自动在 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的几个步骤简单总结一下:

  1. 打开gif文件,拿到native层GifFile;
  2. 通过GifFile 可以获取到gif宽高信息;
  3. 取出gif每一帧图片,进行解码操作,大概就是将图片像素信息读取到缓冲区,然后将缓冲区中的数据填充到Bitmap中去,最后将解码结果回调到应用层,更新显示图片。

核心代码已经贴出,源码放github,没有任何封装,只适合学习参考~

github.com/lanshifu/Gi…

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

源码传送BaseDexClassLoader.java

public String findLibrary(String name) {
        return pathList.findLibrary(name);
}
复制代码

pathList 是 BaseDexClassLoader里的一个DexPathList对象,里面存放dex数组,之前一篇关于启动优化的文章讲MultiDex原理的时候有分析过 findClass 方法,今天要分析的是 findLibrary 方法

DexPathList#findLibrary

DexPathList.java

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源码分析,其它版本可能会有些差异,但原理是一样的)
简单说就是两个步骤:

  1. 检测so文件是否存在;
  2. 调用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

字节码

通过查看字节码可以看到:

  1. 普通方法有Code代码块,native方法没有方法体,所以也就没有对应的Code代码块;
  2. 不管是普通方法还是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函数进行赋值,可以在源码中得到验证

dalvik/vm/oo/Class.cpp

//调用链
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这个类中,

dalvik/vm/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方法名。

所以,这里只要关注两个点:

  1. gDvm.nativeLibs 是哪里赋值的,应该跟System.loadLibrary有关;
  2. 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方法的调用原理小结如下:

  1. 通过javap 命令查看字节码发现普通方法和native方法的调用都是通过invokevirtual指令;
  2. dalvik 虚拟机调用一个方法的时候,如果判断是native方法,则会通过Method类的nativeFunc函数进行调用;
  3. 就JNI默认的静态注册流程分析,当虚拟机加载一个类的时候,会去加载里面的方法,当遇到native方法,则会给nativeFunc赋值(没有调用),指向一个dvmResolveNativeMethod 方法;
  4. dvmResolveNativeMethod 方法,会先从内部的本地方法表查询是否有对应的JNI方法,找到就通过JNI桥建立关系并调用;如果没找到,就遍历动态库(so)查找,先组装native方法名对应的JNI方法名,然后转换成动态库的编码格式,再通过dlsym函数查找动态库中是否有该方法。

如果是在面试中问到native方法调用原理,那么最好能先说System.loadLibrary原理:找到so,再通过dlopen函数去打开so,返回一个句柄,保存到动态库集合中;native 方法的调用会先从内部本地方法表查找,找不到再遍历这个动态库集合,通过dlsym函数查找动态库中是否有对应的JNI方法,有的话就将native方法跟JNI方法建立链接并调用。

全文总结

这篇文章从网易云的进阶课程广告入手:

  1. 通过代码示例介绍了如何使用giflib实现NDK高效加载gif;
  2. 介绍System.loadLibrary的原理,两个步骤,第一步是先查找动态库文件是否存在,第二步是通过dlopen函数打开动态库,返回handle句柄,添加到动态库集合中,还会调用JNI_OnLoad 函数(如果有的话);引申了动态加载so的方式。
  3. 介绍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卡顿过吗?
面试官:今日头条启动很快,你觉得可能是做了哪些优化?

关注下面的标签,发现更多相似文章
评论