正在加载今日诗词....
15 min read

iOS 天问 - 第1讲

每天都要有思考 iOS每日一题 今天是....
iOS 天问 - 第1讲

图片的显示内存大小由什么决定?

1.4decode.png-0f6c23ab-78a7-458f-b735-25eda1da2bf7-1542267659520-22717176

  • 图片的显示占用内存与图片的硬盘占用大小, 其质量没有关系, 仅仅和其本身的分辨率以及颜色占用字节有关.
ImageBuffer  = widthOfImage * heightOfImage * 4; // in pixel not in point

图片显示占用内存大小 = 图片的宽度 乘以 图片的高度 乘以 颜色 RGBA 占用的4个字节;

  • ① 之所有图片的大小与硬盘的占用大小无关是因为硬盘中的图片都是以不同的容器格式被编码了的, 如png, jpeg 等格式, 这些不能被用来直接显示;
  • ② 能够被用来直接显示的只有一种格式就是 bitmap 位图的方式. 要从硬盘中的容器格式变成位图,就会经历 图片解码的过程. 而解码后的位图大小计算方式如上

记录于2018/11/11

大图片的加载经常会出现什么现象?

OOM

2.3renderprocessusingdraw.png-3365a078-7a1c-4123-90ea-a1ad93cc7eb4-1542266952709-53914257

  • iOS 的图片加载方式一般使用的方法有 imageWithName initWithContentOfFile imageWithData 等方式, 当我们调用这些方法时, 系统会自动的将这些从不同途径获取到的图片进行 主线程CPU图片解码 从而让图片正常显示出来.
  • 少量的图片或者说小图, 对于高配置的手机来说系统默认的解码显示方式没有什么问题.
  • 但是如果在列表中快速加载高分辨率的大图,那么就会造成内存暴涨,从而 OOM 导致手机闪退.
  • 业界常常使用的 SDWebImage 被用来快速加载网络图片, 其底层会对大部分图片进行显示前的解码操作,而不是等系统自己去解码,从而降低了 CPU 的负荷,让主线程快速响应任务.
  • 但是 SDWebImage 有个缺陷是 对于大图的一次性解压操作会使内存暴增,导致闪退.
  • 优化方案有,①如果是列表展示占位图,可以重新绘制一个小分辨率的图片用来显示 ② 使用 GPU 分片解码的方式,从而使内存不会出现一次性的暴涨.

当然也有人说可以关闭图片的异步解码

[[[SDImageCache sharedImageCache] config] setShouldDecompressImages:NO];
[[SDWebImageDownloader sharedDownloader] setShouldDecompressImages:NO];

当然我不建议这样处理. if you set this to false, the decompression will be done by the UIKit in main thread which will cause a drop in FPS

记录于 2018/11/11

组件化时 podfile 中的 use_frameworks 意味着什么,使用与不使用的区别

swift静态库迁移

记录于 2018/11/12

iOS 中的 IMP _cmd SEL Method isa 是什么, 在开发中可以怎么使用?

首先官方文档


/// An opaque type that represents an Objective-C class.
typedef struct objc_class *Class;

/// Represents an instance of a class.
struct objc_object {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
};

/// A pointer to an instance of a class.
typedef struct objc_object *id;
#endif

/// An opaque type that represents a method selector.
typedef struct objc_selector *SEL;

