I have many CALayers which are created on the fly while my app is running, and I need to be able to produce a single bitmap of these, which will later be masked.
When I need to create the mask, the CALayers are already drawn to the background (also using shouldRasterize = YES) , and using renderInContext I am able to get a bitmap. However, as the amount of CAlayers increases, the pause caused by renderInContext gets longer and longer. Is there an alternative I can use to renderInContext, or an alternative way I can use it to stop my app temporarily freezing?
The ideal would be to access the already drawn pixel data directly from memory/buffer/cache without using OpenGL, but I am unsure if this is possible with CoreAnimation.
Thanks, any additional information at all would be very useful!
Rob is right about renderInContext: being the right method to use here. Render in context does actually render the layer's pixel data into a context. Here's a sample application that will draw 10,000 layers on a background thread...
The application does the following:
Create's a UIView
Adds 10,000 layers to that view's layer
Rendering begins when you touch the screen (i.e. it's an iOS sample app)
Creates a background thread
Renders the UIView's layer into a context (which in turn renders its sublayers)
Creates a UIImage with the content of the render context
Adds the new image to the screen in a UIImageView
It does all this while running a timer on the main thread, showing that the background thread actually doesn't block the main thread
Here is the code...
First, create a subview with a lot of layers:
#implementation C4WorkSpace {
UIView *v;
dispatch_queue_t backgroundRenderQueue;
CFTimeInterval beginTime;
NSTimer *timer;
NSInteger timerCallCount;
}
-(void)setup {
//create a view to hold a bunch of CALayers
v = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 400, 400)];
v.center = CGPointMake(384,512);
//create a buch of CALayers and add them to a view
for(int i = 0; i < 10000; i++) {
CALayer *l = [[CALayer alloc] init];
l.frame = CGRectMake([self random:390],[self random:390],10,10);
l.backgroundColor = [UIColor blueColor].CGColor;
l.borderColor = [UIColor orangeColor].CGColor;
l.borderWidth = 2.0f;
[v.layer addSublayer:l];
}
//add the view to the application's main view
[self.view addSubview:v];
}
-(NSInteger)random:(NSInteger)value {
srandomdev();
return ((NSInteger)random())%value;
}
Second, create a method that will start a timer and then triggers the render...
-(void)touchesBegan {
timer = [NSTimer scheduledTimerWithTimeInterval:0.03f target:self selector:#selector(printTime) userInfo:nil repeats:YES];
[[NSRunLoop mainRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
[self render];
}
-(void)printTime {
NSLog(#"%d (main thread running)",++timerCallCount);
}
Third, create a render loop with a callback method that puts an image on the screen after rendering is completed.
-(void)render {
NSLog(#"render was called");
//create the queue
backgroundRenderQueue = dispatch_queue_create("backgroundRenderQueue",DISPATCH_QUEUE_CONCURRENT);
//create a async call on the background queue
dispatch_async(backgroundRenderQueue, ^{
//create a cgcontext
NSUInteger width = (NSUInteger)v.frame.size.width;
NSUInteger height = (NSUInteger)v.frame.size.height;
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
NSUInteger bytesPerPixel = 4;
NSUInteger bytesPerRow = bytesPerPixel * width;
unsigned char *rawData = malloc(height * bytesPerRow);
NSUInteger bitsPerComponent = 8;
CGContextRef context = CGBitmapContextCreate(rawData, width, height, bitsPerComponent, bytesPerRow, colorSpace, kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big);
//render the layer and its subviews
[v.layer renderInContext:context];
//create a callback async on the main queue when rendering is complete
dispatch_async(dispatch_get_main_queue(), ^{
//create an image from the context
UIImage *m = [UIImage imageWithCGImage:CGBitmapContextCreateImage(context)];
UIImageView *uiiv = [[UIImageView alloc] initWithImage:m];
//add the image view to the main view
[self.view addSubview:uiiv];
CGColorSpaceRelease(colorSpace);
CGContextRelease(context);
NSLog(#"rendering complete");
[timer invalidate];
});
});
}
NOTE: if your layers aren't all in the same sublayer, you can easily call a for loop that translates the context to and from the origin of each CALayer and draw each layer individually into the context itself
When I run this I get the following output:
2013-03-18 07:14:28.617 C4iOS[21086:907] render was called
2013-03-18 07:14:28.648 C4iOS[21086:907] 1 (main thread running)
2013-03-18 07:14:28.680 C4iOS[21086:907] 2 (main thread running)
2013-03-18 07:14:28.709 C4iOS[21086:907] 3 (main thread running)
2013-03-18 07:14:28.737 C4iOS[21086:907] 4 (main thread running)
2013-03-18 07:14:28.767 C4iOS[21086:907] 5 (main thread running)
2013-03-18 07:14:28.798 C4iOS[21086:907] 6 (main thread running)
2013-03-18 07:14:28.828 C4iOS[21086:907] 7 (main thread running)
2013-03-18 07:14:28.859 C4iOS[21086:907] 8 (main thread running)
2013-03-18 07:14:28.887 C4iOS[21086:907] 9 (main thread running)
2013-03-18 07:14:28.917 C4iOS[21086:907] 10 (main thread running)
2013-03-18 07:14:28.948 C4iOS[21086:907] 11 (main thread running)
2013-03-18 07:14:28.978 C4iOS[21086:907] 12 (main thread running)
2013-03-18 07:14:29.010 C4iOS[21086:907] 13 (main thread running)
2013-03-18 07:14:29.037 C4iOS[21086:907] 14 (main thread running)
2013-03-18 07:14:29.069 C4iOS[21086:907] 15 (main thread running)
2013-03-18 07:14:29.097 C4iOS[21086:907] 16 (main thread running)
2013-03-18 07:14:29.130 C4iOS[21086:907] 17 (main thread running)
2013-03-18 07:14:29.159 C4iOS[21086:907] 18 (main thread running)
2013-03-18 07:14:29.189 C4iOS[21086:907] 19 (main thread running)
2013-03-18 07:14:29.217 C4iOS[21086:907] 20 (main thread running)
2013-03-18 07:14:29.248 C4iOS[21086:907] 21 (main thread running)
2013-03-18 07:14:29.280 C4iOS[21086:907] 22 (main thread running)
2013-03-18 07:14:29.309 C4iOS[21086:907] 23 (main thread running)
2013-03-18 07:14:29.337 C4iOS[21086:907] 24 (main thread running)
2013-03-18 07:14:29.369 C4iOS[21086:907] 25 (main thread running)
2013-03-18 07:14:29.397 C4iOS[21086:907] 26 (main thread running)
2013-03-18 07:14:29.405 C4iOS[21086:907] rendering complete
renderInContext: is the best tool here, but you don't need to run it on the main thread. Just move this to a background thread and it'll stop freezing your app.
Related
my app crashed when nsstring getting contents of URL.
website = [NSString stringWithFormat:#"hidden."];
contents = [NSString stringWithContentsOfURL:[NSURL URLWithString:website] encoding:NSUTF8StringEncoding error:nil];
the crash log is:
void _WebThreadLockFromAnyThread(bool), 0x865cc40: Obtaining the web lock from a thread other than the main thread or the web thread. UIKit should not be called from a secondary thread.
2012-07-10 11:15:17.654 MyApplication[1505:19407] OK // some output
2012-07-10 11:15:17.660 MyApplication[1505:19407] bool _WebTryThreadLock(bool), 0x865cc40: Tried to obtain the web lock from a thread other than the main thread or the web thread. This may be a result of calling to UIKit from a secondary thread. Crashing now...
1 WebThreadLock
2 -[UITextRangeImpl isEmpty]
3 -[UITextRange(UITextSelectionAdditions) _isCaret]
4 -[UITextSelectionView setCaretBlinks:]
5 -[UIKeyboardImpl setCaretBlinks:]
6 -[UIKeyboardImpl setDelegate:force:]
7 -[UIKeyboardImpl setDelegate:]
8 -[UIPeripheralHost(UIKitInternal) _reloadInputViewsForResponder:]
9 -[UIResponder(UIResponderInputViewAdditions) reloadInputViews]
10 -[UIResponder(Internal) _windowBecameKey]
11 -[UITextField _windowBecameKey]
12 -[UIWindow makeKeyWindow]
13 -[UIWindow makeKeyAndVisible]
14 -[MyApplicationAppDelegate checkLogin]
15 -[NSThread main]
16 __NSThread__main__
17 _pthread_start
18 thread_start
the code is called with [self performSelectorInBackground:#selector(checkLogin) withObject:nil]; from IBAction. what i am doing wrong?
(Xcode 4.3, Mac OS X Lion, iPhone Simulator 5.1)
According to that error message, you're trying to update a UIKit object from a background thread, and the UI can only be updated from the main thread.
Instead, use self performSelectorOnMainThread to call back to the main thread to update your UI with whatever information you received when your web call returns.
From your main thread:
[self performSelectorInBackground:#selector(checkLogin) withObject:nil];
In the background:
- (void)checkLogin {
// Do web stuff
...
[self performSelectorOnMainThread:#selector(updateUI)
withObject:nil
waitUntilDone:NO];
}
- (void)updateUI {
[statusLabel setText:#"Done!"];
}
Try using
- (id)performSelector:(SEL)aSelector withObject:(id)anObject.
Scenario is like this--
In my app there is an Scroll View with many instances of
MyCustomImageDownloaderController(containing imageViews where images are to be assigned after downloading) .
As per our requirement, an image has to be downloaded as we move on to a page.
Scroll View + (MyCustomImageDownloaderController1, MyCustomImageDownloaderController2, MyCustomImageDownloaderController3.... etc)
Let's say i am scrolling on it,
i reached to page 1 --> image for it should start downloading
i reached to page 2 --> image for it should start downloading...so on
and if i am on page 3 and images for previous pages if not been dowloaded, they should stop downloading.
So i tried it with using threads..
on API..
- (void)scrollViewDidEndDecelerating:(UIScrollView *)sender{
Step 1) calculated currentPageNumber
Step 2) started thread for downloading image with url for this currentPage
//startBackGroundThreadForPlaceImage:(NSURL *) url
Step 3)stopped thread for previous page , if that is still running
}
Now My MyCustomImageDownloaderController is as
-(void) startBackGroundThreadForPlaceImage:(NSURL *) url{
if(isImageDownloaded == NO){
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
//[self performSelectorInBackground:#selector(loadImageInBackground:) withObject:imageUrl];
myThread = [[NSThread alloc] initWithTarget:self selector:#selector(loadImageInBackground:) object:imageUrl];
//[NSThread detachNewThreadSelector:#selector(loadImageInBackground:) toTarget:self withObject:imageUrl];
[myThread start];
NSLog(#"The current thread is %# ", [[NSThread currentThread] name]);
[pool release];
}
}
NOW Here selector does the work of loading image and assigning to image view
Now Stopping the thread
-(void) stopBackgroundThread{
[myThread cancel];
//[[NSThread currentThread] cancel];
//if([[NSThread currentThread] isCancelled]) {
//[NSThread exit];
//}
[NSThread exit];
}
-(BOOL) isThreadRunning{
return [myThread isExecuting];
}
So i tried a lot of things, but could not Stop the thread in between..
Basically once instantiated thread using any of three methods
1) perform Selector in BackGround
2) NSThread detach new thread
3) NSThread alloc..init with..
In first 2 methods how to get the instance of the newly created thread, so that i could stoop it,
as NSThread currentThread doest not give that
in Method 3,
myThread = [[NSThread alloc] initWithTarget:self selector:#selector(loadImageInBackground:) object:imageUrl];
when i tried
[myThread cancel];
It did not cancel that thread,,
When i tried
[NSThread exit];
it hangs on current screen,,,,i guess it has stopped the main thread
Please help me
Thanks in Advance*strong text*
It's generally better to ask the thread to stop, rather than forcing it, which should be considered a last resort. You should, therefore, frequently check a 'stop flag' and when this gets set, terminate the current thread (by simply exiting the method, in this case). You just need to provide a property on the class the thread is operating on so callers can ask the thread to stop.
It's no different in C++ or Java.
I'm using the CCScrollLayer and I wanna prepare images during player select the stages. It's simple but when I scroll stages, it tasks time(delay to load images).
so I decided to use NSThread. and I got a message "cocos2d: CCSpriteFrameCache: Trying to use file 'Level3.png' as texture" from cocos2d. then It's supposed to appear. but These images I loaded on thread doesn't to appear as I want. just nothing.
-(void) moveToStagePage:(int)page
{
...
[NSThread detachNewThreadSelector:#selector(prepareTexture:) toTarget:self withObject:[NSNumber numberWithInt:page]];
...
}
below source is the code which preparing images.(Thread)
-(void) prepareTexture:(NSNumber*)number
{
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
int _page = [number intValue];
NSLog(#"%d Thread start", _page);
if(loadingTexNum != 0 && (_page + 1) != loadingTexNum)
{
[[CCSpriteFrameCache sharedSpriteFrameCache] removeSpriteFramesFromFile:[NSString stringWithFormat:#"Level%d.plist", loadingTexNum]];
loadingTexNum = _page + 1;
[[CCSpriteFrameCache sharedSpriteFrameCache] addSpriteFramesWithFile:[NSString stringWithFormat:#"Level%d.plist", loadingTexNum]];
}
if(loadingTexNum == 0 && (_page + 1) != loadingTexNum)
{
loadingTexNum = _page + 1;
[[CCSpriteFrameCache sharedSpriteFrameCache] addSpriteFramesWithFile:[NSString stringWithFormat:#"Level%d.plist", loadingTexNum]];
}
[NSThread sleepForTimeInterval:10.0];
NSLog(#"%d Thread release", _page);
[pool release];
}
OpenGL doesn't support loading on multiple threads unless you pass the openGL context.
Each thread in a Mac OS X process has a single current OpenGL rendering context. Every time your application calls an OpenGL function, OpenGL implicitly looks up the context associated with the current thread and modifies the state or objects associated with that context.
OpenGL is not reentrant. If you modify the same context from multiple threads simultaneously, the results are unpredictable. Your application might crash or it might render improperly. If for some reason you decide to set more than one thread to target the same context, then you must synchronize threads by placing a mutex around all OpenGL calls to the context, such as gl* and CGL*. OpenGL commands that block—such as fence commands—do not synchronize threads.
Source
You can either use - (void) addImageAsync:(NSString *)filename target:(id) target selector:(SEL)selector; in the CCTextureCache class, calling this on your main thread.
Or you can implement your own, effectively the same as addImageAsync.
For your reference, this is how CCTextureCache achieves loading the images:
NSAutoreleasePool *autoreleasepool = [[NSAutoreleasePool alloc] init];
// textures will be created on the main OpenGL context
// it seems that in SDK 2.2.x there can't be 2 threads creating textures at the same time
// the lock is used for this purpose: issue #472
[contextLock lock];
if( auxEAGLcontext == nil ) {
auxEAGLcontext = [[EAGLContext alloc]
initWithAPI:kEAGLRenderingAPIOpenGLES1
sharegroup:[[[[CCDirector sharedDirector] openGLView] context] sharegroup]];
if( ! auxEAGLcontext )
CCLOG(#"cocos2d: TextureCache: Could not create EAGL context");
}
if( [EAGLContext setCurrentContext:auxEAGLcontext] ) {
// load / create the texture
CCTexture2D *tex = [self addImage:async.data];
// The callback will be executed on the main thread
[async.target performSelectorOnMainThread:async.selector withObject:tex waitUntilDone:NO];
[EAGLContext setCurrentContext:nil];
} else {
CCLOG(#"cocos2d: TetureCache: EAGLContext error");
}
[contextLock unlock];
[autoreleasepool release];
I believe what is happening is that the main thread tries to use the images before the prepareTexture thread has had a chance to load the textures.
If you immediately try to create sprites with the new textures, for example right in the moveToStagePage method, then that is going to fail. Your threaded texture loading method will need to flag to the other thread that it has completed loading the textures. The easiest way is to simply toggle a BOOL variable. Only when the texture loading thread signaled that the textures have been loaded should you try to create sprites etc. using these textures.
When loading textures on a separate thread you need to use [[CCTextureCache sharedTextureCache] addImageAsync:texture target:target selector:#selector(onTextureLoaded)]; (else the `GLContext gets messed up).
I have a Quartz context declared in the main loop as
UIGraphicsBeginImageContext(mySize);
ctx = UIGraphicsGetCurrentContext();
// bla bla bla
This context is changing, as the user is doing stuff on the screen.
At some point in the app I need a new thread to be fired and grab what the context has, saving to a UIImageView.
on the thread I have something like
myImageView.image = UIGraphicsGetImageFromCurrentImageContext();
but this is giving me nil, as the thread is not able to know that the current context is.
How do I solve that?
thanks.
OK, it may be a kludge but you could try this
- (void) myUIGraphicsGetImageFromCurrentImageContext
{
myImageView.image= UIGraphicsGetImageFromCurrentImageContext();
}
Then in your thread replace the myImageView.Image=…line with this:
[self performSelectorOnMainThread:#selector(myUIGraphicsGetImageFromCurrentImageContext) withObject:nil waitUntilDone:YES];
I have an app that loads multiple thumbnail images into a UIScrollVIew. This is a lengthy operation, and so as not to block up the display of the rest of the UI, I am running it in a separate thread. This works fine the first time at application launch, but later a new set of images needs to be loaded into the UIScrollView. When I detach a thread a second time the app crashes (sometimes). Code follows:
// this call is in a separate method
[NSThread detachNewThreadSelector:#selector(addThumbnailsToScrollView) toTarget:self withObject:nil];
// this is the main entry point for the thread
- (void) addThumbnailsToScrollView {
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; // Top-level pool
// now place all the thumb views as subviews of the scroll view
float xPosition = THUMB_H_PADDING;
int pageIndex = 0;
for (Page *page in self.pages) {
// get the page's bitmap image and scale it to thumbnail size
NSString *name = [page valueForKey:#"pageBackground"];
NSString *basePath = [[NSBundle mainBundle] pathForResource:page.pageBackground ofType:#"jpg" inDirectory:nil];
UIImage *thumbImage = [UIImage imageWithContentsOfFile:basePath];
thumbImage = [thumbImage imageScaledToSize:CGSizeMake(80, 100)];
// create a ThumbImageView for each page and add it to the thumbnailScrollView
if (thumbImage) {
ThumbImageView *thumbView = [[ThumbImageView alloc] initWithImage:thumbImage];
[thumbView setDelegate:self];
[thumbView setImageName:name];
[thumbView setImageSize:CGSizeMake(80, 100)];
[thumbView setPageIndex:pageIndex];
pageIndex ++;
CGRect frame = [thumbView frame];
frame.origin.y = 0;
frame.origin.x = xPosition;
[thumbView setFrame:frame];
[thumbnailPagesScrollView addSubview:thumbView];
[thumbView release];
xPosition += (frame.size.width + THUMB_H_PADDING);
}
}
[self hightlightThumbnailPageAtIndex:0];
[(UIActivityIndicatorView *)[thumbnailPagesScrollView.superview viewWithTag:100] stopAnimating];
[pool release]; // Release the objects in the pool.
}
I thought that a detached thread exits as soon as the main entry routine was completed. Wouldn't the second call to detach a thread be a new thread? Why is the app crashing, but sometimes not?
Thanks
Jk
You cannot touch UIKit (meaning UIScrollVIew) in a secondary thread - you need to reorganize so that the fetch takes place in a secondary thread but you make a NSData object (containing the image binary) available to your primary thread for each thumbnail so that it can do everything related to actually displaying them.
Apple repeatedly warn in documentation that UIKit is not thread-safe.
I would suggest adding the thumbView to the thumbnailPagesScrollView on the main thread rather than a separate thread. There might be issues on the retain count of the object across threads. There is a convenience method performSelectorOnMainThread I think it is to do that. You could pass thumbView to that and then add it to the subview.
Alternatively you could do the whole if statement on the main thread as thats not the thing that will interrupt the user.
Also with your activity indicator this should be stopped on the main thread. Everything UI related should be done on the main thread.