IBigerBiger的成长之路

插件化实现(三)Activity插件化

前面一篇文章对于插件中资源的获取进行了讲解,我们可以利用这种方式来实现换肤等等功能,并且对于类加载器相关知识也有了一定的了解,这一篇则是对于插件中四大组件之一的Activity进行相关说明了。

启动一个Activity有两种方式

  • 直接调用Context类的startActivity方法;这种方式启动的Activity没有Activity栈,因此不能以standard方式启动,必须加上FLAG_ACTIVITY_NEW_TASK这个Flag。
  • 调用被Activity类重载过的startActivity方法,通常在我们的Activity中直接调用这个方法就是这种形式;

接下来看一下这两种方式的调用

1.Context类的startActivity方法

可以看到Context是一个抽象类,而这个startActivity也是一个抽象方法,如下

1
public abstract void startActivity(Intent intent, @Nullable Bundle options);

它的具体实现是在ContextImpl内,我们看下ContextImpl类的startActivity方法

1
2
3
4
5
6
7
8
9
10
11
12
13
@Override
public void startActivity(Intent intent, Bundle options) {
warnIfCallingFromSystemProcess();
if ((intent.getFlags()&Intent.FLAG_ACTIVITY_NEW_TASK) == 0) {
throw new AndroidRuntimeException(
"Calling startActivity() from outside of an Activity "
+ " context requires the FLAG_ACTIVITY_NEW_TASK flag."
+ " Is this really what you want?");
}
mMainThread.getInstrumentation().execStartActivity(
getOuterContext(), mMainThread.getApplicationThread(), null,
(Activity) null, intent, -1, options);
}

这里是调用了Instrumentation类的execStartActivity方法

2.Activity类的startActivity方法

调用如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public void startActivityForResult(Intent intent, int requestCode, @Nullable Bundle options) {
if (mParent == null) {
Instrumentation.ActivityResult ar =
mInstrumentation.execStartActivity(
this, mMainThread.getApplicationThread(), mToken, this,
intent, requestCode, options);
if (ar != null) {
mMainThread.sendActivityResult(
mToken, mEmbeddedID, requestCode, ar.getResultCode(),
ar.getResultData());
}
if (requestCode >= 0) {
mStartedActivity = true;
}
cancelInputsAndStartExitTransition(options);
// TODO Consider clearing/flushing other event sources and events for child windows.
} else {
if (options != null) {
mParent.startActivityFromChild(this, intent, requestCode, options);
} else {
// Note we want to go through this method for compatibility with
// existing applications that may have overridden it.
mParent.startActivityFromChild(this, intent, requestCode);
}
}
}

可以看到这里也是调用了Instrumentation类的execStartActivity方法

所以其实这两种启动方式后面都是调用的同样的方法,对于启动插件中的Activity,自然我们是需要了解整个Activity的启动流程的,通过流程分析去寻找可以实现启动插件中Activity的方法

Activity启动流程

上面说到Activity启动会走到Instrumentation类的execStartActivity方法,如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public ActivityResult execStartActivity(
Context who, IBinder contextThread, IBinder token, Activity target,
Intent intent, int requestCode, Bundle options) {
IApplicationThread whoThread = (IApplicationThread) contextThread;
Uri referrer = target != null ? target.onProvideReferrer() : null;
...
try {
intent.migrateExtraStreamToClipData();
intent.prepareToLeaveProcess();
int result = ActivityManagerNative.getDefault()
.startActivity(whoThread, who.getBasePackageName(), intent,
intent.resolveTypeIfNeeded(who.getContentResolver()),
token, target != null ? target.mEmbeddedID : null,
requestCode, 0, null, options);
checkStartActivityResult(result, intent);
} catch (RemoteException e) {
throw new RuntimeException("Failure from system", e);
}
return null;
}

可以看到这里的调用了ActivityManagerNative类的startActivity方法,而ActivityManagerNative实际上就是ActivityManagerService这个远程对象的Binder代理对象;每次需要与AMS打交道的时候,需要借助这个代理对象通过驱动进而完成IPC调用。

所以我们可以去ActivityManagerService中寻找startActivity方法,如下

1
2
3
4
5
6
7
8
@Override
public final int startActivity(IApplicationThread caller, String callingPackage,
Intent intent, String resolvedType, IBinder resultTo, String resultWho, int requestCode,
int startFlags, ProfilerInfo profilerInfo, Bundle options) {
return startActivityAsUser(caller, callingPackage, intent, resolvedType, resultTo,
resultWho, requestCode, startFlags, profilerInfo, options,
UserHandle.getCallingUserId());
}

这里调用了startActivityAsUser方法,而这里又掉用了ActivityStackSupervisor类的startActivityMayWait方法。

startActivityMayWait这个方法前面对参数进行了一系列处理,我们需要知道的是,在这个方法内部对传进来的Intent进行了解析,并尝试从中取出关于启动Activity的信息,然后这个方法调用了startActivityLocked方法;在startActivityLocked方法内部进行了一系列重要的检查:比如权限检查,Activity的exported属性检查等等;

