ASMultiplexImageNode.mm 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883
  1. //
  2. // ASMultiplexImageNode.mm
  3. // AsyncDisplayKit
  4. //
  5. // Copyright (c) 2014-present, Facebook, Inc. All rights reserved.
  6. // This source code is licensed under the BSD-style license found in the
  7. // LICENSE file in the root directory of this source tree. An additional grant
  8. // of patent rights can be found in the PATENTS file in the same directory.
  9. //
  10. #import <AsyncDisplayKit/ASMultiplexImageNode.h>
  11. #import <AssetsLibrary/AssetsLibrary.h>
  12. #import <AsyncDisplayKit/ASAvailability.h>
  13. #import <AsyncDisplayKit/ASDisplayNode+FrameworkSubclasses.h>
  14. #import <AsyncDisplayKit/ASDisplayNodeExtras.h>
  15. #import <AsyncDisplayKit/ASPhotosFrameworkImageRequest.h>
  16. #import <AsyncDisplayKit/ASEqualityHelpers.h>
  17. #import <AsyncDisplayKit/ASInternalHelpers.h>
  18. #if PIN_REMOTE_IMAGE
  19. #import <AsyncDisplayKit/ASPINRemoteImageDownloader.h>
  20. #else
  21. #import <AsyncDisplayKit/ASBasicImageDownloader.h>
  22. #endif
  23. NSString *const ASMultiplexImageNodeErrorDomain = @"ASMultiplexImageNodeErrorDomain";
  24. static NSString *const kAssetsLibraryURLScheme = @"assets-library";
  25. static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0};
  26. /**
  27. @abstract Signature for the block to be performed after an image has loaded.
  28. @param image The image that was loaded, or nil if no image was loaded.
  29. @param imageIdentifier The identifier of the image that was loaded, or nil if no image was loaded.
  30. @param error An error describing why an image couldn't be loaded, if it failed to load; nil otherwise.
  31. */
  32. typedef void(^ASMultiplexImageLoadCompletionBlock)(UIImage *image, id imageIdentifier, NSError *error);
  33. @interface ASMultiplexImageNode ()
  34. {
  35. @private
  36. // Core.
  37. id<ASImageCacheProtocol> _cache;
  38. id<ASImageDownloaderProtocol> _downloader;
  39. __weak id<ASMultiplexImageNodeDelegate> _delegate;
  40. struct {
  41. unsigned int downloadStart:1;
  42. unsigned int downloadProgress:1;
  43. unsigned int downloadFinish:1;
  44. unsigned int updatedImageDisplayFinish:1;
  45. unsigned int updatedImage:1;
  46. unsigned int displayFinish:1;
  47. } _delegateFlags;
  48. __weak id<ASMultiplexImageNodeDataSource> _dataSource;
  49. struct {
  50. unsigned int image:1;
  51. unsigned int URL:1;
  52. unsigned int asset:1;
  53. } _dataSourceFlags;
  54. // Image flags.
  55. BOOL _downloadsIntermediateImages; // Defaults to NO.
  56. ASDN::Mutex _imageIdentifiersLock;
  57. NSArray *_imageIdentifiers;
  58. id _loadedImageIdentifier;
  59. id _loadingImageIdentifier;
  60. id _displayedImageIdentifier;
  61. __weak NSOperation *_phImageRequestOperation;
  62. // Networking.
  63. ASDN::RecursiveMutex _downloadIdentifierLock;
  64. id _downloadIdentifier;
  65. // Properties
  66. BOOL _shouldRenderProgressImages;
  67. //set on init only
  68. BOOL _downloaderImplementsSetProgress;
  69. BOOL _downloaderImplementsSetPriority;
  70. BOOL _cacheSupportsClearing;
  71. }
  72. //! @abstract Read-write redeclaration of property declared in ASMultiplexImageNode.h.
  73. @property (nonatomic, readwrite, copy) id loadedImageIdentifier;
  74. //! @abstract The image identifier that's being loaded by _loadNextImageWithCompletion:.
  75. @property (nonatomic, readwrite, copy) id loadingImageIdentifier;
  76. /**
  77. @abstract Returns the next image identifier that should be downloaded.
  78. @discussion This method obeys and reflects the value of `downloadsIntermediateImages`.
  79. @result The next image identifier, from `_imageIdentifiers`, that should be downloaded, or nil if no image should be downloaded next.
  80. */
  81. - (id)_nextImageIdentifierToDownload;
  82. /**
  83. @abstract Returns the best image that is immediately available from our datasource without downloading or hitting the cache.
  84. @param imageIdentifierOut Upon return, the image identifier for the returned image; nil otherwise.
  85. @discussion This method exclusively uses the data source's -multiplexImageNode:imageForIdentifier: method to return images. It does not fetch from the cache or kick off downloading.
  86. @result The best UIImage available immediately; nil if no image is immediately available.
  87. */
  88. - (UIImage *)_bestImmediatelyAvailableImageFromDataSource:(id *)imageIdentifierOut;
  89. /**
  90. @abstract Loads and displays the next image in the receiver's loading sequence.
  91. @discussion This method obeys `downloadsIntermediateImages`. This method has no effect if nothing further should be loaded, as indicated by `_nextImageIdentifierToDownload`. This method will load the next image from the data-source, if possible; otherwise, the session's image cache will be queried for the desired image, and as a last resort, the image will be downloaded.
  92. */
  93. - (void)_loadNextImage;
  94. /**
  95. @abstract Fetches the image corresponding to the given imageIdentifier from the given URL from the session's image cache.
  96. @param imageIdentifier The identifier for the image to be fetched. May not be nil.
  97. @param imageURL The URL of the image to fetch. May not be nil.
  98. @param completionBlock The block to be performed when the image has been fetched from the cache, if possible. May not be nil.
  99. @param image The image fetched from the cache, if any.
  100. @discussion This method queries both the session's in-memory and on-disk caches (with preference for the in-memory cache).
  101. */
  102. - (void)_fetchImageWithIdentifierFromCache:(id)imageIdentifier URL:(NSURL *)imageURL completion:(void (^)(UIImage *image))completionBlock;
  103. #if TARGET_OS_IOS
  104. /**
  105. @abstract Loads the image corresponding to the given assetURL from the device's Assets Library.
  106. @param imageIdentifier The identifier for the image to be loaded. May not be nil.
  107. @param assetURL The assets-library URL (e.g., "assets-library://identifier") of the image to load, from ALAsset. May not be nil.
  108. @param completionBlock The block to be performed when the image has been loaded, if possible. May not be nil.
  109. @param image The image that was loaded. May be nil if no image could be downloaded.
  110. @param error An error describing why the load failed, if it failed; nil otherwise.
  111. */
  112. - (void)_loadALAssetWithIdentifier:(id)imageIdentifier URL:(NSURL *)assetURL completion:(void (^)(UIImage *image, NSError *error))completionBlock;
  113. /**
  114. @abstract Loads the image corresponding to the given image request from the Photos framework.
  115. @param imageIdentifier The identifier for the image to be loaded. May not be nil.
  116. @param request The photos image request to load. May not be nil.
  117. @param completionBlock The block to be performed when the image has been loaded, if possible. May not be nil.
  118. @param image The image that was loaded. May be nil if no image could be downloaded.
  119. @param error An error describing why the load failed, if it failed; nil otherwise.
  120. */
  121. - (void)_loadPHAssetWithRequest:(ASPhotosFrameworkImageRequest *)request identifier:(id)imageIdentifier completion:(void (^)(UIImage *image, NSError *error))completionBlock;
  122. #endif
  123. /**
  124. @abstract Downloads the image corresponding to the given imageIdentifier from the given URL.
  125. @param imageIdentifier The identifier for the image to be downloaded. May not be nil.
  126. @param imageURL The URL of the image to downloaded. May not be nil.
  127. @param completionBlock The block to be performed when the image has been downloaded, if possible. May not be nil.
  128. @param image The image that was downloaded. May be nil if no image could be downloaded.
  129. @param error An error describing why the download failed, if it failed; nil otherwise.
  130. */
  131. - (void)_downloadImageWithIdentifier:(id)imageIdentifier URL:(NSURL *)imageURL completion:(void (^)(UIImage *image, NSError *error))completionBlock;
  132. @end
  133. @implementation ASMultiplexImageNode
  134. #pragma mark - Getting Started / Tearing Down
  135. - (instancetype)initWithCache:(id<ASImageCacheProtocol>)cache downloader:(id<ASImageDownloaderProtocol>)downloader
  136. {
  137. if (!(self = [super init]))
  138. return nil;
  139. _cache = (id<ASImageCacheProtocol>)cache;
  140. _downloader = (id<ASImageDownloaderProtocol>)downloader;
  141. _downloaderImplementsSetProgress = [downloader respondsToSelector:@selector(setProgressImageBlock:callbackQueue:withDownloadIdentifier:)];
  142. _downloaderImplementsSetPriority = [downloader respondsToSelector:@selector(setPriority:withDownloadIdentifier:)];
  143. _cacheSupportsClearing = [cache respondsToSelector:@selector(clearFetchedImageFromCacheWithURL:)];
  144. _shouldRenderProgressImages = YES;
  145. self.shouldBypassEnsureDisplay = YES;
  146. return self;
  147. }
  148. - (instancetype)init
  149. {
  150. #if PIN_REMOTE_IMAGE
  151. return [self initWithCache:[ASPINRemoteImageDownloader sharedDownloader] downloader:[ASPINRemoteImageDownloader sharedDownloader]];
  152. #else
  153. return [self initWithCache:nil downloader:[ASBasicImageDownloader sharedImageDownloader]];
  154. #endif
  155. }
  156. - (void)dealloc
  157. {
  158. [_phImageRequestOperation cancel];
  159. }
  160. #pragma mark - ASDisplayNode Overrides
  161. - (void)clearContents
  162. {
  163. [super clearContents]; // This actually clears the contents, so we need to do this first for our displayedImageIdentifier to be meaningful.
  164. [self _setDisplayedImageIdentifier:nil withImage:nil];
  165. // NOTE: We intentionally do not cancel image downloads until `clearPreloadedData`.
  166. }
  167. - (void)didExitPreloadState
  168. {
  169. [super didExitPreloadState];
  170. [_phImageRequestOperation cancel];
  171. [self _setDownloadIdentifier:nil];
  172. if (_cacheSupportsClearing && self.loadedImageIdentifier != nil) {
  173. [_cache clearFetchedImageFromCacheWithURL:[_dataSource multiplexImageNode:self URLForImageIdentifier:self.loadedImageIdentifier]];
  174. }
  175. // setting this to nil makes the node fetch images the next time its display starts
  176. _loadedImageIdentifier = nil;
  177. [self _setImage:nil];
  178. }
  179. - (void)didEnterPreloadState
  180. {
  181. [super didEnterPreloadState];
  182. [self _loadImageIdentifiers];
  183. }
  184. - (void)displayDidFinish
  185. {
  186. [super displayDidFinish];
  187. // We may now be displaying the loaded identifier, if they're different.
  188. UIImage *displayedImage = self.image;
  189. if (displayedImage) {
  190. if (!ASObjectIsEqual(_displayedImageIdentifier, _loadedImageIdentifier))
  191. [self _setDisplayedImageIdentifier:_loadedImageIdentifier withImage:displayedImage];
  192. // Delegateify
  193. if (_delegateFlags.displayFinish) {
  194. if (ASDisplayNodeThreadIsMain())
  195. [_delegate multiplexImageNodeDidFinishDisplay:self];
  196. else {
  197. __weak __typeof__(self) weakSelf = self;
  198. dispatch_async(dispatch_get_main_queue(), ^{
  199. __typeof__(self) strongSelf = weakSelf;
  200. if (!strongSelf)
  201. return;
  202. [strongSelf.delegate multiplexImageNodeDidFinishDisplay:strongSelf];
  203. });
  204. }
  205. }
  206. }
  207. }
  208. - (BOOL)placeholderShouldPersist
  209. {
  210. return (self.image == nil && self.imageIdentifiers.count > 0);
  211. }
  212. /* displayWillStart in ASNetworkImageNode has a very similar implementation. Changes here are likely necessary
  213. in ASNetworkImageNode as well. */
  214. - (void)displayWillStart
  215. {
  216. [super displayWillStart];
  217. [self didEnterPreloadState];
  218. if (_downloaderImplementsSetPriority) {
  219. {
  220. ASDN::MutexLocker l(_downloadIdentifierLock);
  221. if (_downloadIdentifier != nil) {
  222. [_downloader setPriority:ASImageDownloaderPriorityImminent withDownloadIdentifier:_downloadIdentifier];
  223. }
  224. }
  225. }
  226. }
  227. /* didEnterVisibleState / didExitVisibleState in ASNetworkImageNode has a very similar implementation. Changes here are likely necessary
  228. in ASNetworkImageNode as well. */
  229. - (void)didEnterVisibleState
  230. {
  231. [super didEnterVisibleState];
  232. if (_downloaderImplementsSetPriority) {
  233. ASDN::MutexLocker l(_downloadIdentifierLock);
  234. if (_downloadIdentifier != nil) {
  235. [_downloader setPriority:ASImageDownloaderPriorityVisible withDownloadIdentifier:_downloadIdentifier];
  236. }
  237. }
  238. [self _updateProgressImageBlockOnDownloaderIfNeeded];
  239. }
  240. - (void)didExitVisibleState
  241. {
  242. [super didExitVisibleState];
  243. if (_downloaderImplementsSetPriority) {
  244. ASDN::MutexLocker l(_downloadIdentifierLock);
  245. if (_downloadIdentifier != nil) {
  246. [_downloader setPriority:ASImageDownloaderPriorityPreload withDownloadIdentifier:_downloadIdentifier];
  247. }
  248. }
  249. [self _updateProgressImageBlockOnDownloaderIfNeeded];
  250. }
  251. #pragma mark - Core
  252. - (void)setImage:(UIImage *)image
  253. {
  254. ASDisplayNodeAssert(NO, @"Setting the image directly on an ASMultiplexImageNode is unsafe. It will be cleared in didExitPreloadRange and will have no way to restore in didEnterPreloadRange");
  255. super.image = image;
  256. }
  257. - (void)_setImage:(UIImage *)image
  258. {
  259. super.image = image;
  260. }
  261. - (void)setDelegate:(id <ASMultiplexImageNodeDelegate>)delegate
  262. {
  263. if (_delegate == delegate)
  264. return;
  265. _delegate = delegate;
  266. _delegateFlags.downloadStart = [_delegate respondsToSelector:@selector(multiplexImageNode:didStartDownloadOfImageWithIdentifier:)];
  267. _delegateFlags.downloadProgress = [_delegate respondsToSelector:@selector(multiplexImageNode:didUpdateDownloadProgress:forImageWithIdentifier:)];
  268. _delegateFlags.downloadFinish = [_delegate respondsToSelector:@selector(multiplexImageNode:didFinishDownloadingImageWithIdentifier:error:)];
  269. _delegateFlags.updatedImageDisplayFinish = [_delegate respondsToSelector:@selector(multiplexImageNode:didDisplayUpdatedImage:withIdentifier:)];
  270. _delegateFlags.updatedImage = [_delegate respondsToSelector:@selector(multiplexImageNode:didUpdateImage:withIdentifier:fromImage:withIdentifier:)];
  271. _delegateFlags.displayFinish = [_delegate respondsToSelector:@selector(multiplexImageNodeDidFinishDisplay:)];
  272. }
  273. - (void)setDataSource:(id <ASMultiplexImageNodeDataSource>)dataSource
  274. {
  275. if (_dataSource == dataSource)
  276. return;
  277. _dataSource = dataSource;
  278. _dataSourceFlags.image = [_dataSource respondsToSelector:@selector(multiplexImageNode:imageForImageIdentifier:)];
  279. _dataSourceFlags.URL = [_dataSource respondsToSelector:@selector(multiplexImageNode:URLForImageIdentifier:)];
  280. #if TARGET_OS_IOS
  281. _dataSourceFlags.asset = [_dataSource respondsToSelector:@selector(multiplexImageNode:assetForLocalIdentifier:)];
  282. #endif
  283. }
  284. - (void)setShouldRenderProgressImages:(BOOL)shouldRenderProgressImages
  285. {
  286. __instanceLock__.lock();
  287. if (shouldRenderProgressImages == _shouldRenderProgressImages) {
  288. __instanceLock__.unlock();
  289. return;
  290. }
  291. _shouldRenderProgressImages = shouldRenderProgressImages;
  292. __instanceLock__.unlock();
  293. [self _updateProgressImageBlockOnDownloaderIfNeeded];
  294. }
  295. - (BOOL)shouldRenderProgressImages
  296. {
  297. ASDN::MutexLocker l(__instanceLock__);
  298. return _shouldRenderProgressImages;
  299. }
  300. #pragma mark -
  301. #pragma mark -
  302. - (NSArray *)imageIdentifiers
  303. {
  304. ASDN::MutexLocker l(_imageIdentifiersLock);
  305. return _imageIdentifiers;
  306. }
  307. - (void)setImageIdentifiers:(NSArray *)imageIdentifiers
  308. {
  309. {
  310. ASDN::MutexLocker l(_imageIdentifiersLock);
  311. if (ASObjectIsEqual(_imageIdentifiers, imageIdentifiers)) {
  312. return;
  313. }
  314. _imageIdentifiers = [[NSArray alloc] initWithArray:imageIdentifiers copyItems:YES];
  315. }
  316. [self setNeedsPreload];
  317. }
  318. - (void)reloadImageIdentifierSources
  319. {
  320. // setting this to nil makes the node think it has not downloaded any images
  321. _loadedImageIdentifier = nil;
  322. [self _loadImageIdentifiers];
  323. }
  324. #pragma mark -
  325. #pragma mark - Core Internal
  326. - (void)_setDisplayedImageIdentifier:(id)displayedImageIdentifier withImage:(UIImage *)image
  327. {
  328. if (ASObjectIsEqual(displayedImageIdentifier, _displayedImageIdentifier))
  329. return;
  330. _displayedImageIdentifier = displayedImageIdentifier;
  331. // Delegateify.
  332. // Note that we're using the params here instead of self.image and _displayedImageIdentifier because those can change before the async block below executes.
  333. if (_delegateFlags.updatedImageDisplayFinish) {
  334. if (ASDisplayNodeThreadIsMain())
  335. [_delegate multiplexImageNode:self didDisplayUpdatedImage:image withIdentifier:displayedImageIdentifier];
  336. else {
  337. __weak __typeof__(self) weakSelf = self;
  338. dispatch_async(dispatch_get_main_queue(), ^{
  339. __typeof__(self) strongSelf = weakSelf;
  340. if (!strongSelf)
  341. return;
  342. [strongSelf.delegate multiplexImageNode:strongSelf didDisplayUpdatedImage:image withIdentifier:displayedImageIdentifier];
  343. });
  344. }
  345. }
  346. }
  347. - (void)_setDownloadIdentifier:(id)downloadIdentifier
  348. {
  349. ASDN::MutexLocker l(_downloadIdentifierLock);
  350. if (ASObjectIsEqual(downloadIdentifier, _downloadIdentifier))
  351. return;
  352. if (_downloadIdentifier) {
  353. [_downloader cancelImageDownloadForIdentifier:_downloadIdentifier];
  354. }
  355. _downloadIdentifier = downloadIdentifier;
  356. }
  357. #pragma mark - Image Loading Machinery
  358. - (void)_loadImageIdentifiers
  359. {
  360. // Grab the best possible image we can load right now.
  361. id bestImmediatelyAvailableImageIdentifier = nil;
  362. UIImage *bestImmediatelyAvailableImage = [self _bestImmediatelyAvailableImageFromDataSource:&bestImmediatelyAvailableImageIdentifier];
  363. ASMultiplexImageNodeLogDebug(@"[%p] Best immediately available image identifier is %@", self, bestImmediatelyAvailableImageIdentifier);
  364. // Load it. This kicks off cache fetching/downloading, as appropriate.
  365. [self _finishedLoadingImage:bestImmediatelyAvailableImage forIdentifier:bestImmediatelyAvailableImageIdentifier error:nil];
  366. }
  367. - (UIImage *)_bestImmediatelyAvailableImageFromDataSource:(id *)imageIdentifierOut
  368. {
  369. ASDN::MutexLocker l(_imageIdentifiersLock);
  370. // If we don't have any identifiers to load or don't implement the image DS method, bail.
  371. if ([_imageIdentifiers count] == 0 || !_dataSourceFlags.image) {
  372. return nil;
  373. }
  374. // Grab the best available image from the data source.
  375. UIImage *existingImage = self.image;
  376. for (id imageIdentifier in _imageIdentifiers) {
  377. // If this image is already loaded, don't request it from the data source again because
  378. // the data source may generate a new instance of UIImage that returns NO for isEqual:
  379. // and we'll end up in an infinite loading loop.
  380. UIImage *image = ASObjectIsEqual(imageIdentifier, _loadedImageIdentifier) ? existingImage : [_dataSource multiplexImageNode:self imageForImageIdentifier:imageIdentifier];
  381. if (image) {
  382. if (imageIdentifierOut) {
  383. *imageIdentifierOut = imageIdentifier;
  384. }
  385. return image;
  386. }
  387. }
  388. return nil;
  389. }
  390. #pragma mark -
  391. /**
  392. @note: This should be called without _downloadIdentifierLock held. We will lock
  393. super to read our interface state and it's best to avoid acquiring both locks.
  394. */
  395. - (void)_updateProgressImageBlockOnDownloaderIfNeeded
  396. {
  397. BOOL shouldRenderProgressImages = self.shouldRenderProgressImages;
  398. // Read our interface state before locking so that we don't lock super while holding our lock.
  399. ASInterfaceState interfaceState = self.interfaceState;
  400. ASDN::MutexLocker l(_downloadIdentifierLock);
  401. if (!_downloaderImplementsSetProgress || _downloadIdentifier == nil) {
  402. return;
  403. }
  404. ASImageDownloaderProgressImage progress = nil;
  405. if (shouldRenderProgressImages && ASInterfaceStateIncludesVisible(interfaceState)) {
  406. __weak __typeof__(self) weakSelf = self;
  407. progress = ^(UIImage * _Nonnull progressImage, CGFloat progress, id _Nullable downloadIdentifier) {
  408. __typeof__(self) strongSelf = weakSelf;
  409. if (strongSelf == nil) {
  410. return;
  411. }
  412. ASDN::MutexLocker l(strongSelf->_downloadIdentifierLock);
  413. //Getting a result back for a different download identifier, download must not have been successfully canceled
  414. if (ASObjectIsEqual(strongSelf->_downloadIdentifier, downloadIdentifier) == NO && downloadIdentifier != nil) {
  415. return;
  416. }
  417. [strongSelf _setImage:progressImage];
  418. };
  419. }
  420. [_downloader setProgressImageBlock:progress callbackQueue:dispatch_get_main_queue() withDownloadIdentifier:_downloadIdentifier];
  421. }
  422. - (void)_clearImage
  423. {
  424. // Destruction of bigger images on the main thread can be expensive
  425. // and can take some time, so we dispatch onto a bg queue to
  426. // actually dealloc.
  427. UIImage *image = self.image;
  428. CGSize imageSize = image.size;
  429. BOOL shouldReleaseImageOnBackgroundThread = imageSize.width > kMinReleaseImageOnBackgroundSize.width ||
  430. imageSize.height > kMinReleaseImageOnBackgroundSize.height;
  431. if (shouldReleaseImageOnBackgroundThread) {
  432. ASPerformBackgroundDeallocation(image);
  433. }
  434. [self _setImage:nil];
  435. }
  436. #pragma mark -
  437. - (id)_nextImageIdentifierToDownload
  438. {
  439. ASDN::MutexLocker l(_imageIdentifiersLock);
  440. // If we've already loaded the best identifier, we've got nothing else to do.
  441. id bestImageIdentifier = _imageIdentifiers.firstObject;
  442. if (!bestImageIdentifier || ASObjectIsEqual(_loadedImageIdentifier, bestImageIdentifier)) {
  443. return nil;
  444. }
  445. id nextImageIdentifierToDownload = nil;
  446. // If we're not supposed to download intermediate images, load the best identifier we've got.
  447. if (!_downloadsIntermediateImages) {
  448. nextImageIdentifierToDownload = bestImageIdentifier;
  449. }
  450. // Otherwise, load progressively.
  451. else {
  452. NSUInteger loadedIndex = [_imageIdentifiers indexOfObject:_loadedImageIdentifier];
  453. // If nothing has loaded yet, load the worst identifier.
  454. if (loadedIndex == NSNotFound) {
  455. nextImageIdentifierToDownload = [_imageIdentifiers lastObject];
  456. }
  457. // Otherwise, load the next best identifier (if there is one)
  458. else if (loadedIndex > 0) {
  459. nextImageIdentifierToDownload = _imageIdentifiers[loadedIndex - 1];
  460. }
  461. }
  462. return nextImageIdentifierToDownload;
  463. }
  464. - (void)_loadNextImage
  465. {
  466. // Determine the next identifier to load (if any).
  467. id nextImageIdentifier = [self _nextImageIdentifierToDownload];
  468. if (!nextImageIdentifier) {
  469. [self _finishedLoadingImage:nil forIdentifier:nil error:nil];
  470. return;
  471. }
  472. self.loadingImageIdentifier = nextImageIdentifier;
  473. __weak __typeof__(self) weakSelf = self;
  474. ASMultiplexImageLoadCompletionBlock finishedLoadingBlock = ^(UIImage *image, id imageIdentifier, NSError *error) {
  475. __typeof__(self) strongSelf = weakSelf;
  476. if (!strongSelf)
  477. return;
  478. // Only nil out the loading identifier if the loading identifier hasn't changed.
  479. if (ASObjectIsEqual(strongSelf.loadingImageIdentifier, nextImageIdentifier)) {
  480. strongSelf.loadingImageIdentifier = nil;
  481. }
  482. [strongSelf _finishedLoadingImage:image forIdentifier:imageIdentifier error:error];
  483. };
  484. ASMultiplexImageNodeLogDebug(@"[%p] Loading next image, ident: %@", self, nextImageIdentifier);
  485. // Ask our data-source if it's got this image.
  486. if (_dataSourceFlags.image) {
  487. UIImage *image = [_dataSource multiplexImageNode:self imageForImageIdentifier:nextImageIdentifier];
  488. if (image) {
  489. ASMultiplexImageNodeLogDebug(@"[%p] Acquired next image (%@) from data-source", self, nextImageIdentifier);
  490. finishedLoadingBlock(image, nextImageIdentifier, nil);
  491. return;
  492. }
  493. }
  494. NSURL *nextImageURL = (_dataSourceFlags.URL) ? [_dataSource multiplexImageNode:self URLForImageIdentifier:nextImageIdentifier] : nil;
  495. // If we fail to get a URL for the image, we have no source and can't proceed.
  496. if (!nextImageURL) {
  497. ASMultiplexImageNodeLogError(@"[%p] Could not acquire URL for next image (%@). Bailing.", self, nextImageIdentifier);
  498. finishedLoadingBlock(nil, nil, [NSError errorWithDomain:ASMultiplexImageNodeErrorDomain code:ASMultiplexImageNodeErrorCodeNoSourceForImage userInfo:nil]);
  499. return;
  500. }
  501. #if TARGET_OS_IOS
  502. // If it's an assets-library URL, we need to fetch it from the assets library.
  503. if ([[nextImageURL scheme] isEqualToString:kAssetsLibraryURLScheme]) {
  504. // Load the asset.
  505. [self _loadALAssetWithIdentifier:nextImageIdentifier URL:nextImageURL completion:^(UIImage *downloadedImage, NSError *error) {
  506. ASMultiplexImageNodeCLogDebug(@"[%p] Acquired next image (%@) from asset library", weakSelf, nextImageIdentifier);
  507. finishedLoadingBlock(downloadedImage, nextImageIdentifier, error);
  508. }];
  509. }
  510. // Likewise, if it's a iOS 8 Photo asset, we need to fetch it accordingly.
  511. else if (ASPhotosFrameworkImageRequest *request = [ASPhotosFrameworkImageRequest requestWithURL:nextImageURL]) {
  512. [self _loadPHAssetWithRequest:request identifier:nextImageIdentifier completion:^(UIImage *image, NSError *error) {
  513. ASMultiplexImageNodeCLogDebug(@"[%p] Acquired next image (%@) from Photos Framework", weakSelf, nextImageIdentifier);
  514. finishedLoadingBlock(image, nextImageIdentifier, error);
  515. }];
  516. }
  517. #endif
  518. else // Otherwise, it's a web URL that we can download.
  519. {
  520. // First, check the cache.
  521. [self _fetchImageWithIdentifierFromCache:nextImageIdentifier URL:nextImageURL completion:^(UIImage *imageFromCache) {
  522. __typeof__(self) strongSelf = weakSelf;
  523. if (!strongSelf)
  524. return;
  525. // If we had a cache-hit, we're done.
  526. if (imageFromCache) {
  527. ASMultiplexImageNodeCLogDebug(@"[%p] Acquired next image (%@) from cache", strongSelf, nextImageIdentifier);
  528. finishedLoadingBlock(imageFromCache, nextImageIdentifier, nil);
  529. return;
  530. }
  531. // If the next image to load has changed, bail.
  532. if (!ASObjectIsEqual([strongSelf _nextImageIdentifierToDownload], nextImageIdentifier)) {
  533. finishedLoadingBlock(nil, nil, [NSError errorWithDomain:ASMultiplexImageNodeErrorDomain code:ASMultiplexImageNodeErrorCodeBestImageIdentifierChanged userInfo:nil]);
  534. return;
  535. }
  536. // Otherwise, we've got to download it.
  537. [strongSelf _downloadImageWithIdentifier:nextImageIdentifier URL:nextImageURL completion:^(UIImage *downloadedImage, NSError *error) {
  538. ASMultiplexImageNodeCLogDebug(@"[%p] Acquired next image (%@) from download", strongSelf, nextImageIdentifier);
  539. finishedLoadingBlock(downloadedImage, nextImageIdentifier, error);
  540. }];
  541. }];
  542. }
  543. }
  544. #if TARGET_OS_IOS
  545. - (void)_loadALAssetWithIdentifier:(id)imageIdentifier URL:(NSURL *)assetURL completion:(void (^)(UIImage *image, NSError *error))completionBlock
  546. {
  547. ASDisplayNodeAssertNotNil(imageIdentifier, @"imageIdentifier is required");
  548. ASDisplayNodeAssertNotNil(assetURL, @"assetURL is required");
  549. ASDisplayNodeAssertNotNil(completionBlock, @"completionBlock is required");
  550. ALAssetsLibrary *assetLibrary = [[ALAssetsLibrary alloc] init];
  551. [assetLibrary assetForURL:assetURL resultBlock:^(ALAsset *asset) {
  552. ALAssetRepresentation *representation = [asset defaultRepresentation];
  553. CGImageRef coreGraphicsImage = [representation fullScreenImage];
  554. UIImage *downloadedImage = (coreGraphicsImage ? [UIImage imageWithCGImage:coreGraphicsImage] : nil);
  555. completionBlock(downloadedImage, nil);
  556. } failureBlock:^(NSError *error) {
  557. completionBlock(nil, error);
  558. }];
  559. }
  560. - (void)_loadPHAssetWithRequest:(ASPhotosFrameworkImageRequest *)request identifier:(id)imageIdentifier completion:(void (^)(UIImage *image, NSError *error))completionBlock
  561. {
  562. ASDisplayNodeAssertNotNil(imageIdentifier, @"imageIdentifier is required");
  563. ASDisplayNodeAssertNotNil(request, @"request is required");
  564. ASDisplayNodeAssertNotNil(completionBlock, @"completionBlock is required");
  565. /*
  566. * Locking rationale:
  567. * As of iOS 9, Photos.framework will eventually deadlock if you hit it with concurrent fetch requests. rdar://22984886
  568. * Concurrent image requests are OK, but metadata requests aren't, so we limit ourselves to one at a time.
  569. */
  570. static NSLock *phRequestLock;
  571. static NSOperationQueue *phImageRequestQueue;
  572. static dispatch_once_t onceToken;
  573. dispatch_once(&onceToken, ^{
  574. phRequestLock = [NSLock new];
  575. phImageRequestQueue = [NSOperationQueue new];
  576. phImageRequestQueue.maxConcurrentOperationCount = 10;
  577. phImageRequestQueue.name = @"org.AsyncDisplayKit.MultiplexImageNode.phImageRequestQueue";
  578. });
  579. // Each ASMultiplexImageNode can have max 1 inflight Photos image request operation
  580. [_phImageRequestOperation cancel];
  581. __weak __typeof(self) weakSelf = self;
  582. NSOperation *newImageRequestOp = [NSBlockOperation blockOperationWithBlock:^{
  583. __strong __typeof(weakSelf) strongSelf = weakSelf;
  584. if (strongSelf == nil) { return; }
  585. PHAsset *imageAsset = nil;
  586. // Try to get the asset immediately from the data source.
  587. if (_dataSourceFlags.asset) {
  588. imageAsset = [strongSelf.dataSource multiplexImageNode:strongSelf assetForLocalIdentifier:request.assetIdentifier];
  589. }
  590. // Fall back to locking and getting the PHAsset.
  591. if (imageAsset == nil) {
  592. [phRequestLock lock];
  593. // -[PHFetchResult dealloc] plays a role in the deadlock mentioned above, so we make sure the PHFetchResult is deallocated inside the critical section
  594. @autoreleasepool {
  595. imageAsset = [PHAsset fetchAssetsWithLocalIdentifiers:@[request.assetIdentifier] options:nil].firstObject;
  596. }
  597. [phRequestLock unlock];
  598. }
  599. if (imageAsset == nil) {
  600. NSError *error = [NSError errorWithDomain:ASMultiplexImageNodeErrorDomain code:ASMultiplexImageNodeErrorCodePHAssetIsUnavailable userInfo:nil];
  601. completionBlock(nil, error);
  602. return;
  603. }
  604. PHImageRequestOptions *options = [request.options copy];
  605. // We don't support opportunistic delivery – one request, one image.
  606. if (options.deliveryMode == PHImageRequestOptionsDeliveryModeOpportunistic) {
  607. options.deliveryMode = PHImageRequestOptionsDeliveryModeHighQualityFormat;
  608. }
  609. if (options.deliveryMode == PHImageRequestOptionsDeliveryModeHighQualityFormat) {
  610. // Without this flag the result will be delivered on the main queue, which is pointless
  611. // But synchronous -> HighQualityFormat so we only use it if high quality format is specified
  612. options.synchronous = YES;
  613. }
  614. PHImageManager *imageManager = strongSelf.imageManager ? : PHImageManager.defaultManager;
  615. [imageManager requestImageForAsset:imageAsset targetSize:request.targetSize contentMode:request.contentMode options:options resultHandler:^(UIImage *image, NSDictionary *info) {
  616. NSError *error = info[PHImageErrorKey];
  617. if (error == nil && image == nil) {
  618. error = [NSError errorWithDomain:ASMultiplexImageNodeErrorDomain code:ASMultiplexImageNodeErrorCodePhotosImageManagerFailedWithoutError userInfo:nil];
  619. }
  620. if (NSThread.isMainThread) {
  621. dispatch_async(dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0), ^{
  622. completionBlock(image, error);
  623. });
  624. } else {
  625. completionBlock(image, error);
  626. }
  627. }];
  628. }];
  629. // If you don't set this, iOS will sometimes infer NSQualityOfServiceUserInteractive and promote the entire queue to that level, damaging system responsiveness
  630. newImageRequestOp.qualityOfService = NSQualityOfServiceUserInitiated;
  631. _phImageRequestOperation = newImageRequestOp;
  632. [phImageRequestQueue addOperation:newImageRequestOp];
  633. }
  634. #endif
  635. - (void)_fetchImageWithIdentifierFromCache:(id)imageIdentifier URL:(NSURL *)imageURL completion:(void (^)(UIImage *image))completionBlock
  636. {
  637. ASDisplayNodeAssertNotNil(imageIdentifier, @"imageIdentifier is required");
  638. ASDisplayNodeAssertNotNil(imageURL, @"imageURL is required");
  639. ASDisplayNodeAssertNotNil(completionBlock, @"completionBlock is required");
  640. if (_cache) {
  641. [_cache cachedImageWithURL:imageURL callbackQueue:dispatch_get_main_queue() completion:^(id <ASImageContainerProtocol> imageContainer) {
  642. completionBlock([imageContainer asdk_image]);
  643. }];
  644. }
  645. // If we don't have a cache, just fail immediately.
  646. else {
  647. completionBlock(nil);
  648. }
  649. }
  650. - (void)_downloadImageWithIdentifier:(id)imageIdentifier URL:(NSURL *)imageURL completion:(void (^)(UIImage *image, NSError *error))completionBlock
  651. {
  652. ASDisplayNodeAssertNotNil(imageIdentifier, @"imageIdentifier is required");
  653. ASDisplayNodeAssertNotNil(imageURL, @"imageURL is required");
  654. ASDisplayNodeAssertNotNil(completionBlock, @"completionBlock is required");
  655. // Delegate (start)
  656. if (_delegateFlags.downloadStart)
  657. [_delegate multiplexImageNode:self didStartDownloadOfImageWithIdentifier:imageIdentifier];
  658. __weak __typeof__(self) weakSelf = self;
  659. void (^downloadProgressBlock)(CGFloat) = nil;
  660. if (_delegateFlags.downloadProgress) {
  661. downloadProgressBlock = ^(CGFloat progress) {
  662. __typeof__(self) strongSelf = weakSelf;
  663. if (!strongSelf)
  664. return;
  665. [strongSelf.delegate multiplexImageNode:strongSelf didUpdateDownloadProgress:progress forImageWithIdentifier:imageIdentifier];
  666. };
  667. }
  668. // Download!
  669. ASPerformBlockOnBackgroundThread(^{
  670. [self _setDownloadIdentifier:[_downloader downloadImageWithURL:imageURL
  671. callbackQueue:dispatch_get_main_queue()
  672. downloadProgress:downloadProgressBlock
  673. completion:^(id <ASImageContainerProtocol> imageContainer, NSError *error, id downloadIdentifier) {
  674. // We dereference iVars directly, so we can't have weakSelf going nil on us.
  675. __typeof__(self) strongSelf = weakSelf;
  676. if (!strongSelf)
  677. return;
  678. ASDN::MutexLocker l(_downloadIdentifierLock);
  679. //Getting a result back for a different download identifier, download must not have been successfully canceled
  680. if (ASObjectIsEqual(_downloadIdentifier, downloadIdentifier) == NO && downloadIdentifier != nil) {
  681. return;
  682. }
  683. completionBlock([imageContainer asdk_image], error);
  684. // Delegateify.
  685. if (strongSelf->_delegateFlags.downloadFinish)
  686. [strongSelf->_delegate multiplexImageNode:weakSelf didFinishDownloadingImageWithIdentifier:imageIdentifier error:error];
  687. }]];
  688. [self _updateProgressImageBlockOnDownloaderIfNeeded];
  689. });
  690. }
  691. #pragma mark -
  692. - (void)_finishedLoadingImage:(UIImage *)image forIdentifier:(id)imageIdentifier error:(NSError *)error
  693. {
  694. // If we failed to load, we stop the loading process.
  695. // Note that if we bailed before we began downloading because the best identifier changed, we don't bail, but rather just begin loading the best image identifier.
  696. if (error && !([error.domain isEqual:ASMultiplexImageNodeErrorDomain] && error.code == ASMultiplexImageNodeErrorCodeBestImageIdentifierChanged))
  697. return;
  698. _imageIdentifiersLock.lock();
  699. NSUInteger imageIdentifierCount = [_imageIdentifiers count];
  700. _imageIdentifiersLock.unlock();
  701. // Update our image if we got one, or if we're not supposed to display one at all.
  702. // We explicitly perform this check because our datasource often doesn't give back immediately available images, even though we might have downloaded one already.
  703. // Because we seed this call with bestImmediatelyAvailableImageFromDataSource, we must be careful not to trample an existing image.
  704. if (image || imageIdentifierCount == 0) {
  705. ASMultiplexImageNodeLogDebug(@"[%p] loaded -> displaying (%@, %@)", self, imageIdentifier, image);
  706. id previousIdentifier = self.loadedImageIdentifier;
  707. UIImage *previousImage = self.image;
  708. self.loadedImageIdentifier = imageIdentifier;
  709. [self _setImage:image];
  710. if (_delegateFlags.updatedImage) {
  711. [_delegate multiplexImageNode:self didUpdateImage:image withIdentifier:imageIdentifier fromImage:previousImage withIdentifier:previousIdentifier];
  712. }
  713. }
  714. // Load our next image, if we have one to load.
  715. if ([self _nextImageIdentifierToDownload])
  716. [self _loadNextImage];
  717. }
  718. @end
  719. @implementation NSURL (ASPhotosFrameworkURLs)
  720. + (NSURL *)URLWithAssetLocalIdentifier:(NSString *)assetLocalIdentifier targetSize:(CGSize)targetSize contentMode:(PHImageContentMode)contentMode options:(PHImageRequestOptions *)options
  721. {
  722. ASPhotosFrameworkImageRequest *request = [[ASPhotosFrameworkImageRequest alloc] initWithAssetIdentifier:assetLocalIdentifier];
  723. request.options = options;
  724. request.contentMode = contentMode;
  725. request.targetSize = targetSize;
  726. return request.url;
  727. }
  728. @end