美文网首页Android 面试
从数据结构与算法以及设计模式角度去学习View的绘制流程

从数据结构与算法以及设计模式角度去学习View的绘制流程

作者: 郑土强ztq | 来源:发表于2020-03-15 19:17 被阅读0次

    0.前言

    很多小伙伴可能在学习view的绘制流程源码的时候有点抓不住重点,所以在分析代码的时候绕来绕去脑袋晕乎乎的。今天我就来给大家化繁为简,只关注它最核心的东西。从数据结构与算法还有设计模式的角度带领大家真正去掌握。我这篇文章旨在让大家能更深刻理解View绘制流程的设计,不涉及具体的细节。最好的效果是大家先看这篇文章,然后根据文中介绍的知识点去自行查看源码。或者感到吃力的话可以结合别的大牛写的文章去看源码。_

    1.先修知识点

    首先,View体系的数据结构就是树形结构。ViewGroup继承View,而且ViewGroup持有View的引用,所以这不就是一个树的节点嘛。数据结构跟他的算法是相关的,所以至少你要掌握树的遍历,尤其是树的先序遍历,也就是深度遍历。

    在view体系设计中也涉及到了几个设计模式,分别是组合模式,责任链模式,模板方法模式。(当然还有其他如观察者模式,适配器模式等等,不在这次的讨论范围。)

    1. 组合模式

      如果想实现一个树状的关系,那么就可以使用组合模式。如View和ViewGroup的关系,ViewGroup继承于View,同时也含有子View的引用集合。组合模式一般用于树形结构,所以在这里不需要展开。你只需要知道,View体系本身就是组合模式的体现。

    2. 责任链模式

      如果想实现一个调用可以让多个类都有机会去处理,那么可以使用责任链模式。类Node含有一个自己的引用,相当于一个链表指针,指向下一个节点。

      class Node {
          public String name;
          public Node next;
      
          public Node(String name) {
              this.name = name;
          }
      
          public void operate(int num) {
              //1.自己先来处理
              System.out.println(String.format("我是节点%s,我在处理:%d", name, num));
              //2.分发给下一节点处理
              if (next != null) {
                  next.operate(num);
              }
          }
      }
      
      public class Main {
          public static void main(String[] args) {
              Node[] nodes = new Node[5];
              Node head = nodes[0] = new Node("0");
              for (int i = 1; i < 5; i++) {//构造的链表为:0->1->2->3->4
                  nodes[i] = new Node(i + "");
                  head.next = nodes[i];
                  head = nodes[i];
              }
              head = nodes[0];
              head.operate(100);
          }
      }
      结果为:
      我是节点0,我在处理:100
      我是节点1,我在处理:100
      我是节点2,我在处理:100
      我是节点3,我在处理:100
      我是节点4,我在处理:100
      

      通过把每个处理者看成是链表上面的一个节点,实现一个调用可以分发给多个处理者去处理。

    3. 模板方法模式

      如果某一个功能逻辑的流程是比较固定的,但是有一定的步骤,那么可以通过模板方法模式把具体步骤交给子类去实现。

      这个怎么理解呢?以上面责任链模式为例,每个节点的operate的流程是固定的:1.自己处理消息,2.把消息分发给下一个节点。但是可以发现上面的例子有点鸡肋,因为每个Node节点的处理是完全一样的,这看起来没什么意义。好吧,那结合模板方法模式来进行一个改造。

      abstract class Node {
          public String name;
          public Node next;
      
          public Node(String name) {
              this.name = name;
          }
      
          public void operate(int num) {
              //1.自己先来处理
              int result = onOperate(num);
              System.out.println(String.format("处理的结果:%d", result));
              //2.分发给下一节点处理
              if (next != null) {
                  next.operate(result);
              }
          }
      
          //抽象方法,具体的处理交给子类根据自己的需求去实现
          protected abstract int onOperate(int num);
      }
      

      可以看到把原先自己直接处理的逻辑抽成了一个抽象函数,这样子类就必须去实现onOperate方法去做自己的处理逻辑。假设有这样的需求:实现三个节点,一个是进行+1的操作;一个是进行-1的操作;一个是乘2的操作。

      class AddNode extends Node {
          public AddNode() {
              super("加法器");
          }
      
          @Override
          protected int onOperate(int num) {
              System.out.println(String.format("我是%s,我将对%d进行加1操作", name, num));
              return num + 1;
          }
      }
      
      class MinusNode extends Node {
          public MinusNode() {
              super("减法器");
          }
      
          @Override
          protected int onOperate(int num) {
              System.out.println(String.format("我是%s,我将对%d进行减1操作", name, num));
              return num - 1;
          }
      }
      
      class MultiNode extends Node {
          public MultiNode() {
              super("乘法器");
          }
      
          @Override
          protected int onOperate(int num) {
              System.out.println(String.format("我是%s,我将对%d进行乘2操作", name, num));
              return num * 2;
          }
      }
      
      public class Main {
          public static void main(String[] args) {
              int num = 100;
              //做运算:(num+1)*2-1 = 201
              Node add = new AddNode();
              Node minus = new MinusNode();
              Node multi = new MultiNode();
      
              add.next = multi;
              multi.next = minus;
      
              add.operate(num);
          }
      }
      结果为:
      我是加法器,我将对100进行加1操作
      处理的结果:101
      我是乘法器,我将对101进行乘2操作
      处理的结果:202
      我是减法器,我将对202进行减1操作
      处理的结果:201
      

      模板方法模式体现在:因为每个节点具体的消息处理逻辑是不一样的,通过把operate流程固定,把消息处理逻辑写成抽象函数onOperate交给节点子类去实现。这样不同的节点就可以做不同的处理了。

      发现一个彩蛋了没?onOperate函数怎么那么熟悉?

      先来想想经常接触到的onXXX方法:onCreate,onMeasure,onInterceptTouchEvent……没错,事实上掌握了这几个设计模式,很多时候源码的阅读都会很流畅了。如触摸事件分发,View绘制的三大过程,Activity生命周期回调,AsyncTask...等等的机制和原理。推荐大家一定要找时间深入研究,成体系地学习一下设计模式。这是高级工程师架构设计必备技能。

    4. 树的遍历

      为了真正关注核心点而不被其他的东西干扰带偏,所以我假定View树是一个二叉树,或者说我选取一个二叉树View树来进行分析。

      首先来回顾一下树的遍历(递归版):

    树的例子.png
    class Node{
        int id;
        Node left;
        Node right;
    
        public Node(int id) {
            this.id = id;
        }
    }
    
    public class Main {
        public static void main(String[] args) {
            Node[] nodes = new Node[8];
            for (int i = 0; i < 8; i++) {
                nodes[i] = new Node(i);
            }
            nodes[0].left = nodes[1];
            nodes[0].right = nodes[2];
            nodes[1].left = nodes[3];
            nodes[2].left = nodes[4];
            nodes[2].right = nodes[5];
            nodes[3].left = nodes[6];
            nodes[3].right = nodes[7];
            dfs(nodes[0]);
        }
    
        private static void dfs(Node root) {
            if (root == null) {
                return;
            }
            System.out.println(root.id);
            if (root.left != null) {
                dfs(root.left);
            }
            if (root.right != null) {
                dfs(root.right);
            }
        }
    }
    结果为:
    0
    1
    3
    6
    7
    2
    4
    5
    

    相信很多人都能写出上面的深度遍历代码,but,这显然不够“java”,严格来说这是c语言形式的写法,只是把节点看成是数据实体,不那么面向对象。那好,我们来实现更加面向对象的深度遍历写法。

    面向对象也就是类里面有数据也有行为,那我们就把遍历的行为交给类去做。说白了就是把dfs函数写成成员函数。

    class Node {
        int id;
        Node left;
        Node right;
    
        public Node(int id) {
            this.id = id;
        }
    
        //把dfs写成成员函数
        public void dfs() {
            System.out.println(this.id);
            if (left != null) {
                left.dfs();
            }
            if (right != null) {
                right.dfs();
            }
    
        }
    }
    
    public class Main {
        public static void main(String[] args) {
            Node[] nodes = new Node[8];
            for (int i = 0; i < 8; i++) {
                nodes[i] = new Node(i);
            }
            nodes[0].left = nodes[1];
            nodes[0].right = nodes[2];
            nodes[1].left = nodes[3];
            nodes[2].left = nodes[4];
            nodes[2].right = nodes[5];
            nodes[3].left = nodes[6];
            nodes[3].right = nodes[7];
            //dfs(nodes[0]);
            nodes[0].dfs();
        }
    }
    结果为:
    0
    1
    3
    6
    7
    2
    4
    5
    

    好了,树的遍历主要是想说明Java版的面向对象的写法。因为我在百度随意搜索了一下,发现基本都是用c语言版本的写法来写的。

    2.View的Measure流程的核心

    前面洋洋洒洒写了那么多,现在终于可以应用啦。理解上面的知识点能让你更加容易理解复杂的View的Measure流程。

    为了真正关注核心点而不被其他的东西干扰带偏,所以我假定View树是一个二叉树,或者说我选取一个二叉树View树来进行分析。

    测量就是计算每个View的大小,先来定义View类。

    abstract class View {
        int id;
        int width;
        int height;
        View left;
        View right;
    
        public View(int id) {
            this.id = id;
        }
    
        final public void measure(int width, int height) {
            //1.具体如何测量交给子类决定
            onMeasure(width, height);
        }
    
        //设置测量值
        public void setMeasuredDimension(int w, int h) {
            width = w;
            height = h;
            System.out.println(String.format("%d的测量结果是w=%d,h=%d", id, width, height));
        }
    
        protected abstract void onMeasure(int width, int height);
    }
    

    很简陋的一个类,但是包含了最基本的要素了。measure方法里就用了模板方法模式,把具体如何测量交给子类实现。而且用final关键字,所以子类不能覆写measure,也就是说measure方法的流程不让改动。

    注意:下文子节点是指View树的子节点,父节点是指View树的父节点,注意跟父类子类区分开。这是两回事来的。

    好了,再来实现两个子类,不妨就叫TextView,ImageView。TextView具体的测量就是把父节点传递过来的值减去10,而ImageView是减去20。

    class TextView extends View {
    
        public TextView(int id) {
            super(id);
        }
    
        @Override
        protected void onMeasure(int width, int height) {
            int myW = width - 10;
            int myH = height - 10;
            setMeasuredDimension(myW, myH);
            //去测量子节点
            if (left != null) {
                left.measure(myW, myH);
            }
            if (right != null) {
                right.measure(myW, myH);
            }
        }
    }
    
    class ImageView extends View {
    
        public ImageView(int id) {
            super(id);
        }
    
        @Override
        protected void onMeasure(int width, int height) {
            int myW = width - 20;
            int myH = height - 20;
            setMeasuredDimension(myW, myH);
            //去测量子节点
            if (left != null) {
                left.measure(myW, myH);
            }
            if (right != null) {
                right.measure(myW, myH);
            }
        }
    }
    

    大家可以看到,子节点的测量也是交给子类去负责分发测量了。跟之前讨论模板方法模式时有点不同,但是本质上是一样的。只是模板方法模式的例子是父类负责分发,这里是子类分发。

    简陋View树(含测量结果).png

    构造上图的View树进行测试。

    public class Main {
        public static void main(String[] args) {
            View decorView = new ImageView(0);
            View imageView1 = new ImageView(1);
            View imageView2 = new ImageView(2);
            View textView3 = new TextView(3);
            View textView4 = new TextView(4);
    
            decorView.left = imageView1;
            decorView.right = imageView2;
            imageView1.left = textView3;
            imageView1.right = textView4;
    
            //获取window窗口大小(一般是手机屏幕大小),假设是1080x1920
            int windowW = 1080;
            int windowH = 1920;
            decorView.measure(windowW, windowH);
        }
    }
    结果为:
    0的测量结果是w=1060,h=1900
    1的测量结果是w=1040,h=1880
    3的测量结果是w=1030,h=1870
    4的测量结果是w=1030,h=1870
    2的测量结果是w=1040,h=1880
    

    根据上图和运行结果可知,View的测量是深度遍历的。测量到一个节点时,这个节点负责去发起子节点的测量,这是责任链模式;而为了把具体测量实现交给子类,使用了模板方法模式。

    3.更进一步

    有的小伙伴可能说了,你这个跟Android实际的View代码出入有点大啊,你看都没有体现出View跟ViewGroup呢!好吧,那我们来实现更加贴近Android的代码实现吧。

    更进一步的例子.png
    public class Main {
        public static void main(String[] args) {
            LinearLayout linearLayout0 = new LinearLayout(0);
            LinearLayout linearLayout1 = new LinearLayout(1);
            TextView textView2 = new TextView(2);
            LinearLayout linearLayout3 = new LinearLayout(3);
            LinearLayout linearLayout4 = new LinearLayout(4);
    
            linearLayout0.left = linearLayout1;
            linearLayout0.right = textView2;
            linearLayout1.left = linearLayout3;
            linearLayout1.right = linearLayout4;
    
             //获取window窗口大小,假设是1080x1920
            int windowW = 1080;
            int windowH = 1920;
            linearLayout0.measure(windowW,windowH);
    
        }
    }
    
    class View {
        int id;
        int width;
        int height;
    
        public View(int id) {
            this.id = id;
        }
    
        final public void measure(int width, int height) {
            //1.具体如何测量交给子类决定
            onMeasure(width, height);
        }
    
        //设置测量值
        public void setMeasuredDimension(int w, int h) {
            width = w;
            height = h;
            System.out.println(String.format("%d的测量结果是w=%d,h=%d", id, width, height));
        }
    
        protected void onMeasure(int width, int height) {
            //默认实现为直接设置父类传递过来的参数
            setMeasuredDimension(width, height);
        }
    
    }
    
    class ViewGroup extends View {
        public ViewGroup(int id) {
            super(id);
        }
    
        //ViewGroup才有子View
        View left;
        View right;
    
        @Override
        protected void onMeasure(int width, int height) {
            //默认实现为把width,height减去50作为自己的参数
            int myW = width - 50;
            int myH = height - 50;
            setMeasuredDimension(myW, myH);
            //发起子节点的测量
            if (left != null) {
                left.measure(myW, myH);
            }
            if (right != null) {
                right.measure(myW, myH);
            }
        }
    }
    
    //View的子类没有子节点,只需要关心自己的测量
    class TextView extends View {
        public TextView(int id) {
            super(id);
        }
    
        //实现自己的测量逻辑,把width,height减去10
        @Override
        protected void onMeasure(int width, int height) {
            setMeasuredDimension(width - 10, height - 10);
        }
    }
    
    //ViewGroup的子类有子节点,需要发起子节点的测量
    class LinearLayout extends ViewGroup {
        public LinearLayout(int id) {
            super(id);
        }
    
        //把width,height减去30作为自己的参数
        @Override
        protected void onMeasure(int width, int height) {
            int myW = width - 30;
            int myH = height - 30;
            setMeasuredDimension(myW, myH);
            //负责发起子节点的测量,这里实现为先测量右节点再测量左节点
            if (right != null) {
                right.measure(myW, myH);
            }
            if (left != null) {
                left.measure(myW, myH);
            }
        }
    }
    结果为:
    0的测量结果是w=1050,h=1890
    2的测量结果是w=1040,h=1880
    1的测量结果是w=1020,h=1860
    4的测量结果是w=990,h=1830
    3的测量结果是w=990,h=1830
    

    重要的点都在代码上注释了。

    可以看到,之前的遍历顺序是01342,现在是02143了,因为LinearLayout是先进行右节点的测量。

    4.总结

    View的体系设计用到了许多设计模式,这里主要是责任链模式和模板方法模式,理解设计模式能更加容易读懂源码。

    View的遍历是深度遍历,需要掌握Java版的实现。

    layout以及draw流程的核心也差不多也是这样,大家跟着我说的去分析源码效果更好。注意子节点的draw流程直接由父类发起了,子类只需要在onDraw中绘制自己的内容即可。

    文中关注的重点在于如何实现一颗View树的测量过程。还有很多细节没有涉及,例如MeasureSpec。实际上View最终测量结果是结合我们在xml自己定义的参数和父View自己的参数去决定的。

    小提示:大家在看递归代码时可以结合画一下调用栈去分析。

    如有写得不准确的地方,欢迎交流指正。_

    相关文章

      网友评论

        本文标题:从数据结构与算法以及设计模式角度去学习View的绘制流程

        本文链接:https://www.haomeiwen.com/subject/lonmehtx.html