对新闻客户端APP的性能优化实战


对新闻客户端APP的性能优化

1. 解决启动白屏问题

当系统加载并启动 App 时,需要耗费相应的时间,这样会造成用户会感觉到当点击 App 图标时会有 “延迟” 现象, 为了解决这一问题,Google 的做法是在 App 创建的过程中,先展示一个空白页面,让用户体会到点击图标之后立 马就有响应。 如果你application或activity启动的过程太慢,导致系统的BackgroundWindow没有及时被替换,就会出现启动 时白屏或黑屏的情况(取决于Theme主题是Dark还是Light)。 消除启动时的黑/白屏问题,大部分App都采用自己在Theme中设置背景图的方式来解决。

在Style里面配置

<style name = "AppTheme.Luncher">
    <item name="android:windowBackground">@drwaable/windows_bg</item>
    <item name="android:windowFullscreen">true</item>
</style>
windows_bg
<layer-list xmlns:andrid=............>
    <item android:drawable="@android:color/white"/>
    <item>
        <bitmap
                android:gravity="center"
                android:scaleType="centercrop"
                android:src="@drawable/bg" />
    </item>
</layer-list>

在manifest.xml 中

........................
	<application
                ..............
                 ............
                 >
        <activity
                  android:name=".Mainactivity"
                  android:theme="@style/Apptheme.luncher">

在Activity的onCreate 改回原来的

public class onCreate() {
	setTheme(R.style.AppTheme);
}

解决了白屏问题, 启动APK 会加载一个新闻的图标

2. 启动耗时优化

在性能测试中存在启动时间2-5-8原则:

  • 当用户能够在2秒以内得到响应时,会感觉系统的响应很快;
  • 当用户在2-5秒之间得到响应时,会感觉系统的响应速度还可以;
  • 当用户在5-8秒以内得到响应时,会感觉系统的响应速度很慢,但是还可以接受;
  • 而当用户在超过8秒后仍然无法得到响应时,会感觉系统糟透了,或者认为系统已经失去响应。

​ 而Google 也提出一项计划:Android Vitals 。该计划旨在改善 Android 设备的稳定性和性能。当选择启用了该计 划的用户运行您的用时,其 Android 设备会记录各种指标,包括应用稳定性、应用启动时间、电池使用情况、呈 现时间和权限遭拒等方面的数据。Google Play 管理中心 会汇总这些数据,并将其显示在 Android Vitals 信息中心 内。 当应用启动时间过长时,Android Vitals 可以通过 Play 管理中心提醒您,从而帮助提升应用性能。

Android Vitals 在您的应用出现以下情况时将其启动时间视为过长:

  • 冷启动用了 5 秒或更长时间。
  • 温启动用了 2 秒或更长时间。
  • 热启动用了 1.5 秒或更长时间。

实际上不同的应用因为启动时需要初始化的数据不同,启动时间自然也会不同。相同的应用也会因为在不同的设 备,因为设备性能影响启动速度不同。所以实际上启动时间并没有绝对统一的标准,我们之所以需要进行启动耗时 的统计的,可能在于产品对我们应用启动时间提出具体的要求。

2.1 优化前

来看看优化之前启动耗时的最小值

adb shell am start -S -W packageName/LunchActivity

ThisTime : 3340 ms
TotalTime : 3340 ms
waitTime : 3413 ms
  • ​ WaitTime:总的耗时,包括前一个应用Activity pause的时间和新应用启动的时间;
  • ​ ThisTime表示一连串启动Activity的最后一个Activity的启动耗时;
  • ​ TotalTime表示新应用启动的耗时,包括新进程的启动和Activity的启动,但不包括前一个应用Activity pause 的耗时。

开发者一般只要关心TotalTime即可,这个时间才是自己应用真正启动的耗时。启动时间总花费了 3340 ms, 大概3 秒的样子,当用户在2-5秒之间得到响应,只能说感觉系统的响应速度还可以, 但能不能更快呢?能够在2秒以内得到响应时,会感觉系统的响应很快。

2.2 启动优化工具 CPU Profile/TraceView

