IOS基础知识:调试修复BUG

原创:知识点总结性文章
创作不易,请珍惜,之后会持续更新,不断完善
个人比较喜欢做笔记和写总结,毕竟好记性不如烂笔头哈哈,这些文章记录了我的IOS成长历程,希望能与大家一起进步
温馨提示:由于不支持目录跳转,大家可通过command + F 输入目录标题后迅速寻找到你所需要的内容

目录

  • 一、崩溃名场面
    • 1、野指针访问
    • 2、查找不到指定的方法
    • 3、集合类
    • 4、KVO与KVC
    • 5、UITableView或者UICollectionView
    • 6、清除过往编译
  • 二、Crash Log
    • 1、Crash Log 崩溃日志收集方式
    • 2、应用Crash日志获取的整个流程
    • 3、使用苹果提供的 Crash 崩溃收集服务
    • 4、通过Xcode查看设备崩溃信息
    • 5、通过友盟、百度、腾讯Bugly等第三方崩溃统计工具
    • 6、自己实现应用内崩溃收集,并上传服务器
  • 三、断点
    • 1、文件行断点
    • 2、异常断点
    • 3、符号断点
    • 4、其他断点
  • 四、导航面板
    • 1、调试工具
    • 2、输出窗口
    • 3、变量查看窗口
    • 4、查看线程
  • 五、断点调试命令
    • 1、断言
    • 2、NSLog
    • 3、捕获异常
    • 4、常用的标识符
  • 六、LLDB
    • 1、LLDB的简介
    • 2、常用命令
    • 3、帮助文档
    • 4、设置断点
    • 5、观察点命令
    • 6、线程堆栈信息
    • 7、直接返回值
    • 8、查看变量
    • 9、计算表达式
    • 10、打印变量
  • 七、单元测试
    • 1、测试导航面板
    • 2、书写单元测试类
  • 八、UI测试
  • 九、真机调试
    • 1、安装步骤
    • 2、问题
  • 十、工程配置
    • 1、PCH头文件
    • 2、国际化操作(本地化)
  • Demo
  • 参考文献

一、崩溃名场面

1、野指针访问

EXC_BAD_ACCESS
具体场景
  1. 定义property该用strong/weak修饰误用成assign
  2. objc_setAssociatedObject方法中该用OBJC_ASSOCIATION_RETAIN_NONATOMIC修饰的对象误用成OBJC_ASSOCIATION_ASSIGN
  3. NSNotification/KVOaddObserver并没有removeObserver
  4. block回调之前并没有判空而是直接调用
解决方案
  1. 深刻了解各种关键字修饰内存语义的区别,正确运用,例如delegate属性一般都用weak修饰
  2. debug阶段启动僵尸对象模式,enbale Zombie Objects帮助辅助定位问题。
  3. 对于NSNotificatio/KVO addObserverremoveObserver一定要成对出现,推荐使用FaceBook开源的第三方库FBKVOController
  4. block回调之前先做判空

2、查找不到指定的方法

unrecognized selector sent to instance
  • 头文件已经声明方法,但是在.m文件中没有实现或者把方法名修改,却没有在头文件中同步
  • 调用代理类的方法的时候没有判断代理类是否已经实现对应的方法而直接调用,编译可以通过但是运行时会crash,应该先用 respondsToSelector 方法先判断一下,然后再进行调用
  • 对于id类型的对象没有判断类型直接强转调用方法
  • @property (nonatomic, copy) NSMutableArray *mutableArray;,用copy修饰的可变属性在赋值之后会变成不可变属性,比如这里调用addObject方法之后就会crash

3、集合类

a、读取数组内容
问题:数组越界
NSArray *array = @[@"a",@"b",@"c"];
id letter = [array objectAtIndex:3];
Terminating app due to uncaught exception 'NSRangeException', reason: '*** -[__NSArrayI objectAtIndex:]: index 8 beyond bounds [0 .. 7]
解决方案:从数组中的某个下标取元素的时候先判断下标与数组长度的关系
if (index < [[self currentUsers] count]) {
    UserModel * model = [[self currentUsers] objectAtIndex:index];
    return model;
}
解决方案:对 NSMutableArray 以及 NSMutableDictionary 自定义一些安全的扩展方法
-(id)objectAtIndexSafely:(NSUInteger)index {
    if (index >= self.count) {
        return nil;
    }
    return [self objectAtIndex:index];
}

-(void)setObjectSafely:(id)anObject forKey:(id )aKey {
    if (!aKey) {
        return;
    }
    if (!anObject){
        return;
    }
    [self setObject:anObject forKey:aKey];
}

b、修改数组内容
问题:向数组中插入空对象
NSMutableArray *mutableArrray = [NSMutableArray array];
[mutableArray addObject:nil];
failed: caught "NSInvalidArgumentException", " * -[__NSPlaceholderDictionary initWithObjects:forKeys:count:]: attempt to insert nil object from objects[1]
解决方案:遍历时需要修改原数组的时候可以遍历原数组的一个拷贝
NSMutableArray *copyArray = [NSMutableArray arrayWithArray:self.items];
 for(id item in copyArray) {
     if (item != self.currentItem) {
         [self.items removeGuideViewItem:item];
     }
}

c、setObject:ForKey 方法
问题:调用可变字典 setObject:ForKey 方法, key 或者 value 为空,特别注意字面量写法
@{@"itemID":article.itemID}//这里itemID可能为空
failed: caught "NSInvalidArgumentException", " * setObjectForKey: object cannot be nil (key: no_nillKey)

failed: caught "NSInvalidArgumentException", " * setObjectForKey: key cannot be nil"
解决方案:调用NSMutableDictionary的setValue:ForKey:方法而不是setObject:ForKey:方法,少用字面量语法。NSDictionary内部对value做了处理,[mutableDictionary setValue:nil ForKey:@"name"]不会崩溃

