【list类源码】【医疗监护系统源码】【java逆向出源码】screenrecord 源码

1.linux下关于adb命令的一些知识
2.android开发设置屏蔽录制
3.Android性能优化:定性和定位Android图形性能问题——以后台录屏进程为例

screenrecord 源码

linux下关于adb命令的一些知识

       Android Debug Bridge,一般简称为adb,主要存放在sdk安装目录下的platform-tools文件夹中,它是一个非常强大的命令行工具,通过这个工具你能够与你的android设备进行交互。同时这是一个包含了以下三部分的cs模式的程序:

       1) adb client

       ã€€ã€€ä»Žå›¾ä¸­ï¼Œæˆ‘们知道client是运行在PC端的,每当我们发起一个adb命令的时候,就会开启一个client程序。当然,当我们开启DDMS或者ADT的时候,也会自动创建client。

       ã€€ã€€å½“我们开启一个client的时候,它首先会去检测后台是否已经有一个server程序在运行着,否则会开启一个adb-server进程。

       ã€€ã€€æ‰€æœ‰çš„client都是通过端口与adb-server进行通信的。

       2)adb daemon(adbd)

       ã€€ã€€ä»Žå›¾ä¸­ï¼Œæˆ‘们知道daemon是作为一个后台进程运行在模拟器/真实Android设备中的。

       ã€€ã€€daemon使用端口的范围是-,每个模拟器/设备连接到PC端时,总会开启这么一个后台进程,并且为其分配了两个连续的端口,比如:

       ã€€ã€€ã€€ã€€Emulator 1,源码 console:

       ã€€ã€€ã€€ã€€Emulator 1, adb:

       ã€€ã€€ä¹Ÿæ­£å› ä¸ºæ¯ä¸ªè®¾å¤‡éƒ½åˆ†ä¸€ç»„两个端口,也已adb连接手机的最大数量为。

       ã€€ã€€è¯´å›žç«¯å£çš„作用,在这两个端口中,其中偶数端口是用于server 与设备进行交互的,可以让server直接从设备中读取数据,而奇数端口是用来与设备的adbd进行连接通信的。

       2) adb server

       ã€€ã€€ä»Žå›¾ä¸­ï¼Œæˆ‘们同样可以知道,server也是作为一个后台的程序运行在PC端的,他负责管理client进程以及adb daemon之间的通信。

       ã€€ã€€å½“一个server开启的时候,他会自动绑定并且监听端口,接收client通过该端口发送过来的命令。同时server还会对-间的奇数端口进行扫描,进行对已连接设备的定位。

        1.2 常用的adb命令

       é¦–先我们先来介绍一下,adb命令的使用格式吧:

       adb [-d|-e|-s ]

       adb是必须的,接下来方括号[]里面的内容不是必须的,最后才是我们需要执行的命令操作,例如 adb -s emulator- install UCBrowser.apk (这是安装UC浏览器的意思)

       ä¸‹é¢å…ˆä»‹ç»ä¸‹ï¼Œå‰é¢å‡ ä¸ªå‚数的含义吧:

       ã€€ã€€-d: 让唯一连接到该PC端的真实安卓设备执行命令,如果发现USB中连接有多部设备,将会报错

       ã€€ã€€-e: 让唯一连接到该PC端的模拟器执行命令,如果发现开启了多个模拟器,将会报错

       ã€€ã€€-s:通过设备的序列号进行指定设备执行命令

       å¦‚果设备只连接有一个设备或者一个模拟器的时候,可以不用声明这三个参数,adb默认会让这部唯一连接到的设备进行命令执行。

       ok,下面开始讲解常用的命令吧:

       1) 查看当前PC端连接有多少设备:

       adb devices

       ç»ˆç«¯ä¸­çš„执行结果为:

       ä»Žä¸Šå›¾ä¸­ï¼Œæˆ‘们看到了设备有的两种状态,其实执行该命令,可能返回的状态有三种:

       ã€€ã€€I. device 设备已经成功连接到了adb-server

       ã€€ II. offline 设备并没有连接到adb或者没有响应

       ã€€ III. no device 并没有设备/模拟器连接

        2) 查看adb的版本

       adb version

       ç»ˆç«¯ä¸­çš„执行结果为:

       3) 给设备进行软件的安装

       adb -s install

       eg:

       adb -s eba9 install D://Test.apk

       ç»ˆç«¯ä¸­çš„执行结果为:

       é™¤äº†ä¸Šé¢è¿™ç§æ–¹æ³•ï¼Œæˆ‘们还有另外一个命令:

       adb -s shell pm install [options]

       å…¶ä¸­[options]中最常用的选项是-r,代表着本次安装是重新安装,会保留用户数据。

       4) 卸载设备中已经安装的软件

       adb -s uninstall

       eg:

       adb -s eba9 uninstall cn.uc.test

       ç»ˆç«¯ä¸­çš„执行结果为:

        除了上面这种方法,我们还有另外一个命令:

       adb -s uninstall [options]

       è¿™é‡Œçš„options是-k,加上该参数表明卸载软件后依旧保留用户数据

       5) 将数据从设备复制到PC中

       adb -s pull

       eg:

       adb -s eba9 pull /sdcard/stericson-ls D://

        终端的执行结果:

        在D://盘中我们也可以发现stericson-ls文件

       6) 讲数据从PC端复制到设备中

       adb -s push

       eg:

       adb -s eba9 push d://stericson-ls /sdcard/

       ç»ˆç«¯çš„执行结果:

       7) 获取连接到的手机的序列号

       adb get-serialno

       ç»ˆç«¯çš„执行结果:

       8)获取设备的连接状态

       adb -s get-state

       eg:

       adb -s eba9 get-stat

       ç»ˆç«¯çš„执行结果:

       9) 等待设备连接成功

       adb wait-for-device

       è¯¥å‘½ä»¤å°†ä¼šé˜»å¡žä¸€ç›´åˆ°é»˜è®¤è¿žæŽ¥çš„那部机器状态变为device,也就是连接成功。

       ) adb-server 的开启与关闭

       adb kill-server

       adb start-server

       ç»ˆç«¯çš„执行结果:

       æœ‰æ—¶å€™æˆ‘们开启server会失败,这是因为我们装了的手机助手,豌豆荚或者其他软件自带有自己的adb,导致端口冲突开启失败。主要关掉那些软件并且杀掉那些软件自带的adb进程重来即可。

       1.2 手机执行Shell命令

       ä¼—所周知,Android的内核本身就是Linux,所以Android本身也支持Linux命令,但是因为是手机系统,所以进行了一定的删减,部分命令不支持,而不支持的部分,我们可以通过安装busybox这个软件进行

       å¡«è¡¥ã€‚

       ä¸‹é¢æˆ‘们讲解下如何在手机中执行Shell命令:

        1) 每次都加上adb shell来进行命令的发送与执行

       adb shell

       2) 先通过adb shell进入手机Shell执行状态,然后直接输入Linux命令,如图:

       ã€€ã€€

       1.3 Activity Manager 常用命令与参数说明

        一般情况下,我们最常用它来进行app的开启,并且传递一定的参数过去,比如:

       adb shell am start -a android.intent.action.VIEW -n cn.uc.test/.MainActivity -d

       åœ¨è¿™é‡Œå°±ä¸åšå¤ªè¿‡è¯¦ç»†å’Œé‡å¤çš„说明了

       1.4 Package Manager 常用命令与参数说明

       1) 使一个apk处于禁用状态,相当于卸载了:

       disable

       å…¶ä¸­ PACKAGE_OR_COMPONENT的格式是 package/class, 如 com.UCMobile/.main.UCMobile

       eg:

       adb shell su disable com.UCMobile/.main.UCMobile

       ç»ˆç«¯æ‰§è¡Œç»“果:

       åŒæ—¶æˆ‘们在手机上发现,UC浏览器的图标不见了

       2) 让一个apk从禁用状态恢复过来

       enable

       å…¶ä¸­ PACKAGE_OR_COMPONENT的格式是 package/class, 如 com.UCMobile/.main.UCMobile

       eg:

       adb shell su enable com.UCMobile/.main.UCMobile

       ç»ˆç«¯æ‰§è¡Œç»“果:

       åŒæ—¶æˆ‘们从手机上发现,UC浏览器的图标又回来啦。

       3) 查看一个apk所在的路径

       pm path

       eg:

       adb shell pm path com.UCMobile

       ç»ˆç«¯æ‰§è¡Œç»“果:

       4) 查看手机中装的所有的包

       pm list packages

       å†æ¬¡ä¸åšè¿‡å¤šçš„说明

       1.5其他常用的adb命令

       1) 屏幕录像

       ä¾‹å¦‚,屏幕开始录像并且储存到/sdcard中,同时名字为demo.mp4

       adb shell screenrecord /sdcard/demo.mp4

       2) 关闭设备请求,开启设备

       adb shell stop

       adb shell start

       3)设备关机以及重启

       adb reboot

       adb shutdown

