辛辛苦苦从各个博客上总结出来的~哈哈哈,拿去!
属性修饰词
1.retain:引用计数+1(setter方法对参数进行release旧值,再retain新值)
2.release:引用计数-1(nil只是把一个对象的指针置为空,只是切断了指针与内存中的对象联系,并没有释放,而真正的释放是release)
3.autorelease:MRC可以延迟对象的内存释放
4.autoreleasepool:首先对象入池,池中装有很多autorelease对象,一旦池子被销毁来,池子就会出栈,对象释放内存。main函数为什么有个自动释放池,是因为每个程序中至少存在一个,否则autorelease对象不能对应收到release消息,导致内存泄漏。每个线程都有一个自动释放池,线程结束后会自动销毁掉相关的池子(入栈、出栈)
5.strong:指向并持有该对象,引用计数+1,引用计数为0销毁,通过nil来销毁
6.weak:指向但是不持有,引用计数不变,Runtime中对该属性进行了相关操作,无需处理,自动销毁(Runtime维护了一个weak指针变量的Hash表,key是weak指向的内存地址,value是所有指针,实际上是一个数组指针,释放时根据对象地址获取所有weak指针地址数组,然后遍历数组,把其中的数据置为nil,最后把这个对象从weak表中删除)。Xib本身拖拽的控件是强引用,所以到了拖拽代码是弱引用,用weak。
7.assign:主要修饰基本数据类型,如NSInteger,CGFloat,存储在栈中,内存无需程序员管理,setter方法直接赋值,而不进行retain操作
8.copy:类似Strong,copy多用于修饰有可变类型的不可变对象上,如NSString,NSArray,NSDictionary
9.__unsafe_unretain:类似weak,但是当对象被释放后,指针依然保存着之前的地址,被释放后的地址变为僵尸对象,这也就造成了野指针,容易造成Crash,访问被释放的地址就会出问题,所以说是不安全的,而weak在指向的内存销毁后可以将指针置为nil,更加安全。 setter方法进行Copy操作,与retain一样。还有一点,它不会自动变为nil的。
10.atomic:为了保证多线程的情况下,编译器会自动生成一些互斥加锁代码,避免该变量的读写不同步的问题(atomic可以保证setter和getter存取的线程安全并不保证整个对象是线程安全的。比如声明一个NSMutableArray的原子属性数组,此时self.array和self.array=otherArray都是线程安全的,但是使用[self.array objectAtIndex:index]就不是线程安全的,需要用锁来保证线程安全),默认atomic
11.nonatomic:如果该对象无需考虑多线程的情况,这个属性会让编译器少生成一些互斥代码,可以提高效率
12.readonly:属性是只读的,默认是读写,只生成getter方法
13.readwrite:属性是读写的,生成setter方法和getter方法
常见关键词
1.@property:编辑器会自动生成一个以下划线开头的实例变量,并且会自动生成setter、getter方法。(设置setter和getter方法名:getter=getterName, setter=setterName)
2.@synthesize:如果属性没有手动实现setter和getter方法,则编辑器会自动生成setter和getter方法
3.@dynamic:属性的setter和getter方法由用户自己实现,不自动生成。
setter方法和getter方法
备注1:getter方法又称之为懒加载、延迟加载,在需要的时候才加载,效率低,占用内存小,区别在于懒加载需要进行判断一下是否存在没有再去实例化,优点是代码可读性高,彼此之间独立性高,松耦合
备注2:@property属性的setter方法和getter方法是不能同时进行重写的,一旦全部重写,系统就不会帮你生成这个成员变量了,就会报错(不信你试试)。怎么解决这个问题呢,这就需要手动生成成员变量,然后就可以重写了或者用下面的方法(@synthesize age = myAge;即可)
原始写法:
- (void)setAge:(int)age; // setter方法声明
- (int)age; // setter方法声明
- (void)setAge:(int)age // setter方法实现
{
_age = age;
}
- (int)age // getter方法实现
{
return _age;
}
新写法:
@property int age; // 这句代码等价于声明了一个实例属性和它的setter方法和getter方法
@synthesize age; // 这句代码等价于实现setter和getter
@synthesize age = _age; // 上面哪种方式的getter方法名就是变量名,容易混淆,所以这么写就可以使用_age和使用age是一样的。
@synthesize age = myAge;
- (void)setAge:(NSString *)age
{
myAge = age;
}
- (NSString *)age
{
return myAge;
}
点语法、下划线和箭头(self. 、 _和->)
self.:等价于[self setName:xxx],会调用setter方法(retaincount+1)
_:仅仅是对一个指针的赋值,不会调用setter方法,这就是一个简单的指针复制(retaincount不变)
->:用于指向结构体子数据的指针,用来取子数据
堆和栈
1.堆:动态分配内存,需要程序员自己申请,程序员自己管理
2.栈:自动分配内存,自动销毁,先入后出,栈上的内容存在自动销毁的情况
UIWebView和WKWebView
iOS8以后,苹果推出了新框架Wekkit,提供了替换UIWebView的组件WKWebView。使用WKWebView,速度会更快,占用内存少,在性能、稳定性、功能方面有很大提升,直观体现是内存占用变少,支持了更多的HTML5特性,高达60fps的滚动刷新率以及内置手势。WKWebview提供了API实现js交互 不需要借助JavaScriptCore或者webJavaScriptBridge。使用WKUserContentController实现js native交互。简单的说就是先注册约定好的方法,然后再调用。强大的三方:WebViewJavascriptBridge
UIWebView
框架:UIWebviewDelegate 、JavaScriptCore、WebViewJavascriptBridge,这三个框架都可以对JS和OC交互拦截
代理:
1. UIWebViewDelegate:是否允许加载网页、开始加载网页、网页加载完成、网页加载错误
JS调用OC:
1. shouldStartLoadWithRequest该代理方法中判断JS与OC交互、webViewDidFinishLoad、三方代理方法registerHandler: handler:,
OC调用JS:查看访问html的body标签内内容的方法。
1.NSString *htmlStr = [webView stringByEvaluatingJavaScriptFromString:@"document.body.outerHTML"];
WKWebView
框架:@import WebKit;
代理:WKNavigationDelegate,WKUIDelegate,WKScriptMessageHandler
1.WKNavigationDelegate:开始加载、加载中有内容开始返回、加载完成、加载失败、接收到服务器跳转请求之后调用、在收到响应后决定是否跳转、在发送请求之前决定是否跳转(这个协议主要处理一些跳转、加载处理操作)
2.WKUIDelegate:为WebView提供了原生的弹框方便iOS做更改(警告、确认框、输入框)runJavaScriptAlertPanelWithMessage、runJavaScriptConfirmPanelWithMessage、runJavaScriptTextInputPanelWithPrompt,JS在调用alert()、confirm()和prompt()方法时没有反应,若要正常使用这三个方法,iOS需要实现WKUIDelegate中的三个方法模拟JS的这三个方法。WKUIDelegate中的三个方法都有completionHandlerblock参数,在iOS实现对应的功能后必须调用此block完成回调,否则会崩溃。还有一个代理方法是创建一个新的WKWebView。这个协议主要处理JS脚本,确认框,警告框等。此代理方法在使用中最好实现,否则遇到网页alert的时候,如果此代理方法没有实现,则不会出现弹框提示。
3.WKScriptMessageHandler: didReceiveScriptMessage此代理方法就是和JavaScript交互相关。
JS调用OC:
1.WKUserContentController对象添加Script消息回调addScriptMessageHandler:self name:@"注册一个name的js方法"。
2.相关类WKWebViewConfiguration设置其中的userContentController属性。
3.初始化WKWebView时候设置属性configuration为WKWebViewConfiguration对象。
4.didReceiveScriptMessage代理方法处理交互事件
5.最后dealloc不要忘记removeScriptMessageHandlerForName:@"注册一个name的js方法"
OC调用JS:查看访问html的body标签内内容的方法。
1.[self.wkWebView evaluateJavaScript:@"JS的方法名称" completionHandler:回调];
深拷贝和浅拷贝 以及各个修饰词的区别
1.Copy和Strong
(1)修饰不可变的字符串
示例代码:
@property (strong, nonatomic) NSString *strStrong;
@property (copy, nonatomic) NSString *strCopy;
NSString *str = @"aaaa";
NSLog(@"str = %@ 内存地址 = %p 指针地址 = %p",str,str,&str);
self.strStrong = str;
NSLog(@"strong = %@ 内存地址 = %p 指针地址 = %p",self.strStrong,self.strStrong,&_strStrong);
self.strCopy = str;
NSLog(@"copy = %@ 内存地址 = %p 指针地址 = %p\n",self.strCopy,self.strCopy,&_strCopy);
------------------------------------------------------------------------------------------------
str = @"bbb";
NSLog(@"str = %@ 内存地址 = %p 指针地址 = %p",str,str,&str);
NSLog(@"strong = %@ 内存地址 = %p 指针地址 = %p",self.strStrong,self.strStrong,&_strStrong);
NSLog(@"copy = %@ 内存地址 = %p 指针地址 = %p\n",self.strCopy,self.strCopy,&_strCopy);
打印结果:
2019-05-05 11:11:08.840288+0800 TTT[3684:27977] str = aaaa 内存地址 = 0x108dfe098 指针地址 = 0x7ffee6e00a98
2019-05-05 11:11:08.840362+0800 TTT[3684:27977] strong = aaaa 内存地址 = 0x108dfe098 指针地址 = 0x7fe957417710
2019-05-05 11:11:08.840426+0800 TTT[3684:27977] copy = aaaa 内存地址 = 0x108dfe098 指针地址 = 0x7fe957417718
------------------------------------------------------------------------------------------------
2019-05-05 11:11:08.840575+0800 TTT[3684:27977] str = bbb 内存地址 = 0x108dfe118 指针地址 = 0x7ffee6e00a98
2019-05-05 11:11:08.840658+0800 TTT[3684:27977] strong = aaaa 内存地址 = 0x108dfe098 指针地址 = 0x7fe957417710
2019-05-05 11:11:08.840731+0800 TTT[3684:27977] copy = aaaa 内存地址 = 0x108dfe098 指针地址 = 0x7fe957417718
结论:源对象为不可变字符串而言,不论使用copy还是strong,所对应的值是不会发生变化的,strong和copy不会开辟新的内存,固不是深拷贝,属于浅拷贝。
补充:此处使用的是self.赋值,如果使用下划线结果是一样的,此处的self.是浅拷贝
(2)修饰可变的字符串
示例代码:
@property (strong, nonatomic) NSString *strStrong;
@property (copy, nonatomic) NSString *strCopy;
NSMutableString *mutableStr = [[NSMutableString alloc] initWithString:@"aaa"];
NSLog(@"mutableStr = %@ 内存地址 = %p 指针地址 = %p",mutableStr,mutableStr,&mutableStr);
self.strStrong = mutableStr;
NSLog(@"strong = %@ 内存地址 = %p 指针地址 = %p",self.strStrong,self.strStrong,&_strStrong);
self.strCopy = mutableStr;
NSLog(@"copy = %@ 内存地址 = %p 指针地址 = %p\n",self.strCopy,self.strCopy,&_strCopy);
[mutableStr appendString:@"bbb"];
NSLog(@"mutableStr = %@ 内存地址 = %p 指针地址 = %p",mutableStr,mutableStr,&mutableStr);
NSLog(@"strong = %@ 内存地址 = %p 指针地址 = %p",self.strStrong,self.strStrong,&_strStrong);
NSLog(@"copy = %@ 内存地址 = %p 指针地址 = %p",self.strCopy,self.strCopy,&_strCopy);
打印结果:
2019-05-05 11:23:18.961313+0800 TTT[3728:32476] mutableStr = aaa 内存地址 = 0x600002944030 指针地址 = 0x7ffee8f8ca98
2019-05-05 11:23:18.961510+0800 TTT[3728:32476] strong = aaa 内存地址 = 0x600002944030 指针地址 = 0x7fb61f612810
2019-05-05 11:23:18.961594+0800 TTT[3728:32476] copy = aaa 内存地址 = 0x8d76ba2dd400d6bd 指针地址 = 0x7fb61f612818
2019-05-05 11:23:18.961682+0800 TTT[3728:32476] mutableStr = aaabbb 内存地址 = 0x600002944030 指针地址 = 0x7ffee8f8ca98
2019-05-05 11:23:18.961835+0800 TTT[3728:32476] strong = aaabbb 内存地址 = 0x600002944030 指针地址 = 0x7fb61f612810
2019-05-05 11:23:18.961946+0800 TTT[3728:32476] copy = aaa 内存地址 = 0x8d76ba2dd400d6bd 指针地址 = 0x7fb61f612818
结论:数据源为可变字符串而言,使用copy申明属性,会开辟一块新的内存空间存放值,源数据无论怎样变化,都不会影响copy属性中的值,属于深拷贝。使用strong申明属性,不会开辟新的内存空间,只会引用到数据源内存地址,因此数据改变,则strong属性也会改变,属于浅拷贝。
补充:此处使用的是self.赋值,如果使用下划线,则还是“aaa”,因为@property声明属性变量时,会自动生成下划线+属性名命名的实例变量,并生成其对应的getter、setter方法(@synthesize copyyStr = _copyyStr),self.赋值时,会调用setter方法,而下划线是直接赋值,并不会调用setter方法
(3)修饰可变数组、可变字典
示例代码:
@property (copy, nonatomic) NSMutableArray *originArray_0;
@property (strong, nonatomic) NSMutableArray *originArray_1;
self.originArray_0 = [NSMutableArray arrayWithCapacity:0];
self.originArray_1 = [NSMutableArray arrayWithCapacity:0];
NSLog(@"self. originArray_0 class name %@",[self.originArray_0 class]);
NSLog(@"self. originArray_1 class name %@",[self.originArray_1 class]);
打印结果:
2019-05-05 11:33:17.006850+0800 TTT[3920:36550] self. originArray_0 class name __NSArray0
2019-05-05 11:33:17.006963+0800 TTT[3920:36550] self. originArray_1 class name __NSArrayM
结论:copy申明可变数据,初始化或赋值后,变成不可变数组,对数组执行增删会出错误,这是因为copy属性修饰后,在初始化或赋值时,会先执行copy操作,然后赋值。所以开发中对可变数组、字典使用strong属性修饰
扩展:
(1)NSArray0:不可变的数组
(2)NSArrayM:NSMutbleArray *arr4 = [NSMutbleArray array];---初始化后的可变数组类名
(3)NSArrayI:NSArray *arr2 = [[NSArray alloc]init];---初始化后的不可变数组类名
(4)NSPlaceHolderArray:NSArray *arr3 = [NSArray alloc];---alloc但未初始化init的
汇总:
浅拷贝:指针的复制,指向同一块内存。
深拷贝:内存的复制,指向不同的内存,互不干涉。
1.当原字符串是NSString时,因为其是不可变字符串,所以无论使用strong还是copy修饰,都指向原来的对象,copy操作只是做了一次浅拷贝。
2.当原字符串是NSMutableString时,strong只是将原字符串的引用计数+1,而copy则是对原字符串做了次深拷贝,从而生成了一个新对象,并且copy的对象指向了这个新对象。
所以如果字符串是NSMutableString,使用strong只会增加引用计数,但是copy会执行一次深拷贝,会造成不必要的内存浪费。而如果原字符串是NSString,strong和copy效果一样不会存在这个问题,但是我们一般声明NSString时,不希望它改变,所以一般情况下建议使用copy,这样可以避免NSMutableString带来的错误。
3.所以,在我们写代码过程中,一般不希望NSString改变,固然声明为copy,我们对NSMutalbleArray时,我们需要它改变,所以声明为Strong,Copy会让可变变为不可变的哦(很重要、牢牢记住)
2.Copy和retain
a.copy其实是建立了一个相同的对象,而retain不是
b.copy是内容拷贝,retain是指针拷贝
c.copy是内容拷贝,对于像NSString,的确是这样,但是如果copy是一个NSArray呢?这时只是copy指向了array中相对应元素的指针,这便是浅拷贝。
3.assign和retain
a.assign:简单赋值,不更改索引计数
b.assign就是直接赋值
c.retain使用了引用计数,retain引用计数+1,release引用计数-1,当引用计数为0时,dealloc函数被调用,内存被回收。
4.weak和strong
a.默认是strong,只有一种情况你需要使用weak,那就是防止循环引用,A类包含B类,B类包含了A类,这时候要使用weak。
b.声明weak的指针,指针指向的地址一旦被释放,这些指针都将被置为nil,可以有效的防止野指针。
c.strong和weak是ARC引入的对象变量属性,ARC下的strong相当于MRC下的retain,ARC下的weak相当于MRC下的assign
5.retain和strong
在ARC机制下,retain和strong基本相同,MRC下可以看到不同,MR下strong修饰Block内部进行了Copy(retain是NSStackBlock,strong和copy是MSMallocBlock)
6.weak和assign
weak比assign多了一个功能,当对象小时候自动把指针变成nil,而strong等同于retain
。
Block
一、写法
@property (copy, nonatomic) void (^block)(void);
typedef void(^XBTBlock)(void); @property (copy, nonatomic) XBTBlock block;
二、block
1.block本质上也是一个OC对象,它内部也有个isa指针,block是封装了函数调用以及函数调用环境的OC对象,block是封装函数及其上下文的OC对象。
2.为什么block对auto和static变量捕获有差异?auto自动变量可能会销毁的,内存可能会消失,不采用指针访问;static变量一直保存在内存中,指针访问即可
3.block对全局变量的捕获方式是?block不需要对全局变量捕获,都是直接采用取值的
4.为什么局部变量需要捕获?考虑作用域的问题,需要跨函数访问,就需要捕获
5.block的变量捕获(capture),为了保证block内部能够正常访问外部的变量,block有个变量捕获机制。局部变量(auto访问方式值传递、static访问方式指针传递)可以捕获到block内部,全局变量不可以捕获到block内部,访问方式直接访问。
6.block里访问self是否会捕获?会,self是当调用block函数的参数,参数是局部变量,self指向调用者
7.block里访问成员变量是否会捕获?会,成员变量的访问其实是self->xx,先捕获self,再通过self访问里面的成员变量
8.Block分为三种:全局Block数据区、栈Block、堆Block(定义函数外的Block和定义函数内没有捕获任何自动变量、定义函数内自动捕获自动变量、栈Block Copy)(NSGlobalBlock、NSStackBlock、NSMallocBlock),block的类型,取决于isa指针,可以通过调用class方法或者isa指针查看具体类型,最终都是继承自NSBlock类型
9.如何判断block是哪种类型?没有访问auto变量的block是全局Block,访问了auto变量的block是栈block,copy过后的block是堆block。
10.对每种类型block调用copy操作后是什么结果?全局block copy后什么都不做,栈区block copy后从栈复制到堆,堆block copy,引用计数增加。
11.在ARC环境下,编译器会根据情况自动将栈上的block复制到堆上的几种情况?block作为函数返回值时,将block赋值给__strong指针时,block作为Cocoa API中方法名含有usingBlock的方法参数时,block作为GCD API的方法参数时
二、block相关
1.__block:ARC和MRC都可以使用,可以修饰对象,也可以修饰基本数据类型。__block修饰的对象可以在Block内部重新赋值。__block在ARC下可能会导致循环引用,MRC下避免循环引用。__block是强引用类型。__block可以预防死锁。__block可以修改变量的值。__block可以捕获外界变量。
2.__weak:只能在ARC下使用,只能修饰对象,不能修饰基本数据类型。__weak修饰的对象不可以在Block内部重新赋值。__weak在ARC下可以避免循环引用。__weak是弱引用类型。Block中用到了外部变量则用__weak修饰,避免内存泄漏循环引用。
3.Block容易引起循环引用的原因,因为Block内部用到的外部变量进行retain的时机和该block的执行时机是不同步的,在block声明的时候就对外部变量进行了retain,而block何时执行甚至是否执行是不可预测的,造成循环引用和内存泄漏。还有就是block一般是匿名的,而且copy赋值,手动释放block对象比较困难。对象引用了Block,Block又引用对象,造成循环引用,强引用,需要若引用。
4.为什么block修饰词用copy,因为Block存档在栈中,所以block会在函数调用结束后被销毁,在调用就是空指针异常,如果用copy修饰的话,可以另其保存在堆区,它的生命周期会随着对象的销毁而结束。
6.__strong:有些对象还没有使用就已经被释放了,是因为弱引用对象本身,还没使用就被释放了,解决办法就是使用__strong。比如GCD是个很特殊的,因为它内部对对象是弱引用的,在Block中使用GCD会导致这个对象还没使用就被释放了。所以__strong只是Block内部的强引用,不会印象block以外的释放。
7.为什么要用weakSelf和strongSelf?假如有个页面正在网络请求,用户不愿意等待了点击了返回按钮,这时候weakSelf控制器不存在了,Block中用到了weakSelf,虽然if判断weakSelf是否存在,但是依然可能会崩溃,因为判断语句中用到了weakSelf,所以为了安全起见加上strongSelf,因为如果self有效那么strongSelf就是利用了循环引用的特征保证了block执行的时候self对象不会被释放,strongSelf是局部变量执行完成后就会释放掉。
8.Masonry里面的block是局部block栈上,block内部引用self不会造成循环引用。是否会发生循环引用要看函数内部是否copy到了堆区,比如把它赋给全局block。
结果:10(创建block的时候已经把age的值存储在里面了,捕获变量)
int age=10;
void (^Block)(void) = ^{
NSLog(@"age:%d",age);
};
age = 20;
Block();
结果:10,11(atuo变量block访问方式是值传递,static变量block访问方式是指针传递)
auto int age = 10;
static int num = 25;
void (^Block)(void) = ^{
NSLog(@"age:%d,num:%d",age,num);
};
age = 20;
num = 11;
Block();
TableView的优化
造成卡顿的原因:CPU原因、GPU原因,CPU核数越多处理问题能力越强,如果硬件配置有限,就要考虑如何发挥最大限度作用,造成卡顿的原因就是阻塞了主线程。
1.减轻CPU负荷(CPU主要负责快速调度任务,大量计算工作,所以优化应该将CPU计算降低)
1.1 减少Cell的自定义类型,重用Cell
1.1 提前计算好Cell的高度,缓存在相应的数据源模型中(获取后台数据时缓存)
1.2 尽可能降低Storyboard、Xib的使用(因为其本身是xml文件,添加删除控件多了一个encode、decode,增加CPU使用率,并且要避免臃肿XIB文件,因为它是在主线程加载布局)
1.3 滑动过程中尽量减少重新布局(因为自动布局是给控件添加约束,约束最终还是转换为Frame,大量的约束会增加CPU的计算)
1.4 少用或者不用透明的图层,使用不透明视图,对于不透明的View设置opaque为YES,这样在绘制View时就不用考虑被View覆盖的其他内容,避免GPU对Cell下面的内容也进行绘制。
1.5 当Cell中显示内容来自Web则需要异步加载,缓存结果请求。还可以去缓存一些View放在model中。
1.6 减少subViews
1.7 不要addView动态添加View在Cell上
2.不要阻塞主线程(因为UIKit基本上都是在主线程上进行,当所有代码逻辑都放在主线程时候,耗时操作可能会卡住主线程造成程序无法响应卡顿)
2.1 比如说UImage的加载方式,imageNamed会对图片缓存,如果使用多次可以选择用它,imageWithContentsOfFile适用于图片只会用到一次没必要缓存,用完就释放了。还有就是按需加载,例如SDWebImage实现的异步加载(TableView滚动时后的停止滑动或者减速时候后开始异步加载网络图片的原理就是滑动时获取偏移量,从而获取当前显示的Cell,从而判断加载完成的图片不再进行加载)
2.2 尽量避免离屏渲染,比如阴影、圆角、图层蒙版这些。
2.3 异步绘制复杂界面(UIView中的layer属性才是视图显示的本质,layer中的contents属性就是要显示的具体内容,其实就是绘制在一张画布上,绘制完成从画布中导出图片,再把图片交给contents就完成了显示,重写displayLayer方法,在该方法中异步线程里创建画布,并绘制,然后主线程传给layer.contents)
数据读写同步
加锁
1.锁:又叫临界区,指的是一块对公共资源进行访问的代码,并非一种机制或是算法。
2.常用的锁:互斥锁、自旋锁、读写锁、递归锁、条件锁、信号量
互斥锁:是一种用于多线程编程中,防止两条线程同时对同一公共资源(比如全局变量)进行读写的机制。该目的通过将代码切片成一个一个的临界区而达成。(NSLock、pthread_mutex、@synchronized)
自旋锁:是用于多线程同步的一种锁,线程反复检查锁变量是否可用。由于线程在这一过程中保持执行,因此是一种忙等待。一旦获取了自旋锁,线程会一直保持该锁,直至显式释放自旋锁。 自旋锁避免了进程上下文的调度开销,因此对于线程只会阻塞很短时间的场合是有效的。(OSSpinLock、os_unfair_lock)
读写锁:是计算机程序的并发控制的一种同步机制,也称“共享-互斥锁”、多读者-单写者锁) 用于解决多线程对公共资源读写问题。读操作可并发重入,写操作是互斥的。 读写锁通常用互斥锁、条件变量、信号量实现。(pthread_rwlock)
递归锁:递归锁有一个特点,就是同一个线程可以加锁N次而不会引发死锁。(NSRecursiveLock、pthread_mutex(recursive))
条件锁:就是条件变量,当进程的某些资源要求不满足时就进入休眠,也就是锁住了。当资源被分配到了,条件锁打开,进程继续运行。(NSCondition、NSConditionLock)
信号量:是一种更高级的同步机制,互斥锁可以说是semaphore在仅取值0/1时的特例。信号量可以有更多的取值空间,用来实现更加复杂的同步,而不单单是线程间互斥。(dispatch_semaphore)
自定义对象的读写怎么保证线程安全?
需求:这个对象有很多属性,时时刻刻读取和写入,并且保证线程安全
方案:用栏栅dispatch_barrier_async,重写get 和setter 方法。
[https://www.jianshu.com/p/9d067f11f5b9](https://www.jianshu.com/p/9d067f11f5b9)
进程、线程、队列、串行、并行
进程:正在进行中的程序被称为进程,负责程序运行的内存分配,每一个进程都有自己独立的虚拟内存空间。
线程:是进程中的一个独立执行路径控制单元,一个进程中至少包含一条线程叫做主线程。
全局队列概念:是系统开发的,直接拿过来用,与并行队列类似,但调试时无法确认操作所在队列
主队列概念:每一个应用程序对应唯一一个主队列,直接get使用,在多线程开发中,使用主队列更新UI
队列:队列负责管理多个任务,队列拥有一个线程池,池子里有一个或多个线程,它按要求将每个任务调度到某一个线程执行,说白了队列就是添加任务,队列就是往线程中添加任务,注意添加的时机。(串行、并行)一种先进先出的数据结构,线程的创建和回收不需言程序员操作,由队列负责。
线程:同步不会创建新的线程,会阻塞当前的线程在这个线程里执行任务,异步不会阻塞当前线程,会选择在恰当的时机在当前线程或者另开线程执行任务,看系统如何调度,开始任务和完成任务时间是不确定的。(同步、异步)
串行:后一个任务等待前一个任务结束后再执行,按添加顺序一个个执行(A执行完才添加B任务)
并行:后一个任务不会等待前一个任务,不等前一个任务完成就会分配新任务(B不用等待A执行完再添加任务)队列是先进先出的,任务执行完毕,不一定出队列,只有前面的任务执行完了,才会出队列
同步:dispatch_sync,同步不会创建新的线程,会阻塞当前的线程在这个线程里执行任务
异步:dispatch_async,异步不会阻塞当前线程,会选择在恰当的时机在当前线程或者另开线程执行任务,看系统如何调度,开始任务和完成任务时间是不确定的。
在主线程中通过Sync方式把block加入到主线程队列,那么当前线程(主线程)就会被阻塞,等待block执行完毕后回调,而blcok被加入了主线程队列中,主线程队列又是一个串行队列,那么block的执行就需要等待主线程执行完前面的任务后才能被执行,这样的相互等待状态产生了死锁,block永远没有机会执行。
Queue(队列):队列分为串行和并行。串行队列上面你按照A、B、C、D的顺序添加四个任务,这四个任务按顺序执行,结束顺序也肯定是A、B、C、D。而并行队列上面这四个任务同时执行,完成的顺序是随机的,每次都可能不一样。
Async VS Sync(异步执行和同步执行):使用dispatch_async 调用一个block,这个block会被放到指定的queue队尾等待执行,至于这个block是并行还是串行执行只和dispatch_async参数里面指定的queue是并行和串行有关。但是dispatch_async会马上返回。
使用dispatch_sync 同样也是把block放到指定的queue上面执行,但是会等待这个block执行完毕才会返回,阻塞当前queue直到sync函数返回。
所以队列是串行、并行 和 同步、异步执行调用block是两个完全不一样的概念。这两个概念清楚了之后就知道为什么死锁了。
a.串行队列+同步任务:不会开启新的线程,任务逐步完成。
b.并行队列+同步任务:不会开启新的线程,任务逐步完成。
c.串行队列+异步任务:开启新的线程,任务逐步完成。(开启1条子线程)
d.并行队列+异步任务:开启新的线程,任务是同步执行的。(开启多条子线程)
e.主队列 + 同步任务:主线程死锁,子线程不开启新的线程,主线程串行执行任务。
f.主队列 + 异步任务:不开启新线程,主线程执行任务,串行执行任务。
只要是异步就会创建新的线程,只要是异步并行执行的任务就是同时执行任务的。主队列很特殊。
线程题
事例1
答案与分析:1,异步输出1243,同步主队列造成死锁,使用dispatch_sync是把block内的内容放到指定的主队列上面执行,但是会等待这个block执行完毕才会返回,阻塞当前队列直到sync函数返回。在主线程中通过Sync方式把block加入到主线程队列,那么当前线程(主线程)就会被阻塞,等待block执行完毕后回调,而blcok被加入了主线程队列中,主线程队列又是一个串行队列,那么block的执行就需要等待主线程执行完前面的任务后才能被执行,这样的相互等待状态产生了死锁,block永远没有机会执行。如果执行上面语句的线程不是主线程则不会造成死锁。
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(@"1");
dispatch_sync(dispatch_get_main_queue(), ^{
[self doSomething];
NSLog(@"3");
});
NSLog(@"2");
}
- (void)doSomething
{
NSLog(@"4");
}
事例2
答案与分析:1,异步输出1234,同步主队列造成死锁。
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(@"1");
dispatch_sync(dispatch_get_main_queue(), ^{
[self performSelector:@selector(doSomething) withObject:nil afterDelay:0];
NSLog(@"3");
});
NSLog(@"2");
}
- (void)doSomething
{
NSLog(@"4");
}
事例3
答案与分析:1432,异步输出1243
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(@"1");
dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[self doSomething];
NSLog(@"3");
});
NSLog(@"2");
}
- (void)doSomething
{
NSLog(@"4");
}
事例4
答案与分析:1324,异步输出123
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(@"1");
dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[self performSelector:@selector(doSomething) withObject:nil afterDelay:0];
NSLog(@"3");
});
NSLog(@"2");
}
- (void)doSomething
{
NSLog(@"4");
}
事例5
答案与分析:14532,异步输出12435,第一个异步第二个同步输出12453,第一个同步第二个异步输出14352
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(@"1");
dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"4");
dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"5");
});
NSLog(@"3");
});
NSLog(@"2");
}
- (void)doSomething
{
NSLog(@"4");
}
设计模式
工厂模式
作为一种创建类模式,在任何需要生成复杂对象的地方,都可以使用工厂方法模式。
有一点需要注意的地方就是复杂对象适合使用工厂模式,而简单对象,特别是只需要通过 new 就可以完成创建的对象,无需使用工厂模式。如果使用工厂模式,就需要引入一个工厂类,会增加系统的复杂度。
用法:新建工厂类,然后创建某个视图。说白了就是由一个工厂类根据传入的参数决定创建哪一种的产品类。比如用单例创建工厂累。
策略模式
定义了一系列的算法,并将每一个算法封装起来,而且使它们还可以相互替换。策略模式让算法独立于使用它的客户而独立变化。策略模式是对算法的包装,是把使用算法的责任和算法本身分割开来,委派给不同的对象管理。看到策略模式的时候有的时候跟简单工厂相比较,其实有很大的迷惑性,都是继承多态感觉没有太大的差异性,简单工厂模式是对对象的管理,策略模式是对行为的封装。
Instruments工具
1.Blank: 创建一个空的模板,可以从Library库中添加其他模板.
2.Activity Monitor: 监控进程级别的CPU,内存,磁盘,网络使用情况,可以得到你的应用程序在手机运行时总共占用的内存大小.
3.Allocations:跟踪过程的匿名虚拟内存和堆的对象提供类名和可选保留/释放历史,可以检测每一个堆对象的分配内存情况.
4.Cocoa Layout : 观察NSLayoutConstraint对象的改变,帮助我们判断什么时间什么地点的constraint是否合理.观察约束变化,找出布局代码的问题所在.
5.Core Animation: 图形性能,这个模块显示程序显卡性能以及CPU使用情况,查看界面流畅度.
6.CoreData: 这个模块跟踪Core Data文件系统活动.
7.Counters : 收集使用时间或基于事件的抽样方法的性能监控计数器(PMC)事件.
8.Energy Log: 耗电量监控.
9.File Activity: 检测文件创建,移动,变化,删除等.
10.一般的措施内存使用情况,检查泄漏的内存,并提供了所有活动的分配和泄漏模块的类对象分配统计信息以及内存地址历史记录.
11.Metal System Trace: Metal API是apple 2014年在ios平台上推出的高效底层的3D图形API,它通过减少驱动层的API调用CPU的消耗提高渲染效率.
12.Network: 用链接工具分析你的程序如何使用TCP/IP和UDP/IP链接.
13.SceneKit: 3D性能状况分析.
14.System Trace: 系统跟踪,通过显示当前被调度线程提供综合的系统表现,显示从用户到系统的转换代码通过两个系统调用或内存操作.
15.System Usage: 这个模板记录关于文件读写,sockets,I/O系统活动,输入输出.
16.Time Profiler(时间探查): 执行对系统的CPU上运行的进程低负载时间为基础采样.
17.Zombies: 测量一般的内存使用,专注于检测过度释放的野指针对象,也提供对象分配统计,以及主动分配的内存地址历史.
性能优化
##### 处理器优化(CPU和GPU)
在屏幕成像的过程中,CPU和GPU起着至关重要的作用,CPU:中央处理器,负责对象的创建和销毁、对象属性的调整、布局计算、文本的计算和排版、图片的格式转换和解码、图像的绘制(Core Graphics),GPU:图形处理器,负责纹理的渲染。流程 :CPU计算 - GPU渲染 - 帧缓存读取 - 视频控制器显示 - 屏幕正常显示,在iOS中是双缓冲机制,有前帧缓存、后帧缓存,当垂直同步信号来的时候,开始水平同步信号按行进行刷新屏幕像素。
卡顿原因:CPU、CPU高消耗
卡顿解决:尽可能减少CPU、GPU的资源消耗
CPU优化:
1.尽量用轻量级的对象,比如用不到事件处理的地方,可以考虑使用CALayer取代UIView
2.不要频繁地调用UIView的相关属性,比如frame、bounds、transform等属性,尽量减少不必要的修改
CPU优化
3.尽量提前计算好布局,在有需要时一次性调整对应的属性,不要多次修改属性,比如tableViewCell刷新时候,每个cell对应一个处理好的layout值。
4.Autolayout会比直接设置frame消耗更多的CPU资源,这个在新版本的iOS里autolayout性能已经接近于frame。
5.图片的size最好刚好跟UIImageView的size保持一致,有些厂商的cdn上会包含这个功能,拉取图片时候可以直接指定图片大小,无论对渲染还是内存都是一个好的方案。
6.控制一下线程的最大并发数量
7.尽量把耗时的操作放到子线程,文本处理(尺寸计算、绘制),图片处理(解码、绘制)
GPU优化
1.尽量避免短时间内大量图片的显示,尽可能将多张图片合成一张进行显示
2.GPU能处理的最大纹理尺寸是4096x4096,一旦超过这个尺寸,就会占用CPU资源进行处理,所以纹理尽量不要超过这个尺寸
3.尽量减少视图数量和层次
4.减少透明的视图(alpha<1),不透明的就设置opaque为YES
5.尽量避免出现离屏渲染
(在OpenGL中,GPU有2种渲染方式,On-Screen Rendering:当前屏幕渲染,在当前用于显示的屏幕缓冲区进行渲染操作,Off-Screen Rendering:离屏渲染,在当前屏幕缓冲区以外新开辟一个缓冲区进行渲染操作
离屏渲染消耗性能的原因:需要创建新的缓冲区,离屏渲染的整个过程,需要多次切换上下文环境,先是从当前屏幕(On-Screen)切换到离屏(Off-Screen);等到离屏渲染结束以后,将离屏缓冲区的渲染结果显示到屏幕上,又需要将上下文环境从离屏切换到当前屏幕,哪些操作会触发离屏渲染?光栅化,layer.shouldRasterize = YES。遮罩,layer.mask,圆角,同时设置layer.masksToBounds = YES,layer.cornerRadius大于0,考虑通过CoreGraphics绘制裁剪圆角,或者叫美工提供圆角图片,阴影,layer.shadowXXX,如果设置了layer.shadowPath就不会产生离屏渲染)
卡顿检测(平时所说的“卡顿”主要是因为在主线程执行了比较耗时的操作,可以添加Observer到主线程RunLoop中,通过监听RunLoop状态切换的耗时,以达到监控卡顿的目的)
##### 耗电优化
耗电主要来源(CPU处理,网络,定位,图像)
优化(尽可能降低CPU、GPU功耗,少用定时器,优化I/O操作尽量不要频繁写入小数据,最好批量一次性写入读写大量重要数据时,考虑用dispatch_io,其提供了基于GCD的异步操作文件I/O的API。用dispatch_io系统会优化磁盘访问数据量比较大的,建议使用数据库(比如SQLite、CoreData),网络优化减少、压缩网络数据如果多次请求的结果是相同的,尽量使用缓存使用断点续传,否则网络不稳定时可能多次传输相同的内容网络不可用时不要尝试执行网络请求让用户可以取消长时间运行或者速度很慢的网络操作设置合适的超时时间)
* 尽可能降低CPU、GPU功耗
* 少用定时器
* 优化I/O操作
1.尽量不要频繁写入小数据,最好批量一次性写入
2.读写大量重要数据时,考虑用dispatch_io,其提供了基于GCD的异步操作文件I/O的API。用dispatch_io系统会优化磁盘访问
3.数据量比较大的,建议使用数据库(比如SQLite、CoreData)
* 网络优化
1.减少、压缩网络数据
2.如果多次请求的结果是相同的,尽量使用缓存
3.使用断点续传,否则网络不稳定时可能多次传输相同的内容
4.网络不可用时,不要尝试执行网络请求
5.让用户可以取消长时间运行或者速度很慢的网络操作,设置合适的超时时间
6.批量传输,比如,下载视频流时,不要传输很小的数据包,直接下载整个文件或者一大块一大块地下载。如果下载广告,一次性多下载一些,然后再慢慢展示。如果下载电子邮件,一次下载多封,不要一封一封地下载
* 定位优化
1.如果只是需要快速确定用户位置,最好用CLLocationManager的requestLocation方法。定位完成后,会自动让定位硬件断电
2.如果不是导航应用,尽量不要实时更新位置,定位完毕就关掉定位服务
3.尽量降低定位精度,比如尽量不要使用精度最高的kCLLocationAccuracyBest
4.需要后台定位时,尽量设置pausesLocationUpdatesAutomatically为YES,如果用户不太可能移动的时候系统会自动暂停位置更新
5.尽量不要使用startMonitoringSignificantLocationChanges,优先考虑startMonitoringForRegion:
* 硬件检测优化
1.用户移动、摇晃、倾斜设备时,会产生动作(motion)事件,这些事件由加速度计、陀螺仪、磁力计等硬件检测。在不需要检测的场合,应该及时关闭这些硬件
##### 启动优化
* APP的启动可以分为2种
1.冷启动(Cold Launch):从零开始启动APP
2.热启动(Warm Launch):APP已经在内存中,在后台存活着,再次点击图标启动APP
* APP启动时间的优化,主要是针对冷启动进行优化
* 通过添加环境变量可以打印出APP的启动时间分析(Edit scheme -> Run -> Arguments)
DYLD_PRINT_STATISTICS设置为1
如果需要更详细的信息,那就将DYLD_PRINT_STATISTICS_DETAILS设置为1
* APP的冷启动可以概括为3大阶段
dyld
runtime
main
1.dyld
* dyld(dynamic link editor),Apple的动态链接器,可以用来装载Mach-O文件(可执行文件、动态库等)
* 启动APP时,dyld所做的事情有
装载APP的可执行文件,同时会递归加载所有依赖的动态库
当dyld把可执行文件、动态库都装载完毕后,会通知Runtime进行下一步的处理
2.runtime
* 启动APP时,runtime所做的事情有
调用map_images进行可执行文件内容的解析和处理
在load_images中调用call_load_methods,调用所有Class和Category的+load方法
进行各种objc结构的初始化(注册Objc类 、初始化类对象等等)
调用C++静态初始化器和**attribute**((constructor))修饰的函数
* 到此为止,可执行文件和动态库中所有的符号(Class,Protocol,Selector,IMP,…)都已经按格式成功加载到内存中,被runtime 所管理
3.main
* 总结一下
APP的启动由dyld主导,将可执行文件加载到内存,顺便加载所有依赖的动态库
并由runtime负责加载成objc定义的结构
所有初始化工作结束后,dyld就会调用main函数
接下来就是UIApplicationMain函数,AppDelegate的application:didFinishLaunchingWithOptions:方法
按照不同的阶段
* dyld
减少动态库、合并一些动态库(定期清理不必要的动态库)
减少Objc类、分类的数量、减少Selector数量(定期清理不必要的类、分类)
减少C++虚函数数量
Swift尽量使用struct
* runtime
用+initialize方法和dispatch_once取代所有的**attribute**((constructor))、C++静态构造器、ObjC的+load
* main
在不影响用户体验的前提下,尽可能将一些操作延迟,不要全部都放在finishLaunching方法中
按需加载
##### 安装包瘦身
* 安装包(IPA)主要由可执行文件、资源组成
* 资源(图片、音频、视频等)
采取无损压缩
去除没有用到的资源: [https://github.com/tinymind/LSUnusedResources](https://links.jianshu.com/go?to=https%3A%2F%2Fgithub.com%2Ftinymind%2FLSUnusedResources)
* 可执行文件瘦身
编译器优化
Strip Linked Product、Make Strings Read-Only、Symbols Hidden by Default设置为YES
去掉异常支持,Enable C++ Exceptions、Enable Objective-C Exceptions设置为NO, Other C Flags添加-fno-exceptions
* 利用AppCode([https://www.jetbrains.com/objc/](https://links.jianshu.com/go?to=https%3A%2F%2Fwww.jetbrains.com%2Fobjc%2F))检测未使用的代码:菜单栏 -> Code -> Inspect Code
* 编写LLVM插件检测出重复代码、未被调用的代码
##### LinkMap
* XCode生成LinkMap文件,可以查看可执行文件的具体组成
![image](https://img.haomeiwen.com/i11238923/c24cddf0bdd856f5.png?imageMogr2/auto-orient/strip|imageView2/2/w/663/format/webp)
* 可借助第三方工具解析LinkMap文件: [https://github.com/huanxsd/LinkMap](https://links.jianshu.com/go?to=https%3A%2F%2Fgithub.com%2Fhuanxsd%2FLinkMap)
视频直播
主播端:音视频采集、视频处理、音视频编码压缩、推流
服务器端:CDN数据分发、截屏、录制、转码等
用户端:拉流、解码、播放
1.主播端
音视频采集:视频采集(AVFoundation和三方GPUIImageVideoCamera)、音频采集(AVFoundation)
视频处理:GPUIImage美颜、水印等(三方SDK)
音视频编码压缩:软编码(要做视频编解码h.264、MPEG可以用三方,音频编解码mp3、ACC可以用三方)、硬编码(要做视频编解码Video Toolbok可以用原生,音频编解码Audio Toolbok可以用原生)
推流:音视频封装(FLV或者TS格式)、协议(RTMP、HLS、FLV)、数据推送(无)
2.服务器端
CDN数据分发、截屏、录制、转码等
3.用户端
拉流:RTMP、HLS、FLV
解码:软解码、硬解码
播放:ijkPlayer开源播放器
其他功能:
聊天、礼物、提出、禁言、管理等
流媒体:是指采用流式传输的方式在网上播放的媒体格式, 是边传边播的媒体,是多媒体的一种!
帧:视频是由很多连续图像组成, 每一帧就代表一幅静止的图像
GOP:画面组,一个GOP就是一组连续的画面,每个画面都是一帧,GOP就是很多帧的集合!
帧的分类:I帧、P帧、B帧
硬解码:由显卡核心GPU来对高清视频进行解码工作,CPU占用率很低,画质效果比软解码略差一点,需要对播放器进行设置。
优点:播放流畅、低功耗
缺点:受视频格式限制、功耗大、画质没有软解码好
软解码:由CPU负责解码进行播放
优点:不受视频格式限制、画质略好于硬解
缺点:会占用过高的资源、对于高清视频可能没有硬解码流畅(主要看CPU的能力)
TS: 是流媒体封装格式的一种,流媒体封装的好处就是不需要加载索引再播放,大大降低了首次载入的延迟,两个TS片段可以无缝拼接,播放器能连续播放!
FLV: 也是一种流媒体的封装格式,但他形成的文件极小、加载速度极快,使得网络观看视频文件成为可能,因此FLV格式成为了当今主流视频格式
RTMP:TCP长链接,每个时刻的数据收到后立刻转发,内容延迟1-3秒
FLV:HTTP长链接,每个时刻的数据收到后立刻转发,内容延迟1-3秒
HLS:HTTP短链接,集合一段时间生成TS文件更新m3u8,内容延迟>10秒
IJKPlayer:是一个基于 ffplay 的轻量级视频播放器易于集成,编译配置可裁剪,方便控制安装包大小,支持硬件加速解码,更加省电。三方ZFPlayer。
Crash
1.对象release了但是没有置为nil,访问该对象时会产生Crash,所以我们应该先release再nil,先nil再release会造成内存泄漏
常见的Appstore审核被拒原因
1.包含敏感内容
2.游客登录不支持内购
3.游客登录不能够访问非基于用户个人信息的内容
4.没有提供测试账号、演示视频等
5.不支持IPV6,HTTPS
Runtime
OC是动态语言,运行时,特性主要是消息传递机制,如果消息(方法)在对象中找不到,就进行转发。
runtime基本是用C语言和汇编语言写的。OC到C语言的过度就是由runtime来实现的。然而我们使用OC进行面向对象开发,而C语言更多的是面向过程开发,这就需要将面向对象的类转变为面向过程的结构体。
[obj eat],编译器转成消息发送objc_msgSend(obj, eat)
执行流程:首先通过obj的isa指针找到它的class,在class的method list找到eat,如果class中没找到eat,继续它的superclass中找,一旦找到eat这个函数,就去执行它的实现IMP,最后转发IMP的return值,但这种实现有个问题,效率低。但一个class 往往只有 20% 的函数会被经常调用,可能占总调用次数的 80% 。每个消息都需要遍历一次objc_method_list 并不合理。如果把经常被调用的函数缓存下来,那可以大大提高函数查询的效率。这也就是objc_class 中另一个重要成员objc_cache 做的事情 - 再找到eat 之后,把eat 的method_name 作为key ,method_imp作为value 给存起来。当再次收到eat 消息的时候,可以直接在cache 里找到,避免去遍历objc_method_list。从前面的源代码可以看到objc_cache是存在objc_class 结构体中的。
Objective-C类是由Class类型来表示的,它实际上是一个指向objc_class结构体的指针。
类对象就是一个结构体struct objc_class,类结构体中保存了父类指针、类名、版本、实例大小、实例变量列表、方法列表、缓存、遵守的协议列表
任何NSObject继承体系下的meta-class都使用NSObject的meta-class作为自己的所属类,而基类的meta-class的isa指针是指向它自己。
IMP:方法实现
消息转发:消息转发的最后三次机会resolveInstanceMethod、frowardingTargetForSelector、methodSignatureForSelector,首先,Objective-C运行时会调用 +resolveInstanceMethod:或者 +resolveClassMethod:,让你有机会提供一个函数实现。如果你添加了函数并返回YES, 那运行时系统就会重新启动一次消息发送的过程。如果目标对象实现了-forwardingTargetForSelector:,Runtime 这时就会调用这个方法,给你把这个消息转发给其他对象的机会。如果在上一步还不能处理未知消息,则唯一能做的就是启用完整的消息转发机制了。
首先它会发送-methodSignatureForSelector:消息获得函数的参数和返回值类型。如果-methodSignatureForSelector:返回nil ,Runtime则会发出 -doesNotRecognizeSelector: 消息,程序这时也就挂掉了。如果返回了一个函数签名,Runtime就会创建一个NSInvocation 对象并发送 -forwardInvocation:消息给目标对象。
Runtime的应用:关联对象给分类增加属性、黑魔法方法添加和替换、消息转发(热更新解决BUG)、实现NSCoding自动归档和解档、字典模型的转换
多个Category实现同一个方法的调用顺序
最高优先级为父类,分类的优先级为编译的顺序,从上到下,如何知道编译顺序,很简单,点击工程,Target然后Build Phases下的Compile Sources看其内部文件顺序即可。最后仅仅只会调用分类最后编译的。原因就是根据runtime的消息传递机制中的核心函数void objc_msgSend(id self,SEL cmd,...)来发送消息,先从当前类中查找调用的方法,若没有找到则继续从其父类中一层层往上找,那么对于category重写同一个方法,则在消息传递的过程中,会最先找到category中的方法并执行该方法。对于多个分类调用同一个方法,Xcode在运行时是根据buildPhases->Compile Sources里面的从上至下顺序编译的,编译时通过压栈的方式将多个分类压栈,根据后进先出的原则,后编译的会被先调用,(插入顶部添加,即[methodLists insertObject:category_method atIndex:0]; 所以objc_msgSend遍历方法列表查找SEL 对应的IMP时,会先找到分类重写的那个,调用执行)当objc_msgSend找到方法并调用之后,就不再继续传递消息,所以形成所谓的覆盖。
load方法调用是在main函数之前,主类和分类都会都会加载load方法。普通方法则只调用最后编译的。普通方法是在main函数之后调用。
分类中的方法名和主类方法名一样会报警告, 不会报错。声明和实现可以写在不同的分类中, 依然能找到实现。当第一次用到类的时候, 如果重写了+ initialize方法,会去调用。当调用子类的+ initialize方法时候, 先调用父类的,如果父类有分类, 那么分类的+ initialize会覆盖掉父类的, 和普通方法差不多。父类的+ initialize不一定会调用, 因为有可能父类的分类重写了它。
常见第三方框架的实现原理
1.SDWebImage:当我门需要获取网络图片的时候,获得URL后我们SDWebImage实现的并不是直接去请求网路,而是检查图片缓存中有没有和URl相关的图片,如果有则直接返回image,如果没有则进行下一步。当图片缓存中没有图片时,SDWebImage依旧不会直从网络上获取,而是检查沙盒中是否存在图片,如果存在,则把沙盒中对应的图片存进image缓存中,然后按着第一步的判断进行。如果沙盒中也不存在,则显示占位图,然后根据图片的下载队列缓存判断是否正在下载,如果下载则等待,避免二次下载。如果不存则创建下载队列,下载完毕后将下载操作从队列中清除,并且将image存入图片缓存中。刷新UI(当然根据实际情况操作)将image存入沙盒缓存。
2.MJExtesion:MJExtension是给NSObject增加了category,就是使用runtime获取该类和其所有父类的所有的属性名,并根据属性名在数据字典中获取对应的值,然后通过setValue:forKey设置属性的值.
for循环和for in
只会输出123然后奔溃
NSMutableArray *array = [NSMutableArray arrayWithArray:@[@"1", @"2", @"3", @"4"]];
for (NSString *str in array) {
if ([str isEqualToString:@"3"]) {
[array removeObject:str];
}
NSLog(@"-----------------%@", str);
}
str输出123,arr[i]输出124
NSMutableArray *array = [NSMutableArray arrayWithArray:@[@"1", @"2", @"3", @"4"]];
for (int i = 0; i < array.count; i++) {
NSString *str = array[i];
if ([str isEqualToString:@"3"]) {
[array removeObject:str];
}
NSLog(@"---------%@", array[i]);
NSLog(@"-----------------%@", str);
}
HTTP和HTTPS、TCP和UDP、Get和Post
Http:超文本输出协议,基于请求与响应,无状态的,应用层的协议,常基于TCP/IP协议传输数据。80端口。HTTP不需要ca申请证书。明文传输。
Https:以安全为目标的 HTTP 通道,在HTTP的基础上通过传输加密和身份认证保证了传输过程的安全性 。HTTPS 在HTTP 的基础下加入SSL层,HTTPS 的安全基础是 SSL,因此加密的详细内容就需要 SSL。443端口。SSL就是安全套接层。HTTPS需要ca申请证书。加密传输。
TCP:面向连接的,需要建立和断开连接,是流协议,数据大小无限制,可靠协议,会处理丢包重发以及乱序等情况。
UDP:面向无连接的,不需要建立和断开连接,是数据包协议,数据大小有限制,不可靠协议,不会处理丢包以及乱序。
Get:不安全,因为传输过程中数据被放在请求的URL中,传输的数据量小因为URL长度有限制。对服务器来说安全的(?拼接数据)
Post:较为安全,所有数据对用户来说不可见,可以传输大量数据,对服务器来说不安全的(将数据放到数据体中)
热启动和冷启动
热启动:就是按下home键的时候,app还存在一段时间,这时点击app马上就能恢复到原状态,这种启动我们称为热启动。
冷启动:app被kill掉之后,重新打开启动过程为冷启动。
即时通讯
前言:
首先我们要去选择:传输协议选择TCP还是UPD。聊天协议基于Scoket或者WebScoket或者其他似有协议还是MQTT还是XMPP。自己基于OS底层Socket进行封装还是三方框架基础上封装。传输数据格式是JSON还是XML还是谷歌推出的ProtocolBuffer。细节方面要考虑如何保持TCP长链接、心跳机制、Qos、重连机制等还有安全考虑。
时间复杂度 和 空间复杂度
它们是用来评价算法效率高低的两个标准
1.时间复杂度:执行算法需要消耗的时间长短。越快越好。一般用大“O”来表示时间复杂度T(n) = O(f(n)),n是影响复杂度变化的因子,f(n)是复杂度具体算法。大O符号表示法并不是用于来真实代表算法的执行时间的,它是用来表示代码执行时间的增长变化趋势的。
常数阶:O(1)
线性阶:O(n)
对数阶:O(logN)
线性对数阶:O(nlogN)
平方阶:O(n²)
立方阶:O(n³)
K次方阶:O(n^k)
指数阶:(2^n)
事例1:常数阶O(1)
int a = 1;
int a = 2;
int c = 3;
时间复杂度为O(1)(并没有随着某个变量的增长而增长,那么无论这类代码有多长,即使有几万几十万行,都可以用O(1)来表示它的时间复杂度。)
事例2:线性阶O(n)
for (i = 1; i <= n; i++) {
j = i;
j++;
}
时间复杂度为O(n),大O符号表示法并不是用于来真实代表算法的执行时间的,它是用来表示代码执行时间的增长变化趋势的。
事例3:对数阶O(logN)
int i = 1;
while (i < n) {
i = i * 2;
}
时间复杂度为:O(logn),这个是对阶数,假如n=10,则循环10除以2次,就是n除以2次,应该是O(log2n),这里的底数对于研究程序运行效率不重要,写代码时要考虑的是数据规模n对程序运行效率的影响,常数部分则忽略。
事例4:线性对数阶O(nlogN)
for (m = 1; m < n; m++) {
i = 1;
while (i < n) {
i = i * 2;
}
}
时间复杂度为O(nlogN)。线性对数阶O(nlogN) 其实非常容易理解,将时间复杂度为O(logn)的代码循环N遍的话,那么它的时间复杂度就是 n * O(logN),也就是了O(nlogN)。
事例5:平方阶O(n²)
for(x = 1; i <= n; x++){
for(i = 1; i <= n; i++) {
j = i;
j++;
}
}
时间复杂度为O(n²) ;
2.空间复杂度:执行算法需要消耗的存储空间。越少越好。
O(1):算法执行所需要的临时空间不随着某个变量n的大小而变化,即此算法空间复杂度为一个常量,代码中的 i、j、m 所分配的空间都不随着处理数据量变化,因此它的空间复杂度 S(n) = O(1)。
int i = 1;
int j = 2;
++i;
j++;
int m = i + j;
O(n):这段代码中,第一行new了一个数组出来,这个数据占用的大小为n,后面虽然有循环,但没有再分配新的空间,因此,这段代码的空间复杂度主要看第一行即可,即 S(n) = O(n)。
int[] m = new int[n]
for(i = 1; i <= n; ++i) {
j = i;
j++;
}
泛型
定义:泛型可以让你使用定义的类型来编写灵活的、可重用的函数和类型,可以避免重复,以清晰,抽象的方式表达其意图。用人话来说(😔),泛型给予我们更抽象的封装函数或类的能力,不严谨的来讲,一门语言越抽象使用越方便。Swift中的Array和Dictionary都是基于泛型编写的集合类型
异步并发多任务 任务执行结束统一处理
信号量
使用信号量来控制一下新开辟的分线程数,同时使用 dispatch_group_t 做并发任务结束后统一操作的处理。
- (void)test22 {
dispatch_queue_t scheduleQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_group_t scheduleGroup = dispatch_group_create();
dispatch_semaphore_t semaphore = dispatch_semaphore_create(4);//初始并发量为4
NSLog(@"----------------最前面");
for (int i = 0; i < 7; i++)
{
dispatch_group_async(scheduleGroup, scheduleQueue, ^{
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); //占用一个并发数
dispatch_group_enter(scheduleGroup); //进入组
NSLog(@"----------前面------%i", i);
[BYNetPort requestFileSettingsSuccess:^(id responseObject) {
NSLog(@"--------succeesCount--: %i",i);
dispatch_semaphore_signal(semaphore); //解除占用的并发数
dispatch_group_leave(scheduleGroup); //离开组
} failure:^(NSError *error) {
NSLog(@"--------succeesCount--: %i",i);
dispatch_semaphore_signal(semaphore);
dispatch_group_leave(scheduleGroup);
}];
});
}
dispatch_group_wait(scheduleGroup, DISPATCH_TIME_FOREVER); //等待组中的任务执行
///组中所有任务完成后收到的回调
dispatch_group_notify(scheduleGroup, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"---------------全部请求完成");
});
}
队列组实现
{
// 创建队列组
dispatch_group_t group = dispatch_group_create();
// 创建并发队列
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
// 开子线程,任务1
dispatch_group_async(group, queue, ^{
[NSData dataWithContentsOfURL:[NSURL URLWithString:@"https://img-blog.csdn.net/20180421152137506"]];
NSLog(@"任务1 完成,线程:%@", [NSThread currentThread]);
});
// 开子线程,任务2
dispatch_group_async(group, queue, ^{
[NSData dataWithContentsOfURL:[NSURL URLWithString:@"https://img-blog.csdn.net/20170112145924755?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvaGVyb193cWI=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center"]];
NSLog(@"任务2 完成,线程:%@", [NSThread currentThread]);
});
// 全部完成
dispatch_group_notify(group, queue, ^{
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"全部完成,线程:%@", [NSThread currentThread]);
});
});
}
[https://blog.csdn.net/hero_wqb/article/details/80271206](https://blog.csdn.net/hero_wqb/article/details/80271206)
字符串反转
思路:设立两个begin和end哨兵,然后将这两个哨兵对应的值进行交换,当 begin >=end 的时候,结束。
void reverseStr(char *chr){
char *begin = chr;
char *end = chr + strlen(chr) - 1;
while (begin < end) {
char temp = *begin;
*(begin ++) = *end;
*(end --) = temp;
}
}
OC代码:
- (NSString *)reversalString:(NSString *)originString{
NSString *resultStr = @"";
for (NSInteger i = originString.length -1; i >= 0; i--) {
NSString *indexStr = [originString substringWithRange:NSMakeRange(i, 1)];
resultStr = [resultStr stringByAppendingString:indexStr];
}
return resultStr;
}
链表
数组:需要一块连续的内存空间来存储, 对内存要求比较高。简单易用, 使用连续的内存空间,可以借助 CPU 的缓存机制, 预读数组中的数据, 访问效率更高。数组的确定是大小固定, 一经声明就要占用整块连续的内存空间。 如果声明数组过大, 导致系统没有足够的连续内存空间分配给它, 导致“内存不足”. 声明数组过小, 可能出现不够用的情况, 只能再申请更大的内存空间, 把原始数组拷贝过去, 非常耗时。
链表:通过指针, 将一组零散的内存块串联起来使用。链表在内存中不连续且无序, 对 CPU 缓存不友好。链表本身没有限制, 天然支持动态扩容。链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。而单向链表就是在每一个结点中除了保存的数据之外只有一个指向下一个节点的指针。双向链表的每一个节点除了保存的数据之外,即有指向下一个节点的指针还有指向上一个节点的指针。
链表有:单链表、双向链表、循环链表、双向循环链表
如果代码对内存的使用非常苛刻, 数组更合适, 链表中的每个节点都需要消耗额外的存储空间存储下一个节点的指针。 对链表进行频繁的插入、删除操作还会导致内存频繁的申请和释放, 容易造成内存碎片。
单链表反转
1->2->3->4->NULL 经过反转之后变成 4->3->2->1->NULL
思路:头插法,需要我们定义一个新的头结点作为新的链表,然后利用头插法,将原来的链表的每一个节点取出来,然后去新的链表里面做头插法,这样就可以反转了,这里需要一个新的头结点,和遍历原来链表的一个P指针。
/// 构造一个链表
struct Node *constructList(void){
// 当前节点
struct Node *cur = NULL;
// 头结点
struct Node *head = NULL;
for (int i = 1 ; i < 5 ; i ++) {
struct Node *node = malloc(sizeof(struct Node));
node->data = i;
// 如果头结点为空
if (head == NULL) {
// 将当前节点赋值为头结点
head = node;
} else {
// 将当前节点的next指向这个新的节点,形成链表
cur->next = node;
}
// 将当前节点赋向后移动
cur = node;
}
cur->next = NULL;
return head;
}
上面的代码是构造一个链表,构造一个链表的思想就是,创建一个节点,然后如果当前有头结点,就将当前节点的next指向新创建的节点,然后将当前节点向后移动,如果当前没有头结点,就将头结点和当前节点都赋值为新创建的节点.
/// 链表的反转
/// @param head 头结点
struct Node * reverseList(struct Node *head){
// 新链表哨兵指针
struct Node *newHead = NULL; //(旧的头指针是新的尾指针,next需要指向NULL)
// 旧链表哨兵指针
struct Node *p = head; //(先保存head)
// 旧链表遍历完毕
while (p != NULL) {
// 下一个节点(先保留下一个step要处理的指针)
struct Node *temp = p->next;
// 将这个节点头插到新的链表里面(然后p q交替工作进行反向)
p->next = newHead;
// 更改新链表的头位置(p指针移动:也就是将地址重新赋值)
newHead = p;
// 旧链表的头向后移动(q指针移动)
p = temp;
}
return newHead; // 最后q必然指向NULL,所以返回了p作为新的头指针
}
上面的代码是反转一个链表,反转链表的思想就是新建一个链表然后利用头插法,先从原来的链表里面按照顺序一个个取,取出来之后到新的链表里面进行头插,就形成了头插。
常见算法题
冒泡排序
相邻元素两两比较,比较完一趟,最(大或小)值出现在末尾
从左往右依次对比, 大的往后换位置, 循环5-1 = 4次,排序前 2,1,4,5,3 => 排序后 1,2,3,4,5
题目:排序2,1,4,5,3
答1:
for (int i = 0; i < arr.count - 1; i++) { //趟数
for (int j = 0; j < arr.count - i - 1; j++) { //比较次数
if (arr[j] > arr[j+1]) { //如果左边 > 右边 就交换位置
NSNumber *temp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = temp;
}
}
}
答2:
int endTag = 0; // 加一个标识
for (int i = 0; i < arr.count - 1; i++) { //趟数
for (int j = 0; j < arr.count - i - 1; j++) { //比较次数
if (arr[j] > arr[j+1]) { //如果左边 > 右边 就交换位置
NSNumber *temp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = temp;
endTag = i; // 发生了交换,更新tag值
}
}
if (endTag < i) { // 如果没有发生交换,说明已经排好序了, 直接退出循环
break;
}
}
选择排序
最值出现在起始端(每次轮询右移一位)
从左往右依次两两对比, 小的值 跟第1位换位置, 循环5-1 = 4次,排序前 2,1,4,5,3 => 排序后 1,2,3,4,5
题目:排序2,1,4,5,3
答:
for(int i = 0; i < arr.count - 2; i++) { //趟数
NSNumber *temp = arr[i];//参考的对象
for(int j = i + 1; j < arr.count - 1; j++) { //比较次数
//比参考值小 就交换位置
if(arr[j] < temp){
arr[i] = arr[j];
arr[j] = temp;
}
}
}
二分查找(折半查找)
从已排序的数中找到对应的值(即已知最大值,最小值, 找其中的值)
题目:排序2,1,4,5,3
答:
- (int)midFind:(NSMutableArray *)arr num:(int)num {
int min = 0;//最小值位置
int max = (int)arr.count - 1;//最大值位置
while (min <= max) {
int mid = (min + max)/2;//中间位置
if ([arr[mid] intValue] < num) {//猜小了
min = mid + 1;//mid值被排除了,所以最小值是mid + 1
} else if ([arr[mid] intValue] > num) {//猜大了
max = mid - 1;//mid值被排除了,所以最大值是mid - 1
} else {
return mid;
}
//如果最小值=最大值也找不到的话,说明没有这个对象
if (min == max && min != num) {
return -1;
}
}
return -1;
}
递归:
从0数到100
//从0数到100
[self recursionSort:0];
//递归
- (void)recursionSort:(int)num {
if (num < 100) {
num += 1;
[self recursionSort:num];
}
}
快速排序
第一步: 随机取一个值作为参考值
第二步: 小于参考值的数都放左侧, 大于放右侧
第三步: 递归对左右两侧分别执行上述两步(排序数<2时退出)
[https://www.jianshu.com/p/16fe5dd02df0](https://www.jianshu.com/p/16fe5dd02df0)
一些概念
1.闪屏:一般指电脑显示器上的显示问题,电脑在运行过程中,屏幕画面出现闪烁或不规则闪动,有时会出现横条线和竖条线。闪屏和雪花屏主要是显卡的问题造成的,雪花屏类似电视的雪花屏,闪屏就像显卡驱动有问题那样,画面完整,但眼睛看着极为不舒服
2.闪屏图:闪屏图又称启动图,泛指APP启动的画面。有些人也将APP引导图说成闪屏图。
网友评论