Android熱修復(fù):Qfix方案的gradle實(shí)踐

Android熱修復(fù):Qfix方案的gradle實(shí)踐

一木蹬、Android熱修復(fù)方案的發(fā)展

Android熱修復(fù)技術(shù)現(xiàn)在主流分為NativeHook,ClassLoader以及新出現(xiàn)的Instant-Run方案儒旬,在工作中因?yàn)閳F(tuán)隊(duì)需要為團(tuán)隊(duì)引入了QZone的ClassLoader插樁方案,當(dāng)初開發(fā)的時候(15年底)市面上還只有AndFix和ClassLoader兩種方案康吵,而那時的AndFix兼容性也還比較差直砂,就選擇了ClassLoader方案幌陕,基于網(wǎng)上開源的Nuwa方案修改而來。

2016年的時候Android熱修復(fù)方案如雨后春筍存崖,ClassLoader方案新起之秀有Tinker,QFix等冻记。對比幾種新出的方案,調(diào)研之后認(rèn)為QFix與團(tuán)隊(duì)之前使用的ClassLoader方案最為接近来惧,同時無需插樁冗栗,避免了插樁性能損失問題,在16年下半年時候?qū)F(tuán)隊(duì)的熱修復(fù)方案升級到了QFix方案供搀。

由于團(tuán)隊(duì)的熱修復(fù)方案需要對原來的方案做不少兼容隅居,所以會有不少雍余的代碼。17年年初就想著自己寫個QFix的開源gradle實(shí)現(xiàn)葛虐,現(xiàn)項(xiàng)目已完成發(fā)布到Github,地址:https://github.com/alexclin0188/QFixPatch胎源。 這篇文章主要是對開發(fā)過程的一個記錄,發(fā)出來供有興趣的同學(xué)參考和自己以后復(fù)習(xí)使用屿脐。

二涕蚤、QFix方案原理

QFix方案原理介紹原文。有興趣的同學(xué)可以仔細(xì)研讀下這篇原理介紹的诵,這里就不再詳細(xì)講解万栅,補(bǔ)丁修復(fù)的基本原來和ClassLoader類同,反射在DexPahList類中dex數(shù)組前插入補(bǔ)丁dex西疤,區(qū)別在于QFix使用在native層調(diào)用dvmResolveClass方法來解決pre-verify的問題烦粒,不再需要插樁。

GitHub上有一個開源的簡單的QFix方案Eclipse工程demo:https://github.com/lizhangqu/QFix, QFixPatch項(xiàng)目即是基于這個demo擴(kuò)展而來的一個gradle實(shí)踐瘪阁。

三撒遣、Gradle插件開發(fā)

gradle插件生成補(bǔ)丁邏輯和Nuwa類似,只是增加了dexdump分析補(bǔ)丁類class-id的操作管跺,簡單的流程示意圖如下:

qfix_gradle_proto.png

首先是buildBase义黎,構(gòu)建基礎(chǔ)apk和保存基礎(chǔ)信息,在dexTask之前增加hook豁跑,將原始apk中的類的sha值保存到hash.txt中廉涕,以便生成補(bǔ)丁時使用。然后在buildPatch執(zhí)行時比較找出修改的類(補(bǔ)丁類)艇拍,并通過dexdump分析這些補(bǔ)丁類在基礎(chǔ)apk的dex中的class-id狐蜕,將分析得到的class-id信息和補(bǔ)丁類通過dex工具打包成一個補(bǔ)丁apk

3.1 創(chuàng)建插件工程

在一個AS工程內(nèi),如果有buildSrc目錄卸夕,這個目錄就會被當(dāng)做gradle插件源碼目錄編譯层释,并可以直接在工程模塊中直接使用編寫的gradle插件,下面是QFixPatch工程gradle插件源碼目錄截圖

qfix-gradle-source-code.png

和Android模塊不同的是其中有resources目錄快集,下面有個META-INF.gradle-plugins文件夾贡羔,內(nèi)有propertites文件廉白,而這個文件是用來注冊插件入口類的,下面是alexclin.qfix.properties的內(nèi)容

//alexclin.qfix.properties文件乖寒,properties文件名稱即為gradle插件名
implementation-class=alexclin.qfix.QFixPlugin

我們gradle插件的使用方式是在模塊的build.gradle中增加apply猴蹂,如下

//build.gradle文件

......
apply plugin: 'alexclin.qfix'
......

而這里的apply plugin的作用最終就是讓項(xiàng)目在執(zhí)行g(shù)radle編譯時調(diào)用我們的實(shí)現(xiàn)類alexclin.qfix.QFixPlugin的apply函數(shù),下面讓我們看QFixPlugin類的代碼

