且构网

分享程序员开发的那些事...
且构网 - 分享程序员编程开发的那些事

使用友盟+的APM服务实现基于Xamarin.Android的应用程序启动性能优化

更新时间:2022-06-17 20:31:12

背景

随着移动应⽤程序开发越来越流⾏, 越来越多的应⽤程序浮现于市场。 但是, 开发移动应⽤程序 并不是⼀个简单的过程, 需要花费⼤量时间, 尤其是如果想要⼀个可跨 Apple、Android 和 Windows 运⾏的可扩展移动应⽤程序。

然⽽, 糟糕的性能可能会极⼤地损害⽤户体验。 ⽤户在任何时候都不希望看到 10 秒以上的启动 画⾯。 如果等待时间过⻓,他们可能会感到⽣⽓、 放弃购物、 减少停留时间或完全卸载应⽤程序。

随着开发平台的普及, 我们需要正确的⼯具和⽅法来满⾜不断增⻓的需求。Xamarin就是这样⼀种框架, 它⽀持在 Android iOS Windows 平台上共享单个代码库。

所以,我们将在 Xamarin.Android应⽤程序中测试性能, 就像在 AndroidStudio 中使⽤ Java 开发⼀样, 我们可以使⽤c#对性能进⾏测试, 从⽽优化启动时间。

测试总启动时间

⾸先测试程序在不同设备的启动时间,此处⽤到的⼯具是友盟+推出的U-APM。从图中可以看 出应⽤程序在启动时间上还存在着⼀定的优化空间。

使用友盟+的APM服务实现基于Xamarin.Android的应用程序启动性能优化

Android 上,ActivityManager系统进程会显示⼀条“初始显示时间”⽇志消息,可以更好地了 解整体启动时间。在命令⾏使⽤adb logcat快速查看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

使用友盟+的APM服务实现基于Xamarin.Android的应用程序启动性能优化

诊断问题

好, ⽬前应⽤程序由于readRawTextFile很慢。 现在我该怎么办?

⾸先我们需要对以下⼏个组件有⼀个系统性的了解

安卓ART

Android 运⾏时 (ART) Android 上的应⽤程序和系统服务使⽤的托管运⾏时。ART 作为运⾏ 时执⾏ Dalvik 可执⾏⽂件 (.dex ⽂件 - D alvik EX可执⾏⽂件) , 这是⼀种⽤于存储 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 shell setprop debug.mono.profile log:calls

adb shell Android 设备或模拟器上运⾏单个 shell 命令。 setprop设置Android系统属性, 似于其他平台上的环境变量。

 

然后只需强制退出并重新启动应⽤程序。 下次启动时, Mono 会在 Android应⽤程序的本地⽬ 录中保存⼀个⽂件。 prole.mlpd注意这⾥存在⼀个问题, 该⽂件只能由应⽤程序本身访问, 因此我们必须使⽤命令来定位⽂件: run-as

$ adb shell run-as com.lgq.wood.expiramentation ls -l files/.__override__

-rw-rw-r-- 1 u0_a411 u0_a411 515303 2020-07-27 09:29 profile.mlpd

为了从设备上获取⽂件, 我使⽤了⼀个已知的可写⽬录, 例如:  /sdcard/Download/

$ adb shell run-as com.lgq.wood.expiramentation cp files/.__override__/prof

复制⽂件后, 您可以使⽤adbpull将⽂件获取到您的台式计算机:prole.mlpd

$ adb pull /sdcard/Download/profile.mlpd

/sdcard/Download/profile.mlpd: 1 file pulled, 0 skipped. 162.7 MB/s (515303

prole.mlpd是⼀个⼆进制⽂件,

Windows ⽤户需要在⽤于 Linux  Windows ⼦系统中安装 Mono才能运⾏。

有了上⾯的⼀系列代码, 就会出现⼀些有趣的数字。

解决方案

我们通过前⽂的调⽤, 可以发现以下⼏个函数可能需要相当⻓的时间:

使用友盟+的APM服务实现基于Xamarin.Android的应用程序启动性能优化


还可以看到内存分配, 例如:

使用友盟+的APM服务实现基于Xamarin.Android的应用程序启动性能优化

请注意, 如果您需要查看这些分配来⾃哪些⽅法, 您可以传递到。 --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);

}

}

查看⽅法调⽤的差异, 我们可以看到⼀个明显的时间优化:

使用友盟+的APM服务实现基于Xamarin.Android的应用程序启动性能优化

我们还可以看到以下内存分配的差异:

使用友盟+的APM服务实现基于Xamarin.Android的应用程序启动性能优化

这⼀点, 和我们在U-APM中测试得到的瓶颈函数相吻合, 瓶颈确实是处在readRawTextFile函数中, 我们尝试了以下⼏种⽅法, 也⼀定程度上缓解了启动问题, 但收益并没有U-APM中的    readRawTextFile那么⼤。在此列出, 仅供参考:

1.  我们可以缓存 Web 请求的结果

2.  我们可以从磁盘上的⽂件加载之前的调⽤结果, ⽐如设置24 ⼩时内有效。

3.  由于调⽤不是互相依赖,我们可以同时进⾏异步调⽤

4.  在服务器端, 我们可以进⾏⼀个新的 API 调⽤, 在⼀个请求中返回所有调⽤的数据


结论:

优化性能很难, ⽅向也很多。 关于代码慢的定位部分, 改动后可能会发现这⼀部分根本不会产⽣ 效果, 对代码产⽣影响的***⽅法是测试、 测试, 然后再次测试。 改变后再次测试。 ⽽通过测试 去提升性能, 往往能针对问题做预先准备。 也往往能更核⼼地提升核⼼性能瓶颈, 从⽽带来⽅⽅⾯⾯的全⽅位提升。




作者:丁立戈