普通视图

发现新文章,点击刷新页面。
昨天以前首页

Techniques, Philosophy, and Tools for Android Performance Optimization

作者 Gracker
2022年1月7日 00:42

黑客与画家

In his book Hackers & Painters, Paul Graham asserted, “The disparity in the efficiency of languages is becoming more pronounced, hence the rising importance of profilers. Currently, performance analysis isn’t given the attention it deserves. Many still seem to hold onto the belief that the key to accelerating program execution lies in developing compilers that generate faster code. As the gap between code efficiency and machine performance widens, it will become increasingly apparent that enhancing the execution speed of application software hinges on having a good profiler to guide program development.” by Paul Graham, Hackers & Painters

A Google search for “Android optimization tools” yields an abundance of related content. The issue with these results is that they either contain highly repetitive content or directly explain usage methods. Rarely do they introduce a holistic architecture, inadvertently instilling a misguided belief of “one tool fixes all”. Drawing from the extensive experience of my team, I can assert that in the realm of performance analysis, no such magic bullet tool exists. Tools evolve, old problems re-emerge in new forms, and without mastering core logic, one remains on the technological surface.

This article first systematically untangles the observability technology in performance analysis, encompassing data types, capture methods, and analysis techniques. Subsequently, we introduce the “big three” analysis tools provided by Google. The aim is to impart immutable theoretical knowledge and corresponding tools available in the Android environment to the reader. This wealth of information can facilitate a more direct application of predecessors’ experiences, circumventing unnecessary detours.

It’s crucial to note that there are certainly more than these three tools available for performance optimization. However, these three are our “go-to first-hand tools”. Prior to delving into further analysis, you’ll find yourself dependent on these three tools for bottleneck identification. Subsequent analyses, tailored to distinct domain characteristics, should then leverage corresponding tools.

1 Observability Techniques in Performance Analysis

  • Has this operation been executed? How long did it take?
  • Why is there a significant difference between two versions?
  • What operations is the system executing when CPU usage is high?
  • Why has the startup speed slowed down?
  • Why does this page always stutter when scrolling?

You’ve likely been asked similar questions by colleagues or bosses more than once. The most primitive idea might be to obtain the relevant logs and analyze them one by one. Based on past experience, one would search for clues by looking for keywords. If the desired information is not available, the next step is to add logs and attempt to reproduce the issue locally. This approach is not only time-consuming and laborious, but also wastes developmental resources. Have you ever wondered if there is a more efficient method in the industry? A method that can improve efficiency by an order of magnitude, allowing us to spend our time solving problems instead of on mundane, repetitive physical tasks?

Of course, there is (otherwise this article wouldn’t exist)—we refer to it as observability techniques.

As the computer industry has evolved, pioneers in computing have devised a category known as “observability techniques.” It involves utilizing tools to observe the intricate details of complex systems’ operations—the more detailed, the better. Mobile operating systems evolved from embedded systems. Nowadays, the computing power of mid-to-high-end Android phones can catch up with a mainframe from two decades ago, and the resulting software complexity is also immense.

Employing a well-designed and smoothly operating observability technique can significantly accelerate software development efficiency. This is because, despite using a variety of preemptive static code detection and manual code reviews, it is impossible to block software issues 100%. Problems only become apparent after the software is run in a real environment, which might be an automated test case of yours. Even then, you still need to sift through your logs and re-read code to identify the problem. For these reasons, every engineering team needs a fully functional observability tool as one of their fundamental infrastructures.

Observability is a systematic engineering effort that allows you to delve deeper into occurrences within the software. It can be used to understand the internal operational processes of software systems (especially complex business logic or interactive systems), troubleshoot, and even optimize the program by identifying bottlenecks. For complex systems, understanding the entire operational process through code reading can be challenging. A more efficient approach is to utilize observability tools to obtain the software’s operational status most intuitively.

We will explore data types, data acquisition methods, and analysis methods to help you understand observability techniques in the sections below.

1.1 Data Types

Logs can be in the form of key-value pairs, JSON, CSV, relational databases, or any other formats. We recreate the entire state of the system at the time it was running through logs to solve a specific issue, observe the operation of a module, or even depict the behavioral patterns of system users. In observability technology, log types are classified into Log, Metric, and Trace types.

Log Type

Logs are the most rudimentary form of data recording, typically noting what happened at what time in which module, whether the log level is a warning or an error. Nearly all systems, whether embedded devices or computers in cars, utilize this form of log. It is the simplest, most direct, and easiest to implement. Almost all Log types are stored as strings, presenting data in lines of text. Logs are the most basic type, and through conversion, can be turned into Metric or Trace types, though the conversion process can become a bottleneck when dealing with massive amounts of data.

Different log types are usually distinguished by error, warning, and debug levels. Naturally, error logs are your primary concern. However, in practice, this classification is not always strict, as many engineers do not differentiate between them, possibly due to a lack of classification analysis for different log levels in their engineering development environment. In summary, you can grade Log types according to your objectives. It acts like an index, enhancing the efficiency of problem analysis and target information location.

Metric Type

Metric types are more focused compared to Log types, recording numerical changes in a particular dimension. Key points are the “dimension” and “numerical change.” Dimensions could be CPU usage, CPU Cluster operation frequency, or context switch counts. Numerical changes can be instant values at the time of sampling (snapshot type), the difference from the previous sampling, or aggregated statistical values over a period. Statistics are often used in practice, such as when wanting to observe the average CPU usage five minutes before an issue occurred. In this case, an arithmetic mean or weighted average calculation of all values within these five minutes is required.

Aggregation is a useful tool because it’s not possible for a person to analyze all Metric values individually. Determining the existence of a problem through aggregation before conducting detailed analysis is a more economical and efficient method.

Another benefit of the Metric type is its fixed content format, allowing data storage through pre-encoding, utilizing space more compactly and occupying less disk space. The most straightforward application is data format storage; Metric types, using integers or floating numbers of fixed byte data, are more space-efficient than Log types, which generally use ASCII encoding.

In addition to specific values, enumeration values can also be stored (to some extent, their essence is numerical). Different enumeration values represent different meanings, possibly indicating on and off statuses, or different event types.

Trace Type

Trace types indicate the time, name, and duration of an event. Relationships among multiple events identify parent-child or sibling connections. Trace types are the most convenient data analysis method when dissecting complex call relationships across multiple threads.

Trace types are particularly suitable for Android application and system-level analysis scenarios because they can diagnose:

  1. Function call chains
  2. Binder call chains during invocation
  3. Cross-process event stream tracing

In the design of Android’s application running environment, an application can’t perform all functionalities independently; it requires extensive interaction with the SystemServer. Communication with the SystemServer is facilitated through Binder, a communication method detailed later in this article. For now, understand that it involves cross-process calling. Accurate restoration of call relationships requires data from both ends, making Trace the optimal information recording method.

You can manually add starting and ending points for Trace types and insert multiple intervals within a function. With pre-compilation technology or language features, Trace intervals can automatically be instrumented at the beginning and end of functions. In an ideal scenario, the latter is the best approach as it allows for understanding what functions are running in the system, their execution conditions, and call relationships. This information can identify the most frequently called (hottest) functions and the most time-consuming ones. Understandably, this method incurs a significant performance loss due to the frequency and magnitude of function calls, especially in complex systems.

An alternative approach involves approximating the above effect by sampling call stacks. Shorter sampling intervals more closely approximate real call relationships and durations, but they can’t be too short, as obtaining stack operations itself becomes a load due to increased frequency. This method, known as a Profiler in the industry, is the basis for most programming language Profiler tools.

1.2 Data Acquisition Methods

Static Code and Dynamic Tracing

Static code collection is the most primitive method. It’s straightforward to implement but requires recompiling and reinstalling the program each time new content is added. If the information you need to diagnose a problem isn’t available, you have no choice but to repeat the entire process. A more advanced approach is to pre-install data collection points at all potential areas of interest, and use dynamic switches to control their output. This technique balances performance impacts and allows dynamic enabling of logs as needed, albeit at a high cost.

Dynamic tracing technology has always been available but is often considered the “holy grail” in the debugging and tracing field due to its steep learning curve. It demands a deep understanding of low-level technologies, especially in areas like compilation, ELF format, the kernel, and programming languages associated with pre-set probes and dynamic tracing. Indeed, dynamic tracing even has its own set of programming languages to cater to the dynamic implementation needs of developers. This approach balances performance and flexibility and enables dynamic retrieval of desired information even in live versions.

In Android application development and system-level development, dynamic tracing is rarely used and is occasionally employed in kernel development. Typically, only specialized performance analysts might utilize these tools. Two critical elements of dynamic tracing are probes and dynamic languages. The program’s execution permission must be handed over to the dynamic tracing framework at specific probe points during runtime. The logic executed by the framework is written by developers using dynamic languages.

Therefore, your program must first have probes. Linux kernel and other frameworks have embedded corresponding probe points, but Android application layers lack these. Currently, dynamic frameworks like eBPF on Android are mainly used by kernel developers.

Unconditional and Conditional Capture

Unconditional capture is straightforward: data is continuously captured after triggering, regardless of any conditions. The drawback is that when the observed object generates a large volume of data, it could significantly impact the system. In such cases, reducing the volume of data captured can mitigate the impact, striking a balance between meeting requirements and minimizing performance loss.

Conditional capture is often employed in scenarios where anomalies can be identified. For instance, capturing logs is triggered when a specific observed value exceeds a pre-set threshold and continues for a certain duration or until another threshold is reached. This method is a slight improvement over unconditional capture as it only impacts the system when an issue arises, leaving it unaffected at other times. However, it requires the capability to identify anomalies, and those anomalies should not necessitate historical data preceding the occurrence. Lowering the threshold can increase the probability of triggering data capture, leading to the same issues faced with unconditional capture, and requiring a balance of performance loss.

Disk Write Strategy

Continuous disk writing involves storing all data captured during the entire data capture process, which can strain storage resources. If the trigger point, such as an anomaly, can be identified, selective disk writing becomes an option. To ensure the validity of historical data, logs are temporarily stored in a RingBuffer and only written to disk upon receiving a disk write command. This method balances performance and storage pressure but at the cost of runtime memory consumption and the accuracy of the trigger.

1.3 Analysis Methods

Data Visualization Analysis

As the complexity of problem analysis increases, especially with the need to address performance issues arising from the interactions among multiple modules, data visualization analysis methods have emerged. These methods visualize events on respective lanes with time as the horizontal axis, facilitating a clear understanding of when specific events occur and their interactions with other systems. In Android, tools like Systrace/Perfetto and, earlier, KernelShark, are fundamentally of this type. The “Trace Type” mentioned in “Data Types” often employs this kind of visualization.

Systrace’s visualization framework is built on a Chrome subproject called Catapult. The Trace Event Format outlines the data formats supported by Catapult. If you have Trace type data, you can use this framework for data visualization. AOSP build systems and the Android app compilation process also output corresponding Trace files, with visualization effects based on Catapult.

Database Analysis

For extensive data analysis, formatting data and converting it into two-dimensional data tables enables efficient query operations using SQL. In the server domain, technology stacks like ELK offer flexible formatted search and statistical functions. With databases and Python, you can even create an automated data diagnostic toolchain.

From the discussion above, it’s evident that text analysis and database analysis serve different analytical purposes. Text analysis is sufficient for evaluating the time consumption of a single module, visualization tools are needed for interactions among multiple systems, and SQL tools are required for complex database analysis. Regardless of the analysis method, the core is data analysis. In practice, we often convert data using other tools to support different analysis methods, such as transitioning from text analysis to database analysis.

Choosing the right analysis method according to your objectives can make your work highly efficient.

For Android developers, Google provides several essential performance analysis tools to assist both system and app developers in optimizing their programs.

2 Google’s Android Performance Analysis Tools

Based on practical experience, the most commonly used tools are Systrace, Perfetto, and the Profiler tool in Android Studio. Only after identifying the main bottlenecks using these tools would you need to resort to other domain-specific tools. Therefore, we will focus on the application scenarios, advantages, and basic usage of these three tools. For a horizontal comparison between the tools, please refer to the content in the next chapter, “Comprehensive Comparison.”

2.1 First Generation System Performance Analysis Tool - Systrace

Systrace is a visualization analysis tool for the Trace type and represents the first generation of system-level performance analysis tools. It supports all the features facilitated by the Trace type. Before the emergence of Perfetto, Systrace was essentially the only performance analysis tool available. It presents the operating information of both the Android system and apps graphically. Compared to logs, Systrace’s graphical representation is more intuitive; and compared to TraceView, the performance overhead of capturing Systrace can be virtually ignored, minimizing the impact of the observer effect to the greatest extent.

Systrace

Systrace Design Philosophy

Systrace embeds information similar to logs, known as TracePoints (essentially Ftrace information), at key system operations (such as Touch operations, Power button actions, sliding operations, etc.), system mechanisms (including input distribution, View drawing, inter-process communication, process management mechanisms, etc.), and software and hardware information (covering CPU frequency information, CPU scheduling information, disk information, memory information, etc.). These TracePoints depict the execution time of core operation processes and the values of certain variables. The Android system collects these TracePoints scattered across various processes and writes them into a file. After exporting this file, Systrace analyzes the information from these TracePoints to obtain the system’s operational information over a specific period.

In the Android system, some essential modules have default inserted TracePoints, classified by TraceTag, with information sources as follows:

  1. TracePoints in the Framework Java layer are implemented through the android.os.Trace class.
  2. TracePoints in the Framework Native layer are executed using the ATrace macro.
  3. App developers can customize Trace through the android.os.Trace class.

Consequently, Systrace can collect and display all information from both upper and lower layers of Android. For Android developers, Systrace’s most significant benefit is turning the entire Android system’s operational status from a black box into a white box. Its global nature and visualization make Systrace the first choice for Android developers when analyzing complex performance issues.

Practical Applications

The parsed Systrace, rich in system information, is naturally suited for analyzing the performance issues of both Android Apps and the Android system. Android app developers, system developers, and kernel developers can all use Systrace to diagnose performance problems.

  1. From a Technical Perspective:
    Systrace can cover major categories involved in performance, such as response speed, frame drops or janks, and ANR (Application Not Responding) issues.

  2. From a User Perspective:
    Systrace can analyze various performance issues encountered by users, including but not limited to:

    • Application Launch Speed Issues: Including cold start, warm start, and hot start.
    • Slow Interface Transitions: Including slow transitions and janky animations.
    • Slow Non-Transition Click Operations: Such as toggles, pop-ups, long presses, selections, etc.
    • Slow Screen Brightness Adjustment Speed: Including slow on/off speed, slow unlocking, slow face recognition, etc.
    • List Scrolling Jankiness:
    • Window Animation Lag:
    • Interface Loading Jankiness:
    • Overall System Lag:
    • App Unresponsiveness: Including freeze and crash issues.

When encountering the above problems, various methods can be employed to capture Systrace. The parsed file can then be opened in Chrome for analysis.

The ability to trace and visualize these issues makes Systrace an invaluable tool for developers aiming to optimize the performance of Android applications and the system itself. By analyzing the data collected, developers can identify bottlenecks and problematic areas, formulate solutions, and effectively improve the performance and responsiveness of apps and the Android operating system.

2.2 The Next-Generation Performance Analysis Full Stack Tool - Perfetto

