Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android 熱補丁技術——資源的熱修復

Android 熱補丁技術——資源的熱修復

編輯:關於Android編程

前言

今年真是熱補丁框架的洪荒之力爆發的一年,短短幾個月內,已經出現了好幾個熱修復的框架了,基本上都是大同小異,這裡我就不過多的去評論這些框架。只有自己真正的去經歷過,你才會發現其中的大寫的坑

事實上,現在出現的大多數熱修復的框架,穩定性和兼容性都還達不到要求,包括阿裡的Andfix,據同事說,我們自己的app原本沒有多少crash,接入了andfix倒引起了一部分的crash,這不是一個熱修復框架所應該具有的“變態功能”。雖然阿裡百川現在在大力推廣這套框架,我依舊不看好,只是其思路還是有學習價值的。

Dex的熱修復總結

Dex的熱修復目前來看基本上有四種方案:

阿裡系的從native層入手,見AndFix QQ空間的方案,插樁,見安卓App熱補丁動態修復技術介紹 微信的方案,見微信Android熱補丁實踐演進之路,dexDiff和dexPatch,方法很牛逼,需要全量插入,但是這個全量插入的dex中需要刪除一些過早加載的類,不然同樣會報class is pre verified異常,還有一個缺點就是合成占內存和內置存儲空間。微信讀書的方式和微信類似,見Android Patch 方案與持續交付,不過微信讀書是miniloader方式,啟動時容易ANR,在我錘子手機上變現出來特別明顯,長時間的卡圖標現象。 美團的方案,也就是instant run的方案,見Android熱更新方案Robust

此外,微信的方案是多classloader,這種方式可以解決用multidex方式在部分機型上不生效patch的問題,同時還帶來一個好處,這種多classloader的方式使用的是instant run的代碼,如果存在native library的修復,也會帶來極大的方便。

Native Library熱修復總結

而native libraray的修復,目前來說,基本上有兩種方案。。

類似multidex的dex方式,插入目錄到數組最前面,具體文章見Android熱更新之so庫的熱更新,需要處理系統的兼容性問題,系統分隔線是Android 6.0 第二種方式需要依賴多classloader,在構造BaseDexClassLoader的時候,獲取原classloader的native library,通過環境變量分隔符(冒號),將patch的native library與原目錄進行連接,patch目錄在前,這樣同樣可以達到修復的目的,缺點是需要依賴dex的熱修復,優點是應用native library時不需要處理兼容性問題,當然從patch中釋放出來的時候也需要處理兼容性問題。

第二種方式的實現可以看看BaseDexClassLoader的構造函數

 BaseDexClassLoader(String dexPath, File optimizedDirectory, String libraryPath, ClassLoader parent) 

只需要在修復dex的同時,如果有native library,則獲取原來的路徑與patch的路徑進行連接,偽代碼如下:

nativeLibraryPath = 獲取與原始路徑;
nativeLibraryPath = patchNativeLibraryPath + File.pathSeparator + nativeLibraryPath; 
IncrementalClassLoader inject = IncrementalClassLoader.inject(
                classLoader,
                nativeLibraryPath,
                optDir.getAbsolutePath(),
                dexList);

而這種方式需要強依賴dex的修復,如果沒有dex,就無能為例了,實際情況基本上是兩種方式交叉使用,在沒有dex的情況下,使用另外一種方式。

而native library還有一個坑,就是從patch中釋放so的過程,這個過程需要處理兼容性,在android 21以下,通過下面這個函數去釋放

com.android.internal.content.NativeLibraryHelper.copyNativeBinariesIfNeededLI

而在andrdod 21及以上,則通過下面的這幾個函數去釋放

com.android.internal.content.NativeLibraryHelper$Handle.create()
com.android.internal.content.NativeLibraryHelper.findSupportedAbi()
com.android.internal.content.NativeLibraryHelper.copyNativeBinaries()

資源的熱修復

而對於資源的熱修復,其實主要還是和插件化的思路是一樣的,具體實現可以參考兩個

