IOS面试整理

1、无序数组中的中位数

  • //求一个无序数组的中位数
    int findMedian(int a[], int aLen)
    {
    int low = 0;
    int high = aLen - 1;
    int mid = (aLen - 1) / 2;
    int div = PartSort(a, low, high);
    ​
    while (div != mid)
    {
    if (mid < div)
    {
      //左半区间找
      div = PartSort(a, low, div - 1);
    }
    else
    {
      //右半区间找
      div = PartSort(a, div + 1, high);
    }
    }
    //找到了
    return a[mid];
    }
int PartSort(int a[], int start, int end)
{
  int low = start;
  int high = end;
  //选取关键字
int key = a[end];
​
while (low < high)
{
  //左边找比key大的值
  while (low < high && a[low] <= key)
  {
      ++low;
  }
   
  //右边找比key小的值
  while (low < high && a[high] >= key)
  {
      --high;
  }
   
  if (low < high)
  {
      //找到之后交换左右的值
      int temp = a[low];
      a[low] = a[high];
      a[high] = temp;
  }
}
​
int temp = a[high];
a[high] = a[end];
a[end] = temp;
​
return low;
}

2、id 和 instanceType 有什么区别?

相同点

instancetype 和 id 都是万能指针,指向对象。

不同点:

1.id 在编译的时候不能判断对象的真实类型,instancetype 在编译的时候可以判断对象的真实类型。

2.id 可以用来定义变量,可以作为返回值类型,可以作为形参类型;instancetype 只能作为返回值类型。

3、[self class]和[super class]的区别

当使用[self class]时,这时的self是Son,在使用objc_msgSend时,第一个参数是receiver也就是self,也是 Son* son这个实例。第二个参数,要先找到class这个方法的selector,先从Son这个类开始找,没有,然后到Son的父类 Father中去找,也没有,再去Father的父类NSObject去找,一层一层向上找之后,在NSObject的类中发现这个class方法,而 NSObject的这个class方法,就是返回receiver的类别,所以这里输出Son。

当使用[super class]时,这时要转换成objc_msgSendSuper的方法。先构造objc_super的结构体吧,第一个成员变量就是self, 第二个成员变量是Father,然后要找class这个selector,先去superClass也就是Father中去找,没有,然后去Father 的父类中去找,结果还是在NSObject中找到了。然后内部使用函数objc_msgSend(objc_super->receiver, @selector(class)) 去调用,此时已经和我们用[self class]调用时相同了,此时的receiver还是Son* son,所以这里返回的也是Son

4、self和super的区别

  • self调用自己方法,super调用父类方法

  • self是类,super是预编译指令

  • [self class] 和 [super class] 输出是一样的

  • self和super底层实现原理

    1.当使用 self 调用方法时,会从当前类的方法列表中开始找,如果没有,就从父类中再找;

    而当使用 super 时,则从父类的方法列表中开始找,然后调用父类的这个方法

    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;
    };

    5、struct和class的区别

    • 类: 引用类型(位于栈上面的指针(引用)和位于堆上的实体对象)

    • 结构:值类型(实例直接位于栈中)

    6、setNeedsDisplay 和 layoutIfNeeded 两者是什么关系?

    UIView的setNeedsDisplay和setNeedsLayout两个方法都是异步执行的。而setNeedsDisplay会自动调用drawRect方法,这样可以拿到UIGraphicsGetCurrentContext进行绘制;而setNeedsLayout会默认调用layoutSubViews,给当前的视图做了标记;layoutIfNeeded 查找是否有标记,如果有标记及立刻刷新。

    只有setNeedsLayout和layoutIfNeeded这二者合起来使用,才会起到立刻刷新的效果。

7、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]方法。

8、keyWindow 和 delegate的window有何区别

  • delegate.window 程序启动时设置的window对象。

  • keyWindow 这个属性保存了[windows]数组中的[UIWindow]对象,该对象最近被发送了[makeKeyAndVisible]消息

