1. 程式人生 > >【React Native】原始碼分析之Native UI的封裝和管理

【React Native】原始碼分析之Native UI的封裝和管理

  ReactNative作為使用React開發Native應用的新框架,隨著時間的增加,無論是社群還是個人對她的興趣與日遞增。此文目的是希望和大家一起欣賞一下ReactNative的部分原始碼。閱讀原始碼好處多多,讓攻城獅更溜的開發ReactNative應用的同時,也能梳理RN專案的設計思路,增加自己的內功修為,^_^。
  好的,就讓我們輕鬆的開始吧。此篇是以Android平臺原始碼分析為主,分享Native UI的封裝和管理,重點涉及react-native原始碼中com.facebook.react.uimanager包中的相關類。
  通過下圖對剖析的原始碼部分有個整體的概念,這是從下向上的呼叫關係。
UIManagerModule呼叫關係

  因為上層是向我們直接暴露的類,所以我們採用從上向下的分析過程,以ReactImageManager作為切入點進行分析。兩個原因

  • 圖片是任何應用都必不可少的元素
  • ReactImageView封裝Facebook的Fresco圖片框架,在剖析的過程中可同時梳理RN封裝第三方框架的過程。

首先看一下ReactImageManager的程式碼實現:

@ReactModule(name = ReactImageManager.REACT_CLASS)
public class ReactImageManager extends SimpleViewManager<ReactImageView
> {
protected static final String REACT_CLASS = "RCTImageView"; @Override public String getName() { return REACT_CLASS; } @Override public ReactImageView createViewInstance(ThemedReactContext context) { return new ReactImageView( context, getDraweeControllerBuilder(), getCallerContext()); } }

  此處的ReactImageView就是ReactNative封裝的影象處理相關的Native UI ,他的定義如下,使用過FacebookFresco圖片開源專案的開發者應該會很熟悉GenericDraweeView類,繼承她實現自己的圖片展示邏輯。

public class ReactImageView extends GenericDraweeView {}

  通過ReactImageManager對本地ReactImageView進行管理。

知識點一:封裝React可以使用的Native UI View,需要建立一個ViewManager進行管理。

  可以說這是標準ViewManager的官方推薦的寫法,繼承SimpleViewManager重寫getNamecreateViewInstance方法,但是此處我們不禁會問–為什麼?為什麼要重寫這兩個方法,在原始碼中是什麼用的呼叫關係,導致了這種結果。

下面看一張ViewManager的繼承關係圖:
ViewManager的繼承關係
  上圖可以清晰反饋ReactImageManager的繼承關係,最終定位到ViewManager類,同時SimpleViewManager負責對View的管理,而對ViewGroup的封裝需要繼承ViewGroupManager實現。也許上面問題的答案我們可以在他的超父類ViewManager中找到答案。

看一下ViewManager的類圖可以給我們什麼資訊:
ViewManager類圖

OK~,ViewManager中定義我們關心的getNamecreateViewInstance抽象方法。而createViewInstance的使用是在createView方法中,看原始碼:

/**
 *  ViewManager類原始碼
 *  Creates a view and installs event emitters on it.
 */
public final T createView(
    ThemedReactContext reactContext,
    JSResponderHandler jsResponderHandler) {
  T view = createViewInstance(reactContext);
  addEventEmitters(reactContext, view);
  if (view instanceof ReactInterceptingViewGroup) {
    ((ReactInterceptingViewGroup) view).setOnInterceptTouchEventListener(jsResponderHandler);
  }
  return view;
}

此方法完成兩件事:

  1. 建立本地View物件,通過抽象方法createViewInstance(reactContext)完成,所以子類必須實現這個方法,否則View物件為空。
  2. 通過抽象方法addEventEmitters()註冊事件的型別。(比如我們自定義的監聽事件,需要子類在此方法中註冊)

OK ~ , 以ViewManager的createView()為切入口,看一下整個建立可以被React使用的Native UI的呼叫過程。

NativeViewHierarchyManager

  檢視createView()的呼叫,引出一個新的類,名字叫NativeViewHierarchyManager,同樣位於com.facebook.react.uimanager包中。在她的實現中,有這麼一段程式碼,

