插件化——插樁式實現(xiàn)Activity跳轉(zhuǎn)

代碼(已適配android10)已上傳github中抹剩,親測可用撑帖,對你有用的話,記得star澳眷,謝謝

先上效果圖

ezgif.com-video-to-gif_gaitubao_598x1023.gif

前言

關(guān)于插件化網(wǎng)上比比皆是胡嘿,但很遺憾之前開發(fā)一直沒有真正遇到過插件化的公司項目。由于疫情原因換了家新公司并且提前轉(zhuǎn)正钳踊,這個項目也是我們用組件化從0開始重構(gòu)衷敌,目前已開發(fā)完成。最近領(lǐng)導(dǎo)說apk包體積太大了拓瞪,而且里面有個模塊缴罗,可以根據(jù)接口類型動態(tài)加載,所以這篇文章誕生了祭埂。

插件化概念

將整個app拆分成很多模塊面氓,每個模塊都是一個apk,最終打包的時候?qū)⑺拗鱝pk和插件apk分開打包蛆橡,插件apk通過動態(tài)下發(fā)到宿主apk侧但,實現(xiàn)了動態(tài)加載插件并大大減少了包體積。

插件化優(yōu)點

  • 提高編譯速度:開發(fā)過程中航罗,每個模塊都是獨立開發(fā)的禀横,編譯的時候每次運行不需要都編譯所有的業(yè)務(wù)邏輯代碼,所以會適當(dāng)?shù)奶岣呶覀兊拈_發(fā)速度粥血;

  • 業(yè)務(wù)模塊完全解耦:每個業(yè)務(wù)都是完全獨立的柏锄,這樣開發(fā)過程中每個模塊的功能改變和其他模塊沒有任何關(guān)系,甚至可以隨意的去掉某一部分功能复亏;

  • 利于團隊開發(fā):插件開發(fā)是團隊開發(fā)中用的最多的一種開發(fā)模式趾娃,可以更加的去分工,每個組只需要負責(zé)自己的功能缔御,減少溝通成本提高開發(fā)效率抬闷;

  • 動態(tài)更新插件,按需下載模塊:對于一些不怎么常用的功能,可以讓用戶按需下載模塊笤成,從而減少工程的大小评架,讓用戶在下載的時候能夠節(jié)省流量以及等待時間,而且功能升級的時候可以不更新主應(yīng)用只更新插件炕泳;

  • 解決android 655535問題纵诞。

插件化誕生

舉個美團的例子,你就懂了


image.png
image.png

美食頁面有那么多應(yīng)用培遵,如果單純的用webview實現(xiàn)那里面的支付浙芙,地圖和圖片瀏覽等有點不切實際,或者全部寫一個app里面籽腕,那包體積少說也有200M嗡呼,可是你去應(yīng)用市場看到,也才80M左右皇耗,這時插件化出場了


image.png

實現(xiàn)插件化的方式:

  • 插樁式(本篇文章講的就是這種方式)
  • Hook方式南窗,這個到時也會學(xué)習(xí)一個Hook的效果。
  • 反射廊宪,但是在Android9.0中有很多反射是用不了了,所以這種基本上不會用了女轿。
插樁式原理

一圖勝千言箭启,看圖


結(jié)構(gòu)流程圖.png

上圖右邊美團外賣是以一個單獨的apk(可以這樣理解:一個apk就是一個插件)存在的,宿主App(美團)想要打開插件(美團外賣)中的某一個Activity蛉迹,但是美團外賣這個插件很顯然是沒有上下文對象的【原因:因為此插件沒有安裝到手機上】傅寡,要想啟動Activity必須要解決上下文這個東西,所以此時就需要在宿主APP中插一個樁北救,聲明一個代理的Activity荐操,如下:

流程圖.png

此時ProxyActivity是一個空殼,可是沒有顯示插件的東西呀珍策,怎么辦托启?其實是這樣的:
流程圖.png

如何將一個未安裝的插件apk的Activity能顯示在這個代理ProxyActivity中呢?其實要想插件Activity顯示出來肯定得要調(diào)用它里面的生命周期方法攘宙,而對于插件而言就是將自己Activity中的各種生命周期方法通過接口對外暴露給宿主的ProxyActivity屯耸,然后插件Activity中需要的Context則是借用ProxyActivity,這樣最終就能達到我們調(diào)用的目的蹭劈,目的達成最終插件化也就這實現(xiàn)了疗绣。
所以實現(xiàn)宿主Activity跳轉(zhuǎn)插件化Activity,需要2樣?xùn)|西:
①暴露代理Activity的生命周期給插件化铺韧;
②提供上下文給插件化

