0%

上一篇文章我们讲到 alloc 在开辟内存空间之前,对对要分配的内存空间提前进行计算,并最终使用 16 字节对其方法进行对其,提升了读取的效率。但是16 字节对其之前,如何计算对象实际需要的空间呢?

1. 对象内存分析

先展示一段测试代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@interface LGPerson : NSObject

@property (nonatomic, strong) NSString *name;
@property (nonatomic, strong) NSString *name1;
@property (nonatomic, assign) short a;
@property (nonatomic, assign) int b;
@property (nonatomic, assign) double c;

@end

// 调用
LGPerson *person = [[LGPerson alloc] init];

NSLog(@"sizeof——%lu", sizeof([person class]));
NSLog(@"class_getInstanceSize——%lu", class_getInstanceSize([person class]));
NSLog(@"malloc_size——%lu", malloc_size((__bridge const void *)(person)));

sizeof——8
class_getInstanceSize——40
malloc_size——48

这里可以发现,对于类本身,sizeof 可以查询到它所占用的地址空间只有 8 位。

而 class_getInstanceSize 为何是 40?没有 16 进制对其?

其实 class_getInstanceSize 获取到的是实例对象实际需要占用的内存空间,而且实际使用一般只会做 8 字节对齐。

而 malloc_size 则是系统实际为该实例对象分配的空间。也就是经过 16 字节对其后的效果。

2. 结构体内存对齐

看完了对象,我们看下结构体是如何进行内存对齐的。(实际上要比对象对其要简单些,并且不涉及 16 进制对其的优化,能更直观的得出内存空间实际占用的计算)

预备知识

在展示实例之前,我们先看下实例中用到的类型的内存空间情况

1
2
3
4
5
6
7
8
9
10
11
NSLog(@"double size %lu", sizeof(double));
NSLog(@"int size%lu", sizeof(int));
NSLog(@"short size %lu", sizeof(short));
NSLog(@"char size %lu", sizeof(char));
NSLog(@"NSString size %lu", sizeof(NSString *));

// double size 8
// int size 4
// short size 2
// char size 1
// NSString * size 8(存储地址)

初级案例

ok,这些类型的空间大小大家应该熟悉了。下面介绍实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
typedef struct Str1 {
int o;
short s;
char a;
double c;
NSString *x;
} S1;

typedef struct Str2 {
double a;
int d;
short b;
char c;
S1 s1;
} S2;
NSLog(@"%lu", sizeof(S1)); // 24
NSLog(@"%lu", sizeof(S2)); // 40

我们先来看下结构体 Str1 的内存大小 - 16。

实际上,基础类型的存储位置为栈内存,而结构体的存储也使用了内存对其的技术,用于减少 cpu 的访问内存次数,提升读取效率。

首先我们需要了解一下结构体内存对其的规则:

  1. 结构体变量的首地址是其最长基本类型成员的整数倍;
  2. 结构体每个成员相对于结构体首地址的偏移量(offset)都是成员大小的整数倍,如不满足,对前一个成员填充字节以满足;
  3. 结构体的总大小为结构体最大基本类型成员变量大小的整数倍;
  4. 结构体中的成员变量都是分配在连续的内存空间中。
1
2
3
4
5
6
7
8
9
typedef struct Str1 {
int o; // 占用 4 字节,分别为 0 ~ 3
short s; // 占用 2 字节,分别为 4 ~ 5
char a; // 占用 1 字节, 分别为 6
NSString *x; // 占用8字节, 但由于规则2限制,需要对空间 7 进行填充,实际存储位置为 8 ~ 15
double c; // 占用8字节,分别为 16 - 23
} S1; // 根据规则3,最大成员大小为8,0~23 总共占用24字节,刚好符合 8 的倍数。

NSLog(@"%lu", S1); // 24

所以,根据规则,我们得出上述结构体 Str1 内存大小为 24 字节。

进阶案例

