interview-Normal

736 阅读54分钟

优秀第三方库总结

属性修饰符

  • 线程安全类 nonatomic/atomic
  • 读写权限类 readwrite/readonly
  • 内存管理类 assign/copy/strong/weak

还有其他的例如

  • class 类属性
  • setter=/getter= 自定义setter/getter方法名
  • retain MRC下内存管理

内存泄露

  • Block
    __weak typeof(self) weakself = self;
    self.textBlock = [TestBlock headerWithRefreshingBlock:^{
        //使用strongself 防止被提前释放
        __strong typeof(self) strongself = weakself;
        strongself.data ... 
    }];
  • 代理
@property (nonatomic, weak) id delegate;
  • NSTimer 解决 1.使用block timer方法 2.使用class_addMethod()转移引用 3.使用NSProxy虚基类转移引用
  • 非OC类 解决: 主动调用释放方法
    CGImageRef ref = [context createCGImage:outputImage fromRect:outputImage.extent];
    
    UIImage *endImg = [UIImage imageWithCGImage:ref];
    
    _imageView.image = endImg;
    
    CGImageRelease(ref);//非OC对象需要手动内存释放
  • 地图类 由于地图类会生成大量实例,内存会暴涨,所以需要及时管理
- (void)clearMapView{
    self.mapView = nil;
    self.mapView.delegate =nil;
    self.mapView.showsUserLocation = NO;
    [self.mapView removeAnnotations:self.annotations]; 
    [self.mapView removeOverlays:self.overlays];  
    [self.mapView setCompassImage:nil];
}
  • 大次数循环内存暴涨问题 使用@autoreleasepool
 for (int i = 0; i < 100000; i++) {
        @autoreleasepool {
            NSString *string = @"Abc";
            string = [string lowercaseString];
            string = [string stringByAppendingString:@"xyz"];
            NSLog(@"%@", string);            
        }
    }

分类和扩展

  • 分类:能在添加方法,对外暴露;添加属性需要使用关联对象;不能添加成员变量

关联对象示例: set方法objc_setAssociatedObject中的4个参数:Object、Key、Value、Policy的关系 Map{Object:Map{Key:{Value+Policy}}}

.h
#import <UIKit/UIKit.h>

@interface UIView (DTBoderAdd)
/** 边框圆角 * */*
**@property** (**nonatomic**,**assign**) CGFloat cornerRadius;
@end

.m
- (void)setCornerRadius:(CGFloat)cornerRadius {
    self.layer.cornerRadius = cornerRadius;
    objc_setAssociatedObject(self, @selector(cornerRadius), @(cornerRadius), OBJC_ASSOCIATION_ASSIGN);
}

- (CGFloat)cornerRadius {
    return [objc_getAssociatedObject(self, @selector(cornerRadius)) floatValue];
}

  • 扩展:能在.m文件中添加属性、方法、成员变量,不能对外暴露

static

  • static修饰局部变量: 该局部变量只能初始化一次,在内存中的地址不变,并随着程序的结束而销毁掉(延长了局部变量的生命周期)
  • static修饰全局变量: 本来是在整个源程序的所有文件都可见,static修饰后,改为只在申明自己的文件可见,即修改了作用域。

热更新

bugly热更新

Bugly卡顿监控

编译架构

指令集

  1. armv7|armv7s|arm64都是ARM处理器的指令集,用于真机
  2. i386|x86_64 是Mac处理器的指令集,用于模拟机

64与32位

  • 模拟器32位处理器测试需要i386架构,
  • 模拟器64位处理器测试需要x86_64架构,
  • 真机32位处理器需要armv7(<=iPhone4S),或者armv7s(iPhone5,iPhone5C)架构,
  • 真机64位处理器需要arm64(>=iPhone5s之后机型)架构。

Xcode设置架构 截屏2022-01-10 下午8.50.32.png

参考 iOS armv7, armv7s, arm64区别与应用32位、64位配置

如何处理第三方SDK冲突

  • 两个第三方SDK冲突时,在编译时报 duplicate symbol 错误
  • 方法:移除合并冲突文件
  • 步骤
  1. 使用终端cd到要修改的sdk所载文件目录(最好备份一个出来,改完再替换)
  2. 查看静态库文件包的架构:使用 lipo -info xxx.a命令,lipo -info命令可以查看Mach-o文件的架构。
/Users $ lipo -info /Users/masonry.framework
Architectures in the fat file: $ file /Users/testDemo are: armv7 armv7s arm64

3. 使用 lipo -thin命令分离出相应的架构

lipo -thin armv7 Masonry -output Masonry-armv7

  1. 使用ar -t xxx.a命令,查看库中所包含的文件列表,eg:ar -t Masonry-armv7
  2. 使用 ar -d -sv XXXXX-armv7.a XXXX.o,从每个架构文件中删除对应的冲突.o文件,eg:
ar -d -sv Masonry-armv7 MASCompositeConstraint.o
ar -d -sv Masonry-armv7 MASViewConstraint.o

  1. 使用 lipo -create命令把armv7、i386、x86_64、arm64 所有架构的重复文件按照上面方法删除完成后。我们要把这几个架构合并到原来的库里面。eg: lipo -create Masonry-x86_64 Masonry-armv7 Masonry-i386 Masonry-arm64 -output Masonry

  2. 最后将这个新生成的静态库文件替换掉之前的,发现冲突解决。

参考 iOS-集成多个SDK内部文件重复冲突解决

WKWebViewJavascriptBridge 原理

  • Native to JavaScript:原生通过[_webView stringByEvaluatingJavaScriptFromString:javascriptCommand]: 方法执行一段 JavaScript
  • JavaScript to Native:在网页中加载一个 Custom URL Scheme 的链接(直接设置 window.location 或者新建一个 iframe 去加载这个 URL),原生中拦截 UIWebView 的代理方法 - webView:shouldStartLoadWithRequest:navigationType:,然后根据约定好的协议做相应的处理

崩溃处理

崩溃日志获取

  1. 上传appStore的app,可以通过appstoreconnect中获取

  2. 利用Xcode organizer crash获取。

  3. Crashlytics,Hockeyapp ,友盟,Bugly 等等。

  4. 通过iOSSDK中提供了一个现成的函数 NSSetUncaughtExceptionHandler 用来做异常处理

崩溃日志解析

  1. 用Xcode自带的 symbolicatecrash 工具配合对应的.dSYM来解析的.crash文件
  2. 终端命令dwarfdump命令,配合.dSYM解析
  3. 无符号解析:(58-WBBlades)。通过读取崩溃地址(symbol)配合Mahc-o+ASLR解析

组件化

  • 主题思想:构建中间件
  • 组件划分:依据
  1. 基础模块:底层组件、宏定义...
  2. 通用模块:用户中心、登录、通用UI组件、、、
  3. 业务模块:首页、发现、关注、、、
  • 组件划分注意点
  1. 依赖下沉:如果同级模块中出现相互依赖部分,那么要这部分抽出来,下沉到下一个模块
  2. 依赖不可倒置:要上层依赖下层进行开发,下层不能依赖上层

MGJRouter

简介

  • 原理
  1. 注册URL生成路由表
  2. openUrl实现跳转
  • 细节
  1. 以路由的形式去做全局的注册
  2. 参数可以放到路由的URL中,或者放到routerParameters字典中。
  3. 反向传值(回调),参数中有block,涉及到硬编码
[MGJRouter openURL:@"LWT://Test3/PushMainVC"withUserInfo:@{
    @"navigationVC" : self.navigationController,
    @"block":^(NSString * text){NSLog(@"%@",text); },
}completion:nil];

4. 可以返回对象

[MGJRouter registerURLPattern:@"mgj://loadFourthViewController" toObjectHandler:^id(NSDictionary *routerParameters) {
    NSString *labelText = routerParameters[MGJRouterParameterUserInfo][@"text"];
    FourthViewController *vc = [[FourthViewController alloc] init];
    vc.labelText = labelText;
    //返回block中的对象
    return vc;
}];

- (IBAction)fourClick:(UIButton *)sender {
    NSString *textStr = @"abcdefg";
    [self.navigationController pushViewController:[MGJRouter objectForURL:@"mgj://loadFourthViewController" withUserInfo:@{@"text" : textStr}] animated:YES];
}

弊端

  1. url维护成本高(url字符串属于硬编码,易写错);
  2. 一开始就被写入到了内存并常驻,如果组件多,就会造成内存占用过多

CTMediator

主题思想:构建中间件+ target-action

截屏2022-01-10 下午9.31.19.png

简介

-target-action原理

  1. 每个组件, 提供一个统一披露的接口文件 eg:新建基于CTMediator的Category:CTMediator+A
CTMediator+A.h
- (UIViewController *)A_aViewControllerWithBlcok:(LikeBlock)block;

CTMediator+A.m
- (UIViewController *)A_aViewControllerWithBlcok:(LikeBlock)block {
    /*
        AViewController *viewController = [[AViewController alloc] init];
        viewController.block  = block;
     */
    //内部通过`NSClassFromString`动态获取类,并创建实例
    //通过`NSSelectorFromString`动态获取SEL
    //通过`performSelector+NSInvocation`动态调用方法
    return [self performTarget:@"A" action:@"viewController" params:nil shouldCacheTarget:NO];
}

