美文网首页iOS面试题/2019
面试技巧攻克(2)-Objective-C语言

面试技巧攻克(2)-Objective-C语言

作者: 一意孤行的程序猿 | 来源:发表于2020-07-29 16:36 被阅读0次

    一、语言基础

    1、#import和#include,@class有什么区别?

    import不会重复引入头文件

    @class是向前声明,告诉编译器有这么一个类的定义,但是暂时不引入,保证编译可以通过,直到运行时采取查看类的实现文件。这样也可以避免重复引用甚至循环引用等问题。

    2、#import<>和#import“”有什么区别?

    import<>只会去系统目录下寻找
    import“”会先去用户目录下寻找,如果找不到,会继续去系统目录下寻找

    3、Objective-C中堆和栈有什么区别?

    堆一帮用来存放oc对象,需要手动申请和释放内存,ARC环境下由编译器管理,不需要手动管理。
    栈由系统自动分配,一般存放非oc对象的基本类型数据,例如int,float,不需要手动管理。

    4、self和super有什么区别?为什么要使用[super init]?

    self是一个类的隐藏参数,指向当前实例对象,super是编译器标识符,在运行时,self和super指向同一个实例对象。
    区别在于:当self调用方法时,会优先在当前类的方法列表中寻找方法,当使用super调用方法时,会优先从父类的方法列表中寻找方法。

    子类初始化时,调用[super init]方法,主要是为了避免造成未知的错误,如果父类初始化不成功,返回nil,可以根据父类初始化结果,做响应容错处理。

    二、属性和实例变量

    1、属性和实例变量的区别是什么?

    使用实例变量的方式声明的变量,只能在类内部访问,类外无法访问,而且不能使用“.”语法访问变量,如果需要对外提供访问能力,需要手动实现set和get方法。
    使用属性的方式声明的变量,编译器会自动生成set和get方法,也就可以使用“.”语法访问变量。如果属性是声明在h文件中,类内部和外部都可以访问这个变量,如果是声明在m文件中,则只能当前类内部访问,外部包括子类都无法访问。

    2、修饰属性的关键字有哪些,分别有什么作用?

    修饰属性的关键字有以下几个
    (1)原子性:nonatomic,atomic
    (2)读写控制:readonly,readwrite,getter,setter
    (3)内存管理:assign,retain,weak,strong,copy,__unsafe_unretained

    原子性(nonatomic,atomic)

    在多线程中,同一个变量被多个线程同时访问,会造成数据污染,因此为了安全,Objective-C中默认属性为atomic,即对set方法加锁,保证多线程下数据安全。同样的也会为此承担一部分的资源开销。应用中不是特殊情况(多线程通信)一般属性声明为nonatomic,这样可以提高访问时的性能。

    读写控制(readonly,readwrite,getter,setter)

    readonly标识只读,编译器只提供get方法
    readwrite标识读写,编译器提供set和get方法
    getter和setter用来指定存取方法

    内存管理(assign,retain,weak,strong,copy)

    assign可以修饰oc对象和和oc对象的基础类型,标识简单赋值,指针弱引用,不会对引用计数+1
    weak修饰弱引用,只能修饰oc对象,和assign相同,不同的是,weak修饰的变量在销毁后,自动将指针置为nil,避免野指针。
    retain修饰oc对象,为了持有对象,声明强引用,引用计数+1。
    strong和reatin类似,在ARC中,用strong代替retain。
    copy建立一个和原有对象内容相同且引用计数为1的新的对象。

    3、什么时候使用weak关键字?和assign有什么区别?

    (1)ARC中为了避免循环引用,可以让其中一个对象使用weak修饰,常见“delegate,block”
    (2)Interface Builder中IBOutlet修饰的控件一般也使用weak
    区别:weak只能修饰oc对象,并且在销毁后自动将指针置为nil,避免野指针,而assign可以修饰oc对象和非oc对象的基础类型数据,当对象销毁后,不会将指针置为nil,形成野指针,再次调用时会导致崩溃。

    4、nonatomic和atomic有什么区别?atomic是绝对的线程安全么?如果不是该如何实现?

    区别:对属性的存取操作是否添加加锁操作,来保证多线程下数据存取的安全性。在执行效率上nonatomic比atomic存取效率更高。

    绝对线程安全么?不是绝对安全,可以保证大部分情况下数据读取的一致性,比如在多线程下,两个线程都对属性进行循环+1操作,导致对属性的操作,变为读取,+1,存储的三个操作,而atomic只能保证读取和存储操作,无法保证+1操作时的原子性。

    如何保证绝对线程的安全?其实只要给线程中执行的代码块加锁就能实现多线程访问的安全。

    三、实例方法和类方法

    1、什么是类工厂方法?

    简单的说就是用来快速创建对象的的类方法,可以直接返回一个初始化好的对象。UIKit中最经典的就是UIButton类中的buttonWithType:类工厂方法。
    特征:
    (1)一定是类方法
    (2)返回值一定是id/instancetype类型
    (3)规范的类方法名,一般以小写类名为开头

    2、OC中有方法重载么?

    没有,因为函数语法定义的问题,OC编译器不允许定义函数名相同,参数个数相同,但是返回类型和参数类型不同的方法。

    四、数据类型和运算符

    1、OC中NSInteger和int基础数据类型有什么区别?

    NSInteger是long 和 int 的别名,在预编译阶段,NSInteger会根据系统是32位的还是64位的来动态确定是int类型还是long类型,NSInteger也是官方推荐使用的基本数据类型。

    2、instancetype和id有什么区别?

    instancetype和id都可以指向任意OC对象,不同的是:
    (1)id可以作为返回值,形参,变量,并将对象的确定延迟到运行时。
    (2)instancetype只能作为返回值,并且在预编译时已经确定类型。

    五、继承和多态

    1、OC中有多继承么?

    OC中没有多继承,但是可以通过组合,协议,分类实现类似多继承。

    2、OC为什么不能实现多继承?

    因为OC的消息机制,名字查找发生在运行时,而不是编译时,不能解决多个基类的二义性。

    六、分类和扩展

    1、什么是Category?作用什么?

    Category是OC在不破坏已有类的情况下,为该类添加新方法的一种方式。

    作用:
    (1)对现有类添加方法
    (2)在没有源代码的情况下,对类进行扩展
    (3)将类中方法的实现分散到不同文件中,减小单个文件体积
    (4)可以按需动态加载不同的Category

    特性:
    (1)重名的情况下,类别中的方法优先级高于原类中的方法
    (2)不能直接添加成员变量(可以使用runtime实现,较为复杂)
    (3)同一个类的不同类别声明了相同方法,调用时不确定
    (4)可以添加属性,但是不会生产set和get 方法,需要通过关联对象实现

    2、Category的实现原理是什么?为什么只能添加方法,不能添加属性?

    (1)先看一下Category在runtime源码中,Categroy定义为一个结构体

    //分类的定义,结构体
    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的定义里可以看出,分类可以添加实例方法,类方法,实现协议,添加属性,而不能添加实例变量,因为没有存储实例变量列表的指针。

    Category是如何加载的?
    (1)_objc_init runtime入口函数,初始化
    (2)map_images 加锁
    (3)map_images_nolock 完成类的注册,初始化,及load方法加载
    (4)_read_images 完成类的加载,协议的加载,类别的加载等工作
    (5)remethodizeClass 这一步非常关键,它将类别绑定到目标类上
    (6)attachCategories 这是最重要的一步,将类别中的方法,属性绑定到目标类
    (7)attachLists 将目标类中的方法和分类中的方法放到一个列表中

    _read_images具体作用:将类别和目标类绑定,并重建目标类的方法列表
    attachCategories具体作用:分配一个新的列表空间,用来存放类别的实例方法,类方法,协议方法,交给attachLists处理
    attachLists具体作用:创建一个新的列表,与类别中传过来的列表融合在一起,变成新的方法列表。
    如果有重名的方法,类别中的方法位置更靠前,类方法位置靠后,也就解释了为什么类别的方法优先级要高于目标类的方法。

    需要注意的是:尽管Category定义中有存放属性的变量,但是源码实现中,并不会为属性生成set和get方法,所以需要借助关联对象,来手动实现。

    3、关联对象是怎么实现的?

    翻一下runtime的源码,在objc-references.mm文件中有个方法_object_set_associative_reference:

    
    void _object_set_associative_reference(id object, void *key, id value, uintptr_t policy) {
        // retain the new value (if any) outside the lock.
        ObjcAssociation old_association(0, nil);
        id new_value = value ? acquireValue(value, policy) : nil;
        {
            AssociationsManager manager;
            AssociationsHashMap &associations(manager.associations());
            disguised_ptr_t disguised_object = DISGUISE(object);
            if (new_value) {
                // break any existing association.
                AssociationsHashMap::iterator i = associations.find(disguised_object);
                if (i != associations.end()) {
                    // secondary table exists
                    ObjectAssociationMap *refs = i->second;
                    ObjectAssociationMap::iterator j = refs->find(key);
                    if (j != refs->end()) {
                        old_association = j->second;
                        j->second = ObjcAssociation(policy, new_value);
                    } else {
                        (*refs)[key] = ObjcAssociation(policy, new_value);
                    }
                } else {
                    // create the new association (first time).
                    ObjectAssociationMap *refs = new ObjectAssociationMap;
                    associations[disguised_object] = refs;
                    (*refs)[key] = ObjcAssociation(policy, new_value);
                    _class_setInstancesHaveAssociatedObjects(_object_getClass(object));
                }
            } else {
                // setting the association to nil breaks the association.
                AssociationsHashMap::iterator i = associations.find(disguised_object);
                if (i !=  associations.end()) {
                    ObjectAssociationMap *refs = i->second;
                    ObjectAssociationMap::iterator j = refs->find(key);
                    if (j != refs->end()) {
                        old_association = j->second;
                        refs->erase(j);
                    }
                }
            }
        }
        // release the old value (outside of the lock).
        if (old_association.hasValue()) ReleaseValue()(old_association);
    }
    

    我们可以看到所有的关联对象都由AssociationsManager管理,而AssociationsManager定义如下:

    
    class AssociationsManager {
        static OSSpinLock _lock;
        static AssociationsHashMap *_map;               // associative references:  object pointer -> PtrPtrHashMap.
    public:
        AssociationsManager()   { OSSpinLockLock(&_lock); }
        ~AssociationsManager()  { OSSpinLockUnlock(&_lock); }
     
        AssociationsHashMap &associations() {
            if (_map == NULL)
                _map = new AssociationsHashMap();
            return *_map;
        }
    

    AssociationsManager里面是由一个静态AssociationsHashMap来存储所有的关联对象的。这相当于把所有对象的关联对象都存在一个全局map里面。而map的的key是这个对象的指针地址(任意两个不同对象的指针地址一定是不同的),而这个map的value又是另外一个AssociationsHashMap,里面保存了关联对象的kv对。
    而在对象的销毁逻辑里面,见objc-runtime-new.mm:

    void *objc_destructInstance(id obj) 
    {
        if (obj) {
            Class isa_gen = _object_getClass(obj);
            class_t *isa = newcls(isa_gen);
     
            // Read all of the flags at once for performance.
            bool cxx = hasCxxStructors(isa);
            bool assoc = !UseGC && _class_instancesHaveAssociatedObjects(isa_gen);
     
            // This order is important.
            if (cxx) object_cxxDestruct(obj);
            if (assoc) _object_remove_assocations(obj);
     
            if (!UseGC) objc_clear_deallocating(obj);
        }
     
        return obj;
    }
    

    runtime的销毁对象函数objc_destructInstance里面会判断这个对象有没有关联对象,如果有,会调用_object_remove_assocations做关联对象的清理工作。

    4、Category中有+load方法么?什么时候调用的?load方法可以继承么?

    load方法是不可以继承的,因为load方法不是通过消息传递(_objc_msgSend)方式调用的,是直接通过函数指针调用的。因此load方法不存在类的层级遍历。
    Category中也有load方法,和类中load方法不同的是,它不是简单的继承或者覆盖,而是独立的load方法。和类中的load方法没有关系。

    在runtime加载时调用load方法,调用顺序:父类,子类,分类。

    六、Block

    1、Block的原理是什么?使用的时候需要注意什么?

    Block是闭包,可以作为参数,变量,返回值使用。在iOS中广泛应用,比如GCD,动画,循环。
    通过下面一段代码来分析一下Block原理:

    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            int a = 10;//声明一个变量,存在栈上
            
            void(^testBlock)(int i) = ^(int i){
                NSLog(@"a : %d",a);
                NSLog(@"i : %d",i);
            };
            
            a = 20;
            
            testBlock(a);//调用block
        }
        return 0;
    }
    

    打印结果为:a = 10,i=20
    通过结果可以发现,block具有保存变量瞬时值的特性,记录了a修改之前的值。
    通过Clang来看一下底层C语言的实现:

    clang -rewrite-objc main.m
    

    以下为main函数C语言实现

    //定义了block的结构体
    struct __main_block_impl_0 {
      struct __block_impl impl;//block的结构体
      struct __main_block_desc_0* Desc;//block描述对象,
      int a;//存放变量的a的值
        
    //构造函数
      __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, int flags=0) : a(_a) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    
    //block大括号对应的实现
    static void __main_block_func_0(struct __main_block_impl_0 *__cself, int i) {
      int a = __cself->a; // bound by copy//a的值使用的是结构体中指针指向的值,在构造时已经确定为10,而不是使用的是block外部变量a的地址,
    
                NSLog((NSString *)&__NSConstantStringImpl__var_folders_vt_3r7qfrfs39729zxpgrbp45z40000gp_T_main_b630e6_mi_0,a);//打印输出时使用的也是内部变量a,并不是外部变量a或者使用a的指针,所以为10.
                NSLog((NSString *)&__NSConstantStringImpl__var_folders_vt_3r7qfrfs39729zxpgrbp45z40000gp_T_main_b630e6_mi_1,i);//i的值为参数的值,这里为20
            }
    
    int main(int argc, const char * argv[]) {
        /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
            int a = 10;
    
            void(*testBlock)(int i) = ((void (*)(int))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a));
    
            a = 20;
    
            ((void (*)(__block_impl *, int))((__block_impl *)testBlock)->FuncPtr)((__block_impl *)testBlock, a);
        }
        return 0;
    }
    

    这里可以看出,__main_block_impl_0定义有一个成员变量a,用来保存构造函数中传入的a的值,相当于构造时复制了一份a的值,block执行时,使用的是block内部成员变量a的值,而不是block外部的a的值,

    仔细观察可以发现:struct __main_block_impl_0 其实是 对 struct __block_impl impl的封装

    struct __block_impl {
      void *isa; 类似对象的指针
      int Flags;
      int Reserved;
      void *FuncPtr;
      }
    

    isa类似对象的指针,指向block保存的区域:
    (1)_NSConcreteStackBlock:栈区存储的block
    (2)_NSConcreteMallocBlock:堆区存储的block
    (1)_NSConcreteGlobalBlock:全局区存储的block

    栈block 在函数的作用域结束后,释放
    堆block在retainCount为0时,释放
    全局block和程序的生命周期相同

    FuncPtr指针,指向block的执行函数,即执行大括号内的代码逻辑。

    总结一下:Block底层是由结构体实现的,block的调用是由函数实现的

    Block使用注意事项:

    在block中使用自动变量,无法在block中修改自动变量的值,因为在构造过程中,a的值已经确定了。

    如果要修改自动变量的值,需要在自动变量前加上__block修饰,全局变量和静态变量不需要加修饰也可以在block中修改他们。

    __block 修饰的自动变量,为何能够修改?改动一下代码:

    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            __block int a = 10;//声明一个变量,存在栈上
            
            void(^testBlock)(int i) = ^(int i){
                NSLog(@"a : %d",a);
                NSLog(@"i : %d",i);
                a++;
            };
            
            a = 20;
            
            testBlock(a);//调用block
            NSLog(@"a = %d",21);
            
        }
        return 0;
    }
    

    结果输出为:a = 20,i = 20 ,a = 21;
    重复刚才的clang命令,会发现,block结构体的构造函数传入的是b的地址,也就是说不加修饰的话,是值传递,存在拷贝,而加了修饰的话,是指针传递,所以block内部修改变量的话,外部也会修改.

    最后再说一下block从栈复制到堆的几种情况:
    (1)手动调用block的copy方法;
    (2)将block赋给__strong修饰的对象,同时block中还要引用外部变量时
    (3)将block作为函数返回值时
    (4)向Cocoa框架含有usingBlock的方法或者GCD的API传递block参数时

    2、什么是Block循环引用?如何解决循环引用?

    Block中直接使用外部强指针会导致循环引用。
    解决办法:
    (1)对当前对象弱引用
    (2)使用完block后,手动将一方置为nil
    (3)将外部对象作为参数传入block
    (4)使用Weak-Strong Dance方式来解决(也是使用最多的一种方式)

    其他

    1、OC中load方法和initialize的方法有什么区别?

    (1)load方法不能继承,是通过函数指针调用,runtime运行时调用,较早
    (2)initialize方法可以继承,是通过消息传递调用的,第一次收到消息时调用,较晚

    2、copy方法是深复制还是浅复制?

    浅复制是复制对象的指针,深复制是复制对象内容,生成新的对象。

    copy不管是深复制还是浅复制,复制出的对象都是不可变的。
    mutablCopy复制的出的都是可变的。

    按照容器和非容器类型,可变和不可变类型分。有如下几种情况。

    (1)容器-不可变: NSArray (copy 浅拷贝,mutableCopy深拷贝)
    (2)容器-可变 :NSMutableArray (copy 深拷贝,mutableCopy深拷贝)
    (3)非容器-不可变 :NSString(copy 浅拷贝,mutableCopy深拷贝)
    (4)非容器-可变:NSMutableString (copy 深拷贝,mutableCopy深拷贝)

    copy对可变对象,为深复制,原对象引用计数不+1,对于不可变对象是浅复制,引用计数+1,始终返回不可变对象。
    mutableCopy始终是深复制,原对象引用计数不+1,始终返回可变对象。

    非集合类:只有不可变对象进行copy操作时是浅复制([immutableObject copy] // 浅复制),其他都是深复制
    集合类:只有不可变对象进行copy操作时是浅复制([immutableObject copy] // 浅复制),其他都是单层深复制

    结交人脉

    最后推荐个我的iOS交流群:789143298
    '有一个共同的圈子很重要,结识人脉!里面都是iOS开发,全栈发展,欢迎入驻,共同进步!(群内会免费提供一些群主收藏的免费学习书籍资料以及整理好的几百道面试题和答案文档!)

    收录:iOS大蝠

    相关文章

      网友评论

        本文标题:面试技巧攻克(2)-Objective-C语言

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