1.UITableView的数据条数太多时会消耗内存,可以给UITableViewCell、UICollectionViewCell、UITableViewHeaderFooterView设置正确的复用ID,充分复用。
2.有透明度的View(alpha值在0到1之间),opaque的值应该设置为YES,可以优化渲染系统、提高性能。(当alpha值为0或1时,opaque的值对性能没有影响)
3.避免过于庞大的XIB/StoryBord文件,当加载XIB文件的时候,所有的内容都会被加到内存里,如果有一个不会立刻用到的View,就是在浪费内存资源。
4.不要让主线程承担过多的任务,否则会阻塞主线程,使app失去反应。
5.加载本地图片时,应该保证UIImageView的大小和图片的大小相同,因为缩放图片是很消耗资源的,特别是在UIImageView嵌套在UIScrollView中的情况下。如果是从网络加载的图片,可以等图片加载完成以后,开启一个子线程缩放图片,然后再放到UIImageView中。
6.在合适的场景下选择合适的数据类型,对于数组:使用索引查询很快,使用值查询很慢,插入删除很慢,对于字典:用键来查找很快,对于集合:用值来查找很快,插入删除很快。
7.网络下载文件时压缩(目前AFNetworking已经默认压缩)
8.当UIScrollView嵌套大量UIView时会消耗内存,可以模仿UITableView的重用方式解决,当网络请求的时候可以使用延迟加载来显示请求错误的页面,因为网络请求错误的页面不会马上用到,如果每次都先加载出来会消耗内存。
9.不大可能改变但经常使用的东西可以使用缓存,比如cell的行高可以缓存起来,这样reloaddata的时候效率会很高。还有一些网络数据,不需要每次都请求的,应该缓存起来,可以写入数据库,也可以写入plist文件。
10.在appDelegate和UIViewController中都有处理内存警告的方法,注册并接受内存警告的通知,一旦收到通知就移除缓存,释放不需要的内存空间。
11.一些对象的初始化很慢,比如NSDateFormatter和NSCalendar,但你又必须要使用它,这时可以重用它们,有两种方式,第一种是添加属性到你的类,第二种是创建静态变量(类似于单例)
12.服务器端和客户端使用相同的数据结构,避免反复处理数据,UIWebView中尽可能少的使用框架,用原声js最好,因为UIView的加载比较慢。
13.在循环创建变量处理数据的时候,使用自动释放池可以及时的释放内存。
14.加载本地图片的时候,如果只使用一次使用imageWithContentOfFile方法,因为imageNamed方法会缓存图片,消耗内存。
在实际开发中可能会有一些耗时的操作,这时可以开辟一个子线程把耗时的操作放到子线程中,当耗时操作执行完毕以后再回到主线程刷新UI。必须要在主线程刷新UI,因为多线程是不安全的,如果在子线程中刷新UI可能会导致未知错误。
回到主线程的方法是performSelectorOnMainTread
延时执行的代码:performSelector:onThread:withObject:waitUntillDone:
使用GCD回到主线程的方法:dispatch_get_main_queue()
使用GCD开启线程:dispatch_async([əˈsɪŋk] )
二者的区别:dispatch_async()不受运行循环模式的影响
GCD中有两个核心概念,队列和任务。队列存放任务,任务的取出遵循FIFO原则。队列其实就是线程池,在OC中以dispatch_queue_t表示,队列分串行队列和并发队列。任务其实就是线程执行的代码,在OC中以Block表示。在队列中执行任务有两种方式:同步执行和异步执行。
串行队列:任务一个一个执行。
并发队列:同一时间有多个任务被执行。
区别:会不会有任务放在别的线程(因为并发队列是取出一个任务放到别的线程,再取出一个任务放到另一个线程,由于动作很快,可以忽略不计,所以看起来所有任务都是一起执行的)
同步执行:不会开启新的线程,任务按顺序执行。
异步执行:会开启新的线程,任务可以并发执行。
区别:会不会开启新的线程。
组合:
同步串行队列:one by one
异步串行队列:one by one (因为前一个任务不执行完毕,队列不会调度)
同步并行队列:one by one (因为同步执行不会开启新的线程)
异步并发队列:可以实现任务的并发,经常用到
主队列:主队列是串行队列,只有一个线程,那就是主线程,添加到主队列中的任务会在主线执行。通过dispatch_get_main_queue获取主队列。
全局队列:全局队列是并发队列。可以通过dispatch_get_global_queue获取不同级别的全局队列。
IP协议(网络层协议)
TCP:传输控制协议,主要解决数据如何在网络中传输,面向连接,可靠。(传输层协议)
UDP:用户数据报协议,面向数据报,不可靠。
HTTP:主要解决如何包装数据。(应用层协议)
Socket:是对TCP/IP协议的封装,Socket本身并不是协议,而是一个调用接口(API),通过Socket,我们才能使用TCP/IP协议。(传输层协议)
application:willFinishLaunchingWithOptions: 程序启动
application:didFinishLaunchingWithOptions: 入口,只执行一次,启动完成准备开始运行
applicationWillResignActive: 切换到非活动状态,如按下home键、切换程序
applicationDidBecomeActive: 切换到激活状态
applicationDidEnterBackground: 应用程序进入后台
applicationWillEnterForeground: 应用程序将要被激活
applicationWillTerminate: 应用程序将要退出
App的启动过程:
打开程序——执行main函数——UIAPPlicationMain函数——初始化UIAPPlicationMain函数(设置代理,开启runloop)——监听系统事件,通知AppDelegate——程序结束
总结:面试官问的是应用程序的生命周期,而我答的是Viewcontroller的生命周期,面试官主要想听到的关键词应该是:main函数、UIApplicationMain、AppDelegate、runloop、监听
另外总结一下关于runloop的知识点:
runloop:运行循环,在程序运行过程中循环做一些事
runloop的作用:保持程序持续运行、处理App中的各种事件、提高资源利用率
runloop在实际项目中的应用:控制线程的生命周期、解决NSTimer在滑动时停止工作的问题、监控应用的卡顿、性能优化
动画有两种基本类型:隐式动画(一直存在,需要手动关闭)和显式动画(不存在,需要手动创建)
UIView的动画:
UIViewAnimationOptionCurveEaseInOut //时间曲线函数,由慢到快
UIViewAnimationOptionCurveEaseIn //时间曲线函数,由慢到特别快
UIViewAnimationOptionCurveEaseOut //时间曲线函数,由快到慢
UIViewAnimationOptionTransitionFlipFromLeft //转场从左翻转
UIViewAnimationOptionTransitionFlipFromRight //转场从右翻转
UIViewAnimationOptionTransitionCurlUp //上卷转场
UIViewAnimationOptionTransitionCurlDown //下卷转场
用法:animateWithDuration、transitionWithView
CAAnimation动画分类:
1.基础动画(如物品放入购物车进行移动)( CABasicAnimation)
2.关键帧动画,图片帧(如人、动物走动)( CAKeyframAnimation)
3.转场动画(一个到另一个场景,如翻页)( CATransition)
4.组合动画( CAAnimationGroup)
可以做动画的值:
1.形状系列:frame bounds
2.位置系列:center
3.色彩系列:alpha color
4.角度系列:transform(旋转的角度)
数组和链表有以下不同:
(1)存储形式:数组是一块连续的空间,声明时就要确定长度。链表是一块可不连续的动态空间,长度可变,每个节点要保存相邻结点指针;
(2)数据查找:数组的线性查找速度快,查找操作直接使用偏移地址。链表需要按顺序检索结点,效率低;
(3)数据插入或删除:链表可以快速插入和删除结点,而数组则可能需要大量数据移动;
(4)越界问题:链表不存在越界问题,数组有越界问题。
数组便于查询,链表便于插入删除。数组节省空间但是长度固定,链表虽然变长但是占了更多的存储空间。
1 客户端打包请求。
其中包括URL、端口、账号和密码等。使用账号和密码登陆应该用的是POST方式,所以相关的用户信息会被加载到body中。这个请求应该包含3个方面:网络地址、协议和资源路径。注意:这里用的是HTTPS,即HTTP+SSL/TLS,在HTTP上又加了一层处理加密信息的模块(相当于加了一个锁)。这个过程相当于客户端请求钥匙。
2 服务器端接受请求。
一般客户端的请求会先被发送到DNS服务器中。DNS服务器负责将网络地址解析成IP地址,这个IP地址对应网上的一台计算机。这其中可能发生Hosts Hijack和ISP failure的问题。过了DNS这一关,信息就到服务器端,此时客户端和服务端的端口之间会建立一个socket连接。socket一般都是以file descriptor的方式解析请求的。这个过程相当于服务器端分析是否要想客户端发送钥匙模板。
3 服务器端返回数字证书。
服务器端会有一套数字证书(相当于一个钥匙模板),这个证书会先被发送个客户端。这个过程相当于服务端向可独断发送钥匙模板。
4 客户端生成加密信息。
根据收到的数字证书(钥匙模板),客户端就会生成钥匙,并把内容锁起来,此时信息已经被加密。这个过程相当于客户端生成钥匙并锁上请求。
5 客户端方发送加密信息。
服务器端会收到由自己发送的数字证书加密的信息。这个时候生成的钥匙也一并被发送到服务端。这个过程相当于客户端发送请求。
6 服务端解锁加密信息。
服务端收到加密信息后,会根据得到的钥匙进行解密,并把要返回的数据进行对称加密。这个过程相当于服务器端解锁请求,生成、加锁回应信息。
7 服务器端向客户端返回信息。
客户端会收到相应的加密信息。这个过程相当于服务器端向客户端发送回应信息。
8 客户端解锁返回信息。
客户端会用刚刚生成的钥匙进行解密,将内容显示在浏览器上。
一。load和initialize的共同点
1.如果父类和子类都被调用,父类的调用一定在子类之前
+load方法要点
当类被引用进项目的时候就会执行load函数(在main函数开始执行之前),与这个类是否被用到无关,每个类的load函数只会自动调用一次.由于load函数是系统自动加载的,因此不需要再调用[super load],否则父类的load函数会多次执行。
- 1.当父类和子类都实现load函数时,父类的load方法执行顺序要优先于子类
- 2.当一个类未实现load方法时,不会调用父类load方法
- 3.类中的load方法执行顺序要优先于类别(Category)
- 4.当有多个类别(Category)都实现了load方法,这几个load方法都会执行,但执行顺序不确定(其执行顺序与类别在Compile Sources中出现的顺序一致)
- 5.当然当有多个不同的类的时候,每个类load 执行顺序与其在Compile Sources出现的顺序一致
注意:
load调用时机比较早,当load调用时,其他类可能还没加载完成,运行环境不安全.
load方法是线程安全的,它使用了锁,我们应该避免线程阻塞在load方法.
+initialize方法要点
initialize在类或者其子类的第一个方法被调用前调用。即使类文件被引用进项目,但是没有使用,initialize不会被调用。由于是系统自动调用,也不需要显式的调用父类的initialize,否则父类的initialize会被多次执行。假如这个类放到代码中,而这段代码并没有被执行,这个函数是不会被执行的。
- 1.父类的initialize方法会比子类先执行
- 2.当子类不实现initialize方法,会把父类的实现继承过来调用一遍。在此之前,父类的方法会被优先调用一次
- 3.当有多个Category都实现了initialize方法,会覆盖类中的方法,只执行一个(会执行Compile Sources 列表中最后一个Category 的initialize方法)
所谓死锁: 是指两个或两个以上的进程(线程)在执行过程中,因争夺资源(如数据源,内存等,变量不是资源)而造成的一种互相等待的现象,若无外部处理作用,它们都将无限等待下去。
死锁形成的原因:
- 系统资源不足
- 进程(线程)推进的顺序不恰当;
- 资源分配不当
死锁形成的条件:
- 互斥条件:所谓互斥就是进程在某一时间内独占资源。
- 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
- 不剥夺条件:进程已获得资源,在末使用完之前,不能强行剥夺。
- 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
在GCD中,主要的死锁就是当前串行队列里面同步执行当前串行队列。解决的方法就是将同步的串行队列放到另外一个线程执行。
(1)任务派发
(2)队列种类
(3)GCD队列种类
1、首先去该类的方法 cache中查找,如果找到了就返回它;
2、如果没有找到,就去该类的方法列表中查找。如果在该类的方法列表中找到了,则将 IMP返回,并将它加入cache中缓存起来。根据最近使用原则,这个方法再次调用的可能性很大,缓存起来可以节省下次调用再次查找的开销;
3、如果在该类的方法列表中没找到对应的 IMP,在通过该类结构中的 super_class指针在其父类结构的方法列表中去查找,直到在某个父类的方法列表中找到对应的IMP,返回它,并加入cache中;
4、如果在自身以及所有父类的方法列表中都没有找到对应的 IMP,则看是不是可以进行动态方法决议
5、如果动态方法决议没能解决问题,进入下面要讲的消息转发流程。
该消息函数做了动态绑定所需要的一切工作:
1,它首先找到 SEL 对应的方法实现 IMP。因为不同的类对同一方法可能会有不同的实现,所以找到的方法实现依赖于消息接收者的类型。
2, 然后将消息接收者对象(指向消息接收者对象的指针)以及方法中指定的参数传递给方法实现 IMP。
3, 最后,将方法实现的返回值作为该函数的返回值返回。
编译器会自动插入调用该消息函数objc_msgSend的代码,我们无须在代码中显示调用该消息函数。当objc_msgSend找到方法对应的实现时,它将直接调用该方法实现,并将消息中所有的参数都传递给方法实现,同时,它还将传递两个隐藏的参数:消息的接收者以及方法名称 SEL。这些参数帮助方法实现获得了消息表达式的信息。它们被认为是”隐藏“的是因为它们并没有在定义方法的源代码中声明,而是在代码编译时是插入方法的实现中的。
尽管这些参数没有被显示声明,但在源代码中仍然可以引用它们(就象可以引用消息接收者对象的实例变量一样)。在方法中可以通过self来引用消息接收者对象,通过选标_cmd来引用方法本身。在下面的例子中,_cmd 指的是eat方法,self指的收到eat消息的对象。在这两个参数中,self更有用一些。实际上,它是在方法实现中访问消息接收者对象的实例变量的途径。
查找 IMP 的过程:
前面说了,objc_msgSend 会根据方法选标 SEL 在类结构的方法列表中查找方法实现IMP。这里头有一些文章,我们在前面的类结构中也看到有一个叫objc_cache *cache 的成员,这个缓存为提高效率而存在的。每个类都有一个独立的缓存,同时包括继承的方法和在该类中定义的方法。
NSThread+runloop实现常驻线程
NSThread在实际开发中比较常用到的场景就是去实现常驻线程。
1.影响启动性能的因素
main()函数之前耗时的影响因素
- 动态库加载越多,启动越慢。
- ObjC类越多,启动越慢
- C的constructor函数越多,启动越慢
- C++静态对象越多,启动越慢
- ObjC的+load越多,启动越慢
实验证明,在ObjC类的数目一样多的情况下,需要加载的动态库越多,App启动就越慢。同样的,在动态库一样多的情况下,ObjC的类越多,App的启动也越慢。需要加载的动态库从1个上升到10个的时候,用户几乎感知不到任何分别,但从10个上升到100个的时候就会变得十分明显。同理,100个类和1000个类,可能也很难查察觉得出,但1000个类和10000个类的分别就开始明显起来。
同样的,尽量不要写的C函数,也尽量不要用到C++的静态对象;至于ObjC的方法,似乎大家已经习惯不用它了。任何情况下,能用来完成的,就尽量不要用到以上的方法。
main()函数之后耗时的影响因素
- 执行main()函数的耗时
- 执行applicationWillFinishLaunching的耗时
- rootViewController及其childViewController的加载、view及其subviews的加载
applicationWillFinishLaunching的耗时
2.main启动之前性能优化:
(1). 移除不需要用到的动态库
(2). 移除不需要用到的类
(3). 合并功能类似的类和扩展(Category)
(4). 压缩资源图片
(5). 优化applicationWillFinishLaunching
(6). 优化rootViewController加载
(7).尽量不使用内嵌(embedded)的dylib,加载内嵌dylib性能开销较大。
(8).清理项目中冗余的类、category。对于同一个类有多个category的,建议进行合并。
(9).将不必须在+load方法中做的事情延迟到+initialize中。
- 利用DYLD_PRINT_STATISTICS分析main()函数之前的耗时
- 重新梳理架构,减少动态库、ObjC类的数目,减少Category的数目
- 定期扫描不再使用的动态库、类、函数,例如每两个迭代一次
- 用dispatch_once()代替所有的 attribute((constructor)) 函数、C++静态对象初始化、ObjC的+load
- 在设计师可接受的范围内压缩图片的大小,会有意外收获
- 利用锚点分析applicationWillFinishLaunching的耗时
- 将不需要马上在applicationWillFinishLaunching执行的代码延后执行
- rootViewController的加载,适当将某一级的childViewController或subviews延后加载
- 如果你的App可能会被后台拉起并冷启动,可考虑不加载rootViewController
- 不应放过的一些小细节
- 异步操作并不影响指标,但有可能影响交互体验,例如大量网络请求导致数据拥堵
- 有时候一些交互上的优化比技术手段效果更明显,视觉上的快决不是冰冷的数据可以解释的,好好和你们的设计师谈谈动画
3.main()方法调用之后过程的解析:
main()方法调用之后,主要是didFinishLaunchingWithOptions方法中初始化必要的服务,显示首页内容等操作。这时候我们可以做的事情主要有:
1、将一些不影响首页展示的服务放到其他线程中去处理,或者延时处理和懒加载。延时处理可以监听Runloop的状态,当进入kCFRunLoopBeforeWaiting(即将休眠状态)再去处理任务,最大限度的利用CPU等系统资源。
2、使用Xcode的Instruments的Time Profiler工具,分析启动过程中比较耗时的方法和操作,然后,进行具体的优化。
3、重点关注TabBarController和首页的性能,保证尽快的能展示出来。这两个控制器及里边的view尽量用代码进行布局,不使用storyboard和xib,如果在布局上想更进一步的优化,那就连autolayout(Massonry)都不要使用,直接使用frame进行布局。
4、本地缓存。首页的数据离线化,优先展示本地缓存数据,等待网络数据返回之后更新缓存并展示。
一、 分类和类扩展区别
1. 分类实现原理
- Category编译之后的底层结构是struct category_t,里面存储着分类的对象方法、类方法、属性、协议信息
- 在程序运行的时候,runtime会将Category的数据,合并到类信息中(类对象、元类对象中)
2. Category和Class Extension的区别是什么?
- Class Extension在编译的时候,它的数据就已经包含在类信息中
- Category是在运行时,才会将数据合并到类信息中
二、 分类为啥不能添加成员变量
struct _category_t { const char *name; struct _class_t *cls; const struct _method_list_t *instance_methods; // 对象方法列表 const struct _method_list_t *class_methods; // 类方法列表 const struct _protocol_list_t *protocols; // 协议列表 const struct _prop_list_t *properties; // 属性列表 };
1.从结构体可以知道,有,所以分类可以,但是分类只会生成该属性对应的和的,没有去。
2.结构体没有,所以不能声明成员变量。
1. Category的加载处理过程
- 1.通过Runtime加载某个类的所有Category数据
- 2.把所有Category的方法、属性、协议数据,合并到一个大数组中,后面参与编译的Category数据,会在数组的前面
- 3.将合并后的分类数据(方法、属性、协议),插入到类原来数据的前面
三,总结:
1、类别原则上只能添加方法而不能添加属性(能添加属性的原因只是通过runtime解决无setter/getter方法的问题而已,如果调用_成员变量,程序还是会报错)。
2、类扩展不仅可以增加方法,还可以增加实例变量(或者属性),只是该变量默认是@private类型的。(所以作用范围只能在自身类,而不是子类或者其它地方)
3、类扩展中声明的方法没被实现,编译器会报警,但是类别中的方法没被实现编译器是不会有任何警告的,这是因为类扩展是在编译阶段被添加到类中,而分类是在运行时添加到类中。
4、类扩展不能像类别那样拥有独立的实现部分(@implementation部分),和本类共享一个实现。也就是说,类扩展所声明的方法必须依托对应宿主类的实现部分来实现。
他们之间的结构关系如下:
MVVM 的优势
低耦合:View 可以独立于Model变化和修改,一个 viewModel 可以绑定到不同的 View 上
可重用性:可以把一些视图逻辑放在一个 viewModel里面,让很多 view 重用这段视图逻辑
独立开发:开发人员可以专注于业务逻辑和数据的开发 viewModel,设计人员可以专注于页面设计
可测试:通常界面是比较难于测试的,而 MVVM 模式可以针对 viewModel来进行测试
MVVM 的弊端
数据绑定使得Bug 很难被调试。你看到界面异常了,有可能是你 View 的代码有 Bug,也可能是 Model 的代码有问题。数据绑定使得一个位置的 Bug 被快速传递到别的位置,要定位原始出问题的地方就变得不那么容易了。
对于过大的项目,数据绑定和数据转化需要花费更多的内存(成本)。主要成本在于:
数组内容的转化成本较高:数组里面每项都要转化成Item对象,如果Item对象中还有类似数组,就很头疼。
转化之后的数据在大部分情况是不能直接被展示的,为了能够被展示,还需要第二次转化。
只有在API返回的数据高度标准化时,这些对象原型(Item)的可复用程度才高,否则容易出现类型爆炸,提高维护成本。
调试时通过对象原型查看数据内容不如直接通过NSDictionary/NSArray直观。
同一API的数据被不同View展示时,难以控制数据转化的代码,它们有可能会散落在任何需要的地方。
1、nil--- 当一个对象置为nil时,这个对象的内存地址就会被系统收回。置空之后是不能进行retain,copy等跟引用计数有关的任何操作的。
2、Nil--- nil完全等同于Nil,只不过由于编程习惯,人们一般把对象置空用nil,把类置空用Nil。
3、NULL--- 这个是从C语言继承来的,就是一个简单的空指针
4、[NSNull null]
1.KVC
我们可以用setValue:的方法设置私有属性,并利用valueForKey:的方法访问私有属性。假设我们有一个类Person,并且这个类有一个私有属性name。看代码:
Person * ls = [[Person alloc] init];
[ls setValue:@"wo" forKey:@"name"];
2.runtime
我们可以利用runtime获取某个类的所有属性(私有属性、非私有属性),在获取到某个类的属性后就可以对该属性进行访问以及修改了。
isa 指的就是 是个什么,对象的isa指向类,类的isa指向元类(meta class),元类isa指向元类的根类。isa帮助一个对象找到它的方法。isa:是一个Class 类型的指针. 每个实例对象有个isa的指针,他指向对象的类,而Class里也有个isa的指针, 指向meteClass(元类)。元类保存了类方法的列表。当类方法被调用时,先会从本身查找类方法的实现,如果没有,元类会向他父类查找该方法。同时注意的是:元类(meteClass)也是类,它也是对象。元类也有isa指针,它的isa指针最终指向的是一个根元类(root meteClass).根元类的isa指针指向本身,这样形成了一个封闭的内循环。
-
RunLoop的结构组成
- 主要有以下六种状态:
- kCFRunLoopEntry -- 进入runloop循环
- kCFRunLoopBeforeTimers -- 处理定时调用前回调
- kCFRunLoopBeforeSources -- 处理input sources的事件
- kCFRunLoopBeforeWaiting -- runloop睡眠前调用
- kCFRunLoopAfterWaiting -- runloop唤醒后调用
- kCFRunLoopExit -- 退出runloop
在CF中,和RunLoop相关的结构有下面几个类:(RunLoop应用场景)
RunLoop的组成结构如下图:
CFRunLoopRef 与 NSRunLoop之间的转换时toll-free的。关于RunLoop的具体实现代码,我们会在下面提到。
RunLoop提供了如下功能(括号中CF表明了在CF库中对应的数据结构名称):
1.RunLoop(CFRunLoop)使你的线程保持忙碌(有事干时)或休眠状态(没事干时)间切换(由于休眠状态的存在,使你的线程不至于意外退出)。
2.RunLoop提供了处理事件源(source0,source1)机制(CFRunLoopSource)。
3.RunLoop提供了对Timer的支持(CFRunLoopTimer)。
4.RunLoop自身会在多种状态间切换(run,sleep,exit等),在状态切换时,RunLoop会通知所注册的5.Observer(CFRunLoopObserver),使得系统可以在特定的时机执行对应的操作。相关的如AutoreleasePool 的Pop/Push,手势识别等。
RunLoop在run时,会进入如下图所示的do while循环:
1.RunLoop和Thread是一一对应的(key: pthread value:runLoop)
2.Thread默认是没有对应的RunLoop的,仅当主动调用Get方法时,才会创建
3.所有Thread线程对应的RunLoop被存储在全局的__CFRunLoops字典中。同时,主线程在static CFRunLoopRef __main,子线程在TSD中,也存储了线程对应的RunLoop,用于快速查找。
这里有一点要弄清,Thread和RunLoop不是包含关系,而是平等的对应关系。Thread的若干功能,是通过RunLoop实现的。另一点是,RunLoop自己是不会Run的,需要我们手动调用Run方法(Main RunLoop会由系统启动),我们的RunLoop才会跑圈。静止(注意,这里的静止不是休眠的意思)的RunLoop是不会做任何事情的
每次RunLoop开始Run的时候,都必须指定一个Mode,称为RunLoopMode。
如,timer是基于RunLoop实现的,我们在创建timer时,可以指定timer的mode:
NSTimer *timer = [NSTimer timerWithTimeInterval:1 repeats:NO block:^(NSTimer * _Nonnull timer) {
NSLog(@"do timer");
}];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes]; // 指定timer在common modes(default mode + event tracking mode) 下运行
这也就解释了,为什么当我们在滑动scrollview的时候,timer事件不会被回调。因为如果我们将timer添加到默认的主线程 的defaultmode时,当用户滑动scrollview的时候,main RunLoop 会切换到event tracking mode下来接收处理密集的滑动事件,这时候,添加在default mode下的timer是不会被触发的。解决方法就是,我们将timer添加到common modes下,让其在default mode和Event tracking mode下面都可以被调用。
(3)RunLoop Source
苹果文档将RunLoop能够处理的事件分为Input sources和timer事件。下面这张图取自苹果官网,不要注意那些容易让人混淆的细节,只看Thread , Input sources 和 Timer sources三个大方块的关系即可,不要关注里面的内容。
source0 VS source1
相同
1. 均是__CFRunLoopSource类型,这就像一个协议,我们甚至可以自己拓展__CFRunLoopSource,定义自己的source。
2. 均是需要被Signaled后,才能够被处理。
3. 处理时,均是调用__CFRunLoopSource._context.version(0?1).perform,其实这就是调用一个函数指针。
不同
source0需要手动signaled,source1系统会自动signaled
source0需要手动唤醒RunLoop,才能够被处理: CFRunLoopWakeUp(CFRunLoopRef rl)。而source1 会自动唤醒(通过mach port)RunLoop来处理。
Source1 由RunLoop和内核管理,Mach Port驱动。
Source0 则偏向应用层一些,如Cocoa里面的UIEvent处理,会以source0的形式发送给main RunLoop。
(4)Timer
我们经常使用的timer有几种?
NSTimer & PerformSelector:afterDelay:(由RunLoop处理,内部结构为CFRunLoopTimerRef)
GCD Timer(由GCD自己实现,不通过RunLoop)
CADisplayLink(通过向RunLoop投递source1 实现回调)
关于Timer的计时,是通过内核的mach time或GCD time来实现的。在RunLoop中,NSTimer在激活时,会将休眠中的RunLoop通过_timerPort唤醒,(如果是通过GCD实现的NSTimer,则会通过另一个CGD queue专用mach port),之后,RunLoop会调用来回调到timer的fire函数。
项目的代码很多,前两天老大突然跟我说项目中某一个ViewController的dealloc()方法没有被调用,存在内存泄漏问题,需要排查原因,解决内存泄漏问题。由于刚加入项目组不久,对出问题的模块的代码还不太熟悉,所以刚拿到问题时觉得很棘手,再加上作为一个iOS菜鸟,对内存泄漏的排查方法和原因确实基本上不了解。所以,也借着这样的机会,我研究了一下关于iOS开发中内存泄漏的排查方法和原因分析。
首先,补充两个基本概念的解释:
- 内存溢出 (out of memory):是指程序在申请内存时,没有足够的内存空间供其使用,出现out of memory。通俗理解就是内存不够,通常在运行大型软件或游戏时,软件或游戏所需要的内存远远超出了你主机内安装的内存所承受大小,就叫内存溢出。
- 内存泄露( memory leak):是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光。
一、排查方法
静态分析方法(Analyze)和动态分析方法(Instrument的leak)。
Analyze 优点:
1、使用操作容易。
2、能够在编码阶段,开发自行进行代码检查。早期发现代码隐患。
3、直接分析源代码来发现程序中的错误,而不需要实际运行。
4、自动检测Objective-C程序中的BUG,发现内存泄露和其它问题。
5、内存问题发现越早,解决的代价就越小。
主要分析以下四种问题:
1、逻辑错误:访问空指针或未初始化的变量等;
2、内存管理错误:如内存泄漏等;
3、声明错误:从未使用过的变量;
4、Api调用错误:未包含使用的库和框架。
Instruments里面工具很多,常用:
1). Time Profiler: 性能分析
2). Zombies:检查是否访问了僵尸对象,但是这个工具只能从上往下检查,不智能。
3). Allocations:用来检查内存,写算法的那批人也用这个来检查。
4). Leaks:检查内存,看是否有内存泄露。
1.1 静态内存泄漏分析方法
通过xcode打开项目,然后点击product-->Analyze,如下图左侧的图所示,这样就开始对项目进行静态内存泄漏分析,分析结果如下图右侧的图所示。根据分析结果进行休整之后在进行分析就好了。
静态分析方法能发现大部分的问题,但是只能是静态分析结果,有一些并不准确,还有一些动态分配内存的情形并没有进行分析。所以仅仅使用静态内存泄漏分析得到的结果并不是非常可靠,如果需要,我们需要将对项目进行更为完善的内存泄漏分析和排查。那就需要用到我们下面要介绍的动态内存泄漏分析方法Instruments中的Leaks方法进行排查。
分析内存泄露不能把所有的内存泄露查出来,有的内存泄露是在运行时,用户操作时才产生的。那就需要用到Instruments了。具体操作是通过xcode打开项目,然后点击product-->profile,如下图左侧图所示。
按上面操作,build成功后跳出Instruments工具,如上图右侧图所示。选择Leaks选项,点击右下角的【choose】按钮,这时候项目程序也在模拟器或手机上运行起来了,在手机或模拟器上对程序进行操作,工具显示效果如下:
点击左上角的红色圆点,这时项目开始启动了,由于leaks是动态监测,所以手动进行一系列操作,可检查项目中是否存在内存泄漏问题。如图所示,橙色矩形框中所示绿色为正常,如果出现如右侧红色矩形框中显示红色,则表示出现内存泄漏。
选中Leaks Checks,在Details所在栏中选择CallTree,并且在右下角勾选Invert Call Tree 和Hide System Libraries,会发现显示若干行代码,双击即可跳转到出现内存泄漏的地方,修改即可。
二、内存泄漏的原因分析
在目前主要以ARC进行内存管理的开发模式,导致内存泄漏的根本原因是代码中存在循环引用,从而导致一些内存无法释放,这就会导致dealloc()方法无法被调用。主要原因大概有一下几种类型。
2.1 ViewController中存在NSTimer
如果你的ViewController中有NSTimer,那么你就要注意了,因为当你调用
时的 target:self
- 理由:这时 ,增加了ViewController的,
即强引用,强引用。造成循环引用。 - 解决方案:在恰当时机调用即可。
一个比较隐秘的因素,你去找找与这个类有关的代理,有没有强引用属性?如果你这个VC需要外部传某个Delegate进来,来通过Delegate+protocol的方式传参数给其他对象,那么这个delegate一定不要强引用,尽量assign或者weak,否则你的VC会持续持有这个delegate,直到它自身被释放。
- 理由:如果代理用修饰,ViewController()会强引用,强引用,内部强引用ViewController()。造成内存泄漏。
- 解决方案:代理尽量使用修饰。
@optional
- (void)animationButton:(QiAnimationButton *)button willStartAnimationWithCircleView:(QiCircleAnimationView *)circleView;
- (void)animationButton:(QiAnimationButton *)button didStartAnimationWithCircleView:(QiCircleAnimationView *)circleView;
- (void)animationButton:(QiAnimationButton *)button willStopAnimationWithCircleView:(QiCircleAnimationView *)circleView;
- (void)animationButton:(QiAnimationButton *)button didStopAnimationWithCircleView:(QiCircleAnimationView *)circleView;
- (void)animationButton:(QiAnimationButton *)button didRevisedAnimationWithCircleView:(QiCircleAnimationView *)circleView;
@end
@property (nonatomic, weak) id <QiAnimationButtonDelegate> delegate;
- (void)startAnimation; //!< 开始动画
- (void)stopAnimation; //!< 结束动画
- (void)reverseAnimation; //!< 最后的修改动画
这个可能就是经常容易犯的一个问题了,Block体内使用实例变量也会造成循环引用,使得拥有这个实例的对象不能释放。因为该block本来就是当前viewcontroller的一部分,现在盖子部门又强引用self,导致循环引用无法释放。 例如你这个类叫OneViewController,有个属性是NSString *name; 如果你在block体中使用了self.name,或者_name,那样子的话这个类就没法释放。 要解决这个问题其实很简单,就是在block之前申明当前的self引用为弱引用即可。
- 理由:如果被当前ViewController()持有,这时,如果block内部再持有ViewController(),就会造成循环引用。
- 解决方案:在外部对弱化,再在block内部强化已经弱化的
__weak typeof(self) weakSelf = self;
[self.operationQueue addOperationWithBlock:^{
__strong typeof(weakSelf) strongSelf = weakSelf;
if (completionHandler) {
KTVHCLogDataStorage(@"serial reader async end, %@", request.URLString);
completionHandler([strongSelf serialReaderWithRequest:request]);
}
}];
这个问题也是我的项目中内存泄漏的问题所在。我们有时候需要在子视图或者某个cell中点击跳转等操作,需要在子视图或cell中持有当前的ViewController对象,这样跳转之后的back键才能直接返回该页面,同时也不销毁当前ViewController。此时,你就要注意在子视图或者cell中对当前页面的持有对象不能是强引用,尽量assign或者weak,否则会造成循环引用,内存无法释放。
CSDN八大内部排序算法介绍
github上搜集的几大算法原理和实现代码,只有JavaScript、Python、Go、Java的实现代码
github上搜集的几大算法时间复杂度和空间复杂度比较
iOS 开发中常用的排序(冒泡、选择、快速、插入、希尔、归并、基数)算法 几种常用算法OC实现(他的归并排序好像写的有点问题)
1. 冒泡排序算法(Bubble Sort)
相邻元素进行比较,按照升序或者降序,交换两个相邻元素的位置 是一种“稳定排序算法”
1.1 网上文字理论
1.2 算法步骤
- 比较相邻的元素。如果第一个比第二个大,就交换他们两个。
- 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。
- 针对所有的元素重复以上的步骤,除了最后一个。
- 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
1.3 动图演示
bubbleSort.gif
1.4 什么时候最快
当输入的数据已经是正序时。
1.5 什么时候最慢
当输入的数据是反序时。
1.6 冒泡排序代码示例
- (void)bubbleSortWithArray:(NSMutableArray *)array {
for (int i = 0; i < array.count - 1; i++) {
//外层for循环控制循环次数
for (int j = 0; j < array.count - 1 - i; j++) {
//内层for循环控制交换次数
if ([array[j] integerValue] > [array[j + 1] integerValue]) {
[array exchangeObjectAtIndex:j withObjectAtIndex:j + 1];
}
}
}
}
快速排序图文理解,通过哨兵站岗理解快速排序
2.1 网上文字理解
快速排序是由东尼·霍尔所发展的一种排序算法。在平均状况下,排序 n 个项目要 Ο(nlogn) 次比较。在最坏状况下则需要 Ο(n2) 次比较,但这种状况并不常见。事实上,快速排序通常明显比其他 Ο(nlogn) 算法更快,因为它的内部循环(inner loop)可以在大部分的架构上很有效率地被实现出来。
快速排序使用分治法(Divide and conquer)策略来把一个串行(list)分为两个子串行(sub-lists)。
快速排序又是一种分而治之思想在排序算法上的典型应用。本质上来看,快速排序应该算是在冒泡排序基础上的递归分治法。
快速排序的名字起的是简单粗暴,因为一听到这个名字你就知道它存在的意义,就是快,而且效率高!它是处理大数据最快的排序算法之一了。虽然 Worst Case 的时间复杂度达到了 O(n²),但是人家就是优秀,在大多数情况下都比平均时间复杂度为 O(n logn) 的排序算法表现要更好,可是这是为什么呢,我也不知道。好在我的强迫症又犯了,查了 N 多资料终于在《算法艺术与信息学竞赛》上找到了满意的答案: 快速排序的最坏运行情况是 O(n²),比如说顺序数列的快排。但它的平摊期望时间是 O(nlogn),且 O(nlogn) 记号中隐含的常数因子很小,比复杂度稳定等于 O(nlogn) 的归并排序要小很多。所以,对绝大多数顺序性较弱的随机数列而言,快速排序总是优于归并排序。
2.2 算法步骤
- 从数列中挑出一个元素,称为 “基准”(pivot);
- 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;
- 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序;
递归的最底部情形,是数列的大小是零或一,也就是永远都已经被排序好了。虽然一直递归下去,但是这个算法总会退出,因为在每次的迭代(iteration)中,它至少会把一个元素摆到它最后的位置去。
2.3 动图演示
quickSort.gif
2.4 快速排序代码示例
- (void)quickSortArray:(NSMutableArray *)array
leftIndex:(NSInteger)left
rightIndex:(NSInteger)right {
if (left > right) {
return;
}
NSInteger i = left;
NSInteger j = right;
//记录基准数 pivoty
NSInteger key = [array[i] integerValue];
while (i < j) {
//首先从右边j开始查找(从最右边往左找)比基准数(key)小的值<---
while (i < j && key <= [array[j] integerValue]) {
j--;
}
//如果从右边j开始查找的值[array[j] integerValue]比基准数小,则将查找的小值调换到i的位置
if (i < j) {
array[i] = array[j];
}
//从i的右边往右查找到一个比基准数小的值时,就从i开始往后找比基准数大的值 --->
while (i < j && [array[i] integerValue] <= key) {
i++;
}
//如果从i的右边往右查找的值[array[i] integerValue]比基准数大,则将查找的大值调换到j的位置
if (i < j) {
array[j] = array[i];
}
}
//将基准数放到正确的位置,----改变的是基准值的位置(数组下标)---
array[i] = @(key);
//递归排序
//将i左边的数重新排序
[self quickSortArray:array leftIndex:left rightIndex:i - 1];
//将i右边的数重新排序
[self quickSortArray:array leftIndex:i + 1 rightIndex:right];
}
它的改进(相比较冒泡算法)在于:先并不急于调换位置,先从A[0]开始逐个检查,看哪个数最小就记下该数所在的位置P,等一躺扫描完毕,再把A[P]和A[0]对调,这时A[0]到A[n]中最小的数据就换到了最前面的位置。是一个“不稳定排序算法”
它是一种简单直观的排序算法,无论什么数据进去都是 O(n²) 的时间复杂度。所以用到它的时候,数据规模越小越好。唯一的好处可能就是不占用额外的内存空间。
选择排序算法一: 直接选择排序(straight select sort)
3.1 算法步骤
- 首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置
- 再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。
- 重复第二步,直到所有元素均排序完毕。
3.2 动图演示
selectionSort.gif
3.3 直接选择排序示例代码
- (void)selectSortWithArray:(NSMutableArray *)array {
for (int i = 0; i < array.count; i++) {
for (int j = i + 1; j < array.count; j++) {
if (array[i] > array[j]) {
[array exchangeObjectAtIndex:i withObjectAtIndex:j];
}
}
}
}
选择排序算法二:
堆排序(heap sort 涉及到完全二叉树的概念)
网上文字理解
堆排序是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。堆排序可以说是一种利用堆的概念来排序的选择排序。分为两种方法:
- 大顶堆:每个节点的值都大于或等于其子节点的值,在堆排序算法中用于升序排列;
- 小顶堆:每个节点的值都小于或等于其子节点的值,在堆排序算法中用于降序排列;
堆排序的平均时间复杂度为 Ο(nlogn)。
算法步骤
- 创建一个堆 H[0……n-1];
- 把堆首(最大值)和堆尾互换;
- 把堆的尺寸缩小 1,并调用 shift_down(0),目的是把新的数组顶端数据调整到相应位置;
- 重复步骤 2,直到堆的尺寸为 1。
动图演示
heapSort.gif
堆排序代码示例
- (void)heapSortWithArray:(NSMutableArray *)array {
//循环建立初始堆
for (NSInteger i = array.count * 0.5; i >= 0; i--) {
[self heapAdjustWithArray:array parentIndex:i length:array.count];
}
//进行n-1次循环,完成排序
for (NSInteger j = array.count - 1; j > 0; j--) {
//最后一个元素和第一个元素进行交换
[array exchangeObjectAtIndex:j withObjectAtIndex:0];
//筛选R[0]结点,得到i-1个结点的堆
[self heapAdjustWithArray:array parentIndex:0 length:j];
NSLog(@"第%ld趟:", array.count - j);
[self printHeapSortResult:array begin:0 end:array.count - 1];
}
}
- (void)heapAdjustWithArray:(NSMutableArray *)array
parentIndex:(NSInteger)parentIndex
length:(NSInteger)length {
NSInteger temp = [array[parentIndex] integerValue]; //temp保存当前父结点
NSInteger child = 2 * parentIndex + 1; //先获得左孩子
while (child < length) {
//如果有右孩子结点,并且右孩子结点的值大于左孩子结点,则选取右孩子结点
if (child + 1 < length && [array[child] integerValue] < [array[child + 1] integerValue]) {
child++;
}
//如果父结点的值已经大于孩子结点的值,则直接结束
if (temp >= [array[child] integerValue]) {
break;
}
//把孩子结点的值赋值给父结点
array[parentIndex] = array[child];
//选取孩子结点的左孩子结点,继续向下筛选
parentIndex = child;
child = 2 * child + 1;
}
array[parentIndex] = @(temp);
}
- (void)printHeapSortResult:(NSMutableArray *)array
begin:(NSInteger)begin
end:(NSInteger)end {
for (NSInteger i = 0; i < begin; i++) {
}
for (NSInteger i = begin; i <= end; i++) {
}
//打印堆排序
NSLog(@"堆排序升序结果是--->%@",array);
}
4.1 网上文字理解
插入排序的代码实现虽然没有冒泡排序和选择排序那么简单粗暴,但它的原理应该是最容易理解的了,因为只要打过扑克牌的人都应该能够秒懂。插入排序是一种最简单直观的排序算法,它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。
插入排序和冒泡排序一样,也有一种优化算法,叫做拆半插入。
4.2 算法步骤
- 将第一待排序序列第一个元素看做一个有序序列,把第二个元素到最后一个元素当成是未排序序列。
- 从头到尾依次扫描未排序序列,将扫描到的每个元素插入有序序列的适当位置。(如果待插入的元素与有序序列中的某个元素相等,则将待插入元素插入到相等元素的后面)
4.3 动图演示
insertionSort.gif
4.4 插入排序代码示例
- (void)insertSortWithArray:(NSMutableArray *)array {
NSInteger j;
for (NSInteger i = 1; i < array.count; i++) {
//取出每一个待插入的数据,从array[1]开始查找
NSInteger temp = [array[i] integerValue];
for (j = i - 1; j >= 0 && temp < [array[j] integerValue]; j--) {
//如果之前的数比temp大,就将这个数往后移动一个位置,留出空来让temp插入,和整理扑克牌类似
[array[j + 1] integerValue] = [array[j] integerValue]];
array[j] = [NSNumber numberWithInteger:temp];
}
}
}
5.1 网上文字理解
归并排序(Merge sort)是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。
作为一种典型的分而治之思想的算法应用,归并排序的实现由两种方法:
- 自上而下的递归(所有递归的方法都可以用迭代重写,所以就有了第 2 种方法);
- 自下而上的迭代;
在《数据结构与算法 JavaScript 描述》中,作者给出了自下而上的迭代方法。
和选择排序一样,归并排序的性能不受输入数据的影响,但表现比选择排序好的多,因为始终都是 O(nlogn) 的时间复杂度。代价是需要额外的内存空间。
5.2 算法步骤
- 申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列;
- 设定两个指针,最初位置分别为两个已经排序序列的起始位置;
- 比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置;
- 重复步骤 3 直到某一指针达到序列尾;
- 将另一序列剩下的所有元素直接复制到合并序列尾。
5.3 动图演示
mergeSort.gif
5.4 归并排序代码示例 参考简书作者OC代码
//自顶向下的归并排序
/
递归使用归并排序,对array[left...right]的范围进行排序
@param array 数组
@param left 左边界
@param right 右边界
*/
- (void)mergeSortWithArray:(NSMutableArray *)array
left:(NSInteger)left
right:(NSInteger)right {
//判断递归到底的情况
if (left >= right) {
//这时候只有一个元素或者是不存在的情况
return;
}
//中间索引的位置
NSInteger middle = (right + left) / 2;
//对 left --- middle 区间的元素进行排序操作
[self mergeSortWithArray:array left:left right:middle];
//对 middle + 1 ---- right 区间的元素进行排序操作
[self mergeSortWithArray:array left:middle + 1 right:right];
//两边排序完成后进行归并操作
[self mergeSortWithArray:array left:left middle:middle right:right];
}
/
对 [left middle] 和 [middle + 1 right]这两个区间归并操作
@param array 传入的数组
@param left 左边界
@param middle 中间位置
@param right 右边界
*/
- (void)mergeSortWithArray:(NSMutableArray *)array
left:(NSInteger)left
middle:(NSInteger)middle
right:(NSInteger)right {
//拷贝一个数组出来
NSMutableArray *copyArray = [NSMutableArray arrayWithCapacity:right - left + 1];
for (NSInteger i = left; i <= right; i++) {
//这里要注意有left的偏移量,所以copyArray赋值的时候要减去left
copyArray[i - left] = array[i];
}
NSInteger i = left, j = middle + 1;
//循环从left开始到right区间内给数组重新赋值,注意赋值的时候也是从left开始的,不要习惯写成了从0开始,还有都是闭区间
for (NSInteger k = left; k <= right; k++) {
//当左边界超过中间点时 说明左半部分数组越界了 直接取右边部分的数组的第一个元素即可
if (i > middle) {
//给数组赋值 注意偏移量left 因为这里是从left开始的
array[k] = copyArray[j - left];
//索引++
j++;
} else if (j > right) {//当j大于右边的边界时证明有半部分数组越界了,直接取左半部分的第一个元素即可
array[k] = copyArray[i - left];
//索引++
i++;
} else if (copyArray[i - left] > copyArray[j - left]) {//左右两半部分数组比较
//当右半部分数组的第一个元素要小时 给数组赋值为右半部分的第一个元素
array[k] = copyArray[j - left];
//右半部分索引加1
j++;
} else {//右半部分数组首元素大于左半部分数组首元素
array[k] = copyArray[i - left];
i++;
}
}
}
6.1 网上文字理解
希尔排序,也称递减增量排序算法,是插入排序的一种更高效的改进版本。但希尔排序是非稳定排序算法。
希尔排序是基于插入排序的以下两点性质而提出改进方法的:
- 插入排序在对几乎已经排好序的数据操作时,效率高,即可以达到线性排序的效率;
- 但插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位;
希尔排序的基本思想是:先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录“基本有序”时,再对全体记录进行依次直接插入排序。
6.2 算法步骤
- 选择一个增量序列 t1,t2,……,tk,其中 ti > tj, tk = 1;
- 按增量序列个数 k,对序列进行 k 趟排序;
- 每趟排序,根据对应的增量 ti,将待排序列分割成若干长度为 m 的子序列,分别对各子表进行直接插入排序。仅增量因子为 1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。
6.4 希尔排序代码示例
7. 基数排序(radix sort)
7.1 文字理解
基数排序是一种非比较型整数排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。由于整数也可以表达字符串(比如名字或日期)和特定格式的浮点数,所以基数排序也不是只能使用于整数。
7.2 基数排序 vs 计数排序 vs 桶排序
- 基数排序:根据键值的每位数字来分配桶;
- 计数排序:每个桶只存储单一键值;
- 桶排序:每个桶存储一定范围的数值;
7.3 动图演示
radixSort.gif
7.4 基数排序代码示例
8. 计数排序(counting sort)
8.1 文字理解
计数排序的核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。
8.2 动图演示
countingSort.gif
8.3 计数排序代码示例(无)
9. 桶排序(bucket sort)
9.1 文字理解
桶排序是计数排序的升级版。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。为了使桶排序更加高效,我们需要做到这两点:
- 在额外空间充足的情况下,尽量增大桶的数量
- 使用的映射函数能够将输入的 N 个数据均匀的分配到 K 个桶中
同时,对于桶中元素的排序,选择何种比较排序算法对于性能的影响至关重要。
9.2 什么时候最快
当输入的数据可以均匀的分配到每一个桶中。
9.3 什么时候最慢
当输入的数据被分配到了同一个桶中。
Swift和Objective-C的联系:
Swift和Objective-C共用一套运行时环境,Swift的类型可以桥接到Objective-C,反之亦然。两者可以互相引用混合编程。此外,Objective-C中积累的许多类库在Swift中大部分仍然可以直接使用。
Swift和Objective-C的区别:
- 语法风格:Objective-C的语法风格受到C语言的影响,使用方括号([ ])来调用方法。而Swift则采用了更现代化的语法风格,更加简洁易读,更接近自然语言的表达方式。
- 类型安全:Swift是一门类型安全的语言,鼓励程序员在代码中明确值的类型。而Objective-C则不然,声明了字符串类型仍然可以传递NSNumber类型给它。
- 内存管理:Swift的内存管理更自动化,而Objective-C需要手动管理内存,使用ARC(自动引用计数)来简化这一过程。
- 新特性:Swift支持泛型、元组等新特性,而Objective-C中没有这些特性。
- 学习难度:Swift的入手难度较低,而Objective-C由于语法较为繁琐,对初学者不太友好。
Swift的优势:
- 易读性和易维护性:Swift的语法简洁,文件结构清晰,易于阅读和维护。
- 安全性:Swift是类型安全的语言,可以在编译时检查类型错误。
- 高效性:Swift的运算性能较高,代码更少且效率更高。
Swift的缺点:
- 社区和开源项目:目前使用人数比例较低,社区的开源项目也较少。
- 兼容性问题:对于不支持Swift的第三方类库,需要进行混合编程,利用桥接文件实现
- 更强的类型安全和错误处理机制:Swift是一种静态类型语言,编译器可以在编译时检查类型错误和其他常见的编程错误,从而减少运行时错误,提高应用程序的稳定性和安全性。相比之下,Objective-C是动态类型语言,类型检查主要在运行时进行,容易导致运行时错误。
- 更简洁的语法:Swift的语法更加简洁,易于阅读和编写,这可以提高研发效率。Objective-C的语法相对冗长和复杂。
- 更好的性能:Swift的性能通常比Objective-C更好,尤其是在处理大量数据时。Swift采用了一些现代编程语言的特性,如自动引用计数和结构体,以提高性能。
- 更强的特性支持:Swift具有许多Objective-C所不具备的特性,如结构体、泛型和函数式编程等。这些特性可以帮助开发人员更好地组织和管理代码,提高应用程序的可重用性和可维护性。
- 更好的互操作性:Swift兼容Objective-C,可以在同一个项目中同时使用两种语言编写代码,并且可以无缝调用Objective-C的代码。这意味着可以逐步迁移项目,同时使用两种语言编写的代码。
详细解释这些优势:
- 类型安全和错误处理机制:Swift的静态类型系统要求所有变量的类型在编译时就已经确定,编译器在编译阶段进行类型检查,能够更早地发现类型错误,从而避免运行时错误,提高代码的安全性和可靠性。相比之下,Objective-C由于其动态特性,类型检查主要在运行时进行,容易导致运行时错误。
- 语法简洁性:Swift的语法设计更加简洁和易读,例如不需要行尾的分号,方法和函数的调用使用标准的括号内逗号分隔的参数列表,这使得代码更加干净和有表现力。相比之下,Objective-C的语法相对复杂。
- 性能优势:Swift的性能通常比Objective-C更好,这得益于其静态类型和编译时优化。Swift支持内联函数和高级编译器优化,可以进一步提高应用程序的性能。相比之下,Objective-C作为动态语言,需要在运行时进行类型检查和消息传递,这些操作会导致额外的开销,影响性能。
- 特性支持:Swift支持许多现代编程特性,如结构体、泛型和函数式编程等,这些特性可以帮助开发人员更好地组织和管理代码,提高代码的质量和性能。相比之下,Objective-C在这些方面的支持较弱。
- 互操作性:Swift兼容Objective-C,可以在同一个项目中同时使用两种语言编写代码,并且可以无缝调用Objective-C的代码。这意味着可以逐步迁移项目,同时使用两种语言编写的代码,提高了开发的灵活性和效率。
1.学习曲线:虽然 Swift 的语法相对简单直观,但它提供了许多特性(例如泛型、闭包),这使得完全理解和掌握 Swift 需要一定时间。
2.兼容性问题:Swift 在发布后不断发展,新版本可能会引入不兼容的更改,这可能需要开发者花时间去适配他们的代码。
3.性能问题:虽然 Swift 旨在提供高性能,但在某些特定的情况下,与 Objective-C 或 C++ 编写的代码相比,Swift 程序可能不会有最优化的性能。
4.第三方库支持:虽然 Swift 在移动端开发中取得了广泛的应用,但在服务器端或桌面应用开发中,可用的第三方库可能不如 Objective-C 或 C++ 那么丰富。
5.工具链限制:Swift 的开发和发布受限于苹果的 SDK,这可能限制了其在某些平台的应用。
6.安全性问题:Swift 引入了一些新的安全特性,如自动引用计数(ARC)和过程调用安全(Parameter calls),但这些特性可能导致开发者在处理内存管理时出现困难。
针对这些问题,解决方案可能包括:
通过不断学习和实践来掌握 Swift 的复杂特性。
关注 Swift 的发展,并在必要时对代码进行重构以适应新版本。
对于性能问题,可以使用 Swift 性能优化工具,并考虑使用 C 代码扩展等方法。
寻找或贡献更多的第三方库来补充 Swift 在特定领域的不足。
保持对新的开发工具和环境的关注,如果需要,可以使用其他支持 Swift 的工具链。
在安全性问题上,进行代码审查,使用静态分析工具,并遵循最佳实践以提高代码安全性。
由于这些问题都有相应的解决策略,具体到实践中,开发者可以根据具体需求和上下文来应对。
1.可选项(Optionals): Swift 的可选项用于处理变量可能不存在的情况,而不是像 Objective-C 那样使用指针。
var optionalString: String? = "Hello, Swift!"
2.强制解包(Forced Unwrapping): 可以通过在变量后面加上感叹号(!)来强制解包可选项。
print(optionalString!) // 如果optionalString为nil,这会导致运行时错误
可选绑定(Optional Binding): 可以检查并且绑定可选值,在检查的同时赋值给一个临时常量或变量。
if let string = optionalString {
print(string)
}
3.泛型(Generics): Swift 支持泛型编程,允许定义可以用于多种类型的函数或容器。
func swap<T>(_ a: inout T, _ b: inout T) {
let temporaryA = a
a = b
b = temporaryA
}
4.类型推断(Type Inference): Swift 可以自动推断变量的类型,不需要像 Objective-C 那样显式指定类型。
let inferredType = "Hello, Swift!"
5.结构体(Structs): Swift 的结构体是值类型,可以更高效地创建复杂的数据结构,并且有更多的语法糖。
struct Point {
var x: Int
var y: Int
}
6.扩展(Extensions): Swift 允许你给现有的类型添加新的功能,甚至可以扩展它的原始实现。
extension Int {
func repeatMe() -> Int {
return self * 2
}
}
7.协议(Protocols): Swift 的协议比 Objective-C 的委托更强大,它可以定义方法、属性、下标访问,并且可以有默认实现。
protocol ExampleProtocol {
var property: Int { get set }
func method()
}
8.自动引用计数(Automatic Reference Counting, ARC): Swift 使用 ARC 自动管理内存,避免了许多常见的内存泄漏问题。
错误处理(Error Handling): Swift 提供了强大而优雅的错误处理机制,包括可以抛出和捕获错误的能力。
enum PrinterError: Error {
case outOfPaper
case noToner
case onFire
}
func send(job: Int, toPrinter printerName: String) throws {
if printerName == "Never Has Toner" {
throw PrinterError.noToner
}
// 发送打印任务的代码
}
这些是 Swift 相较 Objective-C 独有的一些语法特性。每一项都可以展开讨论,因为它们各自都有着重要的应用场景和教育意义。
1.类型安全 (Type Safety)
Swift 是类型安全的语言,这意味着它要求你在进行操作之前明确指定数据的类型。Objective-C 不是类型安全的,它可以在运行时通过id类型进行隐式类型转换。
2.可选项 (Optionals)
Swift 引入了可选项的概念,用于处理变量可能不存在的情况。Objective-C 通常使用指针和nil来处理可能为空的对象。
3.属性 (Properties)
Swift 使用属性来自动管理存储行为,而 Objective-C 需要手动管理内存。
4.构造器 (Initializers)
Swift 提供了自定义构造器,并且可以选择是否要指定构造器。Objective-C 需要使用init方法,并且不能有选择地初始化属性。
5.扩展 (Extensions)
Swift 允许你扩展现有类型添加新的功能,而 Objective-C 不支持这一点。
6.协议 (Protocols)
Swift 的协议是一种类型,可以有默认实现和扩展。Objective-C 的协议不是类型,不能有默认实现或扩展。
7.错误处理 (Error Handling)
Swift 引入了错误处理机制,而 Objective-C 需要使用NSError对象作为参数传递错误信息。
8.自动引用计数 (Automatic Reference Counting, ARC)
Swift 使用ARC来自动管理内存,而 Objective-C 需要手动管理内存。
9.内存管理 (Memory Management)
Swift 使用自动引用计数 (ARC) 来管理内存,而 Objective-C 需要开发者手动管理内存(使用引用计数、自动释放池等)。
10.断言和预处理器 (Assertions and Preprocessor Directives)
Swift 使用assert函数和预处理器指令来控制调试信息,而 Objective-C 使用NSAssert宏。
这些是Swift和Objective-C之间主要的不同点。当然,两者都有其优点和适用场景,开发者可以根据项目需求选择合适的语言。
Swift 是一种混合型的编程语言,它既有面向对象编程的特征,也支持函数式编程的概念。它是静态类型的语言,同时支持协程和泛型等高级特性。
面向对象的特征:
类和对象
继承和多态
访问控制(如:private, internal, public)
函数式编程特征:
闭包和高阶函数
map, filter, reduce 等函数式编程范式的函数
内置协程和泛型支持
Swift 混合了面向对象和函数式编程的特性,这使得它在开发现代应用时提供了灵活性和表达力。
在Swift编程语言中,访问级别修饰符指定了代码在不同的模块(例如:源文件、库、框架)内部或者外部的访问权限。以下是Swift语言中的五种访问级别:
open:允许在定义模块外部的任何代码访问,也就是说,这个成员可以被任何继承了定义模块的子类访问。
public:允许在定义模块外部的任何代码访问,但是不允许在定义模块内部的任何子类访问。
internal:允许在定义模块内部的任何代码访问,但是不允许在定义模块外部的任何代码访问。
fileprivate:只允许在定义的那个源文件内部访问,不允许在其他源文件中访问,甚至不允许在同一个源文件中的其他类访问。
private:只允许在定义它的那个作用域内访问,不允许在其他任何地方访问。
以下是一些使用这些关键字的示例代码:
// 使用 internal 关键字
internal var myInternalVar = "Hello, World!"
// 使用 fileprivate 关键字
fileprivate func myFilePrivateFunction() {
print("This is a file-private function.")
}
// 使用 private 关键字
private let myPrivateConstant = "I am private!"
// 使用 public 关键字
public class MyClass {
public var myPublicVar = "Hello, Swift!"
public func myPublicFunction() {
print("This is a public function.")
}
}
// 使用 open 关键字
open class MyOpenClass {
open func myOpenFunction() {
print("This is an open function.")
}
}
在选择使用哪种访问级别时,应考虑以下原则:
如果你想让你的实体(例如类、方法、变量等)对定义模块的外部可用,使用 public。
如果你想让你的实体仅在定义模块内部可用,使用 internal。
如果你想让你的实体仅在一个源文件内部可用,使用 fileprivate。
如果你想让你的实体仅在一个特定的作用域内可用,使用 private。
如果你想让你的类或者类的成员在定义模块的子类中可用,使用 open。
Swift 的内存管理机制与 Objective-C一样为 ARC(Automatic Reference Counting)。它的基本原理是,一个对象在没有任何强引用指向它时,其占用的内存会被回收。反之,只要有任何一个强引用指向该对象,它就会一直存在于内存中。
- strong 代表着强引用,是默认属性。当一个对象被声明为 strong 时,就表示父层级对该对象有一个强引用的指向。此时该对象的引用计数会增加1。
- weak 代表着弱引用。当对象被声明为 weak 时,父层级对此对象没有指向,该对象的引用计数不会增加1。它在对象释放后弱引用也随即消失。继续访问该对象,程序会得到 nil,不亏崩溃
- unowned 与弱引用本质上一样。唯一不同的是,对象在释放后,依然有一个无效的引用指向对象,它不是 Optional 也不指向 nil。如果继续访问该对象,程序就会崩溃。
加分回答:
- weak 和 unowned 的引入是为了解决由 strong 带来的循环引用问题。简单来说,就是当两个对象互相有一个强指向去指向对方,这样导致两个对象在内存中无法释放(详情请参考第3章第3节第8题)。
weak 和 unowned 的使用场景有如下差别:
- 当访问对象时该对象可能已经被释放了,则用 weak。比如 delegate 的修饰。
- 当访问对象确定不可能被释放,则用 unowned。比如 self 的引用。
- 实际上为了安全起见,很多公司规定任何时候都使用 weak 去修饰。
- Swift中若要使用Objective-C代码,可以在ProjectName-Bridging-Header.h里添加Objective-C的头文件名称,Swift文件中即可调用相应的Objective-C代码。一般情况Xcode会在Swift项目中第一次创建Objective-C文件时自动创建ProjectName-Bridging-Header.h文件。
- Objective-C中若要调用Swift代码,可以导入Swift生成的头函数ProjectName-Swift.h来实现。
- Swift文件中若要规定固定的方法或属性暴露给Objective-C使用,可以在方法或属性前加上@objc来声明。如果该类是NSObject子类,那么Swift会在非private的方法或属性前自动加上@objc。
- 在协议和方法前都加上 @objc 关键字,然后再在方法前加上 optional 关键字。该方法实际上是把协议转化为Objective-C的方式然后进行可选定义。示例如下:
- 用扩展(extension)来规定可选方法。Swift中,协议扩展(protocol extension)可以定义部分方法的默认实现,这样这些方法在实际调用中就是可选实现的了。示例如下:
在类的定义中使用final关键字声明类、属性、方法和下标。final声明的类不能被继承,final声明的属性、方法和下标不能被重写。
guard也是基于一个表达式的布尔值去判断一段代码是否该被执行。与if语句不同的是,guard只有在条件不满足的时候才会执行这段代码。
defer的用法是,这条语句并不会马上执行,而是被推入栈中,直到函数结束时才再次被调用。
- public:可以别任何人访问,但是不可以被其他module复写和继承。
- open:可以被任何人访问,可以被继承和复写。
- 数据类型和内存管理
- struct(结构体):是值类型,直接包含数据,赋值时进行深拷贝,即复制内容。每个struct的实例都是独立的,修改一个不会影响其他实例。struct没有引用计数,不会因为循环引用导致内存泄漏。
- class(类):是引用类型,存储的是数据的引用地址。赋值时进行浅拷贝,即复制引用地址。多个变量可以引用同一个实例,修改其中一个会影响所有引用该实例的变量。
- 继承
- struct:不支持继承,不能从一个struct继承另一个struct。
- class:支持继承,可以从一个类继承另一个类,并且可以重写父类的方法和属性。
- 初始化
- struct:所有struct都会有一个编译器自动生成的初始化器,保证所有成员都有初始值。可以在构造函数中直接初始化属性。
- class:需要在构造函数中显式定义初始化器,不能直接在构造函数中初始化属性,需要创建一个带参数的构造函数。
- 性能
- struct:分配在栈上,通常比class更快速,因为它是值类型,没有引用计数的开销。
- class:分配在堆上,有引用计数的开销,可能会因为循环引用导致内存泄漏。
16.1优点
- 安全性: 因为 Struct 是用值类型传递的,它们没有引用计数。
- 内存: 由于他们没有引用数,他们不会因为循环引用导致内存泄漏。
- 速度: 值类型通常来说是以栈的形式分配的,而不是用堆。因此他们比 Class 要快很多!
- 拷贝:Objective-C 里拷贝一个对象,你必须选用正确的拷贝类型(深拷贝、浅拷贝),而值类型的拷贝则非常轻松!
- 线程安全: 值类型是自动线程安全的。无论你从哪个线程去访问你的 Struct ,都非常简单。
16.2 缺点
- Objective-C与swift混合开发:OC调用的swift代码必须继承于NSObject。
- 继承:struct不能相互继承。
- NSUserDefaults:Struct 不能被序列化成 NSData 对象。
- 如何设置实时渲染?
- 异步同步任务的区别?
- 什么是NSError对象? NSError有三部分组成,分别为 Domain是一个字符串,标记一个错误域
- 什么是Enum? 是一种类型,包含了相关的一组数据
- 为什么使用synchronized? 保证在一定时间内,只有一个线程访问它
- strong, weak,copy 有什么不同? :引用计数会增加 :不会增加引用计数 : 意味着我们在创建对象时复制该对象的值
- 什么是ABI? 应用程序二进制接口
- 在Cocoa中有哪些常见的设计模式 创造性:单例(Singleton) 结构性: 适配器(Adapter) 行为:观察者(Observer)
- Realm数据库的好处 a. 开源的DB framework b. 快 c. ios 安卓都可以使用
- Swift 优势是什么? a. 类型安全 b. 闭包 c. 速度快
- 什么是泛型? 泛型可以让我们定义出灵活,且可重用的函数和类型,避免重复代码
- 解释 Swift 中的 lazy? lazy是 Swift 中的一个关键字,他可以延迟属性的初始化时间,知道用到这个属性时,才去加载它
- 解释什么是 defer? 延迟执行,当你离开当前代码块,就会去执行
- KVC 和 KCO 的区别? KVC: 它是一种用间接方式访问类的属性的机制 KVO: 它是一种观察者模式,被观察的对象如果有改变,观察者就会收到通知
- Gurad的好处? 可以使语句变得更简洁,避免嵌套很多层,可以使用break,return提前退.
1. 函数用于对集合中的每个元素应用一个指定的转换闭包,然后返回一个包含转换结果的新集合。
2. 函数用于从集合中选择满足指定条件的元素,并返回一个包含满足条件的元素的新集合。
3. 函数用于将集合中的所有元素组合成单个值,并返回该值。
4. 函数用于对集合中的每个元素应用一个转换闭包,并将结果拼接成一个新的集合。
5. 函数用于对集合中的每个元素应用一个转换闭包,并过滤掉结果中的 nil 值。
6. 函数用于检查序列中的所有元素是否都满足指定条件。
Swift是一种强大而灵活的编程语言,它支持多种数据类型,包括基础类型、枚举、结构体、类等。在Swift中,Any、AnyHashable、AnyObject和AnyClass是四种特殊的类型,它们各自具有不同的特性和用途。
- Any类型:
Any类型是Swift中一个非常特殊的类型,它可以表示任何类型。这意味着你可以将任何东西赋给一个Any类型的变量,包括基本数据类型、枚举、结构体、类等。使用Any类型可以让你在编程中更加灵活,但是也需要注意,由于类型被隐式地转换为了动态类型,所以在运行时可能会出现类型错误。 - AnyHashable类型:
AnyHashable类型是遵循Hashable协议的Any类型的别名。在Swift中,字典(Dictionary)和集合(Set)需要键(key)和元素(value)的类型遵循Hashable协议。由于Any类型可以表示任何类型,所以在实际使用中,你可能需要将Any类型的值转换为特定的类型以作为字典或集合的键。使用AnyHashable可以让你方便地进行这样的转换。 - AnyObject类型:
AnyObject类型是一个协议,任何对象都实现了这个协议。它主要用来表示任何类的实例。在Swift中,你可以使用AnyObject来存储任何对象的实例。值得注意的是,由于所有的类都隐式地实现了这个协议,因此只有类实例可以被赋给一个AnyObject类型的变量,而结构体和枚举的实例则不能。 - AnyClass类型:
AnyClass类型是AnyObject.Type的别名,表示任意类的元类型。在Swift中,你可以使用AnyClass来存储类的类型信息。这意味着你可以将一个类的类型作为AnyClass类型的值来使用,这在泛型编程中非常有用。
在实际使用中,这些特殊的类型可以帮助你更加灵活地处理各种数据类型,但是也需要注意它们可能带来的类型安全问题。在可能的情况下,尽量使用更具体的数据类型而不是这些特殊的类型,以确保代码的类型安全和可维护性。
1、 关联值:允许你存储与枚举成员值相关联的自定义类型的值。这使得枚举可以存储更多的信息,并能根据不同的场景返回不同类型的关联值。
2、 原始值:枚举成员可以有原始值,常见的原始值类型有字符串、字符或任何整数或浮点数类型。这使得枚举更容易在不同的上下文中转换和使用。
3、 递归枚举:通过在枚举成员前使用indirect关键字,枚举可以是递归的。这意味着枚举成员的关联值可以是枚举本身的一个实例,非常适合表示具有递归结构的数据模型,如树形结构。
4、 扩展和协议:枚举可以遵循协议,并且可以通过扩展来增加额外的功能。这为在枚举上定义共通的行为提供了一种强大的方式。
实现线程安全的方法在Swift中是至关重要的,尤其是在多线程环境下操作共享资源时。以下是实现线程安全的几种常用方法:
1、 使用串行队列:创建一个串行DispatchQueue,并将所有对共享资源的访问操作提交到这个队列中。由于串行队列一次只执行一个任务,这保证了同一时间只有一个线程能访问该资源。
2、 使用同步锁:Swift可以使用DispatchSemaphore或NSLock等锁机制来同步对共享资源的访问。在访问资源前加锁,在访问后解锁,以此来保证在任何时刻只有一个线程能访问该资源。
3、 使用原子操作:对于简单的数据类型,可以使用原子操作来实现线程安全。原子操作是系统级别的,能够保证操作的完整性,不会被其他线程打断。
4、 使用线程安全的数据结构:Swift标准库和第三方库提供了一些线程安全的数据结构,如ThreadSafeArray或Atomic等,这些数据结构内部已经实现了线程安全的保护。
1、 类型安全:泛型代码让你能够写出抽象和可复用的函数和类型,同时保留类型检查的优点。这意味着编译器可以自动检测类型错误。
2、 减少代码量:使用泛型可以减少重复代码,因为你可以用单一的函数或类型来处理不同类型的数据,而不是为每种数据类型编写特定的函数或类型。
3、 提高性能:泛型代码在编译时被实例化,这意味着编译器生成的代码已经是针对特定类型优化的。这可以在保持代码抽象和灵活性的同时,提供与非泛型代码相同的运行时性能。
4、 提升表达能力和灵活性:泛型让库和框架的设计者能够提供高度灵活和可配置的API,而无需牺牲类型安全或性能。
1、 定义一个接受闭包作为参数的函数。这个闭包的类型取决于你期望的回调数据类型和逻辑。
2、 在异步操作完成时,调用这个闭包,并将结果作为闭包的参数传递。
3、 当你调用这个函数时,传入一个闭包,这个闭包定义了当异步操作完成并返回结果时需要执行的操作。
1、 避免内存泄漏的关键方法之一是使用弱引用(weak)和无主引用(unowned)。当你预期引用可能会变成nil时,应该使用弱引用;如果引用始终不会变成nil,使用无主引用。
2、 在闭包中,使用[weak self]或[unowned self]捕获列表来打破循环强引用是一种常用的做法。这样可以确保闭包内部对实例的引用不会阻止Swift的自动引用计数(ARC)机制释放实例。
3、 使用自动引用计数(ARC)工具和内存分析器来识别和修复内存泄漏。Xcode提供了强大的工具,如Leaks和Allocations,来帮助开发者找到和修复内存泄漏问题。
1、 当你调用一个类的方法时,Swift运行时会查找这个类的虚拟派发表,找到对应方法的实际实现地址,然后跳转到这个地址执行方法。
2、 由于动态派发的存在,Swift可以在运行时而非编译时决定调用哪个方法的实现,这增加了程序的灵活性,但也可能略微降低性能。
3、 Swift中默认情况下类的方法是动态派发的。然而,通过使用final关键字标记方法或类,可以阻止方法被重写,从而允许编译器优化调用,采用更快的静态派发。
1、 利用模式匹配,可以匹配各种类型的值,包括枚举、元组和特定范围的值。这使得处理复杂的数据结构变得简单直观。
2、 模式匹配支持使用where子句来进一步细化条件,提供了更高的灵活性和表达力。
3、 在处理集合时,模式匹配可以与for-in循环结合使用,以便于对集合中的每个元素执行复杂的匹配逻辑。
4、 模式匹配不仅可以简化代码,提高代码的可读性,还能有效地减少错误。通过集中处理所有相关的条件分支,避免了零散的if或guard语句可能导致的逻辑遗漏。
1、 利用枚举的关联值来存储与每个枚举案例相关的额外信息。这使得枚举可以表达更复杂的状态或事件,同时保持代码整洁和组织良好。
2、 使用枚举来定义一组相关的命令或操作,然后通过switch语句来匹配并执行相应的逻辑。这种方式使得新增或修改命令变得非常简单。
3、 结合使用枚举和协议,可以定义一组遵循共同协议的枚举,这样即使它们代表不同的状态或事件,也能以统一的方式处理。
4、 利用枚举的原始值(通常用于表示静态或不变的数据)和计算属性,可以为枚举值附加更多的上下文信息,增加代码的可读性和易用性。
1、 编译时多态性(也称为静态多态性)主要通过方法重载和泛型实现。在编译时,编译器根据调用的参数类型和数量决定使用哪个具体的方法或函数。泛型也是编译时多态性的一个例子,它允许函数或类型与任何数据类型一起工作,类型检查发生在编译时。
2、 运行时多态性(也称为动态多态性)在Swift中主要通过继承和协议来实现。它允许在运行时决定调用哪个对象的哪个方法,这依赖于对象的实际类型。在Swift中,类的继承关系和协议的实现提供了运行时多态性,使得同一接口可以有多个实现,具体使用哪个实现在运行时通过动态派发来决定
1、 明确的访问级别有助于定义一个清晰的API边界。通过将内部实现细节设为private或fileprivate,可以隐藏不希望外部使用者访问的部分,只暴露必要的接口给外部使用。
2、 使用public或open访问级别可以明确指定哪些接口是设计用来被其他模块或框架使用的。open访问级别还允许在模块外被继承或重写,适用于设计可扩展的框架。
3、 合理的访问级别设置有助于模块的解耦。通过限制跨模块的直接访问,可以更容易地维护和重构代码,因为改动的影响范围更加可控。
4、 在大型项目或团队协作中,合理利用访问级别可以减少意外的修改和使用错误,提高代码的安全性和稳定性。
1、 定义协议来声明委托任务或功能。这些任务通常由委托者发起,委托方进行实现。
2、 在委托者类型中,定义一个遵循该协议的可选属性。这个属性用于持有任何遵循协议的实例的引用。
3、 委托方类型实现该协议,提供协议中定义的任务或功能的具体实现。
4、 在适当的时候,委托者通过协议定义的方法调用委托方提供的实现。
1、 数据验证:可以创建属性包装器来自动检查属性值是否满足特定条件,例如是否在给定的范围内,或者是否符合正则表达式。
2、 管理线程访问:对于多线程或并发编程,属性包装器可以用来确保属性访问的线程安全性,例如通过同步访问控制。
3、 懒加载:属性包装器可以用于实现属性的懒加载逻辑,即仅在第一次访问属性时计算其初始值。
4、 存储管理:可以利用属性包装器来透明地实现属性的持久化,比如自动从数据库加载和保存数据。
5、 观察者模式:属性包装器可以用来监控属性值的变化,执行一些操作,如更新UI或触发事件,当属性值改变时。
1、 async标记的函数表示它是异步的,可以在其内部执行耗时的操作而不阻塞当前线程。
2、 await用来调用异步函数,表示调用者需要等待异步操作完成。使用await时,编译器会自动将代码切换到合适的线程,确保当前的用户界面保持响应。
3、 解决的问题包括:简化异步代码的编写,使其更加直观和易于理解;避免了嵌套回调导致的复杂性和可读性差的问题;提高了代码的可维护性和错误处理的清晰度。
4、 异步/等待模式还提高了并发任务的性能和效率,因为它允许系统更优化地管理任务执行的资源和调度。
1、 定义协议:首先定义一个或多个协议,声明需要实现的方法和属性。
2、 实现协议:不同的类、结构体或枚举可以遵守这些协议,并提供具体的实现。
3、 使用协议类型:在函数、方法或者变量中使用协议类型作为类型标注。这允许你接受任何遵守该协议的实例,从而实现多态性。
4、 协议作为类型:协议本身可以作为类型使用,这意味着你可以声明一个协议类型的变量或常量,它们在运行时可以引用任何遵守该协议的实例。
可选链(Optional Chaining)是一种在当前可选项可能为nil的情况下查询和调用属性、方法及下标的过程。如果可选项有值,那么可选链调用会成功;如果可选项是nil,则可选链调用返回nil。可选链可以让你在不需要强制解包的情况下,安全地访问可选项的属性、方法和下标。
1、 使用可选链代替强制解包:当你尝试从可选项中取出值时,可选链提供了一种不会引起运行时错误的方法。
2、 多级可选链:你可以通过连接多个可选链调用来深入访问多层可选类型的属性、方法和下标。如果链中的任何一个节点是nil,整个表达式的结果也是nil。
3、 与可选绑定结合使用:可选链的结果是一个可选值,你可以使用可选绑定(if let或guard let)来检测可选链的结果是否存在。
4、 对方法的可选链调用:如果你尝试通过可选链调用方法,该方法的返回类型将是一个可选值,即使方法本身定义时返回的不是可选值。
1、 静态库:在编译时,静态库的代码会被整合到最终的可执行文件中。每个使用静态库的应用都会有一份库的拷贝,这意味着静态库的更新需要重新编译应用。
2、 动态库:与静态库不同,动态库在应用运行时被加载。这意味着多个应用可以共享同一份动态库的拷贝,减少了应用的体积。当动态库更新时,不需要重新编译使用它的应用,只需替换动态库文件即可。
3、 内存占用:使用静态库会增加应用的总体积,因为库的代码被整合进了应用。而动态库虽然可以减少单个应用的体积,但如果有多个应用同时运行并使用同一动态库,它们将共享这份库的内存拷贝。
4、 兼容性和版本控制:动态库更易于管理和更新,因为它们是独立于应用外的。但这也带来了版本兼容性问题,需要确保应用与动态库的兼容性。
5、 安全性和隐私:静态库被编译进应用中,更不易被替换或篡改。而动态库由于是在运行时加载,可能面临被替换的风险,但也使得安全更新更加容易实施。
dart是一种面向对象语言,dart是flutter的程序开发语言。
runApp函数是渲染根widget树的函数
一般情况下runApp函数会在main函数里执行
什么是widget? 在flutter里有几种类型的widget?分别有什么区别?能分别说一下生命周期吗?
widget在flutter里基本是一些UI组件
有两种类型的widget,分别是statefulWidget 和 statelessWidget两种
statelessWidget不会自己重新构建自己,但是statefulWidget会
Hot Restart 和 Hot Reload 有什么区别吗?
Hot Reload比Hot Restart快,Hot Reload会编译我们文件里新加的代码并发送给dart虚拟机,dart会更新widgets来改变UI,而Hot Restart会让dart 虚拟机重新编译应用。另一方面也是因为这样, Hot Reload会保留之前的state,而Hot Restart回你重置所有的state回到初始值。
Stream 用来处理连续的异步操作,Stream 是一个抽象类,用于表示一序列异步数据的源。它是一种产生连续事件的方式,可以生成数据事件或者错误事件,以及流结束时的完成事件
Stream 分单订阅流和广播流。
网络状态的监控
await的出现会把await之前和之后的代码分为两部分,await并不像字面意思所表示的程序运行到这里就阻塞了,而是立刻结束当前函数的执行并返回一个Future,函数内剩余代码通过调度异步执行。
async是和await搭配使用的,await只在async函数中出现。在async 函数里可以没有await或者有多个await。
在 Flutter 中有两种处理异步操作的方式 Future 和 Stream,Future 用于处理单个异步操作,Stream 用来处理连续的异步操作。
key是Widgets,Elements和SemanticsNodes的标识符。
key有LocalKey 和 GlobalKey两种。
LocalKey 如果要修改集合中的控件的顺序或数量。GlobalKey允许 Widget 在应用中的任何位置更改父级而不会丢失 State。
profile model 是用来评估app性能的,profile model 和release mode是相似的,只有保留了一些需要评估app性能的debug功能。在模拟器上profile model是不可用的。
foundation有一个静态的变量kReleaseMode来表示是否是release mode
isolate是Dart对actor并发模式的实现。 isolate是有自己的内存和单线程控制的运行实体。isolate本身的意思是“隔离”,因为isolate之间的内存在逻辑上是隔离的。isolate中的代码是按顺序执行的,任何Dart程序的并发都是运行多个isolate的结果。因为Dart没有共享内存的并发,没有竞争的可能性所以不需要锁,也就不用担心死锁的问题
列举在flutter的状态管理方案?
Scoped Model
Redux
BLoC
RxDart
provider
Dart 当中的 「…」表示什么意思?
Dart 当中的 「…」意思是 「级联操作符」,为了方便配置而使用。「…」和「.」不同的是 调用「…」后返回的相当于是 this,而「.」返回的则是该方法返回的值 。
Dart 的作用域
Dart 没有 「public」「private」等关键字,默认就是公开的,私有变量使用 下划线 _开头。
Dart 是不是单线程模型?是如何运行的?
Dart 是单线程模型,运行的的流程如下图。
简单来说,Dart 在单线程中是以消息循环机制来运行的,包含两个任务队列,一个是“微任务队列” microtask queue,另一个叫做“事件队列” event queue。
当Flutter应用启动后,消息循环机制便启动了。首先会按照先进先出的顺序逐个执行微任务队列中的任务,当所有微任务队列执行完后便开始执行事件队列中的任务,事件任务执行完毕后再去执行微任务,如此循环往复,生生不息。
前面说过, Dart 是单线程的,不存在多线程,那如何进行多任务并行的呢?其实,Dart的多线程和前端的多线程有很多的相似之处。Flutter的多线程主要依赖Dart的并发编程、异步和事件驱动机制。
简单的说,在Dart中,一个Isolate对象其实就是一个isolate执行环境的引用,一般来说我们都是通过当前的isolate去控制其他的isolate完成彼此之间的交互,而当我们想要创建一个新的Isolate可以使用Isolate.spawn方法获取返回的一个新的isolate对象,两个isolate之间使用SendPort相互发送消息,而isolate中也存在了一个与之对应的ReceivePort接受消息用来处理,但是我们需要注意的是,ReceivePort和SendPort在每个isolate都有一对,只有同一个isolate中的ReceivePort才能接受到当前类的SendPort发送的消息并且处理。
前面说过,Dart 在单线程中是以消息循环机制来运行的,其中包含两个任务队列,一个是“微任务队列” microtask queue,另一个叫做“事件队列” event queue。
在Java并发编程开发中,经常会使用Future来处理异步或者延迟处理任务等操作。而在Dart中,执行一个异步任务同样也可以使用Future来处理。在 Dart 的每一个 Isolate 当中,执行的优先级为 :Main > MicroTask > EventQueue。
在Dart中,Stream 和 Future 一样,都是用来处理异步编程的工具。它们的区别在于,Stream 可以接收多个异步结果,而Future 只有一个。
Stream 的创建可以使用 Stream.fromFuture,也可以使用 StreamController 来创建和控制。还有一个注意点是:普通的 Stream 只可以有一个订阅者,如果想要多订阅的话,要使用 asBroadcastStream()。
Stream有两种订阅模式:单订阅(single) 和 多订阅(broadcast)。单订阅就是只能有一个订阅者,而广播是可以有多个订阅者。这就有点类似于消息服务(Message Service)的处理模式。单订阅类似于点对点,在订阅者出现之前会持有数据,在订阅者出现之后就才转交给它。而广播类似于发布订阅模式,可以同时有多个订阅者,当有数据时就会传递给所有的订阅者,而不管当前是否已有订阅者存在。
Stream 默认处于单订阅模式,所以同一个 stream 上的 listen 和其它大多数方法只能调用一次,调用第二次就会报错。但 Stream 可以通过 transform() 方法(返回另一个 Stream)进行连续调用。通过 Stream.asBroadcastStream() 可以将一个单订阅模式的 Stream 转换成一个多订阅模式的 Stream,isBroadcast 属性可以判断当前 Stream 所处的模式。
await for是不断获取stream流中的数据,然后执行循环体中的操作。它一般用在直到stream什么时候完成,并且必须等待传递完成之后才能使用,不然就会一直阻塞。
Stream stream = new Stream.fromIterable([‘不开心’, ‘面试’, ‘没’, ‘过’]);
main() async{
await for(String s in stream){
print(s);
}
}
mixin 是Dart 2.1 加入的特性,以前版本通常使用abstract class代替。简单来说,mixin是为了解决继承方面的问题而引入的机制,Dart为了支持多重继承,引入了mixin关键字,它最大的特殊处在于:mixin定义的类不能有构造方法,这样可以避免继承多个类而产生的父类构造方法冲突。mixins的对象是类,mixins绝不是继承,也不是接口,而是一种全新的特性,可以mixins多个类,mixins的使用需要满足一定条件
请简单介绍下Flutter框架,以及它的优缺点?
Flutter是Google推出的一套开源跨平台UI框架,可以快速地在Android、iOS和Web平台上构建高质量的原生用户界面。同时,Flutter还是Google新研发的Fuchsia操作系统的默认开发套件。在全世界,Flutter正在被越来越多的开发者和组织使用,并且Flutter是完全免费、开源的。Flutter采用现代响应式框架构建,其中心思想是使用组件来构建应用的UI。当组件的状态发生改变时,组件会重构它的描述,Flutter会对比之前的描述,以确定底层渲染树从当前状态转换到下一个状态所需要的最小更改。
优点
• 热重载(Hot Reload),利用Android Studio直接一个ctrl+s就可以保存并重载,模拟器立马就可以看见效果,相比原生冗长的编译过程强很多;
• 一切皆为Widget的理念,对于Flutter来说,手机应用里的所有东西都是Widget,通过可组合的空间集合、丰富的动画库以及分层课扩展的架构实现了富有感染力的灵活界面设计;
• 借助可移植的GPU加速的渲染引擎以及高性能本地代码运行时以达到跨平台设备的高质量用户体验。简单来说就是:最终结果就是利用Flutter构建的应用在运行效率上会和原生应用差不多。
缺点
• 不支持热更新;
• 三方库有限,需要自己造轮子;
• Dart语言编写,增加了学习难度,并且学习了Dart之后无其他用处,相比JS和Java来说。
介绍下Flutter的理念架构
其实也就是下面这张图。
由上图可知,Flutter框架自下而上分为Embedder、Engine和Framework三层。其中,Embedder是操作系统适配层,实现了渲染 Surface设置,线程设置,以及平台插件等平台相关特性的适配;Engine层负责图形绘制、文字排版和提供Dart运行时,Engine层具有独立虚拟机,正是由于它的存在,Flutter程序才能运行在不同的平台上,实现跨平台运行;Framework层则是使用Dart编写的一套基础视图库,包含了动画、图形绘制和手势识别等功能,是使用频率最高的一层。
介绍下FFlutter的FrameWork层和Engine层,以及它们的作用
Flutter的FrameWork层是用Drat编写的框架(SDK),它实现了一套基础库,包含Material(Android风格UI)和Cupertino(iOS风格)的UI界面,下面是通用的Widgets(组件),之后是一些动画、绘制、渲染、手势库等。这个纯 Dart实现的 SDK被封装为了一个叫作 dart:ui的 Dart库。我们在使用 Flutter写 App的时候,直接导入这个库即可使用组件等功能。
Flutter的Engine层是Skia 2D的绘图引擎库,其前身是一个向量绘图软件,Chrome和 Android均采用 Skia作为绘图引擎。Skia提供了非常友好的 API,并且在图形转换、文字渲染、位图渲染方面都提供了友好、高效的表现。Skia是跨平台的,所以可以被嵌入到 Flutter的 iOS SDK中,而不用去研究 iOS闭源的 Core Graphics / Core Animation。Android自带了 Skia,所以 Flutter Android SDK要比 iOS SDK小很多。
介绍下Widget、State、Context 概念
Widget:在Flutter中,几乎所有东西都是Widget。将一个Widget想象为一个可视化的组件(或与应用可视化方面交互的组件),当你需要构建与布局直接或间接相关的任何内容时,你正在使用Widget。
Widget树:Widget以树结构进行组织。包含其他Widget的widget被称为父Widget(或widget容器)。包含在父widget中的widget被称为子Widget。
Context:仅仅是已创建的所有Widget树结构中的某个Widget的位置引用。简而言之,将context作为widget树的一部分,其中context所对应的widget被添加到此树中。一个context只从属于一个widget,它和widget一样是链接在一起的,并且会形成一个context树。
State:定义了StatefulWidget实例的行为,它包含了用于”交互/干预“Widget信息的行为和布局。应用于State的任何更改都会强制重建Widget。
简述Widget的StatelessWidget和StatefulWidget两种状态组件类
StatelessWidget: 一旦创建就不关心任何变化,在下次构建之前都不会改变。它们除了依赖于自身的配置信息(在父节点构建时提供)外不再依赖于任何其他信息。比如典型的Text、Row、Column、Container等,都是StatelessWidget。它的生命周期相当简单:初始化、通过build()渲染。
StatefulWidget: 在生命周期内,该类Widget所持有的数据可能会发生变化,这样的数据被称为State,这些拥有动态内部数据的Widget被称为StatefulWidget。比如复选框、Button等。State会与Context相关联,并且此关联是永久性的,State对象将永远不会改变其Context,即使可以在树结构周围移动,也仍将与该context相关联。当state与context关联时,state被视为已挂载。StatefulWidget由两部分组成,在初始化时必须要在createState()时初始化一个与之相关的State对象。
Flutter的Widget分为StatelessWidget和StatefulWidget两种。其中,StatelessWidget是无状态的,StatefulWidget是有状态的,因此实际使用时,更多的是StatefulWidget。StatefulWidget的生命周期如下图。
initState():Widget 初始化当前 State,在当前方法中是不能获取到 Context 的,如想获取,可以试试 Future.delayed()
didChangeDependencies():在 initState() 后调用,State对象依赖关系发生变化的时候也会调用。
deactivate():当 State 被暂时从视图树中移除时会调用这个方法,页面切换时也会调用该方法,和Android里的 onPause 差不多。
dispose():Widget 销毁时调用。
didUpdateWidget:Widget 状态发生变化的时候调用。
首先看一下这几个对象的含义及作用。
• Widget :仅用于存储渲染所需要的信息。
• RenderObject :负责管理布局、绘制等操作。
• Element :才是这颗巨大的控件树上的实体。
Widget会被inflate(填充)到Element,并由Element管理底层渲染树。Widget并不会直接管理状态及渲染,而是通过State这个对象来管理状态。Flutter创建Element的可见树,相对于Widget来说,是可变的,通常界面开发中,我们不用直接操作Element,而是由框架层实现内部逻辑。就如一个UI视图树中,可能包含有多个TextWidget(Widget被使用多次),但是放在内部视图树的视角,这些TextWidget都是填充到一个个独立的Element中。Element会持有renderObject和widget的实例。记住,Widget 只是一个配置,RenderObject 负责管理布局、绘制等操作。
在第一次创建 Widget 的时候,会对应创建一个 Element, 然后将该元素插入树中。如果之后 Widget 发生了变化,则将其与旧的 Widget 进行比较,并且相应地更新 Element。重要的是,Element 不会被重建,只是更新而已。
Flutter中的状态和前端React中的状态概念是一致的。React框架的核心思想是组件化,应用由组件搭建而成,组件最重要的概念就是状态,状态是一个组件的UI数据模型,是组件渲染时的数据依据。
Flutter的状态可以分为全局状态和局部状态两种。常用的状态管理有ScopedModel、BLoC、Redux / FishRedux和Provider。详细使用情况和差异可以自行了解。
Flutter的绘制流程如下图所示。
Flutter只关心向 GPU提供视图数据,GPU的 VSync信号同步到 UI线程,UI线程使用 Dart来构建抽象的视图结构,这份数据结构在 GPU线程进行图层合成,视图数据提供给 Skia引擎渲染为 GPU数据,这些数据通过 OpenGL或者 Vulkan提供给 GPU。
默认情况下,Flutter Engine层会创建一个Isolate,并且Dart代码默认就运行在这个主Isolate上。必要时可以使用spawnUri和spawn两种方式来创建新的Isolate,在Flutter中,新创建的Isolate由Flutter进行统一的管理。
事实上,Flutter Engine自己不创建和管理线程,Flutter Engine线程的创建和管理是Embeder负责的,Embeder指的是将引擎移植到平台的中间层代码,Flutter Engine层的架构示意图如下图所示。
在Flutter的架构中,Embeder提供四个Task Runner,分别是Platform Task Runner、UI Task Runner Thread、GPU Task Runner和IO Task Runner,每个Task Runner负责不同的任务,Flutter Engine不在乎Task Runner运行在哪个线程,但是它需要线程在整个生命周期里面保持稳定。
Flutter 通过 PlatformChannel 与原生进行交互,其中 PlatformChannel 分为三种:
• BasicMessageChannel :用于传递字符串和半结构化的信息。
• MethodChannel :用于传递方法调用(method invocation)。
• EventChannel : 用于数据流(event streams)的通信。
同时 Platform Channel 并非是线程安全的 ,更多详细可查阅闲鱼技术的 《深入理解Flutter Platform Channel》
简述Flutter 的热重载
Flutter 的热重载是基于 JIT 编译模式的代码增量同步。由于 JIT 属于动态编译,能够将 Dart 代码编译成生成中间代码,让 Dart VM 在运行时解释执行,因此可以通过动态更新中间代码实现增量同步。
热重载的流程可以分为 5 步,包括:扫描工程改动、增量编译、推送更新、代码合并、Widget 重建。Flutter 在接收到代码变更后,并不会让 App 重新启动执行,而只会触发 Widget 树的重新绘制,因此可以保持改动前的状态,大大缩短了从代码修改到看到修改产生的变化之间所需要的时间。
另一方面,由于涉及到状态的保存与恢复,涉及状态兼容与状态初始化的场景,热重载是无法支持的,如改动前后 Widget 状态无法兼容、全局变量与静态属性的更改、main 方法里的更改、initState 方法里的更改、枚举和泛型的更改等。
可以发现,热重载提高了调试 UI 的效率,非常适合写界面样式这样需要反复查看修改效果的场景。但由于其状态保存的机制所限,热重载本身也有一些无法支持的边界
版权声明:
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若内容造成侵权、违法违规、事实不符,请将相关资料发送至xkadmin@xkablog.com进行投诉反馈,一经查实,立即处理!
转载请注明出处,原文链接:https://www.xkablog.com/bcyy/26958.html