android开发设置屏蔽录制

       é¡¹ç›®å¼€å‘中,为了用户信息的安全,会有禁止页面被截屏、录屏的需求。

       è¿™ç±»èµ„料,在网上有很多,一般都是通过设置Activity的Flag解决,如:

       //禁止页面被截屏、录屏getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE);

       è¿™ç§è®¾ç½®å¯è§£å†³ä¸€èˆ¬çš„防截屏、录屏的需求。

       å¦‚果页面中有弹出Popupwindow,在录屏视频中的效果是:

       éžPopupwindow区域为黑色

       ä½†Popupwindow区域仍然是可以看到的

       å¦‚下面两张Gif图所示:

       æœªè®¾ç½®FLAG_SECURE,录屏的效果,如下图(git图片中间的水印忽略):

       è®¾ç½®äº†FLAG_SECURE之后,录屏的效果,如下图(git图片中间的水印忽略):

       åŽŸå› åˆ†æž

       çœ‹åˆ°äº†ä¸Šé¢çš„效果,我们可能会有疑问PopupWindow不像Dialog有自己的window对象,而是使用WindowManager.addView方法将View显示在Activity窗体上的。那么,Activity已经设置了FLAG_SECURE,为什么录屏时还能看到PopupWindow?

       æˆ‘们先通过getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE);来分析下源码:

       1、Window.java

       //window布局参数private final WindowManager.LayoutParams mWindowAttributes =        new WindowManager.LayoutParams();//添加标识public void addFlags(int flags) {

       setFlags(flags, flags);

       }//通过mWindowAttributes设置标识public void setFlags(int flags, int mask) {        final WindowManager.LayoutParams attrs = getAttributes();

       attrs.flags = (attrs.flags&~mask) | (flags&mask);

       mForcedWindowFlags |= mask;

       dispatchWindowAttributesChanged(attrs);

       }//获得布局参数对象,即mWindowAttributespublic final WindowManager.LayoutParams getAttributes() {        return mWindowAttributes;

       }

       é€šè¿‡æºç å¯ä»¥çœ‹åˆ°ï¼Œè®¾ç½®window属性的源码非常简单,即:通过window里的布局参数对象mWindowAttributes设置标识即可。

       2、PopupWindow.java

       //显示PopupWindowpublic void showAtLocation(View parent, int gravity, int x, int y) {

       mParentRootView = new WeakReference<>(parent.getRootView());

       showAtLocation(parent.getWindowToken(), gravity, x, y);

       }//显示PopupWindowpublic void showAtLocation(IBinder token, int gravity, int x, int y) {        if (isShowing() || mContentView == null) {            return;

       }

       TransitionManager.endTransitions(mDecorView);

       detachFromAnchor();

       mIsShowing = true;

       mIsDropdown = false;

       mGravity = gravity;

       //创建Window布局参数对象

       final WindowManager.LayoutParams p =createPopupLayoutParams(token);

       preparePopup(p);

       p.x = x;

       p.y = y;

       invokePopup(p);

       }//创建Window布局参数对象protected final WindowManager.LayoutParams createPopupLayoutParams(IBinder token) {        final WindowManager.LayoutParams p = new WindowManager.LayoutParams();

       p.gravity = computeGravity();

       p.flags = computeFlags(p.flags);

       p.type = mWindowLayoutType;

       p.token = token;

       p.softInputMode = mSoftInputMode;

       p.windowAnimations = computeAnimationResource();        if (mBackground != null) {

       p.format = mBackground.getOpacity();

       } else {

       p.format = PixelFormat.TRANSLUCENT;

       }        if (mHeightMode < 0) {

       p.height = mLastHeight = mHeightMode;

       } else {

       p.height = mLastHeight = mHeight;

       }        if (mWidthMode < 0) {

       p.width = mLastWidth = mWidthMode;

       } else {

       p.width = mLastWidth = mWidth;

       }

       p.privateFlags = PRIVATE_FLAG_WILL_NOT_REPLACE_ON_RELAUNCH

       | PRIVATE_FLAG_LAYOUT_CHILD_WINDOW_IN_PARENT_FRAME;

       p.setTitle("PopupWindow:" + Integer.toHexString(hashCode()));        return p;

       }//将PopupWindow添加到Window上private void invokePopup(WindowManager.LayoutParams p) {        if (mContext != null) {

       p.packageName = mContext.getPackageName();

       }        final PopupDecorView decorView = mDecorView;

       decorView.setFitsSystemWindows(mLayoutInsetDecor);

       setLayoutDirectionFromAnchor();

       mWindowManager.addView(decorView, p);        if (mEnterTransition != null) {

       decorView.requestEnterTransition(mEnterTransition);

       }

       }

       é€šè¿‡PopupWindow的源码分析,我们不难看出,在调用showAtLocation时,会单独创建一个WindowManager.LayoutParams布局参数对象,用于显示PopupWindow,而该布局参数对象上并未设置任何防止截屏Flag。

       å¦‚何解决

       åŽŸå› æ—¢ç„¶æ‰¾åˆ°äº†ï¼Œé‚£ä¹ˆå¦‚何处理呢?

       å†å›žå¤´åˆ†æžä¸‹Window的关键代码:

       //通过mWindowAttributes设置标识public void setFlags(int flags, int mask) {        final WindowManager.LayoutParams attrs = getAttributes();

       attrs.flags = (attrs.flags&~mask) | (flags&mask);

       mForcedWindowFlags |= mask;

       dispatchWindowAttributesChanged(attrs);

       }

       å…¶å®žåªéœ€è¦èŽ·å¾—WindowManager.LayoutParams对象,再设置上flag即可。

       ä½†æ˜¯PopupWindow并没有像Activity一样有直接获得window的方法,更别说设置Flag了。我们再分析下PopupWindow的源码:

       //将PopupWindow添加到Window上private void invokePopup(WindowManager.LayoutParams p) {        if (mContext != null) {

       p.packageName = mContext.getPackageName();

       }

       final PopupDecorView decorView = mDecorView;

       decorView.setFitsSystemWindows(mLayoutInsetDecor);

       setLayoutDirectionFromAnchor();        //添加View

       mWindowManager.addView(decorView, p);        if (mEnterTransition != null) {

       decorView.requestEnterTransition(mEnterTransition);

       }

       }

       æˆ‘们调用showAtLocation,最终都会执行mWindowManager.addView(decorView, p);

       é‚£ä¹ˆæ˜¯å¦å¯ä»¥åœ¨addView之前获取到WindowManager.LayoutParams呢?

       ç­”案很明显,默认是不可以的。因为PopupWindow并没有公开获取WindowManager.LayoutParams的方法,而且mWindowManager也是私有的。

       å¦‚何才能解决呢?

       æˆ‘们可以通过hook的方式解决这个问题。我们先使用动态代理拦截PopupWindow类的addView方法,拿到WindowManager.LayoutParams对象,设置对应Flag,再反射获得mWindowManager对象去执行addView方法。

       é£Žé™©åˆ†æžï¼š

       ä¸è¿‡ï¼Œé€šè¿‡hook的方式也有一定的风险,因为mWindowManager是私有对象,不像Public的API,谷歌后续升级Android版本不会考虑其兼容性,所以有可能后续Android版本中改了其名称,那么我们通过反射获得mWindowManager对象不就有问题了。不过从历代版本的Android源码去看,mWindowManager被改的几率不大,所以hook也是可以用的,我们尽量写代码时考虑上这种风险,避免以后出问题。

       public class PopupWindow {

       ......    private WindowManager mWindowManager;

       ......

       }

       è€ŒaddView方法是ViewManger接口的公共方法,我们可以放心使用。

       public interface ViewManager{    public void addView(View view, ViewGroup.LayoutParams params);    public void updateViewLayout(View view, ViewGroup.LayoutParams params);    public void removeView(View view);

       }

       åŠŸèƒ½å®žçŽ°

       è€ƒè™‘到hook的可维护性和扩展性,我们将相关代码封装成一个独立的工具类吧。

       package com.ccc.ddd.testpopupwindow.utils;

       import android.os.Handler;

       import android.view.WindowManager;

       import android.widget.PopupWindow;

       import java.lang.reflect.Field;

       import java.lang.reflect.InvocationHandler;

       import java.lang.reflect.Method;

       import java.lang.reflect.Proxy;public class PopNoRecordProxy implements InvocationHandler {    private Object mWindowManager;//PopupWindow类的mWindowManager对象

       public static PopNoRecordProxy instance() {        return new PopNoRecordProxy();

       }    public void noScreenRecord(PopupWindow popupWindow) {        if (popupWindow == null) {            return;

       }        try {            //通过反射获得PopupWindow类的私有对象:mWindowManager

       Field windowManagerField = PopupWindow.class.getDeclaredField("mWindowManager");

       windowManagerField.setAccessible(true);

       mWindowManager = windowManagerField.get(popupWindow);            if(mWindowManager == null){                return;

       }            //创建WindowManager的动态代理对象proxy

       Object proxy = Proxy.newProxyInstance(Handler.class.getClassLoader(), new Class[]{ WindowManager.class}, this);            //注入动态代理对象proxy(即:mWindowManager对象由proxy对象来代理)

       windowManagerField.set(popupWindow, proxy);

       } catch (IllegalAccessException e) {

       e.printStackTrace();

       } catch (NoSuchFieldException e) {

       e.printStackTrace();

       }

       }

       @Override    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {        try {            //拦截方法mWindowManager.addView(View view, ViewGroup.LayoutParams params);

       if (method != null && method.getName() != null && method.getName().equals("addView")

       && args != null && args.length == 2) {                //获取WindowManager.LayoutParams,即:ViewGroup.LayoutParams

       WindowManager.LayoutParams params = (WindowManager.LayoutParams) args[1];                //禁止录屏

       setNoScreenRecord(params);

       }

       } catch (Exception ex) {

       ex.printStackTrace();

       }        return method.invoke(mWindowManager, args);

       }    /

**

       * 禁止录屏

       */

       private void setNoScreenRecord(WindowManager.LayoutParams params) {

       setFlags(params, WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE);

       }    /

**

       * 允许录屏

       */

       private void setAllowScreenRecord(WindowManager.LayoutParams params) {

       setFlags(params, 0, WindowManager.LayoutParams.FLAG_SECURE);

       }    /

**

       * 设置WindowManager.LayoutParams flag属性(参考系统类Window.setFlags(int flags, int mask))

       

*

       * @param params WindowManager.LayoutParams

       * @param flags  The new window flags (see WindowManager.LayoutParams).

       * @param mask   Which of the window flag bits to modify.

       */

       private void setFlags(WindowManager.LayoutParams params, int flags, int mask) {        try {            if (params == null) {                return;

       }            params.flags = (params.flags & ~mask) | (flags & mask);

       } catch (Exception ex) {

       ex.printStackTrace();

       }

       }

       }

       Popwindow禁止录屏工具类的使用,代码示例:

          //创建PopupWindow

       //正常项目中,该方法可改成工厂类

       //正常项目中,也可自定义PopupWindow,在其类中设置禁止录屏

       private PopupWindow createPopupWindow(View view, int width, int height) {

       PopupWindow popupWindow = new PopupWindow(view, width, height);        //PopupWindow禁止录屏

       PopNoRecordProxy.instance().noScreenRecord(popupWindow);        return popupWindow;

       }   //显示Popupwindow

       private void showPm() {

       View view = LayoutInflater.from(this).inflate(R.layout.pm1, null);

       PopupWindow  pw = createPopupWindow(view,ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);

       pw1.setFocusable(false);

       pw1.showAtLocation(this.getWindow().getDecorView(), Gravity.BOTTOM | Gravity.RIGHT, PopConst.PopOffsetX, PopConst.PopOffsetY);

       }

       å½•å±æ•ˆæžœå›¾ï¼š

