2022加油(fmy)

2022加油(fmyz)

一、基础

1.说出常用的属性修饰关键字

原子性和非原子性

  • nonatomic 非原子操作。决定编译器生成的setter和getter方法是否是原子操作。atomic表示多线程安全,一般使用nonatomic,效率高。

读写属性

  • readwrite 是可读可写特性。需要生成getter方法和setter方法。
  • readonly 是只读特性。只会生成getter方法,不会生成setter方法,不希望属性在类外改变。

内存属性

  • assign 是赋值特性。assign一般用于修饰基本数据类型。
  • retain(MRC)/strong(ARC) 表示持有特性。引用计数加1。一般修饰对象类型。
  • copy表示拷贝特性。。
  • weak 弱引用,引用计数不会加1。对象释放的时候指针自动重置为nil。多用来解决循环引用问题。如delegate和xib连出来的空间

2.atomic原子性是绝对线程安全的么?

  • atomic的本质是保证get set方法的线程安全,并不是保证修饰的对象的线程安全。
  • atomic与nonatomic的本质区别其实也就是在setter方法上的操作不同,atomic保证了getter和setter存取方法的线程安全,两者都不能保证整个对象是线程安全的。
  • nonatomic的速度要比atomic的快。

3.说下Category原理,以及为什么只能添加方法不能添加属性?

  • category_t结构体主要包含方法列表、属性列表、协议列表,不包含成员变量列表。
  • 分类实现是将分类中的方法、属性、协议放在category_t结构体中,运行时结构体中的方法拷贝到对象的方法列表methodList中。
  • 可以添加属性,但不会生成set、get方法,因为Category_t结构体中不存在成员变量列表。成员变量列表是存在实例对象结构体中的,并在编译阶段就已经决定好了。分类是在运行时才去加载的,无法拷贝属性到对象的成员变量列表。
  • category_t 结构体
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; // 属性
    // Fields below this point are not always present on disk.
    struct property_list_t *_classProperties;
    method_list_t *methodsForMeta(bool isMeta) {
        if (isMeta) return classMethods;
        else return instanceMethods;
    }
    property_list_t *propertiesForMeta(bool isMeta, struct header_info *hi);
};

4.load 和 initialize的区别

(1)调用时机:

  • load方法在pre-main阶段当装载类信息的时候就调用
  • initialize是当类第一次接收到消息的时候。

(2)调用顺序:

  • load方法的调用顺序为父类load > 子类 load > 分类load,且都会调用;
  • initialize调用顺序为父类initialize > 子类或者分类initialize,且父类initialize只会调用一次,如果有分类initialize会覆盖子类的initialize

(3)调用方式

  • load方法是通过isa指针找到对应的方法与实现
  • initialize方法是通过runtime的消息转发机制调用的

(4)实际使用

  • load方法一般用于方法交换
  • initialize方法一般用于初始化全局变量或静态变量。

5.分类和扩展之间的区别

(1)Category

  • 不可以添加属性,需要通过runtime添加属性。
  • 如果分类中有和原有类同名的方法, 会优先调用分类中的方法, 就是说会忽略原有类的方法。所以同名方法调用的优先级为 分类 > 本类 > 父类。因此在开发中尽量不要覆盖原有类
  • 如果多个分类中都有和原有类中同名的方法, 那么调用该方法的时候执行谁由编译器决定;编译器会执行最后一个参与编译的分类中的方法

(2)Extension

  • ExtensionCategory的一个特例。类扩展与分类相比只少了分类的名称,所以称之为“匿名分类”。一般写在.m文件中,用来扩展私有属性和方法。
  • 定义在 .m 文件中的类扩展方法为私有的,定义在 .h 文件(头文件)中的类扩展方法为公有的。类扩展是在 .m 文件中声明私有方法的非常好的方式。

