源碼:https://github.com/Yichou/apkrunner
想解決任何問題之前都得追溯根源
那麼我們來看看 apk 是如何啓動的,
首先你得安裝這個apk,完了在Launcher點擊圖標,然後apk就啓動了,
辣麼:點擊apk圖標系統做了工作?
我們從logcat觀察, 新建一個過濾器以 ActivityManager 或者 system_process 爲 Tag,清空logcat,點擊apk圖標,大概可以看到下面的日誌:
從日誌可以看出有3個步驟,
1、啓動Activity,
Intent 內容:action=android.intent.action.MAIN category=android.intent.category.LAUNCHER,
很熟悉吧,每個Android程序主 Activity 都需要配置這2個屬性,系統也是通過這2個屬性來查找入口activity,如果你沒配置category=android.intent.category.LAUNCHER 系統就不會在桌面創建圖標
2、啓動進程,
啓動進程以運行activity,每一個app都有獨立的進程(當然不同app也可以共用一個進程,這是後話)
3、渲染UI,
就是解析 layout 生成界面並展示,然後你就看到了畫面
瞭解了步驟,那麼我們就需要模仿系統幹這些事情,
一、創建進程,
這個很簡單,我們只需要給 activity或者service配置 android:process=":app0" 參數,系統就會創建新的進程來啓動這個 activity,不然就是在app主進程啓動。android:taskAffinity=".QApp0" 參數是配置獨立任務,就是在系統任務切換界面會多出一個窗口
假設我們調用了 startActivity(this, com.apkrunner.ProxyActivity0)
二、運行Activity,
這個我們需要從apk的 AndroidManifest.xml 解析出 LAUNCHER Activity(詳見代碼),假設我們解析到:demo.LauncherActivity (後面會用)
解析到了直接調用 startAcvity(new Intent(this, demo.LauncherActivity.class)) 嗎?
NO,你一定遇到過,如果你忘記在 AndroidManifest.xml 添加Activity,啓動會崩潰
想要解決這個問題,還是隻有一個辦法,追本溯源,瞭解系統如何啓動 Activity
startActivity 最終實現調用的是(用 Eclipse 跟着源碼一步步走下去就能找到,在 android.app.Instrumentation 1419 行)
int result = ActivityManagerNative.getDefault()
.startActivity(whoThread, who.getBasePackageName(), intent,
intent.resolveTypeIfNeeded(who.getContentResolver()),
token, target != null ? target.mEmbeddedID : null,
requestCode, 0, null, null, options);
ActivityManagerNative 通過IPC 調到 system_process ActivityManagerService 服務,(此處省略1w行)系統處理完後回調app進程,最終到 ActivityThread 內部 Handler :
android.app.ActivityThread line 1190
case LAUNCH_ACTIVITY: {
Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "activityStart");
ActivityClientRecord r = (ActivityClientRecord)msg.obj;
r.packageInfo = getPackageInfoNoCheck(
r.activityInfo.applicationInfo, r.compatInfo);
handleLaunchActivity(r, null);
Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
} break;
(友情提示:觀看此文時請打開源碼,跟隨腳步)我們現在到了 handleLaunchActivity
現在又到了 performLaunchActivity(ActivityClientRecord r, Intent customIntent)
關鍵步驟來了,
Activity activity = null;
try {
java.lang.ClassLoader cl = r.packageInfo.getClassLoader();
activity = mInstrumentation.newActivity(
cl, component.getClassName(), r.intent);
android.app.Instrumentation newActivity line 1057
public Activity newActivity(ClassLoader cl, String className,
Intent intent)
throws InstantiationException, IllegalAccessException,
ClassNotFoundException {
return (Activity)cl.loadClass(className).newInstance();
}
這個代碼很簡單吧,就是 loadClass newInstance,反射法加載 Activity 的class,在創建一個對象,這些都是在 app 進程做的事情,既然是在 app 進程,那麼我們就完全可以控制裏面的調用參數,將 className 替換成 demo.LauncherActivity
知道了原理,那麼如何實現呢?
首先,我們啓動 Activity 需要傳一個 Intent 對象,AMS用來查找Activity 組件,performLaunchActivity 方法的 r 參數裏面有一個 intent 對象,這個intent 就是startActivity()方法傳遞的(這一點非常關鍵),
1.使用動態代理攔截 ActivityManagerNative 的 startActivity 方法:
判斷 Intent 參數,如果要啓動的 Activity 是未安裝的apk的,那麼把他換成宿主已聲明的,
private Intent makeProxy(Intent oIntent, String proxyClass) {
Intent intent = new Intent();
intent.setClassName(ApkRunner.getShellPkg(), proxyClass);
intent.putExtra(FLAG_PROXY, true);
intent.putExtra(KEY_INTENT, oIntent);
/**
* 加標誌過去會導致一些莫名的問題,我們就默認給他啓動一個好了 2014-4-3
*/
// intent.addFlags(oIntent.getFlags());
return intent;
}
把原始的 Intent 作爲一個參數存儲到 Intent ,
2.攔截 ActivityThread H 的 handleMessage(Message msg)方法:
用反射替換 Handler 的 callback 對象。
H 原本的 callback 對象是 null ,所以你的 callback -> boolean handleMessage(Message msg) 要返回 false,讓系統調用原始版本。
在 LAUNCH_ACTIVITY 消息替換 (ActivityClientRecord r) r.activityInfo 和 r.intent
activityInfo 對象的作用,看這行 line 2808
r.packageInfo = getPackageInfo(aInfo.applicationInfo
這行代碼創建了一個 LoadedApk 對象,這個對象非常關鍵,一個 apk 加載之後所有信息都保存在此對象(比如:DexClassLoader、Resources、Application),一個包對應一個對象,以包名區別,而 ActivityThread 裏設計可以緩存N個LoadedApk,以包名爲key存儲在一個Map裏。看 getPackageInfo 方法的部分代碼:
packageInfo =
new LoadedApk(this, aInfo, compatInfo, baseLoader,
securityViolation, includeCode &&
(aInfo.flags&ApplicationInfo.FLAG_HAS_CODE) != 0);
if (includeCode) {
mPackages.put(aInfo.packageName,
new WeakReference<LoadedApk>(packageInfo));
} else {
mResourcePackages.put(aInfo.packageName,
new WeakReference<LoadedApk>(packageInfo));
}
所以,我們需要替換 r.activityInfo ,activityInfo 使用 PackageManager getPackageArchiveInfo 創建
到了這裏你可能發現了,一個apk運行時有3大關鍵要素
Context Resource ClassLoader
分別是,上下文環境,資源管理器,類管理器
上下文環境通用的,
資源管理器我們需要用未安裝 apk 去創建
類管理器可以用 DexClassLoader,下面我們來一一分解
1、ClassLoader
系統做法是 android.app.LoadedApk line 318
ClassLoader baseParent = ClassLoader.getSystemClassLoader().getParent();
String tmpPath = mApkPath;
FileUtils.createDir(appLibPath);
mClassLoader = new DexClassLoader(tmpPath,
appLibPath,
appSoPath,
baseParent);
用 DexClassloader,所以我們要在 LoadedApk 創建後用反射替換掉 mClassLoader 對象
2、Resources
android.app.LoadedApk line 318
public Resources getResources(ActivityThread mainThread) {
if (mResources == null) {
mResources = mainThread.getTopLevelResources(mResDir,
Display.DEFAULT_DISPLAY, null, this);
}
return mResources;
}
AssetManager assets = new AssetManager();
if (assets.addAssetPath(resDir) == 0) {
return null;
}
...
r = new Resources(assets, dm, config, compatInfo, token);
所以你知道如何創建一個 apk 的 Resources 了,對的,關鍵點就是 assets.addAssetPath(resDir)
創建好 Resources 後,替換 LoadedApk 的 mResources 對象
到這裏準備工作似乎已經妥當,
那麼 app啓動創建的第一個組件就是 Application
android.app.LoadedApk line 486
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 {
java.lang.ClassLoader cl = getClassLoader();
ContextImpl appContext = ContextImpl.createAppContext(mActivityThread, this);
app = mActivityThread.mInstrumentation.newApplication(
cl, appClass, appContext);
appContext.setOuterContext(app);
} catch (Exception e) {
}
mActivityThread.mAllApplications.add(app);
mApplication = app;
Application對象搞定後,我們模擬調用一下 onCreate() 方法,OK 大功告成,一個 apk 運行所需要的環境就搭建好了,
是不是感覺沒講完?
的確是沒講完,
不過大體流程已介紹,
剩下的結合源碼領悟吧