Android性能优化:定性和定位Android图形性能问题——以后台录屏进程为例

       简介

       发现、定性与定位

       总结

       跟不上旋律节奏的源码VSYNC

       严重异常耗时的dequeueBuffer

       VirtualDisplay合成耗时

       结论

       FPS

       初步定位问题

       定性问题

       定位问题

       成果展示

       参考

简介

       本文记录一次Android图形性能问题的分析过程——发现、定性和定位图形性能问题,源码以及探讨的源码性能优化方案。

       环境:Android Q + MTK + ARM Mali-G。源码

       所分析的源码list类源码性能问题(下称case):打开录屏应用并启动后台录屏,滑动前台应用(滑屏)。源码性能表现差:CPU、源码GPU负载显著升高、源码掉帧、源码用户明显卡顿感,源码帧率不足帧,源码帧渲染、源码合成耗时急剧飙升(渲染耗时平均为ms左右)。源码

       经过优化后,源码相同环境和条件下,渲染帧率稳定在帧(提升一倍),渲染耗时平均为8.ms左右(为优化前的不到三分之一的消耗)。

       关键词 Keywords: Screen Recording; Frame rate; FPS; GPU utilization; Jank; MediaProjection; VirtualDisplay; MediaCodec; Perfetto; Inferno; Surface; SurfaceTexture; VSYNC; SurfaceFlinger; HWC; Hardware composer; GPU; OpenGL;