Google initiated the first submission in 2017, and over the next four years (up until Dec 2021), over 100 developers made close to 37,000 commits. There are PRs and merges almost daily, marking it as an exceptionally active project. Besides its powerful features, its ambition is significant. The official website claims it to be the next-generation cross-platform tool for Trace/Metric data capture and analysis. Its application is also quite extensive; apart from the Perfetto website, Windows Performance Tool, Android Studio, and Huawei’s GraphicProfiler also support the visualization and analysis of Perfetto data. We believe Google will continue investing resources in the Perfetto project. It is poised to be the next-generation performance analysis tool, wholly replacing Systrace.

Highlighted Features

The most significant improvement of Perfetto over Systrace is its ability to support long-duration data capture. This is made possible by a service that runs in the background, enabling the encoding of collected data using Protobuf and saving it to disk. From the perspective of data sourcing, the core principle is consistent with Systrace, both based on the Linux kernel’s Ftrace mechanism for recording key events in both user and kernel spaces (ATRACE, CPU scheduling). Perfetto supports all functionalities provided by Systrace, hence the anticipation of Systrace being replaced by Perfetto entirely.

Perfetto

Perfetto’s support for data types, acquisition methods, and analysis approaches is unprecedentedly comprehensive. It supports virtually all types and methods. ATRACE enables the support for Trace type, a customizable node reading mechanism supports Metric type, and in UserDebug versions, Log type support is realized by obtaining Logd data.

You can manually trigger capture and termination via the Perfetto.dev webpage or command-line tools, initiate long-duration capture via the developer options in the settings, or dynamically start data capture via the Perfetto Trigger API integrated within the framework. This covers all scenarios one might encounter in a project.

In terms of data analysis, Perfetto offers a data visualization analysis webpage similar to Systrace, but with an entirely different underlying implementation. The biggest advantage is its ability to render ultra-large files, a feat Systrace cannot achieve (it might crash or become extremely laggy with files over 300M). On this visualization webpage, one can view various processed data, execute SQL query commands, and even view logcat content. Perfetto Trace files can be converted into SQLite-based database files, enabling on-the-spot SQL execution or running pre-written SQL scripts. You can even import it into data science tool stacks like Jupyter to share your analysis strategies with colleagues.

For example, if you want to calculate the total CPU consumption of the SurfaceFlinger thread, or identify which threads are running on large cores, etc., you can collaborate with domain experts to translate their experiences into SQL commands. If that still does not meet your requirements, Perfetto also offers a Python API, converting data into DataFrame format, enabling virtually any desired data analysis effect.

With all these offerings, developers have abundant aspects to explore. From our team’s practical experience, it can almost cover every aspect from feature development, function testing, CI/CD, to online monitoring and expert systems. In the subsequent series of articles on our planet, we will focus on Perfetto’s powerful features and the expert systems developed based on it, aiding you in pinpointing performance bottlenecks with a single click.

Practical Application

Perfetto has become the primary tool used in performance analysis, with Systrace’s usage dwindling. Hence, the tool you should master first is Perfetto, learning its usage and the metrics it provides.

However, Perfetto has its boundaries. Although it offers high flexibility, it essentially remains a static data collector and not a dynamic tracing tool, fundamentally different from eBPF. The runtime cost is relatively high because it involves converting Ftrace data to Perfetto data on the mobile device. Lastly, it doesn’t offer text analysis methods; additional analyses can only be performed via webpage visualization or operating SQLite. In summary, Perfetto is powerful, covering almost every aspect of observability technology, but also has a relatively high learning curve. The knowledge points worth exploring and learning are plentiful, and we will focus on this part in our upcoming articles.

2.3 Android Studio Profiler Tool

The integrated development environment for Android application development (officially recommended) is Android Studio (previously it was Eclipse, but that has been phased out). It naturally needs to integrate development and performance optimization. Fortunately, with the iterations and evolution of Android Studio, it now has its own performance analysis tool, Android Profiler. This is a collective tool integrating several performance analysis utilities, allowing developers to optimize performance without downloading additional tools while developing applications in Android Studio.

Currently, Android Studio Profiler has integrated four types of performance analysis tools: CPU, Memory, Network, and Battery. The CPU-related performance analysis tool is the CPU Profiler, the star of this chapter. It integrates all CPU-related performance analysis tools, allowing developers to choose based on their needs. Many people might know that Google has developed some independent CPU performance analysis tools, like Perfetto, Simpleperf, and Java Method Trace. CPU Profiler does not reinvent the wheel; it gathers data from these known tools and parses it into a desired style, presenting it through a unified interface.

Highlighted Features

CPU Profiler integrates performance analysis tools: Perfetto, Simpleperf, and Java Method Trace. It naturally possesses all or part of the functionalities of these tools, such as:

  1. System Trace Recording: Information captured with Perfetto, useful for analyzing process function duration, scheduling, rendering, etc. However, it’s a simplified version, only displaying process-strongly related information and filtering out short-duration events. It’s recommended to export the Trace file for analysis on https://ui.perfetto.dev/.
  2. Java Method Trace Recording: It gathers function call stack information from the virtual machine, used for analyzing Java function calls and duration.
  3. C/C++ Function Trace: Information captured with Simpleperf. Simpleperf gathers data from the CPU’s performance monitoring unit (PMU) hardware component. C/C++ Method Trace has only partial functionalities of Simpleperf, used for analyzing C/C++ function calls and durations.

CPU Profiler

Practical Application

Application performance issues are mainly divided into two categories: slow response and lack of smoothness.

  • Slow response issues include slow app startup, slow page transitions, slow list loading, slow button responses, etc.
  • Lack of smoothness issues include unsmooth list scrolling, page sliding not following hand movements, animation judders, etc.

How to use CPU Profiler in these scenarios? The basic approach is to capture a System Trace first, analyze and locate the issue with System Trace. If the issue can’t be pinpointed, further analysis and location should be done with Java Method Trace or C/C++ Function Trace.

Taking an extremely poor-performing application as an example, suppose Systrace TracePoint is inserted at the system’s critical positions and the code is unfamiliar. How do you identify the performance bottleneck? First, run the application and record a System Trace with CPU Profiler (the tool usage will be introduced in later articles), as shown below:

From the above Trace, it’s evident that the onDrawFrame operation in the egl_core thread is time-consuming. If the issue isn’t apparent, it’s advised to export it to https://ui.perfetto.dev/ for further analysis. By looking into the source code, we find that onDrawFrame is the duration of the Java function onDrawFrame. To analyze the duration of the Java function, we need to record a Java Method Trace, as follows:

From the above Trace, it’s easy to see that a native function called Utils.onDraw is time-consuming. Because it involves C/C++ code, another C/C++ Function Trace needs to be recorded for further analysis, as shown below:

It becomes clear that the code executed a sleep function within the native Java_com_gl_shader_Utils_onDraw, pinpointing the culprit for the poor performance!

The greatest advantage of CPU Profiler in AS is the integration of various sub-tools, enabling all operations in one place. It’s incredibly convenient for application developers. However, system developers might not be so lucky.

2.4 Comparative Analysis

Tool NameApplication ScenarioData TypeData Acquisition MethodAnalysis Method
SystraceAndroid System & App Performance AnalysisTrace TypeUnconditional Capture, Continuous LoggingVisual Analysis
PerfettoAndroid System & App Performance AnalysisMetric Type, Trace TypeUnconditional Capture, Continuous LoggingVisual Analysis, Database Analysis
AS ProfilerAndroid System & App Performance AnalysisTrace TypeUnconditional Capture, Continuous LoggingVisual Analysis
SimplePerfJava/C++ Function Execution Time Analysis, PMU CountersTrace TypeUnconditional Capture, Continuous LoggingVisual Analysis, Text Analysis
Snapdragon Profiler Tools & ResourcesPrimarily for Qualcomm GPU Performance AnalyzerTrace Type, Metric TypeUnconditional Capture, Continuous LoggingVisual Analysis
Mali Graphics DebuggerARM GPU Analyzer (for MTK, Kirin chips)Trace Type, Metric TypeUnconditional Capture, Continuous LoggingVisual Analysis
Android Log/dumpsysComprehensive AnalysisLog TypeConditional Capture, Continuous Capture but not LoggingText Analysis
AGI (Android GPU Inspector)Android GPU AnalyzerTrace Type, Metric TypeUnconditional Capture, Continuous LoggingVisual Analysis
eBPFDynamic Tracing of Linux Kernel BehaviorMetric TypeDynamic Tracing, Conditional Capture, Continuous Capture but not LoggingText Analysis
FTraceLinux Kernel TracingLog TypeStatic Code, Conditional Capture, Continuous Capture but not LoggingText Analysis

3 On “Instruments, Techniques, Philosophy”

Technical revolutions and improvements are often reflected at the “instruments” level. The development direction of tools by the Linux community and Google is towards enhancing the integration of tools so that necessary information can be easily found in one place, or towards the collection of more information. In summary, the development trajectory at the instruments level is traceable and developmental rules can be summarized. We need to accurately understand their capabilities and application scenarios during rapid iterations of tools, aiming to improve problem-solving efficiency rather than spending time learning new tools.

The “techniques” level depends on specific business knowledge, understanding how a frame is rendered, how the CPU selects processes for scheduling, how IO is dispatched, etc. Only with an understanding of business knowledge can one choose the right tools and correctly interpret the information provided by these tools. With rich experience, sometimes you can spot clues even without looking at the detailed information provided by tools. This is a capability that arises when your business knowledge is enriched to a certain extent, and your brain forms complex associative information, elevating you above the tools.

At the “philosophy” level, considerations are about the nature of the problem that needs to be solved. What is the essence of the problem? What extent should be achieved, and what cost should be incurred to achieve what effect? For solving a problem, which path has the highest “input-output ratio”? What is the overall strategy? To accomplish something, what should be done first and what should be done next, and what is the logical dependency relationship?

In subsequent articles, explanations will be provided in the “instruments, techniques, philosophy” manner for a technology or a feature. We aim not only to let you learn a knowledge point but also to stimulate your ability to extrapolate. When faced with similar tools or problems, or even completely different systems, you can handle them with ease. Firmly grasping the essence, you can choose the appropriate tools or information through evaluating the “input-output ratio” and solve problems efficiently.

About Me && Blog

  1. About Me: I am eager to interact and progress together with everyone.
  2. Follow me on Twitter
  3. Blog Content Navigation
  4. Record of Excellent Blog Articles - Essential Skills and Tools for Android Performance Optimization

An individual can move faster, a group can go further.

Android 性能优化的术、道、器

作者 Gracker
2022年1月7日 00:42

黑客与画家

Paul Graham 在其著作 <黑客与画家> 中断言:“不同语言的执行效率差距正变得越来越大,所以性能分析器(profiler)将变得越来越重要。目前,性能分析并没有受到重视。许多人好像仍然相信,程序运行速度提升的关键在于开发出能够生成更快速代码的编译器。代码效率与机器性能的差距正在不断加大,我们将会越来越清楚地看到,应用软件运行速度提升的关键在于有一个好的性能分析器帮助指导程序开发。”
by Paul Graham 黑客与画家

谷歌搜索 「Android 优化工具」,你会找到很多与此相关的内容。他们的问题在于要么是内容高度重复、要么是直接讲使用方法,很少会给你介绍整体性的架构,一不小心就会让人会种「一个工具搞定一切」的错误认知。以笔者团队的多年经验来看,在性能分析领域这种银弹级别的工具是不存在的。工具在发展,老问题会以新的方式变样出现,不掌握核心逻辑的话始终会让你浮于技术的表面。

本文首先系统性的梳理性能分析中的可观测性技术,它涵盖数据类型、抓取方法以及分析方法等三部分内容,之后是介绍谷歌提供的「三大件」分析工具。目的是想让你了解不变的理论性的知识,以及与之对应的在安卓环境中可用的工具,这些可以让你少走一些弯路,直接复用前辈们的经验。

需要特别说明的是,对于性能优化肯定不止有这三个工具可用,但这个三个工具是我们平时用到的「第一手工具」。进行进一步分析之前,你都需要依赖这三个工具进行瓶颈定位,之后才应不同领域特性选择对应的工具进行下钻分析。

1 性能分析中的可观测性技术

  • 这个操作到底有没有被执行?执行时间有多长?
  • 为什么两个版本的前后差异这么大?
  • 当 CPU 使用量变高的时候系统都在执行什么操作?
  • 为什么启动速度变慢了?
  • 为什么这个页面滑动总是会卡一下?

相信你不止一次被同事、被老板问到过类似的问题。最原始的想法应该是,首先是拿到相关的日志进行逐个分析。根据以往经验,通过查找关键字寻找蛛丝马迹。如果没有想看的信息,那就加上日志尝试本地复现。费时费力不说,也还费研发资源。但你有没有想过行业里有没有更高效的方法?可以提高一个数量级的那种,把我们的时间花在问题解决上而不是无聊的重复性体力活儿上?

答案当然是有的(否则就不会有这篇文章了),我们称他为可观测性技术。

计算机行业发展至今,计算机前辈们捣鼓出了所谓的「可观测性技术」的类别。它研究的是通过工具,来观测复杂系统的运行细节,内容越细越好。 移动操作系统之前是由嵌入式发展而来的,现在的中高端安卓手机算力都能赶得上二十几年前的一个主机的算力,在此算力基础上所带来的软件复杂度也是非常巨大的。

如果你的程序部署了一个精心设计且运行良好的可观测性技术,可以大大加快研发软件的效率,因为即使我们使用了各种各样的前置性静态代码检测、人工代码审查,也无法 100% 拦截软件的问题。只有在真实环境里运行之后才知道是否真正发生了问题,即使这个环境可能是一个你的自动化测试用例。即使这样,你还需要翻阅你的日志,重读代码来找出问题。出于这些原因,每个工程团队都需要有一个功能完备的可观测性工具作为他们的基础设施之一。

可观测性技术是一个系统性工程,它能够让你更深入的了解软件里发生的事情。可用于了解软件系统内部运行过程(特别是对于业务逻辑或者交互关系复杂的系统)、排查问题甚至通过寻找瓶颈点优化程序本身。对于复杂的系统来说,你通过阅读代码来了解整个运行过程其实是很困难的事情,更高效的方法就是借助此类工具,以最直观的的方式获取软件运行的状态。

下面将从 数据类型、数据获取方法、分析方法 这三个主题来帮助你了解可观测性技术。

1.1 数据类型

日志的形式可能是键值对(key=Value),JSON、CSV,关系型数据库或者其他任何格式。其次我们通过日志还原出系统当时运行的整个状态,目的是为了解决某个问题,观察某个模块的运行方式,甚至刻画系统使用者的行为模式。在可观测性技术上把日志类型分类为 Log 类型、Metric 类型,以及 Trace 类型。

数据类型

Log 类型

Log 是最朴素的数据记录方式,一般记录了什么模块在几点发生了什么事情,日志等级是警告还是错误。 绝大部分系统,不管是嵌入式设备还是汽车上的计算机,他们所使用的日志形式几乎都是这种形式。这是最简单,最直接也最好实现的一种方式。几乎所有的 Log 类型是通过 string 类型的方式存储,数据呈现形式是一条一条的文本数据。Log 是最基本的类型,因此通过转换,可以将 Log 类型转换成 Metric 或者 Trace 类型,当然成本就是转换的过程,当数据量非常巨大的时候这可能会成为瓶颈。

