iOS面试题精选

作者: 王家小雷 | 来源:发表于2019-02-24 14:29 被阅读5次

    一 基础知识
    1.如何令自己所写的对象具有拷贝功能?

    目的 想让自己创建的类具有copy方法

    第一个返回不可变类型 遵循NSCopying协议

    实现

    @protocol NSCopying

    • (id)copyWithZone:(nullable NSZone *)zone;

    @end

    第二个让自己的类具备mutableCopy方法,并且返回回可变类型,必须遵守NSMutableCopying

    实现

    @protocol NSMutableCopying

    • (id)mutableCopyWithZone:(nullable NSZone *)zone;

    @end

    实例

    .h

    import <Foundation/Foundation.h>

    NS_ASSUME_NONNULL_BEGIN

    @interface Person : NSObject<NSCopying,NSMutableCopying>

    @property(nonatomic,copy) NSString *name;
    @property(nonatomic,copy) NSString *age;
    @end

    NS_ASSUME_NONNULL_END
    .m

    import "Person.h"

    @implementation Person
    -(id)copyWithZone:(NSZone *)zone{
    return self;
    }

    • (id)mutableCopyWithZone:(nullable NSZone *)zone{
      Person *person = [[[self class] allocWithZone:zone] init];
      person.age = self.age;
      person.name=self.name;
      return person;
      }
      @end

    调用

    -(void)test1{
    Person *p = [[Person alloc] init];
    p.name = @"1";
    p.age=@"2";

    Person *p2 = [p copy];
    p2.name=@"2222222";
    NSLog(@"%@",p2.name);
    NSLog(@"%@",p.name);
    Person *p3 = [p mutableCopy];
    p3.name=@"33333";
    NSLog(@"%@",p2.name);
    NSLog(@"%@",p.name);
    NSLog(@"%@",p3.name);
    

    }

    2.****说说你理解****weak****属性?

    weak****实现原理:

    Runtime****维护了一个****weak****表,用于存储指向某个对象的所有****weak****指针。****weak****表其实是一个****hash****(哈希)表,****Key****是所指对象的地址,****Value****是****weak****指针的地址(这个地址的值是所指对象的地址)数组。

    1****、初始化时:****runtime****会调用****objc_initWeak****函数,初始化一个新的****weak****指针指向对象的地址。

    2****、添加引用时:****objc_initWeak****函数会调用 objc_storeWeak() 函数, objc_storeWeak() 的作用是更新指针指向,创建对应的弱引用表。

    3****、释放时,调用****clearDeallocating****函数。****clearDeallocating****函数首先根据对象地址获取所有****weak****指针地址的数组,然后遍历这个数组把其中的数据设为****nil****,最后把这个****entry****从****weak****表中删除,最后清理对象的记录。

    追问的问题一:

    1.****实现****weak****后,为什么对象释放后会自动为****nil****?

    runtime ****对注册的类,****会进行布局,对于**** weak ****对象会放入一个**** hash ****表中。****用**** weak ****指向的对象内存地址作为**** key****,当此对象的引用计数为**** 0 ****的时候会**** dealloc****,假如**** weak ****指向的对象内存地址是**** a ****,那么就会以**** a ****为键,****在这个**** weak ****表中搜索,找到所有以**** a ****为键的**** weak ****对象,从而设置为**** nil ****。

    追问的问题二:

    2.****当****weak****引用指向的对象被释放时,又是如何去处理****weak****指针的呢?

    1****、调用****objc_release

    2****、因为对象的引用计数为****0****,所以执行****dealloc

    3****、在****dealloc****中,调用了****_objc_rootDealloc****函数

    4****、在****_objc_rootDealloc****中,调用了****object_dispose****函数

    5****、调用****objc_destructInstance

    6****、最后调用****objc_clear_deallocating,****详细过程如下:

    a. 从****weak****表中获取废弃对象的地址为键值的记录

    b. 将包含在记录中的所有附有 weak****修饰符变量的地址,赋值为 nil

    c. 将****weak****表中该记录删除

    d. 从引用计数表中删除废弃对象的地址为键值的记录

    3.Swift mutating关键字的使用
    Swift中protocol的功能比OC中强大很多,不仅能再class中实现,同时也适用于struct、enum。
    使用 mutating 关键字修饰方法是为了能在该方法中修改 struct 或是 enum 的变量,在设计接口的时候,也要考虑到使用者程序的扩展性。所以要多考虑使用mutating来修饰方法。

    4.UIView 和CALayer是什么关系?

    1.

    UIView 继承自****UIResponder****类

    CALayer****直接继承 NSObject

    所以****UIView****可以响应事件,****Layer****不可以

    2. UIView****是****CALayer****的****delegate(****CALayerDelegate****)

    3. UIView****主要处理事件,****CALayer****负责绘制就更好

    4. 每个 UIView 内部都有一个 CALayer 在背后提供内容的绘制和显示,并且 UIView****的尺寸样式都由内部的 Layer 所提供。两者都有树状层级结构,****layer 内部有****SubLayers****,****View 内部有 SubViews.****但是 Layer View 多了个****AnchorPoint

    283FEBD0-3424-4E05-9D04-9901CAE0A356.png

    5.@synthesize 和@dynamic 分别有什么用?

    1)@property有两个对应的词,一个是@synthesize,一个是@dynamic。如果@synthesize和@dynamic都没写,那么默认的就是@syntheszie var = _var;
    2)@synthesize的语义是如果你没有手动实现setter方法和getter方法,那么编译器会自动为你加上这两个方法。
    3)@dynamic告诉编译器:属性的setter与getter方法由用户自己实现,不自动生成。(当然对于readonly的属性只需提供getter即可)。假如一个属性被声明为@dynamic var,然后你没有提供@setter方法和@getter方法,编译的时候没问题,但是当程序运行到instance.var = someVar,由于缺setter方法会导致程序崩溃;或者当运行到someVar = var时,由于缺getter方法同样会导致崩溃。编译时没问题,运行时才执行相应的方法,这就是所谓的动态绑定。

    6.动态绑定?

    1. 在objective-c中,一个对象内能调用指定的方法不是由编译器决定而是由运行时决定,这被称作是方法的动态绑定。
    2. 在objective-c里,对象不调用方法,而是接收消息,消息 表达式为: [reciver message];运行时系统首先确定接收者的类型(动态类型识别),然 后根据消息名在类的方法列表里选择相应的方法执行,所 以在源代码里消息也称为选择器(selector)
    3. 消息函数的作用:
      – 首先通过第一个参数的receiver,找到它的isa 指针,然 后在isa 指向的Class 对象中使用第二个参数selector 查 找方法;
      – 如果没有找到,就使用当前Class 对象中的新的isa 指针 到上一级的父类的Class 对象中查找;
      – 当找到方法后,再依据receiver 的中的self 指针找到当前 的对象,调用当前对象的具体实现的方法(IMP),然后传 递参数,调用实现方法。
      – 假如一直找到NSObject 的Class 对象,也没有找到你调 用的方法,就会报告不能识别发送消息的错误。

    7.Category(分类),Extension(扩展)和继承的区别?
    1.分类
    category是在现有类的基础上添加新的方法,利用objective-c 的动态运行时分配机制,可以为现有类添加新方法。可以在分类中添加方法和成员变量,但是添加的成员变量不会自动生成setter和getter方法,需要在实现部分给出实现。
    2.扩展
    iOS中的extension就是匿名的分类,只有头文件没有实现文件。只能扩展方法,不能添加成员变量。扩展的方法只能在原类中实现。例如你扩展NSString,那么你只能在NSString的.m实现(这是不可能的),所以尽量少用扩展。用分类就可以了。
    3.继承
    在iOS中继承是单继承,既只能有一个父类。在继承中,子类可以使用父类的方法和变量,当子类想对本类或者父类的变量进行初始化,那么需要重写init()方法 。父类也可以访问子类的方法和成员变量。

    8.isa?
    一个对象(Object)的isa指向了这个对象的类(Class),而这个对象的类(Class)的isa指向了metaclass。这样我们就可以找到静态方法和变量了。
    Objective-C的运行时是动态的,它能让你在运行时为类添加方法或者去除方法以及使用反射。这在其它语言是不多见的。

    类的实例对象的 isa 指向它的类;类的 isa 指向该类的 metaclass;
    类的 super_class 指向其父类,如果该类为根类则值为 NULL;
    metaclass 的 isa 指向根 metaclass,如果该 metaclass 是根 metaclass则指向自身;
    metaclass 的 super_class 指向父 metaclass,如果该 metaclass 是根 metaclass则指向该 metaclass 对应的类;
    Object-C 为每个类的定义生成两个 objc_class ,一个普通的 class,另一个即metaclass。我们可以在运行期创建这两个 objc_class 数据结构,然后使用 objc_addClass将 class注册到运行时系统中,以此实现动态地创建一个新的类。
    

    9.为什么代理用weak?代理的delegate和dataSource有什么区别?block和代理的区别?
    防止循环引用。例如View有一个协议,需要一个代理实现回调。一个Controller添加这个View,并且遵守协议,成为View的代理。如果不用week,用strong,Controller ->View -> delegate -> Controller,就循环引用了。
    delegate偏重于与用户交互的回调,有那些方法可以供我使用,例如UITableviewDelegate;dataSource偏重于数据的回调,view里面有什么东西,属性都是什么,例如UITableviewDatasource;

    block
    在 iOS中, block一共分三种。
    (1)全局静态 block,不会访问任何外部变量,执行完就销毁。
    ^{
    NSLog(@"Hello World!");
    }();
    (2)保存在栈中的 block,当函数返回时会被销毁,和第一种的区别就是调用了外部变量。
    [UIView animateWithDuration:3 animations:^{
    self.view.backgroundColor = [UIColor redColor];
    }];
    (3)保存在堆中的 block,当引用计数为 0 时会被销毁。例如按钮的点击事件,一直存在,即使执行过,也不销毁,因为按钮还可能被点击。直到持有按钮的View被销毁,它才会被销毁。

    import <UIKit/UIKit.h>

    typedef void(^ButtonClickBlcok)();

    @interface TestView : UIView

    @property (nonatomic, copy) ButtonClickBlcok buttonClickBlcok;

    @end

    import "TestView.h"

    @implementation TestView

    • (IBAction)buttonClick:(id)sender {
      if (self.buttonClickBlcok) {
      self.buttonClickBlcok();
      }
      }
      @end

    block优点
    block的代码可读性更好。因为block只要实现就可以了,而代理需要遵守协议并且实现协议里的方法,而两者还不在一个地方。代理使用起来也更麻烦,因为要声明协议、声明代理属性、遵守协议、实现协议里的方法。block不需要声明,也不需要遵守,只需要声明属性和实现就可以了。
    block是一种轻量级的回调,可以直接访问上下文,由于block的代码是内联的,运行效率更高。block就是一个对象,实现了匿名函数的功能。所以我们可以把block当做一个成员变量、属性、参数使用,使用起来非常灵活。
    block 缺点
    blcok的运行成本高。block出栈需要将使用的数据从栈内存拷贝到堆内存,当然对象的话就是引用计数加1,使用完或者block置nil后才销毁。delegate只是保存了一个对象指针(一定要用week修饰delegate,不然也会循环引用),直接回调,没有额外消耗。就像C的函数指针,只多做了一个查表动作。
    block容易造成循环引用,而且不易察觉。因为为了blcok不被系统回收,所以我们都用copy关键字修饰,实行强引用。block对捕获的变量也都是强引用,所以就会造成循环引用。

    10.id和NSObject的区别?
    1.NSObject包含了一些其他的方法,需要实现NSObject协议,可以用NSObject来表示id,但是不能用id来表示NSObject
    2.id关键字在编译的时候不会被检查,而NSObject在编译的时候被被检查是否含有一些错误的方法
    3.id可以是任何对象,包括不是NSObject的对象
    4.定义id的时候不需要
    ,而定义NSOject的时候需要。

    11.使用系统的某些block api(如UIView的block版本写动画时),是否也要考虑循环问题?
    系统的某些block api中,UIView的block版本写动画时不需要考虑,但也有一些api 需要考虑:
    所谓“引用循环”是指双向的强引用,所以那些“单向的强引用”(block 强引用 self )没有问题,比如这些:
    [UIViewanimateWithDuration:durationanimations:^{ [self.superviewlayoutIfNeeded]; }];
    但如果你使用一些参数中可能含有 ivar 的系统 api ,如 GCD 、NSNotificationCenter就要小心一点:比如GCD 内部如果引用了 self,而且 GCD 的其他参数是 ivar,则要考虑到循环引用:
    weak__typeof(self) weakSelf = self;
    dispatch_group_async(_operationsGroup,_operationsQueue, ^{
    typeof(self) strongSelf = weakSelf;
    [strongSelf doSomething];
    [strongSelf doSomethingElse];
    } );

    weak__typeof(self) weakSelf = self;
    _observer = [[NSNotificationCenter defaultCenter]addObserverForName:@"testKey"object:nilqueue:nilusingBlock:^(NSNotification*note) {
    typeof(self) strongSelf = weakSelf;
    [strongSelf dismissModalViewControllerAnimated:YES];
    }];
    self --> _observer --> block --> self 显然这也是一个循环引用。

    12.用@property声明的NSString(或NSArray,NSDictionary)经常使用copy关键字,为什么?如果改用strong关键字,可能造成什么问题?
    1、因为父类指针可以指向子类对象,使用copy的目的是为了让本对象的属性不受外界影响,使用copy无论给我传入是一个可变对象还是不可对象,我本身持有的就是一个不可变的副本.
    2、如果我们使用是strong,那么这个属性就有可能指向一个可变对象,如果这个可变对象在外部被修改了,那么会影响该属性.

    copy此特质所表达的所属关系与strong类似。然而设置方法并不保留新值,而是将其“拷贝” (copy)。 当属性类型为NSString时,经常用此特质来保护其封装性,因为传递给设置方法的新值有可能指向一个NSMutableString类的实例。这个类是NSString的子类,表示一种可修改其值的字符串,此时若是不拷贝字符串,那么设置完属性之后,字符串的值就可能会在对象不知情的情况下遭人更改。所以,这时就要拷贝一份“不可变” (immutable)的字符串,确保对象中的字符串值不会无意间变动。只要实现属性所用的对象是“可变的” (mutable),就应该在设置新属性值时拷贝一份。

    13.static有什么作用?

    用static声明局部变量,使其变为静态存储方式(静态数据区),作用域不变;用static声明外部变量,其本身就是静态变量,这只会改变其连接方式,使其只在本文件内部有效,而其他文件不可连接或引用该变量。
    2.作用于函数:
    使用static用于函数定义时,对函数的连接方式产生影响,使得函数只在本文件内部有效,对其他文件是不可见的。这样的函数又叫作静态函数。使用静态函数的好处是,不用担心与其他文件的同名函数产生干扰,另外也是对函数本身的一种保护机制。
    如果想要其他文件可以引用本地函数,则要在函数定义时使用关键字extern,表示该函数是外部函数,可供其他文件调用。另外在要引用别的文件中定义的外部函数的文件中,使用extern声明要用的外部函数即可。

    二 底层
    14.main()之前的过程有哪些?
    1.系统先读取App的可执行文件(Mach-O文件),从里面获得dyld的路径,然后加载dyld,dyld去初始化运行环境。
    2.开启缓存策略,加载程序相关依赖库(其中也包含我们的可执行文件),并对这些库进行链接,最后调用每个依赖库的初始化方法,在这一步,runtime被初始化。
    3.当所有依赖库的初始化后,轮到最后一位(程序可执行文件)进行初始化,在这时runtime会对项目中所有类进行类机构初始化,然后调用所有的load方法。最后dyld返回main函数地址,main函数被调用,我们便来到程序入口main函数。

    15.KVO的基本原理?
    1.KVO是通过给对象object的属性property注册observer, 然后在被观察property的值改变时候, 会对observer发送消息的这样一种机制.
    2.实现原理
    当给对象的某个属性注册了一个 observer, 那么这个对象的isa指针指向的class会被改变, 此时系统会创建一个新的中间类(intermediate class)继承原来的class, 然后通过runtime 将原来的isa指针指向这个新的中间类.然后中间类会重写setter方法, 重写的 setter 方法会负责在调用原 setter 方法之前和之后添加willChangeValueForKey:, didChangeValueForKey:两个方法,通知所有观察对象值的更改, 从而触发KVO消息.

    16.Swift下如何使用KVC和KVO?
    1.KVC
    key-value coding
    是一种间接访问对象的机制
    key的值就是属性名称的字符串,返回的value是任意类型,需要自己转化为需要的类型
    KVC主要就是两个方法
    (1)通过key设置对应的属性
    (2)通过key获取对应的属性
    举例
    class TestForKVC:NSObject{
    var hwcCSDN = "hello world"
    }
    var instance = TestForKVC()
    var value = instance.valueForKey("hwcCSDN") as String
    instance.setValue("hello hwc",forKey:"hwcCSDN")

    2.KVO
    key-value observing
    建立在KVC之上的的机制
    主要功能是检测对象属性的变化
    这是一个完善的机制,不需要用户自己设计复杂的观察者模式
    对需要观察的属性要在前面加上dynamic关键字
    举例
    第一步,对要观察的对象的属性加上dynamic关键字
    class ToObserver:NSObject{
    dynamic var hwcDate = NSDate()
    func updateDate(){
    hwcDate = NSDate()
    }
    }
    第二步,声明一个全局的用来区分是哪个被观察属性的变量
    private var mycontext = 0
    第三步,在要观察的类中addObserver,在析构中removeObserver,重写observerValueForKeyPath
    class TestForCSDN:UIViewController{
    var testVariable = ToObserver()
    override func viewDidLoad(){
    super.viewDidLoad()
    testVariable.addObserver(self,forKeyPath:"hwcDate",options:.New,context:&mycontext)
    }
    deinit{
    testVariable.removeObserver(self,forKeyPath:"hwcDate")
    }
    overfide func observeValueForKeyPath(keyPath:String,ofObject:AnyObject,change:[NSObject:AnyObject],context:UnsafeMutablePointer<Void>){
    if(context == &mycontext){
    println("Changed to:(change[NSKeyValueChangeNewKey]!)")
    }
    }
    }
    这样,就可以在函数observeValueForKeyPath检测到变化了

    7.Swift 有哪些模式匹配?
    1.对枚举的匹配:
    2.类型匹配(用到两个关键字 is、as(不是as?))
    3.自定义类型匹配
    我们自定的类型是无法进行模式匹配的,也就是不能在 switch/case 语句中使用。如果想要达到可匹配的效果,那么就有必有了解一下匹配操作符 ~=
    4.关于 Optional 匹配
    当switch传入的值为optional时,如果不想解包,可以使用x?(相当于Optional.some(x))语法糖来匹配可选值。
    总结
    Swift中的匹配模式要比OC中强大的多。归纳起来大概分为以下几点:
    1.除了可以匹配枚举类型外,支持更多的原生类型匹配,包过Int、String、Float、Tuple、Range等
    2.可以对类型进行匹配,配合 as、is 来使用
    3.重载匹配操作符~=,可以支持自定义类型的匹配操作
    4.可结合where、if、guard、for来使用,使得代码简洁优雅而高效
    5.对Optional类型的匹配支持很友好,可简化部分判断逻辑

    18.objc在向一个对象发送消息时,发生了什么?
    根据对象的isa指针找到该对象所属的类,去obj的对应的类中找方法
    1.首先,在相应操作的对象中的缓存方法列表中找调用的方法,如果找到,转向相应实现并执行。
    2.如果没找到,在相应操作的对象中的方法列表中找调用的方法,如果找到,转向相应实现执行
    3.如果没找到,去父类指针所指向的对象中执行1,2.
    4.以此类推,如果一直到根类还没找到,转向拦截调用,走消息转发机制。
    5.如果没有重写拦截调用的方法,程序报错。

    19.静态库的原理是什么?你有没有自己写过静态编译库,遇到了哪些问题?
    1.静态库本质上是一种可执行的二进制格式,可以载入内存中执行。是程序代码的集合,共享代码的一种方式。
    静态库是闭源库,不公开源代码,都是编译后的二进制文件,不暴露具体实现。
    静态库 一般都是以.a或者 .framework形式存在。

    静态库编译的文件比较大,因为整个函数库的数据都会被整合到代码中,这样的好处就是编译后的程序不需要外部的函数库支持,不好的一点就是如果改变静态函数库,就需要程序重新编译,多次使用就有多份冗余拷贝。

    使用静态库的好处-模块化分工合作,可重用,避免少量改动导致大量的重复编译链接

    可能遇到的问题
    1.framework中用到了NSClassFromString,但是转换出来的class 一直为nil。解决方法:在主工程的【Other Linker Flags】需要添加参数【-ObjC]即可。

    2.如果Xcode找不到框架的头文件,你可能是忘记将它们声明为public了。 解决方法:进入target的Build Phases页,展开Copy Headers项,把需要public的头文件从Project或Private部分拖拽到Public部分。

    3.尽量不要用 xib 。由于静态框架采用静态链接,linker会剔除所有它认为无用的代码。不幸的是,linker不会检查xib文件,因此如果类是在xib中引用,而没有在O-C代码中引用,linker将从最终的可执行文件中删除类。这是linker的问题,不是框架的问题(当你编译一个静态库时也会发生这个问题)。苹果内置框架不会发生这个问题,因为他们是运行时动态加载的,存在于iOS设备固件中的动态库是不可能被删除的。
    解决办法
    1、 让框架的最终用户关闭linker的优化选项,通过在他们的项目的Other Linker Flags中添加-ObjC和-all_load。
    2、 在框架的另一个类中加一个该类的代码引用。例如,假设你有个MyTextField类,被linker剔除了。假设你还有一个MyViewController,它在xib中使用了MyTextField,MyViewController并没有被剔除。你应该这样做:
    在MyTextField中:
    +(void)forceLinkerLoad_ {}
    在MyViewController中:
    +(void)initialize {[MyTextField forceLinkerLoad_];}
    他们仍然需要添加-ObjC到linker设置,但不需要强制all_load了。
    第2种方法需要你多做一点工作,但却让最终用户避免在使用你的框架时关闭linker优化(关闭linker优化会导致object文件膨胀)。

    20.runloop是来做什么的?runloop和线程有什么关系?主线程默认开启了runloop吗?子线程呢?
    1.runloop 的作用就是用来管理线程的,当线程的runloop开启后,线程就会在执行完任务后,处于休眠状态,随时等待接受新的任务,而不是退出。
    2.只有主线程的runloop是默认开启的,所以程序在开启后,会一直运行,不会退出,其他线程的runloop如果需要开启,就手动开启。

    runloop内部是如何实现的
    1、有一个判断循环的条件,满足条件,就一直循环
    2、线程得到唤醒事件被唤醒,事件处理完毕以后,回到睡眠状态,等待下次唤醒

    21.不手动指定autoreleasepool的前提下,一个autorealese对象在什么时刻释放?(比如在一个vc的viewDidLoad中创建)
    Autorelease对象出了作用域之后,会被添加到最近一次创建的自动释放池中,并会在当前的 runloop 迭代结束时释放。
    如果在一个vc的viewDidLoad中创建一个 Autorelease对象,那么该对象会在 viewDidAppear 方法执行前就被销毁了。

    22.oc完整的消息转发机制+代码实现
    1.先在本类中搜索该方法的实现,如果有则直接调用若果没有则去父类中搜索直到NSObject,如果NSObject没有则进入消息转发(类的动态方法解析、备用接受者对象、完整的消息转发)。

    2.类的动态方法解析:
    首先创建SonPerson类,在ViewController 里面写
    id person = [[SonPerson alloc]init];
    [person appendString:@""];
    注意这里要用id 不然编译报错。
    在该类和父类中没有找到该方法的实现则会执行
    +(BOOL)resolveClassMethod:(SEL)sel
    或+(BOOL)resolveInstanceMethod:(SEL)sel 方法。
    在+(BOOL)resolveClassMethod:(SEL)sel 或
    +(BOOL)resolveInstanceMethod:(SEL)sel 方法 中利用黑魔法runtime 动态添加方法实现。
    void dynamicAdditionMethodIMP(id self,SEL _cmd){
    NSLog(@"dynamicAdditionMethodIMP");
    }
    +(BOOL)resolveClassMethod:(SEL)sel{
    NSLog(@"resolveInstanceMethod: %@", NSStringFromSelector(sel));
    if(sel ==@selector(appendString:)) {
    class_addMethod([selfclass], sel, (IMP)dynamicAdditionMethodIMP,"v@:");
    returnYES;
    }
    return[superresolveClassMethod:sel];
    }

    BOOL class_addMethod(Class cls, SEL name, IMP imp,constchar*types);
    第一个参数是需要添加方法的类,第二个参数是一个selector,也就是实例方法的名字,第三个参数是一个IMP类型的变量也就是函数实现,需要传入一个C函数,这个函数至少有两个参数,一个是id self一个是SEL _cmd,第四个参数是函数类型。具体设置方法可以看注释。
    控制台输出:
    resolveInstanceMethod: appendString:
    dynamicAdditionMethodIMP

    2.备用接受者: 在+(BOOL)resolveClassMethod:(SEL)sel
    或+(BOOL)resolveInstanceMethod:(SEL)sel 方法返回NO的时候进入备用接受者阶段 。
    创建一个备用接受者类ForwardPerson 实现appendString:方法
    -(void)appendString:(NSString*)str{
    NSLog(@"%@===%@",NSStringFromClass([self class]),NSStringFromSelector(_cmd));
    }
    在SonPerson类中实现- (id)forwardingTargetForSelector:(SEL)aSelector 方法 并返回一个备用接受者对象

    • (id)forwardingTargetForSelector:(SEL)aSelector{
      NSLog(@"forwardingTargetForSelector");

    return [ForwardPerson new];
    }
    控制台输出:
    forwardingTargetForSelector
    ForwardPerson===appendString:

    1. 完整的消息转发:当-(void)forwardInvocation:(NSInvocation*)anInvocation 方法方法nil的时候则进入消息转发的最后阶段,完整的消息转发。也需要创建一个转发对象ForwardInvocation

    import "ForwardInvocation.h"

    @implementation ForwardInvocation
    -(void)appendString:(NSString*)str{
    NSLog(@"%@===%@",NSStringFromClass([self class]),NSStringFromSelector(_cmd));
    }
    @end

    在SonPerson中实现-(void)forwardInvocation:(NSInvocation)anInvocation和- (NSMethodSignature)methodSignatureForSelector:(SEL)aSelector方法
    -(void)forwardInvocation:(NSInvocation)anInvocation{
    NSLog(@"forwardInvocation");
    if ([ForwardInvocation instancesRespondToSelector:anInvocation.selector]) {
    [anInvocation invokeWithTarget:self.invocation];
    }
    }
    /
    必须重新这个方法,消息转发机制使用从这个方法中获取的信息来创建NSInvocation对象 返回nil上面方法不执行*/

    • (NSMethodSignature)methodSignatureForSelector:(SEL)aSelector{
      NSMethodSignature
      signature = [super methodSignatureForSelector:aSelector];
      if(!signature){
      if ([ForwardInvocation instancesRespondToSelector:aSelector]){
      signature = [ForwardInvocation instanceMethodSignatureForSelector:aSelector];
      }
      }
      returnsignature;
      }
      控制台输出:
      forwardInvocation
      ForwardInvocation===appendString:
      787C0404-4631-4988-88F7-6E9BA304F5B6.png

    23.以+scheduledTimeWithTimeInterval…的方式触发的timer,在滑动页面上的列表时,timer会暂停回调,为什么?如何解决?
    这里强调一点:在主线程中以+scheduledTimerWithTimeInterval...的方式触发的timer 默认是运行在 NSDefaultRunLoopMode 模式下的,当滑动页面上的列表时,进入了 UITrackingRunLoopMode 模式,这时候 timer 就会停止,可以修改 timer 的运行模式为 NSRunLoopCommonModes,这样定时器就可以一直运行了
    在子线程中通过 scheduledTimerWithTimeInterval:...方法来构建NSTimer
    方法内部已经创建 NSTimer 对象,并加入到 RunLoop 中,运行模式为NSDefaultRunLoopMode

    由于 Mode 有 timer 对象,所以 RunLoop 就开始监听定时器事件了,从而开始进入运行循环
    这个方法仅仅是创建 RunLoop 对象,并不会主动启动 RunLoop,需要再调用 run方法来启动

    如果在主线程中通过 scheduledTimerWithTimeInterval:...方法来构建 NSTimer,就不需要主动启动 RunLoop 对象,因为主线程的 RunLoop 对象在程序运行起来就已经被启动了 //userInfo参数:用来给NSTimer的userInfo 属性赋值,userInfo 是只读的,只能在构建 NSTimer 对象时赋值

    [NSTimer scheduledTimerWithTimeInterval:1.0 target:selfselector:@selector(run:) userInfo:@"ya 了 个 hoo"repeats:YES];

    // scheduledTimer...方法创建出来 NSTimer 虽然已经指定了默认模式,但是【允许你修改模式】
    [[NSRunLoop currentRunLoop] addTimer:timerforMode:NSRunLoopCommonModes];

    // 【仅在子线程】需要手动启动 RunLoop 对象,进入运行循环[[NSRunLoop currentRunLoop] run];

    24.如何手动触发一个value的KVO?

    键值观察通知依赖于 NSObject 的两个方法: willChangeValueForKey:和 didChangevlueForKey:。在一个被观察属性发生改变之前,willChangeValueForKey: 一定会被调用,这就会记录旧的值。而当改变发生后,observeValueForKey:ofObject:change:context: 和 didChangeValueForKey:也会被调用。如果可以手动实现这些调用,就可以实现“手动触发”了。
    @property (nonatomic, strong) NSDate *now;
    - (void)viewDidLoad {
    [super viewDidLoad];
    _now = [NSDate date];
    [self addObserver:self forKeyPath:@"now" options:NSKeyValueObservingOptionNew context:nil];
    NSLog(@"1");
    [self willChangeValueForKey:@"now"]; // 手动触发 self.now 的 KVO,必写。
    NSLog(@"2");
    [self didChangeValueForKey:@"now"]; // 手动触发 self.now 的 KVO,必写。
    NSLog(@"4");
    }
    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context {
    NSLog(@"3");
    }
    控制台打印 1-2-3-4
    注释didChangeValueForKey
    打印124
    说明 didChangeValueForKey: 内部调用了 observeValueForKeyPath:ofObject:change:context:

    25.如何定位和分析项目中影响性能的地方?以及如何进行性能优化?
    1.定位
    1.借助instruments这个利器分析出问题出在哪

    1. Time Profiler查看程序哪些部分最耗时
    2. Leaks查看内存是否泄漏

    2.优化
    1.用ARC管理内存
    2.在正确的地方使用 reuseIdentifier
    table view用 tableView:cellForRowAtIndexPath: 为rows分配cells的时候,它的数据应该重用自UITableViewCell。一个table view维持一个队列的数据可重用的UITableViewCell对象。不使用reuseIdentifier的话,每显示一行table view就不得不设置全新的cell。这对性能的影响可是相当大的,尤其会使app的滚动体验大打折扣。
    3.尽量把views设置为完全不透明
    (opaque)这个属性给渲染系统提供了一个如何处理这个view的提示。如果设为YES, 渲染系统就认为这个view是完全不透明的,这使得渲染系统优化一些渲染过程和提高性能。如果设置为NO,渲染系统正常地和其它内容组成这个View。默认值是YES。
    在相对比较静止的画面中,设置这个属性不会有太大影响。然而当这个view嵌在scroll view里边,或者是一个复杂动画的一部分,不设置这个属性的话会在很大程度上影响app的性能。
    换种说法,大家可能更好理解:只要一个视图的不透明度小于1,就会导致blending.blending 操作在iOS的图形处理器(GPU)中完成的,blending主要指的是混合像素颜色的计算。举个例子,我们把两个图层叠加在一起,如果第一个图层的有透明效果,则最终像素的颜色计算需要将第二个图层也考虑进来。这一过程即为Blending。为什么Blending会导致性能的损失?原因是很直观的,如果一个图层是完全不透明的,则系统直接显示该图层的颜色即可。而如果图层是带透明效果的,则会引入更多的计算,因为需要把下面的图层也包括进来,进行混合后颜色的计算。
    4.避免过于庞大的XIB
    当你加载一个XIB的时候所有内容都被放在了内存里,包括任何图片。如果有一个不会即刻用到的view,你这就是在浪费宝贵的内存资源了。
    5.不要阻塞主线程
    6.在Image Views中调整图片大小
    如果要在UIImageView中显示一个来自bundle的图片,你应保证图片的大小和UIImageView的大小相同。在运行中缩放图片是很耗费资源的,特别是UIImageView嵌套在UIScrollView中的情况下。
    如果图片是从远端服务加载的你不能控制图片大小,比如在下载前调整到合适大小的话,你可以在下载完成后,最好是用background thread,缩放一次,然后在UIImageView中使用缩放后的图片。

    1. 选择正确的Collection
      比如
      Arrays: 有序的一组值。使用index来lookup很快,使用value lookup很慢, 插入/删除很慢。
      Dictionaries: 存储键值对。 用键来查找比较快。
      Sets: 无序的一组值。用值来查找很快,插入/删除很快。
    2. 打开gzip压缩
      大量app依赖于远端资源和第三方API,你可能会开发一个需要从远端下载XML, JSON, HTML或者其它格式的app。
      问题是我们的目标是移动设备,因此你就不能指望网络状况有多好。一个用户现在还在edge网络,下一分钟可能就切换到了3G。不论什么场景,你肯定不想让你的用户等太长时间。
      减小文档的一个方式就是在服务端和你的app中打开gzip。这对于文字这种能有更高压缩率的数据来说会有更显著的效用。好消息是,iOS已经在NSURLConnection中默认支持了gzip压缩,当然AFNetworking这些基于它的框架亦然。像Google App Engine这些云服务提供者也已经支持了压缩输出。

    26.串行和并行,异步和同步的区别?
    串行、并行:
    指的是任务的执行方式。串行是指多个任务时,各个任务按顺序执行,完成一个之后才能进行下一个。并行指的是多个任务可以同时执行。异步是多个任务并行的前提条件。
    同步、异步:
    指的是能否开启新的线程。同步不能开启新的线程,异步可以。

    27.线程是什么?进程是什么?二者的区别和联系?
    线程是CPU独立运行和独立调度的基本单位(可以理解为一个进程中执行的代码片段)。
    进程是资源分配的基本单位(进程是一块包含了某些资源的内存区域)。

    联系
    进程和线程都是由操作系统所体会的程序运行的基本单元,系统利用该基本单元实现系统对应用的并发性。进程是线程的容器,真正完成代码执行的线程,而进程则作为线程的执行环境。一个程序至少包含一个进程,一个进程至少包含一个线程,一个进程中的所有线程共享当前进程所拥有的资源
    区别
    线程和进程的区别主要在于它们是不同的操作系统资源管理方式。进程有独立的地址空间,一个进程崩溃后,在保护模式的影响下不会对其他进程产生影响,而线程只是一个进程中的不同执行路径。线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间,一个线程死掉就等同于整个进程死掉,所以多进程的程序要比多线程的程序健壮,但在进程切换时,耗费资源较大,效率要差一些。但对于一些要求同时进行并且又要共享某些变量的并发操作,只能用线程,不能用进程。

    三 线程和app
    28.谈谈你对多线程开发的理解?ios中有几种实现多线程的方法?

    好处

    1****、使用线程可以把程序中占据时间长的任务放到后台去处理,如图片、视频的下载

    2****、发挥多核处理器的优势,并发执行让系统运行的更快、更流畅,用户体验更好

    缺点

    1****、大量的线程降低代码的可读性,

    2****、更多的线程需要更多的内存空间

    3****、当多个线程对同一个资源出现争夺的时候要注意线程安全的问题。

    iOS有三种多线程编程的技术

    1.****NSThread(****两种创建方式****)

    1.[NSThread detachNewThreadSelector:@selector(doSomething:) toTarget:self withObject:nil];

    *2.NSThread myThread = [[NSThread alloc] initWithTarget:self selector:@selector(doSomething:) object:nil];

    [myThread start];

    2. NSOperationQueue

    NSOperationQueueoprationQueue= [[NSOperationQueuealloc] init];*

    oprationQueueaddOperationWithBlock:^{

    //****这个****block****语句块在子线程中执行

    }

    3****、Grand Central Dispatch (GCD)

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

    //****耗时的操作

    dispatch_async(dispatch_get_main_queue(), ^{

    //****更新界面

    });

    });

    29.viewConteroller生命周期?


    C0F5AD07-D3EE-4938-8AD3-792C60FA2EC4.png

    30.内存管理的几条规则是什么?按照默认法则,哪些关键字生成的对象需要手动释放?在和property结合的时候怎么有效避免内存泄漏?
    规则是
    谁创建,谁释放
    谁引用,谁管理

    使用new、alloc、copy关键字生成的对象和retain了的对象需要手动释放。设置为autorelease的对象不需要手动释放,会直接进入自动释放池。

    内存管理主要要避免“过早释放”和“内存泄漏”,对于“过早释放”需要注意@property设置特性时,一定要用对特性关键字,对于“内存泄漏”,一定要申请了要负责释放,要细心。设置正确的property属性,对于retain需要在合适的地方释放。

    31.dispatch_barrier_async的作用是什么?
    在并行队列中,为了保持某些任务的顺序,需要等待一些任务完成后才能继续进行,使用 barrier 来等待之前任务完成,避免数据竞争等问题。
    dispatch_barrier_async 函数会等待追加到Concurrent Dispatch Queue并行队列中的操作全部执行完之后,然后再执行 dispatch_barrier_async 函数追加的处理,等dispatch_barrier_async 追加的处理执行结束之后,Concurrent Dispatch Queue才恢复之前的动作继续执行。

    dispatch_queue_t concurrentQueue = dispatch_queue_create("my.concurrent.queue", DISPATCH_QUEUE_CONCURRENT);
    dispatch_async(concurrentQueue, ^(){
    NSLog(@"dispatch-1");
    });
    dispatch_async(concurrentQueue, ^(){
    NSLog(@"dispatch-2");
    });
    dispatch_barrier_async(concurrentQueue, ^(){
    NSLog(@"dispatch-barrier");
    });
    dispatch_async(concurrentQueue, ^(){
    NSLog(@"dispatch-3");
    });
    dispatch_async(concurrentQueue, ^(){
    NSLog(@"dispatch-4");
    });

    dispatch_barrier_async 作用是在并行队列中,等待前面两个操作并行操作完成,这里是并行输出
    dispatch-1,dispatch-2
    然后执行
    dispatch_barrier_async中的操作,(现在就只会执行这一个操作)执行完成后,即输出
    "dispatch-barrier,
    最后该并行队列恢复原有执行状态,继续并行执行
    dispatch-3,dispatch-4

    32.如何用GCD同步若干个异步调用?(如根据若干个url异步加载多张图片,然后在都下载完成和合成一张整图)
    使用Dispatch Group追加block到Global Group Queue,这些block如果全部执行完毕,就会执行Main Dispatch Queue中的结束处理的block
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_group_t group = dispatch_group_create();
    dispatch_group_async(group, queue, ^{ /*加载图片1 / });
    dispatch_group_async(group, queue, ^{ /
    加载图片2 / });
    dispatch_group_async(group, queue, ^{ /
    加载图片3 */ });
    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
    // 合并图片
    });

    33.http和https的区别?
    1.https协议需要到ca申请证书,一般免费证书较少,因而需要一定费用。
    2.http是超文本传输协议,信息是明文传输,https则是具有安全性的ssl加密传输协议。
    3.http和https使用的是完全不同的连接方式,用的端口也不一样,前者是80,后者是443。
    4.http的连接很简单,是无状态的;HTTPS协议是由SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议,比http协议安全。
    HTTP:是互联网上应用最为广泛的一种网络协议,是一个客户端和服务器端请求和应答的标准(TCP),用于从WWW服务器传输超文本到本地浏览器的传输协议,它可以使浏览器更加高效,使网络传输减少。
    HTTPS:是以安全为目标的HTTP通道,简单讲是HTTP的安全版,即HTTP下加入SSL层,HTTPS的安全基础是SSL,因此加密的详细内容就需要SSL。
    HTTPS协议的主要作用可以分为两种:一种是建立一个信息安全通道,来保证数据传输的安全;另一种就是确认网站的真实性

    34.服务器能否知道APNS推送后有没有到达客户端的方法?
    APNS是苹果提供的远程推送的服务,APP开发此功能之后,用户允许推送之后,服务端可以向安装了此app的用户推送信息。但是APNS推送无法保证100%到达。
    目前关于APNS苹果更新了新的策略,即 APNS/HTTP2.
    如果服务器像APNS服务器推送信息之后,服务器能够接收到APNS是否真的成功像客户端推送成功了某个信息。这样在一定程度上提高了APNS的成功概率

    00310A73-AF11-4EBA-914C-F25F57D5356A.png

    35.什么方式可以看到上架app的头文件?
    目前只会把.ipa文件换成.zip然后解压然后查看里面的info.plist和资源文件

    36. iOS IAP内购审核可能失败的问题?

    苹果****IAP****最大的坑点:****applicationUsername=nil****,你懂得

    另外:****IAP****和第三方支付最大的不同点

    第三方支付:客户端只要给服务器传商品参数给服务器让我们服务器向第三方支付服务器请求交易订单这样的好处是安全,可控制,可查询,然后我们客户端根据服务器给我们的交易订单来拉起支付

    IAP:****如果也向第三方流程一样由服务器创建订单再下发给客户端然后调用****IAP****的话我们无法控制这笔订单是否成功不可控制因素太多,比如这笔交易没有成功而服务器已经生成了订单然后客户端根据苹果的交易结果去服务器验证在由服务器去向****IAP****验证延时太长。所以内购我们不能按照这样的流程来

    而是客户端先向****IAP****发起请求支付,交易完成以后客户端再去我们自己的服务器创建订单然后再让服务器根据证订单再向苹果服务器去验证这笔交易是否完成最后通知客户端

    0513D6F2-243B-4B66-A287-A9E6BD3C1CFD.png
    一:协议

    1.****协议,税务和银行业务信息的填写

    2.****内购商品的添加

    3.****添加沙盒测试账号

    4.****内购代码具体实现

    5.****内购注意事项

    二****.****协议****.****税务和银行信息的填写入口

    2.2****、选择申请合同类型

    进入协议、税务和银行业务页面后,会有****3****种合同类型,如果你之前没有主动申请过去合同,那么一般你现在激活的合同只有****iOS Free Application****一种。

    页面内容分为两块: Request Contracts(****申请合同****)
    Contracts In Effect(****已生效合同****)****。

    合同类型分为****3****种: iOS Free Application(****免费应用合同****)
    iOS Paid Application(****付费应用合同****)
    iAd App NetNetwork(****广告合同****)

    2.3****、申请****iOS Paid Application****合同****(****协议、税务和银行业务****3****个都要填写****)

    先点击****Contact Info 的****Set Up

    有些银行通过下面的****Look up CNAPS Code****方法查不到,就需要借助百度了,一定要准确查询,否则会有问题。推荐一个地址

    https://e.czbank.com/CORPORBANK/query_unionBank_index.jsp****如果查不到自己的银行****cnaps code****可以打电话给银行客服

    货币类型可能有歧义,看你是想收美元还是人民币了,都说美元合适。不过,我做的时候为了避免事情,还是选择了****CNY****,支持国产。还有一点,银行账号如果是对公的账号,需要填写公司的英文名称,如果没有的话,上拼音!然后点击保存银行信息就算****ok****了,然后退回到最开始的页面

    如果以上信息填写完毕,状态一直是****Processing****不要怀疑自己填写出错,那是需要审核一般****1****到****3****天就能通过

    二、为****app****添加内购产品

    在你点击添加内购产品按钮后会有弹框,提示你选择类型,这个就要看你****app****的需求了

    填写完审核信息后,点击右上角的****“****存储****”****按钮,就添加了一个内购产品****~

    三、添加沙盒技术测试员

    在****iTunes Connect****的用户和智能中选择****“****沙盒技术测试员****”****,填写信息保存以后就有一个测试员了

    四、具体实现

    五、要注意的事项!

    1.bundleID****要与****iTunes Connect****上你****App****的相同,不然是请求不到产品信息的

    2.****在沙盒环境进行测试内购的时候,要使用没有越狱的苹果手机

    3.****在沙盒环境下真机测试内购时,请去****app store****中注销你的****apple ID****,不然发起支付购买请求后会直接****case****:****SKPaymentTransactionStateFailed****。使用沙盒测试员的账号时不需要真正花钱的。

    4.****如果只添加了一个沙盒测试员账号,当一个真机已经使用了这个账号,另一个真机再使用这个账号支付也是会发生错误的。那就去多建几个沙盒测试员账号使用不同的,反正也是免费的,填写也很快。

    5.****监听购买结果,当失败和成功时代码中要调用:

    **[[SKPaymentQueue defaultQueue] finishTransaction:transaction]; **

    该方法通知苹果支付队列该交易已完成,不然就会已发起相同 ID 的商品购买就会有此项目将免费恢复的提示

    六、请在本地做一下凭证存储

    **现在订单正确性的验证是:iOS客户端(购买成功)→ 后台→后台到苹果服务器验证→处理后台返回结果做相应逻辑处理。 **

    **我们前端购买成功后,凭证本地保留一份,当与后台验证成功后,再将本地保留的凭证删除。否者一直使用本地已经保留的凭证与后台交互。 **

    注:由于以前我在的iTunes Connect中填写过协议,税务,和银行业务步骤无法复现所以部分图片来自简书作者:睡不着的叶-《iOS开发 内购流程 手把手教你还不学?》文章。

    七:出现的问题

    **如果在真机上运行代码请将真是apple id账号退出用测试账号登入 **

    **App Store审核的时候也使用的是沙盒购买,所以验证购买凭证的时候需要判断返回Status Code决定是否去沙盒进行二次验证,为了线上用户的使用,验证的顺序肯定是先验证正式环境,此时若返回值为21007,就需要去沙盒二次验证,因为此购买的是在沙盒进行的。 **

    **审核不通过:原因没有提供测试账号,或者审核时用的是正式环境验证链接 **

    **如果年会员或者月会员的话选择是—消耗性商品,订阅和消耗性返回数据有区别 **

    **生成的订单怎么对应上这个订单呢-》后台来做(推荐)也可以前台来做 **

    凭证存储一定要在本地做一下(防止网络原因或者和后台中断访问)然后等和后台交互过确认以后再删除

    八:坑点(转自简书****NewPan****)

    **1.如果用户后买成功以后,网络就不行了,那么苹果的 IAP 也收不到支付成功的通知,就没法通知 APP,我们也没法给用户发货。 **

    **2.如果 IAP 通知我们支付成功,我们驱动服务器去 IAP 服务器查询失败的话,那就要等下次 APP 启动的时候,才会重新通知我们有未验证的订单。这个周期根本没法想象,如果用户一个月不重启 APP,那么我们可能一个月没法给用户发货 **

    **3.有人反馈,IAP 通知已经交易成功了,此时去沙盒里取收据数据,发现为空,或者出现通知交易成功那笔交易没有被及时的写入到沙盒数据中,导致我们服务器去 IAP 服务器查询的时候,查不到这笔订单。 **

    **4.如果用户的交易还没有得到验证,就把 APP 给卸载了,以后要怎么恢复那些没有被验证的订单? **

    **5.越狱手机有无数奇葩的收据丢失或无效或被替换的问题,应该怎样酌情处理? **

    **解决:越狱手机一律不准内购 **

    相关文章

      网友评论

        本文标题:iOS面试题精选

        本文链接:https://www.haomeiwen.com/subject/ehzvyqtx.html