(3)区别

  • Category只可以添加方法,Extension可以添加属性和方法。
  • Extension只能用于自身类,而不能用于子类或者其他地方。
  • Extension中声明的方法必须要实现,否则编译器警告。Category不会。这是因为Extension是编译阶段被添加到类中,Category是运行时被添加到类中。

6.多个分类中都有相同的方法名,执行那个分类?

  • 和编译顺序有关,执行最后一个编译的分类
  • 可以在buildPhases->Compile Sources里面调整编译顺序

7.给Category设置关联对象实现原理

  • 关联对象并不是放在了原来的对象里面,而是自己维护了一个全局的map用来存放每一个对象及其对应关联属性表格
  • 关联对象并不是存储在被关联对象本身内存中,而是存储在全局的统一的一个AssociationsManager中,如果设置关联对象为nil,就相当于是移除关联对象

8.控制器的生命周期

参考链接

9.怎么样让自己的对象用copy修饰

  • 实现NSCopying协议并实现-(id)copyWithZone(NSZone *)zone协议方法
  • 如有可变类型需要实现NSMutableCopying协议并实现协议方法

10.如何创建一个单例

  • 1.首先GCD实现单例,只要我们在外面调用shareManager这个方法,返回的对象始终是一个,因为dispatch_once只执行一次
static DataManager *manager = nil;

@implementation DataManager

+ (instancetype)shareManager{
   static dispatch_once_t onceToken;
   dispatch_once(&onceToken, ^{
       manager = [[self alloc] init];
   });
   return manager;
}
@end

  • 2.如果我们不小心调用了allocallocWithZone方法,就不是同一个对象了。调用alloc方法时,会自动调用allocWithZone方法,此时只需要重写AllocWithZone即可
+ (id)allocWithZone:(struct _NSZone *)zone{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        manager = [super allocWithZone:zone];
    });
    return manager;
}

3.不小心调用了copy、mutableCopy方法(需要实现NSCopyingNSMutableCopying协议实现copyWithZonemutableCopyWithZone代理方法,否则崩溃)

- (nonnull id)copyWithZone:(nullable NSZone *)zone {
    return manager;
}

- (nonnull id)mutableCopyWithZone:(nullable NSZone *)zone {
    return manager;
}

参考链接

二、项目架构

1.RAC关键字用法

  • RAC中关键字用法

三、网络类

1.讲一下https通信过程

参考链接

四、内存管理

1.内存中的五大区

  • 栈:局部变量、函数的参数、对象的指针。运行时分配。栈是由编译器自动分配并释放。
  • 堆:使用allocnew生成的对象。运行时分配。堆是由程序员分配和释放。
  • 全局区、静态区:包含为初始化的数据和已经初始化的数据。编译时分配
  • 常量区:常量字符串。编译时分配
  • 代码:存放程序的二进制代码。编译时分配

2.内存管理方式

(1)TaggedPointer

  • 对于小对象如NSNumber,指针指向对象,不再指向地址

(2)NONPOINTER_ISA(非指针型的ISA)

  • 参考链接

(3) SideTables 散列表

  • 自旋锁
  • 引用计数表
  • 弱引用表

3.ARC&MRC

ios开发中是通过引用计数来进行对象内存管理的。主要分为两种方式

  • MRC: 通过手动引用计数来进行对象的内存管理
  • ARC: 通过自动引用计数来管理内存。编译器LLVM和Runtime(weak对象的释放)来共同协作在对应的位置插入相应的retain和release操作,实现了ARC的全部功能。ARC中新增weak, strong属性关键字。
  • retain release retainCount dealloc内部实现 参考链接

以下是MRC中常用到的几个关键字

  • alloc : 分配对象的内存空间。
  • retain : 使一个对象的引用计数加1
  • release : 使对象的引用计数减1
  • retainCount : 获取当前对象的引用计数值
  • autorelease : 当前对象会在autoreleasePool结束的时候,调用这个对象的release操作,进行引用计数减1
  • dealloc : 在MRC中若调用dealloc,需要显示的调用[super dealloc],来释放父类的相关成员变量

