Gradle學(xué)習(xí)筆記(六)-- NuwaGradle解析

簡(jiǎn)介

Nuwa熱補(bǔ)丁是基于下文中手Q熱補(bǔ)丁輕量級(jí)方案的具體實(shí)現(xiàn),大致是通過(guò)“插樁”方式提前加載需要“打補(bǔ)丁”的類,以避免bug 類被加載。想要了解具體原理的可以參考本文參考。這里有兩部分,Nuwa實(shí)現(xiàn)了補(bǔ)丁類的加載動(dòng)作,NuwaGradle 則實(shí)現(xiàn)了補(bǔ)丁類的生成。這里我們重點(diǎn)關(guān)注如何生成補(bǔ)丁。

預(yù)備知識(shí)

在制作補(bǔ)丁的時(shí)候,首先要對(duì)一個(gè)apk的生成過(guò)程有一個(gè)大致的了解。了解了如何生成apk,才能知道我們應(yīng)該在哪一步去獲取制作補(bǔ)丁的“原材料”。

apk的生成過(guò)程

大致步驟如下:

  1. 使用aapt生成R.java類文件
  2. 使用android SDK提供的aidl.exe把.aidl轉(zhuǎn)成.java文件
  3. javac編譯.java類文件生成class文件
  4. 使用android SDK提供的dx.bat命令行腳本生成classes.dex文件
  5. 使用Android SDK提供的aapt.exe生成資源包文件
  6. apkbuilder 生成未簽名的apk安裝文件
  7. 使用jdk的jarsigner對(duì)未簽名的包進(jìn)行apk簽名
  8. zipAlign 對(duì)齊

思路

我們制作補(bǔ)丁時(shí),必須防止類被打上ISPREVERIFIED這個(gè)標(biāo)記。
原理一個(gè)類直接引用到的類不在同一個(gè)dex中即可。這樣,就能防止類被打上ISPREVERIFIED標(biāo)記并能進(jìn)行熱更新。

簡(jiǎn)單來(lái)說(shuō),就是將所有類的構(gòu)造函數(shù)中,引用另一個(gè)hack.dex中的類,這個(gè)類叫Hack.class,然后在加載補(bǔ)丁patch.dex前動(dòng)態(tài)加載這個(gè)hack.dex,但是有一個(gè)類的構(gòu)造函數(shù)中不能引用Hack.class,這個(gè)類就是Application類的子類,一旦這個(gè)類的構(gòu)造函數(shù)中加入Hack.class這個(gè)類,那么程序運(yùn)行時(shí)就會(huì)找不到Hack.class這個(gè)類,因?yàn)檫€沒(méi)有被加載。

生成補(bǔ)丁有幾個(gè)需要注意的地方:

  1. 尋找插入task的點(diǎn)
    在gradle1.5以下的版本中,我們可以找到dex task ,而gradle1.5以上,dex task 無(wú)法找到。
    這里便用 transformClassesWithDexForXXX 這個(gè)task。在其之前執(zhí)行生成改造類的工作。
    沒(méi)有開(kāi)啟Multidex的情況,存在一個(gè)preDex的Task。preDex會(huì)在dex任務(wù)之前把所有的庫(kù)工程和第三方j(luò)ar包提前打成dex,下次運(yùn)行只需重新dex被修改的庫(kù),以此節(jié)省時(shí)間。dex任務(wù)會(huì)把preDex生成的dex文件和主工程中的class文件一起生成class.dex,這樣就需要針對(duì)有無(wú)preDex,做不同的修改字節(jié)碼策略即可。
  2. 改造字節(jié)類,在類的構(gòu)造函數(shù)中插入另一個(gè)dex中的類
    對(duì)于java的.class的改造,我們一般會(huì)用到asm或者javasist 這兩類工具,而NuwaGradle則采用了asm,和同事溝通,發(fā)現(xiàn)使用javasist時(shí),有些類的無(wú)參構(gòu)造函數(shù)可能無(wú)法找到。這里就貼下asm的具體實(shí)現(xiàn):
