遷移老文章到掘金javascript
相關係列文章html
最近好像嘮叨了好多RN的東西╮(╯_╰)╭,嘮叨的我都以爲有點貧,就當隨手記筆記吧java
關於ReactNative的Image組件,我一直很好奇他內部具體的工做過程,這裏面有不少有意思的東西,畢竟Image這個東西即使是純源生開發也能夠作的很複雜很精妙,好比SDWebImage
的無比強大的網絡緩存,網圖控制,好比ASDK
裏面的asyncDisplay,好比YYWebImage
中身兼網絡緩存控制與異步高效解碼繪製。今天咱們來看看ReactNative是如何處理Image的node
Facebook的官方文檔:ImageComponentreact
從文檔中咱們能夠看到ImageComponent一共能夠讀取三種圖片,不管用那種方式,只要把他們賦值給source,好像圖片就能天然生效了ios
隨着jsbundle一塊兒打包的資源文件,在文檔中以這種方式require('./img/check.png')
使用,其中的路徑是圖片小相對於index.ios.js
這個文件的路徑。git
靜態圖片資源是什麼意思呢?咱們用RN確定是指望熱更新的,確定指望rn的js代碼與功能所須要用到的圖片,一塊兒隨着網絡下載的客戶端本地,從而生效,從而展示,因此這些圖片須要隨着js一塊兒被打包,當執行了node.js的打包命令後,會生成一個bundle目錄,這裏面有最核心的jsbundl也就是js代碼包,同時這裏面還有個asset目錄,裏面放着全部一塊兒打包的資源文件,這就是RN的靜態圖片資源的概念github
這裏要提到iOS的圖片資源管理,iOS會把全部的圖片打包進入app本身獨有的資源文件包之中,這部分圖片屬於APP管理,是隨着每一個app的包一塊兒提審,一塊兒發版,簡單地說這部分圖片的管理,不能隨着網絡下載隨意存放和讀取和更新,是純源生iOSAPP的資源管理與讀取的方案chrome
若是想在RN裏面,顯示這種源生APP資源的話就要經過{uri:name}
的方式,其中name是資源文件在源生管理器裏面的名字,這樣就能夠在RN的環境裏,讀取出native環境裏的資源數據庫
這個就很好理解了,恩,不從APP本地不管是RN包仍是native包裏面讀圖,直接從網絡里拉圖,{uri:url}
其中url是圖片的網絡地址
讀了以前的文章,咱們應該清楚,全部的RN的ImageComponent最終都會經過源生的UIModule,實現最終的源生的展示效果,那麼這個UIModule就是RCTImageView,你們能夠從源碼中看到這個類
關注一下這個類的- (void)setSource:(RCTImageSource *)source
方法,看起來全部在JS裏賦值給Image的Source的屬性,都會傳過來經過這個方法傳給RCTImageView,而後再經過RCTImageView的reloadImage
方法去讀取圖片內容,這部分後面還會講。
但我很驚訝與這裏面的代碼,剛纔咱們講到,RN是有三種大相徑庭的圖片讀取方式的,傳入的是三種大相徑庭的數據,並且是讀取大相徑庭的三種類型的圖片
在個人認知裏面,徹底不一樣的三種方案,在setSource
與reloadImage
裏面應該會按着三種方式,至少有個if else
之類的差別化處理,但出乎個人意料,RN在這兩處的代碼是徹底一致而且統一的,代碼一鼓作氣沒有任何分支處理
咱們寫這樣一段JS代碼,在一個頁面裏同時展示3種圖片
render() {
return (
<View>
<Image source={require('./res/kakaka.jpg')}/>
<Image source={{uri: 'ScreenCover_night'}}
style={{width: 40, height: 40}}/>
<Image source={{uri: 'https://facebook.github.io/react/img/logo_og.png'}}
style={{width: 50, height: 50}}/>
</View>
);
}
複製代碼
這3個文件真正執行的時候,在OC的setSource處打斷點卻發現大相徑庭的景象
本來的輸入參數
在setSource斷點裏徹底變了,徹底變成了imageURL
這太不符合個人認知了,爲何輸入參數會如此整齊劃一的統統變成了iOS下的URL?不管是本地URL仍是遠程URL,由於他們徹底被統一成了同一種URL類型,從而iOS的這兩處OC代碼徹底不須要if分支就能一個邏輯處理全部圖片
我很好奇究竟是哪段代碼處理了這種統一化?
若是想弄明白rn是如何把這三種方案統一的,那天然得從JS源碼入手看起,咱們將要很大程度的關注/node_module/react-native/Libraries/Image
這個目錄下的幾個關鍵JS文件。
想弄明白這裏面的運做過程,最好的辦法就是利用RN的chrome-debug方案,在關鍵位置上打上斷點,看看到底代碼調用棧是如何一步步執行的
那咱們的焦點就落在了目錄下這個Image.ios.js
的文件上,能夠看出來沒錯,這就是Image組件的JS源碼,咱們會看到這麼幾行
render: function() {
var source = resolveAssetSource(this.props.source) || {};
//balabalabala
//....中間代碼略去
}
return (
<RawImage {...this.props} style={style} resizeMode={resizeMode} tintColor={tintColor} source={source} /> ); }, 複製代碼
能夠知道,咱們傳給JS的Source屬性的輸入參數this.props.source
,到底是如何處理的 他看起來就是resolveAssetSource()
處理了一下原封不動的就進入後面的流程了,傳遞數據給native進行渲染的流程
這部分流程在這裏,咱們須要看一下/node_module/react-native/Libraries/ReactNative/ReactNativeBaseComponent.js
文件,全部的RN界面組件,不管是標籤文字,仍是圖片,地圖,轉菊花,都是經過這個BaseComponent來調用UIManger(也就是APIModule RCTUIManager的JS側入口)繪製到native上的。你們只關注mountComponent
這個方法就行了
mountComponent: function(rootID, transaction, context) {
//balabalabala
//....中間代碼略去
//...用來獲取Component的props
var updatePayload = ReactNativeAttributePayload.create(
this._currentElement.props,
this.viewConfig.validAttributes
);
//balabalabala
//....中間代碼略去 用來獲取nativeTopRootID
//... call OC 建立native界面組件
UIManager.createView(
tag,
this.viewConfig.uiViewClassName,
ReactNativeTagHandles.rootNodeIDToTag[nativeTopRootID],
updatePayload
);
//balabalabala
//....中間代碼略去
return {
rootNodeID: rootID,
tag: tag
};
}
複製代碼
咱們梳理一下過程
首先咱們三種example的輸入參數是
在最終mountComponent的時候,updatePayload獲取到的props裏面,咱們打斷點查看一下,看看通過了無數的JS代碼處理後,對於Image組件這塊的內存數據是怎樣的,實踐事後會發現,這裏的props必定會還有一個uri屬性,三種example此時的uri屬性分別是
JS代碼就到此爲止,走過UIManager.createView
以後,就進入了OC的代碼邏輯了,這個放在後面細說。
因而咱們會發現除了require的靜態圖片資源,輸入參數和輸出結果變化很是大之外,另外兩種example基本沒啥變化,咱們先從簡單的下手,看一看後兩種example是如何簡單處理的
順着剛纔貼出的代碼能夠知道,Image.ios.js
只是簡單的把{uri:xxx}
的輸入參數傳給resolveAssetSource.js
的resolveAssetSource方法
,處理一下,而後添加了幾個額外屬性,而後直接複製給this.props.source,以後就傳給了ReactNativeBaseComponent.js
了。
resolveAssetSource.js
的resolveAssetSource方法
更是簡單粗暴,由於咱們輸入的是{uri:xx}
他就是一個對象,這方法什麼也不作直接返回
if (typeof source === 'object') {
return source;
}
//其餘處理
複製代碼
因此咱們在UIManager最終傳值的時候,會看到一個跟咱們輸入數據沒啥變化的一個JS Object,只是多了幾個屬性而已
這個過程就比較複雜了,並且這個過程會涵蓋rn的打包,執行,兩大重要環節
rn的打包是經過在rn根目錄下,執行node.js的一行打包腳本命令,最終把咱們編輯過程當中的js業務文件,js框架文件,res資源文件,總體打包到bundle目錄之下,對於圖片來講,我本覺得只是把圖片換個打包目錄另存爲而已,但當我一步一步追蹤源碼的時候,我發現我錯了。
require('./res/kakaka.jpg')
舉例來講,在這個目錄下的kakaka.jpg文件會另存爲到bundle/asset/res/kakaka.jpg
這個位置,成爲rn包中的一部分。
但毫不僅僅是另存爲,打包腳本還會在圖片文件中植入一行js代碼,若是你在AssetRegistry.js
的registerAsset
方法打上斷點,去查看調用棧,你會發現,竟然調用棧裏的一行JS代碼,來自這個圖片文件,有圖爲證(牀說中貼吧各類往圖片裏藏老司機開車的種子鏈接,就是用這種方式╮(╯_╰)╭)
這行JS代碼是
module.exports = require("react-native/Libraries/Image/AssetRegistry").registerAsset({"__packager_asset":true,"fileSystemLocation":"/Users/Awhisper/Desktop/yuedu_RN_BRANCH/Main/YDReactNative/res","httpServerLocation":"/assets/res","width":1242,"height":150,"scales":[1],"files":["/Users/Awhisper/Desktop/yuedu_RN_BRANCH/Main/YDReactNative/res/kakaka.jpg"],"hash":"319fbd6959f45c18b1843e71d3bdd991","name":"kakaka","type":"jpg"});
複製代碼
這說明,在打包腳本執行的時候,打包腳本會把這個圖片的全部信息,包括打包前原來的絕對路徑,打包後的相對路徑,打包後的host路徑,打包後的文件hash,打包後的文件名全都以源碼js寫入的方式,寫進圖片文件裏。而且這個圖片文件還執行了一行代碼AssetRegistry. registerAsset
這個圖片雖然被植入了JS代碼,可是他並無馬上生效,但正由於圖片內部存在JS代碼,因此他能夠經過require(xxx)
的方式進行加載(其實RN也擴寫了require.js這個庫),也就是說當咱們在RN運行環節,一旦執行了require(圖片)
這行代碼,AssetRegistry. registerAsset
就會馬上被執行
打包完成了,圖片已經被植入了JS代碼,如今RN該開始運行了,一旦運行到<Image source={require('./res/kakaka.jpg')}/>
這句話時候,就至關於require了圖片內的js代碼,因而就執行了AssetRegistry. registerAsset
。
這個函數幹了些啥呢?他把圖片內被植入的js代碼中的一大堆圖片信息參數,全都push進了一個全局的數組裏,而且返回了一個索引值index
function registerAsset(asset: PackagerAsset): number {
return assets.push(asset);
}
複製代碼
當咱們Image.ios.js
獲取this.props.source的時候,咱們斷點查看var source
的值你會發現他是一個數字也就是1!這就是index
回到resolveAssetSource.js
的resolveAssetSource方法
,此時咱們的輸入參數已經不是一個JS Object了,而是一個數字1,因而天然也就沒有直接return,而是進一步處理
if (typeof source === 'object') {
return source;
}
//其餘處理
var asset = AssetRegistry.getAssetByID(source);
if (!asset) {
return null;
}
const resolver = new AssetSourceResolver(getDevServerURL(), getBundleSourcePath(), asset);
if (_customSourceTransformer) {
return _customSourceTransformer(resolver);
}
return resolver.defaultAsset();
複製代碼
沒錯他從全局的資源數組裏,按着index取出來那個含有資源詳細信息的字典,而且稍加處理和改造,返回給了Image.ios.js
這就是爲啥咱們從這個JS層的輸入參數
變成了這樣的JS層的輸出參數
上文提到三種example在JS層最終輸出參數是這樣的
他們會經過UIManager傳給OC的RCTUIManager從而進行建立和繪製,此時此刻你會發現,(1)與(3)已經變成了URL的樣式,已經能夠直接進行URLLoad了,可是(2)還不是一個URL,說明(2)還須要在OC層面進行轉換統一,這個轉換過程又發生在哪呢?
這咱們就要順着OC的RCTUIManager去追蹤了,在createView
方法裏面(上一篇源碼分析提到過RCTUIManager與RCTComponentData的關係,我就不細講了)
RCTUIManager-createView:xxxx
方法
RCTComponentData-setProps:forView:
方法(RCTUIManager line:910)
RCTComponentData-propBlockForKey:inDictionary:
方法(RCTComponentData line:343) (此處代碼有點繞,函數的做用是取出block,真正要看的是函數後面的括號執行block)RCTImageSource-RCTImageSource
方法
RCTImageSource-NSURL
方法沒錯,RCTImageSource-NSURL
就是關鍵,在這個函數裏若是發現url字符串能夠被轉化成NSURL,則直接return該NSURL(因此例子1,3沒有任何變化直接return),若是傳來的是像2例子那樣一個名字ScreenCover_night
,在這段代碼裏,會主動向iOS獨有的資源管理類[NSBundle mainBundle].resourcePath
來申請iOS本地資源路徑,從而將ScreenCover_night
轉化成真正意義上的URL
file:///Users/Awhisper/Library/Developer/CoreSimulator/Devices/2D84E82E-AEDD-4B7B-A59A-F44C3B6721F2/data/Containers/Bundle/Application/B80C514C-639F-42FE-812F-3ECF457BFEC8/yuedu.app/ScreenCover_night
複製代碼
輸入
通過了JS層的初步歸一,歸一處理了example(1)的狀況,變成了
通過RCTConvert,OC層的二次歸一,歸一處理了example(2)的狀況,變成了
如今全部的輸入source都已經不折不扣統一成了url,不管是遠程url,仍是本地磁盤文件url,因此後續的loadImage過程,就無需特別針對處理了,直接能夠進行load了
這是一個頗有趣的事情!
咱們都知道RN的網絡圖片是有緩存的,可是今天在羣裏討論的時候,卻發現了一個頗有意思的事情,我發現RN內部,不止一種圖片緩存的方案。
對於源生客戶端來講,一般會使用SDWebImage
這樣的第三方庫去處理網絡圖片,由於他有着很是強大的內存緩存,磁盤緩存,有着靈活的緩存管理手段,以圖片url爲key,統一在一個字典表裏進行存儲,不管是內存仍是磁盤。
RN也不例外,你能夠找到一個RCTImageStoreManager
的類,幹着相似的事情,以字典+urlKey的方式管理一堆UIImage,但奇怪的是,這個類竟然沒有任何方法調用。
RCTImageStoreManager
是一個APIModule,他含有RCT_EXPORT_MODULE()
代碼。也就是說JS層是能夠直接操做RCTImageStoreManager
的,可是順藤摸瓜尋找JS層是如何使用卻發現,只有2個JS文件使用了它,ImageStore.js
與ImageEditor.js
,有趣的事情來了,這兩個JS組件就好端端的躺在rn框架代碼裏,可是並無被任何人使用,沒有被Image組件直接使用,網上google了一下發現相關內容很是之少,只有極個別人會用一下。RN的英文官網也搜不到這兩個組件的介紹。
但正如我說,這一整套源生native緩存方案,不管是OC側的源碼仍是JS測的源碼就這麼好端端的呆在RN的框架源碼裏面,等待着被人使用,雖然幾乎沒有。若是你嘗試一下,發現一切都運做正常
搜reactnative image cache
相關字樣的時候,你能夠發現github上有不少第三方重新寫的一套相似ImageStore.js
與ImageEditor.js
的解決方案,看來要麼是有人以爲facebook寫好的現成的不夠牛逼,基於數據庫從新封裝了一套,要麼是有人壓根都不知道facebook寫好了一個,因而本身重寫了一遍,哈哈
總之fb本身寫好的那一套native緩存方案,存在感異常的低啊哈哈哈哈哈
都說了,fb本身的native緩存方案存在感如此之低,太多的人都不知道,github上的開源的解決方案其實普及率也沒那麼大,不少人用RN也沒用github上的三方緩存也沒用fb提供的緩存,但RN的圖片依然仍是有緩存功能的,這是爲啥?
這就回到了RCTImageView的reloadImage
函數了,它裏面會起一個NSURLSession去拉取網絡圖片數據,當拉取到圖片緩存數據後,會使用OC源生的NSURLCache緩存整個URLRequest
//RCTImageLoader-loadImageOrDataWithTag:xxx(line:390)
dispatch_async(_URLCacheQueue, ^{
// Cache the response
// TODO: move URL cache out of RCTImageLoader into its own module
BOOL isHTTPRequest = [request.URL.scheme hasPrefix:@"http"];
[strongSelf->_URLCache storeCachedResponse:
[[NSCachedURLResponse alloc] initWithResponse:response
data:data
userInfo:nil
storagePolicy:isHTTPRequest ? NSURLCacheStorageAllowed: NSURLCacheStorageAllowedInMemoryOnly]
forRequest:request];
// Process image data
processResponse(response, data, nil);
//clean up
[weakSelf dequeueTasks];
});
複製代碼
因此說,若是你沒有使用任何的native圖片cache方案,不管是fb提供的仍是三方的,rn依然會幫助你進行圖片緩存,使用的方法就是系統級NSURLCache的整個URLRequest的緩存,這個緩存是系統級的,會和你其餘的非rn的native的http緩存請求混在一塊兒處理(具體看NSURLCache的使用,native能夠自由的單開和共用)