//QFixPlugin類
public class QFixPlugin implements Plugin<Project> {
    static final String CLASS_ID_TXT = "class-ids.txt"

    static final String PATCH_NAME = "patch"
    static final String DEBUG = "debug"
    static final String PLUGIN_NAME = "qfix"

    def debugOn

    @Override
    void apply(Project project) {
        if (project.getPlugins().hasPlugin(AppPlugin)) {
            project.extensions.create(PLUGIN_NAME, QFixExtension, project)
            applyPlugin(project)
        }
    }

    //真正的applyplugin邏輯代碼
    private void applyPlugin(Project project) {
        project.afterEvaluate {
            def extension = project.extensions.findByName(PLUGIN_NAME) as QFixExtension

            Creator creator = new Creator(project, extension);
            debugOn = extension.debugOn

            project.android.applicationVariants.each { variant ->

                if (variant.name.contains(DEBUG) && !debugOn)
                    return;
                configTasks(project, variant, creator);
            }

            //添加總的buildPatch Task
            def releasePatch = project.tasks.findByName("assembleReleasePatch")
            def debugPatch = project.tasks.findByName("assembleDebugPatch")
            if (releasePatch != null || debugPatch != null) {
                def buildPatchTask = project.task("buildPatch")
                if (debugPatch) buildPatchTask.dependsOn debugPatch
                if (releasePatch) buildPatchTask.dependsOn releasePatch
            }

            def assembleDebugBase = project.tasks.findByName("assembleDebugBase")
            def assembleReleaseBase = project.tasks.findByName("assembleReleaseBase")
            if (assembleDebugBase != null || assembleReleaseBase != null) {
                def assembleBase = project.task("buildBase");
                if (assembleDebugBase) assembleBase.dependsOn assembleDebugBase
                if (assembleReleaseBase) assembleBase.dependsOn assembleReleaseBase
            }
        }
    }
    
    ....
 }

可以看到上面這段代碼里面楣嘁,主要是在真正的applyplugin函數(shù)對項(xiàng)目進(jìn)行了Task設(shè)置磅轻,并對buildBase,buildPatch,assembleDebugBase,assembleDebugPacth,assembleReleaseBase,assembleReleasePacth的依賴關(guān)系做了設(shè)置。

實(shí)際上只有assemble<Viriant>Base,assemble<Viriant>Patch的區(qū)別,前者是構(gòu)建基礎(chǔ)apk包和保存基礎(chǔ)信息逐虚,后者是構(gòu)建補(bǔ)丁

3.2 插件設(shè)置QFixExtension

gradle插件支持屬性設(shè)置聋溜,如我們的QFix插件在build.gradle中屬性設(shè)置,如下

//qfix插件在app模塊build.gradle中的屬性設(shè)置
qfix{  
    debugOn true //debug模式是否開啟補(bǔ)丁構(gòu)建task,可選
    outputDir 'qfix_output'//補(bǔ)丁基礎(chǔ)信息默認(rèn)輸入目錄和構(gòu)建輸出目錄叭爱,必須
    excludeClass = ["App.class"]//需要排除的類勤婚,會自動排除Application及其父類,此處只是示例涤伐,可選
    //includePackage = []//需要包含的package匹舞,可選
    strictMode true //嚴(yán)格模式,用于解決ART下dex激進(jìn)內(nèi)聯(lián)問題婿奔,詳情參看README,可選
}

而上面這個設(shè)置實(shí)際對應(yīng)的是一個Extension類睦尽,在qfix-gradle插件中對應(yīng)的就是QFixExtension類

package alexclin.qfix

import org.gradle.api.Project

class QFixExtension {
    HashSet<String> includePackage = []
    HashSet<String> excludeClass = []
    //補(bǔ)丁構(gòu)建在debug版是否開啟器净,默認(rèn)開啟
    boolean debugOn = false
    //補(bǔ)丁基礎(chǔ)信息保存目錄和補(bǔ)丁輸出目錄
    String outputDir
    //嚴(yán)格模式,啟用則打包所有引用補(bǔ)丁類的class到補(bǔ)丁中,應(yīng)對ART激進(jìn)內(nèi)聯(lián)引起的問題
    boolean strictMode = false;

    QFixExtension(Project project) {
    }
}

3.3 設(shè)置Task和hook dexTask

