編輯:關於Android編程
然後我說了Nuwa有坑,有人就問Nuwa到底有哪些坑,這篇文章對自己在Nuwa上走過的坑做一個總結,如果你遇到了其他坑,歡迎留言,我會統一加到文章中去。當然有些也不算是Nuwa的坑,算是ClassLoader這種方式進行熱修復暴露出來的問題吧。
在不混淆的情況下,Nuwa在這一方面是沒有什麼問題的,但是一旦混淆了,有些類你不想讓他注入字節碼,它卻注入了,這是為什麼呢,原因是Nuwa處理的是混淆後的jar,混淆後的jar包名和類名發生了變化,你再使用配置進去的excludeClass是無法主動不進行字節碼注入處理的,除非你加進去的是混淆後的類名,但是在沒混淆前,我們是根本不知道混淆後的類名的,有人說,我可以先混淆一遍,混淆完了查看一下mapping文件,找到對應的混淆後的類名,加到excludeClass中去,可以是可以,難道你不覺得蛋疼嗎,而且這樣也很有可能出現差錯。那麼有沒有更好的方法呢?當然有。
混淆後在outputs目錄下會產生一個mapping.txt文件,我們能不能解析這個文件,將混淆後的類還原為原來的類名呢,這個文件的大致內容就像下面這樣。
android.support.graphics.drawable.AnimatedVectorDrawableCompat$1 -> android.support.a.a.c:
android.support.graphics.drawable.AnimatedVectorDrawableCompat this$0 -> a
629:629:void (android.support.graphics.drawable.AnimatedVectorDrawableCompat) ->
632:633:void invalidateDrawable(android.graphics.drawable.Drawable) -> invalidateDrawable
637:638:void scheduleDrawable(android.graphics.drawable.Drawable,java.lang.Runnable,long) -> scheduleDrawable
642:643:void unscheduleDrawable(android.graphics.drawable.Drawable,java.lang.Runnable) -> unscheduleDrawable
android.support.graphics.drawable.AnimatedVectorDrawableCompat$AnimatedVectorDrawableCompatState -> android.support.a.a.d:
int mChangingConfigurations -> a
android.support.graphics.drawable.VectorDrawableCompat mVectorDrawable -> b
java.util.ArrayList mAnimators -> c
android.support.v4.util.ArrayMap mTargetNameMap -> d
473:503:void (android.content.Context,android.support.graphics.drawable.AnimatedVectorDrawableCompat$AnimatedVectorDrawableCompatState,android.graphics.drawable.Drawable$Callback,android.content.res.Resources) ->
507:507:android.graphics.drawable.Drawable newDrawable() -> newDrawable
512:512:android.graphics.drawable.Drawable newDrawable(android.content.res.Resources) -> newDrawable
517:517:int getChangingConfigurations() -> getChangingConfigurations
android.support.graphics.drawable.AnimatedVectorDrawableCompat$AnimatedVectorDrawableDelegateState -> android.support.a.a.e:
android.graphics.drawable.Drawable$ConstantState mDelegateState -> a
424:426:void (android.graphics.drawable.Drawable$ConstantState) ->
430:434:android.graphics.drawable.Drawable newDrawable() -> newDrawable
439:443:android.graphics.drawable.Drawable newDrawable(android.content.res.Resources) -> newDrawable
448:452:android.graphics.drawable.Drawable newDrawable(android.content.res.Resources,android.content.res.Resources$Theme) -> newDrawable
457:457:boolean canApplyTheme() -> canApplyTheme
462:462:int getChangingConfigurations() -> getChangingConfigurations
仔細觀察一下,還是挺有規律的,第一行是原始類名對應的混淆類名,中間用->分割,之後是原始變量名對應的混淆後的變量名,還是用->分割,但是開頭縮進了四個空格。最後是方法的混淆,最前面是方法的行數,使用:分割,兩個數字分割後再跟一個:,後面就是原始方法名對應的混淆方法名,也是使用->分割。方法和變量都是有類型的。你是不是想到怎麼解析了,沒錯,正則表達式,別急著寫代碼,在寫代碼之前我們先看看有沒有造好的輪子可以用用,在github上搜一下proguard,沒結果。。。再換個關鍵字,retrace,為什麼是retrace呢,因為proguard自帶了一個腳本叫retrace,可以從混淆後的異常信息還原為原始類的異常信息。結果出來了,在code中選擇java,第一頁的最後一條就是。這裡我把這個倉庫fork到自己的倉庫中去了,見地址https://github.com/lizhangqu/retrace
當然不能完完全全的直接用,其實我們用得到的就三個類,一個是ClassMapping.java,一個是MethodMapping.java,還有一個是Retrace.java,至於如何改造,靠你自己了,源碼都擺在你面前了你還不會改造?改造後的結果就是傳入混淆後的全類名,返回原始的全類目,這樣跟excludeClass進行對比就能正確處理了。
沒有被修改的類被卻被打進了patch,為什麼?我們修改了一個復雜一點的類,准備打patch了,發現被打進patch的類怎樣不是一個,還包含了一大堆其他的類,為什麼呢?打修復包時利用正式包的mapping,修復bug,修改了原先的類,改變的類會改變,但有些類沒有改變也會因為混淆的關系產生變化(混淆會剔除一些無用的方法,打修復包時那些無用的方法可能會加上),這就造成了有些類沒有修改,但也會出現在修復包中。當然這種情況出現的概率還是挺大的,但是出現的類的數量就不一定了,有多了一個的,也有多了一坨的。。。。怎樣解決。。。無解,多就多了呗。。。最多也就是patch包大小變大了。只能盡量避免這種情況的發生,比如打修復包的時候不要修改原有的縮進,一不小心手賤重新進行格式化,可能原來的代碼沒有格式化,你這麼一格式化,整個類都發生了變化,包括這個類的內部類,這樣patch的類的數量就會爆增。所以打patch的時候應該盡可能的減少代碼的改動。
為什麼會出現這種現象?
出現這種現象的原因是Application類我們沒有引用hack.apk,為什麼不引用呢,因為在加載Application類之前我們還沒加載hack.apk,引用了就會報找不到類的異常,於是這個類不能打,並且在加載hack.apk前用的類都不能引用hack.apk。於是就導致了Application類被打上了那個標記進行了校驗。然後Application直接引用的類就無法打patch了,一打patch就會報那個異常Class ref in pre-verified class resolved to unexpected implementation
如何解決這個問題?
直接引用的類不能打patch,但是間接引用的可以打呀,把直接引用的類改成間接引用就ok了,怎麼做呢?新建一個中間類,比如PatchUtil,裡面有一個init方法,入參是Application,把原來在Application中的邏輯全都轉移到PatchUtil中去,然後Application引用PatchUtil類進行調用,最終將一大推直接引用的類變成了間接引用,同時PatchUtil變成了直接引用的類,於是原來一大坨不能打patch的類變成了一個類不能打patch,還是值得的。
注入失敗的原因是什麼(混淆和私有構造函數)?
如果代碼不混淆,字節碼注入是沒問題的,所以這個原因還是混淆導致的,混淆之後,很多類沒有了< init >,或者< init > 變成了 < clinit >,為什麼會這樣呢,我估計是剔除了無用方法導致的。還有一個特殊的情況就是私有構造函數,比如單例的情況下就存在只有一個私有構造函數,私有構造函數字節碼中沒有 < init >,甚至更絕,也沒有 < clinit >,這種情況是百分百注入不進去的,而Nuwa的邏輯是判斷name是不是等於< init >並且在構造函數的末尾。但是實際測試情況是絕大多數的類混淆之後字節碼中都沒有< init >或者< clinit >。
如何去解決注入失敗的問題?
能不能插一個成員變量呢,實際測試結果是不能。。。具體原因我也不清楚。那麼沒有構造函數就給它插一個構造函數,但是卻又不能顯示的插一個構造函數,因為這種情況也可能是有問題的,比如原來就有一個私有構造函數,你再插一個公有構造函數,肯定是有問題的,於是就演變成了給它插一段靜態初始化的代碼就可以了,在這段代碼中直接引用Hack.class。就像這樣子
static{
System.out.println(com.package.Hack.class);
}
至於這段代碼怎麼插。。。我表示用asm插我真的不會插,所以我把Nuwa插字節碼的那段代碼從使用asm插字節碼替換成了用javassist插,至於怎麼插,見後文。
如何修改注入的字節碼使其找不到類也不會報錯Nuwa原來的字節碼注入是在構造函數中注入一段這樣的代碼
System.out.println(Hack.class);
這段代碼有什麼問題呢,仔細用腦子想一下,萬一有些類在加載Hack.class之前就使用了,並且我們一不小心給他注入了這段代碼,那麼程序運行就會立馬crash,於是,我們想能不能不讓這段代碼執行呢,答案是可能的,通過一個if語句,讓它永遠進不去這個if語句就可以了,下面是一種方式,當然你完全可以使用其他類似的代碼
if(Boolean.FALSE.booleanValue()){
System.out.println(Hack.class)
}
這樣這段代碼就永遠不會被執行,即使提取使用了某個不應該使用的類,程序也不會crash,最多是控制台輸出一條log,說這個引用的類找不到。而實際測試結果是,即使報了這個log,也還是能打patch的。
public static signedApk(Logger logger, def variant, File apkFile) {
if (!apkFile.exists())
return;
def signingConfigs = variant.getSigningConfig()
if (signingConfigs == null) {
logger.error "no need to sign"
return;
}
def args = [JavaEnvUtils.getJdkExecutable('jarsigner'),
'-verbose',
'-sigalg', 'MD5withRSA',
'-digestalg', 'SHA1',
'-keystore', signingConfigs.storeFile,
'-keypass', signingConfigs.keyPassword,
'-storepass', signingConfigs.storePassword,
apkFile.absolutePath,
signingConfigs.keyAlias]
def proc = args.execute()
}
public static zipalign(Project project, File apkFile) {
if (apkFile.exists()) {
def sdkDir
Properties properties = new Properties()
File localProps = project.rootProject.file("local.properties")
if (localProps.exists()) {
properties.load(localProps.newDataInputStream())
sdkDir = properties.getProperty("sdk.dir")
} else {
sdkDir = System.getenv("ANDROID_HOME")
}
if (sdkDir) {
def cmdExt = Os.isFamily(Os.FAMILY_WINDOWS) ? '.exe' : ''
File dest = new File("${apkFile.absolutePath}.zipalign");
def argv = []
argv << '-f' //overwrite existing outfile.zip
// argv << '-z' //recompress using Zopfli
argv << '-v' //verbose output
argv << '4' //alignment in bytes, e.g. '4' provides 32-bit alignment
argv << apkFile.absolutePath
argv << dest.absolutePath //output
project.exec {
commandLine "${sdkDir}/build-tools/${project.android.buildToolsVersion}/zipalign${cmdExt}"
args argv
}
if (apkFile.exists()) {
apkFile.delete()
}
dest.renameTo(apkFile)
} else {
throw new InvalidUserDataException('$ANDROID_HOME is not defined')
}
}
}
然後客戶端需要做的就是根據這個patch的前面和當前app的簽名進行校驗即可。
這個不能算是Nuwa的坑,只不過Nuwa使用了ASM來進行注入字節碼,ASM的可讀性實在是太差,對於不懂字節碼的人來說有一定的難度,所以必須提高代碼的可讀性,降低維護成本。
如何降低維護成本替換asm為javassist,相對asm來說,javassist在性能上可能差一點,但是在可讀性上,那絕對是對開發人員友好的,因為寫的就是java代碼。下面我們來演示一下注入之前說的那段代碼
if(Boolean.FALSE.booleanValue()){
System.out.println(Hack.class)
}
ClassPool classPool = ClassPool.getDefault();
//這裡動態生成Hack類,插入到classpatch中,因為javassist生成字節碼需要依賴這個類,這裡采用動態生成
CtClass hackClass = classPool.makeClass("com.lizhangqu.hack.Hack")
byte[] hackBytes = hackClass.toBytecode()
hackClass.defrost()
classPool.insertClassPath(new ByteArrayClassPath("com.weidian.hack.Hack", hackBytes))
Nuwa原來注入字節碼的函數原型是這樣的
private static byte[] referHackWhenInit(InputStream inputStream) {
}
入參是InputStream,返回值是字節碼的byte數組,我們不改變函數原型,編寫這個注入函數
private
static byte[] referHackByJavassistWhenInit(ClassPool classPool, InputStream inputStream) {
CtClass clazz = classPool.makeClass(inputStream)
CtConstructor ctConstructor = clazz.makeClassInitializer()
ctConstructor.insertAfter("if(Boolean.FALSE.booleanValue()){System.out.println(com.weidian.hack.Hack.class);}")
def bytes = clazz.toBytecode()
clazz.defrost()
return bytes
}
入參多了一個ClassPool參數,這個參數就是前面的那個ClassPool,裡面包含了Hack這個類。這裡面的關鍵是makeClassInitializer函數,這個函數的作用就是生成一段靜態初始化的代碼,如果不存在的話會新建一個,存在的話就返回,然後我們在這個最後面插入一段字節碼,即
if(Boolean.FALSE.booleanValue()){System.out.println(com.lizhangqu.hack.Hack.class);}
插入完成後轉換成字節數組,記得調用defrost方法進行解凍,否則會有異常。最終生產的代碼就是這樣的。
static{
if(Boolean.FALSE.booleanValue(){
System.out.println(com.lizhangqu.hack.Hack.class);
}
}
在Android5.0與6.0上兼容性表現得如何?
實際情況下,我測了三個系統版本,即4.4,5.0,6.0,實際測試結果怎麼樣呢,三個系統版本打patch都是沒有問題的,唯一需要特殊處理的系統可能是6.0,為什麼是6.0呢,因為6.0多了一個運行時權限申請。
Android 6.0 動態權限申請的坑
為什麼這是個坑呢,因為測試的時候我是把patch放到sdcard根目錄進行測試的,這種情況下,對應6.0的系統來說,讀寫sdcard除了需要在manifest文件中進行聲明之外,還需要動態申請權限,因為對於用戶來說,讀寫sdcard屬於危險權限,需要用戶主動授權,所以6.0的系統,如果你的patch在sdcard,你可能需要加入類似這樣的申請權限的代碼
int permission = ContextCompat.checkSelfPermission(getApplicationContext(), Manifest.permission.WRITE_EXTERNAL_STORAGE);
if (permission != PackageManager.PERMISSION_GRANTED) {
Log.e("TAG", "未授權");
ActivityCompat.requestPermissions(this,
new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},
100);
}
之後只要用戶授權了,就能正常的打patch了。
以上就是最近我遇到的一些坑的簡述以及簡單的給出了解決思路,如果你遇到了其他坑,歡迎留言。
Android中自定義View的實現比較簡單,無非就是繼承父類,然後重載方法,即便如此,在實際編碼中難免會遇到一些坑,我把自己遇到的一些問題和解決方法總結一下,希望對廣大
一、問題描述 使用BordercastReceiver和Service組件實現下述功能:1.當手機處於來電狀態,啟動監聽服務,對來電進行監聽錄音。2.設置電話黑名單,當
本文實例講述了Android基於socket實現的簡單C/S聊天通信功能。分享給大家供大家參考,具體如下:主要想法:在客戶端上發送一條信息,在後台開辟一個線程充當服務端,
首先來看下面的效果: 從上面的圖片可以看到,當添加多張圖片的時候,能夠在下方形成一個畫廊的效果,我們左右拉動圖片來看我們添加進去的圖片,效果是不是好了很多呢?下面來看