从系统设计的角度,组件和窗口的逻辑实体都存在于系统服务,比如Activity创建后,其逻辑控制主体在AMS,对于窗口,其逻辑控制主体在WMS
android将逻辑主体放置于系统服务,系统就可以对组件和窗口的生命周期,显示状态进行强掌控,这样就能做到在各种状态变更时能做到及时回调通知
所以,创建任何组件,都需要通过RPC通讯到AMS创建 — 第一个hook点
那逻辑主体确定后,AMS就需要创建进程去运行真实的Activity对象(可以认为它是一个提线木偶)
Android进程启动后,JAVA的入口是ActivityThread.main
ActivityThread主要干两件事件
组件相关数据主要包括两个
还有,ActivityThread的设计本身好像就支持加载多个application,多个application会被保存到mAllApplications中
DroidPlugin实现了一个简易的IPluginManagerImpl用于插件APK包的安装和解析,当然这部分代码是参考系统的PMS来实现的,主要职责:
PluginProcessManager.preLoadApk(mHostContext, targetActivityInfo);
preLoadApk内部会根据targetActivityInfo包含的包名来判断LoadedApk是
否创建,如果未创建,则会通过反射调用ActivityThread的函数来创建插件
LoadedApk并保存到ActivityThread的mPackages中,接着创建
PluginClassLoader并设置到LoadedApk对象中
我们先来看下Android常规Activity的启动流程
ActivityClientRecord r = (ActivityClientRecord)msg.obj;
//通过activityinfo中包含的application信息创建loaedapk并保存于packageinfo
r.packageInfo = getPackageInfoNoCheck(
r.activityInfo.applicationInfo, r.compatInfo);
handleLaunchActivity(r, null);
理解上面第1和第5步很重要,因为DroidPlugin的Activity hook就是基于这两个点来进行的,原理总结如下:
.stub.ActivityStub$P00$Standard00
.stub.ActivityStub$P00$SingleInstance00
.stub.ActivityStub$P00$SingleInstance01
.stub.ActivityStub$P00$SingleInstance02
.stub.ActivityStub$P00$SingleInstance03
.stub.ActivityStub$P00$SingleTask00
.stub.ActivityStub$P00$SingleTask01
.stub.ActivityStub$P00$SingleTask02
.stub.ActivityStub$P00$SingleTask03
.stub.ActivityStub$P00$SingleTop00
.stub.ActivityStub$P00$SingleTop01
.stub.ActivityStub$P00$SingleTop02
.stub.ActivityStub$P00$SingleTop03
ActivityInfo activityInfo = resolveActivity(intent);
if (activityInfo != null && isPackagePlugin(activityInfo.packageName)) {
ComponentName component = selectProxyActivity(intent);
if (component != null) {
Intent newIntent = new Intent();
try {
ClassLoader pluginClassLoader = PluginProcessManager.getPluginClassLoader(component.getPackageName());
setIntentClassLoader(newIntent, pluginClassLoader);
} catch (Exception e) {
Log.w(TAG, "Set Class Loader to new Intent fail", e);
}
newIntent.setComponent(component);
newIntent.putExtra(Env.EXTRA_TARGET_INTENT, intent);
newIntent.setFlags(intent.getFlags());
String callingPackage = (String) args[1];
if (TextUtils.equals(mHostContext.getPackageName(), callingPackage)) {
newIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
args[intentOfArgIndex] = newIntent;
args[1] = mHostContext.getPackageName();
}
//PluginCallback.java
private boolean handleLaunchActivity(Message msg) {
try {
Object obj = msg.obj;
Intent stubIntent = (Intent) FieldUtils.readField(obj, "intent");
//ActivityInfo activityInfo = (ActivityInfo) FieldUtils.readField(obj, "activityInfo", true);
stubIntent.setExtrasClassLoader(mHostContext.getClassLoader());
Intent targetIntent = stubIntent.getParcelableExtra(Env.EXTRA_TARGET_INTENT);
// 这里多加一个isNotShortcutProxyActivity的判断,因为ShortcutProxyActivity的很特殊,启动它的时候,
// 也会带上一个EXTRA_TARGET_INTENT的数据,就会导致这里误以为是启动插件Activity,所以这里要先做一个判断。
// 之前ShortcutProxyActivity错误复用了key,但是为了兼容,所以这里就先这么判断吧。
if (targetIntent != null && !isShortcutProxyActivity(stubIntent)) {
IPackageManagerHook.fixContextPackageManager(mHostContext);
ComponentName targetComponentName = targetIntent.resolveActivity(mHostContext.getPackageManager());
ActivityInfo targetActivityInfo = PluginManager.getInstance().getActivityInfo(targetComponentName, 0);
if (targetActivityInfo != null) {
if (targetComponentName != null && targetComponentName.getClassName().startsWith(".")) {
targetIntent.setClassName(targetComponentName.getPackageName(), targetComponentName.getPackageName() + targetComponentName.getClassName());
}
ResolveInfo resolveInfo = mHostContext.getPackageManager().resolveActivity(stubIntent, 0);
ActivityInfo stubActivityInfo = resolveInfo != null ? resolveInfo.activityInfo : null;
if (stubActivityInfo != null) {
PluginManager.getInstance().reportMyProcessName(stubActivityInfo.processName, targetActivityInfo.processName, targetActivityInfo.packageName);
}
PluginProcessManager.preLoadApk(mHostContext, targetActivityInfo);
ClassLoader pluginClassLoader = PluginProcessManager.getPluginClassLoader(targetComponentName.getPackageName());
setIntentClassLoader(targetIntent, pluginClassLoader);
setIntentClassLoader(stubIntent, pluginClassLoader);
boolean success = false;
try {
targetIntent.putExtra(Env.EXTRA_TARGET_INFO, targetActivityInfo);
if (stubActivityInfo != null) {
targetIntent.putExtra(Env.EXTRA_STUB_INFO, stubActivityInfo);
}
success = true;
} catch (Exception e) {
Log.e(TAG, "putExtra 1 fail", e);
}
if (!success && Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT) {
try {
ClassLoader oldParent = fixedClassLoader(pluginClassLoader);
targetIntent.putExtras(targetIntent.getExtras());
targetIntent.putExtra(Env.EXTRA_TARGET_INFO, targetActivityInfo);
if (stubActivityInfo != null) {
targetIntent.putExtra(Env.EXTRA_STUB_INFO, stubActivityInfo);
}
fixedClassLoader(oldParent);
success = true;
} catch (Exception e) {
Log.e(TAG, "putExtra 2 fail", e);
}
}
if (!success) {
Intent newTargetIntent = new Intent();
newTargetIntent.setComponent(targetIntent.getComponent());
newTargetIntent.putExtra(Env.EXTRA_TARGET_INFO, targetActivityInfo);
if (stubActivityInfo != null) {
newTargetIntent.putExtra(Env.EXTRA_STUB_INFO, stubActivityInfo);
}
FieldUtils.writeDeclaredField(msg.obj, "intent", newTargetIntent);
} else {
FieldUtils.writeDeclaredField(msg.obj, "intent", targetIntent);
}
FieldUtils.writeDeclaredField(msg.obj, "activityInfo", targetActivityInfo);
Log.i(TAG, "handleLaunchActivity OK");
} else {
Log.e(TAG, "handleLaunchActivity oldInfo==null");
}
} else {
Log.e(TAG, "handleLaunchActivity targetIntent==null");
}
} catch (Exception e) {
Log.e(TAG, "handleLaunchActivity FAIL", e);
}
if (mCallback != null) {
return mCallback.handleMessage(msg);
} else {
return false;
}
}
先用intent中拿出之前保存到extra的插件intentIntent targetIntent = stubIntent.getParcelableExtra(Env.EXTRA_TARGET_INTENT);
接着根据targetIntent获取对应的activityinfoComponentName targetComponentName = targetIntent.resolveActivity(mHostContext.getPackageManager());
ActivityInfo targetActivityInfo = PluginManager.getInstance().getActivityInfo(targetComponentName, 0);
最后将数据写回到ActivityClientRecord,完成最终的替换 FieldUtils.writeDeclaredField(msg.obj, "intent", targetIntent);
FieldUtils.writeDeclaredField(msg.obj, "activityInfo", targetActivityInfo);
同样的,先来看看service的常规启动流程
ActivityManagerNative.getDefault().publishService(
data.token, data.intent, binder);
Service跟Activity还是存在很大的区别的,service非常独立,也就是说,系统创建service后,除了调用规定的那些回调,传递intent外,剩下就是service自己玩自己的,跟系统一毛钱关系都没有了
Activity则不同,因为其涉及到窗口,所以会存在大量的交互,比如WMS,IMS等
对于DroidPlugin来说,插件service的hook,则会简单很多,只需要用一个stub service做为代理,在stubservice内部根据传入的intent去管理插件service对象即可:
.stub.ServiceStub$StubP00$P00
在startservice和bindservice时,只需要把目标sevice缓存stubservice,并将真实的intent作为extra传递到stub service就可以了
private static ServiceInfo replaceFirstServiceIntentOfArgs(Object[] args) throws RemoteException {
int intentOfArgIndex = findFirstIntentIndexInArgs(args);
if (args != null && args.length > 1 && intentOfArgIndex >= 0) {
Intent intent = (Intent) args[intentOfArgIndex];
ServiceInfo serviceInfo = resolveService(intent);
if (serviceInfo != null && isPackagePlugin(serviceInfo.packageName)) {
ServiceInfo proxyService = selectProxyService(intent);
if (proxyService != null) {
Intent newIntent = new Intent();
//FIXBUG:https://github.com/Qihoo360/DroidPlugin/issues/122
//如果插件中有两个Service:ServiceA和ServiceB,在bind ServiceA的时候会调用ServiceA的onBind并返回其IBinder对象,
// 但是再次bind ServiceA的时候还是会返回ServiceA的IBinder对象,这是因为插件系统对多个Service使用了同一个StubService
// 来代理,而系统对StubService的IBinder做了缓存的问题。这里设置一个Action则会穿透这种缓存。
newIntent.setAction(proxyService.name + new Random().nextInt());
newIntent.setClassName(proxyService.packageName, proxyService.name);
newIntent.putExtra(Env.EXTRA_TARGET_INTENT, intent);
newIntent.setFlags(intent.getFlags());
args[intentOfArgIndex] = newIntent;
return serviceInfo;
}
}
}
return null;
}
接着在stubservice会创建ServcesManager用于插件service管理,所有的stub service回调会同步到ServcesManager里:
public int onStart(Context context, Intent intent, int flags, int startId) throws Exception {
Intent targetIntent = intent.getParcelableExtra(Env.EXTRA_TARGET_INTENT);
if (targetIntent != null) {
ServiceInfo targetInfo = PluginManager.getInstance().resolveServiceInfo(targetIntent, 0);
if (targetInfo != null) {
Service service = mNameService.get(targetInfo.name);
if (service == null) {
handleCreateServiceOne(context, intent, targetInfo);
}
handleOnStartOne(targetIntent, flags, startId);
}
}
return -1;
}
看到没,ServcesManager自己管理mNameService map,service信息则是通过extr中中真实的插件intent来获得,onbind函数同样:
public IBinder onBind(Context context, Intent intent) throws Exception {
Intent targetIntent = intent.getParcelableExtra(Env.EXTRA_TARGET_INTENT);
if (targetIntent != null) {
ServiceInfo info = PluginManager.getInstance().resolveServiceInfo(targetIntent, 0);
Service service = mNameService.get(info.name);
if (service == null) {
handleCreateServiceOne(context, intent, info);
}
return handleOnBindOne(targetIntent);
}
return null;
}
这两个函数在mNameService未包含该service实例的时候,都会调用handleCreateServiceOne,通过反射调用ActivityThrea的方法创建service,从而达到调用oncreate的目地
在插件apk被启动的时候,会通过分析查看apk的receiver组件信息,然后动态注册
先介绍ContentProvider的实现原理
数据连接建立后,后续跟系统也没一毛钱关系了,那理论上provider跟service是一样的,只要能hook数据发送端,接收端用一个stubprovider做代理就可以搞定了
DroidPlugin定义的stubprovider
.stub.ContentProviderStub$StubP00
发送端hook,就是替换binder proxy的过程,看DroidPlugin的getContentProvider的hook代码:
@Override
protected boolean beforeInvoke(Object receiver, Method method, Object[] args) throws Throwable {
if (args != null) {
final int index = 1;
if (args.length > index && args[index] instanceof String) {
String name = (String) args[index];
mStubProvider = null;
mTargetProvider = null;
ProviderInfo info = mHostContext.getPackageManager().resolveContentProvider(name, 0);
mTargetProvider = PluginManager.getInstance().resolveContentProvider(name, 0);
//这里有个很坑爹的事情,就是当插件的contentprovider和host的名称一样,冲突的时候处理方式。
//在Android系统上,是不会出现这种事情的,因为系统在安装的时候做了处理。而我们目前没做处理。so,在出现冲突时候的时候优先用host的。
if (mTargetProvider != null && info != null && TextUtils.equals(mTargetProvider.packageName, info.packageName)) {
mStubProvider = PluginManager.getInstance().selectStubProviderInfo(name);
// PluginManager.getInstance().reportMyProcessName(mStubProvider.processName, mTargetProvider.processName);
// PluginProcessManager.preLoadApk(mHostContext, mTargetProvider);
if (mStubProvider != null) {
args[index] = mStubProvider.authority;
} else {
Log.w(TAG, "getContentProvider,fake fail 1");
}
} else {
mTargetProvider = null;
Log.w(TAG, "getContentProvider,fake fail 2=%s", name);
}
}
}
return super.beforeInvoke(receiver, method, args);
}
这里不管是什么请求,authority都会被改成stub provider的authority,在请求结束后,在将authority关联contentprovider对应的binder proxy设置成DroidPlugin自己的
Object provider = FieldUtils.readField(invokeResult, "provider");
if (provider != null) {
boolean localProvider = FieldUtils.readField(toObj, "provider") == null;
IContentProviderHook invocationHandler = new IContentProviderHook(mHostContext, provider, mStubProvider, mTargetProvider, localProvider);
invocationHandler.setEnable(true);
Class<?> clazz = provider.getClass();
List<Class<?>> interfaces = Utils.getAllInterfaces(clazz);
Class[] ifs = interfaces != null && interfaces.size() > 0 ? interfaces.toArray(new Class[interfaces.size()]) : new Class[0];
Object proxyprovider = MyProxy.newProxyInstance(clazz.getClassLoader(), ifs, invocationHandler);
FieldUtils.writeField(invokeResult, "provider", proxyprovider);
FieldUtils.writeField(toObj, "provider", proxyprovider);
}
接着在IContentProviderHook对发送uri做替换
if (!mLocalProvider && mStubProvider != null) {
final int index = indexFirstUri(args);
if (index >= 0) {
Uri uri = (Uri) args[index];
String authority = uri.getAuthority();
if (!TextUtils.equals(authority, mStubProvider.authority)) {
Uri.Builder b = new Builder();
b.scheme(uri.getScheme());
b.authority(mStubProvider.authority);
b.path(uri.getPath());
b.query(uri.getQuery());
b.appendQueryParameter(Env.EXTRA_TARGET_AUTHORITY, authority);
b.fragment(uri.getFragment());
args[index] = b.build();
}
}
}
将uri的authority替换成stub provider的,将插件provider的authority保存到Env.EXTRA_TARGET_AUTHORITY这个parameter中
stubprovider实现就很简单了,根据Env.EXTRA_TARGET_AUTHORITY的值来创建插件provider,接着做代理就好了,这里不就贴代码了
下面是contentprovider常规初始化流程,大家可以了解下
try {
final java.lang.ClassLoader cl = c.getClassLoader();
localProvider = (ContentProvider)cl.
loadClass(info.name).newInstance();
provider = localProvider.getIContentProvider();
if (provider == null) {
Slog.e(TAG, "Failed to instantiate class " +
info.name + " from sourceDir " +
info.applicationInfo.sourceDir);
return null;
}
if (DEBUG_PROVIDER) Slog.v(
TAG, "Instantiating local provider " + info.name);
// XXX Need to create the correct context for this provider.
localProvider.attachInfo(c, info);
} catch (java.lang.Exception e) {
if (!mInstrumentation.onException(null, e)) {
throw new RuntimeException(
"Unable to get provider " + info.name
+ ": " + e.toString(), e);
}
return null;
}
从代码里可以看出getIContentProvider返回的native binder才是contentprovider数据传输的核心
还有一点很重要,通过AMS.getContentProvider->ActivityThread.acquireProvider,由于ActivityThread处理都是发送消息到mH,所以它是异步的,AMS.getContentProvider如果立即返回,肯定是空的,所以它必须要等待后续ActivityManagerNative.publishContentProviders执行完成后才返回,看AMS.getContentProviderImpl部分代码:
//ActivityManagerService.getContentProviderImpl
//.....前面代码没贴
// Wait for the provider to be published...
synchronized (cpr) {
while (cpr.provider == null) {
if (cpr.launchingApp == null) {
return null;
}
try {
if (conn != null) {
conn.waiting = true;
}
cpr.wait();
} catch (InterruptedException ex) {
} finally {
if (conn != null) {
conn.waiting = false;
}
}
}
}
return cpr != null ? cpr.newHolder(conn) : null;
如果插件都在主进程启动运行,可能有人会有疑问,LoadedApk会不会乱掉?答案肯定是不会的,因为这个是DroidPlugin这个实现方案的前提,咱们看LoadedApk的生成代码
private LoadedApk getPackageInfo(ApplicationInfo aInfo, CompatibilityInfo compatInfo,
ClassLoader baseLoader, boolean securityViolation, boolean includeCode) {
synchronized (mResourcesManager) {
WeakReference<LoadedApk> ref;
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, this, 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));
}
}
return packageInfo;
}
}
ActivityThread会保存LoadeApk的map,key就是package name,所以各个插件的LoadedApk可以独立的存在ActivityThread中
Android资源获取依赖
由于四大组件和Application这五个入口类的创建使用的是插件的class loader,那他们使用过程中用到的R.java肯定是对应插件的,这个不会有任何问题
不过context本质是ContextImpl对象实例,这个对象不是基于插件的class loader创建的,这个要注意,但是它对插件resource独立获取没任何影响,因为
所以,只要完成了插件LoadedApk的创建,组件运行过程中的resource就可以正常获取
DroidPlugin的设计真的很巧妙,作者能构思出这种方案,对组件的初始化肯定是非常熟悉的,这套插件化方案出来也很多年了,最近看一遍,主要还是想学习作者的实现思路,同时也加深自己对组件初始化相关代码的理解
组件实现能被偷天换日是基于Android这么一个设计前提,AMS只是保存组件的逻辑对象主体,ActivityThread只是基于逻辑主体token来创建本地组件对象并做后续跟踪,这就为修改本地组件对象提供了可能
不过这种方式对系统潜入太大了,兼容性会比较差