背景
随着移动应⽤程序开发越来越流⾏,越来越多的应⽤程序浮现于市场。但是,开发移动应⽤程序并不是⼀个简单的过程,需要花费⼤量时间,尤其是如果想要⼀个可跨Apple、Android和 Windows 运⾏的可扩展移动应⽤程序。
然⽽, 糟糕的性能可能会极⼤地损害⽤户体验。 ⽤户在任何时候都不希望看到 10 秒以上的启动 画⾯。如果等待时间过⻓,他们可能会感到⽣⽓、放弃购物、减少停留时间或完全卸载应⽤程序。
随着开发平台的普及,我们需要正确的⼯具和⽅法来满⾜不断增⻓的需求。Xamarin就是这样⼀种框架,它⽀持在Android、iOS和Windows平台上共享单个代码库。
所以,我们将在Xamarin.Android应⽤程序中测试性能,就像在AndroidStudio中使⽤Java开发⼀样,我们可以使⽤c#对性能进⾏测试,从⽽优化启动时间。
测试总启动时间
⾸先测试程序在不同设备的启动时间,此处⽤到的⼯具是友盟+推出的U-APM。从图中可以看 出应⽤程序在启动时间上还存在着⼀定的优化空间。
在Android上,ActivityManager系统进程会显示⼀条“初始显示时间”⽇志消息,可以更好地了解整体启动时间。在命令⾏使⽤adblogcat快速查看Android设备⽇志。或者使⽤Visual Studio中的Android调试⽇志。
在Windows上,运⾏以下powershell:
> adb logcat -d | Select-String Displayed |
输出:
ActivityTaskManager: Displayed com.lgq.wood.expiramentation/.MainActivity: |
上述⽇志消息是在 x86 Android 模拟器上从 Visual Studio 调试应⽤程序时捕获的。启动/连接 调试器会产⽣⼀些额外的开销,并且缺少Debug编译时的优化。如果我们简单地切换到Release配置并再次部署和运⾏应⽤程序:
ActivityTaskManager: Displayed com.lgq.wood.expiramentation/.MainActivity: |
ActivityTaskManager: Displayed com.lgq.wood.expiramentation/.MainActivity: |
因为我们最终⽬标是提⾼移动应⽤程序的性能,那么第⼀步应该是实际测试卡顿函数的具体位置。如果盲⽬地进⾏代码更改,最终可能会和我们推测的结果产⽣很⼤的分歧,如果⼀些复杂的性能改进甚⾄会损害代码库的可维护性。这个过程应该是:测试,做出修改,再次测试,并且重复以上步骤。
采⽤U-APM测得卡顿位置主要出现于:
com.lgq.wood.expiramentation.apache.http.impl.exec.readRawTextFile |
诊断问题
好, ⽬前应⽤程序由于readRawTextFile很慢。 现在我该怎么办?
⾸先我们需要对以下⼏个组件有⼀个系统性的了解
安卓ART
Android运⾏时(ART)是Android上的应⽤程序和系统服务使⽤的托管运⾏时。ART作为运⾏时执⾏Dalvik可执⾏⽂件(.dex⽂件-DalvikEX可执⾏⽂件),这是⼀种⽤于存储Dalvik 字节码的紧凑格式。
ART 通过在安装应⽤程序时将整个应⽤程序编译为本机代码, 引⼊了提前 (AOT) 编译。 这带来 了更快的应⽤程序执⾏和改进的内存分配。 以及垃圾收集机制、 更准确的分析等等。
为了实现这⼀点,ART使⽤dex2oat来创建⼀个ELF(可执⾏和链接格式)的可执⾏⽂件。缺点 是需要额外的时间来编译。 此外, 应⽤程序会占⽤⼤量磁盘内存来存储已编译的代码。
AOT
Mono运⾏时提供AOT功能。Mono将预编译程序集以最⼩化JIT时间并减少内存使⽤ 。 Mono可以在⽀持它的平台(如Android) 上⽣成ELF.so⽂件。然后它在原始程序集旁边存储⼀个预编译的图像。
即
Mono.Android.dll → libaot-Mono.Android.dll.so |
然后,这些⽂件可以被Mono运⾏时使⽤,并省略JIT开销
启动跟踪
Mono引⼊了⼀项功能,允许在应⽤程序上使⽤内置的AOT分析器来⽣成AOT配置⽂件。分析器进⾏内存分析、执⾏时间分析,甚⾄是基于统计的抽样分析。这会⽣成⼀个AOT配置⽂ 件,当使⽤带有配置⽂件的Mono的AOT功能时,该配置⽂件可⽤于优化应⽤程序。启动跟踪可⽤于Visual Studio 2019 版本 16.2或Visual Studio for Mac 2019 版本 8.2。
可以通过编辑Android项⽬的.csproj⽂件并在Release<PropertyGroup>中添加以下属性来开始使⽤启动跟踪:
<PropertyGroup Condition = " '$(Configuration)|$(Platform)' == 'Release|Any |
也可以在项⽬设置的Android选项中进⾏设置, Mono的AOT编译器启⽤使⽤会默认配置⽂件的启动跟踪,并在部署时加快Android应⽤程序的启动时间。
实际分析
我们需要实际分析我们的代码并需要改进。切换回Debug配置,并通过运⾏以下命令启⽤Mono分析器:
$ adb shellsetpropdebug.mono.profilelog:calls |
adbshell在Android设备或模拟器上运⾏单个shell命令。setprop设置Android系统属性,类似于其他平台上的环境变量。
然后只需强制退出并重新启动应⽤程序。下次启动时,Mono会在Android应⽤程序的本地⽬录中保存⼀个⽂件。profile.mlpd注意这⾥存在⼀个问题,该⽂件只能由应⽤程序本身访问,因此我们必须使⽤命令来定位⽂件:run-as
$ adbshellrun-ascom.lgq.wood.expiramentationls-lfiles/.__override__ -rw-rw-r-- 1u0_a411u0_a4115153032020-07-2709:29profile.mlpd |
为了从设备上获取⽂件,我使⽤了⼀个已知的可写⽬录,例如: /sdcard/Download/
$ adb shell run-as com.lgq.wood.expiramentationcpfiles/.__override__/prof |
复制⽂件后,您可以使⽤adbpull将⽂件获取到您的台式计算机:profile.mlpd
$ adb pull /sdcard/Download/profile.mlpd /sdcard/Download/profile.mlpd: 1 file pulled,0skipped.162.7MB/s(515303 |
profile.mlpd是⼀个⼆进制⽂件,
Windows ⽤户需要在⽤于 Linux 的 Windows ⼦系统中安装 Mono才能运⾏。
有了上⾯的⼀系列代码, 就会出现⼀些有趣的数字。
解决方案
我们通过前⽂的调⽤,可以发现以下⼏个函数可能需要相当⻓的时间:
还可以看到内存分配,例如:
请注意, 如果您需要查看这些分配来⾃哪些⽅法, 您可以传递到。 --tracesmprof-report
我们做出了多种尝试,也都收到了⼀定成效。但是我们最意想不到的是,下⾯这个简单的改动。我们尝试将string直接从stream中读取,⽽不是使⽤响应的内容创建,然后使⽤新的System.Text.Json库来进⾏更⾼效的JSON解析:
// At the top using System.Text.Json; //... async Task<Response> GetSlides() |
{ var response = await httpClient.GetAsync("https://httpbin.org/json"); response.EnsureSuccessStatusCode(); using (var stream = await response.Content.ReadAsStreamAsync()) { return await JsonSerializer.DeserializeAsync<Response>(stream); } } |
查看⽅法调⽤的差异, 我们可以看到⼀个明显的时间优化:
我们还可以看到以下内存分配的差异:
这⼀点, 和我们在U-APM中测试得到的瓶颈函数相吻合, 瓶颈确实是处在readRawTextFile函数中,我们尝试了以下⼏种⽅法,也⼀定程度上缓解了启动问题,但收益并没有U-APM中的 readRawTextFile那么⼤。在此列出,仅供参考:
1. 我们可以缓存 Web 请求的结果
2. 我们可以从磁盘上的⽂件加载之前的调⽤结果,⽐如设置24⼩时内有效。
3. 由于调⽤不是互相依赖,我们可以同时进⾏异步调⽤
4. 在服务器端,我们可以进⾏⼀个新的API调⽤,在⼀个请求中返回所有调⽤的数据
结论:
优化性能很难,⽅向也很多。关于代码慢的定位部分,改动后可能会发现这⼀部分根本不会产⽣效果,对代码产⽣影响的最佳⽅法是测试、测试,然后再次测试。改变后再次测试。⽽通过测试去提升性能,往往能针对问题做预先准备。也往往能更核⼼地提升核⼼性能瓶颈,从⽽带来⽅⽅⾯⾯的全⽅位提升。
作者:丁立戈