Android視圖加載到顯示(基於API 29)分析

Activity視圖從創建到顯示

就算是鹹魚也要做最鹹的那條。

沒錯,入口點當然是onCreate()中的SetContentView(R.layout.xxx)

  • 調用mWindow.setContentView(R.layout.xxx),mWindow是Activity被創建時在attach()中創建的PhoneWindow對象.
  • PhoneWindow中生成DecorView,具體是new了一個DecorView,這個DecorView是一個FrameLayout,即viewGroup.
  • ViewGroup mContentParent = decorView.findViewById(com.android.internal.R.id.content)
  • 下一步 LayoutInflate.inflate(R.layout.xxx,mContentParent),成功將我們傳入的佈局,加入到名爲content的這個佈局之中。

LayoutInflate是如何工作的?或者說LayoutInflate.inflate(R.layout.xx,root,true/false),這幾個參數有啥效果?

  • LayoutInflate是如何創建的?

    • mLayoutInflater = LayoutInflater.from(context);

        //可見,LayoutInflate是一個服務,因爲要加載app裏面的資源,當然需要用服務去搞事情。
        public static LayoutInflater from(Context context) {
        LayoutInflater LayoutInflater =
                (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        if (LayoutInflater == null) {
            throw new AssertionError("LayoutInflater not found.");
        }
        return LayoutInflater;}
      
  • 常見的添加布局是如何搞的?

    • mLayoutInflater.inflate(layoutResID, mContentParent);

       public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) {
       //這邊傳入的就是 id,ViewParent,true.
       return inflate(resource, root, root != null); }
      
       final Resources res = getContext().getResources();
       XmlResourceParser parser = res.getLayout(resource);
         try {
      		 return inflate(parser, root, attachToRoot);
        } finally {
       	  parser.close();
        }
        //這個res的實現是ResoursesImpl,其中使用了AssertManager去獲取這個佈局,先確定這個佈局資源是存在的,然後,加載這個資源佈局
        XmlResourceParser parser = res.getLayout(resource);
      
       final ResourcesImpl impl = mResourcesImpl;
       impl.getValue(id, value, true);
       if (value.type == TypedValue.TYPE_STRING) {
           return impl.loadXmlResourceParser(value.string.toString(), id,
                   value.assetCookie, type);
      
       //loadXmlResourceParser
       //load這個xml資源的時候,ResourcesImpl中有一個大小爲4的數組,用於緩存
       //native 方法去尋找這個資源佈局
           final XmlBlock block = mAssets.openXmlBlockAsset(assetCookie, file);
               if (block != null) {
       			//緩存處理
                   final int pos = (mLastCachedXmlBlockIndex + 1) % num;
      
  • inflate(parser, root, attachToRoot); 已經找到這個資源文件,並且轉換成XmlResourceParser,下一步,inflate

      public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
      synchronized (mConstructorArgs) {
          final AttributeSet attrs = Xml.asAttributeSet(parser);
          View result = root;
          try {
              advanceToRootNode(parser);
              final String name = parser.getName();
              if (TAG_MERGE.equals(name)) {
                  rInflate(parser, root, inflaterContext, attrs, false);
              } else {
                  // Temp is the root view that was found in the xml
      			//這一步主要是生成根View
                  final View temp = createViewFromTag(root, name, inflaterContext, attrs);
                  ViewGroup.LayoutParams params = null;
    
                  if (root != null) {
      				//root不爲空,那麼就獲取到傳入root的佈局屬性
                      // Create layout params that match root, if supplied
                      params = root.generateLayoutParams(attrs);
                      if (!attachToRoot) {
                          // Set the layout params for temp if we are not
                          // attaching. (If we are, we use addView, below)
                          temp.setLayoutParams(params);
                      }
                  }
      			//開始解析指定佈局的xml文件
                  rInflateChildren(parser, temp, attrs, true);
    
                  // We are supposed to attach all the views we found (int temp)
                  // to root. Do that now.
                  if (root != null && attachToRoot) {
      				//佈局文件生成的View添加進根Root
                      root.addView(temp, params);
                  }
      			// 僅僅是根據佈局文件生成View,就返回這個View
                  if (root == null || !attachToRoot) {
                      result = temp;
                  }
              }
    
          return result;
      }
    

    }

  • rInflateChildren() 調用了rInflate()
          
      void rInflate(XmlPullParser parser, View parent, Context context,
          AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {
    
      final int depth = parser.getDepth();
      int type;
      boolean pendingRequestFocus = false;
      //解析tag的循環
      while (((type = parser.next()) != XmlPullParser.END_TAG ||
              parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {
    
          if (type != XmlPullParser.START_TAG) {
              continue;
          }
    
          final String name = parser.getName();
    
     		 ...else if (TAG_INCLUDE.equals(name)) {
              if (parser.getDepth() == 0) {
                  throw new InflateException("<include /> cannot be the root element");
      		//解析我們經常使用的include標籤
      			 parseInclude(parser, context, parent, attrs);
              }
              else{
      		//解析xml文件裏面的控件
              final View view = createViewFromTag(parent, name, context, attrs);
              final ViewGroup viewGroup = (ViewGroup) parent;
              final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
      		//本身外層是while循環,而這下一句,產生遞歸效果,因爲,可能這個節點是一個ViewGroup,那麼就需要進去遍歷
              rInflateChildren(parser, view, attrs, true);
              viewGroup.addView(view, params);
          }
      }	
      
      //注意這個onFinishInflate(),它代表了佈局解析完畢,自定義ViewGroup,有時就會用到這個方法。
      if (finishInflate) {
          parent.onFinishInflate();
      }
    

    }

  • createViewFromTag()

       View view = tryCreateView(parent, name, context, attrs);
      //以下爲tryCreateView()的代碼
      if (mFactory2 != null) {
      	//這個Factory爲LayoutInflate的一個接口,返回的是個View,也就是更具名字生成View,具體怎麼生成,這個過程交給了這個工廠,我們去找一下在哪裏實現的
          view = mFactory2.onCreateView(parent, name, context, attrs);
      } else if (mFactory != null) {
          view = mFactory.onCreateView(name, context, attrs);
      } else {
          view = null;
      }
      return view;}
    
  • 尋找Factory在哪裏初始化進來的

      //同時我們在LayotInflate中還發現了setFactory()和setFactory2()來設置的方法。
      public void setFactory2(Factory2 factory) {
      //Factory只能設置一個
      if (mFactorySet) {
          throw new IllegalStateException("A factory has already been set on this LayoutInflater");
      }
      if (factory == null) {
          throw new NullPointerException("Given factory can not be null");
      }
      mFactorySet = true;
    
  • 找了不一會兒,我們發現AppCompatActivity,裏面的:

       @Override
      protected void onCreate(@Nullable Bundle savedInstanceState) {
      final AppCompatDelegate delegate = getDelegate();
      //這裏installViewFactory()
      delegate.installViewFactory();
      delegate.onCreate(savedInstanceState);
      //在onCreate之前設置Factory, 所以,你想根據自己的規則創建View,你需要在onCreate()的super之前設置就沒問題了。
      super.onCreate(savedInstanceState);
      }
    
       @Override
       public void installViewFactory() {
      LayoutInflater layoutInflater = LayoutInflater.from(mContext);
      if (layoutInflater.getFactory() == null) {
      	//爲空才設置,也就是我們自己可以創建自己的Factory,自己去創建View
          LayoutInflaterCompat.setFactory2(layoutInflater, this);
      } else {
          if (!(layoutInflater.getFactory2() instanceof AppCompatDelegateImpl)) {
              Log.i(TAG, "The Activity's LayoutInflater already has a Factory installed"
                      + " so we can not install AppCompat's");
          	}}}
    
      public AppCompatDelegate getDelegate() {
      if (mDelegate == null) {
      //創建了 AppCompatDelegateImpl
          mDelegate = AppCompatDelegate.create(this, this);
      }
      return mDelegate;
      }
      //而這個AppCompatDelegateImpl實現了LayoutInflater.Factory2接口
       /**
       * From {@link LayoutInflater.Factory2}.
      */
      @Override
      public final View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
          return createView(parent, name, context, attrs);
      }
    
  • onCreateView中創建了一個類:AppCompatViewInflate,並且 mAppCompatViewInflater.createView(),來到這個createView看一看:

        switch (name) {
          case "TextView":
      		// new AppCompatTextView(context, attrs),new 出來了我們需要用的TextView
              view = createTextView(context, attrs);
              verifyNotNull(view, name);
              break;
          case "ImageView":
              view = createImageView(context, attrs);
              verifyNotNull(view, name);
              break;
          case "Button":
              view = createButton(context, attrs);
              verifyNotNull(view, name);
              break;
          case "EditText":
              view = createEditText(context, attrs);
              verifyNotNull(view, name);
              break;
      ........
      //創建我們的自定View,所以自定義View需要寫上全路徑,因爲需要用到反射。
      if (view == null && originalContext != context) {
          view = createViewFromTag(context, name, attrs);
      }
    
      if (view != null) {
          // If we have created a view, check its android:onClick
          checkOnClickListener(view, attrs);
      }
    
      return view;
    

那麼解釋我們經常使用LayoutInfalte.inflate(R.layout.xxx,root,true/faalse)第二個參數和第三個參數的意義下面這幾行代碼就差不多了:

   // We are supposed to attach all the views we found (int temp)
  // to root. Do that now.
  	if (root != null && attachToRoot) {
         root.addView(temp, params);
     }
// Decide whether to return the root that was passed in or the
// top view found in xml.
  if (root == null || !attachToRoot) {
          result = temp;}

到此,我們的setContView就差不多了,

  • 分析了傳入佈局
  • 生成DecorView
  • 將佈局使用LayoutInflate.inflate添加進R.id.content的一些過程。

View顯示過程。假裝你已經知道Activity的生命週期,並且也知道代碼在哪裏執行。那我們直接來到應用入口點的那個類。

小明同學,請等等····是哪個類??嗚,那就告訴你吧,是ActivityThread

  • 來到執行resume的方法:handleResumeActivity()

      @Override
      public void handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward,
          String reason) {
      //onNewIntent(),以及調用Activity的resume()生命週期函數,都在下面這個方法執行
      final ActivityClientRecord r = performResumeActivity(token, finalStateRequest, reason);
      ...
      //獲取到PhoneWindow,
       r.window = r.activity.getWindow();
      //獲取DecorView
          View decor = r.window.getDecorView();
      //decorView設置爲不可見
          decor.setVisibility(View.INVISIBLE);
      //拿到windowManagerImpl
          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.mWindowAdded) {
          a.mWindowAdded = true;
      	//WindowManagerImpl的addView方法,傳入參數爲decorView,和Window的屬性
      	//故事就從這裏開始了
          wm.addView(decor, l);
      ...	
      //調用Activity裏面的makeVisisble()使得視圖可見
       if (r.activity.mVisibleFromClient) {
              r.activity.makeVisible();
          }
      //你說了解Handler,那來說說IdleHandler吧。
       Looper.myQueue().addIdleHandler(new Idler());
    
  • ok,可以看到上面一系列“熟悉”的流程。我們重點關注上面的wm.addView(DecorView,WindowManager.LayoutParams). 進入WindowManagerImpl.

      @Override
      public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
          applyDefaultToken(params);
      	//單例的WindowMagerGlobal.
          mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
      }
    
  • WindowManagerGlobal

      	//創建ViewRootImpl
          root = new ViewRootImpl(view.getContext(), display);
          view.setLayoutParams(wparams);
      	//集合緩存
          mViews.add(view);
          mRoots.add(root);
          mParams.add(wparams);
          // do this last because it fires off messages to start doing things
      	//調用ViewRootImpl的setView方法:
          root.setView(view, wparams, panelParentView);
    
  • 來到ViewRootImpl的setView,很快我們看到了一個名爲requestLayout的方法

      @Override
      public void requestLayout() {
          if (!mHandlingLayoutInLayoutRequest) {
              checkThread();
              mLayoutRequested = true;
              scheduleTraversals();
          }
      }
      //上面的checkThread
      void checkThread() {
      還是那個熟悉的提示,原來這麼多年來,提示非主線程不能更新Ui的提示,都在這裏孤孤單單,今天我終於來看她了。
          if (mThread != Thread.currentThread()) {
              throw new CalledFromWrongThreadException(
                      "Only the original thread that created a view hierarchy can touch its views.");
          }
      }
      //上面的scheduleTraversals()
      void scheduleTraversals() {
      if (!mTraversalScheduled) {
          mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
          mChoreographer.postCallback(
                  Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
       }}
      注意這個mTraversalRunnable
          final class TraversalRunnable implements Runnable {
      @Override
      public void run() {
          doTraversal(); //調用了doThrversal()
      }}
    

doThaversal()中調用了performTraversals() 好的,我們的故事開始了~~

  • performTraversals中一直往下走,走啊走,你會看到一行代碼:

      // Ask host how big it wants to be
      performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);	
    
  • 再走:

       performLayout(lp, mWidth, mHeight);	
    
  • 再接再厲

       performDraw();
    
  • 完畢之後,我們看

      private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
          if (mView == null) {
              return;
          }
          Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
          try {
      	//這個mView就是DecorView
              mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
          } finally {
              Trace.traceEnd(Trace.TRACE_TAG_VIEW);
          }
      }
    
  • DecorView也是繼承自View,來到View中的measure(),我們看到:

      調用了onMeasure,decorView的父類是FragmLayout
      onMeasure(widthMeasureSpec, heightMeasureSpec);
    
  • FrameLayout##onMeasure()

      @Override
       protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
      int count = getChildCount();
    
      int maxHeight = 0;
      int maxWidth = 0;
      int childState = 0;
      //注意到,開始循環遍歷ViewGroup裏面包含的子View
      for (int i = 0; i < count; i++) {
          final View child = getChildAt(i);
      	//不爲Gone的不去測算,爲InVISibility也需要測算的
          if (mMeasureAllChildren || child.getVisibility() != GONE) {
              measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);}
      .......
      setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
              resolveSizeAndState(maxHeight, heightMeasureSpec,
                      childState << MEASURED_HEIGHT_STATE_SHIFT));
    
  • measureChildWithMargins()

      protected void measureChildWithMargins(View child,
          int parentWidthMeasureSpec, int widthUsed,
          int parentHeightMeasureSpec, int heightUsed) {
      //拿到孩子寫在佈局裏面的寬高屬性
      final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
      //測算子孩子的大小
      final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
              mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
                      + widthUsed, lp.width);
      final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
              mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
                      + heightUsed, lp.height);
      //看到再次調用child.measure(),measure中會調用onMeasure()
      child.measure(childWidthMeasureSpec, childHeightMeasureSpec);}
    

View的Measure過程

  • 由父類和子類確定子類的模式和大小。

      public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
      int specMode = MeasureSpec.getMode(spec);
      int specSize = MeasureSpec.getSize(spec);
      int size = Math.max(0, specSize - padding);
      int resultSize = 0;
      int resultMode = 0;
      switch (specMode) {
      // Parent has imposed an exact size on us
      case MeasureSpec.EXACTLY:
          if (childDimension >= 0) {
              resultSize = childDimension;
              resultMode = MeasureSpec.EXACTLY;
          } else if (childDimension == LayoutParams.MATCH_PARENT) {
              // Child wants to be our size. So be it.
              resultSize = size;
              resultMode = MeasureSpec.EXACTLY;
          } else if (childDimension == LayoutParams.WRAP_CONTENT) {
              // Child wants to determine its own size. It can't be
              // bigger than us.
              resultSize = size;
              resultMode = MeasureSpec.AT_MOST;
          }
          break;
    
      // Parent has imposed a maximum size on us
      case MeasureSpec.AT_MOST:
          if (childDimension >= 0) {
              // Child wants a specific size... so be it
              resultSize = childDimension;
              resultMode = MeasureSpec.EXACTLY;
          } else if (childDimension == LayoutParams.MATCH_PARENT) {
              // Child wants to be our size, but our size is not fixed.
              // Constrain child to not be bigger than us.
              resultSize = size;
              resultMode = MeasureSpec.AT_MOST;
          } else if (childDimension == LayoutParams.WRAP_CONTENT) {
              // Child wants to determine its own size. It can't be
              // bigger than us.
              resultSize = size;
              resultMode = MeasureSpec.AT_MOST;
          }
          break;
    
      // Parent asked to see how big we want to be
      case MeasureSpec.UNSPECIFIED:
          if (childDimension >= 0) {
              // Child wants a specific size... let him have it
              resultSize = childDimension;
              resultMode = MeasureSpec.EXACTLY;
          } else if (childDimension == LayoutParams.MATCH_PARENT) {
              // Child wants to be our size... find out how big it should
              // be
              resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
              resultMode = MeasureSpec.UNSPECIFIED;
          } else if (childDimension == LayoutParams.WRAP_CONTENT) {
              // Child wants to determine its own size.... find out how
              // big it should be
              resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
              resultMode = MeasureSpec.UNSPECIFIED;
          }
          break;
      }
      //noinspection ResourceType
      return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
      }
    
  • 父佈局是Exactly

    • 子佈局如果爲AT_MOST(wrap_content), 那麼模式爲AT_MOST,其他都是Exactly
  • 父佈局是AT_MOST

    • 子View寫爲準確值(100dp),那麼模式是Exactly

    • 子View是wrap_content或match_parent,都是AT_MOST模式。

    • 上面兩句話讀起來有點晦澀難懂,記住Exactly是已經確定了,AT_MOST就是待計算。

    • 父類確定(Exactly)了,子類確定(100dp和match_parent)那麼子類也是確定(Exactly)

    • 父類不確定(AT_MOST), 子類確定(100dp)那麼是Exactly,否則,子類都不確定。

  • 舉一個小栗子,就是ScrollView包裹ListView ,你會發現只顯示了一條數據。網上給出的解決方案很多都是: 計算ListView 的高度,然後更改List的LayoutParams。竟然把所有的ListView所有的孩子都拿來累加高度。

這個問題是因爲ScrollView,傳過來的模式是:UNSPECIFIED,ListView中的onMeasure,對於UNSPECIFIED的處理是:

if (heightMode == MeasureSpec.UNSPECIFIED) {
        heightSize = mListPadding.top + mListPadding.bottom + childHeight +
                getVerticalFadingEdgeLength() * 2;
    }

所以正確的打開方式:

//繼承一下ListView,然後,重寫onMeasure()方法:
val height = MeasureSpec.makeMeasureSpec(heightMeasureSpec, MeasureSpec.AT_MOST)
super.onMeasure(widthMeasureSpec, height)

另外,我們還看到有performLayout()和performDraw()方法,調用也都是大同小異,完成各自的功能。

  • performMeasure – > measure – > onMeasure
  • performLayout – > layout – > layout
  • performDraw – > draw – > drawbackground – > ondraw.
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章