注册
环信即时通讯云

环信即时通讯云

单聊、群聊、聊天室...
环信开发文档

环信开发文档

Demo体验

Demo体验

场景Demo,开箱即用
RTE开发者社区

RTE开发者社区

汇聚音视频领域技术干货,分享行业资讯
技术讨论区

技术讨论区

技术交流、答疑
资源下载

资源下载

收集了海量宝藏开发资源
iOS Library

iOS Library

不需要辛辛苦苦的去找轮子, 这里都有
Android Library

Android Library

不需要辛辛苦苦的去找轮子, 这里都有

iOS -性能优化 _RunLoop原理去监控

iOS
ios 利用RunLoop的原理去监控卡顿一、卡顿问题的几种原因复杂 UI 、图文混排的绘制量过大;在主线程上做网络同步请求;在主线程做大量的 IO 操作;运算量过大,CPU 持续高占用;死锁和主子线程抢锁。二、监测卡顿的思路监测FPS:FPS 是一秒显示的帧...
继续阅读 »

ios 利用RunLoop的原理去监控卡顿

一、卡顿问题的几种原因

  • 复杂 UI 、图文混排的绘制量过大;
  • 在主线程上做网络同步请求;
  • 在主线程做大量的 IO 操作;
  • 运算量过大,CPU 持续高占用;
  • 死锁和主子线程抢锁。

二、监测卡顿的思路

  1. 监测FPS:FPS 是一秒显示的帧数,也就是一秒内画面变化数量。如果按照动画片来说,动画片的 FPS 就是 24,是达不到 60 满帧的。也就是说,对于动画片来说,24 帧时虽然没有 60 帧时流畅,但也已经是连贯的了,所以并不能说 24 帧时就算是卡住了。 由此可见,简单地通过监视 FPS 是很难确定是否会出现卡顿问题了,所以我就果断弃了通过监视 FPS 来监控卡顿的方案。
  2. RunLoop:通过监控 RunLoop 的状态来判断是否会出现卡顿。RunLoop原理这里就不再多说,主要说方法,首先明确loop的状态有六个
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {

kCFRunLoopEntry ,
// 进入 loop

kCFRunLoopBeforeTimers ,
// 触发 Timer 回调

kCFRunLoopBeforeSources ,
// 触发 Source0 回调

kCFRunLoopBeforeWaiting ,
// 等待 mach_port 消息

kCFRunLoopAfterWaiting ),
// 接收 mach_port 消息

kCFRunLoopExit ,
// 退出 loop

kCFRunLoopAllActivities
// loop 所有状态改变

}

我们需要监测的状态有两个:RunLoop 在进入睡眠之前和唤醒后的两个 loop 状态定义的值,分别是 kCFRunLoopBeforeSources 和 kCFRunLoopAfterWaiting ,也就是要触发 Source0 回调和接收 mach_port 消息两个状态。

 

三、如何检查卡顿

先粗略说下步骤:

  • 创建一个 CFRunLoopObserverContext 观察者;
  • 将创建好的观察者 runLoopObserver 添加到主线程 RunLoop 的 common 模式下观察;
  • 创建一个持续的子线程专门用来监控主线程的 RunLoop 状态;
  • 一旦发现进入睡眠前的 kCFRunLoopBeforeSources 状态,或者唤醒后的状态 kCFRunLoopAfterWaiting,在设置的时间阈值内一直没有变化,即可判定为卡顿;
  • dump 出堆栈的信息,从而进一步分析出具体是哪个方法的执行时间过长;

上代码:


//
// SMLagMonitor.h
//
// Created by DaiMing on 16/3/28.
//

#import <Foundation/Foundation.h>

@interface SMLagMonitor : NSObject

+ (instancetype)shareInstance;

- (void)beginMonitor; //开始监视卡顿
- (void)endMonitor; //停止监视卡顿

@end


//
// SMLagMonitor.m
//
// Created by DaiMing on 16/3/28.
//

#import "SMLagMonitor.h"
#import "SMCallStack.h"
#import "SMCPUMonitor.h"

@interface SMLagMonitor() {
int timeoutCount;
CFRunLoopObserverRef runLoopObserver;
@public
dispatch_semaphore_t dispatchSemaphore;
CFRunLoopActivity runLoopActivity;
}
@property (nonatomic, strong) NSTimer
*cpuMonitorTimer;
@end

@implementation SMLagMonitor

#pragma mark - Interface
+ (instancetype)shareInstance {
static id instance = nil;
static dispatch_once_t dispatchOnce;
dispatch_once(
&dispatchOnce, ^{
instance
= [[self alloc] init];
});
return instance;
}

- (void)beginMonitor {
//监测 CPU 消耗
self.cpuMonitorTimer = [NSTimer scheduledTimerWithTimeInterval:3
target:self
selector:@selector(updateCPUInfo)
userInfo:nil
repeats:YES];
//监测卡顿
if (runLoopObserver) {
return;
}
dispatchSemaphore
= dispatch_semaphore_create(0); //Dispatch Semaphore保证同步
//创建一个观察者
CFRunLoopObserverContext context = {0,(__bridge void*)self,NULL,NULL};
runLoopObserver
= CFRunLoopObserverCreate(kCFAllocatorDefault,
kCFRunLoopAllActivities,
YES,
0,
&runLoopObserverCallBack,
&context);
//将观察者添加到主线程runloop的common模式下的观察中
CFRunLoopAddObserver(CFRunLoopGetMain(), runLoopObserver, kCFRunLoopCommonModes);

//创建子线程监控
dispatch_async(dispatch_get_global_queue(0, 0), ^{
//子线程开启一个持续的loop用来进行监控
while (YES) {
long semaphoreWait = dispatch_semaphore_wait(dispatchSemaphore, dispatch_time(DISPATCH_TIME_NOW, 20*NSEC_PER_MSEC));
if (semaphoreWait != 0) {
if (!runLoopObserver) {
timeoutCount
= 0;
dispatchSemaphore
= 0;
runLoopActivity
= 0;
return;
}
//两个runloop的状态,BeforeSources和AfterWaiting这两个状态区间时间能够检测到是否卡顿
if (runLoopActivity == kCFRunLoopBeforeSources || runLoopActivity == kCFRunLoopAfterWaiting) {
// 将堆栈信息上报服务器的代码放到这里
//出现三次出结果
// if (++timeoutCount < 3) {
// continue;
// }
NSLog(@"monitor trigger");
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH,
0), ^{
// [SMCallStack callStackWithType:SMCallStackTypeAll];
});
}
//end activity
}// end semaphore wait
timeoutCount = 0;
}
// end while
});

}

- (void)endMonitor {
[self.cpuMonitorTimer invalidate];
if (!runLoopObserver) {
return;
}
CFRunLoopRemoveObserver(CFRunLoopGetMain(), runLoopObserver, kCFRunLoopCommonModes);
CFRelease(runLoopObserver);
runLoopObserver
= NULL;
}

#pragma mark - Private

static void runLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info){
SMLagMonitor
*lagMonitor = (__bridge SMLagMonitor*)info;
lagMonitor
->runLoopActivity = activity;

dispatch_semaphore_t semaphore
= lagMonitor->dispatchSemaphore;
dispatch_semaphore_signal(semaphore);
}


- (void)updateCPUInfo {
thread_act_array_t threads;
mach_msg_type_number_t threadCount
= 0;
const task_t thisTask = mach_task_self();
kern_return_t kr
= task_threads(thisTask, &threads, &threadCount);
if (kr != KERN_SUCCESS) {
return;
}
for (int i = 0; i < threadCount; i++) {
thread_info_data_t threadInfo;
thread_basic_info_t threadBaseInfo;
mach_msg_type_number_t threadInfoCount
= THREAD_INFO_MAX;
if (thread_info((thread_act_t)threads[i], THREAD_BASIC_INFO, (thread_info_t)threadInfo, &threadInfoCount) == KERN_SUCCESS) {
threadBaseInfo
= (thread_basic_info_t)threadInfo;
if (!(threadBaseInfo->flags & TH_FLAGS_IDLE)) {
integer_t cpuUsage
= threadBaseInfo->cpu_usage / 10;
if (cpuUsage > 70) {
//cup 消耗大于 70 时打印和记录堆栈
NSString *reStr = smStackOfThread(threads[i]);
//记录数据库中
// [[[SMLagDB shareInstance] increaseWithStackString:reStr] subscribeNext:^(id x) {}];
NSLog(@"CPU useage overload thread stack:\n%@",reStr);
}
}
}
}
}

@end

使用,直接在APP didFinishLaunchingWithOptions 方法里面这样写:

[[SMLagMonitor shareInstance] beginMonitor];

 

搞定!

链接:https://www.cnblogs.com/qiyiyifan/p/11089735.html

收起阅读 »

iOS 实例化讲解RunLoop

iOS
实例化讲解RunLoop之前看过很多有关RunLoop的文章,其中要么是主要介绍RunLoop的基本概念,要么是主要讲解RunLoop的底层原理,很少用真正的实例来讲解RunLoop的,这其中有大部分原因是由于大家在项目中很少能用到RunLoop吧。基于这种原...
继续阅读 »

实例化讲解RunLoop

之前看过很多有关RunLoop的文章,其中要么是主要介绍RunLoop的基本概念,要么是主要讲解RunLoop的底层原理,很少用真正的实例来讲解RunLoop的,这其中有大部分原因是由于大家在项目中很少能用到RunLoop吧。基于这种原因,本文中将用很少的篇幅来对基础内容做以介绍,然后主要利用实例来加深大家对RunLoop的理解,本文中的代码已经上传GitHub,大家可以下载查看,有问题欢迎Issue我。本文主要分为如下几个部分:

  • RunLoop的基础知识
  • 初识RunLoop,如何让RunLoop进驻线程
  • 深入理解Perform Selector
  • 一直"活着"的后台线程
  • 深入理解NSTimer
  • 让两个后台线程有依赖性的一种方式
  • NSURLConnetction的内部实现
  • AFNetWorking中是如何使用RunLoop的?
  • 其它:利用GCD实现定时器功能
  • 延伸阅读

一、RunLoop的基本概念:

什么是RunLoop?提到RunLoop,我们一般都会提到线程,这是为什么呢?先来看下官方对RunLoop的定义:RunLoop系统中和线程相关的基础架构的组成部分(和线程相关),一个RunLoop是一个事件处理环,系统利用这个事件处理环来安排事务,协调输入的各种事件。RunLoop的目的是让你的线程在有工作的时候忙碌,没有工作的时候休眠(和线程相关)。可能这样说你还不是特别清楚RunLoop究竟是用来做什么的,打个比方来说明:我们把线程比作一辆跑车,把这辆跑车的主人比作RunLoop,那么在没有'主人'的时候,这个跑车的生命是直线型的,其启动,运行完之后就会废弃(没有人对其进行控制,'撞坏'被收回),当有了RunLoop这个主人之后,‘线程’这辆跑车的生命就有了保障,这个时候,跑车的生命是环形的,并且在主人有比赛任务的时候就会被RunLoop这个主人所唤醒,在没有任务的时候可以休眠(在IOS中,开启线程是很消耗性能的,开启主线程要消耗1M内存,开启一个后台线程需要消耗512k内存,我们应当在线程没有任务的时候休眠,来释放所占用的资源,以便CPU进行更加高效的工作),这样可以增加跑车的效率,也就是说RunLoop是为线程所服务的。这个例子有点不是很贴切,线程和RunLoop之间是以键值对的形式一一对应的,其中key是thread,value是runLoop(这点可以从苹果公开的源码中看出来)其实RunLoop是管理线程的一种机制,这种机制不仅在IOS上有,在Node.js中的EventLoop,Android中的Looper,都有类似的模式。刚才所说的比赛任务就是唤醒跑车这个线程的一个source;RunLoop Mode就是,一系列输入的source,timer以及observerRunLoop Mode包含以下几种: NSDefaultRunLoopMode,NSEventTrackingRunLoopMode,UIInitializationRunLoopMode,NSRunLoopCommonModes,NSConnectionReplyMode,NSModalPanelRunLoopMode,至于这些mode各自的含义,读者可自己查询,网上不乏这类资源;

二、初识RunLoop,如何让RunLoop进驻线程

我们在主线程中添加如下代码:

  1. while (1) {
  2. NSLog(@"while begin");
  3. // the thread be blocked here
  4. NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
  5. [runLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
  6. // this will not be executed
  7. NSLog(@"while end");
  8. }

这个时候我们可以看到主线程在执行完[runLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];之后被阻塞而没有执行下面的NSLog(@"while end");同时,我们利用GCD,将这段代码放到一个后台线程中:

  1. dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
  2. while (1) {
  3. NSLog(@"while begin");
  4. NSRunLoop *subRunLoop = [NSRunLoop currentRunLoop];
  5. [subRunLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
  6. NSLog(@"while end");
  7. }
  8. });

这个时候我们发现这个while循环会一直在执行;这是为什么呢?我们先将这两个RunLoop分别打印出来:

主线程的RunLoop


由于这个日志比较长,我就只截取了上面的一部分。
我们再看我们新建的子线程中的RunLoop,打印出来之后:

backGroundThreadRunLoop.png


从中可以看出来:我们新建的线程中:

 

  1. sources0 = (null),
  2. sources1 = (null),
  3. observers = (null),
  4. timers = (null),

我们看到虽然有Mode,但是我们没有给它soures,observer,timer,其实Mode中的这些source,observer,timer,统称为这个Modeitem,如果一个Mode中一个item都没有,则这个RunLoop会直接退出,不进入循环(其实线程之所以可以一直存在就是由于RunLoop将其带入了这个循环中)。下面我们为这个RunLoop添加个source:

  1. dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
  2. while (1) {
  3. NSPort *macPort = [NSPort port];
  4. NSLog(@"while begin");
  5. NSRunLoop *subRunLoop = [NSRunLoop currentRunLoop];
  6. [subRunLoop addPort:macPort forMode:NSDefaultRunLoopMode];
  7. [subRunLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
  8. NSLog(@"while end");
  9. NSLog(@"%@",subRunLoop);
  10. }
  11. });

这样我们可以看到能够实现了和主线程中相同的效果,线程在这个地方暂停了,为什么呢?我们明天让RunLoop在distantFuture之前都一直run的啊?相信大家已经猜出出来了。这个时候线程被RunLoop带到‘坑’里去了,这个‘坑’就是一个循环,在循环中这个线程可以在没有任务的时候休眠,在有任务的时候被唤醒;当然我们只用一个while(1)也可以让这个线程一直存在,但是这个线程会一直在唤醒状态,及时它没有任务也一直处于运转状态,这对于CPU来说是非常不高效的。
小结:我们的RunLoop要想工作,必须要让它存在一个Item(source,observer或者timer),主线程之所以能够一直存在,并且随时准备被唤醒就是应为系统为其添加了很多Item

三、深入理解Perform Selector

我们先在主线程中使用下performselector:

  1. - (void)tryPerformSelectorOnMianThread{
  2. [self performSelector:@selector(mainThreadMethod) withObject:nil]; }
  3. - (void)mainThreadMethod{
  4. NSLog(@"execute %s",__func__);
  5. // print: execute -[ViewController mainThreadMethod]
  6. }

这样我们在ViewDidLoad中调用tryPerformSelectorOnMianThread,就会立即执行,并且输出:print: execute -[ViewController mainThreadMethod];
和上面的例子一样,我们使用GCD,让这个方法在后台线程中执行

  1. - (void)tryPerformSelectorOnBackGroundThread{
  2. dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
  3. [self performSelector:@selector(backGroundThread) onThread:[NSThread currentThread] withObject:nil waitUntilDone:NO];
  4. });
  5. }
  6. - (void)backGroundThread{
  7. NSLog(@"%u",[NSThread isMainThread]);
  8. NSLog(@"execute %s",__FUNCTION__);
  9. }

同样的,我们调用tryPerformSelectorOnBackGroundThread这个方法,我们会发现,下面的backGroundThread不会被调用,这是什么原因呢?
这是因为,在调用performSelector:onThread: withObject: waitUntilDone的时候,系统会给我们创建一个Timer的source,加到对应的RunLoop上去,然而这个时候我们没有RunLoop,如果我们加上RunLoop:

  1. - (void)tryPerformSelectorOnBackGroundThread{
  2. dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
  3. [self performSelector:@selector(backGroundThread) onThread:[NSThread currentThread] withObject:nil waitUntilDone:NO];
  4. NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
  5. [runLoop run];
  6. });
  7. }

这时就会发现我们的方法正常被调用了。那么为什么主线程中的perfom selector却能够正常调用呢?通过上面的例子相信你已经猜到了,主线程的RunLoop是一直存在的,所以我们在主线程中执行的时候,无需再添加RunLoop。从Apple的文档中我们也可以得到验证:

Each request to perform a selector is queued on the target thread’s run loop and the requests are then processed sequentially in the order in which they were received. 每个执行perform selector的请求都以队列的形式被放到目标线程的run loop中。然后目标线程会根据进入run loop的顺序来一一执行。

小结:当perform selector在后台线程中执行的时候,这个线程必须有一个开启的runLoop

四、一直"活着"的后台线程

现在有这样一个需求,每点击一下屏幕,让子线程做一个任务,然后大家一般会想到这样的方式:

  1. @interface ViewController ()
  2. @property(nonatomic,strong) NSThread *myThread;
  3. @end
  4. @implementation ViewController
  5. - (void)alwaysLiveBackGoundThread{
  6. NSThread *thread = [[NSThread alloc]initWithTarget:self selector:@selector(myThreadRun) object:@"etund"];
  7. self.myThread = thread;
  8. [self.myThread start];
  9. }
  10. - (void)myThreadRun{
  11. NSLog(@"my thread run");
  12. }
  13. - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
  14. NSLog(@"%@",self.myThread);
  15. [self performSelector:@selector(doBackGroundThreadWork) onThread:self.myThread withObject:nil waitUntilDone:NO];
  16. }
  17. - (void)doBackGroundThreadWork{
  18. NSLog(@"do some work %s",__FUNCTION__);
  19. }
  20. @end

这个方法中,我们利用一个强引用来获取了后台线程中的thread,然后在点击屏幕的时候,在这个线程上执行doBackGroundThreadWork这个方法,此时我们可以看到,在touchesBegin方法中,self.myThread是存在的,但是这是为是什么呢?这就要从线程的五大状态来说明了:新建状态、就绪状态、运行状态、阻塞状态、死亡状态,这个时候尽管内存中还有线程,但是这个线程在执行完任务之后已经死亡了,经过上面的论述,我们应该怎样处理呢?我们可以给这个线程的RunLoop添加一个source,那么这个线程就会检测这个source等待执行,而不至于死亡(有工作的强烈愿望而不死亡):

  1. - (void)myThreadRun{
  2. [[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
  3. [[NSRunLoop currentRunLoop] run]
  4. NSLog(@"my thread run");
  5. }

这个时候再次点击屏幕,我们就会发现,后台线程中执行的任务可以正常进行了。
小结:正常情况下,后台线程执行完任务之后就处于死亡状态,我们要避免这种情况的发生可以利用RunLoop,并且给它一个Source这样来保证线程依旧还在

五、深入理解NSTimer

我们平时使用NSTimer,一般是在主线程中的,代码大多如下:

  1. - (void)tryTimerOnMainThread{
  2. NSTimer *myTimer = [NSTimer scheduledTimerWithTimeInterval:0.5 target:self
  3. selector:@selector(timerAction) userInfo:nil repeats:YES];
  4. [myTimer fire];
  5. }
  6. - (void)timerAction{
  7. NSLog(@"timer action");
  8. }

这个时候代码按照我们预定的结果运行,如果我们把这个Tiemr放到后台线程中呢?

  1. dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
  2. NSTimer *myTimer = [NSTimer scheduledTimerWithTimeInterval:0.5 target:self selector:@selector(timerAction) userInfo:nil repeats:YES];
  3. [myTimer fire];
  4. });

这个时候我们会发现,这个timer只执行了一次,就停止了。这是为什么呢?通过上面的讲解,想必你已经知道了,NSTimer,只有注册到RunLoop之后才会生效,这个注册是由系统自动给我们完成的,既然需要注册到RunLoop,那么我们就需要有一个RunLoop,我们在后台线程中加入如下的代码:

  1. NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
  2. [runLoop run];

这样我们就会发现程序正常运行了。在Timer注册到RunLoop之后,RunLoop会为其重复的时间点注册好事件,比如1:10,1:20,1:30这几个时间点。有时候我们会在这个线程中执行一个耗时操作,这个时候RunLoop为了节省资源,并不会在非常准确的时间点回调这个Timer,这就造成了误差(Timer有个冗余度属性叫做tolerance,它标明了当前点到后,容许有多少最大误差),可以在执行一段循环之后调用一个耗时操作,很容易看到timer会有很大的误差,这说明在线程很闲的时候使用NSTiemr是比较傲你准确的,当线程很忙碌时候会有较大的误差。系统还有一个CADisplayLink,也可以实现定时效果,它是一个和屏幕的刷新率一样的定时器。如果在两次屏幕刷新之间执行一个耗时的任务,那其中就会有一个帧被跳过去,造成界面卡顿。另外GCD也可以实现定时器的效果,由于其和RunLoop没有关联,所以有时候使用它会更加的准确,这在最后会给予说明。

六、让两个后台线程有依赖性的一种方式

给两个后台线程添加依赖可能有很多的方式,这里说明一种利用RunLoop实现的方式。原理很简单,我们先让一个线程工作,当工作完成之后唤醒另外的一线程,通过上面对RunLoop的说明,相信大家很容易能够理解这些代码:

  1. - (void)runLoopAddDependance{
  2. self.runLoopThreadDidFinishFlag = NO;
  3. NSLog(@"Start a New Run Loop Thread");
  4. NSThread *runLoopThread = [[NSThread alloc] initWithTarget:self selector:@selector(handleRunLoopThreadTask) object:nil];
  5. [runLoopThread start];
  6. NSLog(@"Exit handleRunLoopThreadButtonTouchUpInside");
  7. dispatch_async(dispatch_get_global_queue(0, 0), ^{
  8. while (!_runLoopThreadDidFinishFlag) {
  9. self.myThread = [NSThread currentThread];
  10. NSLog(@"Begin RunLoop");
  11. NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
  12. NSPort *myPort = [NSPort port];
  13. [runLoop addPort:myPort forMode:NSDefaultRunLoopMode];
  14. [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
  15. NSLog(@"End RunLoop");
  16. [self.myThread cancel];
  17. self.myThread = nil;
  18. }
  19. });
  20. }
  21. - (void)handleRunLoopThreadTask
  22. {
  23. NSLog(@"Enter Run Loop Thread");
  24. for (NSInteger i = 0; i < 5; i ++) {
  25. NSLog(@"In Run Loop Thread, count = %ld", i);
  26. sleep(1);
  27. }
  28. #if 0
  29. // 错误示范
  30. _runLoopThreadDidFinishFlag = YES;
  31. // 这个时候并不能执行线程完成之后的任务,因为Run Loop所在的线程并不知道runLoopThreadDidFinishFlag被重新赋值。Run Loop这个时候没有被任务事件源唤醒。
  32. // 正确的做法是使用 "selector"方法唤醒Run Loop。 即如下:
  33. #endif
  34. NSLog(@"Exit Normal Thread");
  35. [self performSelector:@selector(tryOnMyThread) onThread:self.myThread withObject:nil waitUntilDone:NO];
  36. // NSLog(@"Exit Run Loop Thread");
  37. }

七、NSURLConnection的执行过程

在使用NSURLConnection时,我们会传入一个Delegate,当我们调用了[connection start]之后,这个Delegate会不停的收到事件的回调。实际上,start这个函数的内部会获取CurrentRunloop,然后在其中的DefaultMode中添加4个source。如下图所示,CFMultiplexerSource是负责各种Delegate回调的,CFHTTPCookieStorage是处理各种Cookie的。如下图所示:

NSURLConnection的执行过程


从中可以看出,当开始网络传输是,我们可以看到NSURLConnection创建了两个新的线程:com.apple.NSURLConnectionLoader和com.apple.CFSocket.private。其中CFSocket是处理底层socket链接的。NSURLConnectionLoader这个线程内部会使用RunLoop来接收底层socket的事件,并通过之前添加的source,来通知(唤醒)上层的Delegate。这样我们就可以理解我们平时封装网络请求时候常见的下面逻辑了:

 

  1. while (!_isEndRequest)
  2. {
  3. NSLog(@"entered run loop");
  4. [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
  5. }
  6. NSLog(@"main finished,task be removed");
  7. - (void)connectionDidFinishLoading:(NSURLConnection *)connection
  8. {
  9. _isEndRequest = YES;
  10. }

这里我们就可以解决下面这些疑问了:

  1. 为什么这个While循环不停的执行,还需要使用一个RunLoop? 程序执行一个while循环是不会耗费很大性能的,我们这里的目的是想让子线程在有任务的时候处理任务,没有任务的时候休眠,来节约CPU的开支。
  2. 如果没有为RunLoop添加item,那么它就会立即退出,这里的item呢? 其实系统已经给我们默认添加了4个source了。
  3. 既然[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];让线程在这里停下来,那么为什么这个循环会持续的执行呢?因为这个一直在处理任务,并且接受系统对这个Delegate的回调,也就是这个回调唤醒了这个线程,让它在这里循环。

八、AFNetWorking中是如何使用RunLoop的?

在AFN中AFURLConnectionOperation是基于NSURLConnection构建的,其希望能够在后台线程来接收Delegate的回调。
为此AFN创建了一个线程,然后在里面开启了一个RunLoop,然后添加item

  1. + (void)networkRequestThreadEntryPoint:(id)__unused object {
  2. @autoreleasepool {
  3. [[NSThread currentThread] setName:@"AFNetworking"];
  4. NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
  5. [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
  6. [runLoop run];
  7. }
  8. }
  9. + (NSThread *)networkRequestThread {
  10. static NSThread *_networkRequestThread = nil;
  11. static dispatch_once_t oncePredicate;
  12. dispatch_once(&oncePredicate, ^{
  13. _networkRequestThread = [[NSThread alloc] initWithTarget:self selector:@selector(networkRequestThreadEntryPoint:) object:nil];
  14. [_networkRequestThread start];
  15. });
  16. return _networkRequestThread;
  17. }

这里这个NSMachPort的作用和上文中的一样,就是让线程不至于在很快死亡,然后RunLoop不至于退出(如果要使用这个MachPort的话,调用者需要持有这个NSMachPort,然后在外部线程通过这个port发送信息到这个loop内部,它这里没有这么做)。然后和上面的做法相似,在需要后台执行这个任务的时候,会通过调用:[NSObject performSelector:onThread:..]来将这个任务扔给后台线程的RunLoop中来执行。

  1. - (void)start {
  2. [self.lock lock];
  3. if ([self isCancelled]) {
  4. [self performSelector:@selector(cancelConnection) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];
  5. } else if ([self isReady]) {
  6. self.state = AFOperationExecutingState;
  7. [self performSelector:@selector(operationDidStart) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];
  8. }
  9. [self.lock unlock];
  10. }

GCD定时器的实现

  1. - (void)gcdTimer{
  2. // get the queue
  3. dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
  4. // creat timer
  5. self.timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
  6. // config the timer (starting time,interval)
  7. // set begining time
  8. dispatch_time_t start = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC));
  9. // set the interval
  10. uint64_t interver = (uint64_t)(1.0 * NSEC_PER_SEC);
  11. dispatch_source_set_timer(self.timer, start, interver, 0.0);
  12. dispatch_source_set_event_handler(self.timer, ^{
  13. // the tarsk needed to be processed async
  14. dispatch_async(dispatch_get_global_queue(0, 0), ^{
  15. for (int i = 0; i < 100000; i++) {
  16. NSLog(@"gcdTimer");
  17. }
  18. });
  19. });
  20. dispatch_resume(self.timer);

链接:https://www.jianshu.com/p/536184bfd163
收起阅读 »

还不会Hook?一份React Hook学习笔记

Hook 是 React 16.8.0 版本增加的新特性,可以在函数组件中使用 state 以及其他的 React 特性。 ✌️为什么要使用 Hook? 在组件之间复用状态逻辑很难 由providers,consumers,高阶组件,render prop...
继续阅读 »

Hook 是 React 16.8.0 版本增加的新特性,可以在函数组件中使用 state 以及其他的 React 特性。


✌️为什么要使用 Hook?




  • 在组件之间复用状态逻辑很难


    providersconsumers,高阶组件,render props等其他抽象层组成的组件会形成嵌套地狱,使用 Hook 可以从组件中提取状态逻辑,使得这些逻辑可以单独测试并复用。Hook 使你在无需修改组件结构的情况下复用状态逻辑




  • 复杂组件难以理解


    每个生命周期常常包含一些不相关的逻辑。例如,组件常常在 componentDidMountcomponentDidUpdate 中获取数据。但是,同一个 componentDidMount 中可能也包含很多其它的逻辑,如设置事件监听,而之后需在 componentWillUnmount 中清除。相互关联且需要对照修改的代码被进行了拆分,而完全不相关的代码却在同一个方法中组合在一起。




  • 难以理解的 class




下面介绍几个常用的Hook。


2. useState


useState让函数组件也可以有state状态,并进行状态数据的读写操作。


const [xxx, setXxx] = useState(initValue); // 解构赋值

📐useState() 方法


参数


第一次初始化指定的值在内部作缓存。可以按照需要使用数字字符串对其进行赋值,而不一定是对象


如果想要在state中存储两个不同的变量,只需调用 useState() 两次即可。


返回值


包含2个元素的数组,第1个为内部当前状态值,第2个为更新状态值的函数,一般直接采用解构赋值获取。


📐setXxx() 的写法


setXxx(newValue):参数为非函数值,直接指定新的状态值,内部用其覆盖原来的状态值


setXxx(value => newValue):参数为函数接收原本的状态值,返回新的状态值,内部用其覆盖原来的状态值。


📐 完整示例


const App = () => {
const [count, setCount] = useState(0);

const add = () => {
// 第一种写法
// setCount(count + 1);
// 第二种写法
setCount(count => count + 1);
};

return (
<Fragment>
<h2>当前求和为:{count}</h2>
<button onClick={add}>点我+1</button>
</Fragment>
);
};

useState就是一个 Hook,唯一的参数就是初始state,在这里声明了一个叫count的 state 变量,然后把它设为0。React会在重复渲染时记住它当前的值,并且提供最新的值给我们的函数。我们可以通过调用setCount来更新当前的count


在函数中,我们可以直接用 count


<h2>当前求和为:{count}</h2>

更新state


setCount(count + 1);
setCount(count => count + 1);

📐 使用多个 state 变量


可以在一个组件中多次使用State Hook


// 声明多个 state 变量
const [age, setAge] = useState(42);
const [fruit, setFruit] = useState('banana');
const [todos, setTodos] = useState([{ text: '学习 Hook' }]);

📌不是必须要使用多个state变量,仍然可以将相关数据分为一组。但是,不像 class 中的 this.setStateuseState中更新state变量是替换。不是合并


3. useEffect


useEffect可以在函数组件中执行副作用操作(用于模拟类组件中的生命周期钩子)。


React 中的副作用操作



  • ajax 请求数据获取

  • 设置订阅 / 启动定时器

  • 手动更改真实 DOM


📐 使用规则


useEffect(() => {
// 在此可以执行任何带副作用操作
// 相当于componentDidMount()
return () => {
// 在组件卸载前执行
// 在此做一些收尾工作, 比如清除定时器/取消订阅等
// 相当于componentWillUnmount()
};
}, [stateValue]); // 监听stateValue
// 如果省略数组,则检测所有的状态,状态有更新就又调用一次回调函数
// 如果指定的是[], 回调函数只会在第一次render()后执行一次

可以把 useEffect 看做如下三个函数的组合:



  • componentDidMount()

  • componentDidUpdate()

  • componentWillUnmount()


📐 每次更新的时候都运行 Effect


// ...
useEffect(() => {
// ...
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});

调用一个新的 effect 之前会对前一个 effect 进行清理。下面按时间列出一个可能会产生的订阅和取消订阅操作调用序列:


// Mount with { friend: { id: 100 } } props
ChatAPI.subscribeToFriendStatus(100, handleStatusChange); // 运行第一个 effect

// Update with { friend: { id: 200 } } props
ChatAPI.unsubscribeFromFriendStatus(100, handleStatusChange); // 清除上一个 effect
ChatAPI.subscribeToFriendStatus(200, handleStatusChange); // 运行下一个 effect

// Update with { friend: { id: 300 } } props
ChatAPI.unsubscribeFromFriendStatus(200, handleStatusChange); // 清除上一个 effect
ChatAPI.subscribeToFriendStatus(300, handleStatusChange); // 运行下一个 effect

// Unmount
ChatAPI.unsubscribeFromFriendStatus(300, handleStatusChange); // 清除最后一个 effect

📐 通过跳过 Effect 进行性能优化


如果某些特定值在两次重渲染之间没有发生变化,可以通知 React 跳过对 effect 的调用,只要传递数组作为 useEffect第二个可选参数即可:


useEffect(() => {
document.title = `You clicked ${count} times`;
}, [count]); // 仅在 count 更改时更新

如果 count 的值是 5,而且组件重渲染的时候 count 还是等于 5,React 将对前一次渲染的 [5] 和后一次渲染的 [5] 进行比较。因为数组中的所有元素都是相等的(5 === 5),React 会跳过这个 effect,这就实现了性能的优化。


当渲染时,如果 count 的值更新成了 6,React 将会把前一次渲染时的数组 [5] 和这次渲染的数组 [6] 中的元素进行对比。这次因为 5 !== 6,React 就会再次调用 effect。


📌 如果数组中有多个元素,即使只有一个元素发生变化,React 也会执行 effect。


对于有清除操作的 effect 同样适用:


useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}

ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
}, [props.friend.id]); // 仅在 props.friend.id 发生变化时,重新订阅

📐 使用多个 Effect 实现关注点分离


使用 Hook 其中一个目的就是要解决 class 中生命周期函数经常包含不相关的逻辑,但又把相关逻辑分离到了几个不同方法中的问题。下述代码是将前述示例中的计数器好友在线状态指示器逻辑组合在一起的组件:


function FriendStatusWithCounter(props) {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `You clicked ${count} times`;
});

const [isOnline, setIsOnline] = useState(null);
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}

ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});
// ...
}

📐 完整示例


import React, { useState, Fragment, useEffect } from 'react';
import ReactDOM from 'react-dom';

const App = () => {
const [count, setCount] = useState(0);

useEffect(() => {
let timer = setInterval(() => {
setCount(count => count + 1);
}, 500);
console.log('@@@@');
return () => {
clearInterval(timer);
};
}, [count]);
// 检测count的变化,每次变化,都会输出'@@@@'
// 如果是[],则只会输出一次'@@@@'

const add = () => {
setCount(count => count + 1);
};

const unmount = () => {
ReactDOM.unmountComponentAtNode(document.getElementById('root'));
};

return (
<Fragment>
<h2>当前求和为:{count}</h2>
<button onClick={add}>点我+1</button>
<button onClick={unmount}>卸载组件</button>
</Fragment>
);
};

export default App;

4. useRef


useRef可以在函数组件中存储 / 查找组件内的标签或任意其它数据。保存标签对象,功能与 React.createRef() 一样。


const refContainer = useRef(initialValue);

useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数(initialValue)。返回的 ref 对象在组件的整个生命周期内保持不变。


📌 当 ref 对象内容发生变化时,useRef不会通知你。变更 .current 属性不会引发组件重新渲染。


import React, { Fragment, useRef } from 'react';

const Demo = () => {
const myRef = useRef();

//提示输入的回调
function show() {
console.log(myRef.current.value);
}

return (
<Fragment>
<input type="text" ref={myRef} />
<button onClick={show}>点击显示值</button>
</Fragment>
);
};

export default Demo;

5. Hook规则


Hook 就是 JavaScript 函数,但是使用它们会有两个额外的规则:


只在最顶层使用 Hook


不要在循环,条件或嵌套函数中调用 Hook ,在 React 函数的最顶层调用 Hook。


如果想要有条件地执行一个 effect,可以将判断放到 Hook 的内部


useEffect(function persistForm() {
// 👍 将条件判断放置在 effect 中
if (name !== '') {
localStorage.setItem('formData', name);
}
});

只在 React 函数中调用 Hook


不要在普通的 JavaScript 函数中调用 Hook。可以:



  • 在 React 的函数组件中调用 Hook

  • 在自定义 Hook 中调用其他 Hook




链接:https://juejin.cn/post/6992733298493489183
收起阅读 »

图解React源码 - React 应用的3种启动方式

在前文reconciler 运作流程把reconciler的流程归结成 4 个步骤. 本章节主要讲解react应用程序的启动过程, 位于react-dom包, 衔接reconciler 运作流程中的输入步骤. 在正式分析源码之前, 先了解一下react应用的启...
继续阅读 »

在前文reconciler 运作流程reconciler的流程归结成 4 个步骤.


本章节主要讲解react应用程序的启动过程, 位于react-dom包, 衔接reconciler 运作流程中的输入步骤.


在正式分析源码之前, 先了解一下react应用的启动模式:


在当前稳定版react@17.0.2源码中, 有 3 种启动方式. 先引出官网上对于这 3 种模式的介绍, 其基本说明如下:




  1. legacy 模式: ReactDOM.render(<App />, rootNode). 这是当前 React app 使用的方式. 这个模式可能不支持这些新功能(concurrent 支持的所有功能).


    // LegacyRoot
    ReactDOM.render(<App />, document.getElementById('root'), dom => {}); // 支持callback回调, 参数是一个dom对象



  2. Blocking 模式: ReactDOM.createBlockingRoot(rootNode).render(<App />). 目前正在实验中, 它仅提供了 concurrent 模式的小部分功能, 作为迁移到 concurrent 模式的第一个步骤.


    // BolckingRoot
    // 1. 创建ReactDOMRoot对象
    const reactDOMBolckingRoot = ReactDOM.createBlockingRoot(
    document.getElementById('root'),
    );
    // 2. 调用render
    reactDOMBolckingRoot.render(<App />); // 不支持回调



  3. Concurrent 模式: ReactDOM.createRoot(rootNode).render(<App />). 目前在v18.0.0-alpha,和experiment版本中发布. 这个模式开启了所有的新功能.


    // ConcurrentRoot
    // 1. 创建ReactDOMRoot对象
    const reactDOMRoot = ReactDOM.createRoot(document.getElementById('root'));
    // 2. 调用render
    reactDOMRoot.render(<App />); // 不支持回调



注意: 虽然17.0.2的源码中有createRootcreateBlockingRoot方法(如果自行构建, 会默认构建experimental版本), 但是稳定版的构建入口排除掉了这两个 api, 所以实际在npm i react-dom安装17.0.2稳定版后, 不能使用该 api.如果要想体验非legacy模式, 需要显示安装alpha版本(或自行构建).


启动流程


在调用入口函数之前,reactElement(<App/>)和 DOM 对象div#root之间没有关联, 用图片表示如下:


process-before.png


创建全局对象


无论Legacy, Concurrent或Blocking模式, react 在初始化时, 都会创建 3 个全局对象



  1. ReactDOM(Blocking)Root对象





  1. fiberRoot对象



    • 属于react-reconciler包, 作为react-reconciler在运行过程中的全局上下文, 保存 fiber 构建过程中所依赖的全局状态.

    • 其大部分实例变量用来存储fiber 构造循环(详见两大工作循环)过程的各种状态.react 应用内部, 可以根据这些实例变量的值, 控制执行逻辑.




  2. HostRootFiber对象



    • 属于react-reconciler包, 这是 react 应用中的第一个 Fiber 对象, 是 Fiber 树的根节点, 节点的类型是HostRoot.




这 3 个对象是 react 体系得以运行的基本保障, 一经创建大多数场景不会再销毁(除非卸载整个应用root.unmount()).


这一过程是从react-dom包发起, 内部调用了react-reconciler包, 核心流程图如下(其中红色标注了 3 个对象的创建时机).


function-call.png


下面逐一解释这 3 个对象的创建过程.


创建 ReactDOM(Blocking)Root 对象


由于 3 种模式启动的 api 有所不同, 所以从源码上追踪, 也对应了 3 种方式. 最终都 new 一个ReactDOMRootReactDOMBlockingRoot的实例, 需要创建过程中RootTag参数, 3 种模式各不相同. 该RootTag的类型决定了整个 react 应用是否支持可中断渲染(后文有解释).


下面根据 3 种 mode 下的启动函数逐一分析.


legacy 模式


legacy模式表面上是直接调用ReactDOM.render, 跟踪ReactDOM.render后续调用legacyRenderSubtreeIntoContainer(源码链接)


function legacyRenderSubtreeIntoContainer(
parentComponent: ?React$Component<any, any>,
children: ReactNodeList,
container: Container,
forceHydrate: boolean,
callback: ?Function,
) {
let root: RootType = (container._reactRootContainer: any);
let fiberRoot;
if (!root) {
// 初次调用, root还未初始化, 会进入此分支
//1. 创建ReactDOMRoot对象, 初始化react应用环境
root = container._reactRootContainer = legacyCreateRootFromDOMContainer(
container,
forceHydrate,
);
fiberRoot = root._internalRoot;
if (typeof callback === 'function') {
const originalCallback = callback;
callback = function() {
// instance最终指向 children(入参: 如<App/>)生成的dom节点
const instance = getPublicRootInstance(fiberRoot);
originalCallback.call(instance);
};
}
// 2. 更新容器
unbatchedUpdates(() => {
updateContainer(children, fiberRoot, parentComponent, callback);
});
} else {
// root已经初始化, 二次调用render会进入
// 1. 获取ReactDOMRoot对象
fiberRoot = root._internalRoot;
if (typeof callback === 'function') {
const originalCallback = callback;
callback = function() {
const instance = getPublicRootInstance(fiberRoot);
originalCallback.call(instance);
};
}
// 2. 调用更新
updateContainer(children, fiberRoot, parentComponent, callback);
}
return getPublicRootInstance(fiberRoot);
}

继续跟踪legacyCreateRootFromDOMContainer. 最后调用new ReactDOMBlockingRoot(container, LegacyRoot, options);


function legacyCreateRootFromDOMContainer(
container: Container,
forceHydrate: boolean,
): RootType {
const shouldHydrate =
forceHydrate || shouldHydrateDueToLegacyHeuristic(container);
return createLegacyRoot(
container,
shouldHydrate
? {
hydrate: true,
}
: undefined,
);
}

export function createLegacyRoot(
container: Container,
options?: RootOptions,
): RootType {
return new ReactDOMBlockingRoot(container, LegacyRoot, options); // 注意这里的LegacyRoot是固定的, 并不是外界传入的
}

通过以上分析,legacy模式下调用ReactDOM.render有 2 个核心步骤:



  1. 创建ReactDOMBlockingRoot实例(在 Concurrent 模式和 Blocking 模式中详细分析该类), 初始化 react 应用环境.

  2. 调用updateContainer进行更新.


Concurrent 模式和 Blocking 模式


Concurrent模式和Blocking模式从调用方式上直接可以看出



  1. 分别调用ReactDOM.createRootReactDOM.createBlockingRoot创建ReactDOMRootReactDOMBlockingRoot实例

  2. 调用ReactDOMRootReactDOMBlockingRoot实例的render方法


export function createRoot(
container: Container,
options?: RootOptions,
): RootType {
return new ReactDOMRoot(container, options);
}

export function createBlockingRoot(
container: Container,
options?: RootOptions,
): RootType {
return new ReactDOMBlockingRoot(container, BlockingRoot, options); // 注意第2个参数BlockingRoot是固定写死的
}

继续查看ReactDOMRootReactDOMBlockingRoot对象


function ReactDOMRoot(container: Container, options: void | RootOptions) {
// 创建一个fiberRoot对象, 并将其挂载到this._internalRoot之上
this._internalRoot = createRootImpl(container, ConcurrentRoot, options);
}
function ReactDOMBlockingRoot(
container: Container,
tag: RootTag,
options: void | RootOptions,
) {
// 创建一个fiberRoot对象, 并将其挂载到this._internalRoot之上
this._internalRoot = createRootImpl(container, tag, options);
}

ReactDOMRoot.prototype.render = ReactDOMBlockingRoot.prototype.render = function(
children: ReactNodeList,
): void {
const root = this._internalRoot;
// 执行更新
updateContainer(children, root, null, null);
};

ReactDOMRoot.prototype.unmount = ReactDOMBlockingRoot.prototype.unmount = function(): void {
const root = this._internalRoot;
const container = root.containerInfo;
// 执行更新
updateContainer(null, root, null, () => {
unmarkContainerAsRoot(container);
});
};

ReactDOMRootReactDOMBlockingRoot有相同的特性



  1. 调用createRootImpl创建fiberRoot对象, 并将其挂载到this._internalRoot上.

  2. 原型上有renderumount方法, 且内部都会调用updateContainer进行更新.


创建 fiberRoot 对象


无论哪种模式下, 在ReactDOM(Blocking)Root的创建过程中, 都会调用一个相同的函数createRootImpl, 查看后续的函数调用, 最后会创建fiberRoot 对象(在这个过程中, 特别注意RootTag的传递过程):


// 注意: 3种模式下的tag是各不相同(分别是ConcurrentRoot,BlockingRoot,LegacyRoot).
this._internalRoot = createRootImpl(container, tag, options);

function createRootImpl(
container: Container,
tag: RootTag,
options: void | RootOptions,
) {
// ... 省略部分源码(有关hydrate服务端渲染等, 暂时用不上)
// 1. 创建fiberRoot
const root = createContainer(container, tag, hydrate, hydrationCallbacks); // 注意RootTag的传递
// 2. 标记dom对象, 把dom和fiber对象关联起来
markContainerAsRoot(root.current, container);
// ...省略部分无关代码
return root;
}

export function createContainer(
containerInfo: Container,
tag: RootTag,
hydrate: boolean,
hydrationCallbacks: null | SuspenseHydrationCallbacks,
): OpaqueRoot {
// 创建fiberRoot对象
return createFiberRoot(containerInfo, tag, hydrate, hydrationCallbacks); // 注意RootTag的传递
}

创建 HostRootFiber 对象


createFiberRoot中, 创建了react应用的首个fiber对象, 称为HostRootFiber(fiber.tag = HostRoot)


export function createFiberRoot(
containerInfo: any,
tag: RootTag,
hydrate: boolean,
hydrationCallbacks: null | SuspenseHydrationCallbacks,
): FiberRoot {
// 创建fiberRoot对象, 注意RootTag的传递
const root: FiberRoot = (new FiberRootNode(containerInfo, tag, hydrate): any);

// 1. 这里创建了`react`应用的首个`fiber`对象, 称为`HostRootFiber`
const uninitializedFiber = createHostRootFiber(tag);
root.current = uninitializedFiber;
uninitializedFiber.stateNode = root;
// 2. 初始化HostRootFiber的updateQueue
initializeUpdateQueue(uninitializedFiber);

return root;
}

在创建HostRootFiber时, 其中fiber.mode属性, 会与 3 种RootTag(ConcurrentRoot,BlockingRoot,LegacyRoot)关联起来.


export function createHostRootFiber(tag: RootTag): Fiber {
let mode;
if (tag === ConcurrentRoot) {
mode = ConcurrentMode | BlockingMode | StrictMode;
} else if (tag === BlockingRoot) {
mode = BlockingMode | StrictMode;
} else {
mode = NoMode;
}
return createFiber(HostRoot, null, null, mode); // 注意这里设置的mode属性是由RootTag决定的
}

注意:fiber树中所节点的mode都会和HostRootFiber.mode一致(新建的 fiber 节点, 其 mode 来源于父节点),所以HostRootFiber.mode非常重要, 它决定了以后整个 fiber 树构建过程.


运行到这里, 3 个对象创建成功, react应用的初始化完毕.


将此刻内存中各个对象的引用情况表示出来:



  1. legacy


process-legacy.png



  1. concurrent


process-concurrent.png



  1. blocking


process-blocking.png


注意:



  1. 3 种模式下,HostRootFiber.mode是不一致的

  2. legacy 下, div#rootReactDOMBlockingRoot之间通过_reactRootContainer关联. 其他模式是没有关联的

  3. 此时reactElement(<App/>)还是独立在外的, 还没有和目前创建的 3 个全局对象关联起来


调用更新入口



  1. legacy
    回到legacyRenderSubtreeIntoContainer函数中有:


// 2. 更新容器
unbatchedUpdates(() => {
updateContainer(children, fiberRoot, parentComponent, callback);
});


  1. concurrent 和 blocking
    ReactDOM(Blocking)Root原型上有render方法


ReactDOMRoot.prototype.render = ReactDOMBlockingRoot.prototype.render = function(
children: ReactNodeList,
): void {
const root = this._internalRoot;
// 执行更新
updateContainer(children, root, null, null);
};

相同点:



  1. 3 种模式在调用更新时都会执行updateContainer. updateContainer函数串联了react-domreact-reconciler, 之后的逻辑进入了react-reconciler包.


不同点:




  1. legacy下的更新会先调用unbatchedUpdates, 更改执行上下文为LegacyUnbatchedContext, 之后调用updateContainer进行更新.




  2. concurrentblocking不会更改执行上下文, 直接调用updateContainer进行更新.




继续跟踪updateContainer函数


export function updateContainer(
element: ReactNodeList,
container: OpaqueRoot,
parentComponent: ?React$Component<any, any>,
callback: ?Function,
): Lane {
const current = container.current;
// 1. 获取当前时间戳, 计算本次更新的优先级
const eventTime = requestEventTime();
const lane = requestUpdateLane(current);

// 2. 设置fiber.updateQueue
const update = createUpdate(eventTime, lane);
update.payload = { element };
callback = callback === undefined ? null : callback;
if (callback !== null) {
update.callback = callback;
}
enqueueUpdate(current, update);

// 3. 进入reconcier运作流程中的`输入`环节
scheduleUpdateOnFiber(current, lane, eventTime);
return lane;
}

updateContainer函数位于react-reconciler包中, 它串联了react-domreact-reconciler. 此处暂时不深入分析updateContainer函数的具体功能, 需要关注其最后调用了scheduleUpdateOnFiber.


在前文reconciler 运作流程中, 重点分析过scheduleUpdateOnFiber输入阶段的入口函数.


所以到此为止, 通过调用react-dom包的api(如: ReactDOM.render), react内部经过一系列运转, 完成了初始化, 并且进入了reconciler 运作流程的第一个阶段.


思考


可中断渲染


react 中最广为人知的可中断渲染(render 可以中断, 部分生命周期函数有可能执行多次, UNSAFE_componentWillMount,UNSAFE_componentWillReceiveProps)只有在HostRootFiber.mode === ConcurrentRoot | BlockingRoot才会开启. 如果使用的是legacy, 即通过ReactDOM.render(<App/>, dom)这种方式启动时HostRootFiber.mode = NoMode, 这种情况下无论是首次 render 还是后续 update 都只会进入同步工作循环, reconciliation没有机会中断, 所以生命周期函数只会调用一次.


对于可中断渲染的宣传最早来自2017 年 Lin Clark 的演讲. 演讲中阐述了未来 react 会应用 fiber 架构, reconciliation可中断等(13:15 秒). 在v16.1.0中应用了 fiber.


在最新稳定版v17.0.2中, 可中断渲染虽然实现, 但是并没有在稳定版暴露出 api. 只能安装alpha版本才能体验该特性.


但是不少开发人员认为稳定版本的react已经是可中断渲染(其实是有误区的), 大概率也是受到了各类宣传文章的影响. 前端大环境还是比较浮躁的, 在当下, 更需要静下心来学习.


总结


本章节介绍了react应用的 3 种启动方式. 分析了启动后创建了 3 个关键对象, 并绘制了对象在内存中的引用关系. 启动过程最后调用updateContainer进入react-reconciler包,进而调用schedulerUpdateOnFiber函数, 与reconciler运作流程中的输入阶段相衔接.


写在最后


本文属于图解react源码系列中的运行核心板块, 本系列近 20 篇文章,真的是为了搞懂React源码, 进而提升架构和编码能力.


目前图解部分初稿已经全部完成, 将在8月全部更新, 如文章有表述错误, 会在github第一时间修正.


链接:https://juejin.cn/post/6992771308157141022
收起阅读 »

白话聊React为何采用函数式编程的不可变数据

前言 大家好,今天来聊一下React采用函数式编程的理念:不可变数据。 看到标题的你不用担心,你可能在顾虑需要函数式编程的知识,完全不需要,今天我们就0基础聊聊什么是不可变数据?React采用这种方式有什么好处? 例子 React采用函数式编程的不可变数据特性...
继续阅读 »

前言


大家好,今天来聊一下React采用函数式编程的理念:不可变数据。


看到标题的你不用担心,你可能在顾虑需要函数式编程的知识,完全不需要,今天我们就0基础聊聊什么是不可变数据?React采用这种方式有什么好处?


例子


React采用函数式编程的不可变数据特性。


而在React中不可变值的意思就是:始终保持state的原值不变。


不要直接修改state,遇到数组或者对象,采用copy一份出去做改变。


举个简单的例子:


基本类型


错误的做法:


this.state.count++
this.setState({
count:this.state.count
})

正确的做法:


this.setState({
count:this.state.count + 1
})

引用类型


错误的做法:



this.state.obj1.a = 100
this.state.obj2.a = 100
this.setState({
obj1: this.state.obj1,
obj2: this.state.obj2
})

正确的做法:


this.setState({
obj1: Object.assign({}, this.state.obj1, {a: 100}),
obj2: {...this.state.obj2, a: 100}
})

函数式编程不可变值的优势


我们先聊聊函数式编程中不可变值的好处。


减少bug


好比我们用const定义一个变量,如果你要改变这个变量,你需要把变量copy出去修改。
这样的做法,可以让你的代码少非常多的bug


生成简单的状态管理便于维护


因为,程序中的状态是非常不好维护的,特别是在并发的情况下更不好维护。
试想一下:如果你的代码有一个复杂的状态,当以后别人改你代码的时候,是很容易出bug的。


React中采用函数式编程的不可变值


我们再来看看React中采用函数式编程


性能优化


在生命周期 shouldComponentUpdate 中,React会对新旧state进行比较,如果直接修改state去用于其他变量的计算,而实际上state并不需要修改,则会导致怪异的更新以及没必要的更新,因此采用这种方式是非常巧妙,且效率非常的高。


可追踪修改痕迹,便于排错


使用this.setState的方式进行修改state的值,相当于开了一个改变值的口子,所有的修改都会走这样的口子,相比于直接修改,这样的控制力更强,能够有效地记录与追踪每个state的改变,对排查bug十分有帮助。


结尾


关于React函数式编程你有什么观点或者想法,欢迎在评论区告诉我。


链接:https://juejin.cn/post/6992919744165150751

收起阅读 »

这几个关键的数据结构都不会,你也配学react源码???

不知道大家在学习react源码的时候有没有这样的感觉:fiber对象的结构太复杂了,不仅是属性繁多,而且有些属性还是个巨复杂的对象。我在学习hooks的时候,这种感觉尤为强烈。那么,这篇文章就以fiber.memoizedState和fiber.updateQ...
继续阅读 »

不知道大家在学习react源码的时候有没有这样的感觉:fiber对象的结构太复杂了,不仅是属性繁多,而且有些属性还是个巨复杂的对象。我在学习hooks的时候,这种感觉尤为强烈。那么,这篇文章就以fiber.memoizedStatefiber.updateQueue这两个重要属性为例,梳理一下他们的结构和作用,希望对大家阅读源码有帮助。


先来看类组件


首先来看类组件。类组件的fiber.memoizedStatefiber.updateQueue比较简单,memoizedState就是我们定义的state对象,updateQueue的结构如下:


fiber.updateQueue {
baseState,
firstBaseUpdate,
lastBaseUpdate,
shared: {
pending,
},
effects
}

首先,前三个属性baseStatefirstBaseUpdatelastBaseUpdate是和可中断更新相关的。在concurrent mode下,当前的更新可能被更高优先级的更新打断,而baseState就记录了被跳过的update节点之前计算出的statefirstBaseStatelastBaseUpdate构成一条链表,记录因为优先级不足而被跳过的所有更新。shared.pending才是真正记录了所有update对象的链表,这是一条环形链表,shared.pending指向该链表的最后一个update对象,effects则是一个数组,存储了setState的回调函数。关于类组件的state计算原理,可以看我的这篇文章,这里面有更加详细的讲解,本文还是主要介绍数据结构。


看函数组件


了解hooks原理的同学应该都知道,react使用链表来存储函数组件中用到的所有hooks,而这个链表就保存在fiber.memoizedState。这个hooks链表的每一项都是hook对象,hook对象的结构如下


hook {
memoizedState,
baseState,
baseQueue,
queue,
next
}

hooks链表.jpg



没错,fiber.memoizedState指向了hook链表,而每个hook对象还有一个memoizedState对象!!!



不论是哪种hook,都会使用这一种数据结构,而有些hook可能只会用到其中的几个属性。下面,就由简入繁,介绍不同的hook都用到了哪些属性。


useCallback, useMemo, useRef


这三个hook都只使用到了hook.memoizedState这一个属性。


useCallback接受一个需要缓存的函数和依赖数组,而hook.memoizedState则保存了一个数组,数组内放了缓存函数和依赖数组


function mountCallback(callback, deps) {
var hook = mountWorkInProgressHook();
var nextDeps = deps === undefined ? null : deps;
hook.memoizedState = [callback, nextDeps];
return callback;
}

useMemouseCallback基本一致,只不过useMemo保存了函数的返回值


function mountMemo(nextCreate, deps) {
var hook = mountWorkInProgressHook();
var nextDeps = deps === undefined ? null : deps;
var nextValue = nextCreate();
hook.memoizedState = [nextValue, nextDeps];
return nextValue;
}

至于useRef就能简单了,保存一个对象即可


function mountRef(initialValue) {
var hook = mountWorkInProgressHook();
var ref = {
current: initialValue
};

{
Object.seal(ref);
}

hook.memoizedState = ref;
return ref;
}

这里使用Object.seal封闭了ref对象,Object.seal就是在不可扩展(Object.preventExtensions)的基础上不可配置(configurable变为false)。


useState


看完了三个简单的hook,现在加大一点难度,看看useState


useState就比较复杂了,用到了hook对象身上的所有属性


hook {
memoizedState: 保存的state,
baseState:和类组件的updateQueue.baseState作用相同,
baseQueue: 和类组件的updateQueue.firstBaseUpdate和lastBaseUpdate作用相同,
queue: {
pending: 保存update对象的环状链表,
dispatch: dispatchAction,useState的第二个返回值,
lastRenderedReducer: 上次更新时用到的reducer,
lastRenderedState: 上次更新的state
}
}

这里需要注意一点,useState中用到了hook.queue这个属性,而hook.queue.pending和类组件的updateQueue.shared.pending比较类似,都是用来保存update对象的。而使用useState时,fiber.updateQueuenull,也就是说和类组件不同,useState并没有用到updateQueue,这一点容易让人困惑。


useEffect


接下来看看我个人认为最复杂的hookuseEffect。说他复杂,是因为useEffect的数据结构很绕,下面一起来看一下


首先,关于hook对象身上的几个属性,useEffect只用到了一个,就是memoizedState,而在memoizedState上保存的又是一个effect对象


hook {
memoized: {
tag: 用于区分是useEffect还是useLayoutEffect,
create: 就是useEffect的回调函数,
destory: useEffect回调的返回值,
deps: 依赖数组,
next: 下一个effect对象,构成环装链表
},
baseState: null,
baseQueue: null,
queue: null
}

此外,useEffect还用到了fiber.updateQueue,这个updateQueue的结构还和类组件的不一样


// 类组件的updateQueue
{
baseState,
firstBaseUpdate,
lastBaseUpdate,
shared: {
pending
},
effects
}

// useEffect的updateQueue
{
lastEffect
}

useEffect不仅将effect对象放在了hook.memoized上,还放在了fiber.updateQueue上,并且都是环形链表


举个🌰


前面说了这么多,可能大家还是比较困惑,下面看个实际的例子


const App = () => {
const [num, setNum] = useState(1)
const handleClick1 = useCallback(() => {
setNum((pre) => pre + 1)
}, [])

const [count, setCount] = useState(1)
const handleClick2 = useCallback(() => {
setCount((pre) => pre + 1)
}, [])

useEffect(() => {
console.log('1st effect', num)
}, [num])

useEffect(() => {
console.log('2nd effect', num)
}, [num])

useLayoutEffect(() => {
console.log('use layout', count)
}, [count])

return (
<div>
<p>{num}</p>
<p>{count}</p>
<button onClick={handleClick1}>click me</button>
<button onClick={handleClick2}>click me</button>
</div>
)
}

上面的例子中,我们使用了两个useState,并且每个事件处理函数都使用useCallback进行了包装,此外还有useEffectuseLayoutEffect,下面看一下



  1. 首先,执行useState,因此在fiber.memoizedState上挂载了一个useStatehook对象


例子1.jpg



  1. 接下来执行到useCallback,在hooks链表中再添加一项


例子2.jpg



  1. 接下来又是useStateuseCallback,因此和前两步一样


例子3.jpg



  1. 之后来到比较复杂的useEffect了,useEffect不仅会添加hook对象到hooks链表中,还会修改updateQueue


例子4.jpg



  1. 后面还是一个useEffect,这一步就比较复杂了


例子5.jpg


可以看到,使用useEffect时,不仅构成了hooks链表,effect对象也构成了一条环形链表



  1. 之后执行了useLayoutEffect


例子6.jpg


最后来总结一下这个例子


例子总结.jpg


链接:https://juejin.cn/post/6993150359317250085
收起阅读 »

react hooks 万字总结

Hooks is what? react-hooks是react16.8以后,react新增的钩子API,它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性. Hook是一些可以让你在函数组件里“钩入” React sta...
继续阅读 »

Hooks is what?



  • react-hooks是react16.8以后,react新增的钩子API,它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性.

  • Hook是一些可以让你在函数组件里“钩入” React state 及生命周期等特性的函数。


why use Hooks?


类组件的缺点:(来自官网动机




  • 在组件之间复用状态逻辑很难




  • 复杂组件变得难以理解




  • 难以理解的 class




你必须去理解 JavaScript 中 this 的工作方式,这与其他语言存在巨大差异。还不能忘记绑定事件处理器。没有稳定的语法提案,代码非常冗余。


hooks的出现,解决了上面的问题。另外,还有一些其他的优点



  • 增加代码的可复用性,逻辑性,弥补无状态组件没有生命周期,没有数据管理状态state的缺陷

  • react-hooks思想更趋近于函数式编程。用函数声明方式代替class声明方式,虽说class也是es6构造函数语法糖,但是react-hooks写起来函数即是组件,无疑也提高代码的开发效率(无需像class声明组件那样写声明周期,写生命周期render函数等)


Hooks没有破坏性改动



  • 完全可选的。  你无需重写任何已有代码就可以在一些组件中尝试 Hook。但是如果你不想,你不必现在就去学习或使用 Hook。

  • 100% 向后兼容的。  Hook 不包含任何破坏性改动。

  • 现在可用。  Hook 已发布于 v16.8.0。


使用Hooks的规则


1. 只在最顶层使用 Hook,不要在循环,条件或嵌套函数中调用 Hook


确保总是在你的 React 函数的最顶层调用他们。遵守这条规则,你就能确保 Hook 在每一次渲染中都按照同样的顺序被调用。这让 React 能够在多次的 useState 和 useEffect 调用之间保持 hook 状态的正确。


2. 只在 React 函数中调用 Hook


不要在普通的 JavaScript 函数中调用 Hook,你可以:



  • ✅ 在 React 的函数组件中调用 Hook

  • ✅ 在自定义 Hook 中调用其他 Hook


至于为什么会有这些规则,如果你感兴趣,请参考Hook 规则


useState



const [state, setState] = useState(initialState)




  • useState 有一个参数(initialState 可以是一个函数,返回一个值,但一般都不会这么用),该参数可以为任意数据类型,一般用作默认值.

  • useState 返回值为一个数组,数组的第一个参数为我们需要使用的 state,第二个参数为一个改变state的函数(功能和this.setState一样)


来看一个计时器的案例


import React,{useState} from "react";
function Example() {
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>Click me</button>
</div>
);
}
export default Example;


  • 第一行:  引入 React 中的 useState Hook。它让我们在函数组件中存储内部 state。

  • 第三行:  在 Example 组件内部,我们通过调用 useState Hook 声明了一个新的 state 变量。它返回一对值给到我们命名的变量上。我们把变量命名为 count,因为它存储的是点击次数。我们通过传 0 作为 useState 唯一的参数来将其初始化为 0。第二个返回的值本身就是一个函数。它让我们可以更新 count 的值,所以我们叫它 setCount

  • 第七行:  当用户点击按钮后,我们传递一个新的值给 setCount。React 会重新渲染 Example 组件,并把最新的 count 传给它。


使用多个state 变量


 // 声明多个 state 变量
const [age, setAge] = useState(42);
const [fruit, setFruit] = useState('banana');
const [todos, setTodos] = useState([{ text: '学习 Hook' }]);

不必使用多个 state 变量。State 变量可以很好地存储对象和数组,因此,你仍然可以将相关数据分为一组。


更新State


import React,{useState} from "react";
function Example() {
const [count, setCount] = useState(0);
const [person, setPerson] = useState({name:'jimmy',age:22});
return (
<div>
<p>name {person.name} </p>
// 如果新的 state 需要通过使用先前的 state 计算得出,那么可以将回调函数当做参数传递给 setState。
// 该回调函数将接收先前的 state,并返回一个更新后的值。
<button onClick={() => setCount(count=>count+1)}>Click me</button>
<button onClick={() => setPerson({name:'chimmy'})}>Click me</button>
</div>
);
}
export default Example;
复制代码

setPerson更新person时,不像 class 中的 this.setState,更新 state 变量总是替换它而不是合并它。上例中的person为{name:'chimmy'} 而不是{name:'chimmy',age:22}


useEffect


Effect Hook 可以让你在函数组件中执行副作用(数据获取,设置订阅以及手动更改 React 组件中的 DOM 都属于副作用)操作



useEffect(fn, array)



useEffect在初次完成渲染之后都会执行一次, 配合第二个参数可以模拟类的一些生命周期。


如果你熟悉 React class 的生命周期函数,你可以把 useEffect Hook 看做 componentDidMount``componentDidUpdate 和 componentWillUnmount 这三个函数的组合。


useEffect 实现componentDidMount


如果第二个参数为空数组,useEffect相当于类组件里面componentDidMount。


import React, { useState, useEffect } from "react";
function Example() {
const [count, setCount] = useState(0);
useEffect(() => {
console.log("我只会在组件初次挂载完成后执行");
}, []);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>Click me</button>
</div>
);
}
export default Example;

页面渲染完成后,会执行一次useEffect。打印“我只会在组件初次挂载完成后执行”,当点击按钮改变了state,页面重新渲染后,useEffect不会执行。


useEffect 实现componentDidUpdate


如果不传第二个参数,useEffect 会在初次渲染和每次更新时,都会执行。


import React, { useState, useEffect } from "react";
function Example() {
const [count, setCount] = useState(0);
useEffect(() => {
console.log("我会在初次组件挂载完成后以及重新渲染时执行");
});
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>Click me</button>
</div>
);
}
export default Example;

初次渲染时,会执行一次useEffect,打印出“我会在初次组件挂载完成后以及重新渲染时执行”。
当点击按钮时,改变了state,页面重新渲染,useEffect都会执行,打印出“我会在初次组件挂载完成后以及重新渲染时执行”。


useEffect 实现componentWillUnmount


effect 返回一个函数,React 将会在执行清除操作时调用它。


useEffect(() => {
console.log("订阅一些事件");
return () => {
console.log("执行清楚操作")
}
},[]);

注意:这里不只是组件销毁时才会打印“执行清楚操作”,每次重新渲染时也都会执行。至于原因,我觉得官网解释的很清楚,请参考 解释: 为什么每次更新的时候都要运行 Effect


控制useEffect的执行


import React, { useState, useEffect } from "react";
function Example() {
const [count, setCount] = useState(0);
const [number, setNumber] = useState(1);
useEffect(() => {
console.log("我只会在cout变化时执行");
}, [count]);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>Click cout</button>
<button onClick={() => setNumber(count + 1)}>Click number</button>
</div>
);
}
export default Example;

上面的例子,在点击 click cout按钮时,才会打印“我只会在cout变化时执行”。 因为useEffect 的第二个参数的数组里面的依赖是cout,所以,只有cout发生改变时,useEffect 才会执行。如果数组中有多个元素,即使只有一个元素发生变化,React 也会执行 effect。


使用多个 Effect 实现关注点分离


使用 Hook 其中一个目的就是要解决 class 中生命周期函数经常包含不相关的逻辑,但又把相关逻辑分离到了几个不同方法中的问题。


import React, { useState, useEffect } from "react";
function Example() {
useEffect(() => {
// 逻辑一
});
useEffect(() => {
// 逻辑二
});
useEffect(() => {
// 逻辑三
});
return (
<div>
useEffect的使用
</div>
);
}
export default Example;

Hook 允许我们按照代码的用途分离他们,  而不是像生命周期函数那样。React 将按照 effect 声明的顺序依次调用组件中的每一个 effect。


useEffect中使用异步函数


useEffect是不能直接用 async await 语法糖的


/* 错误用法 ,effect不支持直接 async await*/
useEffect(async ()=>{
/* 请求数据 */
const res = await getData()
},[])

useEffect 的回调参数返回的是一个清除副作用的 clean-up 函数。因此无法返回 Promise,更无法使用 async/await


那我们应该如何让useEffect支持async/await呢?


方法一(推荐)


const App = () => {
useEffect(() => {
(async function getDatas() {
await getData();
})();
}, []);
return <div></div>;
};

方法二


  useEffect(() => {
const getDatas = async () => {
const data = await getData();
setData(data);
};
getDatas();
}, []);

useEffect 做了什么


通过使用这个 Hook,你可以告诉 React 组件需要在渲染后执行某些操作。React 会保存你传递的函数(我们将它称之为 “effect”),并且在执行 DOM 更新之后调用它。


为什么在组件内部调用 useEffect


将 useEffect 放在组件内部让我们可以在 effect 中直接访问 count state 变量(或其他 props)。我们不需要特殊的 API 来读取它 —— 它已经保存在函数作用域中。Hook 使用了 JavaScript 的闭包机制,而不用在 JavaScript 已经提供了解决方案的情况下,还引入特定的 React API。


useContext


概念



const value = useContext(MyContext);



接收一个 context 对象(React.createContext 的返回值)并返回该 context 的当前值。当组件上层最近的 <MyContext.Provider> 更新时,该 Hook 会触发重渲染,并使用最新传递给 MyContext provider 的 context value 值。即使祖先使用 React.memo 或 shouldComponentUpdate,也会在组件本身使用 useContext 时重新渲染。


别忘记 useContext 的参数必须是 context 对象本身



  • 正确:  useContext(MyContext)

  • 错误:  useContext(MyContext.Consumer)

  • 错误:  useContext(MyContext.Provider)


示例


index.js


import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
// 创建两个context
export const UserContext = React.createContext();
export const TokenContext = React.createContext();
ReactDOM.render(
<UserContext.Provider value={{ id: 1, name: "chimmy", age: "20" }}>
<TokenContext.Provider value="我是token">
<App />
</TokenContext.Provider>
</UserContext.Provider>,
document.getElementById("root")
);

app.js


import React, { useContext } from "react";
import { UserContext, TokenContext } from "./index";

function Example() {
let user = useContext(UserContext);
let token = useContext(TokenContext);
console.log("UserContext", user);
console.log("TokenContext", token);
return (
<div>
name:{user?.name},age:{user?.age}
</div>
);
}
export default Example;

打印的值如下


41PXCT[XET_ZJ7]79K@~6JX.png


提示


如果你在接触 Hook 前已经对 context API 比较熟悉,那应该可以理解,useContext(MyContext) 相当于 class 组件中的 static contextType = MyContext 或者 <MyContext.Consumer>


useContext(MyContext) 只是让你能够读取 context 的值以及订阅 context 的变化。你仍然需要在上层组件树中使用 <MyContext.Provider> 来为下层组件提供 context。


useReducer


概念



const [state, dispatch] = useReducer(reducer, initialArg, init);



useState 的替代方案。它接收一个形如 (state, action) => newState 的 reducer,并返回当前的 state 以及与其配套的 dispatch 方法。(如果你熟悉 Redux 的话,就已经知道它如何工作了。)


在某些场景下,useReducer 会比 useState 更适用,例如 state 逻辑较复杂且包含多个子值,或者下一个 state 依赖于之前的 state 等。并且,使用 useReducer 还能给那些会触发深更新的组件做性能优化,因为你可以向子组件传递 dispatch 而不是回调函数


注意点



React 会确保 dispatch 函数的标识是稳定的,并且不会在组件重新渲染时改变。这就是为什么可以安全地从 useEffect 或 useCallback 的依赖列表中省略 dispatch



示例


import React, { useReducer } from "react";
export default function Home() {
function reducer(state, action) {
switch (action.type) {
case "increment":
return { ...state, counter: state.counter + 1 };
case "decrement":
return { ...state, counter: state.counter - 1 };
default:
return state;
}
}
const [state, dispatch] = useReducer(reducer, { counter: 0 });
return (
<div>
<h2>Home当前计数: {state.counter}</h2>
<button onClick={(e) => dispatch({ type: "increment" })}>+1</button>
<button onClick={(e) => dispatch({ type: "decrement" })}>-1</button>
</div>
);
}

useCallback


概念


const memoizedCallback = useCallback(
() => {
doSomething(a, b);
},
[a, b],
);

返回一个 [memoized]回调函数。


把内联回调函数及依赖项数组作为参数传入 useCallback,它将返回该回调函数的 memoized 版本,该回调函数仅在某个依赖项改变时才会更新。当你把回调函数传递给经过优化的并使用引用相等性去避免非必要渲染(例如 shouldComponentUpdate)的子组件时,它将非常有用。


示例


import React, { useState } from "react";
// 子组件
function Childs(props) {
console.log("子组件渲染了");
return (
<>
<button onClick={props.onClick}>改标题</button>
<h1>{props.name}</h1>
</>
);
}
const Child = React.memo(Childs);
function App() {
const [title, setTitle] = useState("这是一个 title");
const [subtitle, setSubtitle] = useState("我是一个副标题");
const callback = () => {
setTitle("标题改变了");
};
return (
<div className="App">
<h1>{title}</h1>
<h2>{subtitle}</h2>
<button onClick={() => setSubtitle("副标题改变了")}>改副标题</button>
<Child onClick={callback} name="桃桃" />
</div>
);
}

执行结果如下图
image.png


当我点击改副标题这个 button 之后,副标题会变为「副标题改变了」,并且控制台会再次打印出子组件渲染了,这就证明了子组件重新渲染了,但是子组件没有任何变化,那么这次 Child 组件的重新渲染就是多余的,那么如何避免掉这个多余的渲染呢?


找原因


我们在解决问题的之前,首先要知道这个问题是什么原因导致的?


咱们来分析,一个组件重新重新渲染,一般三种情况:



  1. 要么是组件自己的状态改变

  2. 要么是父组件重新渲染,导致子组件重新渲染,但是父组件的 props 没有改变

  3. 要么是父组件重新渲染,导致子组件重新渲染,但是父组件传递的 props 改变


接下来用排除法查出是什么原因导致的:


第一种很明显就排除了,当点击改副标题 的时候并没有去改变 Child 组件的状态;


第二种情况,我们这个时候用 React.memo 来解决了这个问题,所以这种情况也排除。


那么就是第三种情况了,当父组件重新渲染的时候,传递给子组件的 props 发生了改变,再看传递给 Child 组件的就两个属性,一个是 name,一个是 onClickname 是传递的常量,不会变,变的就是 onClick 了,为什么传递给 onClick 的 callback 函数会发生改变呢?其实在函数式组件里每次重新渲染,函数组件都会重头开始重新执行,那么这两次创建的 callback 函数肯定发生了改变,所以导致了子组件重新渲染。


用useCallback解决问题


const callback = () => {
doSomething(a, b);
}
const memoizedCallback = useCallback(callback, [a, b])

把函数以及依赖项作为参数传入 useCallback,它将返回该回调函数的 memoized 版本,这个 memoizedCallback 只有在依赖项有变化的时候才会更新。


那么只需这样将传给Child组件callback函数的改造一下就OK了


const callback = () => { setTitle("标题改变了"); };
// 通过 useCallback 进行记忆 callback,并将记忆的 callback 传递给 Child
<Child onClick={useCallback(callback, [])} name="桃桃" />

这样我们就可以看到只会在首次渲染的时候打印出子组件渲染了,当点击改副标题和改标题的时候是不会打印子组件渲染了的。


useMemo


概念



const cacheSomething = useMemo(create,deps)




  • create:第一个参数为一个函数,函数的返回值作为缓存值。

  • deps: 第二个参数为一个数组,存放当前 useMemo 的依赖项,在函数组件下一次执行的时候,会对比 deps 依赖项里面的状态,是否有改变,如果有改变重新执行 create ,得到新的缓存值。

  • cacheSomething:返回值,执行 create 的返回值。如果 deps 中有依赖项改变,返回的重新执行 create 产生的值,否则取上一次缓存值。


useMemo原理


useMemo 会记录上一次执行 create 的返回值,并把它绑定在函数组件对应的 fiber 对象上,只要组件不销毁,缓存值就一直存在,但是 deps 中如果有一项改变,就会重新执行 create ,返回值作为新的值记录到 fiber 对象上。


示例


function Child(){
console.log("子组件渲染了")
return <div>Child</div>
}
function APP(){
const [count, setCount] = useState(0);
const userInfo = {
age: count,
name: 'jimmy'
}
return <Child userInfo={userInfo}>
}


当函数组件重新render时,userInfo每次都将是一个新的对象,无论 count 发生改变没,都会导致 Child组件的重新渲染。


而下面的则会在 count 改变后才会返回新的对象。


function Child(){
console.log("子组件渲染了")
return <div>Child</div>
}
function APP(){
const [count, setCount] = useState(0);
const userInfo = useMemo(() => {
return {
name: "jimmy",
age: count
};
}, [count]);
return <Child userInfo={userInfo}>
}

实际上 useMemo 的作用不止于此,根据官方文档内介绍:以把一些昂贵的计算逻辑放到 useMemo 中,只有当依赖值发生改变的时候才去更新。


import React, {useState, useMemo} from 'react';

// 计算和的函数,开销较大
function calcNumber(count) {
console.log("calcNumber重新计算");
let total = 0;
for (let i = 1; i <= count; i++) {
total += i;
}
return total;
}
export default function MemoHookDemo01() {
const [count, setCount] = useState(100000);
const [show, setShow] = useState(true);
const total = useMemo(() => {
return calcNumber(count);
}, [count]);
return (
<div>
<h2>计算数字的和: {total}</h2>
<button onClick={e => setCount(count + 1)}>+1</button>
<button onClick={e => setShow(!show)}>show切换</button>
</div>
)
}

当我们去点击 show切换按钮时,calcNumber这个计算和的函数并不会出现渲染了.只有count 发生改变时,才会出现计算.


useCallback 和 useMemo 总结


简单理解呢 useCallback 与 useMemo 一个缓存的是函数,一个缓存的是函数的返回就结果。useCallback 是来优化子组件的,防止子组件的重复渲染。useMemo 可以优化当前组件也可以优化子组件,优化当前组件主要是通过 memoize 来将一些复杂的计算逻辑进行缓存。当然如果只是进行一些简单的计算也没必要使用 useMemo。


我们可以将 useMemo 的返回值定义为返回一个函数这样就可以变通的实现了 useCallback。useCallback(fn, deps) 相当于 useMemo(() => fn, deps)


useRef



const refContainer = useRef(initialValue);



useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数(initialValue)。返回的 ref 对象在组件的整个生命周期内保持不变


useRef 获取dom


useRef,它有一个参数可以作为缓存数据的初始值,返回值可以被dom元素ref标记,可以获取被标记的元素节点.


import React, { useRef } from "react";
function Example() {
const divRef = useRef();
function changeDOM() {
// 获取整个div
console.log("整个div", divRef.current);
// 获取div的class
console.log("div的class", divRef.current.className);
// 获取div自定义属性
console.log("div自定义属性", divRef.current.getAttribute("data-clj"));
}
return (
<div>
<div className="div-class" data-clj="我是div的自定义属性" ref={divRef}>
我是div
</div>
<button onClick={(e) => changeDOM()}>获取DOM</button>
</div>
);
}
export default Example;

1.png


useRef 缓存数据


useRef还有一个很重要的作用就是缓存数据,我们知道usestate ,useReducer 是可以保存当前的数据源的,但是如果它们更新数据源的函数执行必定会带来整个组件从新执行到渲染,如果在函数组件内部声明变量,则下一次更新也会重置,如果我们想要悄悄的保存数据,而又不想触发函数的更新,那么useRef是一个很棒的选择。


下面举一个,每次换成state 上一次值的例子


import React, { useRef, useState, useEffect } from "react";
function Example() {
const [count, setCount] = useState(0);

const numRef = useRef(count);

useEffect(() => {
numRef.current = count;
}, [count]);

return (
<div>
<h2>count上一次的值: {numRef.current}</h2>
<h2>count这一次的值: {count}</h2>
<button onClick={(e) => setCount(count + 10)}>+10</button>
</div>
);
}
export default Example;

当 ref 对象内容发生变化时,useRef 并不会通知你。变更 .current 属性不会引发组件重新渲染。所以,上面的例子中虽然numRef.current的值,已经改变了,但是页面上还是显示的上一次的值,重新更新时,才会显示上一次更新的值。


写在最后


如果文章中有什么错误,欢迎指出。


链接:https://juejin.cn/post/6993139082054336548

收起阅读 »

JAVA面向对象简介

文章目录概念了解举例说明:创建一个Soldier类举例说明:创建一个FlashLight类举例说明:创建一个Car类类中包含的变量类中的方法概念了解Java是一种面向对象的程序设计语言,了解面向对象的编程思想对于学习Java开发相当重要。面向对象是一种符合人类...
继续阅读 »

文章目录

概念了解

Java是一种面向对象的程序设计语言,了解面向对象的编程思想对于学习Java开发相当重要。

面向对象是一种符合人类思维习惯的编程思想。现实生活中存在各种形态不同的事物,这些事物之间存在着各种各样的联系。在程序中使用对象来映射现实中的事物,使用对象的关系来描述事物之间的联系,这种思想就是面向对象。

面向对象和面向过程的区别
提到面向对象,自然会想到面向过程,面向过程就是分析解决问题所需要的步骤,然后用函数把这些步骤一一实现,使用的时候一个一个依次调用就可以了。面向对象则是把解决的问题按照一定规则划分为多个独立的对象,然后通过调用对象的方法来解决问题。当然,一个应用程序会包含多个对象,通过多个对象的相互配合来实现应用程序的功能,这样当应用程序功能发生变动时,只需要修改个别的对象就可以了,从而使代码更容易得到维护。

首先了解几个概念:


类是一个模板,它描述一类对象的行为和状态。

对象(实例)
对象是类的一个实例(对象不是找个女朋友),有状态和行为。例如,一条狗是一个对象,它的状态有:颜色、名字、品种;行为有:摇尾巴、叫、吃等。
每个对象占用独立的内存空间,保存各自的属性数据。
每个对象可以独立控制,让它执行指定的方法代码。

举例说明:创建一个Soldier类

我们定义一个士兵类,他的属性有 id(表示他的唯一编号)、blood(表示他的血量)。他的行为有 go(前进)、attack(攻击)

Soldier类

public class Soldier {
//成员变量
int id;//唯一编号,默认0
int blood = 100;//血量,默认满血


//成员方法
public void go(TextView tv) {
if (blood == 0) {
tv.setText(id + "已阵亡,无法前进" + "\n" + tv.getText());
return;
}
tv.setText(id + "前进" + "\n" + tv.getText());
}

public void attack(TextView tv) {
if (blood == 0) {
tv.setText(id + "已阵亡,无法攻击" + "\n" + tv.getText());
return;
}

tv.setText(+id + "号士兵发起进攻" + "]\n" + tv.getText());

int b = new Random().nextInt(30);
if (blood < b) {
blood = b;
}
blood -= b;

if (blood == 0) {
tv.setText("["+id + "号士兵阵亡" + "\n" + tv.getText());
} else {
tv.setText("[士兵" + id + "进攻完毕,血量" + blood + "\n" + tv.getText());
}
}
}

xml

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="vertical">


<Button
android:id="@+id/button1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="doClick"
android:text="新建士兵" />


<Button
android:id="@+id/button2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="doClick"
android:text="前进" />


<Button
android:id="@+id/button3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="doClick"
android:text="进攻" />


<TextView
android:id="@+id/textView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="#222222"
android:textSize="15sp"/>

</LinearLayout>

MainActivity

public class MainActivity extends AppCompatActivity {
TextView textView;
Soldier s1;//默认null

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
textView = findViewById(R.id.textView);
}

public void doClick(View view) {
switch (view.getId()) {
case R.id.button1:
f1();
break;
case R.id.button2:
f2();
break;
case R.id.button3:
f3();
break;
}
}

public void f1() {
s1 = new Soldier();
s1.id = 9527;
//用s1找到士兵对象内存空间
//访问它的属性变量id
textView.setText("士兵9527已创建" + "\n");
}

public void f2() {
s1.go(textView);
}

public void f3() {
s1.attack(textView);
}
}

运行程序:
在这里插入图片描述

举例说明:创建一个FlashLight类

我们来创建一个手电筒的类。它的属性有颜色、开光状态。它的方法有开灯、关灯

FlashLight.java

public class FlashLight {
//属性变量,成员变量
int color = Color.BLACK;
boolean on = false;

public void turnOn() {
on = true;
}

public void turnOff() {
on = false;
}
}

xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/layout"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:background="@android:color/black"
android:orientation="vertical">


<ToggleButton
android:id="@+id/toggleButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="doClick"
android:textOff="关"
android:textOn="开" />


<Button
android:id="@+id/button1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="doClick"
android:text="红" />


<Button
android:id="@+id/button2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="doClick"
android:text="白" />


<Button
android:id="@+id/button3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="doClick"
android:text="蓝" />


</LinearLayout>

MainActivity.java

public class MainActivity extends AppCompatActivity {
FlashLight flashLight = new FlashLight();
LinearLayout linearLayout;
ToggleButton toggleButton;

Button red;
Button white;
Button blue;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

linearLayout = findViewById(R.id.layout);
toggleButton = findViewById(R.id.toggleButton);
red = findViewById(R.id.button1);
white = findViewById(R.id.button2);
blue = findViewById(R.id.button3);
}

public void doClick(View view) {
switch (view.getId()) {
case R.id.button1:
f1();
break;
case R.id.button2:
f2();
break;
case R.id.button3:
f3();
break;
case R.id.toggleButton:
f4();
break;
}
}

public void f1() {
//用light变量找到手电筒对象的内存空间地址
//访问它的color变量
flashLight.color = Color.RED;
show();
}

public void f2() {
flashLight.color = Color.WHITE;
show();
}

public void f3() {
flashLight.color = Color.BLUE;
show();
}

public void f4() {
//判断开关指示按钮状态
if (toggleButton.isChecked()) {
flashLight.turnOn();
} else {
flashLight.turnOff();
}
show();
}

public void show() {
//根据flashlight属性控制界面
if (flashLight.on) {
linearLayout.setBackgroundColor(flashLight.color);
} else {
linearLayout.setBackgroundColor(Color.BLACK);
}
}
}

运行程序:
在这里插入图片描述

举例说明:创建一个Car类

我们来创建一个汽车类,它的属性有颜色color、品牌brand、速度speed。它的方法有前进、停止。
Car.java

public class Car {
public String color;
public String brand;
public int speed;

public void go(TextView tv) {
tv.append("\n" + color + brand + "汽车以时速" + speed + "前进");
}

public void stop(TextView tv) {
tv.append("\n" + color + brand + "汽车停止");
}
}

xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">


<Button
android:id="@+id/button1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="doClick"
android:text="创建汽车" />


<Button
android:id="@+id/button2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="doClick"
android:text="go" />


<Button
android:id="@+id/button3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="doClick"
android:text="stop" />


<TextView
android:id="@+id/textView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="18sp"
android:textColor="#222222" />


</LinearLayout>

MainActivity

public class MainActivity extends AppCompatActivity {
Car car;

Button create;
Button go;
Button stop;
TextView textView;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

create = findViewById(R.id.button1);
go = findViewById(R.id.button2);
stop = findViewById(R.id.button3);
textView = findViewById(R.id.textView);
}

public void doClick(View view) {
switch (view.getId()) {
case R.id.button1:
f1();
break;
case R.id.button2:
f2();
break;
case R.id.button3:
f3();
break;
}
}

private void f1() {
car = new Car();
//默认值如下
//color="";
//brand="";
//speed=0;
car.color = "红色";
car.brand = "BMW";
car.speed = 80;
textView.setText("汽车已创建");
}

private void f2() {
car.go(textView);
}

private void f3() {
car.stop(textView);
}
}

运行程序:
在这里插入图片描述

类中包含的变量

一个类可以包含以下类型变量:
1、局部变量
在方法、构造方法或者语句块中定义的变量被称为局部变量。变量声明和初始化都是在方法中,方法结束后,变量就会自动销毁。
局部变量有以下特点:
①必须手动初始化(分配内存空间),没有默认值
②局部变量作用域,到它定义的代码块为止
③作用域内不能重复定义

void f1(){
int a = 10;
if(){
int a = 9;//这样是不允许的,作用域范围内重复定义了
print(a);
}
}
void f1(){
int a = 10;
if(){
print(a);
int b = 100;
}//此时b的作用范围已结束,可以再定义一个b,不是同一个
int b = 1000;
}

2、成员变量
成员变量是定义在类中,方法体之外的变量。这种变量在创建对象的时候实例化。成员变量可以被类中方法、构造方法和特定类的语句块访问。
成员变量有以下特点:
①定义在类中,自动初始化,有默认值

Int a;默认0
boolean b;默认false
int[] c;默认null

例如我们第一个例子 Soldier 类中,唯一编号 id,默认值就是 0。
②访问范围:类中都可以访问,根据访问范围设置,类外也可以访问

3、类变量
类变量也声明在类中,方法体之外,但必须声明为 static 类型。
关于关键字 static 后边的文章中会讲到


收起阅读 »

Android 渲染系列-绘制流程总览

前言谈到Android的渲染,可能会想到测量、布局、绘制三大流程。但我们的view到底是如何一步一步显示到屏幕的?App的CPU/GPU渲染到底是什么?OpenGL/Vulkan/skia是什么? surfaceFlinger和HAL又是什么呢?带着这些问题,...
继续阅读 »

前言

谈到Android的渲染,可能会想到测量、布局、绘制三大流程。但我们的view到底是如何一步一步显示到屏幕的?App的CPU/GPU渲染到底是什么?OpenGL/Vulkan/skia是什么? surfaceFlinger和HAL又是什么呢?

带着这些问题,我们今天就深入的去学习Android绘制的整个流程吧。

参考分层思想,我们大概把整个渲染分为App层和SurfaceFlinger层,先讲各层都做什么工作,后面在把二者联系起来。

相关概念

  1. Vsync信号

由系统设备产生。假设在60HZ的屏幕上,屏幕就会每16ms进行一次扫描,在两次扫描中间会有一个间隔,此时系统就会发出Vsync信号,来通知APP(Vsync-app)进行渲染,SurfaceFlinger(Vsync-sf)来进行swap缓冲区进行展示。因此,只要App的渲染过程(CPU计算+GPU绘制)不超过16ms,画面就会显得很流畅。

说明:

  • 如果系统检测到硬件支持,则Vysnc信号由硬件产生,否则就由软件模拟产生。这个了解即可。
  • Vsync offset机制: Vsync-app、Vsync-sf并不是同时通知的,Vsync-sf会相对晚些,但对于我们App开发者来说,即可认为约等于同时发生。
  1. OpenGL、Vulkan、skia的关系
  • OpenGL: 是一种跨平台的3D图形绘制规范接口。OpenGL EL则是专门针对嵌入式设备,如手机做了优化。
  • Vulkan: 跟OpenGL相同功能,不过它同时支持3D、2D,比OpenGL更加的轻量、性能更高。
  • skia: skia是图像渲染库,2D图形绘制自己就能完成。3D效果(依赖硬件)由OpenGL、Vulkan、Metal支持。它不仅支持2D、3D,同时支持CPU软件绘制和GPU硬件加速。Android、flutter都是使用它来完成绘制。
  1. GPU和OpenGL什么关系

OpenGL是规范,GPU就是该规范具体的设备实现者。

  1. Surface 与 graphic buffer

插入一个问题: 一个Android程序有多少个window??

应用本身+系统(menu+statusBar)+dialog+toast+popupWindow。

Android的一个window对应一个surface(请注意: 一个surface不一定对应window。如surfaceView), 一个surface对应一个BufferQueue。 进程可以同时拥有多个surface,如使用surfaceView(封装了单独的surface,且在单独的子线程操作)。

canvas 是通过surface.lockCnavas得到(最终调用JNI的framework层的surface.lock方法获取graphic buffer)。

surface通过dequeue拿到graphic buffer,然后进行渲染绘制,渲染完成后回到BufferQueu队列,最后通知surfaceFlinger来消费。

  1. SurfaceFlinger 是什么?

可以认为它是协调缓冲区数据和设备显示的协调者。 Vsync信号、三倍缓冲、缓冲区的合成操作都是由它来控制。

1 Android渲染演变

了解Android系统对渲染的不断优化历史,对于理解渲染很有帮助。

  • Android 4.1

引入了project butter黄油计划:Vsync、三倍缓冲、choreography编舞者。

  • android 5.0

引入了RenderThread线程(该线程是系统在framework层维护),把之前CPU直接操作绘制指令(OpenGL/vulkan/skia)部分,交给了单独的渲染线程。减少主线程的工作。即使主线程卡住,渲染也不受影响。

  • Android7.0 引入了Vulkan支持。 OpenGL是3D渲染API,VulKan是用来替换OpenGL的。它不仅支持3D,也支持2D,同时更加轻量级。

2 App做了什么(重点)

从invalidate()(它会在onVsync信号来的时候(也就是下一帧)触发onDraw()方法)方法调用开始来分析整个过程。Activity的显示最终会调用requestLayout方法,关于Activity的启动过程可以自行查阅(后续有空在写篇文章~)。

invalidate()让 drawing cache(绘制缓存)无效,也就是所谓的标脏,所以才会要重新进行绘制。 requestLayout()方法跟invalidate()调用的是同一个方法:scheduleTraversals(),如下:

ViewRootImpl.java

void invalidate() {
mDirty.set(0, 0, mWidth, mHeight);
if (!mWillDrawSoon) {
scheduleTraversals();
}
}
@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
scheduleTraversals();
}
}
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
if (!mUnbufferedInputDispatch) {
scheduleConsumeBatchedInput();
}
notifyRendererOfFramePending();
pokeDrawLockIfNeeded();
}
}

viewRootImpl的invalidate()方法会postCalback到choreography。

choreography是在viewRootImpl创建的的时注册了监听系统的vsync信号。

当onVsync回调下一帧的时候,就会执行choreography的doFrame()方法,然后执行callback,调用 viewRootImpl的performTraversal()--doTraversal()方法,从而执行onMeasure()、onLayout()、onDraw()三大流程。

那UI线程如何与RenderThread交互呢? 什么时候把绘制好的数据交给SurfaceFlinger呢?

image.png

onMeasure()、onLayout()计算出view的大小和摆放的位置,这都是UI线程要做的事情。

  1. 在draw()方法中进行绘制,但此时是没有真正去绘制。而是把绘制的指令封装为displayList,进一步封装为RendNode,在同步给RenderThread。
  2. RenderThread通过 dequeue() 拿到graphic buffer(surfaceFlinger的缓冲区),根据绘制指令直接操作OpenGL的绘制接口,最终通过GPU设备把绘制指令渲染到了离屏缓冲区graphic buffer。
  3. 完成渲染后,把缓冲区交还给SurfaceFlinger的BufferQueue。SurfaceFlinger会通过硬件设备进行layer的合成,最终展示到屏幕。

image.png

以上流程也体现了生产者与消费者模式:

image.png

生产者: APP,再深入点就是canvas->surface。

消费者:SurfaceFlinger

BufferQueue 的大小一般是3。

  • 一块缓冲区用来被SurfaceFlinger交由设备展示
  • 一块用来App绘制缓冲数据
  • 还有一块,如果App绘制超过一帧时间16ms的时候,当下一帧vsync到来,其中两块都已经被占用,所以要用到第三块,避免此次vsync信号CPU和GPU处于空闲(因为如果空闲的话,下下帧就会出现jank)。

SurfaceFlinger 做了什么

SurfaceFlinger是显示合成系统。在应用程序请求创建surface的时候,SurfaceFlinger会创建一个Layer。Layer是SurfaceFlinger操作合成的基本单元。所以,一个surface对应一个Layer。

当应用程序把绘制好的GraphicBuffer数据放入BufferQueue后,接下来的工作就是SurfaceFlinger来完成了。

image.png

说明:

系统会有多个应用程序,一个程序有多个BufferQueue队列。SurfaceFlinger就是用来决定何时以及怎么去管理和显示这些队列的。

SurfaceFlinger请求HAL硬件层,来决定这些Buffer是硬件来合成还是自己通过OpenGL来合成。

最终把合成后的buffer数据,展示在屏幕上。

最后,附上官方完整渲染架构:

image.png

收起阅读 »

Android Activity通讯方式

Android Activity通讯方式Activity 之间传递信息是很常见的方式,比如页面的跳转需要携带信息,比如第一个页面的参数需要到第二个页面显示,Android中对这种传值通讯提供了多种方式,这些方式又有什么异同呢。一、Bundle传递含义:把数据封...
继续阅读 »

Android Activity通讯方式

Activity 之间传递信息是很常见的方式,比如页面的跳转需要携带信息,比如第一个页面的参数需要到第二个页面显示,Android中对这种传值通讯提供了多种方式,这些方式又有什么异同呢。

一、Bundle传递

含义:把数据封装在Bundle 对象中,通过 Intent 跳转时携带。

伪代码

传递基本数据类型和String类型


// 传递
Bundle bundle = new Bundle();
bundle.putString("name", "Jack");
bundle.putInt("id", 1);

Intent intent = new Intent(this, MainActivity2.class);
intent.putExtras(bundle);
startActivity(intent);

// 接收
Bundle bundle = getIntent().getExtras();
String name = bundle.getString("name");
int id = bundle.getInt("id");

Log.d("===", "name:" + name + " _ id:" + id);

传递对象

对象传递前需要对对象进行序列化,否则会报错。

需要注意的是

通过序列化传递的对象,传输和接收的对象虽然内容相同,但是引用地址是不同的。 也就是说改了接收的对象改了值,原始传递页面的对象不受影响

  • 何为序列化?

序列化是为了将对象数据转换成字节流的形式,方便进行传输。 所以在传递的对象的时候我们需要进行序列化,在接收端再进行反序列化就可以恢复对象。

  • 如何实现序列化

Serializable 和 Parcelable

Serializable: 实体类直接实现Serializable接口


import java.io.Serializable;
public class Student implements Serializable {

private int id;
private String name;

public Student() {
}

public Student(int id, String name) {
this.id = id;
this.name = name;
}

public int getId() {
return id;
}

public void setId(int id) {
this.id = id;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}
}


// 发送
Student student = new Student(1, "Jack");
Bundle bundle = new Bundle();bundle.putSerializable("student", student);
Intent intent = new Intent(this, MainActivity2.class);
intent.putExtras(bundle);startActivity(intent);


// 接收
Bundle bundle = getIntent().getExtras();
Student student = (Student) bundle.getSerializable("student");
Log.d("===", "person:"+student.getName());

Parcelable: 实现接口并且实现方法

import android.os.Parcel;
import android.os.Parcelable;

public class Student implements Parcelable {

private int id;
private String name;

public Student() {
}

public Student(int id, String name) {
this.id = id;
this.name = name;
}

protected Student(Parcel in) {
id = in.readInt();
name = in.readString();
}

public static final Creator<Student> CREATOR = new Creator<Student>() {
@Override
public Student createFromParcel(Parcel in) {
return new Student(in);
}

@Override
public Student[] newArray(int size) {
return new Student[size];
}
};

public int getId() {
return id;
}

public void setId(int id) {
this.id = id;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

@Override
public int describeContents() {
return 0;
}

@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(id);
dest.writeString(name);
}
}


// 发送
Student student = new Student(1, "Jack");
Bundle bundle = new Bundle();
bundle.putParcelable("student", student);
Intent intent = new Intent(this, MainActivity2.class);
intent.putExtras(bundle);
startActivity(intent);

// 接收
Bundle bundle = getIntent().getExtras();
Student student = (Student) bundle.getParcelable("student");
Log.d("===", "person:"+student.getName());

从代码看来,Serializable 来自Java,而Parcelable 来自Android,Parcelable 是Android优化过后的产物,在相同条件下,Parcelable 可以减少很大的内存占用。

二、广播传递

广播是Android 中的四大组件之一,相当于一个全局监听器,可以监听其它App或者系统的广播消息。

在Activity之间虽然也可以传递数据,但是有点大材小用。

三、外部存储

如果有大量的数据,在 A Activity 中可以将数据临时保存在存储卡中,跳转到B Activity 后再从存储卡中取出。

四、静态变量

将数据保存在静态变量中,在 A Activity 中对静态变量进行赋值,跳转到B Activity 后从静态变量获取数据,然后回收静态变量。

五、Application 中转

自定义的application类,临时保存变量,为了代码整洁,一般不用。

六、ARouter

Arouter 也有一些传递消息的方法。比如 withObject、withString 等

看看它们的内部方法

/**
* Set object value, the value will be convert to string by 'Fastjson'
*
* @param key a String, or null
* @param value a Object, or null
* @return current
*/
public Postcard withObject(@Nullable String key, @Nullable Object value) {
serializationService = ARouter.getInstance().navigation(SerializationService.class);
mBundle.putString(key, serializationService.object2Json(value));
return this;
}


/**
* Inserts a String value into the mapping of this Bundle, replacing
* any existing value for the given key. Either key or value may be null.
*
* @param key a String, or null
* @param value a String, or null
* @return current
*/
public Postcard withString(@Nullable String key, @Nullable String value) {
mBundle.putString(key, value);
return this;
}


同样是用Bundle传输,原理和Bundle一致。

七、EventBus

在使用Bundle传递数据时,当数据量过大(超过1M时),就会抛出 TransactionTooLargeException 异常。

使用 EventBus 可以很轻松的解决这个问题。

1. EventBus简介

EventBus是一种用于Android的事件发布-订阅总线,由GreenRobot开发,可以很方便的进行数据传递。

2. EventBus 三个组成部分

  • Event:事件,它可以是任意类型,EventBus会根据事件类型进行全局的通知。
  • Subscriber:事件订阅者,在EventBus 3.0之前我们必须定义以onEvent开头的那几个方法,分别是onEvent、onEventMainThread、onEventBackgroundThread和onEventAsync,而在3.0之后事件处理的方法名可以随意取,不过需要加上注解@subscribe,并且指定线程模型,默认是POSTING。
  • Publisher:事件的发布者,可以在任意线程里发布事件。一般情况下,使用EventBus.getDefault()就可以得到一个EventBus对象,然后再调用post(Object)方法即可。

3. EventBus 四种线程模型

  • POSTING:默认,表示事件处理函数的线程跟发布事件的线程在同一个线程。
  • MAIN:表示事件处理函数的线程在主线程(UI)线程,因此在这里不能进行耗时操作。
  • BACKGROUND:表示事件处理函数的线程在后台线程,因此不能进行UI操作。如果发布事件的线程是主线程(UI线程),那么事件处理函数将会开启一个后台线程,如果果发布事件的线程是在后台线程,那么事件处理函数就使用该线程。
  • ASYNC:表示无论事件发布的线程是哪一个,事件处理函数始终会新建一个子线程运行,同样不能进行UI操作。

4. EventBus 实战

4.1 引入依赖

implementation 'org.greenrobot:eventbus:3.1.1'

4.2 新建一个实体类,作为传递的对象

public class MessageInfo {

    private String message;

    public MessageInfo(String message) {
        this.message = message;
    }

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }
}

4.3 定义接收事件

Activity:

public class MainActivity extends AppCompatActivity {
  
  private TextView tv;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        tv = findViewById(R.id.tv);

        EventBus.getDefault().register(this); //初始化EventBus
    }


    @Override
    protected void onDestroy() {
        super.onDestroy();

        EventBus.getDefault().unregister(this); //释放
    }


    // 定义接收的事件
    @Subscribe(threadMode = ThreadMode.MAIN)
    public void getMessage(MessageInfo messageInfo) {
        tv.setText(messageInfo.getMessage());
        Toast.makeText(this, "接收到的消息为:" + messageInfo.getMessage(), Toast.LENGTH_SHORT).show();
    }


    public void GoMain2Activity(View view) {
        Intent intent = new Intent(this, Main2Activity.class);
        startActivity(intent);
    }
}

layout:

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:onClick="GoMain2Activity"
        android:text="Hello World!"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />


    <TextView
        android:id="@+id/tv"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text=""/>

</android.support.constraint.ConstraintLayout>

4.4 定义发送事件

Activity:

public class Main2Activity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main2);
    }


    /**
     * 发送消息
     *
     * @param view
     */
    public void publishMessage(View view) {
        EventBus.getDefault().post(new MessageInfo("小李子"));
    }
}

layout:

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".Main2Activity">

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello World!"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        android:onClick="publishMessage"/>

</android.support.constraint.ConstraintLayout>

4.5 粘性事件

所谓的黏性事件,就是指发送了该事件之后再订阅者依然能够接收到的事件。使用黏性事件的时候有两个地方需要做些修改。一个是订阅事件的地方,这里我们在先打开的Activity中注册监听黏性事件: 添加 sticky = true 属性


// 定义接收的事件
    @Subscribe(threadMode = ThreadMode.MAIN, sticky = true)
    public void getMessage(MessageInfo messageInfo) {
        tv.setText(messageInfo.getMessage());
        Toast.makeText(this, "接收到的消息为:" + messageInfo.getMessage(), Toast.LENGTH_SHORT).show();
    }

在发送事件时使用postSticky来发送:

  /**
     * 发送消息
     *
     * @param view
     */
    public void publishMessage(View view) {
        EventBus.getDefault().postSticky(new MessageInfo("小李子"));
        Intent intent = new Intent(this, MainActivity.class);
        startActivity(intent);
    }


4.6 优先级

在Subscribe注解中总共有3个参数,上面我们用到了其中的两个,这里我们使用以下第三个参数,即priority。它用来指定订阅方法的优先级,是一个整数类型的值,默认是0,值越大表示优先级越大。在某个事件被发布出来的时候,优先级较高的订阅方法会首先接受到事件。 这里有几个地方需要注意:

  • 只有当两个订阅方法使用相同的ThreadMode参数的时候,它们的优先级才会与priority指定的值一致;
  • 只有当某个订阅方法的ThreadMode参数为POSTING的时候,它才能停止该事件的继续分发。

八、EventBus 问题汇总(持续更新...)

问题一、EventBus 粘性事件接收不到的问题

1、起因

由于EventBus发送的是对象,我们经常构建一个共用的对象,在共用对象中添加tag,用于方便在接收中区分作用。

例如用EventBusHelper工具类发送EventBusMessage对象。

2、遇到的问题

在发送粘性事件时,在A场景发送了一次,然后还没有接收,然后在B场景又发送了一次,这时B发送的粘性事件可以收到,而A场景的粘性事件被替换掉了。

这时因为在EventBus的源码中,粘性事件使用Map集合存储,key为 object.getClass(), 当我们自定义EventBusMessage的时候,导致object.getClass()一直是相同的,以至于会替换前一次的key。

    public void postSticky(Object event) {
synchronized (stickyEvents) {
stickyEvents.put(event.getClass(), event);
}
// Should be posted after it is putted, in case the subscriber wants to remove immediately
post(event);
}

3、解决方法

创造多个单独的对象,用于发送粘性服务。

上面的方法比较Low,还没有想到比较优雅的方法,有的话分享出来吧。

问题二、Post/postSticky 不能传递基本类型。

eg: 使用post/postSticky直接传递,在Subscribe处接收int,此时收不到消息。 将int tag改为 Integer tag 就能收到。

这时因为EventBus中要求传递的为Object,Object属于类类型,也就是复合数据类型,int属于简单数据类型。

 EventBus.getDefault().post(111);

@Subscribe(threadMode = ThreadMode.MAIN)
public void getMessage(int tag) {
if (tag == 111)
Log.d(TAG, "received");
}



收起阅读 »

关于 PendingIntent 您需要知道的那些事

PendingIntent 是 Android 框架中非常重要的组成部分,但是目前大多数与该主题相关的开发者资源更关注它的实现细节,即 "PendingIntent 是由系统维护的 token 引用",而忽略了它的用途。由于 Android 12 对 Pend...
继续阅读 »

PendingIntent 是 Android 框架中非常重要的组成部分,但是目前大多数与该主题相关的开发者资源更关注它的实现细节,即 "PendingIntent 是由系统维护的 token 引用",而忽略了它的用途。

由于 Android 12 对 PendingIntent 进行了 重要更新,包括需要显式确定 PendingIntent 是否是可变的,所以我认为有必要和大家深入聊聊 PendingIntent 有什么作用,系统如何使用它,以及为什么您会需要可变类型的 PendingIntent。

PendingIntent 是什么?

PendingIntent 对象封装了 Intent 对象的功能,同时以您应用的名义指定其他应用允许哪些操作的执行,来响应用户未来会进行的操作。比如,所封装的 Intent 可能会在闹铃关闭后或者用户点击通知时被触发。

PendingIntent 的关键点是其他应用在触发 intent 时是 以您应用的名义。换而言之,其他应用会使用您应用的身份来触发 intent。

为了让 PendingIntent 具备和普通 Intent 一样的功能,系统会使用创建 PendingIntent 时的身份来触发它。在大多数情况下,比如闹铃和通知,其中所用到的身份就是应用本身。

我们来看应用中使用 PendingIntent 的不同方式,以及我们使用这些方式的原因。

常规用法

使用 PendingIntent 最常规最基础的用法是作为关联某个通知所进行的操作。

val intent = Intent(applicationContext, MainActivity::class.java).apply {
action = NOTIFICATION_ACTION
data = deepLink
}
val pendingIntent = PendingIntent.getActivity(
applicationContext,
NOTIFICATION_REQUEST_CODE,
intent,
PendingIntent.FLAG_IMMUTABLE
)
val notification = NotificationCompat.Builder(
applicationContext,
NOTIFICATION_CHANNEL
).apply {
// ...
setContentIntent(pendingIntent)
// ...
}.build()
notificationManager.notify(
NOTIFICATION_TAG,
NOTIFICATION_ID,
notification
)

可以看到我们构建了一个标准类型的 Intent 来打开我们的应用,然后,在添加到通知之前简单用 PendingIntent 封装了一下。

在本例中,由于我们明确知道未来需要进行的操作,所以我们使用 FLAG_IMMUTABLE 标记构建了无法被修改的 PendingIntent

调用 NotificationManagerCompat.notify() 之后工作就完成了。当系统显示通知,且用户点击通知时,会在我们的 PendingIntent 上调用 PendingIntent.send(),来启动我们的应用。

更新不可变的 PendingIntent

您也许会认为如果应用需要更新 PendingIntent,那么它需要是可变类型,但其实并不是。应用所创建的 PendingIntent 可通过 FLAG_UPDATE_CURRENT 标记来更新。

val updatedIntent = Intent(applicationContext, MainActivity::class.java).apply {
action = NOTIFICATION_ACTION
data = differentDeepLink
}

// 由于我们使用了 FLAG_UPDATE_CURRENT 标记,所以这里可以更新我们在上面创建的
// PendingIntent
val updatedPendingIntent = PendingIntent.getActivity(
applicationContext,
NOTIFICATION_REQUEST_CODE,
updatedIntent,
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
)
// 该 PendingIntent 已被更新

在接下来的内容中我们会解释为什么将 PendingIntent 设置为可变类型。

跨应用 API

通常的用法并不局限于与系统交互。虽然在某些操作后使用 startActivityForResult() 和 onActivityResult() 来 接收回调 是非常常见的用法,但它并不是唯一用法。

想象一下一个线上订购应用提供了 API 使其他应用可以集成。当 Intent 启动了订购食物的流程后,应用可以 Intent 的 extra 的方式访问 PendingIntent。一旦订单完成传递,订购应用仅需启动一次 PendingIntent

在本例中,订购应用使用了 PendingIntent 而没有直接发送 activity 结果,因为订单可能需要更长时间进行提交,而让用户在这个过程中等待是不合理的。

我们希望创建一个不可变的 PendingIntent,因为我们不希望线上订购应用修改我们的 Intent。当订单生效时,我们仅希望其他应用发送它,并保持它本身不变。

可变 PendingIntent

但是如果我们作为订购应用的开发者,希望添加一个特性可以允许用户回送消息至调用订购功能的应用呢?比如可以让调用的应用提示,"现在是披萨时间!"

要实现这样的效果就需要使用可变的 PendingIntent 了。

既然 PendingIntent 本质上是 Intent 的封装,有人可能会想可以通过一个 PendingIntent.getIntent() 方法来获得其中所封装的 Intent。但是答案是不可以的。那么该如何实现呢?

PendingIntent 中除了不含任何参数的 send() 方法之外,还有其他 send 方法的版本,包括这个可以接受 Intent 作为参数的 版本:

fun PendingIntent.send(
context: Context!,
code: Int,
intent: Intent?
)

这里的 Intent 参数并不会替换 PendingIntent 所封装的 Intent,而是通过 PendingIntent 在创建时所封装的 Intent 来填充参数。

我们来看下面的例子。

val orderDeliveredIntent = Intent(applicationContext, OrderDeliveredActivity::class.java).apply {
action = ACTION_ORDER_DELIVERED
}
val mutablePendingIntent = PendingIntent.getActivity(
applicationContext,
NOTIFICATION_REQUEST_CODE,
orderDeliveredIntent,
PendingIntent.FLAG_MUTABLE
)

这里的 PendingIntent 会被传递到我们的线上订购应用。当传递完成后,应用可以得到一个 customerMessage,并将其作为 intent 的 extra 回传,如下示例所示:

val intentWithExtrasToFill = Intent().apply {
putExtra(EXTRA_CUSTOMER_MESSAGE, customerMessage)
}
mutablePendingIntent.send(
applicationContext,
PENDING_INTENT_CODE,
intentWithExtrasToFill
)

调用端的应用会在它的 Intent 中得到 EXTRA_CUSTOMER_MESSAGE extra,并显示消息。

声明可变的 PendingIntent 时需要特别注意的事

⚠️当创建可变的 PendingIntent 时,始终 显式设置要启动的 Intent 的 component。可以通过我们上面的实现方式操作,即显式设置要接收的准确类名,不过也可以通过 Intent.setComponent() 实现。

您的应用可能会在某些场景下调用 Intent.setPackage() 来实现更方便。但是请特别注意这样的做法有可能会 匹配到多个 component。如果可以的话,最好指定特定的 component。

⚠️如果您尝试覆写使用 FLAG_IMMUTABLE 创建的 PendingIntent 中的值,那么该操作会失败且没有任何提示,并传递原始封装未修改的 Intent

请记住应用总是可以更新自身的 PendingIntent,即使是不可变类型。使 PendingIntent 成为可变类型的唯一原因是其他应用需要通过某种方式更新其中封装的 Intent

关于标记的详情

我们上面介绍了少数几个可用于创建 PendingIntent 的标记,还有一些标记也为大家介绍一下。

FLAG_IMMUTABLE: 表示其他应用通过 PendingIntent.send() 发送到 PendingIntent 中的 Intent 无法被修改。应用总是可以使用 FLAG_UPDATE_CURRENT 标记来修改它自己的 PendingIntent。

在 Android 12 之前的系统中,不带有该标记创建的 PendingIntent 默认是可变类型。

⚠️ Android 6 (API 23) 之前的系统中,PendingIntent 都是可变类型。

🆕FLAG_MUTABLE: 表示由 PendingIntent.send() 传入的 intent 内容可以被应用合并到 PendingIntent 中的 Intent。

⚠️ 对于任何可变类型的 PendingIntent,始终 设置其中所封装的 Intent 的 ComponentName。如果未采取该操作的话可能会造成安全隐患。

该标记是在 Android 12 版本中加入。Android 12 之前的版本中,任何未指定 FLAG_IMMUTABLE标记所创建的 PendingIntent 都是隐式可变类型。

FLAG_UPDATE_CURRENT: 向系统发起请求,使用新的 extra 数据更新已有的 PendingIntent,而不是保存新的 PendingIntent。如果 PendingIntent 未注册,则进行注册。

FLAG_ONE_SHOT: 仅允许 PendingIntent (通过 PendingIntent.send()) 被发送一次。对于传递 PendingIntent 时,其内部的 Intent 仅能被发送一次的场景就非常重要了。该机制可能便于操作,或者可以避免应用多次执行某项操作。

🔐 使用 FLAG_ONE_SHOT 来避免类似 "重放攻击" 的问题。

FLAG_CANCEL_CURRENT: 在注册新的 PendingIntent 之前,取消已存在的某个 PendingIntent。该标记用于当某个 PendingIntent 被发送到某应用,然后您希望将它转发到另一个应用,并更新其中的数据。使用 FLAG_CANCEL_CURRENT 之后,之前的应用将无法再调用 send 方法,而之后的应用可以调用。

接收 PendingIntent

有些情况下系统或者其他框架会将 PendingIntent 作为 API 调用的返回值。举一个典型例子是方法 MediaStore.createWriteRequest(),它是在 Android 11 中新增的。

static fun MediaStore.createWriteRequest(
resolver: ContentResolver,
uris: MutableCollection<Uri>
): PendingIntent

正如我们应用创建的 PendingIntent 一样,它是以我们应用的身份运行,而系统创建的 PendingIntent,它是以系统的身份运行。具体到这里 API 的使用场景,它允许应用打开 Activity 并赋予我们的应用 Uri 集合的写权限。

总结

我们在本文中介绍了 PendingIntent 如何作为 Intent 的封装使系统或者其他应用能够在未来某一时间以某个应用的身份启动该应用所创建的 Intent。

我们还介绍了 PendingIntent 为何需要设置为不可变,以及这么做并不会影响应用修改自身所创建的 PendingIntent 对象。可以通过 FLAG_UPDATE_CURRENT 标记加上 FLAG_IMMUTABLE 来实现该操作。

我们还介绍了如果 PendingIntent 是可变的,需要做的预防措施 — 保证对封装的 Intent 设置 ComponentName

最后,我们介绍了有时系统或者框架如何向应用提供 PendingIntent,以便我们能够决定如何并且何时运行它们。

收起阅读 »

Android工程Gradle构建-笔记

1、统一版本库管理1.1、统一版本号管理创建一个gradle文件统一管理 不同module下的第三方库和其他属性的配置参数 如下,在项目根目录创建config.gradleext { COMPILE_SDK = 30 APPLICATION_ID ...
继续阅读 »

1、统一版本库管理

1.1、统一版本号管理

创建一个gradle文件统一管理 不同module下的第三方库和其他属性的配置参数 如下,在项目根目录创建config.gradle

ext {
COMPILE_SDK = 30
APPLICATION_ID = "com.chenyangqi.gradle"
MIN_SDK = 19
TARGET_SDK = 30
VERSION_CODE = 1
VERSION_NAME = "1.0.0"

JVM_TARGET = '1.8'

MULTIDEX = 'androidx.multidex:multidex:2.0.1'
CORE_KTX = 'androidx.core:core-ktx:1.3.2'
APPCOMPAT = 'androidx.appcompat:appcompat:1.2.0'
ANDROID_MATERIAL = 'com.google.android.material:material:1.3.0'
CONSTRAINTLAYOUT = 'androidx.constraintlayout:constraintlayout:2.1.0'
TEST_JUNIT = 'junit:junit:4.+'
ANDROID_EXT_JUNIT = 'androidx.test.ext:junit:1.1.2'
ANDROID_TEST_ESPRESSO = 'androidx.test.espresso:espresso-core:3.3.0'
}

然后在项目的gradle中引用config.gradle

apply from: project.file('config.gradle')

1.2、local.perporties使用场景

一些不便对外展示的敏感参数可以定义在local.properties中,如一些付费库的key,maven仓库的用户名和密码等

sdk.dir=/Users/mutou/Library/Android/sdk
username=chenyangqi
password=123456

编译时通过如下方式获取local.properties中定义的属性

Properties properties = new Properties()
properties.load(project.rootProject.file("local.properties").newDataInputStream())
def username = properties.getProperty('username')
def password = properties.getProperty('password')

在运行时获得Local.properties属性要借助BuildConfig

def getUsername() {
Properties properties = new Properties()
properties.load(project.rootProject.file('local.properties').newDataInputStream())
return properties.getProperty("username");
}

android {
defaultConfig {
buildConfigField "String", "USERNAME", "\""+getUsername()+"\""
}
}

1.3、版本冲突处理

出现依赖冲突时可通过gradle的dependencies查看依赖树,定位冲突位置 ,比如我要查看的Build Variants为oppoNormalProdRelease,命令如下

./gradlew :app:dependencies --configuration oppoNormalProdReleaseCompileClasspath

依赖树如下

mutou@chenyangqi GradleDemo % ./gradlew :app:dependencies --configuration oppoNormalProdReleaseCompileClasspath
...
oppoNormalProdReleaseCompileClasspath - Compile classpath for compilation 'oppoNormalProdRelease' (target (androidJvm)).
+--- org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.5.21
| +--- org.jetbrains.kotlin:kotlin-stdlib:1.5.21
| | +--- org.jetbrains:annotations:13.0
| | \--- org.jetbrains.kotlin:kotlin-stdlib-common:1.5.21
| \--- org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.5.21
| \--- org.jetbrains.kotlin:kotlin-stdlib:1.5.21 (*)
+--- androidx.multidex:multidex:2.0.1
+--- androidx.core:core-ktx:1.3.2
| +--- org.jetbrains.kotlin:kotlin-stdlib:1.3.71 -> 1.5.21 (*)
| +--- androidx.annotation:annotation:1.1.0
| \--- androidx.core:core:1.3.2
| +--- androidx.annotation:annotation:1.1.0
| +--- androidx.lifecycle:lifecycle-runtime:2.0.0 -> 2.1.0
| | +--- androidx.lifecycle:lifecycle-common:2.1.0
| | | \--- androidx.annotation:annotation:1.1.0
| | +--- androidx.arch.core:core-common:2.1.0
| | | \--- androidx.annotation:annotation:1.1.0
| | \--- androidx.annotation:annotation:1.1.0
| \--- androidx.versionedparcelable:versionedparcelable:1.1.0
| +--- androidx.annotation:annotation:1.1.0
| \--- androidx.collection:collection:1.0.0 -> 1.1.0
| \--- androidx.annotation:annotation:1.1.0
+--- androidx.appcompat:appcompat:1.2.0
| +--- androidx.annotation:annotation:1.1.0
| +--- androidx.core:core:1.3.0 -> 1.3.2 (*)
| +--- androidx.cursoradapter:cursoradapter:1.0.0
| | \--- androidx.annotation:annotation:1.0.0 -> 1.1.0
| +--- androidx.fragment:fragment:1.1.0
| | +--- androidx.annotation:annotation:1.1.0
| | +--- androidx.core:core:1.1.0 -> 1.3.2 (*)
| | +--- androidx.collection:collection:1.1.0 (*)
| | +--- androidx.viewpager:viewpager:1.0.0
| | | +--- androidx.annotation:annotation:1.0.0 -> 1.1.0
| | | +--- androidx.core:core:1.0.0 -> 1.3.2 (*)
| | | \--- androidx.customview:customview:1.0.0
| | | +--- androidx.annotation:annotation:1.0.0 -> 1.1.0
| | | \--- androidx.core:core:1.0.0 -> 1.3.2 (*)
| | +--- androidx.loader:loader:1.0.0
| | | +--- androidx.annotation:annotation:1.0.0 -> 1.1.0
| | | +--- androidx.core:core:1.0.0 -> 1.3.2 (*)
| | | +--- androidx.lifecycle:lifecycle-livedata:2.0.0
| | | | +--- androidx.arch.core:core-runtime:2.0.0
| | | | | +--- androidx.annotation:annotation:1.0.0 -> 1.1.0
| | | | | \--- androidx.arch.core:core-common:2.0.0 -> 2.1.0 (*)
| | | | +--- androidx.lifecycle:lifecycle-livedata-core:2.0.0
| | | | | +--- androidx.lifecycle:lifecycle-common:2.0.0 -> 2.1.0 (*)
| | | | | +--- androidx.arch.core:core-common:2.0.0 -> 2.1.0 (*)
| | | | | \--- androidx.arch.core:core-runtime:2.0.0 (*)
| | | | \--- androidx.arch.core:core-common:2.0.0 -> 2.1.0 (*)
| | | \--- androidx.lifecycle:lifecycle-viewmodel:2.0.0 -> 2.1.0
| | | \--- androidx.annotation:annotation:1.1.0
| | +--- androidx.activity:activity:1.0.0
| | | +--- androidx.annotation:annotation:1.1.0
| | | +--- androidx.core:core:1.1.0 -> 1.3.2 (*)
| | | +--- androidx.lifecycle:lifecycle-runtime:2.1.0 (*)
| | | +--- androidx.lifecycle:lifecycle-viewmodel:2.1.0 (*)
| | | \--- androidx.savedstate:savedstate:1.0.0
| | | +--- androidx.annotation:annotation:1.1.0
| | | +--- androidx.arch.core:core-common:2.0.1 -> 2.1.0 (*)
| | | \--- androidx.lifecycle:lifecycle-common:2.0.0 -> 2.1.0 (*)
| | \--- androidx.lifecycle:lifecycle-viewmodel:2.0.0 -> 2.1.0 (*)
| +--- androidx.appcompat:appcompat-resources:1.2.0
| | +--- androidx.annotation:annotation:1.1.0
| | +--- androidx.core:core:1.0.1 -> 1.3.2 (*)
| | +--- androidx.vectordrawable:vectordrawable:1.1.0
| | | +--- androidx.annotation:annotation:1.1.0
| | | +--- androidx.core:core:1.1.0 -> 1.3.2 (*)
| | | \--- androidx.collection:collection:1.1.0 (*)
| | \--- androidx.vectordrawable:vectordrawable-animated:1.1.0
| | +--- androidx.vectordrawable:vectordrawable:1.1.0 (*)
| | +--- androidx.interpolator:interpolator:1.0.0
| | | \--- androidx.annotation:annotation:1.0.0 -> 1.1.0
| | \--- androidx.collection:collection:1.1.0 (*)
| \--- androidx.drawerlayout:drawerlayout:1.0.0
| +--- androidx.annotation:annotation:1.0.0 -> 1.1.0
| +--- androidx.core:core:1.0.0 -> 1.3.2 (*)
| \--- androidx.customview:customview:1.0.0 (*)
+--- com.google.android.material:material:1.3.0
| +--- androidx.annotation:annotation:1.0.1 -> 1.1.0
| +--- androidx.appcompat:appcompat:1.1.0 -> 1.2.0 (*)
| +--- androidx.cardview:cardview:1.0.0
| | \--- androidx.annotation:annotation:1.0.0 -> 1.1.0
| +--- androidx.coordinatorlayout:coordinatorlayout:1.1.0
| | +--- androidx.annotation:annotation:1.1.0
| | +--- androidx.core:core:1.1.0 -> 1.3.2 (*)
| | +--- androidx.customview:customview:1.0.0 (*)
| | \--- androidx.collection:collection:1.0.0 -> 1.1.0 (*)
| +--- androidx.constraintlayout:constraintlayout:2.0.1 -> 2.1.0
| +--- androidx.core:core:1.2.0 -> 1.3.2 (*)
| +--- androidx.dynamicanimation:dynamicanimation:1.0.0
| | +--- androidx.core:core:1.0.0 -> 1.3.2 (*)
| | +--- androidx.collection:collection:1.0.0 -> 1.1.0 (*)
| | \--- androidx.legacy:legacy-support-core-utils:1.0.0
| | +--- androidx.annotation:annotation:1.0.0 -> 1.1.0
| | +--- androidx.core:core:1.0.0 -> 1.3.2 (*)
| | +--- androidx.documentfile:documentfile:1.0.0
| | | \--- androidx.annotation:annotation:1.0.0 -> 1.1.0
| | +--- androidx.loader:loader:1.0.0 (*)
| | +--- androidx.localbroadcastmanager:localbroadcastmanager:1.0.0
| | | \--- androidx.annotation:annotation:1.0.0 -> 1.1.0
| | \--- androidx.print:print:1.0.0
| | \--- androidx.annotation:annotation:1.0.0 -> 1.1.0
| +--- androidx.annotation:annotation-experimental:1.0.0
| +--- androidx.fragment:fragment:1.0.0 -> 1.1.0 (*)
| +--- androidx.lifecycle:lifecycle-runtime:2.0.0 -> 2.1.0 (*)
| +--- androidx.recyclerview:recyclerview:1.0.0 -> 1.1.0
| | +--- androidx.annotation:annotation:1.1.0
| | +--- androidx.core:core:1.1.0 -> 1.3.2 (*)
| | +--- androidx.customview:customview:1.0.0 (*)
| | \--- androidx.collection:collection:1.0.0 -> 1.1.0 (*)
| +--- androidx.transition:transition:1.2.0
| | +--- androidx.annotation:annotation:1.1.0
| | +--- androidx.core:core:1.0.1 -> 1.3.2 (*)
| | \--- androidx.collection:collection:1.0.0 -> 1.1.0 (*)
| +--- androidx.vectordrawable:vectordrawable:1.1.0 (*)
| \--- androidx.viewpager2:viewpager2:1.0.0
| +--- androidx.annotation:annotation:1.1.0
| +--- androidx.fragment:fragment:1.1.0 (*)
| +--- androidx.recyclerview:recyclerview:1.1.0 (*)
| +--- androidx.core:core:1.1.0 -> 1.3.2 (*)
| \--- androidx.collection:collection:1.1.0 (*)
...
BUILD SUCCESSFUL in 1s

1.3.1、去除冲突依赖

当确定最终只保留的版本时,过滤掉其他版本,如下只保留ore:0.9.5.0

api("com.afollestad.material-dialogs:core:0.9.5.0") {
exclude group: 'com.android.support'
}

1.3.2、CompileOnly只编译不打包

当我们开发SDK时如果需要依赖第三方库,使用CompileOnly引用第三方库,而让使用SDK的开发者去决定选择哪个版本的第三方库,避免他人调用你的SDK时出现版本冲突

1.3.3、通过gradle脚本检测依赖库版本

待实现...

2、MultiDex分包

2.1、65535产生原因

默认情况下,Android工程代码编译后的.class文件会打包进一个dex中,dex中每一个方法会使用C++中的unsigned short类型的字段标记,unsigned short取值范围为0~65535,所以一旦方法数超过上限就会造成65536

2.2、分包

通过分包解决65535问题,根据minSdk版本不同分两种情况

2.2.1、minSdk>=21时分包

在app module下的build.gradle中设置multiDexEnabled=true即可

android {
compileSdk 30
defaultConfig {
applicationId "com.chenyangqi.gradle"
minSdk 21
targetSdk 30
versionCode 1
versionName "1.0"
multiDexEnabled true
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
...
}

2.2.2、minSdk<21时分包

minSdk小于21时除了设置multidexEnabled=true还要引用androidx.multidex:multidex:2.0.1这个Google提供的分包库

android {
compileSdk 30

defaultConfig {
applicationId "com.chenyangqi.gradle"
minSdk 19
targetSdk 30
versionCode 1
versionName "1.0"
multiDexEnabled true
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
...
}

dependencies {
implementation 'androidx.multidex:multidex:2.0.1'
...
}

然后再在application中继承分包库中的MultiDexApplication,又分三种情况

没有自定义Application时,直接在清单文件中注册name=MultiDexApplication

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.chenyangqi.gradle">

<application
android:name="androidx.multidex.MultiDexApplication"
...
</application>

</manifest>

有自定义Application时直接继承

class MyApplication:MultiDexApplication() {
}

当自定义Application已继承其他父类时重写attachBaseContext方法进行初始化

class MyApplication:OtherApplication() {

override fun onCreate() {
super.onCreate()
}

override fun attachBaseContext(base: Context?) {
super.attachBaseContext(base)
MultiDex.install(this)
}
}

3、代码混淆

3.1、开启混淆

buildTypes {
debug {
minifyEnabled false
shrinkResources false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
release {
minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}

设置minifyEnabled=true和shrinkResources=true启用压缩、混淆和优化功能, proguard-android-optimize.txt为存放在SDK中Android默认的混淆规则,存放路径~/SDK/tools/proguard/proguard-android-optimize.txt,proguard-rules.pro为项目中自己定义的混淆规则

3.2、常用混淆规则

关键字描述
keep保留类和类中的成员,防止被混淆或者移除
keepnames保留类和类中的成员,防止被混淆,但是当成员没有被引用时会被移除
keepclassmembers只保留类中的成员,防止他们被混淆或者移除
keepclassmembersnames只保留类中的成员,防止他们被混淆或者移除,但是当类中的成员没有被引用时还是会被移除
keepclasseswithmembers保留类和类中的成员,前提是指明的类中必须含有该成员,没有的话还是会被混淆
keepclasseswithmembersnames保留类和类中的成员,前提是指明的类中必须含有该成员,没有的话还是会被混淆。需要注意的是没有被引用的成员会被移除
 
关键字描述
<filed>匹配类中的所有字段
<method>匹配类中的所有方法
<init>匹配类中的所有构造函数
*匹配任意长度字符,但不含包名分隔符(.)。比如说我们的完整类名是com.example.test.MyActivity,使用com.*,或者com.exmaple.*都是无法匹配的,因为*无法匹配包名中的分隔符,正确的匹配方式是com.exmaple.*.*,或者com.exmaple.test.*,这些都是可以的。但如果你不写任何其它内容,只有一个*,那就表示匹配所有的东西。
**匹配任意长度字符,并且包含包名分隔符(.)。比如proguard-android.txt中使用的-dontwarn android.support.**就可以匹配android.support包下的所有内容,包括任意长度的子包。
***匹配任意参数类型。比如void set*(***)就能匹配任意传入的参数类型,*** get*()就能匹配任意返回值的类型。
匹配任意长度的任意类型参数。比如void test(…)就能匹配任意void test(String a)或者是void test(int a, String b)这些方法。

3.3、Android常用混淆模板

#-------------------------------------------基本不用动区域--------------------------------------------
#---------------------------------基本指令区----------------------------------
-optimizationpasses 5
-dontskipnonpubliclibraryclassmembers
-printmapping proguardMapping.txt
-optimizations !code/simplification/cast,!field/*,!class/merging/*
-keepattributes *Annotation*,InnerClasses
-keepattributes Signature
-keepattributes SourceFile,LineNumberTable
#----------------------------------------------------------------------------

#---------------------------------默认保留区---------------------------------
#继承activity,application,service,broadcastReceiver,contentprovider....不进行混淆
-keep public class * extends android.app.Activity
-keep public class * extends android.app.Application
-keep public class * extends androidx.multidex.MultiDexApplication
-keep public class * extends android.app.Service
-keep public class * extends android.content.BroadcastReceiver
-keep public class * extends android.content.ContentProvider
-keep public class * extends android.app.backup.BackupAgentHelper
-keep public class * extends android.preference.Preference
-keep public class * extends android.view.View
-keep class android.support.** {*;}

-keep public class * extends android.view.View{
*** get*();
void set*(***);
public <init>(android.content.Context);
public <init>(android.content.Context, android.util.AttributeSet);
public <init>(android.content.Context, android.util.AttributeSet, int);
}
-keepclasseswithmembers class * {
public <init>(android.content.Context, android.util.AttributeSet);
public <init>(android.content.Context, android.util.AttributeSet, int);
}
#这个主要是在layout 中写的onclick方法android:onclick="onClick",不进行混淆
-keepclassmembers class * extends android.app.Activity {
public void *(android.view.View);
}

-keepclassmembers class * implements java.io.Serializable {
static final long serialVersionUID;
private static final java.io.ObjectStreamField[] serialPersistentFields;
private void writeObject(java.io.ObjectOutputStream);
private void readObject(java.io.ObjectInputStream);
java.lang.Object writeReplace();
java.lang.Object readResolve();
}

-keep class **.R$* {
*;
}

-keepclassmembers class * {
void *(*Event);
}

#枚举
-keepclassmembers enum * {
public static **[] values();
public static ** valueOf(java.lang.String);
}

-keep class * implements android.os.Parcelable {
public static final android.os.Parcelable$Creator *;
}

#// natvie 方法不混淆
-keepclasseswithmembernames class * {
native <methods>;
}

#保持 Parcelable 不被混淆
-keep class * implements android.os.Parcelable {
public static final android.os.Parcelable$Creator *;
}

#---------------------------------webview------------------------------------
-keepclassmembers class android.webkit.WebView {
public *;
}
-keepclassmembers class * extends android.webkit.WebViewClient {
public void *(android.webkit.WebView, java.lang.String, android.graphics.Bitmap);
public boolean *(android.webkit.WebView, java.lang.String);
}
-keepclassmembers class * extends android.webkit.WebViewClient {
public void *(android.webkit.WebView, java.lang.String);
}
#----------------------------------------------------------------------------
#---------------------------------------------------------------------------------------------------
#---------------------------------实体类---------------------------------
#修改成你对应的包名
-keep class com.chenyangqi.gradle.proguard.bean.** { *; }

#---------------------------------第三方包-------------------------------

#---------------------------------反射相关的类和方法-----------------------

#---------------------------------与js互相调用的类------------------------

#---------------------------------自定义View的类------------------------

收起阅读 »

iOS 组件化方案

iOS
为什么要组件化?易移植、易维护、易重构、易根据业务做加减法、易开发理想中的组件化组件化最终应该达到每个组件可以单独开发,单独维护,不会对其他组件进行强依赖。理想的架构应该在横向上能够拆分出容器层,开源三方库,基础组件,业务形态SDK组件,普通业务组件;在纵向上...
继续阅读 »

为什么要组件化?
易移植、易维护、易重构、易根据业务做加减法、易开发

理想中的组件化
组件化最终应该达到每个组件可以单独开发,单独维护,不会对其他组件进行强依赖。
理想的架构应该在横向上能够拆分出容器层,开源三方库,基础组件,业务形态SDK组件,普通业务组件;在纵向上能够进行组件解耦,组件之间可以单独开发、维护、复用以及组件之间合理的通信机制。随着业务的复杂度增加,理想中的架构也应该不断的变化,


如何进行组件化
先进行组件的拆分,然后进行组件的之间的服务调度,然后进行事件分发包含系统事件以及组件本身自定义的事件实现比较完善的解耦

组件的拆分
将不同的业务代码按照业务的划分整合成一个独立的组件成为独立repo。

进行组件之间服务的调度实现接口依赖
1、将组件之间的依赖实体转为依赖抽象、依赖接口
2、不进行依赖直接依靠OC自身的动态性进行方法的调用

完善组件之间的通信以及事件分发
将系统事件、应用事件以及业务事件进行分发。基本可以做到每个组件之间,组件之间实现无耦合的通信,以及对系统、应用事件的感知。实现每一个组件都可以成为一个独立的APP运行

通用方案调研
目前业界有三大通用方案:面向接口进行解耦、使用URL路由的方式以及使用runtime进行解耦。各自的代表为:serviceCenter注册的BeeHive、URL注册的Router、使用runtime+Category的CTMediator
BeeHive的架构与解析


Beehive在服务调度上采用了protocol的方式,依赖指定的接口来实现组件之间的耦合。

服务注册
注册一个服务主要有两个问题:如何注册以及注册时间,BeeHive采用了三种服务注册的方式

Annotation方式注册
将所需要注册的服务通过注解的方式保存在可执行文件中,在可执行文件加载是的过程中实现protocol与class的注册。这种方式注册服务简单方便,每个组件可以在自己自行注册,不需要集中注册。整体流程对开发者比较友好

保存服务名
@BeeHiveService(UserTrackServiceProtocol,BHUserTrackViewController)
//
#define BeeHiveDATA(sectname) __attribute((used, section("__DATA,"#sectname" ")))
#define BeeHiveService(servicename,impl) \
class BeeHive; char * k##servicename##_service BeeHiveDATA(BeehiveServices) = "{ \""#servicename"\" : \""#impl"\"}";

这里是将服务名字存在__Data(这是数据段用于存放初始化好的变量)段中的指定区域,到时候和其他资源一起打入ipa中
__attribute((used))是为了防止release模式下被优化(在单测上尤其必要。有些接口只有单测代码引用了这个时候如果不加used进行修饰就无法进行单测)
为什么#要带两个""
进行注册
通过constructor进行修饰,在可执行文件加载之前设置mach-o和动态共享库的加载后的回调
在回调里面通过传过来的Mach-o的文件头地址以及之前设置好的__data段的section名字读取出存在该section的服务protocol以及class然后利用serviceCenter执行服务注册的逻辑,等待使用的时候进行初始化,避免内存常驻

__attribute__((constructor))
void initProphet() {
_dyld_register_func_for_add_image(dyld_callback); //添加image的load回调
}

NSMutableArray *configs = [NSMutableArray array];
unsigned long size = 0;
#ifndef __LP64__
// 读取存贮的服务的section地址
uintptr_t *memory = (uintptr_t*)getsectiondata(mhp, SEG_DATA, BeehiveServiceSectName, &size);
#else
const struct mach_header_64 *mhp64 = (const struct mach_header_64 *)mhp;
// 读取存贮的服务的section地址
uintptr_t *memory = (uintptr_t*)getsectiondata(mhp64, SEG_DATA, BeehiveServiceSectName, &size);
#endif
// 遍历改section获取服务的protocol以及class
unsigned long counter = size/sizeof(void*);
for(int idx = 0; idx < counter; ++idx){
char *string = (char*)memory[idx];
NSString *str = [NSString stringWithUTF8String:string];
if(!str)continue;

BHLog(@"config = %@", str);
if(str) [configs addObject:str];
}
//register services
for (NSString *map in configs) {
NSData *jsonData = [map dataUsingEncoding:NSUTF8StringEncoding];
NSError *error = nil;
id json = [NSJSONSerialization JSONObjectWithData:jsonData options:0 error:&error];
if (!error) {
if ([json isKindOfClass:[NSDictionary class]] && [json allKeys].count) {

NSString *protocol = [json allKeys][0];
NSString *clsName = [json allValues][0];

if (protocol && clsName) {
[[BHServiceManager sharedManager] registerService:NSProtocolFromString(protocol) implClass:NSClassFromString(clsName)];
}

}
}
}

使用文件的方式进行注册
将所需要注册的服务通过plist文件打包到app中,在APP启动的lauch回调中实现实现protocol与class的注册

保存服务名
在文件中以key-value的形式保存服务名到plist文件中

进行注册
在app的lauch中找到对应保存服务的plist文件进行读取,然后使用serviceCenter将服务保存字典中allServicesDict,等待使用的时候进行初始化,避免内存常驻

NSString *plistPath = [[NSBundle mainBundle] pathForResource:serviceConfigName ofType:@"plist"];
if (!plistPath) {
return;
}
NSArray *serviceList = [[NSArray alloc] initWithContentsOfFile:plistPath];

[self.lock lock];
for (NSDictionary *dict in serviceList) {
NSString *protocolKey = [dict objectForKey:@"service"];
NSString *protocolImplClass = [dict objectForKey:@"impl"];
if (protocolKey.length > 0 && protocolImplClass.length > 0) {
[self.allServicesDict addEntriesFromDictionary:@{protocolKey:protocolImplClass}];
}
}
// 使用的时候主动创建服务的实现方
id<UserTrackServiceProtocol> v4 = [[BeeHive shareInstance] createService:@protocol(UserTrackServiceProtocol)];

使用+load的方式进行注册
无需保存文件名直接在+load方法中利用serviceCenter进行protocol与class的注册,等待使用的时候进行初始化,避免内存常驻

总结
三种注册方式,使用注解的注册方式整体来说比较优雅,对开发者也是比较友好的,注册的时机也是最靠前的。其次是+load也是执行main函数之前使用起来也比较简单。之后是文件注册,注册时间再main函数之后,APP启动中进行注册,一个是时间需要人为的去保证,其实使用起来需要统一改plist文件不是很好的处理方式。

服务的调度
服务的创建
在使用服务之前进行进行服务的获取,使用懒加载避免了服务在内存中常驻

id<UserTrackServiceProtocol> v4 = [[BeeHive shareInstance] createService:@protocol(UserTrackServiceProtocol)];
1
服务的实现类可以是普通实例也可以是单例,业务方可以自行实现singleton方法进行选择。内部还有一个shouldCache参数决定是否使用cache(看起来作用不是很大,如果是单例则无论是否使用cache,获取的都会是唯一的实例,如果不是单例则无论是否使用cache获取的都是新的实例)。
shouldCache这里可以修改一下使用,由于业务方是不知道每次获取的服务是单例还是唯一的实例,所以确实可以增加shouldCache给业务方使用,当业务方选择使用缓存的时候,这时候如果没有缓存返回空给业务方一个感知,有缓存的时候正常返回。这样shouldCache才有意义

if (shouldCache) {
id protocolImpl = [[BHContext shareInstance] getServiceInstanceFromServiceName:serviceStr];
if (protocolImpl) {
return protocolImpl;
}
//增加else
else {
return nil
}
}
1
2
3
4
5
6
7
8
9
10
服务获取以及移除
在已经创建的服务列表中移除所创建的服务或者获取服务。

- (id)getServiceInstanceFromServiceName:(NSString *)serviceName
{
return [[BHContext shareInstance] getServiceInstanceFromServiceName:serviceName];
}
- (void)removeServiceWithServiceName:(NSString *)serviceName
{
[[BHContext shareInstance] removeServiceWithServiceName:serviceName];
}
1
2
3
4
5
6
7
8
服务的调用
通过协议定义好的接口进行指定服务的调用

总结
能够进行较好的服务调度基本能满足需求,使用延迟创建的方式可以避免内存常驻。区分了单例获取与普通实例获取。在cache设计上可以稍作完善,使得cache变的更有意义

进行组件之间的通信以及消息分发
BeeHive在实现组件调度的同时,实现了组件的可插拨,同时实现了一套消息分发的机制,利用BHContext容器进行组件与组件,组件之前的通信以及消息分发,完善通信机制,一定程度上做到了每一个组件都是一个五脏俱全的小APP,以及彼此之间的一些相互影响

总结
BeeHive架构下组件实现了可插拔,可以独立存在进行开发。组件之间解耦相对彻底,可以进行通信。每个组件有自己的生命周期,独立管理。组件之间通过protocol进行服务之间的调度,对开发者比较友好。同时protocol对方法调用的参数等也进行了约束,

MGJRouter架构解析


MGJRouter使用URL路由的方式进行组件之间的服务调度

服务的注册
app启动期间注册指定的scheme与block

服务的调度
通过解析scheme,进行对应block的获取以及调用

总结
总体来说比较轻量,使用起来简单,但是可用范围也有限。
1、只能注册block,可以增加protocol的注册或者实现解析指定参数利用NSInvocation进行方法的调用
2、注册过程依赖字符串,容易引发问题

CTMediator Target-Action方案
该方案没有注册的流程,使用的runtime利用字符串来实现服务的调度

服务的调度
远程调度
约定好URL规则之后,将远程调度在内部转为本地调度

本地调度
利用CTMediator使用performTarget:action:param进行方法的调度,在CTMediator中利用invocation的方式行方法的调用

分类的使用
为了避免方法签名的不确定性,CTMediator使用分类的方式明确了对外的接口同时在分类内部将参数转成字典传参,使用runtime主动发现服务方后在利用invocation进行方法的调用

总结
整体上利用runtime主动发现服务比注册服务来说解耦是比较完善的。使用category也一定程度上解决了参数的校验问题
但是在mediator内部使用仍然是perfromSelector来调用,所以整体上始终是无法做到完美的参数校验。此外增加了Category层,在里面进行参数的中转以及一些适配逻辑相比较protocol而言并没有减轻增加服务的复杂度

三种方案对比
方案 S W 适用场景
URLRouter 使用简单;支持远程调用 字符串的方式硬编码较多,维护成本大;内存常驻;组件依赖中间件;参数缺乏强约束(字典) 远程调用
Service 面向接口更符合iOS开发者习惯;编编译阶段进行检查,使用上比rumtime方式更安全;支持复杂参数 组件依赖中间件,新增服务稍微麻烦 内部调用
Runtime+category 新增组件更加快速;依赖最小;支持复杂参数 使用runtime缺少参数的校验;对开发者要求更高,使用不当容易产生问题,;不利于重构 内部调用
公司内部解决方案
抖音与头条使用的方案与Beehive类似主要使用的protocol-class的方案进行解耦

使用 TTRoute进行页面之间的跳转.
利用protocol-class进行方法的调用

————————————————

原文链接:https://blog.csdn.net/songzhuo1991/article/details/115977726

收起阅读 »

iOS - http & https & 网络请求过程

iOS
给大家总结网络请求过程:三次握手图集: 看了此图, 于是乎,问题来了, 不是TCP链接的时候需要三次握手么( http://blog.csdn.net/whuslei/article/details/6667471 ),问题确实来...
继续阅读 »

给大家总结网络请求过程:

三次握手图集:

 

看了此图, 于是乎,问题来了, 不是TCP链接的时候需要三次握手么( http://blog.csdn.net/whuslei/article/details/6667471 ),问题确实来了, 三次握手每次都需要应用层的数据报文么, 于是乎搜得答案

 

具体链接:http://blog.csdn.net/luozenghui529480823/article/details/12978957  了解了网络链接, 有必要了解HTTP 和Https ,  那么首先看一下Http :http://www.jianshu.com/p/81632fea327c 这都是深度好文啊,就连cookie你都知道原理了吧,那么看看https吧,

 

  一看我们的工程既有https又有http你会发现有这个东西,

 

既然 https如此安全, 那机制是什么呢,我们都知道HTTPS能够加密信息,以免敏感信息被第三方获取。所以很多银行网站或电子邮箱等等安全级别较高的服务都会采用HTTPS协议。

下面我们介绍下https:

HTTPS其实是有两部分组成:HTTP +SSL/ TLS,也就是在HTTP上又加了一层处理加密信息的模块。服务端和客户端的信息传输都会通过TLS进行加密,所以传输的数据都是加密后的数据。具体是如何进行加密,解密,验证的,且看下图。

1. 客户端发起HTTPS请求

这个没什么好说的,就是用户在浏览器里输入一个https网址,然后连接到server的443端口。

2. 服务端的配置

采用HTTPS协议的服务器必须要有一套数字证书,可以自己制作,也可以向组织申请。区别就是自己颁发的证书需要客户端验证通过,才可以继续访问,而使用受信任的公司申请的证书则不会弹出提示页面(startssl就是个不错的选择,有1年的免费服务)。这套证书其实就是一对公钥和私钥。如果对公钥和私钥不太理解,可以想象成一把钥匙和一个锁头,只是全世界只有你一个人有这把钥匙,你可以把锁头给别人,别人可以用这个锁把重要的东西锁起来,然后发给你,因为只有你一个人有这把钥匙,所以只有你才能看到被这把锁锁起来的东西。

3. 传送证书

这个证书其实就是公钥,只是包含了很多信息,如证书的颁发机构,过期时间等等。

4. 客户端解析证书

这部分工作是有客户端的TLS来完成的,首先会验证公钥是否有效,比如颁发机构,过期时间等等,如果发现异常,则会弹出一个警告框,提示证书存在问题。如果证书没有问题,那么就生成一个随即值。然后用证书对该随机值进行加密。就好像上面说的,把随机值用锁头锁起来,这样除非有钥匙,不然看不到被锁住的内容。

5. 传送加密信息

这部分传送的是用证书加密后的随机值,目的就是让服务端得到这个随机值,以后客户端和服务端的通信就可以通过这个随机值来进行加密解密了。

6. 服务段解密信息

服务端用私钥解密后,得到了客户端传过来的随机值(私钥),然后把内容通过该值进行对称加密。所谓对称加密就是,将信息和私钥通过某种算法混合在一起,这样除非知道私钥,不然无法获取内容,而正好客户端和服务端都知道这个私钥,所以只要加密算法够彪悍,私钥够复杂,数据就够安全。

7. 传输加密后的信息

这部分信息是服务段用私钥加密后的信息,可以在客户端被还原

8. 客户端解密信息

客户端用之前生成的私钥解密服务段传过来的信息,于是获取了解密后的内容。整个过程第三方即使监听到了数据,也束手无策。

 

SSL的位置

SSL介于应用层和TCP层之间。应用层数据不再直接传递给传输层,而是传递给SSL层,SSL层对从应用层收到的数据进行加密,并增加自己的SSL头。

RSA性能是非常低的,原因在于寻找大素数、大数计算、数据分割需要耗费很多的CPU周期,所以一般的HTTPS连接只在第一次握手时使用非对称加密,通过握手交换对称加密密钥,在之后的通信走对称加密。

http://www.cnblogs.com/ttltry-air/archive/2012/08/20/2647898.html

HTTPS在传输数据之前需要客户端(浏览器)与服务端(网站)之间进行一次握手,在握手过程中将确立双方加密传输数据的密码信息。TLS/SSL协议不仅仅是一套加密传输的协议,更是一件经过艺术家精心设计的艺术品,TLS/SSL中使用了非对称加密,对称加密以及HASH算法。握手过程的具体描述如下:

1.浏览器将自己支持的一套加密规则发送给网站。

2.网站从中选出一组加密算法与HASH算法,并将自己的身份信息以证书的形式发回给浏览器。证书里面包含了网站地址,加密公钥,以及证书的颁发机构等信息。

3.浏览器获得网站证书之后浏览器要做以下工作

a)验证证书的合法性(颁发证书的机构是否合法,证书中包含的网站地址是否与正在访问的地址一致等),如果证书受信任,则浏览器栏里面会显示一个小锁头,否则会给出证书不受信的提示。

b) 如果证书受信任,或者是用户接受了不受信的证书,浏览器会生成一串随机数的密码,并用证书中提供的公钥加密。

c)使用约定好的HASH算法计算握手消息,并使用生成的随机数对消息进行加密,最后将之前生成的所有信息发送给网站。

4.网站接收浏览器发来的数据之后要做以下的操作:

a) 使用自己的私钥将信息解密取出密码,使用密码解密浏览器发来的握手消息,并验证HASH是否与浏览器发来的一致。

b) 使用密码加密一段握手消息,发送给浏览器。

5.浏览器解密并计算握手消息的HASH,如果与服务端发来的HASH一致,此时握手过程结束,之后所有的通信数据将由之前浏览器生成的随机密码并利用对称加密算法进行加密

这里浏览器与网站互相发送加密的握手消息并验证,目的是为了保证双方都获得了一致的密码,并且可以正常的加密解密数据,为后续真正数据的传输做一次测试。另外,HTTPS一般使用的加密与HASH算法如下:

非对称加密算法:RSA,DSA/DSS

对称加密算法:AES,RC4,3DES

HASH算法:MD5,SHA1,SHA256

总结:

服务器 用RSA生成公钥和私钥

把公钥放在证书里发送给客户端,私钥自己保存

客户端首先向一个权威的服务器检查证书的合法性,如果证书合法,客户端产生一段随机数,这个随机数就作为通信的密钥,我们称之为对称密钥,用公钥加密这段随机数,然后发送到服务器

服务器用密钥解密获取对称密钥,然后,双方就已对称密钥进行加密解密通信了

链接:https://blog.csdn.net/songzhuo1991/article/details/104349714?utm_medium=distribute.pc_relevant.none-task-blog-2~default~baidujs_title~default-1.control&spm=1001.2101.3001.4242

收起阅读 »

OC与Swift API的交互

互用性是让 Swift 和 Objective-C 相接合的一种特性,使你能够在一种语言编写的文件中使用另一种语言。当你准备开始把 Swift 融入到你的开发流程中时,你应该懂得如何利用互用性来重新定义并提高你写 Cocoa 应用的方案。互用性很重要的一点就是...
继续阅读 »
互用性是让 Swift 和 Objective-C 相接合的一种特性,使你能够在一种语言编写的文件中使用另一种语言。当你准备开始把 Swift 融入到你的开发流程中时,你应该懂得如何利用互用性来重新定义并提高你写 Cocoa 应用的方案。

互用性很重要的一点就是允许你在写 Swift 代码时使用 Objective-C 的 API 接口。当你导入一个 Objective-C 框架后,你可以使用原生的 Swift 语法实例化它的 Class 并且与之交互。

初始化

为了使用 Swift 实例化 Objective-C 的 Class,你应该使用 Swift 语法调用它的一个初始化器。当 Objective-C 的init方法变化到 Swift,他们用 Swift 初始化语法呈现。“init”前缀被截断当作一个关键字,用来表明该方法是初始化方法。那些以“initWith”开头的init方法,“With”也会被去除。从“init”或者“initWith”中分离出来的这部分方法名首字母变成小写,并且被当做是第一个参数的参数名。其余的每一部分方法名依次变味参数名。这些方法名都在圆括号中被调用。

举个例子,你在使用 Objective-C 时会这样做:

1.  //Objective-C
2. UITableView *myTableView = [[UITableView alloc]
3. initWithFrame:CGRectZero style:UITableViewStyleGrouped];

在 Swift 中,你应该这样做:

1.  //Swift
2. let myTableView: UITableView = UITableView(frame: CGRectZero, style: .Grouped)

你不需要调用 alloc,Swift 能替你处理。注意,当使用 Swift 风格的初始化函数的时候,“init”不会出现。
你可以在初始化时显式的声明对象的类型,也可以忽略它,Swift 能够正确判断对象的类型。


1.  //Swift
2. let myTextField = UITextField(frame: CGRect(0.0, 0.0, 200.0, 40.0))
这里的UITableView和UITextField对象和你在 Objective-C 中使用的具有相同的功能。你可以用一样的方式使用他们,包括访问属性或者调用各自的类中的方法。
为了统一和简易,Objective-C 的工厂方法也在 Swift 中映射为方便的初始化方法。这种映射能够让他们使用同样简洁明了的初始化方法。例如,在 Objective-C 中你可能会像下面这样调用一个工厂方法:

1.  //Objective-C
2. UIColor *color = [UIColor colorWithRed:0.5 green:0.0 blue:0.5 alpha:1.0];

在 Swift 中,你应该这样做:

1.  //Swift
2. let color = UIColor(red: 0.5, green: 0.0, blue: 0.5, alpha: 1.0)

访问属性

在 Swift 中访问和设置 Objective-C 对象的属性时,使用点语法:

1.  // Swift
2. myTextField.textColor = UIColor.darkGrayColor()
3. myTextField.text = "Hello world"
4. if myTextField.editing {
5. myTextField.editing = false
6. }

当 get 或 set 属性时,直接使用属性名称,不需要附加圆括号。注意,darkGrayColor后面附加了一对圆括号,这是因为darkGrayColor是UIColor的一个类方法,不是一个属性。

在 Objective-C 中,一个有返回值的无参数方法可以被作为一个隐式的访问函数,并且可以与访问器使用同样的方法调用。但在 Swift 中不再能够这样做了,只有使用@property关键字声明的属性才会被作为属性引入。

方法

在 Swift 中调用 Objective-C 方法时,使用点语法。

当 Objective-C 方法转换到 Swift 时,Objective-C 的selector的第一部分将会成为方法名并出现在圆括号的前面,而第一个参数将直接在括号中出现,并且没有参数名,而剩下的参数名与参数则一一对应的填入圆括号中。

举个例子,你在使用 Objective-C 时会这样做:

1.  //Objective-C
2. [myTableView insertSubview:mySubview atIndex:2];

在 Swift 中,你应该这样做:

1.  //Swift
2. myTableView.insertSubview(mySubview, atIndex: 2)

如果你调用一个无参方法,仍必须在方法名后面加上一对圆括号

1.  //Swift
2. myTableView.layoutIfNeeded()

id 兼容性(id Compatibility)

Swift 包含一个叫做AnyObject的协议类型,表示任意类型的对象,就像 Objective-C 中的id一样。AnyObject协议允许你编写类型安全的 Swift 代码同时维持无类型对象的灵活性。因为AnyObject协议保证了这种安全,Swift 将 id 对象导入为 AnyObject。

举个例子,跟 id 一样,你可以为AnyObject类型的对象分配任何其他类型的对象,你也同样可以为它重新分配其他类型的对象。

1.  //Swift
2. var myObject: AnyObject = UITableViewCell()
3. myObject = NSDate()

你也可以在调用 Objective-C 方法或者访问属性时不将它转换为具体类的类型。这包括了 Objcive-C 中标记为 @objc 的方法。

1.  //Swift
2. let futureDate = myObject.dateByAddingTimeInterval(10)
3. let timeSinceNow = myObject.timeIntervalSinceNow
然而,由于直到运行时才知道AnyObject的对象类型,所以有可能在不经意间写出不安全代码。另外,与 Objective-C 不同的是,如果你调用方法或者访问的属性 AnyObject 对象没有声明,将会报运行时错误。比如下面的代码在运行时将会报出一个 unrecognized selector error 错误:

1.  //Swift
2. myObject.characterAtIndex(5)
3. // crash, myObject does't respond to that method

但是,你可以通过 Swift 的 optinals 特性来排除这个 Objective-C 中常见的错误,当你用AnyObject对象调用一个 Objective-C 方法时,这次调用将会变成一次隐式展开 optional(implicitly unwrapped optional)的行为。你可以通过 optional 特性来决定 AnyObject 类型的对象是否调用该方法,同样的,你可以把这种特性应用在属性上。

举个例子,在下面的代码中,第一和第二行代码将不会被执行因为length属性和characterAtIndex:方法不存在于 NSDate 对象中。myLength常量会被推测成可选的Int类型并且被赋值为nil。同样你可以使用if-let声明来有条件的展开这个方法的返回值,从而判断对象是否能执行这个方法。就像第三行做的一样。


1.  //Swift
2. let myLength = myObject.length?
3. let myChar = myObject.characterAtIndex?(5)
4. if let fifthCharacter = myObject.characterAtIndex(5) {
5. println("Found \(fifthCharacter) at index 5")
6. }


对于 Swift 中的强制类型转换,从 AnyObject 类型的对象转换成明确的类型并不会保证成功,所以它会返回一个可选的值。而你需通过检查该值的类型来确认转换是否成功。

1.  //Swift
2. let userDefaults = NSUserDefaults.standardUserDefaults()
3. let lastRefreshDate: AnyObject? = userDefaults.objectForKey("LastRefreshDate")
4. if let date = lastRefreshDate as? NSDate {
5. println("\(date.timeIntervalSinceReferenceDate)")
6. }

当然,如果你能确定这个对象的类型(并且确定不是nil),你可以添加as操作符强制调用。

1.  //Swift
2. let myDate = lastRefreshDate as NSDate
3. let timeInterval = myDate.timeIntervalSinceReferenceDate

使用nil

在Objective-C中,对象的引用可以是值为NULL的原始指针(同样也是Objective-C中的nil)。而在Swift中,所有的值–包括结构体与对象的引用–都被保证为非空。作为替代,你将这个可以为空的值包装为optional type。当你需要宣告值为空时,你需要使用nil。你可以在Optionals中了解更多。

因为Objective-C不会保证一个对象的值是否非空,Swift在引入Objective-C的API的时候,确保了所有函数的返回类型与参数类型都是optional,在你使用Objective-C的API之前,你应该检查并保证该值非空。 在某些情况下,你可能绝对确认某些Objective-C方法或者属性永远不应该返回一个nil的对象引用。为了让对象在这种情况下更加易用,Swift使用 implicitly unwrapped optionals 方法引入对象, implicitly unwrapped optionals 包含optional 类型的所有安全特性。此外,你可以直接访问对象的值而无需检查nil。当你访问这种类型的变量时, implicitly unwrapped optional 首先检查这个对象的值是否不存在,如果不存在,将会抛出运行时错误。

扩展(Extensions)

Swift 的扩展和 Objective-C 的类别(Category)相似。扩展为原有的类,结构和枚举丰富了功能,包括在 Objective-C 中定义过的。你可以为系统的框架或者你自己的类型增加扩展,只需要导入合适的模块并且保证你在 Objective-C 中使用的类、结构或枚举拥有相同的名字。

举个例子,你可以扩展UIBezierPath类来为它增加一个等边三角形,这个方法只需提供三角形的边长与起点。

1.  //Swift
2. extension UIBezierPath {
3. convenience init(triangleSideLength: Float, origin: CGPoint) {
4. self.init()
5. let squareRoot = Float(sqrt(3))
6. let altitude = (squareRoot * triangleSideLength) / 2
7. moveToPoint(origin)
8. addLineToPoint(CGPoint(triangleSideLength, origin.x))
9. addLineToPoint(CGPoint(triangleSideLength / 2, altitude))
10. closePath()
11. }
12. }
你也可以使用扩展来增加属性(包括类的属性与静态属性)。然而,这些属性必须是通过计算才能获取的,扩展不会为类,结构体,枚举存储属性。下面这个例子为CGRect类增加了一个叫area的属性。

1.  //Swift
2. extension CGRect {
3. var area: CGFloat {
4. return width * height
5. }
6. }
7. let rect = CGRect(x: 0.0, y: 0.0, width: 10.0, height: 50.0)
8. let area = rect.area
9. // area: CGFloat = 500.0

你同样可以使用扩展来为类添加协议而无需增加它的子类。如果这个协议是在 Swift 中被定义的,你可以添加 comformance 到它的结构或枚举中无论它们在 Objective-C 或在 Swift 中被定义。

你不能使用扩展来覆盖 Objective-C 类型中存在的方法与属性。

闭包(Closures)

Objective-C 中的blocks会被自动导入为 Swift 中的闭包。例如,下面是一个 Objective-C 中的 block 变量:


1.  //Objective-C
2. void (^completionBlock)(NSData *, NSError *) = ^(NSData *data, NSError *error) {/* ... */}

而它在 Swift 中的形式为

1.  //Swift
2. let completionBlock: (NSData, NSError) -> Void = {data, error in /* ... */}

Swift 的闭包与 Objective-C 中的 blocks 能够和睦相处,所以你可以把一个 Swift 闭包传递给一个把 block 作为参数的 Objective-C 函数。Swift 闭包与函数具有互通的类型,所以你甚至可以传递 Swift 函数的名字。
闭包与 blocks 语义上想通但是在一个地方不同:变量是可以直接改变的,而不是像 block 那样会拷贝变量。换句话说,Swift 中变量的默认行为与 Objective-C 中 __block 变量一致。

比较对象

当比较两个 Swift 中的对象时,可以使用两种方式。第一种,使用(==),判断两个对象内容是否相同。第二种,使用(===),判断常量或者变量是否为同一个对象的实例。

Swift 与 Objective-C 一般使用 == 与 === 操作符来做比较。Swift 的 == 操作符为源自 NSObject 的对象提供了默认的实现。在实现 == 操作符时,Swift 调用 NSObject 定义的 isEqual: 方法。

NSObject 类仅仅做了身份的比较,所以你需要在你自己的类中重新实现 isEqual: 方法。因为你可以直接传递 Swift 对象给 Objective-C 的 API,你也应该为这些对象实现自定义的 isEqual: 方法,如果你希望比较两个对象的内容是否相同而不是仅仅比较他们是不是由相同的对象派生。

作为实现比较函数的一部分,确保根据Object comparison实现对象的hash属性。更进一步的说,如果你希望你的类能够作为字典中的键,也需要遵从Hashable协议以及实现hashValues属性。

Swift 类型兼容性

当你定义了一个继承自NSObject或者其他 Objective-C 类的 Swift 类,这些类都能与 Objective-C 无缝连接。所有的步骤都有 Swift 编译器自动完成,如果你从未在 Objective-C 代码中导入 Swift 类,你也不需要担心类型适配问题。另外一种情况,如果你的 Swift 类并不来源自 Objectve-C 类而且你希望能在 Objecive-C 的代码中使用它,你可以使用下面描述的 @objc 属性。

@objc可以让你的 Swift API 在 Objective-C 中使用。换句话说,你可以通过在任何 Swift 方法、类、属性前添加@objc,来使得他们可以在 Objective-C 代码中使用。如果你的类继承自 Objective-C,编译器会自动帮助你完成这一步。编译器还会在所有的变量、方法、属性前加 @objc,如果这个类自己前面加上了@objc关键字。当你使用@IBOutlet,@IBAction,或者是@NSManaged属性时,@objc也会自动加在前面。这个关键字也可以用在 Objetive-C 中的 target-action 设计模式中,例如,NSTimer或者UIButton。

当你在 Objective-C 中使用 Swift API,编译器基本对语句做直接的翻译。例如,Swift API func playSong(name: String)会被解释为- (void)playSong:(NSString *)name。然而,有一个例外:当在 Objective-C 中使用 Swift 的初始化函数,编译器会在方法前添加“initWith”并且将原初始化函数的第一个参数首字母大写。例如,这个 Swift 初始化函数init (songName: String, artist: String将被翻译为- (instancetype)initWithSongName:(NSString *)songName artist:(NSString *)artist 。

Swift 同时也提供了一个@objc关键字的变体,通过它你可以自定义在 Objectiv-C 中转换的函数名。例如,如果你的 Swift 类的名字包含 Objecytive-C 中不支持的字符,你就可以为 Objective-C 提供一个可供替代的名字。如果你给 Swift 函数提供一个 Objecytive-C 名字,要记得为带参数的函数添加(:)

1.  //Swift
2. @objc(Squirrel)
3. class Белка {
4. @objc(initWithName:)
5. init (имя: String) { /*...*/ }
6. @objc(hideNuts:inTree:)
7. func прячьОрехи(Int, вДереве: Дерево) { /*...*/ }
8. }

当你在 Swift 类中使用@objc(<#name#>)关键字,这个类可以不需要命名空间即可在 Objective-C 中使用。这个关键字在你迁徙 Objecive-C 代码到 Swift 时同样也非常有用。由于归档过的对象存贮了类的名字,你应该使用@objc(<#name#>)来声明与旧的归档过的类相同的名字,这样旧的类才能被新的 Swift 类解档。

Objective-C 选择器(Selectors)

一个 Objective-C 选择器类型指向一个 Objective-C 的方法名。在 Swift 时代,Objective-C 的选择器被Selector结构体替代。你可以通过字符串创建一个选择器,比如let mySelector: Selector = "tappedButton:"。因为字符串能够自动转换为选择器,所以你可以把字符串直接传递给接受选择器的方法。


1.  //Swift
2. import UIKit
3. class MyViewController: UIViewController {
4. let myButton = UIButton(frame: CGRect(x: 0, y: 0, width: 100, height: 50))

6. init(nibName nibNameOrNil: String!, bundle nibBundleOrNil: NSBundle!) {
7. super.init(nibName: nibName, bundle: nibBundle)
8. myButton.targetForAction("tappedButton:", withSender: self)
9. }

11. func tappedButton(sender: UIButton!) {
12. println("tapped button")
13. }
14. }

注意: performSelector:方法和相关的调用选择器的方法没有导入到 Swift 中因为它们是不安全的。

如果你的 Swift 类继承自 Objective-C 的类,你的所有方法都可以用作 Objective-C 的选择器。另外,如果你的 Swift 类不是继承自 Objective-C,如果你想要当选择器来使用你就需要在前面添加@objc关键字,详情请看Swift 类型兼容性。


作者:iOS鑫
链接:https://www.jianshu.com/p/c17977fd96aa




收起阅读 »

iOS 优雅的处理网络数据,你真的会吗?不如看看这篇.

相信大家平时在用 App 的时候, 往往有过这样的体验,那就是加载网络数据等待的时间过于漫长,滚动浏览时伴随着卡顿,甚至在没有网络的情况下,整个应用处于不可用状态。那么我们该怎么去提高用户体验,保证用户没有漫长的等待感,还可以轻松自在的享受等待,对加载后的内容...
继续阅读 »
相信大家平时在用 App 的时候, 往往有过这样的体验,那就是加载网络数据等待的时间过于漫长,滚动浏览时伴随着卡顿,甚至在没有网络的情况下,整个应用处于不可用状态。那么我们该怎么去提高用户体验,保证用户没有漫长的等待感,还可以轻松自在的享受等待,对加载后的内容有明确的预期呢?

案例分享

在现代的工作生活中,手机早已不是单纯的通信工具了,它更像是一个集办公,娱乐,消费的终端,潜移默化的成为了我们生活的一部分。所以作为 iOS 开发者的我们,在日常的开发中,也早已不是处理显示零星的数据这么简单,为了流量往往我们需要在 App 里显示大量有价值的信息来吸引用户,如何优雅的显示这些海量的数据,考量的就是你的个人经验了。

正如大多数 iOS 开发人员所知,显示滚动数据是构建移动应用中常见的任务,Apple 的 SDK 提供了 UITableView 和 UICollectionVIew 这俩大组件来帮助执行这样的任务。但是,当需要显示大量数据时,确保平滑如丝的滚动可能会非常的棘手。所以今天正好趁这个机会,和大家分享一下处理大量可滚动数据方面的个人经验。

在这篇文章中,你将会学到以下内容:

1.让你的 App 可以无限滚动(infinite scrolling),并且滚动数据无缝加载

2.让你的 App 数据滚动时避免卡顿,实现平滑如丝的滚动

3.异步存储(Cache)和获取图像,来使你的 App 具有更高的响应速度


无限滚动,无缝加载

提到列表分页,相信大家第一个想到的就是 MJRefresh,用于上拉下拉来刷新数据,当滚动数据到达底部的时候向服务器发送请求,然后在控件底部显示一个 Loading 动画,待请求数据返回后,Loading 动画消失,由 UITableView 或者 UICollectionView 控件继续加载这些数据并显示给用户,效果如下图所示





在这种情况下就造成了一种现象,那就是 App 向服务器请求数据到数据返回这段时间留下了一个空白,如果在网络差的情况下,这段空白的时间将会持续,这给人的体验会很不好。那该如何去避免这种现象呢!或者说我们能否去提前获取到其余的数据,在用户毫无感知的情况下把数据请求过来,看上去就像无缝加载一样呢!

答案当然是肯定的!

为了改善应用程序体验,在 iOS 10 上,Apple 对 UICollectionView 和 UITableView 引入了 Prefetching API,它提供了一种在需要显示数据之前预先准备数据的机制,旨在提高数据的滚动性能。

首先,我先和大家介绍一个概念:无限滚动,无限滚动是可以让用户连续的加载内容,而无需分页。在 UI 初始化的时候 App 会加载一些初始数据,然后当用户滚动快要到达显示内容的底部时加载更多的数据。

多年来,像 Instagram, Twitter 和 Facebook 这样的社交媒体公司都使这种技术。如果查看他们的 App ,你就可以看到无限滚动的实际效果,这里我就给大伙展示下 Instagram 的效果吧!





如何实现

由于 Instagram 的 UI 过于复杂,在这我就不去模仿实现了,但是我模仿了它的加载机制,同样的实现了一个简单的数据无限滚动和无缝加载的效果。

简单的说下我的思路:

先自定义一个 Cell 视图,这个视图由一个 UILabel 和 一个 UIImageView 构成,用于显示文本和网络图片;然后模拟网络请求来获取数据,注意该步骤一定是异步执行的;最后用 UITableView 来显示返回的数据,在 viewDidLoad 中先请求网络数据来获取一些初始化数据,然后再利用 UITableView 的 Prefetching API 来对数据进行预加载,从而来实现数据的无缝加载。

那关于无限滚动该如何实现呢!其实这个无限滚动并不是真正意义上的永无止尽,严格意义上来讲它是有尽头的,只不过这个功能背后的数据是不可估量的,只有大量的数据做支持才能让应用一直不断的从服务端获取数据。

正常情况下,我们在构建 UITableView 这个控件的时候,需要对它的行数(numsOfRow)做一个初始化,这个行数对我们实现无限加载和无缝加载是一个很关键的因素,假设我们每次根据服务端返回的数据量去更新 UITableView 的行数并 Reload,那我之前说的 Prefetching API 在这种情况下就失去作用了,因为它起作用的前提是要保证预加载数据时 UITableView 当前的行数要小于它的总行数。当然前者也可以实现数据加载,但它的效果就不是无缝加载,它在每次加载数据的时候都会有一个 Loading 等待的时间。

回到我上面所说的无限滚动, 其实实现起来并不难,正常情况下,我们向服务端请求大量相同类型的数据的时候,都会提供一个接口,我称之为分页请求接口,该接口在每次数据返回的时候,都会告诉客户端总共有多少页数据,每页的数据量是多少,当前是第几页,这样我们就能计算出来总共的数据有多少,而这就是 UITableView 的总行数。

响应数据的示范如下(为清楚起见,它仅显示与分页有关的字段):


{

"has_more": true,
"page": 1,
"total": 84,
"items": [

...
...
]
}




下面,我就用代码来一步步的实现它吧!

模拟分页请求

由于没有找到合适的分页测试接口,于是我自己模拟一了一个分页请求接口,每次调用该接口的时候都延时 2s 来模拟网络请求的状态,代码如下:

 func fetchImages() {
guard !isFetchInProcess else {
return
}

isFetchInProcess = true
// 延时 2s 模拟网络环境
print("+++++++++++ 模拟网络数据请求 +++++++++++")
DispatchQueue.global().asyncAfter(deadline: DispatchTime.now() + 2) {
print("+++++++++++ 模拟网络数据请求返回成功 +++++++++++")
DispatchQueue.main.async {
self.total = 1000
self.currentPage += 1
self.isFetchInProcess = false
// 初始化 30个 图片
let imagesData = (1...30).map {
ImageModel(url: baseURL+"\($0).png", order: $0)
}
self.images.append(contentsOf: imagesData)

if self.currentPage > 1 {
let newIndexPaths = self.calculateIndexPathsToReload(from: imagesData)
self.delegate?.onFetchCompleted(with: newIndexPaths)
} else {
self.delegate?.onFetchCompleted(with: .none)
}
}
}
}

数据回调处理:

extension ViewController: PreloadCellViewModelDelegate {

func onFetchCompleted(with newIndexPathsToReload: [IndexPath]?) {
guard let newIndexPathsToReload = newIndexPathsToReload else {
tableView.tableFooterView = nil
tableView.reloadData()
return
}

let indexPathsToReload = visibleIndexPathsToReload(intersecting: newIndexPathsToReload)
indicatorView.stopAnimating()
tableView.reloadRows(at: indexPathsToReload, with: .automatic)
}

func onFetchFailed(with reason: String) {
indicatorView.stopAnimating()
tableView.reloadData()
}
}

预加载数据

首先如果你想要 UITableView 预加载数据,你需要在 viewDidLoad() 函数中插入如下代码,并且请求第一页的数据:

override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
...
tableView.prefetchDataSource = self
...
// 模拟请求图片
viewModel.fetchImages()
}

然后,你需要实现 UITableViewDataSourcePrefetching 的协议,它的协议里包含俩个函数:

// this protocol can provide information about cells before they are displayed on screen.

@protocol UITableViewDataSourcePrefetching <NSObject>

@required

// indexPaths are ordered ascending by geometric distance from the table view
- (void)tableView:(UITableView *)tableView prefetchRowsAtIndexPaths:(NSArray<NSIndexPath *> *)indexPaths;

@optional

// indexPaths that previously were considered as candidates for pre-fetching, but were not actually used; may be a subset of the previous call to -tableView:prefetchRowsAtIndexPaths:
- (void)tableView:(UITableView *)tableView cancelPrefetchingForRowsAtIndexPaths:(NSArray<NSIndexPath *> *)indexPaths;

@end

第一个函数会基于当前滚动的方向和速度对接下来的 IndexPaths 进行 Prefetch,通常我们会在这里实现预加载数据的逻辑。

第二个函数是一个可选的方法,当用户快速滚动导致一些 Cell 不可见的时候,你可以通过这个方法来取消任何挂起的数据加载操作,有利于提高滚动性能, 在下面我会讲到。

实现这俩个函数的逻辑代码为:

extension ViewController: UITableViewDataSourcePrefetching {
// 翻页请求
func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
let needFetch = indexPaths.contains { $0.row >= viewModel.currentCount}
if needFetch {
// 1.满足条件进行翻页请求
indicatorView.startAnimating()
viewModel.fetchImages()
}

for indexPath in indexPaths {
if let _ = viewModel.loadingOperations[indexPath] {
return
}

if let dataloader = viewModel.loadImage(at: indexPath.row) {
print("在 \(indexPath.row) 行 对图片进行 prefetch ")
// 2 对需要下载的图片进行预热
viewModel.loadingQueue.addOperation(dataloader)
// 3 将该下载线程加入到记录数组中以便根据索引查找
viewModel.loadingOperations[indexPath] = dataloader
}
}
}

func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]){
// 该行在不需要显示的时候,取消 prefetch ,避免造成资源浪费
indexPaths.forEach {
if let dataLoader = viewModel.loadingOperations[$0] {
print("在 \($0.row) 行 cancelPrefetchingForRowsAt ")
dataLoader.cancel()
viewModel.loadingOperations.removeValue(forKey: $0)
}
}
}
}

最后,再加上俩个有用的方法该功能就大功告成了:

    // 用于计算 tableview 加载新数据时需要 reload 的 cell
func visibleIndexPathsToReload(intersecting indexPaths: [IndexPath]) -> [IndexPath] {
let indexPathsForVisibleRows = tableView.indexPathsForVisibleRows ?? []
let indexPathsIntersection = Set(indexPathsForVisibleRows).intersection(indexPaths)
return Array(indexPathsIntersection)
}

// 用于确定该索引的行是否超出了目前收到数据的最大数量
func isLoadingCell(for indexPath: IndexPath) -> Bool {
return indexPath.row >= (viewModel.currentCount)
}

见证时刻的奇迹到了,请看效果:




通过日志,我们也可以清楚的看到,在滚动的过程中是有 Prefetch 和 CancelPrefetch 操作的:


好了,到这里我就简单的实现了 UITableView 无尽滚动和对数据无缝加载的效果,你学会了吗?

如何避免滚动时的卡顿

当你遇到滚动卡顿的应用程序时,通常是由于任务长时间运行阻碍了 UI 在主线程上的更新,想让主线程有空来响应这类更新事件,第一步就是要将消耗时间的任务交给子线程去执行,避免在获取数据时阻塞主线程。

苹果提供了很多为应用程序实现并发的方式,例如 GCD,我在这里对 Cell 上的图片进行异步加载使用的就是它。

代码如下:


class DataLoadOperation: Operation {
var image: UIImage?
var loadingCompleteHandle: ((UIImage?) -> ())?
private var _image: ImageModel
private let cachedImages = NSCache<NSURL, UIImage>()

init(_ image: ImageModel) {
_image = image
}

public final func image(url: NSURL) -> UIImage? {
return cachedImages.object(forKey: url)
}

override func main() {
if isCancelled {
return
}

guard let url = _image.url else {
return
}
downloadImageFrom(url) { (image) in
DispatchQueue.main.async { [weak self] in
guard let ss = self else { return }
if ss.isCancelled { return }
ss.image = image
ss.loadingCompleteHandle?(ss.image)
}
}

}

// Returns the cached image if available, otherwise asynchronously loads and caches it.
func downloadImageFrom(_ url: NSURL, completeHandler: @escaping (UIImage?) -> ()) {
// Check for a cached image.
if let cachedImage = image(url: url) {
DispatchQueue.main.async {
print("命中缓存")
completeHandler(cachedImage)
}
return
}

URLSession.shared.dataTask(with: url as URL) { data, response, error in
guard
let httpURLResponse = response as? HTTPURLResponse, httpURLResponse.statusCode == 200,
let mimeType = response?.mimeType, mimeType.hasPrefix("image"),
let data = data, error == nil,
let _image = UIImage(data: data)
else { return }
// Cache the image.
self.cachedImages.setObject(_image, forKey: url, cost: data.count)
completeHandler(_image)
}.resume()
}
}

那具体如何使用呢!别急,听我娓娓道来,这里我再给大家一个小建议,大家都知道 UITableView 实例化 Cell 的方法是:tableView:cellForRowAtIndexPath: ,相信很多人都会在这个方法里面去进行数据绑定然后更新 UI,其实这样做是一种比较低效的行为,因为这个方法需要为每个 Cell 调用一次,它应该快速的执行并返回重用 Cell 的实例,不要在这里去执行数据绑定,因为目前在屏幕上还没有 Cell。我们可以在 tableView:willDisplayCell:forRowAtIndexPath: 这个方法中进行数据绑定,这个方法在显示cell之前会被调用。

为每个 Cell 执行下载任务的实现代码如下:

 func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: "PreloadCellID") as? ProloadTableViewCell else {
fatalError("Sorry, could not load cell")
}

if isLoadingCell(for: indexPath) {
cell.updateUI(.none, orderNo: "\(indexPath.row)")
}

return cell
}

func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
// preheat image ,处理将要显示的图像
guard let cell = cell as? ProloadTableViewCell else {
return
}

// 图片下载完毕后更新 cell
let updateCellClosure: (UIImage?) -> () = { [unowned self] (image) in
cell.updateUI(image, orderNo: "\(indexPath.row)")
viewModel.loadingOperations.removeValue(forKey: indexPath)
}

// 1\. 首先判断是否已经存在创建好的下载线程
if let dataLoader = viewModel.loadingOperations[indexPath] {
if let image = dataLoader.image {
// 1.1 若图片已经下载好,直接更新
cell.updateUI(image, orderNo: "\(indexPath.row)")
} else {
// 1.2 若图片还未下载好,则等待图片下载完后更新 cell
dataLoader.loadingCompleteHandle = updateCellClosure
}
} else {
// 2\. 没找到,则为指定的 url 创建一个新的下载线程
print("在 \(indexPath.row) 行创建一个新的图片下载线程")
if let dataloader = viewModel.loadImage(at: indexPath.row) {
// 2.1 添加图片下载完毕后的回调
dataloader.loadingCompleteHandle = updateCellClosure
// 2.2 启动下载
viewModel.loadingQueue.addOperation(dataloader)
// 2.3 将该下载线程加入到记录数组中以便根据索引查找
viewModel.loadingOperations[indexPath] = dataloader
}
}
}

对预加载的图片进行异步下载(预热):


func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
let needFetch = indexPaths.contains { $0.row >= viewModel.currentCount}
if needFetch {
// 1.满足条件进行翻页请求
indicatorView.startAnimating()
viewModel.fetchImages()
}

for indexPath in indexPaths {
if let _ = viewModel.loadingOperations[indexPath] {
return
}

if let dataloader = viewModel.loadImage(at: indexPath.row) {
print("在 \(indexPath.row) 行 对图片进行 prefetch ")
// 2 对需要下载的图片进行预热
viewModel.loadingQueue.addOperation(dataloader)
// 3 将该下载线程加入到记录数组中以便根据索引查找
viewModel.loadingOperations[indexPath] = dataloader
}
}
}

取消 Prefetch 时,cancel 任务,避免造成资源浪费

func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]){
// 该行在不需要显示的时候,取消 prefetch ,避免造成资源浪费
indexPaths.forEach {
if let dataLoader = viewModel.loadingOperations[$0] {
print("在 \($0.row) 行 cancelPrefetchingForRowsAt ")
dataLoader.cancel()
viewModel.loadingOperations.removeValue(forKey: $0)
}
}
}

经过这般处理,我们的 UITableView 滚动起来肯定是如丝般顺滑,小伙伴们还等什么,还不赶紧试一试。


图片缓存

虽然我在上面对我的应用增加了并发操作,但是我一看 Xcode 的性能分析,我不禁陷入了沉思,我的应用程序太吃内存了,假如我不停的刷,那我的手机应该迟早会把我的应用给终止掉,下图是我刷到 200 行的时候的性能分析图:

内存



可以看到我的应用的性能分析很不理想,究其原因在于我的应用里显示了大量的图片资源,每次来回滚动的时候,都会重新去下载新的图片,而没有对图片做缓存处理。

所以,针对这个问题,我为我的应用加入了缓存 NSCache 对象,来对图片做一个缓存,具体代码实现如下:


class ImageCache: NSObject {

private var cache = NSCache<AnyObject, UIImage>()
public static let shared = ImageCache()
private override init() {}

func getCache() -> NSCache<AnyObject, UIImage> {
return cache
}
}

在下载开始的时候,检查有没有命中缓存,如果命中则直接返回图片,否则重新下载图片,并添加到缓存中:

func downloadImageFrom(_ url: URL, completeHandler: @escaping (UIImage?) -> ()) {
// Check for a cached image.
if let cachedImage = getCacheImage(url: url as NSURL) {
print("命中缓存")
DispatchQueue.main.async {
completeHandler(cachedImage)
}
return
}

URLSession.shared.dataTask(with: url) { data, response, error in
guard
let httpURLResponse = response as? HTTPURLResponse, httpURLResponse.statusCode == 200,
let mimeType = response?.mimeType, mimeType.hasPrefix("image"),
let data = data, error == nil,
let _image = UIImage(data: data)
else { return }

// Cache the image.
ImageCache.shared.getCache().setObject(_image, forKey: url as NSURL)

completeHandler(_image)
}.resume()
}
有了缓存的加持后,再用 Xcode 来查看我的应用的性能,就会发现内存和磁盘的占用已经下降了很多:




关于图片缓存的技术,这里只是用了最简单的一种,外面很多开源的图片库都有不同的缓存策略,感兴趣的可以去 GitHub 上学习一下它们的源码,我在这里就不做赘述了。




作者:iOS鑫
链接:https://www.jianshu.com/p/f00f43e401da

收起阅读 »

iOS 对接系统“fileAPP” - 文件操作

1、前言iOS文件存储机制每个iOS程序都有一个独立的文件系统(存储空间),而且只能在对应的文件系统中进行操作,此区域被称为沙盒。应用必须待在自己的沙盒里,其他应用不能访问该沙盒。所有的非代码文件都要保存在此,例如属性文件plist、文本文件、图像、图标、媒体...
继续阅读 »

1、前言

iOS文件存储机制

每个iOS程序都有一个独立的文件系统(存储空间),而且只能在对应的文件系统中进行操作,此区域被称为沙盒。应用必须待在自己的沙盒里,其他应用不能访问该沙盒。所有的非代码文件都要保存在此,例如属性文件plist、文本文件、图像、图标、媒体资源等

沙盒中相关路径

AppName.app 应用程序的程序包目录,包含应用程序的本身。由于应用程序必须经过签名,所以不能在运行时对这个目录中的内容进行修改,否则会导致应用程序无法启动。

Documents/ 保存应用程序的重要数据文件和用户数据文件等。用户数据基本上都放在这个位置(例如从网上下载的图片或音乐文件),该文件夹在应用程序更新时会自动备份,在连接iTunes时也可以自动同步备份其中的数据。

Library:这个目录下有两个子目录,可创建子文件夹。可以用来放置您希望被备份但不希望被用户看到的数据。该路径下的文件夹,除Caches以外,都会被iTunes备份.

Library/Caches: 保存应用程序使用时产生的支持文件和缓存文件(保存应用程序再次启动过程中需要的信息),还有日志文件最好也放在这个目录。iTunes 同步时不会备份该目录并且可能被其他工具清理掉其中的数据。
Library/Preferences: 保存应用程序的偏好设置文件。NSUserDefaults类创建的数据和plist文件都放在这里。会被iTunes备份。

相关问题

因为每个app下的沙盒路径都是封闭的,其他应用访问不到,也就导致了文件不能共享。只能自己在代码中维护本app内的文件。这给用户带来很多的不便。因此,苹果除了一个系统的“文件”的app,用来管理app内的文件。但是系统的“文件”这个app不是可以管理全部的app下的文件,他只能管理对“文件”这个app开放权限的APP。

计划

最近在做IM开发,涉及到文件传输,我们不想做的特别封闭,所以就想可以发送app以外的文件(比如微信中保存下来的)具体实现就是:从微信里下载的文件保存到“ fileAPP”内,然后我们在发送文件时,可以发送本地文件,也可以发送“ fileAPP”内保存的文件

2、让自己的app对“ fileAPP”开放管理权限

2.1、在Identifiers下选择你要添加icloud的boundid把icloud配置勾选上


2.2、工程配置



点击Caoablity左侧的加号,搜索iCloud,然后添加即可

2.3、 设置info.plist

第一个是 UIFileSharingEnabled,这个可以使 iTunes 分享你文件夹内的内容;第二个是 LSSupportsOpeningDocumentsInPlace ,它保证了你文件夹内本地文件的获取权限,你需要将这两个键值对的值设置为 YES

以上设置完,重新运行app之后,在系统的“ fileAPP”中会出现一个以你的APP命名的文件夹,里面包含了APP内沙盒的目录和文件






但是这里面的文件很乱,而且会将沙盒内 Documents 文件夹内的所有文件都显示出来

2.4、如何在fileAPP里隐藏重要的文件?

如果它不是那么重要的,我们可以将它们存放在 cachesDirectory 或者是 temporaryDirectory 文件夹下面;如果它是重要的文件,大多数情况下,我们是需要将它们备份在 iCloud 上的,这样的文件我们建议将它存放在 applicationSupportDirectory 目录下




以上,是将自己的文件共享给 fileAPP

3、app如何获取到“文件”app下管理的文件到本app内

3.1、调起fileAPP 文件目录

//打开文件APP
- (void)presentDocumentCloud {
NSArray *documentTypes = @[@"public.content", @"public.text", @"public.source-code ", @"public.image", @"public.audiovisual-content", @"com.adobe.pdf", @"com.apple.keynote.key", @"com.microsoft.word.doc", @"com.microsoft.excel.xls", @"com.microsoft.powerpoint.ppt"];

UIDocumentPickerViewController *documentPickerViewController = [[UIDocumentPickerViewController alloc] initWithDocumentTypes:documentTypes inMode:UIDocumentPickerModeOpen];
documentPickerViewController.delegate = self;
[self presentViewController:documentPickerViewController animated:YES completion:nil];
}

这里的documentTypes数组内设置要拿的文件格式

UIDocumentPickerMode有四种:

typedef NS_ENUM(NSUInteger, UIDocumentPickerMode) {
UIDocumentPickerModeImport,
UIDocumentPickerModeOpen,
UIDocumentPickerModeExportToService,
UIDocumentPickerModeMoveToService
} API_DEPRECATED("Use appropriate initializers instead",ios(8.0,14.0)) API_UNAVAILABLE(tvOS);


UIDocumentPickerModeImport : 将文件拿出来UIDocumentPickerModeOpen:打开文件
后面是将文件传到fileAPP内的操作,类似于微信的保存文件到fileAPP内的操作。这里官方文档之给了前两个,后面的我们也没涉及到,以后研究吧。

3.2、设置代理
<UIDocumentPickerDelegate, UIDocumentInteractionControllerDelegate>
3.3、实现代理
#pragma mark - UIDocumentPickerDelegate
- (void)documentPicker:(UIDocumentPickerViewController *)controller didPickDocumentAtURL:(NSURL *)url {

BOOL fileUrlAuthozied = [url startAccessingSecurityScopedResource];
if (fileUrlAuthozied && [self iCloudEnable]) {
//通过文件协调工具来得到新的文件地址,以此得到文件保护功能
NSFileCoordinator *fileCoordinator = [[NSFileCoordinator alloc] init];
NSError *error;

[fileCoordinator coordinateReadingItemAtURL:url options:0 error:&error byAccessor:^(NSURL *newURL) {
//读取文件
NSString *fileName = [newURL lastPathComponent];
fileName = [fileName stringByRemovingPercentEncoding];
NSData * data = [NSData dataWithContentsOfURL:newURL];

self.completionBlock(data,fileName);
self.completionBlock = nil;
}];
[url stopAccessingSecurityScopedResource];
}
}

这里需要将拿到的url做处理,应为这里拿到的url是fileAPP文件的本地文件路径,在自己的app内通过[NSData dataWithContentsOfURL:url]方法是无法拿到数据的,应该是只能拿自己沙盒文件路径的设置的原因。

 [fileCoordinator coordinateReadingItemAtURL:url options:0 error:&error byAccessor:^(NSURL *newURL) {

}];

这么处理后,拿到的newUrl就是本地的路径了,通过[NSData dataWithContentsOfURL:url]即可拿到文件data。

之后即可对文件进行发送上传等操作。


作者:冰棍儿好烫嘴
链接:https://www.jianshu.com/p/19b1c0f124e2



收起阅读 »

基于环信MQTT消息云,Web客户端快速实现消息收发

仓库地址: https://gitee.com/yoki_ss_admin/task-1-web使用说明:实例化客户端client.connect();var topic = 'topic/chat'; if(clienct.isConnect){ ...
继续阅读 »

基于环信封装的mqtt的Web客户端,实现了基础的获取 token、服务器连接、消息订阅、取消订阅、消息发送、断开连接


仓库地址: https://gitee.com/yoki_ss_admin/task-1-web

使用说明:

  • 文件引用
  • 实例化客户端
  • 服务器连接
client.connect();
  • 订阅
var topic = 'topic/chat';
if(clienct.isConnect){
client.subscribe(topic);
}
  • 取消订阅
var topic = 'topic/chat';
if(clienct.isConnect){
client.unsubscribe(topic);
}
  • 消息发送
var topic = 'topic/chat';
var message = '这是我要发送的消息';
if(clienct.isConnect){
client.sendMessage(topic,message);
}
  • 连接断开
if(clienct.isConnect){
client.discounnect(topic);
}

运行截图

image

项目依赖

收起阅读 »

CameraX 入门食用方法

CameraX 已经发布了 1.0正式版 对于涉及到使用摄像头的 App , 能否充分利用摄像头有着很大的区别,为此 对 CameraX 进行了解与认知有一定的必要性.📑即将学会用 Jetpack 组件支持库 CameraX 创建相机、拍摄、预览⭕要求Goog...
继续阅读 »

CameraX 已经发布了 1.0正式版 对于涉及到使用摄像头的 App , 能否充分利用摄像头有着很大的区别,为此 对 CameraX 进行了解与认知有一定的必要性.

📑即将学会

用 Jetpack 组件支持库 CameraX 创建相机、拍摄、预览

⭕要求

Google官方声明 CameraX 是 Jetpack组件支持库,帮助我们简化相机相关应用的开发工作。并且提供了一致且易用的 API 接口,适用于大多数 Android 设备,并向后兼容至 Android 5.0(API 级别 21)

因此 创建Android应用的时候mininumSDK 需要选择5.0

前置内容

使用 Intent 进行拍照

在应用中拍照的最简便的方法便是使用MediaStore.ACTION_IMAGE_CAPTURE,传入Intent并启动

image.png 这会启动系统照相机,并为用户提供一套完整的拍照功能 这时,如果用户授予了用户拍照权限,并对拍照图片满意,我们将在onActivityResult中获取图片.默认情况下,拍摄的照片会以缩略图的形式返回,使用键data可以获得缩略图

Bundle extras = data.getExtras();
Bitmap imageBitmap = (Bitmap) extras.get("data");

而要获取完整图片,需要在启动相机的Intent中,在intent的意图中,添加MediaStore.EXTRA_OUTPUT 参数中设置文件的输出URI,该URI会存储完整的照片

image.png 存储后进行完整图片路径后的图片 并进行设置 该存储设置有一定的局限性,请参考官方对拍照的操作 拍照  |  Android 开发者  |  Android Developers (google.cn)image.png 这是我们日常中会使用到的一些摄像头获取图片的操作. 而在开发过程中,我们对camera2的API使用时还是会有很多脏代码,而CameraX作为Google推出的jetpack组件.简化了对Camera的开发,让我们看看如何使用CameraX

实战

创建项目

image.png

在 App 模块中添加 CameraX 依赖 并同步

image.png

使用 PreviewView 实现 CameraX 预览

1.修改布局文件 布局中添加 PreviewView

在布局文件中进行修改 image.png

Manifest声明相机权限
<uses-permission android:name="android.permission.CAMERA" />
动态申请相机权限
//覆写onRequestPermissionsResult()方法 
override fun onRequestPermissionsResult(
requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
if (requestCode == REQUEST_CODE_PERMISSIONS) {
if (allPermissionsGranted()) {
startCamera()
} else {
Toast.makeText(this,
"权限未得到用户授予",
Toast.LENGTH_SHORT).show()
finish()
}
}
2.请求 ProcessCameraProvider

ProcessCameraProvider是一个单例 用于将相机的生命周期绑定到应用程序进程中的任何 生命周期持有者。因为cameraX具有生命周期感应 这可以使我们的应用程序省去打开和关闭相机的任务

val cameraProviderFuture = ProcessCameraProvider.getInstance(this)
3.检查 ProcessCameraProvider 可用性
cameraProviderFuture.addListener(Runnable {}, ContextCompat.getMainExecutor(this))
4.ProcessCameraProvider可用后 选择相机并绑定生命周期和用例
  • 创建 Preview
  • 指定所需的相机
  • 将所选相机和任意用例绑定到生命周期
  • 将 Preview 连接到 PreviewView
 cameraProviderFuture.addListener(Runnable {
// 1将camera 绑定到 生命周期持有者
val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()

// 2创建 Preview。
val preview = Preview.Builder()
.build()
.also {
//preview 连接 previewView
it.setSurfaceProvider(findViewById<PreviewView>(R.id.viewFinder).getSurfaceProvider())
}

//3指定所需的相机
val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA

try {
// 4绑定前解绑
cameraProvider.unbindAll()

// 5绑定用户用例和相机到生命周期
cameraProvider.bindToLifecycle(
this, cameraSelector, preview)

} catch(exc: Exception) {
Log.e(TAG, "Use case binding failed", exc)
}

}, ContextCompat.getMainExecutor(this))

效果图

image.png

这是camera的预览功能

cameraX相片拍摄

配置应用拍摄图片
  • 利用 ImageCapture.Builder 构建 imageCapture
  • 绑定用户用例和相机到生命周期 多了一个 imageCapture
cameraProvider.bindToLifecycle(this, cameraSelector, preview, imageCapture)
拍照
  • 获取对 ImageCapture 用例的引用
  • 创建图片存储容纳文件
  • 创建OutputFileOptions指定输出方式 输出路径等内容
val outputFileOptions = ImageCapture.OutputFileOptions.Builder(File(...)).build()
  • 对 imageCapture 对象调用 takePicture()方法。传入刚才构建的 outputOptions 以及保存图片的回调

最终修改代码

cameraProviderFuture.addListener(Runnable {
// 1将camera 绑定到 生命周期持有者
val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()

// 2创建 Preview。
val preview = Preview.Builder()
.build()
.also {
//preview 连接 previewView
it.setSurfaceProvider(findViewById<PreviewView>(R.id.viewFinder).getSurfaceProvider())
}
//todo 新增行
//图像捕获 构建
imageCapture = ImageCapture.Builder()
.build()

//3指定所需的相机
val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA

try {
// 4绑定前解绑
cameraProvider.unbindAll()

//todo 新增修改行
// 5绑定用户用例和相机到生命周期
cameraProvider.bindToLifecycle(
this, cameraSelector, preview, imageCapture)

} catch(exc: Exception) {
Log.e(TAG, "Use case binding failed", exc)
}

}, ContextCompat.getMainExecutor(this))


private fun takePhoto() {
// 获取图像捕获用例
val imageCapture = imageCapture ?: return


// 创建存储文件对象
val photoFile = File(
outputDirectory,
SimpleDateFormat(FILENAME_FORMAT, Locale.CHINA
).format(System.currentTimeMillis()) + ".jpg")

// 输出条件构建
val outputOptions = ImageCapture.OutputFileOptions.Builder(photoFile).build()

// 拍照 传入输出条件 以及拍照回调
imageCapture.takePicture(
outputOptions, ContextCompat.getMainExecutor(this), object : ImageCapture.OnImageSavedCallback {
override fun onError(exc: ImageCaptureException) {
//异常打印
Log.e(TAG, "拍照失败: ${exc.message}", exc)
}

override fun onImageSaved(output: ImageCapture.OutputFileResults) {
val savedUri = Uri.fromFile(photoFile)
val msg = "拍照成功: $savedUri"
Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT).show()
Log.d(TAG, msg)
}
})
}

运行展示

image.png


收起阅读 »

RecyclerView列表动画

一 ItemAnimator的使用触发删除动画mDatas.remove(position); notifyItemRemoved(position) 触发添加动画mDatas.add(position,data); notifyItemInserted(p...
继续阅读 »

一 ItemAnimator的使用

触发删除动画

mDatas.remove(position);
notifyItemRemoved(position)

触发添加动画

mDatas.add(position,data);
notifyItemInserted(position)

触发改变动画

mDatas.set(position,newData);
notifyItemChanged(position)

使用简单动画

//RecyclerView.LayoutManager.java
public boolean supportsPredictiveItemAnimations() {
return false;
}

添加/删除/改变都是渐入渐出动画:

使用预测性动画

//RecyclerView.LayoutManager.java
public boolean supportsPredictiveItemAnimations() {
return true;
}

二 配合LayoutAnimation使用

RecyclerView和普通的ViewGroup一样支持LayoutAnimation.使用起来比较简单,设置RecyclerView的 animateLayoutChangeslayoutAnimation属性即可.

案例

layout_animation_recyclerview.gif
图片出处:Layout animations on RecyclerView

layoutanimation1.gif
图片出处:RecyclerView 与 LayoutAnimation 实现的进入动画(一 ): List

三 自定义RecyclerView列表动画

案例学习:自定义change动画

效果:

代码实现:

1.新建MyChangeAnimator类继承DefaultItemAnimator

private class MyChangeAnimator extends DefaultItemAnimator {
}

2.重写DefaultItemAnimatorcanReuseUpdatedViewHolder()方法.

@Override
public boolean canReuseUpdatedViewHolder(RecyclerView.ViewHolder viewHolder) {
//change动画在同一个ItemHolder上执行
return true;
}

3.新建ColorTextInfo类继承ItemHolderInfo,新增两个字段记录item的颜色和文字.

private class ColorTextInfo extends ItemHolderInfo {
int color;
String text;
}

4.重写DefaultItemAnimatorobtainHolderInfo()方法,新建一个ColorTextInfo对象返回.

@Override
public ItemHolderInfo obtainHolderInfo() {
return new ColorTextInfo();
}
  1. 重写DefaultItemAnimatorrecordPreLayoutInformation()recordPostLayoutInformation()方法.在item变化前和变化后记录颜色和文字信息.
public ItemHolderInfo recordPreLayoutInformation(RecyclerView.State state,
RecyclerView.ViewHolder viewHolder, int changeFlags, List<Object> payloads) {
ColorTextInfo info = (ColorTextInfo) super.recordPreLayoutInformation(state, viewHolder,changeFlags, payloads);
//记录item变化前的颜色和文字
return getItemHolderInfo((MyViewHolder) viewHolder, info);
}

public ItemHolderInfo recordPostLayoutInformation(@NonNull RecyclerView.State state,
@NonNull RecyclerView.ViewHolder viewHolder) {
ColorTextInfo info = (ColorTextInfo) super.recordPostLayoutInformation(state, viewHolder);
//记录item变化后的颜色和文字
return getItemHolderInfo((MyViewHolder) viewHolder, info);
}

//从viewHolder上获取ColorTextInfo信息
private ItemHolderInfo getItemHolderInfo(MyViewHolder viewHolder, ColorTextInfo info) {
final MyViewHolder myHolder = viewHolder;
final int bgColor = ((ColorDrawable) myHolder.container.getBackground()).getColor();
info.color = bgColor;
info.text = (String) myHolder.textView.getText();
return info;
}

6.重写DefaultItemAnimatoranimateChange()方法,执行自定义的change动画

顺序执行
顺序执行
开始change动画
旧的颜色到黑色的渐变
旧的文字绕x轴旋转0->90度
黑色到新的颜色的渐变
新的文字绕x轴旋转-90->0度
change动画结束
public boolean animateChange(@NonNull final RecyclerView.ViewHolder oldHolder,
@NonNull final RecyclerView.ViewHolder newHolder,
@NonNull ItemHolderInfo preInfo, @NonNull ItemHolderInfo postInfo) {

...

final MyViewHolder viewHolder = (MyViewHolder) newHolder;

//读取change前后的颜色和文字信息
ColorTextInfo oldInfo = (ColorTextInfo) preInfo;
ColorTextInfo newInfo = (ColorTextInfo) postInfo;
int oldColor = oldInfo.color;
int newColor = newInfo.color;
final String oldText = oldInfo.text;
final String newText = newInfo.text;

LinearLayout newContainer = viewHolder.container;
final TextView newTextView = viewHolder.textView;


// 构造旧的颜色到黑色的渐变动画
ObjectAnimator fadeToBlack = null, fadeFromBlack;
fadeToBlack = ObjectAnimator.ofInt(newContainer, "backgroundColor",
startColor, Color.BLACK);
fadeToBlack.setEvaluator(mColorEvaluator);

// 构造黑色到新的颜色的渐变动画
fadeFromBlack = ObjectAnimator.ofInt(newContainer, "backgroundColor",
Color.BLACK, newColor);

// 这两个渐变动画顺序执行
AnimatorSet bgAnim = new AnimatorSet();
bgAnim.playSequentially(fadeToBlack, fadeFromBlack);

//构造旧的文字绕x轴旋转0->90度的动画
ObjectAnimator oldTextRotate = null, newTextRotate;
    oldTextRotate = ObjectAnimator.ofFloat(newTextView, View.ROTATION_X, 0, 90);
    oldTextRotate.setInterpolator(mAccelerateInterpolator);
    oldTextRotate.addListener(new AnimatorListenerAdapter() {
     boolean mCanceled = false;
     @Override
     public void onAnimationStart(Animator animation) {
     //动画开始时显示旧的文字
     newTextView.setText(oldText);
     }

     @Override
     public void onAnimationCancel(Animator animation) {
     mCanceled = true;
     }

     @Override
     public void onAnimationEnd(Animator animation) {
     if (!mCanceled) {
     //动画结束时显示新的文字
     newTextView.setText(newText);
     }
     }
    });

// 构造新的文字绕x轴旋转-90->0度的动画
newTextRotate = ObjectAnimator.ofFloat(newTextView, View.ROTATION_X, -90, 0);
newTextRotate.setInterpolator(mDecelerateInterpolator);

//两个旋转动画顺序执行
AnimatorSet textAnim = new AnimatorSet();
textAnim.playSequentially(oldTextRotate, newTextRotate);

// 渐变的动画和旋转的动画同时执行
AnimatorSet changeAnim = new AnimatorSet();
changeAnim.playTogether(bgAnim, textAnim);
changeAnim.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
dispatchAnimationFinished(newHolder);
...
}
});
changeAnim.start();

return true;
}

7.设置RecyclerViewItemAnimator为自定义的MyChangeAnimator

mRecyclerView.setItemAnimator(mChangeAnimator);

这是简化后的代码.官方完整的demo里还包括了边界情况的处理,处理了上一次change动画没执行完时触发新的change动画的情况.
代码地址:github.com/android/vie…

自定义add和del动画

目标效果:

代码实现:

1.拷贝DefaultItemAnimator源码,命名为DefaultItemAnimatorOpen.java

2.重写添加和删除的动画实现.(这一步需要修改DefaultItemAnimatorOpen中一些方法和属性的可见性为protected).

参考DefaultItemAnimatorOpen中对应方法的实现,修改动画相关的代码即可:

class CustomAddDelAnimation : DefaultItemAnimatorOpen() {


override fun animateAdd(holder: RecyclerView.ViewHolder): Boolean {
//重写item添加的动画
resetAnimation(holder)
//将新增的item绕x轴旋转90度
holder.itemView.rotationX = 90f
mPendingAdditions.add(holder)
return true
}

override fun animateAddImpl(holder: RecyclerView.ViewHolder) {
//添加item时执行绕x轴90->0度的旋转
mAddAnimations.add(holder)
holder.itemView.animate().apply {
rotationX(0f)
duration = addDuration
interpolator = DecelerateInterpolator(3f)
setListener(object : AnimatorListenerAdapter() {

override fun onAnimationStart(animation: Animator?) {
dispatchAddStarting(holder)
}

override fun onAnimationCancel(animation: Animator?) {
ViewHelper.clear(holder.itemView)
}

override fun onAnimationEnd(animation: Animator) {
ViewHelper.clear(holder.itemView)
dispatchAddFinished(holder)
mAddAnimations.remove(holder)
dispatchFinishedWhenDone()
}
})
}.start()
}

override fun animateRemove(holder: RecyclerView.ViewHolder): Boolean {
//重写item删除的动画
resetAnimation(holder)
mPendingRemovals.add(holder)
return true
}

override fun animateRemoveImpl(holder: RecyclerView.ViewHolder) {
//删除item时执行绕x轴0->90度的旋转
mRemoveAnimations.add(holder)
holder.itemView.animate().apply {
rotationX(90f)
duration = addDuration
interpolator = DecelerateInterpolator(3f)
setListener(object : AnimatorListenerAdapter() {

override fun onAnimationStart(animation: Animator?) {
dispatchRemoveStarting(holder)
}

override fun onAnimationCancel(animation: Animator?) {
ViewHelper.clear(holder.itemView)
}

override fun onAnimationEnd(animation: Animator) {
ViewHelper.clear(holder.itemView)
dispatchRemoveFinished(holder)
mRemoveAnimations.remove(holder)
dispatchFinishedWhenDone()
}
})
}.start()
}

}

demo地址:HoopAndroidDemos

四 RecyclerView列表动画的原理简析

«abstract»ItemAnimator«interface»ItemAnimatorListener«interface»ItemAnimatorFinishedListenerItemHolderInfo«abstract»SimpleItemAnimatorDefaultItemAnimator依赖依赖依赖继承继承

默认可预测动画执行的顺序

执行删除动画
移动动画
改变动画
item动画执行结束
执行延迟的item动画(下一帧)
默认效果为渐出
默认效果为平移
默认效果为渐出+渐入
添加动画(默认效果为渐入)
重置状态

列表动画是立即执行吗?
animateXXX()方法返回true时,延迟到在下一帧率执行.返回false时立即执行.
DefaultItemAnimator的动画都是在下一帧执行.

列表动画的实现流程

自定义立即执行的change动画:

RecyclerViewItenAnimatordispatchLayoutStep1()recordPreLayoutInformation()dispatchLayoutStep3()recordPostLayoutInformation()animateChange()RecyclerViewItenAnimator

默认延迟执行的add动画:

RecyclerViewItenAnimatordispatchLayoutStep3()animateMove()animateAdd()postAnimationRunner()animateMoveImplanimateAddImplRecyclerViewItenAnimator

收起阅读 »

【开源库剖析】KOOM V1.0.5 源码解析

一、官方项目介绍1.1 描述:KOOM是快手性能优化团队在处理移动端OOM问题的过程中沉淀出的一套完整解决方案。其中Android Java内存部分在LeakCanary的基础上进行了大量优化,解决了线上内存监控的性能问题,在不影响用户体验的前提下线上采集内存...
继续阅读 »

一、官方项目介绍

1.1 描述:

KOOM是快手性能优化团队在处理移动端OOM问题的过程中沉淀出的一套完整解决方案。其中Android Java内存部分在LeakCanary的基础上进行了大量优化,解决了线上内存监控的性能问题,在不影响用户体验的前提下线上采集内存镜像并解析。从 2020 年春节后在快手主APP上线至今解决了大量OOM问题,其性能和稳定性经受住了海量用户与设备的考验,因此决定开源以回馈社区。

1.2 特点:

  • 比leakCanary更丰富的泄漏场景检测;
  • 比leakCanary更好的检测性能;
  • 功能全面的支持线上大规模部署的闭环监控系统;

1.3 KOOM框架

image.png

1.4 快手KOOM核心流程包括:

  • 配置下发决策;
  • 监控内存状态;
  • 采集内存镜像;
  • 解析镜像文件(以下简称hprof)生成报告并上传;
  • 问题聚合报警与分配跟进。

1.5 泄漏检测触发机制优化:

泄漏检测触发机制leakCanary做法是GC过后对象WeakReference一直不被加入 ReferenceQueue,它可能存在内存泄漏。这个过程会主动触发GC做确认,可能会造成用户可感知的卡顿,而KOOM采用内存阈值监控来触发镜像采集,将对象是否泄漏的判断延迟到了解析时,阈值监控只要在子线程定期获取关注的几个内存指标即可,性能损耗很低。

image.png

1.6 heap dump优化:

传统方案会冻结应用几秒,KOOM会fork新进程来执行dump操作,对父进程的正常执行没有影响。暂停虚拟机需要调用虚拟机的art::Dbg::SuspendVM函数,谷歌从Android 7.0开始对调用系统库做了限制,快手自研了kwai-linker组件,通过caller address替换和dl_iterate_phdr解析绕过了这一限制。

image.png

随机采集线上真实用户的内存镜像,普通dump和fork子进程dump阻塞用户使用的耗时如下:

image.png

而从官方给出的测试数据来看,效果似乎是非常好的。

二、官方demo演示

这里就直接跑下官方提供的koom-demo

点击按钮,经过dump heap -> heap analysis -> report cache/koom/report/三个流程(heap analysis时间会比较长,但是完全不影响应用的正常操作),最终在应用的cache/koom/report里生成json报告:

cepheus:/data/data/com.kwai.koom.demo/cache/koom/report # ls
2020-12-08_15-23-32.json

模拟一个最简单的单例CommonUtils持有LeakActivity实例的内存泄漏,看下json最终上报的内容是个啥:

{
   "analysisDone":true,
   "classInfos":[
       {
           "className":"android.app.Activity",
           "instanceCount":4,
           "leakInstanceCount":3
       },
       {
           "className":"android.app.Fragment",
           "instanceCount":4,
           "leakInstanceCount":3
       },
       {
           "className":"android.graphics.Bitmap",
           "instanceCount":115,
           "leakInstanceCount":0
       },
       {
           "className":"libcore.util.NativeAllocationRegistry",
           "instanceCount":1513,
           "leakInstanceCount":0
       },
       {
           "className":"android.view.Window",
           "instanceCount":4,
           "leakInstanceCount":0
       }
   ],
   "gcPaths":[
       {
           "gcRoot":"Local variable in native code",
           "instanceCount":1,
           "leakReason":"Activity Leak",
           "path":[
               {
                   "declaredClass":"java.lang.Thread",
                   "reference":"android.os.HandlerThread.contextClassLoader",
                   "referenceType":"INSTANCE_FIELD"
               },
               {
                   "declaredClass":"java.lang.ClassLoader",
                   "reference":"dalvik.system.PathClassLoader.runtimeInternalObjects",
                   "referenceType":"INSTANCE_FIELD"
               },
               {
                   "declaredClass":"",
                   "reference":"java.lang.Object[]",
                   "referenceType":"ARRAY_ENTRY"
               },
               {
                   "declaredClass":"com.kwai.koom.demo.CommonUtils",
                   "reference":"com.kwai.koom.demo.CommonUtils.context",
                   "referenceType":"STATIC_FIELD"
               },
               {
                   "reference":"com.kwai.koom.demo.LeakActivity",
                   "referenceType":"instance"
               }
           ],
           "signature":"378fc01daea06b6bb679bd61725affd163d026a8"
       }
   ],
   "runningInfo":{
       "analysisReason":"RIGHT_NOW",
       "appVersion":"1.0",
       "buildModel":"MI 9 Transparent Edition",
       "currentPage":"LeakActivity",
       "dumpReason":"MANUAL_TRIGGER",
       "jvmMax":512,
       "jvmUsed":2,
       "koomVersion":1,
       "manufacture":"Xiaomi",
       "nowTime":"2020-12-08_16-07-34",
       "pss":32,
       "rss":123,
       "sdkInt":29,
       "threadCount":17,
       "usageSeconds":40,
       "vss":5674
   }
}

这里主要分三个部分:类信息、gc引用路径、运行基本信息。这里从gcPaths中能看出LeakActivity被CommonUtils持有了引用。

框架使用这里参考官方接入文档即可,这里不赘述: github.com/KwaiAppTeam…

三、框架解析

3.1 类图

image.png

3.2 时序图

KOOM初始化流程

image.png

KOOM执行初始化方法,10秒延迟之后会在threadHandler子线程中先通过check状态判断是否开始工作,工作内容是先检查是不是有未完成分析的文件,如果有就就触发解析,没有则监控内存。

heap dump流程

image.png

HeapDumpTrigger

  • startTrack:监控自动触发dump hprof操作。开启内存监控,子线程5s触发一次检测,看当前是否满足触发heap dump的条件。条件是由一系列阀值组织,这部分后面详细分析。满足阀值后会通过监听回调给HeapDumpTrigger去执行trigger。
  • trigger:主动触发dump hprof操作。这里是fork子进程来处理的,这部分也到后面详细分析。dump完成之后通过监听回调触发HeapAnalysisTrigger.startTrack触发heap分析流程。

heap analysis流程

image.png

HeapAnalysisTrigger

  • startTrack 根据策略触发hprof文件分析。
  • trigger 直接触发hprof文件分析。由单独起进程的service来处理,工作内容主要分内存泄漏检测(activity/fragment/bitmap/window)和泄漏数据整理缓存为json文件以供上报。

四、核心源码解析

经过前面的分析,基本上对框架的使用和结构有了一个宏观了解,这部分就打算对一些实现细节进行简单分析。

4.1 内存监控触发dump规则

这里主要是研究HeapMonitor中isTrigger规则,每隔5S都会循环判断该触发条件。

com/kwai/koom/javaoom/monitor/HeapMonitor.java
@Override
public boolean isTrigger() {
  if (!started) {
   return false;
  }
  HeapStatus heapStatus = currentHeapStatus();
  if (heapStatus.isOverThreshold) {
   if (heapThreshold.ascending()) {
     if (lastHeapStatus == null || heapStatus.used >= lastHeapStatus.used) {
       currentTimes++;
     } else {
       currentTimes = 0;
     }
   } else {
     currentTimes++;
   }
  } else {
   currentTimes = 0;
  }
  lastHeapStatus = heapStatus;
  return currentTimes >= heapThreshold.overTimes();
}
private HeapStatus lastHeapStatus;
private HeapStatus currentHeapStatus() {
  HeapStatus heapStatus = new HeapStatus();
  heapStatus.max = Runtime.getRuntime().maxMemory();
  heapStatus.used = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory();
  heapStatus.isOverThreshold = 100.0f * heapStatus.used / heapStatus.max > heapThreshold.value();
  return heapStatus;
}

com/kwai/koom/javaoom/common/KConstants.java

public static class HeapThreshold {
  public static int VM_512_DEVICE = 510;
  public static int VM_256_DEVICE = 250;
  public static int VM_128_DEVICE = 128;
  public static float PERCENT_RATIO_IN_512_DEVICE = 80;
  public static float PERCENT_RATIO_IN_256_DEVICE = 85;
  public static float PERCENT_RATIO_IN_128_DEVICE = 90;

  public static float getDefaultPercentRation() {
   int maxMem = (int) (Runtime.getRuntime().maxMemory() / MB);
   if (maxMem >= VM_512_DEVICE) {
     return KConstants.HeapThreshold.PERCENT_RATIO_IN_512_DEVICE;
   } else if (maxMem >= VM_256_DEVICE) {
     return KConstants.HeapThreshold.PERCENT_RATIO_IN_256_DEVICE;
   } else if (maxMem >= VM_128_DEVICE) {
     return KConstants.HeapThreshold.PERCENT_RATIO_IN_128_DEVICE;
   }
   return KConstants.HeapThreshold.PERCENT_RATIO_IN_512_DEVICE;
  }

  public static int OVER_TIMES = 3;
  public static int POLL_INTERVAL = 5000;
}

这里就是针对不同内存大小做了不同的阀值比例:

  • 应用内存>512M 80%
  • 应用内存>256M 85%
  • 应用内存>128M 90%
  • 低于128M的默认按80%

应用已使用内存/最大内存超过该比例则会触发heapStatus.isOverThreshold。连续满足3次触发heap dump,但是这个过程会考虑内存增长性,3次范围内出现了使用内存下降或者使用内存/最大内存低于对应阀值了则清零。

因此规则总结为:3次满足>阀值条件且内存一直处于上升期才触发。这样能减少无效的dump。

4.2 fork进程执行dump操作实现

这里先对比下目前市面上三方框架的主流实现方案:

image.png

LeakCanaray、Matrix、Probe采用的方案:
直接执行Debug.dumpHprofData(),它执行过程会先挂起当前进程的所有线程,然后执行dump操作,生成完成hprof文件之后再唤醒所有线程。整个过程非常耗时,会带来比较明显的卡顿,因此这个痛点严重影响该功能带到线上环境。

KOOM采用的方案:
主进程fork子进程来处理hprof dump操作,主进程本身只有在fork 子进程过程会短暂的suspend VM, 之后耗时阻塞均发生在子进程内,对主进程完全不产生影响。suspend VM本身过程时间非常短,从测试结果来看完全可以忽略不计

接下来详细分析下fork dump方案的实现: 目前项目中默认使用ForkJvmHeapDumper来执行dump。

com/kwai/koom/javaoom/dump/ForkJvmHeapDumper.java
@Override
public boolean dump(String path) {
  boolean dumpRes = false;
  try {
   int pid = trySuspendVMThenFork();//暂停虚拟机,copy-on-write fork子进程
   if (pid == 0) {//子进程中
     Debug.dumpHprofData(path);//dump hprof
     exitProcess();//_exit(0) 退出进程
   } else {//父进程中
     resumeVM();//resume当前虚拟机
     dumpRes = waitDumping(pid);//waitpid异步等待pid进程结束
   }
  } catch (Exception e) {
   e.printStackTrace();
  }
  return dumpRes;
}

为什么需要先suspendVM然后再fork?
起初我理解主要是让fork前后的内存镜像保存一致性,但是对于内存泄漏来说这个造成的影响并不大,demo直接fork好像也没有什么问题,何况这块做了大量工作绕过Android N的限制去suspendVM肯定是有其必要性的。
最终才发现,单线程是没问题的,因为线程已经停了,demo加多线程dump会卡在suspendVM。因此需要先suspendVM,再fork,最后resumeVM。

好,确认工作流之后,来尝试实现:

native层:

这里fork、waitPid 、exitProcess都比较简单,直接忽略,重点看vm相关操作:

正常操作就应该是:

void *libHandle = dlopen("libart.so", RTLD_NOW);//打开libart.so, 拿到文件操作句柄
void *suspendVM = dlsym(libHandle, LIBART_DBG_SUSPEND);//获取suspendVM方法引用
void *resumeVM = dlsym(libHandle, LIBART_DBG_RESUME);//获取resumeVM方法引用
dlclose(libHandle);//关闭libart.so文件操作句柄

这在Android N以下的版本这么操作是OK了,但是谷歌从Android 7.0开始对调用系统库做了限制,基于此前提,快手自研了kwai-linker组件,通过caller address替换和dl_iterate_phdr解析绕过了这一限制,官方文档对限制说明,那么接下来就分析下KOOM是如何绕过此限制的。

源码参考:Android 9.0

/bionic/libdl/libdl.cpp
02__attribute__((__weak__))
103void* dlopen(const char* filename, int flag) {
104 const void* caller_addr = __builtin_return_address(0);//得到当前函数返回地址
105 return __loader_dlopen(filename, flag, caller_addr);
106}

/bionic/linker/dlfcn.cpp
152void* __loader_dlopen(const char* filename, int flags, const void* caller_addr) {
153 return dlopen_ext(filename, flags, nullptr, caller_addr);
154}

131static void* dlopen_ext(const char* filename,
132 int flags,
133 const android_dlextinfo* extinfo,
134 const void* caller_addr) {
135 ScopedPthreadMutexLocker locker(&g_dl_mutex);
136 g_linker_logger.ResetState();
137 void* result = do_dlopen(filename, flags, extinfo, caller_addr);//执行do_dlopen
138 if (result == nullptr) {
139 __bionic_format_dlerror("dlopen failed", linker_get_error_buffer());
140 return nullptr;
141 }
142 return result;
143}

/bionic/linker/linker.cpp
2049void* do_dlopen(const char* name, int flags,
2050 const android_dlextinfo* extinfo,
2051 const void* caller_addr) {
2052 std::string trace_prefix = std::string("dlopen: ") + (name == nullptr ? "(nullptr)" : name);
2053 ScopedTrace trace(trace_prefix.c_str());
2054 ScopedTrace loading_trace((trace_prefix + " - loading and linking").c_str());
2055 soinfo* const caller = find_containing_library(caller_addr);
2056 android_namespace_t* ns = get_caller_namespace(caller);
...
2141 return nullptr;
2142}

这里dlopen最终执行是通过__loader_dlopen,只不过默认会传入当前函数地址,这个地址其实就是做了caller address校验,如果检测出是三方地址则校验不通过,这里传入系统函数地址则能通过校验,例如dlerror。

那么KOOM的做法是:

大于N小于Q的Android版本

using __loader_dlopen_fn = void *(*)(const char *filename, int flag, void *address);
void *handle = ::dlopen("libdl.so", RTLD_NOW);//打开libel.so
//这里直接调用其__loader_dlopen方法,它与dlopen区别是可以传入caller address
auto __loader_dlopen = reinterpret_cast<__loader_dlopen_fn>(::dlsym(handle,"__loader_dlopen"));
__loader_dlopen(lib_name, flags, (void *) dlerror);//传入dlerror系统函数地址,保证caller address校验通过,绕过Android N限制。

Android Q及其以上的版本

因为Q引入了runtime namespace,因此__loader_dlopen返回的handle为nullptr

这里通过dl_iterate_phdr在当前进程中查询已加载的符合条件的动态库基对象地址。

int DlFcn::dl_iterate_callback(dl_phdr_info *info, size_t size, void *data) {
auto target = reinterpret_cast<dl_iterate_data *>(data);
if (info->dlpi_addr != 0 && strstr(info->dlpi_name, target->info_.dlpi_name)) {
target->info_.dlpi_addr = info->dlpi_addr;
target->info_.dlpi_phdr = info->dlpi_phdr;
target->info_.dlpi_phnum = info->dlpi_phnum;
// break iterate
return 1;
}

// continue iterate
return 0;
}

dl_iterate_data data{};
data.info_.dlpi_name = "libart.so";
dl_iterate_phdr(dl_iterate_callback, &data);
CHECKP(data.info_.dlpi_addr > 0)
handle = __loader_dlopen(lib_name, flags, (void *) data.info_.dlpi_addr);

这里dl_iterate_callback会回调当前进程所装载的每一个动态库,这里过滤出libart.so对应的地址:data.info_.dlpi_addr,再通过__loader_dlopen尝试打开libart.so。

附:

struct dl_phdr_info {
ElfW(Addr) dlpi_addr;//基对象地址
const ElfW(Phdr)* dlpi_phdr;//指针数组
ElfW(Half) dlpi_phnum;//
...
};

这便是快手自研的kwai-linker组件通过caller address替换和dl_iterate_phdr解析绕过Android 7.0对调用系统库做的限制的具体实现。也是fork dump方案的核心技术点。

4.3 内存泄漏检测实现

内存泄漏检测核心代码在于SuspicionLeaksFinder.find

public Pair<List<ApplicationLeak>, List<LibraryLeak>> find() {
  boolean indexed = buildIndex();
  if (!indexed) {
   return null;
  }
  initLeakDetectors();
  findLeaks();
  return findPath();
}

这里是针对Bitmap size做判断,超过768*1366这个size的认为泄漏。

另外,NativeAllocationRegistryLeakDetector和WindowLeakDetector两类还没做具体泄漏判断规则,不参与对象泄漏检测,只是做了统计。

总结: 整体看下来,KOOM有两个值得借鉴的点:

  • 1.触发内存泄漏检测,常规是watcher activity/fragment的onDestroy,而KOOM是定期轮询查看当前内存是否到达阀值;
  • 2.dump hprof,常规是对应进程dump,而KOOM是fork进程dump。

收起阅读 »

深入理解内存泄漏

一、JVM内存模型常见jvm内存模型,主要分为堆区,本地方法栈,虚拟机栈,程序计数器,和方法区。如下图所示: (1)程序计数器每个线程都会有自己私有的程序计数器(PC)。可以看作是当前线程所执行的字节码的行号指示器。 也可以理解为下一条将要执行的指令...
继续阅读 »

一、JVM内存模型

常见jvm内存模型,主要分为堆区,本地方法栈,虚拟机栈,程序计数器,和方法区。如下图所示: image.png

(1)程序计数器

每个线程都会有自己私有的程序计数器(PC)。可以看作是当前线程所执行的字节码的行号指示器。 也可以理解为下一条将要执行的指令的地址或者行号。字节码解释器就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、 循环、 跳转、 异常处理、 线程上下文切换,线程恢复时,都要依赖PC.

  • 如果线程正在执行的是一个Java方法,PC值为正在执行的虚拟机字节码指令的地址
  • 如果线程正在执行的是Native方法,PC值为空(未定义)

(2)虚拟机栈(VM Stack)

简介

VM Stack也是线程私有的区域。他是java方法执行时的字典:它里面记录了局部变量表、 操作数栈、 动态链接、 方法出口等信息。 **在《java虚拟机规范》一书中对这部分的描述如下:**栈帧( Frame)是用来存储数据和部分过程结果的数据结构,同时也被用来处理动态链接 (Dynamic Linking)、 方法返回值和异常分派( Dispatch Exception)。栈帧随着方法调用而创建,随着方法结束而销毁——无论方法是正常完成还是异常完成(抛出了在方法内未被捕获的异常)都算作方法结束。栈帧的存储空间分配在 Java 虚拟机栈( §2.5.5)之中,每一个栈帧都有自己的局部变量表( Local Variables, §2.6.1)、操作数栈( OperandStack, §2.6.2)和指向当前方法所属的类的运行时常量池( §2.5.5)的引用。 VM-Stack 说白了,VM Stack是一个栈,也是一块内存区域。 所以,他是有大小的。虽然有大小,但是一般而言,各种虚拟机的实现都支持动态扩展这部分内存。

  • 如果线程请求的栈深度太大,则抛出StackOverflowError
  • 如果动态扩展时没有足够的大小,则抛出OutOfMemoryError以下代码肯定会导致StackOverflowError:

StackOverflowError

public static void method() {
method();
}

public static void main(String[] args) {
method();
}

Exception in thread "main" java.lang.StackOverflowError
at xxx.xxx.xxx.method(JavaVMStackSOF.java:10)
复制代码

(3)本地方法栈

Java 虚拟机实现可能会使用到传统的栈(通常称之为“ C Stacks”)来支持 native 方法( 指使用 Java 以外的其他语言编写的方法)的执行,这个栈就是本地方法栈( Native MethodStack)。 VM Stack是为执行java方法服务的,此处的Native Method Stack是为执行本地方法服务的。 此处的本地方法指定是和具体的底层操作系统层面相关的接口调用了(这部分太高高级了,不想深究……)。 《java虚拟机规范》中没有对这部分做具体的规定。所以就由VM的实现者自由发挥了。 有的虚拟机(比如HotSpot)将VM Stack和Native Method Stack合二为一,所以VM的另一种内存区域图就如下面所示了: image.png

(4)方法区

方法区是由所有线程共享的内存区域。 方法区存储的大致内容如下:

  • 每一个类的结构信息
    • 运行时常量池( Runtime Constant Pool)
    • 字段和方法数据
    • 构造函数和普通方法的字节码内容
  • 类、实例、接口初始化时用到的特殊方法

以下是本人对《java虚拟机规范》一书中对方法区的介绍的总结:

  • 在虚拟机启动的时候被创建
  • 虽然方法区是堆的逻辑组成部分,但是简单的虚拟机实现可以选择在这个区域不实现垃圾收集
  • 不限定实现方法区的内存位置和编译代码的管理策略
  • 容量可以是固定大小的,也可以随着程序执行的需求动态扩展,并在不需要过多空间时自动收缩。
  • 方法区在实际内存空间中可以是不连续的
  • Java 虚拟机实现应当提供给程序员或者最终用户调节方法区初始容量的手段
    • 对于可以动态扩展和收缩方法区来说,则应当提供调节其最大、最小容量的手段
  • 如果方法区的内存空间不能满足内存分配请求,那 Java 虚拟机将抛出一个OutOfMemoryError 异常

(5)堆

简介

在 Java 虚拟机中,堆( Heap)是可供各条线程共享的运行时内存区域,也是供所有类实例和数组对象分配内存的区域。 以下是本人对《java虚拟机规范》一书中对Java堆的介绍的总结:

  • 在虚拟机启动的时候就被创建
  • 是所有线程共享的内存区域
  • 存储了被自动内存管理系统所管理的各种对象
  • Java 堆的容量可以是固定大小的,也可以随着程序执行的需求动态扩展,并在不需要过多空间时自动收缩
  • Java 堆所使用的内存不需要保证是连续的
  • 如果实际所需的堆超过了自动内存管理系统能提供的最大容量,那 Java 虚拟机将会抛出一个OutOfMemoryError 异常
  • 实现者应当提供给程序员或者最终用户调节 Java 堆初始容量的手段
  • 所有的对象实例以及数组都要在堆上分配
  • 至于堆内存的详细情况,将在后续的GC相关文章中介绍。

堆内存中的OutOfMemoryError以下示例代码肯定导致堆内存溢出:

public static void main(String[] args) {
   ArrayList<Integer> list = Lists.newArrayList();
   while (true) {
       list.add(1);
  }
}
复制代码

无限制的往list中添加元素,无论你的堆内存分配的多大,都会有溢出的时候。

java.lang.OutOfMemoryError: Java heap space
复制代码

二、内存优化工具

(1)LeakCanary

介绍

leakCanary这是一个集成方便, 使用便捷,配置超级简单的框架,实现的功能却是极为强大的线下内存检测工具。 screenshot-2.0.png

如何使用

dependencies {
// debugImplementation because LeakCanary should only run in debug builds.
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.7'
}
复制代码

在项目集成之后,在Android Studio Logcat日志通过筛选LeakCanary可以看到如下日志,标志LeakCanary已经安装成功,并且已经启动。 image.png

内存泄漏

我们经常会用到很多单例的工具类,往往这些单例也是经常容易发生内存泄漏的地方。下面我们模拟一下单例工具类造成内存泄漏的情况,封装一个ToastUtils的单例类,内部持有context的引用,这样在页面销毁之后依然持有context的引用,造成无法销毁,从而造成内存泄漏。

object ToastUtils {
private var context: Context? = null

fun toast(context: Context, text: String) {
this.context =context
Toast.makeText(context, text, Toast.LENGTH_LONG)
}
}
复制代码

在控制台和手机上都可以看到内存泄漏的信息。如下所示: image.pngimage.png 手机上可以明显看到内存泄漏的列表,通过点击item即可看到详细的堆栈信息。

====================================
1 APPLICATION LEAKS

References underlined with "~~~" are likely causes.
Learn more at https://squ.re/leaks.

76243 bytes retained by leaking objects
Signature: 409f986871fac1acf6527c76b4d658d03ffa8e11
┬───
│ GC Root: Local variable in native code

├─ android.os.HandlerThread instance
│ Leaking: NO (PathClassLoader↓ is not leaking)
│ Thread name: 'LeakCanary-Heap-Dump'
│ ↓ Thread.contextClassLoader
├─ dalvik.system.PathClassLoader instance
│ Leaking: NO (ToastUtils↓ is not leaking and A ClassLoader is never leaking)
│ ↓ ClassLoader.runtimeInternalObjects
├─ java.lang.Object[] array
│ Leaking: NO (ToastUtils↓ is not leaking)
│ ↓ Object[].[620]
├─ com.caichen.article_caichen.ToastUtils class
Leaking: NO (a class is never leaking)
│ ↓ static ToastUtils.context
│ ~~~~~~~
╰→ com.caichen.article_caichen.LeakActivity instance
Leaking: YES (ObjectWatcher was watching this because com.caichen.article_caichen.LeakActivity received
Activity#onDestroy() callback and Activity#mDestroyed is true)
Retaining 76.2 kB in 1135 objects
key = 42b604fb-b746-48f5-8ff5-f56cd01a570e
watchDurationMillis = 5204
retainedDurationMillis = 190
mApplication instance of android.app.Application
mBase instance of androidx.appcompat.view.ContextThemeWrapper
====================================
复制代码

同时通过控制台也可以看到详细的信息。

如何分析

    ├─ com.caichen.article_caichen.ToastUtils class
Leaking: NO (a class is never leaking)
│ ↓ static ToastUtils.context
│ ~~~~~~~
╰→ com.caichen.article_caichen.LeakActivity instance
复制代码

通过堆栈信息,大致会得到内存泄漏的大致引用调用路径,最终定位到ToastUtils类中,存在内存泄漏的地方就是内部的context变量,因为被单例对象所持有那么他引用的context和单例的生命周期相同。所以当页面消失时,无法被垃圾回收器销毁从而造成内存泄漏。

(2)Profile Memory

介绍

Profile Memory是 Android Profiler 中的一个组件,可帮助您识别可能会导致应用卡顿、冻结甚至崩溃的内存泄漏和内存抖动。它显示一个应用内存使用量的实时图表,让您可以捕获堆转储、强制执行垃圾回收以及跟踪内存分配。

如何使用

如需打开内存性能分析器,请按以下步骤操作:

  1. 依次点击 View > Tool Windows > Profiler(您也可以点击工具栏中的 Profile 图标 )。
  2. 从 Android Profiler 工具栏中选择要分析的设备和应用进程。如果您已通过 USB 连接设备但系统未列出该设备,请确保您已启用 USB 调试
  3. 点击 MEMORY 时间轴上的任意位置以打开内存性能分析器。

如图所示: image.png 点击Memory选项可以进入内存性能分析器界面, image.png 右上角分别展示出jvm对应的内存的情况。 内存计数中的类别如下:

  • Total:内存占用的总和值
  • Java:从 Java 或 Kotlin 代码分配的对象的内存。
  • Native:从 C 或 C++ 代码分配的对象的内存。即使您的应用中不使用 C++,您也可能会看到此处使用了一些原生内存,因为即使您编写的代码采用 Java 或 Kotlin 语言,Android 框架仍使用原生内存代表您处理各种任务,如处理图像资源和其他图形。
  • Graphics:图形缓冲区队列为向屏幕显示像素(包括 GL 表面、GL 纹理等等)所使用的内存。(请注意,这是与 CPU 共享的内存,不是 GPU 专用内存。)
  • Stack:您的应用中的原生堆栈和 Java 堆栈使用的内存。这通常与您的应用运行多少线程有关。
  • Code:您的应用用于处理代码和资源(如 dex 字节码、经过优化或编译的 dex 代码、.so 库和字体)的内存。
  • Others:您的应用使用的系统不确定如何分类的内存。
  • Allocated:您的应用分配的 Java/Kotlin 对象数。此数字没有计入 C 或 C++ 中分配的对象。如果连接到搭载 Android 7.1 及更低版本的设备,只有在内存性能分析器连接到您运行的应用时,才开始此分配计数。因此,您开始分析之前分配的任何对象都不会被计入。但是,Android 8.0 及更高版本附带一个设备内置性能剖析工具,该工具可跟踪所有分配,因此,在 Android 8.0 及更高版本上,此数字始终表示您的应用中待处理的 Java 对象总数。

内存抖动

什么是内存抖动,内存抖动就是内存在短暂时间频繁的GC和分配内存,导致内存不稳定。体现在profile中就是内存曲线呈现锯齿状的形状。 image.png

影响

  1. 频繁的创建对象造成内存碎片和不足
  2. 碎片的内存无法被分配从而容易导致内存溢出
  3. 频繁GC导致应用性能下降

如何分析

  1. 通过点击Record然后可以记录出内存片的内存信息

image.png

  1. 通过分析可以看出,string,char,int和StringBuilder都是占用内存比较多的对象
  2. 分析源码可以看出,在handleMessage中不断的进行创建intArray的对象,然后handlerMessage执行完毕之后创建的对象进行销毁,所以内存曲线呈现出来的形状呈锯齿状。
  3. 其中可以看出int占用的内存并没有string占有的内存大,那是因为通过log日志打印的字符串,通过StringBuilder进行了拼接,所以string和stringBuilder占用的内存比Int占用的内存还要大。
private fun initHandler() {
handler = Handler(Looper.getMainLooper(), Handler.Callback {
for (index in 0..1000) {
val asc = IntArray(100000) { 0 }
Log.d("YI", "$asc")
}
handler?.sendEmptyMessageDelayed(0, 300)
true
})

handler?.sendEmptyMessageDelayed(0, 300)
}
复制代码

(3)Memory Analyzer

介绍

Eclipse Memory Analysis Tools (MAT) 是一个分析 Java堆数据的专业工具,用它可以定位内存泄漏的原因。

如何使用

  1. 通过As导出的Heap文件不能直接使用需要通过SDK/platform-tools中的Hprof可执行文件将androidhea文件进行转化。
  2. 打开MAT程序,通过Open heap打开文件。

image.pngimage.png

如何分析

  1. 通过筛选类进行查看实例

image.png

  1. 通过包名进行查看实例

image.png 图中1处可可以指定分类的方式,图中2处可以找到自己包名下面的类,图中3处可以看到所有的包名下的实例和个数。可以看出图中LeakActivity的实例的个数11个,我们可以大致猜测出这个Activity是泄漏了。 接着我们查看Dominator Tree然后可以看出,罗列出leakActivity的所有的实例。 image.png 右键选中,然后点击Merge Shortest Paths to GC Roots可以看到引用路径。LeakActivity的引用呗Toastutils所持有。然后通过检查源码,可以看出内部context持有外部的引用,ToastUtils又是单例造成了activity的泄漏。 image.png


收起阅读 »

轻量级APP启动信息构建方案

背景在头条的启动框架下,启动任务已经划分的较为明确,而启动时序是启动任务中的关键信息。目前我们获取这些信息的主要手段是看systrace,但直接读systrace存在一些问题:systrace在release下一些信息不全,例如IO线程信息,而启动优化的主要评...
继续阅读 »

背景

在头条的启动框架下,启动任务已经划分的较为明确,而启动时序是启动任务中的关键信息。目前我们获取这些信息的主要手段是看systrace,但直接读systrace存在一些问题:

  • systrace在release下一些信息不全,例如IO线程信息,而启动优化的主要评估场景是release
  • systrace信息相对较重,可阅读性差,同时对启动任务的阅读的干扰性大

在上述问题的影响下,会增加开发人员排查、验证启动任务问题,以及优化启动任务的难度。

因此本文考虑设计一个轻量级的信息描述、收集与信息重建方案,灵活适应release模式与debug模式,同时增加可阅读性,降低开发人员排查问题的成本。

1 方案设计

轻量级启动信息构建方案主要由三部分组成:

  • 启动信息构建:负责提炼关键信息做成新数据结构
  • 启动信息收集:负责收集、输出各个任务的信息到重建模块
  • 启动信息重建:负责信息构建、输出可视化图形

2 具体模块实现

2.1 启动信息构建

data class InitDataStruct(
var startTime: Long = 0,
var duration: Long = 0,
var currentProcess: String = "",
var taskName: String = ""
)
复制代码

关键的启动信息主要有这么几个维度:

  • 启动时间(归一化)
  • 启动耗时
  • 启动线程
  • 启动名称

而并不关心,即需要剔除掉的任务:

  • 非启动任务信息(这并不是说它不重要,只是在启动框架这一环它并不是高优)
  • 启动任务stack

Format形如

{"task_name":"class com.xxx.xxxTask","start_time":5,"duration":9,"current_process":"AA xxxThread#4"}
复制代码

2.2 启动信息收集

由于没接入公司平台(太小),因此考虑就以log的方式输出结果。

大概是希望实现下面的功能,但一个一个加就有点复制粘贴有点太low了

调研了一下有一种AspectJ的做法,可以利用

@PointCut("execution(* com.xxx.xxx.xxxTask.run(*))")

在task周围埋下切入点

利用@Before@After注入切入代码即可。

2.3 启动信息收集与绘制

由于目前是依赖人工进行启动分析,因此我们收集启动信息的手段依赖于Console打印的日志,形如

{"task_name":"class com.xxx.Task","start_time":0,"duration":2,"current_process":"main"}
复制代码

这里我们直接写个读取工具给他转义一下,让他变成具有可读性的数据结构

# 在Client中以json保存下来的
def toInitInfo(json):
return InitInfo(json["start_time"], json["duration"], json["current_process"], str(json["task_name"]).split('.')[-1])

class InitInfo:
#startTime和duration均做了归一化
def __init__(self, startTime, duration, currentProcessName, taskName):
self.startTime = startTime
self.taskName = taskName
self.duration = duration
self.currentProcessName = currentProcessName

def printitself(self):
print("task_name : " + self.taskName)
print("\tstartTime : " + str(self.startTime))
print("\tduration : " + str(self.duration))
print("\tcurrentProcessName : " + self.currentProcessName)

# 获取task时长
def getNameCombineDuration(self):
return self.taskName + " " + str(self.duration)

# 获取当前打印的最大长度
def getConstructLen(self):
return len(self.getNameCombineDuration()) + 2

def generateFormatStr(self, perTime, perBlank):
totalLen = max(3, int(1.0 * perBlank * max(1, self.duration) / perTime))
cntLen = max(0, totalLen - self.getConstructLen())
strr = "|" + (cntLen / 2 + cntLen % 2) * "-" + self.getNameCombineDuration()[0:min(totalLen - 2, len(self.getNameCombineDuration()))]+ cntLen / 2 * "-" + "|"
return strr

def generateBlank(self, timeNow, perTime, perBlank):
strr = max(0, int((self.startTime - timeNow) / perTime) * perBlank) * " "
return strr
复制代码

并将所有task插入到list中,以完成时间作为sort Function

def sortByEnd(initInfo1, initInfo2):
return (initInfo1.startTime + initInfo1.duration) <= (initInfo2.startTime + initInfo2.duration)

def dealWithList():
for item in line_jsons:
if(taskMap.has_key(item.currentProcessName)):
taskMap[item.currentProcessName].append(item)
else:
taskMap[item.currentProcessName] = []
taskMap[item.currentProcessName].append(item)
复制代码

现在到了问题的核心,我们该采用什么规则把绘图绘制出来,这取决于我们需要得到的信息有哪些:

  • 第一种:分析启动任务耗时,可采用类似systrace,横轴为固定的单位时间长度,纵轴是currentProcess
def drawMp():
duraLen = 0
maxLen = 0

# 10ms间隔
currentPerTime = 10
endFile = open("timeline.txt","w")

# 先保证起始坐标轴一致
for key in taskMap.keys():
maxLen = max(maxLen, len(key))

# 计算最长字符串

for item in line_jsons:
duraLen = max(duraLen, item.getConstructLen())

# 画个坐标轴
xplot = maxLen * " " + " :"
for index in range(0, (line_jsons[-1].startTime + line_jsons[-1].duration) / currentPerTime):
cntLen = duraLen - 2 - len(str(index * currentPerTime))
xplot += "|" + (cntLen / 2 + cntLen % 2) * "-" + str(index * currentPerTime) + cntLen / 2 * "-" + "|"

endFile.write(xplot + "\n")

# 画图
for key in taskMap.keys():
strr = key + (maxLen - len(key)) * " " + " :"
timeNow = 0
for item in taskMap[key]:
item.printitself()
strr += item.generateBlank(timeNow, perTime = currentPerTime, perBlank = duraLen)
strr += item.generateFormatStr(10, duraLen)
timeNow = item.startTime + item.duration

strr += "\n"
endFile.write(strr)

endFile.close()
复制代码
  • 第二种:分析启动任务排布的合理性,即是否存在长尾型的启动路径,这里考虑横轴为离散化后的启动任务时间,纵轴为currentProcess
## 第二种画图法:离散

# 离散点阵图
duraCordi = []

def drawMp2():
# 离散单位区间长度
duraLen = 0

def addBlank(st, ed):
return (ed - st) * duraLen * " "

def formatString(st, ed, taskName, duraLen):
strr = "|"
leftBlank = (ed - st) * duraLen - 2 - len(taskName)
strr += (leftBlank / 2 + leftBlank % 2) * "-"
strr += taskName
strr += leftBlank / 2 * "-" + "|"
return strr

# 先离散
# 最短是 -> |maxLen(xxxTask)|
dura = []
filee = open("timeline2.txt","w")
for item in line_jsons:
duraLen = max(duraLen, len(item.getNameCombineDuration()) + 2)
dura.append(item.startTime)
dura.append(item.startTime + item.duration)

duraCordi = list(set(dura))
duraCordi.sort()
print(duraCordi)

#再遍历塞值进去
maxLen = 0
for key in taskMap.keys():
maxLen = max(maxLen, len(key))

for key in taskMap.keys():
currentIndex = 0
strr = key + (maxLen - len(key)) * " " + " :"
for item in taskMap[key]:
stIndex = bisect.bisect_left(duraCordi, item.startTime)
edIndex = bisect.bisect_left(duraCordi, item.startTime + max(item.duration, 1))
strr += addBlank(currentIndex, stIndex)
strr += formatString(stIndex, edIndex, item.getNameCombineDuration(), duraLen = duraLen)
currentIndex = edIndex

strr += "\n"
filee.write(strr)

filee.close()
复制代码

3 效果对比

  • 第一种启动耗时为单位的

  • 第二种启动时间离散化后的

比如我们需要分析启动任务的排布是否合理,就可以看第二种图像,可以看到主线程启动任务较多,可能存在一定的长尾效应。

相比systrace,更为轻量

收起阅读 »

JetpackMVVM七宗罪之二:在launchWhenX中启动协程

首先承认这个系列有点标题党,Jetpack 的 MVVM 本身没有错,错在开发者的某些使用不当。本系列将分享那些 AAC 中常见的错误用法,帮助大家打造更健康的应用架构 Flow vs LiveData 自 StateFlow/ SharedFlow ...
继续阅读 »




首先承认这个系列有点标题党,Jetpack 的 MVVM 本身没有错,错在开发者的某些使用不当。本系列将分享那些 AAC 中常见的错误用法,帮助大家打造更健康的应用架构



Flow vs LiveData


自 StateFlow/ SharedFlow 出现后, 官方开始推荐在 MVVM 中使用 Flow 替换 LiveData。 ( 见文章:从 LiveData 迁移到 Kotlin 数据流 )


Flow 基于协程实现,具有丰富的操作符,通过这些操作符可以实现线程切换、处理流式数据,相比 LiveData 功能更加强大。 但唯有一点不足,无法像 LiveData 那样感知生命周期。


感知生命周期为 LiveData 至少带来以下两个好处:




  1. 避免泄漏:当 lifecycleOwner 进入 DESTROYED 时,会自动删除 Observer

  2. 节省资源:当 lifecycleOwner 进入 STARTED 时才开始接受数据,避免 UI 处于后台时的无效计算。



Flow 也需要做到上面两点,才能真正地替代 LiveData。


lifecycleScope


lifecycle-runtime-ktx 库提供了 lifecycleOwner.lifecycleScope 扩展,可以在当前 Activity 或 Fragment 销毁时结束此协程,防止泄露。


Flow 也是运行在协程中的,lifecycleScope 可以帮助 Flow 解决内存泄露的问题:


lifecycleScope.launch {
viewMode.stateFlow.collect {
updateUI(it)
}
}

虽然解决了内存泄漏问题, 但是 lifecycleScope.launch 会立即启动协程,之后一直运行直到协程销毁,无法像 LiveData 仅当 UI 处于前台才执行,对资源的浪费比较大。


因此,lifecycle-runtime-ktx 又为我们提供了 LaunchWhenStartedLaunchWhenResumed ( 下文统称为 LaunchWhenX


launchWhenX 的利与弊


LaunchWhenX 会在 lifecycleOwner 进入 X 状态之前一直等待,又在离开 X 状态时挂起协程。 lifecycleScope + launchWhenX 的组合终于使 Flow 有了与 LiveData 相媲美的生命周期可感知能力:




  1. 避免泄露:当 lifecycleOwner 进入 DESTROYED 时, lifecycleScope 结束协程

  2. 节省资源:当 lifecycleOwner 进入 STARTED/RESUMED 时 launchWhenX 恢复执行,否则挂起。



但对于 launchWhenX 来说, 当 lifecycleOwner 离开 X 状态时,协程只是挂起协程而非销毁,如果用这个协程来订阅 Flow,就意味着虽然 Flow 的收集暂停了,但是上游的处理仍在继续,资源浪费的问题解决地不够彻底。



资源浪费


举一个资源浪费的例子,加深理解


fun FusedLocationProviderClient.locationFlow() = callbackFlow<Location> {
val callback = object : LocationCallback() {
override fun onLocationResult(result: LocationResult?) {
result ?: return
try { offer(result.lastLocation) } catch(e: Exception) {}
}
}
// 持续获取最新地理位置
requestLocationUpdates(
createLocationRequest(), callback, Looper.getMainLooper())

}

如上,使用 callbackFlow 封装了一个 GoogleMap 中获取位置的服务,requestLocationUpdates 实时获取最新位置,并通过 Flow 返回


class LocationActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

// 进入 STATED 时,collect 开始接收数据
// 进入 STOPED 时,collect 挂起
lifecycleScope.launchWhenStarted {
locationProvider.locationFlow().collect {
// Update the UI
}
}
}
}

LocationActivity 进入 STOPED 时, lifecycleScope.launchWhenStarted 挂起,停止接受 Flow 的数据,UI 也随之停止更新。但是 callbackFlow 中的 requestLocationUpdates 仍然还在持续,造成资源的浪费。


因此,即使在 launchWhenX 中订阅 Flow 仍然是不够的,无法完全避免资源的浪费


解决办法:repeatOnLifecycle


lifecycle-runtime-ktx 自 2.4.0-alpha01 起,提供了一个新的协程构造器 lifecyle.repeatOnLifecycle, 它在离开 X 状态时销毁协程,再进入 X 状态时再启动协程。从其命名上也可以直观地认识这一点,即围绕某生命周期的进出反复启动新协程



使用 repeatOnLifecycle 可以弥补上述 launchWhenX 对协程仅挂起而不销毁的弊端。因此,正确订阅 Flow 的写法应该如下(以在 Fragment 中为例):


onCreateView(...) {
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.lifecycle.repeatOnLifecycle(STARTED) {
viewMode.stateFlow.collect { ... }
}
}
}

当 Fragment 处于 STARTED 状态时会开始收集数据,并且在 RESUMED 状态时保持收集,最终在 Fragment 进入 STOPPED 状态时结束收集过程。


需要注意 repeatOnLifecycle 本身是个挂起函数,一旦被调用,将走不到后续代码,除非 lifecycle 进入 DESTROYED。


冷流 or 热流


顺道提一点,前面举得地图SDK的例子是个冷流的例子,对于热流(StateFlow/SharedFlow)是否有必要使用 repeatOnLifecycle 呢? 个人认为热流的使用场景中,像前面例子那样的情况会少一些,但是在 StateFlow/SharedFlow 的实现中,需要为每个 FlowCollector 分配一些资源,如果 FlowCollector 能即使销毁也是有利的,同时为了保持写法的统一,无论冷流热流都建议使用 repeatOnLifecycle


最后:Flow.flowWithLifecycle


当我们只有一个 Flow 需要收集时,可以使用 flowWithLifecycle 这样一个 Flow 操作符的形式来简化代码


lifecycleScope.launch {
viewMode.stateFlow
.flowWithLifecycle(this, Lifecycle.State.STARTED)
.collect { ... }
}

当然,其本质还是对 repeatOnLifecycle 的封装:


public fun <T> Flow<T>.flowWithLifecycle(
lifecycle: Lifecycle,
minActiveState: Lifecycle.State = Lifecycle.State.STARTED
)
: Flow<T> = callbackFlow {
lifecycle.repeatOnLifecycle(minActiveState) {
this@flowWithLifecycle.collect {
send(it)
}
}
close()
}


收起阅读 »

客户端网络优化(一)-原理篇

0x01 前言 网络优化是客户端技术方向中公认的一个深度领域,对于 App 性能和用户体验至关重要。本文除了 DNS 、连接和带宽方面的优化技术外,会结合着优化的一些实践,以及在成本和收益的衡量,会有区别于市面上其他的分享,希望对大家有所帮助。 为什么优化...
继续阅读 »

0x01 前言


网络优化是客户端技术方向中公认的一个深度领域,对于 App 性能和用户体验至关重要。本文除了 DNS 、连接和带宽方面的优化技术外,会结合着优化的一些实践,以及在成本和收益的衡量,会有区别于市面上其他的分享,希望对大家有所帮助。


为什么优化


肯定有同学会有疑问,网络请求不就是通过网络框架 SDK 去服务端请求数据吗,尤其在这个性能过剩的年代,时间都不会差多少,我们还有没有必要再去抠细节做优化了,废话不多说咱们直接看数据,来证明优化的价值



  • BBC 发现网站加载时间每延长 1 秒 用户便会流失 10%


  • Google 发现页面加载时间超过 353% 的用户将停止访问


  • Amazon 发现加载时间每延长 1 秒一年就会减少 16 亿美元的营收



如何优化


想知道如何优化,首先我们需要先确定优化方向:



  • 提高成功率

  • 减少请求耗时

  • 减少网络带宽


接下来我们了解下 https 网络连接流程,如下图:


connection.png


从上图我们能清晰的看出 https 连接经历了以下几个流程:



  • DNS Query: 1 个 RTT

  • TCP 需要经历 SYNSYN/ACKACK 三次握手1.5个RTT,不过 ACKClientHello 合并了: 1 个 RTT

  • TLS 需要经过握手和密钥交换: 2 个 RTT

  • Application Data 数据传输


综上所述,一个 https 连接需要 4 个 RTT 才到数据传输阶段,如果我们能减少建连的 RTT 次数,或者降低数据传输量,会对网络的稳定和速度带来很大的提升。


0x02 DNS 优化



  • DNS & HttpDNS



DNS(Domain Name System,域名系统),DNS 用于用户在网络请求时,根据域名查出 IP 地址,使用户更方便的访问互联网。传统DNS面临DNS缓存、解析转发、DNS攻击等问题,具体的DNS流程如下图所示:


local_dns.png


HttpDNS 优先通过 HTTP 协议与自建 DNS 服务器交互,如果有问题再降级到运营商的 LocalDNS 方案,既有效防止域名劫持,提高域名解析效率又保证了稳定可靠,HttpDNS 的原理如下图所示:


httpdns.png


HttpDNS优势



  • 稳定性:绕过运营商 LocalDNS,避免域名劫持,解决了由于 Local DNS 缓存导致的变更域名后无法即时生效的问题

  • 精准调度:避免 LocalDNS 调度不准确问题,自己做解析,返回最优服务端 IP 地址

  • 缩短解析延迟:通过域名预解析、缓存 DNS 解析结果等方式实现缩短域名解析延迟的效果


综述


先来看看主要的两点收益



  • 防止劫持,不过由于客户端大都已经全栈 HTTPS 了,HTTP 时代的中间人攻击已经基本没有了,但是还是有收益的。

  • 解析延迟带来的速度提升,目前全栈 HTTP/2 后,大都已经是长连接,数据统计单域名下通过 DNS Query 占比 5%+,DNS 解析平均耗时 80ms 左右,如果平摊到全量的网络请求 HttpDNS 会带来 1% 左右的提升


上面的收益没有提到精准调度,是因为我们的 APP 流量主要在国内,国内节点相对丰富,服务整体质量也较高,即使出现调度不准确的情况,差值也不会太大,但如果在国外情况可能会差很多。


0x03 连接优化


长连接


长连接指在一个连接上可以连续发送多个数据包,做到连接复用 我们知道从 HTTP/1.1 开始就支持了长连接,但是 HTTP/2 中引入了多路复用的技术。 大家可以通过 该链接 直观感受下 HTTP/2HTTP/1.1 到底快了多少。


http1_2.png


这是 Akamai 公司建立的一个官方的演示,用以说明 HTTP/2 相比于之前的 HTTP/1.1 在性能上的大幅度提升。同时请求 379 张图片,从加载时间 的对比可以看出HTTP/2在速度上的优势。


HTTP/2的多路复用技术代替了原来的序列和阻塞机制。 HTTP/1.x 中,如果想并发多个请求,必须使用多个 TCP 链接,且浏览器为了控制资源,还会对单个域名有连接数量限制,有了二进制分帧之后,HTTP/2不再依赖 TCP 链接去实现多流并行了,所有请求都是通过一个 TCP 连接并发送完成,利用请求优先级解决关键请求阻塞问题,使得重要的请求优先得到响应。这样更容易实现全速传输,减少 TCP 慢启动时间,提高传输的速度。传输流程如下图:


multiplexing.png


说了这么多优点,那多路复用有缺点么,主要是受限TCP的限制,一般来说同一域名下只需要使用一个 TCP 连接。但当连接中出现频繁丢包情况,就会有队头阻塞问题,所有的包都会等待丢包重传成功后传输,这样HTTP/2的表现情况反倒不如 HTTP/1.1了,HTTP/1.1还可以通过多个 TCP 连接并行传输数据。


域名合并


随着开发规模逐渐扩大,各业务团队出于独立性和稳定性的考虑,纷纷申请了自己的接口域名,域名会越来越多。我们知道 HTTP 属于应用层协议,在传输层使用 TCP 协议,在网络层使用IP协议,所以 HTTP 的长连接本质上是 TCP 长连接,是对一个域名( IP )来说的,如果域名过多面临下面几个问题:



  • 长连接的复用效率降低

  • 每个域名都需要经过 DNS 服务来解析服务器 IP。

  • HTTP 请求需要跟不同服务器同时保持长连接,增加了客户端和服务端的资源消耗。


具体方案如下图所示


domain_merge.png


TLS-v1.2 会话恢复


会话恢复主要是通过减少 TLS 密钥协商交换的过程,在第二次建连时提高连接效率 2-RTT -> 1-RTT 。具体是如何实现的呢?包含两种方式,一种是 Sesssion ID,一种是 Session Ticket。下面讲解了 Session Ticket 的原理:


img



  • Session ID

    Session ID 类似我们熟知的 Session 的概念。 由服务器端支持,协议中的标准字段,因此基本所有服务器都支持,客户端只缓存会话 ID,服务器端保存会话 ID 以及协商的通信信息,占用服务器资源较多。


  • Session Ticket

    Session ID 存在一些弊端,比如分布式系统中的 Session Cache 同步问题,如果不能同步则大大限制了会话复用效率,但 Session Ticket 没问题。Session Ticket 更像我们熟知的 Cookie 的概念,Session Ticket 用只有服务端知道的安全密钥加密过的会话信息,保存在客户端上。客户端在 ClientHello 时带上了 Session Ticket,服务器如果能成功解密就可以完成快速握手。



不管是 Session ID 还是 Session Ticket 都存在时效性问题,不是永久有效的。


TLS-v1.3


首先需要明确的是,同等情况下,TLS/1.3TLS/1.2 少一个 RTT 时间。并且废除了一些不安全的算法,提升了安全性。首次握手,TLS/1.2完成 TLS 密钥协商需要 2 个 RTT 时间,TLS/1.3只需要 1 个 RTT 时间。会话恢复 TLS/1.2 需要 1 个 RTT 时间,TLS/1.3 则因为在第一个包中携带数据(early data),只需要 0 个 RTT,有点类似 TLS 层的 TCP Fast Open



  • 首次握手流程

    img


  • 会话恢复-0RTT

    TLS/1.3 中更改了会话恢复机制,废除了原有的 Session IDSession Ticket 的方式,使用 PSK 的机制,并支持了 0RTT模式下的恢复模式(实现 0-RTT 的条件比较苛刻,目前不少浏览器虽然支持 TLS/1.3 协议,但是还不支持发送 early data,所以它们也没法启用 0-RTT 模式的会话恢复)。当 client 和 server 共享一个 PSK(从外部获得或通过一个以前的握手获得)时,TLS/1.3 允许 client 在第一个发送出去的消息中携带数据("early data")。Client 使用这个 PSK 生成 client_early_traffic_secret 并用它加密 early data。Server 收到这个 ClientHello 之后,用 ClientHello 扩展中的 PSK 导出 client_early_traffic_secret 并用它解密 early data0-RTT 会话恢复模式如下:

    img



HTTP/3


QUIC 首次在2013年被提出,互联网工程任务组在 2018 年的时候将基于 QUIC 协议的 HTTP (HTTP over QUIC) 重命名为 HTTP/3。不过目前 HTTP/3 还没有最终定稿,最新我看已经到了第 34 版,应该很快就会发布正式版本。某些意义上说 HTTP/3 协议实际上就是IETF QUICQUICQuick UDP Internet Connections,快速 UDP 网络连接) 基于 UDP,利用 UDP 的速度与效率。同时 QUIC 也整合了 TCPTLSHTTP/2 的优点,并加以优化。用一张图可以清晰地表示他们之间的关系。


HTTP/3主要带来了零 RTT 建立连接、连接迁移、队头阻塞/多路复用等诸多优势,具体细节暂不介绍了。推出HTTP/3(QUIC)的原理与实践,敬请期待。


0x04 带宽优化


HTTP/2 头部压缩


HTTP1.xheader 中的字段很多时候都是重复的,例如 method:getscheme:https 等。随着请求增长,这些请求中的冗余标头字段不必要地消耗带宽,从而显著增加了延迟,因此,HTTP/2 头部压缩技术应时而生,使用的压缩算法为 HPACK。借用 Google 的性能专家 Ilya Grigorik 在 Velocity 2015 ? SC 会议中分享的一张图,让我们了解下头部压缩的原理:


head_compress1.png


上述图主要涉及两个点



  • 静态表中定义了61个 header 字段与 Index,可以通过传输 Index 进而获取 header,极大减少了报文大小。静态表中的字段和值固定,而且是只读的。详见静态表

  • 动态表接在静态表之后,结构与静态表相同,以先进先出的顺序维护的 header 字段列表,可随时更新。同一个连接上产生的请求和响应越多,动态字典积累得越全,头部压缩效果也就越好。所以尽量上面提到的域名合并,不仅提升连接性能也能提高带宽优化效果


简单描述一下 HPACK 算法的过程:



  • 发送方和接受方共同维护一份静态表和动态表

  • 发送方根据字典表的内容,编码压缩消息头部

  • 接收方根据字典表进行解码,并且根据指令来判断是否需要更新动态表


看完了 HTTP/2 头部压缩的介绍,那在没有头部压缩技术的 HTTP/1 时代,当处理一些通用参数时,我们当时只能把参数压缩后放入 body 传输,因为 header 只支持 ascii 码,压缩导致乱码,如果放入 header 还得 encode 或者 base64 编码,不仅增大了体积还要浪费解码的性能。


数据表明通用参数通过 HTTP/2header 传输,由于长连通道大概在 90%+ 复用比例,压缩率可以达到惊人的 90%,同样比压缩后放在 body 中减少约 50% 的 size,如果遇到一些无规则的文本数据,zip 压缩率也会随着变低,这时候提升将会更明显。说了这么多,最后让我们通过抓包看下,HTTP/2 头部压缩的效果吧:


head_compress2.png


Json vs Protobuffer


Protobuffer 是 Google 出品的一种轻量 & 高效的结构化数据存储格式,性能比 Json 好,Size 比 Json 要小



  • Protobuffer 数据格式

    因为咱们这里说的是带宽,所以咱们详细说下 size 这块的优化,相比 json 来说,Protobuffer 节省了字段名字和分隔符,具体格式如下:

    // tag: 字段标识号,不可重复
    // type: 字段类型
    // length: 字段长度,当data可以用Varint表示的时候不需要 (可选)
    // data: 字段数据

    <tag> <type> [<length>] <data>

  • Protobuffer 数据编码方式

    Protobuffer 的数据格式不可避免的存在定长数据和变长数据的表示问题,编码方式用到了 Varint & Zigzag。 这里主要介绍下 Varint,因为 Zigzag 主要是对于负数时的一个补充( VarInt 不太适合表示负数,有兴趣的同学可以自行查下资料 ) ,Varint 其实是一种变长的编码方式,用字节表示数字,征用了每个字节的最高位 (MSB), MSB 是 1 话,则表示还有后序字节,一直到 MSB 为 0 的字节为止,具体表示如下表:

       0 ~ 2^07 - 1 0xxxxxxx
    2^07 ~ 2^14 - 2 1xxxxxxx 0xxxxxxx
    2^14 ~ 2^21 - 3 1xxxxxxx 1xxxxxxx 0xxxxxxx
    2^21 ~ 2^28 - 4 1xxxxxxx 1xxxxxxx 1xxxxxxx 0xxxxxxx

    不难看出值越小的数字,使用越少的字节数表示。如对于 int32 类型的数字,一般需要 4 个字节 表示,但是用了 Varint 小于 128 一个字节就够了,但是对于大的 int32 数字(大于2^28)会需要 5 个 字节 来表示,但大多数情况下,数据中不会有很大的数字,所以采用 Varint 方法总是可以用更少的字节数来表示数字


  • 具体示例

    首先定义一个实体类

    // bean
    class Person {
    int age = 1;
    }

    Json 的序列化结果

    // json
    {
    "age":1
    }

    Protobuffer 的序列化结果

    // Protobuffer
    00001000 00000001

    简单说下 Protobuffer 的序列化逻辑吧,Personage 字段取值为 1 的话,类型为 int32 则对应的编码是:0x08 0x01age 的类型是 int32,对应的 type 取 0。而它的 tag 又是 1,所以第一个字节是 (1 << 3) | 0 = 0x08,第二个字节是数字 1 的 VarInt 编码,即 0x01

    // bean
    class Person {
    int age = 1;
    }

    // Protobuffer 描述文件
    message Person {
    int32 age = 1;
    }

    // protobuf值
    +-----+---+--------+
    |00001|000|00000001|
    +-----+---+--------+
    tag type data

  • 优化数据

    原始数据 Size ProtobufferJson 小 30%左右,压缩后 Size 差距 10%+



0x05 总结


上面说了这么多技术的原理,接下来分享下我们的技术选型以及思考,希望能给大家带来帮助



  • HttpDNS

    我们的应用主要在国内使用,这个功能只对首次建连有提升,目前首次建连的比例较小,也就在5%左右,所以对性能提升较小;安全问题再全栈切了 https 后也没有那么突出;并且 HttpDNS 也会遇到一些技术难点,比如自建 DNS 需要维护一套聪明的服务端IP调度策略;客户端需要注意 IP 缓存 以及 IPV4 IPV6 的双栈探测和选取策略等,综合考虑下目前我们没有使用HttpDNS


  • 长连接&域名合并

    长连接和域名合并,这两个放在一起说下吧,因为他们是相辅相成的,域名合并后,不同业务线使用一个长连接通道,减少了TCP 慢启动时间,更容易实现全速传输,优势还是很明显的。

    长连接方案还是有很多的,涉及到 HTTP/1.1HTTP/2HTTP/3、自建长链等方式,最终我们选择了HTTP/2,并不是因为这个方案性能最优,而是综合来看这个方案最有优势,HTTP/1.1 就不用说了,这个方案早就淘汰了,而 HTTP/3 虽然性能好,但是目前阶段并没有完整稳定的前后端框架,所以只适合尝鲜,是我们接下来的一个目标,自建长链虽然能结合业务定制逻辑,达到最优的性能,但是比较复杂,也正是由于特殊的定制导致没办法方便的切换官方的协议(如HTTP/3


  • TLS 协议

    TLS 协议我们积极跟进了官方最新稳定版 TLS/1.3 版本的升级,客户端在 Android 10iOS 12.2 后已经开始默认支持 TLS/1.3,想在低系统版本上支持需要接入三方扩展库,由于支持的系统版本占比已经很高,后面也会越来越高,并且 TLS 协议是兼容式的升级,允许客户端同时存在TLS/1.2TLS/1.3两个版本,综合考虑下我们的升级策略就是只升级服务端

    TLS 协议升级主要带来两个方面的提升,性能和安全。先来说下性能的提升,TLS/1.3 对比TLS/1.2版本来说,性能的提升体现在减少握手时的一次 RTT 时间,由于连接复用率很高,所以性能的提升和 HttpDNS 一样效果很小。至于安全前面也有提到,废弃了一些相对不安全的算法,提升了安全性


  • 带宽优化

    带宽优化对于网络传输的速度和用户的流量消耗都是有帮助的,所以我们要尽量减少数据的传输,那么在框架层来说主要的策略有两种:

    一是减少相同数据的传输次数,也就是对应着 HTTP/2 的头部压缩,原理上面也有介绍,这里就不在赘述了,对于目前长连接的通道,header 中不变化的数据只传输一个标识即可,这个的效果还是很明显的,所以框架可以考虑一些通用并且不长变化的参数放在 Header 中传输,但是此处需要注意并不是所有的参数都适合在 Header 中,因为 Header 中的数据只支持 ASCII 编码,所以有一些敏感数据放在此处会容易被破解,如果加密后在放进来,还需要在进行 Base64 编码处理,这样可能会加大很多传输的 size,所以框架侧要进行取舍 (PS:在补充个 Header 字段的小坑:HTTP/2 Header都会转成小写,而 HTTP/1.x 大小写均可,在升级 HTTP/2 协议的时候需要注意下)

    二是减少传输 size,常规的做法如切换更省空间的数据格式 Protobuffer,因为关联到数据格式的变动,需要前后端一起调整,所以改造历史业务成本会比较高有阻力,比较适合在新的业务上尝鲜,总结出一套最佳实践后,再去尝试慢慢改造旧的业务


收起阅读 »

SpringBoot实战基于异常日志的邮件报警

SpringBoot实战基于异常日志的邮件报警 相信所有奋斗在一线的小伙伴,会很关心自己的系统的运行情况,一般来说,基础设施齐全一点的公司都会有完善的报警方案,那么如果我们是一个小公司呢,不能因为基础设施没有,就失去对象的感知能力吧;如果我们的系统大量异...
继续阅读 »





SpringBoot实战基于异常日志的邮件报警



相信所有奋斗在一线的小伙伴,会很关心自己的系统的运行情况,一般来说,基础设施齐全一点的公司都会有完善的报警方案,那么如果我们是一个小公司呢,不能因为基础设施没有,就失去对象的感知能力吧;如果我们的系统大量异常却不能实时的触达给我们,那么也就只会有一个结果--杀个程序猿祭天


本文简单的介绍一种实现思路,基于error日志来实现邮件的报警方案


I. 项目环境


1. 项目依赖


本项目借助SpringBoot 2.2.1.RELEASE + maven 3.5.3 + IDEA进行开发


开一个web服务用于测试


<dependencies>
<!-- 邮件发送的核心依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
</dependencies>

2. 配置


邮件相关配置如下,注意使用自己的用户名 + 授权码填充下面缺失的配置


spring:
#邮箱配置
mail:
host: smtp.163.com
from: xhhuiblog@163.com
# 使用自己的发送方用户名 + 授权码填充
username:
password:
default-encoding: UTF-8
properties:
mail:
smtp:
auth: true
starttls:
enable: true
required: true

II. 异常日志的邮件预警


1. 设计思路


接下来这个方案的主要出发点在于,当程序出现大量的异常,表明应用多半出现了问题,需要立马发送给项目owner


要实现这个方案,关键点就在于异常出现的感知与上报



  • 异常的捕获,并输出日志(这个感觉属于标配了吧,别告诉我现在还有应用不输出日志文件的...)

    • 对于这个感知,借助logback的扩展机制,可以实现,后面介绍


  • 异常上报:邮件发送


关于email的使用姿势,推荐参考博文 SpringBoot 系列之邮件发送姿势介绍


2. 自定义appender


定义一个用于错误发送的Appender,如下


public class MailUtil extends AppenderBase<ILoggingEvent> {

public static void sendMail(String title, String context) {
SimpleMailMessage simpleMailMessage = new SimpleMailMessage();
//邮件发送人
simpleMailMessage.setFrom(ContextUtil.getEnvironment().getProperty("spring.mail.from", "bangzewu@126.com"));
//邮件接收人,可以是多个
simpleMailMessage.setTo("bangzewu@126.com");
//邮件主题
simpleMailMessage.setSubject(title);
//邮件内容
simpleMailMessage.setText(context);

JavaMailSender javaMailSender = ContextUtil.getApplicationContext().getBean(JavaMailSender.class);
javaMailSender.send(simpleMailMessage);
}

private static final long INTERVAL = 10 * 1000 * 60;
private long lastAlarmTime = 0;

@Override
protected void append(ILoggingEvent iLoggingEvent) {
if (canAlarm()) {
sendMail(iLoggingEvent.getLoggerName(), iLoggingEvent.getFormattedMessage());
}
}

private boolean canAlarm() {
// 做一个简单的频率过滤
long now = System.currentTimeMillis();
if (now - lastAlarmTime >= INTERVAL) {
lastAlarmTime = now;
return true;
} else {
return false;
}
}
}

3. Spring容器


上面的邮件发送中,需要使用JavaMailSender,写一个简单的SpringContext工具类,用于获取Bean/Propertiy


@Component
public class ContextUtil implements ApplicationContextAware, EnvironmentAware {

private static ApplicationContext applicationContext;

private static Environment environment;

@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
ContextUtil.applicationContext = applicationContext;
}

@Override
public void setEnvironment(Environment environment) {
ContextUtil.environment = environment;
}

public static ApplicationContext getApplicationContext() {
return applicationContext;
}

public static Environment getEnvironment() {
return environment;
}
}

4. logback配置


接下来就是在日志配置中,使用我们上面定义的Appender


logback-spring.xml文件内容如下:


<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d [%t] %-5level %logger{36}.%M\(%file:%line\) - %msg%n</pattern>
<!-- 控制台也要使用UTF-8,不要使用GBK,否则会中文乱码 -->
<charset>UTF-8</charset>
</encoder>
</appender>

<appender name="errorAlarm" class="com.git.hui.demo.mail.util.MailUtil">
<!--如果只是想要 Error 级别的日志,那么需要过滤一下,默认是 info 级别的,ThresholdFilter-->
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>ERROR</level>
</filter>
</appender>


<!-- 指定项目中某个包,当有日志操作行为时的日志记录级别 -->
<!-- 级别依次为【从高到低】:FATAL > ERROR > WARN > INFO > DEBUG > TRACE -->
<!-- additivity=false 表示匹配之后,不再继续传递给其他的logger-->
<logger name="com.git.hui" level="DEBUG" additivity="false">
<appender-ref ref="STDOUT"/>
<appender-ref ref="errorAlarm"/>
</logger>

<!-- 控制台输出日志级别 -->
<root level="INFO">
<appender-ref ref="STDOUT"/>
</root>
</configuration>

5. 测试demo


接下来演示一下,是否可以达到我们的预期


@Slf4j
@RestController
@SpringBootApplication
public class Application {

public static void main(String[] args) {
SpringApplication.run(Application.class);
}

@GetMapping("div")
public String div(int a, int b) {
try {
return String.valueOf(a / b);
} catch (Exception e) {
log.error("div error! {}/{}", a, b, e);
return "some error!";
}
}
}


5.小结


本篇博文主要提供了一个思路,借助logback的扩展机制,来实现错误日志与预警邮件绑定,实现一个简单的应用异常监控


上面这个实现只算是一个雏形,算是抛砖引玉,有更多可以丰富的细节,比如



  • 飞书/钉钉通知(借助飞书钉钉的机器来报警,相比较于邮件感知性更高)

  • 根据异常类型,做预警的区分

  • 更高级的频率限制等


在这里推荐一个我之前开源的预警系统,可以实现灵活预警方案配置,频率限制,重要性升级等



III. 不能错过的源码和相关知识点


0. 项目


收起阅读 »

swift基础语法(内部函数,外部函数)

内部函数: 默认情况下的参数都是内部参数外部函数: 如果有多个参数的情况, 调用者并不知道每个参数的含义,         只能通过查看头文件的形式理解参数的含义    ...
继续阅读 »
内部函数: 默认情况下的参数都是内部参数
外部函数: 如果有多个参数的情况, 调用者并不知道每个参数的含义,
         只能通过查看头文件的形式理解参数的含义
        那么能不能和OC一样让调用者直观的知道参数的含义呢? 使用外部参数
         外部参数只能外部用, 函数内部不能使用, 函数内部只能使用内部参数
func divisionOpertaion1(a: Double, b:Double) -> Double{
    return a / b
}
func divisionOpertaion2(dividend: Double, divisor:Double) -> Double{
    return dividend / divisor
}
func divisionOpertaion3(dividend a: Double, divisor b:Double) -> Double{
    return a / b
}
print(divisionOpertaion3(dividend: 10, divisor: 3.5))
func divisionOpertaion4(a: Double, divisor b:Double) -> Double{
    return a / b
}
print(divisionOpertaion4(10, divisor: 3.5))
输出结果:
2.85714285714286
2.85714285714286
 
 
func divisionOpertaion(dividend: Double, divisor:Double) -> Double{
    return dividend / divisor
}
print(divisionOpertaion(10, divisor: 3.5))
输出结果:2.85714285714286
 
默认参数:
可以在定义函数的时候给某个参数赋值, 当外部调用没有传递该参数时会自动使用默认值
func joinString(s1:String ,toString s2:String, jioner s3:String) ->String
{
    return s1 + s3 + s2;
}
func joinString2(s1:String ,toString
                 s2:String, jioner
                 s3:String = "❤️") ->String
{
    return s1 + s3 + s2;
}
print(joinString2("hi", toString:"beauty"))
输出结果:hi❤️beauty
 
如果指定了默认参数, 但是确没有声明外部参数时
系统会自动把内部参数名称既作为内部参数也作为外部参数名称
并且在调用时如果需要修改默认参数的值必须写上外部参数名称
func joinString3(s1:String ,toString
                 s2:String,
             jioner:String = "❤️") ->String
{
    return s1 + jioner + s2;
}
print(joinString3("hi", toString:"beauty", jioner:"🐔"))
输出结果: hi🐔beauty
 
在其它语言中默认参数智能出现在参数列表的最后面, 但是在Swift中可以出现在任何位置
func joinString4(s1:String ,
             jioner:String = "❤️",
        toString s2:String) ->String
{
    return s1 + jioner + s2;
}
print(joinString4("hi", jioner:"🐔", toString:"beauty"))
输出结果: hi🐔beauty
 
常量参数和遍历参数:
默认情况下Swift中所有函数的参数都是常量参数
如果想在函数中修改参数, 必须在参数前加上var
func swap(var a:Int, var b:Int)
{
    print("交换前 a = \(a) b = \(b)")
    let temp = a;
    a = b;
    b = temp;
    print("交换后 a = \(a) b = \(b)")
}
swap(10, b: 20)
输出结果:
交换前 a = 10 b = 20
交换后 a = 20 b = 10
 
inout参数
如果想在函数中修改外界传入的参数
可以将参数的var换成inout, 这回会传递参数本身而不是参数的值
func swap(inout a:Int, inout b:Int)
{
    let temp = a;
    a = b;
    b = temp;
}
var x1 = 10;
var y1 = 20;
print("交换前 a = \(x1) b = \(y1)")
swap(&x1, b: &y1)
print("交换后 a = \(x1) b = \(y1)")
输出结果:
交换前 a = 10 b = 20
交换后 a = 20 b = 10
 
 
变参函数
如果没有变参函数 , 并且函数的参数个数又不确定那么只能写多个方法或者用将函数参数改为集合
变参只能放到参数列表的最后一位, 变参必须指定数据类型, 变参只能是同种类型的数据
 
func add(num1:Int, num2:Int, num3:Int) -> Int
{
    let sum = num1 + num2 + num3
    return sum
}
print(add(1, num2: 2, num3: 3))
输出结果:6
 
func add(nums:[Int]) -> Int
{
    var sum = 0;
    for num in nums
    {
        sum += num
    }
    return sum
}
print(add([1, 2, 3]))
输出结果:6
 
func add(nums:Int...) -> Int
{
    var sum = 0;
    for num in nums
    {
        sum += num
    }
    return sum
}
print(add(1, 2, 3))
输出结果:6
 
func add(other:Int, nums:Int...) -> Int
{
    var sum = 0;
    for num in nums
    {
        sum += num
    }
    return sum
}
print(add(99, nums: 1, 2, 3))
输出结果:6
收起阅读 »

swift基础语法(内部函数,外部函数)

内部函数: 默认情况下的参数都是内部参数外部函数: 如果有多个参数的情况, 调用者并不知道每个参数的含义,         只能通过查看头文件的形式理解参数的含义    ...
继续阅读 »
内部函数: 默认情况下的参数都是内部参数
外部函数: 如果有多个参数的情况, 调用者并不知道每个参数的含义,
         只能通过查看头文件的形式理解参数的含义
        那么能不能和OC一样让调用者直观的知道参数的含义呢? 使用外部参数
         外部参数只能外部用, 函数内部不能使用, 函数内部只能使用内部参数
func divisionOpertaion1(a: Double, b:Double) -> Double{
    return a / b
}
func divisionOpertaion2(dividend: Double, divisor:Double) -> Double{
    return dividend / divisor
}
func divisionOpertaion3(dividend a: Double, divisor b:Double) -> Double{
    return a / b
}
print(divisionOpertaion3(dividend: 10, divisor: 3.5))
func divisionOpertaion4(a: Double, divisor b:Double) -> Double{
    return a / b
}
print(divisionOpertaion4(10, divisor: 3.5))
输出结果:
2.85714285714286
2.85714285714286
 
 
func divisionOpertaion(dividend: Double, divisor:Double) -> Double{
    return dividend / divisor
}
print(divisionOpertaion(10, divisor: 3.5))
输出结果:2.85714285714286
 
默认参数:
可以在定义函数的时候给某个参数赋值, 当外部调用没有传递该参数时会自动使用默认值
func joinString(s1:String ,toString s2:String, jioner s3:String) ->String
{
    return s1 + s3 + s2;
}
func joinString2(s1:String ,toString
                 s2:String, jioner
                 s3:String = "❤️") ->String
{
    return s1 + s3 + s2;
}
print(joinString2("hi", toString:"beauty"))
输出结果:hi❤️beauty
 
如果指定了默认参数, 但是确没有声明外部参数时
系统会自动把内部参数名称既作为内部参数也作为外部参数名称
并且在调用时如果需要修改默认参数的值必须写上外部参数名称
func joinString3(s1:String ,toString
                 s2:String,
             jioner:String = "❤️") ->String
{
    return s1 + jioner + s2;
}
print(joinString3("hi", toString:"beauty", jioner:"🐔"))
输出结果: hi🐔beauty
 
在其它语言中默认参数智能出现在参数列表的最后面, 但是在Swift中可以出现在任何位置
func joinString4(s1:String ,
             jioner:String = "❤️",
        toString s2:String) ->String
{
    return s1 + jioner + s2;
}
print(joinString4("hi", jioner:"🐔", toString:"beauty"))
输出结果: hi🐔beauty
 
常量参数和遍历参数:
默认情况下Swift中所有函数的参数都是常量参数
如果想在函数中修改参数, 必须在参数前加上var
func swap(var a:Int, var b:Int)
{
    print("交换前 a = \(a) b = \(b)")
    let temp = a;
    a = b;
    b = temp;
    print("交换后 a = \(a) b = \(b)")
}
swap(10, b: 20)
输出结果:
交换前 a = 10 b = 20
交换后 a = 20 b = 10
 
inout参数
如果想在函数中修改外界传入的参数
可以将参数的var换成inout, 这回会传递参数本身而不是参数的值
func swap(inout a:Int, inout b:Int)
{
    let temp = a;
    a = b;
    b = temp;
}
var x1 = 10;
var y1 = 20;
print("交换前 a = \(x1) b = \(y1)")
swap(&x1, b: &y1)
print("交换后 a = \(x1) b = \(y1)")
输出结果:
交换前 a = 10 b = 20
交换后 a = 20 b = 10
 
 
变参函数
如果没有变参函数 , 并且函数的参数个数又不确定那么只能写多个方法或者用将函数参数改为集合
变参只能放到参数列表的最后一位, 变参必须指定数据类型, 变参只能是同种类型的数据
 
func add(num1:Int, num2:Int, num3:Int) -> Int
{
    let sum = num1 + num2 + num3
    return sum
}
print(add(1, num2: 2, num3: 3))
输出结果:6
 
func add(nums:[Int]) -> Int
{
    var sum = 0;
    for num in nums
    {
        sum += num
    }
    return sum
}
print(add([1, 2, 3]))
输出结果:6
 
func add(nums:Int...) -> Int
{
    var sum = 0;
    for num in nums
    {
        sum += num
    }
    return sum
}
print(add(1, 2, 3))
输出结果:6
 
func add(other:Int, nums:Int...) -> Int
{
    var sum = 0;
    for num in nums
    {
        sum += num
    }
    return sum
}
print(add(99, nums: 1, 2, 3))
输出结果:6
收起阅读 »

swift 基础语法(19-闭包,闭包函数回调,尾随闭包,闭包捕获值)

闭包:函数是闭包的一种类似于OC语言的block闭包表达式(匿名函数) -- 能够捕获上下文中的值语法: in关键字的目的是便于区分返回值和执行语句闭包表达式的类型和函数的类型一样, 是参数加上返回值, 也就是in之前的部分{   ...
继续阅读 »
闭包:
函数是闭包的一种
类似于OC语言的block
闭包表达式(匿名函数) -- 能够捕获上下文中的值

语法: in关键字的目的是便于区分返回值和执行语句
闭包表达式的类型和函数的类型一样, 是参数加上返回值, 也就是in之前的部分
{
    (参数) -> 返回值类型 in
    执行语句
}
完整写法
let say:(String) -> Void = {
    (name: String) -> Void in
    print("hi \(name)")
}
say("qbs")
输出结果:  hi qbs
 
没有返回值写法
let say:(String) ->Void = {
    (name: String) in
    print("hi \(name)")
}
say("qbs")
输出结果:  hi qbs
 
没有参数没有返回值写法
let say:() ->Void = {
    print("hi qbs")
}
say()
输出结果:  hi qbs
 
 
闭包表达式作为回调函数
传统数组排序写法:
缺点: 不一定是小到大, 不一定是全部比较, 有可能只比较个位数
           所以, 如何比较可以交给调用者决定
func bubbleSort(inout array:[Int])
{
    let count = array.count;
    for var i = 1; i < count; i++
    {
        for var j = 0; j < (count - i); j++
        {
            if array[j] > array[j + 1]
            {
                let temp = array[j]
                array[j] = array[j + 1]
                array[j + 1] = temp
            }
        }
    }
}
 
闭包写法:
func bubbleSort(inout array:[Int], cmp: (Int, Int) -> Int)
{
    let count = array.count;
    for var i = 1; i < count; i++
    {
        for var j = 0; j < (count - i); j++
        {
            if cmp(array[j], array[j + 1]) == -1
            {
                let temp = array[j]
                array[j] = array[j + 1]
                array[j + 1] = temp
            }
        }
    }
}
 
let cmp = {
    (a: Int, b: Int) -> Int in
    if a > b{
        return 1;
    }else if a < b
    {
        return -1;
    }else
    {
        return 0;
    }
}
var arr:Array<Int> = [31, 13, 52, 84, 5]
bubbleSort(&arr, cmp: cmp)
print(arr)
 
输出结果:
[84, 52, 31, 13, 5]
 
 
闭包作为参数传递
var arr:Array<Int> = [31, 13, 52, 84, 5]
bubbleSort(&arr, cmp: {
    (a: Int, b: Int) -> Int in
    if a > b{
        return 1;
    }else if a < b
    {
        return -1;
    }else
    {
        return 0;
    }
})
print(arr)
输出结果:
[84, 52, 31, 13, 5]
 
尾随闭包:
如果闭包是最后一个参数, 可以直接将闭包写到参数列表后面
这样可以提高阅读性. 称之为尾随闭包
bubbleSort(&arr) {
    (a: Int, b: Int) -> Int in
    if a > b{
        return 1;
    }else if a < b
    {
        return -1;
    }else
    {
        return 0;
    }
}
 
闭包表达式优化
 1.类型优化, 由于函数中已经声明了闭包参数的类型
   所以传入的实参可以不用写类型
 2.返回值优化, 同理由于函数中已经声明了闭包的返回值类型
   所以传入的实参可以不用写类型
 3.参数优化, swift可以使用$索引的方式来访问闭包的参数, 默认从0开始
bubbleSort(&arr){
   (a , b) -> Int in
   (a , b) in
    if $0 > $1{
        return 1;
    }else if $0 < $1
    {
        return -1;
    }else
    {
        return 0;
    }
}
 
 
如果只有一条语句可以省略return
let hehe = {
    "我是qbs"
}
闭包捕获值
func getIncFunc() -> (Int) -> Int
{
    var max = 10
    func incFunc(x :Int) ->Int{
        print("incFunc函数结束")
        max++
        return max + x
    }
    当执行到这一句时inc参数就应该被释放了
    但是由于在内部函数中使用到了它, 所以它被捕获了
    同理, 当执行完这一句时max变量就被释放了
    但是由于在内部函数中使用到了它, 所以它被捕获了
    print("getIncFunc函数结束")
    return incFunc
}
 
被捕获的值会和与之对应的方法绑定在一起
同一个方法中的变量会被绑定到不同的方法中
let incFunc = getIncFunc()
print("---------")
print(incFunc(5))
print("---------")
print(incFunc(5))
输出结果:
getIncFunc函数结束
---------
incFunc
函数结束
16
---------
incFunc
函数结束
17
 
 
let incFunc2 = getIncFunc(5)
print(incFunc2(5))
输出结果:
getIncFunc函数结束
incFunc函数结束
16
收起阅读 »

Kotlin infix 关键字与高阶函数的应用[第一行代码 Kotlin 学习笔记]

使用 infix 函数构建更可读的语法 在前面的 Kotlin 学习笔记中,我们已经多次使用过 A to B 这样的语法结构构建键值对,包括 Kotlin 自带的 mapOf() 函数。 这种语法结构的优点是可读性高,相比于调用一个函数,它更接近于使用英语...
继续阅读 »

使用 infix 函数构建更可读的语法


在前面的 Kotlin 学习笔记中,我们已经多次使用过 A to B 这样的语法结构构建键值对,包括 Kotlin 自带的 mapOf() 函数。


这种语法结构的优点是可读性高,相比于调用一个函数,它更接近于使用英语的语法来编写程序。可能你会好奇,这种功能是怎么实现的呢?to 是不是 Kotlin 语言中的一个关键字?本节我们就对这个功能进行深度解密。


首先,to 并不是 Kotlin 语言中的一个关键字,之所以我们能够使用 A to B 这样的语法结构,是因为 Kotlin 提供了一种高级语法糖特性:infix 函数。当然,infix 函数也并不是什么难理解的事物,它只是把编程语言函数调用的语法规则调整了一下而已,比如 A to B 这样的写法,实际上等价于 A.to(B) 的写法。


下面我们就通过两个具体的例子来学习一下 infix 函数的用法,先从简单的例子看起。


String 类中有一个 startsWith() 函数,你一定使用过,它可以用于判断一个字符串是否是以某个指定参数开头的。比如说下面这段代码的判断结果一定会是 true:


if ("Hello Kotlin".startsWith("Hello")) {
// 处理具体的逻辑
}

startsWith() 函数的用法虽然非常简单,但是借助 infix 函数,我们可以使用一种更具可读性的语法来表达这段代码。新建一个 infix.kt 文件,然后编写如下代码:


infix fun String.beginsWith(prefix: String) = startsWith(prefix)

首先,除去最前面的 infix 关键字不谈,这是一个 String 类的扩展函数。我们给 String 类添加了一个 beginsWith() 函数,它也是用于判断一个字符串是否是以某个指定参数开头的,并且它的内部实现就是调用的 String 类的 startsWith() 函数。


但是加上了 infix 关键字之后,beginsWith() 函数就变成了一个 infix 函数,这样除了传统的函数调用方式之外,我们还可以用一种特殊的语法糖格式调用 beginsWith() 函数,如下所示:


if ("Hello World" beginsWith "Hello") {
// 处理具体的逻辑
}

从这个例子就能看出,infix 函数的语法规则并不复杂,上述代码其实就是调用的 " HelloKotlin " 这个字符串的 beginsWith() 函数,并传入了一个 "Hello" 字符串作为参数。但是 infix 函数允许我们将函数调用时的小数点、括号等计算机相关的语法去掉,从而使用一种更接近英语的语法来编写程序,让代码看起来更加具有可读性。


另外,infix 函数由于其语法糖格式的特殊性,有两个比较严格的限制:首先,infix 函数是不能定义成顶层函数的,它必须是某个类的成员函数,可以使用扩展函数的方式将它定义到某个类当中;其次,infix 函数必须接收且只能接收一个参数,至于参数类型是没有限制的。只有同时满足这两点, infix 函数的语法糖才具备使用的条件,你可以思考一下是不是这个道理。


看完了简单的例子,接下来我们再看一个复杂一些的例子。比如这里有一个集合,如果想要判断集合中是否包括某个指定元素,一般可以这样写:


val list = listOf("Apple", "Banana", "Orange", "Pear", "Grape")
if (list.contains("Banana")) {
// 处理具体的逻辑
}

很简单对吗?但我们仍然可以借助 infix 函数让这段代码变得更加具有可读性。在 infix.kt 文件中添加如下代码:


infix fun <T> Collections<T>.has(element: T) = contains(element)

可以看到,我们给 Collection 接口添加了一个扩展函数,这是因为 Collection 是 Java 以及 Kotlin 所有集合的总接口,因此给 Collection 添加一个 has() 函数,那么所有集合的子类就都可以使用这个函数了。


另外,这里还使用了泛型函数的定义方法,从而使得 has() 函数可以接收任意具体类型的参数。而这个函数内部的实现逻辑就相当简单了,只是调用了 Collection 接口中的 contains() 函数而已。也就是说,has() 函数和 contains() 函数的功能实际上是一模一样的,只是它多了一个 infix 关键字,从而拥有了 infix 函数的语法糖功能。


现在我们就可以使用如下的语法来判断集合中是否包括某个指定的元素:


val list = listOf("Apple", "Banana", "Orange", "Pear", "Grape")
if (list has "Banana") {
// 处理具体的逻辑
}

好了,两个例子都已经看完了,你对于 infix 函数应该也了解得差不多了。但是或许现在你的心中还有一个疑惑没有解开,就是 mapOf() 函数中允许我们使用 A to B 这样的语法来构建键值对,它的具体实现是怎样的呢?为了解开谜团,我们直接来看一看 to() 函数的源码吧,如下所示:


public infix fun <A, B> A.to(that: B): Pair<A, B> = Pair(this, that)

可以看到,这里使用定义泛型函数的方式将 to() 函数定义到了 A 类型下,并且接收一个 B 类型的参数。因此 A 和 B 可以是两种不同类型的泛型,也就使得我们可以构建出字符串 to 整型这样的键值对。


再来看 to() 函数的具体实现,非常简单,就是创建并返回了一个 Pair 对象。也就是说,A to B 这样的语法结构实际上得到的是一个包含 A、B 数据的 Pair 对象,而 mapOf() 函数实际上接收的正是一个 Pair 类型的可变参数列表,这样我们就将这种神奇的语法结构完全解密了。


本着动手实践的精神,其实我们也可以模仿 to() 函数的源码来编写一个自己的键值对构建函数。在 infix.kt 文件中添加如下代码:


infix fun <A, B> A.with(that: B): Pair<A, B> = Pair(this, that)

这里只是将 to() 函数改名成了 with() 函数,其他实现逻辑是相同的,因此相信没有什么解释的必要。现在我们的项目中就可以使用 with() 函数来构建键值对了,还可以将构建的键值对传入 mapOf() 方法中:


val map = mapOf("Apple" with 1, "Banana" with 2, "Orange" with 3, "Pear" with 4, "Grape" with 5)

是不是很神奇?这就是 infix 函数给我们带来的诸多有意思的功能,灵活运用它确实可以让语法变得更具可读性。


高阶函数的应用


高阶函数非常适用于简化各种 API 的调用,一些 API 的原有用法在使用高阶函数简化之后,不管是在易用性还是可读性方面,都可能会有很大的提升。


为了进行举例说明,我们在本节会使用高阶函数简化 SharedPreferences 和 ContentValues 这两种 API 的用法,让它们的使用变得更加简单。


简化 SharedPreferences 的用法


首先来看 SharedPreferences,在开始对它进行简化之前,我们先回顾一下 SharedPreferences 原来的用法。向 SharedPreferences 中存储数据的过程大致可以分为以下 3 步:



  1. 调用 SharedPreferences 的 edit() 方法获取 SharedPreferences.Editor 对象;

  2. 向 SharedPreferences.Editor 对象中添加数据;

  3. 调用 apply() 方法将添加的数据提交,完成数据存储操作。


对应的代码示例如下:


val editor = getSharedPreferences("data", Context.MODE_PRIVATE).edit()
editor.putString("name", "Tom")
editor.putInt("age", 28)
editor.putBoolean("married", false)
editor.apply()

当然,这段代码其实本身已经足够简单了,但是这种写法更多还是在用 Java 的编程思维来编写代码,而在 Kotlin 当中我们明显可以做到更好。


接下来我们就尝试使用高阶函数简化 SharedPreferences 的用法,新建一个 SharedPreferences.kt 文件,然后在里面加入如下代码:


fun SharedPreferences.open(block: SharedPreferences.Editor.() -> Unit) {
val editor = edit()
editor.block()
editor.apply()
}

这段代码虽然不长,但是涵盖了高阶函数的各种精华,下面我来解释一下。


首先,我们通过扩展函数的方式向 SharedPreferences 类中添加了一个 open 函数,并且它还接收一个函数类型的参数,因此 open 函数自然就是一个高阶函数了。


由于 open 函数内拥有 SharedPreferences 的上下文,因此这里可以直接调用 edit() 方法来获取 SharedPreferences.Editor 对象。另外 open 函数接收的是一个 SharedPreferences.Editor 的函数类型参数,因此这里需要调用 editor.block() 对函数类型参数进行调用,我们就可以在函数类型参数的具体实现中添加数据了。最后还要调用 editor.apply() 方法来提交数据,从而完成数据存储操作。


定义好了 open 函数之后,我们以后在项目中使用 SharedPreferences 存储数据就会更加方便了,写法如下所示:


getSharedPreferences("data", Context.MODE_PRIVATE).open {
putString("name", "Tom")
putInt("age", 28)
putBoolean("married", false)
}

可以看到,我们可以直接在 SharedPreferences 对象上调用 open 函数,然后在 Lambda 表达式中完成数据的添加操作。注意,现在 Lambda 表达式拥有的是 SharedPreferences.Editor 的上下文环境,因此这里可以直接调用相应的 put 方法来添加数据。最后我们也不再需要调用 apply() 方法来提交数据了,因为 open 函数会自动完成提交操作。


怎么样,使用高阶函数简化之后,不管是在易用性还是在可读性上,SharedPreferences 的用法是不是都简化了很多?这就是高阶函数的魅力所在。好好掌握这个知识点,以后在诸多其他 API 的使用方面,我们都可以使用这个技巧,让API变得更加简单。


当然,最后不得不提的是,其实 Google 提供的 KTX 扩展库中已经包含了上述 SharedPreferences 的简化用法,这个扩展库会在 Android Studio 创建项目的时候自动引入 build.gradle 的 dependencies 中。


因此,我们实际上可以直接在项目中使用如下写法来向 SharedPreferences 存储数据:


getSharedPreferences("data", Context.MODE_PRIVATE).edit {
putString("name", "Tom")
putInt("age", 28)
putBoolean("married", false)
}

可以看到,其实就是将 open 函数换成了 edit 函数,但是 edit 函数的语义性明显要更好一些。当然,我前面命名成 open 函数,主要是为了防止和 KTX 的 edit 函数同名,以免你在理解的时候产生混淆。


那么你可能会问了,既然 Google 的 KTX 库中已经自带了一个 edit 函数,我们为什么还编写这个 open 函数呢?这是因为我希望你对于高阶函数的理解不要仅仅停留在使用的层面,而是要知其然也知其所以然。KTX 中提供的功能必然是有限的,但是掌握了它们背后的实现原理,你将可以对无限的 API 进行更多的扩展。


简化 ContentValues 的用法


接下来我们开始学习如何简化 ContentValues 的用法。


ContentValues 的基本用法在 7.4 节中已经学过了,它主要用于结合 SQLiteDatabase 的 API 存储和修改数据库中的数据,具体的用法示例如下:


val values = ContentValues()
values.put("name", "Game of Thrones")
values.put("author", "George Martin")
values.put("pages", 720)
values.put("price", 20.85)
db.insert("Book", null, values)

你可能会说,这段代码可以使用 apply 函数进行简化。这当然没有错,只是我们其实还可以做到更好。


不过在正式开始我们的简化之旅之前,我还得向你介绍一个额外的知识点。还记得在 2.6.1 小节中学过的 mapOf() 函数的用法吗?它允许我们使用 "Apple" to 1 这样的语法结构快速创建一个键值对。这里我先为你进行部分解密,在 Kotlin 中使用 A to B 这样的语法结构会创建一个 Pair 对象,暂时你只需要知道这些就可以了,至于为什么,我们将在第 9 章的 Kotlin 课堂中学习。


有了这个知识前提之后,就可以进行下一步了。新建一个 ContentValues.kt 文件,然后在里面定义一个 cvOf() 方法,如下所示:


fun cvOf(vararg pairs: Pair<String, Any?>): ContentValues {

}

这个方法的作用是构建一个 ContentValues 对象,有几点我需要解释一下。首先,cvOf() 方法接收了一个 Pair 参数,也就是使用 A to B 语法结构创建出来的参数类型,但是我们在参数前面加上了一个 vararg 关键字,这是什么意思呢?其实 vararg 对应的就是 Java 中的可变参数列表,我们允许向这个方法传入 0 个、1 个、2 个甚至任意多个 Pair 类型的参数,这些参数都会被赋值到使用 vararg 声明的这一个变量上面,然后使用 for-in 循环可以将传入的所有参数遍历出来。


再来看声明的 Pair 类型。由于 Pair 是一种键值对的数据结构,因此需要通过泛型来指定它的键和值分别对应什么类型的数据。值得庆幸的是,ContentValues 的所有键都是字符串类型的,这里可以直接将 Pair 键的泛型指定成 String。但 ContentValues 的值却可以有多种类型(字符串型、整型、浮点型,甚至是 null),所以我们需要将 Pair 值的泛型指定成 Any?。这是因为 Any 是 Kotlin 中所有类的共同基类,相当于 Java 中的 Object,而 Any? 则表示允许传入空值。


接下来我们开始为 cvOf() 方法实现功能逻辑,核心思路就是先创建一个 ContentValues 对象,然后遍历 pairs 参数列表,取出其中的数据并填入 ContentValues 中,最终将 ContentValues 对象返回即可。思路并不复杂,但是存在一个问题:Pair 参数的值是 Any? 类型的,我们怎样让它和 ContentValues 所支持的数据类型对应起来呢?这个确实没有什么好的办法,只能使用 when 语句一一进行条件判断,并覆盖 ContentValues 所支持的所有数据类型。结合下面的代码来理解应该更加清楚一些:


fun cvOf(vararg pairs: Pair<String, Any?>): ContentValues {
val cv = ContentValues()
for (pair in pairs) {
val key = pair.first
val value = pair.second
when (value) {
is Int -> cv.put(key, value)
is Long -> cv.put(key, value)
is Short -> cv.put(key, value)
is Float -> cv.put(key, value)
is Double -> cv.put(key, value)
is Boolean -> cv.put(key, value)
is String -> cv.put(key, value)
is Byte -> cv.put(key, value)
is ByteArray -> cv.put(key, value)
null -> cv.putNull(key)
}
}
return cv
}

可以看到,上述代码基本就是按照刚才所说的思路进行实现的。我们使用 for-in 循环遍历了 pairs 参数列表,在循环中取出了 key 和 value,并使用 when 语句来判断 value 的类型。注意,这里将 ContentValues 所支持的所有数据类型全部覆盖了进去,然后将参数中传入的键值对逐个添加到 ContentValues 中,最终将 ContentValues 返回。


另外,这里还使用了 Kotlin 中的 Smart Cast 功能。比如 when 语句进入 Int 条件分支后,这个条件下面的 value 会被自动转换成 Int 类型,而不再是 Any? 类型,这样我们就不需要像 Java 中那样再额外进行一次向下转型了,这个功能在 if 语句中也同样适用。


有了这个 cvOf() 方法之后,我们使用 ContentValues 时就会变得更加简单了,比如向数据库中插入一条数据就可以这样写:


val values = cvOf("name" to "Game of Thrones", "author" to "George Martin", "pages" to 720, "price" to 20.85)
db.insert("Book", null, values)

怎么样?现在我们可以使用类似于 mapOf() 函数的语法结构来构建 ContentValues 对象,有没有觉得很神奇?


当然,虽然 cvOf() 方法已经非常好用了,但是它和高阶函数却一点关系也没有。因为 cvOf() 方法接收的参数是 Pair 类型的可变参数列表,返回值是 ContentValues 对象,完全没有用到函数类型,这和高阶函数的定义不符。


从功能性方面,cvOf() 方法好像确实用不到高阶函数的知识,但是从代码实现方面,却可以结合高阶函数来进行进一步的优化。比如借助 apply 函数,cvOf() 方法的实现将会变得更加优雅:


fun cvOf(vararg pairs: Pair<String, Any?>) = ContentValues().apply {
for (pair in pairs) {
val key = pair.first
when (val value = pair.second) {
is Int -> put(key, value)
is Long -> put(key, value)
is Short -> put(key, value)
is Float -> put(key, value)
is Double -> put(key, value)
is Boolean -> put(key, value)
is String -> put(key, value)
is Byte -> put(key, value)
is ByteArray -> put(key, value)
null -> putNull(key)
}
}
}

由于 apply 函数的返回值就是它的调用对象本身,因此这里我们可以使用单行代码函数的语法糖,用等号替代返回值的声明。另外,apply 函数的 Lambda 表达式中会自动拥有 ContentValues 的上下文,所以这里可以直接调用 ContentValues 的各种 put 方法。借助高阶函数之后,你有没有觉得代码变得更加优雅一些了呢?


当然,虽然我们编写了一个非常好用的 cvOf() 方法,但是或许你已经猜到了,KTX 库中也提供了一个具有同样功能的 contentValuesOf() 方法,用法如下所示:


val values = contentValuesOf("name" to "Game of Thrones", "author" to "George Martin", "pages" to 720, "price" to 20.85)
db.insert("Book", null, values)

平时我们在编写代码的时候,直接使用 KTX 提供的 contentValuesOf() 方法就可以了,但是通过本小节的学习,你不仅掌握了它的用法,还明白了它的源码实现,有没有觉得收获了更多呢?


收起阅读 »

IOS-实例化讲解RunLoop(应用于子线程)

iOS
实例化讲解RunLoop之前看过很多有关RunLoop的文章,其中要么是主要介绍RunLoop的基本概念,要么是主要讲解RunLoop的底层原理,很少用真正的实例来讲解RunLoop的,这其中有大部分原因是由于大家在项目中很少能用到RunLoop吧。基于这种原...
继续阅读 »

实例化讲解RunLoop

之前看过很多有关RunLoop的文章,其中要么是主要介绍RunLoop的基本概念,要么是主要讲解RunLoop的底层原理,很少用真正的实例来讲解RunLoop的,这其中有大部分原因是由于大家在项目中很少能用到RunLoop吧。基于这种原因,本文中将用很少的篇幅来对基础内容做以介绍,然后主要利用实例来加深大家对RunLoop的理解,本文中的代码已经上传GitHub,大家可以下载查看,有问题欢迎Issue我。本文主要分为如下几个部分:

  • RunLoop的基础知识
  • 初识RunLoop,如何让RunLoop进驻线程
  • 深入理解Perform Selector
  • 一直"活着"的后台线程
  • 深入理解NSTimer
  • 让两个后台线程有依赖性的一种方式
  • NSURLConnetction的内部实现
  • AFNetWorking中是如何使用RunLoop的?
  • 其它:利用GCD实现定时器功能
  • 延伸阅读

一、RunLoop的基本概念:

什么是RunLoop?提到RunLoop,我们一般都会提到线程,这是为什么呢?先来看下官方对RunLoop的定义:RunLoop系统中和线程相关的基础架构的组成部分(和线程相关),一个RunLoop是一个事件处理环,系统利用这个事件处理环来安排事务,协调输入的各种事件。RunLoop的目的是让你的线程在有工作的时候忙碌,没有工作的时候休眠(和线程相关)。可能这样说你还不是特别清楚RunLoop究竟是用来做什么的,打个比方来说明:我们把线程比作一辆跑车,把这辆跑车的主人比作RunLoop,那么在没有'主人'的时候,这个跑车的生命是直线型的,其启动,运行完之后就会废弃(没有人对其进行控制,'撞坏'被收回),当有了RunLoop这个主人之后,‘线程’这辆跑车的生命就有了保障,这个时候,跑车的生命是环形的,并且在主人有比赛任务的时候就会被RunLoop这个主人所唤醒,在没有任务的时候可以休眠(在IOS中,开启线程是很消耗性能的,开启主线程要消耗1M内存,开启一个后台线程需要消耗512k内存,我们应当在线程没有任务的时候休眠,来释放所占用的资源,以便CPU进行更加高效的工作),这样可以增加跑车的效率,也就是说RunLoop是为线程所服务的。这个例子有点不是很贴切,线程和RunLoop之间是以键值对的形式一一对应的,其中key是thread,value是runLoop(这点可以从苹果公开的源码中看出来)其实RunLoop是管理线程的一种机制,这种机制不仅在IOS上有,在Node.js中的EventLoop,Android中的Looper,都有类似的模式。刚才所说的比赛任务就是唤醒跑车这个线程的一个source;RunLoop Mode就是,一系列输入的source,timer以及observerRunLoop Mode包含以下几种: NSDefaultRunLoopMode,NSEventTrackingRunLoopMode,UIInitializationRunLoopMode,NSRunLoopCommonModes,NSConnectionReplyMode,NSModalPanelRunLoopMode,至于这些mode各自的含义,读者可自己查询,网上不乏这类资源;

二、初识RunLoop,如何让RunLoop进驻线程

我们在主线程中添加如下代码:

  1. while (1) {
  2. NSLog(@"while begin");
  3. // the thread be blocked here
  4. NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
  5. [runLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
  6. // this will not be executed
  7. NSLog(@"while end");
  8. }

这个时候我们可以看到主线程在执行完[runLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];之后被阻塞而没有执行下面的NSLog(@"while end");同时,我们利用GCD,将这段代码放到一个后台线程中:

  1. dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
  2. while (1) {
  3. NSLog(@"while begin");
  4. NSRunLoop *subRunLoop = [NSRunLoop currentRunLoop];
  5. [subRunLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
  6. NSLog(@"while end");
  7. }
  8. });

这个时候我们发现这个while循环会一直在执行;这是为什么呢?我们先将这两个RunLoop分别打印出来:

主线程的RunLoop


由于这个日志比较长,我就只截取了上面的一部分。
我们再看我们新建的子线程中的RunLoop,打印出来之后:

backGroundThreadRunLoop.png


从中可以看出来:我们新建的线程中:

 

  1. sources0 = (null),
  2. sources1 = (null),
  3. observers = (null),
  4. timers = (null),

我们看到虽然有Mode,但是我们没有给它soures,observer,timer,其实Mode中的这些source,observer,timer,统称为这个Modeitem,如果一个Mode中一个item都没有,则这个RunLoop会直接退出,不进入循环(其实线程之所以可以一直存在就是由于RunLoop将其带入了这个循环中)。下面我们为这个RunLoop添加个source:

  1. dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
  2. while (1) {
  3. NSPort *macPort = [NSPort port];
  4. NSLog(@"while begin");
  5. NSRunLoop *subRunLoop = [NSRunLoop currentRunLoop];
  6. [subRunLoop addPort:macPort forMode:NSDefaultRunLoopMode];
  7. [subRunLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
  8. NSLog(@"while end");
  9. NSLog(@"%@",subRunLoop);
  10. }
  11. });

这样我们可以看到能够实现了和主线程中相同的效果,线程在这个地方暂停了,为什么呢?我们明天让RunLoop在distantFuture之前都一直run的啊?相信大家已经猜出出来了。这个时候线程被RunLoop带到‘坑’里去了,这个‘坑’就是一个循环,在循环中这个线程可以在没有任务的时候休眠,在有任务的时候被唤醒;当然我们只用一个while(1)也可以让这个线程一直存在,但是这个线程会一直在唤醒状态,及时它没有任务也一直处于运转状态,这对于CPU来说是非常不高效的。
小结:我们的RunLoop要想工作,必须要让它存在一个Item(source,observer或者timer),主线程之所以能够一直存在,并且随时准备被唤醒就是应为系统为其添加了很多Item

三、深入理解Perform Selector

我们先在主线程中使用下performselector:

  1. - (void)tryPerformSelectorOnMianThread{
  2. [self performSelector:@selector(mainThreadMethod) withObject:nil]; }
  3. - (void)mainThreadMethod{
  4. NSLog(@"execute %s",__func__);
  5. // print: execute -[ViewController mainThreadMethod]
  6. }

这样我们在ViewDidLoad中调用tryPerformSelectorOnMianThread,就会立即执行,并且输出:print: execute -[ViewController mainThreadMethod];
和上面的例子一样,我们使用GCD,让这个方法在后台线程中执行

  1. - (void)tryPerformSelectorOnBackGroundThread{
  2. dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
  3. [self performSelector:@selector(backGroundThread) onThread:[NSThread currentThread] withObject:nil waitUntilDone:NO];
  4. });
  5. }
  6. - (void)backGroundThread{
  7. NSLog(@"%u",[NSThread isMainThread]);
  8. NSLog(@"execute %s",__FUNCTION__);
  9. }

同样的,我们调用tryPerformSelectorOnBackGroundThread这个方法,我们会发现,下面的backGroundThread不会被调用,这是什么原因呢?
这是因为,在调用performSelector:onThread: withObject: waitUntilDone的时候,系统会给我们创建一个Timer的source,加到对应的RunLoop上去,然而这个时候我们没有RunLoop,如果我们加上RunLoop:

  1. - (void)tryPerformSelectorOnBackGroundThread{
  2. dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
  3. [self performSelector:@selector(backGroundThread) onThread:[NSThread currentThread] withObject:nil waitUntilDone:NO];
  4. NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
  5. [runLoop run];
  6. });
  7. }

这时就会发现我们的方法正常被调用了。那么为什么主线程中的perfom selector却能够正常调用呢?通过上面的例子相信你已经猜到了,主线程的RunLoop是一直存在的,所以我们在主线程中执行的时候,无需再添加RunLoop。从Apple的文档中我们也可以得到验证:

Each request to perform a selector is queued on the target thread’s run loop and the requests are then processed sequentially in the order in which they were received. 每个执行perform selector的请求都以队列的形式被放到目标线程的run loop中。然后目标线程会根据进入run loop的顺序来一一执行。

小结:当perform selector在后台线程中执行的时候,这个线程必须有一个开启的runLoop

四、一直"活着"的后台线程

现在有这样一个需求,每点击一下屏幕,让子线程做一个任务,然后大家一般会想到这样的方式:

  1. @interface ViewController ()
  2. @property(nonatomic,strong) NSThread *myThread;
  3. @end
  4. @implementation ViewController
  5. - (void)alwaysLiveBackGoundThread{
  6. NSThread *thread = [[NSThread alloc]initWithTarget:self selector:@selector(myThreadRun) object:@"etund"];
  7. self.myThread = thread;
  8. [self.myThread start];
  9. }
  10. - (void)myThreadRun{
  11. NSLog(@"my thread run");
  12. }
  13. - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
  14. NSLog(@"%@",self.myThread);
  15. [self performSelector:@selector(doBackGroundThreadWork) onThread:self.myThread withObject:nil waitUntilDone:NO];
  16. }
  17. - (void)doBackGroundThreadWork{
  18. NSLog(@"do some work %s",__FUNCTION__);
  19. }
  20. @end

这个方法中,我们利用一个强引用来获取了后台线程中的thread,然后在点击屏幕的时候,在这个线程上执行doBackGroundThreadWork这个方法,此时我们可以看到,在touchesBegin方法中,self.myThread是存在的,但是这是为是什么呢?这就要从线程的五大状态来说明了:新建状态、就绪状态、运行状态、阻塞状态、死亡状态,这个时候尽管内存中还有线程,但是这个线程在执行完任务之后已经死亡了,经过上面的论述,我们应该怎样处理呢?我们可以给这个线程的RunLoop添加一个source,那么这个线程就会检测这个source等待执行,而不至于死亡(有工作的强烈愿望而不死亡):

  1. - (void)myThreadRun{
  2. [[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
  3. [[NSRunLoop currentRunLoop] run]
  4. NSLog(@"my thread run");
  5. }

这个时候再次点击屏幕,我们就会发现,后台线程中执行的任务可以正常进行了。
小结:正常情况下,后台线程执行完任务之后就处于死亡状态,我们要避免这种情况的发生可以利用RunLoop,并且给它一个Source这样来保证线程依旧还在

五、深入理解NSTimer

我们平时使用NSTimer,一般是在主线程中的,代码大多如下:

  1. - (void)tryTimerOnMainThread{
  2. NSTimer *myTimer = [NSTimer scheduledTimerWithTimeInterval:0.5 target:self
  3. selector:@selector(timerAction) userInfo:nil repeats:YES];
  4. [myTimer fire];
  5. }
  6. - (void)timerAction{
  7. NSLog(@"timer action");
  8. }

这个时候代码按照我们预定的结果运行,如果我们把这个Tiemr放到后台线程中呢?

  1. dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
  2. NSTimer *myTimer = [NSTimer scheduledTimerWithTimeInterval:0.5 target:self selector:@selector(timerAction) userInfo:nil repeats:YES];
  3. [myTimer fire];
  4. });

这个时候我们会发现,这个timer只执行了一次,就停止了。这是为什么呢?通过上面的讲解,想必你已经知道了,NSTimer,只有注册到RunLoop之后才会生效,这个注册是由系统自动给我们完成的,既然需要注册到RunLoop,那么我们就需要有一个RunLoop,我们在后台线程中加入如下的代码:

  1. NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
  2. [runLoop run];

这样我们就会发现程序正常运行了。在Timer注册到RunLoop之后,RunLoop会为其重复的时间点注册好事件,比如1:10,1:20,1:30这几个时间点。有时候我们会在这个线程中执行一个耗时操作,这个时候RunLoop为了节省资源,并不会在非常准确的时间点回调这个Timer,这就造成了误差(Timer有个冗余度属性叫做tolerance,它标明了当前点到后,容许有多少最大误差),可以在执行一段循环之后调用一个耗时操作,很容易看到timer会有很大的误差,这说明在线程很闲的时候使用NSTiemr是比较傲你准确的,当线程很忙碌时候会有较大的误差。系统还有一个CADisplayLink,也可以实现定时效果,它是一个和屏幕的刷新率一样的定时器。如果在两次屏幕刷新之间执行一个耗时的任务,那其中就会有一个帧被跳过去,造成界面卡顿。另外GCD也可以实现定时器的效果,由于其和RunLoop没有关联,所以有时候使用它会更加的准确,这在最后会给予说明。

六、让两个后台线程有依赖性的一种方式

给两个后台线程添加依赖可能有很多的方式,这里说明一种利用RunLoop实现的方式。原理很简单,我们先让一个线程工作,当工作完成之后唤醒另外的一线程,通过上面对RunLoop的说明,相信大家很容易能够理解这些代码:

  1. - (void)runLoopAddDependance{
  2. self.runLoopThreadDidFinishFlag = NO;
  3. NSLog(@"Start a New Run Loop Thread");
  4. NSThread *runLoopThread = [[NSThread alloc] initWithTarget:self selector:@selector(handleRunLoopThreadTask) object:nil];
  5. [runLoopThread start];
  6. NSLog(@"Exit handleRunLoopThreadButtonTouchUpInside");
  7. dispatch_async(dispatch_get_global_queue(0, 0), ^{
  8. while (!_runLoopThreadDidFinishFlag) {
  9. self.myThread = [NSThread currentThread];
  10. NSLog(@"Begin RunLoop");
  11. NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
  12. NSPort *myPort = [NSPort port];
  13. [runLoop addPort:myPort forMode:NSDefaultRunLoopMode];
  14. [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
  15. NSLog(@"End RunLoop");
  16. [self.myThread cancel];
  17. self.myThread = nil;
  18. }
  19. });
  20. }
  21. - (void)handleRunLoopThreadTask
  22. {
  23. NSLog(@"Enter Run Loop Thread");
  24. for (NSInteger i = 0; i < 5; i ++) {
  25. NSLog(@"In Run Loop Thread, count = %ld", i);
  26. sleep(1);
  27. }
  28. #if 0
  29. // 错误示范
  30. _runLoopThreadDidFinishFlag = YES;
  31. // 这个时候并不能执行线程完成之后的任务,因为Run Loop所在的线程并不知道runLoopThreadDidFinishFlag被重新赋值。Run Loop这个时候没有被任务事件源唤醒。
  32. // 正确的做法是使用 "selector"方法唤醒Run Loop。 即如下:
  33. #endif
  34. NSLog(@"Exit Normal Thread");
  35. [self performSelector:@selector(tryOnMyThread) onThread:self.myThread withObject:nil waitUntilDone:NO];
  36. // NSLog(@"Exit Run Loop Thread");
  37. }

七、NSURLConnection的执行过程

在使用NSURLConnection时,我们会传入一个Delegate,当我们调用了[connection start]之后,这个Delegate会不停的收到事件的回调。实际上,start这个函数的内部会获取CurrentRunloop,然后在其中的DefaultMode中添加4个source。如下图所示,CFMultiplexerSource是负责各种Delegate回调的,CFHTTPCookieStorage是处理各种Cookie的。如下图所示:

NSURLConnection的执行过程


从中可以看出,当开始网络传输是,我们可以看到NSURLConnection创建了两个新的线程:com.apple.NSURLConnectionLoader和com.apple.CFSocket.private。其中CFSocket是处理底层socket链接的。NSURLConnectionLoader这个线程内部会使用RunLoop来接收底层socket的事件,并通过之前添加的source,来通知(唤醒)上层的Delegate。这样我们就可以理解我们平时封装网络请求时候常见的下面逻辑了:

 

  1. while (!_isEndRequest)
  2. {
  3. NSLog(@"entered run loop");
  4. [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
  5. }
  6. NSLog(@"main finished,task be removed");
  7. - (void)connectionDidFinishLoading:(NSURLConnection *)connection
  8. {
  9. _isEndRequest = YES;
  10. }

这里我们就可以解决下面这些疑问了:

  1. 为什么这个While循环不停的执行,还需要使用一个RunLoop? 程序执行一个while循环是不会耗费很大性能的,我们这里的目的是想让子线程在有任务的时候处理任务,没有任务的时候休眠,来节约CPU的开支。
  2. 如果没有为RunLoop添加item,那么它就会立即退出,这里的item呢? 其实系统已经给我们默认添加了4个source了。
  3. 既然[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];让线程在这里停下来,那么为什么这个循环会持续的执行呢?因为这个一直在处理任务,并且接受系统对这个Delegate的回调,也就是这个回调唤醒了这个线程,让它在这里循环。

八、AFNetWorking中是如何使用RunLoop的?

在AFN中AFURLConnectionOperation是基于NSURLConnection构建的,其希望能够在后台线程来接收Delegate的回调。
为此AFN创建了一个线程,然后在里面开启了一个RunLoop,然后添加item

  1. + (void)networkRequestThreadEntryPoint:(id)__unused object {
  2. @autoreleasepool {
  3. [[NSThread currentThread] setName:@"AFNetworking"];
  4. NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
  5. [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
  6. [runLoop run];
  7. }
  8. }
  9. + (NSThread *)networkRequestThread {
  10. static NSThread *_networkRequestThread = nil;
  11. static dispatch_once_t oncePredicate;
  12. dispatch_once(&oncePredicate, ^{
  13. _networkRequestThread = [[NSThread alloc] initWithTarget:self selector:@selector(networkRequestThreadEntryPoint:) object:nil];
  14. [_networkRequestThread start];
  15. });
  16. return _networkRequestThread;
  17. }

这里这个NSMachPort的作用和上文中的一样,就是让线程不至于在很快死亡,然后RunLoop不至于退出(如果要使用这个MachPort的话,调用者需要持有这个NSMachPort,然后在外部线程通过这个port发送信息到这个loop内部,它这里没有这么做)。然后和上面的做法相似,在需要后台执行这个任务的时候,会通过调用:[NSObject performSelector:onThread:..]来将这个任务扔给后台线程的RunLoop中来执行。

  1. - (void)start {
  2. [self.lock lock];
  3. if ([self isCancelled]) {
  4. [self performSelector:@selector(cancelConnection) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];
  5. } else if ([self isReady]) {
  6. self.state = AFOperationExecutingState;
  7. [self performSelector:@selector(operationDidStart) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];
  8. }
  9. [self.lock unlock];
  10. }

GCD定时器的实现

  1. - (void)gcdTimer{
  2. // get the queue
  3. dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
  4. // creat timer
  5. self.timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
  6. // config the timer (starting time,interval)
  7. // set begining time
  8. dispatch_time_t start = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC));
  9. // set the interval
  10. uint64_t interver = (uint64_t)(1.0 * NSEC_PER_SEC);
  11. dispatch_source_set_timer(self.timer, start, interver, 0.0);
  12. dispatch_source_set_event_handler(self.timer, ^{
  13. // the tarsk needed to be processed async
  14. dispatch_async(dispatch_get_global_queue(0, 0), ^{
  15. for (int i = 0; i < 100000; i++) {
  16. NSLog(@"gcdTimer");
  17. }
  18. });
  19. });
  20. dispatch_resume(self.timer);
  21. }

链接: https://blog.csdn.net/qq_22389025/article/details/85264178

收起阅读 »

看完 React 哲学,我悟了

前言 最近测试给我提的的 bug 终于少了很多, 在 codeReview 的时候同事们也很少指出我那个地方写的不对 反而对我整体的文件结构和组件的编写结构及状态的设计提出了更高的要求,不得不说我这代码水平还是有所提高的,表示在稳步提升的过程还有很大的进步空...
继续阅读 »

前言


最近测试给我提的的 bug 终于少了很多, 在 codeReview 的时候同事们也很少指出我那个地方写的不对


反而对我整体的文件结构和组件的编写结构及状态的设计提出了更高的要求,不得不说我这代码水平还是有所提高的,表示在稳步提升的过程还有很大的进步空间


但是当我在看到同事给我说的整个组件如何分离才能提高维护性和复用性,别人在看的时候也能更清晰的知道这部分的逻辑


当时我就好奇,为啥同事能有这种见解,难道只是因为经验比我多,思考比我深入吗?我的直觉告诉我没有这么简单。


如何在看到设计图的时候就想好如何划分这块业务的逻辑,如何设计自己想要的数据结构,我在脑中思索着,忽然我想起初学 React 的时候那个被我撇过一眼就速度滑过的概念 React 哲学


我立马去官网看着概念, 果然那些我以前我以前对于一个组件不知道证明设计结构和状态,这些组件写到后面状态不通,还有我几乎还会在每个子组件都请求一遍数据,对于一些想显示的数据都定义一个 state 来简单粗暴的解决


在写一个模块的时候我几乎是马上就着手去写,往往都是没有任何思考和设计只是想到什么就写什么,只想着赶快把功能学完,以至于写出很多缝缝补补不合理的代码让我踩过很多坑,收获很多 bug ,在看到之前的组件,我的第一想法就是重构。


说了这么多我的血泪史,我们看一下 React 哲学到底是说的什么,它都是如何解决我上述的痛点的,我又因此悟到了什么?


准备阶段


首先在我们写代码之前肯定会有的是会有的一是 PM 的产品设计图,二是后端同学返回的 JSON 数据


image-20210801203507483的副本.png


[
{category: "Sporting Goods", price: "$49.99", stocked: true, name: "Football"},
{category: "Sporting Goods", price: "$9.99", stocked: true, name: "Baseball"},
{category: "Sporting Goods", price: "$29.99", stocked: false, name: "Basketball"},
{category: "Electronics", price: "$99.99", stocked: true, name: "iPod Touch"},
{category: "Electronics", price: "$399.99", stocked: false, name: "iPhone 5"},
{category: "Electronics", price: "$199.99", stocked: true, name: "Nexus 7"}
];


先理解这样一个简单的产品设计图所包含的需求都有哪些,这是一个展示商品的列表,用户可以在对商品进行关键字搜索,并且通过点击复选框选择是是否展示现货,商品列表包含商品名和价格,商品支持分类显示,其中告罄的商品名为红色显示。


当我们基本了解产品图所表达的需求之后,就可以开始代码编写的第一步了


通过产品图划分组件层级


在一开始不太熟练划分的时候可以在产品设计稿上通过画方框来确定组件和子组件,可以报组件当成一个函数或者对象来看,组件同样遵照单一功能原则,也就是说一个组件只负责一个功能


同时一个好的 JSON 数据模型也应该是和组件一一对应的,组件与数据模型中的某个部分匹配


image-20210801205928730的副本.png


不同的颜色划分成不同的组件,可以分成五部分:



  1. FilterableProductTable (橙色): 是整个示例应用的整体

  2. SearchBar (蓝色): 接受所有的用户输入

  3. ProductTable (绿色): 展示数据内容并根据用户输入筛选结果

  4. ProductCategoryRow (天蓝色): 为每一个产品类别展示标题

  5. ProductRow (红色): 每一行展示一个产品


组件名应该能让人迅速 get 到这个组件的写的是什么(不得不说我之前的组件命名真的太糟糕了,过几天回头看都是一脸懵逼的那种


组件的层级划分:



  • FilterableProductTable

    • SearchBar

    • ProductTable

      • ProductCategoryRow

      • ProductRow






React 构建静态页面


当我们划分好了组件层级之后可以来写代码了,先利用已有数据模型来写一个不包含交互的UI渲染,这是因为UI渲染的代码比较多,交互要考虑的细节比较多,把这两个过程分开写不容易漏掉一些细节,整个思路也比较清晰


通过复用编写的组件,使用 props 来进行数据的传递,父组件把数据进行层层的传递,这也是 React 的一个特点就是单向数据流动,在这个过程中先不使用 state ,因为 state 表示的是会随着时间变化而变化的,所以在交互的过程中使用


构建应用的时候可以使用自上而下或者自上而下的方法,自上而下表示先写层级最高的组件,如FilterableProductTable 组件,这种比较适合简单的应用; 自下而上表示先写层级最低的组件,如 ProductRow 组件,这种方法比较适合大型的应用构建


const PRODUCTS = [
{category: 'Sporting Goods', price: '$49.99', stocked: true, name: 'Football'},
{category: 'Sporting Goods', price: '$9.99', stocked: true, name: 'Baseball'},
{category: 'Sporting Goods', price: '$29.99', stocked: false, name: 'Basketball'},
{category: 'Electronics', price: '$99.99', stocked: true, name: 'iPod Touch'},
{category: 'Electronics', price: '$399.99', stocked: false, name: 'iPhone 5'},
{category: 'Electronics', price: '$199.99', stocked: true, name: 'Nexus 7'}
];

const FilterableProductTable = () => (
<div>
<SearchBar />
<ProductTable products={PRODUCTS} />
</div>
);

const SearchBar = () => (
<form>
<input type="text" placeholder="Search..." />
<p>
<input type="checkbox" /> Only show products in stock
</p>
</form>
);

const ProductTable = ({ products }) => {
const rows = [];
let lastCategory = null;

products.forEach((product) => {
if (product.category !== lastCategory) {
rows.push(
<ProductCategoryRow
category={product.category}
key={product.category}
/>
);
rows.push(<ProductRow product={product} key={product.name} />);
}
lastCategory = product.category;
});

return (
<table>
<thead>
<tr>
<th>Name</th>
<th>Price</th>
</tr>
</thead>
<tbody>{rows}</tbody>
</table>
);
};

const ProductCategoryRow = ({ category }) => (
<tr>
<td colSpan="2">{category}</td>
</tr>
);

const ProductRow = ({ product }) => {
const name = product.stocked ? (
product.name
) : (
<span style={{ color: "red" }}>{product.name}</span>
);

return (
<tr>
<td>{name}</td>
<td>{product.price}</td>
</tr>
);
};


确定 state 的最小且完整的集合


当我们一些其他的数据来触发改变基础数据,让UI具有交互结果,在 React 中就可以使用 state 来表示


最小且完整的表示在于我们可以先找到一些会根据时间产生变化的全部数据,再从这些数据中选出最必要的数据作为 state ,其他数据能通过计算得到。


看一下当前应用有哪些数据:



  • 商品的原始数据

  • 用户的搜索数据

  • 复选框是否选中的值

  • 经过筛选后的数据


在确定这些数据能否成为 state 可以先问一下自己这几个问题



  • 数据是否能通过 props 来传递

  • 是否会通过时间而产生改变

  • 是否可以通过其他 stateprops 计算得到


那么最后我们就可以确认,原始数据可以通过 props 传递,用户搜索的数据和复选框的值可以作为 state ,筛选后的数据可以通过原始数据和用户搜索数据以及复选框数据计算得来。所以最后 state 可以是:



  • 用户的搜索数据

  • 复选框是否选中的值


确定 state 放置的位置


当确定了 state 的最小集合之后,接下来就该确定 state 应该放置在哪个组件里


在前面我们知道了 React 是单向的数据流,自上而下的流动,所以我们应把 state 写在共同所有者(也就是需要这些 state 的组件的共同父组件)


我们可以看到 ProductRow 组件需要筛选后的数据, SearchBar 组件需要搜索的数据和复选框的值, 所以就可以把 state 放在它们的共同所有者组件 FilterableProductTable 组件里,再通过 props 来进行 state 的传递


添加反向数据流


当我们要通过层级较低的组件改变层级较高的组件,就需要通过反向数据流的方式


React 中的反向数据流是通过需要高层级组件通过 props 把改变 state 的方法 (回调函数) 传递给层级较低的组件,子组件 state 的改变后的值传给这个回调函数。


在当前应用中如果想要拿到最新的 state 就需要FilterableProductTable 必须将一个能够触发 state 改变的回调函数(callback)传递给 SearchBar。我们可以使用输入框的 onChange 事件来监视用户输入的变化,并通知 FilterableProductTable 传递给 SearchBar 的回调函数。


const FilterableProductTable = () => {
const [filterText, setFilterText] = React.useState("");
const [inStockOnly, setInStockOnly] = React.useState(false);

return (
<div>
<SearchBar
filterText={filterText}
setFilterText={setFilterText}
inStockOnly={inStockOnly}
setInStockOnly={setInStockOnly}
/>
<ProductTable
products={PRODUCTS}
inStockOnly={inStockOnly}
filterText={filterText}
/>
</div>
);
};

const SearchBar = ({
filterText,
setFilterText,
inStockOnly,
setInStockOnly,
}) => {
const handleProductsSearch = (value) => {
setFilterText(value);
};

const handleStockCheck = (value) => {
setInStockOnly(value);
};

return (
<form>
<input
type="text"
placeholder="Search..."
value={filterText}
onChange={handleProductsSearch}
/>
<p>
<input
type="checkbox"
value={inStockOnly}
onChange={handleStockCheck}
/>{" "}
Only show products in stock
</p>
</form>
);
};

const ProductTable = ({ products, inStockOnly, filterText }) => {
const rows = [];
let lastCategory = null;

products.forEach((product) => {
if (product.name.indexOf(filterText) === -1) {
return;
}
if (inStockOnly && !product.stocked) {
return;
}
if (product.category !== lastCategory) {
rows.push(
<ProductCategoryRow
category={product.category}
key={product.category}
/>
);
rows.push(<ProductRow product={product} key={product.name} />);
}
lastCategory = product.category;
});

return (
<table>
<thead>
<tr>
<th>Name</th>
<th>Price</th>
</tr>
</thead>
<tbody>{rows}</tbody>
</table>
);
};

总结


React 哲学并没有对深奥的道理,相反它更倡导我们把代码写得更加简洁清晰,更具有模块化,这一点在写大型的项目尤为重要,在写代码之前就把大致的结构和涉及的数据结构设计好,会减少 Bug 的产生,减少重构的时间,减少维护的成本。


链接:https://juejin.cn/post/6991650159935356935

收起阅读 »

淘宝详情页分发推荐算法总结:用户即时兴趣强化

商品详情页是手淘内流量最大的模块之一,它加载了数十亿级商品的详细信息,是用户整个决策过程必不可少的一环。这个区块不仅要承接用户对当前商品充分感知的诉求,同时也要能肩负起其他来源导流流量的留存,最终尽可能地激活平台内部流量以及外部流量在整个生态中的活跃度。同时,...
继续阅读 »

商品详情页是手淘内流量最大的模块之一,它加载了数十亿级商品的详细信息,是用户整个决策过程必不可少的一环。这个区块不仅要承接用户对当前商品充分感知的诉求,同时也要能肩负起其他来源导流流量的留存,最终尽可能地激活平台内部流量以及外部流量在整个生态中的活跃度。同时,商品详情页也是众多场景乃至平台链接的纽带,用户在平台中的行为轨迹总会在多场景和详情页间不断交替,并在详情页产生进一步的行为决策(加购/购买等)。因而详情页上除了具备承接用户的“了解更多”的诉求,也应同时满足平台“起承转合中间件”的诉求。


详情页内流量具备两个显著的特性:



  1. 流量大,常是用户购买决策环节;

  2. 承接了大量的外部引流。


出于这两个重要特性,同时也出于提升平台黏度,尽可能地提升用户行为的流畅度的产品设计考量,我们在详情页内部设立了一些全网分发场景,并基于这些场景特点进行了一些算法探索。


背景


信息爆炸导致用户对于海量信息的触达寥若晨星,对于有效信息的触达更是凤毛麟角。如果说社交媒体是无声者的发声者,那推荐系统俨然可以看作是海量信息的发声者,同时也是平台用户被曝光信息的制造者。所以我们有责任与义务做到推荐内容的保质与品控,这对于推荐系统是极大的诉求与挑战。当下的推荐系统通过深度挖掘用户行为,对用户进行个性化需求挖掘与实时兴趣捕捉,旨在于帮助用户在海量信息中快速,精准地定位,从而更好的完成智能化服务。


详情页的分发推荐肩负着【服务商家】,【提升用户使用体验】以及【利好平台分发效能】的重要责任。这给我们场景提出了三个方面不同侧重的需求,它们需要被统筹兼顾,以期能够打造出一个更好的流量分发阵地。我们解决这三个需求的方式是开辟同店商品推荐前置的全网分发模块,在极大程度保证商家权益的同时,让用户能够在一个聚焦的页面快速定位海量商品中“猜你喜欢”的商品。详情页内的推荐和公域推荐有一个最大的差异:每个详情页面都是主商品的信息衍生场,推荐内容受到它较强的约束。现有的大多数研究缺乏对具有先验信息的场景的探索:它们只强调用户的个性化兴趣。有一些重要的、直接相关的先验信息被直接忽略。我们观察到,在单个商品/主题唤醒的推荐页面上,用户的点击行为和主商品(唤醒推荐页面的商品/主题)是高度同质的。在这些场景下,用户已经通过主商品给模型传达了一个很聚焦很明确的意图,所以推荐的相关结果不能肆意泛化。但同时,一味的聚集又回降低分发的效能,使得用户在浏览过程中产生疲劳感。因而这些场景的推荐内容,应当遵循“意图明确,适度发散”的策略。当然,因为有主商品信息的加持,我们在模型调优时能够因地制宜地架构推荐策略,做出一些和其他场景相比,更明确更可解释的用户体验,这是我们写这篇文章的初衷。如果对这样的“以品推品”场景想要知道更多的细节,本篇文章将带您一起来看我们的探索问题——“用户即时兴趣强化与延伸”,以及模型解法和线上工程实践。


场景介绍


其中,全网流量分发场景主要包括详情页底部信息流(邻家好货),主图横滑(新增),加购弹层(新增)。这些场景打破了商家私域画地为牢的局面,充分地提升了私域全网分发的能效。当然为了兼顾商家利益,这些场景将分为两个部分(同店内容推荐模块和跨店内容推荐模块)。


image.png


image.png


技术探索


算法问题定义——即时兴趣强化


进入详情页是用户主动发起的行为,因而用户对于当前页面的主商品有着较强的兴趣聚焦。主商品的信息能够帮助我们快速地定位用户的即时兴趣,这对于推荐算法来说是至关重要的。虽然现在有很多方法将行为序列的末位替代即时兴趣,或是使用模型挖掘即时兴趣,但这些方法均是在不确定事件中进行推理,没有详情页天然带有主商品这样的强意图信息。基于此,我们的工作将从推荐技术的不同方面,将这部分信息建模并加以强化,以期使得详情页分发场景能够结合场景特点,尽可能地满足用户的即时需求。


召回


背景


随着深度学习技术在多个领域的普及以及向量检索技术的兴起,一系列基于类似思想的深度学习召回技术相继涌现。Youtube在2016年提出了DNN在推荐系统做召回的思路,它将用户历史行为和用户画像信息相结合,极大地提升了匹配范围的个性化和丰富性。我们的工作基于同组师兄的召回工作《SDM: 基于用户行为序列建模的深度召回》,《User-based Sequential Deep Match》 也是这一思路的一脉相承。SDM能够很好地建模用户兴趣的动态变化,并且能够综合长短期行为在不同维度进行用户表征,从而更好的使用低维向量表达用户和商品,最终借助大规模向量检索技术完成深度召回。SDM上线较base(多路i2i召回merge)ipv指标提升了2.80%。较SDM模型,CIDM模型IPV提升4.69%。在此基础上,为了契合详情页分发场景的特点,我们丰富并挖掘了主商品相关信息,并将其作为即时兴趣对召回模型进行结构改良。


模型——CIDM(Current Intention Reinforce Deep Match )


image.png


为了能够让模型SDM能够将主商品信息catch到并与用户行为产生交互,我们设计了如下的模型结构,其中trigger即为详情页中的主商品,我们从几个方面对它进行表征及强化:



  1. Trigger-Layer:启发于论文1,对主商品显式建模:除SDM中建模用户长、短期偏好之外,引入用户即时偏好层将主商品特征与长短期偏好融合作为用户最终表达;

  2. Trigger-Attention: 即将原模型中使用的self-attention改为由trigger作为目标的target-attention;

  3. Trigger-Lstm:借鉴论文2中的建模思路,我们将lstm的结构中引入了trigger信息,并添加trigger-gate让lstm倾向于记住更多关于主商品的内容;

  4. Trigger-filter-sequence:实验发现,使用主商品的叶子类目,一级类目过滤得到的序列作为原序列的补充进行召回建模,能够增加收益,故在数据源中添加了cate-filter-seq以及cat1-filter-sequece。


其中前两个点都是比较显而易见的,这里就不再赘述,我们将三四两个创新点详细阐述。


论文2中论证了添加时间门能够更好地捕捉用户的短期和长期兴趣,基于这个结论,我们尝试设计一个trigger-gate用于在模型捕获序列特征中引入trigger信息的影响。我们尝试了多种结构变体,比较work的两种方式(如图):



  1. 将trigger信息作为记忆门的一路输入,即通过sigmoid函数后与之前想要更新的信息相乘;

  2. 平行于第一个记忆门,添加一个新的即时兴趣门,其输入为细胞输入以及当前主商品,和记忆门结构一致。


这样的方式能够将主商品的信息保留的更充分。


image.png


第一种方法,仅是对记忆门进行了修改:


image.png


第二种方法,新加了一个即时兴趣门:


image.png


image.png


这两个实验在离线hr指标分别增长+1.07%. 1.37%,最优版本线上指标ipv+1.1%。


出于我们自己的实验结论:"使用主商品的叶子类目和一级类目过滤得到的序列作为原始序列的补充,作为模型输入能够提升预测准度“。这说明,主商品的结构信息是具有明显的效益的,以它为条件能够对序列样本产生正向约束。究其根本,原始序列中一些和当前主商品相关性较小的样本被过滤掉了,这相当于对数据进行去噪处理。沿着这个思路,联想到自编码机的主要应用为数据降噪与特征降维,故考虑采用基于AE结构的模型对序列进行处理,更多的,由于我们是定向去噪(即剔除与主商品不相关的行为),我们使用变分自编码机(VAE),借主商品信息在隐变量空间对序列表达进行约束,以确保隐层能较好抽象序列数据的特点。


变分自编码机是具有对偶结构(包括编码器和解码器)联合训练的系列模型,它借鉴变分推断的思路,在隐变量空间进行个性化定制,比较契合我们即使兴趣建模的需求。首先我们有一批数据样本图片,其似然分布可以表示为图片,最大化其对数似然时后验概率分布图片是不可知的,因而VAEs用自定义分布图片来近似真实的后验概率图片计算,使用KL散度作为两个分布的相似程度的度量。整体的优化函数可以表示为:


image.png


具体推导可以参见论文5。其中第一项作为使假设的后验分布图片和先验分布图片尽量接近,第二项为重构损失,保证自编码结构整体的稳定性。其中,先验分布图片是我们自定义的,这里想要将主商品的信息融入其中,因而我们假设图片,即使用主商品的表示作为高斯分布的均值,采样batch的二阶矩作为高斯分布的方差带入其中。因此,模型的优化函数变为:


image.png


启发于论文3、4, 我们将网络结构设计为如下形式,使用主商品的特征向量作为mu和sigma引入到变分自编码网络中,规范隐空间中序列特征的表达,并将学习得到的序列隐空间变量seq_hid作为用户的强意图序列表达trigger_emb,和长短期偏好融合。


image.png


这实验在离线hr指标增长+2.23%,线上未测试。


效果


较SDM模型,CIDM模型线上效果IPV提升4.69%。


精排


背景


精排模型基于DIN(Deep Interest Networks)进行探索与发展,我们的想法是在序列信息基础之上融入主商品更多的信息。序列信息挖掘和主商品信息强化其实是我们场景两个需求的外化,主商品信息强化能够很好地抓住用户即时意图,满足用户即时的聚焦需求;而序列信息挖掘是基于当前意图的延伸,能够一定程度上对意图进行发散,使推荐结果不会产生过于集中而带来体验疲劳。当然这两方面需要权衡,让模型识别其中“聚”,“散”的时机与程度。在此基础上,我们进行了1、挖掘主商品更多的语义信息;2、强化主商品信息对于序列特征抽取的指引与影响。


精排模型——DTIN(Deep Trigger-based Interest Network)


首先,我们希望能够挖掘主商品更多的语义信息,这一部分,我们将主商品(trigger)相关的特征和待打分商品(candidate)对齐,然后将这部分特征直接拼到模型的wide侧,让模型提升对于主商品表征的敏感度。


其次,由于DIN的motivation是引入注意力机制来更精准的捕获用户的兴趣点,作为比待打分商品更强的用户兴趣点体现,我们设计了一个双attention结构来强化这部分信息。如图所示,首先,将trigger和candidate商品特征concat,传入第一层attention结构中,学得第一层加权向量图片。这部分权值融合了trigger和candidate的信息,它可以被看作基于主商品及待打分商品交叉的用户兴趣提取。然后,仅使用主商品信息作为查询query传入第二层attention结构中,学得第二层加权向量图片,它可以被看作仅基于即时兴趣的延伸兴趣捕获。之后这两个权重向量按位相乘作为序列加权向量。模型结构设计这部分经历了大量的探索实验,如果有兴趣欢迎大家一起来讨论,这里只呈现我们实验中效果最佳版本。


image.png


效果


较DIN模型,DTIN模型IPV提升9.34%, 对应离线实验auc提升4.6%,gauc提升5.8%。


粗排


动机


粗排模型为的是解决推荐系统应用于工业界的特殊问题,在召回集合较大时,精排模型因复杂度太高而无法保证打分效率。因而粗排模型应运而生。由于详情页分发场景需要从全网亿级商品中进行商品召回,且召回阶段使用了多种召回方式的组合(包括i2i, 向量召回等)。这使得召回数量级较大,而且多路召回存在交叉使得匹配特征不在同一尺度上,这给后续的精排模型带来了较大的压力。基于此,我们开发了桥接召回和精排两部分的粗排模块,它的目标是对召回结果进行初筛,不仅需要兼顾效率与精度,也需要具有兼容多尺度召回方式的能力。基于我们的场景特点,在粗排初筛阶段进行了基于主商品的即时意图的建模。


模型——Tri-tower(Triple-tower Preparatory Ranking Framework)


出于粗排模型对于效率的要求不能构建过于复杂的结构,基于双塔粗排模型,我们针对强化即时兴趣的方向新添加了一个主商品塔trigger-tower,该塔和商品塔的特征保持一致,在顶端输出logits后和商品塔做交叉,作为之前双塔模型的补充添加在sigmoid函数的输入中。模型结构如下:


image.png


其中 Trigger net 和 Item net 使用 item 侧更轻量的一些统计类特征,User net也在deep match的基础上对大规模的id类特征进行了筛检。确保粗排模型轻量且服务快速。最终三塔粗排模型较无粗排模型,IPV指标提升3.96%。


总结


总体来看,详情页分发场景的优化思路比较统一,都是对主商品信息进行挖掘,并在模型中将用户历史行为进行关联加强。我们和传统的兴趣挖掘网络相比,附增了一道关口(即时兴趣强化),将那些明确的,和当前最相关的意图保留下来。通过这样的方式,推荐的结果就有一定程度的收敛。同时,多元兴趣在模型中并没有被完全抹去,只是通过attention网络动态调权来影响结果的发散程度,这也确保我们推荐结果一定的个性化和可发散性。


至此已阐述完“用户即时兴趣强化与延伸”课题在私域分发场景三个主要环节:召回-粗排-精排上面的有收益的尝试,当然这个过程也伴随着很多失败的探索,无论是模型优化和工程实践上的阻塞,都给我们带来了丰硕的实践经验。除了这三个主要模型外,我们在策略和其他环节的模型上也都针对该问题进行了优化,这里不再赘述。如果您对细节或者后续的优化方向感兴趣,欢迎与我们联系。


引用



  1. Tang, Jiaxi, et al. "Towards neural mixture recommender for long range dependent user sequences." The World Wide Web Conference. 2019.

  2. Zhu, Yu, et al. "What to Do Next: Modeling User Behaviors by Time-LSTM." IJCAI. Vol. 17. 2017

  3. Liang, Dawen, et al. "Variational autoencoders for collaborative filtering." Proceedings of the 2018 world wide web conference. 2018.

  4. Li, Xiaopeng, and James She. "Collaborative variational autoencoder for recommender systems." Proceedings of the 23rd ACM SIGKDD international conference on knowledge discovery and data mining. 2017.

  5. Zhao, Shengjia, Jiaming Song, and Stefano Ermon. "Towards deeper understanding of variational autoencoding models." arXiv preprint arXiv:1702.08658 (2017).


链接:https://juejin.cn/post/6992169847207493639

收起阅读 »

用three.js写一个3D地球

着色器的入门介绍 Webgl绘制图形是基于着色器(shader)的绘图机制,着色器提供了灵活且强大的绘制二维或三维图形的方法,所有Webgl程序必须使用它。 着色器语言类似于c语言,当我们写webgl程序时,着色器语言以字符串的形式嵌入在javascrip...
继续阅读 »

着色器的入门介绍



Webgl绘制图形是基于着色器(shader)的绘图机制,着色器提供了灵活且强大的绘制二维或三维图形的方法,所有Webgl程序必须使用它。



着色器语言类似于c语言,当我们写webgl程序时,着色器语言以字符串的形式嵌入在javascript语言中。


比如,要在屏幕上绘制一个点,代码如下:


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
body {
margin: 0
}
</style>
</head>
<body>
<canvas id="webgl"></canvas>
</body>
<script>
//将canvas的大小设置为屏幕大小
var canvas = document.getElementById('webgl')
canvas.height = window.innerHeight
canvas.width = window.innerWidth

//获取webgl绘图上下文
var gl = canvas.getContext('webgl')

//将背景色设置为黑色
gl.clearColor(0.0, 0.0, 0.0, 1.0)
gl.clear(gl.COLOR_BUFFER_BIT)

//顶点着色器代码(字符串形式)
var VSHADER_SOURCE =
`void main () {
gl_Position = vec4(0.5, 0.5, 0.0, 1.0); //点的位置:x: 0.5, y: 0.5, z: 0。齐次坐标
gl_PointSize = 10.0; //点的尺寸,非必须,默认是0
}`

//片元着色器代码(字符串形式)
var FSHADER_SOURCE =
`void main () {
gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0); //点的颜色:四个量分别代表 rgba
}`

//初始化着色器
initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE)

//绘制一个点,第一个参数为gl.POINTS
gl.drawArrays(gl.POINTS, 0, 1)

function initShaders(gl, vshader, fshader) {
var program = createProgram(gl, vshader, fshader);
if (!program) {
console.log('Failed to create program');
return false;
}
gl.useProgram(program);
gl.program = program;
return true;
}

function createProgram(gl, vshader, fshader) {
var vertexShader = loadShader(gl, gl.VERTEX_SHADER, vshader);
var fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fshader);
if (!vertexShader || !fragmentShader) {
return null;
}
var program = gl.createProgram();
if (!program) {
return null;
}
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
var linked = gl.getProgramParameter(program, gl.LINK_STATUS);
if (!linked) {
var error = gl.getProgramInfoLog(program);
console.log('Failed to link program: ' + error);
gl.deleteProgram(program);
gl.deleteShader(fragmentShader);
gl.deleteShader(vertexShader);
return null;
}
return program;
}

function loadShader(gl, type, source) {
// 创建着色器对象
var shader = gl.createShader(type);
if (shader == null) {
console.log('unable to create shader');
return null;
}
gl.shaderSource(shader, source);
gl.compileShader(shader);
var compiled = gl.getShaderParameter(shader, gl.COMPILE_STATUS);
if (!compiled) {
var error = gl.getShaderInfoLog(shader);
gl.deleteShader(shader);
return null;
}
return shader;
}
</script>
</html>

上面代码在屏幕右上区域绘制了一个点。


image.png


绘制这个点需要三个必要的信息:位置、尺寸和颜色。



  • 顶点着色器指定点的位置和尺寸。(下面的代码中,gl_Positiongl_PointSizegl_FragColor 都是着色器的内置全局变量。)


var VSHADER_SOURCE = 
`void main () {
gl_Position = vec4(0.5, 0.5, 0.0, 1.0); //指定点的位置
gl_PointSize = 10.0; //指定点的尺寸
}`


  • 片元着色器指定点的颜色。


var FSHADER_SOURCE = 
`void main () {
gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0); //指定点的颜色
}`

attribute变量 和 uniform变量


上面的例子中,我们直接在着色器中指定了点的位置、尺寸和颜色。而实际操作中,这些信息基本都是由js传递给着色器。


用于 js代码 和 着色器代码 通信的变量是attribute变量uniform变量


使用哪一种变量取决于需要传递的数据本身,attribute变量用于传递与顶点相关的数据,uniform变量用于传递与顶点无关的数据。


下面的例子中,要绘制的点的坐标将由js传入。


  //顶点着色器
var VSHADER_SOURCE =
`attribute vec4 a_Position; //声明一个attribute变量a_Position,用于接受js传递的顶点位置
void main () {
gl_Position = a_Position; //将a_Position赋值给gl_Position
gl_PointSize = 10.0;
}`

//片元着色器
var FSHADER_SOURCE =
`void main () {
gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
}`

initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE)

//js代码中,获取a_Position的存储位置,并向其传递数据
var a_Position = gl.getAttribLocation(gl.program, 'a_Position')
gl.vertexAttrib3f(a_Position, 0.5, 0.5, 0.0)

gl.drawArrays(gl.POINTS, 0, 1)

varying变量


我们从js传给着色器的通常是顶点相关的数据,比如我们要绘制一个三角形,三角形的顶点位置和顶点颜色由js传入。三个顶点的位置可以确定三角形的位置,那么整个三角形的颜色由什么确定呢?


这就需要varying变量出场了。


webgl中的颜色计算:


顶点着色器中,接收js传入的每个顶点的位置和颜色数据。webgl系统会根据顶点的数据,插值计算出,顶点之间区域中,每个片元(可以理解为组成图像的最小渲染点)的颜色值。插值计算由webgl系统自动完成。


计算出的每个片元的颜色值,再传递给 片元着色器片元着色器根据每个片元的颜色值渲染出图像。


顶点着色器片元着色器,传递工作由varying变量完成。


image.png


代码如下。



  • 顶点着色器代码


var VSHADER_SOURCE = 
`attribute vec4 a_Position; //顶点位置
attribute vec4 a_Color; //顶点颜色
varying vec4 v_Color; //根据顶点颜色,计算出三角形中每个片元的颜色值,然后将每个片元的颜色值传递给片元着色器。
void main () {
gl_Position = a_Position;
v_Color = a_Color; // a_Color 赋值给 v_Color
}`


  • 片元着色器代码


var FSHADER_SOURCE = 
`precision mediump float;
varying vec4 v_Color; //每个片元的颜色值
void main () {
gl_FragColor = v_Color;
}`


  • js代码


var verticesColors = new Float32Array([     //顶点位置和颜色
0.0, 0.5, 1.0, 0.0, 0.0, // 第一个点,前两个是坐标(x,y; z默认是0),后三个是颜色
-0.5, -0.5, 0.0, 1.0, 0.0, // 第二个点
0.5, -0.5, 0.0, 0.0, 1.0 // 第三个点
])

//以下是通过缓冲区向顶点着色器传递顶点位置和颜色
var vertexColorBuffer = gl.createBuffer()

gl.bindBuffer(gl.ARRAY_BUFFER, vertexColorBuffer)
gl.bufferData(gl.ARRAY_BUFFER, verticesColors, gl.STATIC_DRAW)

var FSIZE = verticesColors.BYTES_PER_ELEMENT
var a_Position = gl.getAttribLocation(gl.program, 'a_Position')
gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, FSIZE * 5, 0)
gl.enableVertexAttribArray(a_Position)

var a_Color = gl.getAttribLocation(gl.program, 'a_Color')
gl.vertexAttribPointer(a_Color, 3, gl.FLOAT, false, FSIZE * 5, FSIZE * 2)
gl.enableVertexAttribArray(a_Color)

//绘制一个三角形,第一个参数为gl.TRIANGLES
gl.drawArrays(gl.TRIANGLES, 0, 3)

下面最终绘制出来的效果:


image.png


纹理映射的简单理解


在上面的例子中,我们是为每个顶点指定颜色值。


延伸一下,纹理映射是为每个顶点指定纹理坐标,然后webgl系统会根据顶点纹理坐标,插值计算出每个片元的纹理坐标。


然后在片元着色器中,会根据传入的纹理图像,以及每个片元的纹理坐标,取出纹理图像中对应纹理坐标上的颜色值(纹素),作为该片元的颜色值,并进行渲染。


纹理坐标的特点:



  • 纹理图像左下角为原点(0, 0)。

  • 向右为横轴正方向,横轴最大值为 1(图像右边缘)。

  • 向上为纵轴正方向,纵轴最大值为 1(图像上边缘)。


image.png
不管纹理图像的尺寸是多少,纹理坐标的范围都是: x轴:0-1,y轴:0-1


画一个3D地球


使用webgl进行绘制,步骤和API都比较繁琐,所幸我们可以借助three.js


three.js中的ShaderMaterial可以让我们自己定制着色器,直接操作像素。我们只需要理解着色器的基本原理。


开始画地球吧。


基础球体


基础球体的绘制比较简单,用three.js提供的材质就行。关于材质的基础,在 用three.js写一个反光球 有比较详细的介绍。


var loader = new THREE.TextureLoader() 
var group = new THREE.Group()

//创建本体
var geometry = new THREE.SphereGeometry(20,30,30) //创建球形几何体
var earthMaterial = new THREE.MeshPhongMaterial({ //创建材质
map: loader.load( './images/earth.png' ), //基础纹理
specularMap: loader.load('./images/specular.png'), //高光纹理,指定物体表面中哪部分比较闪亮,哪部分相对暗淡
normalMap: loader.load('./images/normal.png'), //法向纹理,创建更加细致的凹凸和褶皱
normalScale: new THREE.Vector2(3, 3)
})
var sphere = new THREE.Mesh(geometry, earthMaterial) //创建基础球体
group.add(sphere)

image.png


流动大气


使用ShaderMaterial自定义着色器。大气的流动,是通过每次在requestAnimationFrame渲染循环中改变纹理坐标实现。为了使流动更加自然,加入噪声。


//顶点着色器
var VSHADER_SOURCE = `
varying vec2 vUv;
void main () {
vUv = uv; //顶点纹理坐标
gl_Position = projectionMatrix * viewMatrix * modelMatrix * vec4( position, 1.0 );
}
`

//片元着色器
var FSHADER_SOURCE = `
uniform float time; //时间变量
uniform sampler2D fTexture; //大气纹理图像
uniform sampler2D nTexture; //噪声纹理图像
varying vec2 vUv; //片元纹理坐标
void main () {
vec2 newUv= vUv + vec2( 0, 0.02 ) * time; //向量加法,根据时间变量计算新的纹理坐标

//利用噪声随机使纹理坐标随机化
vec4 noiseRGBA = texture2D( nTexture, newUv );
newUv.x += noiseRGBA.r * 0.2;
newUv.y += noiseRGBA.g * 0.2;

gl_FragColor = texture2D( fTexture, newUv ); //提取大气纹理图像的颜色值(纹素)
}
`

var flowTexture = loader.load('./images/flow.png')
flowTexture.wrapS = THREE.RepeatWrapping
flowTexture.wrapT = THREE.RepeatWrapping

var noiseTexture = loader.load('./images/noise.png')
noiseTexture.wrapS = THREE.RepeatWrapping
noiseTexture.wrapT = THREE.RepeatWrapping

//着色器材质
var flowMaterial = new THREE.ShaderMaterial({
uniforms: {
fTexture: {
value: flowTexture,
},
nTexture: {
value: noiseTexture,
},
time: {
value: 0.0
},
},
// 顶点着色器
vertexShader: VSHADER_SOURCE,
// 片元着色器
fragmentShader: FSHADER_SOURCE,
transparent: true
})
var fgeometry = new THREE.SphereGeometry(20.001,30,30) //创建比基础球体略大的球状几何体
var fsphere = new THREE.Mesh(fgeometry, flowMaterial) //创建大气球体
group.add(fsphere)
scene.add( group )

创建了group,基础球体和大气球体,都加入到group,作为一个整体,设置转动和位置,都直接修改group的属性。


var clock = new THREE.Clock()
//渲染循环
var animate = function () {
requestAnimationFrame(animate)
var delta = clock.getDelta()
group.rotation.y -= 0.002 //整体转动
flowMaterial.uniforms.time.value += delta //改变uniforms.time的值,用于片元着色器中的纹理坐标计算
renderer.render(scene, camera)
}

animate()

image.png


光晕


创建光晕用的是精灵(Sprite),精灵是一个总是面朝着摄像机的平面,这里用它来模拟光晕,不管球体怎么转动,都看上去始终处于光晕中。


var ringMaterial = new THREE.SpriteMaterial( {  //创建点精灵材质
map: loader.load('./images/ring.png')
} )
var sprite = new THREE.Sprite( ringMaterial ) //创建精灵,和普通物体的创建不一样
sprite.scale.set(53,53, 1) //设置精灵的尺寸
scene.add( sprite )

最终效果图:


earth-gif-l.gif


链接:https://juejin.cn/post/6992445067344478239

收起阅读 »

这种微前端设计思维听说过吗?

前言:最近有种感觉,好像微前端成为当下前端工程师的标配,从single-spa到qiankun,各种微前端架构解决方案层出不穷。那一夜,我在翻阅github时,留意到一个新的微前端框架,来自京东零售开源的MicroApp,号称无需像上面提到那两个框架一样需要对...
继续阅读 »

前言:最近有种感觉,好像微前端成为当下前端工程师的标配,从single-spa到qiankun,各种微前端架构解决方案层出不穷。那一夜,我在翻阅github时,留意到一个新的微前端框架,来自京东零售开源的MicroApp,号称无需像上面提到那两个框架一样需要对子应用的渲染逻辑调整,甚至还不用修改webpack配置。还有一个成功引起我注意的是:它把web-components的概念给用上了!让我们一探究竟!



1.饭后小菜 - Web Components 🍵


众所周知,Web Components 是一种原生实现可服用web组件的方案,你可以理解为类似在vue、React这类框架下开发的组件。不同的是,基于这个标准下开发的组件可以直接在html下使用,不用依赖其他第三方的库。



换句话说:部分现代浏览器提供的API使我们创建一个可复用的组件而无需依赖任何框架成为一种可能,不会被框架所限制



主要包括以下几个特征:



  • 使用custom elements自定义标签

  • 使用shadow DOM做样式隔离

  • 使用 templates and slots 实现组件拓展 (本期不拓展)


那 Web Components是如何创建一个组件的?我们来看下下面这个demo实践


1.1 实践



针对web components的实践, 我在github上找到一个demo。如下图所示,假设一个页面是由三个不同团队负责独立开发,A团队负责红色区域的整体展示功能,B团队和C团队分别负责蓝色和绿色区域(在红色区域内展示),那他们是怎么实现的?



image.png


我们以绿色区域的功能为示例,来看看demo的代码实例,本质上可以理解为定义一个组件green-recos


carbon (27).png


通过上图,我们来分析这段代码,主要包括以下几点信息:



  • 如何自定义元素?: 通过Api:window.customElements中的defind方法来定义注册好的实例

  • 如何定义一个组件实例?: 通过继承HTMLElement定义一个是实例类

  • 如何与外部通信的?:通过创建一个CustomEvent来自定义一个新的事件,然后通过addEventListener来监听以及element.dispatchEvent() 来分发事件

  • 如何控制组件的生命周期?: 主要是包括这几个生命周期函数,顺序如下 👇



constructor(元素初始化) -> attributeChangedCallback(当元素增加、删除、修改自身属性时,被调用) -> connectedCallback(当元素首次被插入文档DOM时,被调用) -> disconnectedCallback(当 custom element从文档DOM中删除时,被调用)`



拓展:



1.2 关于兼容性



👨‍🎓 啊乐同学:树酱,听说web component兼容性不太好?咋整?



image.png
你可以看上图👆 ,大部分浏览器新版本支持,如果想兼容旧版本,莫慌,可以通过引入polyfill来解决兼容问题 webcomponents/polyfills


你也可以通过坚挺WebComponentsReady这个事件来得知web components是否成功加载


1.3 关于样式冲突


关于样式,上面例子的样式是全局引用的,并没有解决样式冲突的问题,那如果想基于Web Components 开发组件,又担心各组件间存在样式冲突,这个时候你可以使用Shadow DOM来解决,有点类似vue中定义组件中的scoped处理



Shadow DOM: 也称影子DOM,它可以将一个隐藏的、独立的 DOM 附加到一个元素上。如下图MDN官方介绍图所示



image.png


那基于web component如何开发一个挂在#shadow-root的组件?


carbon (28).png


我们可以看到通过上图对比上一节的例子,多了attachShadow的方法使用。它是啥玩意?



官方介绍:通过attachShadow来将一个 shadow root 附加到任何一个元素上。它接受一个配置对象作为参数,该对象有一个 mode 属性。当mode为true,则表示可以通过页面内的 JavaScript 方法来获取 Shadow DOM



🌲 扩展阅读:



1.4 注意细节



啊乐同学:树君,那我在vue中可以使用Web Component开发的自定义组件吗?



可以的,但是有一点要注意就是,Vue 组件开发很类似自定义元素,如果我们不做点“手段”处理,vue会把你基于Web Component开发的组件当作本身框架下的组件来看待,so 我们需要配置ignoredElements,下图是vue官网的示例


image.png


如果想了解更多关于Web Component的组件开发,可以看看下面这个开源的组件库



2 Mrcio-app



一不小心绕远了,言归正传,聊聊今日主角:micro-app



使用过qiankun的童鞋知道,我们要在基座集成一个微应用离不开下面👇 这三要素:



  • 在基座注册子应用

  • 需要在子应用定义好生命周期函数

  • 修改微应用的webpack打包方式


虽然改造成本不算特别高,但是能尽量降低对源代码的侵入性不香吗?


Mrcio-app 走的就是极简的路线,只要修改一丢丢代码就可以实现微应用的集成,号称是目前市面上接入微前端成本最低的方案。那它是如何做到的?


2.1 原理


本质上 micro-app 是基于类WebComponent + HTML Entry实现的微前端架构


image.png



官方介绍:通过自定义元素micro-app的生命周期函数connectedCallback监听元素被渲染,加载子应用的html并转换为DOM结构,递归查询所有js和css等静态资源并加载,设置元素隔离,拦截所有动态创建的script、link等标签,提取标签内容。将加载的js经过插件系统处理后放入沙箱中运行,对css资源进行样式隔离,最后将格式化后的元素放入micro-app中,最终将micro-app元素渲染为一个微前端的子应用。在渲染的过程中,会执行开发者绑定的生命周期函数,用于进一步操作。





  • 关于HTML Entry:相信用过qiankun 的童鞋应该都很熟悉,就是加载微应用的入口文件,一方面对微应用的静态资源js、CSS等文件进行fetch,一方面渲染微应用的dom




  • 类WebComponent: 我们在上一节学习web Component中了解到两个特征:CustomElementShadowDom,前者使得我们可以创建自定义标签,后者则促使我们可以创建支持隔离样式和元素隔离的阴影DOM。而首次提及的类WebComponent是个啥玩意?本质上就是通过使用CustomElement结合自定义的ShadowDom实现WebComponent基本一致的功能




换句话说:让微前端下微应用实现真正意义上的组件化


2.2 很赞的机制


micro-app 有这几个机制我觉得很赞:



  • 不用像qiankun一样在每个微应用都预先定义好生命周期函数,如:createdmounted等,而是另辟蹊径,当你在基座集成后,在基座可以直接定义,也可以进行全局监听。如下所示


carbon (29).png


上图的属性配置中name是微应用的名称配置,url是子应用页面地址配置,其他则是各个生命周期函数的定义



  • 资源地址自动补全:我们在基座加载微应用的时候,当微应用涉及图片或其他资源加载时,如果访问路径是相对地址,我们会发现会以基座应用所在域名地址补全静态资源,导致资源加载错误。而micro-app支持将子应用静态资源的相对地址补全为绝对地址,解决了上述的问题


image.png


2.3 实践


2.3.1 demo上手



上手也很简单,以vue2应用为例,具体参考 github文档。这里不做重复陈述



通过官方在线演示vue微应用Demo,我们来看看集成后的效果


image.png


在控制台我们可以看到,基座加载完微应用"vue2",在自定义标签micro-app渲染后就是一个完整子应用Dom,有点类似iframe的感觉,然后该子应用的css样式,都多了一个前缀 micro-app[name=vue2]。这是利用标签的name属性为每个样式添加前缀,将子应用的样式影响禁锢在当前标签区域,避免各个微应用之间的样式冲突。这是micro-app的默认隔离机制



啊乐同学:树酱,他这个元素隔离是怎么实现的?



你听我解释,看下一节源码分析


2.3.2 渲染微应用的过程


渲染微应用的过程主要流程图可以参照官方提供,主要包括以下流程


image.png



  • fetch 子应用HTMl: 获取html,然后转换为dom结构并递归处理每一个子元素,对不同元素做相应的处理 源码链接


目的是为了提取微应用的link和script,绑定style作用域。最后实现将微应用的style挂在micro-app-head中 核心源码如下
carbon (6).png


通过源码的阅读,当我们在微应用的初始化定义的app.scopecss配置时(默认开启),就会调用scopedCSS处理dom
,以此实现绑定微应用的css作用域,让我们看下这个方法的实现 源码链接


我在源码中看到scoped_css主要针对几种cssRule来做区分处理



啊恒同学:树酱,什么是Css Rule?



这是一个有历史的概念了,CSSRule 表示一条 CSS 规则。而一个 CSS 样式表包含了一组表示规则CSSRule对象。 CSSRule 有几种不同的规则类型,你可以在micro-app主要针对以下几种常规的cssRule区分处理



  • CSSRule.STYLE_RULE: 一般的style规则

  • CSSRule.MEDIA_RULE: CSS @media 媒体属性查询的规则

  • CSSRule.SUPPORTS_RULE: CSS @support 可以根据浏览器对CSS特性的支持情况来定义不同的样式的规则


carbon (7).png


最后将转化成功的style内容,append到micro-app-head中



啊恒同学:树酱,你说micro-app隔离元素支持shadowDom ?



是的,如果开启shadowDOM后,上面提到的默认的样式隔离将失效。 且兼容性会比较差


下面是个删减版:关于mircro-app通过Web Component + shadowDOM的实现子应用初始化的定义,具体的源码你可以阅读框架源码中关于micro_app_element的定义 源码链接
carbon (8).png


本质上开启shadowDom后,<micro-app>标签才算真正实现意义上的WebComponent



链接:https://juejin.cn/post/6992500880364797960

收起阅读 »

你可能不知道的动态组件玩法?

○ 背景 知道的大佬请轻锤😂。 这篇是作者在公司做了活动架构升级后,产出的主文的前导篇,考虑到本文相对独立,因此抽离出单独成文。 题目为动态组件,但为了好理解可以叫做远程加载动态组件,后面统一简化称为“远程组件”。 具体是怎么玩呢?别着急,听我慢慢道来,看...
继续阅读 »

○ 背景



知道的大佬请轻锤😂。


这篇是作者在公司做了活动架构升级后,产出的主文的前导篇,考虑到本文相对独立,因此抽离出单独成文。



题目为动态组件,但为了好理解可以叫做远程加载动态组件,后面统一简化称为“远程组件”。


具体是怎么玩呢?别着急,听我慢慢道来,看完后会感慨Vue组件还能这么玩🐶,还会学会一个Stylelint插件,配有DEMO,以及隐藏在最后的彩蛋。


作者曾所在我司广告事业部,广告承载方式是以刮刮卡、大转盘等活动页进行展示,然后用户参与出广告券弹层。


旁白说:远程组件其实在可视化低代码平台也有类似应用,而我们这里也是利用了类似思路实现解耦了活动页和券弹层。继续主题...


image.png


遗留系统早先版本是一个活动就绑定一个弹层,1对1的绑定关系。


image.png


现在的场景是一个活动可能出不同样式的弹层,这得把绑定关系解除。我们需要多对多,就是一个活动页面可以对应多个广告券弹层,也可以一个广告券弹层对应多个活动页面。


我们可以在本地预先写好几个弹层,根据条件选择不同的弹层,可以满足一个活动对多个弹层。


而我们的需求是让活动页面对应无数种弹层,而不是多种,所以不可能把所有弹层都写在本地。因此怎么办呢?


image.png


因此我们要根据所需,然后通过判断所需的弹层,远端返回对应的代码。其实就是我们主题要讲到的远程组件


讲得容易,该怎么做呢?


○ 远程组件核心


Pure版本


如果是Pure JS、CSS组成的弹层,很自然的我们想到,通过动态的插入JS脚本和CSS,就能组成一个弹层。因此把编译好的JS、CSS文件可以存放在远端CDN。


image.png


看上图,我们可以看到弹窗出来之前,浏览器把CSS、JS下载下来了,然后根据既定代码拼装成一个弹层。


// CSS插入
<link rel="stylesheet" href="//yun.xxx.com/xxx.css">

// JS的动态插入

<script type="text/javascript">
var oHead = document.querySelector('.modal-group');
var oScript = document.createElement('script');
oScript.type = "text/javascript";
oScript.src = "//yun.xxx.com/xxx.js";
oHead.appendChild(oScript);
</script>

通过上面可知,JS、CSS方式能实现Pure版本的远程组件,而在Vue环境下能实现吗。如果按照Pure JS、CSS动态插入到Vue活动下,也是可以很粗糙的实现的。


但有没有更优雅的方式呢?


image.png


Vue版本



选型这篇不细讨论了,后续的主篇会讲为什么选择Vue。



上述是遗留系统的方式,如果我们要技术栈迁移到Vue,也需要对远程组件迁移,我们需要改造它。


让我们来回顾下Vue的一些概念。


组件形式


「对象组件」


一个弹窗,其实我们可以通过一个Vue组件表示,我们想把这个组件放到CDN,直接下载这个文件,然后在浏览器环境运行它可行吗?我们来尝试下。


基于Vue官方文档,我们可以把如下的选项对象传入Vue,通过new Vue来创建一个组件。


{
mounted: () => {
console.log('加载')
},
template: "<div v-bind:style=\"{ color: 'red', fontSize: '12' + 'px' }\">Home component</div>"
}

借助于包含编译器的运行时版本,我们可以处理字符串形式的Template。



-- 运行时-编译器-vs-只包含运行时




如果你需要在客户端编译模板 (比如传入一个字符串给Template选项,或挂载到一个元素上并以其 DOM 内部的 HTML 作为模板),就将需要加上编译器,即完整版



似乎找到了新世界的大门。


image.png


我们确实是可以通过这种形式实现Template、Script、CSS了,但对于开发同学,字符串形式的Template、内嵌的CSS,开发体验不友好。


image.png


「单文件组件」


这个时候很自然地想到SFC - 单文件组件。



文件扩展名为.vue的**single-file components (单文件组件)**为以上所有问题提供了解决方法 -- Vue文档。




image.png




但怎么样才能让一个.vue组件从远端下载下来,然后在当前活动Vue环境下运行呢?这是个问题,由于.vue文件浏览器是识别不了的,但.js文件是可以的。


我们先想一下,.vue文件是最终被转换成了什么?


image.png
(图片来源:1.03-vue文件的转换 - 简书


通过转换,实际变成了一个JS对象。所以怎么才能把.vue转换成.js呢?


有两种方式,一种通过运行时转换,我们找到了http-vue-loader。通过Ajax获取内容,解析Template、CSS、Script,输出一个JS对象。


image.png


而考虑到性能和兼容性,我们选择预编译,通过CSS预处理器、HTML模版预编译器。


Vue的官方提供了vue-loader,它会解析文件,提取每个语言块,如有必要会通过其它 loader 处理,最后将他们组装成一个 ES Module,它的默认导出是一个 Vue.js 组件选项的对象。这指的是什么意思呢?官方提供选项对象形式的组件DEMO。


有了理论支持,现在需要考虑下实践啦,用什么编译?


image.png


怎么构建


由于webpack编译后会带了很多关于模块化相关的无用代码,所以一般小型的库会选择rollup,这里我们也选择rollup。


// rollup.config.js
import vue from 'rollup-plugin-vue'
import commonjs from 'rollup-plugin-commonjs'

export default {
input: './skin/SkinDemo.vue',
output: {
format: 'iife',
file: './dist/rollup.js',
name: 'MyComponent'
},
plugins: [
commonjs(),
vue()
]
}

通过rollup-plugin-vue,我们可以把.vue文件转成.js,
rollup编译输出的iife形式js。


image.png


可以看到script、style、template分别被处理成对应的片段,通过整合计算,这些片段会生成一个JS对象,保存为.js文件。下图就是一个组件选项的对象。


image.png


可以通过项目:github.com/fly0o0/remo…,尝试下rollup文件夹下的构建,具体看README说明。


我们已经有了一个 Vue.js 组件选项的对象,怎么去让它挂载到对应的Vue App上呢?


image.png


挂载方式


回想之前通读Vue入门文档,遇到一个动态组件的概念,但当时并不太理解它的使用场景。
image.png


动态组件是可以不固定具体的组件,根据规则替换不同的组件。从文档上看出,支持一个组件的选项对象。


最终实现


首先需要构建.vue文件,然后通过Ajax或动态Script去加载远端JS。由于Ajax会有跨域限制,所以这里我们选择动态Script形式去加载。


而我们刚才使用Rollup导出的方式是把内容挂载在一个全局变量上。那就知道了,通过动态Script插入后,就有一个全局变量MyComponent,把它挂载在动态组件,最终就能把组件显示在页面上了。


具体怎么操作?欠缺哪些步骤,首先我们需要一个加载远程.js组件的函数。


// 加载远程组件js

function cleanup(script){
if (script.parentNode) script.parentNode.removeChild(script)
script.onload = null
script.onerror = null
script = null
}

function scriptLoad(url) {
const target = document.getElementsByTagName('script')[0] || document.head

let script = document.createElement('script')
script.src = url
target.parentNode.insertBefore(script, target)

return new Promise((resolve, reject) => {
script.onload = function () {
resolve()
cleanup(script)
}
script.onerror = function () {
reject(new Error('script load failed'))
cleanup(script)
}
})
}

export default scriptLoad

然后把加载下来的组件,挂载在对应的动态组件上。


<!-- 挂载远程组件 -->

<template>
<component

:is="mode">
</component>
</template>

<script>
import scriptLoad from "./scriptLoad"

export default {
name: "Remote",
data() {
return {
mode: "",
};
},
mounted() {
this.mountCom(this.url)
},
methods: {
async mountCom(url) {
// 下载远程js
await scriptLoad(url)

// 挂载在mode
this.mode = window.MyComponent

// 清除MyComponent
window.MyComponent = null
},
}
}
</script>

基本一个Vue的远程组件就实现了,但发现还存在一个问题。


image.png


全局变量MyComponent需要约定好,但要实现比较好的开发体验来说,应该尽量减少约定。


导出方式


怎么解决呢?由于我们导出是使用的IIFE方式,其实Rollup还支持UMD方式,包含了Common JS和AMD两种方式。


我们通过配置Rollup支持UMD。


// rollup.config.js
import vue from 'rollup-plugin-vue'
import commonjs from 'rollup-plugin-commonjs'

export default {
input: './skin/SkinDemo.vue',
output: {
format: 'umd',
file: './dist/rollup.js',
name: 'MyComponent'
},
plugins: [
commonjs(),
vue()
]
}

可以看到构建完毕后,支持三种方式导出。
image.png


我们可以模拟node环境,命名全局变量exports、module,就可以在module.exports变量上拿到导出的组件。
image.png


具体实现核心代码如下。


<!-- 挂载远程组件 -->

<template>
<component

:is="mode">
</component>
</template>

<script>
import scriptLoad from "./scriptLoad"

export default {
name: "Remote",
data() {
return {
mode: "",
};
},
mounted() {
this.mountCom(this.url)
},
methods: {
async mountCom(url) {
// 模拟node环境
window.module = {}
window.exports = {}

// 下载远程js
await scriptLoad(url)

// 挂载在mode
this.mode = window.module.exports

// 清除
delete window.module
delete window.exports
},
}
}
</script>

终于搞定了Vue版本的远程组件加载的方式。


image.png


接下来得想一想,怎么处理远程组件(弹层)的设计了。


小结


通过使用Vue动态组件实现了远程组件功能,取代了老架构。image.png


可以通过以下地址去尝试一下远程组件弹层,按照项目的README操作一下。会得到以下远程组件弹层。


项目地址:github.com/fly0o0/remo…


image.png


○ 远程组件(弹层)设计



远程组件已达成,这部分主要是对远程弹层组件的一些设计。



对于远程单组件本身来说,只需要根据数据渲染视图,根据用户行为触发业务逻辑,整个代码逻辑是这样的。


需要考虑组件复用、组件通讯、组件封装、样式层级等方向。


首先我们先看看组件复用。


为了方便统一管理和减少冗余代码,我们一般写一些类似的组件会抽取一部分可以公共的组件,例如按钮等。


但远程单组件代码和页面端代码是分离的啊(可以理解为两个webpack入口打包出的产物),我们得想想公共组件需要放在哪里了。


image.png


组件复用


现在可以发现有三种情况,我们利用枚举法尝试想一遍。


打包 📦


公共组件和远程组件打包一起


放在一起肯定不合适,不仅会引起远程组件变大,还不能让其他远程组件复用。往下考虑再看看。


image.png


公共组件单独打包


远程组件、公共组件分别单独打包,这样也是不利的,由于远程组件抽离的公共组件少于5个,而且代码量较少,单独作为一层打包,会多一个后置请求,影响远程组件的第一时间展示。


继续考虑再看看。
image.png
公共组件和页面核心库打包一起


把公共组件和页面核心库打包到一起,避免后面远程组件用到时候再加载,可以提升远程组件的展示速度。
image.png


因此最终敲定选择最后种,把公共组件和页面核心库打包在一起。


如果把远程组件.js和公共组件分开了,那我们该怎么才能使用公共组件啊?😂


image.png


注册 🔑


回顾下Vue官方文档,Vue.component它可以提供注册组件的能力,然后在全局能引用到。我们来试试吧。


公共组件例如按钮、关闭等,需要通过以下途径去注册。


一个按钮组件


// 本地页面端(本地是相较于在远端CDN)

<!-- 按钮组件 -->
<template>
<button type="button" @click="use">
</button>
</template>

<script>
export default {
name: 'Button',
inject: ['couponUseCallback'],
methods: {
use() {
this.couponUseCallback && this.couponUseCallback()
}
}
}
</script>

一个关闭组件


// 本地页面端(本地是相较于在远端CDN)

<!-- 关闭组件 -->
<template>
<span @click="close"></span>
</template>

<script>
export default {
name: "CouponClose",
inject: ["couponCloseCallback"],
methods: {
close() {
this.couponCloseCallback && this.couponCloseCallback();
},
},
};
</script>

<style lang="less" scoped>
.close {
&.gg {
background-image: url("//yun.tuisnake.com/h5-mami/dist/close-gg.png") !important;
background-size: 100% !important;
width: 92px !important;
height: 60px !important;
}
}
</style>

通过Vue.component全局注册公共组件,这样在远程组件中我们就可以直接调用了。


// 本地页面端(本地是相较于在远端CDN)

<script>
Vue.component("CpButton", Button);
Vue.component("CpClose", Close);
</script>

解决了公共组件复用的问题,后面需要考虑下远程组件和页面容器,还有不同类型的远程组件之间的通讯问题。


image.png


组件通讯


可以把页面容器理解为父亲、远程组件理解为儿子,两者存在父子组件跨级双向通讯,这里的父子也包含了爷孙和爷爷孙的情况,因此非props可以支持。那怎么处理?


可以通过在页面核心库中向远程组件 provide 自身,远程组件中 inject 活动实例,实现事件的触发及回调。


那不同类型的远程组件之间怎么办呢,使用Event Bus,可以利用顶层页面实例作为事件中心,利用 on 和 emit 进行沟通,降低不同类别远程组件之间的耦合度。


image.png


组件封装


现在有个组件封装的问题,先看个例子,基本就大概有了解了。


现有3个嵌套组件,如下图。** **现在需要从顶层组件Main.vue给底层组件RealComponent的一个count赋值,然后监听RealComponent的input组件的事件,如果有改变通知Main.vue里的方法。怎么做呢?


image.png


跨层级通信,有多少种方案可以选择?



  1. 我们使用vuex来进行数据管理,对于这个需求过重。

  2. 自定义vue bus事件总线(如上面提到的),无明显依赖关系的消息传递,如果传递组件所需的props不太合适。

  3. 通过props一层一层传递,但需要传递的事件和属性较多,增加维护成本。


而还有一种方式可以通过attrsattrs和listeners,实现跨层级属性和事件“透传”。


主组件


// Main.vue

<template>
<div>
<h2>组件Main 数据项:{{count}}</h2>
<ComponentWrapper @changeCount="changeCount" :count="count">
</ComponentWrapper>
</div>
</template>
<script>
import ComponentWrapper from "./ComponentWrapper";
export default {
data() {
return {
count: 100
};
},
components: {
ComponentWrapper
},
methods: {
changeCount(val) {
console.log('Top count', val)
this.count = val;
}
}
};
</script>

包装用的组件


有的时候我们为了对真实组件进行一些功能增加,这时候就需要用到包装组件(特别是对第三方组件库进行封装的时候)。


// ComponentWrapper.vue

<template>
<div>
<h3>组件包裹层</h3>
<RealComponent v-bind="$attrs" v-on="$listeners"></RealComponent>
</div>
</template>
<script>
import RealComponent from "./RealComponent";
export default {
inheritAttrs: false, // 默认就是true
components: {
RealComponent
}
};
</script>

真正的组件


// RealComponent.vue

<template>
<div>
<h3>真实组件</h3>
<input v-model="myCount" @input="inputHanlder" />
</div>
</template>
<script>
export default {
data() {
return {
myCount: 0
}
},
created() {
this.myCount = this.$attrs.count; // 在组件Main中传递过来的属性
console.info(this.$attrs, this.$listeners);
},
methods: {
inputHanlder() {
console.log('Bottom count', this.myCount)
this.$emit("changeCount", this.myCount); // 在组件Main中传递过来的事件,通过emit调用顶层的事件
// this.$listeners.changeCount(this.myCount) // 或者通过回调的方式
}
}
};
</script>

从例子中回归本文里来,我们要面对的场景是如下这样。


远程组件其实有两层,一层是本地(页面内),一层是远端(CDN)。本地这层只是做封装用的,可以理解为只是包装了一层,没有实际功能。这时候可以理解为本地这一层组件就是包装层,包装层主要做了导入远程组件的功能没办法去除,需要利用上面的特性去传递信息给远程组件。


样式层级


远程组件在本文可以简单理解为远端的弹层组件,公司业务又涉及到不同的弹层类别,每种弹层类别可能会重叠。


约定z-index


因此划分 0~90 为划分十层,后续可根据实际情况增加数值,设定各远程组件容器只能在规定层级内指定 z-index。


// const.js
const FLOOR = {
MAIN: 0, // 主页面容器
COUPON_MODAL: 20, // 广告弹层
OTHER_MODAL: 30, // 其他弹层
ERROR_MODAL: 90,
...
}

设置每种远程组件即弹层的包裹层。



// CouponModalWrapper.vue
<script>
<template>
<div :style="{'z-index': FLOOR.COUPON_MODAL}" @touchmove.prevent>
<slot></slot>
</div>
</template>

// OtherModalWrapper.vue
<template>
<div :style="{'z-index': FLOOR.OTHER_MODAL}" @touchmove.prevent>
<slot></slot>
</div>
</template>

// 这里只是为了表意简单,实际上两个Wrapper.vue可以合并

然后每类别各自引入对应的弹层包裹层。


// 每类别公共组件有一个

// CouponModal2.vue
<template>
<CouponModalWrapper>
...
</CouponModalWrapper>
</template>

// OtherModal2.vue
<template>
<OtherModalWrapper>
...
</OtherModalWrapper>
</template>

通过这种约定的方式,可以避免一些问题,但假如真的有人想捣乱怎么办?


image.png


别着急,有办法的。


借助stylelint


思路是这样的,每类别的远程组件是单独有对应的主文件夹,可以为这个文件夹定义最高和最小可允许的z-index,那该怎么做呢?


不知道大家有使用过自动加-webkit等前缀的插件 - autoprefixer没有,它其实是基于一款postcss工具做的。而我们经常用作css校验格式的工具stylelint也是基于它开发的。


这时候我们想到,能不能通过stylelint的能力,进行约束呢,我们发现找了官方文档并没有我们想要的API。


我们需要自己开发一个stylelint插件,来看看一个基本的stylelint插件的插件。


image.png


stylelint通过stylelint.createPlugin方法,接受一个函数,返回一个函数。


const stylelint = require('stylelint');
const ruleName = 'plugin/z-index-range-plugin';

function rule(options) {
// options传入的配置
return (cssRoot, result) => {
// cssRoot即为postcss对象
};
}

module.exports = stylelint.createPlugin(
ruleName,
rule
);

函数中可以拿到PostCSS对象,可以利用PostCSS对代码进行解析成AST、遍历、修改、AST变代码等操作。


有一些我们可用的概念。



  • rule,选择器,比如.class { z-index: 99 }。

  • decl,属性,比如z-index: 99。


我们需要检查z-index的值,因此需要遍历CSS检查z-index。我们可以调用cssRoot.walkDecls对做遍历:


// 遍历
cssRoot.walkDecls((decl) => {
// 获取属性定义
if (decl) {
// ...
}
});

前置基础知识差不多够用了。


image.png


假如我们要检测一个两个文件夹下的.css文件的z-index是否合乎规矩。


我们设置好两个模块stylelint配置文件下的z-index范围。


这里我们可以看到stylelint配置文件,两个css文件。


├── .stylelintrc.js
├── module1
│ └── index.css
├── module2
│ └── index2.css

stylelint配置文件


// .stylelintrc.js
module.exports = {
"extends": "stylelint-config-standard",
// 自定义插件
"plugins": ["./plugin.js"],
"rules": {
// 自定义插件的规则
"plugin/z-index-range-plugin": {
// 设置的范围,保证各模块不重复
"module1": [100, 199],
"module2": [200, 299]
}
}
}

CSS测试文件


/* module1/index.css */
.classA {
color: red;
width: 99px;
height: 100px;
z-index: 99;
}

/* module2/index.css */
.classB {
color: red;
width: 99px;
height: 100px;
z-index: 200;
}


我们要达到的目的是,运行如下命令,会让module1/index.css报错,说z-index小于预期。


npx stylelint "*/index.css"

于是乎我们完成了如下代码,达成了预期目的。


const stylelint = require('stylelint');
const ruleName = 'plugin/z-index-range-plugin';

function ruleFn(options) {
return function (cssRoot, result) {

cssRoot.walkDecls('z-index', function (decl) {
// 遍历路径
const path = decl.source.input.file
// 提取文件路径里的模块信息
const match = path.match(/module\d/)
// 获取文件夹
const folder = match?.[0]
// 获取z-index的值
const value = Number(decl.value);
// 获取设定的最大值、最小值
const params = {
min: options?.[folder]?.[0],
max: options?.[folder]?.[1],
}

if (params.max && Math.abs(value) > params.max) {
// 调用 stylelint 提供的report方法给出报错提示
stylelint.utils.report({
ruleName,
result,
node: decl,
message: `Expected z-index to have maximum value of ${params.max}.`
});
}

if (params.min && Math.abs(value) < params.min) {
// 调用 stylelint 提供的report方法给出报错提示
stylelint.utils.report({
ruleName,
result,
node: decl,
message: `Expected z-index to have minimum value of ${params.min}.`
});
}
});
};
}

module.exports = stylelint.createPlugin(
ruleName,
ruleFn
);

module.exports.ruleName = ruleName;

可以尝试项目:github.com/fly0o0/styl…,试一试感受一下🐶。


这样基本一个远程弹层的设计就完成了。


但还是遇到了些问题,艰难😂。


image.png


○ 遇到的问题


我们兴冲冲的打算发上线了,结果报错了🐶。报的错是webpackJsonp不是一个function。


不要慌,先吃个瓜镇静镇静。webpackJsonp是做什么的呢?


异步加载的例子


先看下以下例子,通过import的按需异步加载特性加载了test.js,以下例子基于Webpack3构建。


// 异步加载 test.js
import('./test').then((say) => {
say();
});

然后生成了异步加载文件 0.bundle.js。


// 异步加载的文件,0.bundle.js
webpackJsonp(
// 在其它文件中存放着的模块的 ID
[0],
// 本文件所包含的模块
[
// test.js 所对应的模块
(function (module, exports) {
function ;(content) {
console.log('i am test')
}

module.exports = say;
})
]
);

和执行入口文件 bundle.js。


// 执行入口文件,bundle.js
(function (modules) {
/***
* webpackJsonp 用于从异步加载的文件中安装模块。
*
*/
window["webpackJsonp"] = function webpackJsonpCallback(chunkIds, moreModules, executeModules) {
var moduleId, chunkId, i = 0, resolves = [], result;
for (; i < chunkIds.length; i++) {
chunkId = chunkIds[i];
if (installedChunks[chunkId]) {
resolves.push(installedChunks[chunkId][0]);
}
installedChunks[chunkId] = 0;
}
for (moduleId in moreModules) {
if (Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
modules[moduleId] = moreModules[moduleId];
}
}
while (resolves.length) {
resolves.shift()();
}
};

// 模拟 require 语句
function __webpack_require__(moduleId) {
}

/**
* 用于加载被分割出去的,需要异步加载的 Chunk 对应的文件
*/
__webpack_require__.e = function requireEnsure(chunkId) {
// ... 省略代码
return promise;
};

return __webpack_require__(__webpack_require__.s = 0);
})
(
[
// main.js 对应的模块
(function (module, exports, __webpack_require__) {
// 通过 __webpack_require__.e 去异步加载 show.js 对应的 Chunk
__webpack_require__.e(0).then(__webpack_require__.bind(null, 1)).then((show) => {
// 执行 show 函数
show('Webpack');
});
})
]
);

可以看出webpackJsonp的作用是加载异步模块文件。但为什么会报webpackJsonp不是一个函数呢?


开始排查问题


我们开始检查构建出的源码,发现我们的webpackJsonp并不是一个函数,而是一个数组(现已知Webpack4,当时排查时候不知道)。


我们发现异步文件加载的时候确实是变成了数组,通过push去增加一个异步模块到系统里。


// 异步加载的文件

(window["webpackJsonp"] = window["webpackJsonp"] || []).push([[/* chunk id */ 0], {
"./src/async.js": (function(module, __webpack_exports__, __webpack_require__) {

//...

}))

且在执行入口文件也发现了webpackJsonp被定义为了数组。


// 执行入口文件,bundle.js中的核心代码  

var jsonpArray = window["webpackJsonp"] = window["webpackJsonp"] || [];
var oldJsonpFunction = jsonpArray.push.bind(jsonpArray);
jsonpArray.push = webpackJsonpCallback;
jsonpArray = jsonpArray.slice();
for (var i = 0; i < jsonpArray.length; i++) webpackJsonpCallback(jsonpArray[i]);
var parentJsonpFunction = oldJsonpFunction;

确实我们构建出的源码的webpackJsonp是一个数组,确实不是一个函数了,感觉找到了一点线索。但为什么会webpackJsonp会函数形式去使用呢?


我们怀疑报错处有问题,开始排查报错处,发现对应的文件确实是用webpackJsonp当作函数去调用的,这是什么情况?🤔️


这时我们注意到报错的都是老架构下的远程组件,是不是在老架构的项目里会有什么蛛丝马迹?


我们开始探索老架构,这时候发现老架构是使用的webpack3,而我们新架构是使用webpack4构建的。难道是这里出了问题?💡


于是我们用webpack3重新构建了下老架构的远程组件,发现webpackJsonp对应的确实是函数,如上一节“异步加载的例子”里所示。


所以定位到了原因,webpack4和webpack3分别构建了新老两种的异步远程组件,webpackJsonp在版本4下是数组,而在版本3下面是函数。


image.png


细心的同学可能已经发现上面的图在之前出现过,webpack4构建的入口文件去加载webpack3构建的异步组件,就出现了章节头出现的webpackJsonp不是函数的错误。


image.png


好好想一想,大概有几个方案。



  1. 批量去修改webpack3构建出来的异步组件中webpackJsonp的命名,然后在容器页面入口里自定义异步加载能力(webpackJsonp功能)的函数。

  2. 重新去用webpack4构建所有遗留的老架构webpack3构建出来的异步组件。

  3. 搜寻是否有官方支持,毕竟这是一个webpack4从webpack3的过来的breack changes。


第一个方案工作量有点大,且怎么保证异步组件和入口文件同步修改完毕呢?
第二个方案工作量也很大,对于所有老架构的异步组件都得更新,且更新后的可靠性堪忧,万一有遗漏。
第三个方案看起来是最靠谱的。


image.png


于是在第三个方案的方向下,开始做了搜寻。


我们通过webpack4源码全局搜寻webpackJsonp,发现了jsonpFunction。通过官方文档找到了jsonpFunction是可以自定义webpack4的webpackJsonp的名称。比如可以改成如下。


output: {
// 自定义名称
jsonpFunction: 'webpack4JsonpIsArray'
},

这样后,webpackJsonp就不是一个数组了,而是未定义了。因此我们需要在我们的公共代码库里提供webpackJsonp函数版本的定义。如异步加载的例子小节所提到的。


// webpackJsonp函数版
!(function (n) {
window.webpackJsonp = function (t, u, i) {
//...
}
}([]))

以此来提供入口页面能加载webpack3构建的异步文件的能力。


○ 演进



我们还对远程组件弹层做了一些演进,由于跟本文关联度不大,只做一些简单介绍。



图片压缩问题


券弹层的券有PNG、JPG、GIF格式,需要更快的展现速度,因此我们做了图片压缩的统一服务。


image.png


gif处理策略:github.com/kornelski/g…
png处理策略:pngquant.org


效率问题


有规律的远程组件,可通过搭建工具处理,因此我们构建了可视化低代码建站工具,有感兴趣的同学留言,我考虑写一篇😂 。


image.png



链接:https://juejin.cn/post/6992483283187531789

收起阅读 »

AFNetWorking为何在发起请求时要通过runloop!OC 中常用关键字的区别!

最近几天经历了多场面试,由于简历上写了runloop,跟AFNetworing的字眼。面试官好像特别喜欢问这个问题。一连几场都遇到。可惜平时开发过程中,知识的累计跟沉淀不足。都不能回答的很好..趁着现在有时间。查阅一下资料 在这里进行一个总结。。Questio...
继续阅读 »

最近几天经历了多场面试,由于简历上写了runloop,跟AFNetworing的字眼。面试官好像特别喜欢问这个问题。一连几场都遇到。

可惜平时开发过程中,知识的累计跟沉淀不足。都不能回答的很好..


趁着现在有时间。查阅一下资料 在这里进行一个总结。。

Question:AFNetworking 2.x怎么开启常驻子线程?为何需要常驻子线程?对应以下代码:


+ (void)networkRequestThreadEntryPoint:(id)__unused object {
@autoreleasepool {
[[NSThread currentThread] setName:@"AFNetworking"];

NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
[runLoop run];
}
}

+ (NSThread *)networkRequestThread {
static NSThread *_networkRequestThread = nil;
static dispatch_once_t oncePredicate;
dispatch_once(&oncePredicate, ^{
_networkRequestThread = [[NSThread alloc] initWithTarget:self selector:@selector(networkRequestThreadEntryPoint:) object:nil];
[_networkRequestThread start];
});

return _networkRequestThread;
}

首先,我们要了解为何要开启常驻子线程?

NSURLConnection的接口是异步的,然后会在发起的线程回调。而一个子线程,在同步代码执行完成之后,一般情况下,线程就退出了。那么想要接收到NSURLConnection的回调,就必须让子线程至少存活到回调的时机。而AF让线程常驻的原因是,当发起多个http请求的时候,会统一在这个子线程进行回调的处理,所以干脆就让其一直存活下来。

上面说的一般情况,子线程执行完任务就会退出,那么什么情况下,子线程能够继续存活呢?这就涉及到第二个问题了,AF是如何开启常驻线程的,这里实际上考察的是runloop的基础知识。

这里简单来说,当runloop发现还有source/timer/observer的时候,runloop就不会退出。所以AF这里就通过给当前runloop添加一个NSMachPort,这个port实际上相对于添加了一个source事件源,这样子线程的runloop就会一直处于循环状态,等待别的线程向这个port发送消息,而实际上AF这里是没有消息发送到这个port的。


OC 中 strong, weak, assign, copy 的区别

strong

强引用,只可以修饰对象,属性的默认修饰符,其修饰的对象引用计数增加1

weak

弱引用,只可以修饰对象,指向但不拥有对象,其修饰的对象引用计数不增加,可以避免循环引用,weak修饰的对象释放后,指针会被系统置为nil,此时向对象发送消息不会奔溃

assign

可以修饰对象和基本数据类型,如果修饰对象,其修饰的对象引用计数不增加,可以避免循环引用,但assign修饰的对象释放后,指针不会被系统置为nil,这会产生野指针的问题,此时向对象发送消息会奔溃。所以assign通常用于基本数据类型,如int ,float, NSInteger, CGFloat ,这是因为基本数据类型放在栈区,先进先出,基本数据类型出栈后,assign修饰的变量就不存在了,不用担心指针的问题。

copy

引用,修饰不可变的对象,比如NSString, NSArray, NSDictionary。copy和strong类似,不同之处在于,copy修饰的对象会先在内存中拷贝一个新的对象,copy会指向那个新的对象的内存地址,这样避免了多个指针指向同一对象,而导致的其中一个指针改变了对象,其他指针指向的对象跟着改变,举个例子:

@property(strong) NSString *name1;
@property(copy) NSString *name2;

NSMutableString *name3 = [NSMutableString stringWithString:@"Danny"];
self.name1 = name3;
self.name2 = name3;
[name3 setString:@"Liming"];
NSLog(@"%@", self.name1); // Liming
NSLog(@"%@", self.name2); // Danny

我们可以看到使用strong的属性name1会跟着name3变,因为他们都指向同一个NSMutableString的对象,而name2预先拷贝了name1,从而避免了和name1一起变化。

copy的原则就是,把一个对象赋值给一个属性变量,当这个对象变化了,如果希望属性变量变化就使用strong属性,如果希望属性变量不跟着变化,就是用copy属性。






收起阅读 »

iOS一些容易被忽略的基础面试题

什么是对象 ,OC中的对象有哪些?对象是类的实例;是通过一个类创建出来的实例,一般称之为实例对象;OC中的常见对象有实例对象、类对象、元类对象;什么是类?什么是元类?类对象和类,元类对象和元类有什么区别?类: 是面向对象程序设计(OOP,Object-Orie...
继续阅读 »




什么是对象 ,OC中的对象有哪些?

对象是类的实例;是通过一个类创建出来的实例,一般称之为实例对象;OC中的常见对象有实例对象、类对象、元类对象;

什么是类?什么是元类?类对象和类,元类对象和元类有什么区别?

类: 是面向对象程序设计(OOP,Object-Oriented Programming)实现信息封装的基础。类是一种用户定义的引用数据类型,也称类类型。每个类包含数据说明和一组操作数据或传递消息的函数。类的实例称为对象
元类:以类作为其实例的类;
类对象:类本身也是一个对象,所以就有类对象;类对象可以通过实例对象的ISA指针获得
元类对象:元类本身也是一个对象,所以就有元类对象;元类对象可以通过类对象的ISA指针获得
区分二者:

  • 类、元类是面向对象编程中的一种类型
  • 类对象、元类对象是一种对象

什么是分类?

分类也是一个类,其底层结构和类稍有不同;给分类添加的方法会在运行时合并到原有类的方法列表(二维数组)中
分类多用来给类做扩展使用;在OC开发中应用广泛





什么是类扩展?
类扩展:用来给类扩充私有属性、方法

什么是数组?
数组可表示为占用一块连续的内存空间用来存储元素的数据结构;OC中的数组有可变和不可变两种;可变数组做了优化利用环形缓冲区技术提高增删改查时的性能

什么是字典?
字典以键值的形式存储数据,底层实现是哈希表;OC对象作为字典的Key需要遵守NSCopying协议并且实现hash和isEqual两个方法。比如:NSNumber、NSArray 、NSDictionary、自定义OC对象 都可以作为key

什么是集合?
集合是一种用来存储数据的数据结构,内部存储的数据时无序的,其他和数组相同

OC语法有哪些?
OC中的语法有点语法.,这里的点一般转化为setter、getter方法调用

什么是Method?
Method是method_t的结构体,是对一个方法的描述:

struct method_t{
SEL name; //函数名/方法名
const char *types;//编码(返回值类型、参数类型)
IMP imp; //指向函数的指针(函数地址)
}



什么是内敛函数?

  • 内联函数基本概念
    在c++中,预定义宏的概念是用内联函数来实现的,而内联函数本身也是一个真正的函数。内联函数具有普通函数的所有行为。唯一不同之处在于内联函数会在适当的地方像预定义宏一样展开,所以不需要函数调用的开销。因此应该不使用宏,使用内联函数。

在普通函数(非成员函数)函数前面加上inline关键字使之成为内联函数。但是必须注意必须函数体和声明结合在一起,否则编译器将它作为普通函数来对待。

inline void func(int a);

以上写法没有任何效果,仅仅是声明函数,应该如下方式来做:

inline int func(int a){return ++;}

注意: 编译器将会检查函数参数列表使用是否正确,并返回值(进行必要的转换)。

这些事预处理器无法完成的。
内联函数的确占用空间,但是内联函数相对于普通函数的优势只是省去了函数调用时候的压栈,跳转,返回的开销。我们可以理解为内联函数是以空间换时间。

  • 类内部的内联函数

    为了定义内联函数,通常必须在函数定义前面放一个inline关键字。但是在类内部定义内联函数时并不是必须的。任何在类内部定义的函数自动成为内联函数。

什么是构造函数?
在一个类中定义一个和类名相同的函数,这个函数就是构造函数

面向对象的设计原则是什么 ?
单一责任原则开闭原则接口隔离原则依赖倒置原则里式替换原则迪米特原则

  • 单一责任原则
    一个类只负责一件事情,CALayer只负责动画和视图的显示,UIView只负责事件的传递、事件的响应
  • 开闭原则
    对修改关闭,对扩展开放; 要考虑API的后续扩展,而不是在原有基础上来回修改
  • 接口隔离原则
    使用多协议的方式来定义接口,而不是一个臃肿的协议;比如delagate, datesource
  • 依赖倒置原则
    抽象不应该依赖具体实现,具体实现依赖于抽象
  • 里式替换原则
    父类和子类无缝衔接,且原有功能不受影响;比如:KVO, 用完就走不留痕迹
  • 迪米特原则
    高内聚,低耦合

面向对象语言的三大特性是什么 ?
封装、继承、多态

OC的继承体系





关键字的使用标准?
ARC环境修饰OC对象用strong、copy、weak,修饰基本数据类型用assign;
静态变量用static, 修饰为常量用const

常用设计模式?

设计模式分为四类:结构型模式、创建型模式、行为型模式、软件设计原则

常用的结构型模式有:代理、装饰
常用的创建型模式有:单利、工厂
常用的行为型模式有:观察者、发布订阅模式

代理:是一种消息传递方式,一个完整的代理模式包括:委托对象、代理对象和协议。

  • 请代理三部曲:
    1 定义代理协议
    2 声明delegate对象
    3 调用代理方法
  • 当别人代理的三部曲
    1 遵循协议
    2 设置代理
    3 实现方法

装饰:动态地给一个类添加一些额外的职责;Category 就是实现了装饰的设计模式;Category是一个特殊的类,通过它可以给类添加方法的接口与实现;
观察者:包含通知和KVO
单利:单:唯一,例:实例;即唯一的一个实例,该实例自创建开始到程序退出由系统自动释放;单利常被当做共有类使用;

系统常见单利类
UIApplication(应用程序实例类)
NSNotificationCenter(消息中心类)
NSFileManager(文件管理类)
NSUserDefaults(应用程序设置)
NSURLCache(请求缓存类)
NSHTTPCookieStorage(应用程序cookies池)


工厂模式:分为简单工厂模式、工厂模式、抽象工厂模式
简单工厂模式:简单工厂模式是由一个工厂对象决定创建出哪一种产品类的实例。简单工厂模式是工厂模式家族中最简单实用的模式,可以理解为是不同工厂模式的一个特殊实现。
工厂模式:抽象了工厂接口的具体产品,应用程序的调用不同工厂创建不同产品对象。(抽象产品)
抽象工厂模式:在工厂模式的基础上抽象了工厂,应用程序调用抽象的工厂发发创建不同产品对象。(抽象产品+抽象工厂)

懒加载:把初始化逻辑通过重写的方式封装起来,到需要时直接调用的方式
懒加载的优点

  • 相对来说,如果代码量不是很多,可读性略强
  • 相对来说,防止为nil,减少了后续使用时安全检查的后顾之忧
  • 使用适当,可节省内存资源
  • 一定程度上,节省了某一个期间内的时间
  • 使用得当,优化性能,提高用户体验
    懒加载的缺点
  • 使用太泛滥,导致可读性变差
  • 使用不得当,可能会造成死循环,导致crash
  • 代码量增多(每增加一个懒加载,代码会平均多出3-4行)

什么时候使用懒加载?

一般情况下,不需要使用懒加载,懒加载未必能增强可读性、独立性,滥用反而让可读性适得其反。简言之,就是在逻辑上,觉得现在不需要加载,而在后面某一时间段内可能会加载,就可以考虑懒加载

生产者消费者:
在编码中,有时会遇到一个模块产生数据,另外一个模块处理数据的情况,不论是为了模块间的结偶或是并发处理还是忙闲不均,我们都会在产生和处理数据的模块之间放置缓存区,作为生产和处理数据的仓库。以上的模型就是生产者消费者模型
生产者-消费者

中介者:

  • 中介者模式又叫做调停者模式,其实就是中间人或者调停者的意思
  • 概念:中介者模式(Mediator),用一个中介者对象来封装一系列的对象交互。中介者使各个对象不需要显式地相互引用,从而使其耦合松散,而且可 以独立地改变他们之间的交互
  • UINavigationViewController就是属于一个中介者
  • 中介者模式的优缺点
    中介者模式很容易在系统中应用,也很容易在系统中误用。当系统出现了多对多交互复杂的对象群时,不要急于使用中介者模式,而要先反思你在系统上设计是否合理。
    优点就是集中控制,减少了对象之间的耦合度。缺点就是太过于集中
  • 应用场景
    对象间的交互虽定义明确然而非常复杂,导致一组对象彼此相互依赖而且难以理解。
    因为对象引用了许多其他对象并与其通信,导致对象难以复用。
    想要定制一个分布在多个类中的逻辑或者行为,又不想生成太多子类

发布订阅模式

其实基本的设计模式中并没有发布订阅模式,上面也说了,他只是观察者模式的一个别称。但是经过时间的沉淀,似乎他已经强大了起来,已经独立于观察者模式,成为另外一种不同的设计模式。
在现在的发布订阅模式中,称为发布者的消息发送者不会将消息直接发送给订阅者,这意味着发布者和订阅者不知道彼此的存在。在发布者和订阅者之间存在第三个组件,称为消息代理或调度中心或中间件,它维持着发布者和订阅者之间的联系,过滤所有发布者传入的消息并相应地分发它们给订阅者。

举一个例子,你在微博上关注了A,同时其他很多人也关注了A,那么当A发布动态的时候,微博就会为你们推送这条动态。A就是发布者,你是订阅者,微博就是调度中心,你和A是没有直接的消息往来的,全是通过微博来协调的(你的关注,A的发布动态

观察者模式和发布订阅模式有什么区别?

观察者模式: 观察者(Observer)直接订阅(Subscribe)主题(Subject),而当主题被激活的时候,会触发(Fire Event)观察者里的事件。

发布订阅模式: 订阅者(Subscriber)把自己想订阅的事件注册(Subscribe)到调度中心(Topic),当发布者(Publisher)发布该事件(Publish topic)到调度中心,也就是该事件触发时,由调度中心统一调度(Fire Event)订阅者注册到调度中心的处理代码。常见的是用协议的方式来做

  • 观察者模式是不是发布订阅模式

网上关于这个问题的回答,出现了两极分化,有认为发布订阅模式就是观察者模式的,也有认为观察者模式和发布订阅模式是真不一样的。

其实我不知道发布订阅模式是不是观察者模式,就像我不知道辨别模式的关键是设计意图还是设计结构(理念),虽然《JavaScript设计模式与开发实践》一书中说了分辨模式的关键是意图而不是结构。

如果以结构来分辨模式,发布订阅模式相比观察者模式多了一个中间件订阅器,所以发布订阅模式是不同于观察者模式的;如果以意图来分辨模式,他们都是实现了对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知,并自动更新,那么他们就是同一种模式,发布订阅模式是在观察者模式的基础上做的优化升级。

不过,不管他们是不是同一个设计模式,他们的实现方式确实有差别,我们在使用的时候应该根据场景来判断选择哪个

block

  • block是封装了函数调用和函数调用环境的OC对象,block分为3种类型:NSGlobalBlock、NSStackBlock、NSMallocBlock; 其都继承自NSBlock,NSBlock 继承自NSObject;
  • block使用需注意循环引用问题;一般需要使用强弱引用、__block来解决问题
  • block声明为属性时需要使用copy或strong来修饰;因为block最初是被分配在栈空间,内存由系统管理;但一般使用block是需要在运行时的某一个时机使用,所以需要开发者自己管理block的内存,使用copy和strong修饰会把block的内存复制到堆空间,这样就达到了自己管理内存的目的

为什么block一开始的内存会被分配在栈空间?
block使用会有两种情况:局部变量typedef声明
局部变量申请的内存肯定在栈空间

对象的本质?
对象的本质是结构体;
内存分配原理:以16个字节为单位且遵循了内存对齐原则向堆内存申请内存空间

isa指针?
isa指针是OC对象的第一个成员变量;isa是一个联合体结构,通过位域来存储数据;
isa最重要的作用是用于消息发送;

位域宏定义(真机环境arm64)
# if __arm64__
# define ISA_MASK 0x0000000ffffffff8ULL
# define ISA_MAGIC_MASK 0x000003f000000001ULL
# define ISA_MAGIC_VALUE 0x000001a000000001ULL
# define ISA_BITFIELD \
uintptr_t nonpointer : 1; 拿二进制的1位来存储 \
uintptr_t has_assoc : 1; \
uintptr_t has_cxx_dtor : 1; \
uintptr_t shiftcls : 33;
/*MACH_VM_MAX_ADDRESS 0x1000000000*/ \
uintptr_t magic : 6; \
uintptr_t weakly_referenced : 1; \
uintptr_t deallocating : 1; \
uintptr_t has_sidetable_rc : 1; \
uintptr_t extra_rc : 19
# define RC_ONE (1ULL<<45)
# define RC_HALF (1ULL<<18)



OC的内存管理原则 ?
OC中内存管理是通过引用计数管理实现的,当一个对象的引用计数为0时就会进入释放流程;ARC利用LLVM编译器动态的在合适的位置添加内存管理代码的方式帮助开发者管理内存,同时又通过runtime管理weak修饰的弱引用表;基本实现了不用开发者关心内存问题就可以进行开发;

  • block、定时器时需要注意循环引用问题
  • 声明属性时需要注意强弱引用的使用

多线程?

即 multithreading , 是指从软件或者硬件上实现多个线程并发执行的技术。
具有多线程能力的计算机因有硬件支持而能够在同一时间执行多于一个线程,进而提升整体处理性能。具有这种能力的系统包括对称多处理机、多核心处理器以及芯片级多处理(Chip-level multithreading)或同时多线程(Simultaneous multithreading)处理器。在一个程序中,这些独立运行的程序片段叫作“线程”(Thread),利用它编程的概念就叫作“多线程处理(Multithreading)”。
多线程的调度原理可以认为是:时间片轮转调度算法,每个线程都会分配一个时间片然后大家轮着做任务,多线程执行时会快速切换时间片来完成多线程任务的执行;其实操作系统对进程、线程都是按照这种调度逻辑实现的

程序、进程、线程、例程、协程是什么?

  • 程序:全称 计算机程序(Computer Program),是一组计算机能识别和执行的指令,又称计算机软件
    是指为了得到某种结果而可以由计算机等具有信息处理能力的装置执行的代码化指令序列,用某些程序设计语言编写,如:C、C++、OC等;它运行于电子计算机

  • 进程:是计算机中的程序关于某数据集合上的一次运行活动;是独立运行、独立分配资源和独立接受调度的基本单位;是操作系统结构的基础

在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体

  • 线程:线程是计算机调度的最小单位,用来处理不同的任务;

  • 例程:即函数,一个函数就可以看做是一个例程

  • 协程:利用单线程执行多任务的技术解决方案,性能上避免线程间切换要优于线程调度;是线程的更小拆分,又称之为“微线程”,是一种用户太的轻量级线程;
    和线程的区别:
    线程是系统级别的,它们由操作系统调度;同时是可被调度的最小单位;
    协程则是程序级别的,由程序员根据需要自己调度
    子程序:函数
    在一个线程中会有很多子程序,在子程序执行过程中可以中断去执行别的子程序,而别的子程序也可以中断回来继续执行之前的子程序,这个过程及称为协程。也就是说在同一线程内一段代码在执行过程中会中断然后跳转执行别的代码,接着在之前中断的地方继续开始执行,类似于yield操作

实例的生命周期?

  • alloc、new、copy、mutableCopy
  • 引用计数变化
  • 引用计数为0
  • dealloc


作者:9523_it
链接:https://www.jianshu.com/p/7646a2e8165f





收起阅读 »

直播APP开发搭建,直播APP源码涉及方方面面

最近直播APP源码的热度持续上升,究其原因还是因为直播市场的繁荣。直播APP开发搭建到底能做哪些事呢?1. 主播用户开启手机直播 可以设置只有签约主播才能开启手机直播,也可以设置所有注册用户都有权限 开启手机直播的时候可以选择手机前后手机摄像头设备,也可以在直...
继续阅读 »

最近直播APP源码的热度持续上升,究其原因还是因为直播市场的繁荣。

直播APP开发搭建到底能做哪些事呢?

1. 主播用户开启手机直播 可以设置只有签约主播才能开启手机直播,也可以设置所有注册用户都有权限 开启手机直播的时候可以选择手机前后手机摄像头设备,也可以在直播过程中进行切换。

2. 玩家用户观看手机直播 玩家可以通过手机APP观看主播的直播。

3. 用户之间聊天互动 用户可以通过聊天窗口进行文字和表情的聊天,聊天部分使用node.js实现。

4. 用户之间礼物赠送 玩家可以充值后购买赠送礼物给主播,用户和主播分别可以在用户中心可以看到自己赠送和获得礼物的详细列表。

播放列表

直播APP开发的媒体播放器提供下面的方法来访问播放列表中的剪辑:

Next 方法,跳到节目(播放列表)中的下一个剪辑;

Previous 方法,跳回到节目中的上一个剪辑;

媒体播放器的一个特性是能够预览节目中的每一个剪辑,使用如下属性:

PreviewMode 属性,决定媒体播放器当前是否处于预览模式;

直播APP开发搭建,直播APP源码涉及方方面面

了解流媒体(直播APP开发搭建需要用到流媒体)

流媒体开发:网络层(socket或st)负责传输,协议层(rtmp或hls)负责网络打包,封装层(flv、ts)负责编解码数据的封装,编码层(h.264和aac)负责图像,音频压缩。

帧:每帧代表一幅静止的图像。

GOP:(Group of Pictures)画面组,一个GOP就是一组连续的画面,每个画面都是一帧,一个GOP就是很多帧的集合。

直播的数据,其实是一组图片,包括I帧、P帧、B帧,当用户第一次观看的时候,会寻找I帧,而播放器会到服务器寻找到最近的I帧反馈给用户。因此,GOP Cache增加了端到端延迟,因为它必须要拿到最近的I帧。

直播APP的开发说难也难,说容易也相当容易。

难,是因为搞个直播要考虑的东西太多了,根本不是一个人能够搞定的。因为直播中运用到的技术难点非常之多,视频/音频处理,图形处理,视频/音频压缩,CDN分发,即时通讯等技术,每一个技术都够你学几年的。

从采集编码传输到解码播放这个过程要处理的东西太多了:单就视频的采集来说就涉及两方面数据的采集:音频采集和图像采集,涉及到各种算法,这是一个很庞大的工作量。


收起阅读 »

iOS Swift 高阶函数

iOS
在Swift的集合类型中,有许多十分便捷的函数。相比于Objective-C,这些高阶函数会引起你的极度舒适。因为在Swift的许多函数中引入了闭包元素,这就直接造就了它的灵活性,简直就是极致的便捷。下面就来对Swift集合类中的这些高阶函数进行总结。// 全...
继续阅读 »

在Swift的集合类型中,有许多十分便捷的函数。相比于Objective-C,这些高阶函数会引起你的极度舒适。因为在Swift的许多函数中引入了闭包元素,这就直接造就了它的灵活性,简直就是极致的便捷。

下面就来对Swift集合类中的这些高阶函数进行总结。

// 全文的基础数据
let numbers = [7, 6, 10, 9, 8, 1, 2, 3, 4, 5]
1
2
1.sort函数
对原集合进行给定条件排序。
无返回值,直接修改原集合,所以这个集合应该是可变类型的。

var sortArr = numbers
numbers.sort { a, b in
return a < b
}
// [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
1
2
3
4
5
另外,系统还定义了一个sort()函数,即对集合进行升序排序的函数。但这个函数并不是上面函数不传入缺省值的情况,而是另外一个函数。

var sortArr2 = numbers
numbers.sort()
// [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
1
2
3
2.sorted函数
sorted函数与sort函数对应。
将集合进行给定条件排序,返回一个新的集合,不修改原集合。

let sortedArr = numbers.sorted { a, b in
return a > b
}
// [10, 9, 8, 7, 6, 5, 4, 3, 2, 1]

// sorted()函数
let sortedArr2 = numbers.sorted()
// [10, 9, 8, 7, 6, 5, 4, 3, 2, 1]

// 闭包简写
let sortedArr3 = sortedArr2.sorted(by: >)
// [10, 9, 8, 7, 6, 5, 4, 3, 2, 1]
1
2
3
4
5
6
7
8
9
10
11
12
闭包的省略写法
因为在高阶函数中大部分都使用了闭包,所以我认为有必要做一个铺垫,以更好地理解本文。清楚闭包简写的请跳过本段,直奔第3条。

由于sort函数使用了闭包,所以自主定义的闭包可以简写为如下格式:

numbers.sort(by: >)
// [10, 9, 8, 7, 6, 5, 4, 3, 2, 1]
1
2
以上述方法为例,一个完整的闭包应该是这样的:

numbers.sorted { (a: Int, b: Int) -> Bool in
return a > b
}
1
2
3
然后,可以省略闭包中的返回值。

numbers.sorted { (a: Int, b: Int) in
return a > b
}
1
2
3
然后,再可以省略形参的类型,让编译器去自主推断。

numbers.sorted { a, b in
return a > b
}
1
2
3
再然后,还可以让$0,$1…来代替第一个,第二个形参,以此类推。

numbers.sorted { return $0 > $1 }
1
再然后,省略return。一般的,到这里也就足够简化了。毕竟在实际开发中我们需要使用闭包中的参数进行一些复杂的判断。

numbers.sorted { $0 > $1 }
1
如果你不需要复杂的判断,那么还可以写成下面这样,代表降序排序。

numbers.sorted(by: >)
1
3.map函数
按照闭包中的返回结果,将集合中对应元素进行替代,也就是映射函数。

// 数组数值转换为其各自平方
let mapArr = numbers.map { $0 * $0 }
// [49, 36, 100, 81, 64, 1, 4, 9, 16, 25]
1
2
3
可选类型的 map, flatMap函数
另外,不仅CollectionType有map和flatMap函数,在Optional类型中,也存在这两个函数。
它们的作用是对可选类型就行解包操作,若有值则进入闭包,并返回一个 Optional类型;若为nil,则直接返回当前可选类型的nil。

let num1: Int? = 3
let num2: Int? = nil

let numMap1 = num1.map {
$0 * 2
}
numMap1 // 6
type(of: numMap1) // Optional<Int>.Type

let numMap2 = num2.map {
$0 == 0
}
numMap2 // nil
type(of: numMap2) // Optional<Bool>.Type


let numFlatMap1 = num1.flatMap {
$0 * $0
}
numFlatMap1 // 9
type(of: numFlatMap1) // Optional<Int>.Type

let numFlatMap2 = num2.flatMap {
$0 == 0
}
numFlatMap2 // nil
type(of: numFlatMap2) // Optional<Bool>.Type
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
还有一种应用场景,就是解析可选类型的时候,map和flatMap函数会让你的代码更加优雅。

举个例子,当解析并判断可选类型的时候,你可能会经过一堆if或者guard判断,如下所示:

func loadURL(url: URL) {
print(url.absoluteString)
}

let urlStr: String? = "https://github.com/wangyanchang21"
guard let siteStr = urlStr else {
assert(false)
}
guard let url = URL(string: siteStr) else {
assert(false)
}
loadURL(url: url)
1
2
3
4
5
6
7
8
9
10
11
12
如果使用map和flatMap函数的话,就会有十分优雅的感觉。

// 这行优雅的代码代替上面的代码
urlStr.flatMap(URL.init).map(loadURL)
1
2
但有一点需要注意,这里 map替换 flatMap会报错, 原因在于 flatMap闭包可以返回 nil, 而 map闭包不可以。就如下面的代码编译不会通过:

// compile error
// urlStr.map(URL.init).map(loadURL)
1
2
再举一个例子:

let date: Date? = Date()
let format = date.map(DateFormatter().string)
1
2
我在函数的闭包形式中也写过这种优雅的写法具体是怎么回事。有兴趣可以了解一下。

4.flatMap函数
也是一种映射函数,这个函数具有多重功能,所以也就造成了这个函数有一个历史问题,稍后会解释。

第一种情况,解析首层元素,若有nil则过滤,就不会降维

let optLatticeNumbers = [[1, Optional(2), 3], [3, nil, 5], nil]
// 解析首层元素, 若有nil则过滤, 就不会降维
let flatMapArr2 = optLatticeNumbers.flatMap { $0 }
// [[1, 2, 3], [3, nil, 5]]
1
2
3
4
第二种情况,解析首层元素,若没有nil,则会降维

let latticeNumbers = [[1, Optional(2), 3], [3, nil, 5]]
// 解析首层元素, 若没有nil, 则会降维
let flatMapArr = latticeNumbers.flatMap { $0 }
// [1, 2, 3, 3, nil, 5]
1
2
3
4
所以flatMap的功能就有两个了,一个功能是解析并过滤首层元素为nil的元素,一个功能是对多维集合进行降维。原因是,其实这是两个功能是两个函数,只是在调用时代码上没有区别。

flatMap和compactMap的关系
但从表面上看,flatMap函数违背了单一功能原则,将过滤nil和降维两个功能于隐藏条件中进行判定。这也就是那个历史问题。

因此,为了将过滤nil和降维两个功能于区分开,swift4.1开始,就只保留了降维的flatMap函数,并弃用了过滤nil的flatMap函数,又用开放的新函数compactMap来替代弃用的函数。

所以,当需要过滤nil的时候,请使用compactMap函数;当需要进行降维时,请使用flatMap函数。这也就是flatMap和compactMap之间的区别。

5.compactMap函数
Swift4.1开始开放的一种映射函数,会解析并过滤首层元素为nil的元素。

let compactMapArr = optLatticeNumbers.compactMap { $0 }
// [[1, 2, 3], [3, nil, 5]]
let compactMapArr2 = latticeNumbers.compactMap { $0 }
// [[1, 2, 3], [3, nil, 5]]
1
2
3
4
compactMap函数作为过滤nil的flatMap函数的替代函数。当集合中的元素为一个一维集合,他们之间的功能是没有差别的。

let flatNumbers = [1, Optional(2), 3, nil, Optional(5), nil]

let flatMapArr = latticeNumbers.flatMap { $0 }
// [1, 2, 3, 5]
let compactMapArr = optLatticeNumbers.compactMap { $0 }
// [1, 2, 3, 5]
1
2
3
4
5
6
6.filter函数
按照条件进行元素过滤。

let filterArr = numbers.filter { num in
return num < 3 || num > 8
}
// [10, 9, 1, 2]
1
2
3
4
7.reduce函数
以指定参数为基础,按照条件进行拼接

let reduceNumber = numbers.reduce(100) { result, num in
return result + num
}
// 155

let reduceString = ["C", "O", "D", "E"].reduce("word: ") { result, num in
return result + num
}
// "word: CODE"
1
2
3
4
5
6
7
8
9
8.prefix函数
正向取满足条件的元素,进行新集合创建。一旦出现不满足条件的元素,则跳出循环,不再执行。

let prefixArr = numbers.prefix { $0 < 10 }
// [7, 6]
1
2
prefix相关函数:
upTo: 正向取元素创建数组, 包含小于指定index的元素

let prefixUpToArr = numbers.prefix(upTo: 5)
// [7, 6, 10, 9, 8]
1
2
through: 正向取元素创建数组, 包含小于等于指定index的元素

let prefixThroughArr = numbers.prefix(through: 2)
// [7, 6, 10]
1
2
maxLength: 正向取元素创建数组, 包含指定的元素个数

let prefixMaxLengthArr = numbers.prefix(6)
// [7, 6, 10, 9, 8, 1]
1
2
9.drop函数
与prefix函数对应。正向跳过满足条件的元素,进行新集合创建。一旦出现不满足条件的元素,则跳出循环,不再执行。

let dropArr = numbers.drop { $0 < 10 }
// [10, 9, 8, 1, 2, 3, 4, 5]
1
2
drop相关函数:
dropFirst: 正向跳过元素创建数组, 跳过指定元素个数, 缺省值为1

let dropFirstArr = numbers.dropFirst(3)
// [7, 6, 10, 9, 8]
1
2
dropLast: 返向跳过元素创建数组, 跳过指定元素个数, 缺省值为1

let dropLastArr = numbers.dropLast(5)
// [7, 6, 10, 9, 8]
1
2
10.first函数
正向找出第一个满足条件的元素。

let first = numbers.first { $0 < 7 }
// 6
1
2
11.last函数
与first函数对应。反向找出第一个满足条件的元素。

let last = numbers.last { $0 > 5 }
// 8
1
2
12.firstIndex函数
正向找出第一个满足条件的元素下标。

let firstIndex = numbers.firstIndex { $0 < 7 }
// 1
1
2
13.lastIndex函数
反向找出第一个满足条件的元素下标。

let lastIndex = numbers.lastIndex { $0 > 5 }
// 4
1
2
14.partition函数
按照条件进行重新排序,不满足条件的元素在集合前半部分,满足条件的元素后半部分,但不是完整的升序或者降序排列。
返回值为排序完成后集合中第一个满足条件的元素下标。

var partitionNumbers = [20, 50, 30, 10, 40, 20, 60]
let pIndex = partitionNumbers.partition { $0 > 30 }
// partitionNumbers = [20, 20, 30, 10, 40, 50, 60]
// pIndex = 4
1
2
3
4
15.min函数
按条件排序后取最小元素。

let min = numbers.min { $0 % 5 < $1 % 5 }
// 10
1
2
min()函数,自然升序取最小。

let minDefault = numbers.min()
// 1
1
2
16.max函数
按条件排序后取最大元素。

let maxDictionary = ["aKey": 33, "bKey": 66, "cKey": 99]
let max = maxDictionary.max { $0.value < $1.value }
// (key "cKey", value 99)
1
2
3
max()函数,自然升序取最大。

let maxDefault = numbers.max()
// 10
1
2
17.removeAll函数
移除原集合中所有满足条件的元素。
无返回值,直接修改原集合,所以这个集合应该是可变类型的。

var removeArr = numbers
removeArr.removeAll { $0 > 6 }
// [6, 1, 2, 3, 4, 5]
1
2
3
18.集合遍历
forEach函数:

numbers.forEach { num in
print(num)
}
1
2
3
for-in函数:

for num in numbers where num < 5 {
print(num)
}
1
2
3
与enumerated()函数配合使用:

for (index, num) in numbers.enumerated() {
print("\(index)-\(num)")
}
1
2
3
关于集合遍历的性能问题,可以看这里enumerated() 和 enumerateObjectsUsingBlock。

19.shuffled函数
shuffled函数,打乱集合中元素的的顺序。

let ascendingNumbers = 0...9
let shuffledArr = ascendingNumbers.shuffled()
// [3, 9, 2, 6, 4, 5, 0, 1, 7, 8]
1
2
3
20.contains函数
contains函数,判断集合中是否包含某元素。

let containsBool = numbers.contains(8)
let containsBool1 = numbers.contains(11)
// true
// false
1
2
3
4
21.split和joined函数
split函数,字符串的函数,按条件分割字符串,为子字符串创建集合。与Objective-C中的componentsSeparatedByString:方法类似。

let line = "123Hi!123I'm123a123coder.123"
let splitArr = line.split { $0.isNumber }
// ["Hi!", "I'm", "a", "coder."]

// 也可指定字符
let splitArr2 = line.split(separator: "1")
// ["23Hi!", "23I'm", "23a", "23coder.", "23"]
1
2
3
4
5
6
7
joined函数,数组元素连接指定字符拼接成一个字符串。与Objective-C中的componentsJoinedByString:方法类似。

let joined = splitArr.joined(separator: "_")
// "Hi!_I'm_a_coder."

// 也可以只传入字符
let joined2 = splitArr2.joined(separator: "#")
// "23Hi!#23I'm#23a#23coder.#23"
1
2
3
4
5
6
22.zip函数
将两个数组合并为一个元组组成的数组。

let titles = ["aaa", "bbb", "ccc"]
let numbers = [111, 222, 333]
let zipA = zip(titles, numbers)
for (title, num) in zipA {
print("\(title)-\(num)")
}
1
2
3
4
5
6
打印结果:

aaa-111
bbb-222
ccc-333

原文链接:https://blog.csdn.net/wangyanchang21/article/details/89955249

收起阅读 »

iOS 八种经典排序算法

iOS
一、冒泡排序(Bubble Sort)冒泡排序是一种简单的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果它们的顺序错误就把它们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会...
继续阅读 »

一、冒泡排序(Bubble Sort)

冒泡排序是一种简单的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果它们的顺序错误就把它们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。

1.1算法复杂度

时间平均复杂度:O(n^2) 最坏复杂度:O(n^2) 最好复杂度: O(n) 空间复杂度: O(1) 稳定

1.2算法过程描述

  • <1>比较相邻的元素。如果第一个比第二个大,就交换它们两个;

  • <2>对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对,这样在最后的元素应该会是最大的数;

  • <3>针对所有的元素重复以上的步骤,除了最后一个;

  • <4>重复步骤1~3,直到排序完成。

1.3代码实现

冒泡排序

1.4执行Log信息

冒泡排序Log1

冒泡排序Log2

二、选择排序(Selection Sort)

选择排序(Selection-sort)是一种简单直观的排序算法。它的工作原理:首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。

2.1算法复杂度

时间平均复杂度:O(n^2) 最坏复杂度:O(n^2) 最好复杂度: O(n^2) 空间复杂度: O(1) 不稳定

2.2算法过程描述

n个记录的直接选择排序可经过n-1趟直接选择排序得到有序结果。具体算法描述如下:

  • <1>初始状态:无序区为R[1..n],有序区为空;

  • <2>第i趟排序(i=1,2,3…n-1)开始时,当前有序区和无序区分别为R[1..i-1]和R(i..n)。该趟排序从当前无序区中-选出关键字最小的记录 R[k],将它与无序区的第1个记录R交换,使R[1..i]和R[i+1..n)分别变为记录个数增加1个的新有序区和记录个数减少1个的新无序区;

  • <3>n-1趟结束,数组有序化了。

2.3代码实现

选择排序

2.4执行Log信息

选择排序Log

三、插入排序(Insertion Sort)

插入排序(Insertion-Sort)的算法描述是一种简单直观的排序算法。它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。

3.1算法复杂度

时间平均复杂度:O(n^2) 最坏复杂度:O(n^2) 最好复杂度: O(n) 空间复杂度: O(1) 稳定

3.2算法过程描述

一般来说,插入排序都采用in-place在数组上实现。具体算法描述如下:

  • <1>从第一个元素开始,该元素可以认为已经被排序;

  • <2>取出下一个元素,在已经排序的元素序列中从后向前扫描;

  • <3>如果该元素(已排序)大于新元素,将该元素移到下一位置;

  • <4>重复步骤3,直到找到已排序的元素小于或者等于新元素的位置;

  • <5>将新元素插入到该位置后;

  • <6>重复步骤2~5。

3.3代码实现

插入排序

3.4执行Log信息

插入排序Log

四、希尔排序(Shell Sort)

1959年Shell发明,第一个突破O(n2)的排序算法,是简单插入排序的改进版。它与插入排序的不同之处在于,它会优先比较距离较远的元素。希尔排序又叫缩小增量排序。

4.1算法复杂度

时间平均复杂度:O(n^1.3) 最坏复杂度:O(n^2) 最好复杂度: O(n) 空间复杂度: O(1) 不稳定

4.2算法过程描述

先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,具体算法描述:

  • <1>选择一个增量序列t1,t2,…,tk,其中ti>tj,tk=1;

  • <2>按增量序列个数k,对序列进行k 趟排序;

  • <3>每趟排序,根据对应的增量ti,将待排序列分割成若干长度为m 的子序列,分别对各子表进行直接插入排序。仅增量因子为1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。

4.3代码实现

希尔排序

4.4执行Log信息

希尔排序Log

五、归并排序(Merge Sort)

归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为2-路归并。

5.1算法复杂度

时间平均复杂度:O(nlog2^n) 最坏复杂度:O(nlog2^n) 最好复杂度: O(nlog2^n) 空间复杂度: O(n) 稳定

5.2算法过程描述

  • <1>把长度为n的输入序列分成两个长度为n/2的子序列;

  • <2>对这两个子序列分别采用归并排序;

  • <3>将两个排序好的子序列合并成一个最终的排序序列。

5.3代码实现

归并排序1

归并排序2

5.4执行Log信

归并排序Log

六、快速排序(Quick Sort)

快速排序的基本思想:通过一趟排序将待排记录分隔成独立的两部分,其中一部分记录的关键字均比另一部分的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序。

6.1算法复杂度

时间平均复杂度:O(nlog2^n) 最坏复杂度:O(n^2) 最好复杂度: O(nlog2^n) 空间复杂度: O(nlog2^n) 不稳定

6.2算法过程描述

快速排序使用分治法来把一个串(list)分为两个子串(sub-lists)。具体算法描述如下:

  • <1>从数列中挑出一个元素,称为 “基准”(pivot);

  • <2>重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;

  • <3>递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。

6.3代码实现

快速排序1

快速排序2

6.4执行Log信息

快速排序Log

七、堆排序(Heap Sort)

堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。

7.1算法复杂度

时间平均复杂度:O(nlog2^n) 最坏复杂度:O(nlog2^n) 最好复杂度: O(nlog2^n) 空间复杂度: O(1) 不稳定

7.2算法过程描述

  • <1>将初始待排序关键字序列(R1,R2….Rn)构建成大顶堆,此堆为初始的无序区;

  • <2>将堆顶元素R[1]与最后一个元素R[n]交换,此时得到新的无序区(R1,R2,……Rn-1)和新的有序区(Rn),且满足R[1,2…n-1]<=R[n];

  • <3>由于交换后新的堆顶R[1]可能违反堆的性质,因此需要对当前无序区(R1,R2,……Rn-1)调整为新堆,然后再次将R[1]与无序区最后一个元素交换,得到新的无序区(R1,R2….Rn-2)和新的有序区(Rn-1,Rn)。不断重复此过程直到有序区的元素个数为n-1,则整个排序过程完成。

7.3代码实现

堆排序1

堆排序2

7.4执行Log信息

堆排序Log1

堆排序Log2

八、计数排序(Counting Sort)

计数排序不是基于比较的排序算法,其核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。 作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。

8.1算法复杂度

时间平均复杂度:O(n+k) 最坏复杂度:O(n+k) 最好复杂度: O(n+k) 空间复杂度: O(n+k) 稳定

8.2算法过程描述

  • <1>找出待排序的数组中最大和最小的元素;

  • <2>统计数组中每个值为i的元素出现的次数,存入数组C的第i项;

  • <3>对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加);

  • <4>反向填充目标数组:将每个元素i放在新数组的第C(i)项,每放一个元素就将C(i)减去1。

8.3代码实现

计数排序

8.4执行Log信息

计数排序Log

终于结束了,最后附两张快速排序和堆排序的动态展示图!!!!觉得不错的记得点个喜欢/关注哦!

附:

快速排序动态图

 堆排序动态图


链接 http://www.cocoachina.com/cms/wap.php?action=article&id=22988

收起阅读 »

iOS OC项目转Swift指南

iOS
运行环境:Xcode 11.1 Swift5.0最近参与的一个项目需要从Objective-C(以下简称OC)转到Swift,期间遇到了一些坑,于是有了这篇总结性的文档。如果你也有将OC项目Swift化的需求,可以作为参考。OC转Swift有一个大前提就是你要...
继续阅读 »

运行环境:Xcode 11.1 Swift5.0

最近参与的一个项目需要从Objective-C(以下简称OC)转到Swift,期间遇到了一些坑,于是有了这篇总结性的文档。如果你也有将OC项目Swift化的需求,可以作为参考。

OC转Swift有一个大前提就是你要对Swift有一定的了解,熟悉Swift语法,最好是完整看过一遍官方的Language Guide

转换的过程分自动化和手动转译,鉴于自动化工具的识别率不能让人满意,大部分情况都是需要手动转换的。

自动化工具

有一个比较好的自动化工具Swiftify,可以将OC文件甚至OC工程整个转成Swift,号称准确率能达到90%。我试用了一些免费版中的功能,但感觉效果并不理想,因为没有使用过付费版,所以也不好评价它就是不好。

Swiftify还有一个Xcode的插件Swiftify for Xcode,可以实现对选中代码和单文件的转化。这个插件还挺不错,对纯系统代码转化还算精确,但部分代码还存在一些识别问题,需要手动再修改。

手动Swift化

桥接文件

如果你是在项目中首次使用Swift代码,在添加Swift文件时,Xcode会提示你添加一个.h的桥接文件。如果不小心点了不添加还可以手动导入,就是自己手动生成一个.h文件,然后在Build Settings > Swift Compiler - General > Objective-C Bridging Header中填入该.h文件的路径。

image.png

这个桥接文件的作用就是供Swift代码引用OC代码,或者OC的三方库。

#import "Utility.h"
#import
复制代码

Bridging Header的下面还有一个配置项是Objective-C Generated Interface Header Name,对应的值是ProjectName-Swift.h。这是由Xcode自动生成的一个隐藏头文件,每次Build的过程会将Swift代码中声明为外接调用的部分转成OC代码,OC部分的文件会类似pch一样全局引用这个头文件。因为是Build过程中生成的,所以只有.m文件中可以直接引用,对于在.h文件中的引用下文有介绍。

Appdelegate(程序入口)

Swift中没有main.m文件,取而代之的是@UIApplicationMain命令,该命令等效于原有的执行main.m。所以我们可以把main.m文件进行移除。

系统API

对于UIKit框架中的大部分代码转换可以直接查看系统API文档进行转换,这里就不过多介绍。

property(属性)

Swift没有property,也没有copynonatomic等属性修饰词,只有表示属性是否可变的letvar

注意点一 OC中一个类分.h.m两个文件,分别表示用于暴露给外接的方法,变量和仅供内部使用的方法变量。迁移到Swift时,应该将.m中的property标为private,即外接无法直接访问,对于.h中的property不做处理,取默认的internal,即同模块可访问。

对于函数的迁移也是相同的。

注意点二 有一种特殊情况是在OC项目中,某些属性在内部(.m)可变,外部(.h)只读。这种情况可以这么处理:

private(set) var value: String
复制代码

就是只对valueset方法就行private标记。

注意点三 Swift中针对空类型有个专门的符号?,对应OC中的nil。OC中没有这个符号,但是可以通过在nullablenonnull表示该种属性,方法参数或者返回值是否可以空。

如果OC中没有声明一个属性是否可以为空,那就去默认值nonnull

如果我们想让一个类的所有属性,函数返回值都是nonnull,除了手动一个个添加之外还有一个宏命令。

NS_ASSUME_NONNULL_BEGIN
/* code */
NS_ASSUME_NONNULL_END
复制代码

enum(枚举)

OC代码:

typedef NS_ENUM(NSInteger, PlayerState) {
PlayerStateNone = 0,
PlayerStatePlaying,
PlayerStatePause,
PlayerStateBuffer,
PlayerStateFailed,
};

typedef NS_OPTIONS(NSUInteger, XXViewAnimationOptions) {
XXViewAnimationOptionNone = 1 << 0,
XXViewAnimationOptionSelcted1 = 1 << 1,
XXViewAnimationOptionSelcted2 = 1 << 2,
}
复制代码

Swift代码

enum PlayerState: Int {
case none = 0
case playing
case pause
case buffer
case failed
}
struct ViewAnimationOptions: OptionSet {
let rawValue: UInt
static let None = ViewAnimationOptions(rawValue: 1<<0)
static let Selected1 = ViewAnimationOptions(rawValue: 1<<0)
static let Selected2 = ViewAnimationOptions(rawValue: 1 << 2)
//...
}
复制代码

Swift没有NS_OPTIONS的概念,取而代之的是为了满足OptionSet协议的struct类型。

懒加载

OC代码:

- (MTObject *)object {
if (!_object) {
_object = [MTObject new];
}
return _object;
}
复制代码

Swift代码:

lazy var object: MTObject = {
let object = MTObject()
return imagobjecteView
}()
复制代码

闭包

OC代码:

typedef void (^DownloadStateBlock)(BOOL isComplete);
复制代码

Swift代码:

typealias DownloadStateBlock = ((_ isComplete: Bool) -> Void)
复制代码

单例

OC代码:

+ (XXManager *)shareInstance {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
instance = [[self alloc] init];
});
return instance;
}
复制代码

Swift对单例的实现比较简单,有两种方式:

第一种

let shared = XXManager()// 声明在全局命名区(global namespace)
Class XXManager {
}
复制代码

你可能会疑惑,为什么没有dispatch_once,如何保证多线程下创建的唯一性?其实是这样的,Swift中全局变量是懒加载,在AppDelegate中被初始化,之后所有的调用都会使用该实例。而且全局变量的初始化是默认使用dispatch_once的,这保证了全局变量的构造器(initializer)只会被调用一次,保证了shard原子性

第二种

Class XXManager {
static let shared = XXManager()
private override init() {
// do something
}
}
复制代码

Swift 2 开始增加了static关键字,用于限定变量的作用域。如果不使用static,那么每一个shared都会对应一个实例。而使用static之后,shared成为全局变量,就成了跟上面第一种方式原理一致。可以注意到,由于构造器使用了 private 关键字,所以也保证了单例的原子性。

初始化方法和析构函数

对于初始化方法OC先调用父类的初始化方法,然后初始自己的成员变量。Swift先初始化自己的成员变量,然后在调用父类的初始化方法。

OC代码:

// 初始化方法
@interface MainView : UIView
@property (nonatomic, strong) NSString *title;
- (instancetype)initWithFrame:(CGRect)frame title:(NSString *)title NS_DESIGNATED_INITIALIZER;
@end

@implementation MainView
- (instancetype)initWithFrame:(CGRect)frame title:(NSString *)title {
if (self = [super initWithFrame:frame]) {
self.title = title;
}
return self;
}
@end
// 析构函数
- (void)dealloc {
//dealloc
}
复制代码

上面类在调用时

Swift代码:

class MainViewSwift: UIView {
let title: String
init(frame: CGRect, title: String) {
self.title = title
super.init(frame: frame)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
//deinit
}
}
复制代码

函数调用

OC代码:

// 实例函数(共有方法)
- (void)configModelWith:(XXModel *)model {}
// 实例函数(私有方法)
- (void)calculateProgress {}
// 类函数
+ (void)configModelWith:(XXModel *)model {}
复制代码
// 实例函数(共有方法)
func configModel(with model: XXModel) {}
// 实例函数(私有方法)
private func calculateProgress() {}
// 类函数(不可以被子类重写)
static func configModel(with model: XXModel) {}
// 类函数(可以被子类重写)
class func configModel(with model: XXModel) {}
// 类函数(不可以被子类重写)
class final func configModel(with model: XXModel) {}
复制代码

OC可以通过是否将方法声明在.h文件表明该方法是否为私有方法。Swift中没有了.h文件,对于方法的权限控制是通过权限关键词进行的,各关键词权限大小为: private < fileprivate < internal < public < open

其中internal为默认权限,可以在同一module下访问。

NSNotification(通知)

OC代码:

// add observer
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(method) name:@"NotificationName" object:nil];
// post
[NSNotificationCenter.defaultCenter postNotificationName:@"NotificationName" object:nil];
复制代码

Swift代码:

// add observer
NotificationCenter.default.addObserver(self, selector: #selector(method), name: NSNotification.Name(rawValue: "NotificationName"), object: nil)
// post
NotificationCenter.default.post(name: NSNotification.Name(rawValue: "NotificationName"), object: self)
复制代码

可以注意到,Swift中通知中心NotificationCenter不带NS前缀,通知名由字符串变成了NSNotification.Name的结构体。

改成结构体的目的就是为了便于管理字符串,原本的字符串类型变成了指定的NSNotification.Name类型。上面的Swift代码可以修改为:

extension NSNotification.Name {
static let NotificationName = NSNotification.Name("NotificationName")
}
// add observer
NotificationCenter.default.addObserver(self, selector: #selector(method), name: .NotificationName, object: nil)
// post
NotificationCenter.default.post(name: .NotificationName, object: self)
复制代码

protocol(协议/代理)

OC代码:

@protocol XXManagerDelegate 
- (void)downloadFileFailed:(NSError *)error;
@optional
- (void)downloadFileComplete;
@end

@interface XXManager: NSObject
@property (nonatomic, weak) id delegate;
@end
复制代码

Swift中对protocol的使用拓宽了许多,不光是class对象,structenum也都可以实现协议。需要注意的是structenum为指引用类型,不能使用weak修饰。只有指定当前代理只支持类对象,才能使用weak。将上面的代码转成对应的Swift代码,就是:

@objc protocol XXManagerDelegate {
func downloadFailFailed(error: Error)
@objc optional func downloadFileComplete() // 可选协议的实现
}
class XXManager: NSObject {
weak var delegate: XXManagerDelegate?
}
复制代码

@objc是表明当前代码是针对NSObject对象,也就是class对象,就可以正常使用weak了。

如果不是针对NSObject对象的delegate,仅仅是普通的class对象可以这样设置代理:

protocol XXManagerDelegate: class {
func downloadFailFailed(error: Error)
}
class XXManager {
weak var delegate: XXManagerDelegate?
}
复制代码

值得注意的是,仅@objc标记的protocol可以使用@optional

Swift和OC混编注意事项

函数名的变化

如果你在一个Swift类里定义了一个delegate方法:

@objc protocol MarkButtonDelegate {
func clickBtn(title: String)
}
复制代码

如果你要在OC中实现这个协议,这时候方法名就变成了:

- (void)clickBtnWithTitle:(NSString *)title {
// code
}
复制代码

这主要是因为Swift有指定参数标签,OC却没有,所以在由Swift方法名生成OC方法名时编译器会自动加一些修饰词,已使函数作为一个句子可以"通顺"。

在OC的头文件里调用Swift类

如果要在OC的头文件里引用Swift类,因为Swift没有头文件,而为了让在头文件能够识别该Swift类,需要通过@class的方法引入。

@class SwiftClass;

@interface XXOCClass: NSObject
@property (nonatomic, strong) SwiftClass *object;
@end
复制代码

对OC类在Swift调用下重命名

因为Swift对不同的module都有命名空间,所以Swift类都不需要添加前缀。如果有一个带前缀的OC公共组件,在Swift环境下调用时不得不指定前缀是一件很不优雅的事情,所以苹果添加了一个宏命令NS_SWIFT_NAME,允许在OC类在Swift环境下的重命名:

NS_SWIFT_NAME(LoginManager)
@interface XXLoginManager: NSObject
@end
复制代码

这样我们就将XXLoginManager在Swift环境下的类名改为了LoginManager

引用类型和值类型

  • struct 和 enum 是值类型,类 class 是引用类型。
  • StringArray和 Dictionary都是结构体,因此赋值直接是拷贝,而NSStringNSArray 和NSDictionary则是类,所以是使用引用的方式。
  • struct 比 class 更“轻量级”,struct 分配在栈中,class 分配在堆中。

id类型和AnyObject

OC中id类型被Swift调用时会自动转成AnyObject,他们很相似,但却其实概念并不一致。Swift中还有一个概念是Any,他们三者的区别是:

  • id 是一种通用的对象类型,它可以指向属于任何类的对象,在OC中即是可以代表所有继承于NSObject的对象。
  • AnyObject可以代表任何class类型的实例。
  • Any可以代表任何类型,甚至包括func类型。

从范围大小比较就是:id < AnyObject < Any

其他语法区别及注意事项(待补充)

1、Swift语句中不需要加分号;

2、关于Bool类型更加严格,Swift不再是OC中的非0就是真,真假只对应truefalse

3、Swift类内一般不需要写self,但是闭包内是需要写的。

4、Swift是强类型语言,必须要指定明确的类型。在Swift中IntFloat是不能直接做运算的,必须要将他们转成同一类型才可以运算。

5、Swift抛弃了传统的++--运算,抛弃了传统的C语言式的for循环写法,而改为for-in

6、Swift的switch操作,不需要在每个case语句结束的时候都添加break

7、Swift对enum的使用做了很大的扩展,可以支持任意类型,而OC枚举仅支持Int类型,如果要写兼容代码,要选择Int型枚举。

8、Swift代码要想被OC调用,需要在属性和方法名前面加上@objc

9、Swift独有的特性,如泛型,struct,非Int型的enum等被包含才函数参数中,即使添加@objc也不会被编译器通过。

10、Swift支持重载,OC不支持。

11、带默认值的Swift函数再被OC调用时会自动展开。

语法检查

对于OC转Swift之后的语法变化还有很多细节值得注意,特别是对于初次使用Swift这门语言的同学,很容易遗漏或者待着OC的思想去写代码。这里推荐一个语法检查的框架SwiftLint,可以自动化的检查我们的代码是否符合Swift规范。

可以通过cocoapods进行引入,配置好之后,每次Build的过程,Lint脚本都会执行一遍Swift代码的语法检查操作,Lint还会将代码规范进行分级,严重的代码错误会直接报错,导致程序无法启动,不太严重的会显示代码警告(⚠️)。

如果你感觉SwiftLint有点过于严格了,还可以通过修改.swiftlint.yml文件,自定义属于自己的语法规范。

链接:https://juejin.im/post/5e5a4f20518825495a277aa7

收起阅读 »

Android 面试题及答案

15、 说说mvc模式的原理,它在android中的运用,android的官方建议应用程序的开发采用mvc模式。何谓mvc? mvc是model,view,controller的缩写,mvc包含三个部分:   模型(model)对象:是应用程序的主体部分,所有...
继续阅读 »

15、 说说mvc模式的原理,它在android中的运用,android的官方建议应用程序的开发采用mvc模式。何谓mvc?


mvc是model,view,controller的缩写,mvc包含三个部分:


  模型(model)对象:是应用程序的主体部分,所有的业务逻辑都应该写在该层。


  视图(view)对象:是应用程序中负责生成用户界面的部分。也是在整个mvc架构中用户唯一可以看到的一层,接收用户的输入,显示处理结果。


  控制器(control)对象:是根据用户的输入,控制用户界面数据显示及更新model对象状态的部分,控制器更重要的一种导航功能,响应用户出发的相关事件,交给m层处理。


  android鼓励弱耦合和组件的重用,在android中mvc的具体体现如下:


  1)视图层(view):一般采用xml文件进行界面的描述,使用的时候可以非常方便的引入,当然,如果你对android了解的比较的多了话,就一定可以想到在android中也可以使用JavaScript+html等的方式作为view层,当然这里需要进行java和javascript之间的通信,幸运的是,android提供了它们之间非常方便的通信实现。


  2)控制层(controller):android的控制层的重任通常落在了众多的acitvity的肩上,这句话也就暗含了不要在acitivity中写代码,要通过activity交割model业务逻辑层处理,这样做的另外一个原因是android中的acitivity的响应时间是5s,如果耗时的操作放在这里,程序就很容易被回收掉。


  3)模型层(model):对数据库的操作、对网络等的操作都应该在model里面处理,当然对业务计算等操作也是必须放在的该层的。


16、 什么是ANR 如何避免它?


答:ANR:Application Not Responding。在Android中,活动管理器和窗口管理器这两个系统服务负责监视应用程序的响应,当用户操作的在5s内应用程序没能做出反应,BroadcastReceiver在10秒内没有执行完毕,就会出现应用程序无响应对话框,这既是ANR。


避免方法:Activity应该在它的关键生命周期方法(如onCreate()和onResume())里尽可能少的去做创建操作。潜在的耗时操作,例如网络或数据库操作,或者高耗时的计算如改变位图尺寸,应该在子线程里(或者异步方式)来完成。主线程应该为子线程提供一个Handler,以便完成时能够提交给主线程。


17、 什么情况会导致Force Close ?如何避免?能否捕获导致其的异常?


答:程序出现异常,比如nullpointer。


避免:编写程序时逻辑连贯,思维缜密。能捕获异常,在logcat中能看到异常信息


18、 描述一下android的系统架构


android系统架构分从下往上为linux 内核层、运行库、应用程序框架层、和应用程序层。


linuxkernel:负责硬件的驱动程序、网络、电源、系统安全以及内存管理等功能。


libraries和 android runtime:libraries:即c/c++函数库部分,大多数都是开放源代码的函数库,例如webkit(引擎),该函数库负责 android网页浏览器的运行,例如标准的c函数库libc、openssl、sqlite等,当然也包括支持游戏开发2dsgl和 3dopengles,在多媒体方面有mediaframework框架来支持各种影音和图形文件的播放与显示,例如mpeg4、h.264、mp3、 aac、amr、jpg和png等众多的多媒体文件格式。android的runtime负责解释和执行生成的dalvik格式的字节码。


 applicationframework(应用软件架构),java应用程序开发人员主要是使用该层封装好的api进行快速开发。


  applications:该层是java的应用程序层,android内置的googlemaps、e-mail、即时通信工具、浏览器、mp3播放器等处于该层,java开发人员开发的程序也处于该层,而且和内置的应用程序具有平等的位置,可以调用内置的应用程序,也可以替换内置的应用程序。


  上面的四个层次,下层为上层服务,上层需要下层的支持,调用下层的服务,这种严格分层的方式带来的极大的稳定性、灵活性和可扩展性,使得不同层的开发人员可以按照规范专心特定层的开发。


android应用程序使用框架的api并在框架下运行,这就带来了程序开发的高度一致性,另一方面也告诉我们,要想写出优质高效的程序就必须对整个 applicationframework进行非常深入的理解。精通applicationframework,你就可以真正的理解android的设计和运行机制,也就更能够驾驭整个应用层的开发。


19、 请介绍下ContentProvider是如何实现数据共享的。


一个程序可以通过实现一个Content provider的抽象接口将自己的数据完全暴露出去,而且Content providers是以类似数据库中表的方式将数据暴露。Content providers存储和检索数据,通过它可以让所有的应用程序访问到,这也是应用程序之间唯一共享数据的方法。


要想使应用程序的数据公开化,可通过2种方法:创建一个属于你自己的Content provider或者将你的数据添加到一个已经存在的Content provider中,前提是有相同数据类型并且有写入Content provider的权限。


如何通过一套标准及统一的接口获取其他应用程序暴露的数据?


Android提供了ContentResolver,外界的程序可以通过ContentResolver接口访问ContentProvider提供的数据。


20、 Service和Thread的区别?


答:servie是系统的组件,它由系统进程托管(servicemanager);它们之间的通信类似于client和server,是一种轻量级的ipc通信,这种通信的载体是binder,它是在linux层交换信息的一种ipc。而thread是由本应用程序托管。1). Thread:Thread 是程序执行的最小单元,它是分配CPU的基本单位。可以用 Thread 来执行一些异步的操作。


2). Service:Service 是android的一种机制,当它运行的时候如果是Local Service,那么对应的 Service 是运行在主进程的 main 线程上的。如:onCreate,onStart 这些函数在被系统调用的时候都是在主进程的 main 线程上运行的。如果是Remote Service,那么对应的 Service 则是运行在独立进程的 main 线程上。


既然这样,那么我们为什么要用 Service 呢?其实这跟 android 的系统机制有关,我们先拿 Thread 来说。Thread 的运行是独立于 Activity 的,也就是说当一个 Activity 被 finish 之后,如果你没有主动停止 Thread 或者 Thread 里的 run 方法没有执行完毕的话,Thread 也会一直执行。因此这里会出现一个问题:当 Activity 被 finish 之后,你不再持有该 Thread 的引用。另一方面,你没有办法在不同的 Activity 中对同一 Thread 进行控制。  


举个例子:如果你的 Thread 需要不停地隔一段时间就要连接服务器做某种同步的话,该 Thread 需要在 Activity 没有start的时候也在运行。这个时候当你 start 一个 Activity 就没有办法在该 Activity 里面控制之前创建的 Thread。因此你便需要创建并启动一个 Service ,在 Service 里面创建、运行并控制该 Thread,这样便解决了该问题(因为任何 Activity 都可以控制同一 Service,而系统也只会创建一个对应 Service 的实例)。  


因此你可以把 Service 想象成一种消息服务,而你可以在任何有 Context 的地方调用 Context.startService、Context.stopService、Context.bindService,Context.unbindService,来控制它,你也可以在 Service 里注册 BroadcastReceiver,在其他地方通过发送 broadcast 来控制它,当然这些都是 Thread 做不到的。


21、 Android本身的api并未声明会抛出异常,则其在运行时有无可能抛出runtime异常,你遇到过吗?诺有的话会导致什么问题?如何解决?


答:会,比如nullpointerException。我遇到过,比如textview.setText()时,textview没有初始化。会导致程序无法正常运行出现forceclose。打开控制台查看logcat信息找出异常信息并修改程序。


22、 IntentService有何优点?


答:Acitivity的进程,当处理Intent的时候,会产生一个对应的Service; Android的进程处理器现在会尽可能的不kill掉你;非常容易使用


23、 如果后台的Activity由于某原因被系统回收了,如何在被系统回收之前保存当前状态?


答:重写onSaveInstanceState()方法,在此方法中保存需要保存的数据,该方法将会在activity被回收之前调用。通过重写onRestoreInstanceState()方法可以从中提取保存好的数据


24、 如何将一个Activity设置成窗口的样式。


答:中配置:android :theme="@android:style/Theme.Dialog" 


另外android:theme="@android:style/Theme.Translucent" 是设置透明


25、 如何退出Activity?如何安全退出已调用多个Activity的Application?


答:对于单一Activity的应用来说,退出很简单,直接finish()即可。当然,也可以用killProcess()和System.exit()这样的方法。


对于多个activity,1、记录打开的Activity:每打开一个Activity,就记录下来。在需要退出时,关闭每一个Activity即可。2、发送特定广播:在需要结束应用时,发送一个特定的广播,每个Activity收到广播后,关闭即可。3、递归退出:在打开新的Activity时使用startActivityForResult,然后自己加标志,在onActivityResult中处理,递归关闭。为了编程方便,最好定义一个Activity基类,处理这些共通问题。


在2.1之前,可以使用ActivityManager的restartPackage方法。


它可以直接结束整个应用。在使用时需要权限android.permission.RESTART_PACKAGES。


注意不要被它的名字迷惑。


可是,在2.2,这个方法失效了。在2.2添加了一个新的方法,killBackground Processes(),需要权限 android.permission.KILL_BACKGROUND_PROCESSES。可惜的是,它和2.2的restartPackage一样,根本起不到应有的效果。


另外还有一个方法,就是系统自带的应用程序管理里,强制结束程序的方法,forceStopPackage()。它需要权限android.permission.FORCE_STOP_PACKAGES。并且需要添加android:sharedUserId="android.uid.system"属性。同样可惜的是,该方法是非公开的,他只能运行在系统进程,第三方程序无法调用。


因为需要在Android.mk中添加LOCAL_CERTIFICATE := platform。


而Android.mk是用于在Android源码下编译程序用的。


从以上可以看出,在2.2,没有办法直接结束一个应用,而只能用自己的办法间接办到。


现提供几个方法,供参考:


1、抛异常强制退出:


该方法通过抛异常,使程序Force Close。


验证可以,但是,需要解决的问题是,如何使程序结束掉,而不弹出Force Close的窗口。


2、记录打开的Activity:


每打开一个Activity,就记录下来。在需要退出时,关闭每一个Activity即可。


3、发送特定广播:


在需要结束应用时,发送一个特定的广播,每个Activity收到广播后,关闭即可。


4、递归退出


在打开新的Activity时使用startActivityForResult,然后自己加标志,在onActivityResult中处理,递归关闭。


除了第一个,都是想办法把每一个Activity都结束掉,间接达到目的。但是这样做同样不完美。你会发现,如果自己的应用程序对每一个Activity都设置了nosensor,在两个Activity结束的间隙,sensor可能有效了。但至少,我们的目的达到了,而且没有影响用户使用。为了编程方便,最好定义一个Activity基类,处理这些共通问题。


26、 AIDL的全称是什么?如何工作?能处理哪些类型的数据?


答:全称是:Android Interface Define Language


在Android中, 每个应用程序都可以有自己的进程. 在写UI应用的时候, 经常要用到Service. 在不同的进程中, 怎样传递对象呢?显然, Java中不允许跨进程内存共享. 因此传递对象, 只能把对象拆分成操作系统能理解的简单形式, 以达到跨界对象访问的目的. 在J2EE中,采用RMI的方式, 可以通过序列化传递对象. 在Android中, 则采用AIDL的方式. 理论上AIDL可以传递Bundle,实际上做起来却比较麻烦。


AIDL(AndRoid接口描述语言)是一种借口描述语言; 编译器可以通过aidl文件生成一段代码,通过预先定义的接口达到两个进程内部通信进程的目的. 如果需要在一个Activity中, 访问另一个Service中的某个对象, 需要先将对象转化成AIDL可识别的参数(可能是多个参数), 然后使用AIDL来传递这些参数, 在消息的接收端, 使用这些参数组装成自己需要的对象.


AIDL的IPC的机制和COM或CORBA类似, 是基于接口的,但它是轻量级的。它使用代理类在客户端和实现层间传递值. 如果要使用AIDL, 需要完成2件事情: 1. 引入AIDL的相关类.; 2. 调用aidl产生的class.


AIDL的创建方法:


AIDL语法很简单,可以用来声明一个带一个或多个方法的接口,也可以传递参数和返回值。 由于远程调用的需要, 这些参数和返回值并不是任何类型.下面是些AIDL支持的数据类型:


1. 不需要import声明的简单Java编程语言类型(int,boolean等)


2. String, CharSequence不需要特殊声明


3. List, Map和Parcelables类型, 这些类型内所包含的数据成员也只能是简单数据类型, String等其他比支持的类型.


(另外: 我没尝试Parcelables, 在Eclipse+ADT下编译不过, 或许以后会有所支持)


27、 请解释下Android程序运行时权限与文件系统权限的区别。


答:运行时权限Dalvik( android授权) 


文件系统 linux 内核授权


28、 系统上安装了多种浏览器,能否指定某浏览器访问指定页面?请说明原由。


通过直接发送Uri把参数带过去,或者通过manifest里的intentfilter里的data属性
29、 android系统的优势和不足



答:Android平台手机 5大优势: 


一、开放性 


在优势方面,Android平台首先就是其开发性,开发的平台允许任何移动终端厂商加入到Android联盟中来。显著的开放性可以使其拥有更多的开发者,随着用户和应用的日益丰富,一个崭新的平台也将很快走向成熟。开放性对于Android的发展而言,有利于积累人气,这里的人气包括消费者和厂商,而对于消费者来讲,随大的受益正是丰富的软件资源。开放的平台也会带来更大竞争,如此一来,消费者将可以用更低的价位购得心仪的手机。


二、挣脱运营商的束缚 


在过去很长的一段时间,特别是在欧美地区,手机应用往往受到运营商制约,使用什么功能接入什么网络,几乎都受到运营商的控制。从去年iPhone 上市 ,用户可以更加方便地连接网络,运营商的制约减少。随着EDGE、HSDPA这些2G至3G移动网络的逐步过渡和提升,手机随意接入网络已不是运营商口中的笑谈,当你可以通过手机IM软件方便地进行即时聊天时,再回想不久前天价的彩信和图铃下载业务,是不是像噩梦一样?互联网巨头Google推动的Android终端天生就有网络特色,将让用户离互联网更近。


三、丰富的硬件选择 


这一点还是与Android平台的开放性相关,由于Android的开放性,众多的厂商会推出千奇百怪,功能特色各具的多种产品。功能上的差异和特色,却不会影响到数据同步、甚至软件的兼容,好比你从诺基亚 Symbian风格手机 一下改用苹果 iPhone ,同时还可将Symbian中优秀的软件带到iPhone上使用、联系人等资料更是可以方便地转移,是不是非常方便呢?


四、不受任何限制的开发商 


Android平台提供给第三方开发商一个十分宽泛、自由的环境,不会受到各种条条框框的阻扰,可想而知,会有多少新颖别致的软件会诞生。但也有其两面性,血腥、暴力、情色方面的程序和游戏如可控制正是留给Android难题之一。


五、无缝结合的Google应用 


如今叱诧互联网的Google已经走过10年度历史,从搜索巨人到全面的互联网渗透,Google服务如地图、邮件、搜索等已经成为连接用户和互联网的重要纽带,而Android平台手机将无缝结合这些优秀的Google服务。


再说Android的5大不足:


一、安全和隐私 


由于手机 与互联网的紧密联系,个人隐私很难得到保守。除了上网过程中经意或不经意留下的个人足迹,Google这个巨人也时时站在你的身后,洞穿一切,因此,互联网的深入将会带来新一轮的隐私危机。


二、首先开卖Android手机的不是最大运营商 


众所周知,T-Mobile在23日,于美国纽约发布 了Android首款手机G1。但是在北美市场,最大的两家运营商乃AT&T和Verizon,而目前所知取得Android手机销售权的仅有 T-Mobile和Sprint,其中T-Mobile的3G网络相对于其他三家也要逊色不少,因此,用户可以买账购买G1,能否体验到最佳的3G网络服务则要另当别论了!


三、运营商仍然能够影响到Android手机 


在国内市场,不少用户对购得移动定制机不满,感觉所购的手机被人涂画了广告一般。这样的情况在国外市场同样出现。Android手机的另一发售运营商Sprint就将在其机型中内置其手机商店程序。


四、同类机型用户减少 


在不少手机论坛都会有针对某一型号的子论坛,对一款手机的使用心得交流,并分享软件资源。而对于Android平台手机,由于厂商丰富,产品类型多样,这样使用同一款机型的用户越来越少,缺少统一机型的程序强化。举个稍显不当的例子,现在山寨机泛滥,品种各异,就很少有专门针对某个型号山寨机的讨论和群组,除了哪些功能异常抢眼、颇受追捧的机型以外。


五、过分依赖开发商缺少标准配置 


在使用PC端的Windows Xp系统的时候,都会内置微软Windows Media Player这样一个浏览器程序,用户可以选择更多样的播放器,如Realplay或暴风影音等。但入手开始使用默认的程序同样可以应付多样的需要。在 Android平台中,由于其开放性,软件更多依赖第三方厂商,比如Android系统的SDK中就没有内置音乐 播放器,全部依赖第三方开发,缺少了产品的统一性。


30、 Android dvm的进程和Linux的进程, 应用程序的进程是否为同一个概念 


答:DVM指dalivk的虚拟机。每一个Android应用程序都在它自己的进程中运行,都拥有一个独立的Dalvik虚拟机实例。而每一个DVM都是在Linux 中的一个进程,所以说可以认为是同一个概念。


31、 sim卡的EF文件是什么?有何作用


答:sim卡的文件系统有自己规范,主要是为了和手机通讯,sim本 身可以有自己的操作系统,EF就是作存储并和手机通讯用的


32、 嵌入式操作系统内存管理有哪几种, 各有何特性


  页式,段式,段页,用到了MMU,虚拟空间等技术


33、 什么是嵌入式实时操作系统, Android 操作系统属于实时操作系统吗?


嵌入式实时操作系统是指当外界事件或数据产生时,能够接受并以足够快的速度予以处理,其处理的结果又能在规定的时间之内来控制生产过程或对处理系统作出快速响应,并控制所有实时任务协调一致运行的嵌入式操作系统。主要用于工业控制、 军事设备、 航空航天等领域对系统的响应时间有苛刻的要求,这就需要使用实时系统。又可分为软实时和硬实时两种,而android是基于linux内核的,因此属于软实时。


34、 一条最长的短信息约占多少byte?


  中文70(包括标点),英文160,160个字节。  


35、 如何将SQLite数据库(dictionary.db文件)与apk文件一起发布


解答:可以将dictionary.db文件复制到Eclipse Android工程中的res aw目录中。所有在res aw目录中的文件不会被压缩,这样可以直接提取该目录中的文件。可以将dictionary.db文件复制到res aw目录中


36、 如何将打开res aw目录中的数据库文件?


解答:在Android中不能直接打开res aw目录中的数据库文件,而需要在程序第一次启动时将该文件复制到手机内存或SD卡的某个目录中,然后再打开该数据库文件。


复制的基本方法是使用getResources().openRawResource方法获得res aw目录中资源的 InputStream对象,然后将该InputStream对象中的数据写入其他的目录中相应文件中。在Android SDK中可以使用SQLiteDatabase.openOrCreateDatabase方法来打开任意目录中的SQLite数据库文件。


37、 DDMS和TraceView的区别? 


DDMS是一个程序执行查看器,在里面可以看见线程和堆栈等信息,TraceView是程序性能分析器 。


38、 java中如何引用本地语言 


可以用JNI(java native interface  java 本地接口)接口 。


39、 谈谈Android的IPC(进程间通信)机制 


IPC是内部进程通信的简称, 是共享"命名管道"的资源。Android中的IPC机制是为了让Activity和Service之间可以随时的进行交互,故在Android中该机制,只适用于Activity和Service之间的通信,类似于远程方法调用,类似于C/S模式的访问。通过定义AIDL接口文件来定义IPC接口。Servier端实现IPC接口,Client端调用IPC接口本地代理。


40、 NDK是什么


NDK是一些列工具的集合,NDK提供了一系列的工具,帮助开发者迅速的开发C/C++的动态库,并能自动将so和java 应用打成apk包。


NDK集成了交叉编译器,并提供了相应的mk文件和隔离cpu、平台等的差异,开发人员只需简单的修改mk文件就可以创建出so


作者:优弧同学
链接:https://juejin.cn/post/6844903441039360007
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

Android 面试题及答案

‍‍8、跟activity和Task 有关的 Intent启动方式有哪些?其含义?‍‍ ‍‍核心的Intent Flag有:‍‍  ‍‍FLAG_ACTIVITY_NEW_TASK‍‍  ‍‍FLAG_ACTI...
继续阅读 »

‍‍8、跟activity和Task 有关的 Intent启动方式有哪些?其含义?‍‍


‍‍核心的Intent Flag有:‍‍ 


‍‍FLAG_ACTIVITY_NEW_TASK‍‍ 


‍‍FLAG_ACTIVITY_CLEAR_TOP ‍‍


‍‍FLAG_ACTIVITY_RESET_TASK_IF_NEEDED ‍‍


‍‍FLAG_ACTIVITY_SINGLE_TOP‍‍


‍‍FLAG_ACTIVITY_NEW_TASK‍‍ 


  ‍‍如果设置,这个Activity会成为历史stack中一个新Task的开始。一个Task(从启动它的Activity到下一个Task中的 Activity)定义了用户可以迁移的Activity原子组。Task可以移动到前台和后台;在某个特定Task中的所有Activity总是保持相同的次序。‍‍ 


 ‍‍ 这个标志一般用于呈现“启动”类型的行为:它们提供用户一系列可以单独完成的事情,与启动它们的Activity完全无关。 


 使用这个标志,如果正在启动的Activity的Task已经在运行的话,那么,新的Activity将不会启动;代替的,当前Task会简单的移入前台。参考FLAG_ACTIVITY_MULTIPLE_TASK标志,可以禁用这一行为。 


  这个标志不能用于调用方对已经启动的Activity请求结果。


FLAG_ACTIVITY_CLEAR_TOP 


  如果设置,并且这个Activity已经在当前的Task中运行,因此,不再是重新启动一个这个Activity的实例,而是在这个Activity上方的所有Activity都将关闭,然后这个Intent会作为一个新的Intent投递到老的Activity(现在位于顶端)中。 


  例如,假设一个Task中包含这些Activity:A,B,C,D。如果D调用了startActivity(),并且包含一个指向Activity B的Intent,那么,C和D都将结束,然后B接收到这个Intent,因此,目前stack的状况是:A,B。 


  上例中正在运行的Activity B既可以在onNewIntent()中接收到这个新的Intent,也可以把自己关闭然后重新启动来接收这个Intent。如果它的启动模式声明为 “multiple”(默认值),并且你没有在这个Intent中设置FLAG_ACTIVITY_SINGLE_TOP标志,那么它将关闭然后重新创建;对于其它的启动模式,或者在这个Intent中设置FLAG_ACTIVITY_SINGLE_TOP标志,都将把这个Intent投递到当前这个实例的onNewIntent()中。 


  这个启动模式还可以与FLAG_ACTIVITY_NEW_TASK结合起来使用:用于启动一个Task中的根Activity,它会把那个Task中任何运行的实例带入前台,然后清除它直到根Activity。这非常有用,例如,当从Notification Manager处启动一个Activity。 


FLAG_ACTIVITY_RESET_TASK_IF_NEEDED 


    如果设置这个标志,这个activity不管是从一个新的栈启动还是从已有栈推到栈顶,它都将以the front door of the task的方式启动。这就讲导致任何与应用相关的栈都讲重置到正常状态(不管是正在讲activity移入还是移除),如果需要,或者直接重置该栈为初始状态。


FLAG_ACTIVITY_SINGLE_TOP 


  如果设置,当这个Activity位于历史stack的顶端运行时,不再启动一个新的


FLAG_ACTIVITY_BROUGHT_TO_FRONT 


  这个标志一般不是由程序代码设置的,如在launchMode中设置singleTask模式时系统帮你设定。 


FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET 


  如果设置,这将在Task的Activity stack中设置一个还原点,当Task恢复时,需要清理Activity。也就是说,下一次Task带着 FLAG_ACTIVITY_RESET_TASK_IF_NEEDED标记进入前台时(典型的操作是用户在主画面重启它),这个Activity和它之上的都将关闭,以至于用户不能再返回到它们,但是可以回到之前的Activity。 


  这在你的程序有分割点的时候很有用。例如,一个e-mail应用程序可能有一个操作是查看一个附件,需要启动图片浏览Activity来显示。这个 Activity应该作为e-mail应用程序Task的一部分,因为这是用户在这个Task中触发的操作。然而,当用户离开这个Task,然后从主画面选择e-mail app,我们可能希望回到查看的会话中,但不是查看图片附件,因为这让人困惑。通过在启动图片浏览时设定这个标志,浏览及其它启动的Activity在下次用户返回到mail程序时都将全部清除。 


FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS 


  如果设置,新的Activity不会在最近启动的Activity的列表中保存。 


FLAG_ACTIVITY_FORWARD_RESULT 


  如果设置,并且这个Intent用于从一个存在的Activity启动一个新的Activity,那么,这个作为答复目标的Activity将会传到这个新的Activity中。这种方式下,新的Activity可以调用setResult(int),并且这个结果值将发送给那个作为答复目标的 Activity。 


FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY 


  这个标志一般不由应用程序代码设置,如果这个Activity是从历史记录里启动的(常按HOME键),那么,系统会帮你设定。 


FLAG_ACTIVITY_MULTIPLE_TASK 


  不要使用这个标志,除非你自己实现了应用程序启动器。与FLAG_ACTIVITY_NEW_TASK结合起来使用,可以禁用把已存的Task送入前台的行为。当设置时,新的Task总是会启动来处理Intent,而不管这是是否已经有一个Task可以处理相同的事情。 


  由于默认的系统不包含图形Task管理功能,因此,你不应该使用这个标志,除非你提供给用户一种方式可以返回到已经启动的Task。 


  如果FLAG_ACTIVITY_NEW_TASK标志没有设置,这个标志被忽略。 


FLAG_ACTIVITY_NO_ANIMATION 


  如果在Intent中设置,并传递给Context.startActivity()的话,这个标志将阻止系统进入下一个Activity时应用 Acitivity迁移动画。这并不意味着动画将永不运行——如果另一个Activity在启动显示之前,没有指定这个标志,那么,动画将被应用。这个标志可以很好的用于执行一连串的操作,而动画被看作是更高一级的事件的驱动。 


FLAG_ACTIVITY_NO_HISTORY 


  如果设置,新的Activity将不再历史stack中保留。用户一离开它,这个Activity就关闭了。这也可以通过设置noHistory特性。 


FLAG_ACTIVITY_NO_USER_ACTION 


  如果设置,作为新启动的Activity进入前台时,这个标志将在Activity暂停之前阻止从最前方的Activity回调的onUserLeaveHint()。 


  典型的,一个Activity可以依赖这个回调指明显式的用户动作引起的Activity移出后台。这个回调在Activity的生命周期中标记一个合适的点,并关闭一些Notification。 


  如果一个Activity通过非用户驱动的事件,如来电或闹钟,启动的,这个标志也应该传递给Context.startActivity,保证暂停的Activity不认为用户已经知晓其Notification。 


FLAG_ACTIVITY_PREVIOUS_IS_TOP 


  If set and this intent is being used to launch a new activity from an existing one, the current activity will not be counted as the top activity for deciding whether the new intent should be delivered to the top instead of starting a new one. The previous activity will be used as the top, with the assumption being that the current activity will finish itself immediately. 


FLAG_ACTIVITY_REORDER_TO_FRONT 


  如果在Intent中设置,并传递给Context.startActivity(),这个标志将引发已经运行的Activity移动到历史stack的顶端。 


  例如,假设一个Task由四个Activity组成:A,B,C,D。如果D调用startActivity()来启动Activity B,那么,B会移动到历史stack的顶端,现在的次序变成A,C,D,B。如果FLAG_ACTIVITY_CLEAR_TOP标志也设置的话,那么这个标志将被忽略。 


9、 请描述下Activity的生命周期。


答:activity的生命周期方法有:onCreate()、onStart()、onReStart()、onResume()、onPause()、onStop()、onDestory();


可见生命周期:从onStart()直到系统调用onStop()


前台生命周期:从onResume()直到系统调用onPause()


10、 activity在屏幕旋转时的生命周期


答:不设置Activity的android:configChanges时,切屏会重新调用各个生命周期,切横屏时会执行一次,切竖屏时会执行两次;设置Activity的android:configChanges="orientation"时,切屏还是会重新调用各个生命周期,切横、竖屏时只会执行一次;设置Activity的android:configChanges="orientation|keyboardHidden"时,切屏不会重新调用各个生命周期,只会执行onConfigurationChanged方法


11、 如何启用Service,如何停用Service。


服务的开发比较简单,如下:


第一步:继承Service类


public class SMSService extends Service {}

第二步:在AndroidManifest.xml文件中的节点里对服务进行配置:


服务不能自己运行,需要通过调用Context.startService()或Context.bindService()方法启动服务。这两个方法都可以启动Service,但是它们的使用场合有所不同。使用startService()方法启用服务,调用者与服务之间没有关连,即使调用者退出了,服务仍然运行。使用bindService()方法启用服务,调用者与服务绑定在了一起,调用者一旦退出,服务也就终止,大有“不求同时生,必须同时死”的特点。


如果打算采用Context.startService()方法启动服务,在服务未被创建时,系统会先调用服务的onCreate()方法,接着调用onStart()方法。如果调用startService()方法前服务已经被创建,多次调用startService()方法并不会导致多次创建服务,但会导致多次调用onStart()方法。采用startService()方法启动的服务,只能调用Context.stopService()方法结束服务,服务结束时会调用onDestroy()方法。


如果打算采用Context.bindService()方法启动服务,在服务未被创建时,系统会先调用服务的onCreate()方法,接着调用onBind()方法。这个时候调用者和服务绑定在一起,调用者退出了,系统就会先调用服务的onUnbind()方法,接着调用onDestroy()方法。如果调用bindService()方法前服务已经被绑定,多次调用bindService()方法并不会导致多次创建服务及绑定(也就是说onCreate()和onBind()方法并不会被多次调用)。如果调用者希望与正在绑定的服务解除绑定,可以调用unbindService()方法,调用该方法也会导致系统调用服务的onUnbind()-->onDestroy()方法。


服务常用生命周期回调方法如下: 


onCreate() 该方法在服务被创建时调用,该方法只会被调用一次,无论调用多少次startService()或bindService()方法,服务也只被创建一次。


onDestroy()该方法在服务被终止时调用。


与采用Context.startService()方法启动服务有关的生命周期方法


onStart() 只有采用Context.startService()方法启动服务时才会回调该方法。该方法在服务开始运行时被调用。多次调用startService()方法尽管不会多次创建服务,但onStart() 方法会被多次调用。


与采用Context.bindService()方法启动服务有关的生命周期方法


onBind()只有采用Context.bindService()方法启动服务时才会回调该方法。该方法在调用者与服务绑定时被调用,当调用者与服务已经绑定,多次调用Context.bindService()方法并不会导致该方法被多次调用。


onUnbind()只有采用Context.bindService()方法启动服务时才会回调该方法。该方法在调用者与服务解除绑定时被调用


12、 注册广播有几种方式,这些方式有何优缺点?请谈谈Android引入广播机制的用意。


答:首先写一个类要继承BroadcastReceiver


第一种:在清单文件中声明,添加




   
复制代码

‍‍第二种使用代码进行注册如:‍‍


IntentFilter filter =  new IntentFilter("android.provider.Telephony.SMS_RECEIVED");
IncomingSMSReceiver receiver = new IncomgSMSReceiver();
registerReceiver(receiver.filter);复制代码

两种注册类型的区别是:


1)第一种不是常驻型广播,也就是说广播跟随程序的生命周期。


2)第二种是常驻型,也就是说当应用程序关闭后,如果有信息广播来,程序也会被系统调用自动运行。


13、 请解释下在单线程模型中Message、Handler、Message Queue、Looper之间的关系。


答:简单的说,Handler获取当前线程中的looper对象,looper用来从存放Message的MessageQueue中取出Message,再有Handler进行Message的分发和处理.


Message Queue(消息队列):用来存放通过Handler发布的消息,通常附属于某一个创建它的线程,可以通过Looper.myQueue()得到当前线程的消息队列


Handler:可以发布或者处理一个消息或者操作一个Runnable,通过Handler发布消息,消息将只会发送到与它关联的消息队列,然也只能处理该消息队列中的消息


Looper:是Handler和消息队列之间通讯桥梁,程序组件首先通过Handler把消息传递给Looper,Looper把消息放入队列。Looper也把消息队列里的消息广播给所有的


Handler:Handler接受到消息后调用handleMessage进行处理


Message:消息的类型,在Handler类中的handleMessage方法中得到单个的消息进行处理


在单线程模型下,为了线程通信问题,Android设计了一个Message Queue(消息队列), 线程间可以通过该Message Queue并结合Handler和Looper组件进行信息交换。下面将对它们进行分别介绍:


1. Message 


    Message消息,理解为线程间交流的信息,处理数据后台线程需要更新UI,则发送Message内含一些数据给UI线程。


2. Handler 


    Handler处理者,是Message的主要处理者,负责Message的发送,Message内容的执行处理。后台线程就是通过传进来的 Handler对象引用来sendMessage(Message)。而使用Handler,需要implement 该类的 handleMessage(Message)方法,它是处理这些Message的操作内容,例如Update UI。通常需要子类化Handler来实现handleMessage方法。


3. Message Queue 


    Message Queue消息队列,用来存放通过Handler发布的消息,按照先进先出执行。


    每个message queue都会有一个对应的Handler。Handler会向message queue通过两种方法发送消息:sendMessage或post。这两种消息都会插在message queue队尾并按先进先出执行。但通过这两种方法发送的消息执行的方式略有不同:通过sendMessage发送的是一个message对象,会被 Handler的handleMessage()函数处理;而通过post方法发送的是一个runnable对象,则会自己执行。


4. Looper 


    Looper是每条线程里的Message Queue的管家。Android没有Global的Message Queue,而Android会自动替主线程(UI线程)建立Message Queue,但在子线程里并没有建立Message Queue。所以调用Looper.getMainLooper()得到的主线程的Looper不为NULL,但调用Looper.myLooper() 得到当前线程的Looper就有可能为NULL。对于子线程使用Looper,API Doc提供了正确的使用方法:这个Message机制的大概流程:


    1. 在Looper.loop()方法运行开始后,循环地按照接收顺序取出Message Queue里面的非NULL的Message。


    2. 一开始Message Queue里面的Message都是NULL的。当Handler.sendMessage(Message)到Message Queue,该函数里面设置了那个Message对象的target属性是当前的Handler对象。随后Looper取出了那个Message,则调用 该Message的target指向的Hander的dispatchMessage函数对Message进行处理。在dispatchMessage方法里,如何处理Message则由用户指定,三个判断,优先级从高到低:


    1) Message里面的Callback,一个实现了Runnable接口的对象,其中run函数做处理工作;


    2) Handler里面的mCallback指向的一个实现了Callback接口的对象,由其handleMessage进行处理;


    3) 处理消息Handler对象对应的类继承并实现了其中handleMessage函数,通过这个实现的handleMessage函数处理消息。


    由此可见,我们实现的handleMessage方法是优先级最低的!


    3. Handler处理完该Message (update UI) 后,Looper则设置该Message为NULL,以便回收!


    在网上有很多文章讲述主线程和其他子线程如何交互,传送信息,最终谁来执行处理信息之类的,个人理解是最简单的方法——判断Handler对象里面的Looper对象是属于哪条线程的,则由该线程来执行! 


    1. 当Handler对象的构造函数的参数为空,则为当前所在线程的Looper; 


2. Looper.getMainLooper()得到的是主线程的Looper对象,Looper.myLooper()得到的是当前线程的Looper对象。


14、 简要解释一下activity、 intent 、intent filter、service、Broadcase、BroadcaseReceiver


答:一个activity呈现了一个用户可以操作的可视化用户界面;一个service不包含可见的用户界面,而是在后台运行,可以与一个activity绑定,通过绑定暴露出来接口并与其进行通信;一个broadcast receiver是一个接收广播消息并做出回应的component,broadcast receiver没有界面;一个intent是一个Intent对象,它保存了消息的内容。对于activity和service来说,它指定了请求的操作名称和待操作数据的URI,Intent对象可以显式的指定一个目标component。如果这样的话,android会找到这个component(基于manifest文件中的声明)并激活它。但如果一个目标不是显式指定的,android必须找到响应intent的最佳component。它是通过将Intent对象和目标的intent filter相比较来完成这一工作的;一个component的intent filter告诉android该component能处理的intent。intent filter也是在manifest文件中声明的。


作者:优弧同学
链接:https://juejin.cn/post/6844903441039360007
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

Android 面试题及答案

1、 Android的四大组件是哪些,它们的作用? 答:Activity:Activity是Android程序与用户交互的窗口,是Android构造块中最基本的一种,它需要为保持各界面的状态,做很多持久化的事情,妥善管理生命周期以及一些跳转逻辑 service...
继续阅读 »

1、 Android的四大组件是哪些,它们的作用?


答:Activity:Activity是Android程序与用户交互的窗口,是Android构造块中最基本的一种,它需要为保持各界面的状态,做很多持久化的事情,妥善管理生命周期以及一些跳转逻辑


service:后台服务于Activity,封装有一个完整的功能逻辑实现,接受上层指令,完成相关的事物,定义好需要接受的Intent提供同步和异步的接口


Content Provider:是Android提供的第三方应用数据的访问方案,可以派生Content Provider类,对外提供数据,可以像数据库一样进行选择排序,屏蔽内部数据的存储细节,向外提供统一的借口模型,大大简化上层应用,对数据的整合提供了更方便的途径


BroadCast Receiver:接受一种或者多种Intent作触发事件,接受相关消息,做一些简单处理,转换成一条Notification,统一了Android的事件广播模型


2、 请介绍下Android中常用的五种布局。


常用五种布局方式,分别是:FrameLayout(框架布局),LinearLayout (线性布局),AbsoluteLayout(绝对布局),RelativeLayout(相对布局),TableLayout(表格布局)。


一、FrameLayout:所有东西依次都放在左上角,会重叠,这个布局比较简单,也只能放一点比较简单的东西。二、LinearLayout:线性布局,每一个LinearLayout里面又可分为垂直布局(android:orientation="vertical")和水平布局(android:orientation="horizontal" )。当垂直布局时,每一行就只有一个元素,多个元素依次垂直往下;水平布局时,只有一行,每一个元素依次向右排列。三、AbsoluteLayout:绝对布局用X,Y坐标来指定元素的位置,这种布局方式也比较简单,但是在屏幕旋转时,往往会出问题,而且多个元素的时候,计算比较麻烦。四、RelativeLayout:相对布局可以理解为某一个元素为参照物,来定位的布局方式。主要属性有:相对于某一个元素android:layout_below、      android:layout_toLeftOf相对于父元素的地方android:layout_alignParentLeft、android:layout_alignParentRigh;五、TableLayout:表格布局,每一个TableLayout里面有表格行TableRow,TableRow里面可以具体定义每一个元素。每一个布局都有自己适合的方式,这五个布局元素可以相互嵌套应用,做出美观的界面。


3、 android中的动画有哪几类,它们的特点和区别是什么  


答:两种,一种是Tween动画、还有一种是Frame动画。Tween动画,这种实现方式可以使视图组件移动、放大、缩小以及产生透明度的变化;另一种Frame动画,传统的动画方法,通过顺序的播放排列好的图片来实现,类似电影。


4、 android 中有哪几种解析xml的类?官方推荐哪种?以及它们的原理和区别。


答:XML解析主要有三种方式,SAX、DOM、PULL。常规在PC上开发我们使用Dom相对轻松些,但一些性能敏感的数据库或手机上还是主要采用SAX方式,SAX读取是单向的,优点:不占内存空间、解析属性方便,但缺点就是对于套嵌多个分支来说处理不是很方便。而DOM方式会把整个XML文件加载到内存中去,这里Android开发网提醒大家该方法在查找方面可以和XPath很好的结合如果数据量不是很大推荐使用,而PULL常常用在J2ME对于节点处理比较好,类似SAX方式,同样很节省内存,在J2ME中我们经常使用的KXML库来解析。


5、 ListView的优化方案


答:1、如果自定义适配器,那么在getView方法中要考虑方法传进来的参数contentView是否为null,如果为null就创建contentView并返回,如果不为null则直接使用。在这个方法中尽可能少创建view。


  2、给contentView设置tag(setTag()),传入一个viewHolder对象,用于缓存要显示的数据,可以达到图像数据异步加载的效果。


  3、如果listview需要显示的item很多,就要考虑分页加载。比如一共要显示100条或者更多的时候,我们可以考虑先加载20条,等用户拉到列表底部的时候再去加载接下来的20条。


6、 请介绍下Android的数据存储方式。


答:使用SharedPreferences存储数据;文件存储数据;SQLite数据库存储数据;使用ContentProvider存储数据;网络存储数据;


Preference,File, DataBase这三种方式分别对应的目录是/data/data/Package Name/Shared_Pref, /data/data/Package Name/files, /data/data/Package Name/database 。


一:使用SharedPreferences存储数据


首先说明SharedPreferences存储方式,它是 Android提供的用来存储一些简单配置信息的一种机制,例如:登录用户的用户名与密码。其采用了Map数据结构来存储数据,以键值的方式存储,可以简单的读取与写入,具体实例如下:


void ReadSharedPreferences(){
String strName,strPassword;
SharedPreferences   user = getSharedPreferences(“user_info”,0);
strName = user.getString(“NAME”,””);
strPassword = user getString(“PASSWORD”,””);
}
void WriteSharedPreferences(String strName,String strPassword){
SharedPreferences   user = getSharedPreferences(“user_info”,0);
uer.edit();
user.putString(“NAME”, strName);
user.putString(“PASSWORD” ,strPassword);
user.commit();
}复制代码

‍‍数据读取与写入的方法都非常简单,只是在写入的时候有些区别:先调用edit()使其处于编辑状态,然后才能修改数据,最后使用commit()提交修改的数据。实际上SharedPreferences是采用了XML格式将数据存储到设备中,在DDMS中的File Explorer中的/data/data//shares_prefs下。使用SharedPreferences是有些限制的:只能在同一个包内使用,不能在不同的包之间使用。‍‍


‍‍二:文件存储数据‍‍


‍‍文件存储方式是一种较常用的方法,在Android中读取/写入文件的方法,与 Java中实现I/O的程序是完全一样的,提供了openFileInput()和openFileOutput()方法来读取设备上的文件。具体实例如下:‍‍


String fn = “moandroid.log”;
FileInputStream fis = openFileInput
(fn);
FileOutputStream fos = openFileOutput
(fn,Context.MODE_PRIVATE);复制代码

‍‍三:网络存储数据‍‍


‍‍网络存储方式,需要与Android 网络数据包打交道,关于Android 网络数据包的详细说明,请阅读Android SDK引用了Java SDK的哪些package?。‍‍


‍‍四:ContentProvider‍‍


‍‍1、ContentProvider简介‍‍


‍‍当应用继承ContentProvider类,并重写该类用于提供数据和存储数据的方法,就可以向其他应用共享其数据。虽然使用其他方法也可以对外共享数据,但数据访问方式会因数据存储的方式而不同,如:采用文件方式对外共享数据,需要进行文件操作读写数据;采用sharedpreferences共享数据,需要使用sharedpreferences API读写数据。而使用ContentProvider共享数据的好处是统一了数据访问方式。‍‍


‍‍2、Uri类简介‍‍


‍‍Uri代表了要操作的数据,Uri主要包含了两部分信息:1.需要操作的ContentProvider ,2.对ContentProvider中的什么数据进行操作,一个Uri由以下几部分组成:‍‍


‍‍1.scheme:ContentProvider(内容提供者)的scheme已经由Android所规定为:content://…‍‍


‍‍2.主机名(或Authority):用于唯一标识这个ContentProvider,外部调用者可以根据这个标识来找到它。‍‍


‍‍3.路径(path):可以用来表示我们要操作的数据,路径的构建应根据业务而定,如下:‍‍


要操作contact表中id为10的记录,可以构建这样的路径:/contact/10


要操作contact表中id为10的记录的name字段, contact/10/name


要操作contact表中的所有记录,可以构建这样的路径:/contact?


要操作的数据不一定来自数据库,也可以是文件等他存储方式,如下:


要操作xml文件中contact节点下的name节点,可以构建这样的路径:/contact/name


如果要把一个字符串转换成Uri,可以使用Uri类中的parse()方法,如下:


Uri uri = Uri.parse("content://com.changcheng.provider.contactprovider/contact")


3、UriMatcher、ContentUrist和ContentResolver简介


因为Uri代表了要操作的数据,所以我们很经常需要解析Uri,并从 Uri中获取数据。Android系统提供了两个用于操作Uri的工具类,分别为UriMatcher 和ContentUris 。掌握它们的使用,会便于我们的开发工作。


UriMatcher:用于匹配Uri,它的用法如下:


‍‍1.首先把你需要匹配Uri路径全部给注册上,如下:‍‍


//常量UriMatcher.NO_MATCH表示不匹配任何路径的返回码(-1)。
UriMatcher uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
//如果match()方法匹配content://com.changcheng.sqlite.provider.contactprovider /contact路径,返回匹配码为1
uriMatcher.addURI(“com.changcheng.sqlite.provider.contactprovider”, “contact”, 1);//添加需要匹配uri,如果匹配就会返回匹配码
//如果match()方法匹配 content://com.changcheng.sqlite.provider.contactprovider/contact/230路径,返回匹配码为2
uriMatcher.addURI(“com.changcheng.sqlite.provider.contactprovider”, “contact/#”, 2);//#号为通配符复制代码

‍‍2.注册完需要匹配的Uri后,就可以使用uriMatcher.match(uri)方法对输入的Uri进行匹配,如果匹配就返回匹配码,匹配码是调用 addURI()方法传入的第三个参数,假设匹配 content://com.changcheng.sqlite.provider.contactprovider/contact路径,返回的匹配码为1。‍‍


‍‍ContentUris:用于获取Uri路径后面的ID部分,它有两个比较实用的方法:‍‍


‍‍withAppendedId(uri, id)用于为路径加上ID部分‍‍


‍‍parseId(uri)方法用于从路径中获取ID部分‍‍


‍‍ContentResolver:当外部应用需要对ContentProvider中的数据进行添加、删除、修改和查询操作时,可以使用 ContentResolver 类来完成,要获取ContentResolver 对象,可以使用Activity提供的getContentResolver()方法。 ContentResolver使用insert、delete、update、query方法,来操作数据。‍‍


‍‍7、activity的启动模式有哪些?是什么含义?‍‍


‍‍答:在android里,有4种activity的启动模式,分别为: ‍‍


‍‍“standard” (默认) ‍‍


‍‍“singleTop” ‍‍


‍‍“singleTask” ‍‍


‍‍“singleInstance”


‍‍它们主要有如下不同:‍‍


‍‍1. 如何决定所属task ‍‍


‍‍“standard”和”singleTop”的activity的目标task,和收到的Intent的发送者在同一个task内,除非intent包括参数FLAG_ACTIVITY_NEW_TASK。‍‍ 


‍‍如果提供了FLAG_ACTIVITY_NEW_TASK参数,会启动到别的task里。 ‍‍


‍‍“singleTask”和”singleInstance”总是把activity作为一个task的根元素,他们不会被启动到一个其他task里。‍‍


‍‍2. 是否允许多个实例 ‍‍


‍‍“standard”和”singleTop”可以被实例化多次,并且存在于不同的task中,且一个task可以包括一个activity的多个实例; ‍‍


‍‍“singleTask”和”singleInstance”则限制只生成一个实例,并且是task的根元素。 singleTop要求如果创建intent的时候栈顶已经有要创建的Activity的实例,则将intent发送给该实例,而不发送给新的实例。‍‍


‍‍3. 是否允许其它activity存在于本task内 ‍‍


‍‍“singleInstance”独占一个task,其它activity不能存在那个task里;如果它启动了一个新的activity,不管新的activity的launch mode 如何,新的activity都将会到别的task里运行(如同加了FLAG_ACTIVITY_NEW_TASK参数)。 ‍‍


‍‍而另外三种模式,则可以和其它activity共存。‍‍


‍‍4. 是否每次都生成新实例‍‍ 


‍‍“standard”对于没一个启动Intent都会生成一个activity的新实例; ‍‍


‍‍“singleTop”的activity如果在task的栈顶的话,则不生成新的该activity的实例,直接使用栈顶的实例,否则,生成该activity的实例。 ‍‍


‍‍比如现在task栈元素为A-B-C-D(D在栈顶),这时候给D发一个启动intent,如果D是 “standard”的,则生成D的一个新实例,栈变为A-B-C-D-D。‍‍ 


‍‍如果D是singleTop的话,则不会生产D的新实例,栈状态仍为A-B-C-D ‍‍


‍‍如果这时候给B发Intent的话,不管B的launchmode是”standard” 还是 “singleTop” ,都会生成B的新实例,栈状态变为A-B-C-D-B。‍‍


‍‍“singleInstance”是其所在栈的唯一activity,它会每次都被重用。‍‍


‍‍“singleTask”如果在栈顶,则接受intent,否则,该intent会被丢弃,但是该task仍会回到前台。‍‍


‍‍当已经存在的activity实例处理新的intent时候,会调用onNewIntent()方法 如果收到intent生成一个activity实例,那么用户可以通过back键回到上一个状态;如果是已经存在的一个activity来处理这个intent的话,用户不能通过按back键返回到这之前的状态。


作者:优弧同学
链接:https://juejin.cn/post/6844903441039360007
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

为数不多的人知道的 Kotlin 技巧

Google 引入 Kotlin 的目的就是为了让 Android 开发更加方便,自从官宣 Kotlin 成为了 Android 开发的首选语言之后,已经有越来越多的团队,在项目使用 Kotlin。众所周知 xml 十分耗时,因此在 Android 10.0 ...
继续阅读 »

Google 引入 Kotlin 的目的就是为了让 Android 开发更加方便,自从官宣 Kotlin 成为了 Android 开发的首选语言之后,已经有越来越多的团队,在项目使用 Kotlin。

众所周知 xml 十分耗时,因此在 Android 10.0 上新增加 tryInflatePrecompiled 方法,这是一个在编译期运行的一个优化,因为布局文件越复杂 XmlPullParser 解析 XML 越耗时, tryInflatePrecompiled 方法根据 XML 预编译生成 compiled_view.dex, 然后通过反射来生成对应的 View,从而减少 XmlPullParser 解析 XML 的时间,但是目前一直处于禁用状态。源码解析请查看 Android 资源加载源码分析一

因此一些体量比较大的应用,为了极致的优化,缩短一点时间,对于简单的布局,会使用 Kotlin 去重写这部分 UI,但是门槛还是很高,随着 Jetpack Compose 的出现,其目的是让您更快、更轻松地构建原生 Android 应用,前不久 Google 正式发布了 Jetpack Compose 1.0。

Kotlin 优势已经体现在了方方面面,结合着 Kotlin 的高级函数的特性可以让代码可读性更强,更加简洁,但是如果使用不当会对性能造成一些损耗,更多内容可前往查看。

以上两篇文章,主要分享了 Kotlin 在实际项目中使用的技巧,以及如果使用不当会对 性能 和 内存 造成的那些影响以及如何规避这些问题等等。

通过这篇文章你将学习到以下内容:

  • 什么是 Contract,以及如何使用?
  • Kotlin 注解在项目中的使用?
  • 一行代码接受 Activity 或者 Fragment 传递的参数?
  • 一行代码实现 Activity 之间传递参数?
  • 一行代码实现 Fragment 之间传递参数?
  • 一行代码实现点击事件,避免内存泄露?

KtKit 仓库

这篇文章主要围绕一个新库 KtKit 来介绍一些 Kotlin 技巧,正如其名 KtKit 是用 Kotlin 语言编写的工具库,包含了项目中常用的一系列工具,是 Jetpack ktx 系列的补充,涉及到了很多从 Kotlin 源码、Jetpack ktx、anko 等等知名的开源项目中学习到的技巧,包含了 Kotlin 委托属性、高阶函数、扩展函数、内联、注解的使用等等。

如果想要使用文中的 API 需要将下列代码添加在模块级 build.gradle 文件内, 最新版本号请查看 版本记录

implementation "com.hi-dhl:ktkit:${ktkitVersion}"

因为篇幅原因,文章中不会过多的涉及源码分析,源码部分将会在后续的文章中分享。

什么是 Contract,以及如何使用

众所周知 Kotlin 是比较智能的,比如 smart cast 特性,但是在有些情况下显得很笨拙,并不是那么智能,如下所示。

public inline fun String?.isNotNullOrEmpty(): Boolean {
return this != null && !this.trim().equals("null", true) && this.trim().isNotEmpty()
}

fun testString(name: String?) {
if (name.isNotNullOrEmpty()) {
println(name.length) // 1
}
}

正如你所见,只有字符串 name 不为空时,才会进入注释 1 的地方,但是以上代码却无法正常编译,如下图所示。

编译器会告诉你一个编译错误,经过代码分析只有当字符串 name 不为空时,才会进入注释 1 的地方,但是编译器却无法正常推断出来,真的是编译器做不到吗?看看官方文档是如何解释的。

However, as soon as these checks are extracted in a separate function, all the smartcasts immediately disappear:

将检查提取到一个函数中, smart cast 所带来的效果都会消失

编译器无法深入分析每一个函数,原因在于实际开发中我们可能写出更加复杂的代码,而 Kotlin 编译器进行了大量的静态分析,如果编译器去分析每一个函数,需要花费时间分析上下文,增加它的编译耗时的时间。

如果要解决上诉问题,这就需要用到 Contract 特性,Contract 是 Kotlin 提供的非常有用的特性,Contract 的作用就是当 Kotlin 编译器没有足够的信息去分析函数的情况的时候,Contracts 可以为函数提供附加信息,帮助 Kotlin 编译器去分析函数的情况,修改代码如下所示。

inline fun String?.isNotNullOrEmpty(): Boolean {
contract {
returns(true) implies (this@isNotNullOrEmpty != null)
}

return this != null && !this.trim().equals("null", true) && this.trim().isNotEmpty()
}

fun testString(name: String?) {
if (name != null && name.isNotNullOrEmpty()) {
println(name.length) // 1
}
}

相比于之前的代码,在 isNotNullOrEmpty() 函数中添加了 contract 代码块即可正常编译通过,这行代码的意思就是,如果返回值是 true ,this 所指向对象就不为 null。 而在 Kotlin 标准库中大量的用到 contract 特性。 上述示例的使用可前往查看 KtKit/ProfileActivity.kt

Kotlin 注解在项目中的使用

contract 是 Kotlin 1.3 添加的实验性的 API,如果我们调用实验性的 API 需要添加 @ExperimentalContracts 注解才可以正常使用,但是如果添加 @ExperimentalContracts 注解,所有调用这个方法的地方都需要添加注解,如果想要解决这个问题。只需要在声明 contract 文件中的第一行添加以下代码即可。

@file:OptIn(ExperimentalContracts::class)

在上述示例中使用了 inline 修饰符,但是编译器会有一个黄色警告,如下图所示。

编译器建议我们将函数作为参数时使用 Inline,Inline (内联函数) 的作用:提升运行效率,调用被 inline 修饰符的函数,会将方法内的代码段放到调用处。

既然 Inline 修饰符可以提升运行效率,为什么还给出警告,因为 Inline 修饰符的滥用会带来性能损失,更多内容前往查看 Inline 修饰符带来的性能损失

Inline 修饰符常用于下面的情况,编译器才不会有警告:

  • 将函数作为参数(例如:lambda 表达式)
  • 结合 reified 实化类型参数一起使用

但是在普通的方法中,使用 Inline 修饰符,编译会给出警告,如果方法体的代码段很短,想要通过 Inline 修饰符提升性能(虽然微乎其微),可以在文件的第一行添加下列代码,可消除警告。

@file:Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER")

然后在使用 Inline 修饰符的地方添加以下注解,即可愉快的使用。

@kotlin.internal.InlineOnly

注解 @kotlin.internal.InlineOnly 的作用:

  • 消除编译器的警告
  • 修改内联函数的可见性,在编译时修改成 private
// 未添加 InlineOnly 编译后的代码
public static final void showShortToast(@NotNull Context $this$showShortToast, @NotNull String message) {
......
Toast.makeText($this$showShortToast, (CharSequence)message, 0).show();
}


// 添加 InlineOnly 编译后的代码
@InlineOnly
private static final void showShortToast(Context $this$showShortToast, String message) {
......
Toast.makeText($this$showShortToast, (CharSequence)message, 0).show();
}

关于注解完整的使用案例,可前往仓库 KtKit 查看。

一行代码接受 Activity 或者 Fragment 传递的参数

如果想要实现一行代码接受 Activity 或者 Fragment 传递的参数,可以通过 Kotlin 委托属性来实现,在仓库 KtKit 中提供了两个 API,根据实际情况使用即可。案例可前往查看 KtKit/ProfileActivity.kt

class ProfileActivity : Activity() {
// 方式一: 不带默认值
private val userPassword by intent<String>(KEY_USER_PASSWORD)

// 方式二:带默认值:如果获取失败,返回一个默认值
private val userName by intent<String>(KEY_USER_NAME) { "公众号:ByteCode" }
}

一行代码实现 Activity 之间传递参数

这个思路是参考了 anko 的实现,同样是提供了两个 API , 根据实际情况使用即可,可以传递 Android 支持的任意参数,案例可前往查看 KtKit/ProfileActivity.kt

// API:
activity.startActivity<ProfileActivity> { arrayOf( KEY_USER_NAME to "ByteCode" ) }
activity.startActivity<ProfileActivity>( KEY_USER_NAME to "ByteCode" )

// Example:
class ProfileActivity : Activity() {
......
companion object {
......

// 方式一
activity.startActivity<ProfileActivity> {
arrayOf(
KEY_USER_NAME to "ByteCode",
KEY_USER_PASSWORD to "1024"
)
}

// 方式二
activity.startActivity<ProfileActivity>(
KEY_USER_NAME to "ByteCode",
KEY_USER_PASSWORD to "1024"
)
}
}

Activity 之间传递参数 和 并回传结果

// 方式一
context.startActivityForResult<ProfileActivity>(KEY_REQUEST_CODE,
KEY_USER_NAME to "ByteCode",
KEY_USER_PASSWORD to "1024"
)

// 方式二
context.startActivityForResult<ProfileActivity>(KEY_REQUEST_CODE) {
arrayOf(
KEY_USER_NAME to "ByteCode",
KEY_USER_PASSWORD to "1024"
)
}

回传结果

// 方式一
setActivityResult(Activity.RESULT_OK) {
arrayOf(
KEY_RESULT to "success",
KEY_USER_NAME to "ByteCode"
)
}

// 方式二
setActivityResult(
Activity.RESULT_OK,
KEY_RESULT to "success",
KEY_USER_NAME to "ByteCode"
)

一行代码实现 Fragment 之间传递参数

和 Activity 一样提供了两个 API 根据实际情况使用即可,可以传递 Android 支持的任意参数,源码前往查看 KtKit/LoginFragment.kt

// API: 
LoginFragment().makeBundle( KEY_USER_NAME to "ByteCode" )
LoginFragment().makeBundle { arrayOf( KEY_USER_NAME to "ByteCode" ) }

// Example:
class LoginFragment : Fragment(R.layout.fragment_login) {
......
companion object {
......
// 方式一
fun newInstance1(): Fragment {
return LoginFragment().makeBundle(
KEY_USER_NAME to "ByteCode",
KEY_USER_PASSWORD to "1024"
)
}

// 方式二
fun newInstance2(): Fragment {
return LoginFragment().makeBundle {
arrayOf(
KEY_USER_NAME to "ByteCode",
KEY_USER_PASSWORD to "1024"
)
}
}
}
}

一行代码实现点击事件,避免内存泄露

KtKit 提供了常用的三个 API:单击事件、延迟第一次点击事件、防止多次点击

单击事件

view.click(lifecycleScope) { showShortToast("公众号:ByteCode" }

延迟第一次点击事件

// 默认延迟时间是 500ms
view.clickDelayed(lifecycleScope){ showShortToast("公众号:ByteCode" }

// or
view.clickDelayed(lifecycleScope, 1000){ showShortToast("公众号:ByteCode") }

防止多次点击

// 默认间隔时间是 500ms
view.clickTrigger(lifecycleScope){ showShortToast("公众号:ByteCode") }

// or
view.clickTrigger(lifecycleScope, 1000){ showShortToast("公众号:ByteCode") }

但是 View#setOnClickListener 造成的内存泄露,如果做过性能优化的同学应该会见到很多这种 case。

根本原因在于不规范的使用,在做业务开发的时候,根本不会关注这些,那么如何避免这个问题呢,Kotlin Flow 提供了一个非常有用的 API callbackFlow,源码如下所示。

fun View.clickFlow(): Flow<View> {
return callbackFlow {
setOnClickListener {
safeOffer(it)
}
awaitClose { setOnClickListener(null) }
}
}

callbackFlow 正如其名将一个 callback 转换成 flow,awaitClose 会在 flow 结束时执行。

那么 flow 什么时候结束执行

源码中我将 Flow 通过 lifecycleScope 与 Activity / Fragment 的生命周期绑定在一起,在 Activity / Fragment 生命周期结束时,会结束 flow , flow 结束时会将 Listener 置为 null,有效的避免内存泄漏,源码如下所示。

inline fun View.click(lifecycle: LifecycleCoroutineScope, noinline onClick: (view: View) -> Unit) {
clickFlow().onEach {
onClick(this)
}.launchIn(lifecycle)
}

收起阅读 »

iOS研发助手DoraemonKit技术实现(一)

一、前言一个比较成熟的App,经历了多个版本的迭代之后,为了方便调式和测试,往往会积累一些工具来应付这些场景。最近我们组就开源了一款适用于iOS App线下开发、测试、验收阶段,内置在App中的工具集合。使用DoraemonKit,你无需连接电脑,就可以对于A...
继续阅读 »

一、前言

一个比较成熟的App,经历了多个版本的迭代之后,为了方便调式和测试,往往会积累一些工具来应付这些场景。最近我们组就开源了一款适用于iOS App线下开发、测试、验收阶段,内置在App中的工具集合。使用DoraemonKit,你无需连接电脑,就可以对于App的信息进行快速的查看和修改。一键接入、使用方便,提高开发、测试、视觉同学的工作效率,提高我们App上线的完整度和稳定性。

目前DoraemonKit拥有的功能大概分为以下几点:

  1. 常用工具 : App信息展示,沙盒浏览、MockGPS、H5任意门、子线程UI检查、日志显示。
  2. 性能工具 : 帧率监控、CPU监控、内存监控、流量监控、自定义监控。
  3. 视觉工具 : 颜色吸管、组件检查、对齐标尺。
  4. 业务专区 : 支持业务测试组件接入到DoraemonKit面板中。

拿我们App接入效果如下:




面两行是业务线自定义的工具,接入方可以自定义。除此之外都是内置工具集合。

因为里面功能比较多,大概会分三篇文章介绍DoraemonKit的使用和技术实现,这是第一篇主要介绍常用工具集中的几款工具实现。

二、技术实现

2.1:App信息展示




我们要看一些手机信息或者App的一些基本信息的时候,需要到系统设置去找,比较麻烦。特别是权限信息,在我们app装的比较多的时候,我们很难快速找到我们app的权限信息。而这些信息从代码角度都是比较容易获取的。我们把我们感兴趣的信息列表出来直接查看,避免了去手机设置里查看或者查看源代码的麻烦。

获取手机型号

我们从手机设置里面是找不到我们的手机具体是哪一款的文字表述的,比如我的手机是iphone8 Pro,在手机型号里面显示的是MQ8E2CH/A。对于iPhone不熟悉的人很难从外表对iphone进行区分。而手机型号,我们从代码角度就很好获取。


+ (NSString *)iphoneType{
struct utsname systemInfo;
uname(&systemInfo);
NSString *platform = [NSString stringWithCString:systemInfo.machine encoding:NSUTF8StringEncoding];

//iPhone
if ([platform isEqualToString:@"iPhone1,1"]) return @"iPhone 1G";
...
//其他对应关系请看下面对应表
return platform;
}

iPhone设备类型与通用手机类型一一对应关系表

设备类型通用类型
iPhone1,1iPhone 1G
iPhone1,2iPhone 3G
iPhone2,1iPhone 3GS
iPhone3,1iPhone 4
iPhone3,2iPhone 4
iPhone4,1iPhone 4S
iPhone5,1iPhone 5
iPhone5,2iPhone 5
iPhone5,3iPhone 5C
iPhone5,4iPhone 5C
iPhone6,1iPhone 5S
iPhone6,2iPhone 5S
iPhone7,1iPhone 6 Plus
iPhone7,2iPhone 6
iPhone8,1iPhone 6S
iPhone8,2iPhone 6S Plus
iPhone8,4iPhone SE
iPhone9,1iPhone 7
iPhone9,3iPhone 7
iPhone9,2iPhone 7 Plus
iPhone9,4iPhone 7 Plus
iPhone10,1iPhone 8
iPhone10.4iPhone 8
iPhone10,2iPhone 8 Plus
iPhone10,5iPhone 8 Plus
iPhone10,3iPhone X
iPhone10,6iPhone X
iPhone11,8iPhone XR
iPhone11,2iPhone XS
iPhone11,4iPhone XS Max
Phone11,6iPhone XS Max

获取手机系统版本

//获取手机系统版本
NSString *phoneVersion = [[UIDevice currentDevice] systemVersion];

获取App BundleId

一个app分为测试版本、企业版本、appStore发售版本,每一个app长得都一样,如何对他们进行区分呢,那就要用到BundleId这个属性了。


//获取bundle id
NSString *bundleId = [[NSBundle mainBundle] bundleIdentifier];

获取App 版本号


//获取App版本号
NSString *bundleVersionCode = [[[NSBundle mainBundle]infoDictionary] objectForKey:@"CFBundleVersion"];

权限信息查看


当我们发现App运行不正常,比如无法定位,网络一直失败,无法收到推送信息等问题的时候,我们第一个反应就是去手机设置里面去看我们app相关的权限有没有打开。DoraemonKit集成了对于地理位置权限、网络权限、推送权限、相机权限、麦克风权限、相册权限、通讯录权限、日历权限、提醒事项权限的查询。

由于代码比较多,这里就不一一贴出来了。大家可以去DorameonKit/Core/Plugin/AppInfo中自己去查看。这里讲一下,权限查询结果几个值的意义。

  • NotDetermined => 用户还没有选择。
  • Restricted => 该权限受限,比如家长控制。
  • Denied => 用户拒绝使用该权限。
  • Authorized => 用户同意使用该权限。

2.2:沙盒浏览




以前如果我们要去查看App缓存、日志信息,都需要访问沙盒。由于iOS的封闭性,我们无法直接查看沙盒中的文件内容。如果我们要去访问沙盒,基本上有两种方式,第一种使用Xcode自带的工具,从Windows-->Devices进入设备管理界面,通过Download Container的方式导出整个app的沙盒。第二种方式,就是自己写代码,访问沙盒中指定文件,然后使用NSLog的方式打印出来。这两种方式都比较麻烦。

DoraemonKit给出的解决方案:就是自己做一个简单的文件浏览器,通过NSFileManager对象对沙盒文件进行遍历,同时支持对于文件和文件夹的删除操作。对于文件支持本地预览或者通过airdrop的方式或者其他分享方式发送到PC端进行更加细致的操作。

怎么用NSFileManager对象遍历文件和删除文件这里就不说了,大家可以参考DorameonKit/Core/Plugin/Sanbox中的代码。这里讲一下:如何将手机中的文件快速上传到Mac端?刚开始我们还绕了一点路,我们在手机端搭了一个微服务,mac通过浏览器去访问它。后来和同事聊天的时候知道了UIActivityViewController这个类,可以十分便捷地吊起系统分享组件或者是其他注册到系统分享组件中的分享方式,比如微信、钉钉。实现代码非常简单,如下所示:


- (void)shareFileWithPath:(NSString *)filePath{

NSURL *url = [NSURL fileURLWithPath:filePath];
NSArray *objectsToShare = @[url];

UIActivityViewController *controller = [[UIActivityViewController alloc] initWithActivityItems:objectsToShare applicationActivities:nil];
NSArray *excludedActivities = @[UIActivityTypePostToTwitter, UIActivityTypePostToFacebook,
UIActivityTypePostToWeibo,
UIActivityTypeMessage, UIActivityTypeMail,
UIActivityTypePrint, UIActivityTypeCopyToPasteboard,
UIActivityTypeAssignToContact, UIActivityTypeSaveToCameraRoll,
UIActivityTypeAddToReadingList, UIActivityTypePostToFlickr,
UIActivityTypePostToVimeo, UIActivityTypePostToTencentWeibo];
controller.excludedActivityTypes = excludedActivities;

[self presentViewController:controller animated:YES completion:nil];
}

2.3:MockGPS




我们有些业务会根据地理位置不同,而有不同的业务处理逻辑。而我们开发或者测试,当然不可能去每一个地址都测试一遍。这种情况下,测试同学一般会找到我们让我们手动改掉系统获取经纬度的回调,或者修改GPX文件,然后再重新打一个包。这样也非常麻烦。

DoraemonKit给出的解决方案:提供一套地图界面,支持在地图中滑动选择或者手动输入经纬度,然后自动替换掉我们App中返回的当前经纬度信息。这里的难点是如何不需要重新打包自动替换掉系统返回的当前经纬度信息?

CLLocationManager的delegate中有一个方法如下:

/*
* locationManager:didUpdateLocations:
*
* Discussion:
* Invoked when new locations are available. Required for delivery of
* deferred locations. If implemented, updates will
* not be delivered to locationManager:didUpdateToLocation:fromLocation:
*
* locations is an array of CLLocation objects in chronological order.
*/

- (void)locationManager:(CLLocationManager *)manager
didUpdateLocations:(NSArray *)locations API_AVAILABLE(ios(6.0), macos(10.9));

我们通常是在这个函数中获取当前系统的经纬度信息。我们如果想要没有侵入式的修改这个函数的默认实现方式,想到的第一个方法就是Method Swizzling。但是真正在实现过程中,你会发现Method Swizzling需要当前实例和方法,方法是- (void)locationManager:(CLLocationManager *)manager
didUpdateLocations:(NSArray *)locations 我们有了,但是实例,每一个app都有自己的实现,无法做到统一处理。我们就换了一个思路,如何能获取该实现了该定位方法的实例呢?就是使用Method Swizzling Hook住CLLocationManager的setDelegate方法,就能获取具体是哪一个实例实现了- (void)locationManager:(CLLocationManager *)manager
didUpdateLocations:(NSArray *)locations 方法。

具体方法如下:

第一步: 生成一个CLLocationManager的分类CLLocationManager(Doraemon),在这个分类中,实现- (void)doraemon_swizzleLocationDelegate:(id)delegate这个方法,用来进行方法交换。

- (void)doraemon_swizzleLocationDelegate:(id)delegate {
if (delegate) {
//1、让所有的CLLocationManager的代理都设置为[DoraemonGPSMocker shareInstance],让他做中间转发
[self doraemon_swizzleLocationDelegate:[DoraemonGPSMocker shareInstance]];
//2、绑定所有CLLocationManager实例与delegate的关系,用于[DoraemonGPSMocker shareInstance]做目标转发用。
[[DoraemonGPSMocker shareInstance] addLocationBinder:self delegate:delegate];

//3、处理[DoraemonGPSMocker shareInstance]没有实现的selector,并且给用户提示。
Protocol *proto = objc_getProtocol("CLLocationManagerDelegate");
unsigned int count;
struct objc_method_description *methods = protocol_copyMethodDescriptionList(proto, NO, YES, &count);
NSMutableArray *array = [NSMutableArray array];
for(unsigned i = 0; i < count; i++)
{
SEL sel = methods[i].name;
if ([delegate respondsToSelector:sel]) {
if (![[DoraemonGPSMocker shareInstance] respondsToSelector:sel]) {
NSAssert(NO, @"你在Delegate %@ 中所使用的SEL %@,暂不支持,请联系DoraemonKit开发者",delegate,sel);
}
}
}
free(methods);

}else{
[self doraemon_swizzleLocationDelegate:delegate];
}
}


在这个函数中主要做了三件事情,1、将所有的定位回调统一交给[DoraemonGPSMocker shareInstance]处理 2、[DoraemonGPSMocker shareInstance]绑定了所有CLLocationManager与它的delegate的一一对应关系。3、处理[DoraemonGPSMocker shareInstance]没有实现的selector,并且给用户提示。

第二步:当有一个定位回调过来的时候,我们先传给[DoraemonGPSMocker shareInstance],然后[DoraemonGPSMocker shareInstance]再转发给它绑定过的所有的delegate。那我们App为例,绑定关系如下:


{
"0x2800a07a0_binder" = "";
"0x2800a07a0_delegate" = "";
"0x2800b59a0_binder" = "";
"0x2800b59a0_delegate" = "";
}

由此可见,我们App的统一定位KDDriverLocationManager和苹果地图的定位MAMapLocationManager都是使用都是CLLocationManager提供的。

具体 DoraemonGPSMocker这个类如何实现,请参考DorameonKit/Core/Plugin/GPS中的代码。


2.4:H5任意门





有的时候Native和H5开发同时开发一个功能,H5依赖native提供入口,而这个时候Native还没有开发好,这个时候H5开发就没法在App上看到效果。再比如,有些H5页面处于的位置比较深入,就像我们代驾司机端,做单流程比较多,有的H5界面需要很繁琐的操作才能展示到App上,不方便我们查看和定位问题。
这个时候我们可以为app做一个简单的浏览器,输入url,使用自带的容器进行跳转。因为每一个app的H5容器基本上都是自定义过得,都会有自己的bridge定制化,所以这个H5容器没有办法使用系统原生的UIWebView或者WKWebView,就只能交给业务方自己去完成。我们在DorameonKit初始化的时候,提供了一个回调让业务方用自己的H5容器去打开这个Url:

[[DoraemonManager shareInstance] addH5DoorBlock:^(NSString *h5Url) {
//使用自己的H5容器打开这个链接
}];

这个工具实现比较简单,就不多说了,代码路径在DorameonKit/Core/Plugin/H5.


2.5:子线程UI检查






在iOS中是不允许在子线程中对UI进行操作和渲染的,不然会造成未知的错误和问题,甚至会导致crash。我们在最近几个版本中发现新增了一些crash,调查原因就是在子线程中操作UI导致的。为了对于这种情况可以提早被我们发现,我在在DorameonKit中增加了子线程UI渲染检查查询。

具体事项思路,我们hook住UIView的三个必须在主线程中操作的绘制方法。1、setNeedsLayout 2、setNeedsDisplay 3、setNeedsDisplayInRect:。然后判断他们是不是在子线程中进行操作,如果是在子线程进行操作的话,打印出当前代码调用堆栈,提供给开发进行解决。具体代码如下:

@implementation UIView (Doraemon)

+ (void)load{
[[self class] doraemon_swizzleInstanceMethodWithOriginSel:@selector(setNeedsLayout) swizzledSel:@selector(doraemon_setNeedsLayout)];
[[self class] doraemon_swizzleInstanceMethodWithOriginSel:@selector(setNeedsDisplay) swizzledSel:@selector(doraemon_setNeedsDisplay)];
[[self class] doraemon_swizzleInstanceMethodWithOriginSel:@selector(setNeedsDisplayInRect:) swizzledSel:@selector(doraemon_setNeedsDisplayInRect:)];
}

- (void)doraemon_setNeedsLayout{
[self doraemon_setNeedsLayout];
[self uiCheck];
}

- (void)doraemon_setNeedsDisplay{
[self doraemon_setNeedsDisplay];
[self uiCheck];
}

- (void)doraemon_setNeedsDisplayInRect:(CGRect)rect{
[self doraemon_setNeedsDisplayInRect:rect];
[self uiCheck];
}

- (void)uiCheck{
if([[DoraemonCacheManager sharedInstance] subThreadUICheckSwitch]){
if(![NSThread isMainThread]){
NSString *report = [BSBacktraceLogger bs_backtraceOfCurrentThread];
NSDictionary *dic = @{
@"title":[DoraemonUtil dateFormatNow],
@"content":report
};
[[DoraemonSubThreadUICheckManager sharedInstance].checkArray addObject:dic];
}
}
}

@end

完整代码实现请参考DorameonKit/Core/Plugin/SubThreadUICheck


2.6:日志显示



这个主要是方便我们查看本地日志,以前我们如果要查看日志,需要自己写代码,访问沙盒导出日志文件,然后再查看。也是比较麻烦的。

DoraemonKit的解决方案是:我们每一次触发日志的时候,都把日志内容显示到界面上,方便我们查看。
如何实现的呢?因为我们这个工具并不是一个通用性的工具,只针对于底层日志库是CocoaLumberjack的情况。稍微讲一下的CocoaLumberjack原理,所有的log都会发给DDLog对象,其运行在自己的一个GCD队列中,之后,DDLog会将log分发给其下注册的一个或者多个Logger中,这一步在多核下面是并发的,效率很高。每一个Logger处理收到的log也是在它们自己的GCD队列下做的,它们询问其下的Formatter,获取Log消息格式,然后根据Logger的逻辑,将log消息分发到不同的地方。系统自带三个Logger处理器,DDTTYLogger,主要将日志发送到Xcode控制台;DDASLLogger,主要讲日志发送到苹果的日志系统Console.app; DDFileLogger,主要将日志发送到文件中保存起来,也是我们开发用到最多的。但是自带的Logger并不满足我们的需求,我们的需求是将日志显示到UI界面中,所以我们需要新建一个类DoraemonLogger,继承于DDAbstractLogger,然后重写logMessage方法,将每一条传过来的日志打印到UI界面中。





这个工具参考LumberjackConsole这个开源项目完成,因为刚出iOS11的时候,作者没有适配,所以我们自己拷贝一份代码出来,自己维护了。 完整代码实现请参考DorameonKit/WithLogger中.




作者:景铭巴巴
链接:https://www.jianshu.com/p/00763123dbc4







收起阅读 »

iOS - 极其强大的性能库DoraemonKit

每一个稍微有点规模的 App,总会自带一些线下的测试功能代码,比如环境切换功能、帧率查看功能等等,这些功能的切换入口往往放在各式各样的入口中,比如一些特殊的手势,双击 statusBar,双击某一个功能区块,或者新建一个 keyWindow 始终至于 App ...
继续阅读 »


每一个稍微有点规模的 App,总会自带一些线下的测试功能代码,比如环境切换功能、帧率查看功能等等,这些功能的切换入口往往放在各式各样的入口中,比如一些特殊的手势,双击 statusBar,双击某一个功能区块,或者新建一个 keyWindow 始终至于 App 最上方等等,而且每一个 App 里面的线下附带功能模块很多是相似的,比如帧率查看、内存和 CPU 监控等等,但是现在基本上都是每个 App 都是自己实现了一份,经历了以上的问题之后,DoKit 就有了它存在的意义。

DoKit 是一个功能平台,能够让每一个 App 快速接入一些常用的或者你没有实现的一些辅助开发工具、测试效率工具、视觉辅助工具,而且能够完美在 Doraemon 面板中接入你已经实现的与业务紧密耦合的一些非通有的辅助工具,并搭配我们的dokit平台,让功能得到延伸,接入方便,便于扩展。


一、平台工具(http://www.dokit.cn)

  1. 【数据Mock】 App接口Mock解决方案,提供一套基于App网络拦截的接口Mock方案,无需修改代码即可完成对于接口数据的Mock。
  2. 【健康体检】 一键式操作,整合DoKit多项工具,数据可视化,快速准确定位问题,让你对app的性能了如指掌。
  3. 【文件同步助手】 通过终端服务,让你的终端空间在平台端完整的展现并提供强大的文件以及数据库操作能力。
  4. 【一机多控】 主从同步,释放人力,让研发测试效率提升看得见

二、常用工具

  1. 【App 信息查看】 快速查看手机信息,App 基础信息、签名相关、权限信息的渠道,避免去手机设置查找或者查看项目源代码的麻烦;
  2. 【开发者选项 Android特有】 一键跳转开发者选项,避免安卓由于平台差异导致的入口不一致
  3. 【本地语言】 一键跳转本地语言,避免安卓由于平台差异导致的入口不一致
  4. 【沙盒浏览】 App 内部文件浏览的功能,支持删除和预览, 并且能通过 AirDrop 或者其他分享方式上传到 PC 中,进行更加细致的操作;
  5. 【MockGPS】 App 能定位到全国各地,支持地图地位和手动输入经纬度;
  6. 【H5任意门】 开发测试同学可以快速输入 H5 页面地址,查看该页面效果;
  7. 【Crash查看】 方便本地打印出出现 Crash 的堆栈;
  8. 【子线程UI】 快速定位哪一些 UI 操作在非主线程中进行渲染,避免不必要的问题;(iOS独有)
  9. 【清除本地数据】 一键删除沙盒中所有数据;
  10. 【NSLog】 把所有 NSLog 信息打印到UI界面,避免没有开发证书无法调试的尴尬;
  11. 【Lumberjack】 每一条 CocoaLumberjack 的日志信息,都在在 App 的界面中显示出来,再也不需要导出日志这么麻烦;(iOS独有)
  12. 【DBView】 通过网页方便快捷的操作应用内数据库,让数据库的调试变得非常优雅;
  13. 【模拟弱网】 限制网速,模拟弱网环境下App的运行情况。(android独有)

三、性能检测

  1. 【帧率】 App 帧率信息提供波形图查看功能,让帧率监控的趋势更加明显;
  2. 【CPU】 App CPU 使用率信息提供波形图查看功能,让 CPU 监控的趋势更加形象;
  3. 【内存】 App 内存使用量信息提供波形图查看功能,让内存监控的趋势更加鲜明;
  4. 【流量监控】 拦截 App 内部流量信息,提供波形图展示、流量概要展示、流量列表展示、流量筛选、流量详情,对流量信息统一拦截,成为我们 App 中自带的 "Charles";
  5. 【卡顿】 锁定 App 出现卡顿的时刻,打印出对应的代码调用堆栈;
  6. 【大图检测】 通过流量监测,找出所有的大小超标的图片,避免下载大图造成的流量浪费和渲染大图带来的CPU消耗。
  7. 【启动耗时】 无侵入的统计出App启动过程的总共耗时;
  8. 【UI层级检查】 检查出每一个页面中层级最深的元素;
  9. 【函数耗时】 从函数级别分析app性能瓶颈;
  10. 【Load】 找出所有的Load方法,并给出耗时分析;(iOS独有)
  11. 【内存泄漏】 找出App中所有的内存泄漏的问题。

四、视觉工具

  1. 【颜色吸管】 方便设计师 UI 捉虫的时候,查看每一个组件的颜色值是否设置正确;
  2. 【组件检查】 可以抓取任意一个UI控件,查看它们的详细信息,包括控件名称、控件位置、背景色、字体颜色、字体大小;
  3. 【对齐标尺】 参考 Android 系统自带测试工具,能够实时捕获屏幕坐标,并且可以查看组件是否对齐;
  4. 【元素边框线】 绘制出每一个 UI 组件的边框,对于组件布局有一定的参考意义。

五、Weex专项工具(CML专项工具)

  1. 【console日志查看】 方便在端上查看每一个Weex文件中的console日志,提供分级和搜索功能;
  2. 【storage缓存查看】 将Weex中的storage模块的本地缓存数据可视化展示;
  3. 【容器信息】 查看每一个打开的Weex页面的基本信息和性能数据;
  4. 【DevTool】 快速开启Weex DevTool的扫码入口。

tips : 如果使用我们滴滴优秀的开源跨端方案 chameleon 也可以集成该工具集合

六、支持自定义的业务工具集成到面板中

统一维护和管理所有的测试模块,详见接入手册

七、微信小程序专项工具


收起阅读 »

微信开源框架-崩溃、卡顿和爆内存Matrix

当前工具监控范围包括:崩溃、卡顿和爆内存,包含以下两款插件:WCCrashBlockMonitorPlugin: 基于 KSCrash 框架开发,具有业界领先的卡顿堆栈捕获能力,同时兼备崩溃捕获能力。WCMemoryStatPlu...
继续阅读 »

当前工具监控范围包括:崩溃、卡顿和爆内存,包含以下两款插件:

  • WCCrashBlockMonitorPlugin: 基于 KSCrash 框架开发,具有业界领先的卡顿堆栈捕获能力,同时兼备崩溃捕获能力。

  • WCMemoryStatPlugin: 一款性能优化到极致的爆内存监控工具,能够全面捕获应用爆内存时的内存分配以及调用堆栈情况。

特性

WCCrashBlockMonitorPlugin

  • 接入简单,代码无侵入
  • 通过检查 Runloop 运行状态判断应用是否卡顿,同时支持 iOS/macOS 平台
  • 增加耗时堆栈提取,卡顿线程快照日志中附加最近时间最耗时的主线程堆栈

WCMemoryStatPlugin

  • 在应用运行期间获取对象存活以及相应的堆栈信息,在检测到应用爆内存时进行上报
  • 使用平衡二叉树存储存活对象,使用 Hash Table 存储堆栈,将性能优化到极致

使用方法

安装

  • 通过 Cocoapods 安装

    1. 先安装 CocoaPods
    2. 通过 pod repo update 更新 matrix 的 Cocoapods 版本;
    3. 在 Podfile 对应的 target 中,添加 pod 'matrix-wechat',并执行 pod install;
    4. 在项目中使用 Cocoapods 生成的 .xcworkspace运行工程;
    5. 在你的代码文件头引入头文件 #import ,就可以接入微信的性能探针工具了!
  • 通过静态库安装

    1. 获取 Matrix 源码;
    2. 打开命令行,在 matrix/matrix-iOS 代码目录下执行 make 进行编译生成静态库;编译完成后,iOS 平台的库在 matrix/matrix-iOS/build_ios 目录下,macOS 平台的库在 matrix/matrix-iOS/build_macos目录下;
    3. 工程引入静态库:
    • iOS 平台:使用 matrix/matrix-iOS/build_ios 路径下的 Matrix.framework,将 Matrix.framework以静态库的方式引入工程;
    • macOS 平台:使用 matrix/matrix-iOS/build_macos 路径下的 Matrix.framework,将 Matrix.framework 以静态库的方式引入工程。
    1. 添加头文件 #import ,就可以接入微信的性能探针工具了!

启动监控

在以下地方:

  • 程序 main 函数入口;
  • AppDelegate 中的 application:didFinishLaunchingWithOptions:
  • 或者其他应用启动比较早的时间点。

添加类似如下代码,启动插件:

#import 

Matrix *matrix = [Matrix sharedInstance];
MatrixBuilder *curBuilder = [[MatrixBuilder alloc] init];
curBuilder.pluginListener = self; // pluginListener 回调 plugin 的相关事件

WCCrashBlockMonitorPlugin *crashBlockPlugin = [[WCCrashBlockMonitorPlugin alloc] init];
[curBuilder addPlugin:crashBlockPlugin]; // 添加卡顿和崩溃监控

WCMemoryStatPlugin *memoryStatPlugin = [[WCMemoryStatPlugin alloc] init];
[curBuilder addPlugin:memoryStatPlugin]; // 添加内存监控功能

[matrix addMatrixBuilder:curBuilder];

[crashBlockPlugin start]; // 开启卡顿和崩溃监控
// [memoryStatPlugin start];
// 开启内存监控,注意 memoryStatPlugin 开启之后对性能损耗较大,建议按需开启

接收回调获得监控数据

设置 MatrixBuilder 对象中的 pluginListener,实现 MatrixPluginListenerDelegate。

// 设置 delegate

MatrixBuilder *curBuilder = [[MatrixBuilder alloc] init];
curBuilder.pluginListener = <一个遵循 MatrixPluginListenerDelegate 的对象>;

// MatrixPluginListenerDelegate

- (void)onInit:(id)plugin;
- (void)onStart:(id)plugin;
- (void)onStop:(id)plugin;
- (void)onDestroy:(id)plugin;
- (void)onReportIssue:(MatrixIssue *)issue;

各个添加到 MatrixBuilder 的 plugin 会将对应的事件通过 pluginListener 回调。

重要:通过 onReportIssue: 获得 Matrix 处理后的数据,监控数据格式详见:Matrix for iOS/macOS 数据格式说明

Demo

至此,Matrix 已经集成到应用中并且开始收集崩溃、ANR、卡顿和爆内存数据,如仍有疑问,请查看示例:samples/sample-apple/MatrixDemo


常见问题及源码下载:https://github.com/Tencent/matrix#matrix_ios_cn



收起阅读 »

iOS 界面优化

卡顿原因计算机通过CPU、GPU、显示器三者协同工作将试图显示到屏幕上1、CPU将需要显示的内容计算出来,提交到GPU2、GPU将内容渲染完成后将渲染后的内容存放到FrameBuffer(帧缓冲区)3、视频控制器根据VSync(垂直同步)信号来读取FrameB...
继续阅读 »

卡顿原因

计算机通过CPUGPU显示器三者协同工作将试图显示到屏幕上

  • 1、CPU将需要显示的内容计算出来,提交到GPU
  • 2、GPU将内容渲染完成后将渲染后的内容存放到FrameBuffer(帧缓冲区)
  • 3、视频控制器根据VSync(垂直同步)信号来读取FrameBuffer中的数据
  • 4、将转换的数模传递给显示器显示


iOS设备中采用双缓存区+VSync

在收到VSync信号后,系统的图形服务通过CADisplayLink等机制通知App,在主程序中调度CPU计算显示的内容,随后将计算好的内容提交到GPU变换、合成、渲染,GPU将渲染结果提交帧缓冲区,等待下一个VSync信号到来时显示到屏幕上。由于垂直同步机制的原因,如果再一个VSync时间内,CPU或者GPU没有完成内容的处理,就会导致当前处理的帧丢弃,此时屏幕会保持上一帧的显示,造成掉帧


卡顿检测

  • FPS监控:因为iOS设备屏幕的刷新时间是60次/秒,一次刷新就是一次VSync信号,时间间隔是1000ms/60 = 16.67ms,所有如果咋16.67ms内下一帧数据没有准备好,就会产生掉帧
  • RunLoop监控:通过子线程检测主线程的RunLoop的状态,kCFRunLoopBeforeSourceskCFRunLoopAfterWaiting两个状态之间的耗时是否达到一定的阈值

FPS监控

参照YYKit中的YYFPSLabel,其中通过CADisplayLink来实现,通过刷新次数/时间差得到刷新频率


class YPFPSLabel: UILabel {

fileprivate var link: CADisplayLink = {
let link = CADisplayLink.init()
return link
}()

fileprivate var count: Int = 0
fileprivate var lastTime: TimeInterval = 0.0
fileprivate var fpsColor: UIColor = {
return UIColor.green
}()
fileprivate var fps: Double = 0.0

override init(frame: CGRect) {
var f = frame
if f.size == CGSize.zero {
f.size = CGSize(width: 80.0, height: 22.0)
}

super.init(frame: f)

self.textColor = UIColor.white
self.textAlignment = .center
self.font = UIFont.init(name: "Menlo", size: 12)
self.backgroundColor = UIColor.lightGray
//通过虚拟类
link = CADisplayLink.init(target: CJLWeakProxy(target:self), selector: #selector(tick(_:)))
link.add(to: RunLoop.current, forMode: RunLoop.Mode.common)
}

required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

deinit {
link.invalidate()
}

@objc func tick(_ link: CADisplayLink){
guard lastTime != 0 else {
lastTime = link.timestamp
return
}

count += 1
//时间差
let detla = link.timestamp - lastTime
guard detla >= 1.0 else {
return
}

lastTime = link.timestamp
//刷新次数 / 时间差 = 刷新频次
fps = Double(count) / detla
let fpsText = "\(String.init(format: "%.2f", fps)) FPS"
count = 0

let attrMStr = NSMutableAttributedString(attributedString: NSAttributedString(string: fpsText))
if fps > 55.0 {
//流畅
fpsColor = UIColor.green
}else if (fps >= 50.0 && fps <= 55.0){
//一般
fpsColor = UIColor.yellow
}else{
//卡顿
fpsColor = UIColor.red
}

attrMStr.setAttributes([NSAttributedString.Key.foregroundColor: fpsColor], range: NSMakeRange(0, attrMStr.length - 3))
attrMStr.setAttributes([NSAttributedString.Key.foregroundColor: UIColor.white], range: NSMakeRange(attrMStr.length - 3, 3))

DispatchQueue.main.async {
self.attributedText = attrMStr
}
}

}

RunLoop监控

开辟子线程,通过监听主线程的kCFRunLoopBeforeSourceskCFRunLoopAfterWaiting两个Activity之间的差值


#import "YPBlockMonitor.h"

@interface YPBlockMonitor (){
CFRunLoopActivity activity;
}

@property (nonatomic, strong) dispatch_semaphore_t semaphore;
@property (nonatomic, assign) NSUInteger timeoutCount;

@end

@implementation YPBlockMonitor

+ (instancetype)sharedInstance {
static id instance = nil;
static dispatch_once_t onceToken;

dispatch_once(&onceToken, ^{
instance = [[self alloc] init];
});
return instance;
}

- (void)start{
[self registerObserver];
[self startMonitor];
}

static void CallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info)
{
LGBlockMonitor *monitor = (__bridge LGBlockMonitor *)info;
monitor->activity = activity;
// 发送信号
dispatch_semaphore_t semaphore = monitor->_semaphore;
dispatch_semaphore_signal(semaphore);
}

- (void)registerObserver{
CFRunLoopObserverContext context = {0,(__bridge void*)self,NULL,NULL};
//NSIntegerMax : 优先级最小
CFRunLoopObserverRef observer = CFRunLoopObserverCreate(kCFAllocatorDefault,
kCFRunLoopAllActivities,
YES,
NSIntegerMax,
&CallBack,
&context);
CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
}

- (void)startMonitor{
// 创建信号
_semaphore = dispatch_semaphore_create(0);
// 在子线程监控时长
dispatch_async(dispatch_get_global_queue(0, 0), ^{
while (YES)
{
// 超时时间是 1 秒,没有等到信号量,st 就不等于 0, RunLoop 所有的任务
long st = dispatch_semaphore_wait(self->_semaphore, dispatch_time(DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC));
if (st != 0)
{
if (self->activity == kCFRunLoopBeforeSources || self->activity == kCFRunLoopAfterWaiting)
{
if (++self->_timeoutCount < 2){
NSLog(@"timeoutCount==%lu",(unsigned long)self->_timeoutCount);
continue;
}
// 一秒左右的衡量尺度 很大可能性连续来 避免大规模打印!
NSLog(@"检测到超过两次连续卡顿");
}
}
self->_timeoutCount = 0;
}
});
}

@end

界面优化

UIView和CALayer的关系

  • UIView是基于UIKit框架,继承自UIResponder,可以处理事件,管理子视图
  • CALayer是基于CoreAnimation的,继承自NSObject,只负责显示,不能处理事件
  • UIKit组件最终都会分解为layer,存储到图层树
  • UIView中的部分属性,frame、bounds、transform等,来自CALayer的映射
  • CALayer内部没有属性,在调用属性时,内部通过运行时resolveInstanceMethod方法为对象临时添加一个方法,并将对应属性值保存到内部的Dictionary,同时通知delegate、创建动画等

CPU层面的优化

  • 1、对于不需要触摸的控件使用CALayer代替UIView

  • 2、减少UIViewCALayer的属性修改

  • 3、大量对象释放时,移动到后台线程释放

  • 4、预排版:在异步子线程中提前计算好视图的大小

  • 5、Autolayout在简单页面情况下们可以很好的提升开发效率,但是对于复杂视图而言,会产生严重的性能问题,随着视图数量的增长,Autolayout带来的CPU消耗是呈指数上升的。所以尽量使用代码布局

  • 6、文本处理

    • 对于文本没有特殊要求的,可以使用UILabel内部实现方法,放在子线程中执行
      • 计算文本宽高:[NSAttributedString boundingRectWithSize:options:context:]
      • 文本绘制:[NSAttributedString drawWithRect:options:context:]
    • 使用自定义文本控件,通过TextKit或者CoreText进行异步文本绘制。CoreText对象创建后,可以直接获取文本宽高等信息。CoreText直接使用了CoreGraphics占用内存小,效率高
  • 7、图片优化
    在使用UIImage或者CGImageSource方法创建图片时,图片数据不会立即解码,而是在设置到UIImageView/CALayer.contents中,然后由CALayer提交到GPU渲染前才在主线程进行解码,可以参考SDWebImage中对图片的处理,在子线程中先将图片绘制到CGBitmapContext,然后从Bitmap直接创建图片

    • 使用PNG图片,而非JPGE图片
    • 子线程中解码,主线程渲染,即通过Bitmap创建图片,在子线程赋值image
    • 优化图片大小,避免动态缩放
    • 多图合成一张图片显示
  • 8、避免使用透明View,会导致GPU在计算像素时,会将下层图层的像素也计算进来,颜色混合处理

  • 9、按需加载:例如通过RunLoop分发任务,ScrollView滚动时不加载

  • 10、少使用addView 给cell动态添加view

GPU层面优化

GPU主要是接收CPU提交的纹理+顶点,经过一系列transform,最终混合并渲染,输出到屏幕上
1、避免短时间显示大量图片,可以将多张图片合成一张
2、控制图片尺寸不超过4096x4096,因为图片超过这个尺寸,CPU会先进行预处理再提交给GPU
3、减少视图层级和数量
4、避免离屏渲染
5、异步渲染,例如可以将cell中的所有控件、视图合成一张位图进行显示



作者:木扬音
链接:https://www.jianshu.com/p/2f9a06932879

收起阅读 »