​ 如果发现显示时间比希望的时间长,则可以继续尝试识别启动过程中的瓶颈。 查找瓶颈的一个好方法是使用 Android Studio CPU 性能剖析器。402 Traceview是android平台配备一个很好的性能分析的工具。它可以通过图形化的方式让我们了解我们要跟踪 的程序的性能,并且能具体到每个方法的执行时间。但是目前Traceview 已弃用。如果使用 Android Studio 3.2 或更高版本,则应改为使用 CPU Profiler 要在应用启动过程中自动开始记录 CPU 活动,请执行以下操作:

要在应用启动过程中自动开始记录 CPU 活动,请执行以下操作:

  1. 依次选择 Run > Edit Configurations。

  2. 在 Profiling 标签中,勾选 Start recording CPU activity on startup 旁边的复选框

  3. 从菜单中选择 CPU 记录配置。

    1. Sample Java Methods 对 Java 方法采样:在应用的 Java 代码执行期间,频繁捕获应用的调用堆栈。分析器会比较捕获的数据集, 以推导与应用的 Java 代码执行有关的时间和资源使用信息。如果应用在捕获调用堆栈后进入一个方法并在下 次捕获前退出该方法,分析器将不会记录该方法调用。如果您想要跟踪生命周期如此短的方法,应使用检测 跟踪。

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

    3. Sample C/C++ Functions 对 C/C++ 函数采样:捕获应用的原生线程的采样跟踪数据。要使用此配置,您必须将应用部署到搭载 Android 8.0(API 级别 26)或更高版本的设备上。

    4. Trace System Calls 跟踪系统调用:捕获非常翔实的细节,以便您检查应用与系统资源的交互情况。您可以检查线程状态的确切 时间和持续时间、直观地查看所有内核的 CPU 瓶颈在何处,并添加要分析的自定义跟踪事件。要使用此配 置,您必须将应用部署到搭载 Android 7.0(API 级别 24)或更高版本的设备上。 此跟踪配置在 systrace 的基础上构建而成。您可以使用 systrace 命令行实用程序指定除 CPU Profiler 提供的 选项之外的其他选项。systrace 提供的其他系统级数据可帮助您检查原生系统进程并排查丢帧或帧延迟问 题。

  4. 点击 Apply。

  5. 依次选择 Run > Profile,将您的应用部署到搭载 Android 8.0(API 级别 26)或更高版本的设备上

本次调查不通过cpu profile 的方式 trace java method 进行采样, 太卡, 而是通过debug API的方式进行采样处理

public class ArchDemoApplication extends Application {
    Debug.startMethodTracingSample(new File(Environment.getExternalStarageDirectory(), "enjoy").getAbsolutePath(),8*1024*1024,1_1000);
}

在 mainActivity 的 onWindowFocusChange(boolean hasFocus) 手指点app 可以响应,需要申请文件读取权限。

public class MyApplication extends Application {
	public MyApplication() {
		Debug.startMethodTracing("enjoy");
		}
		//.....
	}
public class MainActivity extends AppCompatActivity {
	@Override
	public void onWindowFocusChanged(boolean hasFocus) {
		super.onWindowFocusChanged(hasFocus);
		Debug.stopMethodTracing();
	}
	//.......
}

运行App,则会在sdcard中生成一个enjoy.trace文件(需要sdcard读写权限)。将手机中的trace文件保存至电 脑,随后拖入Android Studio即可。把生成的enjoy文件拖入到AS中

image-20220619202854534

Call Chart

​ 以图形来呈现方法跟踪数据或函数跟踪数据,其中调用的时间段和时间在横轴上表示,而其被调用方则在纵轴上显 示。对系统 API 的调用显示为橙色,对应用自有方法的调用显示为绿色,对第三方 API(包括 Java 语言 API)的调 用显示为蓝色。 (实际颜色显示有Bug)

image-20220619204623416

如上图,自定义Application的 onCreate 调用了 Thread.sleep 耗时为:3s。

Call Chart 已经比原数据可读性高很多,但它仍然不方便发现那些运行时间很长的代码,这时我们便需要使用 Flame Chart。

Flame Chart

提供一个倒置的调用图表,用来汇总完全相同的调用堆栈。也就是说,将具有相同调用方顺序的完全相同的方法或 函数收集起来,并在火焰图中将它们表示为一个较长的横条 。

横轴显示的是百分比数值。由于忽略了时间线信息,Flame Chart 可以展示每次调用消耗时间占用整个记录时长的 百分比。 同时纵轴也被对调了,在顶部展示的是被调用者,底部展示的是调用者。此时的图表看起来越往上越窄, 就好像火焰一样,因此得名: 火焰图。

