iOS 基础知识概述

iOS 基础知识概述

基本修饰属性

  • assion
    -基本用于修饰基本数据类型 如 int 等 是弱引用
  • copy
    • copy 修饰不可变对象 和strong 修饰符 一样对当前的对象进行一个强引用 copy 修饰可变对象 会对当前对象 进行深拷贝 生成一个不可变对象
    • 追问?用strong 修饰 会有什么问题 ?用strong 修饰可变对象 在某些可能会发生数据被修改风险 这个根据需求进行判断
    • 还可用用于对于block 进行修饰 本质意义要从栈上复制到堆上
    • 为什么 要把block 从栈上 复制到堆上?延长block的生命周期
  • strong
    • strong 是一个强引用 会对当前对象保证在合适的生命周期不会销毁
  • weak
    • weak 弱引用 不会增加引用计数 一般用于防止循环引用使用
  • notatomic
    • 非原子性 就不会保证线程读写安全
  • atomic
    • 原子性 在修饰对象时 会对当前对象set 和 get 方法进行加锁 保存读写安全

weak 实现原理

  • 当我们用weak 修饰对象时 会调用initWeak方法 判断对象是否为nil 不为nil 初始化一个新的weak指针对象的地址 调用storeWeak方法 更新指针指向 创建对应的弱引用表 释放时 调用clearDeallocating 函数 会根据对象的地址获取所有weak指针地址数组 ,然后遍历这个数据把期中的数据设置为nil 最后把这个从数据从这个weak表中 清理对象记录。
  • weak 相关问题

**Block **

  • 本质是一个oc 对象
  • Block 变量截取
    • 自动变量值被Block 截获 只能执行Block语法瞬间值。保存后就不能修改此值。Block 中使用自动变量后 在Block 的结构体实例中重写改自动变量也不会改变原先截获的自动变量
  • 截获对象
    • 而在ARC环境下,对于声明为__block的外部对象,在block内部会进行retain,以至于在block环境内能安全的引用外部对象。对于没有声明__block的外部对象,在block中也会被retain。

KVO和KVC 实现原理

  • 当某个类的属性对象第一次被观察时,系统就会在运行期动态地创建该类的一个派生类,在这个派生类中重写基类中任何被观察属性的setter 方法。派生类在被重写的setter方法内实现真正的通知机制
    如果原类为ClassName,那么生成的派生类名为NSKVONotifying_ClassName
    每个类对象中都有一个isa指针指向当前类,当一个类对象的第一次被观察,那么系统会偷偷将isa指针指向动态生成的派生类,从而在给被监控属性赋值时执行的是派生类的setter方法
    键值观察通知依赖于NSObject 的两个方法: willChangeValueForKey: 和 didChangevlueForKey:;在一个被观察属性发生改变之前, willChangeValueForKey:一定会被调用,这就 会记录旧的值。而当改变发生后,didChangeValueForKey:会被调用,继而 observeValueForKey:ofObject:change:context: 也会被调用。
    补充:KVO的这套实现机制中苹果还偷偷重写了class方法,让我们误认为还是使用的当前类,从而达到隐藏生成的派生类
  • kvc
    • KVC(key-Value coding) 键值编码,指iOS开发中,可以允许开发者通过Key名直接访问对象的属性,或者给对象的属性赋值。不需要调用明确的存取方法,这样就可以在运行时动态访问和修改对象的属性,而不是在编译时确定。
    • 程序优先调用setKey:属性值方法,代码通过setter方法完成设置。注意,这里的key是指成员变量名,首字母大小写要符合KVC的命名规范,下同
      如果没有找到setName:方法,KVC机制会检查+(BOOL)accessInstanceVariablesDirectly方法有没有返回YES,默认返回的是YES,如果你重写了该方法让其返回NO,那么在这一步KVC会执行setValue: forUndefineKey:方法,不过一般不会这么做。所以KVC机制会搜索该类里面有没有名为_key的成员变量,无论该变量是在.h,还是在.m文件里定义,也不论用什么样的访问修饰符,只要存在_key命名的变量,KVC都可以对该成员变量赋值。
      如果该类既没有setKey:方法,也没有_key成员变量,KVC机制会搜索_isKey的成员变量。
      同样道理,如果该类没有setKey:方法,也没有_key和_isKey成员变量,KVC还会继续搜索key和isKey的成员变量,再给他们赋值。
      如果上面列出的方法或者成员变量都不存在,系统将会执行该对象的setValue:forUndefinedKey:方法,默认是抛出异常。