发现、定性与定位FPS

       计算FPS的方法和工具 Android框架层通过hwui配合底层完成渲染。该框架本身提供了逐帧渲染分段耗时记录。通过dumpsys gfxinfo可以获取。

io.microshow.screenrecorder/io.microshow.screenrecorder.activity.MainActivity/android.view.ViewRootImpl@6b9b8a9?(visibility=0)DrawPrepare?Process?Execute3...................1................

       使用工具统计帧率与平均耗时(同时打印GPU负载),在开启后台录屏的情况下滑动屏幕,平均渲染耗时高达~ms,超出.ms一倍,导致帧率仅帧,显著低于帧。

Average?elapsed?.?msFPS:??│?9.?0.?.?2.#?GPU负载?LOADING?BLOCKING?IDLE?0?#?case的对比——未开启后台录屏Average?elapsed?9.?msFPS:??│?1.?0.?5.?1.

       通过gfx柱状图直观感受性能数据 直观地感受图形渲染性能,除了帧率感受、触控延时外,还可以通过将gfxinfo的分段耗时通过柱状图展示在屏幕上。

       这是case性能问题的gfxinfo柱状图,可以看到红柱和绿柱都非常高,远远超越了流畅标准。其中,绿柱异常放大表明两个Vsync之间耗时显著增长,红柱异常放大表明应用层应用加速使用的DisplayLists大量增长、或图形层使用GLES调用GPU耗时显著增多导致的GPU执行绘制指令耗时变长。

