集成了有段时间了,遇到的坑也比较多。
总的来说就是这套框架还不成熟,可能是没有经过实际运行的检验吧,另外也不支持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
本文探讨了Hotfix技术在实际项目中的集成经验与挑战,重点分析了资源文件变更、AndroidManifest.xml修改、Application及Provider调用限制等问题,并提出了解决方案。

被折叠的 条评论
为什么被折叠?



