iOS面试题iOS面试题汇总

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

2019-11-02  本文已影响0人  iOS大蝠

一、语言基础

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] // 浅复制),其他都是单层深复制
上一篇下一篇

猜你喜欢

热点阅读