d、线程读写
问题:一边遍历数组,一边修改数组内容。或者多线程环境中,一个线程在读另外一个线程在写
for(id item in self.itemArray) {
 if (item != self.currentItem) {
     [self.itemArray removeItem:item];
   }
}
Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '*** -[__NSArrayM insertObject:atIndex:]: object cannot be nil'
解决方案:保证多线程中读写操作的原子性——加锁,信号量,GCD串行队列,GCD dispatch_barrier_async方法等
_cache = [[NSMutableDictionary alloc] init];
_queue = dispatch_queue_create("com.mutablearray.safety", DISPATCH_QUEUE_CONCURRENT);
-(id)cacheObjectForKey: (id)key {
    __block obj;
    dispatch_sync(_queue, ^{
        obj = [_cache objectForKey: key];
    });
    return obj;
}
-(void)setCacheObject: (id)obj forKey: (id)key {
    dispatch_barrier_async(_queue, ^{
        [_cache setObject: obj forKey: key];
    });
}

4、KVO与KVC

a、多次移除了观察者
Terminating app due to uncaught exception 'NSRangeException', reason: 'Cannot remove an observer  for the key path "frame" from  because it is not registered as an observer.'

当对同一个keypath进行多次removeObserver时会导致程序crash,这种情况常常出现在父类有一个KVO,父类在deallocremove了一次,子类又remove了一次。所以我们需要确保addObserverremoveObserver一定要成对出现,推荐使用FaceBook开源的第三方库FBKVOController。也可以分别在父类以及本类中定义各自的context字符串,比如在本类中定义context@"ThisIsMyKVOContextNotSuper";,然后在deallocremove observer时指定移除的自身添加的observer。这样iOS就能知道移除的是自己的KVO,而不是父类中的KVO,避免二次remove造成crash


b、setValue 方法
value为nil
[people1 setValue:nil forKey:@"age"]
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '[ setNilValueForKey]: could not set nil as the value for the key age.' // 调用setNilValueForKey抛出异常
找不到对应的key
[viewController setValue:@"crash" forKey:@"undefined"];
*** Terminating app due to uncaught exception 'NSUnknownKeyException', reason: '[ setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key undefined.'//在类中找不到对应的key

5、UITableView或者UICollectionView

*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'UITableView (; layer = ; contentOffset: {0, 0}; contentSize: {375, 2946}>) failed to obtain a cell from its dataSource ()'
问题:UITableView的 cellForRowAtIndexPath方法或者UICollectionView的cellForItemAtIndexPath方法因为异常返回了nil
原因
  • numberOfRowsInSection返回的数目不正确,导致行数比cellForRoAtIndexPath预期的多,于是cellForRowAtIndexPath方法就不能正确返回超出预期的cell
  • cellForRowAtIndexPath中逻辑有误,漏了一些情况,导致有些cell不能正确返回

6、清除过往编译

为节约开发者点击运行按钮后的编译时间,Xcode 采用了分批次编译的技术,即每次编译完成后会将编译结果保存起来,开发者下次编译运行时只编译两次之间开发者新增或修改的内容,以此来大幅节省时间。若你偶尔发现编译无法顺利进行,或编译时间比预期缓慢太多,可能是此分批编译过程出了小问题。点击 Xcode 上方「Product - Clean Build Folder 」按钮来清除过往编译,之后再重新运行往往便可解决。


二、Crash Log

1、Crash Log 崩溃日志收集方式

需要收集崩溃日志的原因

开发好的APP上传到iTunes connect上后,用testflight安装测试时,在登录页闪退。因为在开发证书打的生产包上是正常的,没法定位bug,只好根据崩溃日志定位。

收集崩溃日志的方式
  1. 使用苹果提供的Crash崩溃收集服务(麻烦较少用)
  2. Xcode-Devices中直接查看某个设备的崩溃信息
  3. 通过友盟、百度、腾讯Bugly等第三方崩溃统计工具
  4. 自己实现应用内崩溃收集,并上传服务器

2、应用Crash日志获取的整个流程

应用的整个流程
a、编译应用程序

编译器将源代码编译成机器代码的过程中,会生成调试符号,这些调试符号将生成的二机制文件中的每一条机器指令映射回源代码行。根据调试信息格式的构建设置(setting builds--Debug Information Format),这些调试符号会存储在二进制文件中或者附随的调试文件中(.dSYM)。在默认的情况下,应用的调试版本,会将调试符号保存在已经编译的二进制文件中;而在发布版本中,会单独生成附随的调试符号表(.dSYM)以减小二进制文件的大小。应用在每次构建时,都会生成一个用于标记该次构建过程的唯一的uuid,调试符号和应用程序二进制文件通过UUID进行绑定。某次构建过程中生成的调试符号,即使同样的源代码,也不能操作非本次构建生成的应用二进制文件。


b、打包应用程序

当打包应用程序进行分发时,Xcode将收集应用程序二进制文件以及.dSYM文件,并将它们存储在主文件夹内的某个位置。 可以在Xcode Organizer“Archived”部分下找到所有已存档的应用程序。 如果想要符号化这些异常日志,就必须保存每次发布应用时构建的打包文件(.xcarchive文件)


c、上传应用程序

如果要通过App Store分发应用程序,或使用Test Flight进行Beta测试,在将应用上传到iTunes Connect时,可以选择是否包括dSYM文件。 在提交对话框中,选中“Include app symbols for your application…”。 上传dSYM文件对于接收从TestFlight用户和选择共享诊断数据的客户收集的崩溃报告是必要的。需要注意的是,即使你上传了.dysm文件从App Review获取到的异常日志也是未符号化的,需要使用Xcode来进行符号化。


d、产生异常日志

当应用在设备上发生异常时,系统会产生异常日志并存储在手机设备上。用户可以按照调试已部署的iOS应用中的步骤直接从其设备检索崩溃报告。 如果已经通过AdHocEnterprise方式分发了应用程序,则这是从用户那里获取崩溃报告的唯一方法。


e、符号化的崩溃报告

从移动设备中检索到的崩溃报告没有符号化,需要使用Xcode进行符号化。Xcode使用与应用程序二进制文件关联的dSYM文件将回溯中的每个地址替换为其源代码中的原始位置,得到的结果是一个符号化的崩溃报告。

如果用户选择与Apple共享诊断数据,或者用户已通过TestFlight安装了应用程序的Beta版本,则崩溃报告将上传到App Store,可以在Xcode中下载异常日志。

App Store象征着崩溃报告,并将其与类似的崩溃报告进行分组。 这种相似的崩溃报告的汇总称为崩溃点。带符号的崩溃报告可在Xcode的崩溃管理器中进行查看使用。


