替換Application
按照TInker官方文檔,接入Tinker Patch需要把原來項目中Application的代碼移動到ApplicationLike中,然而這可不是件小事情,我們的application可能包含各種初始化,并且很多地方調(diào)用了application的public方法。比如
import android.support.multidex.MultiDexApplication;
public class MyApp extends MultiDexApplication {
private static MyApp sInstance;
@Override
public void onCreate() {
super.onCreate();
sInstance = this;
initNetWork();
initFresco();
initStetho();
initXXX1();
initXXX2();
ActivityFetcher.init();
}
public String getAccount() {
return "xxx";
}
public String getXXX() {
return "xxx";
}
private void initXXX2() {
}
private void initXXX1() {
}
private void initNetWork() {
}
private void initFresco() {
}
private void initStetho() {
}
public static MyApp getApplication() {
return sInstance;
}
}
調(diào)用MyApp.getApplication(),注冊activity監(jiān)聽
public class ActivityFetcher {
private static List<WeakReference<Activity>> sActivities = new ArrayList<>();
public static void init() {
MyApp.getApplication().registerActivityLifecycleCallbacks(new Application.ActivityLifecycleCallbacks() {
@Override
public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
sActivities.add(new WeakReference<Activity>(activity));
}
@Override
public void onActivityDestroyed(Activity activity) {
for (WeakReference<Activity> reference : sActivities) {
if (reference.get() == activity) {
sActivities.remove(reference);
return;
}
}
}
});
}
public static List<WeakReference<Activity>> getActivities() {
return Collections.unmodifiableList(sActivities);
}
}
再比如需要context的地方直接把 MyApp.getApplication()作為了參數(shù)
Toast.makeText(MyApp.getApplication(), "請求失敗", Toast.LENGTH_SHORT).show();
還有某些地方調(diào)用了application種的各種public方法
String account = MyApp.getApplication().getAccount();
String xxx = MyApp.getApplication().getXXX();
如果把a(bǔ)pplication的代碼都搬到ApplicationLike中的話改動量可能會很大,有沒有更好的方案呢?肯定是有的。
首選我們看一下Application的源碼,發(fā)現(xiàn)并沒有什么特殊的,只不過是繼承自ContextWrapper,并新增了幾個public registXXX方法,所以Application其實只是一個代理,真整的context其實是ContextImpl對象,Application繼承來得所有方法其實最終都是交給了ContextImpl對象處理。Application對象的創(chuàng)建過程大致如下
相關(guān)代碼如下
LoadedApk#makeApplication
public Application makeApplication(boolean forceDefaultAppClass,
Instrumentation instrumentation) {
Application app = null;
ContextImpl appContext = ContextImpl.createAppContext(mActivityThread, this);
app = mActivityThread.mInstrumentation.newApplication(
cl, appClass, appContext);
appContext.setOuterContext(app);
instrumentation.callApplicationOnCreate(app);
return app;
}
ContextImpl#createAppContext
static ContextImpl createAppContext(ActivityThread mainThread, LoadedApk packageInfo) {
ContextImpl context = new ContextImpl(null, mainThread, packageInfo, null, null, null, 0,
null);
context.setResources(packageInfo.getResources());
return context;
}
Instrumentation#newApplication
static public Application newApplication(Class<?> clazz, Context context)
throws InstantiationException, IllegalAccessException,
ClassNotFoundException {
Application app = (Application)clazz.newInstance();
app.attach(context);
return app;
}
Application#attach
final void attach(Context context) {
attachBaseContext(context);
}
Instrumentation#callApplicationOnCreate
public void callApplicationOnCreate(Application app) {
app.onCreate();
}
可以看到只不過是先new了一個ContextImpl對象,然后通過反射創(chuàng)建了一個Application對象,并把contextimpl對象設(shè)置給了Application,然后調(diào)用了Application的onCreate等方法。所以我們完全可以手動new這個MyApp,然后把真正的context attach給MyApp,然后調(diào)用相關(guān)的方法即可。
所以我們可以這么做
public class MyApp extends TinkerCtxWrap {
private static MyApp sInstance;
@Override
public void onCreate() {
super.onCreate();
sInstance = this;
initNetWork();
initFresco();
initStetho();
initXXX1();
initXXX2();
ActivityFetcher.init();
}
public String getAccount() {
return "xxx";
}
public String getXXX() {
return "xxx";
}
private void initXXX2() {
}
private void initXXX1() {
}
private void initNetWork() {
}
private void initFresco() {
}
private void initStetho() {
}
public static MyApp getApplication() {
return sInstance;
}
}
public class TinkerCtxWrap extends Application {
@Override
public void attachBaseContext(Context base) {
super.attachBaseContext(base);
}
@Override
public void registerComponentCallbacks(ComponentCallbacks callback) {
Application application = getRealApplication();
application.registerComponentCallbacks(callback);
}
@Override
public void unregisterComponentCallbacks(ComponentCallbacks callback) {
Application application = getRealApplication();
application.unregisterComponentCallbacks(callback);
}
public void registerActivityLifecycleCallbacks(Application.ActivityLifecycleCallbacks callback) {
Application application = getRealApplication();
application.registerActivityLifecycleCallbacks(callback);
}
public void unregisterActivityLifecycleCallbacks(Application.ActivityLifecycleCallbacks callback) {
Application application = getRealApplication();
application.unregisterActivityLifecycleCallbacks(callback);
}
@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR2)
public void registerOnProvideAssistDataListener(Application.OnProvideAssistDataListener callback) {
Application application = getRealApplication();
application.registerOnProvideAssistDataListener(callback);
}
@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR2)
public void unregisterOnProvideAssistDataListener(Application.OnProvideAssistDataListener callback) {
Application application = getRealApplication();
application.unregisterOnProvideAssistDataListener(callback);
}
protected Application getRealApplication() {
return (Application) getBaseContext().getApplicationContext();
}
}
public class ApplicationLike extends DefaultApplicationLike {
private TinkerCtxWrap ctxWrap;
public ApplicationLike(Application application, int i, boolean b, long l, long l1, Intent intent) {
super(application, i, b, l, l1, intent);
}
public void onCreate() {
super.onCreate();
ctxWrap.onCreate();
}
@Override
public void onBaseContextAttached(Context base) {
super.onBaseContextAttached(base);
MultiDex.install(base);
ctxWrap = new MyApp();
ctxWrap.attachBaseContext(base);
}
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
ctxWrap.onConfigurationChanged(newConfig);
}
@Override
public void onLowMemory() {
super.onLowMemory();
ctxWrap.onLowMemory();
}
@Override
public void onTerminate() {
super.onTerminate();
ctxWrap.onTerminate();
}
@Override
public void onTrimMemory(int level) {
super.onTrimMemory(level);
ctxWrap.onTrimMemory(level);
}
}
我們只是把MyApp的父類改成了TinkerCtxWrap,TinkerCtxWrap其實是繼承自Application,并把a(bǔ)ttachBaseContext改成了public,然后新增了幾個registerXXX等方法。
然后我們在ApplicationLike的onBaseContextAttached中先調(diào)用Multidex.install加載了所有dex文件,然后new了MyApp,并把context attach了進(jìn)去,然后在ApplicationLike的生命周期方法中調(diào)用了MyApp的對應(yīng)方法。這樣我們就不需要把原來MyApp中的代碼搬到ApplicationLike中了,并且MyApp還是繼承自Application,需要application對象的地方仍舊可以使用MyApp.getApplication()獲取。不過要注意,MyApp其實只是一個代理了,真正的Application其實是TinkerApplication,所以activity等中通過getApplicationContext得到的context就不能強(qiáng)轉(zhuǎn)成MyApp了。
打包失敗
Too many classes in --main-dex-list, main dex capacity exceeded
用了一年tinker,最近打包突然一直失敗,不開啟tinker打包可以成功,開啟打包就提示Too many classes in --main-dex-list, main dex capacity exceeded。意思是主dex中類太多了,我們知道Application及引用的類會被打倒主dex中,沒辦法,只能精簡一下,由于我們的業(yè)務(wù)代碼最開始執(zhí)行的地方是MyApp,所以理論上所有的業(yè)務(wù)代碼都可以不在主dex中,所以我們把ApplicationLike的onBaseContextAttached改成了通過反射方式加載
@Override
public void onBaseContextAttached(Context base) {
super.onBaseContextAttached(base);
MultiDex.install(base);
try {
ctxWrap = (TinkerCtxWrap) Class.forName("xx.xx.MyApp").newInstance();
} catch (Exception e) {
throw new RuntimeException("創(chuàng)建MyApp失敗");
}
ctxWrap.attachBaseContext(base);
}
本以為這樣就不會把MyApp打到主dex中,從而所有的業(yè)務(wù)代碼都不會打到主dex中,不過試了下還是打包失敗。于是我們又改成了這樣,唯一的區(qū)別就是new了一個String,同時我們還把stetho等線上包用不到的庫去掉,終于打包成功了。
@Override
public void onBaseContextAttached(Context base) {
super.onBaseContextAttached(base);
MultiDex.install(base);
try {
ctxWrap = (TinkerCtxWrap) Class.forName(new String("xx.xx.MyApp")).newInstance();
} catch (Exception e) {
throw new RuntimeException("創(chuàng)建MyApp失敗");
}
ctxWrap.attachBaseContext(base);
}
然后我們看了下編譯時生成的maindexlist文件,里面列出了將近8000條!不開啟tinker時這個文件中只有不到1000條,經(jīng)過多次嘗試,發(fā)現(xiàn)8000大概是上限,這就意味著我們下次發(fā)版時可能又無法打包了。
反編譯了下勉強(qiáng)打包成功的apk包,發(fā)現(xiàn)主dex中有好多kotlin的類,貌似所有打了@SerializedName,@Deprecated等注解的類也都打到了主dex中,另外kotlin類上也打了一個@Metadata注解,這些注解有個共同特點,都是RUNTIME的,比如
package com.google.gson.annotations;
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.METHOD})
public @interface SerializedName {
String value();
String[] alternate() default {};
}
懷疑所有打了這種運行時注解的類都會打到主dex中,于是下載了 tinker-patch-sample工程,通過gradle命令生成了2000個類,每個中30個方法,所有類上都打上 @Anno這個注解,@Anno這個注解是我們自定義的,定義如下
@Retention(RUNTIME)
@Target({TYPE})
public @interface Anno {
}
這時開起tinker打release包時問題復(fù)現(xiàn)了!一直打包失敗,提示 Too many classes in --main-dex-list, main dex capacity exceeded ,當(dāng)我們把Anno上的RUNTIME改成CLASS時,即
@Retention(CLASS)
@Target({TYPE})
public @interface Anno {
}
這時就可以打包成功!所以問題最終出在了tinker上,tinker會把所有打了RUNTIME注解的類打到主dex中,tinker官方一直沒有解決方案。沒辦法,我們只能自己解決
分包
當(dāng)然你可以把最低兼容版本改成api21,這時打包就沒問了,不過如果不想丟棄5.0以下用戶的話只能分包解決了。
gradle2.x可以使用dex-knife分包,不過我們是3.1.4,dex-knife分包可能存在兼容問題,gradle3.1.4打包時會生成一個maindexlist文件,這個文件決定了主dex中的類,我們只需要在生成這個文件后把不需要打包到主dex的類移除掉就可以了。maindexlist文件會在執(zhí)行transformClassesWithMultidexlistForXXX task時生成,我們只需要在這個task執(zhí)行后把maindexlist的文件內(nèi)容換掉即可。
我們在app module下創(chuàng)建了一個main_dex_split_for_tinker.gradle文件,里面代碼如下
project.afterEvaluate {
//解決開啟tinker時打包失敗問題 Too many classes in --main-dex-list, main dex capacity exceeded。 exclude_class.txt中配置排除的類
//一定要驗證5.0以下android啟動時是否崩潰!
if (android.defaultConfig.minSdkVersion.getApiLevel() >= 21) {
return
}
if (project.hasProperty("tinkerPatch") == false) {
return
}
def configuration = project.tinkerPatch
if (!configuration.tinkerEnable) {
return
}
android.applicationVariants.all { variant ->
def variantName = variant.name.capitalize()
def multidexTask = project.tasks.findByName("transformClassesWithMultidexlistFor${variantName}")
if (multidexTask != null) {
def splitTask = createSplitDexTask(variant);
multidexTask.finalizedBy splitTask
}
}
}
def createSplitDexTask(variant) {
def variantName = variant.name.capitalize()
return task("replace${variantName}MainDexClassList").doLast {
//從主dex移除的列表
def excludeClassList = []
File excludeClassFile = new File("${project.projectDir}/exclude_class.txt")
if (excludeClassFile.exists()) {
excludeClassFile.eachLine { line ->
if (!line.trim().isEmpty() && line.startsWith("#") == false) {
excludeClassList.add(line.trim())
}
}
excludeClassList.unique()
}
def mainDexList = []
File mainDexFile = new File("${project.buildDir}/intermediates/multi-dex/${variant.dirName}/maindexlist.txt")
println "${project.buildDir}/intermediates/multi-dex/${variant.dirName}/maindexlist.txt exist: ${mainDexFile.exists()}"
if (mainDexFile.exists()) {
mainDexFile.eachLine { line ->
if (!line.isEmpty()) {
mainDexList.add(line.trim())
}
}
mainDexList.unique()
if (!excludeClassList.isEmpty()) {
def newMainDexList = mainDexList.findResults { mainDexItem ->
def isKeepMainDexItem = true
for (excludeClassItem in excludeClassList) {
if (mainDexItem.contains(excludeClassItem)) {
isKeepMainDexItem = false
break
}
}
if (isKeepMainDexItem) mainDexItem else null
}
if (newMainDexList.size() < mainDexList.size()) {
mainDexFile.delete()
mainDexFile.createNewFile()
mainDexFile.withWriterAppend { writer ->
newMainDexList.each {
writer << it << '\n'
writer.flush()
}
}
}
}
}
}
}
然后再app module下的build.gradle文件中引入上面的文件
apply from: "main_dex_split_for_tinker.gradle"
然后我們在app module下新建exclude_class.txt文件,用于配置需要從maindexlist中移除的類,例如
#類路徑包含如下的,都會建議打包工具不要打到主dex中,但可能還會被打到主dex中。由于所有的業(yè)務(wù)代碼都會在multidex.install后執(zhí)行,理論上所有的業(yè)務(wù)代碼都可以不在主dex中
com/facebook/fresco
com/facebook/drawee
com/facebook/imageformat
com/facebook/imagepipeline
com/alibaba
com/taobao
com/eclipsesource/v8
最終我們打包成功了,maindexlist中只剩了3000多個類,當(dāng)然還可以更少,這樣很長一段時間內(nèi)我們不用擔(dān)心打包時主dex超限了。測試了下兼容性良好,5.0以下也沒有啟動崩潰。
最近幾天研究了下build gradle源碼,發(fā)現(xiàn)根本不需要上面的分包代碼,只要配置下就可以了 。默認(rèn)會把所有打了運行時注解的類全部打到主dex中,可以通過如下配置禁止掉
android{
dexOptions {
keepRuntimeAnnotatedClasses false
}
}
相關(guān)源碼如下:
public class MainDexListBuilder {
private static final String CLASS_EXTENSION = ".class";
private static final int STATUS_ERROR = 1;
private static final String EOL = System.getProperty("line.separator");
private static final String USAGE_MESSAGE =
"Usage:" + EOL + EOL +
"Short version: Don't use this." + EOL + EOL +
"Slightly longer version: This tool is used by mainDexClasses script to build" + EOL +
"the main dex list." + EOL;
/**
By default we force all classes annotated with runtime annotation to be kept in the
main dex list. This option disable the workaround, limiting the index pressure in the main
dex but exposing to the Dalvik resolution bug. The resolution bug occurs when accessing
annotations of a class that is not in the main dex and one of the annotations as an enum
parameter.
* @see <a >bug discussion</a>
*
*/
private static final String DISABLE_ANNOTATION_RESOLUTION_WORKAROUND =
"--disable-annotation-resolution-workaround";
private Set<String> filesToKeep = new HashSet<String>();
public static void main(String[] args) {
int argIndex = 0;
boolean keepAnnotated = true;
while (argIndex < args.length -2) {
if (args[argIndex].equals(DISABLE_ANNOTATION_RESOLUTION_WORKAROUND)) {
keepAnnotated = false;
} else {
System.err.println("Invalid option " + args[argIndex]);
printUsage();
System.exit(STATUS_ERROR);
}
argIndex++;
}
if (args.length - argIndex != 2) {
printUsage();
System.exit(STATUS_ERROR);
}
try {
MainDexListBuilder builder = new MainDexListBuilder(keepAnnotated, args[argIndex],
args[argIndex + 1]);
Set<String> toKeep = builder.getMainDexList();
printList(toKeep);
} catch (IOException e) {
System.err.println("A fatal error occured: " + e.getMessage());
System.exit(STATUS_ERROR);
return;
}
}
public MainDexListBuilder(boolean keepAnnotated, String rootJar, String pathString)
throws IOException {
ZipFile jarOfRoots = null;
Path path = null;
try {
try {
jarOfRoots = new ZipFile(rootJar);
} catch (IOException e) {
throw new IOException("\"" + rootJar + "\" can not be read as a zip archive. ("
+ e.getMessage() + ")", e);
}
path = new Path(pathString);
ClassReferenceListBuilder mainListBuilder = new ClassReferenceListBuilder(path);
mainListBuilder.addRoots(jarOfRoots);
for (String className : mainListBuilder.getClassNames()) {
filesToKeep.add(className + CLASS_EXTENSION);
}
if (keepAnnotated) {
keepAnnotated(path);
}
} finally {
try {
jarOfRoots.close();
} catch (IOException e) {
// ignore
}
if (path != null) {
for (ClassPathElement element : path.elements) {
try {
element.close();
} catch (IOException e) {
// keep going, lets do our best.
}
}
}
}
}
/**
* Returns a list of classes to keep. This can be passed to dx as a file with --main-dex-list.
*/
public Set<String> getMainDexList() {
return filesToKeep;
}
private static void printUsage() {
System.err.print(USAGE_MESSAGE);
}
private static void printList(Set<String> fileNames) {
for (String fileName : fileNames) {
System.out.println(fileName);
}
}
/**
* Keep classes annotated with runtime annotations.
*/
private void keepAnnotated(Path path) throws FileNotFoundException {
for (ClassPathElement element : path.getElements()) {
forClazz:
for (String name : element.list()) {
if (name.endsWith(CLASS_EXTENSION)) {
DirectClassFile clazz = path.getClass(name);
if (hasRuntimeVisibleAnnotation(clazz)) {
filesToKeep.add(name);
} else {
MethodList methods = clazz.getMethods();
for (int i = 0; i<methods.size(); i++) {
if (hasRuntimeVisibleAnnotation(methods.get(i))) {
filesToKeep.add(name);
continue forClazz;
}
}
FieldList fields = clazz.getFields();
for (int i = 0; i<fields.size(); i++) {
if (hasRuntimeVisibleAnnotation(fields.get(i))) {
filesToKeep.add(name);
continue forClazz;
}
}
}
}
}
}
}
private boolean hasRuntimeVisibleAnnotation(HasAttribute element) {
Attribute att = element.getAttributes().findFirst(
AttRuntimeVisibleAnnotations.ATTRIBUTE_NAME);
return (att != null && ((AttRuntimeVisibleAnnotations)att).getAnnotations().size()>0);
}
}
相關(guān)注釋
/**
* Keep all classes with runtime annotations in the main dex in legacy multidex.
*
* <p>This is enabled by default and works around an issue that will cause the app to crash
* when using java.lang.reflect.Field.getDeclaredAnnotations on older android versions.
*
* <p>This can be disabled for for apps that do not use reflection and need more space in their
* main dex.
*
* <p>See <a >http://b.android.com/78144</a>.
*/
@Override
public boolean getKeepRuntimeAnnotatedClasses() {
return keepRuntimeAnnotatedClasses;
}
public void setKeepRuntimeAnnotatedClasses(
boolean keepRuntimeAnnotatedClasses) {
this.keepRuntimeAnnotatedClasses = keepRuntimeAnnotatedClasses;
}