class NuwaProcessor {
    /**
     * 處理jar
     * @param hashFile
     * @param jarFile
     * @param patchDir
     * @param map
     * @param includePackage
     * @param excludeClass
     * @return
     */
    public static processJar(File hashFile, File jarFile, File patchDir, Map map, HashSet includePackage, HashSet excludeClass) {
        if (jarFile) {
            /**
             * classes.jar dex后的文件
             */
            def optJar = new File(jarFile.getParent(), jarFile.name + ".opt")
 
            def file = new JarFile(jarFile);
            Enumeration enumeration = file.entries();
            JarOutputStream jarOutputStream = new JarOutputStream(new FileOutputStream(optJar));
 
            /**
             * 枚舉jar文件中的所有文件
             */
            while (enumeration.hasMoreElements()) {
                JarEntry jarEntry = (JarEntry) enumeration.nextElement();
                String entryName = jarEntry.getName();
                ZipEntry zipEntry = new ZipEntry(entryName);
 
                InputStream inputStream = file.getInputStream(jarEntry);
                jarOutputStream.putNextEntry(zipEntry);
                /**
                 * 以class結(jié)尾的文件并且在include中不在exclude中,并且不是cn/jiajixin/nuwa/包中的文件
                 */
                if (shouldProcessClassInJar(entryName, includePackage, excludeClass)) {
                    /**
                     * 構(gòu)造函數(shù)中注入字節(jié)碼
                     */
                    def bytes = referHackWhenInit(inputStream);
                    /**
                     * 寫(xiě)入子杰
                     */
                    jarOutputStream.write(bytes);
 
                    /**
                     * hash校驗(yàn)
                     */
                    def hash = DigestUtils.shaHex(bytes)
                    /**
                     * 加入hash值
                     */
                    hashFile.append(NuwaMapUtils.format(entryName, hash))
                    /**
                     * hash值與上一release版本不一樣則拷到對(duì)應(yīng)的目錄,作為patch的類
                     */
                    if (NuwaMapUtils.notSame(map, entryName, hash)) {
                        NuwaFileUtils.copyBytesToFile(bytes, NuwaFileUtils.touchFile(patchDir, entryName))
                    }
                } else {
                    /**
                     * 否則直接輸出文件不處理
                     */
                    jarOutputStream.write(IOUtils.toByteArray(inputStream));
                }
                jarOutputStream.closeEntry();
            }
            jarOutputStream.close();
            file.close();
            /**
             * 刪除jar文件
             */
            if (jarFile.exists()) {
                jarFile.delete()
            }
            /**
             * dex后的文件重命名為jar文件
             */
            optJar.renameTo(jarFile)
        }
 
    }
 
    //refer hack class when object init
    private static byte[] referHackWhenInit(InputStream inputStream) {
        ClassReader cr = new ClassReader(inputStream);
        ClassWriter cw = new ClassWriter(cr, 0);
        ClassVisitor cv = new ClassVisitor(Opcodes.ASM4, cw) {
            @Override
            public MethodVisitor visitMethod(int access, String name, String desc,
                                             String signature, String[] exceptions) {
 
                MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions);
                mv = new MethodVisitor(Opcodes.ASM4, mv) {
                    @Override
                    void visitInsn(int opcode) {
                        /**
                         * 如果是構(gòu)造函數(shù)
                         */
                        if ("".equals(name) & opcode == Opcodes.RETURN) {
                            /**
                             * 注入代碼
                             */
                            super.visitLdcInsn(Type.getType("Lcn/jiajixin/nuwa/Hack;"));
                        }
                        super.visitInsn(opcode);
                    }
                }
                return mv;
            }
 
        };
        cr.accept(cv, 0);
        return cw.toByteArray();
    }
 
    /**
     * 是否需要在preDex前處理
     * @param path
     * @return
     */
    public static boolean shouldProcessPreDexJar(String path) {
        return path.endsWith("classes.jar") & !path.contains("com.android.support") && !path.contains("/android/m2repository");
    }
 
    /**
     * jar中的文件是否需要處理
     * @param entryName
     * @param includePackage
     * @param excludeClass
     * @return
     */
    private static boolean shouldProcessClassInJar(String entryName, HashSet includePackage, HashSet excludeClass) {
        return entryName.endsWith(".class") & !entryName.startsWith("cn/jiajixin/nuwa/") && NuwaSetUtils.isIncluded(entryName, includePackage) && !NuwaSetUtils.isExcluded(entryName, excludeClass) && !entryName.contains("android/support/")
    }
 
    /**
     * 處理class
     * @param file
     * @return
     */
    public static byte[] processClass(File file) {
        def optClass = new File(file.getParent(), file.name + ".opt")
 
        FileInputStream inputStream = new FileInputStream(file);
        FileOutputStream outputStream = new FileOutputStream(optClass)
        /**
         * 對(duì)class注入字節(jié)碼
         */
        def bytes = referHackWhenInit(inputStream);
        outputStream.write(bytes)
        inputStream.close()
        outputStream.close()
        if (file.exists()) {
            file.delete()
        }
        optClass.renameTo(file)
        return bytes
    }
}
  1. 對(duì)于混淆時(shí),應(yīng)有mapping文件
    我們一般發(fā)布的apk,都是混淆過(guò)的,所以可能需要對(duì)某些混淆的類來(lái)“打補(bǔ)丁”,這里就需要在執(zhí)行progurad這個(gè)task的時(shí)候指定mapping文件,具體如下:
  def proguardTask = project.tasks.findByName("proguard${variant.name.capitalize()}")
  if (oldNuwaDir) {
            def mappingFile = NuwaFileUtils.getVariantFile(oldNuwaDir, variant, MAPPING_TXT)
            NuwaAndroidUtils.applymapping(proguardTask, mappingFile)
   }

