iOS基础系列(三):各种优化

面试总结六部曲

9、进阶知识点

1、APP内存

1、APP内存优化

APP在运行过程中,如果内存占用过高则会引起以下几个问题

  • 被操作系统的守护进程给杀掉,无论是前台还是后台;
  • 耗电增大,手机发热;
  • 系统可能会运行卡顿(不是换入换出到磁盘,而是解压缩和重压缩内存);

解决方案

  • 1、内存泄漏检测
    • 1、instruments
    • 2、集成MLeaksFinder库到开发的target,检测内存泄漏;
  • 2、在合适的地方添加AutoreleasePool来及时释放内存,比如全局IM消息的接收和解析,视频回放的消息过滤等
  • 3、OOM捕捉:用来分析短期内存增长过快的原因
  • 4、了解mmap,测试其在图片映射的内存降低数据;

2、内存泄漏检测原理

内存泄漏的检测方式

    1. 静态检测:使用XCode分析功能,Product->Analyze

使用静态检测可以检查出一些明显的没有释放的内存,包括NSObject和CF开头的内存泄漏,最常见问题有2种,这些问题都不复杂,需要的是细心:

  • 2、动态监测:Instruments中的Leak动态分析内存泄漏,product->profile ->leaks 打开工具主窗口
  • 3、 第三方工具MLeaksFinder

内存泄漏原理

先看看 Leaks,从苹果的开发者文档里可以看到,一个 app 的内存分三类:

  • Leaked memory(泄漏内存): 应用程序未引用的、不能再次使用或释放的内存(也可以通过使用Leaks仪器检测到)
  • Abandoned memory(废弃的内存): 仍然被你的应用程序引用的没有任何用处的内存。
  • Cached memory(缓存内存): 仍然被应用程序引用的内存,可以再次使用以获得更好的性能。

原理

MLeaksFinder 一开始从 UIViewController 入手。我们知道,当一个 UIViewController 被 pop 或 dismiss 后,该 UIViewController 包括它的 view,view 的 subviews 等等将很快被释放(除非你把它设计成单例,或者持有它的强引用,但一般很少这样做)。于是,我们只需在一个 ViewController 被 pop 或 dismiss 一小段时间后,看看该 UIViewController,它的 view,view 的 subviews 等等是否还存在。

具体的方法是,为基类 NSObject 添加一个方法-willDealloc方法,该方法的作用是,先用一个弱指针指向 self,并在一小段时间(3秒)后,通过这个弱指针调用-assertNotDealloc,而-assertNotDealloc主要作用是直接中断言。

这样,当我们认为某个对象应该要被释放了,在释放前调用这个方法,如果3秒后它被释放成功,weakSelf 就指向 nil,不会调用到-assertNotDealloc方法,也就不会中断言,如果它没被释放(泄露了),-assertNotDealloc就会被调用中断言。这样,当一个 UIViewController 被 pop 或 dismiss 时(我们认为它应该要被释放了),我们遍历该 UIViewController 上的所有 view,依次调-willDealloc,若3秒后没被释放,就会中断言。

3、野指针处理

当所指向的对象被释放或者收回,但是对该指针没有作任何的修改,以至于该指针仍旧指向已经回收的内存地址,此情况下该指针便称野指针.

为什么Obj-C野指针的Crash那么多

  • 跑不进出错的逻辑,执行不到出错的代码,这种可以提高测试场景覆盖度来解决。
  • 跑进了有问题的逻辑,但是野指针指向的地址并不一定会导致Crash,这就有点看人品了?

为什么跑进了有问题的逻辑,但还是不一定会导致Crash呢?

现实大概是下面几种可能的情况:

  • 1、对象释放后内存没被改动过,原来的内存保存完好,可能不Crash或者出现逻辑错误(随机Crash)。
  • 2、对象释放后内存没被改动过,但是它自己析构的时候已经删掉某些必要的东西,可能不Crash、Crash在访问依赖的对象比如类成员上、出现逻辑错误(随机Crash)。
  • 3、对象释放后内存被改动过,写上了不可访问的数据,直接就出错了很可能Crash在objc_msgSend上面(必现Crash,常见)。
  • 4、对象释放后内存被改动过,写上了可以访问的数据,可能不Crash、出现逻辑错误、间接访问到不可访问的数据(随机Crash)。
  • 5、对象释放后内存被改动过,写上了可以访问的数据,但是再次访问的时候执行的代码把别的数据写坏了,遇到这种Crash只能哭了(随机Crash,难度大,概率低)!!
  • 6、对象释放后再次release(几乎是必现Crash,但也有例外,很常见)。

解决思路

  • 通过fishhook替换C函数的free方法为自身方法safe_free,就类似runtime的方法交换
bool init_safe_free() {
    _unfreeQueue = ds_queue_create(MAX_STEAL_MEM_NUM);
    orig_free = (void(*)(void*))dlsym(RTLD_DEFAULT, "free");
    rebind_symbols((struct rebinding[]){{"free", (void*)safe_free}}, 1);
    return true;
}
复制代码
  • 然后在safe_free方法中对已经释放变量的内存,填充0x55,使已经释放变量不能访问,从而使某些野指针从不必现Crash变成了必现。

这里之所以填充为0x55是因为Xcode的僵尸对象填充的就是0x55。
如果填充为像0x22这样的数据也是可以,因为之前这里是存储的是一个对象,这个对象被数据覆盖了,当你调用方法的时候,数据无法响应对应的方法,因此也会导致崩溃。

2、APP性能监控

性能监控主要是从两方面进行监控

  • 1、线下监控:Instruments
  • 2、线上监控
    • 1、CPU 使用率
    • 2、FPS 的帧率
    • 3、内存

对于线上性能监控,我们需要先明白两个原则:

  • 监控代码不要侵入到业务代码中;
  • 采用性能消耗最小的监控方案。

线上性能监控,主要集中在 CPU 使用率、FPS 的帧率和内存这三个方面

CPU 使用率的线上监控方法

App 作为进程运行起来后会有多个线程,每个线程对 CPU 的使用率不同。各个线程对 CPU 使用率的总和,就是当前 App 对 CPU 的使用率

在 iOS 系统中,你可以在 usr/include/mach/thread_info.h 里看到线程基本信息的结构体,其中的 cpu_usage 就是 CPU 使用率

struct thread_basic_info {
  time_value_t    user_time;     // 用户运行时长
  time_value_t    system_time;   // 系统运行时长
  integer_t       cpu_usage;     // CPU 使用率
  policy_t        policy;        // 调度策略
  integer_t       run_state;     // 运行状态
  integer_t       flags;         // 各种标记
  integer_t       suspend_count; // 暂停线程的计数
  integer_t       sleep_time;    // 休眠的时间
};
复制代码