调用:
UIViewController *viewController = [[CTMediator sharedInstance] A_aViewControllerWithBlcok:^(BOOL islike) {
   //回调使用block
}];
[self.navigationController pushViewController:viewController animated:YES];

2. 额外的维护一个中间件的分类扩展(在此处进行硬解码 通过运行时进行物理解耦) 3. 其他地方通过target-action;的方案进行交互 4. [self performTarget:@"A" action:@"viewController" params:nil shouldCacheTarget:NO];属于硬编码。 5. 远程组件化通讯:其他app调用,主要是通过openURL的方式,需要在AppDelegate的openUrl代理方法中进行处理.(看上面的架构图)

- (id _Nullable)performActionWithUrl:completion:{
    处理url参数 - 遍历并存放到字典中
    处理targetName,目的是为了防止黑客通过远程方式调用本地模块
    [self performTarget:action:params:shouldCacheTarget:];
    回调处理
}

viewDidLoad 如何提前触发

ViewController *vc = [ViewController new];
[vc loadViewIfNeeded];

iOS autolayout 底层数据格式

是 XML。多元一次方程。

布局使用原生还是xib/sb

xib/sb

优点:

  1. 界面直观,代码量少,不用理解上下文
  2. 提高开发效率

缺点:

  1. 多人编辑同一个xib/sb时,很难维护。
  2. xib/sb打开慢,性能差
  3. 控件复用程度不高。(你有见过主流的第三方 UI 框架中使用 IB 的吗?)

NSCache

概念

  1. 使用key-value方式,类似NSDictionary的缓存集合容器。
  2. 缓存临时存储短时间使用但创建昂贵的对象。
  • 特点
  1. NSCache拥有多种自动删除的策略,一旦内存不足,会根据这些策略会自动删除一些对象,以减少内存占用。
  2. NSCache是线程安全的。不同的线程在增删改查时,不需要锁定缓存区域
  3. 不同于NSMutableDictionary对象,NSCache不会拷贝key对象
#### NSMutableDictionary 对key实现了NSCopying协议
- (void)setObject:(ObjectType)anObject forKey:(KeyType <NSCopying>)aKey;
#### NSCache 对key没有实现NSCopying协议
- (void)setObject:(ObjectType)obj forKey:(KeyType)key;

SDWebImage图片下载库的缓存机制就是通过NSCache来实现的

缓存限制属性

NSCache提供了几个属性来限制缓存的大小

//countLimit限定了缓存最多维护的对象的个数
@property NSUInteger countLimit;
//totalCostLimit属性来限定缓存能维持的最大内存
@property NSUInteger totalCostLimit

存取方法

NSCache提供了一组方法来存取key-value对,类似于NSMutableDictionary类

- (id)objectForKey:(id)key
- (void)setObject:(id)obj forKey:(id)key
- (void)removeObjectForKey:(id)key
- (void)removeAllObjects

NSCacheDelegate

@protocol NSCacheDelegate <NSObject>
@optional
- (void)cache:(NSCache *)cache willEvictObject:(id)obj;
@end

NSCache对象还有一个代理属性

@property(assign) id< NSCacheDelegate > delegate

实现NSCacheDelegate代理的对象会在对象即将从缓存中移除时执行一些特定的操作,因此代理对象可以实现以下方法

动态库&静态库

库的概念

库是写好的、现有的、可以复用的代码。是一可以被操作系统写入内存可执行代码的二进制文件。库有两种:静态库动态库

静态库和动态库

静态库

链接时会被完整的copy到可执行文件中,被使用多次,就有多份copy。代表的有.a文件还有静态的.framework文件。eg:某静态库A,在微信和抖音中都有使用,那么其两个可执行文件在链接时都要加载这份静态库。

动态库

  • 系统动态库 链接时不复制,程序运行时由系统动态加载到内存,系统只加载一次,多个程序共用,节省内存。eg:UIKit,微信和抖音都有使用该库,但是内存系统加载了一份,像UIKit、UIFoundation等也被称为共享缓存。
  • 自己打包的动态库 自己打包的动态库则是在应用程序里的,但是与静态库不同,它不在可执行的文件中

库的制作

静态库 .a/.framework

  • .a newfile -> Staic Library

  • .frmaework newfile -> Framework. 这里要设置一下,因为默认是动态库,要改为静态库。Build Settings => Mach-O Type 改为 Static Library:

动态库(自己制作的)

系统默认设置的就是动态类型,然后就是将新添加的头文件公开,整个流程和 .framework 静态库一样

FMDB是否线程安全,如何处理

在多线程中,FMDB使用GCD之FMDatabaseQueue(串行队列)+dispatch_sync(同步),来保证线程安全。但是其还是会发生多线程使用同一个数据库连接、预处理语句,即线程不安全

FMDB如何保证线程安全

有两种解决方案

  1. 在文档中强调:需要开发者手动调用close
[_queue inDatabase:^(FMDatabase * _Nonnull db) {    FMResultSet *result = [db executeQuery:@"select * from test where a = '1'"];
    // 安全
    while ([result next]) {
    }
    
    // 安全
    if ([result next]) {
    }
    // 手动调用
    [result close];
}];

2. 在 [FMDatabaseQueue inDatabase:] 函数的最后,调用 [FMDatabase closeOpenResultSets] 帮助调用者关闭所有 FMResultSet

iOS签名机制

加密算法

  1. base64:可以将任意的二进制数据进行编码,编码成为65个字符(09 az A~Z + / =)组成的文本文件。仅仅是一种编码方式,可逆
  2. Hash散列算法结果位数唯一不可逆,代表:MD5、SHA1/256
  3. 加盐:增加加密程度
static NSString * salt = @"JKHDSFHK*()&&";
//用户登录 -- 直接md5
NSString * pwd = @"123456";
pwd = [pwd stringByAppendingString:salt];//加盐
pwd = pwd.md5String;

4. 对称加密:相同的密钥可以用于信息的加密和解密,掌握密钥才能获取信息.代表:AES-CBC、DES、3DES、AES-GCM... 5. 非对称加密:有公钥和私钥才能相互解密,安全性高,但是解密速度慢;代表:RSA、DSA...

数字签名

比如某段数据A需要涉及到很高的安全性;我们可以将这段数据A先进性hash得B,再将B进行RSA加密成C,并将A和C打包发给服务器,这样安全性就会很高;而此时C就是数字签名;

iOS 双层签名机制

双向签名

  1. 首先iphone和苹果appstore分别拥有公钥A私钥A
  2. 而在app开发的时候,本地MAC会生成一对公钥M私钥M,这个公钥M就是CSR文件
  3. 在申请开发证书的时候,MAC会先将公钥M(CSR文件)发给appstore服务器,服务器用私钥ACSR文件有进行一次签名,签名之后得到的就是描述文件Provisiong profile,而这个profile文件中包含了:设备ids,appid,权限文件...还有一个证书,而这个证书包含了用私钥A加密后的公钥M公钥M的hash值
  4. 当本地电脑拿到这个描述文件Provisiong profile后会取出里面的证书进行钥匙串访问,将证书私钥M进行关联;
  5. 当app在安装的时候(或者Command + B),本地的私钥M会对app进行签名,签名的内容包括MachO文件还有刚才的从苹果服务器上申请描述文件Provisiong profile。然后将这些东西打包成一个app包,安装到iPhone手机上
  6. iPhone手机拿到app包开始安装的时候,会进行验证,如果验证成功就能安装成功
  • 6.1 用公钥A验证描述文件Provisiong中的证书,如果验证成功就证明这个行为是苹果服务器允许的,即验证证书签名
  • 6.2 用公钥A解密证书拿到公钥M,然后公钥M又去解密私钥M,。一步就是验证app签名
  • 6.3 通过上述两步验证,到达双层签名的目的,app也就能安装了。

所以双层签名:证书签名+app签名

iOS 应用重签名

即使用自己新建的app重签已有的app,已达到能够安装运行的目的

  1. 使用codesign(终端命令)
  2. shell脚本+xcode重签
  3. monkey 直接重签

代码注入(动态库注入)

代码注入其实就是HOOK,利用:Runtime - MethodSwizzle去改变原先sel和imp的对应关系

iOS应用安全

反hook

  1. 使用fishhook,去hook核心代码(method_exchangeImplementations,setimp,getImp),让其他进攻代码找不到原方法
  2. 如何自己的代码用到hook的核心代码,我们可以使用全局宏定义替换原来的方法
  3. 一般代码被攻击都是在load方法中,如果攻击发生在load放之前即很难搞了
  4. 针对于3这种情况,可以把防护代码也写在framework中,至于修改MachO中framework加载的顺序那就很难操作了。关于修改动态库加载顺序可以在Build Phases-Link Binary With Libraries中修改,前提是各个库之间没有依赖关系 参考:iOS:动态库的加载顺序

