复习资料

2,642 阅读56分钟

下载&后台下载相关

首先了解NSURLSession

NSURLSession中负责下载策略的URLSessionConfiguration

SessionDelegate中负责下载的DownloadSessionDelegate

extension DownloadSessionDelegate: URLSessionDownloadDelegate {
    
	//响应来自远程服务器的会话级身份验证请求
	// 以下两种情况时会调用该方法
	// 1. 远程服务器请求客户端证书
	// 2. 当 session 与使用 SSL 或 TLS 的远程服务器首次建立连接时,使用该方法验证服务器的证书链。
    func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
        let method = challenge.protectionSpace.authenticationMethod
        
        if method == NSURLAuthenticationMethodServerTrust {
            let credential = URLCredential.init(trust: challenge.protectionSpace.serverTrust!)
            completionHandler(URLSession.AuthChallengeDisposition.useCredential, credential)
        }
    }
    
  //urlSession(_:didBecomeInvalidWithError:)方法用以通知 URL session 该 session 已失效。如果通过调用finishTasksAndInvalidate()方法使会话无效,会话会在最后一个 task 完成或失败后调用该方法;如果通过调用invalidateAndCancel()方法使会话无效,会话立即调用该方法。
    public func urlSession(_ session: URLSession, didBecomeInvalidWithError error: Error?) {
        manager?.didBecomeInvalidation(withError: error)
    }
    
    //当所有事件都已传递时,系统会调用URLSessionDelegate协议的urlSessionDidFinishEvents(forBackgroundURLSession:)方法。在该方法内,获取在上一步保存的 backgroundCompletionHandler 并执行。
    public func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) {
        manager?.didFinishEvents(forBackgroundURLSession: session)
    }
    
    //接收进度更新
    public func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
        guard let manager = manager,
            let currentURL = downloadTask.currentRequest?.url,
            let task = manager.fetchTask(currentURL: currentURL)
            else { return }
        task.didWriteData(bytesWritten: bytesWritten, totalBytesWritten: totalBytesWritten, totalBytesExpectedToWrite: totalBytesExpectedToWrite)
    }
    
    //代表一个下载任务已完成下载
    public func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
        guard let manager = manager,
            let currentURL = downloadTask.currentRequest?.url,
            let task = manager.fetchTask(currentURL: currentURL)
            else { return }
        task.didFinishDownloadingTo(location: location)
    }
    
    //用户手动结束app,则所有正在下载、已计划的任务均会取消
    //或则调用了task.cancel()等结束任务。
    public func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
        guard let manager = manager,
            let currentURL = task.currentRequest?.url,
            let downloadTask = manager.fetchTask(currentURL: currentURL)
            else { return }
        downloadTask.didComplete(task: task, error: error)
    }
}

NSURLSessionTask介绍,以及下载专用的NSURLSessionDownloadTask

NSURLSessionDownloadTask暂停生成用于断点续传的resumedata:

后台下载

  • iOS 原生级别后台下载详解
  • 重点:completionHandler的作用: 处理完事件后,执行参数中的块,以便应用程序可以拍摄用户界面的新快照。一般在urlSessionDidFinishEvents函数中调用completionHandler。
  • 准备
    • 在 App 启动的时候AppDelegate的application(_:didFinishLaunchingWithOptions:)创建Background Sessions,后面会说明原因。
  • 下载过程中
    • 当创建了Background Sessions,系统会把它的identifier记录起来,只要 App 重新启动后,创建对应的Background Sessions,它的代理方法也会继续被调用
  • 下载完成
    • 在前台
      • 跟普通的 downloadTask 一样,调用相关的 session 代理方法
      • Background Session,如果是任务被session管理,则下载中的 tmp 格式缓存文件会在沙盒的 caches 文件夹里;
        • 截屏2021-06-15 上午10.25.30.png
      • 如果不被session管理,且可以恢复,则缓存文件会被移动到 Tmp 文件夹里;
      • 如果不被Background Session管理,且不可以恢复,则缓存文件会被删除。
      • 手动 Kill App 会调用了cancel(byProducingResumeData:)或者cancel,最后会调用urlSession(_:task:didCompleteWithError:)代理方法,可以在这里做集中处理,管理 downloadTask,把resumeData保存起来。
        • 截屏2021-06-15 上午10.28.00.png
      • 进入后台、crash 或者被系统关闭,系统会有另外一个进程对下载任务进行管理,没有开启的任务会自动开启,已经开启的会保持原来的状态(继续运行或者暂停),当 App 重新启动后,创建对应的Background Sessions,可以使用session.getTasksWithCompletionHandler(_:)方法来获取任务,session 的代理方法也会继续被调用
      • 最令人意外的是,只要没有手动 Kill App,就算重启手机,重启完成后原来在运行的下载任务还是会继续下载,实在牛逼
    • 在后台
      • 当Background Sessions里面所有的任务(注意是所有任务,不单单是下载任务)都完成后,会调用AppDelegate的application(_:handleEventsForBackgroundURLSession:completionHandler:)方法,激活 App
      • 然后跟在前台时一样,调用相关的 session 代理方法,didFinishDownloadingTo & didCompleteWithError
      • 最后再调用urlSessionDidFinishEvents(forBackgroundURLSession:)方法
    • crash 或者 App 被系统关闭
      • 当Background Sessions里面所有的任务(注意是所有任务,不单单是下载任务)都完成后,会自动启动 App,调用AppDelegate的application(_:didFinishLaunchingWithOptions:)方法(这就是为什么要在这个方法中创建Background Sessions)
      • 然后调用application(_:handleEventsForBackgroundURLSession:completionHandler:)方法
      • 当创建了对应的Background Sessions后,才会跟在前台时一样,调用相关的 session 代理方法,最后再调用urlSessionDidFinishEvents(forBackgroundURLSession:)方法
    • 总结
      • 只要不在前台,当所有任务完成后会调用AppDelegate的application(_:handleEventsForBackgroundURLSession:completionHandler:)方法
      • 只有创建了对应Background Sessions,才会调用对应的 session 代理方法,如果不在前台,还会调用urlSessionDidFinishEvents(forBackgroundURLSession:)
  • 下载错误
    • 支持后台下载的 downloadTask 失败的时候,在urlSession(_:task:didCompleteWithError:)方法里面的(error as NSError).userInfo可能会出现一个 key 为NSURLErrorBackgroundTaskCancelledReasonKey的键值对,由此可以获得只有后台下载任务失败时才有相关的信息
  • 最大并发数
    • 支持后台下载的URLSession的特性,系统会限制并发任务的数量,以减少资源的开销。同时对于不同的 host,就算httpMaximumConnectionsPerHost设置为 1,也会有多个任务并发下载,所以不能使用httpMaximumConnectionsPerHost来控制下载任务的并发数。
  • 前后台切换
    • 在 downloadTask 运行中,App进行前后台切换,会导致urlSession(_:downloadTask:didWriteData:totalBytesWritten:totalBytesExpectedToWrite:)方法不调用
    • 解决办法:使用通知监听UIApplication.didBecomeActiveNotification,延迟 0.1 秒调用suspend方法,再调用resume方法
  • 缓存文件
    • 前面说了恢复下载依靠的是resumeData,其实还需要对应的缓存文件,在resumeData里可以得到缓存文件的文件名(在 iOS 8 获得的是缓存文件路径),因为之前推荐使用cancel(byProducingResumeData:)方法暂停任务,那么缓存文件会被移动到沙盒的 Tmp 文件夹,这个文件夹的数据在某些时候会被系统自动清理掉,所以为了以防万一,最好是额外保存一份。

上传

通过multipart form data上传的机制和原理

//请求头
NSString *headerString = [NSString stringWithFormat:@"multipart/form-data; boundary=%@", boundary];
[request setValue:headerString forHTTPHeaderField:@"Content-Type"];

//请求体
bodyStr = [NSString stringWithFormat:@"Content-Disposition: form-data; name=\"%@\"; filename=\"%@\" \r\n", name, fileName];
[data appendData:[bodyStr dataUsingEncoding:NSUTF8StringEncoding]];
[data appendData:[@"Content-Type: image/png\r\n\r\n" dataUsingEncoding:NSUTF8StringEncoding]];
[data appendData:[NSData dataWithContentsOfURL:fileURL]];
[data appendData:[@"\r\n" dataUsingEncoding:NSUTF8StringEncoding]];

在multipart基础上实现分片上传

headers = ["Content-Range": "bytes=\(UploadManager.share().offset*model.uploadFileChunk!)-\(model.uploadSecretFileSize!)",
                "wnd-token": Defaults.shared.get(for: wndToken) ?? ""]

Tiercel源码解析

云盘上传

  1. 将添加的所有上传任务创建为DownloadTask,并保存self.sessionManager.cache.storeUploadTasks(self.waittingUploadArray)
  2. 检查正在上传的任务数,将可上传任务数补充至3个。uploadArray&waittingUploadArray
    • 被暂停的任务重新开始(旧任务)
      • 获取上传状态
        • 1.如果未完成,则执行上传。
        • 2.已完成上传的任务,将任务从等待数组中移除,继续添加任务。
  3. 如果是照片或视频,则文件拷贝到沙盒。
    • 检查硬盘空间
    • 上传之前,将待上传文件复制到解密区。
  4. 通过密码中间件计算哈希
    • 网络请求拿到公钥
  5. 密钥转保护、文件加密
  6. 文件预上传
    • 秒传
    • 非秒传
  7. 将文件分片,读取第一片上传
  8. 分片上传完成后,判断是否还有分片未上传
    • 未上传完,分片加一,重新调用上传
    • 全部上传完,继续添加上传任务