上面案例中,str2 是一个嵌套类型结构体,他的大小又是如何求得的呢?

💡知识点:结构体嵌套的内存对其方式

如果一个结构体B里嵌套另一个结构体A,还是以最大成员类型的字节对齐,但是结构体A存储起点为A内部最大成员整数倍的地方。(struct B里存有struct A,A里有char,int,double等成员,那A应该从8的整数倍开始存储。),结构体A中的成员的对齐规则仍满足自身的规则

1
2
3
4
5
6
7
typedef struct Str2 {
double a; // 8字节 0 ~ 7
int d; // 4 字节: 8 ~ 11
short b; // 2字节: 12 ~ 13
char c; // 1字节: 14
S1 s1; // 根据上面的结果,此处大小为 24 字节。此处需要注意,依据结构体嵌套对其方式,存储起始点则为最长 8 字节的倍数,即 16 ~ 39
} S2; // 综上,40 刚好为 8 的整数倍。即如 sizeof 得出的结果为,40

示例: SomeClass 对象 alloc 方法调用

1
SomeClass *sc = [[SomeClass alloc] init]

重点1. 对象调用 alloc,底层 C 接口 objc_alloc_init

1
2
3
4
5
6
7
8
9
10
+ (id)alloc {
return _objc_rootAlloc(self);
}
// Base class implementation of +alloc. cls is not nil.
// Calls [cls allocWithZone:nil].
id
objc_alloc_init(Class cls)
{
return [callAlloc(cls, true/*checkNil*/, false/*allocWithZone*/) init];
}

这里要注意的是,alloc 核心方法都在 callAlloc, init 调用需要看各自类的实现,系统根类(NSObject)没有太多实现, 下面是 init 的实现。

1
2
3
4
5
6
7
8
9
10
11
12
- (id)init {
return _objc_rootInit(self);
}

id
_objc_rootInit(id obj)
{
// In practice, it will be hard to rely on this function.
// Many classes do not properly chain -init calls.
// 根类仅仅返回对象本身
return obj;
}

说起 alloc/init,不得不提下他们的聚合版本 new,实际上他的内部实现等同于 alloc + init,但是为何不推荐使用呢?

1
2
3
+ (id)new {
return [callAlloc(self, false/*checkNil*/) init];
}

在UIKit 或 Foundation 框架中,很多类会将 first design 方法替换成自定义 initWithXXX 方法,使用 new 将很难发现这些类初始化失败的原因,导致代码可读性降低,查错困难。

小插曲

Xcode 10 和 Xcode 11 在 debug 时,堆栈不太一致

Xcode 11 中不会在堆栈中显示调用 objc_alloc_init,只有注释中说明了这一步骤。

对于这个问题,目前的说法是源码开源得不够充分。以下这段代码虽然未调用到,但其逻辑也是耐人寻味。 大致猜测是这个方法等于交换单次的Method Swizzling

重点2. 计算要开辟的内存大小

这里需要注意,oc 对象本身需要的内存大小基础上,会进行 16 字节对其,目的是为了进行读取速度优化,空间换时间。实际上大多只需要 8 字节就能存储完成,如下为 16 字节对其代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
size_t fastInstanceSize(size_t extra) const
{
ASSERT(hasFastInstanceSize(extra));

if (__builtin_constant_p(extra) && extra == 0) {
return _flags & FAST_CACHE_ALLOC_MASK16;
} else {
size_t size = _flags & FAST_CACHE_ALLOC_MASK;
// remove the FAST_CACHE_ALLOC_DELTA16 that was added
// by setFastInstanceSize
return align16(size + extra - FAST_CACHE_ALLOC_DELTA16);
}
}
// 16 字节对其算法
static inline size_t align16(size_t x) {
// 抹掉末尾的0,只保留 16 的倍数
return (x + size_t(15)) & ~size_t(15);
}

SwiftUI的拥有众多杀手级特性,其中一个就是 Xcode Preview 支持无需使用模拟器即可预览布局。让我们看看如何为你的UIKit视图控制器和视图添加支持,以便它们也可以与 Xcode Preview 一起使用。

