ReactNative是如何讓JS代碼『變成』Android控制項的?
5 人贊了文章
徐萌陽
ReactNative是如何讓JS代碼『變成』Android控制項的?
編寫的JS代碼是如何『變』成一個個Android的控制項的呢?JS的FlexBox布局是如何『轉譯』成Android的布局的呢?
在找到這兩個問題答案前,先介紹下RN的渲染引擎——Yoga。Yoga
隨著這幾年前端技術的崛起,作為前端UI骨架的布局系統也在其中佔據了越來越重要的位置。不管是在移動端、桌面端還是Web端,特別是不同設備的屏幕大小和解析度千變萬化,如何構建良好的布局系統以便應付這些變化已經變得越來越重要。
目前,各個平台都有自己的一套解決方案。iOS平台有自動布局系統,Android有容器布局系統,而Web端有基於CSS的布局系統。多種布局系統共存所帶來的弊端是很明顯的,平台間的共享變得很困難,而每個平台都需要專人來開發維護,增加了開發成本。
Facebook在這個問題上沒有少下功夫。首先,Facebook在React Native里引入了一種跨平台的基於CSS的布局系統,它實現了Flexbox規範。基於這個布局系統,不同團隊終於可以走到一起,一起解決缺陷,改進性能,讓這個系統更加地貼合Flexbox規範。
隨著這個系統的不斷完善,Facebook決定對它進行重啟發布,並取名Yoga。
Yoga是基於C實現的。之所以選擇C,首先當然是從性能方面考慮的。基於C實現的Yoga比之前Java實現在性能上提升了33%。其次,使用C實現可以更容易地跟其它平台集成。到目前為止,Yoga已經有以下幾個平台的綁定:Java(Android)、Objective-C(UIKit)、C#(.NET)。
Yoga使用方式
目前Yoga的1.5.0在Android上已經支持直接使用xml布局了,但接下來我要追蹤的View渲染原理肯定不會在Android上生成靜態xml布局。所以這裡還得看下如何使用Java代碼編寫Yoga布局。
這裡我直接引用官網的代碼示例。YogaNode root = new YogaNode();root.setWidth(500);root.setHeight(300);root.setAlignItems(CENTER);root.setJustifyContent(CENTER);root.setPadding(ALL, 20);YogaNode text = new YogaNode();text.setWidth(200);text.setHeight(25);YogaNode image = new YogaNode();image.setWidth(50);image.setHeight(50);image.setPositionType(ABSOLUTE);image.setPosition(END, 20);image.setPosition(TOP, 20);root.addChildAt(text, 0);root.addChildAt(image, 1);
簡而言之,創建子節點並設置顯示屬性後放入根節點即完成了Yoga的布局。
渲染原理
如何布局
我的思路是要找到這個問題的答案,首先要先搞清楚View如何被創建?
之前看到過一篇文章React-Native 源碼分析二-JSX如何渲染成原生頁面,這裡作者直接把我引導到UIManagerModule
,幫我少走了不少彎路。這個類里有好多被聲明為@ReactMethod
的方法。public class UIManagerModule extends ReactContextBaseJavaModule { ··· @ReactMethod public void createView() @ReactMethod public void updateView() @ReactMethod public void manageChildren() @ReactMethod public void measure() ···}
以上我只列舉了一部分,但通過方法名稱即可了解這裡是JS和Native關於頁面渲染的切口。跟蹤createView()方法,發現了ShaowNode的創建。
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); ··· }
這裡只是把創建好的ShaowNode放到ShadowNodeRegistry
里。ShadowNodeRegistry
的核心是一個存放ReactShadowNode
的列表。
SparseArray<ReactShadowNode>
那麼肯定會有邏輯來遍歷這個列表來處理單個ReactShadowNode
。
<View stylex={{flex:1}}> <View stylex={{flex:1}}> <Text stylex={{flex:1}}>顯示1</Text> </View> <View stylex={{flex:1}}> <Text stylex={{flex:1}}>顯示2</Text> </View></View>
按照之前上面createView()里的邏輯,ShadowNodeRegistry
里會有所有View的ReactShadowNode
。
但是發現裡面有10個ReactShadowNode
。多出來的幾個我們後面分析,繼續跟蹤布局流程。
接下來會通過遞歸的方式將各個ReactShadowNode
以樹的方式連接起來。
這裡要說的一點是RCTRawText
僅包含Text的文案,和RCTText
是一一綁定的,但在創建View的過程中也會被封裝成一個ReactShadowNode
。後來經過測試,發現2-RCTView
和3-RTCView
除了承載要顯示的內容之外,還會承載一些框架自己的View,比如經常看到的黃色提示框。所以去除2、3、7和10之後,整個樹狀結構和JS代碼里的布局結構就一一對應起來了。
本以為在Native的View層級也是這樣,但通過Layout Inspector發現比這個簡單。
那麼4、5和8去哪了?這個問題我後面再跟蹤,繼續看View是怎樣布局的。
我覺得這裡要簡要說明下Native對於View的處理流程。Native接收到JS的渲染請求後,會將其封裝為類型為ViewOperation
的操作原子類。這些原子類會被暫時緩存到一個列表中。當UIManagerModule
的onBatchComplete()被調用才會輪詢緩存列表執行操作。簡單來說onBatchComplete()是Native和JS完成一次通訊後才會被調用,這塊流程可以參閱Native和JS通訊原理了解。
View的創建,尺寸計算、更新坐標等細節操作均被封裝成了不同的ViewOperation
。在UIViewOperationQueue
里可以看到詳細類型。這裡我只列舉幾個。
private final class UpdateLayoutOperation extends ViewOperation { ··· @Override public void execute() { Systrace.endAsyncFlow(Systrace.TRACE_TAG_REACT_VIEW, "updateLayout", mTag); mNativeViewHierarchyManager.updateLayout(mParentTag, mTag, mX, mY, mWidth, mHeight); } } private final class CreateViewOperation extends ViewOperation { ··· @Override public void execute() { Systrace.endAsyncFlow(Systrace.TRACE_TAG_REACT_VIEW, "createView", mTag); mNativeViewHierarchyManager.createView( mThemedContext, mTag, mClassName, mInitialProps); } } private final class ManageChildrenOperation extends ViewOperation { ··· @Override public void execute() { mNativeViewHierarchyManager.manageChildren( mTag, mIndicesToRemove, mViewsToAdd, mTagsToDelete); } }
回到剛才的問題,發現View的布局最終由UpdateLayoutOperation
執行。從execute()一路跟下去,最終發現在NativeViewHierarchyManager
的updateLayout()方法里找到了View的布局邏輯。
private void updateLayout(View viewToUpdate, int x, int y, int width, int height) { ··· viewToUpdate.layout(x, y, x + width, y + height); ··· }
至於x和y是怎樣被計算的,暫且不去研究,但追蹤到這裡可以明確,View的布局並沒有使用Android視圖容器組件的特性,而是通過計算View的x、y坐標後,直接使用layout()方法在父控制項里定位,這也就變相解釋了ReactRootView
的父類是FrameLayout
的用意。
接下來要弄明白的一個問題是那些最終沒有展示在屏幕上的ReactShadowNode
去哪了?猜測應該有類似『過濾器』的邏輯在某個環節把他們過濾掉了。
帶著這個問題繼續研究。既然View的布局是由任務隊列中的UpdateLayoutOperation
執行,那麼也許跟蹤UpdateLayoutOperation
被放進隊列的邏輯能找到答案。
順著這個思路跟蹤到在NativeViewHierarchyOptimizer
的applyLayoutRecursive()
方法。UpdateLayoutOperation
被放進任務隊列果然有『前置條件』。
private void applyLayoutRecursive(ReactShadowNode toUpdate, int x, int y) { if (!toUpdate.isLayoutOnly() && toUpdate.getNativeParent() != null) { int tag = toUpdate.getReactTag(); mUIViewOperationQueue.enqueueUpdateLayout( toUpdate.getNativeParent().getReactTag(), tag, x, y, toUpdate.getScreenWidth(), toUpdate.getScreenHeight()); return; } ···}
問題焦點進入到ReactShadowNode
的isLayoutOnly()方法里。這個方法只是返回了一個布爾值。
public final boolean isLayoutOnly() { return mIsLayoutOnly; }
那麼就搜一下mIsLayoutOnly什麼時候被賦值的。
/** * Sets whether this node only contributes to the layout of its children without doing any * drawing or functionality itself. */ public final void setIsLayoutOnly(boolean isLayoutOnly) { ··· mIsLayoutOnly = isLayoutOnly; }
這個方法的注釋正是我要找的問題的表象。繼續搜這個方法被調用的地方,被帶到了NativeViewHierarchyOptimizer
的handleCreateView()。
public void handleCreateView( ReactShadowNode node, ThemedReactContext themedContext, @Nullable ReactStylesDiffMap initialProps) { ··· boolean isLayoutOnly = node.getViewClass().equals(ViewProps.VIEW_CLASS_NAME) && isLayoutOnlyAndCollapsable(initialProps); node.setIsLayoutOnly(isLayoutOnly); ··· }
ViewProps.VIEW_CLASS_NAME
的值為RCTView
,這也就是我一直在找的被弄丟的4、5和8的節點類型。到此,這個問題的答案就找到了。
但是還有個小問題,總不能所有的RCTView
都被過濾掉吧?
為了驗證這個問題,我在JS代碼里給第二個Text的父View設置一個背景色。
<View stylex={{flex:1}}> <View stylex={{flex:1}}> <Text stylex={{flex:1}}>顯示1</Text> </View> <View stylex={{flex:1,backgroundColor:#223344}}> <Text stylex={{flex:1}}>顯示2</Text> </View></View>
然後再次進入handleCreateView()里,發現『8-RCTView』的isLayoutOnly這次變成了false。原因就是『&&』後面的isLayoutOnlyAndCollapsable()。
這個方法的返回值是根據View的屬性判斷的,核心邏輯是ViewProps
的isLayoutOnly()。ViewProps
里保存了一個名為LAYOUT_ONLY_PROPS
的靜態屬性列表,具體值可以到這個類里詳細看,這裡我就簡單概括下,裡面包括了設置margin、padding、position等一系列屬性。isLayoutOnly()會判斷View的屬性所有的key都是LAYOUT_ONLY_PROPS
里的屬性的話,返回true。我剛才添加的backgroundColor
不屬於這個列表裡的屬性,自然這個方法返回false,所以『8-RCTView』就會被繪製到屏幕上。
綜上,RN會將所有JS編寫的View封裝成ShaowNode,並自動過濾不需要顯示的View以減少嵌套層級。最終計算View的坐標值然後繪製到ReactRootView封裝的FrameLayout上。
怎樣給View分配ID
前面跟蹤了View的繪製流程,眾所周知,Android上所有的View都有一個ID。那麼RN是怎樣給View分配ID的呢?
一路跟進創建View的方法,終於在在NativeViewHierarchyManager
的createView()方法中發現了設置id的代碼。
public void createView( ThemedReactContext themedContext, int tag, String className, @Nullable ReactStylesDiffMap initialProps) { ··· ViewManager viewManager = mViewManagers.get(className); View view = viewManager.createView(themedContext, mJSResponderHandler); view.setId(tag); ··· }
接下來把焦點放到tag是怎樣傳進來的。一層層網上扒代碼,跟蹤到UIManagerModule
的createView()方法後,發現了非常熟悉的@ReactMethod
,這就說明View的id並不是在Native生成的,看來得到JS代碼里尋找答案。
ReactNativeBaseComponent
調用了createView。var tag = ReactNativeTagHandles.allocateTag();UIManager.createView( tag, this.viewConfig.uiViewClassName, nativeTopRootTag, updatePayload, );
這裡發現ReactNativeTagHandles
負責生成tag,通過方法名allocateTag猜測難道tag是自動遞增生成的?進去一看,果不其然。
var INITIAL_TAG_COUNT = 1;var ReactNativeTagHandles = { tagsStartAt: INITIAL_TAG_COUNT, tagCount: INITIAL_TAG_COUNT, allocateTag: function(): number { // Skip over root IDs as those are reserved for native while (this.reactTagIsNativeTopRootID(ReactNativeTagHandles.tagCount)) { ReactNativeTagHandles.tagCount++; } var tag = ReactNativeTagHandles.tagCount; ReactNativeTagHandles.tagCount++; return tag; }, reactTagIsNativeTopRootID: function(reactTag: number): boolean { // We reserve all tags that are 1 mod 10 for native root views return reactTag % 10 === 1; },};module.exports = ReactNativeTagHandles;
整個類邏輯比較簡單,就是每次通過++的方式將tag的值遞增,而且發現ReactRootView的tag恆定為1,那麼在Native是不是這樣呢?我用JS編寫一個簡單的頁面。
<View stylex={{flex:1}}> <View stylex={{flex:1}}> <Text stylex={{flex:1}}>點擊顯示</Text> </View> <View stylex={{flex:1}}> <Text stylex={{flex:1}}>點擊顯示</Text> </View></View>
然後用工具Layout Inspector來驗證。
可以看到ReactRootView的id果然為1,然後兩個ReactTextView的id分別為6和9。
怎樣給View設置屬性
為了跟蹤屬性設置流程,我修改了JS代碼,給第一個Text背景和文字添加了顏色。
<View stylex={{flex:1}}> <View stylex={{flex:1}}> <Text stylex={{flex:1,color:#661100,backgroundColor:#998800}}>點擊顯示</Text> </View> <View stylex={{flex:1}}> <Text stylex={{flex:1}}>點擊顯示</Text> </View></View>
發現在UIManagerModule
的createView()接收到了tag為5,props值為{{"allowFontScaling":true,"ellipsizeMode":"tail","accessible":true,"backgroundColor":-6715392,"color":-10088192,"flex":1} }
的調用。通過backgroundColor和color即可判定這是我要跟蹤的View。
根據剛才是跟蹤View繪製流程的思路,背景色的繪製肯定會被封裝成某個ViewOperation
,然後等待任務隊列輪詢。順著這個思路,發現背景色的更改會被CreateViewOperation
的execute()執行,最終會進入ViewManagerPropertyUpdater
的updateProps()。
public static <T extends ReactShadowNode> void updateProps(T node, ReactStylesDiffMap props) { ShadowNodeSetter<T> setter = findNodeSetter(node.getClass()); ReadableMap propMap = props.mBackingMap; ReadableMapKeySetIterator iterator = propMap.keySetIterator(); while (iterator.hasNextKey()) { String key = iterator.nextKey(); setter.setProperty(node, key, props); } }
這裡的propMap即為JS傳進來的View的屬性。
KEYVALUEallowFontScalingtrueellipsizeModetailaccessibletruecolor-10088192backgroundColor-6715392flex1
updateProps()會遍歷這個Map,將Key傳進setter里進一步處理。那麼繼續跟進去看setProperty()。
@Override public void setProperty(T manager, V v, String name, ReactStylesDiffMap props) { ViewManagersPropertyCache.PropSetter setter = mPropSetters.get(name); if (setter != null) { setter.updateViewProp(manager, v, props); } }
這裡根據Key獲取其對應的ViewManagersPropertyCache.PropSetter
來處理View的屬性。那麼mPropSetters
應該是類似靜態註冊表的存在。那麼就看看mPropSetters
是怎樣初始化的。
mPropSetters = ViewManagersPropertyCache.getNativePropSettersForViewManagerClass(viewManagerClass);
繼續跟進getNativePropSettersForViewManagerClass()
static Map<String, PropSetter> getNativePropSettersForViewManagerClass( Class<? extends ViewManager> cls) { ··· props = new HashMap<>( getNativePropSettersForViewManagerClass( (Class<? extends ViewManager>) cls.getSuperclass())); extractPropSettersFromViewManagerClassDefinition(cls, props); CLASS_PROPS_CACHE.put(cls, props); return props; }
核心邏輯是extractPropSettersFromViewManagerClassDefinition()方法,因為源碼較長,就簡要介紹下。這個方法里會遞歸掃描ViewManager
及其父類中聲明了ReactProp的方法,然後將其放到mPropSetters
中。現在我跟蹤的是Text組件,對應的是ReactTextViewManager。
public class ReactTextViewManager extends BaseViewManager<ReactTextView, ReactTextShadowNode> {···@ReactProp(name = ViewProps.ELLIPSIZE_MODE)@ReactProp(name = ViewProps.TEXT_ALIGN_VERTICAL)@ReactPropGroup(names = { ViewProps.BORDER_RADIUS, ViewProps.BORDER_TOP_LEFT_RADIUS, ViewProps.BORDER_TOP_RIGHT_RADIUS, ViewProps.BORDER_BOTTOM_RIGHT_RADIUS, ViewProps.BORDER_BOTTOM_LEFT_RADIUS }, defaultFloat = YogaConstants.UNDEFINED)@ReactPropGroup(names = { "borderColor", "borderLeftColor", "borderRightColor", "borderTopColor", "borderBottomColor" }, customType = "Color")···}
但我並沒有找到backgroundColor和Color對應的聲明。別急,不是還會遞歸父類么?果然在父類BaseViewManager
找到了。
@ReactProp(name = PROP_BACKGROUND_COLOR, defaultInt = Color.TRANSPARENT, customType = "Color") public void setBackgroundColor(T view, int backgroundColor) { view.setBackgroundColor(backgroundColor); }
其根本邏輯還是Native的View的setBackgroundColor()方法。
那麼還有一個問題,我始終沒有找到color的聲明。猜測color應該也是以@ReactProp聲明的,只不過沒有在ViewManager的類里。突然想到所有JS的View都會在Native被封裝為ShadowNode,每個ViewManager都會有個createShadowNodeInstance()方法來建立和ShadowNode的關係。ReactTextViewManager
對應的是ReactTextShadowNode
。在這個類里不僅找到了color的聲明,還找到了非常熟悉的fontSize屬性的聲明。
public class ReactTextShadowNode extends LayoutShadowNode {···@ReactProp(name = ViewProps.FONT_SIZE, defaultFloat = UNSET)public void setFontSize(float fontSize) { mFontSizeInput = fontSize; if (fontSize != UNSET) { fontSize = mAllowFontScaling ? (float) Math.ceil(PixelUtil.toPixelFromSP(fontSize)) : (float) Math.ceil(PixelUtil.toPixelFromDIP(fontSize)); } mFontSize = (int) fontSize; markUpdated(); }@ReactProp(name = ViewProps.COLOR)public void setColor(@Nullable Integer color) { mIsColorSet = (color != null); if (mIsColorSet) { mColor = color; } markUpdated(); }···}
但是說好的調用setTextColor()的,別只賦值給mColor就不管了~
好吧,那就全局搜一下mColor是怎麼用的。在ReactTextShadowNode
的這個方法里找到了答案。
private static void buildSpannedFromTextCSSNode( ReactTextShadowNode textShadowNode, SpannableStringBuilder sb, List<SetSpanOperation> ops) { ··· ops.add(new SetSpanOperation(start, end, new ForegroundColorSpan(textShadowNode.mColor))); ops.add(new SetSpanOperation(start, end, new AbsoluteSizeSpan(textShadowNode.mFontSize))); ··· }
原來並沒有按照我猜測的調用setTextColor()套路出牌,而是使用SpannableString
來實現文本效果。
總結以上結論,Native保存了一份JS屬性和Native控制項屬性設置的映射表,所有flexbox的屬性設置都是由Native控制項的屬性設置API實現。
通過跟蹤源碼,發現渲染流程及原理遠不止本文所述,所以本文可能有理解誤差,歡迎指正。
推薦閱讀:
※Android四大組件之三(Content Provider)
※Android使用ViewPager實現左右循環滑動及輪播效果
※在Eclipse下搭建Android開發環境教程(2)
※多線程中的問題和非同步消息處理機制
※阿里雲OSS對象存儲OSS文件上傳
TAG:ReactNative | Android |