開干

新建項目

宿主是app多矮,插件是orderfood,這里注意orderfood也是application

image.png

宿主app插件orderfood新建完成哈打,此時需要一個接口來暴露插件orderfood Activity的生命周期塔逃,所以還需要定義一個宿主app插件orderfood之間公共的library讯壶,里面會定義各種公共接口,這里起名library為:lib_plugin 患雏,如下:

image.png

添加依賴

然后添加對它的依賴


宿主app的依賴.png
插件app的依賴.png

①暴露生命周期

然后在library中定義Activity生命周期的公共接口鹏溯,如下:


image.png

然后插件Activity得要將其生命周期方法對外暴露,所以需要實現(xiàn)這個接口:


image.png

但是如圖并未對接口中的方法進行重寫淹仑,因為這樣寫是不合適的丙挽,插件中肯定會有n個Activity的,所以需要抽取一個BaseActivity出來匀借,然后再由它來實現(xiàn)抽象接口才靠譜颜阐,所以:
image.png

image.png

image.png

②提供上下文給插件化app
上面提到過,插件是不會裝在手機上的apk吓肋,那么插件中的Activity是沒有上下文的

image.png

所以需要在BaseActivity中來先重寫一個這個方法
image.png

我們已經(jīng)在插件化把相應(yīng)的方法進行重寫凳怨,此時需要把代理的上下文傳給插件app,我們在宿主App新建一個代理Activity是鬼,并在清單文件注冊:
image.png

image.png

接下來就是把代理Activity上下文傳給插件化Activity中去肤舞,也就是如何調(diào)用BaseActivity中的attach()方法,這里就要用到反射了均蜜,這里需要知道要跳轉(zhuǎn)插件Activity的全類名李剖,所以這里通過Intent的參數(shù)傳進來,如下:
image.png

然后通過反射來獲取到要跳轉(zhuǎn)插件Activity的對象囤耳,由于插件的所有Activity都繼承了BaseActivity了篙顺,而BaseActivity又實現(xiàn)了公共模塊的PluginInterface接口,所以最終就可以調(diào)用attach方法充择,如下:
image.png

所以代理Activity中改成如下:
image.png

我只重寫了onStart() 和 onReusme()德玫,剩余的方法是一樣的。

在宿主中加載插件

對于加載插件一般有2種:內(nèi)置和外置椎麦。
內(nèi)置:就是插件的apk放在assert文件目錄中
外置:從服務(wù)器進行下載到手機sd卡上
不管哪種方式宰僧,都需要將插件的類加載進來才行,所以對宿主app的進行修改:

image.png

image.png

接下來就是加載插件了观挎,新建一個插件管理器

 public class PluginManager {
      private Context mContext;//插件的資源對象
      private Resources pluginResource;

      //插件的類加載器
      private DexClassLoader dexClassLoader;

      //插件的包信息類
      private PackageInfo packageInfo;

      private static PluginManager pluginManager = new PluginManager();

      private PluginManager() {
      }

      public static PluginManager getInstance() {
          return pluginManager;
      }

      public void setContext(Context context) {
          this.mContext = context;
      }

      //加載插件apk
      public void loadPlugin(String pluginPath) {
          //獲取包管理器
          PackageManager packageManager = mContext.getPackageManager();
          //獲取插件的包信息類
          packageInfo = packageManager.getPackageArchiveInfo(pluginPath, PackageManager.GET_ACTIVITIES);

          //插件解壓后的目錄
          File pluginFile = mContext.getDir("plugin", Context.MODE_PRIVATE);

          //獲取到類加載器
          dexClassLoader = new DexClassLoader(pluginPath, pluginFile.getAbsolutePath(), null, mContext.getClassLoader());

          //獲取到插件的資源對象
          try {
              AssetManager assetManager = AssetManager.class.newInstance();
              Method addAssetPath = assetManager.getClass().getDeclaredMethod("addAssetPath", String.class);
              addAssetPath.invoke(assetManager, pluginPath);
              pluginResource = new Resources(assetManager,mContext.getResources().getDisplayMetrics(),mContext.getResources().getConfiguration());
          } catch (Exception e) {
              e.printStackTrace();
          }
      }

      public Resources getPluginResource() {
          return pluginResource;
      }

      public DexClassLoader getDexClassLoader() {
          return dexClassLoader;
      }

      public PackageInfo getPackageInfo() {
          return packageInfo;
      }

}

