objective-c知识梳理-语言基础

304 阅读13分钟

1.objective-c中的self和super有什么区别?为什么要使用[super init]?

  • self和super都是指向相同的实例对象,只不过调用方法时一个从当前类找,一个从父类中找
  • [self class]和[super class]都是打印当前所在类,是一样的,因为class方法调用会走消息发送流程objc_msgSend(id,sel,...),传递的第一个参数是当前对象
  • 使用super通常用来调用父类中的方法以达到初始化的作用,如果不这么做可能有无法预知的事情发生,通过if(self = [super init]){逻辑容错处理}

2.#import与#include,@class有什么区别?#import<>与#import""又有什么区别?

  • #import与#include都是用来引入头文件的,与#include相比,import的优势是不会重复引入头文件,相当于c/c++中的#pragma once的作用,保证头文件只被编译一次
  • @class表示在头文件里面申明一下需要使用的头文件,保证在编译阶段能通过,运行阶段能在确定需要的时候引入文件,有效防止重复引入甚至循环引用问题
  • #import<>表示引入系统头文件,#import""表示优先引入本地头文件,如果没有则会去系统查找头文件 3.object-c中的堆和栈的区别是什么?
  • 堆在内存里是向上生长,对象的内存是动态分配的,一般存放object-c对象,并且需要手动释放内存,ARC环境下对象由编译器管理,不需要手动释放
  • 栈在内存里是相下生长,内存一般由系统分配,系统管理,存储基本的数据类型
  • 一般我们的宗旨是趋向于栈内存的使用,因为堆内存对对象的分配,查找,销毁等都比较消耗时间 4.属性和实例变量的区别?
  • 实例变量只能在类内访问,类外不能访问,也不能通过点语法访问,如果想这样做需要实现自己的存取方法,属性就是编译器自动生成一组与属性对应的存取方法,我们可以直接访问
  • 通过@dynamic来控制属性的setter和getter方法是否自动合成
  • 属性和实例变量的控制访问权限:@public公开变量,表示变量对所有类都是可见的;@protected表示变量对子类可见,对其他类来说变量是不可见的,不可访问;@private表示变量是私有的,子类不可见@package是oc中特有的修饰符,一般在开发静态库的时候用到,这个关键字修饰的变量对于framework内部来说是@protected类型,对于包外面来说是@private不可访问,起到安全保护的作用 5.属性中的修饰关键字有哪些?
  • atomic,nonatomic:原子性和非原子性,原子性会对存取方法进行加锁保护,多线程下访问存取方法时相对安全,因为在存取方法过程在执行时无法控制外部因素对属性的修改,使用,销毁,因此不是绝对安全,由于是加了自旋锁(现在系统优化成互斥锁了)导致性能降低,所以我们在大多数时候是使用nonatomic非原子属性
  • assign:直接简单赋值,不会增加引用计数,一般用于基础数据类型(NSInteger)
  • weak:修饰弱引用,不增加引用对象的计数,用于避免循环引用,对象消失后会把对象指针设置成nil,防止悬挂指针出现
  • retain:常用于引用类型,是为了持有对象,声明强引用,将指针本来指向的旧对象释放掉,然后指向新的引用对象并将引用计数+1
  • strong:原理和retain类似,只不过在ARC自动引用计数时,用strong代替retain
  • copy:建立一个和原对象内容相同的且引用计数为1的对象,两个指针分别指向不同的内存地址,当然这两句话也不会绝对的正确的,字符串常量的copy不会增加引用计数和内存copy,block属性的copy关键字也跟当前block的当时是否有变量捕获环境有关 6.类方法和实例方法有什么区别
  • 实例方法用“-”修饰,类方法用“+”修饰
  • 类方法中调用self是类对象,实例方法中调用self是实例对象,属性关键字中有一个class属性表示类属性,需要我们手动添加存取方法,通常和关联对象结合使用,例子:
@property(class,nonatomic,assign) BOOL supportFace;
 
 + (void)setSupportFace:(BOOL)face{
     objc_setAssociatedObject(self, &share_support_face, [NSNumber numberWithBool:face], OBJC_ASSOCIATION_ASSIGN);  
 }
  • 实例对象可以访问实例变量,类对象只能访问类对象 7.什么是类工厂方法?
  • 类工厂方法就是快速创建对象的类方法,它可以直接返回一个初始化好的对象。类似于button的buttonWithType类工厂方法,当然我们还需要了解的是抽象工厂方法,跟类工厂方法类似只不过把实现细节都包装在一个抽象类里面,最突出的例子就是oc基础语法中的类簇模型,创建数组时可能是__NSArray0,__NSSingleObjectArray__NSArrayI,所以请不要轻易尝试创建NSStringNSArray,NSDictionary的子类
  • 类工厂方法的几个特征:一定是类方法,返回值一定是id/instancetype类型,因为要返回一个对象,规范的方法名会说明工厂方法返回的是什么对象,例如从buttonWithType能明显的知道这里返回的是一个button 8.object-c中有方法的重载吗?
  • 不完全支持,如果是相同方法名,不同参数个数是可以支持的,如果仅仅只是通过返回值和参数类型来判断是否重载,oc明显是不支持 9.object-c中的NSIner类型和c语言中的int类型有什么区别?
  • NSInterger是int和long类型的封装,会根据操作系统位数,自动的返回最大的类型 10.instancetype和id有什么区别?
  • instancetype和id可以用来指向任意objective-c对象,不同的是id可以用作变量,形参,返回值,并且将对象的确定延迟到程序运行时;而instancetype只能作为返回值使用,并且在编译的时候就确定了返回类型
  • 返回id类型对象,无法调用NSMutableArray类中的任何方法,使用instancetype的目的是返回所在类的类型,可以直接使用 11.object-c有多继承吗?没有的话用什么代替?
  • object-c中没有多继承,用协议来实现多继承 12.object-c中Category的作用是什么?
  • 当为一个类添加新的方法时,在object-c中有4中方式可以做到,第一种是直接在类里面再写一个方法,这样就破坏了类的封装性;第二种是通过继承,但是只是为了扩展一个方法开销有点大;第三种是通过协议扩展类的方法,虽然通过协议能扩展类的方法,可以大大降低代码耦合度,但是实现上比较复杂,为了添加一个方法有点小题大做的感觉;第四种就是通过Category扩展一个类的方法,使用类别能在不创建新类的前提下为类新增方法
  • 类别不能添加属性,只能通过重写setter和getter方法结合关联属性来曲线添加属性 13.Category的优点和缺点有哪些?
  • 优点:在不改变一个类的前提下,对一个已存在的类添加新的方法
  • 可以在没有源代码的情况下对框架内的类进行扩展,例如NSString类
  • 减少单个文件体积
  • 可以加载不同需求的Category
  • 缺点如下:方法的扩展是硬编码,一个方法只能对应一个功能,不能动态修改
  • 类别中的方法优先级高于原类中的方法,是因为类别中的方法在read_image中会动态添加在原类方法的前面,所以会优先访问到类别中的方法
  • 不能直接添加成员变量(但是可以通过关联属性达到这个目的)
  • 同一个类的Category中不能有重名方法 14.Category的实现原理是什么?Category为什么只能添加方法不能添加属性?
  • 我们先来看看Category的内部结构:
  • 从源码中对Category定义来看,分类中虽然可以添加实例方法,类方法,协议和属性列表,但是不能添加实例变量,因为Category定义中没有存储实例变量对应的指针变量 Gategory的加载顺序
     const char *name;//categor的名称
     classref_t cls;//扩展的类(用于绑定到相应的类)
     ...
     struct method_list_t *classMethods;//添加的类的方法列表
     struct protocol_list_t *protocols;//添加的协议列表
     ...
 }
graph TD
Start --> objc_init-->map_image-->map_images_nolock-->read_images-->remethodizeClass-->attachCategoryes-->attachLists --> Stop
  • objc_init是runtime的入口函数,进行一些初始化的工作
  • map_images加锁
  • map_images_unlock完成所有类的注册和fixup(随机偏移值ASLR,在运行中通过image list命令可以动态查看ASLR的基准值),以及调用load方法
  • read_image完成类的加载,协议的加载,类别的加载等工作
  • remethodizeClass这一步非常重要,它将类别绑定到目标类
  • attachCategory将类别中的方法和属性列表绑定到目标类
  • attachLists将目标类中的方法和分类中的方法放到一个列表中 首先我们来看看read_image中的代码:
category_t **catlist = _getObjc2CategoryList(hi,&count);//拿到所有类别
for(i = 0; i < count; ++i){
    category_t *cat = catlist[i];
    Class cls = remapClass(cat->cls);//通过category相关属性,得到目标类
    ...
}
addUnattachedCategoryForClass(cat,cls,hi);
remethodizeClass(cls);//重新类构建方法列表
...
remethodizeClass(cls->ISA());//重新元类构建方法列表
...
总结:read_image这个方法里面主要做了两件事情,第一件事情是将Category和目标类绑定在一起,第二件事情是重新构建目标类的方法列表

attachCategories的源码如下:

    ...
    method_list_t **mlists = (method_list_t **)malloc(cats->count * sizeof(*mlists);
    ...
    method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
    if(mlist){
        mlists[mcount++] = mlist;//收集类别中的方法
    }
    ...
    rw->methods.attachLists(mlists,mcount);//最主要的方法
    ...
}

attachCategories的主要工作是分配一个新的方法列表空间,用来存法规Category中的实例方法,类方法和协议方法,然后将这个方法交给attachList处理

attachLists的实现源码如下:

    memmove(array()->lists+addenCount,array()->list,oldCount*sizeof(array())->list[0]);//开辟空间
    memcpy(array()->lists,addedLists,addedCount*sizeof(arry()->lists[0]));//把类原有的方法绑定到内存后面部分
    ...
    //总结分类中的方法会在内存中的位置会放在原类的前面,分类本身的顺序由编译顺序来体现
}

