為什么要檢測圖片資源?
- 避免不小心把未壓縮,不合適的圖片資源打入apk中,造成apk過大
- 圖片打入apk前,可以自動化轉換,壓縮
實現思路
- 思路一:使用gradle在aapt編譯期,掃描匯總資源的文件夾,過濾出不符合要求的圖片資源,并拋出異常中斷編譯
- 思路二:是思路一的進階。還是在使用gradle在aapt編譯期,查找有沒有合適的gradle task,提供給我們遍歷所有資源的機會
gradle插件實現
gradle插件實現的基礎
簡單對gradle插件實現進行復習
插件搭建
- 新建一個模塊
- 配置好該模塊的上傳配置(mvn.gradle)
- 在build中,對gradleApi進行依賴
- scss復制代碼
- Apply plugin: 'kotlin' //插件如果使用kotlin實現,需要依賴kotlindependencies { implementation gradleApi() implementation localGroovy() implementation 'com.Android.tools.build:gradle:3.4.2'}
- 在mAIn下面新建resources.META-INF.gradle-plugins文件夾
- 在該文件夾中創建一個和module同名的.properties文件,在里面配置上你的插件入口類
- 例:
- arduino復制代碼
- implementation-class=com.xxx.checkbigimage.image.ImagePlugin
插件的基本實現
上面講到要配置一個入口類,這個入口類就是實現了Plugin接口的類,它有一個override fun apply(project: Project)方法,就是我們插件開始執行的地方,相當于main函數,參數project就是整個工程的配置文件
可以使用以下方法,從我們使用插件的地方獲取到對插件的配置
Python/ target=_blank class=infotextkey>Python復制代碼project.extensions.create("config", Config::class.JAVA)mConfig = project.property("config") as Config
Config是一個java bean數據類
"config"是我們在build中的配置名稱
這樣一個簡單gradle插件就實現了
圖片資源檢測插件實現
上面說了為什么要實現這樣一個插件和該如何實現一個gradle插件,那么下面就具體介紹該插件的實現過程
想要的功能
- 檢測和攔截功能
- 檢測是否有大小超標的圖片
- 檢測是否有寬高超標的圖片
- 攔截非webp資源,并進行提示
- 自動化壓縮
- 自動壓縮png,jpg等資源
- 白名單設置
- 一些統計功能
實現過程
上面已經說了gradle插件的實現,那么我們就從apply方法開始說起。
瞄準task掛鉤
既然是要hock android打包的編譯過程,那就要尋找android打包時,合適的task
想hock task,首先應該拿到任務task集合
在android插件編譯生成apk的過程中,有好多task都可以生成apk,它們的名字基于Build Types 和 Product Flavor 生成。那么我們怎么拿到具體生成apk的task組呢?
為了解決這個問題。android插件有幾個屬性,就是我們平常配置的變體(所謂的環境),androd中有三類變體
- applicationVariants(只適用于 app plugin)
- libraryVariants(只適用于 library plugin)
- testVariants(app、library plugin 均適用)
這三個對象都是實現了BaseVariant(BaseVariantImpl為實現這個接口的抽象類)接口的類的對象的集合
屬性名
屬性類型
說明
name
String
Variant 的名字,唯一
description
String
Variant 的描述說明
dirName
String
Variant 的子文件夾名,唯一。可能有不止一個子文件夾,例如 “debug/flavor1”
baseName
String
Variant 輸出的基礎名字,必須唯一
outputFile
File
Variant 的輸出,該屬性可讀可寫
processManifest
ProcessManifest
處理 Manifest 的 task
aidlCompile
AidlCompile
編譯 AIDL 文件的 task
renderscriptCompile
RenderscriptCompile
編譯 Renderscript 文件的 task
mergeResources
MergeResources
合并資源文件的 task
mergeAssets
MergeAssets
合并 assets 的 task
processResources
ProcessAndroidResources
處理并編譯資源文件的 task
generateBuildConfig
GenerateBuildConfig
生成 BuildConfig 類的 task
javaCompile
JavaCompile
編譯 Java 源代碼的 task
processJavaResources
Copy
處理 Java 資源的 task
assemble
DefaultTask
Variant 的標志性 assemble task
因為我們的插件應該可以應用在主工程或者模塊包上的,所以當我們插件運行后,我們要檢測當前使用我們插件的模塊是主工程,還是模塊包
kotlin復制代碼val hasAppPlugin = project.plugins.hasPlugin("com.android.application")val variants = if (hasAppPlugin) { (project.property("android") as AppExtension).applicationVariants} else { (project.property("android") as LibraryExtension).libraryVariants}
找到想要hock的任務
我們想hock住android插件運行的task任務,就需要一個重要的gradle回調
erlang復制代碼project.afterEvaluate{...}
afterEvaluate該方法就是整個gradle配置文件配置成功后的回調,證明此時配置已檢查完畢,所有task已經就緒,已經可以開始按指定順序運行task了,那么我就需要在這個回調里辦事!
Grade 執行順序
執行setting,檢測所有module,為每個模塊配置project
加載build.properties,生成task執行鏈表和配置
執行某個指定task,然后會先執行該task所依賴的task
配置完成后,開始遍歷variants中所有的變體
arduino復制代碼project.afterEvaluate { variants.all { variant -> ... }}
我們的目標task:mergeResourcesProvider
mergeResourcesProvider這個任務就是android插件合并所有module中資源的task,看名字就知道了。
我們可以從變體中獲取這個task對象
ini復制代碼val mergeResourcesTask = variant.mergeResourcesProvider.get()
那么,我們自己的任務呢?
gradle api提供給我們可以在代碼中生成task的方法
ini復制代碼val mcPicTask = project.task("CheckBigImage${variant.name.capitalize()}")
使用project.task("taskname")來生成一個我們自己需要執行的task
然后我們編寫這個task的邏輯,也是本插件的邏輯
復制代碼mcPicTask.doLast {...}
variant里面有各種對象,allRawAndroidResources恰好就是我們需要的。它只有3.3以上才會有。
ini復制代碼val dir = variant.allRawAndroidResources.files
這個dir對象,就是android所有文件資源的files集合
ok。讓我們遍歷這個文件list吧!
scss復制代碼for (channelDir: File in dir) {check(channelDir)}fun check(file: File) { if(file.isDirectory) { check(file)} else { process(file)}}
如果遇到文件夾,這里是一個遞歸調用。
如果遇到文件,就可以按照自己的規則處理了。
掛鉤mergeResourcesProvider
我們task寫好后,需要和mergeResourcesProvider掛鉤
less復制代碼mergeResourcesTask.dependsOn(project.tasks.findByName(mcPicTask.name))
使mergeResourcesTask依賴我們的mcPicTask,當mergeResourcesTask執行前,就會先執行我們的mcPicTask了!!
注意:此處直接使用mergeResourcesTask系統task依賴我們的task,我們的task執行順序會和mergeResourcesTask原有的依賴混雜在一起,不可控。后面講一種可控的方法
攔截圖片的邏輯
這個邏輯應該實現在上面偽代碼process(file:File)方法中
- 首先我們只需要處理圖片,所以對參數file進行首輪過濾,只留下后綴名為圖片的文件
- kotlin復制代碼
- fun isImage(file: File): Boolean { return (file.name.endsWith(Const.JPG) || file.name.endsWith(Const.PNG) || file.name.endsWith(Const.JPEG) || file.name.endsWith(Const.GIF) || file.name.endsWith(Const.WEB_P) ) && !file.name.endsWith(Const.DOT_9PNG)}
- 需要檢查圖片的寬高的話,可以使用java的原生api
- arduino復制代碼
- val sourceImg = ImageIO.read(FileInputStream(imgFile))if (sourceImg.height > maxHeight || sourceImg.width > maxWidth) { ...
- 需要過濾圖片大小的話
- lua復制代碼
- if (imgFile.length() >= maxSize) { LogUtil.log(SIZE_TAG, imgFile.path, true.toString()) return true}
壓縮圖片邏輯
這里我們只處理普通圖片轉換為webp的壓縮。jpg,png的自壓縮原理相同,就不復述了
想壓縮轉換webp圖片,需要用到轉換工具
google提供的有一套命令行轉換工具:cwebp ,各個平臺都有,我們去下載一套,放在我們的主工程文件夾下就可以了
這里需要注意的是:為了方便,如果把cwebp命令行程序放在環境變量下,那么執行命令時,拼接命令時,直接拼接cwebp就好。
如果使用工程目錄下的cwebp,執行前,需要在cwebp命令前面拼接它所在的工程目錄。
使用
lua復制代碼project.rootDir.path
可以獲取工程的根目錄
如何執行命令行程序呢?
可以使用java的api
scss復制代碼Runtime.getRuntime().exec(cmd)
現在可以愉快的轉換圖片了
bash復制代碼Tools.cmd("cwebp", "${imgFile.path} -o ${webpFile.path} -m 6 -quiet")
轉換后,記得把原圖刪掉
優化點:
有的圖片轉換后比以前還大,這里需要注意
第一次掃描過后的無法優化的圖片,可以存在一個text文本當中,第二次執行時,就不要去轉換了
系統兼容
在linux系統上,創建和刪除文件都需要權限,如果沒有權限就會失敗。這時需要先判斷當前的操作系統是不是linux,如果是,可以執行chmod 755 -R ${FileUtil.getRootDirPath()}添加權限
這里可以優化一下,在我們的mcPicTask前面再加一個task,用來添加權限,這個task只對文件夾進行遞歸添加就可以了,比一個一個文件要來的快。
因為我們不清楚系統的task(mergeResourcesTask)都依賴了哪些,那么如何在依賴上再加依賴,如何插入task呢?
gradle api提供給了我們一個方法,
xxx.taskDependencies.getDependencies(xxx)可以獲取自己的依賴樹
在這里就是
scss復制代碼(project.tasks.findByName(chmodTask.name) asTask).dependsOn(mergeResourcesTask.taskDependencies.getDependencies(mergeResourcesTask))
讓chmodTask依賴mergeResourcesTask的依賴。假如mergeResourcesTask是A,chmodTask是B。A依賴一個系統的C。那么上面的代碼就是讓B依賴了C。這時的task圖就是 B->C,A->C
接下來我們再把mcPicTask(簡稱為D)也依賴進來
arduino復制代碼(project.tasks.findByName(mcPicTask.name) as Task).dependsOn(project.tasks.findByName(chmodTask.name) as Task)
這時就是D->B->C,A->C
最后,回到我們剛剛攔截圖片的邏輯的最后代碼
less復制代碼mergeResourcesTask.dependsOn(project.tasks.findByName(mcPicTask.name))
就變成了A->D->B->C,也就是mergeResourcesTask->mcPicTask->chmodTask->原依賴task,依賴和執行順序是相反的。
正常的代碼就是
scss復制代碼(project.tasks.findByName(chmodTask.name) asTask).dependsOn(mergeResourcesTask.taskDependencies.getDependencies(mergeResourcesTask))(project.tasks.findByName(mcPicTask.name) as Task).dependsOn(project.tasks.findByName(chmodTask.name) as Task)mergeResourcesTask.dependsOn(project.tasks.findByName(mcPicTask.name))
Tips
直接使用
mergeResourcesTask.dependsOn(project.tasks.findByName(mcPicTask.name))插入task。執行順序打印
......
Task :app:mainApkListPersistenceDebug UP-TO-DATE
Task :app:CheckBigImageDebug
Task :app:generateDebugResValues UP-TO-DATE Task :app:generateDebugResources UP-TO-DATE Task :app:mergeDebugResources
......
而使用正規的插入法順序
Task :app:mainApkListPersistenceDebug UP-TO-DATE Task :app:generateDebugResValues UP-TO-DATE Task :app:generateDebugResources UP-TO-DATE Task :app:chmodDebug
Task :app:CheckBigImageDebug
Task :app:mergeDebugResources
gradle版本差異
我們上面的例子,都是基于比較最新的gradle和android gradle tools版本(>3.3),android插件直接提供給了我們allRawAndroidResources,方便無比,直接在merge前遍歷它就好了。
那么3.3之前的版本呢?就是我們最初的設想了,在合并完各個module資源后,掃描merge文件夾!這里又有aapt和aapt2的差異
方法一
關掉aapt2
ini復制代碼android.enableAapt2=false
在mergeDebugResources后,processDebugResources前掃描文件夾
前面說過,mergeDebugResources是合并所有module的資源文件到固定目錄
那么processDebugResources是什么呢?就是處理這些已經合并完成的文件,生成R.id,資源索引之類的文件
那么我們的任務就必須插入到processDebugResources前面,而不是mergeDebugResources了
方法二
仔細翻了翻MergeResources里面的方法,有一個getResSet和computeResourceSetList看起來有點意思。那么computeResourceSetList中又調用了getResSet。最后發現computeResourceSetList果然可以獲取所有文件列表。
less復制代碼/*** Computes the list of resource sets to be used during execution based all the inputs.*/@VisibleForTesting@NonNullList<ResourceSet> computeResourceSetList()
注釋也很有意思,有道翻譯一下:根據所有輸入計算執行期間使用的資源集列表。
鑒于該方法是友元方法,就使用反射獲取。
因為3.3之后,aapt2是強制開啟的,并且aapt2 merge后的文件不是原文件了哦!注意aapt1合并后,還是正常的xxx.png。aapt2合并后的文件擴展名為flat
所以,方法一不支持大于3.3的gradle版本。方法二支持。可以平滑過渡到新版本。鑒于新版本的gradle直接提供了allRawAndroidResources這樣的方法,所以在3.3以上,直接使用它就可以了
allRawAndroidResources和掃描合并文件夾的差異。
allRawAndroidResources提供的是未合并前的資源路徑
- 源碼依賴的module,編譯時,會獲取該文件的真實路徑
- aar依賴的路徑,會獲取到aar-cache的路徑
- 所以:如果開啟自動轉換webp功能你會發現:你本地源代碼中的png,都轉成了webp
掃描合并文件夾,掃描的是編譯期merge成功后的文件夾
- 不會影響源代碼
優化
- 已經掃描過的,且確認無法經過webp優化的圖片,把這些名稱寫入一個本地文件,優化掃描速度
未來想做的事情
統計
- 攔截了多少圖片
- 轉換了多少圖片
- 3. 統計各個模塊的圖片資源情況。在合適的時間進行預警