/// A pointer to the function of a method implementation. 
#if !OBJC_OLD_DISPATCH_PROTOTYPES
typedef void (*IMP)(void /* id, SEL, ... */ ); 
#else
typedef id _Nullable (*IMP)(id _Nonnull, SEL _Nonnull, ...); 
#endif

  • opaque 含义: opaque 直译的意思是不透明的,C 语言中允许通过 typedef 申明一个抽象的结构体类型,你无需定义struct __opaque的具体实现,就能在其他函数的声明中使用该数据类型的 _指针_
  • OC 的基础 Class id IMP 通通都是结构体指针, 也是 Objective-CC 语言超集的原因
  • Class 是 类的结构体 objc_class 的指针
  • id 是 对象的结构体 objc_object 的指针
  • SEL 是 方法选择子结构体 objc_selector 的指针
    • 它并不是方法的签名, 方法的签名 NSMethodSignature 要结合 class 存在, 不同的类可以由 相同的 SEL, 但是不能有 相同的方法签名.
  • _cmd, 代表当前方法的 selector, 也就是Method 中的 IMP 真正关联的 SEL
  • IMP 是 方法实现函数 void (*)(void /* id, SEL, ... */ ) 的别名, 类似 Block 的感觉
    • IMP是一个函数指针,指向的是函数的具体实现的内存地址。在runtime中消息传递和转发的目的就是为了找到IMP,并执行函数 , 即在运行时被决定, 因此有效率问题(相对于静态语言).
    • OBJC_OLD_DISPATCH_PROTOTYPES 是一个编译选项, 我们可以在 Xcode 设置 YES or NO 这样 IMP 就会有不同的解释, 如果是 YES 那么 我们使用 IMP 时就要注意两个 不为空的参数问题
    • 直接调用 IMP 肯定要比完全走一遍 runtime 要快得多, 不过也少了 runtime很多的黑科技

另附上摘抄自method-swizzling

  • Selector(typedef struct objc_selector *SEL):在运行时 Selectors 用来代表一个方法的名字。Selector 是一个在运行时被注册(或映射)的C类型字符串。Selector由编译器产生并且在当类被加载进内存时由运行时自动进行名字和实现的映射。
  • Method(typedef struct objc_method *Method):方法是一个不透明的用来代表一个方法的定义的类型。
  • Implementation(typedef id (*IMP)(id, SEL,...)):这个数据类型指向一个 方法的实现的最开始的地方。该方法为当前CPU架构使用标准的C方法调用来实现。该方法的第一个参数指向调用方法的自身(即内存中类的实例对象,若是调用类方法,该指针则是指向元类对象 metaclass )。第二个参数是这个方法的名字 selector,该方法的真正参数紧随其后。

isa

那么后面看看 isa 的秘密吧 ,这里会涉及到 类与对象的关系


struct objc_object {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
};

struct objc_class {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY; // isa 指针

#if !__OBJC2__
    Class _Nullable super_class                             OBJC2_UNAVAILABLE; //父类的指针 
    const char * _Nonnull name                              OBJC2_UNAVAILABLE;  //类名
    long version                                             OBJC2_UNAVAILABLE; // 版本号
    long info                                                OBJC2_UNAVAILABLE;
    long instance_size                                     OBJC2_UNAVAILABLE; //实例变量的大小, 所有的实例变量 
    struct objc_ivar_list * _Nullable ivars                  OBJC2_UNAVAILABLE; // 实例变量的链表
    struct objc_method_list * _Nullable * _Nullable methodLists                    OBJC2_UNAVAILABLE; // 方法的链表, 分类的实现逻辑即动态向其中添加方法
    struct objc_cache * _Nonnull cache                       OBJC2_UNAVAILABLE; // 方法缓存的链表
    struct objc_protocol_list * _Nullable protocols          OBJC2_UNAVAILABLE; // 协议的链表
#endif

} OBJC2_UNAVAILABLE;
/* Use `Class` instead of `struct objc_class *` */

按照顺序来看, 显示一个对象的结构体 objc_object 中有一个 isa, 它是 类的结构体 objc_class 的指针 Class (指向一个类), 而类的结构体 objc_class 中也有 isa, 这样就形成了一个链式结构,那它指向哪儿呢. 再看

struct objc_class : objc_object {
    // Class ISA;
    Class superclass;
    cache_t cache;             // formerly cache pointer and vtable
    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags
    
    ... 省略
}

objc-runtime-new.h

而从上面可以看到, objc_class 继承自 objc_object, 意味着它也是一个对象;
那么既然 类也是一个对象 (类对象), 那么它是谁的实例呢?
实际上它是元类 (metaClass) 的对象, 见下图!

isa-2564f309-866d-4450-8794-c872cc6112c9-1542122164628-54360326

