Gradle學習筆記(六)-- NuwaGradle解析

簡介

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

預備知識

在制作補丁的時候,首先要對一個apk的生成過程有一個大致的了解。了解了如何生成apk,才能知道我們應該在哪一步去獲取制作補丁的“原材料”。

apk的生成過程

大致步驟如下:

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

思路

我們制作補丁時,必須防止類被打上ISPREVERIFIED這個標記。
原理一個類直接引用到的類不在同一個dex中即可。這樣,就能防止類被打上ISPREVERIFIED標記并能進行熱更新。

簡單來說,就是將所有類的構造函數中,引用另一個hack.dex中的類,這個類叫Hack.class,然后在加載補丁patch.dex前動態加載這個hack.dex,但是有一個類的構造函數中不能引用Hack.class,這個類就是Application類的子類,一旦這個類的構造函數中加入Hack.class這個類,那么程序運行時就會找不到Hack.class這個類,因為還沒有被加載。

生成補丁有幾個需要注意的地方:

  1. 尋找插入task的點
    在gradle1.5以下的版本中,我們可以找到dex task ,而gradle1.5以上,dex task 無法找到。
    這里便用 transformClassesWithDexForXXX 這個task。在其之前執行生成改造類的工作。
    沒有開啟Multidex的情況,存在一個preDex的Task。preDex會在dex任務之前把所有的庫工程和第三方jar包提前打成dex,下次運行只需重新dex被修改的庫,以此節省時間。dex任務會把preDex生成的dex文件和主工程中的class文件一起生成class.dex,這樣就需要針對有無preDex,做不同的修改字節碼策略即可。
  2. 改造字節類,在類的構造函數中插入另一個dex中的類
    對于java的.class的改造,我們一般會用到asm或者javasist 這兩類工具,而NuwaGradle則采用了asm,和同事溝通,發現使用javasist時,有些類的無參構造函數可能無法找到。這里就貼下asm的具體實現:
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結尾的文件并且在include中不在exclude中,并且不是cn/jiajixin/nuwa/包中的文件
                 */
                if (shouldProcessClassInJar(entryName, includePackage, excludeClass)) {
                    /**
                     * 構造函數中注入字節碼
                     */
                    def bytes = referHackWhenInit(inputStream);
                    /**
                     * 寫入子杰
                     */
                    jarOutputStream.write(bytes);
 
                    /**
                     * hash校驗
                     */
                    def hash = DigestUtils.shaHex(bytes)
                    /**
                     * 加入hash值
                     */
                    hashFile.append(NuwaMapUtils.format(entryName, hash))
                    /**
                     * hash值與上一release版本不一樣則拷到對應的目錄,作為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) {
                        /**
                         * 如果是構造函數
                         */
                        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)
        /**
         * 對class注入字節碼
         */
        def bytes = referHackWhenInit(inputStream);
        outputStream.write(bytes)
        inputStream.close()
        outputStream.close()
        if (file.exists()) {
            file.delete()
        }
        optClass.renameTo(file)
        return bytes
    }
}
  1. 對于混淆時,應有mapping文件
    我們一般發布的apk,都是混淆過的,所以可能需要對某些混淆的類來“打補丁”,這里就需要在執行progurad這個task的時候指定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中的具體實現:

  //使用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
/**
 * 對jar進行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系統,加入后綴.bat
             */
            def cmdExt = Os.isFamily(Os.FAMILY_WINDOWS) ? '.bat' : ''
            def stdout = new ByteArrayOutputStream()
            /**
             * 拼接命令
             * dx --dex --output=patch.jar classDir
             * classDir是注入字節碼后的補丁目錄
             */
            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')
        }
    }
}

實現

具體實現可以看Android 熱修復Nuwa的原理及Gradle插件源碼解析,寫的很不錯,我這里就不再做無用功了。

參考

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容