美文网首页
IOS基础:调试修复BUG

IOS基础:调试修复BUG

作者: 时光啊混蛋_97boy | 来源:发表于2020-10-19 18:28 被阅读0次

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

目录

  • 崩溃名场面
    • 1、野指针访问 EXC_BAD_ACCESS
    • 2、查找不到指定的方法 unrecognized selector sent to instance
    • 3、集合类
    • 4、KVO与KVC
    • 5、UITableView或者UICollectionView
    • 6、清除过往编译
  • 一、Crash Log
    • 1、应用Crash日志获取的整个流程
    • 2、Crash Log 崩溃日志收集方式
  • 二、断点
    • 1、文件行断点
    • 2、异常断点
    • 3、符号断点
    • 4、其他断点
  • 三、导航面板
    • 1、调试工具
    • 2、输出窗口
    • 3、变量查看窗口
    • 4、查看线程
  • 四、断点调试命令
    • 1、断言与NSLog
    • 2、捕获异常
    • 3、LLDB
  • 五、使用 XCTest测试框架
  • 六、UI测试
  • 七、真机调试
    • 1、安装步骤
    • 2、问题
  • 八、工程配置
    • 1、国际化
    • 2、PCH头文件
  • 九、常用技巧
  • 参考文献

崩溃名场面

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

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

3、集合类

Terminating app due to uncaught exception 'NSRangeException', reason: '*** -[__NSArrayI objectAtIndex:]: index 8 beyond bounds [0 .. 7]
failed: caught "NSInvalidArgumentException", " * -[__NSPlaceholderDictionary initWithObjects:forKeys:count:]: attempt to insert nil object from objects[1]
failed: caught "NSInvalidArgumentException", " * setObjectForKey: object cannot be nil (key: no_nillKey)
failed: caught "NSInvalidArgumentException", " * setObjectForKey: key cannot be nil"
Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '*** -[__NSArrayM insertObject:atIndex:]: object cannot be nil'
具体场景
  1. 数组越界
NSArray *array = @[@"a",@"b",@"c"];
id letter = [array objectAtIndex:3];
  1. 向数组中插入空对象
NSMutableArray *mutableArrray = [NSMutableArray array];
[mutableArray addObject:nil];
  1. 调用可变字典setObject:ForKey方法,key或者value为空,特别注意字面量写法
@{@"itemID":article.itemID}//这里itemID可能为空
  1. 一边遍历数组,一边修改数组内容。或者多线程环境中,一个线程在读另外一个线程在写
for(id item in self.itemArray) {
 if (item != self.currentItem) {
     [self.itemArray removeItem:item];
   }
}
解决方案
  1. 从数组中的某个下标取元素的时候先判断下标与数组长度的关系,如:
if (index < [[self currentUsers] count]) {
    UserModel * model = [[self currentUsers] objectAtIndex:index];
    return model;
}
  1. NSMutableArray以及NSMutableDictionary自定义一些安全的扩展方法,如:
-(id)objectAtIndexSafely:(NSUInteger)index {
    if (index >= self.count) {
        return nil;
    }
    return [self objectAtIndex:index];
}
-(void)setObjectSafely:(id)anObject forKey:(id <NSCopying>)aKey {
    if (!aKey) {
        return;
    }
    if (!anObject){
        return;
    }
    [self setObject:anObject forKey:aKey];
}
  1. 调用NSMutableDictionarysetValue:ForKey:方法而不是setObject:ForKey:方法,少用字面量语法
    NSDictionary内部对value做了处理,[mutableDictionary setValue:nil ForKey:@"name"]不会崩溃
  2. 保证多线程中读写操作的原子性:加锁,信号量,GCD串行队列,GCD dispatch_barrier_async方法等,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];
        });
    }
  1. 遍历时需要修改原数组的时候可以遍历原数组的一个拷贝,如:
NSMutableArray *copyArray = [NSMutableArray arrayWithArray:self.items];
 for(id item in copyArray) {
     if (item != self.currentItem) {
         [self.items removeGuideViewItem:item];
     }
}

4、KVO与KVC

KVO
Terminating app due to uncaught exception 'NSRangeException', reason: 'Cannot remove an observer <UIView 0x7f9a90f0a0d0> for the key path "frame" from <ViewController 0x7f9a90e07010> 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

KVC
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '[ setNilValueForKey]: could not set nil as the value for the key age.' // 调用setNilValueForKey抛出异常
*** Terminating app due to uncaught exception 'NSUnknownKeyException', reason: '[<ViewController 0x7ff968606ee0> setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key undefined.'//在类中找不到对应的key
  1. valuenil,如:
[people1 setValue:nil forKey:@"age"]
  1. 在本类中找不到对应的key,如:
[viewController setValue:@"crash" forKey:@"undefined"];

5、UITableView或者UICollectionView

*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'UITableView (<ContainerTableView: 0x7fec623a6400; baseClass = UITableView; frame = (0 0; 375 567); clipsToBounds = YES; autoresize = W+H; gestureRecognizers = <NSArray: 0x608002844bf0>; layer = <CALayer: 0x60800183eec0>; contentOffset: {0, 0}; contentSize: {375, 2946}>) failed to obtain a cell from its dataSource (<FeedBaseDelegate: 0x600003668540>)'

UITableViewcellForRowAtIndexPath方法或者UICollectionViewcellForItemAtIndexPath方法因为异常返回了nil,出现这种情况的原因有:

  • numberOfRowsInSection返回的数目不正确,导致行数比cellForRoAtIndexPath预期的多,于是cellForRowAtIndexPath方法就不能正确返回超出预期的cell了。
  • cellForRowAtIndexPath中逻辑有误,漏了一些情况,导致有些cell不能正确返回。

6、清除过往编译

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


一、Crash Log

背景

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

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

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

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

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

  4. 当应用在设备上发生异常时,系统会产生异常日志并存储在手机设备上。

  5. 用户可以按照调试已部署的iOS应用中的步骤直接从其设备检索崩溃报告。 如果已经通过AdHocEnterprise方式分发了应用程序,则这是从用户那里获取崩溃报告的唯一方法。

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

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

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

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

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

方式一、使用苹果提供的Crash崩溃收集服务。(较少用)

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

Crash Log 崩溃日志

在手机上的 设置-->隐私-->分析-->分析数据中可以查看到异常。

直接在手机上查看

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

dSYM 符号集
  • 符号集是我们对ipa文件进行打包之后,和.app文件同级的后缀名为.dSYM的文件,这个文件必须使用Xcode进行打包才有。
  • 每一个.dSYM文件都有一个UUID,和.app文件中的UUID对应,代表着是一个应用。而.dSYM文件中每一条崩溃信息也有一个单独的UUID,用来和程序的UUID进行校对。
  • 我们如果不使用.dSYM文件获取到的崩溃信息都是不准确的。
  • 符号集中存储着文件名、方法名、行号的信息,是和可执行文件的16进制函数地址对应的,通过分析崩溃的.Crash文件可以准确知道具体的崩溃信息,包括文件名、方法名、行号等。

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

Archive

选中archive的版本右击,选择Show in Finder就可以选中archived文件然后显示包内容,就可以找到dSYM文件了。

dsym文件位置
symbolicatecrash 工具

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

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

在桌面上建立一个Crash文件夹,将.crash.dSYMsymbolicatecrash文件都放入文件夹中。

Crash文件夹

在终端下进入该文件夹,使用命令解析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
 
...

方式二、通过Xcode查看设备崩溃信息

除了上面的系统分析工具来进行分析,如果是打包电脑直接连接崩溃后的手机,则可以选择window-> devices -> 选择自己的手机 -> view device logs或者通过~/Library/Logs/CrashReporter/MobileDevice/就可以查看我们的崩溃信息了。只要手机上的应用是这台电脑安装打包的,这样的崩溃信息系统已经为我们符号化好了,如果还是没有符号化完毕 ,我们选择文件,然后右击选择Re-Sysbomlicate就可以。如果是使用其他电脑进行的打包,我们可以在这里面将Crash文件导出,自己通过命令行的方式进行解析。

通过Xcode查看设备崩溃信息

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

经过Xcode符号化的异常日志

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

查看不同版本中的异常

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

以友盟为例,使用cocoapods引入需要的类库:

    pod 'UMCCommon', '~> 2.1.1'
    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

在发布版本时,保存并向友盟后台上传符号表文件:

保存并向友盟后台上传符号表文件

每天抽时间去后台关注一下应用发生了哪些异常,及时定位异常并修复。

及时定位异常并修复

方式四:自己实现应用内崩溃收集,并上传服务器

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

//定义异常捕获函数原型
typedef void NSUncaughtExceptionHandler(NSException *exception);
 
//注册捕获函数
FOUNDATION_EXPORT void NSSetUncaughtExceptionHandler(NSUncaughtExceptionHandler * _Nullable);

NSException类的内部声明如下:

@interface NSException : NSObject <NSCopying, NSSecureCoding> {
    @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<NSNumber *> *callStackReturnAddresses API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
@property (readonly, copy) NSArray<NSString *> *callStackSymbols API_AVAILABLE(macos(10.6), ios(4.0), watchos(2.0), tvos(9.0));
 
// 用于抛出异常,中断执行
- (void)raise;
 
@end

功能实现如下:

@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

这里有一个细节问题:系统提供了捕获系统异常的方法,但是只有这一个方法,如果应用中有多个捕获异常的实现,那么先注册的异常捕获方法就会被后来注册的方法覆盖掉,造成之前的注册的异常捕获方法不能执行,所以负责的注册方法是在自己注册方法之前保存之前的捕获实现,在异常处理方法中,调用原来的异常捕获实现,从而确保每一个注册的异常捕获方法都在可以执行.

二、断点

1、文件行断点

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

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

打上断点之后,对断点进行编辑,设置相应过滤条件:


条件断点

Condition:返回一个布尔值,当布尔值为真触发断点,一般里面我们可以写一个表达式。
Ignore:忽略前N次断点,到N+1次再触发断点。
Action:断点触发事件。
AppleScript:执行脚本。

Capture GPU Frame:用于OpenGL ES调试,捕获断点处GPU当前绘制帧。
Debugger Command:和控制台中输入LLDB调试命令一致。
Log Message:输出自定义格式信息至控制台。
Shell Command:接收命令文件及相应参数列表,Shell Command是异步执行的,只有勾选“Wait until done”才会等待Shell命令执行完在执行调试。
Sound:断点触发时播放声音。
Options(Automatically continue after evaluating actions选项):选中后,表示断点不会终止程序的运行。

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、调试工具

调试工具.png
  • 视图调试按钮可以显示视图层次结构
  • 模拟位置按钮可以向模拟器设备发送虚拟的位置坐标,它用于位置服务应用中的测试
  • 使用跳转栏, 可以跳转到具体工程下某个类的方法中,能够跟踪程序的运行过程
跳转栏
  • 在断点挂起之后 ,点击继续执行按钮可以继续执行
  • 单步跳过按钮是单步执行,遇到方法和函数时不进入
  • 单步进入按钮,则进入方法或者函数里
  • 单步跳出按钮在从方法或函数里面跳回到原来调用它的地方时使用

2、输出窗口

输出窗口有 3个选择 All OutputDebugger OutputTarget Output

调试程序时,可以在 Debugger Output窗口中执行编译器的调试命令。

Target Output窗口中可以显示程序出错和异常等信息,函数(如 NSLogassert函数)输出的信息。

3、变量查看窗口

图标 A是自动变量、 S是静态变量、 R是寄存器、 L是本地变量。

Auto:查看经常使用的变量 。
Local Variables: 查看本地变量。
All Variables查看全部变量,包括寄存器和全局变量。

  • A-自动变量
  • S-静态变量
  • R-寄存器
  • L-本地变量

选择变量i= (int) 10, 点击鼠标右键,从弹出的快捷菜单中选择EditValue可以修改变量。
print description of listData 打印变量。

4、查看线程

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

初学者往往毫无头绪,不知道如何跟踪异常栈,以及如何分析异常栈报告。

** First throw call stack:( ...)之间的内容就是异常栈信息:
1: 栈输出序号,序号越大,表示越早被调用
2: 调用方法(或函数)所属的框架(或库)
3: 所属的类名
4: 函数名
5: 编译之后的代码偏移量,没有帮助

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


四、断点调试命令

1、断言与NSLog

a、断言

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

assert(i>=o&&i<9,"i变量超出了范围.”)
NSAssert(>i:o&&<9,@"i:%i变量超出了范固.",i);
b、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位小数,字符串截取字符个数
- //字符左对齐

2、捕获异常

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


3、LLDB

a、简介

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
b、常用命令

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

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

expr:动态执行指定表达式

expr i = 101
输出:(int)$0 = 101
c、帮助文档

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

(lldb) help
(lldb) help expression

d、设置断点

// 使用命令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"字 符串

e、观察点命令

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

// 为循环体变量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 观察点编号 

f、线程堆栈信息

当发生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

g、直接返回值

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

(lldb) thread return NO

f、查看变量

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

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

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

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

//具体变量baz
(lldb) target variable baz
h、计算表达式

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

I、打印变量

一般情况下,我们直接用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
<UIView: 0x7fb2a40344a0; frame = (0 0; 375 667); autoresize = W+H; layer = <CALayer: 0x7fb2a4018c80>>
(lldb) po self.view
<UIView: 0x7fb2a40344a0; frame = (0 0; 375 667); autoresize = W+H; layer = <CALayer: 0x7fb2a4018c80>>

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

4、常用的标识符

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

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

FIXME 标识符

五、使用 XCTest测试框架

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

import XCTest
class PITaxTests: XCTestCase {//作为XCTest框架的测试用例类 ,需要继承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 {            
        }
    }

属性:

    //定义了TaxRevenueBL类型的bl属性
    var bl: TaxRevenueBL!
    
    //在setup方法中初始化bl
    override func setUp() {
        super.setUp()
        self.bl = TaxRevenueBL()
    }
    //在tearDown方法中释放bl
    override func tearDown() {
        self.bl = nil
        super.tearDown()
    }

方法:

//测试月应纳税额超过80000元 用例7
func testCalculateLevel7() {
        let dbRevenue = 103500.0
        let tax = self.bl.calculate(dbRevenue)
        //测试方法中的XCTAssertEqual是XCTest框架中定义的断言函数
        XCTAssertEqual(tax, 31495.0, "用例7测试失败")
    }

六、UI测试

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

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

录制脚本:提供测试脚本录制工具 UI Recording , 通过该工具可以生成 Objective-CSwift语言的测试脚本,辅助功能提供了访问界面中 UI元素的能力,然后打开右边的标识检查器 , 在Accessibility---+ Label中设置控件的标签。

录制脚本

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

修改录制脚本:

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

七、真机调试

1、安装步骤

a、巧用真机

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

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

步骤一:运行XcodeXcode–》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达到最大限度
错误提示
Communication with Apple failed.
Your maximum App ID limit has been reached. You may create up to 10 App IDs every 7 days.

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

解决方法
  • 换一个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、国际化

Command+N在工程内新建一个StringsFile文件

StringsFile文件

❷ 选择Info,在Localization中添加需要的语言。

Info

❸ 回到文件列表,选中该文件,如Localized.strings,打开Xcode右边区域,查看该文件详情,找到“Localization”,选中需要的语言,然后Localized.strings下会自动生成对应的国际化文件。

Localized.strings

❹ 使用国际化时,用NSLocalizedString(<#key#>, <#comment#>)来获取项目中使用的字符串。如果需要跟随系统语言,则要在Info.plist文件中添加Application has localized display name,并设置为YES
注:Info.plist中的国际化,对StringsFile的命名只能是InfoPlist.strings

2、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

配置工程环境

九、常用技巧

新建分类问题
新建时选择Objective-C File
获取网络接口

❶ 开启开发者模式。

开启开发者模式

❷ 在网页上点击inspect element

insepect elements

❸ 找到文本或者图片的网络接口,即可使用。

获取网络接口

参考文献

iOS Crash异常日志收集
命令行工具解析Crash文件,dSYM文件进行符号化
漫谈iOS Crash收集框架

相关文章

  • IOS基础:调试修复BUG

    原创:知识点总结性文章创作不易,请珍惜,之后会持续更新,不断完善个人比较喜欢做笔记和写总结,毕竟好记性不如烂笔头哈...

  • 7月13号

    测试ios创建地点流程 检验已修复bug 测试多个Android版本bug ,上线1.5

  • Python调试

    项目开发过程中很难一次完全运行,总是会有各种各样的bug,修复这些bug需要各种调试手段。 调试技巧一: prin...

  • iOS Bug 调试

    EXC_BAD_ACCESS https://www.jianshu.com/p/4989c498e21e

  • iOS Bug 调试

    1.打印log调试 #ifdef DEBUG # define DLog(fmt, ...) NSLog((@"[...

  • iOS 调试BUG

  • 客户端爬取SDK更新公告

    如果你还不知道客户端爬取,出门右拐,看我之前博客。 BUG修复 修复不能在后台添加爬虫的bug 功能更新 IOS ...

  • iOS逆向目录

    越狱最新进展 一.逆向基础 iOS逆向基础01-越狱iOS逆向基础02-编译&调试iOS逆向基础03-符号表iOS...

  • 软件调试的技巧

    这篇文章是《调试九法:软硬件错误的排查之道》的阅读笔记。这本书的主旨,是介绍如何修复bug:找出bug发生的原因、...

  • iOS线上修复bug

    以前对于iOS来说,线上出现bug,都很苦恼,因为iOS上线审核周期太长,至少需要一周时间,还是在审核成功的情况下...

网友评论

      本文标题:IOS基础:调试修复BUG

      本文链接:https://www.haomeiwen.com/subject/icllmktx.html