Atlas或者攜程的插件化框架 Instant run的資源處理方式,甚至可以做到運行期立即生效。

本篇文章就來說說資源的熱修復的實現思路,在這之前,需要貼兩個鏈接,以下文章的內容基於這兩個鏈接去實現,所以務必先看看,不然會一臉懵逼。一個是instant run的源碼,自備梯子,另一個是馮老師寫的一個類,這個類在Atlas中出現過,後來被馮老師重寫了,同樣自備梯子。

重要的事情說三遍
自備梯子
自備梯子
自備梯子

instant-run源碼 Hack.java實現

資源的熱修復實現,主要由一下幾個步驟組成:

提前感知系統兼容性,不兼容則不進行後續操作 服務器端生成patch的資源,客戶端應用patch的資源 替換系統AssetManger,加入patch的資源

對於第一步,我們需要先看看instant run對於資源部分的實現,其偽代碼如下

AssetManager newAssetManager = new AssetManager();
newAssetManager.addAssetPath(externalResourceFile)

// Kitkat needs this method call, Lollipop doesn't. However, it doesn't seem to cause any harm
// in L, so we do it unconditionally.
newAssetManager.ensureStringBlocks();

// Find the singleton instance of ResourcesManager
ResourcesManager resourcesManager = ResourcesManager.getInstance();

// Iterate over all known Resources objects
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
    for (WeakReference wr : resourcesManager.mActiveResources.values()) {
        Resources resources = wr.get();
        // Set the AssetManager of the Resources instance to our brand new one
        resources.mAssets = newAssetManager;
        resources.updateConfiguration(resources.getConfiguration(), resources.getDisplayMetrics());
    }
}

代碼很簡單,通過調用addAssetPath將patch的資源加到新建的AssetManager對象中,然後將內存中所有Resources對象中的AssetManager對象替換為新建的AssetManager對象。當然還需要處理兼容性問題,對於兼容性問題,則需要用到馮老師的Hack類(這裡我為了與原來馮老師沒有重寫前的Hack類做區分,將其重命名了HackPlus,意思你懂的),具體Hack過程請參考Atlas或者攜程的插件化框架的實現,然後基於instant run進行實現,當然這種方式有一部分資源是修復不了了,比如notification

坑麼,你沒遇到,總是說沒有,遇到了,坑無數。

主要的分界線是Android 19 和 Android N

首先需要拿到App運行後內存中的Resources對象

Android N,通過ResourcesManager中的mResourceReferences去獲取Resources對象,是個ArrayList對象 Android 19到Android N(不含N),通過ResourcesManager中的mActiveResources去獲取Resources對象,是個ArrayMap對象 Android 19以下,通過ActivityThread的mActiveResources去獲取Resources對象,是個HashMap對象。 接著就是替換Resources中的AssetManager對象
Android N,替換的是Resources對象中的mResourcesImpl成員變量中的mAssets成員變量。 Android N以前,替換的是Resources對象中的mAssets成員變量。 對於Android 19以下,ActivityThread是通過ActivityThread中的靜態函數currentActivityThread獲取的的,這裡有個坑,如果在主線程獲取還好,但是萬一在子線程獲取,在低版本的Android上可能就是Null,因為在低版本,這個變量是通過ThreadLocal進行存儲的,對於這種情況,只要檢測當前線程是不是主線程,如果是主線程,則直接獲取,如果不是主線程,則阻塞當前線程,然後切換到主線程獲取,獲取完成後通知阻塞線程。

