Android之View的诞生之谜

文章独家授权公众号:码个蛋
更多分享:http://www.cherylgood.cn

前言

  • hello,大家好,平时大家都说自定义view,这次给大家带来有关view的相关知识,希望你喜欢!
  • 作为一名正在岗位上的Android开发者,工作中常常需要我们使用自定义View去实现一些天马行空的效果,而作为一名正在寻找工作的Android开发者而言,面试过程中自定义View的相关知识点也是热门的面试题目之一哦,好东西我们怎么能错过呢;
  • 之前我们在上一篇Android Touch事件分发机制详解之由点击引发的战争中讲述View的事件分发机制,在里面也讲了很多与View相关的知识点。
  • 作为Android开发者,我们应该不断的丰富自身的知识体系结构,加强Android开发内功的修炼(个人看法:学习Android内部底层一些的知识,可视为内功。而对于api的灵活使用,可视为招式)。
  • 本次我们将来探索自定义View的内功心法之自定义View的死亡三部曲:测量、布局、绘制。
  • 在了解死亡三部曲之前,我们先从上层的视角看下死亡三部曲的执行流程。

我们在了解死亡三部曲之前,先了解下我们activity的布局文件是如何被加载的。

  • 我们的activity中的视图是什么时候被加载的呢?有个方法你肯定会很眼熟:setContentView(R.layout.main);其实我们的activity就是通过这个方法加载我们的布局文件进行视图的渲染。那么我们就从他入手吧。

  • 我们进入setContentView(R.layout.main)的源码看一下,注意代码中的注视:

    public void setContentView(@LayoutRes int layoutResID) {
        //1、调用getWindow().setContentView(layoutResID);
        //  加载我们的布局资;getWindow实际上是调用了phoneWindow
      getWindow().setContentView(layoutResID);
        //2、
      initWindowDecorActionBar();
      }
    
  • window是什么东东?window是一个抽象类,他只有一个实现类,那就是phoneWindow,phoneWindow是android系统中窗口的顶级类,之前在Android Touch事件分发机制详解之由点击引发的战争有讲到,不了解的可以看下。


  • 我们接着看 getWindow().setContentView(layoutResID);

     @Override  
    public void setContentView(int layoutResID) {  
    //在渲染布局资源前做一些前期准备工作
    //1、 判断mContentParent是否为null,mContentParent其实
    // 是负责加载我们页面内容的容器,后面我们会讲到
    if (mContentParent == null) {  
    installDecor();  
    } else {  
      //1、如果不为null,说明原来页面上已经有内容了,
      //  所以我们要移除所有的内容,后面再加载新的内容上去
    mContentParent.removeAllViews();  
    }  
      //调用mLayoutInflater来根据我们的布局资源id渲染视图
    mLayoutInflater.inflate(layoutResID, mContentParent);  
    .....
    }  
    
  • 在 渲染我们的布局文件前,先调用了installDecor()来初始化mContentParent,之前也说mContentParent是负责加载我们页面内容的容器,到底是不是呢?我们看下installDecor源码便知道了:

     private void installDecor() {
    //mDecor是window下的一个内部类,你可以理解成他是window用来填充视图的容器
    if (mDecor == null) {
    //1、通过 mDecor = generateDecor(); 实例化了DecorView,
    //  而DecorView则是PhoneWindow类的一个内部类,继承于
    //  FrameLayout;
      mDecor = generateDecor(); 
    mDecor.setDescendantFocusability(
    ViewGroup.FOCUS_AFTER_DESCENDANTS);
      mDecor.setIsRootNamespace(true);
      if (!mInvalidatePanelMenuPosted &&
     mInvalidatePanelMenuFeatures != 0) {     
    mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
      }
    }
    if (mContentParent == null) {
    //2、通过传入mDecor来初始化mContentParent
      mContentParent = generateLayout(mDecor);
      ...
      } 
    }
    }
    
  • 从2处我们看到mContentParent被创建,那么它是如何被创建的呢,他真的是如我们前面所说负责加载内容部分的父容器么?我们来一探究竟,我们看 mContentParent = generateLayout(mDecor)的源码:
    protected ViewGroup generateLayout(DecorView decor) {
    // 1、获得系统当前的style
    TypedArray a = getWindowStyle();
    ...
    if (a.getBoolean(R.styleable.Window_windowNoTitle, false)) {
    //2、如果style是Window_windowNoTitle是true,
    //说明当前的style是没有标题部分的,则请求移除标题
    requestFeature(FEATURE_NO_TITLE);
    } else if (a.getBoolean(R.styleable.Window_windowActionBar, false)) {
    // 3、同样,检查是否需要显示系统的ActionBar
    requestFeature(FEATURE_ACTION_BAR);
    }
    ...
    //4、下面开始初始化我们的mContentParent了
    int layoutResource;
    int features = getLocalFeatures();
    if ((features & (1 << FEATURE_SWIPE_TO_DISMISS)) != 0) {
    layoutResource = R.layout.screen_swipe_dismiss;
    } else if(...){
    ...
    }

      //6、这句就把我们的contentParent实例化了,
      // 这就是我们PhoneWindow. DecorView下的一个
      // view,该view包含了两个子view,一个是装在状
      // 态栏的,一个是我们的布局文件。
      View in = mLayoutInflater.inflate(layoutResource, null);  
      decor.addView(in, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT)); 
      mContentRoot = (ViewGroup) in;
      //7、很熟悉的findViewById是不是?ID_ANDROID_CONTENT定位的其实就是内容不问的布局容器了
      ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
      if (contentParent == null) {
          throw new RuntimeException("Window couldn't find content container view");
      }
      ...
      return contentParent;
    }
    
  • 小小的发现:从上面的代码我们可以解释很多开发中的技巧,看下面的代码,在加载我们的资源文件前,他就检查了FEATURE_ACTION_BAR和FEATURE_NO_TITLE属性,所以我们想让activity全屏或者没有actionBar的话,必须在setContentView调用之前设置。


  • 接下来我们回到前面
    setContentViewgetWindow().setContentView(layoutResID);方法,继续看mLayoutInflater.inflate(layoutResID, mContentParent); 这个方法 mContenParent我们已经知道是什么了,然后通过mLayoutInflater.inflate,我们的布局就被渲染出来了。

  • DecorView补充: DecorView是整个ViewTree的最顶层View,我们之前分析过她是是个FrameLayout布局,代表了整个应用的界面。在该布局下面,有标题view和内容view这两个子元素,而内容view则是上面提到的mContentParent。如下图:


    DecorView.png
  • 小结:调用setContentView方法,实例化了DecorView, DecorView有两个子布局,一个是加载顶部状态栏的,一个是加载我们的内容布局的,activity添加的xml就是内容布局的一个字元素


  • 到目前为止,通过setContentView实例化了DecorView并且加载了设置进来的布局文件。然后,并没有发现任何与测量、布局、绘制相关的点,可能你会想,我们不会搞错了吧,其实没有哦,你们想想,setContentView实在,既然还是不可见的,那我为什么要耗费资源去测量呢,你最终能不能露个脸还说不准呢。亏本的买卖咱不干。其实要想知道什么时候开始执行测量等工作,我们可以看下ActivityThread的源码,ActivityThread是android用来管理activity的,这家伙知道的肯定多一些。那么我们就来了解下ActivityThread的执行流程。

  • 首先ActivityThread通过调用handleLaunchActivity启动我们的目标activity,

      private performLaunchActivity (ActivityClientRecord r,Intent customIntent{
      ......
      activity.mCalled = false;
              //1、下面调用了Activity的onCreate方法
              if (r.isPersistable()) {
                  mInstrumentation.callActivityOnCreate(activity, r.state, r.persistentState);
            } else {
                 mInstrumentation.callActivityOnCreate(activity, r.state);
            }
              if (!activity.mCalled) {
                throw new SuperNotCalledException(
                     "Activity " + r.intent.getComponent().toShortString() +
                    " did not call through to super.onCreate()");
            }
      
    }
    
  • 也就是说在performLaunchActivity调用之后,activity的onCreate被调用,我们的资源文件不加载,但是此时还是不可见的,也就还没有进行侧脸之类的事情。

  • 然后我们继续看ActivityThread.handleResumeActivity的源码:
    final void handleResumeActivity(IBinder token, boolean clearHide, boolean isForward) {
    ......
    //1、可以看到,这里执行了activity的onResume方法
    ActivityClientRecord r = performResumeActivity(token, clearHide);
    if (r != null) {
    final Activity a = r.activity;
    .......
    if (r.window == null && !a.mFinished && willBeVisible) {

           // 2、获得window对象
          r.window = r.activity.getWindow();
    
          //3、 从window中获取DecorView对象
          View decor = r.window.getDecorView(); 
          decor.setVisibility(View.INVISIBLE);
    
          //4、从activity中获得与之关联的windowManager对象
          ViewManager wm = a.getWindowManager(); 
          WindowManager.LayoutParams l = r.window.getAttributes();
          a.mDecor = decor;
          l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
          l.softInputMode |= forwardBit;
          if (a.mVisibleFromClient) {
              a.mWindowAdded = true;
            //5、终于找到你了,这里将decor与WindowManager关联上,也就是将我们的decor正式
    //添加到window中,
              wm.addView(decor, l); 
          }
          ......
      }
    }
    }
    

知识补充:

  • Window是一个抽象的概念,一个Window对应一个View和一个ViewRootImpl;
  • Window和View是通过ViewRootImpl联系起来的。
  • ViewRootImpl才是一个View真正实现的动作。
  • WindowManager中也有一个WindowManagerImpl作为实现的类,负责具体的操作。

  • 跟到这里,我们来总结一下,activity启动过程中,在执行handleResumeActivity时将我们的顶层视图DecorView通过WindowManager挂载到window中。

  • 而WindowManager是个接口类,那么我们看看其实类对象WindowManagerImpl.addView方法

     public void addView(View view, ViewGroup.LayoutParams params) {
      //1、这里通过mGlobal调用addView进行添加,而mGlobal是什么呢?
      mGlobal.addView(view, params, mDisplay, mParentWindow);
    }
    
  • mGlobal其实是WindowManagerGlobal的一个内部实例,接着看WindowManagerGlobal.addView的源码:

    public void addView(View view, ViewGroup.LayoutParams params,
          Display display, Window parentWindow) {
      ......
      //注意这个对象
      ViewRootImpl root;
      View panelParentView = null;
    
      synchronized (mLock) {
          ......
          //1、通过DecorView获得上下文以及传入display实例化一个ViewRootImpl对象
        //也就是说ViewRootImpl与DecorView关联起来了
          root = new ViewRootImpl(view.getContext(), display); 
          view.setLayoutParams(wparams);
          mViews.add(view);
          mRoots.add(root);
          mParams.add(wparams);
      }
      try {
        //2、这里调用了ViewRootImpl的setView方法,将DecorView与ViewRootImpl产生来关联。
          root.setView(view, wparams, panelParentView); 
      } catch (RuntimeException e) {
          synchronized (mLock) {
              final int index = findViewLocked(view, false);
              if (index >= 0) {
                  removeViewLocked(index, true);
              }
          }
          throw e;
      }
    }
    
  • 我们继续看ViewRootImpl.setView方法的源码

    public final class ViewRootImpl implements ViewParent,  
    public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {  
      synchronized (this) {  
          if (mView == null) {  
              mView = view;  
              ......  
              if (view instanceof RootViewSurfaceTaker) {  
                //1、这里会向系统发出申请,接管屏幕视图的渲染工作
                  mSurfaceHolderCallback = 
                 ((RootViewSurfaceTaker)view).willYouTakeTheSurface();  
                  if (mSurfaceHolderCallback != null) {  
                      mSurfaceHolder = new TakenSurfaceHolder();  
                      mSurfaceHolder.setFormat(PixelFormat.UNKNOWN);  
                  }  
              }  
    
          //2、这里,我们看到了很熟悉的一个方法,这就是绘制我们的view的入口了
            requestLayout();
              ......  
    
              try {
                  mOrigWindowType = mWindowAttributes.type;
                  mAttachInfo.mRecomputeGlobalAttributes = true;
                  collectViewAttributes();
                //3、通过WindowSession来完成Window的添加过程这是一个IPC的过程,这里就不在深入了。
                  res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
                          getHostVisibility(), mDisplay.getDisplayId(),
                          mAttachInfo.mContentInsets, mInputChannel);
              } catch (RemoteException e) {
                  mAdded = false;
                  mView = null;
                  mAttachInfo.mRootView = null;
                  mInputChannel = null;
                  mFallbackEventHandler.setView(null);
                  unscheduleTraversals();
                  setAccessibilityFocus(null, null);
                  throw new RuntimeException("Adding window failed", e);
              } finally {
                  if (restore) {
                      attrs.restore();
                  }
              }
    
              ......  
          }  
      }  
    }  
    
    ......  
    }  
    
  • setView完成的工作很多,如声明输入事件的管道,DisplayManager的注册,view的绘画,window的添加等等

  • 作为绘制view的入口,我们来看下requestLayout方法
    @Override
    public void requestLayout() {
    if (!mHandlingLayoutInLayoutRequest) {
    checkThread();
    mLayoutRequested = true;
    //1 、很开心,开始调度进行绘制流程了
    scheduleTraversals();
    }
    }

  • ViewRootImpl.scheduleTraversals()调用后,系统会发起一个异步消息,然后在异步消息执行过程中调用performTraversals()完成具体的View树遍历;

  • 小子,总算是找到你了,我们来看下胜利的果实吧!

     private void performTraversals() {
          ...
      if (!mStopped) {
        //1、获取顶层布局的childWidthMeasureSpec
          int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);  
        //2、获取顶层布局的childHeightMeasureSpec
          int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
          //3、测量开始测量
          performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);       
          }
      } 
    
      if (didLayout) {
        //4、执行布局方法
          performLayout(lp, desiredWindowWidth, desiredWindowHeight);
          ...
      }
      if (!cancelDraw && !newSurface) {
       ...
     //5、开始绘制了哦
              performDraw();
          }
      } 
    

总结:

  • 通过上面内容,我们学到了一些小技巧,如移除状态栏的一些步骤,之前我们可能知道,嗯,是的,要在setContentView前调用requestFeature才可以,通过这次分析,我们之前可能是知道要这样子做才行,现在我们知道了为什么要这样子做。是不是写起代码来更踏实了呢?
  • 通过这次分析,我们对于activity的创建流程也略知一二,希望对你有帮助
  • 测量、布局、绘制的工作我们放到下一章节进行学习
  • 如果你看到这里,我要对你说声谢谢,非常感谢你能看完这篇文章
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,117评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,328评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,839评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,007评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,384评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,629评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,880评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,593评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,313评论 1 243
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,575评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,066评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,392评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,052评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,082评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,844评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,662评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,575评论 2 270

推荐阅读更多精彩内容