為什么圖像在顯示到屏幕上之前要進(jìn)行解碼
一般我們使用的圖像是JPEG/PNG歼指,這些圖像數(shù)據(jù)不是位圖爹土,而是是經(jīng)過(guò)編碼壓縮后的數(shù)據(jù),需要線將它解碼轉(zhuǎn)成位圖數(shù)據(jù)踩身,然后才能把位圖渲染到屏幕上着饥。
當(dāng)你用 UIImage 或 CGImageSource 的那幾個(gè)方法創(chuàng)建圖片時(shí),圖片數(shù)據(jù)并不會(huì)立刻解碼惰赋。圖片設(shè)置到 UIImageView 或者 CALayer.contents 中去宰掉,并且 CALayer 被提交到 GPU 前,CGImage 中的數(shù)據(jù)才會(huì)得到解碼赁濒。這一步是發(fā)生在主線程的轨奄,并且不可避免。
圖片加載的工作流
概括來(lái)說(shuō)拒炎,從磁盤中加載一張圖片挪拟,并將它顯示到屏幕上,中間的主要工作流如下:
- 假設(shè)我們使用
+imageWithContentsOfFile:
方法從磁盤中加載一張圖片击你,這個(gè)時(shí)候的圖片并沒(méi)有解壓縮玉组; - 然后將生成的
UIImage
賦值給UIImageView
; - 接著一個(gè)隱式的
CATransaction
捕獲到了UIImageView
圖層樹(shù)的變化丁侄; - 在主線程的下一個(gè) run loop 到來(lái)時(shí)惯雳,Core Animation 提交了這個(gè)隱式的 transaction ,這個(gè)過(guò)程可能會(huì)對(duì)圖片進(jìn)行 copy 操作鸿摇,而受圖片是否字節(jié)對(duì)齊等因素的影響石景,這個(gè) copy 操作可能會(huì)涉及以下部分或全部步驟:
- 分配內(nèi)存緩沖區(qū)用于管理文件 IO 和解壓縮操作;
- 將文件數(shù)據(jù)從磁盤讀到內(nèi)存中拙吉;
- 將壓縮的圖片數(shù)據(jù)解碼成未壓縮的位圖形式潮孽,這是一個(gè)非常耗時(shí)的 CPU 操作;
- 最后 Core Animation 使用未壓縮的位圖數(shù)據(jù)渲染
UIImageView
的圖層筷黔。
在上面的步驟中往史,我們提到了圖片的解壓縮是一個(gè)非常耗時(shí)的 CPU 操作,并且它默認(rèn)是在主線程中執(zhí)行的佛舱。那么當(dāng)需要加載的圖片比較多時(shí)椎例,就會(huì)對(duì)我們應(yīng)用的響應(yīng)性造成嚴(yán)重的影響揽乱,尤其是在快速滑動(dòng)的列表上,這個(gè)問(wèn)題會(huì)表現(xiàn)得更加突出粟矿。
圖像的解碼
解碼操作是比較耗時(shí)的,并且沒(méi)有GPU硬解碼损拢,只能通過(guò)CPU陌粹,iOS默認(rèn)會(huì)在主線程對(duì)圖像進(jìn)行解碼。解碼過(guò)程是一個(gè)相當(dāng)復(fù)雜的任務(wù)福压,需要消耗非常長(zhǎng)的時(shí)間掏秩。60FPS ≈ 0.01666s per frame = 16.7ms per frame,這意味著在主線程超過(guò)16.7ms的任務(wù)都會(huì)引起掉幀荆姆。很多庫(kù)都解決了圖像解碼的問(wèn)題蒙幻,不過(guò)由于解碼后的圖像太大,一般不會(huì)緩存到磁盤胆筒,SDWebImage的做法是把解碼操作從主線程移到子線程邮破,讓耗時(shí)的解碼操作不占用主線程的時(shí)間。
對(duì)于PNG圖片來(lái)說(shuō)仆救,因?yàn)槲募赡芨笫愫停约虞d會(huì)比JPEG更長(zhǎng),但是解碼會(huì)相對(duì)較快彤蔽,而且Xcode會(huì)把PNG圖片進(jìn)行解碼優(yōu)化之后引入工程摧莽。JPEG圖片更小,加載更快顿痪,但是解壓的步驟要消耗更長(zhǎng)的時(shí)間镊辕,因?yàn)镴PEG解壓算法比基于zip的PNG算法更加復(fù)雜。
當(dāng)加載圖片的時(shí)候蚁袭,iOS通常會(huì)延遲解壓圖片的時(shí)間征懈,直到加載到內(nèi)存之后。因?yàn)樾枰诶L制之前進(jìn)行解壓揩悄,這就會(huì)在準(zhǔn)備繪制圖片的時(shí)候影響性能受裹。
iOS通常會(huì)延時(shí)解壓圖片,等到圖片在屏幕上顯示的時(shí)候解壓圖片虏束。解壓圖片是非常耗時(shí)的操作棉饶。
圖像解碼的核心方法CGBitmapContextCreate
CGContextRef CGBitmapContextCreate(
void * data,
size_t width,
size_t height,
size_t bitsPerComponent,
size_t bytesPerRow,
CGColorSpaceRef _Nullable space,
uint32_t bitmapInfo)
上面的第一個(gè)參數(shù)是一個(gè)只想一塊內(nèi)存的指針,這塊內(nèi)存用于存儲(chǔ)被繪制的圖形镇匀,這塊內(nèi)存的size最小不能小于bytesPerRow*height(圖形每行的字節(jié)數(shù)乘以圖形的高度)照藻,傳遞NULL意味著由這個(gè)函數(shù)來(lái)管理圖形的內(nèi)存,這可以減少內(nèi)存泄漏的問(wèn)題汗侵;
第二個(gè)參數(shù)with是圖形的width幸缕;
第三個(gè)參數(shù)高度是圖形的高度群发;
第四個(gè)參數(shù)是像素中每一個(gè)顏色分量的像素位數(shù);
pixel formatter
Pixel Format
我們前面已經(jīng)提到了发乔,位圖其實(shí)就是一個(gè)像素?cái)?shù)組熟妓,而像素格式則是用來(lái)描述每個(gè)像素的組成格式,它包括以下信息:
- Bits per component :一個(gè)像素中每個(gè)獨(dú)立的顏色分量使用的 bit 數(shù)栏尚;
- Bits per pixel :一個(gè)像素使用的總 bit 數(shù)起愈;
- Bytes per row :位圖中的每一行使用的字節(jié)數(shù)。
有一點(diǎn)需要注意的是译仗,對(duì)于位圖來(lái)說(shuō)抬虽,像素格式并不是隨意組合的,目前只支持以下有限的 17 種特定組合:
Color Spaces
在 Quartz 中纵菌,一個(gè)顏色是由一組值來(lái)表示的阐污,比如 0, 0, 1 。而顏色空間則是用來(lái)說(shuō)明如何解析這些值的咱圆,離開(kāi)了顏色空間笛辟,它們將變得毫無(wú)意義。比如序苏,下面的值都表示藍(lán)色:
如果不知道顏色空間隘膘,那么我們根本無(wú)法知道這些值所代表的顏色。比如 0, 0, 1 在 RGB 下代表藍(lán)色杠览,而在 BGR 下則代表的是紅色弯菊。在 RGB 和 BGR 兩種顏色空間下,綠色是相同的踱阿,而紅色和藍(lán)色則相互對(duì)調(diào)了管钳。因此,對(duì)于同一張圖片软舌,使用 RGB 和 BGR 兩種顏色空間可能會(huì)得到兩種不一樣的效果:
BitmapInfo
Color Spaces and Bitmap Layout
我們前面已經(jīng)知道了才漆,像素格式是用來(lái)描述每個(gè)像素的組成格式的,比如每個(gè)像素使用的總 bit 數(shù)和每個(gè)獨(dú)立的顏色分量使用的 bit 數(shù)佛点。而要想確保 Quartz 能夠正確地解析這些 bit 所代表的含義醇滥,我們還需要提供位圖的布局信息 CGBitmapInfo :
typedef CF_OPTIONS(uint32_t, CGBitmapInfo) {
kCGBitmapAlphaInfoMask = 0x1F,
kCGBitmapFloatInfoMask = 0xF00,
kCGBitmapFloatComponents = (1 << 8),
kCGBitmapByteOrderMask = kCGImageByteOrderMask,
kCGBitmapByteOrderDefault = (0 << 12),
kCGBitmapByteOrder16Little = kCGImageByteOrder16Little,
kCGBitmapByteOrder32Little = kCGImageByteOrder32Little,
kCGBitmapByteOrder16Big = kCGImageByteOrder16Big,
kCGBitmapByteOrder32Big = kCGImageByteOrder32Big
} CG_AVAILABLE_STARTING(__MAC_10_0, __IPHONE_2_0);
它主要提供了三個(gè)方面的布局信息:
- alpha 的信息;
- 顏色分量是否為浮點(diǎn)數(shù)超营;
- 像素格式的字節(jié)順序鸳玩。
其中,alpha 的信息由枚舉值 CGImageAlphaInfo來(lái)表示:
typedef CF_ENUM(uint32_t, CGImageAlphaInfo) {
kCGImageAlphaNone, /* For example, RGB. */
kCGImageAlphaPremultipliedLast, /* For example, premultiplied RGBA */
kCGImageAlphaPremultipliedFirst, /* For example, premultiplied ARGB */
kCGImageAlphaLast, /* For example, non-premultiplied RGBA */
kCGImageAlphaFirst, /* For example, non-premultiplied ARGB */
kCGImageAlphaNoneSkipLast, /* For example, RBGX. */
kCGImageAlphaNoneSkipFirst, /* For example, XRGB. */
kCGImageAlphaOnly /* No color data, alpha data only */
};
上面的注釋其實(shí)已經(jīng)比較清楚了演闭,它同樣也提供了三個(gè)方面的 alpha 信息:
- 是否包含 alpha 不跟;
- 如果包含 alpha ,那么 alpha 信息所處的位置米碰,在像素的最低有效位窝革,比如 RGBA 购城,還是最高有效位,比如 ARGB 虐译;
- 如果包含 alpha 瘪板,那么每個(gè)顏色分量是否已經(jīng)乘以 alpha 的值,這種做法可以加速圖片的渲染時(shí)間漆诽,因?yàn)樗苊饬虽秩緯r(shí)的額外乘法運(yùn)算侮攀。比如,對(duì)于 RGB 顏色空間拴泌,用已經(jīng)乘以 alpha 的數(shù)據(jù)來(lái)渲染圖片,每個(gè)像素都可以避免 3 次乘法運(yùn)算惊橱,紅色乘以 alpha 蚪腐,綠色乘以 alpha 和藍(lán)色乘以 alpha 。
那么我們?cè)诮鈮嚎s圖片的時(shí)候應(yīng)該使用哪個(gè)值呢税朴?根據(jù) Which CGImageAlphaInfo should we use 和官方文檔中對(duì) UIGraphicsBeginImageContextWithOptions
函數(shù)的討論:
You use this function to configure the drawing environment for rendering into a bitmap. The format for the bitmap is a ARGB 32-bit integer pixel format using host-byte order. If the opaque parameter is YES, the alpha channel is ignored and the bitmap is treated as fully opaque (kCGImageAlphaNoneSkipFirst | kCGBitmapByteOrder32Host). Otherwise, each pixel uses a premultipled ARGB format (kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrder32Host).
我們可以知道回季,當(dāng)圖片不包含 alpha 的時(shí)候使用 kCGImageAlphaNoneSkipFirst
,否則使用 kCGImageAlphaPremultipliedFirst
正林。另外泡一,這里也提到了字節(jié)順序應(yīng)該使用 32 位的主機(jī)字節(jié)順序 kCGBitmapByteOrder32Host
,而這個(gè)值具體是什么觅廓,我們后面再討論鼻忠。
至于顏色分量是否為浮點(diǎn)數(shù),這個(gè)就比較簡(jiǎn)單了杈绸,直接邏輯或 kCGBitmapFloatComponents
就可以了帖蔓。更詳細(xì)的內(nèi)容就不展開(kāi)了,因?yàn)槲覀円话阌貌簧线@個(gè)值瞳脓。
接下來(lái)塑娇,我們來(lái)簡(jiǎn)單地了解下像素格式的字節(jié)順序,它是由枚舉值 CGImageByteOrderInfo
來(lái)表示的:
typedef CF_ENUM(uint32_t, CGImageByteOrderInfo) {
kCGImageByteOrderMask = 0x7000,
kCGImageByteOrder16Little = (1 << 12),
kCGImageByteOrder32Little = (2 << 12),
kCGImageByteOrder16Big = (3 << 12),
kCGImageByteOrder32Big = (4 << 12)
} CG_AVAILABLE_STARTING(__MAC_10_12, __IPHONE_10_0);
它主要提供了兩個(gè)方面的字節(jié)順序信息:
對(duì)于 iPhone 來(lái)說(shuō),采用的是小端模式烧栋,但是為了保證應(yīng)用的向后兼容性写妥,我們可以使用系統(tǒng)提供的宏,來(lái)避免 Hardcoding :
#ifdef __BIG_ENDIAN__
#define kCGBitmapByteOrder16Host kCGBitmapByteOrder16Big
#define kCGBitmapByteOrder32Host kCGBitmapByteOrder32Big
#else /* Little endian. */
#define kCGBitmapByteOrder16Host kCGBitmapByteOrder16Little
#define kCGBitmapByteOrder32Host kCGBitmapByteOrder32Little
#endif
根據(jù)前面的討論审姓,我們知道字節(jié)順序的值應(yīng)該使用的是 32 位的主機(jī)字節(jié)順序 kCGBitmapByteOrder32Host
耳标,這樣的話不管當(dāng)前設(shè)備采用的是小端模式還是大端模式,字節(jié)順序始終與其保持一致邑跪。
下面次坡,我們來(lái)看一張圖呼猪,它非常形象地展示了在使用 16 或 32 位像素格式的 CMYK 和 RGB 顏色空間下,一個(gè)像素是如何被表示的:
我們從圖中可以看出砸琅,在 32 位像素格式下宋距,每個(gè)顏色分量使用 8 位;而在 16 位像素格式下症脂,每個(gè)顏色分量則使用 5 位谚赎。
好了,了解完這些相關(guān)知識(shí)后诱篷,我們?cè)倩剡^(guò)頭來(lái)看看 CGBitmapContextCreate
函數(shù)中每個(gè)參數(shù)所代表的具體含義:
-
data
:如果不為NULL
壶唤,那么它應(yīng)該指向一塊大小至少為bytesPerRow * height
字節(jié)的內(nèi)存;如果 為NULL
棕所,那么系統(tǒng)就會(huì)為我們自動(dòng)分配和釋放所需的內(nèi)存闸盔,所以一般指定NULL
即可; -
width
和height
:位圖的寬度和高度琳省,分別賦值為圖片的像素寬度和像素高度即可迎吵; -
bitsPerComponent
:像素的每個(gè)顏色分量使用的 bit 數(shù),在 RGB 顏色空間下指定 8 即可针贬; -
bytesPerRow
:位圖的每一行使用的字節(jié)數(shù)击费,大小至少為width * bytes per pixel
字節(jié)。有意思的是桦他,當(dāng)我們指定 0 時(shí)蔫巩,系統(tǒng)不僅會(huì)為我們自動(dòng)計(jì)算,而且還會(huì)進(jìn)行 cache line alignment 的優(yōu)化快压,更多信息可以查看 what is byte alignment (cache line alignment) for Core Animation? Why it matters? 和 Why is my image’s Bytes per Row more than its Bytes per Pixel times its Width? 批幌,親測(cè)可用; -
space
:就是我們前面提到的顏色空間嗓节,一般使用 RGB 即可荧缘; -
bitmapInfo
:就是我們前面提到的位圖的布局信息。
+imageNamed:
通過(guò) imageNamed 創(chuàng)建 UIImage 時(shí)拦宣,當(dāng) UIImage 第一次顯示到屏幕上時(shí)截粗,其內(nèi)部的解碼方法才會(huì)被調(diào)用,并且內(nèi)存中自動(dòng)緩存解壓后的圖片鸵隧。當(dāng)APP第一次退到后臺(tái)和收到內(nèi)存警告時(shí)绸罗,緩存才會(huì)被自動(dòng)清空。
+imageWithContentsOfFile:
這個(gè)方法不會(huì)緩存解壓后的圖片豆瘫,也就是說(shuō)每次調(diào)用時(shí)都會(huì)對(duì)文件進(jìn)行加載和解壓珊蟀。iOS通常會(huì)延遲解壓圖片,為了提升性能,在屏幕繪制前可以強(qiáng)制解壓育灸。
有兩種方法可以強(qiáng)制解壓:
- 將圖片的一個(gè)像素繪制成一個(gè)像素大小的CGContext腻窒。這樣仍然會(huì)解壓整張圖片,但是繪制本身并沒(méi)有消耗任何時(shí)間磅崭。這樣的好處在于加載的圖片并不會(huì)在特定的設(shè)備上為繪制做優(yōu)化儿子,所以可以在任何時(shí)間點(diǎn)繪制出來(lái)。同樣iOS也就可以丟棄解壓后的圖片來(lái)節(jié)省內(nèi)存了砸喻。
//解壓縮這個(gè)image柔逼,即時(shí)它只有一個(gè)像素。
- (void)decompressImage:(UIImage *)image
{
UIGraphicsBeginImageContext(CGSizeMake(1, 1));
[image drawAtPoint:CGPointZero];
UIGraphicsEndImageContext();
}
- 將整張圖片繪制到CGContext中割岛,丟棄原始的圖片愉适,并且用一個(gè)從上下文內(nèi)容中新的圖片來(lái)代替。這樣比繪制單一像素那樣需要更加復(fù)雜的計(jì)算癣漆,但是因此產(chǎn)生的圖片將會(huì)為繪制做優(yōu)化维咸,而且由于原始?jí)嚎s圖片被拋棄了,iOS就不能夠隨時(shí)丟棄任何解壓后的圖片來(lái)節(jié)省內(nèi)存了扑媚。
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView
cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
//dequeue cell
UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"Cell" forIndexPath:indexPath];
...
//切換到子線程
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
//load image
NSInteger index = indexPath.row;
NSString *imagePath = self.imagePaths[index];
UIImage *image = [UIImage imageWithContentsOfFile:imagePath];
//redraw image using device context
UIGraphicsBeginImageContextWithOptions(imageView.bounds.size, YES, 0);
[image drawInRect:imageView.bounds];
image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
//set image on main thread, but only if index still matches up
dispatch_async(dispatch_get_main_queue(), ^{
if (index == cell.tag) {
imageView.image = image;
}
});
});
return cell;
}
Large Image Downsizing
SDWebImage解碼的方法在SDWebImageDecoder這個(gè)類里腰湾。這個(gè)類里有兩個(gè)方法雷恃,decodedImageWithImage
是直接對(duì)圖片解碼疆股,decodedAndScaledDownImageWithImage
這個(gè)方法里會(huì)先判斷圖片的要解壓縮的圖片大小是否超過(guò)60M,沒(méi)超過(guò)的話會(huì)調(diào)用decodedImageWithImage
這個(gè)方法直接解碼圖片倒槐,否則會(huì)對(duì)原圖片進(jìn)行縮放以減少占用內(nèi)存空間旬痹,并且解碼圖片時(shí)會(huì)把原始的圖片數(shù)據(jù)分成多個(gè)tail進(jìn)行解碼。這個(gè)過(guò)程應(yīng)該是參考了apple的 Large Image Downsizing
在這個(gè)demo里讨越,有一張large_leaves_70mp.jpg的圖片两残,它在磁盤上的大小是8.3M,但它的像素是7033x10110的把跨,也就是說(shuō)圖片解碼后顯示在屏幕上時(shí)所占的內(nèi)存是7033x10110x4byte(一個(gè)像素是4byte)人弓,也就是271MB,這樣一張圖片着逐,通過(guò)通常方法(imageView.image=image)是無(wú)法正常顯示的崔赌。
demo里原始圖片進(jìn)行了縮放,并且對(duì)把原始圖片分成多個(gè)tail進(jìn)行解碼耸别。
縮放的主要代碼:
原始圖片的在像素上的寬高:
sourceResolution.width = CGImageGetWidth(sourceImage.CGImage);
sourceResolution.height = CGImageGetHeight(sourceImage.CGImage);
計(jì)算縮放比例
imageScale = destTotalPixels / sourceTotalPixels;
計(jì)算縮放目標(biāo)圖片的寬高
destResolution.width = (int)( sourceResolution.width * imageScale );
destResolution.height = (int)( sourceResolution.height * imageScale );
創(chuàng)建縮放目標(biāo)圖片的context:
// create the output bitmap context
destContext = CGBitmapContextCreate( destBitmapData, destResolution.width, destResolution.height, 8, bytesPerRow, colorSpace, kCGImageAlphaPremultipliedLast );
雖然我們對(duì)超大圖片進(jìn)行了縮放健芭,但是依然較大,特別是在繪制的時(shí)候秀姐,非常耗性能慈迈。所以Sample中的方法是將該圖的繪制分成多個(gè)Tile來(lái)進(jìn)行,在該Sample中這張圖片被分成了14個(gè)Tile省有。sourceTile表示從原圖上截取的Tile尺寸痒留,destTile表示最終繪制到界面上的Tile尺寸谴麦。
demo里通過(guò)宏tileTotalPixels
定義了一個(gè)tile的總的尺寸,然后計(jì)算了原圖片一個(gè)tile的高度
sourceTile.size.width = sourceResolution.width;
// the source tile height is dynamic. Since we specified the size
// of the source tile in MB, see how many rows of pixels high it
// can be given the input image width.
sourceTile.size.height = (int)( tileTotalPixels / sourceTile.size.width );
然后根據(jù)比例計(jì)算目標(biāo)tile的高度:
destTile.size.height = sourceTile.size.height * imageScale;
計(jì)算原圖片tile的個(gè)數(shù):
int iterations = (int)( sourceResolution.height / sourceTile.size.height );
int remainder = (int)sourceResolution.height % (int)sourceTile.size.height; if( remainder ) iterations++;
根據(jù)tile的個(gè)數(shù)進(jìn)行循環(huán)狭瞎,把每一個(gè)原始圖片的tile繪制到context中细移,代碼里的注釋提到了,CGContextDrawImage調(diào)用時(shí)數(shù)據(jù)會(huì)被解碼熊锭。
for( int y = 0; y < iterations; ++y ) {
// create an autorelease pool to catch calls to -autorelease made within the downsize loop.
NSAutoreleasePool* pool2 = [[NSAutoreleasePool alloc] init];
NSLog(@"iteration %d of %d",y+1,iterations);
sourceTile.origin.y = y * sourceTileHeightMinusOverlap + sourceSeemOverlap;
destTile.origin.y = ( destResolution.height ) - ( ( y + 1 ) * sourceTileHeightMinusOverlap * imageScale + destSeemOverlap );
// create a reference to the source image with its context clipped to the argument rect.
sourceTileImageRef = CGImageCreateWithImageInRect(sourceImage.CGImage, sourceTile);
// if this is the last tile, it's size may be smaller than the source tile height.
// adjust the dest tile size to account for that difference.
if( y == iterations - 1 && remainder ) {
float dify = destTile.size.height;
destTile.size.height = CGImageGetHeight( sourceTileImageRef ) * imageScale;
dify -= destTile.size.height;
destTile.origin.y += dify;
}
// read and write a tile sized portion of pixels from the input image to the output image.
CGContextDrawImage( destContext, destTile, sourceTileImageRef );
/* release the source tile portion pixel data. note,
releasing the sourceTileImageRef doesn't actually release the tile portion pixel
data that we just drew, but the call afterward does. */
CGImageRelease(sourceTileImageRef);
/* while CGImageCreateWithImageInRect lazily loads just the image data defined by the argument rect,
that data is finally decoded from disk to mem when CGContextDrawImage is called. sourceTileImageRef
maintains internally a reference to the original image, and that original image both, houses and
caches that portion of decoded mem. Thus the following call to release the source image. */
[sourceImage release];
// free all objects that were sent -autorelease within the scope of this loop.
[pool2 drain];
// we reallocate the source image after the pool is drained since UIImage -imageNamed
// returns us an autoreleased object.
if( y < iterations - 1 ) {
sourceImage = [[UIImage alloc] initWithContentsOfFile:[[NSBundle mainBundle] pathForResource:kImageFilename ofType:nil]];
[self performSelectorOnMainThread:@selector(updateScrollView:) withObject:nil waitUntilDone:YES];
}
}