iOS多线程

Objective-C本身不支持多继承 可以通过一下方式实现多继承

classA 实现了methodA 方法 classB 实现了 methodB 方法 classC 要同时实现methodA和methodB方法 在C++ 中用多继承就能实现,但是Objective c 不支持多重继承,那如何实现。

方法1. 组合方式,用ClassC 添加ClassA ,ClassB成员变量 来调用methodA,methodB

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
//定义ClassA以及其methodA
@interface ClassA : NSObject {
}
- (void)methodA;
@end
//定义ClassB以及其methodB
@interface ClassB : NSObject {
}
- (void)methodB;
@end
//定义ClassC以及其需要的methodA,methodB
@interface ClassC : NSObject {
ClassA *a;
ClassB *b;
}
- (id)initWithA:(ClassA *)A b:(ClassB *)B;
- (void)methodA;
- (void)methodB;
@end
//注意在ClassC的实现
@implementation ClassC
- (id)initWithA:(ClassA *)A b:(ClassB *)B{
a=[[ClassA alloc] initWithClassA: A];//[A copy];
b=[[ClassB alloc] initWithClassB: B];//[B copy];
}
- (void)methodA{
[a methodA];
}
- (void)methodB{
[b methodB];
}

方法2.协议protocol

设置ClassA delegate和 ClasssB delegate 以及实现方法ClassA里的methodA,和ClasssB里的methodB。ClassC遵守这两个协议就可以。

方法3.类别

ClassC的类别 可以实现ClassA的methodA和ClassB的methodB两个方法,这样ClassC就可以调用methodA和methodB

方法4.消息转发机制

我们知道objective-c中调用方法的方式是发消息,那如果给一个实例对象发一个未定义的消息呢?结果就是crash,其实这中间系统给我们第二次机会,就是可以转发该消息
如果未调到定义的消息,runtime会给该实例第二次机会,首先调用methodSignatureForSelector 或去方法签名,然后调用forwardInvocation,如果用户自己定义的类,没有重写这两个方法,即不支持方法转发

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
@interface LOCBird : NSObject {
NSString* _name;
}
@end
@implementation LOCBird
- (id)init
{
self = [super init];
if (self) {
_name = [[NSString alloc] initWithString:@"I am a Bird!!"];
}
return self;
}
- (void)dealloc
{
[_name release];
[super dealloc];
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
NSMethodSignature* signature = [super methodSignatureForSelector:aSelector];
if (signature==nil) {
signature = [_name methodSignatureForSelector:aSelector];
}
NSUInteger argCount = [signature numberOfArguments];
for (NSInteger i=0 ; i<argCount ; i++) {
NSLog(@"%s" , [signature getArgumentTypeAtIndex:i]);
}
NSLog(@"returnType:%s ,returnLen:%d" , [signature methodReturnType] , [signature methodReturnLength]);
NSLog(@"signature:%@" , signature);
return signature;
}
- (void)forwardInvocation:(NSInvocation *)anInvocation
{
NSLog(@"forwardInvocation:%@" , anInvocation);
SEL seletor = [anInvocation selector];
if ([_name respondsToSelector:seletor]) {
[anInvocation invokeWithTarget: _name];
}
}
@end

多继承消息转发原理就是 ClassC 没法实现methodA 和methodB 但是有成员变量ClassA 实力和ClassB实例。可以在用ClassC调用methodA 和methodB方法的时候消息转发给对应的实例就不会导致crash。其实和1组合方式类似。

深入理解Objective-C:Category

Category使用场景

Category是Objective-C 2.0之后添加的语言特性,Category的主要作用是为已经存在的类添加方法。除此之外,apple还推荐了Category的另外两个使用场景
可以把类的实现分开在几个不同的文件里面。

不过除了apple推荐的使用场景,广大开发者脑洞大开,还衍生出了category的其他几个
使用场景:

  • 模拟多继承
  • 把framework的私有方法公开

    Category好处

    这样做有几个显而易见的好处,
  • 可以减少单个文件的体积
  • 可以把不同的功能组织到不同的category里
  • 可以由多个开发者共同完成一个类
  • 可以按需加载想要的category 等等。
  • 声明私有方法

Category和Extension

Extension看起来很像一个匿名的Category,但是Extension和有名字的Category几乎完全是两个东西。 Extension在编译期决议,它就是类的一部分,在编译期和头文件里的
@interface以及实现文件里的@implement一起形成一个完整的类,它伴随类的产生而产生,亦随之一起消亡。extension一般用来隐藏类的私有信息,你必须有一个类的源码才能为一个类添加extension,所以你无法为系统的类比如NSString添加extension。

但是Category则完全不一样,它是在运行期决议的。
就Category和Extension的区别来看,我们可以推导出一个明显的事实,Extension可以添
加实例变量,而Category是无法添加实例变量的(因为在运行期,对象的内存布局已经确
定,如果添加实例变量就会破坏类的内部布局,这对编译型语言来说是灾难性的)。

挑灯细览-Category真面目

我们知道,所有的OC类和对象,在runtime层都是用struct表示的,Category也不例外,在
runtime层,Category用结构体Category_t(在objc-runtime-new.h中可以找到此定义),
它包含了
1)、类的名字(name)
2)、类(cls)
3)、Category中所有给类添加的实例方法的列表(instanceMethods)
4)、Category中所有添加的类方法的列表(classMethods)
5)、Category实现的所有协议的列表(protocols)
6)、Category中添加的所有属性(instanceProperties)

需要注意的有两点:
1)、Category的方法没有“完全替换掉”原来类已经有的方法,也就是说如果Category和原来
类都有methodA,那么Category附加完成之后,类的方法列表里会有两个methodA
2)、Category的方法被放到了新方法列表的前面,而原来类的方法被放到了新方法列表的
后面,这也就是我们平常所说的Category的方法会“覆盖”掉原来类的同名方法,这是因为
运行时在查找方法的时候是顺着方法列表的顺序查找的,它只要一找到对应名字的方法,
就会罢休,殊不知后面可能还有一样名字的方法。
系统在初始化runtime后,main函数启动前对其进行bind,此时会调用map_images方法
对其进行类结构初始化等处理,然后再调用在load_image方法调用所有的load方法,所以
在runtime调用项目中所有的load方法时,我们所有的类的结构已经
加载进内存并初始化了,此时在load方法中可以创建任何类的实例并给他们发送消息。

1.由于load方法在main函数前被调用,所以在load方法中尽量避免费时的操作,因为这些
操作都将导致程序启动时间的延长。
2.runtime中call_load_methods里是通过load方法的地址直接调用的load方法,而不是通
过消息机制来调用的。

Category Q & A

1)、在类的+load方法调用的时候,我们可以调用category中声明的方法么?
2)、这么些个+load方法,调用顺序是咋样的呢?

1)、可以调用,因为附加Category到类的工作会先于+load方法的执行
2)、+load的执行顺序是先类,后Category,而Category的+load执行顺序是根据编译顺序
决定的。
虽然对于+load的执行顺序是这样,但是对于“覆盖”掉的方法,则会先找到最后一个编译的
Category里的对应方法。

const extern static 详解

不管是从事哪种语言的开发工作,const extern static 这三个关键字的用法和原理都是我们必须明白的。本文将对此做出非常详细的讲解。

const

const是这三个中最简单的一个关键字。主要用于声明常量。常量和变量的样子没什么两样,只是前者的值是不可修改的。

举个例子:

1
2
int const a;
const int a;

这两条语句都把a声明为一个整数,它的值不能被修改,在这里,这两条语句是等价的,只是表现形式不同。

那么问题来了,既然a的值是不能被修改的,那我应该如何让a在一开始就有一个值呢?答案分两种情况:

1. 在声明时就赋值 int const a = 15;
2. 函数中被声明为const的形参在函数被调用时会得到形参的值。
1
2
3
4
5
int func(int const a) {
return a + 10;
}
int b = func(10);
printf("%i",b);

打印结果为20,可以看出,a在调用func的时候被赋值为10;

