docs: refine

This commit is contained in:
LiuBinPeng
2022-04-19 17:15:49 +08:00
parent e2871d54e4
commit 7241220c8e
92 changed files with 10837 additions and 1963 deletions

View File

@@ -180,4 +180,34 @@ if (self.socket == NULL) {
[_delegates setObject:delegate forKey:task];
[task resume];
}
```
### NSURLProtocol 主意事项
使用 NSURLProtocol 的时候,如果是代理 NSURLSession 的网络请求,则需要重写 protocolClasses 方法。但是在你往给方法设置 protocolClasses 的时候可能全局也有其他 SDK、工具类也做了修改。这样子需要注意不能丢弃别人的也不能丢弃自己的。参考 OHHTTPStubs 在注册 NSURLProtocol 子类的处理
```
+ (void)setEnabled:(BOOL)enable forSessionConfiguration:(NSURLSessionConfiguration*)sessionConfig
{
// Runtime check to make sure the API is available on this version
if ( [sessionConfig respondsToSelector:@selector(protocolClasses)]
&& [sessionConfig respondsToSelector:@selector(setProtocolClasses:)])
{
NSMutableArray * urlProtocolClasses = [NSMutableArray arrayWithArray:sessionConfig.protocolClasses];
Class protoCls = HTTPStubsProtocol.class;
if (enable && ![urlProtocolClasses containsObject:protoCls])
{
[urlProtocolClasses insertObject:protoCls atIndex:0];
}
else if (!enable && [urlProtocolClasses containsObject:protoCls])
{
[urlProtocolClasses removeObject:protoCls];
}
sessionConfig.protocolClasses = urlProtocolClasses;
}
else
{
NSLog(@"[OHHTTPStubs] %@ is only available when running on iOS7+/OSX9+. "
@"Use conditions like 'if ([NSURLSessionConfiguration class])' to only call "
@"this method if the user is running iOS7+/OSX9+.", NSStringFromSelector(_cmd));
}
}
```

102
Chapter1 - iOS/1.102.md Normal file
View File

@@ -0,0 +1,102 @@
# LLVM
[LLVM](https://llvm.org/) 项目是模块化、可重用的编译器以及工具链技术的集合
> The LLVM Project is a collection of modular and reusable compiler and toolchain technologies.
## 结构
![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/LLVM-segment.png)
LLVM 由三部分构成:
- FrontEnd前端词法分析、语法分析、语义分析、生成中间代码
- Optimizer优化器优化中间代码
- Backend后端生成目标程序机器码
![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/LLVM-Structure.png)
正是由于这样的设计,使得 LLVM 具备很多有点:
- 不同的前端后端使用统一的中间代码 LLVM Intermediate Representation (LLVM IR)
- 如果需要支持一种新的编程语言,那么只需要实现一个新的前端
- 如果需要支持一种新的硬件设备,那么只需要实现一个新的后端
- 优化阶段是一个通用的阶段它针对的是统一的LLVM IR不论是支持新的编程语言还是支持新的硬件设备都不需要对优化阶段做修改
- 相比之下GCC 的前端和后端没分得太开,前端后端耦合在了一起。所以 GCC 为了支持一门新的语言,或者为了支持一个新的目标平台,就
变得特别困难
LLVM现在被作为实现各种静态和运行时编译语言的通用基础结构(GCC家族、Java、.NET、Python、Ruby、Scheme、Haskell、D等)
## Clang
[Clang](http://clang.llvm.org/) 是 LLVM 的一个子项目,基于 LLVM 架构的 c/c++/Objective-C 语言的编译器前端
GCC 是 c/c++ 等的编译器
Clang 相较于 GCC具备下面优点
- 编译速度快在某些平台上Clang的编译速度显著的快过GCC(Debug模式下编译OC速度比GGC快3倍)
- 占用内存小Clang 生成的 AST 所占用的内存是 GCC 的五分之一左右
- 模块化设计Clang 采用基于库的模块化设计,易于 IDE 集成及其他用途的重用
- 诊断信息可读性强在编译过程中Clang 创建并保留了大量详细的元数据 (metadata),有利于调试和错误报告
- 设计清晰简单,容易理解,易于扩展增强
![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/LLVM-phase.png)
### 查看编译过程
```shell
clang -ccc-print-phases main.m
```
对 main.m 文件
![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/clang-phase.png)
可以看到经历了输入、预处理、编译、LLVM Backend、汇编、链接、绑定架构7个阶段。
查看 preprocessor (预处理)的结果:`clang -E main.m`。预处理主要做的事情就是头文件导入、宏定义替换等。
词法分析,生成 Token`clang -fmodules -E -Xclang -dump-tokens main.m`
```c
#import <Foundation/Foundation.h>
int main(int argc, const char * argv[]) {
int a = 1;
int b =2;
int c = a + b;
return 0;
}
```
![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/clang-analysize.png)
语法分析生成语法树ASTAbstract Syntax Tree`clang -fmodules -fsyntax-only -Xclang -ast-dump main.m`
![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/clang-ast.png)
### LLVM IR

0
Chapter1 - iOS/1.103.md Normal file
View File

View File

@@ -83,11 +83,11 @@
#pragma clang diagnostic pop
10. Xcode Instruments 内存泄漏检测工具 Leaks 在内存检测后,无法看到具体的堆栈信息。
![Leaks](./../assets/2020-11-25-InstrumentMemoryLeaks.jpg)
![Leaks](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-11-25-InstrumentMemoryLeaks.jpg)
涂上右下方的 `Heaviest Stack Trace` 模块看不到对应的堆栈信息。一番定位问题后发现是工程项目在 debug 阶段Build Setting 中的 **Debug Information Format** 选项的 debug 条目是没有 dSYM 文件的,我们要想看到堆栈信息,就必须选择 `DWARF with dSYM File` 选项。
![](./../assets/2020-11-25-BuildSettingsDebugInformationFormat.png)
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-11-25-BuildSettingsDebugInformationFormat.png)
DWARF即 ***Debug With Arbitrary Record Format*** ,是一个标准调试信息格式,即调试信息。这部分信息可以查看我的[这篇文章](./1.74.md)中讲 iOS 符号化的部分。

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,10 +1,6 @@
# NSTimer 中的内存泄露
- GCD 的 timer
- NSProxy
- 采用 Block 的形式为 NSTimer 增加分类
NSTimer、CADisplayLink 的 基础 API `[NSTimer scheduledTimersWithTimeInterval:1 repeat:YES block:nil]` 和当前的 VC 都会互相持有,造成环,会存在内存泄漏问题
```objective-c
@interface ViewController()
@@ -50,7 +46,332 @@
1. 控制器不再强引用定时器
2. 定时器不再保留当前的控制器
```objective-c
## 解决方案:
### 替换 NSTimer API
```objectivec
__weak typeof(self) weakSelf = self;
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
[weakSelf timerTest];
}];
```
### GCD Timer
CADisplayLink、NSTimer 都是依靠 RunLoop 实现的,所以当 RunLoop 任务繁重的时候,定时器可能不准。
GCD 的定时器会更加准时,底层依赖系统内核。
```objectivec
@property (nonatomic, strong) dispatch_source_t timer;
// 创建队列
dispatch_queue_t queue = dispatch_get_main_queue();
// 创建 GCD 定时器
dispatch_source_t timerSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
uint64_t start = 2.0;
uint64_t interval = 1.0;
// 设置定时器周期
dispatch_source_set_timer(timerSource, dispatch_time(DISPATCH_TIME_NOW, start * NSEC_PER_SEC), interval * NSEC_PER_SEC, 0);
// 设置定时器任务
dispatch_source_set_event_handler(timerSource, ^{
NSLog(@"tick tock");
});
// 启动定时器
dispatch_resume(timerSource);
self.timer = timerSource;
```
为什么 GCD timer 会更准确?因为普通定时器运行依赖 RunLoopRunLoop 一个运行周期内的任务繁忙程度是不确定的。当某次任务繁重,那么定时器调度就不准时。
GCD timer 不依赖 RunLoop系统底层驱动所以会更加准确。因为和 RunLoop 无关,所以和 UI 滚动RunLoop mode 切换到 UITrackingMode 也不影响 GCD timer。
### 打破循环引用NSTimer target 自定义
```objectivec
@interface LBPProxy : NSObject
+ (instancetype)proxyWithObject:(id)target;
@property (nonatomic, weak) id target;
@end
@implementation LBPProxy
+ (instancetype)proxyWithObject:(id)target{
LBPProxy *proxy = [[LBPProxy alloc] init];
proxy.target = target;
return proxy;
}
- (id)forwardingTargetForSelector:(SEL)aSelector{
return self.target;
}
@end
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:[LBPProxy proxyWithObject:self] selector:@selector(timerTest) userInfo:nil repeats:YES];
```
### 高精度定时器封装
项目中经常使用定时器,普通定时器存在精度丢失的问题、循环引用的问题,为了使用方法我们封装一个定时器
```objectivec
#import <Foundation/Foundation.h>
@interface PreciousTimer : NSObject
+ (NSString *)execTask:(void(^)(void))task
start:(NSTimeInterval)start
interval:(NSTimeInterval)interval
repeats:(BOOL)repeats
async:(BOOL)async;
+ (NSString *)execTask:(id)target
selector:(SEL)selector
start:(NSTimeInterval)start
interval:(NSTimeInterval)interval
repeats:(BOOL)repeats
async:(BOOL)async;
+ (void)cancelTask:(NSString *)name;
@end
#import "PreciousTimer.h"
@implementation PreciousTimer
static NSMutableDictionary *timers_;
dispatch_semaphore_t semaphore_;
+ (void)initialize
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
timers_ = [NSMutableDictionary dictionary];
semaphore_ = dispatch_semaphore_create(1);
});
}
+ (NSString *)execTask:(void (^)(void))task start:(NSTimeInterval)start interval:(NSTimeInterval)interval repeats:(BOOL)repeats async:(BOOL)async
{
if (!task || start < 0 || (interval <= 0 && repeats)) return nil;
// 队列
dispatch_queue_t queue = async ? dispatch_get_global_queue(0, 0) : dispatch_get_main_queue();
// 创建定时器
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
// 设置时间
dispatch_source_set_timer(timer,
dispatch_time(DISPATCH_TIME_NOW, start * NSEC_PER_SEC),
interval * NSEC_PER_SEC, 0);
dispatch_semaphore_wait(semaphore_, DISPATCH_TIME_FOREVER);
// 定时器的唯一标识
NSString *name = [NSString stringWithFormat:@"%zd", timers_.count];
// 存放到字典中
timers_[name] = timer;
dispatch_semaphore_signal(semaphore_);
// 设置回调
dispatch_source_set_event_handler(timer, ^{
task();
if (!repeats) { // 不重复的任务
[self cancelTask:name];
}
});
// 启动定时器
dispatch_resume(timer);
return name;
}
+ (NSString *)execTask:(id)target selector:(SEL)selector start:(NSTimeInterval)start interval:(NSTimeInterval)interval repeats:(BOOL)repeats async:(BOOL)async
{
if (!target || !selector) return nil;
return [self execTask:^{
if ([target respondsToSelector:selector]) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[target performSelector:selector];
#pragma clang diagnostic pop
}
} start:start interval:interval repeats:repeats async:async];
}
+ (void)cancelTask:(NSString *)name
{
if (name.length == 0) return;
dispatch_semaphore_wait(semaphore_, DISPATCH_TIME_FOREVER);
dispatch_source_t timer = timers_[name];
if (timer) {
dispatch_source_cancel(timer);
[timers_ removeObjectForKey:name];
}
dispatch_semaphore_signal(semaphore_);
}
@end
```
使用 Demo
```objectivec
- (void)viewDidLoad{
[super viewDidLoad];
NSLog(@"now");
self.timerId = [PreciousTimer execTask:^{
NSLog(@"tick tock %@", [NSThread currentThread]);
} start:2 interval:1 repeats:YES async:YES];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
[PreciousTimer cancelTask:self.timerId];
}
```
说明:直接 `performSelector` 存在警告,可以告诉编译器忽略警告。可以在 Xcode 点开警告,查看详情,复制 `[]` 里面的字符串去忽略警告
![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/ignoreXcodewarning.png)
### NSProxy
```objectivec
#import "LBPProxy.h"
@implementation LBPProxy
+ (instancetype)proxyWithObject:(id)target{
LBPProxy *proxy = [LBPProxy alloc];
proxy.target = target;
return proxy;
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel{
return [self.target methodSignatureForSelector:sel];
}
- (void)forwardInvocation:(NSInvocation *)invocation{
// 方法1
invocation.target = self.target;
[invocation invoke];
// 方法2
[invocation invokeWithTarget:self.target];
}
@end
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:[LBPProxy proxyWithObject:self] selector:@selector(timerTest) userInfo:nil repeats:YES];
```
QA: 自己写的继承自 NSObject 的代理对象和继承自 NSProxy 的代理有何区别?
NSProxy 效率更高。继承自 NSObject 的代理内部运行的时候还是存在方法查找isa、superclass、cache、methods流程。
看一段神奇的代码
`LBPProxy`
```objectivec
@interface LBPProxy : NSObject
+ (instancetype)proxyWithObject:(id)target;
@property (nonatomic, weak) id target;
@end
@implementation LBPProxy
+ (instancetype)proxyWithObject:(id)target{
LBPProxy *proxy = [LBPProxy alloc];
proxy.target = target;
return proxy;
}
- (id)forwardingTargetForSelector:(SEL)aSelector{
return self.target;
}
@end
```
`LBPProxy2`
```objectivec
@interface LBPProxy2 : NSProxy
+ (instancetype)proxyWithObject:(id)target;
@property (nonatomic, weak) id target;
@end
@implementation LBPProxy2
+ (instancetype)proxyWithObject:(id)target{
LBPProxy2 *proxy = [LBPProxy2 alloc];
proxy.target = target;
return proxy;
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel{
return [self.target methodSignatureForSelector:sel];
}
- (void)forwardInvocation:(NSInvocation *)invocation{
// 方法1
invocation.target = self.target;
[invocation invoke];
// 方法2
[invocation invokeWithTarget:self.target];
}
@end
```
main.m
```objectivec
ViewController *vc = [[ViewController alloc] init];
LBPProxy *p1 = [LBPProxy proxyWithObject:vc];
LBPProxy2 *p2 = [LBPProxy2 proxyWithObject:vc];
NSLog(@"%d %d",
[p1 isKindOfClass:[UIViewController class]],
[p2 isKindOfClass:[UIViewController class]]);
appDelegateClassName = NSStringFromClass([AppDelegate class]);
// 0 1
```
为什么打印出 `0 1`。
分析:
- p1 是 LBPProxy 类,继承于 NSObject 所以就不是 UIViewController 类型。
- p2 是 LBPProxy2 类,继承自 NSProxy当调用 isKindOfClass 这个方法的时候,也会进行消息转发,即调用 `forwardInvocation` 方法,其内部实现 `[invocation invokeWithTarget:self.target];` 则触发 self.target 的逻辑。此时 self.target 就是 VC所以为 1。
这一点可以查看 GUN 查看下源码印证。`NSProxy.m`
```objectivec
- (BOOL) isKindOfClass: (Class)aClass
{
NSMethodSignature *sig;
NSInvocation *inv;
BOOL ret;
sig = [self methodSignatureForSelector: _cmd];
inv = [NSInvocation invocationWithMethodSignature: sig];
[inv setSelector: _cmd];
[inv setArgument: &aClass atIndex: 2];
[self forwardInvocation: inv];
[inv getReturnValue: &ret];
return ret;
}
```
可以看到内部直接调用了消息转发。
### 采用 Block 的形式为 NSTimer 增加分类
```objectivec
//.h文件
#import <Foundation/Foundation.h>
@@ -166,6 +487,4 @@ __strong __typeof(&*weakSelf)self = weakSelf;
}
```
iOS 10 中,定时器 api 增加了 block 方法,实现原理与此类似,这里采用分类为 NSTimer 增加 block 参数的方法,最终的行为一致

View File

@@ -1,8 +1,4 @@
# KVC && KVO
# KVC && KVO
## 一、基本用法-字典快速赋值
@@ -24,13 +20,12 @@ KVC 可以将字典里面和 model 同名的 property 进行快速赋值 `setVal
运行上面的代码,代码不崩溃,只不过在输出值的时候输出了 null
- 情况二:在 NSDictionary 中存在某个值,但是在 model 里面没有值
运行后编译成功,但是代码奔溃掉。原因是 KVC 。所以我们只需要实现这么一个方法。甚至不需要写函数体部分
```
- (void)setValue:(id)value forUndefinedKey:(NSString *)key{
}
```
@@ -38,7 +33,6 @@ KVC 可以将字典里面和 model 同名的 property 进行快速赋值 `setVal
我们照样可以利用 **setValue:forUndefinedKey:** 去处理
```objective-c
//model
@property (nonatomic,copy)NSString *name;
@@ -65,7 +59,6 @@ NSMutableArray *hobbies = [_person mutableArrayValueForKeyPath:@"hobbies"];
```
- 情况五: 注册依赖键.
KVO 可以观察属性的二级属性对象的所有属性变化。说人话就是“假如 Person 类有个 Dog 类Dog 类有 name、fur、weight 等属性,我们给 Person 的 Dog 属性观察,假如 Dog 的任何属性变化是Person 的观察者对象都可以拿到当前的变化值。我们只需要在 Person 中写下面的方法即可”
@@ -82,7 +75,7 @@ self.person.dog.weight = 50;
// Person.m
+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
if ([key isEqualToString:@"dog"]) {
NSArray *affectingKeys = @[@"name", @"fur", @"weight"];
keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
@@ -91,9 +84,6 @@ self.person.dog.weight = 50;
}
```
## 二、 KVO 的本质
kVO 是Objective-C 对观察者模式的实现。也是 Cocoa Binding 的基础。
@@ -109,18 +99,39 @@ kVO 是Objective-C 对观察者模式的实现。也是 Cocoa Binding 的基础
return NO;
}
```
3. 若类有实例变量 NSString *_foo 调用 setValue:forKey: 是以 foo 还是 _foo 作为 key
都可以
4. KVC 的 keyPath 中的集合运算符如何使用
- 必须用在 **集合对象** 或者 **普通对象的集合属性** 上
-简单的集合运算符有 @avg、@count、@max、@min、@sum
5. KVO 和 KVC 的 keyPath 一定是属性吗?
可以是成员变量
可以是成员变量
6. KVO 中 派生类的 setter 方法内部实现调用了 Foundation 框架中的 `_NSSetIntValueAndNotify`.
7. 直接修改对象的成员变量会触发 KVO 吗?
不会。因为成员变量没有 setter.
```
@interface Person: NSObject
{
@public:
    int age;
}
@end
```
8. 手动触发 KVO调用 willChangeValueForKey、didChangeValueForKey
9.
@@ -142,17 +153,13 @@ Apple 文档告诉我们:被观察对象的 `isa指针` 会指向一个中间
- 键值观察通知依赖于 NSObject 的两个方法:`willChangeValueForKey:、didChangeValueForKey:` 。在一个被观察属性改变之前,调用 `willChangeValueForKey:` 记录旧的值。在属性值改变之后调用 `didChangeValueForKey:`,从而 `observeValueForKey:ofObject:change:context:` 也会被调用。
![KVO原理图](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2018_11_12_KVO.png)
为什么要选择是继承的子类而不是分类呢?
子类在继承父类对象,子类对象调用调方法的时候先看看当前子类中是否有方法实现,如果不存在方法则通过 isa 指针顺着继承链向上找到父类中是否有方法实现,如果父类种也不存在方法实现,则继续向上找...直到找到 NSObject 类为止,系统会抛出几次机会给程序员补救,如果未做处理则奔溃
关于分类与子类的关系可以看看我之前的 [文章](1.50.md).
## 四、 模拟实现系统的 KVO
1. 创建被观察对象的子类
@@ -160,6 +167,7 @@ Apple 文档告诉我们:被观察对象的 `isa指针` 会指向一个中间
3. 外界改变 isa 指针class方法重写
我们用自己的类模拟系统的 KVO。
```
//NSObject+LBPKVO.h
#import <Foundation/Foundation.h>
@@ -169,7 +177,7 @@ NS_ASSUME_NONNULL_BEGIN
@interface NSObject (LBPKVO)
- (void)lbpKVO_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
@end
NS_ASSUME_NONNULL_END
@@ -189,15 +197,15 @@ NS_ASSUME_NONNULL_END
Class myclass = objc_allocateClassPair(self.class, [currentClassName UTF8String], 0);
// 生成后不能马上使用,必须先注册
objc_registerClassPair(myclass);
//2. 重写 setter 方法
class_addMethod(myclass,@selector(setName:) , (IMP)setName, "v@:@");
//3. 修改 isa
object_setClass(self, myclass);
//4. 将观察者保存到当前对象里面
objc_setAssociatedObject(self, "observer", observer, OBJC_ASSOCIATION_ASSIGN);
//5. 将传递的上下文绑定到当前对象里面
objc_setAssociatedObject(self, "context", (__bridge id _Nullable)(context), OBJC_ASSOCIATION_RETAIN);
}
@@ -212,7 +220,7 @@ void setName (id self, SEL _cmd, NSString *name) {
object_setClass(self, class_getSuperclass(class));
//2. 调用父类的 setName 方法
objc_msgSend(self, @selector(setName:), name);
//3. 调用观察
id observer = objc_getAssociatedObject(self, "observer");
id context = objc_getAssociatedObject(self, "context");
@@ -235,7 +243,7 @@ void setName (id self, SEL _cmd, NSString *name) {
_person.hobbies = [@[@"iOS"] mutableCopy];
NSDictionary *context = @{@"name": @"成吉思汗", @"hobby" : @"弯弓射大雕"};
[_person lbpKVO_addObserver:self forKeyPath:@"hobbies" options:(NSKeyValueObservingOptionNew) context:(__bridge void * _Nullable)(context)];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
@@ -247,10 +255,8 @@ void setName (id self, SEL _cmd, NSString *name) {
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
NSLog(@"%@",change);
}
```
KVO 的缺陷:
KVO 虽然很强大,你只能重写 `-observeValueForKeyPath:ofObject:change:context: ` 来获得通知,想要提供自定义的 selector ,不行;想要传入一个 block 没门儿。感觉如果加入 block 就更棒了。
@@ -261,4 +267,54 @@ KVO 的改装:
## 五、 KVC
`setValueForKey` 用来设置对象的一层属性值修改。
`setValueForKeyPath` 可以设置对象的某个属性(属性本身是对象)的属性值修改
KVC 之后会触发 KVO。为什么探究下 `setValueForKey`
`[self.person setValue:@10 forKey:@"age"]` 会先调用 `setKey:` 同名的方法,找不到则调用 `_setKey:` 的方法,如果还是找不到则调用 `+(BOOL)accessInstanceVariableDirectlt`,如果该方法返回 YES则可以直接修改成员变量的值会按照 _key、_isKey、key、isKey 的顺序寻找成员变量,如果找到则直接赋值,没找到则抛出异常 NSUnknownKeyException
整个流程如下
![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/KVC-process.png)
```
@implementation Person
- (void)setAge:(int)age
{
_age = age;
}
- (void)_setAge:(int)age
{
_age = age;
}
+ (BOOL)accessInstanceVariableDirectlt
{
return YES;
}
@end
```
valueForKey 原理
- 按照 getKey、key、isKey、_key 的顺序寻找方法实现
- 找到则直接调用方法,返回值
- 如果没找到则调用 accessInstanceVariableDirectlt 方法,询问是否可以访问成员变量。为 NO 则抛出异常
- 为 YES 则按照 __key、isKey、key、isKey 的顺序访问成员变量。找到哪个则返回值
- 都没找到则抛出异常
![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/KVC-get-process.png)

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,5 @@
# App 瘦身之道
App 的包大小做优化的目的就是为了节省用户流量,提高用户的下载速度,也是为了用户手机节省更多的空间。另外 App Store 官方规定 App 安装包如果超过 150MB那么不可以使 OTAover-the-air环境下载也就是只可以在 WiFi 环境下载,企业或者独立开发者万万不想看到这一点。免得失去大量的用户。
同时如果你的 App 需要适配 iOS7、iOS8 那么官方规定主二进制 text 段的大小不能超过 60MB。如果不能满足这个标准则无法上架 App Store。
@@ -12,16 +10,26 @@ App 的包大小做优化的目的就是为了节省用户流量,提高用户
App 瘦身一般指的是安装包IPA主要由可执行文件、资源组成。
对于产物的分析,可以查看可执行文件的具体组成。
Xcode - Build Setting - Write Link Map File 设置为 YES。修改 Path to Link Map File 即可。
可借助第三方工具解析LinkMap文件 [GitHub - huanxsd/LinkMap: 检查每个类占用空间大小工具](https://github.com/huanxsd/LinkMap)
## 1. App Thinning
App Thinning 是指 iOS9 以后引入的一项优化,官方描述如下
> The App Store and operating system optimize the installation of iOS, tvOS, and watchOS apps by tailoring app delivery to the capabilities of the users particular device, with minimal footprint. This optimization, called app thinning, lets you create apps that use the most device features, occupy minimum disk space, and accommodate future updates that can be applied by Apple. Faster downloads and more space for other apps and content provides a better user experience.
Apple 会尽可能,自动降低分发到具体用户时,所需要下载的 App 大小。其中包含三项主要功能Slicing、Bitcode、On-Demand Resources。
App Thinning 是苹果公司推出的一项改善 App 下载进程的新技术,主要为了解决用户下载 App 耗费过高流量的问题,同时还可以节省用户设备存储空间。
### 1.1 Slicing
![Slicing](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2018-11-15-AppSlicing.jpeg)
@@ -36,7 +44,6 @@ App Thinning 是苹果公司推出的一项改善 App 下载进程的新技术
![变体](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2018-11-15-AppVariant.jpeg)
### 1.2 Bitcode
> Bitcode is an intermediate representation of a compiled program. Apps you upload to iTunes Connect that contain bitcode will be compiled and linked on the App Store. Including bitcode will allow Apple to re-optimize your app binary in the future without the need to submit a new version of your app to the App Store.
@@ -47,8 +54,8 @@ Bitcode 是一种程序`中间码`。包含 Bitcode 配置的程序将会在 App
开启位置Build Settings -> Enable Bitcode -> 设置为 YES
开启 Bitcode有这么2点需要注意
- 全部都要支持。我们所依赖的静态库、动态库、Cocoapods 管理的第三方库,都需要开启 Bitcode。否则会编译失败
- 奔溃定位。开启 Bitcode 后最终生成的可执行文件是 Apple 自动生成的,同时会产生新的符号表文件,所以我们无法使用自己包生成的 dYSM 符号化文件来进行符号化。
@@ -59,12 +66,10 @@ Bitcode 是一种程序`中间码`。包含 Bitcode 配置的程序将会在 App
在上传到 App Store 时需勾选“Includ app symbols for your application...”。勾选之后 Apple 会自动生成对应的 dYSM然后可以在 Xcode -> Window -> Organizer 中,或者在 Apple Store Connect 中下载对应的 dYSM 来进行符号化
![App Connect-dYSM](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2018-11-15-AppConnectYSM.jpeg)
![Xcode-dYSM](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2018-11-15-XcodedYSM.jpeg)
那么 Bitcode 会对 App Thining 有什么作用?
在 New Features in Xcode7 中有这么一段描述:
@@ -74,8 +79,7 @@ Bitcode 是一种程序`中间码`。包含 Bitcode 配置的程序将会在 App
App Store 会再按需将这个 bitcode 编译进 32/64 位的可执行文件。
所以网上铺天盖地地说 Bitcode 完成了具体架构的拆分,从而实现瘦包
### 1.3 on-Demand Resources
### 1.3 on-Demand Resources
on-Demand Resource 即一部分图片可以被放置在苹果的服务器上,不随着 App 的下载而下载,直到用户真正进入到某个页面时才下载这些资源文件。
@@ -85,8 +89,6 @@ on-Demand Resource 即一部分图片可以被放置在苹果的服务器上,
如需支持 iOS9 以下系统,那么无法使用这个功能,否则上传会失败
## 2 包体积
2个概念
@@ -106,21 +108,17 @@ Universal 指通用设备,即未应用 App slicing 优化,同时包含了所
观察 .ipa 的大小和 Universal 对应的包大小相当,稍微小一点,因为 App Store 对 .ipa 做了加密处理
有时候下载 App 会提示“此项目大于 150MB除非此项目支持增量下载否则您必须连接至 WiFi 才能下载”。150MB 针对的是下载大小。
- 下载大小:通过 WiFi 下载的压缩 App 大小
- 安装大小:此 App 将在用户设备上占用磁盘空间的大小
所以我们要瘦包,关键在于减小 .app 文件的大小。
### 2.1 Architectures
如果不支持32位以及 iOS8 ,去掉 armv7 ,可执行文件以及库会减小,即本地 .ipa 也会减小
### 2.2 Resources
资源的优化也就是平时的细心与审查。
@@ -138,23 +136,20 @@ Bundle: 非放在 Asset Catlog 中管理的图片资源。包括 Bundle散落
- 图片管理方式规范
- on-Demand Resource游戏的、前置关卡依赖、滤镜App 等的依赖资源,建议用这种方式动态下载图片资源)
#### 2.2.1 无用文件的删除
无用文件主要包含:无用图片、无用非图片部分。
非图片部分:资源较少,使用方式固定。比如音频、字体。需要手动排查
图片部分:主要使用一个开源的 Mac App [LSUnusedResources](https://github.com/tinymind/LSUnusedResources) 进行冗余图片的排查。
删除无用的图片过程可以概括为下面6步
1. 通过 find 命令获取 App 安装包中的所有资源文件
2. 设置用到的资源类型。比如 gif、jpg、jpeg、png、webp
3. 使用正则匹配出在源码中使用到的资源名,比如 pattern = @"@"(.+?)""
4. 使用 find 命令找到篇所有资源文件再去源码中找到使用到的资源文件2个集合的差集就是无用资源了。
5. 确认无用资源后可以使用 NSFileManager 进行文件的删除。
如果不想重新写一个工具,那么可以直接使用开源的工具 LSUnusedResources
@@ -168,14 +163,15 @@ Bundle: 非放在 Asset Catlog 中管理的图片资源。包括 Bundle散落
//...
}
```
源码中的正则表达式处理的情况并不是很准确。可以根据自己的情况修改正则即可
源码中的正则表达式处理的情况并不是很准确。可以根据自己的情况修改正则即可
#### 2.2.2 图片资源的压缩
删除了无用的资源,那么对于资源这块还是有操作的空间的,比如图片资源的压缩。目前压缩比较好的方案就是 WebP它是谷歌公司的一个开源项目。
WebP 的优势:
- 压缩率高。支持有损和无损2种方式比如将 Gif 图可以转换为 Animated WebP有损模式下可以减小 64%,无损模式下可以减小 19%
- WebP 支持 Alpha 透明和 24-bit 颜色数,不会像 PNG8 那样因为色彩不够出现毛边。
@@ -184,7 +180,6 @@ Google 公司在开源 WebP 的同时,还提供了一个图片压缩工具 [cw
缺点WebP 在 CUP 消耗和解码时间上会比 PNG 高2倍所以我们做选择的时候需要取舍。
#### 2.2.3 重复文件删除
重复文件,即两个内容完全一致的文件。但是文件命名不一样。
@@ -198,7 +193,6 @@ fdupes 是 Linux 下的一个工具,它由 Adrian Lopez 用 C 语言编写并
执行结束后会在命令行展示出来,所以需要我们人工将这些文件确认对比后删除掉。
#### 2.2.4 大文件压缩
图片本身的压缩,建议使用 ImageOptim。它整合了 Win、Linux 上诸多著名图片处理工具的特色,比如 PNGOUT、AdvPNG、Pngcrush、OptiPNG、JpegOptim、Gifsicle 等。
@@ -209,10 +203,8 @@ Xcode 提供给我们2个编译选项来帮助压缩图像
- Compress PNG Files: 打包的时候自动对图片进行无损压缩。使用的工具为 pngcrush压缩比蛮高。
- Remove Text Medadata From PNG Files移除 PNG 资源的文本字符,比如图像名称、作者、版权、创作时间、注释等信息
#### 2.2.5 图片管理方式规范
##### 2.2.5.1 主工程中的图片管理
工程中所有使用的 Asset Catlog 管理的图片(在 .xcassets 文件夹下)最终都会输出到 Asset.car 内。不在 Asset.car 内的都归为 Bundle 管理。
@@ -249,6 +241,7 @@ for (NSInteger index = 0; index < 10; index++) {
}
self.imageView.image = images.lastObject;
```
</details>
Timeprofile-imageNamedFromAssets
@@ -260,52 +253,57 @@ TimeProfile-imageWithContentsOfFile
Timeprofile-UIImageNamedFromFolder
![Timeprofile-UIImageNamedFromFolder](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2019-05-07-Timeprofile-UIImageNamedFromFolder.png)
Images.xcassets
- 图片大小要精确,不要出现图片太大的情况
- 不要存放大图,不然会产生缓存
- 不要存 jpg 图片,打包会变大
- 图片不需要额外压缩(有人做过实验,对放入 assets 里面的图片进行压缩后打包发现包体积反而增大,怀疑是 Xcode 的编译选项 Compress PNG Files 自动对图片进行压缩2种压缩起了冲突反而增大
##### 2.2.5.2 各个 pod 库中的图片管理
CocoPods 中两种资源引用方式介绍下:
- resource_bundles
> We strongly recommend library developers to adopt resource bundles as there can be name collisions using the resources attribute.
允许定义当前的 pod 库的最远包的名称和文件。用 hash 形式声明key 是 bundle 的名称value 是需要包含文件的通配 patterns
CocoPods 官方强烈推荐该方法引用资源,因为 key-value 可以避免相同资源的名称冲突
> We strongly recommend library developers to adopt resource bundles as there can be name collisions using the resources attribute.
> 允许定义当前的 pod 库的最远包的名称和文件。用 hash 形式声明key 是 bundle 的名称value 是需要包含文件的通配 patterns
> CocoPods 官方强烈推荐该方法引用资源,因为 key-value 可以避免相同资源的名称冲突
- resources
> We strongly recommend library developers to adopt resource bundles as there can be name collisions using the resources attribute. Moreover, resources specified with this attribute are copied directly to the client target and therefore they are not optimised by Xcode.
使用该方法引用资源,被指定的资源会被拷贝进 target 工程的 main bundle 中。
> We strongly recommend library developers to adopt resource bundles as there can be name collisions using the resources attribute. Moreover, resources specified with this attribute are copied directly to the client target and therefore they are not optimised by Xcode.
> 使用该方法引用资源,被指定的资源会被拷贝进 target 工程的 main bundle 中。
说说项目中的情况吧:在工程中之前是通过 resource_bundles 引用资源的。资源是放在 Resources 目录下的图片引用。查询资料后说「如果图片资源放到 .xcasset 里面 Xcode 会帮我们自动优化、可以使用 Slicing 等(这里不仅仅指的是 resource_bundle 下的 xcassets」。所以动手将各个 Pod 库里面的图片全都通过 Assets Catalog 的方式进行处理。
![Pod组件库图片处理前后对比](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2019-05-08-Cocopod-Assets.png)
步骤:
- 在各个 Pod 组件库里面的 Resources 目录下新建 Asset Catalog 文件,命名为 Images.xcassets
- 将 Resources 里面零散的图片资源拖进 Images.xcassets 里面
- 修改每个组件库的 podspec 文件
<details>
<details>
<summary>点击展开</summary>
```
s.resource_bundles = {
'XQ_UI' => ['XQ_UI/Assets/*.xcassets']
}
</details>
```
<summary>点击展开</summary>
```
s.resource_bundles = {
'XQ_UI' => ['XQ_UI/Assets/*.xcassets']
}
</details>
```
- 主工程执行 pod install
话说 `resources` 和 `resource_bundles` 都可以使用 Asset Catalog那么有何区别
- resources 只会将资源文件 copy 到 target 工程,最后和 target 工程的图片资源以及同样使用该方式的 Pod 库的图片资源共同打包到一个 `Assets.car` 中。因此图片资源会有混乱的可能。
- resource_bundles 会生成一个你在 `podspec` 中指定名称的 bundle且在 bundle 中也会生成一个 Assets.car。所以图片是肯定不会混乱的但是图片的访问方式需要注意。
解决方法:为每个 pod 新建一个图片的分类,比如 UIImage+XQUIModule。然后访问图片的时候通过 `[UIImage xquiModuleImageNamed:@"pull"]` 访问。
<details>
@@ -334,18 +332,15 @@ CocoPods 中两种资源引用方式介绍下:
}
@end
```
</details>
#### 2.2.6 矢量图的使用
事实上,对于 App 里面的单色图标,比如左上角的返回按钮、底部的 tabBar等只要是单色的纯色图标都是可以使用矢量图代替的比如 PDF、ttf 字体图标等。这样就不需要添加 @2x、@3x 图标,节省了空间。
iOS 中如何使用 ttf 矢量图,可以查看这个 [Repo](https://github.com/FantasticLBP/IconFont_Demo)
## 3. Executable file
### 3.1 编译选项优化
@@ -365,7 +360,6 @@ optimization 选项设置为 space 可以减少包大小
> For statically linked executables, dead-code stripping is the process of removing unreferenced code from the executable file. If the code is unreferenced, it must not be used and therefore is not needed in the executable file. Removing dead code reduces the size of your executable and can help reduce paging.
删除静态链接的可执行文件中未引用的代码
Debug 设置为 NO Release 设置为 YES 可减少可执行文件大小。
@@ -394,7 +388,6 @@ Build Settings -> code Generation -> Optimization Level
默认选项,不做修改。
#### 3.1.5 Swift Compiler - Code Generation
Xcode 9.3 版本之后 Swift 编译器提供了新的 Optimization Level 选项来帮助减少 Swift 可执行文件的大小:
@@ -409,7 +402,6 @@ Xcode 9.3 版本之后 Swift 编译器提供了新的 Optimization Level 选项
除了 -O 和 -Osize 还有另外一个概念也值得说一下。 就是 Single File 和 Whole Module 。 在之前的 XCode 版本,这两个选项和 -O 是连在一起设置的Xcode 9.3 中,将他们分离出来,可以独立设置:
Single File 和 Whole Module 这两个模式分别对应编译器以什么方式处理优化操作。
- Single File逐个文件进行优化它的好处是对于增量编译的项目来说它可以减少编译时间对没有更改的源文件不用每次都重新编译。并且可以充分利用多核 CPU并行优化多个文件提高编译速度。但它的缺点就是对于一些需要跨文件的优化操作它没办法处理。如果某个文件被多次引用那么对这些引用方文件进行优化的时候会反复的重新处理这个被引用的文件如果你项目中类似的交叉引用比较多就会影响性能。
@@ -420,7 +412,6 @@ Single File 和 Whole Module 这两个模式分别对应编译器以什么方式
故,在 Relese 模式下 -Osize 和 Whole Module 同时开启效果会最好!
#### 3.1.6 Strip Symbol Information
1、Deployment Postprocessing
@@ -434,7 +425,6 @@ Symbols Hidden by Default 会把所有符号都定义成”private extern”
Release 设置为 YESDebug 设置为 NO。
#### 3.1.7 Exceptions
在 iOS微信安装包瘦身 一文中,有提到:
@@ -447,7 +437,6 @@ Symbols Hidden by Default 会把所有符号都定义成”private extern”
可能和项目中用到比较少有关系。故保持开启状态。
#### 3.1.8 Link-Time Optimization
Link-Time Optimization 是 LLVM 编译器的一个特性,用于在 link 中间代码时,对全局代码进行优化。这个优化是自动完成的,因此不需要修改现有的代码;这个优化也是高效的,因为可以在全局视角下优化代码。
@@ -464,10 +453,8 @@ Link-Time Optimization 是 LLVM 编译器的一个特性,用于在 link 中间
在新的版本中,苹果使用了新的优化方式 Incremental大大减少了链接的时间。建议开启。
总结,开启这个优化后,一方面减少了汇编代码的体积,一方面提高了代码的运行效率。
### 3.2 代码瘦身
代码的优化,即通过删除无用类、无用方法、重复方法等,来达到可执行文件大小的减小。
@@ -478,35 +465,32 @@ Link-Time Optimization 是 LLVM 编译器的一个特性,用于在 link 中间
- 基于 Clang 扫描
- 基于可执行文件扫描
- 基于源码扫描
先谈几个概念。
可执行文件就是 **Mach-O** 文件,其大小是油代码量决定的,通常情况下,对可执行文件进行瘦身,就是找到并删除无用代码的过程。找到无用代码的过程类比找到无用图片的思路。
- 找到类和方法的全集
- 找到使用过的类和方法集合
- 取2者差集得到无用代码集合
- 工程师确认后,删除即可
LinkMap 文件分为3部分Object File、Section、Symbols。
![LinkMap结构](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2019-05-06-LinkMap-Structure.png)
- Object File包含了代码工程的所有文件
- Section描述了代码段在生成的 Mach-O 里的偏移位置和大小
- Symbols会列出每个方法、类、Block以及它们的大小
先说说如何快速找到方法和类的全集?
我们可以通过 **LinkMap** 来获得所有的代码类和方法的信息。获取 LinkMap 可以通过将 Build Setting 里面的 **Write Link Map File** 设置为 YES然后指定 **Path to Link Map File** 的路径就可以得到每次编译后的 LinkMap 文件了。
![Xcode中设置获取LinkMap](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2019-05-06-LinkMap-Xcode.png)
#### 3.2.1 基于 clang 扫描
基本思路是基于 clang AST。追溯到函数的调用层级记录所有定义的方法/类和所有调用的方法/类,再取差集。具体原理参考 如何使用 Clang Plugin 找到项目中的无用代码,目前只有思路没有现成的工具。
#### 3.2.2 基于可执行文件扫描LinkMap 结合 Mach-O 找无用代码)
上面我们得知可以通过 LinkMap 统计出所有的类和方法,还可以清晰地看到代码所占包大小的具体分布,进而有针对性地进行代码优化。
@@ -517,7 +501,6 @@ LinkMap 文件分为3部分Object File、Section、Symbols。
![LinkMap-Symbols](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2019-05-05-LinkMap-Symbols.png)
得到了代码的全集信息后,我们还需要找到已经使用过的方法和类,这样才可以获取差集,找到无用代码。所以接下来就谈谈如何通过 Mach-O 取到使用过的类和方法。
Objective-C 中的方法都会通过 **objc_msgSend** 来调用,而 objc_msgSend 在 Mach-O 文件里是通过 **_objc_selrefs** 这个 **section** 来获取 selector 这个参数的。
@@ -526,7 +509,6 @@ Objective-C 中的方法都会通过 **objc_msgSend** 来调用,而 objc_msgSe
那么Mach-O 文件中的 _objc_selrefs、_objc_classrefs、_objc_superrefs 如何查看呢?
1. 使用 otool 等命令逆向可执行文件中引用到的类/方法和所有定义的类/方法然后计算差集。具体参考iOS微信安装包瘦身目前只有思路没有现成的工具。
2. 使用 [MachOView](https://github.com/gdbinit/MachOView) 查看。但是这个项目运行不起来,这个新的 [Repo](https://github.com/fangshufeng/MachOView) 可以运行起来。
@@ -538,14 +520,12 @@ Objective-C 中的方法都会通过 **objc_msgSend** 来调用,而 objc_msgSe
由于 Objective-C 是一门动态语言所以检测出的结果仍旧需要我们2次确认。
#### 3.2.3 基于源码扫描
一般都是对源码文件进行字符串匹配。例如将 A *a、[A xxx]、NSStringFromClass("A")、objc_getClass("A") 等归类为使用的类,@interface A : B 归类为定义的类,然后计算差集。
基于源码扫描 有个已经实现的工具 - fui但是它的实现原理是查找所有 #import "A" 和所有的文件进行比对,所以结果相对于上面的思路来说可能更不准确。
#### 3.2.4 通过 AppCode 查找无用代码
AppCode 提供了 Inspect Code 来诊断代码,其中含有查找无用代码的功能。它可以帮助我们查找出 AppCode 中无用的类、无用的方法甚至是无用的 import ,但是无法扫描通过字符串拼接方式来创建的类和调用的方法,所以说还是上面所说的 基于源码扫描 更加准确和安全。
@@ -559,7 +539,6 @@ AppCode 提供了 Inspect Code 来诊断代码,其中含有查找无用代码
- 无用宏Unused macro 是无用的宏。
- 无用全局Unused global declaration 是无用全局声明。
#### 3.2.5 运行时真正检测类是否用过
通过上述手段找到并删除了无用代码。App 不断上线迭代蛮多代码都不会被调用了(业务被砍掉了)。这种方式下这些无用的代码也是可以被删除的。
@@ -603,8 +582,6 @@ isInitialized 的结果会保存到元类的 class_rw_t 结构体的 flags 信
既然可以在运行的期间知道类是否初始化了,那么就可以找出哪些类未初始化,即可以找到在真实环境里面没有用到的类并删除掉。
## 4. App Extension
App Extension 的占用,都放在 Plugin 文件夹内。它是独立打包签名,然后再拷贝进 Target App Bundle 的。
@@ -616,12 +593,11 @@ App Extension 的占用,都放在 Plugin 文件夹内。它是独立打包签
所以,如果可能的话,把相关的依赖改成动态库方式,达到共享。
## 5. 静态库瘦身
项目中都会引入第三方静态库。通过 lipo 工具可以查看支持的指令集,比如查看微博 SDK
终端切换到微博 SDK 的目录下执行下面命令
- 静态库指令集信息查看:`lipo -info libname.a(或者libname.framework/libname)`
```Shell
@@ -634,7 +610,6 @@ lipo -info libWeiboSDK.a
- 静态库拆分:`lipo 静态库文件路径 -thin CPU架构 -output 拆分后的静态库文件路径`
- 静态库合并:`lipo -create 静态库1文件路径 静态库2文件路径... 静态库n文件路径 -output 合并后的静态库文件径`
```Shell
lipo libWeiboSDK.a -thin armv7 -output libWeiboSDK-armv7.a
lipo libWeiboSDK.a -thin arm64 -output libWeiboSDK-arm64.a
@@ -646,35 +621,33 @@ lipo create libWeiboSDK-armv7.a libWeiboSDK-arm64.a -output libWeiboSDK.device.a
1. 平时使用包含模拟器指令集的静态库,在 App 发布的时候去掉
2. 如果使用 Cocoapods 管理可以使用2份 Podfile 文件。一份包含指令集一份不包含,发布的时候切换 Podfile 文件即可。或者一份 Podfile 文件,但是配置不同的环境设置
补充2个说明
1. dSYM 文件
符号表文件 .dSYM 文件是从 Mach-O 文件中抽取调试信息而得到的文件目录,实际用于保存调试信息的是 DWARF 文件
- 自动生成。Xcode 会在工程编译或者归档的时候自动生成 .dSYM 文件,在 Buld setting 设置中有开关可以设置去关掉 .dSYM 文件
- 手动生成。通过脚本从 Mach-O 文件中提取出来。
```
$ /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/dsymutil /Users/wangzz/Library/Developer/Xcode/DerivedData/YourApp-cqvijavqbptjyhbwewgpdmzbmwzk/Build/Products/Debug-iphonesimulator/YourApp.app/YourApp -o YourApp.dSYM
```
该方式通过 dsymutil 工具,从项目编译结果 .app 目录下的 Mach-O 文件中提取出调试符号表文件。Xcode 在归档的时候是通过它生辰的 .dSYM 文件
- 手动生成。通过脚本从 Mach-O 文件中提取出来。
```
$ /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/dsymutil /Users/wangzz/Library/Developer/Xcode/DerivedData/YourApp-cqvijavqbptjyhbwewgpdmzbmwzk/Build/Products/Debug-iphonesimulator/YourApp.app/YourApp -o YourApp.dSYM
```
该方式通过 dsymutil 工具,从项目编译结果 .app 目录下的 Mach-O 文件中提取出调试符号表文件。Xcode 在归档的时候是通过它生辰的 .dSYM 文件
2. DWARF 文件
DebuggingWith Arbitrary Record Formats 是 ELF 和 Mach-O 等文件格式中用来存储和处理调试信息的标准格式,.dSYM 文件中真正保存符号表数据的是 DWARF 文件。DWARF 文件中不同的数据都保存在相应的 section 中。
最后的一个对比效果图:
![瘦身效果图](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2019-05-09-AppThinning-Comparation.png)
总结瘦身技术常见操作就这些但是维持应用包体积的瘦身却是一个观念从日常开发到线上发布都需要有这个意识。这样当你在写代码的时候就会考虑同样一个效果你的具体实现手段是怎么样的。比如为了一个稍微炫酷的效果就要引入一个很大的三方库有了“瘦身”的意识你很大可能就是自己动手撸一个代码。比如一些无用资源的管理方式、有用的图片资源的高效管理方式等等。有了意识行动自然会往这个方面去靠。😂大道理一套一套的。我也不想的毕竟是playboy
其中遇到了一个神奇的问题。lint 的时候看到一些未使用的依赖库。见 [问题](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.53.md)
**By the way**
如果在应用包瘦身方面有其他的做法,请告知,完善文章。
参考文章:
- [Humble Assets Catalog](http://lingyuncxb.com/2019/04/14/HumbleAssetCatalog/)
- [关于 Pod 库的资源引用 resource_bundles or resources](http://zhoulingyu.com/2018/02/02/pod-resource-reference/)

View File

@@ -1,16 +1,382 @@
# App 启动时间优化
## 启动分类
- 冷启动Cold Launch点击 App 图标启动前,进程不在系统中。需要系统新创建一个进程并加载 Mach-O 文件dyld 从 Mach-O 头信息中读取依赖undefined的动态库库从动态库共享缓存中读取并链接经历一次完整的启动过程。
- 热启动Warm LaunchApp 在冷启动后,用户将 App 退后台。此阶段App 的进程还在系统中,用户重新启动进入 App 的过程,开发对该阶段能做的事情非常少。
所以我们聊启动时间优化,通常是指冷启动。此外启动慢,也就是看到主页面的过程很慢,都是发生在主线程上的。
为了量化启动时间,要么自定义 APM 监控。要么利用 Xcode 提供的启动时间统计。通过添加环境变量可以打印出APP的启动时间分析Edit scheme -> Run -> Arguments
- `DYLD_PRINT_STATISTICS` 设置为1
- 如果需要更详细的信息,那就将 `DYLD_PRINT_STATISTICS_DETAILS` 设置为1
## 启动阶段划分
App 冷启动可以划分为3大阶段
- 第一阶段:进程创建到 main 函数执行dyld、runtime
- 第二阶段main 函数到 `didFinishLaunchingWithOptions`
- 第三阶段:`didFinishLaunchingWithOptions` 到业务首页加载完成
这里说的阶段都是某一个步骤的最后一步。比如第一阶段的 main 函数执行的结束时刻
```shell
xnu_run () {
t1
    //...
}
main () {
// ..
// t2
}
```
## 第一阶段:进程创建到 main 函数执行dyld、runtime
![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/AppLaunchingTime.png)
这一阶段主要包括 dyld 和 Runtime 、main 函数的工作。
### dyld
iOS 的可执行文件都是 Mach-O 格式,所以 App 加载过程就是加载 Mach-O 文件的过程。
```c
struct mach_header_64 {
uint32_t magic; // 64位还是32位
cpu_type_t cputype; // CPU 类型,比如 arm 或 X86
cpu_subtype_t cpusubtype; // CPU 子类型,比如 armv8
uint32_t filetype; // 文件类型
uint32_t ncmds; // load commands 的数量
uint32_t sizeofcmds; // load commands 大小
uint32_t flags; // 标签
uint32_t reserved; // 保留字段
};
```
加载 Mach-O 文件,内核会先 fork 进程,并为进程分配虚拟内存、为进程创建主线程、代码签名等,用户态 dyld 会对 Mach-O 文件做库加载和符号解析。
细节可以查看代码,在 xnu 的 `kern_exec.c`
```c
int __mac_execve(proc_t p, struct __mac_execve_args *uap, int32_t *retval) {
// 字段设置
...
int is_64 = IS_64BIT_PROCESS(p);
struct vfs_context context;
struct uthread *uthread; // 线程
task_t new_task = NULL; // Mach Task
...
context.vc_thread = current_thread();
context.vc_ucred = kauth_cred_proc_ref(p);
// 分配大块内存,不用堆栈是因为 Mach-O 结构很大。
MALLOC(bufp, char *, (sizeof(*imgp) + sizeof(*vap) + sizeof(*origvap)), M_TEMP, M_WAITOK | M_ZERO);
imgp = (struct image_params *) bufp;
// 初始化 imgp 结构里的公共数据
...
uthread = get_bsdthread_info(current_thread());
if (uthread->uu_flag & UT_VFORK) {
imgp->ip_flags |= IMGPF_VFORK_EXEC;
in_vfexec = TRUE;
} else {
// 程序如果是启动态,就需要 fork 新进程
imgp->ip_flags |= IMGPF_EXEC;
// fork 进程
imgp->ip_new_thread = fork_create_child(current_task(),
NULL, p, FALSE, p->p_flag & P_LP64, TRUE);
// 异常处理
...
new_task = get_threadtask(imgp->ip_new_thread);
context.vc_thread = imgp->ip_new_thread;
}
// 加载解析 Mach-O
error = exec_activate_image(imgp);
if (imgp->ip_new_thread != NULL) {
new_task = get_threadtask(imgp->ip_new_thread);
}
if (!error && !in_vfexec) {
p = proc_exec_switch_task(p, current_task(), new_task, imgp->ip_new_thread);
should_release_proc_ref = TRUE;
}
kauth_cred_unref(&context.vc_ucred);
if (!error) {
task_bank_init(get_threadtask(imgp->ip_new_thread));
proc_transend(p, 0);
thread_affinity_exec(current_thread());
// 继承进程处理
if (!in_vfexec) {
proc_inherit_task_role(get_threadtask(imgp->ip_new_thread), current_task());
}
// 设置进程的主线程
thread_t main_thread = imgp->ip_new_thread;
task_set_main_thread_qos(new_task, main_thread);
}
...
}
```
Mach-O 文件非常大,系统为 Mach-O 分配一块大内存 imgp接下来会初始化 imgp 里的公共数据。内存处理完__mac_execve 会通过 fork_create_child 函数 for 出一个新的进程,然后通过 `exec_activate_image` 函数解析加载 Mach-O 文件到内存 imgp 中。最后调用 `task_set_main_thread_qos` 设置新 fork 出来进程的主线程。
```c
struct execsw {
int (*ex_imgact)(struct image_params *);
const char *ex_name;
} execsw[] = {
{ exec_mach_imgact, "Mach-o Binary" },
{ exec_fat_imgact, "Fat Binary" },
{ exec_shell_imgact, "Interpreter Script" },
{ NULL, NULL}
};
```
可以看到 Mach-O 文件解析使用 `exec_mach_imgact` 函数。该函数内部调用 `load_machfile` 来加载 Mach-O 文件,解析 Mach-O 文件后得到 load command 信息,通过映射方式加载到内存中。`activate_exec_state()` 函数处理解析加载 Mach-O 后的结构信息,设置执行 App 的入口点。
之后会通过 `load_dylinker()` 函数来解析加载 dyld然后将入口地址改为 dyld 入口地址。至此,内核部分就完成 Mach-O 文件的加载,剩下的就是用户态 dyld 加载 App 了。
dyld 入口函数为 `_dyld_start`dyld 属于用户态进程,不在 xnu 中,具体实现可以查看 [dyld/dyldStartup.s at master · opensource-apple/dyld · GitHub](https://github.com/opensource-apple/dyld/blob/master/src/dyldStartup.s)`_dyld_start` 会加载 App 动态库,处理完成后会返回 App 的入口地址。然后执行 App 的 main 函数。
dylddynamic link editorApple的动态链接器可以用来装载 Mach-O 文件(可执行文件、动态库等)
启动 APP 时dyld 所做的事情有
- 装载 APP 的可执行文件,同时会递归加载所有依赖的动态库
- 当 dyld 把可执行文件、动态库都装载完毕后,会通知 Runtime 进行下一步的处理。
其中包括 ASLRrebase、bind。
QA这里的通知 Runtime 怎么理解?
查看 objc4 的源代码 `objc-os.mm` 文件中的 `_objc_init` 方法
```c
/***********************************************************************
* _objc_init
* Bootstrap initialization. Registers our image notifier with dyld.
* Called by libSystem BEFORE library initialization time
**********************************************************************/
void _objc_init(void){
static bool initialized = false;
if (initialized) return;
initialized = true;
// fixme defer initialization until an objc-using image is found?
environ_init();
tls_init();
static_init();
lock_init();
exception_init();
_dyld_objc_notify_register(&map_images, load_images, unmap_image);
}
```
方法注释说的很明白,被 dyld 所调用
### Runtime
启动 APP 时Runtime 所做的事情有
- 调用 `map_images` 进行可执行文件内容的解析和处理
-`load_images` 中调用 `call_load_methods`,调用所有 Class、Category 的 `+load`方法
- 进行各种 objc 结构的初始化(注册 Objc 类 、初始化类对象等等)
- 调用 C++ 静态初始化器和 `__attribute__((constructor))` 修饰的函数
到此为止,可执行文件和动态库中所有的符号(ClassProtocolSelectorIMP…)都已经按格式成功加载到内存中,被 Runtime 所管理
## 第二阶段main 函数到 didFinishLaunchingWithOptions
APP的启动由 dyld 主导将可执行文件加载到内存顺便加载所有依赖的动态库。并由Runtime 负责加载成 objc 定义的结构。所有初始化工作结束后dyld 就会调用 main 函数
接下来就是 `UIApplicationMain` 函数。main 函数内部其实没啥逻辑,可能会存在一些防止逆向相关的安全代码。这部分对启动耗时没啥影响,可以忽略先。
AppDelegate 的`application:didFinishLaunchingWithOptions:` 方法。此阶段主要是各种 SDK 的注册、初始化、APM 监控系统的启动、热修复 SDK 的设置、App 初始化数据的加载、IO 等等。
## 第三阶段:`didFinishLaunchingWithOptions` 到业务首页加载完成
这部分主要是首页渲染相关逻辑,不同的场景,首页的定义可能不一样。比如未登录的时候首页就是登陆页、否则就是某个主页
- 首屏数据的网络/DB IO 读取
- 渲染数据的计算
## 启动优化
### 第一阶段
#### dyld
- 减少动态库加载合并一些动态库。定期清理不必要的动态库iOS 规定开发者写的动态库不能超过6个
#### Runtime
-`+initialize` 方法和 `dispatch_once` 取代所有的 `__attribute__((constructor))`、C++静态构造器、ObjC 的 `+load`
- +load 方法中的代码可以监控等 App 启动完成后才去执行。或使用 + initialize 方法。一个 +load 方法中如果执行 hook 方法替换大约影响4ms。
- 减少 Objc 类、分类的数量、减少 selector 数量(定期清理不必要的类、分类)。推荐工具 fui。
- 减少 C++ 虚函数数量
- Swift 尽量使用 struct
- 控制 C++ 的全局变量的数据
### 第二阶段
- 在不影响用户体验的前提下,尽可能将一些操作延迟,不要全部都放在 `application:didFinishLaunchingWithOptions` 方法中
- SDK 初始化遵循规范
- 任务启动器
- 二进制重排
- 方法耗时统计time profiler、os_signpost
AppDelegate 中 `application:didFinishLaunchingWithOptions` 函数阶段一般有很多业务代码介入,大多数启动时间问题都是在此阶段造成的。
- 比如二方、三方 SDK 的注册、初始化。这其中有些 SDK 比如 APM 是有必要放在很早的阶段。有些则不需要,比如日志 SDK 的初始化
### 第三阶段
很多时候,需要梳理出那些功能是首屏渲染所需要的初始化功能,那些是非首页需要的功能,按照业务场景梳理并治理。
QA
静态库、动态库?
静态库:.o文件集合。静态库编译、链接后就不存在了变为可执行文件了
动态库:一个已经链接完全的镜像。已经被静态链接过
动态库不可以变为静态库。静态库可以变为动态库。
静态库缺点:产物体积比较大,影响包大小(大)。链接到 App 之后App 体积会比较小(??)静态库 strip
动态库缺点:除了系统动态库之外,没有真正意义上的动态库(不会放到系统的共享缓冲区)
适用场景:
静态库不影响启动时间、动态库代码保密性好。
Framework 只是打包方式,动态、静态库都可以支持。甚至可以存放资源文件。
## 二进制重排
### 虚拟内存、物理内存、内存分页
早期计算机内部都是物理内存。物理内存一个问题就是安全问题,通过地址访问到别的应用程序的数据。进程如果能直接访问物理内存无疑是很不安全的,(诞生了解决方案:虚拟内存)所以操作系统在物理内存的上又建立了一层虚拟内存。
一个应用程序在使用的时候并不是全部使用的,往往是使用了一个应用程序的某个或者某几个功能。
所以早期工程师的第一个解决方案是将一个应用程序分块,按需加载功能模块的内存地址数据。
虚拟内存是间接访问了内存条。
内存分页iOS 一页就是16KB。
物理内存如果满了,则将最活跃的内存数据,覆盖掉,最不活跃的内存数据;
ASLR为了安全问题诞生。
自己的代码中有 NSLog 代码,可是 NSLog 函数的代码实现在 Foundation 动态库中。
App 启动则用 dyld 去加载库,共享缓存库。
虚拟地址:偏移是编译后就能确定的。
内存缺页异常:在使用中,访问虚拟内存的一个 page 而对应的物理内存缺不存在(没有被加载到物理内存中),则发生缺页异常。影响耗时,在几毫秒之内。
什么时候发生大量的缺页异常?一个应用程序刚启动的时候。
启动时所需要的代码分布在第一页、第二页、第三页...第200页。这样的情况下启动时间会影响较大所以解决思路就是将应用程序启动刻所需要的代码二进制优化一下统一放到某几页这样就可以避免内存缺页异常则优化了 App 启动时间。
dylib loading time
rebase/binding time 修正符号是由于 ASLR 导致。binding time 是动态库去 bind lazy table、non-lazy table 所占用的时间。rebase 地址偏移ASLR。 rebase 的时间如何缩小Mach-O 文件大小变小。 binding time 变小则需要动态库变小。2者优化手段冲突
Objc setup timeSwift 这部分占优势
initializer timeload 方法耗时。
slowest intializers
libS
libMain
查看 LinkMap。发现方法展示顺序是按照写代码的顺序展示的。
![image-20200814211215976](/Users/lbp/Library/Application Support/typora-user-images/image-20200814211215976.png)
### 有没有办法将 App 启动需要的方法集中收拢?
1. 在 Xcode 的 Build Settings 中设置 **Order File**Write Link Map Files 设置为 YES进行观察
2. 如果你给 Xcode 工程根目录下指定一个 order 文件,比如 `refine.order`,则 Xcode 会按照指定的文件顺序进行二进制数据重排。分析 App 启动阶段,将优先需要加载的函数、方法,集中合并,利用 Order File减小缺页异常从而减小启动时间。
### 如何拿到启动时刻所调用的所有方法名称
clang 插桩,才可以 hook OC、C、block、Swift 全部。LLVM 官方文档。
二进制重排提升 App 启动速度是通过「解决内存缺页异常」(内存缺页会有几毫秒的耗时)来提速的。
一个 App 发生大量「内存缺页」的时机就是 App 刚启动的时候。所以优化手段就是「将影响 App 启动的方法集中处理放到某一页或者某几页」虚拟内存中的页。Xcode 工程允许开发者指定 「Order File」可以「按照文件中的方法顺序去加载」可以查看 linkMap 文件(需要在 Xcode 中的 「Buiild Settings」中设置 Order File、Write Link Map Files 参数)。
其实难点是如何拿到启动时刻所调用的所用方法?代码可能是 Swift、block、c、OC所以hook 肯定不行、fishhook 也不行,用 clang 插桩可行
- https://mp.weixin.qq.com/s/SUHaGD1T2Vce4Ag-qgxtgg
在看 App 启动时间优化之前先看2个方法 **load****initialize**
load
> Invoked whenever a class or category is added to the Objective-C runtime; implement this method to perform class-specific behavior upon loading.
当一个类或者它的分类被加载到 Objective-c 的 runtime 中的时候会出发 load 方法。可以实现这个方法去执行一些类特定的行为。
> 当一个类或者它的分类被加载到 Objective-c 的 runtime 中的时候会出发 load 方法。可以实现这个方法去执行一些类特定的行为。
initialize
> Initializes the class before it receives its first message.
当一个类接收到第一条消息的时候会初始化。
> 当一个类接收到第一条消息的时候会初始化。
load 方法会在类被加载到 runtime 的时候调用。且父类的 load 方法比子类先执行。load 方法只会执行1次。
initialize 方法会在第一次收到消息的时候调用。父类的 initialize 方法比子类先执行。假如有 Person 类,还有一个子类 children。子类第一次收到消息的时候会先调用父类的 initialize然后调用子类的 initialize如果子类没有实现 initialize 那么父类的 initialize 会执行多次。

View File

@@ -1,6 +1,4 @@
# 对象在内存中的存储
# 对象在内存中的存储底层原理
## 一、 栈、堆、BSS、数据段、代码段是什么
@@ -16,9 +14,6 @@ BSS段bss segment通常用来存储程序中未被初始化的全局变
![内存](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/ram.png)
## 二、研究下对象在内存中如何存储?
```Objective-C
@@ -28,22 +23,23 @@ Person *p1 = [Person new]
看这行代码,先来看几个注意点:
new底层做的事情
* 在堆内存中申请1块合适大小的空间
* 在这块内存上根据类模版创建对象。类模版中定义了什么属性就依次把这些属性声明在对象中;对象中还存在一个属性叫做**isa**,是一个指针,指向对象所属的类在代码段中地址
* 初始化对象的属性。这里初始化有几个原则a、如果属性的数据类型是基本数据类型则赋值为0b、如果属性的数据类型是C语言的指针类型则赋值为NULLc、如果属性的数据类型为OC的指针类型则赋值为nil。
* 返回堆空间上对象的地址
* 在堆内存中申请1块合适大小的空间
* 在这块内存上根据类模版创建对象。类模版中定义了什么属性就依次把这些属性声明在对象中;对象中还存在一个属性叫做**isa**,是一个指针,指向对象所属的类在代码段中地址
* 初始化对象的属性。这里初始化有几个原则a、如果属性的数据类型是基本数据类型则赋值为0b、如果属性的数据类型是C语言的指针类型则赋值为NULLc、如果属性的数据类型为OC的指针类型则赋值为nil。
* 返回堆空间上对象的地址
注意:
* 对象只有属性没有方法。包括类本身的属性和一个指向代码段中的类isa指针
* 如何访问对象的属性?指针名-&gt;属性名;本质:根据指针名找到指针指向的对象,再根据属性名查找来访问对象的属性值
* 如何调用方法?[指针名 方法];本质根据指针名找到指针指向的对象再发现对象需要调用方法再通过对象的isa指针找到代码段中的类再调用类里面方法
* 对象只有属性没有方法。包括类本身的属性和一个指向代码段中的类isa指针
* 如何访问对象的属性?指针名-&gt;属性名;本质:根据指针名找到指针指向的对象,再根据属性名查找来访问对象的属性值
* 如何调用方法?[指针名 方法];本质根据指针名找到指针指向的对象再发现对象需要调用方法再通过对象的isa指针找到代码段中的类再调用类里面方法
为什么不把方法存储在对象中?
* 因为以类为模版创建的对象只有属性可能不相同,而方法相同,如果堆区的对象里面也保存方法的话就会很重复,浪费了堆空间,因此将方法存储与代码段
* 因为以类为模版创建的对象只有属性可能不相同,而方法相同,如果堆区的对象里面也保存方法的话就会很重复,浪费了堆空间,因此将方法存储与代码段
* 所以一个类创建的n个对象的isa指针的地址值都相同都指向代码段中的类地址
* 所以一个类创建的n个对象的isa指针的地址值都相同都指向代码段中的类地址
做个小实验
@@ -92,3 +88,451 @@ int main(int argc, const char * argv[]) {
![p2](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2017-05-15%20下午5.35.34.png)
**可以 看到Person类的3个对象p1、p2、p3的isa的值相同。**
## 三、一个对象占用多少内存空间?
```objectivec
size_t class_getInstanceSize(Class cls)
{
if (!cls) return 0;
return cls->alignedInstanceSize();
}
// Class's ivar size rounded up to a pointer-size boundary.
uint32_t alignedInstanceSize() {
return word_align(unalignedInstanceSize());
}
```
```objectivec
id class_createInstance(Class cls, size_t extraBytes)
{
return _class_createInstanceFromZone(cls, extraBytes, nil);
}
static __attribute__((always_inline))
id
_class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone,
bool cxxConstruct = true,
size_t *outAllocatedSize = nil)
{
if (!cls) return nil;
assert(cls->isRealized());
// Read class's info bits all at once for performance
bool hasCxxCtor = cls->hasCxxCtor();
bool hasCxxDtor = cls->hasCxxDtor();
bool fast = cls->canAllocNonpointer();
size_t size = cls->instanceSize(extraBytes);
if (outAllocatedSize) *outAllocatedSize = size;
id obj;
if (!zone && fast) {
obj = (id)calloc(1, size);
if (!obj) return nil;
obj->initInstanceIsa(cls, hasCxxDtor);
}
else {
if (zone) {
obj = (id)malloc_zone_calloc ((malloc_zone_t *)zone, 1, size);
} else {
obj = (id)calloc(1, size);
}
if (!obj) return nil;
// Use raw pointer isa on the assumption that they might be
// doing something weird with the zone or RR.
obj->initIsa(cls);
}
if (cxxConstruct && hasCxxCtor) {
obj = _objc_constructOrFree(obj, cls);
}
return obj;
}
size_t instanceSize(size_t extraBytes) {
size_t size = alignedInstanceSize() + extraBytes;
// CF requires all objects be at least 16 bytes.
if (size < 16) size = 16;
return size;
}
```
用2种方式获取
- class_getInstanceSize([NSObject class]):8。返回实例对象的成员变量所占用的内存大小一个空对象只有 isa 指针所以只有8字节
- malloc_size((__bridge const void *)obj):16。Apple 规定对象至少16个字节。但是只有一个 isa所以只占用8个字节。
内存对齐:结构体的最终大小必须是最大成员的倍数。比如
```c
struct Person_IMPL {
struct NSObject_IMPL NSObject_IVARS; // 8字节
int age; // 4字节
};
```
8*2=16字节
## 四、类继承的本质
写一个最基础的类
```objectivec
@interface Person:NSObject
@end
@implementation Person
@end
```
clang 转为 c 代码看看, `xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main.cpp`
```c
struct NSObject_IMPL {
Class isa;
}
struct Person_IMPL {
struct NSObject_IMPL NSObject_IVARS;
};
```
如果创建一个继承自 Person 的 Student 类呢
```c
struct Student_IMPL {
struct Person_IMPL Person_IVARS;
NSInteger _age;
NSString *_name;
};
```
首先我们可以知道一个 OC 类的属性可以写多种数据类型,那么大概率是 C 结构体实现。用 clang 转为 c 可以得到印证。另外存在类的继承关系的时候,子类结构体中第一个信息是父类结构体对象。其次是当前子类自己的信息。根节点一定是 NSObject_IMPL 结构体,且其中只有 `Class isa`
观察 clang 转换后的 c 代码,发现 property 没有看到 setter、getter 方法。为什么这么设计?
方法不会存储到实例对象中去的。因为方法在各个对象中是通用的,方法存储在类对象的方法列表中。
```objectivec
@interface Person:NSObject
{
int _height;
int _age;
int _weight;
}
@end
@implementation Person
@end
struct NSObject_IMPL {
Class isa;
};
struct PersonIMPL {
struct NSObject_IMPL ivars;
int _height;
int _age;
int _weight;
};
struct PersonIMPL person = {};
Person *p = [[Person alloc] init];
NSLog(@"%zd", class_getInstanceSize([Person class])); // 24这个数值代表我们这个类这个结构体创建出来至少只需要24字节.
NSLog(@"%zd", sizeof(person)); // 24这个数值代表我们这个类这个结构体只需要24字节就够
NSLog(@"%zd", malloc_size((__bridge const void *)p)); // 32。iOS 系统会做优化比如为了加速访问速度会按照16的倍数进行分配。
```
`class_getInstanceSize`这个数值代表我们这个类这个结构体创建出来至少只需要24字节. `malloc_size` iOS 系统会做优化比如为了加速访问速度会按照16的倍数进行分配。
iOS 中系统分配内存都是16的倍数。pageSize系统在分配内存的时候也存在内存对齐。
GUN 都存在内存对齐这个概念。
sizeof 是运算符。
实例对象:
类对象isa、superclass、属性信息、对象方法信息、协议信息、成员变量信息...
元类对象:存储 isa、superclass、类方法信息...
一个实例对象只有一个类对象,一个实例对象只有一个元类对象。 `class_isMetaClass()`判断一个类是否为元类对象
`objc_getClass()` 如果传递 instance 实例对象,返回 class 类对象;传递 Class 类对象,返回 meta-class 元类对象;传递 meta-class 元对象,则返回 NSObject(基类)的 meta-class 对象
![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/objc-isa.png)
instance 的 isa 指向 Class。当调用方法时通过 instance 的 isa 找到 Class最后找到对象方法的实现进行调用
class 的 isa 指向 meta-class。当调用类方法的时通过 class 的 isa 找到 meta-class最后找到类方法进行调用。
![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/objc-superclass.png)
当 Student 实例对象调用 Person 的对象方法时,首先根据 Student 对象的 isa 找到 Stduent 的 Class 类对象,然后根据 Stduent Class 类对象中的 superClass 找到 Person 的 Class 类对象,在类对象的对象方法列表中找到方法实现并调用。
当 Stduent 实例对象调用 init 方法时候,首先根据 Student 对象的 isa 找到 Stduent 的 Class 类对象,然后根据 Stduent Class 类对象中的 superClass 找到 Person 的 Class 类对象,找到 Person Claas 的 superClass 到 NSObject 类对象,在 NSObject 类对象的方法列表中找到 `init` 方法并调用。
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/objc-metaclass-superclass.png)
当 Stduent 对象调用类方法的时候,先根据 isa 找到 Student 的元类对象,然后在元类对象的 superclass 找到 Person 的元类对象,再根据 Person 元类对象的 superClass 找到 NSObject 的元类对象。最后找到元类对象的方法列表,调用到对象方法。
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/class-isa-superclass.png)
```objectivec
@interface Student : NSObject
@end
@implementation Person
@end
@interface NSObject(TestMessage)
@end
@implementation NSObject(TestMessage)
- (void)test
{
NSLog(@"%s", __func__);
}
@end
```
奇怪的是,我们给 Student 类对象调用 test 方法,`[Student test]` 则调用成功。是不是很奇怪站在面向对象的角度出发Student 类对象根据 isa 找到元对象,此时元对象方法列表中没有类对象,所以根据 superclass 找到 NSObject 元类对象,发现元类对象自身也没有类方法。但是为什么调用了 `-(void)test` 对象方法?
因为NSObject 元类对象的 superClass 继承自 NSObject 的类对象,类对象是存储对象方法的,所以定义在 NSObject 分类中的 `-(void)test` 最终会被调用。
从64位开始iOS 的 isa 需要与 ISA_MASK 进行与位运算。& ISA_MASK 才可以得到真正的类对象地址。
为了打印和研究类对象中的 superclass、isa
```objectivec
// 实例对象
Person *p = [[Person alloc] init];
Student *s = [[Student alloc] init];
// 类对象
class pclass = object_getClass(p);
class sclass = object_getClass(s);
// Mock 系统结构体 object_class
struct mock_object_class {
class isa;
class superclass;
};
// 转换如下
struct mock_object_class *person = (__bridge mock_object_class *)[[Person alloc] init];
struct mock_object_class *student = (__bridge mock_object_class *)[[Student alloc] init];
```
如何查看类真正的结构?在 Xcode 中打印出来
思路:查看 Class 内部的数据,发现是 struct所以我们自己定义一个 struct去承接类对象的元类对象信息
```c
#import <Foundation/Foundation.h>
#ifndef MockClassInfo_h
#define MockClassInfo_h
# if __arm64__
# define ISA_MASK 0x0000000ffffffff8ULL
# elif __x86_64__
# define ISA_MASK 0x00007ffffffffff8ULL
# endif
#if __LP64__
typedef uint32_t mask_t;
#else
typedef uint16_t mask_t;
#endif
typedef uintptr_t cache_key_t;
struct bucket_t {
cache_key_t _key;
IMP _imp;
};
struct cache_t {
bucket_t *_buckets;
mask_t _mask;
mask_t _occupied;
};
struct entsize_list_tt {
uint32_t entsizeAndFlags;
uint32_t count;
};
struct method_t {
SEL name;
const char *types;
IMP imp;
};
struct method_list_t : entsize_list_tt {
method_t first;
};
struct ivar_t {
int32_t *offset;
const char *name;
const char *type;
uint32_t alignment_raw;
uint32_t size;
};
struct ivar_list_t : entsize_list_tt {
ivar_t first;
};
struct property_t {
const char *name;
const char *attributes;
};
struct property_list_t : entsize_list_tt {
property_t first;
};
struct chained_property_list {
chained_property_list *next;
uint32_t count;
property_t list[0];
};
typedef uintptr_t protocol_ref_t;
struct protocol_list_t {
uintptr_t count;
protocol_ref_t list[0];
};
struct class_ro_t {
uint32_t flags;
uint32_t instanceStart;
uint32_t instanceSize; // instance对象占用的内存空间
#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;
};
struct class_rw_t {
uint32_t flags;
uint32_t version;
const class_ro_t *ro;
method_list_t * methods; // 方法列表
property_list_t *properties; // 属性列表
const protocol_list_t * protocols; // 协议列表
Class firstSubclass;
Class nextSiblingClass;
char *demangledName;
};
#define FAST_DATA_MASK 0x00007ffffffffff8UL
struct class_data_bits_t {
uintptr_t bits;
public:
class_rw_t* data() {
return (class_rw_t *)(bits & FAST_DATA_MASK);
}
};
/* OC对象 */
struct mock_objc_object {
void *isa;
};
/* 类对象 */
struct mock_objc_class : mock_objc_object {
Class superclass;
cache_t cache;
class_data_bits_t bits;
public:
class_rw_t* data() {
return bits.data();
}
mock_objc_class* metaClass() {
return (mock_objc_class *)((long long)isa & ISA_MASK);
}
};
#endif /* MockClassInfo_h */
Student *stu = [[Student alloc] init];
stu->_weight = 10;
mock_objc_class *studentClass = (__bridge mock_objc_class *)([Student class]);
mock_objc_class *personClass = (__bridge mock_objc_class *)([Person class]);
class_rw_t *studentClassData = studentClass->data();
class_rw_t *personClassData = personClass->data();
class_rw_t *studentMetaClassData = studentClass->metaClass()->data();
class_rw_t *personMetaClassData = personClass->metaClass()->data();
```
## 五、 内存对齐
Demo1
```objectivec
@interface Person : NSObject
{
int _age;
int _height;
}
@end
struct Person_IMPL {
Class isa;
int _age;
int _height;
};
Person *person = [[Person alloc] init];
NSLog(@"%zd", malloc_size((__bridge const void *)person)); // 16
NSLog(@"%zd", sizeof(struct Person_IMPL)); // 16
```
isa 指针8字节 + int _age 4字节 + _hright 字节 = 16 字节
Demo2
```objectivec
@interface Person : NSObject
{
int _age;
int _height;
int _no;
}
@end
struct Person_IMPL {
Class isa;
int _age;
int _height;
int _no;
};
Person *person = [[Person alloc] init];
NSLog(@"%zd", malloc_size((__bridge const void *)person)); // 32
NSLog(@"%zd", sizeof(struct Person_IMPL)); // 24
```
isa 指针8字节 + int _age 4字节 + _hright 字节 + _no 4 字节 = 20 字节因为存在内存对齐因为结构体本身对齐内存对齐必须为8的倍数所以占据24个字节的内存。
结构体占据24字节为什么运行起来后通过 `malloc_size` 得到32个字节这个涉及到运行时内存对齐。规定 **iOS 中内存对齐以 16 的倍数为准**。
Demo
```objectivec
void *temp = malloc(4);
NSLog(@"%zd", malloc_size(temp));
// 16
```
可以看到 malloc 申请了4个字节但是打印却看到16个字节。
查看源码也可以出来分配内存最小是以16的倍数为基准进行分配的。
```c
#define NANO_MAX_SIZE 256 /* Buckets sized {16, 32, 48, 64, 80, 96, 112, ...} */
```

View File

@@ -58,7 +58,7 @@ _displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(p_di
![卡顿原因](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-02-04-ios_frame_drop.png)
VSync 信号到来后,系统图形服务会通过 CADisplayLink 等机制通知 AppApp 主线程开始在 CPU 中计算显示内容(视图创建、布局计算、图片解码、文本绘制等)。然后将计算的内容提交到 GPUGPU 经过图层的变换、合成、渲染,随后 GPU 把渲染结果提交到帧缓冲区,等待下一次 VSync 信号到来再显示之前渲染好的结果。在垂直同步机制的情况下,如果在一个 VSync 时间周期内CPU 或者 GPU 没有完成内容的提交,就会造成该帧的丢弃,等待下一次机会再显示,这时候屏幕上还是之前渲染的图像,所以这就是 CPU、GPU 层面界面卡顿的原因。
VSync 信号到来后,系统图形服务会通过 CADisplayLink 等机制通知 AppApp 主线程开始在 CPU 中计算显示内容(视图创建、布局计算、图片解码、文本绘制等)。然后将计算的内容提交到 GPUGPU 经过图层的变换、合成、渲染,随后 GPU 把渲染结果提交到帧缓冲区,等待下一次 VSync 信号到来再显示之前渲染好的结果。在垂直同步机制的情况下,如果在一个 VSync 时间周期内CPU 或者 GPU 没有完成内容的提交,就会造成该帧的丢弃(也叫掉帧),等待下一次机会再显示,这时候屏幕上还是之前渲染的图像,所以这就是 CPU、GPU 层面界面卡顿的原因。
目前 iOS 设备有双缓存机制也有三缓冲机制Android 现在主流是三缓冲机制,在早期是单缓冲机制。
[iOS 三缓冲机制例子](https://ios.developreference.com/article/12261072/Metal+newBufferWithBytes+usage)
@@ -553,8 +553,6 @@ curNode.costTime > (parentNode.costTime * 1.1/n) && curNode.costTime > parentNod
这样情况下,业务方可以针对特定的业务流程做性能统计分析
## 二、 App 启动时间监控
### 1. App 启动时间的监控
@@ -773,6 +771,8 @@ for (int i = 0; i < threadCount; i++) {
## 四、 OOM 问题
大多数情况下 OOM 问题比 crash 问题更严重,线上稳定性问题主要是由于 OOM 造成的,因为线下可以利用 Xcode 的一些工具解决并定位 crash。线上也有类似 KSCrash 这样的优秀监控工具。但是 OOM 方面线下只有一些三方的工具,这些工具大多是基于 OC 对象引用关心实现的,所以只能判断 OC 对象。另外比较耗费性能,所以线上 OOM 问题还是比较多的。
### 1. 基础知识准备
硬盘:也叫做磁盘,用于存储数据。你存储的歌曲、图片、视频都是在硬盘里。
@@ -1624,6 +1624,51 @@ for 循环打印出每个进程(也就是 App的 pid、Priority、User Data
### 4. 如何判定发生了 OOM
首先我们无法通过正常手段监控 OOM因为 XNU 源码显示发生 OOM 发送的信号为 SIGKILL该信号无法监控。
```c
/*
* The jetsam no frills kill call
* Return: 0 on success
* error code on failure (EINVAL...)
*/
static int jetsam_do_kill(proc_t p, int jetsam_flags, os_reason_t jetsam_reason) {
int error = 0;
error = exit_with_reason(p, W_EXITCODE(0, SIGKILL), (int *)NULL, FALSE, FALSE, jetsam_flags, jetsam_reason);
return error;
}
```
FacekBook 提出排除法监控 OOM。
![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/Facebook-OOM.jpeg)
- App 更新了版本
- App 发生了崩溃
- 用户手动退出
- 操作系统更新了版本
- App 切换到后台之后进程终止
其实不够全,存在误判,增加下面几种 case
- 覆盖安装
- WatchDog 崩溃
- 后台启动
- XCTest/UITest 等自动化测试框架驱动
- 应用 exit 主动退出
OOM 导致 crash 前app 一定会收到低内存警告吗?
做 2 组对比实验:
@@ -2793,49 +2838,47 @@ CFNetwork 使用 CFReadStreamRef 来传递数据,使用回调函数的形式
}
@end
```
@implementation NetworkDelegateProxy
#pragma mark - life cycle
+ (instancetype)sharedInstance {
static NetworkDelegateProxy *_sharedInstance = nil;
```
- (instancetype)sharedInstance {
static NetworkDelegateProxy *_sharedInstance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_sharedInstance = [NetworkDelegateProxy alloc];
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_sharedInstance = [NetworkDelegateProxy alloc];
});
return _sharedInstance;
}
#pragma mark - public Method
+ (instancetype)setProxyForObject:(id)originalTarget withNewDelegate:(id)newDelegate
{
NetworkDelegateProxy *instance = [NetworkDelegateProxy sharedInstance];
instance->_originalTarget = originalTarget;
instance->_NewDelegate = newDelegate;
return instance;
- (instancetype)setProxyForObject:(id)originalTarget withNewDelegate:(id)newDelegate {
NetworkDelegateProxy *instance = [NetworkDelegateProxy sharedInstance];
instance->_originalTarget = originalTarget;
instance->_NewDelegate = newDelegate;
return instance;
}
- (void)forwardInvocation:(NSInvocation *)invocation
{
if ([_originalTarget respondsToSelector:invocation.selector]) {
- (void)forwardInvocation:(NSInvocation *)invocation {
if ([_originalTarget respondsToSelector:invocation.selector]) {
[invocation invokeWithTarget:_originalTarget];
[((NSURLSessionAndConnectionImplementor *)_NewDelegate) invoke:invocation];
[((NSURLSessionAndConnectionImplementor *)_NewDelegate) invoke:invocation];
}
}
- (nullable NSMethodSignature *)methodSignatureForSelector:(SEL)sel
{
return [_originalTarget methodSignatureForSelector:sel];
- (nullable NSMethodSignature *)methodSignatureForSelector:(SEL)sel{
return [_originalTarget methodSignatureForSelector:sel];
}
@end
```
@@ -6139,7 +6182,7 @@ file_names[ 4]:
<起始地址> <结束地址> <函数> [<文件名:行号>]
```
#### 4.4 **如何获取地址**
#### 4.4 如何获取地址
image 加载的时候会进行相对基地址进行重定位,并且每次加载的基地址都不一样,函数栈 frame 的地址是重定位后的绝对地址,我们要的是重定位前的相对地址。
@@ -6358,21 +6401,21 @@ Crash log 统一入库 Kibana 时是没有符号化的,所以需要符号化
Weex 由于历史原因,不做性能监控了,现有业务代码继续跑着,只业务问题和稳定性问题。
## Weex 异常监控
### Weex 异常监控
### 背景
#### 背景
Weex 由于历史原因,不能很好的统计异常。比如在页面的模版代码中绑定了 data 中的一个对象,此时对象可能并没有值,而是依赖后续的网络请求完成,对象才有了具体的值 data 改变,数据驱动,页面再次 render。所以监控代码会认为第一次 render 的时候访问对象不存在的属性。
真正有问题的代码和不影响业务的异常信息,都会被 Vue 官方认为是一场。基于这样的背景,我们无法 pick 出真正异常或者是开发者判空代码没写好的问题。
### 解决思路
#### 解决思路
按照异常的等级,可以划分为影响业务和不影响业务。问题来了,什么叫做“影响业务”?这是我们自己定义的词,也就是影响用户是否正常操作 App。比如说页面白屏、点击某个按钮无响应等等。定义为 Error。其他不影响业务的定义为 Warning。
### 技术实现
#### 技术实现
#### Warning 异常
##### Warning 异常
采用主流方案React、Vue 都提供了框架自己的异常监控方案,由于 Weex 是在 Vue 基础上实现。包括 Native 和 Vue 的 UI 双线程机制、事件机制等等。其余我们不关心Weex 开发中,开发和都是写 Vue 去实现页面和逻辑,所以我们监控 Vue 的异常就满足了。
@@ -6417,7 +6460,7 @@ Weex 由于历史原因,不能很好的统计异常。比如在页面的模版
componentName = this.fetchComponentName(vm)
componentPath = this.fetchComponentPath(vm)
}
let errorInfo = {
name: err.name,
reason: err.message,
@@ -6442,13 +6485,13 @@ export default APM;
由于 Weex 代码是单独可运行和部署的,因此前端没有统一的入口,所以在发布阶段监控代码需要配合打包机,利用脚本动态插入到页面代码中。
#### Error 监控
##### Error 监控
根据我们对“影响业务”的定义Error 包括:页面白屏 + 事件无法响应。
所以我们来拆解下问题。
##### 页面白屏
###### 页面白屏
根据 Weex SDK 整个完整流程得出,白屏包括以下几个可能:
@@ -6465,14 +6508,13 @@ export default APM;
return;
}
```
- Weex 资源网络请求成功,但是数据内容为空
```objectivec
_mainBundleLoader.onFinished = ^(WXResourceResponse *response, NSData *data) {
// ...
if (!data) {
// Weex 资源网络请求成功,但是数据内容为空。也就是下载下来的资源无法消费,页面无法正常渲染
NSString *errorMessage = [NSString stringWithFormat:@"Request to %@ With no data return", WeexSafeString(request.URL)];
@@ -6484,14 +6526,13 @@ export default APM;
}
}
```
- Weex 资源网络请求成功,但是数据 JSON 序列化失败
```objectivec
_mainBundleLoader.onFinished = ^(WXResourceResponse *response, NSData *data) {
// ...
NSString *jsBundleString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
if (!jsBundleString) {
// Weex 资源网络请求成功,但是网络数据 JSON 序列化失败,也就是下载下来的资源无法消费,页面无法正常渲染
@@ -6503,8 +6544,7 @@ export default APM;
}
}
```
- Weex 资源网络请求异常(网络请求 _mainBundleLoader.onFailed
```objectivec
@@ -6518,10 +6558,8 @@ export default APM;
// ...
};
```
##### 点击事件无响应
###### 点击事件无响应
调试 WeexSDK 发现 SDK 内部通过给 JSContext 注册了 `callNativeModule` 方法来实现调用 Native Module 的 Method。
@@ -6555,7 +6593,7 @@ export default APM;
};
[[WeexAPM sharedInstance] reportError:errorDict];
} @finally {
}
}
}
@@ -6604,7 +6642,7 @@ export default APM;
}
```
#### JS ExceptionHandler
##### JS ExceptionHandler
JS 引擎中异常回调中上报错误
@@ -6636,6 +6674,7 @@ typedef NS_ENUM(NSUInteger, WeexAPMType) {
统计线上数据发现Weex 页面渲染失败率为4.09%,所以看上去页面渲染失败率还是蛮高的。目前发现失败率最高的场景为 App 启动时候按照功能模块组织的配置清单。
类似于下面的配置:
```
"//goods/detail": {
"configParams": "",
@@ -6648,18 +6687,15 @@ typedef NS_ENUM(NSUInteger, WeexAPMType) {
因此,针对于这样的场景。我们希望针对 Weex 资源的拉取和访问机制做一些优化。
目前有几个流程不太合理:
1. 当前启动时的js 预载入流程里的JS下载失败 和 进入Weex页面后的JS拉取失败 两处的上报失败未做区分没法实际统计加载页面时有多少JS获取失败的情况。
2. 随着业务迭代, Weex 页面越来越多,需要做 JS 拉取相关优化,避免白屏问题
3. 目前 JS 缓存逻辑为,每个 Weex 模块单独维护一个 LRUCache并且会做大量的预加载但可能大多数页面根本不会访问到。浪费了内存资源去做了一些不会被调用到的页面预加载可能还会造成 OOM 问题
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/WeexResourcePull.png)
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/WeexResourcePull.png)
// todo? 参考前端资源模块化,如何实现资源预加载(全局 LRU或者基于用户行为路径的预热功能比如某个收银员角色固定的情况下他日常的操作行为是固定的商品扫码、开单
### 2. Flutter 异常监控
#### 2. Flutter 异常监控
## 九、子线程 UI 监控
@@ -6697,8 +6733,6 @@ typedef NS_ENUM(NSUInteger, WeexAPMType) {
具体可以参考这个 [Demo](https://github.com/FantasticLBP/MainThreadChecker)
## 十、页面渲染时长统计
当我们的产品经理、TL、领导或者任何关心我们产品质量的某个角色问你你们的 App 看上去好像比较卡,很难用,这时候我们心里一阵空虚,卡吗?
@@ -6747,10 +6781,8 @@ typedef NS_ENUM(NSUInteger, WeexAPMType) {
另外随着问题的暴露或者技术研究的渗入,监控方案本身是可以迭代演进的。
1. 8060 会有特例比如8060满足了且此时主线程空了。因为某个 ImageView 在子线程上根据 URL 异步请求资源,之后会再次触发渲染。所以这个情况下,方案还是存在问题的
## 十一、 APM 小结
1. 通常来说各个端的监控能力是不太一致的,技术实现细节也不统一。所以在技术方案评审的时候需要将监控能力对齐统一。每个能力在各个端的数据字段必须对齐(字段个数、名称、数据类型和精度),因为 APM 本身是一个闭环,监控了之后需符号化解析、数据整理,进行产品化开发、最后需要监控大盘展示等

View File

@@ -24,7 +24,7 @@ int main(int argc, const char * argv[]) {
在 foo 方法里面下断点,见下图
![rename symbol](./../assets/2020-02-25-asm.png)
![rename symbol](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-02-25-asm.png)
可以看到,`foo` 方法的 symbol 被变为 `@杭城小刘`,变量 `age` 被变为 `objc_age`
@@ -43,7 +43,7 @@ int main(int argc, char * argv[]) {
}
```
![App main 方法 rename 失败](./../assets/2020-02-25-asm2.png)
![App main 方法 rename 失败](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-02-25-asm2.png)
可以看到 App 工程主入口函数 `main` 函数,想修改为 `mook_main`。但是报错 `ld: entry point (_main) undefined. fir architecture x86_64`

File diff suppressed because it is too large Load Diff

View File

@@ -16,14 +16,12 @@ NSURLProtocol 是 Foundation 框架中 URL Loading System 的一部分。它可
答案就是通过子类化来定义新的或是已经存在的 URL 加载行为。如果当前的网络请求是可以被拦截的,那么开发者只需要将一个自定义的 NSURLProtocol 子类注册到 App 中,在这个子类中就可以拦截到所有请求并进行修改。
## 二、NSURLProtocol 使用场景
### 1. 技术层面
NSURLProtocol 是 URL Loading System 的一部分,所以它可以拦截所有基于 URL Loading System 的网络请求:
- NSURLSession
- NSURLConnection
- NSURLDownload
@@ -31,9 +29,10 @@ NSURLProtocol 是 URL Loading System 的一部分,所以它可以拦截所有
- NSHTTPURLResponse
- NSURLRequest
- NSMutableURLRequest
所以,基础这些基础技术开发的网络框架比如 AFNetworking、Alamofire 也可以拦截。
所以,基础这些基础技术开发的网络框架比如 AFNetworking、Alamofire 也可以拦截。
想到了2种场景不能拦截
- 早期使用 CFNetwork 实现的 ASIHTTPRequest 框架就无法拦截
- UIWebView 也是可以被拦截的。但是 WKWebView 是基于 webkit不走底层 c socket。
@@ -54,9 +53,6 @@ NSURLProtocol 是 URL Loading System 的一部分,所以它可以拦截所有
- 自定义网络请求,过滤垃圾内容
- H5 加速,请求走本地离线包
## 三、NSURLProtocol 的相关方法
创建协议对象
@@ -69,6 +65,7 @@ NSURLProtocol 是 URL Loading System 的一部分,所以它可以拦截所有
```
注册和注销协议类
```Objective-c
// 尝试注册 NSURLProtocol 的子类,使之在 URL 加载系统中可见
+ (BOOL)registerClass:(Class)protocolClass;
@@ -103,6 +100,7 @@ NSURLProtocol 允许开发者去获取、添加、删除 request 对象的任意
提供请求的规范版本
如果你想要用特定的某个方式来修改请求,可以用下面这个方法。
```Objective-c
// 返回指定请求的规范版本
+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request;
@@ -118,6 +116,7 @@ NSURLProtocol 允许开发者去获取、添加、删除 request 对象的任意
启动和停止加载
这是子类中最重要的两个方法,不同的自定义子类在调用这两个方法时会传入不同的内容,但共同点都是围绕 protocol 客户端进行操作。
```Objective-c
// 开始加载
- (void)startLoading;
@@ -138,24 +137,19 @@ NSURLProtocol 允许开发者去获取、添加、删除 request 对象的任意
- (NSURLSessionTask *)task;
```
## 四、 如何利用 NSProtocol 拦截网络请求
NSURLProtocol 在实际应用中,主要是完成两步:拦截 URL 和 URL 转发。先来看如何拦截网络请求。
**创建 NSURLProtocol 子类**
这里创建一个名为 HTCustomURLProtocol 的子类。
```Objective-c
@interface HTCustomURLProtocol : NSURLProtocol
@end
```
**注册 NSURLProtocol 的子类**
在合适的位置注册这个子类。对基于 NSURLConnection 或者使用 [NSURLSession sharedSession] 初始化对象创建的网络请求,调用 registerClass 方法即可。
@@ -166,16 +160,12 @@ NSURLProtocol 在实际应用中,主要是完成两步:拦截 URL 和 URL
// [NSURLProtocol registerClass:[HTCustomURLProtocol class]];
```
如果需要全局监听,可以设置在 `AppDelegate.m` 的 `didFinishLaunchingWithOptions` 方法中。如果只需要在单个 UIViewController 中使用,记得在合适的时机注销监听:
```objective-c
[NSURLProtocol unregisterClass:[NSClassFromString(@"HTCustomURLProtocol") class]];
```
如果是基于 `NSURLSession` 的网络请求,且不是通过 `[NSURLSession sharedSession]` 方式创建的,就得配置 `NSURLSessionConfiguration` 对象的 `protocolClasses` 属性。
```objective-c
@@ -184,16 +174,10 @@ NSURLProtocol 在实际应用中,主要是完成两步:拦截 URL 和 URL
NSURLSession *session = [NSURLSession sessionWithConfiguration:config];
```
**实现 NSURLProtocol 子类**
> 注册 → 拦截 → 转发 → 回调 → 结束
以拦截 UIWebView 为例,这里需要重写父类的这五个核心方法。
```objective-c
@@ -323,11 +307,8 @@ didCancelAuthenticationChallenge:challenge];
}
```
注意NSURLConnection 已经被废弃,推荐使用 NSURLSession 进行网络请求,它好处多多,具体的自行查阅官方介绍。
## 五、 读源码,学习 NSURLProtocol
iOS 中网络测试框架 [ OHHTTPStubs](https://github.com/AliSoftware/OHHTTPStubs)的实现就是利用了 NSURLProtocol 实现的。
@@ -349,15 +330,8 @@ HTTPStubsProtocol 继承自 NSURLProtocol可以在 HTTP 请求发送之前对
`firstStubPassingTestForRequest` 方法内部会判断请求是否需要被当前对象处理
紧接着开始发送网络请求。实际上在 `- (void)startLoading` 方法中可以用任何网络能力去完成请求,比如 NSURLSession、NSURLConnection、AFNetworking 或其他网络框架。OHHTTPStubs 的做法是获取 request、client 对象。如果 HTTPStubs 单例中包含 `onStubActivationBlock` 对象,则执行该 block然后利用 responseBlock 对象返回一个 HTTPStubsResponse 响应对象。
## 六、 补充内容
### 1. 使用 NSURLSession 时的注意事项
@@ -368,16 +342,12 @@ HTTPStubsProtocol 继承自 NSURLProtocol可以在 HTTP 请求发送之前对
• 如果要用 registerClass 注册,只能通过 ` [NSURLSession sharedSession] `的方式创建网络请求。
### 2. 注册多个 NSURLProtocol 子类
当有多个自定义 NSURLProtocol 子类注册到系统中的话,会按照他们注册的反向顺序依次调用 URL 加载流程,也就是最后注册的 NSURLProtocol 会被优先判断。
对于通过配置 NSURLSessionConfiguration 对象的 protocolClasses 属性来注册的情况protocolClasses 数组中只有第一个 NSURLProtocol 会起作用,后续的 NSURLProtocol 就无法拦截到了。
### 3. 如何拦截 WKWebview
WKWebView 在独立于 app 进程之外的进程中执行网络请求,请求数据不经过主进程,因此,在 WKWebView 上直接使用 NSURLProtocol 无法拦截请求。苹果开源的 webKit2 源码暴露了 私有API
@@ -402,15 +372,15 @@ if ([cls respondsToSelector:sel]) {
该方案还存在2个严重缺陷
1. post 请求 body 数据被清空
由于 WKWebview 在独立的进程中执行网络请求,一旦注册 registerSchemeForCustomProtocol httphttps scheme 后,网络请求将从 Network Process 发送到 App Process这样 NSURLProtocol 才能拦截网络请求。在 Webkit2 的设计里使用 **MessageQueue** 进行进程间通信, Network Process 会将请求 encode 成一个 message然后通过 IPC 发送给 App Process出于性能角度的考虑encode 的时候 HTTPBody 和 HTTPBodyStream 这2个字段被丢弃掉了。可以查看 [webkit2 源码](https://github.com/WebKit/webkit/blob/fe39539b83d28751e86077b173abd5b7872ce3f9/Source/WebKit2/Shared/mac/WebCoreArgumentCodersMac.mm#L61-L88)。
2. 对 ATS 支持不足
info.plist 中打开 ATS 开关,设置 Allow Arbitrary Loads 选项为 NO设置 registerSchemeForCustomProtocol 注册了 httphttps schemeWKWebView 发起的所有 http 网络请求将被阻塞(即使 Allow Arbitrary Loads in Web Content 选项为 YES
WKWebView 可以注册 customScheme比如自定义 scheme`Hybrid://`,因此使用离线包但不使用 post 方式的请求可以通过 customScheme 发起。比如 `Hybrid://www.xxx.com/`,然后在 App 进程被 NSURLProtocol 拦截这个请求,然后加载离线包资源。
不足:使用 post 方式的请求需要修改 h5 侧代码scheme
### 4. WKWebView loadRequest 问题
@@ -440,34 +410,9 @@ if ([cls respondsToSelector:sel]) {
6. 网络请求完成后,通过 NetworkProtocolClient 将请求结果返回给 WKWebView。
### 5. 拦截 WebView 内 Ajax 请求
其实上述的方法也是可行,不过使用私有 API 的方式不是很推荐,一般在穷途末路的时候才选择私有 API所以另一种思路是 hook Web 端的 ajax 请求。在执行 hook 后的 ajax 请求的时候将 ajax 的请求相关信息请求方式、header、body 等)以 messageHandler 的方式告诉 Native然后起到监控的效果。
参考: https://www.jianshu.com/p/7337ac624b8ehttps://github.com/wendux/Ajax-hook
1.
关于 WKWebview 的各种问题可以查看这篇[文章](https://www.tuicool.com/articles/QbE3Mb7)。
1. 关于 WKWebview 的各种问题可以查看这篇[文章](https://www.tuicool.com/articles/QbE3Mb7)。

View File

@@ -1,71 +1,113 @@
# fishhook 原理
1. image: 代码编译后的可执行文件,被加载到内存中,就叫做镜像文件。
2. MachO 可执行文件被 dyld 加载到内存中,加载时并不是所有的符号都可以确定地址,有些是通过 lazy bind 在真正调用的时候绑定的。
3. iOS 代码在编译时没有办法确定方法的实现地址。**动态库共享缓存**里面有动态库。NSLog 属于 Foundation 框架,每个手机内部中的地址不一定。
## 先看看怎么用
![image-20200810182447587](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/image-20200810182447587.png)
经常会遇到 hook oc 方法,但是遇到像 NSLog、objc_msgSend 等方法的时候 OC Runtime 就不满足了,有了 fishhook 神器hook “c 函数”已不是难题。
4. DYLD 动态链接器负责将可执行文件加载到内存中。App 启动后DYLD 将 Foundation、UIKit 等加载进动态库共享缓存中,但是加载到的位置不确定。
为什么对 “c 函数”加了引号,带着问题往下看
5. 可执行文件头有 **Load Commands**:加载命令。告诉 DYLD 依赖了什么。
Hook NSLog上 Demo
6. 从 NSLog 找到代码实现经历过2种方式。早期重定向。现在PIC 技术(位置代码独立)。
```objectivec
static void (*SystemLog)(NSString *format, ...);
- (void)viewDidLoad {
[super viewDidLoad];
struct rebinding NSLogRebinding = {
"NSLog",
lbpLog,
(void *)&SystemLog
};
struct rebinding rebs[1] = {NSLogRebinding};
rebind_symbols(rebs, 1);
NSLog(@"沙沙");
}
void lbpLog(NSString *format, ...) {
format = [NSString stringWithFormat:@"fishhook 探索 - %@", format];
SystemLog(format);
}
@end
// fishhook 探索 - 沙沙
```
7. 可执行文件:
可以看到 hook 成功了。
- 代码段:可读可执行
- 数据段:可读可写
8. 写的业务代码里面假如某一行调用了 `NSLog`,那么在编译阶段,使用 NSLog 只是 IDE 提供了功能,让你可以看到声明而已。编译后的可执行文件,还是不知道 NSLog 的具体函数地址,它指向了一个表(类似一个应用程序的外部函数名,函数真正实现地址),去这个表里面找地址,这个表叫做**符号表**。
当 DYLD 加载当前可执行文件的时候,才将这个表每个编号对应的函数地址去填上去,这个动作叫做**符号绑定**。
当真正去调用 NSLog 函数的时候才去这个符号表中去寻找函数地址,去调用实现。
| 编号(符号) | 地址 | |
| ------------ | -------- | ---- |
| NSLog | 0xaabbcc | |
| ... | ... | |
![image-20200810201822593](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/mage-20200810201822593.png)
9. fishhook 做的事情就是将系统的符号表,将符号表中的特定符号对应的地址,修改为自定义的函数地址。起到了 hook 作用。也就是说外部的 c 函数,在 iOS 中的调用属于**动态调用**。
https://www.bilibili.com/video/BV1UZ4y1u7Ba?from=search&seid=14997461811427810898
```c
struct rebinding {
const char *name; // 需要 hook 的函数名称c 字符串
void *replacement; // 新函数地址
void **replaced; // 原始函数地址的指针
};
```
fishhook去 hook c 函数的原理。
## 原理窥探
假如我们的代码中调用了 NSLog 函数,因为 NSLog 的实现在 Foundation 库中,动态库在内存中的地址是不固定的, ASLR 机制下,
所以在编译阶段是没办法确定
我们知道 NSLog 函数实现在 Foundation 库中,而我们开发自己写的其他函数则在自身可执行文件中,也就是 Mach-O。
这里稍微展开谈谈静态链接和动态链接。
链接分为静态链接和动态链接。早期计算机都是采用静态链接这种方式的。静态链接存在缺点:
- 对于计算机内存和磁盘浪费很严重。想象下,每个程序内部保留了 printf、scanf 等公用库函数,还有很多其他库函数和所需要的数据结构。
- 程序的开发和发布很不方便。比如应用 A使用的 Lib.o 是一个第三方厂商提供的,当 lib.o 修复 bug 或者升级,开发都需要将应用 A 重新链接再发布,整个周期很不方便。
要解决空间浪费和更新困难最简单的办法就是把程序的模块拆分,形成独立文件,而不再将他们静态地链接在一起。而是等到程序运行起来才进行链接,也就是动态链接。
动态链接涉及运行时的链接以及多个文件的装载,必须有操作系统级别的支持。此时还有个角色叫做动态链接库。所有应用都可以在运行时使用它。
程序与 lib 动态库之间的链接工作是由动态链接器完成的,而不是静态链接器 ld 完成的。也就是动态链接是把链接这个过程由程序装载前被推迟到了装载的时候。
但也带来了坏处因为都是程序每次装载的时候进行重新链接。有解决方案叫做延迟绑定Lazy binding可使得动态链接对性能的影响减的最小。据估算动态链接相比静态链接存在大约5%的性能损失,但换来程序在空间上的节省和程序构建和升级的灵活性,是值得的。
## fishHook 不能 hook 自定义函数
地址无关代码PIC
可执行文件、动态链接库,加载到内存中的时候,会存在多种文件格式,系统为了统一标准,让加载到内存中的文件必须是 Mach-O 文件格式
Hopper Disassembler v4
- Mach-O 的定义?结构组成
-
装载时重定位是解决动态模块中有绝对地址引用的方案之一。但其存在一个很大缺点,是指令部分无法在多个进程间共享,这样就是失去动态链接节省内存这一优势。我们还需要一种更优雅的方案,希望程序模块中的共享指令部分在装载时不需要因为装载地址改变而改变,所以实现的基本想法就是把指令中那些需要被修改的部分分离出来,跟数据部分放在一起,这样指令部分就可以保持不变,而数据部分可以在每个进程中拥有一个副本,这个方案就是 地址无关代码Position Independent CodePIC 技术
fishhook 可以 hook c 函数的原因
写的业务代码里面假如某一行调用了 `NSLog`,那么在编译阶段,使用 NSLog 只是 IDE 提供了功能,让你可以看到声明而已。编译后的可执行文件,还是不知道 NSLog 的具体函数地址。这个底层是如何工作的呢
1. 函数符号 数据段 被修改
2. 函数符号为何位于数据段?
3. 动态库每次被加载到内存中,地址都是随机不确定的。所以需要符号地址的修正。符号重定位、重绑定
4. Lazy Symbol Pointers 懒汉模式Non-Lazy Symbol Pointers 启动就去绑定
在工程编译阶段,所产生的 Mach-O 可执行文件中会预留出一段空间,这个空间被叫做符号表,存放在 `_DATA` 数据段中,且数据段是可读可写的。
工程中所有引用了动态库共享缓存区中的系统符号,其指向的地址设置成符号地址。比如工程中 NSLog那么编译时就会在 Mach-O 中创建一个 NSLog 符号,工程中的 NSLog 就指向这个符号
当 dyld 将 Mach-O 加载到内存中时,读取 header 中 load command 信息,找出需要加载哪些库文件,去做绑定的操作。比如 dyld 会找到 Foundation 中的 NSLog 的真实地址写到 _DATA 段的符号表中 NSLog 对应的符号上。
当 DYLD 加载当前可执行文件的时候,才将这个表每个编号对应的函数地址去填上去,这个动作叫做**符号绑定**。
它指向了一个表(类似一个应用程序的外部函数名,函数真正实现地址),去这个表里面找地址,这个表叫做**符号表**。
当真正去调用 NSLog 函数的时候才去这个符号表中去寻找函数地址,去调用实现。
调用外部函数(在内部找不到方法实现)的时候,在 Mach-O 的数据段生成一个区域,叫做符号表。符号表的 key 就是方法名,比如 NSLog。
fishHook 做的事情就是 rebind将 NSLog 真正实现的地址指向到其他我们生成的函数地址去hook
PIC 技术。
| 编号(符号) | 地址 | |
| ------ | -------- | --- |
| NSLog | 0xaabbcc | |
| ... | ... | |
![image-20200810201822593](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/mage-20200810201822593.png)
fishhook 做的事情就是将系统的符号表,将符号表中的特定符号对应的地址,修改为自定义的函数地址。起到了 hook 作用。也就是说外部的 c 函数,在 iOS 中的调用属于**动态调用**。
知道了 fishhook 的工作原理,我们就知道 fishhook 是 hook 不了自己写的 c 函数了。因为自定义函数是没有通过符号去寻找函数真正地址的这个过程。而系统库是通过符号去绑定真实地址的。可以通过 `rebind_symbols` 这个名称得以印证。
1. 可以 hook c++吗?为什么
2. linux 平台下能否 hook c/c++
3.

View File

@@ -1,21 +1,902 @@
# block 原理
# block 底层原理
1. 解决循环引用不应该使用 weakself而是使用 strong-weak
```Objective-c
__weak typeof(self) Weakself = self;
self.block = ^ {
__strong typeof(Weakself) Strongself = Weakself;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^ {
NSLog(@"%@", Strongself.name);
});
## block 本质
block 本质上就是一个 oc 对象,也有 isa 指针
block 是封装了函数调用和函数调用环境的 OC 对象
![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/block-structure.png)
```objectivec
int age = 27;
void(^block)(void) = ^(){
    NSLog(@"age:%d", age);
};
self.block();
block();
```
![image-20200810214301251](/Users/lbp/Library/Application Support/typora-user-images/image-20200810214301251.png)
`xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main.cpp`
转换为 c++ 如下
```c
int age = 27;
void(*block)(void) = &__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age));
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
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) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
```
可以看到 `__main_block_impl_0` 是一个结构体有3个变量其中 `__main_block_impl_0` 是一个构造方法接收4个参数第四个参数 flags 是非必须的。
`__main_block_func_0` 参数是一个方法实现。
```c
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int age = __cself->age; // bound by copy
NSLog((NSString *)&__NSConstantStringImpl__var_folders_z5_ksvb7q252lbdfg78236t7tt00000gn_T_main_eb3c55_mi_0, age);
}
```
```c
static struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};
```
`__main_block_desc_0` 是一个 block 信息的描述,占用了 `sizeof(struct __main_block_impl_0)` 大小的空间。
`void(*block)(void) = &__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age));` 第一行代码也就是将构造一个 struct 给 block 变量。
`((**void** (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);` 第二行代码其实就是 `block->FuncPtr(block)` 根据 block 内部 FuncPtr 方法并调用。
```c
static struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};
```
为什么 block->FuncPtr 可以直接访问,而不是 block 先访问 impl再访问 FuncPtr因为 __block_impl 就是 __main_block_impl_0 这个结构体的第一个变量地址(结构体特性)
```c
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
```
类似于下面代码
```
struct __main_block_impl_0 {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
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) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
```
## block 变量捕获
```objectivec
int age = 27;
void(^block)(void) = ^(){
NSLog(@"age:%d", age);
};
age = 30;
block();
```
输出27。因为 Block 会对变量进行捕获。
```c
int age = 27;
void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age));
age = 30;
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
```
变量分为static、auto、register。
static表示作为静态变量存储在数据区。
auto一般的变量不加修饰词则默认为 autoauto 表示作为自动变量存储在栈上。意味着离开作用域变量会自动销毁。
register这个关键字告诉编译器尽可能的将变量存在CPU内部寄存器中而不是通过内存寻址访问以提高效率。是尽可能不是绝对。如果定义了很多 register 变量可能会超过CPU 的寄存器个数,超过容量。所以只是可能。
| 作用域 | 捕获到 block 内部 | 访问方式 |
| ---------- | ------------ | ---- |
| 局部变量 auto | YES | 值传递 |
| 局部变量static | YES | 指针传递 |
| 全局变量 | NO | 直接访问 |
Demo2
```objectivec
auto int age = 27;
static int height = 176;
void(^block)(void) = ^(){
NSLog(@"age:%d, height: %d", age, height);
};
age = 30;
height = 177;
block();
// age:27, height: 177
```
clang 转为 c++
```c
auto int age = 27;
static int height = 176;
void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age, &height));
age = 30;
height = 177;
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int age;
int *height;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int *_height, int flags=0) : age(_age), height(_height) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
```
可以看到 static 修饰的 height 在 c++ 代码底层用指针被 block 捕获,所以值修改后,是最终的 177static 修饰的 age被 block 捕获是值传递方式所以还是27
Demo3
```objectivec
int age = 27;
static int height = 176;
int main(int argc, const char * argv[]) {
@autoreleasepool {
void(^block)(void) = ^(){
NSLog(@"age:%d, height: %d", age, height);
};
age = 26;
height = 177;
block();
}
return 0;
}
```
转为 c++
```c
void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
age = 26;
height = 177;
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
NSLog((NSString *)&__NSConstantStringImpl__var_folders_z5_ksvb7q252lbdfg78236t7tt00000gn_T_main_65da50_mi_0, age, height);
}
```
可以看到全局变量是直接访问的,不进行拷贝。
为什么这么设计?
因为 auto 修饰的变量一出作用域马上回收,所以 block 为了自身运行信息的完整性所以会捕获。static 修饰的变量,数据一直在内存中,所以执行 block 代码的时候是用指针获取内存中最新的数据。全局变量不会捕获。
self 会捕获吗?
会,因为 self 就是局部变量。 一个 oc 方法转换为 `void test(Person *self, SEL _cmd)` 形式,所以 self 也是局部变量,会被捕获
## block 类型
block 的类型可以通过 isa 或者 class 方法查看,最终都是继承自 NSBlock 类型
`__NSGlobalBlock__` (`_NSConcreteGlobalBlock`):程序的数据区域(.data 区)
`__NSStackBlock__` (`_NSConcreteStackBlock`)
`__NSMallocBlock__`(`_NSConcreteMallockBlock`)
```objectivec
void(^block)(void) = ^(){
NSLog(@"Hello block");
};
NSLog(@"%@", [block class]); // __NSGlobalBlock__
NSLog(@"%@", [[block class] superclass]); // NSBlock
NSLog(@"%@", [[[block class] superclass] superclass]); // NSObjec
```
![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/block-memorylayout.png)
代码存放在 text 段static 修饰的数据存放在 data 区,程序员手动申请的内存存放在堆,局部变量存放在栈区
### 如何判断 block 属于什么类型
Demo1
```objectivec
void(^block)(void) = ^{
NSLog(@"");
};
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSLog(@"%@", [block class]);
}
return 0;
}
```
`__NSGlobalBlock__` ,此**类型的 block 用结构体实例的内容不依赖于执行时的状态所以整个程序只需要1个实例。因此存放于全局变量相同的数据区域即可。**
```objectivec
void(^block)(void);
void test()
{
int age = 22;
block = ^ {
NSLog(@"ag:%d", age);
};
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
test();
block(); // age:-1074793800
}
return 0;
}
```
为什么会打印出 `age:-1074793800`。 因为 block 访问了auto 变量,所以是 `__NSStackBlock__`。那么 block 内部的数据在栈上维护,当出了 test 方法后block 内部变量的地址到底指向什么是不确定的,可能会出现异常。
| block 类型 | 环境 |
| ------------------- | ------------------------------ |
| `__NSGlobalBlock__` | 没有访问 auto 变量 |
| `__NSStackBlock__` | 访问了 auto 变量 |
| `__NSMallocBlock__` | `__NSStackBlock__` 调用了 copy 方法 |
Demo1:
```objectivec
MyBlock block;
{
Person *person = [[Personalloc] init];
block = ^{
NSLog(@"block called");
};
NSLog(@"%@", [block class]);
};
```
MRC 环境: 如果 block 不访问外部局部变量,则`__NSGlobalBlock__`
ARC 环境:如果 block 不访问外部局部变量,则`__NSGlobalBlock__`
Demo2:
```objectivec
typedef void(^MyBlock)(void);
int main(int argc, const char * argv[]) {
@autoreleasepool {
MyBlock block;
{
auto Person *person = [[Person alloc] init];
person.age = 10;
block = ^{
NSLog(@"age:%zd", person.age);
};
NSLog(@"%@", [block class]); // __NSStackBlock__
};
}
return 0;
}
```
MRC 环境下:如果访问了 auto 变量,则为 `__NSStackBlock__`
ARC 环境下:**ARC 下面比较特殊,默认局部变量对象都是强指针,存放在堆里面。所以 block 为 `__NSMallocStack__`**
Demo3:
```objectivec
typedef void(^MyBlock)(void);
int main(int argc, const char * argv[]) {
@autoreleasepool {
MyBlock block;
{
auto Person *person = [[Person alloc] init];
person.age = 10;
block = [^{
NSLog(@"age:%zd", person.age);
} copy];
NSLog(@"%@", [block class]); // __NSMallocBlock__
};
}
return 0;
}
```
MRC 下:如果 block 调用 copy 方法,则 block 为 `__NSMallocStck__`
ARC 下:如果 block 调用 copy 方法,则 block 仍旧为 `__NSMallocBlock__``__NSMallocBlock__` 调用 copy 仍旧为 `__NSMallocBlock__`
在 ARC 下,如果有一个强指针引用 block则 block 会被拷贝到堆上,成为 `__NSMallocStck`
字节对齐k值8的倍数。
| Block 类 | 原本位置 | 复制效果 | |
| --------------------------- | ------ | ------ | --- |
| `__NSConcreteStackBlock__` | 栈 | 栈复制到堆 | |
| `__NSConcreteGlobalBlock__` | 程序的数据段 | 什么也不做 | |
| `__NSConcreteMallocBlock__` | 堆 | 引用计数+1 | |
字节对齐的原因:
### 内存管理
### 思考题
查看 block 编译成 c++ 代码的源码可以发现 `__main_block_desc_0` 结构体内部是变化的。什么意思呢reserved、Block_size 是一直有的void (*copy)、void (*dispose) 只有在修饰对象的时候才有。为什么这么设计?
```c
static struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
void (*dispose)(struct __main_block_impl_0*);
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0};
```
因为 block 会对变量进行内存管理。`void *copy``void *dispose` 都是内存管理的方法。
如果 block 访问的不是对象,则结构体没有 `void *copy``void *dispose`
Demo1:
```objectivec
{
Person *person = [[Person alloc] init];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"1---%@", person);
});
}
```
1s 后 person 执行了 dealloc 方法
Demo2
```objectivec
{
__weak Person *person = [[Person alloc] init];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"1---%@", person);
});
}
```
马上执行了 Person 的 dealloc 方法。因为 `__weak` 修饰block 内部的 `_Block_object_assign` 会根据 `__strong` 为对象引用计数 +1`__weak` 则引用计数不变。所以是 `__weak` 修饰,出离作用域则立马会释放 Person 对象。
`_Block_object_assign` 会根据内存修饰符来对内存进行操作。
Demo3
```objectivec
{
Person *person = [[Person alloc] init];
__weak Person *weakP = person;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"1---%@", person);
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"2---%@", person);
});
});
}
```
3s 后执行 Person 的 dealloc
Demo4
```objectivec
{
Person *person = [[Person alloc] init];
__weak Person *weakP = person;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"1---%@", weakP);
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"2---%@", person);
});
});
}
```
3s 后执行 Person 的 dealloc 方法
Demo5
```objectivec
{
Person *person = [[Person alloc] init];
__weak Person *weakP = person;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"1---%@", person);
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"2---%@", weakP);
});
});
}
```
1s 后执行 Person 的 dealloc 方法。
结论block 作为 GCD API 的方法参数时,如果 block 内部访问了对象,对象的生命周期结束需要查看强引用结束时刻。
### ARC 环境下编译器会自动会 block copy 复制到堆上
1. block 作为函数返回值时(如果栈 block离开方法作用域之后return 给新的变量区使用,由于栈变化了,所以不安全。比如访问 auto 变量的栈 block可能某个变量已经不是之前的某个值了
2. 将 block 赋值给 __strong 指针时
3. block 作为 Cocoa API 中方法名含有 usingBlock 的方法参数时
4. block 作为 GCD API 的方法参数时
栈空间的 block 不会对变量进行 copy 操作
堆空间的 block 会堆变量自动进行 copy 操作
`__NSStackBlock__` 内部访问了对象,默认是 `__strong` 修饰。如果对象是 `__weak` 则 block 转换 c++ 内部捕获的对象,也用 weak 修饰
```c
typedef void(^MyBlock)(void);
int main(int argc, const char * argv[]) {
@autoreleasepool {
MyBlock block;
{
__weak Person *person = [[Person alloc] init];
person.age = 27;
block = ^{
NSLog(@"age:%zd", person.age);
};
person.age = 28;
};
}
return 0;
}
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
Person *__weak person;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, Person *__weak _person, int flags=0) : person(_person) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
```
## block 修改变量
Demo1
```objectivec
typedef void(^MyBlock)(void);
int main(int argc, const char * argv[]) {
@autoreleasepool {
int age = 27;
MyBlock block = ^{
age = 28;
};
NSLog(@"%zd", age);
}
return 0;
}
```
运行会报错 `// Variable is not assignable (missing __block type specifier)` 为什么不能修改?继续查看 c++ 源代码
```c
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int age = __cself->age; // bound by copy
NSLog((NSString *)&__NSConstantStringImpl__var_folders_z5_ksvb7q252lbdfg78236t7tt00000gn_T_main_f31a48_mi_0, age);
}
static struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
int age = 27;
MyBlock block = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age));
NSLog((NSString *)&__NSConstantStringImpl__var_folders_z5_ksvb7q252lbdfg78236t7tt00000gn_T_main_f31a48_mi_1, age);
}
return 0;
}
```
可以看到在 block 内部修改外部变量也就是在新创建的函数内部,修改 main 函数内部的变量 😂 这怎么可能?
全局变量、static 变量在 block 内部可以修改。
- `__block` 用于解决 block 内部无法修改 auto 变量的问题。
- `__block` 不能修饰 static、全局变量
- 编译器会将 `__block` 修饰的变量包装为一个对象(后续修改则通过指针找到结构体对象,结构体对象再修改里面的值)
Demo
```objectivec
__block int age = 27;
MyBlock block = ^{
age = 28;
};
```
转为 C++
```c
__attribute__((__blocks__(byref))) __Block_byref_age_0 age = {(void*)0,(__Block_byref_age_0 *)&age, 0, sizeof(__Block_byref_age_0), 27};
MyBlock block = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_age_0 *)&age, 570425344));
NSLog((NSString *)&__NSConstantStringImpl__var_folders_z5_ksvb7q252lbdfg78236t7tt00000gn_T_main_f0f60a_mi_0, (age.__forwarding->age));
struct __Block_byref_age_0 {
void *__isa;
__Block_byref_age_0 *__forwarding;
int __flags;
int __size;
int age;
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
__Block_byref_age_0 *age = __cself->age; // bound by ref
(age->__forwarding->age) = 28;
}
```
可以看到 `__block int age = 27;` 变为了 `__Block_byref_age_0 age` 结构体。block 内部的函数在修改 age 的时候其实就是通过 `__main_block_impl_0` 结构体的 age 找到 `__Block_byref_age_0`,然后访问 `__Block_byref_age_0` 中的成员变量 `__forwarding` 访问成员变量 age并修改值。
![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/block-forwarding.png)
`__block` 修饰基本数据类型和对象,对于生成的结构体也不一样。
QA为什么`__block` 变量的 `__Block_byref_age_0` 结构体并不在 block 结构体 `__main_block_impl_0` 中?
因为这样做可以在多个 block 中使用 `__block` 变量。
Demo
```objectivec
struct __Block_byref_age_0 {
void *__isa;
__Block_byref_age_0 *__forwarding;
int __flags;
int __size;
int age;
};
struct __Block_byref_p_1 {
void *__isa;
__Block_byref_p_1 *__forwarding;
int __flags;
int __size;
void (*__Block_byref_id_object_copy)(void*, void*);
void (*__Block_byref_id_object_dispose)(void*);
NSObject *p;
};
```
block 只要和对象打交道,结构体里面要管理内存,所以会有 `void *copy` ,`void *dispose`
block 内部操作数组等类型,不需要加 `__block`
Demo知道 `__block` 的本之后,下面打印的 age 的地址是 struct 里面哪个的值?
```objectivec
__block int age = 27;
MyBlock block = ^{
age = 28;
};
NSLog(@"%p", &age);
```
知道转换为c++后的效果,我们可以在代码中按照结构体,自己定义并转接到 block
```objectivec
struct __Block_byref_age_0 {
void *__isa; // 0x0000000105231f70 +8
struct __Block_byref_age_0 *__forwarding; // 0x0000000105231f78 + 8
int __flags; // 0x0000000105231f80 +4
int __size; // 0x0000000105231f84 + 4
int age; // 0x0000000105231f88
};
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
void (*copy)(void);
void (*dispose)(void);
};
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
struct __Block_byref_age_0 *age; // by ref
};
typedef void(^MyBlock)(void);
int main(int argc, const char * argv[]) {
@autoreleasepool {
__block int age = 27;
MyBlock block = ^{
age = 28;
};
struct __main_block_impl_0 *blockImpl = (__bridge struct __main_block_impl_0 *)block;
NSLog(@"%p", &age);
}
return 0;
}
```
我们将断点设置到 NSLog 这里,打印出自定义结构体 `__main_block_impl_0` 中的 age 。
![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/Block-variableAddress.png)
```c
// 0x0000000105231f70
struct __Block_byref_age_0 {
void *__isa; // 地址0x0000000105231f70 长度:+8
struct __Block_byref_age_0 *__forwarding; // 地址0x0000000105231f78 长度:+8
int __flags; // 地址0x0000000105231f80 长度:+4
int __size; // 地址0x0000000105231f84 长度:+4
int age; // 地址0x0000000105231f88
};
```
将地址打印出来。该地址就是 `__Block_byref_age_0` 结构体的地址,也就是结构体内第一个 `isa` 的地址。我们计算下,规则如下:
- 指针长度8个字节
- int 长度4个字节
算出来 age 的地址为 `0x0000000105231f88` ,此时 Xcode 打印出的地址也是 `0x105231f88`。其实也就是 `blockImple->age->age` 的地址
block 内部对变量的值修改其实就是对 block 内部自定义结构体内部的变量修改。
当 block 被 copy 到堆上
- 会调用 block 内部的 copy 函数
- copy 函数内部会调用 `_Block_object_assign` 函数
- `_Block_object_assign` 函数会根据所指向对象的修饰符__strong、__weak、__unsafe_unretained做出相应的操作形成强引用retain或者弱引用注意这里仅限于ARC时会retainMRC时不会retain
当 block 从堆中移除
- 会调用 block 内部的 dispose 函数
- dispose 函数会调用 `_Block_object_dispose` 函数
- `_Block_object_dispose` 函数会自动释放 `__block` 修饰的变量release
## `__forwarding` 的设计
![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/block_forwarding.png)
当block在栈中时`__Block_byref_age_0`结构体内的`__forwarding`指针指向结构体自己。
而当block被复制到堆中时栈中的`__Block_byref_age_0`结构体也会被复制到堆中一份,而此时栈中的`__Block_byref_age_0`结构体中的`__forwarding`指针指向的就是堆中的`__Block_byref_age_0`结构体,堆中`__Block_byref_age_0`结构体内的`__forwarding`指针依然指向自己。
## Block 内存引用
`__block ` 修饰符修饰的对象在内存中如下
![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/block-object-memoery.png)
```c
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
__attribute__((__blocks__(byref))) __Block_byref_p_0 p = {(void*)0,(__Block_byref_p_0 *)&p, 33554432, sizeof(__Block_byref_p_0), __Block_byref_id_object_copy_131, __Block_byref_id_object_dispose_131, ((Person *(*)(id, SEL))(void *)objc_msgSend)((id)((Person *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Person"), sel_registerName("alloc")), sel_registerName("init"))};
void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_p_0 *)&p, 570425344));
}
return 0;
}
static void __Block_byref_id_object_copy_131(void *dst, void *src) {
_Block_object_assign((char*)dst + 40, *(void * *) ((char*)src + 40), 131);
}
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
_Block_object_assign((void*)&dst->p, (void*)src->p, 8/*BLOCK_FIELD_IS_BYREF*/);
}
```
如果 `__block` 修饰 `__strong` 则表示 block_impl 结构体中的 person 成员变量指向一个新的结构体 `__Block_byref_person_0`。这个线是强引用。
`__Block_byref_person_0` 结构体成员变量 person 真正的 Person 对象的引用关系要看 block 外部 person 的修饰是 `__strong` 还是 `__weak`,因为从栈上拷贝到堆上,会调用 block 的 desc 的 `__main_block_copy_0`,本质上调用的是 `_Block_object_assign`
`__Block_byref_id_object_copy_131` 方法里的 40 代表什么?
```c
struct __Block_byref_p_0 {
void *__isa; 8
__Block_byref_p_0 *__forwarding; 8
int __flags; 4
int __size; 4
void (*__Block_byref_id_object_copy)(void*, void*); 8
void (*__Block_byref_id_object_dispose)(void*); 8
Person *p;
};
__attribute__((__blocks__(byref))) __Block_byref_p_0 p = {
0,
&p,
33554432,
sizeof(__Block_byref_p_0),
__Block_byref_id_object_copy_131,
__Block_byref_id_object_dispose_131,
((Person *(*)(id, SEL))(void *)objc_msgSend)((id)((Person *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Person"), sel_registerName("alloc")), sel_registerName("init"))
};
```
`__Block_byref_p_0` 结构体地址上偏移40就是 p 对象。
## 循环引用
self 是一个局部变量block 访问 self即存在捕获变量的效果。
### ARC 下
`__weak``__unsafe_unretained``__block``
区别在于:`__weak` 不会产生强引用,指向的对象销毁时,会自动给指针置为 nil
`__unsafe_retained` 不会产生强引用,不安全。当指向的对象销毁时,指针地址值不变。
```objectivec
@interface Person : NSObject
@property (nonatomic, assign) NSInteger age;
@property (nonatomic, copy) void (^block)(void);
- (void)test;
@end
@implementation Person
- (void)dealloc
{
NSLog(@"%s", __func__);
}
- (void)test
{
    
__weak typeof(self) weakself = self;
self.block = ^{
weakself.age = 23;
};
self.block();
NSLog(@"age:%ld", (long)self.age);
}
@end
Person *p = [[Person alloc] init];
[p test];
```
方法1: `__weak` 修饰。`__weak typeof(self) weakself = self;`
方法2: `__unsafe_retained` 修饰。`**__unsafe_unretained** **typeof**(**self**) weakself = **self**;`
方法3: `__block` 修饰。因为此时会构成3角关系。所以需要调用 block。block 内部需要将对象设置为 nil。
```objectivec
__block Person *weakself = [[Person alloc] init];
p.block = ^{
weakself.age = 23;
NSLog(@"%ld", weakself.age);
weakself = nil;
};
p.block();
```
`__unsafe_retained` 因为不安全所以不推荐,`__block` 因为使用繁琐,且必须等到调用 block 才会释放内存所以不推荐。ARC 下最佳用 `__weak`
![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/block_object_cycle.png)
### MRC 下
方法1: `__unsafe_retained` 修饰。`**__unsafe_unretained** **typeof**(**self**) weakself = **self**;`
方法2: `__block` 修饰。MRC 下不会对 block 内部的对象引用计数 +1
## 总结
block 本质是什么?
封装了函数调用及其调用环境的 OC 对象
`__block` 的作用是什么?
可以对 block 外部的变量进行捕获,可以修改。但是需要注意内存管理相关问题。比如`__weak`、`__unsafe_unretained`、`__block`
修改 NSMutableArray 不需要加 `__block`?
是的

View File

@@ -1,99 +1,204 @@
# 二进制重排
# DYLD
dynamic loader动态加载器。在 MacOS/iOS 中,使用 `/usr/lib/dyld` 程序来加载动态库的。
## DYLDdyld shared cache 动态库共享缓存
UIKit、CoreGraphics 等。从 iOS13 开始为了提高性能绝大部分的系统动态库文件都被打包存放到了一个缓存文件中dyld shared cache中。
存放路径为:`/System/Libarey/Caches/com.apple.dyld/dyld_shared_cache_armX`
其中,`X` 代表 ARM 处理器的指令集架构。V6、V7、V7s、arm64、arm64e。不同架构对应的动态库缓存不一致。
所有指令集原则是向下兼容的。动态库共享缓存一个非常明显的好处是节省内存。
具体底层源码可以查看dyld 源码中 `dyld2.cpp` 文件,函数入口为 load 方法。
```c
ImageLoader* load(const char* path, const LoadContext& context, unsigned& cacheIndex);
```
某个 App 被第一次打开时候, dyld 根据动态库的依赖,循环加载动态库到动态库共享缓存中(内存),后续其他 App 第一次打开发现使用了动态库A已经在动态库共享缓存中存在则不需要加载。这一步调用方法为 `findInSharedCacheImage`
## dyld 应用
窥探系统库底层实现的时候可能需要从动态库共享缓存中提取出某个 Framework比如 UIKit。这时候要么用第三方工具要么用 dyld 的能力。
查看 `dsc_extractor.cpp` 代码可以看到是具备抽取能力的。修改源码,将 if 宏定义去掉。如下
```c
#include <stdio.h>
#include <stddef.h>
#include <dlfcn.h>
## 启动检测
typedef int (*extractor_proc)(const char* shared_cache_file_path, const char* extraction_root_path,
void (^progress)(unsigned current, unsigned total));
- App 动态库不要超过6个。
int main(int argc, const char* argv[])
{
if ( argc != 3 ) {
fprintf(stderr, "usage: dsc_extractor <path-to-cache-file> <path-to-device-dir>\n");
return 1;
}
- 静态库:影响 Mach-O 文件。
//void* handle = dlopen("/Volumes/my/src/dyld/build/Debug/dsc_extractor.bundle", RTLD_LAZY);
void* handle = dlopen("/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/usr/lib/dsc_extractor.bundle", RTLD_LAZY);
if ( handle == NULL ) {
fprintf(stderr, "dsc_extractor.bundle could not be loaded\n");
return 1;
}
- 动态库:影响 App 启动时间。
extractor_proc proc = (extractor_proc)dlsym(handle, "dyld_shared_cache_extract_dylibs_progress");
if ( proc == NULL ) {
fprintf(stderr, "dsc_extractor.bundle did not have dyld_shared_cache_extract_dylibs_progress symbol\n");
return 1;
}
int result = (*proc)(argv[1], argv[2], ^(unsigned c, unsigned total) { printf("%d/%d\n", c, total); } );
fprintf(stderr, "dyld_shared_cache_extract_dylibs_progress() => %d\n", result);
return 0;
}
```
然后用 clang++ 编译,命令为 `clang++ dsc_extractor.cpp`
## 虚拟内存、物理内存、内存分页
将编译后的产物复制到动态库共享缓存目录下去。然后执行命令`./dsc_extractor dyld_shared_cache_armv7s armv7s`,代表将动态库提取到 armv7s 目录下。
早期计算机内部都是物理内存。物理内存一个问题就是安全问题,通过地址访问到别的应用程序的数据。进程如果能直接访问物理内存无疑是很不安全的,(诞生了解决方案:虚拟内存)所以操作系统在物理内存的上又建立了一层虚拟内存。
## Mach-O
一个应用程序在使用的时候并不是全部使用的,往往是使用了一个应用程序的某个或者某几个功能。
Mach Object 的缩写,是 iOS/MacOS 上用于存储程序、库的标准格式
所以早期工程师的第一个解决方案是将一个应用程序分块,按需加载功能模块的内存地址数据。
在 XNU 源码中可以查看 Mach-O 的定义。`loader.h`
属于 Mach-O 格式的文件类型有:
```c
#define MH_OBJECT 0x1 /* relocatable object file */
#define MH_EXECUTE 0x2 /* demand paged executable file */
#define MH_FVMLIB 0x3 /* fixed VM shared library file */
#define MH_CORE 0x4 /* core file */
#define MH_PRELOAD 0x5 /* preloaded executable file */
#define MH_DYLIB 0x6 /* dynamically bound shared library */
#define MH_DYLINKER 0x7 /* dynamic link editor */
#define MH_BUNDLE 0x8 /* dynamically bound bundle file */
#define MH_DYLIB_STUB 0x9 /* shared library stub for static */
/* linking only, no section contents */
#define MH_DSYM 0xa /* companion file with only debug */
/* sections */
#define MH_KEXT_BUNDLE 0xb /* x86_64 kexts */
```
虚拟内存是间接访问了内存条。
### 常见的 Mach-O 文件类型
内存分页iOS 一页就是16KB。
MH_OBJECT
物理内存如果满了,则将最活跃的内存数据,覆盖掉,最不活跃的内存数据;
- 目标文件(.o
ASLR为了安全问题诞生。
- 静态库文件(.a静态库其实就是 N 个 `.o` 合并在一起
自己的代码中有 NSLog 代码,可是 NSLog 函数的代码实现在 Foundation 动态库中。
MH_EXECUTE可执行文件
App 启动则用 dyld 去加载库,共享缓存库。
MH_DYLIB动态库文件
- dylib
- .framework
虚拟地址:偏移是编译后就能确定的。
MY_DYLINKER动态链接编辑器 (/usr/lib/dyld)
MH_DSYM存储着二进制文件符号。`.dSYM/Contents/Resource/DWARF/xx` 常用于还原堆栈
Xcode 中也可以查看 Mach-O 文件类型
内存缺页异常:在使用中,访问虚拟内存的一个 page 而对应的物理内存缺不存在(没有被加载到物理内存中),则发生缺页异常。影响耗时,在几毫秒之内。
![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/MachOFileType.png)
什么时候发生大量的缺页异常?一个应用程序刚启动的时候。
### Universal Binary
启动时所需要的代码分布在第一页、第二页、第三页...第200页。这样的情况下启动时间会影响较大所以解决思路就是将应用程序启动刻所需要的代码二进制优化一下统一放到某几页这样就可以避免内存缺页异常则优化了 App 启动时间
通用二进制文件,可以同时适用于多种架构的二进制文件,包含多种不同架构的独立的二进制文件
因为要存储多种架构的代码,所以通用二进制文件通常比单一平台的二进制程序更大
由于两种架构有共同的一些资源所以并不会达到单一版本的2倍多。
执行的过程中,只调用一部分代码,运行起来不需要额外的内存。
因为通用二进制文件比原来的大所以被成为“胖二进制文件”Fat Binary
dylib loading time
查看某可执行文件(Test)支持的架构指令集
rebase/binding time 修正符号是由于 ASLR 导致。binding time 是动态库去 bind lazy table、non-lazy table 所占用的时间。rebase 地址偏移ASLR。 rebase 的时间如何缩小Mach-O 文件大小变小。 binding time 变小则需要动态库变小。2者优化手段冲突
`lipo -info Test`
Objc setup timeSwift 这部分占优势
将某个指令集拆出来比如 arm64
initializer timeload 方法耗时。
`lipo Test -thin arm64 -o Test_arm64`
slowest intializers
也可以将多个指令集合并
libS
`lipo -create Test_arm64 Test_armv7 -output Test_universal`
libMain
## Mach-O 结构
![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/Mach-OStructure.png)
一个 Mach-O 文件包含3块
查看 LinkMap。发现方法展示顺序是按照写代码的顺序展示的。
- Header文件类型、目标架构类型信息
![image-20200814211215976](/Users/lbp/Library/Application Support/typora-user-images/image-20200814211215976.png)
- Load commands描述文件在虚拟内存中的逻辑结构、布局
- Raw segment data在 Load Commands 中定义的 segment 的原始数据
可以用 [GitHub - fangshufeng/MachOView: 分析Macho必备工具](https://github.com/fangshufeng/MachOView) 和系统自带的 atool 查看 Mach-O 信息
## 有没有办法将 App 启动需要的方法集中收拢?
![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/otoolhelp.png)
1. 在 Xcode 的 Build Settings 中设置 **Order File**Write Link Map Files 设置为 YES进行观察
用 MachOView 查看 DDD Mach-O 文件
2. 如果你给 Xcode 工程根目录下指定一个 order 文件,比如 `refine.order`,则 Xcode 会按照指定的文件顺序进行二进制数据重排。分析 App 启动阶段,将优先需要加载的函数、方法,集中合并,利用 Order File减小缺页异常从而减小启动时间。
![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/MachOPageZero.png)
![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/MachOText.png)
可以看到在 Mach-O 文件上,`__PAGEZERO``VM Size` 有值,但是 File Size 为0也就是说 `__PAGEZERO` 在 Mach-O 中不占据内存,但是程序运行起来之后,会占据虚拟内存。所以代码段在 Mach-O 中 File Offset 为0如果前面的 `__PAGEZERO` 的 File size 有值,这里的 File Offset 就不为0
## 如何拿到启动时刻所调用的所有方法名称
**在没有 ASLR 的时候__TEXT 代码段地址 = __PAGEZEROR 地址**
clang 插桩,才可以 hook OC、C、block、Swift 全部。LLVM 官方文档。
## ASLR
### 未使用 ASLR 的问题
- 函数代码存放在 `__TEXT`
- 全局变量存放在 `__DATA`
二进制重排提升 App 启动速度是通过「解决内存缺页异常」(内存缺页会有几毫秒的耗时)来提速的。
- 可执行文件的内存地址为 `0x0`
一个 App 发生大量「内存缺页」的时机就是 App 刚启动的时候。所以优化手段就是「将影响 App 启动的方法集中处理放到某一页或者某几页」虚拟内存中的页。Xcode 工程允许开发者指定 「Order File」可以「按照文件中的方法顺序去加载」可以查看 linkMap 文件(需要在 Xcode 中的 「Buiild Settings」中设置 Order File、Write Link Map Files 参数)。
- 可执行文件 Header 的内存地址,就是 `LC_SEGMENT(__TEXT)` 中的 VM Address
- arm64 0x100000000
- 非 arm640x4000
其实难点是如何拿到启动时刻所调用的所用方法?代码可能是 Swift、block、c、OC所以hook 肯定不行、fishhook 也不行,用 clang 插桩可行
也可以使用 `size -l -m -x DDD` 指令来查看 Mach-O 的内存分布
![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/MachOInsepect.png)
![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/ASLRDemo.png)
我们会发现根据 Mach-O 文件中的信息File Size、File Offset、VM Address、VM Size 可以判断出 `__TEXT` 段内函数信息,这样子不够安全
### ASLR 诞生
- https://mp.weixin.qq.com/s/SUHaGD1T2Vce4Ag-qgxtgg
Address Space Layout Randomization地址空间布局随机化。是一种针对缓冲区溢出的安全保护技术通过堆、栈、共享库映射等线性区布局的随机化通过增加攻击者预测目的地址的难度防止攻击者直接定位攻击代码的位置达到阻止溢出攻击目的的一种技术。在 iOS 4.3 引入。
![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/ASLROffset.png)
- LC_SEGMENT (__TEXT) 的 VM Address 0x10005000
- ASLR 随机偏移 0x5000也就是可执行文件的内存地址
在有 ASLR 的时候__TEXT 代码段地址 = __PAGEZEROR 地址
在 Mach-O 文件中的地址是原始地址
```shell
代码运行起来函数真实地址 = ASLR-Offset + __PAGEZEROarm640x100000000其他0x4000+ 函数基于 Mach-O 的地址
```

View File

@@ -58,7 +58,7 @@ Vue 支持单向绑定和双向绑定。单向绑定其实就是 Model 到 View
使用 React Redux 一个典型的流程图如下
![React-Redux](./../assets/2019-06-26-Redux-Structures.png)
![React-Redux](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2019-06-26-Redux-Structures.png)
假如我们把 action 和 dispatcher 的实现隐藏在框架内部,这个图可以简化为

View File

@@ -9,7 +9,7 @@
* [4、如何优雅地调试手机网页](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.4.md)
* [5、事件响应者链](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.5.md)
* [6、外卖App双列表联动](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.6.md)
* [7、在内存剖析对象](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.7.md)
* [7、对象在内存中的存储底层原理](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.7.md)
* [8、教你实现微信公众号效果长按图片保存到相册](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.8.md)
* [9、hitTest和pointInside方法你真的熟吗](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.9.md)
* [10、HyBrid探索](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.10.md)
@@ -41,9 +41,9 @@
* [35、一些布局小知识](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.35.md)
* [36、iOS数值计算精度丢失问题](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.36.md)
* [37、一些看到但未尝试的知识](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.37.md)
* [38、RunLoop](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.38.md)
* [39、RunLoop下](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.39.md)
* [40、RunLoop的应用](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.40.md)
* [38、RunLoop探究](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.38.md)
* [39、多线程探究](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.39.md)
* [40、内存问题研究](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.40.md)
* [41、iOS 应用启动性能优化资料汇总](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.41.md)
* [42、App security](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.42.md)
* [43、奇技淫巧调试篇](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.43.md)
@@ -51,7 +51,7 @@
* [45、NSTimer 的内存泄漏](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.45.md)
* [46、KVC && KVO](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.46.md)
* [47、金额格式化](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.47.md)
* [48、OC类别Catrgory拓展Extension](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.48.md)
* [48、类别Category拓展Extension、load、initialize](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.48.md)
* [49、MVC、MVP、MVVM](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.49.md)
* [50、“静态库”和“动态库”](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.50.md)
* [51、cocoapods 相关小技巧](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.51.md)
@@ -92,9 +92,9 @@
* [86、GCD 源码探究](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.86.md)
* [87、Objective-C 底层探究](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.87.md)
* [88、fishhook 原理](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.88.md)
* [89、block 原理](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.89.md)
* [89、block 底层原理](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.89.md)
* [90、YYImage 框架原理,探索图片高效加载原理](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.90.md)
* [91、二进制重排](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.91.md)
* [91、DYLD](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.91.md)
* [92、flutter 无痕埋点](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.92.md)
* [93、flutter 新功能引导](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.93.md)
* [94、APM-Wake Up](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.94.md)
@@ -103,4 +103,6 @@
* [97、__attribute__ 的骚操作](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.97.md)
* [98、前端、BFF、后端一些常见的设计模式](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.98.md)
* [99、客户端质量把控](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.99.md)
* [100、iOS 端底层网络错误](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.100.md)
* [100、iOS 端底层网络错误](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.100.md)
* [101、离屏渲染](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.101.md)
* [102、LLVM](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.102.md)

View File

@@ -398,7 +398,7 @@ Electron 架构和 Chromium 架构类似也是具有1个主进程和多个渲
- ![chrome inspect](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-04-21-electronChromeInspect.png)
- 重新开启服务 `npm start`,在 chrome inspect 面板的 `Target` 节点中选择需要调试的页面
- 在面板中可以看到主进程执行的 `main.js`。可以加断点进行调试
![chrome inspect](./../assets/2020-04-21-Electron-MainProcessInspect.png)
![chrome inspect](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-04-21-Electron-MainProcessInspect.png)
方法二:利用 VS Code 调试 Electron 主进程。
@@ -846,6 +846,6 @@ app.listen(33855)
1. App 包体积大小是一个工程治理的一个永恒话题,伴随着 App 每一次版本发布的生命周期App 包大小的意义就不再赘述这里讲讲【App 包体积】这个命题如何与 Electron 结合起来。
App 包体积的治理方案可以查看 [App瘦身之道](./../ Chapter1\ -\ iOS/1.60.md) 这篇文章。目的是通过 Electron 这个技术打造有赞自己的移动潘多拉魔盒囊括必要的各种能力所以【App 包体积】这个命题可以结合 Electron将包大小检测能力作为魔盒的能力之一。
App 包体积的治理方案可以查看 [App瘦身之道](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/ Chapter1\ -\ iOS/1.60.md) 这篇文章。目的是通过 Electron 这个技术打造有赞自己的移动潘多拉魔盒囊括必要的各种能力所以【App 包体积】这个命题可以结合 Electron将包大小检测能力作为魔盒的能力之一。
2. 桌面端技术选型的时候现在多了一些选择Electron、[Tauri](https://github.com/tauri-apps/tauri)、ImGui。其中 Tauri 就是 WebView + Rust 的实现。ImGui 是一个 C/C++ 实现的即时渲染框架。

View File

@@ -9,7 +9,7 @@
* [4、如何优雅地调试手机网页](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.4.md)
* [5、事件响应者链](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.5.md)
* [6、外卖App双列表联动](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.6.md)
* [7、在内存剖析对象](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.7.md)
* [7、对象在内存中的存储底层原理](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.7.md)
* [8、教你实现微信公众号效果长按图片保存到相册](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.8.md)
* [9、hitTest和pointInside方法你真的熟吗](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.9.md)
* [10、HyBrid探索](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.10.md)
@@ -40,9 +40,9 @@
* [35、一些布局小知识](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.35.md)
* [36、iOS数值计算精度丢失问题](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.36.md)
* [37、一些看到但未尝试的知识](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.37.md)
* [38、RunLoop](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.38.md)
* [39、RunLoop下](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.39.md)
* [40、RunLoop的应用](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.40.md)
* [38、RunLoop探究](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.38.md)
* [39、多线程探究](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.39.md)
* [40、内存问题研究](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.40.md)
* [41、iOS 应用启动性能优化资料汇总](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.41.md)
* [42、App security](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.42.md)
* [43、奇技淫巧调试篇](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.43.md)
@@ -50,7 +50,7 @@
* [45、NSTimer 的内存泄漏](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.45.md)
* [46、KVC && KVO](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.46.md)
* [47、金额格式化](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.47.md)
* [48、OC类别Catrgory拓展Extension](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.48.md)
* [48、类别Category拓展Extension、load、initialize](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.48.md)
* [49、MVC、MVP、MVVM](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.49.md)
* [50、“静态库”和“动态库”](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.50.md)
* [51、cocoapods 相关小技巧](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.51.md)
@@ -91,9 +91,9 @@
* [86、GCD 源码探究](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.86.md)
* [87、Objective-C 底层探究](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.87.md)
* [88、fishhook 原理](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.88.md)
* [89、block 原理](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.89.md)
* [89、block 底层原理](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.89.md)
* [90、YYImage 框架原理,探索图片高效加载原理](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.90.md)
* [91、二进制重排](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.91.md)
* [91、DYLD](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.91.md)
* [92、flutter 无痕埋点技术方案](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.92.md)
* [93、flutter 新功能引导](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.93.md)
* [94、APM-Wake Up](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.94.md)
@@ -103,6 +103,8 @@
* [98、前端、BFF、后端一些常见的设计模式](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.98.md)
* [99、客户端质量把控](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.99.md)
* [100、iOS 端底层网络错误](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.100.md)
* [101、离屏渲染](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.101.md)
* [102、LLVM](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.102.md)
* [Chapter2 - Web FrontEnd](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter2%20-%20Web%20FrontEnd/chapter2.md)
* [1、-last-child与-last-of-type你只是会用有研究过区别吗](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter2%20-%20Web%20FrontEnd/2.1.md)
* [2、正则表达式](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter2%20-%20Web%20FrontEnd/2.2.md)

BIN
assets/ASLRDemo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

BIN
assets/ASLROffset.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

BIN
assets/AppLaunchingTime.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 184 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 469 KiB

BIN
assets/Facebook-OOM.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

BIN
assets/KVC-get-process.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 607 KiB

BIN
assets/KVC-process.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 992 KiB

BIN
assets/LLVM-Structure.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 479 KiB

BIN
assets/LLVM-phase.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

BIN
assets/LLVM-segment.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

BIN
assets/Mach-OStructure.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 404 KiB

BIN
assets/MachOFileType.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 350 KiB

BIN
assets/MachOInsepect.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 MiB

BIN
assets/MachOPageZero.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 326 KiB

BIN
assets/MachOText.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 341 KiB

143
assets/MockClassInfo.h Normal file
View File

@@ -0,0 +1,143 @@
//
// MockClassInfo.h
// TestClass
//
// Created by MJ Lee on 2018/3/8.
// Copyright © 2018年 MJ Lee. All rights reserved.
//
#import <Foundation/Foundation.h>
#ifndef MockClassInfo_h
#define MockClassInfo_h
# if __arm64__
# define ISA_MASK 0x0000000ffffffff8ULL
# elif __x86_64__
# define ISA_MASK 0x00007ffffffffff8ULL
# endif
#if __LP64__
typedef uint32_t mask_t;
#else
typedef uint16_t mask_t;
#endif
typedef uintptr_t cache_key_t;
struct bucket_t {
cache_key_t _key;
IMP _imp;
};
struct cache_t {
bucket_t *_buckets;
mask_t _mask;
mask_t _occupied;
};
struct entsize_list_tt {
uint32_t entsizeAndFlags;
uint32_t count;
};
struct method_t {
SEL name;
const char *types;
IMP imp;
};
struct method_list_t : entsize_list_tt {
method_t first;
};
struct ivar_t {
int32_t *offset;
const char *name;
const char *type;
uint32_t alignment_raw;
uint32_t size;
};
struct ivar_list_t : entsize_list_tt {
ivar_t first;
};
struct property_t {
const char *name;
const char *attributes;
};
struct property_list_t : entsize_list_tt {
property_t first;
};
struct chained_property_list {
chained_property_list *next;
uint32_t count;
property_t list[0];
};
typedef uintptr_t protocol_ref_t;
struct protocol_list_t {
uintptr_t count;
protocol_ref_t list[0];
};
struct class_ro_t {
uint32_t flags;
uint32_t instanceStart;
uint32_t instanceSize; // instance对象占用的内存空间
#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;
};
struct class_rw_t {
uint32_t flags;
uint32_t version;
const class_ro_t *ro;
method_list_t * methods; // 方法列表
property_list_t *properties; // 属性列表
const protocol_list_t * protocols; // 协议列表
Class firstSubclass;
Class nextSiblingClass;
char *demangledName;
};
#define FAST_DATA_MASK 0x00007ffffffffff8UL
struct class_data_bits_t {
uintptr_t bits;
public:
class_rw_t* data() {
return (class_rw_t *)(bits & FAST_DATA_MASK);
}
};
/* OC对象 */
struct mock_objc_object {
void *isa;
};
/* 类对象 */
struct mock_objc_class : mock_objc_object {
Class superclass;
cache_t cache;
class_data_bits_t bits;
public:
class_rw_t* data() {
return bits.data();
}
mock_objc_class* metaClass() {
return (mock_objc_class *)((long long)isa & ISA_MASK);
}
};
#endif /* MockClassInfo_h */

Binary file not shown.

After

Width:  |  Height:  |  Size: 847 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 417 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 611 KiB

BIN
assets/RunLoop-RunIssue.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 286 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 312 KiB

BIN
assets/RunLoop-Specific.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 795 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 205 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 531 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 908 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 618 KiB

View File

@@ -0,0 +1,35 @@
int __forwarding__(void *frameStackPointer, int isStret) {
id receiver = *(id *)frameStackPointer;
SEL sel = *(SEL *)(frameStackPointer + 8);
const char *selName = sel_getName(sel);
Class receiverClass = object_getClass(receiver);
// 调用 forwardingTargetForSelector:
if (class_respondsToSelector(receiverClass, @selector(forwardingTargetForSelector:))) {
id forwardingTarget = [receiver forwardingTargetForSelector:sel];
if (forwardingTarget && forwardingTarget != receiver) {
return objc_msgSend(forwardingTarget, sel, ...);
}
}
// 调用 methodSignatureForSelector 获取方法签名后再调用 forwardInvocation
if (class_respondsToSelector(receiverClass, @selector(methodSignatureForSelector:))) {
NSMethodSignature *methodSignature = [receiver methodSignatureForSelector:sel];
if (methodSignature && class_respondsToSelector(receiverClass, @selector(forwardInvocation:))) {
NSInvocation *invocation = [NSInvocation _invocationWithMethodSignature:methodSignature frame:frameStackPointer];
[receiver forwardInvocation:invocation];
void *returnValue = NULL;
[invocation getReturnValue:&value];
return returnValue;
}
}
if (class_respondsToSelector(receiverClass,@selector(doesNotRecognizeSelector:))) {
[receiver doesNotRecognizeSelector:sel];
}
// The point of no return.
kill(getpid(), 9);
}

BIN
assets/autoreleasepool.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 145 KiB

BIN
assets/block-forwarding.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 250 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

BIN
assets/block-structure.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 295 KiB

BIN
assets/block_forwarding.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 286 KiB

BIN
assets/clang-analysize.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 MiB

BIN
assets/clang-ast.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

BIN
assets/clang-phase.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 514 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 338 KiB

BIN
assets/iOS-MemoryLayout.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 494 KiB

BIN
assets/objc-isa-mask.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

BIN
assets/objc-isa.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

BIN
assets/objc-superclass.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 829 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 388 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 497 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 389 KiB

BIN
assets/otoolhelp.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 278 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

BIN
assets/runtime-class.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 405 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 334 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 154 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 509 KiB

BIN
assets/runtime-isa-demo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 183 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 252 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 273 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 168 KiB

BIN
assets/runtime-super.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 450 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 577 KiB