开发者控制台

如何衡量和改进Fire OS中的应用启动时间

Mayur Ahir Jun 06, 2023
Share:
How to App performance Best practices
Blog_Header_Post_Img

应用启动性能在打造引人入胜的用户体验方面发挥着重要作用。如果应用启动速度缓慢,用户就会产生挫败感,并且往往会抛弃该产品。相比之下,通过在开发过程中对代码进行一些优化,可以实现快速启动。本文将介绍衡量和改进应用首次启动体验中的性能的最佳实践。

衡量应用启动时间

loading steps

通过衡量启动和加载时间,您可以更好地了解从头到尾的体验中的启动性能。要实现这种插桩方式,首先需要调用活动中的可用API。然后检查日志,以计算应用启动与应用资源完整显示之间经过的时间,并查看层次结构。通过在您自己的设备上对不同的应用启动场景进行本地模拟,可以实时快速测试和衡量更新的影响。

用来衡量Fire OS上应用启动时间的数据点有两个:“显示第一帧的时间”和“准备好供使用的时间”(RTU)。建议对这些时间进行分析和比较,同时优化用户与应用交互时的启动体验。其中每项测量详情如下:

app start types

显示第一帧的时间是指从应用启动到向用户显示第一帧画面所用的时间。在此期间,应用可能会绘制背景、启动导航、加载本地资产,以及渲染本地加载或从远程端点获取的内容的占位符。

在Fire OS 5(API级别22)及更高版本中,logcat会提供一个称为Displayed的值,表示启动相关进程与屏幕上完成相应活动第一帧画面的绘制之间经过的时间。该值表示应用显示第一帧的时间。

准备好供使用的时间(RTU)描述了从应用启动到其在屏幕上完全绘制并准备好供用户开始交互所用的时间。到了RTU这一点,应用已在屏幕上完成所有渲染,并且会显示从远程端点获取的内容。如果通过网络加载远程资源时出现明显的延迟,则最好显示进度指示器,以保持用户的注意力。

对于Fire TV和Android应用,“延迟加载”是指以下情况:应用启用窗口初始绘制、在后台异步加载资源并更新视图层次结构。请将所有资源加载完毕并显示视图视为单独的指标,并手动触发reportFullyDrawn(),让系统知道活动已通过延迟加载完成。该值表示应用准备好供使用的时间。注意:​为了可靠地报告该指标,建议查看应用的日志,以确定在触发reportFullyDrawn()之前应考虑哪些活动。

如何衡量应用启动时间

app loading time

在您向亚马逊应用商店提交Amazon Fire TV应用或游戏时,这些应用或游戏必须通过一系列测试才有资格发布。当您开发应用时,请在提交应用之前使用我们文档中的测试开始时间标准作为改进关键绩效指标的指南。

注意:​进行这一部分中列出的每项测量时,都请务必首先确保您的应用及其进程没有在后台运行。您还应该测量这些值并多次强制关闭应用,以获得平均值。

冷启动:显示第一帧的时间

启动应用并通过观察日志来衡量绘制第一帧画面所用的时间。

Copied to clipboard
ActivityManager: Displayed {package}/{activity}: +1s534ms
冷启动: 准备好供使用的时间

启动应用并衡量日志,了解从启动到用户可以与应用完全交互要过多长时间。

Copied to clipboard
ActivityManager: Fully drawn {package}/{activity}: +4s54ms
热启动:显示第一帧的时间

启动应用,然后按下遥控器上的主页按钮,将应用置于后台。从Fire TV的“Recent apps”(最近使用的应用)行中,选择您的应用并重新启动,将其置于前台。通过观察日志来衡量绘制第一帧画面所用的时间。

Copied to clipboard
ActivityManager: Displayed {package}/{activity}: +1s534ms
启动:准备好供使用的时间

启动应用,然后按下遥控器上的主页按钮,将应用置于后台。从Fire TV的“Recent apps”(最近使用的应用)行中,选择您的应用并重新启动,将其置于前台。通过观察日志来衡量应用完全加载所用的时间——完全加载是指来到客户可以与应用交互的应用画面。