当const修饰指针变量的时候,情况就变得更加有趣了,因为指针变量有两样东西都有可能成为常量,指针变量 和 它指向的实体。我们再看下边的这个例子:

首先我们先声明一个普通的指针

1
int *p;

我们在int和* 之间加上const,就变成了下边的代码:

1
int const *p;

那么现在p就变成了一个指向整型常量的指针了,你可以修改指针p的值,但不能修改p指向的值。我们举个例子:

1
2
3
4
5
6
int a = 10;
int b = 20;
int const *p = &a;
int *p1 = &b;
p = p1;
printf("%i",*p);

上边的代码中,我们让p指向a,p1指向b,然后修改指针p,最后打印结果为20,这说明我们可以修改指针p的值。再看下边的代码:

1
2
3
4
5
6
7
int a = 10;
int b = 20;
int const *p = &a;
int *p1 = &b;
// 下边的代码会报错
*p = 60;
printf("%i",*p);

好了,通过上边的代码相信大家应该能够明白const放在int和*之间的作用了,那么我们现在把const放在*和p之间会发生什么呢?

1
int * const p;

此时,指针p为一个指向整型的常量指针,也就是说不能修改指针的值,可以修改它指向的值。我们依然举例说明:

1
2
3
4
5
int a = 10;
int b = 20;
int * const p = &a;
*p = 50;
printf("%i",a);

由上边代码可以看出,我们通过p修改了a的值,同样,打印结果为50。我们尝试修改指针p:

1
2
3
4
5
6
7
8
int a = 10;
int b = 20;
int * const p = &a;
*p = 50;
int *p1 = &b;
// 下边代码会报错
p = p1;
printf("%i",a);

注意:如果把代码写成这样

1
int const * const p;

无论是指针还是它指向的整型都是常量,不可修改。

使用说明: 当你声明变量时,如果变量的值不会被修改,你应该在声明中使用const关键字。这种做法不仅使你的意图在其他阅读你的程序的人面前得到更清晰的展现,而且当这个值被意外修改时,编译器能够发现这个问题。

作用域
可能很多同学会认为 作用域 很简单,其实不然,很多优秀的代码都有一个共同点,就是合理的利用了标识符的作用域。同样,这也与变量的存储属性有关系,后边我们会解释到的。

编译器能够确认四种不同的作用域,他们分别为:

代码块作用域(block scope)
文件作用域(file scope)
原型作用域(prototype scope)
函数作用域(function scope)
1.代码块作用域(block scope)
位于一对花括号之间的所有语句成为一个代码块。任何在代码块的开始位置声明的标识符都具有代码块作用域,表示他们可以被这个代码中的所有语句访问。

当代码块处于嵌套转态时,声明于内层代码块的标识符的作用域到达该代码块的尾部便告终止。然而,如果内层代码块有一个标识符的名字与外层代码块的一个标识符同名,内层的那个标识符就将隐藏外层的标识符—–外层的那个标识符无法在内层代码中通过名字访问。

这里说一些有意思的事,由于两个代码块的变量不可能同时存在,所以编译器可以把他们放到同一个内存地址中。

注意: 我们应该避免在嵌套的代码块中使用相同的变量名,这样会在程序的调试和维护期间引起混淆。

2.文件作用域(file scope)
任何在所有代码块之外声明的标识符都具有文件作用域(file scope),它表示这些标识符从他们的声明之处直到它所在的源文件结尾处都是可以访问的。

注意: 在头文件中通过#include或者#import导入的文件,就好像写到该头文件中一样,他们的作用域并不会局限于他们自身的文件中。

3.原型作用域(prototype scope)
原型作用域(prototype scope)可能不太好理解,我们看上图中的3和8,这两个函数被声明了。也就是说只在声明函数的时候,这个形参可有可无,名字和函数定义的形参可以相同,也可以不同,唯一能冲突的地方是,不能再同一函数声明中不止一次的使用同一名字。

4.函数作用域(function scope)
该作用域只适用于语句标签,语句标签用于goto语句,一个函数中的所有语句标签必须是唯一的。举个例子:

1
2
3
4
5
6
7
8
9
#include <stdio.h>
int main(int argc, char *argv[])
{
int i=1;
tt:printf("%d\n",i++);
if (i<10)
goto tt;
return 0;
}

链接属性
在讲解extern static之前,还必须了解链接属性这个概念,出现了链接这两个字,说明跟代码的编译有关系。

我们知道,当组成一个程序的各个源文件分别被编译后,所有的目标文件以及那些从一个或多个函数库引用的函数链接在一起,形成可执行文件。然而,会有这样一种情况,如果相同的标识符,比如说int a,出现在几个不同的源文件中时, 该怎么处理这种情况呢?

我们为每个标识符加一个属性,这个属性告诉编译器如何处理不同源文件中的标识符,那么这个属性就是链接属性。链接属性一共有三种:

external(外部) 属于external属性的标识符不论声明多少次,位于几个源文件,都表示同一实体。
internal(内部) 属于internal属性的标识符在同一个源文件内的所有声明都指向同一个实体,但不同源文件的多个声明分属不同的实体。
none(无) 没有链接属性的标识符(none)总是被当做单独的个体,也就是说该标识符的多个声明被当做独立不同的个体。
下边我用一个例子来演示一下上边说的内容:

首先我新建了一个工程,在main.m中写了下边代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#import <UIKit/UIKit.h>
#import "AppDelegate.h"
#include <float.h>
int a = 100;
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}

可以看出,定义了一个变量a,这个变量a默认属性为外部链接(external),也就是说我不能在别的文件中再次声明变量a了。然后我在ViewController.m中写了下边的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
extern int a;
printf("a:%i -p:%p\n",a,&a);
extern int a;
printf("a:%i -p:%p\n",a,&a);
extern int a;
printf("a:%i -p:%p\n",a,&a);
extern int a;
printf("a:%i -p:%p\n",a,&a);
extern int a;
printf("a:%i -p:%p\n",a,&a);
}
打印结果:
a:100 -p:0x10d9a0dc8
a:100 -p:0x10d9a0dc8
a:100 -p:0x10d9a0dc8
a:100 -p:0x10d9a0dc8
a:100 -p:0x10d9a0dc8

可以看出,extern int a; 这一句的extern关键字把int a 的链接属性变成了外部链接,因此编译器就会去取main.m中的a的值。我虽然多次声明了extern int a;,但都指向了同一实体。

我们通过上面的代码,再次对链接属性进行解释。在缺省情况下,标识符b,c和f的链接属性为external。其余标识符的链接属性为none。比较特殊的是函数f。他其实是一个外部链接属性,就像我们调用系统函数一样,f只是一个函数名称。但我们在该源文件调用函数f时,可能会链接别的源文件中f的定义。甚至这个函数的定义出现在某个函数库中。

extern / static

其实,关键字extern和static用于在声明中修改标识符的链接属性。如果某个声明在正常情况下具有external链接属性,在它前面加上static关键字,可以使他的链接属性变为internal,也就是只能在源文件中被操作。

static是很有用的,当我们只想把一个变量或者函数限制在本源文件中,不行被别的文件或人员访问的时候,就是使用static的时候。

注意: static只对缺省链接属性为external的声明才具有改变链接属性的作用,在上图中的5中,把代码改成static int e;,这个时候static的作用就不是改变链接属性。而是为变量e分配一个静态内存,每次访问这个变量,都会在这个静态内存中取值。

从技术角度讲,这两个关键字只有在声明中才是必须的,当用于具有文件作用域的声明时,这个关键字是可选的。然而,如果你在一个地方定义变量,并在其他源文件使用这个变量的声明时添加extern关键字,可以使读者更容易理解你的用途。

当extern关键字用于源文件中一个标识符的第一次声明时,它指定该标识符具有external链接属性。但是,如果它由于该标识符的第2次或以后的声明时,它并不会更改由第一次声明所指定的链接属性。在下图中的声明4并不会改变i的链接属性。

存储类型(storage class)

