当前位置: 首页 > article >正文

Android那两个你碰不到但是很重要的类之ViewRootImpl

作者:Drummor

前言

这两个类就是ActivityThread和ViewRootImpl,之所以说碰不到是因为我们无法通过正常的方式引用这两个类或者其类的对象,调用方法或者直接拿他的属性。但他们其实又无处不在,应用开发中很多时候都和他们息息相关,阅读他们掌握其内部实现对我们理解Android运行机理有醍醐灌顶之疗效,码读百变其义自见,常读常新。本文就尝试从几个我们经常接触的方面先谈谈ViewRootImpl。

1.1 ViewRootImpl哪来的?

首先是ViewRootImpl,位于android.view包下,从它所处的位置大概能猜到,跟View相关。其作用一句话总结,就是连接Window和View的纽带。

这个要从我们最熟悉的Activity开始,我们知道Activity的设置布局View是通过setContentView() 方法这个方法里面也大有文章,我们简单的梳理下。

  • Activity setcontentView()内部调用了getWindow().setContentView(layoutResID);也就是调用了Window的setContentView方法,Android里Window的唯一实现类就是PhoneWindow,PhoneWindow setContentView,初始化DecorView和把我们设置的View作为其子类。
  • 目光转移到ActivityThread没错是我们提及的另外一个主角,先关注他的handleResumeActivity()方法,里面关键的部门代码,
public void handleResumeActivity(){
    r.window = r.activity.getWindow();
    View decor = r.window.getDecorView();
    ViewManager wm = a.getWindowManager();
    ViewManager wm = a.getWindowManager();
    WindowManager.LayoutParams l = r.window.getAttributes();
    wm.addView(decor, l);
}
  • WindowManager的实现类WindowManageImpl的addView方法里调用了mGlobal.updateViewLayout(view, params);
  • 最后我们在WindowManagerGlobal的addView方法里找到了
public void addView(){
    root = new ViewRootImpl(view.getContext(), display);
    view.setLayoutParams(wparams);
    mViews.add(view);
    mRoots.add(root);
    mParams.add(wparams);
}

小结

  • 通过梳理这个过程我们知道,setContenview()其实只是在Window的下面挂了一个View链,View链的根就是ViewRootImpl。
  • Window通过把View和Activity联系在一起。
  • View链的真正添加操作最终交给了WindowManagerGlobal执行。
  • 补充一点:PopupWindow本质就是在当前Window下挂了一个View链,PopupWindow本身没有Window,就如雷锋塔没有雷锋一样;Dialog是有自己的window关于这点可自行查阅源码考证。

2 ViewRootImpl 一个View链渲染的中转站

View的渲染是自定而上层层向下发起的,大致经历测量布局和绘制,View链的管理者就是ViewRootImpl。通过scheduleTraversals()方法发起渲染动作。交给Choreographer安排真正执行的时间关于Choreographer不熟悉的可以参考我的其他文章。最终执行performTraversals() 方法。

private void performTraversals(){
    performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
    performLayout(lp, mWidth, mHeight);
    performDraw();
}

3 不能在子线程操作View?

ViewRoot的RequestLayout中有这样一段代码:

@Override
public void requestLayout() {
    if (!mHandlingLayoutInLayoutRequest) {
        checkThread();
        mLayoutRequested = true;
        scheduleTraversals();
    }
}

void checkThread() {
    if (mThread != Thread.currentThread()) {
        throw new CalledFromWrongThreadException(
                "Only the original thread that created a view hierarchy can touch its views.");
    }
}
  • 我们对View的操作,比如给TextView设置text,最终都会触发ViewRootImpl的requestLayout() 方法,该方法有如上的一个check逻辑。这就是我们常说的不能在子线程中更新View。
  • 其实子线程中可以执行View的操作,但是有个前提是:View还未挂载时。 View未挂载时时不会触发requestLayout的,还只是一个普普通通的java对象。那挂载逻辑在哪?

4 View 挂载

  • 在ViewRootImpl的performTraversals() 里有这个代码
private void performTraversals(){
    host.dispatchAttachedToWindow(mAttachInfo, 0);//此处的host为ViewGroup
}
  • ViewGroup的dispatchAttachedToWindo()方法会把AttachInfo对象分配每一个View,最终实现我们所谓的挂载。
void dispatchAttachedToWindow(AttachInfo info, int visibility) {
    for (int i = 0; i < count; i++) {
        final View child = children[i];
        child.dispatchAttachedToWindow(info,
                combineVisibility(visibility, child.getVisibility()));
    }
  • 实现挂载的View有任何风吹草动就会把事件传递到大bossViewRootImpl这里了。

通过addView添加进的View也是会收到父View的mAttachInfo这里不展开了。

5 View.post()的Runnable最终在哪执行了?

public boolean post(Runnable action) {
    final AttachInfo attachInfo = mAttachInfo;
    if (attachInfo != null) {
        return attachInfo.mHandler.post(action);
    }
    getRunQueue().post(action);
    return true;
}
  • 以上是View post()的代码,可见如果已经实现挂载的View,会直接把post进来的消息交给Hanlder处理了给执行,不然就post了HandlerActionQueue里。
void dispatchAttachedToWindow(AttachInfo info, int visibility) {
  ..
    if (mRunQueue != null) {
        mRunQueue.executeActions(info.mHandler);//内部也是调用handler.post()
        mRunQueue = null;
    }
    ..
}
  • 最终这些Runnable会在View挂载的时候执行,也就是dispatchAttachedToWindow()方法里执行。

6 为什么View.post 可以获取宽高

  • 这个是是一个问题延伸,在Activity中直接获取宽高是获取不到的,我们通常会使用view.post一个Runnable来获取。原因就是Activity onCreate时通过setContentView只是创建了View而未实现挂载,挂载是在onResume时,未挂载的View其实没有经历测量过程。

  • 而通过post的方式,通过上一小节知道,未挂载的View上post之后,任务会在挂载之后,通过handler重新post,此时已经ViewRootImpl已经执行了performTraversals()完成了测量自然可以得到宽高。

7 还有一点值得注意

ViewRootImpl 不单单是渲染的中转站,还是触摸事件的中转站。

硬件传感器接收到触摸事件经过层层传递分发到应用窗口的第一站就是ViewRootImpl。为什么这么说?因为我有证据~。这是ViewRoot里的代码

public void setView(){
    ..
    mInputEventReceiver = new WindowInputEventReceiver(inputChannel,
        Looper.myLooper());
}
  • WindowInputEventReceiver是ViewRootImpl的一个内部类,其接收到input事件后,就会进行事件分发。
  • 这里给我们的启发是,并不是所有的主线程任务执行都是通过Handler机制, onTouch()事件是底层直接回调过来的,这就和我们之前卡顿监控说的方案里有一项就是对onTouchEvent的监控。

  • ViewRoot的代码有一万多行,本文分析的只是冰山一角,里面有大量细节直接研究。
  • 通过ViewRootImpl相关几个点,简单的做了介绍分析希望对你有帮助。

Android 学习笔录

Android 性能优化篇:https://qr18.cn/FVlo89
Android 车载篇:https://qr18.cn/F05ZCM
Android Framework底层原理篇:https://qr18.cn/AQpN4J
Android 音视频篇:https://qr18.cn/Ei3VPD
Jetpack全家桶篇(内含Compose):https://qr18.cn/A0gajp
Kotlin 篇:https://qr18.cn/CdjtAF
Gradle 篇:https://qr18.cn/DzrmMB
OkHttp 源码解析笔记:https://qr18.cn/Cw0pBD
Flutter 篇:https://qr18.cn/DIvKma
Android 八大知识体:https://qr18.cn/CyxarU
Android 核心笔记:https://qr21.cn/CaZQLo
Android 往年面试题锦:https://qr18.cn/CKV8OZ
2023年最新Android 面试题集https://qr18.cn/CgxrRy
Android 车载开发岗位面试习题:https://qr18.cn/FTlyCJ
音视频面试题锦:https://qr18.cn/AcV6Ap


http://www.kler.cn/a/15238.html

相关文章:

  • TDSQL 免密码登录
  • Ubuntu 22.04 上快速搭建 Samba 文件共享服务器
  • 数据分析24.11.13
  • 【Nginx】反向代理Https时相关参数:
  • C/C++中使用MYSQL
  • Acrobat Pro DC 2023(pdf免费转化word)
  • ToLua框架
  • 全国计算机等级三级网络技术试卷详解(二)
  • taro之项目初始化模板
  • JavaScript中的数据结构和算法
  • 一个朋友弄来的,太牛了,特别是后面内容,不看不知道,一看吓一跳,电话,热线
  • Leetcodes刷题之删除链表的倒数N个结点和删除链表的中间的结点
  • SD卡变成RAW格式怎么办?SD卡RAW格式的解决办法
  • HTML + CSS + JS 利用邮编查询 API 实现邮编查询工具
  • 【Mycat2】什么是原型库(Prototype)
  • 被遗忘的Java关键字:transient
  • 【刷题之路】LeetCode 203. 移除链表元素
  • Arduino学习笔记5
  • ( 字符串) 205. 同构字符串 ——【Leetcode每日一题】
  • digitalworld.local: JOY(ftp将可读文件夹上传到可写文件夹)
  • 在Linux操作系统上部署wgcloud监控
  • 《美团机器学习实践》读后感和一点思考
  • 智慧医疗服务平台有哪些优势?
  • 一些非常实用的JS前端面试题
  • 从0开始搭建一个简单的前后端分离的XX系统-vue+Springboot+mybatis-plus+mysql
  • ChatGPT实现数据结构转换