Profiler与内存分析优化之十-Profiler与内存分析优化

1,412 阅读5分钟

Profiler与内存分析优化

1.版本信息

Android Studio:2021.2.1

android sdk:

minSdk 21
targetSdk 32

开发语言: kotlin

2.运行一个空工程

新建一个空工程后,打开Profiler面板,SESSIONS点击加号,选择正在运行的进程。

如果机器安卓版本为API 26 以下级别,会看到提示 Advanced profiling unavailable for the selected process Configure this setting in tht Run Configuration

点击Run Configuration后,勾选上Enable additional support for older devices (API level <26),然后重新运行应用,可以看到之前的提示不再出现了。(开启后,有可能导致在调试安卓8.0系统的机器时,原有代码编译不通过,但调试8.0机器时却正常运行的问题)

但还是会提示 Advanced profiling is unavailable for the selected process 所选进程无法使用高级分析

3.内存分析

除了可以用Profiler分析内存之外,还可以用dumpsys命令查看内存使用情况

3.1 procstats

获取过去三小时内应用的内存占用情况统计信息

adb shell dumpsys procstats --hours 3

3.2 meminfo

adb shell dumpsys meminfo package_name|pid [-d]

3.3 模拟内存增长

AndroidManifest.xml里开启大内存

<application
	android:largeHeap="true">
</application>
private val objs = mutableListOf<Any>()

class MainActivity : AppCompatActivity() {
    private val TAG = "MainActivityTag";
    private var threads = mutableListOf<Thread>()
    private val isStop = AtomicBoolean(false)

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        val binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)
        binding.apply {

            consumeMemory.setOnClickListener {
                Thread {
                    while (!isStop.get()) {
                        (0..1000).forEach {
                            objs.add(
                                DataClassConsumeMemory(
                                    age = 18,
                                    name = System.currentTimeMillis().toString(),
                                    time = System.currentTimeMillis()
                                )
                            )
                            objs.add(
                                ConsumeMemory(
                                    age = 18,
                                    name = System.currentTimeMillis().toString(),
                                    time = System.currentTimeMillis()
                                )
                            )
                        }
                        Log.d(TAG, "size:${objs.size}")
                        Thread.sleep(100)
                    }
                }.start()
            }
        }
    }
}

代码运行后,可以看到内存一直在增长,然后在经过一段时间后,应用闪退

3.4 adb查看GC信息

adb logcat |grep GC


: Starting a blocking GC Explicit
41.952  3624  3631 I art     : Explicit concurrent mark sweep GC freed 36112(3MB) AllocSpace objects, 0(0B) LOS objects, 24% free, 48MB/64MB, paused 514us total 166.728ms
42.659  3624  3631 I art     : Starting a blocking GC Explicit
42.853  3624  3631 I art     : Explicit concurrent mark sweep GC freed 32084(877KB) AllocSpace objects, 0(0B) LOS objects, 24% free, 49MB/65MB, paused 17.199ms total 194.005ms50.742  3624  3635 I art     : Background sticky concurrent mark sweep GC freed 260646(6MB) AllocSpace objects, 0(0B) LOS objects, 10% free, 58MB/65MB, paused 562us total 121.987ms
53.775  3624  3635 I art     : Background sticky concurrent mark sweep GC freed 104254(2MB) AllocSpace objects, 0(0B) LOS objects, 0% free, 66MB/66MB, paused 21.971ms total 173.381ms
54.025  3624  3635 I art     : Background partial concurrent mark sweep GC freed 8039(3MB) AllocSpace objects, 0(0B) LOS objects, 20% free, 63MB/79MB, paused 589us total 197.941ms
01.858  3624  3635 I art     : Background sticky concurrent mark sweep GC freed 260645(6MB) AllocSpace objects, 0(0B) LOS objects, 8% free, 72MB/79MB, paused 661us total 115.078ms
05.301  3624  3635 I art     : Background sticky concurrent mark sweep GC freed 116287(3MB) AllocSpace objects, 0(0B) LOS objects, 3% free, 76MB/79MB, paused 626us total 104.554ms
07.869  3624  3635 I art     : Background partial concurrent mark sweep GC freed 16061(439KB) AllocSpace objects, 0(0B) LOS objects, 16% free, 79MB/95MB, paused 644us total 279.146ms
15.780  3624  3635 I art     : Background sticky concurrent mark sweep GC freed 264655(7MB) AllocSpace objects, 0(0B) LOS objects, 7% free, 88MB/95MB, paused 33.748ms total 191.107ms
18.001  3624  3635 I art     : Background sticky concurrent mark sweep GC freed 76186(2MB) AllocSpace objects, 0(0B) LOS objects, 0% free, 98MB/98MB, paused 733us total 170.209ms
18.391  3624  3635 I art     : Background partial concurrent mark sweep GC freed 12046(4MB) AllocSpace objects, 0(0B) LOS objects, 14% free, 94MB/110MB, paused 740us total 331.799ms
26.265  3624  3635 I art     : Background sticky concurrent mark sweep GC freed 264655(7MB) AllocSpace objects, 0(0B) LOS objects, 6% free, 103MB/110MB, paused 36.305ms total 
188.487ms
29.684  3624  3635 I art     : Background sticky concurrent mark sweep GC freed 112781(3MB) AllocSpace objects, 0(0B) LOS objects, 2% free, 107MB/110MB, paused 37.133ms total 
154.143ms
31.207  3624  3635 I art     : Background sticky concurrent mark sweep GC freed 51620(1411KB) AllocSpace objects, 0(0B) LOS objects, 1% free, 108MB/110MB, paused 823us total 111.132ms
32.339  3624  3635 I art     : Background partial concurrent mark sweep GC freed 20077(548KB) AllocSpace objects, 0(0B) LOS objects, 12% free, 110MB/126MB, paused 38.515ms total 403.578ms
40.257  3624  3635 I art     : Background sticky concurrent mark sweep GC freed 264655(7MB) AllocSpace objects, 0(0B) LOS objects, 5% free, 119MB/126MB, paused 42.145ms total 
218.301ms

