Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> 淺談Android Small插件化框架源碼

淺談Android Small插件化框架源碼

編輯:關於Android編程

Android Small插件化框架源碼分析

目錄

概述
Small如何使用
插件加載流程
待改進的地方

一、概述

Small是一個寫得非常簡潔的插件化框架,工程源碼位置:https://github.com/wequick/Small
插件化的方案,說到底要解決的核心問題只有三個:

1.1 插件類的加載

這個問題的解決和其它插件化框架的解決方法差不多。Android的類是由DexClassLoader加載的,通過反射可以將插件包動態加載進去。Small的gradle插件生成的是.so包,在初始化的時候會通過.so文件生成.zip文件,再由.zip文件生成一個dex元素,反射添加到宿主類加載器的dexPathList裡。

1.2 插件資源的處理

這裡各插件化框架解決辦法一般有兩種想法:一是插件間不共享資源訪問,辦法就是每個插件生成一個AssertManager來訪問它自己的資源,這樣就不會存在資源id沖突的問題;另一種是大家都共用一個AssetManager,這樣插件的資源是共享的,可以相互訪問,但是要解決資源id沖突的問題。Small采用的是後者,通過修改aapt的生成產物解決了資源id沖突問題,由於共享資源訪問,可以做到極小或者根本沒有資源冗余,從而減小插件包的大小;

1.3 Activity注冊和生命周期問題

*大部分插件化框架解決辦法都是采用在宿主工程裡預先注冊Activity占坑,然後通過占坑Activity將生命周期回調傳回給插件Activity的方式。這裡Small處理的比較有特色,通過替換 ActivityThread 裡的mInstrumentation,在Instrumentation的newActivty實現裡面實例化了插件Activity,通過較小改動就能完全解決生命周期回調的問題。

Small的功能模塊主要有:
gradle-small插件:Small中的一個gradle自定義插件,用於打包組件;
small library:提供給用戶使用的Android Library,主要提供插件加載,解析等功能;

二、使用Small

2.1工程命名
首先Small對工程名稱如下要求:

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中聲明,格式如下:

{
  "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)

3.3 插件打包過程

todo

四、需要改進的點

加載插件優先級
目前load插件的時候是把所有插件都加載進來才算准備好,應該改為只加載必備插件就可以發准備好回調讓主流程繼續跑,其他插件在後台繼續加載;

優化通過PackageManager獲取包內Activity信息
經實際測試,通過PackageManager獲取包內Activity信息會耗時很大(700ms),但是加載插件又需要插件包裡的activity的信息,擬通過在打包時提取相應信息放入文件,加載插件只需要讀取文件解析就行了;

插件合法性校驗
目前的插件合法性是通過包的簽名對比來實現的,也是調用的PackageManager,效率比較低.

字符串資源超過128編譯報錯問題
目前發現插件處理資源的時候,如果工程中存在strings.xml中有字符串資源超過128字符就會報錯,還有存在字符串樣式的也會有問題;應該是作者對資源索引表中的StringPoll結構理解有誤導致。

嵌入式企鵝圈原創團隊由阿裡、魅族、nvidia、龍芯、炬力、拓爾思等資深工程師組成。百分百原創,每周兩篇,分享嵌入式、Linux、物聯網、GPU、Android、自動駕駛等技術。

   
  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved