手擼網易雲進階課程-性能優化之NDK高效加載GIF

以前不少次看到網易雲課程廣告,裏面有個熟悉的標題是這樣的html

性能優化之NDK高效加載GIF--NDK開發實戰 三個小節以下:java

  1. 安卓NDK開發快速入門
  2. giflib在安卓開發中的使用
  3. NDK加載GIF較傳統加載方式的優點

這個廣告標題已經看到過好屢次了,是一個進階課程,那麼確定是收費的,我想不花錢就能學會NDK高效加載GIF,行嗎?android

首先第一點,安卓NDK開發快速入門並非很難,大多數Android開發都是屬於應用層開發,不多涉及NDK,只要找一篇入門文章學習便可。c++

第二點,giflib在安卓開發中的使用,看到 giflib,猜測應該是一個開源庫來的,在安卓中使用這個開源庫,應該是跟FFmpeg相似,須要會一點NDK,用c++代碼調用這個開源庫api,而後經過JNI,提供給Android端調用,若是單純是API使用,對於有JNI、NDK基礎的同窗來講,這一節其實難度不是很大。git

最後一點,NDK加載GIF較傳統加載方式的優點,是對標題的「高效加載」進行補充了,猜測這一節可能會講giflib 加載GIF的原理,爲何高效? 應該還會對比其它的不使用NDK加載的方式,例如Glide,爲何就慢?這個涉及到加載gif原理了,能掌握的話,面試是不虧的。github

會NDK,會使用NDK高效加載gif,知道其中的原理,這三點都會,吹吹牛逼應該不成問題。面試

有了這個思路以後,我以爲我應該能夠寫出這個所謂的網易雲的進階課程~canvas

固然,寫好這一篇文章,須要考慮初中級的同窗可能對JNI、NDK不熟悉,cmake語法可能須要說起,流程必須清晰,必須提供能夠運行的demo,對原理必須解釋清楚等等~windows

根據以前文章的風格,這篇文章不會單單介紹NKD高效加載gif,同時會把涉及到的相關知識點都總結總結,例如:so加載原理、native方法調用原理~api

直接進入正文吧~

1、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++ 有指針概念,指針箭頭指向的是一個內存地址,不少方法參數是指針類型,簡單理解就是址傳遞;
  • 指針若是指向的是一個數組,那麼指針表明數組首地址,訪問該指針就是訪問數組的首地址。

2、giflib在安卓開發中的使用

giflib 是啥呢?
經過搜索引擎,發現 giflib是android源碼中的一個用C語言寫的加載GIF的庫

xref/external/giflib

giflib

把.c 和 .h 結尾的文件下載下來,放到giflib文件夾中備用

giflib

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

3、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)

# 一、指定源文件目錄,把 /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}  # 二、把giflib源文件添加到 native-lib 這個庫中去
        ${MAIN_SOURCE} # 同2,咱們寫的cpp源文件
)

target_link_libraries(
        native-lib

        # 三、給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) {
            //一、若是是圖像數據塊,須要繪製到 ScreenBuffer 中
            case IMAGE_DESC_RECORD_TYPE :
            ...
            break;
            
            
            //二、額外信息塊,獲取幀之間間隔、透明顏色下標
            case EXTENSION_RECORD_TYPE:
            ...
            break;
            
     } while (RecordType != TERMINATE_RECORD_TYPE);

複製代碼

主要是處理兩種類型的塊

註釋1,圖像數據塊,這個圖像數據須要繪製到前面提到的屏幕buffer上面,相應的代碼以下:

case IMAGE_DESC_RECORD_TYPE :
                // 一、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]) {
                            // 二、從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;

                //三、將數據繪製到Bitmap上
                drawBitmap(env, bitmap, GifFile, ScreenBuffer, bitmapWidth, ColorMap,
                           GifFile->ImageCount - 1, pSavedImage,transparentColorIndex);

                //四、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) {

    //一、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++) {
            //二、從 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;
	    //一、遍歷全部幀
	    while (!(done || err() || header.frameCount > maxFrames)) {
	      int code = read();
	      switch (code) {
	        case IMAGE_SEPARATOR:
	          if (header.currentFrame == null) {
	            header.currentFrame = new GifFrame();
          }
          //二、讀取每一幀的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();
    
    ...
    // 三、每一幀都緩存到list
    header.frames.add(header.currentFrame);
    
   }
   
複製代碼

從上面註釋123能夠看出,Glide解析Gif是先一次性將全部圖片幀信息解析出來緩存到List中

而經過giflib的方式是解析一幀就更新顯示一幀。那麼從首次加載速度上對比,Glide確定是要比giflib慢,特別是當gif中圖片幀比較多的時候。


看到這裏,是否會有一些疑問,例如:
咱們編譯的so動態庫,JVM是如何加載這個so的?Java層調用一個native方法,最終是如何調用到so中對應的c++方法的?

4、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)) {
            //一、先查找so是否存在
            String filename = loader.findLibrary(libraryName);
            if (filename == null) {
                //so 不存在,拋異常
                throw new UnsatisfiedLinkError(loader + " couldn't find \"" +
                                               System.mapLibraryName(libraryName) + "\"");
            }
            // 二、so存在,nativeLoad方法加載so
            String error = nativeLoad(filename, loader);
            if (error != null) {
                throw new UnsatisfiedLinkError(error);
            }
            return;
        }

        ...
    }
