1. Surface Flinger, Window, Activity, View 的关系
- View -> Activity(内部包含Window) -> WindowManagerService -> Surface Flinger
- Surface Flinger
- 专门负责UI内容的渲染.
- 运行于运行时层,用于和HAL层通信.
- Surface Flinger不在意渲染的实际内容,将其变成屏幕可以理解的格式,发给HAL
- Window
- 在应用框架层,通过Java进行了封装
- Window负责可视化内容的排版
- Window将排版结果,通过WindowManager与WindowManagerService执行Binder通信,最终将排版结果交给Surface Flinger呈现
- Surface Flinger为每个Window都映射了一份Surface.
- ViewGroup
- 实际View的排版工作,是由ViewGroup完成,通过递归操作,完成了其内部所有控件的measure-layout-draw流程
- ViewGroup将排版结果发给Window
- Activity
- Activity可以看成是对Window的封装.对系统而言,Activity仍然是一个被管理的窗口.对开发者而言,只关注里面的模板方法即可.
- Activity是为了实现 多窗口管理 及 视图管理 的需求.
- Activity主要内容涉及 视图加载及优化, 生命周期, 重建及数据恢复, 窗口间跳转等.
2. 滑动冲突
- 滑动冲突产生的原因: 为了保持用户操作的连贯性,尽量避免页面的跳转.让用户在一个界面通过横向及纵向滑动浏览尽可能多的内容.
- 滑动冲突常见场景
- 首页信息流: 纵向RecyclerView,Item是横向的BANNER
- 解决滑动冲突:
- RecyclerView本身已经解决了与嵌套布局的滑动冲突
- 采用 RecyclerView + SnapHelper实现,双重机制避免滑动冲突
- github.com/zhimaochen/…
- 解决滑动冲突:
- 详情页: 可折叠顶部 + 粘性 TabLayout + 底部 ViewPager 嵌套 RecyclerView
- 解决滑动冲突:
- CoordinateLayout + AppBarLayout + CollapsingToolbarLayout 来 实现粘性浮动及避免滑动冲突
- github.com/xuexiangjys…
- 解决滑动冲突:
- 通用的滑动冲突解决方案: NestedScrollingParent2 和 NestedScrollingChild2
- github.com/sangxiaonia…
-
NestedScrolling 是Andorid 5.0推出的一个嵌套滑动机制,主要是利用 NestedScrollingParent 和 NestedScrollingChild 让父View和子View在滚动时互相协调配合,极大的方便了我们对于嵌套滑动的处理。通过 NestedScrolling 我们可以很简单的实现类似知乎首页,QQ空间首页等非常漂亮的交互效果。
但是有一个问题,对于fling的传递,NestedScrolling的处理并不友好,child只是简单粗暴的将fling结果抛给parent。对于fling,要么child处理,要么parent处理。当我们想要先由child处理一部分,剩余的再交个parent来处理的时候,就显得比较乏力了;
在Andorid 8.0 ,推出了一个升级版本 NestedScrollingParent2 和 NestedScrollingChild2 ,友好的处理了fling的分配问题,可以实现非常丝滑柔顺的滑动效果
3. 卡顿检测的几种方法
www.infoq.cn/article/wei…
juejin.cn/post/700618…
juejin.cn/post/697356…
medium.com/androiddeve…
developer.android.google.cn/studio/prof…
- Choreographer 类,用于同 Vsync 机制配合,实现统一调度界面绘图。 系统每隔 16.6ms 发出 VSYNC 信号,来通知界面进行重绘、渲染,理想情况下每一帧的周期为 16.6ms,代表一帧的刷新频率。开发者可以通过 Choreographer 的 postFrameCallback 设置自己的 callback,你设置的 callcack 会在下一个 frame 被渲染时触发.
fun monitorF1(view: android.view.View) { Log.d(TAG, "monitorF1. start.") var lastTime: Long = -1 var cost: Long = -1 Choreographer.getInstance().postFrameCallback(object : Choreographer.FrameCallback { override fun doFrame(frameTimeNanos: Long) { if (lastTime < 0) { lastTime = frameTimeNanos } else { cost = (frameTimeNanos - lastTime) / 1000000 lastTime = frameTimeNanos Log.d(TAG, "monitorF1. cost:$cost") if(cost > 16){ //说明当前帧的刷新 和 上一帧耗时过大,要打印线程 //开启子线程获取堆栈信息 //dumpStackTraceInNewThread() } } Choreographer.getInstance().postFrameCallback(this) } }) } * D/LagMonitor: monitorF1. cost:16 * D/LagMonitor: monitorF1. cost:15 * D/LagMonitor: monitorF1. cost:16 - 通过监听主线程对应的Looper实例的loop逻辑.在loog中会执行死循环:
//获取Looper实例 final Looper me = myLooper(); //开启死循环 for(;;){ //1: 获取Message实例 Message msg = queue.next(); //2: 获取Looper实例关联的Printer实例 final Printer logging = me.mLogging; //3: Message处理前打印 if (logging != null) { logging.println(">>>>> Dispatching to " + msg.target + " " + msg.callback + ": " + msg.what); } //4: Message处理 msg.target.dispatchMessage(msg); //5: Message处理后打印 if (logging != null) { logging.println("<<<<< Finished to " + msg.target + " " + msg.callback); } } //可以设置Looper实例的Printer public void setMessageLogging(@Nullable Printer printer) { mLogging = printer; }- Looper实例执行setMessageLoggine可以设置loop死循环中,打印日志的Printer实例
- 通过传入的自定义Printer实例,可以在println中获取得到Message及Message执行完毕的时间,进而判断当下Message处理时间是否过长.
- 发现指定Message处理时间过长,此时再去获取堆栈信息也晚了,已经执行完了.应该开启单独线程,定时获取主线程堆栈信息保存为 时间:堆栈信息 的映射.
- 发现指定Message处理时间过长,则将对应时间段的堆栈打印出来.
- 为避免一直保持堆栈信息,可以设置指定时间后将对应信息删除.
- 字节码插桩实现在方法头尾添加方法耗时统计
- 通过Gradle Plugin+ASM,编译期在每个方法开始和结束位置分别插入一行代码,统计方法耗时
插桩前 fun method(){ run() } 插桩后 fun method(){ input(1) run() output(1) }- 字节码插桩时可以添加对调用线程的判断,比如非主线程调用可以不处理
- 目前微信的Matrix 使用的卡顿监控方案就是字节码插桩
- 避免方法数暴增:在方法的入口和出口应该插入相同的函数,在编译时提前给代码中每个方法分配一个独立的 ID 作为参数。
- 过滤简单的函数:过滤一些类似直接 return、i++ 这样的简单函数,并且支持黑名单配置。对一些调用非常频繁的函数,需要添加到黑名单中来降低整个方案对性能的损耗。
- JankStats Library
- 使用Google提供的JankStates,可以在代码中实时监听指定Window的卡顿情况
- JankStates好处是集成简单,且可用在State中自由添加不同方向的信息
- 通过FrameData中的isJank属性,由系统自动判断是否发生了卡顿.可以直接将之前保存的不同方向的可疑点的值全打印出来.
4. synchronize , 重量级锁 , 轻量级锁 , 偏向锁 , 公平锁 , 非公平锁 , 悲观锁 , 乐观锁
juejin.cn/post/684490…
www.cnblogs.com/butterfly10…
juejin.cn/post/684490…
mp.weixin.qq.com/s?__biz=Mzk…
Synchronized 升级到重量级锁之后就下不来了?你错了!
juejin.cn/post/684490…
juejin.cn/post/684490…
- 锁有4种状态
- 无锁状态, 偏向锁状态, 轻量级锁状态, 重量级锁状态
- 随着线程竞争激烈,锁逐渐升级
- 锁可以升级,但不可降级
- 重量级锁
- 在JDK1.6之前内置锁的实现方式.简单说,重量级锁是采用互斥量来控制对互斥资源的访问.
- 在JDK1.6之前, synchronized实现的内置锁都是重量级锁.因为synchronized需要使用JVM的monitorenter和monitorexit, 本质上是依赖操作系统的 Mutex Lock 互斥量 来实现的,我们一般称之为重量级锁.
- 使用Mutex Lock需要将当前线程挂起并从用户态切换到内核态来执行,耗时较长,切换代价非常高,尤其是同步代码块频繁执行的情况.
- 轻量级锁
- 轻量级锁是相对于重量级锁而言,本意是在没有多线程竞争情况下,减少重量级锁使用的OS的互斥量带来的性能损耗.
- 轻量级锁提升性能的依据是绝大多数情况下,同步代码块执行过程中不存在线程竞争,可以使用CAS操作避免互斥量的开销
- 轻量级锁在加锁过程会执行CAS操作
- CAS操作成功则加锁成功
- CAS操作失败有2种可能
- 当前线程已经获取锁,直接执行同步代码块即可
- 锁对象当前被其他线程锁定.当前线程会执行一定次数的自旋,尝试获取锁
- 自旋: 执行一段无意义的类似循环逻辑,执行完毕重新执行CAS获取锁.一定次数自旋内仍未获取锁,则该轻量级锁就升级为重量级锁.当前线程会真正挂起进入阻塞状态.
- 偏向锁
- 轻量锁是在无多线程竞争情况下,通过CAS操作替换,避免OS操作Mutext Lock互斥量.
- 偏向锁是在无多线程竞争情况下,同一个线程仅执行一次CAS操作,将后续冗余的CAS操作也避免.
- 偏向锁的获取及释放及升级
- 偏向锁第一次被线程A获取,A会执行1次CAS操作,将锁对象对象头中的记录的Thread ID保存为A的ID
- A获取偏向锁成功后,执行同步代码块并退出,其持有的偏向锁并不释放
- A下次再次执行该代码块可直接进入,无需再次CAS获取锁.因为之前就没有释放.
- 若后续B要执行同步代码块,会执行CAS尝试获取锁
- 若A此时已退出同步代码块,则B的CAS操作成功.对应Thread ID更新为B的ID
- 若A此时未退出同步代码块,则B的CAS操作失败.该偏向锁升级为轻量级锁,B执行自旋尝试获取该轻量级锁(转换为轻量级锁的逻辑)
- 锁的升级: 偏向锁 -> 轻量级锁 -> 重量级锁
- 锁的一些优化
- 6.1. 适应性自旋
- JDK1.6之后引入了适应性自旋
- 如果某个轻量级锁,之前线程A自旋X次获得了锁,且A正在运行中,则JVM认为当下B也可以通过自旋获得锁,允许B自旋X甚至更多次尝试获取锁.
- 如果某个自旋锁很少通过自旋被获取,则后续获取该轻量级锁,会忽略自旋,直接将其升级为重量级锁,避免自旋空转消耗CPU.
- 6.2. 锁消除
- 编译期执行时,对一些检测到不可能存在多线程竞争的锁,执行移除,执行时是无锁的.
- 6.3. 锁粗化
- JVM检测到一连串零碎的操作都是对同一个对象加锁,则会将加锁的范围粗化到整个操作序列的外部.避免锁的重复获取/释放.
- 6.1. 适应性自旋
- 在JDK1.6之后,不必过于担心synchronize影响性能. 偏向锁 -> 轻量级锁 -> 重量级锁 .不是高并发情况下基本会以偏向锁的形态运行.性能很好
- 乐观锁 和 悲观锁 juejin.cn/post/684490… mp.weixin.qq.com/s/gSPgFtKZW… juejin.cn/post/706254… juejin.cn/post/684490… juejin.cn/post/684490… juejin.cn/post/684490… AtomicStampedReference: CAS+版本号机制. 避免ABA问题.