4.weak的实现原理

  • 初始化时:调用objc_initweak函数,初始化一个心得weak指针指向对象地址
  • 添加引用时:objc_initweak调用objc_storeWeak函数。objc_storeWeak的作用是更新指针指向,创建对应的弱引用表。objc_storeWeak调用weak_register_no_lock函数,讲weak指针添加到弱引用表中。添加的位置是通过哈希算法查找的。
  • 释放时:调用clearDeallocating函数,clearDeallocating函数通过调用weak_clear_no_lock方法根据对象地址获取所有weak指针数组,然后遍历该数组将其中数据置为nil,最后把这个entry从weak表中移除,最后清理对象记录。

弱引用表结构

   struct weak_table_t {
    // 保存了所有指向指定对象的 weak 指针
    weak_entry_t *weak_entries;
    // 存储空间
    size_t    num_entries;
    // 参与判断引用计数辅助量
    uintptr_t mask;
    // hash key 最大偏移值
    uintptr_t max_hash_displacement;
};

  • 参考1
  • 参考2

5.什么情况下会导致内存泄漏

  • NSTimer :self 持有 timer,timer 在初始化时持有 self,造成循环引用。 解决的方法就是,在dealloc方法以外,使用 invalidate 方法销掉 timer。
  • block : block中使用self时,会导致self,block的互相持有,无法释放。__weak修饰
    代理使用Strong修饰也会导致
  • 通知及kvo没有移除监听也会导致内存泄漏
  • OC中直接运用C语言 (静态分析analyze)

五、Block

1.block用什么关键字修饰?

  • MRC情况下:
    Block分为三种类型,全局、栈区、堆区。当没有访问外部变量的时候属于全局block,访问局部变量且没有被copy时属于栈区block,访问局部变量且copy后属于堆区block。栈区内存可能随时被回收和释放,释放后再对该block引用,会造成系统崩溃。使用 copy 修饰,会将栈区的 block 拷贝到堆区,retain修饰会在栈区 ,所以使用copy。

  • ARC情况下
    当没有访问外部变量的时候属于全局block,访问局部变量且没有强指针指向的时候属于栈区block,
    访问局部变量且有强指针指向的时候属于堆区block。用strong、 copy修饰的都在堆区,所以用copy strong都可以。平常使用都用copy修饰,主要是strong是ARC时期引入的,开发者早已在MRC中习惯使用copy来修饰block。

  • block建立在栈上,而不是堆上,这么做一个是为性能考虑,还有就是方便访问局部变量。

参考链接

六、进程线程

1. 进程和线程区别

  • 进程:是一个有独立功能的程序,可以理解为手机上的一个app。每个进程是相互独立的,每个进程运行在受保护的内存空间。
  • 线程:进程执行的最小单位,一个进程要执行任务最少开启一条线程(主线程)。线程是cpu分配资源和调度的最小单位。
  • 区别:(1)线程是进程的执行单元,进程中所有任务都在线程中执行。(2)同一个进程内的线程共享进程资源

七、cocoapods

  • 参考链接1

八、RunLoop

1.自动释放池与runloop关系?

  • 第一个 Observer 监视的事件是 Entry(即将进入Loop),其回调内会调用 _objc_autoreleasePoolPush() 创建自动释放池。优先级最高,保证创建释放池发生在其他所有回调之前。第二个 Observer监视了两个事件:BeforeWaiting(准备进入休眠)时调用_objc_autoreleasePoolPop()_objc_autoreleasePoolPush()释放旧的池并创建新池;Exit(即将退出Loop)时调用_objc_autoreleasePoolPop()来释放自动释放池。优先级最低,保证其释放池子发生在其他所有回调之后。
  • 顺序就是Entry-->push ---> BeforeWaiting--->pop-->push -->Exit-->pop,按照这样的顺便,保证了,没一次push都对应一个pop。

