0%

NSObject是OC中大部分类的基类,了解NSObject对象的初始化对于我们进一步理解对象内存的管理有一定帮助,本文主要探索allocinit底层实现。

如何debug源码

可以参考 https://juejin.cn/post/6844903959161733133

alloc

新建一个工程,在main方法里添加代码

1
NSObject *objc = [NSObject alloc];

添加一个断点符号 objc_alloc

运行工程,现在项目就会断点到objc_alloc方法

1
2
3
4
objc_alloc(Class cls)
{
return callAlloc(cls, true/*checkNil*/, false/*allocWithZone*/);
}

这里会调用 callAlloc方法,参数checkNil = true,allocWithZone = false,下面具体看callAlloc方法

callAlloc方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
{
#if __OBJC2__
//判断是否为空
if (slowpath(checkNil && !cls)) return nil;
//判断是否有allocWithZone方法
if (fastpath(!cls->ISA()->hasCustomAWZ())) {
return _objc_rootAllocWithZone(cls, nil);
}
#endif

// No shortcuts available.
if (allocWithZone) {
return ((id(*)(id, SEL, struct _NSZone *))objc_msgSend)(cls, @selector(allocWithZone:), nil);
}
return ((id(*)(id, SEL))objc_msgSend)(cls, @selector(alloc));
}

可以看出如果有自定义的allocWithZone方法就会调用_objc_rootAllocWithZone方法,后面会根据allocWithZone是否为true选择执行allocWithZone还是alloc方法。这里我们一步步走断点,最后调用的是alloc方法。

1
2
3
+ (id)alloc {
return _objc_rootAlloc(self);
}

alloc的方法调用了_objc_rootAlloc

1
2
3
4
_objc_rootAlloc(Class cls)
{
return callAlloc(cls, false/*checkNil*/, true/*allocWithZone*/);
}

可以看到_objc_rootAlloc调用的也是callAlloc方法,和上一次调用不同的是参数checkNil = true allocWithZone = true

第二次调用callAlloc方法

一步步执行断点看到这次执行到allocWithZone方法

1
2
3
4
5
6
7
8
9
+ (id)allocWithZone:(struct _NSZone *)zone {
return _objc_rootAllocWithZone(self, (malloc_zone_t *)zone);
}
_objc_rootAllocWithZone(Class cls, malloc_zone_t *zone __unused)
{
// allocWithZone under __OBJC2__ ignores the zone parameter
return _class_createInstanceFromZone(cls, 0, nil,
OBJECT_CONSTRUCT_CALL_BADALLOC);
}

可以看到核心代码在_class_createInstanceFromZone方法里

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
_class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone,
int construct_flags = OBJECT_CONSTRUCT_NONE,
bool cxxConstruct = true,
size_t *outAllocatedSize = nil)
{
ASSERT(cls->isRealized());

// Read class's info bits all at once for performance
bool hasCxxCtor = cxxConstruct && cls->hasCxxCtor();
bool hasCxxDtor = cls->hasCxxDtor();
bool fast = cls->canAllocNonpointer();
size_t size;

size = cls->instanceSize(extraBytes);
if (outAllocatedSize) *outAllocatedSize = size;

id obj;
if (zone) {
// 根据给定的 zone 和 size 开辟内存
obj = (id)malloc_zone_calloc((malloc_zone_t *)zone, 1, size);
} else {
// 根据 size 开辟内存
obj = (id)calloc(1, size);
}
if (slowpath(!obj)) {
if (construct_flags & OBJECT_CONSTRUCT_CALL_BADALLOC) {
return _objc_callBadAllocHandler(cls);
}
return nil;
}
// 这里 zone 传入的也是nil,而 fast 拿到的是 true,所以会进入这里的逻辑
if (!zone && fast) {
obj->initInstanceIsa(cls, hasCxxDtor);
} else {
// Use raw pointer isa on the assumption that they might be
// doing something weird with the zone or RR.
obj->initIsa(cls);
}

if (fastpath(!hasCxxCtor)) {
return obj;
}
// 如果有 C++ 初始化构造器和析构器,进行优化加速整个流程
construct_flags |= OBJECT_CONSTRUCT_FREE_ONFAILURE;
return object_cxxConstructFromClass(obj, cls, construct_flags);
}

_class_createInstanceFromZone方法主要调用了三个函数 instanceSizecallocinitInstanceIsa

先看instanceSize函数

