Android高手笔记-屏幕适配 & UI优化

屏幕与适配

  • 由于Android碎片化严重,屏幕分辨率千奇百怪,而想要在各种分辨率的设备上显示基本一致的效果,适配成本越来越高;

  • 屏幕适配究其根本只有两个问题:

  1. 在不同尺寸及分辨率上UI的一致(影响着用户体验);

  2. 从效果图到UI界面代码的转化效率(影响着开发效率);

适配方式:

px

  • 在标识尺寸时,Android官方并不推荐使用px(像素),因为不同分辨率的屏幕上,同样像素大小的控件,在分辨率越高的手机上UI显示效果越小;(因为分辨率越高单位尺寸内容纳的像素数越多)

dp

  • 所以官方推荐使用dp作为尺寸单位来适配UI,dp在不同分辨率和尺寸的手机上代表了不同的真实像素;(px和dp的转化关系:px=dp*(dpi/160),其中dpi是像素密度,是系统软件上指定的单位尺寸的像素数量,往往是写在系统出厂配置文件的一个固定值,这和屏幕硬件的ppi(物理像素密度)是不同的,是参考了物理像素密度后,人为指定的一个值,保证了某个区间内的物理像素密度在软件上都使用同一个值,有利于UI适配的简化)这就是最原始的Android适配方案:dp+自适应布局和weight比例布局,基本可以解决不同手机上的适配问题;

  • 但是这种方案有两个缺陷:

  1. 只能适配大部分手机(也做不到和效果图的完全一致),某些特殊机型仍需单独适配(比如同样1920*1080的手机的dpi却可能不同);

  2. 设计稿到布局代码的实现效率低(设计稿的宽高和手机屏幕的宽高不同,px和dp间的转换,往往需要百分比或估算等,会极大地拉低开发效率);

宽高限定符适配

  • 穷举市面上所有的Android手机的宽高像素值,设定一个基准的分辨率(最好和设计稿的宽高一致),其他分辨率根据这个基准分辨率来计算,生成对应的dimens文件(可通过java、python脚本实现自动生成),放在不同的尺寸文件夹(values)内部;(插图)如480_320为基准,对于800_480的dimens文件:x1=(480/320)*1=1.5px; x2=(480/320)_2=3px;但是这种方案也有个致命缺陷,需要精准命中才能适配,如1440_750的手机如果找不到对应的尺寸文件夹就只能用统一默认的dimens文件,UI就可能变形,而Android手机厂商众多,机型更是不可枚举,所以容错机制很差;

鸿洋的AndroidAutoLayout适配方案等动态计算UI适配框架

  • 鸿洋的适配方案也来自于宽高限定符方案的启发(目前已经停止维护);因为框架要在运行时会在onMeasure里面做变换,我们自定义的控件可能会被影响或限制,可能有些特定的控件,需要单独适配,这里面可能存在的暗坑是不可预见的;整个适配工作是有框架完成的,而不是系统完成的,一旦使用这个框架,未来一旦遇到很难解决的问题,替换起来是非常麻烦的,而且项目一旦停止维护,后续的升级就只能靠你自己了;

smallestWidth适配 或者叫sw限定符适配

  • 指的是Android会识别屏幕可用高度和宽度的较小者的dp值(其实就是手机的宽度值),然后根据识别到的结果去资源文件中寻找对应限定符的文件夹下的资源文件。这种机制和上文提到的宽高限定符适配原理上是一样的,都是系统通过特定的规则来选择对应的文件举个例子,小米5的dpi是480,横向像素是1080px,根据px=dp(dpi/160),横向的dp值是1080/(480/160),也就是360dp,系统就会去寻找是否存在value-sw360dp的文件夹以及对应的资源文件;(插图)smallestWidth限定符适配和宽高限定符适配最大的区别在于,有很好的容错机制,如果没有value-sw360dp文件夹,系统会向下寻找,比如离360dp最近的只有value-sw350dp, 那么Android就会选择value-sw350dp文件夹下面的资源文件,这个特性就完美的解决了 上文提到的宽高限定符的容错问题。(通过java、python脚本实现自动生成dimens文件)(插图) (这种方案的优势是稳定性,不会有暗坑)

  • smallestWidth适配方案有一个小问题,那就是它是在Android 3.2 以后引入的,Google的本意是用它来适配平板的布局文件(但是实际上显然用于diemns适配的效果更好)所以这种方案支持的最小版本就是Android3.2了;

  • 还有一个缺陷就是多个dimens文件可能导致apk变大,根据生成的dimens文件的覆盖范围和尺寸范围,apk可能会增大300kb-800kb左右;

  • 糗百的拉丁吴大佬生成好的文件https://github.com/ladingwu/dimens_sw

  • 所有的适配方案都不是用来取代match_parent,wrap_content的,而是用来完善他们的;