public void createView(
    ThemedReactContext themedContext,
    int tag,
    String className,
    @Nullable ReactStylesDiffMap initialProps) {
  UiThreadUtil.assertOnUiThread();
  try {
    ViewManager viewManager = mViewManagers.get(className);

    View view = viewManager.createView(themedContext, mJSResponderHandler);
    mTagsToViews.put(tag, view);
    mTagsToViewManagers.put(tag, viewManager);
    view.setId(tag);
    if (initialProps != null) {
      viewManager.updateProperties(view, initialProps);
    }
  } finally {
    Systrace.endSection(Systrace.TRACE_TAG_REACT_VIEW);
  }
}

此方法完成以下幾個工作:

  1. 做執行緒判斷,此方法必須在UI執行緒中呼叫。
  2. 通過ClassName獲取到對應的ViewManager
  3. 建立View例項,對應到我們剖析的主角就是ReactImageView,使用的方法就是上文提到的ViewManagercreateView方法;
  4. 分別儲存ViewViewMangermTagsToViewsmTagsToViewManagers中;
  5. 設定新建立的ViewId,為什麼要這麼做?
    是為了重用,減少開銷,由於不是通過XML的形式建立,所以View並沒有對應的ID,需要手動去設定,這裡設定的ID值為傳遞過來的引數Tag
  6. 如果所有屬性都初始化(@ReactPro註解的方法)完成,做一次回撥,通知ViewManager去做屬性全部初始化成功之後的操作。

最終會呼叫ViewManagerupdateProperties函式,目的是更新屬性Props和給子類重新整理的機會。

public final void updateProperties(T viewToUpdate, ReactStylesDiffMap props) {
  ViewManagerPropertyUpdater.updateProps(this, viewToUpdate, props);
  onAfterUpdateTransaction(viewToUpdate);
}
  • 更新屬性。
  • 更新之後要做的事情交給子類去實現。

例如我們的主角ReactImageManager要做的事情就是:

//ReactImageManager原始碼
@Override
protected void onAfterUpdateTransaction(ReactImageView view) {
  super.onAfterUpdateTransaction(view);
  view.maybeUpdateView();
}

判斷是否需要更新ImageView試圖,如果需要馬上更新。

知識點二:如果你的需求中要求在屬性都初始化完成之後,要做一些處理,請重寫onAfterUpdateTransaction方法。

OK~,NativeViewHierarchyManager類設計用途,除了觸發ViewManager建立Native UI的衍生物件對外,還有哪些?請看類圖:

NativeViewHierarchyManager類圖

  NativeViewHierarchyManager通過兩個主要類控制Native UI View的建立、更新、佈局修改、屬性變化等。其中一個是上文提到的ViewManager類,另外一個是ViewManagerRegister類。後者存放一個ViewManager的對映關係,通過getName的返回值作為key值,而getName的返回值,也是在JavaScript中定義Module時使用名字,如此當JavaScript呼叫React元件時,通過名稱可以找到對應的ViewManager,通過ViewManager可以找到對應的Native UI View,從而可以使用JavaScript構建原生應用效果。帖一下下ViewManagerRegister的程式碼,方便理解viewManager.getName()方法的使用。

 //ViewManagerRegistry原始碼
 public ViewManagerRegistry(List<ViewManager> viewManagerList) {
    for (ViewManager viewManager : viewManagerList) {
      mViewManagers.put(viewManager.getName(), viewManager);
    }
  }

知識點三: 自定義ViewManager為什麼要重寫getName方法?其一為JavaScript使用封裝後的ReactView時,能對應到原生自定義的ViewManager,從而操作View;其二JavaScript當建立元件類時會使用這個名字。

知識點四:自定義ViewManager重寫createViewInstance的目的是建立Native UI View的物件,並且新增到本地檢視層級結構中。

UIViewOperationQueue

  那麼我的問題又來了,誰呼叫的NativeViewHierarchyManagercreateView方法吶?傳遞的Tag又是如何定義的?OK,我們在原始碼中找到UIViewOperationQueue這個Java類,好樣的,根據名字感覺她是UIView的執行佇列。具體是不是吶,那我們來看下程式碼:

//UIViewOperationQueue原始碼
private final NativeViewHierarchyManager mNativeViewHierarchyManager;
private final class CreateViewOperation extends ViewOperation {

  private final ThemedReactContext mThemedContext;
  private final String mClassName;
  private final @Nullable ReactStylesDiffMap mInitialProps;
  ...
  @Override
  public void execute() {
    mNativeViewHierarchyManager.createView(
        mThemedContext,
        mTag,
        mClassName,
        mInitialProps);
  }
}

  程式碼寫的清晰明瞭,當有UI操作(動畫、View的層次結構發生變化的時候),就會執行execute方法,也就是呼叫NativeViewHierarchyManagercreateView方法建立新的View物件。來個庖丁解牛CreateViewOperation在哪裡被呼叫?

// UIViewOperationQueue原始碼
@GuardedBy("mNonBatchedOperationsLock")
private ArrayDeque<UIOperation> mNonBatchedOperations = new ArrayDeque<>();

public void enqueueCreateView(
    ThemedReactContext themedContext,
    int viewReactTag,
    String viewClassName,
    @Nullable ReactStylesDiffMap initialProps) {
  synchronized (mNonBatchedOperationsLock) {
    mNonBatchedOperations.addLast(
      new CreateViewOperation(
        themedContext,
        viewReactTag,
        viewClassName,
        initialProps));
  }
}

  建立一個數組佇列,佇列的名字為mNonBatchedOperations,每次呼叫enqueueCreateView方法,向陣列佇列中新增一個建立View的操作。
那麼除了建立本地檢視,她還定義了那些操作吶:

  • ViewOperation:根據Tag,指定原生View去操作;
  • RemoveRootViewOperation:刪除TootView的操作;
  • UpdatePropertiesOperation:更新屬性操作;
  • UpdateLayoutOperation:更新Native View的位置和大小的操作;
  • ManageChildrenOperation:管理子檢視操作;
  • RegisterAnimationOperation:註冊動畫的操作;
  • AddAnimationOperation : 增加動畫的操作;
  • SetLayoutAnimationEnabledOperation:設定佈局動畫是否可用的操作
  • MeasureOperation:測量操作

  可以把UIViewOperationQueue看成一個緩衝帶,他不去完成實質性的操作,真正的實現都在NativeViewHierarchyManager中完成,他將JavaScript要對Native View做的所有操作都放在對應佇列中,快取起來批量處理。根據上面的程式碼,建立Native View衍生物件的操作,已經放到了佇列中,那麼是誰操作的佇列去新增操作(Operation)吶?

come on 搞起~

NativeViewHierarchyOptimizer

  不難跟到NativeViewHierarchyOptimizer類,看名字像是NativeViewHierarchy的優化程式,看程式碼後,你還別說還真是做優化本地UI檢視層級結構的工作的,看看此類的官方介紹:

負責優化本地檢視層次結構,同時仍然遵循JS指定的最終UI樣式。 基本上,JS向我們傳送了一個節點層次結構,雖然在JS中容易理解,但是直接轉換為本地檢視效率很低。 這個類位於UIManagerModule(直接接收來自JS的檢視命令)和UIViewOperationQueue之間,它使本地檢視層次上的實際操作入隊。它能夠從UIManagerModule獲取指令,並將輸出指令傳遞到本地檢視層次結構,使用較少的檢視,實現相同的效果。

對於NativeViewHierarchyOptimizer的優化過程,咱們看一下他的實現思路,程式碼如下:

private static final boolean ENABLED = true;
/**
 * Handles a createView call. May or may not actually create a native view.
 */