Top Down Tree

如果我们需要更精确的时间信息,就需要使用 Top Down Tree。 Top Down Tree显示一个调用列表,在该列表中 展开方法或函数节点会显示它调用了的方法节点。

对于每个节点,三个时间信息:

  • Self Time —— 运行自己的代码所消耗的时间;
  • Children Time —— 调用其他方法的时间;
  • Total Time —— 前面两者时间之和。

Bottom Up Tree

方便地找到某个方法的调用栈。在该列表中展开方法或函数节点会显示哪个方法调用了自己。

通过工具可以定位到耗时代码,然后查看是否可以进行优化。对于APP启动来说,启动耗时包括Android系统启动 APP进程加上APP启动界面的耗时时长,我们可做的优化是APP启动界面的耗时,也就是说从Application的构建到 主界面的 onWindowFocusChanged 的这一段时间。 因此在这段时间内,我们的代码需要尽量避免耗时操作,检查的方向包括:主线程IO;第三方库初始化或程序需要 使用的数据等初始化改为异步加载/懒加载;减少布局复杂度与嵌套层级;Multidex(5.0以上无需考虑)等。

2.3 优化点 1 优化 SharedPreference

image-20220619155148739

SharedPreference 涉及到 在子线程进行XML读取, SP 的定位就是保存简单轻量级的 key value 数据, 虽然在子线程读取, 但如果主线程需要读取结果, 那就也需要对 sp 进行优化。

2.4 布局异步加载

LayoutInflater加载xml布局的过程会在主线程使用IO读取XML布局文件进行XML解析,再根据解析结果利用反射 创建布局中的View/ViewGroup对象。这个过程随着布局的复杂度上升,耗时自然也会随之增大。Android为我们 提供了 Asynclayoutinflater 把耗时的加载操作在异步线程中完成,最后把加载结果再回调给主线程。

dependencies {
implementation "androidx.asynclayoutinflater:asynclayoutinflater:1.0.0"
}
new AsyncLayoutInflater(this)
.inflate(R.layout.activity_main, null, new AsyncLayoutInflater.OnInflateFinishedListener() {
	@Override
	public void onInflateFinished(@NonNull View view, int resid, @Nullable ViewGroup parent) {
		setContentView(view);
		//......
	}
});

1、使用异步 inflate,那么需要这个 layout 的 parent 的 generateLayoutParams 函数是线程安全的;

2、所有构建的 View 中必须不能创建 Handler 或者是调用 Looper.myLooper;(因为是在异步线程中加载的,异 步线程默认没有调用 Looper.prepare );

3、AsyncLayoutInflater 不支持设置 LayoutInflater.Factory 或者 LayoutInflater.Factory2;

4、不支持加载包含 Fragment 的 layout

5、如果 AsyncLayoutInflater 失败,那么会自动回退到UI线程来加载布局;

2.5 优化点2 新闻数据缓存 异步加载

在fragment的onCreate的时候读取了缓存的数据和 SP, 占用了一定的时间,改为了通过IdleHandler 进行空闲时间异步加载

public class headLineNewsViewModel extend MvvmBaseViewModel<ChannelsModel, ChannelsModel.Channel> {
	public HeadLineNowsViewModel() {
		model = new ChannelsModel();
		model.register(this);
		
		looper.myQueue().addIdleHandler(new MessageQueue.IdeleHandler() {
			@Override
			public boolean queueIdle() {
				model.getCacheDataAndload();
				return false;
			}
		});
	}
}

2.6 代码优化

在 DispatchMessage的 onChange 中耗费了 700 多的毫秒数, 有两处调用了notifyDataSetChange, 如图

image-20220619153108667

删除其中一个可以优化 快300 ms

再启动 一次 看看启动时间, 优化了大约 500 ms

ThisTime 2801
TotalTime 2801
WaitTime 3457

3.6 布局优化

​ measure、layout、draw这三个过程都包含自顶向下的View Tree遍历耗时,如果视图层级太深自然需要更多的时 间来完成整个绘测

过程,从而造成启动速度慢、卡顿等问题。而onDraw在频繁刷新时可能多次出发,因此 onDraw更不能做耗时操作,同时需要注意内存

