如何从服务器提取数据,使其易于访问,并使其持久化而不会降低我的应用程序的界面?

时间:2014-02-11 03:08:10

标签: ios objective-c multithreading grand-central-dispatch

在构建我的应用程序时,Marco Polo(getmarcopolo.com),我发现应用程序中最具挑战性的部分之一是从服务器中提取数据而不会降低界面速度并且不会崩溃。我现在已经解决了这个问题,并希望与任何其他具有相同问题的开发人员分享我的知识。

从服务器提取数据时,需要考虑许多因素:

  1. 数据完整性 - 服务器不会遗漏任何数据

  2. 数据持久性 - 数据被缓存,即使离线也可以访问

  3. 对接口(主线程)的干扰不足 - 使用多线程实现

  4. 速度 - 使用线程并发实现

  5. 缺少线程冲突 - 使用串行线程队列实现

  6. 所以问题是,你如何实现全部5?

    我已经在下面回答了这个问题,但我很乐意听到有关如何改进流程的反馈(使用此示例),因为我觉得现在在一个地方找到它并不容易。

2 个答案:

答案 0 :(得分:4)

我将使用在通知Feed中刷新marco的示例。我还将指的是Apple的GCD库(参见https://developer.apple.com/library/mac/documentation/Performance/Reference/GCD_libdispatch_Ref/Reference/reference.html)。首先,我们创建一个单例(参见http://www.galloway.me.uk/tutorials/singleton-classes/):

@implementation MPOMarcoPoloManager

+ (MPOMarcoPoloManager *)instance {

    static MPOMarcoPoloManager *_instance = nil;

    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
            _instance = [[self alloc] init];
    });

    return _instance;
}

@end

这允许我们随时从任何文件调用[MPOMarcoPoloManager实例],并访问单例中的属性。它还确保始终只有一个marco polos实例。 'static dispatch_once_t onceToken; dispatch_once(& onceToken,^ {'确保线程稳定性。

下一步是添加我们将公开访问的数据结构。在这种情况下,将marcos的NSArray添加到头文件中,以及'instance'的公开声明:

@interface MPOMarcoPoloManager : NSObject

+ (MPOMarcoPoloManager *)instance;

@property (strong, nonatomic) NSArray *marcoPolos;

@end

现在可以公开访问数组和实例,现在是确保数据持久性的时候了。我们将通过添加缓存数据的功能来实现此目的。以下代码将 1.将serverQueue初始化为全局队列,允许多个线程同时运行 2.将localQueue初始化为一个串行队列,该队列一次只允许运行一个线程。应该在此线程上完成所有本地数据操作,以确保没有线程冲突 3.给我们一个方法来调用我们的NSArray缓存,其对象符合NSCoding(参见http://nshipster.com/nscoding/) 4.尝试从缓存中提取数据结构,如果不能

,则初始化一个新数据结构
@interface MPOMarcoPoloManager()

@property dispatch_queue_t serverQueue;
@property dispatch_queue_t localQueue;

@end

@implementation MPOMarcoPoloManager

+ (MPOMarcoPoloManager *)instance {

    static MPOMarcoPoloManager *_instance = nil;

    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
            _instance = [[self alloc] init];
    });

    return _instance;
}

- (id)init {
    self = [super init];

    if (self) {

        _marcoPolos = [NSKeyedUnarchiver unarchiveObjectWithFile:self.marcoPolosArchivePath];

        if(!self.marcoPolos) {
            _marcoPolos = [NSArray array];
        }

        //serial queue
        _localQueue = dispatch_queue_create([[NSBundle mainBundle] bundleIdentifier].UTF8String, NULL);

        //Parallel queue
        _serverQueue = dispatch_queue_create(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), NULL);
    }

    return self;
}

- (NSString *)marcoPolosArchivePath {
    NSArray *cacheDirectories = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);

    NSString *cacheDirectory = [cacheDirectories objectAtIndex:0];

    return [cacheDirectory stringByAppendingFormat:@"marcoPolos.archive"];
}

- (BOOL)saveChanges {
    BOOL success = [NSKeyedArchiver archiveRootObject:self.marcoPolos toFile:[self marcoPolosArchivePath]];
    return success;
}

@end

现在我们已经拥有了单身人士的结构,现在是时候添加刷新我们marco的能力了。将refreshMarcoPolosInBackgroundWithCallback:((^)(NSArray * result,NSError * error))的声明添加到头文件中:

...
- (void)refreshMarcoPolosInBackground:((^)(NSArray *result, NSError *error))callback;
...