因为每个线程都会有这个thread_basic_info结构体,所以接下来的事情就好办了,你只需要定时(比如,将定时间隔设置为 2s)去遍历每个线程,累加每个线程的cpu_usage字段的值,就能够得到当前 App 所在进程的 CPU 使用率了

FPS 的帧率

FPS 是指图像连续在显示设备上出现的频率。FPS 低,表示 App 不够流畅,还需要进行优化

iOS 系统中没有一个专门的结构体,用来记录与 FPS 相关的数据。但是,对 FPS 的监控也可以比较简单的实现:通过注册 CADisplayLink 得到屏幕的同步刷新率,记录每次刷新时间,然后就可以得到 FPS。

内存使用量的线上监控方法

内存信息存在 task_info.h (完整路径 usr/include/mach/task.info.h)文件的 task_vm_info 结构体中,其中 phys_footprint 就是物理内存的使用,而不是驻留内存 resident_size。结构体里和内存相关的代码如下

struct task_vm_info {
  mach_vm_size_t  virtual_size;       // 虚拟内存大小
  integer_t region_count;             // 内存区域的数量
  integer_t page_size;
  mach_vm_size_t  resident_size;      // 驻留内存大小
  mach_vm_size_t  resident_size_peak; // 驻留内存峰值

  ...

  /* added for rev1 */
  mach_vm_size_t  phys_footprint;     // 物理内存

  ...
复制代码

类似于对 CPU 使用率的监控,我们只要从这个结构体里取出 phys_footprint 字段的值,就能够监控到实际物理内存的使用情况了


uint64_t memoryUsage() {
    task_vm_info_data_t vmInfo;
    mach_msg_type_number_t count = TASK_VM_INFO_COUNT;
    kern_return_t result = task_info(mach_task_self(), TASK_VM_INFO, (task_info_t) &vmInfo, &count);
    if (result != KERN_SUCCESS)
        return 0;
    return vmInfo.phys_footprint;
}
复制代码

3、包瘦身

官方 App Thinning

App Thinning 是由苹果公司推出的一项可以改善 App 下载进程的新技术,主要是为了解决用户下载 App 耗费过高流量的问题,同时还可以节省用户 iOS 设备的存储空间。

App Thinning原理:每个App包会包含多个芯片(如真机、模拟器)的指令集架构文件,App Thinning会使用户只下载一个适合自己设备的芯片指令架构文件。

简单说,使用App Thinning之后苹果会自动帮你把App包按照型号分割成不同变体,保证每个型号只下载自己所需要的资源。而自己所需要做的,就是通过Asset Catalog模板创建xcassets目录,将2x与3x图片放进去。

App Thinning 会专门针对不同的设备来选择只适用于当前设备的内容以供下载。比如,iPhone 6 只会下载 2x 分辨率的图片资源,iPhone 6plus 则只会下载 3x 分辨率的图片资源。

无用图片资源

图片资源的优化空间,主要体现在删除无用图片图片资源压缩这两方面

删除无用图片

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

如果你不想自己重新写一个工具的话,可以选择开源的工具直接使用。我觉得目前最好用的是LSUnusedResources

图片资源压缩

对于 App 来说,图片资源总会在安装包里占个大头儿。对它们最好的处理,就是在不损失图片质量的前提下尽可能地作压缩。目前比较好的压缩方案是,将图片转成 WebP。

代码瘦身

App 的安装包主要是由资源可执行文件组成的。

LinkMap 结合 Mach-O 找无用代码

LinkMap文件是Xcode产生可执行文件的同时生成的链接信息,用来描述可执行文件的构造成分,包括代码段__TEXT和数据段__DATA的分布情况

LinkMap 文件分为三部分:Object File、Section 和 Symbols

  • Object File 包含了代码工程的所有文件;
  • Section 描述了代码段在生成的 Mach-O 里的偏移位置和大小;
  • Symbols 会列出每个方法、类、block,以及它们的大小。

通过 LinkMap ,你不光可以统计出所有的方法和类,还能够清晰地看到代码所占包大小的具体分布,进而有针对性地进行代码优化

怎么通过 Mach-O 取到使用过的方法和类。

iOS 的方法都会通过 objc_msgSend 来调用。而,objc_msgSend 在 Mach-O 文件里是通过 __objc_selrefs 这个 section 来获取 selector 这个参数的。

  • 1、__objc_selrefs里是被调用过的方法
  • 2、__objc_classrefs 里是被调用过的类
  • 3、__objc_superrefs 是调用过 super 的类
  • 4、通过 __objc_classrefs 和 __objc_superrefs,我们就可以找出使用过的类和子类

Objective-C 是门动态语言,方法调用可以写成在运行时动态调用,这样就无法收集全所有调用的方法和类。所以,我们通过这种方法找出的无用方法和类就只能作为参考,还需要二次确认

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

我们要怎么检查出这些无用代码呢?通过 ObjC 的 runtime 源码,我们可以找到怎么判断一个类是否初始化过的函数,如下:


#define RW_INITIALIZED (1<<29)
bool isInitialized() {
   return getMeta()->data()->flags & RW_INITIALIZED;
}
复制代码

isInitialized 的结果会保存到元类的 class_rw_t 结构体的 flags 信息里,flags 的 1<<29 位记录的就是这个类是否初始化了的信息。而 flags 的其他位记录的信息


// 类的方法列表已修复
#define RW_METHODIZED         (1<<30)

// 类已经初始化了
#define RW_INITIALIZED        (1<<29)

// 类在初始化过程中
#define RW_INITIALIZING       (1<<28)

// class_rw_t->ro 是 class_ro_t 的堆副本
#define RW_COPIED_RO          (1<<27)

// 类分配了内存,但没有注册
#define RW_CONSTRUCTING       (1<<26)

// 类分配了内存也注册了
#define RW_CONSTRUCTED        (1<<25)

// GC:class有不安全的finalize方法
#define RW_FINALIZE_ON_MAIN_THREAD (1<<24)

// 类的 +load 被调用了
#define RW_LOADED             (1<<23)
复制代码

flags 采用位方式记录布尔值的方式,易于扩展、所用存储空间小、检索性能也好。所以,经常阅读优秀代码,特别有助于提高我们自己的代码质量。

既然能够在运行中看到类是否初始化了,那么我们就能够找出有哪些类是没有初始化的,即找到在真实环境中没有用到的类并清理掉。

4、Hook

1、AOP 方案的对比与思考

AOP 思想

  • AOP:Aspect Oriented Programming,译为面向切面编程,是可以通过预编译的方式和运行期动态实现,在不修改源代码的情况下,给程序动态统一添加功能的技术。
  • 面向对象编程(OOP)适合定义从上到下的关系,但不适用于从左到右,计算机中任何一门新技术或者新概念的出现都是为了解决一个特定的问题的,我们看下AOP解决了什么样的问题。

AOP主流方案