Copied to clipboard
ActivityManager: Fully drawn {package}/{activity}: +4s54ms
施reportFullyDrawn()的最佳实践

您需要调用Activity.reportFullyDrawn(),因为system_server只会选择绘制第一帧的时间戳,而在该状态下,客户还无法开始使用您的应用。调用reportFullyDrawn()时,system_server会注册接收时的时间戳。该时间戳更接近需要记录的状态。

请记住,您可能会难以确定调用reportFullyDrawn()的正确时间,具体取决于所使用的架构。通常建议重点关注客户体验,而不一定要侧重于技术角度。更具体地说,一旦应用能够为客户提供有意义的体验以开始交互,就应该调用reportFullyDrawn()。例如,当您仍在后台加载资产时,占位符元素可能会在屏幕上当前显示不出来的部分渲染。

有效实施reportFullyDrawn()可以改善应用的用户体验。此外,全面了解数据处理、网络活动和多线程的情况,是助力提升整体用户体验时必须优先考虑的重要因素。

“冷”和“热”两种启动的处理方式
在冷启动期间,系统会调用Activity#onCreate方法。在应用创建步骤结束时,请重写此方法以初始化Fire TV功能集成所特有的库(例如Alexa VSK、ADM等)。有关通过应用的onCreate方法初始化Alexa客户端库的详细示例,请参阅亚马逊应用商店开发者文档

对于热启动,测量范围的起点是在应用视图层次结构膨胀之前调用Activity#onCreate时,终点是调用Activity#onResume时。最好重写Activity#onResume方法,并添加reportFullyDrawn()调用来跟踪热启动准备好供使用的时间,因为onCreate在某些情况下不会被调用(例如,当用户按下主页按钮并从Fire TV的“Recent apps”行返回应用时,系统会直接将活动带到前台并仅调用onResume)。

使用WebView时,应在远程网页应用的UI完全加载后调用reportFullyDrawn()。请考虑重写应用中的WebViewClient#onPageFinished方法,以确保仅在页面加载时报告准备好供使用的时间,而不会在发生触摸、点击按钮等其他用户事件时报告。

注意:建议对应用日志进行实证分析,并确定在不同的活动生命周期状态下执行的操作,以便在适当的阶段可靠地触发reportFullyDrawn()

WebView实施示例:

Copied to clipboard
// MainActivity.java
public class MainActivity extends Activity {

    ...
    private static final Uri mBaseUri = Uri.parse("https://mysampleapp.com");

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_main);

        if (savedInstanceState == null) {
            getSupportFragmentManager().beginTransaction()
                .replace(R.id.main_browse_fragment, new MainFragment())
                .commitNow();
        }

        WebView mWebView = findViewById(R.id.web_layout);
        mWebView.getSettings().setJavaScriptEnabled(true);
        mWebView.loadUrl(mBaseUri.toString());
        mWebView.setWebViewClient(new WebViewClient() {
            @Override
            public void onPageFinished(WebView view, String url) {
                super.onPageFinished(view, url);

                reportFullyDrawn();
            }
        });
    }
    
    ...
}

改进应用启动时间

为了确保快速启动、流畅的用户体验以及没有阻碍的使用场景,请考虑您的应用将如何加载资源,以及快速启动体验的前期障碍都有哪些。此外,我们还有一篇介绍如何为Fire OS开发启动画面的相关开发文章。下面提供了一些改进应用启动时间的小技巧:

在设备而非模拟器上进行衡量

请务必在各种设备和Fire OS版本上进行测试,以确保您的应用快速启动并按预期运行。这有助于找出特定设备上可能发生的任何性能问题。虽然模拟器是用于在开发过程中进行快速功能检查的出色工具,但不应将它用于衡量性能,因为物理器件(例如CPU、内存和存储)的功能会在正确优化应用性能方面发挥重要作用。

不要将已发布应用的启动时间与侧载应用进行比较

如果应用没有通过亚马逊应用商店安装,则在物理设备上测量应用的性能时,显示的结果会存在差异。例如,在亚马逊应用商店发布应用的过程包括代码注入,旨在适应不适用于侧载应用的亚马逊应用商店服务。

