1. 程式人生 > >Android外掛化、元件化

Android外掛化、元件化

轉載僅供本人存檔及後續研究使用,請尊重原創。

轉載自:https://blog.csdn.net/qq941263013/article/details/82864553

 

如今移動app市場已經是百花齊放,其中有不乏有很多大型公司、巨型公司都是通過app創業發展起來的;app型別更加豐富,有電子商務、有視訊、有社交、有工具等等,基本上涵蓋了各行各業每個角落,為了更加具有競爭力app不僅功能上有創性,內容也更加多元化,更加飽滿,所以出現了巨大的工程。這些工程程式碼不停新增如果沒有一個好的架構所有程式碼將會強耦合在一起,功能直接也會有很多依賴,那麼就會出現很多問題;例如:

1、修改功能困難,牽一髮動全身。很多地方如果api寫的不好,封裝不優雅,那麼就會出現改一個地方需要改很多地方的呼叫。
2、更新迭代工作中冗餘廢棄程式碼資源過多造成刪除冗餘變得很複雜,並且很可能出現很多bug。

大型app有哪些架構解決方案?

在編碼架構上有:

  1. mvc

  2. mvp

  3. mvvm

從專案結構上有:

  1. 外掛化

  2. 元件化

這裡我們一個個來分析說明:

首先我們來看看編碼設計模式,上面的模式都是抽象模式,所以這個具象界定沒有官方一致的規定。因此要根據自己的理解設計自己的一套mvc模式,不一定是具象到什麼細節這類的,下面討論的也是會舉出例子來說明自己的理解。

程式碼設計模式

MVC全名是Model View Controller,是模型(model)-檢視(view)-控制器(controller)的縮寫,一種軟體設計典範,用一種業務邏輯、資料、介面顯示分離的方法組織程式碼,將業務邏輯聚集到一個部件裡面,在改進和個性化定製介面及使用者互動的同時,不需要重新編寫業務邏輯。MVC被獨特的發展起來用於對映傳統的輸入、處理和輸出功能在一個邏輯的圖形化使用者介面的結構中。

舉個栗子:具有生命週期的activity相當於Controller, 自己開發封裝用於獲取資料(網路資料、本地資料、資料處理邏輯等)的api相當與Model,xml控制元件和自定義控制控制元件顯示資料的邏輯相當與view。
mvc模式是非常常見的模式基本上有基本概念就能按照這個模式進行開發,這裡就不過多討論了。

MVP 全稱:Model-View-Presenter ;MVP 是從經典的模式MVC演變而來,它們的基本思想有相通的地方:Controller/Presenter負責邏輯的處理,Model提供資料,View負責顯示。

舉個栗子:Adapter相當與Presenter控制控制資料與顯示的分離,向Adapter餵食資料的api獲取處理資料相當與Model,支援Adapter的顯示的控制元件相當於View層。

mvp是從mvc基礎上衍生出來的,mvp看上去與mvc好像沒有什麼差別,但是實際不然,mvc model資料與view層組合是直接組合,難免會產生耦合,這樣model複用性有一定缺失。mvp優化這種結構,他抽象出來一個介面規則,那麼view需要支援這個規則,而model按照這個規則向裡面餵食資料。這樣解開耦合model view,這樣model與view連結邏輯都是用Presenter控制。

MVVM是Model-View-ViewModel的簡寫。微軟的WPF帶來了新的技術體驗,如Silverlight、音訊、視訊、3D、動畫……,這導致了軟體UI層更加細節化、可定製化。同時,在技術層面,WPF也帶來了 諸如Binding、Dependency Property、Routed Events、Command、DataTemplate、ControlTemplate等新特性。MVVM(Model-View-ViewModel)框架的由來便是MVP(Model-View-Presenter)模式與WPF結合的應用方式時發展演變過來的一種新型架構框架。它立足於原有MVP框架並且把WPF的新特性糅合進去,以應對客戶日益複雜的需求變化。

舉個栗子:使用databing可以搭建mvvm架構,獲取網路資料封裝api相當於Model,資料處理後分給databing設定介面繫結資料來源和和介面上繫結的邏輯相當於ViewModel層,用於最終現實的控制元件相當於view層。
其實看到上面的似乎有點模糊不清楚,用mvp作為參照,只是p層替換成了vm層,增加xml功能屬性,能夠利於view層屬性方法來擴張功能,將一些與介面相關邏輯處理加入這層,更加細分了抽象層次。

