关于Android APP在线热修复bug方案的调研(三)(集成Nuwa遇到的坑与解决)
2016-03-14 17:33
477 查看
集成了有段时间了,遇到的坑也比较多。
总的来说就是这套框架还不成熟,可能是没有经过实际运行的检验吧,另外也不支持ART。
集成需要很小心,稍不注意可能就会惹得一身骚。
下面算是集成的一些总结吧。
比如:要修改到string.xml或者layout.xml等,就不能应用到补丁
检测:补丁制作工具会检查资源文件是否有改变,有的话会停止制作补丁
2. 对于要修改到AndroidManifest.xml的change不适用
比如:要在Manifest中增加一个service,也不能应用到补丁。
检测:补丁制作工具会检查Manifest是否有改变,有的话会停止制作补丁
对于要修改到Provider相关的chagne不使用,因为它在补丁加载之前就会启动
3. 适用于class级别的修改
补丁方案就好比是替换了原来APK中的classes.dex文件,所以只适用于class级别的修改
并且是要在补丁加载之后才被加载的class。
a.该class无法应用于hot fix(小问题)
b.同时该class会被标志为private类型,也就是说这个class不能被插入cn.jiajixin.nuwa.Hack 或者调用补丁包中的代码,否则就会挂掉。(大问题,且非常容易撤出一大堆的文件)
挂掉的异常通常为下面两种:
class先于hack.apk加载:
标志为private类型的class调用了补丁文件中的class:
那么面对上面的问题该怎么解决?
第一个异常在buld.gradle中排除该class参与hot fix,并且排除参与混淆即可.
第二个异常有两种方式解决:
a.通过修改代码搬出去(比较好的解决方式)
b.参考第一种解决方式。
解释执行用的符号引入,一般没有问题。
可如果是在native code模式下,dex会被转为OAT文件,其中的符号函数调用等都会被转成内存地址。
如果有文件缺失,那么内存地址就时一个无效的。此时调用就会挂掉。
如果当你看到挂掉的call stack非常的奇怪,出现的一些函数是你的代码中没有使用到的函数,那么恭喜你,中招了。
来一个实列分析:
像下面这个异常:注意看codePointCount这个函数,App中没有任何地方会调用它。
这个异常其实是因为在ART模式下运行时,内存地址跑飞了。
为什么会跑飞了呢?为了解决这个问题,花了几天事件看了ART相关的才明白。
我们直接来看生成的OAT文件内容,它便是ART要执行的最终指令:
之前通过debug定位到这个异常在运行到下面的这个函数时就会挂掉
0x0053: invoke-static {v1, v3}, void com.nq.safelauncher.d.h.b(java.lang.String, java.lang.String) // method@202
而这个函数所在的文件com.nq.safelauncher.d.h其实并没有包含在补丁文件中,所以在生成native code时,上面的move指令movs r0, #202也就是错误的,
正常的值应该是像这样的 movw r0, #40824。
所以这也就导致blx跳转的函数地址是一个错误的地址,也就看到call stack跑飞了。
因为native代码和class不一样,class的都是符号引用,运行时才会进行解析,所以如果补丁中有文件缺失,在DVM下运行是没问题的。
但是ART下,编译成native的阶段,就会进行符号重定位,如果找不到符号所对应的地址,也就直接把method index作为内存地址给赋过去了。
此时可能系统直接抛出一个Exception或许更好些?
以上的便是集成过程中遇到的2个比较大的问题把,另外其实还有一些细节问题,比如对于排除要引入hack.apk的class同时也需要排除它参与混淆等。
最后下来基本也只采用了Nuwa修改class引入hack.apk的功能。
自己搞了个制作补丁的脚本,可供参考思路和一些细节:
总的来说就是这套框架还不成熟,可能是没有经过实际运行的检验吧,另外也不支持ART。
集成需要很小心,稍不注意可能就会惹得一身骚。
下面算是集成的一些总结吧。
1.hotfix适用范围:
1. 对于要修改到resource级别的change不适用比如:要修改到string.xml或者layout.xml等,就不能应用到补丁
检测:补丁制作工具会检查资源文件是否有改变,有的话会停止制作补丁
2. 对于要修改到AndroidManifest.xml的change不适用
比如:要在Manifest中增加一个service,也不能应用到补丁。
检测:补丁制作工具会检查Manifest是否有改变,有的话会停止制作补丁
对于要修改到Provider相关的chagne不使用,因为它在补丁加载之前就会启动
3. 适用于class级别的修改
补丁方案就好比是替换了原来APK中的classes.dex文件,所以只适用于class级别的修改
并且是要在补丁加载之后才被加载的class。
2.主要问题一Application以及Provider不要调用APP中其他的class文件。
如果有调用,那么这个被调用的class可能会在载入补丁文件之前就被DVM加载进内存,此时会有2个问题:a.该class无法应用于hot fix(小问题)
b.同时该class会被标志为private类型,也就是说这个class不能被插入cn.jiajixin.nuwa.Hack 或者调用补丁包中的代码,否则就会挂掉。(大问题,且非常容易撤出一大堆的文件)
挂掉的异常通常为下面两种:
class先于hack.apk加载:
E/AndroidRuntime(11948): java.lang.NoClassDefFoundError: cn.jiajixin.nuwa.Hack E/AndroidRuntime(11948): at com.nq.mam.app.MAMApp$1.<init>(MAMApp.java:201) E/AndroidRuntime(11948): at com.nq.mam.app.MAMApp.<init>(MAMApp.java:201) E/AndroidRuntime(11948): at java.lang.Class.newInstanceImpl(Native Method) E/AndroidRuntime(11948): at java.lang.Class.newInstance(Class.java:1208) E/AndroidRuntime(11948): at android.app.Instrumentation.newApplication(Instrumentation.java:990) E/AndroidRuntime(11948): at android.app.Instrumentation.newApplication(Instrumentation.java:975) E/AndroidRuntime(11948): at android.app.LoadedApk.makeApplication(LoadedApk.java:504) E/AndroidRuntime(11948): at android.app.ActivityThread.handleBindApplication(ActivityThread.java:4367) E/AndroidRuntime(11948): at android.app.ActivityThread.access$1600(ActivityThread.java:141) E/AndroidRuntime(11948): at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1273) E/AndroidRuntime(11948): at android.os.Handler.dispatchMessage(Handler.java:102) E/AndroidRuntime(11948): at android.os.Looper.loop(Looper.java:136) E/AndroidRuntime(11948): at android.app.ActivityThread.main(ActivityThread.java:5072) E/AndroidRuntime(11948): at java.lang.reflect.Method.invokeNative(Native Method) E/AndroidRuntime(11948): at java.lang.reflect.Method.invoke(Method.java:515) E/AndroidRuntime(11948): at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:793) E/AndroidRuntime(11948): at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:609) E/AndroidRuntime(11948): at dalvik.system.NativeStart.main(Native Method) W/ActivityManager( 1218): Force finishing activity com.nq.mdm/.activity.MDMSplashActivity
标志为private类型的class调用了补丁文件中的class:
那么面对上面的问题该怎么解决?
第一个异常在buld.gradle中排除该class参与hot fix,并且排除参与混淆即可.
第二个异常有两种方式解决:
a.通过修改代码搬出去(比较好的解决方式)
b.参考第一种解决方式。
3.主要问题二:制作补丁一定要将整个dex都作为补丁文件,否则不能支持ART。
ART分为Dalvik解释执行以及ART native code执行两种模式。解释执行用的符号引入,一般没有问题。
可如果是在native code模式下,dex会被转为OAT文件,其中的符号函数调用等都会被转成内存地址。
如果有文件缺失,那么内存地址就时一个无效的。此时调用就会挂掉。
如果当你看到挂掉的call stack非常的奇怪,出现的一些函数是你的代码中没有使用到的函数,那么恭喜你,中招了。
来一个实列分析:
像下面这个异常:注意看codePointCount这个函数,App中没有任何地方会调用它。
E/AndroidRuntime(12795): FATAL EXCEPTION: main E/AndroidRuntime(12795): Process: com.nq.safelauncher, PID: 12795 E/AndroidRuntime(12795): java.lang.StringIndexOutOfBoundsException: length=23; regionStart=851452864; regionLength=-851452863 E/AndroidRuntime(12795): at java.lang.String.startEndAndLength(String.java:504) E/AndroidRuntime(12795): at java.lang.String.codePointCount(String.java:1718) E/AndroidRuntime(12795): at com.nq.safelauncher.service.LockService.a(Unknown Source) E/AndroidRuntime(12795): at com.nq.safelauncher.service.b.run(Unknown Source) E/AndroidRuntime(12795): at android.os.Handler.handleCallback(Handler.java:739) E/AndroidRuntime(12795): at android.os.Handler.dispatchMessage(Handler.java:95) E/AndroidRuntime(12795): at android.os.Looper.loop(Looper.java:135) E/AndroidRuntime(12795): at android.app.ActivityThread.main(ActivityThread.java:5254) E/AndroidRuntime(12795): at java.lang.reflect.Method.invoke(Native Method) E/AndroidRuntime(12795): at java.lang.reflect.Method.invoke(Method.java:372) E/AndroidRuntime(12795): at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:903) E/AndroidRuntime(12795): at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:698) E/WifiStateMachine( 813): WifiStateMachine CMD_START_SCAN source -2 txSuccessRate=0.72 rxSuccessRate=6.81 targetRoamBSSID=any RSSI=-45 E/WifiStateMachine( 813): WifiStateMachine starting scan for "MDM-test"WPA_PSK with 5745,2437
这个异常其实是因为在ART模式下运行时,内存地址跑飞了。
为什么会跑飞了呢?为了解决这个问题,花了几天事件看了ART相关的才明白。
我们直接来看生成的OAT文件内容,它便是ART要执行的最终指令:
7: boolean com.nq.safelauncher.service.LockService.a(com.nq.safelauncher.service.LockService, java.lang.String) (dex_method_idx=250) DEX CODE: .......................... 0x003e: const-string v4, "yanchen-----[checkAuth] 1:" // string@402 0x0040: invoke-direct {v3, v4}, void java.lang.StringBuilder.<init>(java.lang.String) // method@286 0x0043: iget-object v4, v8, Lcom/nq/safelauncher/d/e; com.nq.safelauncher.service.LockService.g // field@101 0x0045: const-string v5, "auth_mdm" // string@222 0x0047: invoke-virtual {v4, v5}, boolean com.nq.safelauncher.d.e.b(java.lang.String) // method@194 0x004a: move-result v4 0x004b: invoke-virtual {v3, v4}, java.lang.StringBuilder java.lang.StringBuilder.append(boolean) // method@290 0x004e: move-result-object v3 0x004f: invoke-virtual {v3}, java.lang.String java.lang.StringBuilder.toString() // method@291 0x0052: move-result-object v3 //////下面这个便是导致挂掉的函数/////////////////////// 0x0053: invoke-static {v1, v3}, void com.nq.safelauncher.d.h.b(java.lang.String, java.lang.String) // method@202 0x0056: sget-object v1, Ljava/lang/String; com.nq.safelauncher.service.LockService.a // field@96 0x0058: new-instance v3, java.lang.StringBuilder // type@117 ............................. ............................. GC map objects: v0 (r6), v1 (r7), v3 (r8), v5 ([sp + #52]), v8 (r11), v9 ([sp + #104]) 0x0000e680: 4680 mov r8, r0 0x0000e682: f64d3edd movw lr, #56285 0x0000e686: f2c73e16 movt lr, #29462 0x0000e68a: f6497078 movw r0, #40824 0x0000e68e: f2c700dc movt r0, #28892 0x0000e692: 4641 mov r1, r8 0x0000e694: f8d1c000 ldr.w r12, [r1, #0] suspend point dex PC: 0x004f GC map objects: v0 (r6), v1 (r7), v3 (r8), v5 ([sp + #52]), v8 (r11), v9 ([sp + #104]) 0x0000e698: 47f0 blx lr suspend point dex PC: 0x004f GC map objects: v0 (r6), v1 (r7), v3 (r8), v5 ([sp + #52]), v8 (r11), v9 ([sp + #104]) 0x0000e69a: f8d9e224 ldr.w lr, [r9, #548] ; pInvokeStaticTrampolineWithAccessCheck 0x0000e69e: 4680 mov r8, r0 ///////move的赋值202有问题////////////////// 0x0000e6a0: 20ca movs r0, #202 0x0000e6a2: 1c39 mov r1, r7 0x0000e6a4: 4642 mov r2, r8 0x0000e6a6: 47f0 blx lr .............................
之前通过debug定位到这个异常在运行到下面的这个函数时就会挂掉
0x0053: invoke-static {v1, v3}, void com.nq.safelauncher.d.h.b(java.lang.String, java.lang.String) // method@202
而这个函数所在的文件com.nq.safelauncher.d.h其实并没有包含在补丁文件中,所以在生成native code时,上面的move指令movs r0, #202也就是错误的,
正常的值应该是像这样的 movw r0, #40824。
所以这也就导致blx跳转的函数地址是一个错误的地址,也就看到call stack跑飞了。
因为native代码和class不一样,class的都是符号引用,运行时才会进行解析,所以如果补丁中有文件缺失,在DVM下运行是没问题的。
但是ART下,编译成native的阶段,就会进行符号重定位,如果找不到符号所对应的地址,也就直接把method index作为内存地址给赋过去了。
此时可能系统直接抛出一个Exception或许更好些?
以上的便是集成过程中遇到的2个比较大的问题把,另外其实还有一些细节问题,比如对于排除要引入hack.apk的class同时也需要排除它参与混淆等。
最后下来基本也只采用了Nuwa修改class引入hack.apk的功能。
自己搞了个制作补丁的脚本,可供参考思路和一些细节:
#!/bin/bash #yanchen #2016-1-7 function show_red_font(){ if [ "$1" != "" ];then echo -e "\033[31m ${1} ${2} \033[0m" fi } function mycls(){ if [ "$OS" != "Windows_NT" ];then clear fi } function showUsage(){ show_red_font "Usage: patchBuild flag buildOutApk releasedApk" show_red_font "Demo: patchBuild myapp D:/xxx/app/build/outputs/apk/app-released.apk D:/release/app-released.apk" echo } function getVersionCode(){ versioncodeLine=`grep "versionCode" ${patchOutDir}/apktool.yml` str1=${versioncodeLine#*\'} str2=${str1%\'} echo $str2 } function checkResourceChange(){ patchXmlHash=`java -jar ../libs/fileHash.jar ${patchOutDir}/res/values/public.xml` releaseXmlHash=`java -jar ../libs/fileHash.jar ${releaseOutDir}/res/values/public.xml` resourceChanged="" isChanged=0 if [ "$patchXmlHash" != "$releaseXmlHash" ];then ((isChanged++)) resourceChanged="${isChanged}: resource资源有变化" fi patchManifest1Hash=`java -jar ../libs/fileHash.jar ${patchOutDir}/AndroidManifest.xml` releaseManifest1Hash=`java -jar ../libs/fileHash.jar ${releaseOutDir}/AndroidManifest.xml` ManifextChanged="" if [ "$patchManifest1Hash" != "$releaseManifest1Hash" ]; then ((isChanged++)) ManifextChanged="${isChanged}: AndroidManifest.xml有变化" fi sed -i '/apkFileName/'d ${patchOutDir}/apktool.yml sed -i '/apkFileName/'d ${releaseOutDir}/apktool.yml patchManifest2Hash=`java -jar ../libs/fileHash.jar ${patchOutDir}/apktool.yml` releaseManifest2Hash=`java -jar ../libs/fileHash.jar ${releaseOutDir}/apktool.yml` ManifextPackageInfoChanged="" if [ "$patchManifest2Hash" != "$releaseManifest2Hash" ];then ((isChanged++)) ManifextPackageInfoChanged="${isChanged}: apktool.yml有变化(versionCode/versionName/...)" fi if [ $isChanged -gt 0 ];then echo show_red_font "检测到有${isChanged}项内容发生变化,制作补丁失败:" show_red_font $resourceChanged show_red_font $ManifextChanged show_red_font $ManifextPackageInfoChanged echo exit fi } #清屏 #mycls #检查参数个数 isParamsOk="true" if [ $# == 0 ]; then showUsage exit elif [ $# != 2 ]; then show_red_font "参数错误..." exit fi #即将编译出来的APK scriptPath=$(cd `dirname $0`; pwd) buildOutFile=$(cd `dirname $1`; pwd)"/`basename $1`" releasedOutFile=$(cd `dirname $2`; pwd)"/`basename $2`" appId="mcm" outDir="out" cd $scriptPath cd .. chmod 777 $releasedOutFile chmod 777 $buildOutFile #检查release apk if [[ "$releasedOutFile" != *.apk ]]; then show_red_font "参数为错误:请填写需要制作补丁的APK路径" exit fi if [ ! -f "$releasedOutFile" ]; then show_red_font "没有找到文件:$releasedOutFile" exit fi #检查build out apk if [[ "$buildOutFile" != *.apk ]]; then show_red_font "参数为错误:请填写基版APK路径" exit fi if [ ! -f "$buildOutFile" ]; then show_red_font "没有找到文件:$releasedOutFile" exit fi #修改权限,svn下可能没有执行权限 #chmod a+x gradlew #1.编译 #rm $buildOutFile #./gradlew build #if [ $? != 0 ];then # exit; #fi #current in mybuild directory... #if [ ! -f "$buildOutFile" ]; then # show_red_font "没有编译出:${buildOutFile}" # exit #fi #cd to mybuild directory cd $scriptPath rm -rf $outDir mkdir $outDir patchFileOut="patch_`basename $buildOutFile`" releaseFileOut="released_`basename $releasedOutFile`" cp $buildOutFile $outDir/$patchFileOut cp $releasedOutFile $outDir/$releaseFileOut cd $outDir patchOutDir="patch_out" releaseOutDir="release_out" java -jar ../libs/apktool.jar d $patchFileOut -o ${patchOutDir} if [ $? != 0 ];then show_red_font "解压${patchFileOut}失败..." exit fi echo echo java -jar ../libs/apktool.jar d $releaseFileOut -o ${releaseOutDir} if [ $? != 0 ];then show_red_font "解压${releaseFileOut}失败..." exit fi checkResourceChange #从patch apk中提取classes.dex jar -xvf $patchFileOut classes.dex #将上一步提取出来的classes.dex压缩成jar文件 jar -cfv patch.jar classes.dex if [ $? != 0 ];then show_red_font "提取classes.dex失败..." exit fi echo "开始压缩classes.dex..." jar -cfv patch.jar classes.dex if [ $? != 0 ];then show_red_font "压缩失败..." exit fi #获取version code #jar重命名 patchJarHash=`java -jar ../libs/fileHash.jar patch.jar` versionCode=`getVersionCode` if [ "${versionCode}" == "" ];then show_red_font "获取versionCode失败..." exit fi patchJarFileName="patch_${appId}_${versionCode}_${patchJarHash}.jar" mv patch.jar ${patchJarFileName} #clean.. rm classes.dex rm -rf $patchFileOut rm -rf $releaseFileOut rm -rf ${patchOutDir}/ rm -rf ${releaseOutDir}/ echo echo "补丁文件路径:" echo `pwd`/${patchJarFileName} echo show_red_font "=================Patch build success=================" echo
相关文章推荐
- Android Studio 插件 —— GsonFormat
- Android设计模式系列(12)--SDK源码之生成器模式(建造者模式)
- 有用的Unity社区贴
- Android Fresco 图片框架加载图片解决不能warp_content得问题
- CocoaPods安装与使用
- 采用MQTT协议实现Android消息推送
- 用Kotlin写响应式编程RxAndroid
- iOS 高效添加圆角效果实战讲解
- Android系统Intent中的Uri使用
- Android中糟糕的AsyncTask
- 盟聚解说微信朋友圈广告营销
- Android设计模式系列(11)--SDK源码之策略模式
- android:layout_weight的真实含义
- Android开发者入门必知了解谷歌官方Android开发文档
- Android 之Hierarchy Tool Window
- 在Android系统外部和内部读取Android应用的签名
- Android设计模式系列(10)--SDK源码之原型模式
- android4.4短信拦截怎么实现,abortBroadcast()不能实现啊
- android GridLayout。。。
- Unity中对SQL数据库的操作