我们在将Unity打包成il2cpp+release模式的apk的之后,内部的代码是会被打包成so文件的,这个东西其实是linux的动态链接库的格式,对应的就是windows的dll或者Mac的dylib之类的东西。在android这边也可以叫做符号表,因为它内部会保存虚拟内存地址与汇编代码的的对应关系。
这样打包以后,会降低我们的包体积,而且有助于我们的代码保护不被抄袭,但是同时也会导致我们开发者看不懂报错信息,因为这些报错信息都是些虚拟内存地址,而不是函数的名字,那么对于线上发生的报错信息,我们怎么去阅读呢?
其实刚才我们就说过了,这个so文件不仅是动态链接库,内部还保存着虚拟内存地址与函数之间的映射关系,所以我们要读懂这些报错信息,还需要借助这个动态链接库。
接下来我们就讲一下,如何利用这so文件
打包过程中保存so文件打包android项目,我们是可以选择直接导出为apk或者导出为gradle项目的,如果我们在打包位apk的时候,勾选打包设置Create Symbols.zip
,就会在apk的同级目录下将所有的so文件保存下来
而如果选择的是导出为gradle项目,并且架构选择是arm64-v8a,那么我们自己的代码因为选择了il2cpp,就会将我们的代码打包为libil2cpp.so ,作为动态链接库存放在gradle项目的的unityLibrary/src/main/jniLibs/arm64-v8a下面
这里解释一下这个路径,如果熟悉安卓开发的同学会对这个路径很敏感,这个路径基本就是普通安卓项目默认的动态链接库的路径,jniLibs,拆开是jni + libs,jni的意思是java native interface,就是java的原生方法的意思,后面那个文件夹是架构,在不同架构的手机上,会调用不同的文件夹下的动态链接库。如果只选择了arm64-v8a,那么这个apk就只能运行在改架构下的手机上。
除了我们自己代码会打包成libil2cpp.so ,unity自身引擎代码,或者第三方包的代码,也会打包成各种so文件,如libunity.so , 但是这些文件是不在这个目录下的,我们有两个地方可以找到这些so文件,一个就是unityLibrary/src/symbols/arm64-v8a,另一种就是在Unity项目的/Temp/StagingArea/libs/下面,我们需要在打包完成之后把它们复制出来。
这里需要注意一点,如果是用editor来手动build,那么手动拷贝就好,但是如果是通过命令行batchmode的方式打包,就要注意,如果想要BuildPlayer方法运行完成以后再去复制,是获取不到的,因为一旦editor关闭,Temp文件夹就会被删除。
这个时候我们就需要用到PostProcessBuildAttribute这个hook,在Build完成之后,退出editor之前加上一段逻辑去保存。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 using UnityEngine;using System.Collections;using UnityEditor.Callbacks;using UnityEditor;using System.IO;using System;public class PostProcessBuild { [PostProcessBuildAttribute() ] public static void OnPostprocessBuild (BuildTarget target, string pathToBuiltProject ) { if (target == BuildTarget.Android) PostProcessAndroidBuild(pathToBuiltProject); } public static void PostProcessAndroidBuild (string pathToBuiltProject ) { UnityEditor.ScriptingImplementation backend = UnityEditor.PlayerSettings.GetScriptingBackend(UnityEditor.BuildTargetGroup.Android); if (backend == UnityEditor.ScriptingImplementation.IL2CPP) { CopyARMSymbols("armeabi-v7a" ); CopyARMSymbols("arm64-v8a" ); } } private static void CopyARMSymbols (string target ) { const string libpath = "/Temp/StagingArea/libs/" ; string sourcePath = Directory.GetCurrentDirectory() + libpath + $"{target} /" ; string desPath = Directory.GetCurrentDirectory() + $"/Build/android/unityLibrary/symbols/{target} " ; if (!Directory.Exists(desPath)) { Directory.CreateDirectory(desPath); } CopyDirectory(sourcePath, desPath); } private static void CopyDirectory (string sourcePath, string targetPath ) { if (System.IO.Directory.Exists(sourcePath)) { string [] files = System.IO.Directory.GetFiles(sourcePath); foreach (string s in files) { var fileName = System.IO.Path.GetFileName(s); var destFile = System.IO.Path.Combine(targetPath, fileName); Debug.Log($"fileName: {fileName} , destFile: {destFile} " ); System.IO.File.Copy(s, destFile, true ); } } else { Console.WriteLine("Source path does not exist!" ); } } }
使用保存的so去解析日志有了so之后,我们就可以解析全是地址的日志了。
因为这个其实是android的动态链接库,所以解析需要用到android的工具:addr2line,我是直接用了unity安装的android工具,而且根据架构不同,要用不同的工具,因为我的是arm64-v8a,我用到的是aarch64-linux-android-addr2line,具体路径每个人电脑不同,找到就好
我们可以直接在命令行运行:
1 aarch64-linux-android-addr2line -f -C -e libil2cpp.so 日志中的地址
当然也可以通过代码去做这件事,因为一行行解析很费劲,我这里只用了lib2cpp.so和libunity.so
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 import sysimport osdef OutCrash (filename, version, target ): addr2linePath = "/Applications/Unity/Hub/Editor/2020.3.20f1/PlaybackEngines/AndroidPlayer/NDK/toolchains/llvm/prebuilt/darwin-x86_64/bin/aarch64-linux-android-addr2line -f -C -e " il2cppdebugsoPath = "/Users/bytedance/Desktop/Unity/Android/symbols/" + version + "/" + target + "/libil2cpp.so " unitydebugsoPath = "/Users/bytedance/Desktop/Unity/Android/symbols/" + version + "/" + target + "/libunity.so " f = open (filename,'r' ) logstr = f.readlines() il2cppflag = 'libil2cpp' unityflag = 'libunity' crashEndFlag = 'libunity.so\n' for log in logstr: OutCmd(log,addr2linePath,crashEndFlag,unitydebugsoPath) OutCmd(log,addr2linePath,il2cppflag,il2cppdebugsoPath) OutCmd(log,addr2linePath,unityflag,unitydebugsoPath) def OutCmd (log,addr2linePath,debugFlagStr,debugsoPath ): if log.endswith(debugFlagStr): startIndex = log.index(' pc ' ) endflag = log.index(r' /data/' ) addstr = log[startIndex+4 :endflag] print (addstr) cmdstr = addr2linePath +debugsoPath+addstr os.system(cmdstr) else : unitystart = log.find(debugFlagStr) if unitystart >= 0 : unitylen = log.index(debugFlagStr) unitylen = unitylen + len (debugFlagStr) +1 endlen = log.find('(' ) if endlen >= 0 : endIndex = log.index('(' ) addstr = log[unitylen:endIndex] addstr = addstr.replace(' ' ,'' ) cmdstr = addr2linePath + debugsoPath +addstr print (addstr) os.system(cmdstr) OutCrash(sys.argv[1 ], sys.argv[2 ], sys.argv[3 ])