文章

iOS进阶面试2023版本

近期总结

感恩网易,感恩阿里,感恩现公司。

劝退应届生做移动端,劝退,劝退。

iOS面试总结

上一篇较完整的基础知识总结,见iOS相关基础看这篇就够了

自我介绍

这一块结合简历进行阐述即可,简历上提到的东西包括设计的基础一定要熟练记住,因为可能涉及太久远会有些遗忘。

八股文

阿里八股文

【简要的总结一下,在近几家公司的主要成就和核心技术贡献,不超过3件事?】

【这几件事的关键结果是什么?如何用一个相对技术完备的视角去评估,项目结果是否足够好?】

【听到都是好的部分,项目过程和结果有什么不足?有没延续思考以及实际改善行动?】

【为了更好的达成项目目标,有没给下游团队提出过要求?讲讲过程和结果?】

【除了工作中既定目标,有没有给自己设定更高的要求?讲讲过程和结果?】

【除了既定产品需求之外,有没有通过技术驱动产品创新的想法,是因为你的想法带来的变化?】

【能不能举个技术上比较深入、追求极致、精益求精的案例?讲讲过程和结果?】

【感觉最近几年技术成长怎么样?成长主要在哪里啊?有什么瓶颈?】

【是什么时间发现自己有瓶颈感的?为什么之前没有行动?如何突破瓶颈的?】

【觉得自己的最核心优势在哪里?哪些方面不足或需要提升吗?】

【提到的不足的部分,有没有采取实际行动,尝试去改变?】

【遇到工作和人生遇最大的挫折是什么?如何调整心态去应对的?】

【有没有因为XXX吃过亏?如何应对的?】

【你来阿里巴巴想为什么?适合做什么?灵魂拷问,你在技术上的追求和理想又是什么?】

【往年绩效、特别荣誉奖项?为什么绩效不好?为什么没拿到S3.75,期间有没做过什么努力?】

【离职原因?如何阿里巴巴面试失败,还留在这家公司的话,你接下来会怎样规划和变化?】

【未来规划】

如果进行复盘的话,你会怎么做

主要成就和核心技术贡献

从面试官角度考虑面试

碰到有挑战的

解决了什么难点问题

项目中碰到的创新

weex的流程

个人工作经历,所以可能会被问到,总结如下:

  1. 离线编译流程:

    Weex的编译产物是二进制。通过def+rax的插件,可以将产物构建成二进制文件wlm,低版本兜底weex1.0版本js,兜底weex 2.0版本js,h5降级产物。

  2. 端上拿到weex资源链接之后:

    根据请求头的accept字段获取不同类型资源。优先字节码,其次环境不支持版本不匹配的情况执行兜底js代码,如果引擎初始化有问题,就会使用h5降级。

    资源下载的过程中,Weex容器启动内部的脚本引擎、渲染引擎,并初始化各种资源(数据预取、字体库、Skia Shader预热等)。

  3. 下载好二进制文件之后,交给引擎执行,如果是js源码,会有额外的Parse过程,字节码的话就直接解码。

  4. 构建前端框架的vdom树,调用DOM API,构建DOM Tree(C++),生成CSSOM(匹配样式表计算出节点样式)。

  5. 下一步到渲染引擎,根据CSSOM,展开成RenderObject树(实现CSS布局和绘制算法),生成Layer Tree,绘制layer,光栅化,合成,最后提交GPU完成渲染。

  6. 图片加载过程,在图片请求回来加载完整之后,会重新触发布局、重新绘制以及后续的合成和光栅化。

  7. 用户交互操作,也是根据端上响应链机制找到目标节点,然后触发事件回调给到引擎执行相关的JS代码,如果JS代码中更新了节点,例如修改的DOM属性或是样式,则会差量更新RenderObject树,然后触发重排、重绘、合成光栅化等一系列操作。

简单讲就是: 执行脚本 —> 构建节点 —> 布局/绘制 —> 合成/光栅化 —> 渲染上屏

lua热修复详解

个人工作经历相关,总结如下:

脚本语言中运行速度最快的是 Lua,lua是基于寄存器的虚拟机实现(更简单,更高效),python是基于堆栈的,都是动态数据类型

