- iOS/Objective-C 元类和类别
- objective-c - -1001 错误,当 NSURLSession 通过 httpproxy 和/etc/hosts
- java - 使用网络类获取 url 地址
- ios - 推送通知中不播放声音
在构建我的应用程序 Marco Polo (getmarcopolo.com) 时,我发现该应用程序最具挑战性的部分之一是从服务器提取数据,同时不会降低界面速度并且不会崩溃。我现在已经解决了这个问题,并希望与遇到相同问题的任何其他开发者分享我的知识。
从服务器提取数据时,需要考虑许多因素:
数据完整性 - 服务器永远不会遗漏任何数据
数据持久化——数据被缓存,即使离线也可以访问
不干扰界面(主线程)——使用多线程实现
速度 - 使用线程并发实现
没有线程冲突 - 使用串行线程队列实现
那么问题是,您如何实现所有 5 个目标?
我已经在下面回答了这个问题,但很想听听有关如何改进流程的反馈(通过这个例子),因为我觉得现在在一个地方找到它不是很容易。
最佳答案
我将使用刷新通知提要中的宏的示例。我还会提到 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 实例],并访问单例中的属性。它还确保始终只有一个马可波罗实例。 'static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{'保证线程稳定性。
下一步是添加我们将公开访问的数据结构。在这种情况下,将 marcos 的 NSArray 添加到头文件,以及“实例”的公共(public)声明:
@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
现在我们有了单例的结构,是时候添加刷新我们的宏的能力了。将 refreshMarcoPolosInBackgroundWithCallback:((^)(NSArray *result, NSError *error)) 的声明添加到头文件中:
...
- (void)refreshMarcoPolosInBackground:((^)(NSArray *result, NSError *error))callback;
...
现在是实现刷新的时候了。请注意,所有服务器调用都在 serverQueue(并行)上执行,任何数据操作都在 localQueue(串行)上完成。方法完成后,我们使用所谓的 C block (参见 https://developer.apple.com/library/ios/documentation/cocoa/Conceptual/Blocks/Articles/00_Introduction.html)将结果回调到主线程。任何作用于后台线程的任务都应该有一个回调到主线程来通知接口(interface)刷新已经完成(无论是否成功)。
...
- (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);
});
});
});
}
...
您可能想知道为什么我们要为这样的事情操作队列中的数据,但是让我们添加一个方法,我们可以在其中将宏标记为已查看。我们不想等待服务器更新,但我们也不希望在可能导致线程冲突的庄园中操作本地对象。因此,让我们将此声明添加到头文件中:
...
- (void)setMarcoPoloAsViewed:(MPOMarcoPolo *)marcoPolo inBackgroundWithlocalCallback:((^)())localCallback
serverCallback:((^)(NSError *error))serverCallback;
...
现在是实现该方法的时候了。请注意,本地操作是在串行队列上完成的,然后立即回调到主线程,允许界面更新而无需等待服务器连接。然后它更新服务器,并在一个单独的回调中回调主线程以通知接口(interface)服务器保存已完成。
- (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);
});
}
});
}
使用此设置,可以在后台进行刷新,同时将宏设置为已查看,同时确保不同时操作本地模型。虽然只有两种方法,localQueue 的必要性可能并不明显,但当有许多不同类型的操作可用时,它就变得至关重要。
关于ios - 如何从服务器提取数据、使其易于访问并持久保存而不减慢应用程序的界面速度?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/21692263/
初学者 android 问题。好的,我已经成功写入文件。例如。 //获取文件名 String filename = getResources().getString(R.string.filename
我已经将相同的图像保存到/data/data/mypackage/img/中,现在我想显示这个全屏,我曾尝试使用 ACTION_VIEW 来显示 android 标准程序,但它不是从/data/dat
我正在使用Xcode 9,Swift 4。 我正在尝试使用以下代码从URL在ImageView中显示图像: func getImageFromUrl(sourceUrl: String) -> UII
我的 Ubuntu 安装 genymotion 有问题。主要是我无法调试我的数据库,因为通过 eclipse 中的 DBMS 和 shell 中的 adb 我无法查看/data/文件夹的内容。没有显示
我正在尝试用 PHP 发布一些 JSON 数据。但是出了点问题。 这是我的 html -- {% for x in sets %}
我观察到两种方法的结果不同。为什么是这样?我知道 lm 上发生了什么,但无法弄清楚 tslm 上发生了什么。 > library(forecast) > set.seed(2) > tts lm(t
我不确定为什么会这样!我有一个由 spring data elasticsearch 和 spring data jpa 使用的类,但是当我尝试运行我的应用程序时出现错误。 Error creatin
在 this vega 图表,如果我下载并转换 flare-dependencies.json使用以下 jq 到 csv命令, jq -r '(map(keys) | add | unique) as
我正在提交一个项目,我必须在其中创建一个带有表的 mysql 数据库。一切都在我这边进行,所以我只想检查如何将我所有的压缩文件发送给使用不同计算机的人。基本上,我如何为另一台计算机创建我的数据库文件,
我有一个应用程序可以将文本文件写入内部存储。我想仔细看看我的电脑。 我运行了 Toast.makeText 来显示路径,它说:/数据/数据/我的包 但是当我转到 Android Studio 的 An
我喜欢使用 Genymotion 模拟器以如此出色的速度加载 Android。它有非常好的速度,但仍然有一些不稳定的性能。 如何从 Eclipse 中的文件资源管理器访问 Genymotion 模拟器
我需要更改 Silverlight 中文本框的格式。数据通过 MVVM 绑定(bind)。 例如,有一个 int 属性,我将 1 添加到 setter 中的值并调用 OnPropertyChanged
我想向 Youtube Data API 提出请求,但我不需要访问任何用户信息。我只想浏览公共(public)视频并根据搜索词显示视频。 我可以在未经授权的情况下这样做吗? 最佳答案 YouTube
我已经设置了一个 Twilio 应用程序,我想向人们发送更新,但我不想回复单个文本。我只是想让他们在有问题时打电话。我一切正常,但我想在发送文本时显示传入文本,以确保我不会错过任何问题。我正在使用 p
我有一个带有表单的网站(目前它是纯 HTML,但我们正在切换到 JQuery)。流程是这样的: 接受用户的输入 --- 5 个整数 通过 REST 调用网络服务 在服务器端运行一些计算...并生成一个
假设我们有一个名为 configuration.js 的文件,当我们查看内部时,我们会看到: 'use strict'; var profile = { "project": "%Projec
这部分是对 Previous Question 的扩展我的: 我现在可以从我的 CI Controller 成功返回 JSON 数据,它返回: {"results":[{"id":"1","Sourc
有什么有效的方法可以删除 ios 中 CBL 的所有文档存储?我对此有疑问,或者,如果有人知道如何从本质上使该应用程序像刚刚安装一样,那也会非常有帮助。我们正在努力确保我们的注销实际上将应用程序设置为
我有一个 Rails 应用程序,它与其他 Rails 应用程序通信以进行数据插入。我使用 jQuery $.post 方法进行数据插入。对于插入,我的其他 Rails 应用程序显示 200 OK。但在
我正在为服务于发布请求的 API 调用运行单元测试。我正在传递请求正文,并且必须将响应作为帐户数据返回。但我只收到断言错误 注意:数据是从 Azure 中获取的 spec.js const accou
我是一名优秀的程序员,十分优秀!