Android热修复技术——QQ空间补丁方案解析(3)

本文涉及的产品
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: 要想实现热更新的目的,就必须在dex分包完成之后操作字节码文件。比较常用的字节码操作工具有ASM和javaassist。另外还需要在Gradle的构建过程中完成插桩的操作

如前文所述,要想实现热更新的目的,就必须在dex分包完成之后操作字节码文件。比较常用的字节码操作工具有ASM和javaassist。相比之下ASM提供一系列字节码指令,效率更高但是要求使用者对字节码操作有一定了解。而javaassist虽然效率差一些但是使用门槛较低,本文选择使用javaassist。关于javaassist可以参考Java 编程的动态性, 第四部分: 用 Javassist 进行类转换

正常App开发过程中,编译,打包过程都是Android Studio自动完成。如无特殊需求无需人为干预,但是要实现插桩就必须在Android Studio的自动化打包流程中加入插桩的过程。

1. Gradle,Task,Transform,Plugin

Android Studio采用Gradle作为构建工具,所有有必要了解一下Gradle构建的基本概念和流程。如果不熟悉可以参考一下下列文章:

Gradle的构建工程实质上是通过一系列的Task完成的,所以在构建apk的过程中就存在一个打包dex的任务。Gradle 1.5以上版本提供了一个新的API:Transform,官方文档对于Transform的描述是:

The goal of this API is to simplify injecting custom class manipulations without having to deal with tasks, and to offer more flexibility on what is manipulated. The internal code processing (jacoco, progard, multi-dex) have all moved to this new mechanism already in 1.5.0-beta1.

    1. The Dex class is gone. You cannot access it anymore through the variant API (the getter is still there for now but will throw an exception)
    1. Transform can only be registered globally which applies them to all the variants. We'll improve this shortly.
    1. There's no way to control ordering of the transforms.

Transform任务一经注册就会被插入到任务执行队列中,并且其恰好在dex打包task之前。所以要想实现插桩就必须创建一个Transform类的Task。

1.1 Task

Gradle的执行脚本就是由一系列的Task完成的。Task有一个重要的概念:input的output。每一个task需要有输入input,然后对input进行处理完成后在输出output。

1.2 Plugin

Gradle的另外一个重要概念就是Plugin。整个Gradle的构建体系都是有一个一个的plugin构成的,实际Gradle只是一个框架,提供了基本task和指定标准。而具体每一个task的执行逻辑都定义在一个个的plugin中。详细的概念可以参考:Writing Custom Plugins
在Android开发中我们经常使用到的plugin有:"com.android.application","com.android.library","java"等等。
每一个Plugin包含了一系列的task,所以执行gradle脚本的过程也就是执行目标脚本所apply的plugin所包含的task。

1.3 创建一个包含Transform任务的Plugin

    1. 新建一个module,选择library module,module名字必须叫BuildSrc
    1. 删除module下的所有文件,除了build.gradle,清空build.gradle中的内容
    1. 然后新建以下目录 src-main-groovy
    1. 修改build.gradle如下,同步
apply plugin: 'groovy'

repositories {
    jcenter()
}

dependencies {
    compile gradleApi()
    compile 'com.android.tools.build:gradle:1.5.0'
    compile 'org.javassist:javassist:3.20.0-GA'//javaassist依赖
}
    1. 像普通module一样新建package和类,不过这里的类是以groovy结尾,新建类的时候选择file,并且以.groovy作为后缀
    1. 自定义Plugin:
package com.hotpatch.plugin

import com.android.build.api.transform.*
import com.android.build.gradle.internal.pipeline.TransformManager
import org.apache.commons.codec.digest.DigestUtils
import org.apache.commons.io.FileUtils
import org.gradle.api.Project

public class PreDexTransform extends Transform {

    Project project;

    public PreDexTransform(Project project1) {
        this.project = project1;

        def libPath = project.project(":hack").buildDir.absolutePath.concat("/intermediates/classes/debug")
        println libPath
        Inject.appendClassPath(libPath)
        Inject.appendClassPath("/Users/liyazhou/Library/Android/sdk/platforms/android-24/android.jar")
    }
    @Override
    String getName() {
        return "preDex"
    }

    @Override
    Set<QualifiedContent.ContentType> getInputTypes() {
        return TransformManager.CONTENT_CLASS
    }

    @Override
    Set<QualifiedContent.Scope> getScopes() {
        return TransformManager.SCOPE_FULL_PROJECT
    }

    @Override
    boolean isIncremental() {
        return false
    }