複製代碼

loadLibrary0 方法主要兩個步驟,一、查找so是否存;二、調用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) {
        //一、名稱映射,至關於轉換,例如過濾一些空格啥的
        String fileName = System.mapLibraryName(libraryName);
        //二、應用全部so庫應該都放在nativeLibraryPathElements裏了,遍歷一下
        for (NativeLibraryElement element : nativeLibraryPathElements) {
            //三、從 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();
  {
    //一、讀緩存,第一次加載成功會放緩存
    library = libraries_->Get(path);
  }
...
  //有緩存的狀況下
  if (library != nullptr) {
    // 二、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;
    }
    //三、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.
  ...
  //四、打開共享庫,從註釋看,最終是會經過 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(
        //五、打開so成功以後,建立一個 SharedLibrary,handle做爲參數之一
        new SharedLibrary(env,
                          self,
                          path,
                          handle,
                          needs_native_bridge,
                          class_loader,
                          class_loader_allocator));
    //六、加到緩存中
    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;
  //七、找找看是否重寫了 JNI_OnLoad 方法
  void* sym = library->FindSymbol("JNI_OnLoad", nullptr);
  //八、沒有重寫,成功標誌爲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
    // 九、有重寫 JNI_OnLoad 函數,調用它
    ...
    //十、調用 JNI_OnLoad 函數,返回JNI版本號
    int version = (*jni_on_load)(this, nullptr);
    ...
    // 十一、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 的邏輯:
註釋一、二、3,讀緩存,若是以前已經加載過so,那麼判斷ClassLoader是否是一致,不一致就返回失敗,一致就返回成功,不容許一個so被兩個ClassLoader同時加載。
註釋四、五、6:經過dlopen函數打開so,成功則建立對應的SharedLibrary 對象,而且加到緩存中。
註釋七、八、九、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方法,進行調用。

5、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)
{
    ...省略其它代碼
    // 一、判斷是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.
         */
         // 二、調用 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)
{
    ... 

    // 一、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.
     */
     // 一、對於靜態方法,要先確保該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 */
    //二、從內部的本地方法表查詢
    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 */
    //三、 從動態連接庫中查詢
    void* func = lookupSharedLibMethod(method);
    if (func != NULL) {
        /* found it, point it at the JNI bridge and then call it */
        //四、找到就調用
        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

//一、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);
    // 二、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;
                // 三、匹配到對應方法,返回
                if (dvmCompareNameDescriptorAndMethod(pMeth->name,
                    pMeth->signature, method) == 0)
                {
                    /* match */
                    //ALOGV("+++ match on %s.%s %s at %p",
                    //    className, methodName, methodSignature, pMeth->fnPtr);
                    //四、返回這個方法
                    return pMeth->fnPtr;
                }

                pMeth++;
            }
        }
		 
        // 五、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 */
};
複製代碼

再回頭看數組的初始化

//一、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)
{
	// 一、動態庫集合裏面放的就是 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.
     */
    // 二、經過native方法名,獲取對應的JNI方法名
    preMangleCM =
        createJniNameString(meth->clazz->descriptor, meth->name, &len);
    if (preMangleCM == NULL)
        goto bail;

	//三、對JNI方法進行處理,轉換成so中對應的格式
    mangleCM = mangleString(preMangleCM, len);
    if (mangleCM == NULL)
        goto bail;

    ALOGV("+++ calling dlsym(%s)", mangleCM);
    // 四、經過 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卡頓過嗎?
面試官:今日頭條啓動很快,你以爲多是作了哪些優化?

相關文章
相關標籤/搜索