runtime

对象:OC中的对象指向的是一个objc_object指针类型,typedef struct objc_object *id;从它的结构体中可以看出,它包括一个isa指针,指向的是这个对象的类对象,一个对象实例就是通过这个isa找到它自己的Class,而这个Class中存储的就是这个实例的方法列表、属性列表、成员变量列表等相关信息的

类:在OC中的类是用Class来表示的,实际上它指向的是一个objc_class的指针类型,typedef struct objc_class *Class

OC的Class类型包括如下数据(即:元数据metadata):super_class(父类类对象);name(类对象的名称);version、info(版本和相关信息);instance_size(实例内存大小);ivars(实例变量列表);methodLists(方法列表);cache(缓存);protocols(实现的协议列表);

当然也包括一个isa指针,这说明Class也是一个对象类型,所以我们称之为类对象,这里的isa指向的是元类对象(metaclass),元类中保存了创建类对象(Class)的类方法的全部信息。

为什么要设计metaclass
类对象、元类对象能够复用消息发送流程机制;
单一职责原则

为什么对象方法没有保存的对象结构体里,而是保存在类对象的结构体里?
方法是每个对象互相可以共用的,如果每个对象都存储一份方法列表太浪费内存,由于对象的isa是指向类对象的,当调用的时候,直接去类对象中查找就行了。可以节约很多内存空间的

class_ro_t 和 class_rw_t 的区别?
class_rw_t提供了运行时对类拓展的能力,而class_ro_t存储的大多是类在编译时就已经确定的信息。二者都存有类的方法、属性(成员变量)、协议等信息,不过存储它们的列表实现方式不同。简单的说class_rw_t存储列表使用的二维数组,class_ro_t使用的一维数组。 class_ro_t存储于class_rw_t结构体中,是不可改变的。保存着类的在编译时就已经确定的信息。而运行时修改类的方法,属性,协议等都存储于class_rw_t中

category如何被加载的,两个category的load方法的加载顺序,两个category的同名方法的加载顺序

+load 方法是 images 加载的时候调用,假设有一个 XXXClass 类,其主类和所有分类的 +load 都会被调用,优先级是先调用主类,且如果主类有继承链,那么加载顺序还必须是基类的 +load ,接着是父类,最后是子类;category 的 +load 则是按照编译顺序来的,先编译的先调用,后编译的后调用,可在 Xcode 的 BuildPhase 中查看
分类添加到了 rw = cls->data() 中的 methods/properties/protocols 中,实际上并无覆盖,只是查找到就返回了,导致本类函数无法加载。

initialize && Load

类第一次被使用到的时候会被调用,底层实现有个逻辑先判断父类是否被初始化过,没有则先调用父类,然后在调用当前类的 initialize 方法.

一个类 A 存在多个 category ,且 category中各自实现了 initialize 方法,这时候走的是 消息发送流程,也就说 initialize 方法只会调用一次,也就是最后编译的那个category中的 initialize 方法。
如果+load 方法中调用了其他类:比如 B 的某个方法,其实就是走消息发送流程,由于 B 没有初始化过,则会调用其 initialize 方法,但此刻 B 的 +load 方法可能还没有被系统调用过。