一般情况下 delegate.window 和 keyWindow 是同一个对象,但不能保证keyWindow就是delegate.window,因为keyWindow会因为makeKeyAndVisible而变化,例如,程序中添加了一个悬浮窗口,这个时候keywindow就会变化。

9、说一下 JS 和 OC 互相调用的几种方式?

  • js调用oc的三种方式:

    根据网页重定向截取字符串通过url scheme判断

    替换方法.context[@"copyText"]

    注入对象:遵守协议JSExport,设置context[@

  • oc调用js代码两种方式

    通过webVIew调用 webView stringByEvaluatingJavaScriptFromString: 调用

    通过JSContext调用[context evaluateScript:];

10、如何让自己的类用copy修饰符?如何重写带copy关键字的setter?

  • 若想令自己所写的对象具有拷贝功能,则需实现 NSCopying 协议。如果自定义的对象分为可变版本与不可变版本,那么就要同时实现 NSCopying 与 NSMutableCopying 协议。

    具体步骤:

    需声明该类遵从 NSCopying 协议

    实现 NSCopying 协议。该协议只有一个方法:

    - (id)copyWithZone:(NSZone *)zone;

    注意:一提到让自己的类用 copy 修饰符,我们总是想覆写copy方法,其实真正需要实现的却是 “copyWithZone” 方法。

  • 重写带 copy 关键字的 setter,例如:

    - (void)setName:(NSString *)name {
        //[_name release];
        _name = [name copy];
    }

11、深拷贝与浅拷贝

浅拷贝只是对指针的拷贝,拷贝后两个指针指向同一个内存空间,深拷贝不但对指针进行拷贝,而且对指针指向的内容进行拷贝,经深拷贝后的指针是指向两个不同地址的指针。

当对象中存在指针成员时,除了在复制对象时需要考虑自定义拷贝构造函数,还应该考虑以下两种情形:

  • 当函数的参数为对象时,实参传递给形参的实际上是实参的一个拷贝对象,系统自动通过拷贝构造函数实现;

  • 当函数的返回值为一个对象时,该对象实际上是函数内对象的一个拷贝,用于返回函数调用处。

copy方法:如果是非可扩展类对象,则是浅拷贝。如果是可扩展类对象,则是深拷贝。

mutableCopy方法:无论是可扩展类对象还是不可扩展类对象,都是深拷贝。

12、@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 语法来指定实例变量的名字.

13、@protocol和category中如何使用@property

  • 在 protocol 中使用 property 只会生成 setter 和 getter 方法声明,我们使用属性的目的,是希望遵守我协议的对象能实现该属性

  • category 使用 @property 也是只会生成 setter 和 getter 方法的声明,如果我们真的需要给 category 增加属性的实现,需要借助于运行时的两个函数:objc_setAssociatedObject和objc_getAssociatedObject

    简述 Category 的实现原理

    我们知道 Objective-C 通过 Runtime 运行时来实现动态语言这个特性,所有的类和对象,在 Runtime 中都是用结构体来表示的,Category 在 Runtime 中是用结构体 category_t 来表示的,下面是结构体 category_t 具体表示:

    typedef struct category_t {
        const char *name;//类的名字 主类名字
        classref_t cls;//类
        struct method_list_t *instanceMethods;//实例方法的列表
        struct method_list_t *classMethods;//类方法的列表
        struct protocol_list_t *protocols;//所有协议的列表
        struct property_list_t *instanceProperties;//添加的所有属性
    } category_t;

    通过结构体 category_t 可以知道,在 Category 中我们可以增加实例方法、类方法、协议、属性。我们这里简述下 Category 的实现原理:

    1. 在编译时期,会将分类中实现的方法生成一个结构体 method_list_t 、将声明的属性生成一个结构体 property_list_t ,然后通过这些结构体生成一个结构体 category_t 。

  1. 然后将结构体 category_t 保存下来

    1. 在运行时期,Runtime 会拿到编译时期我们保存下来的结构体 category_t

    2. 然后将结构体 category_t 中的实例方法列表、协议列表、属性列表添加到主类中

    3. 将结构体 category_t 中的类方法列表、协议列表添加到主类的 metaClass 中

    这里需要注意的是:category_t 中的方法列表是插入到主类的方法列表前面(类似利用链表中的 next 指针来进行插入),所以这里 Category 中实现的方法并不会真正的覆盖掉主类中的方法,只是将 Category 的方法插到方法列表的前面去了。运行时在查找方法的时候是顺着方法列表的顺序查找的,它只要一找到对应名字的方法,就会停止查找,这里就会出现覆盖方法的这种假象了。

    // 这里大概就类似这样子插入
    newproperties->next = cls->data()->properties;
    cls->data()->properties = newproperties;,

    通过上面的简述,我们大概了解了 Category 的实现原理,就可以知道 Extension 跟 Category 是两种实现模式,一个是在编译时期实现的,一个是在运行时期决定的。

14、iOS内存分区情况

  • 栈区(Stack)

    由编译器自动分配释放,存放函数的参数,局部变量的值等

    栈是向低地址扩展的数据结构,是一块连续的内存区域

  • 堆区(Heap)

    由程序员分配释放

    是向高地址扩展的数据结构,是不连续的内存区域

  • 全局区

    全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域,未初始化的全局变量和未初始化的静态变量在相邻的另一块区域

    程序结束后由系统释放

  • 常量区

    常量字符串就是放在这里的

    程序结束后由系统释放

  • 代码区

    存放函数体的二进制代码

  • 注:

    • 在 iOS 中,堆区的内存是应用程序共享的,堆中的内存分配是系统负责的

    • 系统使用一个链表来维护所有已经分配的内存空间(系统仅仅记录,并不管理具体的内容)

    • 变量使用结束后,需要释放内存,OC 中是判断引用计数是否为 0,如果是就说明没有任何变量使用该空间,那么系统将其回收

    • 当一个 app 启动后,代码区、常量区、全局区大小就已经固定,因此指向这些区的指针不会产生崩溃性的错误。而堆区和栈区是时时刻刻变化的(堆的创建销毁,栈的弹入弹出),所以当使用一个指针指向这个区里面的内存时,一定要注意内存是否已经被释放,否则会产生程序崩溃(也即是野指针报错)

15、iOS内存管理方式

  • Tagged Pointer(小对象)

    Tagged Pointer 专门用来存储小的对象,例如 NSNumber 和 NSDate

    Tagged Pointer 指针的值不再是地址了,而是真正的值。所以,实际上它不再是一个对象了,它只是一个披着对象皮的普通变量而已。所以,它的内存并不存储在堆中,也不需要 malloc 和 free

    在内存读取上有着 3 倍的效率,创建时比以前快 106 倍

    objc_msgSend 能识别 Tagged Pointer,比如 NSNumber 的 intValue 方法,直接从指针提取数据

    使用 Tagged Pointer 后,指针内存储的数据变成了 Tag + Data,也就是将数据直接存储在了指针中

  • NONPOINTER_ISA (指针中存放与该对象内存相关的信息) 苹果将 isa 设计成了联合体,在 isa 中存储了与该对象相关的一些内存的信息,原因也如上面所说,并不需要 64 个二进制位全部都用来存储指针。

    isa 的结构:

    // x86_64 架构
    struct {
        uintptr_t nonpointer        : 1;  // 0:普通指针,1:优化过,使用位域存储更多信息
        uintptr_t has_assoc         : 1;  // 对象是否含有或曾经含有关联引用
        uintptr_t has_cxx_dtor      : 1;  // 表示是否有C++析构函数或OC的dealloc
        uintptr_t shiftcls          : 44; // 存放着 Class、Meta-Class 对象的内存地址信息
        uintptr_t magic             : 6;  // 用于在调试时分辨对象是否未完成初始化
        uintptr_t weakly_referenced : 1;  // 是否被弱引用指向
        uintptr_t deallocating      : 1;  // 对象是否正在释放
        uintptr_t has_sidetable_rc  : 1;  // 是否需要使用 sidetable 来存储引用计数
        uintptr_t extra_rc          : 8;  // 引用计数能够用 8 个二进制位存储时,直接存储在这里
    };
    
    // arm64 架构
    struct {
        uintptr_t nonpointer        : 1;  // 0:普通指针,1:优化过,使用位域存储更多信息
        uintptr_t has_assoc         : 1;  // 对象是否含有或曾经含有关联引用
        uintptr_t has_cxx_dtor      : 1;  // 表示是否有C++析构函数或OC的dealloc
        uintptr_t shiftcls          : 33; // 存放着 Class、Meta-Class 对象的内存地址信息
        uintptr_t magic             : 6;  // 用于在调试时分辨对象是否未完成初始化
        uintptr_t weakly_referenced : 1;  // 是否被弱引用指向
        uintptr_t deallocating      : 1;  // 对象是否正在释放
        uintptr_t has_sidetable_rc  : 1;  // 是否需要使用 sidetable 来存储引用计数
        uintptr_t extra_rc          : 19;  // 引用计数能够用 19 个二进制位存储时,直接存储在这里
    };

    这里的 has_sidetable_rc 和 extra_rc,has_sidetable_rc 表明该指针是否引用了 sidetable 散列表,之所以有这个选项,是因为少量的引用计数是不会直接存放在 SideTables 表中的,对象的引用计数会先存放在 extra_rc 中,当其被存满时,才会存入相应的 SideTables 散列表中,SideTables 中有很多张 SideTable,每个 SideTable 也都是一个散列表,而引用计数表就包含在 SideTable 之中。

  • 散列表(引用计数表、弱引用表)

    引用计数要么存放在 isa 的 extra_rc 中,要么存放在引用计数表中,而引用计数表包含在一个叫 SideTable 的结构中,它是一个散列表,也就是哈希表。而 SideTable 又包含在一个全局的 StripeMap 的哈希映射表中,这个表的名字叫 SideTables。

    当一个对象访问 SideTables 时:

    • 首先会取得对象的地址,将地址进行哈希运算,与 SideTables 中 SideTable 的个数取余,最后得到的结果就是该对象所要访问的 SideTable

    • 在取得的 SideTable 中的 RefcountMap 表中再进行一次哈希查找,找到该对象在引用计数表中对应的位置

    • 如果该位置存在对应的引用计数,则对其进行操作,如果没有对应的引用计数,则创建一个对应的 size_t 对象,其实就是一个 uint 类型的无符号整型

    弱引用表也是一张哈希表的结构,其内部包含了每个对象对应的弱引用表 weak_entry_t,而 weak_entry_t 是一个结构体数组,其中包含的则是每一个对象弱引用的对象所对应的弱引用指针。

16、KVC实现原理

  • KVC,键-值编码,使用字符串直接访问对象的属性。

  • 底层实现,当一个对象调用setValue方法时,方法内部会做以下操作:

    1.检查是否存在相应key的set方法,如果存在,就调用set方法

    2.如果set方法不存在,就会查找与key相同名称并且带下划线的成员属性,如果有,则直接给成员属性赋值

    3.如果没有找到_key,就会查找相同名称的属性key,如果有就直接赋值

    4.如果还没找到,则调用valueForUndefinedKey:和setValue:forUndefinedKey:方法

17、KVO的实现原理

KVO

KVO-键值观察机制,原理如下:

  • 1.当给A类添加KVO的时候,runtime动态的生成了一个子类NSKVONotifying_A,让A类的isa指针指向NSKVONotifying_A类,重写class方法,隐藏对象真实类信息

  • 2.重写监听属性的setter方法,在setter方法内部调用了Foundation 的 _NSSetObjectValueAndNotify 函数

  • 3._NSSetObjectValueAndNotify函数内部

    a) 首先会调用 willChangeValueForKey

    b) 然后给属性赋值

    c) 最后调用 didChangeValueForKey

    d) 最后调用 observer 的 observeValueForKeyPath 去告诉监听器属性值发生了改变 .

  • 4.重写了dealloc做一些 KVO 内存释放

