数据结构
1.数据结构的存储一般分为几种?各有什么特点
数据结构的存储一般常用的有两种 顺序存储结构 和 链式存储结构
顺序存储结构:
比如,数组,1-2-3-4-5-6-7-8-9-10,存储是按顺序的。再比如栈和队列等
链式存储结构:
比如,数组,1-2-3-4-5-6-7-8-9-10,链式存储就不一样了 1(地址)-2(地址)-7(地址)-4(地址)-5(地址)-9(地址)-8(地址)-3(地址)-6(地址)-10(地址)。每个数字后面跟着一个地址 而且存储形式不再是顺序
2.集合结构 线性结构 树形结构 图形结构
- 集合结构
一个集合,就是一个圆圈中有很多个元素,元素与元素之间没有任何关系 这个很简单
- 线性结构
一个条线上站着很多个人。 这条线不一定是直的。也可以是弯的。也可以是值的 相当于一条线被分成了好几段的样子 (发挥你的想象力)。 线性结构是一对一的关系
- 树形结构
做开发的肯定或多或少的知道xml 解析 树形结构跟他非常类似。也可以想象成一个金字塔。树形结构是一对多的关系
- 图形结构
这个就比较复杂了。他呢 无穷。无边 无向(没有方向)图形机构 你可以理解为多对多 类似于我们人的交集关系
3.单向链表 双向链表 循环链表
4.数组和链表区别
- 数组
数组元素在内存上连续春芳,可以通过下表查找元素;插入,删除需要移动大量元素,比较适用于元素很少的情况
- 链表
链表中的元素在内存中不是顺序存储的,查找慢,插入、删除只需要对元素指针重新赋值,效率高
堆、栈和队列
堆
- 堆是一种经过排序的树形数据结构,每个节点都有一个值,通常我们所说的堆的数据结构是指二叉树。所以堆在数据结构中通常可以被看做是一棵树的数组对象。而且堆需要满足一下两个性质:
1.堆中某个节点的值总是不大于或不小于其父节点的值;
2.堆总是一棵完全二叉树。
- 堆分为两种情况,有最大堆和最小堆。将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆,在一个摆放好元素的最小堆中,父结点中的元素一定比子结点的元素要小,但对于左右结点的大小则没有规定谁大谁小
- 堆常用来实现优先队列,堆的存取是随意的,这就如同我们在图书馆的书架上取书,虽然书的摆放是有顺序的,但是我们想取任意一本时不必像栈一样,先取出前面所有的书,书架这种机制不同于箱子,我们可以直接取出我们想要的书。
栈
- 栈是限定仅在表尾进行插入和删除操作的线性表。我们把允许插入和删除的一端称为栈顶,另一端称为栈底,不含任何数据元素的栈称为空栈。栈的特殊之处在于它限制了这个线性表的插入和删除位置,它始终只在栈顶进行。
- 栈是一种具有后进先出的数据结构,又称为后进先出的线性表,简称 LIFO(Last In First Out)结构。也就是说后存放的先取,先存放的后取,这就类似于我们要在取放在箱子底部的东西(放进去比较早的物体),我们首先要移开压在它上面的物体(放进去比较晚的物体)。
- 堆栈中定义了一些操作。两个最重要的是PUSH和POP。PUSH操作在堆栈的顶部加入一个元素。POP操作相反,在堆栈顶部移去一个元素,并将堆栈的大小减一。
- 栈的应用—递归
队列
- 队列是只允许在一端进行插入操作、而在另一端进行删除操作的线性表。允许插入的一端称为队尾,允许删除的一端称为队头。它是一种特殊的线性表,特殊之处在于它只允许在表的前端进行删除操作,而在表的后端进行插入操作,和栈一样,队列是一种操作受限制的线性表。
- 队列是一种先进先出的数据结构,又称为先进先出的线性表,简称 FIFO(First In First Out)结构。也就是说先放的先取,后放的后取,就如同行李过安检的时候,先放进去的行李在另一端总是先出来,后放入的行李会在最后面出来。
输入一棵二叉树的根结点,求该树的深度?
二叉树的节点定义如下:
struct BinaryTreeNode
{
int m_nValue ;
BinaryTreeNode* m_pLeft;
BinarvTreeNode* m_pRight ;
}
- 如果一棵树只有一个结点,它的深度为1。
- 如果根结点只有左子树而没有右子树,那么树的深度应该是其左子树的深度加1;同样如果根结点只有右子树而没有左子树,那么树的深度应该是其右子树的深度加1。
- 如果既有右子树又有左子树,那该树的深度就是其左、右子树深度的较大值再加1。
int TreeDepth(TreeNode* pRoot)
{
if(pRoot == nullptr)
return 0;
int left = TreeDepth(pRoot->left);
int right = TreeDepth(pRoot->right);
return (left>right) ? (left+1) : (right+1);
}
输入一课二叉树的根结点,判断该树是不是平衡二叉树?
- 重复遍历结点
先求出根结点的左右子树的深度,然后判断它们的深度相差不超过1,如果否,则不是一棵二叉树;如果是,再用同样的方法分别判断左子树和右子树是否为平衡二叉树,如果都是,则这就是一棵平衡二叉树
- 遍历一遍结点
遍历结点的同时记录下该结点的深度,避免重复访问
方法1
struct TreeNode{
int val;
TreeNode* left;
TreeNode* right;
};
int TreeDepth(TreeNode* pRoot){
if(pRoot==NULL)
return 0;
int left=TreeDepth(pRoot->left);
int right=TreeDepth(pRoot->right);
return left>right?(left+1):(right+1);
}
bool IsBalanced(TreeNode* pRoot){
if(pRoot==NULL)
return true;
int left=TreeDepth(pRoot->left);
int right=TreeDepth(pRoot->right);
int diff=left-right;
if(diff>1 || diff<-1)
return false;
return IsBalanced(pRoot->left) && IsBalanced(pRoot->right);
}
方法2
bool IsBalanced_1(TreeNode* pRoot,int& depth){
if(pRoot==NULL){
depth=0;
return true;
}
int left,right;
int diff;
if(IsBalanced_1(pRoot->left,left) && IsBalanced_1(pRoot->right,right)){
diff=left-right;
if(diff<=1 || diff>=-1){
depth=left>right?left+1:right+1;
return true;
}
}
return false;
}
bool IsBalancedTree(TreeNode* pRoot){
int depth=0;
return IsBalanced_1(pRoot,depth);
}
Foundation(基础)
1.nil NIL NSNULL区别
- nil、NIL 可以说是等价的,都代表内存中一块空地址。
- NSNULL 代表一个指向 nil 的对象。
2.如何实现一个线程安全的 NSMutableArray?
NSMutableArray是线程不安全的,当有多个线程同时对数组进行操作的时候可能导致崩溃或数据错误
- 线程锁:使用线程锁对数组读写时进行加锁
- 派发队列:在《Effective Objective-C 2.0…》书中第41条:多用派发队列,少用同步锁中指出:使用“串行同步队列”(serial synchronization queue),将读取操作及写入操作都安排在同一个队列里,即可保证数据同步。而通过并发队列,结合GCD的栅栏块(barrier)来不仅实现数据同步线程安全,还比串行同步队列方式更高效。
3.atomic修饰符是绝对安全吗,为什么?
不是,所谓的安全只是局限于 Setter、Getter 的访问器方法而言的,你对它做 Release 的操作是不会受影响的。这个时候就容易崩溃了。
4.实现 isEqual 和 hash 方法时要注意什么?
- hash
对关键属性的hash值进行位或运算作为hash值
- isEqual
==运算符判断是否是同一对象, 因为同一对象必然完全相同
判断是否是同一类型, 这样不仅可以提高判等的效率, 还可以避免隐式类型转换带来的潜在风险
判断对象是否是nil, 做参数有效性检查
各个属性分别使用默认判等方法进行判断
返回所有属性判等的与结果
5.id 和 instanceType 有什么区别?
- 相同点
instancetype 和 id 都是万能指针,指向对象。
- 不同点
1.id 在编译的时候不能判断对象的真实类型,instancetype 在编译的时候可以判断对象的真实类型。
2…id 可以用来定义变量,可以作为返回值类型,可以作为形参类型;instancetype 只能作为返回值类型。
self和super的区别
- self调用自己方法,super调用父类方法
- self是类,super是预编译指令
- [self class] 和 [super class] 输出是一样的
- self和super底层实现原理
1.当使用 self 调用方法时,会从当前类的方法列表中开始找,如果没有,就从父类中再找;
而当使用 super 时,则从父类的方法列表中开始找,然后调用父类的这个方法sd
2.当使用 self 调用时,会使用 objc_msgSend 函数:id objc_msgSend(id theReceiver, SEL theSelector, …)
第一个参数是消息接收者,第二个参数是调用的具体类方法的 selector,后面是 selector 方法的可变参数。以 [self setName:] 为例,编译器会替换成调用 objc_msgSend 的函数调用,其中 theReceiver 是 self,theSelector 是 @selector(setName:),这个 selector 是从当前 self 的 class 的方法列表开始找的 setName,当找到后把对应的 selector 传递过去。
3.当使用 super 调用时,会使用 objc_msgSendSuper 函数:id objc_msgSendSuper(struct objc_super *super, SEL op, …)
第一个参数是个objc_super的结构体,第二个参数还是类似上面的类方法的selector
struct objc_super {
id receiver;
Class superClass;
};
7.@synthesize和@dynamic分别有什么作用?
- @property有两个对应的词,一个是 @synthesize,一个是 @dynamic。如果 @synthesize和 @dynamic都没写,那么默认的就是@syntheszie var = _var;
- @synthesize 的语义是如果你没有手动实现 setter 方法和 getter 方法,那么编译器会自动为你加上这两个方法。
- @dynamic 告诉编译器:属性的 setter 与 getter 方法由用户自己实现,不自动生成。(当然对于 readonly 的属性只需提供 getter 即可)。假如一个属性被声明为 @dynamic var,然后你没有提供 @setter方法和 @getter 方法,编译的时候没问题,但是当程序运行到 instance.var = someVar,由于缺 setter 方法会导致程序崩溃;或者当运行到 someVar = var 时,由于缺 getter 方法同样会导致崩溃。编译时没问题,运行时才执行相应的方法,这就是所谓的动态绑定。
8.typeof 和 __typeof,typeof 的区别?
- __typeof __() 和 __typeof() 是 C语言 的编译器特定扩展,因为标准 C 不包含这样的运算符。 标准 C 要求编译器用双下划线前缀语言扩展(这也是为什么你不应该为自己的函数,变量等做这些)
- typeof() 与前两者完全相同的,只不过去掉了下划线,同时现代的编译器也可以理解。
- 所以这三个意思是相同的,但没有一个是标准C,不同的编译器会按需选择符合标准的写法。
10.struct和class的区别
- 类: 引用类型(位于栈上面的指针(引用)和位于堆上的实体对象)
- 结构:值类型(实例直接位于栈中)
UIKit
1.UIView 和 CALayer 是什么关系?
- UIView 继承 UIResponder,而 UIResponder 是响应者对象,可以对iOS 中的事件响应及传递,CALayer 没有继承自 UIResponder,所以 CALayer 不具备响应处理事件的能力。CALayer 是 QuartzCore 中的类,是一个比较底层的用来绘制内容的类,用来绘制UI
- UIView 对 CALayer 封装属性,对 UIView 设置 frame、center、bounds 等位置信息时,其实都是UIView 对 CALayer 进一层封装,使得我们可以很方便地设置控件的位置;例如圆角、阴影等属性, UIView 就没有进一步封装,所以我们还是需要去设置 Layer 的属性来实现功能。
- UIView 是 CALayer 的代理,UIView 持有一个 CALayer 的属性,并且是该属性的代理,用来提供一些 CALayer 行的数据,例如动画和绘制。
Bounds 和 Frame 的区别?
- Bounds:一般是相对于自身来说的,是控件的内部尺寸。如果你修改了 Bounds,那么子控件的相对位置也会发生改变。
- Frame :是相对于父控件来说的,是控件的外部尺寸。
setNeedsDisplay 和 layoutIfNeeded 两者是什么关系?
- UIView的setNeedsDisplay和setNeedsLayout两个方法都是异步执行的。而setNeedsDisplay会自动调用drawRect方法,这样可以拿到UIGraphicsGetCurrentContext进行绘制;而setNeedsLayout会默认调用layoutSubViews,给当前的视图做了标记;layoutIfNeeded 查找是否有标记,如果有标记及立刻刷新。
- 只有setNeedsLayout和layoutIfNeeded这二者合起来使用,才会起到立刻刷新的效果。
4.谈谈对UIResponder的理解
UIResponder类是专门用来响应用户的操作处理各种事件的,包括触摸事件(Touch Events)、运动事件(Motion Events)、远程控制事件(Remote Control Events)。我们知道UIApplication、UIView、UIViewController这几个类是直接继承自UIResponder,所以这些类都可以响应事件。当然我们自定义的继承自UIView的View以及自定义的继承自UIViewController的控制器都可以响应事件。
5.loadView的作用?
loadView方法会在每次访问UIViewController的view(比如controller.view、self.view)而且view为nil时会被调用,此方法主要用来负责创建UIViewController的view(重写loadView方法,并且不需要调用[super loadView])
这里要提一下 [super loadView],[super loadView]做了下面几件事。
- 它会先去查找与UIViewController相关联的xib文件,通过加载xib文件来创建UIViewController的view,如果在初始化UIViewController指定了xib文件名,就会根据传入的xib文件名加载对应的xib文件,如果没有明显地传xib文件名,就会加载跟UIViewController同名的xib文件
- 如果没有找到相关联的xib文件,就会创建一个空白的UIView,然后赋值给UIViewController的view属性
综上,在需要自定义UIViewController的view时,可以通过重写loadView方法且不需要调用[super loadView]方法。
6.使用 drawRect有什么影响?
drawRect 方法依赖 Core Graphics 框架来进行自定义的绘制 缺点:它处理 touch 事件时每次按钮被点击后,都会用 setNeddsDisplay 进行强制重绘;而且不止一次,每次单点事件触发两次执行。这样的话从性能的角度来说,对 CPU 和内存来说都是欠佳的。特别是如果在我们的界面上有多个这样的UIButton 实例,那就会很糟糕了。这个方法的调用机制也是非常特别. 当你调用 setNeedsDisplay 方法时, UIKit 将会把当前图层标记为 dirty,但还是会显示原来的内容,直到下一次的视图渲染周期,才会将标记为 dirty 的图层重新建立 Core Graphics 上下文,然后将内存中的数据恢复出来, 再使用 CGContextRef 进行绘制
7.keyWindow 和 delegate的window有何区别
- delegate.window 程序启动时设置的window对象。
- keyWindow 这个属性保存了[windows]数组中的[UIWindow]对象,该对象最近被发送了[makeKeyAndVisible]消息
一般情况下 delegate.window 和 keyWindow 是同一个对象,但不能保证keyWindow就是delegate.window,因为keyWindow会因为makeKeyAndVisible而变化,例如,程序中添加了一个悬浮窗口,这个时候keywindow就会变化。
WebView
1.说一下 JS 和 OC 互相调用的几种方式?
根据网页重定向截取字符串通过url scheme判断
替换方法.context[@“copyText”]
注入对象:遵守协议JSExport,设置context[@
通过webVIew调用 webView stringByEvaluatingJavaScriptFromString: 调用
通过JSContext调用[context evaluateScript:];
2.在使用 WKWedView 时遇到过哪些问题?
白屏问题,Cookie 问题,在WKWebView上直接使用NSURLProtocol无法拦截请求,在WKWebView 上通过loadRequ发起的post请求body数据被丢失,截屏问题等
消息传递的方式
1.KVC实现原理
- KVC,键-值编码,使用字符串直接访问对象的属性。
- 底层实现,当一个对象调用setValue方法时,方法内部会做以下操作:
1.检查是否存在相应key的set方法,如果存在,就调用set方法
2.如果set方法不存在,就会查找与key相同名称并且带下划线的成员属性,如果有,则直接给成员属性赋值
3.如果没有找到_key,就会查找相同名称的属性key,如果有就直接赋值
4.如果还没找到,则调用valueForUndefinedKey:和setValue:forUndefinedKey:方法
2.KVO的实现原理
- KVO-键值观察机制,原理如下:
1.当给A类添加KVO的时候,runtime动态的生成了一个子类NSKVONotifying_A,让A类的isa指针指向NSKVONotifying_A类,重写class方法,隐藏对象真实类信息
2.重写监听属性的setter方法,在setter方法内部调用了Foundation的_NSSetObjectValueAndNotify 函数
- ()_NSSetObjectValueAndNotify函数内部
- 首先会调用 willChangeValueForKey
- 然后给属性赋值
- 最后调用 didChangeValueForKey
- 最后调用 observer 的 observeValueForKeyPath 去告诉监听器属性值发生了改变
4.重写了dealloc做一些 KVO 内存释放
3.如何手动触发KVO方法
- 手动调用willChangeValueForKey和didChangeValueForKey方法
- 键值观察通知依赖于 NSObject 的两个方法: willChangeValueForKey: 和 didChangeValueForKey。在一个被观察属性发生改变之前, willChangeValueForKey: 一定会被调用,这就 会记录旧的值。而当改变发生后, didChangeValueForKey 会被调用,继而 observeValueForKey:ofObject:change:context: 也会被调用。如果可以手动实现这些调用,就可以实现“手动触发”了 有人可能会问只调用didChangeValueForKey方法可以触发KVO方法,其实是不能的,因为willChangeValueForKey: 记录旧的值,如果不记录旧的值,那就没有改变一说了
4.通知和代理有什么区别
- 通知是观察者模式,适合一对多的场景
- 代理模式适合一对一的反向传值
- 通知耦合度低,代理的耦合度高
5.block和delegate的区别
- delegate运行成本低,block的运行成本高
- block出栈需要将使用的数据从栈内存拷贝到堆内存,当然对象的话就是加计数,使用完或者block置nil后才消除。delegate只是保存了一个对象指针,直接回调,没有额外消耗。就像C的函数指针,只多做了一个查表动作。
- delegate更适用于多个回调方法(3个以上),block则适用于1,2个回调时。
6.为什么Block用copy关键字
Block在没有使用外部变量时,内存存在全局区,然而,当Block在使用外部变量的时候,内存是存在于栈区,当Block copy之后,是存在堆区的。存在于栈区的特点是对象随时有可能被销毁,一旦销毁在调用的时候,就会造成系统的崩溃。所以Block要用copy关键字。
组件化
1.组件化有什么好处?
- 业务分层、解耦,使代码变得可维护;
- 有效的拆分、组织日益庞大的工程代码,使工程目录变得可维护;
- 便于各业务功能拆分、抽离,实现真正的功能复用;
- 业务隔离,跨团队开发代码控制和版本风险控制的实现;
- 模块化对代码的封装性、合理性都有一定的要求,提升开发同学的设计能力;
+在维护好各级组件的情况下,随意组合满足不同客户需求;(只需要将之前的多个业务组件模块在新的主App中进行组装即可快速迭代出下一个全新App)
2.你是如何组件化解耦的?
- 分层
1.基础功能组件:按功能分库,不涉及产品业务需求,跟库Library类似,通过良好的接口拱上层业务组件调用;不写入产品定制逻辑,通过扩展接口完成定制;
2.基础UI组件:各个业务模块依赖使用,但需要保持好定制扩展的设计
3.业务组件:业务功能间相对独立,相互间没有Model共享的依赖;业务之间的页面调用只能通过UIBus进行跳转;业务之间的逻辑Action调用只能通过服务提供;
- 中间件:target-action,url-block,protocol-class
3.为什么CTMediator方案优于基于Router的方案?
Router的缺点:
- 在组件化的实施过程中,注册URL并不是充分必要条件。组件是不需要向组件管理器注册URL的,注册了URL之后,会造成不必要的内存常驻。注册URL的目的其实是一个服务发现的过程,在iOS领域中,服务发现的方式是不需要通过主动注册的,使用runtime就可以了。另外,注册部分的代码的维护是一个相对麻烦的事情,每一次支持新调用时,都要去维护一次注册列表。如果有调用被弃用了,是经常会忘记删项目的。runtime由于不存在注册过程,那就也不会产生维护的操作,维护成本就降低了。 由于通过runtime做到了服务的自动发现,拓展调用接口的任务就仅在于各自的模块,任何一次新接口添加,新业务添加,都不必去主工程做操作,十分透明。
- 在iOS领域里,一定是组件化的中间件为openURL提供服务,而不是openURL方式为组件化提供服务。如果在给App实施组件化方案的过程中是基于openURL的方案的话,有一个致命缺陷:非常规对象(不能被字符串化到URL中的对象,例如UIImage)无法参与本地组件间调度。 在本地调用中使用URL的方式其实是不必要的,如果业务工程师在本地间调度时需要给出URL,那么就不可避免要提供params,在调用时要提供哪些params是业务工程师很容易懵逼的地方。
- 为了支持传递非常规参数,蘑菇街的方案采用了protocol,这个会侵入业务。由于业务中的某个对象需要被调用,因此必须要符合某个可被调用的protocol,然而这个protocol又不存在于当前业务领域,于是当前业务就不得不依赖public Protocol。这对于将来的业务迁移是有非常大的影响的。
CTMediator的优点:
- 调用时,区分了本地应用调用和远程应用调用。本地应用调用为远程应用调用提供服务。
- 组件仅通过Action暴露可调用接口,模块与模块之间的接口被固化在了Target-Action这一层,避免了实施组件化的改造过程中,对Business的侵入,同时也提高了组件化接口的可维护性。
- 方便传递各种类型的参数。
4.基于CTMediator的组件化方案,有哪些核心组成?
- CTMediator中间件:集成就可以了
- 模块Target_%@:模块的实现及提供对外的方法调用Action_methodName,需要传参数时,都统一以NSDictionary的形式传入
- CTMediator+%@扩展:扩展里声明了模块业务的对外接口,参数明确,这样外部调用者可以很容易理解如何调用接口。
内存管理
1.什么情况使用weak关键字,相比assign有什么不同?
- 什么情况使用 weak 关键字?
在 ARC 中,在有可能出现循环引用的时候,往往要通过让其中一端使用 weak 来解决,比如: delegate 代理属性
自身已经对它进行一次强引用,没有必要再强引用一次,此时也会使用 weak,自定义 IBOutlet 控件属性一般也使用 weak;当然,也可以使用strong。在下文也有论述:《IBOutlet连出来的视图属性为什么可以被设置成weak?》
- 不同点:
weak 此特质表明该属性定义了一种“非拥有关系” (nonowning relationship)。为这种属性设置新值时,设置方法既不保留新值,也不释放旧值。此特质同assign类似, 然而在属性所指的对象遭到摧毁时,属性值也会清空(nil out)。 而 assign 的“设置方法”只会执行针对“纯量类型” (scalar type,例如 CGFloat 或 NSlnteger 等)的简单赋值操作。
assign 可以用非 OC 对象,而 weak 必须用于 OC 对象
2.如何让自己的类用copy修饰符?如何重写带copy关键字的setter?
- 若想令自己所写的对象具有拷贝功能,则需实现 NSCopying 协议。如果自定义的对象分为可变版本与不可变版本,那么就要同时实现 NSCopying 与 NSMutableCopying 协议。
具体步骤:
需声明该类遵从 NSCopying 协议
实现 NSCopying 协议。该协议只有一个方法:
- (id)copyWithZone:(NSZone *)zone;
1
注意:一提到让自己的类用 copy 修饰符,我们总是想覆写copy方法,其实真正需要实现的却是 “copyWithZone” 方法。
- (void)setName:(NSString *)name {
//[_name release];
_name = [name copy];
}
3.深拷贝与浅拷贝
浅拷贝只是对指针的拷贝,拷贝后两个指针指向同一个内存空间,深拷贝不但对指针进行拷贝,而且对指针指向的内容进行拷贝,经深拷贝后的指针是指向两个不同地址的指针。
当对象中存在指针成员时,除了在复制对象时需要考虑自定义拷贝构造函数,还应该考虑以下两种情形:
- 当函数的参数为对象时,实参传递给形参的实际上是实参的一个拷贝对象,系统自动通过拷贝构造函数实现;
- 当函数的返回值为一个对象时,该对象实际上是函数内对象的一个拷贝,用于返回函数调用处。
copy方法:如果是非可扩展类对象,则是浅拷贝。如果是可扩展类对象,则是深拷贝。
mutableCopy方法:无论是可扩展类对象还是不可扩展类对象,都是深拷贝。
@property的本质是什么?ivar、getter、setter是如何生成并添加到这个类中的
- @property 的本质是实例变量(ivar)+存取方法(access method = getter + setter),即 @property = ivar + getter + setter;
“属性” (property)作为 Objective-C 的一项特性,主要的作用就在于封装对象中的数据。 Objective-C 对象通常会把其所需要的数据保存为各种实例变量。实例变量一般通过“存取方法”(access method)来访问。其中,“获取方法” (getter)用于读取变量值,而“设置方法” (setter)用于写入变量值。
- ivar、getter、setter 是自动合成这个类中的
完成属性定义后,编译器会自动编写访问这些属性所需的方法,此过程叫做“自动合成”(autosynthesis)。需要强调的是,这个过程由编译 器在编译期执行,所以编辑器里看不到这些“合成方法”(synthesized method)的源代码。除了生成方法代码 getter、setter 之外,编译器还要自动向类中添加适当类型的实例变量,并且在属性名前面加下划线,以此作为实例变量的名字。在前例中,会生成两个实例变量,其名称分别为 _firstName 与 _lastName。也可以在类的实现代码里通过 @synthesize 语法来指定实例变量的名字.
@protocol和category中如何使用@property
- 在 protocol 中使用 property 只会生成 setter 和 getter 方法声明,我们使用属性的目的,是希望遵守我协议的对象能实现该属性
- category 使用 @property 也是只会生成 setter 和 getter 方法的声明,如果我们真的需要给 category 增加属性的实现,需要借助于运行时的两个函数:objc_setAssociatedObject和objc_getAssociatedObject
6.简要说一下@autoreleasePool的数据结构??
简单说是双向链表,每张链表头尾相接,有 parent、child指针
每创建一个池子,会在首部创建一个 哨兵 对象,作为标记
最外层池子的顶端会有一个next指针。当链表容量满了,就会在链表的顶端,并指向下一张表。
BAD_ACCESS在什么情况下出现?
访问了悬垂指针,比如对一个已经释放的对象执行了release、访问已经释放对象的成员变量或者发消息。 死循环
8.使用CADisplayLink、NSTimer有什么注意点?
CADisplayLink、NSTimer会造成循环引用,可以使用YYWeakProxy或者为CADisplayLink、NSTimer添加block方法解决循环引用
9.iOS内存分区情况
- 栈区(Stack)
由编译器自动分配释放,存放函数的参数,局部变量的值等
栈是向低地址扩展的数据结构,是一块连续的内存区域
- 堆区(Heap)
由程序员分配释放
是向高地址扩展的数据结构,是不连续的内存区域
- 全局区
全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域,未初始化的全局变量和未初始化的静态变量在相邻的另一块区域
程序结束后由系统释放
- 常量区
常量字符串就是放在这里的
程序结束后由系统释放
代码区
存放函数体的二进制代码
- 注:
在 iOS 中,堆区的内存是应用程序共享的,堆中的内存分配是系统负责的
系统使用一个链表来维护所有已经分配的内存空间(系统仅仅记录,并不管理具体的内容)
变量使用结束后,需要释放内存,OC 中是判断引用计数是否为 0,如果是就说明没有任何变量使用该空间,那么系统将其回收
当一个 app 启动后,代码区、常量区、全局区大小就已经固定,因此指向这些区的指针不会产生崩溃性的错误。而堆区和栈区是时时刻刻变化的(堆的创建销毁,栈的弹入弹出),所以当使用一个指针指向这个区里面的内存时,一定要注意内存是否已经被释放,否则会产生程序崩溃(也即是野指针报错)
10.循环引用
循环引用的实质:多个对象相互之间有强引用,不能释放让系统回收。
如何解决循环引用?
-
1.避免产生循环引用,通常是将 strong 引用改为 weak 引用。 比如在修饰属性时用weak 在block内调用对象方法时,使用其弱引用,这里可以使用两个宏
#define WS(weakSelf) __weak __typeof(&*self)weakSelf = self; // 弱引用
#define ST(strongSelf) __strong __typeof(&*self)strongSelf = weakSelf; //使用这个要先声明weakSelf 还可以使用__block来修饰变量 在MRC下,__block不会增加其引用计数,避免了循环引用 在ARC下,__block修饰对象会被强引用,无法避免循环引用,需要手动解除。
-
2.在合适时机去手动断开循环引用。 通常我们使用第一种。
-
代理(delegate)循环引用属于相互循环引用
delegate 是iOS中开发中比较常遇到的循环引用,一般在声明delegate的时候都要使用弱引用 weak,或者assign,当然怎么选择使用assign还是weak,MRC的话只能用assign,在ARC的情况下最好使用weak,因为weak修饰的变量在释放后自动指向nil,防止野指针存在
-
NSTimer循环引用属于相互循环使用
在控制器内,创建NSTimer作为其属性,由于定时器创建后也会强引用该控制器对象,那么该对象和定时器就相互循环引用了。 如何解决呢? 这里我们可以使用手动断开循环引用: 如果是不重复定时器,在回调方法里将定时器invalidate并置为nil即可。 如果是重复定时器,在合适的位置将其invalidate并置为nil即可
-
3.block循环引用
一个简单的例子:
@property (copy, nonatomic) dispatch_block_t myBlock;
@property (copy, nonatomic) NSString *blockString;
- (void)testBlock {
self.myBlock = ^() {
NSLog(@"%@",self.blockString);
};
}
由于block会对block中的对象进行持有操作,就相当于持有了其中的对象,而如果此时block中的对象又持有了该block,则会造成循环引用。 解决方案就是使用__weak修饰self即可
__weak typeof(self) weakSelf = self;
self.myBlock = ^() {
NSLog(@"%@",weakSelf.blockString);
};
并不是所有block都会造成循环引用。 只有被强引用了的block才会产生循环引用 而比如dispatch_async(dispatch_get_main_queue(), ^{}),[UIView animateWithDuration:1 animations:^{}]这些系统方法等 或者block并不是其属性而是临时变量,即栈block
[self testWithBlock:^{
NSLog(@"%@",self);
}];
- (void)testWithBlock:(dispatch_block_t)block {
block();
}
还有一种场景,在block执行开始时self对象还未被释放,而执行过程中,self被释放了,由于是用weak修饰的,那么weakSelf也被释放了,此时在block里访问weakSelf时,就可能会发生错误(向nil对象发消息并不会崩溃,但也没任何效果)。 对于这种场景,应该在block中对 对象使用__strong修饰,使得在block期间对 对象持有,block执行结束后,解除其持有。
__weak typeof(self) weakSelf = self;
self.myBlock = ^() {
__strong __typeof(self) strongSelf = weakSelf;
[strongSelf test];
};
<<<<<<< HEAD
数据存储
1.iOS 开发中数据持久性有哪几种?
iOS本地数据保存有多种方式,比如NSUserDefaults、归档、文件保存、数据库、CoreData、KeyChain(钥匙串)等多种方式。其中KeyChain(钥匙串)是保存到沙盒范围以外的地方,也就是与沙盒无关。
2.FMDB数据结构变化升级
- 使用columnExists:inTableWithName方法判断数据表中是否存在字段
- 如果不存在,则添加, 如:向bbb表中添加aaa字段 -> ALTER TABLE bbb ADD ‘aaa’ TEXT
多线程
1.进程与线程
-
进程:
1.进程是一个具有一定独立功能的程序关于某次数据集合的一次运行活动,它是操作系统分配资源的基本单元.
2.进程是指在系统中正在运行的一个应用程序,就是一段程序的执行过程,我们可以理解为手机上的一个app.
3.每个进程之间是独立的,每个进程均运行在其专用且受保护的内存空间内,拥有独立运行所需的全部资源
-
线程
1.程序执行流的最小单元,线程是进程中的一个实体.
2.一个进程要想执行任务,必须至少有一条线程.应用程序启动的时候,系统会默认开启一条线程,也就是主线程
-
进程和线程的关系
1.线程是进程的执行单元,进程的所有任务都在线程中执行
2.线程是 CPU 分配资源和调度的最小单位
3.一个程序可以对应多个进程(多进程),一个进程中可有多个线程,但至少要有一条线程
4.同一个进程内的线程共享进程资源
2.什么是多线程?
- 多线程的实现原理:事实上,同一时间内单核的CPU只能执行一个线程,多线程是CPU快速的在多个线程之间进行切换(调度),造成了多个线程同时执行的假象。
- 如果是多核CPU就真的可以同时处理多个线程了。
- 多线程的目的是为了同步完成多项任务,通过提高系统的资源利用率来提高系统的效率。
3.多线程的优点和缺点
4.多线程的 并行 和 并发 有什么区别?
- 并行:充分利用计算机的多核,在多个线程上同步进行
- 并发:在一条线程上通过快速切换,让人感觉在同步进行
5.iOS中实现多线程的几种方案,各自有什么特点?
- NSThread 面向对象的,需要程序员手动创建线程,但不需要手动销毁。子线程间通信很难。
- GCD c语言,充分利用了设备的多核,自动管理线程生命周期。比NSOperation效率更高。
- NSOperation 基于gcd封装,更加面向对象,比gcd多了一些功能。
6.多个网络请求完成后执行下一步
- 使用GCD的dispatch_group_t
创建一个dispatch_group_t
每次网络请求前先dispatch_group_enter,请求回调后再dispatch_group_leave,enter和leave必须配合使用,有几次enter就要有几次leave,否则group会一直存在。
当所有enter的block都leave后,会执行dispatch_group_notify的block。
NSString *str = @"http://xxxx.com/";
NSURL *url = [NSURL URLWithString:str];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
NSURLSession *session = [NSURLSession sharedSession];
dispatch_group_t downloadGroup = dispatch_group_create();
for (int i=0; i<10; i++) {
dispatch_group_enter(downloadGroup);
NSURLSessionDataTask *task = [session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
NSLog(@"%d---%d",i,i);
dispatch_group_leave(downloadGroup);
}];
[task resume];
}
dispatch_group_notify(downloadGroup, dispatch_get_main_queue(), ^{
NSLog(@"end");
});
- 使用GCD的信号量dispatch_semaphore_t
dispatch_semaphore信号量为基于计数器的一种多线程同步机制。如果semaphore计数大于等于1,计数-1,返回,程序继续运行。如果计数为0,则等待。dispatch_semaphore_signal(semaphore)为计数+1操作,dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER)为设置等待时间,这里设置的等待时间是一直等待。
创建semaphore为0,等待,等10个网络请求都完成了,dispatch_semaphore_signal(semaphore)为计数+1,然后计数-1返回
NSString *str = @"http://xxxx.com/";
NSURL *url = [NSURL URLWithString:str];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
NSURLSession *session = [NSURLSession sharedSession];
dispatch_semaphore_t sem = dispatch_semaphore_create(0);
for (int i=0; i<10; i++) {
NSURLSessionDataTask *task = [session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
NSLog(@"%d---%d",i,i);
count++;
if (count==10) {
dispatch_semaphore_signal(sem);
count = 0;
}
}];
[task resume];
}
dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"end");
});
7.多个网络请求顺序执行后执行下一步
- 使用信号量semaphore
每一次遍历,都让其dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER),这个时候线程会等待,阻塞当前线程,直到dispatch_semaphore_signal(sem)调用之后
NSString *str = @"http://www.xxx.com";
NSURL *url = [NSURL URLWithString:str];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
NSURLSession *session = [NSURLSession sharedSession];
dispatch_semaphore_t sem = dispatch_semaphore_create(0);
for (int i=0; i<10; i++) {
NSURLSessionDataTask *task = [session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
NSLog(@"%d---%d",i,i);
dispatch_semaphore_signal(sem);
}];
[task resume];
dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
}
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"end");
});
8.异步操作两组数据时, 执行完第一组之后, 才能执行第二组
- 这里使用dispatch_barrier_async栅栏方法即可实现
dispatch_queue_t queue = dispatch_queue_create("test", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
NSLog(@"第一次任务的主线程为: %@", [NSThread currentThread]);
});
dispatch_async(queue, ^{
NSLog(@"第二次任务的主线程为: %@", [NSThread currentThread]);
});
dispatch_barrier_async(queue, ^{
NSLog(@"第一次任务, 第二次任务执行完毕, 继续执行");
});
dispatch_async(queue, ^{
NSLog(@"第三次任务的主线程为: %@", [NSThread currentThread]);
});
dispatch_async(queue, ^{
NSLog(@"第四次任务的主线程为: %@", [NSThread currentThread]);
});
9.多线程中的死锁?
死锁是由于多个线程(进程)在执行过程中,因为争夺资源而造成的互相等待现象,你可以理解为卡主了。产生死锁的必要条件有四个:
- 互斥条件 : 指进程对所分配到的资源进行排它性使用,即在一段时间内某资源只由一个进程占用。如果此时还有其它进程请求资源,则请求者只能等待,直至占有资源的进程用毕释放。
- 请求和保持条件 : 指进程已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其它进程占有,此时请求进程阻塞,但又对自己已获得的其它资源保持不放。
- 不可剥夺条件 : 指进程已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放。
- 环路等待条件 : 指在发生死锁时,必然存在一个进程——资源的环形链,即进程集合{P0,P1,P2,···,Pn}中的P0正在等待一个P1占用的资源;P1正在等待P2占用的资源,……,Pn正在等待已被P0占用的资源。
最常见的就是 同步函数 + 主队列 的组合,本质是队列阻塞。
dispatch_sync(dispatch_get_main_queue(), ^{
NSLog(@"2");
});
NSLog(@"1");
// 什么也不会打印,直接报错
10.GCD执行原理?
- GCD有一个底层线程池,这个池中存放的是一个个的线程。之所以称为“池”,很容易理解出这个“池”中的线程是可以重用的,当一段时间后这个线程没有被调用胡话,这个线程就会被销毁。注意:开多少条线程是由底层线程池决定的(线程建议控制再3~5条),池是系统自动来维护,不需要我们程序员来维护(看到这句话是不是很开心?) 而我们程序员需要关心的是什么呢?我们只关心的是向队列中添加任务,队列调度即可。
- 如果队列中存放的是同步任务,则任务出队后,底层线程池中会提供一条线程供这个任务执行,任务执行完毕后这条线程再回到线程池。这样队列中的任务反复调度,因为是同步的,所以当我们用currentThread打印的时候,就是同一条线程。
- 如果队列中存放的是异步的任务,(注意异步可以开线程),当任务出队后,底层线程池会提供一个线程供任务执行,因为是异步执行,队列中的任务不需等待当前任务执行完毕就可以调度下一个任务,这时底层线程池中会再次提供一个线程供第二个任务执行,执行完毕后再回到底层线程池中。
- 这样就对线程完成一个复用,而不需要每一个任务执行都开启新的线程,也就从而节约的系统的开销,提高了效率。在iOS7.0的时候,使用GCD系统通常只能开58条线程,iOS8.0以后,系统可以开启很多条线程,但是实在开发应用中,建议开启线程条数:35条最为合理。
动画
图像处理
Runtime
RunLoop
网络
算法
项目框架
设计模式
数据安全及加密
调试技巧
性能优化
源码理解
代码管理
持续集成
逆向(窃取密码,防护破解,算法加密)
参考:
https://ios.nobady.cn/Multi-thread.html