前言
組件化和插件化已經提出了很久了,到現在也是比較穩定的一種架構方案了,在三年前,組件化和插件提出來沒多久,前公司就已經在項目中使用了,只是當時還只是菜鳥,沒有資格參與到架構的建設中,只是在大佬搭好的架構中寫一些業務代碼。當時的做法基本上也和現在網上流行的大多數使用的方案是一致的。
最近花了半個月的時間自己從0到一的設計了一個完全組件化的架構的demo,當然里面有些使用的技術可能不是最合適的,但是我覺得對於要開始實現組價化或者學習組件多多少少都會有一些參考的價值。在搭建架構的前期,也參考了很多人的思想,但是當自己動手操作起來的時候,根本不是那么一回事,有些反而更加容易造成一些不必要的代碼編寫和更加容易產生bug,在開發階段和發布階段沒有完全的統一,很容易出現問題,造成各種各樣的問題。
比如說,現在網上很多方案都喜歡在gradle.properties 定義一個值,通過改變這個值來切換庫模式和獨立調試模式。這並沒有什么問題。我們知道庫模式是apply plugin: 'com.android.library' 而獨立模式為apply plugin: 'com.android.application' 。舉個例子,我們在平常開發的時候,有很多同學喜歡使用注解來減少findviewbyid。但是在庫模式下你會發現這些會報錯等問題。還有創建兩份AndroidManifest。要維護兩份清單文件,稍有不慎,在最后合並的時候就會出問題,這些都是隱藏的問題。
模塊化,組件化,插件化的區別
我個人認為這幾個都是一次次進化的過程。從開始無架構,所有代碼都all in one 一快到我們把一些公共組件,業務抽取出來作為module,這些就是屬於簡單的模塊化了。
只是說他們層次清晰了,但是耦合性還是很強。而組件化就是在模塊化的基礎上再次拆分,組件之間相互獨立,可以獨立運行,他們都是可以單獨抽取出來作為SDK對外使用的。
插件化和組件化最大的區別就是插件化能夠動態的修改,而組件化不行。
組件的划分
實現組件化不管是使用什么樣的技術路徑,都要考慮以下幾個問題
代碼隔離:怎么把一個完整的項目拆分成若干個有機的整體
組件單獨調試:把項目拆分為若干個獨立的有機整體,那么怎樣把這些有機整體單獨運行起來呢?
UI跳轉:一個整體的項目,可以通過intent來進行跳轉,但是組件之間是相互隔離的,怎么實現他們之間的跳轉呢?包括數據的傳遞,UI的混合等等都是需要考慮的。
組件間按需依賴:在調試階段,怎樣把一個,兩個...組件整合到一塊來進行調試呢?那么項目開發完以后,怎么把所有的組件融合到一塊呢?
組件的生命周期:我們的目標是可以做到對組件可以按需、動態的使用,因此就會涉及到組件加載、卸載和降維的生命周期。
組件間的通信:組件之間是沒有相互依賴的,那怎么才能夠讓組件之間的數據實現共享?
資源隔離:組件之間相互獨立,各個開發人員負責對應的模塊,很容易出現資源命名出現相同,最后集成調試的時候出現沖突,這些又該怎么避免呢?
......
在這里提一下,組件化一個一時半會就能夠完完全全實現的,他需要花很長的時間慢慢的進行拆分,也許今天設計的某個方案很不錯,但是過一段時間以后,就會成為一個冗余代碼塊。
demo的架構
不同的項目划分的層都是不一樣的,具體划分幾層,怎么划分,完全取決於自己的項目結構,大小,公司開發人員的數量等。划分的粒度不宜過大,也不宜過小,合適自己的就是最好的。demo這樣划分的目的是:我看過網上很多案例,module全部放出來到整個項目路徑下,根本根本就分不清哪個是組件,哪個是公共組件,那些是第三方組件,很凌亂。
demo架構圖
目前demo中直接划分為4層,分別是apps,component,basecomponent,baselib.
apps:主要包括宿主殼,調試殼。宿主殼和調試殼的本質是一樣的,只是宿主殼是針對於所有的組件。而調試殼針對的某一個或幾個組件。該層是不能有一點java代碼,就只有一個AndroidManifest 和build.gradle
component:組件層,項目拆分出來的組件,全部放到該層中。這些組件都能夠獨立的運行小項目,組件之間不能有一點點的依賴關系。
basecomponent:該層主要放一些公共的組件,比如說多個組件共用某一個頁面等。還可以放一些公共類,比如BaseActivity,BaseFragment等。具體在項目中放什么,取決於自己項目需要。但是要注意的是,千萬不要什么都放,不然很容易造成該層的代碼臃腫。放到該層的代碼最好能夠有人員review。
baselib:這一層主要放的是一些第三方的東西,比如說我們封裝的網絡請求,數據庫,多媒體等等。
組件的創建
新組件的來源基本來自於兩方面:第一:由於舊的組件過於臃腫,從舊組件中繼續拆分出來形成新的組件。第二:新的功能作為一個新的組件添加進來。
無論是那種形式創建組件,在加入新的組件之前一定要明確該組件是做什么,是否需要與原有組件交互,是否存在與原有組件共用部分。其次拆分的粒度一定要保持好不宜太大,也不宜太小,具體的需要根據項目和開發人員決定。
新組件中架構的選擇:目前比較比較常用的基本就是MVC,MVP,MVVM這三種,具體選擇哪種取決於個人,我個人還是比較推薦MVVM的。為什么呢?
大家有沒有發現,很多人升級了Android studio以后(具體從哪個版本開始,我也忘記了,我的是直接3.0 - > 3.5)很多東西都變成了AndroidX了,而這個AndroidX就是Google 提出的JetPack中一部分,同時還提供了LiveData + ViewModel + DataBinding來讓我們快速的實現這個MVVM架構,同時數據的生命周期也就是ViewModel的生命周期和activity的生命周期系統直接幫我們處理好了,這對於我們來說便利了許多。
當然有些同學就是喜歡presenter,也是可以使用MVP的,這里推薦一個插件給大家,在創建MVP架構是事半功倍。GitHub地址:https://github.com/JessYanCoding/MVPArms/blob/master/MVPArms.md
在demo中,組件component_one 中實現一個一個mvp的例子,在component_two中實現額一個MVVP的例子,有需要的可以參考。
組件化開發
組件化開發往往會有多個開發人員,每個人的編碼風格,命名風格都是不一樣的,所以最好能夠統一。很多人都知道阿里巴巴推出了一份編碼格式的文檔,但是我覺得沒幾個人去看,看了也記不住。所以這里推薦一個插件給大家:Alibaba Java Coding Guidelines 。AS上直接File -- > Settings -- > Plugins 直接進行搜索安裝。這樣在編碼過程中哪里不合理,都會有提示出來。
代碼隔離
前面說的,組件之間是相互隔離,都是相互獨立的一個個體,組件A的存在與不存在都不能夠影響到組件B,組件在單獨調試中,即使訪問不了其他組件,那也不能閃退,所以說這個邊界一定要處理好。
組件單獨調試
現在網上很多文章都是通過這樣的形式來切換庫模式和調試模式的。
在gradle.properties 中定義一個常量 isDebug ,true 為調試模式,false為庫模式,然后通過這個常量在各個build.gradle中進行判斷
if (isDebug.toBoolean()) {
apply plugin: 'com.android.application'
} else {
apply plugin: 'com.android.library'
}
sourceSets {
main {
if (isDebug.toBoolean()) {
manifest.srcFile 'src/main/manifest/AndroidManifest.xml'
} else {
manifest.srcFile 'src/main/AndroidManifest.xml'
}
}
}
基本都是這樣寫的,這樣寫也是沒有問題的,demo的前期也試着這樣寫,但是后面發生的問題實在是多的不想解決。
第一:在調試模式下開發,如果使用了ButterKnife等第三方庫注解來替代findViewById(),,換成apply plugin: 'com.android.library' 模式下,這些都是編譯不過的,還得修改。
第二:一個組件要維護兩份AndroidManifest文件,這個代價非常的大,稍有出錯,那排查也是很麻煩的事情
所以通過這種方式進行切換模式在小一點的項目還行得通,但是在大的項目中,根本就不行。在demo中,只有一種模式,就是apply plugin: 'com.android.library' 而我們可以通過給module添加一個殼來實現調試,這樣開發,基本就是調試和集成的時候都是library,出現問題,也能夠及時的處理,不會等到最后才發現。
在demo中,殼程序
殼程序中只有一個build.gradle和AndroidManifest兩個文件,在AndroidManifest只需要添加一個入口,而所有的殼程序都是 apply plugin: 'com.android.application' 這樣在組件還是library的模式下一樣可以對組件進行調試。
這樣做的好處就是可以百分百的在library模式下開發組件,不會出現切換模式時所產生的一系列問題。可靠性大大的增強。
多組件調試(組件按需依賴)
在demo中每個殼程序的build.gradle
dependencies {
implementation project(path: ':component:app')
implementation project(path: ':component:component_one')
implementation project(path: ':component:component_two')
}
需要調試幾個組件就依賴幾個組件。
UI跳轉
demo中,組件之間是獨立的,他們之間的跳轉只要是通過ARouter 來進行頁面之間的跳轉的。
Android原生已經支持AndroidManifest去管理App跳轉,為什么還需要這個路由來進行跳轉呢?
我個人理解是這樣的
第一:使用顯示intent 進行跳轉,會導致組件之間的耦合很嚴重,背離了我們組件化的目的了
第二:使用隱式跳轉,可以是可以,但是並不是特別符合組件化開發,由於需要在AndroidManifest進行注冊,管理起來比較麻煩,出了問題,比較難排查,在加上每個加上了Scheme的Activity都可以被打開,會存在一定風險。
第三:使用路由有降級功能,就是說即使頁面不存在,也可以跳轉到到另外一個頁面做一些提示,用戶體驗更好
第四:路由有攔截功能,可以在跳轉之前做一些判斷,從而決定是否需要攔截。減少了不必要的代碼編寫。
ARouter
具體可以參考官方文檔:https://github.com/alibaba/ARouter/blob/master/README_CN.md
ARouter的原理:
1、通過apt技術,利用反射注解編譯生成類,封裝目標界面的類信息。
2、初始化時,把編譯生成的類通過key-value的方式存儲到ARouter中。
3、操着着通過key拿到對應的類信息。
4、把操作者的信息和目標界面信息進行結合關聯起來,實現跳轉。
組件的生命周期
組件之間是相互獨立的,每一個單獨的組件都是可以獨立運行的。而一個應用application只有一個,如果在每一個組件都設置一個application,顯然是不合理的。但是如果組件單獨運行,需要初始化一些第三方組件的東西,該怎么弄呢?在demo中,主要是通過反射的形式。
我們在每一個組件的AndroidManifest文件中添加
<meta-data
android:name="com.noahedu.component_one.ComponentOneApp"
android:value="ModuleConfig" />
然后通過getPackageManager().getApplicationInfo 來獲取到meta-data 值,在通過反射拿到對應的實例對象,回調對應的接口。具體可以參考basecomponent_common中的appproxy包下的實現方式。當然,現在網上也有很多通過注解的形式能夠自動注入的形式實現,看自己的選擇了。
組件間通信
組件間通信的方式有很多,比如SharedPreferences ,文件,廣播,EventBus,AIDL這些都是可以的。但是SharedPreferences ,文件 在線程安全方面並不好,同時改,同時取,很容易出現問題。
現在網上也有很多方案用的是EventBus來進行數據傳遞,EventBus有一個不太好的地方就是有點難找訂閱方寫在哪里。當然主要看自己項目怎么選擇。如果選擇使用EventBus來說為數據傳遞,那么這里推薦一個插件 Eventbus3 Intellij Plugin 這個插件可以快速定位出發送方和訂閱方位置。
demo中使用的方案是將接口下層到base層
定義接口
public interface IGetComponOneDataService {
void setData(String str);
String getData();
}
public class ServiceFactory {
private static ServiceFactory instace = null;
private IGetComponOneDataService iGetComponOneDataService;
public static ServiceFactory getInstance(){
if(null == instace){
synchronized (ServiceFactory.class){
if(null == instace){
instace = new ServiceFactory();
}
}
}
return instace;
}
public void setService(IGetComponOneDataService service){
this.iGetComponOneDataService = service;
}
public IGetComponOneDataService getiGetComponOneDataService(){
return iGetComponOneDataService;
}
}
然后各個有需要的組件可以實現IGetComponOneDataService 接口,通過暴露出來的接口來獲取數據。
這種方案如果維護不好很容易導致代碼臃腫,什么代碼都往下沉,所以選擇這種方案,需要合理的review
資源隔離
組件之間都是不同的開發人員維護,就很容易在資源上命名出現相同的,最后合包的時候就出現沖突。所以在命名上面就要有一個規則。
給 Module 內的資源名增加前綴, 避免資源名沖突
resourcePrefix "${project.name.toLowerCase().replaceAll("-", "_")}_"
相當於在各個組件命名中加一個前綴,如果沒有添加,則會有提示出來。這樣就避免了資源沖突的問題了。
組件間依賴
gradle升級到3.0+ 以后,依賴就是使用implementation,api ,runtionOnly,compileOnly 進行依賴控制了
implementtation 短依賴。比如依賴module A 那么就是只能用到module A里面的東西。module A 依賴其他的第三方都不能使用。
api 長依賴。和implementation相反,依賴了module A ,那么module A依賴的第三方也都能使用。
runtimeOnly :只有大包成apk的時候才會被大包進去。
compileOnly: 只有在本地編譯時有效,大包不會打進去。
組件維護
組件維護
隨着項目需求的增加,module會越來越來,對於開發人員來說,維護起來會越來越困難,特別是人手不足的情況下,所以說針對組件的划分,一定要合理。太大,那沒有意義,太小,成本也大。所以粒度要合理。從需求上來看,如果某個需求相對獨立,和已有的需求關聯不大,那么完全可以作為一個單獨組件。具體需要從需求上分析。
apk體積的優化
參考:https://blog.csdn.net/cchp1234/article/details/77750428
原因:
1、屏幕適配問題,增加多種資源文件和圖片
2、機型適配問題
3、Android 版本兼容問題
4、各種開發框架,第三方lib引入
5、炫酷的UI、UE效果
6、冗余代碼等
assets 優化
1、字體文件:可以使用字體資源文件編輯神器Glyphs進行壓縮刪除不需要的字符從而減少APK的大小。
2、WEB頁面:可以考慮使用7zip壓縮工具對該文件進行壓縮,在正式使用的時候解壓。
3、某些圖片:可以使用tinypng進行圖片壓縮, 目前tinypng已經支持png和jpg圖片、.9圖的壓縮。
4、將緩存文件放到服務端,通過網絡下載。
5、對無用的音頻文件刪除。
res優化
1、對於一些不必要的設備尺寸,不必要全部(主要看產品需求);
2、對資源文件,主要是圖片資源進行壓縮,壓縮工具是 ImageOptim;
3、一些UI效果可以使用代碼渲染替代圖片資源;
4、資源文件的復用,比如兩個頁面的背景圖片相同底色不同,就可以復用背景圖片,設置不同的底色;
5、使用 VectorDrawable 和 SVG 圖片來替換原有圖片。如果提升 minSdkVersion 效果會更好,minSdkVersion 21 以上直接使用矢量圖,可以不用為了兼容低版本而另外生成 .png 圖片文件。使用SVG不用考慮屏幕適配問題,體積非常小;
6、如果raw文件夾下有音頻文件,盡量不要使用無損的音頻格式,比如wav。可以考慮相比於mp3同等質量但文件更小的opus音頻格式。
7、能不用圖片的就不用圖片,可以使用shape代碼實現。
8、使用 WEBP。較大 png、jpg文件轉化為 webp 文件,這個 AS 自帶,在圖片(單個)或包含圖片的文件夾(批量)上右擊選擇 Convert to WebP 即可。Webp 無損圖片比 PNG 圖片的 size 小 26%。Webp 有損圖片在同等 SSIM(結構化相似)質量下比 JPEG 小 25-34% 。無損Webp支持透明度(透明通道)只占22%額外的字節。如果可以接受有損RGB壓縮,有損Webp也支持透明度,通常比PNG文件size小3倍。
幀動畫優化
一個幀動畫會有多張圖片組成,那么很容易導致apk的體積增大。這里推薦使用SVGA來展示動畫。
SVGA具體使用參考:https://github.com/svga/SVGAPlayer-Android/blob/master/readme.zh.md
Heading
強烈推薦:利用AndResGuard資源壓縮打包工具 這個打包工具和java代碼混淆有點類似 只是AndResGuard 混淆的是資源文件而已
使用方法:https://github.com/shwenzhang/AndResGuard
lib目錄優化
1、減少不必要的.so文件。比如一些第三方SDK要求引入.so文件,通常還很大。
2、選擇性刪除 arm64-v8a、armeabi-v7a、armeabi、x86下的so文件:
mips / mips64: 極少用於手機可以忽略
x86 / x86_64: x86 架構的手機都會包含由 Intel 提供的稱為 Houdini 的指令集動態轉碼工具,實現對 arm.so 的兼容,再考慮 x86 1% 以下的市場占有率,x86 相關的 .so 也是可以忽略的
armeabi: ARM v5 這是相當老舊的一個版本,缺少對浮點數計算的硬件支持,在需要大量計算時有性能瓶頸
armeabi-v7a: ARM v7 目前主流版本
arm64-v8a: 64 位支持。注意:arm64-v8a是可以向下兼容的,但前提是你的項目里面沒有arm64-v8a的文件夾。如果你有兩個文件夾armeabi和arm64-v8a兩個文件夾,armeabi里面有a.so 和 b.so,arm64-v8a里面只有a.so,那么arm64-v8a的手機在用到b的時候發現有arm64-v8a的文件夾,發現里面沒有b.so,就報錯了,所以這個時候刪掉arm64-v8a文件夾,這個時候手機發現沒有適配arm64-v8a,就會直接去找armeabi的so庫,所以要么你別加arm64-v8a,要么armeabi里面有的so庫,arm64-v8a里面也必須有。
最終解決:只用armeabi,其余像 mips, x86, armeabi-v7a, arm64-v8a都刪掉。手機發現沒有適配arm64-v8a,就會直接去找armeabi的 so 庫。
其他
對於其他無用文件,代碼,及時清理.......................