  • 1、Method Swizzle:使用Runtime交换方法的核心就是:method_exchangeImplementations, 它实际上将两个方法的实现进行交换
  • 2、Aspects:它利用Method Swizzling技术为已有的类或者实例方法添加额外的代码
  • 3、**ISA-swizzle KVO:**利用 KVO 的运行时 ISA-swizzle 原理,动态创建子类、并重写相关方法,并且添加我们想要的方法,然后在这个方法中调用原来的方法,从而达到 hook 的目的
  • 4、**Fishhook:**hook底层函数指针
  • 5、Thunk 技术:
  • 6、Clang 插桩:作为Xcode内置的编译器Clang其实是提供了一套插桩机制,这种依赖编译器做的AOP 方案,适用于与开发、测试阶段做一些检测工具,例如:代码覆盖、Code Lint、静态分析等。

iOS 中主流的 AOP 的方案和一些知名的框架,有编译期、链接期、运行时的,从源代码到程序装载到内存执行,整个过程的不同阶段都可以有相应的方案进行选择。

2、fishhook原理

method swizzle

原理很简单就是利用OC的runtime特性来动态修改objc_msgSend函数中的id/selector参数,来改变id与selector之间的对应关系,以交换id对应的method,从而实现hook,这也是为啥OC是一门动态语言。

fishhook

众所周知,C语言是一门静态语言,而静态语言在编译完成后变量、函数及其参数就已经确定,无法修改。但fishhook就能在程序运行时动态修改C函数

fishhook能够hookC函数的原因不外乎这几点:

  • 1、函数符号属于外部符号,位于动态链接库,进而位于数据段,只有数据段的内容才能被修改;
  • 2、dyld提供了获取镜像信息的接口,如获取Mach-O header、ASLR等,进而就可以获取懒加载符号表、非懒加载符号表、间接符号表、符号表、字符串表等符号相关的所有信息;
  • 3、通过遍历函数符号建立函数符号与函数名称的对应关系,就可以通过函数名称就可以找到最终的函数符号地址,就可以修改函数符号的内容来指向自己的实现。

fishhook为何只能hook动态链接库函数,不能hook自定义函数

  • 自定义函数在代码段,代码段具有只读可执行权限,因此fishhook无法hook自定义函数
  • fishhook能够hook动态链接库Foundation.framework中的NSLog,重要原因之一就是:函数符号位于数据段,只有数据段内容才能被修改!!!

如何确定函数符号地址

  • 懒加载符号表Lazy Symbol Pointer Table与间接符号表Indirect Symbol Table中的符号一一对应;
  • 间接符号表保存了函数符号在符号表Symbol Table中的偏移量;
  • 符号表中每项为struct nlist结构体,其中保存了函数符号在字符串表String Table中的偏移量;
  • 通过字符串表的偏移量就可以找到最终的函数符号对于的函数名称

如何hook底层objc_msgSend

  • 与fishhook框架类似,我们先要拥有hook的能力。
    • 首先,设计两个结构体:一个是用来记录符号的结构体,一个是用来记录符号表的链表
    • 其次,遍历动态链接器dyld内所有的image,取出其中的header和slide。以便我们接下来拿到符号表。
    • 我们在dyld内拿到了所有image。接下来,我们从image内找到符号表内相关的segment_command_t,遍历符号表找到所要替换的segname,再进行下一步方法替换
    • 最后,通过符号表以及我们所要替换的方法的实现,进行指针地址替换
  • 通过汇编语言编写出我们的hook_objc_msgSend方法
    • 因为objc_msgSend是通过汇编语言写的,我们想要替换objc_msgSend方法还需要从汇编语言下手

为什么hook了objc_msgSend就可以掌握所有objc方法的耗时

因为objc_msgSend是所有Objective-C方法调用的必经之路,所有的Objective-C方法都会调用到运行时底层的objc_msgSend方法。所以只要我们可以hook objc_msgSend,我们就可以掌握所有objc方法的耗时

5、Crash

1、Crash产生原因

  • 1、Mach异常  Mach:[mʌk],操作系统微内核,是许多新操作系统的设计基础。
  • 2、Objective-C 异常(NSException):NSSetUncaughtExceptionHandler(&HandleException) 设置未捕获的异常处理程序

一般是由 Mach异常或 Objective-C 异常(NSException)引起的。我们可以针对这两种情况抓取对应的 Crash 事件

  • 1、Mach异常是最底层的内核级异常,如EXC_BAD_ACCESS(内存访问异常)
  • 2、Unix Signal是Unix系统中的一种异步通知机制,Mach异常在host层被ux_exception转换为相应的Unix Signal,并通过threadsignal将信号投递到出错的线程
  • 3、 NSException是OC层,由iOS库或者各种第三方库或Runtime验证出错误而抛出的异常。如NSRangeException(数组越界异常)
  • 4、当错误发生时候,先在最底层产生Mach异常;Mach异常在host层被转换为相应的Unix Signal; 在OC层如果有对应的NSException(OC异常),就转换成OC异常,OC异常可以在OC层得到处理;如果OC异常一直得不到处理,程序会强行发送SIGABRT信号中断程序。在OC层如果没有对应的NSException,就只能让Unix标准的signal机制来处理了。
  • 5、在捕获Crash事件时,优选Mach异常。因为Mach异常处理会先于Unix信号处理发生,如果Mach异常的handler让程序exit了,那么Unix信号就永远不会到达这个进程了。而转换Unix信号是为了兼容更为流行的POSIX标准(SUS规范),这样就不必了解Mach内核也可以通过Unix信号的方式来兼容开发

2、异常捕获

1、Mach异常捕获

task_set_exception_ports(),设置内核接收Mach异常消息的Port,替换为自定义的Port后,即可捕获程序执行过程中产生的异常消息

mach异常即便注册了对应的处理,也不会导致影响原有的投递流程。此外,即便不去注册mach异常的处理,最终经过一系列的处理,mach异常会被转换成对应的UNIX信号,一种mach异常对应了一个或者多个信号类型。

2、signal捕获

当错误发生时候,先在最底层产生Mach异常;Mach异常在host层被转换为相应的Unix Signal; 在OC层如果有对应的NSException(OC异常),就转换成OC异常,OC异常可以在OC层得到处理;如果OC异常一直得不到处理,程序会强行发送SIGABRT信号中断程序。在OC层如果没有对应的NSException,就只能让Unix标准的signal机制来处理了

在signal.h中声明了32种异常信号,常见的有以下几种