今日头条适配方案

  • 通过修改density(density = dpi / 160)值,强行把所有不同尺寸分辨率的手机的宽度dp值改成一个统一的值,这样就解决了所有的适配问题这个方案侵入性很低,而且也没有涉及私有API,只是对老项目是不太友好;

  • 如果我们想在所有设备上显示完全一致,其实是不现实的,因为屏幕高宽比不是固定的,16:9、4:3甚至其他宽高比层出不穷,宽高比不同,显示完全一致就不可能了。但是通常下,我们只需要以宽(支持上下滑动的页面)或高(不支持上下滑动的页面)一个维度去适配

  • 通过阅读源码,我们可以得知,density 是 DisplayMetrics 中的成员变量,而 DisplayMetrics 实例通过 Resources#getDisplayMetrics 可以获得,而Resouces通过Activity或者Application的Context获得;DisplayMetrics 中和适配相关的几个变量:

  1. DisplayMetrics.density 就是上述的density;

  2. DisplayMetrics.densityDpi 就是上述的dpi;

  3. DisplayMetrics#scaledDensity 字体的缩放因子,正常情况下和density相等,但是调节系统字体大小后会改变这个值;

  • 布局文件中dp的转换,最终都是调用TypedValue#applyDimension(int unit, float value,DisplayMetrics metrics) (插图)来进行转换,方法中用到的DisplayMetrics正是从Resources中获得的;再看看图片的decode,BitmapFactory#decodeResourceStream方法(插图),也是通过 DisplayMetrics 中的值来计算的;因此,想要满足上述需求,我们只需要修改 DisplayMetrics 中和 dp 转换相关的变量即可;所以得到了下面适配方案:假设设计图宽度是360dp,以宽维度来适配,那么适配后的 density = 设备真实宽(单位px) / 360,接下来只需要把我们计算好的 density 在系统中修改下即可,同时在 Activity#onCreate 方法中调用下但是会有字体过小的现象,原因是在上面的适配中,我们忽略了DisplayMetrics#scaledDensity的特殊性,将DisplayMetrics#scaledDensity和DisplayMetrics#density设置为同样的值,从而某些用户在系统中修改了字体大小失效了,但是我们还不能直接用原始的scaledDensity,直接用的话可能导致某些文字超过显示区域,因此我们可以通过计算之前scaledDensity和density的比获得现在的scaledDensity;但是测试后发现另外一个问题,就是如果在系统设置中切换字体,再返回应用,字体并没有变化。于是还得监听下字体切换,调用 Application#registerComponentCallbacks 注册下onConfigurationChanged 监听即可;

  • 可以参考https://github.com/Blankj/AndroidUtilCodehttps://github.com/JessYanCoding/AndroidAutoSize这两个开源库;

UI优化

CPU 与 GPU

  • Android的绘制实现主要是借助CPU与GPU结合刷新机制共同完成的。

  • 除了屏幕,UI 渲染还依赖两个核心的硬件:CPU 与 GPU。

  • UI 组件在绘制到屏幕之前,都需要经过 Rasterization(栅格化)操作,而栅格化操作又是一个非常耗时的操作。GPU(Graphic Processing Unit )也就是图形处理器,它主要用于处理图形运算,可以帮助我们加快栅格化操作。

  • CPU软件绘制使用的是 Skia 库,它是一款能在低端设备如手机上呈现高质量的 2D 跨平台图形框架,类似 Chrome、Flutter 内部使用的都是 Skia 库;

OpenGL 与 Vulkan

  • 对于硬件绘制,我们通过调用 OpenGL ES 接口利用 GPU 完成绘制。OpenGL是一个跨平台的图形 API,它为 2D/3D 图形处理硬件指定了标准软件接口。而 OpenGL ES 是 OpenGL 的子集,专为嵌入式设备设计。

  • Android 7.0 把 OpenGL ES 升级到最新的 3.2 版本同时,还添加了对Vulkan的支持。Vulkan 是用于高性能 3D 图形的低开销、跨平台 API。相比 OpenGL ES,Vulkan 在改善功耗、多核优化提升绘图调用上有着非常明显的优势。

  • 把应用程序图形渲染过程当作一次绘画过程,那么绘画过程中 Android 的各个图形组件的作用是:

1. 画笔:Skia 或者 OpenGL。我们可以用 Skia 画笔绘制 2D 图形,也可以用 OpenGL 来绘制 2D/3D 图形。正如前面所说,前者使用 CPU 绘制,后者使用 GPU 绘制。  
2. 画纸:Surface。所有的元素都在 Surface 这张画纸上进行绘制和渲染。在 Android 中,Window 是 View 的容器,每个窗口都会关联一个 Surface。  
而 WindowManager 则负责管理这些窗口,并且把它们的数据传递给 SurfaceFlinger。  
3. 画板:Graphic Buffer。Graphic Buffer 缓冲用于应用程序图形的绘制,在 Android 4.1 之前使用的是双缓冲机制;在 Android 4.1 之后,使用的是三缓冲机制。  
4. 显示:SurfaceFlinger。它将 WindowManager 提供的所有 Surface,通过硬件合成器 Hardware Composer 合成并输出到显示屏。  

Android 渲染的演进

  1. 在 Android 3.0 之前,或者没有启用硬件加速时,系统都会使用软件方式来渲染 UI;

  2. Androd 3.0 开始,Android 开始支持硬件加速;

  3. Android 4.0 时,默认开启硬件加速;

  4. Android 4.1:

  1. 开启了Project Butter: 主要包含两个组成部分,一个是 VSYNC,一个是 Triple Buffering。VSYNC信号:对于 Android 4.0,CPU 可能会因为在忙别的事情,导致没来得及处理 UI 绘制。为解决这个问题,Project Buffer 引入了VSYNC,它类似于时钟中断。每收到 VSYNC 中断,CPU 会立即准备 Buffer 数据,由于大部分显示设备刷新频率都是 60Hz(一秒刷新 60 次),也就是说一帧数据的准备工作都要在 16ms 内完成。三缓冲机制 Triple Buffering:Android 4.1 之前,Android 使用双缓冲机制,CPU、GPU 和显示设备都能使用各自的缓冲区工作,互不影响

  2. Android 4.1还新增了 Systrace 性能数据采样和分析工具。

  3. Tracer for OpenGL ES 也是 Android 4.1 新增加的工具,它可逐帧、逐函数的记录 App 用 OpenGL ES 的绘制过程。它提供了每个 OpenGL 函数调用的消耗时间,所以很多时候用来做性能分析。但因为其强大的记录功能,在分析渲染问题时,当 Traceview、Systrace 都显得棘手时,还找不到渲染问题所在时,此时这个工具就会派上用场了。

  1. Android 4.2,系统增加了检测绘制过度工具;

  2. Android 5.0:RenderThread:

  • 经过 Project Butter 黄油计划之后,Android 的渲染性能有了很大的改善。但是不知道你有没有注意到一个问题,虽然我们利用了 GPU 的图形高性能运算,但是从计算 DisplayList,到通过 GPU 绘制到 Frame Buffer,整个计算和绘制都在 UI 主线程中完成。

  • Android 5.0 引入了两个比较大的改变。一个是引入了 RenderNode 的概念,它对 DisplayList 及一些 View 显示属性做了进一步封装。另一个是引入了 RenderThread,所有的 GL 命令执行都放到这个线程上,渲染线程在 RenderNode 中存有渲染帧的所有信息,可以做一些属性动画,这样即便主线程有耗时操作的时候也可以保证动画流畅。

  • 还可以开启 Profile GPU Rendering 检查。

  1. Android 6.0 ,在 gxinfo 添加了更详细的信息;

  2. 在 Android 7.0 又对 HWUI 进行了一些重构,而且支持了 Vulkan;

  3. 在 Android P 支持了 Vulkun 1.1。

UI 渲染测量

  • 测试工具:Profile GPU Rendering 和 Show GPU Overdraw。

  • 问题定位工具:Systrace 和 Tracer for OpenGL ES

  • Layout Inspector: AndroidStudio自带的工具,它的主要作用就是用来查看视图层级结构的,开启路径如下: 点击Tools工具栏 ->第三栏的Layout Inspector -> 选中当前的进程;

  • Choreographer:用来获取FPS的,并且可以用于线上使用,具备实时性,但是仅能在Api 16之后使用,具体的调用代码如下:

    private long mStartFrameTime = 0;  
    private int mFrameCount = 0;  
      
    /**  
     * 单次计算FPS使用160毫秒  
     */  
    private static final long MONITOR_INTERVAL = 160L;  
    private static final long MONITOR_INTERVAL_NANOS = MONITOR_INTERVAL * 1000L * 1000L;  
      
    /**  
     * 设置计算fps的单位时间间隔1000ms,即fps/s  
     */  
    private static final long MAX_INTERVAL = 1000L;  
      
    @TargetApi(Build.VERSION_CODES.JELLY_BEAN)  
    private void getFPS() {  
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) {  
            return;  
        }  
        Choreographer.getInstance().postFrameCallback(new Choreographer.FrameCallback() {  
            @Override  
            public void doFrame(long frameTimeNanos) {  
                if (mStartFrameTime == 0) {  
                    mStartFrameTime = frameTimeNanos;  
                }  
                long interval = frameTimeNanos - mStartFrameTime;  
                if (interval > MONITOR_INTERVAL_NANOS) {  
                    double fps = (((double) (mFrameCount * 1000L * 1000L)) / interval) * MAX_INTERVAL;  
                    // log输出fps  
                    LogUtils.i("当前实时fps值为: " + fps);  
                    mFrameCount = 0;  
                    mStartFrameTime = 0;  
                } else {  
                    ++mFrameCount;  
                }  
      
                Choreographer.getInstance().postFrameCallback(this);  
            }  
        });  
    }  
    
    
  • 我们需要排除掉页面没有操作的情况,即只在界面存在绘制的时候才做统计。我们可以通过 addOnDrawListener 去监听界面是否存在绘制行为: getWindow().getDecorView().getViewTreeObserver().addOnDrawListener

  • Choreographer.getInstance().postFrameCallback();

  • 使用Choreographer获取FPS的完整代码如下

  • 在 Android Studio 3.1 之后,Android 推荐使用Graphics API Debugger(GAPID)来替代 Tracer for OpenGL ES 工具。GAPID 可以说是升级版,它不仅可以跨平台,而且功能更加强大,支持 Vulkan 与回放。

  • 通过上面的几个工具,我们可以初步判断应用 UI 渲染的性能是否达标,例如是否经常出现掉帧、掉帧主要发生在渲染的哪一个阶段、是否存在 Overdraw 等。

  • 虽然这些图形化界面工具非常好用,但是它们难以用在自动化测试场景中,那有哪些测量方法可以用于自动化测量 UI 渲染性能呢?

1. gfxinfo

  • gfxinfo可以输出包含各阶段发生的动画以及帧相关的性能信息,具体命令如下:
  • adb shell dumpsys gfxinfo 包名
  • 除了渲染的性能之外,gfxinfo 还可以拿到渲染相关的内存和 View hierarchy 信息。在 Android 6.0 之后,gxfinfo 命令新增了 framestats 参数,可以拿到最近 120 帧每个绘制阶段的耗时信息: adb shell dumpsys gfxinfo 包名 framestats

2. SurfaceFlinger

  • 除了耗时,我们还比较关心渲染使用的内存。可以通过下面的命令拿到系统 SurfaceFlinger 相关的信息:adb shell dumpsys SurfaceFlinger

获取界面布局耗时

  1. AOP
@Around("execution(* android.app.Activity.setContentView(..))")  
public void getSetContentViewTime(ProceedingJoinPoint joinPoint) {  
    Signature signature = joinPoint.getSignature();  
    String name = signature.toShortString();  
    long time = System.currentTimeMillis();  
    try {  
        joinPoint.proceed();  
    } catch (Throwable throwable) {  
        throwable.printStackTrace();  
    }  
    LogHelper.i(name + " cost " + (System.currentTimeMillis() - time));  
}  

  1. LayoutInflaterCompat.setFactory2
  • 上面我们使用了AOP的方式监控了Activity的布局加载耗时,那么,如果我们需要监控每一个控件的加载耗时,该怎么实现呢?
@Override  
protected void onCreate(@Nullable Bundle savedInstanceState) {  
  
    // 使用LayoutInflaterCompat.Factory2全局监控Activity界面每一个控件的加载耗时,  
    // 也可以做全局的自定义控件替换处理,比如:将TextView全局替换为自定义的TextView。  
    LayoutInflaterCompat.setFactory2(getLayoutInflater(), new LayoutInflater.Factory2() {  
        @Override  
        public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {  
  
            if (TextUtils.equals(name, "TextView")) {  
                // 生成自定义TextView  
            }  
            long time = System.currentTimeMillis();  
            // 1  
            View view = getDelegate().createView(parent, name, context, attrs);  
            LogHelper.i(name + " cost " + (System.currentTimeMillis() - time));  
            return view;  
        }  
  
        @Override  
        public View onCreateView(String name, Context context, AttributeSet attrs) {  
            return null;  
        }  
    });  
  
    // 2、setFactory2方法需在super.onCreate方法前调用,否则无效  
    super.onCreate(savedInstanceState);  
    setContentView(getLayoutId());  
    unBinder = ButterKnife.bind(this);  
    mActivity = this;  
    ActivityCollector.getInstance().addActivity(this);  
    onViewCreated();  
    initToolbar();  
    initEventAndData();  
}  
  

UI优化的常用手段

1. 尽量使用硬件加速

  • 之所以不能使用硬件加速,是因为硬件加速不能支持所有的 Canvas API;

  • 如果使用了不支持的 API,系统就需要通过 CPU 软件模拟绘制,这也是渐变、磨砂、圆角等效果渲染性能比较低的原因。

  • SVG 也是一个非常典型的例子,SVG 有很多指令硬件加速都不支持。

  • 但我们可以用一个取巧的方法,提前将这些 SVG 转换成 Bitmap 缓存起来,这样系统就可以更好地使用硬件加速绘制。

  • 同理,对于其他圆角、渐变等场景,我们也可以改为 Bitmap 实现。

