插件化庫VirtualAPK詳解

前篇文章《Android組件化和插件化開發(fā)》主要介紹了Android組件化和插件化的架構(gòu)特點、兩者的對比分析以及推薦了學(xué)習(xí)組件化的相關(guān)文章,本編主要介紹下目前插件化開源庫的使用情況,以及著重介紹下VirtualAPK庫,供大家參考。

插件化的技術(shù)背景

插件化主要就是利用動態(tài)加載技術(shù)

通過服務(wù)器配置一些參數(shù),Android APP獲取這些參數(shù)再做出相應(yīng)的邏輯,這是常有的事,比如現(xiàn)在大部分APP都有一個啟動頁面,如果到了一些重要的節(jié)日,APP的服務(wù)器會配置一些與時節(jié)相關(guān)的圖片,APP啟動時候再把原有的啟動圖換成這些新的圖片,這樣就能提高用戶的體驗了。

再則,早期個人開發(fā)者在安卓市場上發(fā)布應(yīng)用的時候,如果應(yīng)用里包含有廣告,那么有可能會審核不通過,那么就通過在服務(wù)器配置一個開關(guān),審核應(yīng)用的時候先把開關(guān)關(guān)閉,這樣應(yīng)用就不會顯示廣告了;安卓市場審核通過后,再把服務(wù)器的廣告開關(guān)給打開,以這樣的手段規(guī)避市場的審核。所以現(xiàn)在安卓市場開始掃描APK里面的Manifest甚至dex文件,查看開發(fā)者的APK包里是否有廣告的代碼,如果有就有可能審核不通過。通過服務(wù)器怕配置開關(guān)參數(shù)的方法行不通了,開發(fā)者們開始想,“既然這樣,能不能先不要在APK寫廣告的代碼,在用戶運行APP的時候,再從服務(wù)器下載廣告的代碼運行,再實現(xiàn)廣告呢?”。答案是肯定的,這就是動態(tài)加載。

在程序運行的時候,加載一些程序自身原本不存在的可執(zhí)行文件并運行這些文件里的代碼邏輯。

看起來就像是應(yīng)用從服務(wù)器下載了一些代碼,然后再執(zhí)行這些代碼!

使用動態(tài)加載技術(shù),一般來說會使得Android開發(fā)工作變得更加復(fù)雜,這種開發(fā)方式不是官方推薦的,不是目前主流的Android開發(fā)方式,Github 和 StackOverflow 上面外國的開發(fā)者也對此不是很感興趣,外國相關(guān)的教程更是少得可憐,目前只有在大天朝才有比較深入的研究和應(yīng)用,特別是一些SDK組件項目和 BAT家族的項目上,Github上的相關(guān)開源項目基本是國人在維護。

動態(tài)加載的大致過程就是

  • 把可執(zhí)行文件(.so/dex/jar/apk)拷貝到應(yīng)用APP內(nèi)部存儲;
  • 加載可執(zhí)行文件;
  • 調(diào)用具體的方法執(zhí)行業(yè)務(wù)邏輯;

幾個主流插件化開源框架的對比

特性 DynamicLoadApk DynamicAPK Small DroidPlugin VirtualAPK
支持四大組件 只支持Activity 只支持Activity 只支持Activity 全支持 全支持
組件無需在宿主 manifest中預(yù)注冊 ×
插件依賴宿主 ×
支持PendingIntent × × ×
Android特性支持 大部分 大部分 大部分 幾乎全部 幾乎全部
兼容性適配 一般 一般 中等
插件構(gòu)建 部署aapt Gradle插件 Gradle插件

阿里的atlas:Atlas 是伴隨著手機淘寶不斷發(fā)展而衍生出來的一個運行于 Android 系統(tǒng)上的插件化框架,也可以叫動態(tài)組件化框架,主要提供了解耦化、組件化、動態(tài)性的支持。是目前比較成熟的方案,功能強大,但相對的,使用和集成的難度也比較大。

騰訊的Shadow:Shadow是一個騰訊自主研發(fā)的Android插件框架,并且一直在維護中,但使用和集成難道稍大,有興趣的可以研究下。

VirtualAPK的接入

1.1、宿主工程引入VirtualApk

  • 在項目Project的build.gradle中添加依賴
dependencies {
    classpath 'com.didi.virtualapk:gradle:0.9.8.6'
}
  • 在宿主app的build.gradle中引入VirtualApk的host插件
