UIView与CALayer的区别?
UIView继承于UIResponder->NSObject
CALayer继承于NSObject
UIView能够响应事件,CALayer不可以
UIView主要是对显示内容的管理而 CALayer 主要侧重显示内容的绘制。
在做 iOS 动画的时候,修改非 RootLayer的属性(譬如位置、背景色等)会默认产生隐式动画,而修改UIView则不会。
UIView与CALayer的区别
UIbutton继承链,UIControl UIResponder区别
UIButton继承链:UIButton-UIControl-UIView-UIResponder-NSObject
UIControl发起action,UIResponder处理action。UIResponder可以通过重写touchesBegan:withEvent:等方法接收到UIEvent和UITouch的详细信息,然后处理。
为什么要在主线程刷新UI?
UIKit并不是一个 线程安全 的类,UI操作涉及到渲染访问各种View对象的属性,如果异步操作下会存在读写问题,而为其加锁则会耗费大量资源并拖慢运行速度。另一方面因为整个程序的起点UIApplication是在主线程进行初始化,所有的用户事件都是在主线程上进行传递(如点击、拖动),所以view只能在主线程上才能对事件进行响应。而在渲染方面由于图像的渲染需要以60帧的刷新率在屏幕上 同时 更新,在非主线程异步化的情况下无法确定这个处理过程能够实现同步更新。
UIImage *image = [UIImage imageNamed:/imageWithContentOfFile:]; // 这两个的区别
- imageNamed:
返回的对象会保存在缓存中,只有退出程序才会释放内存,但下一次调用很快,直接从缓存中读取即可。
2.imageWithContentOfFile:
返回的对象不会保存在缓存中,一旦对象销毁就会释放内存
所以只有在一些整个程序生命周期里经常用到的图像采用imageNamed来处理,其他的(尤其是图像文件非常大的)文件都应该用imageWithContentOfFile来处理。
try、try?与try!
try:手动捕捉异常
try?:系统帮我们处理,出现异常返回nil;没有异常返回对应的对象
try!:直接告诉系统,该方法没有异常。如果出现异常程序会crash
如果页面 A 跳转到 页面 B,A 的 viewDidDisappear 方法和 B 的 viewDidAppear 方法哪个先调用?
答:顺序 A-B-A-B
1、调用A的viewwilldisappear
2、调用B的viewwillappear
3、调用A的viewDidDisappear
4、调用B的viewDidAppear
2、A present C
答:顺序A-C-C-A
1、调用A的viewWillDisappear
2、调用C的viewWillAppear
3、调用C的viewDidAppear
4、调用A的viewDidDisappear
drawRect 与 layoutSubviews的调用时机
首先两个方法都是异步执行。layoutSubviews方便数据计算,drawRect方便视图重绘。
layoutSubviews在以下情况下会被调用:
1、init初始化不会触发layoutSubviews。
2、addSubview会触发layoutSubviews。
3、设置view的Frame会触发layoutSubviews,当然前提是frame的值设置前后发生了变化。
4、滚动一个UIScrollView会触发layoutSubviews。
5、旋转Screen会触发父UIView上的layoutSubviews事件。
6、改变一个UIView大小的时候也会触发父UIView上的layoutSubviews事件。
7、直接调用setLayoutSubviews。
drawRect在以下情况下会被调用:
1、如果在UIView初始化时没有设置rect大小,将直接导致drawRect不被自动调用。 drawRect 掉用是在Controller->loadView, Controller->viewDidLoad 两方法之后掉用的.所以不用担心在 控制器中,这些View的drawRect就开始画了.这样可以在控制器中设置一些值给View(如果这些View draw的时候需要用到某些变量 值).
2、该方法在调用sizeT o Fit后被调用,所以可以先调用sizeToFit计算出size。然后系统自动调用drawRect:方法。
3、通过设置contentMode属性值为UIViewContentModeRedraw。那么将在每次设置或更改frame的时候自动调用drawRect:。
4、直接调用setNeedsDisplay,或者setNeedsDisplayInRect:触发drawRect:,但是有个前提条件是rect不能为0。
以上1,2推荐;而3,4不提倡
什么是离屏渲染,以及为什么会产生离屏渲染?
正常渲染流程
APP中的数据经过CPU计算和GPU渲染后,将结果存放在帧缓冲区,利用视频控制器从帧缓冲区中取出,并显示到屏幕上。
在GPU的渲染流程中,显示到屏幕上的图像是遵循大画家算法按照由远及近的顺序,依次将结果存储到帧缓冲区
视屏控制器从帧缓冲区中读取一帧数据,将其显示到屏幕上后,会立即丢弃这帧数据,不会做任何保留,这样做的目的是可以节省空间,且在屏幕上是各自显示各自的,互相不影响。
离屏渲染流程
当App需要进行额外的渲染和合并时,例如按钮设置圆角,我们是需要对UIButton这个控件中的所有图层都进行圆角+裁剪,然后再将合并后的结果存入帧缓存区,再从帧缓存中取出交由屏幕显示,这时,在正常的渲染流程中,我们是无法做到对所有图层进行圆角裁剪的,因为它是用一个丢一个。所以我们需要提前将处理好的结果放入离屏缓冲区,最后将几个图层进行叠加合并,存放到站缓冲区,最后屏幕上就是我们想实现的效果。
离屏缓存区就是一个临时的缓冲区,用来存放在后续操作使用,但目前并不使用的数据。
离屏渲染再给我们带来方便的同时,也带来了严重的性能问题。由于离屏渲染中的离屏缓冲区,是额外开辟的一个存储空间,当它将数据转存到Frame Buffer时,也是需要耗费时间的,所以在转存的过程中,仍有掉帧的可能。
离屏缓冲区的空间并不是无限大的, 它是又上限的,最大只能是屏幕的2.5倍
为什么我们明知有性能问题时,还是要使用离屏渲染呢?
可以处理一些特殊的效果,这种效果并不能一次就完成,需要使用离屏缓冲区来保存中间状态,不得不使用离屏渲染,这种情况下的离屏渲染是系统自动触发的,例如经常使用的圆角、阴影、高斯模糊、光栅化等
可以提升渲染的效率,如果一个效果是多次实现的,可以提前渲染,保存到离屏缓冲区,以达到复用的目的。这种情况是需要开发者手动触发的。
圆角中离屏渲染的触发时机?
首先说明下CALayer的构成,如图所示,它是由backgroundColor、contents、borderWidth&borderColor构成的。跟我们即将解释的圆角触发离屏渲染息息相关。
官方文档告诉我们,设置cornerRadius只会对CALayer中的backgroundColor 和 boder设置圆角,不会设置contents的圆角,如果contents需要设置圆角,需要同时将maskToBounds / clipsToBounds设置为true。
所以我们可以理解为圆角不生效的根本原因是没有对contents设置圆角,而按钮设置的image是放在contents里面的,所以看到的界面上的就是image没有进行圆角裁剪。
在离屏渲染触发的场景中,按照性能影响从高到低排序,如下所示
shadows(阴影)
conerRadius > 0 + maskToBounds = true(常见的圆角设置手段)
mask(遮罩)
allowsGroupOpacity(组不透明)
edge antialiasing(抗锯齿)
针对不同场景说明为什么以及怎么解决离屏渲染问题
添加了阴影的layer(layer.shadow)
layer本身是一块矩形区域,而阴影是作用于在整个非透明区域,并显示在所有layer的最下方。
根据画家算法,由远及近的渲染,阴影是第一个被渲染的,但是阴影渲染有一个前提:我们必须画完所有的layer和子layer后。
所以这时我们就需要一个临时缓存,这个缓存区就是离屏缓冲区,用来将所有layer都渲染完成,再根据所有layer和子layer组合后的图层的形状,添加阴影到FrameBuffer,最后显示到屏幕上
优化方案
使用阴影必须保证 layer 的masksToBounds = false,因此阴影与系统圆角不兼容。但是注意,只是在视觉上看不到,对性能的影响依然。通常使用指定路径来避免离屏渲染
方案1:指定路径
在上述代码的基础上增加以下两行代码
let path = UIBezierPath(rect:btn.bounds)
btn.layer.shadowPath = path.cgPath
需要进行裁剪的layer(layer.masksToBounds / view.clipsToBounds)
这种场景就是我们常用的圆角处理,当我们需要绘制一个带有圆角并且需要剪切圆角以外内容的容器时,就会触发离屏渲染,例如UIButton、UIImageView等
注意:iOS官方针对UIImageView有一些优化,
==> 在iOS9之前,UIImageView和UIButton通过cornerRadius+masksToBounds设置圆角都会触发离屏渲染,
==> 但是UIImageView在ios9以后,针对UIImageView中的image设置圆角并不会触发离屏渲染,如果加上了背景色或者阴影等其他效果还是会触发离屏渲染的
优化方案
对于content无内容或者内容非背景透明(不涉及到圆角以外的区域)的layer,直接设置layer的backgroundColor + cornerRadius 属性绘制圆角
后台绘制,前台设置图片
- (void)setCircleImage {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
UIImage *circleImage = [image imageWithCircle];
dispatch_async(dispatch_get_main_queue(), ^{
imageView.image = circleImage;
});
});
}
#import "UIImage+Addtions.h"
@implementation UIImage (Addtions)
//返回一张圆形图片
- (instancetype)imageWithCircle {
UIGraphicsBeginImageContextWithOptions(self.size, NO, 0);
UIBezierPath *path = [UIBezierPath bezierPathWithOvalInRect:CGRectMake(0,0,self.width,self.height)];
[path addClip];
[self drawAtPoint:CGPointZero];
UIImage *image = UIGraphcisGetImageFromCurrentImageContext();
UIGraphcisEndImageContext();
return image;
}
- 使用混合图层,在layer上方叠加相应mask形状的半透明layer
sublayer.contents = (id)[UIImage imageNamed:@"xxxx"].CGImage;
[view.layer addSublayer:sublayer];
绘制了文字的 layer (UILabel, CATextLayer, Core Text 等)
优化方案
直接通过label的layer设置背景色 + cornerRadius
label.layer.backgroundColor = aColor
label.layer.cornerRadius = 5
总结
RoundedCorner(圆角) 在仅指定cornerRadius时不会触发离屏渲染,仅适用于特殊情况:contents为 nil 或者contents不会遮挡背景色时的圆角。
Shawdow 可以通过指定路径来取消离屏渲染。
Mask 无法取消离屏渲染,可以利用混合图层来进行优化。
原文链接:https://blog.csdn.net/lin1109221208/article/details/107341640
原文链接:https://blog.csdn.net/lin1109221208/article/details/107185268
有关HTTP问题
HTTP:超文本传输协议,用户客户端与服务端的传输规则,可传输任意类型的数据
HTTP请求包含的要素:请求行,请求头,请求体,响应行,响应头,实体内容
请求行:POST /lyric/3g_lyric HTTP/1.1
请求行包含:请求方法,请求统一资源标识符URI,HTTP版本
请求方法:GET,POST,HEAD,PUT等
HTTP与服务器交互基本的方法有4种,GET,POST,PUT,DELETE
GET: 请求获取Request-URI所标识的资源
POST: 在Request-URI所标识的资源后附加新的数据
HEAD: 请求获取由Request-URI所标识的资源的响应消息报头
PUT: 请求服务器存储一个资源,并用Request-URI作为其标识
DELETE: 请求服务器删除Request-URI所标识的资源
TRACE: 请求服务器回送收到的请求信息,主要用于测试或诊断
CONNECT: 保留将来使用
OPTIONS: 请求查询服务器的性能,或者查询与资源相关的选项和需求
请求头:存放客户端给服务的的附加信息
Host:目标服务器的网络地址
Accept:让服务端知道客户端所能接受的数据类型 text/html
Content-Type:body中的数据类型,如application/json,charset=UTF-8
Accept-Encoding:客户端支持的数据压缩格式,如gzip
User-Agent:客户端的软件环境
Connection:keep-alive,HTTP1.1才开始有的,告诉服务端这是一个持久连接
Content-Length: body的长度,如果body为空则该字段值为0。一般POST请求中才会有
Cookie:记录着用户信息的保存在本地的数据
//添加请求头
[NSMutableURLRequest addValue: forHTTPHeaderField”];
//获取当前请求设置的field
[NSURLRequest allHTTPHeaderFields];
响应状态行:服务端返回给客户端的状态信息,包含HTTP版本号,状态码,状态码对应的英文名称
错误码分为以下几类:
1XX:信息提示。不代表成功或者失败,表示临时响应,100表示继续,101 表示切换协议
2XX:成功
3XX:重定向 304(NOT MODIFIED):该资源在上次请求之后无修改
4XX:客户端错误,404文件为找到,说明URI有问题,414URI太长, 403:无权限
5XX:服务器错误,504网关超时
http服务器常用状态码及其含义
HTTPS与HTTP的区别
一、https协议需要到ca申请证书,一般免费证书很少,需要交费。
二、http是超文本传输协议,信息是明文传输,https 则是具有安全性的ssl加密传输协议。
三、http和https使用的是完全不同的连接方式,用的端口也不一样,前者是80,后者是443。
四、http的连接很简单,是无状态的;HTTPS协议是由SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议,比http协议安全。
HTTP请求全过程
Swift中Class和Struct区别?
类是引用类型,结构体是值类型。值类型在传递和赋值时将进行复制,而引用类型则只会使用引用对象的一个"指向"。所以他们两者之间的区别就是两个类型的区别。
classTemperature {
varvalue: Float = 37.0
}
classPerson {
vartemp: Temperature?
func sick() {
temp?.value = 41.0
}
}
let A = Person()
let B = Person()
let temp = Temperature()
A.temp = temp
B.temp = temp
由于 Temperature 是 class ,为引用类型,故 A 的 temp 和 B 的 temp指向同一个对象。A 的 temp修改了,B 的 temp 也随之修改。这样 A 和 B 的 temp 的值都被改成了41.0。如果将 Temperature 改为 struct,为值类型,则 A 的 temp 修改不影响 B 的 temp。
内存中,引用类型诸如类是在堆(heap)上,而值类型诸如结构体实在栈(stack)上进行存储和操作。相比于栈上的操作,堆上的操作更加复杂耗时,所以苹果官方推荐使用结构体,这样可以提高 App 运行的效率。
class有这几个功能struct没有的:
class可以继承,这样子类可以使用父类的特性和方法
类型转换可以在runtime的时候检查和解释一个实例的类型
可以用deinit来释放资源
一个类可以被多次引用
struct也有这样几个优势:
结构较小,适用于复制操作,相比于一个class的实例被多次引用更加安全。
无须担心内存memory leak或者多线程冲突问题
在封装SDK中:
1、swift调用oc
iOS 制作framework时,swift调用OC,不支持桥接,故得换一种方式调用。
步骤:
点击target ->Build Settings -> Allow Non-modular Includes In Framework Modules 设置为YES
然后在 Build Phases 中 Headers 的把你想要调用的oc文件暴露到Public中去
然后在对外的统一接口文件中暴露这个头文件
2、oc调用swift
同样的方式,在SDK中会提示找不到Product Module Name -Swift.h 这个头文件
步骤:
这里需要将Product Module Name -Swift.h这个头文件的引用改为引用#import 这个头文件
【JWEmotionTrackeriOSSDK就是你封装的SDK名称】
总结:oc通过xxx-swift.h调用swift【系统自动生成】。swift通过xxx-Bridging-Header.h调用oc【系统提示生成或手动创建】创建oc项目,xcode不会自动创建桥接文件。需要先创建xxx-Bridging-Header.h,才会生成xxx-swift.h文件。这句话很重要,如果不创建桥接文件,那么会提示 添加好 头文件之后 调用的时候,Bad receiver type XXX,这时候,需要手动创建桥接文件。
触摸事件是如何从屏幕转移到APP内的?
1.手指触摸屏幕,屏幕感到触摸产生事件交给IOKit
2.IOKit会把事件包装成IOHIdEvent,经由mach_port交给SpringBoad
mach_port 进程端口,各进程通信都是靠它
SpringBoad 是系统进程,也可以说是系统界面进程,处理和接受触摸事件
3.SpingBoad进程接收到触摸事件,触发了主线程runloop的Source1事件回调
/*
SpingBoad进程接受到触摸事件的时候,会分析触摸事件发生的时候 用户是在屏幕上界面翻页还是在某app内,如果是前者会唤起SpingBoad本身主线程的runloop的Source0事件回调,将事件交由桌面系统去消耗,如果是后者,触摸事件通过IPC传递给前台APP进程,接下来的事情便是APP内部对于触摸事件的响应了
*/
简单点说 Source1就是系统干的事 Source0就是我们干的事
正经说 Source1基本就是系统事件,Source0基本就是应用层事件
Source1 :基于mach_Port的,来自系统内核或者其他进程的事件,可以主动唤醒休眠中的RunLoop(开发过程中我们一般不主动使用IPC《进程间通信》)。mach_port大家就理解成进程间相互发送消息的一种机制就好。
Source0 :非基于Port的 处理事件,什么叫非基于Port的呢?就是说你这个消息不是其他进程或者内核直接发送给你的。
链接:https://www.jianshu.com/p/d547e5393373
响应链和事件传递
事件的分发和传递。
1.当iOS程序中发生触摸事件后,系统会将事件加入到UIApplication管理的一个任务队列中
2.UIApplication将处于任务队列最前端的事件向下分发。即UIWindow。
3.UIWindow将事件向下分发,即UIView。
4.UIView首先看自己是否能处理事件,触摸点是否在自己身上。如果能,那么继续寻找子视图。
5.遍历子控件,重复以上两步。
6.如果没有找到,那么自己就是事件处理者。
7.如果自己不能处理,那么不做任何处理。
其中 UIView不接受事件处理的情况主要有以下三种
1)alpha <0.01
2)userInteractionEnabled = NO
3.hidden = YES.
怎么寻找最合适的view
// 此方法返回的View是本次点击事件需要的最佳View
- (UIView)hitTest:(CGPoint)point withEvent:(UIEvent)event
// 判断一个点是否落在范围内
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent*)event
// 因为所有的视图类都是继承BaseView
- (UIView)hitTest:(CGPoint)point withEvent:(UIEvent)event {
// 1.判断当前控件能否接收事件
if(self.userInteractionEnabled == NO || self.hidden == YES || self.alpha <= 0.01) return nil;
// 2. 判断点在不在当前控件
if([self pointInside:point withEvent:event] == NO) return nil;
// 3.从后往前遍历自己的子控件
NSInteger count = self.subviews.count;
for(NSInteger i = count - 1; i >= 0; i--) {
UIViewchildView = self.subviews[I];
// 把当前控件上的坐标系转换成子控件上的坐标系
CGPointchildP = [self convertPoint:point toView:childView];
UIViewfitView = [childView hitTest:childP withEvent:event];
if(fitView) {
// 寻找到最合适的view
return fitView;
}
}
// 循环结束,表示没有比自己更合适的view
return self;
}
响应者链
响应链是从最合适的view开始传递,处理事件传递给下一个响应者,响应者链的传递方法是事件传递的反方法,如果所有响应者都不处理事件,则事件被丢弃。我们通常用响应者链来获取上几级响应者,方法是UIResponder的nextResponder方法。
__weak,__block区别,及实现原理,为何__block修饰的值可以改变,__weak修饰的值不能改变?
__weak用于修饰变量,后者用户修饰属性,__weak主要用于防止block中的循环引用。
__block也用于修饰变量。它是引用修饰,其修饰的值是动态变化的,可以被重新赋值。
__block用于修饰block内部将要修改的外部变量。
使用__weak和__unsafe_unretained修饰符合一解决循环引用的问题,__weak会使block内部将指针变为弱指针。
__weak 和 __unsafe_unretained的区别。
__weak不会产生强引用,指向的对象销毁时,会自动将指针置为nil
__unsafe_unretained不会产生强引用,不安全,指向的对象销毁时,指针存储的地址值不变
__strong 和 __weak
在block内部重新使用__strong修饰self变量是为了在block内部有一个强指针指向weakSelf避免在block调用的时候weakSelf已经被销毁。
__block修饰的变量为什么能在block里面能改变其值?
__block用于解决block内部不能修改auto变量值的问题,__block不能修饰静态变量和全局变量
_block 所起到的作用就是只要观察到该变量被 block 所持有,就将“外部变量”在栈中的内存地址放到了堆中。进而在block内部也可以修改外部变量的值。
runtime如何实现weak变量的自动置nil?
runtime对注册的类, 会进行布局,对于weak对象会放入一个hash表中。 用weak指向的对象内存地址作为key,当此对象的引用计数为0的时候会dealloc.假如weak指向的对象内存地址是A,那么就会以A为键, 在这个weak表中搜索,找到所有以A为键的weak对象,从而设置为nil.
#import跟 #include 有什么区别,@class呢,#import<> 跟 #import””有什么区别?
1). #import是Objective-C导入头文件的关键字,#include是C/C++导入头文件的关键字,使用#import头文件会自动只导入一次,不会重复导入。
2). @class告诉编译器某个类的声明,当执行时,才去查看类的实现文件,可以解决头文件的相互包含。
3). #import<>用来包含系统的头文件,#import””用来包含用户头文件。
load方法和initalize方法有什么区别和共同点
initialize方法是在第一次调用该类的方法的时候调用,load方法是在程序启动之前就调用,父类先于子类执行,类先于分类执行。
总结:
1.load和initialize都会在实例化对象之前调用,以main函数为分水岭,前者是在main函数之前,后者是在main函数之后。
2.load和initialize方法都不会显示的调用父类的方法而是自动调用,即使子类没有initialize方法也会调用父类的方法,load方法不会调用父类。
3.load和initialize方法内部使用了锁,因此他们是线程安全的,实现时要尽可能简单,避免线程阻塞,不要再次使用锁。
4.load方法常用来method swizzle,initialize常常用于初始化全局变量和静态变量.
调用方式
1、load是根据函数地址直接调用
2、initialize是通过objc_msgSend调用
调用时刻
1、load是runtime加载类、分类的时候调用(只会调用一次)
2、initialize是类第一次接收到消息的时候调用, 每一个类只会initialize一次(如果子类没有实现initialize方法, 会调用父类的initialize方法, 所以父类的initialize方法可能会调用多次)
load和initializee的调用顺序
1、load:父子分
先调用类的load, 在调用分类的load
先编译的类, 优先调用load, 调用子类的load之前, 会先调用父类的load
先编译的分类, 优先调用load
2、initialize 分子父
先初始化分类, 后初始化子类
通过消息机制调用, 当子类没有initialize方法时, 会调用父类的initialize方法, 所以父类的initialize方法会调用多次
对A类创建了分类B和C,都有同一个方法eat,B和C的方法实现不同,在A里面调用eat,实现是谁的
答:和分类的编译顺序有关,最后一个被编辑的执行eat方法
多线程
不手动指定autoreleasepool的前提下,一个autorealese对象在什么时刻释放?
分两种情况:手动干预释放时机、系统自动去释放。
手动干预释放时机 - 指定autoreleasepool就是所谓的:当前作用域大括号结束时释放。
系统自动去释放 - 不手动指定autoreleasepool
Autorelease对象出了作用域之后,会被添加到最近一次创建的自动释放池中,并会在当前的 runloop 迭代结束时释放。
如果在一个vc的viewDidLoad中创建一个 Autorelease对象,那么该对象会在 viewDidAppear 方法执行前就被销毁了。
黑幕背后的autorelease
ios 用过哪些锁?哪些锁的性能比较高?
RunLoop和线程的关系?runloop的mode作用是什么?
一、RunLoop和线程的关系:
1.RunLoop 的作用就是来管理线程的,当线程的 RunLoop开启后,线程就会在执行完任务后,处于休眠状态,随时等待接受新的任务,而不是退出。
2.只有主线程的RunLoop是默认开启的,所以程序在开启后,会一直运行,不会退出。其他线程的RunLoop如果需要开启,就手动开启,
二、runloop内部是如何实现的:
1、有一个判断循环的条件,满足条件,就一直循环
2、线程得到唤醒事件被唤醒,事件处理完毕以后,回到睡眠状态,等待下次唤醒
二、runloop的mode作用是什么?:
1.model 主要是用来指定事件在运行循环中的优先级的,分为:
- NSDefaultRunLoopMode(kCFRunLoopDefaultMode):默认,空闲状态
- UITrackingRunLoopMode :ScrollView滑动时
- UIInitializationRunLoopMode :启动时
- NSRunLoopCommonModes(kCFRunLoopCommonModes):Mode集合
2.苹果公开提供的 Mode有两个:
- NSDefaultRunLoopMode(kCFRunLoopDefaultMode)
- NSRunLoopCommonModes(kCFRunLoopCommonModes)
原文https://blog.csdn.net/shihuboke/article/details/78417299
RunLoop分析学习
KVC getter 和 setter 的搜索策略是什么?
setter实现原理
1、查找是否实现setter、_setter方法,如果有,优先调用setter方法完成赋值
2、当没找到setter方法,调用accessInstanceVariableesDirectly询问。
如果返回YES,顺序匹配变量名与 _<key>, _is<Key>, <key>, is<Key>,匹配到则设定其值
如果返回NO,结束查找。并调用 setValue: forUnderfinedKey: 报异常
3、如果既没有setter也没有实例变量时,调用 setValue: forUnderfinedKey:
getter实现原理
1、查找是否实现getter方法,依次匹配‘-get<Key>’ 、 ‘-<key>’ 、 ’is<Key>‘,如果找到,直接返回
2、当没有找到getter方法,调用accessInstanceVariablesDirectly询问
如果返回YES,_<key>, _is<Key>, <key>, is<Key>,找到了返回对应的值
如果返回NO,结束查找,并调用 valueForUnderfinedKey: 报异常
3、如果没有找到getter方法和属性值,调用valueForUnderfinedKey: 报异常
KVC 有什么实际的应用
属性赋值、添加私有成员变量、KVC可以修改类的私有变量、KVC可以用来给model赋值
如UITextField的placeHolderText默认style在需求中达不到要求,我们可以直接通过KVC快速定义自己的style,代码如下:
[textField setValue:[UIColor redColor] forKeyPath:@"placeholderLabel.textColor"];
引用计数原理
SideTable中包含三个成员:自旋锁(slock),引用计数表(RefcountMap),弱引用表(weak_table_t)。引用计数表是个哈希表,用来存储对象的引用计数。弱引用表也是哈希表,用来存放对象的弱引用指针。
SideTables是一个全局的哈希数组,里面存储了多张SideTable,在iOS的真机模式下,最多8张,在MacOS或者模拟器模式下,最多64张。每一个对象对应一个SideTable,每一个SideTable存储多个对象的引用计数和弱引用指针。
nonpointer类型的对象,引用计数存储在isa的extra_rc和SideTable的RefcountMap中。当被retain或者realease时,先操作extra_rc,溢出或者为0时,才操作SideTable。
非nonpointer类型的对象,引用计数只存储在SideTable的RefcountMap中。
TaggedPointer对象,内存由系统管理,不用处理引用计数。
当对象被弱引用时,这个弱引用指针会存储在SideTable的weak_table_t中。
当对象被释放时,会先执行C++析构函数,删除关联对象,清空引用计数,将弱引用指针设为nil后清空,最后free释放内存空间。
retainCount获取的引用计数值要比对象的真实值多1,最小为1。
alloc的对象引用计数为0。
链接:https://www.jianshu.com/p/0ecd91dc34aa
自动释放池底层实现
内存管理
Swizzle 的优缺点? 缺点会导致什么问题?
每一个OC实例对象都保存有isa指针和实例变量,其中isa指针所属类,类维护一个运行时可接收的方法列表(MethodLists); 方法列表(MethodLists)中保存selector & IMP的映射关系。在运行时,通过selecter找到匹配的IMP,从而找到的具体的实现函数。
开发中可以利用Objective-C的动态特性,在运行时替换selector对应的方法实现(IMP),达到给hook的目的。下图是利用 Method Swizzle 来替换selector对应IMP后的方法列表示意图。
@implementation NSObject (Swizzle)
+(void)load {
//调换IMP
Method originMethod = class_getInstanceMethod([NSObject class], @seletor(description));
Method newMethod = class_getInstanceMethod([NSObject class], @seletor(replace_description));
method_exchangeImplementations(originMethod,newMethod);
}
- (void)replace_description {
NSLog(@"description 被 Swizzle 了");
[self replace_description];
}
使用swizzle时,我们应该注意哪些问题呢?
问题一:继承问题
如果 originalMethod 是其父类实现的,那么直接 method_exchangeImplementations 是把父类中的 originalMethod 给替换了,导致该父类以及其他子类调用的 originalMethod 也会被替换
解决: 通过 class_addMethod 判断 method 是不是属于本类自己实现的?
class_addMethod 返回 YES -> addMethod 成功,class中不存在 method,也就是存在父类中。addMethod之后,当前class也就存在method 了(覆盖了父类的方法)
class_addMethod 返回 NO -> addMethod 失败,class中存在 method,说明当前方法属于当前class
判断之后,再执行 exchange
如何做到对象级别的 swizzle?
只对某个对象进行 swizzle,不影响其他对象
方案:
1.类本身支持。可以标记一下,在执行方法时,判断是否存在标记来判断是否执行swizzle 之后的方法。可以参考:第三方库 DZNEmptyDataSet(统一空白页)
2.动态生成一个当前对象所属类的子类,并将当前对象与子类关联。这样的话,swizzle的都是其子类的方法,不会影响父类。可以参考:第三方库 Aspects
_objc_msgForward
_objc_msgForward 是一个函数指针(和 IMP 的类型一样),是用于消息转发的:当向一个对象发送一条消息,但它并没有实现的时候,_objc_msgForward 会直接走消息转发。
OC 消息转发机制
众所周知OC的一个对象在发送消息的时候首先在cache里找,如果找不到就在该类的struct objc_method_list列表中去搜索,如果找到则直接调用相关方法的实现,如果没有找到就会通过super_class指针沿着继承树向上去搜索,如果找到就跳转,如果到了继承树的根部(通常为NSObject)还没有找到。那就会调用NSObjec的一个方法doesNotRecognizeSelector:,这样就会报unrecognized selector 错误。其实在调用doesNotRecognizeSelector:方法之前还会进行消息转发---还有三次机会来补救。也就是常说的OC消息转发的三次补救措施。
总的来说一个OC消息的发送会经历四个阶段(该四个阶段都是搜索到NSObject再进入下阶段)
1)先在本类中搜索改方法的实现,如果有则直接调用若果没有则去父类中搜索直到NSObject,如果NSObject没有则进入消息转发(类的动态方法解析、备用接受者对象、完整的消息转发)。
2)类的动态方法解析:
在该类和父类中没有找到该方法的实现则会执行 +(BOOL)resolveClassMethod:(SEL)sel 或+(BOOL)resolveInstanceMethod:(SEL)sel 方法。在+(BOOL)resolveClassMethod:(SEL)sel 或+(BOOL)resolveInstanceMethod:(SEL)sel 方法 中利用黑魔法runtime 动态添加方法实现
3)备用接受者: 在+(BOOL)resolveClassMethod:(SEL)sel 或+(BOOL)resolveInstanceMethod:(SEL)sel 方法返回NO的时候进入备用接受者阶段 。
创建一个备用接受者类ForwardPerson 实现appendString:方法
在SonPerson类中实现- (id)forwardingTargetForSelector:(SEL)aSelector 方法 并返回一个备用接受者对象
4)完整的消息转发:当-(void)forwardInvocation:(NSInvocation*)anInvocation 方法方法nil的时候则进入消息转发的最后阶段,完整的消息转发。也需要创建一个转发对象ForwardInvocation
在SonPerson中实现
-(void)forwardInvocation:(NSInvocation*)anInvocation和
- (NSMethodSignature*)methodSignatureForSelector:(SEL)aSelector方法
-(void)forwardInvocation:(NSInvocation*)anInvocation{
NSLog(@"forwardInvocation");
if ([ForwardInvocation instancesRespondToSelector:anInvocation.selector]) {
[anInvocation invokeWithTarget:self.invocation];
}
}
/必须重新这个方法,消息转发机制使用从这个方法中获取的信息来创建NSInvocation对象 返回nil上面方法不执行/
链接:https://www.jianshu.com/p/1073daee5b92
@implementation Son : Father
- (id)init {
self = [superinit];
if(self) {
NSLog(@"%@", NSStringFromClass([self class]));
NSLog(@"%@", NSStringFromClass([super class]));
}
return self;
}
答案:都输出 Son
self 是类的隐藏参数,指向当前调用方法的这个类的实例。
super 是一个编译器标示符,和 self 是指向的同一个消息接受者
当使用 self 调用方法时,会从当前类的方法列表中开始找,如果没有,就从父类中再找;而当使用 super 时,则从父类的方法列表中开始找。然后调用父类的这个方法。
当调用 [self class] 时,实际先调用的是 objc_msgSend函数,第一个参数是 Son当前的这个实例,然后在 Son 这个类里面去找 - (Class)class这个方法,没有,去父类 Father里找,也没有,最后在 NSObject类中发现这个方法。而 - (Class)class的实现就是返回self的类别,故上述输出结果为 Son。
而当调用 [super class]时,会转换成objc_msgSendSuper函数。第一步先构造 objc_super 结构体,结构体第一个成员就是 self 。 第二个成员是 (id)class_getSuperclass(objc_getClass(“Son”)) , 实际该函数输出结果为 Father。 第二步是去 Father这个类里去找 - (Class)class,没有,然后去NSObject类去找,找到了。最后内部是使用 objc_msgSend(objc_super->receiver, @selector(class))去调用, 此时已经和[self class]调用相同了,故上述输出结果仍然返回 Son。
IMP、SEL Method 都表示什么意思?
Method
/// An opaque type that represents a method in a class definition.代表类定义中一个方法的不透明类型
typedef struct objc_method *Method;
struct objc_method {
SEL method_name OBJC2_UNAVAILABLE;
char *method_types OBJC2_UNAVAILABLE;
IMP method_imp OBJC2_UNAVAILABLE;
}
我们来看下objc_method这个结构体的内容:
SEL method_name 方法名
char *method_types 方法类型
IMP method_imp 方法实现
在这个结构体重,我们已经看到了SEL和IMP,说明SEL和IMP其实都是Method的属性。
SEL
Objc.h
/// An opaque type that represents a method selector.代表一个方法的不透明类型
typedef struct objc_selector *SEL;
selector是SEL的一个实例
selector既然是一个string,我觉得应该是类似className+method的组合,命名规则有两条:
同一个类,selector不能重复
不同的类,selector可以重复
IMP
/// A pointer to the function of a method implementation. 指向一个方法实现的指针
typedef id (*IMP)(id, SEL, ...);
endif
指向最终实现程序的内存地址的指针
综上,在iOS的runtime中,Method通过selector和IMP两个属性,实现了快速查询方法及实现,相对提高了性能,又保持了灵活性。
关于重载和重写的区别
oc是不支持的重载的
oc和swift都是支持重写的。关于重写,只能发生在父类和子类之间
OC是否支持重载? 为什么?
理解 OC 中的方法调用
[self foo]
通过 clang 转为 c++ 代码为
((void (*)(id, SEL))(void *)objc_msgSend)((id)self, sel_registerName("foo"));
foo 方法的调用,实际是调用了 objc_msgSend 这个函数,self,sel 作为函数的参数。
再来看下上述两个方法。两者的 sel 都是foo:。对于一个类,其内部有一个方法的列表。msgSend 函数的实现为根据 sel 从方法列表中获取 imp。因此上述两个方法的 SEL 存在重复,msgSend 函数不能区分,找到对应的 imp。因此,在 OC 语言中不存在上述的方法重载。
isa 指针是什么?
struct objc_object {
Class isa OBJC_ISA_AVAILABILITY;
};
通过注释和代码不难发现,我们创建的一个对象或实例其实就是一个struct objc_object结构体,而我们常用的id也就是这个结构体的指针。
这个结构体只有一个成员变量,这是一个Class类型的变量isa,也是一个结构体指针
面向对象中每一个对象都必须依赖一个类来创建,因此对象的isa指针就指向对象所属的类根据这个类模板能够创建出实例变量、实例方法等。
isa指针里面都存了什么?
struct objc_class {
Class isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class super_class OBJC2_UNAVAILABLE;
const char *name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list *ivars OBJC2_UNAVAILABLE;
struct objc_method_list **methodLists OBJC2_UNAVAILABLE;
struct objc_cache *cache OBJC2_UNAVAILABLE;
struct objc_protocol_list *protocols OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;
/* Use `Class` instead of `struct objc_class *` */
struct objc_classs结构体里存放的数据称为元数据(metadata),通过成员变量的名称我们可以猜测里面存放有指向父类的指针、类的名字、版本、实例大小、实例变量列表、方法列表、缓存、遵守的协议列表等,
Person *p = [[Person alloc] init];
//输出1
NSLog(@"%d", [p class] == object_getClass℗);
//输出0
NSLog(@"%d", class_isMetaClass(object_getClass℗));
//输出1
NSLog(@"%d", class_isMetaClass(object_getClass([Person class])));
//输出0
NSLog(@"%d", object_getClass(p) == object_getClass([Person class]));
通过代码可以看出,一个实例对象通过class方法获取的Class就是它的isa指针指向的类对象,而类对象不是元类,类对象的isa指针指向的对象是元类。
一个NSObject对象占用多少内存?
答:一个指针变量所占用的大小(64bit占8个字节,32bit占4个字节)
对象的isa指针指向哪里?
答:instance对象的isa指针指向class对象,class对象的isa指针指向meta-class对象,meta-class对象的isa指针指向基类的meta-class对象,基类自己的isa指针也指向自己。
OC的类信息存放在哪里?
答:成员变量的具体值存放在instance对象。对象方法,协议,属性,成员变量信息存放在class对象。类方法信息存放在meta-class对象。
链接:https://www.jianshu.com/p/aa7ccadeca88
(APP)进程间8中常用通信方式总结
https://blog.csdn.net/kuangdacaikuang/article/details/78891379
- 1 URL Scheme
- 2 Keychain
- 3 UIPasteboard
- 4 UIDocumentInteractionController
- 5 local socket
- 6 AirDrop
- 7 UIActivityViewController
- 8 App Groups
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[self performSelector:@selector(sureTestMethod:)
withObject:params
afterDelay:3];
});
- (void)sureTestMethod:(id)objcet {
NSLog(@"sureTestMethodCall");
}
performSelector: withObject: afterDelay:是在当前Runloop中延时执行的,而子线程的Runloop默认不开启,因此无法响应方法。
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[self performSelector:@selector(sureTestMethod:)
withObject:params
afterDelay:3];
[[NSRunLoop currentRunLoop]run];
});
这里有个坑需要注意,曾经尝试将 [[NSRunLoop currentRunLoop]run]添加在performSelector: withObject: afterDelay:方法前,但发现延迟方法仍然不调用,这是因为若想开启某线程的Runloop,必须具有timer、source、observer任一事件才能触发开启。
简言之如下代码在执行 [[NSRunLoop currentRunLoop]run]前没有任何事件添加到当前Runloop,因此该线程的Runloop是不会开启的,从而延迟事件不执行。