IOS 开发高手课 学习笔记(第二部分)

第二部分主要是性能监控相关Part 7. 包大小:如何从资源和代码层面实现全方位瘦身?官方 App ThinningApp Thinning 是由苹果公司推出的一项可以改善 App 下载进程的新技术,主要是为了解决用户下载 App 耗费过高流量的问题,同时还可以节省用户 iOS 设备的存储空间。App Thinning 会专门针对不同的设备来选择只适用于当前设备的内容以供下载。App Thinning 有三种方式,包括:App Slicing、Bitcode、On-Demand Resource.
摘要由CSDN通过智能技术生成

第二部分主要是性能监控相关

Part 7. 包大小:如何从资源和代码层面实现全方位瘦身?

官方 App Thinning

App Thinning 是由苹果公司推出的一项可以改善 App 下载进程的新技术,主要是为了解决用户下载 App 耗费过高流量的问题,同时还可以节省用户 iOS 设备的存储空间。App Thinning 会专门针对不同的设备来选择只适用于当前设备的内容以供下载。
App Thinning 有三种方式,包括:App Slicing、Bitcode、On-Demand Resources。App Slicing,会在你向 iTunes Connect 上传 App 后,对 App 做切割,创建不同的变体,这样就可以适用到不同的设备。On-Demand Resources,主要是为游戏多关卡场景服务的。它会根据用户的关卡进度下载随后几个关卡的资源,并且已经过关的资源也会被删掉,这样就可以减少初装 App 的包大小。Bitcode ,是针对特定设备进行包大小优化,优化不明显。那么,如何在你项目里使用 App Thinning 呢?其实,这里的大部分工作都是由 Xcode 和 App Store 来帮你完成的,你只需要通过 Xcode 添加 xcassets 目录,然后将图片添加进来即可。

无用图片资源

删除无用图片的过程,可以概括为下面这 6 大步。通过

  1. find 命令获取 App 安装包中的所有资源文件,比如 find /Users/daiming/Project/ -name。
  2. 设置用到的资源的类型,比如 jpg、gif、png、webp。
  3. 使用正则匹配在源码中找出使用到的资源名,比如 pattern = @"@"(.+?)""。
  4. 使用 find 命令找到的所有资源文件,再去掉代码中使用到的资源文件,剩下的就是无用资源了。
  5. 对于按照规则设置的资源名,我们需要在匹配使用资源的正则表达式里添加相应的规则,比如 @“image_%d”。
  6. 确认无用资源后,就可以对这些无用资源执行删除操作了。这个删除操作,你可以使用 NSFileManger 系统类提供的功能来完成。

如果你不想自己重新写一个工具的话,可以选择开源的工具直接使用。我觉得目前最好用的是 LSUnusedResources,特别是对于使用编号规则的图片来说,可以通过直接添加规则来处理。
https://github.com/tinymind/LSUnusedResources.git

图片资源压缩

目前比较好的压缩方案是,将图片转成 WebP。WebP 是 Google 公司的一个开源项目。首先,我们一起看看选择 WebP 的理由:WebP 压缩率高,而且肉眼看不出差异,同时支持有损和无损两种压缩模式。比如,将 Gif 图转为 Animated WebP ,有损压缩模式下可减少 64% 大小,无损压缩模式下可减少 19% 大小。WebP 支持 Alpha 透明和 24-bit 颜色数,不会像 PNG8 那样因为色彩不够而出现毛边。
Google 公司在开源 WebP 的同时,还提供了一个图片压缩工具 cwebp来将其他图片转成 WebP。
cwebp 语法如下:

cwebp [options] input_file -o output_file.webp

比如,你要选择无损压缩模式的话,可以使用如下所示的命令:

cwebp -lossless original.png -o new.webp

在 cwebp 语法中,还有一个比较关键的参数 -q float。图片色值在不同情况下,可以选择用 -q 参数来进行设置,在不损失图片质量情况下进行最大化压缩:小于 256 色适合无损压缩,压缩率高,参数使用 -lossless -q 100;大于 256 色使用 75% 有损压缩,参数使用 -q 75;远大于 256 色使用 75% 以下压缩率,参数 -q 50 -m 6。
除了 cwebp 工具外,你还可以选择由腾讯公司开发的iSparta。

图片压缩完了并不是结束,我们还需要在显示图片时使用 libwebp 进行解析。这里有一个 iOS 工程使用 libwebp 的范例,你可以点击这个链接查看。https://github.com/carsonmcdonald/WebP-iOS-example.git
不过,WebP 在 CPU 消耗和解码时间上会比 PNG 高两倍。所以,我们有时候还需要在性能和体积上做取舍。我的建议是,如果图片大小超过了 100KB,你可以考虑使用 WebP;而小于 100KB 时,你可以使用网页工具 TinyPng或者 GUI 工具ImageOptim进行图片压缩。这两个工具的压缩率没有 WebP 那么高,不会改变图片压缩方式,所以解析时对性能损耗也不会增加。