方法查询-> 动态解析-> 消息转发
【self test】会转换成 objc_megsend方法 检测当前targte 是否为nil 如果为nil则忽略
- 不是 会从当前对象 方法列表里面查找方法 如果找到了 就直接调用执行 如果没有找到就会去父类方法列表里面查找 如果还没有找到 就根类方法列表查找 如果还没有找到 就会走消息转发流程
- 通过resolveInstanceMethod 得知方法是否动态添加,YES则通过 class_addMethod 动态添加方法,处理消息,否则进入下一步、
- forwardingTargetForSelect 用于指定那个对象来响应消息。如果返回nil 则进入第三步
- methodSignatureForSelector 进行方法签名,可以将函数参数类型和返回值封装。如果返回nil 说明消息无法处理并报错
- 把 imp 指向_objc_msgForward函数指针 最后执行这个IMP
runLoop

线程和 RunLoop 之间是一一对应的,其关系是保存在一个全局的 Dictionary 里。线程刚创建时并没有 RunLoop,如果你不主动获取,那它一直都不会有。RunLoop 的创建是发生在第一次获取时,RunLoop 的销毁是发生在线程结束时。你只能在一个线程的内部获取其 RunLoop(主线程除外)。
mode
同时苹果还提供了一个操作 Common 标记的字符串:kCFRunLoopCommonModes (NSRunLoopCommonModes),你可以用这个字符串来操作 Common Items,或标记一个 Mode 为 “Common”。使用时注意区分这个字符串和其他 mode name。

  1. kCFRunLoopDefaultMode: App的默认 Mode,通常主线程是在这个 Mode 下运行的。
  2. UITrackingRunLoopMode: 界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响。
  3. kCFRunLoopCommonModes: 这是一个占位的 Mode,没有实际作用。

PerformSelecter
当调用 NSObject 的 performSelecter:afterDelay: 后,实际上其内部会创建一个 Timer 并添加到当前线程的 RunLoop 中。所以如果当前线程没有 RunLoop,则这个方法会失效。

当调用 performSelector:onThread: 时,实际上其会创建一个 Timer 加到对应的线程去,同样的,如果对应线程没有 RunLoop 该方法也会失效。

GCD

  • GCD中常用的函数有哪些及使用场景
    • dispatch_async 开启一个异步的网络请求
    • dispatch_after 简单的延迟执行的方式
    • dispatch_once 只执行一次的代码 创建单例
    • dispatch_group 维护一些异步任务的同步问题
    • dispatch_barrier_async 文件的读写操作时使用,保证读操作的准确性。另外,有一点需要注意,dispatch_barrier_sync和dispatch_barrier_async只在自己创建的并发队列上有效,在全局(Global)并发队列、串行队列上,效果跟dispatch_(a)sync效果一样。
    • dispatch_semaphore_signal
  • 同步、异步、串行、并行的区别
    • 同步 不开启线程
    • 异步 开启线程
    • 同步 串行 不开启线程 按顺序执行
    • 异步 串行 开启线程 按顺序执行
    • 同步 并行 不会新建线程 按顺序执行
    • 异步 并行 会开多条线程 操作无序
      11
    • dispatch_async(dispatch_get_main(), ^{})的实现原理
      - dispatch_async 指的是将指定的Block 异步的追加到指定的queue中 这个函数不做任何等待
      • dispatch_sync 将指定的block 同步追加到指定的queue 中在追加之后 dispatch——sync 会一直等待
    • performSelector:..afterDelay:的实现原理
      - 内部会创建一个Timer 并添加到当前线程的RunLoop 如果在子线程下 调用方法 是不会执行的 子线程Runloop 默认是不开启的
    • runloop与线程的关系
      • 线程和Runloop 之间是一一对应的,其关系是保存在一个全局的字典里。线程刚创建没有Runloop 。Runloop 的创建是发生在第一次获取时 Runloop 的销毁时发生在线程结束时 只能在一个线程内部获取其Runloop(主线程除外)

当调用 dispatch_async(dispatch_get_main_queue(), block) 时,libDispatch 会向主线程的 RunLoop 发送消息,RunLoop会被唤醒,并从消息中取得这个 block,并在回调 CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE() 里执行这个 block。但这个逻辑仅限于 dispatch 到主线程,dispatch 到其他线程仍然是由 libDispatch 处理的。

自动释放池(autoreleasepool)

Autorelease Pool 是由多个 AutoreleasePoolPage 对象以双向链表的方式组织起来的数据结构。

