首页 > 代码库 > 【React Native】源码分析之Native UI的封装和管理

【React Native】源码分析之Native UI的封装和管理

??ReactNative作为使用React开发Native应用的新框架,随着时间的增加,无论是社区还是个人对她的兴趣与日递增。此文目的是希望和大家一起欣赏一下ReactNative的部分源码。阅读源码好处多多,让攻城狮更溜的开发ReactNative应用的同时,也能梳理RN项目的设计思路,增加自己的内功修为,^_^。
??好的,就让我们轻松的开始吧。此篇是以Android平台源码分析为主,分享Native UI的封装和管理,重点涉及react-native源码中com.facebook.react.uimanager包中的相关类。
??通过下图对剖析的源码部分有个整体的概念,这是从下向上的调用关系。
技术分享

??因为上层是向我们直接暴露的类,所以我们采用从上向下的分析过程,以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的继承关系图:
技术分享
??上图可以清晰反馈ReactImageManager的继承关系,最终定位到ViewManager类,同时SimpleViewManager负责对View的管理,而对ViewGroup的封装需要继承ViewGroupManager实现。也许上面问题的答案我们可以在他的超父类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通过两个主要类控制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

??好的,为什么UIManagerModule方法能够被JavaScript调用,答案是方法被@ReactMethod注解 ,但是为什么?没事就问句问什么^_^,我们下篇文章再详解分析。

谢谢阅读,希望能对您理解ReactNative有帮助~~~

<script type="text/javascript"> $(function () { $(‘pre.prettyprint code‘).each(function () { var lines = $(this).text().split(‘\n‘).length; var $numbering = $(‘
    ‘).addClass(‘pre-numbering‘).hide(); $(this).addClass(‘has-numbering‘).parent().append($numbering); for (i = 1; i <= lines; i++) { $numbering.append($(‘
  • ‘).text(i)); }; $numbering.fadeIn(1700); }); }); </script>

    【React Native】源码分析之Native UI的封装和管理