2. Create View 优化

  • View 的创建也是在 UI 线程里,对于一些非常复杂的界面,这部分的耗时不容忽视。包括各种 XML 的随机读的 I/O 时间、解析 XML 的时间、生成对象的时间(Framework 会大量使用到反射)。

  • 优化方式:

    Button button=new Button(this);  
    button.setBackgroundColor(Color.RED);  
    button.setText("Hello World");  
    ViewGroup viewGroup = (ViewGroup) LayoutInflater.from(this).inflate(R.layout.activity_main, null);  
    viewGroup.addView(button);  
    
    
    public static boolean prepareLooperWithMainThreadQueue(boolean reset) {  
        if (isMainThread()) {  
            return true;  
        } else {  
            ThreadLocal<Looper> threadLocal = (ThreadLocal) ReflectionHelper.getStaticFieldValue(Looper.class, "sThreadLocal");  
            if (threadLocal == null) {  
                return false;  
            } else {  
                Looper looper = null;  
                if (!reset) {  
                    Looper.prepare();  
                    looper = Looper.myLooper();  
                    Object queue = ReflectionHelper.invokeMethod(Looper.getMainLooper(), "getQUeue", new Class[0], new Object[0]);  
                    if (!(queue instanceof MessageQueue)) {  
                        return false;  
                    }  
                }  
                ReflectionHelper.invokeMethod(threadLocal, "set", new Class[]{Object.class}, new Object[]{looper});  
                return true;  
            }  
        }  
    }  
    // 要注意的是,在创建完 View 后我们需要把线程的 Looper 恢复成原来的。  
      
    private static boolean isMainThread() {  
        return Looper.myLooper() == Looper.getMainLooper();  
    }  
    
    
    implementation 'com.android.support:asynclayoutinflater:28.0.0'  
    // 内部分别使用了IO和反射的方式去加载布局解析器和创建对应的View  
    // setContentView(R.layout.activity_main);  
    // 使用AsyncLayoutInflater进行布局的加载  
    new AsyncLayoutInflater(MainActivity.this).inflate(R.layout.activity_main, null, new AsyncLayoutInflater.OnInflateFinishedListener() {  
            @Override  
            public void onInflateFinished(@NonNull View view, int i, @Nullable ViewGroup viewGroup) {  
                setContentView(view);  
                // findViewById、视图操作等  
        }  
    });  
    super.onCreate(savedInstanceState);  
    
    
  • AsyncLayoutInflater是通过侧面缓解的方式去缓解布局加载过程中的卡顿,但是它依然存在一些问题:

  • Android AsyncLayoutInflater 限制及改进:

    /**  
     * 实现异步加载布局的功能,修改点:  
     *  
     * 1. super.onCreate之前调用没有了默认的Factory;  
     * 2. 排队过多的优化;  
     */  
    public class AsyncLayoutInflaterPlus {  
      
        private static final String TAG = "AsyncLayoutInflaterPlus";  
        private Handler mHandler;  
        private LayoutInflater mInflater;  
        private InflateRunnable mInflateRunnable;  
        // 真正执行加载任务的线程池  
        private static ExecutorService sExecutor = Executors.newFixedThreadPool(Math.max(2,  
                Runtime.getRuntime().availableProcessors() - 2));  
        // InflateRequest pool  
        private static Pools.SynchronizedPool<AsyncLayoutInflaterPlus.InflateRequest> sRequestPool = new Pools.SynchronizedPool<>(10);  
        private Future<?> future;  
      
        public AsyncLayoutInflaterPlus(@NonNull Context context) {  
            mInflater = new AsyncLayoutInflaterPlus.BasicInflater(context);  
            mHandler = new Handler(mHandlerCallback);  
        }  
      
        @UiThread  
        public void inflate(@LayoutRes int resid, @Nullable ViewGroup parent, @NonNull CountDownLatch countDownLatch,  
                            @NonNull AsyncLayoutInflaterPlus.OnInflateFinishedListener callback) {  
            if (callback == null) {  
                throw new NullPointerException("callback argument may not be null!");  
            }  
            AsyncLayoutInflaterPlus.InflateRequest request = obtainRequest();  
            request.inflater = this;  
            request.resid = resid;  
            request.parent = parent;  
            request.callback = callback;  
            request.countDownLatch = countDownLatch;  
            mInflateRunnable = new InflateRunnable(request);  
            future = sExecutor.submit(mInflateRunnable);  
        }  
      
        public void cancel() {  
            future.cancel(true);  
        }  
      
        /**  
         * 判断这个任务是否已经开始执行  
         *  
         * @return  
         */  
        public boolean isRunning() {  
            return mInflateRunnable.isRunning();  
        }  
      
        private Handler.Callback mHandlerCallback = new Handler.Callback() {  
            @Override  
            public boolean handleMessage(Message msg) {  
                AsyncLayoutInflaterPlus.InflateRequest request = (AsyncLayoutInflaterPlus.InflateRequest) msg.obj;  
                if (request.view == null) {  
                    request.view = mInflater.inflate(  
                            request.resid, request.parent, false);  
                }  
                request.callback.onInflateFinished(  
                        request.view, request.resid, request.parent);  
                request.countDownLatch.countDown();  
                releaseRequest(request);  
                return true;  
            }  
        };  
      
        public interface OnInflateFinishedListener {  
            void onInflateFinished(View view, int resid, ViewGroup parent);  
        }  
      
        private class InflateRunnable implements Runnable {  
            private InflateRequest request;  
            private boolean isRunning;  
      
            public InflateRunnable(InflateRequest request) {  
                this.request = request;  
            }  
      
            @Override  
            public void run() {  
                isRunning = true;  
                try {  
                    request.view = request.inflater.mInflater.inflate(  
                            request.resid, request.parent, false);  
                } catch (RuntimeException ex) {  
                    // Probably a Looper failure, retry on the UI thread  
                    Log.w(TAG, "Failed to inflate resource in the background! Retrying on the UI"  
                            + " thread", ex);  
                }  
                Message.obtain(request.inflater.mHandler, 0, request)  
                        .sendToTarget();  
            }  
      
            public boolean isRunning() {  
                return isRunning;  
            }  
        }  
      
        private static class InflateRequest {  
            AsyncLayoutInflaterPlus inflater;  
            ViewGroup parent;  
            int resid;  
            View view;  
            AsyncLayoutInflaterPlus.OnInflateFinishedListener callback;  
            CountDownLatch countDownLatch;  
      
            InflateRequest() {  
            }  
        }  
      
        private static class BasicInflater extends LayoutInflater {  
            private static final String[] sClassPrefixList = {  
                    "android.widget.",  
                    "android.webkit.",  
                    "android.app."  
            };  
      
            BasicInflater(Context context) {  
                super(context);  
                if (context instanceof AppCompatActivity) {  
                    // 加上这些可以保证AppCompatActivity的情况下,super.onCreate之前  
                    // 使用AsyncLayoutInflater加载的布局也拥有默认的效果  
                    AppCompatDelegate appCompatDelegate = ((AppCompatActivity) context).getDelegate();  
                    if (appCompatDelegate instanceof LayoutInflater.Factory2) {  
                        LayoutInflaterCompat.setFactory2(this, (LayoutInflater.Factory2) appCompatDelegate);  
                    }  
                }  
            }  
      
            @Override  
            public LayoutInflater cloneInContext(Context newContext) {  
                return new AsyncLayoutInflaterPlus.BasicInflater(newContext);  
            }  
      
            @Override  
            protected View onCreateView(String name, AttributeSet attrs) throws ClassNotFoundException {  
                for (String prefix : sClassPrefixList) {  
                    try {  
                        View view = createView(name, prefix, attrs);  
                        if (view != null) {  
                            return view;  
                        }  
                    } catch (ClassNotFoundException e) {  
                        // In this case we want to let the base class take a crack  
                        // at it.  
                    }  
                }  
      
                return super.onCreateView(name, attrs);  
            }  
        }  
      
        public AsyncLayoutInflaterPlus.InflateRequest obtainRequest() {  
            AsyncLayoutInflaterPlus.InflateRequest obj = sRequestPool.acquire();  
            if (obj == null) {  
                obj = new AsyncLayoutInflaterPlus.InflateRequest();  
            }  
            return obj;  
        }  
      
        public void releaseRequest(AsyncLayoutInflaterPlus.InflateRequest obj) {  
            obj.callback = null;  
            obj.inflater = null;  
            obj.parent = null;  
            obj.resid = 0;  
            obj.view = null;  
            sRequestPool.release(obj);  
        }  
      
    }  
    
    
  • View 重用:ListView、RecycleView 通过 View 的缓存与重用大大地提升渲染性能。因此我们可以参考它们的思想,实现一套可以在不同 Activity 或者 Fragment 使用的 View 缓存机制。

  • AsynclayoutInflater异步创建View

  • 异步创建:那我们能不能在线程提前创建 View,实现 UI 的预加载吗?可以通过又一个非常取巧的方式来实现。在使用线程创建 UI 的时候,先把线程的 Looper 的 MessageQueue 替换成 UI 线程 Looper 的 Queue。

  • 使用代码创建;

  1. 不能设置LayoutInflater.Factory,需要通过自定义AsyncLayoutInflater的方式解决,由于它是一个final,所以需要将代码直接拷处进行修改。

  2. 因为是异步加载,所以需要注意在布局加载过程中不能有依赖于主线程的操作。

  • X2C: 框架保留了XML的优点,并解决了其IO操作和反射的性能问题。开发人员只需要正常写XML代码即可,在编译期,X2C会利用APT工具将XML代码翻译为Java代码。

3. measure/layout 优化

  • 渲染流程中 measure 和 layout 也是需要 CPU 在主线程执行的;

  • 优化方法:减少 UI 布局层次,优化 layout 的开销,尽量不要重复去设置背景

  • 单层布局:尽量选择LinearLayout或FrameLayout,而少用 RelativeLayout,应为RelativeLayout功能较复杂,更耗性能;但从程序扩展性的角度看,更倾向于RelativeLayout

  • 多层布局:布局较复杂时,RelativeLayout能够有效的减少布局层级

  • <include/>标签:实现布局文件的复用,如app自定义的TitleBar 只支持 layout_xx 和id属性,当include和被包含布局的根标签都指定了id时,以include为准;指定layout_xx属性时, 必须也要指定layout_width和layout_height,否则无法生效

  • <merge/>标签:在UI的结构优化中起着非常重要的作用,它可以删减多余的层级,优化UI。<merge/>多用于替换FrameLayout或者当一个布局包含另一个时,<merge/>标签消除视图层次结构中多余的视图组。例如你的主布局文件是垂直布局,引入了一个垂直布局的include,这是如果include布局使用的LinearLayout就没意义了, 使用的话反而减慢你的UI表现。这时可以使用<merge/>标签优化。

  • <ViewStub/>标签:懒加载,不会影响UI初始化时的性能;各种不常用的布局,如进度条、显示错误消息等可以使用ViewStub标签,以减少内存使用量,加快渲染速度

  • 使用 style 来定义通用的属性,从而重复利用代码,减少代码量

  • 封装组合view实现view复用

  • 使用 LinearLayoutCompat 组件来实现线性布局元素之间的分割线,从而减少了使用View来实现分割线效果

  • 布局优化:

  • Litho:异步布局

    1. 配置Litho的相关依赖  
    // 项目下  
    repositories {  
        jcenter()  
    }  
      
    // module下  
    dependencies {  
        // ...  
        // Litho  
        implementation 'com.facebook.litho:litho-core:0.33.0'  
        implementation 'com.facebook.litho:litho-widget:0.33.0'  
      
        annotationProcessor 'com.facebook.litho:litho-processor:0.33.0'  
      
        // SoLoader  
        implementation 'com.facebook.soloader:soloader:0.5.1'  
      
        // For integration with Fresco  
        implementation 'com.facebook.litho:litho-fresco:0.33.0'  
      
        // For testing  
        testImplementation 'com.facebook.litho:litho-testing:0.33.0'  
      
        // Sections (options,用来声明去构建一个list)  
        implementation 'com.facebook.litho:litho-sections-core:0.33.0'  
        implementation 'com.facebook.litho:litho-sections-widget:0.33.0'  
        compileOnly 'com.facebook.litho:litho-sections-annotations:0.33.0'  
      
        annotationProcessor 'com.facebook.litho:litho-sections-processor:0.33.0'  
    }  
    2. Application下的onCreate方法中初始化SoLoader:  
    @Override  
    public void onCreate() {  
        super.onCreate();  
        SoLoader.init(this, false);  
        //Litho使用了Yoga进行布局,而Yoga包含有native依赖,在Soloader.init方法中对这些native依赖进行了加载。  
    }  
    3. 在Activity的onCreate方法中添加如下代码即可显示单个的文本视图:  
     // 1、将Activity的Context对象保存到ComponentContext中,并同时初始化  
    // 一个资源解析者实例ResourceResolver供其余组件使用。  
    ComponentContext componentContext = new ComponentContext(this);  
    // 2、Text内部使用建造者模式以实现组件属性的链式调用,下面设置的text、  
    // TextColor等属性在Litho中被称为Prop,此概念引申字React。  
    Text lithoText = Text.create(componentContext)  
            .text("Litho text")  
            .textSizeDip(64)  
            .textColor(ContextCompat.getColor(this, R.color.light_deep_red))  
                .build();  
    // 3、设置一个LithoView去展示Text组件:LithoView.create内部新建了一个  
    // LithoView实例,并用给定的Component(lithoText)进行初始化  
    setContentView(LithoView.create(componentContext, lithoText));  
    4. 使用自定义Component  
    Litho中的视图单元叫做Component,即组件,它的设计理念来源于React组件化的思想。  
    每个组件持有描述一个视图单元所必须的属性与状态,用于视图布局的计算工作。  
    视图最终的绘制工作是由组件指定的绘制单元(View或Drawable)来完成的。  
    @LayoutSpec  
    public class ListItemSpec {  
      
        @OnCreateLayout  
        static Component onCreateLayout(ComponentContext context) {  
            // Column的作用类似于HTML中的<div>标签  
            return Column.create(context)  
                    .paddingDip(YogaEdge.ALL, 16)  
                    .backgroundColor(Color.WHITE)  
                    .child(Text.create(context)  
                                .text("Litho Study")  
                                .textSizeSp(36)  
                             .textColor(Color.BLUE)  
                                .build())  
                    .child(Text.create(context)  
                                .text("JsonChao")  
                                .textSizeSp(24)  
                             .textColor(Color.MAGENTA)  
                                .build())  
                    .build();  
        }  
    }  
    // 2、构建ListItem组件  
    ListItem listItem = ListItem.create(componentContext).build();  
    
    
  • Litho是 Facebook 开源的声明式 Android UI 渲染框架,它是基于另外一个 Facebook 开源的布局引擎Yoga开发的。
  • Flutter:自己的布局 + 渲染引擎

  • RenderThread 与 RenderScript

  • Android 5.0,系统增加了 RenderThread,对于 ViewPropertyAnimator 和 CircularReveal 动画,我们可以使用RenderThead 实现动画的异步渲染。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,732评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,496评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,264评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,807评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,806评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,675评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,029评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,683评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,704评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,666评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,773评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,413评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,016评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,204评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,083评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,503评论 2 343

推荐阅读更多精彩内容