NuwaAndroidUtils中的具體實(shí)現(xiàn):

  //使用mapping文件做proguard
   public static applymapping(DefaultTask proguardTask, File mappingFile) {
       if (proguardTask) {
           if (mappingFile.exists()) {
               proguardTask.applymapping(mappingFile)
           } else {
               println "$mappingFile does not exist"
           }
       }
   }
  1. 生成dex
/**
 * 對(duì)jar進(jìn)行dex操作
 * @param project
 * @param classDir
 * @return
 */
public static dex(Project project, File classDir) {
    if (classDir.listFiles().size()) {
        def sdkDir
        /**
         * 獲得sdk目錄
         */
        Properties properties = new Properties()
        File localProps = project.rootProject.file("local.properties")
        if (localProps.exists()) {
            properties.load(localProps.newDataInputStream())
            sdkDir = properties.getProperty("sdk.dir")
        } else {
            sdkDir = System.getenv("ANDROID_HOME")
        }
        if (sdkDir) {
            /**
             * 如果是windows系統(tǒng),加入后綴.bat
             */
            def cmdExt = Os.isFamily(Os.FAMILY_WINDOWS) ? '.bat' : ''
            def stdout = new ByteArrayOutputStream()
            /**
             * 拼接命令
             * dx --dex --output=patch.jar classDir
             * classDir是注入字節(jié)碼后的補(bǔ)丁目錄
             */
            project.exec {
                commandLine "${sdkDir}/build-tools/${project.android.buildToolsVersion}/dx${cmdExt}",
                        '--dex',
                        "--output=${new File(classDir.getParent(), PATCH_NAME).absolutePath}",
                        "${classDir.absolutePath}"
                standardOutput = stdout
            }
            def error = stdout.toString().trim()
            if (error) {
                println "dex error:" + error
            }
        } else {
            throw new InvalidUserDataException('$ANDROID_HOME is not defined')
        }
    }
}

實(shí)現(xiàn)

具體實(shí)現(xiàn)可以看Android 熱修復(fù)Nuwa的原理及Gradle插件源碼解析,寫(xiě)的很不錯(cuò),我這里就不再做無(wú)用功了。

參考

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,967評(píng)論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,273評(píng)論 3 415
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事?!?“怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 175,870評(píng)論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 62,742評(píng)論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,527評(píng)論 6 407
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 55,010評(píng)論 1 322
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,108評(píng)論 3 440
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 42,250評(píng)論 0 288
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,769評(píng)論 1 333
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,656評(píng)論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,853評(píng)論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,371評(píng)論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,103評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 34,472評(píng)論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 35,717評(píng)論 1 281
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,487評(píng)論 3 390
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,815評(píng)論 2 372

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