每个 AutoreleasePoolPage 只能存储有限个对象指针。当新的对象加入 Autorelease Pool 的时候,如果当前的 AutoreleasePoolPage 存储空间不够,会新初始化一个 AutoreleasePoolPage,加入到链表末端。

Autorelease Pool 可以被嵌套创建。创建一个新的 Autorelease Pool 的时候,会在当前 AutoreleasePoolPage 中插入边界对象 POOL_BOUNDARY,以和上一个 Autorelease Pool 以区分。

当 Autorelease Pool 销毁的时候,对 AutoreleasePoolPage 里存储的所有对象依次从后往前调用 release,直到遇到对象 POOL_BOUNDARY,表明当前 Autorelease Pool 中的对象已经被全部释放。

App启动后,苹果在主线程 RunLoop 里注册了两个 Observer,其回调都是 _wrapRunLoopWithAutoreleasePoolHandler()。

第一个 Observer 监视的事件是 Entry(即将进入Loop),其回调内会调用 _objc_autoreleasePoolPush() 创建自动释放池。其 order 是-2147483647,优先级最高,保证创建释放池发生在其他所有回调之前。

第二个 Observer 监视了两个事件: BeforeWaiting(准备进入休眠) 时调用_objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 释放旧的池并创建新池;Exit(即将退出Loop) 时调用 _objc_autoreleasePoolPop() 来释放自动释放池。这个 Observer 的 order 是 2147483647,优先级最低,保证其释放池子发生在其他所有回调之后。

在主线程执行的代码,通常是写在诸如事件回调、Timer回调内的。这些回调会被 RunLoop 创建好的 AutoreleasePool 环绕着,所以不会出现内存泄漏,开发者也不必显示创建 Pool 了。

Autoreleasepool是由多个AutoreleasePoolPage以双向链表的形式连接起来的,

Autoreleasepool的基本原理:在每个自动释放池创建的时候,会在当前的AutoreleasePoolPage中设置一个标记位,在此期间,当有对象调用autorelsease时,会把对象添加到AutoreleasePoolPage中,若当前页添加满了,会初始化一个新页,然后用双向量表链接起来,并把新初始化的这一页设置为hotPage,当自动释放池pop时,从最下面依次往上pop,调用每个对象的release方法,直到遇到标志位。 AutoreleasePoolPage结构如下

class AutoreleasePoolPage {
magic_t const magic;
id *next;//下一个存放autorelease对象的地址
pthread_t const thread; //AutoreleasePoolPage 所在的线程
AutoreleasePoolPage * const parent;//父节点
AutoreleasePoolPage *child;//子节点
uint32_t const depth;//深度,也可以理解为当前page在链表中的位置
uint32_t hiwat;
}