2.请指出下面这段代码问题

 for (int i = 0; i < 10000; i++ ) {
        NSString *str = @"ABC";
        str = [str lowercaseString];
        str = [str stringByAppendingString:@"xyz"];
        NSLog(@"%@",str);
    }
  • 内存得不到及时地释放,内存暴涨.RunLoop是在每个事件循环结束后才会自动释放池去使对象的引用计数减一,对于引用计数为0的对象才会真正被销毁、回收内存。正确的写法如下
for ( int i = 0 ; i < 10000 ; ++ i ) {
   @ autoreleasepool {
     NSString *str = @"Abc" ;
     str = [ str lowercaseString ] ;
     str = [ str stringByAppendingString : @"xyz" ] ;
      
     NSLog ( @"%@" , str ) ;
   }
}

九、性能优化

1.降低app包大小

  • 利用 AppCode 检测未使用的代码:菜单栏 -> Code -> Inspect Code
  • 无损压缩项目中的图片资源,删除无用资源图片
  • 编译器优化项目优化(编译器优化级别、去除符号信息)
  • 去掉舍弃架构armv7
  • 去掉oc与swift混编

2. 正确的复用cell

  • 尽量避免使用透明色
  • 动态计算并缓存行高,避免重新布局
  • 加载网络数据,使用异步加载,缓存请求结果
  • 滑动很快时,按需加载范围内的cell
  • 刷新使用reloadSection
  • cell中的控件尽量少,避免动态的添加视图
  • 不要做多余的绘制工作
  • 尽量少用xib

3.APP的启动优化

优化的时候会将优化以main()函数为界分为2个部分,即main之前的pre-main阶段 和 main()之后。

(1) pre-main阶段这个阶段是由dyld(动态连接器)来操作的,设置DYLD_PRINT_STATISTICS进行检测这个阶段耗时。

  • dylib loading time: 动态库加载耗时(169.23ms)。关于动态库的加载,这个是不可避免的,我们能做的就是减少动态库的引用,官方的建议的是动态库的使用应该在6个以内,所以这里就引入了一个动态库合并的概念,通过合并动态库,从而减少在pre-main时的加载时间。

  • rebase/binding: 偏移修正/符号绑定。这个过程由操作系统完成。(ASLR安全机制,在二进制文件头部添加随机值)/

  • ObjC setup: OC类注册。这也就意味着项目中OC类越多,这里消耗的时间也就会增加。

  • initializer: 这个阶段指的是+ (void)loadC++构造函数等初始化操作。 这里可以看到用时5.1 seconds,是所有项做高的。这里是因为我在项目里面随便的一个类里实现了+(void)load函数,并模拟了一个耗时操作。所以这里的优化比较明确:1. 能不使用+load就尽量不要使用,可以将load内部逻辑推迟到initialize时;2. 使用到了load,就尽量不要在内部执行耗时操作;3. 如果混编了C++代码,要尽量减少构造函数中的耗时操作

  • slowest intializers: 启动时用时最慢的文件,这个可以看到耗时最多的是TestApp项目本身,这里主要是由于那个模拟的耗时操作导致。

(2) 在main()函数之后的优化就因项目不同而异了,大致有这么几个核心:

  • 业务逻辑:这里主要指APP从启动到首页呈现的阶段。尽量减少与该阶段无关且没有必要的初始化代码操作,把这部分代码以懒加载的方式处理。
  • 删除无用代码:这里是随着业务的发展,APP不断的迭代更新,会产生很多的的下架业务,从而堆积了很多的无用代码,这些代码会增加ObjC setup的耗时,所以要清理掉。
  • 多线程操作:在启动时,将一些必要的非UI业务且需要初始化操作的任务放在子线程中,这样可以在APP启动的时候,发挥CPU的最大性能。
  • 启动页面:首要呈现的画面,尽量减少使用 .xib 或storyBoard来实现,因为它们需要解析成代码,会造成耗时。

文章参考

你可能感兴趣的:(2022加油(fmy))