早在16年9月份,美團技術團隊就寫過一篇文章描述 Android 熱補丁框架Robust的簡單實現原理,可是並無開源;而後在17年3月份,美團團隊宣佈正式開源 Robust而且配套了自動打補丁包工具。本系列文章主要解析Robust實現原理,分爲幾個方面javascript
本文爲第一篇,主要講解補丁加載過程和基礎包插樁過程,分析版本 0.3.2
。html
不得不說 InstantRun 真是個好東西。目前主流的熱修復框架都有或多或少的參考 InstantRun 的某些技術點,好比 Tinker 的官方文章中明確考慮過 InstantRun 中的 Application 替換,雖然最後沒有采用,可是身爲其兄弟庫的 TinkerPatch 中一鍵接入方案就採用的該技術點。關於該技術點,能夠參考我以前寫的一篇文章 一鍵接入Tinker 。java
咱們知道,InstantRun 對應三種更新機制:android
若是你還不熟悉 InstantRun,請參考個人這篇文章從Instant run談Android替換Application和動態加載機制git
而這篇文章的主角 Robust ,其熱修復的關鍵技術點就是採用了 InstantRun 中的熱更新機制,對應於多 ClassLoader 的動態加載方案,即一個 dex 文件對應一個新建 ClassLoader 。github
Robust 的原理能夠簡單描述爲:安全
ChangeQuickRedirect
靜態變量的邏輯咱們來分別分析。app
打基礎包時,Robust 爲每一個類新增了一個類型爲 ChangeQuickRedirect
的靜態變量,而且在每一個方法前,增長判斷該變量是否爲空的邏輯,若是不爲空,走打基礎包時插樁的邏輯,不然走正常邏輯。咱們反編譯出基礎包中的代碼以下:框架
//SecondActivity
public static ChangeQuickRedirect u;
protected void onCreate(Bundle bundle) {
if (u != null) {
if (PatchProxy.isSupport(new Object[]{bundle}, this, u, false, 78)) {
PatchProxy.accessDispatchVoid(new Object[]{bundle}, this, u, false, 78);
return;
}
}
super.onCreate(bundle);
...
}複製代碼
對應於補丁文件,須要有三個文件ide
PatchesInfoImpl
用於記錄修改的類,及其對應的 ChangeQuickRedirect
接口的實現,咱們反編譯補丁包得出如下結果,其中的類名是混淆後的。public class PatchesInfoImpl implements PatchesInfo {
public List getPatchedClassesInfo() {
List arrayList = new ArrayList();
arrayList.add(new PatchedClassInfo("com.meituan.sample.robusttest.l", "com.meituan.robust.patch.SampleClassPatchControl"));
arrayList.add(new PatchedClassInfo("com.meituan.sample.robusttest.p", "com.meituan.robust.patch.SuperPatchControl"));
arrayList.add(new PatchedClassInfo("com.meituan.sample.SecondActivity", "com.meituan.robust.patch.SecondActivityPatchControl"));
EnhancedRobustUtils.isThrowable = false;
return arrayList;
}
}複製代碼
xxxPatchControl
是 ChangeQuickRedirect
接口的具體實現,是一個代理,具體的替換方法是在 xxxPatch
類中public class SecondActivityPatchControl implements ChangeQuickRedirect {
...
public boolean isSupport(String methodName, Object[] paramArrayOfObject) {
return "78:79:90:".contains(methodName.split(":")[3]);
}
public Object accessDispatch(String methodName, Object[] paramArrayOfObject) {
try {
SecondActivityPatch secondActivityPatch;
...
Object obj = methodName.split(":")[3];
if ("78".equals(obj)) {
secondActivityPatch.onCreate((Bundle) paramArrayOfObject[0]);
}
if ("79".equals(obj)) {
return secondActivityPatch.getTextInfo((String) paramArrayOfObject[0]);
}
if ("90".equals(obj)) {
secondActivityPatch.RobustPubliclambda$onCreate$0((View) paramArrayOfObject[0]);
}
return null;
} catch (Throwable th) {
th.printStackTrace();
}
}
}複製代碼
最終調用 accessDispatch
方法,該方法會根據傳遞過來的方法簽名,調用xxxPatch
的修改過的方法。
xxxPatch
具體的替換實現類,代碼就不貼了。其過程能夠簡單描述爲,下發補丁包後,新建 DexClassLoader 加載補丁 dex 文件,反射獲得 PatchesInfoImpl
class,並建立其對象,調用 getPatchedClassesInfo()
方法獲得哪些修改的類(好比 SecondActivity),而後再經過反射循環拿到每一個修改類在當前環境中的的class,將其中類型爲 ChangeQuickRedirect
的靜態變量反射修改成 xxxPatchControl.java
這個class new 出來的對象。
用官方的一種圖很好的表達了替換原理。
demo中的補丁加載就一句
new PatchExecutor(getApplicationContext(), new PatchManipulateImp(), new Callback()).start();複製代碼
PatchExecutor
是個 Thread
public class PatchExecutor extends Thread {
@Override
public void run() {
...
applyPatchList(patches);
...
}
/** * 應用補丁列表 */
protected void applyPatchList(List<Patch> patches) {
...
for (Patch p : patches) {
...
currentPatchResult = patch(context, p);
...
}
}
protected boolean patch(Context context, Patch patch) {
...
DexClassLoader classLoader = new DexClassLoader(patch.getTempPath(), context.getCacheDir().getAbsolutePath(),
null, PatchExecutor.class.getClassLoader());
patch.delete(patch.getTempPath());
...
try {
patchsInfoClass = classLoader.loadClass(patch.getPatchesInfoImplClassFullName());
patchesInfo = (PatchesInfo) patchsInfoClass.newInstance();
} catch (Throwable t) {
...
}
...
for (PatchedClassInfo patchedClassInfo : patchedClasses) {
...
try {
oldClass = classLoader.loadClass(patchedClassName.trim());
Field[] fields = oldClass.getDeclaredFields();
for (Field field : fields) {
if (TextUtils.equals(field.getType().getCanonicalName(), ChangeQuickRedirect.class.getCanonicalName()) && TextUtils.equals(field.getDeclaringClass().getCanonicalName(), oldClass.getCanonicalName())) {
changeQuickRedirectField = field;
break;
}
}
...
try {
patchClass = classLoader.loadClass(patchClassName);
Object patchObject = patchClass.newInstance();
changeQuickRedirectField.setAccessible(true);
changeQuickRedirectField.set(null, patchObject);
} catch (Throwable t) {
...
}
} catch (Throwable t) {
...
}
}
return true;
}
}複製代碼
開啓一個子線程,經過指定的路徑去讀patch文件的jar包,patch文件能夠爲多個,每一個patch文件對應一個 DexClassLoader 去加載,每一個patch文件中存在PatchInfoImp,經過遍歷其中的類信息進而反射修改其中 ChangeQuickRedirect
對象的值。
相似 InstantRun , Robust 也是使用 Transform API 修改字節碼文件,該 API 容許第三方插件在 .class 文件打包爲 dex 文件以前操做編譯好的 .class 字節碼文件。
Robust 中的 Gradle-Plugin
就是操做字節碼的名爲 robust
的 gradle 插件項目。咱們來簡單看下實現。
class RobustTransform extends Transform implements Plugin<Project> {
...
@Override
void apply(Project target) {
//解析項目下robust.xml配置文件
robust = new XmlSlurper().parse(new File("${project.projectDir}/${Constants.ROBUST_XML}"))
...
project.android.registerTransform(this)
project.afterEvaluate(new RobustApkHashAction())
}
@Override
void transform(Context context, Collection<TransformInput> inputs, Collection<TransformInput> referencedInputs, TransformOutputProvider outputProvider, boolean isIncremental) throws IOException, TransformException, InterruptedException {
...
ClassPool classPool = new ClassPool()
project.android.bootClasspath.each {
logger.debug "android.bootClasspath " + (String) it.absolutePath
classPool.appendClassPath((String) it.absolutePath)
}
...
def box = ConvertUtils.toCtClasses(inputs, classPool)
insertRobustCode(box, jarFile)
writeMap2File(methodMap, Constants.METHOD_MAP_OUT_PATH)
...
}
}複製代碼
首先讀取 robust.xml 配置文件並初始化,可配置選項包括:
而後經過 Transform
API 調用 transform()
方法,掃描全部類加入到 classPool
中,調用 insertRobustCode()
方法。
def insertRobustCode(List<CtClass> box, File jarFile) {
ZipOutputStream outStream=new JarOutputStream(new FileOutputStream(jarFile));
new ForkJoinPool().submit {
box.each { ctClass ->
if (isNeedInsertClass(ctClass.getName())) {
//將class設置爲public ctClass.setModifiers(AccessFlag.setPublic(ctClass.getModifiers()))
boolean addIncrementalChange = false;
ctClass.declaredBehaviors.findAll {
//規避接口和無方法類
if (ctClass.isInterface() || ctClass.declaredMethods.length < 1) {
return false;
}
if (!addIncrementalChange) {
//插入 public static ChangeQuickRedirect changeQuickRedirect;
addIncrementalChange = true;
ClassPool classPool = it.declaringClass.classPool
CtClass type = classPool.getOrNull(Constants.INTERFACE_NAME);
CtField ctField = new CtField(type, Constants.INSERT_FIELD_NAME, ctClass);
ctField.setModifiers(AccessFlag.PUBLIC | AccessFlag.STATIC)
ctClass.addField(ctField)
logger.debug "ctClass: " + ctClass.getName();
}
if (it.getMethodInfo().isStaticInitializer()) {
return false
}
// synthetic 方法暫時不aop 好比AsyncTask 會生成一些同名 synthetic方法,對synthetic 以及private的方法也插入的代碼,主要是針對lambda表達式
if ((it.getModifiers() & AccessFlag.SYNTHETIC) != 0 && !AccessFlag.isPrivate(it.getModifiers())) {
return false
}
//不支持構造方法
if (it.getMethodInfo().isConstructor()) {
return false
}
//規避抽象方法
if ((it.getModifiers() & AccessFlag.ABSTRACT) != 0) {
return false
}
//規避NATIVE方法
if ((it.getModifiers() & AccessFlag.NATIVE) != 0) {
return false
}
//規避接口
if ((it.getModifiers() & AccessFlag.INTERFACE) != 0) {
return false
}
if (it.getMethodInfo().isMethod()) {
if (AccessFlag.isPackage(it.modifiers)) {
it.setModifiers(AccessFlag.setPublic(it.modifiers))
}
//判斷是否有方法調用,返回是否插莊
boolean flag = modifyMethodCodeFilter(it)
if (!flag) {
return false
}
}
//方法過濾
if (isExceptMethodLevel && exceptMethodList != null) {
for (String exceptMethod : exceptMethodList) {
if (it.name.matches(exceptMethod)) {
return false
}
}
}
if (isHotfixMethodLevel && hotfixMethodList != null) {
for (String name : hotfixMethodList) {
if (it.name.matches(name)) {
return true
}
}
}
return !isHotfixMethodLevel
}.each { ctBehavior ->
// methodMap must be put here
methodMap.put(ctBehavior.longName, insertMethodCount.incrementAndGet());
try {
if (ctBehavior.getMethodInfo().isMethod()) {
boolean isStatic = ctBehavior.getModifiers() & AccessFlag.STATIC;
CtClass returnType = ctBehavior.getReturnType0();
String returnTypeString = returnType.getName();
def body = "if (${Constants.INSERT_FIELD_NAME} != null) {"
body += "Object argThis = null;"
if (!isStatic) {
body += "argThis = \$0;"
}
body += " if (com.meituan.robust.PatchProxy.isSupport(\$args, argThis, ${Constants.INSERT_FIELD_NAME}, $isStatic, " + methodMap.get(ctBehavior.longName) + ")) {"
body += getReturnStatement(returnTypeString, isStatic, methodMap.get(ctBehavior.longName));
body += " }"
body += "}"
ctBehavior.insertBefore(body);
}
} catch (Throwable t ) {
logger.error "ctClass: " + ctClass.getName() + " error: " + t.toString();
}
}
}
zipFile(ctClass.toBytecode(),outStream,ctClass.name.replaceAll("\\.","/")+".class");
}
}.get()
outStream.close();
logger.debug "robust insertMethodCount: " + insertMethodCount.get()
}複製代碼
該方法作了如下幾件事:
public static ChangeQuickRedirect changeQuickRedirect;
最後調用 writeMap2File()
將插樁的方法信息寫入 robust/methodsMap.robust 文件中,此文件和混淆的mapping文件須要備份。
到這裏本篇文章結束,主要講了下基礎原理、補丁加載流程和插樁過程。咱們也能夠簡單的對 Robust 作下總結。
優勢:
固然,有優勢就會有缺點:
總的來講,Robust是可用的、高穩定性的、成功率很高(官方說99.9%)的、無侵入的一款優秀的熱修復框架。