android元件化方案

元件化:

Android studio改變了專案構建方式,eclipse環境下的工作空間和project變成現在的module和專案,這樣類別雖然不精確但是這個不是重點,重點他加入專案構建工具gradle使得我們專案構建變得非常簡單了。接下來用一個專案元件化方案來體會一下專案元件化的。

元件化好處:

1、架構清晰業務元件間完成解耦合。
2、每個業務元件都可以根據BU(業務單元)需求完成獨立app釋出。
3、開發中使開發者更加輕鬆,加快功能開發除錯的速度。
4、業務元件整體刪除新增替換變得非常輕鬆,減少工程中的程式碼資源等冗餘檔案。
5、業務降級,業務元件在促銷高峰期間可以以業務為單元關閉,保證核心業務元件的順利執行。

專案元件化方案

概述:
1、module library 切換。
2、元件間跳轉uri跳轉。
3、元件間通訊 binder機制。

首先看看專案中的角色:
這裡寫圖片描述
從上圖可以發現有一根業務匯流排講所有元件個串聯起來,其中元件匯流排相當於主工程(殼工程mudule),而業務元件相當於工程中(mudule/library)。可以看出元件化實現可以有自己認定的維度,這裡只是使用了最常用的維度按照業務區分元件。
上面是從抽象角來描述的一張圖,下面我們從具體角度來來看看工程結構:
這裡寫圖片描述

從圖片可以看出,主要有三個角色:

1、主工程(殼工程mudele):主要負責事情不塞入任何具體業務邏輯,主要用於使用組合業務元件、初始化配置和釋出應用配置等操作。

2、元件(module/library):主要實現具體業務邏輯,儘可能保證業務獨立性,例如現在手淘這樣一個大型的app幾乎每個功能塊都能夠拿出來作為一個獨立業務app。但是沒有這麼大型也可以按照小一些的業務邏輯來區分元件,例如:購物車元件、收銀臺元件、使用者中心元件等,具體更具自己的專案需要來劃分。

3、公共庫(library):公共使用的工具類、sdk等庫,例如eventbus、xutils、rxandroid、自定義工具類等等,這些庫可以做成一個公共common sdk、也可以實現抽離很細按照需求依賴使用。
他們之間的關係則是 主工程依賴元件、元件依賴公共庫。

元件開發中分為兩種模式一種開發測試模式、一種是釋出模式:
1、開發測試模式:這種模式下面元件應該是獨立module模式,module是可以獨立執行的,只要保證他對其他業務沒有依賴就可以獨立開發測試。
2、釋出模式:這時候元件應該library模式被主工程依賴組合,釋出執行,所有業務將組合成完整app釋出執行。

上面模式提出了個幾個問題我們可以一一來解決;

問題一:上面兩種模式要求元件一會是module,一會是library這樣切換是如何實現的?
問題二:業務之間跳轉如何進行跳轉?
問題三:雖然業務元件相對獨立,但是如果有時候一定需要獲取其他元件執行是某些狀態下資料,也就是元件資料間的資料互通如何實現?

問題一:業務元件module/library切換解決方法

是用gradle輕鬆可以解決這個問題;每當我們用AndroidStudio建立一個Android專案後,就會在專案的根目錄中生成一個檔案 gradle.properties,我們將使用這個檔案的一個重要屬性:在Android專案中的任何一個build.gradle檔案中都可以把gradle.properties中的常量讀取出來;那麼我們在上面提到解決辦法就有了實際行動的方法,首先我們在gradle.properties中定義一個常量值 isPlugin(是否是元件開發模式,true為是,false為否):

isPlugin=false

然後我們在業務元件的build.gradle中讀取 isPlugin,但是 gradle.properties 還有一個重要屬性: gradle.properties 中的資料型別都是String型別,使用其他資料型別需要自行轉換;也就是說我們讀到 isPlugin 是個String型別的值,而我們需要的是Boolean值,程式碼如下:

if (isPlugin.toBoolean()) {

apply plugin: 'com.android.application'

} else {

apply plugin: 'com.android.library'

}

這樣可以輕鬆設定isModule就可以變成切換module、library。

接下來要解決的是就是包名,要知道library是不需要包名的,那麼就可以這樣操作:

defaultConfig {

if (isPlugin.toBoolean()){

applicationId 'com.example.rspluginmodule'

}


....


}