要作为已发布的应用运行精确测试,请使用亚马逊的动态应用测试(LAT)服务。将您的应用上传到LAT后,应用将在亚马逊应用商店的生产环境中体验全套亚马逊服务。利用LAT有助于确保应用的运行方式和客户将会看到的发布在亚马逊应用商店的应用完全相同。

延迟加载、密集型任务延迟和应用启动性能分析

影响应用启动时间的最大因素之一是应用资源的大小。应用使用的资源越多,启动所需的时间就越长。请尝试使用尽可能少的资源,尤其是在应用首次启动时。这意味着使用更小的图像、更少的数据和更少的线程。

“延迟加载”是一项技术,让应用仅在需要相关资源时加载这些资源。如果您不需要在应用启动时初始化所有库,则可以延迟加载它们或禁用自动初始化。请使用Android Studio Profiler来确定哪些资源加载所需时间最长。由此可以通过减少前期加载的数据量来帮助缩短应用启动时间。

建议在启动时使用偏静态、可以快速绘制的简单UI,例如使用可以用来代替真实视图的视图占位符。只有在调用reportFullyDrawn()后,才应该激活和利用要求较高的动画、密集绘图运算和UI操作。还可以考虑最初显示静态数据,从而在不会进一步延迟应用启动的情况下,让用户沉浸在应用中。后台线程让您能够一边加载应用,一边在后台执行任务,从而有助于缩短应用启动时间。这有助于减少用户等待应用变得可供使用的时间。

考虑采用易用的数据管理方式

数据管理是应用快速启动的关键,并且需要对应用生命周期采取整体方法:想一想您希望在应用启动时向用户呈现哪些数据,然后在检索这些数据后将其永久存储。

请考虑不同存储方法的速度。例如,与使用平台的内置文件操作来存储数据相比,应用中加载的任何形式的数据库都很可能会降低启动速度。

为了解决该问题,请在应用启动时将相关数据直接序列化到文件系统。定义用于实施java.io.Serializable接口的数据表示方式,并利用Android的ObjectOutputStreamObjectInputStream类,如以下示例所示:

Copied to clipboard
public void serialize(Context context, String filename, Serializable object) throws Exception {
    FileOutputStream fOut = context.openFileOutput(filename, Context.MODE_PRIVATE);
    ObjectOutputStream oOut = new ObjectOutputStream(fOut);
    oOut.writeObject(object);
    oOut.close();
}

public Object deserialize(Context context, String filename) throws Exception {
    FileInputStream fIn = context.openFileInput(filename);
    ObjectInputStream oIn = new ObjectInputStream(fIn);
    Object result = oIn.readObject();
    oIn.close();

    return result;
}

此方法允许快速序列化和反序列化您想要最初呈现给用户的数据,并且与使用其他框架相比,可以显著加快应用启动速度。

多次测量后计算平均值

优化reportFullyDrawn()调用需要仔细分析各个应用生命周期阶段发生的情况。为此,Android提供了用于采集宏基准指标性能工具和示例。应在多次测量后计算平均值,以帮助了解您的应用是否在启动时尝试执行过多操作,或者尝试初始化过多资源。

为了帮助您实施这项改进措施,我们提供了一个示例脚本来自动对您的应用进行基准测试。请将com.example.myapplication.MainActivity分别替换为实际程序包名称和活动名称:

Copied to clipboard
// app_start_times.kts
import java.util.Scanner

private val CMD_LOGCAT = "adb logcat"
private val CMD_HOME = "adb shell input keyevent 3"
private val COOLDOWN_MILIS: Long = 1 * 1000

//APP PARAMETERS
private val PACKAGE_NAME = "com.example.myapplication"
private val ACTIVITY_NAME = "MainActivity"

//SCRIPT PARAMETERS
private var MEASURE_TIMES = 10
private var MEASURE_COLD_START = true
private var MEASURE_WARM_START = false

val logcatScanner = startAdbLogcat()

prepareApp(MEASURE_COLD_START, logcatScanner)
runMeasureLoop(MEASURE_COLD_START, MEASURE_TIMES, logcatScanner)

prepareApp(MEASURE_WARM_START, logcatScanner)
runMeasureLoop(MEASURE_WARM_START, MEASURE_TIMES, logcatScanner)

