Talk about ReactNative Image Component

遷移老文章到掘金javascript

相關係列文章html

最近好像嘮叨了好多RN的東西╮(╯_╰)╭,嘮叨的我都以爲有點貧,就當隨手記筆記吧java

關於ReactNative的Image組件,我一直很好奇他內部具體的工做過程,這裏面有不少有意思的東西,畢竟Image這個東西即使是純源生開發也能夠作的很複雜很精妙,好比SDWebImage的無比強大的網絡緩存,網圖控制,好比ASDK裏面的asyncDisplay,好比YYWebImage中身兼網絡緩存控制與異步高效解碼繪製。今天咱們來看看ReactNative是如何處理Image的node

ImageComponent的基本用法

Facebook的官方文檔:ImageComponentreact

從文檔中咱們能夠看到ImageComponent一共能夠讀取三種圖片,不管用那種方式,只要把他們賦值給source,好像圖片就能天然生效了ios

  • 與jsbundle一塊兒打包packag的資源文件(require方式)
  • iOS源生APP的ImageAsset內部被iOS管理的資源文件({uri:name}方式)
  • 網絡上的圖片({uri:url}方式)

靜態圖片資源

隨着jsbundle一塊兒打包的資源文件,在文檔中以這種方式require('./img/check.png')使用,其中的路徑是圖片小相對於index.ios.js這個文件的路徑。git

靜態圖片資源是什麼意思呢?咱們用RN確定是指望熱更新的,確定指望rn的js代碼與功能所須要用到的圖片,一塊兒隨着網絡下載的客戶端本地,從而生效,從而展示,因此這些圖片須要隨着js一塊兒被打包,當執行了node.js的打包命令後,會生成一個bundle目錄,這裏面有最核心的jsbundl也就是js代碼包,同時這裏面還有個asset目錄,裏面放着全部一塊兒打包的資源文件,這就是RN的靜態圖片資源的概念github

APP的圖片資源

這裏要提到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是有三種大相徑庭的圖片讀取方式的,傳入的是三種大相徑庭的數據,並且是讀取大相徑庭的三種類型的圖片

在個人認知裏面,徹底不一樣的三種方案,在setSourcereloadImage裏面應該會按着三種方式,至少有個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處打斷點卻發現大相徑庭的景象

本來的輸入參數

  • require('./res/kakaka.jpg')
  • {uri: 'ScreenCover_night'}
  • {uri: 'https://facebook.github.io/react/img/logo_og.png'}

在setSource斷點裏徹底變了,徹底變成了imageURL

  • http://localhost:8081/assets/res/kakaka.jpg?platform=ios&hash=319fbd6959f45c18b1843e71d3bdd991
  • 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
  • https://facebook.github.io/react/img/logo_og.png

這太不符合個人認知了,爲何輸入參數會如此整齊劃一的統統變成了iOS下的URL?不管是本地URL仍是遠程URL,由於他們徹底被統一成了同一種URL類型,從而iOS的這兩處OC代碼徹底不須要if分支就能一個邏輯處理全部圖片

我很好奇究竟是哪段代碼處理了這種統一化?

圖片源Source輸入參數在JS裏的歸一化處理

若是想弄明白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的輸入參數是

  • require('./res/kakaka.jpg')
  • {uri: 'ScreenCover_night'}
  • {uri: 'https://facebook.github.io/react/img/logo_og.png'}

在最終mountComponent的時候,updatePayload獲取到的props裏面,咱們打斷點查看一下,看看通過了無數的JS代碼處理後,對於Image組件這塊的內存數據是怎樣的,實踐事後會發現,這裏的props必定會還有一個uri屬性,三種example此時的uri屬性分別是

  • http://localhost:8081/assets/res/kakaka.jpg?platform=ios&hash=319fbd6959f45c18b1843e71d3bdd991
  • ScreenCover_night
  • https://facebook.github.io/react/img/logo_og.png

JS代碼就到此爲止,走過UIManager.createView以後,就進入了OC的代碼邏輯了,這個放在後面細說。

因而咱們會發現除了require的靜態圖片資源,輸入參數和輸出結果變化很是大之外,另外兩種example基本沒啥變化,咱們先從簡單的下手,看一看後兩種example是如何簡單處理的

{uri:xxx}的處理過程

順着剛纔貼出的代碼能夠知道,Image.ios.js只是簡單的把{uri:xxx}的輸入參數傳給resolveAssetSource.jsresolveAssetSource方法,處理一下,而後添加了幾個額外屬性,而後直接複製給this.props.source,以後就傳給了ReactNativeBaseComponent.js了。

resolveAssetSource.jsresolveAssetSource方法更是簡單粗暴,由於咱們輸入的是{uri:xx}他就是一個對象,這方法什麼也不作直接返回

if (typeof source === 'object') {
    return source;
}
//其餘處理
複製代碼

因此咱們在UIManager最終傳值的時候,會看到一個跟咱們輸入數據沒啥變化的一個JS Object,只是多了幾個屬性而已

require(imagepath)的處理過程

這個過程就比較複雜了,並且這個過程會涵蓋rn的打包,執行,兩大重要環節

  • 一. RN的打包環節

rn的打包是經過在rn根目錄下,執行node.js的一行打包腳本命令,最終把咱們編輯過程當中的js業務文件,js框架文件,res資源文件,總體打包到bundle目錄之下,對於圖片來講,我本覺得只是把圖片換個打包目錄另存爲而已,但當我一步一步追蹤源碼的時候,我發現我錯了。

require('./res/kakaka.jpg') 舉例來講,在這個目錄下的kakaka.jpg文件會另存爲到bundle/asset/res/kakaka.jpg這個位置,成爲rn包中的一部分。

但毫不僅僅是另存爲,打包腳本還會在圖片文件中植入一行js代碼,若是你在AssetRegistry.jsregisterAsset方法打上斷點,去查看調用棧,你會發現,竟然調用棧裏的一行JS代碼,來自這個圖片文件,有圖爲證(牀說中貼吧各類往圖片裏藏老司機開車的種子鏈接,就是用這種方式╮(╯_╰)╭)

chrome 斷點

這行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就會馬上被執行

  • 二. RN的運行環節

打包完成了,圖片已經被植入了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.jsresolveAssetSource方法,此時咱們的輸入參數已經不是一個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層的輸入參數

  • require('./res/kakaka.jpg')

變成了這樣的JS層的輸出參數

  • http://localhost:8081/assets/res/kakaka.jpg?platform=ios&hash=319fbd6959f45c18b1843e71d3bdd991

圖片源Source輸入參數在OC裏的歸一化處理

上文提到三種example在JS層最終輸出參數是這樣的

  • http://localhost:8081/assets/res/kakaka.jpg?platform=ios&hash=319fbd6959f45c18b1843e71d3bdd991
  • ScreenCover_night
  • https://facebook.github.io/react/img/logo_og.png

他們會經過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)
      • invocation(invocation去執行setter,真正的setSource,而且伴隨着複雜切晦澀的宏處理,在宏的處理過程當中,把js傳過來的json字典會經過RCTConvert轉換成RCTImageSource,推薦直接在下面的位置斷點看效果)
        • 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
複製代碼

圖片源Source輸入參數歸一

輸入

  • require('./res/kakaka.jpg')
  • {uri: 'ScreenCover_night'}
  • {uri: 'https://facebook.github.io/react/img/logo_og.png'}

通過了JS層的初步歸一,歸一處理了example(1)的狀況,變成了

  • http://localhost:8081/assets/res/kakaka.jpg?platform=ios&hash=319fbd6959f45c18b1843e71d3bdd991
  • ScreenCover_night
  • https://facebook.github.io/react/img/logo_og.png

通過RCTConvert,OC層的二次歸一,歸一處理了example(2)的狀況,變成了

  • http://localhost:8081/assets/res/kakaka.jpg?platform=ios&hash=319fbd6959f45c18b1843e71d3bdd991
  • 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
  • https://facebook.github.io/react/img/logo_og.png

如今全部的輸入source都已經不折不扣統一成了url,不管是遠程url,仍是本地磁盤文件url,因此後續的loadImage過程,就無需特別針對處理了,直接能夠進行load了

關於RCTImageView的緩存

這是一個頗有趣的事情!

咱們都知道RN的網絡圖片是有緩存的,可是今天在羣裏討論的時候,卻發現了一個頗有意思的事情,我發現RN內部,不止一種圖片緩存的方案。

RN的源生native緩存方案(存在感極低)

對於源生客戶端來講,一般會使用SDWebImage這樣的第三方庫去處理網絡圖片,由於他有着很是強大的內存緩存,磁盤緩存,有着靈活的緩存管理手段,以圖片url爲key,統一在一個字典表裏進行存儲,不管是內存仍是磁盤。

RN也不例外,你能夠找到一個RCTImageStoreManager的類,幹着相似的事情,以字典+urlKey的方式管理一堆UIImage,但奇怪的是,這個類竟然沒有任何方法調用。

RCTImageStoreManager是一個APIModule,他含有RCT_EXPORT_MODULE()代碼。也就是說JS層是能夠直接操做RCTImageStoreManager的,可是順藤摸瓜尋找JS層是如何使用卻發現,只有2個JS文件使用了它,ImageStore.jsImageEditor.js,有趣的事情來了,這兩個JS組件就好端端的躺在rn框架代碼裏,可是並無被任何人使用,沒有被Image組件直接使用,網上google了一下發現相關內容很是之少,只有極個別人會用一下。RN的英文官網也搜不到這兩個組件的介紹。

但正如我說,這一整套源生native緩存方案,不管是OC側的源碼仍是JS測的源碼就這麼好端端的呆在RN的框架源碼裏面,等待着被人使用,雖然幾乎沒有。若是你嘗試一下,發現一切都運做正常

第三方擴展的native緩存方案

reactnative image cache相關字樣的時候,你能夠發現github上有不少第三方重新寫的一套相似ImageStore.jsImageEditor.js的解決方案,看來要麼是有人以爲facebook寫好的現成的不夠牛逼,基於數據庫從新封裝了一套,要麼是有人壓根都不知道facebook寫好了一個,因而本身重寫了一遍,哈哈

總之fb本身寫好的那一套native緩存方案,存在感異常的低啊哈哈哈哈哈

RN的http圖片緩存方案

都說了,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能夠自由的單開和共用)

相關文章
相關標籤/搜索