Asynchronous texture loading on iOS + GCD

Here's what we're going to cover:

  1. I will briefly describe GLKit and how you can use it to load textures.
  2. We will then cover how to asynchronously load textures.
  3. I'll explain a minor caveat regarding memory.
  4. I will demonstrate how to use dispatch groups for completion notification.
    Cowboys can skip to the complete code listing.

GLKIT

If you're doing OpenGL on iOS and avoided using GLKit, I highly suggest you look into it. It has a myriad of useful helper classes and functions (GLKVectorN, GLKMatrixN, GLKMatrix4Translate(...)) and is well worth exploring. I wasted hours converting C++ GLU code for iOS before I realised that the GLK Math Utilities includes GLKMathProject and GLKMathUnproject, for converting between 2D and 3D points. Definitely have a quick browse of the documentation.

GLKit provides a kind of fake fixed pipeline if you want to write your GL in the older style or you are free to pick and mix what you need to achieve your own fully programmable pipeline.

GLKTEXTUREINFO & GLKTEXTURELOADER

One of the provided helpers is GLKTextureInfo and its sibling GLKTextureLoader. GLKTextureLoader lets you easily load textures in many image formats from disk.

Normally you would be using GLKTextureLoader like this:

NSError *error;
GLKTextureInfo *texture;
NSDictionary *options = @{GLKTextureLoaderOriginBottomLeft:@YES};
texture = [GLKTextureLoader textureWithContentsOfFile:imagePath
                                              options:options
                                                error:&error];
if(error){
  // give up
}
NSLog(@"Texture loaded, name: %d, WxH: %d x %d",
      texture.name,
      texture.width,
      texture.height);
glBindTexture(GL_TEXTURE_2D, texture.name);

DOING THINGS ASYNCHRONOUSLY

It's very simple to load texutres in a background thread with GLKTextureLoader. Instead of using the class method, you allocate a GLKTextureLoader instance and pass in a EAGLShareGroup. The sharegroup allows different EAGLContext to share textures and buffers.

//
// Imagine you have this in your .h
//

@property (strong) GLKTextureLoader *asyncTextureLoader;
@property (strong) GLKTextureInfo *hugeTexture;
@property (strong) EAGLContext *context;


//
// somewhere in your .m initialization code
//

// create GL context
self.context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];

// check errors, etc ...

// create texture loader and give it the context share group.
self.asyncTextureLoader = [GLKTextureLoader alloc] initWithSharegroup:self.context.sharegroup]

//
// Later, when you need to load your texures
//

// same as before
NSDictionary *options = @{GLKTextureLoaderOriginBottomLeft:@YES};
// get a GCD queue to run the load on
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

void (^complete)(GLKTextureInfo*, NSError*) = ^(GLKTextureInfo *texture,
                                                NSError *e){
  //
  // NOTE: the GLKTextureLoader documentation incorrectly states that the 
  //       completion block is passed a GLTextureName and an NSError when
  //       infact its passed a GLKTextureInfo instance and an NSError.
  //       Just let xcodes autocompletion guide you to the right signature.
  //
  if(e){
    // give up
    return;
  }

  // set your property
  self.hugeTexture = texture;

  // (detecting that you're ready to bind and draw is left as 
  // an exercise to the reader, the easiest way would be to
  // check self.hugeTexture == nil in your update method)
};
// load texture in queue and pass in completion block
[self.asyncTextureLoader textureWithContentsOfFile:@"my_texture_path.png"
                                           options:options
                                             queue:queue
                                 completionHandler:complete];

FIXING LEAKS

There is one perhaps not so obvious memory leak with this code, if you call it multiple times.

GLKTextureInfo doesn't own any memory beyond a few GLuint's. When you re-assign self.hugeTexture, the GLKTextureInfo gets deallocated but the memory used for the pixels is not. That memory is owned by OpenGL and you must call glDeleteTextures to free it.

// get the texture gl name
GLuint name = self.hugeTexture.name;
// delete texture from opengl
glDeleteTextures(1, &name);
// set texture info to nil (or your new texture, etc)
self.hugeTexture = nil;

You might think to put this in your completion block and then you're home free but you will still be leaking memory. The details are not 100% clear to me, but from what I know:

  1. Every iOS thread requires its own EAGLContext.
  2. Your completion handler is run on the queue you passed in. (Try logging dispatch_queue_get_label(dispatch_get_current_queue()) in a few places to see this.)
    Since we are not executing the async load on the main queue (what would be the point of that?), our completion handler is not run on the main queue and does not have access to the correct context.

There are two solutions to this:

  1. Delete your texture in the main queue, before you run your async texture load
  2. Force the completion hander to run on the main queue

The first option is easy, you just call the glDeleteTextures code above, before calling textureWithContentsOfFile.

To perform the second option, you'll have to modify your completion block slightly, calling your delete code in a dispatch block on the main queue. See the complete code listing for an example.

COMPLETE CODE LISTING

//
// Imagine you have this in your .h
//

