android架構設計之插件化、組件化

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

轉載自:https://blog.csdn.net/mhhyoucom/article/details/79000072

 

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

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

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

在編碼架構上有:

mvc
mvp
mvvm

從項目結構上有:

插件化
組件化

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

首先我們來看看編碼設計模式,上面的模式都是抽象模式,所以這個具象界定沒有官方一致的規定。因此要根據自己的理解設計自己的一套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、業務組件解耦合,能夠實現業務組件熱拔插。
2、更改了公司開發的迭代模式,從以前以整個app發版流程更改爲app發版和業務插件發版流程。
3、實現了用戶不需要更新app版本完成bug修復和業務組件升級等熱修復功能。
4、組件化有的好處插件化都有,因爲插件化建立在組件化架構之上。

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

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

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

1、dex文件java編譯之後的代碼文件。
2、app中使用資源文件。
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、項目一些偏向ui界面具有更新要求快的模塊可以使用,例如一些出銷頁面更新較快的地方。
2、activity爲主,大部分框架對activity支持較好。
3、對so等第三方庫依賴較少,so對插件化兼容性穩定考驗比較大。
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提供用戶體驗的情況比較合適。

上面是對項目中插件化開發積累的經驗,如果有建議可以給我留言。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章