    @Override
    void transform(Context context, Collection<TransformInput> inputs, Collection<TransformInput> referencedInputs, TransformOutputProvider outputProvider, boolean isIncremental) throws IOException, TransformException, InterruptedException {

        // 遍历transfrom的inputs
        // inputs有两种类型,一种是目录,一种是jar,需要分别遍历。
        inputs.each {TransformInput input ->
            input.directoryInputs.each {DirectoryInput directoryInput->

                //TODO 注入代码
                Inject.injectDir(directoryInput.file.absolutePath)

                def dest = outputProvider.getContentLocation(directoryInput.name,
                        directoryInput.contentTypes, directoryInput.scopes, Format.DIRECTORY)
                // 将input的目录复制到output指定目录
                FileUtils.copyDirectory(directoryInput.file, dest)
            }

            input.jarInputs.each {JarInput jarInput->


                //TODO 注入代码
                String jarPath = jarInput.file.absolutePath;
                String projectName = project.rootProject.name;
                if(jarPath.endsWith("classes.jar")
                        && jarPath.contains("exploded-aar/"+projectName)
                        // hotpatch module是用来加载dex,无需注入代码
                        && !jarPath.contains("exploded-aar/"+projectName+"/hotpatch")) {
                    Inject.injectJar(jarPath)
                }

                // 重命名输出文件(同目录copyFile会冲突)
                def jarName = jarInput.name
                def md5Name = DigestUtils.md5Hex(jarInput.file.getAbsolutePath())
                if(jarName.endsWith(".jar")) {
                    jarName = jarName.substring(0,jarName.length()-4)
                }
                def dest = outputProvider.getContentLocation(jarName+md5Name, jarInput.contentTypes, jarInput.scopes, Format.JAR)
                FileUtils.copyFile(jarInput.file, dest)
            }
        }
    }
}
  • 8.Inject.groovy, JarZipUtil.groovy
package com.hotpatch.plugin

import javassist.ClassPool
import javassist.CtClass
import org.apache.commons.io.FileUtils

public class Inject {

    private static ClassPool pool = ClassPool.getDefault()

    /**
     * 添加classPath到ClassPool
     * @param libPath
     */
    public static void appendClassPath(String libPath) {
        pool.appendClassPath(libPath)
    }

    /**
     * 遍历该目录下的所有class,对所有class进行代码注入。
     * 其中以下class是不需要注入代码的:
     * --- 1. R文件相关
     * --- 2. 配置文件相关(BuildConfig)
     * --- 3. Application
     * @param path 目录的路径
     */
    public static void injectDir(String path) {
        pool.appendClassPath(path)
        File dir = new File(path)
        if(dir.isDirectory()) {
            dir.eachFileRecurse { File file ->

                String filePath = file.absolutePath
                if (filePath.endsWith(".class")
                        && !filePath.contains('R$')
                        && !filePath.contains('R.class')
                        && !filePath.contains("BuildConfig.class")
                        // 这里是application的名字,可自行配置
                        && !filePath.contains("HotPatchApplication.class")) {
                    // 应用程序包名,可自行配置
                    int index = filePath.indexOf("com/hotpatch/plugin")
                    if (index != -1) {
                        int end = filePath.length() - 6 // .class = 6
                        String className = filePath.substring(index, end).replace('\\', '.').replace('/','.')
                        injectClass(className, path)
                    }
                }
            }
        }
    }

    /**
     * 这里需要将jar包先解压,注入代码后再重新生成jar包
     * @path jar包的绝对路径
     */
    public static void injectJar(String path) {
        if (path.endsWith(".jar")) {
            File jarFile = new File(path)


            // jar包解压后的保存路径
            String jarZipDir = jarFile.getParent() +"/"+jarFile.getName().replace('.jar','')

            // 解压jar包, 返回jar包中所有class的完整类名的集合(带.class后缀)
            List classNameList = JarZipUtil.unzipJar(path, jarZipDir)

            // 删除原来的jar包
            jarFile.delete()

            // 注入代码
            pool.appendClassPath(jarZipDir)
            for(String className : classNameList) {
                if (className.endsWith(".class")
                        && !className.contains('R$')
                        && !className.contains('R.class')
                        && !className.contains("BuildConfig.class")) {
                    className = className.substring(0, className.length()-6)
                    injectClass(className, jarZipDir)
                }
            }

            // 从新打包jar
            JarZipUtil.zipJar(jarZipDir, path)

            // 删除目录
            FileUtils.deleteDirectory(new File(jarZipDir))
        }
    }