后台上传

iOS项目技术还债之路《一》后台下载趟坑

上传加密流程

下载解密流程

预览流程

文件存储流程

iOS底层-第一章 oc对象本质

  • 一个OC对象在内存中是如何布局的?
    • 结构体、isa指针
  • 一个NSObject对象占用多少内存?
    • 分配16个字节,使用8个字节(isa指针)
    • 实际上class_getInstanceSize获取到的大小, 是内存对齐之后的大小, 即所有成员变量中, 占用内存最大的那个成员变量的倍数, Person内的NSObject_IMPL有一个isa占用8个字节, _age占用4个字节, 所以class_getInstanceSize获取到的是8的倍数, 即16个字节
  • OC中给实例对象分配空间是按照16的倍数递增的。
    • OC中给实例对象分配空间时, 是按照16, 32, 48, 64, 80, 96...按照16的倍数递增的, 所以malloc_size函数获取到的Student实例内存是32

iOS底层-第二章 OC对象的分类

  • 如何获取类对象?
    • -class
    • +class
    • object_getClass(实例对象)
    • 每个类在内存中有且只有一个class对象
  • 如何获取元类对象?
    • object_getClass(类对象)
    • 每个类在内存中有且只有一个meta-class对象
  • objc_getClass、object_getClass、-class、+class的区别?
    • objc_getClass传入字符串类名,返回对应的类对象
    • object_getClass传入instance对象、class对象、meta-class对象,返回class对象、meta-class对象、NSObject(基类)meta-class对象
    • -class、+class返回类对象
  • -class、+class底层实现?
    • return self->isa
    • return self
  • object_getClass(obj)与[obj class]的区别

iOS底层-第三章 isa和superclass

  • OC三种对象的区别?

    • instance对象:isa、其他成员变量
    • class对象和meta-class对象底层结构相同,class的属性、对象方法、协议、成员变量不为空,meta-class的类方法不为空。
  • OC调用方法是发送消息机制

    • objc_msgSend(person, sel_registerName("personInstanceMethod"));
  • isa指针的指向是什么样的?

    • instance的isa指向class
      • 当调用对象方法时, 通过instance的isa找到class, 最后找到对象方法的实现进行调用
    • class的isa指向meta-class
      • 当调用类方法时, 通过class的isa找到meta-class, 最后找到类方法的实现进行调用
    • meta-class的isa指向基类的meta-class
  • superclass指针的指向是什么样的?

    • class的superclass会指向父类的class对象, 最后指向的是NSObject的class对象, 而NSObject的class对象中的superclass指针, 会指向nil
    • 如果在发现NSObject的class中也没有找到要调用的方法时, 就会报错unrecognized selector sent to instance
    • 基类NSObject的meta-class对象的superclass最终指向的是NSObject的class对象, 而不是指向nil
      image.png

iOS底层-第四章 kvo

  • kvo的本质是什么?
    • 利用runtime动态生成一个子类,并且让instance对象的isa指向这个全新的子类。
    • 子类命名为NSKVONotifying_xxx,重写set、class、superclass、dealloc方法,新增_isKVOA方法。
    • class方法返回原instance对象的类对象。
    • superclass方法指向原instance的类对象。
    • isKVOA返回true。
    • dealloc会增加一些监听的释放。
    • set方法新增willChangeValueForKey和didChangeValueForKey,并且在didChangeValueForKey内部会出发监听器Observer的监听方法。
  • kvo动态生成的对象结构是什么样的?
    • NSKVONotifying_Person的类对象中, 一共有两个指针isa和superclass, 四个方法setAge:, class, dealloc和_isKVOA
  • 如果直接修改对象的成员变量,是否会出发监听器?
    • 直接修改对象,不会调用set方法,将不会出发观察者。
  • 如何手动触发kvo?
    • 通过调用willChangeValueForKey和didChangeValueForKey方法,可以手动调用kvo,两个方法必须同时出现。 截屏2021-03-26 下午3.49.51.png

iOS底层-第五章 kvc

  • setValue:forKey的原理?
    • 按照setKey、_setkey顺序查找方法
    • 没有找到方法,则查看accessInstanceVariablesDirectly方法返回值,默认为true。
    • 若为true,按照_key、_isKey、key、isKey顺序查找成员变量,找到了直接赋值。
    • 若为false或没有找到成员变量,调用valueForUndefineKey:并跑出异常NSUnknownKeyException

截屏2021-03-26 下午4.08.48.png

  • valueForKey的原理?
    • 按照getKey、key、isKey、_key顺序查找方法
    • 没有找到方法,则查看accessInstanceVariablesDirectly方法返回值,默认为true。
    • 若为true,按照_key、isKey、key、isKey顺序查找成员变量,找到了直接取值。
    • 若为false或没有找到成员变量,调用valueForUndefineKey:并跑出异常NSUnknownKeyException

截屏2021-03-26 下午4.09.37.png

  • kvc赋值时,会触发kvo吗?
    • 使用KVO给属性或成员变量赋值时, 都会触发KVO, 系统会自动调用willChangeValueForKey:和didChangeValueForKey:两个方法

iOS底层-第六章 category

  • category的实现原理?

    • category编译之后的底层结构是struct category_t,里面存储着分类的类名, 实例方法列表, 类方法列表, 协议列表和属性列表。
    • 在程序运行的时候,runtime会将Category的数据,合并到类信息中(类对象、元类对象中)
      • 首先重新分配内存,可以足够放下类和Category中左右的方法数据。
      • 接着将类中原有的方法地用到方法列表的最右边。
      • 最后将Category中的方法copy到方法列表的最前面。
      • 属性和协议也是同样的方法。
      • 此时,Category的方法就放在了方法列表中的前面,而类中的原有方法则存也在于方法列表的最后面。
      • 如果调用类的方法,则会从方法列表中从前往后查找,而如果Category中有相同的方法,那么会直接使用Category中的方法。
  • Category和Class Extension的区别?

    • class Extension在编译的时候,他的数据就已经包含在类信息中。
    • Category在运行时,才会将数据合并到类信息中。

iOS底层-第七章 load & initialize

  • Category中有load方法, load方法会在runtime加载类的时候调用
    • 类的load方法调用早于Category中的load方法, 调用子类的load方法之前, 会先调用父类的load方法
    • 没有关系的类会根据编译顺序调用load方法, Category会根据编译顺序调用load方法
    • 所有的类和分类, load方法只会调用一次
  • 当一个类在查找方法的时候, 会先判断当前类是否初始化, 如果没有初始化就会去掉用initialize方法
    • 如果这个类的父类没有初始化, 就会先调用父类的initialize方法, 再调用自己的initialize方法
    • 如果该类没有实现initialize方法,会执行父类的initialize方法。
    • 类在调用initialize时, 使用的是objc_msgSend消息机制调用
  • load和initialize的区别是什么?
    • load & initialize 调用时机
      • load 加载类、分类时调用
      • initialize 类第一次接收到消息时调用
    • load & initialize 调用方式
      • load 根据函数地址直接调用
      • initialize 通过objc_msgsend调用
    • load & initialize 调用顺序
      • load 父类load->子类load->分类load,先编译的类优先调用load
      • initialize 父类init->子类init(子类未实现则调用其父类的方法,所以父类的initialize方法可能会调用多次)
  • 通过代码调用load会怎样?
    • 会根据消息传递机制通过该实例的类对象查找load函数。但是一般情况下不会主动调用load方法, 都是让系统自动调用。

iOS底层-第八章 关联对象

  • Category为什么不能存放成员变量?
    • 因为category_t的底层结构中,只有存放实例方法、类方法、协议、实例属性、类属性的list,没有存放成员变量的list。
  • 如何在category中存放成员变量?
    • 可以通过关联对象,关联对象并不是存储在被关联的对象内存中,而是存储在全局的统一的一个AssociationsManager中。

iOS底层-第九章 block的底层结构

  • block的三种类型?
    • GlobalBlock
      • 存在于内存的数据区域(.data区)
      • 内部没有使用auto类型变量的block, 就是__NSGlobalBlock__类型
      • __NSGlobalBlock__类型的block调用copy后类型不变, 还是__NSGlobalBlock__类型(还在数据区)
    • StackBlock
      • 存在于内存的栈区
      • 内部使用了auto类型变量的block, 就是__NSStackBlock__类型
    • MallocBlock
      • 存在于内存的堆区
      • __NSStackBlock__类型的block调用copy后就是__NSMallocBlock__类型, 通过copy, 将block从栈区复制到了堆区
      • __NSMallocBlock__类型的block调用copy后类型不变, 还是__NSMallocBlock__类型(不会生成新的block, 原有引用计数+1)
  • block对auto、static、全局变量捕获方式
  • 全局静态变量全局变量的区别, 全局静态变量是有作用域的, 只可以被声明的.h .m / .c 中访问到, 全局变量是没有作用域的,无论在任何地方,引入一下 Test.h,便可以获得并使用全局变量。