wax 只支持32位 线程保护不好

vc替换doSomeThing这件事,lua怎么传递给OC那边的?

当OC在执行doSomeThing的时候,如何执行到lua实现的版本的?

lua调用函数的时候,又如何转换成调用OC的函数?

  1. lua元表

    lua元表metatable特性, 当读取表中不存在某个key的时候,会转移到设置的元表中__index所指向的地方__

    当设置一个不存在key的值的时候,会转移到关联的元表的__newindex中,key对应的value可以是function

    这个就相当于是钩子的实现了,function三个参数比较关键,1 table 2 key 3value

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
local fan = {}
print(fan.money) -- nil
local mis = { money = 100}
print(mis.money) -- 100
local _M = { __index = mis }
setmetatable(fan, _M)
print(fan.money) -- 100
fan.bonus = 10
_M = { __newindex = function(t, k, v) 
   mis.bonus = mis.money + v
  end
}

setmetatable(fan, _M)
fan.bonus = 200
print(fan.bonus) -- nil
print(mis.bonus) -- 300

元表中还有其他,比如说__call __add __sub __eq 帮我们做一些很高级的事情

比如想调用fan(),这个时候就会报错调用不了

1
2
3
4
5
6
7
8
9
local _M = { __call = function() 

   print("fan")

  end}

setmetatable(fan, _M)

fan()
  1. lua环境,另一个高级特性

就类似代码块中的一个环境,局部环境,lua有个全局的环境叫_G,是全局的,在调用print的时候,默认是_G.print

setfenv(1, {}) – 1改变当前作用域的环境 2 改变外层作用域

所以通过setfenv就更改方法调用的环境

1
2
3
4
5
6
7
8
9
10
11
12
function f()

 local t = {}

 setmetatable(t, { __newindex = function (t,k,v) 

	 print(k)
   end})

 setfenv(2, t)

end

f() – 先将外层环境设空,然后关联一个元表,并赋值__newindex,这样在该环境中赋值的时候

function abc() – 控制台直接打印abc

end

– 其实就是类似 _G.abc = function() end

  1. c 调用 lua

比如lua写了一个add的方法,通过c实现的lua api,luaL_loadfile将lua脚本,加载到lua的虚拟机里面,这样lua在全局的环境里面就有了一个add的方法

然后在c里面通过lua api:lua_getGloable找到这个添加的add方法,然后lua_push添加执行参数,最后用lua_pcall进行执行

(栈原理)

  1. lua 调用 c

比如首先我们要有一个c的函数在,myadd,在lua里面要调用的话,首先需要注册到lua的虚拟机里面,luaL_newlib,luaL_register方法

lua中注册c函数的固定格式

typedef int (*) (lua_State *)

  1. demo解析
1
2
3
4
5
6
7
8
9
spa_class("ViewController")



function doSomeThing(self)

	self:view():setBackgroundColor_(UIColor:grayColor())

end

首先会在执行我们的热修复脚本之前,会预装载一些lua方法进入到lua虚拟机里面,并且向lua虚拟机里面注册一些c方法,比如create初始化环境

整个热修复lua脚本第一行,是取出需要进行方法替换的类的类名,将他设置在userdata中(lua中是table,key-value;c中就是一个结构体指针),同时也会给userdata设置一个关联的元表,这个元表中的index和newindex就是指向c语言注册的两个方法__index__newindex,并将这个设为外层的环境

第二行写的就是需要替换的方法函数,通过lua语言特性,这就是向当前环境(第一行说的环境)的元表中设置key=方法名的,value=函数,这样就会走到我们第一行替换的__newindex中,取三个参数,1 userdata中的class名,2 方法名 3 函数,首先判断是函数才执行的逻辑,接着就是我们熟悉的runtime方法替换了,判断这个类里面是否有这个方法,将origin method添加为另外一个方法,swizzle forwardInvocation,然后将其指向_objc_msgForward(后面调用就会走到我们自己写的forwardInvocation中)。最后将lua function存储到class的userdata的环境中

以上就是脚本注入方法替换的过程