1
2
3
4
5
6
7
8
9
10
inline size_t instanceSize(size_t extraBytes) const {
if (fastpath(cache.hasFastInstanceSize(extraBytes))) {
return cache.fastInstanceSize(extraBytes);
}

size_t size = alignedInstanceSize() + extraBytes;
// CF requires all objects be at least 16 bytes.
if (size < 16) size = 16;
return size;
}

这个函数的返回值是一个size_t的数据,在calloc函数中会用到这个返回值。即请求开辟的内存空间大小,并且开辟的空间大小不小于16。而且调用的了alignedInstanceSize函数来进行字节对齐。即开辟的内存空间大小一定是16字节的整数倍。

calloc是C语言的函数,开辟内存空间

initInstanceIsa函数 对象isa的初始化,以及绑定内存空间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
objc_object::initInstanceIsa(Class cls, bool hasCxxDtor)
{
ASSERT(!cls->instancesRequireRawIsa());
ASSERT(hasCxxDtor == cls->hasCxxDtor());

initIsa(cls, true, hasCxxDtor);
}
objc_object::initIsa(Class cls, bool nonpointer, UNUSED_WITHOUT_INDEXED_ISA_AND_DTOR_BIT bool hasCxxDtor)
{
ASSERT(!isTaggedPointer());

isa_t newisa(0);

if (!nonpointer) {
newisa.setClass(cls, this);
} else {
ASSERT(!DisableNonpointerIsa);
ASSERT(!cls->instancesRequireRawIsa());


#if SUPPORT_INDEXED_ISA
ASSERT(cls->classArrayIndex() > 0);
newisa.bits = ISA_INDEX_MAGIC_VALUE;
// isa.magic is part of ISA_MAGIC_VALUE
// isa.nonpointer is part of ISA_MAGIC_VALUE
newisa.has_cxx_dtor = hasCxxDtor;
newisa.indexcls = (uintptr_t)cls->classArrayIndex();
#else
newisa.bits = ISA_MAGIC_VALUE;
// isa.magic is part of ISA_MAGIC_VALUE
// isa.nonpointer is part of ISA_MAGIC_VALUE
# if ISA_HAS_CXX_DTOR_BIT
newisa.has_cxx_dtor = hasCxxDtor;
# endif
newisa.setClass(cls, this);
#endif
newisa.extra_rc = 1;
}

// This write must be performed in a single store in some cases
// (for example when realizing a class because other threads
// may simultaneously try to use the class).
// fixme use atomics here to guarantee single-store and to
// guarantee memory order w.r.t. the class index table
// ...but not too atomic because we don't want to hurt instantiation
isa = newisa;
}

init

init做的事情比较简单,直接将生成的对象返回

1
2
3
4
5
6
7
8
9
- (id)init {
return _objc_rootInit(self);
}
_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;
}

原地排序

原地排序算法,就是指空间复杂度是O(1)的排序算法。

稳定性

如果待排序的序列中存在值相等的元素,经过排序之后,相等元素之间原有的先后顺序不变那这个算法就是稳定的排序算法,反之就是不稳定的排序算法。

冒泡排序

冒泡排序只会操作相邻的两个数据。每次冒泡操作都会对相邻的两个元素进行比较,看是否满足大小关系要求。如果不满足就让它俩互换。一次冒泡会让至少一个元素移动到它应该在的位置,重复 n 次,就完成了 n 个数据的排序工作。

最好情况下时间复杂度:O(n)

最坏情况下时间复杂度:O(n²)

平均时间复杂度:O(n²)

算法代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

// 冒泡排序,a表示数组,n表示数组大小
public void bubbleSort(int[] a, int n) {
if (n <= 1) return;

for (int i = 0; i < n; ++i) {
// 提前退出冒泡循环的标志位
boolean flag = false;
for (int j = 0; j < n - i - 1; ++j) {
if (a[j] > a[j+1]) { // 交换
int tmp = a[j];
a[j] = a[j+1];
a[j+1] = tmp;
flag = true; // 表示有数据交换
}
}
if (!flag) break; // 没有数据交换,提前退出
}
}

插入排序

插入排序的思路是,首先,我们将数组中的数据分为两个区间,已排序区间和未排序区间。初始已排序区间只有一个元素,就是数组的第一个元素。插入算法的核心思想是取未排序区间中的元素,在已排序区间中找到合适的插入位置将其插入,并保证已排序区间数据一直有序。重复这个过程,直到未排序区间中元素为空,算法结束。

最好情况下时间复杂度:O(n)

最坏情况下时间复杂度:O(n²)

平均时间复杂度:O(n²)

算法代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

