该文章主要整理一些小知识点,主要涉及 iOS 以及计算基础相关知识点,某些知识点暂时只有标题,后续会持续更新。笔者最近一段时间面试过程中发现一些普遍现象,对于一些很不起眼的问题,很多开发者都只停留在知道、听说过的层面,但是一旦问 是什么 和 为什么 ,很多应试者回答的并不理想,比如下面的几个问题:
CALayer代替视图组件,如果某天产品改需求,要求添加触发事件,那么CALayer上怎么添加触发事件?userAgent, 请问 userAgent 是什么?(问过几次,纯 iOS 开发者没几人知道只说有印象)View 和 Model是完全独立开来的,很多开发者都说自己使用的是 MVC 模式,当问起:为什么实际开发中自定义视图组件时通常都会引入 Model ,并重写 setModel 方法?这还是不是 MVC ?[A addSubView:B]、[A addSubView:C]、C.userInteractionEnabled = NO,其中 B 视图和 C 视图有重叠,请问:B 视图添加点击事件能否响应?多数应试者第一反应是不能,结合响应链流程来看,答案显然是错误的。以上仅是部分典型小知识点,更多内容请详看此文。
两种方法: convertPoint和hitTest:,hitTest: 返回的顺序严格按照图层树的图层顺序。
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
CGPoint point = [[touches anyObject] locationInView:self.view];
CGPoint redPoint = [self.redLayer convertPoint:point fromLayer:self.view.layer];
if ([self.redLayer containsPoint:redPoint]) {
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"point red" message:@"" delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil];
[alert show];
}
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
CGPoint point = [[touches anyObject] locationInView:self.view];
CALayer *layer = [self.view.layer hitTest:point];
if (layer == self.redLayer) {
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"point red" message:@"" delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil];
[alert show];
}else if (layer == self.yellowLayer){
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"point yellow" message:@"" delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil];
[alert show];
}
}
堆空间的存在主要是为了延长对象的生命周期,并使得对象的生命周期可控。
从 64bit 开始,iOS 引入了Tagged Pointer技术,用于优化NSNumber、NSDate、NSString等小对象的存储。在没有使用Tagged Pointer之前, NSNumber等对象需要动态分配内存、维护引用计数等,NSNumber指针存储的是堆中NSNumber对象的地址值;使用Tagged Pointer之后,NSNumber指针里面存储的数据变成了:Tag + Data,也就是将数据直接存储在了指针中。当指针不够存储数据时,会使用动态分配内存的方式来存储数据。

UIWebView 默认开启了WebKitAllowUniversalAccessFromFileURLs 和 WebKitAllowFileAccessFromFileURLs 属性。利用这个漏洞给某个 App 下发一个 HTML 文件,当 UIWebView 使用 file 协议打开这个 HTML 文件, HTML 文件中含有一段窃取用户数据的 JS 代码,就会导致用户数据泄露。
NSString *filePath = [[NSBundle mainBundle] pathForResource:@"index" ofType:@"html"];
_webView = [[UIWebView alloc] initWithFrame:self.view.bounds];
[_webView loadRequest:[NSURLRequest requestWithURL:[NSURL fileURLWithPath:filePath]]];
<!DOCTYPE html>
<html>
<body>
<script>
// 这个可以是手机任意一个文件地址
var localfile = "/etc/passwd"
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
if (xhr.readyState == 4) {
alert(xhr.responseText);
}
}
try {
xhr.open("GET", localfile, true);
xhr.send();
} catch (ex) {
alert(ex.message);
}
</script>
</body>
</html>
上面代码可以读取出手机端 /etc/passwd 的文件。这个漏洞访问其他应用的数据,而不必需要用户的许可。但WKWiebView的 WebKitAllowUniversalAccessFromFileURLs 和 WebKitAllowFileAccessFromFileURLs 默认是关闭的(可以手动控制),不会存在这样的风险。
补充:针对 https 请求UIWebView需要做额外处理,借助NSURLConnection做证书验证,而WKWebView无需做过多额外处理。
缓存原因参考苹果官方文档:
Creating a date formatter is not a cheap operation. If you are likely to use a formatter frequently, it is typically more efficient to cache a single instance than to create and dispose of multiple instances. One approach is to use a static variable.
通知 NSNotification 在注册者被回收时需要手动移除,是一直以来的使用准则。原因是在 MRC 时代,通知中心持有的是注册者的 unsafe_unretained 指针,在注册者被回收时若不对通知进行手动移除,则指针指向被回收的内存区域,变为野指针。此时发送通知会造成 crash 。而在 iOS 9 以后,通知中心持有的是注册者的 weak 指针,这时即使不对通知进行手动移除,指针也会在注册者被回收后自动置空。因为向空指针发送消息是不会有问题的。
[UIImage imageNamed:] 传了 nil 或者传入@"",控制台会输出[framework] CUICatalog: Invalid asset name supplied: '(null)'。通过符号断点可定位。

NSDictionary *dict = @{@1: @"1",
@2: @"2",
@3: @"3",
@4: @"4"};
[[NSUserDefaults standardUserDefaults] setObject:dict forKey:@"key"];
[[NSUserDefaults standardUserDefaults] synchronize];
执行上述代码会报如下错误:
[User Defaults] Attempt to set a non-property-list object {
3 = "3";
2 = "3";
1 = "1";
4 = "4";
} as an NSUserDefaults/CFPreferences value for key `key`
The value parameter can be only property list objects: NSData, NSString, NSNumber, NSDate, NSArray, or NSDictionary. For NSArray and NSDictionary objects, their contents must be property list objects.
......
And although NSDictionary and CFDictionary objects allow their keys to be objects of any type, if the keys are not string objects, the collections are not property-list objects.
苹果官网有上述这样一段话,能往 NSUserDefaults 里存储的对象只能是 property list objects,包括 NSData,NSString, NSNumber, NSDate, NSArray, NSDictionary,且对于 NSArray 和 NSDictionary 这两个容器对象,它们所包含的内容也必需是 property list objects。重点看最后一句话,虽然 NSDictionary 和 CFDictionary 对象的 Key 可以为任何类型(只要遵循 NSCopying 协议即可),但是如果当Key 不为字符串 string 对象时,此时这个字典对象就不能算是property list objects了,所以不能往 NSUserDefaults 中存储,不然就会报错。
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_async(queue, ^{
NSLog(@"1");
[self performSelector:self withObject:@selector(test) afterDelay:.0];
NSLog(@"3");
});
- (void)test{
NSLog(@"2");
}
上述代码的执行结果并非 1 2 3 ,而是 1 3。原因是performSelector: withObject: afterDelay:的本质是往 RunLoop中添加定时器,而子线程默认是没有启动RunLoop。performSelector: withObject: afterDelay:接口虽然和performSelector:系列接口长得很类似。但前者存在于RunLoop相关文件,后者存在于NSObject相关文件。


