CoreText

在CDChatList为了达到页面流畅的效果,用到了coretext,关键点记录如下

聊天页面卡顿点主要有,计算聊天气泡高度、富文本渲染时的性能消耗太大,这两个可以异步操作,并缓存

气泡高度的计算一般都是用NSString的boundingRectWithSize: options: attributes: context:方法计算,算出来的结果可以放在消息Model中缓存并可以本地化,这样就只需计算一次。

富文本渲染有两种方法,UILabel中的attributedText可以复值富文本,但是boundingRectWithSize就不太好用了,而且要考虑到带表情的富文本,所以就得用另一个方法

⬇️

CoreText

首先需要注意的是,

Coretext只有C的接口

,所以得手动管理一些c的内存,同时还得了解字体字形相关信息。

UILabel对coretext进行了封装,我们这里直接调用coretext,可以获得不错的性能表现。

coretext可以实现文本尺寸的计算,文本在子线程绘制,绘制完成后再赋值给view,这样耗时操作都可以不在主线程操作,不影响页面流畅。

上代码

首先创建需要显示的富文本,这里可能包含表情,
形如: @”呵呵哒,然后来个表情[微笑][骷髅]”,
这里的[微笑][骷髅]就是需要被替换成表情图片,先用正则换成空字符占位,等后续图片绘制,绘制部分具体代码可见CDChatList的CDLabel实现

1
2
3
4
NSMutableAttributedString *attString = [[NSMutableAttributedString alloc] initWithString:data.msgString attributes:dic];

// 先创建framesetter
CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)attString);

这步计算文本内容范围,猜测boundingRectWithSize也是用的这个方法,入参很像
这里的size就是需要缓存的

1
2
3
4
5
6
7
8
9
CGSize caSize = CTFramesetterSuggestFrameSizeWithConstraints(framesetter, CFRangeMake(0,attString.length), nil, size, nil);

// 这两步获得需要绘制文本的CGPath和CTFrame
CGPathRef path = CGPathCreateWithRect(CGRectMake(0, 0, caSize.width, caSize.height), NULL);
CTFrameRef frame = CTFramesetterCreateFrame(framesetter,CFRangeMake(0, [attString length]), path, NULL);

// 注意需要释放内存
CFRelease(framesetter);
CFRelease(path);

渲染展示内容

1
2
3
4
5
6
7
UIGraphicsBeginImageContextWithOptions(caSize, NO, 0);
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetTextMatrix(context, CGAffineTransformIdentity);
CGContextTranslateCTM(context, 0, caSize.height);
CGContextScaleCTM(context, 1.0, -1.0); // coretext坐标系翻转
CTFrameDraw(frame, context);
//.....将上面正则出来的表情图片也绘制在这个context上

最终我们得到这个需要展示的图片内容

1
2
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();

在展示上面的图片时,将他赋给layer的contents
这一步必须在主线程执行,可以使用NSCache对此缓存,以达到流畅的效果,且不用担心内存问题

1
self.layer.contents = (__bridge id)data.contents.CGImage;

最终效果在GitHub见CDChatList

参考文章

iOS中的富文本技术(2)-CoreText框架

猿题库iOS客户端的技术细节(三):基于CoreText的排版引擎

基于 CoreText 的排版引擎:基础

CoreText

在CDChatList为了达到页面流畅的效果,用到了coretext,关键点记录如下

聊天页面卡顿点主要有,计算聊天气泡高度、富文本渲染时的性能消耗太大,这两个可以异步操作,并缓存

气泡高度的计算一般都是用NSString的boundingRectWithSize: options: attributes: context:方法计算,算出来的结果可以放在消息Model中缓存并可以本地化,这样就只需计算一次。

富文本渲染有两种方法,UILabel中的attributedText可以复值富文本,但是boundingRectWithSize就不太好用了,而且要考虑到带表情的富文本,所以就得用另一个方法

⬇️

CoreText

首先需要注意的是,

Coretext只有C的接口

,所以得手动管理一些c的内存,同时还得了解字体字形相关信息。

UILabel对coretext进行了封装,我们这里直接调用coretext,可以获得不错的性能表现。

coretext可以实现文本尺寸的计算,文本在子线程绘制,绘制完成后再赋值给view,这样耗时操作都可以不在主线程操作,不影响页面流畅。

上代码

