文章目录
-
- [1. 如何理解RunLoop](#1. 如何理解RunLoop)
- [2. 如何理解RunTime](#2. 如何理解RunTime)
- [3. KVO与KVC有什么联系](#3. KVO与KVC有什么联系)
- [4. iOS的事件传递过程](#4. iOS的事件传递过程)
- [5. CALayer 与 UIView 的关系](#5. CALayer 与 UIView 的关系)
- [6. iOS中为什么代理需要用 weak 修饰](#6. iOS中为什么代理需要用 weak 修饰)
- [7. Block 为什么要用 copy 修饰](#7. Block 为什么要用 copy 修饰)
- [8. 什么是 Block](#8. 什么是 Block)
- [9. iOS 是如何实现 APNs 的](#9. iOS 是如何实现 APNs 的)
- [10. 谈谈对内存管理的理解](#10. 谈谈对内存管理的理解)
- [11. 什么是内存池](#11. 什么是内存池)
- [12. 为什么Block 使用了 __block,就可以修改block内部的变量](#12. 为什么Block 使用了 __block,就可以修改block内部的变量)
- [13. iOS的启动优化都有哪些](#13. iOS的启动优化都有哪些)
- [14. 阐述一下APP的启动流程](#14. 阐述一下APP的启动流程)
- [15. KVC 原理](#15. KVC 原理)
- [16. KVO 原理](#16. KVO 原理)
- [17. category 为什么不能添加属性?](#17. category 为什么不能添加属性?)
- [18. 函数定义时 - 和 + 的含义及区别](#18. 函数定义时 - 和 + 的含义及区别)
- [19. 类扩展和分类的区别](#19. 类扩展和分类的区别)
- [20. App的启动流程](#20. App的启动流程)
- [21. 谈谈对单例模式的理解](#21. 谈谈对单例模式的理解)
- [22. 谈一谈对runtime的理解](#22. 谈一谈对runtime的理解)
- [23. 如何查看手机中的崩溃日志?](#23. 如何查看手机中的崩溃日志?)
- [24. App启动优化](#24. App启动优化)
1. 如何理解RunLoop
Runloop
(运行循环)是iOS和macOS中的一个核心概念,它负责管理事件和计时器,以确保应用程序能够在正确的时间响应用户的输入,并在不占用过多资源的情况下保持活动状态。
在一个iOS或macOS应用程序中,有一个主线程,所有的用户界面更新、网络请求、定时器、事件处理等都是在该线程中执行的。而 Runloop 就是这个线程中的一个对象,它不断地监听系统事件,如触摸事件、定时器事件等,一旦有事件发生,就会通知相应的处理方法来处理该事件。
Runloop
监听的事件主要分为两种:输入源(Input Source)和定时源(Timer Source)。输入源包括触摸事件、键盘事件、鼠标事件等,定时源则是基于时间的触发器,如 NSTimer、CADisplayLink 等。当事件发生时,Runloop
会将事件分发给相应的处理方法,并在事件处理完成后继续监听事件。
Runloop 的好处在于它可以让应用程序在等待事件时保持活动状态,同时也能够在没有事件时降低资源占用。因此,Runloop 是 iOS 和 macOS 应用程序能够保持流畅和响应的关键之一。
2. 如何理解RunTime
Runtime
(运行时)是指程序在运行期间的行为,即代码被编译成二进制文件之后,在运行时期间的行为。在编译时,我们可以预测代码的行为,但是在运行时,程序的行为往往会有一些意外情况,比如动态创建对象、动态绑定方法等。
在Objective-C
中,Runtime是一个重要的概念,它为Objective-C
提供了一些高级特性,如消息传递、动态绑定和反射等。Objective-C
的对象都是在运行时创建的,它们的属性和方法也是在运行时添加的。因此,Objective-C
程序的运行时环境是非常动态的。
使用Runtime
可以做很多有用的事情,例如:
- 动态创建类、对象和方法;
- 拦截方法调用并修改方法实现;
- 动态交换方法的实现;
- 实现 KVO(键值观察)功能;
- 实现消息转发;
总的来说,Runtime
是一种强大的工具,它可以帮助我们更好地理解Objective-C
的内部机制,并为我们提供了一些有用的功能,让我们能够更加灵活地编写高效的代码。
3. KVO与KVC有什么联系
KVC
(键值编码)和KVO
(键值观察)是Objective-C
中的两个重要机制,它们之间有一定的联系,同时也有一定的区别。
-
KVC
是一种通过字符串访问对象属性的机制,它允许我们使用字符串来获取和设置对象的属性,而不需要显式地调用对象的getter
和setter
方法。KVC
的核心是通过key
来访问对象的属性,即通过字符串访问对象的属性。 -
KVO
则是一种观察者模式的实现,它允许一个对象监听另一个对象的某个属性的变化,并在该属性发生变化时自动得到通知。在iOS开发中,我们通常使用KVO
来实现对于UI控件的响应、数据模型的观察等场景。
KVC
和KVO
之间的联系在于,KVO
的实现需要使用到KVC
。当我们对一个对象的属性进行观察时,KVO
会自动为被观察的对象动态生成一个子类,并在该子类中重写被观察属性的setter
方法。在该setter
方法中,KVO
会添加一些通知的逻辑,以便在属性值发生变化时发送通知。
因此,当我们使用KVO
时,可以通过KVC
来访问被观察对象的属性值。同时,KVC
也可以在不使用KVO
的情况下,直接访问对象的属性,从而使我们的代码更加简洁和易读。
需要注意的是,KVC
和KVO
虽然在一些场景下会同时使用,但是它们是两个不同的机制,应该根据具体的需求来选择是否使用它们。
4. iOS的事件传递过程
事件从UIApplication
开始,按照视图层次结构自顶向下地传递。在这个过程中,系统会根据事件类型和响应者链来决定将事件发送给哪个视图或控件。当事件传递到某个视图或控件时,该视图或控件会尝试处理该事件。如果该视图或控件不能处理该事件,则会将该事件转发给下一个响应者。这个过程会一直持续到某个响应者处理了该事件,或者事件传递到UIApplication时结束。
当事件处理完成后,系统会对事件做出最终的响应。例如,在触摸事件中,系统会根据手指的状态来判断该事件是否是一个单击、双击、长按等手势。同时,系统还会在事件结束时对相关的UI元素进行更新和重绘。
在事件传递过程中,可以通过重写视图或控件的响应方法来改变事件的传递和处理行为。例如,可以重写hitTest:withEvent:
方法来指定事件的响应对象;重写pointInside:withEvent:
方法来指定响应区域;重写touchesBegan:withEvent:
方法来处理触摸事件等。
总之,在iOS中,事件传递过程是一个复杂而又精细的机制。了解事件传递过程对于优化应用程序的响应速度和用户体验有着重要的意义。
5. CALayer 与 UIView 的关系
CALayer
是UIView
的底层实现,可以看作是UIView
的图层。每个UIView
都有一个对应的CALayer
对象,它负责管理UIView
的内容和显示。UIView
提供了一些高级的界面控制功能,例如响应用户交互事件、布局管理、自动绘制等,而CALayer
则负责渲染UIView
的内容,包括绘制、变换、动画等。
UIView
和CALayer
的关系可以用以下两种方式来理解:
-
UIView
是CALayer
的外观:UIView
提供了许多高级的界面控制功能,例如用户交互、布局管理等,这些功能都建立在CALayer
的基础之上。因此,可以将UIView
看作是CALayer
的外观,它通过对CALayer
的属性进行设置,来控制图层的显示和行为。 -
UIView
是CALayer
的管理者:UIView
负责管理CALayer
对象的创建、销毁、布局等,它维护了一个CALayer
的层级结构,并负责对图层的属性进行设置。UIView
还提供了一些高级的功能,例如自动绘制、动画效果等,这些功能都是基于CALayer
实现的。
因此,可以说CALayer
是UIView
的底层实现,UIView
提供了对CALayer
的封装,使得开发者可以更加方便地使用和管理图层。在开发中,如果需要实现一些高级的界面效果,例如复杂的动画、图层蒙版等,就需要直接使用CALayer
来实现。
6. iOS中为什么代理需要用 weak 修饰
避免循环引用:如果代理对象使用strong
来修饰,那么代理对象就会对委托对象进行强引用,而委托对象又会对代理对象进行引用,从而形成循环引用。这样就会导致对象无法释放,造成内存泄漏。因此,使用weak
来修饰代理对象可以避免循环引用问题。
被代理对象通常具有更长的生命周期:在大多数情况下,代理对象是由被代理对象持有的,因此被代理对象通常具有更长的生命周期。如果使用strong
来修饰代理对象,那么代理对象就会一直存在于内存中,即使被代理对象已经不存在了。这样就会造成内存浪费。
总之,使用weak
来修饰代理对象可以避免循环引用问题,同时也更符合代理对象与被代理对象之间的实际关系。
7. Block 为什么要用 copy 修饰
Block
在定义时会捕获作用域中的变量和对象,并将其保存在一个数据结构中,以便在后续的调用中使用。
由于Block
可能在定义时保存了局部变量或对象的引用,如果在Block
在定义后延迟执行时,这些局部变量或对象已经被销毁,那么在Block
执行时就会访问到已经释放的内存空间,导致程序崩溃。因此,为了避免这种问题,Block
需要使用copy
修饰。
使用copy
修饰Block
会将其从栈区复制到堆区,这样就能保证在Block
执行时,其所依赖的对象和变量的引用不会因为作用域的改变而失效。具体来说,当Block
被复制到堆区时,其所依赖的对象和变量也会被一并复制到堆区,这样就能保证在Block
执行时,其所依赖的对象和变量的引用是有效的。
需要注意的是,在ARC
(Automatic Reference Counting
)环境下,如果Block
没有捕获任何对象,则可以使用strong
来修饰,因为这种情况下Block
不会持有任何对象的引用。但是如果Block
捕获了对象或变量,则仍然需要使用copy
来修饰,以确保Block
的正确性。
block
使用 copy
是从 MRC
遗留下来的"传统",在 MRC
中,方法内部的 block
是在栈区的。使用 copy
可以把它放到堆区;在 ARC
中写不写都行。
对于 block
使用 copy
还是 strong
效果是一样的,但写上 copy
也无伤大雅,还能时刻提醒我们:编译器自动对 block
进行了 copy
操作。如果不写 copy
,该类的调用者有可能会忘记或者根本不知道"编译器会自动对 block
进行了 copy
操作",他们有可能会在调用之前自行拷贝属性值。这种操作多余而低效。
8. 什么是 Block
Block
是一种用于封装一段代码的数据类型。Block
实际上是一个匿名函数,它可以捕获一些变量和常量,并将它们封装在一起,形成一个可以在需要时执行的代码块。Block
可以被当作一个对象来使用,它可以作为方法参数 、成员变量 、局部变量 、数组元素 等等。Block
中捕获的变量和常量被保存在Block
中,可以在Block
执行时使用。
在使用Block
时,需要注意以下几点:
Block
中捕获的变量需要在Block
执行时仍然存在。如果Block
中捕获的变量是局部变量,那么需要使用__block
修饰符来使其在Block
执行时仍然存在。
- Block
中使用的变量需要正确地被引用。如果Block
中使用了对象,那么需要使用strong
或weak
修饰符来指定对该对象的引用方式。如果Block
中使用了基本数据类型的变量,则不需要使用修饰符。
Block
可以作为参数传递给方法或函数,也可以在方法或函数中定义。如果Block
作为参数传递给方法或函数,则需要使用^
符号定义。
总之,Block
是一种非常强大和灵活的数据类型,它可以用于封装一段代码,并将其当做一个对象来使用。Block
可以捕获变量和常量,并且可以作为参数传递给方法或函数,在实际开发中非常常用。
9. iOS 是如何实现 APNs 的
APNs
(Apple Push Notification service
)是苹果提供的推送服务,它允许开发者向用户设备发送推送通知。iOS系统实现APNs的过程如下:
- iOS应用程序需要使用
APNs
推送服务,需要首先向APNs服务器注册。注册过程中,iOS应用程序会生成一个设备令牌(Device Token),并将该令牌发送给APNs服务器。设备令牌是一个唯一标识符,用于标识该设备的APNs推送服务。 - APNs服务器会将设备令牌保存在其数据库中,并为该设备生成一个注册ID(Registration ID),用于标识该设备在APNs服务器上的注册信息。
- 当开发者需要向某个设备发送推送通知时,需要将推送通知发送给APNs服务器。
- APNs服务器会根据设备令牌和注册ID,将推送通知发送到对应的设备上。
- 接收到推送通知的设备会显示通知内容,并执行相应的操作。
在实际开发中,iOS应用程序可以通过使用APNs SDK,来向APNs服务器注册、发送推送通知等操作。具体来说,可以使用APNs SDK中提供的API,通过创建连接、发送请求等方式,来实现对APNs服务器的访问和操作。同时,iOS应用程序也需要提供一些必要的配置信息,如证书、证书密码、设备令牌等等,以便与APNs服务器进行通信
10. 谈谈对内存管理的理解
在iOS中,主要采用了自动引用计数(Automatic Reference Counting
,ARC)的机制来进行内存管理。
ARC
是一种编译时自动插入内存管理代码的技术,它可以自动地分析对象的引用关系,并在对象不再被使用时自动释放对象所占用的内存。ARC
机制可以极大地简化内存管理的过程,减少了手动释放内存的工作量,提高了应用程序的性能和稳定性。
在使用ARC
时,需要注意以下几点:
- 对象的引用关系需要正确地维护。如果对象被多个变量引用,那么需要确保所有引用都正确地被处理。
- 对象的循环引用需要避免。循环引用会导致对象无法被正确地释放,从而导致内存泄漏。
- 对象的生命周期需要正确地管理。对象在不再被使用时应该及时释放,以便及时回收内存资源。
- 除了
ARC
机制外,iOS还提供了一些手动管理内存的方式,如MRC
(Manual Reference Counting
)和内存池技术等。在使用这些技术时,需要更加谨慎地管理对象的引用关系和生命周期,以免出现内存泄漏等问题。
11. 什么是内存池
内存池是一种优化内存分配和管理的技术,它可以提高内存分配和释放的效率,减少因频繁申请和释放内存而带来的性能损失。
内存池的基本思想是在程序初始化时预先分配一定数量的内存块,并把它们存放在一个池子里面。当需要申请内存时,不再使用系统提供的malloc()
或new
运算符来动态申请内存,而是直接从内存池中取出一个内存块进行使用。当不再需要这个内存块时,也不用立即将其释放,而是将其放回内存池中,以备后续使用。
内存池的优点主要有:
- 减少内存分配和释放的次数,降低内存碎片的产生,提高内存分配和释放的效率。
- 程序初始化时可以预先分配一定数量的内存,避免在程序运行过程中频繁地进行内存分配和释放,提高程序的性能。
- 内存池可以减少内存申请和释放带来的锁竞争,从而提高程序的并发性能。
在iOS开发中,内存池的应用比较广泛,例如在网络编程中使用的Socket连接池、数据库连接池、图片缓存池等。内存池技术可以有效地优化内存管理,提高程序的性能和稳定性,但需要注意合理地设计内存池的大小和对象的生命周期,以避免因内存池过大或过小而造成的资源浪费或性能损失。
12. 为什么Block 使用了 __block,就可以修改block内部的变量
__block
修饰符标记后,block
就会访问标记变量本身内存地址,而未标记对象则访问截获拷贝后的变量内存地址、
13. iOS的启动优化都有哪些
14. 阐述一下APP的启动流程
- 系统先读取App的可执行文件(
Mach-O
文件),获取到dyld
(动态库)的路径,并加载dyld
(动态库链接程序)。 dyld
去初始化运行环境、开启缓存策略(冷热启动)、加载依赖库(读取文件、验证、注册到系统核心)、可执行文件、链接依赖库,并调用每个依赖库的初始化方法。- 在上一步runtime被初始化,当所有的依赖库初始化后,程序可执行文件进行初始化,这个时候runtime会对项目中的所有类进行类结构初始化,然后调用所有类的
+load
方法。
1、runtime初始化方法 _objc_init 中最后注册了两个通知:
map_images: 主要是在镜像加载进内容后对其二进制内容进行解析,初始化里面的类结构等
load_images: 主要是调用call_load_methods 按照继承层次依次调用Class的 +load方法 然后是Category的+ load方法。(call_load_methods 调用load 是通过方法地址直接调用的load方法,并不是通过消息机制,这就是为什么分类中的load方法并不会覆盖主类以及其他同主类的分类里的load 方法实现了。)
2、runtime 调用项目中所有的load方法时,所有的类的结构已经初始化了,此时在load方法中可以使用任何类创建实例并给他们发送消息。
4、最后dyld返回main函数地址,main函数被调用。dyld会缓存上一次把信息加载内存的缓存,所以第二次比第一次启动快一点。
pre-main
初始化空间,加载镜像,载入动态库链接器,链接动态库,objcsetup,callloadmethod,
动态库越多,启动越慢,+load方法多也会影响
优化方向:静态库,cocoapods :linkage => :static指令 将所有的动态库转为静态库。 要注意资源获取的方式问题。
合并动态库:cocoapods-pod-merge 支持合并动态库,要注意import方式会改变,作用域前缀要加上
main
main函数以后就是尽量减少第一个页面展示前的工作,有些组件库自己的工作可以派发到库自己处理,或者异步处理
启动项热拔插
main函数之前优化
启动的第一步是加载动态库,加载系统的动态库使很快的,因为可以缓存,而加载内嵌的动态库速度较慢。所以,提高这一步的效率的关键是:减少动态库的数量。
Rebase和Bind都是为了解决指针引用的问题。对于Objective C开发来说,主要的时间消耗在Class/Method的符号加载上,所以常见的优化方案是:
合并Category和功能类似的类。比如:UIView+Frame,UIView+AutoLayout...合并为一个
删除无用的方法和类。
15. KVC 原理
KVC是基于runtime
机制实现的。可以访问私有成员变量、可以间接修改私有变量的值。
KVC键值查找原理
setValue:forKey:搜索方式
1、首先搜索setKey:方法.(key指成员变量名, 首字母大写)
2、上面的setter方法没找到, 如果类方法accessInstanceVariablesDirectly返回YES. 那么按 _key, _isKey,key, iskey的顺序搜索成员名。(这个类方法是NSKeyValueCodingCatogery中实现的类方法, 默认实现为返回YES)
3、如果没有找到成员变量, 调用setValue:forUnderfinedKey:
valueForKey:的搜索方式
1、首先按getKey, key, isKey的顺序查找getter方法, 找到直接调用. 如果是BOOL、int等内建值类型, 会做NSNumber的转换.
2、上面的getter没找到, 查找countOfKey, objectInKeyAtindex, KeyAtindexes格式的方法. 如果countOfKey和另外两个方法中的一个找到, 那么就会返回一个可以响应NSArray所有方法的代理集合的NSArray消息方法.
3、还没找到, 查找countOfKey, enumeratorOfKey, memberOfKey格式的方法. 如果这三个方法都找到, 那么就返回一个可以响应NSSet所有方法的代理集合.
4、还是没找到, 如果类方法accessInstanceVariablesDirectly返回YES. 那么按 _key, _isKey, key, iskey的顺序搜索成员名.
5、再没找到, 调用valueForUndefinedKey.
16. KVO 原理
1.KVO是基于runtime机制实现的
2.当某个类的属性对象第一次被观察时,系统就会在运行期动态地创建该类的一个派生类,在这个派生类中重写基类中任何被观察属性的setter 方法。派生类在被重写的setter方法内实现真正的通知机制
3.如果原类为Person,那么生成的派生类名为NSKVONotifying_Person
4.每个类对象中都有一个isa指针指向当前类,当一个类对象的第一次被观察,那么系统会偷偷将isa指针指向动态生成的派生类,从而在给被监控属性赋值时执行的是派生类的setter方法
5.键值观察通知依赖于NSObject 的两个方法: willChangeValueForKey: 和 didChangevlueForKey:;在一个被观察属性发生改变之前, willChangeValueForKey:一定会被调用,这就 会记录旧的值。而当改变发生后,didChangeValueForKey:会被调用,继而 observeValueForKey:ofObject:change:context: 也会被调用。
深入
1.Apple 使用了 isa 混写(isa-swizzling)来实现 KVO 。当观察对象A时,KVO机制动态创建一个新的名为:?NSKVONotifying_A的新类,该类继承自对象A的本类,且KVO为NSKVONotifying_A重写观察属性的setter?方法,setter?方法会负责在调用原?setter?方法之前和之后,通知所有观察对象属性值的更改情况。
2.NSKVONotifying_A类剖析:在这个过程,被观察对象的 isa 指针从指向原来的A类,被KVO机制修改为指向系统新创建的子类 NSKVONotifying_A类,来实现当前类属性值改变的监听;
3.所以当我们从应用层面上看来,完全没有意识到有新的类出现,这是系统"隐瞒"了对KVO的底层实现过程,让我们误以为还是原来的类。但是此时如果我们创建一个新的名为"NSKVONotifying_A"的类(),就会发现系统运行到注册KVO的那段代码时程序就崩溃,因为系统在注册监听的时候动态创建了名为NSKVONotifying_A的中间类,并指向这个中间类了。
4.(isa 指针的作用:每个对象都有isa 指针,指向该对象的类,它告诉 Runtime 系统这个对象的类是什么。所以对象注册为观察者时,isa指针指向新子类,那么这个被观察的对象就神奇地变成新子类的对象(或实例)了。)?因而在该对象上对 setter 的调用就会调用已重写的 setter,从而激活键值通知机制。
5.子类setter方法剖析:KVO的键值观察通知依赖于 NSObject 的两个方法:willChangeValueForKey:和 didChangevlueForKey:,在存取数值的前后分别调用2个方法: 被观察属性发生改变之前,willChangeValueForKey:被调用,通知系统该 keyPath?的属性值即将变更;当改变发生后, didChangeValueForKey: 被调用,通知系统该 keyPath?的属性值已经变更;之后,?observeValueForKey:ofObject:change:context: 也会被调用。且重写观察属性的setter?方法这种继承方式的注入是在运行时而不是编译时实现的。
17. category 为什么不能添加属性?
category 它是在运行期决议的,因为在运行期,对象的内存布局已经确定,如果添加实例变量就会破坏类的内部布局,这对编译型语言来说是灾难性的。
extension看起来很像一个匿名的category,但是extension和有名字的category几乎完全是两个东西。 extension在编译期决议,它就是类的一部分,在编译期和头文件里的@interface以及实现文件里的@implement一起形成一个完整的类,它伴随类的产生而产生,亦随之一起消亡。extension一般用来隐藏类的私有信息,你必须有一个类的源码才能为一个类添加extension,所以你无法为系统的类比如NSString添加extension。
但是category则完全不一样,它是在运行期决议的。
就category和extension的区别来看,我们可以推导出一个明显的事实,extension可以添加实例变量,而category是无法添加实例变量的。
那为什么 使用Runtime技术中的关联对象可以为类别添加属性。
其原因是:关联对象都由AssociationsManager管理,AssociationsManager里面是由一个静态AssociationsHashMap来存储所有的关联对象的。这相当于把所有对象的关联对象都存在一个全局map里面。而map的的key是这个对象的指针地址(任意两个不同对象的指针地址一定是不同的),而这个map的value又是另外一个AssociationsHashMap,里面保存了关联对象的kv对。
如合清理关联对象?
runtime的销毁对象函数objc_destructInstance里面会判断这个对象有没有关联对象,如果有,会调用_object_remove_assocations做关联对象的清理工作。
18. 函数定义时 - 和 + 的含义及区别
19. 类扩展和分类的区别
分类不能添加成员变量【虽然可以添加属性 但是一旦调用就会报方法找不到的错误】 (可以通过runtime给分类间接添加成员变量),而类扩展可以添加成员变量;
分类中的属性不会自动实现set方法和get方法,而类扩展中的属性再转为底层时是可以自动实现set、get方法
类扩展中添加的新方法,不实现会报警告。categorygory中定义了方法不实现则没有这个问题
类扩展可以定义在.m文件中,这种扩展方式中定义的变量都是私有的,也可以定义在.h文件中,这样定义的代码就是共有的,类扩展在.m文件中声明私有方法是非常好的方式。
类扩展不能像分类那样拥有独立的实现部分(@implementation部分),也就是说,类扩展所声明的方法必须依托对应类的实现部分来实现。
20. App的启动流程
-
main函数之前
系统将App的可执行文件(
Mach-O
文件)和dyld
加载到内存,由dyld
进行动态链接。 -
设置相关环境变量
根据环境变量设置相应的值以及获取当前运行架构。例如配置环境变量打印启动流程耗时:
DYLD_PRINT_STATISTICS
和DYLD_PRINT_STATISTICS_DETAILS
。 -
加载共享缓存库
加载动态共享缓存库到动态库共享缓存区,例如UIKit、CoreFoundation等官方库。
-
加载动态库
把所有的可执行文件所依赖的动态库递归加载到内存中。
-
rebase和binding
iOS采用ASLR技术(地址空间布局随机化),加载App的内存地址是随机的,rebase会根据随机的偏移量对原来的地址做重定向。
binding进行符号绑定。指向image外部动态库的指针被符号(symbol)绑定。dyld需要去符号表里查找,找到对应的实现。
-
Objc setup
(1) 注册ObjC类
(2) 把category的定义插入方法列表
(3) selector唯一性检查
-
initializer
(1)调用所有类、分类的+load方法
(2)调用attribute((constructor))修饰的函数
(3)非基本类型的C++静态全局变量的创建(通常是类或结构体)
-
map_images
与load_images
map_images : dyld 将 image 加载进内存时 , 会触发该函数。
load_images : dyld 初始化 image 会触发该方法. ( 我们所熟知的 load 方法也是在此处调用 ) 。
dyld在初始化其他动态库之前,会最先初始化系统库libsystem,运行Runtime。系统库libsystem初始化完成后,就会初始化其他动态库,然后由Runtime调用map_images来读取类、方法、协议以及分类并存储到对应的表中(注意:分类并不是直接存,而是通过attachLists方法把分类的数据添加到类里面),然后Runtime会继续调用load_images调用所有类的load方法以及分类的load方法,这些都做完之后,通过dyld提供的回调_dyld_objc_notify_register,告诉dyld加载完毕,然后dyld就开始找主程序的入口main函数,最后进入程序的main函数。
- load方法的调用顺序
+load
方法是在load_images
中调用的。
load方法调用顺序为:先处理类,后处理分类;处理类的顺序是先父类,后子类。
在调用类的load方法时,做了递归处理,会先调用父类的load,然后再调用子类的load,所有类的load方法调用完成后,才会开始处理所有类的分类,分类的处理顺序取决于Mach-O头文件,和类的顺序没有直接关系。先后顺序即:父类->子类->所有类的分类。
- main函数阶段
main()
会调用UIApplicationMain()
,直至application:didFinishLaunchingWithOptions:
执行完毕,整个启动流程就完成了。
21. 谈谈对单例模式的理解
单例模式是一种设计模式,它确保一个类只有一个实例,并提供一个全局访问点来访问该实例。单例模式通常用于需要全局唯一对象的场景,比如配置管理、资源管理、日志记录等。
- 唯一实例: 单例模式确保一个类只有一个实例存在,无论何时何地,对该类的实例访问都只会返回同一个对象。这样可以避免创建多个对象造成资源的浪费,同时保证对象的唯一性。
- 全局访问点: 单例模式提供了一个全局访问点来访问唯一的实例,使得其他对象可以通过该访问点获取单例对象的引用。这样可以方便地在程序的任何地方使用单例对象,而不需要显式地创建对象或传递对象的引用。
- 延迟实例化: 单例模式通常采用延迟实例化的方式来创建单例对象,即在第一次访问单例对象时才创建对象。这样可以节省资源,避免在程序启动时就创建对象,提高程序的启动速度。
- 线程安全性: 在多线程环境下,单例模式需要考虑线程安全性的问题,确保在多线程环境下也能正确地创建和访问单例对象。常见的线程安全实现方式包括使用同步锁、静态变量初始化、延迟初始化等。
- 生命周期管理: 单例模式通常是全局存在的,因此需要考虑对象的生命周期管理,确保单例对象在合适的时机被正确地释放和清理。这样可以避免内存泄漏和资源泄漏问题。
- 全局状态共享: 单例模式可以用来实现全局状态共享,多个对象可以共享同一个单例对象的状态,从而实现数据共享和通信。
总的来说,单例模式是一种简单而又实用的设计模式,它提供了一种方便的方式来管理全局唯一对象,并确保对象的唯一性、延迟实例化、线程安全性等特性,适用于许多需要全局唯一对象的场景。但是在使用单例模式时需要注意线程安全性和生命周期管理等问题,以确保单例对象的正确使用和管理。
22. 谈一谈对runtime的理解
- 动态性 :
Runtime
是Objective-C
的运行时系统,它允许在程序运行时动态地创建类、修改类的结构、调用方法等。这种动态性使得Objective-C
具有很强的灵活性,可以实现一些在静态语言中难以实现的功能,比如动态派发、消息转发等。 - 元数据 :
Runtime
中存储了关于类、对象、方法等的元数据信息,包括类的名称、父类、实例变量、方法列表等。通过这些元数据信息,可以在运行时对类和对象进行操作,比如动态创建类和对象、动态添加方法等。 - 消息传递 : 在
Objective-C
中,方法调用是通过消息传递的方式实现的,即发送一个消息给对象,对象根据消息选择合适的方法来执行。Runtime
提供了消息传递的机制,包括消息发送、方法解析、消息转发等,使得Objective-C
具有动态派发的特性。 - 方法调用 :
Runtime
提供了一系列方法来实现方法的调用,包括实例方法调用、类方法调用、动态方法解析、方法交换等。通过这些方法,可以在运行时动态地调用方法,甚至可以修改方法的实现。 - 内存管理 :
Runtime
还提供了一些内存管理的功能,包括自动引用计数(ARC
)和自动释放池(Autorelease Pool
)。通过这些功能,可以在运行时对对象的内存进行管理,确保对象的引用计数正确,避免内存泄漏和野指针问题。
综上所述,Runtime
是 Objective-C
的运行时系统,它提供了一系列功能,包括动态性 、元数据 、消息传递 、方法调用 、内存管理 等,使得 Objective-C
具有很强的灵活性和动态性。深入理解 Runtime
可以帮助开发者更好地理解 Objective-C
语言的运行机制,从而编写出更加灵活和高效的代码。
23. 如何查看手机中的崩溃日志?
(1)点击xcode中的window;
(2)选择需要查看日志的设备;
(3)选择view of device log;
24. App启动优化
- main之前:
- 主要工作:加载dylib和可执行文件
- 优化方法:
- 减少不必要的
framework
,因为动态链接比较耗时; - 移除不需要的类;
- 合并类似的类和拓展;
- 压缩资源图片;
- 将不必须在
+load
方法中做的事情延迟到+initialize
中; - 删减没有被调用到或者已经废弃的方法
- 减少不必要的
- main之后:
- 主要工作:从
main
到applicationWillFinishLaunching执行完成的时间 显示首页 ; - 优化方法:
- 尽量使用纯代码实现UI;
- 对某些可以延迟加载的业务进行延迟加载;
- 减少启动时的网络请求;
- 主要工作:从