为了标识出不同的日志类型等级,一般使用错误、警告、调试等级别来划分日志等级。显然,错误类型的是你首要关注的日志等级。不过实践中也不会严格按照这种方式划分,因为很多工程师不会严格区分他们之间的差异,这可能是他们的工程开发环境中不太会对不同等级的日志进行分类分析有关。总之,你可以根据你的目的,将 Log 类型进行等级划分,它就像一个索引一样,可以进一步可以提高分析问题、定位目标信息的效率。

Metric 类型

Metric 类型相比 Log 类型使用目的上更为聚焦,它记录的是某个维度上数值的变化。知识点是「维度」与「数值」的变化。维度可能是 CPU 使用率、CPU Cluster 运行频率,或者上下文切换次数。数值变化既可以是采样时候的瞬时值(成为快照型)、与前一次采样时的差值(增或减)、或者某个时段区间的统计聚合值。实践中经常会使用统计值,比如我想看问题发生时刻前 5 分钟的 CPU 平均使用量。这时候需要将这五分钟内的所有数值做算数平均计算,或者是加权平均(如: 离案发点越近的样本它的权重就越高)。Log 类型当然可以实现 Metric 类型的效果,但是操作起来非常麻烦而且其性能损耗可能也不小。

聚合是非常有用的工具,因为人不可能逐个分析所有的 Metric 值,因此借助聚合的方式判断是否出了问题之后再进行详细的分析是更为经济高效的方法。

Metric 类型的另外一个好处是它的内容格式是比较固定的,因此可以通过预编码的方式进行数据存储,空间的利用率会更紧凑进而占用的磁盘空间就更少。最简单的应用就是数据格式的存储上,如果使用 Log 类型,一般采用的是 ASCII 编码,而 Metric 使用的是整数或者浮点等固定 byte 数的数据,当存储较大数值时显然 ASCII 编码需要的字节数会多于数字型数据,并且在进行数据处理的时候你可以直接使用 Metric 数据,而不需要把 Log 的 ASCII 转换成数字型后再做转换。

除了是具体的数值之外,也可以存储枚举值(某种程度上它的本质就是数值)。不同的枚举值代表不同的意义,可能是开和关、可能是不同的事件类型。

Trace 类型

Trace 类型标识了事件发生的时间、名称、耗时。多个事件通过关系,标识出了是父与子还是兄弟。当分析多个线程间复杂的调用关系时 Trace 类型是最方便的数据分析方式。

Trace 类型特别适用于 Android 应用与系统级的分析场景,因为用它可以诊断:

  1. 函数调用链
  2. Binder 调用时的调用链
  3. 跨进程事件流跟踪

Android 的应用程序运行环境的设计中,一个应用程序是无法独自完成所有的功能的,它需要跟 SystemServer 有大量的交互才能完成它的很多功能。与 SystemServer 间的通讯是通过 Binder 完成,它的通讯方式后面的文章再详细介绍,到目前为止你只需要知道它的调用关系是跨进程调用即可。这需要本端与远端的数据才能准确还原出调用关系,Trace 类型是完成这种信息记录的最佳方式。

Trace 类型可以由你手动添加开始与结束点,在一个函数里可以添加多个这种区间。通过预编译技术或者编程语言的特性,在函数的开头与结尾里自动插桩 Trace 区间。理想情况下后者是最好的方案,因为我们能知道系统中运行的所有的函数是哪些、执行情况与调用关系是什么。可以拿这些信息统计出调用次数最多(最热点)的函数是什么,最耗时的函数又是什么。可想而知这种方法带来的性能损耗非常大,因为函数调用的频次跟量级是非常大的,越是复杂的系统量级就越大。

因此有一种迂回的方法,那就通过采样获取调用栈的方式近似拟合上面的效果。采样间隔越短,就越能拟合真实的调用关系与耗时,但间隔也不能太小因为取堆栈的操作本身的负载就会变高因为次数变多了。这种方法,业界管他叫 Profiler,你所见过的绝大部分编程语言的 Profiler 工具都是基于这个原理实现的。

1.2 数据获取方法

数据获取方法

静态代码与动态跟踪

静态代码的采集方式是最原始的方式,优点是实现简单缺点是每次新增内容的时候需要重新编译、安装程序。当遇到问题之后你想看的信息恰好没有的话,就没有任何办法进一步定位问题,只能重新再来一遍整个过程。更进一步的做法是预先把所有可能需要的地方上加入数据获取点,通过动态判断开关的方式选择是否输出,这既可以控制影响性能又能够在需要日志的时候可以动态打开,只不过这种方法的成本非常高。

动态跟踪技术其实一直都存在,只是它的学习成本比较高,被誉为调试跟踪领域里的屠龙刀。它需要你懂比较底层的技术,特别是编译、ELF 格式、内核、以及熟悉代码中的预设的探针、动态跟踪所对应的编程语言。对,你没看错,这种技术甚至还有自己的一套编程语言用于「动态」的实现开发者需求。这种方式兼具性能、灵活性,甚至线上版本里遇到异常后可以动态查看你想看的信息。

Android 应用开发、系统级开发中用的比较少,内核开发中偶尔会用一些。只有专业、专职的性能分析人员才可能会用上这类工具。它有两个关键点,探针与动态语言,程序运行过程中需要有对应的探针点将程序执行权限交接到动态跟踪框架,框架执行的逻辑是开发者使用动态语言来编写的逻辑。

所以,你的程序里首先是要有探针,好在 Linux 内核等框架埋好了对应的探针点,但是 android 应用层是没有现成的。所以目前 Android 上能用动态框架,如 eBPF 基本都是内核开发者在使用。

无条件式抓取与有条件式抓取

无条件式抓取比较好理解,触发抓取之后不管发生任何事情,都会持续抓取数据。缺点是被观测对象产生的数据量非常大的时候可能会对系统造成比较大的影响,这种时候只能通过降低数据量的方式来缓解。需要做到既能满足需求,性能损失又不能太大。

有条件式抓取经常用在可以识别出的异常的场景里。比如当系统的某个观测值超过了预先设定的阈值时,此时触发抓取日志并且持续一段时间或者达到另外一种阈值之后结束抓取。这相比于前面一个方法稍微进步了一些,仅在出问题的时候对系统有影响,其他时候没有任何影响点。但它需要你能够识别出异常,并且这种异常是不需要异常发生之前的历史数据。当然你可以通过降低阈值来更容易达到触发点,这可能会提高触发数据抓取的概率,这时候会遇到前面介绍的无条件式抓取遇到的同样的问题,需要平衡性能损失。

落盘策略

持续落盘是存储整个数据抓取过程中的所有数据,代价是存储的压力。如果能知道触发点,比如能够检测到异常点,这时候可以选择性的落盘。为了保证历史数据的有效性,因此把日志先暂存储到 RingBuffer 中,只有接受到落盘指令后再进行落盘存储。这种方式兼顾了性能与存储压力,但成本是运行时内存损耗与触发器的准确性。

1.3 分析方式

分析方式

数据可视化分析

随着问题分析的复杂化,出现了要解决多个模块间交互的性能问题需求,业界就出现了以时间为横轴把对应事件放到各自泳道上的数据可视化分析方法,可以方便的看到所关心事件什么时候发生、与其他系统的交互信息等等。在 Android 里我们常用的 Systrace/Perfetto 以及更早之前的 KernelShark 等工具本质上都是这一类工具。在「数据类型」提到的 「Trace 类型」,经常采用这种可视化分析方法。

Systrace 的可视化框架是基于 Chrome 的一个叫 Catapult 的子项目构建。Trace Event Format 讲述了 Catapult 所支持的数据格式,如果你有 Trace 类型的数据,完全可以使用此框架来展示可视化数据。AOSP 编译系统,安卓应用的编译过程,也都有相应的 Trace 文件输出,它们也都基于 Catapult 实现了可视化效果。

数据库分析

面对大量数据分析的分析,通过对数据进行格式化,把他们转换成二维数据表,借助 SQL 语言可实现高效的查询操作。在服务器领域中 ELK 等技术栈可以实现更为灵活的格式化搜索与统计功能。借助数据库与 Python,你甚至可以实现一套自动化数据诊断工具链。

从上面的讨论可知,从文本分析到数据库分析他们要面对的分析目的是不一样的。单纯的看一个模块的耗时用文本分析就够用了,多个系统间的交互那就要用可视化工具,复杂的数据库分析就要用到 SQL 的工具。无论哪种分析方式,本质上都是针对数据的分析,在实战中我们经常会通过其他工具对数据进行转换以支持不同的分析方式,比如从文本分析方式改成数据库分析方式。

根据自己的目的,选择合适的分析方式才会让你的工作事倍功半。

对于 Android 开发者来说,Google 提供了几个非常重要的性能分析工具,帮助系统开发者、应用开发者来优化他们的程序。

2 谷歌提供的 Andorid 性能分析工具

从实践经验来看最常用的工具有 Systrace,Perfetto 与 Android Studio 中的 Profiler 工具。通过他们定位出主要瓶颈之后,你才需要用到其他领域相关工具。因此,会重点介绍这三个工具的应用场景,它的优点以及基本的使用方法。 工具之间的横向对比,请参考下一个「综合对比」这一章节的内容。

2.1 初代系统性能分析工具 - Systrace

Systrace 是 Trace 类型的可视化分析工具,是第一代系统级性能分析工具。Trace 类型所支持的功能它都有支持。在 Perfetto 出现之前,基本上是唯一的性能分析工具,它将 Android 系统和 App 的运行信息以图形化的方式展示出来,与 Log 相比,Systrace 的图像化方式更为直观;与 TraceView 相比,抓取 Systrace 时候的性能开销基本可以忽略,最大程度地减少观察者效应带来的影响。

Systrace

Systrace 的设计思路

系统的一些关键操作(比如 Touch 操作、Power 按钮、滑动操作等)、系统机制(input 分发、View 绘制、进程间通信、进程管理机制等)、软硬件信息(CPU 频率信息、CPU 调度信息、磁盘信息、内存信息等)的关键流程上,插入类似 Log 的信息,我们称之为 TracePoint(本质是 Ftrace 信息),通过这些 TracePoint 来展示一个核心操作过程的执行时间、某些变量的值等信息。然后 Android 系统把这些散布在各个进程中的 TracePoint 收集起来,写入到一个文件中。导出这个文件后,Systrace 通过解析这些 TracePoint 的信息,得到一段时间内整个系统的运行信息。

Android 系统中,一些重要的模块都已经默认插入了一些 TracePoint,通过 TraceTag 来分类,其中信息来源如下

  1. Framework Java 层的 TracePoint 通过 android.os.Trace 类完成
  2. Framework Native 层的 TracePoint 通过 ATrace 宏完成
  3. App 开发者可以通过 android.os.Trace 类自定义 Trace

这样 Systrace 就可以把 Android 上下层的所有信息都收集起来并集中展示,对于 Android 开发者来说,Systrace 最大的作用就是把整个 Android 系统的运行状态,从黑盒变成了白盒。全局性和可视化使得 Systrace 成为 Android 开发者在分析复杂的性能问题的时候的首选。

实践中的应用情况

解析后的 Systrace 由于有大量的系统信息,天然适合分析 Android App 和 Android 系统的性能问题, Android 的 App 开发者、系统开发者、Kernel 开发者都可以使用 Systrace 来分析性能问题。

  1. 从技术角度来说,Systrace 可覆盖性能涉及到的 响应速度卡顿丢帧ANR 这几个大类。
  2. 从用户角度来说,Systrace 可以分析用户遇到的性能问题,包括但不限于:
    1. 应用启动速度问题,包括冷启动、热启动、温启动
    2. 界面跳转速度慢、跳转动画卡顿
    3. 其他非跳转的点击操作慢(开关、弹窗、长按、选择等)
    4. 亮灭屏速度慢、开关机慢、解锁慢、人脸识别慢等
    5. 列表滑动卡顿
    6. 窗口动画卡顿
    7. 界面加载卡顿
    8. 整机卡顿
    9. App 点击无响应、卡死闪退

在遇到上述问题后,可以使用多种方式抓取 Systrace ,将解析后的文件在 Chrome 打开,然后就可以进行分析

2.2 新一代性能分析全栈工具 - Perfetto

谷歌在 2017 年开始了第一笔提交,随后的 4 年(截止到 2021.12)内总共有 100 多位开发者提交了近 3.7W 笔提交,几乎每天都有 PR 与 Merge 操作,是一个相当活跃的项目。 除了功能强大之外其野心也非常大,官网上号称它是下一代面向可跨平台的 Trace/Metric 数据抓取与分析工具。应用也比较广泛,除了 Perfetto 网站,Windows Performance ToolAndroid Studio,以及华为的 GraphicProfiler 也支持 Perfetto 数据的可视化与分析。 我们相信谷歌还会持续投入资源到 Perfetto 项目,可以说它应该就是下一代性能分析工具了,会完全取代 Systrace。

提供的亮点功能

Perfetto 相比 Systrace 最大的改进是可以支持长时间数据抓取,这是得益于它有一个可在后台运行的服务,通过它实现了对收集上来的数据进行 Protobuf 的编码并存盘。从数据来源来看,核心原理与 Systrace 是一致的,也都是基于 Linux 内核的 Ftrace 机制实现了用户空间与内核空间关键事件的记录(ATRACE、CPU 调度)。Systrace 提供的功能 Perfetto 都支持,由此才说 Systrace 最终会被 Perfetto 替代。

Perfetto

Perfetto 所支持的数据类型、获取方法,以及分析方式上看也是前所未有的全面,它几乎支持所有的类型与方法。数据类型上通过 ATRACE 实现了 Trace 类型支持,通过可定制的节点读取机制实现了 Metric 类型的支持,在 UserDebug 版本上通过获取 Logd 数据实现了 Log 类型的支持。

你可以通过 Perfetto.dev 网页、命令行工具手动触发抓取与结束,通过设置中的开发者选项触发长时间抓取,甚至你可以通过框架中提供的 Perfetto Trigger API 来动态开启数据抓取,基本上涵盖了我们在项目上能遇到的所有的情境。

在数据分析层面,Perfetto 提供了类似 Systrace 操作的数据可视化分析网页,但底层实现机制完全不同,最大的好处是可以支持超大文件的渲染,这是 Systrace 做不到的(超过 300M 以上时可能会崩溃、可能会超卡)。在这个可视化网页上,可以看到各种二次处理的数据、可以执行 SQL 查询命令、甚至还可以看到 logcat 的内容。Perfetto Trace 文件可以转换成基于 SQLite 的数据库文件,既可以现场敲 SQL 也可以把已经写好的 SQL 形成执行文件。甚至你可以把他导入到 Jupyter 等数据科学工具栈,将你的分析思路分享给其他伙伴。

比如你想要计算 SurfaceFlinger 线程消耗 CPU 的总量,或者运行在大核中的线程都有哪一些等等,可以与领域专家合作,把他们的经验转成 SQL 指令。如果这个还不满足你的需求, Perfetto 也提供了 Python API,将数据导出成 DataFrame 格式近乎可以实现任意你想要的数据分析效果。

这一套下来供开发者可挖掘的点就非常多了,从笔者团队的实践来看,他几乎可以覆盖从功能开发、功能测试、CI/CD 以及线上监控、专家系统等方方面面。本星球的后续系列文章中,也会重点介绍 Perfetto 的强大功能与基于它开发的专家系统,可以帮助你「一键解答」性能瓶颈。