  • 1、SIGILL    执行了非法指令,一般是可执行文件出现了错误
  • 2、SIGTRAP    断点指令或者其他trap指令产生
  • 3、SIGABRT    调用abort产生
  • 4、SIGBUS    非法地址。比如错误的内存类型访问、内存地址对齐等
  • 5、SIGSEGV    非法地址。访问未分配内存、写入没有写权限的内存等
  • 6、SIGFPE    致命的算术运算。比如数值溢出、NaN数值等

对各种信号都进行注册,捕获到异常信号后,在处理方法 handleSignalException 里通过 backtrace_symbols(回溯符号) 方法就能获取到当前的堆栈信息。堆栈信息可以先保存在本地,下次启动时再上传到崩溃监控服务器就可以了

void SignalExceptionHandler(int signal)
{
  NSMutableString *mstr = [[NSMutableString alloc] init];
  [mstr appendString:@"Stack:\n"];
  void* callstack[128];
  int i, frames = backtrace(callstack, 128);
  char** strs = backtrace_symbols(callstack, frames);
  for (i = 0; i <frames; ++i) {
    [mstr appendFormat:@"%s\n", strs[i]];
  }
  [SignalHandler saveCreash:mstr];

}

void InstallSignalHandler(void)
{
  signal(SIGHUP, SignalExceptionHandler);
  signal(SIGINT, SignalExceptionHandler);
  signal(SIGQUIT, SignalExceptionHandler);

  signal(SIGABRT, SignalExceptionHandler);
  signal(SIGILL, SignalExceptionHandler);
  signal(SIGSEGV, SignalExceptionHandler);
  signal(SIGFPE, SignalExceptionHandler);
  signal(SIGBUS, SignalExceptionHandler);
  signal(SIGPIPE, SignalExceptionHandler);
}
复制代码

3、NSException捕获

通过UncaughtExceptionHandler的实现

void HandleException(NSException *exception)
{
  // 异常的堆栈信息
  NSArray *stackArray = [exception callStackSymbols];
  // 出现异常的原因
  NSString *reason = [exception reason];
  // 异常名称
  NSString *name = [exception name];
  NSString *exceptionInfo = [NSString stringWithFormat:@"Exception reason:%@\nException name:%@\nException stack:%@",name, reason, stackArray];
  NSLog(@"%@", exceptionInfo);
  [UncaughtExceptionHandler saveCreash:exceptionInfo];
}

void InstallUncaughtExceptionHandler(void)
{
  NSSetUncaughtExceptionHandler(&HandleException);
}
复制代码

3、NSException异常

常见的NSException异常有

  • 1、unrecognized selector crash
  • 2、KVO crash
  • 3、NSNotification crash
  • 4、NSTimer crash
  • 5、Container crash(数组越界,插nil等)
  • 6、NSString crash (字符串操作的crash)
  • 7、Bad Access crash (野指针)
  • 8、UI not on Main Thread Crash (非主线程刷UI(机制待改善))

NSException信息

  • name :  用于唯一地识别异常的短字符串。名称是必需的
  • reason:一个更长的包含造成异常原因的“人类可读的”字符串。原因是必需的。
  • userInfo:主要当异常被抛出时,返回原因等信息的一个字典。

4、日志内容

整个日志内容中,直接和Crash信息相关,最能帮助开发者定位问题部分是: 异常信息 和 线程回溯部分的内容。日志主要分为六个部分:

  • 进程信息:发生Crash闪退进程的相关信息
    • Hardware Model: 标识设备类型。 如果很多崩溃日志都是来自相同的设备类型,说明应用只在某特定类型的设备上有问题
    • Process是应用名称。中括号里面的数字是闪退时应用的进程ID。
  • 基本信息:给出了一些基本信息,包括闪退发生的日期和时间,设备的iOS版本。
  • 异常信息:闪退发生时抛出的异常类型。还能看到异常编码和抛出异常的线程。
    • Exception Type异常类型:通常包含1.7中的Signal信号和EXC_BAD_ACCESS,NSRangeException等。
    • Exception Codes:异常编码:
    • Crashed Thread:发生Crash的线程id
  • 线程回溯:回溯是闪退发生时所有活动帧清单。它包含闪退发生时调用函数的清单。
  • 线程状态:闪退时寄存器中的值。一般不需要这部分的信息,因为回溯部分的信息已经足够让你找出问题所在
  • 二进制映像:闪退时已经加载的二进制文件。

5、符号表

号表是内存地址与函数名、文件名、行号的映射表。符号表元素如下所示:
<起始地址> <结束地址> <函数> [<文件名:行号>]

为什么要配置符号表?

为了能快速并准确地定位用户APP发生Crash的代码位置,Bugly使用符号表对APP发生Crash的程序堆栈进行解析和还原

iOS平台中,dSYM文件是指具有调试信息的目标文件,文件名通常为:xxx.app.dSYM。

为了方便找回Crash对应的dSYM文件和还原堆栈,建议每次构建或者发布APP版本的时候,备份好dSYM文件

6、程序编译过程

1596080805341-cd39ccc5-3261-444b-bfdd-1694c8af0f4a.png

1、LLVM & Clang

LLVM 其实是一系列的编译组件的集合。而 Clang (标准读法是克朗) 是作为其中的前端。

1596078270391-7b423b1e-da61-4883-8e68-7aa17ca75e94.png

  • **编译器前端 Front End:**编译器前端的任务是解析源代码,具体工作内容包括下列三个流程
    • 词法分析
    • 语法分析
    • 语义分析

检查源代码是否存在错误,然后构建抽象语法树(Abstract Syntax Tree, AST)。 LLVM的前端还会生成中间代码(intermediate representation, IR)。

  • **优化器 Optimizer:**优化器负责进行各种优化。改善代码的运行时间,例如消除冗余计算等。
  • **编译器后端 Back End:**将代码映射到目标指令集。生成机器语言,并且进行机器相关的代码优化

iOS 中的编译过程

一共分为 7 大阶段。

  • input:输入阶段,表示将 main.m 文件输入,文件格式是 OC
  • preprocessor:预处理阶段,这个过程包括宏的替换,头文件的导入
  • compiler:编译阶段,进行词法分析、语法分析、语义分析,最终生成 IR
  • backend:后端,LLVM 会通过一个一个的 Pass 去优化,最终生成汇编代码。
  • assembler:汇编,生成目标文件
  • linker:链接,链接需要的动态库和静态库,生成可执行文件
  • bind-arch:架构绑定,通过不同的架构,生成对应的可执行文件

2、符号是怎么绑定到地址上的?

解释器 &编译器

你是不是经常会好奇自己参与的这么些项目,为什么有的编译起来很快,有的却很慢;编译完成后,有的启动得很快,有的却很慢。其实,在理解了编译和启动时链接器所做的事儿之后,你就可以从根儿上找到这些问题的答案了。

