2017-09-09 13:57:09

React Native: 缓存iOS图片

相较于Android和iOS, React Native只能说属于玩票性质

使用React Native写iOS时,很容易发现iOS上的很多图片并没有缓存,并且经常会出现一些图片无法显示的问题。对于一个正常的移动App来说,这是一个致命的问题。本文介绍一下如何解决它

追根溯源

https://github.com/facebook/react-native/blob/master/Libraries/Image/RCTImageCache.m#L70

static const NSUInteger RCTMaxCachableDecodedImageSizeInBytes = 1048576; // 1MB
...
  CGFloat bytes = image.size.width * image.size.height * image.scale * image.scale * 4;
  if (bytes <= RCTMaxCachableDecodedImageSizeInBytes) {
    [self->_decodedImageCache setObject:image
                                 forKey:cacheKey
                                   cost:bytes];
  }

不出所料,iOS上RN的缓存逻辑一如既往地令人惊奇,首先1MB的最大缓存限制是什么鬼,为什么要设置这个限制?其次计算图片大小竟然是用宽缩放缩放4来计算的,如下正则里挑出任何一种图片都能把这个逻辑吊起来打:

const imagePattern = new RegExp('(ai|bmp|bw|cin|cr2|crw|dcr|dng|dib|dpx|eps|erf|exr|gif|hdr|icb|iff|jpe|jpeg|jpg|mos|mrw|nef|orf|pbm|pef|pct|pcx|pdf|pic|pict|png|ps|psd|pxr|raf|raw|rgb|rgbe|rla|rle|rpf|sgi|srf|tdi|tga|tif|tiff|vda|vst|x3f|xyze)', 'i')

如何修复

显而易见本文的问题是图片没有被iOS内部的逻辑缓存,那么在不更改原生代码的情况下,我们自己先把图片下载到本地即可。这里推荐如下的库

react-native-cached-image

这个库是跨平台的,本文只用到iOS的部分,因为android上并没有图片无法被缓存的问题。依照文档进行安装后,使用如下语句将Image组件替换掉即可。

import { CachedImage } from 'react-native-cached-image'
if (isIOS) {

  class CachedImageWrapper extends Component<any, any> {
    constructor(props) {
      super(props)
    }

    renderImage = props => <Image ref={'cachedImage'} {...props}/>

    render() {
      return <CachedImage {...this.props} renderImage={this.renderImage}/>
    }
  }
  for (const i in Image) CachedImageWrapper[i] = Image[i]

与前文中介绍的一样,这里renderImage函数中的Image组件不能通过import的形式拿引用,否则会造成无限循环,使用如下语句导入Image才行

import ReactNative from 'react-native'

const { Image } = ReactNative

修复react-native-cached-image的问题

react-native-cached-image本身也有巨大的问题(RN的代码都这样了,我也不期望第三方代码好到哪里去),在模拟器中重复安装两次app,即可触发这样的场景:所有之前被缓存的图片全部是一片空白。照例查询源代码,发现获得缓存图片路径的语句如下:

// https://github.com/kfiroo/react-native-cached-image/blob/master/ImageCacheManager.js#L37
return urlCache.get(cacheableUrl)
            .then(filePath => {
                if (!filePath) {
                    // console.log('ImageCacheManager: cache miss', cacheableUrl);
                    throw new Error('URL expired or not in cache');
                }
                // console.log('ImageCacheManager: cache hit', cacheableUrl);
                return filePath;
            })
            // url is not found in the cache or is expired
            .catch(() => {
                const filePath = path.getImageFilePath(cacheableUrl, options.cacheLocation);
                // remove expired file if exists
                return fs.deleteFile(filePath)
                    // get the image to cache (download / copy / etc)
                    .then(() => getCachedFile(filePath))
                    // add to cache
                    .then(() => urlCache.set(cacheableUrl, filePath, options.ttl))
                    // return filePath

urlCache是AsyncStorage的一个包装,上面的逻辑根本没有检查图片路径是否存在,因为模拟器每次安装时app的CacheDir都不同,因此上次缓存下来的图片路径当然在新的路径里找不到了,此时给Image组件一个onError方法,可以看到报错,说图片找不到。

最后,依照这个文件进行修改,即可修复这个从底层到第三方都十分坑爹的问题了。

本文链接:https://smallpath.me/post/react-native-ios-cached-image

-- EOF --