预备知识

需要提前了解的一个坏消息是,Xcode Preview 对你的工程设置及电脑系统有严苛的要求。除了需要 Xcode 11+ 之外,你还需要将系统升级至 macOS Catalina,并且需要设置你的工程的最小 development target 支持版本为 iOS 13,你可以只在 Debug 环境下设置支持预览,这样就不用在发行版中添加不必要的程序。

预览一个 View Controller

在让 view controllers 正确使用 Xcode Previews 之前,需要以下准备:

阅读全文 »

最近研究家庭本地组网,大家普遍吐槽光猫自带的无线网信号和稳定性一般,建议改为桥接使用自己的路由。

自用电信天翼光猫,默认路由模式,改桥接需要打电话预约售后上面。不想麻烦别人,自己查资料,本文也是记录下自己的爬坑过程。

方案一:使用桥接/路由模式转换配置页(推荐)

阅读全文 »

线性表中,链表拥有更灵活的存储结构,不要求存储空间连续,对于插入和删除节点十分友好,但除了插入删除,其他大部分操作是将复杂度均为 O(n),需要在使用时考虑具体的场景。

  • 种类
    链表又包括单向链表、双向链表、循环链表等形式,其优缺点各不相同。

  • 应用场景

    • redis 中使用的跳表则是拥有多级索引的链表升级版本,在搜索、插入、删除等操作都非常高效。
    • 在工业级哈希表中,会使用链表实现桶结构,用于解决哈希冲突问题,被称为拉链法。(JDK 1.8之后采用链表+红黑树)

本文主要使用 Swift 实现双向链表,如下为代码实现。

阅读全文 »

顺序表(Sequence List),使用固定内存空间进行初始化的数据结构,可进行随机访问且时间复杂度为O(1)。

通常会直接使用系统标准库中的 Array 模拟,本文将使用通用化的数据结构描述顺序表。

如下为顺序表代码实现。

阅读全文 »

Q:手机连接电脑后不充电,并且一直断开重连。

A:Mac 中的 USB 进程卡死导致 USB 口无法响应,杀掉进程就好了。

执行以下命令就能杀死该进程。

1
sudo killall -STOP -c usbd
阅读全文 »

  • 报错问题
1
ERROR: Failed to download Chromium r588429! Set "PUPPETEER_SKIP_CHROMIUM_DOWNLOAD" env variable to skip download.

网上有很多解决方案,大多是修改 Chromium 下载源,或者使用 cnpm,再或者跳过下载,手动去官网下载 Chromium。

自测了一下都不太好用。

从 puppeteer 的 issue 中找到国际友人的解决方案。

  • 解决方案
1
$ sudo npm install puppeteer@1.8.0 --unsafe-perm=true --allow-root

亲测好用,记录一下。

众所周知,国内 GitHub 的加载速度不太理想,而大多数开发者会使用 Github 的 Github Page 功能搭建博客,因此博客的访问速度也会受到影响。为了解决访问速度的问题,我们将博客转移到了 gitee 中( OSChina 旗下 git 平台)

1. 问题:使用 hexo 部署到 gitee 后,博客页面并没有更新

  • 原因: gitee page 只有付费版才能自动更新,免费版只能手动点击 “设置” 中的更新按钮

2. 自动化解决方案

  • 使用 puppeteer 操作浏览器进行更新按钮点击。

3. 源码如下:

阅读全文 »

Visual Studio CodeVSCode)是一款微软推出的跨平台代码编辑器,并且有数百人定期更新维护的杰出的开源项目。VSCode 是最早对 Language Server Protocol (LSP) 进行支持的开发工具,并且在不同的语言技术中也提供了杰出的开发体验。

同此前宣布的目前已经发布的 Xcode,现在是一个很好的时机来将这个工具集成到自己的开发工作中。

阅读全文 »