How to cache images in Objective-C - objective-c

I want to create a method to cache an image from an URL, I got the code in Swift since I had used it before, how can I do something similar to this in Objective-C:
import UIKit
let imageCache: NSCache = NSCache<AnyObject, AnyObject>()
extension UIImageView {
func loadImageUsingCacheWithUrlString(urlString: String) {
self.image = nil
if let cachedImage = imageCache.object(forKey: urlString as AnyObject) as? UIImage {
self.image = cachedImage
return
}
let url = URL(string: urlString)
if let data = try? Data(contentsOf: url!) {
DispatchQueue.main.async(execute: {
if let downloadedImage = UIImage(data: data) {
imageCache.setObject(downloadedImage, forKey: urlString as AnyObject)
self.image = downloadedImage
}
})
}
}
}

Before you convert this, you might consider refactoring to make it asynchronous:
One should never use Data(contentsOf:) for network requests because (a) it is synchronous and blocks the caller (which is a horrible UX, but also, in degenerate cases, can cause the watchdog process to kill your app); (b) if there is a problem, there’s no diagnostic information; and (c) it is not cancelable.
Rather than updating image property when done, you should consider completion handler pattern, so caller knows when the request is done and the image is processed. This pattern avoids race conditions and lets you have concurrent image requests.
When you use this asynchronous pattern, the URLSession runs its completion handlers on background queue. You should keep the processing of the image and updating of the cache on this background queue. Only the completion handler should be dispatched back to the main queue.
I infer from your answer, that your intent was to use this code in a UIImageView extension. You really should put this code in a separate object (I created a ImageManager singleton) so that this cache is not only available to image views, but rather anywhere where you might need images. You might, for example, do some prefetching of images outside of the UIImageView. If this code is buried in the
Thus, perhaps something like:
final class ImageManager {
static let shared = ImageManager()
enum ImageFetchError: Error {
case invalidURL
case networkError(Data?, URLResponse?)
}
private let imageCache = NSCache<NSString, UIImage>()
private init() { }
#discardableResult
func fetchImage(urlString: String, completion: #escaping (Result<UIImage, Error>) -> Void) -> URLSessionTask? {
if let cachedImage = imageCache.object(forKey: urlString as NSString) {
completion(.success(cachedImage))
return nil
}
guard let url = URL(string: urlString) else {
completion(.failure(ImageFetchError.invalidURL))
return nil
}
let task = URLSession.shared.dataTask(with: url) { data, response, error in
guard
error == nil,
let responseData = data,
let httpUrlResponse = response as? HTTPURLResponse,
200 ..< 300 ~= httpUrlResponse.statusCode,
let image = UIImage(data: responseData)
else {
DispatchQueue.main.async {
completion(.failure(error ?? ImageFetchError.networkError(data, response)))
}
return
}
self.imageCache.setObject(image, forKey: urlString as NSString)
DispatchQueue.main.async {
completion(.success(image))
}
}
task.resume()
return task
}
}
And you'd call it like:
ImageManager.shared.fetchImage(urlString: someUrl) { result in
switch result {
case .failure(let error): print(error)
case .success(let image): // do something with image
}
}
// but do not try to use `image` here, as it has not been fetched yet
If you wanted to use this in a UIImageView extension, for example, you could save the URLSessionTask, so that you could cancel it if you requested another image before the prior one finished. (This is a very common scenario if using this in table views and the user scrolls very quickly, for example. You do not want to get backlogged in a ton of network requests.) We could
extension UIImageView {
private static var taskKey = 0
private static var urlKey = 0
private var currentTask: URLSessionTask? {
get { objc_getAssociatedObject(self, &Self.taskKey) as? URLSessionTask }
set { objc_setAssociatedObject(self, &Self.taskKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) }
}
private var currentURLString: String? {
get { objc_getAssociatedObject(self, &Self.urlKey) as? String }
set { objc_setAssociatedObject(self, &Self.urlKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) }
}
func setImage(with urlString: String) {
if let oldTask = currentTask {
currentTask = nil
oldTask.cancel()
}
image = nil
currentURLString = urlString
let task = ImageManager.shared.fetchImage(urlString: urlString) { result in
// only reset if the current value is for this url
if urlString == self.currentURLString {
self.currentTask = nil
self.currentURLString = nil
}
// now use the image
if case .success(let image) = result {
self.image = image
}
}
currentTask = task
}
}
There are tons of other things you might do in this UIImageView extension (e.g. placeholder images or the like), but by separating the UIImageView extension from the network layer, one keeps these different tasks in their own respective classes (in the spirit of the single responsibility principle).
OK, with that behind us, let us look at the Objective-C rendition. For example, you might create an ImageManager singleton:
// ImageManager.h
#import UIKit;
NS_ASSUME_NONNULL_BEGIN
typedef NS_ENUM(NSUInteger, ImageManagerError) {
ImageManagerErrorInvalidURL,
ImageManagerErrorNetworkError,
ImageManagerErrorNotValidImage
};
#interface ImageManager : NSObject
// if you make this singleton, mark normal instantiation methods as unavailable ...
+ (instancetype)alloc __attribute__((unavailable("alloc not available, call sharedImageManager instead")));
- (instancetype)init __attribute__((unavailable("init not available, call sharedImageManager instead")));
+ (instancetype)new __attribute__((unavailable("new not available, call sharedImageManager instead")));
- (instancetype)copy __attribute__((unavailable("copy not available, call sharedImageManager instead")));
// ... and expose singleton access point
#property (class, nonnull, readonly, strong) ImageManager *sharedImageManager;
// provide fetch method
- (NSURLSessionTask * _Nullable)fetchImageWithURLString:(NSString *)urlString completion:(void (^)(UIImage * _Nullable image, NSError * _Nullable error))completion;
#end
NS_ASSUME_NONNULL_END
and then implement this singleton:
// ImageManager.m
#import "ImageManager.h"
#interface ImageManager()
#property (nonatomic, strong) NSCache<NSString *, UIImage *> *imageCache;
#end
#implementation ImageManager
+ (instancetype)sharedImageManager {
static dispatch_once_t onceToken;
static ImageManager *shared;
dispatch_once(&onceToken, ^{
shared = [[self alloc] initPrivate];
});
return shared;
}
- (instancetype)initPrivate
{
self = [super init];
if (self) {
_imageCache = [[NSCache alloc] init];
}
return self;
}
- (NSURLSessionTask *)fetchImageWithURLString:(NSString *)urlString completion:(void (^)(UIImage *image, NSError *error))completion {
UIImage *cachedImage = [self.imageCache objectForKey:urlString];
if (cachedImage) {
completion(cachedImage, nil);
return nil;
}
NSURL *url = [NSURL URLWithString:urlString];
if (!url) {
NSError *error = [NSError errorWithDomain:[[NSBundle mainBundle] bundleIdentifier] code:ImageManagerErrorInvalidURL userInfo:nil];
completion(nil, error);
return nil;
}
NSURLSessionTask *task = [NSURLSession.sharedSession dataTaskWithURL:url completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
if (error) {
dispatch_async(dispatch_get_main_queue(), ^{
completion(nil, error);
});
return;
}
if (!data) {
NSError *error = [NSError errorWithDomain:[[NSBundle mainBundle] bundleIdentifier] code:ImageManagerErrorNetworkError userInfo:nil];
dispatch_async(dispatch_get_main_queue(), ^{
completion(nil, error);
});
}
UIImage *image = [UIImage imageWithData:data];
if (!image) {
NSDictionary *userInfo = #{
#"data": data,
#"response": response ? response : [NSNull null]
};
NSError *error = [NSError errorWithDomain:[[NSBundle mainBundle] bundleIdentifier] code:ImageManagerErrorNotValidImage userInfo:userInfo];
dispatch_async(dispatch_get_main_queue(), ^{
completion(nil, error);
});
}
[self.imageCache setObject:image forKey:urlString];
dispatch_async(dispatch_get_main_queue(), ^{
completion(image, nil);
});
}];
[task resume];
return task;
}
#end
And you'd call it like:
[[ImageManager sharedImageManager] fetchImageWithURLString:urlString completion:^(UIImage * _Nullable image, NSError * _Nullable error) {
if (error) {
NSLog(#"%#", error);
return;
}
// do something with `image` here ...
}];
// but not here, because the above runs asynchronously
And, again, you could use this from within a UIImageView extension:
#import <objc/runtime.h>
#implementation UIImageView (Cache)
- (void)setImage:(NSString *)urlString
{
NSURLSessionTask *oldTask = objc_getAssociatedObject(self, &taskKey);
if (oldTask) {
objc_setAssociatedObject(self, &taskKey, nil, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
[oldTask cancel];
}
image = nil
objc_setAssociatedObject(self, &urlKey, urlString, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
NSURLSessionTask *task = [[ImageManager sharedImageManager] fetchImageWithURLString:urlString completion:^(UIImage * _Nullable image, NSError * _Nullable error) {
NSString *currentURL = objc_getAssociatedObject(self, &urlKey);
if ([currentURL isEqualToString:urlString]) {
objc_setAssociatedObject(self, &urlKey, nil, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
objc_setAssociatedObject(self, &taskKey, nil, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
if (image) {
self.image = image;
}
}];
objc_setAssociatedObject(self, &taskKey, task, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
#end

After trial and error this worked:
#import "UIImageView+Cache.h"
#implementation UIImageView (Cache)
NSCache* imageCache;
- (void)loadImageUsingCacheWithUrlString:(NSString*)urlString {
imageCache = [[NSCache alloc] init];
self.image = nil;
UIImage *cachedImage = [imageCache objectForKey:(id)urlString];
if (cachedImage != nil) {
self.image = cachedImage;
return;
}
NSURL *url = [NSURL URLWithString:urlString];
NSData *data = [NSData dataWithContentsOfURL:url];
if (data != nil) {
dispatch_async(dispatch_get_main_queue(), ^{
UIImage *downloadedImage = [UIImage imageWithData:data];
if (downloadedImage != nil) {
[imageCache setObject:downloadedImage forKey:urlString];
self.image = downloadedImage;
}
});
}
}
#end

Related

NSURLSessionDownloadTask in NSOperation crashes on cancel

I'm trying to create a DownloadOperation subclass of NSOperation to download data asynchronously. Everything seemed to be working fine until I tried to add cancelling support. Basically, the completion handler of the operation's NSURLSessionDownloadTask seems to be called after the operation has been released. It will crash with EXC_BAD_ACCESS at the line weakSelf.state = kFinished.
The full sample project is here: https://github.com/angstsmurf/DownloadOperationQueue. Press Command+. after running to crash.
#import "DownloadOperation.h"
typedef enum OperationState : NSUInteger {
kReady,
kExecuting,
kFinished
} OperationState;
#interface DownloadOperation ()
#property NSURLSessionDownloadTask *task;
#property OperationState state;
#end
#implementation DownloadOperation
// default state is ready (when the operation is created)
#synthesize state = _state;
- (void)setState:(OperationState)state {
#synchronized(self) {
if (_state != state) {
[self willChangeValueForKey:#"isExecuting"];
[self willChangeValueForKey:#"isFinished"];
_state = state;
[self didChangeValueForKey: #"isExecuting"];
[self didChangeValueForKey: #"isFinished"];
}
}
}
- (OperationState)state {
#synchronized (self) {
return _state;
}
}
- (BOOL)isReady { return (self.state == kReady); }
- (BOOL)isExecuting { return (self.state == kExecuting); }
- (BOOL)isFinished { return (self.state == kFinished); }
- (BOOL)isAsynchronous {
return YES;
}
- (instancetype)initWithSession:(NSURLSession *)session downloadTaskURL:(NSURL *)downloadTaskURL completionHandler:(nullable void (^)(NSURL * _Nullable, NSURLResponse * _Nullable, NSError * _Nullable))completionHandler {
self = [super init];
if (self) {
__unsafe_unretained DownloadOperation *weakSelf = self;
// use weak self to prevent retain cycle
_task = [[NSURLSession sharedSession] downloadTaskWithURL:downloadTaskURL
completionHandler:^(NSURL * _Nullable localURL, NSURLResponse * _Nullable response, NSError * _Nullable error) {
/*
if there is a custom completionHandler defined,
pass the result gotten in downloadTask's completionHandler to the
custom completionHandler
*/
if (completionHandler) {
completionHandler(localURL, response, error);
}
/*
set the operation state to finished once
the download task is completed or have error
*/
weakSelf.state = kFinished;
}];
}
return self;
}
- (void)start {
/*
if the operation or queue got cancelled even
before the operation has started, set the
operation state to finished and return
*/
if (self.cancelled) {
self.state = kFinished;
return;
}
// set the state to executing
self.state = kExecuting;
NSLog(#"downloading %#", self.task.originalRequest.URL.absoluteString);
// start the downloading
[self.task resume];
}
-(void)cancel {
[super cancel];
// cancel the downloading
[self.task cancel];
}
#end
As pointed out in the comments by Scott Thompson, the correct keyword to use for the weakSelf variable is __weak, not __unsafe_unretained.

Microsoft MSAL ObjC - Trying to acquire token Interactively for multiple scopes

I am trying to acquire token Interactively for multiple scopes, Policy and RMS scopes using Objective C. I am not sure whether I am doing it right or wrong, the way I am trying to get it as below.
I have written a method "aquireToken" where I am calling the function twice one for Policy scope and other RMS scope and updating result in NSDictionary.
There is a flag which is being updated inside the "completionBlock = ^(MSALResult *result, NSError *error)". But its value is not reflected in the caller "aquireToken" function.
The code snippet is as below:
- (void) aquireToken
{
policyTokenResult = false;
rmsTokenResult = false;
NSError *error = nil;
MSALPublicClientApplication *application = [self createPublicClientApplication:&error];
[self retrieveTokens:application forScopes:scopesPolicy isPolicy:true];
if (policyTokenResult)
{
[self retrieveTokens:application forScopes:scopesRMS isPolicy:false];
}
for (NSString* key in resultMap) {
id value = resultMap[key];
// id object = [resultDict objectForKey:key];
NSLog(#"%# = %#", key, value);
// do stuff
}
}
- (void)retrieveTokens:(MSALPublicClientApplication*) application
forScopes: (NSArray<NSString *> *) scopes
isPolicy: (BOOL) isPolicy
{
NSError *error = nil;
MSALAccount* userAccount = nil;
for (MSALAccount *account in [application allAccounts:&error])
{
if([[account.username uppercaseString] isEqualToString:[authID uppercaseString]])
{
NSLog(#"Account Found: \t%#", account.username);
userAccount = account;
break;
}
}
MSALCompletionBlock completionBlock;
__block __weak MSALCompletionBlock weakCompletionBlock;
weakCompletionBlock = completionBlock = ^(MSALResult *result, NSError *error)
{
dispatch_async(dispatch_get_main_queue(), ^{
if (!error)
{
if (isPolicy)
{
[resultMap setObject:result.accessToken forKey:#"PolicyAccessToken"];
[resultMap setObject:result.account.username forKey:#"UserId"];
authID = result.account.username;
policyTokenResult = true;
}
else
{
[resultMap setObject:result.accessToken forKey:#"RMSAccessToken"];
rmsTokenResult = true;
}
if(policyTokenResult && rmsTokenResult)
{
[resultMap setObject:#"" forKey:#"ResultStatusSuccess"];
}
return;
}
if ([error.domain isEqualToString:MSALErrorDomain] && error.code == MSALErrorInteractionRequired)
{
[self acquireTokenInteractive:application scopes:scopes isPolicy:isPolicy completionBlock:weakCompletionBlock];
return;
}
});
};
if(userAccount)
{
[self acquireTokenSilent:application scopes:scopes forAccount:userAccount isPolicy:isPolicy completionBlock:completionBlock];
}
else
{
[self acquireTokenInteractive:application scopes:scopes isPolicy:isPolicy completionBlock:completionBlock];
}
}
- (void) acquireTokenSilent: (MSALPublicClientApplication *) application
scopes: (NSArray<NSString *> *) scopes
forAccount: (MSALAccount *) userAccount
isPolicy: (BOOL) isPolicy
completionBlock: (MSALCompletionBlock) completionBlock
{
MSALSilentTokenParameters *silentParams = [[MSALSilentTokenParameters alloc] initWithScopes:scopes account:userAccount];
[application acquireTokenSilentWithParameters:silentParams completionBlock:completionBlock];
}
- (void) acquireTokenInteractive: (MSALPublicClientApplication *) application
scopes: (NSArray<NSString *> *) scopes
isPolicy: (BOOL) isPolicy
completionBlock: (MSALCompletionBlock)completionBlock
{
MSALInteractiveTokenParameters *interactiveParams = [[MSALInteractiveTokenParameters alloc] initWithScopes:scopes];
[interactiveParams setPromptType:MSALPromptTypeSelectAccount];
interactiveParams.completionBlockQueue = dispatch_get_main_queue();
[application acquireTokenWithParameters:interactiveParams completionBlock:completionBlock];
}

How to share saved games across devices?

I implemented GameKit into my iOS game including the saved game feature.
Here an example how I save and load a game:
MobSvcSavedGameData.h
#ifndef MOBSVC_SAVEDGAMEDATA_H
#define MOBSVC_SAVEDGAMEDATA_H
#import <Foundation/Foundation.h>
#interface MobSvcSavedGameData : NSObject <NSCoding>
#property (readwrite, retain) NSString *data;
+(instancetype)sharedGameData;
-(void)reset;
#end
#endif /* MOBSVC_SAVEDGAMEDATA_H */
MobSvcSavedGameData.m
#import "MobSvcSavedGameData.h"
#import <Foundation/Foundation.h>
#interface MobSvcSavedGameData () <NSObject, NSCoding>
#end
#implementation MobSvcSavedGameData
#pragma mark MobSvcSavedGameData implementation
static NSString * const sgDataKey = #"data";
+ (instancetype)sharedGameData {
static id sharedInstance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedInstance = [[self alloc] init];
});
return sharedInstance;
}
- (void)reset
{
self.data = nil;
}
- (void)encodeWithCoder:(NSCoder *)encoder
{
[encoder encodeObject:self.data forKey: sgDataKey];
}
- (nullable instancetype)initWithCoder:(nonnull NSCoder *)decoder {
self = [self init];
if (self) {
self.data = [decoder decodeObjectForKey:sgDataKey];
}
return self;
}
#end
For simplicity my saved game object above has only a NSString which will be serialised and uploaded like so:
void MobSvc::uploadSavedGameDataAwait(const char *name, const char *data)
{
GKLocalPlayer *mobSvcAccount = [GKLocalPlayer localPlayer];
if(mobSvcAccount.isAuthenticated)
{
MobSvcSavedGameData *savedGameData = [[MobSvcSavedGameData alloc] init];
savedGameData.data = [NSString stringWithUTF8String:data];
[mobSvcAccount saveGameData:[NSKeyedArchiver archivedDataWithRootObject:savedGameData] withName:[[NSString alloc] initWithUTF8String:name] completionHandler:^(GKSavedGame * _Nullable savedGame __unused, NSError * _Nullable error) {
if(error == nil)
{
NSLog(#"Successfully uploaded saved game data");
}
else
{
NSLog(#"Failed to upload saved game data: %#", error.description);
}
}];
}
}
And this is how I download the most recent saved game on the next play session again:
void MobSvc::downloadSavedGameDataAwait(const char *name)
{
GKLocalPlayer *mobSvcAccount = [GKLocalPlayer localPlayer];
if(mobSvcAccount.isAuthenticated)
{
[mobSvcAccount fetchSavedGamesWithCompletionHandler:^(NSArray<GKSavedGame *> * _Nullable savedGames, NSError * _Nullable error) {
if(error == nil)
{
GKSavedGame *savedGameToLoad = nil;
for(GKSavedGame *savedGame in savedGames) {
const char *sname = savedGame.name.UTF8String;
if(std::strcmp(sname, name) == 0)
{
if (savedGameToLoad == nil || savedGameToLoad.modificationDate < savedGame.modificationDate) {
savedGameToLoad = savedGame;
}
}
}
if(savedGameToLoad != nil) {
[savedGameToLoad loadDataWithCompletionHandler:^(NSData * _Nullable data, NSError * _Nullable error) {
if(error == nil)
{
MobSvcSavedGameData *savedGameData = [NSKeyedUnarchiver unarchiveObjectWithData:data];
NSLog(#"Successfully downloaded saved game data: %#", [savedGameData.data cStringUsingEncoding:NSUTF8StringEncoding]);
}
else
{
NSLog(#"Failed to download saved game data: %#", error.description);
}
}];
}
}
else
{
NSLog(#"Failed to prepare saved game data: %#", error.description);
}
}];
}
}
I tested this by uploading a random string and receiving it on the next session by using the same name. It works! However, as soon as I try to download the saved game from my second iPhone it does not work. On both phones I'm logged into the same Game-Center account, I could confirm this by comparing the playerId in the GKLocalPlayer instance.
I've set up the proper iCloud container and linked my game to it, but the logs in the iCloud container backend remain empty.
What is going on? How can I share the saved game across Apple devices?
The above sample in the question works just fine. It's mandatory that the user logs into iCloud and uses the same apple ID on the Game Center login, because the saved games will be stored in the iCloud.
Unfortunately, I was testing the whole without iCloud, so it couldn't work.

EXC_BAD_ACCESS for an object created inside a Block

I have always been nervous when it comes to blocks and GCD because my mind tells me that it looks very complex!
I am getting a crash inside a block which ideally looks alright to me:
#pragma mark -
-(void)fetchOrdersListWithInfoDict:(NSDictionary*)infoDict completionBlock:(CompletionBlock)completionBlock errorBlock:(ErrorBlock)errorBlock
{
__weak VTVehicleServiceNetworkManager *weakSelf = self;
TaskBlock fetchOrdersListTaskBlock = ^()
{
__block __strong HLOrdersDataProvider *ordersDataProvider = nil;
NSBlockOperation *fetchOrdersOperation = [NSBlockOperation blockOperationWithBlock:[^{
ordersDataProvider = [[HLOrdersDataProvider alloc] init];
[ordersDataProvider performFetchOrdersListWithInfoDict:infoDict
completionBlock:completionBlock
errorBlock:errorBlock];
} copy]];
[weakSelf.dataOperationQueue addOperation:fetchOrdersOperation];
};
[self fetchDataWithTaskBlock:[fetchOrdersListTaskBlock copy]
errorBlock:^(NSError *error) {
errorBlock(error);
}];
}
I was able to trace out the zombie object but I am not able to figure out why is this object turning out to be a zombie. Here is the snapshot from profile:
I have gone through the following guides (1, 2) to see if I can find out what I am doing wrong but I was no where near to find out what is going wrong.
Any help and reference text to what I am doing wrong will help.
Edit:
I have tried what #Jerimy has suggested and in fact my code which I have posted earlier was exactly the same as required: Declaring and initializing ordersDataProvider inside the block operation itself. But since it was crashing at the same point I tried to declare it outside the block just to see if it addresses the crash.
Below is the new code I tested:
#pragma mark -
-(void)fetchOrdersListWithInfoDict:(NSDictionary*)infoDict completionBlock:(CompletionBlock)completionBlock errorBlock:(ErrorBlock)errorBlock
{
__weak VTVehicleServiceNetworkManager *weakSelf = self;
completionBlock = [completionBlock copy];
errorBlock = [errorBlock copy];
TaskBlock fetchOrdersListTaskBlock = ^()
{
NSBlockOperation *fetchOrdersOperation = [NSBlockOperation blockOperationWithBlock:^{
HLOrdersDataProvider *ordersDataProvider = [[HLOrdersDataProvider alloc] init];
[ordersDataProvider performFetchOrdersListWithInfoDict:infoDict
completionBlock:completionBlock
errorBlock:errorBlock];
}];
[weakSelf.dataOperationQueue addOperation:fetchOrdersOperation];
};
[self fetchDataWithTaskBlock:[fetchOrdersListTaskBlock copy]
errorBlock:^(NSError *error) {
errorBlock(error);
}];
}
The crash from Profile:
There is not much from the stack trace as well, SDMHTTPRequest is a library and am very sure there is nothing wrong there, and the HLOrdersDataProvider is the zombie object which I was able to trace out in Instruments app:
EDIT 2
Adding the interface and implementation of HLOrdersDataProvider for more details:
#interface HLOrdersDataProvider : HLDataProvider
-(void)performFetchOrdersListWithInfoDict:(NSDictionary*)infoDict completionBlock:(CompletionBlock)completionBlock errorBlock:(ErrorBlock)errorBlock;
#end
#implementation HLOrdersDataProvider
-(void)performFetchOrdersListWithInfoDict:(NSDictionary*)infoDict completionBlock:(CompletionBlock)completionBlock errorBlock:(ErrorBlock)errorBlock
{
// Using SDMConnectivity
NSString *queryString = [infoDict valueForKey:#"$filter"];
NSString *appendStringForEndpoint = [kRowsetsKeyword stringByAppendingFormat:#"?%#", queryString];
[self fetchDataFromServerWithEndPointAppendString:appendStringForEndpoint
completionBlock:completionBlock
errorBlock:errorBlock];
}
#pragma mark - Service Agent related
-(NSString*)collectionName
{
return [NSString stringWithString:kRowsetsKeyword];
}
-(void)requestFinished:(SDMHttpRequest *)request
{
NSError *error = nil;
// Let's parse the response and send the results back to the caller
NSString *collectionName = [self collectionName];
NSData *responseData = [request responseData];
NSArray *entitiesArray = [self parseODataEntriesWithData:responseData
withCollectionName:collectionName
error:&error];
if (error)
[self triggerFailureBlockWithArgument:error];
else
[self triggerCompletionBlockWithArgument:entitiesArray];
}
#end
Also, HLOrdersDataProvider is inherited from HLDataProvider so below is the interface and implementation of this class too:
#import <Foundation/Foundation.h>
//#import "SDMHttpRequestDelegate.h"
#import "SDMRequestBuilder.h"
#import "SDMHttpRequest.h"
#import "SDMParser.h"
#import "HLConstant.h"
#import "HLConnectionData.h"
#interface HLDataProvider : NSObject <SDMHttpRequestDelegate>
#property (copy, atomic) CompletionBlock completionBlock;
#property (copy , atomic) ErrorBlock errorBlock;
#property (copy, atomic) CompletionBlockWithDataFetchStatus completionBlockWithDataFetchStatus;
+ (id)sharedInstance;
- (NSMutableArray*)parseODataEntriesWithData:(NSData*)data withCollectionName:(NSString*)collectionName;
- (NSMutableArray*)parseODataEntriesWithData:(NSData*)data withCollectionName:(NSString*)collectionName error:(NSError**)outError;
- (NSMutableArray*)parseJSONEntriesWithData:(NSData*)data;
-(NSArray*)fetchEntriesFromDatabaseWithEntityName:(NSString*)entityName relationshipObjects:(NSMutableArray*)relationships;
-(void)updateDatabaseWithEntries:(NSMutableArray*)scanEntries;
-(void)updateDatabaseWithJSONEntries:(NSMutableArray*)scanEntries;
-(id)parsedOdataResultFromEntries:(NSMutableArray*)entries;
-(void)fetchDataFromServerWithEndPointAppendString:(NSString*)appendStr completionBlock:(CompletionBlock)inCompletionBlock errorBlock:(ErrorBlock)inErrorBlock;
-(NSString*)collectionName;
-(void)triggerCompletionBlockWithArgument:(id)inArg;
-(void)triggerFailureBlockWithArgument:(NSError*)inArg;
#end
#implementation HLDataProvider
+ (id)sharedInstance
{
//Subclassess will override this method
return nil;
}
-(NSMutableArray*)parseODataEntriesWithData:(NSData*)data withCollectionName:(NSString*)collectionName
{
return [self parseODataEntriesWithData:data
withCollectionName:collectionName
error:NULL];
}
-(NSMutableArray*)parseODataEntriesWithData:(NSData*)data withCollectionName:(NSString*)collectionName error:(NSError**)outError
{
NSMutableArray *entriesArray = nil;
#try {
entriesArray = sdmParseODataEntriesXML(data,
[[[HLConnectionData metaDataDocument] getCollectionByName:collectionName] getEntitySchema],
[HLConnectionData serviceDocument]);
}
#catch (NSException *exception) {
NSLog(#"Got exception: %#", exception);
if (outError)
{
*outError = [NSError errorWithDomain:#"Vehicle Service"
code:-1001
userInfo:[NSDictionary dictionaryWithObject:exception forKey:NSLocalizedDescriptionKey]];
}
}
#finally {
}
return entriesArray;
}
- (NSMutableArray*)parseJSONEntriesWithData:(NSData*)data
{
NSError *error = nil;
id object = [NSJSONSerialization
JSONObjectWithData:data
options:0
error:&error];
NSMutableArray *resultArray = nil;
if(error) { /* JSON was malformed, act appropriately here */ }
if([object isKindOfClass:[NSDictionary class]])
{
resultArray = [NSMutableArray arrayWithObject:object];
}
else if ([object isKindOfClass:[NSArray class]])
{
resultArray = [NSMutableArray arrayWithArray:object];
}
return resultArray;
}
#pragma mark -
#pragma mark - Data Fetch - Server - SDMConnectivity
-(void)fetchDataFromServerWithEndPointAppendString:(NSString*)appendStr completionBlock:(CompletionBlock)inCompletionBlock errorBlock:(ErrorBlock)inErrorBlock;
{
self.errorBlock = inErrorBlock;
self.completionBlock = inCompletionBlock;
id<SDMRequesting> request = nil;
//NSString *clientStr = #"&sap-client=320&sap-language=EN";
NSString *urlStr =[NSString stringWithFormat:#"%#/%#",[HLConnectionData applicationEndPoint], appendStr];
urlStr = [urlStr stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
[SDMRequestBuilder setRequestType:HTTPRequestType];
request=[SDMRequestBuilder requestWithURL:[NSURL URLWithString:urlStr]];
[request setUsername:kUserName];
/*Set Password in SDMRequesting object*/
[request setPassword:kPassword];
[request setRequestMethod:#"GET"];
[request setTimeOutSeconds:kTimeoutInterval];
/*set the Delegate. This class must adhere to SDMHttpRequestDelegate to get the callback*/
[request setDelegate:self];
/*Call startAsynchronous API to request object to retreive Data asynchrnously in the call backs */
[request startSynchronous];
}
-(void)updateDatabaseWithEntries:(NSMutableArray*)scanEntries
{
//Subclasses will override this
}
-(void)updateDatabaseWithJSONEntries:(NSMutableArray*)scanEntries
{
//Subclasses will override this
}
-(id)parsedOdataResultFromEntries:(NSMutableArray*)entries
{
//Subclasses will override this
return nil;
}
-(void)deleteExistingEntriesFromCoredata
{
//Subclasses will override this
}
-(NSArray*)fetchEntriesFromDatabaseWithEntityName:(NSString*)entityName relationshipObjects:(NSMutableArray*)array
{
//Subclasses will overide this method
return nil;
}
#pragma mark - SDMHTTPRequestDelegate methods
- (void)requestStarted:(SDMHttpRequest*) request
{
}
- (void)requestFinished:(SDMHttpRequest*) request
{
// For service doc and metadata we instantiate HLDataProvider, so we send this raw SDMHTTPRequest object as-is. For other service agents like HLOrdersDataProvider we send the parsed information, check the subclass' implementation of -requestFinished: method
[self triggerCompletionBlockWithArgument:request];
}
-(void)triggerCompletionBlockWithArgument:(id)inArg
{
self.completionBlock(inArg);
}
- (void)requestFailed:(SDMHttpRequest*) request
{
[self triggerFailureBlockWithArgument:request.error];
}
-(void)triggerFailureBlockWithArgument:(NSError*)inArg
{
self.errorBlock(inArg);
}
- (void)requestRedirected:(SDMHttpRequest*) request
{
}
#pragma mark - Service Agent related
-(NSString*)collectionName
{
// Should be overridden by the subclasses
return nil;
}
The referenced code is very convoluted (you're doing things in a very complicated way).
First off, you should not be creating the copy of your blocks in the caller. Make the copy in the callee if necessary (ie: to copy it to heap instead of using the stack-allocated block if you are going to call it after the stack has been popped). In almost all APIs using blocks, it is not the caller's responsibility to ensure that a block is on heap.
There is no reason for your ordersDataProvider variable to be declared in the scope of fetchOrdersListTaskBlock because it is only ever used inside of fetchOrdersOperation's block.
I don't immediately see the cause of your crash, but I suspect that simplifying your code will help reveal the problem. Perhaps the issue is in HLOrdersDataProvider's initializer.
Try something like:
-(void)fetchOrdersListWithInfoDict:(NSDictionary*)infoDict completionBlock:(CompletionBlock)completionBlock errorBlock:(ErrorBlock)errorBlock
{
completionBlock = [completionBlock copy];
errorBlock = [errorBlock copy];
__weak VTVehicleServiceNetworkManager *weakSelf = self;
TaskBlock fetchOrdersListTaskBlock = ^{
NSBlockOperation *fetchOrdersOperation = [NSBlockOperation blockOperationWithBlock:^{
HLOrdersDataProvider *ordersDataProvider = [[HLOrdersDataProvider alloc] init];
[ordersDataProvider performFetchOrdersListWithInfoDict:infoDict
completionBlock:completionBlock
errorBlock:errorBlock];
}];
[weakSelf.dataOperationQueue addOperation:fetchOrdersOperation];
};
[self fetchDataWithTaskBlock:fetchOrdersListTaskBlock
errorBlock:errorBlock];
}
Or better yet, re-design your class to work like this (I don't see a need for NSBlockOperation in your example):
-(void)fetchOrdersListWithInfoDict:(NSDictionary*)infoDict completionBlock:(CompletionBlock)completionBlock errorBlock:(ErrorBlock)errorBlock
{
completionBlock = [completionBlock copy];
errorBlock = [errorBlock copy];
TaskBlock fetchOrdersListTaskBlock = ^{
HLOrdersDataProvider *ordersDataProvider = [[HLOrdersDataProvider alloc] init];
[ordersDataProvider performFetchOrdersListWithInfoDict:infoDict
completionBlock:completionBlock
errorBlock:errorBlock];
};
[self fetchDataWithTaskBlock:fetchOrdersListTaskBlock
errorBlock:errorBlock];
}

Objective-C crash with zombie object

I've made an implementation for an JSON-RPC (a little bit modified) Server/Client in objective-c with the GCDAsyncSocket library. but the app crashes on responding to an request. without debugging for zombies i'm getting this error:
JSONRPCTestServer(1301,0x7fff7f887960) malloc: *** error for object 0x10014db10: pointer being freed was not allocated
*** set a breakpoint in malloc_error_break to debug
the screenshot in xcode shows the error is in the completeCurrentRead method of the GCDAsyncSocket library.
when debugging for zombies it logs this:
2013-02-04 14:36:16.430 JSONRPCTestServer[1367:603] *** -[__NSArrayI release]: message sent to deallocated instance 0x1005b6fd0
and instruments shows this:
as this happens when a response to the rpc-call is of type nsarray i'd guess its this array that is causing the error. the rpc-method is:
-(NSArray *)testArray:(GCDAsyncSocket *)sock {
return [NSArray arrayWithObjects:#"test1",#"test2", nil];
}
The JSON-RPC header is:
#import <Foundation/Foundation.h>
#import "JSONRPCMethod.h"
#import "JSONRPCResponse.h"
#import "JSONRPCError.h"
#import "JSONRPCArgument.h"
#import "JSONRPCRequest.h"
#import "GCDAsyncSocket.h"
#class GCDAsyncSocket;
#protocol JSONRPCResponseDelegate <NSObject>
#optional
-(void)rpcSocket:(GCDAsyncSocket*)sock returnedValue:(id)retVal forMethod:(NSString*)m id:(id)i;
-(void)rpcSocket:(GCDAsyncSocket*)sock returnedError:(JSONRPCError*)err forMethod:(NSString*)m id:(id)i;
-(void)rpcReturnedValue:(id)retVal forMethod:(NSString*)m id:(id)i;
-(void)rpcReturnedError:(JSONRPCError*)err forMethod:(NSString*)m id:(id)i;
#end
#interface JSONRPC : NSObject {
NSMutableArray *supportedMethods;
GCDAsyncSocket *mainSocket;
NSMutableArray *connectedSockets;
NSMutableArray *responseDelegates;
BOOL isServer;
}
+(JSONRPC*)sharedConnection;
-(BOOL)startServer:(NSUInteger)port;
-(BOOL)connectToServer:(NSString*)host port:(NSUInteger)port;
-(BOOL)addMethod:(JSONRPCMethod*)method;
-(void)removeMethod:(JSONRPCMethod*)method;
-(void)removeMethodsWithTarget:(id)target;
-(void)sendRequest:(JSONRPCRequest*)req toSocket:(GCDAsyncSocket*)sock;
-(void)sendRequest:(JSONRPCRequest*)req;
-(void)sendNotification:(JSONRPCRequest*)req toSocket:(GCDAsyncSocket*)sock;
-(void)sendNotification:(JSONRPCRequest*)req;
-(void)sendNotification:(JSONRPCRequest*)req toSocketsWithUserData:(id)userData;
#end
.m is:
#import "JSONRPC.h"
#import "GCDAsyncSocket.h"
#define kGeneralReadTimeout -1.0
#define kGeneralWriteTimeout -1.0
#implementation JSONRPC
- (id)init
{
self = [super init];
if (self) {
isServer = NO;
supportedMethods = [[NSMutableArray alloc] init];
mainSocket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:dispatch_get_main_queue()];
connectedSockets = [[NSMutableArray alloc] init];
responseDelegates = [[NSMutableArray alloc] init];
}
return self;
}
+ (JSONRPC *)sharedConnection {
static JSONRPC *sharedSingleton;
#synchronized(self)
{
if (!sharedSingleton)
sharedSingleton = [[JSONRPC alloc] init];
return sharedSingleton;
}
}
-(BOOL)startServer:(NSUInteger)port {
// Now we tell the socket to accept incoming connections.
// We don't care what port it listens on, so we pass zero for the port number.
// This allows the operating system to automatically assign us an available port.
isServer = YES;
NSError *err = nil;
if ([mainSocket acceptOnPort:port error:&err]) {
} else {
DDLogError(#"Error while starting JSON-RPC Server: %#",err);
return NO;
}
DDLogInfo(#"Started JSON-RPC Server on port %hu",[mainSocket localPort]);
return YES;
}
-(BOOL)connectToServer:(NSString *)host port:(NSUInteger)port {
NSError *err = nil;
mainSocket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:dispatch_get_main_queue()];
[mainSocket connectToHost:host onPort:port error:&err];
if(err != nil) {
DDLogError(#"Couldn't connect to host %#:%lu (Error: %#)",host,port,err);
return NO;
}
return YES;
}
-(BOOL)addMethod:(JSONRPCMethod *)method {
for (JSONRPCMethod *meth in supportedMethods) {
if([meth.name isEqualToString:method.name]) {
return NO;
}
}
[supportedMethods addObject:method];
return YES;
}
-(void)removeMethod:(JSONRPCMethod *)method {
[supportedMethods removeObject:method];
}
-(void)removeMethodsWithTarget:(id)target {
NSMutableArray *toRemove = [[NSMutableArray alloc] init];
for (JSONRPCMethod *meth in supportedMethods) {
if(meth.target == target) {
[toRemove addObject:meth];
}
}
[supportedMethods removeObjectsInArray:toRemove];
}
-(void)sendRequest:(JSONRPCRequest *)req toSocket:(GCDAsyncSocket*)sock {
[responseDelegates addObject:req];
[req setIdentifier:[NSNumber numberWithUnsignedInteger:[responseDelegates count]-1]];
[self sendPackage:[req dictionary] toSocket:sock];
}
-(void)sendRequest:(JSONRPCRequest *)req {
[self sendRequest:req toSocket:mainSocket];
}
-(void)sendNotification:(JSONRPCRequest *)req toSocket:(GCDAsyncSocket*)sock{
[req setIdentifier:nil];
[self sendPackage:[req dictionary] toSocket:sock];
}
-(void)sendNotification:(JSONRPCRequest *)req {
[self sendNotification:req toSocket:mainSocket];
}
-(void)sendNotification:(JSONRPCRequest *)req toSocketsWithUserData:(id)userData {
NSMutableArray *matchingSockets = [[NSMutableArray alloc] init];
for (GCDAsyncSocket*sock in connectedSockets) {
if(sock.userData == userData) {
[matchingSockets addObject:sock];
}
}
if(matchingSockets.count == 0)
return;
[req setIdentifier:nil];
NSData *pkgData = [self writableDataFromDictionary:[req dictionary]];
for (GCDAsyncSocket*sock in matchingSockets) {
[sock writeData:pkgData withTimeout:kGeneralWriteTimeout tag:0];
}
}
#pragma mark Socket Delegate
- (void)socket:(GCDAsyncSocket *)sock didAcceptNewSocket:(GCDAsyncSocket *)newSocket {
[connectedSockets addObject:newSocket];
[newSocket readDataToData:[GCDAsyncSocket ZeroData] withTimeout:kGeneralReadTimeout tag:0];
}
- (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(UInt16)port {
DDLogVerbose(#"socket:didConnectToHost:%# port:%hu", host, port);
[sock readDataToData:[GCDAsyncSocket ZeroData] withTimeout:kGeneralReadTimeout tag:0];
}
- (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)err {
DDLogVerbose(#"socketDidDisconnect:%#", err);
if(isServer)
[connectedSockets removeObject:sock];
}
-(void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag {
// So, we've received something from the client
// As we have to cut out the last 0x00 for JSONSerialization it has to be longer than 1 byte
if(data.length > 1) {
// Shorten out that 0x00
data = [data subdataWithRange:NSMakeRange(0, data.length-1)];
// Try to serialize
NSError *err;
NSDictionary *dict = [NSJSONSerialization JSONObjectWithData:data options:0 error:&err];
DDLogVerbose(#"Dict: %#",dict);
if(err != nil) {
// The package isn't json
JSONRPCResponse *response = [JSONRPCResponse responseWithError:[JSONRPCError invalidRequest]];
[self sendPackage:[response dictionary] toSocket:sock];
} else {
JSONRPCResponse *response = [self handleDictionary:dict fromSocket:sock];
if(response != nil) {
[self sendPackage:[response dictionary] toSocket:sock];
}
}
}
[sock readDataToData:[GCDAsyncSocket ZeroData] withTimeout:kGeneralReadTimeout tag:0];
}
-(JSONRPCResponse*)handleDictionary:(NSDictionary*)dict fromSocket:(GCDAsyncSocket*)sock {
// Check if the "id" is of a correct value/type
id identifier = [dict valueForKey:#"id"];
if(!(identifier == nil || [identifier isKindOfClass:[NSNumber class]])) {
return [JSONRPCResponse responseWithError:[JSONRPCError invalidRequest]];
}
// Handle the package
NSString *methodName = [dict valueForKey:#"method"];
id errorValue = [dict valueForKey:#"error"];
id resultValue = [dict valueForKey:#"result"];
if([methodName isKindOfClass:[NSString class]]) {
// We have a string as method
DDLogInfo(#"Method: %#, object: %#",methodName,[dict valueForKey:#"params"]);
for (JSONRPCMethod *method in supportedMethods) {
if([method.name isEqualToString:methodName]) {
id result = nil;
if(isServer == YES) {
// It is a server and the method needs to know from where the call comes
result = [method invoke:[dict valueForKey:#"params"] fromSocket:sock];
} else {
// It is a client and we don't need to know where the call is from. it can only be the server.
result = [method invoke:[dict valueForKey:#"params"]];
}
if([result isKindOfClass:[JSONRPCError class]]) {
return [JSONRPCResponse responseWithError:result id:identifier];
} else if(result != nil) {
return [JSONRPCResponse responseWithResult:result id:identifier];
} else {
return nil;
}
}
}
} else if(resultValue != nil) {
// We have a response from our partner
//DDLogInfo(#"Result: %#",resultValue);
NSUInteger responseDelegateId = [identifier unsignedIntegerValue];
if(responseDelegateId < [responseDelegates count]) {
JSONRPCRequest *originalRequest = [responseDelegates objectAtIndex:responseDelegateId];
if(originalRequest.sender == nil) {
return nil;
}
#try {
SEL selector;
if(isServer) {
selector = #selector(rpcSocket:returnedValue:forMethod:id:);
} else {
selector = #selector(rpcReturnedValue:forMethod:id:);
}
NSMethodSignature *signature = [originalRequest.sender methodSignatureForSelector:selector];
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
[invocation setTarget:originalRequest.sender];
[invocation setSelector:selector];
NSUInteger startArg = 2;
if(isServer) {
[invocation setArgument:&sock atIndex:startArg];
startArg++;
}
NSString *method = [originalRequest method];
id orgId = [originalRequest identifier];
[invocation setArgument:&resultValue atIndex:startArg];
[invocation setArgument:&method atIndex:startArg+1];
[invocation setArgument:&orgId atIndex:startArg+2];
[invocation invoke];
}
#catch (NSException *exception) {
DDLogWarn(#"Couldn't find a response: %#",exception);
}
}
} else if(errorValue != nil) {
// We have a string as method
DDLogInfo(#"Error: %#",errorValue);
} else {
return [JSONRPCResponse responseWithError:[JSONRPCError invalidRequest] id:identifier];
}
return nil;
}
-(void)sendPackage:(NSDictionary *)dict toSocket:(GCDAsyncSocket *)sock {
NSData *answerData = [self writableDataFromDictionary:dict];
[sock writeData:answerData withTimeout:kGeneralWriteTimeout tag:0];
}
-(NSData*)writableDataFromDictionary:(NSDictionary*)dict {
NSMutableData *answerData = [[NSMutableData alloc] init];
// Serialize the answer
NSError *err = nil;
NSData *jsonData = [NSJSONSerialization dataWithJSONObject:dict options:0 error:&err];
if(err != nil) {
// Log
DDLogError(#"JSON-RPC had an internal error while converting the answer to JSON. The answer-dictionary is: %#",dict);
// Form answer manually
jsonData = [NSData dataWithBytes:"{\"error\":{\"code\":-32700,\"message\":\"Parse error\"}}"
length:49];
}
// Format the answer
[answerData appendData:jsonData];
[answerData appendData:[GCDAsyncSocket ZeroData]];
return answerData;
}
i just don't know how to fix this. why do i keep getting an error with an nsarray but when i return a nsnumber it works? how can i fix this?