内存分布了解一下

  • 栈区:内存管理由系统控制,存储的为非静态的局部变量,例如:函数参数,在函数中生命的对象的指针等。当系统的栈区大小不够分配时,系统会提示栈溢出。

  • 堆区:内存管理由程序控制,存储的为malloc , new ,alloc出来的对象。

    • 如果程序没有控制释放,那么在程序结束时,由系统释放。但在程序运行过程中,会出现内存泄露、内存溢出问题。分配方式类似于链表
  • 全局存储区(静态存储区):全局变量、静态变量会存储在此区域。事实上全局变量也是静态的,因此,也叫全局静态存储区。

    • 存储方式: 初始化的全局变量跟静态变量放在一片区域,未初始化的全局变量与静态变量放在相邻的另一片区域。
    • 程序结束后由系统释放。
  • 文字常量区:在程序中使用的常量存储在此区域。程序结束后,由系统释放。在程序中使用的常量,都会到文字常量区获取。

  • 程序代码区:存放函数体的二进制代码。

    • 运行程序就是执行代码,代码要执行就要加载进内存。
  • ARC环境下, block的类型问题

    • __NSStackBlock__类型的block做为函数返回值时, 会将返回的block复制到堆区
    • 将__NSStackBlock__类型的block赋值给__strong指针时, 会将block复制到堆区
    • 如果没有__strong指针引用__NSStackBlock__类型的block, 那么block的类型依然是__NSStackBlock__
    • block作为Cocoa API中方法名含有usingBlock的方法参数时, block在堆区
    • block作为GCD API的方法参数时, block在堆区
  • 对象类型的auto变量捕获

    • 栈中的block不会将对象类型的auto变量进行retain处理, 只有在将block复制到堆上时, 才会将对象类型的auto变量进行retain处理(引用计数+1)
    • 当堆中的block释放时, 会对其中的对象类型的auto变量进行release处理(引用计数-1), 如果此时对象类型的auto变量的引用计数为零, 就会被释放
    • 当block被复制到堆上时,会调用__main_block_copy_0函数, 来对捕获的对象类型的auto变量进行强引用
    • 当block从堆上移除时, 又会被调用__main_block_dispose_0函数, 对捕获的对象类型的auto变量解除强
  • __weak修饰符

    • 当block捕获到的对象类型的auto变量被__weak修饰时, 即便block被复制到了堆上, __main_block_copy_0方法也不会对被捕获的对象类型的auto变量进行强引用