// 插入排序,a表示数组,n表示数组大小
public void insertionSort(int[] a, int n) {
if (n <= 1) return;

for (int i = 1; i < n; ++i) {
int value = a[i];
int j = i - 1;
// 查找插入的位置
for (; j >= 0; --j) {
if (a[j] > value) {
a[j+1] = a[j]; // 数据移动
} else {
break;
}
}
a[j+1] = value; // 插入数据
}
}

选择排序

选择排序算法的实现思路有点类似插入排序,也分已排序区间和未排序区间。但是选择排序每次会从未排序区间中找到最小的元素,将其放到已排序区间的末尾。

选择排序的最好情况时间复杂度、最坏情况和平均情况时间复杂度都为 O(n2)。

选择排序是一种不稳定的排序算法。

算法代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

public int[] selectionSort(int[] sourceArray) throws Exception {
int[] arr = Arrays.copyOf(sourceArray, sourceArray.length);

// 总共要经过 N-1 轮比较
for (int i = 0; i < arr.length - 1; i++) {
int min = i;
// 每轮需要比较的次数 N-i
for (int j = i + 1; j < arr.length; j++) {
if (arr[j] < arr[min]) {
// 记录目前能找到的最小值元素的下标
min = j;
}
}

// 将找到的最小值和i位置所在的值进行交换
if (i != min) {
int tmp = arr[i];
arr[i] = arr[min];
arr[min] = tmp;
}

}
return arr;
}

检测方法

工程根目录执行命令 grep -r advertisingIdentifier .

如果输出有”matches”标识项目中有使用

上线选择

  • 1、在 App 内投放广告 服务应用中的广告。如果你的应用中集成了广告的时候,你需要勾选这一项。
  • 2、将此 App 安装归因于先前投放的特定广告 跟踪广告带来的安装。如果你使用了第三方的工具来跟踪广告带来的激活以及一些其他事件,但是应用里并没有展示广告你需要勾选这一项。
  • 3、将此 App 中发生的操作归因于先前投放的特定广告 跟踪广告带来的用户的后续行为。如果你使用了第三方的工具来跟踪广告带来的激活以及一些其他事件。
  • 4、iOS 中的“限制广告跟踪”设置 这一项下的内容其实就是对你的应用使用 IDFA 的目的做下确认,只要你选择了采集 IDFA,那么这一项都是需要勾选的。

选择总结

  1. 如果你的应用里只是集成了广告,不追踪广告带来的激活行为,那么选择1和4;
  2. 如果你的应用没有广告,而又获取了IDFA。建议选择2和4;

转载于 https://blog.csdn.net/weixin_34392906/article/details/92404376

什么是sentry

1
sentry是一个强大的分布式报错信息收集平台,图形化程度很高,可以帮助我们直观的发现当前部署的系统存在哪些问题。

准备工作

官方提供了两种部署方案

出于方便考虑,这里选择了docker

安装docker(只是Mac)

mac版docker 下载地址: https://download.docker.com/mac/stable/Docker.dmg

安装完成后终端执行 docker --version来查看版本号,确认是否安装成功。

搭建sentry(linux和Mac通用)

  1. 获取sentry

    从GitHub上获取最新的sentry

    git clone https://github.com/getsentry/onpremise.git

  2. 搭建sentry

    sentry的readme文件里有步骤

    1. 创建本地数据库和sentry容器

      docker volume create --name=sentry-data && docker volume create --name=sentry-postgres

      Docker volumes have to be created manually, as they are declared as external to be more durable.

    2. 创建配置文件.env

      cp -n .env.example .env

    3. 编译docker服务

      docker-compose build

    4. 生成secret key

      docker-compose run --rm web config generate-secret-key

      将key配置到.env文件里,SENTRY_SECRET_KEY: ‘***

    5. 构建数据库,创建一个超级用户

      docker-compose run --rm web upgrade

      这步会提示创建超级用户,用户名是邮箱,记住用户名和密码后面要用到。

    6. 启动所有服务

      docker-compose up -d

    7. 浏览器中输入localhost:9000,即可访问sentry服务了,用户名和密码用第5步设置的。

iOS项目接入sentry

