1. 程式人生 > >Android 熱修復方案Tinker(一) Application改造

Android 熱修復方案Tinker(一) Application改造

基於Tinker V1.7.5

這篇文章主要分析一下Tinker隔離Application.至於為什麼要隔離Application?可以參考上一篇 Android 熱修復方案分析文章中說到的Qzone方案,要給除了Application子類所有的類注入一個獨立dex中的類引用,來避免class被打上CLASS_ISPREVERIFIED標記.而這個獨立的dex是在Application啟動之後載入的,所以Application子類就不能插入獨立dex的引用也就不能進行修復.不僅如此,由於Application子類被打上CLASS_ISPREVERIFIED標記,那麼Application子類直接引用的物件就無法打補丁包,會丟擲pre-verified

異常.

在這種前提下還是想修復儘可能多的類怎麼辦?對於Qzone方案的解決方法就是把Application子類中的邏輯都抽離到一個獨立的類例如ApplicationProxy中,Application只引用這個ApplicationProxy類從而減小影響範圍.Tinker使用的是全量更新,補丁也是在Application中載入的,所以Tinker的Application也是不能修改的.並且由於Tinker的全量更新不能相容加固方案,如果app使用了加固的話可以使用usePreGeneratedPatchDex模式回退到Qzone方案上,這樣的話就需要面臨同樣的問題.

同時還因為Android N混合編譯與對熱補丁影響解析

,這會造成要修復的class被快取在App image中,App image中的class會插入PathClassLoader中,而PathClassLoader 載入補丁的時候不會替換快取的class,最終會導致在全量更新的情況下部分類是從base.apk中載入,部分類是從patch.dex中載入,丟擲IllegalAccessError.Tinker的解決方案是在執行時改寫PathClassLoader來載入類,讓App image中的快取失效.而Application這個入口類還是隻能用系統的PathClassLoader來載入,從這個角度來說也需要Application解耦出來.

Application 隔離

Tinker提供了兩種方式來隔離Application,分別是DefaultLifeCycle註解和繼承TinkerApplication,DefaultApplicationLike.推薦使用DefaultLifeCycle註解來隔離Application,這種方式會編譯自動生成Application,減少一些誤操作.而繼承TinkerApplication,DefaultApplicationLike其實就是自己寫出註解生成的程式碼,所以這篇文章就只介紹註解的方式.

@DefaultLifeCycle(
        application = "com.test.MyApplication",
        flags = ShareConstants.TINKER_ENABLE_ALL,
        loadVerifyFlag = false
)
public class ApplicationLike extends DefaultApplicationLike {
    public MyApplicationLike(Application application, int tinkerFlags, boolean tinkerLoadVerifyFlag,
                                 long applicationStartElapsedTime, long applicationStartMillisTime, Intent tinkerResultIntent,
                                 Resources[] resources, ClassLoader[] classLoader, AssetManager[] assetManager) {
        super(application, tinkerFlags, tinkerLoadVerifyFlag, 
                applicationStartElapsedTime, applicationStartMillisTime, tinkerResultIntent, 
                resources, classLoader, assetManager);
    }
}

在編譯的過程時註解執行完畢之後就會在設定的包路徑下生成出真實的Application java檔案,預設繼承TinkerApplication.將註解中配置的資料傳遞給建構函式的引數.然後被編譯成.class打包到dex檔案中.

public class MyApplication extends TinkerApplication {

    public MyApplication() {
        super(7, "cn.jesse.patchersample.ApplicationLike", "com.tencent.tinker.loader.TinkerLoader", false);
    }

}

DefaultLifeCycle註解包含四個屬性,分別是真實的application路徑(application), 補丁loader的路徑(loaderClass), 補丁支援不同檔案格式flag(flags), 是否每次都校驗補丁包MD5的flag(loadVerifyFlag).

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.SOURCE)
@Inherited
public @interface DefaultLifeCycle {

    /**
     * 真實的Application
     */
    String application();

    /**
     * patch loader
     */
    String loaderClass() default "com.tencent.tinker.loader.TinkerLoader";

    /**
     * 支援的檔案型別
     * ShareConstants.TINKERDISABLE:不支援任何型別的檔案
     * ShareConstants.TINKERDEXONLY:只支援dex檔案
     * ShareConstants.TINKERLIBRARYONLY:只支援library檔案
     * ShareConstants.TINKERDEXANDLIBRARY:只支援dex與res的修改
     * ShareConstants.TINKERENABLEALL:支援任何型別的檔案,也是我們通常的設定的模式
     */
    int flags();

    /**
     * 是否每次都校驗補丁包的MD5
     */
    boolean loadVerifyFlag() default false;
}

在註解處理器AnnotationProcessor中,根據註解的引數和真實的Application模板,生成真正的Application.

在Android studio中 annotation module必須是java library否則在處理anno的時候會找不到AbstractProcessor.如果需要除錯這部分程式碼的話可以通過配置gradle --deamon和加斷點來debug.

DefaultLifeCycle ca = e.getAnnotation(DefaultLifeCycle.class);

//拿到代理類的名稱和包名
String lifeCycleClassName = ((TypeElement) e).getQualifiedName().toString();
String lifeCyclePackageName = lifeCycleClassName.substring(0, lifeCycleClassName.lastIndexOf('.'));
lifeCycleClassName = lifeCycleClassName.substring(lifeCycleClassName.lastIndexOf('.') + 1);

//拼裝出真實的Application類名稱
String applicationClassName = ca.application();
if (applicationClassName.startsWith(".")) {
    applicationClassName = lifeCyclePackageName + applicationClassName;
}