首先创建需要显示的富文本,这里可能包含表情,
形如: @”呵呵哒,然后来个表情[微笑][骷髅]”,
这里的[微笑][骷髅]就是需要被替换成表情图片,先用正则换成空字符占位,等后续图片绘制,绘制部分具体代码可见CDChatList的CDLabel实现

1
2
3
4
NSMutableAttributedString *attString = [[NSMutableAttributedString alloc] initWithString:data.msgString attributes:dic];

// 先创建framesetter
CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)attString);

这步计算文本内容范围,猜测boundingRectWithSize也是用的这个方法,入参很像
这里的size就是需要缓存的

1
2
3
4
5
6
7
8
9
CGSize caSize = CTFramesetterSuggestFrameSizeWithConstraints(framesetter, CFRangeMake(0,attString.length), nil, size, nil);

// 这两步获得需要绘制文本的CGPath和CTFrame
CGPathRef path = CGPathCreateWithRect(CGRectMake(0, 0, caSize.width, caSize.height), NULL);
CTFrameRef frame = CTFramesetterCreateFrame(framesetter,CFRangeMake(0, [attString length]), path, NULL);

// 注意需要释放内存
CFRelease(framesetter);
CFRelease(path);

渲染展示内容

1
2
3
4
5
6
7
UIGraphicsBeginImageContextWithOptions(caSize, NO, 0);
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetTextMatrix(context, CGAffineTransformIdentity);
CGContextTranslateCTM(context, 0, caSize.height);
CGContextScaleCTM(context, 1.0, -1.0); // coretext坐标系翻转
CTFrameDraw(frame, context);
//.....将上面正则出来的表情图片也绘制在这个context上

最终我们得到这个需要展示的图片内容

1
2
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();

在展示上面的图片时,将他赋给layer的contents
这一步必须在主线程执行,可以使用NSCache对此缓存,以达到流畅的效果,且不用担心内存问题

1
self.layer.contents = (__bridge id)data.contents.CGImage;

最终效果在GitHub见CDChatList

参考文章

iOS中的富文本技术(2)-CoreText框架

猿题库iOS客户端的技术细节(三):基于CoreText的排版引擎

基于 CoreText 的排版引擎:基础

RunLoop应用

网上关于runloop的解析已经更丰富了,这里只记录runloop的实践部分

RunLoop

干嘛的

为了实现线程不退出,可以随时接受消息执行任务,
node.js 的事件处理,windows程序的消息循环,iOS、OSX的RunLoop都是这种机制

线程和runloop一一对应,关系保存在全局字典中

主线程自带runloop,无需创建,新建的线程需要手动对应runloop,不让执行完成就结束了

常驻线程

常驻线程的意义,我理解是用来处理需要在子线程长期处理的事情,比如说记日志,埋点,IO数据处理等等之类。

这样想的话其实和NSOperationqueue或者GCD实现一个串行队列应该没有什么区别。

代码

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

static NSThread *workThread;


-(void)initThread{

workThread = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil];

[workThread start];
}


// 让子线程runloop跑起来,防止线程结束
-(void)run{

[[NSRunLoop currentRunLoop] addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];

while (!_stopRunning) {
@autoreleasepool {
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
}
}
}


// 这段代码直接复制自weex
// 在子线程执行代码
+ (void)_performBlockOnBridgeThread:(void (^)(void))block
{
if ([NSThread currentThread] == workThread) {
block();
} else {
[self performSelector:@selector(_performBlockOnBridgeThread:)
onThread:[self jsThread]
withObject:[block copy]
waitUntilDone:NO];
}
}


runloop observer

可以通过CFRunLoopAddObserver,给runloop添加观察者,具体实践如下

这是CDChatList中的一段代码,主要是为了实现,label在scrollview滚动时,去除选中文字的样式,如果不用runloop也是可以用通知或者其他的代理形式实现,但是会有一定的耦合

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
currentMode = CFRunLoopCopyCurrentMode(CFRunLoopGetMain());

__weak typeof(self) weakS = self;
// 这里监听了所有的runloop事件,然后在回调中过滤出滚动事件,因为滚动事件会一直回调,所以这里需要特别处理,只观察进入滚动的时机
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
if (weakS) {
__strong typeof(weakS) strongS = weakS;
CFComparisonResult rest = CFStringCompare(strongS->currentMode, CFRunLoopCopyCurrentMode(CFRunLoopGetMain()), kCFCompareBackwards);
if (rest != kCFCompareEqualTo) {
strongS->currentMode = CFRunLoopCopyCurrentMode(CFRunLoopGetMain());
if ((NSString *)CFBridgingRelease(strongS->currentMode) == UITrackingRunLoopMode) {
[strongS scrollDidScroll];
}
}
}
});

CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);

下面是YYTransaction中相关的代码,具有类似的实现,这里监听的目的应该是在runloop将要进入休眠时,把不需要立即执行的任务执行,以达到async的效果。

在AsyncDisplayKit中也有类似的用法

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
static NSMutableSet *transactionSet = nil;

static void YYRunLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) {
if (transactionSet.count == 0) return;
NSSet *currentSet = transactionSet;
transactionSet = [NSMutableSet new];
[currentSet enumerateObjectsUsingBlock:^(YYTransaction *transaction, BOOL *stop) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[transaction.target performSelector:transaction.selector];
#pragma clang diagnostic pop
}];
}

static void YYTransactionSetup() {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
transactionSet = [NSMutableSet new];
CFRunLoopRef runloop = CFRunLoopGetMain();
CFRunLoopObserverRef observer;

observer = CFRunLoopObserverCreate(CFAllocatorGetDefault(),
kCFRunLoopBeforeWaiting | kCFRunLoopExit,
true, // repeat
0xFFFFFF, // after CATransaction(2000000)
YYRunLoopObserverCallBack, NULL);
CFRunLoopAddObserver(runloop, observer, kCFRunLoopCommonModes);
CFRelease(observer);
});
}

GCD之NSoperation

NSOperation是基于GCD的面相对象封装,这里把重点计一下。

NSoperation的dependency

任务依赖是NSoperation的重要功能,可以让GCD的任务同步更直观的实现出来,不过有些地方也需要注意。

1
2
3
4
5
6
7
8
9
10
11
12
13
NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"1-1: %@",[NSThread currentThread]);
}];

NSBlockOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"2-1");
}];

[op1 addDependency:op2];

[op2 start];
[op1 start];

由于op1依赖于op2的完成,如果[op1 start]写在[op2 start]前面的话,

会抛出异常-[__NSOperationInternal _start:]: receiver is not yet ready to execute,

你可以这么写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
NSOperationQueue *qq = [[NSOperationQueue alloc] init];

NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"1-1: %@",[NSThread currentThread]);
}];

NSBlockOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"2-1");

NSLog(@"2-3");
}];

[op1 addDependency:op2];

[qq addOperation:op1];
[qq addOperation:op2];

就不需要关心触发顺序的问题了,因为NSOperationQueue会帮你管理

NSoperation的queuePriority

queuePriority是次于dependency的属性,在dependency没有指明的情况下,NSOperationQueue会依据NSOperation的queuePriority来决定执行先后。

异步任务的同步问题

看以下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
NSOperationQueue *queue = [[NSOperationQueue alloc] init];

NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"start-1");
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"end-1");
});
}];

NSBlockOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"start-2");
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"end-2");
});
}];

[op1 addDependency:op2];

[queue addOperation:op1];
[queue addOperation:op2];

输出

1
2
3
4
2018-04-23 09:27:57.391220+0800 testP[20559:439375] start-2
2018-04-23 09:27:57.391494+0800 testP[20559:439382] start-1
2018-04-23 09:27:59.583992+0800 testP[20559:439102] end-2
2018-04-23 09:27:59.584309+0800 testP[20559:439102] end-1

可以发现,operationqueue对异步任务是不能同步的,想要实现对异步任务的同步,就得重写NSOperation子类,也就是控制他的isFinished属性。

NSOperation是通过KVO isFinished/isExecuting等属性来判断任务的生命周期,重写这几个关键属性就可以了,上代码

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
@interface SubOperation:NSBlockOperation
{
// 这是我们用来代替isFinishe的属性
BOOL sub_isFinish;
}


-(void)stopOperation;
@end

@implementation SubOperation


// 重写isFinished,替换掉他
-(BOOL)isFinished{
return sub_isFinish;
}

/*
* 这里需要手动调用此方法,来告诉operation,需要调用isFinished来结束operation
* 为什么没有直接调,setvalueforkey的方法呢,试试看就知道了
*/

-(void)stopOperation{
[self willChangeValueForKey:@"isFinished"];
sub_isFinish = YES;
[self didChangeValueForKey:@"isFinished"];
}

@end

现在重新测试下,需要加上我们手动的方法,可以发现可以实现异步任务同步了

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
 NSOperationQueue *queue = [[NSOperationQueue alloc] init];