变量的存储类型这个概念对我们来说也很重要。变量的存储类型是指存储变量的内存类型。变量的存储类型决定变量何时创建,何时销毁以及它的值将保持多久。有三个地方可以存储变量:普通内存,运行时堆栈,硬件寄存器。

变量的缺省存储类型取决于它的声明位置。凡是在任何代码块之外声明的变量总是存储于静态内存中,也就是不属于堆栈的内存,这类变量称为静态变量。对于这类变量,你无法为它们指定其他存储类型。 静态变量在程序运行之前创建,在程序的整个执行期间始终存在,它始终保持原来的值,除非给他赋一个不同的值或程序结束。

在代码块内部声明的变量的缺省存储类型是自动的(automatic),也就是说它存储于堆栈中,称为自动变量。 有一关键字auto就是用于修饰这种存储类型的,但它极少用,因为代码块中的变量在缺省情况下就是自动变量。当程序执行到声明自动变量的代码块时,自动变量才被创建,当程序的执行流离开该代码块时,这些自动变量便自行销毁。 也就是说函数的调用也是有时间的,为了让代码更快,可以适当的加入内联函数。

如果该代码块被数次执行,例如一个函数被反复调用,这些自动变量每次都将从新创建。在代码块再次执行时,这些自动变量在堆栈中所占用的内存位置有可能和原先的位置相同,也可能不同。即使他们所占据的位置相同,你也不能保证这块内存同时不会有其他的用途。因此我们可以说,自动变量在代码块执行完毕后就消失。

对于在代码块内部声明的变量,如果给他加上关键字static,可以使它的存储类型从自动变为静态。 具有静态存储类型的变量在整个程序执行过程中一直存在,而不仅仅在声明它的代码块的执行时存在。注意:修改变量的存储属性并不表示修改变量的作用域,它仍然只能在代码块内部按名字访问。

函数的形式参数不能声明为静态,因为实参总是在堆栈中传递给函数,用于支持递归。

关键字register可以用于自动变量的声明,提示他们应该存储于机器的硬件寄存器而不是内存中,这类变量称为寄存器变量。通常寄存器变量比存储于内存中的变量访问起来效率更高。但是编译器不一定要理睬register关键字,如果有太多的变量被声明为register,它只会选取前几个实际存储于寄存器中,其他的就按普通的自动变量处理。如果一个编译器自己具有一套寄存器优化方法,它也可能忽略register关键字,其依据是由编译器决定哪些变量存储于寄存器中比人脑的决定更为合理些。

在典型情况下,你希望把使用频率最高的那些变量声明为寄存器变量。在有些计算机中,如果把指针声明为寄存器变量,程序的效率将能得到提高,尤其是那些频繁执行间接访操作的指针。你可以把函数的形参声明为寄存器变量,编译器会在函数的起始位置生成指令,把这些值从堆栈复制到寄存器中。但是,完全有可能,这个优化措施所节省的时间和空间上的开销还抵不上复制和几个值所使用的开销。

我们举个例子,程序的计算是在cpu中执行的,那么要获取计算用的数据,可以在内存中获取,也可以在寄存器中获取,相对于内存来说,寄存器的读取效率更高一些。我们把数据放到寄存器中拱cpu读取,执行完毕后,在把寄存器原来的值恢复。

寄存器变量的创建和销毁时间和自动变量相同,但它需要一些额外的工作。在一个使用寄存器变量的函数返回之前,这些寄存器先前存储的值必须恢复,确保调用者的寄存器变量未被破坏,这也是为什么寄存器效率高德原因,不需要反复的创建和销毁实际存储的实体。许多机器使用运行时堆栈来完成这个任务。当程序开始执行时,它把需要使用的寄存器的内容都保存到堆栈中,当函数返回时,这些值再复制回寄存器中。

在许多机器的硬件实现中,并不为寄存器指定地址。同样,由于寄存器值得保存和恢复,某个特定的寄存器在不同的时刻所保存的值不一定相同。基于这个原理,机器并不向你提供寄存器变量的地址。

初始化

现在我们把话题返回到变量声明中变量的初始化问题。自动变量和静态变量的初始化存在一个重要区别。

在静态变量的初始化中,我们可以把想要初始化的值放在当程序执行时变量将会使用的那个位置。当可执行文件载入到内存中时,这个已经保存了正确初始值的位置将赋值给那个变量。完成这个任务并不需要额外的时间,也不需要额外的指令,变量将会得到正确的值,因为已经知道变量的内存地址。如果不显式地指定初始值,静态变量将初始值为0.

自动变量的初始化需要更多的开销,因为当程序链接时还无法确定自动变量的存储位置。事实上,函数的局部变量在函数的每次调用中占据不同的位置。基于这个理由,自动变量没有缺省的初始值,而显式的初始化将在代码块的起始处插入一条隐式的赋值语句。

这种技巧造成4种后果:

自动变量的初始化较之赋值语句效率并无提高。 除了声明为const的变量之外,在声明变量的同时进行初始化和先声明后赋值只有风格之差,并无效率之别。

1
2
3
4
5
6
int func(int x) {
int a;
a = 100;
// 上边的代码是先声明后初始化,对于自动变量,跟 int a = 100; 没有效率上的区别
return a;
}

这条隐式的赋值语句,是自动变量在程序执行到他们所声明的函数时,每次都将重新初始化。这个与静态变量有大不同,后者只是在程序开始执行前初始化一次。
由于初始化在运行时进行,你可以用任何表达式作为初始化值。

1
2
3
4
5
int func(int x) {
// 由于自动变量实在运行时才赋值的,所以当运行到下边的代码时,已经知道x的值,然后给a赋值
int a = x + 10;
return a;
}

除非你对自动变量进行显示的初始化,否则当自动变量创建时,它们的值总是垃圾。

static总结

当用于不同的上下文环境时,static关键字具有不同的意思。

当它用于函数定义时,或用于代码块之外的变量声明时,static关键字用于修改标识符的链接属性,从external改为internal,但标识符的存储类型和作用域不受影响。用这种方式声明的函数或变量只能在声明他们的源文件中访问。
当它用于代码块内部的变量声明时,static关键字用于修改变量的存储类型。从自动变量修改为静态变量,但变量的链接属性和作用域不受影响。用这种方式声明的变量在程序执行之前创建,并在程序的整个执行期间一直存在。

你理解多线程吗?

工作忙完一段时间,整理资料,总结知识,发现对多线程这块有点遗忘,写篇博客谈谈多线程,温故而知新。
大家面试,想必都会被问到,你理解多线程吗?还可能追问道
1、有哪几种多线程,基于什么语言的?
2、生命周期是如何管理的?
3、你更倾向于那种?现在常用的两种,谈谈你的看法?

第一种:pthread

特点:
1、一套通用的多线程API
2、适用于Unix、Linux、Windows等系统
3、跨平台、可移植
4、使用难度大
5、使用语言:C语言
6、开发使用频率:几乎不用,
7、线程生命周期:由程序员进行管理

第二种:NSThread

特点:
1、使用更加面向对象
2、简单易用,可直接操作线程对象
3、使用语言:OC语言
4、开发使用频率:偶尔使用
5、线程生命周期:由程序员进行管理

第三种:GCD

特点:
1、旨在替换NSThread等线程技术
2、充分利用设备的多核(自动)
3、使用语言:C语言
4、开发使用频率:经常使用
5、线程生命周期:自动管理

第四种:NSOperation

特点:
1、基于GCD(底层是GCD)
2、比GCD多了一些更简单的实用的功能
3、使用更加面向对象
4、使用语言:OC语言
5、开发使用频率:经常使用
6、线程生命周期:自动管理

多线程的原理

同一时间,CPU只能处理1条线程,只有一条线程在工作,多线程并发执行,其实就是CPU快速的在多条线程之间调度,如果CPU调度线程的时间足够快,就造成了多线程并发的假象了;
思考 如果线程非常多,会发生什么情况?
CPU会在N多线程之间调度,CPU会累死,消耗大量的CPU资源,每条线程被调度执行的频率被大大降低。

多线程优点

