如今移動(dòng)app市場(chǎng)已經(jīng)是百花齊放,其中有不乏有很多大型公司、巨型公司都是通過app創(chuàng)業(yè)發(fā)展起來的;app類型更加豐富,有電子商務(wù)、有視頻、有社交、有工具等等,基本上涵蓋了各行各業(yè)每個(gè)角落,為了更加具有競(jìng)爭(zhēng)力app不僅功能上有創(chuàng)性,內(nèi)容也更加多元化,更加飽滿,所以出現(xiàn)了巨大的工程。這些工程代碼不停添加如果沒有一個(gè)好的架構(gòu)所有代碼將會(huì)強(qiáng)耦合在一起,功能直接也會(huì)有很多依賴,那么就會(huì)出現(xiàn)很多問題;例如:
1、修改功能困難,牽一發(fā)動(dòng)全身。很多地方如果api寫的不好,封裝不優(yōu)雅,那么就會(huì)出現(xiàn)改一個(gè)地方需要改很多地方的調(diào)用。
2、更新迭代工作中冗余廢棄代碼資源過多造成刪除冗余變得很復(fù)雜,并且很可能出現(xiàn)很多bug。
大型app有哪些架構(gòu)解決方案?
在編碼架構(gòu)上有:
mvc
mvp
mvvm
- 1
- 2
- 3
- 4
從項(xiàng)目結(jié)構(gòu)上有:
插件化
組件化
- 1
- 2
- 3
這里我們一個(gè)個(gè)來分析說明:
首先我們來看看編碼設(shè)計(jì)模式,上面模式的模式都是抽象模式,所以這個(gè)具象界定沒有官方一致的規(guī)定。所以要根據(jù)自己的理解很解釋都有自己的一套mvc模式,不一定是具象到什么細(xì)節(jié)這類的,下面討論的也是會(huì)舉出例子來說明自己的理解。
代碼設(shè)計(jì)模式
MVC全名是Model View Controller,是模型(model)-視圖(view)-控制器(controller)的縮寫,一種軟件設(shè)計(jì)典范,用一種業(yè)務(wù)邏輯、數(shù)據(jù)、界面顯示分離的方法組織代碼,將業(yè)務(wù)邏輯聚集到一個(gè)部件里面,在改進(jìn)和個(gè)性化定制界面及用戶交互的同時(shí),不需要重新編寫業(yè)務(wù)邏輯。MVC被獨(dú)特的發(fā)展起來用于映射傳統(tǒng)的輸入、處理和輸出功能在一個(gè)邏輯的圖形化用戶界面的結(jié)構(gòu)中。
舉個(gè)栗子:具有生命周期的activity相當(dāng)于Controller, 自己開發(fā)封裝用于獲取數(shù)據(jù)(網(wǎng)絡(luò)數(shù)據(jù)、本地?cái)?shù)據(jù)、數(shù)據(jù)處理邏輯等)的api相當(dāng)與Model,xml控件和自定義控制控件顯示數(shù)據(jù)的邏輯相當(dāng)與view。
mvc模式是非常常見的模式基本上有基本概念就能按照這個(gè)模式進(jìn)行開發(fā),這里就不過多討論了。
MVP 全稱:Model-View-Presenter ;MVP 是從經(jīng)典的模式MVC演變而來,它們的基本思想有相通的地方:Controller/Presenter負(fù)責(zé)邏輯的處理,Model提供數(shù)據(jù),View負(fù)責(zé)顯示。
舉個(gè)栗子:Adapter相當(dāng)與Presenter控制控制數(shù)據(jù)與顯示的分離,向Adapter喂食數(shù)據(jù)的api獲取處理數(shù)據(jù)相當(dāng)與Model,支持Adapter的顯示的控件相當(dāng)于View層。
mvp是從mvc基礎(chǔ)上衍生出來的,mvp看上去與mvc好像沒有什么差別,但是實(shí)際不然,mvc model數(shù)據(jù)與view層組合是直接組合,難免會(huì)產(chǎn)生耦合,這樣model復(fù)用性有一定缺失。mvp優(yōu)化這種結(jié)構(gòu),他抽象出來一個(gè)接口規(guī)則,那么view需要支持這個(gè)規(guī)則,而model按照這個(gè)規(guī)則向里面喂食數(shù)據(jù)。這樣解開耦合model view,這樣model與view鏈接邏輯都是用Presenter控制。
MVVM是Model-View-ViewModel的簡(jiǎn)寫。微軟的WPF帶來了新的技術(shù)體驗(yàn),如Silverlight、音頻、視頻、3D、動(dòng)畫……,這導(dǎo)致了軟件UI層更加細(xì)節(jié)化、可定制化。同時(shí),在技術(shù)層面,WPF也帶來了 諸如Binding、Dependency Property、Routed Events、Command、DataTemplate、ControlTemplate等新特性。MVVM(Model-View-ViewModel)框架的由來便是MVP(Model-View-Presenter)模式與WPF結(jié)合的應(yīng)用方式時(shí)發(fā)展演變過來的一種新型架構(gòu)框架。它立足于原有MVP框架并且把WPF的新特性糅合進(jìn)去,以應(yīng)對(duì)客戶日益復(fù)雜的需求變化。
舉個(gè)栗子:使用databing可以搭建mvvm架構(gòu),獲取網(wǎng)絡(luò)數(shù)據(jù)封裝api相當(dāng)于Model,數(shù)據(jù)處理后分給databing設(shè)置界面綁定數(shù)據(jù)源和和界面上綁定的邏輯相當(dāng)于ViewModel層,用于最終現(xiàn)實(shí)的控件相當(dāng)于view層。
其實(shí)看到上面的似乎有點(diǎn)模糊不清楚,用mvp作為參照,只是p層替換成了vm層,增加xml功能屬性,能夠利于view層屬性方法來擴(kuò)張功能,將一些與界面相關(guān)邏輯處理加入這層,更加細(xì)分了抽象層次。
android組件化方案
組件化:
Android studio改變了項(xiàng)目構(gòu)建方式,eclipse環(huán)境下的工作空間和project變成現(xiàn)在的module和項(xiàng)目,這樣類別雖然不精確但是這個(gè)不是重點(diǎn),重點(diǎn)他加入項(xiàng)目構(gòu)建工具gradle使得我們項(xiàng)目構(gòu)建變得非常簡(jiǎn)單了。接下來用一個(gè)項(xiàng)目組件化方案來體會(huì)一下項(xiàng)目組件化的。
組件化好處:
1、架構(gòu)清晰業(yè)務(wù)組件間完成接耦合。
2、每個(gè)業(yè)務(wù)組件都可以根據(jù)BU需求完成獨(dú)立app發(fā)布。
3、開發(fā)中使開發(fā)者更加輕松,開發(fā)中加快功能開發(fā)調(diào)試的速度。
4、業(yè)務(wù)組件整體刪除添加替換變得非常輕松,減少工程中的代碼資源等冗余文件。
5、業(yè)務(wù)降級(jí),業(yè)務(wù)組件在促銷高峰期間可以業(yè)務(wù)為單元關(guān)閉,保證核心業(yè)務(wù)組件的順利執(zhí)行。
項(xiàng)目組件化方案
概述:
1、module library 切換。
2、組件間跳轉(zhuǎn)uri跳轉(zhuǎn)。
3、組件間通訊 binder機(jī)制。
首先看看項(xiàng)目中的角色:
從上圖可以發(fā)現(xiàn)有一根業(yè)務(wù)總線講所有組件個(gè)串聯(lián)起來,其中組件總線相當(dāng)于主工程(殼工程mudule),而業(yè)務(wù)組件相當(dāng)于工程中(mudule/library)??梢钥闯鼋M件化實(shí)現(xiàn)可以有自己認(rèn)定的維度,這里只是使用了最常用的維度按照業(yè)務(wù)區(qū)分組件。
上面是從抽象角來描述的一張圖,下面我們從具體角度來來看看工程結(jié)構(gòu):
從圖片可以看出,主要有三個(gè)角色:
1、主工程(殼工程mudele):主要負(fù)責(zé)事情不塞入任何具體業(yè)務(wù)邏輯,主要用于使用組合業(yè)務(wù)組件、初始化配置和發(fā)布應(yīng)用配置等操作。
2、組件(module/library):主要實(shí)現(xiàn)具體業(yè)務(wù)邏輯,盡可能保證業(yè)務(wù)獨(dú)立性,例如現(xiàn)在手淘這樣一個(gè)大型的app幾乎每個(gè)bu功能塊都能夠拿出來作為一個(gè)獨(dú)立業(yè)務(wù)app。但是沒有這么大型也可以按照小一些的業(yè)務(wù)邏輯來區(qū)分組件,例如:購(gòu)物車組件、收銀臺(tái)組件、用戶中心組件等,具體更具自己的項(xiàng)目需要來劃分。
3、公共庫(library):公共使用的工具類、sdk等庫,例如eventbus、xutils、rxandroid、自定義工具類等等,這些庫可以做成一個(gè)公共common sdk、也可以實(shí)現(xiàn)抽離很細(xì)按照需求依賴使用。
他們之間的關(guān)系則是 主工程依賴組件、組件依賴公共庫。
組件開發(fā)中分為兩種模式一種開發(fā)測(cè)試模式、一種是發(fā)布模式:
1、開發(fā)測(cè)試模式:這種模式下面組件應(yīng)該是獨(dú)立module模式,module是可以獨(dú)立運(yùn)行的,只要保證他對(duì)其他業(yè)務(wù)沒有依賴就可以獨(dú)立開發(fā)測(cè)試。
2、發(fā)布模式:這時(shí)候組件應(yīng)該library模式被主工程依賴組合,發(fā)布運(yùn)行,所有業(yè)務(wù)將組合成完整app發(fā)布運(yùn)行。
上面模式提出了個(gè)幾個(gè)問題我們可以一一來解決;
問題一:上面兩種模式要求組件一會(huì)是module,一會(huì)是library這樣切換是如何實(shí)現(xiàn)的?
問題二:業(yè)務(wù)之間跳轉(zhuǎn)如何進(jìn)行跳轉(zhuǎn)?
問題三:雖然業(yè)務(wù)組件相對(duì)獨(dú)立,但是如果有時(shí)候一定需要獲取其他組件運(yùn)行是某些狀態(tài)下數(shù)據(jù),也就是組件數(shù)據(jù)間的數(shù)據(jù)互通如何實(shí)現(xiàn)?
問題一:業(yè)務(wù)組件module/library切換解決方法
是用gradle輕松可以解決這個(gè)問題;每當(dāng)我們用AndroidStudio創(chuàng)建一個(gè)Android項(xiàng)目后,就會(huì)在項(xiàng)目的根目錄中生成一個(gè)文件 gradle.properties,我們將使用這個(gè)文件的一個(gè)重要屬性:在Android項(xiàng)目中的任何一個(gè)build.gradle文件中都可以把gradle.properties中的常量讀取出來;那么我們?cè)谏厦嫣岬浇鉀Q辦法就有了實(shí)際行動(dòng)的方法,首先我們?cè)趃radle.properties中定義一個(gè)常量值 isPlugin(是否是組件開發(fā)模式,true為是,false為否):
isPlugin=false
- 1
然后我們?cè)跇I(yè)務(wù)組件的build.gradle中讀取 isPlugin,但是 gradle.properties 還有一個(gè)重要屬性: gradle.properties 中的數(shù)據(jù)類型都是String類型,使用其他數(shù)據(jù)類型需要自行轉(zhuǎn)換;也就是說我們讀到 isPlugin 是個(gè)String類型的值,而我們需要的是Boolean值,代碼如下:
if (isPlugin.toBoolean()) {
apply plugin: 'com.android.application'
} else {
apply plugin: 'com.android.library'
}
- 1
- 2
- 3
- 4
- 5
這樣可以輕松設(shè)置isModule就可以變成切換module、library。
接下來要解決的是就是包名,要知道library是不需要包名的,那么就可以這樣操作:
defaultConfig {
if (isPlugin.toBoolean()){
applicationId 'com.example.rspluginmodule'
}
....
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
最后還要處理AndroidManifest.xml問題,因?yàn)閘ibrary、module的主配置文件是有區(qū)別的:
可以這樣處理首先在main文件家中創(chuàng)建release文件夾然后拷貝一份AndroidManifest.xml進(jìn)入release文件夾,那么發(fā)布模式下使用的就是release文件夾下面的AndroidManifest.xml,而開發(fā)模式下用的就是默認(rèn)的AndroidManifest.xml,這樣就要對(duì)release文件夾下面的AndroidManifest.xml進(jìn)行修改因?yàn)殚_發(fā)模式下release文件夾下面是用來給library使用的。
結(jié)構(gòu)如圖:
修改內(nèi)容release文件夾AndroidManifest.xml內(nèi)容為:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas./apk/res/android"
xmlns:tools="http://schemas./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>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
可以發(fā)現(xiàn)上面去掉了application很多module使用的屬性。
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas./apk/res/android"
xmlns:tool="http://schemas./apk/res-auto"
xmlns:tools="http://schemas./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">
<!--測(cè)試入口activity 只有在module環(huán)境下配置-->
<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>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
那么AndroidManifest.xml文件已經(jīng)建立好了,接下來就要修改gradle配置了;
sourceSets {
main {
if (isPlugin.toBoolean()){
manifest.srcFile 'src/main/AndroidManifest.xml'
}else {
manifest.srcFile 'src/main/release/AndroidManifest.xml'
}
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
這樣問題一就全部解決了,我們可以輕松使用isPlugin來切換業(yè)務(wù)組件的module和library。接下來我們看看業(yè)務(wù)組件間如何進(jìn)行跳轉(zhuǎn)問題。
問題二:業(yè)務(wù)組件間跳轉(zhuǎn)解決方法
不管是開發(fā)模式或者是發(fā)布模式我們都需要處理界面間跳轉(zhuǎn)問題,在業(yè)務(wù)運(yùn)行階段經(jīng)常會(huì)有跳轉(zhuǎn)到不同業(yè)務(wù)組件的界面的需求,我們用什么方法是可以解決這個(gè)問題,其實(shí)android本身提供了這種機(jī)制,而且在很多地方都在使用。例如:調(diào)用系統(tǒng)拍照功能、電話功能等這些功能都是在不同app當(dāng)中,你也可以理解為不同的module當(dāng)中,他們之間的調(diào)用底層都是進(jìn)程通訊,實(shí)現(xiàn)手段非常簡(jiǎn)單,就是是使用意圖篩選器。
可以看到上面的AndroidManifest.xml中配置組件activity時(shí)候都有配置意圖篩選器;
<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>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
我們就可以通過隱式意圖方式打開新的其他組件activity;舉個(gè)例子我要打開RSPluginTestActivity類;就可以調(diào)用下面的方法。
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;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
上面使用的是uri跳轉(zhuǎn),也可以簡(jiǎn)單點(diǎn)使用跳轉(zhuǎn)。
這里還有一個(gè)就是規(guī)范問題:
1、組件命名規(guī)范,java類名加大些前綴,例如RSPluginTestActivity RS就是前綴,類似ios要求的代碼約定,xml、image等資源文件使用對(duì)應(yīng)前綴例如 rs_ 。
2、組件內(nèi)的activity、service系統(tǒng)組件要遵守rest風(fēng)格(rest風(fēng)格把業(yè)務(wù)對(duì)象看作資源用唯一uri標(biāo)識(shí)調(diào)用),組件間盡量能夠通過uri唯一標(biāo)識(shí)調(diào)用,不用過多業(yè)務(wù)bean傳遞依賴。
這樣問題二組件跳轉(zhuǎn)問題就解決了。接下來就來解決最后一座大山問題了:
問題三:組件間通訊問題
組件間如果按照規(guī)范應(yīng)該業(yè)務(wù)邏輯獨(dú)立,對(duì)其他模塊沒有耦合的情況,但是有時(shí)候要發(fā)生那么數(shù)據(jù)交換的話要怎么解決?如果嚴(yán)格按照rest風(fēng)格業(yè)務(wù)組件每塊只需要通過界面間跳轉(zhuǎn)的方式就可以輕松通過intent將數(shù)據(jù)傳輸過去,基本上可以滿足組件間數(shù)據(jù)傳遞的問題。但是這個(gè)只是簡(jiǎn)單夸界面數(shù)據(jù)傳遞,那么如果要是沒有界面跨越也想組件間數(shù)據(jù)傳遞那么要怎么解決?類似web開發(fā)http協(xié)議可以通過get post傳遞數(shù)據(jù),那么不跳頁的時(shí)候數(shù)據(jù)應(yīng)該如何通訊,web提供了ajax機(jī)制。那么android提供什么機(jī)制滿足我們需求?
如果按照地耦合的方式開發(fā),我們業(yè)務(wù)組件間是可以獨(dú)立存在,并且不需要依賴其他業(yè)務(wù)組件,如果公共部分就可以提取成公共業(yè)務(wù)組件工具庫library,但是開發(fā)需求總是非常多變,如果有時(shí)候有著情況時(shí)候我們就要用到進(jìn)程通訊aidl。首先要知道組件開發(fā)模式下的組件都是獨(dú)立module,那么每個(gè)獨(dú)立的module都是獨(dú)立進(jìn)程;在發(fā)布模式下面每個(gè)業(yè)務(wù)組件又是library,那么進(jìn)程變成了一個(gè)。aidl解決進(jìn)程間通訊、系統(tǒng)組件activity、service通訊問題的方案。aidl實(shí)際上是android提供生成binder接口的方法而已,實(shí)際上底層使用的都是binder機(jī)制。
binder機(jī)制這里簡(jiǎn)單介紹一下,他基本上貫通了了怎么android系統(tǒng)和應(yīng)用,首先他是android首選進(jìn)程通訊機(jī)制(IPC),android是建立在linux kernel之上的所以他支持linux的IPC方式,例如:網(wǎng)絡(luò)鏈接進(jìn)程通訊(Internet Process Connection): 管道(Pipe)、信號(hào)(Signal)和跟蹤(Trace)、插口(Socket)、報(bào)文隊(duì)列(Message)、共享內(nèi)存(Share Memory)和信號(hào)量(Semaphore)。那么為什么還要出現(xiàn)binder機(jī)制那是因?yàn)樗轻槍?duì)移動(dòng)端這種時(shí)效性快、資源消耗低而設(shè)計(jì)出來了,是移動(dòng)端首選的進(jìn)程通訊方式。從binder應(yīng)用的范圍就知道他重要性,除了Zygote進(jìn)程和SystemServer進(jìn)程之間使用的socket通訊之外,基本上其他進(jìn)程通訊都是用的是binder方式。
首先我們來看一張圖:
可以發(fā)現(xiàn)每個(gè)module都provider屬于自己提供出去的action,這樣這些可以在提供其他業(yè)務(wù)組件調(diào)用。這時(shí)候provider端相當(dāng)于服務(wù)端,提供處理后數(shù)據(jù),調(diào)用相當(dāng)于客戶端。
下面看看binder機(jī)制實(shí)現(xiàn)方法:
首先第一步創(chuàng)建進(jìn)程通訊接口:
CommonProvider.java
/**
* 作者: 李一航
* 時(shí)間: 18-1-4.
*/
public interface CommonProvider extends IInterface {
String getJsonData(String jsonParm) throws RemoteException;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
這里只是創(chuàng)建一個(gè)action function 例子,可以根據(jù)自己需要?jiǎng)?chuàng)建多個(gè)action function。
接下來創(chuàng)建service端實(shí)現(xiàn)基類:
CommonStub.java
/**
* 作者: 李一航
* 時(shí)間: 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);
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
service端實(shí)現(xiàn)了binder機(jī)制回調(diào)動(dòng)作:onTransact 方法。這里將會(huì)調(diào)用繼承類的接口實(shí)現(xiàn)法方法:getJsonData
接下來創(chuàng)建遠(yuǎn)程代理類:
CommonProxy.java
/**
* 作者: 李一航
* 時(shí)間: 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;
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
使用代理可以拿到接口代理對(duì)象完成action操作。
三個(gè)binder使用的類、接口可以作為基礎(chǔ)類庫使用。接下來可以完成遠(yuǎn)程調(diào)用例子:
創(chuàng)建service業(yè)務(wù)實(shí)現(xiàn)類:
CommonProviderImp.java
/**
* 作者: 李一航
* 時(shí)間: 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;
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
這里只完成了簡(jiǎn)單的輸入輸出,方便理解測(cè)試;
創(chuàng)建調(diào)用service類:
PluginProviderService.java
/**
* 作者: 李一航
* 時(shí)間: 18-1-4.
*/
public class PluginProviderService extends Service {
@Nullable
@Override
public IBinder onBind(Intent intent) {
return new CommonProviderImp();
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
在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>
- 1
- 2
- 3
- 4
- 5
- 6
上面provider暴露的action function完成了,接下來就在其他業(yè)務(wù)組件中完成調(diào)用測(cè)試。
創(chuàng)建調(diào)用測(cè)試activity調(diào)用:
ClientActivity.java
/**
* 作者: 李一航
* 時(shí)間: 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;
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
組件間通訊就這樣完成了,這里為了好理解代碼都是使用了最簡(jiǎn)單樣子,可以在項(xiàng)目進(jìn)行封裝優(yōu)化來適合項(xiàng)目復(fù)用。
組件化總結(jié)
組件化是大型app開發(fā)中非常重要架構(gòu)設(shè)計(jì),其實(shí)上面貢獻(xiàn)的只是組件化方案的核心部分,要是一套完整組件化還需要處理很多細(xì)節(jié)問題,在開發(fā)中還要不斷封裝、優(yōu)化、復(fù)用等才能夠使得架構(gòu)清晰健壯。上面組件化方案中主要涉及到的知識(shí)點(diǎn)并不復(fù)雜,概括一下有g(shù)radle項(xiàng)目配置、router uri、 binder進(jìn)程間通訊等。組件化重要的思想,現(xiàn)在很多文章有各種各樣的方案,理清思路選擇適合自己項(xiàng)目的架構(gòu)才是最重要的。
android插件化方案
有了上面組件化方案理解之后,插件化理解也不是難事,首先我們還是用業(yè)務(wù)為界限來劃分組件內(nèi)容;開發(fā)模式下面module本來就是一個(gè)獨(dú)立app,只是發(fā)布模式下變成library,那么插件化就是不存在發(fā)布模式開發(fā)模式,每個(gè)組件業(yè)務(wù)就是一個(gè)獨(dú)立apk開發(fā),然后通過主工程app動(dòng)態(tài)加載部署業(yè)務(wù)組件apk。
插件化帶來的好處:
1、業(yè)務(wù)組件解耦合,能夠?qū)崿F(xiàn)業(yè)務(wù)組件熱拔插。
2、更改了公司開發(fā)的迭代模式,從以前以整個(gè)app發(fā)版流程更改為app發(fā)版和業(yè)務(wù)插件發(fā)版流程。
3、實(shí)現(xiàn)了用戶不需要更新app版本完成bug修復(fù)和業(yè)務(wù)組件升級(jí)等熱修復(fù)功能。
4、組件化有的好處插件化都有,因?yàn)椴寮⒃诮M件化架構(gòu)之上。
- 1
- 2
- 3
- 4
- 5
那么插件化帶來都是好處,有沒有缺點(diǎn)呢?現(xiàn)在很多各式各樣的開源插件框架都有各自優(yōu)點(diǎn)和缺點(diǎn);接下來我們還是一問題的形式來解決這樣問題。
首先我們來了解一下插件化實(shí)現(xiàn)的原理,由于插件化原理涵蓋內(nèi)容太多這里只是介紹一下核心內(nèi)容;我們了解一下app打包過程。請(qǐng)看下圖:
上面是android打包形成apk的一個(gè)過程,可以發(fā)現(xiàn)android開發(fā)主要的部分是整合編譯代碼、整合編譯資源,然后就是安全簽名保證apk完整性。我們?cè)倏匆粡垐D:
上面是一個(gè)apk解壓之后的文件,可以看出,里面幾個(gè)比較重要的部分:
1、dex文件java編譯之后的代碼文件。
2、app中使用資源文件。
3、so文件動(dòng)態(tài)鏈接庫。
- 1
- 2
- 3
- 4
而插件化動(dòng)態(tài)加載部署這些內(nèi)容。加載上面的內(nèi)容就產(chǎn)生幾個(gè)技術(shù)問題:dex文件加載、資源文件加載、so文件加載部署、activity、service等android組件的動(dòng)態(tài)注冊(cè)。
首先是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;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
夾在apk中dex就是通過DexClassLoader api來加載的,通過DexClassLoader就可以調(diào)用apk中java代碼,接下來就是通過反射機(jī)制去替換app的ClassLoader,這一步步驟對(duì)android framework源碼依賴非常大,然后通過ClassLoader的雙親機(jī)制將主工程app的ClassLoader設(shè)置成父級(jí)ClassLoader,這樣加載dex步驟就完成了,具體實(shí)現(xiàn)技術(shù)篇幅太大以后有空會(huì)專門出一篇文章。
然后是加載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;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
通過上面的方法可以加載出資源內(nèi)容,接下來也是通過反射替換app默認(rèn)夾在的mResources對(duì)象,這樣資源加載完成。
上面比較核心的功能思路,由于篇幅還有很多細(xì)節(jié)這里不能夠全部列出,比如so文件加載部署、activity、service等android組件的動(dòng)態(tài)注冊(cè)都是非常依賴源碼的操作,都是要使用大量的反射來修改系統(tǒng)的成員變量等,所以其實(shí)插件化實(shí)施起來最大的困難就是適配機(jī)型的源碼,所有成本就在這里。那么我們?cè)摬辉撚貌寮??首先根?jù)公司項(xiàng)目實(shí)際情況認(rèn)定,需不需要插件化提供的熱更新功能,如果需要可以選擇插件化。接下來我們來看看市面上插件化框架對(duì)比!
每一款框架都是自己優(yōu)點(diǎn)和缺點(diǎn),但是似乎都不能夠滿足所有功能,這里我總結(jié)一下什么時(shí)候應(yīng)該用到插件化,首先不是所有地方都是插件化而是局部使用,符合一下條件可以考慮使用:
1、項(xiàng)目一些偏向ui界面具有更新要求快的模塊可以使用,例如一些出銷頁面更新較快的地方。
2、activity為主,大部分框架對(duì)activity支持較好。
3、對(duì)so等第三方庫依賴較少,so對(duì)插件化兼容性穩(wěn)定考驗(yàn)比較大。
4、沒有使用aop切面編程的代碼。
- 1
- 2
- 3
- 4
- 5
speed-tools插件化框架使用
這里自己寫了一個(gè)對(duì)源碼侵入性小的插件化框架speed tools。
github: speed-tools
可以的話可以 star 一下 ^-^
首先看看項(xiàng)目結(jié)構(gòu):
lib_speed_tools: 插件化核心功能library
module_host_main:宿主工程主工程,負(fù)責(zé)加載部署apk
module_client_one:測(cè)試業(yè)務(wù)apk 1
module_client_two:測(cè)試業(yè)務(wù)apk 2
lib_img_utils:測(cè)試imageloader圖片框架
注意:需要使用speed tools 只需要依賴lib_speed_tools即可,然后開始配置插件化步驟:
首先在module_client_one中創(chuàng)建業(yè)務(wù)邏輯類: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);
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
SpeedBaseInterfaceImp業(yè)務(wù)組件中業(yè)務(wù)activity代理類,他是實(shí)現(xiàn)了主要的生命周期方法,相當(dāng)于組件的activity類。
然后創(chuàng)建hock類每個(gè)業(yè)務(wù)組件中只創(chuàng)建一個(gè):ClientMainActivity.java
public class ClientMainActivity extends SpeedClientBaseActivity {
@Override
public SpeedBaseInterface getProxyBase() {
return new TestClass();
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
這個(gè)類在組件中是唯一的,作用就是hock在獨(dú)立測(cè)試時(shí)候使用。
接下來配置配置組件的AndroidManifest.xml
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/SpeedTheme">
<!--必須設(shè)置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>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
組件意圖寫死保持一直,root_class 是調(diào)用死后使用對(duì)于配置的com.example.clientdome.TestClass業(yè)務(wù)類。這樣業(yè)務(wù)組件配置完成。
接下來配置宿主工程module_host_main;
創(chuàng)建宿主工程唯一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;
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
整個(gè)宿主工程創(chuàng)建一個(gè)類即可,用戶是hock activity;然后創(chuàng)建一個(gè)開屏頁apk第一次加載時(shí)候需要一些時(shí)間,放入開屏等待頁面是非常合適的。
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("當(dāng)前是主宿主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);
}
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
加載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);
- 1
- 2
- 3
- 4
業(yè)務(wù)apk都是放在assets目錄中。最后配置AndroidManifest.xml文件:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas./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">
<!--啟動(dòng)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
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
這樣所有配置結(jié)束,插件化實(shí)現(xiàn)。
總結(jié):
1、插件化在項(xiàng)目中還是加入很多機(jī)制,如果主工程下載更新機(jī)制,配合后臺(tái)區(qū)分用戶版本發(fā)布能夠支持的業(yè)務(wù)插件等,這些東西加起來是個(gè)龐大的工程;
2、現(xiàn)在市面上很多成熟的插件框架都android framework依賴還是很重,但也有侵入性小的框架,例如speed tools。
3、插件架構(gòu)不是全局使用就好,而是根據(jù)自己的需要結(jié)合實(shí)際需求來使用,常更新有不滿足html5提供用戶體驗(yàn)的情況比較合適。
上面是對(duì)項(xiàng)目中插件化開發(fā)積累的經(jīng)驗(yàn),如果有建議可以給我留言。