后面在方法执行的时候,就会走到我们的forwardInvocation中,会从class userdata环境中提取出lua function,push到栈上面

invocation中根据函数签名提取参数,转换成lua类型,push到栈(C调用lua的方式就是把函数push到栈,再把参数push到栈,然后lua_pcall的形式进行执行)

最后就是lua_pcall完成lua function的调用

然后就是lua的函数体执行

self:view() ====> 其实就是 f=self.view f() self即是userdata,拿view这个key的时候就会走到我们预先注册的c的__index中,获取到这个key找到selector

这个时候我们push一个函数invoke到调用栈中,这个c函数会被lua调用

在invoke中我们取得到环境中的类名,取得到__index的执行的点方法的名字view,然后封装成NSInvocation去执行方法(当然如果有参数还涉及到lua的参数转化为oc的参数)

将返回值转换成lua类型(新的userdata),返回给lua

持续调用以上过程完成最终的调用

(其中UIColor就是走到了我们预先埋入的lua方法里面去创建一个UIColor的元表(同ViewController元表的创建),然后执行以上方法)

  1. 参数转换

lua到oc转换:参数持久化,是用一个对象持有住参数,然后将对象放到自动释放池中,在对象delloc的时候去执行free

lua调用C

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
// mylib.c
#include <stadio.h>
#include <lua.h>
#include <lualib.h>
#include <luaxlib.h>
#include <math.h>

static int myadd(lua_State *L) {
	int a = luaL_checknumber(L, 1);
	int b = luaL_checknumber(L, 2);
	lua_pushnumber(L, a+b);
	return 1;
}

static const struct luaL_Reg mylib [] = {
	{"add", myadd},
	{NULL, NULL}
};

int luaopen_mylib(lua_State *L_ {
	luaL_newlib(L, mylib);
	return 1;
}

//call.lua
#!/usr/local/bin/lua
lib = require "mylib"
print(lib.add(1,2))

c调用lua

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
// test.lua
function add(x, y)
	return x+y
end

//test.c
#include <stdio.h>
#include <lua.h>
#include <lualib.h>
#include <lauxlib.h>
#include <math.h>

int main() {
	lua_State *L = luaL_newstate();
	luaL_openlibs(L);

	if(luaL_loadfile(L, "test.lua") || lua_pcall(L,0,0,0)) {
		printf("error %s\n", lua_tostring(L, -1));
		return -1;
	}
	lua_getglobal(L, "add");
	lua_pushnumber(L, 10);
	lua_pushnumber(L, 20);
	if (lua_pcall(L, 2, 1, 0) != 0) {
		printf("error %s\n", lua_tostring(L, -1));
		return -1;
	}
	double z = lua_tonumber(L, -1);
	printf("z = %f \n", z);
	lua_pop(L, 1);
	lua_cloase(L);
	return 0;
}

占内存大小

iOS的内存对齐是按照最大对齐字节数进行对齐的。在ARM架构下,是按8字节对齐的。

例如结构体重包含一个4字节的int和2字节的short,则结构体大小应该为6字节,但是iOS会将其对齐到8字节,即结构体大小为8字节。

内存对齐可以提高内存读写的效率和减少内存浪费

1
2
3
4
5
@interface ClassA : NSObject {
  int a;
  int b;
}
@end

占内存大小16个字节,其中isa占8个字节,a和b各占4个字节,总共16字节,如果是32位机型就是12字节,isa占4个字节。

注释掉一个b还是占用16,因为是按8进行对齐的,实际总共12,对齐到16。再注释掉a,就是8字节。

增加一个属性

@property (nonatomic, assign) int c; // 则变成24,实际是20,向8对齐。

增加NSString *d属性,是8字节。然后再算对齐。

增加一个函数方法,不增加内存大小

可以用class_getInstanceSize进行计算

iOS中的对象都是使用动态内存分配的,即在运行时动态分配内存。对象的内存分配首先通过alloc方法分配对象存储空间,然后通过init方法对对象进行初始化。

当我们调用一个类的alloc方法时,它会在内存中分配一块足够大的地方,用来存储该类的实例变量。大小由类中的实例变量的数量和类型决定。

分配的内存包括一个isa指针,指向该对象的类,以及实例变量所占用的内存,isa指针是指向其类的指针,他是一个指针变量,因此在分配内存时为骑分配内存空间。

alloc只分配内存,不初始化实例变量。如果没有调用init方法,实例变量可能包含未定义的值。

多线程奔溃

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

@property(nonatomic, strong) id a;

@end



ClassA *test = ClassA.new;

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

  for (int i = 0; i < 100; I ++) {

		test.a = NSObject.new; // 会有bad_access崩溃,改成atomic就没事了

  }

});