可以参考sentry官方文档https://docs.sentry.io/clients/cocoa/

  1. 导入sentry

    podfile文件加入

    pod 'Sentry', :git => 'https://github.com/getsentry/sentry-cocoa.git', :tag => '4.1.0'

    terminal执行pod install

  2. 项目配置

    appdelegate文件didfinishLaunch方法中,dsn登录sentry后在新建Objective-C项目里查看。

    1
    2
    3
    4
    5
    6
    7
    NSError *error = nil;
    SentryClient *client = [[SentryClient alloc] initWithDsn:@"https://<key>@sentry.io/<project>" didFailWithError:&error];
    SentryClient.sharedClient = client;
    [SentryClient.sharedClient startCrashHandlerWithError:&error];
    if (nil != error) {
    NSLog(@"%@", error);
    }

以上两步之后,就可以正常上传crash日志了

由于项目最近crash率比较高,为了更好地监控线上程序包的crash,所以开发将异常崩溃捕获发送钉钉消息的功能。

异常的捕获

通过使用NSSetUncaughtExceptionHandler方法将异常捕获然后进行处理。

在Appdelegate中定义如下函数,用来处理捕获到的异常。

1
2
3
4
5
6
7
8
9
10
11
void uncaughtExceptionHandler(NSException *exception)

{
// 异常的堆栈信息
NSArray *stackArray = [exception callStackSymbols];
// 出现异常的原因
NSString *reason = [exception reason];
// 异常名称
NSString *name = [exception name];
//TODO 发送钉钉消息
}

下面是捕获异常的代码,didFinishLaunchingWithOptions方法中写入:

1
NSSetUncaughtExceptionHandler(&uncaughtExceptionHandler);

到这里就可以进行异常捕获了,下面我们写一段让程序崩溃的代码进行测试

1
2
3
- (void)crashMethod{
[self performSelector:@selector(thisMthodDoesNotExist) withObject:nil];//调用一个不存在的方法。
}

到这里uncaughtExceptionHandler就可以捕获到应用的崩溃Exception了。

第三方冲突

如果项目里使用了友盟等第三方异常监控分析工具,会和我们NSSetUncaughtExceptionHandler方法冲突,导致第三方不会上传异常信息,解决方案是使用NSGetUncaughtExceptionHandler()获取到第三方捕获异常的Handler,当我们处理完异常将NSException对象传给第三方的Handler。

1
static NSUncaughtExceptionHandler *_previousHandler;//Appdelegate定义一个全局静态变量用来保存第三方的异常Handler

didFinishLaunchingWithOptions方法中在NSSetUncaughtExceptionHandler之前先保存之前的Handler:

1
2
3
// 保存第三方处理异常的handler
_previousHandler = NSGetUncaughtExceptionHandler();
NSSetUncaughtExceptionHandler(&uncaughtExceptionHandler);

最后在uncaughtExceptionHandler函数最后(即我们自定义的异常处理之后),将exception回传给第三方Handler。

1
2
3
4
//将exception回传给第三方Handler
if (_previousHandler) {
_previousHandler(exception);
}

给钉钉群发送消息

#####1、首先是在钉钉群里新建一个自定义机器人,获取到机器人的webhook。详细的步骤可以看钉钉API文档:

https://open-doc.dingtalk.com/docs/doc.htm?spm=a219a.7629140.0.0.Uf9uaN&treeId=257&articleId=105735&docType=1#s1

#####2、向机器人的webhook发送post消息。

下面是用AFNetworking发送post请求的代码,注意请求参数的格式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
   NSDictionary *dict = @{
@"msgtype":@"text",
@"text":@{@"content":self.errorMessage?self.errorMessage:@""},
@"at":@{
@"atMobiles":self.arrayAtPhones?self.arrayAtPhones:@[],
@"isAtAll":@NO
}
};
NSError *error;
NSData *jsonData = [NSJSONSerialization dataWithJSONObject:dict options:NSJSONWritingPrettyPrinted error:&error];
AFURLSessionManager *sessionManager = [[AFURLSessionManager alloc] init];
AFJSONResponseSerializer *responseSerializer = [AFJSONResponseSerializer serializer];
responseSerializer.acceptableContentTypes = [NSSet setWithObjects:@"application/json", @"text/json", @"text/javascript",@"text/html", @"text/plain", @"image/jpeg", nil];
sessionManager.responseSerializer = responseSerializer;
NSMutableURLRequest *req = [[NSMutableURLRequest alloc] initWithURL:[NSURL URLWithString:self.dingdingUploadUrl]];
[req setHTTPMethod:@"POST"];
[req setHTTPBody:jsonData];
//不设置会返回参数错误
[req setValue:@"application/json" forHTTPHeaderField:@"Content-Type"];
[[sessionManager dataTaskWithRequest:req uploadProgress:nil downloadProgress:nil completionHandler:^(NSURLResponse * _Nonnull response, id _Nullable responseObject, NSError * _Nullable error) {

}]resume];