实践中的应用情况

性能分析首要用到的工具就是 Perfetto,使用 Systrace 的场景是越来越少了。所以,你首要掌握的工具应该是 Perfetto,学习它的用法以及它提供的指标。

不过 Perfetto 也有一些边界,首先它虽然提供了较高的灵活性但本质上还是静态数据收集器,不是动态跟踪工具,跟 eBPF 还是有本质上的差异。其次运行时成本比较高,因为涉及到在手机中实现 Ftrace 数据到 Perfetto 数据的转换。最后他不提供文本分析方式,只能通过网页可视化或者操作 SQLite 来进行额外的分析了。综合来看 Perfetto 是功能强大,几乎涵盖了可观测性技术的方方面面,但是使用门槛也比较高。值得挖掘与学习的知识点比较多,我们后续的文章中也会重点安排此部分的内容。

2.3 Android Studio Profiler 工具

Android 的应用开发集成环境(官方推荐)是 Android Studio (之前是Eclipse,不过已经淘汰了) ,它自然而然也需要把开发和性能调优集成一起。非常幸运的是,随着 Android Studio 的迭代、演进,到目前,Android Studio 有了自己的性能分析工具 Android Profiler,它是一个集合体,集成了多种性能分析工具于一体,让开发者可以在 Android Studio 做开发应用,也不用再下载其它工具就能让能做性能调优工作。

目前 Android Studio Profiler 已经集成了 4 类性能分析工具: CPU、Memory、Network、Battery,其中 CPU 相关性能分析工具为 CPU Profiler,也是本章的主角,它把 CPU 相关的性能分析工具都集成在了一起,开发者可以根据自己需求来选择使用哪一个。可能很多人都知道,谷歌已经开发了一些独立的 CPU 性能分析工具,如 Perfetto、Simpleperf、Java Method Trace 等,现在又出来一个 CPU Profiler,显然不可能去重复造轮子,CPU Profiler 目前做法就是:从这些已知的工具中获取数据,然后把数据解析成自己想要的样式,通过统一的界面展示出来。

提供的亮点功能

CPU Profiler 集成了性能分析工具:Perfetto、Simpleperf、Java Method Trace,它自然而然具备了这些工具的全部或部分功能,如下:

  1. System Trace Recording,它是用 Perfetto 抓取的信息,可用于分析进程函数耗时、调度、渲染等情况,但是它一个精简版,只能显示进程强相关的信息且会过滤掉耗时短的事件,建议将 Trace 导出文件后在 https://ui.perfetto.dev/ 上进行分析。
  2. Java Method Trace Recording,它是从虚拟机获取函数调用栈信息,用于分析 Java 函数调用和耗时情况。
  3. C/C++ Function Trace,它是用 Simpleperf 抓取的信息,Simpleperf 是从 CPU 的性能监控单元 PMU 硬件组件获取数据。 C/C++ Method Trace 只具备 Simpleperf 部分功能,用于分析 C/C++ 函数调用和耗时情况。

CPU Profiler

实践中的应用情况

应用的性能问题主要分为两类:响应慢、不流畅。

  • 响应慢问题常有:应用启动慢、页面跳转慢、列表加载慢、按钮响应慢等
  • 不流畅问题常有:列表滑动不流畅、页面滑动不跟手、动画卡顿等

CPU Profiler 在这些场景中要如何使用呢?基本的思路是:首先就要抓 System Trace,先用System Trace 分析、定位问题,如果不能定位到问题,再借助 Java Method Trace 或 C/C++ Function Trace 进一步分析定位。

以一个性能极差的应用为例,在系统的关键位置插了 Systrace TracePoint,假设对代码不熟悉,那要怎么找到性能瓶颈呢?我们先把应用跑起来,通过 CPU Profiler 录制一个 System Trace (后面文章会介绍工具的使用方法)如下:

通过上面 Trace 可以知道是在 egl_core 线程中的 onDrawFrame 操作耗时,如果发现不了问题,建议导出到 https://ui.perfetto.dev/ 进一步分析,可以查找源代码看看 onDrawFrame 是什么东西, 我们通过查找发现 onDrawFrame 是 Java 函数 onDrawFrame 的耗时,要分析 Java 函数耗时情况,我们要录制一个 Java Method Trace,如下:

通过上面 Trace 很容易发现是一个叫做 Utils.onDraw 的 native 函数耗时,因为涉及到C/C++ 代码,所以要再录制一个 C/C++ Function Trace 进一步分析,如下:

可以发现在 native 的 Java_com_gl_shader_Utils_onDraw 中代码执行了 sleep,它就是导致了性能低下的罪魁祸首!

AS 中的 CPU Profiler 最大优势是集成了各种子工具,在一个地方就能操作一切,对应用开发者来说是非常方便的,不过对系统开发者来说可能没那么幸运。

2.4 综合对比

工具名称应用场景数据类型获取方法分析方式
SystraceAndroid 系统与应用性能分析Trace 类型无条件抓取 持续落盘可视化分析
PerfettoAndroid 系统与应用性能分析Metric 类型 Trace 类型无条件抓取 持续落盘可视化分析 数据库分析
AS ProfilerAndroid 系统与应用性能分析Trace 类型无条件抓取 持续落盘可视化分析
SimplePerfJava/C++ 函数执行耗时 分析 PMU 计数器Trace 类性无条件抓取 持续落盘可视化分析 文本分析
Snapdragon Profiler Tools & Resources主要是高通 GPU 性能分析器Trace 类型 Metric 类型无条件抓取 持续落盘可视化分析
Mali Graphics DebuggerARM GPU 分析器(MTK、麒麟芯片)Trace 类型 Metric 类型无条件抓取 持续落盘可视化分析
Android Log/dumpsys综合分析Log 类型有条件抓取 持续抓取但不落盘文本分析
AGI(Android GPU Inspector)Android GPU 分析器Trace 类型 Metric 类型无条件抓取 持续落盘可视化分析
eBPFLinux 内核行为动态跟踪Metric 类型动态跟踪 有条件抓取 持续抓取但不落盘文本分析
FTraceLinux 内核埋点Log 类型静态代码 有条件抓取 持续抓取但不落盘文本分析

3 关于「器、术、道」

技术上的变革、改进更多是体现在「器」层面,Linux 社区以及谷歌所开发的工具发展方向朝着提高工具的集成化使得在一个地方可以方便查到所需的信息、或者是朝着获取更多信息的方向发展。总之,器层面他们的发展轨迹是可寻的,可总结出发展规律。 我们需要在工具快速迭代的时候准确的认识到他们能力以及应用场景,其目的是提高解决问题的效率,而不是把时间花在学习新工具上。

「术」层面依赖具体的业务知识,知道一帧是如何被渲染的、CPU 是如何选择进程调度的、IO 是如何被下发的等等。只有了解了业务知识才能正确的选择工具并正确的解读工具所提供的信息。随着经验的丰富,有时候你都不需要看到工具提供的详细信息,也可以查到蛛丝马迹,这就是当你业务知识丰富到一定程度,大脑里形成了复杂的关联性信息之后凌驾于工具之上的一种能力。

「道」层面思考的是要解决什么问题,问题的本质是什么?做到什么程度以及需要投入什么样的成本达成什么样的效果。为了解决一个问题,什么样的路径的「投入产出比」是最高的?整体打法是什么样?为了完成一件事,你首先要做什么其次是做什么,前后依赖关系的逻辑又是什么?

后续的文章中,会依照「器、术、道」方式讲解一个技术、一个功能,我们不止想让你学习到一个知识点,更想激发你举一反三的能力。遇到类似的工具或者类似的问题、更进一步是完全不同的系统,都能够从容应对。牢牢抓住本质,通过评估「投入产出比」选择合适的工具或信息,高效解决问题。

4 关于「The Performance 知识星球」

为了更好地交流与输出高质量文章,我们创建了名为 「The Performance」的知识星球,主理人是三个国内一线手机厂商性能优化方面的一线开发者,有多年性能相关领域的工作经验,提供Android 性能相关的一站式知识服务,涵盖了基础、方法论、工具使用和最宝贵的案例分析。