启动没有在Manifestfest中显示声明的Activity抛异常也是这里发生的:

1
2
3
4
5
if (err == ActivityManager.START_SUCCESS && aInfo == null) {
// We couldn't find the specific class specified in the Intent.
// Also the end of the line.
err = ActivityManager.START_CLASS_NOT_FOUND;
}

抛出异常后,Instrumentation类的execStartActivity方法调用checkStartActivityResult方法来捕获这个异常,然后报出错误信息如下

1
2
3
4
5
6
7
8
case ActivityManager.START_CLASS_NOT_FOUND:
if (intent instanceof Intent && ((Intent)intent).getComponent() != null)
throw new ActivityNotFoundException(
"Unable to find explicit activity class "
+ ((Intent)intent).getComponent().toShortString()
+ "; have you declared this activity in your AndroidManifest.xml?");
throw new ActivityNotFoundException(
"No Activity found to handle " + intent);

正常流程走的话,就是ActivityStackSupervisor与ActivityStack之间的调用方法了,我们没有必要了解这些调用方法到底做了什么,流程如下


图1 ActivityStackSupervisor与ActivityStack之间调用

这里最后调用了ActivityStackSupervisor中realStrartActivityLocked方法,如下

1
2
3
4
5
6
7
8
9
10
11
final boolean realStartActivityLocked(ActivityRecord r,
ProcessRecord app, boolean andResume, boolean checkConfig)
throws RemoteException {
...
app.thread.scheduleLaunchActivity(new Intent(r.intent), r.appToken,
System.identityHashCode(r), r.info, new Configuration(mService.mConfiguration),
new Configuration(stack.mOverrideConfig), r.compat, r.launchedFromPackage,
task.voiceInteractor, app.repProcState, r.icicle, r.persistentState, results,
newIntents, !andResume, mService.isNextTransitionForward(), profilerInfo);
...
}

从现在开始则是开始了真正的Activity对象创建以及启动过程,这里调用了ApplicationThread的scheduleLaunchActivity方法,而ApplicationThread与前面的ApplicationThread类一样,它真正的实现是在ActivityThread里,那么接下来看一下ActivityThread的scheduleLaunchActivity方法,如下

1
2
3
4
5
6
7
8
9
10
11
12
13
public final void scheduleLaunchActivity(Intent intent, IBinder token, int ident,
ActivityInfo info, Configuration curConfig, Configuration overrideConfig,
CompatibilityInfo compatInfo, String referrer, IVoiceInteractor voiceInteractor,
int procState, Bundle state, PersistableBundle persistentState,
List<ResultInfo> pendingResults, List<ReferrerIntent> pendingNewIntents,
boolean notResumed, boolean isForward, ProfilerInfo profilerInfo) {
updateProcessState(procState, false);
ActivityClientRecord r = new ActivityClientRecord();
...
sendMessage(H.LAUNCH_ACTIVITY, r);
}

ActivityThread中的Handler会对发送的LAUNCH_ACTIVITY消息进行处理,处理如下

1
2
3
4
5
6
7
Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "activityStart");
final ActivityClientRecord r = (ActivityClientRecord) msg.obj;
r.packageInfo = getPackageInfoNoCheck(
r.activityInfo.applicationInfo, r.compatInfo);
handleLaunchActivity(r, null);
Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);