18、如何手动触发KVO方法

  • 手动调用willChangeValueForKey和didChangeValueForKey方法

  • 键值观察通知依赖于 NSObject 的两个方法: willChangeValueForKey: 和 didChangeValueForKey。在一个被观察属性发生改变之前, willChangeValueForKey: 一定会被调用,这就 会记录旧的值。而当改变发生后, didChangeValueForKey 会被调用,继而 observeValueForKey:ofObject:change:context: 也会被调用。如果可以手动实现这些调用,就可以实现“手动触发”了 有人可能会问只调用didChangeValueForKey方法可以触发KVO方法,其实是不能的,因为willChangeValueForKey: 记录旧的值,如果不记录旧的值,那就没有改变一说了

19、block和delegate的区别

  • delegate运行成本低,block的运行成本高

    block出栈需要将使用的数据从栈内存拷贝到堆内存,当然对象的话就是加计数,使用完或者block置nil后才消除。delegate只是保存了一个对象指针,直接回调,没有额外消耗。就像C的函数指针,只多做了一个查表动作。

  • delegate更适用于多个回调方法(3个以上),block则适用于1,2个回调时。

20、Http 和 Https 的区别?Https为什么更加安全?

  • 区别

    1.HTTPS 需要向机构申请 CA 证书,极少免费。

    2.HTTP 属于明文传输,HTTPS基于 SSL/TLS进行加密传输。

    3.HTTP 端口号为 80,HTTPS 端口号为 443 。

    4.HTTPS 是加密传输,有身份验证的环节,更加安全。

  • 安全

    SSL(安全套接层) TLS(传输层安全)

    以上两者在传输层之上,对网络连接进行加密处理,保障数据的完整性,更加的安全。

