本文基于React Native 0.32 對(duì) 官方提供的
Image
組件進(jìn)行分析徽龟。
Image
是一個(gè)用于顯示多種圖片類型的React組件,它可以顯示來(lái)自網(wǎng)絡(luò)唉地,assets目錄据悔,本地SD卡,用戶自定義目錄的圖片耘沼。官網(wǎng)給出的用法如下:
renderImages() {
return (
<View>
<Image
style={styles.icon}
source={require('./icon.png')}
/>
<Image
style={styles.logo}
source={{uri: 'http://facebook.github.io/react/img/logo_og.png'}}
/>
</View>
);
}
那么它是如何實(shí)現(xiàn)的呢屠尊?為了更好地對(duì)RN如何封裝一個(gè)自定義組件進(jìn)行說明,下面我將分別從JS端和Native端的源碼進(jìn)行分析耕拷。
React端
在JS端讼昆,Image組件的源碼位于
react-native/Libraries/Image/Image.android.js
可以看到,盡管RN在0.18后全面轉(zhuǎn)向了ES6骚烧,但組件Image仍然采用了ES5風(fēng)格的JavaScript浸赫。
屬性的定義
Image在propTypes
中定義了該組件支持的各種屬性及其屬性值的類型,這里我們著重介紹source
屬性和style
屬性的定義赃绊,而對(duì)于組件接收到的屬性值的處理則放在了render
函數(shù)中
圖片URI
組件Image自身定義了如下幾種屬性:
source {uri: string}, number
: 表示圖片資源的位置既峡。
source: PropTypes.oneOfType([//可以接受如下三種形式的資源位置
PropTypes.shape({
//`uri`是一個(gè)表示圖片的資源標(biāo)識(shí)的字符串,它可以是一個(gè)http地址或是一個(gè)本地文件路徑
uri: PropTypes.string,
}),
// 也可以是一個(gè)通過函數(shù)`require('./path/to/image.png')`獲取到靜態(tài)資源
PropTypes.number,
//也可以接受一個(gè)包含了多個(gè)圖片uri的數(shù)組碧查,在數(shù)組里可以指定每個(gè)圖片顯示的寬高
PropTypes.arrayOf(
PropTypes.shape({
uri: PropTypes.string,
width: PropTypes.number,
height: PropTypes.number,
}))
]),
loadingIndicatorSource
: 表示在真正圖片在加載過程中所顯示的圖片运敢,在加載網(wǎng)絡(luò)圖片的場(chǎng)景下特別有用。
loadingIndicatorSource: PropTypes.oneOfType([//該屬性與source的定義相似忠售,但是不支持多圖传惠。
PropTypes.shape({
uri: PropTypes.string,
}),
// Opaque type returned by require('./image.jpg')
PropTypes.number,
]),
組件樣式style
的定義
style
: 定義了Image這個(gè)組件可以接收的樣式。
style: StyleSheetPropType(ImageStylePropTypes)
在ImageStylePropTypes
中定義了開發(fā)者可以為Image設(shè)置用到的style屬性:
var ImageStylePropTypes = {
//引入其他公用屬性
...LayoutPropTypes,
...ShadowPropTypesIOS,
...TransformPropTypes,
//可以設(shè)置圖片的調(diào)整模式
resizeMode: ReactPropTypes.oneOf(Object.keys(ImageResizeMode)),
backfaceVisibility: ReactPropTypes.oneOf(['visible', 'hidden']),
//背景色
backgroundColor: ColorPropType,
//邊框色
borderColor: ColorPropType,
//邊框?qū)挾? borderWidth: ReactPropTypes.number,
//邊框圓角度數(shù)
borderRadius: ReactPropTypes.number,
overflow: ReactPropTypes.oneOf(['visible', 'hidden']),
tintColor: ColorPropType,
opacity: ReactPropTypes.number,
overlayColor: ReactPropTypes.string,
borderTopLeftRadius: ReactPropTypes.number,
borderTopRightRadius: ReactPropTypes.number,
borderBottomLeftRadius: ReactPropTypes.number,
borderBottomRightRadius: ReactPropTypes.number,
};
ReactNative把LayoutPropTypes
等一些公用的style屬性提取出來(lái)稻扬,為了便于其他組件復(fù)用卦方。
其他屬性
progressiveRenderingEnabled
:表示是否采用漸進(jìn)式加載,漸進(jìn)式加載時(shí)圖片會(huì)從模糊到清晰漸漸呈現(xiàn)泰佳。
fadeDuration
: 圖片淡入淡出時(shí)間盼砍,毫秒。
resizeMode
: 決定當(dāng)組件尺寸和圖片尺寸不成比例的時(shí)候如何調(diào)整圖片的大小逝她,目前支持以下幾種模式:
-
cover
: 在保持圖片寬高比的前提下縮放圖片浇坐,直到寬度和高度都大于等于容器視圖的尺寸(如果容器有-padding內(nèi)襯的話,則相應(yīng)減去)黔宛。note
:這樣圖片完全覆蓋甚至超出容器近刘,容器中不留任何空白。 -
contain
: 在保持圖片寬高比的前提下縮放圖片,直到寬度和高度都小于等于容器視圖的尺寸(如果容器有padding內(nèi)襯的話跌宛,則相應(yīng)減去)酗宋。note
:這樣圖片完全被包裹在容器中,容器中可能留有空白 -
stretch
: 拉伸圖片且不維持寬高比疆拘,直到寬高都剛好填滿容器蜕猫。 -
center
: 居中不拉伸。
另外哎迄,Image還可以通過屬性指定圖片加載階段的回調(diào)函數(shù):
onLoad
: 圖片加載成功完成時(shí)調(diào)用此回調(diào)函數(shù)回右。onLoadStart
:圖片加載開始時(shí)調(diào)用。onLoadEnd
:圖片加載結(jié)束后漱挚,無(wú)論成功還是失敗翔烁,調(diào)用此回調(diào)函數(shù)。
Mixin
在 React component 構(gòu)建過程中旨涝,為了將同樣的功能添加到多個(gè)組件當(dāng)中蹬屹,可以將這些通用的功能包裝成一個(gè)mixin,然后導(dǎo)入到組件中白华。(ES6不支持mixin)
Image組件同樣引入了Mixin對(duì)象:
mixins: [NativeMethodsMixin]
Image所用到的主要功能是setNativeProps
函數(shù):
setNativeProps: function (nativeProps) {
if (process.env.NODE_ENV !== 'production') {
warnForStyleProps(nativeProps, this.viewConfig.validAttributes);
}
var updatePayload = ReactNativeAttributePayload.create(nativeProps, this.viewConfig.validAttributes);
UIManager.updateView(findNodeHandle(this), this.viewConfig.uiViewClassName, updatePayload);
}
我們都知道慨默,React Diff會(huì)計(jì)算出Virtual Dom中真正變化的部分并進(jìn)行渲染,而setNativeProps
函數(shù)的作用在于直接將屬性對(duì)象傳遞給Native層弧腥,不經(jīng)過diff這個(gè)過程厦取。這就意味著,如果不在接下來(lái)的render過程中包含這些屬性管搪,這些屬性仍然會(huì)起作用虾攻。
那么,Image組件會(huì)傳遞哪些屬性給Native呢更鲁?默認(rèn)情況下霎箍,Image設(shè)置并傳遞了viewConfig
對(duì)象:
viewConfig: {
uiViewClassName: 'RCTView',
validAttributes: ReactNativeViewAttributes.RCTView,
}
而在組件的生命周期回調(diào)函數(shù)componentWillMount
和componentWillReceiveProps
則會(huì)更新屬性值:
_updateViewConfig: function(props) {
if (props.children) {//有子組件
this.viewConfig = {
uiViewClassName: 'RCTView',
validAttributes: ReactNativeViewAttributes.RCTView,
};
} else {
this.viewConfig = {//無(wú)子組件
uiViewClassName: 'RCTImageView',
validAttributes: ImageViewAttributes,
};
}
},
componentWillMount: function() {
this._updateViewConfig(this.props);
},
componentWillReceiveProps: function(nextProps) {
this._updateViewConfig(nextProps);
},
可以看到,如果Image有子組件的情況下則會(huì)傳遞ReactNativeViewAttributes.RCTView
,否則會(huì)傳遞ImageViewAttributes
:
var ImageViewAttributes = merge(ReactNativeViewAttributes.UIView, {
src: true,
loadingIndicatorSrc: true,
resizeMode: true,
progressiveRenderingEnabled: true,
fadeDuration: true,
shouldNotifyLoadEvents: true,
});
渲染
作為一個(gè)組件岁经,Image首先需要對(duì)各種屬性進(jìn)行相應(yīng)的處理工作朋沮,然后根據(jù)開發(fā)者設(shè)置的屬性值,相應(yīng)的業(yè)務(wù)場(chǎng)景以及組件狀態(tài)渲染出相應(yīng)的界面缀壤。這部分邏輯在render
函數(shù)中定義。
source
屬性解析
Image首先會(huì)對(duì)開發(fā)者傳入的source
進(jìn)行解析纠亚,判斷需要從哪里加載圖片:
const source = resolveAssetSource(this.props.source);
const loadingIndicatorSource = resolveAssetSource(this.props.loadingIndicatorSource);
來(lái)看一下resolveAssetSource.js
中的路徑解析邏輯:
//由屬性定義可知塘慕,這里傳入的source值可以是一個(gè)uri對(duì)象,或者require函數(shù)返回的數(shù)字
function resolveAssetSource(source: any): ?ResolvedAssetSource {
if (typeof source === 'object') {//如果是uri對(duì)象則不作處理直接返回
return source;
}
//獲取靜態(tài)圖片的基本信息
var asset = AssetRegistry.getAssetByID(source);
if (!asset) {
return null;
}
const resolver = new AssetSourceResolver(getDevServerURL(), getBundleSourcePath(), asset);
if (_customSourceTransformer) {
return _customSourceTransformer(resolver);
}
return resolver.defaultAsset();
}
這里創(chuàng)建了AssetSourceResolver
蒂胞,并傳入了三個(gè)參數(shù):
-
getDevServerURL
: 如果從Node服務(wù)器加載JS图呢,則返回相應(yīng)路徑;
function getDevServerURL(): ?string {
if (_serverURL === undefined) {
var scriptURL = SourceCode.scriptURL;//調(diào)用Native module獲取JSbundle路徑
var match = scriptURL && scriptURL.match(/^https?:\/\/.*?\//);
if (match) {
// 從網(wǎng)絡(luò)中獲取JS
_serverURL = match[0];
} else {
// 從本地加載JS文件
_serverURL = null;
}
}
return _serverURL;
}
-
getBundleSourcePath
:如果用戶自定義了JS文件路徑,則返回?zé)o協(xié)議頭的路徑蛤织;否則返回空
function getBundleSourcePath(): ?string {
if (_bundleSourcePath === undefined) {
const scriptURL = SourceCode.scriptURL;
if (!scriptURL) {//未傳遞JS路徑
_bundleSourcePath = null;
return _bundleSourcePath;
}
if (scriptURL.startsWith('assets://')) {
// 未指定JS文件路徑赴叹,離線時(shí)默認(rèn)從asset目錄加載
_bundleSourcePath = null;
return _bundleSourcePath;
}
if (scriptURL.startsWith('file://')) {
//如果開發(fā)者指定了JS的目錄,則返回去除協(xié)議頭部的文件路徑
_bundleSourcePath = scriptURL.substring(7, scriptURL.lastIndexOf('/') + 1);
} else {
_bundleSourcePath = scriptURL.substring(0, scriptURL.lastIndexOf('/') + 1);
}
}
return _bundleSourcePath;
}
-
asset
:使用AssetRegistry.getAssetByID
返回的圖片基本信息
這里需要注意的是指蚜,RN允許開發(fā)者在Native端自定義JS的加載路徑乞巧,在JS端可以調(diào)用SourceCode.scriptURL
來(lái)獲取。如果開發(fā)者未指定JSbundle的路徑摊鸡,則在離線環(huán)境下返回asset目錄绽媒,開發(fā)環(huán)境下從node服務(wù)器讀取。
通過上述三個(gè)參數(shù):JSbundle在node服務(wù)器的路徑免猾,JS在本地的路徑以及圖片的基本信息是辕,RN構(gòu)建了AssetSourceResolver
對(duì)象,并調(diào)用了defaultAsset()
:
defaultAsset(): ResolvedAssetSource {
if (this.isLoadedFromServer()) {//從服務(wù)器加載,返回url
return this.assetServerURL();
}
if (Platform.OS === 'android') {
return this.isLoadedFromFileSystem() ?
this.drawableFolderInBundle() ://從自定義路徑加載
this.resourceIdentifierWithoutScale();//從asset目錄加載
} else {
return this.scaledAssetPathInBundle();
}
}
defaultAsset()
方法其實(shí)根據(jù)具體的場(chǎng)景返回了一個(gè)包含了圖片加載所需信息的對(duì)象而已猎提。
值得我們注意的是获三,如果開發(fā)者未指定JSbundle的加載路徑,則會(huì)調(diào)用resourceIdentifierWithoutScale
方法锨苏,該方法中會(huì)調(diào)用assetPathUtils
對(duì)圖片的加載路徑進(jìn)行處理疙教,把圖片目錄/圖片名稱
的格式處理成圖片目錄_圖片名稱
的形式。(這是由于打包命令同樣會(huì)調(diào)用assetPathUtils
對(duì)資源文件進(jìn)行處理)
根據(jù)上述代碼的分析蚓炬,對(duì)于source屬性的處理解析而言松逊,我們可以得出結(jié)論如下:
- 通過uri指定圖片路徑,則對(duì)source屬性不做處理肯夏,直接返回包含了uri字符串的對(duì)象经宏。
- 通過require()請(qǐng)求靜態(tài)資源的方式加載圖片,則會(huì)根據(jù)加載位置的不同返回路徑不同的對(duì)象:
- 開發(fā)環(huán)境下驯击,從node server加載JS 烁兰,則返回類似
http://localhost:8081/index.android.bundle?platform=android&dev=true
的路徑。 - 離線狀態(tài)下徊都,如果未指定JS路徑沪斟,則返回經(jīng)過處理的asset路徑,例如:
- 如果用戶未指定JS路徑暇矫,則返回用戶自定義的路徑主之,例如
file:///sdcard/AwesomeModule/drawable-mdpi/icon.png
.
style
處理
通過resolveAssetSource
對(duì)source屬性解析后,我們得到了一個(gè)包含了圖片加載信息的對(duì)象李根,并利用該對(duì)象中的信息創(chuàng)建了要渲染的樣式style
槽奕,并將source屬性封裝為一個(gè)含有uri對(duì)象的數(shù)組:
const {width, height} = source;
//讀取到圖片的寬高,Image定義的style房轿,開發(fā)者自定義的style
style = flattenStyle([{width, height}, styles.base, this.props.style]);
sources = [{uri: source.uri}];
函數(shù)flattenStyle
接收了一個(gè)包含各類樣式對(duì)象的數(shù)組粤攒。通過遞歸所森,flattenStyle
把數(shù)組中的樣式都進(jìn)行了整理,合并為一個(gè)統(tǒng)一的style
對(duì)象夯接。
渲染組件
在組件渲染之前焕济,Image把之前處理過的style,source以及開發(fā)者傳入的組件的屬性進(jìn)行了整理合并:
const nativeProps = merge(this.props, {
style,//把style解析合并傳遞給native
shouldNotifyLoadEvents: !!(onLoadStart || onLoad || onLoadEnd),
src: sources,//歷史原因?qū)е翹ative端對(duì)應(yīng)的屬性名為src盔几,但開發(fā)者在react端要用source
loadingIndicatorSrc: loadingIndicatorSource ? loadingIndicatorSource.uri : null,
});
通過對(duì)開發(fā)者傳入的組件的屬性晴弃,樣式以及source等屬性進(jìn)行合并后,根據(jù)不同的業(yè)務(wù)場(chǎng)景渲染出相應(yīng)的組件:
if (nativeProps.children) {
//如果Image組件中包含了其他組件
const containerStyle = filterObject(style, (val, key) => !ImageSpecificStyleKeys.has(key));
const imageStyle = filterObject(style, (val, key) => ImageSpecificStyleKeys.has(key));
const imageProps = merge(nativeProps, {
style: [imageStyle, styles.absoluteImage],
children: undefined,
});
return (
<View style={containerStyle}>
<RKImage {...imageProps}/>
{this.props.children}
</View>
);
} else {
if (this.context.isInAParentText) {//如果是TextView內(nèi)嵌Image
return <RCTTextInlineImage {...nativeProps}/>;
} else {//渲染普通情況下的Image組件
return <RKImage {...nativeProps}/>;
}
}
Image組件提供了兩種圖片展示方式问欠,內(nèi)嵌于TextView中的圖片和普通圖片:
var RKImage = requireNativeComponent('RCTImageView', Image, cfg);
var RCTTextInlineImage = requireNativeComponent('RCTTextInlineImage', Image, cfg);
可以看到肝匆,內(nèi)嵌圖片的情況下引用了Native端實(shí)現(xiàn)的RCTTextInlineImage
組件,而普通圖片則使用了RCTImageView
顺献。
通過對(duì)Image組件在JavaScript端代碼進(jìn)行分析旗国,我們了解該組件是如何定義屬性以及如何根據(jù)開發(fā)者所設(shè)置的屬性渲染出不同的效果。而對(duì)于這些屬性所設(shè)置的效果進(jìn)行顯示和響應(yīng)則要靠Native層的實(shí)現(xiàn)注整。
Native端
我們知道能曾,ReactNative的圖片相關(guān)處理工作是交給圖片庫(kù)Fresco完成的,這里我們不去深究細(xì)節(jié)肿轨,只去關(guān)注組件本身的實(shí)現(xiàn)邏輯寿冕。Image組件在native端實(shí)的現(xiàn)位于:
.../react/views/image/ReactImageManager.java
對(duì)于自定義的組件,在Native端需要?jiǎng)?chuàng)建一個(gè)ReactImageManager
負(fù)責(zé)進(jìn)行Native組件的創(chuàng)建椒袍,相關(guān)屬性的管理驼唱,事件的響應(yīng)等工作,而真正的具體實(shí)現(xiàn)操作則要放在Native組件中完成驹暑。
同樣玫恳,Image組件通過ReactImageManager
來(lái)完成Native組件ReactImageView
的創(chuàng)建和管理:
public ReactImageView createViewInstance(ThemedReactContext context) {
return new ReactImageView(
context,
getDraweeControllerBuilder(),
getCallerContext());
}
//與react端關(guān)聯(lián)
public String getName() {
return "RCTImageView";
}
通過注解@ReactProp
或ReactPropGroup
,ReactImageManager
導(dǎo)出了暴露給React端的屬性設(shè)置方法优俘,將相關(guān)屬性值交給Native組件ReactImageView
京办,以src
屬性為例:
@ReactProp(name = "src")
public void setSource(ReactImageView view, @Nullable ReadableArray sources) {
view.setSource(sources);
}
public void setSource(@Nullable ReadableArray sources) {
mSources.clear();
if (sources != null && sources.size() != 0) {
if (sources.size() == 1) {
mSources.add(new ImageSource(getContext(), sources.getMap(0).getString("uri")));
} else {
for (int idx = 0; idx < sources.size(); idx++) {
ReadableMap source = sources.getMap(idx);
mSources.add(new ImageSource(
getContext(),
source.getString("uri"),
source.getDouble("width"),
source.getDouble("height")));
}
}
}
mIsDirty = true;
}
ReactImageView
維護(hù)了相關(guān)的屬性對(duì)象,并在接收到React端傳遞過來(lái)的屬性值后進(jìn)行相應(yīng)處理解析帆焕,但此時(shí)不會(huì)刷新組件惭婿,只會(huì)標(biāo)記一下該組件需要刷新。而統(tǒng)一的刷新操作則放在ReactImageManager
的onAfterUpdateTransaction
方法中進(jìn)行:
protected void onAfterUpdateTransaction(ReactImageView view) {
super.onAfterUpdateTransaction(view);
view.maybeUpdateView();
}
具體的刷新實(shí)現(xiàn)是通過圖片庫(kù)Fresco進(jìn)行實(shí)現(xiàn)的叶雹,這里不再詳細(xì)分析财饥。
總結(jié)
通過對(duì)整個(gè)Image組件的分析,我們可以看到React端定義了組件可以使用的屬性類型和事件折晦,并在渲染時(shí)將它們傳遞給Native層佑力;Native層通過ViewManager
來(lái)對(duì)Native組件進(jìn)行對(duì)傳遞過來(lái)的屬性值進(jìn)行管理,而具體的實(shí)現(xiàn)則放在Native組件中筋遭。
如有疏漏之處,還請(qǐng)各位反饋。