上图可以清晰的描述出对象与类的关系. 而联系他们的就是 isa 了!!!

  1. 对象 isa 指向 类 (即类对象)
  • 类对象的 isa 指向其 metaClass 元类
  • 元类的 isa 指向其 根元类, 根元类指向自身
  • 类定义中还包含一个 superclass
    • 对象的 superclass 就是其类对象的 superclass
    • 类对象的 superclass 指向 👆 父级 (也是一个类对象), 依次向上一直到 NSObject, 然后是nil
  • isa 表示的是一个 归属
  • superclass 表示的是 继承关系
  • 这里比较绕的就是在 类对象 的 isasuperclass 指向不同

isa 它是一个指向对象的类定义的指针. 也是这个对象内存地址的首地址.

A pointer to the class definition of which this object is an instance , Every Objective-C object hides a data structure whose first member—or instance variable—is the isa pointer. The isa pointer is critical to the message-dispatch mechanism and to the dynamism of Cocoa objects.

Class isa;

这里还要补充的是, 除了上图中的 isa 连线, 我们还要知道 isa 如何起作用.

  • 普通意义上的对象 在调用 实例方法的时候, 是要通过其 isa 指针查找到其 类对象 (内存区域在__代码段__),然后找到 class_data_bits_t,也就是类对象的数据区 class_data_bits_t is the class_t->data field (class_rw_t pointer plus flags), 从这里查找相应方法的实现, 注意是IMP所指向的函数内存首地址. (具体的查找细节就不说了, 关键词是如上图 上溯)

  • 而普通意义上的对象 在调用 类方法的时候, 和上面一条类似, 类对象自身的内存区域也不会存储的, 它会查找 isa 指向的 元类, 从中取查找 方法的实现, 如果是继承的方法,那么它会继续向它元类的父级查找,一直找到定义它的地方!

    • 注意先 cache 缓存链表,再 methodLists , 再 super ...
    • 上面所说的方法的实现都不会在堆区是因为一个对象的方法也就是函数都是一致的, 为了节省空间,完全不需要重复存储, 主要要从模板中获取就可以.
    • 类方法和对象方法的调用效率上没多大区别
    • 类对象和元类对象是唯一的, 区别于普通的对象可以创建多个. 其在 main 方法之前会被实例化 iOS 程序 main 函数之前发生了什么
    • 类或对象的概念 ,其接口统一,使用了设计模式的 组合模式.
  • 深入了解 isa

  • 对象在堆区, 只有实例变量, 不保存对象方法列表

  • 类对象在全局变量, 保存对象方法(也就是对象实例的元数据), 不保存 类方法列表

  • 元类在全局变量, 保存类方法列表, 继承链中的每一个类 都有一个元类 (因为需要保存自己独有的类方法)

记录于 2018/11/13

iOS 开发常用的交互方式有哪些, 各自的使用场景?

直接传值

  • 特点:
  • 一对一直接交互, 使用方便
  • 限制了数据来源
  • 适用于: 看场景来确定是否合适
  • 缺点:
    • 不够灵活

Notification

  • 特点:
  • 一对多, 松耦合
  • 观察者模式
  • 适用于:
    • 跨模块比较多的时候, 松耦合
    • 不确定有哪个对象需要处理这个事件
    • 组件想独立复用,但一些改变需要通知到其他对象,而又不知道是哪些对象
  • 缺点:
    • 交互不明确,容易硬编码, 需要安全移除观察者

KVO

  • 特点:
  • 一对一 , 松耦合
  • 观察者模式
  • 适用于:
    • 基于 KVC , 必须要符合这个前提!!!
    • 明确知道监听对象,且有引用关系的场景, 用于属性观察
    • 注意 UIKit 不支持 KVO, 界面修改 UI 是收不到 变化监听的,只有代码中使用了点语法 setter 才可以
  • 缺点:
    • 需要安全移除观察者