3、使用苹果提供的 Crash 崩溃收集服务

Xcode 崩溃日志符号化必备三样东西:Crash Log 崩溃日志、dSYM符号集、symbolicatecrash 工具。

a、Crash Log 崩溃日志

在手机上的 设置-->隐私-->分析-->分析数据中可以查看到异常。不过这里的日志是没有符号化的。选择所需的日志,复制文本或点击右上角的分享按钮分享出去,将文件分享到能够符号化的设备如XCode上进行符号化处理,并且把分享得到的.ips.synced或者复制文本而来的.txt文件的后缀名改为.crash,因为Xcode不接受没有.crash扩展名的崩溃日志。


b、dSYM 符号集

符号集是我们对ipa文件进行打包之后,和.app文件同级的后缀名为.dSYM的文件,这个文件必须使用Xcode进行打包才有。每一个.dSYM文件都有一个UUID,和.app文件中的UUID对应,代表着是一个应用。而.dSYM文件中每一条崩溃信息也有一个单独的UUID,用来和程序的UUID进行校对。

我们如果不使用.dSYM文件获取到的崩溃信息都是不准确的。符号集中存储着文件名、方法名、行号的信息,是和可执行文件的16进制函数地址对应的,通过分析崩溃的.Crash文件可以准确知道具体的崩溃信息,包括文件名、方法名、行号等。

我们每次Archive一个包之后,都会随之生成一个dSYM文件。每次发布一个版本,我们都需要备份这个文件,以方便以后的调试。进行崩溃信息符号化的时候,必须使用当前应用打包的电脑所生成的dSYM文件,其他电脑生成的文件可能会导致分析不准确的问题。


c、symbolicatecrash 工具

当程序崩溃的时候,我们可以获得到崩溃的错误堆栈,但是这个错误堆栈都是0x开头的16进制地址,需要我们使用Xcode自带的symbolicatecrash工具来将.Crash.dSYM文件进行符号化,就可以得到详细崩溃的信息,即将0x开头的地址替换为响应的代码和具体行数。查找symbolicatecrash(终端)。

find /Applications/Xcode.app -name symbolicatecrash -type f
如何符号化?

在桌面上建立一个Crash文件夹,将.crash.dSYMsymbolicatecrash文件都放入文件夹中。在终端下进入该文件夹,使用命令解析Crash文件,*号指的是具体的文件名需要我们替换。

