本文整理自:
http://www.infoq.com/cn/articles/ctrip-android-dynamic-loading
http://blog.csdn.net/lzyzsd/article/details/49768283
一、简介
携程Android App的插件化和动态加载框架已上线半年多,经历了初期的探索和持续的打磨优化,新框架和工程配置经受住了生产实践的考验。本文将详细介绍Android平台插件式开发和动态加载技术的原理和实现细节,回顾携程Android App的架构演化过程,期望我们的经验能帮助到更多的Android工程师。
例如用于调试功能的模块可以在需要时进行下载后进行加载,减少App Size
这个只是一个理想,暂时没有这么做,实际操作起来要复杂些。
二、基本原理关于插件化思想,软件业已经有足够多的用户教育。无论是日常使用的浏览器,还是陪伴程序员无数日夜的Eclipse,甚至连QQ背后,都有插件化技术的支持。对于携程来说以前机票,酒店,火车票,旅行日程这些BU虽然已经拆分为了各自的工程,但是他们的Native代码都是直接集成到携程旅行这个APP的apk中,并且在启动时全部加载。插件化和动态加载就是把这些BU的Native代码分别打包成apk文件,放在携程旅行这个APP的apk的asset目录中。用户在启动时,主要加载框架的dex,其他的在需要时再加载。
三、主要难题
我们要在Android上实现插件化,主要需要考虑2个问题:
编译期:资源和代码的编译。特别是各插件包资源索引文件R中id的冲突四、Android资源和代码的编译
首先来回顾下Android是如何进行编译的。请看下图:
-I add an existing package to base include set
-G A file to output proguard options into.
-J specify where to output R.java resource constant definitions
//android.jar中的资源,其PackageID为0x01
public static final int cancel = 0x01040000;
//用户app中的资源,PackageID总是0x7F
public static final int zip_code = 0x7f090f2e;
我们修改aapt后,是可以给每个子apk中的资源分配不同头字节PackageID,这样就不会再互相冲突。
appt相关的源码都在framework/base/tools/aapt目录下。
首先查看ResourceTable.cpp中的构造函数
ResourceTable::ResourceTable(Bundle* bundle, const String16& assetsPackage, ResourceTable::PackageType type)
ssize_t packageId = -1;
switch (mPackageType) {
case App:
case AppFeature:
packageId = 0x7f;
break;
case System:
packageId = 0x01;
break;
case SharedLibrary:
packageId = 0x00;
break;
default:
assert(0);
break;
}
status_t buildResources(Bundle* bundle, const sp& assets, sp& builder)
ResourceTable::PackageType packageType = ResourceTable::App;
if (bundle->getBuildSharedLibrary()) {
packageType = ResourceTable::SharedLibrary;
} else if (bundle->getExtending()) {
packageType = ResourceTable::System;
} else if (!bundle->getFeatureOfPackage().isEmpty()) {
packageType = ResourceTable::AppFeature;
}
else if (!bundle->getFeatureOfPackage().isEmpty()) {
packageType = ResourceTable::AppFeature;
} else if (bundle->getPackageId() != 0) {
packageType = bundle.getPackageId();
}
switch (*cp) {
case 'v':
bundle.setVerbose(true);
break;
case 'a':
bundle.setAndroidList(true);
break;
2、代码的编译
大家对Java代码的编译应该相当熟悉,只需要注意以下几个问题即可:1、运行时资源的加载
平常我们使用资源,都是通过AssetManager类和Resources类来访问的。获取它们的方法位于Context类中。
/** Return an AssetManager instance for your application's package. */
public abstract AssetManager getAssets();
/** Return a Resources instance for your application's package. */
public abstract Resources getResources();
private final Resources mResources;
@Override
public AssetManager getAssets() {
return getResources().getAssets();
}
@Override
public Resources getResources() {
return mResources;
}
/**
* Add an additional set of assets to the asset manager. This can be
* either a directory or ZIP file. Not for use by applications. Returns
* the cookie of the added asset, or 0 on failure.
* {@hide}
*/
public final int addAssetPath(String path) {
synchronized (this) {
int res = addAssetPathNative(path);
makeStringBlocks(mStringBlocks);
return res;
}
}
当然,上述替换都会针对Application的Context来操作。
把Resources反注到普通Context
BundleCore.java中Resources本身的反注呢?
public void runDelegateResources() {
try {
DelegateResources.newDelegateResources(RuntimeArgs.androidApplication, RuntimeArgs.delegateResources);
} catch (Exception ex) {
ex.printStackTrace();
Map dic = new HashMap();
dic.put("error", ex.getMessage());
CtripActionLogUtil.logTrace("o_resource_error", dic);
}
}
DelegateResources的源码如下:
/**
* Created by yb.wang on 15/1/5.
* 挂载载系统资源中,处理框架资源加载
*/
public class DelegateResources extends Resources {
static final Logger log;
static {
log = LoggerFactory.getLogcatLogger("DelegateResources");
}
public DelegateResources(AssetManager assets, Resources resources) {
super(assets, resources.getDisplayMetrics(), resources.getConfiguration());
}
public static void newDelegateResources(Application application, Resources resources) throws Exception {
List bundles = Framework.getBundles();
if (bundles != null && !bundles.isEmpty()) {
Resources delegateResources;
List arrayList = new ArrayList();
arrayList.add(application.getApplicationInfo().sourceDir);
for (Bundle bundle : bundles) {
//if (BundleCore.DELAYED_PACKAGES.containsValue(bundle.getLocation())) continue;
arrayList.add(((BundleImpl) bundle).getArchive().getArchiveFile().getAbsolutePath());
}
AssetManager assetManager = AssetManager.class.newInstance();
for (String str : arrayList) {
SysHacks.AssetManager_addAssetPath.invoke(assetManager, str);
}
//处理小米UI资源
if (resources == null || !resources.getClass().getName().equals("android.content.res.MiuiResources")) {
delegateResources = new DelegateResources(assetManager, resources);
} else {
Constructor declaredConstructor = Class.forName("android.content.res.MiuiResources").getDeclaredConstructor(new Class[]{AssetManager.class, DisplayMetrics.class, Configuration.class});
declaredConstructor.setAccessible(true);
delegateResources = (Resources) declaredConstructor.newInstance(new Object[]{assetManager, resources.getDisplayMetrics(), resources.getConfiguration()});
}
RuntimeArgs.delegateResources = delegateResources;
AndroidHack.injectResources(application, delegateResources);
StringBuffer stringBuffer = new StringBuffer();
stringBuffer.append("newDelegateResources [");
for (int i = 0; i < arrayList.size(); i++) {
if (i > 0) {
stringBuffer.append(",");
}
stringBuffer.append(arrayList.get(i));
}
stringBuffer.append("]");
log.log(stringBuffer.toString(), Logger.LogLevel.DBUG);
}
}
}
public static void injectResources(Application application, Resources resources) throws Exception {
Object activityThread = getActivityThread();
if (activityThread == null) {
throw new Exception("Failed to get ActivityThread.sCurrentActivityThread");
}
Object loadedApk = getLoadedApk(activityThread, application.getPackageName());
if (loadedApk == null) {
throw new Exception("Failed to get ActivityThread.mLoadedApk");
}
SysHacks.LoadedApk_mResources.set(loadedApk, resources);
SysHacks.ContextImpl_mResources.set(application.getBaseContext(), resources);
SysHacks.ContextImpl_mTheme.set(application.getBaseContext(), null);
}
把Resources反注到Activity中
Activity是通过ActivityThread创建的。我们可以通过一些代码,让Acitivity在创建时,把Resources反注到Activity中。
首先要得到当前应用程序的ActivityThread。
ActivityThread.java的源码中有一个隐藏的如下方法
public static ActivityThread currentActivityThread() {
return sCurrentActivityThread;
}
在GitHub开源项目 DynamicAPK中是通过SysHacks.java的如下方法来初始化ActivityThread.java的
currentActivityThread方法和AssetManager.java的addAssetPath()方法的。
public static void allMethods() throws HackAssertionException {
ActivityThread_currentActivityThread = ActivityThread.method("currentActivityThread", new Class[0]);
AssetManager_addAssetPath = AssetManager.method("addAssetPath", String.class);
Application_attach = Application.method("attach", Context.class);
}
public static Object getActivityThread() throws Exception {
if (_sActivityThread == null) {
if (Thread.currentThread().getId() == Looper.getMainLooper().getThread().getId()) {
_sActivityThread = SysHacks.ActivityThread_currentActivityThread.invoke(null, new Object[0]);
} else {
Handler handler = new Handler(Looper.getMainLooper());
synchronized (SysHacks.ActivityThread_currentActivityThread) {
handler.post(new ActivityThreadGetter());
SysHacks.ActivityThread_currentActivityThread.wait();
}
}
}
return _sActivityThread;
}
public void init(Application application) throws Exception {
SysHacks.defineAndVerify();
RuntimeArgs.androidApplication = application;
RuntimeArgs.delegateResources = application.getResources();
AndroidHack.injectInstrumentationHook(new InstrumentationHook(AndroidHack.getInstrumentation(), application.getBaseContext()));
}
这里调用AndroidHack的injectInstrumentationHook方法把自己定制的InstrumentationHook通过反射反注到ActivityThread中
public static void injectInstrumentationHook(Instrumentation instrumentation) throws Exception {
Object activityThread = getActivityThread();
if (activityThread == null) {
throw new Exception("Failed to get ActivityThread.sCurrentActivityThread");
}
SysHacks.ActivityThread_mInstrumentation.set(activityThread, instrumentation);
}
InstrumentationHook继承自系统的Instrumentation,其核心代码如下:
public Activity newActivity(Class> cls, Context context, IBinder iBinder, Application application, Intent intent, ActivityInfo activityInfo, CharSequence charSequence, Activity activity, String str, Object obj) throws InstantiationException, IllegalAccessException {
Activity newActivity = this.mBase.newActivity(cls, context, iBinder, application, intent, activityInfo, charSequence, activity, str, obj);
if (RuntimeArgs.androidApplication.getPackageName().equals(activityInfo.packageName) && SysHacks.ContextThemeWrapper_mResources != null) {
SysHacks.ContextThemeWrapper_mResources.set(newActivity, RuntimeArgs.delegateResources);
}
return newActivity;
}
public void callActivityOnCreate(Activity activity, Bundle bundle) {
if (RuntimeArgs.androidApplication.getPackageName().equals(activity.getPackageName())) {
ContextImplHook contextImplHook = new ContextImplHook(activity.getBaseContext());
if (!(SysHacks.ContextThemeWrapper_mBase == null || SysHacks.ContextThemeWrapper_mBase.getField() == null)) {
SysHacks.ContextThemeWrapper_mBase.set(activity, contextImplHook);
}
SysHacks.ContextWrapper_mBase.set(activity, contextImplHook);
}
this.mBase.callActivityOnCreate(activity, bundle);
}
ContextImplHook.java源码如下:
/**
* Created by yb.wang on 15/1/6.
* Android Context Hook 挂载载系统的Context中,拦截相应的方法
*/
public class ContextImplHook extends ContextWrapper {
static final Logger log;
static {
log = LoggerFactory.getLogcatLogger("ContextImplHook");
}
public ContextImplHook(Context context) {
super(context);
}
@Override
public Resources getResources() {
log.log("getResources is invoke", Logger.LogLevel.INFO);
return RuntimeArgs.delegateResources;
}
@Override
public AssetManager getAssets() {
log.log("getAssets is invoke", Logger.LogLevel.INFO);
return RuntimeArgs.delegateResources.getAssets();
}
}
简单点说是这样
创建一个自己的DelegateResources,她继承于Resources。这个自己的DelegateResources使用了通过如下方式,使用了自己定制的AssetManager。
AssetManager assetManager = AssetManager.class.newInstance();
for (String str : arrayList) {
SysHacks.AssetManager_addAssetPath.invoke(assetManager, str);
}
delegateResources = new DelegateResources(assetManager, resources);
public static void injectResources(Application application, Resources resources) throws Exception {
Object activityThread = getActivityThread();
if (activityThread == null) {
throw new Exception("Failed to get ActivityThread.sCurrentActivityThread");
}
Object loadedApk = getLoadedApk(activityThread, application.getPackageName());
if (loadedApk == null) {
throw new Exception("Failed to get ActivityThread.mLoadedApk");
}
SysHacks.LoadedApk_mResources.set(loadedApk, resources);
SysHacks.ContextImpl_mResources.set(application.getBaseContext(), resources);
SysHacks.ContextImpl_mTheme.set(application.getBaseContext(), null);
}
而ContextImplHook本身只是把Context包了一下,重写了 getResources(),返回了我们前面创建的自己的delegateResources;同时也重写了getAssets,返回了我们前面创建的自己的delegateResources的AssetManager。
注:这里的ClassLoader loader可以通过Application来获得。
当然,针对不同Android版本,类加载方式略有不同,可以参考MultiDex源码做具体的区别处理。
六、编码实现
具体编码实现主要分为三部分:
对aapt工具的修改。运行时加载代码的实现。
具体实现细节请参考GitHub开源项目DynamicAPK。