目前星球的内容规划如下(两个 ## 之间的是标签,相关的话题都会打上对应的标签,方便大家点击感兴趣的标签查看对应的知识)

  • #The Performance# — 可以提早阅读「Android 性能优化 - 系统性课程」的电子书,每周会放出已经写好的章节。「Android 性能优化 - 系统性课程」是我们规划的一本讲 Android 性能优化的电子书,目前开发者社区有相当多高质量的性能优化理论知识和实践文章和开源库,但是目前市面上缺乏一个完整的、系统性的、包含了性能优化原理、工具、实践等内容、面向初级开发中和中级开发者、面向 App 开发者和系统开发者,且持续更新的 Android 性能优化工具书。书的大纲 (暂定) 我们已经基本上列好了,预计会花费一年左右的时间来完成,在星球中会放出写好的章节,让大家提前看到。
    • Part 1: → 性能工程
    • Part 2: → 以性能角度分析 Android 交互与核心系统
    • Part 3: → 以性能角度分析 Linux 内核核心子系统设计与实现
    • Part 4: → 问题场景分析思路
    • Part 5: → 分析与调试工具
    • Part 6: → 质量守护 - 性能监控方法与工具
  • #性能工具# — 分享 Android 开发中使用到的性能分析工具以及其使用方法,同时也提供 1V1 的 Systrace、Perfetto 等性能工具的视频指导。性能工具的使用,最好还是以视频的方式展示会直观很多,文章是静态的,很多地方比较难讲清楚,1V1 的视频会议指导也算是一个学习的方法
  • #案例分析# — 典型案例分析思路总结、球友提供的案例分析与讨论。案例分析是学习的一个很重要的途径,阅读大量的实际性能案例对以后自己分析和解决性能问题是非常有帮助的,同时也欢迎大家提供案例和解决方法,怕泄露信息的话,我们会对关键信息进行打码
  • #经典解读# — 经典方案、课程重读,例如优秀的三方库解析、Android 开发高手课重读等。比如可以对方案进行深度的剖析,横向对比等;对 Android 开发高手课进行重读和查漏补缺
  • #知识分享# — 优秀文章、博客、工具分享。业界有很大大牛的博客、经过实际业务考验的开源方案、各种性能工具等,我们会寻找这些优秀的内容,分享给大家
  • #知识沉淀# — 微信群聊精华、微信问答、博客留言解答等
  • #性能面试# — Android 性能相关的面试题搜集和解答,也算是刚需了吧
  • #编程语言# — 编程语言相关的使用技巧分享
  • #效能提升# — 效能提升分享,包括开发者开发效能、工作效能提升方法、工程效率、工具推荐等,磨刀不误砍柴工嘛
  • #行业动态# — 性能相关新技术第一时间解读报告,包括但不限于下面的内容
    • 行业峰会、学术峰会新思路解读报告
    • 论文、行业、书籍介绍、视频
    • Android 大版本性能相关介绍
    • Android 新硬件性能相关内容介绍
    • Android 性能相关开源项目解读
  • #大咖分享# — 每月定期邀请行业大咖进行经验分享、案例分析
  • #工作内推# — 各大厂商内推工作机会介绍

TeamWork - 付费知识星球

注意: iOS 手机用户不要直接在星球里面付款,在微信界面长按图片扫描二维码加入即可,否则苹果会收取高昂的手续

5 附录

关于我 && 博客

下面是个人的介绍和相关的链接,期望与同行的各位多多交流,三人行,则必有我师!

  1. 博主个人介绍 :里面有个人的微信和微信群链接。
  2. 本博客内容导航 :个人博客内容的一个导航。
  3. 个人整理和搜集的优秀博客文章 - Android 性能优化必知必会 :欢迎大家自荐和推荐 (微信私聊即可)
  4. Android性能优化知识星球 : 欢迎加入,多谢支持~

一个人可以走的更快 , 一群人可以走的更远

微信扫一扫

Android 中的“后台无效动画“行为分析

作者 Gracker
2019年10月24日 08:27

当一个 Android App 退到后台之后,只要他没有被杀死,那么他做什么事情大家都不要奇怪,因为这就是 Android。但是当用户知道一个你一个 App 退到后台之后还在持续做无效的动画,而这个动画完全是无意义的,而且用户还不知道他在做动画,消耗用户那可怜的电量的时候,轻则被多任务杀掉,禁止后台运行,重则直接卸载。

一般的开发者很难发现这个问题,但是如果你经常使用 Systrace ,多开几十个应用然后退回到桌面,左右滑动抓取 Systrace ,就可以很容易发现,总有那么几个后台的应用,还在频繁地做无效的动画。

这里说的后台做动画,指的是由于某种原因,应用在退到后台之后,用户看不到任何这个 App 界面的时候,他仍然在后台不断地更新,耗费 CPU。引起这个问题的原因可能有好多个,毕竟 往 Choreographer 扔 CALLBACK_ANIMATION 的地方太多了,而且每个应用可能都不一样,但最终还是需要各个应用去做修复

下面我们就以两个实例,从技术的角度来看一下事件发生时候的情况和原因,希望看到这篇文章的开发者,检查一下自己的应用是否有这个问题,有则改之,无则恭喜

实例 - 网易新闻

我们在使用网易新闻后,将网易新闻退到后台,然后左右滑动桌面,抓 Systrace 来看:

网易新闻到后台之后还在持续做 Animation 的回调(红框内),每一帧都还是在 doFrame 操作

放大每一个 doFrame 来看,Choreographer 中的 input 和 traversal 都没有触发,只有 animation 的回调一直在执行

我们把这份 Trace 上的 cpu 部分全选,然后下面按照 Wall Duration 排序,可以发现网易新闻后台动画执行时间最长。应用已经在后台且不可见的时候,还在这么频繁地工作,占用 CPU 资源,消耗电量,实在是不应该

抓对应的 MethodTrace 来看,就是在做动画,没有进行关闭 ,动画依旧在每一帧进行 onAnimationUpdate 的回调 ,可以看到这里是因为使用了 Airbnb 的 Lottie 库导致的,动画没有关闭,所以还是一直在做触发

实例 - QQ音乐

启动 QQ 音乐,然后回到桌面, 左右滑动桌面并抓取 Systrace 和 MethodTrace ,可以看到跟上面的网易新闻的表现一致

抓取了 QQ 音乐的后台动画时候的 MethodTrace 发现,也是由于退到后台之后,没有暂停动画导致的,也是 Airbnb 的 Lottie 的锅, 而且 QQ 音乐有三个动画没有停止,比网易新闻还要严重一些

放大后可以看到

当然也不是每一个都是 Airbnb 的 Lottie 动画库引起的,比如下面这个,就是普通的动画没有结束

根本原因

根本原因是应用在不可见之后,没有将动画暂停,导致应用切换到后台之后,依然在刷新动画的回调,但此时由于是不可见的,不会触发 Input Callback 和 draw Callback ,所以也不会有任何的绘制操作,也就是说这个 Animation 的刷新完全是没有意义的(当然也有可能是业务需求?)

上面两个例子里面,网易新闻和 QQ 音乐都是因为使用了 Lottie 来实现动画,但是没有正确的关闭导致的。

开发建议

Lottie 库的 issue 列表里面有人提到了这个情况:

提出问题:

I recently did some benchmarking on an app which uses lottie to do some animations (autoplay and looping). I noticed that there is quite some CPU usage when the app is in the background and tried to investigate.

It seems to me looping animations do not pause/stop when the containing LottieAnimationView is off screen, and/or the Activity is paused.

I believe this is due to the cleanup code being only in onDetachedFromWindow() which is not necessarily being called once the Activity goes into a paused state and most definitely not, when the view is simply not visible (GONE, INVISIBLE ) anymore.

解决方法:

Overriding LottieAnimationView and doing the following solves the visibility issue for me and Lottie is paused when not visible.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Override
protected void onVisibilityChanged(@NonNull View changedView, int visibility) {
super.onVisibilityChanged(changedView, visibility);
if (visibility == VISIBLE && wasAnimatingWhenVisibilityChanged) {
resumeAnimation();
} else {
if (isAnimating()) {
wasAnimatingWhenVisibilityChanged = true;
pauseAnimation();
} else {
wasAnimatingWhenVisibilityChanged = false;
}
}
}

总之就是 : 当 App 不可见的时候,停止所有的动画:pauseAnimation!!!

本文其他地址

由于博客留言交流不方便,点赞或者交流,可以移步本文的知乎或者掘金页面
知乎 - Android 中的“后台无效动画“行为分析
掘金 - Android 中的“后台无效动画“行为分析

关于我 && 博客

下面是个人的介绍和相关的链接,期望与同行的各位多多交流,三人行,则必有我师!

  1. 博主个人介绍 :里面有个人的微信和微信群链接。
  2. 本博客内容导航 :个人博客内容的一个导航。
  3. 个人整理和搜集的优秀博客文章 - Android 性能优化必知必会 :欢迎大家自荐和推荐 (微信私聊即可)
  4. Android性能优化知识星球 : 欢迎加入,多谢支持~

一个人可以走的更快 , 一群人可以走的更远

微信扫一扫

Android 基于 Choreographer 的渲染机制详解

作者 Gracker
2019年10月22日 18:55

本文介绍了 App 开发者不经常接触到但是在 Android Framework 渲染链路中非常重要的一个类 Choreographer。包括 Choreographer 的引入背景、Choreographer 的简介、部分源码解析、Choreographer 与 MessageQueue、Choreographer 和 APM,以及手机厂商基于 Choreographer 的一些优化思路

Choreographer 的引入,主要是配合 Vsync ,给上层 App 的渲染提供一个稳定的 Message 处理的时机,也就是 Vsync 到来的时候 ,系统通过对 Vsync 信号周期的调整,来控制每一帧绘制操作的时机. 目前大部分手机都是 60Hz 的刷新率,也就是 16.6ms 刷新一次,系统为了配合屏幕的刷新频率,将 Vsync 的周期也设置为 16.6 ms,每个 16.6 ms , Vsync 信号唤醒 Choreographer 来做 App 的绘制操作 ,这就是引入 Choreographer 的主要作用. 了解 Choreographer 还可以帮助 App 开发者知道程序每一帧运行的基本原理,也可以加深对 Message、Handler、Looper、MessageQueue、Measure、Layout、Draw 的理解

本文是 Systrace 系列文章的第八篇,主要是对 Systrace 中的 Choreographer 进行简单介绍

本系列的目的是通过 Systrace 这个工具,从另外一个角度来看待 Android 系统整体的运行,同时也从另外一个角度来对 Framework 进行学习。也许你看了很多讲 Framework 的文章,但是总是记不住代码,或者不清楚其运行的流程,也许从 Systrace 这个图形化的角度,你可以理解的更深入一些。

系列文章目录

  1. Systrace 简介
  2. Systrace 基础知识 - Systrace 预备知识
  3. Systrace 基础知识 - Why 60 fps ?
  4. Systrace 基础知识 - SystemServer 解读
  5. Systrace 基础知识 - SurfaceFlinger 解读
  6. Systrace 基础知识 - Input 解读
  7. Systrace 基础知识 - Vsync 解读
  8. Systrace 基础知识 - Vsync-App :基于 Choreographer 的渲染机制详解
  9. Systrace 基础知识 - MainThread 和 RenderThread 解读
  10. Systrace 基础知识 - Triple Buffer 解读
  11. Systrace 基础知识 - CPU Info 解读
  12. Systrace 流畅性实战 1 :了解卡顿原理
  13. Systrace 流畅性实战 2 :案例分析: MIUI 桌面滑动卡顿分析
  14. Systrace 流畅性实战 3 :卡顿分析过程中的一些疑问
  15. Systrace 响应速度实战 1 :了解响应速度原理
  16. Systrace 响应速度实战 2 :响应速度实战分析-以启动速度为例
  17. Systrace 响应速度实战 3 :响应速度延伸知识
  18. Systrace 线程 CPU 运行状态分析技巧 - Runnable 篇
  19. Systrace 线程 CPU 运行状态分析技巧 - Running 篇
  20. Systrace 线程 CPU 运行状态分析技巧 - Sleep 和 Uninterruptible Sleep 篇

主线程运行机制的本质

在讲 Choreographer 之前,我们先理一下 Android 主线程运行的本质,其实就是 Message 的处理过程,我们的各种操作,包括每一帧的渲染操作 ,都是通过 Message 的形式发给主线程的 MessageQueue ,MessageQueue 处理完消息继续等下一个消息,如下图所示

MethodTrace 图示

Systrace 图示

演进

引入 Vsync 之前的 Android 版本,渲染一帧相关的 Message ,中间是没有间隔的,上一帧绘制完,下一帧的 Message 紧接着就开始被处理。这样的问题就是,帧率不稳定,可能高也可能低,不稳定,如下图

MethodTrace 图示

Systrace 图示

可以看到这时候的瓶颈是在 dequeueBuffer, 因为屏幕是有刷新周期的, FB 消耗 Front Buffer 的速度是一定的, 所以 SF 消耗 App Buffer 的速度也是一定的, 所以 App 会卡在 dequeueBuffer 这里,这就会导致 App Buffer 获取不稳定, 很容易就会出现卡顿掉帧的情况.

对于用户来说,稳定的帧率才是好的体验,比如你玩王者荣耀,相比 fps 在 60 和 40 之间频繁变化,用户感觉更好的是稳定在 50 fps 的情况.

所以 Android 的演进中,引入了 Vsync + TripleBuffer + Choreographer 的机制,其主要目的就是提供一个稳定的帧率输出机制,让软件层和硬件层可以以共同的频率一起工作。

引入 Choreographer

Choreographer 的引入,主要是配合 Vsync ,给上层 App 的渲染提供一个稳定的 Message 处理的时机,也就是 Vsync 到来的时候 ,系统通过对 Vsync 信号周期的调整,来控制每一帧绘制操作的时机. 至于为什么 Vsync 周期选择是 16.6ms (60 fps) ,是因为目前大部分手机的屏幕都是 60Hz 的刷新率,也就是 16.6ms 刷新一次,系统为了配合屏幕的刷新频率,将 Vsync 的周期也设置为 16.6 ms,每隔 16.6 ms ,Vsync 信号到来唤醒 Choreographer 来做 App 的绘制操作 ,如果每个 Vsync 周期应用都能渲染完成,那么应用的 fps 就是 60 ,给用户的感觉就是非常流畅,这就是引入 Choreographer 的主要作用

当然目前使用 90Hz 刷新率屏幕的手机越来越多,Vsync 周期从 16.6ms 到了 11.1ms,上图中的操作要在更短的时间内完成,对性能的要求也越来越高,具体可以看新的流畅体验,90Hz 漫谈 这篇文章

Choreographer 简介

Choreographer 扮演 Android 渲染链路中承上启下的角色

  1. 承上:负责接收和处理 App 的各种更新消息和回调,等到 Vsync 到来的时候统一处理。比如集中处理 Input(主要是 Input 事件的处理) 、Animation(动画相关)、Traversal(包括 measure、layout、draw 等操作) ,判断卡顿掉帧情况,记录 CallBack 耗时等
  2. 启下:负责请求和接收 Vsync 信号。接收 Vsync 事件回调(通过 FrameDisplayEventReceiver.onVsync );请求 Vsync(FrameDisplayEventReceiver.scheduleVsync)

从上面可以看出来, Choreographer 担任的是一个工具人的角色,他之所以重要,是因为通过 Choreographer + SurfaceFlinger + Vsync + TripleBuffer 这一套从上到下的机制,保证了 Android App 可以以一个稳定的帧率运行(20fps、90fps 或者 60fps),减少帧率波动带来的不适感。

了解 Choreographer 还可以帮助 App 开发者知道程序每一帧运行的基本原理,也可以加深对 Message、Handler、Looper、MessageQueue、Input、Animation、Measure、Layout、Draw 的理解 , 很多 APM 工具也用到了 Choreographer( 利用 FrameCallback + FrameInfo ) + MessageQueue ( 利用 IdleHandler ) + Looper ( 设置自定义 MessageLogging) 这些组合拳,深入了解了这些之后,再去做优化,脑子里的思路会更清晰。

另外虽然画图是一个比较好的解释流程的好路子,但是我个人不是很喜欢画图,因为平时 Systrace 和 MethodTrace 用的比较多,Systrace 是按从左到右展示整个系统的运行情况的一个工具(包括 cpu、SurfaceFlinger、SystemServer、App 等关键进程),使用 SystraceMethodTrace 也可以很方便地展示关键流程。当你对系统代码比较熟悉的时候,看 Systrace 就可以和手机运行的实际情况对应起来。所以下面的文章除了一些网图之外,其他的我会多以 Systrace 来展示。

从 Systrace 的角度来看 Choreogrepher 的工作流程

下图以滑动桌面为例子,我们先看一下从左到右滑动桌面的一个完整的预览图(App 进程),可以看到 Systrace 中从左到右,每一个绿色的帧都表示一帧,表示最终我们可以手机上看到的画面

  1. 图中每一个灰色的条和白色的条宽度是一个 Vsync 的时间,也就是 16.6ms
  2. 每一帧处理的流程:接收到 Vsync 信号回调-> UI Thread –> RenderThread –> SurfaceFlinger(图中未显示)
  3. UI Thread 和 RenderThread 就可以完成 App 一帧的渲染,渲染完的 Buffer 抛给 SurfaceFlinger 去合成,然后我们就可以在屏幕上看到这一帧了
  4. 可以看到桌面滑动的每一帧耗时都很短(Ui Thread 耗时 + RenderThread 耗时),但是由于 Vsync 的存在,每一帧都会等到 Vsync 才会去做处理

有了上面这个整体的概念,我们将 UI Thread 的每一帧放大来看,看看 Choreogrepher 的位置以及 Choreogrepher 是怎么组织每一帧的

Choreographer 的工作流程

  1. Choreographer 初始化
  2. 初始化 FrameHandler ,绑定 Looper
  3. 初始化 FrameDisplayEventReceiver ,与 SurfaceFlinger 建立通信用于接收和请求 Vsync
  4. 初始化 CallBackQueues
  5. SurfaceFlinger 的 appEventThread 唤醒发送 Vsync ,Choreographer 回调 FrameDisplayEventReceiver.onVsync , 进入 Choreographer 的主处理函数 doFrame
  6. Choreographer.doFrame 计算掉帧逻辑
  7. Choreographer.doFrame 处理 Choreographer 的第一个 callback : input
  8. Choreographer.doFrame 处理 Choreographer 的第二个 callback : animation
  9. Choreographer.doFrame 处理 Choreographer 的第三个 callback : insets animation
  10. Choreographer.doFrame 处理 Choreographer 的第四个 callback : traversal
  11. traversal-draw 中 UIThread 与 RenderThread 同步数据
  12. Choreographer.doFrame 处理 Choreographer 的第五个 callback : commit ?
  13. RenderThread 处理绘制命令,将处理好的绘制命令发给 GPU 处理
  14. 调用 swapBuffer 提交给 SurfaceFlinger 进行合成(此时 Buffer 并没有真正完成,需要等 CPU 完成后 SurfaceFlinger 才能真正使用,新版本的 Systrace 中有 gpu 的 fence 来标识这个时间)

第一步初始化完成后,后续就会在步骤 2-9 之间循环

同时也附上这一帧所对应的 MethodTrace(这里预览一下即可,下面会有详细的大图)

下面我们就从源码的角度,来看一下具体的实现

源码解析

下面从源码的角度来简单看一下,源码只摘抄了部分重要的逻辑,其他的逻辑则被剔除,另外 Native 部分与 SurfaceFlinger 交互的部分也没有列入,不是本文的重点,有兴趣的可以自己去跟一下。

Choreographer 的初始化

Choreographer 的单例初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Thread local storage for the choreographer.
private static final ThreadLocal<Choreographer> sThreadInstance =
new ThreadLocal<Choreographer>() {
@Override
protected Choreographer initialValue() {
// 获取当前线程的 Looper
Looper looper = Looper.myLooper();
......
// 构造 Choreographer 对象
Choreographer choreographer = new Choreographer(looper, VSYNC_SOURCE_APP);
if (looper == Looper.getMainLooper()) {
mMainInstance = choreographer;
}
return choreographer;
}
};

Choreographer 的构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private Choreographer(Looper looper, int vsyncSource) {
mLooper = looper;
// 1. 初始化 FrameHandler
mHandler = new FrameHandler(looper);
// 2. 初始化 DisplayEventReceiver
mDisplayEventReceiver = USE_VSYNC
? new FrameDisplayEventReceiver(looper, vsyncSource)
: null;
mLastFrameTimeNanos = Long.MIN_VALUE;
mFrameIntervalNanos = (long)(1000000000 / getRefreshRate());
//3. 初始化 CallbacksQueues
mCallbackQueues = new CallbackQueue[CALLBACK_LAST + 1];
for (int i = 0; i <= CALLBACK_LAST; i++) {
mCallbackQueues[i] = new CallbackQueue();
}
......
}

FrameHandler

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private final class FrameHandler extends Handler {
......
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_DO_FRAME://开始渲染下一帧的操作
doFrame(System.nanoTime(), 0);
break;
case MSG_DO_SCHEDULE_VSYNC://请求 Vsync
doScheduleVsync();
break;
case MSG_DO_SCHEDULE_CALLBACK://处理 Callback
doScheduleCallback(msg.arg1);
break;
}
}
}

Choreographer 初始化链

在 Activity 启动过程,执行完 onResume 后,会调用 Activity.makeVisible(),然后再调用到 addView(), 层层调用会进入如下方法

1
2
3
4
5
6
7
8
9
ActivityThread.handleResumeActivity(IBinder, boolean, boolean, String) (android.app) 
-->WindowManagerImpl.addView(View, LayoutParams) (android.view) 
-->WindowManagerGlobal.addView(View, LayoutParams, Display, Window) (android.view) 
-->ViewRootImpl.ViewRootImpl(Context, Display) (android.view) 
public ViewRootImpl(Context context, Display display) {
......
mChoreographer = Choreographer.getInstance();
......
}

FrameDisplayEventReceiver 简介

