前言:
上一次真正更新內容還是在年初,想想在新的公司已經大半年了。在脫離只做Android上層需求的局限后,這半年期間遇到了很多問題也學習到了很多不一樣的東西。比如沙盒技術、Hook、自定義反射Task、反編譯、研究系統源碼解決沙盒bug等。今天就來談一下Android混淆以及自定義gradle Task的相關知識,帶您體驗不一樣的,更加完備(包括四大組件、native)的混淆知識。
說到Android的混淆,隨便百度都是一抓一大把.比如怎么開啟混淆,怎么做到代碼和資源的混淆或是包括一些諸如 -Keep、 -keepclassmembers、 -keepclasseswithmembers、dontwarn等混淆命令。本人覺得這些都是一個Android程序員必須要掌握的基本技能。
但是你是否有想過,為什么目前不需要對四大組件做keep 系統都默認不會去混淆呢? 為什么我沒有對native相關的代碼做keep,系統同樣會幫我們做避免混淆的操作呢?
開始研究前,我們先引入R8混淆編譯器的東西。
Android Gradle插件升級至3.4.0版本之后,帶來一個新特性-新一代混淆工具R8,做為D8的升級版替代Proguard;在應用壓縮、應用優化方面提供更極致的體驗。
R8 一步到位地完成了所有的縮減(shrinking),去糖(desugaring)和 轉換成 Dalvik 字節碼(dexing )過程。
縮減(shrinking)過程實現以下三個重要的功能:
代碼縮減:從應用及其庫依賴項中檢測并安全地移除未使用的類、字段、方法和屬性。
資源縮減:從封裝應用中移除不使用的資源,包括應用庫依賴項中的不使用的資源。
優化:檢查并重寫代碼,以進一步減小應用的 DEX 文件的大小。
混淆:縮短類和成員的名稱,從而減小 DEX 文件的大小。
?R8 和當前的代碼縮減解決方案 Proguard 相比,R8 可以更快地縮減代碼,同時改善輸出大小。
轉至:https://blog.csdn.net/fitaotao/article/details/115083963
簡單來說R8就是Proguard的升級版。
1、四大組件混淆
好了,暫時忘記這個概念。我們來做一個簡單的需求:去除四大組件的混淆。
對于這個需求也許你會有個疑問?我們正常開發不是都是對四大組件做keep操作嗎?為什么要去除呢?如果去除了,那編譯出的apk,pms在解析AndroidnddManifest.xml的時候不就認不出四大組件的名字了嗎?
沒錯,混淆后pms在解析的時候確實會出問題,導致app閃退。Android的混淆確實這點沒做好,就是不會在混淆的同時去同步的把AndroidnddManifest.xml的類名改掉,所以這個需要我們在合適的編譯點創建任務根據mapping混淆表去修改對應的四大組件類名。
OK,去除四大組件的混淆,也許你直接就把四大組件的相關keep刪除,本以為大功告成卻發現為什么明明刪除了配置,四大組件還是沒有達到混淆效果呢?
研究這個問題前,我們執行下打包任務,直接看最后幾個任務
Task :module-api:mergeReleaseJavaResource
Task :module-api:transformClassesAndResourcesWithR8ForRelease
Task :module-api:transformClassesAndResourcesWithSyncLibJarsForRelease
Task :module-api:bundleReleaseAar
Task :module-api:reBundleAarRelease
其中mergeReleaseJavaResource之后系統已經生成了aapt-rules.txt文件(build\intermediates\proguard-rules\release\aapt_rules.txt)
點進去你會驚奇的發現,里面內容是一個混淆表,其中左邊的類名都是四大組件的名字,右邊轉成的名字還是原來的名字,聰明的你或許會有個猜想:難道四大組件的混淆在這里配置的嗎?
沒錯,實際上aapt_rules.txt會去解析AndroidManifest.xml中關于四大組件的類名,aapt_rules.txt所keep住的所有內容都將會添加到最后的混淆配置中,即使你不寫keep配置,系統仍然默認幫你避免這些混淆。
所以這個需求的關鍵點
1、在編譯時候的mergeReleaseJavaResource任務后清空aapt_rules.txt文件的內容,
2、在transformClassesAndResourcesWithR8ForRelease(生成mapping文件)解析混淆表的內容,存儲到map中,然后替換編譯后的(build\intermediates\library_manifest\release\AndroidManifest.xml)AndroidManifest.xml文件中的四大組件的名字。
下面展示下代碼實現
1,清空
open class DontKeepDefaultRulesTask : ProguardDefaultTask() {
private val nativeKeep = "-keepclasseswithmembernames class * {\n" +
" native <methods>;\n" +
"}"
@TaskAction
fun delete() {
doTask {
val aaptFile = File(project.buildDir, "intermediates/proguard-rules/$buildType/aapt_rules.txt")
if (aaptFile.exists()) {
aaptFile.writeText("")
}
//刪除系統默認的 native keep規則
val defaultFile = File(project.buildDir, "intermediates/proguard-files")
defaultFile.listFiles()?.forEach {
var content = it.readText()
content = content.replace(nativeKeep,"")
it.writeText(content)
}
}
}
}
創建任務,到對應的文件中清空內容(忽略下面的native的混淆)
val mergeJavaResource = taskContainer.findByName("merge${buildType}JavaResource")
//該任務后刪除aapt_rules.txt
val aaptDeleteTask = taskContainer.create("AaptRulesDeleteFor$buildType", DontKeepDefaultRulesTask::class.java)
aaptDeleteTask.buildType = buildType
mergeJavaResource?.finalizedBy(aaptDeleteTask)
該任務在mergeReleaseJavaResource后執行
2、替換
open class ManifestRebuildTask : ProguardDefaultTask() {
@TaskAction
fun rebuild() {
doTask {
val mappingFile = File(project.buildDir, "outputs/mapping/$buildType/mapping.txt")
if (!mappingFile.exists()) {
println("mapping file not exits")
return@doTask
}
val map = MappingParsingUtil.parsing(mappingFile)
val manifestFile = File(project.buildDir, "intermediates/library_manifest/$buildType/AndroidManifest.xml")
if (!manifestFile.exists()) {
println("androidManifest file not exits")
return@doTask
}
var content = manifestFile.readText()
content = content.replace("\$", "inner")
map.forEach {
val keyStr = it.key.replace("\$", "inner")
val valueStr = it.value.replace("\$", "inner")
content = content.replace(keyStr, valueStr)
}
content = content.replace("inner", "\$")
manifestFile.writeText(content)
}
}
}
思路:在transformClassesAndResourcesWithR8ForRelease后獲取mapping文件并解析到map臨時變量中,同時替換AndroidManifest.xml中相關的類名(inner的替換操作是因為 java中 $符號比較特殊,他是內部類符號,同時也是正則表達式表示匹配字符串的結尾)
該任務在transformClassesAndResourcesWithR8ForRelease后執行
val transformClassesAndResourcesWithR8Task = taskContainer.findByName("transformClassesAndResourcesWithR8For$buildType")
//該任務后修改androidManifest.xml
val manifestRebuildTask = taskContainer.create("ManifestRebuildFor$buildType", ManifestRebuildTask::class.java)
manifestRebuildTask.buildType = buildType
transformClassesAndResourcesWithR8Task?.finalizedBy(manifestRebuildTask)
2、Native相關的混淆
同四大組件一樣,網上在講到native混淆配置的時候通常要加一句
-keepclasseswithmembernames class * {
native <methods>;
}
保持native的方法不被混淆,否則c端就沒法認識你的類名和方法名了。
但是我想說真正嚴謹的混淆是需要你在Android端做類名及方法名的混淆的,雖然c端肯定有自己的操作對so進行混淆。
或許你此時的處理方式也是毫不猶豫的去除上面的keep配置,然后重新編譯,不出意外同樣沒有任何效果。此時你的心情或許如下圖
不急,有了上面的經驗或許你會想到:是不是系統又對這塊做了默認操作呢?
坑爹,原來又是系統搞的鬼。不過有了之前去除四大組件混淆的默認操作,同樣我們也可以在mergeReleaseJavaResource任務后遍歷三個文件,去除這句配置。(代碼往上翻)
好了,去除默認的keep操作有了,那么問題來了。不管系統最后會混淆成上面字母,那么c端的開發者要怎么知道到底改成什么名字呢?
對于四大組件,Android有默認的AndroidnddManifest.xml可修改操作,讓系統知道我們改后的名字,從而不至于導致pms解析失敗,那native要怎么辦呢?
不慌,方法都是相通的。同樣,我們可以和c端規定一份頭文件,把類名及方法名混淆后的名字改在該文件中,在編譯他們的native任務的時候,c端會先去引入修改后的改頭文件,從而拿到混淆后的名字。
但是問題來了,這個任務明顯是要在native的任務之前(preReleaseBuild),此時mapping文件根本都還沒生成,連我們自己都不知道最終生成的名字,還怎么給他們改呢?
我們換一種思路,能不能事先給他們規定成想要的名字呢?
我們知道混淆規則中可以添加一條applyMapping的命令,該命令可以做到增量添加mapping的內容,此時剛好達到我們想要的結果。那么接下來,我們只需在preReleaseBuild命令前加一個生成native.mapping文件并填充內容的任務即可
open class ProguardFileCreateTask : ProguardDefaultTask() {
var isMinifyEnabled = false
@TaskAction
fun create() {
doTask {
//創建 NamedGen頭文件
val copyFile = File(project.rootDir, "library-native${File.separator}src${File.separator}main${File.separator}cpp${File.separator}NamedGen.h")
val random = getRandomLetter(3)
//創建native.mapping文件用于指定native方法的混淆并修改復制的模板文件
val nativeMappingFile = File(project.projectDir, "native.mapping")
//..\mxxxx_PluginGame\library-native\src\main\cpp\Named.h
val nameHFile = File(project.rootDir, "library-native${File.separator}src${File.separator}main${File.separator}cpp${File.separator}Named.h")
val fileStringBuffer = StringBuffer()
val mappingStringBuffer = StringBuffer()
if (isMinifyEnabled) {
nameHFile.readLines().forEach { line ->
when {
line.startsWith("O_CLASS") -> {
//O_CLASS(com___mxxxx___library_native___NativeUtils, com/mxxxx/library_native/NativeUtils);
val randomClassName = "${random}/${getRandomLetter(2)}"
fileStringBuffer.append("O_CLASS(com___mxxxx___library_native___NativeUtils, $randomClassName)\n")
mappingStringBuffer.append("com.mxxxx.library_native.NativeUtils -> ${randomClassName.replace("/", ".")}:\n")
}
line.startsWith("O_FUNC") -> {
//O_FUNC(boolean, nativeHookOpenDexFileNative(java.lang.reflect.Method), nativeHookOpenDexFileNative)
val randomFunName = getRandomLetter(2)
val tempStr = line.replace(line.substring(line.lastIndexOf(",") + 1, line.lastIndexOf(")") + 1), "${randomFunName})")
fileStringBuffer.append("${tempStr}\n")
mappingStringBuffer.append("\t${resolveFuncName(line, randomFunName)}\n")
}
else -> {
fileStringBuffer.append("${line}\n")
}
}
}
copyFile.writeText(fileStringBuffer.toString())
nativeMappingFile.writeText(mappingStringBuffer.toString())
} else {
copyFile.writeText(nameHFile.readText())
mappingStringBuffer.append("com.mxxxx.library_native.NativeUtils -> com.mxxxx.library_native.NativeUtils:")
}
}
}
}
對應的混淆規則
總結
最后說一下,本文的干貨除了分析混淆規則外還有就是關于Gradle插件相關的內容,另外我們開發的Android項目都會引入一個com.android.tools.build:gradle.xxxx,每個版本都會自帶一個默認的R8編譯器的版本,經過測試自帶的1.5.69版本存在很多問題,比如混淆后的文件有些kt的伴生對象找不到,原因是@MetaData的注解莫名被刪除,導致我又創建了一個修改字節碼的任務(利用javaassit將每個類頭部的注解@MetaData去除,使它變成java類),諸如此類的問題還很多,最終利用r8jar的命令去測試,發現2.0.100是最終的版本,越高的版本貌似去除了apply mapping的依賴,換句話說就是不管你加不加-applymapping,他都不起作用。
除了以上兩大需求,其實也可以創建動態生成混淆配置文件(proguard-rules.pro)的任務以及動態生成壓縮后的代碼包字母等,感興趣的可以自己研究自定義Gradle插件的相關知識。