  • 解释器:运行时才去解析代码
    • 解释器会在运行时解释执行代码,获取一段代码后就会将其翻译成目标代码(就是字节码(Bytecode)),然后一句一句地执行目标代码。解释器,是在运行时才去解析代码,这样就比在运行之前通过编译器生成一份完整的机器码再去执行的效率要低。
    • 解释器可以在运行时去执行代码,说明它具有动态性,程序运行后能够随时通过增加和更新代码来改变程序的逻辑。
  • 编译器:编译时,编译器把代码编译成机器码,然后直接在 CPU 上执行机器码的
    • 链接器:最主要的作用,就是将符号绑定到地址上

那么,使用编译器和解释器执行代码的特点,我们就可以概括如下

  • 采用编译器生成机器码执行的好处是效率高,缺点是调试周期长。
  • 解释器执行的好处是编写调试方便,缺点是执行效率低。

编译器会对每个文件进行编译,生成 Mach-O(可执行文件);链接器会将项目中的多个 Mach-O 文件合并成一个。

链接器为什么还要把项目中的多个 Mach-O 文件合并成一个

你肯定不希望一个项目是在一个文件里从头写到尾的吧。项目中文件之间的变量和接口函数都是相互依赖的,所以这时我们就需要通过链接器将项目中生成的多个 Mach-O 文件的符号和地址绑定起来。

没有这个绑定过程的话,单个文件生成的 Mach-O 文件是无法正常运行起来的。因为,如果运行时碰到调用在其他文件中实现的函数的情况时,就会找不到这个调用函数的地址,从而无法继续执行

链接器对代码主要做了哪几件事儿。

  • 1、去项目文件里查找目标代码文件里没有定义的变量。
  • 2、扫描项目中的不同文件,将所有符号定义和引用地址收集起来,并放到全局符号表中。
  • 3、计算合并后长度及位置,生成同类型的段进行合并,建立绑定
  • 4、对项目中不同文件里的变量进行地址重定位

链接器在整理函数的调用关系时,会以 main 函数为源头,跟随每个引用,并将其标记为 live。跟随完成后,那些未被标记 live 的函数,就是无用函数。然后,链接器可以通过打开 Dead code stripping 开关,来开启自动去除无用代码的功能。并且,这个开关是默认开启的。

动态库链接

链接的共用库分为静态库和动态库

  • 1、静态库是编译时链接的库
    • 需要链接进你的 Mach-O 文件里,如果需要更新就要重新编译一次,无法动态加载和更新
  • 2、动态库是运行时链接的库,使用 dyld 就可以实现动态加载。

Mach-O 文件是编译后的产物,而动态库在运行时才会被链接,并没参与 Mach-O 文件的编译和链接

所以 Mach-O 文件中并没有包含动态库里的符号定义。也就是说,这些符号会显示为“未定义”,但它们的名字和对应的库的路径会被记录下来。运行时通过 dlopen 和 dlsym 导入动态库时,先根据记录的库路径找到对应的库,再通过记录的名字符号找到绑定的地址。

dlopen 会把共享库载入运行进程的地址空间,载入的共享库也会有未定义的符号,这样会触发更多的共享库被载入。dlopen 也可以选择是立刻解析所有引用还是滞后去做**。dlopen 打开动态库后返回的是引用的指针,dlsym 的作用就是通过 dlopen 返回的动态库指针和函数符号,得到函数的地址然后使用**。

简单来说, dyld 做了这么几件事儿:

  • 1、先执行 Mach-O 文件,根据 Mach-O 文件里 undefined 的符号加载对应的动态库,系统会设置一个共享缓存来解决加载的递归依赖问题
  • 2、加载后,将 undefined 的符号绑定到动态库里对应的地址上;
  • 3、最后再处理 +load 方法,main 函数返回后运行 static terminator

3、App 如何通过注入动态库的方式实现极速编译调试

Injection 会监听源代码文件的变化,如果文件被改动了,Injection Server 就会执行 rebuildClass 重新进行编译、打包成动态库,也就是 .dylib 文件。编译、打包成动态库后使用 writeSting 方法通过 Socket 通知运行的 App

1595244088796-72b8fc68-9d44-47d7-bc29-e3d598b307aa.png

4、静态分析工具

随着项目的扩大,依靠人工codereview来保证项目的质量,越来越不现实,这时就有必要借助于一种自动化的代码审查工具:程序静态分析

程序静态分析(Program Static Analysis)是指在不运行代码的方式下,通过词法分析、语法分析、控制流、数据流分析等技术对程序代码进行扫描,验证代码是否满足规范性、安全性、可靠性、可维护性等指标的一种代码分析技术。

词法分析,语法分析等工作是由编译器进行的,所以对iOS项目为了完成静态分析,我们需要借助于编译器。对于OC语言的静态分析可以完全通过Clang,对于Swift的静态分析除了Clange还需要借助于SourceKit
Swift语言对应的静态分析工具是SwiftLint,OC语言对应的静态分析工具有Infer和OCLitn

5、静态库、动态库的区别

库的本质是可执行的二进制文件,是资源文件和代码编译的一个集合。根据链接方式不同,可以分为动态库和静态库,其中系统提供的库都属于动态库

  • 1、什么是库?
    – 库 就是程序代码的集合, 是共享程序代码的一种方式
  • 2、库的分类?
    • 开源库
      • 公开源代码, 能看到具体实现
      • 例如MJExtension, MJRefresh, AFNetworking…
    • 闭源库
      • 不公开源代码, 是经过编译后的二进制文件, 看不到具体实现
      • 主要分为: 静态库 和 动态库
  • 3、静态库的存在形式?
    • .a
    • .framework
  • 4、动态库的存在形式?
    • .dylib
    • .tbd
    • .framework
  • 5、静态库和动态库的区别?
    • 静态库在链接时, 会被完整的复制到可执行文件中; 被多次使用, 就有多份拷贝;
    • 动态库则不会复制, 只有一份. 程序运行时动态加载到内存; 系统只加载一次, 多个程序共用, 节省内存;
    • 但是!!!! 项目中如果使用到自己的动态库, 不允许上架!
    • 再但是!!! WWDC2014上公布的 苹果对ios8开放动态加载dylib的接口 也就是说 开放了动态库挂载
  • 6、静态库应用场景?
    • 保护自己的核心代码
    • 国内的企业,掌握有核心技术,同时是又希望更多的程序员来使用其技术,因此采用”闭源”的方式开发使用
    • 例如:百度地图,友盟,JPush等
    • 将MRC的项目,打包成静态库, 可以在ARC下直接使用, 不需要转换
    • 提高工程的编译速度
    1. 静态库的特点?
    • .a + .h
    • 看不到具体实现的代码

静态库形式: .a和.framework