__block SubOperation *op1;
op1 = [SubOperation blockOperationWithBlock:^{
NSLog(@"start-1");
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"end-1");
[op1 stopOperation];
});
}];

__block SubOperation *op2;
op2 = [SubOperation blockOperationWithBlock:^{
NSLog(@"start-2");
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"end-2");
[op2 stopOperation];
});
}];

[op1 addDependency:op2];

[queue addOperation:op1];
[queue addOperation:op2];


2018-04-23 09:30:41.700824+0800 testP[20651:442268] start-2
2018-04-23 09:30:43.895327+0800 testP[20651:442214] end-2
2018-04-23 09:30:43.895927+0800 testP[20651:442271] start-1
2018-04-23 09:30:46.089130+0800 testP[20651:442214] end-1

参考链接

iOS多线程:『NSOperation、NSOperationQueue』详尽总结

其他杂项

给iOS模拟器录屏

1
xcrun simctl io booted recordVideo filename.mov

iTunes 提交没有版本

2018-04-16 11:41:16

提交到iTunes connect 上看不到版本,可能是某些隐私权限你用到了,但是没有在info.plist中说明,比如说蓝牙,这也不会导致测试环境下的崩溃,但生产下就不行了

GCD的线程数目

GCD会维护一个线程池,而线程池中的线程数目是有上限的,具体数字在64位系统上是64,32位系统还没有机会测试

具体的影响,如下代码

1
2
3
4
5
6
7
8
9
10
11
12
- (void)viewDidLoad {
[super viewDidLoad];

dispatch_queue_t quq = dispatch_queue_create("affa", DISPATCH_QUEUE_CONCURRENT);
int taskAmount = 64;
for (int i = 0; i<taskAmount; i++) {
dispatch_async(quq, ^{
sleep(2);
NSLog(@"--%@",[NSThread currentThread]);
});
}
}

直觉上,因为用了dispatch_async,所以应该不会卡主线程,实际上,因为dispatch_async用光了64条线程,其中包含了主线程,所以界面就会卡2秒。

更进一步,如果这里的taskAmount是64的n(n>=1,n为正整数)倍,那么卡顿时间也会变成2*n秒时间。

这种情况下就需要手动的控制并发数量,其实用NSOperationqueue就可以直接的控制,GCD的话没有直接设置的方法,只能用信号量达到类似效果

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
dispatch_queue_t workConcurrentQueue = dispatch_queue_create("workQueue", DISPATCH_QUEUE_CONCURRENT);
dispatch_queue_t serialQueue = dispatch_queue_create("controllQueue",DISPATCH_QUEUE_SERIAL);
dispatch_semaphore_t semaphore = dispatch_semaphore_create(4); // create semaphore: value = 3

dispatch_group_t group = dispatch_group_create();

NSLog(@"begin: %@",[NSThread currentThread]);

for (int i = 0; i < 10; i++) {
__block int index = i;

dispatch_group_async(group, serialQueue, ^{
// If value < 0, then wait here. Else value > 0, then pass, and value -1
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
dispatch_group_async(group, workConcurrentQueue, ^{
sleep(1);
NSLog(@"执行第%d次操作,线程:%@",index, [NSThread currentThread]);
dispatch_semaphore_signal(semaphore); // Perform value +1
});
});
}

dispatch_group_notify(group, workConcurrentQueue, ^{
NSLog(@"okk");
});

泛型实例化

OC中的泛型是所谓的轻量化泛型,暂时还不知道怎么实现泛型实例化,swift中的实现记录在下面

1
2
3
4
5
6
7
func testFunc<T: NSObject>(cls:T) -> T {
let obj = T()
return obj
}

testFunc(cls: NSString())

extern static const 关键字

https://www.jianshu.com/p/2fd58ed2cf55
https://www.jianshu.com/p/3fa703e80720

LayoutIfNeeded小理解

这个方法和另一个方法配对的,setNeedLayout和layoutIfNeed,还有一个关联的方法是layoutSubviews,在我们没有任何干预的情况下,一个view的fram或bounds发生变化时,系统会设置一个flag给这个view,当下一个渲染时机到来时系统会重新按新的布局来渲染视图。setNeedLayout就是我们主动为这个视图设置一个flag,告诉系统这个视图再下一个时机到来时要重新渲染,而layoutIfNeed则是告诉系统,如果设置了flag那么不用等待时机到来了,直接渲染吧。而layoutSubviews这个方法是系统调用的,我们不需要主动调用,我们只需要调用layoutIfNeed就可以了,让系统判断是否在当前时机下立即渲染。

