题库整理(二)

1.mach-0文件的结构都有哪些,iOS程序main()函数之前和之后分别都做了些什么?

  • Mach-O文件格式是OS X与iOS系统上的可执行文件格式,像我们编译过程产生的.O文件,以及程序的可执行文件,动态库等都是Mach-O文件,它的结构如下:


    题库整理(二)_第1张图片
    mach-0.png

Header: 保存了一些基本信息,包括了该文件运行的平台、文件类型、LoadCommands的个数等。
LoadCommands: 可以理解为加载命令,在加载Mach-O文件时会使用这里的数据来确定内存的分布以及相关的加载命令。比如我们的main函数的加载地址,程序所需的dyld的文件路径,以及相关依赖库的文件路径。
Data:这里包含了具体的代码、数据等。

  • t(App总启动时间) = t1(main()之前的加载时间) + t2(main()之后的加载时间),t1 = 系统dylib(动态链接库)和自身App可执行文件的加载;
    t2 = main方法执行之后到AppDelegate类中的- (BOOL)Application:(UIApplication *)Application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions方法执行结束前这段时间,主要是构建第一个界面,并完成渲染展示。

App开始启动后,系统首先加载可执行文件(自身App的所有.o文件的集合),然后加载动态链接库dyld,dyld是一个专门用来加载动态链接库的库。 执行从dyld开始,dyld从可执行文件的依赖开始, 递归加载所有的依赖动态链接库。
动态链接库包括:iOS 中用到的所有系统 framework,加载OC runtime方法的libobjc,系统级别的libSystem,例如libdispatch(GCD)和libsystem_blocks (Block)。

无论对于系统的动态链接库还是对于App本身的可执行文件而言,他们都算是image(镜像),而每个App都是以image(镜像)为单位进行加载的,那么image究竟包括哪些呢?

1.executable可执行文件 比如.o文件。
2.dylib 动态链接库 framework就是动态链接库和相应资源包含在一起的一个文件夹结构。
3.bundle 资源文件 只能用dlopen加载,不推荐使用这种方式加载。

所有动态链接库和我们App中的静态库.a和所有类文件编译后的.o文件最终都是由dyld(the dynamic link editor)Apple的动态链接器来加载到内存中。每个image都是由一个叫做ImageLoader的类来负责加载(一一对应).

  • 动态链接库加载的具体流程

动态链接库的加载步骤具体分为5步:
1.load dylibs image 读取库镜像文件
2.Rebase image
3.Bind image

4.Objc setup
5.initializers

load dylibs image
在每个动态库的加载过程中, dyld需要:
分析所依赖的动态库
找到动态库的mach-o文件
打开文件
验证文件
在系统核心注册文件签名
对动态库的每一个segment调用mmap()

tips:
通常的,一个App需要加载100到400个dylibs, 但是其中的系统库被优化,可以很快的加载。 针对这一步骤的优化有:
1.减少非系统库的依赖
2.合并非系统库
3.使用静态资源,比如把代码加入主程序

rebase/bind
由于ASLR(address space layout randomization)的存在,可执行文件和动态链接库在虚拟内存中的加载地址每次启动都不固定,所以需要这2步来修复镜像中的资源指针,来指向正确的地址。 rebase修复的是指向当前镜像内部的资源指针; 而bind指向的是镜像外部的资源指针。
rebase步骤先进行,需要把镜像读入内存,并以page为单位进行加密验证,保证不会被篡改,所以这一步的瓶颈在IO。bind在其后进行,由于要查询符号表,来指向跨镜像的资源,加上在rebase阶段,镜像已被读入和加密验证,所以这一步的瓶颈在于CPU计算。

tips:
优化该阶段的关键在于减少__DATA segment中的指针数量。我们可以优化的点有:
1.减少Objc类数量, 减少selector数量
2.减少C++虚函数数量
3.转而使用swift stuct(其实本质上就是为了减少符号的数量)

Objc setup
这一步主要工作是:
1.注册Objc类 (class registration)
2.把category的定义插入方法列表 (category registration)
3.保证每一个selector唯一 (selctor uniquing)