代码瘦身
常情况下,对可执行文件进行瘦身,就是找到并删除无用代码的过程。而查找无用代码时,我们可以按照找无用图片的思路,即:首先,找出方法和类的全集;然后,找到使用过的方法和类;接下来,取二者的差集得到无用代码;最后,由人工确认无用代码可删除后,进行删除即可。

  1. LinkMap 结合 Mach-O 找无用代码
  2. 通过 AppCode 找出无用代码
    如果工程量不是很大的话,我还是建议你直接使用 AppCode 来做分析。使用 AppCode 检查出来的无用代码,还需要人工二次确认才能够安全删除掉。

运行时检查类是否真正被使用过:

#define RW_INITIALIZED (1<<29)
bool isInitialized() { 
    return getMeta()->data()->flags & RW_INITIALIZED;
}
  • 扩展问题:苹果公司为什么要设计元类

一些有意义的评论:
1) 苹果设备有针对png图片的显示进行优化,所以并不建议将图片转换为webp,并且使用tinypng工具已经可以将Png图片很好的压缩了~
2)关于元类的文章:what-is-meta-class-in-objective-c

Part 8. iOS 崩溃千奇百怪,如何全面监控?

KVO 问题、NSNotification 线程问题、数组越界、野指针等崩溃信息,是可以通过信号捕获的。但是,像后台任务超时、内存被打爆、主线程卡顿超阈值等信息,是无法通过信号捕捉到的。
目前很多公司的崩溃日志监控系统,都是通过PLCrashReporter 这样的第三方开源库捕获崩溃日志,然后上传到自己服务器上进行整体监控的。而没有服务端开发能力,或者对数据不敏感的公司,则会直接使用 Fabric或者Bugly来监控崩溃。

信号可捕获的崩溃日志收集:

#include <execinfo.h>

void registerSignalHandler(void) {
    signal(SIGSEGV, handleSignalException);
    signal(SIGFPE, handleSignalException);
    signal(SIGBUS, handleSignalException);
    signal(SIGPIPE, handleSignalException);
    signal(SIGHUP, handleSignalException);
    signal(SIGINT, handleSignalException);
    signal(SIGQUIT, handleSignalException);
    signal(SIGABRT, handleSignalException);
    signal(SIGILL, handleSignalException);
}

void handleSignalException(int signal) {
    NSMutableString *crashString = [[NSMutableString alloc]init];
    void* callstack[128];
    int i, frames = backtrace(callstack, 128);
    char** traceChar = backtrace_symbols(callstack, frames);
    for (i = 0; i <frames; ++i) {
        [crashString appendFormat:@"%s\n", traceChar[i]];
    }
    NSLog(crashString);
}

信号捕获不到的崩溃信息怎么收集?
采用 Background Task 方式时,我们可以根据 beginBackgroundTaskWithExpirationHandler 会让后台保活 3 分钟这个阈值,先设置一个计时器,在接近 3 分钟时判断后台程序是否还在执行。如果还在执行的话,我们就可以判断该程序即将后台崩溃,进行上报、记录,以达到监控的效果。

  • 实践代码
- (void)applicationDidEnterBackground:(UIApplication *)application {
   
    self.bgTaskId = [application beginBackgroundTaskWithExpirationHandler:^{
   
        [application endBackgroundTask:self.bgTaskId];
        self.bgTaskId = UIBackgroundTaskInvalid;
    }];
    
    NSDate* st = [NSDate dateWithTimeIntervalSinceNow:0];
    while(1){
   
        [NSThread sleepForTimeInterval:5.0];
        NSDate* date = [NSDate dateWithTimeIntervalSinceNow:0];
        NSTimeInterval durring = [date timeIntervalSinceDate:st];
        NSLog(@"backgroudtask runing times=%.2f", durring);
        if(durring > 290.0){
   
            printStack(0);
            break;
        }
    }
}

其他捕获不到的崩溃情况还有很多,主要就是内存打爆和主线程卡顿时间超过阈值被 watchdog 杀掉这两种情况。其实,监控这两类崩溃的思路和监控后台崩溃类似,我们都先要找到它们的阈值,然后在临近阈值时还在执行的后台程序,判断为将要崩溃,收集信息并上报。
对于内存打爆信息的收集,你可以采用内存映射(mmap)的方式来保存现场。主线程卡顿时间超过阈值这种情况,你只要收集当前线程的堆栈信息就可以了。

采集到崩溃信息后如何分析并解决崩溃问题呢?
通过上面的内容,我们已经解决了崩溃信息采集的问题。现在,我们需要对这些信息进行分析,进而解决 App 的崩溃问题。我们采集到的崩溃日志,主要包含的信息为:进程信息、基本信息、异常信息、线程回溯。进程信息:崩溃进程的相关信息,比如崩溃报告唯一标识符、唯一键值、设备标识;基本信息:崩溃发生的日期、iOS 版本;异常信息:异常类型、异常编码、异常的线程;线程回溯:崩溃时的方法调用栈。通常情况下,我们分析崩溃日志时最先看的是异常信息,分析出问题的是哪个线程,在线程回溯里找到那个线程;然后,分析方法调用栈,符号化后的方法调用栈可以完整地看到方法调用的过程,从而知道问题发生在哪个方法的调用上。