public void handleCreateView(
    ReactShadowNode node,
    ThemedReactContext themedContext,
    @Nullable ReactStylesDiffMap initialProps) {
  if (!ENABLED) {
    int tag = node.getReactTag();
    mUIViewOperationQueue.enqueueCreateView(
        themedContext,
        tag,
        node.getViewClass(),
        initialProps);
    return;
  }

  boolean isLayoutOnly = node.getViewClass().equals(ViewProps.VIEW_CLASS_NAME) &&
      isLayoutOnlyAndCollapsable(initialProps);
  node.setIsLayoutOnly(isLayoutOnly);

  if (!isLayoutOnly) {
    mUIViewOperationQueue.enqueueCreateView(
        themedContext,
        node.getReactTag(),
        node.getViewClass(),
        initialProps);
  }
}

  這裡的ENABLED非常有意思,預設值是true,是私有的常量,不可重新賦值,那就逗了,所有if(!ENABLED)裡面的程式碼永遠不會執行,這是我的理解,如果你有其他的理解,歡迎交流。

  通過isLayoutOnly來判斷是否向建立View的佇列中新增元素,這裡引入了兩個關鍵類ReactShadowNodeViewProps,先來說一下ViewPropsJava類,其定義了很多屬性名的常量。

  //ViewProps原始碼
  public static final String ALIGN_ITEMS = "alignItems";
  public static final String ALIGN_SELF = "alignSelf";
  public static final String OVERFLOW = "overflow";
  public static final String BOTTOM = "bottom";
  ...

  另外將只導致佈局變化(Layout Change),不引起重繪(no Drawing)的常量放在HashSet中,起名為LAYOU_ONLY_PROPS,在NativeViewHierarchyOptimizer類中運用,起到優化本地試圖層級的效果。
  程式碼中的判斷條件為節點為View型別並且僅改變佈局屬性的話,就不需要重新建立本地View的例項,否則建立,通過這種邏輯來優化本地View的例項建立,從而節省記憶體開支。
  當然NativeViewHierarchyOptimizer還做了其他命令的優化工作,將優化後需要Native View執行的操作,儲存到上文中的UIViewOperationQueue中,等待JavaScript批處理執行。
  OK~,那麼JavaScript命令又是通過什麼傳遞到NativeViewHierarchyOptimizer中的吶?ReactShadowNode類又是如何傳遞過來的,JavaScript和Native通訊的過程中扮演什麼樣的角色???

我們離真相越來越近了,Come On~~

UIImplementation

答案是通過UIImplementation類,看一小部分原始碼實現:

//UIImplementation原始碼
protected void handleCreateView(
    ReactShadowNode cssNode,
    int rootViewTag,
    @Nullable ReactStylesDiffMap styles) {
  if (!cssNode.isVirtual()) {
    mNativeViewHierarchyOptimizer.handleCreateView(cssNode, cssNode.getThemedContext(), styles);
  }
}

呼叫的方法很熟悉,上文剛介紹完,繼續跟

/**
 * UIImplementation 原始碼
 * Invoked by React to create a new node with a given tag, class name and properties.
 */
public void createView(int tag, String className, int rootViewTag, ReadableMap props) {
  ReactShadowNode cssNode = createShadowNode(className);
  ReactShadowNode rootNode = mShadowNodeRegistry.getNode(rootViewTag);
  cssNode.setReactTag(tag);
  cssNode.setViewClassName(className);
  cssNode.setRootNode(rootNode);
  cssNode.setThemedContext(rootNode.getThemedContext());
  mShadowNodeRegistry.addNode(cssNode);
  ReactStylesDiffMap styles = null;
  if (props != null) {
    styles = new ReactStylesDiffMap(props);
    cssNode.updateProperties(styles);
  }

  handleCreateView(cssNode, rootViewTag, styles);
}

  在createView函式中,最後呼叫了handleCreateView,另外讓人興奮的是,找到了ReactShadowNode的源頭,在這裡根據className建立名稱為cssNodeReactShadowNode物件,上文使用的node.getReactTag()獲取tag的方法,根源就在此處。在函式的註解中介紹到React通過給定的tag、類名、屬性呼叫這個函式去建立一個新的節點。

注: 在Android中,佈局的每個元素我們稱之為View。在React中,因為採用Web的思想,佈局中的元素被稱之為節點(node)。

  所以分析到這裡,不用看ReactShadowNode的原始碼實現,我們也能猜測到他的用途,他代表了React佈局中的一個元素,對應Native佈局層級中的一個View。他的屬性包括節點Tag(ReactTag)、節點類名(ViewClassName)、根節點資訊(rootNode)、位置、自身大小等等資訊,可以理解為React虛擬數上的一個最基礎的節點。擁有這些資訊,就可獲取到當前節點的位置進行佈局。

