Android熱修復技術——QQ空間補丁方案解析(3)
如前文所述,要想實現熱更新的目的,就必須在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.
- 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)
-
- Transform can only be registered globally which applies them to all the variants. We`ll improve this shortly.
-
- 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
-
- 新建一個module,選擇library module,module名字必須叫BuildSrc
-
- 刪除module下的所有檔案,除了build.gradle,清空build.gradle中的內容
-
- 然後新建以下目錄 src-main-groovy
-
- 修改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依賴
}
-
- 像普通module一樣新建package和類,不過這裡的類是以groovy結尾,新建類的時候選擇file,並且以.groovy作為字尾
-
- 自定義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()
}
}
-
- 在app module下build.gradle檔案中新增新外掛:
apply plugin: com.hotpatch.plugin.Register
- 在app module下build.gradle檔案中新增新外掛:
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
相關文章
- 安卓App熱補丁動態修復技術介紹安卓APP
- Android 熱補丁動態修復框架小結Android框架
- tinker熱修復——補丁載入合成
- 2018深入解析Android熱修復技術Android
- tinker熱修復——dex補丁載入過程
- 深入探索Android熱修復技術原理讀書筆記 —— 熱修復技術介紹Android筆記
- 深入探索Android熱修復技術原理讀書筆記 —— 程式碼熱修復技術Android筆記
- 深入探索Android熱修復技術原理讀書筆記 —— 資源熱修復技術Android筆記
- 淺談Android主流熱修復技術Android
- tinker熱修復——資源補丁載入過程
- 【Android 熱修復】美團Robust熱修復框架原理解析Android框架
- Android 中外掛化學習—教你實現熱補丁動態修復Android
- 筆記 深入探索Android熱修復技術原理筆記Android
- Android熱補丁之Robust(二)自動化補丁原理解析Android
- Openssl多個安全補丁簡易分析危害及修復方案
- Android 熱修復 Tinker Gradle Plugin 解析AndroidGradlePlugin
- Android 熱修復Android
- 谷歌釋出7月Android補丁 修復多個致命漏洞谷歌Android
- 微軟三月補丁更新修復3個0day漏洞微軟
- Android熱修復原理Android
- Android 增量更新完全解析 是增量不是熱修復Android
- Android熱修復原理(一)熱修復框架對比和程式碼修復Android框架
- 谷歌釋出11月Android安全補丁:共修復38處漏洞谷歌Android
- Oracle可恢復空間分配技術Oracle
- Android熱補丁之Robust(三)坑和解Android
- 高精地圖技術專欄 | 基於空間連續性的異常3D點雲修復技術地圖3D
- 你值得知道的Android 熱修復,以及熱修復原理Android
- Android 熱修復總結Android
- 淺析“熱更新”(熱修復)解決方案
- 蘋果釋出補丁修復2018 MacBook Pro過熱降頻問題蘋果Mac
- 利用可恢復空間分配技術自動分配表空間
- 微信 Android 熱補丁實踐演進之路Android
- 微信Android熱補丁實踐演進之路Android
- 微軟修復Bug的補丁產生了新的Bug微軟
- w10系統打不開qq空間如何修復_win10電腦上qq空間打不開怎麼辦Win10
- Oracle表空間時間點恢復技術TSPITROracle
- 微軟11月補丁日,修復12個關鍵漏洞微軟
- Ubuntu釋出PHP重要補丁修復多個PHP漏洞UbuntuPHP