美文网首页
iOS-面试

iOS-面试

作者: 李永开 | 来源:发表于2019-01-11 18:10 被阅读0次

    一.NSObjcet对象在内存中占多少个字节?

    1. 点进去NSObject,发现NSObject内部只有一个Class.
         @interface NSObject <NSObject> {
            Class isa  OBJC_ISA_AVAILABILITY;
        }
    
    1. Class是一个objc_class结构体指针,指向objc_class这个结构体
        typedef struct objc_class *Class;
    
    1. objc_class内部实现
      !__OBJC2__代表:不是objc2这个版本才可以使用下面的定义;
      Use Class instead of struct objc_class:让我们使用Class这个结构体.
    struct objc_class {
        Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
    
    #if !__OBJC2__
        Class _Nullable super_class                              OBJC2_UNAVAILABLE;
        const char * _Nonnull name                               OBJC2_UNAVAILABLE;
        long version                                             OBJC2_UNAVAILABLE;
        long info                                                OBJC2_UNAVAILABLE;
        long instance_size                                       OBJC2_UNAVAILABLE;
        struct objc_ivar_list * _Nullable ivars                  OBJC2_UNAVAILABLE;
        struct objc_method_list * _Nullable * _Nullable methodLists                    OBJC2_UNAVAILABLE;
        struct objc_cache * _Nonnull cache                       OBJC2_UNAVAILABLE;
        struct objc_protocol_list * _Nullable protocols          OBJC2_UNAVAILABLE;
    #endif
    
    } OBJC2_UNAVAILABLE;
    /* Use `Class` instead of `struct objc_class *` */
    

    4.从源码中找到Class结构体(从下往上看)

    #:类最初始的信息,不包含catagory等的信息---ro的意思:只读
    struct class_ro_t {
        uint32_t flags;
        uint32_t instanceStart;
        uint32_t instanceSize;
    #ifdef __LP64__
        uint32_t reserved;
    #endif
    
        const uint8_t * ivarLayout;
        
        const char * name;
        method_list_t * baseMethodList;
        protocol_list_t * baseProtocols;
        const ivar_list_t * ivars;
    
        const uint8_t * weakIvarLayout;
        property_list_t *baseProperties;
    }
    
    #:类的具体信息----rw的意思:可读可写
    struct class_rw_t {
        // Be warned that Symbolication knows the layout of this structure.
        uint32_t flags;
        uint32_t version;
    
        const class_ro_t *ro;//包含了上面的结构体
    
        method_array_t methods;
        property_array_t properties;
        protocol_array_t protocols;
    
        Class firstSubclass;
        Class nextSiblingClass;
    
        char *demangledName;
    }
    
    //因为是继承关系,所以objc_class里面也有一个isa指针
    struct objc_class : objc_object {
        // Class ISA;
        Class superclass;
    
    #方法缓存
        cache_t cache;             // formerly cache pointer and vtable
    
    #:bits用来获取具体的类信息,bits&FAST_DATA_MASK就可以拿到struct class_rw_t的信息.
        class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags
    }
    

    答案:
    arm64架构的数据总线为64bits,也就是8bytes,所以在arm64架构里面一个指针占8个字节.
    如果是arm32,那么就占4个字节.

    拓展
    demo1:

    使用runtime的class_getInstanceSize()方法得到一个 的大小内存对齐过的
    使用malloc_size()得到的是实际分配的内存大小

    -----#import<malloc/malloc.h>
    -----5s,6等arm64架构下输出
    -----
    ----- NSLog(@"%zd",  -----class_getInstanceSize([NSObject class]));//8
    -----
    ----- NSObject *objc = [[NSObject alloc]init];
    ----- NSLog(@"%zd",malloc_size((__bridge const void *)(objc)));//16
    
    • 可以看出来,虽然Class大小应该是8个字节,但是系统实际上给分配了16个字节.
      所以NSObjcet对象在内存中占16个字节(arm64架构).
    demo2:
    @interface Father: NSObject
    {
      int age;
      int weight;
      int height;
    }
    @end
    @implementation Father
    @end
    
    打印:NSLog(@"%zd", class_getInstanceSize([Father class]));
    输出:2019-01-10 10:14:24.498 iOSWorld[2913:385535] 24
    
    Father *f = [[Father alloc]init];
    NSLog(@"%zd",malloc_size((__bridge const void *)(f)));
    输出32.
    

    注意:因为Father继承自NSObject,所以它内部有NSObject的实现,那么它占的内存为8(NSObject的isa指针)+4x(Father的3个int类型) = 8 + 12 =20个字节
    但是:因为结构体计算内存的时候,要考虑内存对齐(和数据总线有关)的问题.
    不满8个字节也要算8个字节,也就是必须是8的倍数,所以实际占用24个字节.
    但但但又因为:苹果规定了每个对象在内存中至少16个字节,也就是16的倍数.所以f这个实例对象实际上占用了32个字节.(objc4源码的alloc方法里面有苹果的规定)
    结论:Father对象其实只需要24个字节的空间,但实际上给分配了32个字节.

    demo3:
    @interface Father : NSObject
    {
        int age;
    }
    @end
    @implementation Father
    @end
    @interface Son : Father
    {
        int weight;
    }
    @end
    @implementation Son
    @end
    打印:NSLog(@"%zd", class_getInstanceSize([Son class]));
    输出:2019-01-10 10:48:00.284 iOSWorld[2940:390486] 16
    

    father占了8(NSObject 指针) + 4(int类型) = 12
    son 继承自 father,所以son的c++结构体应该张这样

    struct Son_IMPL{
      struct Father_IMPL s; //16个字节
      int weight;//4个字节
    }
    

    乍一看16+4= 20, 但是答案是16.
    原因还在于内存对齐,毕竟Father_IMPL还有4个字节是空的,刚好容得下weight这个int

    注意:

    1.结构体内存对齐:结构体的大小必须为最大成员大小的倍数
    用空间换时间,合理利用内存的地址.
    2.OC中,对象的变量和方法不在同一区域,所以对象在内存中占的大小不包括方法.
    解释:NSObject实际上使用了8个字节,但是apple规定分配了16个字节,所以NSObject实际上占用了16个字节(使用和占用不太一样哦)

    apple规定:如果一个对象分配的内存<16,那么就等于16.而且对象分配的内存为16的倍数猜想:给8个字节太小了,放个指针就没有了,都没地方放成员变量,所以就以16为单位了吧😅
    从源码看规定,下面是方法的依次调用

    1.
    + (id)allocWithZone:(struct _NSZone *)zone {
       return _objc_rootAllocWithZone(self, (malloc_zone_t *)zone);
    }
    2.
    obj = class_createInstance(cls, 0);
    3.
    class_createInstance(Class cls, size_t extraBytes)
    4.
    _class_createInstanceFromZone(cls, extraBytes, nil);
    5. 拿到size,分配内存
    size_t size = cls->instanceSize(extraBytes);
    obj = (id)calloc(1, size);
    
    6. size的实现 ,如果<16,则=16
    size_t size = alignedInstanceSize() + extraBytes;//extraBytes为0
    // CF requires all objects be at least 16 bytes.
    if (size < 16) size = 16;
    
    7.alignedInstanceSize()的实现,也就是内存对齐的实现.
    unalignedInstanceSize是对象的实际大小,不经过内存对齐的.经过word_align后就是内存对齐过的.
    word_align(unalignedInstanceSize())
    

    二.类的信息放在哪里?

    *OC对象共3种:
    1.实例对象:存放isa指针(指向类)和变量
    2.类对象:存放isa指针(指向元类)、superClass、属性、对象方法、协议、成员变量
    3.元类对象:存放isa指针(指向元类本身)、superClass、类方法
    所有元类的isa指针都指向根元类(rootMetaClass)
    根元类的superClasss指向RootClass
    答案:类的信息放在元类里面

    拓展:
    @interface Father: NSObject
    {
        int age;
    }
    @end
    @implementation Father
    @end
    Father *father = [[Father alloc]init];
    

    father这个实例对象里面的isa地址:p/x (long)father->isa0x1a1003a7cf9
    Father这个类对象的地址:NSLog(@"%p",object_getClass(father));0x1003a7cf8
    p/x 0x1a1003a7cf9 & 0xffffffff8 = 0x1003a7cf8
    object_getClass获取的是isa指针指向的类,而objc_getClass是通过查找mapPair(字典)的形式获取类
    apple官方源码:实例对象的isa指针需要进行&计算才能得到类对象的真正地址

    为啥需要&运算呢?(下面为objc源码)

    #union:共用体
    union isa_t {
       isa_t() { }
       isa_t(uintptr_t value) : bits(value) { }
    
       Class cls;
       uintptr_t bits;
    #if defined(ISA_BITFIELD)
       struct {
           ISA_BITFIELD;  // defined in isa.h
       };
    #endif
    };
    
    #ISA_BITFIELD的定义
    # if __arm64__
    #   define ISA_MASK        0x0000000ffffffff8ULL
    #   define ISA_MAGIC_MASK  0x000003f000000001ULL
    #   define ISA_MAGIC_VALUE 0x000001a000000001ULL
    #   define ISA_BITFIELD                                                      \
         uintptr_t nonpointer        : 1;                                       \
         uintptr_t has_assoc         : 1;                                       \
         uintptr_t has_cxx_dtor      : 1;                                       \
         uintptr_t shiftcls          : 33; /*MACH_VM_MAX_ADDRESS 0x1000000000*/ >\
         uintptr_t magic             : 6;                                       \
         uintptr_t weakly_referenced : 1;                                       \
         uintptr_t deallocating      : 1;                                       \
         uintptr_t has_sidetable_rc  : 1;                                       \
         uintptr_t extra_rc          : 19
    

    因为apple在arm64后,对isa做了优化.
    原来的isa直接指向类对象或元类对象地址,现在优化过后将位域的33bits保存类对象或元类对象地址,其他的64-33=31位保存其他信息.

    三.extention和category的区别?

    • extension看起来很像一个匿名的category,extension在编译期决议,它就是类的一部分,一般用来隐藏类的私有信息.
    • category则完全不一样,它是在运行期决议的.在运行程序的时候,内核会加载dyld,dyld会加载各种动态库镜像,会加载catagory并将catagroy的类方法添加到已经编译好的类的方法列表里面,然后才会调用main()函数.
      所以说:使用catagory会降低程序的运行速度.
    • 所以extension可以添加实例变量,而category是无法添加实例变量的(因为在运行期,对象的内存布局已经确定,如果添加实例变量就会破坏类的内部布局,这对编译型语言来说是灾难性的.相对的,对象的方法定义都保存在类的可变区域中。

    四. + (void)load方法调用时机?

       //1.从objc-os.mm文件的void _objc_init(void)方法进来
        _dyld_objc_notify_register(&map_images, load_images, unmap_image);
    
       //2.进入load_images
       //判断是否有load方法,没有直接返回
       if (!hasLoadMethods((const headerType *)mh)) return;
    
       //3.发现load方法
       prepare_load_methods((const headerType *)mh);
    
       //4.添加到loadable_classes表中去,这个表中维护了所有类,及其对应的load方法的IMP
       schedule_class_load(remapClass(classlist[i]));
        //内部实现:先递归添加父类的load方法
        schedule_class_load(cls->superclass);
    
       //5.获取非懒加载Category的列表,添加到loadable_categories表中去
       add_category_to_loadable_list(cat);
     
       //6.调用load方法
       call_load_methods();
       //内部实现
        void *pool = objc_autoreleasePoolPush();
        do {
            // 1. Repeatedly call class +loads until there aren't any more
            //调用class的load方法
            while (loadable_classes_used > 0) {
                call_class_loads();
            }
    
            // 2. Call category +loads ONCE
            //调用Category的load方法
            more_categories = call_category_loads();
    
            // 3. Run more +loads if there are classes OR more untried categories
        } while (loadable_classes_used > 0  ||  more_categories);
    
        objc_autoreleasePoolPop(pool);
    
    • + (load)(在main函数开始执行之前,调用顺序和编译顺序无关,并且只调用一次)的顺序是先执行父类中的+load,然后是子类,再然后执行category中的+load如果有多个category,那么就按照编译的顺序来调用+load.
    • 而且分类的load()方法不会覆盖原来类的load()方法,也就是说所有的load方法都会执行.

    五.+ (void)initialize方法调用时机?

    • 在类第一次接收到消息的时候调用,比如第一次alloc的时候.
    • 调用子类的initialize()时候,会先调用父类的initialize().毕竟你爸爸都不存在,你还怎么玩继承呢?
    • catagory的initialize()方法会覆盖原来类的initialize()方法(这是和load()方法的区别)
      解释:initialize()方法走的是msg_send(),所以catagory的initialize()方法调用顺序优先级要高于原来类的优先级
    源码:
    void callInitialize(Class cls)
    {
       ((void(*)(Class, SEL))objc_msgSend)(cls, SEL_initialize);
       asm("");
    }
    

    六.kvc设置值会触发kvo吗

    答案:会的.

    • 如果有set(),那么kvc会触发set(),set()又会触发kvo
    • 如果没有set(),kvc内部会调用willChangeValueForKey()didChangeValueForKey(),同样也会触发kvo

    拓展 : kvc的实现
    kvc会依次寻找下面的方法

    1. setName()
    2. _setName()
    3. 判断accessInstanceVariablesDirectly.如果返回YES,那就开始查找成员变量
    4. 给_name赋值
    5. 给_isName赋值
    6. 给name赋值
    7. 给isName赋值
    8. 如果都没有,报错valueForUndefinedKey.

    七. 打印结果是什么?

    创建了一个son类和father类,他们都有eat()

    @interface Father : NSObject
    - (void)eat;
    @end
    @implementation Father
    - (void)eat
    {
    }
    @end
    
    @interface Son : Father
    - (void)eat;
    @end
    @implementation Son
    - (void)eat
    {
        NSLog(@"self的类为%@",[self class]);
        NSLog(@"super的类为%@",[super class]);
    }
    @end
    
    打印结果:
    2019-01-17 16:39:36.548350+0800 demo[23333:6167589] self的类为Son
    2019-01-17 16:39:36.548401+0800 demo[23333:6167589] super的类为Son
    
    • 为什么[super class]的值也是Son呢?
    1. 使用xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m命令得到c++代码,搜索_Son_eat做删减后得到源码
    static void _I_Son_eat(Son * self, SEL _cmd) {
        NSLog( (objc_msgSend) (self, sel_registerName("class")) );
        NSLog((objc_msgSendSuper)({self, Father.class}, sel_registerName("class")));
    }
    
    1. 第一个NSLog是给self发送class消息.
      因为son没有class()方法,所以会找Father类.Father类也没有所以会找到NSObject的class()方法,所以会返回Son
    2. 第二个NSLog里面调用了objc_msgSendSuper()方法,查看objc_msgSendSuper()的源码
      里面需要传入一个结构体和SEL,和我们简化过的c++代码吻合
    #objc_msgSendSuper
    #方法说明:给一个实例对象的父类发送一条消息
    #参数1:是一个objc_super结构体,包含了接受消息的实例对象和开始搜索方法实现的超类
    #参数2:@SEL 发送的消息
    OBJC_EXPORT id _Nullable
    objc_msgSendSuper(struct objc_super * _Nonnull super, SEL _Nonnull op, ...)
        OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);
    
    1. struct objc_super结构体的源码为
      这里的receiver为刚才我们打印出来的self, super_class为Father.class.
    struct objc_super {
        /// Specifies an instance of a class.
        __unsafe_unretained _Nonnull id receiver;
        __unsafe_unretained _Nonnull Class super_class;
    };
    
    1. objc_msgSendSuper()方法我们可以得出:
      第二个NSLog的内容其实是给self发送class消息,而且查找class()这个方法的起始地为Father这个类的cache,然后是Father这个类的msthodList,然后是Father的父类NSObject.
      所以:既然还是给self发送消息,那么打印的值仍然是self的类Son

    拓展

    我们平时写得self = [super init],其实是告诉系统去父类的methodList中查找方法而已,并不是初始化父类....

    八.objc_getClass()object_getClass()区别

    1.objc_getClass()

    • 根据map表找到Class,并且只能找到类,不能找到元类.
    • 传入字符串
    Class objc_getClass(const char *aClassName)
    {
        if (!aClassName) return Nil;
    
        // NO unconnected, YES class handler
        //获取类
        return look_up_class(aClassName, NO, YES);
    }
    

    2.object_getClass()

    • 直接去找isa指针.
    • 传入实例对象,返回类.传入类对象,返回元类.
    Class object_getClass(id obj)
    {
        if (obj) return obj->getIsa();
        else return Nil;
    }
    

    九.为什么UIKit不是线程安全的

    • 成本较大,性价比低.

    十.property里面的atomic和nonatomic是怎样实现的?

    • 其实是使用了自旋锁(自旋锁适合消耗时间短的操作,而set方法刚好满足,所以使用自旋锁比使用synchronized更合适),看下面的代码
    • atomic保证了同一时刻只有一个set方法能执行,但不保证get方法.(多读单写)
    • atomic并不安全,直接调用 _age=10就跳过了set方法...
    if (!atomic) 
    {  
      oldValue = *slot;  
      *slot = newValue;  
    } 
    else
    {  
      spin_lock_t *slotlock = &PropertyLocks[GOODHASH(slot)];  
      _spin_lock(slotlock);  
      oldValue = *slot;  
      *slot = newValue;          
       _spin_unlock(slotlock);  
    }  
    

    十一.优化

    1.CALayer替代UIView2
    2.提前计算好布局
    3.不适用autolayout
    4.把耗时操作放到子线程
    5.减少图层
    6.减少透明视图的存在.
    7.避免离屏渲染
    8.减少、合并动态库
    9.减少类、分类
    10.不要把所有的东西方法didfinishLaunch

    十二.信号量和互斥锁的区别

    • 信号量用在多线程多任务同步的,一个线程完成了某一个动作就通过信号量告诉别的线程,别的线程再进行某些动作(大家都在semtake的时候,就阻塞在 哪里)
    • 而互斥锁是用在多线程多任务互斥的,一个线程占用了某一个资源,那么别的线程就无法访问,直到这个线程unlock,其他的线程才开始可以利用这个资源。比如对全局变量的访问,有时要加锁,操作完了,在解锁。有的时候锁和信号量会同时使用的
    • 也就是说,信号量不一定是锁定某一个资源,而是流程上的概念,比如:有A,B两个线程,B线程要等A线程完成某一任务以后再进行自己下面的步骤,这个任务 并不一定是锁定某一资源,还可以是进行一些计算或者数据处理之类。而线程互斥量则是“锁住某一资源”的概念,在锁定期间内,其他线程无法对被保护的数据进行操作。在有些情况下两者可以互换。
      互斥量和信号量的区别
    1. 互斥量用于线程的互斥,信号量用于线程的同步。
      这是互斥量和信号量的根本区别,也就是互斥和同步之间的区别。
      互斥:是指某一资源同时只允许一个访问者对其进行访问,具有唯一性和排它性。但互斥无法限制访问者对资源的访问顺序,即访问是无序的。
      同步:是指在互斥的基础上(大多数情况),通过其它机制实现访问者对资源的有序访问。在大多数情况下,同步已经实现了互斥,特别是所有写入资源的情况必定是互斥的。少数情况是指可以允许多个访问者同时访问资源
    2. 互斥量值只能为0/1,信号量值可以为非负整数。
      也就是说,一个互斥量只能用于一个资源的互斥访问,它不能实现多个资源的多线程互斥问题。信号量可以实现多个同类资源的多线程互斥和同步。当信号量为单值信号量是,也可以完成一个资源的互斥访问。
    3. 互斥量的加锁和解锁必须由同一线程分别对应使用,信号量可以由一个线程释放,另一个线程得到。

    总结:
    1.信号量用于同步,互斥锁用于同步.信号量不会锁定资源,而互斥锁会锁定资源.
    2.信号量的值为非负整数,互斥锁只有0和1.
    3.信号量可以在一个线程释放,在另一线程得到.而互斥锁只能在同一线程使用.

    十三.iOS开发中静态库和动态库区别

    静态库和动态库是相对编译期和运行期的:静态库在程序编译时会被链接到目标代码中,程序运行时将不再需要改静态库;而动态库在程序编译时并不会被链接到目标代码中,只是在程序运行时才被载入,因为在程序运行期间还需要动态库的存在。

    动态库形式:.dylib和.framework
    静态库形式:.a和.framework

    使用静态库的好处

    1,模块化,分工合作
    2,避免少量改动经常导致大量的重复编译连接
    3,也可以重用,注意不是共享使用

    使用动态库的好处

    1,使用动态库,可以将最终可执行文件体积缩小
    2,使用动态库,多个应用程序共享内存中得同一份库文件,节省资源
    3,使用动态库,可以不重新编译连接可执行程序的前提下,更新动态库文件达到更新应用程序的目的。

    • 库里面都是函数,是一种可执行代码的二进制格式,可以被载入内存中执行
    • 静态库:
    1. 在程序编译时已经加入到可执行文件中
    2. 不能再多个程序间共享
    3. .a或.framework结尾
    • 动态库:
    1. 在程序运行时才去载入该库
    2. 可以多个程序间共享
    3. .tbd或.framework结尾
    • tips:苹果系统提供的framework(UIKit等)全部属于动态库,可在多个程序间共享.
    动态库和静态库的使用时机
    • iOS8以后苹果允许上架自写动态库,但是会对动态库签名,所以不可以在线更新动态库
    • 一般都选择制作静态库
    • 动态库可以使用的两个地方 1.App Extention 2. 企业级应用

    网络优化

    相关文章

      网友评论

          本文标题:iOS-面试

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