到这里就可以正常给钉钉发送消息了。纯手敲难免有手误,哪里不准确的地方请指出。

demo工程地址:https://github.com/CoderJasons/-CatchCrashTest

什么是内存对齐

从一个例子开始

1
2
3
4
5
6
7
8
9
10
11
12
13
#inclued <iostream>
using namespace std;
struct A{
char a;
int b;
double c;
}A1;
void main(){
cout<<sizeof(A1)<<endl;
cout<<sizeof(A1.a)<<endl;
cout<<sizeof(A1.b)<<endl;
cout<<sizeof(A1.c)<<endl;
}

打印的结果是16,1,4,8。我们发现结构体占用内存的总量并不是其包含的所有元素占用内存的和,这是为什么呢,这就涉及到指针对齐的问题。对齐有两个原则:

1、结构体中元素是按照定义顺序一个一个放到内存中去的,但并不是紧密排列的。从结构体存储的首地址开始,每一个元素放置到内存中时,它都会认为内存是以它自己的大小来划分的,因此元素放置的位置一定会在自己宽度的整数倍上开始(以结构体变量首地址为0计算)。

​ 比如上面的例子,首先系统会将字符串变量a存入第0个字节(相对地址,指结构体内存开辟的首地址);然后存放整型变量b时,会以4个字节为单位进行存储,由于第一个四字节模块已有数据(a),因此它会存入第二个四字节模块,即4~8字节;同理,存放双精度实例变量c时会以8个字节为单位存储,也就是会找到第一个空的且是8的整数倍的位置开始存储,这个例子中由于第一个8字节模块已被占用,所以将c存入第二个8字节模块。

再看下面这个例子

1
2
3
4
5
struct A{
char a;
double b;
int c;
}A2;

这个例子只是互换了b和c的类型,但是结果确不同sizeof(A2)=24,按照第一个规则结果应该是20啊,这是为什么呢,下面就看第二个规则

2、在经过第一原则分析后,检查计算出的存储单元是否为所有元素中最宽的元素的长度的整数倍,是,则结束;若不是,则补齐为它的整数倍。

第二个例子根据第一条规则计算的结果是20但是不是double类型宽度(8)的整数倍,所以补齐到24。

内存对齐的作用

1、平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。

这点应该很容易理解,不做解释了。

2、 性能原因:经过内存对齐后,CPU的内存访问速度大大提升。

CPU把内存当成是一块一块的,块的大小可以是2,4,8,16字节大小,因此CPU在读取内存时是一块一块进行读取的。块大小成为memory access granularity(粒度)

假设CPU要读取一个int型4字节大小的数据到寄存器中,分两种情况讨论:

​ 1、数据从0字节开始

​ 2、数据从1字节开始

假设内存读取粒度为4。当数据从0字节开始时,CPU只需读取内存一次即可吧这4字节的数据完全读取到寄存器中。当该数据是从1字节开始时,问题变得有些复杂,此时该int型数据不是位于内存读取边界上,这就是一类内存未对齐的数据。

此时CPU先访问一次内存,读取0—3字节的数据进寄存器,并再次读取4—5字节的数据进寄存器,接着把0字节和6,7,8字节的数据剔除,最后合并1,2,3,4字节的数据进寄存器。对一个内存未对齐的数据进行了这么多额外的操作,大大降低了CPU性能。

这还属于乐观情况了,上文提到内存对齐的作用之一为平台的移植原因,因为以上操作只有有部分CPU肯干,其他一部分CPU遇到未对齐边界就直接罢工了。

参考链接

http://blog.csdn.net/liukun321/article/details/6974282/

http://www.cppblog.com/snailcong/archive/2009/03/16/76705.html

什么是markdown?

​ Markdown是一种可以使用普通文本编辑器编写的标记语言,通过简单的标记语法,它可以使普通文本内容具有一定的格式。

部分语法

标题
  • # 表示 H1 标签
  • ## 表示 H2 标签 以此类推
  • ###### 表示 H6 标签
代码区块
  • 使用```和语言名称进行标记。例如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    #import <Foundation/Foundation.h>

    int main(int argc, const char * argv[]) {
    @autoreleasepool {
    // insert code here...
    NSLog(@"Hello, World!");
    }
    return 0;
    }
脚注

​ 使用[^heyword]表示脚注。例如:

​ 这是正文[^这是注释]

表格

​ 使用|字段一|字段二|字段三|表示表格。例如:

单价 数量 金额
100 20 2000