最後還要處理AndroidManifest.xml問題,因為library、module的主配置檔案是有區別的:

可以這樣處理首先在main檔案家中建立release資料夾然後拷貝一份AndroidManifest.xml進入release資料夾,那麼釋出模式下使用的就是release資料夾下面的AndroidManifest.xml,而開發模式下用的就是預設的AndroidManifest.xml,這樣就要對release資料夾下面的AndroidManifest.xml進行修改因為開發模式下release資料夾下面是用來給library使用的。

結構如圖:
這裡寫圖片描述

修改內容release資料夾AndroidManifest.xml內容為:


<?xml version="1.0" encoding="utf-8"?>

<manifest xmlns:android="http://schemas.android.com/apk/res/android"

xmlns:tools="http://schemas.android.com/tools"

package="com.example.rspluginmodule">


.........


<application>


<activity

android:name="com.example.rspluginmodule.RSPluginTestActivity"

android:exported="false"

android:screenOrientation="portrait">

<intent-filter>

<data

android:host="sijienet"

android:path="/plugin_uri_path"

android:scheme="app_schem" />

<action android:name="cn.com.bailian.plugin.VIEW_ACTION" />

<category android:name="android.intent.category.DEFAULT" />

</intent-filter>

</activity>


........


</application>


</manifest>

可以發現上面去掉了application很多module使用的屬性。

<?xml version="1.0" encoding="utf-8"?>

<manifest xmlns:android="http://schemas.android.com/apk/res/android"

xmlns:tool="http://schemas.android.com/apk/res-auto"

xmlns:tools="http://schemas.android.com/tools"

package="com.example.rspluginmodule">



........


<application

android:allowBackup="true"

android:label="@string/app_name"

android:supportsRtl="true"

android:theme="@style/AppTheme"

tools:replace="android:allowBackup">


<!--測試入口activity 只有在module環境下配置-->

<activity android:name=".RSMainActivity">

<intent-filter>

<action android:name="android.intent.action.MAIN" />


<category android:name="android.intent.category.LAUNCHER" />

</intent-filter>

</activity>



<activity

android:name="com.example.rspluginmodule.RSPluginTestActivity"

android:exported="false"

android:screenOrientation="portrait">

<intent-filter>

<data

android:host="sijienet"

android:path="/plugin_uri_path"

android:scheme="app_schem" />

<action android:name="cn.com.bailian.plugin.VIEW_ACTION" />

<category android:name="android.intent.category.DEFAULT" />

</intent-filter>

</activity>


..........



</application>


</manifest>
 

那麼AndroidManifest.xml檔案已經建立好了,接下來就要修改gradle配置了;

sourceSets {

main {

if (isPlugin.toBoolean()){

manifest.srcFile 'src/main/AndroidManifest.xml'

}else {

manifest.srcFile 'src/main/release/AndroidManifest.xml'

}

}

}
 

這樣問題一就全部解決了,我們可以輕鬆使用isPlugin來切換業務元件的module和library。接下來我們看看業務元件間如何進行跳轉問題。

問題二:業務元件間跳轉解決方法

不管是開發模式或者是釋出模式我們都需要處理介面間跳轉問題,在業務執行階段經常會有跳轉到不同業務元件的介面的需求,我們用什麼方法是可以解決這個問題,其實android本身提供了這種機制,而且在很多地方都在使用。例如:呼叫系統拍照功能、電話功能等這些功能都是在不同app當中,你也可以理解為不同的module當中,他們之間的呼叫底層都是程序通訊,實現手段非常簡單,就是是使用意圖篩選器。
可以看到上面的AndroidManifest.xml中配置元件activity時候都有配置意圖篩選器;

<intent-filter>

<data

android:host="sijienet"

android:path="/plugin_uri_path"

android:scheme="app_schem" />

<action android:name="cn.com.bailian.plugin.VIEW_ACTION" />

<category android:name="android.intent.category.DEFAULT" />

</intent-filter>
 

我們就可以通過隱式意圖方式開啟新的其他元件activity;舉個例子我要開啟RSPluginTestActivity類;就可以呼叫下面的方法。