apply plugin: 'com.didi.virtualapk.host'
  • 在app中添加依賴
dependencies {
    implementation 'com.didi.virtualapk:core:0.9.8'
}
  • 在Application中完成PluginManager的初始化
public class VirtualApplication extends Application {
   @Override
   protected void attachBaseContext(Context base) {
      super.attachBaseContext(base);
      PluginManager.getInstance(base).init();
   }
}

1.2 插件開發(fā)

插件APK的配置

  • 在插件project中配置
classpath 'com.didi.virtualapk:gradle:0.9.8.6'
  • 在插件app的build.gradle中引入plugin插件
apply plugin: 'com.didi.virtualapk.plugin'
  • 配置插件信息和版本
virtualApk{
    // 插件資源表中的packageId,需要確保不同插件有不同的packageId
    // 范圍 0x1f - 0x7f
    packageId = 0x6f
    // 宿主工程application模塊的路徑,插件的構(gòu)建需要依賴這個路徑
    // targetHost可以設(shè)置絕對路徑或相對路徑
    targetHost = '../../../VirtualAPkDemo/app'
    // 默認(rèn)為true,如果插件有引用宿主的類,那么這個選項可以使得插件和宿主保持混淆一致
    //這個標(biāo)志會在加載插件時起作用
    applyHostMapping = true
}
  • 設(shè)置簽名(Virtual僅支持Release,host項目和plugin項目簽名一致)
signingConfigs {
    release {
        storeFile file('/Users/wuliangliang/AndroidSubjectStudyProject/PluginProject/VirtualAPkDemo/keystore/keystore')
        storePassword '123456'
        keyAlias = 'key'
        keyPassword '123456'
    }
}
buildTypes {
    release {
        signingConfig signingConfigs.release
    }
}

插件的開發(fā)

在VirtualAPK中,插件開發(fā)等同于原生Android開發(fā),因此開發(fā)插件就和開發(fā)APP一樣。

插件和宿主的交互

通過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了,類似宿主和插件共用了一個功能庫來進行交流。并且,插件構(gòu)建的時候會自動將這個aar從apk中剔除。上述就是VirtualAPK中插件和宿主通信的基本方式。

插件中四大組件的已知約束
  • 透明Activity,不能有啟動模式,并且主題中必須含有android:windowIsTranslucent屬性;
<style name="AppTheme.Transparent">
    <item name="android:windowBackground">@android:color/transparent</item>
    <item name="android:windowIsTranslucent">true</item>
</style>
  • 插件中調(diào)用宿主的四大組件,請注意Intent中的包名

    VirtualAPK對Intent的處理遵循Android規(guī)范,插件之間乃至插件和宿主之間,包名是區(qū)分它們的唯一標(biāo)識。
    為了兼容宿主與插件之間的activity互調(diào)的場景,我們?nèi)趸瞬寮陌?,在插件中通過context.getPackageName()取到的仍然是宿主的包名。因此在下面的例子中,假如宿主的包名是"com.didi.virtualapk",然后在插件中啟動一個宿主Activity,仍然可正確的調(diào)用:

// 兼容方式
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);

BroadcastReceiver

  • 靜態(tài)Receiver將被動態(tài)注冊,當(dāng)宿主停止運行時,外部廣播將無法喚醒宿主;
  • 由于動態(tài)注冊的緣故,插件中的Receiver必須通過隱式調(diào)用來喚起。

ContentProvider,支持跨進程訪問ContentProvider

  1. 分情況,插件調(diào)用自己的ContentProvider,如果需要用到call方法,那么需要將provider的uri放到bundle中,否則調(diào)用不生效;
Uri bookUri = Uri.parse("content://com.didi.virtualapk.demo.book.provider/book");
Bundle bundle = PluginContentResolver.getBundleForCall(bookUri);
getContentResolver().call(bookUri, "testCall", null, bundle);
  1. 插件調(diào)用宿主和外部的ContentProvider,無約束;

  2. 宿主調(diào)用插件的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啟動的時候去加載插件,不然的話,請注意插件的加載時機。 考慮一種情況,如果在一個較晚的時機去加載插件并且去訪問插件中的資源,請注意當(dāng)前的Context。比如在宿主Activity(MainActivity)中去加載插件,接著在MainActivity去訪問插件中的資源(比如Fragment),需要做一下顯示的hook,否則部分4.x的手機會出現(xiàn)資源找不到的情況。

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>