抖动。对于布局性能的检测,依然可以使用systrace与traceview按 照绘制流程检查绘制耗时函数。

image-20220619230236944

在左侧id为content之下的就是我们写在XML中的布局。可以明显看出,我们的布局中是一个 LinearLayout ,其中 又包含两个 LinearLayout 。我们应该尽量减少其层级,可以使用ConstraintLayout 约束布局使得布局尽量扁平 化,移除非必需的UI组件。

在SetContentView 加载布局的时候可不可进行优化呢,加快加载时间。

image-20220619153736460

Tools -> LayoutInspector

image-20220619163037744

在ManiActiviy的布局中,上下两个都是调用的系统布局,主要看看我们自定义的 Framelayout 里面有没有可以优化的点

Framelayout主要是被 headlineNewsFragment 索替换, 所以重点看依稀 headlineNewsFragment的布局可不可以被优化

3.6.1 优化点1 viewPage懒加载

viewPager 替换为 ViewPage2

<androidx.viewpager2.widget.ViewPager2 />

ViewPager2默认就实现了懒加载。 但是如果想避免Fragment频繁销毁和创建造成的开销,可以通过 setOffscreenPageLimit () 方法设置预加载数量,将数据加载逻辑放到Fragment的 onResume () 方法中。

viewDataBinding.viewpager2.setOffscreenPageLimit(1)

3.6.2 优化点2 优化 TitleView

image-20220619164454724

TItleView里面嵌套了一层LinearLayout, 为什么呢? 进去看一下, 能不能优化掉这层LinearLayout

image-20220619164709373

​ 因为 TitleView 需要在 RecycleView 里面加了一个分割线,为了方便,直接在xml中添加了分割线 , 那我们可以去掉这个分割线,仅留下一个TextView,不就可以去掉外面一层的LinearLayout 了吗, 把分割线在代码中实现:

public voic onCreate() {
    ..............
    viewDataBinding.listView.addItemDecoration(new RecycleViewDivider(gerContext(), LinearLayoutManager.HORIZONNTAL))l
}

3.6.3 PictureTitleLayout 优化

image-20220619170855337

使用Merge标签

当我们有一些布局元素需要被多处使用时,这时候我们会考虑将其抽取成一个单独的布局文件。在需要使用的地方 通过 include 加载。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
	android:layout_width="match_parent"
	android:layout_height="match_parent"
	android:background="#000000"
	android:orientation="vertical">
	<!-- include layout_merge布局 -->
	<include layout="@layout/layout_merge" />
</LinearLayout>

<!-- layout_merge -->
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
	android:layout_width="wrap_content"
	android:layout_height="wrap_content"
	android:orientation="vertical">
    
	<TextView
		android:background="#ffffff"
		android:layout_width="wrap_content"
		android:layout_height="wrap_content"
		android:text="测试merge" />
</LinearLayout>

这时候我们的主布局文件是垂直的LinearLayout,include的 “layout_merge” 也是垂直的LinearLayout,这时候 include的布局中使用的LinearLayout就没意义了,使用的话反而减慢你的UI表现。这时可以使用merge标签优 化。

<!-- layout_merge -->
<merge xmlns:android="http://schemas.android.com/apk/res/android">
	<TextView
		android:background="#ffffff"
		android:layout_width="wrap_content"
		android:layout_height="wrap_content"
		android:text="测试merge" />
</merge>

修改为merge后,通过LayoutInspector能够发现,include的布局中TextView直接被加入到父布局中。

image-20220619231515654

使用ViewStub 标签

当我们布局中存在一个View/ViewGroup,在某个特定时刻才需要他的展示时,可能会有同学把这个元素在xml中 定义为invisible或者gone,在需要显示时再设置为visible可见。比如在登陆时,如果密码错误在密码输入框上显示 提示。

  • invisible view设置为invisible时,view在layout布局文件中会占用位置,但是view为不可见,该view还是会创建对 象,会被初始化,会占用资源。
  • gone view设置gone时,view在layout布局文件中不占用位置,但是该view还是会创建对象,会被初始化,会占 用资源。