GCD

  • dispatch_sync 将任务 block 通过 push 到队列中,然后按照 FIFO 去执行。
  • dispatch_sync造成死锁的主要原因是堵塞的tid和现在运行的tid为同一个
  • dispatch_async 会把任务包装并保存,之后就会开辟相应线程去执行已保存的任务。
  • semaphore 主要在底层维护一个value的值,使用 signal 进行 + +1,wait进行-1。如果value的值大于或者等于0,则取消阻塞,否则根据timeout参数进行超时判断
  • dispatch_group 底层也是维护了一个 value 的值,等待 group 完成实际上就是等待value恢复初始值。而notify的作用是将所有注册的回调组装成一个链表,在 dispatch_async 完成时判断 value 是不是恢复初始值,如果是则调用dispatch_async异步执行所有注册的回调。
  • dispatch_once 通过一个静态变量来标记 block 是否已被执行,同时使用加锁确保只有一个线程能执行,执行完 block 后会唤醒其他所有等待的线程。
    列举你知道的线程同步策略?
    OSSpinLock 自旋锁,已不再安全,除了这个锁之外,下面写的锁,在等待时,都会进入线程休眠状态,而非忙等
    os_unfair_lock atomic就是使用此锁来保证原子性的
    pthread_mutex_t 互斥锁,并且支持递归实现和条件实现
    NSLock,NSRecursiveLock,基本的互斥锁,NSRecursiveLock支持递归调用,都是对pthread_mutex_t的封装
    NSCondition,NSConditionLock,条件锁,也都是对pthread_mutex_t的封装
    dispatch_semaphore_t 信号量
    @synchronized 也是pthread_mutex_t的封装
    有哪几种锁?各自的原理?它们之间的区别是什么?最好可以结合使用场景来说
    自旋锁:自旋锁在无法进行加锁时,会不断的进行尝试,一般用于临界区的执行时间较短的场景,不过iOS的自旋锁OSSpinLock不再安全,主要原因发生在低优先级线程拿到锁时,高优先级线程进入忙等(busy-wait)状态,消耗大量 CPU 时间,从而导致低优先级线程拿不到 CPU 时间,也就无法完成任务并释放锁。这种问题被称为优先级反转。
    互斥锁:对于某一资源同时只允许有一个访问,无论读写,平常使用的NSLock就属于互斥锁
    读写锁:对于某一资源同时只允许有一个写访问或者多个读访问,iOS中pthread_rwlock就是读写锁
    条件锁:在满足某个条件的时候进行加锁或者解锁,iOS中可使用NSConditionLock来实现
    递归锁:可以被一个线程多次获得,而不会引起死锁。它记录了成功获得锁的次数,每一次成功的获得锁,必须有一个配套的释放锁和其对应,这样才不会引起死锁。只有当所有的锁被释放之后,其他线程才可以获得锁,iOS可使用NSRecursiveLock来实现

哪些场景可以触发离屏渲染?(知道多少说多少)
添加遮罩mask
添加阴影shadow
设置圆角并且设置masksToBounds为true
设置allowsGroupOpacity为true并且layer.opacity小于1.0和有子layer或者背景不为空
开启光栅化shouldRasterize=true

响应链
当一个事件发生后,事件会从父控件传给子控件,也就是说由UIApplication -> UIWindow -> UIView -> initial view,以上就是事件的传递,也就是寻找最合适的view的过程。

2、接下来是事件的响应。首先看initial view能否处理这个事件,如果不能则会将事件传递给其上级视图(inital view的superView);如果上级视图仍然无法处理则会继续往上传递;一直传递到视图控制器view controller,首先判断视图控制器的根视图view是否能处理此事件;如果不能则接着判断该视图控制器能否处理此事件,如果还是不能则继续向上传 递;(对于第二个图视图控制器本身还在另一个视图控制器中,则继续交给父视图控制器的根视图,如果根视图不能处理则交给父视图控制器处理);一直到 window,如果window还是不能处理此事件则继续交给application处理,如果最后application还是不能处理此事件则将其丢弃

MVC和MVVM的区别?MVVM和MVP的区别?
另一个 MVP 与 MVC 之间的重大区别就是,MVP(Passive View)中的视图和模型是完全解耦的,它们对于对方的存在完全不知情,这也是区分 MVP 和 MVC 的一个比较容易的方法。

无论是 MVVM 还是 Presentation Model,其中最重要的不是如何同步视图和展示模型/视图模型之间的状态,是使用观察者模式、双向绑定还是其它的机制都不是整个模式中最重要的部分,最为关键的是展示模型/视图模型创建了一个视图的抽象,将视图中的状态和行为抽离出一个新的抽象,这才是 MVVM 和 PM 中需要注意的。

面向对象的几个设计原则了解么?最好可以结合场景来说。
对于设计模式的六大设计原则,单一职责原则主要说明类的职责要单一;里氏替换原则强调不要破坏继承体系;依赖倒置原则描述要面向接口编程;接口隔离原则讲解设计接口的时候要精简;迪米特法则告诉我们要降低耦合;开闭原则讲述的是对扩展开放,对修改关闭。

可以说几个重构的技巧么?你觉得重构适合什么时候来做?