21、HTTPS的连接建立流程

HTTPS为了兼顾安全与效率,同时使用了对称加密和非对称加密。在传输的过程中会涉及到三个密钥:

  • 服务器端的公钥和私钥,用来进行非对称加密

  • 客户端生成的随机密钥,用来进行对称加密

    https

如上图,HTTPS连接过程大致可分为八步:

  • 1、客户端访问HTTPS连接。

    客户端会把安全协议版本号、客户端支持的加密算法列表、随机数C发给服务端。

  • 2、服务端发送证书给客户端

    服务端接收密钥算法配件后,会和自己支持的加密算法列表进行比对,如果不符合,则断开连接。否则,服务端会在该算法列表中,选择一种对称算法(如AES)、一种公钥算法(如具有特定秘钥长度的RSA)和一种MAC算法发给客户端。

    服务器端有一个密钥对,即公钥和私钥,是用来进行非对称加密使用的,服务器端保存着私钥,不能将其泄露,公钥可以发送给任何人。

    在发送加密算法的同时还会把数字证书和随机数S发送给客户端

  • 3、客户端验证server证书

    会对server公钥进行检查,验证其合法性,如果发现发现公钥有问题,那么HTTPS传输就无法继续。

  • 4、客户端组装会话秘钥

    如果公钥合格,那么客户端会用服务器公钥来生成一个前主秘钥(Pre-Master Secret,PMS),并通过该前主秘钥和随机数C、S来组装成会话秘钥

  • 5、客户端将前主秘钥加密发送给服务端

    是通过服务端的公钥来对前主秘钥进行非对称加密,发送给服务端

  • 6、服务端通过私钥解密得到前主秘钥

    服务端接收到加密信息后,用私钥解密得到前主秘钥。

  • 7、服务端组装会话秘钥

    服务端通过前主秘钥和随机数C、S来组装会话秘钥。

    至此,服务端和客户端都已经知道了用于此次会话的主秘钥。

  • 8、数据传输

    客户端收到服务器发送来的密文,用客户端密钥对其进行对称解密,得到服务器发送的数据。

    同理,服务端收到客户端发送来的密文,用服务端密钥对其进行对称解密,得到客户端发送的数据。

22、TCP 和 UDP的区别

  • TCP:面向连接、传输可靠(保证数据正确性,保证数据顺序)、用于传输大量数据(流模式)、速度慢,建立连接需要开销较多(时间,系统资源)。

  • UDP:面向非连接、传输不可靠、用于传输少量数据(数据包模式)、速度快。</

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值