LLDB常用指令

x:内存段查看
bt:查看调用堆栈
image list:查看image加载
image lookup -t Person:快速查看某个类的信息
breakpoint set:断点设置
expression p po:断点执行
frame variable :变量参数查看
watchpoint set:内存点断watchpoint set variable self->_person->_name,可以侦测变化,类似KVO

第三方:chisel(凿子),更加的强大

pclass 打印对象的类
pviews 打印当前视图层级
pactions 打印对象动作
pmethods 打印对象的实例方法

cycript:运行时动态修改程序

搭配MonkeyApp重签微信的ipa包

切面编程 AOP+埋点

切面编程 AOP+埋点

切面编程AOP

  • Aspects:利用切面编程的思想去hook实例方法,然后返回一个可操作的block
  • 切面编程:像Aspects这种,对原生代码不会去破坏,又很容易进行统一的修改,并且业务代码被统一放在了一个block中。方法切片,保存。

埋点

  • 代码埋点:用预先写好的代码埋在事件交互的代码中发送数据。代表:友盟
  • 可视化埋点:可视化埋点并非完全抛弃了代码埋点,而是在代码埋点的上层封装的一套逻辑来代替手工埋点,大体上架构如下图:

截屏2022-01-10 下午10.22.26.png 主要还是hooks不同的事件

UIViewController PV统计 --> hook viewDidLoad

UIControl 点击统计      -->  hook **sendAction:to:forEvent:

TableView (CollectionView) 的点击统计 --> hook setDelegate方法, 在设置代理的时候再去交互 didSelect 方法来实现

gesture方式添加的的点击统计 --> hook initWithTarget:action:
  • 无侵入埋点:和可视化埋点类似,二者的区别是可视化埋点通过界面配置来决定要统计的事件来源,而无埋点是尽可能把所有能收集的数据全部收集一遍,再通过后台配置要留下哪些统计分析。代表:sensorsdata网易HubbleData无埋点SDK在iOS端的设计与实现 主要包括视图大小坐标等详细数据

定时器

NSTimer

  1. 基于runloop调用,不精确
  2. timer需要加入mode(defaultMode -> UITracingMode -> commonMode),那么就不会受到切换模式的影响
  3. 由于timer持有的target对象,而runloop又会持有timer,会有内存泄露问题(官方文档 timer对target会有一个强引用,直到timer is invalidated。也就是说,在timer调用 invalidate方法之前,timer对target一直都有一个强引用。这也是为什么控制器的dealloc 方法不会被调用的原因。)

NSTimer循环引用解决

  1. 使用NSTimer提供的API,在block中执行定时任务
    __weak typeof(self) weakSelf = self;
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
        [weakSelf fire];
    }];

2. 借助runtime给对象添加消息处理的能力 其实就是把当前的强引用转到target,如果当前的viewcontroller能够正常回收,那么dealloc方法就能够正常执行。

@property (nonatomic,strong) id target;
//创建一个target对象,
_target = [NSObject new];
对于当前这个_target来说,本质上是要作为消息的处理者,显然_target需要一个selector,所以我们动态添加一个方法到当前对象上来,
class_addMethod([_target class], @selector(fire), class_getMethodImplementation([self class], @selector(fire)), "v@:");
_timer = [NSTimer scheduledTimerWithTimeInterval:1.0f target:_target selector:@selector(fire) userInfo:nil repeats:YES];
 
-(void)dealloc{
    NSLog(@"%@ delloc",self);
    [_timer invalidate];
    _timer = nil;  
}
此时viewcontroller的析构函数就可以正常执行。

3. NSProxy虚基类消息转发。通过runtime用虚基类持有weak-target。(self强引用timer强引用proxy弱引用self)

.h
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface INWeakProxy : NSProxy
- (instancetype)initWithObjc:(id)target;
+ (instancetype)proxyWithObjc:(id)target;
@end

.m
#import "INWeakProxy.h"
@interface INWeakProxy ()
@property (nonatomic,weak) id target;
@end

@implementation INWeakProxy
- (instancetype)initWithObjc:(id)target {
    self.target = target;
    return self;
}

+ (instancetype)proxyWithObjc:(id)target {
    return [[self alloc] initWithObjc:target];
}

/**
 这个函数让重载方有机会抛出一个函数的签名,再由后面的forwardInvocation:去执行为给定消息提供参数类型信息
 */
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
    return [self.target methodSignatureForSelector:sel];
}

/**
 * NSInvocation封装了NSMethodSignature,通过invokeWithTarget方法将消息转发给其他对象。这里转发给控制器执行。
 */
- (void)forwardInvocation:(NSInvocation *)invocation {
    if ([self.target respondsToSelector:invocation.selector]) {
        [invocation invokeWithTarget:self.target];
    }
}

//使用
self.proxy = [INWeakProxy alloc];
    self.proxy.target = self;
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self.proxy selector:@selector(fire) userInfo:nil repeats:YES];
  • 上面两种方法都要去调用 invalidate, 因为invalidate方法是唯一能从runloop中移除timer的方式,调用invalidate方法后,runloop会移除对timer的强引用,至于放在 viewWillDisappearviewDidDisappeardealloc中,那就自己根据业务操作吧
- (void)dealloc { 
   [self.timer invalidate]; 
}

GCD timer

gcd的timer加在了主线程是需要runLoop调用的,但是当添加到子线程是不受runLoop影响的

__block int count = 10;
    
// 获得队列
dispatch_queue_t queue = dispatch_get_main_queue();
//dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
// 创建一个定时器
dispatch_source_t _timer_source_t = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
// 设置定时器的各种属性(几时开始任务,每隔多长时间执行一次
// GCD的时间参数,一般是纳秒(1秒 == 10的9次方纳秒)
// 何时开始执行第一个任务
// dispatch_time(DISPATCH_TIME_NOW, 1.0 * NSEC_PER_SEC) 比当前时间晚3秒
dispatch_time_t start = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC));
uint64_t interval = (uint64_t)(1.0 * NSEC_PER_SEC);
dispatch_source_set_timer(_timer_source_t, start, interval, 0);
// 设置回调
dispatch_source_set_event_handler(_timer_source_t, ^{
    NSLog(@"%d",count--);
    if (count == 0) {
        // 取消定时器
        dispatch_cancel(_timer_source_t);
    }
});
// 启动定时器
dispatch_resume(_timer_source_t);

CADisplayLink

CADisplayLink是一个能让我们以和屏幕刷新率相同的频率将内容画到屏幕上的定时器

  • 应用:YYFPSLabel

细数iOS中的锁

VC生命周期

loadView -> viewDidLoad -> viewWillAppear -> viewWillLayoutSubViews(当前有视图中其他控件时,会加载多次) -> viewDidAppear -> viewWillDisappear -> (viewWillLayoutSubviews) -> viewDidDisappear -> dealloc

tip:([self.View addSubview XX]``X等方法都会触发viewWillLayoutSubviews 方法) 并且在视图将要消失的时候 也会触发这个方法

MVC MVVM MVP

  • MVCModel-View—Controller
  1. C过重
  2. 过重导致不易测试
  3. 网络逻辑不知道放在哪里
  1. 将C中部分代码(业务逻辑,网络逻辑,数据缓存)挪到VM中,减轻C。
  2. 但是V和VM的交互还放在C中。
  3. V 持有 VM;
  4. 使用双向绑定(RAC)效果会更好
  • MVPModel-View—Presenter
  1. 将C中部分代码(业务逻辑,网络逻辑,数据缓存)挪到P中,减轻C。
  2. P封装了M
  3. V通过遵守P的协议delegate进行交互。

数组和链表

  • 不同点:
  1. 链表是链式存储结构;数组是顺序存储结构。
  2. 链表增删比组数简单,不需要移动元素、长度易扩充,但是较为困难;数组简单,但增删较为麻烦。
  • 相同点:两种结构均可实现数据的顺序存储,构造出来的模型呈线性结构

多线程

进程&线程

  1. 进程:程序的一次执行
  2. 线程:CPU的基本调度单位

形象点的解释

  • cpu:工厂,工厂可以有好多车间
  • 进程:车间,车间里有好多个工人
  • 线程:工人,车间里的
  • 锁:厕所就像共享内存,可以公用,但不能同时用,工人(线程)要上厕(内存访问)所就要锁门
  • 信号量:相当于车间里的休息室,但是同时只能容纳5个人休息,只有里面的工人休息完出来了,才能进下一个,这个5就相当于信号量;用来保证多个线程不会互相冲突

多线程-NSThread

多线程-GCD

多线程-NSOperation

dispatch_once单利原理

+ (instancetype)sharedInstance {
    static dispatch_once_t onceToken;
    static id instance = nil;
    dispatch_once(&onceToken, ^{
        instance = [[NSObject alloc] init];
    });
    return instance;
}
  1. 首先通过onceToken作为一个唯一标识用来判断对象是否已经初始化(实际就是block有没有执行完成)
  2. 然后在第一次调用block(实际上就是初始化对象的时候)加锁,防止其他线程进入;
  3. 如果此时其他线程进来时发现锁被占用就会进入等待状态;
  4. 当第一次调用block初始化完成之后,就会把onceToken标记的值更新为已初始化,然后释放锁,同时会发出通知,让其他等待的线程返回;
  5. 当二次或多次调用dispatch_once的时候,判断对象已经被初始化,就直接返回,不会再调用block。