./symbolicatecrash ./*.crash ./*.app.dSYM > symbol.crash

如果上面命令不成功,使用以下命令检查一下环境变量。

xcode-select -print-path

返回结果

/Applications/Xcode.app/Contents/Developer/

如果不是上面的结果,需要使用下面命令设置一下导出的环境变量,然后重复上面解析的操作。

export DEVELOPER_DIR=/Applications/XCode.app/Contents/Developer

解析完成后会生成一个新的.Crash文件,这个文件中就是崩溃详细信息。图中红色标注的部分就是我们代码崩溃的部分。

...
Application Specific Information:
abort() called
 
Last Exception Backtrace:
(0x1c1e11c30 0x1c1b2c0c8 0x1c1e6a5f4 0x1c1e74414 0x1c1cf85f0 0x1c1ce9e9c 0x100dbbce0 0x1c5e9dd80 0x1c5e9fb90 0x1c5ea5568 0x1c564d710 0x1c5af97e8 0x1c564e248 0x1c564dc78 0x1c564e064 0x1c564d8e8 0x1c5652098 0x1c5b13214 0x1c5a26e90 0x1c5b131cc 0x1c5651db0 0x1c5b130b4 0x1c5651c0c 0x1c54bd630 0x1c54bc0f4 0x1c54bd360 0x1c5ea391c 0x1c5a48d7c 0x1c6f7b014 0x1c6fa1bd0 0x1c6f860f8 0x1c6fa1864 0x1c1ab900c 0x1c1abbd50 0x1c6fc8384 0x1c6fc8030 0x1c6fc859c 0x1c1d8d260 0x1c1d8d1b4 0x1c1d8c920 0x1c1d877ec 0x1c1d87098 0x1cbef1534 0x1c5ea77ac 0x100dbbb68 0x1c1c06f30)
 
Thread 0 name:  Dispatch queue: com.apple.main-thread
...

4、通过Xcode查看设备崩溃信息

除了上面的系统分析工具来进行分析,如果是打包电脑直接连接崩溃后的手机,则可以选择window-> devices -> 选择自己的手机 -> view device logs或者通过~/Library/Logs/CrashReporter/MobileDevice/就可以查看我们的崩溃信息了。只要手机上的应用是这台电脑安装打包的,这样的崩溃信息系统已经为我们符号化好了。

可以选择查看所有的日常日志或者该设备上的异常日志。这里获取到的异常日志是经过Xcode符号化的,所以可以清楚看到异常调用的堆栈信息。

经过Xcode符号化的异常日志

如果应用已经通过app store进行发布,也可以在打包的设备上通过Xcode进行查看。打开Xcode在,打开window --> Organizer,选中对应的应用,即可查看不同版本中的异常。当然需要App的用户的“隐私”‘‘分析’’‘‘共享iPhone分析’’中的‘‘与应用开发者共享’’打开才行。

查看不同版本中的异常

5、通过友盟、百度、腾讯Bugly等第三方崩溃统计工具

❶ 以友盟为例,使用 cocoapods 引入需要的类库
pod 'UMCCommon'
pod 'UMCSecurityPlugins'
pod 'UMCAnalytics'
pod 'UMCCommonLog'
❷ 在应用启动时进行初始化
#if DEBUG || ISDEV
    [UMConfigure setLogEnabled:true];
    NSString *bundleIdentifier = [[NSBundle mainBundle] bundleIdentifier];
    [UMConfigure initWithAppkey:@"MOBILE_CLICK_KEY_DEVELOP" channel:bundleIdentifier];
#else
    //关闭日志打印
    [UMConfigure setLogEnabled:false];
    //使用加密方式上传日志
    [UMConfigure setEncryptEnabled:true];
    [UMConfigure initWithAppkey:MOBILE_CLICK_KEY_PRODUCTION channel:@"IOS APP"];
#endif
❸ 在发布版本时,保存并向友盟后台上传符号表文件
❹ 每天抽时间去后台关注一下应用发生了哪些异常,及时定位异常并修复

6、自己实现应用内崩溃收集,并上传服务器

a、需要自己定义捕获并收集异常

如果apple自带的异常收集和第三方都不能满足产品的需求,就需要自己定义捕获并收集异常。iOS的异常捕获主要是通过苹果给我们提供的异常处理类NSException来实现。

// 定义异常捕获函数原型
typedef void NSUncaughtExceptionHandler(NSException *exception);
 
// 注册捕获函数
FOUNDATION_EXPORT void NSSetUncaughtExceptionHandler(NSUncaughtExceptionHandler * _Nullable);
b、NSException类的内部声明
@interface NSException : NSObject  {
    @private
    NSString        *name;
    NSString        *reason;
    NSDictionary    *userInfo;
    id          reserved;
}
 
+ (NSException *)exceptionWithName:(NSExceptionName)name reason:(nullable NSString *)reason userInfo:(nullable NSDictionary *)userInfo;
- (instancetype)initWithName:(NSExceptionName)aName reason:(nullable NSString *)aReason userInfo:(nullable NSDictionary *)aUserInfo NS_DESIGNATED_INITIALIZER;
 
// 是发生异常的名称,例如数据访问越界NSRangeException,参数不合法NSInvalidArgumentException等
@property (readonly, copy) NSExceptionName name;
// 异常的原因,是对name的具体解释信息,例如当试图在字典中插入nil时
@property (nullable, readonly, copy) NSString *reason;
// 用户信息,一般用于自定义异常时的传递信息
@property (nullable, readonly, copy) NSDictionary *userInfo;
 
// 产生异常的调用堆栈,调用的顺序自下往上
@property (readonly, copy) NSArray *callStackReturnAddresses API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
@property (readonly, copy) NSArray *callStackSymbols API_AVAILABLE(macos(10.6), ios(4.0), watchos(2.0), tvos(9.0));
 
// 用于抛出异常,中断执行
- (void)raise;
 
@end
c、功能实现
@interface ExceptionHandler : NSObject
+ (void)registerExceptionHandler;
@end
 
@implementation ExceptionHandler
+ (void)registerExceptionHandler {
    static NSUncaughtExceptionHandler *originalExceptionHandler;
    originalExceptionHandler = NSGetUncaughtExceptionHandler();
    void(^dealException)(NSException *) = ^(NSException *exception) {
        if (originalExceptionHandler) {
            originalExceptionHandler(exception);
        }
        NSLog(@"name:%@", exception.name);
        NSLog(@"reason:%@", exception.reason);
        NSLog(@"userInfo:%@", exception.userInfo);
        NSLog(@"callStackSymbols:%@", exception.callStackSymbols);
        //保存信息到本地,在需要的时候发送信息到服务器(实际操作中还需要保存对应的设备类型以确定所使用的指令集等信息)
        
    };
    IMP uncaughExceptionHandler =  imp_implementationWithBlock(dealException);
    NSSetUncaughtExceptionHandler((void(*)(NSException *))uncaughExceptionHandler);
}
 
@end
d、细节问题

系统提供了捕获系统异常的方法,但是只有这一个方法,如果应用中有多个捕获异常的实现,那么先注册的异常捕获方法就会被后来注册的方法覆盖掉,造成之前的注册的异常捕获方法不能执行。


三、断点

1、文件行断点

执行到特定文件某一行时触发,设置文件行断点很简单,直接点击该文件中的行号即可。

  • 断点可以删除、禁止使用和编辑,拖曳断点离开行号列也可以删除断点
  • Edit Breakpoint菜单项、会弹出断点编辑对话框,为断点设定触发条件和忽略次数,并添加动作
  • 默认情况下,循环体10次都触发断点,Condition中设置i==8,只是想看看i==8是什么情况,当程序运行到i==8时就会挂起
  • 可以在Ignore中设置忽略次数,比如设置Ignore为8从而达到同样的效果
  • Action输入框中输入p b, 该命令是在调试窗口输出b变量, p是调试命令,和控制台中输入LLDB调试命令一致
  • Action下面还有 一 个 Options,选中它,可以在动作执行后,程序不再挂起,自动继续执行

2、异常断点

+按钮选择ExceptionBreakpoint

Exception项中可以选择抛出异常对象类型:AllObjective-CC++异常断点。Break项可以设定On Throw还是On Catch,即断点是在抛出时触发还是在捕获时触发。对于Swift版, 抛出异常后, 程序会直接跳到AppDelegate.swift; 对于Objective-C版,抛出异常后,程序会直接跳到main.m中的main函数里面,而异常信息会在输出窗口中输出。


3、符号断点

调用某一个函数或方法时触发,程序挂起在函数或方法的第一行。+按钮选择Symbolic Breakpoint菜单项。

此时可以弹出创建符号断点对话框,指定要中断执行的方法,比如在 Symbol中输入findAll方法,当调用时就会触发断点,挂起在findAll方法的第一行。


4、其他断点

  • Swift错误断点:产生 Swift错误时触发 。
  • OpenGL ES断点:产生OpenGLES异常时触发。
  • 单元测试失败断点:当进行单元测试时,测试失败的情况下断点会停留在测试失败的代码行

四、导航面板

程序错误分为编译错误和逻辑错误 :前者是在程序编译时暴露出来的错误,可以通过Xcode定位 ,编译器还会给出错误原因提示;后者是指程序运行的结果与我们期望的不一致, 这些错误可以通过调试和测试找出 。

在日志导航面板中, 左侧显示每次的操作,右边是对应操作的日志。正常消息是绿色圆形,问题消息包括了警告和错误。点击每一行结尾的显示列表图标=,可列出该项目的详细信息。


1、调试工具

a、调试窗口
  • 视图调试按钮可以显示视图层次结构
  • 模拟位置按钮可以向模拟器设备发送虚拟的位置坐标,它用于位置服务应用中的测试
  • 使用跳转栏, 可以跳转到具体工程下某个类的方法中,能够跟踪程序的运行过程

b、跳转栏
  • 在断点挂起之后 ,点击继续执行按钮可以继续执行
  • 单步跳过按钮是单步执行,遇到方法和函数时不进入
  • 单步进入按钮,则进入方法或者函数里
  • 单步跳出按钮在从方法或函数里面跳回到原来调用它的地方时使用

2、输出窗口

输出窗口有 3个选择 All OutputDebugger OutputTarget Output。调试程序时,可以在 Debugger Output窗口中执行编译器的调试命令。Target Output窗口中可以显示程序出错和异常等信息,函数(如 NSLogassert函数)输出的信息。


3、变量查看窗口

  • Auto:查看经常使用的变量
  • Local Variables: 查看本地变量
  • All Variables查看全部变量,包括寄存器和全局变量
  • A-自动变量
  • S-静态变量
  • R-寄存器
  • L-本地变量

4、查看线程

在跳转栏中选择线程下拉列表,选择某个线程后, Xcode会显示一个代码运行的栈 。选择栈中的方法,此时编辑窗口会进入该方法,如果该方法没有源代码,将显示汇编语言。另一种查看线程的方法是在导航调试面板中查看,该面板只显示大概的调用栈,没有上一种查看方式反应的情况详细。

初学者往往毫无头绪,不知道如何跟踪异常栈,以及如何分析异常栈报告。异常栈是程序抛出异常之前,对象之间方法(或函数)调用的”路径",它是程序运行的“黑匣子"。异常栈的输出内容包括很多信息,如调用顺序、方法所属框架(或库)、方法所属类、方法或函数地址等。栈信息是要从下往上看的。栈信息可能很长,我们不需要每一行都去看 ,只需关注自己的工程代码。假定别人提供给我们的框架(或库)是正确的,先看自己工程中的方法(或函数),找到那条调用语句看看。


五、断点调试命令

1、断言

程序调试执行过程中设置的一些条件,当条件满足时,正常执行。当条件不满足时,终止程序,抛出错误栈信息。NSLog函数是无条件输出,即程序运行到该语句,就会输出结果。 如果想有条件输出结果,在Swift中可以使用断言函数assertassertionFailure。在 Objective-C中,断言函数被定义为 NSAssert宏。

assert(i>=o&&i<9,"i变量超出了范围.”)
NSAssert(>i:o&&<9,@"i:%i变量超出了范固.",i);

2、NSLog

在开发过程中,在调试过程中经常打印不出自己想要的数据格式,还时常报警告,所以我整理了一下iOS中用NSLog打印各种数据类型的样式。

整型占位符
%d //十进制整数, 正数无符号, 负数有 “-” 符号
%o //八进制无符号整数, 没有 0 前缀
%x //十六进制无符号整数, 没有 0x 前缀
%u //十进制无符号整数
%hd //短整型
%ld , %lld //长整型

%zd //有符号 NSInteger型专用输出
%tu //无符号NSUInteger的输出
%lu //sizeof(i)内存中所占字节数
字符占位符
%c //单个字符输出
%s //输出字符串
浮点占位符
%f //以小数形式输出浮点数, 默认 6 位小数
%e //以指数形式输出浮点数, 默认 6 位小数
%g //自动选择 %e 或者 %f 各式
其它形式占位符
%p //输出十六进制形式的指针地址
%@ //输出 Object-C 对象
占位符附加字符
l //在整型和浮点型占位符之前,%d %o %x %u %f %e %g 代表长整型和长字符串
n(任意整数) //%8d代表输出8位数字,输出总位数
.n //浮点数 限制小数位数,%5.2f 表示5位数字 2位小数,字符串截取字符个数
- //字符左对齐

3、捕获异常

  • @try:具体实现
  • @catch:异常处理
  • @finally:资源回收,@finally块中的内容是肯定会被执行的,不要在@finally中使用return@throw等导致方法终止的语句

4、常用的标识符

添加不同类型的标识符来实现在文件中快速跳转至正确位置。常用的标识符有 MARK 划分小节、TODO 添加待办、FIXME 添加待修复等。你也可以通过在代码评论中添加不同标识符的方式来在快速导航条中创建备注提醒,以便在未来查看代码时知道哪里还需要修改。

标识符的具体使用格式为「// 标识符: - 文本」,其中冒号后的小短杆代表分割线,若你不需要分割线则可省略短杆。以 FIXME 标识符为例,若你想在预览窗口的上方添加一个待修复内容的提醒时,则可以使用语法 // FIXME: - 生成预览应改为黑色背景。这段代码会在快速导航中加入一个黄色补丁符号,并在其上方放置分割线。


六、LLDB

1、LLDB的简介

LLDB是个开源的内置于XCode的具有REPL(read-eval-print-loop)特征的Debugger。在日常的开发和调试过程中给开发人员带来了非常多的帮助。

大家一定有使用过ppo命令在调试输出窗口中计算并输出表达式的内容,ppo就是一种LLDB 调试工具的命令。在程序中设置断点,运行时挂起, 在输出窗口中选择DebuggerOutput,此时输出窗口有(lldb)命令提示符,如果不输入命令,直接按Enter键,LLDB会自动执行上次的命令。

举个例子,假设我们给main方法设置一个断点,我们使用下面的命令。

(lldb) breakpoint set -n main
  • command(LLDB调试命令的名称): breakpoint 表示断点命令
  • action(执行命令的操作): set 表示设置断点
  • option(命令选项): -n表示根据方法name设置断点
  • arguement(命令的参数): mian 表示方法名为mian

2、常用命令

p:可以用来打印基本数据类型
call:执行一段代码

call NSLog(@"%@", @"yang") 

expr:动态执行指定表达式

expr i = 101
输出:(int)$0 = 101

3、帮助文档

LLDB其中内置了非常多的功能,选择去硬背每一条指令并不是一个明智的选择。我们只需要记住一些常用的指令,在需要的时候通过help命令来查看相关的描述即可。我们要查看某一个命令改如何使用时,可以使用 help command 来获取对应命令的使用方法。

(lldb) help
(lldb) help expression

4、设置断点

// 使用命令breakpoint set, 该命令可以设置文件行 (file line) 断点和符号(symbolic) 断点,并且都有简略写法
(lldb) breakpoint set --file MasterViewController.m--line 41 
(lldb) b MasterViewController.m:41

// 只需要给ViewController.m文件中的viewDidLoad设置断点
(lldb) breakpoint set -f ViewController.m -l 38

// 在所有的findAll方法调用时挂起, 则属于符号断点设置
(lldb) breakpoint set --selector findAll 

// 查看断点
(lldb) breakpoint list 

// 删除断点:breakpoint set设置的断点需要使用命令来删除,不能通过断点导航面板
(lldb) breakpoint delete 断点编号

// 单步进入
(lldb) thread step-in 

// 单步跳过
(lldb) thread step-over 

// 继续运行:程序会运行到下一个断点或结束
(lldb) thread continue 

// 当前函数或方法返回:不想往下执行 ,而是直接返回函数或方法的结果
(lldb) thread return @"abc" // 返回 @"abc"字 符串

5、观察点命令

为变量设置一个观察点,当这个变量变化的时候, 程序就会挂起。

// 为循环体变量b设置观察点
(lldb) watchpointset variable b

// 为变量设置观察点时,变量不能超过它的作用域,变量b的作用域是for循环体 ,否则命令会出现下面的错误 
error : no variable or instance variable named'b'found in this frame

// 查看观察点
(lldb) watchpiont list

// 删除观察点
(lldb) watchpoint delete 观察点编号 

6、线程堆栈信息

当发生crash的时候,我们可以使用thread backtrace查看堆栈调用。这些frame(帧)和左边红框里的堆栈是一致的。

// 可以看到crash发生在-[ViewController viewDidLoad]中的第23行,只需检查这行代码是不是干了什么非法的事儿就可以了
(lldb) thread backtrace
* thread #1: tid = 0xdd42, 0x000000010afb380b libobjc.A.dylib`objc_msgSend + 11, queue = 'com.apple.main-thread', stop reason = EXC_BAD_ACCESS (code=EXC_I386_GPFLT)
    frame #0: 0x000000010afb380b libobjc.A.dylib`objc_msgSend + 11
  * frame #1: 0x000000010aa9f75e TLLDB`-[ViewController viewDidLoad](self=0x00007fa270e1f440, _cmd="viewDidLoad") + 174 at ViewController.m:23
    frame #2: 0x000000010ba67f98 UIKit`-[UIViewController loadViewIfRequired] + 1198
    frame #3: 0x000000010ba682e7 UIKit`-[UIViewController view] + 27
    frame #4: 0x000000010b93eab0 UIKit`-[UIWindow addRootViewControllerViewIfPossible] + 61
    frame #5: 0x000000010b93f199 UIKit`-[UIWindow _setHidden:forced:] + 282
    frame #6: 0x000000010b950c2e UIKit`-[UIWindow makeKeyAndVisible] + 42

// LLDB还为backtrace专门定义了一个别名:bt,他的效果与thread backtrace相同,如果你不想写那么长一串字母,直接写下bt即可
(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 4.1
  * frame #0: 0x000000010fd2b3a0 VideoRecordingDemo`__35-[AVPlayerDemo addProgressObserver]_block_invoke(.block_descriptor=0x0000600000bee310,

// bt只会打印当前线程堆栈信息,而打印所有线程堆栈信息,使用bt all
(lldb) bt all

7、直接返回值

不想让代码执行某个方法,或者要直接返回一个想要的值。有一个someMethod方法,默认情况下是返回YES。我们想要让他返回NO。我们只需在方法的开始位置加一个断点,当程序中断的时候,输入命令即可,效果相当于在断点位置直接调用return NO;,不会执行断点后面的代码。thread return可以接受一个表达式,调用命令之后直接从当前的frame返回表达式的值。

(lldb) thread return NO

8、查看变量

//所有本地变量
(lldb) frame variable 

//某个具体变量  bar 
(lldb ) frame variable bar 

//print bar的缩写, print命令是打印和计算表达式
(lldb ) p bar 

//包括 global/static变量
(lldb) target variable 

//具体变量baz
(lldb) target variable baz

9、计算表达式

expression命令的作用是执行一个表达式,并将表达式返回的结果输出。说expressionLLDB里面最重要的命令都不为过。因为他能实现2个功能。

功能一:执行某个表达式

我们在代码运行过程中,可以通过执行某个表达式来动态改变程序运行的轨迹。 假如我们在运行过程中,突然想把self.view颜色改成红色,看看效果。我们不必写下代码,重新run,只需暂停程序,用expression改变颜色,再刷新一下界面,就能看到效果。

// 改变颜色
 (lldb) expression -- self.view.backgroundColor = [UIColor redColor]
 // 刷新界面
 (lldb) expression -- (void)[CATransaction flush]
功能二:将返回值输出

也就是说我们可以通过expression来打印东西。 假如我们想打印self.view

(lldb) expression self.view
(UIView *) $0 = 0x00007f8ed7418480

10、打印变量

p

一般情况下,我们直接用expression还是用得比较少的,更多时候我们用的是pprintcall。这三个命令其实都是 expression --的别名(--表示不再接受命令选项)。print用来打印某个东西,可以是变量和表达式,p可以看做是print的简写。

po

OC里所有的对象都是用指针表示的,所以一般打印的时候,打印出来的是对象的指针,而不是对象本身。如果我们想打印对象。我们需要使用命令选项:-O。为了更方便的使用,LLDBexpression -O定义了一个别名:pop打印的是当前对象的地址,而po则会调用对象的description方法,做法和NSLog是一致的。

call

调用某个方法,表面上看起来他们可能有不一样的地方,实际都是执行某个表达式(变量也当做表达式),将执行的结果输出到控制台上。所以你可以用p调用某个方法,也可以用call打印东西。

// 下面代码效果相同
(lldb) expression -- self.view
(UIView *) $1 = 0x00007fb2a40344a0
(lldb) p self.view
(UIView *) $2 = 0x00007fb2a40344a0
(lldb) print self.view
(UIView *) $3 = 0x00007fb2a40344a0
(lldb) call self.view
(UIView *) $4 = 0x00007fb2a40344a0

(lldb) expression -O -- self.view
>
(lldb) po self.view
>

// 你想知道一个视图包含了哪些子视图。当然你可以循环打印子视图,但是下面只需要一个命令即可解决
// 输出视图层级关系(这是一个被隐藏的命令)
po [[self view] recursiveDescription]

七、单元测试

1、测试导航面板

错误警告导航面板:用例测试失败信息,测试方法中的期望值和实际值不一致,断言失败
测试导航面板:绿色图标表示测试成功,红色图标代表测试失败,点击这些图标可以导航到测试方法
输出窗口中看到测试信息:TestSuite开头的是测试用例集合(测试类),以TestCase开头的是测试用例,每个测试用例都包括一个开始日志和一个结束日志,passed说明测试通过


2、书写单元测试类

导入测试框架
import XCTest
作为XCTest框架的测试用例类 ,需要继承XCTestCase
class TaxTests: XCTestCase 
初始化方法,每个测试用例都要执行
// 在测试类运行的生命周期中,这个方法可能多次运行
override func setUp()
{
    super.setUp()
    self.bl = TaxRevenueBL()
}
释放资源的方法,在每个测试用例执行后执行
override func tearDown()
{
        self.bl = nil
        super.tearDown()
}
测试用例方法必须以 test开头
// XCTAssert是XCTestCase框架定义的一个函数,true表示断言通过
func testExample() 
{
    XCTAssert(true, "Pass")
}
性能测试用例方法
func testPerformanceExample()
{
        self.measure
        {
            ...
        }
}

八、UI测试

UI测试从来都是开发人员和测试人员的梦魔,包括 UI事件处理、表示逻辑、控件输入验证和获取UI环境对象。由于UI测试用例都是围绕界面操作而设计的, 一 些测试工具可以将这些操作录制下来 , 生成测试代码, 测试人员可以适当修改测试代码"测试脚本"。

初始化
override func setUp()
{
    super.setUp()
    
    // 测试用例出错后是否继续执行,设置为ture是继续执行,设置为false是终止。
    continueAfterFailure = false
    
    // UI测试必须启动应用,该语句可以启动应用程序
    XCUIApplication().launch()
}
录制脚本

测试框架提供测试脚本录制工具 UI Recording。通过该工具可以生成 Objective-CSwift语言的测试脚本。

录制过程

打开 TaxUITests测试用例类。将光标置于测试方法中 , UlRecording会在这里生成代码 。 然后,点击调试栏中的”录制”按钮开始录制 。在文本框中输入5000,然后点击”计算”按钮,计算获得的结果45.00会显示到标签上 。 这些操作都被UIRecording记录下来,并且在测试用例类中生成了代码。

修改录制脚本
  • UI 元素的层次结构树
  • 录制的脚本不会有 if和 for等逻辑分支和循环语句,录制脚本只是针对特定控件
  • 要想删除表视图中所有的单元格,但是录制过程中只是删除了其中一个单元格, 此时需要修改脚本,为其添加 for和计数语句,使之适用于所有的单元格
属性 API
  • exists 属性:判断元素是否存在
  • descendantsMatchingType:所有后代元素
  • childrenMatchingType:所有直接子元素
  • typeText:获得焦点后,模拟键盘输入字符到元素
  • tap:点击元素动作
  • doubleTap:双击
  • launch:启动被测试的应用程序
  • elementBoundByindex:通过索引访问元素
  • elementMatchingPredicate:通过谓词 NSPredicate 指定查询条件进行查询
  • elementMatchingType:identifier:通过 id 进行查询

九、真机调试

1、安装步骤

a、将数据线连接在 Mac 上

模拟机固然非常方便, 但也有它的固有缺陷,比如没有许多硬件传感器及反馈,性能测试时使用的实际上是你 Mac 的硬件,而不具备 iOS 设备的代表性。当你希望使用真机进行测试,推荐你至少拥有一款打算支持设备类型的较新和最老型号机器进行测试。老机器允许你测试应用程序是否可以在配置较低的机器上运行,新机器允许你尝试所有设备硬件功能,创作上不被束手束脚。

使用真机设备的方式很简单,你只需要一根数据线将其接在 Mac 上即可。第一次程序在设备上运行时,Xcode 会为你的设备安装一些必备文件,可能会花一段时间,耐心等待即可。

❶ 运行Xcode, 点击 Preference 添加账号(能在 appstore下载的账号)
添加账号
❷ 选中刚才添加的 AppleID,点击 Manage Certificates ,再点击 + Apple Development
Manage Certificates
❸ 自定义 bundle id 开始真机调试(创建新 bundle id ——未被其他 team 使用过)系统会自动 repair 产生 provision文件 。这里需要说明一下,如果我们是从网上下载的demo,这里的bundle id一定要进行修改,不然签名的时候会失败。自己的项目在这里签名出现问题的时候也可以尝试修改一下这个bundle identidier
bundle id
❹ 手机(真机)中点击设置(Settings) —> 通用(General)—>描述文件与设备管理—>点击对应的id —->信任(Trust)
描述文件与设备管理

b、无线设备调试

每次连着数据线测试确实有些麻烦,尤其是当你在测试 AR 或需要使用传感器的应用时,拖着一根线会限制你的灵活性。这时你可以设置 Xcode 提供的无线设备调试功能,这项功能需要你在第一次有线连接设备后手动开启。在顶部菜单中,依次选择「Window - Devices and Simulators」,在当前连接的设备中找到你想开启无线调试的设备,并勾选右侧的「Connect via network」即可完成设置。

无线设备调试

设置完成后,任何时候你希望在真机设备上运行程序或调试时,只需要保证电脑和设备使用同一个 Wi-Fi 即可,不再需要数据线将其连接。小提醒:在运行目标设备选择的是虚拟机的情况下,程序打包「Product - Archive」将变得不可用,这时只需要将目标对象选为真机设备即可。


2、问题

a、已经安装的APP数量过多

问题描述:Xcode 真机运行,无法安装提示The maximum number of apps for free development profiles has been reached

Details

Unable to install "VideoRecordingDemo"
Domain: com.apple.dt.MobileDeviceErrorDomain
Code: -402620383
--
The maximum number of apps for free development profiles has been reached.
Domain: com.apple.dt.MobileDeviceErrorDomain
Code: -402620383

问题原因:由于ios设置了自动卸载不常用的应用,而这几个app恰好是已经被卸载的不常用应用,其实就是卸载的不常用应用也会被计入免费的开发app次数 ,所以如果你是这种情况不能安装,删掉这些被卸载的应用就可以安装了!

解决方案:网上搜了一下 说是在都说是app应用达到了上限,删除即可,可是我根本就没有安装应用,仍然报这个提示,所以一直没有解决,直到在stackoverflow上看到一个答案。

stackoverflow

步骤一:打开 Xcode 中的 Device

image.png

步骤二:点击Open ConsoleMac上打开控制台应用程序,并在尝试从Xcode安装应用程序时捕获日志。在左侧的设备下>选择您的iPhone设备>然后搜索MIFreeProfileValidatedAppTracker,找到对应的app然后在IOS 中删掉。

被IOS自动卸载掉的APP占坑了

可以用qihoo.360quiet百度去找对应APP图标卸载,如果找不到对应APP,像我一样全部卸载掉就好了。

卸载完成后就清爽多了

然后再次点击安装,就OK了。

OK

避免再次发生:关掉自动卸载的坑选项。

关掉自动卸载

b、App ID 达到最大限度
错误提示

这个账号达到了最大的app ID数量,因为我的是免费账号,所以每7天只能最多创建10个app ID,所以在真机运行的时候就会报这个错误。

Communication with Apple failed.
Your maximum App ID limit has been reached. You may create up to 10 App IDs every 7 days.
解决方法
  • 换一个apple ID,既然每个账号只能一周最多创建10个app ID,那我们可以用另外的账号来重新登录。
  • 因为我们一般都只有一个apple ID,或者来回更换apple ID比较麻烦,我们可以把以前能够真机运行的demobundle ID拷贝到将要运行的这个项目来,这样就能继续使用了。我们可以把我们能够真机运行的bundle ID给保存下来,做个列表之类的,下次运行的时候我们可以先把之前运行过的项目的bundle ID拿来先使用,而不用xcode自动生成的bundle ID

c、Xcode 真机调试启动非常慢的问题

步骤一:shift+command+G到资源库 ~/Library/Developer/Xcode/iOS DeviceSupport/删除该目录下所有文件。

DeviceSupport

步骤二:选择Xcode->Window->Devices and Simulators 真机设备,鼠标右键选择unpair the device

unpair the device

十、工程配置

1、PCH头文件

a、PCH头文件的作用

pch头文件的内容能被项目中的其他所有源文件共享和访问,是一个预编译文件,可以存放一些全局的宏(整个项目中都用得上的宏),用来包含一些全部的头文件(整个项目中都用得上的头文件),也能自动打开或者关闭日志输出功能,但是因为大家把大量的头文件和宏定义放到pch里边,导致编译时间过长。

#ifndef PrefixHeader_pch
#define PrefixHeader_pch

#define SCREEN_WIDTH [UIScreen mainScreen].bounds.size.width
#define SCREEN_HEIGHT [UIScreen mainScreen].bounds.size.height
#define StatusBarHeight = [[UIApplication sharedApplication] statusBarFrame].size.height
#define NavigationBarHeight = self.navigationController.navigationBar.frame.size.height

#define rgba(r,g,b,a) [UIColor colorWithRed:r/255.0 green:g/255.0 blue:b/255.0 alpha:a]

#endif /* PrefixHeader_pch */