在查看内存时间轴上,也可以看到有触发GC的图标(有的机器没有),也可以手动触发GC,点击MEMORY旁边的Forcing garbage collection按钮,可以看到logcat会打印一条日志。

gc.PNG

3.5 内存计数中的类别

  • Java:从 Java 或 Kotlin 代码分配的对象的内存。

  • Native:从 C 或 C++ 代码分配的对象的内存。

    即使您的应用中不使用 C++,您也可能会看到此处使用了一些原生内存,因为即使您编写的代码采用 Java 或 Kotlin 语言,Android 框架仍使用原生内存代表您处理各种任务,如处理图像资源和其他图形。

  • Graphics:图形缓冲区队列为向屏幕显示像素(包括 GL 表面、GL 纹理等等)所使用的内存。(请注意,这是与 CPU 共享的内存,不是 GPU 专用内存。)

  • Stack:您的应用中的原生堆栈Java 堆栈使用的内存。这通常与您的应用运行多少线程有关。

  • Code:您的应用用于处理代码和资源(如 dex 字节码、经过优化或编译的 dex 代码、.so 库和字体)的内存。

  • Others:您的应用使用的系统不确定如何分类的内存。

  • Allocated:您的应用分配的 Java/Kotlin 对象数。此数字没有计入 C 或 C++ 中分配的对象。

    如果连接到搭载 Android 7.1 及更低版本的设备,只有在内存分析器连接到您运行的应用时,才开始此分配计数。因此,您开始分析之前分配的任何对象都不会被计入。但是,Android 8.0 及更高版本附带一个设备内置性能剖析工具,该工具可跟踪所有分配,因此,在 Android 8.0 及更高版本上,此数字始终表示您的应用中待处理的 Java 对象总数。

3.6 Capture heap dump 捕获堆转储

View objects in your app that are using memory at a specific point in time。

查看应用程序中在特定时间点使用内存的对象

Capture heap dump可以看到的信息有

  • 您的应用分配了哪些类型的对象,以及每种对象有多少。
  • 每个对象当前使用多少内存。
  • 在代码中的什么位置保持着对每个对象的引用。
  • 对象所分配到的调用堆栈。(目前,对于 Android 7.1 及更低版本,只有在记录分配期间捕获堆转储时,才会显示调用堆栈的堆转储。)

选择Capture heap dump ,点击 Record,自动记录一段时间后会自动停止下来。

点击Heap Dump查看

Allocations :内存分配 在选定时间段内通过 malloc()new 运算符分配的对象数。