如果view不一定会显示,此时可以使用 ViewStub 来包裹此View 以避免不需要显示view但是又需要加载view消耗资 源。 viewstub是一个轻量级的view,它不可见,不用占用资源,只有设置viewstub为visible或者调用其inflater()方法 时,其对应的布局文件才会被初始化。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
	android:layout_width="match_parent"
	android:layout_height="match_parent"
	android:background="#000000"
	android:orientation="vertical">
	<ViewStub
		android:id="@+id/viewStub"
		android:layout_width="600dp"
		android:layout_height="500dp"
		android:inflatedId="@+id/textView"
		android:layout="@layout/layout_viewstub" />
</LinearLayout>
	<!-- layout_viewstub -->
	<?xml version="1.0" encoding="utf-8"?>
	<TextView xmlns:android="http://schemas.android.com/apk/res/android"
		android:layout_width="wrap_content"
		android:layout_height="wrap_content"
		android:background="#ffffff"
		android:text="测试viewStub" />

加载viewStub后,可以通过 inflatedId 找到layout_viewstub 中的根View。

再看一下现在的耗时,又优化了 600 ms

ThisTime 2202
TotalTime 2320
waitTime 3045

总体的 启动优化大概优化了快 1 S 的样子, 虽然没有优化到2 S 以内, 但也对启动的性能有了很大的提高。

3. 卡顿优化

大多数用户感知到的卡顿等性能问题的最主要根源都是因为渲染性能。Android系统每隔大概16.6ms发出VSYNC信 号,触发对UI进行渲染,如果每次渲染都成功,这样就能够达到流畅的画面所需要的60fps,为了能够实现60fps, 这意味着程序的大多数操作都必须在16ms内完成。开发app的性能目标就是保持60fps,这意味着每一帧你只有16ms=1000/60的时间来处理所有的任务。

image-20220619212237823

如果某个操作花费时间是24ms,系统在得到VSYNC信号的时候就无法进行正常渲染,这样就发生了丢帧现象。那 么用户在32ms内看到的会是同一帧画面。

image-20220619212325906

有很多原因可以导致丢帧, 一般主线程过多的UI绘制、大量的IO操作或是大量的计算操作占用CPU,都会导致App 界面卡顿。 一般主线程过多的UI绘制、大量的IO操作或是大量的计算操作占用CPU,导致App界面卡顿.

建议阅读:android图形显示系统

3.1 Systrace 卡顿分析

​ Systrace 是Android平台提供的一款工具,用于记录短期内的设备活动。该工具会生成一份报告,其中汇总了 Android 内核中的数据,例如 CPU 调度程序、磁盘活动和应用线程。Systrace主要用来分析绘制性能方面的问 题。在发生卡顿时,通过这份报告可以知道当前整个系统所处的状态,从而帮助开发者更直观的分析系统瓶颈,改 进性能。 TraceView可以看出代码在运行时的一些具体信息,方法调用时长,次数,时间比率,了解代码运行过程的 效率问题,从而针对性改善代码。所以对于可能导致卡顿的耗时方法也可以通过TraceView检测。 要使用Systrace,需要先安装 Python2.7。安装完成后配置环境变量 path ,随后在命令行输入: python — version 进行验证。

Systrace具体使用

执行systrace可以选择配置自己感兴趣的category,常用的有:

标签 描述
gfx Graphics 图形系统,包括SerfaceFlinger,VSYNC消息,Texture,RenderThread等
input Input输入系统,按键或者触摸屏输入;分析滑动卡顿等
view View绘制系统的相关信息,比如onMeasure,onLayout等;分析View绘制性能
am ActivityManager调用的相关信息;分析Activity的启动、跳转
dalvik 虚拟机相关信息;分析虚拟机行为,如 GC停顿
sched CPU调度的信息,能看到CPU在每个时间段在运行什么线程,线程调度情况,锁信息。
disk IO信息
wm WindowManager的相关信息
res 资源加载的相关信息

其实Systrace对于应用开发者来说,能看的并不多。主要用于看是否丢帧,与丢帧时系统以及我们应用大致的一个 状态。我们在抓取systrace文件的时候,切记不要抓取太长时间,也不要太多不同操作。

python systrace.py -t 5 -o F:/Lance/optimizer/lsn2_jank/a.html gfx input view am dalvik sched wm disk res -a PackageName

Android 系统来说 绘制一帧 在16.6 左右 1000/60