能适当的提高程序执行的频率
能适当提高资源利用率(CPU)

多线程缺点

开启线程需要占用一定的内存空间(默认情况下,主线程占用1M,子线程占用512KB),如果开启大量的线程,会占用大量的内存空间,降低程序的性能,线程越多,CPU在调度线程上的开销就越大,程序设计更加复杂:比如线程之间的通信,多线程的数据共享。

你更倾向于哪一种?

就本人而已我是倾向于GCD的 所以下面主要结合实例谈谈GCD
GCD技术是一个轻量级,底层实现隐藏的申请技术,我们能够通过GCD和Block轻松实现多线程编程,有时候,GCD相比其他系统提供的多线程方法更加有效,当然,有时候GCD不是最佳选择,另一个多线程技术NSOperationQueue让我们能够将后台线程以队列方式依序执行,并提供更多操作入口,这和GCD的实现有些类似。

GCD执行原理

GCD有一个底层线程池,这个池中的线程可以重用,当一段时间后这个线程没有被调用的话,这个线程就会被销毁。注意:开多少线程是由底层线程池决定的(建议3-5),池是系统自动来维护,我们只需要关系的是向队列中添加任务,队列调度即可。
1.如果队列中存放都是同步任务,则任务出队后,底层线程池会提供一条线程供和这个任务执行,任务执行完毕后这条线程在回到线程池,这样队列中的任务反复调度,因为是同步的,所以当我们用currentThread打印的时候,就是同一条线程、
2.如果队列中存放的是异步任务,当任务出队后,底层线程池会提供一个线程供任务执行,因为是异步执行,队列中的任务不需要等待当前任务执行完毕就可以调度下一个任务,这时底层线程池中会再次提供一个线程供第二个任务执行,执行完毕后在回到底层的线程池中。

通过案例了解GCD的执行原理

案例一

1
2
3
4
5
6
7
NSLog(@"1");
dispatch_sync(dispatch_get_main_queue(),^{
NSLog(@"2");
});
NSLog(@"3");
输出结果:1

分析 首先执行任务1,接下来程序遇到了同步线程,那么它会进入等待,等待任务2执行完,然后在执行任务3。但是这是队列,有任务来,当然会将任务加到队尾,然后遵循FIFO原则执行任务,那么,现在任务2就被加到最后了,任务3排在了任务2前面,问题来了:任务3要等任务2执行完才能执行,任务2又排在任务3后面,意味着任务2要在任务3执行完才能执行,所以他们进入了互相等等的局面,就卡在这里,发生死锁。

案例二

1
2
3
4
5
6
7
NSLog(@"1");
dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0),^{
NSLog(@"2");
});
NSLog(@"3");
输出结果:1 2 3

分析 首先执行任1,接下来会遇到一个同步线程,程序会进入等待,等待任务2执行完成以后,才能继续任务3,从dispatch_get_global_queue可以看出,任务2被加入到了全局的并行队列中,当并行队列执行完成任务2后,返回到主队列,继续执行任务3

案例三

1
2
3
4
5
6
7
8
9
10
11
dispatch_queue_t queue = dispatch_queue_create("com.demo.serialQueue", DISPATCH_QUEUE_SERIAL);
NSLog(@"1");
dispatch_async(queue, ^{
NSLog(@"2");
dispatch_sync(queue, ^{
NSLog(@"3");
});
NSLog(@"4");
});
NSLog(@"5");
输出结果:1 5 2

案例四

1
2
3
4
5
6
7
8
9
10
NSLog(@"1");
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSLog(@"2");
dispatch_sync(dispatch_get_main_queue(), ^{
NSLog(@"3");
});
NSLog(@"4");
});
NSLog(@"5");
输出结果:1 5 2 3 4

分析 首先将【任务1、异步线程、任务5】加入到MainQueue中,异步线程中的任务是,【任务2,同步线程、任务4】。所以,先执行任务1,然后将异步线程中的任务加入到GlobalQueue中,因为异步线程,所以任务5不用等待,结果就是2和5的输出顺序不一定。然后在看异步线程中的任务执行顺序。任务2执行完以后,遇到同步线程,将同步线程中的任务加入到MainQueue中,这时加入的任务3在任务5的后面,当任务3执行完以后,没有了阻塞,程序继续执行任务4

案例五

1
2
3
4
5
6
7
8
9
10
11
12
13
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSLog(@"1");
dispatch_sync(dispatch_get_main_queue(), ^{
NSLog(@"2");
});
NSLog(@"3");
});
NSLog(@"4");
while (1) {
}
NSLog(@"5");
输出结果:4 1

分析 和上面几个案例的分析类似,先来看看都有哪些任务加入了Main_Queue:【异步线程、任务4、死循环、任务5】。加入到Global_Queue异步线程中的任务有:【任务1、同步任务、任务3】。第一个就是异步线程,任务4不用等待,所以结果任务1,和任务4顺序不一定,任务4完成后,程序进入死循环,Main_Queue阻塞。但是加入到Global_Queue的异步线程不受影响,继续执行任务1后面的同步线程。同步线程中,将任务2加入到了主线程,并且,任务3等待任务2完成以后才能执行。这时的主线程,已经被死循环阻塞了。所以任务2无法执行,当然任务3也无法执行,在死循环后的任务5也不会执行。

iOS App更换图标

前段时间查看Apple官方文档,发现了一个新功能就是更换App图标,发现这个功能后,一顿暗喜,装逼的时候又到了! 可以给用户更好的体验、

例如可以在不同的节日更换不同的图标啦
屏幕截图

美中不足的是该功能(API)当前只支持iOS10.3以上的系统,所以只能当做一项附加功能来进行使用。下面将详细讲解下如何使用代码来实现此功能。

该功能应用的场景
1、白天/夜间模式切换,在切换App主色调同时切换App图标。
2、各类皮肤主题(淘宝就可换肤),附带App图标一块更换。
3、利用App图标表达某种特定功能,如节日,天气。

API与文档

API方法

文档

1、supportsAlternateIcons

只有系统允许改变你的app图标时该值才为YES。你需要在Info.plist文件中的CFBundleIcons这个键内声明可更换的app图标。

2、alternateIconName

当系统展示的是你更换后的app图标时,该值即为图标名字(Info.plist中定义的图标名字)。如果展示的是主图标时,这个值为nil。

3、setAlternateIconName:completionHandler:

alertnateIconName参数:该参数为需要更换的app图标名字,是在你的Info.plist中的CFBundleAlertnateIcons键里定义的。如果你想显示的是用CFBundlePrimaryIcon键所定义的主图标的话,就传入nil。CFBundleAlertnateIcons与CFBundlePrimaryIcon键都是在CFBundleIcons里面定义的。

completionHandler参数:该参数用来处理(更换)结果。当系统尝试更改app的图标后,会将结果数据通过该参数传入并执行(该执行过程是在UIKit所提供的队列执行,并非主队列)。该执行过程会携带一个参数:error。如果更换app图标成功,那么这个参数就是nil。如果更换过程中发生了错误,那么该对象会指明错误信息,并且app的图标保持不变。


使用该方法改变app图标为主图标或者可更换的图标。只有在supportsAlternateIcons的返回值为YES时才能更换

你必须在Info.plist文件的CFBundleIcons键里面声明可以更换的app图标(主图标和可更换图标)。如果需要获取关于可更换图标的配置信息,请查阅 Information Property List Key 里面有关CFBundleIcons的描述。

可变更App图标的配置方法


Info.plist是个字典,假设为NSDictionary *infoPlist。

1.CFBundleIcons是Info.plist字典里的一个键@”CFBundleIcons”。
2.CFBundleIcons对应的value是个字典。
3.CFBundleIcons里面能够包含的键有:CFBundlePrimaryIcon、CFBundleAlternateIcons、UINewsstandIcon。

代码展示下这个绕口的结构:

1
2
3
4
5
6
7
8
NSDictionary *infoPlist;
infoPlist = @{
@"CFBundleIcons" : @{
@"CFBundlePrimaryIcon" : xxx,
@"CFBundleAlternateIcons" : xxx,
@"UINewsstandIcon" : xxx
}
};