設(shè)置Task和hook dexTask的邏輯主要都在QFixPlugin的configTasks函數(shù)中当凡,以下是簡化的函數(shù)代碼山害。

    static void configTasks(Project project, BaseVariant variant, Creator creator) {
        Map hashMap
        //獲取dexTask和proguardTask
        def dexTask = ...
        def proguardTask = ...

        if (creator.patchTaskEnable()) {
            //創(chuàng)建Task,尋找被修改的類沿量,只有assemble<Virant>Patch時才會被調(diào)用
            def diffClassBeforeDex = "diffClassBeforeDex${variant.name.capitalize()}"
            def diffClassBeforeDexTask = ...
            diffClassBeforeDexTask.dependsOn dexTask.taskDependencies.getDependencies(dexTask)

            //創(chuàng)建Task浪慌,將改變的類打成一個dex
            def hotfixPatch = "assemble${variant.name.capitalize()}Patch"
            def hotfixPatchTask = ...
            hotfixPatchTask.dependsOn diffClassBeforeDexTask
        }

        //創(chuàng)建Hook Task, 在dexTask執(zhí)行之前 保存所有類的sha值到對應(yīng)目錄的hash.txt
        def shaClassBeforeDex = "shaClassBeforeDex${variant.name.capitalize()}"
        def shaClassBeforeDexTask = ...

            //備份構(gòu)建過程中的mapping.txt,如果有
            if (proguardTask) {
                def mapFile = new File("${project.buildDir}/outputs/mapping/${variant.dirName}/mapping.txt")
                def newMapFile = creator.getMappingOutFile(variant);
                Utils.copyFile(mapFile, newMapFile)
            }
        }
        shaClassBeforeDexTask.dependsOn dexTask.taskDependencies.getDependencies(dexTask)

        //對assembleRelease或assembleDebug添加Hook朴则,保存apk
        def assembleTaskName = "assemble${variant.name.capitalize()}Base";
        def assembleTask = project.task(assembleTaskName);
        Closure saveAssembleClosure = ...
        
        //設(shè)置依賴關(guān)系权纤,保證assembleXXXBase在android自身的assembleDebug/assembleRelease之后執(zhí)行
        assembleTask.doLast(saveAssembleClosure)
        assembleTask.dependsOn shaClassBeforeDexTask
        assembleTask.dependsOn project.tasks["assemble${variant.name.capitalize()}"]
    }

上面創(chuàng)建的幾個task中間最重要的是shaClassBeforeDexTask和diffClassBeforeDexTask,前者是保存基礎(chǔ)apk中所有類的sha信息乌妒,后者則是對比基礎(chǔ)apk和修改后的代碼類的信息汹想,找出改變的類。先來看shaClassBeforeDexTask

//shaClassBeforeDexTask 保存基礎(chǔ)apk中所有類的sha信息
def shaClassBeforeDexTask = project.task(shaClassBeforeDex) << {
            //準(zhǔn)備工作...
            ...
            
            //保存所有類的sha值到hashFile中
            Set<File> inputFiles = AndroidUtils.getDexTaskInputFiles(project, variant, dexTask)

            if (proguardTask) {
                inputFiles.each {
                    inputFile ->
                        if (inputFile.path.endsWith(".jar")) {
                            shaJarInfo(inputFile, creator.patchSetting, hashFile)
                        }
                }
            } else if (AndroidUtils.compareVersionName(Version.ANDROID_GRADLE_PLUGIN_VERSION, "2.2.3") > -1) {
                //沒有混淆在2.2.3及以后插件上inputFiles不包含當(dāng)前模塊類
                //合并所有jar包
                Set<File> jarAndDir = new HashSet<>(inputFiles)
                jarAndDir.add(new File(project.buildDir, "intermediates/classes/${variant.dirName}"))
                File combinedJar = combineJarAndDir(project, jarAndDir)
                shaJarInfo(combinedJar, creator.patchSetting, hashFile)
            }

            //備份構(gòu)建過程中的mapping.txt
            ......
        }

diffClassBeforeDexTask的作用是找出改變的類并dump分析改變的類在基礎(chǔ)apk的dex中的class-id撤蚊,簡化的代碼如下:

//diffClassBeforeDexTask 找出改變的類并dump分析改變的類在基礎(chǔ)apk的dex中的class-id
def diffClassBeforeDexTask = project.task(diffClassBeforeDex) << {
                //補(bǔ)丁準(zhǔn)備工作, 讀取原來apk的dex目錄古掏,proguard-mapping,hash.txt,以及初始化補(bǔ)丁輸出目錄
                def baseDexDir = ...
                File mappingFile = ...
                hashMap = ...
                File patchOutDir = ...
                //獲取dexTask的輸入jar報(bào)參數(shù)
                //比較所有類與之前保存的sha值是否有差異,有差異則保存到patchClassDir
                Set<File> inputFiles = AndroidUtils.getDexTaskInputFiles(project, variant, dexTask)
                if (proguardTask) {
                    inputFiles.each {
                        inputFile ->
                            if (inputFile.path.endsWith(".jar")) {
                                diffJar(inputFile, hashMap, creator, variant, finder)
                            }
                    }
                } else if (AndroidUtils.compareVersionName(Version.ANDROID_GRADLE_PLUGIN_VERSION, "2.2.3") > -1) {
                    //沒有混淆在2.2.3及以后插件上inputFiles不包含當(dāng)前模塊類
                    //合并所有jar包
                    Set<File> jarAndDir = new HashSet<>(inputFiles)
                    jarAndDir.add(new File(project.buildDir, "intermediates/classes/${variant.dirName}"))
                    File combinedJar = combineJarAndDir(project, jarAndDir)
                    diffJar(combinedJar, hashMap, creator, variant, finder)
                }
                def allRefPatchClasses = ...
                def appName = ...
                //增加dexDump處理侦啸,分析補(bǔ)丁類在基礎(chǔ)apk的dex中class-id并保存
                def dumpCmdPath = AndroidUtils.getDexDumpPath(project, creator.sdkDir);
                File patchClassDir = creator.getClassOutDir(variant);
                File classIdsFile = new File(patchClassDir, CLASS_ID_TXT);
                DexClassIdResolve.dumpDexClassIds(dumpCmdPath, baseDexDir, patchClassDir, classIdsFile,appName,allRefPatchClasses)
            }

可以看到重要的邏輯就是比較sha值和dexDump分析class-id, diffJar函數(shù)主要是讀取jar包中的類并保存sha信息槽唾,代碼如下

static void diffJar(File jarFile, HashMap hashMap, Creator builder, BaseVariant variant, SubClsFinder finder) {
        File basePatchClassDir = builder.getClassOutDir(variant);
        if (!basePatchClassDir.exists()) basePatchClassDir.mkdirs();
        if (jarFile && jarFile.isFile()) {
            def file = new JarFile(jarFile);
            builder.patchSetting.addApplicationAndSuper(file, variant);
            Enumeration enumeration = file.entries();
            while (enumeration.hasMoreElements()) {
                JarEntry jarEntry = (JarEntry) enumeration.nextElement();
                String entryName = jarEntry.getName();
                InputStream inputStream = file.getInputStream(jarEntry);
                if (!builder.patchSetting.isExcluded(entryName)) {
                    def bytes = Utils.readAllBytesAndClose(inputStream);
                    def hash = DigestUtils.shaHex(bytes)
                    if (Utils.notSame(hashMap, entryName, hash)) {
                        finder.addAbsPatchClass(bytes, entryName)
                        Utils.copyBytesToFile(bytes, Utils.touchFile(basePatchClassDir, entryName))
                    } else {
                        finder.addOutPatchClass(bytes, entryName)
                    }
                }
            }
            Collection<String> collections = finder.getRefClasses();
            for (String entryName : collections) {
                ZipEntry zipEntry = file.getJarEntry(entryName);
                InputStream inputStream = file.getInputStream(zipEntry);
                def bytes = Utils.readAllBytesAndClose(inputStream);
                Utils.copyBytesToFile(bytes, Utils.touchFile(basePatchClassDir, entryName))
                inputStream.close();
            }
            file.close();
        }
    }

3.4 調(diào)用dexDump分析補(bǔ)丁類在原始apk的dex中class-id

dexDump的分析邏輯在alexclin.qfix.DexClassIdResolve類中丧枪,主要代碼是readClassIdMap函數(shù),該函數(shù)根據(jù)傳入的dex和補(bǔ)丁類信息夏漱,結(jié)合dexdump工具的輸出豪诲,將補(bǔ)丁類class-id保存下來