NSNotification

原理

NSNotificationCenter 定义了一个结构体NCTable,其内部有两张hash表一张用来存有name的通知,另一张用来存有object的通知;既没有name,又没有object的会存在wildcard这个单向链表中。

typedef struct NCTble {
 Observation   *wildcard;  /* 保存既没有没有传入通知名字也没有传入object的通知*/
 GSIMapTable   nameless;   /*保存没有传入通知名字的通知 */
 GSIMapTable   named; /*保存传入了通知名字的通知 */
 GSIMapTable   cache[CACHESIZE];/*方便快速查找的cache*/
 ...
} NCTable;

//封装 observer、selector等数据的结构体对象
typedef struct  Obs {
  id        observer;   /* 保存接受消息的对象*/
  SEL       selector;   /* 保存注册通知时传入的SEL*/
  struct Obs    *next;      /* 保存注册了同一个通知的下一个观察者*/
  struct NCTbl  *link;  /* 保存该Observation的Table*/
} Observation;
  • 存通知
- (void) addObserver: (id)observer
            selector: (SEL)selector
                name: (NSString*)name
              object: (id)object
  1. selectorobserver封住成一个Observation结构体对象 Observation *o = obsNew(TABLE, selector, observer);

  2. 判断有没有name参数?有,就以namekey就放到named_map中,named_map内部又类似链表,add就完事了

  3. 没有name参数,但有object就以objectkey放到nameless_map中,nameless_map内部又类似链表,add就完事了

  4. 都没有就放到wildcard链表中

  • 发通知
postNotificationName:(NSNotificationName)aName object:(nullable id)anObject
  1. name或者objectkey找到对应的Observer链表,然后遍历链表,将找到的Observation对象放到一个ObserversArray数组中
  2. 使用performSelector遍历调用[observerNode->observer performSelector: observerNode->selector withObject: notification];

通知回调在主线程

// 代码
@interface A : NSObject <NSMachPortDelegate>
- (void)test;
@end
@implementation A
- (void)dealloc {
    [[NSNotificationCenter defaultCenter] removeObserver:self];
}
- (void)test {
    [[NSNotificationCenter defaultCenter] addObserverForName:@"111" object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification * _Nonnull note) {
        NSLog(@"current thread %@ 刷新UI", [NSThread currentThread]);
        // 刷新UI ...
    }];
}
@end
 
// 输出
current thread <NSThread: 0x7bf29110>{number = 3, name = (null)}
2017-02-27 11:53:46.531 notification[29510:12833116] current thread <NSThread: 0x7be1d6f0>{number = 1, name = main} 刷新UI

单元测试

优点

  1. 通过单元测试的代码,质量更高
  2. 问题易暴露、易定位,并且能测试代码性能
  3. 减少调试时间

使用简介

  1. 测试用例一定要test开头
  2. 系统方法
//每个test方法执行之前调用,在此方法中可以定义一些全局属性,类似controller中的viewdidload方法
- (void)setUp {
    [super setUp];
    //测试前期准备
    self.vc = [[UIViewController alloc] init];
}
//每个test方法执行之后调用,释放测试用例的资源代码,这个方法会每个测试用例执行后调用
- (void)tearDown {
    [super tearDown];
    //测试后,销毁对象
    self.vc = nil;
}

//单元测试,一定要以test开头
- (void)testVC {
    //测试view是否加载出来
    XCTAssertNotNil(self.vc.view,@"view未成功加载出来");
}

3. 性能测试

- (void)testPerformanceExample {
    //这个方法主要是做性能测试的,所谓性能测试,主要就是评估一段代码的运行时间。该方法就是性能测试方法的样例。
    // This is an example of a performance test case.
    [self measureBlock:^{
        // Put the code you want to measure the time of here.
    }];
}

4. 以断言用于判断测试是否通过,而不用NSLog 5. 三方单元测试库:OCMock:它可以伪造一个有预定义行为替身,通过这个替身和结果进行对比,从而进行测试

//没有参数的方法
- (void)testGetName{
PersonModel *person = [[PersonModel alloc] init];

//创建一个mock对象
id mockClass = OCMClassMock([PersonModel class]);
//可以给这个mock对象的方法设置预设的参数和返回值
OCMStub([mockClass getPersonName]).andReturn(@"liyong");

//用这个预设的值和实际的值进行比较是否相等
XCTAssertEqualObjects([mockClass getPersonName], [person getPersonName], @"值相等");
}

RxSwift

blog.csdn.net/kyl28288954…

如何捕获崩溃

使用系统接口去捕获崩溃信息FOUNDATION_EXPORT void NSSetUncaughtExceptionHandler(NSUncaughtExceptionHandler * _Nullable);

  1. 用上面的接口在崩溃之前将崩溃信息(函数堆栈信息)写到沙盒
  2. 在下一次启动的时候,从沙盒中取出上传

沙盒结构

  • Application:存放程序源文件,上架前经过数字签名,上架后不可修改。
  • Documents:常用目录,iCloud备份目录,存放数据。(这里不能存缓存文件,否则上架不被通过)
  • Library:
  1. Caches:存放体积大又不需要备份的数据。(常用的缓存路径)
  2. Preference:设置目录,iCloud会备份设置信息。
  • Temp:存放临时文件,不会被备份,而且这个文件下的数据有可能随时被清除的可能。

谓词NSPredicate

谓词就是通过NSPredicate给定的逻辑条件作为约束条件,完成对数据的筛选。和swift中的高阶函数filter差不多

//定义谓词对象,谓词对象中包含了过滤条件(过滤条件比较多)
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"age<%d",30];

//使用谓词条件过滤数组中的元素,过滤之后返回查询的结果
NSArray *array = [persons filteredArrayUsingPredicate:predicate];

OC中的反射机制

  • class反射
通过类名的字符串形式实例化对象。
Class class = NSClassFromString(@"student");
Student *stu = [[class alloc] init];

将类名变为字符串。
Class class =[Student class];
NSString *className = NSStringFromClass(class);
  • SEL的反射
通过方法的字符串形式实例化方法。
SEL selector = NSSelectorFromString(@"setName"); 
[stu performSelector:selector withObject:@"Mike"];

将方法变成字符串。
NSStringFromSelector(@selector*(setName:));

设计模式

  • MVC模式:Model View Control,把模型 视图 控制器 层进行解耦合编写。
  • MVVM模式:Model View ViewModel 把模型 视图 业务逻辑 层进行解耦和编写。
  • 单例模式:通过static关键词,声明全局变量。在整个进程运行期间只会被赋值一次。
  • 观察者模式式:KVO是典型的通知模式,观察某个属性的状态,状态发生变化时通知观察者。
  • 委托模式:代理+协议的组合。实现1对1的反向传值操作。
  • 工厂模式:通过一个类方法,批量的根据已有模板生产对象。
  1. 使用一个接口可以创建出多种对象
+(instancetype)buttonWithType:(UIButtonType)buttonType;
[NSNumber numberWithBool:YES]
[NSNumber numberWithInt:1]
  • 适配器模式: 定义:将不兼容的转换为兼容,就像电源适配器一样,处理全世界不同的电压。
  1. 将一个原始接口转成客户端需要的接口.
  2. 原始接口不兼容现在新的接口,将它们两个接口一起工作需要适配器解决 eg:通过列表适配器将UITableVIewDataSource/UITableViewDelegate相关的代码独立出来,来减轻ViewController

以将UITableVIewDataSource这个代理方法为例, - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath 不同的Cell展示样式是不同的,使用代理把方法暴露出来,用户自己处理,只需要返回UITableViewCell实例即可,而不需要每个tableView展示cell的时候,都写一遍

  • 中介者模式:一般用于解耦 例如组件化中的Mediator。
  • 分类模式:也叫装饰或者扩展模式。Category能在不影响原类的情况,扩展其功能
  • 响应链模式:通过响应链传递数据:[obj nextResponds];
  • 享元模式:tableViewCell 的重用机制就是实现了享元模式。通过共享,减少对象的创建,节省内存。
  • 命令模式:runtime中的消息转发的最后一步会把消息签名封装成一个NSInvocation对象。这种将行为封装成对象,可以很容易的对其进行队列、记录、撤销等管理。

#import跟 #include 有什么区别,@class呢,#import<> 跟 #import””有什么区别?

  1. #import是Objective-C导入头文件的关键字,#include是C/C++导入头文件的关键字,使用#import头文件会自动只导入一次,不会重复导入。
  2. @class告诉编译器某个类的声明,当执行时,才去查看类的实现文件,可以解决头文件的相互包含。
  3. #import<>用来包含系统的头文件,#import””用来包含用户头文件

