美文网首页iOS面试题iOS面试题汇总
面试技巧攻克-Objective-C语言

面试技巧攻克-Objective-C语言

作者: iOS大蝠 | 来源:发表于2019-11-02 00:39 被阅读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 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] // 浅复制),其他都是单层深复制

    相关文章

      网友评论

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

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