b、创建PCH头文件
创建PCH头文件

c、配置工程环境

在工程的TARGETS里边Building Setting中搜索Prefix Header,然后把Precompile Prefix Header右边的NO改为Yes,将Precompile Prefix Header为YES,预编译后的pch文件会被缓存起来,可以提高编译速度。

然后在Precompile Prefix Header下边的Prefix Header右边双击,添加刚刚创建的pch文件的工程路径,添加格式:$(SRCROOT)/项目名称/pch文件名$(SRCROOT)的意思就是工程根目录的意思,添加完成后,他会自动帮你变成你工程所在的路径。如果还不太清楚的话可以右键pch文件,然后show in finder

配置工程环境

2、国际化操作(本地化)

应用名称的多语言显示

对于应用的名称来说,如果没有显式设置的话,系统会默认显示工程名字作为应用的名称,如果需要显示的应用名称与工程的名字不一样,需要在info.plist里显式添加Bundle display nameI字段,并填写对应的string内容为应用的名称。可是如果应用需要使用根据系统的语言设置显示不用语言的名称时该方法就不能满足我们的需求。我们可以通过系统的查找方法来为应用添加多语言支持。

在工程里创建InfoPlist.strings文件。.strings是系统提供的支持多语言显示的文件类型)。

点击右侧,添加语言对于该文件的依赖。选择右侧辅助工具栏中的Localization,单击Localize选项。

