[讀書筆記] 深入探索Android熱修復技術原理 (手淘技術團隊)?二

資源修復技術詳解

接上篇:深入探索Android熱修復技術原理 (手淘技術團隊)

Android資源的熱修復,就是在app不重新安裝的情況下嘹履,利用下發(fā)的補丁包直接更新app中的資源。

目前市面上的很多熱修復方案都參考了Instant run的實現(xiàn)童本。

下面來簡單看一下instant run方案是怎么做資源熱修復的。

Instant Run中的資源修復分為兩步,

  1. 構(gòu)造新的AssetManager钱磅,并通過反射調(diào)用addAssetPath,把這個完整的新資源包加入到AssetManager中似枕。這樣就得到了一個含有所有新資源的AssetManager盖淡。
  2. 找到所有之前引用到原有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。

一個好的資源修復方案

從前面分析可以得出一個好的資源修復方案需要滿足:

  1. 資源包足夠小
  2. 不侵入打包流程

簡單來說梗肝,Sophix提出的方案滿足了上面的要求:

  1. 構(gòu)造一個package id為0x66的資源包榛瓮,這個包里面只包含改變了的資源項,
  2. 在原有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+類完整路徑+方法名的格式病梢。

native方法映射邏輯

總結(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實時修復填抬。

動態(tài)注冊native方法實時生效

實測發(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.hdalvik/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ù)組遍歷

靜態(tài)注冊native方法實時生效

由于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加載策略如下:

  1. 如果存在則加載補丁so庫
  2. 如果不存在揭鳞,那么調(diào)用System.loadLibrary加載安裝apk目錄下的so庫
接口調(diào)用替換實現(xiàn)

雖然此方案實現(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變量所表示的目錄下搜索

反射注入實現(xiàn)

這個方式有點像DexElements數(shù)組的處理

————————

至此

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末乓梨,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子清酥,更是在濱河造成了極大的恐慌扶镀,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,406評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件焰轻,死亡現(xiàn)場離奇詭異臭觉,居然都是意外死亡,警方通過查閱死者的電腦和手機鹦马,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,732評論 3 393
  • 文/潘曉璐 我一進店門胧谈,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人荸频,你說我怎么就攤上這事菱肖。” “怎么了旭从?”我有些...
    開封第一講書人閱讀 163,711評論 0 353
  • 文/不壞的土叔 我叫張陵稳强,是天一觀的道長。 經(jīng)常有香客問我和悦,道長退疫,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,380評論 1 293
  • 正文 為了忘掉前任鸽素,我火速辦了婚禮褒繁,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘馍忽。我一直安慰自己棒坏,他們只是感情好燕差,可當我...
    茶點故事閱讀 67,432評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著坝冕,像睡著了一般徒探。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上喂窟,一...
    開封第一講書人閱讀 51,301評論 1 301
  • 那天测暗,我揣著相機與錄音,去河邊找鬼磨澡。 笑死碗啄,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的钱贯。 我是一名探鬼主播挫掏,決...
    沈念sama閱讀 40,145評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼秩命!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起褒傅,我...
    開封第一講書人閱讀 39,008評論 0 276
  • 序言:老撾萬榮一對情侶失蹤弃锐,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后殿托,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體霹菊,經(jīng)...
    沈念sama閱讀 45,443評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,649評論 3 334
  • 正文 我和宋清朗相戀三年支竹,在試婚紗的時候發(fā)現(xiàn)自己被綠了旋廷。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,795評論 1 347
  • 序言:一個原本活蹦亂跳的男人離奇死亡礼搁,死狀恐怖饶碘,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情馒吴,我是刑警寧澤扎运,帶...
    沈念sama閱讀 35,501評論 5 345
  • 正文 年R本政府宣布,位于F島的核電站饮戳,受9級特大地震影響豪治,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜扯罐,卻給世界環(huán)境...
    茶點故事閱讀 41,119評論 3 328
  • 文/蒙蒙 一负拟、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧歹河,春花似錦掩浙、人聲如沸琉挖。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,731評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽示辈。三九已至,卻和暖如春遣蚀,著一層夾襖步出監(jiān)牢的瞬間矾麻,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,865評論 1 269
  • 我被黑心中介騙來泰國打工芭梯, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留险耀,地道東北人。 一個月前我還...
    沈念sama閱讀 47,899評論 2 370
  • 正文 我出身青樓玖喘,卻偏偏與公主長得像甩牺,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子累奈,可洞房花燭夜當晚...
    茶點故事閱讀 44,724評論 2 354

推薦閱讀更多精彩內(nèi)容