打开抓取的html文件,可以看到我们应用存在非常严重的掉帧,不借助工具直接用肉眼看应用UI是看不出来的。如 果只是单独存在一个红色或者黄色的都是没关系的。关键是连续的红/黄色或者两帧间隔非常大那就需要我们去仔 细观察。按“W” 放大视图,在UIThread(主线程)上面有一条很细的线,表示线程状态。

image-20220619183602657

Systrace 会用不同的颜色来标识不同的线程状态, 在每个方法上面都会有对应的线程状态来标识目前线程所处的状 态。通过查看线程状态我们可以知道目前的瓶颈是什么, 是 CPU 执行慢还是因为 Binder 调用, 又或是进行 IO 操作, 又或是拿不到 CPU 时间片。 通过查看线程状态我们可以知道目前的瓶颈是什么, 是 CPU 执行慢还是因为 Binder 调用, 又或是进行 IO 操作, 又或是拿不到 CPU 时间片

线程状态主要有下面几个:

  • 绿色:表示正在运行。
  • 蓝色:表示可以运行,但是CPU在执行其他线程; 是否后台有太多的任务在跑?Runnable 状态的线程状态持续时间越长,则表示 cpu 的调度越忙,没有 及时处理到这个任务 没有及时处理是因为频率太低?
  • 紫色:可中断的休眠 线程在遇到另一项内核操作(通常是内存管理)时被阻止。 但是实际从Android 9模拟器中拉取数据,遇到IO显示紫色,没有橙色状态显示。
  • 橙色:不可中断的休眠 线程在遇到 I/O 操作时被阻止或正在等待磁盘操作完成。

image-20220619184827477

在这里发现一个卡断的问题之一, ext4_write 一直在做文件操作,原因是RecycleView在滑动的过程中,又在做文件操作(我自己加的),造成了滑动卡顿,删除后就不再卡顿了。

3. 2 Trace API

其实对于APP开发而言,使用systrace的帮助并不算非常大,大部分内容用于设备真机优化之类的系统开发人员观 察。systrace无法帮助应用开发者定位到准确的错误代码位置,我们需要凭借很多零碎的知识点与经验来猜测问题 原因。如果我们有了大概怀疑的具体的代码块或者有想了解的代码块执行时系统的状态,还可以结合 Trace API 打 标签。

Android 提供了Trace API能够帮助我们记录收集自己应用中的一些信息 : Trace.beginSection() 与 Trace.endSection();

public class MainActivity extends AppCompatActivity {
	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		TraceCompat.beginSection("enjoy_launcher"); //Trace.beginSection()
		setContentView(R.layout.activity_main);
		TraceCompat.endSection(); //Trace.endSection()
	}
}

3.3 App层面监控卡顿

systrace可以让我们了解应用所处的状态,了解应用因为什么原因导致的。若需要准确分析卡顿发生在什么函数, 资源占用情况如何,目前业界两种主流有效的app监控方式如下:

1、 利用UI线程的Looper打印的日志匹配;

2、 使用Choreographer.FrameCallback

Looper日志检测卡顿

Android主线程更新UI。如果界面1秒钟刷新少于60次,即FPS小于60,用户就会产生卡顿感觉。简单来说, Android使用消息机制进行UI更新,UI线程有个Looper,在其loop方法中会不断取出message,调用其绑定的 Handler在UI线程执行。如果在handler的dispatchMesaage方法里有耗时操作,就会发生卡顿。

public static void loop() {
	//......
	for (;;) {
	//......
	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);
		}
		//......
	}
}

只要检测 msg.target.dispatchMessage(msg) 的执行时间,就能检测到部分UI线程是否有耗时的操作。注意到这行 执行代码的前后,有两个logging.println函数,如果设置了logging,会分别打印出>>>>> Dispatching to和 <<<<< Finished to 这样的日志,这样我们就可以通过两次log的时间差值,来计算dispatchMessage的执行时 间,从而设置阈值判断是否发生了卡顿。

public final class Looper {
	private Printer mLogging;
	public void setMessageLogging(@Nullable Printer printer) {
		mLogging = printer;
	}
}
public interface Printer {
	void println(String x);
}

Looper 提供了 setMessageLogging(@Nullable Printer printer) 方法,所以我们可以自己实现一个Printer,在 通过setMessageLogging()方法传入即可:

public class BlockCanary {
	public static void install() {
		LogMonitor logMonitor = new LogMonitor();
		Looper.getMainLooper().setMessageLogging(logMonitor);
	}
}
public class LogMonitor implements Printer {
	private StackSampler mStackSampler;
	private boolean mPrintingStarted = false;
	private long mStartTimestamp;
	// 卡顿阈值
	private long mBlockThresholdMillis = 3000;
	//采样频率
	private long mSampleInterval = 1000;
	private Handler mLogHandler;
	public LogMonitor() {
		mStackSampler = new StackSampler(mSampleInterval);
		HandlerThread handlerThread = new HandlerThread("block-canary-io");
		handlerThread.start();
		mLogHandler = new Handler(handlerThread.getLooper());
}
	@Override
	public void println(String x) {
	//从if到else会执行 dispatchMessage,如果执行耗时超过阈值,输出卡顿信息
	if (!mPrintingStarted) {
		//记录开始时间
		mStartTimestamp = System.currentTimeMillis();
		mPrintingStarted = true;
		mStackSampler.startDump();
	} else {
		final long endTime = System.currentTimeMillis();
		mPrintingStarted = false;
	//出现卡顿
	if (isBlock(endTime)) {
		notifyBlockEvent(endTime);
	}
	mStackSampler.stopDump();
	}
}
private void notifyBlockEvent(final long endTime) {
	mLogHandler.post(new Runnable() {
	@Override
	public void run() {
	//获得卡顿时 主线程堆栈
		List<String> stacks = mStackSampler.getStacks(mStartTimestamp, endTime);
	for (String stack : stacks) {
		Log.e("block-canary", stack);
			}
		}
	});
}
private boolean isBlock(long endTime) {
		return endTime - mStartTimestamp > mBlockThresholdMillis;
	}
}
	public class StackSampler {
	public static final String SEPARATOR = "/r/n";
	public static final SimpleDateFormat TIME_FORMATTER =
	new SimpleDateFormat("MM-dd HH:mm:ss.SSS");
	private Handler mHandler;
	private Map<Long, String> mStackMap = new LinkedHashMap<>();
	private int mMaxCount = 100;
	private long mSampleInterval;
	//是否需要采样
	protected AtomicBoolean mShouldSample = new AtomicBoolean(false);
	public StackSampler(long sampleInterval) {
		mSampleInterval = sampleInterval;
		HandlerThread handlerThread = new HandlerThread("block-canary-sampler");
		handlerThread.start();
		mHandler = new Handler(handlerThread.getLooper());
	}
/**
* 开始采样 执行堆栈
*/
public void startDump() {
//避免重复开始
	if (mShouldSample.get()) {
	return;
}
	mShouldSample.set(true);
	mHandler.removeCallbacks(mRunnable);
	mHandler.postDelayed(mRunnable, mSampleInterval);
}
public void stopDump() {
	if (!mShouldSample.get()) {
	return;

	其实这种方式也就是 BlockCanary 原理。
	Choreographer.FrameCallback
}
	mShouldSample.set(false);
	mHandler.removeCallbacks(mRunnable);
}
	public List<String> getStacks(long startTime, long endTime) {
	ArrayList<String> result = new ArrayList<>();
	synchronized (mStackMap) {
		for (Long entryTime : mStackMap.keySet()) {
			if (startTime < entryTime && entryTime < endTime) {
			result.add(TIME_FORMATTER.format(entryTime)
													+ SEPARATOR
														+ SEPARATOR
														+ mStackMap.get(entryTime));
			}
		}
	}
	return result;
}
private Runnable mRunnable = new Runnable() {
@Override
	public void run() {
		StringBuilder sb = new StringBuilder();
	StackTraceElement[] stackTrace = Looper.getMainLooper().getThread().getStackTrace();
	for (StackTraceElement s : stackTrace) {
		sb.append(s.toString()).append("/n");
	}
	synchronized (mStackMap) {
	//最多保存100条堆栈信息
		if (mStackMap.size() == mMaxCount) {
			mStackMap.remove(mStackMap.keySet().iterator().next());
		}
		mStackMap.put(System.currentTimeMillis(), sb.toString());
	}
	if (mShouldSample.get()) {
		mHandler.postDelayed(mRunnable, mSampleInterval);
			}
		}
	};
}