Vsync 的注册、申请、接收都是通过 FrameDisplayEventReceiver 这个类,所以可以先简单介绍一下。 FrameDisplayEventReceiver 继承 DisplayEventReceiver , 有三个比较重要的方法

  1. onVsync – Vsync 信号回调
  2. run – 执行 doFrame
  3. scheduleVsync – 请求 Vsync 信号
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
private final class FrameDisplayEventReceiver extends DisplayEventReceiver implements Runnable {
......
@Override
public void onVsync(long timestampNanos, long physicalDisplayId, int frame) {
......
mTimestampNanos = timestampNanos;
mFrame = frame;
Message msg = Message.obtain(mHandler, this);
msg.setAsynchronous(true);
mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);
}
@Override
public void run() {
mHavePendingVsync = false;
doFrame(mTimestampNanos, mFrame);
}

public void scheduleVsync() {
......
nativeScheduleVsync(mReceiverPtr);
......
}
}

Choreographer 中 Vsync 的注册

从下面的函数调用栈可以看到,Choreographer 的内部类 FrameDisplayEventReceiver.onVsync 负责接收 Vsync 回调,通知 UIThread 进行数据处理。

那么 FrameDisplayEventReceiver 是通过什么方式在 Vsync 信号到来的时候回调 onVsync 呢?答案是 FrameDisplayEventReceiver 的初始化的时候,最终通过监听文件句柄的形式,其对应的初始化流程如下

android/view/Choreographer.java

1
2
3
4
5
6
7
private Choreographer(Looper looper, int vsyncSource) {
mLooper = looper;
mDisplayEventReceiver = USE_VSYNC
? new FrameDisplayEventReceiver(looper, vsyncSource)
: null;
......
}

android/view/Choreographer.java

1
2
3
public FrameDisplayEventReceiver(Looper looper, int vsyncSource) {
super(looper, vsyncSource);
}

android/view/DisplayEventReceiver.java

1
2
3
4
5
6
public DisplayEventReceiver(Looper looper, int vsyncSource) {
......
mMessageQueue = looper.getQueue();
mReceiverPtr = nativeInit(new WeakReference<DisplayEventReceiver>(this), mMessageQueue,
vsyncSource);
}

nativeInit 后续的代码可以自己跟一下,可以对照这篇文章和源码,由于篇幅比较多,这里就不细写了(https://www.jianshu.com/p/304f56f5d486) , 后续梳理好这一块的逻辑后,会在另外的文章更新。

简单来说,FrameDisplayEventReceiver 的初始化过程中,通过 BitTube(本质是一个 socket pair),来传递和请求 Vsync 事件,当 SurfaceFlinger 收到 Vsync 事件之后,通过 appEventThread 将这个事件通过 BitTube 传给 DisplayEventDispatcher ,DisplayEventDispatcher 通过 BitTube 的接收端监听到 Vsync 事件之后,回调 Choreographer.FrameDisplayEventReceiver.onVsync ,触发开始一帧的绘制,如下图

Choreographer 处理一帧的逻辑

Choreographer 处理绘制的逻辑核心在 Choreographer.doFrame 函数中,从下图可以看到,FrameDisplayEventReceiver.onVsync post 了自己,其 run 方法直接调用了 doFrame 开始一帧的逻辑处理

android/view/Choreographer.java

1
2
3
4
5
6
7
8
9
10
11
12
public void onVsync(long timestampNanos, long physicalDisplayId, int frame) {
......
mTimestampNanos = timestampNanos;
mFrame = frame;
Message msg = Message.obtain(mHandler, this);
msg.setAsynchronous(true);
mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);
}
public void run() {
mHavePendingVsync = false;
doFrame(mTimestampNanos, mFrame);
}

doFrame 函数主要做下面几件事

  1. 计算掉帧逻辑
  2. 记录帧绘制信息
  3. 执行 CALLBACK_INPUT、CALLBACK_ANIMATION、CALLBACK_INSETS_ANIMATION、CALLBACK_TRAVERSAL、CALLBACK_COMMIT

计算掉帧逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void doFrame(long frameTimeNanos, int frame) {
final long startNanos;
synchronized (mLock) {
......
long intendedFrameTimeNanos = frameTimeNanos;
startNanos = System.nanoTime();
final long jitterNanos = startNanos - frameTimeNanos;
if (jitterNanos >= mFrameIntervalNanos) {
final long skippedFrames = jitterNanos / mFrameIntervalNanos;
if (skippedFrames >= SKIPPED_FRAME_WARNING_LIMIT) {
Log.i(TAG, "Skipped " + skippedFrames + " frames! "
+ "The application may be doing too much work on its main thread.");
}
}
......
}
......
}

Choreographer.doFrame 的掉帧检测比较简单,从下图可以看到,Vsync 信号到来的时候会标记一个 start_time ,执行 doFrame 的时候标记一个 end_time ,这两个时间差就是 Vsync 处理时延,也就是掉帧

我们以 Systrace 的掉帧的实际情况来看掉帧的计算逻辑

这里需要注意的是,这种方法计算的掉帧,是前一帧的掉帧情况,而不是这一帧的掉帧情况,这个计算方法是有缺陷的,会导致有的掉帧没有被计算到

记录帧绘制信息

Choreographer 中 FrameInfo 来负责记录帧的绘制信息,doFrame 执行的时候,会把每一个关键节点的绘制时间记录下来,我们使用 dumpsys gfxinfo 就可以看到。当然 Choreographer 只是记录了一部分,剩余的部分在 hwui 那边来记录。

从 FrameInfo 这些标志就可以看出记录的内容,后面我们看 dumpsys gfxinfo 的时候数据就是按照这个来排列的

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
// Various flags set to provide extra metadata about the current frame
private static final int FLAGS = 0;

// Is this the first-draw following a window layout?
public static final long FLAG_WINDOW_LAYOUT_CHANGED = 1;

// A renderer associated with just a Surface, not with a ViewRootImpl instance.
public static final long FLAG_SURFACE_CANVAS = 1 << 2;

@LongDef(flag = true, value = {
FLAG_WINDOW_LAYOUT_CHANGED, FLAG_SURFACE_CANVAS })
@Retention(RetentionPolicy.SOURCE)
public @interface FrameInfoFlags {}

// The intended vsync time, unadjusted by jitter
private static final int INTENDED_VSYNC = 1;

// Jitter-adjusted vsync time, this is what was used as input into the
// animation & drawing system
private static final int VSYNC = 2;

// The time of the oldest input event
private static final int OLDEST_INPUT_EVENT = 3;

// The time of the newest input event
private static final int NEWEST_INPUT_EVENT = 4;

// When input event handling started
private static final int HANDLE_INPUT_START = 5;

// When animation evaluations started
private static final int ANIMATION_START = 6;

// When ViewRootImpl#performTraversals() started
private static final int PERFORM_TRAVERSALS_START = 7;

// When View:draw() started
private static final int DRAW_START = 8;

doFrame 函数记录从 Vsync time 到 markPerformTraversalsStart 的时间

1
2
3
4
5
6
7
8
9
10
11
12
13
void doFrame(long frameTimeNanos, int frame) {
......
mFrameInfo.setVsync(intendedFrameTimeNanos, frameTimeNanos);
// 处理 CALLBACK_INPUT Callbacks
mFrameInfo.markInputHandlingStart();
// 处理 CALLBACK_ANIMATION Callbacks
mFrameInfo.markAnimationsStart();
// 处理 CALLBACK_INSETS_ANIMATION Callbacks
// 处理 CALLBACK_TRAVERSAL Callbacks
mFrameInfo.markPerformTraversalsStart();
// 处理 CALLBACK_COMMIT Callbacks
......
}

执行 Callbacks

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void doFrame(long frameTimeNanos, int frame) {
......
// 处理 CALLBACK_INPUT Callbacks
doCallbacks(Choreographer.CALLBACK_INPUT, frameTimeNanos);
// 处理 CALLBACK_ANIMATION Callbacks
doCallbacks(Choreographer.CALLBACK_ANIMATION, frameTimeNanos);
// 处理 CALLBACK_INSETS_ANIMATION Callbacks
doCallbacks(Choreographer.CALLBACK_INSETS_ANIMATION, frameTimeNanos);
// 处理 CALLBACK_TRAVERSAL Callbacks
doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos);
// 处理 CALLBACK_COMMIT Callbacks
doCallbacks(Choreographer.CALLBACK_COMMIT, frameTimeNanos);
......
}

Input 回调调用栈

input callback 一般是执行 ViewRootImpl.ConsumeBatchedInputRunnable

android/view/ViewRootImpl.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
final class ConsumeBatchedInputRunnable implements Runnable {
@Override
public void run() {
doConsumeBatchedInput(mChoreographer.getFrameTimeNanos());
}
}
void doConsumeBatchedInput(long frameTimeNanos) {
if (mConsumeBatchedInputScheduled) {
mConsumeBatchedInputScheduled = false;
if (mInputEventReceiver != null) {
if (mInputEventReceiver.consumeBatchedInputEvents(frameTimeNanos)
&& frameTimeNanos != -1) {
scheduleConsumeBatchedInput();
}
}
doProcessInputEvents();
}
}

Input 时间经过处理,最终会传给 DecorView 的 dispatchTouchEvent,这就到了我们熟悉的 Input 事件分发

Animation 回调调用栈

一般我们接触的多的是调用 View.postOnAnimation 的时候,会使用到 CALLBACK_ANIMATION

1
2
3
4
5
6
7
8
9
10
11
public void postOnAnimation(Runnable action) {
final AttachInfo attachInfo = mAttachInfo;
if (attachInfo != null) {
attachInfo.mViewRootImpl.mChoreographer.postCallback(
Choreographer.CALLBACK_ANIMATION, action, null);
} else {
// Postpone the runnable until we know
// on which thread it needs to run.
getRunQueue().post(action);
}
}

那么一般是什么时候回调用到 View.postOnAnimation 呢,我截取了一张图,大家可以自己去看一下,接触最多的应该是 startScroll,Fling 这种操作

其调用栈根据其 post 的内容,下面是桌面滑动松手之后的 fling 动画。

另外我们的 Choreographer 的 FrameCallback 也是用的 CALLBACK_ANIMATION

1
2
3
4
5
6
7
8
public void postFrameCallbackDelayed(FrameCallback callback, long delayMillis) {
if (callback == null) {
throw new IllegalArgumentException("callback must not be null");
}

postCallbackDelayedInternal(CALLBACK_ANIMATION,
callback, FRAME_CALLBACK_TOKEN, delayMillis);
}

Traversal 调用栈

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
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
//为了提高优先级,先 postSyncBarrier
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
}
}

final class TraversalRunnable implements Runnable {
@Override
public void run() {
// 真正开始执行 measure、layout、draw
doTraversal();
}
}
void doTraversal() {
if (mTraversalScheduled) {
mTraversalScheduled = false;
// 这里把 SyncBarrier remove
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
// 真正开始
performTraversals();
}
}
private void performTraversals() {
// measure 操作
if (focusChangedDueToTouchMode || mWidth != host.getMeasuredWidth() || mHeight != host.getMeasuredHeight() || contentInsetsChanged || updatedConfiguration) {
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
}
// layout 操作
if (didLayout) {
performLayout(lp, mWidth, mHeight);
}
// draw 操作
if (!cancelDraw && !newSurface) {
performDraw();
}
}

doTraversal 的 TraceView 示例

下一帧的 Vsync 请求

由于动画、滑动、Fling 这些操作的存在,我们需要一个连续的、稳定的帧率输出机制。这就涉及到了 Vsync 的请求逻辑,在连续的操作,比如动画、滑动、Fling 这些情况下,每一帧的 doFrame 的时候,都会根据情况触发下一个 Vsync 的申请,这样我们就可以获得连续的 Vsync 信号。

看下面的 scheduleTraversals 调用栈(scheduleTraversals 中会触发 Vsync 请求)

我们比较熟悉的 invalidate 和 requestLayout 都会触发 Vsync 信号请求

我们下面以 Animation 为例,看看 Animation 是如何驱动下一个 Vsync ,来持续更新画面的

ObjectAnimator 动画驱动逻辑

android/animation/ObjectAnimator.java

1
2
3
public void start() {
super.start();
}

android/animation/ValueAnimator.java

1
2
3
4
5
6
7
8
9
private void start(boolean playBackwards) {
......
addAnimationCallback(0); // 动画 start 的时候添加 Animation Callback
......
}
private void addAnimationCallback(long delay) {
......
getAnimationHandler().addAnimationFrameCallback(this, delay);
}

android/animation/AnimationHandler.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public void addAnimationFrameCallback(final AnimationFrameCallback callback, long delay) {
if (mAnimationCallbacks.size() == 0) {
// post FrameCallback
getProvider().postFrameCallback(mFrameCallback);
}
......
}

// 这里的 mFrameCallback 回调 doFrame,里面 post了自己
private final Choreographer.FrameCallback mFrameCallback = new Choreographer.FrameCallback() {
@Override
public void doFrame(long frameTimeNanos) {
doAnimationFrame(getProvider().getFrameTime());
if (mAnimationCallbacks.size() > 0) {
// post 自己
getProvider().postFrameCallback(this);
}
}
};

调用 postFrameCallback 会走到 mChoreographer.postFrameCallback ,这里就会触发 Choreographer 的 Vsync 请求逻辑

android/animation/AnimationHandler.java

1
2
3
public void postFrameCallback(Choreographer.FrameCallback callback) {
mChoreographer.postFrameCallback(callback);
}

android/view/Choreographer.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private void postCallbackDelayedInternal(int callbackType,
Object action, Object token, long delayMillis) {
synchronized (mLock) {
final long now = SystemClock.uptimeMillis();
final long dueTime = now + delayMillis;
mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);

if (dueTime <= now) {
// 请求 Vsync scheduleFrameLocked ->scheduleVsyncLocked-> mDisplayEventReceiver.scheduleVsync ->nativeScheduleVsync
scheduleFrameLocked(now);
} else {
Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);
msg.arg1 = callbackType;
msg.setAsynchronous(true);
mHandler.sendMessageAtTime(msg, dueTime);
}
}
}

通过上面的 Animation.start 设置,利用了 Choreographer.FrameCallback 接口,每一帧都去请求下一个 Vsync
动画过程中一帧的 TraceView 示例

源码小结

  1. Choreographer 是线程单例的,而且必须要和一个 Looper 绑定,因为其内部有一个 Handler 需要和 Looper 绑定,一般是 App 主线程的 Looper 绑定

  2. DisplayEventReceiver 是一个 abstract class,其 JNI 的代码部分会创建一个IDisplayEventConnection 的 Vsync 监听者对象。这样,来自 AppEventThread 的 VSYNC 中断信号就可以传递给 Choreographer 对象了。当 Vsync 信号到来时,DisplayEventReceiver 的 onVsync 函数将被调用。

  3. DisplayEventReceiver 还有一个 scheduleVsync 函数。当应用需要绘制UI时,将首先申请一次 Vsync 中断,然后再在中断处理的 onVsync 函数去进行绘制。

  4. Choreographer 定义了一个 FrameCallback interface,每当 Vsync 到来时,其 doFrame 函数将被调用。这个接口对 Android Animation 的实现起了很大的帮助作用。以前都是自己控制时间,现在终于有了固定的时间中断。

  5. Choreographer 的主要功能是,当收到 Vsync 信号时,去调用使用者通过 postCallback 设置的回调函数。目前一共定义了五种类型的回调,它们分别是:

    1. CALLBACK_INPUT : 处理输入事件处理有关
    2. CALLBACK_ANIMATION : 处理 Animation 的处理有关
    3. CALLBACK_INSETS_ANIMATION : 处理 Insets Animation 的相关回调
    4. CALLBACK_TRAVERSAL : 处理和 UI 等控件绘制有关
    5. CALLBACK_COMMIT : 处理 Commit 相关回调,主要是是用于执行组件 Application/Activity/Service 的 onTrimMemory,在 ApplicationThread 的 scheduleTrimMemory 方法中向 Choreographer 插入的;另外这个 Callback 也提供了一个监测一帧耗时的时机
  6. ListView 的 Item 初始化(obtain\setup) 会在 input 里面也会在 animation 里面,这取决于

  7. CALLBACK_INPUTCALLBACK_ANIMATION 会修改 view 的属性,所以要先与 CALLBACK_TRAVERSAL 执行