[[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
如果在子线程中添加上述两行代码,启动RunLoop, 则代码逻辑可以正常执行。
NSLog(@"1");
[self performSelector:self withObject:@selector(test) afterDelay:.0];
NSLog(@"3");
如果上述代码放在主线程,是可以正常执行的。因为主线程默认开启了 RunLoop。
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSThread *thread = [[NSThread alloc] initWithBlock:^{
NSLog(@"1");
// [[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
// [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
}];
[thread start];
[self performSelector:@selector(test) onThread:thread withObject:nil waitUntilDone:YES];
}
同之前的分析比较,这里同样是在子线程 thread 上执行 performSelector: withObject: afterDelay: 方法,因为子线程没有开启 RunLoop,这里应该只输出 1。 但实际上会在输出 1 之后崩溃,原因在于执行完 thread 的block后,thread 会被释放。打开注释开启子线程 thread 的 RunLoop,代码可正常执行。
每次遍历的时候生成了很多占内存大的对象,如果交于默认的 autoreleasepool 去管理生命周期,会有因为内存飙升产生crash的风险,遍历过程中,可在适当的位置上去使用@autoreleasepool,一旦出了@autoreleasepool作用域,该作用域内的变量会立马释放。如:
for(int i = 0; i < 10000; i++){
@autoreleasepool {
Person *p = [[Person alloc]init];
}
}
但并不是所有的遍历方法都要加上@autoreleasepool,比如enumerateObjectsUsingBlock:方法,仔细阅读苹果官方文档,可发现该方法内部已经添加过@autoreleasepool处理。
自动释放池的主要底层数据结构是:__AtAutoreleasePool 和 AutoreleasePoolPage。
@autoreleasepool{}执行objc_autoreleasePoolPush ,出了@autoreleasepool{}执行objc_autoreleasePoolPop。struct __AtAutoreleasePool {
__AtAutoreleasePool() { // 构造函数,在创建结构体的时候调用
atautoreleasepoolobj = objc_autoreleasePoolPush();
}
~__AtAutoreleasePool() { // 析构函数,在结构体销毁的时候调用
objc_autoreleasePoolPop(atautoreleasepoolobj);
}
void * atautoreleasepoolobj;
};
Person *p = [[[Person alloc]init]autorelease]。每一个AutoreleasePoolPage对象占用4094字节内存,本身成员占用56字节,剩下的空间用来存放 autorelease对象 的地址和 POOL_BOUNDARY。所有的AutoreleasePoolPage对象通过双向链表的形式连接在一起。另外,每个 AutoreleasePoolPage 有个 thread ,说明 autoreleasepool 和线程是一一对应的。如下图:
POOL_BOUNDARY: 进入@autoreleasepool{}执行objc_autoreleasePoolPush后 ,会往 AutoreleasePoolPage 添加 POOL_BOUNDARY,并将 POOL_BOUNDARY 的内存地址作为 objc_autoreleasePoolPush的返回值记录下来;当出了@autoreleasepool{}时执行objc_autoreleasePoolPop,并将之前记录的 POOL_BOUNDARY 地址作为 objc_autoreleasePoolPop的参数,objc_autoreleasePoolPop内部会依次调用 autorelease对象 的 release 方法销毁对象,直到遇到 POOL_BOUNDARY 内存地址为止。
双向链表:上述描述先进后出,实际上是栈的结构。每个AutoreleasePoolPage 的内存空间是连续的,理论上可以当做栈的形式处理,但是单个 AutoreleasePoolPage 容量有限, 所以需要借助链表结构去连接多个 AutoreleasePoolPage 扩容。之所以要使用双向链表,是因为当执行objc_autoreleasePoolPop时,POOL_BOUNDARY 可能在上一个 AutoreleasePoolPage 中,此时需要找到之前的 AutoreleasePoolPage,并释放掉中间的 autorelease 对象。
- (void)viewDidLoad {
[super viewDidLoad];
Person *p = [[[Person alloc] init] autorelease];
NSLog(@"%s",__FUNCTION__);
}
- (void)viewWillAppear{
NSLog(@"%s",__FUNCTION__);
}
-(void)viewDidAppear{
NSLog(@"%s",__FUNCTION__);
}
上述代码, MRC 下 Person 对象会在执行完viewDidLoad和viewWillAppear 方法之后再释放,主要是和 Runloop 有关。iOS 中有个默认的autoreleasepool,主线程的 Runloop 中注册了 2 个 Observer:
kCFRunLoopEntry事件,会调系统默认autoreleasepool的 objc_autoreleasePoolPush() ;kCFRunLoopBeforeWaiting事件,会调系统默认autoreleasepool的 objc_autoreleasePoolPop()、objc_autoreleasePoolPush();kCFRunLoopBeforeExit事件,会调系统默认autoreleasepool的objc_autoreleasePoolPop();上述代码执行结果说明了: viewDidLoad和viewWillAppear在同一个运行周期内。
内存管理中调用alloc、new、copy、mutableCopy方法返回对象,在不需要这个对象时,要调用 release 或autorelease 来释放它,MRC 中通常会使用 release 和 autorelease。
分两种情况:
- (void)viewDidLoad {
[super viewDidLoad];
Person *p = [[Person alloc] init];
}
上述代码如果 ARC 最终转换成Person *p = [[[Person alloc] init] autorelease];则该对象的释放和 RunLoop 有关;如果生成如下代码,则出了方法内部该对象会立马释放,实际验证中是出了方法立马释放。
- (void)viewDidLoad {
[super viewDidLoad];
Person *p = [[Person alloc] init];
[p release];
}
一般情况下只有通过调用 set 方法对值进行改变才会触发 KVO。但是在调用NSMutableArray的 addObject或removeObject 系列方法时,并不会触发它的 set 方法。所以为了实现NSMutableArray的 KVO,官方为我们提供了如下方法:
- (NSMutableArray *)mutableArrayValueForKey:(NSString *)key
在增删元素时,使用上述方法来获取要操作的可变数组,然后再执行添加或删除元素的操作,便能实现 KVO 机制。如:
@property (nonatomic, strong) NSMutableArray *arr;
//添加元素操作
[[self mutableArrayValueForKey:@"arr"] addObject:item];
//移除元素操作
[[self mutableArrayValueForKey:@"arr"] removeObjectAtIndex:0];
如何判断一个页面的viewWillAppear方法是 push 或 present 进来是调用的,还是 pop 或 dismiss 是调用的?一种比较笨拙的方法是通过添加属性标记是进入还是返回调用viewWillAppear方法。还有一种最简单的方法,是直接调用苹果提供的两对 API 。
针对 Push 和 Pop 或 add childViewController 和 remove childViewController 的 API:
@property(nonatomic, readonly, getter=isMovingToParentViewController) BOOL movingToParentViewController NS_AVAILABLE_IOS(5_0);
@property(nonatomic, readonly, getter=isMovingFromParentViewController) BOOL movingFromParentViewController NS_AVAILABLE_IOS(5_0);
针对 Present 和 Dismiss 的 API:
@property(nonatomic, readonly, getter=isBeingPresented) BOOL beingPresented NS_AVAILABLE_IOS(5_0);
@property(nonatomic, readonly, getter=isBeingDismissed) BOOL beingDismissed NS_AVAILABLE_IOS(5_0);
两个水平布局的label,两边间隔分别是12,中间间隔为8(懂意思就行)。如果两个label 都不设置宽度,则左边 label 会拉长,右边 label 自适应。
UILabel *label1 = [[UILabel alloc] initWithFrame:CGRectZero];
label1.backgroundColor = [UIColor redColor];
label1.text = @"我是标题";
[self.view addSubview:label1];
[label1 mas_makeConstraints:^(MASConstraintMaker *make) {
make.centerY.equalTo(self.view);
make.left.equalTo(@(12));
}];
UILabel *label2 = [[UILabel alloc] initWithFrame:CGRectZero];
label2.backgroundColor = [UIColor redColor];
label2.text = @"我是描述";
[self.view addSubview:label2];
[label2 mas_makeConstraints:^(MASConstraintMaker *make) {
make.centerY.equalTo(label1);
make.left.equalTo(label1.mas_right).offset(8);
make.right.equalTo(self.view).offset(-12);
}];
如果想让左边 label 自适应,右边 label 拉升,可以设置控件拉升阻力(即抗拉升),拉升阻力越大越不容易被拉升。所以只要 label1 的拉升阻力比 label2 的大就能达到效果。
//UILayoutPriorityRequired = 1000
[label1 setContentHuggingPriority:UILayoutPriorityRequired
forAxis:UILayoutConstraintAxisHorizontal];
// //UILayoutPriorityDefaultLow = 250
[label2 setContentHuggingPriority:UILayoutPriorityDefaultLow
forAxis:UILayoutConstraintAxisHorizontal];
从左到右依次为红、蓝、黄三个视图三等分,蓝色视图布局依赖红色,黄色视图布局依赖蓝色,如果突然将中间的蓝色视图移除,红色和黄色视图的宽度就无法计算。此种情况可以设置最后一个黄色视图的做约束优先级,移除中间蓝色视图后,红色和黄色视图二等分。
//红 left bottom height
[redView mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.mas_equalTo(self.view.mas_left).with.offset(20);
make.bottom.mas_equalTo(self.view.mas_bottom).with.offset(-80);
make.height.equalTo(@50);
}];
//蓝 left bottom height width=红色
[blueView mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.mas_equalTo(redView.mas_right).with.offset(40);
make.height.width.bottom.mas_equalTo(redView);
}];
//黄 left right height width=红色
[yellowView mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.mas_equalTo(blueView.mas_right).with.offset(40);
make.right.mas_equalTo(self.view.mas_right).with.offset(-20);
make.height.width.bottom.mas_equalTo(redView);
//优先级
//必须添加这个优先级,否则blueView被移除后,redView 和 yellowView 的宽度就不能计算出来
make.left.mas_equalTo(redView.mas_right).with.offset(20).priority(250);
}];
//移除蓝色
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(4 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[blueView removeFromSuperview];
[UIView animateWithDuration:3 animations:^{
//不加这行代码就直接跳到对应的地方,加这行代码就可以执行动画。
//另外还要注意调用layoutIfNeeded的对象必须是执行动画的父视图。
//[blueView.superview layoutIfNeeded];
[self.view layoutIfNeeded];
}];
});
#ifdef DEBUG ... #endif
Library Search Paths 和 Framework Search Paths,分别移除Release环境对应的路径,Debug环境对应的路径保持不变。configurations 选项让对应的库只在 Debug 模式下生效,如:pod 'RongCloudIM/IMKit', '~> 2.8.3',:configurations => ['Debug']