多线程同时修改,或是修改的时候又访问一块内存,可能出现竞态条件(race condition)或是叫数据竞争的情况,由于执行顺序的不确定性,导致最终结果出现错误。

在多线程同时赋值一块内存时,如果不进行同步处理,可能就会出现:

  1. 读写冲突:一个线程正在读取内存中的值,另一个线程正在进行修改,导致读取的值不是最新的,出现错误结果。

  2. 写写冲突:多个线程同时修改同一块内存,导致最终的结果不可预测,可能会导致程序崩溃。

为了避免这种情况,就需要使用同步机制,加锁或是gcd信号量的形式,来协调多线程的执行顺序

设计一个监控主线程卡顿的方法

  1. 获取主线程的Runloop,通过在runloop里面安装Observer来监测RunLoop是否卡顿

  2. 通过在Observer的回调方法里面计算两次回调时间差来判断是否卡顿。

  3. 如果时间差超过阈值,则记录当前卡顿的时间戳和堆栈信息,并通过日志或Crash上报等方式通知开发人员。

https://blog.csdn.net/u011774517/article/details/116736919

https://www.jianshu.com/p/7829eef6e548

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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
CFRunLoopObserverContext context = {0,(__bridge void*)self,NULL,NULL};

runLoopObserver = CFRunLoopObserverCreate(kCFAllocatorDefault,kCFRunLoopAllActivities,YES,0,&runLoopObserverCallBack,&context);



//如果主线程的runloop中common模式中没有观察对象,则添加

if (!CFRunLoopContainsObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes))

{

   CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);

}     



static void runLoopObserverCallBack(CGRunLoopObserverRef observer, CGRunLoopActivity activity, void *info) {

	runLoopActivity = acvitity;

	dispatch_semaphore_signal(_semaphore)

}

//创建子线程监控

dispatch_async(dispatch_get_global_queue(0, 0), ^{

  //子线程开启一个持续的 loop 用来进行监控

  while (YES) 

  {

    long semaphoreWait = dispatch_semaphore_wait(dispatchSemaphore, dispatch_time(DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC));

    if (semaphoreWait != 0) 
    {

    //没有观察者,重置一下标识

      if (!runLoopObserver) 

      {

        timeoutCount = 0;

        dispatchSemaphore = 0;

        runLoopActivity = 0;

        return;

      }

      //BeforeSources 和 AfterWaiting 这两个状态能够检测到是否卡顿

      if (runLoopActivity == kCFRunLoopBeforeSources || runLoopActivity == kCFRunLoopAfterWaiting) {

        //可以设置允许的卡顿次数,一般都是允许一次

         if (++_countTime < 5)

           continue;

        //将堆栈信息上报服务器

         [self uploadStack];

      }

    }

    timeoutCount = 0;

  }

});

组件化的分层思想

https://wenku.baidu.com/view/0a1f908e25fff705cc1755270722192e453658cd.html?wkts=1683861340635&bdQuery=%E7%BB%84%E4%BB%B6%E5%8C%96%E7%9A%84%E5%88%86%E5%B1%82%E5%8E%9F%E5%88%99+%E5%8D%95%E4%B8%80%E8%81%8C%E8%B4%A3%E5%8E%9F%E5%88%99

组件化是一种体系结构模式,它将应用程序划分为独立的模块,每个模块称为组件。这些组组件可以独立开发、测试、部署和维护,也可以组装成完整的应用程序。组件化体系结构可以带来许多优点,如提高代码的可重用性、可维护性、可靠性等。那么,在组件化体系结构中,需要遵循哪些原则呢?下面是组件化的一些原则。

