Mess :https://github.com/JackCho/Mess
Mess介紹
眾所周知,我們開混淆打包后生成的apk里寞冯,Activity享幽、自定義View啄寡、Service等出現(xiàn)在xml里的相關Java類默認都會被keep住,那么這對于app的保護是不足夠好的串纺,Mess就是來解決這個問題丽旅,把即使出現(xiàn)在xml文件中的Java類照樣混淆椰棘。
使用
dependencies {
...
classpath 'me.ele:mess-plugin:1.0.1'
}
apply plugin: 'com.android.library'
apply plugin: 'me.ele.mess'
此外,Mess還提供一個可選配置魔招,ignoreProguard晰搀,由于有些依賴庫本身也配置了相關混淆配置,如com.android.support:recyclerview-v7办斑、com.jakewharton:butterknife等外恕,那么這些文件都將會被添加到proguardFiles中,導致依賴庫無法被混淆乡翅,所以ignoreProguard配置就是來解決這個問題的鳞疲。
比如忽視com.android.support:recyclerview-v7的混淆配置文件,則直接
mess {
ignoreProguard 'com.android.support:recyclerview-v7'
}
實現(xiàn)原理
先來看看Android gradle plugin在構建時最后所走的幾個task:
:app:processReleaseResources
...
:app:transformClassesAndResourcesWithProguardForRelease
:app:transformClassesWithDexForRelease
:app:transformClassesWithShrinkResForRelease
:app:mergeReleaseJniLibFolders
:app:transformNative_libsWithMergeJniLibsForRelease
:app:validateDebugSigning
:app:packageRelease
:app:zipalignRelease
:app:assembleRelease
其中有幾個關鍵性的task蠕蚜,可以看到:app:transformClassesAndResourcesWithProguardForRelease是走在:app:packageRelease之前的尚洽,那么我們就在打包前對混淆的task做些操作來實現(xiàn)我們的目的。
- hook transformClassesAndResourcesWithProguardFor${variant.name.capitalize()}
- hook ProcessAndroidResources Task靶累,將生成的aapt_rules.txt中內(nèi)容清空
- 如果需要混淆依賴庫腺毫,則刪除依賴庫中的proguard.txt文件
- 遍歷一遍mapping.txt獲取所有Java類名的的映射關系得到一個Map
- 拿映射Map替換AndroidManifest.xml里的Java原類名
- 拿映射Map替換layout、menu和value文件夾下的xml的Java原類名
- 重新跑ProcessAndroidResources Task
- 恢復之前刪除依賴庫中的proguard.txt文件
以上就是Mess干的關鍵性的東西挣柬,接下來依次說明潮酒。
hook transformClassesAndResourcesWithProguardFor${variant.name}
這個task是處理類和資源混淆的,也是我們的突破口邪蛔,Mess中大部分自定義task都是圍繞在這個task執(zhí)行的急黎,之后會有詳解。
hook ProcessAndroidResources Task侧到,將生成的aapt_rules.txt中內(nèi)容清空
這一步是雖說只是把aapt_rules.txt文件中的內(nèi)容清空勃教,但是確實Mess Plugin能成功的最關鍵的一步。
ProcessAndroidResources task會生成一個aapt_rules.txt匠抗,可見源碼ProcessAndroidResources.groovy故源,aapt_rules.txt里會keep住我們在xml里所書寫的那些Activity、自定義View等Java類名部分汞贸,還可以看到JackTask.java里的相關代碼:
if (config.isMinifyEnabled()) {
ConventionMappingHelper.map(jackTask, "proguardFiles", new Callable<List<File>>() {
@Override
public List<File> call() throws Exception {
// since all the output use the same resources, we can use the first output
// to query for a proguard file.
File sdkDir = scope.getGlobalScope().getSdkHandler().getAndCheckSdkFolder();
File defaultProguardFile = new File(sdkDir,
SdkConstants.FD_TOOLS + File.separatorChar
+ SdkConstants.FD_PROGUARD + File.separatorChar
+ TaskManager.DEFAULT_PROGUARD_CONFIG_FILE);
List<File> proguardFiles = config.getProguardFiles(true /*includeLibs*/,
ImmutableList.of(defaultProguardFile));
File proguardResFile = scope.getProcessAndroidResourcesProguardOutputFile();
proguardFiles.add(proguardResFile);
// for tested app, we only care about their aapt config since the base
// configs are the same files anyway.
if (scope.getTestedVariantData() != null) {
proguardResFile = scope.getTestedVariantData().getScope()
.getProcessAndroidResourcesProguardOutputFile();
proguardFiles.add(proguardResFile);
}
return proguardFiles;
}
});
jackTask.mappingFile = new File(scope.getProguardOutputFolder(), "mapping.txt");
}
其中getProcessAndroidResourcesProguardOutputFile方法所對應的文件就是我們所需要清空的aapt_rules.txt绳军,可以在VariantScope.java中查看。
@NonNull
public File getProcessAndroidResourcesProguardOutputFile() {
return new File(globalScope.getIntermediatesDir(),
"/proguard-rules/" + getVariantConfiguration().getDirName() + "/aapt_rules.txt");
}
很明顯著蛙,aapt_rules.txt所keep住的所有內(nèi)容都將會添加到最后的混淆配置中删铃,因此,我們需要在ProcessAndroidResources這個Task執(zhí)行之后清空aapt_rules.txt中的內(nèi)容踏堡,以保證編譯出的main.jar中的所有.class都是混淆后的猎唁。
相關代碼如下:
boolean hasProcessResourcesExecuted = false
output.processResources.doLast {
if (hasProcessResourcesExecuted) {
return
}
hasProcessResourcesExecuted = true
def rulesPath = "${project.buildDir.absolutePath}/intermediates/proguard-rules/${variant.dirName}/aapt_rules.txt"
File aaptRules = new File(rulesPath)
aaptRules.delete()
aaptRules << ""
}
如果需要混淆依賴庫,則刪除依賴庫中的proguard.txt文件
這一步就是刪除依賴庫中所保護的內(nèi)容,具體proguard.txt文件位于app目錄下/build/intermediates/exploded-aar/依賴庫maven名/proguard.txt诫隅。
Mess中直接將proguard.txt文件名最后加上~腐魂,如proguard.txt~蹬屹,在linux中表示備份纵揍,以便之后文件的恢復。
相關代碼如下:
public static void hideProguardTxt(Project project, String component) {
renameProguardTxt(project, component, 'proguard.txt', 'proguard.txt~')
}
public static void recoverProguardTxt(Project project, String component) {
renameProguardTxt(project, component, 'proguard.txt~', 'proguard.txt')
}
private static void renameProguardTxt(Project project, String component, String orgName,
String newName) {
MavenCoordinates mavenCoordinates = parseMavenString(component)
File bundlesDir = new File(project.buildDir, "intermediates/exploded-aar")
File bundleDir = new File(bundlesDir,
"${mavenCoordinates.groupId}/${mavenCoordinates.artifactId}")
if (!bundleDir.exists()) return
bundleDir.eachFileRecurse(FileType.FILES) { File f ->
if (f.name == orgName) {
File targetFile = new File(f.parentFile.absolutePath, newName)
println "rename file ${f.absolutePath} to ${targetFile.absolutePath}"
Files.move(f, targetFile)
}
}
}
遍歷一遍mapping.txt獲取所有Java類名的的映射關系得到一個Map
之前第一步已經(jīng)將生成的main.jar中所有的.class文件做相關混淆了滥搭,那么我們之前所在xml里寫的還是原來的Java類名豁生,因此兔毒,我們想要替換xml里的Java類名,就得先知道原先的類名被替換成什么了甸箱,這個時候就得依賴mapping.txt了育叁。
直接遍歷:
File mappingFile = apkVariant.mappingFile
println mappingFile.toString()
mappingFile.eachLine { line ->
//方法名的混淆前面是會有空格的,我們這里只需要拿類名的映射關系
if (!line.startsWith(" ")) {
// 如me.ele.mess.SecondActivity -> me.ele.mess.z:
// -> 作為分割符號
String[] keyValue = line.split("->")
// 原始文件名
String key = keyValue[0].trim()
// 混淆后文件名芍殖,去掉最后一個":"
String value = keyValue[1].subSequence(0, keyValue[1].length() - 1).trim()
// 添加進map
if (!key.equals(value)) {
map.put(key, value)
}
}
}
這樣后map里就存有所有類名的映射關系了豪嗽,但是有個小問題要注意,假如存在這種情況豌骏,me.ele.foo -> me.ele.a龟梦,me.ele.fooNew -> me.ele.b,也就是恰巧有類名是另一個類名的開始部分窃躲,那么這樣對我們之后的替換是會有bug的计贰,會導致fooNew被替換成了aNew。因此框舔,拿到map后需要對map做一次原類名長度的降序排序(也就是map中的key)蹦玫,以避免這個bug發(fā)生赎婚。相關代碼如下:
public static Map<String, String> sortMapping(Map<String, String> map) {
List<Map.Entry<String, String>> list = new LinkedList<>(map.entrySet());
Collections.sort(list, new Comparator<Map.Entry<String, String>>() {
public int compare(Map.Entry<String, String> o1, Map.Entry<String, String> o2) {
return o2.key.length() - o1.key.length()
}
});
Map<String, String> result = new LinkedHashMap<>();
for (Iterator<Map.Entry<String, String>> it = list.iterator(); it.hasNext();) {
Map.Entry<String, String> entry = (Map.Entry<String, String>) it.next();
result.put(entry.getKey(), entry.getValue());
}
return result;
}
至此刘绣,一個正確的map已經(jīng)拿到,接下來就是靠這個map來對相關的xml文件做替換了挣输。
拿映射Map替換AndroidManifest.xml里的Java原類名
細心活纬凤,拿到AndroidManifest.xml一行一行讀取,匹配到相關字符串則進行替換撩嚼,但這里有個小坑停士,由于Java內(nèi)部類的類名是用$符號分割的,剛好它又是正則表達式表示匹配字符串的結尾完丽,因此對于內(nèi)部類恋技,我們應該現(xiàn)將$符號先替換成其他字符串,然后再做類名的替換逻族,Mess中是替換成inner蜻底,相關代碼如下:
File f = new File(path)
StringBuilder builder = new StringBuilder()
f.eachLine { line ->
//<me.ele.base.widget.LoadingViewPager -> <me.ele.aaa
// app:actionProviderClass="me.ele.base.ui.SearchViewProvider" -> app:actionProviderClass="me.ele.bbv"
if (line.contains("<${oldStr}") || line.contains("${oldStr}>") || line.contains("${oldStr}\"")) {
if (line.contains("\$") && oldStr.contains("\$")) {
oldStr = oldStr.replaceAll("\\\$", "inner")
line = line.replaceAll("\\\$", "inner").replaceAll(oldStr, newStr)
} else {
line = line.replaceAll(oldStr, newStr)
}
}
builder.append(line);
builder.append("\n")
}
f.delete()
f << builder.toString()
拿映射Map替換layout、menu和value文件夾下的xml的Java原類名
前一步已經(jīng)把AndroidManifest.xml中的對應Java類名替換了聘鳞,這一步就是替換layout薄辅、menu和value這三個文件夾下的xml內(nèi)容要拂,感謝groovy語法讓整件事情變得非常簡單。layout站楚、menu文件夾大家能立馬理解脱惰,那么value呢?其實就是behavior引入后才存在的窿春,所以value文件夾千萬別忽視拉一。
相關代碼如下:
File layoutDir = new File(getLayoutPath())
File menuDir = new File(getMenuPath())
File valueDir = new File(getValuePath())
[layoutDir, menuDir, valueDir].each {File dir ->
if (dir.exists()) {
dir.eachFileRecurse(FileType.FILES) { File file ->
String orgTxt = file.text
String newTxt = orgTxt
map.each { k, v ->
newTxt = newTxt.replace(k, v)
}
if (newTxt != orgTxt) {
println 'rewrite file: ' + file.absolutePath
file.text = newTxt
}
}
}
}
至此,整個工程的main.jar中的.class文件以及資源文件都替換成相互匹配的混淆后的名稱了旧乞。
重新跑ProcessAndroidResources Task
前些步驟hook后ProcessAndroidResources Task之后我們已經(jīng)把靜態(tài)的文件都替換好了舅踪,那么接下來就還得依靠Android gradle plugin的原有tasks了,于是乎我們重新執(zhí)行ProcessAndroidResources Task良蛮。
ProcessAndroidResources processTask = variantOutput.processResources
processTask.state.executed = false
processTask.execute()
恢復之前刪除依賴庫中的proguard.txt文件
有頭有尾抽碌。
尾語
想要寫出Mess這樣的plugin,對Android整個打包流程是要相當熟悉的决瞳,這樣才能知道什么時候該hook什么task货徙,平常開發(fā)過程中盡量不要直接點擊run按鈕,應該直接通過gradle assemble** 構建皮胡,這樣無數(shù)次的看構建過程中經(jīng)歷哪些task痴颊,然后去閱讀相關task源碼,這樣對整個打包流程才會越來越胸有成竹屡贺。
Mess有個小遺憾蠢棱,那就是ButterKnife這個庫在絕大多數(shù)app中都使用了,但是ButterKnife的混淆規(guī)則中有對使用注解的方法名和變量名做保護甩栈,這樣就比較尷尬了泻仙,會導致Mess對使用ButterKnife庫的app而言是沒多大作用的。
-keepclasseswithmembernames class * { @butterknife.* <methods>; }
-keepclasseswithmembernames class * { @butterknife.* <fields>; }
但是不要灰心量没,ButterMess這個Lib就來解決這個問題玉转,接下來會寫篇詳解ButterMess的文章,先放個ButterMess的鏈接:https://github.com/peacepassion/ButterMess