了解编译的过程么?分为哪几个步骤?
预编译:主要处理以“#”开始的预编译指令。
编译:
词法分析:将字符序列分割成一系列的记号。
语法分析:根据产生的记号进行语法分析生成语法树。
语义分析:分析语法树的语义,进行类型的匹配、转换、标识等。
中间代码生成:源码级优化器将语法树转换成中间代码,然后进行源码级优化,比如把 1+2 优化为 3。中间代码使得编译器被分为前端和后端,不同的平台可以利用不同的编译器后端将中间代码转换为机器代码,实现跨平台。
目标代码生成:此后的过程属于编译器后端,代码生成器将中间代码转换成目标代码(汇编代码),其后目标代码优化器对目标代码进行优化,比如调整寻址方式、使用位移代替乘法、删除多余指令、调整指令顺序等。
汇编:汇编器将汇编代码转变成机器指令。
静态链接:链接器将各个已经编译成机器指令的目标文件链接起来,经过重定位过后输出一个可执行文件。
装载:装载可执行文件、装载其依赖的共享对象。
动态链接:动态链接器将可执行文件和共享对象中需要重定位的位置进行修正。
最后,进程的控制权转交给程序入口,程序终于运行起来了。

静态链接了解么?静态库和动态库的区别?
静态库:链接时完整地拷贝至可执行文件中,被多次使用就有多份冗余拷贝。

动态库:链接时不复制,程序运行时由系统动态加载到内存,供程序调用,系统只加载一次,多个程序共用,节省内存。

内存的几大区域,各自的职能分别是什么?

栈区:有系统自动分配并释放,一般存放函数的参数值,局部变量等
堆区:有程序员分配和释放,若程序员未释放,则在程序结束时有系统释放,在iOS里创建出来的对象会放在堆区
数据段:字符串常量,全局变量,静态变量
代码段:编译之后的代码

TCP为什么要三次握手,四次挥手?

HTTPS是如何实现验证身份和验证完整性的?

使用数字证书和CA来验证身份,首先服务端先向CA机构去申请证书,CA审核之后会给一个数字证书,里面包裹公钥、签名、有效期,用户信息等各种信息,在客户端发送请求时,服务端会把数字证书发给客户端,然后客户端会通过信任链来验证数字证书是否是有效的,来验证服务端的身份。

使用摘要算法来验证完整性,也就是说在发送消息时,会对消息的内容通过摘要算法生成一段摘要,在收到收到消息时也使用同样的算法生成摘要,来判断摘要是否一致。

tabView的优化

  • TableViewCell 复用 在cellForRowAtIndexPath:回调的时候只创建实例,快速返回cell,不绑定数据。在willDisplayCell: forRowAtIndexPath:的时候绑定数据(赋值)。
  • 高度缓存 UITableView-FDTemplateLayoutCell
  • 减少多余的绘制操作 尽可能将多张图片合成为一张进行显示。 优化图片大小,尽量不要动态缩放(contentMode)。
  • 减少离屏渲染 触发离屏渲染: layer.shouldRasterize,光栅化 layer.mask,遮罩 layer.allowsGroupOpacity为YES,layer.opacity的值小于1.0 layer.cornerRadius,并且设置layer.masksToBounds为YES。可以使用剪切过的图片,或者使用layer画来解决。
  • 离屏渲染的优化建议
    使用ShadowPath指定layer阴影效果路径。
    使用异步进行layer渲染(Facebook开源的异步绘制框架AsyncDisplayKit)。
    设置layer的opaque值为YES,减少复杂图层合成。
    尽量使用不包含透明(alpha)通道的图片资源。
    尽量设置layer的大小值为整形值。
    直接让美工把图片切成圆角进行显示,这是效率最高的一种方案。
    很多情况下用户上传图片进行显示,可以在客户端处理圆角。
    使用代码手动生成圆角image设置到要显示的View上,利用UIBezierPath(Core Graphics框架)画出来圆角图片。
  • 异步渲染
  • 按需加载
    利用runloop提高滑动流畅性,在滑动停止的时候再加载内容,像那种一闪而过的(快速滑动),就没有必要加载,可以使用默认的占位符填充内容。

你可能感兴趣的:(iOS 基础知识概述)