iOS底层-第十章 __block和block内存管理

  • block内部修改外部变量的值

    • static修饰的变量, 在block内可以修改变量的值,因为在底层block捕获的是地址
    • 全局变量可以直接在block中修改值,block不会捕获全局变量, 而是直接使用, 所以可以直接改值
    • __block修饰的auto变量
  • __block修饰auto变量

    • auto变量被包装为一个结构体对象
    • 结构体中包含auto变量的地址(__forwarding)和值
    • 当block调用时,会通过age->__forwarding->age找到__Block_byref_age_0中的成员变量, 并修改值
  • block变量的内存管理

    • 基本数据类型的auto变量,当栈上block复制到堆上时, 会直接将捕获的基本数据类型变量复制到堆中
    • 对象类型的auto变量,当变量被强引用修饰时, block复制到堆上的过程中会调用copy函数, copy函数内部会调用里面的_Block_object_assign函数, 对被捕获的对象变量进行强引用
    • 对象类型的auto变量,当对象变量被__weak修饰时, block从栈中复制到堆中, 依然会调用copy函数, copy函数内部会调用里面的_Block_object_assign函数,只不过不会再对被__weak修饰的变量进行强引用
    • __block修饰的auto变量,在底层会被包装成一个__Block_byref_age_0对象,当block从栈上复制到堆上时, 就会调用__main_block_desc_0中的copy函数, 对__Block_byref_age_0对象进行强引用,移除时会调用dispose函数,移除强引用。
  • __block的__forwarding指针

    • 当block在栈上时, 通过__forwarding指针拿到的是栈中的__block结构体
    • 当block在堆上时, 通过__forwarding指针拿到的是堆中的__block结构体
  • 被__block修饰的对象类型

    • 在block成员变量__main_block_desc_0结构体中的copy和dispose是用来处理person到struct __Block_byref_person_0之间连线的
    • 而__block对象中的copy和dispose, 是用来处理struct __Block_byref_person_0中__strong person到[[Person alloc] alloc]对象之间连线的
  • __block + __strong/__weak

    • __block修饰__strong类型的对象类型变量, 会对对象类型变量进行强引用
    • __block修饰__weak类型的对象类型变量, 会对对象类型变量进行弱引用
    • MRC下, __block修饰的__strong类型的对象类型变量, 在block复制到堆上时,不会进行retain处理
    • 总结:
      • 当__block变量在栈上时,不会对指向的对象产生强引用
      • 当__block变量被copy到堆时,会调用__block变量内部的copy函数,copy函数内部会调_Block_object_assign函数,_Block_object_assign函数会根据所指向对象的修饰符(__strong、__weak、__unsafe_unretained)做出相应的操作,形成强引用(retain)或者弱引用(注意:这里仅限于ARC时会retain,MRC时不会retain。
  • 循环引用

    • __weak: 当person被释放时, block中的__weak person会指向nil
    • __unsafe_unretained: 当person被释放时, block中的__unsafe_unretained person不会指向nil, 造成野指针
    • __block: 手动设置nil

iOS底层-第十一章 isa详解

  • isa指针的变化
    • 在arm64架构之前,isa就是一个普通的指针,存储着Class、Meta-Class对象的内存地址
    • 从arm64架构开始,对isa进行了优化,变成了一个共用体(union)结构,还使用位域来存储更多的信息
    • bits占用一个字节,而结构体使用了位域, 也只占一个字节, 所以共用体只占用一个字节
  • 什么是联合体,位域?
  • isa位域解释

iOS底层-第十二章 Class结构

  • class的底层结构
    • 一开始class_data_bits_t bits;指向ro, 在加载的过程中创建了rw, 此时的指向顺序是bits->rw->ro
  • class_rw_t结构
    • class_rw_t里面的methods、properties、protocols是二维数组,是可读可写的,包含了类的初始内容、分类的内容
  • method_t结构
    • method_t是对方法\函数的封装
  • cache_t结构
    • Class内部结构中有个方法缓存(cache_t),用散列表(哈希表)来缓存曾经调用过的方法,可以提高方法的查找速度
  • 方法查找的逻辑
    • 通过isa找到class
    • 先会先从cache中查找, 如果有方法, 直接调用
    • 如果没有方法, 在自身的class->bits->rw中查找方法
    • 如果找到方法直接调动,并将方法缓存到cache中
    • 如果没有找到, 会通过superclass找到父类, 从父类->bits->rw中查找方法
    • 如果在父类中找到方法, 就直接调用, 同时将方法存到自己(不是父类的)的cache中, 如果一直找不到, 就进入下一个阶段
  • 散列表的存储方法
    • 一开始, 系统会分配给cache_t->_buckets一段内存, 假设第一次分配了足够存储3个方法的内存
    • 此时cache_t的mask等于2, 即_buckets的长度 - 1
    • 当存储方法时, 会用SEL & mask, 获取到一个数字, 用这个数字做为索引, 将该方法存储到_buckets中
    • 当一个Student实例调用learning方法时, 就会用@selector(learning) & _buckets.mask来获取存储的索引,然后将learning方法存放到Student类对象的cache中
    • 假设获取到的索引值为0, 那么散列表的结构类似下图
    • 如果Student实例调用父类Person的eat方法时, 根据@selector(eat) & _buckets.mask的结果将becket_t插入到相应位置
    • 假设@selector(eat) & _buckets.mask结果是2, 那么散列表结构类似下图
    • 即使eat是Person中的方法, 但是Student调用,也会存到Student的cache中
    • 当多个数都&一个固定值时, 那么肯定就有重复的可能出现
    • 此时,存储方法的索引就会-1, 然后在查找-1后所在位置是否空缺, 如果有空缺就会存储
    • 如果Student实例调用exercises方法, 会计算@selector(exercises) & _buckets.mask的结果作为索引值
    • 假设@selector(exercises) & _buckets.mask的结果是2, 此时因为索引2的位置已经存储eat方法, 所以索引会-1变成1, 然后查看1的位置是否空缺, 如果空缺就会存储, 如下图
    • 如果Student实例继续调用dancing方法, 此时cache->buckets已经存储满
    • 那么buckets的容量会扩大一倍, 容量变为6,重新计算cache->mask的值为5, 接着清空buckets中之前缓存的所有方法
    • 然后计算@selector(dancing) & _buckets.mask的值, 如果此时的结果是3, 那么散列表的结构类似下图
    • 因为已经清空过cache中的所有方法, 所以此时只存储dancing方法

iOS底层-第十三章 消息发送

  • objc_msgSend
    • OC中调用方法, 会使用objc_msgSend函数给消息接收者发送消息,所以OC调用方法的过程也被称为消息机制
  • 消息发送流程
    • 当消息接收者为空时, 直接返回, 结束objc_msgSend函数的调用
    • 当消息接收者有值时, 查看缓存
    • 如果方法没有被缓存过, 就会查询方法列表
    • 当缓存中没有找到需要调用的方法时, 就会在方法列表中查找, 如果找到就会存到缓存cache中
    • 方法存在cls->rw->methods中, 而methods是个二位数组, 所以需要进行遍历查询, 这里先拿到一维数组调用search_method_list函数查询
    • 在一维数组中查找方法, 这里有两种情况
      • 第一种: 方法列表已经排好序, 会通过findMethodInSortedMethodList函数查找,findMethodInSortedMethodList函数使用的是二分查找的方式查询方法
      • 第二种: 方法列表没有排好序, 会一个一个遍历查找
    • 当找到方法后, 会先将方法存储到cache中
    • 如果在自己的类对象中没有找到需要调用的方法, 就会去查找父类中是否有该方法
      • 1.查找时会一层一层遍历所有父类, 只要某个父类中找到方法, 就会结束查找
      • 2.先从父类的缓存中找, 如果找到, 会先存到自己的cache中
      • 3.如果父类的缓存中没有该方法, 就会从父类的方法列表中查找, 如果找到就会存入到自己的cache中, 并不会存入到父类的cache中
      • 4.如果没找到, 就会通过for循环查看父类的父类中有没有方法, 依次类推, 只要找到就会结束查询, 并存到自己的cache中
    • 如果最后还是没找到, 就会进入下一个阶段, 动态解析阶段

iOS底层-第十四章 动态方法解析

  • 动态方法解析流程
    • 当消息发送过程中,没有找到要调用的方法时, 就会进入动态方法解析阶段,
    • 在动态方法解析过程中, 会根据类对象和元类对象进行判断, 分别处理
      • 类对象调用resolveInstanceMethod:方法
      • 元类对象调用resolveClassMethod:方法
    • 我们可以在+ (BOOL)resolveInstanceMethod:(SEL)sel方法中, 使用Runtime添加其他方法的实现

iOS底层-第十五章 消息转发

  • 消息转发流程
    • 当动态方法解析也没有找到需要调用的方法实现时, 就会进入消息转发阶段。调用的是forwardingTargetForSelector:方法
    • 可以在+forwardingTargetForSelector:方法中设置消息转发的对象
    • 因为objc_msgSend的原理是给 消息接收者 发送 一条消息, 而这个消息是SEL类型的,并且不分是类方法(+)还是对象方法(-)
    • 所以, 我们可以将消息接收者设置为实例对象
    • 如果不实现+forwardingTargetForSelector:方法, 就会调用+methodSignatureForSelector:方法, 并调用+forwardInvocation:方法
    • 截屏2021-03-29 下午2.53.34.png 截屏2021-03-29 下午2.54.49.png

iOS底层-第十六章 super

  • super的结构
    • super的底层调用了objc_msgSendSuper方法, 并传入两个参数
      • __rw_objc_super: 结构体
        • receiver: 消息接收者
        • super_class: 从super_class开始查找调用的方法
      • sel_registerName("run"): 方法SEL
      struct objc_super {
         __unsafe_unretained _Nonnull id receiver;
         __unsafe_unretained _Nonnull Class super_class;
      };
      
    • 从实际代码中可以看到, 这两个成员变量分别传入了self和[Person class]
    • 所以消息接收者是self, 从[Person class]中开始查找方法
    • 总结:
      • super的含义是, 查询方法的起点是父类, 不是本身的类对象
      • 消息接收者是self, 不是父类对象
      • 发送的消息是调用的方法

iOS底层-第十七章 Runloop基本认识

  • runloop与线程的关系

    • 每条线程都有唯一的一个与之对应的RunLoop对象
    • RunLoop保存在一个全局的Dictionary里, 线程作为key, RunLoop对象做为Value
    • 线程刚创建时并没有RunLoop对象, RunLoop会在第一次获取它时创建
    • RunLoop会在线程结束时销毁
    • 主线程的RunLoop已经自动获取(创建), 子线程默认没有开启RunLoop
  • runloop底层结构

    • CFRunLoopRef 结构
    typedef struct CF_BRIDGED_MUTABLE_TYPE(id) __CFRunLoop * CFRunLoopRef;
    struct __CFRunLoop {
    	pthread_t _pthread;
    	CFMutableSetRef _commonModes;
    	CFMutableSetRef _commonModeItems;
    	CFRunLoopModeRef _currentMode;
    	CFMutableSetRef _modes;
    };
    
    • _modes中存放的是CFRunLoopModeRef类型数据, 其中就有_currentMode, 只不过_currentMode是当前使用的mode
    • CFRunLoopModeRef 结构
    typedef struct __CFRunLoopMode *CFRunLoopModeRef;
    struct __CFRunLoopMode {
    	CFStringRef _name;
    	CFMutableSetRef _sources0;
    	CFMutableSetRef _sources1;
    	CFMutableArrayRef _observers;
    	CFMutableArrayRef _timers;
    };
    
  • CFRunLoopModeRef

    • CFRunLoopModeRef代表RunLoop的运行模式
    • 一个RunLoop包含若干个Mode,每个Mode又包含若干个Source0/Source1/Timer/Observer
    • RunLoop启动时只能选择其中一个Mode,作为currentMode
    • 如果需要切换Mode,只能退出当前Loop,再重新选择一个Mode进入
    • 不同组的Source0/Source1/Timer/Observer能分隔开来,互不影响
    • 如果Mode里没有任何Source0/Source1/Timer/Observer,RunLoop会立马退出
    • 常见的两种CFRunLoopModeRef
      • kCFRunLoopDefaultMode(NSDefaultRunLoopMode): App的默认Mode,通常主线程是在这个Mode下运行
      • UITrackingRunLoopMode: 界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响
  • CFRunLoopModeRef各属性作用

    • Source0
      • 触摸事件处理
      • performSelector:onThread:
    • Source1
      • 基于Port的线程间通信
      • 系统事件捕捉
    • Timers
      • NSTimer
      • performSelector:withObject:afterDelay:
    • Observers
      • 用于监听RunLoop的状态
      • UI刷新(BeforeWaiting)
      • Autorelease pool(BeforeWaiting)
  • mode的切换

    • 滚动之前, 先退出kCFRunLoopDefaultMode, 进入UITrackingRunLoopMode
    • 等滚动结束, 会先退出UITrackingRunLoopMode, 进入kCFRunLoopDefaultMode
  • 如何保活

    • 每一条线程都有与之相对应的唯一一个RunLoop, 只有在主动获取RunLoop时才会创建(主线程中的RunLoop由系统自动创建)
    • 当然, 我们在使用RunLoop对线程进行保活的时候, 不能仅仅运行就行了, 因为RunLoop当前执行的_currentModel中如果没有Sources0, Sources1, Timers, Observers, 那么RunLoop会自动退出
    • 所以我们需要创建一个事件让RunLoop处理, 这样RunLoop才不会退出
  • 运行流程

  • runloop休眠和工作的切换

  • RunLoop中的source0、source1

  • iOS Touch Event from the inside out(该文章下有一篇讲解mach机制的文章)

iOS底层-第十八章 多线程的安全隐患(锁)

  • iOS中有哪些锁

    • OSSpinLock
      • 自旋锁,等待锁的线程会处于忙等(busy-wait)状态,一直占用着CPU资源
      • 目前已经不再安全,可能会出现优先级反转问题
      • 可能出现低优先级的线程先加锁,但是CPU更多的执行高优先级线程, 此时就会出现类似死锁的问题
    • os_unfair_lock
      • 互斥锁,用于取代不安全的OSSpinLock, 从iOS10开始才支持
      • 线程会处于休眠状态, 并非忙等
    • pthread_mutex
      • mutex叫做互斥锁,等待锁的线程会处于休眠状态
      • 设置pthread初始化时的属性类型为PTHREAD_MUTEX_RECURSIVE, 这样pthread就是一把递归锁
      • 递归锁允许同一线程内, 对同一把锁进行重复加锁
    • dispatch_semaphore
      • 信号量
      • 使用dispatch_semaphore_t设置信号量为1, 来控制同意之间只有一条线程能执行
    • dispatch_queue(DISPATCH_QUEUE_SERIAL)
      • 同步队列解决多线程隐患
    • NSLock
      • 基于pthread封装的OC对象
      • NSLock是基于pthread封装的normal锁
    • NSRecursiveLock
      • 递归锁
      • 基于pthread封装的OC对象
      • 基于pthread_mutex封装的递归锁
    • NSCondition
      • 基于pthread封装的OC对象
      • 基于pthread_cond & pthread_mutex封装的条件锁
    • NSConditionLock
      • 基于pthread封装的OC对象
      • NSConditionLock是对NSCondition的进一步封装
    • @synchronized
      • @synchronized是对mutex递归锁的封装
      • @synchronized(obj)内部会生成obj对应的递归锁,然后进行加锁、解锁操作
      • 底层是通过os_unfair_recursive_lock封装的锁
  • 什么是优先级反转

    • 假设通过OSSpinLock给两个线程thread1thread2加锁
    • thread优先级高, thread2优先级低
    • 如果thread2先加锁, 但是还没有解锁, 此时CPU切换到thread1
    • 因为thread1的优先级高, 所以CPU会更多的给thread1分配资源, 这样每次thread1中遇到OSSpinLock都处于使用状态
    • 此时thread1就会不停的检测OSSpinLock是否解锁, 就会长时间的占用CPU
    • 这样就会出现类似于死锁的问题
  • iOS锁的性能

    • 性能从高到低排序
    • os_unfair_lock
    • OSSpinLock
    • dispatch_semaphore
    • pthread_mutex
    • dispatch_queue(DISPATCH_QUEUE_SERIAL)
    • NSLock
    • NSCondition
    • pthread_mutex(recursive)
    • NSRecursiveLock
    • NSConditionLock
    • @synchronized
  • 自旋锁、互斥锁比较

    • 什么情况使用自旋锁比较划算?
      • 预计线程等待锁的时间很短
      • 加锁的代码(临界区)经常被调用,但竞争情况很少发生
      • CPU资源不紧张
      • 多核处理器
    • 什么情况使用互斥锁比较划算?
      • 预计线程等待锁的时间较长
      • 单核处理器
      • 临界区有IO操作
      • 临界区代码复杂或者循环量大
      • 临界区竞争非常激烈

iOS底层-第十九章 atomic

  • 基础概念
    • atomic用于保证属性setter、getter的原子性操作,相当于在getter和setter内部加了线程同步的锁
    • 它并不能保证使用属性的过程是线程安全的
    • atomic底层使用的是os_unfair_lock(性能最高)

iOS底层-第二十章 文件的读写安全

  • 多读单写方案
    • pthread_rwlock
    • dispatch_barrier_async
      • 这个函数传入的并发队列必须是自己通过dispatch_queue_cretate创建的
      • 如果传入的是一个串行或是一个全局的并发队列,那这个函数便等同于dispatch_async函数的效果
      • 为什么读用dispatch_sync,写用dispatch_async image.png

iOS底层-第二十一章 内存管理-定时器

  • CADisplayLink
    • 使用频率和屏幕的刷新频率保持一致, 60FPS
  • NSTimer
    • NSTimer依赖于RunLoop,如果RunLoop的任务过于繁重,可能会导致NSTimer不准时
  • GCD定时器
    • GCD定时器不依赖于RunLoop, 会更加的准时

iOS底层-第二十二章 Tagged Pointer

iOS - 老生常谈内存管理(五):Tagged Pointer

iOS 内存管理之 Tagged Pointer

聊聊伪指针 Tagged Pointer

  • iOS程序的内存布局
  • Tagged Pointer
    • 从64bit开始,iOS引入了Tagged Pointer技术,用于优化NSNumber、NSDate、NSString等小对象的存储
    • 在没有使用Tagged Pointer之前, NSNumber等对象需要动态分配内存、维护引用计数等,NSNumber指针存储的是堆中NSNumber对象的地址值
    • 使用Tagged Pointer之后,NSNumber指针里面存储的数据变成了: Tag + Data,也就是将数据直接存储在了指针中
    • 当指针不够存储数据时,才会使用动态分配内存的方式来存储数据
    • objc_msgSend能识别Tagged Pointer,比如NSNumber的intValue方法,直接从指针提取数据,节省了以前的调用开销

iOS底层-第二十三章 OC对象的内存管理

iOS SideTable

iOS开发-weak引用以及sidetable表

  • 引用计数
    • 一个新创建的OC对象引用计数默认是1,当引用计数减为0,OC对象就会销毁,释放其占用的内存空间
    • 调用retain会让OC对象的引用计数+1,调用release会让OC对象的引用计数-1
    • 当调用alloc、new、copy、mutableCopy方法返回了一个对象,在不需要这个对象时,要调用release或者autorelease来释放它
    • 想拥有某个对象,就让它的引用计数+1;不想再拥有某个对象,就让它的引用计数-1
  • MRC下的setter方法
- (void)setName:(NSString *)name {
    if (_name != name) {
        [_name release];
        _name = [name retain];
    }
}
  • copy和mutableCopy
    • 深拷贝: 产生一个新的副本, 与源对象相互独立
    • 浅拷贝: 指针拷贝, 指向源对象
    • 自定义对象的拷贝需要实现NSCopying协议
  • 引用计数的存储
    • 如果指针是Tagged Pointer, 那么直接返回, 否则进入下一步
    • 判断isa是否优化过
      • 如果优化过, 那么最后isa的最后19位存储的是引用计数
      • 如果isa没有优化过, 那么就会进入sidetable_retainCount函数, 获取sidetable中的引用计数
      • sidetable 使用的是spinLock_t
    • 如果最后19位不足以存储, 那么多余的引用计数会存储到sidetable中, 同时将倒数第20位的值置为1, 就是has_sidetable_rc的值为1
    • 如果has_sidetable_rc的值为1, 就会从sidetable_getExtraRC_nolock函数中取出sidetable中存储的引用计数
  • dealloc函数原理
    • isa是优化过的指针, 对象没有被弱引用, 没有关联对象, 没有c++析构函数, 没有将引用计数存到Sidetable中, 就会立即释放
    • 否则调用object_dispose函数
    • 进入object_dispose函数, 可以看到调用了objc_destructInstance函数
    • 进入objc_destructInstance函数, 可以看到对objc的处理, 是在clearDeallocating函数中将弱指针置为nil的
    • 进入clearDeallocating函数, 又可以看到两种情况
      • 对象的isa没有优化过
        • 当isa没有被优化过, 进入sidetable_clearDeallocating函数, 可以看到weak引用是存放到SideTable中的
        • 存放在了SideTable的weak_table_t中
        • 查看weak_table_t, 即weak会被存放到一个全局的散列表中
        • 会通过weak_clear_no_lock函数, 对弱指针置为nil, 同时移除删列表中的weak记录
      • 和优化过, 并且被弱指针引用 或者 将引用计数存放到了Sidetable中
        • 如果isa被优化过, 并且对象被弱引用或者将引用计数存到Sidetable中, 就会调用clearDeallocating_slow函数
        • 进入clearDeallocating_slow函数, 可以看到在函数中, 调用了weak_clear_no_lock函数, 并清空了引用计数

iOS - 老生常谈内存管理(四):内存管理方法源码分析

retainCount方法

  • 在arm64之前,isa不是nonpointer。对象的引用计数全都存储在SideTable中,retainCount方法返回的是对象本身的引用计数值 1,加上SideTable中存储的值;
  • 从arm64开始,isa是nonpointer。对象的引用计数先存储到它的isa中的extra_rc中,如果 19 位的extra_rc不够存储,那么溢出的部分再存储到SideTable中,retainCount方法返回的是对象本身的引用计数值 1,加上isa中的extra_rc存储的值,加上SideTable中存储的值。
  • 所以,其实我们通过retainCount方法打印alloc创建的对象的引用计数为 1,这是retainCount方法的功劳,alloc方法并没有设置对象的引用计数。

retain方法

  • 如果isa不是nonpointer,那么就对Sidetable中的引用计数进行 +1;
  • 如果isa是nonpointer,就将isa中的extra_rc存储的引用计数进行 +1,如果溢出,就将extra_rc中RC_HALF(extra_rc满值的一半)个引用计数转移到sidetable中存储。
  • 从rootRetain函数中我们可以看到,如果extra_rc溢出,设置它的值为RC_HALF,这时候又对sidetable中的refcnt增加引用计数RC_HALF。extra_rc是19位,而RC_HALF宏是(1ULL<<18),实际上相等于进行了 +1 操作。

release方法

  • 如果isa不是nonpointer,那么就对Sidetable中的引用计数进行 -1,如果引用计数 =0,就dealloc对象;
  • 如果isa是nonpointer,就将isa中的extra_rc存储的引用计数进行 -1。如果下溢,即extra_rc中的引用计数已经为 0,判断has_sidetable_rc是否为true即是否有使用Sidetable存储。如果有的话就申请从Sidetable中申请RC_HALF个引用计数转移到extra_rc中存储,如果不足RC_HALF就有多少申请多少,然后将Sidetable中的引用计数值减去RC_HALF(或是小于RC_HALF的实际值),将实际申请到的引用计数值 -1 后存储到extra_rc中。如果extra_rc中引用计数为 0 且has_sidetable_rc为false或者Sidetable中的引用计数也为 0 了,那就dealloc对象。
  • 为什么需要这么做呢?直接先从Sidetable中对引用计数进行 -1 操作不行吗?我想应该是为了性能吧,毕竟访问对象的isa更快。

dealloc方法

  • ① 判断 5 个条件(1.isa为nonpointer;2.没有弱引用;3.没有关联对象;4.没有C++的析构函数;5.没有额外采用SideTabel进行引用计数存储),如果这 5 个条件都成立,直接调用free函数销毁对象,否则调用object_dispose做一些释放对象前的处理;

    • 1.如果有C++的析构函数,调用object_cxxDestruct;
    • 2.如果有关联对象,调用_object_remove_assocations函数,移除关联对象;   * 3.调用weak_clear_no_lock将指向该对象的弱引用指针置为nil;   * 4.调用table.refcnts.erase从引用计数表中擦除该对象的引用计数(如果isa为nonpointer,还要先判断isa.has_sidetable_rc)
  • ③ 调用free函数销毁对象。

  • 根据dealloc过程,__weak修饰符的变量在对象被dealloc时,会将该__weak置为nil。可见,如果大量使用__weak变量的话,则会消耗相应的 CPU 资源,所以建议只在需要避免循环引用的时候使用__weak修饰符。

  • 在《iOS - 老生常谈内存管理(三):ARC 面世 —— 所有权修饰符》章节中提到,__weak对性能会有一定的消耗,当一个对象dealloc时,需要遍历对象的weak表,把表里的所有weak指针变量值置为nil,指向对象的weak指针越多,性能消耗就越多。所以__unsafe_unretained比__weak快。当明确知道对象的生命周期时,选择__unsafe_unretained会有一些性能提升。

清除weak

  • 当一个对象被销毁时,在dealloc方法内部经过一系列的函数调用栈,通过两次哈希查找,第一次根据对象的地址找到它所在的Sidetable,第二次根据对象的地址在Sidetable的weak_table中找到它的弱引用表。弱引用表中存储的是对象的地址(作为key)和weak指针地址的数组(作为value)的映射。weak_clear_no_lock函数中遍历弱引用数组,将指向对象的地址的weak变量全都置为nil。

添加weak

  • 一个被标记为__weak的指针,在经过编译之后会调用objc_initWeak函数,objc_initWeak函数中初始化weak变量后调用storeWeak。添加weak的过程如下:经过一系列的函数调用栈,最终在weak_register_no_lock()函数当中,进行弱引用变量的添加,具体添加的位置是通过哈希算法来查找的。如果对应位置已经存在当前对象的弱引用表(数组),那就把弱引用变量添加进去;如果不存在的话,就创建一个弱引用表,然后将弱引用变量添加进去。

iOS底层-第二十四章 @autoreleasepool

  • autoreleasepool的结构

    • 每个AutoreleasePoolPage对象占用4096字节内存,除了用来存放它内部的成员变量,剩下的空间用来存放autorelease对象的地址
    • AutoreleasePoolPage中除了一开始的56个字节用来存储成员变量, 其他的所有内存空间都是用来存储被autorelease对象的地址
    • 当一个AutoreleasePoolPage不够存储autorelease对象地址时, 就会在创建一个AutoreleasePoolPage
    • 所有的AutoreleasePoolPage对象通过双向链表的形式连接在一起
  • autoreleasepool的运行机制

    • 调用push方法会将一个POOL_BOUNDARY入栈,并且返回其存放的内存地址
    • 调用pop方法时传入一个POOL_BOUNDARY的内存地址,会从最后一个入栈的对象开始发送release消息,直到遇到这个POOL_BOUNDARY
    • id *next指向了下一个能存放autorelease对象地址的区域
  • autoreleasepool的嵌套

  • Runloop和Autorelease

    • iOS在主线程的Runloop中注册了2个Observer
    • 第1个Observer
      • 监听了kCFRunLoopEntry事件,会调用objc_autoreleasePoolPush()
    • 第2个Observer
      • 监听了kCFRunLoopBeforeWaiting事件,会调用objc_autoreleasePoolPop()、objc_autoreleasePoolPush()
      • 监听了kCFRunLoopBeforeExit事件,会调用objc_autoreleasePoolPop()

高手课 - 02 App启动速度怎么做优化与监控?

虚拟内存的那点事儿

  • 动态库共享缓存(dyld shared cache)
    • 好处就是节约内存
  • 动态库的加载
    • 在iOS中是使用了dyld来加载动态库
    • dyld(dynamic loader),动态加载器
  • mach-o
    • 是mach object的缩写,是iOS上用于存储程序、库的标准格式。
  • 常见的mach-o文件
    • mh_object
      • 目标文件(.o):可执行文件和代码之间的中间产物。
        • .c -> .o
        • 多个.o合并为一个可执行文件
      • 静态库文件(.a):静态库其实就是N个.o合并在一起。
    • mh_execute
      • 可执行文件
    • mh_dylib
      • 动态库文件
      • .dylib
      • .framework/xx
    • mh_dsym
      • 存储着二进制文件符号信息的文件
    • mh_dylikner
      • /usr/lib/dyld
      • 动态链接编辑器
  • Universal Binary
    • 通用二进制文件
    • 包含了多种不同架构的独立二进制文件
    • 可以通过命令瘦身
  • mach-o的基础结构
    • Header
      • 文件类型、目标架构类型等
    • Load commands
      • 描述文件在内存中的逻辑结构、布局
    • Rwa segment data
      • 在Load commands中定义的Segment的原始数据
  • dyld和Mach-O的关系
    • dyld负责加载mh_execute、mh_dylib、mh_bundle类型的mach-o文件。

小码哥底层 app的启动

  • 通过添加环境变量可以打印出app的启动事件分析(edit scheme -> run -> arguments)
    • DYLD_PRINT_STATISTICS设置为1
    • DYLD_PRINT_STATISTICS_DETAILS设置为1
  • app冷启动的过程
    • dyld
      • app编译后生成一个Mach-O格式的可执行文件
      • 装载app的可执行文件,同时会递归加载所有依赖的动态库。
      • 当dyld把可执行文件、动态库都装载完成后,会通知runtime进行下一步的处理
    • rebase
    • bind
    • runtime
      • 调用map_images进行可执行文件内容的解析和处理
      • 在load_images中调用call_load_methods,调用所有claa和category的+load方法
      • 进行各种objc结构的初始化(注册objc类、初始化类对象等等)
      • 调用c++静态初始化器和_attribute_((constructor))修饰的函数
      • 到此为止,可执行文件和动态库中所有的符号(class、protocol、selector、imp...)都已经按格式加载到内存中,被runtime所管理。
    • main
      • 所有初始化工作结束后,dyld就会调用main函数。
      • 接下来就是UIApplicationMain函数,AppDelegate的application:didFinishLaunching方法
  • app的启动优化
    • dyld
      • 减少动态库、合并一些动态库
      • 减少objc类、分类的数量、减少selector数量
      • 减少c++虚函数数量
      • swift尽量使用struct
    • runtime
      • 用+initialize和dispatch_once取代_attribute_((constructor))、c++静态构造器、objc的+load
    • main

抖音品质建设 - iOS启动优化《原理篇》

iOS 启动优化 + 监控实践

iOS App启动优化(一):检测启动时间

iOS 优化篇 - 启动优化之Clang插桩实现二进制重排

高手课 - 03 AutoLayout是怎么进行自动布局的

深入理解 Autolayout 与列表性能 -- 背锅的 Cassowary 和偷懒的 CPU

高手课 - 05 符号是怎么绑定到地址上的

  • LLVM 编译过程

    • 预处理
    • 词法分析
    • 语法分析
    • 生成 AST
      • 因为 Swift 在编译时就完成了方法绑定直接通过地址调用属于强类型语言,方法调用不再是像 Objective-C 那样的消息转发,这样编译就可以获得更多的信息用在后面的后端优化上。因此我们可以在 SIL 上对 Swift 做针对性的优化,而这些优化是 LLVM IR 所无法实现的。
      • 强类型和弱类型的语言有什么区别
    • 静态分析
    • 生成 LLVM IR
    • 编译器优化
    • Bitcode (可选)
    • 生成汇编
    • 生成目标文件
      • 链接器做符号和地址绑定
      • 链接器还要把项目中的多个 Mach-O 文件合并成一个
    • 生成可执行文件
  • 源码到可执行文件流程

    • 编译器Clang会将源码XXX.m编译为目标文件XXX.o
    • 链接器会将目标文件链接打包进最终的可执行文件Mach-O中
      • 链接器解决了目标文件和库之间的链接。
    • 点击App ICON时,动态链接器dyld会加载可执行文件以及依赖的动态库,并最终执行到main.m里,至此App启动完成
  • iOS 编译知识小结

  • iOS底层学习 - 从编译到启动的奇幻旅程(一)

高手课 - 06 App如何通过注入动态库的方式实现极速编译调试

  • 创建监听SimpleSocket,通过File Watcher监听观察文件改动
  • 修改代码,保存后重新编译修改的类文件,修改后的文件被编译为了.dylib动态库
  • 然后通过writestring给我们的App发"INJECT"消息,通知App更新代码
  • 通过SwiftEval.instance.loadAndInject方法dlopen加载.dylib动态库
  • 然后通过OC runtime 的class_replaceMethod把整个类的实现方法都替换
  • 然后再调SwiftInjected.injected我们的类收到消息开始重绘UI

高手课 - 07 我们应该使用谁来做静态分析?

  • OCLint/SwiftLint
  • Infer
  • Clang 静态分析器

高手课 - 08 如何利用Clang为App提质?

iOS 查漏补缺 - LLVM & Clang

高手课 - 09 无侵入的埋点方案如何实现

静下心来读源码之Aspects iOS AOP 框架 - Aspects 源码解读

高手课 - 10 包大小:如何从资源和代码层面实现全方位瘦身?

  • 官方 App Thinning
  • 无用图片资源
  • 图片资源压缩
  • 通过 AppCode 找出无用代码

高手课 - 12 iOS 崩溃千奇百怪,如何全面监控?

iOS Crash防护

iOS中常见Crash总结

  • 可捕获崩溃
    • KVO 问题、NSNotification 线程问题、数组越界、野指针等崩溃信息
    • EXC_BAD_ACCESS 这个异常会通过 SIGSEGV 信号发现有问题的线程。虽然信号的种类有很多,但是都可以通过注册 signalHandler 来捕获到。
    • oc自己也有定义一些并没深入到内核的一些exception,这些是通过注册exceptionhandle来进行捕获的
    • 对各种信号都进行了注册,捕获到异常信号后,在处理方法 handleSignalException 里通过 backtrace_symbols 方法就能获取到当前的堆栈信息。堆栈信息可以先保存在本地,下次启动时再上传到崩溃监控服务器就可以了。
  • 不可捕获崩溃
    • 后台任务超时、内存被打爆、主线程卡顿超阈值等信息
    • 设置阀值,保存堆栈信息。
  • 僵尸对象原理
    • 开启Zombie Objects后,dealloc将会被hook,被hook后执行dealloc,内存并不会真正释放,系统会修改对象的isa指针,指向_NSZombie_前缀名称的僵尸类,将该对象变为僵尸对象。
    • 僵尸类做的事情比较单一,就是响应所有的方法:抛出异常,打印一条包含消息内容及其接收者的消息,然后终止程序。

Crash

Unrecoginzed Selector Crash

  • hook NSObject的 -(id)forwardingTargetForSelector:(SEL)aSelector 方法启动消息转发

KVO Crash

  • 找一个 Proxy 用来做转发, 真正的观察者是 Proxy,被观察者出现了通知信息,由 Proxy 做分发。所以 Proxy 里面要保存一个数据结构 {keypath : [observer1, observer2,...]} 。
  • Hook NSObject的KVO相关方法
      • (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context;
      • (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
  • hook dealloc在dealloc中遍历删除proxy中的数对象。

数组 Crash

  • hook 常见的方法加入检测功能并且捕获堆栈信息上报。
  • 不过要注意容器类是类簇,直接hook容器类是无法成功的,需要hook对外隐藏的实际起作用的类,比如NSMutableArray的实际类名为__NSArrayM。

NSNotification Crash

  • 类似 KVO 中间加上 Proxy 层,使用 weak 指针来持有对象
  • 在 dealloc 的时候将未被移除的观察者移除

NSNull Crash

  • 一旦对 NSNull 这个类型调用任何方法都会出现 unrecongized selector 错误

野指针 Crash

  • 借用系统的NSZombies对象的设计
    • 建立白名单机制,由于系统的类基本不会出现野指针,而且 hook 所有的类开销较大。所以我们只过滤开发者自定义的类。
    • hook dealloc 方法 这些需要保护的类我们并不让其释放,而是调用objc_desctructInstance 方法释放实例内部所持有属性的引用和关联对象。
    • 利用 object_setClass(id,Class) 修改 isa 指针将其指向一个Proxy 对象(类比�系统的 KVO 实现),此 Proxy 实现了一个和前面所说的智能转发类一样的 return 0的函数。
    • 在 Proxy 对象内的 - (void)forwardInvocation:(NSInvocation *)anInvocation 中收集 Crash 信息。
    • 缓存的对象是有成本的,我们在缓存对象到达一定数量时候将其释放(object_dispose)。
  • 建议使用的时候如果近期没有野指针的Crash可以不必开启,如果野指针类型的Crash突然增多,可以考虑在 hot Patch 中开启野指针防护,待收取异常信息之后,再关闭此开关。

高手课 - 13 如何利用RunLoop原理去监控卡顿

天罗地网? iOS卡顿监控实战(开源)

iOS卡顿监测方案总结

使用RunLoop检测卡顿

  • 一旦发现进入睡眠前的 kCFRunLoopBeforeSources 状态,或者唤醒后的状态 kCFRunLoopAfterWaiting,在设置的时间阈值内一直没有变化,即可判定为卡顿。接下来,我们就可以 dump 出堆栈的信息,从而进一步分析出具体是哪个方法的执行时间过长。

高手课 - 14 临近 OOM,如何获取详细内存分配信息,分析内存问题?

iOS 性能优化实践:头条抖音如何实现 OOM 崩溃率下降50%+

如何判定发生了OOM

分析字节跳动解决OOM的在线Memory Graph技术实现

高手课 - 15 日志监控:怎样获取 App 中的全量日志?

CocoaLumberjack源码

高手课 - 17 临近 OOM,如何获取详细内存分配信息,分析内存问题?

  • 为什么AFNetworking 2.0需要常驻线程 这个问题的根源在于 AFNetworking 2.0 使用的是 NSURLConnection,而 NSURLConnection 的设计上存在些缺陷。NSURLConnection 发起请求后,所在的线程需要一直存活,以等待接收 NSURLConnectionDelegate 回调方法。但是,网络返回的时间不确定,所以这个线程就需要一直常驻在内存中。AFNetworking 在 3.0 版本时,使用苹果公司新推出的 NSURLSession 替换了 NSURLConnection,从而避免了常驻线程这个坑。NSURLSession 可以指定回调 NSOperationQueue,这样请求就不需要让线程一直常驻在内存里去等待回调了。

  • 类似数据库这种需要频繁读写磁盘操作的任务,尽量使用串行队列来管理,避免因为多线程并发而出现内存问题

新浪 - 1 UI视图相关面试问题

iOS触摸事件全家桶

  • 事件传递
  • 事件响应
    • 如果响应视图无法处理响应事件,则响应事件会通过响应链传递给父视图尝试处理,直到传递给UIApplication。
  • 图像显示原理
    • CPU和GPU是通过事件总线链接在一起的。
    • CPU输出的位图,在适当时机由事件总线上传给GPU。
    • GPU会对位图进行渲染,然后将结果放入帧缓冲区。
    • 视频控制器通过Vsync信号,在指定时间(16.7ms)之前,从帧缓冲区中提取屏幕显示内容,然后显示在显示器上。 WechatIMG36.jpeg WechatIMG37.jpeg
  • 卡顿&掉帧的原因
    • 如果CPU和GPU的工作时长超过16.7ms,那么当Vsync信号来临时,无法提供这一帧的画面,就会出现掉帧现象。
  • 绘制原理
  • 异步绘制
  • 离屏渲染

新浪 - 2 Objective-C语言特性相关面试问题

  • 分类和扩展的区别
    • 分类是运行时决议,扩展是编译时决议
  • 如何手动实现Notification
  • MRC手动修饰变量的setter

加固

  • 加壳
    • 利用特殊的算法,对可执行文件的编码进行改变(比如压缩,加密),以达到保护程序代码的目的。
  • 脱壳
    • 硬脱壳
    • 动态脱壳

新浪 - 4 内存管理相关面试问题

  • 散列表结构
  • sideTable结构
  • sideTable算法
    • 通过对象地址 与Hash表的count取模,获取目标值下标索引。
  • 弱引用表结构
  • retain & relase实现
  • dealloc原理

新浪 - 6 多线程相关面试

  • 死锁

    • 首先在主线程执行主队列中的viewDidLoad函数。
    • 当执行到block时,因为是同步,所以需要hold住主线程中主队列正在执行的viewDidLoad函数,等执行完主队列中block内部代码后,再执行主线程中主队列的viewDidLoad函数。
    • 所以出现了viewDidLoad等待block的情况。
    • block内的代码要执行,必须等待队列中其他函数执行完,即先进先出。
    • 所以出现了block等待viewDidLoad的情况。
    • 最终两个函数相互等待,出现造成死锁。
  • 情况二

    • 首先在主线程执行主队列中的viewDidLoad函数。
    • 当执行到block时,因为是同步,所以需要hold住主线程中主队列正在执行的viewDidLoad函数,等执行完主队列中block内部代码后,再执行主线程中主队列的viewDidLoad函数。
    • 所以出现了viewDidLoad等待block的情况。
    • block内部的代码会在serialQueue的队列中取出,因为serialQueue中block排在最前,所以block会被立即取出,并在主线程中执行。
    • 待block执行完毕,会执行viewDidLoad剩余代码。
  • 情况三

    • 因为是并发队列,所以运行队列中的任务一起执行,不需要等待上一个任务执行完再执行下一个,所以不会死锁。
    • 如果global_queue换成串行队列,就会产生死锁。
  • 异步串行

    • 先执行完viewDidLoad,再执行block内的代码。
  • 子线程默认未开起runloop

    • 因为子线程默认没有开启runloop,performSelector无法执行。
    • 这个方法调用后,在当前runloop里设置了一个timer,来触发这个方法执行。而当前这个方法是在子线程中调用的,在子线程中runloop不是自动创建并跑起来的,需要手动调用,才会创建。因为这个在子线程中的调用没有创建runloop,所以就没有执行
  • 怎样利用GCD实现多读单写?

    • 读的时候使用dispatch_sync,是因为使用同步队列可以在赋值结束后,再执行返回值的操作。
  • 使用GCD实现A、B、C三个任务并发,完成后执行任务D。

  • NSLock死锁问题

新浪 - 8 网络相关面试问题

  • 你都了解哪些状态码? HTTP 响应代码

  • GET和POST的区别?

    • get请求参数以?分隔拼接到URL后面,post请求参数在Body内部。
    • get参数长度显示2048个字符,post一般没有该限制。
    • get请求不安全,post请求比较安全。
  • 连接建立流程

  • HTTP的特点

    • 无连接
      • HTTP持久连接方案可解决该问题
      • 开启持久连接需要设置的头部字段
        • Connection: keep-alive 需要开启持久连接
        • time: 20 持续时间
        • max: 10 持久连接最多可发起的网络请求次数
    • 无状态
      • Cookie

        • 状态保存在客户端
        • 客户端发送的cookie在http请求报文的Cookie首部字段中。
        • 服务器端设置http响应报文的Set-Cookie首部字段。
      • Session

        • 状态存放在服务器端
        • Seesion需要依赖于Cookie机制实现。
  • 怎样判断一个请求是否结束?

    • Content-length: 1024根据所接受数据是否达到Content-length来判断。
    • chunkedpost请求会有多次返回,最后一次返回会有一个空的chunked。
  • Charles抓包原理

    • 当客户端和服务器建立连接时,Charles会拦截到服务器返回的证书(服务器公钥)
    • 然后动态生成一张伪造证书(Charles公钥/假公钥)发送给客户端
    • 客户端收到Charles证书后,进行验证;因为之前我们手机设置了信任,所以验证通过;(只要手机不信任这种证书,HTTPS还是能确保安全的)
    • 客户端生成会话密钥,使用Charles证书对会话密钥进行加密再传输给服务器
    • Charles拦截到客户端传输的数据,使用自己的Charles私钥进行解密得到会话密钥
    • 连接成功后,客户端和服务器通信,客户端对传输的数据使用会话密钥加密并使用公钥对数据摘要进行数字签名,一同传输给服务器;
    • Charles拦截到通信的数据,使用之前获得的会话密钥解密就能得到原始数据;
    • Charles同样也能篡改通信的数据:将篡改后的数据重新加密并重新生成摘要并使用之前获得的公钥进行数字签名,替换原本的签名,再传输给服务器;
    • 服务器收取到数据,按正常流程解密验证;
    • 服务器返回响应数据时,Charles也是类似拦截过程
  • HTTPS链接建立流程是怎样的?

    • 客户端向服务器发送一段报文,报文内容包括三部分,客户端支持的TLS协议版本,客户端支持的加密算法,一段随机数C。
    • 服务器返回客户端一段握手的报文消息,内容也包括三部分,服务器从客户端上报的多种加密算法中选择的加密算法,一段随机数S,服务器证书。
    • 客户端对服务器返回的证书进行验证,判断服务器是否是合法的服务器,即对服务器公钥进行验证。
    • 客户端通过预主密钥、一段随机数C、一段随机数S,组装会话密钥。
    • 客户端通过服务器的公钥对预主密钥进行加密传输。
    • 服务器通过私钥解密得到预主密钥。
    • 服务器通过预主密钥、一段随机数C、一段随机数S,组装会话密钥。
    • 客户端和服务器相互发送加密的握手消息,验证握手是否完成。
  • UDP

  • TCP如何做到可靠传输

  • 滑动窗口协议

    • 接收方有接收缓存,如果发送速度过快,可能造成溢出。
    • 接收方可以动态调整发送方的发送窗口,达到动态调整发送速率的目的。
    • 发送窗口和接收窗口是两个字段,位于TCP报文的首部。
  • 拥塞控制

  • 为什么要进行三次握手而不是两次?

  • DNS解析

新浪 - 9 设计模式、架构、三方库

设计模式

原型模式

  • NSCopying
    • 深拷贝
    • 浅拷贝

工厂模式

  • 简单工厂

  • 工厂方法模式

    • 在工厂方法模式中,我们不再提供一个统一的工厂类来创建所有的产品对象,而是针对不同的产品提供不同的工厂
  • 抽象工厂模式

    • 在工厂方法模式中一个具体工厂只生产一种具体产品, 但是有时候我们需要一个工厂能够生产多个具体产品对象, 而不是一个单一的具体对象,这就引入了抽象工厂模式。
  • 三种工厂方法关系

    • 当抽象工厂模式中每一个具体工厂类只创建一个产品对象,也就是只存在一个产品等级结构时,抽象工厂模式退化成工厂方法模式;
    • 当工厂方法模式中抽象工厂与具体工厂合并,提供一个统一的工厂来创建产品对象,并将创建对象的工厂方法设计为静态方法时,工厂方法模式退化成简单工厂模式。

单例模式

  • dispatch_once 线程安全 有加锁

适配器模式

  • 将一个类接口转化为客户代码需要的另一个接口。适配器使原本由于兼容性而不能协同工作的类可以工作在一起,消除了客户代码和目标对象的类之间的耦合性。

组合模式

  • 组合模式为树形结构的面向对象提供了一种灵活的解决方案
  • view的结构使用组合模式

装饰模式

  • 用于替代继承的技术, 无需定义子类就可以给原来的类增加新的功能, 使用对象关联关系替代继承。
  • 最终执行的是装饰器接入的基本组件中的函数。
  • OC中是category
  • swift中是extension

外观模式

  • 一个客户类要和多个业务类交互, 而这些交互的业务类经常作为一个整体出现, 这个时候可以使用外观模式, 为客户端提供一个简化的入口, 简化客户类和业务类的交互.
  • 类似很多三方库中的manager,用于衔接其他工具类。

代理模式

责任链模式

  • 响应者链
  • NSResponder

命令模式

  • NSInvocation
    • NSInvocation类的实例用于封装Objective-C消息。一个调用对象中含有一个目标对象、一个方法选择器、以及方法参数。
    • 您可以动态地改变调用对象中消息的目标及其参数,一旦消息被执行,您就可以从该对象得到返回值。通过一个调用对象可以多次调用目标或参数不同的消息。
    • 创建NSInvocation对象需要使用NSMethodSignature对象,该对象负责封装与方法参数和返回值有关系的信息。NSMethodSignature对象的创建又需要用到一个方法选择器。
  • Target&Action
    • 当您用Interface Builder构建程序的用户界面时,可以对控件的动作和目标进行设置。您因此可以让控件具有定制的行为,而又不必为控件本身书写任何的代码。动作选择器和目标连接被归档在nib文件中,并在nib文件被解档时复活。您也可以通过向控件或它的单元对象发送setTarget:和setAction:消息来动态地改变目标和动作。

解释器模式

  • 比如判断邮件地址、电话号码、证件号码是否是正确的正则表达式,就是应用了解释器模式。

迭代器模式

  • 这种模式提供一种顺序访问聚合对象(也就是一个集合)中的元素,而又不必暴露潜在表示的方法。迭代器模式将访问和遍历集合元素的责任从集合对象转移到迭代器对象。迭代器定义一个访问集合元素的接口,并对当前元素进行跟踪。不同的迭代器可以执行不同的遍历策略。
  • Foundation框架中的NSEnumerator类实现了迭代器模式。

中介者模式

  • MVC模式是中介者模式的一种表现形式,Comtroller(中介者)承担两个同事类(View和Modle)之间的中转和协调作用。

备忘录模式

  • 这种模式在不破坏封装的情况下,捕捉和外部化对象的内部状态,使对象在之后可以回复到该状态。备忘录模式使关键对象的重要状态外部化,同时保持对象的内聚性。
  • 通过NSCoder对象可以执行编解码操作,在编解码过程中最好使用键化的归档技术(需要调用NSKeyedArchiver和NSKeyedUnarchiver类的方法)。被编解码的对象必须遵循NSCoding协议;该协议的方法在归档过程中会被调用。

观察者模式

  • KVO

状态模式

  • 同一操作在不同状态,执行不同的方案。
  • 代码中包含大量与对象状态有关的条件语句: 一个操作中含有庞大的多分支的条件(if else(或switch case)语句,且这些分支依赖于该对象的状态。
  • 这个状态通常用一个或多个枚举常量表示。通常, 有多个操作包含这一相同的条件结构。
  • State模式将每一个条件分支放入一个独立的类中。这使得你可以根据对象自身的情况将对象的状态作为一个对象,这一对象可以不依赖于其他对象而独立变化。

策略模式

iOS设计模式之策略模式

模版方法模式

不常使用

  • 建造者模式
  • 桥接模式
  • 访问者模式

《Objective-C 高级编程》

《Objective-C 高级编程》干货三部曲(一):引用计数篇

《Objective-C 高级编程》干货三部曲(二):Blocks篇

《Objective-C 高级编程》干货三部曲(三):GCD篇

《Effective Objective-C》

《Effective Objective-C》干货三部曲(一):概念篇

《Effective Objective-C》干货三部曲(二):规范篇

《Effective Objective-C》干货三部曲(三):技巧篇

网络协议

小码哥《网络协议从入门到底层原理》笔记(一、二):基本概念、集线器、网桥、交换机、路由器

小码哥《网络协议从入门到底层原理》笔记(三):MAC地址、IP地址

小码哥《网络协议从入门到底层原理》笔记(四):路由、区域网、NAT

小码哥《网络协议从入门到底层原理》笔记(五):物理层、数据链路层

小码哥《网络协议从入门到底层原理》笔记(六):网络层

小码哥《网络协议从入门到底层原理》笔记(七):传输层、UDP、TCP可靠传输

小码哥《网络协议从入门到底层原理》笔记(八):TCP可靠传输、流量控制、拥塞控制

小码哥《网络协议从入门到底层原理》笔记(九):TCP序号、确认号、建立连接、释放连接

小码哥《网络协议从入门到底层原理》笔记(十):应用层、域名、DNS解析

小码哥《网络协议从入门到底层原理》笔记(十一):HTTP、报文、请求头、状态码、form

知识盲区面试题