1.CFBundleAlternateIcons所对应的value是个字典(iOS中),假设为NSDictionary * alertnateIconsDic。
2.alertnateIconsDic的键,都是备用图标的名字,假设为@”newAppIcon”和@”newAppIcon2”。
3.@”newAppIcon”的value是个包含CFBundleIconFiles和UIPrerenderedIcon这两个键的字典。
4.CFBundleIconFiles的value是字符串或者数组(数组内容也为字符串)。字符串的内容为各尺寸备用图标的名字。
5.UIPrerenderedIcon的value是BOOL值。这个键值所代表的作用在iOS7之后(含iOS7)已失效,在iOS6中可渲染app图标为带高亮效果。所以这个值目前我们可以不用关心。
代码展示下这个绕口的结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@"CFBundleAlternateIcons" : @{
@"newAppIcon" : @{
@"CFBundleIconFiles" : @[
@"newAppIcon"
],
@"UIPrerenderedIcon" : NO
},
@"newAppIcon2" : @{
@"CFBundleIconFiles" : @[
@"newAppIcon2"
],
@"UIPrerenderedIcon" : NO
}
};

实际配置文件(Info.plist)

对照着上述的配置文档,我们实际配置完的Info.plist是这样子的:

拖入对应的App图标:

在Build Phases -> Copy Bundle Resources加入对应的图片资源

然后调用此代码,传入对应的图标名称就可以看到App图标发生了改变啦

1
2
3
4
5
6
7
8
9
10
11
12
13
- (void)setAppIconWithName:(NSString *)iconName {
if (![[UIApplication sharedApplication] supportsAlternateIcons]) {
return;
}
if ([iconName isEqualToString:@""]) {
iconName = nil;
}
[[UIApplication sharedApplication] setAlternateIconName:iconName completionHandler:^(NSError * _Nullable error) {
if (error) {
NSLog(@"更换app图标发生错误了 : %@",error);
}
}];
}

难受 苹果限制真多,上传提交审核的时候出现了。

老老实实在info.plist里面加上这个key

项目在iOS11上遇到的小问题

iOS11正式版出了这么久了,在忙完新版本开发,写下在iOS11上的一些小问题。

一、App图标不显示

现象:升级到iOS11系统下自己的项目桌面app图标不见了
图标不见
出现这种情况我还以为自己手动删除了项目 Images.xcassets中的AppIcon导致没有图标。查看项目和发现这些AppIcon还在,突然发现在Xcode 9中AppIcon有了改变。
AppIcon
发现Xcode 8及之前的版本是可以直接在iTunes Connect上添加App icon。而Xcode 9则是把App icon放置在项目的asset catalog。所以需要把空缺的图标补上,其中一张是1024pt 1x的尺寸。如果没有正确添加iOS App图标,上传到App Store后可能会受到拒绝邮件。
心想既然图标变了那么LaunchImage呢,果不其然LaunchImage也有小变化为了适配苹果即将售出的iPHone X,这里我们需要在这里添加一张新的尺寸图1125px*2436px。
LaunchImage
那么究竟是什么导致,在iOS 11上面图标消失的呢。查证后发现。

原因: 使用了CocoaPods的Xcode工程,在iOS11版的手机上AppIcon不显示,原因是CocoaPods的资源编译脚本在iOS11下出了点问题。

解决办法
1.在Podfile添加脚本修改:
1). 在Podfile 添加如下代码.

1
2
3
4
5
6
7
8
post_install do |installer|
copy_pods_resources_path = "Pods/Target Support Files/Pods-[工程名]/Pods-[工程名]-resources.sh"
string_to_replace = '--compile "${BUILT_PRODUCTS_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}"'
assets_compile_with_app_icon_arguments = '--compile "${BUILT_PRODUCTS_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" --app-icon "${ASSETCATALOG_COMPILER_APPICON_NAME}" --output-partial-info-plist "${BUILD_DIR}/assetcatalog_generated_info.plist"'
text = File.read(copy_pods_resources_path)
new_contents = text.gsub(string_to_replace, assets_compile_with_app_icon_arguments)
File.open(copy_pods_resources_path, "w") {|file| file.puts new_contents }
end

需要注意的是,将[工程名] 换成自己工程的名称

2).然后运行

1
$pod install

2.手动修改
打开工程目录下:[工程名]/Pods/Target Support Files/Pods/Pods-resources.sh这个文件,替换最后一段代码:

修改前:

1
2
printf "%s\0" "${XCASSET_FILES[@]}" | xargs -0 xcrun actool --output-format human-readable-text --notices --warnings --platform "${PLATFORM_NAME}" --minimum-deployment-target "${!DEPLOYMENT_TARGET_SETTING_NAME}" ${TARGET_DEVICE_ARGS} --compress-pngs --compile "${BUILT_PRODUCTS_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}"
fi

修改后:

1
2
printf "%s\0" "${XCASSET_FILES[@]}" | xargs -0 xcrun actool --output-format human-readable-text --notices --warnings --platform "${PLATFORM_NAME}" --minimum-deployment-target "${!DEPLOYMENT_TARGET_SETTING_NAME}" ${TARGET_DEVICE_ARGS} --compress-pngs --compile "${BUILT_PRODUCTS_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" --app-icon "${ASSETCATALOG_COMPILER_APPICON_NAME}" --output-partial-info-plist "${BUILD_DIR}/assetcatalog_generated_info.plist"
fi

然后重新运行工程即可,配置完成后如果启动后发现还没有图标,现在系统低于iOS 11下的手机运行一次,再在iOS 11上启动就回发现有了。

参考:https://github.com/CocoaPods/CocoaPods/issues/7003

二、用到相机功能时闪退

现象:在用户在手机系统iOS 11里面使用App进行身份证、银行卡ORC识别的时候打开相机发生Crash现象;在iOS 11以下正常。
原因:iOS11下,苹果对相册的权限key做了调整
详见:CocoaKeys
解决办法:在Info.plist里添加以下权限
添加权限

三、H5和Native交互的时候控制更严格

现象:在iOS 11上点击UIWebView加载的H5页面,来与原生App进行交互出现Crash,报错webTread。

原因:iOS11下,UIWebView与原生交互的时候出现了线程安全问题;控制更加严格。

解决办法:把调用原生方法的代码放到主线程中运行。
web和Native交互

四、在Xcode 9中的无线调试

Xcode 9 里面把很多简单的快捷键给改复杂了,一些插件不支持了。最有利于开发者的地方就是Xcode 9中的无线调试了。
苹果诟病最多的产品:数据线
心疼的抱住我自己,穿着缝缝补补的衣服,用着自己拼拼接接的数据线
我的数据线

在Xcode 9中没有这样的烦恼了。

升级到Xcode9.0之后,可以通过Wifi连接iOS或tvOS设备进行无线调试。

要求: Xcode 9.0 以上版本、macOS 10.12.4以上版本、iOS 11.0以上版本, tvOS 11.0以上版本

操作步骤:

打开菜单 Window > Devices and Simulators, 然后在打开的菜单中选择 Devices选项.

通过数据线将您的设备,比如iPhone,连接至Mac电脑.
在如下图选择连接的设备,然后在右侧勾选[通过网络连接]复选框.

Xcode 会和你的设备进行配对. 一旦Xcode和设备配对成功,设备名称的右侧会显示一个网络图标

最后将设备的数据线从Mac电脑上取出,就可以通过Wifi无线调试了!

最近学习 JavaScript、node.js 的总结

大前端全栈的热潮下,最近趁工作不太忙,学习了JavaScript,node.js,做一个小小的Demo来记录一下学习的过程。大佬勿喷。小Demo主要功能是记录用户的个人信息,可以对用户的个人信息进行增删改查。前端页面使用HTML +CSS实现,JavaScript控制逻辑,后台使用node.js实现。下面我分三部分总结一下。

一、前端页面

1.1 页面