这里调用了handleLaunchActivity方法,而handleLaunchActivity内部又掉用了performLaunchActivity方法,这个方法代码较多,我省略掉对于我们来说不是很重要的代码,如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
...
try {
java.lang.ClassLoader cl = r.packageInfo.getClassLoader();
activity = mInstrumentation.newActivity(
cl, component.getClassName(), r.intent);
StrictMode.incrementExpectedActivityCount(activity.getClass());
r.intent.setExtrasClassLoader(cl);
r.intent.prepareToEnterProcess();
if (r.state != null) {
r.state.setClassLoader(cl);
}
} catch (Exception e) {
if (!mInstrumentation.onException(activity, e)) {
throw new RuntimeException(
"Unable to instantiate activity " + component
+ ": " + e.toString(), e);
}
}
try {
Application app = r.packageInfo.makeApplication(false, mInstrumentation);
...
if (activity != null) {
Context appContext = createBaseContextForActivity(r, activity);
CharSequence title = r.activityInfo.loadLabel(appContext.getPackageManager());
Configuration config = new Configuration(mCompatConfiguration);
if (DEBUG_CONFIGURATION) Slog.v(TAG, "Launching activity "
+ r.activityInfo.name + " with config " + config);
activity.attach(appContext, this, getInstrumentation(), r.token,
r.ident, app, r.intent, r.activityInfo, title, r.parent,
r.embeddedID, r.lastNonConfigurationInstances, config,
r.referrer, r.voiceInteractor);
...
activity.mCalled = false;
if (r.isPersistable()) {
mInstrumentation.callActivityOnCreate(activity, r.state, r.persistentState);
} else {
mInstrumentation.callActivityOnCreate(activity, r.state);
}
...
return activity;
}

可以看到这里首先通过ClassLoader加载并通过反射创建Activity对象,如果Application还没有创建,那么创建Application对象,然后创建Context对象并回调相应的生命周期方法。

那么到这里Activity启动的流程就结束了,这里其实我只是简单的描述了与我们本文有关的部分,当然如果有兴趣对其中流程更多细节了解的部分,可以去查看老罗的代码分析。

启动插件Activity

首先我们简单想一下启动插件Activity会面临的问题

1.插件中的Activity并没有在我们的宿主Manifestfest中注册
2.Activity对象创建通过ClassLoader加载,这里的ClassLoader是宿主的加载器并非插件Apk的加载器

那我们分别来解决这些已知的问题

瞒天过海—-启动非Manifestfest的Activity

  • 瞒天过海 :意思是瞒住上天,偷渡大海。比喻用谎言和伪装向别人隐瞒自己的真实意图,在背地里偷偷地行动。

怎么瞒住系统去启动非Manifestfest的Activity呢?

我们可以在AndroidManifest.xml里面声明一个替身Activity,在启动的非Manifestfest的Activity的时候将它替换成我们的替身Activity,瞒住系统的检查,然后在合适的时候把这个假的替换成我们真正需要启动的Activity就可以了。

那么接下来的问题就什么时候去将要启动Activity替换为替身Activity和将替身Activity再替换回要启动的Activity?

上面对于Activity启动进行了分析,对于上面的流程可以简化为下图表示


图2 Activity启动流程简图

Activity信息检验过程是在AMS进程完成的,我们对system_server进程里面的操作无能为力,只有在我们App进程里面执行的过程才是有可能被Hook掉的。所以我们只能在第一步里面去做启动Activity替换为替身Activity,第三步做替身Activity替换回要启动的Activity,一步步来分别介绍处理。

1.启动Activity替换为替身Activity

我们之前在启动流程已经分析过了,两种启动方式都会调用到Instrumentation的execStartActivity方法,这里是App进程,而execStartActivity方法则调用ActivityManagerService的startActivity,这里则已经是AMS进程了,所以我们替换启动Activity貌似只有Instrumentation这个方法了。

但是我们仔细分析一下如果hook住Instrumentation的话需要两个入口去做相关的hook,而且其中一个入口还与我们调用的Activity有关联,处理起来会比较麻烦,所以我们可以选择从调用ActivityManagerService这里入手看一下能否满足我们的需求

1
2
3
4
5
int result = ActivityManagerNative.getDefault()
.startActivity(whoThread, who.getBasePackageName(), intent,
intent.resolveTypeIfNeeded(who.getContentResolver()),
token, target != null ? target.mEmbeddedID : null,
requestCode, 0, null, options);

我们看一下这里的 ActivityManagerNative.getDefault()方法,如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
static public IActivityManager getDefault() {
return gDefault.get();
}
private static final Singleton<IActivityManager> gDefault = new Singleton<IActivityManager>() {
protected IActivityManager create() {
IBinder b = ServiceManager.getService("activity");
if (false) {
Log.v("ActivityManager", "default service binder = " + b);
}
IActivityManager am = asInterface(b);
if (false) {
Log.v("ActivityManager", "default service = " + am);
}
return am;
}
};

其实前面也说了,这里是调用了ActivityManagerService这个远程对象的Binder代理对象,而ActivityManagerService继承自ActivityManagerNative,ActivityManagerNative又继承了接口类IActivityManager,所以这里其实可以用动态代理的方式来hook住ActivityManagerService。

至于动态代理是啥?自行百度了

接下来看一下我们Hook住ActivityManagerService的方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//获取ActivityManagerNative类
Class<?> activityManagerNativeClass = Class.forName("android.app.ActivityManagerNative");
//获取ActivityManagerNative中的变量gDefault
Field gDefaultField = activityManagerNativeClass.getDeclaredField("gDefault");
gDefaultField.setAccessible(true);
//gDefault为静态变量,所以可以通过get(null)获取静态变量的对象
Object gDefault = gDefaultField.get(null);
Class<?> singleton = Class.forName("android.util.Singleton");
Field mInstanceField = singleton.getDeclaredField("mInstance");
mInstanceField.setAccessible(true);
//获取gDefault获取原始的的IActivityManager对象
Object rawIActivityManager = mInstanceField.get(gDefault);
Class<?> iActivityManagerInterface = Class.forName("android.app.IActivityManager");
//创建一个IActivityManager对象的代理对象, 然后替换这个字段, 让我们的代理对象帮忙干活
Object proxy = Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(),
new Class<?>[]{iActivityManagerInterface}, new IActivtyManangerHandler(rawIActivityManager));
mInstanceField.set(gDefault, proxy);

