之前很多次看到網易雲課程廣告,裡面有個熟悉的標題是這樣的
效能優化之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卡頓過嗎?
面試官:今日頭條啟動很快,你覺得可能是做了哪些優化?