private static String readClassIdMap(InputStream mInputStream, int mDexIndex, HashSet<String> mPatchSet,Set<String> patchRefSet,ArrayList<ClassIdMap> mClassIdMapList) {
        InputStreamReader isReader = null;
        BufferedReader reader = null;
        String entrance = null;
        try {
            isReader = new InputStreamReader(mInputStream);
            reader = new BufferedReader(isReader);
            boolean findHead = false;
            boolean findClass = false;
            int classIndex = -1;
            long classIdx = -1;
            def line
            while ((line = reader.readLine()) != null) {
                if (line.startsWith("Class #") && line.endsWith(" header:") && !findHead && classIndex < 0) {
                    findHead = true;
                    classIndex = Integer.parseInt(line.substring("Class #".length(), line.indexOf(" header:")));
                } else if (line.startsWith("class_idx") && findHead && classIndex >= 0 && classIdx < 0) {
                    classIdx = Integer.parseInt(line.substring(line.indexOf(": ") + 2));
                } else if (line.startsWith("Class #") && findHead && classIndex >= 0
                        && line.contains(String.valueOf(classIndex)) && classIdx > 0) {
                    findClass = true;
                } else if (line.startsWith("  Class descriptor") && findHead && findClass && classIndex >= 0 && classIdx > 0) {
                    String className = line.substring(line.indexOf("'L") + 2, line.indexOf(";'"));
                    if (mPatchSet.contains(className)) {
                        ClassIdMap item = new ClassIdMap(className, mDexIndex, classIdx);
                        mClassIdMapList.add(item);
                    }else if(!patchRefSet.contains(className)&&entrance==null){
                        entrance = className;
                    }
                    System.out.println("className:"+className)
                    findHead = false;
                    findClass = false;
                    classIndex = -1;
                    classIdx = -1;
                }
            }
        } catch (Exception e) {
            //異常處理和關(guān)閉流
            ......
        }
        return entrance;
    }

因?yàn)樵贏PP注入補(bǔ)丁時調(diào)用dvmResolveClass需要傳入一個引用類,這里使用當(dāng)前dex中和補(bǔ)丁類無調(diào)用關(guān)系的一個類作為該dex中補(bǔ)丁類的入口類

3.5 生成補(bǔ)丁

經(jīng)過上面的流程挂绰,已經(jīng)有所有改變的類(補(bǔ)丁類)和補(bǔ)丁類在基礎(chǔ)apk的Dex中的class-ids信息(class-ids.txt)屎篱,生成補(bǔ)丁就直接調(diào)用dx工具類即可

def hotfixPatchTask = project.task(hotfixPatch) << {
                    //調(diào)用dx工具生成apk
                ......
                AndroidUtils.dex(project, creator.getClassOutDir(variant), creator.sdkDir, pathFilePath)
                //簽名補(bǔ)丁
                SigningConfig signingConfig = variant.signingConfig;
                if(signingConfig!=null){
                    ......
                    if (AndroidUtils.signApk(patchFile, patchSignedFile, signingConfig,compatible))
                        patchFile.delete();
                }
            }

四、補(bǔ)丁客戶端應(yīng)用代碼開發(fā)

4.1 使用Application代理方式

因?yàn)锳pplication類被調(diào)起之后我們才有機(jī)會去加載我們自己的補(bǔ)丁葵蒂,而如果將Application調(diào)用了某些其他類交播,這些類的class也可能在我們加載補(bǔ)丁前就已經(jīng)被加載到ClassLoader中,這樣這些類就沒有機(jī)會被替換了。

為了解決如上這個問題践付,參考了Tinker的方式秦士,也才用Application代理來解決,將真正的Application邏輯寫在代理類中永高。demo中的Application類如下

public class App extends PatchApplication {
    private static final String DELEGATE_NAME = "alexclin.qfix.qfixgradle.AppDelegate";

    public App() {
        super(DELEGATE_NAME);
    }

    @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);
        MultiDex.install(base);
        File debugPatch = new File(Environment.getExternalStorageDirectory(),"debugPatch.apk");
        PatchTool.installPatch(this,debugPatch);
    }
}

下面是PatchApplication的代碼隧土,主要是反射調(diào)起Application代理類的對應(yīng)方法

public abstract class PatchApplication extends Application {
    private String delegateClassName;
    private ApplicationLifeCycle delegate;

    public PatchApplication(String delegateClassName) {
        this.delegateClassName = delegateClassName;
    }

    @Override
    public final void onCreate() {
        super.onCreate();
        ensureDelegate();
        delegate.onCreate();
    }

    @Override
    public final void onTerminate() {
        super.onTerminate();
        if(delegate!=null) delegate.onTerminate();
    }

    @Override
    public final void onConfigurationChanged(Configuration newConfig) {
        super.onConfigurationChanged(newConfig);
        if(delegate!=null) delegate.onConfigurationChanged(newConfig);
    }

    @Override
    public final void onLowMemory() {
        super.onLowMemory();
        if(delegate!=null) delegate.onLowMemory();
    }

    @Override
    public final void onTrimMemory(int level) {
        super.onTrimMemory(level);
        if(delegate!=null) delegate.onTrimMemory(level);
    }

    private void ensureDelegate(){
        if(delegate==null){
            try {
                Class<?> delegateClass = Class.forName(delegateClassName,false,getClassLoader());
                Constructor<?> constructor = delegateClass.getConstructor(Application.class);
                delegate = (ApplicationLifeCycle) constructor.newInstance(this);
            } catch (Exception e) {
                throw new IllegalArgumentException("create app delegate failed",e);
            }
        }
    }
}