所以当我们hook住这里的ActivityManagerNative后,下一次系统运行到这里就会获取到我们的代理对象,通过代理对象我们可以实现一些相关的替换工作。

接下来看一下代理对象的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class IActivtyManangerHandler implements InvocationHandler{
Object mBase;
public IActivtyManangerHandler(Object mBase) {
this.mBase = mBase;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if ("startActivity".equals(method.getName())){
Intent raw;
int index = 0;
for (int i = 0; i < args.length; i++) {
if (args[i] instanceof Intent) {
index = i;
break;
}
}
raw = (Intent) args[index];
ComponentName targetComponentName = raw.getComponent();
Intent intent = new Intent();
ComponentName subComponentName = new ComponentName(stubPackage,SubActivity.class.getName());
intent.setComponent(subComponentName);
intent.putExtra(ActivityHookHelper.EXTRA_TARGET_INTENT,raw);
args[index] = intent;
return method.invoke(mBase,args);
}
return method.invoke(mBase,args);
}
}

这里其实就是监听到了ActivityManagerService的startActivity方法,然后我们取出之前存进去的启动需要启动Activity的intent,替换构建我们替身Activity的intent,同时将启动Activity的intent作为传递数据同时传递过去,完成这些操作后,最后调用method.invoke(mBase,args)方法,继续ActivityManagerService的startActivity方法,这时候已经将其中的参数替换了,并且用替身Activity是可以通过后续的各种Activity信息校验过程的。

那么启动Activity替换为替身Activity我们这里已经完成了,接下来就是还原为启动Activity了

2.替身Activity还原为启动Activity

依据上面的代码分析与前面提到的只有在我们App进程里面执行的过程才是有可能被Hook掉的,所以我们可以处理的地方只有在ActivityStackSupervisor中realStrartActivityLocked方法调用ActivityThread的scheduleLaunchActivity方法后,也就是ActivityThread才是我们可能可以Hook的地方。

scheduleLaunchActivity方法其实是发送LAUNCH_ACTIVITY消息,而接下来ActivityThread中的Handler会对发送的LAUNCH_ACTIVITY消息进行处理。

那么我们了解下Handler是如何处理接收到的Message的,如果我们能拦截这个Message的接收过程,就有可能完成替身恢复工作;Handler类的dispathMesage如下:

1
2
3
4
5
6
7
8
9
10
11
12
public void dispatchMessage(Message msg) {
if (msg.callback != null) {
handleCallback(msg);
} else {
if (mCallback != null) {
if (mCallback.handleMessage(msg)) {
return;
}
}
handleMessage(msg);
}
}

可以看到默认情况下Handler的派发流程是:

  • 如果Message中的callback不为空,通过callback来处理(开头我们提到Message中有一个callback)
  • 如果Handler的mCallback不为空,通过mCallback来处理
  • 如果上面两个都为空,才调用handleMessage来处理

而ActivityThread中仅仅是重构了Handler的handleMessage方法如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public void handleMessage(Message msg) {
if (DEBUG_MESSAGES) Slog.v(TAG, ">>> handling: " + codeToString(msg.what));
switch (msg.what) {
case LAUNCH_ACTIVITY: {
Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "activityStart");
final ActivityClientRecord r = (ActivityClientRecord) msg.obj;
r.packageInfo = getPackageInfoNoCheck(
r.activityInfo.applicationInfo, r.compatInfo);
handleLaunchActivity(r, null);
Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
} break;
}
}

所以我们可以hook住Handler的mCallback,在mCallback.handleMessag中去进行处理,处理后手动调用Handler的handleMessage方法,这样就可以将我们要做的操作完成,并且顺利的调用了handleMessage方法。

接下来看一下如何Hook住ActivityTread的Handler中的mCallback

ActivityTread中有一个静态的ActivityTread变量currentActivityThread,我们可以通过这个静态变量获取到ActivityTread对象,然后获取ActivityTread类中的Handler对象,接下来将Handler对象中的mCallback替换为我们需要操作的Callback即可,如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
Method currentActivityThreadMethod = activityThreadClass.getDeclaredMethod("currentActivityThread");
currentActivityThreadMethod.setAccessible(true);
//获取ActivityThread对象
Object currentActivityThread = currentActivityThreadMethod.invoke(null);
Field mHField = activityThreadClass.getDeclaredField("mH");
mHField.setAccessible(true);
//获取ActivityThread变量mH对象
Handler mH = (Handler) mHField.get(currentActivityThread);
Field mCallBackField = Handler.class.getDeclaredField("mCallback");
mCallBackField.setAccessible(true);
//替换mH这个Handler对象的mCallback变量
mCallBackField.set(mH, new ActivityThreadHandlerCallback(mH));