public RMRouter jump(Activity activity,String url, String parm, int animId){

if (url==null)

return this;


Log.i(TAG,"jump page=="+url);

Log.i(TAG,"jump page parm=="+parm);


Intent intent=new Intent(RMConfig.ROUTER_URL_ACTION, Uri.parse(url));

intent.addCategory(Intent.CATEGORY_DEFAULT);

intent.putExtra(RMConfig.ROUTER_PARM, parm);


PackageManager packageManager=activity.getPackageManager();

List<ResolveInfo> resolveInfos = packageManager.queryIntentActivities(intent, 0);


if (! resolveInfos.isEmpty())

{

activity.startActivity(intent);

selectTranlateAnim(activity, animId);

}else {

Log.i(TAG,"no page");

}

return this;

}

上面使用的是uri跳轉,也可以簡單點使用跳轉。

這裡還有一個就是規範問題:

1、元件命名規範,java類名加大些字首,例如RSPluginTestActivity RS就是字首,類似ios要求的程式碼約定,xml、image等資原始檔使用對應字首例如 rs_ 。
2、元件內的activity、service系統元件要遵守rest風格(rest風格把業務物件看作資源用唯一uri標識呼叫),元件間儘量能夠通過uri唯一標識呼叫,不用過多業務bean傳遞依賴。

這樣問題二元件跳轉問題就解決了。接下來就來解決最後一座大山問題了:

問題三:元件間通訊問題

元件間如果按照規範應該業務邏輯獨立,對其他模組沒有耦合的情況,但是有時候要發生那麼資料交換的話要怎麼解決?如果嚴格按照rest風格業務元件每塊只需要通過介面間跳轉的方式就可以輕鬆通過intent將資料傳輸過去,基本上可以滿足元件間資料傳遞的問題。但是這個只是簡單誇介面資料傳遞,那麼如果要是沒有介面跨越也想元件間資料傳遞那麼要怎麼解決?類似web開發http協議可以通過get post傳遞資料,那麼不跳頁的時候資料應該如何通訊,web提供了ajax機制。那麼android提供什麼機制滿足我們需求?

如果按照地耦合的方式開發,我們業務元件間是可以獨立存在,並且不需要依賴其他業務元件,如果公共部分就可以提取成公共業務元件工具庫library,但是開發需求總是非常多變,如果有時候有著情況時候我們就要用到程序通訊aidl。首先要知道元件開發模式下的元件都是獨立module,那麼每個獨立的module都是獨立程序;在釋出模式下面每個業務元件又是library,那麼程序變成了一個。aidl解決程序間通訊、系統元件activity、service通訊問題的方案。aidl實際上是android提供生成binder介面的方法而已,實際上底層使用的都是binder機制。

binder機制這裡簡單介紹一下,他基本上貫通了了怎麼android系統和應用,首先他是android首選程序通訊機制(IPC),android是建立在linux kernel之上的所以他支援linux的IPC方式,例如:網路連結程序通訊(Internet Process Connection): 管道(Pipe)、訊號(Signal)和跟蹤(Trace)、插口(Socket)、報文佇列(Message)、共享記憶體(Share Memory)和訊號量(Semaphore)。那麼為什麼還要出現binder機制那是因為它是針對移動端這種時效性快、資源消耗低而設計出來了,是移動端首選的程序通訊方式。從binder應用的範圍就知道他重要性,除了Zygote程序和SystemServer程序之間使用的socket通訊之外,基本上其他程序通訊都是用的是binder方式。

首先我們來看一張圖:
這裡寫圖片描述
可以發現每個module都provider屬於自己提供出去的action,這樣這些可以在提供其他業務元件呼叫。這時候provider端相當於服務端,提供處理後資料,呼叫相當於客戶端。

下面看看binder機制實現方法:

首先第一步建立程序通訊介面:
CommonProvider.java


/**

* 作者: 李一航

* 時間: 18-1-4.

*/


public interface CommonProvider extends IInterface {

String getJsonData(String jsonParm) throws RemoteException;

}

 

 

這裡只是建立一個action function 例子,可以根據自己需要建立多個action function。

接下來建立service端實現基類:

CommonStub.java


/**

* 作者: 李一航

* 時間: 18-1-4.

*/


public abstract class CommonStub extends Binder implements CommonProvider {


public static final String DESCRIPTOR="com.ffmpeg.bin.CommonProvider";

public static final int ACTION_1 = IBinder.FIRST_CALL_TRANSACTION;


public CommonStub() {

this.attachInterface(this, DESCRIPTOR);

}


@Override

public IBinder asBinder() {

return this;

}


@Override

protected boolean onTransact(int code, Parcel data, Parcel reply, int flags) throws RemoteException {

switch (code)

{

case INTERFACE_TRANSACTION:

{

reply.writeString(DESCRIPTOR);

return true;

}

case ACTION_1:

{

data.enforceInterface(DESCRIPTOR);

String parm = data.readString();

String jsonData = getJsonData(parm);

reply.writeNoException();

reply.writeString(jsonData);

return true;

}

}

return super.onTransact(code, data, reply, flags);

}

}