初步定位问题

       本节记录初步的医疗监护系统源码分析思路和定位过程。首先我们完成实验(启停后台录屏并滑动屏幕触发渲染)、观测以及记录,拿到了后台录屏启停情况下的FPS、分阶段耗时以及GPU负载(相关数据位于FPS小节)。

       开发的工具输出的统计数据计算结果非常直观,一眼可见,后台录屏为Draw阶段带来额外的~8倍或~8ms耗时,给Process阶段带来额外的~2倍或~ms耗时。帧率从帧坠落到~帧。

       耗时分析 可以看到,主要的额外耗时来自Draw和Process。接下来重点围绕着两part定位问题问题。

StageDescriptionCompDraw创建DisplayLists的耗时。Android的View如果支持硬件加速,绘制工作均通过DisplayLists由GPU绘制,可以处理为onDraw的耗时额外~8ms或~8倍Prepare准备没有额外耗时ProcessDisplayLists执行耗时。即硬件加速机制下提交给GPU绘制的工作耗时额外~ms或~2倍ExecuteFramebuffer前后缓冲区flip动作的耗时,上屏耗时额外不到~1ms

       Hz下,上述4个步骤合计耗时小于.ms为正常情况。case为~ms。主要增量来自Draw和Process。

       经过上述初步分析、观测后,接下来的分析可以围绕Draw和Process开展。由于Android Draw部分涉及较广,包含App 渲染线程(DisplayLists)、UI线程(onDraw方法创建DisplayLists),以及图形栈耗时如SurfaceFlinger、RenderEngine等都可能增加Draw耗时。

       这里一个技巧可以初步判断耗时来自App进程(渲染线程和UI线程)还是来自图形栈。如果能判断耗时来自App或图形栈,那么可以缩小分析范围、减少分析工作量。上述四大阶段的耗时统计分类比较宽,实际上还有更详细的分阶段耗时,它呈现在前文描述过的gfx统计信息柱状图上。gfx柱状图会以蓝色(RGB(,,))呈现onDraw方法创建和更新DisplayLists的耗时。如果case与正常情况对比后,这部分耗时(蓝柱大小对比)差异很小,java逆向出源码即可说明额外的Draw耗时不是来自App的,极可能来自图形栈。Besides,结合过度绘制分析,判断case与正常情况下是否有更多的额外绘制次数可以协同判断。

       ——根据上述指导思想,排查出了case的额外Draw耗时与App onDraw无关,多出来的DisplayLists来自App以外的进程,可能是图形栈如SurfaceFlinger。