所以当scheduleLaunchActivity方法其实是发送LAUNCH_ACTIVITY消息后,其实是我们这里定义的ActivityThreadHandlerCallback这个Callback方法会收到这个消息与相关的参数,接下来我们看下ActivityThreadHandlerCallback这个Callback方法如何处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class ActivityThreadHandlerCallback implements Handler.Callback{
Handler mBase;
public ActivityThreadHandlerCallback(Handler base) {
mBase = base;
}
@Override
public boolean handleMessage(Message msg) {
switch (msg.what){
case 100:
handleLaunchActivity(msg);
break;
}
mBase.handleMessage(msg);
return true;
}
private void handleLaunchActivity(Message msg){
Object obj = msg.obj;
try {
Field intent = obj.getClass().getDeclaredField("intent");
intent.setAccessible(true);
Intent raw = (Intent) intent.get(obj);
Intent target = raw.getParcelableExtra(ActivityHookHelper.EXTRA_TARGET_INTENT);
raw.setComponent(target.getComponent());
} catch (Exception e) {
e.printStackTrace();
}
}
}

可以看到我们这里其实就是取出了其中的替身intent对象,接下来将传递过来的真实Activity的intent对象替换掉替身intent对象,最后调用mH中重载的handleMessage方法,这样就可以继续走Activity的创建流程了,但是其中的数据已经被我们替换完成了。

到这里就完成了关于启动非Manifestfest的Activity,我们绕过了AMS进行对于Activity相关的信息校验。

釜底抽薪—-替换宿主的ClassLoader

  • 瞒天过海 :意思是把柴火从锅底抽掉,才能使水止沸。比喻从根本上解决问题。

前面我们已经完成了关于启动非Manifestfest的Activity,这时候我们要是启动宿主中非Manifestfest的Activity肯定是没有问题的,但是要是启动插件中的Apk还是不行的,前面分析的时候也说到了这里的ClassLoader是宿主的加载器并非插件Apk的加载器

我们继续看一下ClassLoader加载并通过反射创建Activity对象

1
2
java.lang.ClassLoader cl = r.packageInfo.getClassLoader();
activity = mInstrumentation.newActivity(cl, component.getClassName(), r.intent);

这里系统通过待启动的Activity的类名className,然后使用ClassLoader对象cl把这个类加载进虚拟机,最后使用反射创建了这个Activity类的实例对象。cl这个ClasssLoader对象通过r.packageInfo对象的getClassLoader()方法得到,r.packageInfo是一个LoadedApk类的对象;那么,LoadedApk到底是个什么东西??

LoadedApk对象是APK文件在内存中的表示。 Apk文件的相关信息,诸如Apk文件的代码和资源,甚至代码里面的Activity,Service等组件的信息我们都可以通过此对象获取。

而追溯这个r.packageInfo,他来源于handleMesage方法里

1
2
3
4
5
6
7
Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "activityStart");
final ActivityClientRecord r = (ActivityClientRecord) msg.obj;
r.packageInfo = getPackageInfoNoCheck(
r.activityInfo.applicationInfo, r.compatInfo);
handleLaunchActivity(r, null);
Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);

这个r.packageInfo是调用getPackageInfoNoCheck方法获取到的,这个方法又调用了getPackageInfo,我们看下这个方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
private LoadedApk getPackageInfo(ApplicationInfo aInfo, CompatibilityInfo compatInfo,
ClassLoader baseLoader, boolean securityViolation, boolean includeCode,
boolean registerPackage) {
final boolean differentUser = (UserHandle.myUserId() != UserHandle.getUserId(aInfo.uid));
synchronized (mResourcesManager) {
WeakReference<LoadedApk> ref;
if (differentUser) {
// Caching not supported across users
ref = null;
} else if (includeCode) {
ref = mPackages.get(aInfo.packageName);
} else {
ref = mResourcePackages.get(aInfo.packageName);
}
LoadedApk packageInfo = ref != null ? ref.get() : null;
if (packageInfo == null || (packageInfo.mResources != null
&& !packageInfo.mResources.getAssets().isUpToDate())) {
if (localLOGV) Slog.v(TAG, (includeCode ? "Loading code package "
: "Loading resource-only package ") + aInfo.packageName
+ " (in " + (mBoundApplication != null
? mBoundApplication.processName : null)
+ ")");
packageInfo =
new LoadedApk(this, aInfo, compatInfo, baseLoader,
securityViolation, includeCode &&
(aInfo.flags&ApplicationInfo.FLAG_HAS_CODE) != 0, registerPackage);
if (mSystemThread && "android".equals(aInfo.packageName)) {
packageInfo.installSystemApplicationInfo(aInfo,
getSystemContext().mPackageInfo.getClassLoader());
}
if (differentUser) {
// Caching not supported across users
} else if (includeCode) {
mPackages.put(aInfo.packageName,
new WeakReference<LoadedApk>(packageInfo));
} else {
mResourcePackages.put(aInfo.packageName,
new WeakReference<LoadedApk>(packageInfo));
}
}
return packageInfo;
}
}

这里首先通过当前进程userId与传入App信息的userId是否相同,如果相同,那么可以共享缓存数据(要么缓存的代码数据,要么缓存的资源数据)