值得注意的地方是,尽管Category定义中有存放属性的变量,但是Category并不会给属性生成实例变量,没有setter和getter方法,因此不能直接在Category中使用属性,若要在Category中使用属性,需要借助关联对象,手动的添加setter和getter方法

    objc_getAssociatedObject(self,@"newString");//取
    objc_setAssociatedObject(self,@"newString",newString,OBJC_ASSOCIATION_COPY);//存
}

15.Category中使用关联对象生成属性的原理是什么?

  • 首先实例化一个AssociationsManager对象,用来管理所有的关联对象,AssociationManager内部有一个AssociationsHashMap列表,通过散列映射来管理对象和ObjcAssociation之间的关系,而ObjcAssociation才是真正存储关联对象的地方
  • 当对象在释放的时候会检查isa指针的第2位,如果是1就说明存在关联对象,然后在全局的关联对象管理者中删除这个关联绑定对象
  • 关联一个nil的值就是删除这个关联对象 16.Category中有load方法吗?Category中的load方法是什么时候调用的?load方法能继承吗?
  • 虽然load方法是基类NSObject的类方法,但是load方法不可以被子类继承,原因是load方法并不是通过消息传递(objc_msgSend)调用的,而是通过函数指针直接调用,因此,load方法不存在类的层级遍历
  • 类别中的load方法是独立的,跟原类中的load方法没有关系
  • load方法的调用顺序,先调用父类中的load方法,再调用子类中的load方法,最后再调用分类中的load方法 我们来看看原理,找到最重要的方法load_images:
    prepare_load_methods((const headerType *)mh);//在类和分类中查找所有满足条件的load方法
    call_load_method();//调用所有的load方法
}
    ...
    schedule_class_load(remapClass(classlist[i]));//获取子类和父类中的方法
    add_category_to_loadable_list(cat);//获取分类中的方法
    总结:父类中的方法是递归查找,并且把父类的load方法放置在子类的前面,这里就解释了为什么父类中的load方法会先调用了
}
    ...
    loadable_categories[loadable_categories_used].method = method;
    //将分类中的load方法都保存在loadable_categories列表中
    ...
}

从以上代码中知道,load方法的加载顺序是先调用类中的load方法,再调用分类中的load方法,接下来我们看看call_class_loads是如何真正调用的

    ...
    for(i = 0; i < used; ++i){
    load_method_t load_method = (load_method_t)classes[i].method;
    ...
    (*load_method)(cls,SEL_load);//通过函数指针调用load方法
    //总结:通过上面代码可以分析得到,load是通过函数指针调用,并且调用顺讯是先调用父类再调用子类,最后才是分类,分类的调用顺序跟编译顺序有关
  }
}

17.block的原理是什么?

  • block的本质就是闭包
  • block变量的捕获原因可以通过clang -rewrite-objc main.m命令得到,__block修饰的变量在底层是引用的指向,所以用__block修饰的变量是可以修改值的
    void *isa;//类似于对象的isa指针,指向block所存的数据区域
    int Flags;
    int Reserved;
    void *FuncPtr;//指向block的函数执行地址
    //总结:block的底层实现是用结构体实现的,而block的调用是用函数实现
}
  • _NSConcreateStackBlock:block存放在栈区(使用weak修饰的block变量或者参数block)
  • _NSConcreteMallocBlock:block存放在堆区(存在变量捕获)
  • _NSConcreteGlobalBlock:block存放在全局区(没有变量捕获)
  • 可以通过__block修饰自动变量从而达到修改变量的目的,而全局和静态的变量则不需要这样做
  • __forwarding的作用是,block的创建一般是在栈上,在block复制到堆上之后,__forwarding原本指向自己,这个时候会指向堆上的block,是为了防止栈上的block释放后发生未知错误 18.如何解决block的循环引用?
  • 使用完block之后,手动将一方设置为nil
  • 使用weak来修饰变量
  • 通过weak-strong结合使用来解决循环引用
  • 通过heap-stack(栈参数)来解决循环引用,这种方式就是将self通过参数的形式传入block,由系统来管理,开发者无需担心循环引用 19.Objective-C类方法load和initialize的却别有哪些?
  • load方法和initalize方法的相同点两者都可以做一些初始化的工作
  • 不同点是load方法不能继承,因为它是使用函数指针调用的
  • initialize方法是通过消息转发调用的,可以继承,如果子类中没有实现会调用父类中的方法,并且会调用两次需要我们注意,我们可以使用dispatch_once_t来解决多次调用问题
  • initialize会先调用父类再调用子类,如果有分类则会优点调用分类 20.copy方法是深复制还是浅复制
  • copy既可以作为深复制使用,也可以作为浅复制使用,当作为浅复制使用的时候,复制的是对象的指针,当作为深复制来使用的时候,复制的是对象的内容
  • 不管是深复制还是浅复制,复制出来的对象都是不可变的,不同于mutableCopy复制出来的对象是可变的