不可变 + 不可变组合的时候才出现浅拷贝,其他三种情况都是深拷贝。原因在于,两个不可变对象内容一旦确定都是不可变的,所以不会彼此干扰,为了节省内容空间,两个对象可以指向同一块内存。而其他三种情况,都有可变对象的存在,为了避免两个对象之间的彼此干扰,所有会开辟额外的空间。
因为交换了方法的实现 IMP ,如果alert_replaceInitWithString方法内部调用initWithString会出现真正的死循环。下面代码的死循环只是一个假象。
@implementation NSAttributedString (Exception)
+ (void)load{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
@autoreleasepool {
[objc_getClass("NSConcreteAttributedString") swizzleMethod:@selector(initWithString:) swizzledSelector:@selector(alert_replaceInitWithString:)];
}
});
}
-(instancetype)alert_replaceInitWithString:(NSString*)aString{
if (!aString) {
NSString *string = [NSString stringWithFormat:@"[%s:%d行]",[[[NSString stringWithUTF8String:__FILE__] lastPathComponent] UTF8String],__LINE__];
[[[ExceptionAlert alloc]init]showAlertWithString:string];
;
return nil;
}
return [self alert_replaceInitWithString:aString];
}
@end
数组下标最确切的定义应该偏移(offset),如果用 a 来表示数组的首地址,a[0] 就是偏移为 0 的位置,也就是首地址,a[k] 就表示偏移 k 个 type_size 的位置,所以计算 a[k] 的内存地址只需要用这个公式:
a[k]_address = base_address + k * type_size
但是,如果数组从 1 开始计数,那我们计算数组元素 a[k]的内存地址就会变为:
a[k]_address = base_address + (k-1)*type_size
对比两个公式,不难发现,从 1 开始编号,每次随机访问数组元素都多了一次减法运算,对于 CPU 来说,就是多了一次减法指令。数组作为非常基础的数据结构,通过下标随机访问数组元素又是其非常基础的编程操作,效率的优化就要尽可能做到极致。所以为了减少一次减法操作,数组选择了从 0 开始编号,而不是从 1 开始。同理 OC 中的 objc_msgSend 是直接基于汇编实现的,直接抛开 C 或 C++ 层面的代码调用,极可能的提升代码执行效率。
可变数组或字典经过 copy 修饰符修饰后,变成不可变数组或字典,此时再去执行添加或插入元素的时候会发生崩溃。
传统的加密方式存在两个问题:
关于互质关系
如果两个正整数,除了1以外,没有其他公因数,我们就称这两个数是互质关系(coprime)。
量子密码学是基于量子形态做加解密,如果想破解必须要介入到量子状态中,但是量子传输过程中可监听到监听者的介入。目前量子密码仍处于研究阶段,并没有成熟的应用,量子很容易收到外界的干扰而改变状态。
在arm64架构之前,isa 就是一个普通的指针,存储着Class、Meta-Class对象的内存地址。从arm64架构开始,对isa进行了优化,变成了一个共用体(union)结构,还使用位域来存储更多的信息。 isa 的结构如下:

extra_r:里面存储的值是引用计数器减1has_sidetable_rc表示引用计数器是否过大无法存储在isa中,如果为1,那么引用计数会存储在一个叫SideTable的类的属性中。SideTable 结构如下,其中 refcnts 是一个存放着对象引用计数的散列表,用当前对象的地址值作为 key ,对象的引用计数作为 Value。
- (void)viewDidLoad {
[super viewDidLoad];
__strong Person *person1;
__weak Person *person2;
Person *person3;
NSLog(@"111");
{
Person *person = [[MJPerson alloc] init];
//========第一种情况========
//如果只开启该代码,person在111,222 之后释放,调用dealloc。person1 指针指向 person,person1 调用 person 的set 方法进行了retain 操作,所以 person 的生命周期同 person1。
//person1 = person;
//========第二种情况========
//如果只开启该代码,person会在111,222 中间释放。此时 person2 没有强引用(retain) person。
//所谓的 weak 指针原理是指:如何做到对象(person)被销毁之后,指向对象的 weak 指针(person2)立马被清空,置位 nil。
//person2 = person;
//========第三种情况========
//同第一种情况
//person3 = person;
}
NSLog(@"222");
}
@implementation Person
- (void)dealloc{
NSLog(@"%s", __func__);
}
@end
上述代码如果开启了person1 = person person 会在输出111,222 之后释放,调用dealloc;如果开启了person2 = person person 会在111,222 中间释放;如果开启person3 = person,效果同第一种。
__strong是强引用,所以只有离开了viewDidLoad方法后 person 对象才被释放。
一个对象可能会被多次弱引用,当这个对象被销毁时,我们需要找到这个对象的所有弱引用,所以我们需要将这些弱引用的地址(即指针)放在一个容器里(比如数组)。当对象不再被强引用时需要销毁的时候,可以在 SideTable 中通过这个对象的地址找到引用值,首先清空引用值。同时, SideTable结构中还有weak_table,该结构也是一个散列表,key 为对象地址,value 为一个数组,里面保存着指向该对象的所有弱指针。当对象释放的时候,先清空引用哈希表RefcountMap对应的引用值,遍历弱指针数组,依次将各个弱指针置为 nil。
用户设置的密码复杂度可能不够高,同时不同的用户极有可能会使用相同的密码,那么这些用户对应的密文也会相同,这样,当存储用户密码的数据库泄露后,攻击者会很容易便能找到相同密码的用户,从而也降低了破解密码的难度。因此,在对用户密码进行加密时,需要考虑对密码进行掩饰,即使是相同的密码,也应该要保存为不同的密文,即使用户输入的是弱密码,也需要考虑进行增强,从而增加密码被攻破的难度,而使用带盐的加密hash值便能满足该需求。比如密码原本是由字母和数字组成,破解者仅需要在字母和数字中找答案。但是如果密码中混淆了盐(不仅仅只包含字母和数字),破解者仅仅从字母和数字下手,肯定是找不到答案,无疑增加了破解难度。
笔者实际项目开发中,为了网络安全,请求参数按照一定的规则拼接成字符串,然后在字符串中加盐,最后 MD5 签名。后端依照同样的规则校验签名,若签名值一致则通过校验。
User Agent中文名为用户代理,简称 UA,它是一个特殊字符串头,使得服务器能够识别客户使用的操作系统及版本、CPU 类型、浏览器及版本、浏览器渲染引擎、浏览器语言、浏览器插件等。网站在手机端 app 打开和直接在浏览器中打开看到的内容可能不一样,是因为网页可以根据 UA 判断是 app 打开的还是浏览器打开的。
navigator 可以获取到浏览器的信息:navigator.userAgent。webView中获取 User Agent 方式如下:
+(void)initialize{
if ([NSThread isMainThread]) {
[self getUserAgent];
}else{
dispatch_async(dispatch_get_main_queue(), ^{
[self getUserAgent];
});
}
}
+(void)getUserAgent{
UIWebView *webView = [[UIWebView alloc]initWithFrame:CGRectZero];
NSString *userAgent = [webView stringByEvaluatingJavaScriptFromString:@"navigator.userAgent"];
NSDictionary *dict = [NSDictionary dictionaryWithObjectsAndKeys:[NSString stringWithFormat:@"%@meicaiMallIOS",userAgent],@"UserAgent",nil];
[[NSUserDefaults standardUserDefaults] registerDefaults:dict];
}
JS 调 OC ⽬目前主要的方式有三种:
context[@"_OC_catch"] = ^(JSValue *msg, JSValue *stack) {
};
JSExport 可以导出 Objective-C 的属性、实例方法、类方法和初始化⽅方法到 JS 环境,这样就可 以通过 JS 代码直接调⽤用 Objective-C 。通过 JSExport 不仅可以导出⾃自定义类的方法、属性,也可以导出已有类的⽅方法、属性。在导出过程中,类的方法名会被转换成 JS 类型命名,第二个参数的第一个字⺟会被大写,比如- (void)addX:(int)x andY:(int)y;被转为addXAndY(x, y)。除此,JSExport还可以导出已有类的⽅方法、属性。
通过拦截 URL,这种方式是 Web 端通过某种方式发送 URLScheme 请求,之后 Native 拦截到请求并根据URL SCHEME(包括所带的参数)进行相关操作。类似于通过 SCHEME 唤起APP。这种方式的缺点是 url 长度有隐患,并且创建请求需要一定的耗时,比注入 API 的方式调用同样的功能。耗时会比较长。所以还是更推荐使用注入 API 的方式。
OC 调 JS 主要有 UIWebView 、WKWebView 和 JSCore 这三种⽅方式。⽽ UIWebView 的方式其实可以看作是 JSCore 的⽅方式。
// 要执行的 JS 代码,定义一个 add 函数并执⾏行行
NSString *addjs = @"function add(a, b) {return a + b;};add(1,3)";
// sumValue 为执⾏行行后的结果
JSValue *sumValue = [self.context evaluateScript:addjs];
//通过 UIWebView 获取 context
JSContext *context = [_webView
valueForKeyPath:@"documentView.webView.mainFrame.JSContext"];
// 要执行的 JS 代码,定义一个 add 函数并执⾏行行
NSString *addjs = @"function add(a, b) {return a + b;};add(1,3)";
// sumValue 为执⾏行行后的结果
JSValue *sumValue = [self.context evaluateScript:addjs];
evaluateJS: ,通过下面方法来执行 JS 代码。[self.webView evaluateJS:@"function add(a, b) {return a + b;};add(1,3)" completionHandler:^(id _Nullable msg, NSError * _Nullable error) {
NSLog(@"evaluateJS add: %@, error: %@", msg, error);
}];
UIScrollView继承自UIView,内部有一个 UIPanGestureRecongnizer手势。 frame 是相对父视图坐标系来决定自己的位置和大小,而bounds是相对于自身坐标系的位置和尺寸的。该视图 bounds 的 origin 视图本身没有发生变化,但是它的子视图的位置却发生了变化,因为 bounds 的 origin 值是基于自身的坐标系,当自身坐标系的位置被改变了,里面的子视图肯定得变化, bounds 和 panGestureRecognize 是实现 UIScrollView 滑动效果的关键技术点。
frame和bounds对比:
参考
verbose意思为 冗长的、啰嗦的,一般在程序中表示详细信息。此参数可以显示命令执行过程中都发生了什么。pod install或pod update可能会卡在Analyzing dependencies步骤,因为这两个命令会升级 CocoaPods 的 spec 仓库,追加该参数可以省略此步骤,命令执行速度会提升。普遍开发者得理解是:一个是数据,一个是操作。如果从数据传递方向的角度来看,两者的本质是数据传递的方向不同。dataSource 是外部将数据传递到视图内,而 delegate 是将视图内的数据和操作等传递到外部。实际开发封装自定义视图,可以参照数据传递方向分别设置 dataSource 和 delegate。
真正的 MVC 应该是苹果提供的经典UITableView的使用,实际开发中经常在 Cell 中引入Model,本质上来说不算是真正的 MVC ,只能算是 MVC 的变种。真正的 MVC 中 View 和 Model 应该是完全隔离的。苹果的 MVC 中正是因为 View 没有和任何 Model 绑定,所以 cell 的可冲拥堵高,但是缺点是代码过于臃肿。


相同点:
不同点:
指针函数是指带指针的函数,即本质是一个函数,函数返回类型是某一类型的指针。它是一个函数,只不过这个函数的返回值是一个地址值。
int *f(x,y);
函数指针是指向函数的指针变量,即本质是一个指针变量。
int (*f) (int x); /*声明一个函数指针 */
f = func; /* 将func函数的首地址赋给指针f */
NSObject *obj = [[NSObject alloc] init];
代码对应的内存布局如下,obj 指针存在于栈取,obj 对象存在于堆区。obj 指针的回收由栈区自动管理,堆区的内存需要开发者自己管理(MRC)情况。所谓的堆内存回收并不是指将 obj 对象占有的内存给挖去或是将空间数据清空为0,而是指 obj 对象原本占有的空间可以被其他人利用(即其他指针可以指向该空间)。其他指针指向该空间时,重新初始化该空间,将空间原有数据清零。