\1. 单一职责原则 2开闭原则 3.接口隔离原则 4.依赖倒置原则 5.迪米特法则 6.模块化原则

\1. 单一职责原则

单一职责原则指的是一个组件应该只有一个功能,而不是混合多个功能。使用单个A负责的组件更容易测试、维护和修改,并且还可以简化代码的设计和结构。如果在一个组件中有太多的函数,很容易使代码难以理解和维护,并且还会增加出错的风险。

2开闭原则

开闭原则指的是组件应该对扩展开放,对修改关闭。也就是说,在组件的设计过程中,应该考虑未来的可扩展性。将组件的各个部分分开,允许每个部分独立发展,而不会影响其他部分。这样,当需求发生变化时,只需要添加新的功能或组件,而不需要修改现有的功能或组件,修改功能或组件以保持其稳定性和可维护性。

3.接口隔离原则

接口隔离原则意味着组件之间的接口应该被最小化和细化,并且不包括不必要的工作。这可以保持组件之间的独立性和可伸缩性。如果组件之间的接口过于复杂或过于庞大,将给开发人员带来负担和困难,并增加出错的风险。

4.依赖倒置原则

依赖倒置原则是指组件之间的依赖关系应该从抽象到具体,也就是说组件应该依赖于其他组件的抽象接口,而不是具体的实现。这减少了组件之间的耦合,增加了代码的可维护性和可伸缩性。

5.迪米特法则

迪米特法则是指一个组件只应与最小依赖组件相互作用,即最小化组件之间的相互影响。这可以降低代码的复杂性和错误风险,并增加其可伸缩性和可维护性。在组件化体系结构中,迪米特法则也被称为“最少知识原则”。

6.模块化原则

模块化原则是指将组件分成不同的模块,每个模块都应该具有高内聚性和低耦合性。高内聚意味着一个模块内的所有功能都应该紧密相关,而低耦合意味着模块之间的依赖关系应该尽可能的紧密,可以小一些。模块化可以提高代码的可维护性、可重用性和可测试性。

nil Nil NULL NSNull的区别

https://juejin.cn/post/6844903440259219464

nil、Nil、NULL、NSNull的区别

nil:指向一个对象的空指针,对objective c id 对象赋空值

Nil:指向一个类的空指针,表示对类进行赋空值.

NULL:指向其他类型(如:基本类型、C类型)的空指针, 用于对非对象指针赋空值.

NSNull:在集合对象中,表示空值的对象.

nil:

NSString *str = nil;

NSURL *url = nil;

id object = nil;

Nil:

Class Class1 = Nil;

Clsss Class2 = [NSURL class];

NULL:

int *intA = NULL;

char *charC = NULL;

struct structStr = NULL;

NSNull:

NSMutableDictionary *mutableDictionary = [[NSMutableDictionary alloc] init];

[mutableDictionary setObject:nil forKey:@”Key-nil”]; // 会引起Crash

[mutableDictionary setObject:[NSNull null] forKey:@”Key-nil”]; // 不会引起Crash

//所以在使用时,如下方法是比较安全的

[mutableDictionary setObject:(nil == value ? [NSNull null] : value)

​ forKey:@”Key”];

NSArray *array = [NSArray arrayWithObjects:

​ [[NSObject alloc] init],

​ [NSNull null],

​ @”aaa”,

​ nil,

​ [[NSObject alloc] init],

​ [[NSObject alloc] init], nil];

NSLog(@”%ld”, array.count); // 输出 3,NSArray以nil结尾

拼多多笔试题,排列若干个字符串,判断是否首尾可以连接在一起

https://blog.csdn.net/rhx_qiuzhi/article/details/52402853

https://www.jianshu.com/p/7d6f5a3814f7

网上搜到的解答

https://blog.csdn.net/Bazinga521/article/details/97623572

https://blog.csdn.net/qq_40742428/article/details/99286800

有向图无向图介绍

https://zhuanlan.zhihu.com/p/135094687

我自己使用的是递归算法

富文本超链接实现

使用UITextView来实现,AttributeText有个属性叫NSLinkAttributeName,可以触发UITextView的回调