定性问题

       本小节介绍问题追踪过程,通过一些方法定位到各阶段的耗时原因,并定性地得出case性能问题的性质。从本小节开始,围绕Perfetto进行分析。这里贴出perfetto的总览,我将关键的信息排序到顶部。前四行分别为SF负责图形的线程、提交到GPU等待完成的工作、Vsync-App、Vsync-sf,最后两行为case中出现卡顿掉帧的App的主线程(UI)和渲染线程(RenderThread)。

跟不上旋律节奏的VSYNC

       容易看到,Vsync-sf非常不规律。Vsync-sf是触发SurfaceFlinger一次合成工作的基于Hardware VSYNC虚拟出来的一个信号。它相对于真实硬件信号(HW_VSYNC)一个规律的偏移(在case设备上,Vsync-app与Vsync-sf都被配置为8.3ms,即硬件VSYNC到达后,虚拟的Vsync-app和Vsync-sf延时8.3ms后发出,分别触发App绘制、SurfaceFlinger合成。

       而case的Vsync-sf交错、残次、不齐、无规律,显然工况不佳。它将导致SurfaceFlinger不能按照预期的时间间隔将合成的帧提交到Framebuffer(经过Flip后,被提交的Framebuffer将上屏成为显示器的下一帧图像),出现掉帧/丢帧。

       As we can see,查胡麻将源码case的VSYNC-sf出现严重的漂移(见图,第二行的VSYNC-sf残次不齐、跟不上规律、难看且混乱),这导致了丢帧。(但VSYNC-sf的失控仅表示与丢帧的相关性,并不直接表明因果性。)

       VSYNC-sf为什么会出现偏差? 出于功耗的考虑,VSYNC-sf合VSYNC-app并不是一定会触发的。如果app或sf并没有更新画面的需求,那么死板固定地调度它们进行绘制和合成是不必的。编程上,负责触发VSYNC-sf和VSYNC-app的两个EventThread会在requestNextVsync调用后才会将下一个VSYNC-sf或VSYNC-app发出。因此,当(各自EventThread的)requestNextVsync没有调用时,VSYNC-app和VSYNC-sf也就出现漂移。BufferQueueLayer::onFrameAvailable会在应用提交后调用,该方法通过调用SF的signalLayerUpdate触发产生下一个VSYNC-sf。

       换而言之,出于功耗,或别的什么原因(比如耗时导致的延期,人家是线程实现的消息队列),SurfaceFlinger的SFEventThread有可能不调用requestNextVsync,这将导致Vsync-sf在窗口期内短暂消失——但是也不会出现参差不齐的情况。结合case的VSYNC信号报告来看,VSYNC-sf信号异常切实地提示了性能问题——它的不规律现象表明前后Vsync之间有异常耗时,而非低功耗机制被激活或无屏幕刷新(case性能问题复现时一直在滑前台应用的屏,它每ms都有画面更新的需求)。

       VSYNC-sf虽然出现了偏差,但是它与卡顿问题仅有相关性(或者说它是性能问题的结果),并非因果关系。猜测是其他卡顿问题导致了SF延缓了对VSYNC的request,导致其信号出现漂移。VSYNC-sf信号偏差实质上指导意义重大,因为它能提示我们,问题发生在比App更底层的地方(前文分析的结论),且比SurfaceFlinger提交到Framebuffer更上层的位置(VSYNC-sf用于触发合成,合成完成后提交到屏幕双缓冲区)。

       这样,趣味测试源码php将case性能问题的上下界都确定了,问题分析范围从原先的整个图形栈,有效的缩小到了SurfaceFlinger渲染和合成阶段了。

严重异常耗时的dequeueBuffer

       通读Perfetto,可以看到,出了难看的Vsync-sf以外,还可以看到刺眼的超长耗时的draw(App UI线程)以及耗时变态长的dequeueBuffer(App 渲染线程)调用。相对于正常情况,perfetto报告提示的case的draw方法成倍增长的耗时非常容易被误认为耗时“居然来自一开始就排除掉的App进程",这与前文提出的”问题范围“是不能自洽的——它们是相反的结论,肯定哪里不对。仔细分析才能发现,draw方法确实是消耗了更多墙上时间(但是不意味着消耗了更多CPU时间,因为等待过程是sleep的),但是draw方法是因为等待渲染线程的dequeueBuffer造成的耗时,而dequeueBuffer的严重异常耗时却是被底层的图形栈拖累的。

       我们看到,draw严重耗时,渲染线程dequeueBuffer消耗掉~ms的时间。As we all known,Android的Graphics buffer是生产者消费者模型,当作为消费者的SF来不及处理buffer并释放,渲染线程也就需要额外耗时等待buffer就绪。上面还有一段"Waiting GPU Completion"的trace没有贴上来(下图),这段耗时比不开启后台录屏的case下高得多(~3ms对比~ms),说明了一定的GPU性能问题或SF的性能问题,甚至有可能是Display有问题(HWC release耗时过长也会导致SF释放buf、生产者渲染线程dequeueBuffer额外等待)。

       这里的机制比较复杂,不熟悉底层Graphics buffer的流水线模型就不好理解。In one world, dequeueBuffer申请的buffer不是凭空new出来的,而是在App-SurfaceFlinger-Framebuffer这一流水线中循环使用的。流水线中的buffer不是无限的,而是有穷的几个。当底层的伙计,如SF和HWC,使用了buffer但是没有来得及释放时(它们的工作没做完之前不会释放buffer),流水线(可以理解成头尾相接的单向队列(ring buffer))没有可用的buffer,此时dequeueBuffer就不得不进入等待,出现耗时看上去很长的问题。实际上,dequeueBuffer耗时的唯一原因几乎仅仅只有一个:底层消费太慢了,流水线没有剩余buffer,因此需要等待。

       这个模型抽象理解非常简单。下图,右边消费者是底层图形栈——它每消费完一个buffer就会释放掉,每释放一个buffer应用层能用的buffer就加1。左边生产者是App渲染线程——它调用dequeueBuffer申请一个buffer以将它的画面绘制到这个buffer上。buffer送入BufferQueue后由右边的消费者(图形栈)进行消费(合成、上屏显示),然后释放buffer。当图形栈来不及release buffer时,dequeueBuffer的调用者(App渲染线程)将由于无可用buffer,就必须挂起等待了,在perfetto上就留下长长的一段”耗时“(实际上是墙上时间,大部分都没有占用CPU)。

       以上,这就是为什么说App渲染线程dequeueBuffer严重耗时中的耗时为什么要打引号,为什么要说是被图形层拖累了。

       下图可以看到,刨去dequeueBuffer的严重异常耗时,执行渲染的部分耗时相对于正常的case几乎没有差异,这可以断言渲染线程的惨烈耗时主要就是被dequeueBuffer浪费了。

       从GPU Completion来看,此时GPU正在为SF工作,因为在图中看到(不好意思没有截全,下图你是看不出来的),dequeueBuffer总是在SF的GPU Completion结束之后结束的,这就表明SF正在通过GPU消费buffer(调用GPU进行合成后提交,然后标记buffer允许被渲染线程dequeue)。dequeueBuffer获取到就绪的buffer此时此刻取决于SF的消费能力——因为case中它是短板。(当然图形层的buffer可用不止SurfaceFlinger需要释放,因为SF释放后buffer实质上流转到更底层的HWC,等它将Buffer提交到屏幕后才会释放,这里释放后才能给App再次使用(上面哪个模型图把SF和HWC合并为流水线的图形层buffer消费者)。

       从perfetto报告看HWC release非常及时、余量充足,SF的GPU Completion则较紧密地接着dequeueBuffer返回,基本断言是SF太慢了——排除HWC的责任。(下图看不出来,当时没有截图到HWC的release情况。)

       到这里,除了再次确认排除了前台App的问题外,还可以断言问题来自SurfaceFlinger过分耗时。此外将问题范围的下界从整个SF合成流程(上文的Vsync-sf)缩小到了排除HWC的范围。

       结论:渲染耗时一切正常,问题出现在SF消费buffer(合成图形)失速了,导致没有可用的buffer供渲染线程使用。从下图的SF的工况(第三列)来看,情况确实如此。

       既然一口咬定是SF的锅,那就瞧瞧SF。先看SF的INVALIDATE,这没啥好看的,异常case和正常case都是~2.5ms。主要看refresh,正常case ~6.8ms,异常case ~.8ms。refresh包含SF的合成四件套,包括rebuildLayerStack、CalcuateWorkingSet、Prepare、doComposition。Perfetto报告直接表明,case的后台录屏导致的额外一次合成和配套工作是主要的耗时增量。

       之所以会执行两次合成,是因为后台录屏工具编程上通过Android SDK提供的MediaProjection配合VirtualDisplay实现一个虚拟的镜像的屏幕。SurfaceFlinger会将画面输送一份到这个虚拟的Display以实现屏幕图像传送到录屏工具,虚拟的屏幕要求额外的一次合成。从上图可以直接得出结论,case带来的额外工作消耗就是对该录屏用的VirtualDisplay的合成工作(doComposition)带来的。

VirtualDisplay合成耗时

       由于问题范围已经缩小到了很小的一个范围,在SurfaceFlinger的Refresh过程中,case相对正常应用有巨大的差异耗时,几乎完全来自于对VirtualDisplay的合成耗时(doComposition)。同时也可以看到,两次合成(一次是设备的物理屏幕,一次是case的后台录屏工具创建的虚拟屏幕)中,虚拟屏幕的耗时远远高于物理屏幕(4倍以上)。

       通过查看ATRACE的tag(上图,Perfetto中SurfaceFlinger中主线程的各个trace point都是用ATRACE打的tag),结合dumpsys SurfaceFlinger,能直接看到的线索是:

       虚拟屏显著耗时,且合成工作通过GLES调用GPU完成

       物理屏合成耗时很小,它通过HWC合成

       结合图中提示的trace tag、耗时,可以得出结论,使用GPU合成的虚拟屏中因GPU合成耗时很长,导致它显著高于物理屏HWC合成耗时。如果GPU合成能够和HWC合成一样快,或者干脆让虚拟屏也使用HWC合成,那么可以预期SurfaceFlinger的合成工作的消耗将显著降低。

结论

       本小节综合上述三个小节的分析,对节”定性问题“下一个结论。

       耗时的本质已经被看透,录屏工具申请创建的VirtualDisplay没有通过HWC进行合成,而是通过GPU进行合成,它耗时很长导致界面卡顿。In one word,case使用的VirtualDisplay的合成方式不够高效。

       HWC是Hardward Composer。它接收图形数据,类似于往桌面(真的桌面,不是电脑和手机的桌面)上面叠放照片和纸张——即合成过程。这个工作能将界面上几个窗口叠加在一起后送到屏幕上显示。通过GLES调动GPU也能干这活,不过HWC执行合成的动作是纯硬件的——它很快,比GPU快几倍。

定位问题

       前面虽然定性了问题原因是合成方式不够高效,但是没有得出其中的原理——为什么虚拟屏不使用高效的HWC进行合成。本节通过介绍HWC的原理、SurfaceFlinger控制合成方式、虚拟屏Surface特性等来介绍图形栈中合成方式的处理模式。掌握了相关管理后,探讨一些尽量通用的共性的解决方案实现性能优化。最后着重介绍多套优化方案中的一种直面根本原因的解决方法——MediaCodec.MediaFormat创建的支持HWC合成的Surface方案。

       SurfaceFlinger如何决定使用HWC还是GPU合成? SurfaceFlinger合成主要可以依靠两条路径。其中之一是”纯硬“的HWC合成(在dumpsys SurfaceFlinger中可以看到Composition type为DEVICE),另一个是通过OpenGL让GPU进行合成(Composition type为CLIENT)。

       除非是功耗上的设计,否则SurfaceFlinger总是会优先检查本次合成是否支持使用HWC。编程上,在合成阶段之一的prepare过程中,SurfaceFlinger通过prepareFrame在RenderSurface与Hardware Composer(即HWC)的HIDL服务通信,完成hwc layer的创建。但是,layer能够成功创建不意味着一定支持HWC合成。SurfaceFlinger通过getChangedCompositionTypes向HWC查询不支持HWC合成的Layer。该方法返回的layer如果被标记为CLIENT合成,那么这部分Layer无法由HWC进行合成,而只能通过GPU进行合成——case的VirtualDisplay就是这个情况。

       部分layer可能不能由HWC合成的原因(除功耗策略、其他软件策略外):

       HWC layer达到上限 Hardware Composer支持的layer数量是有限的。查阅公开资料可知,HWC合成动作属于硬件提供的能力,它们的合成能力受到硬件本身的限制。Google官方资料对Android设备的要求是,HWC最少应该支持4个Layer,分别用于一个常规页面上最常见的4个层:壁纸、状态栏、导航栏和应用窗口。 在case设备中,经过测试,该平台的HWC最多支持7个能进行HWC合成的layer,从第8个layer开始,完完全全只能使用CLIENT合成亦即SurfaceFlinger调用RenderEngine通过OpenGL调动GPU进行合成。 正是由于HWC合成layer有上限,因此在弹出多个弹窗、叠加过于复杂时,即使界面简单也有可能出现比较明显的卡顿。

       VirtualDisplay的Surface格式不受HWC支持 HWC的硬件合成能力对buffer(Surface封装)内保存的图像的格式有要求。比如,HWC不能处理缩放,仅支持一部分的格式,大多数都还有其他因素会导致不支持,如旋转、部分Alpha等等。In one word,图像格式的数量是远远多于HWC支持的类型数的。当HWC碰到不支持合成的Surface时,就会在前文提过的getChangedCompositionTypes中通知SurfaceFlinger,由SurfaceFlinger转为使用GPU合成。

       结合上述几种情况,设计实验验证。其中通过在物理屏上弹窗来增加Layer以获取HWC Layer上限。确认case无法使用HWC合成不是Layer上限导致的问题后,通过对比来验证Surface格式问题。Surface是对native层的buffer的封装,其类型广泛、实现复杂,一个一个试是不现实的。通过对比性能强劲的类似实现可以一探究竟。Android adb提供一个出厂自带的录屏命令screenrecord、用于测试双屏显示功能的虚拟辅助屏幕(开发者模式-模拟辅助屏)、著名远程窥屏工具scrcpy等三个工具是一系列重要参考。

       经过测试,screenrecord和scrcpy创建的VirtualDisplay支持HWC合成——这是优化目标。首先看看它们的实现。

       编程上,虚拟辅助屏幕采用了与case一模一样的实现——通过创建VirtualDisplay让图形层额外合成一次屏幕到该虚拟屏幕中。虚拟屏幕本质上将画面发送给录屏功能实现,而非进行显示来完成录屏。

       通读screenrecord源码,逻辑上,它与虚拟辅助屏、case录屏应用是相同的——VirtualDisplay录屏。但是编程上略有差异:

       screenrecord直接通过binder与SurfaceFlinger通信,获取了raw VirtualDisplay,而

更多内容请点击【时尚】专栏