IP 是地址,有定位功能;MAC 是身份唯一标识,无定位功能;有了 MAC 地址为什么还要有 IP 地址?举个例子,现在我要和你通信(写信给你),地址用你的身份证号,信能送到你手上吗? 明显不能!身份证号前六位能定位你出生的县,MAC 地址前几位也可以定位生产厂家。但是你出生后会离开这个县(IP 地址变动),哪怕你还在这个县,我总不能满大街喊着你的身份证号去问路边人是否认识这个身份证号的主人,所以此刻需要借助 IP 的定位功能。
具体可参考笔者之前文章 iOS 签名机制,文章中可以找到答案。
命中测试和响应链问题
手势、UIControl、UITouch系列事件关系
触屏事件的处理被分成两个阶段:查找响应者(a)和响应者处理(b、c、d)。
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
NSLog(@"-----%@",self.nextResponder.class);
if (!self.isUserInteractionEnabled || self.isHidden || self.alpha <= 0.01) return nil;
//判断点在不在这个视图里
if ([self pointInside:point withEvent:event]) {
//在这个视图 遍历该视图的子视图
for (UIView *subview in [self.subviews reverseObjectEnumerator]) {
//转换坐标到子视图
CGPoint convertedPoint = [subview convertPoint:point fromView:self];
//递归调用hitTest:withEvent继续判断
UIView *hitTestView = [subview hitTest:convertedPoint withEvent:event];
if (hitTestView) {
//在这里打印self.class可以看到递归返回的顺序。
return hitTestView;
}
}
//这里就是该视图没有子视图了 点在该视图中,所以直接返回本身,上面的hitTestView就是这个。
NSLog(@"命中的view:%@",self.class);
return self;
}
//不在这个视图直接返回nil
return nil;
}