這裡我已經基本實現了反射檢測系統支持性相關的代碼,主要就是對以上分析的內容做反射檢測,一旦發生異常,則不再進行資源的修復,代碼如下(HackPlus的源碼見上面的Hack.java的源碼):

 mAssertionErr;

    public AssertionArrayException(String str) {
        super(str);
        this.mAssertionErr = new ArrayList();
    }

    public void addException(AssertionException hackAssertionException) {
        this.mAssertionErr.add(hackAssertionException);
    }

    public void addException(List list) {
        this.mAssertionErr.addAll(list);
    }

    public List getExceptions() {
        return this.mAssertionErr;
    }

    public static AssertionArrayException mergeException(AssertionArrayException assertionArrayException, AssertionArrayException assertionArrayException2) {
        if (assertionArrayException == null) {
            return assertionArrayException2;
        }
        if (assertionArrayException2 == null) {
            return assertionArrayException;
        }
        AssertionArrayException assertionArrayException3 = new AssertionArrayException(assertionArrayException.getMessage() + ";" + assertionArrayException2.getMessage());
        assertionArrayException3.addException(assertionArrayException.getExceptions());
        assertionArrayException3.addException(assertionArrayException2.getExceptions());
        return assertionArrayException3;
    }

    public String toString() {
        StringBuilder stringBuilder = new StringBuilder();
        for (AssertionException hackAssertionException : this.mAssertionErr) {
            stringBuilder.append(hackAssertionException.toString()).append(";");
            try {
                if (hackAssertionException.getCause() instanceof NoSuchFieldException) {
                    Field[] declaredFields = hackAssertionException.getHackedClass().getDeclaredFields();
                    stringBuilder.append(hackAssertionException.getHackedClass().getName()).append(".").append(hackAssertionException.getHackedFieldName()).append(";");
                    for (Field field : declaredFields) {
                        stringBuilder.append(field.getName()).append(File.separator);
                    }
                } else if (hackAssertionException.getCause() instanceof NoSuchMethodException) {
                    Method[] declaredMethods = hackAssertionException.getHackedClass().getDeclaredMethods();
                    stringBuilder.append(hackAssertionException.getHackedClass().getName()).append("->").append(hackAssertionException.getHackedMethodName()).append(";");
                    for (int i = 0; i < declaredMethods.length; i++) {
                        if (hackAssertionException.getHackedMethodName().equals(declaredMethods[i].getName())) {
                            stringBuilder.append(declaredMethods[i].toGenericString()).append(File.separator);
                        }
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
            stringBuilder.append("@@@@");
        }
        return stringBuilder.toString();
    }
}
" data-snippet-id="ext.27e0015bab58b1c74cae9139390abd6d" data-snippet-saved="false" data-codota-status="done">//這個類用於保存hack過程中發生的異常,一旦mAssertionErr不為空,則表示當前系統不支持資源的熱修復,直接return,不進行修復
public class AssertionArrayException extends Exception {
    private static final long serialVersionUID = 1;
    private List mAssertionErr;

    public AssertionArrayException(String str) {
        super(str);
        this.mAssertionErr = new ArrayList();
    }

    public void addException(AssertionException hackAssertionException) {
        this.mAssertionErr.add(hackAssertionException);
    }

    public void addException(List list) {
        this.mAssertionErr.addAll(list);
    }

    public List getExceptions() {
        return this.mAssertionErr;
    }

    public static AssertionArrayException mergeException(AssertionArrayException assertionArrayException, AssertionArrayException assertionArrayException2) {
        if (assertionArrayException == null) {
            return assertionArrayException2;
        }
        if (assertionArrayException2 == null) {
            return assertionArrayException;
        }
        AssertionArrayException assertionArrayException3 = new AssertionArrayException(assertionArrayException.getMessage() + ";" + assertionArrayException2.getMessage());
        assertionArrayException3.addException(assertionArrayException.getExceptions());
        assertionArrayException3.addException(assertionArrayException2.getExceptions());
        return assertionArrayException3;
    }

    public String toString() {
        StringBuilder stringBuilder = new StringBuilder();
        for (AssertionException hackAssertionException : this.mAssertionErr) {
            stringBuilder.append(hackAssertionException.toString()).append(";");
            try {
                if (hackAssertionException.getCause() instanceof NoSuchFieldException) {
                    Field[] declaredFields = hackAssertionException.getHackedClass().getDeclaredFields();
                    stringBuilder.append(hackAssertionException.getHackedClass().getName()).append(".").append(hackAssertionException.getHackedFieldName()).append(";");
                    for (Field field : declaredFields) {
                        stringBuilder.append(field.getName()).append(File.separator);
                    }
                } else if (hackAssertionException.getCause() instanceof NoSuchMethodException) {
                    Method[] declaredMethods = hackAssertionException.getHackedClass().getDeclaredMethods();
                    stringBuilder.append(hackAssertionException.getHackedClass().getName()).append("->").append(hackAssertionException.getHackedMethodName()).append(";");
                    for (int i = 0; i < declaredMethods.length; i++) {
                        if (hackAssertionException.getHackedMethodName().equals(declaredMethods[i].getName())) {
                            stringBuilder.append(declaredMethods[i].toGenericString()).append(File.separator);
                        }
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
            stringBuilder.append("@@@@");
        }
        return stringBuilder.toString();
    }
}
//具體Hack類,主要Hack AssetManager相關類,
public class AndroidHack {
    private static final String TAG = "AndroidHack";

    //exception
    public static AssertionArrayException exceptionArray;


    //resources
    public static HackPlus.HackedClass AssetManager;
    public static HackedMethod0 AssetManager_construct;
    public static HackPlus.HackedMethod1 AssetManager_addAssetPath;
    public static HackedMethod0 AssetManager_ensureStringBlocks;


    //>=19
    public static HackedClass

使用的時候,只要在加載patch資源前,調用如下方法進行檢測

    if(!AndroidHack.defineAndVerify()){
    //不加載patch資源
        return;
    }
    //加載patch資源邏輯

patch資源的生成比較麻煩,我們放在最後面說明,現在假設我們有一個包含整個apk的資源的文件,需要運行時替換,現在來實現上面的加載patch資源的邏輯,具體邏輯上面反射的時候已經說明了,這時候只需要調用上面反射獲取的包裝類,進行替換即可,直接看代碼中的注釋:

public class ResourceLoader {
    private static final String TAG = "ResourceLoader";

    public static boolean patchResources(Context context, File patchResource) {
        try {
            if (context == null || patchResource == null){
                return false;
            }
            if (!patchResource.exists()) {
                return false;
            }
            //通過構造函數new一個AssetManager對象
            AssetManager newAssetManager = AndroidHack.AssetManager_construct.invoke().statically();
            //調用AssetManager對象的addAssetPath方法添加patch資源
            int cookie = AndroidHack.AssetManager_addAssetPath.invokeWithParam(patchResource.getAbsolutePath()).on(newAssetManager);
            //添加成功時cookie必然大於0
            if (cookie == 0) {
                Logger.e(TAG, "Could not create new AssetManager");
                return false;
            }
            // 在Android 19以前需要調用這個方法,但是Android L後不需要,實際情況Andorid L上調用也不會有問題,因此這裡不區分版本
            // Kitkat needs this method call, Lollipop doesn't. However, it doesn't seem to cause any harm
            // in L, so we do it unconditionally.
            AndroidHack.AssetManager_ensureStringBlocks.invoke().on(newAssetManager);

            //獲取內存中的Resource對象的弱引用
            Collection> references;

            if (Build.VERSION.SDK_INT >= 24) {
                // Android N,獲取的是一個ArrayList,直接賦值給references對象
                // Find the singleton instance of ResourcesManager
                Object resourcesManager = AndroidHack.ResourcesManager_getInstance.invoke().statically();
                //noinspection unchecked
                references = (Collection>) AndroidHack.ResourcesManager_mResourceReferences.on(resourcesManager).get();
            } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
                //Android 19以上 獲得的是一個ArrayMap,調用其values方法後賦值給references
                // Find the singleton instance of ResourcesManager
                Object resourcesManager = AndroidHack.ResourcesManager_getInstance.invoke().statically();
                @SuppressWarnings("unchecked")
                ArrayMap> arrayMap = AndroidHack.ResourcesManager_mActiveResources.on(resourcesManager).get();
                references = arrayMap.values();
            } else {
                //Android 19以下,通過ActivityThread獲取得到的是一個HashMap對象,通過其values方法獲得對象賦值給references
                Object activityThread = AndroidHack.getActivityThread();
                @SuppressWarnings("unchecked")
                HashMap> map = (HashMap>) AndroidHack.ActivityThread_mActiveResources.on(activityThread).get();
                references = map.values();
            }
            //遍歷獲取到的Ressources對象的弱引用,將其AssetManager對象替換為我們的patch的AssetManager
            for (WeakReference wr : references) {
                Resources resources = wr.get();
                // Set the AssetManager of the Resources instance to our brand new one
                if (resources != null) {
                    if (Build.VERSION.SDK_INT >= 24) {
                        Object resourceImpl = AndroidHack.Resources_ResourcesImpl.get(resources);
                        AndroidHack.ResourcesImpl_mAssets.set(resourceImpl, newAssetManager);
                    } else {
                        AndroidHack.Resources_mAssets.set(resources, newAssetManager);
                    }
                    resources.updateConfiguration(resources.getConfiguration(), resources.getDisplayMetrics());
                }
            }
            return true;
        } catch (Throwable throwable) {
            Logger.e(TAG, throwable);
            throwable.printStackTrace();
        }
        return false;
    }
}

這樣一來,就在Appliction啟動的時候完成了資源的熱修復,當然我們也可以像instant run那樣,把activity也處理,不過我們簡單起見,讓其重啟生效,所以activity就不處理了。

於是,我們Appliction的onCreate()中的代碼就變成了下面這個樣子

if (hasResourcePatch){
    if (!AndroidHack.defineAndVerify()) {
        //不加載patch資源
        return;
    }
    //加載patch資源邏輯
    File file = new File("/path/to/patchResource.apk");
    ResourceLoader.patchResources(this, file);
}

這裡有一個坑。

patch應用成功後,如果要刪除patch,patch文件的刪除一定要謹慎,最好先通過配置文件標記patch不可用,下次啟動時檢測該標記,然後再刪除,運行期刪除正在使用的patch文件會導致所有進程的重啟,Application中的所有邏輯會被初始化一次。

還差最後一步,patch的資源從哪裡來,這裡主要講兩種方式。

直接下發整個apk文件,全量的資源,想怎麼用就怎麼用,當然缺點很明顯,文件太大了,下載容易出錯,不過也最簡單。 下發patch部分的資源,在客戶端和沒改變的資源合成新的apk,這種方式的優點是文件小,缺點是合成時占內存,需要開啟多進程去合成,比較復雜,沒有辦法校驗合成文件的md5值。

無論哪一種方式,都需要public.xml去固定資源id。

這裡討論的是第二種方式,所以給出精簡版的實現思路:

首先需要生成public.xml,public.xml的生成通過aapt編譯時添加-P參數生成。相關代碼通過gradle插件去hook Task無縫加入該參數,有一點需要注意,通過appt生成的public.xml並不是可以直接用的,該文件中存在id類型的資源,生成patch時應用進去編譯的時候會報resource is not defined,解決方法是將id類型的資源單獨記錄到ids.xml文件中,相當於一個聲明過程,編譯的時候和public.xml一樣,將ids.xml也參與編譯即可。

/**
* 添加aapt addition -P選項
*/

String processResourcesTaskName = variant.variantData.getScope().getGenerateRClassTask().name
ProcessAndroidResources processResourcesTask = (ProcessAndroidResources) project.tasks.getByName(processResourcesTaskName)

Closure generatePubicXmlClosure = {
    if (processResourcesTask) {
        //添加-P 參數,生成public.xml
        AaptOptions aaptOptions = processResourcesTask.aaptOptions
        File outPublicXml = new File(outputDir, PUBLIC_XML)
        aaptOptions.additionalParameters("-P", outPublicXml.getAbsolutePath())
        processResourcesTask.setAaptOptions(aaptOptions)
    }
}
/**
 * public.xml中對一些選項進行剔除,目前處理id類型資源,不然應用的時候編譯不過,會報resource is not defined,主要是生成一個ids.xml,相當於對這部分資源進行聲明
 */
Closure handlePubicXmlClosure = {
    if (processResourcesTask) {
        File outPublicXml = new File(outputDir, PUBLIC_XML)
        if (outPublicXml.exists()) {
            SAXReader reader = new SAXReader();
            Document document = reader.read(outPublicXml);
            Element root = document.getRootElement();
            List childElements = root.elements();

            File idsFile = new File(outPublicXml.getParentFile(), IDS_XML)
            if (idsFile.exists()) {
                idsFile.delete()
            }
            if (!idsFile.exists()) {
                idsFile.getParentFile().mkdirs()
                idsFile.createNewFile()
            }

            idsFile.append("")
            idsFile.append("\n")
            idsFile.append("")
            idsFile.append("\n")

            for (Element child : childElements) {
                String attrName = child.attribute("name").value
                String attrType = child.attribute("type").value

                if ("id".equalsIgnoreCase(attrType)) {
                    String value = child.asXML()
                    idsFile.append("    \n")
                    project.logger.error "write id item ${attrName}"
                }
            }
            idsFile.append("")

        }
    }
}
if (processResourcesTask) {
    processResourcesTask.doFirst(generatePubicXmlClosure);
    processResourcesTask.doLast(handlePubicXmlClosure)
}

在編譯資源之前,將public.xml和ids.xml文件拷貝到資源目錄values下,並檢測values.xml文件中是否有已經定義的id類型的資源,如果有,則從ids.xml文件中將其刪除,否則會報resource is already defined的異常,也會編譯不過去。

/**
 * 應用public.xml
 */
String mergeResourcesTaskName = variant.variantData.getScope().getMergeResourcesTask().name
MergeResources mergeResourcesTask = (MergeResources) project.tasks.getByName(mergeResourcesTaskName)

Closure applyPubicXmlClosure = {
    if (mergeResourcesTask != null) {
        if (oldTinkerDir != null && needApplyPublicXml) {
            File publicXmlFile = new File(oldTinkerDir, "${dirName}/${PUBLIC_XML}")
            if (publicXmlFile.exists()) {
                File toDir = new File(mergeResourcesTask.outputDir, "values")
                project.copy {
                    project.logger.error "\n$variant.name:copy a ${PUBLIC_XML} from ${publicXmlFile.getAbsolutePath()} to ${toDir}/${PUBLIC_XML}"
                    from(publicXmlFile.getParentFile()) {
                        include PUBLIC_XML
                        rename PUBLIC_XML, "${PUBLIC_XML}"
                    }
                    into(toDir)
                }
            } else {
                logger.error("${publicXmlFile.absolutePath} does not exist")
            }

            File valuesFile = new File(mergeResourcesTask.outputDir, "values/values.xml")
            File oldIdsFile = new File(oldTinkerDir, "${dirName}/${IDS_XML}")

            if (valuesFile.exists() && oldIdsFile.exists()) {
                SAXReader valuesReader = new SAXReader();
                Document valuesDocument = valuesReader.read(valuesFile);
                Element valuesRoot = valuesDocument.getRootElement()
                List publicIds = valuesRoot.selectNodes("item[@type='id']")


                if (publicIds != null && publicIds.size() != 0) {
                    Set existIdItems = new HashSet();
                    for (Element element : publicIds) {
                        existIdItems.add(element.attribute("name").value)
                    }
                    logger.error "existIdItems:${existIdItems}"
                    SAXReader oldIdsReader = new SAXReader();
                    Document oldIdsDocument = oldIdsReader.read(oldIdsFile);
                    Element oldIdsRoot = oldIdsDocument.getRootElement();
                    List oldElements = oldIdsRoot.elements();

                    if (oldElements != null && oldElements.size() != 0) {
                        File newIdsFile = new File(mergeResourcesTask.outputDir, "values/${IDS_XML}")
                        newIdsFile.append("")
                        newIdsFile.append("\n")
                        newIdsFile.append("")
                        newIdsFile.append("\n")

                        for (Element element : oldElements) {
                            String itemName = element.attribute("name").value

                            if (!existIdItems.contains(itemName)) {
                                newIdsFile.append("    ${element.asXML()}\n")
                            } else {
                                logger.error "already exist id item ${itemName}"
                            }
                        }
                        newIdsFile.append("")
                    }
                }


            } else {
                logger.error("${valuesFile.absolutePath} does not exist")
            }

        } else {
            logger.error "res not changed.not to apply public.xml"
        }
    }
}

if (mergeResourcesTask) {
    mergeResourcesTask.doLast(applyPubicXmlClosure);
}

這樣一來,按照正常流程去編譯,生成的apk安裝包就可以獲得了,然後將這個new.apk和有問題的old.apk進行差量算法,這裡只考慮資源相關文件,即assets目錄,res目錄,arsc文件,AndroidManifest.xml文件,相關算法如下:

對比new.apk和old.apk中的所有資源相關的文件。 對於新增資源文件,則直接壓入patch.apk中。 對於刪除的資源文件,則不處理到patch.apk中。 對於改變的資源文件,如果是assets或者res目錄中的資源,則直接壓縮到patch.apk中,如果是arsc文件,則使用bsdiff算法計算其差量文件,壓入patch.apk,文件名不變。 對於改變和新增的文件,通過一個meta文件去記錄其原始文件的adler32和合成後預期文件的adler32值,以及文件名,這是個文本文件,直接壓縮到patch.apk中去。 對patch.apk進行簽名。

這樣做的好處是能將資源patch文件盡可能的減小到最低,實際情況嚴重下來,res目錄下的資源文件大小都非常小,沒有必要去進行diff,所以直接使用原文件,而arsc文件則相對比較大,在考慮文件大小和內存的兩個因素下,犧牲內存換大小還是ok的,所以在下發前,我們對其進行diff,生成diff文件,在客戶端進行合成最終的arsc文件。

客戶端下載到patch.apk後需要進行還原,還原的步驟如下:

考慮到客戶端jni的兼容性問題,bspatch算法全部使用java實現 首先校驗patch.apk的簽名 讀取壓縮包中meta文件,判斷哪些文件是新增文件,哪些文件是改變的文件。 遍歷patch.apk中的文件,如果是新增文件,則壓縮到new.apk文件中去 如果是改變的文件,如果是assets和res文件夾下的資源,則直接壓縮到new.apk文件中,如果是arsc文件,則應用bspatch算法合成最終的arsc文件,壓縮到new.apk中 如果文件沒有改變,則直接復制old.apk中的原始文件到new.apk中 以上任何一個步驟都會去校驗合成時舊文件的adler32和合成後的adler32值和meta文件中記錄的是否符合 由於無法驗證合成後的文件的md5值(沒有記錄哪些文件被刪除了,加上壓縮算法等原因),需要使用一種方式在加載前進行驗證,這裡使用crc32值。 合成成功後計算new.apk文件的crc32值,計算方式進行改進,不計算所有文件內容的crc32,為了快速計算,只計算文件的某一個特定段的crc32值,比如文件從200字節開始到2000字節部分的crc32值,並保存在sharePrefrences中,加載patch前進行校驗crc32,校驗不通過,則直接刪除patch文件,當然這種計算方式有一定概率會把錯誤的文件當成正確的,畢竟計算的不是完整的文件,當然正確的文件是一定不會當成錯誤的,這種低概率事件可以接受。

這種方式的兼容性如何?簡單自測了下,4.0-7.0的模擬器運行全部通過,當然不排除國產奇葩ROM的兼容性,所以這裡我不宣稱100%兼容。

無圖言屌,沒圖你說個jb,先上一張沒有進行熱修復的圖:

這裡寫圖片描述

熱修復之後的效果圖

這裡寫圖片描述

最後送上一句話:

熱修復遠遠沒有你想象的那麼簡單,踩坑之路漫漫,入坑需謹慎。

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved