Android视图绘制原理

Android中的图形界面通常都是由Activity负责展示,实际上Activity内部展示视图还分成了三大部件,最外层的Activity负责生命周期、应用上下文资源,最内层的View负责界面展示和用户触摸消息派发处理,中间层的Window负责界面设置、消息转发,粘合Activity和View对象,通过三层设计明确了每个部件的分工,而且由于Activity的Context上下文体系和View的视图界面体系复杂,直接相互引用形成两个类体系的强耦合,不利于将来代码变化,Window隔离二者使得将来功能变化时两个类体系能够相互独立变化。

Activity在onCreate()创建生命周期回调中可以设置内容布局,如果是首次创建Activity框架内部还会生成DecorView根布局对象,DecorView继承自FrameLayout,它作为Activity内部所有视图的根视图。使用Android Studio3.2的Tools->Layout Inspector分析一下普通Activity里面的视图树结构。从下图可以看到顶层的PhoneWindow$DecorView包含了Activity里所有的界面元素,它的子视图包括navigationBarBackground底部导航栏背景控件,statusBarBackground顶部状态栏背景控件,还有一个LinearLayout布局。LinearLayout布局第一个ViewStub类型的是Activity处于ActionMode状态时展示的菜单栏,ActionMode菜单栏展示时覆盖住ActionBar,当Activity退出ActionMode模式ActionMode菜单栏会消失不见;第二个布局FrameLayout内部包含了ActionBar布局和ContentFrameLayout布局,其中用户自定义的内容布局被放到id值为android.R.id.content的FrameLayout内容布局容器中。在这里插入图片描述
上图展示的基础布局都会在执行Activity.setContentView()时全部生成,当活动管理服务(ActivityManagerService,AMS)通知Activity进入前台展示时,Activity内部的DecorView根布局会被添加并并展在手机屏幕上, 接下来的章节就详细讨论DecorView被添加到屏幕的过程中它的内部视图是如何测量、布局和绘制界面。不过在查看具体代码执行过程之前,先来看几个和视图展示相关的问题,再阅读源码时带着这些问题能够更好地理解整个视图展示流程。

视图展示常见问题

Android开发者都知道视图类型的对象都只能够在主线程中更新,如果在子线程中更新就会导致应用异常终止,是不是说View类型的对象不能够在子线程中修改呢?观察前面贴出的内容布局里的TextView它的内容是“Hello World”,现在在onCreate()方法中启动一个子线程并且将TextView的文本修改为"Good Morning",看看效果如何。

// onCreate()中子线程更新View
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_thread_modify);
    textView = findViewById(R.id.text);
    new Thread(new Runnable() {
        @Override
        public void run() {
            textView.setText("Good Morning");
        }
	}).start();
}

运行上诉代码可以看到TextView的内容确实变成了“Good Morning”,而且Activity正常展示并没有发生异常退出的现象。现在再把onCreate()方法中的子线程注释掉,在onResume()方法中开启同样的线程更新TextView,运行发现和onCreate()中执行结果一样,接着在修改文本方法前让子线程睡眠100毫秒,再编译运行应用,这时就抛出了CalledFromWrongThreadException异常并且终止了应用,只有最后一次运行结果符合预期了。

// onResume中休眠后子线程更新View
@Override
protected void onResume() {
    super.onResume();
    new Thread(new Runnable() {
        @Override
        public void run() {
            ThreadUtils.sleep(100); // 增加让子线程休眠100ms逻辑
            textView.setText("Good Morning");
        }
    }).start();
}
//~ 运行结果
android.view.ViewRootImpl$CalledFromWrongThreadException:
                     Only the original thread that created a view hierarchy can touch its views.
at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:7313)
at android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:1161)

看来在Android中子线程其实是可以更新View对象的,也就是说View对象可以在多个线程中被访问,在多线程程序中能够被多个线程访问的变量被称作共享变量,共享变量在多线程访问过程中如果没有正确的同步会出现线程安全问题。在实际开发过程中几乎所有的应用都会强调一定不要在非主线程中更新View,Android提供的CalledFromWrongThreadException异常就是为了避免多线程访问View对象。

在Activity声明周期最初的onCreate()和onResume()中执行子线程更新View时,实际上更新仅仅传递到DecorView根布局,View视图内部代码中并没有强制规定更新操作必须在主线程中执行,此时并不会抛出子线程更新视图错误异常。当AMS通知Activity展示到前台时DecorView会被绑定到ViewRootImpl对象上,该对象主要负责将用户输入消息传递给DecorView,同时检查视图更新操作是否在主线程发起,如果不是就抛出代码上面的异常信息,ViewRootImpl对象是在何时与DecorView绑定,又是如何检测不在主线程的更新操作的呢?

在开发中通常需要获取到某个视图的尺寸大小,在onCreate()和onResume()中直接调用View.getWidth()和View.getHeight()返回的值都是0,想要能够获取真实的尺寸需要调用View.post(Runnable())消息回调中才能得到视图真正的尺寸值。视图尺寸值在最初是零值主要是因为视图还没有执行过测量、布局和展示过程,View.post()投递的消息回调能够获取到真实的尺寸值就在于它回调执行在视图完全展示之后,View内部是如何保证回调在展示之后执行呢?

最后一个问题,Android系统中如果不使用Activity展示界面,有没有不采用Activity展示界面的方法呢?如下图展示了一个像素尺子,该尺子视图能够测量系统中各种界面元素的尺寸,很显然者就要求像素尺子能够展示在各种应用界面之上,包括Android系统的Launcher界面。
在这里插入图片描述

Android系统中提供了WindowManager服务专门负责将界面添加到屏幕上,通过WindowManager.addView()接口就能够轻松的将自己的视图树添加到屏幕上。直接展示在屏幕上的Window通常要求是系统提示窗类型,为此需要申请权限,在8.0和8.0之前版本两个权限还不完全一样。

<-- 8.0以下权限申请 -->
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<!-- 8.0+权限申请 -->
<uses-permission android:name="android.permission.SYSTEM_OVERLAY_WINDOW" />
// 6.0版本之后有了动态权限申请,还需要在使用时通过权限申请界面请求用户权限。
//  WindowManager展示像素尺子
@Override
protected void onResume() {
    super.onResume();
    if (Build.VERSION.SDK_INT >= 23) {
     //启动Activity让用户授权
if (!Settings.canDrawOverlays(getApplicationContext())) { 
           Intent intent = new Intent(
Settings.ACTION_MANAGE_OVERLAY_PERMISSION,  
Uri.parse("package:" + getPackageName()));
           startActivity(intent);
      }
   }
}

public class RulerWindow {
    private WindowManager windowManager;
    private WindowManager.LayoutParams layoutParams;
    private RulerView ruler; // 像素尺子视图

    public RulerWindow() {
		this.windowManager = (WindowManager) RulerApplication.getContext().
getSystemService(Context.WINDOW_SERVICE);
        this.ruler = new RulerView(RulerApplication.getContext());
        layoutParams = new WindowManager.LayoutParams();
        ruler.measure(
View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED),
                View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED));
        layoutParams.x = CommonUtils.dp2px(50); // 设置左上角位置
        layoutParams.y = CommonUtils.dp2px(50);
        layoutParams.width = ruler.getMeasuredWidth(); // 设置尺子宽高
        layoutParams.height = ruler.getMeasuredHeight();
        layoutParams.gravity = Gravity.TOP | Gravity.LEFT; // 设置其他属性
        layoutParams.format = PixelFormat.RGBA_8888;
        if (Build.VERSION.SDK_INT >= 26) { // 8.0新特性
            layoutParams.type= WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
        }else{
            layoutParams.type= WindowManager.LayoutParams.TYPE_SYSTEM_ALERT;
        }
        layoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
                | WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS;
        initRuler(); // 设置用户触摸回调
    }

    private void initRuler() {
        ruler.setOnTouchListener(new View.OnTouchListener() {
            private int lastX;
            private int lastY;
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                int x = (int) event.getRawX(), y = (int) event.getRawY();
                switch (event.getActionMasked()) {
                    case MotionEvent.ACTION_DOWN: // 记录用户最初按下位置
                        lastX = x;
                        lastY = y;
                        break;
                    case MotionEvent.ACTION_MOVE: // 根据用户移动偏移,移动尺子
                        int deltaX = x - lastX;
                        int deltaY = y - lastY;
                        layoutParams.x += deltaX;
                        layoutParams.y += deltaY;
                        windowManager.updateViewLayout(ruler, layoutParams);
                        lastX = x;
                        lastY = y;
                        break;
                    case MotionEvent.ACTION_UP:
                        break;
                }
                return false;
            }
        });
    }

    public void show() { // 将像素尺子添加到屏幕上
        windowManager.addView(ruler, layoutParams);
    }

    public void hide() { // 将像素尺子从屏幕上移除
        windowManager.removeView(ruler);
    }
}

上面的代码中使用WindowManager方法来向屏幕中添加像素尺子界面,在应用中首先需要向系统申请Overlay权限,权限获取到之后就可以使用WindowManager直接在屏幕上添加视图控件了。在RulerWindow构造函数中先创建出像素尺子视图RulerView,接着为尺子在屏幕上的展示参数LayoutParams设置值,x,y表示在屏幕坐标系的位置,width、height代表尺子展示的大小等,最后initRuler()方法为RulerView设置用户触摸事件处理,当用户在像素尺子所在的位置移动手指时像素尺子要随着用户手指同步移动,在修改了LayoutParams的x,y值后调用WindowManager.updateView()更新当前视图在屏幕中展示的位置。show()方法调用了WindowManager.addView()实现将像素尺子添加到屏幕上,hide()方法调用了WindowManager.removeView()实现将像素尺子从屏幕上移除。像素尺子的实现涉及到了视图展示和消息派发两种机制,接下来的章节就详细介绍视图展示和消息派发两大机制的原理和使用方式。

视图展示过程

其实Activity展示DecorView视图树也是通过WindowManager.addView()方法来实现的,看来要理清视图展示原理需要从addView()方法接口开始。在查阅addView()源码之前需要先了解Android视图框架的基础接口对象View和ViewGroup,其中View代表单独的视图元素,ViewGroup则是多个视图元素的组合,它能够根据特定的规则将多个视图元素排列展示。比如TextView就是专门用来展示文本的视图元素,ImageView则是专门用来展示图片的视图元素;LinearLayout能够支持从左到右的横向展示或者从上到下的竖向展示,FrameLayout则是将所有视图元素叠放在一起,它们都是ViewGroup。ViewGroup从本质上来说也是一种View,ViewGroup继承自View,比View多了一些管理内部视图的接口,它们的实现采用的就是组合模式。多个View和ViewGroup组合起来形成了树形结构,通常被称作视图树(ViewTree),单个的View或ViewGroup也可以被称作视图树。
在这里插入图片描述
View的主要接口包含measure()、layout()和draw(),其中measure()负责测量当前视图的大小,layout()负责将视图放置到指定的位置,draw()负责绘制视图内部的文本、图像等内容。draw()内部还调用了dispatchDraw()负责子视图的绘制派发工作,它其实应该是ViewGroup的接口,但draw()需要调用它就将它放到了View中并做空实现。requestLayout()实在视图大小位置需要变化会要求父对象更新,invalidate()则是在视图内容发生改变的时候要求父对象刷新界面。

//  View/ViewGroup伪代码
public class View {
	public void measure() { // 测量视图
		onMeasure();
	}
	
	public void layout() { // 布局视图
		onLayout();
	}
	
	public void draw() { // 绘制视图
		onDraw();
		dispatchDraw(); // 向子视图派发绘制视图要求
	}
	
	public void dispatchDraw() { } // View中派发子视图绘制接口空实现
	
	public void assignParent(ViewParent parent) { //  设置ViewParent对象
		mParent = parent;
	}
	
	public void requestLayout() { // 请求重新布局
		if (mParent != null) {
			mParent.requestLayout(); // 递归向上请求父视图重新布局
		}
	}
	
	public void invalidate() {
		if (mParent != null) { // 无效视图当前内容,重新绘制当前视图内容
			mParent.invalidateChild(this, null);
		}
	}
}

public class ViewGroup extends View implements ViewParent, ViewMananger {
	public void onMeasure() { // 递归调用子视图测量
		for (View view : getChildren()) {
			view.measure();
		}
}

	public void onLayout() { // 递归调用子视图布局
		for (View view : getChildren()) {
			view.layout();
		}
}

	public void dispatchDraw() { // 递归调用子视图绘制
		for (View view : getChildren()) {
			view.draw();
		}
	}
}
// ViewParent接口可以是ViewGroup类型也可以是ViewRootImpl类型
public interface ViewParent {
	public void requestLayout();
	public ViewParent getParent();
	public void invalidateChild(View child, Rect r);
}
// ViewManager可以是ViewGroup也可以是WindowManager类型
public interface ViewManager {
    public void addView(View view, ViewGroup.LayoutParams params);
    public void updateViewLayout(View view, ViewGroup.LayoutParams params);
public void removeView(View view);
}

ViewGroup视图组继承自View类型,它里面实现了dispatchDraw()方法,其实就是调用了属于它的子视图draw()方法让它们完成绘制的任务。View的其他方法都会被它继承下来,不过它通常会覆盖View默认的onMeasure()和onLayout()实现,通过特定的规则来执行测量和布局任务。ViewGroup实现了两个接口ViewParent和ViewManager,ViewParent里面主要包含重新布局和界面重绘请求,ViewManager接口提供了增加删除和修改内部子视图的接口,ViewGroup既能够作为视图对象的父对象也可以作为视图对象的管理对象。

上面伪代码里的类和接口与视图的展示有着密切的关系,需要开发者对每个类和接口都能够熟练掌握。现在开始查阅WindowManager.addView()方法的实现源码,它首先会创建一个ViewRootImpl对象,然后将加入的视图、参数和root对象都保存起来,最后调用root.setView()方法。ViewRootImpl继承自ViewParent接口,也就是说它可以作为视图的父对象;WindowManager继承自ViewManager接口,它就是视图对象的管理者。第一段代码中View.post()方法在视图没有绑定到ViewRootImpl之前所有提交的Runnable回调都会被保存在ViewRootImpl中的静态sRunQueues队列中。

// Activity展示过程源码
// View的post方法源码
public boolean post(Runnable action) {
    final AttachInfo attachInfo = mAttachInfo;
    if (attachInfo != null) {
        // 如果绑定了ViewRootImpl,直接投递到主线程中
        return attachInfo.mHandler.post(action); 
    }
   // 放到ViewRootImpl的静态sRunQueues对象中保存下来
    getRunQueue().post(action);
    return true;
}

public interface WindowManager extends ViewManager {}
// ViewRootImpl类
public class ViewRootImpl implements ViewParent {
// 静态的队列
static final ThreadLocal<HandlerActionQueue> sRunQueues = new ThreadLocal<HandlerActionQueue>();
}
// WindwoManager addView实现源代码
public void addView(View view, WindowManager.LayoutParams wparams)  {
	root = new ViewRootImpl(view.getContext(), display);
	view.setLayoutParams(wparams);
	mViews.add(view);
	mRoots.add(root);
	mParams.add(wparams);
	root.setView(view, wparams, panelParentView);
}

// ViewRootImpl setView实现源代码
public void setView(View view, WindowManager.LayoutParams params, 
ViewGroup panelParentView) {
	view.assignParent(this); // 设置ViewRootImpl为根节点
	requestLayout(); // 请求布局展示界面
}

在root.setView()方法中首先会对添加的view设置父元素为root对象,之后调用ViewRootImpl.requestLayout()请求布局方法。view对象在Activity中对应的就是DecorView根布局对象,在WindowManager直接添加界面示例中就是RulerView对象,它们的父元素都变成了ViewRootImpl对象,这也正是ViewRootImpl对象的名称由来,它是所有展示在屏幕上的视图树结构的根节点,从此时开始Activity内部的视图树就和ViewRootImpl对象绑定在一起,所有对视图的更新操作都会最终通知到ViewRootImpl对象。

继续跟踪requestLayout()方法,ViewRootImpl内部会包含创建它的线程也就是主线程mThread对象,检查执行checkThread()方法的线程是否和主线程相同,不相同就抛出CalledFromWrongThreadException异常并退出应用;ViewRootImpl继承的其他ViewParent接口里都会有checkThread()方法调用。在视图对象被WindowManager.addView()方法加入内部缓存并绑定ViewRootImpl对象之后,如果视图树内部有requestLayout()操作就会不断地要求父元素执行requestLayout(),最终调用到视图树的根节点ViewRootImpl.requestLayout(),此时就会执行子线程更新异常逻辑。假如视图View对象所在的视图树结构没有绑定ViewRootImpl最后的requestLayout()操作其实执行的是DecorView或者RulerView这类视图元素的requestLayout(),在子线程中操作视图根本不会执行线程检测操作,也就不会出现抛出异常并退出应用的问题。

// 请求重新布局过程源码
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.");
	}
}

顺着requestLayout()方法会调用到scheduleTraversals()方法,它首先为主线程的MessageQueue消息队列增加了同步屏障,再向Choreographer编舞者对象投递了 mTraversalRunnable回调对象,同步屏障和异步消息的出现确保了视图重新布局消息的及时响应,避免出现掉帧卡顿现象。

// 编舞者请求重新更新界面过程源码
void scheduleTraversals() {
	if (!mTraversalScheduled) {
		mTraversalScheduled = true;
        // 向消息队列投递同步屏障
		mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
		mChoreographer.postCallback(
				Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
	}
}
// Choreographer.java
private void postCallbackDelayedInternal(int callbackType,
            Object action, Object token, long delayMillis) {
    synchronized (mLock) {
        if (dueTime <= now) {
            scheduleFrameLocked(now); // 请求VSync信号
        } else {
           Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);
           msg.arg1 = callbackType;
           msg.setAsynchronous(true); // 异步消息,可以越过同步屏障
           mHandler.sendMessageAtTime(msg, dueTime);
        }
    }
}

关于同步屏障和异步消息请参考本人的博客,这里只讨论Choreographer编舞者对象,Android手机为了保持界面的流畅性会要求屏幕的刷新频率保持在60帧每秒,也就是差不多每16毫秒需要刷新一次屏幕内容,刷新的信号被称作VSync竖向同步信号,它是通过后台服务SurfaceFlinger服务提供的,Choreographer就是专门负责向SurfaceFlinger服务请求VSync竖向同步信号并且接受发送来的信号执行界面更新操作。下面的示例代码就能够检测每一帧执行的时间,从后面打印的日志结果可以看出正常情况下每16毫秒左右就会更新一次屏幕内容。

// Choreographer测试代码
Choreographer.getInstance().postFrameCallback(new Choreographer.FrameCallback() {
     private long lastUpdateTime = 0;
     @Override
      public void doFrame(long frameTimeNanos) {
          if (lastUpdateTime == 0) {
             lastUpdateTime = SystemClock.uptimeMillis();
          } else {
             Log.e(TAG, "frame cost time = " + (SystemClock.uptimeMillis() - lastUpdateTime));
             lastUpdateTime = SystemClock.uptimeMillis();
           }
           Choreographer.getInstance().postFrameCallback(this);
       }
});

//~ 运行结果
com.example.mytestproject E/ChoreographerActivity: frame cost time = 16
com.example.mytestproject E/ChoreographerActivity: frame cost time = 17
com.example.mytestproject E/ChoreographerActivity: frame cost time = 17
com.example.mytestproject E/ChoreographerActivity: frame cost time = 16
com.example.mytestproject E/ChoreographerActivity: frame cost time = 17

在Choreographer的postCallbackDelayedInternal()实现中会把回调对象消息设置为异步消息,前面代码中有发送过同步屏障对象,此时主线程消息队列里的同步消息都会被阻拦,主线程优先执行界面同步的异步请求。假设没有之前的同步屏障,所有的任务都是用同步消息,在VSync信号回调执行之前就需要等待主线程消息队列的所有消息执行完成,等待过程中由于手机屏幕不会被刷新,在用户看起来界面就发生了卡顿丢帧现象。

// Activity展示执行测量、布局和绘制
final class TraversalRunnable implements Runnable {
	@Override
	public void run() {
		doTraversal();
	}
}
	
void doTraversal() {
	if (mTraversalScheduled) {
		mTraversalScheduled = false;
        // 从消息队列移除同步屏障
		mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
		performTraversals();
	}
}
private void performTraversals() {
	View host = mView; 
	if (mFirst) {
		host.dispatchAttachedToWindow(mAttachInfo, 0);
		mFirst = false;
	}
	// 执行未完全展示时投递的Runnable事件
	getRunQueue().executeActions(mAttachInfo.mHandler);
	performMeasure(); // host.measure();
	performLayout(); // host.layout();
	performDraw(); // host.draw();
}

TraversalRunnable回调在异步消息中会被优先执行,它内部调用了doTraversal(),该方法内部会在执行performTraversals()之前移除同步屏障对象,确保在执行完操作后其他同步消息能够被处理。performTraversals()方法会在第一次执行时向视图树结构传递绑定到Window对象的回调,再执行getRunQueue().executeActions()也就是把视图树之前post()/postDelay()的回调发送到主线程消息队列中,接着执行视图树的测量布局和绘制操作。视图对象投递的消息在视图树测量、布局和绘制完成之后才会执行,因此在回调Runnable内部获取视图的宽高操作就成功了。

视图树的根布局host执行了measure()测量操作,在Activity中的根布局就是DecorView,它是ViewGroup类型会在测量自己的大小后遍历自己内部的子视图调用它们的child.measure()测量操作,它的子视图有些事ViewGroup类型又会调用孙视图的测量操作,直到视图树的叶子视图节点,这种从上到下的递归执行就保证了视图树内部所有视图都会执行测量操作;layout()和draw()布局与绘制操作与measure()相似也是从上到下的递归执行,保证视图树内所有视图都完成布局和绘制操作。
在这里插入图片描述
WindowManager展示像素尺子Demo


版权声明:本文为xingzhong128原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。