可变数组实现原理

  1. 首先会在内存中开辟一块堆空间,用于储存数组的长度,容量以及数据的首元指针
  2. 至于具体的数据的用线性链表顺序储存
  3. 当容量不够时,会重新开辟空间,然后将数据复制到新的空间里去,最后释放旧的空间

深拷贝 & 浅拷贝

  • 浅拷贝:指针拷贝
  • 深拷贝:内容拷贝

strong copy mutableCopy

  • 后缀 修饰:eg: [xxx copy] [xxx mutableCopy];
  1. 如果xxx是不可变对象:copy是浅拷贝,mutableCopy是深拷贝。strong是指针引用(也是浅拷贝)。
  2. 如果xxx是可变对象:copy、mutableCopy都是深拷贝。strong是指针引用(也是浅拷贝)。
  • 属性修饰:eg:
@property (nonatomic,strong) NSString *xxx;
@property (nonatomic,copy) NSString *xxx;

其set方法内部分别是

//strong 
- (void)setXxx:(NSString *)xxx{
    _xxx = xxx;
}

//copy
- (void)setXxx:(NSString *)xxx{
    _xxx = [xxx copy];
}
  • 总结:
  1. 不可变对象:copy是浅拷贝,mutableCopy是深拷贝,strong是浅拷贝。
  2. 可变对象:copy是深拷贝,mutableCopy是深拷贝,strong是浅拷贝。
  3. 属性加copy修饰会在setter方法内部加上 _xxx = [xxx copy];;
  4. 上述结论同样可以扩展到其他类型NSString/NSMutableString、NSArray/NSMutableArray、NSDictionary/NSMutableDictionary

为什么string用copy而不是strong

strong会有一个问题,如果我用一个mutablestring赋值给这个strong属性,然后我修改这个mutablestring,你这个strong属性就会被修改,就不安全了,是不期望的结果。copy就没有这个问题。

block为什么使用Copy

  • Block内部没有调用外部变量时存放在全局区(ARC和MRC下均是)
  • Block使用了外部变量,这种情况也正式我们平时所常用的方式,Block的内存地址显示在栈区,栈区的特点就是创建的对象随时可能被销毁,一旦被销毁后续再次调用空对象就可能会造成程序崩溃,在对block进行copy后,block存放在堆区.所以在使用Block属性时使用Copy修饰,而在ARC模式下,系统也会默认对Block进行copy操作

cocoapods 原理

  1. 通过名字在podspec文件中找到对应的下载路径,然后下载
  2. 下载完成后,Cocoapods会将所有的依赖库都放到另一个名为Pods的项目中,后让主项目依赖Pods项目
  3. 在编译后Pods项目最终会编译成为一个名为libPods.a的文件,主项目只要依赖这个.a文件即可

pod install与pod update

  • pod install
  1. 对于已经在Podfile.lock文件中有的库,pod install只下载Podfile.lock文件中指定的版本。
  2. Podfile.lock中没有的库,会去下载Podfile文件中最新版本的版本
  • pod update:不会考虑Podfile.lock文件中库的版本,直接去查找最新的库

  • 总结

  1. 项目需要添加或者移除新的库的时候,使用pod install
  2. 项目中的库需要更新的时候,使用pod update

自动释放池AutoreleasePool

  • 作用: 它使得应用在创建新对象时,系统能够有效地管理应用所使用的内存。
  • 创建: runloop在循环检测到事件并启动后就会创建自动释放池

AutoreleasePool 原理

  1. 自动释放池是由 AutoreleasePoolPage 以双向链表的方式实现的;
  2. 每个AutoreleasePoolPage又是一种栈结构,栈底是一个POOL_BOUNDARY(POOL_SENTINEL)(哨兵对象,代表一个 autoreleasepool 的边界)
  3. 当对象调用 autorelease 方法时,会将对象指针加入 AutoreleasePoolPage 的栈中
  4. 当系统调用 AutoreleasePoolPage::pop 方法时,会传一个POOL_SENTINEL对象或者具体要释放的对象的指针
  5. 如果传的是POOL_SENTINEL,那么就会对当前hotPage中所有的对象调用一次release操作;
  6. 如果传的是具体要释放的target对象的指针,那么就会遍历整个链表释放在target对象之前的对象,直到释放target对象才会停止。这个过程也是这些对象调用一次release操作;
  7. pushpop的时候会伴随AutoreleasePoolPageaddPagekillPage

应用场景

  1. 在某个子线程需要大量创建对象时,可以用@autorelease{}包裹对象的执行操作

编译

高级语言(OC) -> 汇编(arm汇编) -> 二进制(Mach-o: 可执行文件)

链接器(DYLD)会将项目中的多个 Mach-O 文件合并成一个。

编译过程

  • 预处理: CLang会预处理代码。比如把对应的宏设置到对应位置、删除注释、条件编译处理、include、import等等
//宏
#define K_ScreenFrame [[UIScreen mainScreen] bounds]

//注释

//条件编译
#ifdef DEBUG
#define NSLog(s,...) NSLog( @"[%@ in line %d] ===========>\n%@", [[NSString stringWithUTF8String:__FILE__] lastPathComponent], __LINE__, [NSString stringWithFormat:(s), ##__VA_ARGS__] )
#else
#define NSLog(...)
#endif
  • 词法分析: 读取每个词,转化为一个个token ,并进行判断和分类.
  • 语法分析: 使用上面的token,生成语法树(AST,Abstract Syntax Tree),通过语法树,我们能知道这个代码是做什么的。
  • 中间代码的生成和优化: 此阶段LLVM 会对代码进行编译优化,例如针对全局变量优化、循环优化、尾递归优化等,最后输出汇编代码xx.ll文件。
  • 生成汇编代码: 汇编器LLVM会将汇编码转为机器码。此时的代码就是.o文件,即二进制文件。
  • 链接:连接器把编译产生的.o文件和(dylib,a,tbd)文件,生成一个mach-o文件。mach-o文件级可执行文件。编译过程全部结束,生成了可执行文件Mach-O image.png

参考iOS编译过程

dyld

在iOS中,dyld(Dynamic Link Editor)是负责动态链接和加载可执行文件及其依赖的动态库的系统组件。它是一个动态链接器,类似于macOS上的dyld,也是从macOS移植到iOS上的关键部分。以下是一些有关dyld的关键点:

  1. 加载可执行文件:当用户启动一个应用程序时,dyld负责加载应用程序的可执行文件到内存中,并解析其依赖的所有动态库。这些动态库可以是系统提供的库,也可以是应用程序自带的库。
  2. 符号解析:dyld在加载过程中会解析符号,这包括全局变量和函数。它会确保每个符号都能被正确解析和链接,这样在运行时,程序可以正确地调用这些符号。
  3. 重定位:为了让代码能够在内存中任何地方运行,dyld会进行重定位。重定位是指将代码中用到的符号地址更新为实际内存中的地址。
  4. 依赖管理:dyld会处理动态库之间的依赖关系,确保所有依赖的库都被正确加载。它会根据库的依赖树递归地加载所有需要的库。
  5. 环境变量:dyld可以通过一些环境变量进行配置,例如DYLD_LIBRARY_PATHDYLD_FRAMEWORK_PATH等,这些环境变量可以影响库的搜索路径和加载行为。
  6. 安全性:为了确保系统和应用程序的安全,dyld有许多安全机制。例如,它会验证动态库的签名以防止加载未经授权的代码,还会进行地址空间布局随机化(ASLR)以增加攻击难度。
  7. 性能优化:dyld使用多种技术来优化应用程序的启动时间。例如,它会缓存一些已解析的符号信息,以减少重复解析的开销,还会利用共享缓存(shared cache)来加快常用系统库的加载速度。

APP启动过程

iOS应用程序的启动过程可以分为几个关键阶段,每个阶段都有特定的任务和目标。以下是一个简化的描述:

  1. 加载可执行文件

    • 当用户点击应用程序图标时,系统会创建一个进程,并启动应用程序的可执行文件。
    • dyld(动态链接器)会加载可执行文件和应用程序所依赖的动态库。
  2. dyld 处理

    • dyld负责解析可执行文件和动态库中的符号。
    • 进行重定位,将符号地址更新为实际内存地址。
    • 处理依赖关系,确保所有依赖的动态库都被正确加载。
  3. 执行应用程序入口

    • 加载过程完成后,dyld将控制权交给应用程序的main函数。
    • main函数是应用程序的入口点,它是由Xcode生成的标准main.m文件中的UIApplicationMain函数调用。
  4. UIApplicationMain 函数

    • UIApplicationMain函数创建应用程序对象(UIApplication或其子类)和应用程序委托(App Delegate)。
    • 设置应用程序的主运行循环(Run Loop),开始处理事件。
  5. App Delegate

    • App Delegate是应用程序的代理,它处理应用程序的生命周期事件,如启动、进入前台、进入后台等。
    • application:didFinishLaunchingWithOptions:是App Delegate中的一个关键方法,当应用程序启动完成并准备好运行时,会调用此方法。
  6. 设置用户界面

    • application:didFinishLaunchingWithOptions:方法中,开发者通常会配置初始的用户界面,例如创建和设置窗口(UIWindow)和根视图控制器(Root View Controller)。
  7. 显示窗口

    • 一旦设置了初始界面,应用程序会将窗口显示在屏幕上,并开始响应用户输入和其他系统事件。

