【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有幫助~~~