首页 > 代码库 > 【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 ,他的定义如下,使用过Facebook
的Fresco
图片开源项目的开发者应该会很熟悉GenericDraweeView
类,继承她实现自己的图片展示逻辑。
public class ReactImageView extends GenericDraweeView {}
??通过ReactImageManager
对本地ReactImageView
进行管理。
知识点一:封装React可以使用的Native UI View,需要创建一个ViewManager进行管理。
??可以说这是标准ViewManager
的官方推荐的写法,继承SimpleViewManager
重写getName
和createViewInstance
方法,但是此处我们不禁会问–为什么?为什么要重写这两个方法,在源码中是什么用的调用关系,导致了这种结果。
下面看一张ViewManager的继承关系图:
??上图可以清晰反馈ReactImageManager
的继承关系,最终定位到ViewManager
类,同时SimpleViewManager
负责对View
的管理,而对ViewGroup
的封装需要继承ViewGroupManager
实现。也许上面问题的答案我们可以在他的超父类ViewManager
中找到答案。
看一下ViewManager的类图可以给我们什么信息:
OK~,ViewManager
中定义我们关心的getName
和createViewInstance
抽象方法。而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;
}
此方法完成两件事:
- 创建本地View对象,通过抽象方法
createViewInstance(reactContext)
完成,所以子类必须实现这个方法,否则View
对象为空。 - 通过抽象方法
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);
}
}
此方法完成以下几个工作:
- 做线程判断,此方法必须在UI线程中调用。
- 通过
ClassName
获取到对应的ViewManager
; - 创建View实例,对应到我们剖析的主角就是
ReactImageView
,使用的方法就是上文提到的ViewManager
的createView
方法; - 分别存储
View
和ViewManger
到mTagsToViews
和mTagsToViewManagers
中; - 设置新创建的
View
的Id
,为什么要这么做?
是为了重用,减少开销,由于不是通过XML
的形式创建,所以View
并没有对应的ID
,需要手动去设置,这里设置的ID
值为传递过来的参数Tag
- 如果所有属性都初始化(
@ReactPro
注解的方法)完成,做一次回调,通知ViewManager
去做属性全部初始化成功之后的操作。
最终会调用ViewManager
的updateProperties
函数,目的是更新属性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
??那么我的问题又来了,谁调用的NativeViewHierarchyManager
的createView
方法呐?传递的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
方法,也就是调用NativeViewHierarchyManager
的createView
方法创建新的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
的队列中添加元素,这里引入了两个关键类ReactShadowNode
和ViewProps
,先来说一下ViewProps
的Java
类,其定义了很多属性名的常量。
//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
创建名称为cssNode
的ReactShadowNode
对象,上文使用的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有帮助~~~
【React Native】源码分析之Native UI的封装和管理