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;
  });
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,524评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,869评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,813评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,210评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,085评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,117评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,533评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,219评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,487评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,582评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,362评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,218评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,589评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,899评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,176评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,503评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,707评论 2 335

推荐阅读更多精彩内容

  • PLEASE READ THE FOLLOWING APPLE DEVELOPER PROGRAM LICENSE...
    念念不忘的阅读 13,413评论 5 6
  • 看完片子总要习惯性瞅瞅影评,生怕自己的感动与眼泪来的太过廉价。这次,泪流满面后看专业影评人指出导演哪哪犯低级错误,...
    岑子辛阅读 307评论 0 0
  • 01 2012年春天,我和云帆从好友变成恋人,说实话,我们还真的有点不适应,而我们走在一起,也让很多朋友惊讶不已。...
    城火阅读 3,560评论 5 7
  • 十八岁 一个从懵懂无知渐变成熟的年纪 那年 我们十八岁 做着多少次重复的梦 想着多少次遗忘的人 那年 我们十八岁 ...
    琈煦阅读 374评论 0 4
  • 导语: 对自己定位,对自己的成长定位,对自己的时间进行合理买卖,才能很好地成长,甚至超速成长。以下以个人商业发展模...
    Firewinter阅读 358评论 0 0