整个过程通常在短短几百毫秒内完成,以确保用户在点击应用程序图标后能够迅速进入应用程序。这个启动过程的每个阶段都至关重要,任何一个阶段出现问题都可能导致应用程序无法正常启动。

APP 启动加载过程

  • dyld 源码 1 由_dyld_start()函数开始,先初始化mach-o文件初始化准备操作(macho_header信息读取、slide随机地址生成...)
  1. 接着调用dyld::_main()函数,先是对一些环境变量打印判断(),Xcode会在控制台打印相关的详细信息:DYLD_PRINT_OPTS/DYLD_PRINT_ENV
if ( sProcessIsRestricted )
        pruneEnvironmentVariables(envp, &apple);
    else
        checkEnvironmentVariables(envp, ignoreEnvironmentVariables);
    if ( sEnv.DYLD_PRINT_OPTS ) 
        printOptions(argv);
    if ( sEnv.DYLD_PRINT_ENV ) 
        printEnvironmentVariables(envp);
    getHostInfo();  

3. 接着实例化主程序,即对macho文件进行实例化 内部是对程序需要的依赖库创建一个个对应的image对象,并对这些image进行链接、初始化(包括runtime初始化)。可以在打断点在lldb上使用image list命令查看加载了哪些image

  1. 加载共享缓存(UIKit、Foundation)

  2. 插入库:即将app本身的库加载(比如:逆向自己生成的framework动态注入就是在这个时候)

  3. 链接主程序

  4. 初始化函数initializeMainExectable();内部是程序的一些初始化逻辑

  5. 经过过一系列的初始化调用notifySingle()函数:该函数还有一个回调:用于在_objc_init初始化-->_dyld_objc_notify_register(&map_images, load_images, unmap_image);

  6. _objc_init初始化 内部调用_dyld_objc_notify_register(&map_images, load_images, unmap_image);由于上面8notifySingle()函数回调,这里会调用load_images这个方法。内部就是load方法的加载,先主类,后分类;

  7. doModinitFunctions()-->这里面会调用所有库内的全局C++对象的构造函数,以及所有带有_attribute_(constructor)标志的C函数

11.返回主程序的函数入口,调用主程序的main函数

程序加载其他点

-map_images_read_imagesstatic Class realizeClassWithoutSwift(Class cls)

  1. 这一步会读取class的data(),将ro写入到rw,即将method_list_t、property_list_t、protocol_list_t写进去;
  2. realizeClassWithoutSwift(Class cls)这步是递归调用,父类和元类
  • methodizeClass 之 attachLists() 以二维数组的方式倒序插入,即后加的在前面

  • 懒加载类和非懒加载类

  1. 实现了load方法就是非懒加载类
  2. 未实现了load方法就是懒加载类

Mach-O

是iOS上可执行文件的格式,常见Mach-O格式的文件:.a/.dyib/.framework/可执行文件/dyld(动态链接器)/.dsym(符号文件) 等

Mach-O 内部结构

若是把MachO整个文件看成是一本书的话。Header相当于书的序;Load commands相当于书的目录;Data相当于书的具体内容。当某个执行文件是由多个MachO文件组成时,内部是一个个MachO叠在一起的。

  • Data段里面会有两张表-懒加载表和非懒加载表
  1. 懒加载表:存放系统函数指针
  2. 非懒加载表:自己写的函数指针
  • 在程序启动的时候 Mach-O 文件会被 DYLD (动态加载器)加载进内存。加载完 Mach-O 后,DYLD接着会去加载 Mach-O 所依赖的动态库。

共享缓存机制

苹果为了节约内存以及提高加载速度,将系统的动态库(UIKit,UIFoundation...)放在内存的一块特殊位置,然后将这块内存共享给其他的应用。这块区域就是动态库共享缓存(dyld shared cache

PIC技术:位置代码独立

编译的时候,符号地址。dyld加载的时候,将符号地址和真实地址绑定。

  1. 由于共享缓存的原因,系统函数地址无法在编译时期确定
  2. 所以APPLE使用PIC技术在MachO文件DATA段建立两张表,懒加载表(包含系统的函数)和非懒加载表(自己工程里面写的函数)
  3. 首次调用懒加载表内的函数时,DYLD会对该函数进行绑定:符号表绑定
  4. 而非懒加载表内部是自己代码里写的函数指针,编译时期就确定了

共享缓存机制和PIC技术的原因,fishhook可以hook系统自带的函数,因为系统的的函数是存放在懒加载表中的,在dyld去进行绑定的时候,fishhook可以修改符号表的绑定从而hook函数。而工程自己写的函数是在非懒加载的表中的,在编译时期就确定了,所以无法被hook

ASLR技术(地址空间布局随机化):

MachO文件加载的时候会对首地址加上一个随机的偏移值,防止被逆向。

rebase & bind

  • stubs 桩:Mach-o中的data段为每个动态库的符号设置的对应的stub; 代码中调用函数(比如NSLog这个函数), 被编译之后, 汇编指令就是调用这个函数对应的stub
  • rebase: 由于ASLR技术(地址空间布局随机化,防止被逆向)的原因,函数的会先放在__Data -> __stub_helper,等待进程被加载
  • bind: 当进程被加载后函数从__stub_helper中转移到了__Data -> __la_symbol_prt

相关问题

  • 1.Mach-O是如何找到系统的函数地址?或者说Mach-O文件是如何链接外部函数的呢?
  1. Moch-O文件会先在__DATA段建立一个空指针(符号地址)
  2. 在DYLD的时候,会将上面的空指针指向具体某个系统函数(外部函数):即将符号地址和真实地址绑定
  • 2.fishook原理 将上面懒加载表里面指向系统函数的指针重新绑定(rebind_symbols符号表重绑定);而自己写方法函数指针在编译时期就已经写在了非懒加载表内所以无法hook

weex

weex调用native

1、在原生的工程里创建一个类继承NSObject 2、遵循WXModuleProtocol协议 3、WX_EXPORT_METHOD 暴露方法给JS调用 4、WXSDKEngine注册组件(在AppDelegate里注册)

[WXSDKEngine registerModule:@"weexToNative" withClass:[weexToNative class]]

5、weex中调用

weex.requireModule("weexToNative").callNative({
        native: "i am weex"
});

native调用weex

WXSDKInstance是实例级对象非应用级对象,所有只能是接收的实例对象才能调用。

//oc代码
[self.instance fireGlobalEvent:@"callJS" params:@{@“1":@"2"}]

//weex代码
var globalEvent = weex.requireModule('globalEvent')
    globalEvent.addEventListener("callWeex", function(e) {
        console.log('JS回调了 参数:' + JSON.stringify(e));
    });

编写原生组件

1.在原生工程中创建一个继承自 WXComponent 的类,以原生的方式在此类中实现组件

-(instancetype)initWithRef:(NSString *)ref type:(NSString *)type styles:(NSDictionary *)styles attributes:(NSDictionary *)attributes events:(NSArray *)events weexInstance:(WXSDKInstance *)weexInstance
{
    if (self = [super initWithRef:ref type:type styles:styles attributes:attributes events:events weexInstance:weexInstance]) {
        //设置属性
        _titleName = [WXConvert NSString:attributes[@"titleName"]];
    }
    return self;
}
- (void)viewDidLoad {
    [super viewDidLoad];
    UIButton *button = [[UIButton alloc]initWithFrame:CGRectMake(0, 0, 375, 100)];
    [button setTitle:_titleName forState:UIControlStateNormal];
    button.backgroundColor = [UIColor blueColor];
    [self.view addSubview:button];
    
}

2.注册原生组件(在AppDelegate中)

[WXSDKEngine registerComponent:@"nativeComponent" withClass:[nativeComponent class]];

3.在weex中调用此组件

 <nativeComponent style="height:200px;width:750px;" titleName = "原生组件"></nativeComponent>

引用计数

1、先判断当前对象是否是Tagged Pointer对象

  • 1:Tagged Pointer专门用来存储小的对象,例如NSDate、NSNumber、NSString
  • 2:Tagged Pointer指针的值不再是地址了,而是真正的值。所以,实际上它不再是一个对象了,它只是一个披着对象皮的普通变量而已。所以,它的内存并不存储在堆中,也不需要mallocfree
  • 3:在内存读取上有着3倍的效率,创建时比以前快106倍。

2、如果对象不是Tagged Pointer类型,那么引用计数放在isa_t isa联合体中,

isa_t isa = {
    Class class = Person;
    uintptr_t bits = 8303516107940673;
    struct {
        uintptr_t nonpointer = 1;
        uintptr_t has_assoc  = 0;
        uintptr_t has_cxx_dtor = 0;
        uintptr_t shiftcls = 536872040; 
        uintptr_t magic = 59;
        uintptr_t weakly_referenced = 0;
        uintptr_t deallocating = 0;
        uintptr_t has_sidetable_rc = 0;
        uintptr_t extra_rc = 0;
    }
}
extra_rc就是存的引用计数,nonpointer = 1表示启用Non-pointer。

当对象使用retain()、release()等函数时进行引用计数操作时,extra_rc就会变化, 但是extra_rc是有存储限制,如果超过255将会附加SideTable辅助存储。

3、SideTable辅助存储

SideTables、SideTable、RefcountMap
比喻
SideTables == 宿舍楼
SideTable  == 宿舍
RefcountMap里存放着具体的床位

3.1、SideTables

全局Hash表,用于管理对象的引用计数和weak指针。内部就是一个个SideTable结构体,使用对象的内存地址作为SideTableKey。并且采用分离锁技术;

3.2、SideTable

1.自旋锁(spinlock_t  slock):用于控制这个SideTable中的读写;自旋锁的效率高,并且可以在任何上下文中使用,所以选择自旋锁而不用其他锁(互斥、信号量)

2.RefCountMap refcnts:里面就是保存一个个具体的内存管理对象

3.weak_table_t weak_table:维护weak指针的结构体

4、RefcountMap refcnts 又有通过对象指针为key找到在RefcountMap中找到自己的引用计数值,使用bit mask技术进行引用计数操作:

  • (1UL<<0) WEAKLY_REFERENCED

表示是否有弱引用指向这个对象,如果有的话(值为1)在对象释放的时候需要把所有指向它的弱引用都变成nil(相当于其他语言的NULL),避免野指针错误。

  • (1UL<<1) DEALLOCATING

表示对象是否正在被释放。1正在释放,0没有。

  • REAL COUNT

REAL COUNT的部分才是对象真正的引用计数存储区。所以咱们说的引用计数加一或者减一,实际上是对整个unsigned long加四或者减四,因为真正的计数是从2^2位开始的。 iOS 内存管理

Block

//简单Block示例
- (void)testBlock {
    int age = 10;
    void(^blcok)(int,int) = ^(int a,int b) {
        NSLog(@"this is block,a = %d,b = %d",a,b);
        NSLog(@"this is block,age = %d",age);
    };
    
    blcok(3,5);
}

//转成C++
- (void)testBlock {
    int age = 10;
    void(*block)(int ,int) = ((void (*)(int, int))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age));
    
    ((void (*)_block_impl *,int,int))((__block_impl *)block)->FuncPtr)((__block_impl *)block,3,5);
}

对比上面个结构两个可知 __main_block_impl_0-->block的对象指针

__main_block_func_0 -->block代码块中的代码

__main_block_desc_0_DATA --> block描述,即block对象占用内存的大小 age --> 被block捕获的临时变量

__main_block_impl_0

struct ____main_block_impl_0 {
    struct __block_impl impl;
    struct __main_block_desc_0 *Desc;
    int age;
    __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc,int _age, int flags = 0) : age(_age) {
    //同名构造函数__main_block_imp_0,构造函数中对一些变量进行了赋值最终会返回一个结构体。
        impl.isa = & _NSConcreteStackBlock;//整个block对象地址,当前在Stack区
        impl.Flags = flags;//flage有默认值,调用的时候可以省略不传
        impl.FuncPtr = fp;//__main_block_func_0 函数地址,即block内代码块地址
        Desc = desc;//储存着block对象占用内存的大小
    }
}
最后的 age(_age)则表示传入的_age参数会自动赋值给age成员,相当于age = _age。