initializers
以上三步属于静态调整(fix-up),都是在修改__DATA segment中的内容,而这里则开始动态调整,开始在堆和堆栈中写入内容。 在这里的工作有:
1.Objc的+load()函数
2.C++的构造函数属性函数 形如attribute((constructor)) void DoSomeInitializationWork()
3.非基本类型的C++静态全局变量的创建(通常是类或结构体)(non-trivial initializer) 比如一个全局静态结构体的构建,如果在构造函数中有繁重的工作,那么会拖慢启动速度

main()调用之前总结:

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

  • main()调用之后的加载时间

在main()被调用之后,App的主要工作就是初始化必要的服务,显示首页内容等。而我们的优化也是围绕如何能够快速展现首页来开展。 App通常在AppDelegate类中的- (BOOL)Application:(UIApplication *)Application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions方法中创建首页需要展示的view,然后在当前runloop的末尾,主动调用CA::Transaction::commit完成视图的渲染。
而视图的渲染主要涉及三个阶段:
1.准备阶段 这里主要是图片的解码
2.布局阶段 首页所有UIView的- (void)layoutSubViews()运行
3.绘制阶段 首页所有UIView的- (void)drawRect:(CGRect)rect运行
再加上启动之后必要服务的启动、必要数据的创建和读取,这些就是我们可以尝试优化的地方

因此,对于main()函数调用之前我们可以优化的点有:

1.不使用xib,直接视用代码加载首页视图
2.NSUserDefaults实际上是在Library文件夹下会生产一个plist文件,如果文件太大的话一次能读取到内存中可能很耗时,这个影响需要评估,如果耗时很大的话需要拆分(需考虑老版本覆盖安装兼容问题)
3.每次用NSLog方式打印会隐式的创建一个Calendar,因此需要删减启动时各业务方打的log,或者仅仅针对内测版输出log
4.梳理应用启动时发送的所有网络请求,是否可以统一在异步线程请求

  • 针对app我们可以优化的点如下:

1.纯代码方式而不是storyboard加载首页UI。
2.对didFinishLaunching里的函数考虑能否挖掘可以延迟加载或者懒加载,需要与各个业务方pm和rd共同check 对于一些已经下线的业务,删减冗余代码。
3.对于一些与UI展示无关的业务,如微博认证过期检查、图片最大缓存空间设置等做延迟加载
4.对实现了+load()方法的类进行分析,尽量将load里的代码延后调用。
5.上面统计数据显示展示feed的导航控制器页面(NewsListViewController)比较耗时,对于viewDidLoad以及viewWillAppear方法中尽量去尝试少做,晚做,不做。

2.app哪些情况会产生崩溃
1.数组越界访问
2.调用了未实现的方法
3.野指针
4.返回空cell
5.类释放时未remove通知,之后收到通知
6.类释放时delegate未置空,之后被回调
7.内存暴涨
8.app升级改变了数据结构
9.字符串的截取越界导致的崩溃
3.事件的传递和响应机制

  • 事件的产生

1.发生触摸事件后,系统会将该事件加入到一个由UIApplication管理的事件队列中,为什么是队列而不是栈?因为队列的特点是FIFO,即先进先出,先产生的事件先处理才符合常理,所以把事件添加到队列。
2.UIApplication会从事件队列中取出最前面的事件,并将事件分发下去以便处理,通常,先发送事件给应用程序的主窗口(keyWindow)。
主窗口会在视图层次结构中找到一个最合适的视图来处理触摸事件,这也是整个事件处理过程的第一步。
3.找到合适的视图控件后,就会调用视图控件的touches方法来作具体的事件处理。

  • 事件的传递

1.触摸事件的传递是从父控件传递到子控件
2.也就是UIApplication->window->寻找处理事件最合适的view
tips: 如果父控件不能接受触摸事件,那么子控件就不可能接收到触摸事件

事件的传递顺序是这样的:
  产生触摸事件->UIApplication事件队列->[UIWindow hitTest:withEvent:]->返回更合适的view->[子控件 hitTest:withEvent:]->返回最合适的view

