編輯:關於Android編程
65K方法數超限
隨著應用不斷迭代,業務線的擴展,應用越來越大,那麼很不幸,總有一天,當你編譯的時候,會遇到一個類似下面的錯誤:
Conversion to Dalvik format failed: Unable to execute dex: method ID not in [0, 0xffff]: 65536
沒錯,這就是臭名昭著的65536方法數超限問題。當然,google也意識到這個問題,所以發布了MultiDex支持庫。喜大普奔,趕緊使用,問題解決?Too Young ! 使用過程中,你會發現MultiDex有不少坑:啟動時間過長、ANR/Crash。當然也有解決方法。但我只想說真的太….麻煩了,還能不能愉快地回家玩游戲了….
上線太慢,更新率太低
總所周知,Android APP發布流程較為漫長,一般需要經歷開發完成—上傳市場—審核—上線幾個階段,而且各個市場都各有各的政策和審核速度,每發一版都是一次煎熬呀。再者,Android APP的升級率跟Android系統升級率一樣,怎一個慢字了得。新版本要覆蓋80%左右,怎麼也需要兩周左右。
一上線就如臨大敵
以為應用上線就完事了?NO !相信大部分開發同學在應用上線的頭一周都是過得提心吊膽的,祈禱著不要出bug,用戶不要反饋問題。但往往事與願違,怎麼辦,趕緊出hotfix版本?
解決方案
就不賣關子了,是的,我們的解決方案是構建一套插件補丁的方案,期望可以無痛解決以上問題。插件化和補丁在目前看來是老生常談的東西了,市面上已經有一堆實現方案,如DroidPlugin、Small、Android-Plugin-Framework。掌閱研究插件化是從2014年中開始,研究補丁是從2016年初開始,相對來說,算是比較晚。直至目前,插件化方案已經達到相對成熟的階段,而補丁方案也已經上線。秉著開源的精神,我們的插件補丁方案最近已經在Github開源— ZeusPlugin。相對其他插件化和熱修復方案,ZeusPlugin最大特點是:簡單易懂,核心類只有6個,類總數只有13個,我們期望開發同學在使用這套方案的同時能理解所有的實現細節,在我們看來,這確實不是什麼晦澀難懂的東西。
原理
要實現插件補丁,其實無非就是要解決幾個問題:插件安裝、資源加載和類加載。這幾點,我們可以參考Android系統加載APK的實現原理。
Android系統加載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目錄下創建對應的應用數據目錄。
啟動APK過程
點擊桌面App圖標,Launcher接收到點擊事件,獲取應用信息,通過Binder IPC向SystemService進程(即system_process)發起startActivity請求(ActivityManagerService(AMS)#startActivity); SystemServer(AMS) 向zygote進程請求啟動一個新進程(ActivityManagerService#startProcessLocked); Zygote進程fork出新的子進程(APP進程),在新進程中執行 ActivityThread 類的 main 方法; App進程創建ActivityThread實例,並通過Binder IPC向 SystemServer(AMS) 請求 attach 到 AMS; SystemServer(AMS) 進程在收到請求後,進行一系列准備工作後,再通過binder IPC向App進程發送bindApplication和scheduleLaunchActivity請求; App進程(ActivityThread)在收到bindApplication請求後,通過handler向主線程發送BIND_APPLICATION消息; 主線程在收到BIND_APPLICATION消息後,根據傳遞過來的ApplicationInfo創建一個對應的LoadApk對象(標志當前APK信息),然後創建ContextImpl對象(標志當前進程的環境),緊接著通過反射創建目標Application,並調用其attach方法,將ContextImpl對象設置為目標Application的上下文環境,最後調用Application的onCreate函數,做一些初始工作; App進程(ApplicationThread)在收到scheduleLaunchActivity請求後,通過handler向主線程發送LAUNCH_ACTIVITY消息; 主線程在收到LAUNCH_ACTIVITY消息後,通過反射機制創建目標Activity,並調用Activity的onCreate()方法。
以上分析都是基於Android 6.0的源碼,其他版本可能有少許差異,但不影響主流程,限於篇幅問題,在此不一一展開分析,只重點分析相關的關鍵幾個步驟。
為什麼提到Android系統加載APK的流程,因為分析完Android系統加載APK的流程,插件補丁方案也就基本能實現出來了,下面我展開說一下。
插件安裝
從APK安裝過程分析得知
配置文件AndroidManifest.xml是在應用安裝時就已經解析並記錄,所以插件的AndroidManifest.xml配置無法生效
每個APK安裝都是獨享空間的,不同APK、同一個APK的不同時間安裝都是完全獨立的。這樣做,個人覺得大大降低了系統的復雜度,而且清晰明了。在這點上, ZeusPlugin插件安裝策略幾乎就是仿照系統設計的。具體可以參考 ZeusPlugin源碼,在此不展開描述。
類加載
從上述啟動APK過程分析7、9可以得知,Application和Activity都是通過反射機制創建的,我們可以看看Application創建具體源碼實現:
ActivityThread#handleBindApplication
private void handleBindApplication(AppBindData data) {
......
//省略代碼
.......
//生成APK信息LoadedApk,即packageInfo
data.info = getPackageInfoNoCheck(data.appInfo, data.compatInfo);
//創建上下文環境
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.
//通過反射機制創建Application實例
Application app = data.info.makeApplication(data.restrictedBackupMode, null);
mInitialApplication = app;
......
//省略代碼
.......
try {
//調用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的實現
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通過反射機制創建Application實例,其內部實現是通過 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;
}
從上述代碼可以得知,系統加載Application時候是先獲取一個特定ClassLoader,然後該ClassLoader通過反射機制創建Application實例。我們繼續看看getClassLoader()的實現
public ClassLoader getClassLoader() {
synchronized (this) {
if (mClassLoader != null) {
return mClassLoader;
}
if (mIncludeCode && !mPackageName.equals("android")) {
......
//省略代碼
.......
//創建ClassLoader
mClassLoader = ApplicationLoaders.getDefault().getClassLoader(zip, lib,
mBaseClassLoader);
StrictMode.setThreadPolicy(oldPolicy);
} else {
if (mBaseClassLoader == null) {
mClassLoader = ClassLoader.getSystemClassLoader();
} else {
mClassLoader = mBaseClassLoader;
}
}
return mClassLoader;
}
}
繼續跟蹤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是一個靜態緩存工具類,其內部維護了一個key為dexPath,value為PathClassLoader的ArrayMap,可以看到,應用程序使用的ClassLoader都是同一個PathClassLoader類的實例
我們繼續扒一扒PathClassLoader的源碼,發現其實現都在父類BaseDexClassLoader中,我們直接找到其
findClass方法
protected Class findClass(String name) throws ClassNotFoundException {
List suppressedExceptions = new ArrayList();
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的任務通其內部一個
DexPathList類對象實現的,它的
findClass方法如下:
public Class findClass(String name, List 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這個數組來查找Class,而dexElements就是APK dexPath裡面的文件。
從上述分析可以得知要實現插件的類加載有兩種方式:
把插件的信息通過反射放進這個數組裡面
替換系統的ClassLoader
考慮到類的隔離性以及框架拓展性,ZeusPlugin目前使用的方案是第二種,根據類加載器的雙親委派模型,我們可以實現一套插件補丁類加載方案,如下圖:
<喎?/kf/ware/vc/" target="_blank" class="keylink">vcD4NCjxibG9ja3F1b3RlPg0KCTxjb2RlPjxjb2RlPjxjb2RlPjxjb2RlPjxjb2RlPjxjb2RlPjxjb2RlPjxjb2RlPjxjb2RlPjxjb2RlPjxjb2RlPjxjb2RlPjxjb2RlPjxjb2RlPjxjb2RlPjxjb2RlPjxjb2RlPjxjb2RlPjxjb2RlPjxjb2RlPs7Sw8fNqLn9t7TJ5NDeuMTPtc2ztcRDbGFzc0xvYWRlcs6qWmV1c0NsYXNzTG9hZGVyo6zG5MTasPy6rLbguPZaZXVzUGx1Z2luQ2xhc3NMb2FkZXIgw7/Su7j2suW8/rbU06bSu7j2WmV1c1BsdWdpbkNsYXNzTG9hZGVyo6y1sdLGs/2y5bz+yrHU8sm+s/3Su7j2WmV1c1BsdWdpbkNsYXNzTG9hZGVyo6y809TY0ru49rLlvP7U8sztvNPSu7j2WmV1c1BsdWdpbkNsYXNzTG9hZGVyo6wgWmV1c0NsYXNzTG9hZGVytcRwYXJlbnTOqtStyrxBUEu1xENsYXNzTG9hZGVyKFBhdGhDbGFzc0xvYWRlcimjrLb41K3KvEFQS7XEQ2xhc3NMb2FkZXK1xHBhcmVudChQYXRoQ2xhc3NMb2FkZXIpzqpaZXVzSG90Zml4Q2xhc3NMb2FkZXIsIFpldXNIb3RmaXhDbGFzc0xvYWRlcrXEcGFyZW50zqrPtc2ztcRDbGFzc0xvYWRlcihCb290Q2xhc3NMb2FkZXIpoaMgPC9jb2RlPjwvY29kZT48L2NvZGU+PC9jb2RlPjwvY29kZT48L2NvZGU+PC9jb2RlPjwvY29kZT48L2NvZGU+PC9jb2RlPjwvY29kZT48L2NvZGU+PC9jb2RlPjwvY29kZT48L2NvZGU+PC9jb2RlPjwvY29kZT48L2NvZGU+PC9jb2RlPjwvY29kZT48L2Jsb2NrcXVvdGU+DQo8aDQgaWQ9"資源加載">資源加載
關於資源加載,我們回到
handleBindApplication方法
private void handleBindApplication(AppBindData data) {
......
//省略代碼
.......
//生成APK信息LoadedApk,即packageInfo
data.info = getPackageInfoNoCheck(data.appInfo, data.compatInfo);
//創建上下文環境
final ContextImpl appContext = ContextImpl.createAppContext(this, data.info);
......
//省略代碼
.......
}
這裡創建了上下文環境,即ContextImpl,再看看createAppContext方法真正實現:
private ContextImpl(ContextImpl container, ActivityThread mainThread,
LoadedApk packageInfo, IBinder activityToken, UserHandle user, boolean restricted,
Display display, Configuration overrideConfiguration, int createDisplayWithId) {
......
//省略代碼
.......
//真正創建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);這段代碼就是真正創建Resources的地方,我們繼續跟進去會發現它最終調用的是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);
//判斷是否已經存在Resources
WeakReference 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);
//}
//創建資源管理器
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;
}
}
......
//省略代碼
.......
//創建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 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就創建好了,這裡有一個關鍵的類AssetManager,它是應用程序的資源管理器,在它的構造函數裡會把
framework/framework-res.apk也會添加到資源路徑中,這是C++調用,有興趣的話,可以參考一下老羅這篇文章。同時這也解釋了為什麼我們開發的應用可以訪問到系統的資源。
通過上述分析,我們可以得知,要實現插件資源加載,只需創建一個
AssetManager,然後把把宿主資源路徑和插件apk路徑添加進去,創建我們自己的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));
}
}
//這裡提前創建一個resource是因為Resources的構造函數會對AssetManager進行一些變量的初始化
//還不能創建系統的Resources類,否則中興系統會出現崩潰問題
PluginResources newResources = new PluginResources(assetManager,
mBaseContext.getResources().getDisplayMetrics(),
mBaseContext.getResources().getConfiguration());
PluginUtil.setField(mBaseContext, "mResources", newResources);
//這是最主要的需要替換的,如果不支持插件運行時更新,只留這一個就可以了
PluginUtil.setField(mPackageInfo, "mResources", newResources);
現在,參考以上思路,我們已經基本可以實現一個插件補丁框架,其實站在巨人的肩膀(Android 系統源碼)上,是不是覺得實現一套插件補丁框架也沒那麼復雜呢?當然,真正項目中,還有很多細節需要處理,譬如說資源分區、代碼混淆等問題。但核心邏輯基本還是以上這些思路。具體實現可以參考 ZeusPlugin源碼
TODO
由於公司業務線、時間精力等原因, ZeusPlugin有一些特性和功能還沒實現,但很多也提上日程了,比如:
demo完善
gradle插件maven遠程依賴
支持補丁更換資源
……..
GitHub
https://github.com/iReaderAndroid/ZeusPlugin
github原文鏈接
前言:最近做基於openfire聊天(仿QQ、微信)翻頁查看聊天記錄,為此做了根據時間倒序查看聊天記錄,先聲明這demo是根據id來倒序(原理和時間倒序一樣) 1,主界面
package cc.testasset; import java.io.InputStream; import android.os.Bundle; import
這周繼續我的Blog,前面幾篇博文簡單的介紹了閱讀Android FW的源碼所需要的基礎知識,主要和C++相關。從這篇博文開始將會和大家一起學
package neal.canvas;import android.content.Context;import android.graphics.Canv