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