    private static void injectClass(String className, String path) {
        CtClass c = pool.getCtClass(className)
        if (c.isFrozen()) {
            c.defrost()
        }
        def constructor = c.getConstructors()[0];
        constructor.insertAfter("System.out.println(com.hotpatch.hack.AntilazyLoad.class);")
        c.writeFile(path)
    }

}
package com.hotpatch.plugin

import java.util.jar.JarEntry
import java.util.jar.JarFile
import java.util.jar.JarOutputStream
import java.util.zip.ZipEntry

/**
 * Created by hp on 2016/4/13.
 */
public class JarZipUtil {

    /**
     * 将该jar包解压到指定目录
     * @param jarPath jar包的绝对路径
     * @param destDirPath jar包解压后的保存路径
     * @return 返回该jar包中包含的所有class的完整类名类名集合,其中一条数据如:com.aitski.hotpatch.Xxxx.class
     */
    public static List unzipJar(String jarPath, String destDirPath) {

        List list = new ArrayList()
        if (jarPath.endsWith('.jar')) {

            JarFile jarFile = new JarFile(jarPath)
            Enumeration<JarEntry> jarEntrys = jarFile.entries()
            while (jarEntrys.hasMoreElements()) {
                JarEntry jarEntry = jarEntrys.nextElement()
                if (jarEntry.directory) {
                    continue
                }
                String entryName = jarEntry.getName()
                if (entryName.endsWith('.class')) {
                    String className = entryName.replace('\\', '.').replace('/', '.')
                    list.add(className)
                }
                String outFileName = destDirPath + "/" + entryName
                File outFile = new File(outFileName)
                outFile.getParentFile().mkdirs()
                InputStream inputStream = jarFile.getInputStream(jarEntry)
                FileOutputStream fileOutputStream = new FileOutputStream(outFile)
                fileOutputStream << inputStream
                fileOutputStream.close()
                inputStream.close()
            }
            jarFile.close()
        }
        return list
    }

    /**
     * 重新打包jar
     * @param packagePath 将这个目录下的所有文件打包成jar
     * @param destPath 打包好的jar包的绝对路径
     */
    public static void zipJar(String packagePath, String destPath) {

        File file = new File(packagePath)
        JarOutputStream outputStream = new JarOutputStream(new FileOutputStream(destPath))
        file.eachFileRecurse { File f ->
            String entryName = f.getAbsolutePath().substring(packagePath.length() + 1)
            outputStream.putNextEntry(new ZipEntry(entryName))
            if(!f.directory) {
                InputStream inputStream = new FileInputStream(f)
                outputStream << inputStream
                inputStream.close()
            }
        }
        outputStream.close()
    }
}
    1. 在app module下build.gradle文件中添加新插件:apply plugin: com.hotpatch.plugin.Register

2. 创建hack.jar

创建一个单独的module,命名为com.hotpatch.plugin.AntilazyLoad:

package com.hotpatch.plugin
public class AntilazyLoad {
}

使用上一篇博客介绍的方法打包hack.jar。然后将hack.jar复制到app module下的assets目录中。另外注意:app module不能依赖hack module。之所以要创建一个hack module,同时人为地在dex打包过程中插入对其他hack.jar中类的依赖,就是要让apk文件在安装的时候不被打上CLASS_ISPREVERIFIED标记。
另外由于hack.jar位于assets中,所以必须要在加载patch_dex之前加载hack.jar。另外由于加载其他路径的dex文件都是在Application.onCreate()方法中执行的,此时还没有加载hack.jar,所以这就是为什么在上一章节插桩的时候不能在Application中插桩的原因。

插桩的过程介绍完了,整个热修复的过程也就差不多了,读者可以参考完整的代码进行demo试用:Hotpatch Demo