​​​​​​​

service端實現了binder機制回撥動作:onTransact 方法。這裡將會呼叫繼承類的介面實現法方法:getJsonData

接下來建立遠端代理類:

CommonProxy.java


/**

* 作者: 李一航

* 時間: 18-1-4.

*/


public class CommonProxy implements CommonProvider {


private IBinder binder;


public CommonProxy(IBinder binder) {

this.binder = binder;

}


public static CommonProvider asInterface(IBinder iBinder){

if (iBinder==null)

return null;

IInterface iInterface = iBinder.queryLocalInterface(CommonStub.DESCRIPTOR);

if (iInterface!=null && iInterface instanceof CommonProvider)

return (CommonProvider)iInterface;

return new CommonProxy(iBinder);

}


@Override

public String getJsonData(String jsonParm) throws RemoteException {

Parcel data = Parcel.obtain();

Parcel reply = Parcel.obtain();

String result=null;

try {

data.writeInterfaceToken(CommonStub.DESCRIPTOR);

data.writeString(jsonParm);

binder.transact(CommonStub.ACTION_1, data, reply, 0);

reply.readException();

result=reply.readString();

}catch (Exception e) {

e.printStackTrace();

}finally {

data.recycle();

reply.recycle();

}

return result;

}


@Override

public IBinder asBinder() {

return binder;

}

}
 

​​​​​​​

使用代理可以拿到介面代理物件完成action操作。

三個binder使用的類、介面可以作為基礎類庫使用。接下來可以完成遠端呼叫例子:

建立service業務實現類:
CommonProviderImp.java


/**

* 作者: 李一航

* 時間: 18-1-4.

*/


public class CommonProviderImp extends CommonStub {

@Override

public String getJsonData(String jsonParm) throws RemoteException {


// TODO: 18-1-4 provider action logic


return "result data string+parm:"+jsonParm;

}

}

 

​​​​​​​

這裡只完成了簡單的輸入輸出,方便理解測試;

建立呼叫service類:
PluginProviderService.java



/**

* 作者: 李一航

* 時間: 18-1-4.

*/


public class PluginProviderService extends Service {

@Nullable

@Override

public IBinder onBind(Intent intent) {

return new CommonProviderImp();

}

}

 

 

​​​​​​​

在AndroidManifest.xml配置service資訊:

<service android:name="com.ffmpeg.bin.PluginProviderService">

<intent-filter>

<action android:name="android.intent.action.PROVIDER_MAIN_ACTION"/>

<category android:name="android.intent.category.DEFAULT"/>

</intent-filter>

</service>
 

​​​​​​​

上面provider暴露的action function完成了,接下來就在其他業務元件中完成呼叫測試。
建立呼叫測試activity呼叫:
ClientActivity.java


/**

* 作者: 李一航

* 時間: 18-1-4.

*/


public class ClientActivity extends AppCompatActivity implements ServiceConnection {


private CommonProvider provider;


@Override

protected void onCreate(@Nullable Bundle savedInstanceState) {

super.onCreate(savedInstanceState);


Intent intent=new Intent();

intent.setComponent(new ComponentName(getPackageName(), PluginProviderService.class.getName()));


bindService(intent,this, BIND_AUTO_CREATE);

}


@Override

protected void onDestroy() {

super.onDestroy();


unbindService(this);

}


@Override

public void onServiceConnected(ComponentName componentName, IBinder iBinder) {

provider = CommonProxy.asInterface(iBinder);

try {

Log.i(ClientActivity.class.getSimpleName(), provider.getJsonData("input"));

} catch (RemoteException e) {

e.printStackTrace();

}

}


@Override

public void onServiceDisconnected(ComponentName componentName) {

provider=null;

}

}

元件間通訊就這樣完成了,這裡為了好理解程式碼都是使用了最簡單樣子,可以在專案進行封裝優化來適合專案複用。

元件化總結