@property (strong) GLKTextureLoader *asyncTextureLoader;
@property (strong) GLKTextureInfo *hugeTexture;
@property (strong) EAGLContext *context;


//
// somewhere in your initialization code
//

// create GL context
self.context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];

// check errors, etc ...

// create texture loader and give it the context share group.
self.asyncTextureLoader = [GLKTextureLoader alloc] initWithSharegroup:self.context.sharegroup]

//
// Later, when you need to load your texures
//

NSDictionary *options = @{GLKTextureLoaderOriginBottomLeft:@YES};
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
void (^complete)(GLKTextureInfo*, NSError*) = ^(GLKTextureInfo *texture,
                                                NSError *e){
  if(e){
    // give up
    return;
  }
  // run our actual completion code on the main queue
  // so the glDeleteTextures call works
  dispatch_sync(dispatch_get_main_queue(), ^{
    // delete texture
    GLuint name = self.hugeTexture.name;
    glDeleteTextures(1, &name);
    // assign loaded texture
    self.hugeTexture = texture;
  });
};
// load texture in queue and pass in completion block
[self.asyncTextureLoader textureWithContentsOfFile:@"my_texture_path.png"
                                           options:options
                                             queue:queue
                                 completionHandler:complete];

KNOWING WHEN MULTIPLE LOADS FINISH

As an aside, you can use GCD dispatch groups to run a completion handler after multiple loads have completed. The process is:

  1. Create a dispatch_group_t with dispatch_group_create()
  2. For each async task you are doing, enter the group with dispatch_group_enter(group)
  3. When your async task is done, leave the group with dispatch_group_leave(group)
  4. Register for group completion with dispatch_group_notify(...)

Here's a contrived example.

//
// imagine we have a 'please wait, loading' view showing and we want
// to hide it after these textures are loaded
//
self.loadingView.hidden = NO;

// files to load
NSArray *files = @[@"my_texture_1.png",
                  @"my_texture_2.png",
                  @"my_texture_3.png"];
// resulting textures will be saved here
NSMutableArray *textures;

// setup defaults for GLKTextureLoader
NSDictionary *options = @{GLKTextureLoaderOriginBottomLeft:@YES};
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

// #1
// create a dispatch group, which we will add our tasks too.
// the group will let us know when all the tasks are complete.
// you can think of a group sort of like a JS promise.
dispatch_group_t textureLoadGroup = dispatch_group_create();

// load each of our textures async
for(NSString *file in files){
    // define our completion hander
    // remember, this is called AFTER the texture has loaded
    void (^complete)(GLKTextureInfo*, NSError*) = ^(GLKTextureInfo *texture,
                                                    NSError *e){
        NSLog(@"Loaded texture: %@", file);
        [textures addObject:texture];
        // #3
        // Leave the dispatch group once we're finished
        dispatch_group_leave(textureLoadGroup);
    };

    // #2
    // join the dispatch group before we start the task
    // this basically increments a counter on the group while
    // dispatch_group_leave will decrement that counter.
    dispatch_group_enter(textureLoadGroup);

    // load texture in queue and pass in completion block
    [self.asyncTextureLoader textureWithContentsOfFile:file
                                               options:options
                                                 queue:queue
                                     completionHandler:complete];
}

// #4
// set a block to be run when the group completes 
// (when everyone who entered has left)
// we'll run this notification block on the main queue
dispatch_group_notify(textureLoadGroup, dispatch_get_main_queue(), ^{
    NSLog(@"All textures are loaded.");
    for(GLKTextureInfo *t in textures){
        NSLog(@"Texture: %d, %d x %d", t.name, t.width, t.height);
    }
    // hide your loading view, etc
    self.loadingView.hidden = YES;
});

Finally, empty groups will fire instantly, which can allow you to tidy up some code that might be run irregularlly.

Imagine you had the following

- (void)resetLayout
{
  if(some_complex_view_visible){
    [UIView animateWithDuration:1.0f
                     animations:^{
                         // some complex animations to hide the complex view
                     }
                     completion:^(BOOL finished) {
                         // reset my other layout elements after the
                         // complex view is hidden
                         self.otherView.hidden = YES;
                     }];
  }
  else{
    // reset my other layout elements.
    self.otherView.hidden = YES;
  }
}

Obviously we have code duplication here and its a prime refactor target.

In the next example, self.otherView.hidden = YES; is run after the complex view animation is complete, or instantly if some_complex_view_visible == NO.

NB: the example could also be done by putting self.otherview.hidden = YES; in a block, then passing the block as the animation completion block and calling the block directly in the else clause. Hopefully you can see where the pattern might be applied in a more complex situation where blocks would get overly convoluted.

You should also note that resetLayout will return before the dispatch_group_notify block is executed. This pattern will not fit all problems but is useful to know. The example code is used only because UI animation code is familiar and easy to understand.