  • 1.静态库在编译时加载,链接时会完整的复制到可执行文件中。
  • 2.静态库的可执行文件通常会比较大,因为所需的数据都会被整合到目标代码中,因此编译后的执行文件不需要外部库的支持,直接就能使用。
  • 3.有多个app使用就会被复制多份,不能共享且占用更多冗余内存。
  • 4.所有的函数都在库中,因此当修改函数时需要重新编译。

.a 和 .framework 的区别
.a 是单纯的二进制文件,.framework是二进制文件+资源文件。
其中.a 不能直接使用,需要 .h文件配合,而.framework则可以直接使用。
.framework = .a + .h + sorrceFile(资源文件)

动态库形式: .dylib和.framework

  • 1.动态库在程序运行时由系统动态加载到内存,供程序调用,如果环境缺少动态库或者库的版本不正确,就会导致程序无法运行。
  • 2.动态库的文件会比较小,因为在编译过程中,数据并没有整合到目标代码中,只有在执行到该函数时才去调用库中的函数,所以首次加载时比较耗时。
  • 3.多个程序可以共享内存中同一份库资源,系统只加载一次,多个程序可共用,节省内存空间。
  • 4.库是动态的,因此修改库中函数时,不需要重新编译

7、App启动优化

1、名词解释

1、Mach-O

Mach-O 是 iOS 系统不同运行时期可执行的文件的文件类型统称。主要分以下三类:

  • Executable([ˌeksɪˈkjuːtəbl]可执行的;可实行的)- 可执行文件,是 App 中的主要二进制文件
  • Dylib– 动态库,在其他平台也叫 DSO 或者 DLL
  • Bundle– 苹果平台特有的类型,是无法被连接的 Dylib。只能在运行时通过 dlopen() 加载

Mach-O 的基本结构分为三个部分:

  • Header包含了 Mach-O 文件的基本信息,如 CPU 架构,文件类型,加载指令数量等
  • Load Commands是跟在 Header 后面的加载命令区,包含文件的组织架构和在虚拟内存中的布局方式,在调用的时候知道如何设置和加载二进制数据
  • Data包含 Load Commands 中需要的各个 Segment 的数据。

绝大多数 Mach-O 文件包括以下三种 Segment:

  • __TEXT– 代码段,包括头文件、代码和常量。只读不可修改。
  • __DATA– 数据段,包括全局变量, 静态变量等。可读可写。
  • __LINKEDIT– 如何加载程序, 包含了方法和变量的元数据(位置,偏移量),以及代码签名等信息。只读不可修改。

2、Image

指的是 Executable,Dylib 或者 Bundle 的一种

3、虚拟内存(Virtual Memory)

虚拟内存是建立在物理内存和进程之间的中间层。是一个连续的逻辑地址空间,而且逻辑地址可以没有对应的实际物理内存地址,也可以让多个逻辑地址对应到一个物理内存地址上。

4、地址空间布局随机化(ASLR)

ASLR(Address Space Layout Randomization)
当 Image 加载到逻辑地址空间的时候,系统会利用 ASLR 技术,使得 Image 的起始地址总是随机的,以避免黑客通过起始地址+偏移量找到函数的地址

5、代码签名(Code Sign)

代码签名可以让 iOS 系统确保要被加载的 Image 的安全性,用 Code Sign 设置签名时,每页内容都会生成一个单独的加密散列值,并存储到__LINKEDIT中去,系统在加载时会校验每页内容确保没有被篡改。

6、dyld

dyld 是 iOS 上的二进制加载器,用于加载 Image。

7、Load dylibs

dyld 在加载 Mach-O 之前会先解析 Header 和 Load Commands, 然后就知道了这个 Mach-O 所依赖的 dylibs,以此类推,通过递归的方式把全部需要的 dylib 都加载进来。
一般来说,一个 App 所依赖的 dylib 在 100 – 400 左右,其中大多数都是系统的 dylib,因为有缓存和共享的缘故,读取速度比较高。

8、Fix-ups

因为 ASLR 和 Code Sign 的原因,刚被加载进来的 dylib 都处于相对独立的状态,为了把它们绑定起来,需要经过一个 Fix-ups 过程。Fix-ups 主要有两种类型:Rebase 和 Bind

rebase修复的是指向当前镜像内部的资源指针; 而bind指向的是镜像外部的资源指针。

Rebase
Rebase 就是针对“因为 ASLR 导致 Mach-O 在加载到内存中是一个随机的首地址”这一个问题做一个数据修正的过程。会将内部指针地址都加上一个偏移量。

所有需要 Rebase 的指针信息已经被编码到__LINKEDIT里。然后就是不断重复地对__DATA中需要 Rebase 的指针加上这个偏移量。这个过程中可能会不断发生 Page Fault 和 COW,从而导致 I/0 的性能损耗问题,不过因为 Rebase 处理的是连续地址,所以内核会预先读取数据,减少 I/O 的消耗

Binding
Binding 就是对调用的外部符号进行绑定的过程。比如我们要使用到UITableView,即符号_OBJC_CLASS_$_UITableView, 但这个符号又不在 Mach-O 中,需要从 UIKit.framework 中获取,因此需要通过 Binding 把这个对应关系绑定到一起。

在运行时,dyld 需要找到符号名对应的实现。而这需要很多计算,包括去符号表里找。找到后就会将对应的值记录到__DATA的那个指针里。Binding 的计算量虽然比 Rebasing 更多,但实际需要的 I/O 操作很少,因为之前 Rebasing 已经做过了

2、App启动

启动时间优化分成两部分

  • T1:main()函数之前,即操作系统加载App可执行文件到内存,然后执行一系列的加载&链接等工作,最后执行至App的main()函数。
  • T2:main()函数之后,即从main()开始,到appDelegate的didFinishLaunchingWithOptions方法执行完毕。

main()函数之前

1592979855742-1d50d96a-1e11-4b02-8f15-71ce2e2eff23.png

  • 1、dyld:加载镜像,动态库
    • 1、加载动态:把App对应的可执行文件加载到内存。
    • 2、加载动态链接库rebase指针调整和bind符号绑定
  • 2、RunTime方法
    • 1、Objc setup
      • 1、注册Objc类 (class registration)
      • 2、把category的定义插入方法列表 (category registration)
      • 3、保证每一个selector唯一 (selector uniquing)
    • 2、Initializers
      • 1、Objc的+load()函数

      • 2、C++的构造函数属性函数

      • 3、非基本类型的C++静态全局变量的创建(通常是类或结构体)

1、加载动态优化

通常的,一个App需要加载100到400个dylibs, 但是其中的系统库被优化,可以很快的加载。 针对这一步骤的优化有:

  • 1、减少非系统库的依赖
  • 2、合并非系统库
  • 3、使用静态资源,比如把代码加入主程序

2、rebase/bind
优化该阶段的关键在于减少__DATA segment中的指针数量。我们可以优化的点有:

  • 1、减少Objc类数量, 减少selector数量
  • 2、减少C++虚函数数量
  • 3、转而使用swift stuct(其实本质上就是为了减少符号的数量)

3**、Objc setup**
这一步主要工作是:

  • 1、注册Objc类 (class registration)
  • 2、把category的定义插入方法列表 (category registration)
  • 3、保证每一个selector唯一 (selctor uniquing)

4、initializers

以上三步属于静态调整(fix-up),都是在修改__DATA segment中的内容,而这里则开始动态调整,开始在堆和堆栈中写入内容。 在这里的工作有:

  • 1、Objc的+load()函数
  • 2、C++的构造函数属性函数 形如attribute((constructor)) void DoSomeInitializationWork()
  • 3、非基本类型的C++静态全局变量的创建(通常是类或结构体)(non-trivial initializer) 比如一个全局静态结构体的构建,如果在构造函数中有繁重的工作,那么会拖慢启动速度

总结对于main()调用之前的耗时我们可以优化的点有

  • 1、减少不必要的framework,因为动态链接比较耗时
  • 2、check framework应当设为optional和required,如果该framework在当前App支持的所有iOS系统版本都存在,那么就设为required,否则就设为optional,因为optional会有些额外的检查
  • 3、合并或者删减一些OC类,关于清理项目中没用到的类
  • 4、删减没有被调用到或者已经废弃的方法
  • 5、将不必须在+load方法中做的事情延迟到+initialize中
  • 6、尽量不要用C++虚函数(创建虚函数表有开销)

main阶段

其实在main()函数之前我们能够进行优化的部分并没有多少,而且操作性也不大,更多的优化其实还是我们对我们代码的优化,很多启动时间的占用更多的是因为我们代码布局的问题

相当一部分的项目都是所有的启动项不加分类一股脑的堆积在了didFinishLaunch中,其实这是相当不合理的。
通过对SDK的梳理和分析,我们发现启动项也需要根据所完成的任务被分类,有些启动项是需要刚启动就执行的操作,如Crash监控、统计上报等,否则会导致信息收集的缺失;有些启动项需要在较早的时间节点完成,例如一些提供用户信息的SDK、定位功能的初始化、网络初始化等;有些启动项则可以被延迟执行,如一些自定义配置,一些业务服务的调用、支付SDK、地图SDK等。我们所做的分阶段启动,首先就是把启动流程合理地划分为若干个启动阶段,然后依据每个启动项所做的事情的优先级把它们分配到相应的启动阶段,优先级高的放在靠前的阶段,优先级低的放在靠后的阶段。

10、架构

1、架构

App确实就是主要做这些事情,但是支撑这些事情的基础,就是做架构要考虑的事情。

  • 调用网络API
  • 页面展示
  • 数据的本地持久化
  • 动态部署方案

上面这四大点,稍微细说一下就是:

  • 如何让业务开发工程师方便安全地调用网络API?然后尽可能保证用户在各种网络环境下都能有良好的体验?
  • 页面如何组织,才能尽可能降低业务方代码的耦合度?尽可能降低业务方开发界面的复杂度,提高他们的效率?
  • 当数据有在本地存取的需求的时候,如何能够保证数据在本地的合理安排?如何尽可能地减小性能消耗?
  • iOS应用有审核周期,如何能够通过不发版本的方式展示新的内容给用户?如何修复紧急bug?

上面几点是针对App说的,下面还有一些是针对团队说的:

  • 收集用户数据,给产品和运营提供参考
  • 合理地组织各业务方开发的业务模块,以及相关基础模块
  • 每日app的自动打包,提供给QA工程师的测试工具

2、MVC&MVVM&MVP

MVC

  • M应该做的事:
    • 给ViewController提供数据
    • 给ViewController存储数据提供接口
    • 提供经过抽象的业务基本组件,供Controller调度
  • C应该做的事:
    • 管理View Container的生命周期
    • 负责生成所有的View实例,并放入View Container
    • 监听来自View与业务有关的事件,通过与Model的合作,来完成对应事件的业务。
  • V应该做的事:
    • 响应与业务无关的事件,并因此引发动画效果,点击反馈(如果合适的话,尽量还是放在View去做)等。
    • 界面元素表达

在iOS开发领域内,UIViewController承载了非常多的事情,比如View的初始化,业务逻辑,事件响应,数据加工等等,当然还有更多我现在也列举不出来,但是我们知道有一件事情Controller肯定逃不掉要做:协调V和M。也就是说,不管怎么拆,协调工作是拆不掉的。

Controller瘦身

主要关注问题就是Controller瘦身

  • 1、模块化
    • 生命周期
    • 通知
    • 方法
    • 代理
    • UI
    • set&get方法
    • 路由
  • 2、做分类
    • 1、基础Controller负责界面布局
    • 2、网络数据层
    • 3、事件层

不能忽略的问题:单元测试

在进行单元测试之前,Controller臃肿问题我们是可以解决的,但是当我们需要单元测试的时候,我们就可以发现问题
由于视图控制器与视图紧密耦合,因此很难测试——因为在编写视图控制器的代码时,你必须模拟View的生命周期,从而使你的业务逻辑尽可能地与View层的代码分隔开来。

MVP

MVP模式    是为了解决MVC的Controller越来越臃肿的问题,进一步明确代码的分工
M-model  V-ViewController  P-Presenter (主持人)

MVC的缺点在于并没有区分业务逻辑和业务展示, 这对单元测试很不友好. MVP针对以上缺点做了优化, 它将业务逻辑和业务展示也做了一层隔离, 对应的就变成了MVCP.

M和V功能不变, 原来的C现在只负责布局, 而所有的业务逻辑全都转移到了P层。P层处理完了业务逻辑,如果要更改view的显示,那么可以通过回调来实现,这样可以减轻耦合,同时可以单独测试P层的业务逻辑

MVVM

MVVM是由微软提出来的,MVVM其实是在MVP的基础上发展起来的。那么MVVM在MVP的基础上改良了啥呢?答案就是数据绑定

MVVM其实就是M-VM-C-V

MVVM和MVP相对于MVC最大的改进在于:P或者VM创建了一个视图的抽象,将视图中的状态和行为抽离出来形成一个新的抽象。这可以把业务逻辑(P/VM)和业务展示(V)分离开单独测试,并且达到复用的目的,逻辑结构更加清晰

MVVM各层的职责和MVP的类似,VM对应P层,只是在MVVM的View层多了数据绑定的操作

为了让View和ViewModel之间能够有比较松散的绑定关系,于是我们使用ReactiveCocoa,KVO,Notification,block,delegate和target-action都可以用来做数据通信,从而来实现绑定,但都不如ReactiveCocoa提供的RACSignal来的优雅,如果不用ReactiveCocoa,绑定关系可能就做不到那么松散那么好,但并不影响它还是MVVM

3、组件化

2C68ED23-B706-4E53-9B82-304E982D6AF9.png

谈到组件化,首先想到的是解耦,模块化.其实组件化就是将模块化抽离,分层,并制定模块间的通讯方式,从而实现解耦的一种方式,主要运用在团队开发.

为什么需要组件化

主要有以下四个原因:

  • 1.模块间解耦
  • 2.模块重用
  • 3.提高团队协作开发效率
  • 4.方便进行单元测试

组件化方案

目前常用的组件化方案主要有两种:

  • 1·本地组件化:主要是通过在工程中创建library,利用cocoapods的workspec进行本地管理,不需要将项目上传git,而是直接在项目中以framework的方式进行调用
  • 2.cocoapods组件化:主要是利用cocoapods来进行模块的远程管理,需要将项目上传git(这里的组件化模块分为公有库和私有库,对公司而言,一般是私有库)

组件化通讯方案

目前主流的主要有以下三种方式:

  • 1.url scheme路由
  • 2.target-action
  • 3.protocol匹配

1、Url-scheme注册(MGJRouter)

iOS系统中默认是支持url scheme方式的,例如可以在浏览器中输入:weixin://就可以打开微信应用。自然在APP内部也可以通过这种方法来实现组件之间的路由设计。

这种方式实现的原理是在APP启动的时候,或者是向以下实例中的每个模块自己的load方法里面注册自己的断链(url),以及对外提供服务(Block),通过url-scheme标记好,然后维护在url-router里面。url-router中保存了各个组件对应的url-scheme,只要其它组件调用了open url的方法,url-router就会去根据url去查找对应的服务并执行

2、target-action

这个方案是基于OC的runtime、category特性动态获取模块,例如通过NSClassFromString获取类并创建实例,通过performSelector+NSInvocation动态调用方法
这种方式主要是以casatwy的CTMediator为代表,其实现思路是:

  • 1.利用分类为路由添加新的接口,在接口通过字符串获取对应的类
  • 2.通过runtime创建实例,动态调用实例的方法

3、protocol class
protocol匹配的实现思路是:

  • 1.将protocol和对应的类进行字典匹配
  • 2.通过用protocol获取class,再动态创建实例

protocol比较典型的三方框架就是阿里的BeeHive.BeeHive借鉴了Spring Service、Apache DSO的架构理念,采用AOP+扩展App生命周期API形式,将业务功能、基础功能模块以模块方式解决大型应用中的复杂问题,并让模块之间以Service形式调用,将复杂问题切分,以AOP方式模块化服务.

4、设计模式

在软件开发中,经过验证的,用于解决在特定环境下,重复出现的特定的问题的解决方案

1、面向对象设计的六大设计原则SOLID

面向对象设计的六大设计原则

  • 1、单一职责:一个类只做一种类型责任,当这个类需要承当其他类型的责任的时候,就需要分解这个类。不过在现实开发中,这个原则是最不可能遵守的,因为每个人对一个类的哪些功能算是同一类型的职责判断都不相同。
  • 2、开放封闭原则:软件实体应该是可扩展,而不可修改的。也就是说,你写完一个类,要想添加功能,不能修改原有类,而是想办法扩展该类。有多种设计模式可以达到这一要求。
  • 3、里氏替换原则:当一个子类的实例应该能够替换任何其超类的实例时,它们之间才具有is-A关系。也就是说接口或父类出现的地方,实现接口的类或子类可以代入,这主要依赖于多态和继承。
  • 4、迪米特法则(最少知道原则):一个对象应该对尽可能少的对象有接触,也就是只接触那些真正需要接触的对象
  • 5、接口分离原则:不能强迫用户去依赖那些他们不使用的接口。换句话说,使用多个专门的接口比使用单一的总接口总要好。 不要提供一个大的接口包括所有功能,应该根据功能把这些接口分割,减少依赖
  • 6、依赖倒置原则:高层模块不应该依赖于低层模块,二者都应该依赖于抽象;抽象不应该依赖于细节,细节应该依赖于抽象

2、设计模式

常见的设计模式有23种,根据目的,我们可以把模型分为三类:创建型,结构型,行为型

  • 1、创建型设计模式:创建型模式与对象的创建有关
  • 2、结构型设计模式:结构型模式处理类和对象的组合
  • 3、行为型设计模式:行为型设计模式对类或对象怎样交互和怎么分配职责进行描述

1、创建型模式

1、工厂模式
工厂模式主要解决根据不同条件创建不同的对象

正常情况下:

if-else逻辑、创建逻辑和业务代码耦合在一起

根据创建逻辑和if-else被转移到的位置,可以分为:
  • 简单工厂模式将不同创建逻辑放到一个工厂类中if-else逻辑在这个工厂类中
  • 工厂模式先用一个工厂类的工厂通过if-else来得到某个工厂再用这个工厂用单一创建逻辑对应的对象
对于很复杂(产品不再只有一种分类方式)的逻辑:
  • 抽象工厂

案例
UITableViewCell注册问题

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{

    return [FactoryCell configUI:indexPath.row withTableView:tableView];
}
复制代码

2、结构型模式

  • 1、适配器模式

适配器模式(Adapter Pattern) :将一个接口转换成客户希望的另一个接口,使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。适配器模式的别名是包装器模式(Wrapper),是一种结构型设计模式

安卓这个模式用的比较多,里面有一个adapter适配器,专门来适配不同类型的itemView

  • 2、代理模式

Proxy [ˈprɒksi]

为其他对象提供一种代理来提供对这个对象的控制访问

其实iOS已经内置了代理的实现,我们只需要使用NSProxy类的两个方法就可以实现代理模式的功能。

-(void)forwardInvocation:(NSInvocation *)anInvocation

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;
复制代码

3、行为型模式

策略模式

定义一族算法类,将每个算法分别封装起来,让它们可以互相替换。策略模式可以使算法的变化独立于使用它们的客户端(这里的客户端代指使用算法的代码)

策略模式表面上看是为了避免 if-else 分支判断逻辑,但更深层次上,还是为了解耦以及控制代码复杂度

我们在网上购买商品的时候,经常遇到各种打折优惠活动,不同的节假日或者时间优惠策略都不相同,如果让我们去实现,那么如何做呢?

常规做法是根据不同的优惠政策,使用if进行判断,写很多判断分支进行处理

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享