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