Small是一个写得很是简洁的插件化框架,工程源码位置:https://github.com/wequick/Small
插件化的方案,说到底要解决的核心问题只有三个:linux
1.1 插件类的加载git
- 这个问题的解决和其它插件化框架的解决方法差很少。Android的类是由DexClassLoader加载的,经过反射能够将插件包动态加载进去。Small的gradle插件生成的是.so包,在初始化的时候会经过.so文件生成.zip文件,再由.zip文件生成一个dex元素,反射添加到宿主类加载器的dexPathList里。
1.2 插件资源的处理github
- 这里各插件化框架解决办法通常有两种想法:一是插件间不共享资源访问,办法就是每一个插件生成一个AssertManager来访问它本身的资源,这样就不会存在资源id冲突的问题;另外一种是你们都共用一个AssetManager,这样插件的资源是共享的,能够相互访问,可是要解决资源id冲突的问题。Small采用的是后者,经过修改aapt的生成产物解决了资源id冲突问题,因为共享资源访问,能够作到极小或者根本没有资源冗余,从而减少插件包的大小;
1.3 Activity注册和生命周期问题web
*大部分插件化框架解决办法都是采用在宿主工程里预先注册Activity占坑,而后经过占坑Activity将生命周期回调传回给插件Activity的方式。这里Small处理的比较有特点,经过替换 ActivityThread 里的mInstrumentation,在Instrumentation的newActivty实现里面实例化了插件Activity,经过较小改动就能彻底解决生命周期回调的问题。json
Small的功能模块主要有:
gradle-small插件:Small中的一个gradle自定义插件,用于打包组件;
small library:提供给用户使用的Android Library,主要提供插件加载,解析等功能;缓存
2.1工程命名
首先Small对工程名称以下要求:bash
- app:host工程
- app.*:app插件工程;
- lib.*:library插件工程;
- web.*:web插件工程;
- 其余:其余assert 工程;
2.2 插件引入
在host工程的rootProject的build.gradle中须要引入small插件;
引入small插件后,默认帮你的全部工程引入了一个library依赖small,咱们经过small提供的各类接口来实现插件化得一些功能,好比加载插件,打开某个插件中的ui界面,建立某个插件提供的fragment对象等;微信
2.3 插件声明
做为host程序,要作的最重要的事情就是插件管理,插件跳转uri声明:
插件声明在host程序的assert/bundle.json中声明,格式以下:app
{
"version": "1.0.0", "bundles": [ { "uri": "lib.utils", "pkg": "net.wequick.example.small.lib.utils" }, { "uri": "lib.style", "pkg": "com.example.mysmall.lib.style" }, { "uri": "main", "pkg": "net.wequick.example.small.app.main" }, { "uri": "home", "pkg": "net.wequick.example.small.app.home", "rules"{ "page1",".MyPage1", "page2","net.wequick.example.small.app.home.MyPage2" } }, { "uri": "message", "pkg": "net.wequick.example.small.app.message" }, { "uri": "find", "pkg": "net.wequick.example.small.app.find" }, { "uri": "mine", "pkg": "net.wequick.example.small.app.mine" }, { "uri": "detail", "pkg": "net.wequick.example.small.app.detail" }, { "uri": "about", "pkg": "net.wequick.example.small.web.about" } ] }
bundles 中的每一个元素都是一个插件的声明;
{
"uri": "home", "pkg": "net.wequick.example.small.app.home", "rules"{ "page1",".MyPage1", "page2","net.wequick.example.small.app.home.MyPage2" } }
在采用small框架的应用中,跳转插件的界面都是经过uri来指定的,也就是一个uri惟一对应一个插件;
pkg是插件的包名;
rules:若是插件提供了多个界面供其余人使用,咱们须要经过rules将它们区分开来;
举个例子:
Small.openUri("home", context);
上面这行语句是打开一个插件的界面,home对应的就是上面的uri字段,咱们经过home,查找到对应的插件,而后它会打开这个插件在AndroidManifest.xml中声明的第一个Activity。
若是你要调起插件中声明的其余acitivity,你就须要用到rules了,首先是在bundles.json中声明你要跳转的acitivity,如上,若是你想调用home插件中的界面activity MyPage1,
你只须要写以下语句:
Small.openUri("home/page1", context);
这个时候调用到的就是net.wequick.example.small.app.home.MyPage1
这个类对应的activity了
你在调用插件的时候也能够经过queryparameter传参:
Small.openUri("home?from=main", context); 在调起的插件工程获取参数: Uri uri = Small.getUri(this); if (uri != null) { String from = uri.getQueryParameter("from"); // Do stuff by `from' }
2.4 插件加载管理
在host中咱们通常要作两件事情:
初始化插件的baseUri;
这个通常在Application的onCreate完成:Small.setBaseUri(“http://m.wequick.net/demo/“);
另外是加载全部插件(待优化,一次加载全部插件过于耗时,咱们应该是只加载必备插件,而后再慢慢加载其余插件)
Small.setUp(this, new net.wequick.small.Small.OnCompleteListener() {
@Override
public void onComplete() {
mContentView.postDelayed(new Runnable() {
@Override
public void run() {
Small.openUri(“main”, LaunchActivity.this);
finish();
}
}, 2000);
}
});
setUp提供了插件加载完成的回调,通常咱们等插件加载完才能经过openUri去启动界面展现;
2.5 经常使用操做
打开界面:
Small.openUri(“main”, context);建立插件提供的fragment :
Fragment fragment = Small.createObject(“fragment-v4”, “home”, context);
若是没有经过rules指定类名,默认的类名是 包名.MainFragment
若是指定了类名,和前面的规则同样。
createObject的第一个参数目前仅支持”fragment”或者”fragment-v4”获取某个插件界面的Intent
有时候咱们不是直接打开界面,好比通知栏通知,咱们须要设置一个PendingIntent ,那这个时候须要的是一个Intent
此时可经过获取:
Intent intent = Small.getIntentOfUri("main",context)
- 获取调用时候的query信息:
//调用 Small.openUri("home?from=main", context); //参数获取; Uri uri = Small.getUri(this); if (uri != null) { String from = uri.getQueryParameter("from"); // Do stuff by `from' }
Small的核心类比较少,主要包含三类:
- Small:接口类,提供用户能使用的各种接口;
- Bundle: 表明插件,保存了插件的所有信息,控制了插件的load流程,以及lauch流程;它会调用各种BundleLauncher来干活;
- BundleLauncher:有多个子类,好比.app.,.lib.类的插件,对应的是ApkBundleLauncher,.web.*对应的就是WebBundleLauncher,其余对应的就是ActivityLauncher
插件相关的操做主要有load和lauch:
其中应用启动的时候要准备插件环境,进行的就是load操做,主要是解析插件信息并缓存起来,并将插件dex和资源添加到host;load完成才能进行其余插件操做
- 在load插件的时候,通常分两步:
preloadBundle,通常判断插件可否被加载,返回false就不须要进行加载了;这里咱们通常进行插件合法性检查
loadBundle(Bundle bundle):真正加载解析插件的各种信息并存入Bundle对象;
lauch指的是通常指加载插件界面,典型的就是调用Small.openUri打开插件界面;- lauch插件的时候也分两步:
prelaunchBundle(Bundle bundle):准备Bundle的一些必要信息:通常是生成bundle的intent信息,主要是要启动的类名的生成;(见ApkBundleLauncher)
launchBundle(Bundle bundle, Context context):判断插件是否能加载,能加载就启动acitivity;
3.1 load的流程
Small.setup --> Bundle.setupLaunchers (调用各BundleLauncher的setUp,其中ApkBundleLauncher的setUp会替换掉ActivityThread的mInstrumentation成员变量) -->Bundle.loadLaunchableBundles(解析bundle.json,并加载bundle) -->Bundle.loadManifest(读取了bundle.json) --> Bundle.loadBundles ( 开启异步线程进行加载) 异步线程里面的流程: -->Bundle.prepareForLaunch -->遍历ActivityLauncher,WebBundleLauncher,ApkBundleLauncher,调用它们的BundleLauncher.resolveBundle 方法寻找合适的BundleLauncher解析插件包; -->BundleLauncher.preloadBundle(主要是判断可否加载) -->BundleLauncher.loadBundle(正真的加载解析动做) 除了直接使用的三个BundleLauncher类(ActivityLauncher,WebBundleLauncher,ApkBundleLauncher),还有两个中间类,SoBundleLauncher 和AssetBundleLauncher, 其中SoBundleLauncher主要是提供了一个preloadBundle函数实现,里面实现了1 按支持的type与package名对比,快速判断此BundleLauncher可否解析此插件;2 校验插件签名是否合法来肯定是否要解析次插件; AssetBundleLauncher从SoBundleLauncher继承,干了WebBundleLauncher 要作的loadBundle大部分的活。
3.2 启动界面流程
Small.openUri --> Bundle.getLaunchableBundle -->Bundle.matchesRule(经过uri匹配合适的bundle) --> Bundle.launchFrom-->ApkBundleLauncher.launchBundle-->ApkBundleLauncher.prelaunchBundle-->BundleLauncher.launchBundle->Activity.startActivityForResult -->ApkBundleLauncher.InstrumentationWrapper.execStartActivity--》ApkBundleLauncher.InstrumentationWrapper.wrapIntent(将插件activity类保存在intent的category中,同时将intent的component里面的类替换为host 中声明的占位Activity,以经过ActivityManager的检查)--》ApkBundleLauncher.InstrumentationWrapper.newActivity(取出intent中的插件activity类,并实例化返回,用于接收生命周期回调) --》ApkBundleLauncher.InstrumentationWrapper.callActivityOnCreate(加入插件apk到AssertManager中,用于读取插件资源,应用插件Theme)
todo
加载插件优先级
目前load插件的时候是把全部插件都加载进来才算准备好,应该改成只加载必备插件就能够发准备好回调让主流程继续跑,其余插件在后台继续加载;优化经过PackageManager获取包内Activity信息
经实际测试,经过PackageManager获取包内Activity信息会耗时很大(700ms),可是加载插件又须要插件包里的activity的信息,拟经过在打包时提取相应信息放入文件,加载插件只须要读取文件解析就好了;插件合法性校验
目前的插件合法性是经过包的签名对比来实现的,也是调用的PackageManager,效率比较低.字符串资源超过128编译报错问题
目前发现插件处理资源的时候,若是工程中存在strings.xml中有字符串资源超过128字符就会报错,还有存在字符串样式的也会有问题;应该是做者对资源索引表中的StringPoll结构理解有误致使。
嵌入式企鹅圈原创团队由阿里、魅族、nvidia、龙芯、炬力、拓尔思等资深工程师组成。百分百原创,每周两篇,分享嵌入式、Linux、物联网、GPU、Android、自动驾驶等技术。欢迎扫码关注微信公众号:嵌入式企鹅圈,实时推送原创文章!