再進一步,建立ReactShadowNode方法:

//UIImplementation原始碼
protected ReactShadowNode createShadowNode(String className) {
  ViewManager viewManager = mViewManagers.get(className);
  return viewManager.createShadowNodeInstance();
}

奧,好熟悉竟然是ViewManager,我們就是從這個類作為入口進行分析的啊,OK~,看createShadowNodeInstance()方法,

//ViewManager原始碼
/**
 * This method should return a subclass of {@link ReactShadowNode} which will be then used for
 * measuring position and size of the view. In mose of the cases this should just return an
 * instance of {@link ReactShadowNode}
 */
public abstract C createShadowNodeInstance();

原來是一個抽象方法,那我們的主角ReactImageManager需要去實現這個方法,找一下發現在SimpleViewManager裡進行了實現,

//SimpleViewManager原始碼
@Override
public LayoutShadowNode createShadowNodeInstance() {
  return new LayoutShadowNode();
}

LayoutShadowNode提供了基本的佈局屬性,如寬高、flex等等,這裡也使用到了ViewProps定義的一些屬性常量。

public class LayoutShadowNode extends ReactShadowNode {

  @ReactProp(name = ViewProps.WIDTH, defaultFloat = CSSConstants.UNDEFINED)
  public void setWidth(float width) {
    setStyleWidth(CSSConstants.isUndefined(width) ? width : PixelUtil.toPixelFromDIP(width));
  }

  這裡就給了我們想象的空間,除去這些基本的佈局屬性,如果我們想自定義View,就可以繼承LayoutShadowNode,新增自定義的佈局屬性,在createShadowNodeInstance()中進行初始化,同樣可以被React承認。具體可以參考ReactTextInlineImageShadowNode類的實現,新增ImageSpan的過程。

知識點五:通過繼承LayoutShadowNode,新增自定義的佈局屬性。就想我們在Android中自定義View新增新屬性,需要在XML中註冊相同。

  另外根據原始碼可以瞭解到,UIImplement還做了一件大事,首先他先建立了ReactShadowNode,我們稱之為影子節點,然後通過UIImplemention建立由指定影子節點的相關屬性建立的Native View。如此一個Native View對應一個ReactShadowNode,JavaScript可以控制影子節點屬性,從而改變Native View的佈局和形態。

注:ReactShadowNode,網上稱之為影子節點,感覺還挺好聽的,JS可以直接控制他的屬性,從而對Native View進行佈局(位置、大小、內容等)。

OK~ ,誰可以控制UIImplementation的呼叫?

UIManagerModule

  答案是com.facebook.react.uimanager包中的關鍵類,名字叫UIManagerModule。通俗一點說,此類的方法可以被JavaScript呼叫,也就是可以接收JavaScript的命令。然後再呼叫UIImplementation去執行具體的操作。
  這是在JavaScript執行緒(非UI執行緒)對View進行佈局和測量的一個關鍵類,是JS控制Native View的入口。然後根據我們上面的一起的一步一步的分析,最終實現控制Native View的效果。

看下面一小部分原始碼:

//UIManagerModule原始碼
@ReactMethod
public void createView(int tag, String className, int rootViewTag, ReadableMap props) {
  mUIImplementation.createView(tag, className, rootViewTag, props);
}

呼叫UIImplementation建立Native View

總結

  最後,我們對上文的整個剖析,做個總結,通過下圖的梳理,希望能對我們理解這部分原始碼思路有所幫助(此圖良心出品^_^)。

Native UI 原始碼梳理

  好的,但是問題又來了,為什麼UIManagerModule方法能夠被JavaScript呼叫,答案是方法被@ReactMethod註解 ,但是為什麼?沒事就問句問什麼^_^,我們下篇文章再詳解分析。

謝謝閱讀,希望能對您理解ReactNative有幫助~~~