__main_block_func_0

static void __main_block_func_0(struct ____main_block_impl_0 *cself,int a, int b) {
码
    int age = __cself->age;//bound by copy 值copy
    NSLog((NSString *)&__NSConstantStringImpl__xxxxxxxxxx_0,a,b);//xxxxxxxxxx为省略部分
    NSLog((NSString *)&__NSConstantStringImpl__xxxxxxxxxx_1,age);   
};)
__main_block_func_0 就是我们在block块中写下的代

__main_block_desc_0_DATA

static struct __main_block_desc_0 {
    size_t reserved; // = 0
    size_t Block_size; // __main_block_impl_0的占用空间大小
} __main_block_desc_0_Data = {0,sizeof(struct ____main_block_impl_0)};

block结论

1.block就是一个_main_block_impl_0结构体对象

2.block代码块中的代码被封装成__main_block_func_0函数,FuncPtr则存储着__main_block_func_0函数的地址。

3.Desc指向__main_block_desc_0结构体对象,其中存储__main_block_impl_0结构体所占用的内存

block 各个结构体之间的关系图 block 各个结构体之间的关系

block的变量捕获

总结

  • 局部变量都会被block捕获,自动变量捕获,静态变量指针捕获。全局变量则不会被block捕获
  • self同样被block捕获,不论对象方法还是类方法都会默认将self作为参数传递给方法内部,属于指针捕获。
  • block中使用成员变量或者调用实例的属性:block中捕获的仍然是self,并通过self不同的方式(eg:[self getName])去获取使用到的属性。

Block类型

1.类型

2.继承链

NSGlobalBlock|NSStackBlock|NSMallocBlock --> NSBlock --> NSObject

3.三种Block调用copy后的效果

blockl类型内存区域copy效果
NSGlobalBlock数据段(全局、静态)什么都不做,不改变类型
NSStackBlock从栈复制到堆,类型变成__NSMallocBlock__
NSMallocBlock引用计数增加,不改变类型

block内存管理

1.ARC对block的作用

  • RAC在 block作为函数返回值时会自动帮助我们对block进行copy操作,以保存block,并在适当的地方进行release操作。
  • block被强指针引用时,RAC也会自动对block进行一次copy操作。
  • block作为系统api时,会在执行完block操作之后系统才会对block进行release操作
dispatch_once(&onceToken, ^{
            
});  
[array enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
            
}]; 
...

2.持有对象

  • 堆空间的block会对person进行一次retain操作,以保证person不会被销毁。堆空间的block自己销毁之后也会对持有的对象进行release操作。
  • __weak: 加了__weak添加之后,person在作用域执行完毕之后就被销毁了。

2.1__main_block_copy_0 & __main_block_dispose_0

  • 当block中捕获对象类型的变量时,我们发现block结构体__main_block_impl_0的描述结构体__main_block_desc_0中多了两个参数copy和dispose函数

copy本质就是__main_block_copy_0函数,__main_block_copy_0函数内部调用_Block_object_assign函数,_Block_object_assign中传入的是person对象的地址,person对象,以及8。内部会根据person对象类型(weak/strong)进行引用计数操作;

dispose本质就是__main_block_dispose_0函数,__main_block_dispose_0函数内部调用_Block_object_dispose函数,_Block_object_dispose函数传入的参数是person对象,以及8。内部会根据person对象类型进行引用计数释放操作;

__block修饰对象类型

  • 通过源码查看,将对象包装在一个新的结构体__Block_byref_person_0中。
  • 结构体内部会有一个person对象,不一样的地方是结构体内部添加了内存管理的两个函数__Block_byref_id_object_copy__Block_byref_id_object_dispose
  • 还有一个指向自己的__Block_byref_person_0类型的__forwarding指针;而这个__forwarding指针在block从栈赋值到堆的时候会指向堆栈中的block结构体:
__block内存管理
  1. 没有使用__block修饰的变量(object 和 weadObj)则根据他们本身被block捕获的指针类型对他们进行强引用或弱引用
  2. 使用__block修饰的变量,__main_block_impl_0结构体内一律使用指针引用生成的结构体,而其他类型的是由传入的对象指针类型决定

block内部重新使用__strong修饰self变量是为了在block内部有一个强指针指向weakSelf避免在block调用的时候weakSelf已经被销毁

被拒总结

  • 使用第三方登录时(wechat),未做安装检测 原因: 苹果在条款中有声明不允许 iOS 应用的正常使用需要依赖另外一个 App。
  • 使用手机硬件时(摄像头,麦克风) 未在plist文件中声明访问权限。
  • 未提供测试账号(或测试账号中没有演示设备)
  • 和硬件相关的 没有提供演示视频
  • 第三方库的热更新
  • 苹果审核人员是在美国的,有些大陆网络美国是访问不了的,审核时要服务器要关闭网络

KVC

KVC是一种能够通过NSKeyValueCoding协议直接访问对象属性的机制。

getter方法- valueForKey:

1.寻找方法get<Key>, <key>, is<Key>, or _<key>

2.判断是否是NSArray。是,查看是否实现countOf<Key>或者objectIn<Key>AtIndex:。实现那就调用并返回值。

3.是否是NSSet。是,查看是否实现countOf<Key>, enumeratorOf<Key>, and memberOf<Key>:。实现那就调用并返回值。

4.非集合类型:

  • 4.1.判断accessInstanceVariablesDirectly:(是否可以直接访问成员变量:默认返回YES)方法是否返回YES
  • 4.2寻找 _<key>, _is<Key>, <key>, or is<Key>这些成员变量