cocoapods

https://www.jianshu.com/p/89605e02bf18

Dispatch Source

在这个文章看到.

详解在这里

Dispatch I/O

http://www.cocoachina.com/industry/20130821/6842.html

iOS中NSString转换成HEX(十六进制)-NSData转换成int

1
2
3
4
5
6
NSString *str = @"0xff055008";
//先以16为参数告诉strtoul字符串参数表示16进制数字,然后使用0x%X转为数字类型
unsigned long red = strtoul([str UTF8String],0,16);
//strtoul如果传入的字符开头是“0x”,那么第三个参数是0,也是会转为十六进制的,这样写也可以:
unsigned long red = strtoul([@"0x6587" UTF8String],0,0);
NSLog(@"转换完的数字为:%lx",red);
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
// 十六进制转换为普通字符串的。 
+ (NSString *)stringFromHexString:(NSString *)hexString { //

char *myBuffer = (char *)malloc((int)[hexString length] / 2 + 1);
bzero(myBuffer, [hexString length] / 2 + 1);
for (int i = 0; i < [hexString length] - 1; i += 2) {
unsigned int anInt;
NSString * hexCharStr = [hexString substringWithRange:NSMakeRange(i, 2)];
NSScanner * scanner = [[[NSScanner alloc] initWithString:hexCharStr] autorelease];
[scanner scanHexInt:&anInt];
myBuffer[i / 2] = (char)anInt;
}
NSString *unicodeString = [NSString stringWithCString:myBuffer encoding:4];
NSLog(@"------字符串=======%@",unicodeString);
return unicodeString;


}

//普通字符串转换为十六进制的。

+ (NSString *)hexStringFromString:(NSString *)string{
NSData *myD = [string dataUsingEncoding:NSUTF8StringEncoding];
Byte *bytes = (Byte *)[myD bytes];
//下面是Byte 转换为16进制。
NSString *hexStr=@"";
for(int i=0;i<[myD length];i++)

{
NSString *newHexStr = [NSString stringWithFormat:@"%x",bytes[i]&0xff];///16进制数

if([newHexStr length]==1)

hexStr = [NSString stringWithFormat:@"%@0%@",hexStr,newHexStr];

else

hexStr = [NSString stringWithFormat:@"%@%@",hexStr,newHexStr];
}
return hexStr;
}
1
2
3
4
5
6
7
//int 转data
int i = 1;
NSData *data = [NSData dataWithBytes: &i length: sizeof(i)];
//data 转int
int i;
[data getBytes: &i length: sizeof(i)];

转自这里

chrome插件

目标:去掉HTML上侧标栏的广告

做个Chrome的小插件

本来想做Safari的插件的,但是资料太少,似乎还需要证书……., 还是做个Chrome的算了吧。

必要的文件

Chrome插件必要的是一个manifest.json

示例

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
{
// 清单文件的版本,这个必须写,而且必须是2
"manifest_version": 2,
// 插件的名称
"name": "去侧边广告的",
// 插件的版本
"version": "1.0.0",
// 插件描述
"description": "简单的Chrome扩展demo",
"content_scripts": [ {
"matches": ["<all_urls>"],
"js": ["jq.js","test.js"],
"run_at": "document_start"
} ],
// 权限申请
"permissions": [
"contextMenus", // 右键菜单
"tabs", // 标签
"notifications", // 通知
"webRequest", // web请求
"webRequestBlocking",
"storage", // 插件本地存储
"http://*/*", // 可以通过executeScript或者insertCSS访问的网站
"https://*/*" // 可以通过executeScript或者insertCSS访问的网站
],
// 向地址栏注册一个关键字以提供搜索建议,只能设置一个关键字
"omnibox": { "keyword" : "go" },
// devtools页面入口,注意只能指向一个HTML文件,不能是JS文件
"devtools_page": "devtools.html"
}

其中的content_scripts是关键,我们就是通过这个test.js把HTML中的,aside隐藏

test.js代码

1
2
3
4
5
6
7
8
document.addEventListener('DOMContentLoaded', function()
{
// 为了防止有js控制iframe的显示之类的问题,这里一直循环,有性能考虑的话就去掉
window.setInterval(function () {
$("aside").remove();
$("iframe").remove();
},1000);
});

此处用了jQuery,所以还需要引用文件