其实响应链就是在命中测试中,走通的路径。用上个章节的例子,整个命中测试的走向是:A✅ --> D❎ --> B✅ --> C❎,我们把没走通的❎的去掉,以第一响应者 B 作为头,依次连接,响应链就是:B -> A。(实际上 A 后面还有控制器等,但在该例子中没有展示控制器等,所以就写到 A)
默认来说,若该结点是 UIView 类型的话,这个 next 属性是该结点的父视图。但也有几个例外:
下面举个例子来说明。如下图所示,触摸点是,那根据命中测试,B 就成为了第一响应者。由于 C 是 B 的父视图、A 是 C 的父视图、同时 A 是 Controller 的根视图,那么按照规则,响应链就是这样的:视图 B -> 视图 C -> 根视图 A -> UIViewController 对象 -> UIWindow 对象 -> UIApplication 对象 -> App Delegate
获取到响应链后,触摸事件首先将会由第一响应者响应,首先触发第一响应者的touchBegin 方法。
重载父 view 的 -(UIView *)hitTest: withEvent: 方法,去掉点击必须在父 view 内的坐标判断逻辑,子 view 就能成为最合适的视图,用于响应事件了。注意内部的坐标转化:判断点击的点是包含子视图,如果包含子视图,则调用子视图的 hitTest 方法。
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
if (!self.isUserInteractionEnabled || self.isHidden || self.alpha <= 0.01) {
return nil;
}
/**
* 此注释掉的方法用来判断点击是否在父View Bounds内,
* 如果不在父view内,就会直接不会去其子View中寻找HitTestView,return 返回
*/
// if ([self pointInside:point withEvent:event]) {
for (UIView *subview in [self.subviews reverseObjectEnumerator]) {
CGPoint convertedPoint = [subview convertPoint:point fromView:self];
UIView *hitTestView = [subview hitTest:convertedPoint withEvent:event];
if (hitTestView) {
return hitTestView;
}
}
return self;
// }
return nil;
}
子View覆盖在父View上,但是要实现穿透子View去响应父View点击事件。解决方法时,重写子 View 的 hitTest 方法。点击的是自身则返回nil, 此时最合适响应者转为父类。
-(UIView*)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
UIView *hitView =[super hitTest:point withEvent:event];
if(hitView == self){
//自动将事件传递到上一层(父视图),自身不做事件处理
return nil;
}
return hitView;
}
方法一:子视图重写以下 touch 系列方法,通过 self.nextResponder 找到父视图,并调用父视图的 touch 系列方法。
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
[self.nextResponder touchesBegan:touches withEvent:event];
[super touchesBegan:touches withEvent:event];
}
-(void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
[self.nextResponder touchesMoved:touches withEvent:event];
[super touchesMoved:touches withEvent:event];
}
-(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
[self.nextResponder touchesEnded:touches withEvent:event];
[super touchesEnded:touches withEvent:event];
}
-(void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event
{
[self.nextResponder touchesCancelled:touches withEvent:event];
[super touchesCancelled:touches withEvent:event];
}
方法二: 如果子视图和父视图的收拾都是 UIGestureRecognizer 相关方法,也可以通过- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer; 方法,同时响应父视图和子视图的事件。此方法返回YES,手势事件会一直往下传递,不论当前层次是否对该事件进行响应。
重载UIButton的-(BOOL)pointInside: withEvent:方法,让Point即使落在Button的Frame外围也返回YES。
- (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event {
return CGRectContainsPoint(HitTestingBounds(self.bounds, self.minimumHitTestWidth, self.minimumHitTestHeight), point);
}
CGRect HitTestingBounds(CGRect bounds, CGFloat minimumHitTestWidth, CGFloat minimumHitTestHeight) {
CGRect hitTestingBounds = bounds;
if (minimumHitTestWidth > bounds.size.width) {
hitTestingBounds.size.width = minimumHitTestWidth;
hitTestingBounds.origin.x -= (hitTestingBounds.size.width - bounds.size.width)/2;
}
if (minimumHitTestHeight > bounds.size.height) {
hitTestingBounds.size.height = minimumHitTestHeight;
hitTestingBounds.origin.y -= (hitTestingBounds.size.height - bounds.size.height)/2;
}
return hitTestingBounds;
}
不能确定代码的运行顺序和结果,是线程不安全的。线程安全是相对于多线程而言的,单线程不会存在线程安全问题。因为单线程代码的执行顺序是唯一确定的,进而可以确定代码的执行结果。
线程不安全的本质原因在于:表面展现在我们眼前的可能是一行代码,但转换成汇编代码后可能对应多行。当多个线程同时去访问代码资源时,代码的执行逻辑就会发生混乱。如数据的写操作,底层实现可能是先读取,再在原有数据的基础上改动。如果此时有一个读操作,原本意图是想在写操作完毕之后再读取数据,但不巧的这个读操作刚好发生在写操作执行的中间步骤中。虽然读操作后与写操作执行,但数据读取的值并不是写操作的结果值,运气不好时还可能发生崩溃。
- (void)viewDidLoad {
[super viewDidLoad];
int a = 100;
a += 200;
NSLog(@"%d",a);
}
如上述代码中的int a = 100;和a += 200;转换的汇编代码,为下面中间八行汇编代码。
0x1098e7621 <+49>: callq 0x1098e7a32 ; symbol stub for: objc_msgSendSuper2
0x1098e7626 <+54>: leaq 0x1a33(%rip), %rax ; @"%d"
0x1098e762d <+61>: movl $0x64, -0x24(%rbp)
0x1098e7634 <+68>: movl -0x24(%rbp), %ecx
0x1098e7637 <+71>: addl $0xc8, %ecx
0x1098e763d <+77>: movl %ecx, -0x24(%rbp)
-> 0x1098e7640 <+80>: movl -0x24(%rbp), %esi
0x1098e7643 <+83>: movq %rax, %rdi
0x1098e7646 <+86>: movb $0x0, %al
0x1098e7648 <+88>: callq 0x1098e7a14 ; symbol stub for: NSLog
APP 启动分为冷启动和热启动,这里主要说下冷启动过程。冷启动分为三阶段: dyld 阶段、runtime阶段、main函数阶段,一般启动时间的优化也是从这三大步着手。
+load方法,attribute((constructor)) 修饰的函数的调用、创建 C++ 静态全局变量。到此为止,可执行文件和动态库中所有的符号(Class、Protocol、Selector、IMP …)都已经按格式成功加载到内存中,被runtime 所管理。在关于 App 包体积优化的一些博客文章中,偶尔看到包体积的优化可以从 C++ 入手,其中有一条是减少内联函数的使用。问题来了,什么是内联函数?为什么要减少内联函数的使用?它和一般函数有什么异同点?和宏相比有什么异同点?
内联函数关键字是 inline ,C++ 中普通函数使用的申明或实现使用inline 修饰后,即为内联函数。注意:递归函数即使被 inline 修饰后也不是内联函数,依然是普通函数。
inline int sum(int a, int b){
return a + b;
}
普通函数调用会开辟一段栈空间执行相关代码,函数执行完再将对应的栈空间回收。而内联函数调用中,编译器会将函数调用直接展开为函数代码。如cout << sum(1, 2) << endl会直接转换为cout << 1 + 2<< endl,由此可见内联函数和一般的宏很类似,都是直接替换相关代码。同宏相比,内联函数只是多了一些函数特性和语法检测功能。
OC 中可以通过关键字 NS_INLINE 使用内联函数。
NS_INLINE void log(int value) {
NSLog(@"%d", value);
}
综上,内联函数或宏省去了参数压栈、生成汇编语言的CALL调用、返回参数、执行return等过程,可以减少函数调用的开销。但是会增加代码体积,所以减少内联函数或宏的使用一定程度上可以减少包体积。但并不是说为了减小包体积完全不去使用内联函数,建议经常会被调用的代码,且代码量不是很多的时候(不超过10行),为减少函数调用的开销,可适当使用内联函数。
objc_msgSend 方法参数
LGPerson *person = [LGPerson alloc];
[person sayHello];
//2.消息发送
objc_msgSend(person, sel_registerName("sayHello"));
有两个类 Animal 和 Cat ,其中 Cat 继承自 Animal 类,在 Cat 类实现如下代码,试问打印结果是什么?
@implementation Cat
- (instancetype)init{
self = [super init];
if (self) {
NSLog(@"%@",[self class]);//Cat
NSLog(@"%@",[self superclass]);//Animal
NSLog(@"%@",[super class]);//Cat
NSLog(@"%@",[super superclass]);//Animal
}
return self;
}
@end
上述代码打印结果一次为: Cat Animal Cat Animal,前两个结果不足为奇,后两个结果似乎有点费解。
super 调用底层会转换为objc_msgSendSuper函数的调用,objc_msgSendSuper 函数接收 2 个参数 objc_super 结构体和 SEL ,objc_super结构如下:
struct objc_super {
__unsafe_unretained _Nonnull id receiver; // 消息接收者
__unsafe_unretained _Nonnull Class super_class; // 消息接收者的父类
};
[super class] 在调用过程中,底层转化为 objc_msgSendSuper({self, [Animal class]}, @selector(class)); ,同 objc_msgSend 函数相比相当于多了第二个参数,但消息接收者仍然是 self ,所以打印结果为 Cat。
the superclass at which to start searching for the method implementation.
objc_msgSendSuper 方法中的第二个参数主要作用是告诉从哪里开始搜索方法实现,一般传入的是父类。这也是实际开发中 [super superClassMethod] 直接调用父类方法的原因。但是如果按照这一点来看,依然和上述打印结果不符合。因此需要看 class 和 superClass 方法的内部实现,class 方法的内部实现返回消息接收者,上述代码消息接收者为 Cat ,因此打印结果仍然是 Cat。superClass 方法内部实现是返回消息接受者的父类,因此打印结果是 Animal。
@interface Person : NSObject
- (void)test;
@end
@implementation Person
- (void)test{
NSLog(@"person: %@,%@,%@,%@",[self class],[self superclass],[super class],[super superclass]);
}
@end
@interface Student : Person
- (void)test;
@end
@implementation Student
- (void)test{
NSLog(@"student:%@,%@,%@,%@",[self class],[self superclass],[super class],[super superclass]);
[super test];
}
@end
调用 Student 的 test 方法,最终打印结果都是 Student,Person,Student,Person。因为实例对象一直是 Student 对应的实例对象,并非是 Person 的实例对象。
待更新。。。。。。
说实在的有时会对各种渲染框架感觉混乱,一会CA、一会CG等等,于是就把这些渲染框架简单汇总了下。
实际开发中可能会遇到严重线程阻塞的情况,比如笔者之前就遇到过使用 MJ 下拉刷新,刷新完毕后 MJ 复位无动画效果,第一猜测就是有阻塞,于是借助 Product --> Profile-->TimeProfiler 工具 第一时间定位到耗时较多的代码。结果发现在渲染 Cell 的时候动态的调用了苹果接口中 html 转属性文本的方法,该方法的解析异常耗时。可按照下图设置 Call Tree ,方便定位耗时代码。

给百万数据排序可以用"桶排序",核心思想是将数据分到几个有序的桶,每个桶里的数据再单独进行排序。桶内排完序之后,再把每个桶里的数据按照顺序依次取出,组成的序列就是有序的了。
如果要排序的数据为 n 个,均匀地划分到 m 个桶内,每个桶里就有 k = n/m 个元素。每个桶内部使用快速排序,则每个桶内时间复杂度为 O(k * logk)。m 个桶排序的时间复是 O(m * k * logk),因为 k = n/m,所以整个桶排序的时间复杂度就是 O(n*log(n/m))。当桶的个数 m 接近数据个数 n 时,log(n/m) 就是非常小的常量,这个时候桶排序的时间复杂度接近 O(n)。
桶排序对要排序数据的要求是非常苛刻的。
O(nlogn) 的排序算法了。假设有 10GB 的订单数据需要排序,内存有限,只有几百 MB,没办法一次性把 10GB 的数据都加载到内存中。先扫秒订单知道金额最小是 1 元,最大是 10 万元。可以将订单划到100个桶内,第一个桶我们存储金额在 1 元到 1000 元之内的订单,第二桶存储金额在 1001 元到 2000 元之内的订单,以此类推。但是订单的数据分布可能并不是非常均匀,某些桶内的数据依然是大于内存空间,此时可以将该桶内的数据再次进行划分,直到能加载到内存为止。
线程安全中为了实现线程阻塞,一般有两种方案:一种是让线程处于休眠状态,此时不会消耗 CPU 资源;另一种方案是让线程忙等或空转,此时会消耗一定的 CPU 资源。前者属于互斥,后者属于自旋。
自旋锁是一种特殊的互斥锁,自旋在线程加锁的情况下,会一直尝试是否解锁,如果没有解锁,会一直循环判断,如果锁已经放开,则继续执行,不再是空转状态。
优点:
缺点:
OSSpinLock 属于自旋锁,Pthred 库中相关的锁,以及 NSLock、@synchronized 等都属于互斥锁。OSSpinLock目前已经不再安全,因为会出现优先级反转问题。 现代操作系统一般采用 时间片轮转算法 调度进程或线程,按照线程的优先级为不同的线程分配不同的时间,优先级越高分配的时间片越多。假设有两个线程 thread1 和 thread2,其中 thread1 的优先级高于 thread2,即thread1 分配的时间片多余 thread2。如果 thread2 正在锁内安全执行,一段时间后 thread1 执行任务时,发现锁未打开,于是会处于忙等状态。由于thread1 的优先级高于 thread2,此时系统会分配更多的时间片给 thread1,thread2 时间片减少,迟迟不能完成,thread1 却一直等待。如此就造成线程优先级反转。什么时候用自旋锁比较划算
什么时候用互斥锁比较划算
双模式、I/O 保护和内存保护、定时器三者是确保操作系统能够运行的关键技术,可以避免外界应用崩溃对操作系统的影响。
双模式
为了保证操作系统不受其它故障程序的影响,进而产生系统崩溃的可能。一种常用的办法是引入双重模式,即用户模式和内核模式。内核模式只能运行操作系统的程序。所有的用户应用程序只能在用户模式下运行。 双模式需要CPU的支持,如果CPU有模式位,则可以在操作系统中实现双模式,目前主流的CPU基本都有模式位。双模式允许操作系统不受其它故障应用程序的影响。特权指令是指可能引起崩溃的指令,该指令只能运行在内核模式中。 如果用户程序需要使用特权指令,可以通过系统提供的API调用。
I/O保护和内存保护
定义所有I/O指令为特权指令,用户应用程序无法直接访问I/O指令,只能通过系统调用进行I/O操作,从而避免非法I/O操作。
利用基址寄存器和限长寄存器隔离不同程序的内存地址。
定时器
如果用户程序死循环或用户程序不调用系统调用,此时操作系统将无法获得CPU并对系统进行管理。解决方法是引入定时器,在一段时间后发生中断,将CPU控制权返回给操作系统。
如果是磁盘重量不变,如果是 SSD硬盘(固态硬盘)会受到影响。
磁硬盘能存储数据靠的是里面的磁铁的方向改变。一个长条形状的磁铁有南极和北极两个端,一种端代表0,另一端代表1,然后通过 01 不同的组合代表不同的意义,只要磁铁足够多,就能用它们排列的顺序代表所有的信息,数据就是这样存在磁硬盘中的。所以磁硬盘重量不会收存储数据大小的影响。
SSD 内部有上万亿个小单元,每个单元表示 0 还是 1,取决于这个单元里装了多少个电子,比如装进去100个电子后,这个单元就代表 1 ,低于这个数值就代表 0。所以对SSD的重量会受到内部电子的影响。一个电子是0.000000……9公斤,30个零。2TB的数据至少要用2×10^13个电子。质量大约就是0.0000000000002公斤,12个零。
计算机之所以会出现运算错误的原因是因为一些小数无法转换二进制数,例如 0.1 就无法用二进制数正确表示。下图说明了小数的二进制小数表达方式,小数的表示方式和整数表示方式类似。

把小数扩大对应的倍数,转成整数进行计算。计算机在进行小数计算时可能会出错,但是在计算整数的时候,只要不超过可处理数值的范围一定不会出现问题。
runtime 并非是 Objective-C 的专利,绝大多数语言都有这个概念,runtime 就是动态库(运行时库)的一部分。比如 C 语言中 glibc 动态链接库通常会被很多操作依赖,包括字符串处理(strlen、strcpy)、信号处理、socket、线程、IO、动态内存分配等等。由于每个程序都依赖于运行时库,这些库一般都是动态链接的。这样一来,运行时库可以存储在操作系统中,很多程序共享一个动态库,这样就可以节省内存占用空间和应用程序大小。
补充:链接一般分为静态链接和动态链接。一般说的预编译、编译、汇编、链接,其中的链接是指静态链接。所谓的动态链接是指: 链接过程被推迟到运行时再进行。
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSThread *thread = [[NSThread alloc] initWithBlock:^{
NSLog(@"1");
}];
[thread start];
[self performSelector:@selector(test) onThread:thread withObject:nil waitUntilDone:YES];
}
- (void)test{
NSLog(@"2");
}
直接执行上述代码,在输出 1 之后,会直接崩溃。主要原因在于,执行完 [thread start] 后,线程立马被杀死。此时再次在线程中调用 test 方法会直接崩溃。 解决该问题的思路主要是保证线程的生命周期,即线程保活。AFN 中,异步网络发起请求,请求回来之后,线程依然没有被杀死,也是利用了线程保活技术。代码如下:
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSThread *thread = [[NSThread alloc] initWithBlock:^{
NSLog(@"1");
[[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
}];
[thread start];
[self performSelector:@selector(test) onThread:thread withObject:nil waitUntilDone:YES];
}
- (void)test{
NSLog(@"2");
}
主要从以下四个方面作总结资源文件、源代码、编译参数配置以及苹果自身优化。
-(UIImage*)imageChangeColor:(UIColor*)color{
UIGraphicsBeginImageContextWithOptions(self.size, NO, 0.0f);//获取画布
[color setFill];//画笔沾取颜色
CGRect bounds = CGRectMake(0, 0, self.size.width, self.size.height);
UIRectFill(bounds);
[self drawInRect:bounds blendMode:kCGBlendModeOverlay alpha:1.0f];//绘制一次
[self drawInRect:bounds blendMode:kCGBlendModeDestinationIn alpha:1.0f];//再绘制一次
UIImage *img = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();//获取图片
return img;
}
+ (UIImage *)getLaunchImage{
CGSize viewSize = [UIScreen mainScreen].bounds.size;
NSString *viewOr = @"Portrait";//垂直
NSString *launchImage = nil;
NSArray *launchImages = [[[NSBundle mainBundle] infoDictionary] valueForKey:@"UILaunchImages"];
for (NSDictionary *dict in launchImages) {
CGSize imageSize = CGSizeFromString(dict[@"UILaunchImageSize"]);
if (CGSizeEqualToSize(viewSize, imageSize) && [viewOr isEqualToString:dict[@"UILaunchImageOrientation"]]) {
launchImage = dict[@"UILaunchImageName"];
}
}
return [UIImage imageNamed:launchImage];
}
import 代码文件,因为运行时的原因,删除类之前做一下核对。1、Optional Level-->Fastest,Smallest[-OS]:含义可以参照该篇文章 2.1 小节。
2、Link-Time Optimization : 它是 LLVM 编译器的一个特性,用于在 link 中间代码时,对全局代码进行优化。这个优化是自动完成的,因此不需要修改现有的代码。苹果使用了新的优化方式 Incremental,大大减少了链接的时间。笔者在实际的项目开发中开启这个配置后,包体积减少了 4 - 5M 左右。
3、Deployment Postprocessing、Strip Linked Product、Strip Debug Symbols During Copy、Symbols hidden by default 四者设置为 YES 后可以去掉不必要的符号信息,减少可执行文件大小。但去除了符号信息之后我们就只能使用 dSYM 来进行符号化了,所以需要将 Debug Information Format 修改为 DWARF with dSYM file。
其它、Dead Code Stripping(仅对静态语言有效):删除静态链接的可执行文件中未引用的代码。Debug 设置为 NO, Release 设置为 YES 可减少可执行文件大小。Xcode 默认会开启此选项,C/C++/Swift 等静态语言编译器会在 link 的时候移除未使用的代码,但是对于 Objective-C 等动态语言是无效的。因为 Objective-C 是建立在运行时上面的,底层暴露给编译器的都是 Runtime 源码编译结果,所有的部分应该都是会被判别为有效代码。
其它、Generate Debug Symbols(有作用但不建议修改): 当 Generate Debug Symbol s选项设置为 YES时,每个源文件在编译成 .o 文件时,编译参数多了 -g 和 -gmodules 两项。打包会生成 symbols 文件。设置为 NO 则 ipa 中不会生成 symbol 文件,可以减少 ipa 大小。但会影响到崩溃的定位。保持默认的开启,不做修改。
我正在编写一个包含C扩展的gem。通常当我写一个gem时,我会遵循TDD的过程,我会写一个失败的规范,然后处理代码直到它通过,等等......在“ext/mygem/mygem.c”中我的C扩展和在gemspec的“扩展”中配置的有效extconf.rb,如何运行我的规范并仍然加载我的C扩展?当我更改C代码时,我需要采取哪些步骤来重新编译代码?这可能是个愚蠢的问题,但是从我的gem的开发源代码树中输入“bundleinstall”不会构建任何native扩展。当我手动运行rubyext/mygem/extconf.rb时,我确实得到了一个Makefile(在整个项目的根目录中),然后当
我已经在Sinatra上创建了应用程序,它代表了一个简单的API。我想在生产和开发上进行部署。我想在部署时选择,是开发还是生产,一些方法的逻辑应该改变,这取决于部署类型。是否有任何想法,如何完成以及解决此问题的一些示例。例子:我有代码get'/api/test'doreturn"Itisdev"end但是在部署到生产环境之后我想在运行/api/test之后看到ItisPROD如何实现? 最佳答案 根据SinatraDocumentation:EnvironmentscanbesetthroughtheRACK_ENVenvironm
我们的git存储库中目前有一个Gemfile。但是,有一个gem我只在我的环境中本地使用(我的团队不使用它)。为了使用它,我必须将它添加到我们的Gemfile中,但每次我checkout到我们的master/dev主分支时,由于与跟踪的gemfile冲突,我必须删除它。我想要的是类似Gemfile.local的东西,它将继承从Gemfile导入的gems,但也允许在那里导入新的gems以供使用只有我的机器。此文件将在.gitignore中被忽略。这可能吗? 最佳答案 设置BUNDLE_GEMFILE环境变量:BUNDLE_GEMFI
这似乎非常适得其反,因为太多的gem会在window上破裂。我一直在处理很多mysql和ruby-mysqlgem问题(gem本身发生段错误,一个名为UnixSocket的类显然在Windows机器上不能正常工作,等等)。我只是在浪费时间吗?我应该转向不同的脚本语言吗? 最佳答案 我在Windows上使用Ruby的经验很少,但是当我开始使用Ruby时,我是在Windows上,我的总体印象是它不是Windows原生系统。因此,在主要使用Windows多年之后,开始使用Ruby促使我切换回原来的系统Unix,这次是Linux。Rub
我正在玩HTML5视频并且在ERB中有以下片段:mp4视频从在我的开发环境中运行的服务器很好地流式传输到chrome。然而firefox显示带有海报图像的视频播放器,但带有一个大X。问题似乎是mongrel不确定ogv扩展的mime类型,并且只返回text/plain,如curl所示:$curl-Ihttp://0.0.0.0:3000/pr6.ogvHTTP/1.1200OKConnection:closeDate:Mon,19Apr201012:33:50GMTLast-Modified:Sun,18Apr201012:46:07GMTContent-Type:text/plain
无论您是想搭建桌面端、WEB端或者移动端APP应用,HOOPSPlatform组件都可以为您提供弹性的3D集成架构,同时,由工业领域3D技术专家组成的HOOPS技术团队也能为您提供技术支持服务。如果您的客户期望有一种在多个平台(桌面/WEB/APP,而且某些客户端是“瘦”客户端)快速、方便地将数据接入到3D应用系统的解决方案,并且当访问数据时,在各个平台上的性能和用户体验保持一致,HOOPSPlatform将帮助您完成。利用HOOPSPlatform,您可以开发在任何环境下的3D基础应用架构。HOOPSPlatform可以帮您打造3D创新型产品,HOOPSSDK包含的技术有:快速且准确的CAD
在应用开发中,有时候我们需要获取系统的设备信息,用于数据上报和行为分析。那在鸿蒙系统中,我们应该怎么去获取设备的系统信息呢,比如说获取手机的系统版本号、手机的制造商、手机型号等数据。1、获取方式这里分为两种情况,一种是设备信息的获取,一种是系统信息的获取。1.1、获取设备信息获取设备信息,鸿蒙的SDK包为我们提供了DeviceInfo类,通过该类的一些静态方法,可以获取设备信息,DeviceInfo类的包路径为:ohos.system.DeviceInfo.具体的方法如下:ModifierandTypeMethodDescriptionstatic StringgetAbiList()Obt
@作者:SYFStrive @博客首页:HomePage📜:微信小程序📌:个人社区(欢迎大佬们加入)👉:社区链接🔗📌:觉得文章不错可以点点关注👉:专栏连接🔗💃:感谢支持,学累了可以先看小段由小胖给大家带来的街舞👉微信小程序(🔥)目录自定义组件-behaviors 1、什么是behaviors 2、behaviors的工作方式 3、创建behavior 4、导入并使用behavior 5、behavior中所有可用的节点 6、同名字段的覆盖和组合规则总结最后自定义组件-behaviors 1、什么是behaviorsbehaviors是小程序中,用于实现
了解Rails缓存如何工作的人可以真正帮助我。这是嵌套在Rails::Initializer.runblock中的代码:config.after_initializedoSomeClass.const_set'SOME_CONST','SOME_VAL'end现在,如果我运行script/server并发出请求,一切都很好。然而,在我的Rails应用程序的第二个请求中,一切都因单元化常量错误而变得糟糕。在生产模式下,我可以成功发出第二个请求,这意味着常量仍然存在。我已通过将以上内容更改为以下内容来解决问题:config.after_initializedorequire'some_cl
我有一个使用PDFKit呈现网页的pdf版本的Rails应用程序。我使用Thin作为开发服务器。问题是当我处于开发模式时。当我使用“bundleexecrailss”启动我的服务器并尝试呈现任何PDF时,整个过程会陷入僵局,因为当您呈现PDF时,会向服务器请求一些额外的资源,如图像和css,看起来只有一个线程.如何配置Rails开发服务器以运行多个工作线程?非常感谢。 最佳答案 我找到的最简单的解决方案是unicorn.geminstallunicorn创建一个unicorn.conf:worker_processes3然后使用它: