資源修復技術詳解
Android資源的熱修復,就是在app不重新安裝的情況下嘹履,利用下發(fā)的補丁包直接更新app中的資源。
目前市面上的很多熱修復方案都參考了Instant run的實現(xiàn)童本。
下面來簡單看一下instant run方案是怎么做資源熱修復的。
Instant Run中的資源修復分為兩步,
- 構(gòu)造新的AssetManager钱磅,并通過反射調(diào)用addAssetPath,把這個完整的新資源包加入到AssetManager中似枕。這樣就得到了一個含有所有新資源的AssetManager盖淡。
- 找到所有之前引用到原有AssetManager的地方,通過反射凿歼,把引用處替換為AssetManager褪迟。
一個Android進程只包含一個ResTable,ResTable的成員變量mPackageGroups就是所有解析過的資源包的集合答憔。任何一個資源包中都含有resources.arsc味赃,它記錄了所有資源id分配情況以及資源中的所有字符串。這些信息是以二進制方式存儲的虐拓。底層的AssetManager的職責就是解析這個文件心俗,然后把相關信息存儲到mPackageGroups里面。
資源文件的格式
整個resources.arsc文件蓉驹,實際上是有一個個ResChunk(簡稱chunk)拼接起來的城榛。
從文件頭開始,每個chunk的頭部都是一個ResChunk_header結(jié)構(gòu)态兴,它指示了這個chunk的大小和數(shù)據(jù)類型狠持。
/**
* Header that appears at the front of every data chunk in a resource.
*/
struct ResChunk_header {
// Type identifier for this chunk. The meaning of this value depends
// on the containing chunk
uint16_t type;
// Size of the chunk header (in bytes). Adding this value to
// the address of the chunk allows you to find its associated data
// (if any)
uint16_t headerSize;
// Total size of this chunk (in bytes). this is the chunkSize plus
// the size of any data associated with the chunk. Adding this value
// to the chunk allow you to completely skip its contents (including
// any child chunks). If this value is the same as chunkSize, there is
// no data associated with the trunk.
uint32_t size;
}
通過ResChunk_header中的type成員,可以知道這個chunk是什么類型瞻润,從而就知道應該如何解析這個chunk喘垂。
解析完一個chunk后,從這個chunk+size的位置開始绍撞,就可以得到下一個chunk的其實位置正勒,這樣就可以一次讀取完這個文件的數(shù)據(jù)內(nèi)容。
一般來說傻铣,一個resources.arsc里面包含若干個package章贞,不過默認情況下,由打包工具aapt打出來的包只有一個package矾柜。這個package里包含了app中的所有資源信息阱驾。
資源信息主要是指每個資源的名稱以及它對應的編號。我們知道怪蔑,Android中的每一個資源都有它唯一的編號。
編號是一個32位數(shù)字丧荐,用十六進制來表示就是0xPPTTEEEE缆瓣。
其中PP為package id,TT為type id虹统,EEEE為entry id弓坞。
運行時資源解析
默認由Android SDK編出來的apk隧甚,是由aapt工具進行打包的,其資源的package id就是0x7f渡冻。
系統(tǒng)的資源包戚扳,是framework-res.jar,package id為0x01族吻。
在走到app的第一行代碼之前帽借,系統(tǒng)就已經(jīng)幫我們構(gòu)造好一個已經(jīng)添加了安裝包資源的AssetManager了。
@frameworks/base/core/java/android/app/ResourcesManager.java
Resources getTopLevelResources(String resDir, String[] splitResDirs, String[] overlayDirs,
String[] libDirs, int displayId, Configuration overrideConfiguration, CompatibilityInfo compatInfo) {
... ...
AssetManager assets = new AssetManager();
// resDir就是安裝包apk
if (resDir != null) {
if (assets.addAssetPath(resDir) == 0) {
return null;
}
}
... ...
}
因此超歌,這個AssetManager里就已經(jīng)包含了系統(tǒng)資源以及app的安裝包砍艾,就是package id為0x01的framework-res.jar中的資源和package id為0x7f的app安裝包資源。
為什么資源無法像dex一樣addPath修改原有的AssetManager
如果此時直接在原有AssetManager上繼續(xù)addAssetPath的完整補丁包的話巍举,由于補丁包里面package id也是0x7f脆荷,就會使得同一個package id的包被加載兩次,這會有怎樣的問題呢懊悯?
在Android L之后蜓谋,這是沒問題的,他會默默地把后來的包添加到之前的包同一個PackageGroup下面炭分。
而在解析的時候孤澎,會與之前的包比較同一個type id鎖對應的類型,如果該類型下的資源項數(shù)目和之前添加過的不一致欠窒,會打出一條warning log覆旭,但是仍舊加入到該類型的TypeList中。
status_t ResTable::parsePackage(const ResTable_package* const pkg, const Header* const header) {
... ...
TypeList& typeList = group->types.editItemAt(typeIndex);
if (!typeList.isEmpty()) {
const Type* existingType = typeList[0];
if (existingType->entryCount != newEntryCount && idmapIndex < 0) {
ALOGW("ResTable_typeSpec entry count inconsistent: given %d, previously %d",
(int)newEntryCount, (int)existingType->entryCount);
// We should normally abort here, but some legacy apps declare
// resources in the 'android' package (old bug in AAPT).
}
}
Type* t = new Type(header, package, newEntryCount);
t->typeSpec = typeSpec;
t->typeSpecFlag = (const uint32_t*) (((const uint8_t*)typeSpec) + dtohs(typeSpec->head.headerSize));
if (idmapIndex >= 0) {
t->idmapEntries = idmapEntries[idmapIndex];
}
typeList.add(t);
... ...
}
但是在get這個資源的時候呢岖妄?
status_t ResTable::getEntry(const PackageGroup* packageGroup,
int typeIndex, int entryIndex, const ResTable_config* config, Entry* outEntry) const {
const TypeList& typeList = packageGroup->type[typeIndex];
... ...
// %% 從第一個type開始遍歷型将,也就是說會先取得安裝包的資源,然后才是補丁包的荐虐。
// Iterate over the Types of each package.
const size_t typeCount = typeList.size();
for (size_t i = 0; i < typeCount; ++i) {
const Type* const typeSpec = typeList[i];
int realEntryIndex = entryIndex;
int realTypeIndex = typeIndex;
bool currentTypeIsOverlay = false;
if (static_cast<size_t>(realEntryIndex) >= typeSpec->entryCount) {
ALOGW("For resource 0x%08x, entry index(%d) is beyond type entryCount(%d)",
Res_MARKID(packageGroup->id-1, typeIndex, entryIndex), entryIndex, static_cast<int>(typeSpec->entryCount));
// We should normally abort here, but some legacy apps declare
// resources in the 'android' package (old bug in AAPT).
continue;
}
const size_t numConfigs = typeSpec->configs.size();
for(size_t c = 0; c < numConfigs; c++) {
... ...
if (bestType != NULL) {
// Check if this one is less specific than the last found. If so,
// we will skip it. We check starting with things we most care
// about to those we least care about.
if (!thisConfig.isBetterThan(bestConfig, config)) {
if (!currentTypeIsOverlay || thisConfig.compare(bestConfig) != 0) {
continue;
}
}
}
bestType = thisType;
bestOffset = thisOffset;
bestConfig = thisConfig;
bestPackage = thisSpec->package;
actualTypeIndex = realTypeIndex;
// If no config
if (config == NULL) {
break;
}
}
}
}
在獲取某個Type的資源時七兜,會從前往后遍歷,也就是說先得到原有安裝包里的資源福扬,除非后面的資源config比前面的更詳細才會發(fā)生覆蓋腕铸。而對于同一個config而言,補丁中資源就永遠無法生效了铛碑。所以在Android L以上的版本狠裹,在原有AssetManager上加入 補丁包 ,是沒有任何作用的汽烦,補丁中的資源無法生效涛菠。
而在Android4.4及以下版本,addAssetPath只是把補丁包的路徑添加到了mAssetPath中,而真正解析的資源包的邏輯是在app第一次執(zhí)行AssetManager::getResTable的時候俗冻。
@android-4.4.4_r2/frameworks/base/libs/andridfw/AssetManager.cpp
const ResTable* AssetManager::getResTable(bool required) const {
// %%mResources 已存在礁叔,直接返回,不再往下走迄薄。
ResTable* rt = mResources;
if (rt) {
return rt;
}
// Iterate through all asset packages, collecting resources from each.
AutoMutex _l(mLock);
if (mResource != NULL) {
return mResource;
}
if (required) {
LOG_FATAL_IF(mAssetPathPaths.size() == 0, "No assets added to AssetManager");
}
if (mCacheMode != CACHE_OFF && !mCacheValid) {
const_cast<AssetManager*>(this)->loadFileNameCacheLocked();
}
const size_t N = mAssetPaths.size();
for (size_t i = 0; i < N; ++i) {
// ...%% 真正解析package的地方...
}
if (required && !rt) {
ALOGW("Unable to find resources find resources.arsc");
}
if (!rt) {
mResources = rt = new ResTable();
}
return rt;
}
而在執(zhí)行到加載補丁代碼的時候琅关,getResTable已經(jīng)執(zhí)行過了無數(shù)次了。
這是因為就算我們之前沒做過任何資源相關操作讥蔽,Android Framework里的代碼也會多次調(diào)用到這里涣易。
所以,以后即使是addAssetPath勤篮,也只是添加到了mAssetPath都毒,并不會發(fā)生解析。所以補丁包里面的資源是完全不生效的碰缔。
所以账劲,像instant run這種方案,一定需要一個全新的AssetManager時金抡,然后再加入完整的新資源包瀑焦,替換掉原有的AssetManager。
一個好的資源修復方案
從前面分析可以得出一個好的資源修復方案需要滿足:
- 資源包足夠小
- 不侵入打包流程
簡單來說梗肝,Sophix提出的方案滿足了上面的要求:
- 構(gòu)造一個package id為0x66的資源包榛瓮,這個包里面只包含改變了的資源項,
- 在原有AssetManager中addAssetPath這個包巫击。
沒錯禀晓!就是這么簡單。
由于補丁包的package id為0x66坝锰,不與目前已經(jīng)0x7f沖突粹懒,因此直接加入到已有的AssetManager中就可以直接使用了。
補丁包里的資源顷级,只包含原有包里面沒有而新包里面有的新增資源以及原有內(nèi)容發(fā)生了改變的資源凫乖。
更加優(yōu)雅的替換AssetManager
對于Android L以后的版本,直接在原有AssetManager上應用patch就行了弓颈。
并且由于用的是原來的AssetManager帽芽,所以原先大量的反射替換操作就完全不需要了,大大提高了補丁加載生效的效率翔冀。
但之前提到過Android 4.4和以下版本导街,addAssetPath是不會加載資源的,必須重新構(gòu)造一個新的AssetManager并加入patch橘蜜,再替換掉原來的菊匿。
** 那么如何省掉版本兼容和反射替換的工作呢付呕? **
其實在AssetManager源碼里面有一個有趣的東西计福。
@frameworks/base/core/java/android/content/res/AssetManager.java
public final class AssetManager {
... ...
private native final void destroy();
... ...
}
很明顯跌捆,這個是用來銷毀AssetManager并釋放資源的函數(shù),我們來看看它具體做了什么
static void android_content_AssetManager_destroy(JNIEnv* env, jobject clazz) {
AssetManager* am = (AssetManager*)(env->GetIntField(clazz, gAssetManagerOffsets.mObject));
ALOGV("Destroying AssetManager %p for Java Object %p\n", am, clazz);
if (am != NULL) {
delete am;
env->setIntField(clazz, gAssetManagerOffsets.mObject, 0);
}
}
可以看到象颖,它首先析構(gòu)了native層的AssetManager佩厚,然后把java層的AssetManager對native層的引用置為空。
AssetManager::~AssetManager(void) {
int count = android_atomic_dec(&gCount);
// ALOGI("Destroying AssetManager inn %p #%d\n", this, count);
delete mConfig;
delete mResource;
// don't have a String class yet, so make sure we clean up
delete[] mLocate;
delete[] mVendor;
}
native層的AssetManager析構(gòu)函數(shù)會析構(gòu)它的所有成員说订,這樣機會釋放之前加載了的資源抄瓦。
而現(xiàn)在,java層的AssetManager已經(jīng)成了空殼陶冷。我們可以調(diào)用它的init方法钙姊,對它重新初始化了!
@frameworks/base/core/java/android/content/res/AssetManger.java
public final class AssetManager {
... ...
private native final void init();
... ...
}
static void android_content_AssetManager_init(JNIEnv* env, jobject clazz) {
AssetManager* am = new AssetManager();
if (am == NULL) {
jniTHrowException(env, "java/lang/OutOfMemoryError", "");
return;
}
am->addDefaultAssets();
ALOGV("Created AssetManager %p for Java object %p\n", am, clazz);
env->setIntField(clazz, gAssetManagerOffsets.mObject, (jint)am);
}
在執(zhí)行init的時候埂伦,會在native層創(chuàng)建一個沒有添加過資源并且mResource沒有初始化的AssetManager煞额。然后我們再對它進行addAssetPath,之后由于mResoure沒有初始化過沾谜,就可以正常走到解析mResource的邏輯膊毁,加載所有add進去的資源了!
核心實現(xiàn)代碼如下:
... ...
// 反射關鍵方法
Method initMeth = assetManagerMethod("init");
Method destroy = assetManagerMethod("destroy");
Method addAssetPathMeth = assetManager("addAssetManager", String.class);
// 析構(gòu)AssetManager
destroyMeth.invoke();
// 重新構(gòu)造AssetManager
initMeth.invoke();
// 置空mStringBlocks
assetManagerField("mStringBlocks").set(am, null);
// 重新添加原有AssetManager中加載過的資源路徑
for (String path : loadedPaths) {
LogTool.d(TAG, "pexyResources" + path);
addAssetPathMeth.invoke(am, path);
}
// 添加patch資源路徑
addAssetPathMeth.invoke(am, patchPath);
// 重新對mStringBlocks賦值基跑,mStringBlocks記錄了之前加載過的所有資源包的String Pool婚温,
// 因此很多時候訪問字符串是通過它來找到的,如果不進行重新構(gòu)造媳否,在后面使用的時候會導致崩潰
assetManagerMethod("ensureStringBlocks").invoke(am);
... ...
由于我們直接是對原有的AssetManager進行析構(gòu)和重構(gòu)栅螟,所有原先對AssetManager對象的引用時沒有發(fā)生變化的,這樣就不需要想Instant Run那樣進行繁瑣的修改了篱竭。
So庫修復技術詳解
so庫加載原理
Java Api提供了兩個接口來加載so庫:
- System.loadLibrary(String libName):傳進去的參數(shù):so庫名稱力图。表示so庫文件位于apk壓縮文件中的libs目錄,最后復制到apk安裝目錄下室抽。
- System.load(String pathName):傳進去的參數(shù):so庫在磁盤中的完整路徑搪哪。記載一個自定義外部so文件。
上述兩個方式加載一個so庫坪圾,實際上最后都調(diào)用nativeLoad
這個native方法去加載so庫晓折,這個方法的參數(shù)是so庫在磁盤中的完整路徑名。
public class MainActivity extends Activity {
static {
System.loadLibrary("jnitest");
}
public static native String stringFromJNI();
public static native void test();
}
// 靜態(tài)注冊stringFromJNI本地方法
extern "c" jstring Java_com_taobao_jni_MainActivity_stringFromJNI(JNIEnv* env, jclass clazz) {
std::string hello = "jni stringFrom JNI old.... ";
return env->NewStringUTF(hello.c_str());
}
// 動態(tài)注冊test方法
void test(JNIEnv* env, jclass clazz) {
LOGD("jni test old.... ");
}
JNINativeMethod nativeMethods[] = {
{"test", "()V", (void *)test}
};
#define JNIREG_CLASS "com/taobao/jni/MainActivity"
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) {
LOGD("old JNI_OnLoad");
....
jclass clz = env->FindClass(JNIREG_CLASS);
if (env->RegisterNatives(clz, nativeMethods, sizeof(nativeMethods)/sizeof(nativeMethod[0])) != JNI_OK) {
return JNI_ERR;
}
return JNI_VERSION_1_4;
}
我們知道JNI編程中兽泄,動態(tài)注冊的native方法必須實現(xiàn)JNI_OnLoad方法漓概,同時實現(xiàn)一個JNINativeMethod[]數(shù)組,靜態(tài)注冊的native方法必須是Java+類完整路徑+方法名的格式病梢。
總結(jié)下:
- 動態(tài)注冊的native方法映射通過加載so庫過程中調(diào)用JNI_OnLoad方法調(diào)用完成胃珍。
- 靜態(tài)注冊的native方法映射是在該native方法第一次執(zhí)行的時候才完成映射梁肿。
動態(tài)注冊native方法實時生效
我們知道動態(tài)注冊的native方法調(diào)用一次JNI_OnLoad都會重新完成一次映射,所以我們需要先加載原來的so庫觅彰,然后再加載補丁so庫吩蔑,就能完成Java層native方法到native層patch后的新方法映射,這樣就完成了動態(tài)注冊native方法的patch實時修復填抬。
實測發(fā)現(xiàn):
ART虛擬機下這樣做是可以做到實時生效的烛芬,但是Dalvik下做不到實時生效。
實際上Dalvik下第二次load補丁so庫飒责,執(zhí)行的仍然是原來的so庫的JNI_OnLoad方法赘娄,所以Dalvik下做不到實時生效。
既然拿到的是so庫的JNI_OnLoad方法宏蛉,那么我們首先懷疑一下兩個函數(shù)是否有問題:
- dlopen():返回給我們一個動態(tài)鏈接庫的句柄
- dlsym():通過dlopen得到的動態(tài)鏈接庫句柄遣臼,來查找一個symbol
源碼在/bionic/linker/dlfcn.cpp
文件,方法調(diào)用鏈為:dlopen->do_dlopen->find_library->find_library_internal
static soinfo* find_library_internal(const char* name) {
soinf* si = find_loaded_library(name);
if (si != NULL) { // so已經(jīng)加載過
if (si->flags & FLAG_LINKED) {
return si; // 直接返回該so庫的句柄
}
DL_ERROR("OOPS: recursive link to \"%s\" ", si->name);
return NULL;
}
TRACE("[ '%S' has not been loaded yes. Locating...] ", name);
si = load_library(name); // so庫從未加載過拾并,load_library執(zhí)行加載
if (si == NULL) {
return NULL;
}
return si;
}
find_loaded_library
方法判斷那么表示的so庫是否已經(jīng)被加載過揍堰,如果加載過直接返回之前的句柄,否則就調(diào)用load_library
嘗試加載so庫辟灰。
static soinfo* find_loaded_library(const char* name) {
soinfo* si;
const char* bname;
// TODO: don't use basename only for determining libraries
// http://code.google.com/p/android/issues/detail?id=6670
bname = strrchr(name, '/');
bname = bname ? bname + 1 : name;
for (si = solist; si != NULL; si = si->next) {
if (!strcmp(bname, si->name)) {
return si;
}
}
return NULL;
}
看代碼注釋个榕,也知道其實這是Dalvik虛擬機下的一個bug,這里是通過base那么去做查找芥喇,傳進來的name實際上是so庫所在磁盤的完整路徑西采。
比如此時修復后的so庫路徑為/data/data/com.taobao.jni/files/libnative-lib.so
。但是此時通過bname:libnative-lib.so
作為key去查找继控,因此補丁包和原包文件命名一致的話械馆,就會發(fā)生新庫永遠無法生效。
因此在Dalvik虛擬機下武通,嘗試對補丁包so進行重命名霹崎,確保bname是全局唯一的,才能做到Dalvik環(huán)境下的動態(tài)注冊的native方法實時生效冶忱。
靜態(tài)注冊native方法實時生效
前面說過靜態(tài)注冊的native方法的攝影是在native方法第一次執(zhí)行的時候就完成了映射尾菇,所以如果native方法在加載補丁so庫之前已經(jīng)執(zhí)行過,那么是否這種時候的靜態(tài)注冊的native方法一定無法修復嗎囚枪?
幸運的是派诬,系統(tǒng)JNI API提供了注冊的接口。
static jint UnregisterNatives(JNIEnv* env, jclass jclazz) {
ClassObject* clazz = (ClassObject*) dvmDecodeIndirectRef(ts.self(), jclazz);
dvmUnregisterJNINativeMethods(clazz);
return JNI_OK;
}
/*
* Un-register all JNI native methods from a class
*/
void dvmUnregisterJNINativeMethods(ClassObject* clazz) {
unregisterJNINativeMethods(clazz->directMethods, clazz->directionMethodCount);
unregisterJNINativeMethods(clazz->virtualMethods, clazz->virtualMethodCount)
}
static void unregisterJNINativeMethods(Method* methods, size_t count) {
while(count != 0) {
count--;
Method* meth = &methods[count];
if (!dvmIsNativeMethod(meth)) {
continue;
}
if (dvmIsAbstractMethod(meth)) { /* avoid abstract method stubs */
continue;
}
dvmSetNativeFunc(meth, dvmResolveNativeMethod, NULL); // meth->nativeFunc重新指向dvmResolveNativeMethod
}
}
UnregisterNatives
函數(shù)會啊jclazz
所在類的所有native方法都重新指向為dvmResolveNativeMethod
链沼,所以調(diào)用UnregisterNatives
之后不管是靜態(tài)注冊還是動態(tài)注冊native
方法默赂、之前是否執(zhí)行過,在加載補丁so的時候都會重新去做映射括勺。
所以我們只需要調(diào)用:
static void patchNativeMethod(JNIEnv *env, jclass clz) {
env->UnregisterNatives(clz);
}
這里有一個難點缆八,因為native方法是在so庫曲掰,所以補丁工具很難檢測出到底是哪個Java類需要解除native方法的注冊。 這個問題暫且放下奈辰。
假設我們現(xiàn)在可以知道哪個具體的Java類需要解除注冊native方法失暴,然后load補丁庫回窘,再次執(zhí)行該native方法纸厉,按照道理來說是可以讓native方法實時生效莽囤,但是這里有個坑咙鞍。
問題現(xiàn)象:
在上面動態(tài)注冊native方法補丁實時生效的部分說過so庫需要重命名房官,測試發(fā)現(xiàn)重命名后的so文件時而生效時而不生效
首先,靜態(tài)注冊的native方法之前從未執(zhí)行過的話或者調(diào)用了UnregisterJNINativeMethods
方法解除注冊续滋,那么該方法將指向dvmResolveNativeMethod(meth->nativeFunc = dvmesolveNativeMethod
)翰守,那么真正運行該方法的時候,實際上執(zhí)行的是dvmResolveNative方法疲酌。
此函數(shù)主要是完成Java層native方法和native層方法的映射邏輯蜡峰。
void dvmResolveNativeMethod(const u4* args, JValue* pResult, const Method* method, Thread* self) {
void* func = lookupSharedLibMethod(method);
... ...
if (func != NULL) {
// 調(diào)用lookupSharedLibMethod方法,拿到so庫文件對應的native方法函數(shù)指針朗恳。
dvmUseJNIBridage((Method*) method, func);
(*method->nativeFunc)(args, pResult, method, self);
return;
}
... ...
dvmThrowUnstatisfiedLinkError("Native method not found", method);
}
static void* lookupSharedLibMethod(const Method* method) {
return (void*) dvmHashForeach(gDvm.nativeLibs, findMethodInLib, (void*) method);
}
int dvmHashForeach(HashTable* pHashTable, HashForeachFunc func, void* arg) {
int i, val, tableSize;
tableSize = pHashTable->tableSize;
for (i = 0; i < tableSize; i++) {
HashEntry* pEnt = &pHashTable->pEntries[i];
if (pEnt->data != NULL && pEnt->data != HASH_TOMBSTONE) {
val = (*func)(pEnt->data, arg);
if (val != 0) {
return val;
}
}
}
return 0;
}
gDvm.nativeLibs
是一個全局變量湿颅,它是一個HashTable,存放著整個虛擬機加載so庫的SharedLib結(jié)構(gòu)指針粥诫。
然后該變量作為參數(shù)傳遞給dvmHashForeach
函數(shù)進行HashTable遍歷油航。
執(zhí)行findMethodInLib
函數(shù)看是否找到對應的native函數(shù)指針,如果第一個就找到怀浆,就直接return谊囚。
這個結(jié)構(gòu)很重要,在虛擬機中大量使用到了HashTable這個數(shù)據(jù)結(jié)構(gòu)执赡,實現(xiàn)源碼在dalvik/vm/Hash.h
和 dalvik/vm/Hash.cpp
文件镰踏。
簡單說下Java的HashTable和這里的HashTable的異同點:
共同點:兩者實際上都是數(shù)組實現(xiàn),都是對key進行hash計算后跟hashtable的長度進行取模作為bucket沙合。
不同點:Dalvik虛擬機下的HashTable實現(xiàn)要比Java中的實現(xiàn)簡單一些奠伪。
Java中的HashTable的put操作要處理hash沖突的情況,一般情況下會在沖突節(jié)點上新增一個鏈表處理沖突首懈,然后get實現(xiàn)會遍歷鏈表
Dalvik下的HashTable的put操作只是簡單的把指針下移到下一個空間點绊率。get實現(xiàn)首先根據(jù)hash值算出bucket位置,然后比較是否一致猜拾,不一致的話即舌,指針下移,HashTable的遍歷實現(xiàn)就是數(shù)組遍歷
由于HashTable的實現(xiàn)方法以及dvmHashForeach
的遍歷實現(xiàn)挎袜,so注冊位置跟文件命名hash后的bucket值有關顽聂,如果順序靠前肥惭,那么生效的永遠是最前面的,而后面一直無法生效紊搪。
可見so庫實時生效方案蜜葱,對于靜態(tài)注冊的native方法有一定的局限性,不能滿足通用性耀石。
so庫冷部署重啟生效實現(xiàn)方案
為了更好的兼容通用性牵囤,我們嘗試通過冷部署重新生效的角度分析下補丁so庫的修復方案。
1滞伟、接口替換方案
提供接口替換System默認加載so庫接口
SoPatchManager.loadLibrary(String libName) -> 代替 System.loadLibrary(String libName)
自定義loadLibrary加載策略如下:
- 如果存在則加載補丁so庫
- 如果不存在揭鳞,那么調(diào)用
System.loadLibrary
加載安裝apk目錄下的so庫
雖然此方案實現(xiàn)簡單,但無法修改第三方包的so庫梆奈。
2野崇、反射注入方案
前面介紹過System.loadLibrary("native-lib")
,調(diào)用native層的時候參數(shù)就會包裝成/data/app-lib/com.taobao.jni-2/libnative-lib.so
亩钟,so庫會在DexPathList.nativeLibraryDirectories/nativeLibraryPathElements
變量所表示的目錄下搜索
這個方式有點像DexElements數(shù)組的處理
————————
至此