//拆分出真實的Application類名稱和包名
String applicationPackageName = applicationClassName.substring(0, applicationClassName.lastIndexOf('.'));
applicationClassName = applicationClassName.substring(applicationClassName.lastIndexOf('.') + 1);

//拿到patch loader的名稱
String loaderClassName = ca.loaderClass();
if (loaderClassName.startsWith(".")) {
    loaderClassName = lifeCyclePackageName + loaderClassName;
}

//讀取Application模板檔案,將模板中的%KEY%佔位符全部替換成真實的資料
final InputStream is = AnnotationProcessor.class.getResourceAsStream(APPLICATION_TEMPLATE_PATH);
final Scanner scanner = new Scanner(is);
final String template = scanner.useDelimiter("\\A").next();
final String fileContent = template
    .replaceAll("%PACKAGE%", applicationPackageName)
    .replaceAll("%APPLICATION%", applicationClassName)
    .replaceAll("%APPLICATION_LIFE_CYCLE%", lifeCyclePackageName + "." + lifeCycleClassName)
    .replaceAll("%TINKER_FLAGS%", "" + ca.flags())
    .replaceAll("%TINKER_LOADER_CLASS%", "" + loaderClassName)
    .replaceAll("%TINKER_LOAD_VERIFY_FLAG%", "" + ca.loadVerifyFlag());

//將完整的Application程式碼寫入到跟代理Application的相同路徑下的檔案中
// 至此註解生成真實Application的工作就完成了
try {
    JavaFileObject fileObject = processingEnv.getFiler().createSourceFile(applicationPackageName + "." + applicationClassName);
    processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, "Creating " + fileObject.toUri());
    Writer writer = fileObject.openWriter();
    try {
        PrintWriter pw = new PrintWriter(writer);
        pw.print(fileContent);
        pw.flush();

    } finally {
        writer.close();
    }
} catch (IOException x) {
    processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, x.toString());
}

Application模板是把一些變化的屬性通過%KEY%的形式代替其中包含檔案包名,類名,建構函式名,tinker_flag, 代理Application, patcher loader, 和補丁校驗位.在註解編譯時就可以根據key對包名,Application名和建構函式的引數進行填充.

package %PACKAGE%;

import com.tencent.tinker.loader.app.TinkerApplication;

/**
 *
 * Generated application for tinker life cycle
 *
 */
public class %APPLICATION% extends TinkerApplication {

    public %APPLICATION%() {
        super(%TINKER_FLAGS%, "%APPLICATION_LIFE_CYCLE%", "%TINKER_LOADER_CLASS%", %TINKER_LOAD_VERIFY_FLAG%);
    }

}

Application 相關依賴

用了DefaultLifeCycle註解之後就把真實的Application和代理Application隔離開了.至於兩者之間是如何同步宣告週期,如何建立依賴關係,以及他們的職責是什麼?可以看一下這部分的類圖.

真實Application

在最早能獲取context的方法attachBaseContext處做相關的初始化工作.像時間統計,利用反射將TinkerLoader物件構建出來並呼叫tryLoad方法(校驗環境後加載補丁),反射創建出代理Application物件,同步Application生命週期和重置safe mode計數.

//記錄系統啟動時間和App啟動時刻
applicationStartElapsedTime = SystemClock.elapsedRealtime();
applicationStartMillisTime = System.currentTimeMillis();

//初始化patch loader, 並且呼叫tryLoad方法
loadTinker();
//確保代理Application物件已經被創建出來了
ensureDelegate();

//反射呼叫代理Application的`onBaseContextAttached`方法同步生命週期
try {
    Method method = ShareReflectUtil.findMethod(delegate, "onBaseContextAttached", Context.class);
    method.invoke(delegate, base);
} catch (Throwable t) {
    throw new TinkerRuntimeException("onBaseContextAttached method not found", t);
}

//重置safe mode計數, 當safe mode計數不小於三次時PatcherResultIntent會記錄patch失敗
if (useSafeMode) {
    String processName = ShareTinkerInternals.getProcessName(this);
    String preferName = ShareConstants.TINKER_OWN_PREFERENCE_CONFIG + processName;
    SharedPreferences sp = getSharedPreferences(preferName, Context.MODE_PRIVATE);
    sp.edit().putInt(ShareConstants.TINKER_SAFE_MODE_COUNT, 0).commit();
}

代理Application

因為要用ApplicationLike代理真實的Application,所以ApplicationLike就要持有Application, resources, classLoader, assetManager的引用.其中tinkerResultIntent屬性中記錄著TinkerLoader啟動和載入patch的狀態.

//Application,resource,assetManager,classLoader的引用
private final Application application;
private Resources[]    resources;
private ClassLoader[]  classLoader;
private AssetManager[] assetManager;

//用於記錄啟動載入patcher loader的狀態
private final Intent tinkerResultIntent;

//系統的存活時間和app啟動時刻
private final long applicationStartElapsedTime;
private final long applicationStartMillisTime;

//註解中的兩個flags
private final int tinkerFlags;
private final boolean tinkerLoadVerifyFlag;

同時還要暴露出常用的Application共有方法,供外部使用.

void onCreate();
void onLowMemory();
void onTrimMemory(int level);
void onTerminate();
void onConfigurationChanged(Configuration newConfig);
void onBaseContextAttached(Context base);

至此Application改造的分析就完成了,上面提到了在Application的attachBaseContext中通過反射調起了TinkerLoader的tryLoad方法.tryLoad方法是載入最終補丁包的入口,下篇文章會沿著這條線繼續分析下去.