APM 与 Choreographer

由于 Choreographer 的位置,许多性能监控的手段都是利用 Choreographer 来做的,除了自带的掉帧计算,Choreographer 提供的 FrameCallback 和 FrameInfo 都给 App 暴露了接口,让 App 开发者可以通过这些方法监控自身 App 的性能,其中常用的方法如下:

  1. 利用 FrameCallback 的 doFrame 回调
  2. 利用 FrameInfo 进行监控
  3. 使用 :adb shell dumpsys gfxinfo framestats
  4. 示例 :adb shell dumpsys gfxinfo com.meizu.flyme.launcher framestats
  5. 利用 SurfaceFlinger 进行监控
  6. 使用 :adb shell dumpsys SurfaceFlinger –latency
  7. 示例 :adb shell dumpsys SurfaceFlinger –latency com.meizu.flyme.launcher/com.meizu.flyme.launcher.Launcher#0
  8. 利用 SurfaceFlinger PageFlip 机制进行监控
  9. 使用 : adb service call SurfaceFlinger 1013
  10. 备注:需要系统权限
  11. Choreographer 自身的掉帧计算逻辑
  12. BlockCanary 基于 Looper 的性能监控

利用 FrameCallback 的 doFrame 回调

FrameCallback 接口

1
2
3
public interface FrameCallback {
public void doFrame(long frameTimeNanos);
}

接口使用

1
Choreographer.getInstance().postFrameCallback(youOwnFrameCallback );

接口处理

1
2
3
4
5
public void postFrameCallbackDelayed(FrameCallback callback, long delayMillis) {
......
postCallbackDelayedInternal(CALLBACK_ANIMATION,
callback, FRAME_CALLBACK_TOKEN, delayMillis);
}

TinyDancer 就是使用了这个方法来计算 FPS (https://github.com/friendlyrobotnyc/TinyDancer)

利用 FrameInfo 进行监控

adb shell dumpsys gfxinfo framestats

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Window: StatusBar
Stats since: 17990256398ns
Total frames rendered: 1562
Janky frames: 361 (23.11%)
50th percentile: 6ms
90th percentile: 23ms
95th percentile: 36ms
99th percentile: 101ms
Number Missed Vsync: 33
Number High input latency: 683
Number Slow UI thread: 273
Number Slow bitmap uploads: 8
Number Slow issue draw commands: 18
Number Frame deadline missed: 287
HISTOGRAM: 5ms=670 6ms=128 7ms=84 8ms=63 9ms=38 10ms=23 11ms=21 12ms=20 13ms=25 14ms=39 15ms=65 16ms=36 17ms=51 18ms=37 19ms=41 20ms=20 21ms=19 22ms=18 23ms=15 24ms=14 25ms=8 26ms=4 27ms=6 28ms=3 29ms=4 30ms=2 31ms=2 32ms=6 34ms=12 36ms=10 38ms=9 40ms=3 42ms=4 44ms=5 46ms=8 48ms=6 53ms=6 57ms=4 61ms=1 65ms=0 69ms=2 73ms=2 77ms=3 81ms=4 85ms=1 89ms=2 93ms=0 97ms=2 101ms=1 105ms=1 109ms=1 113ms=1 117ms=1 121ms=2 125ms=1 129ms=0 133ms=1 150ms=2 200ms=3 250ms=0 300ms=1 350ms=1 400ms=0 450ms=0 500ms=0 550ms=0 600ms=0 650ms=0

---PROFILEDATA---
Flags,IntendedVsync,Vsync,OldestInputEvent,NewestInputEvent,HandleInputStart,AnimationStart,PerformTraversalsStart,DrawStart,SyncQueued,SyncStart,IssueDrawCommandsStart,SwapBuffers,FrameCompleted,DequeueBufferDuration,QueueBufferDuration,
0,10158314881426,10158314881426,9223372036854775807,0,10158315693363,10158315760759,10158315769821,10158316032165,10158316627842,10158316838988,10158318055915,10158320387269,10158321770654,428000,773000,
0,10158332036261,10158332036261,9223372036854775807,0,10158332799196,10158332868519,10158332877269,10158333137738,10158333780654,10158333993206,10158335078467,10158337689561,10158339307061,474000,885000,
0,10158348665353,10158348665353,9223372036854775807,0,10158349710238,10158349773102,10158349780863,10158350405863,10158351135967,10158351360446,10158352300863,10158354305654,10158355814509,471000,836000,
0,10158365296729,10158365296729,9223372036854775807,0,10158365782373,10158365821019,10158365825238,10158365975290,10158366547946,10158366687217,10158367240706,10158368429248,10158369291852,269000,476000,

利用 SurfaceFlinger 进行监控

命令解释:

  1. 数据的单位是纳秒,时间是以开机时间为起始点
  2. 每一次的命令都会得到128行的帧相关的数据

数据:

  1. 第一行数据,表示刷新的时间间隔refresh_period
  2. 第1列:这一部分的数据表示应用程序绘制图像的时间点
  3. 第2列:在SF(软件)将帧提交给H/W(硬件)绘制之前的垂直同步时间,也就是每帧绘制完提交到硬件的时间戳,该列就是垂直同步的时间戳
  4. 第3列:在SF将帧提交给H/W的时间点,算是H/W接受完SF发来数据的时间点,绘制完成的时间点。

掉帧 jank 计算

每一行都可以通过下面的公式得到一个值,该值是一个标准,我们称为jankflag,如果当前行的jankflag与上一行的jankflag发生改变,那么就叫掉帧

ceil((C - A) / refresh-period)

利用 SurfaceFlinger PageFlip 机制进行监控

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Parcel data = Parcel.obtain();
Parcel reply = Parcel.obtain();
                data.writeInterfaceToken("android.ui.ISurfaceComposer");
mFlinger.transact(1013, data, reply, 0);
final int pageFlipCount = reply.readInt();

final long now = System.nanoTime();
final int frames = pageFlipCount - mLastPageFlipCount;
final long duration = now - mLastUpdateTime;
mFps = (float) (frames * 1e9 / duration);
mLastPageFlipCount = pageFlipCount;
mLastUpdateTime = now;
reply.recycle();
data.recycle();

Choreographer 自身的掉帧计算逻辑

SKIPPED_FRAME_WARNING_LIMIT 默认为30 , 由 debug.choreographer.skipwarning 这个属性控制

1
2
3
4
5
6
7
if (jitterNanos >= mFrameIntervalNanos) {
final long skippedFrames = jitterNanos / mFrameIntervalNanos;
if (skippedFrames >= SKIPPED_FRAME_WARNING_LIMIT) {
Log.i(TAG, "Skipped " + skippedFrames + " frames! "
+ "The application may be doing too much work on its main thread.");
}
}

BlockCanary

Blockcanary 做性能监控使用的是 Looper 的消息机制,通过对 MessageQueue 中每一个 Message 的前后进行记录,打到监控性能的目的

android/os/Looper.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static void loop() {
...
for (;;) {
...
// This must be in a local variable, in case a UI event sets the logger
Printer logging = me.mLogging;
if (logging != null) {
logging.println(">>>>> Dispatching to " + msg.target + " " +
msg.callback + ": " + msg.what);
}
msg.target.dispatchMessage(msg);
if (logging != null) {
logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
}
...
}
}

MessageQueue 与 Choreographer

所谓的异步消息其实就是这样的,我们可以通过 enqueueBarrier 往消息队列中插入一个 Barrier,那么队列中执行时间在这个 Barrier 以后的同步消息都会被这个 Barrier 拦截住无法执行,直到我们调用 removeBarrier 移除了这个 Barrier,而异步消息则没有影响,消息默认就是同步消息,除非我们调用了 Message 的 setAsynchronous,这个方法是隐藏的。只有在初始化 Handler 时通过参数指定往这个 Handler 发送的消息都是异步的,这样在 Handler 的 enqueueMessage 中就会调用 Message 的 setAsynchronous 设置消息是异步的,从上面 Handler.enqueueMessage 的代码中可以看到。

所谓异步消息,其实只有一个作用,就是在设置 Barrier 时仍可以不受 Barrier 的影响被正常处理,如果没有设置 Barrier,异步消息就与同步消息没有区别,可以通过 removeSyncBarrier 移除 Barrier

SyncBarrier 在 Choreographer 中使用的一个示例

scheduleTraversals 的时候 postSyncBarrier

1
2
3
4
5
6
7
8
9
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
//为了提高优先级,先 postSyncBarrier
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
}
}

doTraversal 的时候 removeSyncBarrier

1
2
3
4
5
6
7
8
9
void doTraversal() {
if (mTraversalScheduled) {
mTraversalScheduled = false;
// 这里把 SyncBarrier remove
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
// 真正开始
performTraversals();
}
}

Choreographer post Message 的时候,会把这些消息设为 Asynchronous ,这样 Choreographer 中的这些 Message 的优先级就会比较高,

1
2
3
4
Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);
msg.arg1 = callbackType;
msg.setAsynchronous(true);
mHandler.sendMessageAtTime(msg, dueTime);

厂商优化

系统厂商由于可以直接修改源码,也利用这方面的便利,做一些功能和优化,不过由于保密的问题,代码就不直接放上来了,我可以大概说一下思路,感兴趣的可以私下讨论

移动事件优化

Choreographer 本身是没有 input 消息的, 不过修改源码之后,input 消息可以直接给到 Choreographer 这里, 有了这些 Input 消息,Choreographer 就可以做一些事情,比如说提前响应,不去等 Vsync

后台动画优化

当一个 Android App 退到后台之后,只要他没有被杀死,那么他做什么事情大家都不要奇怪,因为这就是 Android。有的 App 退到后台之后还在持续调用 Choreographer 中的 Animation Callback,而这个 Callback 的执行完全是无意义的,而且用户还不知道,但是对 cpu 的占用是比较高的。

所以在 Choreographer 中会针对这种情况做优化,禁止不符合条件的 App 在后台继续无用的操作

帧绘制优化

和移动事件优化一样,由于有了 Input 事件的信息,在某些场景下我们可以通知 SurfaceFlinger 不用去等待 Vsync 直接做合成操作

应用启动优化

我们前面说,主线程的所有操作都是给予 Message 的 ,如果某个操作,非重要的 Message 被排列到了队列后面,那么对这个操作产生影响;而通过重新排列 MessageQueue,在应用启动的时候,把启动相关的重要的启动 Message 放到队列前面,来起到加快启动速度的作用

高帧率优化

90/120 fps 的手机上 , Vsync 间隔从 16.6ms 变成了 11.1ms/8.8ms ,这带来了巨大的性能和功耗挑战,如何在一帧内完成渲染的必要操作,是手机厂商必须要思考和优化的地方:

  1. 超级 App 的性能表现以及优化
  2. 游戏高帧率合作
  3. 120fps、90 fps 和 60 fps 相互切换的逻辑

参考资料

  1. https://www.jianshu.com/p/304f56f5d486
  2. http://gityuan.com/2017/02/25/choreographer/
  3. https://developer.android.com/reference/android/view/Choreographer
  4. https://www.jishuwen.com/d/2Vcc
  5. https://juejin.im/entry/5c8772eee51d456cda2e8099
  6. Android 开发高手课

本文知乎地址

由于博留言交流不方便,点赞或者交流,可以移步本文的知乎界面
知乎 - Android 基于 Choreographer 的渲染机制详解
掘金 - Android 基于 Choreographer 的渲染机制详解

关于我 && 博客

下面是个人的介绍和相关的链接,期望与同行的各位多多交流,三人行,则必有我师!

  1. 博主个人介绍 :里面有个人的微信和微信群链接。
  2. 本博客内容导航 :个人博客内容的一个导航。
  3. 个人整理和搜集的优秀博客文章 - Android 性能优化必知必会 :欢迎大家自荐和推荐 (微信私聊即可)
  4. Android性能优化知识星球 : 欢迎加入,多谢支持~

一个人可以走的更快 , 一群人可以走的更远

微信扫一扫

Android 桌面被杀问题分析案例

作者 Gracker
2019年9月17日 18:58

写这篇文章的契机是因为一个实际遇到的问题 , 这个问题其实不难 , 不过在分析了这个问题然后写日记的时候 , 我突然觉得这个问题分析的过程有必要记录一下 , 分享给大家 . 分析过程中有用到一些工具 , 一些方法 , 也从另外一个聪明的小伙伴梅明那里学到了一些分析技巧和工具的使用技巧 .

这篇文章中分析过程包括我之前在Android 中的卡顿丢帧原因概述 - 方法论 里面提到的一些工具 , 包括 : 复现视频 \ Event Log \ Android Studio 源码和 App Debug \ Android Studio Profile \ Systrace \ Dumpsys \ PS 等 . 大多数工具大家都在开发过程中使用过 , 这次分析正是使用了这些工具相互配合 , 最终找到的问题的原因.

大家看下来可能会觉得 , 这么简单一个问题还需要写一篇文章 ? 我写这篇文章的目的一是为了记录给自己 , 二是觉得分析过程比较有普遍性 , 包括分析思路和工具的使用 , 如果可以帮助到大家 , 那么最好不过了 , 如果你也有好的思路或者独家调试技巧 , 欢迎大家扫描关于我 里面的讨论群二维码加入群聊 , 共同进步!

现象

这个问题是测试直接报过来的 , Bug 描述是典型的按现象描述 : “从应用返回桌面 , 桌面图标加载慢“. 测试这边提供了录制的视频和抓取的 Log , 以及对应的 Systrace 等. 既然现象和 Log 都在 , 那么就开始分析吧.

分析过程

确定问题发生的时间点

  1. 由于测试提供的复现视频 , 首先看复现视频 , 确定时间发生的时间
  2. 根据视频里面的大概时间(精确到分) , 查看对应的 EventLog ,跟视频比对,确定发生的确切时间点 (精确到秒)
  3. 查看 EventLog 和 MainLog , 还原发生时候的用户操作 ,这个例子里面就发现启动和我信这个 App 之后,Launcher 被杀了
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
EventLog
// 启动 com.jx.cmcc.ict.ibelieve 这个 App
09-10 10:14:48.877 1456 2269 I am_set_resumed_activity: [0,com.jx.cmcc.ict.ibelieve/.ui.MainTabActivity,resumeTopActivityInnerLocked]
09-10 10:14:48.886 1456 2269 I am_resume_activity: [0,80317506,54938,com.jx.cmcc.ict.ibelieve/.ui.MainTabActivity]
09-10 10:14:48.891 1456 1485 I sysui_count: [window_time_0,0]
09-10 10:14:48.891 1456 1485 I sysui_multi_action: [757,803,799,window_time_0,802,0]
09-10 10:14:48.902 1456 2269 I am_uid_stopped: 10021

