Markdown版本笔记 | 个人GitHub首页 | 个人博客 | 个人微信 | 个人邮箱 |
---|---|---|---|---|
MyAndroidBlogs | baiqiantao | baiqiantao | bqt20094 | baiqiantao@sina.com |
插件化 VirtualAPK 简介 体验 MDhtml
GitHub
Release notejava
VirtualAPK 框架接入
VirtualAPK四大组件源码分析
VirtualAPK 资源加载机制分析android
我的使用体验:功能强大,遍地是坑!git
VirtualAPK是滴滴出行
自研的一款优秀的插件化框架
。github
VirtualAPK is a powerful
yet lightweight
plugin framework for Android. It can dynamically
load and run an APK file (we call it LoadedPlugin
) seamlessly as an installed application. Developers can use any Class, Resources, Activity, Service, Receiver and Provider in LoadedPlugin
as if they are registered in app's manifest file.服务器
Supported Features微信
Feature | Detail |
---|---|
Supported components | Activity, Service, Receiver and Provider |
Manually register components in AndroidManifest.xml | No need |
Access host app classes and resources | Supported |
PendingIntent | Supported |
Supported Android features | Almost all features |
Compatibility | Almost all devices |
Building system | Gradle plugin |
Supported Android versions | API Level 15+ |
基本原理架构
VirtualAPK 对插件没有额外的约束,原生的apk便可做为插件。插件工程编译生成apk后,便可经过宿主App加载,每一个插件apk被加载后,都会在宿主中建立一个单独的LoadedPlugin
对象。经过这些LoadedPlugin对象,VirtualAPK就能够管理插件
并赋予插件新的意义
,使其能够像手机中安装过的App同样运行。app
一、在 project 的build.gradle
中添加依赖:框架
classpath 'com.android.tools.build:gradle:3.1.4' //这个版本不能修改,不然同步时就会失败 classpath 'com.didi.virtualapk:gradle:0.9.8.6' //2019-1-14最新版本
二、在 app 模块的build.gradle
中使用插件:
apply plugin: 'com.didi.virtualapk.host'
三、在 app 模块的build.gradle
中添加依赖:
implementation 'com.didi.virtualapk:core:0.9.8' //注意,宿主项目中须要包含全部插件项目中的support依赖,不然插件编译不经过(会提示要在宿主中添加依赖) //但对于其余依赖则没有此要求,例如能够在插件中依赖gson,而无需在宿主中依赖gson
四、在 Application 中初始化插件引擎:
@Override protected void attachBaseContext(Context context) { super.attachBaseContext(context); PluginManager.getInstance(context).init(); }
五、在合适的时机加载插件(APP退出后下次使用前仍须要加载):
PluginManager.getInstance(context).loadPlugin(apkFile); //当插件入口被调用后,插件的后续逻辑均不须要宿主干预,均走原生的Android流程。
六、判断是否已加载插件
LoadedPlugin loadedPlugin = PluginManager.getInstance(this).getLoadedPlugin(PKG); //包名 if (loadedPlugin == null) Toast.makeText(this, "还没有加载 " + PKG, Toast.LENGTH_SHORT).show(); else Toast.makeText(this, "已加载 " + loadedPlugin.getPackageName(), Toast.LENGTH_SHORT).show();
七、跳转到插件的Activity中
Intent intent = new Intent(); intent.setClassName(this, "com.didi.virtualapk.demo.aidl.BookManagerActivity"); intent.putExtra("name","包青天"); startActivity(intent);
注意,若是遇到以下提示,能够没必要关心,由于并无什么影响:
Configuration on demand is not supported by the current version of the Android Gradle plugin since you are using Gradle version 4.6 or above. Suggestion: disable configuration on demand by setting org.gradle.configureondemand=false in your gradle.properties file or use a Gradle version less than 4.6.
在VirtualAPK中,插件开发等同于原生Android开发,所以开发插件就和开发APP同样。
若是有使用nativeActivity
须要的用户请更新使用fix_native_activity
分支并修改依赖为CoreLibrary
,将来会合入主线。
构建环境建议直接使用Demo中的配置,插件构建强依赖构建环境
,请不要轻易尝试修改。
一、在 project 的build.gradle
中添加依赖:
classpath 'com.android.tools.build:gradle:3.1.4' //这个版本不能修改,不然同步时就会失败 classpath 'com.didi.virtualapk:gradle:0.9.8.6' //2019-1-14最新版本,和宿主中用的是同一个依赖
二、在 app 模块的gradle.properties
中(如没有请建立)添加以下配置:
android.useDexArchive=false
三、在 app 模块的build.gradle
中使用插件:
apply plugin: 'com.didi.virtualapk.plugin' virtualApk { packageId = 0x6f // 插件资源表中的packageId,须要确保不一样插件有不一样的packageId. targetHost = 'D:/code/PluginDemo/app' // 宿主工程application模块的路径,插件的构建须要依赖这个路径 applyHostMapping = true //默认为true,若是插件有引用宿主的类,那么这个选项可使得插件和宿主保持混淆一致 }
四、构建插件
请经过gradle assemblePlugin
来构建插件,assemblePlugin
依赖于assembleRelease
,这意味着:
release
包,不支持debug
模式的插件包productFlavors
,那么将会构建出多个插件包build
目录下打出来的包是很是小的
如下是正常打的包
其实主要区别在于:插件包是不包含宿主中已经存在
的aar依赖库
和res资源
的内容的,由于这些内容最终是用的宿主包中的。
必定要给插件设置一个资源别名
resourcePrefix
,以防止插件中误用到了宿主中已经存在的资源名,致使解析出错。
最典型的是默认的activity_main.xml
,若是插件和宿主中都有这个布局文件,那么打包后会删除插件中定义的activity_main.xml
,因此在运行时使用的是宿主中的activity_main.xml
,那么就极可能会致使调用findViewBuId时崩溃!
宿主若是更改后最好先build一次,由于生成插件包时须要用到宿主构建时生成的文件。
我经过AS建立了一个最最纯净的项目(默认包含kotlin),结果运行时发现一堆问题。
一、提示设置在app模块中的gradle.properties
中添加android.useDexArchive=false
A problem occurred configuring project ':app'. > Failed to notify project evaluation listener. > Can't using incremental dexing mode, please add 'android.useDexArchive=false' in gradle.properties of :app. > Cannot invoke method onProjectAfterEvaluate() on null object
咱们按照上述提示修改便可。
二、修改后再运行出现以下提示:
Failed to notify task execution listener. > The dependencies [ com.android.support.constraint:constraint-layout:1.1.3, com.android.support:support-fragment:28.0.0, //后面省略二十个 ] that will be used in the current plugin must be included in the host app first. Please add it in the host app as well.
意思是说,在插件项目中包含的库也必须在宿主项目存在。能够发现所有是 support
库,咱们只需统一宿主和插件的support
库版本就能够了,好比都用以下最新的设置:
implementation 'com.android.support:appcompat-v7:28.0.0' implementation 'com.android.support.constraint:constraint-layout:1.1.3'
三、沃日,配置为宿主的依赖后便开始出现各类问题,clean不行、build不行、手动删除build目录也不行,重启AS也不行
AAPT2 error: check logs for details
查看报错详细信息,说什么资源文件找不到什么问题,彻底是莫名其妙嘛,为何会有这个错呢?
网上搜了一通,找不到解决方案,只找到一种委曲求全的扯淡方案,那就是在project中的gradle.properties
中添加android.enableAapt2=false
。
四、添加完以后clean了一下,结果那个问题没有了,又出另外一个莫名其妙的错误:
Process 'command 'D:\software\android_sdk\build-tools\26.0.2\aapt.exe'' finished with non-zero exit value 1
报错的缘由可能和咱们上面的操做有关,由于看到有这么两行信息:
Deprecated Gradle features were used in this build, making it incompatible with Gradle 5.0. See https://docs.gradle.org/4.6/userguide/command_line_interface.html#sec:command_line_warnings
五、把全部设置都还原吧,彻底无法搞嘛!
我猜想可能与插件中采用了kotlin而宿主没有采用有关,因而在宿主中添加了kotlin相关的依赖,结果这货同步时又报一个错:
A problem occurred evaluating project ':CoreLibrary'. > Failed to apply plugin [id 'com.android.library'] > Configuration on demand is not supported by the current version of the Android Gradle plugin since you are using Gradle version 4.6 or above. Suggestion: disable configuration on demand by setting org.gradle.configureondemand=false in your gradle.properties file or use a Gradle version less than 4.6.
意思是说当前版本的Gradle插件不支持按需配置,日了狗了,什么鬼呀,搜索了一下,说能够这样禁用按需配置:
Project
和报错的CoreLibrary
模块中的gradle.properties
文件中设置 org.gradle.configureondemand=false
。配置完成后同步一下发现成功了。
六、继续构建插件
> Failed to notify project evaluation listener. > Can't find C:\Users\baiqi\Desktop\VirtualAPK-master\app\build\VAHost\versions.txt, please check up your host application need apply com.didi.virtualapk.host in build.gradle of host application > Cannot invoke method onProjectAfterEvaluate() on null object
这个错误提示就比较好处理了由于提示找不到versions.txt
,而这个文件是构建后由 VirtualAPK 产生的,咱们要先构建一次宿主app,才能够构建plugin(由于插件构建须要宿主的mapping以及其余信息),能够尝试使用build -> build apk(s)
直接构建宿主apk。
七、而后处理以后继续构建插件又遇到了最初遇到的问题,也就是提示我添加一堆 support
库,干脆我把插件中全部用到的 support
库所有去掉得了,看你还报不报错!
果不其然,又一个错误出来了:
Cannot get property 'id' on null object
这又是什么鬼?
网上搜了半天,有人说,这个问题是由于插件中布局文件没有id,在插件主activity的布局文件中增长一个view,声明一个id就能够了。
然而我按照上述方式设置以后并无任何卵用!
我经过如下指令
gradle assemblePlugin --stacktrace
拿到了以下错误信息:
* Exception is: java.lang.NullPointerException: Cannot get property 'id' on null object at com.didi.virtualapk.aapt.ArscEditor.slice(ArscEditor.groovy:66) ...
而后又去查看了ArscEditor.groovy
中相应的源码:
这意思大体是说,要确保有一个'attr',不然就会报异常(垃圾代码都不判空的吗?)
可是这是什么垃圾东西呢?我搞了老半天,这个问题始终解决不了!
八、坑实在是太多了,填不完了,从新开始集成吧。
此次我决定将demo中的配置所有迁移过来,而后再一点一点的更新到新版本,或添加新功能,看看到哪一步时会失败!
然而理想很丰满现实很骨感,仍旧是遍地错误!
九、从新开始集成!
此次我在网上仔细搜了一遍,发现不少人反映,Gradle的 build tools 版本问题会致使失败,即便使用 demo 中的配置也不行,因此这一次我使用网上说的版本吧。
classpath 'com.android.tools.build:gradle:2.1.3'
gradle-wrapper
distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-all.zip
然而发现这TMD全是扯淡,可能旧版本须要这么配置,然而新版本并不须要这么配置。
十、从新开始集成!
这一次决定在原demo基础上修改,此次终于成功了。具体配置就是上面所述的。
一、功能完备
不须要在宿主manifest中预注册
,每一个组件都有完整的生命周期。
theme
和LaunchMode
,支持透明主题;start
、stop
、bind
和unbind
,并支持跨进程bind插件中的Service;CRUD
和call
方法等,支持跨进程访问插件中的Provider。style
,支持动画;PendingIntent
以及和其相关的Alarm
、Notification
和AppWidget
;Application
以及插件manifest中的meta-data
;so
;二、优秀的兼容性
小米、Vivo、Nubia
等,对未知机型采用自适应适配方案(意思就是说,只象征性的进行了一些适配);AMS
和IContentProvider
,hook过程作了充分的兼容性适配;三、入侵性极低
经过Gradle插件来完成插件的构建
,整个过程对开发者透明。以下是VirtualAPK和主流的插件化框架之间的对比。
特性 | DynamicLoadApk | DynamicAPK | Small | DroidPlugin | VirtualAPK |
---|---|---|---|---|---|
支持四大组件 | 只支持Activity | 只支持Activity | 只支持Activity | 全支持 | 全支持 |
组件无需在宿主中预注册 | √ | × | √ | √ | √ |
插件能够依赖宿主 | √ | √ | √ | × | √ |
支持PendingIntent | × | × | × | √ | √ |
Android特性支持 | 大部分 | 大部分 | 大部分 | 几乎所有 | 几乎所有 |
兼容性适配 | 通常 | 通常 | 中等 | 高 | 高 |
插件构建 | 无 | 部署aapt | Gradle插件 | 无 | Gradle插件 |
已经有那么多优秀的开源的插件化框架,滴滴为何要从新造一个轮子呢?
大部分开源框架所支持的功能还不够全面
除了DroidPlugin,大部分都只支持Activity。
兼容性问题严重,大部分开源方案不够健壮
因为国内Rom尝试深度定制Android系统,这致使插件框架的兼容性问题特别多,而目前已有的开源方案中,除了DroidPlugin,其余方案对兼容性问题的适配程度是不足的。
已有的开源方案不适合滴滴的业务场景
虽说DroidPlugin从功能的完整性和兼容性上来看,是一款很是完善的插件框架,然而它的使用场景和滴滴的业务不符。
DroidPlugin侧重于加载第三方独立插件,而且插件不能访问宿主的代码和资源
。而在滴滴打车中,其余业务模块均须要宿主提供的订单、定位、帐号等数据,所以插件不可能和宿主没有交互。
其实在大部分产品中,一个业务模块实际上并不能垂手可得地独立出来,它们每每都会和宿主有交互,在这种状况下,DroidPlugin就有点力不从心了。
基于上述几点,咱们只能从新造一个轮子,它不但功能全面、兼容性好,还必须可以适用于有耦合的业务插件
,这就是VirtualAPK存在的意义。
在加载耦合插件
方面,VirtualAPK是开源方案的首选,推荐你们使用。
抽象地说
通俗易懂地说
基本原理
合并宿主和插件的ClassLoader
。须要注意的是,插件中的类不能够和宿主重复合并插件和宿主的资源
。重设插件资源的packageId,将插件资源和宿主资源合并去除插件包对宿主的引用
。构建时经过Gradle插件去除插件对宿主的代码以及资源的引用四大组件的实现原理
AMS
,拦截service相关的请求,将其中转给Service Runtime
去处理,Service Runtime
会接管系统的全部操做;IContentProvider
,拦截provider相关的请求,将其中转给Provider Runtime
去处理,Provider Runtime
会接管系统的全部操做。以下是VirtualAPK的总体架构图,更详细的内容请你们阅读源码。
插件如何和宿主交互
经过compile相同aar的方式来交互。
好比,宿主工程中compile了以下aar:
compile 'com.didi.foundation:sdk:1.2.0' compile 'com.didi.virtualapk:core:[newest version]' compile 'com.android.support:appcompat-v7:22.2.0'
可是插件工程须要访问宿主sdk中的类和资源,那么能够在插件工程中一样compile sdk的aar,以下:
compile 'com.didi.foundation:sdk:1.2.0'
这样一来,插件工程就能够正常地引用sdk了。而且,插件构建的时候会自动将这个aar从apk中剔除
。
上述就是VirtualAPK中插件和宿主通讯的基本方式。
然而,VirtualAPK仍然有一些小小的约束,以下注意事项,请务必仔细阅读。
process
、configChanges
等属性,可是支持theme
、launchMode
和screenOrientation
属性overridePendingTransition(int enterAnim, int exitAnim)
这种形式的转场动画,动画资源不能使用插件的(可使用宿主或系统的)动态申请权限
支持LaunchMode和theme
android:windowIsTranslucent
属性;<style name="AppTheme.Transparent"> <item name="android:windowBackground">@android:color/transparent</item> <item name="android:windowIsTranslucent">true</item> </style>
包名
。VirtualAPK对Intent的处理遵循Android规范,插件之间乃至插件和宿主之间,包名是区分它们的惟一标识。
为了兼容宿主与插件之间的activity互调的场景,咱们弱化了插件的包名
,在插件中经过context.getPackageName()
取到的仍然是宿主的包名。所以在下面的例子中,假如宿主的包名是com.didi.virtualapk
,而后在插件中启动一个宿主Activity,仍然可正确的调用:
// 兼容方式 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);
支持跨进程bind service
无约束
静态Receiver将被动态注册
,当宿主中止运行时,外部广播将没法唤醒宿主;隐式调用
来唤起。支持跨进程访问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);
插件调用宿主和外部的ContentProvider,无约束;
宿主调用插件的ContentProvider,须要将provider
的uri
包装一下,经过PluginContentResolver.wrapperUri
方法,若是涉及到call
方法,参考上面所描述的;
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);
推荐你们在Application启动的时候去加载插件,否则的话,请注意插件的加载时机。
考虑一种状况,若是在一个较晚的时机去加载插件而且去访问插件中的资源,请注意当前的Context。好比在宿主Activity(MainActivity)中去加载插件,接着在MainActivity去访问插件中的资源(好比Fragment),须要作一下显示的hook,不然部分4.x的手机会出现资源找不到的状况。
String pkg = "com.didi.virtualapk.demo"; PluginUtil.hookActivityResources(MainActivity.this, pkg);
为了提高性能,VirtualAPK 在加载一个插件时并不会主动去释放插件中的so,除非你在插件apk的manifest中显式地指定VA_IS_HAVE_LIB
为true,以下所示:
<meta-data android:name="VA_IS_HAVE_LIB" android:value="true" />
为了通用性,在armeabi路径下放置对应的so文件便可知足需求。若是考虑性能请作好各类so文件的适配。
The directory of host application doesn't exist!
错误分析:宿主工程的application模块的路径不存在,通常是指路径配错了
解决方式:检测targetHost
这个路径是否正确,相对路径或者绝对路径都行
java.lang.ArrayIndexOutOfBoundsException: 2
错误分析:请检查dependencies
中aar的依赖方式
解决方式:按以下建议修改
dependencies { √ compile 'com.didi.virtualapk:core:0.9.0' √ compile project (":CoreLibrary") // group和version字段必须有 √ compile(group:'test', name:'CoreLibrary-release', version:'0.1', ext:'aar') × releaseCompile 'com.didi.virtualapk:core:0.9.0' × compile(name:'CoreLibrary-release', ext:'aar') }
编译插件时空指针:Cannot invoke method getAt() on null object
解决方式:请确保插件中至少有一个本身的资源
插件的activity能正常打开,可是插件中的资源读取失败
解决方式:依次检查:
packageId
的取值范围(在下面)是否正确com.android.support
包在宿主都有显式依赖,而且版本和宿主保持一致id
是否和宿主的资源重名,重名资源会在构建插件包时被自动剔除Failed to notify project evaluation listener
解决方式:修改Gradle
和build tools
的版本
构建环境建议:
Gradle 2.14.1
com.android.tools.build 2.1.3
java.lang.IllegalStateException: You need to use a Theme.AppCompat theme (or descendant) with this activity
解决方式:构建插件请使用gradle assemblePlugin
,而不能直接经过AndroidStudio run出来一个插件apk。
关于Android M及以上版本动态申请权限问题
从Android 6.0开始,系统采用了新的权限机制,为了保证插件的加载,请保证APP具备SD卡的访问权限。若是你的app没有在android 6.0上作足够的测试,请不要设置targetSdk为23。
注意:目前暂时不支持在插件中动态申请权限。
插件的gradle文件中对于packageID设置有什么范围吗?
不一样apk的packageId值不能相同
,因此插件的packageId范围是介于系统应用(0x01,0x02,...具体占用多少值视系统而定)和宿主(0x7F)之间。生成的插件apk中会发现有些png图片是黑色的,大小为0,这是怎么回事?
为了减少包的大小对于那些没有引用的资源进行压缩了,在gradle中配置shrinkResources true
便可,位置和minifyEnabled true
一块儿。
关于Activity的configchanges
由于configChanges
的选项组合太多,坑位比较多,这个暂时不许备支持,由于在平常使用的时候就横竖经常使用。
iR是什么意思?
install Release,gradle中的一种小驼峰
命名的缩写方式。若是发现冲突,能够经过assembleRelease来实现构建宿主工程。
0.9.1版本的VirtualAPK构建插件在构建插件的时候assets目录下的文件会被删除
这是0.9.1版本的bug,更高版本已经修复,请更新版本。
宿主和插件同时依赖公共的本地jar文件或library module,支持在构建插件时自动剔除吗?
不支持。
构建插件的依赖自动剔除功能仅支持内容稳定不变,路径稳定的资源
,而本地的jar
或其它资源的路径和内容都是可变动的,所以没法直接自动剔除,若是须要剔除,请将资源打包导出部署到maven
或其它依赖管理服务器。若是资源不可公开发布,可在内网部署私有maven服务
。
2019-1-13