為了通用性,在armeabi路徑下放置對應(yīng)的so文件即可滿足需求。如果考慮性能請做好各種so文件的適配。

1.3、執(zhí)行生成插件Plugin

  • 執(zhí)行assemablePlugin
  • 產(chǎn)生Plugin文件將插件Plugin安裝到手機中
adb push ./app/build/outputs/plugin/release/com.alex.kotlin.virtualplugin_20190729172001.apk  /sdcard/plugin_test.apk

注意問題

  1. 要先構(gòu)建一次宿主app,才可以構(gòu)建plugin,否則異常
  2. 插件布局文件中要設(shè)置資源的ID,否則異常:Cannot get property 'id' on null objectplugin
  3. 增加 gradle.properties 文件并配置android.useDexArchive=false,否則異常

1.4、在宿主程序中使用插件Plugin

在宿主App中加載插件apk

private void loadApk() {
   File apkFile = new File(Environment.getExternalStorageDirectory(), "Test.apk");
   if (apkFile.exists()) {
      try {
         PluginManager.getInstance(this).loadPlugin(apkFile);
      } catch (Exception e) {
         e.printStackTrace();
      }
   }
}

在插件下載或安裝到設(shè)備后,獲取插件的文件,調(diào)用PluginManager.loadPlugin()加載插件,PluginManager會完成所有的代碼解析和資源加載;2. 執(zhí)行界面跳轉(zhuǎn)至插件中

final String pkg = "com.alex.kotlin.virtualplugin”; //插件Plugin的包名
Intent intent = new Intent();
intent.setClassName(pkg, "com.alex.kotlin.virtualplugin.MainPluginActivity”);  //目標(biāo)Activity的全路徑
startActivity(intent);

使用VirtualAPK需要注意的問題

集成開發(fā)問題

  1. 注意宿主工程的application模塊的路徑是否正確
virtualApk {
    packageId = 0x6f
    targetHost = '../../VirtualAPK/app' // 檢測這個路徑是否正確,相對路徑或者絕對路徑都行
    applyHostMapping = true
}
  1. packageId:
  • 運行時獲取資源需要通過packageId來映射apk中的資源文件,不同apk的packageId值不能相同,所以插件的packageId范圍是介于系統(tǒng)應(yīng)用(0x01,0x02,...具體占用多少值視系統(tǒng)而定)和宿主(0x7F)之間。
  • 多個插件的packageId和packageName一樣,在宿主中需要確保是唯一的。
  1. com.android.tools.build:gradle“最高支持到3.1.4,即在virtualApk工程中最高只能用classpath 'com.android.tools.build:gradle:3.1.4'
  2. 目前是不支持androidx庫的
  3. 插件中的buildToolsVersion似乎只能到27.0.3,其他的會出錯
  4. 宿主中和插件中的資源文件不能重復(fù)(如布局文件,資源id等)
  5. 插件依賴的所有com.android.support包在宿主都有顯式依賴,并且版本和宿主保持一致
  6. 宿主和插件同時依賴公共的本地jar文件或library module,在構(gòu)建插件時并不會自動剔除:構(gòu)建插件的依賴自動剔除功能僅支持內(nèi)容穩(wěn)定不變,路徑穩(wěn)定的資源,而本地的jar或其它資源的路徑和內(nèi)容都是可變更的,因此無法直接自動剔除,如果需要剔除,請將資源打包導(dǎo)出部署到maven或其它依賴管理服務(wù)器。如果資源不可公開發(fā)布,可在內(nèi)網(wǎng)部署私有maven服務(wù)。

目前暫不支持的特性

  • 暫不支持Activity的一些不常用特性(比如process、configChanges等屬性),但是支持theme、launchMode和screenOrientation屬性;
  • overridePendingTransition(int enterAnim, int exitAnim)這種形式的轉(zhuǎn)場動畫,動畫資源不能使用插件的(可以使用宿主或系統(tǒng)的);
  • 插件中彈通知,需要統(tǒng)一處理,走宿主的邏輯,通知中的資源文件不能使用插件的(可以使用宿主或系統(tǒng)的)。
  • 插件的Activity中不支持動態(tài)申請權(quán)限。
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容