源码: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 运行所需要的环境就搭建好了,
是不是感觉没讲完?
的确是没讲完,
不过大体流程已介绍,
剩下的结合源码领悟吧