选择工程-> PROJECT-> Locations 选择+添加新的语言支持,此处以简体汉语为例子:

在弹出框中,选择需要国际化的文件(刚才我们创建的.strings文件):

现在点开我们最初创建的InfoPlist.strings 文件夹,发现该文件夹下多出了两个子文件夹:

现在我们可以在这个文件夹里填写Info.plist中需要国际化的字段(以应用的名称为例)。在InfoPlist.strings(English)中填写字段对应的英语名称,"CFBundleDisplayName" = "APPName";keyinfoDictionary中对应的key值相同,并且不能使用“”。每个key-value结束必须使用;进行结束。在InfoPlist.strings(Simplified)中填写字段对应的中文名称:"CFBundleDisplayName" = "应用名称";。好了,准备工作结束,设置系统显示语言为中文,运行你的应用,看看应用对应的名称是否是你设置的名称,然后在设置中更改系统的显示语言为英语,查看应用对应的名称。

应用内显示内容的国际化

创建对应的.strings文件,注意需要用Localizable进行命名,勾选需要依赖该文件夹的语言种类,然后在对应的语言中编辑对应的key-value键值对,例如我们定义了一个keyvar,在Localizable.strings(English)中,"var" = "I am English variable";。在Localizable.strings(Simplified)中,"var" = "我是中文变量";。然后我们就可以通过
NSLocalizedString(, )方法,让系统根据当前的语言环境选择相关的语言下对应的valuekey是我们定义的字符串(例如“var”),comment是一个用来说明变量的字符串,可以为nil。在不同的语言环境下使用log打印对应的变量值,系统就会根据当前的系统语言环境选择对应的value值。

NSLog(@"variable = %@",NSLocalizedString(@"var",nil));

Demo

Demo在我的Github上,欢迎下载。
BasicsDemo

参考文献

你可能感兴趣的:(IOS基础知识:调试修复BUG)