现在是实施刷新的时候了。请注意,所有服务器调用都在serverQueue(并行)上执行,任何数据操作都在localQueue(即serial)上完成。当方法完成时,我们使用所谓的C块(参见https://developer.apple.com/library/ios/documentation/cocoa/Conceptual/Blocks/Articles/00_Introduction.html)将结果回调到主线程。任何作用于后台线程的任务都应该对主线程进行回调,以通知接口刷新已完成(无论是否成功)。

...
- (void)refreshMarcoPolosInBackground:((^)(NSArray *result, NSError *error))callback {

//error checking ommitted

    //Run the server call on the global parallel queue
    dispatch_async(_serverQueue, ^{

        NSArray *objects = nil;
        NSError *error = nil;

        //This can be any method with the declaration "- (NSArray *)fetchMarcoPolo:(NSError **)callbackError" that connects to a server and returns objects
        objects = [self fetchMarcoPolo:&error];

        //If something goes wrong, callback the error on the main thread and stop
        if(error) {
            dispatch_async(dispatch_get_main_queue(), ^{
                callback(nil, error);
            });
            return;
        }

        //Since the server call was successful, manipulate the data on the serial queue to ensure no thread collisions
        dispatch_async(_localQueue, ^{

            //Create a mutable copy of our public array to manipulate
            NSMutableArray *mutableMarcoPolos = [NSMutableArray arrayWithArray:_marcoPolos];

            //PFObject is a class from Parse.com
            for(PFObject *parseMarcoPoloObject in objects) {

                BOOL shouldAdd = YES;

                MPOMarcoPolo *marcoPolo = [[MPOMarcoPolo alloc] initWithParseMarcoPolo:parseMarcoPoloObject];
                for(int i = 0; i < _marcoPolos.count; i++) {
                    MPOMarcoPolo *localMP = _marcoPolos[i];
                    if([marcoPolo.objectId isEqualToString:localMP.objectId]) {

                        //Only update the local model if the object pulled from the server was updated more recently than the local object
                        if((localMP.updatedAt && [marcoPolo.updatedAt timeIntervalSinceDate:localMP.updatedAt] > 0)||
                           (!localMP.updatedAt)) {
                            mutableMarcoPolos[i] = marcoPolo;
                        } else {
                            NSLog(@"THERE'S NO NEED TO UPDATE THIS MARCO POLO");
                        }
                        shouldAdd = NO;
                        break;
                    }
                }

                if(shouldAdd) {
                    [mutableMarcoPolos addObject:marcoPolo];
                }
            } 

            //Perform any sorting on mutableMarcoPolos if needed

            //Assign an immutable copy of mutableMarcoPolos to the public data structure
            _marcoPolos = [NSArray arrayWithArray:mutableMarcoPolos];

            dispatch_async(dispatch_get_main_queue(), ^{
                callback(marcoPolos, nil);
            });

        });

    });

}

...

您可能想知道为什么我们会像这样操作队列中的数据,但是我们可以添加一个方法来标记marco。我们不希望等待服务器更新,但我们也不想操纵可能导致线程冲突的庄园中的本地对象。所以我们将这个声明添加到头文件中:

...
- (void)setMarcoPoloAsViewed:(MPOMarcoPolo *)marcoPolo inBackgroundWithlocalCallback:((^)())localCallback
              serverCallback:((^)(NSError *error))serverCallback;
...

现在是实施该方法的时候了。请注意,本地操作是在串行队列上完成的,然后立即回调到主线程,允许接口更新而无需等待服务器连接。然后它更新服务器,并在单独的回调上回调主线程,通知接口服务器保存已完成。

- (void)setMarcoPoloAsViewed:(MPOMarcoPolo *)marcoPolo inBackgroundWithlocalCallback:(MPOOrderedSetCallback)localCallback
              serverCallback:(MPOErrorCallback)serverCallback {

//error checking ommitted

    dispatch_async(_localQueue, ^{

        //error checking ommitted

        //Update local marcoPolo object
        for(MPOMarcoPolo *mp in self.marcoPolos) {
            if([mp.objectId isEqualToString:marcoPolo.objectId]) {

                mp.updatedAt = [NSDate date];
                //MPOMarcoPolo objcts have an array viewedUsers that contains all users that have viewed this marco. I use parse, so I'm going to add a MPOUser object that is created from [PFUser currentUser] but this can be any sort of local model manipulation you need
                [mp.viewedUsers addObject:[[MPOUser alloc] initWithParseUser:[PFUser currentUser]]];

                //callback on the localCallback, so that the interface can update
                dispatch_async(dispatch_get_main_queue(), ^{    
                    //code to be executed on the main thread when background task is finished
                    localCallback(self.marcoPolos, nil);
                });

                break;
            }
        }

    });

    //Update the server on the global parallel queue
    dispatch_async(_serverQueue, ^{

        NSError *error = nil;
        PFObject *marcoPoloParseObject = [marcoPolo parsePointer];
        [marcoPoloParseObject addUniqueObject:[PFUser currentUser] forKey:@"viewedUsers"];

        //Update marcoPolo object on server
        [marcoPoloParseObject save:&error];
        if(!error) {

            //Marco Polo has been marked as viewed on server. Inform the interface
            dispatch_async(dispatch_get_main_queue(), ^{
                serverCallback(nil);
            });

        } else {

            //This is a Parse feature that your server's API may not support. If it does not, just callback the error.
            [marcoPoloParseObject saveEventually];

            NSLog(@"Error: %@", error);
            dispatch_async(dispatch_get_main_queue(), ^{
                serverCallback(error);
            });
        }

    });
}

使用此设置,可以在设置同时查看的marco时刷新背景,同时确保不会同时操作本地模型。虽然只有两种方法,localQueue的必要性可能并不明显,但当有许多不同类型的操作时,它变得至关重要。

答案 1 :(得分:0)

我使用的dataManager包含两个子管理器,核心数据提取管理器和restkit管理器,它们映射到核心数据。

例如:

anywhereInApp.m

[dataManager getData: someSearchPrecate withCompletionBlock: someBlock];

dataManager.m

- (void) getData: somePredicate withCompletionBlock: someblock{
      [self.coreDataManager fetchData: somePredicate withCompletionBlock: some block];
      [self.restkitManager fetchData: somePredicate withCompletionBlock: some block];
}

然后核心数据管理器在一个线程上运行以获取数据并执行完成块。

和reskitmanager运行一个线程,并在http请求和对象映射完成时执行完成块。

通常,完成块会更新集合视图中显示的数据。

只需要担心从核心数据中删除旧数据,但这是另一个故事,可能涉及比较两个不同调用的结果并采取适当的措施。我试着想象一下结果集的维恩图,这一切都很有意义,或者我太累了。喝太多啤酒。