该文章主要整理一些小知识点,主要涉及 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 *)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 *)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]]];
上面代码可以读取出手机端 /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
相关文件。
performSelector: withObject: afterDelay:接口
performSelector:系列接口
autoreleasepool 使用
每次遍历的时候生成了很多占内存大的对象,如果交于默认的 autoreleasepool 去管理生命周期,会有因为内存飙升产生crash的风险,遍历过程中,可在适当的位置上去使用@autoreleasepool
,一旦出了@autoreleasepool
作用域,该作用域内的变量会立马释放。如:
for(int i = 0; i < 10000; i++){
@autoreleasepool {
}
}
但并不是所有的遍历方法都要加上@autoreleasepool
,比如enumerateObjectsUsingBlock:
方法,仔细阅读苹果官方文档,可发现该方法内部已经添加过@autoreleasepool
处理。
autoreleasepool 底层
autoreleasepool 底层是个C++结构体,创建和销毁的时候分别会调用构造函数和析构函数。
struct __AtAutoreleasePool {
__AtAutoreleasePool() { // 构造函数,在创建结构体的时候调用
atautoreleasepoolobj = objc_autoreleasePoolPush();
}
~__AtAutoreleasePool() { // 析构函数,在结构体销毁的时候调用
objc_autoreleasePoolPop(atautoreleasepoolobj);
}
void * atautoreleasepoolobj;
};
系统默认 autoreleasepool
iOS 中有个默认的autoreleasepool
,主线程的 Runloop 中注册了 2 个 Observer:
kCFRunLoopEntry
事件,会调系统默认autoreleasepool
的 objc_autoreleasePoolPush() ;kCFRunLoopBeforeWaiting
事件,会调系统默认autoreleasepool
的 objc_autoreleasePoolPop()
、objc_autoreleasePoolPush()
;kCFRunLoopBeforeExit
事件,会调系统默认autoreleasepool
的objc_autoreleasePoolPop()
;autorelease和autoreleasepool
内存管理中调用alloc、new、copy、mutableCopy方法返回对象,在不需要这个对象时,要调用 release 或autorelease 来释放它,MRC 中通常会使用 release 和 autorelease。autorelease 一般是仅用在 MRC 中。
一般情况下只有通过调用 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;
//如果只开启该代码,person会在111,222 中间释放
//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 对象才被释放。weak 原理说明
一个对象可能会被多次弱引用,当这个对象被销毁时,我们需要找到这个对象的所有弱引用,所以我们需要将这些弱引用的地址(即指针)放在一个容器里(比如数组)。当对象不再被强引用时需要销毁的时候,可以在SideTable中 通过这个对象的地址找到引用值,清空引用值。同时, SideTable
结构中还有weak_table_t
结构,该结构也是一个散列表,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
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
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
滑动效果的关键技术点。
verbose
意思为 冗长的、啰嗦的,一般在程序中表示详细信息。此参数可以显示命令执行过程中都发生了什么。pod install
或pod update
可能会卡在Analyzing dependencies
步骤,因为这两个命令会升级 CocoaPods 的 spec 仓库
,追加该参数可以省略此步骤,命令执行速度会提升。普遍开发者得理解是:一个是数据,一个是操作。如果从数据传递方向的角度来看,两者的本质是数据传递的方向不同。dataSource
是外部将数据传递到视图内,而 delegate
是将视图内的数据和操作等传递到外部。实际开发封装自定义视图,可以参照数据传递方向分别设置 dataSource
和 delegate
。
真正的 MVC 应该是苹果提供的经典UITableView
的使用,实际开发中经常在 Cell
中引入Model
,本质上来说不算是真正的 MVC ,只能算是 MVC 的变种。真正的 MVC 中 View 和 Model 应该是完全隔离的。
相同点:
不同点:
补充:指针函数和函数指针的区别
指针函数是指带指针的函数,即本质是一个函数,函数返回类型是某一类型的指针。它是一个函数,只不过这个函数的返回值是一个地址值。
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 签名机制,文章中可以找到答案。
这是文章前言的问题,请参考 这篇文章 。
不能确定代码的运行顺序和结果,是线程不安全的。线程安全是相对于多线程而言的,单线程不会存在线程安全问题。因为单线程代码的执行顺序是唯一确定的,进而可以确定代码的执行结果。
线程不安全的本质原因在于:表面展现在我们眼前的可能是一行代码,但转换成汇编代码后可能对应多行。当多个线程同时去访问代码资源时,代码的执行逻辑就会发生混乱。如数据的写操作,底层实现可能是先读取,再在原有数据的基础上改动。如果此时有一个读操作,原本意图是想在写操作完毕之后再读取数据,但不巧的这个读操作刚好发生在写操作执行的中间步骤中。虽然读操作后与写操作执行,但数据读取的值并不是写操作的结果值,运气不好时还可能发生崩溃。
- (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
方法,并进行各种objc结构的初始化(注册Objc类 、初始化类对象等等)。到此为止,可执行文件和动态库中所有的符号(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行),为减少函数调用的开销,可适当使用内联函数。
有两个类 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]
直接调用父类方法的原因。
待更新。。。。。。
说实在的有时会对各种渲染框架感觉混乱,一会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 *)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 *)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 大小。但会影响到崩溃的定位。保持默认的开启,不做修改。
苹果自身优化