关于Android APP在线热修复bug方案的调研(三)(集成Nuwa遇到的坑与解决)

集成了有段时间了,遇到的坑也比较多。

总的来说就是这套框架还不成熟,可能是没有经过实际运行的检验吧,另外也不支持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


©️2020 CSDN 皮肤主题: 猿与汪的秘密 设计师:上身试试 返回首页