Choreographer.FrameCallback

Android系统每隔16ms发出VSYNC信号,来通知界面进行重绘、渲染,每一次同步的周期约为16.6ms,代表一帧 的刷新频率。通过Choreographer类设置它的FrameCallback函数,当每一帧被渲染时会触发回调 FrameCallback.doFrame (long frameTimeNanos) 函数。frameTimeNanos是底层VSYNC信号到达的时间戳 。

public class ChoreographerHelper {
	public static void start() {
		if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
			Choreographer.getInstance().postFrameCallback(new 								Choreographer.FrameCallback() {
				long lastFrameTimeNanos = 0;
				@Override
				public void doFrame(long frameTimeNanos) {
					//上次回调时间
					if (lastFrameTimeNanos == 0) {
					lastFrameTimeNanos = frameTimeNanos;
					Choreographer.getInstance().postFrameCallback(this);
					return;
				}
				long diff = (frameTimeNanos - lastFrameTimeNanos) / 1_000_000;
				if (diff > 16.6f) {
					//掉帧数
					int droppedCount = (int) (diff / 16.6);
				}
					lastFrameTimeNanos = frameTimeNanos;
					Choreographer.getInstance().postFrameCallback(this);
				}
			});
		}
	}
}

通过 ChoreographerHelper 可以实时计算帧率和掉帧数,实时监测App页面的帧率数据,发现帧率过低,还可以自 动保存现场堆栈信息。

Looper比较适合在发布前进行测试或者小范围灰度测试然后定位问题,ChoreographerHelper适合监控线上环境 的 app 的掉帧情况来计算 app 在某些场景的流畅度然后有针对性的做性能优化。

4. 过度渲染

过度绘制是指系统在渲染单个帧的过程中多次在屏幕上绘制某一个像素。例如,如果我们有若干界面卡片堆叠在一 起,每张卡片都会遮盖其下面一张卡片的部分内容。但是,系统仍然需要绘制堆叠中的卡片被遮盖的部分。

GPU 过度绘制检查 手机开发者选项中能够显示过度渲染检查功能,通过对界面进行彩色编码来帮我们识别过度绘制。开启步骤如下:

  1. 进入开发者选项 (Developer Options)。
  2. 找到调试 GPU 过度绘制(Debug GPU overdraw)。
  3. 在弹出的对话框中,选择显示过度绘制区域(Show overdraw areas)。

Android 将按如下方式为界面元素着色,以确定过度绘制的次数:

  1. 真彩色:没有过度绘制
  2. 蓝色:过度绘制 1 次
  3. 绿色:过度绘制 2 次 享学课堂
  4. 粉色:过度绘制 3 次
  5. 红色:过度绘制 4 次或更多次

解决过度绘制问题 可以采取以下几种策略来减少甚至消除过度绘制:

  • 移除布局中不需要的背景。 默认情况下,布局没有背景,这表示布局本身不会直接渲染任何内容。但是,当布局具有背景时,其有 可能会导致过度绘制。 移除不必要的背景可以快速提高渲染性能。不必要的背景可能永远不可见,因为它会被应用在该视图上 绘制的任何其他内容完全覆盖。例如,当系统在父视图上绘制子视图时,可能会完全覆盖父视图的背 景。
  • 使视图层次结构扁平化。 可以通过优化视图层次结构来减少重叠界面对象的数量,从而提高性能。 降低透明度。 对于不透明的 view ,只需要渲染一次即可把它显示出来。但是如果这个 view 设置了 alpha 值,则至 少需要渲染两次。这是因为使用了 alpha 的 view 需要先知道混合 view 的下一层元素是什么,然后再 结合上层的 view 进行Blend混色处理。透明动画、淡入淡出和阴影等效果都涉及到某种透明度,这就会 造成了过度绘制。可以通过减少要渲染的透明对象的数量,来改善这些情况下的过度绘制。例如,如需 获得灰色文本,可以在 TextView 中绘制黑色文本,再为其设置半透明的透明度值。但是,简单地通过 用灰色绘制文本也能获得同样的效果,而且能够大幅提升性能

原创文章,作者:bd101bd101,如若转载,请注明出处:https://blog.ytso.com/273247.html

(0)
上一篇 2022年7月10日
下一篇 2022年7月10日

相关推荐

发表回复

登录后才能评论