Delegate

  • 特点:
  • 一对一
    • 可以使用 runtime 实现 一对多
  • 委托模式, 归属于适配器模式(对象适配器)
  • 适用场景:
    • 有些东西需要外部提供的,自己做不了, 或者自己做的成本很高,自己只关心和处理自己任务, 也就是复用一个类, 不能复用的部分让代理去做.
      • 除了公共行为部分,其他定制化的工作, 模板方法采用继承, 当然有些也可以转化为委托
    • 有些自己的行为, 需要或者可以让某些其他类知道,暴露出处
    • 常用在多种设计模式的混合体中
  • 缺点:
    • 不适用多层传递
    • 抽象接口与实现的耦合

Block

  • 特点:
  • 一对一
  • 适配器模式
  • 重点在 调用者与回调在一个地方, 逻辑处理集中,可以获取上下文
  • 书写简单
  • 缺点:
    • 不恰当的使用,容易造成循环引用
    • 嵌套使用的易读性比较差

Event-Dispatch Based On UIResponder

  • 特点:
  • 策略模式, 用于同一行为,不同事件分发
  • 责任链模式, 用于同一行为, 相同事件传递
  • 代码逻辑分离
  • 适用场景:
    • UI 复杂,事件响应很多的地方, 事件需要分发, 转发;
    • 不使用很多 if-else, 而采用策略模式
  • 缺点 :
    • 使用有些复杂, 逻辑性强

记录于 2018/11/14

信号量如何设计

目录

  • 全局变量
  • 线程安全 🔐
  • 如何高效通知

什么是信号量 ?

Wikipedia中对信号量 的描述如下:

信号量(英语:Semaphore)又称为信号标,是一个同步对象,用于保持在0至指定最大值之间的一个计数值。
当线程完成一次对该semaphore对象的等待(wait)时,该计数值减一;当线程完成一次对semaphore对象的释放(release)时,计数值加一。
当计数值为0,则线程等待该semaphore对象不再能成功直至该semaphore对象变成signaled状态。
semaphore对象的计数值大于0,为signaled状态;计数值等于0,为nonsignaled状态.

semaphore对象适用于控制一个仅支持有限个用户的共享资源,是一种不需要使用忙碌等待(busy waiting)的方法。

归纳几点:

  • 多线程访问共享资源的时候使用
  • 信号量, 同步对象, 且值为自然数
  • 相关操作:
    • ① 信号量 wait 等待操作, 将同步对象的值 原子-1

      • 然后 如果 计数器值大于0时, 才可以进入临界区,访问共享资源
      • 计数器值为0时, 线程被挡住, 处于等待状态, 注意 不是忙等 , 因此 消耗 CPU 资源较少.
    • ② 信号量 release 操作, 值 原子+1. 这个动作是在离开临界区时执行的, 此时如果有被挡住的线程,那么就可以去获取这个锁, 进入临界区.

    • 如果信号量是一个任意的整数,通常被称为计数信号量(Counting semaphore),或一般信号量(general semaphore);如果信号量只有二进制的0或1,称为二进制信号量(binary semaphore)。在 linux 系统中,二进制信号量(binary semaphore)又称互斥锁(Mutex).

    • iOS 中的信号量也是基于 锁机制

应用

- 线程加锁 (计数器设置为 **1**)
- 线程同步 (计数器设置为 **1**)
- GCD 中 线程最大并发控制 (计数器设置为 **N**) , 推荐使用 `NSOperationQueue`
-  实现类似 `GCD Group` 任务依赖的功能

记录于 2018/11/15

说一下 UICollectionView 自定义布局的性能优化方式 ?

记录于 2018年11月16日

了解 控制反转 和 依赖注入吗, 如何实现 ?

概念

控制反转(Inversion of Control,缩写为IoC),是面向对象编程中的一种设计原则,可以用来减低计算机代码之间的耦合度。其中最常见的方式叫做依赖注入(Dependency Injection,简称DI),还有一种方式叫“依赖查找”(Dependency Lookup)。通过控制反转,对象在被创建的时候,由一个调控系统内所有对象的外界实体,将其所依赖的对象的引用传递给它。也可以说,依赖被注入到对象中

记录于 2018年11月17日

参考链接

天天一问 by manajay