接下來打包插件撒桨,放到sd卡中:


image.png

打包成功后,如下:


image.png

然后改個名字為:orderfood.apk上傳到sd卡根目錄下:

image.png

image.png

接下來在宿主Activity中實現(xiàn)跳轉(zhuǎn)插件代碼

 public class MainActivity extends AppCompatActivity {

      @Override
      protected void onCreate(Bundle savedInstanceState) {
          super.onCreate(savedInstanceState);
          setContentView(R.layout.activity_main);
      }


      //跳轉(zhuǎn)插件
      public void skipPlugin(View view) {
          PluginManager.getInstance().setContext(this);
          PluginManager.getInstance().loadPlugin(Environment.getExternalStorageDirectory() + "/orderfood.apk");
          PackageInfo packageInfo = PluginManager.getInstance().getPackageInfo();
          Intent intent = new Intent(MainActivity.this, ProxyActivity.class);
          //由于插件只有一個activity键兜,所以取數(shù)組第0個
          intent.putExtra("className", packageInfo.activities[0].name);
          startActivity(intent);
      }
  }

記得加權(quán)限


image.png

接下來運行app凤类,由于我的手機是Android10.0,所以對于sdcard的權(quán)限得要主動申請一下普气,這里就不寫申請的代碼了谜疤,主動到權(quán)限管理中先將其打開,如下:

image.png

代碼(已適配android10)已上傳github
中,親測可用

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末夷磕,一起剝皮案震驚了整個濱河市履肃,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌坐桩,老刑警劉巖尺棋,帶你破解...
    沈念sama閱讀 206,723評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異绵跷,居然都是意外死亡膘螟,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,485評論 2 382
  • 文/潘曉璐 我一進店門碾局,熙熙樓的掌柜王于貴愁眉苦臉地迎上來荆残,“玉大人,你說我怎么就攤上這事净当∧谒梗” “怎么了?”我有些...
    開封第一講書人閱讀 152,998評論 0 344
  • 文/不壞的土叔 我叫張陵像啼,是天一觀的道長俘闯。 經(jīng)常有香客問我,道長忽冻,這世上最難降的妖魔是什么真朗? 我笑而不...
    開封第一講書人閱讀 55,323評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮甚颂,結(jié)果婚禮上蜜猾,老公的妹妹穿的比我還像新娘秀菱。我一直安慰自己振诬,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 64,355評論 5 374
  • 文/花漫 我一把揭開白布衍菱。 她就那樣靜靜地躺著赶么,像睡著了一般。 火紅的嫁衣襯著肌膚如雪脊串。 梳的紋絲不亂的頭發(fā)上辫呻,一...
    開封第一講書人閱讀 49,079評論 1 285
  • 那天,我揣著相機與錄音琼锋,去河邊找鬼放闺。 笑死,一個胖子當(dāng)著我的面吹牛缕坎,可吹牛的內(nèi)容都是我干的怖侦。 我是一名探鬼主播,決...
    沈念sama閱讀 38,389評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼匾寝!你這毒婦竟也來了搬葬?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,019評論 0 259
  • 序言:老撾萬榮一對情侶失蹤艳悔,失蹤者是張志新(化名)和其女友劉穎急凰,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體猜年,經(jīng)...
    沈念sama閱讀 43,519評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡抡锈,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,971評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了码倦。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片企孩。...
    茶點故事閱讀 38,100評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖袁稽,靈堂內(nèi)的尸體忽然破棺而出勿璃,到底是詐尸還是另有隱情,我是刑警寧澤推汽,帶...
    沈念sama閱讀 33,738評論 4 324
  • 正文 年R本政府宣布补疑,位于F島的核電站,受9級特大地震影響歹撒,放射性物質(zhì)發(fā)生泄漏莲组。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,293評論 3 307
  • 文/蒙蒙 一暖夭、第九天 我趴在偏房一處隱蔽的房頂上張望锹杈。 院中可真熱鬧,春花似錦迈着、人聲如沸竭望。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,289評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽咬清。三九已至,卻和暖如春奴潘,著一層夾襖步出監(jiān)牢的瞬間旧烧,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,517評論 1 262
  • 我被黑心中介騙來泰國打工画髓, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留掘剪,地道東北人。 一個月前我還...
    沈念sama閱讀 45,547評論 2 354
  • 正文 我出身青樓奈虾,卻偏偏與公主長得像夺谁,于是被迫代替她去往敵國和親肆汹。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,834評論 2 345

推薦閱讀更多精彩內(nèi)容