在Chrome中打开开发模式,引入本地已经解压的扩展程序

然后就可以了

GCD之同步任务

平时没有怎么用的概念,但是很重要,这里记一下

怎么让线程同步

Dispatch Group

需要在大量任务都执行完成后,执行其他任务,可以用 Dispatch Group

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
// 可以理解为一个任务组,组内的任务完成后就会调用dispatch_group_notify
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);

dispatch_group_async(group, queue, ^{
for (int i = 0; i < 1000; i++) {
if (i == 999) {
NSLog(@"11111111");
}
}

});

dispatch_group_async(group, queue, ^{
NSLog(@"22222222");
});

dispatch_group_async(group, queue, ^{
NSLog(@"33333333");
});

dispatch_group_notify(group, queue, ^{
NSLog(@"done");
});

2018年4月18日更新

对于网络请求这种异步任务,还需要使用 dispatch_group_enter和dispatch_group_leave,来手动处理下,先上代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);

dispatch_group_enter(group);
dispatch_async(queue, ^{
NSLog(@"任务1开始");
sleep(2);
NSLog(@"任务1结束");
dispatch_group_leave(group);
});

dispatch_group_enter(group);
dispatch_async(queue, ^{
NSLog(@"任务2开始");
sleep(1);
NSLog(@"任务2结束");
dispatch_group_leave(group);
});

dispatch_group_notify(group, queue, ^{
NSLog(@"完成了");
});

输出为

1
2
3
4
5
6
17:31:39.314251+0800 testP[29849:2247791] 任务2开始
17:31:39.314251+0800 testP[29849:2247785] 任务1开始
17:31:40.317949+0800 testP[29849:2247791] 任务2结束
17:31:41.318231+0800 testP[29849:2247785] 任务1结束
17:31:41.318588+0800 testP[29849:2247785] 完成了

可以看到任务1和任务2同时执行,因为任务1耗时2秒,所以在任务2后1秒完成,然后发出通知,

dispatch_group_enter和dispatch_group_leave总是成对出现,不然可能导致group没有被释放,从而没有notify;或者group提前释放,导致EXC_BAD_INSTRUCTION,group提前释放

dispatch_group_enter和dispatch_group_leave可以理解为给group添加手动计数,dispatch_group_enter会给group加1,dispatch_group_leave就是减一,group初始计数为0。

当group初始计数为0时,就会执行notify通知,比如如下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);

dispatch_group_notify(group, queue, ^{
NSLog(@"完成了");
});

dispatch_group_enter(group);
dispatch_async(queue, ^{
NSLog(@"任务1开始");
sleep(2);
NSLog(@"任务1结束");
dispatch_group_leave(group);
});

dispatch_group_enter(group);
dispatch_async(queue, ^{
NSLog(@"任务2开始");
sleep(1);
NSLog(@"任务2结束");
dispatch_group_leave(group);
});


输出为

1
2
3
4
5
17:45:46.296758+0800 testP[30209:2259157] 任务2开始
17:45:46.296758+0800 testP[30209:2259158] 任务1开始
17:45:46.296766+0800 testP[30209:2259150] 完成了
17:45:47.301499+0800 testP[30209:2259157] 任务2结束
17:45:48.299812+0800 testP[30209:2259158] 任务1结束

将dispatch_group_notify移到最前,就不会在group完成后得到notify,而是提前执行了

dispatch_barrier_sync 和 dispatch_barrier_async

网上找到的

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

dispatch_async(queue, ^{
NSLog(@"1");
});

dispatch_async(queue, ^{
NSLog(@"2");
});
dispatch_async(queue, ^{
NSLog(@"3");
});


// dispatch_barrier_sync 这个和 dispatch_async与dispatch_sync之间的区别类似
dispatch_barrier_async(queue, ^{
NSLog(@"000000");
});

dispatch_async(queue, ^{
NSLog(@"4");
});

dispatch_async(queue, ^{
NSLog(@"5");
});
dispatch_async(queue, ^{
NSLog(@"6");
});

dispatch_semaphore

1
2
3
1、dispatch_semaphore_create 创建一个semaphore  就是创建一个全局的变量,小于0时会阻塞当前线程
2、dispatch_semaphore_signal 发送一个信号 给信号量加1
3、dispatch_semaphore_wait 等待信号 给信号量减1

这个东西本质是就是立flag,让flag小于0,线程就阻塞了,只有让flag大于0,才能继续