接下来尝试获取缓存数据;如果没有命中缓存数据,才通过LoadedApk的构造函数创建了LoadedApk对象;创建成功之后,如果是同一个uid还放入了缓存。

那么其实我们可以手动将我们的插件Apk的LoadApk放入这块缓存数据中,这样当系统获取的时候就可以获取到插件Apk的LoadApk信息,从而完成后面的创建Activity类的实例对象。

那么我们需要先构建出插件Apk的LoadApk,而构建方法则可以用上述的getPackageInfo,但是这是一个私有的方法,可能会面临后续系统修改带来的变化,并不安全,而获取LoadApk还有一个公共方法就是前面提到的getPackageInfoNoCheck方法,如下

1
2
3
4
public final LoadedApk getPackageInfoNoCheck(ApplicationInfo ai,
CompatibilityInfo compatInfo) {
return getPackageInfo(ai, compatInfo, null, false, true, false);
}

这里需要两个参数

  • ApplicationInfo 从一个特定的应用得到的信息。这些信息是从相对应的Androdimanifest.xml的< application>标签中收集到的。

  • CompatibilityInfo 顾名思义,代表这个App的兼容性信息,比如targetSDK版本等等,在CompatibilityInfo类里面有一个公有字段DEFAULT_COMPATIBILITY_INFO代表默认兼容性信息,我们用这个默认信息即可。

构建ApplicationInfo

ApplicationInfo的数据从Androdimanifest.xml中获取,这与我们前面获取packageName与versionCode相类似,我们前面是通过自己解析相关标签获取的,由于ApplicationInfo中的数据相对较多,所以我们还是通过原生系统中的方法来获取ApplicationInfo。

系统获取ApplicationInfo是通过PackageParser这个类的中的方法来实现的,但是这个类的兼容性很差;Google几乎在每一个Android版本都对这个类动刀子,如果坚持使用系统的解析方式,必须写一系列兼容行代码。

我这里使用是6.0即API 23作为例子,非这个版本可能会出现问题

6.0版本中的PackageParser使用generateApplication方法来获取ApplicationInfo,如下

1
2
public static ApplicationInfo generateApplicationInfo(Package p, int flags,
PackageUserState state)

由于PackageParser是@hide属性,我们首先将这个方法反射出来如下

1
2
3
4
5
6
Class<?> packageParserClass = Class.forName("android.content.pm.PackageParser");
Class<?> packageParser$PackageClass = Class.forName("android.content.pm.PackageParser$Package");
Class<?> packageUserStateClass = Class.forName("android.content.pm.PackageUserState");
Method generateApplicationInfoMethod = packageParserClass.getDeclaredMethod("generateApplicationInfo",
packageParser$PackageClass,int.class,packageUserStateClass);

这里需要三个参数,我们一一来构建他们

  • PackageParser.Package
    这个类代表从PackageParser中解析得到的某个apk包的信息,是磁盘上apk文件在内存中的数据结构表示,因此,要获取这个类,肯定需要解析整个apk文件。PackageParser中解析apk的核心方法是parsePackage,这个方法返回的就是一个Package类型的实例,因此我们调用这个方法即可,代码如下

    1
    2
    3
    Object packageParser = packageParserClass.newInstance();
    Method parsePackageMethod = packageParserClass.getDeclaredMethod("parsePackage", File.class, int.class);
    Object packageObj = parsePackageMethod.invoke(packageParser, apkFile, 0);
  • flags 这个是解析方式 我们选择0解析全部即可

  • PackageUserState 代表不同用户中包的信息。由于Android是一个多任务多用户系统,因此不同的用户同一个包可能有不同的状态,这里我们只需要获取包的信息,因此直接使用默认的即可。代码如下

    1
    Object defaultPackageUserState = packageUserStateClass.newInstance();

到这里参数就准备好了,接下来就是调用上面反射出来的方法,生成我们需要的ApplicationInfo,代码如下

1
2
3
4
5
6
ApplicationInfo applicationInfo = (ApplicationInfo) generateApplicationInfoMethod.invoke(packageParser,
packageObj, 0, defaultPackageUserState);
String apkPath = apkFile.getPath();
applicationInfo.sourceDir = apkPath;
applicationInfo.publicSourceDir = apkPath;

将插件Apk的LoadApk放入缓存数据

前面已经构建好了ApplicationInfo,接下来要构建插件Apk的LoadApk了,前面说了构建LoadApk需要用到getPackageInfoNoCheck方法,同时其中参数ApplicationInfo我们已经构建完成了,另外的一个CompatibilityInfo用默认即可,接下来看代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//获取ActivityThread对象
Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
Method currentActivityThreadMethod = activityThreadClass.getDeclaredMethod("currentActivityThread");
currentActivityThreadMethod.setAccessible(true);
Object currentActivityThread = currentActivityThreadMethod.invoke(null);
//反射获取getPackageInfoNoCheck方法
Class<?> compatibilityInfoClass = Class.forName("android.content.res.CompatibilityInfo");
Method getPackageInfoNoCheckMethod = activityThreadClass.getDeclaredMethod("getPackageInfoNoCheck", ApplicationInfo.class, compatibilityInfoClass);
//构建CompatibilityInfo
Field defaultCompatibilityInfoField = compatibilityInfoClass.getDeclaredField("DEFAULT_COMPATIBILITY_INFO");
defaultCompatibilityInfoField.setAccessible(true);
Object defaultCompatibilityInfo = defaultCompatibilityInfoField.get(null);
//构建ApplicationInfo
ApplicationInfo applicationInfo = generateApplicationInfo(apkFile);
//生成插件的LoadedApk
Object loadedApk = getPackageInfoNoCheckMethod.invoke(currentActivityThread, applicationInfo, defaultCompatibilityInfo);

代码注释很清楚了,这里就构建好了插件的LoadedApk,接下来我们需要替换其中的ClassLoader,关于插件的ClasLoader怎么生成在上一篇文章中已经有说明了,这里就不在赘述了,代码如下

1
2
3
4
5
File optimizedDirectoryFile = TransFormerManager.getContext().getDir("dex", Context.MODE_PRIVATE);
ClassLoader classLoader = new DexClassLoader(apkFile.getPath(), optimizedDirectoryFile.getPath(), null, ClassLoader.getSystemClassLoader());
Field mClassLoaderField = loadedApk.getClass().getDeclaredField("mClassLoader");
mClassLoaderField.setAccessible(true);
mClassLoaderField.set(loadedApk, classLoader);

到这里就真正的构建好了插件的LoadedApk,接下来就是将插件Apk的LoadApk放入缓存数据中,我们首先获取
ActivityThread中的缓存Map变量mPackages,接下来直接调用put方法即可,如下

1
2
3
4
5
6
7
8
9
10
//获取到 mPackages 这个静态成员变量, 这里缓存了dex包的信息
Field mPackagesField = activityThreadClass.getDeclaredField("mPackages");
mPackagesField.setAccessible(true);
Map mPackages = (Map)mPackagesField.get(currentActivityThread);
//保持引用,防止弱引用被GC
mLoadedApk.put(applicationInfo.packageName, loadedApk);
WeakReference weakReference = new WeakReference(loadedApk);
mPackages.put(applicationInfo.packageName,weakReference);

那么到这里就可以将插件中的Activity启动起来了,当然这里的Activity就是纯的Activity非AppCompatActivity

关于启动插件AppCompatActivity

对于上面的成功启动的插件Activity是指的纯的Activity并非AppCompatActivity,启动AppCompatActivity则会报错,当然我花了蛮久时间还是没有解决启动AppCompatActivity的问题,这里我将当时研究时候用的Hook方法和思路说明一下,希望有同学可以指导一下。

当启动AppCompatActivity的时候,会报出如下错误信息


图3 启动AppCompatActivity错误信息

可以看到这里成功启动了插件的AppCompatActivity,但是在AppCompatActivity构建Toolbar的时候出现问题了

所以我们要看一下为什么会错误,错误信息在TypedArray的getDimensionPixelOffset方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public int getDimensionPixelOffset(int index, int defValue) {
if (mRecycled) {
throw new RuntimeException("Cannot make calls to a recycled instance!");
}
index *= AssetManager.STYLE_NUM_ENTRIES;
final int[] data = mData;
final int type = data[index+AssetManager.STYLE_TYPE];
if (type == TypedValue.TYPE_NULL) {
return defValue;
} else if (type == TypedValue.TYPE_DIMENSION) {
return TypedValue.complexToDimensionPixelOffset(
data[index + AssetManager.STYLE_DATA], mMetrics);
} else if (type == TypedValue.TYPE_ATTRIBUTE) {
final TypedValue value = mValue;
getValueAt(index*AssetManager.STYLE_NUM_ENTRIES, value);
throw new UnsupportedOperationException(
"Failed to resolve attribute at index " + index + ": " + value);
}
throw new UnsupportedOperationException("Can't convert to dimension: type=0x"
+ Integer.toHexString(type));
}

可以看到这里错误的原因就是type并非是TYPE_DIMENSION或者TYPE_NULL,所以才会报上述的错误,我查看了正常的启动这里的type是为TYPE_DIMENSION的,这里通过正常与错误对比是由于这里的data数据不一致,从而导致产生的type不一致从而导致的错误。

而data数据是从mData获取的,这个mData是初始化就传进来的,而TintTypeArray在调用TypedArray的getDimensionPixelOffset方法时候会先初始化TypedArray,如下

1
2
3
4
5
public static TintTypedArray obtainStyledAttributes(Context context, AttributeSet set,
int[] attrs, int defStyleAttr, int defStyleRes) {
TypedArray array = context.obtainStyledAttributes(set, attrs, defStyleAttr, defStyleRes);
return new TintTypedArray(context, array);
}

