如何正确使用灭火器deleteRowsAtIndexPaths

iOS教程:如何使用NSFetchedResultsController -
iOS教程:如何使用NSFetchedResultsController
不知不觉我们已经来到了Core Data系列教程的最后一部分了,在这里我们要讨论如何使用NSFetchedResultsController来优化我们的应用,提高应用的运行速度,减少其内存占用。
你是不是已经忘记了以前讲过什么呢?我们来复习一下,在第一篇教程中:《iOS教程:Core Data数据持久性存储基础教程》中我们讲了如何为一个iOS程序创建一个Core Data的数据模型和测试的方法,还有我们还把这个数据模型作为数据源连接到了一个表视图上。
在第二篇教程:《iOS教程:如何使用Core Data & 预加载和引入数据》,我们讲了如何解析不同格式的数据文件到一个Core Data通用的SQlite数据库中,还有如何将这个数据库移植到我们的iOS项目上去,好让我们的应用有一些初始数据。
你可以从这里下载第二部分的源码。
为什么要使用 NSFetchedResultsController?
到目前为止,我们就像在使用SQLite3的方法一样,因为本质上Core Data就是在操作SQLite数据库,但是我们写的代码比直接使用SQLite更少,我们使用各种数据库功能也更容易。
但是,我们还有一个很好用的Core Data特性没有用上,这个特性能够很大程度上的提高我们程序的性能,他就是:NSFetchedResultsController。
现在在我们的例子程序中,我们都是一下再将所有的数据全部加载进视图,对于我们的这个应用而言,这也许是可以接受的,但是如果一个应用有大量的数据,载入速度就会变得很慢,也会给用户体验造成影响。
在理想的情况下,我们只载入用户正在浏览的那一部分的数据,幸运的是,苹果官方已经提供了一个这样做的方法,就是NSFetchedResultsController。
所以,咱们先打开 FBCDMasterViewController.h,把之前的failedBankInfos,这个NSArray数组闪电,加入一个NSFetchedResultsController 代替它:
@interface FBCDMasterViewController : UITableViewController
@property (nonatomic,strong) NSManagedObjectContext* managedObjectC
@property (nonatomic, retain) NSFetchedResultsController *fetchedResultsC
在FBCDMasterViewController.m的synthesize部分,删除以前的failedBankInfos synthesize声明,并且加入:
@synthesize fetchedResultsController = _fetchedResultsC
另一个 NSFetchedResultsController 很酷的特性是你可以在ViewDidUnload中将它重新声明为nil。这意味着这个方法有一个自动的内存管理机制,也就是说当内容不在屏幕之中后,其占用内存会自动被清空。要完成这一切,你所需做的就是在ViewDidUnload中将它声明为空。
- (void)viewDidUnload {
self.fetchedResultsController =
好了,现在到了有趣的部分了,我们开始创建取得的数据的控制器。首先我们声明一个属性,让它随着程序的运行检测取得数据是否存在,如果不存在就创造之。
在文件的头部加入以下代码:
- (NSFetchedResultsController *)fetchedResultsController {
if (_fetchedResultsController != nil) {
return _fetchedResultsC
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription
entityForName:@"FailedBankInfo" inManagedObjectContext:managedObjectContext];
[fetchRequest setEntity:entity];
NSSortDescriptor *sort = [[NSSortDescriptor alloc]
initWithKey:@"details.closeDate" ascending:NO];
[fetchRequest setSortDescriptors:[NSArray arrayWithObject:sort]];
[fetchRequest setFetchBatchSize:20];
NSFetchedResultsController *theFetchedResultsController =
[[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest
managedObjectContext:managedObjectContext sectionNameKeyPath:nil
cacheName:@"Root"];
self.fetchedResultsController = theFetchedResultsC
_fetchedResultsController.delegate =
return _fetchedResultsC
这段代码和我们在 viewDidLoad 方法中使用的很是相似,创建一个fetch请求,将FailedBankInfo物体引出等等,但是仍然有一些新的东西我们要讨论。
首先,当我们使用NSFetchedResultsController,,我们必须设置一个数据分类器来赋给fetch请求,数据分类器就是我们告诉Core Data我们希望引入的数据被褥和储存的方法。
这种数据分类器存在的必要就在于它不仅能够编排所有返回的属性和数据,还可以编排与之相关的所有属性和数据,就好像一个天才的人在做这些事情一样。如果我们想要根据FailedBankDtails中的close date这个属性编排数据,但却想要接收FailedBankInfo中的所有数据,Core Data通过这个特性就可以完成这样的事情。
下一个声明十分的重要,就是设置取得的数据的缓冲值的最大值,这正是我们在这个场景中想要使用这种特性的原因,这样的话,fetched方法就会自动取得设置的值的数据项目,之后把当我们向下查看的时候程序就会自动取得各种数据。
当我们设置好这个fetch的缓冲值的时候,我们就完成了创建 NSFetchedRequestController 并且将它传递给了fetch请求,但是这个方法其实还有以下几个参数:
对于managed object 内容,我们值传递内容。
section name key path允许我们按照魔种属性来分组排列数据内容。
文件名的缓存名字应该被用来处理任何重复的任务,比如说设置分组或者排列数据等。
现在我们已经完全创建好了一个取得部分数据的方法,我们下面修改一下以前使用数据加入数据的方法,让它使用我们取得的数据。
- (void)viewDidLoad {
[super viewDidLoad];
if (![[self fetchedResultsController] performFetch:&error]) {
// Update to handle the error appropriately.
NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
self.title = @"Failed Banks";
我们在这里来操作我们的 fetchedResultsController 并且执行performFetch 方法来取得缓冲的第一批数据。
之后,更新numberOfRowsInSection方法
- (NSInteger)tableView:(UITableView *)tableView
numberOfRowsInSection:(NSInteger)section {
sectionInfo =
[[_fetchedResultsController sections] objectAtIndex:section];
return [sectionInfo numberOfObjects];
更新 cellForRowAtIndexPath 方法:
- (void)configureCell:(UITableViewCell *)cell atIndexPath:(NSIndexPath *)indexPath {
FailedBankInfo *info = [_fetchedResultsController objectAtIndexPath:indexPath];
cell.textLabel.text = info.
cell.detailTextLabel.text = [NSString stringWithFormat:@"%@, %@",
info.city, info.state];
- (UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *CellIdentifier = @"Cell";
UITableViewCell *cell =
[tableView dequeueReusableCellWithIdentifier:CellIdentifier];
// Set up the cell...
[self configureCell:cell atIndexPath:indexPath];
现在我们将之前的逻辑分为一些分开的 configureCell 方法,我们待会会用到。
还有最后一件事情,我们需要为 NSFetchedResultsController设置一个代理方法,好消息是都有模版,其实是我从apple官方的一个例子程序中copy过来的,将以下方法加入在文件的底部:
- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller {
// The fetch controller is about to start sending change notifications, so prepare the table view for updates.
[self.tableView beginUpdates];
- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(NSIndexPath *)newIndexPath {
UITableView *tableView = self.tableV
switch(type) {
case NSFetchedResultsChangeInsert:
[tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
case NSFetchedResultsChangeDelete:
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
case NSFetchedResultsChangeUpdate:
[self configureCell:[tableView cellForRowAtIndexPath:indexPath] atIndexPath:indexPath];
case NSFetchedResultsChangeMove:
[tableView deleteRowsAtIndexPaths:[NSArray
arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
[tableView insertRowsAtIndexPaths:[NSArray
arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
- (void)controller:(NSFetchedResultsController *)controller didChangeSection:(id )sectionInfo atIndex:(NSUInteger)sectionIndex forChangeType:(NSFetchedResultsChangeType)type {
switch(type) {
case NSFetchedResultsChangeInsert:
[self.tableView insertSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade];
case NSFetchedResultsChangeDelete:
[self.tableView deleteSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade];
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller {
// The fetch controller has sent all current change notifications, so tell the table view to process all updates.
[self.tableView endUpdates];
现在编译运行你的应用的话,表面上看起来应该都是一样的,但是如果你看看控制台的话,惊人的事情正在发生哦:
SELECT 0, t0.Z_PK FROM ZFAILEDBANKINFO t0 LEFT OUTER JOIN
ZFAILEDBANKDETAILS t1 ON t0.ZDETAILS = t1.Z_PK
ORDER BY t1.ZCLOSEDATE DESC
total fetch execution time: 0.0033s for 234 rows.
SELECT 0, t0.Z_PK, t0.Z_OPT, t0.ZNAME, t0.ZSTATE, t0.ZCITY,
t0.ZDETAILS FROM ZFAILEDBANKINFO t0 LEFT OUTER JOIN
ZFAILEDBANKDETAILS t1 ON t0.ZDETAILS = t1.Z_PK WHERE
t0.Z_PK IN
(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
ORDER BY t1.ZCLOSEDATE DESC LIMIT 20
total fetch execution time: 0.0022s for 20 rows.
SELECT 0, t0.Z_PK, t0.Z_OPT, t0.ZNAME, t0.ZSTATE, t0.ZCITY,
t0.ZDETAILS FROM ZFAILEDBANKINFO t0 LEFT OUTER JOIN
ZFAILEDBANKDETAILS t1 ON t0.ZDETAILS = t1.Z_PK WHERE
t0.Z_PK IN
(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
ORDER BY t1.ZCLOSEDATE DESC LIMIT 20
total fetch execution time: 0.0017s for 20 rows.
你可以看到,在背后, NSFetchedResultsController 正在从 FailedBankInfo中庸之前设置的顺序取得大量的ID,每次只缓冲一定数量的项目,就像我们预料的一样。
如果直接使用SQLite数据库的话,就会有很多工作要做了,我们何不使用Core Data节省时间呢。
之后看些什么?
这是我制作完成的例子程序源码,欢迎下载。
这是原作者的样板程序:project here&(direct download).
欢迎关注我的围脖: @Oratis
在知乎和豆瓣上,我的名字也是Oratis
我会把之后发表的教程分享到这些社交网络中。
如果你有任何问题,欢迎在底下留言,也欢迎写信给我,我的邮箱地址是:&
更多相关文章
这是接着上一次&iOS教程:Core Data数据持久性存储基础教程&的后续教程,程序也会使用上一次制作完成的. 再上一个教程中,我们只做了一个数据模型,之后我们使用这个数据模型中的数据创建了一个表视图,我们还学习了如何测试数据模型的可行性,今天,我们来看看如何在应用启动的时候,将已经存 ...
Xamarin iOS教程之自定义视图 Xamarin iOS自定义视图 工具栏中的视图在实际应用开发中用的很多,但是为了吸引用户的眼球,开发者可以做出一些自定义的视图. [示例2-33]以下将实现一个自定义的视图.当用户触摸屏幕时,就会出现一个显示手指当前位置的标签视图,以及改变主视图的背景颜色. ...
Xamarin iOS教程之警告视图 Xamarin iOS警告视图 如果需要向用户显示一条非常重要的消息时,警告视图(UIAlertView类)就可以派上用场了.它的功能是把需要注意的信息显示给用户.一般显示一条信息,或者是显示一条信息和几个按钮.本节将主要讲解如何为主视图添加警告视图,如何将警告 ...
Xamarin iOS教程之页面控件 Xamarin iOS 页面控件 在iPhone手机的主界面中,经常会看到一排小白点,那就是页面控件,如图2.44所示.它是由小白点和滚动视图组成,可以用来控制翻页.在滚动滚动视图时可通过页面控件中的小白点来观察当前页面的位置,也可通过点击页面控件中的小白点来滚 ...
Xamarin iOS教程之进度条和滚动视图 Xamarin iOS 进度条 进度条可以看到每一项任务现在的状态.例如在下载的应用程序中有进度条,用户可以很方便的看到当前程序下载了多少,还剩下多少.QQ音乐播放器中也使用到了进度条,它可以让用户看到当前音乐播放了多少,还剩多少等.在Xamarin.i ...
从Win7连接Win2003的Sql Server 2008 1.新建链接服务器链接到Wi ...
MongoDB创建数据库完全可以使用use 如下: 这样就创建了一个数 ...
标 题: 想抛开IDE,用神秘的MAKEFILE享受纯粹写代码的乐趣吗?——手把手与你搭建一个MASM开发环境学写Makefile作 者: liuzewei时 间: 1,4链 接: htt ...
这是练习题,没啥难度,留作纪念,记录下来 1 #include &iostream& 2 3 int main() 4 { 5 6 7 f ...
变量的作用域是在定义时决定的,不是在运行时活动对象是在运行时决定的?如果就创建一个对象,使用完毕就完了,就使用json字面量的方式如果对象被反复创建,反复使用,就使用自定义的构造函数方式优先级内部变量高于形式参数形式 ...
友情链接:
管理员邮箱:info@使用insertRowsAtIndexPaths和deleteRowsAtIndexPaths
使用这两个函数,经常会遇到问题,一般情况下,先更新数据源,再插入或者删除
已投稿到:
以上网友发言只代表其个人观点,不代表新浪网的观点或立场。iOS应用崩溃日志分析——绝对有用,绝对实用啊
作为一名应用开发者,你是否有过如下经历?
为确保你的应用正确无误,在将其提交到应用商店之前,你必定进行了大量的测试工作。它在你的设备上也运行得很好,但是,上了应用商店后,还是有用户抱怨会闪退
如果你跟我一样是个完美主义者,你肯定想将应用做到尽善尽美。于是你打开代码准备修复闪退的问题……但是,从何处着手呢?
这时iOS崩溃日志派上用场了。在大多数情况下,你能从中了解到关于闪退的详尽、有用的信息。
通过本教程,你将学习到一些常见的崩溃日志案例,以及如何从开发设备和iTunes Connect上获取崩溃日志文件。你还将学习到符号化(
symbolication),从日志追踪到代码 。你还将学习调试一个在待定情况下会闪退的应用。
让我们开始动手吧!
什么是崩溃日志,从哪里能得它?
iOS设备上的应用闪退时,操作系统会生成一个崩溃报告,也叫崩溃日志,保存在设备上。
崩溃日志上有很多有用的信息,包括应用是什么情况下闪退的。通常,上面有每个正在执行线程的完整堆栈跟踪信息,所以你能从中了解到闪退发生时各线程都在做什么,并分辨出闪退发生在哪个线程上。
有几种方法可以从设备上获取崩溃日志。
设备与电脑上的iTunes
Store同步后,会将崩溃日志保存在电脑上。根据电脑操作系统的不同,崩溃日志将保存在以下位置:
Mac OS X:~/Library/Logs/CrashReporter/MobileDevice/
XP:&C:Documents
and Settings&USERNAME&Application
ComputerLogsCrashReporterMobileDevice&DEVICE_NAME&
Windows Vista or 7:
&C:Users&USERNAME&AppDataRoamingApple
ComputerLogsCrashReporterMobileDevice&DEVICE_NAME&
当用户抱怨闪退时,你可以要求他让设备与iTunes同步,并根据操作系统的不同,到上述位置把崩溃日志下载下来,然后通过电子邮件发送给你。
你必需尽量获取用户设备生成的所有崩溃日志。因为崩溃日志越多,就越容易诊断问题所在!
另外,如果你装了Xcode,也能很容易通过Xcode从你的设备上获得崩溃日志。将iOS设备连接到电脑上,然后打开Xcode。从菜单栏上选择
Window &菜单,
然后选择&Organizer&(快捷方式是
Shift-CMD-2).
在 Organizer 窗口上,
选中&Devices&标签栏.
在左侧的导航面板上,选中&Device
Logs,&如下图所示:
看看上图,左侧有好几个 Device Logs
菜单项。&LIBRARY&下面的Device
Logs是你所有设备(曾经连接到Xcode的)的日志 。每个设备下面的 Device Logs 是对应设备的日志。
应用提交到App Store后,你也能从 iTunes
Connect 获取到用户的崩溃日志. 登录到 iTunes Connect 上,
选择&Manage Your Applications, 点击相应的应用,
点击应用图标下面的&View Details&按钮,
然后点击右栏Links部分的 &Crash
Reports&。
如果没有崩溃日志,试试点击Refresh
按钮刷新一下。如果你的应用还卖得不多,或者刚上架不久,iTunes Connect账号上也可能还没有任何崩溃日志。
如果iTunes
Connect上有崩溃日志,你将看到如下图:
有时,尽管有用户报告闪退,你仍然看不到崩溃报告。这时,最好让用户直接把崩溃报告发送给你。
什么情况下会产生崩溃日志?
两种主要情况会产生崩溃日志:
1.应用违反操作系统规则。
2.应用中有Bug。
违反iOS规则包括在启动、恢复、挂起、退出时watchdog超时、用户强制退出和低内存终止。让我们详细了解一下吧。
4.x开始,退出应用时,应用不会立即终止,而是退到后台。但是,如果你的应用响应不够快,操作系统有可能会终止你的应用,并产生一个崩溃日志。这些事件与下列UIApplicationDelegate方法相对应:
application:didFinishLaunchingWithOptions:
applicationWillResignActive:
applicationDidEnterBackground:
applicationWillEnterForeground:
applicationDidBecomeActive:
applicationWillTerminate:
上面所有这些方法,应用只有有限的时间去完成处理。如果花费时间太长,操作系统将终止应用。
注意:&如果你没有把需要花费时间比较长的操作(如网络访问)放在后台线程上就很容易发生这种情况。关于如果避免这种情况的信息,请参考我们的另外两篇教程:
Grand Central Dispatch 和 NSOperations。
用户强制退出
4.x开始支持多任务。如果应用阻塞界面并停止响应,
用户可以通过在主屏幕上双击Home按钮来终止应用。此时,操作应用将生成一个崩溃日志。
注意:&双击Home按钮后,你将看到运行过的所有应用。那些应用不一定是正在运行,也不一定是被挂起。
通常,用户点击Home按钮时,应用将在后台保留约10分钟,然后操作系统自动将其终止。
所以双击Home按钮显示的应用列表只是表明那是一系列过去打开过的应用。删除那些应用的图标不会产生任何崩溃日志。
低内存终止
子类化UIViewController时,你或许已经注意到didReceiveMemoryWarning方法。
在前台运行的应用拥有访问和使用内存的最高优化级。然而,这并不意味着该应用能使用设备的所有可用内存
——每个应用只能使用一部分可用内存。
当内存使用达到一定程度时,操作系统将发出一个
UIApplicationDidReceiveMemoryWarningNotification 通知。同时,调用
didReceiveMemoryWarning 方法。
此时,为了让应用继续正常运行,操作系统开始终止在后台的其他应用以释放一些内存。所有后台应用被终止后,如果你的应用还需要更多内存,操作系统会将你的应用也终止掉,并产生一个崩溃日志。而在这种情况下被终止的后台应用,不会产生崩溃日志。
注意:&根据 苹果文档,
Xcode不会自动添加低内存日志。你必需手动获取日志。 然而,根据我的个人经验,使用 Xcode 4.5.2,
低内存日志也会自动导入,只是”Process”和”Type”属性都被标为Unknown(未知)。
另外,值得一提的是在极短时间内分配一大块内存将给系统内存带来巨大负担。这样,也会产生内存警告的通知。
应用中有Bug
如你所想,大多数闪退都是由于应用中有Bug,因此大多数崩溃日志的产生都是因为应用中的Bug。Bug的种类的有很多。
在本教程的后半部分,你将通过调试一个会产生崩溃日志的含有Bug的应用,学习如何找到问题所在并进行修复!
崩溃日志的实例
让我们看看一个崩溃日志的实例,以使你在处理一些实际问题之前心里有谱。
事不宜迟,见见你的新朋友吧:
这报告看起来像天书。:)
我们分几部分来解读吧:
(1) 进程信息
第一部分是闪退进程的相关信息。
Identifier是崩溃报告的唯一标识符。
CrashReporter
Key&是与设备标识相对应的唯一键值。虽然它不是真正的设备标识符,但也是一个非常有用的情报:如果你看到100个崩溃日志的CrashReporter
Key值都是相同的,或者只有少数几个不同的CrashReport值,说明这不是一个普遍的问题,只发生在一个或少数几个设备上。
Model&标识设备类型。
如果很多崩溃日志都是来自相同的设备类型,说明应用只在某特定类型的设备上有问题。上面的日志里,崩溃日志产生的设备是iPhone
Process&是应用名称。中括号里面的数字是闪退时应用的进程ID。
接下来几行不言自明,无需赘述。
(2) 基本信息
这部分给出了一些基本信息,包括闪退发生的日期和时间,设备的iOS版本。如果有很多崩溃日志都来自iOS 6.0,说明问题只发生在iOS
在这部分,你可以看到闪退发生时抛出的异常类型。还能看到异常编码和抛出异常的线程。根据崩溃报告类型的不同,在这部分你还能看到一些另外的信息。
(4) 线程回溯
这部分提供应用中所有线程的回溯日志。
回溯是闪退发生时所有活动帧清单。它包含闪退发生时调用函数的清单。看下面这行日志:
它包括四列:
帧编号—— 此处是2。
二进制库的名称 ——此处是 XYZLib.
调用方法的地址 ——此处是
0x34648e88.
第四列分为两个子列,一个基本地址和一个偏移量。此处是0&83000 + 8740,
第一个数字指向文件,第二个数字指向文件中的代码行。
(5) 线程状态
这部分是闪退时寄存器中的值。一般不需要这部分的信息,因为回溯部分的信息已经足够让你找出问题所在。
(6) 二进制映像
这部分列出了闪退时已经加载的二进制文件。
符号化Symbolication
第一次看到崩溃日志上的回溯时,你或许会觉得它没什么意义。我们习惯使用方法名和行数,而非像这样的神秘位置:
将这些十六进制地址转化成方法名称和行数的过程称之为符号化。
从Xcode的Organizer窗口获取崩溃日志后过几秒钟,崩溃日志将被自动符号化。上面那行被符号化后的版本如下 :
Xcode符号化崩溃日志时,需要访问与App
Store上对应的应用二进制文件以及生成二进制文件时产生的 .dSYM
文件。必需完全匹配才行。否则,日志将无法被完全符号化。
所以,保留每个分发给用户的编译版本非常重要。提交应用前进行归档时,Xcode将保存应用的二进制文件。可以在Xcode
Organizer的Archives标签栏下找到所有已归档的应用文件。
在发现崩溃日志时,如果有相匹配的.dSYM文件和应用二进制文件,Xcode会自动对崩溃日志进行符号化。如果你换到别的电脑或创建新的账户,务必将所有二进制文件移动到正确的位置,使Xcode能找到它们。
(注意:&你必需同时保留应用二进制文件和.dSYM文件才能将崩溃日志完整符号化。每次提交到iTunes
Connect的构建都必需归档。
.dSYM文件和二进制文件是特定绑定于每一次构建和后续构建的,即使来自相同的源代码文件,每一次构建也与其他构建不同,不能相互替换。
如果你使用Build 和 Archive
命令,这些文件会自动放在适当位置。 如果不是使用Build 和
Archive命令,放在Spotlight能够搜索到的位置(比如Home目录)即可。)
低内存闪退
因为低内存崩溃日志与普通崩溃日志略有不同,所以本教程特别分开说明一下。
iOS设备检测到低内存时,虚拟内存系统发出通知请求应用释放内存。这些通知发送到所有正在运行的应用和进程,试图收回一些内存。
如果内存使用依然居高不下,系统将会终止后台线程以缓解内存压力。如果可用内存足够,应用将能够继续运行而不会产生崩溃报告。否则,应用将被iOS终止,并产生低内存崩溃报告。
低内存崩溃日志上没有应用线程的堆栈回溯。相反,上面显示的是以内存页数为单位的各进程内存使用量。(在撰写本文的时候,一个内存页的大小是4KB。)
被iOS因释放内存页终止的进程名称后面你会看到jettisoned
字样。如果看到它出现在你的应用名称后面,说明你的应用因使用太多内存而被终止了。
低内存崩溃日志看起来像这样:
当应用发生低内存闪退时,你必需看看应用中内存使用的方式,以及是如何处理低内存警告的。你可以使用Instruments工具中使用Allocations
和 Leaks来发现内存分配问题和内存泄漏问题。如果你不知道如何利用 Instruments 检查内存问题,可以看看这个教程
还有,别忘记虚拟内存!
Instruments工具的Leaks 和 Allocations 不能跟踪显存使用情况。必需使用 VM Tracker
才能查看显存使用情况。
VM Tracker
默认是关闭的。打开Instrument,手动 选中Automatic Snapshotting 标志或者按下Snapshot Now
本教程后面将会学习如何研究低内存崩溃日志。
在研究真实闪退场景之前,还有一点需要重点介绍一下:就是那些有趣的异常编码 。
你可以在报告的异常部分——前面代码的第3部分找到异常编码。有些编码比较常见。
通常,异常编码以一些文字开头,紧接着是一个或多个十六进制值,此数值正是说明闪退根本性质的所在。
&从这些编码中,可以区分出闪退是因为程序错误、非法内存访问或者是其他原因。
下面是一些常见的异常编码:
0x8badf00d: 读做 “ate bad
food”! (把数字换成字母,是不是很像 :p)该编码表示应用是因为发生watchdog超时而被iOS终止的。
&通常是应用花费太多时间而无法启动、终止或响应用系统事件。
0xbad22222:&该编码表示 VoIP 应用因为过于频繁重启而被终止。
0xdead10cc:&读做 “dead
lock”!该代码表明应用因为在后台运行时占用系统资源,如通讯录数据库不释放而被终止 。
0xdeadfa11:&读做 “dead fall”!
该代码表示应用是被用户强制退出的。根据苹果文档, 强制退出发生在用户长按开关按钮直到出现 “滑动来关机”, 然后长按
Home按钮。强制退出将产生 包含0xdeadfa11 异常编码的崩溃日志, 因为大多数是强制退出是因为应用阻塞了界面。
(注意:&在后台任务列表中关闭已挂起的应用不会产生崩溃日志。
一旦应用被挂起,它何时被终止都是合理的。所以不会产生崩溃日志。)
大展身手的时候到了!好了! 你已经学习了所有分析崩溃日志和修复错误的基础知识!
假设你刚进入Rage-O-Rage有限公司工作。该公司有一个在App
Store上热销的应用,叫&Rage Masters。
你的老板安迪要你帮忙解决几个用户经常抱怨闪退问题。你的任务就是研究这些闪退,符号化用户提供的崩溃日志,查找问题所在,并修复之。
你可以从 这里下载应用的源代码。
(注意:&如果你想自己重新生成崩溃报告,请遵照以下指引:
1.下载源码然后在Xcode中打开工程文件。
2.使用正确的provisioning
profile连接到iOS设备。
3.从Xcode工具栏上选择iOS设备——不是模拟器作为target,然后构建应用。
4.当你在设备上到默认页面(应用的全屏图片)时,立即在Xcode上点击停止按钮。
5.关闭 Xcode。
6.在设备上直接打开应用。
7.测试场景,完成后连接设备到电脑上,通过Xcode获取崩溃日志。)
糟糕的代码
一封来自用户的邮件: “大哥,你的应用就是一坨屎!
我将其下载到我自己的iPod Touch和iPhone上,还下载到我儿子的iPod
Touch上。在所有的设备上,都是还没打开就闪退了……”
别一封来自用户的邮件说,
“我下载了你们的应用,一打开就闪退。真悲催…”
另一封邮件说得更明确:”你们的应用不能运行。我把它下载到我和妻子的设备上。所有设备都是 一打开就闪退了…”
好吧,别灰心!
这些意见藏着什么玄机呢?让我们看看崩溃日志吧:
发现问题了吗?
异常编码是0xbadf00d,还有后面的报告:
这说明应用在启动时就闪退了,iOS的watchdog机制终止了应用。帅! 找到问题了,但是为什会发生这样的事呢?
接着往下看日志。 从下向上读回溯日志。最底下的帧
(frame 25: libdyld.dylib)是最先调用的,然后是帧24, Rage Masters, main
(main.m:16) ,依此类推。
跟应用源代码相关的帧是最重要的。忽略掉系统库和框架。下一个与代码相关的帧是:
应用在执行RMAppDelegate
(RMAppDelegate.m:35)类application:didFinishLaunchingWithOptions:
方法第35 行代码时闪退。打开Xcode看看那行代码:
就是它了! 同步调用web服务?! 在主线程上?! 在
application:didFinishLaunchingWithOptions: 方法上?!! 谁写的代码呀?!
Network calls on the main
thread makes kittens sad.
不管如何,问题得你来修复了。这个调用必需异步进行,甚至更理想的情况是,在application:didFinishLaunchingWithOptions:返回YES之后的其他部分再执行Web服务。
在其他地方调用可能需要比较多的修改。当下,我们只要使应用不闪退就行。可以在日后再实现更好的设计。
将上面那行讨厌的代码(及其下面的三行代码)换成下面这个异步的版本吧:
无法响应事件的按钮
一名用户说: “我不能将某个rage
master添加到书签里面。我想添加的时候应用就闪退…”
用一名用户说 :”书签不能用 …
在详细页面上,点击书签按钮,应用就闪退了!”
上面的抱怨说得不是很清楚,引起问题的原因肯定有多样。看看崩溃日志:
异常代码是SIGABRT。通常,
&SIGABRT 异常是由于某个对象接收到未实现的消息引起的。
或者,用简单的话说,在某个对象上调用了不存在的方法。
这种情况一般不会发生,因为A对象调用了B方法,如果B方法不存在,编译器会报错。但是,如果你是使用selector间接调用方法的,编译器则无法检测对象是否存在该方法了。
回到崩溃日志。它指出闪退发生在编号为0的线程上。
这意味着很可能是在主线程上调用了某个对象没有实现的方法。
如果你接着阅读回溯日志,会发现跟你的代码相关的只有帧22, main.m:16.
这没有多大帮助。&
继续向上查看框架调用,出现这个:
这不是你自己写的代码。但至少它确认了是对象调用了一个没有实现的方法。
回到RMDetailViewController.m文件, 因为那是书签按钮实现动作的地方。 找到书签功能代码:
看起来没什么问题,再检查一下storyboard
(XIB文件) ,确认按钮连接的正确性。
就是它了! 在
MainStoryboard.storyboard,按钮连接的是 bookmarkButtonPressed:
而不是bookmarkButtonPressed (注意后面的分号说明方法有一个参数)。
只要将上面的方法签名修改成这样就能修复问题了:
当然,你也可以简单地在XIB文件上删除错误的连接,然后重新连接方法,使XIB文件连接到正确的方法上。两者方法都行。
又处理了一个闪退问题,好样的。
表格上的Bug
另一用户抱怨道, “在书签视图上无法删除书签…”
还有另一用户抱怨同样的问题, “当我试图删除书签时,应用闪退…”
这些邮件没什么作用,还是看看崩溃日志!
这看起来跟前面那个崩溃日志很像。是另一个SIGABRT
异常。 你可能想知道是否是相同的问题:发送信息到一个没有实现相应方法的对象?
让我们从回溯日志看看哪些方法被调用了。从底部开始,你的源代码最后被调用的是帧 6:
这是UITableViewDataSource
的一个方法. 呵呵?! 毫无疑问苹果已经实现了该方法 —— 你可以重载它, 但不像是还没有实现。而且,这是个可选的委派方法。
所以问题不是调用了一个没有实现的方法。
再看看上面的几个帧:
UITableView调用了它自己的另一个方法 deleteRowsAtIndexPaths:withRowAnimation:
然后是看起来像苹果内部方法的_endCellAnimationsWithContext: 被调用。然后Foundation
framework发生异常handleFailureInMethod:object:file:lineNumber:description:.
这些分析结合用户的抱怨,看起来是你在处理UITableView删除行过程中有Bug。回到Xcode。你知道看哪里吗 ?
能从崩溃日志中判断出来? 就是RMBookmarksViewController.m文件的第68行:
发现问题了吗? 给你点时间,仔细看一下。
找到了吧! 数据源呢?
代码在表格视图上删除了一行,但并没有修改背后的数据源。把上面的代码替换成下面的就能修复问题了:
搞定了!走起,讨厌的 bug!!
吃棒棒糖时闪退!
用户邮件说, “当rage
master吃棒棒糖时应用就闪退…” 另一用户说, “我让rage master 吃棒棒糖,没几次应用就闪退了!”
崩溃日志如下:
这日志跟我们前面见到的相差很多。
这个一个来自iOS
6的低内存崩溃日志。正如我们前面所说的,低内存崩溃日志与其他类型的崩溃日志很不一样,它们不指向特定的文件和代码行。相反,它们画出了闪退时设备上的内存使用情况的图表。
至少,头部还是跟其他崩溃日志很像的:
&提供了 Incident Identifier, CrashReporter Key,
Hardware Model, OS Version等信息。
接下来部分是低内存崩溃日志特有的:
pages&指可用内存页数。每页大小约是4KB, 上面的日志中,可用内存约为3,872 KB
(或者说 3.9 MB)。
pages&是那部分可被清除或重用的内存。在上面的日志中,是0KB。
process是闪退时使用大部分内存的应用名称,在上面的日志中,正是你的应用!
Processes显示了闪退时各进程列表,还包含内存使用量。包含进程名 (第一列), 进程唯一标识符(第二名),
进程使用的内存页数(第三列)。最后一列是每个应用的状态。通常,发生闪退的应用的状态是 frontmost。 这里是 Rage
Masters, 使用28591 页 (or 114.364 MB) 内存——这内存太多了!
通过,最大进程和frontmost状态的应用是相同的,
而且也是引起低内存闪退的应用进程。但是也可能看到最大进程和
frontmost状态应用不同的例子。比如,如果最大进程是SpringBoard, 忽略它 , 因为 SpringBoard
进程是显示主屏幕的应用,出现在你双击home按钮等情况,而且它是一直活动的。
低内存发生时,iOS向活动的应用发出低内存警告并终止后台应用。如果前台应用仍然继续增长内存,iOS将终止它。
为了查找低内存问题的原因,你必需使用Instruments剖析应用。如果你不知道怎么做,可以看一下我们 一篇关于这个方面的教程.。
:] 另外, 你也可以走捷径,响应低内存警告通知,以解决部分闪退问题。
回到Xcode查看RMLollipopLicker.m文件。 这是实现吃棒棒糖的视图控制器。看看源代码:
当用户点击运行按钮, 应用开始一个背景线程,调用
lickLollipop 方法若干次,然后更新界面反映吃棒棒糖的数量。 lickLollipop
方法从属性列表文件(PLIST)文件读取一个长字符串,然后添加到数组上。这些数据并不重要,
能在不影响用户体验的前提下重新创建。
利用每种能够清除和重建数据而不影响用户体验的情况是好习惯。这样能够方便地释放内存,减少低内存警告。
那么,如何提高代码质量呢? 实现
didReceiveMemoryWarning 方法,像下面这样处理数据:
万岁,你研究了4个闪退案例!
你的应用更完善了,并且学到了一些重要的调试技巧。
你可以到这里下载改进后的项目代码。
已投稿到:
以上网友发言只代表其个人观点,不代表新浪网的观点或立场。}

我要回帖

更多关于 如何正确使用标点符号 的文章

更多推荐

版权声明:文章内容来源于网络,版权归原作者所有,如有侵权请点击这里与我们联系,我们将及时删除。

点击添加站长微信