4.1 反射注入補(bǔ)丁dex到ClassLoader中的數(shù)組前面

反射注入補(bǔ)丁dex到ClassLoader中的數(shù)組前面的方式和其它ClassLoader方案的注入方式差別不大,入口函數(shù)是PatchTool.installPatch(Application application,File patchFile)命爬,在此函數(shù)中先通過InjectUtil提供的函數(shù)注入補(bǔ)丁dex曹傀,再使用解析出來的class-id信息調(diào)用native函數(shù)

注入函數(shù)這里只簡單列出

   //InjectUtil類代碼
   //注入補(bǔ)丁Dex
    static boolean injectDex(Application context, File patchFile) {
        ArrayList<File> files = new ArrayList<File>();
        files.add(patchFile);
        try {
            checkApkFiles(files);
            if(isAliyunOs()){   //阿里云OS的注入
                for(File file:files){
                    injectLexFile(context,file);
                }
                return true;
            }else if(isAndroid()){  //普通android-os的注入
                installDex(context,InjectUtil.class.getClassLoader(),context.getDir("dex", 0),files);
                return true;
            }
            throw new IllegalStateException("Current system is not support");
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

4.2 解析補(bǔ)丁apk中的補(bǔ)丁class-id信息

解析補(bǔ)丁apk中class-id信息的邏輯在PatchTool.readPatchClassIds函數(shù)中,代碼如下

//class-ids.txt中的格式如下 第一行是入口類信息饲宛,以:分割皆愉,格式:Dex1入口類:Dex2入口類
//之后是class-id信息,格式:classname-dexIndex-classId
private static List<Pair<String,Long>> readPatchClassIds(File patchFile, String defaultEntranceClass){
        List<Pair<String,Long>> classIds = new ArrayList<Pair<String,Long>>();
        InputStream inputStream = null;
        BufferedReader reader = null;
        SparseArray<String> dexEntrances = new SparseArray<String>();
        try {
            JarFile jarFile = new JarFile(patchFile);
            ZipEntry entry = jarFile.getEntry(CLASS_ID_TXT);
            inputStream = jarFile.getInputStream(entry);
            reader = new BufferedReader(new InputStreamReader(inputStream));
            String line;
            boolean isFirst = true;
            while ((line=reader.readLine())!=null){
                if(isFirst){
                    isFirst = false;
                    if(line.contains(":")){ //解析入口
                        String[] entrances = line.split(":");
                        for(int i=0;i<entrances.length;i++){
                            String entrance = entrances[i];
                            if(TextUtils.isEmpty(entrance))
                                continue;
                            dexEntrances.put(i+1,entrance);
                        }
                        boolean loadEntrance = loadEntranceClasses(dexEntrances);
                        if(!loadEntrance){
                            return null;
                        }
                        continue;
                    }
                }
                if (!TextUtils.isEmpty(line)) {
                    String[] infos = line.split("-");
                    if (infos.length == 3) {
                        long classId = Long.valueOf(infos[2]);
                        int dexIndex = Integer.valueOf(infos[1]);
                        String entrance = getEntranceClass(dexEntrances,dexIndex,defaultEntranceClass);
                        classIds.add(Pair.create(entrance,classId));
                    }
                }
            }
        } catch (Exception e) {
            //.....異常處理艇抠,關(guān)閉流
        }

        return classIds;
    }

然后使用解析出來的class-id信息調(diào)用PatchTool.resolvePatchClass函數(shù)

private static boolean resolvePatchClass(Application app, String[] referrerClassList, long[] classIdxList, int size) {
        if (!sIsLibLoaded) {
            sIsLibLoaded = loadPatchToolLib();
        }
        if (!sIsLibLoaded) {
            boolean unloadResult = InjectUtil.unloadPatchElement(app, 0);
            Log.e(TAG, "load lib failed, unload patch result=" + unloadResult);
            return false;
        } else {
            int resolveResult = nativeResolvePatchClass(referrerClassList, classIdxList, size);
            if (resolveResult != CODE_RESOLVE_PATCH_ALL_SUCCESS) {
                //resolve不成功從dex數(shù)組中卸載對應(yīng)dex
                boolean unloadResult = InjectUtil.unloadPatchElement(app, 0);
                Log.e(TAG, String.format(Locale.ENGLISH,"resolve patch class failed, unload patch result= %b,refClass1:%s",
                        unloadResult,referrerClassList[0]));
                return false;
            } else {
                Log.d(TAG, "resolve patch class success");
                return true;
            }
        }
    }

4.3 調(diào)用nativeResolveClass函數(shù)

上一節(jié)中最終會調(diào)用到nativeResolvePatchClass方法幕庐,這個方法是native方法,實(shí)現(xiàn)在qfixlib/src/main/jni/ResolvePatch.c中,這個C文件也是直接使用https://github.com/lizhangqu/QFix的中的C文件

jint Java_alexclin_patch_qfix_tool_PatchTool_nativeResolvePatchClass(JNIEnv* env,
        jobject thiz, jobjectArray referrerClassList, jlongArray classIdxList, jint size) {
    LOGI("enter nativeResolvePatchClass");
    int referrerClassSize = (*env)->GetArrayLength(env, referrerClassList);
    int classIdxSize = (*env)->GetArrayLength(env, classIdxList);
    if (size <= 0 || referrerClassSize != size || classIdxSize != size) {
        LOGE("CODE_NATIVE_INIT_PARAMETER_ERROR");
        return CODE_NATIVE_INIT_PARAMETER_ERROR;
    }
    jlong* jClassIdxArray = (*env)->GetLongArrayElements(env, classIdxList, 0);
    if (jClassIdxArray == 0) {
        LOGE("CODE_NATIVE_INIT_PARAMETER_ERROR");
        return CODE_NATIVE_INIT_PARAMETER_ERROR;
    }

    void* handle = 0;
    handle = dlopen("/system/lib/libdvm.so", RTLD_LAZY);
    if (handle) {
        void* findFunc = 0;
        int i = 0;
        while(i < ARRAY_SIZE_FIND_CLASS) {
            findFunc = dlsym(handle, ARRAY_SYMBOL_FIND_LOADED_CLASS[i]);
            if (findFunc) {
                break;
            }
            i++;
        }
        if (findFunc) {
            g_pDvmFindLoadedClass_Addr = findFunc;
            void* resolveFunc = 0;
            i = 0;
            while(i < ARRAY_SIZE_RESOLVE_CLASS) {
                resolveFunc = dlsym(handle, ARRAY_SYMBOL_RESOLVE_CLASS[i]);
                if (resolveFunc) {
                    break;
                }
                i++;
            }
            if (resolveFunc) {
                g_pDvmResolveClass_Addr = resolveFunc;
                i = 0;
                while(i < size) {
                    jstring jClassItem = (jstring)((*env)->GetObjectArrayElement(env, referrerClassList, i));
                    const char* classItem = (*env)->GetStringUTFChars(env, jClassItem, 0);
                    if (classItem == 0) {
                        (*env)->ReleaseLongArrayElements(env, classIdxList, jClassIdxArray, 0);
                        LOGE("CODE_NATIVE_ITEM_PARAMETER_ERROR=%d", i);
                        return NUM_FACTOR_PATCH * i + CODE_NATIVE_ITEM_PARAMETER_ERROR;
                    }
                    if (strlen(classItem) < 5 || jClassIdxArray[i] < 0) {
                        (*env)->ReleaseLongArrayElements(env, classIdxList, jClassIdxArray, 0);
                        (*env)->ReleaseStringUTFChars(env, jClassItem, classItem);
                        LOGE("CODE_NATIVE_ITEM_PARAMETER_ERROR=%d", i);
                        return NUM_FACTOR_PATCH * i + CODE_NATIVE_ITEM_PARAMETER_ERROR;
                    }
                    void* referrerClassObj = g_pDvmFindLoadedClass_Addr(classItem);
                    if (referrerClassObj) {
                        void* resClassObj = g_pDvmResolveClass_Addr(referrerClassObj, (unsigned int)jClassIdxArray[i], 1);
                        if (!resClassObj) {
                            (*env)->ReleaseLongArrayElements(env, classIdxList, jClassIdxArray, 0);
                            (*env)->ReleaseStringUTFChars(env, jClassItem, classItem);
                            LOGE("CODE_PATCH_CLASS_OBJECT_ERROR=%d", i);
                            return NUM_FACTOR_PATCH * i + CODE_PATCH_CLASS_OBJECT_ERROR;
                        }
                    } else {
                        (*env)->ReleaseLongArrayElements(env, classIdxList, jClassIdxArray, 0);
                        (*env)->ReleaseStringUTFChars(env, jClassItem, classItem);
                        LOGE("CODE_REFERRER_CLASS_OBJECT_ERROR=%d", i);
                        return NUM_FACTOR_PATCH * i + CODE_REFERRER_CLASS_OBJECT_ERROR;
                    }
                    (*env)->ReleaseStringUTFChars(env, jClassItem, classItem);
                    i++;
                }
            } else {
                (*env)->ReleaseLongArrayElements(env, classIdxList, jClassIdxArray, 0);
                LOGE("CODE_RESOLVE_CLASS_ERROR");
                return CODE_RESOLVE_CLASS_ERROR;
            }
        } else {
            (*env)->ReleaseLongArrayElements(env, classIdxList, jClassIdxArray, 0);
            LOGE("CODE_FIND_LOADED_CLASS_ERROR");
            return CODE_FIND_LOADED_CLASS_ERROR;
        }
    } else {
        (*env)->ReleaseLongArrayElements(env, classIdxList, jClassIdxArray, 0);
        LOGE("CODE_LOAD_DALVIK_SO_ERROR");
        return CODE_LOAD_DALVIK_SO_ERROR;
    }
    (*env)->ReleaseLongArrayElements(env, classIdxList, jClassIdxArray, 0);
    LOGI("CODE_RESOLVE_PATCH_ALL_SUCCESS");
    return CODE_RESOLVE_PATCH_ALL_SUCCESS;
}

到這里加載流程就結(jié)束了家淤。

五异剥、總結(jié)

QFix方案原理和實(shí)現(xiàn)相對Tinker方案來說都簡單不少,比較輕量級媒鼓,不過Tinker實(shí)現(xiàn)比較重的同時可以實(shí)現(xiàn)功能級更新届吁,而QFix方案更多是應(yīng)對bug的修復(fù)。

QFixPatch這個項(xiàng)目算是QFix方案的一個gradle實(shí)踐绿鸣,實(shí)際開發(fā)過程中原創(chuàng)東西不多疚沐,主要工作還是在gradle插件。對于Android熱修復(fù)我也是因?yàn)楣ぷ髦杏行枨笏粤私舛嘁恍┏蹦!m?xiàng)目中有不完善的地方也歡迎各位同學(xué)來github提issue.