相关文章
|
2月前
|
Java 开发工具 Android开发
Android与iOS开发环境搭建全解析####
本文深入探讨了Android与iOS两大移动操作系统的开发环境搭建流程,旨在为初学者及有一定基础的开发者提供详尽指南。我们将从开发工具的选择、环境配置到第一个简单应用的创建,一步步引导读者步入移动应用开发的殿堂。无论你是Android Studio的新手还是Xcode的探索者,本文都将为你扫清开发道路上的障碍,助你快速上手并享受跨平台移动开发的乐趣。 ####
|
1月前
|
存储 Linux API
深入探索Android系统架构:从内核到应用层的全面解析
本文旨在为读者提供一份详尽的Android系统架构分析,从底层的Linux内核到顶层的应用程序框架。我们将探讨Android系统的模块化设计、各层之间的交互机制以及它们如何共同协作以支持丰富多样的应用生态。通过本篇文章,开发者和爱好者可以更深入理解Android平台的工作原理,从而优化开发流程和提升应用性能。
|
1月前
|
Java 调度 Android开发
安卓与iOS开发中的线程管理差异解析
在移动应用开发的广阔天地中,安卓和iOS两大平台各自拥有独特的魅力。如同东西方文化的差异,它们在处理多线程任务时也展现出不同的哲学。本文将带你穿梭于这两个平台之间,比较它们在线程管理上的核心理念、实现方式及性能考量,助你成为跨平台的编程高手。
|
2月前
|
安全 搜索推荐 Android开发
揭秘安卓与iOS系统的差异:技术深度对比
【10月更文挑战第27天】 本文深入探讨了安卓(Android)与iOS两大移动操作系统的技术特点和用户体验差异。通过对比两者的系统架构、应用生态、用户界面、安全性等方面,揭示了为何这两种系统能够在市场中各占一席之地,并为用户提供不同的选择。文章旨在为读者提供一个全面的视角,理解两种系统的优势与局限,从而更好地根据自己的需求做出选择。
143 2
|
2月前
|
安全 搜索推荐 Android开发
揭秘iOS与安卓系统的差异:一场技术与哲学的较量
在智能手机的世界里,iOS和Android无疑是两大巨头,它们不仅定义了操作系统的标准,也深刻影响了全球数亿用户的日常生活。本文旨在探讨这两个平台在设计理念、用户体验、生态系统及安全性等方面的本质区别,揭示它们背后的技术哲学和市场策略。通过对比分析,我们将发现,选择iOS或Android,不仅仅是选择一个操作系统,更是选择了一种生活方式和技术信仰。
|
2月前
|
开发框架 Dart Android开发
安卓与iOS的跨平台开发:Flutter框架深度解析
在移动应用开发的海洋中,Flutter作为一艘灵活的帆船,正引领着开发者们驶向跨平台开发的新纪元。本文将揭开Flutter神秘的面纱,从其架构到核心特性,再到实际应用案例,我们将一同探索这个由谷歌打造的开源UI工具包如何让安卓与iOS应用开发变得更加高效而统一。你将看到,借助Flutter,打造精美、高性能的应用不再是难题,而是变成了一场创造性的旅程。
|
2月前
|
安全 Java Linux
深入解析Android系统架构及其对开发者的意义####
【10月更文挑战第21天】 本文旨在为读者揭开Android操作系统架构的神秘面纱,探讨其如何塑造现代移动应用开发格局。通过剖析Linux内核、硬件抽象层、运行时环境及应用程序框架等关键组件,揭示Android平台的强大功能与灵活性。文章强调了理解Android架构对于开发者优化应用性能、提升用户体验的重要性,并展望了未来技术趋势下Android的发展方向。 ####
58 0
|
2月前
|
搜索推荐 安全 Android开发
安卓与iOS的哲学对话:技术生态中的选择与命运
【10月更文挑战第24天】 在智能设备的世界里,安卓和iOS不仅是操作系统的简单对立,它们代表了不同的技术哲学和生态策略。本文将探讨这两种系统背后的设计理念、用户体验差异以及它们如何塑造我们的数字生活,从而引发对于“我们如何选择技术”这一命题的深入思考。
|
2月前
|
安全 5G Android开发
安卓与iOS的较量:技术深度解析
【10月更文挑战第24天】 在移动操作系统领域,安卓和iOS无疑是两大巨头。本文将深入探讨这两个系统的技术特点、优势和不足,以及它们在未来可能的发展方向。我们将通过对比分析,帮助读者更好地理解这两个系统的本质和内涵,从而引发对移动操作系统未来发展的深思。
60 0
|
程序员 Android开发 开发者
《深入探索Android热修复技术原理》实体书正式出版!给你一个更充实全面的热修复技术视角
我们在2017年6月发布了《深入探索 Android 热修复技术原理》一书的电子版,电子版发布以后得到了很好的反响。之后,我们对电子版内容做了认真的整理和校对,对全书内容进行了全面的充实,于2018年秋正式出版了《深入探索 Android 热修复技术原理》的彩页印刷版实体书,实体书内容更加精彩!
3892 0
《深入探索Android热修复技术原理》实体书正式出版!给你一个更充实全面的热修复技术视角