https://blog.csdn.net/MinggeQingchun/article/details/77894277

识别出文本中的超链,可以使用正则表达式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
NSError *error;

  NSString *regulaStr = @"\\bhttps?://[a-zA-Z0-9\\-.]+(?::(\\d+))?(?:(?:/[a-zA-Z0-9\\-._?,'+\\&%$=~*!():@\\\\]*)+)?";

  NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:regulaStr

                                      options:NSRegularExpressionCaseInsensitive

                                       error:&error];

  NSArray *arrayOfAllMatches = [regex matchesInString:string options:0 range:NSMakeRange(0, [string length])];

   

  for (NSTextCheckingResult *match in arrayOfAllMatches)

  {

    NSString* substringForMatch = [string substringWithRange:match.range];

     NSLog(@"substringForMatch");

  }

https://www.jianshu.com/p/f5212429d786

https://blog.csdn.net/xuchaovip/article/details/38664039

Pan手势实现自制scroll的滚动效果

Frame和bounds,修改自己的bounds就能实现活动效果

C内存 指针加减1问题

NSInteger a=10

NSInteger b=20

&b-&a = -1

内存分配是高地址向低地址分配

所以 &b-&a = -8byteCode,实际输出-8 / 8 = -1

同类指针(当然也只有同类指针允许相减,如pa3和pa0)相减得到的整数值,等于两指针减的距离除以sizeof(声明指针的类型),然后取整(此处即static_cast((pa3 - pa0) / sizeof(int)))

https://blog.csdn.net/onlyou930/article/details/6725051

OC内存对齐原理

https://juejin.cn/post/6971676327036321823

Zombie Objects(僵尸对象)

https://www.jianshu.com/p/493f581d336b

系统在回收对象时,可以不将其真的回收,而是把它转化为僵尸对象。这种对象所在的内存无法重用,因此不可遭到重写,所以将随机变成必然。

1
2
3
4
5
6
7
// Replaced by NSZombies

- (void)dealloc {

  _objc_rootDealloc(self);

}

系统会修改对象的 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
// 获取对象class

Class cls = object_getClass(self);

// 获取对象类名

const char *clsName = class_getName(cls);
// 检测是否带有前缀_NSZombie_