网上的说明例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 创建队列组
dispatch_group_t group = dispatch_group_create();
// 创建信号量,并且设置值为10
dispatch_semaphore_t semaphore = dispatch_semaphore_create(10);
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
for (int i = 0; i < 100; i++){
/*
* 由于是异步执行的,所以每次循环Block里面的dispatch_semaphore_signal根本还没有执行就会执行dispatch_semaphore_wait,
* 从而semaphore-1.当循环10此后,semaphore等于0,则会阻塞线程,直到执行了Block的dispatch_semaphore_signal 才会继续执行
*/

dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
dispatch_group_async(group, queue, ^{
NSLog(@"%i",i);
sleep(2);
// 每次发送信号则semaphore会+1,
dispatch_semaphore_signal(semaphore);
});
}

应用1 网络请求

1
2
3
4
5
6
7
8
9
10
11
12
13
_block BOOL isok = NO;

dispatch_semaphore_t sema = dispatch_semaphore_create(0);
Engine *engine = [[Engine alloc] init];
[engine queryCompletion:^(BOOL isOpen) {
isok = isOpen;
dispatch_semaphore_signal(sema);
} onError:^(int errorCode, NSString *errorMessage) {
isok = NO;
dispatch_semaphore_signal(sema);
}];
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
// todo what you want to do after net callback

应用2 获取权限

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//创建通讯簿的引用
addBook=ABAddressBookCreateWithOptions(NULL, NULL);
//创建一个出事信号量为0的信号
dispatch_semaphore_t sema=dispatch_semaphore_create(0);
//申请访问权限
ABAddressBookRequestAccessWithCompletion(addBook, ^(bool greanted, CFErrorRef error)
{
//greanted为YES是表示用户允许,否则为不允许
if (!greanted) {
tip=1;
}
//发送一次信号
dispatch_semaphore_signal(sema);

});
//等待信号触发
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);

CDChatList

https://github.com/chdo002/CDChatList

CDChatList

CI Status
Version
License
Platform

图片名称 图片名称

高性能的聊天页面解决方案
对聊天列表的高度封装,可灵活配置页面样式

聊天界面其实大同小异,所以这里封装了一个聊天的组件,使用CoreText和手动代码布局,尽量实现简单,通用,高效,易于维护。

项目结构

CDChatListView: UITableView 视图,聊天页面主体

CDBaseMsgCell: 实现消息气泡基本视图

CDTextTableViewCell、CDImageTableViewCell、CDAudioTableViewCell: 继承自CDBaseMsgCell,实现响应功能。
CDSystemTableViewCell: 特殊消息气泡,实现系统通知

CellCaculator: tableview布局计算,并提前渲染cell

ChatConfiguration: chatlist配置类组,UI定制,及资源等

子组件

CDLabel: 富文本标签
CDChatInputBox: 输入框封装组件

安装

支持至iOS 11

1
pod 'CDChatList'

使用

配置 CDChatList

ChatHelpr负责ChatHelpr的UI配置,及组件的资源文件设置

UI配置及资源文件都有默认,所以无需自定义的话,就可以跳过组件的配置

添加 CDChatList 视图

1
2
3
4
CDChatListView *list = [[CDChatListView alloc] initWithFrame:self.view.bounds];
list.msgDelegate = self;
self.listView = list;
[self.view addSubview:self.listView];

CDChatList会将视图控制器automaticallyAdjustsScrollViewInsets及contentInsetAdjustmentBehavior设为NO及Never,并适应导航栏高度

消息模型 MessageModalProtocal

可以使用自己的消息模型,消息模型需遵守MessageModalProtocal,实现相关属性

组件事件 ChatListProtocol

从组件发出的消息

消息列表请求加载更多消息

1
2
-(void)chatlistLoadMoreMsg: (CDChatMessage)topMessage
callback: (void(^)(CDChatMessageArray))finnished;

消息中的点击事件

1
-(void)chatlistClickMsgEvent: (ChatListInfo *)listInfo;

向组件发消息

添加新的数据到底部

1
-(void)addMessagesToBottom: (CDChatMessageArray)newBottomMsgArr;

更新数据源中的某条消息模型(主要是为了更新UI上的消息状态)

1
-(void)updateMessage:(CDChatMessage)message;

使用场景

收/发消息

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
// 发
{
MessageModal *modal;
}
-(void)send{
modal = [[MessageModal alloc] init];
modal.msgState = CDMessageStateSending;
modal.createTime = ...;
modal.msg = ...;
modal.msgType = ...;
[chatList addMessagesToBottom: modal];
}

