遇到問題
-
65K方法數(shù)超限
隨著應用不斷迭代,業(yè)務線的擴展,應用越來越大达箍,那么很不幸堪藐,總有一天细移,當你編譯的時候,會遇到一個類似下面的錯誤:
Conversion to Dalvik format failed: Unable to execute dex: method ID not in [0, 0xffff]: 65536
沒錯,這就是臭名昭著的65536方法數(shù)超限問題。具體原理可以參考由Android 65K方法數(shù)限制引發(fā)的思考這篇文章。當然充择,google也意識到這個問題,所以發(fā)布了MultiDex支持庫匪蟀。喜大普奔椎麦,趕緊使用,問題解決材彪?Too Young ! 使用過程中观挎,你會發(fā)現(xiàn)MultiDex有不少坑:啟動時間過長、ANR/Crash段化。當然也有解決方法嘁捷,可以參考美團自動拆包方案。但我只想說真的太....麻煩了显熏,還能不能愉快地回家玩游戲了....
-
上線太慢雄嚣,更新率太低
總所周知,Android APP發(fā)布流程較為漫長,一般需要經(jīng)歷
開發(fā)完成—上傳市場—審核—上線
幾個階段缓升,而且各個市場都各有各的政策和審核速度鼓鲁,每發(fā)一版都是一次煎熬呀。再者港谊,Android APP的升級率跟Android系統(tǒng)升級率一樣骇吭,怎一個慢字了得。新版本要覆蓋80%左右封锉,怎么也需要兩周左右绵跷。 -
一上線就如臨大敵
以為應用上線就完事了膘螟?NO 成福!相信大部分開發(fā)同學在應用上線的頭一周都是過得提心吊膽的,祈禱著不要出bug荆残,用戶不要反饋問題奴艾。但往往事與愿違,怎么辦内斯,趕緊出hotfix版本蕴潦?
解決方案
就不賣關子了,是的俘闯,我們的解決方案是構(gòu)建一套插件補丁的方案潭苞,期望可以無痛解決以上問題。插件化和補丁在目前看來是老生常談的東西了真朗,市面上已經(jīng)有一堆實現(xiàn)方案此疹,如DroidPlugin、Small遮婶、Android-Plugin-Framework蝗碎。掌閱研究插件化是從2014年中開始,研究補丁是從2016年初開始旗扑,相對來說蹦骑,算是比較晚。直至目前臀防,插件化方案已經(jīng)達到相對成熟的階段眠菇,而補丁方案也已經(jīng)上線。秉著開源的精神袱衷,我們的插件補丁方案最近已經(jīng)在Github開源— ZeusPlugin捎废。相對其他插件化和熱修復方案,ZeusPlugin最大特點是:簡單易懂祟昭,核心類只有6個缕坎,類總數(shù)只有13個,我們期望開發(fā)同學在使用這套方案的同時能理解所有的實現(xiàn)細節(jié)篡悟,在我們看來谜叹,這確實不是什么晦澀難懂的東西匾寝。
原理
要實現(xiàn)插件補丁,其實無非就是要解決幾個問題:插件安裝荷腊、資源加載和類加載艳悔。這幾點,我們可以參考Android系統(tǒng)加載APK的實現(xiàn)原理女仰。
Android系統(tǒng)加載APK
-
APK安裝過程
復制APK安裝包到data/app臨時目錄下猜年,如
vmdl648417937.tmp/base.apk
;解析應用程序的配置文件
AndroidManifest.xml
疾忍;進行Dexopt并生成ODEX,如
vmdl648417937.tmp/oat/arm/base.odex
乔外;將臨時目錄(vmdl648417937.tmp)重命名為
packageName + "-" + suffix
,如com.test_1
;-
在PackageManagerService中將上述步驟生成的apk信息通過mPackages成員變量緩存起來一罩;
mPackages是個ArrayMap杨幼,key為包名,value為PackageParser.Package(apk包信息)
在data/data目錄下創(chuàng)建對應的應用數(shù)據(jù)目錄聂渊。
-
啟動APK過程
- 點擊桌面App圖標差购,Launcher接收到點擊事件,獲取應用信息汉嗽,通過Binder IPC向SystemService進程(即system_process)發(fā)起startActivity請求(?ActivityManagerService(AMS)#startActivity)欲逃;
- SystemServer(AMS) 向zygote進程請求啟動一個新進程(ActivityManagerService#startProcessLocked);
- Zygote進程fork出新的子進程(APP進程)饼暑,在新進程中執(zhí)行 ActivityThread 類的 main 方法稳析;
- App進程創(chuàng)建ActivityThread實例,并通過Binder IPC向 SystemServer(AMS) 請求 attach 到 AMS;
- SystemServer(AMS) 進程在收到請求后撵孤,進行一系列準備工作后迈着,再通過binder IPC向App進程發(fā)送
bindApplication
和scheduleLaunchActivity
請求; - App進程(ActivityThread)在收到
bindApplication
請求后邪码,通過handler向主線程發(fā)送BIND_APPLICATION
消息裕菠; - 主線程在收到
BIND_APPLICATION
消息后,根據(jù)傳遞過來的ApplicationInfo創(chuàng)建一個對應的LoadApk對象(標志當前APK信息),然后創(chuàng)建ContextImpl對象(標志當前進程的環(huán)境),緊接著通過反射創(chuàng)建目標Application闭专,并調(diào)用其attach方法奴潘,將ContextImpl對象設置為目標Application的上下文環(huán)境,最后調(diào)用Application的onCreate函數(shù)影钉,做一些初始工作画髓; - App進程(ApplicationThread)在收到
scheduleLaunchActivity
請求后,通過handler向主線程發(fā)送LAUNCH_ACTIVITY
消息平委; - 主線程在收到
LAUNCH_ACTIVITY
消息后奈虾,通過反射機制創(chuàng)建目標Activity,并調(diào)用Activity的onCreate()方法。
以上分析都是基于Android 6.0的源碼肉微,其他版本可能有少許差異匾鸥,但不影響主流程,限于篇幅問題碉纳,在此不一一展開分析勿负,只重點分析相關的關鍵幾個步驟。
為什么提到Android系統(tǒng)加載APK的流程劳曹,因為分析完Android系統(tǒng)加載APK的流程奴愉,插件補丁方案也就基本能實現(xiàn)出來了,下面我展開說一下铁孵。
插件安裝
從APK安裝過程分析得知
- 配置文件
AndroidManifest.xml
是在應用安裝時就已經(jīng)解析并記錄锭硼,所以插件的AndroidManifest.xml配置無法生效 - 每個APK安裝都是獨享空間的,不同APK库菲、同一個APK的不同時間安裝都是完全獨立的账忘。這樣做志膀,個人覺得大大降低了系統(tǒng)的復雜度熙宇,而且清晰明了。在這點上溉浙, ZeusPlugin插件安裝策略幾乎就是仿照系統(tǒng)設計的烫止。具體可以參考 ZeusPlugin源碼,在此不展開描述戳稽。
類加載
從上述啟動APK過程分析7馆蠕、9可以得知,Application和Activity都是通過反射機制創(chuàng)建的惊奇,我們可以看看Application創(chuàng)建具體源碼實現(xiàn):
ActivityThread#handleBindApplication
private void handleBindApplication(AppBindData data) {
......
//省略代碼
.......
//生成APK信息LoadedApk互躬,即packageInfo
data.info = getPackageInfoNoCheck(data.appInfo, data.compatInfo);
//創(chuàng)建上下文環(huán)境
final ContextImpl appContext = ContextImpl.createAppContext(this, data.info);
......
//省略代碼
.......
try {
// If the app is being launched for full backup or restore, bring it up in
// a restricted environment with the base application class.
//通過反射機制創(chuàng)建Application實例
Application app = data.info.makeApplication(data.restrictedBackupMode, null);
mInitialApplication = app;
......
//省略代碼
.......
try {
//調(diào)用Application onCreate方法·
mInstrumentation.callApplicationOnCreate(app);
} catch (Exception e) {
if (!mInstrumentation.onException(app, e)) {
throw new RuntimeException(
"Unable to create application " + app.getClass().getName()
+ ": " + e.toString(), e);
}
}
} finally {
StrictMode.setThreadPolicy(savedPolicy);
}
}
我們再看看LoadedApk#makeApplication
的實現(xiàn)
public Application makeApplication(boolean forceDefaultAppClass,
Instrumentation instrumentation) {
if (mApplication != null) {
return mApplication;
}
Application app = null;
String appClass = mApplicationInfo.className;
if (forceDefaultAppClass || (appClass == null)) {
appClass = "android.app.Application";
}
try {
//獲取ClassLoader
java.lang.ClassLoader cl = getClassLoader();
if (!mPackageName.equals("android")) {
initializeJavaContextClassLoader();
}
ContextImpl appContext = ContextImpl.createAppContext(mActivityThread, this);
//使用獲取到的ClassLoader通過反射機制創(chuàng)建Application實例,其內(nèi)部實現(xiàn)是通過 ClassLoader.loadClass(className)得到Application Class
app = mActivityThread.mInstrumentation.newApplication(
cl, appClass, appContext);
appContext.setOuterContext(app);
} catch (Exception e) {
if (!mActivityThread.mInstrumentation.onException(app, e)) {
throw new RuntimeException(
"Unable to instantiate application " + appClass
+ ": " + e.toString(), e);
}
}
mActivityThread.mAllApplications.add(app);
mApplication = app;
......
//省略代碼
.......
return app;
}
從上述代碼可以得知颂郎,系統(tǒng)加載Application時候是先獲取一個特定ClassLoader吼渡,然后該ClassLoader通過反射機制創(chuàng)建Application實例。我們繼續(xù)看看getClassLoader()的實現(xiàn)
public ClassLoader getClassLoader() {
synchronized (this) {
if (mClassLoader != null) {
return mClassLoader;
}
if (mIncludeCode && !mPackageName.equals("android")) {
......
//省略代碼
.......
//創(chuàng)建ClassLoader
mClassLoader = ApplicationLoaders.getDefault().getClassLoader(zip, lib,
mBaseClassLoader);
StrictMode.setThreadPolicy(oldPolicy);
} else {
if (mBaseClassLoader == null) {
mClassLoader = ClassLoader.getSystemClassLoader();
} else {
mClassLoader = mBaseClassLoader;
}
}
return mClassLoader;
}
}
繼續(xù)跟蹤ApplicationLoaders類
public ClassLoader getClassLoader(String zip, String libPath, ClassLoader parent)
{
ClassLoader baseParent = ClassLoader.getSystemClassLoader().getParent();
synchronized (mLoaders) {
if (parent == null) {
parent = baseParent;
}
/*
* If we're one step up from the base class loader, find
* something in our cache. Otherwise, we create a whole
* new ClassLoader for the zip archive.
*/
if (parent == baseParent) {
ClassLoader loader = mLoaders.get(zip);
if (loader != null) {
return loader;
}
Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, zip);
PathClassLoader pathClassloader =
new PathClassLoader(zip, libPath, parent);
Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
mLoaders.put(zip, pathClassloader);
return pathClassloader;
}
Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, zip);
PathClassLoader pathClassloader = new PathClassLoader(zip, parent);
Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
return pathClassloader;
}
}
ApplicationLoaders是一個靜態(tài)緩存工具類乓序,其內(nèi)部維護了一個key為dexPath寺酪,value為PathClassLoader的ArrayMap,可以看到替劈,應用程序使用的ClassLoader都是同一個PathClassLoader類的實例
我們繼續(xù)扒一扒PathClassLoader的源碼寄雀,發(fā)現(xiàn)其實現(xiàn)都在父類BaseDexClassLoader中,我們直接找到其findClass
方法
protected Class<?> findClass(String name) throws ClassNotFoundException {
List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
Class c = pathList.findClass(name, suppressedExceptions);
if (c == null) {
ClassNotFoundException cnfe = new ClassNotFoundException("Didn't find class \"" + name + "\" on path: " + pathList);
for (Throwable t : suppressedExceptions) {
cnfe.addSuppressed(t);
}
throw cnfe;
}
return c;
}
可以看到陨献,查找Class的任務通其內(nèi)部一個DexPathList
類對象實現(xiàn)的盒犹,它的findClass
方法如下:
public Class findClass(String name, List<Throwable> suppressed) {
for (Element element : dexElements) {
DexFile dex = element.dexFile;
if (dex != null) {
Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
if (clazz != null) {
return clazz;
}
}
}
if (dexElementsSuppressedExceptions != null) {
suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
}
return null;
}
至此,真相大白,原來急膀,APK類加載是通過遍歷dexElements
這個數(shù)組來查找Class膜蛔,而dexElements就是APK dexPath里面的文件。
從上述分析可以得知要實現(xiàn)插件的類加載有兩種方式:
- 把插件的信息通過反射放進這個數(shù)組里面
- 替換系統(tǒng)的ClassLoader
考慮到類的隔離性以及框架拓展性脖阵,ZeusPlugin目前使用的方案是第二種皂股,根據(jù)類加載器的雙親委派模型,我們可以實現(xiàn)一套插件補丁類加載方案命黔,如下圖:
- 我們通過反射修改系統(tǒng)的ClassLoader為ZeusClassLoader呜呐,其內(nèi)包含多個ZeusPluginClassLoader
- 每一個插件對應一個ZeusPluginClassLoader,當移除插件時則刪除一個ZeusPluginClassLoader悍募,加載一個插件則添加一個ZeusPluginClassLoader蘑辑,
- ZeusClassLoader的parent為原始APK的ClassLoader(PathClassLoader),而原始APK的ClassLoader的parent(PathClassLoader)為ZeusHotfixClassLoader, ZeusHotfixClassLoader的parent為系統(tǒng)的ClassLoader(BootClassLoader)坠宴。
資源加載
關于資源加載洋魂,我們回到handleBindApplication
方法
private void handleBindApplication(AppBindData data) {
......
//省略代碼
.......
//生成APK信息LoadedApk,即packageInfo
data.info = getPackageInfoNoCheck(data.appInfo, data.compatInfo);
//創(chuàng)建上下文環(huán)境
final ContextImpl appContext = ContextImpl.createAppContext(this, data.info);
......
//省略代碼
.......
}
這里創(chuàng)建了上下文環(huán)境喜鼓,即ContextImpl副砍,再看看createAppContext方法真正實現(xiàn):
private ContextImpl(ContextImpl container, ActivityThread mainThread,
LoadedApk packageInfo, IBinder activityToken, UserHandle user, boolean restricted,
Display display, Configuration overrideConfiguration, int createDisplayWithId) {
......
//省略代碼
.......
//真正創(chuàng)建Resources的地方
Resources resources = packageInfo.getResources(mainThread);
if (resources != null) {
if (displayId != Display.DEFAULT_DISPLAY
|| overrideConfiguration != null
|| (compatInfo != null && compatInfo.applicationScale
!= resources.getCompatibilityInfo().applicationScale)) {
resources = mResourcesManager.getTopLevelResources(packageInfo.getResDir(),
packageInfo.getSplitResDirs(), packageInfo.getOverlayDirs(),
packageInfo.getApplicationInfo().sharedLibraryFiles, displayId,
overrideConfiguration, compatInfo);
}
}
mResources = resources;
......
//省略代碼
.......
}
Resources resources = packageInfo.getResources(mainThread);
這段代碼就是真正創(chuàng)建Resources的地方,我們繼續(xù)跟進去會發(fā)現(xiàn)它最終調(diào)用的是ResourcesManager的getTopLevelResources方法
Resources getTopLevelResources(String resDir, String[] splitResDirs,
String[] overlayDirs, String[] libDirs, int displayId,
Configuration overrideConfiguration, CompatibilityInfo compatInfo) {
final float scale = compatInfo.applicationScale;
Configuration overrideConfigCopy = (overrideConfiguration != null)
? new Configuration(overrideConfiguration) : null;
ResourcesKey key = new ResourcesKey(resDir, displayId, overrideConfigCopy, scale);
Resources r;
synchronized (this) {
// Resources is app scale dependent.
if (DEBUG) Slog.w(TAG, "getTopLevelResources: " + resDir + " / " + scale);
//判斷是否已經(jīng)存在Resources
WeakReference<Resources> wr = mActiveResources.get(key);
r = wr != null ? wr.get() : null;
//if (r != null) Log.i(TAG, "isUpToDate " + resDir + ": " + r.getAssets().isUpToDate());
if (r != null && r.getAssets().isUpToDate()) {
if (DEBUG) Slog.w(TAG, "Returning cached resources " + r + " " + resDir
+ ": appScale=" + r.getCompatibilityInfo().applicationScale
+ " key=" + key + " overrideConfig=" + overrideConfiguration);
return r;
}
}
//if (r != null) {
// Log.w(TAG, "Throwing away out-of-date resources!!!! "
// + r + " " + resDir);
//}
//創(chuàng)建資源管理器
AssetManager assets = new AssetManager();
// resDir can be null if the 'android' package is creating a new Resources object.
// This is fine, since each AssetManager automatically loads the 'android' package
// already.
if (resDir != null) {
//添加APK資源路徑
if (assets.addAssetPath(resDir) == 0) {
return null;
}
}
......
//省略代碼
.......
//創(chuàng)建Resources
r = new Resources(assets, dm, config, compatInfo);
if (DEBUG) Slog.i(TAG, "Created app resources " + resDir + " " + r + ": "
+ r.getConfiguration() + " appScale=" + r.getCompatibilityInfo().applicationScale);
synchronized (this) {
WeakReference<Resources> wr = mActiveResources.get(key);
Resources existing = wr != null ? wr.get() : null;
if (existing != null && existing.getAssets().isUpToDate()) {
// Someone else already created the resources while we were
// unlocked; go ahead and use theirs.
r.getAssets().close();
return existing;
}
// XXX need to remove entries when weak references go away
mActiveResources.put(key, new WeakReference<>(r));
if (DEBUG) Slog.v(TAG, "mActiveResources.size()=" + mActiveResources.size());
return r;
}
}
至此庄岖,Resources就創(chuàng)建好了豁翎,這里有一個關鍵的類AssetManager,它是應用程序的資源管理器隅忿,在它的構(gòu)造函數(shù)里會把framework/framework-res.apk
也會添加到資源路徑中心剥,這是C++調(diào)用,有興趣的話背桐,可以參考一下老羅這篇文章优烧。同時這也解釋了為什么我們開發(fā)的應用可以訪問到系統(tǒng)的資源。
通過上述分析链峭,我們可以得知畦娄,要實現(xiàn)插件資源加載,只需創(chuàng)建一個AssetManager
,然后把把宿主資源路徑和插件apk路徑添加進去熏版,創(chuàng)建我們自己的Resources纷责,然后通過反射把PackageInfo的mResources
替換成我們的Resources即可,具體代碼如下:
AssetManager assetManager = AssetManager.class.newInstance();
Method addAssetPath = AssetManager.class.getMethod("addAssetPath", String.class);
addAssetPath.invoke(assetManager, mBaseContext.getPackageResourcePath());
if (mLoadedPluginList != null && mLoadedPluginList.size() != 0) {
//每個插件的packageID都不能一樣
for (String id : mLoadedPluginList.keySet()) {
addAssetPath.invoke(assetManager, PluginUtil.getAPKPath(id));
}
}
//這里提前創(chuàng)建一個resource是因為Resources的構(gòu)造函數(shù)會對AssetManager進行一些變量的初始化
//還不能創(chuàng)建系統(tǒng)的Resources類撼短,否則中興系統(tǒng)會出現(xiàn)崩潰問題
PluginResources newResources = new PluginResources(assetManager,
mBaseContext.getResources().getDisplayMetrics(),
mBaseContext.getResources().getConfiguration());
PluginUtil.setField(mBaseContext, "mResources", newResources);
//這是最主要的需要替換的再膳,如果不支持插件運行時更新,只留這一個就可以了
PluginUtil.setField(mPackageInfo, "mResources", newResources);
現(xiàn)在曲横,參考以上思路喂柒,我們已經(jīng)基本可以實現(xiàn)一個插件補丁框架不瓶,其實站在巨人的肩膀(Android 系統(tǒng)源碼)上,是不是覺得實現(xiàn)一套插件補丁框架也沒那么復雜呢灾杰?當然蚊丐,真正項目中,還有很多細節(jié)需要處理艳吠,譬如說資源分區(qū)麦备、代碼混淆等問題。但核心邏輯基本還是以上這些思路昭娩。具體實現(xiàn)可以參考 ZeusPlugin源碼
TODO
由于公司業(yè)務線凛篙、時間精力等原因, ZeusPlugin有一些特性和功能還沒實現(xiàn)栏渺,但很多也提上日程了呛梆,比如:
demo完善
gradle插件maven遠程依賴
-
支持補丁更換資源
……..