元件化是大型app開發中非常重要架構設計,其實上面貢獻的只是元件化方案的核心部分,要是一套完整元件化還需要處理很多細節問題,在開發中還要不斷封裝、優化、複用等才能夠使得架構清晰健壯。上面元件化方案中主要涉及到的知識點並不複雜,概括一下有gradle專案配置、router uri、 binder程序間通訊等。元件化重要的思想,現在很多文章有各種各樣的方案,理清思路選擇適合自己專案的架構才是最重要的。

android外掛化方案

有了上面元件化方案理解之後,外掛化理解也不是難事,首先我們還是用業務為界限來劃分元件內容;開發模式下面module本來就是一個獨立app,只是釋出模式下變成library,那麼外掛化就是不存在釋出模式開發模式,每個元件業務就是一個獨立apk開發,然後通過主工程app動態載入部署業務元件apk。

外掛化帶來的好處:

 
  1. 1、業務元件解耦合,能夠實現業務元件熱拔插。

  2. 2、更改了公司開發的迭代模式,從以前以整個app發版流程更改為app發版和業務外掛發版流程。

  3. 3、實現了使用者不需要更新app版本完成bug修復和業務元件升級等熱修復功能。

  4. 4、元件化有的好處外掛化都有,因為外掛化建立在元件化架構之上。

那麼外掛化帶來都是好處,有沒有缺點呢?現在很多各式各樣的開源外掛框架都有各自優點和缺點;接下來我們還是一問題的形式來解決這樣問題。

首先我們來了解一下外掛化實現的原理,由於外掛化原理涵蓋內容太多這裡只是介紹一下核心內容;我們瞭解一下app打包過程。請看下圖:
這裡寫圖片描述
上面是android打包形成apk的一個過程,可以發現android開發主要的部分是整合編譯程式碼、整合編譯資源,然後就是安全簽名保證apk完整性。我們再看一張圖:
這裡寫圖片描述

上面是一個apk解壓之後的檔案,可以看出,裡面幾個比較重要的部分:

 
  1. 1、dex檔案java編譯之後的程式碼檔案。

  2. 2、app中使用資原始檔。

  3. 3、so檔案動態連結庫。

而外掛化動態載入部署這些內容。載入上面的內容就產生幾個技術問題:dex檔案載入、資原始檔載入、so檔案載入部署、activity、service等android元件的動態註冊。

首先是dex檔案載入:

public static DexClassLoader readDexFile(Context context, String apkPath, String outDexPath){

DexClassLoader dexClassLoader=null;

try {

dexClassLoader=new DexClassLoader(apkPath, getOutDexpaPath(context, outDexPath), context.getApplicationInfo().nativeLibraryDir, context.getClassLoader());

} catch (Exception e) {

e.printStackTrace();

Log.i(tag,""+e.getMessage());

}

return dexClassLoader;

}
​​​​​​​

夾在apk中dex就是通過DexClassLoader api來載入的,通過DexClassLoader就可以呼叫apk中java程式碼,接下來就是通過反射機制去替換app的ClassLoader,這一步步驟對android framework原始碼依賴非常大,然後通過ClassLoader的雙親機制將主工程app的ClassLoader設定成父級ClassLoader,這樣載入dex步驟就完成了,具體實現技術篇幅太大以後有空會專門出一篇文章。

然後是載入apk中資原始檔:

public static Resources readApkRes(Context context, String apkPath){

Resources resources1=null;

try {

AssetManager assetManager=AssetManager.class.newInstance();

Method addAssetPath = assetManager.getClass().getDeclaredMethod("addAssetPath", String.class);

addAssetPath.invoke(assetManager, apkPath);

Resources resources = context.getResources();

resources1 = new Resources(assetManager, resources.getDisplayMetrics(), resources.getConfiguration());

} catch (Exception e) {

e.printStackTrace();

Log.i(tag,""+e.getMessage());

}

return resources1;

}

 

通過上面的方法可以加載出資源內容,接下來也是通過反射替換app預設夾在的mResources物件,這樣資源載入完成。

上面比較核心的功能思路,由於篇幅還有很多細節這裡不能夠全部列出,比如so檔案載入部署、activity、service等android元件的動態註冊都是非常依賴原始碼的操作,都是要使用大量的反射來修改系統的成員變數等,所以其實外掛化實施起來最大的困難就是適配機型的原始碼,所有成本就在這裡。那麼我們該不該用外掛化?首先根據公司專案實際情況認定,需不需要外掛化提供的熱更新功能,如果需要可以選擇外掛化。接下來我們來看看市面上外掛化框架對比!
這裡寫圖片描述

