补:《Android面试题思考与解答》12月刊(四)

简介: 日子过的好快,12月又过了,也就代表2020也要结束了。不管你在这一年中是开心,是难过,是苦闷,还是平淡,都过去了,向前看,老铁们~

类初始化的触发时机


在同一个类加载器下,一个类型只会被初始化一次,刚才说到new对象是类初始化的一个判断时机,其实一共有六种能够触发类初始化的时机:


  • 虚拟机启动时,初始化包含 main 方法的主类;
  • 遇到 new等指令创建对象实例时,如果目标对象类没有被初始化则进行初始化操作;
  • 当遇到访问静态方法或者静态字段的指令时,如果目标对象类没有被初始化则进行初始化操作;
  • 子类的初始化过程如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化;
  • 使用反射API 进行反射调用时,如果类没有进行过初始化则需要先触发其初始化;
  • 第一次调用java.lang.invoke.MethodHandle 实例时,需要初始化 MethodHandle 指向方法所在的类。


多线程进行类的初始化会出问题吗


不会,<clinit>()方法是阻塞的,在多线程环境下,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>(),其他线程都会被阻塞。


类的实例化触发时机


  • 使用new关键字创建对象
  • 使用Class类的newInstance方法,Constructor类的newInstance方法(反射机制)
  • 使用Clone方法创建对象
  • 使用(反)序列化机制创建对象


<clinit>()方法和<init>()方法区别。


  • <clinit>()方法发生在类初始化阶段,会执行类中的静态类变量的初始化和静态代码块中的逻辑,执行顺序就是语句在源文件中出现的顺序。
  • <init>()方法发生在类实例化阶段,是默认的构造函数,会执行普通成员变量的初始化和普通代码块的逻辑,执行顺序就是语句在源文件中出现的顺序。


在类都没有初始化完毕之前,能直接进行实例化相应的对象吗?


刚才都说了先初始化,再实例化,如果这个问题可以的话那不是打脸了吗?


没错,要打脸了哈哈。


确实是先进行类的初始化,再进行类的实例化,但是如果我们在类的初始化阶段就直接实例化对象呢?比如:


public class Run {
    public static void main(String[] args) {
        new Person2();
    }
}
public class Person2 {
    public static int value1 = 100;
    public static final int value2 = 200;
    public static Person2 p = new Person2();
    public int value4 = 400;
    static{
        value1 = 101;
        System.out.println("1");
    }
    {
        value1 = 102;
        System.out.println("2");
    }
    public Person2(){
        value1 = 103;
        System.out.println("3");
    }
}


嘿嘿,这时候该怎么打印结果呢?


按照上面说过的逻辑,应该是先静态变量和静态代码块,然后普通成员变量和普通代码块,最后是构造函数。


但是因为静态变量又执行了一次new Person2(),所以实例化过程被强行提前了,在初始化过程中就进行了实例化。这段代码的结果就变成了:


23123


所以,实例化不一定要在类初始化结束之后才开始初始化,有可能在初始化过程中就进行了实例化。


类的初始化过程与类的实例化过程的异同?


学了上面的内容,这个问题就很简单了:


  • 类的初始化,是指在类装载,链接之后的一个阶段,会执行<clinit>()方法,初始化静态变量,执行静态代码块等。只会执行一次。
  • 类的实例化,是指在类完全加载到内存中后创建对象的过程,会执行<init>()方法,初始化普通变量,调用普通代码块。可以被调用多次。


一个实例变量在对象初始化的过程中最多可以被赋值几次?


那我们就试试举例出最多的情况,其实也就是每个要经过的地方都对实例变量进行一次赋值:


  • 1、对象被创建时候,分配内存会把实例变量赋予默认值,这是肯定会发生的。
  • 2、实例变量本身初始化的时候,就给他赋值一次,也就是int value1=100。
  • 3、初始化代码块的时候,也赋值一次。
  • 4、构造函数中,在进行赋值一次。


一共四次,看代码:


public class Person3 {
    public int value1 = 100;
    {
        value1 = 102;
        System.out.println("2");
    }
    public Person3(){
        value1 = 103;
        System.out.println("3");
    }
}


高刷手机,60hz,120hz指的是什么


指的是屏幕的刷新频率,也就是一秒内屏幕刷新的次数。刷新频率这个参数是手机出厂就决定的,取决于硬件的固定参数。


高刷手机,一般就是指高刷新率屏幕,也就是大于一般的60hz,比如90hz,120hz等等。它的特点就在于每秒刷新的频率更高,使得画面更加流畅,顺滑,就算出现丢帧等情况,画面还能保证一个稳定性。


屏幕的刷新过程。


屏幕的刷新过程是每一行从左到右,从上到下,顺序显示像素点。当整个屏幕刷新完毕,即一个垂直刷新周期完成,会有短暂的空白期,此时发出VSync 信号。如果是60hz的手机,那么每次屏幕刷新的过程占用时间就是16ms(1000/60)左右。


一般一个图形界面的绘制,需要CPU准备数据,然后GPU进行绘制,绘制完写入缓存区,然后屏幕按照刷新频率来从这个缓存区中取图形显示。


所以整个刷新过程是CPU,GPU,屏幕(Display)三方合作的工作关系。


帧率,VSYNC是什么


帧率,就是GPU一秒内绘制操作的帧数,单位是fps。游戏中比较常见,越大也就代表越流畅。所以这个参数并不是固定值,但是如果屏幕刷新频率是60hz,你的帧率大于60fps也就浪费了,所以一般情况下最好是帧率和屏幕刷新频率保持一致,即同样是60fps。这样就能保证一个比较平滑的视觉动画。


VSync,垂直同步,在Android4.1引进,是一种定时发送绘制信号的机制,它的作用就是让帧率和屏幕刷新率保持一致,防止跳帧卡顿等等。玩过lol的朋友应该都知道,设置界面就可以开启垂直同步选项。


正常如果没有开启vsync,屏幕刷新有可能会出现什么问题呢?


3.png


如图,由于CPU,GPU绘制图像的时间不定,所以就有可能会发生卡顿情况,也就是下一帧的数据没准备好,无法正常显示到屏幕上。


如果我们开启vsync,也就是给CPU和GPU规定了开始绘制帧数据的时间。开启后,系统会每16ms就发送一次vsync信号,CPU收到信号就开始处理数据,然后GPU绘制图像。这样就把16ms最大化的利用起来了,只要CPU和GPU16ms之内把下一帧数据处理好,那么屏幕就能从缓存区中拿到下一帧数据并显示出来了。如图:


2.png


所以vsync信号就是为了保证16ms绘制出一帧的数据出来。使得屏幕每16ms刷新一次,就能用到最新的帧数据了。这样画面就是比较流畅的了。


这整个过程其实就在Choreographer类中实现的,包括同步屏障的使用也在其中,下次会具体讲到。


单缓存,双缓存,三缓存


  • 单缓存。就是CPU计算好的数据交给GPU,然后GPU进行图像绘制,最后放到缓存区。而屏幕就直接从这个缓存区中拿到数据并显示。


但是这样做有个问题就是,因为Display和GPU都是操作的同一个缓存,就会出现同一个画面中有不同帧的数据。比如屏幕刷新的时候,第二帧还没绘制完,那么缓存中就有第二帧数据还有第一帧残留的数据,这样显示出来的画面就有两个帧的画面了,比如画面撕裂


  • 双缓存。这个双缓存就是设计出来解决单缓存问题的。既然Display和GPU不能共用一个缓存,那么就设计两个缓存就可以啦。


FrameBuffer来做显示输出,也就是屏幕每次从这个缓存中取图形数据。BackBuffer用来放下一帧的画面,也就是CPU每次绘制数据到这个缓存中。然后当CPU完整绘制完下一帧图形,也就是BackBuffer准备好,屏幕也显示完上一帧数据的时候,就进行缓存交换,把数据同步到FrameBuffer。而这个缓存交换点,就是vsync信号时刻。


  • 三缓存。Android4.1 引入,一般来说,双缓存就能够使用了,但是为什么还有一个三缓存呢?看图:


1.png


双缓存情况下,如果CPU/GPU处理数据过慢,就会发生上图的情况。也就是vsync信号来的时候,上一帧数据还没绘制完,于是A数据图片显示了两帧的时间,而且由于vsync来的时候cpu才开始处理数据,而图上vsync来的时候,GPU还在处理数据,导致GPU处理完了之后,无法触发下一帧数据的处理,浪费了一大半时间。后面情况类似,只要CPU/GPU处理数据过慢,就会发生Jank(卡顿等问题)


所以这时候就引入了第三个缓存,如图:


0.png


如图所示,在vsync来的时候,如果GPU还没处理好数据帧B的图形,这时候第三个缓存区可以来处理后面C帧的数据,然后第二个vsync信号来的时候,虽然第三缓存区还在用作处理C帧数据,但是之前的BackBuffer又可以来缓存下一帧的数据了。


这样一来,虽然A帧数据还是显示了两个时间点,但是后面由于有新Buffer的加入,可以保证后续图像显示能正常平滑的显示了。就相当于多了一个劳动力,可以最大限度利用好时间。


代码中修改了UI,屏幕是怎么进行刷新的?


当我们用代码修改了UI,比如使用了setText,修改Textview的值。这时候屏幕不会马上绘制刷新。而是会调用到invalidate方法请求重绘,然后会向VSYNC服务发送请求,等到下一个VSYNC信号触发的时候,就开始上面说过的流程,也就是处理数据,绘制图像,具体所做的工作就是测量—布局—绘制。接着,屏幕就可以拿到缓存区中绘制好的图像并显示到屏幕上了。


所以任何UI的改变,都要遵从上述所说的VSYNC机制,只是这个过程很短。当然为了保证最快时间绘制到屏幕上,而不让其他消息影响到VSYNC的响应速度,就加入了同步屏障。


如果界面保持静止不变,屏幕会刷新吗?图像会被重新绘制吗?


首先,屏幕刷新频率这个是不会变的,也就是每隔16ms左右就会进行一次刷新,而刷新的帧数据就是我们的程序内部在接收到刷新的vsync信号之后,经过计算绘制后的图像数据。


但是,app并不是每一个vsync信号都能接收到的,只有当应用有绘制需求的时候,才会通过scheduledVsync 方法申请VSYNC信号,然后下一个屏幕刷新的信号才能被我们的程序所接收到,也就是Choreographer类的onVsync方法才能被执行,然后就开始测量—布局—绘制等工作了。


所以,如果界面不变化,我们的程序就收不到VSYNC信号,也就无法处理数据进行绘制了。只有当需要改变界面的时候,才会去申请这个屏幕刷新服务,才能接收到VSYNC信号。这种情况下,屏幕还会进行刷新,只不过刷新的都是同样的图像数据。


说说UI(布局)优化


UI优化知识点主要分为三部分


  • 第一部分,系统为我们做的优化。由于前端中UI展示的特殊性和重要性,Android团队也是在不断想办法提供UI方面的渲染速度等等,所以也是提供了很多方案进行优化,比如:


硬件加速、黄油计划、RenderThread。


  • 第二部分,具体的优化方案。主要包括:


java代码布局、View重用、xml布局优化、异步布局框架Litho、RenderThread与RenderScript、屏幕适配、Flutter、Jetpack Compose


  • 第三部分,工具使用,主要包括:


Choreographer、monitor、Systrace


具体内容可以查看往期文章 UI(布局)优化全解析


总结


进阶不在于一蹴而就,而在点点滴滴。


参考


《Android开发艺术探索》

https://juejin.cn/post/6863756420380196877

https://www.cnblogs.com/frrj/archive/2018/07/30/brief-info-of-android-display.html

https://www.jianshu.com/p/10db590ed9a6

https://blog.csdn.net/justloveyou_/article/details/72466416

https://kaiwu.lagou.com/course/courseInfo.htm?courseId=67#/detail/pc?id=1860https://www.jianshu.com/p/8a14ed0ed1e9

https://kaiwu.lagou.com/course/courseInfo.htm?courseId=67#/detail/pc?id=1872https://www.cnblogs.com/shakinghead/p/11025805.htmlhttps://blog.csdn.net/lmj623565791/article/details/49300989

https://blog.csdn.net/ylyg050518/article/details/97671874https://juejin.cn/post/6844903641032163336https://juejin.cn/post/6844903958113157128https://www.cnblogs.com/mythou/p/3258715.htmlhttps://www.jianshu.com/p/71480c680a65https://juejin.cn/post/6844903775153422343

https://www.cnblogs.com/huolongluo/p/6523552.htmlhttps://www.jianshu.com/p/f67e05d7cd30https://juejin.cn/post/6900870262062120967https://segmentfault.com/a/1190000023876273

https://www.cnblogs.com/throwable/p/12272269.htmlhttps://www.cnblogs.com/pu20065226/p/12206463.htmlhttps://blog.csdn.net/yisuoyanyv/article/details/104657546

相关实践学习
部署Stable Diffusion玩转AI绘画(GPU云服务器)
本实验通过在ECS上从零开始部署Stable Diffusion来进行AI绘画创作,开启AIGC盲盒。
目录
相关文章
|
4月前
|
安全 Android开发 Kotlin
Android经典面试题之Kotlin延迟初始化的by lazy和lateinit有什么区别?
**Kotlin中的`by lazy`和`lateinit`都是延迟初始化技术。`by lazy`用于只读属性,线程安全,首次访问时初始化;`lateinit`用于可变属性,需手动初始化,非线程安全。`by lazy`支持线程安全模式选择,而`lateinit`适用于构造函数后初始化。选择依赖于属性特性和使用场景。**
155 5
Android经典面试题之Kotlin延迟初始化的by lazy和lateinit有什么区别?
|
4月前
|
安全 Android开发 Kotlin
Android经典面试题之Kotlin中常见作用域函数
**Kotlin作用域函数概览**: `let`, `run`, `with`, `apply`, `also`. `let`安全调用并返回结果; `run`在上下文中执行代码并返回结果; `with`执行代码块,返回结果; `apply`配置对象后返回自身; `also`附加操作后返回自身
61 8
|
4月前
|
Android开发 开发者
Android经典面试题之SurfaceView和TextureView有什么区别?
分享了`SurfaceView`和`TextureView`在Android中的角色。`SurfaceView`适于视频/游戏,独立窗口低延迟,但变换受限;`TextureView`支持复杂变换,视图层级中渲染,适合动画/视频特效,但性能略低。两者在性能、变换、使用和层级上有差异,开发者需按需选择。
79 1
|
4月前
|
SQL 安全 Java
Android经典面试题之Kotlin中object关键字实现的是什么类型的单例模式?原理是什么?怎么实现双重检验锁单例模式?
Kotlin 单例模式概览 在 Kotlin 中,`object` 关键字轻松实现单例,提供线程安全的“饿汉式”单例。例如: 要延迟初始化,可使用 `companion object` 和 `lazy` 委托: 对于参数化的线程安全单例,结合 `@Volatile` 和 `synchronized`
63 6
|
4月前
|
SQL Java Unix
Android经典面试题之Java中获取时间戳的方式有哪些?有什么区别?
在Java中获取时间戳有多种方式,包括`System.currentTimeMillis()`(毫秒级,适用于日志和计时)、`System.nanoTime()`(纳秒级,高精度计时)、`Instant.now().toEpochMilli()`(毫秒级,ISO-8601标准)和`Instant.now().getEpochSecond()`(秒级)。`Timestamp.valueOf(LocalDateTime.now()).getTime()`适用于数据库操作。选择方法取决于精度、用途和时间起点的需求。
67 3
|
4月前
|
XML Android开发 数据格式
Android面试题之DialogFragment中隐藏导航栏
在Android中展示全屏`DialogFragment`并隐藏状态栏和导航栏,可通过设置系统UI标志实现。 记得在布局文件中添加内容,并使用`show()`方法显示`DialogFragment`。
63 2
|
4月前
|
Android开发
Android面试题之View的invalidate方法和postInvalidate方法有什么区别
本文探讨了Android自定义View中`invalidate()`和`postInvalidate()`的区别。`invalidate()`在UI线程中刷新View,而`postInvalidate()`用于非UI线程,通过消息机制切换到UI线程执行`invalidate()`。源码分析显示,`postInvalidate()`最终调用`ViewRootImpl`的`dispatchInvalidateDelayed`,通过Handler发送消息到UI线程执行刷新。
58 1
|
4月前
|
消息中间件 调度 Android开发
Android经典面试题之View的post方法和Handler的post方法有什么区别?
本文对比了Android开发中`View.post`与`Handler.post`的使用。`View.post`将任务加入视图关联的消息队列,在视图布局后执行,适合视图操作。`Handler.post`更通用,可调度至特定Handler的线程,不仅限于视图任务。选择方法取决于具体需求和上下文。
53 0
|
4月前
|
Android开发 Kotlin
Android经典面试题之Kotlin中Lambda表达式有哪些用法
Kotlin的Lambda表达式是匿名函数的简洁形式,常用于集合操作和高阶函数。基本语法是`{参数 -&gt; 表达式}`。例如,`{a, b -&gt; a + b}`是一个加法lambda。它们可在`map`、`filter`等函数中使用,也可作为参数传递。单参数时可使用`it`关键字,如`list.map { it * 2 }`。类型推断简化了类型声明。
28 0
|
4月前
|
Android开发 Kotlin
Android经典面试题之Kotlin中Lambda表达式和匿名函数的区别
**Kotlin中的匿名函数与Lambda表达式概述:** 匿名函数(`fun`关键字,明确返回类型,支持非局部返回)适合复杂逻辑,而Lambda(简洁语法,类型推断)常用于内联操作和高阶函数参数。两者在语法、返回类型和使用场景上有所区别,但都提供无名函数的能力。
35 0
下一篇
无影云桌面