以前不少次看到網易雲課程廣告,裏面有個熟悉的標題是這樣的html
性能優化之NDK高效加載GIF--NDK開發實戰 三個小節以下:java
這個廣告標題已經看到過好屢次了,是一個進階課程,那麼確定是收費的,我想不花錢就能學會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
直接進入正文吧~
這一塊基本沒太大難度的,只是大部分Android開發都是作應用層業務開發,不多涉及到動手寫c++代碼,因此以爲JNI、NDK是很高級的技術,曾經的我也是這麼認爲的。
是否是要會C++? 會確定最好,不會的話,用到的時候學也能夠。
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基礎
giflib 是啥呢?
經過搜索引擎,發現 giflib是android源碼中的一個用C語言寫的加載GIF的庫
把.c 和 .h 結尾的文件下載下來,放到giflib文件夾中備用
先把giflib集成到Android Studio項目中先~
上一步 giflib 已是下載下來了
爲何要新建,主要是考慮到部分同窗對cmake 語法不清楚,因此經過新建項目來熟悉它,
Android Studio 新建 Native C++ 項目
項目名就叫 GifLoaderTest
而後next,finish,
等待同步和編譯完成(可能會提示安裝cmake,按提示安裝便可),這個是能夠運行成功的帶有JNI基本配置的項目。
看下 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集成進去:
對於註釋3,log、jnigraphics、android,這幾個依賴在哪裏呢?答案是在NDK目錄下,例如個人mac是在這個目錄
/Users/{用戶名}/Library/Android/sdk/ndk-bundle/platforms/android-27/arch-arm/usr/lib
NDK工具包提供了一些依賴庫給咱們使用,log 是在控制檯打印日誌,jnigraphics 是圖像操做相關。
定義一個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);
複製代碼
鼠標放在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()
}
複製代碼
各個方法按順序講解:
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層能夠經過傳這個地址過來獲取寬高,解析幀數據啥的
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便可。
這個方法是用來解析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中的數據是有順序的塊,每個塊表明的是當前幀的圖片數據,或者圖片數據的描述,例如延時多少毫秒這些額外數據。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的幾個步驟簡單總結一下:
核心代碼已經貼出,源碼放github,沒有任何封裝,只適合學習參考~
從上面的流程來看,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++方法的?
不少Android開發即便沒接觸過NDK,可是對於 System.loadLibrary("native-lib");
應該不陌生,是否知道其中原理呢?
public static void loadLibrary(String libname) {
Runtime.getRuntime().loadLibrary0(Reflection.getCallerClass(), libname);
}
複製代碼
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
protected String findLibrary(String libname) {
return null;
}
複製代碼
ClassLoader是抽象類,實現類是BaseDexClassLoader
public String findLibrary(String name) {
return pathList.findLibrary(name);
}
複製代碼
pathList 是 BaseDexClassLoader裏的一個DexPathList對象,裏面存放dex數組,以前一篇關於啓動優化的文章講MultiDex原理的時候有分析過 findClass
方法,今天要分析的是 findLibrary
方法
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
方法
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下載下來,拷貝到私有目錄,/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
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源碼分析,其它版本可能會有些差別,但原理是同樣的)
簡單說就是兩個步驟:
dlopen
這個系統函數去打開so,成功則會建立 SharedLibrary 對象而且緩存起來,後面調用native方法按道理應該是從這個SharedLibrary中查找是否有某個JNI方法,進行調用。當簡歷上寫熟悉JNI、NDK開發的時候,有很大的機率面試官會問這個問題,你熟悉JNI,到底熟悉到哪一個程度,native方法調用原理知道不,如何找到對應的JNI方法的呢?
這個部分能講清楚的文章不多了,我只能花點時間總結一下了~
首先作一個測試,寫一個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
經過查看字節碼能夠看到:
invokevirtual
指令。無論是調用普通方法仍是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方法的對應關係~
JNI方法的註冊方式有兩種,默認的和動態註冊,
默認狀況下,JVM加載一個類的時候,會調用native層Class對象的loadClassFromDex
方法,而這個方法內部會調用loadMethodFromDex
方法去加載Class對象內部的方法,當遇到native方法,就會對nativeFunc函數進行賦值,能夠在源碼中獲得驗證
//調用鏈
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 這個方法,
這個方法定義在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方法呢?
分兩種狀況
先看下 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方法,繼續吧~
//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 是一個動態庫集合,先假設 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方法的調用原理小結以下:
invokevirtual
指令;dlsym
函數查找動態庫中是否有該方法。若是是在面試中問到native方法調用原理,那麼最好能先說System.loadLibrary原理:找到so,再經過dlopen
函數去打開so,返回一個句柄,保存到動態庫集合中;native 方法的調用會先從內部本地方法表查找,找不到再遍歷這個動態庫集合,經過dlsym
函數查找動態庫中是否有對應的JNI方法,有的話就將native方法跟JNI方法創建連接並調用。
這篇文章從網易雲的進階課程廣告入手:
dlopen
函數打開動態庫,返回handle句柄,添加到動態庫集合中,還會調用JNI_OnLoad 函數(若是有的話);引伸了動態加載so的方式。相關參考連接:
Android 使用系統庫giflib實現高效gif動畫加載
android app動圖優化:源碼giflib加載gif動圖,性能秒殺glide
圖像解碼之三——giflib解碼gif圖片
因爲換了新工做,這篇文章應該是2019年最後一篇啦~
接下來個人計劃是系統學習下Flutter,掘金沒有像簡書那樣的歸檔功能,因此Flutter文章會先在簡書發佈。
面試官系列文章暫時告一段落吧,下一篇文章是什麼內容還不知道呢,當我發現一個知識點是本身不會的,可能就會花時間去深刻研究它,做爲下一篇文章的主題~
你們有任何問題歡迎在評論區留言~
我在掘金髮布的其它5篇文章:
面試官:簡歷上最好不要寫Glide,不是問源碼那麼簡單
總結UI原理和高級的UI優化方式
面試官:說說多線程併發問題
面試官又來了:你的app卡頓過嗎?
面試官:今日頭條啓動很快,你以爲多是作了哪些優化?