fun startAdbLogcat(): Scanner{
    val logcatProcess = Runtime.getRuntime().exec(CMD_LOGCAT)
    println("ADB Logcat Started")
    Thread.sleep(COOLDOWN_MILIS)
    val stream = logcatProcess.inputStream
    val scanner = Scanner(stream)
    scanner.useDelimiter("\n")
    return scanner
}

fun prepareApp(isWarmStart: Boolean, scanner: Scanner) {
    println("Step 1 - PREPARATION")
    val appStart = generateAppStartAdbCommand()
    val appStop = generateAppStopAdbCommand()
    val fullyDrawm = generateFullyDrawnLog()

    if(isWarmStart) {
        println("  - Opening app once and putting to background (for WARM START measure)")

        // To have proper WARM START benchmarks, open the app once,
        // wait until it is fully drawn and put it in the background
        Runtime.getRuntime().exec(appStart)
        waitForLog(fullyDrawm, scanner)
        Thread.sleep(COOLDOWN_MILIS)
        Runtime.getRuntime().exec(CMD_HOME)
    } else {
        println("  - Making sure that the app is closed (for COLD START measure)")
        Runtime.getRuntime().exec(appStop)
    }
}

fun runMeasureLoop(isWarmStart: Boolean, benchmarkTimes: Int, scanner: Scanner) {
    var totalMillis: Long = 0

    println("Step 2 - BENCHMARKING $benchmarkTimes TIMES")
    for(i in 1..benchmarkTimes) {
        println("  - Run $i")
        val benchmark = benchmarkApp(isWarmStart, scanner)
        totalMillis = totalMillis + benchmark
        Thread.sleep(COOLDOWN_MILIS)
    }

    println("Step 3 - RESULTS")
    println("  - Start type: ${if(isWarmStart) "WARM START" else "COLD START"}")
    println("  - Times benchmarked: $benchmarkTimes")
    println("  - Average start time: ${totalMillis/benchmarkTimes} milliseconds")
}

fun waitForLog(logToSearch: String, scanner: Scanner): String {
    while (scanner.hasNext()) {
        val str = scanner.next().trim()
        if(str.contains(logToSearch)) {
            return str
        }
    }

    return ""
}

fun benchmarkApp(isWarmStart: Boolean, scanner: Scanner): Long {
    val appStart = generateAppStartAdbCommand()
    val appStop = generateAppStopAdbCommand()
    val logToSearch = if(isWarmStart) generateFullyDrawnLog() else generateTimeToDisplayLog()

    Runtime.getRuntime().exec(appStart)
    val log = waitForLog(logToSearch, scanner)
    val millis = extractMiliseconds(log)

    Thread.sleep(COOLDOWN_MILIS)
    if(isWarmStart) {
        println("    * WARM START at $millis milliseconds")
        Runtime.getRuntime().exec(CMD_HOME)
    } else {
        println("    * COLD START at $millis milliseconds")
        Runtime.getRuntime().exec(appStop)
    }

    return millis
}

fun extractMiliseconds(logcat: String): Long {
    //1s234ms -> 1234 millis
    //345ms -> 345 millis
    var milis = logcat.split(" ").last().filter { it.isDigit() }
    return milis.toLong()
}

fun generateAppStartAdbCommand(): String {
    return "adb shell am start -n ${PACKAGE_NAME}/.${ACTIVITY_NAME}"
}

fun generateAppStopAdbCommand(): String {
    return "adb shell am force-stop ${PACKAGE_NAME}"
}

fun generateTimeToDisplayLog(): String {
    return "Displayed ${PACKAGE_NAME}/.${ACTIVITY_NAME}"
}

fun generateFullyDrawnLog(): String {
    return "Fully drawn ${PACKAGE_NAME}/.${ACTIVITY_NAME}"
}

结论

遵循上述最佳实践有助于缩短Fire TV应用的启动时间。通过提高应用的效率并运用最新工具和技术,您可以确保自己的应用能够提供出色的用户体验。

相关文章

最新文章

 

查看有关亚马逊应用商店、应用开发与盈利、亚马逊服务以及更多主题的最新消息。