Native Size:此对象类型使用的原生内存总量(以字节为单位)。只有在使用 Android 7.0 及更高版本时,才会看到此列。您会在此处看到采用 Java 分配的某些对象的内存,因为 Android 对某些框架类(如 Bitmap)使用原生内存

Shallow Size:直接占用内存 此对象类型使用的 Java 内存总量(以字节为单位)

Retained Size:占用总内存 为此类的所有实例而保留的内存总大小(以字节为单位)。

直接通过滤包名查看应用内生成的对象内存占用情况

heap_dump_filter.PNG 可以查看每一个类,创建了多少个实例对象,占用了多少内存,有没有内存泄露(Leaks)

3.7 Record Java/kotlin allocations 记录java/kotlin 内存分配

View how each Java/Kotlin object was allocated over a period of time

查看在一段时间内如何分配每个Java/Kotlin对象

使用方式

1.勾选 Record Java/Kotlin allocations

2.点击 Record

3.开始记录后,需要手动点击 Stop,才会停,而Capture heap dump是会自动停止的。

相比 Capture heap dump 不同/相同的是,可以看到以下信息,其他的大同小异。

  • 分配了哪些类型的对象以及它们使用多少空间(相同点)。
  • 每个分配的堆栈轨迹,包括在哪个线程中(不同点)。
  • 对象在何时被取消分配(仅当使用搭载 Android 8.0 或更高版本的设备时)(不同点)。

3.8 导出导入 HPROF 文件

导出:右键 "Heap Dump"选择"Export"菜单,然后就可以导出**.hprof**文件。

导入:点击 "SESSIONS" 旁边的"+"号,选择"Load from file",选择**.hprof文件**

3.9 内存泄漏检测

内存泄露需要Capture heap dumpRecord Java/kotlin allocations 不会显示内存泄露。

捕获堆转储后,如果有内存泄露,会显示有多个对象泄露了。

以常见的Runnable引用Activity,造成内存泄露为例

class TestLeaksActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val binding = ActivityTestLeaksLayoutBinding.inflate(layoutInflater)
        setContentView(binding.root)

        binding.btnClose.postDelayed({
            //这里会造成内存泄露
            Log.d(logTAG, this@TestLeaksActivity.hashCode().toString());
        }, 10 * 1000)

        binding.btnClose.setOnClickListener {
            finish()
        }
    }
}

leak.PNG

可以配合Record Java/kotlin allocations查看Allocation Call Stack,可以更快的找到内存泄露的代码在哪里。

4.使用 CPU 性能分析器检查 CPU 活动

3.1 创建线程,观察运行结果

在Activity里放三个按钮 分别执行

1.创建线程但不运行只放到一个List里管理

2.运行线程,线程启动后只循环向控制台打印一个字符串,并Thread.sleep(100)

3.停止线程

class MainActivity : AppCompatActivity() {
    private val TAG = "MainActivityTag";
    private var threads = mutableListOf<Thread>()
    private val isStop = AtomicBoolean(false)
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        val binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)
        binding.apply {
            createThread.setOnClickListener {
                val threadName = "ThreadTest:${threads.size+1}"
                threads.add(Thread({
                    while (!isStop.get()) {
                        Log.d(TAG,threadName)
                        Thread.sleep(100)
                    }
                }, threadName))
            }

            startThread.setOnClickListener {
                threads.filter { f ->
                    f.state == Thread.State.NEW
                }.forEach { t -> t.start() }
            }

            stopThread.setOnClickListener {
               isStop.set(true)
                threads.clear()
            }
        }
    }
}

//模拟耗时操作
fun workTime(ms: Long) {
    val start = System.currentTimeMillis()
    while (System.currentTimeMillis() < start + ms) {

    }
}

3.2 观察CPU线程运行状态

打开 CPU 性能剖析器,会看到列出属于应用进程的每个线程,并使用下面列出的颜色在时间轴上指示它们的活动

  • 绿色:表示线程处于活动状态或准备使用 CPU。也就是说,线程处于正在运行或可运行状态。
  • 黄色:表示线程处于活动状态,但它正在等待一项 I/O 操作(如磁盘或网络 I/O),然后才能完成它的工作。
  • 灰色:表示线程正在休眠且没有消耗任何 CPU 时间。 当线程需要访问尚不可用的资源时,就会出现这种情况。在这种情况下,要么线程主动进入休眠状态,要么内核将线程置于休眠状态,直到所需的资源可用。
  • 白色: Dead状态。

