转载请注明出处:https://juejin.cn/post/6844903649504673805android
本文出自 容华谢后的博客git
VirtualApk GitHub地址github
VirtualAPK是滴滴在2017年6月开源的一款插件化框架,支持Android四大组件,以及几乎全部的Android特性,经过Gradle来构建插件,集成与构建十分便捷,目前已经应用在 滴滴出行 App上,兼容市面上几乎全部的Android设备。bash
VirtualAPK支持的Android版本:Android 4.0.3(API 15) - Android P(API P)markdown
什么是插件化?插件化的优点在哪里?app
在开发的过程当中,一个工程一般会被分为多个Module,用来区分不一样的业务模块,一个主Module下面有多个业务Module,也就是咱们常说的Library,发布的时候打成一个apk,全部的逻辑都在这一个apk中,当版本更新或者某一个Module出现问题时,只能是全量更新这个apk,若是过于频繁,用户确定会不爽,而后给你个差评。框架
插件化的出现正好解决了这一难题,主Module不变(宿主),业务Module被分红一个个单独的工程,再也不和主Module一块儿打包,而是分别打包成apk(插件),宿主启动后,动态的去加载插件。当某一个业务模块须要更新时,直接更新插件apk就能够了,全程在后台进行,不须要用户参与操做,但这样作对用户有必定风险,App经过审核后,有可能在后台加载一些非法插件,因此Google Play是禁止插件化App上线的,有海外市场的项目要注意下。ide
在插件化开发中,每一个人负责不一样的插件模块,插件之间彻底解耦,开发完成后,再进行集成测试。一个宿主能够拥有多个插件,一个插件也能够为多个宿主服务。举个栗子,同一个公司,A项目须要集成一个第三方登陆模块,B项目也须要,那么就能够把这个登陆模块作成通用插件,供两个项目同时使用。oop
注意:集成插件化框架的APP不能在Google Play发布。post
注意:目前VirtualApk支持的gradle插件最新版本为3.0.0,如有更新请参考官方Demo。
宿主
dependencies { classpath 'com.didi.virtualapk:gradle:0.9.8.4' } 复制代码
apply plugin: 'com.didi.virtualapk.host' 复制代码
dependencies { implementation 'com.didi.virtualapk:core:0.9.6' } 复制代码
public class VirtualAPKHostApplication extends Application { @Override protected void attachBaseContext(Context base) { super.attachBaseContext(base); // 初始化VirtualAPK PluginManager.getInstance(base).init(); } @Override public void onCreate() { super.onCreate(); // 加载存储根目录的插件apk,实际项目中按需保存 String pluginPath = Environment.getExternalStorageDirectory().getAbsolutePath().concat("/plugin.apk"); File plugin = new File(pluginPath); if (plugin.exists()) { try { PluginManager.getInstance(this).loadPlugin(plugin); } catch (Exception e) { e.printStackTrace(); } } } } 复制代码
不要忘了在清单文件中配置Application:
<application android:name=".VirtualAPKHostApplication"> </application> 复制代码
com.yl.plugin是插件工程的包名,com.yl.plugin.PluginActivity是插件工程中的类,插件工程的包名能够和宿主工程相同,可是相同包名下的类名不能相同,资源名称也不能相同。
public class MainActivity extends AppCompatActivity implements View.OnClickListener { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); findViewById(R.id.btn_start_plugin_activity).setOnClickListener(this); } @Override public void onClick(View view) { if (PluginManager.getInstance(this).getLoadedPlugin("com.yl.plugin") == null) { Toast.makeText(this, "Plugin is not loaded!", Toast.LENGTH_SHORT).show(); } else { Intent intent = new Intent(); intent.setClassName("com.yl.plugin", "com.yl.plugin.PluginActivity"); startActivity(intent); } } } 复制代码
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> 复制代码
-keep class com.didi.virtualapk.internal.VAInstrumentation { *; }
-keep class com.didi.virtualapk.internal.PluginContentResolver { *; }
-dontwarn com.didi.virtualapk.**
-dontwarn android.**
-keep class android.** { *; }
复制代码
插件
dependencies { classpath 'com.didi.virtualapk:gradle:0.9.8.4' } 复制代码
apply plugin: 'com.didi.virtualapk.plugin' 复制代码
须要在buil.gradle文件中的最后位置进行此配置
virtualApk { // 插件资源表中的packageId,须要确保不一样插件有不一样的packageId // 范围 0x1f - 0x7f packageId = 0x6f // 宿主工程application模块的路径,插件的构建须要依赖这个路径 // targetHost能够设置绝对路径或相对路径 // ../VirtualAPKHostDemo/app 表明 VirtualAPKDemo/VirtualAPKHostDemo/app targetHost = '../VirtualAPKHostDemo/app' // 默认为true,若是插件有引用宿主的类,那么这个选项可使得插件和宿主保持混淆一致 applyHostMapping = true } 复制代码
宿主
宿主的构建和正常apk的构建方式是相同的,能够经过Build > Generate Signed APK的方式,也能够经过下面的命令:
gradlew clean assembleRelease
复制代码
若是不想输入命令,还能够这样:
构建完成的apk在app > build > outputs > apk > release目录下。
插件
插件采用下面的命令进行构建:
gradlew clean assemblePlugin
复制代码
若是不想输入命令,还能够这样:
构建完成的apk在app > build > outputs > plugin > release目录下。
注意:由于assemblePlugin依赖于assembleRelease,因此插件包均是Release包,不支持debug模式的插件包。
到这里,宿主和插件就构建完成了,将插件apk拷贝至存储设备根目录,安装运行宿主apk,看下效果:
插件和宿主经过引用相同依赖库的方式来进行交互,好比,宿主工程中引用了A库,
dependencies { implementation 'com.x.x.x.A' } 复制代码
插件工程中若是也须要访问A库中的类和资源,那么能够在插件工程中一样引用A库,这样就能够和宿主工程共用A库了,插件构建的过程当中会自动将A库从apk中剔除。
以一个全局变量举例:
A库中有一个全局变量V = false,若是在插件中将此变量设置为true,那么在宿主中获取到的V值则为true。
如下内容来自官方WiKi:
1.暂不支持Activity的一些不经常使用特性(好比process、configChanges等属性),可是支持theme、launchMode和screenOrientation属性。
2.overridePendingTransition(int enterAnim, int exitAnim)这种形式的转场动画,动画资源不能使用插件的(可使用宿主或系统的)。
3.插件中弹通知,须要统一处理,走宿主的逻辑,通知中的资源文件不能使用插件的(可使用宿主或系统的)。
4.插件的Activity中不支持动态申请权限。
如下内容来自官方WiKi:
Activity,支持LaunchMode和theme
<style name="AppTheme.Transparent"> <item name="android:windowBackground">@android:color/transparent</item> <item name="android:windowIsTranslucent">true</item> </style> 复制代码
VirtualAPK对Intent的处理遵循Android规范,插件之间乃至插件和宿主之间,包名是区分它们的惟一标识。
在下面的例子中,假如宿主的包名是"com.didi.virtualapk",而后在插件中启动一个宿主Activity,下面分别是错误和正确的示范:
// 错误的用法,由于此时intent中的包名是插件的包名 Intent intent = new Intent(this, HostActivity.class); startActivity(intent); // 正确的用法 Intent intent = new Intent(); intent.setClassName("com.didi.virtualapk", "com.didi.virtualapk.HostActivity"); startActivity(intent); 复制代码
可是,若是想在插件中去访问插件的四大组件,那么就没有任何要求了,下面的代码会在插件Activity中尝试启动另外一个插件Activity:
// 正确的用法,由于此时intent中的包名是插件的包名
Intent intent = new Intent(this, PluginActivity.class);
startActivity(intent);
复制代码
Service,支持跨进程bind service
无约束
BroadcastReceiver
静态Receiver将被动态注册,当宿主中止运行时,外部广播将没法唤醒宿主;
因为动态注册的缘故,插件中的Receiver必须经过隐式调用来唤起。
ContentProvider,支持跨进程访问ContentProvider
1)分状况,插件调用本身的ContentProvider,若是须要用到call方法,那么须要将provider的uri放到bundle中,不然调用不生效;
Uri bookUri = Uri.parse("content://com.didi.virtualapk.demo.book.provider/book"); Bundle bundle = PluginContentResolver.getBundleForCall(bookUri); getContentResolver().call(bookUri, "testCall", null, bundle); 复制代码
2)插件调用宿主和外部的ContentProvider,无约束;
3)宿主调用插件的ContentProvider,须要将provider的uri包装一下,经过PluginContentResolver.wrapperUri方法,若是涉及到call方法,参考1)中所描述的;
String pkg = "com.didi.virtualapk.demo"; LoadedPlugin plugin = PluginManager.getInstance(this).getLoadedPlugin(pkg); Uri bookUri = Uri.parse("content://com.didi.virtualapk.demo.book.provider/book"); bookUri = PluginContentResolver.wrapperUri(plugin, bookUri); Cursor bookCursor = getContentResolver().query(bookUri, new String[]{"_id", "name"}, null, null, null); 复制代码
Fragment
推荐你们在Application启动的时候去加载插件,否则的话,请注意插件的加载时机。考虑一种状况,若是在一个较晚的时机去加载插件而且去访问插件中的资源,请注意当前的Context。好比在宿主Activity(MainActivity)中去加载插件,接着在MainActivity去访问插件中的资源(好比Fragment),须要作一下显示的hook,不然部分4.x的手机会出现资源找不到的状况。
String pkg = "com.didi.virtualapk.demo"; PluginUtil.hookActivityResources(MainActivity.this, pkg); 复制代码
so文件的加载
为了提高性能,VirtualAPK在加载一个插件时并不会主动去释放插件中的so,除非你在插件apk的manifest中显式地指定VA_IS_HAVE_LIB为true,以下所示:
<application android:name=".VAApplication" android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:supportsRtl="true" android:theme="@style/HostTheme"> <meta-data android:name="VA_IS_HAVE_LIB" android:value="true" /> ... </application> 复制代码
到这里VirtualAPK的基本用法就介绍完了,若有错误或者遗漏的地方能够给我留言评论,谢谢!
代码已上传至GitHub,欢迎Star、Fork!
GitHub地址:https://github.com/alidili/Demos/tree/master/VirtualAPKDemo
本文Demo的Apk下载地址:
宿主:https://github.com/alidili/Demos/raw/master/VirtualAPKDemo/host.apk
插件:https://github.com/alidili/Demos/raw/master/VirtualAPKDemo/plugin.apk
后续会有系列文章对VirtualAPK的源码进行分析和学习,敬请期待!