熱修復(fù)目前主流是NativeHook,ClassLoader,InstantRun三種方案亮蛔,NativeHook,ClassLoader在android7.0版本上都會有一些兼容性問題,相對來說InstantRun方案兼容性會更好擎厢,目前我也在研究學(xué)習(xí)RobustAceso的路上究流,歡迎有興趣的同學(xué)一起來探討辣吃。

本人Github:https://github.com/alexclin0188 歡迎關(guān)注

Stashed changes

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市芬探,隨后出現(xiàn)的幾起案子神得,更是在濱河造成了極大的恐慌,老刑警劉巖偷仿,帶你破解...
    沈念sama閱讀 207,248評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件哩簿,死亡現(xiàn)場離奇詭異,居然都是意外死亡酝静,警方通過查閱死者的電腦和手機(jī)节榜,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,681評論 2 381
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來别智,“玉大人宗苍,你說我怎么就攤上這事”¢唬” “怎么了讳窟?”我有些...
    開封第一講書人閱讀 153,443評論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長敞恋。 經(jīng)常有香客問我挪钓,道長,這世上最難降的妖魔是什么耳舅? 我笑而不...
    開封第一講書人閱讀 55,475評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮倚评,結(jié)果婚禮上浦徊,老公的妹妹穿的比我還像新娘。我一直安慰自己天梧,他們只是感情好盔性,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,458評論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著呢岗,像睡著了一般冕香。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上后豫,一...
    開封第一講書人閱讀 49,185評論 1 284
  • 那天悉尾,我揣著相機(jī)與錄音,去河邊找鬼挫酿。 笑死构眯,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的早龟。 我是一名探鬼主播惫霸,決...
    沈念sama閱讀 38,451評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼猫缭,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了壹店?” 一聲冷哼從身側(cè)響起猜丹,我...
    開封第一講書人閱讀 37,112評論 0 261
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎硅卢,沒想到半個月后射窒,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,609評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡老赤,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,083評論 2 325
  • 正文 我和宋清朗相戀三年轮洋,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片抬旺。...
    茶點(diǎn)故事閱讀 38,163評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡弊予,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出开财,到底是詐尸還是另有隱情汉柒,我是刑警寧澤,帶...
    沈念sama閱讀 33,803評論 4 323
  • 正文 年R本政府宣布责鳍,位于F島的核電站碾褂,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏历葛。R本人自食惡果不足惜正塌,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,357評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望恤溶。 院中可真熱鬧乓诽,春花似錦、人聲如沸咒程。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,357評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽帐姻。三九已至稠集,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間饥瓷,已是汗流浹背剥纷。 一陣腳步聲響...
    開封第一講書人閱讀 31,590評論 1 261
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留呢铆,地道東北人筷畦。 一個月前我還...
    沈念sama閱讀 45,636評論 2 355
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親鳖宾。 傳聞我的和親對象是個殘疾皇子吼砂,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,925評論 2 344

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