一些被系统杀掉的情况,我们可以通过异常编码来分析。你可以在维基百科上,查看完整的异常编码。这里列出了 44 种异常编码,但常见的就是如下三种:0x8badf00d,表示 App 在一定时间内无响应而被 watchdog 杀掉的情况。0xdeadfa11,表示 App 被用户强制退出。0xc00010ff,表示 App 因为运行造成设备温度太高而被杀掉。

Part 9. 如何利用 RunLoop 原理去监控卡顿

我们先来看一下导致卡顿问题的几种原因:
复杂 UI 、图文混排的绘制量过大;
在主线程上做网络同步请求;
在主线程做大量的 IO 操作;
运算量过大,CPU 持续高占用;
死锁和主子线程抢锁。

RunLoop 原理 (网上很多介绍,这里不记录)
我们都知道,线程的消息事件是依赖于 NSRunLoop 的,所以从 NSRunLoop 入手,就可以知道主线程上都调用了哪些方法。我们通过监听 NSRunLoop 的状态,就能够发现调用方法是否执行时间过长,从而判断出是否会出现卡顿。所以,我推荐的监控卡顿的方案是:通过监控 RunLoop 的状态来判断是否会出现卡顿。

通过对 RunLoop 原理的分析,我们可以看出在整个过程中,loop 的状态包括 6 个,其代码定义如下:typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) { kCFRunLoopEntry , // 进入 loop kCFRunLoopBeforeTimers , // 触发 Timer 回调 kCFRunLoopBeforeSources , // 触发 Source0 回调 kCFRunLoopBeforeWaiting , // 等待 mach_port 消息 kCFRunLoopAfterWaiting ), // 接收 mach_port 消息 kCFRunLoopExit , // 退出 loop kCFRunLoopAllActivities // loop 所有状态改变}如果 RunLoop 的线程,进入睡眠前方法的执行时间过长而导致无法进入睡眠,或者线程唤醒后接收消息时间过长而无法进入下一步的话,就可以认为是线程受阻了。如果这个线程是主线程的话,表现出来的就是出现了卡顿。所以,如果我们要利用 RunLoop 原理来监控卡顿的话,就是要关注这两个阶段。RunLoop 在进入睡眠之前和唤醒后的两个 loop 状态定义的值,分别是 kCFRunLoopBeforeSources 和 kCFRunLoopAfterWaiting ,也就是要触发 Source0 回调和接收 mach_port 消息两个状态。

如何检查卡顿?

要想监听 RunLoop,你就首先需要创建一个 CFRunLoopObserverContext 观察者,代码如下:

CFRunLoopObserverContext context = {
   0,(__bridge void*)self,NULL,NULL};
runLoopObserver = CFRunLoopObserverCreate(kCFAllocatorDefault,kCFRunLoopAllActivities,YES,0,&runLoopObserverCallBack,&context);

将创建好的观察者 runLoopObserver 添加到主线程 RunLoop 的 common 模式下观察。然后,创建一个持续的子线程专门用来监控主线程的 RunLoop 状态。一旦发现进入睡眠前的 kCFRunLoopBeforeSources 状态,或者唤醒后的状态 kCFRunLoopAfterWaiting,在设置的时间阈值内一直没有变化,即可判定为卡顿。接下来,我们就可以 dump 出堆栈的信息,从而进一步分析出具体是哪个方法的执行时间过长。开启一个子线程监控的代码如下:

dispatch_async(dispatch_get_global_queue(0, 0), ^{
   
    //子线程开启一个持续的 loop 用来进行监控
    while (YES) {
   
        long semaphoreWait = dispatch_semaphore_wait(dispatchSemaphore, dispatch_time(DISPATCH_TIME_NOW, 3 * NSEC_PER_SEC));
        if (semaphoreWait != 0) {
   
            if (!runLoopObserver) {
   
                timeoutCount = 0;
                dispatchSemaphore = 0;
                runLoopActivity = 0;
                return;
            }
            //BeforeSources 和 AfterWaiting 这两个状态能够检测到是否卡顿
            if (runLoopActivity == kCFRunLoopBeforeSources || runLoopActivity == kCFRunLoopAfterWaiting) {
   
                //将堆栈信息上报服务器的代码放到这里
            } //end activity
        }// end semaphore wait
        timeoutCount = 0;
    }// end while
});

如何获取卡顿的方法堆栈信息?

获取堆栈信息的一种方法是直接调用系统函数。这种方法的优点在于ÿ

  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 9
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值