3.2.1 Sleeping状态

创建几个线程后,未点击startThread按钮时,线程还没有创建,所以是观察不到的。

点击startThread后,可以看到新增的线程,由于Log.d(TAG,threadName)耗时极小,所以观察到新增的线程一直是灰色的,偶尔会看到一小段绿色,即线程状态绝大部分情况处于Sleeping状态。

CPU占用情况在1~12%之间跳动。

Log.d(TAG,threadName)
Thread.sleep(100)

cpu_occupation.PNG

3.2.2Running和Waiting之间切换状态

Log.d(TAG,threadName)
//Thread.sleep(100)

Thread.speep(100)注释掉,重新运行,创建6个线程运行。重新查看CPU情况,CPU占用在20%~80%之间浮动(取决于线程多少)。

和之前的灰色状态不同的是,线程轴颜色是绿色(Running)和黄色(Waiting)交替,绿色长度较长,黄色较短(取决于线程多少,如果线程多,则等待长,则黄色条比绿色条长,否则反之),说明多个线程在并发执行时,线程不停的在Running和Waiting之间切换。

3.2.3 模拟在线程里耗时操作

Thread({
    while (!isStop.get()) {
        workTime(1000)
        Log.d(TAG, threadName)
    }
}, threadName)

fun workTime(ms: Long) {
    val start = System.currentTimeMillis()
    while (System.currentTimeMillis() < start + ms) {

    }
}

可以看到此时线程一直是绿色,线程一直处于Running状态,CPU占用率也非常高,线程Runnable里不执行任何代码,其实线程也是一直处于Running状态,也是CPU占用高。

3.2.4 System Trace 跟踪系统调用

捕获非常翔实的细节,以便您检查应用与系统资源的交互情况。您可以检查线程状态的确切时间和持续时间、直观地查看所有内核的 CPU 瓶颈在何处,并添加需分析的自定义跟踪事件。当您排查性能问题时,此类信息至关重要。如需使用此配置,您必须将应用部署到搭载 Android 7.0(API 级别 24)或更高版本的设备上。

使用此跟踪配置时,您可以通过检测代码,直观地标记性能剖析器时间轴上的重要代码例程。如需检测 C/C++ 代码,请使用由 trace.h 提供的原生跟踪 API。如需检测 Java 代码,请使用 Trace 类。如需了解详情,请参阅检测您的应用代码

此跟踪配置建立在 systrace 的基础之上。您可以使用 systrace 命令行实用程序指定除 CPU 性能剖析器提供的选项之外的其他选项。systrace 提供的其他系统级数据可帮助您检查原生系统进程并排查丢帧或帧延迟问题

cpu_occupation_system_trace.PNG

3.2.5 Java/Kotlin Method Trace 跟踪 Java/Kotlin 方法

在运行时检测应用,从而在每个方法调用开始和结束时记录一个时间戳。系统会收集并比较这些时间戳,以生成方法跟踪数据,包括时间信息和 CPU 使用率。

请注意,与检测每个方法相关的开销会影响运行时性能,并且可能会影响分析数据;对于生命周期相对较短的方法,这一点更为明显。此外,如果应用在短时间内执行大量方法,则分析器可能很快就会超出其文件大小限制,因而不能再记录更多跟踪数据。

Summary 总结/概要

Longest running events(top 10)

可以查看耗时最长的方法

cpu_occupation_summary.PNG

Top Down

cpu_occupation_top_down.PNG

Flame Chart

cpu_occupation_flame_chart.PNG

Bottom Up

cpu_occupation_bottom_up.PNG

Event

cpu_occupation_event.PNG

3.2.6 Java/Kotlin Method Sample(Legacy) 对 Java/Kotlin 方法采样

在应用的 Java 代码执行期间,频繁捕获应用的调用堆栈。分析器会比较捕获的数据集,以推导与应用的 Java 代码执行有关的时间和资源使用信息。

基于采样的跟踪存在一个固有的问题,那就是如果应用在捕获调用堆栈后进入一个方法并在下次捕获前退出该方法,分析器将不会记录该方法调用。如果您想要跟踪生命周期如此短的方法,应使用插桩跟踪。

信息与跟踪Java/Kotlin方法相比,只有Threads信息,但更详细。

以Top Down为例

cpu_occupation_sample.PNG