if (string_has_prefix(clsName, "_NSZombie_")) {

  // 获取被野指针对象类名

  const char *originalClsName = substring_from(clsName, 10);

  // 获取当前调用方法名

  const char *selectorName = sel_getName(_cmd);

  // 输出日志

  Log(''*** - [%s %s]: message sent to deallocated instance %p", originalClsName, selectorName, self);

  // 结束进程

  abort();

}

Instrument工具使用

https://juejin.cn/post/6865102561507672077#heading-11

Leaks(泄漏):一般的查看内存使用情况,检查泄漏的内存,并提供了所有活动的分配和泄漏模块的类对象分配统计信息以及内存地址历史记录;查看内存泄露,比如循环引用导致的

Time Profiler(时间探查):执行对系统的 CPU上运行的进程低负载时间为基础采样。可以查看某个函数的执行时间

Allocations(内存分配):跟踪过程的匿名虚拟内存和堆的对象提供类名和可选保留/释放历史;查看内存分配的情况,也可以通过xcode的debug memory graph查看

Activity Monitor(活动监视器):显示器处理的 CPU、内存和网络使用情况统计;

Blank(空模板):创建一个空的模板,可以从 Library 库中添加其他模板;

Core Data:监测读取、缓存未命中、保存等操作,能直观显示是否保存次数远超实际需要。

Network:跟踪 TCP/IP 和 UDP/IP 连接。

Engergy Log: 应用的电量消耗情况

Zombies: Zombies用户寻找僵尸对象。

gif和连续动图实现

利用的是UIImageView的animationImages、animationDuration、animationRepeatCount属性,会轮播图片

gif也是先将每帧都解析出来成一张张图片之后用这个方法实现

https://juejin.cn/post/6844903463583907848

常见纠错题

1
2
3
4
5
6
7
8
9
NSArray *items = @[@1, @2, @3];

  int i = -1;

  for (; i < items.count; i++) {

    NSLog(@"%d", i);

  }

for循环一次都不会走进去,数组的count是NSUInteger类型的,-1与其比较时隐式转换成NSUInteger,变成了一个很大的数字

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@implementation Son : Father

- (id)init

{

  self = [super init];

  if (self)

  {

    NSLog(@"%@", NSStringFromClass([self class]));

    NSLog(@"%@", NSStringFromClass([super class]));

  }

return self;

}

@end

两个log输出都是Son,根据消息转发机制,会去找到IMP,然后交由receiver执行,super的有个参数receiver就是self

https://blog.yorkfish.me/2020/Objective-C%E7%9A%84%E4%B8%80%E9%81%93%E9%A2%98%EF%BC%9A[self%20class]%20%E4%B8%8E%20[super%20class]/readme/

swift消息机制

静态派发 函数派发 消息派发

Metaclass isKindOfClass isMemberOfClass

https://juejin.cn/post/6844903460895211533

https://www.leewong.cn/2021/01/01/rumtime-metaclass/

object 的isa指向class,class的isa指向metaClass,class的superClass指向父类

class类方法返回自己,实例方法返回isa,也就是Class

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
48
49
50
51
52
53
54
55
+(Class)class {

  return self;

}

-(Class)class {

  return object_getClass(self);

}

Class object_getClass(id obj) {

  if (obj) return obj->getIsa();

  else return Nil;

}


+(BOOL)isKindOfClass:(Class)cls {

  for (Class tcls = self->ISA(); tcls; tcls = tcls->superclass) {

    if (tcls == cls) return YES;

  }

  return NO;

}



-(BOOL)isKindOfClass:(Class)cls {

  for (Class tcls = [self class]; tcls; tcls = tcls->superclass) {

   if (tcls == cls) return YES;

  }

  return NO;

}

+(BOOL)isMemberOfClass:(Class)cls {
  return self->ISA() == cls;
}

-(BOOL)isMemberOfClass:(Class)cls {
  return [self class] == cls;

}

isKindOfClass类方法遍历从原类开始,递归查找父原类是否等于目标

实例方法是遍历从类开始,递归查找父类是否等于目标

isMemberOfClass类方法是判断自己的原类是否等于目标类

实例方法是判断自己的类是否为目标类

TCP和UDP的区别

https://www.freecodecamp.org/chinese/news/tcp-vs-udp-which-is-faster/

UDP 是一种无连接协议,而 TCP 是一种面向连接的协议。TCP 比 UDP 要慢,这是两种协议的主要区别之一。

总的来说,UDP 是一种更快、更简单、更高效的协议。但是只有 TCP 允许对丢失的数据包进行重新传输。

TCP 和 UDP 的另一个区别是 TCP 可以确保数据从用户到服务器的有序传输(反之亦然)。UDP 不是为端到端通信而设计的,并不会检查接收方的准备情况,因此它需要相对更少的开销并占用更少的空间。

TCP和UDP能共用一个端口嘛

可以的

TCP和UDP是两种不同的传输协议,它们有不同的特性和用途,通常情况下不能共用一个端口。每个端口在一个给定的时间只能由一个协议使用。

TCP(传输控制协议)是一种可靠的、面向连接的协议,它确保数据的可靠传输,包括错误检测和重传机制。TCP使用的端口和UDP使用的端口是独立的,它们有自己的端口范围。

UDP(用户数据报协议)是一种无连接的协议,它不提供数据的可靠传输,不保证数据的顺序和完整性。UDP也有自己的端口范围,与TCP的端口范围不同。

因此,通常情况下,一个端口要么由TCP使用,要么由UDP使用,而不会同时由两者共用。但是,有一些特殊情况下可以实现TCP和UDP共用一个端口,这通常涉及到多路复用技术或应用程序设计的特殊处理。例如,一些应用程序可能会在同一个端口上同时监听TCP和UDP流量,但它们会根据数据包的协议字段将流量分发到不同的处理程序。这种情况需要应用程序设计者明确支持并实现这样的共用方式。

https://www.51cto.com/article/714728.html

本文由作者按照 CC BY 4.0 进行授权