很久以来都想尝试把项目插件化,做到跟h5一样无缝更新,也不会因为一些小bug而发紧急修复版本。但是基于种种众所周知的问题,一直不敢放在公司项目中来用。前几天同事看到360公布了方案,号称3年实际使用,心中又燃起了一些希望,这几天先下了滴滴的开源方案virtualapk,先摸索着试一下。
在此之前,先回顾下我们正常的apk逻辑。当系统启动时,会初始化PMS(PackageManageService),在这个过程中,会去遍历一些系统指定目录下的apk,并且将manifest中的信息读取并保存在内存中。
一张图记录:
按照正常的思维方式,当我们启动一个activity时,系统应该会去找已经注册过的这个activity隶属的application的manifest属性列表,如果不存在这个类,就会报错;如果存在,就会通过一系列步骤后去回调他的Oncreate之类的方法来启动界面。
那么按照一般的插件化方案,被启动的那个activity通常是另外一个apk,存放在sdcard中,也就是系统压根不知道有这么个activity类存在,这里就存在第一个问题。
为了解惑这个问题,回头又看了一遍activity启动的过程:
按照一般的调用方式,当我们执行startActivity时,会实际调用之前分析的逻辑,见
http://blog.csdn.net/ouxie/article/details/55099146
在上面的这篇文章中可以看到,每次当我们启动一个新的Application,都会创建一个ActivityThread对象,
它负责管理一个application的主线程,管理activitys,broadcast等等。其主要成员如下:
这里主要看下mInstrumentation和mH这2个成员
前者是个Instrumentation对象。后者的成员函数列表如下:
在里面我们可以发现我们很熟悉的很多activity以及application的生命周期函数,这个类负责处理系统和我们应用之间的交互。此类的实体在handleBindApplication函数中有创建。
//如果应用的androidmanifest文件中有定义instrumentation,走这里。
if (data.instrumentationName != null) {
InstrumentationInfo ii = null;
try {
ii = appContext.getPackageManager().
getInstrumentationInfo(data.instrumentationName, 0);
} catch (PackageManager.NameNotFoundException e) {
}
if (ii == null) {
throw new RuntimeException(
"Unable to find instrumentation info for: "
+ data.instrumentationName);
}
mInstrumentationAppDir = ii.sourceDir;
mInstrumentationAppLibraryDir = ii.nativeLibraryDir;
mInstrumentationAppPackage = ii.packageName;
mInstrumentedAppDir = data.info.getAppDir();
mInstrumentedAppLibraryDir = data.info.getLibDir();
ApplicationInfo instrApp = new ApplicationInfo();
instrApp.packageName = ii.packageName;
instrApp.sourceDir = ii.sourceDir;
instrApp.publicSourceDir = ii.publicSourceDir;
instrApp.dataDir = ii.dataDir;
instrApp.nativeLibraryDir = ii.nativeLibraryDir;
LoadedApk pi = getPackageInfo(instrApp, data.compatInfo,
appContext.getClassLoader(), false, true);
ContextImpl instrContext = new ContextImpl();
instrContext.init(pi, null, this);
try {
java.lang.ClassLoader cl = instrContext.getClassLoader();
mInstrumentation = (Instrumentation)
cl.loadClass(data.instrumentationName.getClassName()).newInstance();
} catch (Exception e) {
throw new RuntimeException(
"Unable to instantiate instrumentation "
+ data.instrumentationName + ": " + e.toString(), e);
}
mInstrumentation.init(this, instrContext, appContext,
new ComponentName(ii.packageName, ii.name), data.instrumentationWatcher,
data.instrumentationUiAutomationConnection);
if (mProfiler.profileFile != null && !ii.handleProfiling
&& mProfiler.profileFd == null) {
mProfiler.handlingProfiling = true;
File file = new File(mProfiler.profileFile);
file.getParentFile().mkdirs();
Debug.startMethodTracing(file.toString(), 8 * 1024 * 1024);
}
} else {
//如果没自定义的话,走这里。
mInstrumentation = new Instrumentation();
}
当应用要启动一个activity时,会调用mInstrumentation的execStartActivity()
public ActivityResult execStartActivity(
Context who, IBinder contextThread, IBinder token, Activity target,
Intent intent, int requestCode, Bundle options) {
IApplicationThread whoThread = (IApplicationThread) contextThread;
if (mActivityMonitors != null) {
synchronized (mSync) {
final int N = mActivityMonitors.size();
for (int i=0; i<N; i++) {
final ActivityMonitor am = mActivityMonitors.get(i);
if (am.match(who, null, intent)) {
am.mHits++;
if (am.isBlocking()) {
return requestCode >= 0 ? am.getResult() : null;
}
break;
}
}
}
}
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, null, options);
checkStartActivityResult(result, intent);
} catch (RemoteException e) {
}
return null;
}
一路调用下去会通过handler回到上面说的第二个变量mH,关于这个变量可参考之前的文章。
case LAUNCH_ACTIVITY: {
/// M: enable profiling @{
if ( true == mEnableAppLaunchLog && !mIsUserBuild && false == mTraceEnabled ) {
try {
FileInputStream fprofsts_in = new FileInputStream("/proc/mtprof/status");
if ( fprofsts_in.read()== '3' ) {
Log.v(TAG, "start Profiling for empty process");
mTraceEnabled = true;
Debug.startMethodTracing("/data/data/applaunch"); //applaunch.trace
}
} catch (FileNotFoundException e) {
Slog.e(TAG, "mtprof entry can not be found", e);
} catch (java.io.IOException e) {
Slog.e(TAG, "mtprof entry open failed", e);
}
}
/// @}
Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER | Trace.TRACE_TAG_PERF, "activityStart"); /// M: add TRACE_TAG_PERF for performance debug
ActivityClientRecord r = (ActivityClientRecord)msg.obj;
r.packageInfo = getPackageInfoNoCheck(
r.activityInfo.applicationInfo, r.compatInfo);
handleLaunchActivity(r, null);
在这个分支里,如果发现要起的activity并没有被记录,r.packageInfo就会为空。不过这里也没有说走不下去,还是可以无视地走下去。。。然后再看handleLaunchActivity(),直到performLaunchActivity()函数中,有以下一段:
Activity activity = null;
try {
java.lang.ClassLoader cl = r.packageInfo.getClassLoader();
activity = mInstrumentation.newActivity(
cl, component.getClassName(), r.intent);
StrictMode.incrementExpectedActivityCount(activity.getClass());
r.intent.setExtrasClassLoader(cl);
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);
}
}
这里的
activity = mInstrumentation.newActivity(
cl, component.getClassName(), r.intent);
public Activity newActivity(Class<?> clazz, Context context,
IBinder token, Application application, Intent intent, ActivityInfo info,
CharSequence title, Activity parent, String id,
Object lastNonConfigurationInstance) throws InstantiationException,
IllegalAccessException {
Activity activity = (Activity)clazz.newInstance();
ActivityThread aThread = null;
activity.attach(context, aThread, this, token, application, intent,
info, title, parent, id,
(Activity.NonConfigurationInstances)lastNonConfigurationInstance,
new Configuration());
return activity;
}
问题出来了,这个地方class应该是找不到的,也就初始化不了activity了。
那么滴滴的方案是怎么做的呢?来看下:
首先,当壳应用初始化时,完成以下步骤,hook部分变量们这里一共3个。
然后, 初始化完后,必须先load进存放于sdcard中的apk,并解析完需要的内容。
再者,Load完之后,我们正常使用startactivity函数时,会走如下过程:
调用vainstrumentation中的execStartActivity(),这里如果传入的是startActivyt(context, “com.child.test”)这样的intent,会替换为类似(com.didi.virtualapk.core.A.$1),如下代码:
stubActivity = String.format(STUB_ACTIVITY_STANDARD, corePackage, usedStandardStubActivity);
switch (launchMode) {
case ActivityInfo.LAUNCH_MULTIPLE: {
stubActivity = String.format(STUB_ACTIVITY_STANDARD, corePackage, usedStandardStubActivity);
if (windowIsTranslucent) {
stubActivity = String.format(STUB_ACTIVITY_STANDARD, corePackage, 2);
}
break;
}
case ActivityInfo.LAUNCH_SINGLE_TOP: {
usedSingleTopStubActivity = usedSingleTopStubActivity % MAX_COUNT_SINGLETOP + 1;
stubActivity = String.format(STUB_ACTIVITY_SINGLETOP, corePackage, usedSingleTopStubActivity);
break;
}
case ActivityInfo.LAUNCH_SINGLE_TASK: {
usedSingleTaskStubActivity = usedSingleTaskStubActivity % MAX_COUNT_SINGLETASK + 1;
stubActivity = String.format(STUB_ACTIVITY_SINGLETASK, corePackage, usedSingleTaskStubActivity);
break;
}
case ActivityInfo.LAUNCH_SINGLE_INSTANCE: {
usedSingleInstanceStubActivity = usedSingleInstanceStubActivity % MAX_COUNT_SINGLEINSTANCE + 1;
stubActivity = String.format(STUB_ACTIVITY_SINGLEINSTANCE, corePackage, usedSingleInstanceStubActivity);
break;
}
default:break;
}
我们在corelibrary工程中也确实能看到滴滴预留了十几个activity在他的manifest文件中。
然后调用系统的execStartActivity函数。由于VAInstrumentation被反射进去了,所以它的newActivity()会替换上述的系统函数,如下:
@Override
public Activity newActivity(ClassLoader cl, String className, Intent intent) throws InstantiationException, IllegalAccessException, ClassNotFoundException {
try {
cl.loadClass(className);
} catch (ClassNotFoundException e) {
LoadedPlugin plugin = this.mPluginManager.getLoadedPlugin(intent);
String targetClassName = PluginUtil.getTargetActivity(intent);
Log.i(TAG, String.format("newActivity[%s : %s]", className, targetClassName));
if (targetClassName != null) {
Activity activity = mBase.newActivity(plugin.getClassLoader(), targetClassName, intent);
activity.setIntent(intent);
try {
// for 4.1+
ReflectUtil.setField(ContextThemeWrapper.class, activity, "mResources", plugin.getResources());
} catch (Exception ignored) {
// ignored.
}
return activity;
}
}
return mBase.newActivity(cl, className, intent);
这里的targetClassName就是我们原始要启动的“com.child.test”,再下面就是逐步把资源等替换成新的资源。从而达到启动的目的。最后一步应该是必须的,不过滴滴这里在前面execStartActivity的地方就进行了替换,应该是那之后的很多个步骤中某些地方会因为差找不到相应的activity注册会有问题的节奏。不过中间走过的代码实在太多了,不想看了。。。当然它的方案的复杂度远不止如此,这里只是先熟悉下,为接下来的工作做准备。