经过层层调用,最后是通过Resource内部类Theme的obtainStyledAttributes方法,如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public TypedArray obtainStyledAttributes(AttributeSet set,
@StyleableRes int[] attrs, @AttrRes int defStyleAttr, @StyleRes int defStyleRes) {
final int len = attrs.length;
final TypedArray array = TypedArray.obtain(Resources.this, len);
// XXX note that for now we only work with compiled XML files.
// To support generic XML files we will need to manually parse
// out the attributes from the XML file (applying type information
// contained in the resources and such).
final XmlBlock.Parser parser = (XmlBlock.Parser)set;
AssetManager.applyStyle(mTheme, defStyleAttr, defStyleRes,
parser != null ? parser.mParseState : 0, attrs, array.mData, array.mIndices);
array.mTheme = this;
array.mXml = parser;
...
return array;
}

通过debug,发现在通过AssetManager.applyStyle()方法前,array是相同的,但是执行后两者arry就是天翻地覆的差别了,而AssetManager.applyStyle()又是调用下层的方法,无能为力,只好从applyStyle的参数下手,通过参数的差距来看运行错误的原因。

通过debug对比我们会发现参数不同的情况存在mTheme与attrs这两个参数上面,而这里的mTheme是获取context中的theme,所以这两个参数其实都是来源于调用的地方,所以其实这个Context最上层的调用其实是我们启动的这个AppCompatActivity。

而这个attrs参数经过向上查看,则来源于LayoutInflater的inflate方法,创建如下

1
final AttributeSet attrs = Xml.asAttributeSet(parser);

而这个parser则来源于上面的调用如下

1
2
final Resources res = getContext().getResources();
final XmlResourceParser parser = res.getLayout(resource);

这个resource是系统自带的resource,所以在正常启动与异常启动都是一样的,所以这里其实又回到了Context上面。

所以通过分析,问题出在了AppCompatActivity的Context中的两个变量了,分别是Resources与Resources.Theme,而在前面的Activity启动流程中我们可以得知这个AppCompatActivity的Context是在ActivtyThread内创建的。

通过Context代码创建我们可以发现与LoadApk是有关系的

所以我第一个想法是Hook掉LoadApk中的Resources,替换为宿主的Resources,但是这个想法是不能实现的

1
2
3
4
5
6
7
8
9
10
11
12
13
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;

因为代码中虽然从LoadApk中获取了Resource,但是获取后又会重新创建一个,所以我们在LoadApk中Hook一个新的Resource是没有用的。

所以只能另寻他法,通过创建Activity代码我们可以看到如下

1
2
3
4
5
6
7
8
9
10
11
activity.mCalled = false;
if (r.isPersistable()) {
mInstrumentation.callActivityOnCreate(activity, r.state, r.persistentState);
} else {
mInstrumentation.callActivityOnCreate(activity, r.state);
}
if (!activity.mCalled) {
throw new SuperNotCalledException(
"Activity " + r.intent.getComponent().toShortString() +
" did not call through to super.onCreate()");
}

所以我们可以通过hook住Instrumentation这个类,至于怎么hook用动态代理的方式,当callActivityOnCreate执行时,我们先将这个方法拦截住,然后将这个Activity参数的Context变量Hook住,主要是将Context中的Resources与Resources.Theme替换为宿主的,从而Hook住。

经过实践确实是起作用了,onCreate是通过了,但是在onResume里面又会产生错误,主要是Instrumentation这个动态代理我们只hook了callActivityOnCreate方法,但是onResume并非使用这种方式的,所以这个方法也只能作罢!

虽然没有成功,但是还是有成果的,经过测试,会发现Resources.Theme是导致AppCompatContext不能启动的原因。

Context中的Resources.Theme是通过Resources创建的,在上一篇说也有提到,所以就想通过Hook住Resources来使Context中的Resources.Theme是我们可以使用的Theme,至于怎么Hook住Resources,上面所采用的方式就不可以了,我们看一下ResourcesManager.getTopLevelResources方法,来看看这个Resources是如何创建的,代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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);
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;
}
}
...
}

可以看到这里其实也是通过缓存去找这个Resources,所以通过Hook住ResourcesManager,向这个mActiveResources缓存中提前写入Resources,则可以在下一次获取Resources时候成功命中这块缓存,但是这么做还是会有问题的,主要是Context中Resources其实还是需要插件的Resources变量,而只是Context的Theme则用宿主的Theme变量,所以这么做是完全实现不了。

所以只能寻找只Hook住Context的Theme变量的方式了,但是通过查看源码,并没有发现可以Hook的地方,故只能停留在这里了。。。

写在后面的话

虽然对于启动插件AppCompatActivity我这里并没有成功,但是对于整个Activity的启动流程有了很深刻的映像,对于Hook相关的知识也有了一定的理解,相信对于后面的Hook相关的文章,大家一定可以轻松掌握了,peace~~~