政务公开加强网站规范化建设,wordpress下一篇调用,个人域名邮箱怎么弄,360网站建设怎么用今天从实战的角度讲解下如何使用Android Studio Profiler分析应用的CPU和内存。 CPU Profiler 常见问题 问题 用户操作应用的过程中出现卡顿/ANR现象#xff0c;或出现下面日志。 Skipped XX frames! The application may be doing too much work on its main thread.原…今天从实战的角度讲解下如何使用Android Studio Profiler分析应用的CPU和内存。CPU Profiler常见问题问题用户操作应用的过程中出现卡顿/ANR现象或出现下面日志。Skipped XX frames!The application may be doing too much work on its main thread.原因这类问题是由于在主线程中做了耗时操作才导致的。尽管开发人员都会有意识地避开这点但很多时候项目庞大函数调用链冗长或遇到了陌生的函数就会不经意间在主线程中写了耗时操作。实战演练例子点击下图中的按钮在主线程上拷贝应用中的图片到SD卡上。publicclassTimeConsumingActivityextendsAppCompatActivity{privatestaticfinalStringTAGTimeConsumingActivity;OverrideprotectedvoidonCreate(NullableBundlesavedInstanceState){super.onCreate(savedInstanceState);ActivityTimeConsumingBindingbindingActivityTimeConsumingBinding.inflate(getLayoutInflater());setContentView(binding.getRoot());binding.btnTimeConsuming.setOnClickListener(v-{doTimeConsumingTask();});}privatevoiddoTimeConsumingTask(){FilecopyDestDirectorynewFile(Environment.getExternalStorageDirectory(),Sample);FileUtils.deleteDirectory(copyDestDirectory);intcount0;while(count3){//拷贝asset目录下的sample.jpg图片到SD卡目录中FileUtils.checkAssertAndReplaceFile(getApplicationContext(),sample.jpg,copyDestDirectory.getPath()/count.jpg);Log.d(TAG,Copy completed:count);count;}}}执行后我们会发现如下日志Skipped124frames!The application may be doing too much work on its main thread.排查Android Studio Profiler提供了2种Find CPU Hotspots查找CPU热点方式Callstack Sample调用栈采样和Java/Kotlin Method Recording方法记录。特性Callstack Sample (调用栈采样)Java/Kotlin Method Recording方法记录技术原理由JVM定期如每隔几微秒抓取所有线程的调用栈快照。通过统计分析这些快照样本估算出各方法的CPU占用时间比例。在应用运行时通过插桩技术在每个方法的开始和结束处插入记录代码。精确记录每个方法的进入和退出时间戳最终汇总成详细的执行时间数据。性能开销极低。由于只是周期性“拍照”对应用的实际运行性能影响很小适合进行长时间几分钟甚至更久的性能数据收集。非常高。由于每个方法的调用都会被记录会产生巨大的数据量和处理开销会明显拖慢应用的运行速度。因此建议录制时间不超过5秒以免生成过大的文件和分析困难。数据精度统计级精度。它是通过样本来估算的因此存在一定的误差。对于执行时间非常短小于采样间隔的方法有可能被遗漏不在结果中显示。精确级精度。它能记录下每一个被调用的方法及其精确的耗时数据非常详尽没有任何遗漏。使用选择初步排查应用卡顿原因找到引发卡顿的函数。它的低开销允许你进行长时间记录更容易捕捉到偶发的性能问题。已经通过采样定位到了引发卡顿的热点方法后现在想深入分析这个方法内部每一行代码或子函数的精确耗时以进行细致的优化。我们进入耗时测试页面启动Callstack Sample调用栈采样点击耗时操作按钮等待耗时操作按钮复原后停止采样。选择主线程线程名称为应用包名我们将得到以下采样结果左边为Call Chart右边为Flame Chart。在Flame Chart的窗口中输入包名会加粗显示应用内函数。将鼠标悬停在这些函数上逐一排查找到时间占比最大的函数。图中我们可以很清楚看到一个onClick回调的时间占比最大再往上看可以发现函数doTimeConsumingTask在OnClick回调中时间占比最大。所以接下来我们需要深入分析下函数doTimeConsumingTask。实际上没有必要我们的例子非常简单从上图中我们就可以看出函数checkAssertAndReplaceFile在函数doTimeConsumingTask的时间占比最大我们直接看函数checkAssertAndReplaceFile的源码就知道它做的是IO操作但当前线程是主线程所以很明显我们的修复方向就是把函数checkAssertAndReplaceFile改到子线程上执行。可是我们真实的项目远比这个例子复杂得多所以我们先假装不知道接着往下分析。我们进入耗时测试页面启动Java/Kotlin Method Recording方法记录点击耗时操作按钮等待耗时操作按钮复原后停止采样。选择主线程线程名称为main调整下缩放比例我们将得到下面的记录结果。在Flame Chart的窗口中输入doTimeConsumingTask会加粗显示该函数便于我们找到分析的目标。从Flame Chart中我们可以看到函数doTimeConsumingTask的时间占比主要由2个部分组成FileUtils.checkAssertAndReplaceFile和Log.d而且FileUtils.checkAssertAndReplaceFile占比更大。所以接下来我们就需要分析FileUtils.checkAssertAndReplaceFile的源码可以发现该函数执行的是IO操作不能放在主线程中。补充要点CPU Profiler主要依靠四张图帮助我们分析问题Call Chart、Flame Chart、Top Down、Bottom Up。Call ChartCall Chart函数调用图是时间线视图。横轴为时间线清楚地看出函数调用先后。纵轴从上到下调用者指向被调用者调用栈清晰易懂。但不利于我们分析每个函数的耗时因为某些函数单次调用时耗时不算大但被多次调用后总耗时就会很大。这类函数在Call Chart上显示地很分散不利于我们发现它们这时候就需要Flame Chart。Call ChartFlame ChartFlame Chart火焰图是占比视图。它统计了相同调用栈中同一个函数在调用者中的总耗时并且以某个比例显示出来。所以横轴表示函数调用的时间占比纵轴与Call Chart相反从下到上调用者指向被调用者。此时的图表看起来越往上越窄就好像火焰一样因此得名。利用Flame Chart我们可以很方便地找到耗时最大的函数并看出该函数由哪些部分组成这些部分占比多少。但我们无法从Flame Chart看到某个函数的具体耗时这时候就需要Top Down和Bottom Up了。Flame ChartTop DownTop Down Tree向我们展示了一个函数调用了哪些其它函数自身代码的耗时以及调用其它函数的耗时。构建一个 Top Down Tree 并不复杂。以 Flame Chart 为基础您只需要从调用者开始持续添加被调用者作为子节点直到整个 Flame Chart 被遍历一遍您就获得了一个 Top Down Tree:对于每个节点我们关注三个时间信息:Self TimeChildren TimeTotal Time运行自己的代码所消耗的时间调用其它函数的时间前面两者时间之和节点Total Time 节点Self Time 节点Children Time 节点Self 所有子节点Total Time之和。如下图中A Total Time A Self Time A Children Time A Self Time B Total Time D Total Time。Bottom UpBottom Up Tree向我们展示了一个函数被哪些其它函数调用了自身被调用的总时长以及分别被其它函数调用了多长时间。构建Bottom Up Tree是将每个被调用者作为顶点持续添加调用者作为子节点来反向构建出树。由于每个被调用者都可以独立构建出一棵树所以这里其实是森林 (Forest)和Top Down Tree相比Bottom Up Tree上每个节点的Self Time、Children Time和Total Time的含义是不同的。Bottom Up Tree中每个节点的时间信息参照的是各自树的顶部节点。SelfChildrenTotal树顶部的函数顶部节点表示在记录的持续时间内函数在执行自己的代码而非被调用者的代码上所花的总时间。表示在记录的持续时间内函数在执行它的被调用者而非自己的代码上所花的总时间。Self 时间和 Children 时间的总和。调用者子节点表示被调用者在由调用者调用时的总 Self 时间。比如B1的Self表示由B调用C时C的Self时间的总和。表示被调用者在由调用者调用时的总 Children时间。比如B1的Children表示由B调用C时C的Children时间的总和。Self 时间和 Children 时间的总和。也表示被调用者在由调用者调用时的总Total时间。比如B1的Total表示由B调用C时C的Total时间的总和。节点Total Time 所有子节点Total Time之和节点Self Time 所有子节点Self Time之和节点Children Time 所有子节点Children Time之和如下图中C Total Time B1 Total Time D Total Time;C Self Time B1 Self Time D Self Time;C Children Time B1 Children Time D Children Time;Memory Profiler常见问题问题内存泄漏和内存溢出。原因内存泄漏该回收的对象没回收占着茅坑不拉屎。内存溢出内存真的不够用了坑位全满了新人进不来。二者的联系内存泄漏是内存溢出的原因之一持续的内存泄漏会不断蚕食可用内存当剩余内存不足以分配新对象时就会触发内存溢出。内存溢出不一定由泄漏引起一次加载超大的图片、一次性处理超大数据量即使没有泄漏也可能直接 OOM。实战演练内存泄漏从首页进入内存泄漏页面再退出。如此重复操作多次。publicclassMemoryLeakActivityextendsAppCompatActivity{privatestaticfinalStringTAGMemoryLeakActivity;OverrideprotectedvoidonCreate(NullableBundlesavedInstanceState){super.onCreate(savedInstanceState);ActivityMemoryLeakBindingbindingActivityMemoryLeakBinding.inflate(getLayoutInflater());setContentView(binding.getRoot());newHandler().postDelayed(newRunnable(){Overridepublicvoidrun(){Log.d(TAG,MemoryLeakActivity-runnable);}},20_000L);}}排查如果重复操作某些步骤后引发了OutOfMemoryError那么就极有可能是内存泄漏。先按上述操作应用然后点击Android Studio Profiler的Analyze Memory Usage等待几秒后我们可以看到MemoryLeakActivity泄漏了。MemoryLeakActivity泄漏了说明必然存在生命周期比MemoryLeakActivity还要长的对象引用了MemoryLeakActivity。所以我们需要分析引用了MemoryLeakActivity的对象。选中泄漏的MemoryLeakActivity类Instance窗口中会显示泄漏的MemoryLeakActivity对象。选择一个MemoryLeakActivity对象点击References标签就会显示出所有引用该MemoryLeakActivity对象的对象。点击Retained Size标签将这些对象按照Retained Size从大到小的顺序排列。在References窗口中若一个对象的Retained Size该对象的Shallow Size MemoryLeakActivity对象的Retained Size说明该对象是导致MemoryLeakActivity对象泄漏的元凶之一。这里只有1个this$0符合条件鼠标悬停在它上右键点击Jump to Source跳转到源码。从而发现一个Runnable的匿名内部类的对象引用了MemoryLeakActivity对象导致MemoryLeakActivity对象泄漏了。补充要点引用链在Instance和References窗口中对象是这样表示的对象的类名对象的唯一标识(对象的内存地址)。对象的唯一标识通常基于对象的 hashCode() 生成。以上图Instance窗口中的MemoryLeakActivity315206144(0x12c9aa00)为例。在References窗口中有多个对象引用了泄漏的MemoryLeakActivity对象。我们将这些对象左侧的“”展开就可以看到MemoryLeakActivity对象的完整引用链。如下图所示为MemoryLeakActivity对象的其中一条引用链。引用链的格式如下以上图的引用链为例表示如下引用关系MessageQueue对象的mMessages字段引用Message对象Message对象的next字段引用新的Message对象新的Message对象的callback字段引用MemoryLeakActivity的匿名内部类对象即Runnable对象Runnable对象的this$0字段引用MemoryLeakActivity对象注Runnable对象是MemoryLeakActivity对象的非静态内部类的对象Java 编译器会为非静态内部类自动添加的字段this$0指向外部类实例。DepthReferences窗口的标签Depth 是指从 GC Root 到达这个实例的最短路径即这个实例的深度 (Depth):Shallow Size对象本身所占用的内存大小。包括对象头object header和基本类型字段如 int、boolean占用的内存。不包含该对象通过引用指向的其他对象所占用的内存。Retained Size对象支配的内存。如果一个对象A被回收那些只能通过它访问到的对象也将不可达即无法被任何GC Root访问到因此也一起被回收。对象A和只能通过它访问到的对象释放的内存总量就是对象A支配的内存。对象A的Retained Size 对象A的 Shallow Size 只能通过对象A可达的其他对象的 Shallow Size 之和。如上图所示如果对象B没有被外部引用那么对象A支配的内存就是对象ABC的内存之和。如果对象B被外部引用那么对象A支配的内存就只有对象A的内存本身。如果对象B被对象A支配只需要断开A对B的引用就可以回收对象B。下图中PhoneWindow对象的Retained Size比MemoryLeakActivity对象的小所以PhoneWindow对象的Retained Size PhoneWindow对象的Shallow Size MemoryLeakActivity对象的Retained Size。这说明PhoneWindow对象没有支配MemoryLeakActivity对象。也就是说即使断开了PhoneWindow对象对MemoryLeakActivity对象的引用也还有其它对象引用着MemoryLeakActivity对象无法回收MemoryLeakActivity对象。事实也的确如此分析PhoneWindow对象的引用链我们可以看到它最终被MemoryLeakActivity对象的匿名内部类对象即Runnable对象引用。从代码上也可以进一步确定MemoryLeakActivity和PhoneWindow是相互引用publicclassActivity{privateWindowmWindow;finalvoidattach(……){mWindownewPhoneWindow(this,window,activityConfigCallback);}}由此可见凡是Retained Size比MemoryLeakActivity对象小的对象都不是导致内存泄漏的元凶。值得注意的是即使某个对象不支配MemoryLeakActivity对象但是仍然可能这样该对象的Retained Size 该对象的Shallow Size MemoryLeakActivity对象的Retained Size。内存溢出从首页进入内存溢出页面publicclassLoadingSpinnerextendsView{OverrideprotectedvoidonDraw(Canvascanvas){super.onDraw(canvas);if(mRadius0)return;canvas.save();canvas.rotate(mCurrentRotation,mCenterX,mCenterY);floatpetalLengthmRadius*mPetalLengthRatio;floatpetalWidthmRadius*mPetalWidthRatio;floatinnerRadiusmRadius-petalLength;// 绘制每个花瓣for(inti0;imPetalCount;i){canvas.save();floatangle(360f/mPetalCount)*i;canvas.rotate(angle,mCenterX,mCenterY);// 计算透明度按顺序渐变营造旋转效果intalpha(int)(255*((i1f)/mPetalCount));mPaint.setAlpha(alpha);BitmappetalBmpBitmap.createBitmap((int)petalWidth,(int)petalLength,Bitmap.Config.ARGB_8888);CanvaspetalCanvasnewCanvas(petalBmp);petalCanvas.drawColor(mColor);canvas.drawBitmap(petalBmp,mCenterX-petalWidth/2,mCenterY-mRadius,mPaint);canvas.restore();}canvas.restore();}}排查如果不需要重复操作就引发了OutOfMemoryError那么有2种可能。创建了一个大内存的对象对象数量不会随着内存的增长而增长短时间内创建了大量对象对象数量会随着内存的增长而增长进入内存溢出页面后点击Android Studio Profiler的Track Memory Consumption(Java/Kotlin Allocations)。等待一会我们就能看到对象数量虚线表示对象数量随着内存增长而增长并且不断触发垃圾回收。选择Arrange by class将内存中的对象按照类名分组点击Allocations将分组按对象数量从大到小排列。点击其中一个类在Instances窗口中会显示出该类的全部对象选择其中一个对象在右侧会显示出创建该对象的函数调用栈。按照上述步骤研究创建对象数量较多的函数调用栈从中找出该应用内的函数即为优化目标。经过排查就可以发现Bitmap和Canvas对象都是由应用内LoadingSpinner.onDraw()方法创建结合函数调用栈中函数指定的行数就可以找到创建对象的地方然后优化即可。补充要点对象的分组捕获到内存中的对象可以按照下面三种方式分组Arrange by class按类名分组。Arrange by package按包名分组。Arrange by callstack按创建对象的调用堆栈分组。Arrange by packageArrange by callstack按照包名分组的排查方法和按照类名分组的排查方法差不多这里介绍下按照调用栈分组的排查方法。从上面Arrange by callstack的示例图中可以看出大量的函数都是在主线中创建的所以在主线程中逐层比较创建数量的大小。比如下图中dispatchMessage()和next()二者相比dispatchMessage()创建对象的数量较多所以优先排查dispatchMessage()。最终查出应用内LoadingSpinner.onDraw()方法创建大量的Bitmap和Canvas。无法显示函数调用栈在Instances窗口中某些对象无法显示出函数调用栈。这些对象有可能是在Memory Profiler录制之前就已经存在的对象这些对象的分配历史无法追溯所以没有调用栈信息。StringBuilder对象无法显示出函数调用栈Demo在本文顶部请自行下载参考资料Android Studio利用 Profiler 来监控 CPU、内存和网络使用 Android Studio Profiler 工具解析应用的内存和 CPU 使用数据AndroidStudioMemory Profiler的Shallow Size和Retained SizeAndroid Developer 分析应用性能