事件传递给窗口或控件的后,就调用hitTest:withEvent:方法寻找更合适的view。所以是,先传递事件,再根据事件在自己身上找更合适的view。
不管子控件是不是最合适的view,系统默认都要先把事件传递给子控件,经过子控件调用子控件自己的hitTest:withEvent:方法验证后才知道有没有更合适的view。即便父控件是最合适的view了,子控件的hitTest:withEvent:方法还是会调用,不然怎么知道有没有更合适的!即,如果确定最终父控件是最合适的view,那么该父控件的子控件的hitTest:withEvent:方法也是会被调用的。

  • 事件的响应

触摸事件处理的整体过程
1>用户点击屏幕后产生的一个触摸事件,经过一系列的传递过程后,会找到最合适的视图控件来处理这个事件2>找到最合适的视图控件后,就会调用控件的touches方法来作具体的事件处理touchesBegan…touchesMoved…touchedEnded…3>这些touches方法的默认做法是将事件顺着响应者链条向上传递(也就是touch方法默认不处理事件,只传递事件),将事件交给上一个响应者进行处理

响应者链条:在iOS程序中无论是最后面的UIWindow还是最前面的某个按钮,它们的摆放是有前后关系的,一个控件可以放到另一个控件上面或下面,那么用户点击某个控件时是触发上面的控件还是下面的控件呢,这种先后关系构成一个链条就叫“响应者链”。也可以说,响应者链是由多个响应者对象连接起来的链条。

事件的传递与响应:

1、当一个事件发生后,事件会从父控件传给子控件,也就是说由UIApplication -> UIWindow -> UIView -> initial view,以上就是事件的传递,也就是寻找最合适的view的过程。
2、接下来是事件的响应。首先看initial view能否处理这个事件,如果不能则会将事件传递给其上级视图(inital view的superView);如果上级视图仍然无法处理则会继续往上传递;一直传递到视图控制器view controller,首先判断视图控制器的根视图view是否能处理此事件;如果不能则接着判断该视图控制器能否处理此事件,如果还是不能则继续向上传 递;(对于第二个图视图控制器本身还在另一个视图控制器中,则继续交给父视图控制器的根视图,如果根视图不能处理则交给父视图控制器处理);一直到 window,如果window还是不能处理此事件则继续交给application处理,如果最后application还是不能处理此事件则将其丢弃。
3、在事件的响应中,如果某个控件实现了touches...方法,则这个事件将由该控件来接受,如果调用了[supertouches….];就会将事件顺着响应者链条往上传递,传递给上一个响应者;接着就会调用上一个响应者的touches….方法。

  • 如何做到一个事件多个对象处理:

因为系统默认做法是把事件上抛给父控件,所以可以通过重写自己的touches方法和父控件的touches方法来达到一个事件多个对象处理的目的。

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{ 
// 1.自己先处理事件...
NSLog(@"do somthing...");
// 2.再调用系统的默认做法,再把事件交给上一个响应者处理
[super touchesBegan:touches withEvent:event]; 
}
  • tips:事件的传递和响应的区别:

事件的传递是从上到下(父控件到子控件),事件的响应是从下到上(顺着响应者链条向上传递:子控件到父控件。

4.dispatch_barrier_async和dispatch_group

  • dispatch_barrier_async使用Barrier Task方法Dispatch Barrier解决多线程并发读写同一个资源发生死锁

Dispatch Barrier确保提交的闭包是指定队列中在特定时段唯一在执行的一个。在所有先于Dispatch Barrier的任务都完成的情况下这个闭包才开始执行。轮到这个闭包时barrier会执行这个闭包并且确保队列在此过程不会执行其它任务。闭包完成后队列恢复。需要注意dispatch_barrier_async只在自己创建的队列上有这种作用,在全局并发队列和串行队列上,效果和dispatch_sync一样。

  • Block组合Dispatch_groups

dispatch groups是专门用来监视多个异步任务。dispatch_group_t实例用来追踪不同队列中的不同任务。
当group里所有事件都完成GCD API有两种方式发送通知,第一种是dispatch_group_wait,会阻塞当前进程,等所有任务都完成或等待超时。第二种方法是使用dispatch_group_notify,异步执行闭包,不会阻塞。

你可能感兴趣的:(题库整理(二))