网上传着一句话:不会点HTML+CSS都不敢说自己是程序员,所以写了几个简单的页面,也没有什么技术含量,都是div、span、ul、li、input之类的标签。

下图就是我写的页面!

首页无CSS
增加页无CSS

1.2 CSS

哈哈是不是太丑了,那肯定不是有三个星期CSS功力的我写的页面,简单的加入点CSS样式之后,这才是我写的。

首页CSS
增加页CSS
css也就是 margin、padding、width、height等值的配置,当然变成一个高手这些远远不够。

二、前端服务器交互(AJAX)

只是做前端的相比都知道AJAX,这里说说我只怎么处理各个浏览器兼容,并且很高效
问题引出: 犹豫IE低版本不兼容XMLHttpRequest,所以每次调用AJAX就需要判断是否兼容,然后在调用合适的方法,假如浏览器用AJAX调用100个请求,那就要判断100次兼容。所以非常消耗性能和时间。
解决思想: 第一次执行creatXHR函数的时候,需要兼容判断,在判断完成后,直接把创建AJAX函数赋值(这里举例AJAX函数赋值就是A)给creatXHR函数,那么以后调用creatXHR函数的时候执行的就是AJAX函数赋值(A),可以省去其中的兼容性判断。

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
function creatXHR() {
var xhr = null,
flag = false,
ary = [
function () {
return new XMLHttpRequest;
},
function () {
return new ActiveXObject("Microsoft.XMLHTTP");
},
function () {
return new ActiveXObject("Msxml2.XMLHTTP");
},
function () {
return new ActiveXObject("Msxml3.XMLHTTP");
}
];
for (var i = 0, len = ary.length; i < len; i++) {
var curFn = ary[i];
try {
xhr = curFn();
creatXHR = curFn;
flag = true;
break;
} catch (e) {
}
}
if (!flag) {
throw new Error("your browser is not support ajax,please change you broser,try again");
}
return xhr;
}

AJAX向服务器端发送请求的四个步骤;

1
2
3
4
5
var xhr = creatXHR();//1、创建对象
xhr.open(_default.type, _default.url, _default.async);//2、请求类型,网址,是否异步
xhr.onreadystatechange = function () {
};//3、状态变化监听函数
xhr.send(_default.data);//4、发送请求

Demo中处理网络请求核心代码

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
function ajax(option) {
var _default = {
url: "",
type: "get",
dataType: "json",
async: true,
data: null,
getHead: null,
success: null
};
for (var key in option) {
if (option.hasOwnProperty(key)) {
_default[key] = option[key];
}
}
//解决缓存问题
if (_default.type === "get") {
_default.url.indexOf("?") >= 0 ? _default.url += "&" : _default.url += "?";
_default.url += "_=" + Math.random();
}
var xhr = creatXHR();
xhr.open(_default.type, _default.url, _default.async);
xhr.onreadystatechange = function () {
if (/^2\d{2}$/.test(xhr.status)) {
if (xhr.readyState === 2) {
if (typeof _default.getHead === "function") {
_default.getHead.call(xhr);
}
}
if (xhr.readyState === 4) {
var val = xhr.responseText;
if (_default.dataType === "json") {
val = "JSON" in window ? JSON.parse(val) : eval("(" + val + ")");
}
_default.success && _default.success.call(xhr, val);
}
}
};
xhr.send(_default.data);
}

三、后端(node.js)

3.1 环境配置

因为我事先安装好了,所以前两步操作无图(尴尬)
第一步:打开终端,输入以下命令安装Homebrew
ruby -e “$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install);
第二步:安装node,在终端输入以下命令
brew install node
第三步 查看node安装成功与否
node -v
查看是否验证安装node

3.2 node.js的最基本操作

1.node中提供的三个常用的模块

1
2
3
var http = require("http"),
fs = require("fs"),
url = require("url");

2.静态资源文件请求的处理

1
2
3
4
var server = http.createServer(function (req, res) {
// 静态资源文件请求的处理
// 数据接口的处理
});

3.服务创建成功执行回调函数

1
2
3
4
server.listen(8080, function () {
//->当服务创建成功,并且端口号也监听成功之后执行这个回调函数
console.log("server is create success,listening on 8080 port!");
});

3.3 静态资源文件请求的处理

静态资源文件请求的处理其中重要的点是为了兼容,要对请求文件的MIME Type进行匹配。MIME Type是什么?
在浏览器中显示的内容HTML、XML、CSS、PNG、GIF…… 那么,浏览器是如何区分它们,决定什么内容用什么形式来显示?,答案是 MIME Type,也就是该资源的媒体类型。

参考链接:http://tool.oschina.net/commons

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
var server = http.createServer(function (req, res) {
var urlObj = url.parse(req.url, true),
pathname = urlObj.pathname,
query = urlObj.query;
var reg = /\.(HTML|CSS|JS|ICO)/i;
if (reg.test(pathname)) {
var suffix = reg.exec(pathname)[1].toUpperCase();
var suffixMIME = "text/html";
switch (suffix) {
case "CSS":
suffixMIME = "text/css";
break;
case "JS":
suffixMIME = "text/javascript";
break;
}
try {
var conFile = fs.readFileSync("." + pathname, "utf-8");
res.writeHead(200, {'content-type': suffixMIME + ';charset=utf-8;'});
res.end(conFile);
} catch (e) {
res.writeHead(404, {'content-type': 'text/plain;charset=utf-8;'});
res.end("file is not found~");
}
return;
}
});

3.4 API数据接口的处理

这个是Demo实现了数据的增、删、改、查,所以这里简单的对增,删谈谈。

1
2
3
4
5
6
7
// 获取服务器端存储的数据
var customId = null,
customPath = "./json/custom.json",
result = {code: 1, msg: "", data: null},
con = fs.readFileSync(customPath, "utf-8");
con.length === 0 ? con = '[]' : null;
con = JSON.parse(con);

增加一条客户信息的时候要介绍一下node中requestListener的两个监听方法,

1
2
3
4
5
6
7
8
// 在接收到数据的时候调用
req.on("data", function (chunk) {
<!--数据传到服务器不是一次接受到,而是一段一段的数据所以要对数据进行整合 其中参数chunk就是一段一段的数据-->
});
// 在接收数据完成后调用
req.on("end", function () {
<!--数据传递完之后调用,调用之前要先判断数据是否为空-->
});

增加一条客户信息

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
// 增加一条客户信息相关代码
if (pathname === "/addInfo") {
//->获取客户端通过请求主体传递进来的内容
var str = '';
req.on("data", function (chunk) {
str += chunk;
});
req.on("end", function () {
if (str.length === 0) {
res.writeHead(200, {'content-type': 'application/json;charset=utf-8;'});
res.end(JSON.stringify({
code: 1,
msg: "增加失败,没有传递任何需要增加的信息"
}));
return;
}
var data = JSON.parse(str);
//->在现有的DATA中追加一个ID:获取CON中最后一项的ID,新的ID是在原有基础上加一即可,如果之前一项都没有,我们这一项的ID就是1
data["id"] = con.length === 0 ? 1 : parseFloat(con[con.length - 1]["id"]) + 1;
con.push(data);
fs.writeFileSync(customPath, JSON.stringify(con), "utf-8");
res.end(JSON.stringify({
code: 0,
msg: "增加成功!"
}));
});
return;
}

根据传入的用户ID删除这个用户信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//3)根据传递进来的客户ID删除这个客户
if (pathname === "/removeInfo") {
customId = query["id"];
var flag = false;
for (i = 0; i < con.length; i++) {
if (con[i]["id"] == customId) {
con.splice(i, 1);
flag = true;
break;
}
}
result.msg = "删除失败";
if (flag) {
fs.writeFileSync(customPath, JSON.stringify(con), "utf-8");
result = {
code: 0,
msg: "删除成功"
};
}
res.writeHead(200, {'content-type': 'application/json;charset=utf-8;'});
res.end(JSON.stringify(result));
return;
}

四、Demo演示

演示由于服务器中配置的端口号是8080,所以网址的时候需要加上端口号,
现在打开页面(不是本地的页面),通过IP访问。已经不是静态页面了。

Demo演示

因为自己平时学习,所以服务器弄在自己电脑上,所以这里的IP就是我电脑的IP地址。
在终端输入:

1
ifconfig | grep "inet " | grep -v 127.0.0.1

增加操作:
Demo演示

删除操作:
Demo演示

项目地址 麻烦各位大佬给个Starhttps://github.com/leerme/NodeDemo

Longest Substring Without Repeating Characters

最近在LeetCode上刷题,把自己平时学习的过程记录下来。

没有重复字符的最长子串

Given a string, find the length of the longest substring without repeating characters.

Examples:
Given “abcabcbb”, the answer is “abc”, which the length is 3.
Given “bbbbb”, the answer is “b”, with the length of 1.
Given “pwwkew”, the answer is “wke”, with the length of 3. Note that the answer must be a substring, “pwke” is a subsequence and not a substring.

下面是本人的swift实现方法,思路过于麻烦时间复杂度O(n^3);导致在字符串很长的时候超时,

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
class Solution {
func lengthOfLongestSubstring(_ s: String) -> Int {
var resoult = 0 ;
var string = "";
for i in 0 ..< s.characters.count{
for j in i+1 ... s.characters.count {
let indexI = s.index(s.startIndex, offsetBy: i);
let indexJ = s.index(s.startIndex, offsetBy: j);
string = s.substring(with: indexI..<indexJ);
if self.allUnique(string) {
resoult = max(resoult, string.characters.count);
}
}
}
return resoult;
}
func allUnique(_ string:String) -> Bool {
var set:Set<Character> = [];
for i in 0 ..< string.characters.count {
let index = string.index(string.startIndex,offsetBy:i)
let c = string[index];
if set.contains(c) {
return false;
}
set.insert(c);
}
return true;
}
}

优化:方法2 Sliding Window

The naive approach is very straightforward. But it is too slow. So how can we optimize it?

In the naive approaches, we repeatedly check a substring to see if it has duplicate character. But it is unnecessary. If a substring sij from index i to j - 1 is already checked to have no duplicate characters. We only need to check if s[j] is already in the substring sijs
​ij.
To check if a character is already in the substring, we can scan the substring, which leads to an O(n^2) algorithm. But we can do better.
By using HashSet as a sliding window, checking if a character in the current can be done in O(1).
A sliding window is an abstract concept commonly used in array/string problems. A window is a range of elements in the array/string which usually defined by the start and end indices, i.e. [i, j) (left-closed, right-open). A sliding window is a window “slides” its two boundaries to the certain direction. For example, if we slide [i, j)to the right by 11 element, then it becomes [i+1, j+1)(left-closed, right-open).
Back to our problem. We use HashSet to store the characters in current window [i, j)(j = i initially). Then we slide the index jj to the right. If it is not in the HashSet, we slide jj further. Doing so until s[j] is already in the HashSet. At this point, we found the maximum size of substrings without duplicate characters start with index ii. If we do this for all ii, we get our answer.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Solution {
public int lengthOfLongestSubstring(String s) {
int n = s.length();
Set<Character> set = new HashSet<>();
int ans = 0, i = 0, j = 0;
while (i < n && j < n) {
// try to extend the range [i, j]
if (!set.contains(s.charAt(j))){
set.add(s.charAt(j++));
ans = Math.max(ans, j - i);
}
else {
set.remove(s.charAt(i++));
}
}
return ans;
}
}

Complexity Analysis

Time complexity : O(2n) = O(n). In the worst case each character will be visited twice by i and j.

Space complexity : O(min(m, n)) Same as the previous approach. We need O(k) space for the sliding window, where k is the size of the Set. The size of the Set is upper bounded by the size of the string n and the size of the charset/alphabet m;

iOS 事件的传递和响应

谈一谈iOS事件的产生和传递

1.事件的产生

  • 发生触摸事件后,系统会将该事件加入到一个由UIApplication管理的事件队列中.
  • UIApplication会从事件队列中取出最前面的事件,并将事件分发下去以便处理,通常,先发送事件给应用程序的主窗口(keyWindow)。
  • 主窗口会在视图层次结构中找到一个最合适的视图来处理触摸事件,这也是整个事件处理过程的第一步

2.事件的传递

  1. 首先判断主窗口(keyWindow)自己是否能接受触摸事件
  2. 判断触摸点是否在自己身上
  3. 子控件数组中从后往前遍历子控件,重复前面的两个步骤(所谓从后往前遍历子控件,就是首先查找子控件数组中最后一个元素,然后执行1、2步骤
  4. view,比如叫做subView,那么会把这个事件交给这个subView,再遍历这个subView的子控件,直至没有更合适的view为止
  5. 如果没有符合条件的子控件,那么就认为自己最合适处理这个事件,也就是自己是最合适的view

UIView不能接收触摸事件的三种情况:

  • 不允许交互:userInteractionEnabled = NO
  • 隐藏:如果把父控件隐藏,那么子控件也会隐藏,隐藏的控件不能接受事件
  • 透明度:如果设置一个控件的透明度<0.01,会直接影响子控件的透明度。alpha:0.0~0.01为透明。

注 意:默认UIImageView不能接受触摸事件,因为不允许交互,即userInteractionEnabled = NO,所以如果希望UIImageView可以交互,需要userInteractionEnabled = YES。发生在事件传递的时候。

总结

  1. 点击一个UIView或产生一个触摸事件A,这个触摸事件A会被添加到由UIApplication管理的事件队列中(即,首先接收到事件的是UIApplication)。
  2. UIApplication会从事件对列中取出最前面的事件(此处假设为触摸事件A),把事件A传递给应用程序的主窗口(keyWindow)。
  3. 窗口会在视图层次结构中找到一个最合适的视图来处理触摸事件。(至此,第一步已完成)

iOS事件示例

如果想让某个view不能接收事件(或者说,事件传递到某个view那里就断了),那么可以通过刚才提到的三种方式。比如,设置其userInteractionEnabled = NO;那么传递下来的事件就会由该view的父控件处理。
例如,不想让蓝色的view接收事件,那么可以设置蓝色的view的userInteractionEnabled = NO;那么点击黄色的view或者蓝色的view所产生的事件,橙色的view就会成为最合适的view。事件都会由橙色的veiw处理。
所以,不管视图能不能处理事件,只要点击了视图就都会产生事件,关键看该事件是由谁来处理!也就是说,如果蓝色视图不能处理事件,点击蓝色视图产生的触摸事件不会由被点击的视图(蓝色视图)处理!

注意:如果设置父控件的透明度或者hidden,会直接影响到子控件的透明度和hidden。如果父控件的透明度为0或者hidden = YES,那么子控件也是不可见的!

3.如何寻找最合适的view

应用如何找到最合适的控件来处理事件?
  1. 首先判断主窗口(keyWindow)自己是否能接受触摸事件
  2. 触摸点是否在自己身上
  3. 从后往前遍历子控件,重复前面的两个步骤(首先查找数组中最后一个元素)
  4. 如果没有符合条件的子控件,那么就认为自己最合适处理

详述:
1、主窗口接收到应用程序传递过来的事件后,首先判断自己能否接手触摸事件。如果能,那么在判断触摸点在不在窗口自己身上
2、如果触摸点也在窗口身上,那么窗口会从后往前遍历自己的子控件(遍历自己的子控件只是为了寻找出来最合适的view)
3、遍历到每一个子控件后,又会重复上面的两个步骤(传递事件给子控件,1.判断子控件能否接受事件,2.点在不在子控件上)
4、如此循环遍历子控件,直到找到最合适的view,如果没有更合适的子控件,那么自己就成为最合适的view。
找到最合适的view后,就会调用该view的touches方法处理具体的事件。所以,只有找到最合适的view,把事件传递给最合适的view后,才会调用touches方法进行接下来的事件处理。找不到最合适的view,就不会调用touches方法进行事件处理。
注意:之所以会采取从后往前遍历子控件的方式寻找最合适的view只是为了做一些循环优化。因为相比较之下,后添加的view在上面,降低循环次数。

3.1.寻找最合适的view底层剖析

两个重要的方法:
hitTest:withEvent:方法
pointInside方法

3.1.1.hitTest:withEvent:方法

什么时候调用?

只要事件一传递给一个控件,这个控件就会调用他自己的hitTest:withEvent:方法

作用

寻找并返回最合适的view(能够响应事件的那个最合适的view)

注 意:不管这个控件能不能处理事件,也不管触摸点在不在这个控件上,事件都会先传递给这个控件,随后再调用hitTest:withEvent:方法

拦截事件的处理

  • 正因为hitTest:withEvent:方法可以返回最合适的view,所以可以通过重写hitTest:withEvent:方法,返回指定的view作为最合适的view。
  • 不管点击哪里,最合适的view都是hitTest:withEvent:方法中返回的那个view。
  • 通过重写hitTest:withEvent:,就可以拦截事件的传递过程,想让谁处理事件谁就处理事件。
    事件传递给谁,就会调用谁的hitTest:withEvent:方法。

注 意:如果hitTest:withEvent:方法中返回nil,那么调用该方法的控件本身和其子控件都不是最合适的view,也就是在自己身上没有找到更合适的view。那么最合适的view就是该控件的父控件。
所以事件的传递顺序是这样的:
  产生触摸事件->UIApplication事件队列->[UIWindow hitTest:withEvent:]->返回更合适的view->[子控件 hitTest:withEvent:]->返回最合适的view

  
事件传递给窗口或控件的后,就调用hitTest:withEvent:方法寻找更合适的view。所以是,先传递事件,再根据事件在自己身上找更合适的view。
不管子控件是不是最合适的view,系统默认都要先把事件传递给子控件,经过子控件调用自己的hitTest:withEvent:方法验证后才知道有没有更合适的view。即便父控件是最合适的view了,子控件的hitTest:withEvent:方法还是会调用,不然怎么知道有没有更合适的!即,如果确定最终父控件是最合适的view,那么该父控件的子控件的hitTest:withEvent:方法也是会被调用的。
技巧:想让谁成为最合适的view就重写谁自己的父控件的hitTest:withEvent:方法返回指定的子控件,或者重写自己的hitTest:withEvent:方法 return self。但是,建议在父控件的hitTest:withEvent:中返回子控件作为最合适的view!

原因在于在自己的hitTest:withEvent:方法中返回自己有时候会出现问题。因为会存在这么一种情况:当遍历子控件时,如果触摸点不在子控件A自己身上而是在子控件B身上,还要要求返回子控件A作为最合适的view,采用返回自己的方法可能会导致还没有来得及遍历A自己,就有可能已经遍历了点真正所在的view,也就是B。这就导致了返回的不是自己而是触摸点真正所在的view。所以还是建议在父控件的hitTest:withEvent:中返回子控件作为最合适的view!
例如:whiteView有redView和greenView两个子控件。redView先添加,greenView后添加。如果要求无论点击那里都要让redView作为最合适的view(把事件交给redView来处理)那么只能在whiteView的hitTest:withEvent:方法中return self.subViews[0];这种情况下在redView的hitTest:withEvent:方法中return self;是不好使的!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 这里redView是whiteView的第0个子控件
#import "redView.h"
@implementation redView
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
return self;
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
NSLog(@"red-touch");
}@end
// 或者
#import "whiteView.h"
@implementation whiteView
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
return self.subviews[0];
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
NSLog(@"white-touch");
}
@end

hit:withEvent:方法底层会调用pointInside:withEvent:方法判断点在不在方法调用者的坐标系上。

3.1.2.pointInside:withEvent:方法

pointInside:withEvent:方法判断点在不在当前view上(方法调用者的坐标系上)如果返回YES,代表点在方法调用者的坐标系上;返回NO代表点不在方法调用者的坐标系上,那么方法调用者也就不能处理事件。

4事件的响应

4.1.触摸事件处理的整体过程

1>用户点击屏幕后产生的一个触摸事件,经过一系列的传递过程后,会找到最合适的视图控件来处理这个事件2>找到最合适的视图控件后,就会调用控件的touches方法来作具体的事件处理touchesBegan…touchesMoved…touchedEnded…3>这些touches方法的默认做法是将事件顺着响应者链条向上传递(也就是touch方法默认不处理事件,只传递事件),将事件交给上一个响应者进行处理

4.2.响应者链条示意图

响应者链条:在iOS程序中无论是最后面的UIWindow还是最前面的某个按钮,它们的摆放是有前后关系的,一个控件可以放到另一个控件上面或下面,那么用户点击某个控件时是触发上面的控件还是下面的控件呢,这种先后关系构成一个链条就叫“响应者链”。也可以说,响应者链是由多个响应者对象连接起来的链条。

响应者对象:能处理事件的对象,也就是继承自UIResponder的对象
作用:能很清楚的看见每个响应者之间的联系,并且可以让一个事件多个对象处理。

如何判断上一个响应者

  1. 如果当前这个view是控制器的view,那么控制器就是上一个响应者
  2. 如果当前这个view不是控制器的view,那么父控件就是上一个响应者
    响应者链的事件传递过程:

事件处理的整个流程总结:

  1. 触摸屏幕产生触摸事件后,触摸事件会被添加到由UIApplication管理的事件队列中(即,首先接收到事件的是UIApplication)。
  2. UIApplication会从事件队列中取出最前面的事件,把事件传递给应用程序的主窗口(keyWindow)。
  3. 主窗口会在视图层次结构中找到一个最合适的视图来处理触摸事件。(至此,第一步已完成)
  4. 最合适的view会调用自己的touches方法处理事件
  5. touches默认做法是把事件顺着响应者链条向上抛。

事件的传递与响应:

  1. 当一个事件发生后,事件会从父控件传给子控件,也就是说由UIApplication -> UIWindow -> UIView -> initial view,以上就是事件的传递,也就是寻找最合适的view的过程。

  2. 接下来是事件的响应。首先看initial view能否处理这个事件,如果不能则会将事件传递给其上级视图(inital view的superView);如果上级视图仍然无法处理则会继续往上传递;一直传递到视图控制器view controller,首先判断视图控制器的根视图view是否能处理此事件;如果不能则接着判断该视图控制器能否处理此事件,如果还是不能则继续向上传 递;(对于第二个图视图控制器本身还在另一个视图控制器中,则继续交给父视图控制器的根视图,如果根视图不能处理则交给父视图控制器处理);一直到 window,如果window还是不能处理此事件则继续交给application处理,如果最后application还是不能处理此事件则将其丢弃

  3. 在事件的响应中,如果某个控件实现了touches…方法,则这个事件将由该控件来接受,如果调用了[supertouches….];就会将事件顺着响应者链条往上传递,传递给上一个响应者;接着就会调用上一个响应者的touches….方法

如何做到一个事件多个对象处理:

因为系统默认做法是把事件上抛给父控件,所以可以通过重写自己的touches方法和父控件的touches方法来达到一个事件多个对象处理的目的。

(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
1
2
3
4
5
// 1.自己先处理事件...
NSLog(@"do somthing...");
// 2.再调用系统的默认做法,再把事件交给上一个响应者处理
[super touchesBegan:touches withEvent:event];
}

事件的传递和响应的区别:

事件的传递是从上到下(父控件到子控件),事件的响应是从下到上(顺着响应者链条向上传递:子控件到父控件。

参考链接:http://www.jianshu.com/p/2e074db792ba

iOS React/RCTBundleURLProvider.h' file not found

学习React Native使用react-native init 项目名称之后运行项目,编译就报React/RCTBundleURLProvider.h' file not found怎么办。

下面先看一下实现效果.


屏幕截图

解决方法

原来是新版本有问题
新建项目指定版本:
用–version参数创建指定版本的项目。例如react-native init ceshi –version 0.44.3注意版本号必须精确到两个小数点。
项目创建好之后:执行:react-native run-ios