每一款框架都是自己優點和缺點,但是似乎都不能夠滿足所有功能,這裡我總結一下什麼時候應該用到外掛化,首先不是所有地方都是外掛化而是區域性使用,符合一下條件可以考慮使用:

 
  1. 1、專案一些偏向ui介面具有更新要求快的模組可以使用,例如一些出銷頁面更新較快的地方。

  2. 2、activity為主,大部分框架對activity支援較好。

  3. 3、對so等第三方庫依賴較少,so對外掛化相容性穩定考驗比較大。

  4. 4、沒有使用aop切面程式設計的程式碼。

speed-tools外掛化框架使用

這裡自己寫了一個對原始碼侵入性小的外掛化框架speed tools。

github: speed-tools
可以的話可以 star 一下 ^-^

首先看看專案結構:

這裡寫圖片描述

lib_speed_tools: 外掛化核心功能library
module_host_main:宿主工程主工程,負責載入部署apk
module_client_one:測試業務apk 1
module_client_two:測試業務apk 2
lib_img_utils:測試imageloader圖片框架

注意:需要使用speed tools 只需要依賴lib_speed_tools即可,然後開始配置外掛化步驟:

首先在module_client_one中建立業務邏輯類:TestClass.java

/**

* by liyihang

*/

public class TestClass extends SpeedBaseInterfaceImp {


private Activity activity;


@Override

public void onCreate(Bundle savedInstanceState, final Activity activity) {

this.activity=activity;


activity.setContentView(R.layout.activity_client_main);


activity.findViewById(R.id.jump).setOnClickListener(new View.OnClickListener() {

@Override

public void onClick(View v) {

SpeedUtils.goActivity(activity,"first_apk", "two_class");

}

});


ImageView imageView= (ImageView) activity.findViewById(R.id.img_view);

imageView.setVisibility(View.VISIBLE);

ImgUtils.getInstance(activity).showImg("http://img.my.csdn.net/uploads/201309/01/1378037235_3453.jpg", imageView);


}

}

 

 

​​​​​​​

SpeedBaseInterfaceImp業務元件中業務activity代理類,他是實現了主要的生命週期方法,相當於元件的activity類。

然後建立hock類每個業務元件中只建立一個:ClientMainActivity.java

public class ClientMainActivity extends SpeedClientBaseActivity {


@Override

public SpeedBaseInterface getProxyBase() {

return new TestClass();

}



}
​​​​​​​

這個類在元件中是唯一的,作用就是hock在獨立測試時候使用。

接下來配置配置元件的AndroidManifest.xml

<application

android:allowBackup="true"

android:icon="@mipmap/ic_launcher"

android:label="@string/app_name"

android:supportsRtl="true"

android:theme="@style/SpeedTheme">


<!--必須設定root_class-->

<meta-data

android:name="root_class"

android:value="com.example.clientdome.TestClass" />


<meta-data

android:name="two_class"

android:value="com.example.clientdome.TwoClass" />


<activity

android:name=".ClientMainActivity"

android:theme="@style/SpeedTheme">

<intent-filter>

<action android:name="android.intent.action.MAIN" />


<category android:name="android.intent.category.LAUNCHER" />

</intent-filter>


<!--元件意圖-->

<intent-filter>

<data android:scheme="speed_tools" android:host="sijienet.com" android:path="/find_class"/>

<action android:name="android.intent.action.VIEW"/>

<category android:name="android.intent.category.DEFAULT"/>

</intent-filter>

</activity>

</application>

 

 

​​​​​​​

元件意圖寫死保持一直,root_class 是呼叫死後使用對於配置的com.example.clientdome.TestClass業務類。這樣業務元件配置完成。

接下來配置宿主工程module_host_main;

建立宿主工程唯一hock類:ApkActivity.java

/**

* by liyihang

*/

public class ApkActivity extends SpeedHostBaseActivity {



@Override

public String getApkKeyName() {

return HostMainActivity.FIRST_APK_KEY;

}


@Override

public String getClassTag() {

return null;

}

}

​​​​​​​

整個宿主工程建立一個類即可,使用者是hock activity;然後建立一個開屏頁apk第一次載入時候需要一些時間,放入開屏等待頁面是非常合適的。

HostMainActivity.java

/**

* by liyihang

*/

public class HostMainActivity extends AppCompatActivity implements Runnable,Handler.Callback, View.OnClickListener {



public static final String FIRST_APK_KEY="first_apk";

public static final String TWO_APK_KEY="other_apk";


private Handler handler;


private TextView showFont;

private ProgressBar progressBar;

private Button openOneApk;

private Button openTwoApk;


@Override

protected void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);

setContentView(R.layout.activity_host_main);


showFont= (TextView) findViewById(R.id.show_font);

progressBar= (ProgressBar) findViewById(R.id.progressbar);

openOneApk= (Button) findViewById(R.id.open_one_apk);

openTwoApk= (Button) findViewById(R.id.open_two_apk);


handler=new Handler(this);

new Thread(this).start();

}


@Override

public void run() {

String s = "module_client_one-release.apk";

String dexOutPath="dex_output2";

File nativeApkPath = SpeedUtils.getNativeApkPath(getApplicationContext(), s);

SpeedApkManager.getInstance().loadApk(FIRST_APK_KEY, nativeApkPath.getAbsolutePath(), dexOutPath, this);


String s2 = "module_client_two-release.apk";

String dexOutPath2="dex_output3";

File nativeApkPath1 = SpeedUtils.getNativeApkPath(getApplicationContext(), s2);

SpeedApkManager.getInstance().loadApk(TWO_APK_KEY, nativeApkPath1.getAbsolutePath(), dexOutPath2, this);


handler.sendEmptyMessage(0x78);

}


@Override

public boolean handleMessage(Message message) {

showFont.setText("當前是主宿主apk\n外掛apk完畢");

progressBar.setVisibility(View.GONE);

openOneApk.setVisibility(View.VISIBLE);

openTwoApk.setVisibility(View.VISIBLE);

openOneApk.setOnClickListener(this);

openTwoApk.setOnClickListener(this);

return false;

}


@Override

public void onClick(View v) {

if (v.getId()==R.id.open_one_apk)

{

SpeedUtils.goActivity(this, FIRST_APK_KEY, null);

}

if (v.getId()==R.id.open_two_apk)

{

SpeedUtils.goActivity(this, TWO_APK_KEY, null);

}

}

}

 

 

​​​​​​​

載入apk核心程式碼是:

String s = "module_client_one-release.apk";

String dexOutPath="dex_output2";

File nativeApkPath = SpeedUtils.getNativeApkPath(getApplicationContext(), s);

SpeedApkManager.getInstance().loadApk(FIRST_APK_KEY, nativeApkPath.getAbsolutePath(), dexOutPath, this);

​​​​​​​

業務apk都是放在assets目錄中。最後配置AndroidManifest.xml檔案:

<?xml version="1.0" encoding="utf-8"?>

<manifest xmlns:android="http://schemas.android.com/apk/res/android"

package="com.example.hostproject">



<uses-permission android:name="android.permission.INTERNET"/>

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>



<application

android:allowBackup="true"

android:icon="@mipmap/ic_launcher"

android:label="@string/app_name"

android:supportsRtl="true"

android:theme="@style/SpeedTheme">


<!--啟動activity 載入apk-->

<activity android:name=".HostMainActivity">

<intent-filter>

<action android:name="android.intent.action.MAIN" />


<category android:name="android.intent.category.LAUNCHER" />

</intent-filter>

</activity>



<!--元件hack-->

<activity

android:name=".ApkActivity"

android:label="@string/app_name"

android:theme="@style/SpeedTheme" >

<intent-filter>

<data android:scheme="speed_tools" android:host="jason.com" android:path="/find_class"/>

<action android:name="android.intent.action.VIEW"/>

<category android:name="android.intent.category.DEFAULT"/>

</intent-filter>

</activity>

</application>


</manifest>

 

 

​​​​​​​

這樣所有配置結束,外掛化實現。

總結:

1、外掛化在專案中還是加入很多機制,如果主工程下載更新機制,配合後臺區分使用者版本釋出能夠支援的業務外掛等,這些東西加起來是個龐大的工程;

2、現在市面上很多成熟的外掛框架都android framework依賴還是很重,但也有侵入性小的框架,例如speed tools。

3、外掛架構不是全域性使用就好,而是根據自己的需要結合實際需求來使用,常更新有不滿足html5提供使用者體驗的情況比較合適。

上面是對專案中外掛化開發積累的經驗,如果有建議可以給我留言。