5.细节处理

  • 5.1如果检索到的属性值是对象指针,则只需要返回结果
  • 5.2如果该值是NSNumber支持的标量类型,则将其存储在NSNumber实例中并返回
  • 5.3如果该值是NSNumber不支持的标量类型,则转化为NSValue对象并返回 6.报错 valueForUndefinedKey:

setter方法- setValue:forKey:

1.简单式访问:寻找是否存在set:或者 _set的方法,如果有,那就调用,并完成;如果没有进入2

2.实例变量访问:

  • 2.1 判断这个方法accessInstanceVariablesDirectly是否返回YES
  • 2.2 判断是否存在这些实例变量_<key>, _is<Key>, <key>, or is<Key>
  • 2.3 按顺序找到就直接赋值,并完成,不再给下面的成员变量赋值

3.报错:如果1和2都没有找到,那么就报错:setValue:forUndefinedKey:

如何全局关闭KVC

写一个NSObject的分类,用runtime去hook系统的setValue:forKey:valueForKey

@implementation NSObject (InvalidKVC)
+ (void)load{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [RuntimeTool best_MethodSwizzlingWithClass:self oriSEL:@selector(valueForKey) swizzledSEL:@selector(invalid_valueForKey)];
        [RuntimeTool best_MethodSwizzlingWithClass:self oriSEL:@selector(setValue) swizzledSEL:@selector(invalid_setValue)];
    });    
}

- (id)invalid_valueForKey:(NSString *)key {
    return nil;
}

- (void)invalid_setValue:(id)value forKey:(NSString *)key {
    return;
}
@end

KVO

1、动态的生成子类:NSKVONotifying_XXX

2、动态子类NSKVONotifying_Person重写了setNickName class dealloc _isKVOA方法;

  • 1.setNickName: 重写的setNickName:方法内部大概这么实现的
@implementation Person
- (void)setNickName:(NSString *)nickName {
    [self willChangeValueForKey:@"nickName"];
    _nickName = nickName;
    [self didChangeValueForKey:@"nickName"];
}
@end
  • 2.class 为何重写class方法:为了让外界感受不到子类NSKVONotifying_Person的生成
  • 3._isKVOA:判断是否是KVO生成的类

3、移除观察之后 isa指针重新指回来

4、移除观察者后动态子类会被销毁吗?不会。

分类关联对象

  • 1.系统用一个全局的AssociationsManager去管理一张全局的AssociationsHashMap表,表中key是disguised_ptr_t(由object转化而来),valueAssociationsHashMap
  • 2.AssociationsHashMapkey就是上面函数中的const void *key,而valueObjcAssociation
  • 3.ObjcAssociation 内包含着 policy,和具体的value 原理图 截屏2019-11-22上午11.23.05.png 所以关联对象并不是放在了原来的对象里面,而是系统维护了一个全局的map用来存放每一个对象及其对应关联属性表格。

weak 如何自动置为nil

  • 1、首先系统会维护一张散列表(sidetable),这张表中又包含了弱引用表(weak_table),当然还有其他表(eg:引用计数表)
  • 2、当一个属性被设置成weak时,weak_table表中会查找当内部有没有该对象的弱引用数组(weak_entry数组),如果有就直接插入这个属性到这个weak_entry数组,没有就先创建weak_entry数组再插入
  • 3、当对象被释放时(delloc),会通过对象指针去查找weak_table没有该对象的weak_entry数组,有的话遍历weak_entry数组,将内部的属性置为nil;最后将这个weak_entry数组remove

RunLoop

简介

RunLoop是一个对象-循环处理事件;闲时休眠,忙时工作;节省cpu资源,提高程序性能;

与线程的关系

  • Runloop与线程一对一
  • Runloop对象在第一次获取Runloop时创建,在线程结束的时候销毁
  • 主线程默认创建一个Runloop对象,子线程的Runloop对象需要我们主动创建和维护
  • Runloop并不保证线程安全,只能内部操作,不能跨线程操作

RunLoop相关类

  • CFRunLoopRef:代表Runloop对象
  • CFRunLoopModeRef:代表Runloop的运行模式
  • CFRunLoopSourceRef:代表Runloop的输入源/事件源
  • CFRunLoopTimerRef:代表Runloop的定时源
  • CFRunLoopObserveRef:观察者,能够监听Runloop的状态改变

5个类的相互关系

  • 一个Runloop对象( CFRunloopRef)中包含若干个运行模式(CFRunloopModeRef)。而每一个运行模式下又包含若干个输入源(CFRunloopSourceRef) 、定时源(CFRunloopTimerRef) 、观察者(CFRunloopObserveRef)
  • 每次Runloop启动只能指定其中一个运行模式(CFRunloopModeRef),这个运行模式(CFRunloopModeRef)被称为当前运行模式(CurrentMode)
  • 如果需要切换运行模式(CFRunloopModeRef),只能退出当前loop,再重新指定一个与运行模式(CFRunloopModeRef)进入
    • 这样做的目的主要是为了分隔开不同组的输入源(CFRunloopSourceRef)、定时源(CFRunloopTimerRef)、观察者(CFRunloopObserveRef),让其互不影响

CFRunLoopModeRef

系统默认定义了多种运行模式(CFRunLoopModeRef),如下:

  • 1.kCFRunLoopDefaultMode:App的默认运行模式,通常主线程是在这个运行模式下运行

  • 2.UITrackingRunLoopMode:跟踪用户交互事件(用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他Mode影响)

  • 3.UIInitializationRunLoopMode:在刚启动App时第进入的第一个 Mode,启动完成后就不再使用

  • 4.GSEventReceiveRunLoopMode:接受系统内部事件,通常用不到

  • 5.kCFRunLoopCommonModes:伪模式,不是一种真正的运行模式(后边会用到) 其中kCFRunLoopDefaultMode、UITrackingRunLoopMode、kCFRunLoopCommonModes是我们开发中需要用到的模式,具体使用方法我们在 2.3 CFRunLoopTimerRef 中结合CFRunLoopTimerRef来演示说明。

runloop对象结构

RunLoop实战应用

1、NSTimer的使用:

kCFRunLoopCommonModes

2、ImageView推迟显示

当界面中含有UITableView,而且每个UITableViewCell里边都有图片。这时候当我们滚动UITableView的时候,如果有一堆的图片需要显示,那么可能会出现卡顿的现象。 应该推迟图片的显示,也就是ImageView推迟显示图片。有两种方法:

  1. 监听UIScrollView的滚动 因为UITableView继承自UIScrollView,所以我们可以通过监听UIScrollView的滚动,实现UIScrollView相关delegate即可。
  2. 利用PerformSelector设置当前线程的RunLoop的运行模式 利用performSelector方法为UIImageView调用setImage:方法,并利用inModes将其设置为RunLoop下NSDefaultRunLoopMode运行模式。代码如下 [self.imageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"tupian"] afterDelay:4.0 inModes:NSDefaultRunLoopMode];

3、线程常驻 ANFetworking 添加一条用于常驻内存的强引用的子线程,在该线程的RunLoop下添加一个Sources,开启RunLoop。

- (void)viewDidLoad {
    [super viewDidLoad];
    // 创建线程,并调用run1方法执行任务
    self.thread = [[NSThread alloc] initWithTarget:self selector:@selector(run1) object:nil];
    // 开启线程
    [self.thread start];    
}

- (void) run1 {
    // 这里写任务
    NSLog(@"----run1-----");

    // 添加下边两句代码,就可以开启RunLoop,之后self.thread就变成了常驻线程,可随时添加任务,并交于RunLoop处理
    [[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
    [[NSRunLoop currentRunLoop] run];

    // 测试是否开启了RunLoop,如果开启RunLoop,则来不了这里,因为RunLoop开启了循环。
    NSLog(@"未开启RunLoop");
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {   
    // 利用performSelector,在self.thread的线程中调用run2方法执行任务
    [self performSelector:@selector(run2) onThread:self.thread withObject:nil waitUntilDone:NO];
}

- (void) run2 {
    NSLog(@"----run2------");
}

经过运行测试,除了之前打印的----run1-----,每当我们点击屏幕,都能调用----run2------。这样我们就实现了常驻线程的需求。

扩展

Runloop 源码

响应source0:__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__触摸事件 响应source1:__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__系统端口事件

二进制重排

iOS进阶-启动优化

系统复制粘贴怎么实现

操作系统中会有一块地方,称作剪贴板(clipboard),专门用来处理复制粘贴。不同系统的细节可能会不同,但大致上是这样的:

如果是: 文本或者富文本类型(图片+ 文字)类型

当文件为文本或者富文本类型(图片+ 文字)类型,复制操作会克隆一份到剪贴板里面。再将剪贴板里的文本克隆到所粘贴应用程序之中;

大部分的系统在复制文本类的文件时会保留字体、字号等信息

如果是: 文件类型

  • 复制时只会克隆文件的路径到剪贴板,等到粘贴时再具体处理
  • 粘贴
    • 同一分区下,只会更改文件路径
    • 不同分区下,粘贴(或剪切)文件,会重新开辟空间,然后克隆文件;

如果涉及到不通设备之间复制粘贴,都会一个「介于两系统之间的」剪贴板去处理状态