[TOC]
插件化介绍
- 插件化: 在主程序能独立运行的前提下,插件程序给主程序提供一些辅助的功能,目前主要以apk或者jar包的方式提供插件。
- 动态加载:主程序在需要的时候才加载该功能的class文件,以达到减少内存占用的目的。
- 主程序:支持插件功能的应用。大型app都有类似的需求。
- 插件程序:给主程序作为补充的程序。例如一些二维码扫描之类的程序。
开源框架
- dynamic-load-apk github地址
- 360DroidPlugin github地址
实现原理介绍
如何加载插件里的class
DexClassloader
public DexClassLoader(String dexPath, File optimizedDirectory, String libraryPath, ClassLoader parent) {
super(parent);
this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
}
具体参数的含义
Parameters:
dexPath 需要装载的APK或者Jar文件的路径。包含多个路径用File.pathSeparator间隔开,在Android上默认是 ":"
optimizedDirectory 优化后的dex文件存放目录,不能为null
libraryPath 目标类中使用的C/C++库的列表,每个目录用File.pathSeparator间隔开; 可以为 null
parent 该类装载器的父装载器,一般用当前执行类的装载器
classloader会有parent的classloader,这样查找类的时候不管是自己的还是parent的类都能够查找到。
具体查找过程
浅析dex文件加载机制
如何使用插件里的资源
在Android里,资源的获取是通过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();
重写这个两个方法,返回含有插件资源的Resource对象。
加载资源的方法是通过反射,通过调用AssetManager中的addAssetPath方法,我们可以将一个apk中的资源加载到Resources对象中,由于addAssetPath是隐藏API我们无法直接调用,所以只能通过反射, 代码如下:
try {
AssetManager assetManager = AssetManager.class.newInstance();
Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);
addAssetPath.invoke(assetManager, mDexPath);
mAssetManager = assetManager;
} catch (Exception e) {
e.printStackTrace();
}
只要想办法把这个重新实现的的context对象返回给插件,就可以加载插件的资源。这个后面会再讨论到。
如何启动插件中的activity
上面提到的两点基本上是业内已经确定的做法,后面则不是特别确定,我们分实现方式来讨论。
方式一:静态代理的模式 dynamic-load-apk
将Activity的生命周期方法提取出来作为一个接口(比如叫DLPlugin),然后通过代理Activity去调用插件Activity的生命周期方法,这样就完成了插件Activity的生命周期管理
public interface DLPlugin {
public void onStart();
public void onRestart();
public void onActivityResult(int requestCode, int resultCode, Intent
data);
public void onResume();
public void onPause();
public void onStop();
public void onDestroy();
public void onCreate(Bundle savedInstanceState);
public void setProxy(Activity proxyActivity, String dexPath);
public void onSaveInstanceState(Bundle outState);
public void onNewIntent(Intent intent);
public void onRestoreInstanceState(Bundle savedInstanceState);
public boolean onTouchEvent(MotionEvent event);
public boolean onKeyUp(int keyCode, KeyEvent event);
public void onWindowAttributesChanged(LayoutParams params);
public void onWindowFocusChanged(boolean hasFocus);
public void onBackPressed();
}
首先 插件中的Activity要继承DLBasePluginActivity
public class DLBasePluginActivity extends FragmentActivity implements DLPlugin {
然后主程序的Manifest文件中要注册用来代理的Activity
DLProxyActivity内容基本如下:
@Override
protected void onStart() {
mRemoteActivity.onStart();
super.onStart();
}
@Override
public AssetManager getAssets() {
return impl.getAssets() == null ? super.getAssets() : impl.getAssets();
}
@Override
public Resources getResources() {
return impl.getResources() == null ? super.getResources() : impl.getResources();
}
mRemoteActivity就是插件中的activity
这种方式的优势是实现简单易懂,缺点是插件必须基于插件库开发,必须要继承DLBasePluginActivity。
缺点:
- 慎用this(接口除外):因为this指向的是当前对象,即apk中的activity,但是由于activity已经不是常规意义上的activity,所以this是没有意义的,但是如果this表示的是一个接口而不是context,比如activity实现了而一个接口,那么this继续有效。
- 使用that:既然this不能用,那就用that,that是apk中activity的基类BaseActivity中的一个成员,它在apk安装运行的时候指向this,而在未安装的时候指向宿主程序中的代理activity,anyway,that is better than this。
- activity的成员方法调用问题:原则来说,需要通过that来调用成员方法,但是由于大部分常用的api已经被重写,所以仅仅是针对部分api才需要通过that去调用用。同时,apk安装以后仍然可以正常运行。
- 启动新activity的约束:启动外部activity不受限制,启动apk内部的activity有限制,首先由于apk中的activity没注册,所以不支持隐式调用,其次必须通过BaseActivity中定义的新方法startActivityByProxy和startActivityForResultByProxy,还有就是不支持LaunchMode。
方式二: 反射的方式
@Override
protected void onResume() {
super.onResume();
Method onResume = mActivityLifecircleMethods.get("onResume");
if (onResume != null) {
try {
onResume.invoke(mRemoteActivity, new Object[] { });
} catch (Exception e) {
e.printStackTrace();
}
}
}
这种方式的优点是插件的apk不需要依赖任何的库文件,通过查找插件的AndroidManifest.xml文件中的activity,把activity启动起来,直接调用即可。缺点就是反射效率较低。
方式三 : Hook的方式
作者将原生的ActivityManager替换成自己的IActivityManagerHook。当startActivity时如果是插件的Activity,这个方法就会被Hook住,改成调用自己实现的startActivity方法。
IActivityManagerHook.java
@Override
public void onInstall(ClassLoader classLoader) throws Throwable {
Class cls = ActivityManagerNativeCompat.Class();
Object obj = FieldUtils.readStaticField(cls, "gDefault");
if (obj == null) {
ActivityManagerNativeCompat.getDefault();
obj = FieldUtils.readStaticField(cls, "gDefault");
}
if (IActivityManagerCompat.isIActivityManager(obj)) {
setOldObj(obj);
Class> objClass = mOldObj.getClass();
List> interfaces = Utils.getAllInterfaces(objClass);
Class[] ifs = interfaces != null && interfaces.size() > 0 ? interfaces.toArray(new Class[interfaces.size()]) : new Class[0];
Object proxiedActivityManager = MyProxy.newProxyInstance(objClass.getClassLoader(), ifs, this);
FieldUtils.writeStaticField(cls, "gDefault", proxiedActivityManager);
}
这样调用方法时,就会进入到invoke方法里,这里作者重写了所有的方法
public class IActivityManagerHookHandle extends BaseHookHandle {
private static final String TAG = IActivityManagerHookHandle.class.getSimpleName();
public IActivityManagerHookHandle(Context hostContext) {
super(hostContext);
}
@Override
protected void init() {
sHookedMethodHandlers.put("startActivity", new startActivity(mHostContext));
sHookedMethodHandlers.put("startActivityAsUser", new startActivityAsUser(mHostContext));
sHookedMethodHandlers.put("startActivityAsCaller", new startActivityAsCaller(mHostContext));
sHookedMethodHandlers.put("startActivityAndWait", new startActivityAndWait(mHostContext));
sHookedMethodHandlers.put("startActivityWithConfig", new startActivityWithConfig(mHostContext));
sHookedMethodHandlers.put("startActivityIntentSender", new startActivityIntentSender(mHostContext));
sHookedMethodHandlers.put("startVoiceActivity", new
.////// more
}
作者也是蛮拼的。
这样在activitymanager里面替换掉要启动的activityinfo和classloader,就可以启动activity了。
框架作者就是用这种方式,在application初始化的过程中将十几个Hook挂在各个系统API上。并且针对不同版本的API做了很多兼容工作,在用户调用系统API的时候框架悄无声息的运作着,使得框架非常易用,几乎任何已有的apk都可以作为插件运行起来。
个人觉得,DroidPlugin应当是目前最好的开源框架。
其余四大组件
其余和activity的做法类似,
- service一般需要预先注册,如果没有的话只能在主程序中注册一个serivce,用来管理所有插件的service。360的做法是注册了10个serivce,如果插件的serivce超过的10个话就把第一个启动的serivce替换掉。
- broadcast的话动态注册完全没有问题,静态注册的话要读取插件的注册信息,再动态注册一下就可以了,
- contentprovider试验了下一般可以直接访问。
如果使用插件中的view和fragment
这里我们用fragment来做示例。
分为两种情况:
- 插件的fragment是attach在插件的activity上,因为之前assetmanager已经通过代理模式重写过,所以没有关系。
- 插件的fragment是attach在主程序的activity上,这时会默认去读取主程序的资源,显然无法获取。
但是fragment的getResource()方法是final的。无法重写。导致出现问题。
研究办法
- 重写support V4包,把final关键字去掉。
但是因为不知道资源是属于主程序还是插件的,此方法仍然不能彻底解决问题。 - 修改aapt,给每个子apk中的资源分配不同头字节PackageID,这样就不会再互相冲突。
在Android中,所有资源会在Java源码层面生成对应的常量ID,这些ID会记录到R.java文件中,参与到之后的代码编译阶段中。在R.java文件中,Android资源在编译过程中会生成所有资源的ID,作为常量统一存放在R类中供其他代码引用。在R类中生成的每一个int型四字节资源ID,实际上都由三个字段组成。第一字节代表了Package,第二字节为分类,三四字节为类内ID。例如:
//android.jar中的资源,其PackageID为0x01
public static final int cancel = 0x01040000;
//用户app中的资源,PackageID总是0x7F
public static final int zip_code = 0x7f090f2e;
我们修改aapt后,是可以给每个子apk中的资源分配不同头字节PackageID,这样就不会再互相冲突。
修改完之后增加如下一行代码即可
Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);
// 加入插件资源
addAssetPath.invoke(assetManager, dexPath);
//加入宿主资源
addAssetPath.invoke(assetManager,
mContext.getApplicationInfo().sourceDir);
这是我们的做法,把所有资源加载一起,其实这时已经可以根据资源ID的前缀区分这个资源属于插件还是主程序,这样通过重写getResource动态来返回也是可以的
Android打包过程如下:
- 使用aapt生成R.java类文件:aapt.exe package -f -m -J
- 使用android SDK提供的aidl.exe把.aidl转成.java文件:aidl OPTIONS INPUT [OUTPUT]
- 编译.java类文件生成class文件: javac
- 使用android SDK提供的dx.bat命令行脚本生成classes.dex文件:dx.bat --dex --output=
- 使用Android SDK提供的aapt.exe生成资源包文件(包括res、assets、androidmanifest.xml等): aapt package
- 生成未签名的apk安装文件:apkbuilder
- 使用jdk的jarsigner对未签名的包进行apk签名: use jarsigner jarsigner -keystore
我们主要修改的就是第1步R文件的生成过程,然后第5步aapt会用R文件的值作为KEY,来查找真正的资源。
ps:其实根据我aapt dump的结果,图片等资源只是存储一个相对路径,例如drawable/*png,并没有带上包名,按道理说后面也是找不到的,但是demo的结果是可以找到的,后面会深入研究。
具体AAPT的修改方式如下:
aapt/Bundle.h
// 参数类,用来封装aapt的参数例如 aapt package -f
class Bundle {
public:
Bundle(void)
: mCmd(kCommandUnknown), mVerbose(false), mAndroidList(false),
mForce(false), mGrayscaleTolerance(0), mMakePackageDirs(false),
mUpdate(false), mExtending(false),
mRequireLocalization(false), mPseudolocalize(false),
mWantUTF16(false), mValues(false),
mCompressionMethod(0), mJunkPath(false), mOutputAPKFile(NULL),
mManifestPackageNameOverride(NULL), mInstrumentationPackageNameOverride(NULL),
mAutoAddOverlay(false), mGenDependencies(false),
mAssetSourceDir(NULL),
mCrunchedOutputDir(NULL), mProguardFile(NULL),
mAndroidManifestFile(NULL), mPublicOutputFile(NULL),
mRClassDir(NULL), mResourceIntermediatesDir(NULL), mManifestMinSdkVersion(NULL),
mMinSdkVersion(NULL), mTargetSdkVersion(NULL), mMaxSdkVersion(NULL),
mVersionCode(NULL), mVersionName(NULL), mCustomPackage(NULL), mExtraPackages(NULL),
mMaxResVersion(NULL), mDebugMode(false), mNonConstantId(false), mProduct(NULL),
mUseCrunchCache(false), mErrorOnFailedInsert(false), mOutputTextSymbols(NULL),
mSingleCrunchInputFile(NULL), mSingleCrunchOutputFile(NULL),
mArgc(0), mArgv(NULL), mIsPlugin(false)
{}
~Bundle(void) {}
src/aapt/Resource.cpp
static status_t parsePackage(Bundle* bundle, const sp& assets,
const sp& grp)
{
// do something...
assets->setPackage(String8(block.getAttributeStringValue(nameIndex, &len)));
printf("======chenchen test ===package-verifier--> %s\n", assets->getPackage().string());
//add by plugin
if(indexOf(const_cast(assets->getPackage().string()), PLUGIN_PREFIX) != -1){
bundle->setIsPlugin(true);
printf("======chenchen test === it is plugin!!!!");
}
}
在resource.cpp里,在解析完package的信息后,如果发现是插件,就将bundle里的参数设成true。
aapt/ResourceTable.cpp
ResourceTable::ResourceTable(Bundle* bundle, const String16& assetsPackage)
: mAssetsPackage(assetsPackage), mNextPackageId(1), mHaveAppPackage(false),
mIsAppPackage(!bundle->getExtending()),
mNumLocal(0),
mBundle(bundle)
{
if(bundle->IsPlugin()){
mPackageID = PLUGIN_PACKAGE_ID; //赋值
}else {
mPackageID = 127;
}
printf("==== package id is %d ====", mPackageID);
}
修改后的编译方法如下:
lunch sdk_eng
make aapt
# 如果想编译自己操作系统对应的aapt,直接编译就好,同时linux系统可以为window编译aapt可执行程序
window版本执行程序编译命令
USE_MINGW=1 OUT_DIR=out-x86 LOCAL_MULTILIB=32 make aapt
修改之后还有一个问题,就是主程序和插件必须使用不同的packageID,之前的想法是修改gradle打包的命令,最后采取修改在插件中定义meta-data,约定一个特殊的字符串,aapt打包时读取这个字符串,发现则认为是插件。没有的话还是正常的打包策略。这个meta-data的value就是给插件指定的id,这样就可以通过读取meta-data,识别这个资源id属于哪个插件。
assets文件夹的访问
插件和主程序的assets文件夹內的资源不可以有同名文件,应该约定加上一些前缀。如plugin_.mp3, host_.mp3
插件化优缺点讨论
优势
- 模块解耦,
- 动态升级,
- 高效并行开发(编译速度更快)
- 按需加载,内存占用更低
- 节省升级流量
- 65536限制
- 扩展方便,随时加入新功能
劣势
- 黑科技,依赖谷歌源码,需要适配系统版本
- 插件化SDK的实现较为复杂,容易出现难解决的bug
- 有些功能很难支持,例如自定义通知栏,activity的四种启动模式