- (void)resetLayout
{
  // make group
  dispatch_group_t group = dispatch_group_create();
  if(some_complex_view_visible){
    // enter group
    dispatch_group_enter(group);
    [UIView animateWithDuration:1.0f
                     animations:^{
                         // some complex animations to hide the complex view
                     }
                     completion:^(BOOL finished) {
                        // leave group
                        dispatch_group_leave(group);
                     }];
  }
  // this is run instantly if the group is empty, else it is run after 
  // the group becomes empty.
  dispatch_group_notify(group, dispatch_get_main_queue(), ^{
    // reset my other layout elements.
    self.otherView.hidden = YES;
  });
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末义钉,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子钧嘶,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,198評(píng)論 6 514
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡笨使,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,334評(píng)論 3 398
  • 文/潘曉璐 我一進(jìn)店門(mén)僚害,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人繁调,你說(shuō)我怎么就攤上這事萨蚕。” “怎么了蹄胰?”我有些...
    開(kāi)封第一講書(shū)人閱讀 167,643評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵岳遥,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我裕寨,道長(zhǎng)浩蓉,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 59,495評(píng)論 1 296
  • 正文 為了忘掉前任宾袜,我火速辦了婚禮捻艳,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘庆猫。我一直安慰自己认轨,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,502評(píng)論 6 397
  • 文/花漫 我一把揭開(kāi)白布月培。 她就那樣靜靜地躺著嘁字,像睡著了一般。 火紅的嫁衣襯著肌膚如雪杉畜。 梳的紋絲不亂的頭發(fā)上纪蜒,一...
    開(kāi)封第一講書(shū)人閱讀 52,156評(píng)論 1 308
  • 那天,我揣著相機(jī)與錄音此叠,去河邊找鬼纯续。 笑死,一個(gè)胖子當(dāng)著我的面吹牛拌蜘,可吹牛的內(nèi)容都是我干的杆烁。 我是一名探鬼主播,決...
    沈念sama閱讀 40,743評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼简卧,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼兔魂!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起举娩,我...
    開(kāi)封第一講書(shū)人閱讀 39,659評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤析校,失蹤者是張志新(化名)和其女友劉穎构罗,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體智玻,經(jīng)...
    沈念sama閱讀 46,200評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡遂唧,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,282評(píng)論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了吊奢。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片盖彭。...
    茶點(diǎn)故事閱讀 40,424評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖页滚,靈堂內(nèi)的尸體忽然破棺而出召边,到底是詐尸還是另有隱情,我是刑警寧澤裹驰,帶...
    沈念sama閱讀 36,107評(píng)論 5 349
  • 正文 年R本政府宣布隧熙,位于F島的核電站,受9級(jí)特大地震影響幻林,放射性物質(zhì)發(fā)生泄漏贞盯。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,789評(píng)論 3 333
  • 文/蒙蒙 一沪饺、第九天 我趴在偏房一處隱蔽的房頂上張望躏敢。 院中可真熱鬧,春花似錦整葡、人聲如沸父丰。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,264評(píng)論 0 23
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)蛾扇。三九已至,卻和暖如春魏滚,著一層夾襖步出監(jiān)牢的瞬間镀首,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,390評(píng)論 1 271
  • 我被黑心中介騙來(lái)泰國(guó)打工鼠次, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留更哄,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,798評(píng)論 3 376
  • 正文 我出身青樓腥寇,卻偏偏與公主長(zhǎng)得像成翩,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子赦役,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,435評(píng)論 2 359

推薦閱讀更多精彩內(nèi)容

  • PLEASE READ THE FOLLOWING APPLE DEVELOPER PROGRAM LICENSE...
    念念不忘的閱讀 13,489評(píng)論 5 6
  • 看完片子總要習(xí)慣性瞅瞅影評(píng)麻敌,生怕自己的感動(dòng)與眼淚來(lái)的太過(guò)廉價(jià)。這次掂摔,淚流滿面后看專業(yè)影評(píng)人指出導(dǎo)演哪哪犯低級(jí)錯(cuò)誤术羔,...
    岑子辛閱讀 319評(píng)論 0 0
  • 01 2012年春天赢赊,我和云帆從好友變成戀人,說(shuō)實(shí)話级历,我們還真的有點(diǎn)不適應(yīng)释移,而我們走在一起,也讓很多朋友驚訝不已寥殖。...
    城火閱讀 3,632評(píng)論 5 7
  • 十八歲 一個(gè)從懵懂無(wú)知漸變成熟的年紀(jì) 那年 我們十八歲 做著多少次重復(fù)的夢(mèng) 想著多少次遺忘的人 那年 我們十八歲 ...
    琈煦閱讀 382評(píng)論 0 4
  • 導(dǎo)語(yǔ): 對(duì)自己定位玩讳,對(duì)自己的成長(zhǎng)定位,對(duì)自己的時(shí)間進(jìn)行合理買(mǎi)賣(mài)嚼贡,才能很好地成長(zhǎng)锋边,甚至超速成長(zhǎng)。以下以個(gè)人商業(yè)發(fā)展模...
    Firewinter閱讀 366評(píng)論 0 0