// 这里桌面被杀
09-10 10:14:48.903 1456 2269 I am_kill : [0,13509,com.meizu.flyme.launcher,600,kill background]

// 这里开始从 App 返回桌面
09-10 10:14:51.990 1456 1791 I am_pause_activity: [0,80317506,com.jx.cmcc.ict.ibelieve/.ui.MainTabActivity]
09-10 10:14:51.994 1456 1791 I am_task_to_front: [0,54923]
09-10 10:14:51.996 13674 13674 I am_on_paused_called: [0,com.jx.cmcc.ict.ibelieve.ui.MainTabActivity,handlePauseActivity]
09-10 10:14:52.013 1456 2270 I am_uid_running: 10021

// 重新创建桌面进程
09-10 10:14:52.025 1456 2270 I am_proc_start: [0,14013,10021,com.meizu.flyme.launcher,activity,com.meizu.flyme.launcher/.Launcher]
09-10 10:14:52.045 1456 2270 I am_proc_bound: [0,14013,com.meizu.flyme.launcher]
09-10 10:14:52.069 1456 2270 I am_uid_active: 10021
09-10 10:14:52.069 1456 2270 I am_restart_activity: [0,238217861,54923,com.meizu.flyme.launcher/.Launcher]

// 桌面显示
09-10 10:14:52.071 1456 2270 I am_set_resumed_activity: [0,com.meizu.flyme.launcher/.Launcher,minimalResumeActivityLocked]
09-10 10:14:52.335 14013 14013 I am_on_resume_called: [0,com.meizu.flyme.launcher.Launcher,LAUNCH_ACTIVITY]
09-10 10:14:52.437 1456 1504 I am_activity_launch_time: [0,238217861,com.meizu.flyme.launcher/.Launcher,413,413]

那么这里就可以简单还原问题了 , 测试报的是从应用返回桌面 , 桌面图标加载慢 , 从 Event Log 来看 , 桌面显示慢 , 是因为桌面被杀了 , 所以从 App 返回的时候 , 桌面需要重新加载 , 从桌面进程创建到桌面完全显示 , 花费了 413ms(实际到桌面完全显示,花费了至少 2s 左右,因为 Launcher 冷启动还要重新加载内容).

分析被杀原因

从上面的分析来看 , 我们需要找到 Launcher 被杀的原因 , 从现象上来看 , 似乎是和 com.jx.cmcc.ict.ibelieve 这个进程有关系 , 但是我们目前是没有办法确认的 .

这里我们重点看这个这个 Event Log

1
am_kill : [0,13509,com.meizu.flyme.launcher,600,kill background]

这里可以看到 Launcher 被杀的原因是 kill background , 查看对应的源码可知,reason = kill background 是 AMS.killBackgroundProcesses 这里发出的.

ActivityManagerService.killBackgroundProcesses

1
2
3
4
5
6
7
public void killBackgroundProcesses(final String packageName, int userId) {
......
synchronized (this) {
killPackageProcessesLocked(packageName, appId, targetUserId,
ProcessList.SERVICE_ADJ, false, true, true, false, "kill background");
}
}

对源码比较熟悉的同学可以很快知道 , AMS.killBackgroundProcesses 这个接口会提供给三方应用去调用 , 其 Binder 的客户端在 ActivityManager.killBackgroundProcesses 这里

ActivityManager.killBackgroundProcesses

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* Have the system immediately kill all background processes associated
* with the given package. This is the same as the kernel killing those
* processes to reclaim memory; the system will take care of restarting
* these processes in the future as needed.
*
* @param packageName The name of the package whose processes are to
* be killed.
*/
@RequiresPermission(Manifest.permission.KILL_BACKGROUND_PROCESSES)
public void killBackgroundProcesses(String packageName) {
try {
getService().killBackgroundProcesses(packageName,
mContext.getUserId());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}

对 SystemServer 进程进行断点 Debug

知道了上面的代码逻辑 , 我们需要做的就是找到在这个场景下 , 是哪个应用调用 ActivityManager.killBackgroundProcesses 杀掉了桌面. 由于不知道具体是哪个应用(这里虽然我们怀疑是 com.jx.cmcc.ict.ibelieve , 但是没有证据) , 我们先对 SystemServer 进程进行 Debug .

1.首先对源码进行 debug , 首先在 Android 中点击 debug 按钮 , 选择 system_process 这个进程(就是我们所说的 SystemServer) , 然后点击 OK . 代码的断点我们打在上面列出的 ActivityManagerService.killBackgroundProcesses 方法里面.

2.点击启动怀疑的 App ( 可以从 EventLog 和视频里面倒推,找到比较可疑的 App , 安装后进行本地测试复现 , 这里选择了视频中出现的几个应用,包括我们之前怀疑的 com.jx.cmcc.ict.ibelieve- 和我信 ) , 点击其他的应用都不会进入到这个断点, 而在点击 和我信 这个 App 启动后走到的断点

3.这里我们可以看到调用栈是一个 Binder 调用 , 我们需要找到这个 Binder 调用的客户端. 在 AS 里面继续操作 , 点击如下图的计算器按钮 , 输入 getRealCallingPid() 点击下面的 Evaluate , 就可以看到结果. result = 29771

4.通过 PS 命令 , 查看这个 pid 对应的 app

可以看到就是这个应用调用的 killBackgroundProcesses

对 App 进程进行断点 Debug

为了进一步调查,我们对这个 app 进行 debug , 由于没有源码,我们直接把断点打到 android/app/ActivityManager.killBackgroundProcesses 这里(因为这里是客户端代码 , 所以调试 App 进程的时候 , 可以直接打断点 )

本地安装这个应用进行调试, 发现登录后,再次启动, 桌面必会被杀 ,确定就是这个 App 的问题

到了这一步我们已经基本上确定问题就是这个 App 引起的了 , 不过如果我们想看比较详细的调用情况 , 可以使用 Android Studio Profile

使用 Android Studio Profiler 工具

打开 Android Studio , 点击 Profiler 按钮 , 点击 + 号 , 选择 com.jx.cmcc.ict.ibelieve 这个进程 , 然后点击 CPU 这一栏

这里选择了 Trace Java Methods , 然后点击旁边的 Record , 就可以开始进行操作 , 操作结束后 , 点击 Stop , AS 会自动开始解析.

解析结果我们可以看这里

最下面就是刚刚操作所对应的详细函数调用栈 , 以真正运行的顺序展示在我们面前(我经常会用这个工具来查看源码逻辑和三方应用的代码逻辑 , 不管是学习还是解决问题 , 都是一个非常好的方法)

我们使用 ctrl+f 进行搜索 killBackgroundProcesses , 如果有的话 , 会以高亮显示, 我们只需要用鼠标放大就可以看到详细的调用栈

可以看到这个 App 在 loadComplete 回调里面执行了 killBackground 方法.(到了这里,应用开发者就已经知道是哪里的问题了,修复起来飞快)

权限问题分析

如上面所示 , 调用 killBackgroundProcesses 是需要Manifest.permission.KILL_BACKGROUND_PROCESSES 这个权限的 .

1
2
3
@RequiresPermission(Manifest.permission.KILL_BACKGROUND_PROCESSES)
public void killBackgroundProcesses(String packageName) {
}

执行 adb shell dumpsys package com.jx.cmcc.ict.ibelieve 查看 com.jx.cmcc.ict.ibelieve 这个进程所申请的权限 , 发现这个应用在安装的时候就申请了KILL_BACKGROUND_PROCESSES 这个权限 , 且默认是授予的.

1
2
3
4
5
6
install permissions:
......
android.permission.ACCESS_NETWORK_STATE: granted=true
android.permission.KILL_BACKGROUND_PROCESSES: granted=true
android.permission.WRITE_USER_DICTIONARY: granted=true
......

对应的权限级别为 normal

也就是说 , 所有的第三方应用都可以默认有这个权限 , 只要你申请 . 这个案例里面 , 就是因为这个 App 申请了这个权限 , 且执行了错误的行为 , 导致把桌面杀掉 , 严重影响用户体验. Sad !

Systrace 工具可以找出来 Kill 桌面的元凶么?

由于经常使用 Systrace , 那么 Systrace 是否可以找到元凶呢? 答案是可以 (这里如果对如何在 Systrace 上查看唤醒信息不了解 , 可以看一下这篇文章分析 Systrace 预备知识). 我们录制一段 Systrace , 安装下面的顺序去看

1.首先看 system_server 进程对应的 trace ,找到 killProcessGroup 对应的点 , 查看其唤醒情况 , 可以看到是 19688 这个线程唤醒执行 AMS 的 killProcessGroup

在 Systrace 中搜索 19688 , 可以看到是 Binder:1295_1E , 1295 就是 SystemServer

查看对应的 Binder:1295_1E , 看看是哪个线程唤醒这个线程

搜索 7289这个线程 , 可以看到这个线程就是和我信这个 App 的主线程。

查看 7289 , 确定就是 com.jx.cmcc.ict.ibelieve 这个进程 . 也就是和我信这个 App(瘤子).

这里也可以反推出来这个 Kill 是 和我信 这个 App 发起的 , 进一步确认可以使用上面 AS 的 MethodTrace

总结

从上面的分析来看 , 这个问题是由于应用申请了不恰当的权限并错误使用对应的函数导致的一个严重影响用户使用的问题. 一般分析到这一步 , 我们的工作就基本上结束了 , 后续只需要和商店沟通 , 跟 App 开发者联系进行修改即可.

不过令我感到意外的是 android.permission.KILL_BACKGROUND_PROCESSES 这个权限 Google 居然放的这么松 , 我一直以为这个权限是要专门申请以防止 App 滥用或者卵用的(毕竟涉及到其他 App 的生死存亡).

本文知乎地址

由于博客留言交流不方便,点赞或者交流,可以移步本文的知乎界面
知乎 - Android 框架问题分析案例 - 谁杀了桌面?

关于我 && 博客

下面是个人的介绍和相关的链接,期望与同行的各位多多交流,三人行,则必有我师!

  1. 博主个人介绍 :里面有个人的微信和微信群链接。
  2. 本博客内容导航 :个人博客内容的一个导航。
  3. 个人整理和搜集的优秀博客文章 - Android 性能优化必知必会 :欢迎大家自荐和推荐 (微信私聊即可)
  4. Android性能优化知识星球 : 欢迎加入,多谢支持~

一个人可以走的更快 , 一群人可以走的更远

微信扫一扫

Android 系统开发源码环境搭建

作者 Gracker
2018年11月1日 19:40

不管是 Android App 开发工程师还是 Android 系统开发工程师,对 Android 系统有一定的了解是很有必要的,正好我这几天在家里搭建了个开发环境,把过程分享出来,有需要的人可以看一下。

大概的步骤包含下面几个:

  1. 安装 Ubuntu 系统
  2. 配置 Ubuntu 系统
  3. 安装必备的软件
  4. 配置 VPN (可选)
  5. 下载 AOSP 代码
  6. 配置编译环境并编译 Pixel 的代码
  7. 刷机
  8. 修改和编译 Framework 、Service、Res

建议准备下面的硬件,当然没有也没关系,有了更好

  1. PC 或者笔记本
  2. 512GB 的 SSD
  3. Pixel 手机一台

安装 Ubuntu 系统

Linux 这边我建议用 Ubuntu 系统,不建议用虚拟机,直接安装一个新的 Ubuntu 系统会比较好,Ubuntu 目前最新的 LTS 版本是 18.04,目前安装 Ubuntu 的步骤会比较简单:

  1. 下载 Ubuntu18.04 版本:ubuntu 18.04
  2. 使用 Ubuntu 推荐的工具做一个 U 盘启动盘
  3. 使用 U 盘安装 Ubuntu 系统

mydesktop

配置 Ubuntu 系统

  1. 安装搜狗输入法
  2. 安装 vim :sudo apt install vim
  3. 安装 adb
  4. 安装 fastboot

安装必备的软件

  1. VS Code
  2. Android Studio
  3. Meld
  4. Wine
  5. WPS

配置 VPN (可选)

  1. ShadowSocks

下载 AOSP 代码

没有 v-p-n 的话,推荐使用清华的镜像站:https://mirror.tuna.tsinghua.edu.cn/help/AOSP/

## repo 下载 


1
2
3
4
mkdir ~/bin
PATH=~/bin:$PATH
curl https://storage.googleapis.com/git-repo-downloads/repo > ~/bin/repo
chmod a+x ~/bin/repo

建立工作目录

1
2
mkdir WORKING_DIRECTORY
cd WORKING_DIRECTORY

初始化仓库

1
repo init -u https://aosp.tuna.tsinghua.edu.cn/platform/manifest

下载代码(-c –no-tags 能下载更少的代码)

1
repo sync -c --no-tags

配置编译环境并编译 Pixel 的代码

配置编译环境

安装jdk

1
2
sudo apt-get update
sudo apt-get install openjdk-8-jdk

安装相关依赖

1
2
sudo apt-get install git-core gnupg flex bison gperf build-essential zip curl zlib1g-dev gcc-multilib g++-multilib libc6-dev-i386 lib32ncurses5-dev x11proto-core-dev libx11-dev lib32z-dev libgl1-mesa-dev libxml2-utils xsltproc unzip

下载 Pixle 的驱动

编译 Android Master 的代码的话,需要下载对应的手机的驱动,在这个页面找到自己需要的驱动:
https://developers.google.cn/android/blobs-preview
extract-qcom-sailfish

解压如下:
extract-google_devices-sailfish

编译 Pixle 的系统镜像

在源码根目录下执行

1
source build/envsetup.sh

执行下面的命令选择要编译的手机型号和版本(user、userdebug、eng)

1
lunch

lunch

选择好了之后,输入对应的数字或者数字后面的,执行 make 开始编译(可选择加 -j4,4带代表线程数,机器性能好的话可以8或者16,看cpu):

1
make -j8

make-j8

编译成功

out 目录会生成对应的 image
out_folde

刷机

在源码根目录下,执行下面的命令,即可刷入对应的系统到

fastboot

1
fastboot flashall

fastboot-flashal

修改和编译 Framework 、Service、Res

以下命令都在源码根目录下执行

修改代码 IDE

java代码

java 代码推荐使用 AndroidStudio 打开、编辑

c/cpp代码

c/cpp 代码推荐使用 SourceInsight 、Eclipse、VS Code 打开、编辑

编译 Framework

1
mmm framework/base

编译 Services

1
mmm frameworks/base/services

编译 res

1
mmm frameworks/base/core/res

root && remount

root--remount

push

root && remount 之后,就可以把对应的 framework、Services、Res 等 push 进去,重启 shell 即可生效,或者直接 adb sync system 即可, sync system 会把 out目录下对应机型的 system 目录和手机的 system 目录进行同步,很是方便。

例子:

1
adb root && adb remount && adb shell stop && adb sync system && adb shell start

关于我 && 博客

下面是个人的介绍和相关的链接,期望与同行的各位多多交流,三人行,则必有我师!

  1. 博主个人介绍 :里面有个人的微信和微信群链接。
  2. 本博客内容导航 :个人博客内容的一个导航。
  3. 个人整理和搜集的优秀博客文章 - Android 性能优化必知必会 :欢迎大家自荐和推荐 (微信私聊即可)
  4. Android性能优化知识星球 : 欢迎加入,多谢支持~

一个人可以走的更快 , 一群人可以走的更远

微信扫一扫

❌
❌