-(void)sendCallBack:(BOOL)isSuccess{
modal.msgState = isSuccess; // 此处应处理成枚举
[chatList updateMessage: modal];
}



// 收
-(void)receivedNewMessage:(MessageModal *)modal{
[chatList addMessagesToBottom: modal];
}

下拉加载更多消息

消息列表被下拉时,触发此回调

1
2
3
4
5
6
7
-(void)chatlistLoadMoreMsg: (CDChatMessage)topMessage
callback: (void(^)(CDChatMessageArray))finnished
{
// 根据topMessage 获取更多消息
NSArray *msgArr = [self getMoreMessageFrom: topMessage amount: 10];
callback(msgArr);
}

消息点击事件

目前消息体重处理了 文本点击 及 图片点击 事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
-(void)chatlistClickMsgEvent: (ChatListInfo *)listInfo{
if (listInfo.eventType == ChatClickEventTypeTEXT){
// 点击的文本
listInfo.clickedText
// 点击的文字位置 防止有相同的可点击文字
listInfo.range
// 被点击文本的隐藏信息 e.g. <a title="转人工" href="doTransfer">
listInfo.clickedTextContent
} else if (listInfo.eventType == ChatClickEventTypeIMAGE){
// 图片
listInfo.image
// 图片在tableview中的位置
listInfo.msgImageRectInTableView
}
}

TODO

  • 自定义消息内容匹配

利用Runtime替换方法

小组最近想要去掉taikingdata,自己做个用户行为跟踪的组件,要点是,不能影响应用的性能,做到无感知记录,不需要修改原来工程的代码,任务排到我这,小小调研下,决定利用runtime相关方法,现记录如下

记录button点击事件

Method Swizzling

核心代码
1
2
3
4
5
6
7
8
9
10
11
12
13
void swizzling(Class orignClass, SEL orignSelector, Class switchClass, SEL switchSelector){
// 原来的方法
Method orginMethod = class_getInstanceMethod(orignClass, orignSelector);
// 原来的实现
IMP originImp = method_getImplementation(orginMethod);
// 新的方法
Method switchMethod = class_getInstanceMethod(switchClass, switchSelector);
// 给原来的方法换成新的实现
method_exchangeImplementations(orginMethod, switchMethod);
// 将旧的实现新的方法添加到类中
class_addMethod(orignClass, switchSelector, originImp, method_getTypeEncoding(orginMethod));
}

此方法的目的,将目标类的某个需要监听的方法的实现,换成自己的实现,在自己的实现中,又会调回原来的方法的实现,这样包装一下就可以监听到了。
需要注意的一个细节是,原方法的参数列表是未知的,需要特殊处理,这边是监听button的点击事件,所以,就简单的判断了一下,可以覆盖到全部的情况。

具体实现
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
// APP启动时,加载所有的类,会调用load 方法
+(void)load{
/*
先替换掉,button的addtarget方法,这样可以拿到点击的响应方法,然后替换他
*/
// 原来的方法
SEL originSel = @selector(addTarget:action:forControlEvents:);
// 自己的方法
SEL switchSel = @selector(newAddTarget:action:forContrState:);
swizzling(UIButton.class, originSel, TRACK.class, switchSel);
}


-(void)newAddTarget:(id)targ action:(SEL)ac forContrState:(UIControlEvents)eve{
// 此时的self已经指向了UIButton,所以要调用newAddTarget 让他执行旧的实现
[self newAddTarget:targ action:ac forContrState:eve];

// 获得响应方法的参数,正确的实现方式是动态的实现hook方法,并制定参数,但是这里就简单实现了
unsigned int paraCount = method_getNumberOfArguments(class_getInstanceMethod([targ class], ac));

if (paraCount == 2) { // 至少有两个参数 self:调方法的对象 _cmd:SEL 方法签名
swizzling([targ class], ac, TRACK.class, @selector(tapAcion));
} else { // 有多余的参数的话,则应该是多了个UIbutton
swizzling([targ class], ac, TRACK.class, @selector(tapActionWithPara:));
}
}

// 对应的替换方法
-(void)tapAcion{
[self performSelector:@selector(tapAcion)];
NSLog(@"监听到了");
}

-(void)tapActionWithPara:(UIButton *)but{
[self performSelector:@selector(tapActionWithPara:) withObject:but];
NSLog(@"监听到了2");
}