注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

一个"剑气"加载?️

🙇 前言 我们在网页加载的时候总会加上一些过渡效果来引导用户,减少白屏时间,而加载的效果可以用svg也可以使用一些我们封装好的组件,今天就来分享一种"剑气"加载效果。 相信大家看封面都知道效果了,那我们就直接开干吧。 🏋️‍♂️ToDoList 剑气...
继续阅读 »

🙇 前言



  • 我们在网页加载的时候总会加上一些过渡效果来引导用户,减少白屏时间,而加载的效果可以用svg也可以使用一些我们封装好的组件,今天就来分享一种"剑气"加载效果。

  • 相信大家看封面都知道效果了,那我们就直接开干吧。


src=http___image.17173.com_bbs_v1_2012_12_01_1354372326576.gif&refer=http___image.17173.gif


🏋️‍♂️ToDoList



  • 剑气形状

  • 剑气转动

  • 组合剑气


🚴 Just Do It



  • 其实做一个这样的效果仔细看就是有三个类似圆环状的元素进行循环转动,我们只需要拆解出一个圆环来做效果即可,最后再将三个圆环组合起来。


剑气形状



  • 仔细看一道剑气,它的形状是不是很像一把圆圆的镰刀分成一半,而这个镰刀我们可以通过边框和圆角来做。

  • 首先准备一个剑气雏形。


  <div class="sword">
<span>
</div>


  • 我们只需要对一个圆加上一个方向的边框就可以做成半圆的形状,这样类似剑气的半圆环形状就完成了🌪️。


.sword {
position: relative;
margin: 200px auto;
width: 64px;
height: 64px;
border-radius: 50%;
}
.sword span{
position: absolute;
box-sizing: border-box;
width: 100%;
height: 100%;
border-radius: 50%;
}
.sword :first-child{
left: 0%;
top: 0%;
border-bottom: 3px solid #EFEFFA;
}

image.png


剑气转动



  • 因为我们需要剑气一直不停的循环转动,所以我们可以借助cssanimation动画属性就可以自己给它添加一个动画了。

  • animation属性是一个简写属性,可以用于设置以下动画属性分别是:

    • animation-name:指定要绑定到选择器的关键帧的名称

    • animation-duration:动画指定需要多少秒或毫秒完成

    • animation-timing-function:设置动画将如何完成一个周期

    • animation-delay:设置动画在启动前的延迟间隔

    • animation-iteration-count:定义动画的播放次数

    • animation-direction:指定是否应该轮流反向播放动画

    • animation-fill-mode:规定当动画不播放时,要应用到元素的样式

    • animation-play-state:指定动画是否正在运行或已暂停



  • 更多的动画学习可以参考MDN


...
.sword :first-child{
...
animation: sword-one 1s linear infinite;
...
}
@keyframes sword-one {
0% {
transform: rotateZ(0deg);
}
100% {
transform: rotateZ(360deg);
}
}
...


  • 我们可以给定一个不断绕z0deg360deg转动的动画,设定为一秒完成一次一直无限循环,我们来看看效果:


剑气1.gif



  • 接下来让这个半圆弧分别绕x轴和y轴也转动一定角度即可完成一个剑气的转动。


...
@keyframes sword-one {
0% {
transform: rotateX(35deg) rotateY(-45deg) rotateZ(0deg);
}
100% {
transform: rotateX(35deg) rotateY(-45deg) rotateZ(360deg);
}
}
...


  • 我们来看看完成后的效果:


剑气2.gif


组合剑气



  • 最后我们只需要再制作两个剑气在组装起来就好了。


<div class="sword">
<span></span>
<span></span>
<span></span>
</div>


  • 给新添的两个span添加动画和样式。


...
.sword :nth-child(2){
right: 0%;
top: 0%;
animation: sword-two 1s linear infinite;
border-right: 3px solid #EFEFFA;
}

.sword :last-child{
right: 0%;
bottom: 0%;
animation: sword-three 1s linear infinite;
border-top: 3px solid #EFEFFA;
}

@keyframes sword-two {
0% {
transform: rotateX(50deg) rotateY(10deg) rotateZ(0deg);
}
100% {
transform: rotateX(50deg) rotateY(10deg) rotateZ(360deg);
}
}

@keyframes sword-three {
0% {
transform: rotateX(35deg) rotateY(55deg) rotateZ(0deg);
}
100% {
transform: rotateX(35deg) rotateY(55deg) rotateZ(360deg);
}
}
...


  • 这样我们的剑气加载效果就制作好了,以上就是全部代码了,喜欢的可以拿去用哟。

  • 我们来看看最终的效果吧~


剑气3.gif



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

收起阅读 »

iOS AVPlayer的那些坑

这次主要是总结和记录下视频播放遇到的坑,视频播放采用的是AVPlayer这个控件,语法大致如下: NSURL * url = [NSURL fileURLWithPath:@"视频地址"]; AVPlayerItem *playerItem = ...
继续阅读 »

这次主要是总结和记录下视频播放遇到的坑,视频播放采用的是AVPlayer这个控件,语法大致如下:

    NSURL * url = [NSURL fileURLWithPath:@"视频地址"];
AVPlayerItem *playerItem = [AVPlayerItem playerItemWithURL:url];
self.player = [AVPlayer playerWithPlayerItem:playerItem];
[playerItem addObserver:self forKeyPath:@"status" options:NSKeyValueObservingOptionNew context:nil];
self.player.actionAtItemEnd = AVPlayerActionAtItemEndNone;
self.playerLayer = [AVPlayerLayer playerLayerWithPlayer:self.player];
self.playerLayer.videoGravity = AVLayerVideoGravityResizeAspect;
self.playerLayer.frame = self.view.bounds;
[self.view.layer addSublayer:self.playerLayer];

一般说来,这里要监听AVPlayerItem的status属性:

 *
*AVPlayerItem的三种状态
*AVPlayerItemStatusUnknown,
*AVPlayerItemStatusReadyToPlay,
*AVPlayerItemStatusFailed
*/

如果是AVPlayerStatusFailed说明视频加载失败,这时可以通过self.player.error.description属性来找出具体的原因。 问题一:当status变为AVPlayerStatusReadyToPlay后,我们调用play方法真的就能保证视频正常播放吗?

众所周知,AVPlayer支持的视频、音频格式非常广泛,抛开那些无法正常编解码的情况,在某些情况下其可能就是无法正常播放。AVPlayer在进行播放时,会预先解码一些内容,而此时如果我们的App使用CPU过多,I/O读写过多时,有可能导致视频播放声/画不同步,这点尤其在iPhone4上面表现更为明显,用户反馈iOS9.3.2的系统上也很明显。而如果是发生在AVPlayer初始化解码视频的时候,有可能导致视频直接无法播放,这时,我们再调用play或者seekToTime:方法都无法正常播放。建议不要在CPU或者I/O很频繁的情况下使用AVPlayer,例如刚登录App加载各种数据的情况下,可以等App预热以后再使用。

问题二:当rate属性的值大于0后,真的就在播放视频了吗?

当然不是。当发生上面所讲的情况时,我打印了当前的rate情况,是大于0的,但是页面上显示的情况却还是什么也没有。有时候我们如果想要在视频一播放的时候去做一些事情,例如设置一下播放器的背景色,如果我们仅仅是监听这个rate可能无法100%保证有效,而如果我们真的要监听这种情况的话,有一个取巧的方法:


 id _timerObserver = [self.player addBoundaryTimeObserverForTimes:@[[NSValue valueWithCMTime:CMTimeMake(1, 30)]] queue:dispatch_get_main_queue()
usingBlock:^{
//do something
}];

另外如果不需要监听播放进度的时候可以调下面的方法:

[self.player removeTimeObserver:_timerObserver];

问题三:AVPlayer前后台播放

当我们切换到后台后,这时AVPlayer通常会自动暂停,当然如果设置了后台播放音频的话,是可以在后台继续播放声音的,正如苹果自己的WWDC这个App一样。这个功能在我的另一篇文章iOS AVPlayer之后台连续播放视频中解决了这个问题。

问题四:音频通道的抢占引起的无法播放视频问题

这个问题下周我会另开一篇博客专门讲述,今儿就此略过。

问题五:其它App播放声音打断

如果用户当时在后台听音乐,如QQ音乐,或者喜马拉雅这些App,这个时候播放视频后,其会被我们打断,当我们不再播放视频的时候,自然需要继续这些后台声音的播放。

首先,我们需要先向设备注册激活声音打断AudioSessionSetActive(YES);,当然我们也可以通过 [AVAudioSession sharedInstance].otherAudioPlaying;这个方法来判断还有没有其它业务的声音在播放。 当我们播放完视频后,需要恢复其它业务或App的声音,这时我们可以在退到后台的事件中调用如下方法:

- (void)applicationDidEnterBackground:(UIApplication *)application {

NSError *error =nil;
AVAudioSession *session = [AVAudioSession sharedInstance];

// [session setCategory:AVAudioSessionCategoryPlayback error:nil];
BOOL isSuccess = [session setActive:NO withOptions:AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation error:&error];

if (!isSuccess) {

NSLog(@"__%@",error);

}else{

NSLog(@"成功了");
}

}

问题六:在用户插入和拔出耳机时,导致视频暂停,解决方法如下

 //耳机插入和拔掉通知
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(audioRouteChangeListenerCallback:) name:AVAudioSessionRouteChangeNotification object:[AVAudioSession sharedInstance]];

//耳机插入、拔出事件
- (void)audioRouteChangeListenerCallback:(NSNotification*)notification {
NSDictionary *interuptionDict = notification.userInfo;

NSInteger routeChangeReason = [[interuptionDict valueForKey:AVAudioSessionRouteChangeReasonKey] integerValue];

switch (routeChangeReason) {

case AVAudioSessionRouteChangeReasonNewDeviceAvailable:

break;

case AVAudioSessionRouteChangeReasonOldDeviceUnavailable:
{
//判断为耳机接口
AVAudioSessionRouteDescription *previousRoute =interuptionDict[AVAudioSessionRouteChangePreviousRouteKey];

AVAudioSessionPortDescription *previousOutput =previousRoute.outputs[0];
NSString *portType =previousOutput.portType;

if ([portType isEqualToString:AVAudioSessionPortHeadphones]) {
// 拔掉耳机继续播放
if (self.playing) {

[self.player play];
}
}

}
break;

case AVAudioSessionRouteChangeReasonCategoryChange:
// called at start - also when other audio wants to play

break;
}
}

问题七:打电话等中断事件

//中断的通知
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleInterruption:) name:AVAudioSessionInterruptionNotification object:[AVAudioSession sharedInstance]];
//中断事件
- (void)handleInterruption:(NSNotification *)notification{

NSDictionary *info = notification.userInfo;
//一个中断状态类型
AVAudioSessionInterruptionType type =[info[AVAudioSessionInterruptionTypeKey] integerValue];

//判断开始中断还是中断已经结束
if (type == AVAudioSessionInterruptionTypeBegan) {
//停止播放
[self.player pause];

}else {
//如果中断结束会附带一个KEY值,表明是否应该恢复音频
AVAudioSessionInterruptionOptions options =[info[AVAudioSessionInterruptionOptionKey] integerValue];
if (options == AVAudioSessionInterruptionOptionShouldResume) {
//恢复播放
[self.player play];
}

}

}

小提示:如果不起作用,请检查退到后台事件中有什么其它的操作没。因为电话来时,会调用退到后台的事件。

问题七:内存泄露问题 当我们释放一个正在播放的视频时,需要先调用pause方法,如果由于某些原因,例如切前后台时,导致又调用了play方法,那么有可能会hold不住内存空间而导致内存泄漏。其实更靠谱的方法是,还要移除player加载的资源。

总的来说,AVPlayer能满足一般的需求,虽然坑不少。最后,再来安利一下我自己封装的LYAVPlayer,简单方便,支持cocoa pods,只需几行代码即可完成播放:

         LYAVPlayerView *playerView =[LYAVPlayerView alloc]init];         
playerView.frame =CGRectMake(0, 64, ScreenWidth,200);
playerView.delegate =self;//设置代理
[self.view addSubview:playerView];
[playerView setURL:[NSURL URLWithString:VideoURL]];//设置播放的URL
[playerView play];//开始播放

工程中pod 'LYAVPlayer','~> 1.0.1'即可使用。有什么问题请Issues


作者:卢叁
链接:https://www.jianshu.com/p/47c7144db817


收起阅读 »

GPUImage recalculateViewGeometry卡UI线程的问题

原因:更新xocde11.4之后发现GPUImage视频画面渲染特别慢,并且控制台输出如下信息:Main Thread Checker: UI API called on a background thread: -[UIView bounds] PID: 7...
继续阅读 »

原因:
更新xocde11.4之后发现GPUImage视频画面渲染特别慢,并且控制台输出如下信息:

Main Thread Checker: UI API called on a background thread: -[UIView bounds]
PID: 7360, TID: 1812926, Thread name: (none), Queue name: com.sunsetlakesoftware.GPUImage.openGLESContextQueue, QoS: 0
Backtrace:
4 KXLive 0x0000000100e12e60 __39-[GPUImageView recalculateViewGeometry]_block_invoke + 52
5 KXLive 0x0000000100dec788 runSynchronouslyOnVideoProcessingQueue + 108
6 KXLive 0x0000000100e12e0c -[GPUImageView recalculateViewGeometry] + 108
7 KXLive 0x0000000100e13804 __37-[GPUImageView setInputSize:atIndex:]_block_invoke + 312
8 KXLive 0x0000000100dec788 runSynchronouslyOnVideoProcessingQueue + 108
9 KXLive 0x0000000100e136ac -[GPUImageView setInputSize:atIndex:] + 136
10 KXLive 0x0000000100e0ee38 -[GPUImageVideoCamera updateTargetsForVideoCameraUsingCacheTextureAtWidth:height:time:] + 660
11 KXLive 0x0000000100e0fb48 -[GPUImageVideoCamera processVideoSampleBuffer:] + 2120
12 KXLive 0x0000000100e106c4 __74-[GPUImageVideoCamera captureOutput:didOutputSampleBuffer:fromConnection:]_block_invoke + 180
13 libdispatch.dylib 0x0000000105e5d260 _dispatch_call_block_and_release + 24
14 libdispatch.dylib 0x0000000105e5d220 _dispatch_client_callout + 16
15 libdispatch.dylib 0x0000000105e6be80 _dispatch_queue_serial_drain + 768
16 libdispatch.dylib 0x0000000105e60730 _dispatch_queue_invoke + 328
17 libdispatch.dylib 0x0000000105e6cdd8 _dispatch_root_queue_drain_deferred_wlh + 352
18 libdispatch.dylib 0x0000000105e73ebc _dispatch_workloop_worker_thread + 676
19 libsystem_pthread.dylib 0x0000000181e6fe70 _pthread_wqthread + 860
20 libsystem_pthread.dylib 0x0000000181e6fb08 start_wqthread + 4

意思是在子线程中UIView对象调用了bounds ,导致视频画面迟迟渲染不出来。
查找发现GPUImageView的视频渲染类的

(void)recalculateViewGeometry;

方法中有两处调用了bounds。
解决办法:
在GPUImageView中声明一个属性viewBounds来保存view的bounds值,在- (void)layoutSubviews方法中给viewBounds赋值,用viewBounds代替bounds就可以了,此时可以很快的调起摄像头并且渲染出画面。

- (void)layoutSubviews {
[super layoutSubviews];
self.viewBounds = self.bounds;
// The frame buffer needs to be trashed and re-created when the view size changes.
if (!CGSizeEqualToSize(self.bounds.size, boundsSizeAtFrameBufferEpoch) &&
!CGSizeEqualToSize(self.bounds.size, CGSizeZero)) {
runSynchronouslyOnVideoProcessingQueue(^{
[self destroyDisplayFramebuffer];
[self createDisplayFramebuffer];
[self recalculateViewGeometry];
});
}
}

- (void)recalculateViewGeometry;
{
runSynchronouslyOnVideoProcessingQueue(^{
CGFloat heightScaling, widthScaling;

CGSize currentViewSize = self.viewBounds.size;

// CGFloat imageAspectRatio = inputImageSize.width / inputImageSize.height;
// CGFloat viewAspectRatio = currentViewSize.width / currentViewSize.height;

CGRect insetRect = AVMakeRectWithAspectRatioInsideRect(inputImageSize, self.viewBounds);
在xcode11和iOS13开始,系统对在子线程中做UI操作要求更加严格。千万不要在子线程中使用与UI相关的代码

作者:那月无痕
链接:https://www.jianshu.com/p/15cc2cd3a862




收起阅读 »

iOS-GPUImage实现美颜相机功能

本文介绍了如何使用 GPUImage 来实现一个简单的相机。具体功能包括拍照、录制视频、多段视频合成、实时美颜、自定义滤镜实现等。前言AVFoundation 是苹果提供的用于处理基于时间的媒体数据的一个框架。我们想要实现一个相机,需要从手机摄像头采集数据,离...
继续阅读 »

本文介绍了如何使用 GPUImage 来实现一个简单的相机。具体功能包括拍照、录制视频、多段视频合成、实时美颜、自定义滤镜实现等。

前言

AVFoundation 是苹果提供的用于处理基于时间的媒体数据的一个框架。我们想要实现一个相机,需要从手机摄像头采集数据,离不开这个框架的支持。GPUImage 对 AVFoundation 做了一些封装,使我们的采集工作变得十分简单。

另外,GPUImage 的核心魅力还在于,它封装了一个链路结构的图像数据处理流程,简称滤镜链。滤镜链的结构使得多层滤镜的叠加功能变得很容易实现。

在下面介绍的功能中,有一些和 GPUImage 本身的关系并不大,我们是直接调用 AVFoundation 的 API 来实现的。但是,这些功能也是一个相机应用必不可少的一部分。所以,我们也会简单讲一下每个功能的实现方式和注意事项。

滤镜链简介

在 GPUImage 中,对图像数据的处理都是通过建立滤镜链来实现的。

这里就涉及到了一个类 GPUImageOutput 和一个协议 GPUImageInput 。对于继承了 GPUImageOutput 的类,可以理解为具备输出图像数据的能力;对于实现了 GPUImageInput 协议的类,可以理解为具备接收图像数据输入的能力。

顾名思义,滤镜链作为一个链路,具有起点和终点。根据前面的描述,滤镜链的起点应该只继承了 GPUImageOutput 类,滤镜链的终点应该只实现了 GPUImageInput 协议,而对于中间的结点应该同时继承了 GPUImageOutput 类并实现了 GPUImageInput 协议,这样才具备承上启下的作用。

一、滤镜链起点

在 GPUImage 中,只继承了 GPUImageOutput,而没有实现 GPUImageInput 协议的类有六个,也就是说有六种类型的输入源:

1、GPUImagePicture

GPUImagePicture 通过图片来初始化,本质上是先将图片转化为 CGImageRef,然后将 CGImageRef 转化为纹理。

2、GPUImageRawDataInput

GPUImageRawDataInput 通过二进制数据初始化,然后将二进制数据转化为纹理,在初始化的时候需要指明数据的格式(GPUPixelFormat)。

3、GPUImageTextureInput

GPUImageTextureInput 通过已经存在的纹理来初始化。既然纹理已经存在,在初始化的时候就不会重新去生成,只是将纹理的索引保存下来。

4、GPUImageUIElement

GPUImageUIElement 可以通过 UIView 或者 CALayer 来初始化,最后都是调用 CALayer 的 renderInContext: 方法,将当前显示的内容绘制到 CoreGraphics 的上下文中,从而获取图像数据。然后将数据转化为纹理。简单来说就是截屏,截取当前控件的内容。

这个类可以用来实现在视频上添加文字水印的功能。因为在 OpenGL 中不能直接进行文本的绘制,所以如果我们想把一个 UILabel 的内容添加到滤镜链里面去,使用 GPUImageUIElement 来实现是很合适的。

5、GPUImageMovie

GPUImageMovie 通过本地的视频来初始化。首先通过 AVAssetReader 来逐帧读取视频,然后将帧数据转化为纹理,具体的流程大概是:AVAssetReaderOutput -> CMSampleBufferRef -> CVImageBufferRef -> CVOpenGLESTextureRef -> Texture 。

6、GPUImageVideoCamera

GPUImageVideoCamera 通过相机参数来初始化,通过屏幕比例相机位置(前后置) 来初始化相机。这里主要使用 AVCaptureVideoDataOutput 来获取持续的视频流数据输出,在代理方法 captureOutput:didOutputSampleBuffer:fromConnection: 中可以拿到 CMSampleBufferRef ,将其转化为纹理的过程与 GPUImageMovie 类似。

然而,我们在项目中使用的是它的子类 GPUImageStillCamera。 GPUImageStillCamera 在原来的基础上多了一个 AVCaptureStillImageOutput,它是我们实现拍照功能的关键,在 captureStillImageAsynchronouslyFromConnection:completionHandler: 方法的回调中,同样能拿到我们熟悉 CMSampleBufferRef

简单来说,GPUImageVideoCamera 只能录制视频,GPUImageStillCamera 还可以拍照, 因此我们使用 GPUImageStillCamera 。

二、滤镜

滤镜链的关键角色是 GPUImageFilter,它同时继承了 GPUImageOutput 类并实现了 GPUImageInput 协议。GPUImageFilter 实现承上启下功能的基础是「渲染到纹理」,这个操作我们在 《使用 iOS OpenGL ES 实现长腿功能》 一文中已经介绍过了,简单来说就是将结果渲染到纹理而不是屏幕上

这样,每一个滤镜都能把输出的纹理作为下一个滤镜的输入,实现多层滤镜效果的叠加。

三、滤镜链终点

在 GPUImage 中,实现了 GPUImageInput 协议,而没有继承 GPUImageOutput 的类有四个:

1、GPUImageMovieWriter

GPUImageMovieWriter 封装了 AVAssetWriter,可以逐帧从帧缓存的渲染结果中读取数据,最后通过 AVAssetWriter 将视频文件保存到指定的路径。

2、GPUImageRawDataOutput

GPUImageRawDataOutput 通过 rawBytesForImage 属性,可以获取到当前输入纹理的二进制数据。

假设我们的滤镜链在输入源和终点之间,连接了三个滤镜,而我们需要拿到第二个滤镜渲染后的数据,用来做人脸识别。那我们可以在第二个滤镜后面再添加一个 GPUImageRawDataOutput 作为输出,则可以拿到对应的二进制数据,且不会影响原来的渲染流程。

3、GPUImageTextureOutput

这个类的实现十分简单,提供协议方法 newFrameReadyFromTextureOutput:,在每一帧渲染结束后,将自身返回,通过 texture 属性就可以拿到输入纹理的索引。

4、GPUImageView

GPUImageView 继承自 UIView,通过输入的纹理,执行一遍渲染流程。这次的渲染目标不是新的纹理,而是自身的 layer 。

这个类是我们实现相机功能的重要组成部分,我们所有的滤镜效果,都要依靠它来呈现。

功能实现

一、拍照

拍照功能只需调用一个接口就能搞定,在回调方法中可以直接拿到 UIImage。代码如下:

- (void)takePhotoWtihCompletion:(TakePhotoResult)completion {
GPUImageFilter *lastFilter = self.currentFilterHandler.lastFilter;
[self.camera capturePhotoAsImageProcessedUpToFilter:lastFilter withCompletionHandler:^(UIImage *processedImage, NSError *error) {
if (error && completion) {
completion(nil, error);
return;
}
if (completion) {
completion(processedImage, nil);
}
}];
}

值得注意的是,相机的预览页面由 GPUImageView 承载,显示的是整个滤镜链作用的结果。而我们的拍照接口,可以传入这个链路上的任意一个滤镜,甚至可以在后面多加一个滤镜,然后拍照接口会返回对应滤镜的渲染结果。即我们的拍照结果不一定要和我们的预览一致

示意图如下:

[图片上传失败...(image-68bb82-1610618344597)]

<figcaption style="box-sizing: border-box; display: block;"></figcaption>

二、录制视频

1、单段录制

录制视频首先要创建一个 GPUImageMovieWriter 作为链路的输出,与上面的拍照接口类似,这里录制的视频不一定和我们的预览一样。

整个过程比较简单,当我们调用停止录制的接口并回调之后,视频就被保存到我们指定的路径了。

- (void)setupMovieWriter {
NSString *videoPath = [SCFileHelper randomFilePathInTmpWithSuffix:@".m4v"];
NSURL *videoURL = [NSURL fileURLWithPath:videoPath];
CGSize videoSize = self.videoSize;

self.movieWriter = [[GPUImageMovieWriter alloc] initWithMovieURL:videoURL
size:videoSize];

GPUImageFilter *lastFilter = self.currentFilterHandler.lastFilter;
[lastFilter addTarget:self.movieWriter];
self.camera.audioEncodingTarget = self.movieWriter;
self.movieWriter.shouldPassthroughAudio = YES;

self.currentTmpVideoPath = videoPath;
}
- (void)recordVideo {
[self setupMovieWriter];
[self.movieWriter startRecording];
}
- (void)stopRecordVideoWithCompletion:(RecordVideoResult)completion {
@weakify(self);
[self.movieWriter finishRecordingWithCompletionHandler:^{
@strongify(self);
[self removeMovieWriter];
if (completion) {
completion(self.currentTmpVideoPath);
}
}];
}

2、多段录制

在 GPUImage 中并没有提供多段录制的功能,需要我们自己去实现。

首先,我们要重复单段视频的录制过程,这样我们就有了多段视频的文件路径。然后主要实现两个功能,一个是 AVPlayer 的多段视频循环播放;另一个是通过 AVComposition 来合并多段视频,并用 AVAssetExportSession 来导出新的视频。

整个过程逻辑并不复杂,出于篇幅的考虑,代码就不贴了,请到项目中查看。

三、保存

在拍照或者录视频结束后,通过 PhotoKit 保存到相册里。

1、保存图片

- (void)writeImageToSavedPhotosAlbum:(UIImage *)image
completion:(void (^)(BOOL success))completion {
[[PHPhotoLibrary sharedPhotoLibrary] performChanges:^{
[PHAssetChangeRequest creationRequestForAssetFromImage:image];
} completionHandler:^(BOOL success, NSError * _Nullable error) {
if (completion) {
completion(success);
}
}];
}

2、保存视频

- (void)saveVideo:(NSString *)path completion:(void (^)(BOOL success))completion {
NSURL *url = [NSURL fileURLWithPath:path];
[[PHPhotoLibrary sharedPhotoLibrary] performChanges:^{
[PHAssetChangeRequest creationRequestForAssetFromVideoAtFileURL:url];
} completionHandler:^(BOOL success, NSError * _Nullable error) {
dispatch_async(dispatch_get_main_queue(), ^{
if (completion) {
completion(success);
}
});
}];
}

四、闪光灯

系统的闪光灯类型通过 AVCaptureDevice 的 flashMode 属性来控制,其实只有三种,分别是:

  • AVCaptureFlashModeOff 关闭
  • AVCaptureFlashModeOn 开启(在拍照的时候会闪一下)
  • AVCaptureFlashModeAuto 自动(系统会自动判断当前的环境是否需要闪光灯)

但是市面上的相机应用,一般还有一种常亮类型,这种类型在夜间的时候会比较适用。这个功能需要通过 torchMode 属性来实现,它其实是指手电筒。

我们对这个两个属性做一下封装,允许这四种类型来回切换,下面是根据封装的类型来同步系统类型的代码:

- (void)syncFlashState {
AVCaptureDevice *device = self.camera.inputCamera;
if (![device hasFlash] || self.camera.cameraPosition == AVCaptureDevicePositionFront) {
[self closeFlashIfNeed];
return;
}

[device lockForConfiguration:nil];

switch (self.flashMode) {
case SCCameraFlashModeOff:
device.torchMode = AVCaptureTorchModeOff;
device.flashMode = AVCaptureFlashModeOff;
break;
case SCCameraFlashModeOn:
device.torchMode = AVCaptureTorchModeOff;
device.flashMode = AVCaptureFlashModeOn;
break;
case SCCameraFlashModeAuto:
device.torchMode = AVCaptureTorchModeOff;
device.flashMode = AVCaptureFlashModeAuto;
break;
case SCCameraFlashModeTorch:
device.torchMode = AVCaptureTorchModeOn;
device.flashMode = AVCaptureFlashModeOff;
break;
default:
break;
}

[device unlockForConfiguration];
}


另外,由于前置摄像头不支持闪光灯,如果在前置的时候去切换闪光灯,只能修改我们封装的类型。所以在切换到后置的时候,需要去同步一下系统的闪光灯类型:


五、画幅比例

相机的比例通过设置 AVCaptureSession 的 sessionPreset 属性来实现。这个属性并不只意味着比例,也意味着分辨率。

由于不是所有的设备都支持高分辨率,所以这里只使用 AVCaptureSessionPreset640x480 和 AVCaptureSessionPreset1280x720 这两个分辨率,分别用来作为 3:4 和 9:16 的输出。

市面上的相机除了上面的两个比例外,一般还支持 1:1 和 Full (iPhoneX 系列的全屏)比例,但是系统并没有提供对应比例的 AVCaptureSessionPreset 。

这里可以通过 GPUImageCropFilter 来实现,这是 GPUImage 的一个内置滤镜,可以对输入的纹理进行裁剪。使用时通过 cropRegion 属性来传入一个归一化的裁剪区域。

切换比例的关键代码如下:

- (void)setRatio:(SCCameraRatio)ratio {
_ratio = ratio;

CGRect rect = CGRectMake(0, 0, 1, 1);
if (ratio == SCCameraRatio1v1) {
self.camera.captureSessionPreset = AVCaptureSessionPreset640x480;
CGFloat space = (4 - 3) / 4.0; // 竖直方向应该裁剪掉的空间
rect = CGRectMake(0, space / 2, 1, 1 - space);
} else if (ratio == SCCameraRatio4v3) {
self.camera.captureSessionPreset = AVCaptureSessionPreset640x480;
} else if (ratio == SCCameraRatio16v9) {
self.camera.captureSessionPreset = AVCaptureSessionPreset1280x720;
} else if (ratio == SCCameraRatioFull) {
self.camera.captureSessionPreset = AVCaptureSessionPreset1280x720;
CGFloat currentRatio = SCREEN_HEIGHT / SCREEN_WIDTH;
if (currentRatio > 16.0 / 9.0) { // 需要在水平方向裁剪
CGFloat resultWidth = 16.0 / currentRatio;
CGFloat space = (9.0 - resultWidth) / 9.0;
rect = CGRectMake(space / 2, 0, 1 - space, 1);
} else { // 需要在竖直方向裁剪
CGFloat resultHeight = 9.0 * currentRatio;
CGFloat space = (16.0 - resultHeight) / 16.0;
rect = CGRectMake(0, space / 2, 1, 1 - space);
}
}
[self.currentFilterHandler setCropRect:rect];
self.videoSize = [self videoSizeWithRatio:ratio];
}

六、前后置切换

通过调用 GPUImageVideoCamera 的 rotateCamera 方法来实现。

- (void)rotateCamera {
[self.camera rotateCamera];
// 切换摄像头,同步一下闪光灯
[self syncFlashState];
}


七、对焦

AVCaptureDevice 的 focusMode 用来设置聚焦模式,focusPointOfInterest 用来设置聚焦点;exposureMode 用来设置曝光模式,exposurePointOfInterest 用来设置曝光点。

前置摄像头只支持设置曝光,后置摄像头支持设置曝光和聚焦,所以在设置之前要先判断是否支持。

需要注意的是,相机默认输出的图像是横向的,图像向右偏转。而前置摄像头又是镜像,所以图像是向左偏转。我们从 UIView 获得的触摸点,要经过相应的转化,才是正确的坐标。关键代码如下:

- (void)setFocusPoint:(CGPoint)focusPoint {
_focusPoint = focusPoint;

AVCaptureDevice *device = self.camera.inputCamera;

// 坐标转换
CGPoint currentPoint = CGPointMake(focusPoint.y / self.outputView.bounds.size.height, 1 - focusPoint.x / self.outputView.bounds.size.width);
if (self.camera.cameraPosition == AVCaptureDevicePositionFront) {
currentPoint = CGPointMake(currentPoint.x, 1 - currentPoint.y);
}

[device lockForConfiguration:nil];

if ([device isFocusPointOfInterestSupported] &&
[device isFocusModeSupported:AVCaptureFocusModeAutoFocus]) {
[device setFocusPointOfInterest:currentPoint];
[device setFocusMode:AVCaptureFocusModeAutoFocus];
}
if ([device isExposurePointOfInterestSupported] &&
[device isExposureModeSupported:AVCaptureExposureModeAutoExpose]) {
[device setExposurePointOfInterest:currentPoint];
[device setExposureMode:AVCaptureExposureModeAutoExpose];
}

[device unlockForConfiguration];
}

八、改变焦距

改变焦距简单来说就是画面的放大缩小,通过设置 AVCaptureDevice 的 videoZoomFactor 属性实现。

值得注意的是,这个属性有最大值和最小值,设置之前需要做好判断,否则会直接崩溃。代码如下:

- (void)setVideoScale:(CGFloat)videoScale {
_videoScale = videoScale;

videoScale = [self availableVideoScaleWithScale:videoScale];

AVCaptureDevice *device = self.camera.inputCamera;
[device lockForConfiguration:nil];
device.videoZoomFactor = videoScale;
[device unlockForConfiguration];
}

- (CGFloat)availableVideoScaleWithScale:(CGFloat)scale {
AVCaptureDevice *device = self.camera.inputCamera;

CGFloat maxScale = kMaxVideoScale;
CGFloat minScale = kMinVideoScale;
if (@available(iOS 11.0, *)) {
maxScale = device.maxAvailableVideoZoomFactor;
}

scale = MAX(scale, minScale);
scale = MIN(scale, maxScale);

return scale;
}

九、滤镜

1、滤镜的使用

当我们想使用一个滤镜的时候,只需要把它加到滤镜链里去,通过 addTarget: 方法实现。来看一下这个方法的定义:

- (void)addTarget:(id<GPUImageInput>)newTarget;

可以看到,只要实现了 GPUImageInput 协议,就可以成为滤镜链的下一个结点。

2、美颜滤镜

目前美颜效果已经成为相机应用的标配,我们也来给自己的相机加上美颜的效果。

美颜效果本质上是对图片做模糊,想要达到比较好的效果,需要结合人脸识别,只对人脸的部分进行模糊处理。这里并不去探究美颜算法的实现,直接找开源的美颜滤镜来用。

目前找到的实现效果比较好的是 LFGPUImageBeautyFilter ,虽然它的效果肯定比不上现在市面上的美颜类 APP,但是作为学习级别的 Demo 已经足够了。


3、自定义滤镜

打开 GPUImageFilter 的头文件,可以看到有下面这个方法:

- (id)initWithVertexShaderFromString:(NSString *)vertexShaderString 
fragmentShaderFromString:(NSString *)fragmentShaderString;

很容易理解,通过一个顶点着色器和一个片段着色器来初始化,并且可以看到是字符串类型。

另外,GPUImageFilter 中还内置了简单的顶点着色器和片段着色器,顶点着色器代码如下:

NSString *const kGPUImageVertexShaderString = SHADER_STRING
(
attribute vec4 position;
attribute vec4 inputTextureCoordinate;

varying vec2 textureCoordinate;

void main()
{
gl_Position = position;
textureCoordinate = inputTextureCoordinate.xy;
}
);

这里用到了 SHADER_STRING 宏,看一下它的定义:

#define STRINGIZE(x) #x
#define STRINGIZE2(x) STRINGIZE(x)
#define SHADER_STRING(text) @ STRINGIZE2(text)

在 #define 中的 # 是「字符串化」的意思,返回 C 语言风格字符串,而 SHADER_STRING 在字符串前面加了一个 @符号,则 SHADER_STRING 的定义就是将括号中的内容转化为 OC 风格的字符串。

我们之前都是为着色器代码单独创建两个文件,而在 GPUImageFilter 中直接以字符串的形式,写死在代码中,两种方式本质上没什么区别。

当我们想自定义一个滤镜的时候,只需要继承 GPUImageFilter 来定义一个子类,然后用相同的方式来定义两个保存着色器代码的字符串,并且用这两个字符串来初始化子类就可以了。

总结

通过上面的步骤,我们实现了一个具备基础功能的相机。之后会在这个相机的基础上,继续做一些有趣的尝试,欢迎持续关注~


作者:那月无痕
链接:https://www.jianshu.com/p/42f841051337


收起阅读 »

iOS 超隐匿的开发技巧 !!!

1、递归查看 view 的子视图(私有方法,没有代码提示)[self.view recursiveDescription]2、过滤字符串的特殊字符// 定义一个特殊字符的集合 NSCharacterSet *set = [NSCharacterSet char...
继续阅读 »

1、递归查看 view 的子视图(私有方法,没有代码提示)

[self.view recursiveDescription]
2、过滤字符串的特殊字符
// 定义一个特殊字符的集合
NSCharacterSet *set = [NSCharacterSet characterSetWithCharactersInString:
@"@/:;()¥「」"、[]{}#%-*+=_\\|~<>$€^•'@#$%^&*()_+'\""];
// 过滤字符串的特殊字符
NSString *newString = [trimString stringByTrimmingCharactersInSet:set];

3、Transform 属性

//平移按钮
CGAffineTransform transForm = self.buttonView.transform;
self.buttonView.transform = CGAffineTransformTranslate(transForm, 10, 0);

//旋转按钮
CGAffineTransform transForm = self.buttonView.transform;
self.buttonView.transform = CGAffineTransformRotate(transForm, M_PI_4);

//缩放按钮
self.buttonView.transform = CGAffineTransformScale(transForm, 1.2, 1.2);

//初始化复位
self.buttonView.transform = CGAffineTransformIdentity;

4、去掉分割线多余15pt

首先在viewDidLoad方法加入以下代码:
if ([self.tableView respondsToSelector:@selector(setSeparatorInset:)]) {
[self.tableView setSeparatorInset:UIEdgeInsetsZero];
}
if ([self.tableView respondsToSelector:@selector(setLayoutMargins:)]) {
[self.tableView setLayoutMargins:UIEdgeInsetsZero];
}
然后在重写willDisplayCell方法
- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell
forRowAtIndexPath:(NSIndexPath *)indexPath{
if ([cell respondsToSelector:@selector(setSeparatorInset:)]) {
[cell setSeparatorInset:UIEdgeInsetsZero];
}
if ([cell respondsToSelector:@selector(setLayoutMargins:)]) {
[cell setLayoutMargins:UIEdgeInsetsZero];
}
}

5、计算耗时方法时间间隔

// 获取时间间隔
#define TICK CFAbsoluteTime start = CFAbsoluteTimeGetCurrent();
#define TOCK NSLog(@"Time: %f", CFAbsoluteTimeGetCurrent() - start)

6、Color颜色宏定义

// 随机颜色
#define RANDOM_COLOR [UIColor colorWithRed:arc4random_uniform(256) / 255.0 green:arc4random_uniform(256) / 255.0 blue:arc4random_uniform(256) / 255.0 alpha:1]
// 颜色(RGB)
#define RGBCOLOR(r, g, b) [UIColor colorWithRed:(r)/255.0f green:(g)/255.0f blue:(b)/255.0f alpha:1]
// 利用这种方法设置颜色和透明值,可不影响子视图背景色
#define RGBACOLOR(r, g, b, a) [UIColor colorWithRed:(r)/255.0f green:(g)/255.0f blue:(b)/255.0f alpha:(a)]

7、退出iOS应用

- (void)exitApplication {
AppDelegate *app = [UIApplication sharedApplication].delegate;
UIWindow *window = app.window;

[UIView animateWithDuration:1.0f animations:^{
window.alpha = 0;
} completion:^(BOOL finished) {
exit(0);
}];
}

8、NSArray 快速求总和 最大值 最小值 和 平均值

NSArray *array = [NSArray arrayWithObjects:@"2.0", @"2.3", @"3.0", @"4.0", @"10", nil];
CGFloat sum = [[array valueForKeyPath:@"@sum.floatValue"] floatValue];
CGFloat avg = [[array valueForKeyPath:@"@avg.floatValue"] floatValue];
CGFloat max =[[array valueForKeyPath:@"@max.floatValue"] floatValue];
CGFloat min =[[array valueForKeyPath:@"@min.floatValue"] floatValue];
NSLog(@"%f\n%f\n%f\n%f",sum,avg,max,min);

9、Debug栏打印时自动把Unicode编码转化成汉字

 DXXcodeConsoleUnicodePlugin 插件

10、自动生成模型代码的插件

ESJsonFormat-for-Xcode

12、隐藏导航栏上的返回字体

//Swift
UIBarButtonItem.appearance().setBackButtonTitlePositionAdjustment(UIOffsetMake(0, -60), forBarMetrics: .Default)
//OC
[[UIBarButtonItem appearance] setBackButtonTitlePositionAdjustment:UIOffsetMake(0, -60) forBarMetrics:UIBarMetricsDefault];

13、设置导航栏透明

//方法一:设置透明度
[[[self.navigationController.navigationBar subviews]objectAtIndex:0] setAlpha:0.1];
//方法二:设置背景图片
/**
* 设置导航栏,使其透明
*
*/

- (void)setNavigationBarColor:(UIColor *)color targetController:(UIViewController *)targetViewController{
//导航条的颜色 以及隐藏导航条的颜色targetViewController.navigationController.navigationBar.shadowImage = [[UIImage alloc]init];
CGRect rect=CGRectMake(0.0f, 0.0f, 1.0f, 1.0f); UIGraphicsBeginImageContext(rect.size);
CGContextRef context = UIGraphicsGetCurrentContext(); CGContextSetFillColorWithColor(context, [color CGColor]); CGContextFillRect(context, rect);
UIImage *theImage = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); [targetViewController.navigationController.navigationBar setBackgroundImage:theImage forBarMetrics:UIBarMetricsDefault];
}

14、解决同时按两个按钮进两个view的问题

[button setExclusiveTouch:YES];

15、修改 textFieldplaceholder 字体颜色和大小

  textField.placeholder = @"请输入手机号码";
[textField setValue:[UIColor redColor] forKeyPath:@"_placeholderLabel.textColor"];
[textField setValue:[UIFont boldSystemFontOfSize:13] forKeyPath:@"_placeholderLabel.font"];

16、UIImage 与字符串互转

//图片转字符串  
-(NSString *)UIImageToBase64Str:(UIImage *) image
{
NSData *data = UIImageJPEGRepresentation(image, 1.0f);
NSString *encodedImageStr = [data base64EncodedStringWithOptions:NSDataBase64Encoding64CharacterLineLength];
return encodedImageStr;
}

//字符串转图片
-(UIImage *)Base64StrToUIImage:(NSString *)_encodedImageStr
{
NSData *_decodedImageData = [[NSData alloc] initWithBase64Encoding:_encodedImageStr];
UIImage *_decodedImage = [UIImage imageWithData:_decodedImageData];
return _decodedImage;
}



收起阅读 »

自定义 UITableView 的 Cell 删除样式

一、需求先说下我们的需求,在一个 tableView 中,左滑删除某个 cell 时,需要展示如下图所示的样式,浅灰色底色,橘红色 文字。1、修改删除按钮的文字修改删除按钮的文字很简单,只需要实现下面的方法:/...
继续阅读 »

一、需求

先说下我们的需求,在一个 tableView 中,左滑删除某个 cell 时,需要展示如下图所示的样式,浅灰色底色,橘红色 文字。



1、修改删除按钮的文字

修改删除按钮的文字很简单,只需要实现下面的方法:

//修改编辑按钮文字
- (NSString *)tableView:(UITableView *)tableView titleForDeleteConfirmationButtonForRowAtIndexPath:(NSIndexPath *)indexPath {
return @"Delete";
}

2、修改删除按钮的背景颜色和文字颜色

首先我按照常规的在 editActionsForRowAtIndexPath 方法中处理:

- (NSArray*)tableView:(UITableView *)tableView editActionsForRowAtIndexPath:(nonnull NSIndexPath *)indexPath {
// delete action
UITableViewRowAction *deleteAction = [UITableViewRowAction rowActionWithStyle:UITableViewRowActionStyleNormal title:@"Delete" handler:^(UITableViewRowAction *action, NSIndexPath *indexPath) {

}];
deleteAction.backgroundColor = [UIColor colorWithHexString:Color_F7F7F7];
return @[deleteAction];
}


但发现只能通过 deleteAction.backgroundColor 改变背景颜色,却无法改变字体颜色。
而系统提供的几种 UITableViewRowActionStyle 也不符合我的需求:

typedef NS_ENUM(NSInteger, UITableViewRowActionStyle) {
UITableViewRowActionStyleDefault = 0,
UITableViewRowActionStyleDestructive = UITableViewRowActionStyleDefault,
UITableViewRowActionStyleNormal
}

默认 UITableViewRowActionStyleDefault 的样子:


UITableViewRowActionStyleNormal 的样子:



二、解决办法

解决办法有我从网上找来的,最新的 iOS12 的则是我自己试验出来的。解决办法也会随着 iOS 系统的升级而发生变化,因为系统控件的结构可能会发生变化,导致遍历不出要找到的视图。

1、系统版本 < iOS 11 的处理方式

iOS11 以前的处理方式是遍历 Cell 的 subViews 子视图找到 UITableViewCellDeleteConfirmationView ,只需在 Cell 的 .m 文件中添加 layoutSubviews 代码:


- (void)layoutSubviews {
[super layoutSubviews];
for (UIView *subView in self.subviews) {
if ([NSStringFromClass([subView class]) isEqualToString:@"UITableViewCellDeleteConfirmationView"]) {

UIButton *bgView = (UIButton *)[subView.subviews firstObject];
bgView.backgroundColor = [UIColor colorWithHexString:Color_F0F0F0];
[bgView setTitleColor:[UIColor colorWithHexString:Color_MainColor] forState:UIControlStateNormal];
}
}
}

2、iOS 11 <= 系统版本 < iOS 13 的处理方式

这个系统版本需要在 tableView 中添加 layoutSubviews,我是写了一个 tableView 的父类,在父类的 .m 中添加了 layoutSubviews。同时我还在 tableView 的 .h 中声明一个 cellHeightRef 来修改删除 Button 的高度。

  • 简单来说就是在 tableView 的 subviews 中遍历出 UISwipeActionPullView,再从 UISwipeActionPullView 中遍历出 UISwipeActionStandardButton。再修改 button 的样式即可。

- (void)layoutSubviews {
[super layoutSubviews];
for (UIView *subview in self.subviews) {
if ([subview isKindOfClass:NSClassFromString(@"UISwipeActionPullView")]) {
//修改背景颜色
subview.backgroundColor = [UIColor clearColor];
//修改按钮-颜色
UIButton *swipeActionStandardBtn = subview.subviews[0];
if ([swipeActionStandardBtn isKindOfClass:NSClassFromString(@"UISwipeActionStandardButton")]) {
CGFloat swipeActionStandardBtnOX = swipeActionStandardBtn.frame.origin.x;
CGFloat swipeActionStandardBtnW = swipeActionStandardBtn.frame.size.width;
swipeActionStandardBtn.frame = CGRectMake(swipeActionStandardBtnOX, 0, swipeActionStandardBtnW, self.cellHeightRef - 10);

[swipeActionStandardBtn setTitleColor:[UIColor colorWithHexString:Color_MainColor] forState:UIControlStateNormal];
[swipeActionStandardBtn setTitleColor:[UIColor whiteColor] forState:UIControlStateHighlighted];
[swipeActionStandardBtn setTitleColor:[UIColor whiteColor] forState:UIControlStateSelected];
}
}
}
}

3、系统版本 == iOS 13 的处理方式(大于 13 的还未知,等出了新的我再更新)

iOS 13 中和上面的 iOS 13 之前的方法几乎一样,只不过是 tableView 内部的父子视图关系发生了变化, UISwipeActionStandardButton 位置变了。即原来把 subView 改为了 subview.subviews.firstObject,才能得到 UISwipeActionStandardButton


- (void)layoutSubviews {
[super layoutSubviews];
for (UIView *subview in self.subviews) {
if ([subview.subviews.firstObject isKindOfClass:NSClassFromString(@"UISwipeActionPullView")]) {
//修改背景颜色
subview.subviews.firstObject.backgroundColor = [UIColor clearColor];
//修改按钮-颜色
UIButton *swipeActionStandardBtn = subview.subviews.firstObject.subviews[0];
if ([swipeActionStandardBtn isKindOfClass:NSClassFromString(@"UISwipeActionStandardButton")]) {
CGFloat swipeActionStandardBtnOX = swipeActionStandardBtn.frame.origin.x;
CGFloat swipeActionStandardBtnW = swipeActionStandardBtn.frame.size.width;
swipeActionStandardBtn.frame = CGRectMake(swipeActionStandardBtnOX, 0, swipeActionStandardBtnW, self.cellHeightRef - 10);

[swipeActionStandardBtn setTitleColor:[UIColor colorWithHexString:Color_MainColor] forState:UIControlStateNormal];
[swipeActionStandardBtn setTitleColor:[UIColor whiteColor] forState:UIControlStateHighlighted];
[swipeActionStandardBtn setTitleColor:[UIColor whiteColor] forState:UIControlStateSelected];
}
}
}
}

作者:凡几多
链接:https://www.jianshu.com/p/7bfa622cf4dd




收起阅读 »

NSMutableString 不要用 copy

疑问:我们都知道 copy 一般用来修饰 有对应可变类型的不可变对象上,比如 NSString,NSArray 和 NSDictionary。那么为什么不推荐用 copy 去修饰&...
继续阅读 »

疑问:

我们都知道 copy 一般用来修饰 有对应可变类型的不可变对象上,比如 NSString,NSArray 和 NSDictionary。那么为什么不推荐用 copy 去修饰 NSMutableString 和 NSMutableArray 而是用 strong 呢?

测试:

平时没怎么关注过这个问题,那么我就来测试一下。

一、先测试一下为什么 NSString 要用 copy

首先定义两个字符串属性,一个 strong 一个 copy

用 strong 修饰
@property (nonatomic, strong) NSString *str_strong;

用 copy 修饰
@property (nonatomic, copy) NSString *str_copy;
1、用可变字符串 NSMutableString 赋值:
- (void)viewDidLoad {
[super viewDidLoad];
NSMutableString *mutString = [[NSMutableString alloc] initWithFormat:@"原可变字符串"];
// 赋值
self.str_strong = mutString;
self.str_copy = mutString;
// 追加字符串
[mutString appendString:@"+++追加字符串"];

NSLog(@"\n mutString: %@, %p, %p \n str_strong: %@, %p, %p \n str_copy: %@, %p, %p \n" , mutString, mutString, &mutString, self.str_strong, _str_strong, &_str_strong, self.str_copy, _str_copy, &_str_copy);
}
打印结果如下:
 mutString: 原可变字符串+++追加字符串, 0x2828ab6f0, 0x16f027af8 
str_strong: 原可变字符串+++追加字符串, 0x2828ab6f0, 0x127f0cab0
str_copy: 原可变字符串, 0x2828ab8a0, 0x127f0cab8

可以看出 str_strong 和 mutString 指向对象内存是一样的,因为 strong 是 浅拷贝(指针拷贝),他们指向的都是同一个对象,地址没有变化,值当然也就一样了。
str_copy 指向对象的内存地址和他们不一样,因为 str_copy 对象使用的 copy 深拷贝,是一个新的对象,开辟了新的内存地址,不用以前的地址。

2、用不可变字符串 NSString 赋值:
- (void)viewDidLoad {
[super viewDidLoad];
NSString *str = [[NSString alloc] initWithFormat:@"不可变字符串"];
//进行赋值
self.str_strong = str;
self.str_copy = str;
NSLog(@"\n str: %@, %p, %p \n str_strong: %@, %p, %p \n str_copy: %@, %p, %p \n" , str, str, &str, self.str_strong, _str_strong, &_str_strong, self.str_copy, _str_copy, &_str_copy);
}

打印结果如下:

 str: 不可变字符串, 0x283d55860, 0x16fbcbaf8 
str_strong: 不可变字符串, 0x283d55860, 0x100a15bf0
str_copy: 不可变字符串, 0x283d55860, 0x100a15bf8

通过打印结果可以看出,str、str_strong 和 str_copy 这三者指向对象内存一样,不管是 strong 还是 copy 在这里都进行了 浅拷贝,没有重新开辟新的空间,因为这回的str 是 NSString,是不可变的。

所以一般我们是不希望我们创建的 NSString 字符串跟着之后的赋值 mutString 变化而变化的,所以都用 copy 。当然如果你希望字串的值跟着 mutString 变化,也可以使用 strong
但是,如果你创建的是 NSMutableString,那么不要用 copy

二、NSMutableString 不要用 copy

我们使用 NSMutableString 肯定是想用字符串的可变这个属性,但如果你用 copy 去修饰,那么生成的将会是不可变的,当你去调用可变方法时,程序将会崩溃!
测试:

用 copy 修饰 NSMutableString
@property (nonatomic, copy) NSMutableString *mutstr_copy;



同理,也不要对 NSMutableArray 和 NSMutableDictionary 使用 copy 修饰,不然也有可能出现崩溃。

三、总结

  • 1、当原字符串是 NSString ,即不可变字符串时,不管是 strong 还是 copy 属性的对象,都指向原对象,copy操作也只是做了浅拷贝。

  • 2、当原字符串是 NSMutableString 时,即可变字符串时,strong 属性只是增加了原字符串的引用计数,而 copy 属性则是对原字符串做了次深拷贝,产生一个新的对象,且 copy 属性对象指向这个新的对象,且这个 copy 属性对象的类型始终是 NSString,而不是NSMutableString,因此其是不可变的,这时候调用可变操作,将会造成崩溃!

  • 3、 因为 NSMutableString 是 NSString 的子类,父类指针可以指向子类对象,使用 copy 的目的是为了让本对象的属性不受外界影响,这样无论给我传入是一个可变对象还是不可变对象,我本身持有的就是一个不可变的副本,这样更安全。

所以,在声明 NSString 属性时,一般我们都不希望它改变,所以大多数情况下,我们建议用 copy,以免因可变字符串的修改导致的一些非预期问题。而在声明 NSMutableString 则需要使用 strong

举一反三:

把 NSMutableArray 用 copy 修饰有时就会崩溃,因为对这个数组进行了增删改操作,而 copy 后的数组变成了不可变数组 NSArray ,没有响应的增删改方法,所以就崩溃了。

  • 当修饰可变类型的属性时,如 NSMutableArray、NSMutableDictionary、NSMutableString,用 strong
    当修饰不可变类型的属性时,如 NSArray、NSDictionary、NSString,用 copy


作者:凡几多
链接:https://www.jianshu.com/p/5d138efee024

收起阅读 »

【环信MQTT消息云集成指南】常见问题整理

业务开通1. 注册后无法开通MQTT业务问题描述:在注册账户后,登录console控制台,选择【应用列表】中的某一个App,无法开通MQTT业务。 问题原因:(1)使用系统提供的默认demo开通,目前该demo暂不支持开通MQTT业务; (...
继续阅读 »

业务开通

1. 注册后无法开通MQTT业务

问题描述:
在注册账户后,登录console控制台,选择【应用列表】中的某一个App,无法开通MQTT业务。 

问题原因:
(1)使用系统提供的默认demo开通,目前该demo暂不支持开通MQTT业务; 
(2)在console控制台【添加应用】时,数据中心默认为“国内2区”,需要更改为“国内1区”; 


解决方案:
新建应用时,数据中心选择“国内一区”。

客户端集成 

 1.哪些开发语言支持集成MQTT客户端

问题描述:
都有哪些开发语言支持集成MQTT客户端?
解决方案:
MQTT协议属于标准协议,目前已支持多种开发语言集成,包括:Java、iOS、Android、JavaScript、C、Node.js等等。同时集成MQTT客户端可支持两种方式:
(1)选择开源的MQTT SDK:社区提供多种开源的MQTT 客户端SDK,环信已将下载链接整理如下,可根据需要下载。
客户端SDK下载
(2)基于标准协议进行开发:基于标准的协议开发MQTT客户端,也可无差异对接MQTT服务器。

 2.小程序是否支持集成MQTT客户端 

问题描述:
小程序(微信小程序、支付宝小程序)是否支持集成MQTT客户端?
解决方案:
支持的,目前微信及支付宝小程序都支持集成MQTT客户端,可选择使用mqtt.min.js SDK。 
集成步骤如下: 
步骤1:选择mqtt.min.js开源的SDK,目前测试只有4.2.1之前的版本小程序端可以使用。 
步骤2:配置域名地址,在【微信/支付宝开发者平台】->【开发】->【开发管理】->【开发设置】->【服务域名配置】部分配置获取token的域名地址和服务器连接的域名地址。 
(1)服务器连接的域名地址:在console后台【MQTT】->【服务概览】->【服务配置】下的【连接地址】获取; 
(2)获取token域名地址:在console后台【即时通讯】->【服务概览】->【服务版本】下的【REST API访问地址】获取; 
步骤3:获取orgname和appname:appkey由orgname#appname组成,例如“1145210806152081#demo”,orgname为“1145210806152081”,appname为“demo”; 
步骤4:设置用户登录账户,在console后台【应用概览】->【用户认证】下创建账户信息。 
步骤5:获取上述参数,配置到环信小程序demo中的相应字段上,即可成功连接服务器实现消息通信。 下载微信小程序demo下载支付宝小程序demo

3.安卓端如何集成MQTT客户端

问题描述:
安卓端如何集成MQTT客户端?
解决方案:
环信已提供安卓端使用的MQTT demo,下载链接: 安卓端demo

4.iOS端如何集成MQTT客户端 

问题描述:
iOS端如何集成MQTT客户端?
解决方案:
环信已提供iOS端使用的MQTT demo,下载链接:iOS端demo

 5.Java端如何集成MQTT客户端 

问题描述:
Java端如何集成MQTT客户端?
解决方案:
环信已提供Java端使用的MQTT demo,下载链接:Java端demo

6.Web端如何集成MQTT客户端 

问题描述:
Web端如何集成MQTT客户端?
解决方案:
环信已提供Web端使用的MQTT demo,下载链接:Web端demo

 服务器端集成

1.服务器端如何集成MQTT服务

问题描述:
服务器端如何集成MQTT服务?
解决方案:
为实现客户服务器、环信MQTT消息云及客户终端之间进行消息交互,不仅提供了客户终端使用的SDK,同时提供了服务器端集成方法,包括REST API方式服务器端SDK。 
(1)REST API方法 

(2)服务器端SDK下载

客户端连接

1.开通后无法连接MQTT消息云

问题描述:
在创建APP后,通过console后台获取的参数,无法连接MQTT消息云。 
问题原因:
(1)认证鉴权信息填写错误:
a)如果使用MQTT桌面客户端连接,登录密码直接使用token内容。token在【应用概览】->【用户认证】页面,选择相应账户后的【查看token】按钮获取;
b)如果使用代码连接,需要通过【应用概览】->【用户认证】->【用户ID】列表中的username/password先获取token,在通过token进行认证连接; 
(2)clientID填写错误:
a)clientID由两部分组成,形式为“deviceID@AppID”,其中deviceID由用户自定义,AppID通过【服务概览】->【服务配置】中获取。 例:“0023B8@ipd7a0”;
b)部分MQTT客户端会在clientID后自动添加时间戳,需要在连接时,将Append timestamp to MQTT client id置false; 
解决方案:
MQTT消息云连接需要5个基本参数,包括连接地址(Host)、端口(Port)、clientID(MQTT client ID)、用户ID(Username)、token(Password)
以下图为例,连接5个参数为: 
连接地址:aitbj0.cn1.mqtt.chat 
端口:1883(mqtt) 
clientID:0023B8@aitbj0(自定义@AppID) 
Username:test 
token:相应用户ID后的【查看token】内容




 2.MQTT客户端连接消息云时,登录密码是选择用户密码还是token? 

问题描述:
MQTT客户端连接消息云时,登录密码是选择用户密码还是token值。 
问题原因:
取决于使用者是否可以对MQTT客户端连接过程进行修改。 
(1)如果可以修改,则需要先编写获取token流程,然后使用获取的token进行连接登录。当获取token时,会使用用户名、用户密码、org_name、app_name以及token域名等信息;
(2)如果不可以修改,则直接使用token进行连接登录; 
解决方案:
确定使用的MQTT客户端: 
(1)如果使用MQTT桌面客户端,比如MQTT Broker、MQTT BOX等,登录密码直接使用token内容。token在【应用概览】->【用户认证】页面,选择相应账户后的【查看token】按钮获取;
(2)如果使用代码连接,需要通过【应用概览】->【用户认证】->【用户ID】列表中的用户名和用户密码请求token,在通过获取到的token进行认证连接; 

3.MQTT客户端反复出现断开重连现象 

问题描述:
MQTT客户端反复出现断开重连现象。 
问题原因:
(1)在配置参数时,设置了断开重连模式; 
(2)存在两个或两个以上的客户端使用相同的clientid登录,他们会互相踢对方,如果设置成自动重连机制,将会无限循环断开重连; 
解决方案:
确保当前APP下每个clientID的唯一性。

 4.用户名存在100个限制怎么办?

问题描述:
在【应用概览】->【用户认证】下创建用户会有100个 数量的限制,如果登录clientID超过100个怎么办? 
问题原因:
(1)MQTT消息云以clientID作为唯一标识,不同的clientID可以使用相同的用户信息进行登录,因此100个用户信息可以复用;
(2)如果客户需要集成IM、Push等其他的业务,可以联系商务进行版本升级,提高创建用户数量; 
解决方案:
复用用户账户,不同的clientID可以使用相同的用户信息进行登录。

 5.登录报错信息Server has closed connection without connack. 

问题描述:
在MQTT客户端连接时,服务器返回‘Server has closed connection without connack.’ 
问题原因:
(1)MQTT客户端没有启用遗嘱消息(will:false),但是遗嘱消息的Qos等级设置为“1”,标准规定will为false时,willqos必须是0; 
解决方案:
将遗嘱消息的QoS等级设置为'0'。

异常排查

1.如何查看消息是否发送成功 

问题描述:
发布客户端发送消息后,订阅客户端一直收不到消息,如何确认消息是否发送成功?
问题原因:
消息接收不到,有以下可能原因: 
(1)发布客户端没有将消息发布出去; 
(2)订阅客户端没有接收到消息; 
解决方案:
(1)通过调用REST API中查询客户端消息发送&投递记录接口查询; 
(2)通过调用服务器端SDK中QueryMqttRecordMessageOfClient函数查询; 
(3)使用环信console后台中的【记录查询】→【消息记录查询】功能,输入发布/订阅客户端的clientID及相应时间段,即可查看上下行消息记录,定位问题出在发布客户端还是订阅客户端。 


 2.如何查看客户端的订阅关系

问题描述:
如何查看客户端的订阅关系?
解决方案:
(1)通过调用REST API中查询客户端session信息接口查询; 
(2)通过调用服务器端SDK中QuerySessionByClientId函数查询; 
(3)使用环信console后台中的【记录查询】→【设备状态查询】功能,输入待查询客户端的clientID,即可查看在线状态、登录用户ID以及订阅关系。 


3.如何查看客户端的连接记录 

问题描述:
如何查看客户端的连接记录?
解决方案:
(1)通过调用REST API中查询客户端连接记录接口查询; 
(2)通过调用服务器端SDK中QueryMqttRecordDevice函数查询; 
(3)使用环信console后台中的【记录查询】→【设备记录查询】功能,输入待查询客户端的clientID及相应时间段,即可查看客户端连接、订阅以及断开连接等记录。 

 

4.如何查看客户端是否在线 

问题描述:
如何查看客户端是否在线?
解决方案:
(1)通过调用REST API中查询客户端session信息接口查询; 
(2)通过调用服务器端SDK中QuerySessionByClientId函数查询; 
(3)使用环信console后台中的【记录查询】→【设备状态查询】功能,输入待查询客户端的clientID,即可查看在线状态、登录用户ID以及订阅关系。 

收起阅读 »

学会这个,我的http加载速度更快了!

1. 前言 说到 HTTP 怎么提升网络加载速度,就不得不聊一聊 HTTP/2 了。 HTTP/2 的主要目标是通过支持完整的请求与响应复用来减少延迟,通过有效压缩 HTTP 标头字段将协议开销降至最低,同时增加对请求优先级和服务器推送的支持。 HTTP/2 ...
继续阅读 »

1. 前言


说到 HTTP 怎么提升网络加载速度,就不得不聊一聊 HTTP/2 了。


HTTP/2 的主要目标是通过支持完整的请求与响应复用来减少延迟,通过有效压缩 HTTP 标头字段将协议开销降至最低,同时增加对请求优先级和服务器推送的支持。


HTTP/2 没有改动 HTTP 的应用语义。 HTTP 方法、状态代码、URI 和标头字段等核心概念一如往常。 不过,HTTP/2 修改了数据格式化(分帧)以及在客户端与服务器间传输的方式。这两点统帅全局,通过新的分帧层向我们的应用隐藏了所有复杂性。 因此,所有现有的应用都可以不必修改而在新协议下运行。


2. 二进制分帧层


HTTP/2 所有性能增强的核心在于新的二进制分帧层,它定义了如何封装 HTTP 消息并在客户端与服务器之间传输。


image.png


这里所谓的“层”,指的是位于套接字接口与应用可见的高级 HTTP API 之间一个经过优化的新编码机制: HTTP 的语义(包括各种动词、方法、标头)都不受影响,不同的是传输期间对它们的编码方式变了。 HTTP/1.x 协议以换行符作为纯文本的分隔符,而 HTTP/2 将所有传输的信息分割为更小的消息和帧,并采用二进制格式对它们编码。


3. 数据流、消息和帧


新的二进制分帧机制改变了客户端与服务器之间交换数据的方式。 为了说明这个过程,我们需要了解 HTTP/2 的三个概念:



  • 数据流: 已建立的连接内的双向字节流,可以承载一条或多条消息。

  • 消息: 与逻辑请求或响应消息对应的完整的一系列帧。

  • : HTTP/2 通信的最小单位,每个帧都包含帧头,至少也会标识出当前帧所属的数据流。


这些概念的关系总结如下:



  • 所有通信都在一个 TCP 连接上完成,此连接可以承载任意数量的双向数据流。

  • 每个数据流都有一个唯一的标识符和可选的优先级信息,用于承载双向消息。

  • 每条消息都是一条逻辑 HTTP 消息(例如请求或响应),包含一个或多个帧。

  • 帧是最小的通信单位,承载着特定类型的数据,例如 HTTP 标头、消息负载等等。 来自不同数据流的帧可以交错发送,然后再根据每个帧头的数据流标识符重新组装。


image.png


简言之,HTTP/2 将 HTTP 协议通信分解为二进制编码帧的交换,这些帧对应着特定数据流中的消息。所有这些都在一个 TCP 连接内复用。 这是 HTTP/2 协议所有其他功能和性能优化的基础。


4. 请求与响应复用


在 HTTP/1.x 中,如果客户端要想发起多个并行请求以提升性能,则必须使用多个 TCP 连接(请参阅使用多个 TCP 连接)。 这是 HTTP/1.x 交付模型的直接结果,该模型可以保证每个连接每次只交付一个响应(响应排队)。 更糟糕的是,这种模型也会导致队首阻塞,从而造成底层 TCP 连接的效率低下。


HTTP/2 中新的二进制分帧层突破了这些限制,实现了完整的请求和响应复用: 客户端和服务器可以将 HTTP 消息分解为互不依赖的帧,然后交错发送,最后再在另一端把它们重新组装起来。


image.png


快照捕捉了同一个连接内并行的多个数据流。 客户端正在向服务器传输一个 DATA 帧(数据流 5),与此同时,服务器正向客户端交错发送数据流 1 和数据流 3 的一系列帧。因此,一个连接上同时有三个并行数据流。


将 HTTP 消息分解为独立的帧,交错发送,然后在另一端重新组装是 HTTP 2 最重要的一项增强。事实上,这个机制会在整个网络技术栈中引发一系列连锁反应,从而带来巨大的性能提升,让我们可以:



  • 并行交错地发送多个请求,请求之间互不影响。

  • 并行交错地发送多个响应,响应之间互不干扰。

  • 使用一个连接并行发送多个请求和响应。

  • 不必再为绕过 HTTP/1.x 限制而做很多工作(请参阅针对 HTTP/1.x 进行优化,例如级联文件、image sprites 和域名分片。

  • 消除不必要的延迟和提高现有网络容量的利用率,从而减少页面加载时间。

  • 等等…


HTTP/2 中的新二进制分帧层解决了 HTTP/1.x 中存在的队首阻塞问题,也消除了并行处理和发送请求及响应时对多个连接的依赖。 结果,应用速度更快、开发更简单、部署成本更低。


5. 数据流优先级


将 HTTP 消息分解为很多独立的帧之后,我们就可以复用多个数据流中的帧,客户端和服务器交错发送和传输这些帧的顺序就成为关键的性能决定因素。 为了做到这一点,HTTP/2 标准允许每个数据流都有一个关联的权重和依赖关系:



  • 可以向每个数据流分配一个介于 1 至 256 之间的整数。

  • 每个数据流与其他数据流之间可以存在显式依赖关系。


数据流依赖关系和权重的组合让客户端可以构建和传递“优先级树”,表明它倾向于如何接收响应。 反过来,服务器可以使用此信息通过控制 CPU、内存和其他资源的分配设定数据流处理的优先级,在资源数据可用之后,带宽分配可以确保将高优先级响应以最优方式传输至客户端。


image.png


HTTP/2 内的数据流依赖关系通过将另一个数据流的唯一标识符作为父项引用进行声明;如果忽略标识符,相应数据流将依赖于“根数据流”。 声明数据流依赖关系指出,应尽可能先向父数据流分配资源,然后再向其依赖项分配资源。 换句话说,“请先处理和传输响应 D,然后再处理和传输响应 C”。


共享相同父项的数据流(即,同级数据流)应按其权重比例分配资源。 例如,如果数据流 A 的权重为 12,其同级数据流 B 的权重为 4,那么要确定每个数据流应接收的资源比例,请执行以下操作:



  1. 将所有权重求和: 4 + 12 = 16

  2. 将每个数据流权重除以总权重: A = 12/16, B = 4/16


因此,数据流 A 应获得四分之三的可用资源,数据流 B 应获得四分之一的可用资源;数据流 B 获得的资源是数据流 A 所获资源的三分之一。


我们来看一下上图中的其他几个操作示例。 从左到右依次为:



  1. 数据流 A 和数据流 B 都没有指定父依赖项,依赖于隐式“根数据流”;A 的权重为 12,B 的权重为 4。因此,根据比例权重: 数据流 B 获得的资源是 A 所获资源的三分之一。

  2. 数据流 D 依赖于根数据流;C 依赖于 D。 因此,D 应先于 C 获得完整资源分配。 权重不重要,因为 C 的依赖关系拥有更高的优先级。

  3. 数据流 D 应先于 C 获得完整资源分配;C 应先于 A 和 B 获得完整资源分配;数据流 B 获得的资源是 A 所获资源的三分之一。

  4. 数据流 D 应先于 E 和 C 获得完整资源分配;E 和 C 应先于 A 和 B 获得相同的资源分配;A 和 B 应基于其权重获得比例分配。


如上面的示例所示,数据流依赖关系和权重的组合明确表达了资源优先级,这是一种用于提升浏览性能的关键功能,网络中拥有多种资源类型,它们的依赖关系和权重各不相同。 不仅如此,HTTP/2 协议还允许客户端随时更新这些优先级,进一步优化了浏览器性能。 换句话说,我们可以根据用户互动和其他信号更改依赖关系和重新分配权重。


注: 数据流依赖关系和权重表示传输优先级,而不是要求,因此不能保证特定的处理或传输顺序。 即,客户端无法强制服务器通过数据流优先级以特定顺序处理数据流。 尽管这看起来违反直觉,但却是一种必要行为。 我们不希望在优先级较高的资源受到阻止时,还阻止服务器处理优先级较低的资源。


6. 每个来源一个连接


有了新的分帧机制后,HTTP/2 不再依赖多个 TCP 连接去并行复用数据流;每个数据流都拆分成很多帧,而这些帧可以交错,还可以分别设定优先级。 因此,所有 HTTP/2 连接都是永久的,而且仅需要每个来源一个连接,随之带来诸多性能优势。



SPDY 和 HTTP/2 的杀手级功能是,可以在一个拥塞受到良好控制的通道上任意进行复用。 这一功能的重要性和良好运行状况让我吃惊。 我喜欢的一个非常不错的指标是连接拆分,这些拆分仅承载一个 HTTP 事务(并因此让该事务承担所有开销)。 对于 HTTP/1,我们 74% 的活动连接仅承载一个事务 - 永久连接并不如我们所有人希望的那般有用。 但是在 HTTP/2 中,这一比例锐减至 25%。 这是在减少开销方面获得的巨大成效。  (HTTP/2 登陆 Firefox,Patrick McManus)



大多数 HTTP 传输都是短暂且急促的,而 TCP 则针对长时间的批量数据传输进行了优化。 通过重用相同的连接,HTTP/2 既可以更有效地利用每个 TCP 连接,也可以显著降低整体协议开销。 不仅如此,使用更少的连接还可以减少占用的内存和处理空间,也可以缩短完整连接路径(即,客户端、可信中介和源服务器之间的路径) 这降低了整体运行成本并提高了网络利用率和容量。 因此,迁移到 HTTP/2 不仅可以减少网络延迟,还有助于提高通量和降低运行成本。


注: 连接数量减少对提升 HTTPS 部署的性能来说是一项特别重要的功能: 可以减少开销较大的 TLS 连接数、提升会话重用率,以及从整体上减少所需的客户端和服务器资源。


7. 流控制


流控制是一种阻止发送方向接收方发送大量数据的机制,以免超出后者的需求或处理能力: 发送方可能非常繁忙、处于较高的负载之下,也可能仅仅希望为特定数据流分配固定量的资源。 例如,客户端可能请求了一个具有较高优先级的大型视频流,但是用户已经暂停视频,客户端现在希望暂停或限制从服务器的传输,以免提取和缓冲不必要的数据。 再比如,一个代理服务器可能具有较快的下游连接和较慢的上游连接,并且也希望调节下游连接传输数据的速度以匹配上游连接的速度来控制其资源利用率;等等。


上述要求会让您想到 TCP 流控制吗?您应当想到这一点;因为问题基本相同(请参阅流控制)。 不过,由于 HTTP/2 数据流在一个 TCP 连接内复用,TCP 流控制既不够精细,也无法提供必要的应用级 API 来调节各个数据流的传输。 为了解决这一问题,HTTP/2 提供了一组简单的构建块,这些构建块允许客户端和服务器实现其自己的数据流和连接级流控制:



  • 流控制具有方向性。 每个接收方都可以根据自身需要选择为每个数据流和整个连接设置任意的窗口大小。

  • 流控制基于信用。 每个接收方都可以公布其初始连接和数据流流控制窗口(以字节为单位),每当发送方发出 DATA 帧时都会减小,在接收方发出 WINDOW_UPDATE 帧时增大。

  • 流控制无法停用。 建立 HTTP/2 连接后,客户端将与服务器交换 SETTINGS 帧,这会在两个方向上设置流控制窗口。 流控制窗口的默认值设为 65,535 字节,但是接收方可以设置一个较大的最大窗口大小(2^31-1 字节),并在接收到任意数据时通过发送 WINDOW_UPDATE 帧来维持这一大小。

  • 流控制为逐跃点控制,而非端到端控制。 即,可信中介可以使用它来控制资源使用,以及基于自身条件和启发式算法实现资源分配机制。


HTTP/2 未指定任何特定算法来实现流控制。 不过,它提供了简单的构建块并推迟了客户端和服务器实现,可以实现自定义策略来调节资源使用和分配,以及实现新传输能力,同时提升网页应用的实际性能和感知性能(请参阅速度、性能和人类感知)。


例如,应用层流控制允许浏览器仅提取一部分特定资源,通过将数据流流控制窗口减小为零来暂停提取,稍后再行恢复。 换句话说,它允许浏览器提取图像预览或首次扫描结果,进行显示并允许其他高优先级提取继续,然后在更关键的资源完成加载后恢复提取。


8. 服务器推送


HTTP/2 新增的另一个强大的新功能是,服务器可以对一个客户端请求发送多个响应。 换句话说,除了对最初请求的响应外,服务器还可以向客户端推送额外资源(图 12-5),而无需客户端明确地请求。


image.png


注: HTTP/2 打破了严格的请求-响应语义,支持一对多和服务器发起的推送工作流,在浏览器内外开启了全新的互动可能性。 这是一项使能功能,对我们思考协议、协议用途和使用方式具有重要的长期影响。


为什么在浏览器中需要一种此类机制呢?一个典型的网络应用包含多种资源,客户端需要检查服务器提供的文档才能逐个找到它们。 那为什么不让服务器提前推送这些资源,从而减少额外的延迟时间呢? 服务器已经知道客户端下一步要请求什么资源,这时候服务器推送即可派上用场。


事实上,如果您在网页中内联过 CSS、JavaScript,或者通过数据 URI 内联过其他资产(请参阅资源内联),那么您就已经亲身体验过服务器推送了。 对于将资源手动内联到文档中的过程,我们实际上是在将资源推送给客户端,而不是等待客户端请求。 使用 HTTP/2,我们不仅可以实现相同结果,还会获得其他性能优势。 推送资源可以进行以下处理:



  • 由客户端缓存

  • 在不同页面之间重用

  • 与其他资源一起复用

  • 由服务器设定优先级

  • 被客户端拒绝


PUSH_PROMISE 101


所有服务器推送数据流都由 PUSH_PROMISE 帧发起,表明了服务器向客户端推送所述资源的意图,并且需要先于请求推送资源的响应数据传输。 这种传输顺序非常重要: 客户端需要了解服务器打算推送哪些资源,以免为这些资源创建重复请求。 满足此要求的最简单策略是先于父响应(即,DATA 帧)发送所有 PUSH_PROMISE 帧,其中包含所承诺资源的 HTTP 标头。


在客户端接收到 PUSH_PROMISE 帧后,它可以根据自身情况选择拒绝数据流(通过 RST_STREAM 帧)。 (例如,如果资源已经位于缓存中,便可能会发生这种情况。) 这是一个相对于 HTTP/1.x 的重要提升。 相比之下,使用资源内联(一种受欢迎的 HTTP/1.x“优化”)等同于“强制推送”: 客户端无法选择拒绝、取消或单独处理内联的资源。


使用 HTTP/2,客户端仍然完全掌控服务器推送的使用方式。 客户端可以限制并行推送的数据流数量;调整初始的流控制窗口以控制在数据流首次打开时推送的数据量;或完全停用服务器推送。 这些优先级在 HTTP/2 连接开始时通过 SETTINGS 帧传输,可能随时更新。


推送的每个资源都是一个数据流,与内嵌资源不同,客户端可以对推送的资源逐一复用、设定优先级和处理。 浏览器强制执行的唯一安全限制是,推送的资源必须符合原点相同这一政策: 服务器对所提供内容必须具有权威性。


9. 标头压缩


每个 HTTP 传输都承载一组标头,这些标头说明了传输的资源及其属性。 在 HTTP/1.x 中,此元数据始终以纯文本形式,通常会给每个传输增加 500–800 字节的开销。如果使用 HTTP Cookie,增加的开销有时会达到上千字节。 (请参阅测量和控制协议开销。) 为了减少此开销和提升性能,HTTP/2 使用 HPACK 压缩格式压缩请求和响应标头元数据,这种格式采用两种简单但是强大的技术:



  1. 这种格式支持通过静态霍夫曼代码对传输的标头字段进行编码,从而减小了各个传输的大小。

  2. 这种格式要求客户端和服务器同时维护和更新一个包含之前见过的标头字段的索引列表(换句话说,它可以建立一个共享的压缩上下文),此列表随后会用作参考,对之前传输的值进行有效编码。


利用霍夫曼编码,可以在传输时对各个值进行压缩,而利用之前传输值的索引列表,我们可以通过传输索引值的方式对重复值进行编码,索引值可用于有效查询和重构完整的标头键值对。


image.png


作为一种进一步优化方式,HPACK 压缩上下文包含一个静态表和一个动态表: 静态表在规范中定义,并提供了一个包含所有连接都可能使用的常用 HTTP 标头字段(例如,有效标头名称)的列表;动态表最初为空,将根据在特定连接内交换的值进行更新。 因此,为之前未见过的值采用静态 Huffman 编码,并替换每一侧静态表或动态表中已存在值的索引,可以减小每个请求的大小。


注: 在 HTTP/2 中,请求和响应标头字段的定义保持不变,仅有一些微小的差异: 所有标头字段名称均为小写,请求行现在拆分成各个 :method:scheme:authority 和 :path 伪标头字段。


HPACK 的安全性和性能


早期版本的 HTTP/2 和 SPDY 使用 zlib(带有一个自定义字典)压缩所有 HTTP 标头。 这种方式可以将所传输标头数据的大小减小 85% - 88%,显著减少了页面加载时间延迟:



在带宽较低的 DSL 链路中,上行链路速度仅有 375 Kbps,仅压缩请求标头就显著减少了特定网站(即,发出大量资源请求的网站)的页面加载时间。 我们发现,仅仅由于标头压缩,页面加载时间就减少了 45 - 1142 毫秒。  (SPDY 白皮书, chromium.org)



10. 相关阅读



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

收起阅读 »

从伪代码理解View事件分发过程

事件从起源从手指从屏幕按下的瞬间,触摸事件经过一系列处理会来到Activity的dispatchTouchEvent中。Activity.javapublic boolean dispatchTouchEvent(MotionEvent ev) { i...
继续阅读 »

事件从起源

从手指从屏幕按下的瞬间,触摸事件经过一系列处理会来到ActivitydispatchTouchEvent中。

Activity.java

public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
//getWindow().superDispatchTouchEvent(ev) 返回true代表消费了事件
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
//否则调用Activity的onTouchEvent
return onTouchEvent(ev);
}

getWindow()实际返回的是PhoneWindow

PhoneWindow.java

@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}

getWindow().superDispatchTouchEvent(ev)实际会调用到mDecor.superDispatchTouchEvent(event)

DecorView.java

public class DecorView extends FrameLayout 
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}
}

跟踪DecorView会发现DecorView继承自FrameLayout,因为FrameLayout没有重写dispatchTouchEvent方法,所以事件从Activity一路下来,最终事件的入口是ViewGroupdispatchTouchEvent

开发中,事件一般通过层层ViewGroup传递到View中,进行消费。一般View做真正的事件消费。

View的事件分发

View.java--伪代码

/**
* view接收事件的入口,事件由ViewGroup分发过来
*/

public boolean dispatchTouchEvent(MotionEvent event) {
boolean result = false;

//如果设置了OnTouchListener,并且mOnTouchListener.onTouch返回了True,
//设置Result为True,那么代表事件到这已经消费完成了。
if (mOnTouchListener != null && mOnTouchListener.onTouch(this, event)) {
result = true;
}

//没有设置OnTouchListener,或者mOnTouchListener.onTouch返回了false时,result为false
//此时会回调View.onTouchEvent方法
if(!result&& onTouchEvent(event)){
result = true;
}
return result;
}

public boolean onTouchEvent(MotionEvent event) {
//如果设置了onClickListener,那么返回True代表事件到这已经消费完成了。
if (onClickListener != null) {
onClickListener.onClick(this);
return true;
}
return false;
}

dispatchTouchEvent是传入事件的入口,如果设置了mOnTouchListener,并且返回了true,那么dispatchTouchEvent就会返回true,代表事件被当前View消费了。如果没有设置,那么就会回调onTouchEvent方法,如果设置了onClickListener,那么onTouchEvent返回true,同理dispatchTouchEvent就会返回true,代表事件被当前View消费了.

从上面可以看出OnTouchListener先于onTouchEvent执行,onTouchEvent先于onClickListener执行。

ViewGroup的事件分发

ViewGroup.java--伪代码

    /**
* onInterceptTouchEvent 拦截事件
* @return true 代表拦截当前事件,那么事件就不会分发给ViewGroup的child View ,会调用自身的 super.dispatchTouchEvent(event)
* false 代表不拦截当前事件,不拦截事件,那么在dispatchTouchEvent会遍历child View,寻找能消费事件的child View
*/

public boolean onInterceptTouchEvent(MotionEvent ev) {
return false;
}

/**
* @param event 事件
* @param child 如果child 不为null,那么事件分发给它,否则,调用调用自身的 super.dispatchTouchEvent(event)
* @return 是否消费了该事件
*/

private boolean dispatchTransformedTouchEvent(MotionEvent event, View child) {
boolean handled = false;
if (child != null) {
handled = child.dispatchTouchEvent(event);
} else {
handled = super.dispatchTouchEvent(event);
}

return handled;
}


public boolean dispatchTouchEvent(MotionEvent event) {
boolean handled = false;
//是否拦截当前事件
boolean intercepted = onInterceptTouchEvent(event);
//触碰的对象
TouchTarget newTouchTarget = null;
int actionMasked = event.getActionMasked();

if (actionMasked != MotionEvent.ACTION_CANCEL && !intercepted) {
if (actionMasked == MotionEvent.ACTION_DOWN) {
//ViewGroup child View 数组
final View[] children = mChildren;
//倒序遍历,最后的通常是需要处理事件的
for (int i = children.length - 1; i >= 0; i--) {
View child = mChildren[i];
//isContainer 方法判断事件是否落在View中
if (!child.isContainer(event.getX(), event.getY())) {
continue;
}
//找到可以接收事件的View,把事件分发给他,
//如果dispatchTransformedTouchEvent返回了True代表消费了事件
if (dispatchTransformedTouchEvent(event, child)) {
handled = true;
//通过child包装成TouchTarget对象
newTouchTarget = addTouchTarget(child);
break;
}

}
}
}
//如果TouchTarget为null,那么事件就发就自己处理
//mFirstTouchTarget == null 在onInterceptTouchEvent返回true时,或没有找到可以消费的child View时成立
if (mFirstTouchTarget == null) {
handled = dispatchTransformedTouchEvent(event, null);
}
return handled;
}

dispatchTouchEvent是事件接收的入口,如果拦截事件,那么就调用super.dispatchTouchEvent(event),我们知道ViewGroup是继承View的,那么调用super.dispatchTouchEvent(event)等于调用ViewdispatchTouchEvent

View.java

/**
* view接收事件的入口,事件由ViewGroup分发过来
*/

public boolean dispatchTouchEvent(MotionEvent event) {
boolean result = false;

//如果设置了OnTouchListener,并且mOnTouchListener.onTouch返回了True,
//设置Result为True,那么代表事件到这已经消费完成了。
if (mOnTouchListener != null && mOnTouchListener.onTouch(this, event)) {
result = true;
}

//没有设置OnTouchListener,或者mOnTouchListener.onTouch返回了false时,result为false
//此时会回调View.onTouchEvent方法
if(!result&& onTouchEvent(event)){
result = true;
}
return result;
}

如果不拦截,那么就会遍历当前ViewGroupchild view,找能消费事件的View,如果找到,调用dispatchTransformedTouchEvent(event, child),这里的child可以是ViewGroup或者是View,最后根据dispatchTransformedTouchEvent返回值判断是否消费了事件,如果返回false后,那么调用ViewGroupsuper.dispatchTouchEvent(event)

收起阅读 »

我是如何用这3个小工具,助力小姐姐提升100%开发效率的

前言 简单的知识点搭配合适的业务场景,往往能起到意想不到的效果。这篇文章会用三个最基础人人都知道的前端知识来说明如何助力运营小姐姐、公司48+前端开发同学的日常工作,让他们的工作效率得到极大地提升。 看完您可以会收获:用vue从零开始写一个chrome插件&n...
继续阅读 »

前言


简单的知识点搭配合适的业务场景,往往能起到意想不到的效果。这篇文章会用三个最基础人人都知道的前端知识来说明如何助力运营小姐姐、公司48+前端开发同学的日常工作,让他们的工作效率得到极大地提升。


看完您可以会收获:用vue从零开始写一个chrome插件 如何用Object.defineProperty拦截fetch请求`  如何使用油猴脚本开发一个扩展程序  日常提效的一些思考


油猴脚本入门示例



因为接下来的两个小工具都是基于油猴脚本来实现的,所以我们提前先了解一下它



油猴脚本是什么?



油猴脚本(Tampermonkey)是一个流行的浏览器扩展,可以运行用户编写的扩展脚本,来实现各式各样的功能,比如去广告、修改样式、下载视频等。



如何写一个油猴脚本?


1. 安装油猴


以chrome浏览器扩展为例,点击这里先安装


安装完成之后可以看到右上角多了这个


image.png


2. 新增示例脚本 hello world



// ==UserScript==
// @name hello world // 脚本名称
// @namespace http://tampermonkey.net/
// @version 0.1
// @description try to take over the world!
// @author You
// @match https://juejin.cn/* // 表示怎样的url才执行下面的代码
// @icon https://www.google.com/s2/favicons?domain=juejin.cn
// @grant none
// ==/UserScript==

(function() {
'use strict';
alert('hello world')
// Your code here...
})();

没错当打开任意一个https://juejin.cn/*掘金的页面时,都会弹出hello world,而其他的网页如https://baidu.com则不会。


到此你就完成了一个最简单的油猴脚本,接下来我们看一下用同样简单的代码,来解决一个实际问题吧!O(∩_∩)O


3行代码让SSO自动登录


问题是什么?


1. 有一天运营小姐姐要在几个系统之间配置点东西


一顿操作,终于把事情搞定了,心情美美的。


但是她心想,为啥每个系统都要我登录一次,不开心 o( ̄ヘ ̄o#)


1.gif


2. 下午一觉醒来,领导让把上午的配置重新改一下(尽职的小姐姐马上开始操作)


但是让她没想到的是:上午的登录页面仿佛许久没有见到她一样,又和小姐姐来了一次亲密接触😭


此时,她的内心已经开始崩溃了


2.gif


3. 但是这不是结束,以后的每一天她都是这种状态😭😭😭


3.gif


痛点在哪里?



看完上面的动图,我猜你已经在替小姐姐一起骂娘了,这做的什么玩意,太垃圾了。SSO是统一登录,你们这搞的是什么东西。



是的,我的内心和你一样愤愤不平, 一样有一万个草泥马在奔腾,这是哪个sb设计的方案,简直不配做人,一天啥事也不干,尽是跳登录页,输入用户名密码点登录按钮了,久而久之,朋友间见面说的第一句话不是“你吃了吗?”,而是“你登录了吗?”。


不过吐槽完,我们还是要想想如何通过技术手段解决这两个痛点,达到只需要登录一次的目的


1. 在A系统登录之后,跑到其他系统需要重新登录。


2. 登录时效只有2小时,2小时后,需要重新登录


该如何解决?


根本原因还是公司的SSO统一登录方案设计的有问题,所以需要推动他们修改,但是这是一个相对长期的过程,短期内有没有什么办法能让我们愉快的登录呢?


痛点1: 1. 在A系统登录之后,跑到其他系统需要重新登录。已无力回天


痛点2: 2. 登录时效只有2小时,2小时后,需要重新登录已无力回天


我们不好直接侵入各个系统去改造登录逻辑,改造其登录时效,但是却可以对登录页面(示例)做点手脚


image.png


最关键的是:




  1. 用户名输入框




  2. 密码输入框




  3. 点击按钮




所以可以借助油猴脚本,在DOMContentLoaded的时候,插入一下代码,来实现自动登录,减少手动操作的过程,大概原理如下。


结构图.jpg


// ==UserScript==
// @name SSO自动登录
// @namespace http://tampermonkey.net/
// @version 0.1
// @description try to take over the world!
// @author You
// @match https://*.xxx.com/login* // 这里是SSO登录页面地址,表示只有符合这个规则的才注入这段代码
// @grant none
// ==/UserScript==

document.querySelector('#username').value = 'xxx' // 用户名
document.querySelector('#password').value = 'yyy' // 密码
document.querySelector('#login-submit').click() // 自动提交登录

是不是太简单了,简单到令人发指,令人痛恨,令人想吐口水!!!,没有一点技术含量


是不是太简单了,简单到令人发指,令人痛恨,令人想吐口水!!!,没有一点技术含量


是不是太简单了,简单到令人发指,令人痛恨,令人想吐口水!!!,没有一点技术含量


image.png


是的,就这 😄,第一次帮小姐姐解决了困扰她许久的问题,晚上就请我吃了麻辣烫,还夸我"技术"好(此处不是开车


试试效果


gif中前半部分没有开启自动登录的脚本需要手动登录,后半部开启了就可以自动登录了。


autoLogin.gif


拦截fetch请求,只留你想要的页面


问题是什么?


前端常见的调试方式



  1. chrome inspect

  2. vconsole

  3. weinre

  4. 等等


这些方式都有各自的优缺点,比如chrome inspect第一次需要翻墙才能使用,只适用于安卓; vconsole不方便直接调试样式; weinre只适用于调试样式等。


基于这些原因,公司很久之前搞了一个远程调试工具,可以很方便的增删DOM结构、调试样式、查看请求、查看application 修改后手机上立即生效。


autoLogin2.gif


远程调试平台使用流程


他的使用流程大概是这样的




  1. 打开远程调试页面列表


    此页面包含测试环境所有人打开的调试页面链接, 多的时候有上百个




image.png



  1. 点击你要调试的页面,就可以进入像chrome控制台一样调试了


image.png


看完流程你应该大概知道问题在哪里了, 远程调试页面列表不仅仅包含我自己的页面,还包括很多其他人的,导致很难快速找到自己想要调试的页面


该如何解决?


问题解析


有什么办法能让我快速找到自己想要调试的页面呢?其实观察解析这个页面会发现列表是



  1. 通过发送一个请求获取的

  2. 响应中包含设备关键字


image.png


拦截请求


所以聪明的你已经猜到了,我们可以通过Object.defineProperty拦截fetch请求,过滤设备让列表中只存在我们指定的设备(毕竟平时开发时调试的设备基本是固定的,而设备完全相同的概率是很低的,所以指定了设备其实就是唯一标识了自己)页面。


具体如何做呢?



// ==UserScript==
// @name 前端远程调试设备过滤
// @namespace http://tampermonkey.net/
// @version 0.1
// @description try to take over the world!
// @author You
// @match https://chii-fe.xxx.com/ // 指定脚本生效的页面
// @grant none
// @run-at document-start // 注意这里,脚本注入的时机是document-start
// ==/UserScript==

;(() => {
const replaceRe = /\s*/g
// 在这里设置设备白名单
const DEVICE_WHITE_LIST = [
'Xiaomi MI 8',
'iPhone9,2',
].map(
(it) => it.replace(replaceRe, '').toLowerCase())

const originFetch = window.fetch
const recordListUrl = 'record-list'
const filterData = (source) => {
// 数据过滤,返回DEVICE_WHITE_LIST指定的设备的数据
// 详细过程省略
return data
}
// 拦截fetch请求
Object.defineProperty(window, 'fetch', {
configurable:
true,
enumerable:
true,
get () {
return function (url, options) {
return originFetch(url, options).then((response) => {
// 只处理指定的url
if (url.includes(recordListUrl)) {
if (response.clone) {
const cloneRes = response.clone()

return new Promise((resolve, reject) => {
resolve(
{
text: (
) => {
return cloneRes.json().then(json => {
return filterData(JSON.stringify(json))
}
);
}
}
)
}
)
}
}

return response
}
)
}
}
}
)
}
)()


试试效果


通过下图可以看出,过滤前有37个页面,过滤后只剩3个,瞬间就找到你要调试页面,再也不用从几百个页面中寻找你自己的那个啦!


image.png


助力全公司45+前端开发 - chrome插件的始与终



通过插件一键设置ua,模拟用户登录状态,提高开发效率。



先看结果


插件使用方式


new.gif


插件使用结果



团队48+小伙伴也使用起来了



image.png


image.png


背景和问题



日常c端业务中有很多场景都需要用户登录后才能正常进行,而开发阶段基本都是通过chrome模拟手机设备来开发,所以往往会涉及到在chrome浏览器中模拟用户登录,其涉及以下三步(这个步骤比较繁琐)。



备注:保持用户的登录态一般是通过cookie,但也有通过header来做,比如我们公司是改写ua来做的



  1. 获取ua: 前往公司UA生成平台输入手机号生成ua

  2. 添加ua: 将ua复制到chrome devtool设置/修改device

  3. 使用ua: 选择新添加的ua,刷新页面,重新开发调试


ua.gif


来看一段对话



隔壁98年刚毕业妹子:



又过期了,谁又把我挤下去了嘛


好的,稍等一会哈,我换个账号测测


好麻烦哎!模拟一个用户信息,要这么多步骤,好烦呀!!!



我,好奇的大叔:



“细心”了解下,她正在做一个h5活动项目,场景复杂,涉及的状态很多,需要用不同的账号来做测试。


模拟一两个用户还好,但是此刻小姐姐测这么多场景,已经模拟了好多个(谁都会烦啊)


公司的登录体系是单点登录,一个好不容易模拟的账号,有可能别人也在用,结果又被顶掉了,得重新生成,我TM


看着她快气哭的小眼神,作为隔壁桌友好的邻居,此刻我心里只想着一件事...!帮她解决这个恼人的问题。


分析和解决问题



通过上面的介绍您应该可以感觉到我们开发阶段遇到需要频繁切换账号做测试时的烦恼,相对繁琐的ua生成过程导致了它一定是个费时费力的麻烦事。



有没有什么办法让我们的开发效率得到提升,别浪费在这种事情上呢?一起一步步做起来


需求有哪些



提供一种便捷地模拟ua的方式,助力开发效率提升。




  1. 基本诉求:本地开发阶段,希望有更便捷的方式来模拟用户登录

  2. 多账号: 一个项目需要多个账号,不同项目间的账号可以共享也可以不同

  3. 指定域: 只有指定的下才需要模拟ua,不能影响浏览器正常使用

  4. 过期处理: 账号过期后,可以主动生成,无需手动重新获取


如何解决




  1. 需求1:结合前面生成ua阶段,我们可以通过某种方式让用户能直接在当前页面生成ua,无需跳出,一键设置省略手动过程




  2. 需求2:提供多账号管理功能,能直接选中切换ua




  3. 需求3:限定指定域,该ua才生效




  4. 需求4:当使用到过期账号时,可一键重新生成即可




为什么是chrome插件




  1. 浏览器中发送ajax请求的ua无法直接修改,但是chrome插件可以修改请求的ua(很重要的一点




  2. chrome插件popup模式可直接在当前页面打开,无需跳出开发页面,减少跳出过程




用vue从零开始写一个chrome插件



篇幅原因,这里只做示例级别的简单介绍,如果您希望详细了解chrome插件的编写可以参考这里



从一个小例子开始



接下来我们会以下页面为例,说明用vue如何写出来。



ua3.gif


基本功能




  1. 底部tab切换区域viewAviewBviewC




  2. 中间内容区域:切换viewA、B、C分别展示对应的页面




content部分


借助chrome浏览器可以向网页插入脚本的特性,我们会演示如何插入脚本并且在网页加载的时候弹一个hello world


popup与background通信部分


popup完成用户的主要交互,在viewA页面点击获取自定义的ua信息


修改ajax请求ua部分


会演示如果通过chrome插件修改请求header


1. 了解一个chrome插件的构成



  1. manifest.json

  2. background script

  3. content script

  4. popup


1. manifest.json



几乎所有的东西都要在这里进行声明、权限资源页面等等




{
"manifest_version": 2, // 清单文件的版本,这个必须写
"name": "hello vue extend", // 插件的名称,等会我们写的插件名字就叫hello vue extend
"description": "hello vue extend", // 插件描述
"version": "0.0.1", // 插件的版本
// 图标,写一个也行
"icons": {
"48": "img/logo.png"
},
// 浏览器右上角图标设置,browser_action、page_action、app必须三选一
"browser_action": {
"default_icon": "img/logo.png",
"default_title": "hello vue extend",
"default_popup": "popup.html"
},
// 一些常驻的后台JS或后台页面
"background": {
"scripts": [
"js/hot-reload.js",
"js/background.js"
]
},
// 需要直接注入页面的JS
"content_scripts": [{
"matches": [""],
"js": ["js/content.js"],
"run_at": "document_start"
}],
// devtools页面入口,注意只能指向一个HTML文件
"devtools_page": "devcreate.html",
// Chrome40以前的插件配置页写法
"options_page": "options.html",
// 权限申请
"permissions": [
"storage",
"webRequest",
"tabs",
"webRequestBlocking",
""
]
}

2. background script



后台,可以认为是一个常驻的页面,权限很高,几乎可以调用所有的API,可以与popup、content script等通信



3. content script



chrome插件向页面注入脚本的一种形式(js和css都可以)



4. popup



popup是点击browser_action或者page_action图标时打开的一个小窗口网页,焦点离开网页就立即关闭。



比如我们要用vue做的页面。


image.png


2. 改写vue.config.js



manifest.json对文件引用的结构基本决定了打包后的文件路径



打包后的路径


// dist目录用来chrome扩展导入

├── dist
│ ├── favicon.ico
│ ├── img
│ │ └── logo.png
│ ├── js
│ │ ├── background.js
│ │ ├── chunk-vendors.js
│ │ ├── content.js
│ │ ├── hot-reload.js
│ │ └── popup.js
│ ├── manifest.json
│ └── popup.html


源码目录



├── README.md
├── babel.config.js
├── package-lock.json
├── package.json
├── public
│ ├── favicon.ico
│ ├── index.html
│ └── js
│ └── hot-reload.js
├── src
│ ├── assets
│ │ ├── 01.png
│ │ ├── disabled.png
│ │ └── logo.png
│ ├── background
│ │ └── background.js
│ ├── content
│ │ └── content.js
│ ├── manifest.json
│ ├── popup
│ │ ├── App.vue
│ │ ├── main.js
│ │ ├── router.js
│ │ └── views
│ │ ├── viewA.vue
│ │ ├── viewB.vue
│ │ └── viewC.vue
│ └── utils
│ ├── base.js
│ ├── fixCaton.js
│ └── storage.js
└── vue.config.js



修改vue.config.js



主需要稍微改造变成可以多页打包,注意输出的目录结构就可以了




const CopyWebpackPlugin = require('copy-webpack-plugin')
const path = require('path')
// 这里考虑可以添加多页
const pagesObj = {}
const chromeName = ['popup']
const plugins = [
{
from: path.resolve('src/manifest.json'),
to: `${path.resolve('dist')}/manifest.json`
},
{
from: path.resolve('src/assets/logo.png'),
to: `${path.resolve('dist')}/img/logo.png`
},
{
from: path.resolve('src/background/background.js'),
to: `${path.resolve('dist')}/js/background.js`
},
{
from: path.resolve('src/content/content.js'),
to: `${path.resolve('dist')}/js/content.js`
},
]

chromeName.forEach(name => {
pagesObj[name] = {
css: {
loaderOptions: {
less: {
modifyVars: {},
javascriptEnabled: true
}
}
},
entry: `src/${name}/main.js`,
filename: `${name}.html`
}
})

const vueConfig = {
lintOnSave:false, //关闭eslint检查
pages: pagesObj,
configureWebpack: {
entry: {},
output: {
filename: 'js/[name].js'
},
plugins: [new CopyWebpackPlugin(plugins)]
},
filenameHashing: false,
productionSourceMap: false
}

module.exports = vueConfig



3. 热刷新



我们希望修改插件源代码进行打包之后,chrome插件对应的页面能主动更新。为什么叫热刷新而不是热更新呢?因为它其实是全局刷新页面,并不会保存状态。



这里推荐一个github上的解决方案crx-hotreload


4. 完成小例子编写


new.gif


文件目录结构



├── popup
│ ├── App.vue
│ ├── main.js
│ ├── router.js
│ └── views
│ ├── viewA.vue
│ ├── viewB.vue
│ └── viewC.vue



main.js



import Vue from 'vue'
import App from './App.vue'
import router from './router'

Vue.config.productionTip = false

new Vue({
router,
render: h => h(App)
}).$mount('#app')



router.js


import Vue from 'vue'
import Router from 'vue-router'

import ViewA from './views/viewA.vue'
import ViewB from './views/viewB.vue'
import ViewC from './views/viewC.vue'

Vue.use(Router)

export default new Router({
mode: 'history',
base: process.env.BASE_URL,
routes: [
{
path: '/',
name: 'home',
redirect: '/view/a'
},
{
path: '/view/a',
name: 'viewA',
component: ViewA,
},
{
path: '/view/b',
name: 'viewB',
component: ViewB,
},
{
path: '/view/c',
name: 'viewC',
component: ViewC,
},
]
})

App.vue









viewA、viewB、viewC



三个页面基本长得是一样的,只有背景色和文案内容不一样,这里我就只贴viewA的代码了。



需要注意的是这里会演示popup与background,通过sendMessage方法获取background后台数据










background.js


const customUa = 'hello world ua'
// 请求发送前拦截
const onBeforeSendCallback = (details) => {
for (var i = 0; i < details.requestHeaders.length; ++i) {
if (details.requestHeaders[i].name === 'User-Agent') {
details.requestHeaders.splice(i, 1);
break;
}
}
// 修改请求UA为hello world ua
details.requestHeaders.push({
name: 'User-Agent',
value: customUa
});

return { requestHeaders: details.requestHeaders };
}

// 前面的sendMessage获取getCustomUserAgent,会被这里监听
const onRuntimeMessageListener = () => {
chrome.runtime.onMessage.addListener(function (msg, sender, callback) {
if (msg.type === 'getCustomUserAgent') {
callback({
customUa
});
}
});
}

const init = () => {
onRuntimeMessageListener()
onBeforeSendHeadersListener()
}

init()


content.js



演示如何往网页中插入代码




function setScript({ code = '', needRemove = true } = params) {
let textNode = document.createTextNode(code)
let script = document.createElement('script')

script.appendChild(textNode)
script.remove()

let parentNode = document.head || document.documentElement

parentNode.appendChild(script)
needRemove && parentNode.removeChild(script)
}

setScript({
code: `alert ('hello world')`,
})

ua3.gif


关于一键设置ua插件



大体上和小例子差不都,只是功能相对复杂一些,会涉及到





  1. 数据本地存储chrome.storage.sync.get|setchrome.tabs.query等API




  2. popup与background通信、content与background通信




  3. 拦截请求修改UA




  4. 其他的大体就是常规的vue代码编写啦!




这里就不贴详细的代码实现了。



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

收起阅读 »

跨浏览器窗口通讯 ,7种方式,你还知道几种呢?

前言 为什么会扯到这个话题,最初是源于听 y.qq.com/ QQ音乐, 播放器处于单独的一个页面 当你在另外的一个页面搜索到你满意的歌曲的时候,点击播放或添加到播放队列 你会发现,播放器页面做出了响应的响应 这里我又联想到了商城的购物车的场景,体验确实有...
继续阅读 »

前言


为什么会扯到这个话题,最初是源于听 y.qq.com/ QQ音乐,



  • 播放器处于单独的一个页面

  • 当你在另外的一个页面搜索到你满意的歌曲的时候,点击播放或添加到播放队列

  • 你会发现,播放器页面做出了响应的响应


这里我又联想到了商城的购物车的场景,体验确实有提升。

刚开始,我怀疑的是Web Socket作妖,结果通过分析网络请求和看源码,并没有。 最后发现是localStore的storage事件作妖,哈哈。




回归正题,其实在一般正常的知识储备的情况下,我们会想到哪些方案呢?


先抛开如下方式:



  1. 各自对服务器进行轮询或者长轮询

  2. 同源策略下,一方是另一方的 opener


演示和源码


多页面通讯的demo, 为了正常运行,请用最新的chrome浏览器打开。

demo的源码地址



两个浏览器窗口间通信


WebSocket


这个没有太多解释,WebSocket 是 HTML5 开始提供的一种在单个 TCP 连接上进行全双工通讯的协议。当然是有代价的,需要服务器来支持。

js语言,现在比较成熟稳定当然是 socket.iows. 也还有轻量级的ClusterWS


你可以在The WebSocket API (WebSockets)
看到更多的关于Web Socket的信息。


定时器 + 客户端存储


定时器:setTimeout/setInterval/requestAnimationFrame

客户端存储: cookie/localStorage/sessionStorage/indexDB/chrome的FileSystem


定时器没啥好说的,关于客户端存储。



  • cookie: 每次会带到服务端,并且能存的并不大,4kb?,记得不太清楚

  • localStorage/sessionStorage 应该是5MB, sessionStorage关闭浏览器就和你说拜拜。

  • indexDB 这玩意就强大了,不过读取都是异步的,还能存 Blob文件,真的是很high。

  • chrome的FileSystem ,Filesystem & FileWriter API,主要是chrome和opera支持。这玩意就是文件系统。


postMessage


Cross-document messaging 这玩意的支持率98.9%。 好像还能发送文件,哈哈,强大。

不过仔细一看 window.postMessage(),就注定了你首先得拿到window这个对象。 也注定他使用的限制, 两个窗体必须建立起联系。 常见建立联系的方式:



  • window.open

  • window.opener

  • iframe


提到上面的window.open, open后你能获得被打开窗体的句柄,当然也可以直接操作窗体了。




到这里,我觉得一般的前端人员能想到的比较正经的方案应该是上面三种啦。

当然,我们接下来说说可能不是那么常见的另外三种方式。


StorageEvent


Page 1


localStorage.setItem('message',JSON.stringify({
message: '消息',
from: 'Page 1',
date: Date.now()
}))

Page 2


window.addEventListener("storage", function(e) {
console.log(e.key, e.newValue, e.oldValue)
});

如上, Page 1设置消息, Page 2注册storage事件,就能监听到数据的变化啦。


上面的e就是StorageEvent,有下面特有的属性(都是只读):



  • key :代表属性名发生变化.当被clear()方法清除之后所有属性名变为null

  • newValue:新添加进的值.当被clear()方法执行过或者键名已被删除时值为null

  • oldValue:原始值.而被clear()方法执行过,或在设置新值之前并没有设置初始值时则返回null

  • storageArea:被操作的storage对象

  • url:key发生改变的对象所在文档的URL地址


Broadcast Channel


这玩意主要就是给多窗口用的,Service Woker也可以使用。 firefox,chrome, Opera均支持,有时候真的是很讨厌Safari,浏览器支持77%左右。


使用起来也很简单, 创建BroadcastChannel, 然后监听事件。 只需要注意一点,渠道名称一致就可以。

Page 1


    var channel = new BroadcastChannel("channel-BroadcastChannel");
channel.postMessage('Hello, BroadcastChannel!')

Page 2


    var channel = new BroadcastChannel("channel-BroadcastChannel");
channel.addEventListener("message", function(ev) {
console.log(ev.data)
});

SharedWorker


这是Web Worker之后出来的共享的Worker,不通页面可以共享这个Worker。

MDN这里给了一个比较完整的例子simple-shared-worker


这里来个插曲,Safari有几个版本支持这个特性,后来又不支持啦,还是你Safari,真是6。


虽然,SharedWorker本身的资源是共享的,但是要想达到多页面的互相通讯,那还是要做一些手脚的。
先看看MDN给出的例子的ShareWoker本身的代码:


onconnect = function(e) {
var port = e.ports[0];

port.onmessage = function(e) {
var workerResult = 'Result: ' + (e.data[0] * e.data[1]);
port.postMessage(workerResult);
}

}

上面的代码其实很简单,port是关键,这个port就是和各个页面通讯的主宰者,既然SharedWorker资源是共享的,那好办,把port存起来就是啦。

看一下,如下改造的代码:

SharedWorker就成为一个纯粹的订阅发布者啦,哈哈。


var portList = [];

onconnect = function(e) {
var port = e.ports[0];
ensurePorts(port);
port.onmessage = function(e) {
var data = e.data;
disptach(port, data);
};
port.start();
};

function ensurePorts(port) {
if (portList.indexOf(port) < 0) {
portList.push(port);
}
}

function disptach(selfPort, data) {
portList
.filter(port => selfPort !== port)
.forEach(port => port.postMessage(data));
}


MessageChannel


Channel Messaging API的 MessageChannel 接口允许我们创建一个新的消息通道,并通过它的两个MessagePort 属性发送数据。


其需要先通过 postMessage先建立联系。


MessageChannel的基本使用:


var channel = new MessageChannel();
var para = document.querySelector('p');

var ifr = document.querySelector('iframe');
var otherWindow = ifr.contentWindow;

ifr.addEventListener("load", iframeLoaded, false);

function iframeLoaded() {
otherWindow.postMessage('Hello from the main page!', '*', [channel.port2]);
}

channel.port1.onmessage = handleMessage;
function handleMessage(e) {
para.innerHTML = e.data;
}

至于在线的例子,MDN官方有一个版本 MessageChannel 通讯



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

收起阅读 »

更新需要提示用户,需要控制应用是否更新

更新需要提示用户,需要控制应用是否更新1. 方案一在检测到更新后提示用户,让用户选择更新。设置autoDownload参数为false,让应用检测到更新不自动下载,改成手动下载更新包。通过在钩子update-available中,加入对话框提示用户,让用户选择...
继续阅读 »

更新需要提示用户,需要控制应用是否更新

1. 方案一

在检测到更新后提示用户,让用户选择更新。

设置autoDownload参数为false,让应用检测到更新不自动下载,改成手动下载更新包。

通过在钩子update-available中,加入对话框提示用户,让用户选择。

response为0用户选择确定,触发downloadUpdate方法下载应用更新包进行后续更新操作。否则,不下载更新包。

如果我们不配置autoDownload为false,那么问题来了:在弹出对话框的同时,用户还来不及选择,应用自动下载并且更新完成,做不到阻塞。

本文首发于公众号「全栈大佬的修炼之路」,欢迎关注。

重要代码如下:

autoUpdater.autoDownload = false

update-available钩子中弹出对话框

autoUpdater.on('update-available', (ev, info) => {
// // 不可逆过程
const options = {
type: 'info',
buttons: ['确定', '取消'],
title: '更新提示',
// ${info.version} Cannot read property 'version' of undefined
message: '发现有新版本,是否更新?',
cancelId: 1
}
dialog.showMessageBox(options).then(res => {
if (res.response === 0) {
autoUpdater.downloadUpdate()
logger.info('下载更新包成功')
sendStatusToWindow('下载更新包成功');
} else {
return;
}
})
})

2. 方案二

在更新下载完后提示用户,让用户选择更新。

先配置参数autoInstallOnAppQuit为false,阻止应用在检测到更新包后自动更新。

在钩子update-downloaded中加入对话框提示用户,让用户选择。

response为0用户选择确定,更新应用。否则,当前应用不更新。

如果我们不配置autoInstallOnAppQuit为false,那么问题是:虽然第一次应用不更新,但是第二次打开应用,应用马上关闭,还没让我们看到主界面,应用暗自更新,重点是更新完后不重启应用。

重要代码如下:

// 表示下载包不自动更新
autoUpdater.autoInstallOnAppQuit = false
在update-downloaded钩子中弹出对话框
autoUpdater.on('update-downloaded', (ev, releaseNotes, releaseName) => {
logger.info('下载完成,更新开始')
sendStatusToWindow('下载完成,更新开始');
// Wait 5 seconds, then quit and install
// In your application, you don't need to wait 5 seconds.
// You could call autoUpdater.quitAndInstall(); immediately
const options = {
type: 'info',
buttons: ['确定', '取消'],
title: '应用更新',
message: process.platform === 'win32' ? releaseNotes : releaseName,
detail: '发现有新版本,是否更新?'
}
dialog.showMessageBox(options).then(returnVal => {
if (returnVal.response === 0) {
logger.info('开始更新')
setTimeout(() => {
autoUpdater.quitAndInstall()
}, 5000);
} else {
logger.info('取消更新')
return
}
})
});

3. 源码分析

未打包目录位于: electron-builder/packages/electron-updater/src/AppUpdater.ts中。 打包后在electron-updater\out\AppUpdater.d.ts中

  1. 首先进入checkForUpdates()方法,开始检测更新
  2. 正在更新不需要进入
  3. 开始更新前判断autoDownload,为true自动下载,为false不下载等待应用通知。
export declare abstract class AppUpdater extends EventEmitter {
/**
* 当被发现有更新时,是否要自动下载更新
* 场景:可以适用于electron检查更新包提示,用户操作是否需要更新
*/
autoDownload: boolean;
/**
* 在app.quit()后,是否自动将下载下载的更新包更新
* 场景:可以适用于electron下载完更新包提示,用户操作是否需要更新。在第二次打开应用,应用不会自动更新。
*/
autoInstallOnAppQuit: boolean;
}


/**
* 检测是否需要更新
*/
checkForUpdates(): Promise < UpdateCheckResult > {
let checkForUpdatesPromise = this.checkForUpdatesPromise
// 正在检测更新跳过
if (checkForUpdatesPromise != null) {
this._logger.info("Checking for update (already in progress)")
return checkForUpdatesPromise
}

const nullizePromise = () => this.checkForUpdatesPromise = null
// 开始检测更新
this._logger.info("Checking for update")
checkForUpdatesPromise = this.doCheckForUpdates()
.then(it => {
nullizePromise()
return it
})
.catch(e => {
nullizePromise()
this.emit("error", e, `Cannot check for updates: ${(e.stack || e).toString()}`)
throw e
})

this.checkForUpdatesPromise = checkForUpdatesPromise
return checkForUpdatesPromise
}
// 检测更新具体函数
private async doCheckForUpdates(): Promise < UpdateCheckResult > {
// 触发 checking-for-update 钩子
this.emit("checking-for-update")
// 取更新信息
const result = await this.getUpdateInfoAndProvider()
const updateInfo = result.info
// 判断更新信息是否有效
if (!await this.isUpdateAvailable(updateInfo)) {
this._logger.info(`Update for version ${this.currentVersion} is not available (latest version: ${updateInfo.version}, downgrade is ${this.allowDowngrade ? "allowed" : "disallowed"}).`)
this.emit("update-not-available", updateInfo)
return {
versionInfo: updateInfo,
updateInfo,
}
}

this.updateInfoAndProvider = result
this.onUpdateAvailable(updateInfo)

const cancellationToken = new CancellationToken()
//noinspection ES6MissingAwait
// 如果设置autoDownload为true,则开始自动下载更新包,否则不下载
return {
versionInfo: updateInfo,
updateInfo,
cancellationToken,
downloadPromise: this.autoDownload ? this.downloadUpdate(cancellationToken) : null
}
}

如果需要配置updater中的其他参数达到某种功能,我们可以仔细查看其中的配置项。

export abstract class AppUpdater extends EventEmitter {
/**
* 当被发现有更新时,是否要自动下载更新
* 场景:可以适用于electron检查更新包提示,用户操作是否需要更新
*/
autoDownload: boolean;
/**
* 在app.quit()后,是否自动将下载下载的更新包更新
* 场景:可以适用于electron下载完更新包提示,用户操作是否需要更新。在第二次打开应用,应用不会自动更新。
*/
autoInstallOnAppQuit: boolean;
/**
* GitHub提供者。
是否允许升级到预发布版本。
如果应用程序版本包含预发布组件,默认为“true”。0.12.1-alpha.1,这里alpha是预发布组件),否则“false”。
allowDowngrade设置为true,则应用允许降级。
*/
allowPrerelease: boolean;
/**
* GitHub提供者。
获取所有发布说明(从当前版本到最新版本),而不仅仅是最新版本。
@default false
*/
fullChangelog: boolean;
/**
*是否允许版本降级(当用户从测试通道想要回到稳定通道时)。
*仅当渠道不同时考虑(根据语义版本控制的预发布版本组件)。
* @default false
*/
allowDowngrade: boolean;
/**
* 当前应用的版本
*/
readonly currentVersion: SemVer;
private _channel;
protected downloadedUpdateHelper: DownloadedUpdateHelper | null;
/**
* 获取更新通道。
不适用于GitHub。
从更新配置不返回“channel”,仅在之前设置的情况下。
*/
get channel(): string | null;
/**
* 设置更新通道。
不适用于GitHub。
覆盖更新配置中的“channel”。
“allowDowngrade”将自动设置为“true”。
如果这个行为不适合你,明确后简单设置“allowDowngrade”。
*/
set channel(value: string | null);
/**
* 请求头
*/
requestHeaders: OutgoingHttpHeaders | null;
protected _logger: Logger;
get netSession(): Session;
/**
* The logger. You can pass [electron-log](https://github.com/megahertz/electron-log), [winston](https://github.com/winstonjs/winston) or another logger with the following interface: `{ info(), warn(), error() }`.
* Set it to `null` if you would like to disable a logging feature.
* 日志,类型有:info、warn、error
*/
get logger(): Logger | null;
set logger(value: Logger | null);
/**
* For type safety you can use signals, e.g.
为了类型安全,可以使用signals。
例如:
`autoUpdater.signals.updateDownloaded(() => {})` instead of `autoUpdater.on('update-available', () => {})`
*/
readonly signals: UpdaterSignal;
private _appUpdateConfigPath;
/**
* test only
* @private
*/
set updateConfigPath(value: string | null);
private clientPromise;
protected readonly stagingUserIdPromise: Lazy<string>;
private checkForUpdatesPromise;
protected readonly app: AppAdapter;
protected updateInfoAndProvider: UpdateInfoAndProvider | null;
protected constructor(
options: AllPublishOptions | null | undefined,
app?: AppAdapter
);
/**
* 获取当前更新的url
*/
getFeedURL(): string | null | undefined;
/**
* Configure update provider. If value is `string`, [GenericServerOptions](/configuration/publish#genericserveroptions) will be set with value as `url`.
* @param options If you want to override configuration in the `app-update.yml`.
*
* 配置更新提供者。通过提供url
* @param options 如果你想覆盖' app-update.yml '中的配置。
*/
setFeedURL(options: PublishConfiguration | AllPublishOptions | string): void;
/**
* 检查服务其是否有更新
*/
checkForUpdates(): Promise<UpdateCheckResult>;
isUpdaterActive(): boolean;
/**
*
* @param downloadNotification 询问服务器是否有更新,下载并通知更新是否可用
*/
checkForUpdatesAndNotify(
downloadNotification?: DownloadNotification
): Promise<UpdateCheckResult | null>;
private static formatDownloadNotification;
private isStagingMatch;
private computeFinalHeaders;
private isUpdateAvailable;
protected getUpdateInfoAndProvider(): Promise<UpdateInfoAndProvider>;
private createProviderRuntimeOptions;
private doCheckForUpdates;
protected onUpdateAvailable(updateInfo: UpdateInfo): void;
/**
*
* 作用:开始下载更新包
*
* 如果将`autoDownload`选项设置为false,就可以使用这个方法。
*
* @returns {Promise<string>} Path to downloaded file.
*/
downloadUpdate(cancellationToken?: CancellationToken): Promise<any>;
protected dispatchError(e: Error): void;
protected dispatchUpdateDownloaded(event: UpdateDownloadedEvent): void;
protected abstract doDownloadUpdate(
downloadUpdateOptions: DownloadUpdateOptions
): Promise<Array<string>>;
/**
* 作用:下载后重新启动应用程序并安装更新。
*只有在' update- downloads '被触发后才会调用。
*
* 注意:如果在update-downloaded钩子中,让用户选择是否更新应用,选择不更新,那就是没有执行autoUpdater.quitAndInstall()方法。
* 虽然应用没有更新,但是当第二次打开应用的时候,应用检测到本地有更新包,他就会直接更新,最后不会重启更新后的应用。
*
* 为了解决这个问题,需要设置`autoInstallOnAppQuit`为false。关闭应用自动更新。
*
* **Note:** ' autoUpdater.quitAndInstall() '将首先关闭所有的应用程序窗口,然后只在' app '上发出' before-quit '事件。
*这与正常的退出事件序列不同。
*
* @param isSilent 仅Windows以静默模式运行安装程序。默认为false。
* @param isForceRunAfter 即使无提示安装也可以在完成后运行应用程序。不适用于macOS。忽略是否isSilent设置为false。
*/
abstract quitAndInstall(isSilent?: boolean, isForceRunAfter?: boolean): void;
private loadUpdateConfig;
private computeRequestHeaders;
private getOrCreateStagingUserId;
private getOrCreateDownloadHelper;
protected executeDownload(
taskOptions: DownloadExecutorTask
): Promise<Array<string>>;
}

最后,希望大家一定要点赞三连。


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

收起阅读 »

iOS Reveal

iOS Reveal一、概述Reveal是一款UI调试神器,对iOS开发非常有帮助。这里以版本Version 4 (8796)演示二、安装2.1 Mac端安装Reveal官网直接下载安装,可以用试用版。2.2 手机端安装1.cydia直接安装Reveal Lo...
继续阅读 »

iOS Reveal


一、概述

Reveal是一款UI调试神器,对iOS开发非常有帮助。这里以版本Version 4 (8796)演示

二、安装

2.1 Mac端安装

Reveal官网直接下载安装,可以用试用版。

2.2 手机端安装

1.cydia直接安装Reveal Loader插件





打开手机"设置->Reveal-> Enabled Applications"打开需要分析的App


  1. 我这里打开微信

2.3 配置环境

  1. 打开电脑端的Reveal->help->Show Reveal Library in Finder




  1. RevealServer保存到手机中
    进入到Library/目录:

zaizai:~ root# cd /Library/
zaizai:/Library root#

创建RHRevealLoader目录:

zaizai:/Library root# mkdir RHRevealLoader
zaizai:/Library root# cd RHRevealLoader/
zaizai:/Library/RHRevealLoader root# pwd
/Library/RHRevealLoader

RevealServer拷贝到该目录下:

scp -P 12345 RevealServer root@localhost://Library/RHRevealLoader/libReveal.dylib

需要改名为libReveal.dylib


手机端确认:

zaizai:/Library/RHRevealLoader root# ls
libReveal.dylib*

3.重启SpringBoardkill SpringBoard

zaizai:~ root# ps -A | grep SpringBoard
20973 ?? 4:01.57 /System/Library/CoreServices/SpringBoard.app/SpringBoard
23213 ttys000 0:00.01 grep SpringBoard
zaizai:~ root# kill 20973
zaizai:~ root#

2.4 调试微信

重新打开电脑端Reveal和微信,这个时候微信就出现了:



发现页面中微信钱包金额是每一位都是一个UILabel。。。

修改下LabelText




这样余额就改了。并且Revealcycript一样不会阻塞进程。

总结

    1. iOS安装插件
    1. Mac安装App
    1. 动态库导入iPhone

作者:HotPotCat
链接:https://www.jianshu.com/p/ca0a4b73a986


收起阅读 »

objc_msgSend 消息快速查找(cache查找)

一、CacheLookup 查找缓存1.1 CacheLookup源码分析传递的参数是NORMAL, _objc_msgSend, __objc_msgSend_uncached://NORMAL, _objc_msgSend, __objc_msgSend_...
继续阅读 »

一、CacheLookup 查找缓存

1.1 CacheLookup源码分析

传递的参数是NORMAL, _objc_msgSend, __objc_msgSend_uncached

//NORMAL, _objc_msgSend, __objc_msgSend_uncached
.macro CacheLookup Mode, Function, MissLabelDynamic, MissLabelConstant
// requirements:
// //缓存不存在返回NULL,x0设置为0
// GETIMP:
// The cache-miss is just returning NULL (setting x0 to 0)
// 参数说明
// NORMAL and LOOKUP:
// - x0 contains the receiver
// - x1 contains the selector
// - x16 contains the isa
// - other registers are set as per calling conventions
//
//调用过来的p16存储的是cls,将cls存储在x15.
mov x15, x16 // stash the original isa
//_objc_msgSend
LLookupStart\Function:
// p1 = SEL, p16 = isa
//arm64 64 OSX/SIMULATOR
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS
//isa->cache,首地址也就是_bucketsAndMaybeMask
ldr p10, [x16, #CACHE] // p10 = mask|buckets
//lsr逻辑右移 p11 = _bucketsAndMaybeMask >> 48 也就是 mask
lsr p11, p10, #48 // p11 = mask
//p10 = _bucketsAndMaybeMask & 0xffffffffffff = buckets(保留后48位)
and p10, p10, #0xffffffffffff // p10 = buckets
//x12 = cmd & mask w1为第二个参数cmd(self,cmd...),w11也就是p11 也就是执行cache_hash。这里没有>>7位的操作
and w12, w1, w11 // x12 = _cmd & mask
//arm64 64 真机这里p11计算后是_bucketsAndMaybeMask
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
ldr p11, [x16, #CACHE] // p11 = mask|buckets
//arm64 + iOS + !模拟器 + 非mac应用
#if CONFIG_USE_PREOPT_CACHES
//iphone 12以后指针验证
#if __has_feature(ptrauth_calls)
//tbnz 测试位不为0则跳转。与tbz对应。 p11 第0位不为0则跳转 LLookupPreopt\Function。
tbnz p11, #0, LLookupPreopt\Function
//p10 = _bucketsAndMaybeMask & 0x0000ffffffffffff = buckets
and p10, p11, #0x0000ffffffffffff // p10 = buckets
#else
//p10 = _bucketsAndMaybeMask & 0x0000fffffffffffe = buckets
and p10, p11, #0x0000fffffffffffe // p10 = buckets
//p11 第0位不为0则跳转 LLookupPreopt\Function。
tbnz p11, #0, LLookupPreopt\Function
#endif
//eor 逻辑异或(^) 格式为:EOR{S}{cond} Rd, Rn, Operand2
//p12 = selector ^ (selector >> 7) select 右移7位&自己给到p12
eor p12, p1, p1, LSR #7
//p12 = p12 & (_bucketsAndMaybeMask >> 48) = index & mask值 = buckets中的下标
and p12, p12, p11, LSR #48 // x12 = (_cmd ^ (_cmd >> 7)) & mask
#else
//p10 = _bucketsAndMaybeMask & 0x0000ffffffffffff = buckets
and p10, p11, #0x0000ffffffffffff // p10 = buckets
//p12 = selector & (_bucketsAndMaybeMask >>48) = sel & mask = buckets中的下标
and p12, p1, p11, LSR #48 // x12 = _cmd & mask
#endif // CONFIG_USE_PREOPT_CACHES
//arm64 32
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
//后4位为mask前置0的个数的case
ldr p11, [x16, #CACHE] // p11 = mask|buckets
and p10, p11, #~0xf // p10 = buckets 相当于后4位置为0,取前32位
and p11, p11, #0xf // p11 = maskShift 取的是后4位,为mask前置位的0的个数
mov p12, #0xffff
lsr p11, p12, p11 // p11 = mask = 0xffff >> p11
and p12, p1, p11 // x12 = _cmd & mask
#else
#error Unsupported cache mask storage for ARM64.
#endif
//通过上面的计算 p10 = buckets,p11 = mask(arm64真机是_bucketsAndMaybeMask), p12 = index
// p13(bucket_t) = buckets + 下标 << 4 PTRSHIFT arm64 为3. <<4 位为16字节 buckets + 下标 *16 = buckets + index *16 也就是直接平移到了第几个元素的地址。
add p13, p10, p12, LSL #(1+PTRSHIFT)
// p13 = buckets + ((_cmd & mask) << (1+PTRSHIFT))
//这里就直接遍历查找了,因为arm64下cache_next相当于遍历(这里只扫描了前面)
// do {
//p17 = imp, p9 = sel
1: ldp p17, p9, [x13], #-BUCKET_SIZE // {imp, sel} = *bucket--
//sel - _cmd != 0 则跳转 3:,也就意味着没有找到就跳转到__objc_msgSend_uncached
cmp p9, p1 // if (sel != _cmd) {
b.ne 3f // scan more
// } else {
//找到则调用或者返回imp,Mode为 NORMAL
2: CacheHit \Mode // hit: call or return imp 命中
// }
//__objc_msgSend_uncached
//缓存中找不到方法就走__objc_msgSend_uncached逻辑了。
//cbz 为0跳转 sel == nil 跳转 \MissLabelDynamic
3: cbz p9, \MissLabelDynamic // if (sel == 0) goto Miss; 有空位没有找到说明没有缓存
//bucket_t - buckets 由于是递减操作
cmp p13, p10 // } while (bucket >= buckets) //⚠️ 这里一直是往前找,后面的元素在后面还有一次循环。
//无符号大于等于 则跳转1:f b 分别代表front与back
b.hs 1b

//没有命中cache 查找 p13 = mask对应的元素,也就是倒数第二个
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS
//p13 = buckets + (mask << 4) 平移找到对应mask的bucket_t。UXTW 将w11扩展为64位后左移4
add p13, p10, w11, UXTW #(1+PTRSHIFT)
// p13 = buckets + (mask << 1+PTRSHIFT)
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
//p13 = buckets + (mask >> 44) 这里右移44位,少移动4位就不用再左移了。因为maskZeroBits的存在 就找到了mask对应元素的地址
add p13, p10, p11, LSR #(48 - (1+PTRSHIFT))
// p13 = buckets + (mask << 1+PTRSHIFT)
// see comment about maskZeroBits
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
//p13 = buckets + (mask << 4) 找到对应mask的bucket_t。
add p13, p10, p11, LSL #(1+PTRSHIFT)
// p13 = buckets + (mask << 1+PTRSHIFT)
#else
#error Unsupported cache mask storage for ARM64.
#endif
//p12 = buckets + (p12<<4) index对应的bucket_t
add p12, p10, p12, LSL #(1+PTRSHIFT)
// p12 = first probed bucket

//之前已经往前查找过了,这里从后往index查找
// do {
//p17 = imp p9 = sel
4: ldp p17, p9, [x13], #-BUCKET_SIZE // {imp, sel} = *bucket--
//sel - _cmd
cmp p9, p1 // if (sel == _cmd)
//sel == _cmd跳转CacheHit
b.eq 2b // goto hit
//sel != nil
cmp p9, #0 // } while (sel != 0 &&
//
ccmp p13, p12, #0, ne // bucket > first_probed)
//有值跳转4:
b.hi 4b

LLookupEnd\Function:
LLookupRecover\Function:
//仍然没有找到缓存,缓存彻底不存在 __objc_msgSend_uncached()
b \MissLabelDynamic

核心逻辑:

  • 根据不同架构找到bucketssel对应的indexp10 = buckets,p11 = mask / _bucketsAndMaybeMask(arm64_64 是 _bucketsAndMaybeMask),p12 = index
    • arm64_64的情况下如果_bucketsAndMaybeMask0位为1则执行LLookupPreopt\Function
  • p13 = buckets + index << 4找到cls对应的buckets地址,地址平移找到对应bucket_t
  • do-while循环扫描buckets[index]的前半部分(后半部分逻辑不在这里)。
    • 如果存在sel为空,则说明是没有缓存的,就直接__objc_msgSend_uncached()
    • 命中直接CacheHit \Mode,这里ModeNORMAL
  • 平移获得p13 = buckets[mask]对应的元素,也就是最后一个元素(arm64下最后一个不存自身地址,也就相当于buckets[count - 1])。
  • p13 = buckets + mask << 4找到mask对应的buckets地址,地址平移找到对应bucket_t
  • do-while循环扫描buckets[mask]的前面元素,直到index(不包含index)。
    • 命中CacheHit \Mode
    • 如果存在sel为空,则说明是没有缓存的,就直接结束循环。
  • 最终仍然没有找到则执行__objc_msgSend_uncached()
  1. CACHEcache_t相对isa的偏移。 #define CACHE (2 * __SIZEOF_POINTER__)
  2. maskZeroBits始终是40p13 = buckets + (_bucketsAndMaybeMask >> 44)右移44位后就不用再<<4找到对应bucket_t的地址了。这是因为maskZeroBitsarm64_64下存在的原因。
  3. f b 分别代表frontback,往下往上的意思。

1.2 CacheLookup 伪代码实现


//NORMAL, _objc_msgSend, __objc_msgSend_uncached
void CacheLookup(Mode,Function,MissLabelDynamic,MissLabelConstant) {
//1. 根据架构不同集算sel在buckets中的index
if (arm64_64 && OSX/SIMULATOR) {
p10 = isa->cache //_bucketsAndMaybeMask
p11 = _bucketsAndMaybeMask >> 48//mask
p10 = _bucketsAndMaybeMask & 0xffffffffffff//buckets
x12 = sel & mask //index 也就是执行cache_hash
} else if (arm64_64) {//真机 //这个分支下没有计算mask
p11 = isa->cache //_bucketsAndMaybeMask
if (arm64 + iOS + !模拟器 + 非mac应用) {
if (开启指针验证 ) {
if (_bucketsAndMaybeMask 第0位 != 0) {
goto LLookupPreopt\Function
} else {
p10 = _bucketsAndMaybeMask & 0x0000ffffffffffff//buckets
}
} else {
p10 = _bucketsAndMaybeMask & 0x0000fffffffffffe //buckets
if (_bucketsAndMaybeMask 第0位 != 0) {
goto LLookupPreopt\Function
}
}
//计算index
p12 = selector ^ (selector >> 7)
p12 = p12 & (_bucketsAndMaybeMask & 48) = p12 & mask//index
} else {
p10 = _bucketsAndMaybeMask & 0x0000ffffffffffff //buckets
p12 = selector & (_bucketsAndMaybeMask >>48) //index
}
} else if (arm64_32) {
p11 = _bucketsAndMaybeMask
p10 = _bucketsAndMaybeMask &~0xf//buckets 相当于后4位置为0,取前32位
p11 = _bucketsAndMaybeMask & 0xf //mask前置位0的个数
p11 = 0xffff >> p11 //获取到mask的值
x12 = selector & mask //index
} else {
#error Unsupported cache mask storage for ARM64.
}

//通过上面的计算 p10 = buckets,p11 = mask/_bucketsAndMaybeMask, p12 = index
p13 = buckets + index << 4 //找到cls对应的buckets地址。地址平移找到对应bucket_t。

//2.找缓存(这里只扫描了前面)
do {
p13 = *bucket-- //赋值后指向前一个bucket
p17 = bucket.imp
p9 = bucket.sel
if (p9 != selector) {
if (p9 == 0) {//说明没有缓存
__objc_msgSend_uncached()
}
} else {//缓存命中,走命中逻辑 call or return imp
CacheHit \Mode
}
} while(bucket >= buckets) //buckets是首地址,bucket是index对应的buckct往前移动

//查找完后还没有缓存?
//查找 p13 = mask对应的元素,也就是最后一个元素
if (arm64_64 && OSX/SIMULATOR) {
p13 = buckets + (mask << 4)
} else if (arm64_64) {//真机
p13 = buckets + (_bucketsAndMaybeMask >> 44)//这里右移44位,少移动4位就不用再左移了。这里就找到了对应index的bucket_t。
} else if (arm64_32) {
p13 = buckets + (mask << 4)
} else {
#error Unsupported cache mask storage for ARM64.
}

//index的bucket_t 从mask对应的buckets开始再往前找
p12 = buckets + (index<<4)
do {
p17 = imp;
p9 = sel;
*p13--;
if (p9 == selector) {//命中
CacheHit \Mode
}
} while (p9 != nil && bucket > p12)//从后往前 p9位nil则证明没有存,也就不存在缓存了。

//仍然没有找到缓存,缓存彻底不存在。
__objc_msgSend_uncached()
}

二、LLookupPreopt\Function

arm64_64真机的情况下,如果_bucketsAndMaybeMask的第0位为1则会执行LLookupPreopt\Function的逻辑。简单看了下汇编发现与cache_t 中的_originalPreoptCache有关。

2.1 LLookupPreopt\Function 源码分析

LLookupPreopt\Function:
#if __has_feature(ptrauth_calls)
//p10 = _bucketsAndMaybeMask & 0x007ffffffffffffe = buckets
and p10, p11, #0x007ffffffffffffe // p10 = x
//buckets x16为cls 验证
autdb x10, x16 // auth as early as possible
#endif

// x12 = (_cmd - first_shared_cache_sel)
//(_cmd >> 12 + PAGE) << 12 + PAGEOFF 第一个sel
adrp x9, _MagicSelRef@PAGE
ldr p9, [x9, _MagicSelRef@PAGEOFF]
//差值index
sub p12, p1, p9

// w9 = ((_cmd - first_shared_cache_sel) >> hash_shift & hash_mask)
#if __has_feature(ptrauth_calls)
// bits 63..60 of x11 are the number of bits in hash_mask
// bits 59..55 of x11 is hash_shift

// 取到 hash_shift...
lsr x17, x11, #55 // w17 = (hash_shift, ...)
//w9 = index >> hash_shift
lsr w9, w12, w17 // >>= shift
//x17 = _bucketsAndMaybeMask >>60 //mask_bits
lsr x17, x11, #60 // w17 = mask_bits
mov x11, #0x7fff
//x11 = 0x7fff >> mask_bits //mask
lsr x11, x11, x17 // p11 = mask (0x7fff >> mask_bits)
//x9 = x9 & mask
and x9, x9, x11 // &= mask
#else
// bits 63..53 of x11 is hash_mask
// bits 52..48 of x11 is hash_shift
lsr x17, x11, #48 // w17 = (hash_shift, hash_mask)
lsr w9, w12, w17 // >>= shift
and x9, x9, x11, LSR #53 // &= mask
#endif
//x17 = el_offs | (imp_offs << 32)
ldr x17, [x10, x9, LSL #3] // x17 == sel_offs | (imp_offs << 32)
// cmp x12 x17 是否找到sel
cmp x12, w17, uxtw

.if \Mode == GETIMP
b.ne \MissLabelConstant // cache miss
//imp = isa - (sel_offs >> 32)
sub x0, x16, x17, LSR #32 // imp = isa - imp_offs
//注册imp
SignAsImp x0
ret
.else
b.ne 5f // cache miss
//imp(x17) = (isa - sel_offs>> 32)
sub x17, x16, x17, LSR #32 // imp = isa - imp_offs
.if \Mode == NORMAL
//跳转imp
br x17
.elseif \Mode == LOOKUP
//x16 = isa | 3 //这里为或的意思
orr x16, x16, #3 // for instrumentation, note that we hit a constant cache
//注册imp
SignAsImp x17
ret
.else
.abort unhandled mode \Mode
.endif
//x9 = buckets-1
5: ldursw x9, [x10, #-8] // offset -8 is the fallback offset
//计算回调isa x16 = x16 + x9
add x16, x16, x9 // compute the fallback isa
//使用新isa重新查找缓存
b LLookupStart\Function // lookup again with a new isa
.endif
  • 找到imp就跳转/返回。
  • 没有找到返回下一个isa重新CacheLookup
  • 这块进入的查找共享缓存, 与cache_t_originalPreoptCache有关。maskZeroBits4位就是用来判断是否有_originalPreoptCache的。

⚠️@TODO 真机调试的时候进不到这块流程,这块分析的还不是很透彻,后面再补充。

三、CacheHit

在查找缓存命中后会执行CacheHit

3.1 CacheHit源码分析

#define NORMAL 0
#define GETIMP 1
#define LOOKUP 2

// CacheHit: x17 = cached IMP, x10 = address of buckets, x1 = SEL, x16 = isa
.macro CacheHit
//这里传入的为NORMAL
.if $0 == NORMAL
//调用imp TailCallCachedImp(imp,buckets,sel,isa)
TailCallCachedImp x17, x10, x1, x16 // authenticate and call imp
.elseif $0 == GETIMP
//返回imp
mov p0, p17
//imp == nil跳转9:
cbz p0, 9f // don't ptrauth a nil imp
//有imp执行AuthAndResignAsIMP(imp,buckets,sel,isa)最后给到x0返回。
AuthAndResignAsIMP x0, x10, x1, x16 // authenticate imp and re-sign as IMP
9: ret // return IMP
.elseif $0 == LOOKUP
// No nil check for ptrauth: the caller would crash anyway when they
// jump to a nil IMP. We don't care if that jump also fails ptrauth.
//找imp(imp,buckets,sel,isa)
AuthAndResignAsIMP x17, x10, x1, x16 // authenticate imp and re-sign as IMP
//isa与x15比较
cmp x16, x15
//cinc如果相等 就将x16+1,否则就设成0.
cinc x16, x16, ne // x16 += 1 when x15 != x16 (for instrumentation ; fallback to the parent class)
ret // return imp via x17
.else
.abort oops
.endif
.endmacro
  • 这里其实走的是NORMAL逻辑,NORMALcase直接验证并且跳转imp
  • TailCallCachedImp内部执行的是imp^cls,对imp进行了解码。
  • GETIMP返回imp
  • LOOKUP查找注册imp并返回。

3.1 CacheHit伪代码实现

//x17 = cached IMP, x10 = address of buckets, x1 = SEL, x16 = isa
void CacheHit(Mode) {
if (Mode == NORMAL) {
//imp = imp^cls 解码
TailCallCachedImp x17, x10, x1, x16 // 解码跳转imp
} else if (Mode == GETIMP) {
p0 = IMP
if (p0 == nil) {
return
} else {
AuthAndResignAsIMP(imp,buckets,sel,isa)//resign cached imp as IMP
}
} else if (Mode == LOOKUP) {
AuthAndResignAsIMP(x17, buckets, sel, isa)//resign cached imp as IMP
if (isa == x15) {
x16 += 1
} else {
x16 = 0
}
} else {
.abort oops//报错
}
}

四、__objc_msgSend_uncached

在缓存没有命中的情况下会走到__objc_msgSend_uncached()的逻辑:


STATIC_ENTRY __objc_msgSend_uncached
UNWIND __objc_msgSend_uncached, FrameWithNoSaves

// THIS IS NOT A CALLABLE C FUNCTION
// Out-of-band p15 is the class to search
//查找imp
MethodTableLookup
//跳转imp
TailCallFunctionPointer x17

END_ENTRY __objc_msgSend_uncached
  • MethodTableLookup查找imp
  • TailCallFunctionPointer跳转imp

MethodTableLookup

.macro MethodTableLookup

SAVE_REGS MSGSEND

// lookUpImpOrForward(obj, sel, cls, LOOKUP_INITIALIZE | LOOKUP_RESOLVER)
// receiver and selector already in x0 and x1
//x2 = cls
mov x2, x16
//x3 = LOOKUP_INITIALIZE|LOOKUP_RESOLVER //是否初始化,imp没有实现尝试resolver
//_lookUpImpOrForward(receiver,selector,cls,LOOKUP_INITIALIZE | LOOKUP_RESOLVER)
mov x3, #3
bl _lookUpImpOrForward

// IMP in x0
mov x17, x0

RESTORE_REGS MSGSEND

.endmacro

  • 调用_lookUpImpOrForward查找imp。这里就调用到了c/c++的代码了:
IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)

最终会调用_lookUpImpOrForward进入c/c++环境逻辑。

对于架构的一些理解
LP64 //64位
x86_64 // interl 64位
i386 // intel 32位
arm // arm指令 32 位
arm64 //arm64指令
arm64 && LP64 //arm64 64位
arm64 && !LP64 //arm64 32 位


⚠️ 当然也可以通过真机跟踪汇编代码读取寄存器进行,与源码分析的是一致的,走其中的一个分支。

五、 objc_msgSend流程图



总结

  • 判断receiver是否存在。
  • 通过isa获取cls
  • cls内存平移0x10获取cache也就是_bucketsAndMaybeMask
  • 通过buckets & bucketsMask获取buckets`地址。
  • 通过bucketsMask >> maskShift获取mask
  • 通过sel & mask获取第一次查找的index
  • buckets + index << 4找到index对应的地址。
  • do-while循环判断找缓存,这次从[index~0]查找imp
  • 取到buckets[mask]继续do-while循环,从[mask~index)查找imp。两次查找过程中如果有sel为空则会结束查找。走__objc_msgSend_uncached的逻辑。
  • 找到imp就解码跳转imp


作者:HotPotCat
链接:https://www.jianshu.com/p/c29c07a1e93d



收起阅读 »

iOS GCD 实现线程安全的多读单写功能

本文测试 demo 都是在 playground 里用 Swift5 完成的. 使用 GCD实现线程安全修改数据源, 示例中的读写都是对一个字典而言, 实际开发中可以是文件的读写(FileManager 是线程安全的), 可以是数组, 根据自己情况而定.先来了...
继续阅读 »

本文测试 demo 都是在 playground 里用 Swift5 完成的. 使用 GCD实现线程安全修改数据源, 示例中的读写都是对一个字典而言, 实际开发中可以是文件的读写(FileManager 是线程安全的), 可以是数组, 根据自己情况而定.

先来了解一下 GCD 中 队列 , 任务 , 线程, 同步, 异步 之间的关系 和 特点 :
  • GCD 默认有两个队列 : 主队列 和 全局队列
  • 主队列是特殊的串行队列, 主队列的任务一定在主线程执行.
  • 全局队列就是普通的并发队列.
  • 队列中的任务遵守先进先出规则, 即 FIFO.
  • 队列只调试任务.
  • 线程来执行任务.
  • 同步执行不具有开启线程的能力
  • 异步执行具有开启线程的能力, 但是不一定会开启新线程
  • 并发队列允许开启新线程 .
  • 串行队列不允许开启新线程的能力.
  • 栅栏函数堵塞的是队列.

注意 : 主队列同步执行会造成死锁.

应用场景


    1. 开启多个任务去修改数据, 保证资源不被抢占. 比如买火车票, 多个窗口同时出票, 在服务器只能是一个一个来, 不能出现两个人同时买到同一个座位号的情况, 所以此时我们就需要保证数据安全, 即同一时间只能有一个任务去修改数据.

    1. 读操作可以允许多个任务同时加入队列, 但是要保证一个一个执行, 此处使用并发同步, 这么做是为了保证按照外部调用顺序去返回结果, 保证当前读操作完成后, 后面的操作才能进行. 其实是个假多读.

初始化代码

// 并发队列
var queue = DispatchQueue(label: "concurrent", attributes: .concurrent)
// 数据
var dictionary: [String: Any] = [:]

/// 数据初始化
func testInit() {
dictionary = [
"name": "Cooci",
"age": 18,
"girl": "xiaoxiannv"
]
}

读写的关键代码

/// 读的过程
func getSafeValueFor(_ key: String) -> Any? {
var result: Any? = nil
// 并发同步读取数据, 实际是假多读
queue.sync {
result = dictionary[key]
}
return result
}

/// 写的过程
func setSafe(_ value: Any, for key: String) {
// 在子线程完成写任务
// 等待前面任务执行完成后开始写
// 写的完成任务后, 才能继续执行后边添加进此队列的任务
queue.async(flags: .barrier) {
dictionary[key] = value
}
}

首先来看看修改数据 -- 写操作

下面是写操作测试代码和执行结果 :

/// 写的过程
func setSafe(_ value: Any, for key: String) {
queue.async(flags: .barrier) {
dictionary[key] = value
let name = dictionary[key] as? String ?? ""
print("save name = \(name) --> \(Thread.current)")
}
}
/// 测试写的过程
func testWrite() {

setSafe("AAAAA", for: "name")

setSafe("BBBBB", for: "name")

setSafe("CCCCC", for: "name")

print("所有写操作后的任务")

sleep(1)
let name4 = dictionary["name"] ?? "失败"
print("for 后边的代码任务 name4 = \(name4)")
}



  • 我们可以看到 A, B, C 三个操作按照入队的顺序依次执行, 修改数据, name4 取到的是最后一次修改的数据, 这正是我们想要的. 使用并发是为了不堵塞当前线程(当前主线程), 当前线程写操作后面的的代码可以继续执行.

  • 你可能会说, 按照 A, B, C 三个任务添加的顺序输出也不是没可能, 那咱们现在给 setSafe 函数添加一个休眠时长的参数, 让 A 操作休眠 3s, B 休眠 2s, C 休眠 0s, 看看执行顺序是怎样的.


func setSafe(_ value: Any, for key: String, sleepNum: UInt32) {
queue.async(flags: .barrier) {
sleep(sleepNum)
dictionary[key] = value
let name = dictionary[key] as? String ?? ""
print("save name = \(name) --> \(Thread.current)")
}
}
/// 测试写的过程
func testWrite() {

setSafe("AAAAA", for: "name", sleepNum: 3)

setSafe("BBBBB", for: "name", sleepNum: 1)

setSafe("CCCCC", for: "name", sleepNum: 0)

print("所有写操作后的任务")

sleep(5)
let name4 = dictionary["name"] ?? "失败"
print("for 后边的代码任务 name4 = \(name4)")
}

多次执行后的结果都是相同的, 如下图所示 :



由此可见, 添加到队列中的写操作任务(即修改数据源), 只能依次按照添加顺序进行修改, 不会出现资源抢夺现象, 保证了多线程修改数据的安全性.

注意: 此处为什么只有一个线程呢 ?
因为每个任务执行完成后, 队列中已经没有其他任务, GCD 为了节约资源开销, 所以并不会开启新的线程. 也没必要去开启.

再来看看数据的读取 -- 写

并发同步读取数据, 保证外部调用顺序. 此时会堵塞当前线程, 当前线程需要等待读取任务执行完成, 才能继续执行后边代码任务

/// 读的过程
func getSafeValueFor(_ key: String) -> Any? {
var result: Any? = nil
// 在调用此函数的线程同步执行所有添加到 queue 队列的读任务,
// 如果前边有写的任务, 由于 barrier 堵塞队列, 只能等待写任务完成
queue.sync {
result = dictionary[key]
}
return result
}

/// 测试读的过程
func testRead() {

for i in 0...11 {
let order = i % 3
switch order {
case 0:
let name = getSafeValueFor("name") as? String ?? ""
print("\(order) - name = \(name)")
case 1:
let age = getSafeValueFor("age") as? Int ?? 0
print("\(order) - age = \(age)")
case 2:
let girl = getSafeValueFor("girl") as? String ?? "---"
print("\(order) - girl = \(girl)")
default:
break
}
}

print("循环后边的任务")
}

并发异步回调方式读取数据, 当你对外部调用顺序没有要求时, 那你可以这么调用.



/// 读的过程
func getSafeValueFor(_ key: String, completion: @escaping (Any?)->Void) {
queue.async {
let result = dictionary[key]
completion(result)
}
}
func testRead() {
for i in 0...10 {
let order = i % 3
switch order {
case 0:
getSafeValueFor("name") { result in
let name = result as? String ?? "--"
print("\(order) - name = \(name) \(Thread.current)")
}
case 1:
getSafeValueFor("age") { result in
let age = result as? Int ?? 0
print("\(order) - age = \(age) \(Thread.current)")
}
case 2:
getSafeValueFor("girl") { result in
let girl = result as? String ?? "--"
print("\(order) - girl = \(girl) \(Thread.current)")
}
default:
break
}
if i == 5 {
setSafe(100, for: "age")
}
}
print("循环后边的任务")
}




作者:AndyGF
链接:https://www.jianshu.com/p/281b37174dd0



收起阅读 »

iOS 性能调优 二

12.2 测量,而不是猜测    于是现在你知道有哪些点可能会影响动画性能,那该如何修复呢?好吧,其实不需要。有很多种诡计来优化动画,但如果盲目使用的话,可能会造成更多性能上的问题,而不是修复。  &...
继续阅读 »

12.2 测量,而不是猜测

    于是现在你知道有哪些点可能会影响动画性能,那该如何修复呢?好吧,其实不需要。有很多种诡计来优化动画,但如果盲目使用的话,可能会造成更多性能上的问题,而不是修复。

    如何正确的测量而不是猜测这点很重要。根据性能相关的知识写出代码不同于仓促的优化。前者很好,后者实际上就是在浪费时间。

    那该如何测量呢?第一步就是确保在真实环境下测试你的程序。

真机测试,而不是模拟器

    当你开始做一些性能方面的工作时,一定要在真机上测试,而不是模拟器。模拟器虽然是加快开发效率的一把利器,但它不能提供准确的真机性能参数。

    模拟器运行在你的Mac上,然而Mac上的CPU往往比iOS设备要快。相反,Mac上的GPU和iOS设备的完全不一样,模拟器不得已要在软件层面(CPU)模拟设备的GPU,这意味着GPU相关的操作在模拟器上运行的更慢,尤其是使用CAEAGLLayer来写一些OpenGL的代码时候。

    这就是说在模拟器上的测试出的性能会高度失真。如果动画在模拟器上运行流畅,可能在真机上十分糟糕。如果在模拟器上运行的很卡,也可能在真机上很平滑。你无法确定。

    另一件重要的事情就是性能测试一定要用发布配置,而不是调试模式。因为当用发布环境打包的时候,编译器会引入一系列提高性能的优化,例如去掉调试符号或者移除并重新组织代码。你也可以自己做到这些,例如在发布环境禁用NSLog语句。你只关心发布性能,那才是你需要测试的点。

    最后,最好在你支持的设备中性能最差的设备上测试:如果基于iOS6开发,这意味着最好在iPhone 3GS或者iPad2上测试。如果可能的话,测试不同的设备和iOS版本,因为苹果在不同的iOS版本和设备中做了一些改变,这也可能影响到一些性能。例如iPad3明显要在动画渲染上比iPad2慢很多,因为渲染4倍多的像素点(为了支持视网膜显示)。

保持一致的帧率

    为了做到动画的平滑,你需要以60FPS(帧每秒)的速度运行,以同步屏幕刷新速率。通过基于NSTimer或者CADisplayLink的动画你可以降低到30FPS,而且效果还不错,但是没办法通过Core Animation做到这点。如果不保持60FPS的速率,就可能随机丢帧,影响到体验。

    你可以在使用的过程中明显感到有没有丢帧,但没办法通过肉眼来得到具体的数据,也没法知道你的做法有没有真的提高性能。你需要的是一系列精确的数据。

    你可以在程序中用CADisplayLink来测量帧率(就像11章“基于定时器的动画”中那样),然后在屏幕上显示出来,但应用内的FPS显示并不能够完全真实测量出Core Animation性能,因为它仅仅测出应用内的帧率。我们知道很多动画都在应用之外发生(在渲染服务器进程中处理),但同时应用内FPS计数的确可以对某些性能问题提供参考,一旦找出一个问题的地方,你就需要得到更多精确详细的数据来定位到问题所在。苹果提供了一个强大的Instruments工具集来帮我们做到这些。

收起阅读 »

iOS 性能调优 一

性能调优代码应该运行的尽量快,而不是更快 - 理查德    在第一和第二部分,我们了解了Core Animation提供的关于绘制和动画的一些特性。Core Animation功能和性能都非常强大,但如果你对背...
继续阅读 »

性能调优

代码应该运行的尽量快,而不是更快 - 理查德

    在第一和第二部分,我们了解了Core Animation提供的关于绘制和动画的一些特性。Core Animation功能和性能都非常强大,但如果你对背后的原理不清楚的话也会降低效率。让它达到最优的状态是一门艺术。在这章中,我们将探究一些动画运行慢的原因,以及如何去修复这些问题。


12.1 CPU VS GPU

    关于绘图和动画有两种处理的方式:CPU(中央处理器)和GPU(图形处理器)。在现代iOS设备中,都有可以运行不同软件的可编程芯片,但是由于历史原因,我们可以说CPU所做的工作都在软件层面,而GPU在硬件层面。

    总的来说,我们可以用软件(使用CPU)做任何事情,但是对于图像处理,通常用硬件会更快,因为GPU使用图像对高度并行浮点运算做了优化。由于某些原因,我们想尽可能把屏幕渲染的工作交给硬件去处理。问题在于GPU并没有无限制处理性能,而且一旦资源用完的话,性能就会开始下降了(即使CPU并没有完全占用)

    大多数动画性能优化都是关于智能利用GPU和CPU,使得它们都不会超出负荷。于是我们首先需要知道Core Animation是如何在这两个处理器之间分配工作的。

动画的舞台

    Core Animation处在iOS的核心地位:应用内和应用间都会用到它。一个简单的动画可能同步显示多个app的内容,例如当在iPad上多个程序之间使用手势切换,会使得多个程序同时显示在屏幕上。在一个特定的应用中用代码实现它是没有意义的,因为在iOS中不可能实现这种效果(App都是被沙箱管理,不能访问别的视图)。

    动画和屏幕上组合的图层实际上被一个单独的进程管理,而不是你的应用程序。这个进程就是所谓的渲染服务。在iOS5和之前的版本是SpringBoard进程(同时管理着iOS的主屏)。在iOS6之后的版本中叫做BackBoard

    当运行一段动画时候,这个过程会被四个分离的阶段被打破:

  • 布局 - 这是准备你的视图/图层的层级关系,以及设置图层属性(位置,背景色,边框等等)的阶段。

  • 显示 - 这是图层的寄宿图片被绘制的阶段。绘制有可能涉及你的-drawRect:-drawLayer:inContext:方法的调用路径。

  • 准备 - 这是Core Animation准备发送动画数据到渲染服务的阶段。这同时也是Core Animation将要执行一些别的事务例如解码动画过程中将要显示的图片的时间点。

  • 提交 - 这是最后的阶段,Core Animation打包所有图层和动画属性,然后通过IPC(内部处理通信)发送到渲染服务进行显示。

    但是这些仅仅阶段仅仅发生在你的应用程序之内,在动画在屏幕上显示之前仍然有更多的工作。一旦打包的图层和动画到达渲染服务进程,他们会被反序列化来形成另一个叫做渲染树的图层树(在第一章“图层树”中提到过)。使用这个树状结构,渲染服务对动画的每一帧做出如下工作:

  • 对所有的图层属性计算中间值,设置OpenGL几何形状(纹理化的三角形)来执行渲染

  • 在屏幕上渲染可见的三角形

    所以一共有六个阶段;最后两个阶段在动画过程中不停地重复。前五个阶段都在软件层面处理(通过CPU),只有最后一个被GPU执行。而且,你真正只能控制前两个阶段:布局和显示。Core Animation框架在内部处理剩下的事务,你也控制不了它。

    这并不是个问题,因为在布局和显示阶段,你可以决定哪些由CPU执行,哪些交给GPU去做。那么改如何判断呢?

GPU相关的操作

    GPU为一个具体的任务做了优化:它用来采集图片和形状(三角形),运行变换,应用纹理和混合然后把它们输送到屏幕上。现代iOS设备上可编程的GPU在这些操作的执行上又很大的灵活性,但是Core Animation并没有暴露出直接的接口。除非你想绕开Core Animation并编写你自己的OpenGL着色器,从根本上解决硬件加速的问题,那么剩下的所有都还是需要在CPU的软件层面上完成。

    宽泛的说,大多数CALayer的属性都是用GPU来绘制。比如如果你设置图层背景或者边框的颜色,那么这些可以通过着色的三角板实时绘制出来。如果对一个contents属性设置一张图片,然后裁剪它 - 它就会被纹理的三角形绘制出来,而不需要软件层面做任何绘制。

    但是有一些事情会降低(基于GPU)图层绘制,比如:

  • 太多的几何结构 - 这发生在需要太多的三角板来做变换,以应对处理器的栅格化的时候。现代iOS设备的图形芯片可以处理几百万个三角板,所以在Core Animation中几何结构并不是GPU的瓶颈所在。但由于图层在显示之前通过IPC发送到渲染服务器的时候(图层实际上是由很多小物体组成的特别重量级的对象),太多的图层就会引起CPU的瓶颈。这就限制了一次展示的图层个数(见本章后续“CPU相关操作”)。

  • 重绘 - 主要由重叠的半透明图层引起。GPU的填充比率(用颜色填充像素的比率)是有限的,所以需要避免重绘(每一帧用相同的像素填充多次)的发生。在现代iOS设备上,GPU都会应对重绘;即使是iPhone 3GS都可以处理高达2.5的重绘比率,并任然保持60帧率的渲染(这意味着你可以绘制一个半的整屏的冗余信息,而不影响性能),并且新设备可以处理更多。

  • 离屏绘制 - 这发生在当不能直接在屏幕上绘制,并且必须绘制到离屏图片的上下文中的时候。离屏绘制发生在基于CPU或者是GPU的渲染,或者是为离屏图片分配额外内存,以及切换绘制上下文,这些都会降低GPU性能。对于特定图层效果的使用,比如圆角,图层遮罩,阴影或者是图层光栅化都会强制Core Animation提前渲染图层的离屏绘制。但这不意味着你需要避免使用这些效果,只是要明白这会带来性能的负面影响。

  • 过大的图片 - 如果视图绘制超出GPU支持的2048x2048或者4096x4096尺寸的纹理,就必须要用CPU在图层每次显示之前对图片预处理,同样也会降低性能。

CPU相关的操作

    大多数工作在Core Animation的CPU都发生在动画开始之前。这意味着它不会影响到帧率,所以很好,但是他会延迟动画开始的时间,让你的界面看起来会比较迟钝。

    以下CPU的操作都会延迟动画的开始时间:

  • 布局计算 - 如果你的视图层级过于复杂,当视图呈现或者修改的时候,计算图层帧率就会消耗一部分时间。特别是使用iOS6的自动布局机制尤为明显,它应该是比老版的自动调整逻辑加强了CPU的工作。

  • 视图懒加载 - iOS只会当视图控制器的视图显示到屏幕上时才会加载它。这对内存使用和程序启动时间很有好处,但是当呈现到屏幕上之前,按下按钮导致的许多工作都会不能被及时响应。比如控制器从数据库中获取数据,或者视图从一个nib文件中加载,或者涉及IO的图片显示(见后续“IO相关操作”),都会比CPU正常操作慢得多。

  • Core Graphics绘制 - 如果对视图实现了-drawRect:方法,或者CALayerDelegate-drawLayer:inContext:方法,那么在绘制任何东西之前都会产生一个巨大的性能开销。为了支持对图层内容的任意绘制,Core Animation必须创建一个内存中等大小的寄宿图片。然后一旦绘制结束之后,必须把图片数据通过IPC传到渲染服务器。在此基础上,Core Graphics绘制就会变得十分缓慢,所以在一个对性能十分挑剔的场景下这样做十分不好。

  • 解压图片 - PNG或者JPEG压缩之后的图片文件会比同质量的位图小得多。但是在图片绘制到屏幕上之前,必须把它扩展成完整的未解压的尺寸(通常等同于图片宽 x 长 x 4个字节)。为了节省内存,iOS通常直到真正绘制的时候才去解码图片(14章“图片IO”会更详细讨论)。根据你加载图片的方式,第一次对图层内容赋值的时候(直接或者间接使用UIImageView)或者把它绘制到Core Graphics中,都需要对它解压,这样的话,对于一个较大的图片,都会占用一定的时间。

    当图层被成功打包,发送到渲染服务器之后,CPU仍然要做如下工作:为了显示屏幕上的图层,Core Animation必须对渲染树种的每个可见图层通过OpenGL循环转换成纹理三角板。由于GPU并不知晓Core Animation图层的任何结构,所以必须要由CPU做这些事情。这里CPU涉及的工作和图层个数成正比,所以如果在你的层级关系中有太多的图层,就会导致CPU没一帧的渲染,即使这些事情不是你的应用程序可控的。

IO相关操作

    还有一项没涉及的就是IO相关工作。上下文中的IO(输入/输出)指的是例如闪存或者网络接口的硬件访问。一些动画可能需要从山村(甚至是远程URL)来加载。一个典型的例子就是两个视图控制器之间的过渡效果,这就需要从一个nib文件或者是它的内容中懒加载,或者一个旋转的图片,可能在内存中尺寸太大,需要动态滚动来加载。

    IO比内存访问更慢,所以如果动画涉及到IO,就是一个大问题。总的来说,这就需要使用聪敏但尴尬的技术,也就是多线程,缓存和投机加载(提前加载当前不需要的资源,但是之后可能需要用到)。这些技术将会在第14章中讨论。

收起阅读 »

iOS 基于定时器的动画 二

11.2 物理模拟即使使用了基于定时器的动画来复制第10章中关键帧的行为,但还是会有一些本质上的区别:在关键帧的实现中,我们提前计算了所有帧,但是在新的解决方案中,我们实际上实在按需要在计算。意义在于我们可以根据用户输入实时修改动画的逻辑,或者和别的实时动画系...
继续阅读 »

11.2 物理模拟

即使使用了基于定时器的动画来复制第10章中关键帧的行为,但还是会有一些本质上的区别:在关键帧的实现中,我们提前计算了所有帧,但是在新的解决方案中,我们实际上实在按需要在计算。意义在于我们可以根据用户输入实时修改动画的逻辑,或者和别的实时动画系统例如物理引擎进行整合。

Chipmunk

我们来基于物理学创建一个真实的重力模拟效果来取代当前基于缓冲的弹性动画,但即使模拟2D的物理效果就已近极其复杂了,所以就不要尝试去实现它了,直接用开源的物理引擎库好了。

我们将要使用的物理引擎叫做Chipmunk。另外的2D物理引擎也同样可以(例如Box2D),但是Chipmunk使用纯C写的,而不是C++,好处在于更容易和Objective-C项目整合。Chipmunk有很多版本,包括一个和Objective-C绑定的“indie”版本。C语言的版本是免费的,所以我们就用它好了。在本书写作的时候6.1.4是最新的版本;你可以从http://chipmunk-physics.net下载它。

Chipmunk完整的物理引擎相当巨大复杂,但是我们只会使用如下几个类:

  • cpSpace - 这是所有的物理结构体的容器。它有一个大小和一个可选的重力矢量
  • cpBody - 它是一个固态无弹力的刚体。它有一个坐标,以及其他物理属性,例如质量,运动和摩擦系数等等。
  • cpShape - 它是一个抽象的几何形状,用来检测碰撞。可以给结构体添加一个多边形,而且cpShape有各种子类来代表不同形状的类型。

在例子中,我们来对一个木箱建模,然后在重力的影响下下落。我们来创建一个Crate类,包含屏幕上的可视效果(一个UIImageView)和一个物理模型(一个cpBody和一个cpPolyShape,一个cpShape的多边形子类来代表矩形木箱)。

用C版本的Chipmunk会带来一些挑战,因为它现在并不支持Objective-C的引用计数模型,所以我们需要准确的创建和释放对象。为了简化,我们把cpShapecpBody的生命周期和Crate类进行绑定,然后在木箱的-init方法中创建,在-dealloc中释放。木箱物理属性的配置很复杂,所以阅读了Chipmunk文档会很有意义。

视图控制器用来管理cpSpace,还有和之前一样的计时器逻辑。在每一步中,我们更新cpSpace(用来进行物理计算和所有结构体的重新摆放)然后迭代对象,然后再更新我们的木箱视图的位置来匹配木箱的模型(在这里,实际上只有一个结构体,但是之后我们将要添加更多)。

Chipmunk使用了一个和UIKit颠倒的坐标系(Y轴向上为正方向)。为了使得物理模型和视图之间的同步更简单,我们需要通过使用geometryFlipped属性翻转容器视图的集合坐标(第3章中有提到),于是模型和视图都共享一个相同的坐标系。

具体的代码见清单11.3。注意到我们并没有在任何地方释放cpSpace对象。在这个例子中,内存空间将会在整个app的生命周期中一直存在,所以这没有问题。但是在现实世界的场景中,我们需要像创建木箱结构体和形状一样去管理我们的空间,封装在标准的Cocoa对象中,然后来管理Chipmunk对象的生命周期。图11.1展示了掉落的木箱。

清单11.3 使用物理学来对掉落的木箱建模

#import "ViewController.h" 
#import
#import "chipmunk.h"

@interface Crate : UIImageView

@property (nonatomic, assign) cpBody *body;
@property (nonatomic, assign) cpShape *shape;

@end

@implementation Crate

#define MASS 100

- (id)initWithFrame:(CGRect)frame
{
if ((self = [super initWithFrame:frame])) {
//set image
self.image = [UIImage imageNamed:@"Crate.png"];
self.contentMode = UIViewContentModeScaleAspectFill;
//create the body
self.body = cpBodyNew(MASS, cpMomentForBox(MASS, frame.size.width, frame.size.height));
//create the shape
cpVect corners[] = {
cpv(0, 0),
cpv(0, frame.size.height),
cpv(frame.size.width, frame.size.height),
cpv(frame.size.width, 0),
};
self.shape = cpPolyShapeNew(self.body, 4, corners, cpv(-frame.size.width/2, -frame.size.height/2));
//set shape friction & elasticity
cpShapeSetFriction(self.shape, 0.5);
cpShapeSetElasticity(self.shape, 0.8);
//link the crate to the shape
//so we can refer to crate from callback later on
self.shape->data = (__bridge void *)self;
//set the body position to match view
cpBodySetPos(self.body, cpv(frame.origin.x + frame.size.width/2, 300 - frame.origin.y - frame.size.height/2));
}
return self;
}

- (void)dealloc
{
//release shape and body
cpShapeFree(_shape);
cpBodyFree(_body);
}

@end

@interface ViewController ()

@property (nonatomic, weak) IBOutlet UIView *containerView;
@property (nonatomic, assign) cpSpace *space;
@property (nonatomic, strong) CADisplayLink *timer;
@property (nonatomic, assign) CFTimeInterval lastStep;

@end

@implementation ViewController

#define GRAVITY 1000

- (void)viewDidLoad
{
//invert view coordinate system to match physics
self.containerView.layer.geometryFlipped = YES;
//set up physics space
self.space = cpSpaceNew();
cpSpaceSetGravity(self.space, cpv(0, -GRAVITY));
//add a crate
Crate *crate = [[Crate alloc] initWithFrame:CGRectMake(100, 0, 100, 100)];
[self.containerView addSubview:crate];
cpSpaceAddBody(self.space, crate.body);
cpSpaceAddShape(self.space, crate.shape);
//start the timer
self.lastStep = CACurrentMediaTime();
self.timer = [CADisplayLink displayLinkWithTarget:self
selector:@selector(step:)];
[self.timer addToRunLoop:[NSRunLoop mainRunLoop]
forMode:NSDefaultRunLoopMode];
}

void updateShape(cpShape *shape, void *unused)
{
//get the crate object associated with the shape
Crate *crate = (__bridge Crate *)shape->data;
//update crate view position and angle to match physics shape
cpBody *body = shape->body;
crate.center = cpBodyGetPos(body);
crate.transform = CGAffineTransformMakeRotation(cpBodyGetAngle(body));
}

- (void)step:(CADisplayLink *)timer
{
//calculate step duration
CFTimeInterval thisStep = CACurrentMediaTime();
CFTimeInterval stepDuration = thisStep - self.lastStep;
self.lastStep = thisStep;
//update physics
cpSpaceStep(self.space, stepDuration);
//update all the shapes
cpSpaceEachShape(self.space, &updateShape, NULL);
}

@end

图11.1

图11.1 一个木箱图片,根据模拟的重力掉落

添加用户交互

下一步就是在视图周围添加一道不可见的墙,这样木箱就不会掉落出屏幕之外。或许你会用另一个矩形的cpPolyShape来实现,就和之前创建木箱那样,但是我们需要检测的是木箱何时离开视图,而不是何时碰撞,所以我们需要一个空心而不是固体矩形。

我们可以通过给cpSpace添加四个cpSegmentShape对象(cpSegmentShape代表一条直线,所以四个拼起来就是一个矩形)。然后赋给空间的staticBody属性(一个不被重力影响的结构体)而不是像木箱那样一个新的cpBody实例,因为我们不想让这个边框矩形滑出屏幕或者被一个下落的木箱击中而消失。

同样可以再添加一些木箱来做一些交互。最后再添加一个加速器,这样可以通过倾斜手机来调整重力矢量(为了测试需要在一台真实的设备上运行程序,因为模拟器不支持加速器事件,即使旋转屏幕)。清单11.4展示了更新后的代码,运行结果见图11.2。

由于示例只支持横屏模式,所以交换加速计矢量的x和y值。如果在竖屏下运行程序,请把他们换回来,不然重力方向就错乱了。试一下就知道了,木箱会沿着横向移动。

清单11.4 使用围墙和多个木箱的更新后的代码

- (void)addCrateWithFrame:(CGRect)frame
{
Crate *crate = [[Crate alloc] initWithFrame:frame];
[self.containerView addSubview:crate];
cpSpaceAddBody(self.space, crate.body);
cpSpaceAddShape(self.space, crate.shape);
}

- (void)addWallShapeWithStart:(cpVect)start end:(cpVect)end
{
cpShape *wall = cpSegmentShapeNew(self.space->staticBody, start, end, 1);
cpShapeSetCollisionType(wall, 2);
cpShapeSetFriction(wall, 0.5);
cpShapeSetElasticity(wall, 0.8);
cpSpaceAddStaticShape(self.space, wall);
}

- (void)viewDidLoad
{
//invert view coordinate system to match physics
self.containerView.layer.geometryFlipped = YES;
//set up physics space
self.space = cpSpaceNew();
cpSpaceSetGravity(self.space, cpv(0, -GRAVITY));
//add wall around edge of view
[self addWallShapeWithStart:cpv(0, 0) end:cpv(300, 0)];
[self addWallShapeWithStart:cpv(300, 0) end:cpv(300, 300)];
[self addWallShapeWithStart:cpv(300, 300) end:cpv(0, 300)];
[self addWallShapeWithStart:cpv(0, 300) end:cpv(0, 0)];
//add a crates
[self addCrateWithFrame:CGRectMake(0, 0, 32, 32)];
[self addCrateWithFrame:CGRectMake(32, 0, 32, 32)];
[self addCrateWithFrame:CGRectMake(64, 0, 64, 64)];
[self addCrateWithFrame:CGRectMake(128, 0, 32, 32)];
[self addCrateWithFrame:CGRectMake(0, 32, 64, 64)];
//start the timer
self.lastStep = CACurrentMediaTime();
self.timer = [CADisplayLink displayLinkWithTarget:self
selector:@selector(step:)];
[self.timer addToRunLoop:[NSRunLoop mainRunLoop]
forMode:NSDefaultRunLoopMode];
//update gravity using accelerometer
[UIAccelerometer sharedAccelerometer].delegate = self;
[UIAccelerometer sharedAccelerometer].updateInterval = 1/60.0;
}

- (void)accelerometer:(UIAccelerometer *)accelerometer didAccelerate:(UIAcceleration *)acceleration
{
//update gravity
cpSpaceSetGravity(self.space, cpv(acceleration.y * GRAVITY, -acceleration.x * GRAVITY));
}

图11.2

图11.1 真实引力场下的木箱交互

模拟时间以及固定的时间步长

对于实现动画的缓冲效果来说,计算每帧持续的时间是一个很好的解决方案,但是对模拟物理效果并不理想。通过一个可变的时间步长来实现有着两个弊端:

  • 如果时间步长不是固定的,精确的值,物理效果的模拟也就随之不确定。这意味着即使是传入相同的输入值,也可能在不同场合下有着不同的效果。有时候没多大影响,但是在基于物理引擎的游戏下,玩家就会由于相同的操作行为导致不同的结果而感到困惑。同样也会让测试变得麻烦。

  • 由于性能故常造成的丢帧或者像电话呼入的中断都可能会造成不正确的结果。考虑一个像子弹那样快速移动物体,每一帧的更新都需要移动子弹,检测碰撞。如果两帧之间的时间加长了,子弹就会在这一步移动更远的距离,穿过围墙或者是别的障碍,这样就丢失了碰撞。

我们想得到的理想的效果就是通过固定的时间步长来计算物理效果,但是在屏幕发生重绘的时候仍然能够同步更新视图(可能会由于在我们控制范围之外造成不可预知的效果)。

幸运的是,由于我们的模型(在这个例子中就是Chipmunk的cpSpace中的cpBody)被视图(就是屏幕上代表木箱的UIView对象)分离,于是就很简单了。我们只需要根据屏幕刷新的时间跟踪时间步长,然后根据每帧去计算一个或者多个模拟出来的效果。

我们可以通过一个简单的循环来实现。通过每次CADisplayLink的启动来通知屏幕将要刷新,然后记录下当前的CACurrentMediaTime()。我们需要在一个小增量中提前重复物理模拟(这里用120分之一秒)直到赶上显示的时间。然后更新我们的视图,在屏幕刷新的时候匹配当前物理结构体的显示位置。

清单11.5展示了固定时间步长版本的代码

清单11.5 固定时间步长的木箱模拟

#define SIMULATION_STEP (1/120.0)

- (void)step:(CADisplayLink *)timer
{
//calculate frame step duration
CFTimeInterval frameTime = CACurrentMediaTime();
//update simulation
while (self.lastStep < frameTime) {
cpSpaceStep(self.space, SIMULATION_STEP);
self.lastStep += SIMULATION_STEP;
}

//update all the shapes
cpSpaceEachShape(self.space, &updateShape, NULL);
}

避免死亡螺旋

当使用固定的模拟时间步长时候,有一件事情一定要注意,就是用来计算物理效果的现实世界的时间并不会加速模拟时间步长。在我们的例子中,我们随意选择了120分之一秒来模拟物理效果。Chipmunk很快,我们的例子也很简单,所以cpSpaceStep()会完成的很好,不会延迟帧的更新。

但是如果场景很复杂,比如有上百个物体之间的交互,物理计算就会很复杂,cpSpaceStep()的计算也可能会超出1/120秒。我们没有测量出物理步长的时间,因为我们假设了相对于帧刷新来说并不重要,但是如果模拟步长更久的话,就会延迟帧率。

如果帧刷新的时间延迟的话会变得很糟糕,我们的模拟需要执行更多的次数来同步真实的时间。这些额外的步骤就会继续延迟帧的更新,等等。这就是所谓的死亡螺旋,因为最后的结果就是帧率变得越来越慢,直到最后应用程序卡死了。

我们可以通过添加一些代码在设备上来对物理步骤计算真实世界的时间,然后自动调整固定时间步长,但是实际上它不可行。其实只要保证你给容错留下足够的边长,然后在期望支持的最慢的设备上进行测试就可以了。如果物理计算超过了模拟时间的50%,就需要考虑增加模拟时间步长(或者简化场景)。如果模拟时间步长增加到超过1/60秒(一个完整的屏幕更新时间),你就需要减少动画帧率到一秒30帧或者增加CADisplayLinkframeInterval来保证不会随机丢帧,不然你的动画将会看起来不平滑。

物理模拟

收起阅读 »

JS数字之旅——Number

首先来一段神奇的数字比较的代码 23333333333333333 === 23333333333333332 // output: true 233333333333333330000000000 === 23333333333333333999999999...
继续阅读 »

首先来一段神奇的数字比较的代码


23333333333333333 === 23333333333333332
// output: true
233333333333333330000000000 === 233333333333333339999999999
// output: true

咦?明明不一样的两个数字,为啥是相等的呢?


Number


众所周知,每一种编程语言,都有自己的数字类型,像Java里面,有intfloatlongdouble等,不同的类型有不同的可表示的数字范围。


同理,JavaScript也有Number表示数字,但是没有像C、Java等语言那样有表示不同精度的类型,Number可以用来表示整数也可以表示浮点数。由于Number是在内部被表示为64位的浮点数,所以是有边界值,而这个边界值如下:


Number.MAX_VALUE
// output: 1.7976931348623157e+308
Number.MIN_VALUE
// output: 5e-324

最大正数和最小正数



Number.MAX_VALUE代表的是可表示的最大正数,Number.MIN_VALUE代表的是可表示的最小正数。它们的值分别大约是1.79E+3085e-324



这时很容易想到一个问题,那超过MAX_VALUE会发生什么呢?通过下面的代码和输出可以发现,当超过MAX_VALUE,无论什么数字,都一律认为与MAX_VALUE相等,直到超过一定值之后,就会等于Infinity


Number.MAX_VALUE
// output: 1.7976931348623157e+308
Number.MAX_VALUE+1
// output: 1.7976931348623157e+308
Number.MAX_VALUE+1e291
// output: 1.7976931348623157e+308
Number.MAX_VALUE === Number.MAX_VALUE+1e291
// output: true
Number.MAX_VALUE+1e292
// output: Infinity

很明显,最开始的那段代码里面的数字,是在这两个Number.MAX_VALUENumber.MIN_VALUE,没有出现越界的情况,但为什么会发生比较上错误呢?


安全整数


其实,Number还有另一个概念,叫做安全整数,其中MAX_SAFE_INTEGER的定义是最大的整数n,使得n和n+1都能准确表示。而MIN_SAFE_INTEGER的定义则是最小的整数n,使得n和n-1都能准确表示。如下面代码所示,9007199254740991(2^53 - 1) 和 -9007199254740991(-(2^53 - 1)) 就是符合定义的最大和最小整数。


Number.MAX_SAFE_INTEGER
// output: 9007199254740991
Number.MAX_SAFE_INTEGER+1
// output: 9007199254740992
Number.MAX_SAFE_INTEGER+2
// output: 9007199254740992

Number.MIN_SAFE_INTEGER
// output: -9007199254740991
Number.MIN_SAFE_INTEGER-1
// output: -9007199254740992
Number.MIN_SAFE_INTEGER-2
// output: -9007199254740992

这意味着,在这个范围内的整数,进行计算或比较都是精确的。而超过这个区域的整数,则是不安全、有较大误差的。


现在回头看最开始的代码,很明显这些数字都已经超过MAX_SAFE_INTEGER。另外,也可以使用Number.isSafeInteger方法去判断是否是安全整数


Number.isSafeInteger(23333333333333333)
// output: false

Infinity


关于Infinity的一个有趣的地方是,它是一个数字类型,但既不是NaN,也不是整数。


typeof Infinity
// output: "number"
Number.isNaN(Infinity)
// output: false
Number.isInteger(Infinity)
// output: false

进制


JavaScript里面,除了支持十进制以外,也支持二进制、八进制和十六进制的字面量。



  • 二进制:以0b开头

  • 八进制:以0开头

  • 十六进制:以0x开头


0b11001
// output: 25
0234
// output: 156
0x1A2B
// output: 6699

里面出现的字母是不区分大小写,可以放心使用。那当我们遇到上面的进制以字符串形式出现的时候,如何解决呢?答案是使用parseInt


parseInt('11001', 2)
// output: 25
parseInt('0234', 8)
// output: 156
parseInt('0x1A2B', 16)
// output: 6699

parseInt这个方法实际上支持2-36进制的转换,虽然平时绝大多数情况,它通常被用来转十进制格式的字符串,而不会特别声明第二个参数。需要注意的一个特别的点是,部分浏览器,如果字符串是0开头的话,不带第二个参数的话,会默认以八进制进行换算,从而导致一些意想不到的bug。所以,保险起见,还是应该声明第二个参数。


浮点数计算


前面基本上都是在讨论整数,Number在浮点数计算方面,就显得有些力不从心,经常出现摸不着头脑的情况,最著名的莫过于0.1 + 0.2不等于0.3的精度问题。


0.1 + 0.2 === 0.3
// output: false
0.1 + 0.2
// output: 0.30000000000000004
1.5 * 1.2
// output: 1.7999999999999998

但其实这并非JavaScript特有的现象,像Java等语言也是有这种问题存在。究其原因,是因为我们以十进制的角度去计算,但计算机本身是以二进制运行和计算的,这就会导致在浮点数类型的表示和计算上,会存在一定的偏差,当这种偏差累计足够大的时候,就会导致精度问题。


正所谓解决办法总比困难多,仔细想想还是能找到一些解决方案。


有一种思路是,既然尽管出现误差也只有一点点,那就通过toFixed()强行限制小数位数,让存在误差的数值进行转换从而消除误差,但有一定的局限性,当遇上乘法和除法的时候,需要确认限制小数位数具体要多少个才合适。


另一种思路是,浮点数计算有误差,但是整数计算是准确的,那就把浮点数放大转换为整数,然后进行计算后再转换为浮点数。跟上面的方案类似,同样需要解决放大多少倍,以及后面转换为浮点数时的额外计算。


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

收起阅读 »

前端动画lottie-web

lottie是一个跨平台的动画库,通过AE(After Effects)制作动画,再通过AE插件Bodymovin导出Json文件,最终各个终端解析这个Json文件,还原动画。本文中我只介绍前端用到的库lottie-web。 对比三种常规的制作动画方式 Pn...
继续阅读 »

lottie是一个跨平台的动画库,通过AE(After Effects)制作动画,再通过AE插件Bodymovin导出Json文件,最终各个终端解析这个Json文件,还原动画。本文中我只介绍前端用到的库lottie-web。


对比三种常规的制作动画方式



  1. Png序列帧

  2. 2.Gif图

  3. 前端Svg API


先对位图与矢量图有一个基本的概念。



矢量图就是使用直线和曲线来描述的图形,构成这些图形的元素是一些点、线、矩形、多边形、圆和弧线等,它们都是通过数学公式计算获得的,具有编辑后不失真的特点。

位图是由称作像素(图片元素)的单个点组成的,放大会失真。\



Png序列帧


用Png序列帧是也容易理解,用css keyframes操作每一帧需要展示的图片,缺点也很明显,每一帧都是一张图片,占比较大的体积。当然也可以将图片合并成精灵图(Sprites Map),可参考这个方案,使用 gka 一键生成帧动画。Png也是位图,放大会失真,不过可以通过增大图片尺寸,避免模糊。


Gif图


如果之前没有用过动画,用Gif图是最简单的,只需要引入一张图。但是Gif图是位图,不是矢量图,放大会虚。


前端Svg API


Svg API对于动画初学者不太友好,你要实现一个自定义的动画,需要了解Svg的所有的API,虽然它的属性与css的动画有一些相似。它是矢量图,不失真。


lottie


而lottie是一个不太占体积,还原度高,对于初学者友好的库。设计师制作好动画,并且利用Bodymovin插件导出Json文件。而前端直接引用lottie-web库即可,它默认的渲染方式是svg,原理就是用JS操作Svg API。但是前端完全不需要关心动画的过程,Json文件里有每一帧动画的信息,而库会帮我们执行每一帧。


前端安装lottie-web插件


npm install lottie-web

代码调用


import lottie from 'lottie-web';

this.animation = lottie.loadAnimation({
container: this.animationRef.current,
renderer: 'svg',
loop: false,
autoplay: false,
animationData: dataJson,
assetsPath: CDN_URL,
});

介绍一个每个属性的意思。



  • container 当前需要渲染的DOM

  • renderer,渲染方式,默认是Svg,还有Html和Canvas方案。

  • loop 是否循环播放

  • autoplay 是否自动播放

  • animationData AE导出的Json,注意,这里不是路径

  • assetsPath Json文件里资源的绝对路径,webpack项目需要配合这个参数。


动画的播放与暂停,如果动画需要用户触发与暂停,需要有一个切换操作(toggle)


this.animation.play();
this.animation.pause();

动画执行过程中的钩子,可以对动画有一定的控制权



  • complete

  • loopComplete

  • enterFrame

  • segmentStart

  • config_ready(初始配置完成)

  • data_ready(所有动画数据加载完成)

  • DOMLoaded(元素已添加到DOM节点)

  • destroy


// 动画播放完成触发
anm.addEventListener('complete', anmLoaded);

// 当前循环播放完成触发
anm.addEventListener('loopComplete', anmComplete);

// 播放一帧动画的时候触发
anm.addEventListener('enterFrame', enterFrame);

打包时图片资源路径


webpack工程需要注意Json文件如果有图片资源(Png或者Svg),需要将文件放在项目的根目录的static下。这样打包的时候,图片会被打包,并且后缀名不会被改变,当然需要配合assetsPath这个参数,设置图片的绝对路径。而CDN的路径可以通过process.env.CDN_URL从webpack传到前端代码中。


关于源码


关于lottie源码解析,这位老哥已经分析的挺到位了,Lottie原理与源码解析。尽管lottie也一直在迭代,但是顺着这篇解析应该也能理清源码。以及Svg动画的介绍,SVG 动画精髓



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

收起阅读 »

居然不知道CSS能做3D?天空盒子了解一下,颠覆想象?

大家好,这次给大家换换口味,我们来点不一样的东西。请不要理解歪了🐶。 上周六和昊神的一聊,然后就有了这篇文章。 通过H5实现3D全景是挺平常的事情了,通过three.js可以很容易实现一个全景图。 可以这个链接来查看,three.js来实现的,戳👇thre...
继续阅读 »

大家好,这次给大家换换口味,我们来点不一样的东西。请不要理解歪了🐶。


image.png


上周六和昊神的一聊,然后就有了这篇文章。


通过H5实现3D全景是挺平常的事情了,通过three.js可以很容易实现一个全景图。


image.png
可以这个链接来查看,three.js来实现的,戳👇three.js全景图DEMO链接


其实我们通过CSS3也能实现类似的效果,而且性能上更好,兼容性更好,支持低端机型。


是不是很惊讶,CSS居然也能做这种事情?


image.png


好了,放放手上的事情,花10多分钟专心致志🐶,羽飞老师的课开始了。


注意⚠️:建议PC端观摩,因为有挺多例子需要查看后理解更好,不过也不太影响,为手机同学准备了比较多的gif图,准备地好疲乏🥱。


由于本文重点在最后章,文中借用了一些DEMO方便快速带入,可能有所纰漏,欢迎各位大佬拍砖🧱、吐槽💬。


〇 背景


17年双十一前夕,其实也前不了多少天(大家都懂),产品找到我,说要做它,赶在双十一前上线,然后就有了它🐶。


开门见山,直接甩上成品给大家看看。


image.png


那......我就开动啦。我们先看看成品是长啥样的。



可以查看这个,👇CSS全景图DEMO链接


image.png


或者通过如上CSS全景图DEMO二维码进行尝试。


如果是“尊贵”的苹果手机用户🐶,在iOS13以上需要允许陀螺仪才可,如下图,得点击屏幕授权通过。iOS13之前都是默认开启的,苹果真的是一点不考虑向下兼容🥲,有点霸道呀。


image.png


扯远了扯远了,收。


这个时候大家就可以通过旋转手机或拖拽来查看整个全景图了。


image.png


是不是还挺神奇的?不是?


image.png


还是不是?🐶。🦢🦢🦢,不能向苹果学习🐶。


回来回来,接下来讲讲原理,先看看前置知识点。


〇 前置知识


看问题先看全貌,我们先来了解下如题中所提的天空盒子是什么概念。


天空盒子


天空盒子其实通俗的理解,可以理解如果把你放到天空中,上下前后左右都是蓝色的天空。而这个天空可以简单的用六边形来实现。


如下图所示,六边组成了一个封闭空间。



如果把你放到这个空间里,然后把每个空间的墙壁弄成天蓝色,而且每面都是纯蓝天色,这样你就分辨不出自己是不是在天上,还是只是在一个封闭的天空盒子里。



细思极恐,让人想到了缸中之脑,没听过的同学可以看看百度百科的缸中之脑解释


好了,回归主题👻。这样一个天空盒子就形成了一个全景空间图。


那CSS是要怎么才能实现一个天空盒子呢?我们继续。


image.png


CSS 3D坐标系


先来了解一下坐标系的概念。


从二维“反降维”到三维,需要理解下这个坐标系。


image.png


我们可以看到增加一个Z纬度的线,平面就变3D了。


这里需要注意的是CSS3D中,上下轴是Y轴,左右轴是X轴,前后轴是Z轴。可以简单理解为在原有竖着的面对我们的平面中,在X和Y轴中间强行插入一根直线,与Y轴和X轴都成90度,这根直线就是Z轴。


通过上面的处理,这样就形成了一个空间坐标系。


这有什么用呢?


image.png


大家可能有点懵逼,感觉二维都没搞定,突然要搞三维了。


可以先看看这个3D坐标系的DEMO,👇链接在此,可以先随意把玩把玩。



可以看到途中绿色线就是Z轴,红色就是X轴,蓝色就是Z轴。


多玩一玩就有点感觉啦,是不是感觉逐渐有了3D空间的感觉。


没有?


image.png


其他同学们,不要他了,我们继续。


image.png


不管你了,辛苦做了好久的DEMO🐶。继续继续。


如果想深入了解此CSS 3D坐标系演示的DEMO,源码可以查看这里,👇链接在此


说到CSS 3D,肯定离不开CSS3D transform,下面开始学习。


CSS 3D transform


3D transform字面意思翻译过来为三维变换。


3D rotate


我们先从rotate 3d(旋转)开始,这个能辅助我们理解3D坐标系。


rotate X


单杠运动员,如果正面对着我们,就是可以理解为围着X转。


image.png


rotate Y


围着钢管转,就可以理解为围着Y轴在转。



rotate Z


如果我们正面对着摩天轮,其实摩天轮就在围着Z轴在做运动,中间那个白点,可以理解为Z轴从这个圆圈穿透过去的点。



如果还没理解的同学,可以通过之前的CSS3D DEMO,👇链接在此,辅助理解3D rotate。


理解了3D rotate后,可以辅助我们理解三维坐标系。下面我们开始讲解perspective,有一些理解的难度哦。


image.png


perspective


perspective是做什么用的呢?字面意思是视角、透视的意思。


有一种我们从小到大看到的想象,可能我们都并不在意了,就是现实生活中的透视。比如同样的电线杆,会进高远低。其实这个现象是有一些规律的:近大远小、近实远虚、近宽远窄。


image.png


因此在素描、建筑的行业,都会通过一种透视的方式来表达现实世界的3D模型。


image.png


而我们在计算机世界怎么表达3D呢?


image.png


上方图可以辅助大家理解3D的透视perspective,黄色的是电脑或手机屏幕,红色是屏幕里的方块。


image.png


再看看上面这个二维图,可以看到,perspective: 800,代表3D物体距离屏幕(中间那个平面)是800px。


这里还有个概念,perspective-origin,可以看到上面perspective-origin是50% 50%,可以理解为眼睛视角的中心点,分别在x轴、y轴(x轴50%,y轴50%)交叉处。


image.png


没事没事,如果上面这些还不够你理解的,可以看看下面这张图。再不懂就不管你了🐶。


「下图来自:CSS 3D - Scrolling on the z-axis | CSS | devNotes
image.png


上图里的Z就是Z轴的值。Z轴如果是正数的离屏幕更近,如果是负数离屏幕更远。


而Z轴的远近和translateZ分不开,下面来讲解translateZ。


image.png


translateZ


这个属性可以帮助我们理解perspective。


可以通过translate的DEMO进行把玩把玩,有助于理解,戳👇DEMO链接在此



translateZ实现了CSS3D世界空间的近大远小。


看一下这个例子,平面上的translateZ的变换,戳👇DEMO链接在此


Kapture 2021-08-18 at 14.06.30.gif


比如,我们设置元素perspective为201px,则其子元素的translateZ值越小,则看着越小;如果translateZ值越大,则看着越大。当translateZ为200px的时候,该元素会撑满屏幕,当超过201px时候,该元素消失了,跑到我们眼睛后面了。


平面上的translateZ感受完了,来试试三维下的,看看这个DEMO,戳👇链接在此



上图中,如果把perspective往左拖,可以发现front面会离我们越来越远,如果往右拖,反之。


通过这么一节,基本translateZ的作用,大家应该都能理解到位了,还没有?回头看看🐶。


image.png


模拟现实3D空间


其实计算机的3D世界就是现实3D世界的模拟。而和计算机的3D世界中,构建3D空间概念很相近的现实场景,是摄像。我们可以考虑一下如果你去拍照,会有几个要素?


第一个:镜头,第二个:拍摄的环境的空间,第三个:要拍摄的物件。


「下图来自搞懂 CSS 3D,你必须理解 perspective(视域)


image.png


而在CSS的3D世界,我们也需要去模仿这三要素。我们用三层div来表示,第一层是摄像镜头、第二层是立体空间或也可叫舞台,第三层是立体空间内的元素。


大致的HTML代码如下。


<div class="camera">
<div class="space">
<div class="box">
</div>
</div>
</div>

下面就是真枪实弹地干了。


image.png


〇 实现天空盒子


已经知道了足够的前置知识,我们来简单实现一下天空盒子。


六面盒子


需要生成前后、左右、上下六个面。首先我们想一下第一面前面应该怎么放?


前面墙


假设我们在天空盒子(是一个正方体1024px*1024px),我们在正方体里面的中心点,那我们要往前面的墙上贴一张图,需要做什么?


我们回顾下坐标系。


image.png


你可以想象自己站在x轴和y轴交叉的中心点,即你在正方体的中心点。则你的前面的墙就是在z为-512px处,因为是前面,我们无需对这个墙进行旋转。


<html>
<head>
<title>CSS3D天空盒子</title>
<style>
html,
body {
margin: 0;
overflow: hidden;
background-color: #ccc;
}
.camera {
perspective: 512px;
perspective-origin:50% 50%;
}
.space {
width: 1024px;
height: 1024px;
margin: 0 auto;
transform-style: preserve-3d;
}
.space img {
width: 1024px;
height: 1024px;
position: absolute;
}
.space .front {
/* 正面的图无需旋转 */
transform: rotateZ(0) rotateY(0) rotateZ(0) translateZ(-512px);
}
</style>
</head>

<body>
<div class="camera" id="camera">
<div class="space">
<img class="front" src="//yun.dui88.com/tuia/junhehe/skybox/front.jpg" alt="" />
</div>
</div>
</body>
</html>

生成如下页面,演示代码地址:。
image.png


可以看到第一张图被放在了前面。


左面墙


从前面墙放上一张图,然后转向左面墙,需要几步走?


image.png


第一步,需要让平面与前面的墙垂直,这个时候我们需要把左面的图绕着Y轴旋转90度。


左面墙的图本应该放在X轴的-512px位置,但由于做了旋转,所以左面墙对应的坐标系也做了绕着Y轴向下旋转了90度。如果我们想把左侧的图放到对应的位置,我们需要让其在Z轴的-512px位置。


因此代码如下。


<html>
<head>
<title>CSS3D天空盒子</title>
<style>
html,
body {
margin: 0;
overflow: hidden;
background-color: #ccc;
}
.camera {
perspective: 512px;
perspective-origin:50% 50%;
}
.space {
width: 1024px;
height: 1024px;
margin: 0 auto;
transform-style: preserve-3d;
}
.space img {
width: 1024px;
height: 1024px;
position: absolute;
}
.space .front {
/* 正面的图无需旋转 */
transform: rotateZ(0) rotateY(0) rotateZ(0) translateZ(-512px);
}
.space .left {
transform: rotateY(90deg) translateZ(-512px);
}
</style>
</head>
<body>
<div class="camera" id="camera">
<div class="space">
<img class="front" src="//yun.dui88.com/tuia/junhehe/skybox/front.jpg" alt="" />
<img class="left" src="//yun.dui88.com/tuia/junhehe/skybox/left.jpg" alt="" />
</div>
</div>
</body>
</html>

生成的页面如下,演示代码地址


image.png


可以看到左面墙确实生成在了前面墙的左侧。


底面


类似前面墙、左面墙,我们把底面,做了绕着X轴旋转90度,然后沿着Y轴走-512px。


代码如下。


<html>
<head>
<title>CSS3D天空盒子</title>
<style>
html,
body {
margin: 0;
overflow: hidden;
background-color: #ccc;
}
.camera {
perspective: 512px;
perspective-origin:50% 50%;
}
.space {
width: 1024px;
height: 1024px;
margin: 0 auto;
transform-style: preserve-3d;
}
.space img {
width: 1024px;
height: 1024px;
position: absolute;
}
.space .front {
/* 正面的图无需旋转 */
transform: rotateZ(0) rotateY(0) rotateZ(0) translateZ(-512px);
}
.space .left {
transform: rotateY(90deg) translateZ(-512px);
}
.space .bottom {
transform: rotateX(90deg) translateZ(-512px);
}
</style>
</head>
<body>
<div class="camera" id="camera">
<div class="space">
<img class="front" src="//yun.dui88.com/tuia/junhehe/skybox/front.jpg" alt="" />
<img class="left" src="//yun.dui88.com/tuia/junhehe/skybox/left.jpg" alt="" />
<img class="bottom" src="//yun.dui88.com/tuia/junhehe/skybox/bottom.jpg" alt="" />
</div>
</div>
</body>
</html>

生成页面如下,演示代码地址


image.png


可以看到我们底部也有了,看看所有面集成后是什么样。


image.png


所有面


类似上面的操作,我们把六个面补全,下面我们就把六个面都集合起来。


<html>
<head>
<title>CSS3D天空盒子</title>
<style>
html,
body {
overflow: hidden;
margin: 0;
}
.camera {
perspective: 512px;
perspective-origin:50% 50%;
}
.space {
width: 1024px;
height: 1024px;
margin: 0 auto;
transform-style: preserve-3d;
}
.space img {
width: 1024px;
height: 1024px;
position: absolute;
}
.space .front {
/* 正面的图无需旋转 */
transform: rotateZ(0) rotateY(0) rotateZ(0) translateZ(-512px);
}
.space .back {
transform: rotateY(180deg) translateZ(-512px);
}
.space .left {
transform: rotateY(90deg) translateZ(-512px);
}
.space .right {
transform: rotateY(-90deg) translateZ(-512px);
}
.space .bottom {
transform: rotateX(90deg) translateZ(-512px);
}
.space .top {
transform: rotateX(-90deg) translateZ(-512px);
}
</style>
</head>
<body>
<div class="camera" id="camera">
<div class="space">
<img class="front" src="//yun.dui88.com/tuia/junhehe/skybox/front.jpg" alt="" />
<img class="back" src="//yun.dui88.com/tuia/junhehe/skybox/back.jpg" alt="" />
<img class="left" src="//yun.dui88.com/tuia/junhehe/skybox/left.jpg" alt="" />
<img class="right" src="//yun.dui88.com/tuia/junhehe/skybox/right.jpg" alt="" />
<img class="bottom" src="//yun.dui88.com/tuia/junhehe/skybox/bottom.jpg" alt="" />
<img class="top" src="//yun.dui88.com/tuia/junhehe/skybox/top.jpg" alt="" />
</div>
</div>
</body>
</html>

生成页面如下,演示代码地址


image.png


我们发现看不到后方墙(背面墙)。所以我们打算把整个场景转起来。


image.png


盒子旋转


怎么才能把盒子进行旋转?这里需要对六面墙所在的场景,也即是它们上一层的元素。


我们给.cube加上一个动画效果,绕着Y轴钢管舞🐶,回忆起前置知识里的钢管舞没?


<html>
<head>
<title>CSS3D天空盒子</title>
<style>
html,
body {
overflow: hidden;
margin: 0;
}
.camera {
perspective: 512px;
perspective-origin:50% 50%;

}
.space {
width: 1024px;
height: 1024px;
margin: 0 auto;
transform-style: preserve-3d;
}
.space img {
width: 1024px;
height: 1024px;
position: absolute;
}
.space .front {
/* 正面的图无需旋转 */
transform: rotateZ(0) rotateY(0) rotateZ(0) translateZ(-512px);
}
.space .back {
transform: rotateY(180deg) translateZ(-512px);
}
.space .left {
transform: rotateY(90deg) translateZ(-512px);
}
.space .right {
transform: rotateY(-90deg) translateZ(-512px);
}
.space .bottom {
transform: rotateX(90deg) translateZ(-512px);
}
.space .top {
transform: rotateX(-90deg) translateZ(-512px);
}
@keyframes rot {
0% {
transform: rotateY(0deg)
}

10% {
transform: rotateY(90deg)
}

25% {
transform: rotateY(90deg)
}

35% {
transform: rotateY(180deg)
}

50% {
transform: rotateY(180deg)
}

60% {
transform: rotateY(270deg)
}

75% {
transform: rotateY(270deg)
}

85% {
transform: rotateY(360deg)
}

100% {
transform: rotateY(360deg)
}
}
/*为立方体加上帧动画*/
.space {
animation: rot 8s ease-out 0s infinite forwards;
}
</style>
</head>
<body>
<div class="camera" id="camera">
<div class="space">
<img class="front" src="//yun.dui88.com/tuia/junhehe/skybox/front.jpg" alt="" />
<img class="back" src="//yun.dui88.com/tuia/junhehe/skybox/back.jpg" alt="" />
<img class="left" src="//yun.dui88.com/tuia/junhehe/skybox/left.jpg" alt="" />
<img class="right" src="//yun.dui88.com/tuia/junhehe/skybox/right.jpg" alt="" />
<img class="bottom" src="//yun.dui88.com/tuia/junhehe/skybox/bottom.jpg" alt="" />
<img class="top" src="//yun.dui88.com/tuia/junhehe/skybox/top.jpg" alt="" />
</div>
</div>
</body>
</html>

生成页面动画效果如下,这次用的手机拍摄的更真实一些😂,虽然有点糊,演示代码地址


gif (1).gif


既然能自动旋转,我们是不是可以考虑用手动旋转呢?


image.png


手动旋转


大概原理,就是手动拖拽(手机是touchmove,PC是mousemove),拖拽过去走的多少路程,计算出角度,然后把这个角度通过DOM设置(这个过程通过requestAnimationFrame不停地轮询设置)。


启动手动拖拽的代码。


var curMouseX = 0;
var curMouseY = 0;
var lastMouseX = 0;
var lastMouseY = 0;

if (isAndroid || isiOS) {
document.addEventListener('touchstart', mouseDownHandler);
document.addEventListener('touchmove', mouseMoveHandler);
} else {
document.addEventListener('mousedown', mouseDownHandler);
document.addEventListener('mousemove', mouseMoveHandler);
}

function mouseDownHandler(evt) {
lastMouseX = evt.pageX || evt.targetTouches[0].pageX;
lastMouseY = evt.pageY || evt.targetTouches[0].pageY;
}

function mouseMoveHandler(evt) {
curMouseX = evt.pageX || evt.targetTouches[0].pageX;
curMouseY = evt.pageY || evt.targetTouches[0].pageY;
}

具体的不分析了,不是本次的重点。有兴趣的可以直接看代码深入。


且由于我们想使用在手机上,因此做了rem的适配,适配在手机端。


生成页面动画效果如下,演示代码地址



上面是手机录制的旋转视频。既然我们能通过手触旋转,那我们肯定也可以进行陀螺仪旋转。


陀螺仪旋转


大致原理也是如上,把手动拖拽换成了陀螺仪旋转,然后计算旋转角度。


启动陀螺仪的代码。


window.addEventListener('deviceorientation', motionHandler, false)
function motionHandler(event) {
var x = event.beta;
var y = event.gamma;
}

自开头所说,陀螺仪在IOS13+下需要授权。


var isiOS = !!u.match(/\(i[^;]+;( U;)? CPU.+Mac OS X/); // ios??
if (isiOS) {
permission()
}

function permission () {
if ( typeof( DeviceMotionEvent ) !== "undefined" && typeof( DeviceMotionEvent.requestPermission ) === "function" ) {
// (optional) Do something before API request prompt.
DeviceMotionEvent.requestPermission()
.then( response => {
// (optional) Do something after API prompt dismissed.
if ( response == "granted" ) {
window.addEventListener( "devicemotion", (e) => {
// do something for 'e' here.
})
}
})
.catch( console.error )
} else {
alert( "请使用手机浏览器" );
}
}

下面是手机录制展示陀螺仪的例子,生成页面动画效果如下,演示代码地址



这里想深入的同学,可以看一下代码,和上面一样不是本文的重点就不分析了。


有没有感觉写了这么多代码,感觉跟写纯JS操作DOM似的,有没有类似JQuery之类的库呢?


image.png


css3d-engine


上面只是实现了平行旋转,要实现任意角度旋转,我们是基于css3d-engine做了实现。


这一节只是带过,理解了大概的原理后,结合例子去学习这个库还是非常快的。


部分示例代码


文章第一个DEMO就是以这个库为基础进行实践的,地址在这里:github.com/shrekshrek/…


创建stage,stage是舞台,是整个场景的根。


var s = new C3D.Stage();  

创建一个天空盒子的例子,控制各面的素材。


//创建1个立方体放入场景
var c = new C3D.Skybox();
c.size(1024).position(0, 0, 0).material({
front: {image: "images/cube_FR.jpg"},
back: {image: "images/cube_BK.jpg"},
left: {image: "images/cube_LF.jpg"},
right: {image: "images/cube_RT.jpg"},
up: {image: "images/cube_UP.jpg"},
down: {image: "images/cube_DN.jpg"},
}).update();
s.addChild(c);

Tween制作动效


第一个DEMO中动效,是通过Tween.js实现的,地址在这里:github.com/sole/tween.…


为什么DOM元素会有动效,也是因为属性值的变化,而Tween可以控制属性值在一段时间内按规定的规律变化。


下面是一个Tween的示例。


var coords = { x: 0, y: 0 };
var tween = new TWEEN.Tween(coords)
.to({ x: 100, y: 100 }, 1000)
.onUpdate(function() {
console.log(this.x, this.y);
})
.start();

requestAnimationFrame(animate);

function animate(time) {
requestAnimationFrame(animate);
TWEEN.update(time);
}

在最后再体验一下整个处理好后的DEMO,重新感受一下。


image.png


具体的完整版DEMO的源码在此,有兴趣的可以深入研究,由于是之前早几年做的DEMO,代码比较乱,还请见谅,地址在此:github.com/fly0o0/css3…



作者:羽飞
链接:https://juejin.cn/post/6997697496176820255

收起阅读 »

奇思妙想 CSS 3D 动画 | 仅使用 CSS 能制作出多惊艳的动画?

本文将从比较多的方面详细阐述如何利用 CSS 3D 的特性,实现各类有趣、酷炫的动画效果。认真读完,你将会收获到: 了解 CSS 3D 的各种用途 激发你新的灵感,感受动画之美 对于提升 CSS 动画制作水平会有所帮助 CSS 3D 基础知识 本文默认读者...
继续阅读 »

本文将从比较多的方面详细阐述如何利用 CSS 3D 的特性,实现各类有趣、酷炫的动画效果。认真读完,你将会收获到:



  • 了解 CSS 3D 的各种用途

  • 激发你新的灵感,感受动画之美

  • 对于提升 CSS 动画制作水平会有所帮助


CSS 3D 基础知识


本文默认读者掌握一定的 CSS 3D 知识,能够绘制初步的 3D 动画效果。当然这里会再简单过一下 CSS 3D 的基础知识。


使用 transform-style 启用 3D 模式


要利用 CSS3 实现 3D 的效果,最主要的就是借助 transform-style 属性。transform-style 只有两个值可以选择:


// 语法:
transform-style: flat|preserve-3d;

transform-style: flat; // 默认,子元素将不保留其 3D 位置
transform-style: preserve-3d; // 子元素将保留其 3D 位置。

当我们指定一个容器的 transform-style 的属性值为 preserve-3d 时,容器的后代元素便会具有 3D 效果,这样说有点抽象,也就是当前父容器设置了 preserve-3d 值后,它的子元素就可以相对于父元素所在的平面,进行 3D 变形操作。


利用 perspective & perspective-origin 设置 3D视距,实现透视/景深效果


perspective 为一个元素设置三维透视的距离,仅作用于元素的后代,而不是其元素本身。


简单来说,当元素没有设置 perspective 时,也就是当 perspective:none/0 时所有后代元素被压缩在同一个二维平面上,不存在景深的效果。


而如果设置 perspective 后,将会看到三维的效果。


// 语法
perspective: number|none;

// 语法
perspective-origin: x-axis y-axis;
// x-axis : 定义该视图在 x 轴上的位置。默认值:50%
// y-axis : 定义该视图在 y 轴上的位置。默认值:50%

perspective-origin 表示 3D 元素透视视角的基点位置,默认的透视视角中心在容器是 perspective 所在的元素,而不是他的后代元素的中点,也就是 perspective-origin: 50% 50%


通过绘制 Webpack Logo 熟悉 CSS 3D


对于初次接触 CSS 3D 的同学而言,可以通过绘制正方体快速熟悉语法,了解规则。


而 Webpack 的 Logo,正是由 2 个 立方体组成:



以其中一个正方体而言,实现它其实非常容易:



  1. 一个正方体由 6 个面组成,所以首先设定一个父元素 div,然后这个 div 再包含 6 个子 div,同时,父元素设置 transform-style: preserve-3d

  2. 6 个子元素,依次首先旋转不同角度,再通过 translateZ 位移正方体长度的一半距离即可

  3. 父元素可以通过 transformperspective 调整视觉角度


以一个正方体为例子,简单的伪代码如下:


<ul class="cube-inner">
<li class="top"></li>
<li class="bottom"></li>
<li class="front"></li>
<li class="back"></li>
<li class="right"></li>
<li class="left"></li>
</ul>

.cube {
width: 100px;
height: 100px;
transform-style: preserve-3d;
transform-origin: 50px 50px;
transform: rotateX(-33.5deg) rotateY(45deg);

li {
position: absolute;
top: 0;
left: 0;
width: 100px;
height: 100px;
background: rgba(141, 214, 249);
border: 1px solid #fff;
}
.top {
transform: rotateX(90deg) translateZ(50px);
}
.bottom {
transform: rotateX(-90deg) translateZ(50px);
}
.front {
transform: translateZ(50px);
}
.back {
transform: rotateX(-180deg) translateZ(50px);
}
.left {
transform: rotateY(-90deg) translateZ(50px);
}
.right {
transform: rotateY(90deg) translateZ(50px);
}
}

叠加两个,调整颜色和透明度,我们可以非常轻松的实现 Webpack 的 LOGO:



当然,这里的 LOGO 为了保证每条线条视觉上的一致性,其实是没有设置景深效果 perspective 的,我们可以尝试给顶层父容器添加一下如下代码,通过 transformperspective 调整视觉角度,设置景深效果:


.father {
transform-style: preserve-3d;
perspective: 200px;
transform: rotateX(10deg);
}

就可以得到真正的 3D 效果,感受很不一样:



完整的代码,你可以戳这里:CodePen Demo -- Webpack LOGO




OK,热身完毕,接下来,让我们插上想象的翅膀,走进 CSS 3D 的世界。


实现文字的 3D 效果


首先,看看一些有意思的 CSS 3D 文字特效。


要实现文字的 3D 效果,看起来是立体的,通常的方式就是叠加多层。


下面有一些实现一个文字的 3D 效果的方式。


假设我们有如下结构:


<div class="g-container">
<p>Lorem ipsum</p>
</div>

如果什么都不加,文字的展示可能是这样的:



我们可以通过叠加阴影多层,营造 3D 的感觉,主要是合理控制阴影的距离及颜色,核心 CSS 代码如下:


p {
text-shadow:
4px 4px 0 rgba(0, 0, 0, .8),
8px 8px 0 rgba(0, 0, 0, .6),
12px 12px 0 rgba(0, 0, 0, .4),
16px 16px 0 rgba(0, 0, 0, .2),
20px 20px 0 rgba(0, 0, 0, .05);
}


这样,就有了基础的 3D 视觉效果。


3D 氖灯文字效果


基于此,我们可以实现一些 3D 文字效果,来看一个 3D 氖灯文字效果,核心就是:



  • 利用 text-shadow 叠加多层文字阴影

  • 利用 animation 动态改变阴影颜色


<div class="container">
<p class="a">CSS 3D</p>
<p class="b">NEON</p>
<p class="a">EFFECT</p>
</div>

核心 CSS 代码:


.container {
transform: rotateX(25deg) rotateY(-25deg);
}
.a {
color: #88e;
text-shadow: 0 0 0.3em rgba(200, 200, 255, 0.3), 0.04em 0.04em 0 #112,
0.045em 0.045em 0 #88e, 0.09em 0.09em 0 #112, 0.095em 0.095em 0 #66c,
0.14em 0.14em 0 #112, 0.145em 0.145em 0 #44a;
animation: pulsea 300ms ease infinite alternate;
}
.b {
color: #f99;
text-shadow: 0 0 0.3em rgba(255, 100, 200, 0.3), 0.04em 0.04em 0 #112,
0.045em 0.045em 0 #f99, 0.09em 0.09em 0 #112, 0.095em 0.095em 0 #b66,
0.14em 0.14em 0 #112, 0.145em 0.145em 0 #a44;
animation: pulseb 300ms ease infinite alternate;
}
@keyframes pulsea {
// ... 阴影颜色变化
}
@keyframes pulseb {
// ... 阴影颜色变化
}

可以得到如下效果:


4


完整的代码,你可以猛击这里 CSS 灵感 -- 使用阴影实现文字的 3D 氖灯效果


利用 CSS 3D 配合 translateZ 实现真正的文字 3D 效果


当然,上述第一种技巧其实没有运用 CSS 3D。下面我们使用 CSS 3D 配合 translateZ 再进一步。


假设有如下结构:


<div>
<h1>Glowing 3D TEXT</h1>
<h1>Glowing 3D TEXT</h1>
<h1>Glowing 3D TEXT</h1>
<h1>Glowing 3D TEXT</h1>
<h1>Glowing 3D TEXT</h1>
<h1>Glowing 3D TEXT</h1>
<h1>Glowing 3D TEXT</h1>
<h1>Glowing 3D TEXT</h1>
<h1>Glowing 3D TEXT</h1>
<h1>Glowing 3D TEXT</h1>
</div>我们通过给父元素 div 设置 transform-style: preserve-3d,给每个 <h1> 设定不同的 translateZ() 来达到文字的 3D 效果:

div {
transform-style: preserve-3d;
}
h1:nth-child(2) {
transform: translateZ(5px);
}
h1:nth-child(3) {
transform: translateZ(10px);
}
h1:nth-child(4) {
transform: translateZ(15px);
}
h1:nth-child(5) {
transform: translateZ(20px);
}
h1:nth-child(6) {
transform: translateZ(25px);
}
h1:nth-child(7) {
transform: translateZ(30px);
}
h1:nth-child(8) {
transform: translateZ(35px);
}
h1:nth-child(9) {
transform: translateZ(40px);
}
h1:nth-child(10) {
transform: translateZ(45px);
}

当然,辅助一些旋转,色彩变化,就可以得到更纯粹一些 3D 文字效果:



完整的代码,你可以猛击这里 CSS 灵感 -- 3D 光影变换文字效果


利用距离、角度及光影构建不一样的 3D 效果


还有一种很有意思的技巧,制作的过程需要比较多的调试。


合理的利用距离、角度及光影构建出不一样的 3D 效果。看看下面这个例子,只是简单是设置了三层字符,让它们在 Z 轴上相距一定的距离。


简单的伪代码如下:


<div>
<span class='C'>C</span>
<span class='S'>S</span>
<span class='S'>S</span>
<span></span>
<span class='3'>3</span>
<span class='D'>D</span>
</div>

$bright : #AFA695;
$gold : #867862;
$dark : #746853;
$duration : 10s;
div {
perspective: 2000px;
transform-style: preserve-3d;
animation: fade $duration infinite;
}
span {
transform-style: preserve-3d;
transform: rotateY(25deg);
animation: rotate $duration infinite ease-in;

&:after, &:before {
content: attr(class);
color: $gold;
z-index: -1;
animation: shadow $duration infinite;
}
&:after{
transform: translateZ(-16px);
}
&:before {
transform: translateZ(-8px);
}
}
@keyframes fade {
// 透明度变化
}
@keyframes rotate {
// 字体旋转
}
@keyframes shadow {
// 字体颜色变化
}

简单捋一下,上述代码的核心就是:



  1. 父元素、子元素设置 transform-style: preserve-3d

  2. span 元素的两个伪元素复制两个相同的字,利用 translateZ() 让它们在 Z 轴间隔一定距离

  3. 添加简单的旋转、透明度、字体颜色变化


可以得到这样一种类似电影开片的标题 3D 动画,其实只有 3 层元素,但是由于角度恰当,视觉上的衔接比较完美,看上去就非常的 3D。



为什么上面说需要合理的利用距离、角度及光影呢?


还是同一个动画效果,如果动画的初始旋转角度设置的稍微大一点,整个效果就会穿帮:



可以看到,在前几帧,能看出来简单的分层结构。又或者,简单调整一下 perspective,设置父容器的 perspective2000px 改为 500px,穿帮效果更为明显:


8


也就是说,在恰当的距离,合适的角度,我们仅仅通过很少的元素,就能在视觉上形成比较不错的 3D 效果。


上述的完整代码,你可以猛击这里:CSS 灵感 -- 3D 文字出场动画


3D 计数器


当然,发挥想象,我们还可以利用 3D 文字效果,制作出非常多有意思的效果。


譬如这个,我之前运用在我们业务的可视化看板项目中的 3D 计数器:



代码比较长,就不贴出来了,但是也是使用纯 CSS 可以实现的效果。


完整的代码,你可以猛击这里 CSS 灵感 -- 3D 数字计数动画


空间效果


嗯,上述章节主要是关于文字的 3D 效果,下面我们继续探寻 3D 在营造空间效果上的神奇之处。


优秀的 3D 效果,能让人有一种身临其境的感觉,都说 CSS 3D 其实作用有限,能做的不多,但是不代表它不能实现酷炫逼真的效果。


要营造逼真的 3D 效果,关键是恰当好处的运用 perspective 属性。


简单掌握原理,我们也可以很轻松的利用 CSS 3D 绘制一些非常有空间美感的效果。


这里我带领大家快速绘制一副具有空间美感的 CSS 3D 作品。


空间 3D 效果热身


首先,我们借助 Grid/Flex 等布局,在屏幕上布满格子(item),随意点就好:


<ul class="g-container">
<li></li>
<li></li>
// ... 很多子 li
<li></li>
</ul>


初始背景色为黑色,每个 item 填充为白色




接着,改变下每个 item 的形状,让他变成长条形的,可以改变通过改变 item 宽度,使用渐变填充部分等等方式:



接下来,父容器设置 transform-style: preserve-3dperspective,子元素设置 transform: rotateX(45deg),神奇的事情就发生了:



Wow,仅仅 3 步,我们就初步得到了一副具有空间美感的图形,让我们再回到每个子 item 的颜色设置,给它们随机填充不同的颜色,并且加上一个 transform: translate3d() 的动画,一个简单的 CSS 3D 作品就绘制完成了:



基于这个技巧的变形和延伸,我们就可以绘制非常多类似的效果。


在这里,我再次推荐 CSS-Doodle 这个工具,它可以帮助我们快速的创造复杂 CSS 效果。



CSS-doodle 是一个基于 Web-Component 的库。允许我们快速的创建基于 CSS Grid 布局的页面,以实现各种 CSS 效果(或许可以称之为 CSS 艺术)。



我们可以把上述的线条切换成圆弧:



完整的代码可以戳这里,利用 CSS-Doodle 也就几十行:CodePen Demo - CSS-Doodle Random Circle


又譬如袁川老师创作的 Seeding



利用图片素材


当然,基于上述技巧,有的时候会认为利用 CSS 绘制一些线条、圆弧、方块比较麻烦。可以进一步尝试利用现有的素材基于 CSS 3D 进行二次创作,这里有一个非常有意思的技巧。


假设我们有这样一张图形:



这张图先放着备用。在使用这张图之前,我们会先绘制这样一个图形:


<div class="g-container">
<div class="g-group">
<div class="item item-right"></div>
<div class="item item-left"></div>
<div class="item item-top"></div>
<div class="item item-bottom"></div>
<div class="item item-middle"></div>
</div>
</div>

body {
background: #000;
}
.g-container {
position: relative;
}
.g-group {
position: absolute;
width: 100px;
height: 100px;
left: -50px;
top: -50px;
transform-style: preserve-3d;
}
.item {
position: absolute;
width: 100%;
height: 100%;
background: rgba(255, 255, 255, .5);
}
.item-right {
background: red;
transform: rotateY(90deg) translateZ(50px);
}
.item-left {
background: green;
transform: rotateY(-90deg) translateZ(50px);
}
.item-top {
background: blue;
transform: rotateX(90deg) translateZ(50px);
}
.item-bottom {
background: deeppink;
transform: rotateX(-90deg) translateZ(50px);
}
.item-middle {
background: rgba(255, 255, 255, 0.5);
transform: rotateX(180deg) translateZ(50px);
}

一共设置了 5 个子元素,不过仔细看 CSS 代码,其中 4 个子元素都设置了 rotateX/Y(90deg/-90deg),也就是绕 X 轴或者 Y 轴旋转了 90°,在视觉上是垂直屏幕的一张平面,所以直观视觉上我们是不到的,只能看到一个平面 .item-middle


我将 5 个子 item 设置了不同的背景色,结果如下:



现在看来,好像平平无奇,确实也是。


不过,见证奇迹的时候来了,此时,我们给父元素 .g-container 设置一个极小的 perspective,譬如,设置一个 perspective: 4px,看看效果:


.g-container {
position: relative;
+ perspective: 4px;
}
// ...其余样式保持不变

此时,画风骤变,整个效果就变成了这样:



由于 perspective 生效,原本的平面效果变成了 3D 的效果。接下来,我们使用上面准备好的星空图,替换一下上面的背景颜色,全部都换成同一张图,神奇的事情发生了:



由于设置的 perspective 非常之下,而每个 item 的 transform: translateZ(50px) 设置的又比较大,所以图片在视觉上被拉伸的非常厉害。但是整体是充满整个屏幕的。


接下来,我们只需要让视角动起来,给父元素增加一个动画,通过控制父元素的 translateZ() 进行变化即可:


.g-container{
position: relative;
perspective: 4px;
perspective-origin: 50% 50%;
}

.g-group{
position: absolute;
// ... 一些定位高宽代码
transform-style: preserve-3d;
+ animation: move 8s infinite linear;
}

@keyframes move {
0%{
transform: translateZ(-50px) rotate(0deg);
}
100%{
transform: translateZ(50px) rotate(0deg);
}
}

看看,神奇美妙的星空穿梭的效果就出来了,Amazing:



美中不足之处在于,动画没能无限衔接上,开头和结尾都有很大的问题。


当然,这难不倒我们,我们可以:



  1. 通过叠加两组同样的效果,一组比另一组通过负的 animation-delay 提前行进,使两组动画衔接起来(一组结束的时候另外一组还在行进中)

  2. 再通过透明度的变化,隐藏掉 item-middle 迎面飞来的突兀感

  3. 最后,可以通过父元素的滤镜 hue-rotate 控制图片的颜色变化


我们尝试修改 HTML 结构如下:


<div class="g-container">
<div class="g-group">
<div class="item item-right"></div>
<div class="item item-left"></div>
<div class="item item-top"></div>
<div class="item item-bottom"></div>
<div class="item item-middle"></div>
</div>
<!-- 增加一组动画 -->
<div class="g-group">
<div class="item item-right"></div>
<div class="item item-left"></div>
<div class="item item-top"></div>
<div class="item item-bottom"></div>
<div class="item item-middle"></div>
</div>
</div>

修改后的核心 CSS 如下:


.g-container{
perspective: 4px;
position: relative;
// hue-rotate 变化动画,可以让图片颜色一直变换
animation: hueRotate 21s infinite linear;
}

.g-group{
transform-style: preserve-3d;
animation: move 12s infinite linear;
}
// 设置负的 animation-delay,让第二组动画提前进行
.g-group:nth-child(2){
animation: move 12s infinite linear;
animation-delay: -6s;
}
.item {
background: url(https://z3.ax1x.com/2021/08/20/fLwuMd.jpg);
background-size: cover;
opacity: 1;
// 子元素的透明度变化,减少动画衔接时候的突兀感
animation: fade 12s infinite linear;
animation-delay: 0;
}
.g-group:nth-child(2) .item {
animation-delay: -6s;
}
@keyframes move {
0%{
transform: translateZ(-500px) rotate(0deg);
}
100%{
transform: translateZ(500px) rotate(0deg);
}
}
@keyframes fade {
0%{
opacity: 0;
}
25%,
60%{
opacity: 1;
}
100%{
opacity: 0;
}
}
@keyframes hueRotate {
0% {
filter: hue-rotate(0);
}
100% {
filter: hue-rotate(360deg);
}
}

最终完整的效果如下,星空穿梭的效果,整个动画首尾相连,可以一直无限下去,几乎没有破绽,非常的赞:



上述的完整代码,你可以猛击这里:CSS 灵感 -- 3D 宇宙时空穿梭效果


3D 无限延伸视角动画


OK,当掌握了上述技巧之后,我们可以很容易的对其继续变形发散,实现各种各样的无限延伸的 3D 视角动画。


这里还有一个非常有意思的运用了类似技巧的动画:



原理与上述的星空穿梭大致相同,4 面墙的背景图使用 CSS 渐变可以很轻松的绘制出来,接下来就只是需要考虑如何让动画能无限循环下去,控制好首尾的衔接。


该效果最早见于 jkantner 的 CodePen,在此基础上我对其进行了完善和丰富,完整代码,你可以猛击这里:CSS 灵感 -- 3D 无限延伸视角动画



作者:chokcoco
链接:https://juejin.cn/post/6999801808637919239

收起阅读 »

想了解到底啥是个Web Socket?猛戳这里!!!

什么是 Web Socket WebSocket 协议在2008年诞生,2011年成为国际标准,所有浏览器都已经支持了。其是基于TCP的一种新的网络协议,是 HTML5 开始提供的一种在单个TCP连接上进行全双工通讯的协议,它实现了浏览器与服务器全双工(ful...
继续阅读 »

什么是 Web Socket


WebSocket 协议在2008年诞生,2011年成为国际标准,所有浏览器都已经支持了。其是基于TCP的一种新的网络协议,是 HTML5 开始提供的一种在单个TCP连接上进行全双工通讯的协议,它实现了浏览器与服务器全双工(full-duplex)通信——允许服务器主动发送信息给客户端。


都有http协议了,为什么要用Web Socket


WebSocket使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在 WebSocket API 中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。在 WebSocket API 中,浏览器和服务器只需要做一个握手的动作,然后浏览器和服务器之间就形成了一条快速通道,两者之间就直接可以数据互相传送。


HTTP协议是一种无状态、单向的应用层协议,其采用的是请求/响应模型,通信请求只能由客户端发起,服务端对请求做出应答响应,无法实现服务器主动向客户端发起消息,这就注定如果服务端有连续的状态变化,客户端想要获知就非常的麻烦。而大多数Web应用程序通过频繁的异步JavaScript 和 aJax 请求实现长轮询,其效率很低,而且非常的浪费很多的带宽等资源。


HTML5定义的WebSocket协议,能更好的节省服务器资源和带宽,并且能够更实时地进行通讯。WebSocket 连接允许客户端和服务器之间进行全双工通信,以便任一方都可以通过建立的连接将数据推送到另一端。WebSocket 只需要建立一次连接,就可以一直保持连接状态,这相比于轮询方式的不停建立连接显然效率要大大提高。


特点




  • 服务器可以主动向客户端推送信息,客户端也可以主动向服务器发送信息,是真正的双向平等对话




  • 建立在 TCP 协议之上,服务器端的实现比较容易。




  • 与 HTTP 协议有着良好的兼容性。默认端口也是80和443,并且握手阶段采用 HTTP 协议,因此握手时不容易屏蔽,能通过各种 HTTP 代理服务器。




  • 数据格式比较轻量,性能开销小,通信高效。




  • 可以发送文本,也可以发送二进制数据。




  • 没有同源限制,客户端可以与任意服务器通信。




  • 协议标识符是ws(如果加密,则为wss),服务器网址就是 URL。




怎样使用?


执行流程



  • 1 连接建立:客户端向服务端请求建立连接并完成连接建立

  • 2 数据上行:客户端通过已经建立的连接向服务端发送数据

  • 3 数据下行:服务端通过已经建立的连接向客户端发送数据

  • 4 客户端断开:客户端要求断开已经建立的连接

  • 5 服务端断开:服务端要求断开已经建立的连接


客户端


连接建立


连接成功后,会触发 onopen 事件


var ws = new WebSocket("wss://ws.iwhao.top");
ws.onopen = function(evt) {
console.log("Connection open ...");
};

数据上行


  ws.send("Hello WebSockets!");

数据下行


ws.onmessage = function(evt) {
console.log( "Received Message: " + evt.data);
ws.close();
};

客户端断开


ws.close();

服务端断开


ws.onclose = function(evt) {
console.log("closed.");
};

异常报错


如果连接失败,发送、接收数据失败或者处理数据出现错误,browser 会触发 onerror 消息;


ws.onerror = function(evt) {
};

服务端 node


参考



api/浏览器版本兼容性



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

收起阅读 »

我写的页面打开才用了10秒,产品居然说我是腊鸡!!!

背景 产品:你看看这页面加载的如此之慢,怎么会有用户用呢?(并甩给了我一个录屏) 我: (抛出前端应对之策)前端需要加载vue,js,html,css这些都需要时间呀,是不是,别说还需要接口请求,数据库查询,js执行,这些都需要时间是不是,所以加载慢很正常,...
继续阅读 »

背景



  • 产品:你看看这页面加载的如此之慢,怎么会有用户用呢?(并甩给了我一个录屏)

  • : (抛出前端应对之策)前端需要加载vue,js,html,css这些都需要时间呀,是不是,别说还需要接口请求,数据库查询,js执行,这些都需要时间是不是,所以加载慢很正常,让用户用wifi嘛。(嗯。。。心安理得,就是这样。。)

  • 产品: 你上一家公司就是因为有你这样的优秀员工才倒闭的吧?!




这么说我就不服了,先看看视频:


我的影片我.gif
掐指一算,也就10s,还。。。。。。。。。。。。好吧,行吧,我编不下去。




前戏


欲练此功,必先自宫。额。。不对。欲解性能,必先分析。
市面上的体检套餐有很多种, 但其实都是换汤不换药. 那药 (标准) 是什么呢? 我们会在下面说明. 这里我选择了谷歌亲儿子 " 灯塔 "(LightHouse) 进行性能体检.


640.webp
从上面中我们可以看到灯塔是通过几种性能指标及不同权重来进行计分的. 这几种指标主要是根据 PerformanceTiming 和 PerformanceEntry API 标准进行定义. 市面上大多体检套餐也是基于这些指标定制的. 接下来我们来了解下这些指标的含义吧.


具体含义


FCP (First Contentful Paint)



First Contentful Paint (FCP) 指标衡量从页面开始加载到页面内容的任何部分在屏幕上呈现的时间。对于此指标,“内容”是指文本、图像(包括背景图像)、<svg> 元素或非白色 <canvas> 元素。



SI (Speed Index)



速度指数衡量页面加载期间内容的视觉显示速度。



LCP (Largest Contentful Paint)



LCP 测量视口中最大的内容元素何时呈现到屏幕上。这大约是页面的主要内容对用户可见的时间.



TTI (Time to Interactive)



TTI 衡量一个页面需要多长时间才能完全交互。在以下情况下,页面被认为是完全交互的:




  • 页面显示有用的内容,这是由 First Contentful Paint 衡量的,

  • 为大多数可见的页面元素注册了事件处理程序

  • 并且该页面会在 50 毫秒内响应用户交互。


TBT (Total Blocking Time)



FCP 到 TTI 之间, 主线程被 long task(超过 50ms) 阻塞的时间之和



TBT 衡量页面被阻止响应用户输入(例如鼠标点击、屏幕点击或键盘按下)的总时间。总和是通过将所有长任务的阻塞部分相加来计算的,即首次内容绘制和交互时间。任何执行时间超过 50 毫秒的任务都是长任务。 50 毫秒后的时间量是阻塞部分。例如,如果 Lighthouse 检测到 70 毫秒长的任务,则阻塞部分将为 20 毫秒。


CLS (Cumulative Layout Shift)



累计布局偏移值



FID (First Input Delay)



衡量您的用户可能遇到的最坏情况的首次输入延迟。首次输入延迟测量从用户第一次与您的网站交互(例如单击按钮)到浏览器实际能够响应该交互的时间。



体检结果


WechatIMG55139.png


哈哈哈,不愧是优秀的前端工程师。。。6项性能指标挂了5个。




手术方案


优化建议


1629886726026_C607FFC4-676D-4245-86DE-385AE0087581.png
那好,我们一个一个的逐个攻破。


减少初始服务器响应时间


下面是我和后端友好的对话:



  • : 你这首页接口2.39s,你是闭着眼睛写的接口吗?

  • 后端大佬: xxx哔哔哔哔哔哔xxxx,想死吗?!******xxxxx哔哔哔哔哔哔哔哔哔哔

  • : 我也觉得是前端的问题,嗯,打扰了。。。


行,下一个优化点。


减少未使用的 JavaScript


经过分析,我发现首页仅涉及到资源请求,并不需要请求库(我们内部封装)的加载,同时依赖的第三方的库也不需要长时间的版本更新,所以并不需要单独打包到chunk-vendors中。
查看基于 webpack-bundle-analyzer 生成的体积分析报告我发现有两个可优化的大产物:



内部封装的请求库需要md5和sha256加密请求,导致包打包出来多了600kb,于是在和领导商议之后决定用axios重写封装。




vue,vuex,vue-router,clipboard,vue-i18n,axios等三方的库上传cdn,首页预加载。



经过优化, bundle 体积 (gizp 前) 由原来的 841kb 减小至 278kb.


WechatIMG55140.png


避免向现代浏览器提供旧版 JavaScript


WechatIMG55141.png
没有想到太好的代替方案,暂时搁置。


视觉稳定性


优化未设置尺寸的图片元素



改善建议里提到了一项优先级很高的优化就是为图片元素设置显式的宽度和高度, 从而减少布局偏移和改善 CLS.



<img src="hello.png" width="640" height="320" alt="Hello World" />


避免页面布局发生偏移



我们产品中header是可配置的, 这个header会导致网站整体布局下移. 从而造成了较大的布局偏移. 跟产品 'qs'交易后, 讲页面拉长,header脱离文本流固定定位在上方。



最大的内容元素绘制


替换最大内容绘制元素



在改善建议中, 我发现首页的最大内容绘制元素是一段文本, 这也难怪 LCP 指标的数据表现不理想了, 原因: 链路过长 - 首页加载js -> 加载语言包 -> 显示文本内用.




于是, 我决定对最大内容绘制元素进行修改, 从而提升 LCP 时间. 我喵了一眼 Largest Contentful Paint API 关于该元素类型的定义, 将 "目标" 锁定到了一个 loading 元素 (绘制成本低: 默认渲染, 不依赖任何条件和判断). 经过我对该元素的尺寸动了手脚后 (变大), 该元素成功 "上位".



其他


除了针对上面几个指标维度进行优化外, 我还做了几点优化, 这里简单提一下:



  • 优化 DOM 嵌套层级及数量

  • 减少不必要的接口请求

  • 使用 translate 替换 top 做位移 / 动画


优化结果


WechatIMG55142.png


哎,优秀呀,还是优秀的前端工程师呀~~~~~hahahhahaha


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

收起阅读 »

这里是一个让你为所欲为,欲罢不能的抽奖demo

寒暄 抽奖系统有很多,各式各样的,不知道大伙都抽中过什么,还是像我这样经历了绝望,看破红尘,存起来留给下一代。 这种抽奖场景在活动中很常见,为了更好的摸鱼,决定自己去写一个插件来解决重复劳动。接下来为大伙介绍一个不错的宫格抽奖组件,请看官往下挪步 关于gri...
继续阅读 »

寒暄


抽奖系统有很多,各式各样的,不知道大伙都抽中过什么,还是像我这样经历了绝望,看破红尘,存起来留给下一代。


image.png


这种抽奖场景在活动中很常见,为了更好的摸鱼,决定自己去写一个插件来解决重复劳动。接下来为大伙介绍一个不错的宫格抽奖组件,请看官往下挪步


关于grid-roll


grid-roll是一个vue的宫格组件,它让ui和逻辑分离,封装了逻辑和宫格布局,让开发者只关注奖品和按钮的ui部分。



  • 自定义宫格数量,经典的3x3还是10x100都不在话下

  • 多抽功能,一次点击多次抽奖,谷底梭哈,就问你刺不刺激


安装


npm i grid-roll -S
yarn add grid-roll

引入


/** 引入 */
import { gridRoll, gridStart, gridPrize } from 'grid-roll'
import 'grid-roll/dist/grid-roll.min.css'

实践


通过vuecli搭起新项目,这边我们可以直接用掘金抽奖的图片链接,拿过来吧你。


图片上的奖品我都打上了数字记号,这些记号其实就奖品数组的下标,它们对应着奖品位置,布局从左到右一行一行排列,所以我们的奖品数组元素排序要注意下


image.png


通过使用grid-roll,我们只需要定义里面8个奖品和1个按钮的样式就行,用gridStart和gridPrize去包装这些物料,塞进gridRoll里面,gridRoll会帮我们自动调整成九宫格布局。这里,我更喜欢把奖品写成数据去循环生成gridPrize。然后样式布局基本是打开开发者工具复制掘金的样式,所以就不细说了


image.png


介绍下这3个组件:



  • gridRoll:interval这个属性用来定义宫格之前的间隔,默认是没有间隔的,这里我看感觉定义了6px。并且接受两个插槽button和prize

  • gridStart:专门用来做button插槽的组件

  • gridPrize:专门用来做prize插槽的组件









// 这里引入组件和样式
import { gridRoll, gridStart, gridPrize } from "grid-roll";
import "grid-roll/dist/grid-roll.min.css";
expoet default {
data () {
return {
prizes: [
{
id: 1,
text: "66矿石",
img: "https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/32ed6a7619934144882d841761b63d3c~tplv-k3u1fbpfcp-no-mark:0:0:0:0.awebp",
},
{
id: 2,
text: "随机限量徽章",
img: "https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/71c68de6368548bd9bd6c8888542f911~tplv-k3u1fbpfcp-no-mark:0:0:0:0.awebp",
},
{
id: 3,
text: "掘金新款T恤",
img: "https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/5bf91038a6384fc3927dee294a38006b~tplv-k3u1fbpfcp-no-mark:0:0:0:0.awebp",
},
{
id: 4,
text: "Bug",
img: "https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0a4ce25d48b8405cbf5444b6195928d4~tplv-k3u1fbpfcp-no-mark:0:0:0:0.awebp",
},
{
id: 5,
text: "再抽2次解锁",
img: "https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/aabe49b0d5c741fa8d92ff94cd17cb90~tplv-k3u1fbpfcp-no-mark:0:0:0:0.awebp",
},
{
id: 6,
text: "掘金限量桌垫",
img: "https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/c78f363f41a741ffa11dcc8a92b72407~tplv-k3u1fbpfcp-no-mark:0:0:0:0.awebp",
},
{
id: 7,
text: "Yoyo抱枕",
img: "https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/33f4d465a6a9462f9b1b19b3104c8f91~tplv-k3u1fbpfcp-no-mark:0:0:0:0.awebp",
},
{
id: 8,
text: "再抽3次解锁",
img: "https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/4decbd721b2b48098a1ecf879cfca677~tplv-k3u1fbpfcp-no-mark:0:0:0:0.awebp",
},
],
}
}
components: {
gridRoll,
gridStart,
gridPrize,
},
}

从上面可以看到,我们只需要通过gridStart和gridPrize定义好按钮和奖品的样式,放进gridRoll就行,不用再去管其他乱七八糟的操作。


disabled的使用


从官方的图看起来,这边还缺少一个“锁”样式,需要通过抽奖次数进行解锁,除了奖品样式的不同,在滚动的时候还会直接跳过未解锁的奖品。这边gridPrize也有一个对应的prop做这件事。


首先在prizes需要用到“锁”的元素中添加一个字段disabled: true,传给gridPrize,当抽奖开始的时候,滚动会直接跳过disabled为true的奖品,其次我们用disabled来做一些样式区分,这里样式也是照抄掘金




image.png


这里我们基本就完成静态样式啦,接下来就是说说怎么触发这个抽奖


抽奖


抽奖的行为是由gridPrize的startRoll函数提供的,这里通过ref获取gridRoll的实例,定义一个handleLottery方法用来触发startRoll函数。再把handleLottery绑定的抽奖按钮上







methods: {
async handleLottery() {
const value = 1;
/**
* 这里的value为1是指抽取id为1的奖品
* 返回一个Promise实例,内部为了防止多次触发抽奖逻辑,
* resolve会传递一个Boolean,进行是false,抽奖结束返回true
*/

const b = await this.$refs.dial.startRoll(value);
if (b) {
alert(
`🎉你抽到${this.prizes.find((prize) => prize.id === value).text}`
);
} else {
console.warn("稍安勿躁");
}
},
},

同时别忘记了,抽奖滚动的时候,有一个选中的样式,这里gridPrize作用域插槽提供了一个isSelect值用来判断是否滚动到当前奖品,用来做一些样式切换





收起阅读 »

vue、react函数式编程

函数式编程 JavaScript 语言从一诞生,就具有函数式编程的烙印。它将函数作为一种独立的数据类型,与其他数据类型处于完全平等的地位。在 JavaScript 语言中,你可以采用面向对象编程,也可以采用函数式编程。有人甚至说,JavaScript 是有史以...
继续阅读 »

函数式编程


JavaScript 语言从一诞生,就具有函数式编程的烙印。它将函数作为一种独立的数据类型,与其他数据类型处于完全平等的地位。在 JavaScript 语言中,你可以采用面向对象编程,也可以采用函数式编程。有人甚至说,JavaScript 是有史以来第一种被大规模采用的函数式编程语言。


ES6 的种种新增功能,使得函数式编程变得更方便、更强大。本章介绍 ES6 如何进行函数式编程。


柯里化


柯里化(currying)指的是将一个多参数的函数拆分成一系列函数,每个拆分后的函数都只接受一个参数(unary)。


function add (a, b) {
return a + b;
}

add(1, 1) // 2

上面代码中,函数add接受两个参数ab


柯里化就是将上面的函数拆分成两个函数,每个函数都只接受一个参数。


function add (a) {
return function (b) {
return a + b;
}
}
// 或者采用箭头函数写法
const add = x => y => x + y;

const f = add(1);
f(1) // 2

上面代码中,函数add只接受一个参数a,返回一个函数f。函数f也只接受一个参数b


函数合成


函数合成(function composition)指的是,将多个函数合成一个函数。


const compose = f => g => x => f(g(x));

const f = compose (x => x * 4) (x => x + 3);
f(2) // 20

上面代码中,compose就是一个函数合成器,用于将两个函数合成一个函数。


可以发现,柯里化与函数合成有着密切的联系。前者用于将一个函数拆成多个函数,后者用于将多个函数合并成一个函数。


参数倒置


参数倒置(flip)指的是改变函数前两个参数的顺序。


var divide = (a, b) => a / b;
var flip = f.flip(divide);

flip(10, 5) // 0.5
flip(1, 10) // 10

var three = (a, b, c) => [a, b, c];
var flip = f.flip(three);
flip(1, 2, 3); // => [2, 1, 3]

上面代码中,如果按照正常的参数顺序,10 除以 5 等于 2。但是,参数倒置以后得到的新函数,结果就是 5 除以 10,结果得到 0.5。如果原函数有 3 个参数,则只颠倒前两个参数的位置。


参数倒置的代码非常简单。


let f = {};
f.flip =
fn =>
(a, b, ...args) => fn(b, a, ...args.reverse());

执行边界


执行边界(until)指的是函数执行到满足条件为止。


let condition = x => x > 100;
let inc = x => x + 1;
let until = f.until(condition, inc);

until(0) // 101

condition = x => x === 5;
until = f.until(condition, inc);

until(3) // 5

上面代码中,第一段的条件是执行到x大于 100 为止,所以x初值为 0 时,会一直执行到 101。第二段的条件是执行到等于 5 为止,所以x最后的值是 5。


执行边界的实现如下。


let f = {};
f.until = (condition, f) =>
(...args) => {
var r = f.apply(null, args);
return condition(r) ? r : f.until(condition, f)(r);
};

上面代码的关键就是,如果满足条件就返回结果,否则不断递归执行。


队列操作


队列(list)操作包括以下几种。



  • head: 取出队列的第一个非空成员。

  • last: 取出有限队列的最后一个非空成员。

  • tail: 取出除了“队列头”以外的其他非空成员。

  • init: 取出除了“队列尾”以外的其他非空成员。


下面是例子。


f.head(5, 27, 3, 1) // 5
f.last(5, 27, 3, 1) // 1
f.tail(5, 27, 3, 1) // [27, 3, 1]
f.init(5, 27, 3, 1) // [5, 27, 3]

这些方法的实现如下。


let f = {};
f.head = (...xs) => xs[0];
f.last = (...xs) => xs.slice(-1);
f.tail = (...xs) => Array.prototype.slice.call(xs, 1);
f.init = (...xs) => xs.slice(0, -1);

合并操作


合并操作分为concatconcatMap两种。前者就是将多个数组合成一个,后者则是先处理一下参数,然后再将处理结果合成一个数组。


f.concat([5], [27], [3]) // [5, 27, 3]
f.concatMap(x => 'hi ' + x, 1, [[2]], 3) // ['hi 1', 'hi 2', 'hi 3']

这两种方法的实现代码如下。


let f = {};
f.concat =
(...xs) => xs.reduce((a, b) => a.concat(b));
f.concatMap =
(f, ...xs) => f.concat(xs.map(f));

配对操作


配对操作分为zipzipWith两种方法。zip操作将两个队列的成员,一一配对,合成一个新的队列。如果两个队列不等长,较长的那个队列多出来的成员,会被忽略。zipWith操作的第一个参数是一个函数,然后会将后面的队列成员一一配对,输入该函数,返回值就组成一个新的队列。


下面是例子。


let a = [0, 1, 2];
let b = [3, 4, 5];
let c = [6, 7, 8];

f.zip(a, b) // [[0, 3], [1, 4], [2, 5]]
f.zipWith((a, b) => a + b, a, b, c) // [9, 12, 15]

上面代码中,zipWith方法的第一个参数是一个求和函数,它将后面三个队列的成员,一一配对进行相加。


这两个方法的实现如下。


let f = {};

f.zip = (...xs) => {
let r = [];
let nple = [];
let length = Math.min.apply(null, xs.map(x => x.length));

for (var i = 0; i < length; i++) {
xs.forEach(
x => nple.push(x[i])
);

r.push(nple);
nple = [];
}

return r;
};

f.zipWith = (op, ...xs) =>
f.zip.apply(null, xs).map(
(x) => x.reduce(op)
);


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

收起阅读 »

深入理解 Class 和 extends 原理

准备工作 在开始之前,我们需要一个 babel 的环境,方便查看 babel 后的代码,这里我推荐两种方式。chrome 插件 —— ScratchJS,可以设置 babel 来转换代码,通过点击 Toggle output 就能看到 babel 后的代码。b...
继续阅读 »

准备工作


在开始之前,我们需要一个 babel 的环境,方便查看 babel 后的代码,这里我推荐两种方式。

chrome 插件 —— ScratchJS,可以设置 babel 来转换代码,通过点击 Toggle output 就能看到 babel 后的代码。babel 官网推荐的在线编译工具 试一试,可以实时看到转换前后的代码。


本文将以 ScratchJS 转换后的代码为例进行代码分析。


1. class 实现


先从最简单的 class 开始看,下面这段代码涵盖了使用 class 时所有会出现的情况(静态属性、构造函数、箭头函数)。


class Person {
static instance = null;
static getInstance() {
return super.instance;
}
constructor(name, age) {
this.name = name;
this.age = age;
}
sayHi() {
console.log('hi');
}
sayHello = () => {
console.log('hello');
}
sayBye = function() {
console.log('bye');
}
}

而经过 babel 处理后的代码是这样的:


'use strict';

var _createClass = function () {
function defineProperties(target, props) {
for (var i = 0; i < props.length; i++) {
var descriptor = props[i];
descriptor.enumerable = descriptor.enumerable || false;
descriptor.configurable = true;
if ("value" in descriptor)
descriptor.writable = true;
Object.defineProperty(target, descriptor.key, descriptor);
}
}
return function (Constructor, protoProps, staticProps) {
if (protoProps)
defineProperties(Constructor.prototype, protoProps);
if (staticProps)
defineProperties(Constructor, staticProps);
return Constructor;
};
}();

function _classCallCheck(instance, Constructor) {
if (!(instance instanceof Constructor)) {
throw new TypeError("Cannot call a class as a function");
}
}

var Person = function () {
function Person(name, age) {
_classCallCheck(this, Person);

this.sayHello = function () {
console.log('hello');
};

this.sayBye = function () {
console.log('bye');
};

this.name = name;
this.age = age;
}

_createClass(Person, [{
key: 'sayHi',
value: function sayHi() {
console.log('hi');
}
}]);

return Person;
}();

Person.instance = null;

最外层的 Person 变量被赋值给了一个立即执行函数,立即执行函数里面返回的是里面的 Person 构造函数,实际上最外层的 Person 就是里面的 Person 构造函数。


在 Person 类上用 static 设置的静态属性instance,在这里也被直接挂载到了 Person 构造函数上。


1.1 挂载属性方法


Person 类上各个属性的关系是这样的:


image_1dmjbel2cfvdls41h2e1hcmpn39.png-30.9kB


你是不是很好奇,为什么在 Person 类上面设置的 sayHisayHellosayBye 三个方法,编译后被放到了不同的地方处理?


从编译后的代码中可以看到 sayHellosayBye 被放到了 Person 构造函数中定义,而 sayHi_createClass 来处理(_createClasssayHi 添加到了 Person 的原型上面)。


曾经我也以为是 sayHello 使用了箭头函数的缘故才让它最终被绑定到了构造函数里面,后来我看到 sayBye 这种用法才知道这和箭头函数无关。


实际上 class 中定义属性还有一种写法,这种写法和 sayBye 如出一辙,在 babel 编译后会将其属性放到构造函数中,而非原型上面。


class Person {
name = 'tom';
age = 23;
}
// 等价于
class Person {
constructor() {
this.name = 'tom';
this.age = 23;
}
}

如果我们将 name 后面的 'tom' 换成函数呢?甚至箭头函数呢?这不就是 sayByesayHello 了吗?


因此,在 class 中不直接使用 = 来定义的方法,最终都会被挂载到原型上,使用 = 定义的属性和方法,最终都会被放到构造函数中。


1.2 _classCallCheck


Person 构造函数中调用了 _classCallCheck 函数,并将 this 和自身传入进去。
_classCallCheck 中通过 instanceof 来进行判断,instance 是否在 Constructor 的原型链上面,如果不在上面则抛出错误。这一步主要是为了避免直接将 Person 类当做函数来调用。
因此,在ES5中构造函数是可以当做普通函数来调用的,但在ES6中的类是无法直接当普通函数来调用的。



注意:为什么通过 instanceof 可以判断是否将 Person 类当函数来调用呢?
因为如果使用 new 操作符实例化 Person 的时候,那么 instance 就是当前的实例,指向 Person.prototypeinstance instanceof Constructor 必然为true。反之,直接调用 Person 构造函数,那么 instance 就不会指向 Person.prototype



1.3 _createClass


我们再来看 _createClass 函数,这个函数在 Person 原型上面添加了 sayHi 方法。


// 创建原型方法
_createClass(Person, [{
key: 'sayHi',
value: function sayHi() {
console.log('hi');
}
}]);

// _createClass也是一个立即执行函数
var _createClass = function () {
// 将props属性挂载到目标target上面
function defineProperties(target, props) {
for (var i = 0; i < props.length; i++) {
var descriptor = props[i];
descriptor.enumerable = descriptor.enumerable || false;
descriptor.configurable = true;
if ("value" in descriptor)
descriptor.writable = true;
// 通过defineProperty来挂载属性
Object.defineProperty(target, descriptor.key, descriptor);
}
}
// 这个才是“真正的”_createClass
return function (Constructor, protoProps, staticProps) {
// 如果传入了需要挂载的原型方法
if (protoProps)
defineProperties(Constructor.prototype, protoProps);
// 如果传入了需要挂载的静态方法
if (staticProps)
defineProperties(Constructor, staticProps);
return Constructor;
};
}();

_createClass 函数接收三个参数,分别是 Constructor (构造函数)、protoProps(需要挂载到原型上的方法)、staticProps(需要挂载到类上的静态方法)。
在接收到参数之后,_createClass 会进行判断如果有 staticProps,则挂载到 Constructor 构造函数上;如果有 protoProps ,那么挂载到 Constructor 原型上面。
这里的挂载函数 defineProperties 是关键,它对传入的 props 进行了遍历,并设置了其 enumerable(是否可枚举) 和 configurable(是否可配置)、writable(是否可修改)等数据属性。
最后使用了 Object.defineProperty 函数来给设置当前对象的属性描述符。


2. extends 实现


通过上文对 Person 的分析,相信你已经知道了 ES6 中类的实现,这与ES5中的实现大同小异,接下来我们来具体看一下 extends 的实现。
以下面的 ES6 代码为例:


class Child extends Parent {
constructor(name, age) {
super(name, age);
this.name = name;
this.age = age;
}
getName() {
return this.name;
}
}

class Parent {
constructor(name, age) {
this.name = name;
this.age = age;
}
getName() {
return this.name;
}
getAge() {
return this.age;
}
}

babel后的代码则是这样的:


"use strict";

// 省略 _createClass
// 省略 _classCallCheck

function _possibleConstructorReturn(self, call) {
if (!self) {
throw new ReferenceError("this hasn't been initialised - super() hasn't been called");
}
return call
&& (typeof call === "object" || typeof call === "function") ? call : self;
}

function _inherits(subClass, superClass) {
if (typeof superClass !== "function" && superClass !== null) {
throw new TypeError("Super expression must either be null or a function, not " + typeof superClass);
}
subClass.prototype = Object.create(superClass && superClass.prototype, {
constructor: {
value: subClass,
enumerable: false,
writable: true,
configurable: true
}
});
if (superClass)
Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass;
}

var Child = function (_Parent) {
_inherits(Child, _Parent);

function Child(name, age) {
_classCallCheck(this, Child);

var _this = _possibleConstructorReturn(this, Object.getPrototypeOf(Child).call(this, name, age));

_this.name = name;
_this.age = age;
return _this;
}

_createClass(Child, [{
key: "getName",
value: function getName() {
return this.name;
}
}]);

return Child;
}(Parent);

// 省略 Parent(类似上面的 Person 代码)

我们可以清楚地看到,继承是通过_inherits实现的。
为了方便理解,我这里整理了一下原型链的关系:


image_1dmec296p60q11bp1f8c1rid1rc52a.png-43.1kB


除去一些无关紧要的代码,最终的核心实现代码就只有这么多:


var Child = function (_Parent) {

_inherits(Child, _Parent);

function Child(name, age) {

var _this = _possibleConstructorReturn(this, Object.getPrototypeOf(Child).call(this, name, age));

_this.name = name;
_this.age = age;
return _this;
}

return Child;
}(Parent);

和前面的 Person 类实现有所不同的地方是,在 Child 方法中增加调用了 _inherits,还有在设置 nameage 属性的时候,使用的是执行 _possibleConstructorReturn 后返回的 _this,而非自身的 this,我们就重点分析这两步。


2.1 _inherits


先来看_inherits函数的实现代码:


function _inherits(subClass, superClass) { 
// 如果有一个不是函数,则抛出报错
if (typeof superClass !== "function" && superClass !== null) {
throw new TypeError("Super expression must either be null or a function, not " + typeof superClass);
}
// 将 subClass.prototype 设置为 superClass.prototype 的实例
subClass.prototype = Object.create(superClass && superClass.prototype, {
constructor: {
value: subClass,
enumerable: false,
writable: true,
configurable: true
}
});
// 将 subClass 设置为 superClass 的实例(优先使用 Object.setPrototypeOf)
if (superClass)
Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass;
}

_inherits 函数接收两个参数,分别是 subClass (子构造函数)和 subClass (父构造函数),将这个函数做的事情稍微做一下梳理。



  1. 设置 subClass.prototype[[Prototype]]指向 superClass.prototype[[Prototype]]

  2. 设置 subClass[[Prototype]] 指向 superClass


在《深入理解类和继承》一文中,曾经提到过 ES5 中的寄生组合式继承,extends 的实现与寄生组合式继承实则大同小异,仅仅只增加了第二步操作。


2.2 _possibleConstructorReturn


Child 中调用了 _possibleConstructorReturn 函数,将 thisObject.getPrototypeOf(Child).call(this, name, age)) 传了进去。
这个 this 我们很容易理解,就是构造函数的 this,但后面这么长的一串又是什么意思呢?
刚刚在 _inherits 中设置了 Child[[Prototype]] 指向了 Parent,因此可以将后面这串代码简化为 Parent.call(this, name, age)
这样你是不是就很熟悉了?这不就是组合继承中的执行一遍父构造函数吗?
那么 Parent.call(this, name, age) 执行后返回了什么呢?
正常情况下,应该会返回 undefined,但不排除 Parent 构造函数中直接返回一个对象或者函数的可能性。
*** 小课堂:**
在构造函数中,如果什么也没有返回或者返回了原始值,那么默认会返回当前的 this;而如果返回的是引用类型,那么最终实例化后的实例依然是这个引用类型(仅相当于对这个引用类型进行了扩展)。


const obj = {};
function Parent(name) {
this.name = name;
return obj;
}
const p = new Parent('tom');
obj.name; // 'tom'
p === obj; // true

如果没有 self,这里就会直接抛出错误,提示 super 函数还没有被调用。
最后会对 call 进行判断,如果 call 为引用类型,那么返回 call,否则返回 self



注意:call 就是 Parent.call(this, name, age) 执行后返回的结果。



function _possibleConstructorReturn(self, call) { 
if (!self) {
throw new ReferenceError("this hasn't been initialised - super() hasn't been called");
}
return call
&& (typeof call === "object" || typeof call === "function") ? call : self;
}

Child 方法中,最终拿到 _possibleConstructorReturn 执行后的结果作为新的 this 来设置构造函数里面的属性。



思考题:如果直接用 this,而不是 _this,会出现什么问题?



总结


ES6 中提供的 classextends 本质上只是语法糖,底层的实现原理依然是构造函数和寄生组合式继承。
所以对于一个合格的前端工程师来说,即使 ES6 已经到来,对于 ES5 中的这些基础原理我们依然需要好好掌握。


作者:sh22n
链接:https://juejin.cn/post/7001025002287923207

收起阅读 »

高级线程应用之栅栏、信号量、调度组以及source(五)

4.3 Dispatch Source 封装 Timer目标是封装一个类似NSTimer的工具。void dispatch_source_set_timer(dispatch_source_t source, dispatch_time_t start...
继续阅读 »

4.3 Dispatch Source 封装 Timer

目标是封装一个类似NSTimer的工具。

void
dispatch_source_set_timer(dispatch_source_t source,
dispatch_time_t start,
uint64_t interval,
uint64_t leeway);

  • source
    :事件源。
  • start:控制计时器第一次触发的时刻。
    • 参数类型是 dispatch_time_topaque类型),不能直接操作它。需要 dispatch_time 和 dispatch_walltime 函数来创建。
    • 常量 DISPATCH_TIME_NOW 和 DISPATCH_TIME_FOREVER 很常用。
    • 当使用dispatch_time 或者 DISPATCH_TIME_NOW 时,系统会使用默认时钟来进行计时。然而当系统休眠的时候,默认时钟是不走的,也就会导致计时器停止。使用 dispatch_walltime 可以让计时器按照真实时间间隔进行计时。
  • interval:回调间隔时间。
  • leeway:计时器触发的精准程度,就算指定为0系统也无法保证完全精确的触发时间,只是会尽可能满足这个需求。

首先实现一个最简单的封装:

- (instancetype)initTimerWithTimeInterval:(NSTimeInterval)interval queue:(dispatch_queue_t)queue leeway:(NSTimeInterval)leeway repeats:(BOOL)repeats handler:(dispatch_block_t)handler {    
if (self == [super init]) {
self.timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
dispatch_source_set_timer(self.timer, dispatch_walltime(NULL, 0), interval * NSEC_PER_SEC, leeway * NSEC_PER_SEC);
//解决与handler互相持有
__weak typeof(self) weakSelf = self;

//事件回调,这个函数在执行完之后 block 会立马执行一遍。后面隔一定时间间隔再执行一次。
dispatch_source_set_event_handler(self.timer, ^{
if (handler) {
handler();
}
if (!repeats) {
//repeats 为 NO 执行一次后取消
[weakSelf cancel];
}
});
}
return self;
}

这样就满足了最基本的要求,由于handler的调用在设置和恢复后会立马调用,所以需要过滤需改handler实现如下:
//忽略 handler 设置完马上回调
if (weakSelf.isAutoFirstCallback) {
@synchronized(weakSelf) {
weakSelf.isAutoFirstCallback = NO;
}
return;
}
//忽略挂起恢复后的立马回调
if (!weakSelf.resumeCallbackEnable && weakSelf.isResumeCallback) {
@synchronized(weakSelf) {
weakSelf.isResumeCallback = NO;
}
return;
}

if (handler) {
handler();
}

if (!repeats) {
//repeats 为 NO 执行一次后取消
[weakSelf cancel];
}

为了更灵活对注册以及取消source逻辑也进行暴露:

dispatch_source_set_registration_handler(self.timer, ^{
if (weakSelf.startBlock) {
weakSelf.startBlock();
}
});
//取消回调
dispatch_source_set_cancel_handler(self.timer, ^{
if (weakSelf.cancelBlock) {
weakSelf.cancelBlock();
}
});
由于source本身提供了挂起和恢复的功能,同样对其封装。并且需要进行释放操作,所以提供cancel功能:

- (void)start {
//为了与isResumeCallback区分开
@synchronized(self) {
if (!self.isStarted && self.timerStatus == HPTimerSuspend) {
self.isStarted = YES;
self.timerStatus = HPTimerResume;
dispatch_resume(self.timer);
}
}
}

- (void)suspend {
//挂起,挂起的时候不能设置timer为nil
@synchronized(self) {
if (self.timerStatus == HPTimerResume) {
self.timerStatus = HPTimerSuspend;
dispatch_suspend(self.timer);
}
}
}

- (void)resume {
//恢复
@synchronized(self) {
if (self.timerStatus == HPTimerSuspend) {
self.isResumeCallback = YES;
self.timerStatus = HPTimerResume;
dispatch_resume(self.timer);
}
}
}

- (void)cancel {
//取消
@synchronized(self) {
if (self.timerStatus != HPTimerCanceled) {
//先恢复再取消
if (self.timerStatus == HPTimerSuspend) {
[self resume];
}
self.timerStatus = HPTimerCanceled;
dispatch_source_cancel(self.timer);
_timer = nil;
}
}
}

- (void)dealloc {
[self cancel];
}

  • dealloc中主动进行cancel调用方可以不必在自己的dealloc中调用。

这样再暴露一些简单接口就可以直接调用了(调用方需要持有timer):

self.timer = [HPTimer scheduledTimerWithTimeInterval:3 handler:^{
NSLog(@"timer 回调");
}];

五、延迟函数(dispatch_after)

void
dispatch_after(dispatch_time_t when, dispatch_queue_t queue,
dispatch_block_t work)
{
_dispatch_after(when, queue, NULL, work, true);
}

直接调用_dispatch_after


static inline void
_dispatch_after(dispatch_time_t when, dispatch_queue_t dq,
void *ctxt, void *handler, bool block)
{
dispatch_timer_source_refs_t dt;
dispatch_source_t ds;
uint64_t leeway, delta;
//FOREVER 直接返回什么也不做
if (when == DISPATCH_TIME_FOREVER) {
#if DISPATCH_DEBUG
DISPATCH_CLIENT_CRASH(0, "dispatch_after called with 'when' == infinity");
#endif
return;
}

delta = _dispatch_timeout(when);
if (delta == 0) {
if (block) {
//时间为0直接执行handler
return dispatch_async(dq, handler);
}
return dispatch_async_f(dq, ctxt, handler);
}
//精度 = 间隔 / 10
leeway = delta / 10; // <rdar://problem/13447496>
//<1 毫秒 的时候设置最小值为1毫秒
if (leeway < NSEC_PER_MSEC) leeway = NSEC_PER_MSEC;
//大于60s的时候设置为60s,也就是 1ms <= leeway <= 1min
if (leeway > 60 * NSEC_PER_SEC) leeway = 60 * NSEC_PER_SEC;

// this function can and should be optimized to not use a dispatch source
//创建 type 为 after 的 source
ds = dispatch_source_create(&_dispatch_source_type_after, 0, 0, dq);
dt = ds->ds_timer_refs;

dispatch_continuation_t dc = _dispatch_continuation_alloc();
if (block) {
//包装handler
_dispatch_continuation_init(dc, dq, handler, 0, 0);
} else {
_dispatch_continuation_init_f(dc, dq, ctxt, handler, 0, 0);
}
// reference `ds` so that it doesn't show up as a leak
dc->dc_data = ds;
_dispatch_trace_item_push(dq, dc);
//存储handler
os_atomic_store2o(dt, ds_handler[DS_EVENT_HANDLER], dc, relaxed);
dispatch_clock_t clock;
uint64_t target;
_dispatch_time_to_clock_and_value(when, false, &clock, &target);
if (clock != DISPATCH_CLOCK_WALL) {
leeway = _dispatch_time_nano2mach(leeway);
}
dt->du_timer_flags |= _dispatch_timer_flags_from_clock(clock);
dt->dt_timer.target = target;
dt->dt_timer.interval = UINT64_MAX;
dt->dt_timer.deadline = target + leeway;
dispatch_activate(ds);
}
  • 延时时间设置为DISPATCH_TIME_FOREVER直接返回什么也不做。
  • 延时时间为0直接调用dispatch_async执行handler
  • 精度:1ms <= leeway <= 1min要在这个范围,否则会修正。
  • 创建_dispatch_source_type_after类型的source
  • 包装存储handler
  • 调用_dispatch_time_to_clock_and_value进行target设置。

本质上 dispatch_after 也是对 source的封装。

时间单位

#define NSEC_PER_SEC 1000000000ull      1秒 = 10亿纳秒              
#define NSEC_PER_MSEC 1000000ull 1毫秒 = 100万纳秒
#define USEC_PER_SEC 1000000ull 1秒 = 100万微秒
#define NSEC_PER_USEC 1000ull 1微秒 = 1000 纳秒

1s = 1000ms = 100万us = 10亿ns
1ms = 1000us
1us = 1000ns



作者:HotPotCat
链接:https://www.jianshu.com/p/84153e072f44
收起阅读 »

高级线程应用之栅栏、信号量、调度组以及source(四)

四、Dispatch Source在任一线程上调用它的的一个函数 dispatch_source_merge_data 后,会执行 dispatch source 事先定义好的句柄(可以把句柄简单理解为一个 b...
继续阅读 »

四、Dispatch Source

在任一线程上调用它的的一个函数 dispatch_source_merge_data 后,会执行 dispatch source 事先定义好的句柄(可以把句柄简单理解为一个 block ) 这个过程叫 用户事件(Custom event)。是 dispatch source 支持处理的一种事件。

句柄是一种指向指针的指针,它指向的就是一个类或者结构,它和系统有很密切的关系。比如:实例句柄(HINSTANCE),位图句柄(HBITMAP),设备表述句柄(HDC),图标句柄(HICON)等。这当中还有一个通用的句柄,就是HANDLE

Dispatch Source有两点:

  • CPU 负荷非常小,尽量不占用资源 。
  • 联结的优势。
  • dispatch source不受runloop的影响,底层封装的是pthread

相关API

  • dispatch_source_create 创建源
  • dispatch_source_set_event_handler 设置源事件回调
  • dispatch_source_merge_data 源事件设置数据
  • dispatch_source_get_data 获取源事件数据
  • dispatch_resume 继续
  • dispatch_suspend 挂起

4.1 应用

dispatch_source_t
dispatch_source_create(dispatch_source_type_t type,
uintptr_t handle,
uintptr_t mask,
dispatch_queue_t _Nullable queue);
  • typedispatch 源可处理的事件。比如:DISPATCH_SOURCE_TYPE_TIMERDISPATCH_SOURCE_TYPE_DATA_ADD
    • DISPATCH_SOURCE_TYPE_DATA_ADD: 将所有触发结果相加,最后统一执行响应。间隔的时间越长,则每次触发都会响应;如果间隔的时间很短,则会将触发后的结果相加后统一触发。也就是利用CPU空闲时间进行回调。
  • handle:可以理解为句柄、索引或id,如果要监听进程,需要传入进程的ID
  • mask:可以理解为描述,具体要监听什么。
  • queue:处理handle的队列。

有如下一个进度条的案例:

self.completed = 0;
self.queue = dispatch_queue_create("HotpotCat", NULL);
self.source = dispatch_source_create(DISPATCH_SOURCE_TYPE_DATA_ADD, 0, 0, dispatch_get_main_queue());
//设置句柄
dispatch_source_set_event_handler(self.source, ^{
NSLog(@"%@",[NSThread currentThread]);
NSUInteger value = dispatch_source_get_data(self.source);
self.completed += value;
double progress = self.completed / 100.0;
NSLog(@"progress: %.2f",progress);
self.progressView.progress = progress;
});
self.isRunning = YES;
//创建后默认是挂起状态
dispatch_resume(self.source);

创建了一个ADD类型的source,在handle获取进度增量并更新进度条。由于创建后source处于挂起状态,需要先恢复。

可以在按钮的点击事件中进行任务的挂起和恢复:

if (self.isRunning) {
dispatch_suspend(self.source);
dispatch_suspend(self.queue);
NSLog(@"pause");
self.isRunning = NO;
[sender setTitle:@"pause" forState:UIControlStateNormal];
} else {
dispatch_resume(self.source);
dispatch_resume(self.queue);
NSLog(@"running");
self.isRunning = YES;
[sender setTitle:@"running" forState:UIControlStateNormal];
}

任务的执行是一个简单的循环:

for (NSInteger i = 0; i < 100; i++) {
dispatch_async(self.queue, ^{
NSLog(@"merge");
//加不加 sleep 影响 handler 的执行次数。
sleep(1);
dispatch_source_merge_data(self.source, 1);//+1
});
}
  • 在循环中调用dispatch_source_merge_data触发回调。当queue挂起后后续任务就不再执行了。
  • 在不加sleep的情况下handler的回调是小于100次的,任务会被合并。

4.2 源码解析

4.2.1 dispatch_source_create

dispatch_source_t
dispatch_source_create(dispatch_source_type_t dst, uintptr_t handle,
uintptr_t mask, dispatch_queue_t dq)
{
dispatch_source_refs_t dr;
dispatch_source_t ds;
//add对应 _dispatch_source_data_create timer对应 _dispatch_source_timer_create
dr = dux_create(dst, handle, mask)._dr;
if (unlikely(!dr)) {
return DISPATCH_BAD_INPUT;
}
//创建队列
ds = _dispatch_queue_alloc(source,
dux_type(dr)->dst_strict ? DSF_STRICT : DQF_MUTABLE, 1,
DISPATCH_QUEUE_INACTIVE | DISPATCH_QUEUE_ROLE_INNER)._ds;
ds->dq_label = "source";
ds->ds_refs = dr;
dr->du_owner_wref = _dispatch_ptr2wref(ds);

//没有传队列,获取root_queues
if (unlikely(!dq)) {
dq = _dispatch_get_default_queue(true);
} else {
_dispatch_retain((dispatch_queue_t _Nonnull)dq);
}
//目标队列为传进来的dq
ds->do_targetq = dq;
//是timer 并且设置了interval则调用dispatch_source_set_timer进行设置
//也就是说type为timer的时候即使不设置timer也会默认设置。这里时间间隔设置为了handle
if (dr->du_is_timer && (dr->du_timer_flags & DISPATCH_TIMER_INTERVAL)) {
dispatch_source_set_timer(ds, DISPATCH_TIME_NOW, handle, UINT64_MAX);
}
_dispatch_object_debug(ds, "%s", __func__);
//返回自己创建的source,source本身也是队列。
return ds;
}
  • 根据type创建对应的队列。add对应_dispatch_source_data_createtimer对应_dispatch_source_timer_create
  • 如果创建的时候没有传处理handle的队列,会默认获取root_queues中的队列。
  • 设置目标队列为传进来的队列。
  • 如果typeDISPATCH_SOURCE_TYPE_INTERVAL(应该是私有的)则主动调用一次dispatch_source_set_timer
  • 返回自己创建的sourcesource本身也是队列。

_dispatch_source_data_create

static dispatch_unote_t
_dispatch_source_data_create(dispatch_source_type_t dst, uintptr_t handle,
uintptr_t mask)
{
if (handle || mask) {
return DISPATCH_UNOTE_NULL;
}

// bypass _dispatch_unote_create() because this is always "direct"
// even when EV_UDATA_SPECIFIC is 0
dispatch_unote_class_t du = _dispatch_calloc(1u, dst->dst_size);
du->du_type = dst;
du->du_filter = dst->dst_filter;
du->du_is_direct = true;
return (dispatch_unote_t){ ._du = du };
}

直接调用_dispatch_calloc创建返回。

_dispatch_source_timer_create

static dispatch_unote_t
_dispatch_source_timer_create(dispatch_source_type_t dst,
uintptr_t handle, uintptr_t mask)
{
dispatch_timer_source_refs_t dt;
......
//创建
dt = _dispatch_calloc(1u, dst->dst_size);
dt->du_type = dst;
dt->du_filter = dst->dst_filter;
dt->du_is_timer = true;
dt->du_timer_flags |= (uint8_t)(mask | dst->dst_timer_flags);
dt->du_ident = _dispatch_timer_unote_idx(dt);
dt->dt_timer.target = UINT64_MAX;
dt->dt_timer.deadline = UINT64_MAX;
dt->dt_timer.interval = UINT64_MAX;
dt->dt_heap_entry[DTH_TARGET_ID] = DTH_INVALID_ID;
dt->dt_heap_entry[DTH_DEADLINE_ID] = DTH_INVALID_ID;
return (dispatch_unote_t){ ._dt = dt };
}

内部时间给的默认值是最大值。

4.2.2 dispatch_source_set_event_handler

void
dispatch_source_set_event_handler(dispatch_source_t ds,
dispatch_block_t handler)
{
_dispatch_source_set_handler(ds, handler, DS_EVENT_HANDLER, true);
}

调用_dispatch_source_set_handler传递的类型为DS_EVENT_HANDLER

DISPATCH_NOINLINE
static void
_dispatch_source_set_handler(dispatch_source_t ds, void *func,
uintptr_t kind, bool is_block)
{
dispatch_continuation_t dc;
//创建dc存储handler
dc = _dispatch_source_handler_alloc(ds, func, kind, is_block);
//挂起
if (_dispatch_lane_try_inactive_suspend(ds)) {
//替换
_dispatch_source_handler_replace(ds, kind, dc);
//恢复
return _dispatch_lane_resume(ds, DISPATCH_RESUME);
}
......
}
  • 创建_dispatch_source_handler_alloc存储handler,内部会进行标记非DS_EVENT_HANDLER会标记为DC_FLAG_CONSUME
  • _dispatch_lane_try_inactive_suspend挂起队列。
  • _dispatch_source_handler_replace替换handler

  • static inline void
    _dispatch_source_handler_replace(dispatch_source_t ds, uintptr_t kind,
    dispatch_continuation_t dc)
    {
    //handler目标回调为空释放handler
    if (!dc->dc_func) {
    _dispatch_continuation_free(dc);
    dc = NULL;
    } else if (dc->dc_flags & DC_FLAG_FETCH_CONTEXT) {
    dc->dc_ctxt = ds->do_ctxt;
    }
    //保存
    dc = os_atomic_xchg(&ds->ds_refs->ds_handler[kind], dc, release);
    if (dc) _dispatch_source_handler_dispose(dc);
    }
    _dispatch_lane_resume恢复队列,调用队列对应的awake

    • 先调用_dispatch_lane_resume_activate(这也就是set后立马调用的原因):
    static void
    _dispatch_lane_resume_activate(dispatch_lane_t dq)
    {
    if (dx_vtable(dq)->dq_activate) {
    dx_vtable(dq)->dq_activate(dq);
    }

    _dispatch_lane_resume(dq, DISPATCH_ACTIVATION_DONE);
    }

    再调用_dispatch_lane_resume

    4.2.3 dispatch_source_merge_data

    void
    dispatch_source_merge_data(dispatch_source_t ds, uintptr_t val)
    {
    dispatch_queue_flags_t dqf = _dispatch_queue_atomic_flags(ds);
    dispatch_source_refs_t dr = ds->ds_refs;

    if (unlikely(dqf & (DSF_CANCELED | DQF_RELEASED))) {
    return;
    }
    //根据类型存值
    switch (dr->du_filter) {
    case DISPATCH_EVFILT_CUSTOM_ADD:
    //有累加
    os_atomic_add2o(dr, ds_pending_data, val, relaxed);
    break;
    case DISPATCH_EVFILT_CUSTOM_OR:
    os_atomic_or2o(dr, ds_pending_data, val, relaxed);
    break;
    case DISPATCH_EVFILT_CUSTOM_REPLACE:
    os_atomic_store2o(dr, ds_pending_data, val, relaxed);
    break;
    default:
    DISPATCH_CLIENT_CRASH(dr->du_filter, "Invalid source type");
    }
    //唤醒执行回调
    dx_wakeup(ds, 0, DISPATCH_WAKEUP_MAKE_DIRTY);
    }
    • 根据类型对值进行处理,处理完之后唤醒队列执行。

    对于主线程会执行_dispatch_main_queue_wakeup,其中会取到dispatch_queue获取到dc,最后进行handler的调用。

    4.2.4 dispatch_source_get_data

    uintptr_t
    dispatch_source_get_data(dispatch_source_t ds)
    {
    dispatch_source_refs_t dr = ds->ds_refs;
    #if DISPATCH_USE_MEMORYSTATUS
    if (dr->du_vmpressure_override) {
    return NOTE_VM_PRESSURE;
    }
    #if TARGET_OS_SIMULATOR
    if (dr->du_memorypressure_override) {
    return NOTE_MEMORYSTATUS_PRESSURE_WARN;
    }
    #endif
    #endif // DISPATCH_USE_MEMORYSTATUS
    //获取数据
    uint64_t value = os_atomic_load2o(dr, ds_data, relaxed);
    return (unsigned long)(dr->du_has_extended_status ?
    DISPATCH_SOURCE_GET_DATA(value) : value);
    }

    merge_data相反,一个存一个取。

    4.2.5 dispatch_resume

    void
    dispatch_resume(dispatch_object_t dou)
    {
    DISPATCH_OBJECT_TFB(_dispatch_objc_resume, dou);
    if (unlikely(_dispatch_object_is_global(dou) ||
    _dispatch_object_is_root_or_base_queue(dou))) {
    return;
    }
    if (dx_cluster(dou._do) == _DISPATCH_QUEUE_CLUSTER) {
    _dispatch_lane_resume(dou._dl, DISPATCH_RESUME);
    }
    }

    经过调试走的是_dispatch_lane_resume逻辑,与_dispatch_source_set_handler中调用的一致。awake队列。

    4.2.6 dispatch_suspend

    void
    dispatch_suspend(dispatch_object_t dou)
    {
    DISPATCH_OBJECT_TFB(_dispatch_objc_suspend, dou);
    if (unlikely(_dispatch_object_is_global(dou) ||
    _dispatch_object_is_root_or_base_queue(dou))) {
    return;
    }
    if (dx_cluster(dou._do) == _DISPATCH_QUEUE_CLUSTER) {
    return _dispatch_lane_suspend(dou._dl);
    }
    }

    调用_dispatch_lane_suspend挂起队列。

    4.2.7 dispatch_source_cancel

    dispatch_source_cancel(dispatch_source_t ds)
    {
    _dispatch_object_debug(ds, "%s", __func__);

    _dispatch_retain_2(ds);

    if (_dispatch_queue_atomic_flags_set_orig(ds, DSF_CANCELED) & DSF_CANCELED){
    _dispatch_release_2_tailcall(ds);
    } else {
    //_dispatch_workloop_wakeup
    dx_wakeup(ds, 0, DISPATCH_WAKEUP_MAKE_DIRTY | DISPATCH_WAKEUP_CONSUME_2);
    }
    }



    调用_dispatch_workloop_wakeup

    • cancel内部会对状态进行判断,如果是挂起状态会报错。所以需要在运行状态下取消。
    • 调用_dispatch_release_2_tailcall进行释放操作。

    4.2.8 dispatch_source_set_timer

    void
    dispatch_source_set_timer(dispatch_source_t ds, dispatch_time_t start,
    uint64_t interval, uint64_t leeway)
    {
    dispatch_timer_source_refs_t dt = ds->ds_timer_refs;
    dispatch_timer_config_t dtc;

    if (unlikely(!dt->du_is_timer)) {
    DISPATCH_CLIENT_CRASH(ds, "Attempt to set timer on a non-timer source");
    }
    //根据type配置timer和interval
    if (dt->du_timer_flags & DISPATCH_TIMER_INTERVAL) {
    dtc = _dispatch_interval_config_create(start, interval, leeway, dt);
    } else {
    dtc = _dispatch_timer_config_create(start, interval, leeway, dt);
    }
    if (_dispatch_timer_flags_to_clock(dt->du_timer_flags) != dtc->dtc_clock &&
    dt->du_filter == DISPATCH_EVFILT_TIMER_WITH_CLOCK) {
    DISPATCH_CLIENT_CRASH(0, "Attempting to modify timer clock");
    }
    //跟踪配置
    _dispatch_source_timer_telemetry(ds, dtc->dtc_clock, &dtc->dtc_timer);
    dtc = os_atomic_xchg2o(dt, dt_pending_config, dtc, release);
    if (dtc) free(dtc);
    //唤醒
    dx_wakeup(ds, 0, DISPATCH_WAKEUP_MAKE_DIRTY);
    }

    4.2.9 dispatch_source_set_registration_handler

    void
    dispatch_source_set_registration_handler(dispatch_source_t ds,
    dispatch_block_t handler)
    {
    _dispatch_source_set_handler(ds, handler, DS_REGISTN_HANDLER, true);
    }

    也是直接调用的_dispatch_source_set_handler,参数是DS_REGISTN_HANDLER

    4.2.10 dispatch_source_set_cancel_handler

    void
    dispatch_source_set_cancel_handler(dispatch_source_t ds,
    dispatch_block_t handler)
    {
    _dispatch_source_set_handler(ds, handler, DS_CANCEL_HANDLER, true);
    }
    • 直接调用的_dispatch_source_set_handler,参数是DS_CANCEL_HANDLER
    • 会根据DS_REGISTN_HANDLER、DS_CANCEL_HANDLER、DS_EVENT_HANDLER进行handler的获取和释放,因为这三者可能同时存在。

    那么就有个问题设置timer类型后我们没有主动调用dispatch_source_merge_data,那么它是在什么时机调用的呢?在回调中bt:

        frame #2: 0x000000010b6a29c8 libdispatch.dylib`_dispatch_client_callout + 8
    frame #3: 0x000000010b6a5316 libdispatch.dylib`_dispatch_continuation_pop + 557
    frame #4: 0x000000010b6b8e8b libdispatch.dylib`_dispatch_source_invoke + 2205
    frame #5: 0x000000010b6b4508 libdispatch.dylib`_dispatch_root_queue_drain + 351
    frame #6: 0x000000010b6b4e6d libdispatch.dylib`_dispatch_worker_thread2 + 135
    frame #7: 0x00007fff611639f7 libsystem_pthread.dylib`_pthread_wqthread + 220
    frame #8: 0x00007fff61162b77 libsystem_pthread.dylib`start_wqthread + 15

    搜索_dispatch_source_invoke只找到了:

    DISPATCH_VTABLE_INSTANCE(source,
    .do_type = DISPATCH_SOURCE_KEVENT_TYPE,
    .do_dispose = _dispatch_source_dispose,
    .do_debug = _dispatch_source_debug,
    .do_invoke = _dispatch_source_invoke,

    .dq_activate = _dispatch_source_activate,
    .dq_wakeup = _dispatch_source_wakeup,
    .dq_push = _dispatch_lane_push,
    );
    也就是调用的sourcedo_invoke,调用逻辑为_dispatch_root_queue_drain -> _dispatch_continuation_pop_inline -> dx_invoke

    void
    _dispatch_source_invoke(dispatch_source_t ds, dispatch_invoke_context_t dic,
    dispatch_invoke_flags_t flags)
    {
    _dispatch_queue_class_invoke(ds, dic, flags,
    DISPATCH_INVOKE_DISALLOW_SYNC_WAITERS, _dispatch_source_invoke2);

    #if DISPATCH_EVENT_BACKEND_KEVENT
    if (flags & DISPATCH_INVOKE_WORKLOOP_DRAIN) {
    dispatch_workloop_t dwl = (dispatch_workloop_t)_dispatch_get_wlh();
    dispatch_timer_heap_t dth = dwl->dwl_timer_heap;
    if (dth && dth[0].dth_dirty_bits) {
    //调用
    _dispatch_event_loop_drain_timers(dwl->dwl_timer_heap,
    DISPATCH_TIMER_WLH_COUNT);
    }
    }
    #endif // DISPATCH_EVENT_BACKEND_KEVENT
    }




    收起阅读 »

    高级线程应用之栅栏、信号量、调度组以及source(三)

    二、信号量(dispatch_semaphore_t)相关函数:dispatch_semaphore_create:创建信号量dispatch_semaphore_wait:信号量等待dispatch_semaphore_signal:信号量释放信号量有两个效...
    继续阅读 »

    二、信号量(dispatch_semaphore_t

    相关函数:

    • dispatch_semaphore_create:创建信号量
    • dispatch_semaphore_wait:信号量等待
    • dispatch_semaphore_signal:信号量释放

    信号量有两个效果:同步作为锁 与 控制GCD最大并发数

    二元信号量是最简单的一种锁,只有两种状态:占用与非占用。适合只能被唯一一个线程独占访问资源。当二元信号量处于非占用状态时,第一个试图获取该二元信号量的线程会获得该锁,并将二元信号置为占用状态,此后其他的所有视图获取该二元信号量的线程将会等待,直到该锁被释放。

    对于允许多个线程并发访问的资源,多元信号量简称信号量,它是一个很好的选择。一个初始值为 N 的信号量允许 N 个线程并发访问。线程访问资源的时候首先获取信号量,进行如下操作:

    • 将信号量的值减1
    • 如果信号量的值小于0,则进入等待状态,否则继续执行。

    访问完资源之后,线程释放信号量,进行如下操作:

    • 将信号量的值+1
    • 如果信号量的值< 1,唤醒一个等待中的线程。

    2.1 应用

        dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    dispatch_semaphore_t sem = dispatch_semaphore_create(1);
    dispatch_queue_t queue1 = dispatch_queue_create("HotpotCat", NULL);

    dispatch_async(queue, ^{
    dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
    NSLog(@"1 start");
    NSLog(@"1 end");
    dispatch_semaphore_signal(sem);
    });

    dispatch_async(queue1, ^{
    dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
    NSLog(@"2 start");
    NSLog(@"2 end");
    dispatch_semaphore_signal(sem);
    });

    dispatch_async(queue, ^{
    dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
    NSLog(@"3 start");
    NSLog(@"3 end");
    dispatch_semaphore_signal(sem);
    });

    dispatch_async(queue1, ^{
    dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
    NSLog(@"4 start");
    NSLog(@"4 end");
    dispatch_semaphore_signal(sem);
    });

    对于上面的例子输出:

    1 start
    1 end
    2 start
    2 end
    3 start
    3 end
    4 start
    4 end
    这个时候信号量初始化的是1,全局队列与自定义串行队列中的任务按顺序依次执行。
    当将信号量改为2后输出:
    1 start
    2 start
    2 end
    1 end
    3 start
    4 start
    3 end
    4 end

    这个时候1、2先执行无序,3、4后执行无序。这样就控制了GCD任务的最大并发数。

    修改代码如下:

    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    dispatch_semaphore_t sem = dispatch_semaphore_create(0);

    dispatch_async(queue, ^{
    dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
    NSLog(@"1 start");
    NSLog(@"1 end");
    });

    dispatch_async(queue, ^{
    sleep(2);
    NSLog(@"2 start");
    NSLog(@"2 end");
    dispatch_semaphore_signal(sem);
    });

    信号量初始值修改为0,在任务1wait,在任务2signal,这个时候输出如下:

    2 start
    2 end
    1 start
    1 end

    任务2比任务1先执行了。由于信号量初始化为0wait函数后面任务就执行不了一直等待;等到signal执行后发送信号wait就可以执行了。这样就达到了控制流程。任务2中的信号控制了任务1的执行。

    2.2 源码分析

    2.2.1 dispatch_semaphore_create

    /*
    * @param dsema
    * The semaphore. The result of passing NULL in this parameter is undefined.
    */


    dispatch_semaphore_t
    dispatch_semaphore_create(intptr_t value)
    {
    dispatch_semaphore_t dsema;

    // If the internal value is negative, then the absolute of the value is
    // equal to the number of waiting threads. Therefore it is bogus to
    // initialize the semaphore with a negative value.
    if (value < 0) { //>=0 才有用,否则直接返回
    return DISPATCH_BAD_INPUT;// 0
    }

    dsema = _dispatch_object_alloc(DISPATCH_VTABLE(semaphore),
    sizeof(struct dispatch_semaphore_s));
    dsema->do_next = DISPATCH_OBJECT_LISTLESS;
    dsema->do_targetq = _dispatch_get_default_queue(false);
    dsema->dsema_value = value;
    _dispatch_sema4_init(&dsema->dsema_sema, _DSEMA4_POLICY_FIFO);
    dsema->dsema_orig = value;
    return dsema;
    }
    • value < 0的时候无效,只有>= 0才有效,才会执行后续流程。

    2.2.2 dispatch_semaphore_wait

    intptr_t
    dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout)
    {
    //--
    long value = os_atomic_dec2o(dsema, dsema_value, acquire);
    if (likely(value >= 0)) { //>=0 返回
    return 0;
    }
    return _dispatch_semaphore_wait_slow(dsema, timeout);
    }
    • --value大于等于0直接返回0。执行dispatch_semaphore_wait后续的代码。
    • 否则执行_dispatch_semaphore_wait_slow(相当于do-while循环)。

    _dispatch_semaphore_wait_slow
    当信号量为0的时候调用wait后(< 0)就走_dispatch_semaphore_wait_slow逻辑了:

    DISPATCH_NOINLINE
    static intptr_t
    _dispatch_semaphore_wait_slow(dispatch_semaphore_t dsema,
    dispatch_time_t timeout)
    {
    long orig;

    _dispatch_sema4_create(&dsema->dsema_sema, _DSEMA4_POLICY_FIFO);
    //超时直接break
    switch (timeout) {
    default:
    if (!_dispatch_sema4_timedwait(&dsema->dsema_sema, timeout)) {
    break;
    }
    // Fall through and try to undo what the fast path did to
    // dsema->dsema_value
    //NOW的情况下进行超时处理
    case DISPATCH_TIME_NOW:
    orig = dsema->dsema_value;
    while (orig < 0) {
    if (os_atomic_cmpxchgv2o(dsema, dsema_value, orig, orig + 1,
    &orig, relaxed)) {
    return _DSEMA4_TIMEOUT();
    }
    }
    // Another thread called semaphore_signal().
    // Fall through and drain the wakeup.
    //FOREVER则进入wait逻辑。
    case DISPATCH_TIME_FOREVER:
    _dispatch_sema4_wait(&dsema->dsema_sema);
    break;
    }
    return 0;
    }
    • 当值为timeout的时候直接break
    • 当值为DISPATCH_TIME_NOW的时候循环调用_DSEMA4_TIMEOUT()
    #define _DSEMA4_TIMEOUT() KERN_OPERATION_TIMED_OUT
    • 当值为DISPATCH_TIME_FOREVER的时候调用_dispatch_sema4_wait

    _dispatch_sema4_wait

    //    void
    // _dispatch_sema4_wait(_dispatch_sema4_t *sema)
    // {
    // int ret = 0;
    // do {
    // ret = sem_wait(sema);
    // } while (ret == -1 && errno == EINTR);
    // DISPATCH_SEMAPHORE_VERIFY_RET(ret);
    // }

    void
    _dispatch_sema4_wait(_dispatch_sema4_t *sema)
    {
    kern_return_t kr;
    do {
    kr = semaphore_wait(*sema);
    } while (kr == KERN_ABORTED);
    DISPATCH_SEMAPHORE_VERIFY_KR(kr);
    }
  • semaphore_wait并没有搜到实现,这是pthread内核封装的实现。
  • _dispatch_sema4_wait本质上是一个do-while循环,相当于在这里直接卡住执行不到后面的逻辑了。相当于:


  • dispatch_async(queue, ^{
    // dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
    do {
    //循环
    } while (signal <= 0);
    NSLog(@"1 start");
    NSLog(@"1 end");
    });

    结论:value >= 0的时候执行后续的代码,否则do-while循环卡住后续逻辑

    2.2.3 dispatch_semaphore_signal

    /*!
    * @function dispatch_semaphore_signal
    *
    * @abstract
    * Signal (increment) a semaphore.
    *
    * @discussion
    * Increment the counting semaphore. If the previous value was less than zero,
    * this function wakes a waiting thread before returning.
    *
    * @param dsema The counting semaphore.
    * The result of passing NULL in this parameter is undefined.
    *
    * @result
    * This function returns non-zero if a thread is woken. Otherwise, zero is
    * returned.
    */

    intptr_t
    dispatch_semaphore_signal(dispatch_semaphore_t dsema)
    {
    //++操作
    long value = os_atomic_inc2o(dsema, dsema_value, release);
    if (likely(value > 0)) {
    return 0;
    }
    //++ 后还 < 0,则表示做wait操作(--)过多。报错。
    if (unlikely(value == LONG_MIN)) {
    DISPATCH_CLIENT_CRASH(value,
    "Unbalanced call to dispatch_semaphore_signal()");
    }
    //发送信号量逻辑,恢复wait等待的操作。
    return _dispatch_semaphore_signal_slow(dsema);
    }
    • os_atomic_inc2o执行++后值大于0直接返回能够执行。
    • 只有<= 0的时候才执行后续流程,调用_dispatch_semaphore_signal_slow进行异常处理。
    • 注释说明了当值< 0的时候在return之前唤醒一个等待线程。

    _dispatch_semaphore_signal_slow

    intptr_t
    _dispatch_semaphore_signal_slow(dispatch_semaphore_t dsema)
    {
    _dispatch_sema4_create(&dsema->dsema_sema, _DSEMA4_POLICY_FIFO);
    _dispatch_sema4_signal(&dsema->dsema_sema, 1);
    return 1;
    }

    直接调用_dispatch_sema4_signal

    _dispatch_sema4_signal

    #define DISPATCH_SEMAPHORE_VERIFY_KR(x) do { \
    DISPATCH_VERIFY_MIG(x); \
    if (unlikely((x) == KERN_INVALID_NAME)) { \
    DISPATCH_CLIENT_CRASH((x), \
    "Use-after-free of dispatch_semaphore_t or dispatch_group_t"); \
    } else if (unlikely(x)) { \
    DISPATCH_INTERNAL_CRASH((x), "mach semaphore API failure"); \
    } \
    } while (0)


    //经过调试走的是这个逻辑
    void
    _dispatch_sema4_signal(_dispatch_sema4_t *sema, long count)
    {
    do {
    kern_return_t kr = semaphore_signal(*sema);//+1
    DISPATCH_SEMAPHORE_VERIFY_KR(kr);// == -1 报错
    } while (--count);//do-while(0) 只执行一次
    }

    相当于内部做了+1操作。这也是当信号量初始值为0的时候dispatch_semaphore_signal执行完毕后dispatch_semaphore_wait能够执行的原因。

    小结:

    • dispatch_semaphore_wait进行--操作,减完是负值进入do-while循环,阻塞后续流程
    • dispatch_semaphore_signal进行++操作,加完值不大于0进入后续报错流程
    • semaphore_signal 与 semaphore_wait才是信号量能控制最大并发数的根本原因,否则dispatch_semaphore_signaldispatch_semaphore_signal都是判断后直接返回,相当于什么都没做

    semaphore_signal & semaphore_wait

    三、调度组

    最直接的作用: 控制任务执行顺序
    相关API:

    • dispatch_group_create 创建组
    • dispatch_group_async 进组任务 (与dispatch_group_enterdispatch_group_leave搭配使用效果相同)
      • dispatch_group_enter 进组
      • dispatch_group_leave 出组
    • dispatch_group_notify 进组任务执行完毕通知
    • dispatch_group_wait 进组任务执行等待时间

    3.1 应用

    dispatch_group_t group = dispatch_group_create();
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    dispatch_queue_t queue1 = dispatch_queue_create("test", DISPATCH_QUEUE_CONCURRENT);

    dispatch_group_async(group, queue, ^{
    sleep(3);
    NSLog(@"1");
    });

    dispatch_group_async(group, queue1, ^{
    sleep(2);
    NSLog(@"2");
    });

    dispatch_group_async(group, queue1, ^{
    sleep(1);
    NSLog(@"3");
    });

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

    dispatch_group_notify(group, dispatch_get_global_queue(0, 0), ^{
    NSLog(@"5");
    });

    有如上案例,任务5永远在任务1、2、3、4之后执行。

    当然也可以使用enterleave配合dispatch_async使用:

    dispatch_group_t group = dispatch_group_create();
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    dispatch_queue_t queue1 = dispatch_queue_create("test", DISPATCH_QUEUE_CONCURRENT);

    //先 enter 再 leave
    dispatch_group_enter(group);
    dispatch_async(queue, ^{
    sleep(3);
    NSLog(@"1");
    dispatch_group_leave(group);
    });

    dispatch_group_enter(group);
    dispatch_async(queue1, ^{
    sleep(2);
    NSLog(@"2");
    dispatch_group_leave(group);
    });

    dispatch_group_enter(group);
    dispatch_async(queue1, ^{
    sleep(1);
    NSLog(@"3");
    dispatch_group_leave(group);
    });

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

    dispatch_group_notify(group, dispatch_get_global_queue(0, 0), ^{
    NSLog(@"5");
    });

    效果相同,需要注意的是dispatch_group_enter要比dispatch_group_leave先调用,并且必须成对出现,否则会崩溃。当然两种形式也可以混着用。

    3.2 源码分析

    根据上面的分析有3个问题:

    • 1.dispatch_group_enter为什么要比dispatch_group_leave先调用,否则崩溃?
    • 2.能够实现同步的原理是什么?
    • 3.dispatch_group_async为什么等价于dispatch_group_enter + dispatch_group_leave?

    之前的版本调度组是封装了信号量,目前新版本的是调度组自己写了一套逻辑。

    3.2.1 dispatch_group_create


    dispatch_group_t
    dispatch_group_create(void)
    {
    return _dispatch_group_create_with_count(0);
    }

    //creat & enter 写在一起的写法,信号量标记位1
    dispatch_group_t
    _dispatch_group_create_and_enter(void)
    {
    return _dispatch_group_create_with_count(1);
    }

    是对_dispatch_group_create_with_count的调用:

    static inline dispatch_group_t
    _dispatch_group_create_with_count(uint32_t n)
    {
    dispatch_group_t dg = _dispatch_object_alloc(DISPATCH_VTABLE(group),
    sizeof(struct dispatch_group_s));
    dg->do_next = DISPATCH_OBJECT_LISTLESS;
    dg->do_targetq = _dispatch_get_default_queue(false);
    if (n) {
    os_atomic_store2o(dg, dg_bits,
    (uint32_t)-n * DISPATCH_GROUP_VALUE_INTERVAL, relaxed);
    os_atomic_store2o(dg, do_ref_cnt, 1, relaxed); // <rdar://22318411>
    }
    return dg;
    }

    调用_dispatch_object_alloc创建group,与信号量写法相似

    3.2.2 dispatch_group_enter

    void
    dispatch_group_enter(dispatch_group_t dg)
    {
    // The value is decremented on a 32bits wide atomic so that the carry
    // for the 0 -> -1 transition is not propagated to the upper 32bits.
    //0-- -> -1,与信号量不同的是没有wait
    uint32_t old_bits = os_atomic_sub_orig2o(dg, dg_bits,
    DISPATCH_GROUP_VALUE_INTERVAL, acquire);
    uint32_t old_value = old_bits & DISPATCH_GROUP_VALUE_MASK;
    if (unlikely(old_value == 0)) {
    _dispatch_retain(dg); // <rdar://problem/22318411>
    }
    if (unlikely(old_value == DISPATCH_GROUP_VALUE_MAX)) {
    DISPATCH_CLIENT_CRASH(old_bits,
    "Too many nested calls to dispatch_group_enter()");
    }
    }
    • 0--变为-1,与信号量不同的是没有wait操作。

    3.2.3 dispatch_group_leave

    void
    dispatch_group_leave(dispatch_group_t dg)
    {
    // The value is incremented on a 64bits wide atomic so that the carry for
    // the -1 -> 0 transition increments the generation atomically.
    //-1++ -> 0
    uint64_t new_state, old_state = os_atomic_add_orig2o(dg, dg_state,
    DISPATCH_GROUP_VALUE_INTERVAL, release);
    //#define DISPATCH_GROUP_VALUE_MASK 0x00000000fffffffcULL
    // old_state & DISPATCH_GROUP_VALUE_MASK 是一个很大的值
    uint32_t old_value = (uint32_t)(old_state & DISPATCH_GROUP_VALUE_MASK);
    //-1 & DISPATCH_GROUP_VALUE_MASK == DISPATCH_GROUP_VALUE_1,old_value = -1
    if (unlikely(old_value == DISPATCH_GROUP_VALUE_1)) {//old_value == -1
    old_state += DISPATCH_GROUP_VALUE_INTERVAL;
    do {
    new_state = old_state;
    if ((old_state & DISPATCH_GROUP_VALUE_MASK) == 0) {
    new_state &= ~DISPATCH_GROUP_HAS_WAITERS;
    new_state &= ~DISPATCH_GROUP_HAS_NOTIFS;
    } else {
    // If the group was entered again since the atomic_add above,
    // we can't clear the waiters bit anymore as we don't know for
    // which generation the waiters are for
    new_state &= ~DISPATCH_GROUP_HAS_NOTIFS;
    }
    if (old_state == new_state) break;
    } while (unlikely(!os_atomic_cmpxchgv2o(dg, dg_state,
    old_state, new_state, &old_state, relaxed)));
    //调用 _dispatch_group_wake,唤醒 dispatch_group_notify
    return _dispatch_group_wake(dg, old_state, true);
    }
    //old_value 为0的情况下直接报错,也就是先leave的情况下直接报错
    if (unlikely(old_value == 0)) {
    DISPATCH_CLIENT_CRASH((uintptr_t)old_value,
    "Unbalanced call to dispatch_group_leave()");
    }
    }
    • -1++变为0,当old_value == -1的时候调用_dispatch_group_wake唤醒dispatch_group_notify
    • 既然old_value == -1的时候才唤醒,那么多次enter只有最后一次leave的时候才能唤醒。
    • old_value == 0的时候直接报错,这也就是为什么先调用leave直接发生了crash

    3.2.4 dispatch_group_notify

    void
    dispatch_group_notify(dispatch_group_t dg, dispatch_queue_t dq,
    dispatch_block_t db)
    {
    dispatch_continuation_t dsn = _dispatch_continuation_alloc();
    _dispatch_continuation_init(dsn, dq, db, 0, DC_FLAG_CONSUME);
    _dispatch_group_notify(dg, dq, dsn);
    }

    调用_dispatch_group_notify

    static inline void
    _dispatch_group_notify(dispatch_group_t dg, dispatch_queue_t dq,
    dispatch_continuation_t dsn)
    {
    uint64_t old_state, new_state;
    dispatch_continuation_t prev;

    dsn->dc_data = dq;
    _dispatch_retain(dq);

    prev = os_mpsc_push_update_tail(os_mpsc(dg, dg_notify), dsn, do_next);
    if (os_mpsc_push_was_empty(prev)) _dispatch_retain(dg);
    os_mpsc_push_update_prev(os_mpsc(dg, dg_notify), prev, dsn, do_next);
    if (os_mpsc_push_was_empty(prev)) {
    os_atomic_rmw_loop2o(dg, dg_state, old_state, new_state, release, {
    new_state = old_state | DISPATCH_GROUP_HAS_NOTIFS;
    if ((uint32_t)old_state == 0) {//循环判断 old_state == 0 的时候 wake
    os_atomic_rmw_loop_give_up({
    return _dispatch_group_wake(dg, new_state, false);
    });
    }
    });
    }
    }
    • old_state == 0的时候调用_dispatch_group_wake,也就是调用blockcallout。与leave调用了同一个方法。

    为什么两个地方都调用了?
    因为在leave的时候dispatch_group_notify可能已经执行过了,任务已经保存在了group中,leave的时候本身尝试调用一次。
    当然leave中也可能是一个延时任务,当调用leave的时候notify可能还没有执行,就导致notify任务还不存在。所以需要在notify中也调用。

    _dispatch_group_wake

    static void
    _dispatch_group_wake(dispatch_group_t dg, uint64_t dg_state, bool needs_release)
    {
    uint16_t refs = needs_release ? 1 : 0; // <rdar://problem/22318411>

    if (dg_state & DISPATCH_GROUP_HAS_NOTIFS) {
    dispatch_continuation_t dc, next_dc, tail;

    // Snapshot before anything is notified/woken <rdar://problem/8554546>
    dc = os_mpsc_capture_snapshot(os_mpsc(dg, dg_notify), &tail);
    do {
    dispatch_queue_t dsn_queue = (dispatch_queue_t)dc->dc_data;
    next_dc = os_mpsc_pop_snapshot_head(dc, tail, do_next);
    //异步回调,执行block callout
    _dispatch_continuation_async(dsn_queue, dc,
    _dispatch_qos_from_pp(dc->dc_priority), dc->dc_flags);
    _dispatch_release(dsn_queue);
    } while ((dc = next_dc));

    refs++;
    }

    if (dg_state & DISPATCH_GROUP_HAS_WAITERS) {
    _dispatch_wake_by_address(&dg->dg_gen);
    }

    if (refs) _dispatch_release_n(dg, refs);
    }
    • 调用_dispatch_continuation_async相当于调用的是dispatch_async执行notify的任务。
    • 任务先保存在在group中,唤醒notify的时候才将任务加入队列。

    3.2.5 dispatch_group_async

    dispatch_group_async(dispatch_group_t dg, dispatch_queue_t dq,
    dispatch_block_t db)
    {
    dispatch_continuation_t dc = _dispatch_continuation_alloc();
    //标记 DC_FLAG_GROUP_ASYNC
    uintptr_t dc_flags = DC_FLAG_CONSUME | DC_FLAG_GROUP_ASYNC;
    dispatch_qos_t qos;

    qos = _dispatch_continuation_init(dc, dq, db, 0, dc_flags);
    _dispatch_continuation_group_async(dg, dq, dc, qos);
    }

    调用_dispatch_continuation_group_async

    static inline void
    _dispatch_continuation_group_async(dispatch_group_t dg, dispatch_queue_t dq,
    dispatch_continuation_t dc, dispatch_qos_t qos)
    {
    //调用enter
    dispatch_group_enter(dg);
    dc->dc_data = dg;
    //dispatch_async
    _dispatch_continuation_async(dq, dc, qos, dc->dc_flags);
    }

    • 内部先调用dispatch_group_enter,在这里就等待wakeup的调用了
    • 再调用_dispatch_continuation_async,相当于dispatch_async

    那么leave在什么时候调用呢?
    肯定要在callout执行完毕后调用。_dispatch_continuation_async的调用以全局队列为例调用_dispatch_root_queue_push,最终会调用到_dispatch_continuation_invoke_inline






    在这里就进行了逻辑区分,有group的情况下(dispatch_group_async的时候dc_flags进行了标记)调用的是_dispatch_continuation_with_group_invoke

    static inline void
    _dispatch_continuation_with_group_invoke(dispatch_continuation_t dc)
    {
    struct dispatch_object_s *dou = dc->dc_data;
    unsigned long type = dx_type(dou);
    if (type == DISPATCH_GROUP_TYPE) {
    //callout
    _dispatch_client_callout(dc->dc_ctxt, dc->dc_func);
    _dispatch_trace_item_complete(dc);
    //leave
    dispatch_group_leave((dispatch_group_t)dou);
    } else {
    DISPATCH_INTERNAL_CRASH(dx_type(dou), "Unexpected object type");
    }
    }


    • callout后调用了dispatch_group_leave

    dispatch_group_async 底层是对 dispatch_group_enter + dispatch_group_leave 的封装

    • dispatch_group_async中先进行dispatch_group_enter,然后执行dispatch_async
    • 在回调中先_dispatch_client_callout然后dispatch_group_leave


    收起阅读 »

    高级线程应用之栅栏、信号量、调度组以及source(二)

    1.2.1.2 _dispatch_lane_non_barrier_completestatic void _dispatch_lane_non_barrier_complete(dispatch_lane_t dq, dispatch_wa...
    继续阅读 »


    1.2.1.2 _dispatch_lane_non_barrier_complete

    static void
    _dispatch_lane_non_barrier_complete(dispatch_lane_t dq,
    dispatch_wakeup_flags_t flags)
    {
    ......
    _dispatch_lane_non_barrier_complete_finish(dq, flags, old_state, new_state);
    }

    其中是对_dispatch_lane_non_barrier_complete_finish的调用。

    DISPATCH_ALWAYS_INLINE
    static void
    _dispatch_lane_non_barrier_complete_finish(dispatch_lane_t dq,
    dispatch_wakeup_flags_t flags, uint64_t old_state, uint64_t new_state)
    {
    if (_dq_state_received_override(old_state)) {
    // Ensure that the root queue sees that this thread was overridden.
    _dispatch_set_basepri_override_qos(_dq_state_max_qos(old_state));
    }

    if ((old_state ^ new_state) & DISPATCH_QUEUE_IN_BARRIER) {
    if (_dq_state_is_dirty(old_state)) {
    //走_dispatch_lane_barrier_complete逻辑
    return _dispatch_lane_barrier_complete(dq, 0, flags);
    }

    if ((old_state ^ new_state) & DISPATCH_QUEUE_ENQUEUED) {
    if (!(flags & DISPATCH_WAKEUP_CONSUME_2)) {
    _dispatch_retain_2(dq);
    }
    dispatch_assert(!_dq_state_is_base_wlh(new_state));
    _dispatch_trace_item_push(dq->do_targetq, dq);
    return dx_push(dq->do_targetq, dq, _dq_state_max_qos(new_state));
    }

    if (flags & DISPATCH_WAKEUP_CONSUME_2) {
    _dispatch_release_2_tailcall(dq);
    }
    }

    走的是_dispatch_lane_barrier_complete逻辑:

    DISPATCH_NOINLINE
    static void
    _dispatch_lane_barrier_complete(dispatch_lane_class_t dqu, dispatch_qos_t qos,
    dispatch_wakeup_flags_t flags)
    {
    dispatch_queue_wakeup_target_t target = DISPATCH_QUEUE_WAKEUP_NONE;
    dispatch_lane_t dq = dqu._dl;

    if (dq->dq_items_tail && !DISPATCH_QUEUE_IS_SUSPENDED(dq)) {
    struct dispatch_object_s *dc = _dispatch_queue_get_head(dq);
    //串行队列
    if (likely(dq->dq_width == 1 || _dispatch_object_is_barrier(dc))) {
    if (_dispatch_object_is_waiter(dc)) {
    //栅栏中的任务逻辑
    return _dispatch_lane_drain_barrier_waiter(dq, dc, flags, 0);
    }
    } else if (dq->dq_width > 1 && !_dispatch_object_is_barrier(dc)) {
    return _dispatch_lane_drain_non_barriers(dq, dc, flags);
    }

    if (!(flags & DISPATCH_WAKEUP_CONSUME_2)) {
    _dispatch_retain_2(dq);
    flags |= DISPATCH_WAKEUP_CONSUME_2;
    }
    target = DISPATCH_QUEUE_WAKEUP_TARGET;
    }

    uint64_t owned = DISPATCH_QUEUE_IN_BARRIER +
    dq->dq_width * DISPATCH_QUEUE_WIDTH_INTERVAL;
    //执行栅栏后续的代码
    return _dispatch_lane_class_barrier_complete(dq, qos, flags, target, owned);
    }
    • _dispatch_lane_drain_barrier_waiter执行栅栏函数中的任务。
    • _dispatch_lane_class_barrier_complete执行栅栏函数后续的代码。

    调用_dispatch_lane_drain_barrier_waiter执行栅栏函数中的任务:


    static void
    _dispatch_lane_drain_barrier_waiter(dispatch_lane_t dq,
    struct dispatch_object_s *dc, dispatch_wakeup_flags_t flags,
    uint64_t enqueued_bits)
    {
    ......
    return _dispatch_barrier_waiter_redirect_or_wake(dq, dc, flags,
    old_state, new_state);
    }

    直接调用_dispatch_barrier_waiter_redirect_or_wake

    static void
    _dispatch_barrier_waiter_redirect_or_wake(dispatch_queue_class_t dqu,
    dispatch_object_t dc, dispatch_wakeup_flags_t flags,
    uint64_t old_state, uint64_t new_state)
    {
    ......
    return _dispatch_waiter_wake(dsc, wlh, old_state, new_state);
    }

    调用_dispatch_waiter_wake

    static void
    _dispatch_waiter_wake(dispatch_sync_context_t dsc, dispatch_wlh_t wlh,
    uint64_t old_state, uint64_t new_state)
    {
    dispatch_wlh_t waiter_wlh = dsc->dc_data;

    if ((_dq_state_is_base_wlh(old_state) && !dsc->dsc_from_async) ||
    _dq_state_is_base_wlh(new_state) ||
    waiter_wlh != DISPATCH_WLH_ANON) {
    _dispatch_event_loop_wake_owner(dsc, wlh, old_state, new_state);
    }
    if (unlikely(waiter_wlh == DISPATCH_WLH_ANON)) {
    //走这里
    _dispatch_waiter_wake_wlh_anon(dsc);
    }
    }

    调用_dispatch_waiter_wake_wlh_anon:

    static void
    _dispatch_waiter_wake_wlh_anon(dispatch_sync_context_t dsc)
    {
    if (dsc->dsc_override_qos > dsc->dsc_override_qos_floor) {
    _dispatch_wqthread_override_start(dsc->dsc_waiter,
    dsc->dsc_override_qos);
    }
    //执行
    _dispatch_thread_event_signal(&dsc->dsc_event);
    }

    其中是对线程发送信号。

    对于_dispatch_root_queue_wakeup而言:

    void
    _dispatch_root_queue_wakeup(dispatch_queue_global_t dq,
    DISPATCH_UNUSED dispatch_qos_t qos, dispatch_wakeup_flags_t flags)
    {
    if (!(flags & DISPATCH_WAKEUP_BLOCK_WAIT)) {
    DISPATCH_INTERNAL_CRASH(dq->dq_priority,
    "Don't try to wake up or override a root queue");
    }
    if (flags & DISPATCH_WAKEUP_CONSUME_2) {
    return _dispatch_release_2_tailcall(dq);
    }
    }

    内部没有对barrier的处理,所以全局队列栅栏函数无效。

    因为全局队列不仅有你的任务,还有其它系统的任务。如果加barrier不仅影响你自己的任务还会影响系统的任务。对于全局队列而言栅栏函数就是个普通的异步函数。

    整个流程如下:




    1.2.2 dispatch_barrier_async

    dispatch_barrier_async源码如下:


    void
    dispatch_barrier_async(dispatch_queue_t dq, dispatch_block_t work)
    {
    dispatch_continuation_t dc = _dispatch_continuation_alloc();
    uintptr_t dc_flags = DC_FLAG_CONSUME | DC_FLAG_BARRIER;
    dispatch_qos_t qos;

    qos = _dispatch_continuation_init(dc, dq, work, 0, dc_flags);
    _dispatch_continuation_async(dq, dc, qos, dc_flags);
    }

    调用的是_dispatch_continuation_async

    static inline void
    _dispatch_continuation_async(dispatch_queue_class_t dqu,
    dispatch_continuation_t dc, dispatch_qos_t qos, uintptr_t dc_flags)
    {
    #if DISPATCH_INTROSPECTION
    if (!(dc_flags & DC_FLAG_NO_INTROSPECTION)) {
    _dispatch_trace_item_push(dqu, dc);
    }
    #else
    (void)dc_flags;
    #endif
    return dx_push(dqu._dq, dc, qos);
    }

    调用了dx_push,对应的自定义队列是_dispatch_lane_concurrent_push。全局队列是_dispatch_root_queue_push

    _dispatch_lane_concurrent_push:

    void
    _dispatch_lane_concurrent_push(dispatch_lane_t dq, dispatch_object_t dou,
    dispatch_qos_t qos)
    {
    if (dq->dq_items_tail == NULL &&
    !_dispatch_object_is_waiter(dou) &&
    !_dispatch_object_is_barrier(dou) &&
    _dispatch_queue_try_acquire_async(dq)) {
    return _dispatch_continuation_redirect_push(dq, dou, qos);
    }

    _dispatch_lane_push(dq, dou, qos);
    }

    断点跟踪走的是_dispatch_lane_push

    DISPATCH_NOINLINE
    void
    _dispatch_lane_push(dispatch_lane_t dq, dispatch_object_t dou,
    dispatch_qos_t qos)
    {
    dispatch_wakeup_flags_t flags = 0;
    struct dispatch_object_s *prev;

    if (unlikely(_dispatch_object_is_waiter(dou))) {
    return _dispatch_lane_push_waiter(dq, dou._dsc, qos);
    }

    dispatch_assert(!_dispatch_object_is_global(dq));
    qos = _dispatch_queue_push_qos(dq, qos);

    prev = os_mpsc_push_update_tail(os_mpsc(dq, dq_items), dou._do, do_next);
    if (unlikely(os_mpsc_push_was_empty(prev))) {
    _dispatch_retain_2_unsafe(dq);
    flags = DISPATCH_WAKEUP_CONSUME_2 | DISPATCH_WAKEUP_MAKE_DIRTY;
    } else if (unlikely(_dispatch_queue_need_override(dq, qos))) {
    _dispatch_retain_2_unsafe(dq);
    flags = DISPATCH_WAKEUP_CONSUME_2;
    }
    os_mpsc_push_update_prev(os_mpsc(dq, dq_items), prev, dou._do, do_next);
    if (flags) {
    //栅栏函数走这里。
    //#define dx_wakeup(x, y, z) dx_vtable(x)->dq_wakeup(x, y, z)
    //dx_wakeup 对应 dq_wakeup 自定义全局队列对应 _dispatch_lane_wakeup,全局队列对应 _dispatch_root_queue_wakeup
    return dx_wakeup(dq, qos, flags);
    }
    }

    栅栏函数走_dispatch_lane_wakeup逻辑:

    void
    _dispatch_lane_wakeup(dispatch_lane_class_t dqu, dispatch_qos_t qos,
    dispatch_wakeup_flags_t flags)
    {
    dispatch_queue_wakeup_target_t target = DISPATCH_QUEUE_WAKEUP_NONE;

    if (unlikely(flags & DISPATCH_WAKEUP_BARRIER_COMPLETE)) {
    return _dispatch_lane_barrier_complete(dqu, qos, flags);
    }
    if (_dispatch_queue_class_probe(dqu)) {
    target = DISPATCH_QUEUE_WAKEUP_TARGET;
    }
    //走这里
    return _dispatch_queue_wakeup(dqu, qos, flags, target);
    }

    继续断点走_dispatch_queue_wakeup逻辑:


    void
    _dispatch_queue_wakeup(dispatch_queue_class_t dqu, dispatch_qos_t qos,
    dispatch_wakeup_flags_t flags, dispatch_queue_wakeup_target_t target)
    {
    ......

    if (unlikely(flags & DISPATCH_WAKEUP_BARRIER_COMPLETE)) {
    ......
    //loop _dispatch_lane_wakeup //_dq_state_merge_qos
    return _dispatch_lane_class_barrier_complete(upcast(dq)._dl, qos,
    flags, target, DISPATCH_QUEUE_SERIAL_DRAIN_OWNED);
    }

    if (target) {
    ......
    #if HAVE_PTHREAD_WORKQUEUE_QOS
    } else if (qos) {
    ......
    if (likely((old_state ^ new_state) & enqueue)) {
    ...... //_dispatch_queue_push_queue断点断不住,断它内部断点
    return _dispatch_queue_push_queue(tq, dq, new_state);
    }
    #if HAVE_PTHREAD_WORKQUEUE_QOS
    if (unlikely((old_state ^ new_state) & DISPATCH_QUEUE_MAX_QOS_MASK)) {
    if (_dq_state_should_override(new_state)) {
    return _dispatch_queue_wakeup_with_override(dq, new_state,
    flags);
    }
    }
    #endif // HAVE_PTHREAD_WORKQUEUE_QOS
    done:
    if (likely(flags & DISPATCH_WAKEUP_CONSUME_2)) {
    return _dispatch_release_2_tailcall(dq);
    }
    }

    这里断点走了_dispatch_queue_push_queue逻辑(_dispatch_queue_push_queue本身断不住,断它内部断点):


    static inline void
    _dispatch_queue_push_queue(dispatch_queue_t tq, dispatch_queue_class_t dq,
    uint64_t dq_state)
    {
    #if DISPATCH_USE_KEVENT_WORKLOOP
    if (likely(_dq_state_is_base_wlh(dq_state))) {
    _dispatch_trace_runtime_event(worker_request, dq._dq, 1);
    return _dispatch_event_loop_poke((dispatch_wlh_t)dq._dq, dq_state,
    DISPATCH_EVENT_LOOP_CONSUME_2);
    }
    #endif // DISPATCH_USE_KEVENT_WORKLOOP
    _dispatch_trace_item_push(tq, dq);
    //_dispatch_lane_concurrent_push
    return dx_push(tq, dq, _dq_state_max_qos(dq_state));
    }

    内部走的是_dispatch_lane_concurrent_push逻辑,这里又继续走了_dispatch_lane_push的逻辑了,在这里就造成了循环等待。当队列中任务执行完毕后_dispatch_lane_wakeup中就走_dispatch_lane_barrier_complete逻辑了。

    可以通过barrier前面的任务加延迟去验证。直接断点_dispatch_lane_barrier_complete,当前面的任务执行完毕后就进入_dispatch_lane_barrier_complete断点了。

    _dispatch_lane_barrier_complete源码如下:

    static void
    _dispatch_lane_barrier_complete(dispatch_lane_class_t dqu, dispatch_qos_t qos,
    dispatch_wakeup_flags_t flags)
    {
    ......
    return _dispatch_lane_class_barrier_complete(dq, qos, flags, target, owned);
    }

    走了_dispatch_lane_class_barrier_complete逻辑:

    static void
    _dispatch_lane_class_barrier_complete(dispatch_lane_t dq, dispatch_qos_t qos,
    dispatch_wakeup_flags_t flags, dispatch_queue_wakeup_target_t target,
    uint64_t owned)
    {
    ......
    again:
    os_atomic_rmw_loop2o(dq, dq_state, old_state, new_state, release, {
    ......
    } else if (unlikely(_dq_state_is_dirty(old_state))) {
    ......
    flags |= DISPATCH_WAKEUP_BARRIER_COMPLETE;
    //自定义并行队列 _dispatch_lane_wakeup
    return dx_wakeup(dq, qos, flags);
    });
    } else {
    new_state &= ~DISPATCH_QUEUE_MAX_QOS_MASK;
    }
    });
    ......
    }

    调用走的是_dispatch_lane_wakeup

    void
    _dispatch_lane_wakeup(dispatch_lane_class_t dqu, dispatch_qos_t qos,
    dispatch_wakeup_flags_t flags)
    {
    dispatch_queue_wakeup_target_t target = DISPATCH_QUEUE_WAKEUP_NONE;

    if (unlikely(flags & DISPATCH_WAKEUP_BARRIER_COMPLETE)) {
    //barrier完成了就走这里的逻辑,barrier之前的任务执行完毕。
    return _dispatch_lane_barrier_complete(dqu, qos, flags);
    }
    if (_dispatch_queue_class_probe(dqu)) {
    target = DISPATCH_QUEUE_WAKEUP_TARGET;
    }
    //走这里
    return _dispatch_queue_wakeup(dqu, qos, flags, target);
    }

    这个时候就又走到了_dispatch_lane_barrier_complete

    DISPATCH_WAKEUP_BARRIER_COMPLETE状态是在_dispatch_lane_resume中进行变更的:

    _dispatch_root_queue_push内部并没有对barrier的处理,与全局队列逻辑一致。所以barrier函数传递全局队列无效。

    整个过程如下:




    作者:HotPotCat
    链接:https://www.jianshu.com/p/84153e072f44


    收起阅读 »

    高级线程应用之栅栏、信号量、调度组以及source(一)

    一、栅栏函数CPU的乱序执行能力让我们对多线程的安全保障的努力变得异常困难。因此要保证线程安全,阻止CPU换序是必需的。遗憾的是,现在并不存在可移植的阻止换序的方法。通常情况下是调用CPU提供的一条指令,这条指令常常被称为barrier。一条barrier指令...
    继续阅读 »

    一、栅栏函数

    CPU的乱序执行能力让我们对多线程的安全保障的努力变得异常困难。因此要保证线程安全,阻止CPU换序是必需的。遗憾的是,现在并不存在可移植的阻止换序的方法。通常情况下是调用CPU提供的一条指令,这条指令常常被称为barrier。一条barrier指令会阻止CPU将该指令之前的指令交换到barrier之后,反之亦然。换句话说,barrier指令的作用类似于一个拦水坝,阻止换序穿透这个大坝。

    栅栏函数最直接的作用:控制任务执行顺序,导致同步效果。
    有两个函数:

    • dispatch_barrier_async:前面的任务执行完毕才会执行barrier中的逻辑,以及barrier后加入队列的任务。
    • dispatch_barrier_sync:作用相同,但是会堵塞线程,影响后面的任务执行 。

    ⚠️:栅栏函数只能控制同一队列并发,相当于针对队列而言。

    1.1 应用

    1.1.1 dispatch_barrier_async 与 dispatch_barrier_sync 效果

    有如下案例:

    - (void)test {
    dispatch_queue_t concurrentQueue = dispatch_queue_create("HotpotCat", DISPATCH_QUEUE_CONCURRENT);
    dispatch_async(concurrentQueue, ^{
    sleep(3);
    NSLog(@"1");
    });
    dispatch_async(concurrentQueue, ^{
    NSLog(@"2");
    });
    dispatch_barrier_async(concurrentQueue, ^{
    NSLog(@"3:%@",[NSThread currentThread]);
    });
    dispatch_async(concurrentQueue, ^{
    NSLog(@"4");
    });
    NSLog(@"5");
    }

    分析:barrier阻塞的是自己以及concurrentQueue队列中在它后面加入的任务。由于这里使用的是异步函数所以任务125顺序不定,34之前。
    输出:

    GCDDemo[49708:5622304] 5
    GCDDemo[49708:5622437] 2
    GCDDemo[49708:5622434] 1
    GCDDemo[49708:5622434] 3:<NSThread: 0x600003439040>{number = 6, name = (null)}
    GCDDemo[49708:5622434] 4

    如果将dispatch_barrier_async改为dispatch_barrier_sync同步函数,则任务5会被阻塞。12(顺序不定)在3之前执行,45(顺序不定)在之后。

    1.1.2 栅栏函数存在的问题

    1.1.2.1 栅栏函数与全局队列

    concurrentQueue改为全局队列:

    dispatch_queue_t concurrentQueue = dispatch_get_global_queue(0, 0);
    dispatch_async(concurrentQueue, ^{
    NSLog(@"1");
    });
    dispatch_async(concurrentQueue, ^{
    NSLog(@"2");
    });
    dispatch_barrier_async(concurrentQueue, ^{
    NSLog(@"3:%@",[NSThread currentThread]);
    });
    dispatch_async(concurrentQueue, ^{
    NSLog(@"4");
    });
    NSLog(@"5");

    输出:

    GCDDemo[49872:5632760] 5
    GCDDemo[49872:5632979] 1
    GCDDemo[49872:5633673] 2
    GCDDemo[49872:5633675] 4
    GCDDemo[49872:5633674] 3:<NSThread: 0x600001160240>{number = 10, name = (null)}

    这个时候栅栏函数无论同步还是异步都无效了(有可能系统调度刚好符合预期)。
    这也就意味着全局并发队列不允许使用栅栏函数,一定是自定义队列才能使用。

    1.1.2.1 栅栏函数与不同队列

    将任务24放入另外一个队列:

    dispatch_queue_t concurrentQueue = dispatch_queue_create("Hotpot", DISPATCH_QUEUE_CONCURRENT);
    dispatch_queue_t concurrentQueue2 = dispatch_queue_create("Cat", DISPATCH_QUEUE_CONCURRENT);
    dispatch_async(concurrentQueue, ^{
    sleep(3);
    NSLog(@"1");
    });
    dispatch_async(concurrentQueue2, ^{
    NSLog(@"2");
    });
    dispatch_barrier_async(concurrentQueue, ^{
    NSLog(@"3:%@",[NSThread currentThread]);
    });
    dispatch_async(concurrentQueue2, ^{
    NSLog(@"4");
    });
    NSLog(@"5");

    输出:

    GCDDemo[49981:5639766] 5
    GCDDemo[49981:5640003] 2
    GCDDemo[49981:5639998] 4
    GCDDemo[49981:5639997] 1
    GCDDemo[49981:5639998] 3:<NSThread: 0x600003761500>{number = 5, name = (null)}

    这个时候concurrentQueue2中的任务先执行了,它并不受栅栏函数的影响。那么说明 栅栏函数只对同一个队列中的任务起作用

    1.1.3 栅栏函数作为锁使用

    有如下代码:

    NSMutableArray *array = [NSMutableArray array];
    dispatch_queue_t concurrentQueue = dispatch_queue_create("Hotpot", DISPATCH_QUEUE_CONCURRENT);
    for (int i = 0; i < 1000; i++) {
    dispatch_async(concurrentQueue, ^{
    [array addObject:@(i)];
    });
    }
    • 多个线程同时操作array
    • addObject的时候有可能存在同一时间对同一块内存空间写入数据。
      比如写第3个数据的时候,当前数组中数据是(1、2)这个时候有2个线程同时写入数据就存在了(1、2、3)(1、2、4)`这个时候数据就发生了混乱造成了错误。

    在运行的时候由于线程不安全(可变数组线程不安全),发生了写入错误直接报错:




    将数组添加元素的操作放入dispatch_barrier_async中:

    NSMutableArray *array = [NSMutableArray array];
    dispatch_queue_t concurrentQueue = dispatch_queue_create("Hotpot", DISPATCH_QUEUE_CONCURRENT);
    for (int i = 0; i < 1000; i++) {
    dispatch_async(concurrentQueue, ^{
    dispatch_barrier_async(concurrentQueue , ^{
    [array addObject:@(i)];
    });
    });
    }

    这样就没问题了,加入栅栏函数写入数据的时候相当于加了锁。

    1.2 原理分析

    根据1.1中的案例有3个问题:

    • 1.为什么栅栏函数能起作用?
    • 2.为什么全局队列无效?
    • 3.为什么任务必须在同一队列才有效?

    1.2.1 dispatch_barrier_sync

    dispatch_barrier_sync源码如下:

    void
    dispatch_barrier_sync(dispatch_queue_t dq, dispatch_block_t work)
    {
    uintptr_t dc_flags = DC_FLAG_BARRIER | DC_FLAG_BLOCK;
    if (unlikely(_dispatch_block_has_private_data(work))) {
    return _dispatch_sync_block_with_privdata(dq, work, dc_flags);
    }
    _dispatch_barrier_sync_f(dq, work, _dispatch_Block_invoke(work), dc_flags);
    }

    直接调用_dispatch_barrier_sync_f

    static void
    _dispatch_barrier_sync_f(dispatch_queue_t dq, void *ctxt,
    dispatch_function_t func, uintptr_t dc_flags)
    {
    _dispatch_barrier_sync_f_inline(dq, ctxt, func, dc_flags);
    }

    仍然是对_dispatch_barrier_sync_f_inline的调用:

    static inline void
    _dispatch_barrier_sync_f_inline(dispatch_queue_t dq, void *ctxt,
    dispatch_function_t func, uintptr_t dc_flags)
    {
    dispatch_tid tid = _dispatch_tid_self();

    if (unlikely(dx_metatype(dq) != _DISPATCH_LANE_TYPE)) {
    DISPATCH_CLIENT_CRASH(0, "Queue type doesn't support dispatch_sync");
    }

    dispatch_lane_t dl = upcast(dq)._dl;

    if (unlikely(!_dispatch_queue_try_acquire_barrier_sync(dl, tid))) {
    //死锁走这里的逻辑,同步栅栏函数也走这里
    return _dispatch_sync_f_slow(dl, ctxt, func, DC_FLAG_BARRIER, dl,
    DC_FLAG_BARRIER | dc_flags);
    }

    if (unlikely(dl->do_targetq->do_targetq)) {
    return _dispatch_sync_recurse(dl, ctxt, func,
    DC_FLAG_BARRIER | dc_flags);
    }
    _dispatch_introspection_sync_begin(dl);
    _dispatch_lane_barrier_sync_invoke_and_complete(dl, ctxt, func
    DISPATCH_TRACE_ARG(_dispatch_trace_item_sync_push_pop(
    dq, ctxt, func, dc_flags | DC_FLAG_BARRIER)));
    }

    栅栏函数这个时候走的也是_dispatch_sync_f_slow逻辑:

    static void
    _dispatch_sync_f_slow(dispatch_queue_class_t top_dqu, void *ctxt,
    dispatch_function_t func, uintptr_t top_dc_flags,
    dispatch_queue_class_t dqu, uintptr_t dc_flags)
    {
    dispatch_queue_t top_dq = top_dqu._dq;
    dispatch_queue_t dq = dqu._dq;
    if (unlikely(!dq->do_targetq)) {
    return _dispatch_sync_function_invoke(dq, ctxt, func);
    }
    ......
    _dispatch_trace_item_push(top_dq, &dsc);
    //死锁报错
    __DISPATCH_WAIT_FOR_QUEUE__(&dsc, dq);

    if (dsc.dsc_func == NULL) {
    // dsc_func being cleared means that the block ran on another thread ie.
    // case (2) as listed in _dispatch_async_and_wait_f_slow.
    dispatch_queue_t stop_dq = dsc.dc_other;
    return _dispatch_sync_complete_recurse(top_dq, stop_dq, top_dc_flags);
    }

    _dispatch_introspection_sync_begin(top_dq);
    _dispatch_trace_item_pop(top_dq, &dsc);
    _dispatch_sync_invoke_and_complete_recurse(top_dq, ctxt, func,top_dc_flags
    DISPATCH_TRACE_ARG(&dsc));
    }

    断点调试走的是_dispatch_sync_complete_recurse

    static void
    _dispatch_sync_complete_recurse(dispatch_queue_t dq, dispatch_queue_t stop_dq,
    uintptr_t dc_flags)
    {
    bool barrier = (dc_flags & DC_FLAG_BARRIER);
    do {
    if (dq == stop_dq) return;
    if (barrier) {
    //唤醒执行
    //_dispatch_lane_wakeup
    dx_wakeup(dq, 0, DISPATCH_WAKEUP_BARRIER_COMPLETE);
    } else {
    //已经执行完成没有栅栏函数
    _dispatch_lane_non_barrier_complete(upcast(dq)._dl, 0);
    }
    dq = dq->do_targetq;
    barrier = (dq->dq_width == 1);
    } while (unlikely(dq->do_targetq));
    }
    • 这里进行了递归调用,循环条件是dq->do_targetq也就是 仅对当前队列有效
    • 唤醒执行栅栏前任务执行_dispatch_lane_wakeup逻辑。
    • 当栅栏前的任务执行完毕走_dispatch_lane_non_barrier_complete逻辑。这也就是为什么栅栏起作用的原因。

    dx_wakeup在全局队列是_dispatch_root_queue_wakeup,在自定义并行队列是_dispatch_lane_wakeup

    1.2.1.1 _dispatch_lane_wakeup

    void
    _dispatch_lane_wakeup(dispatch_lane_class_t dqu, dispatch_qos_t qos,
    dispatch_wakeup_flags_t flags)
    {
    dispatch_queue_wakeup_target_t target = DISPATCH_QUEUE_WAKEUP_NONE;

    if (unlikely(flags & DISPATCH_WAKEUP_BARRIER_COMPLETE)) {
    //barrier完成了就走这里的逻辑,barrier之前的任务执行完毕。
    return _dispatch_lane_barrier_complete(dqu, qos, flags);
    }
    if (_dispatch_queue_class_probe(dqu)) {
    target = DISPATCH_QUEUE_WAKEUP_TARGET;
    }
    //走这里
    return _dispatch_queue_wakeup(dqu, qos, flags, target);
    }
    • 在栅栏函数执行完毕后才走_dispatch_lane_barrier_complete_dispatch_lane_non_barrier_complete中的逻辑就汇合了。
    • 没有执行完毕的时候执行_dispatch_queue_wakeup

    _dispatch_queue_wakeup源码如下:

    void
    _dispatch_queue_wakeup(dispatch_queue_class_t dqu, dispatch_qos_t qos,
    dispatch_wakeup_flags_t flags, dispatch_queue_wakeup_target_t target)
    {
    ......

    if (likely((old_state ^ new_state) & enqueue)) {
    ......
    //_dispatch_queue_push_queue 断点断不住,走这里。
    return _dispatch_queue_push_queue(tq, dq, new_state);
    }
    ......
    }

    最终走的是_dispatch_queue_push_queue逻辑:

    static inline void
    _dispatch_queue_push_queue(dispatch_queue_t tq, dispatch_queue_class_t dq,
    uint64_t dq_state)
    {
    #if DISPATCH_USE_KEVENT_WORKLOOP
    if (likely(_dq_state_is_base_wlh(dq_state))) {
    _dispatch_trace_runtime_event(worker_request, dq._dq, 1);
    return _dispatch_event_loop_poke((dispatch_wlh_t)dq._dq, dq_state,
    DISPATCH_EVENT_LOOP_CONSUME_2);
    }
    #endif // DISPATCH_USE_KEVENT_WORKLOOP
    _dispatch_trace_item_push(tq, dq);
    //_dispatch_lane_concurrent_push
    return dx_push(tq, dq, _dq_state_max_qos(dq_state));
    }

    内部是对_dispatch_lane_concurrent_push的调用:

    void
    _dispatch_lane_concurrent_push(dispatch_lane_t dq, dispatch_object_t dou,
    dispatch_qos_t qos)
    {
    if (dq->dq_items_tail == NULL &&
    !_dispatch_object_is_waiter(dou) &&
    !_dispatch_object_is_barrier(dou) &&
    _dispatch_queue_try_acquire_async(dq)) {
    return _dispatch_continuation_redirect_push(dq, dou, qos);
    }

    _dispatch_lane_push(dq, dou, qos);
    }

    这里直接调用_dispatch_lane_push

    void
    _dispatch_lane_push(dispatch_lane_t dq, dispatch_object_t dou,
    dispatch_qos_t qos)
    {
    ......
    if (flags) {
    //栅栏函数走这里。
    //#define dx_wakeup(x, y, z) dx_vtable(x)->dq_wakeup(x, y, z)
    //dx_wakeup 对应 dq_wakeup 自定义全局队列对应 _dispatch_lane_wakeup,全局队列对应 _dispatch_root_queue_wakeup
    return dx_wakeup(dq, qos, flags);
    }
    }

    又调用回了_dispatch_lane_wakeup,相当于一直扫描。


    收起阅读 »

    iOS 基于定时器的动画 一

    基于定时器的动画我可以指导你,但是你必须按照我说的做。 -- 骇客帝国    在第10章“缓冲”中,我们研究了CAMediaTimingFunction,它是一个通过控制动画缓冲来模拟物理效果例如加速或者减速来...
    继续阅读 »

    基于定时器的动画

    我可以指导你,但是你必须按照我说的做。 -- 骇客帝国

        在第10章“缓冲”中,我们研究了CAMediaTimingFunction,它是一个通过控制动画缓冲来模拟物理效果例如加速或者减速来增强现实感的东西,那么如果想更加真实地模拟物理交互或者实时根据用户输入修改动画改怎么办呢?在这一章中,我们将继续探索一种能够允许我们精确地控制一帧一帧展示的基于定时器的动画。

    11.1 定时帧

    动画看起来是用来显示一段连续的运动过程,但实际上当在固定位置上展示像素的时候并不能做到这一点。一般来说这种显示都无法做到连续的移动,能做的仅仅是足够快地展示一系列静态图片,只是看起来像是做了运动。

    我们之前提到过iOS按照每秒60次刷新屏幕,然后CAAnimation计算出需要展示的新的帧,然后在每次屏幕更新的时候同步绘制上去,CAAnimation最机智的地方在于每次刷新需要展示的时候去计算插值和缓冲。

    在第10章中,我们解决了如何自定义缓冲函数,然后根据需要展示的帧的数组来告诉CAKeyframeAnimation的实例如何去绘制。所有的Core Animation实际上都是按照一定的序列来显示这些帧,那么我们可以自己做到这些么?

    NSTimer

    实际上,我们在第三章“图层几何学”中已经做过类似的东西,就是时钟那个例子,我们用了NSTimer来对钟表的指针做定时动画,一秒钟更新一次,但是如果我们把频率调整成一秒钟更新60次的话,原理是完全相同的。

    我们来试着用NSTimer来修改第十章中弹性球的例子。由于现在我们在定时器启动之后连续计算动画帧,我们需要在类中添加一些额外的属性来存储动画的fromValuetoValueduration和当前的timeOffset(见清单11.1)。

    清单11.1 使用NSTimer实现弹性球动画

    @interface ViewController ()

    @property (nonatomic, weak) IBOutlet UIView *containerView;
    @property (nonatomic, strong) UIImageView *ballView;
    @property (nonatomic, strong) NSTimer *timer;
    @property (nonatomic, assign) NSTimeInterval duration;
    @property (nonatomic, assign) NSTimeInterval timeOffset;
    @property (nonatomic, strong) id fromValue;
    @property (nonatomic, strong) id toValue;

    @end

    @implementation ViewController

    - (void)viewDidLoad
    {
    [super viewDidLoad];
    //add ball image view
    UIImage *ballImage = [UIImage imageNamed:@"Ball.png"];
    self.ballView = [[UIImageView alloc] initWithImage:ballImage];
    [self.containerView addSubview:self.ballView];
    //animate
    [self animate];
    }

    - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
    {
    //replay animation on tap
    [self animate];
    }

    float interpolate(float from, float to, float time)
    {
    return (to - from) * time + from;
    }

    - (id)interpolateFromValue:(id)fromValue toValue:(id)toValue time:(float)time
    {
    if ([fromValue isKindOfClass:[NSValue class]]) {
    //get type
    const char *type = [(NSValue *)fromValue objCType];
    if (strcmp(type, @encode(CGPoint)) == 0) {
    CGPoint from = [fromValue CGPointValue];
    CGPoint to = [toValue CGPointValue];
    CGPoint result = CGPointMake(interpolate(from.x, to.x, time), interpolate(from.y, to.y, time));
    return [NSValue valueWithCGPoint:result];
    }
    }
    //provide safe default implementation
    return (time < 0.5)? fromValue: toValue;
    }

    float bounceEaseOut(float t)
    {
    if (t < 4/11.0) {
    return (121 * t * t)/16.0;
    } else if (t < 8/11.0) {
    return (363/40.0 * t * t) - (99/10.0 * t) + 17/5.0;
    } else if (t < 9/10.0) {
    return (4356/361.0 * t * t) - (35442/1805.0 * t) + 16061/1805.0;
    }
    return (54/5.0 * t * t) - (513/25.0 * t) + 268/25.0;
    }

    - (void)animate
    {
    //reset ball to top of screen
    self.ballView.center = CGPointMake(150, 32);
    //configure the animation
    self.duration = 1.0;
    self.timeOffset = 0.0;
    self.fromValue = [NSValue valueWithCGPoint:CGPointMake(150, 32)];
    self.toValue = [NSValue valueWithCGPoint:CGPointMake(150, 268)];
    //stop the timer if it's already running
    [self.timer invalidate];
    //start the timer
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1/60.0
    target:self
    selector:@selector(step:)
    userInfo:nil
    repeats:YES];
    }

    - (void)step:(NSTimer *)step
    {
    //update time offset
    self.timeOffset = MIN(self.timeOffset + 1/60.0, self.duration);
    //get normalized time offset (in range 0 - 1)
    float time = self.timeOffset / self.duration;
    //apply easing
    time = bounceEaseOut(time);
    //interpolate position
    id position = [self interpolateFromValue:self.fromValue
    toValue:self.toValue
    time:time];
    //move ball view to new position
    self.ballView.center = [position CGPointValue];
    //stop the timer if we've reached the end of the animation
    if (self.timeOffset >= self.duration) {
    [self.timer invalidate];
    self.timer = nil;
    }
    }

    @end

    很赞,而且和基于关键帧例子的代码一样很多,但是如果想一次性在屏幕上对很多东西做动画,很明显就会有很多问题。

    NSTimer并不是最佳方案,为了理解这点,我们需要确切地知道NSTimer是如何工作的。iOS上的每个线程都管理了一个NSRunloop,字面上看就是通过一个循环来完成一些任务列表。但是对主线程,这些任务包含如下几项:

    • 处理触摸事件
    • 发送和接受网络数据包
    • 执行使用gcd的代码
    • 处理计时器行为
    • 屏幕重绘

    当你设置一个NSTimer,他会被插入到当前任务列表中,然后直到指定时间过去之后才会被执行。但是何时启动定时器并没有一个时间上限,而且它只会在列表中上一个任务完成之后开始执行。这通常会导致有几毫秒的延迟,但是如果上一个任务过了很久才完成就会导致延迟很长一段时间。

    屏幕重绘的频率是一秒钟六十次,但是和定时器行为一样,如果列表中上一个执行了很长时间,它也会延迟。这些延迟都是一个随机值,于是就不能保证定时器精准地一秒钟执行六十次。有时候发生在屏幕重绘之后,这就会使得更新屏幕会有个延迟,看起来就是动画卡壳了。有时候定时器会在屏幕更新的时候执行两次,于是动画看起来就跳动了。

    我们可以通过一些途径来优化:

    • 我们可以用CADisplayLink让更新频率严格控制在每次屏幕刷新之后。
    • 基于真实帧的持续时间而不是假设的更新频率来做动画。
    • 调整动画计时器的run loop模式,这样就不会被别的事件干扰。

    CADisplayLink是CoreAnimation提供的另一个类似于NSTimer的类,它总是在屏幕完成一次更新之前启动,它的接口设计的和NSTimer很类似,所以它实际上就是一个内置实现的替代,但是和timeInterval以秒为单位不同,CADisplayLink有一个整型的frameInterval属性,指定了间隔多少帧之后才执行。默认值是1,意味着每次屏幕更新之前都会执行一次。但是如果动画的代码执行起来超过了六十分之一秒,你可以指定frameInterval为2,就是说动画每隔一帧执行一次(一秒钟30帧)或者3,也就是一秒钟20次,等等。

    CADisplayLink而不是NSTimer,会保证帧率足够连续,使得动画看起来更加平滑,但即使CADisplayLink也不能保证每一帧都按计划执行,一些失去控制的离散的任务或者事件(例如资源紧张的后台程序)可能会导致动画偶尔地丢帧。当使用NSTimer的时候,一旦有机会计时器就会开启,但是CADisplayLink却不一样:如果它丢失了帧,就会直接忽略它们,然后在下一次更新的时候接着运行。

    计算帧的持续时间

    无论是使用NSTimer还是CADisplayLink,我们仍然需要处理一帧的时间超出了预期的六十分之一秒。由于我们不能够计算出一帧真实的持续时间,所以需要手动测量。我们可以在每帧开始刷新的时候用CACurrentMediaTime()记录当前时间,然后和上一帧记录的时间去比较。

    通过比较这些时间,我们就可以得到真实的每帧持续的时间,然后代替硬编码的六十分之一秒。我们来更新一下上个例子(见清单11.2)。

    清单11.2 通过测量没帧持续的时间来使得动画更加平滑

    @interface ViewController ()

    @property (nonatomic, weak) IBOutlet UIView *containerView;
    @property (nonatomic, strong) UIImageView *ballView;
    @property (nonatomic, strong) CADisplayLink *timer;
    @property (nonatomic, assign) CFTimeInterval duration;
    @property (nonatomic, assign) CFTimeInterval timeOffset;
    @property (nonatomic, assign) CFTimeInterval lastStep;
    @property (nonatomic, strong) id fromValue;
    @property (nonatomic, strong) id toValue;

    @end

    @implementation ViewController

    ...

    - (void)animate
    {
    //reset ball to top of screen
    self.ballView.center = CGPointMake(150, 32);
    //configure the animation
    self.duration = 1.0;
    self.timeOffset = 0.0;
    self.fromValue = [NSValue valueWithCGPoint:CGPointMake(150, 32)];
    self.toValue = [NSValue valueWithCGPoint:CGPointMake(150, 268)];
    //stop the timer if it's already running
    [self.timer invalidate];
    //start the timer
    self.lastStep = CACurrentMediaTime();
    self.timer = [CADisplayLink displayLinkWithTarget:self
    selector:@selector(step:)];
    [self.timer addToRunLoop:[NSRunLoop mainRunLoop]
    forMode:NSDefaultRunLoopMode];
    }

    - (void)step:(CADisplayLink *)timer
    {
    //calculate time delta
    CFTimeInterval thisStep = CACurrentMediaTime();
    CFTimeInterval stepDuration = thisStep - self.lastStep;
    self.lastStep = thisStep;
    //update time offset
    self.timeOffset = MIN(self.timeOffset + stepDuration, self.duration);
    //get normalized time offset (in range 0 - 1)
    float time = self.timeOffset / self.duration;
    //apply easing
    time = bounceEaseOut(time);
    //interpolate position
    id position = [self interpolateFromValue:self.fromValue toValue:self.toValue
    time:time];
    //move ball view to new position
    self.ballView.center = [position CGPointValue];
    //stop the timer if we've reached the end of the animation
    if (self.timeOffset >= self.duration) {
    [self.timer invalidate];
    self.timer = nil;
    }
    }

    @end

    Run Loop 模式

    注意到当创建CADisplayLink的时候,我们需要指定一个run looprun loop mode,对于run loop来说,我们就使用了主线程的run loop,因为任何用户界面的更新都需要在主线程执行,但是模式的选择就并不那么清楚了,每个添加到run loop的任务都有一个指定了优先级的模式,为了保证用户界面保持平滑,iOS会提供和用户界面相关任务的优先级,而且当UI很活跃的时候的确会暂停一些别的任务。

    一个典型的例子就是当是用UIScrollview滑动的时候,重绘滚动视图的内容会比别的任务优先级更高,所以标准的NSTimer和网络请求就不会启动,一些常见的run loop模式如下:

    • NSDefaultRunLoopMode - 标准优先级
    • NSRunLoopCommonModes - 高优先级
    • UITrackingRunLoopMode - 用于UIScrollView和别的控件的动画

    在我们的例子中,我们是用了NSDefaultRunLoopMode,但是不能保证动画平滑的运行,所以就可以用NSRunLoopCommonModes来替代。但是要小心,因为如果动画在一个高帧率情况下运行,你会发现一些别的类似于定时器的任务或者类似于滑动的其他iOS动画会暂停,直到动画结束。

    同样可以同时对CADisplayLink指定多个run loop模式,于是我们可以同时加入NSDefaultRunLoopModeUITrackingRunLoopMode来保证它不会被滑动打断,也不会被其他UIKit控件动画影响性能,像这样:

    self.timer = [CADisplayLink displayLinkWithTarget:self selector:@selector(step:)];
    [self.timer addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
    [self.timer addToRunLoop:[NSRunLoop mainRunLoop] forMode:UITrackingRunLoopMode];

    CADisplayLink类似,NSTimer同样也可以使用不同的run loop模式配置,通过别的函数,而不是+scheduledTimerWithTimeInterval:构造器

    self.timer = [NSTimer timerWithTimeInterval:1/60.0
    target:self
    selector:@selector(step:)
    userInfo:nil
    repeats:YES];
    [[NSRunLoop mainRunLoop] addTimer:self.timer
    forMode:NSRunLoopCommonModes];
    收起阅读 »

    iOS 缓冲 二

    10.2 自定义缓冲函数在第八章中,我们给时钟项目添加了动画。看起来很赞,但是如果有合适的缓冲函数就更好了。在显示世界中,钟表指针转动的时候,通常起步很慢,然后迅速啪地一声,最后缓冲到终点。但是标准的缓冲函数在这里每一个适合它,那该如何创建一个新的呢?除了+f...
    继续阅读 »

    10.2 自定义缓冲函数

    在第八章中,我们给时钟项目添加了动画。看起来很赞,但是如果有合适的缓冲函数就更好了。在显示世界中,钟表指针转动的时候,通常起步很慢,然后迅速啪地一声,最后缓冲到终点。但是标准的缓冲函数在这里每一个适合它,那该如何创建一个新的呢?

    除了+functionWithName:之外,CAMediaTimingFunction同样有另一个构造函数,一个有四个浮点参数的+functionWithControlPoints::::(注意这里奇怪的语法,并没有包含具体每个参数的名称,这在objective-C中是合法的,但是却违反了苹果对方法命名的指导方针,而且看起来是一个奇怪的设计)。

    使用这个方法,我们可以创建一个自定义的缓冲函数,来匹配我们的时钟动画,为了理解如何使用这个方法,我们要了解一些CAMediaTimingFunction是如何工作的。

    三次贝塞尔曲线

    CAMediaTimingFunction函数的主要原则在于它把输入的时间转换成起点和终点之间成比例的改变。我们可以用一个简单的图标来解释,横轴代表时间,纵轴代表改变的量,于是线性的缓冲就是一条从起点开始的简单的斜线(图10.1)。

    图10.1

    图10.1 线性缓冲函数的图像

    这条曲线的斜率代表了速度,斜率的改变代表了加速度,原则上来说,任何加速的曲线都可以用这种图像来表示,但是CAMediaTimingFunction使用了一个叫做三次贝塞尔曲线的函数,它只可以产出指定缓冲函数的子集(我们之前在第八章中创建CAKeyframeAnimation路径的时候提到过三次贝塞尔曲线)。

    你或许会回想起,一个三次贝塞尔曲线通过四个点来定义,第一个和最后一个点代表了曲线的起点和终点,剩下中间两个点叫做控制点,因为它们控制了曲线的形状,贝塞尔曲线的控制点其实是位于曲线之外的点,也就是说曲线并不一定要穿过它们。你可以把它们想象成吸引经过它们曲线的磁铁。

    图10.2展示了一个三次贝塞尔缓冲函数的例子

    图10.2

    图10.2 三次贝塞尔缓冲函数

    实际上它是一个很奇怪的函数,先加速,然后减速,最后快到达终点的时候又加速,那么标准的缓冲函数又该如何用图像来表示呢?

    CAMediaTimingFunction有一个叫做-getControlPointAtIndex:values:的方法,可以用来检索曲线的点,这个方法的设计的确有点奇怪(或许也就只有苹果能回答为什么不简单返回一个CGPoint),但是使用它我们可以找到标准缓冲函数的点,然后用UIBezierPathCAShapeLayer来把它画出来。

    曲线的起始和终点始终是{0, 0}和{1, 1},于是我们只需要检索曲线的第二个和第三个点(控制点)。具体代码见清单10.4。所有的标准缓冲函数的图像见图10.3。

    清单10.4 使用UIBezierPath绘制CAMediaTimingFunction

    @interface ViewController ()

    @property (nonatomic, weak) IBOutlet UIView *layerView;

    @end

    @implementation ViewController

    - (void)viewDidLoad
    {
    [super viewDidLoad];
    //create timing function
    CAMediaTimingFunction *function = [CAMediaTimingFunction functionWithName: kCAMediaTimingFunctionEaseOut];
    //get control points
    CGPoint controlPoint1, controlPoint2;
    [function getControlPointAtIndex:1 values:(float *)&controlPoint1];
    [function getControlPointAtIndex:2 values:(float *)&controlPoint2];
    //create curve
    UIBezierPath *path = [[UIBezierPath alloc] init];
    [path moveToPoint:CGPointZero];
    [path addCurveToPoint:CGPointMake(1, 1)
    controlPoint1:controlPoint1 controlPoint2:controlPoint2];
    //scale the path up to a reasonable size for display
    [path applyTransform:CGAffineTransformMakeScale(200, 200)];
    //create shape layer
    CAShapeLayer *shapeLayer = [CAShapeLayer layer];
    shapeLayer.strokeColor = [UIColor redColor].CGColor;
    shapeLayer.fillColor = [UIColor clearColor].CGColor;
    shapeLayer.lineWidth = 4.0f;
    shapeLayer.path = path.CGPath;
    [self.layerView.layer addSublayer:shapeLayer];
    //flip geometry so that 0,0 is in the bottom-left
    self.layerView.layer.geometryFlipped = YES;
    }

    @end

    图10.3

    图10.3 标准CAMediaTimingFunction缓冲曲线

    那么对于我们自定义时钟指针的缓冲函数来说,我们需要初始微弱,然后迅速上升,最后缓冲到终点的曲线,通过一些实验之后,最终结果如下:

    [CAMediaTimingFunction functionWithControlPoints:1 :0 :0.75 :1];

    如果把它转换成缓冲函数的图像,最后如图10.4所示,如果把它添加到时钟的程序,就形成了之前一直期待的非常赞的效果(见代清单10.5)。

    图10.4

    图10.4 自定义适合时钟的缓冲函数

    清单10.5 添加了自定义缓冲函数的时钟程序

    - (void)setAngle:(CGFloat)angle forHand:(UIView *)handView animated:(BOOL)animated
    {
    //generate transform
    CATransform3D transform = CATransform3DMakeRotation(angle, 0, 0, 1);
    if (animated) {
    //create transform animation
    CABasicAnimation *animation = [CABasicAnimation animation];
    animation.keyPath = @"transform";
    animation.fromValue = [handView.layer.presentationLayer valueForKey:@"transform"];
    animation.toValue = [NSValue valueWithCATransform3D:transform];
    animation.duration = 0.5;
    animation.delegate = self;
    animation.timingFunction = [CAMediaTimingFunction functionWithControlPoints:1 :0 :0.75 :1];
    //apply animation
    handView.layer.transform = transform;
    [handView.layer addAnimation:animation forKey:nil];
    } else {
    //set transform directly
    handView.layer.transform = transform;
    }
    }

    更加复杂的动画曲线

    考虑一个橡胶球掉落到坚硬的地面的场景,当开始下落的时候,它会持续加速知道落到地面,然后经过几次反弹,最后停下来。如果用一张图来说明,它会如图10.5所示。

    图10.5

    图10.5 一个没法用三次贝塞尔曲线描述的反弹的动画

    这种效果没法用一个简单的三次贝塞尔曲线表示,于是不能用CAMediaTimingFunction来完成。但如果想要实现这样的效果,可以用如下几种方法:

    • CAKeyframeAnimation创建一个动画,然后分割成几个步骤,每个小步骤使用自己的计时函数(具体下节介绍)。
    • 使用定时器逐帧更新实现动画(见第11章,“基于定时器的动画”)。

    基于关键帧的缓冲

    为了使用关键帧实现反弹动画,我们需要在缓冲曲线中对每一个显著的点创建一个关键帧(在这个情况下,关键点也就是每次反弹的峰值),然后应用缓冲函数把每段曲线连接起来。同时,我们也需要通过keyTimes来指定每个关键帧的时间偏移,由于每次反弹的时间都会减少,于是关键帧并不会均匀分布。

    清单10.6展示了实现反弹球动画的代码(见图10.6)

    清单10.6 使用关键帧实现反弹球的动画

    @interface ViewController ()

    @property (nonatomic, weak) IBOutlet UIView *containerView;
    @property (nonatomic, strong) UIImageView *ballView;

    @end

    @implementation ViewController

    - (void)viewDidLoad
    {
    [super viewDidLoad];
    //add ball image view
    UIImage *ballImage = [UIImage imageNamed:@"Ball.png"];
    self.ballView = [[UIImageView alloc] initWithImage:ballImage];
    [self.containerView addSubview:self.ballView];
    //animate
    [self animate];
    }

    - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
    {
    //replay animation on tap
    [self animate];
    }

    - (void)animate
    {
    //reset ball to top of screen
    self.ballView.center = CGPointMake(150, 32);
    //create keyframe animation
    CAKeyframeAnimation *animation = [CAKeyframeAnimation animation];
    animation.keyPath = @"position";
    animation.duration = 1.0;
    animation.delegate = self;
    animation.values = @[
    [NSValue valueWithCGPoint:CGPointMake(150, 32)],
    [NSValue valueWithCGPoint:CGPointMake(150, 268)],
    [NSValue valueWithCGPoint:CGPointMake(150, 140)],
    [NSValue valueWithCGPoint:CGPointMake(150, 268)],
    [NSValue valueWithCGPoint:CGPointMake(150, 220)],
    [NSValue valueWithCGPoint:CGPointMake(150, 268)],
    [NSValue valueWithCGPoint:CGPointMake(150, 250)],
    [NSValue valueWithCGPoint:CGPointMake(150, 268)]
    ];

    animation.timingFunctions = @[
    [CAMediaTimingFunction functionWithName: kCAMediaTimingFunctionEaseIn],
    [CAMediaTimingFunction functionWithName: kCAMediaTimingFunctionEaseOut],
    [CAMediaTimingFunction functionWithName: kCAMediaTimingFunctionEaseIn],
    [CAMediaTimingFunction functionWithName: kCAMediaTimingFunctionEaseOut],
    [CAMediaTimingFunction functionWithName: kCAMediaTimingFunctionEaseIn],
    [CAMediaTimingFunction functionWithName: kCAMediaTimingFunctionEaseOut],
    [CAMediaTimingFunction functionWithName: kCAMediaTimingFunctionEaseIn]
    ];

    animation.keyTimes = @[@0.0, @0.3, @0.5, @0.7, @0.8, @0.9, @0.95, @1.0];
    //apply animation
    self.ballView.layer.position = CGPointMake(150, 268);
    [self.ballView.layer addAnimation:animation forKey:nil];
    }

    @end

    图10.6

    图10.6 使用关键帧实现的反弹球动画

    这种方式还算不错,但是实现起来略显笨重(因为要不停地尝试计算各种关键帧和时间偏移)并且和动画强绑定了(因为如果要改变动画的一个属性,那就意味着要重新计算所有的关键帧)。那该如何写一个方法,用缓冲函数来把任何简单的属性动画转换成关键帧动画呢,下面我们来实现它。

    流程自动化

    在清单10.6中,我们把动画分割成相当大的几块,然后用Core Animation的缓冲进入和缓冲退出函数来大约形成我们想要的曲线。但如果我们把动画分割成更小的几部分,那么我们就可以用直线来拼接这些曲线(也就是线性缓冲)。为了实现自动化,我们需要知道如何做如下两件事情:

    • 自动把任意属性动画分割成多个关键帧
    • 用一个数学函数表示弹性动画,使得可以对帧做便宜

    为了解决第一个问题,我们需要复制Core Animation的插值机制。这是一个传入起点和终点,然后在这两个点之间指定时间点产出一个新点的机制。对于简单的浮点起始值,公式如下(假设时间从0到1):

    value = (endValue – startValue) × time + startValue;

    那么如果要插入一个类似于CGPointCGColorRef或者CATransform3D这种更加复杂类型的值,我们可以简单地对每个独立的元素应用这个方法(也就CGPoint中的x和y值,CGColorRef中的红,蓝,绿,透明值,或者是CATransform3D中独立矩阵的坐标)。我们同样需要一些逻辑在插值之前对对象拆解值,然后在插值之后在重新封装成对象,也就是说需要实时地检查类型。

    一旦我们可以用代码获取属性动画的起始值之间的任意插值,我们就可以把动画分割成许多独立的关键帧,然后产出一个线性的关键帧动画。清单10.7展示了相关代码。

    注意到我们用了60 x 动画时间(秒做单位)作为关键帧的个数,这时因为Core Animation按照每秒60帧去渲染屏幕更新,所以如果我们每秒生成60个关键帧,就可以保证动画足够的平滑(尽管实际上很可能用更少的帧率就可以达到很好的效果)。

    我们在示例中仅仅引入了对CGPoint类型的插值代码。但是,从代码中很清楚能看出如何扩展成支持别的类型。作为不能识别类型的备选方案,我们仅仅在前一半返回了fromValue,在后一半返回了toValue

    清单10.7 使用插入的值创建一个关键帧动画

    float interpolate(float from, float to, float time)
    {
    return (to - from) * time + from;
    }

    - (id)interpolateFromValue:(id)fromValue toValue:(id)toValue time:(float)time
    {
    if ([fromValue isKindOfClass:[NSValue class]]) {
    //get type
    const char *type = [fromValue objCType];
    if (strcmp(type, @encode(CGPoint)) == 0) {
    CGPoint from = [fromValue CGPointValue];
    CGPoint to = [toValue CGPointValue];
    CGPoint result = CGPointMake(interpolate(from.x, to.x, time), interpolate(from.y, to.y, time));
    return [NSValue valueWithCGPoint:result];
    }
    }
    //provide safe default implementation
    return (time < 0.5)? fromValue: toValue;
    }

    - (void)animate
    {
    //reset ball to top of screen
    self.ballView.center = CGPointMake(150, 32);
    //set up animation parameters
    NSValue *fromValue = [NSValue valueWithCGPoint:CGPointMake(150, 32)];
    NSValue *toValue = [NSValue valueWithCGPoint:CGPointMake(150, 268)];
    CFTimeInterval duration = 1.0;
    //generate keyframes
    NSInteger numFrames = duration * 60;
    NSMutableArray *frames = [NSMutableArray array];
    for (int i = 0; i < numFrames; i++) {
    float time = 1 / (float)numFrames * i;
    [frames addObject:[self interpolateFromValue:fromValue toValue:toValue time:time]];
    }
    //create keyframe animation
    CAKeyframeAnimation *animation = [CAKeyframeAnimation animation];
    animation.keyPath = @"position";
    animation.duration = 1.0;
    animation.delegate = self;
    animation.values = frames;
    //apply animation
    [self.ballView.layer addAnimation:animation forKey:nil];
    }

    这可以起到作用,但效果并不是很好,到目前为止我们所完成的只是一个非常复杂的方式来使用线性缓冲复制CABasicAnimation的行为。这种方式的好处在于我们可以更加精确地控制缓冲,这也意味着我们可以应用一个完全定制的缓冲函数。那么该如何做呢?

    缓冲背后的数学并不很简单,但是幸运的是我们不需要一一实现它。罗伯特·彭纳有一个网页关于缓冲函数(http://www.robertpenner.com/easing),包含了大多数普遍的缓冲函数的多种编程语言的实现的链接,包括C。这里是一个缓冲进入缓冲退出函数的示例(实际上有很多不同的方式去实现它)。

    float quadraticEaseInOut(float t) 
    {
    return (t < 0.5)? (2 * t * t): (-2 * t * t) + (4 * t) - 1;
    }

    对我们的弹性球来说,我们可以使用bounceEaseOut函数:

    float bounceEaseOut(float t)
    {
    if (t < 4/11.0) {
    return (121 * t * t)/16.0;
    } else if (t < 8/11.0) {
    return (363/40.0 * t * t) - (99/10.0 * t) + 17/5.0;
    } else if (t < 9/10.0) {
    return (4356/361.0 * t * t) - (35442/1805.0 * t) + 16061/1805.0;
    }
    return (54/5.0 * t * t) - (513/25.0 * t) + 268/25.0;
    }

    如果修改清单10.7的代码来引入bounceEaseOut方法,我们的任务就是仅仅交换缓冲函数,现在就可以选择任意的缓冲类型创建动画了(见清单10.8)。

    清单10.8 用关键帧实现自定义的缓冲函数

    - (void)animate
    {
    //reset ball to top of screen
    self.ballView.center = CGPointMake(150, 32);
    //set up animation parameters
    NSValue *fromValue = [NSValue valueWithCGPoint:CGPointMake(150, 32)];
    NSValue *toValue = [NSValue valueWithCGPoint:CGPointMake(150, 268)];
    CFTimeInterval duration = 1.0;
    //generate keyframes
    NSInteger numFrames = duration * 60;
    NSMutableArray *frames = [NSMutableArray array];
    for (int i = 0; i < numFrames; i++) {
    float time = 1/(float)numFrames * i;
    //apply easing
    time = bounceEaseOut(time);
    //add keyframe
    [frames addObject:[self interpolateFromValue:fromValue toValue:toValue time:time]];
    }
    //create keyframe animation
    CAKeyframeAnimation *animation = [CAKeyframeAnimation animation];
    animation.keyPath = @"position";
    animation.duration = 1.0;
    animation.delegate = self;
    animation.values = frames;
    //apply animation
    [self.ballView.layer addAnimation:animation forKey:nil];
    }

    总结
    在这一章中,我们了解了有关缓冲和CAMediaTimingFunction类,它可以允许我们创建自定义的缓冲函数来完善我们的动画,同样了解了如何用CAKeyframeAnimation来避开CAMediaTimingFunction的限制,创建完全自定义的缓冲函数。

    在下一章中,我们将要研究基于定时器的动画--另一个给我们对动画更多控制的选择,并且实现对动画的实时操纵。


    收起阅读 »

    iOS 缓冲 一

    缓冲生活和艺术一样,最美的永远是曲线。 -- 爱德华布尔沃 - 利顿在第九章“图层时间”中,我们讨论了动画时间和CAMediaTiming协议。现在我们来看一下另一个和时间相关的机制--所谓的缓冲。Core Animation使用缓冲来使动画移动更平滑更自然,...
    继续阅读 »

    缓冲

    生活和艺术一样,最美的永远是曲线。 -- 爱德华布尔沃 - 利顿

    在第九章“图层时间”中,我们讨论了动画时间和CAMediaTiming协议。现在我们来看一下另一个和时间相关的机制--所谓的缓冲。Core Animation使用缓冲来使动画移动更平滑更自然,而不是看起来的那种机械和人工,在这一章我们将要研究如何对你的动画控制和自定义缓冲曲线。

    10.1 动画速度

    动画实际上就是一段时间内的变化,这就暗示了变化一定是随着某个特定的速率进行。速率由以下公式计算而来:

    velocity = change / time

    这里的变化可以指的是一个物体移动的距离,时间指动画持续的时长,用这样的一个移动可以更加形象的描述(比如positionbounds属性的动画),但实际上它应用于任意可以做动画的属性(比如coloropacity)。

    上面的等式假设了速度在整个动画过程中都是恒定不变的(就如同第八章“显式动画”的情况),对于这种恒定速度的动画我们称之为“线性步调”,而且从技术的角度而言这也是实现动画最简单的方式,但也是完全不真实的一种效果。

    考虑一个场景,一辆车行驶在一定距离内,它并不会一开始就以60mph的速度行驶,然后到达终点后突然变成0mph。一是因为需要无限大的加速度(即使是最好的车也不会在0秒内从0跑到60),另外不然的话会干死所有乘客。在现实中,它会慢慢地加速到全速,然后当它接近终点的时候,它会慢慢地减速,直到最后停下来。

    那么对于一个掉落到地上的物体又会怎样呢?它会首先停在空中,然后一直加速到落到地面,然后突然停止(然后由于积累的动能转换伴随着一声巨响,砰!)。

    现实生活中的任何一个物体都会在运动中加速或者减速。那么我们如何在动画中实现这种加速度呢?一种方法是使用物理引擎来对运动物体的摩擦和动量来建模,然而这会使得计算过于复杂。我们称这种类型的方程为缓冲函数,幸运的是,Core Animation内嵌了一系列标准函数提供给我们使用。

    CAMediaTimingFunction

    那么该如何使用缓冲方程式呢?首先需要设置CAAnimationtimingFunction属性,是CAMediaTimingFunction类的一个对象。如果想改变隐式动画的计时函数,同样也可以使用CATransaction+setAnimationTimingFunction:方法。

    这里有一些方式来创建CAMediaTimingFunction,最简单的方式是调用+timingFunctionWithName:的构造方法。这里传入如下几个常量之一:

    kCAMediaTimingFunctionLinear 
    kCAMediaTimingFunctionEaseIn
    kCAMediaTimingFunctionEaseOut
    kCAMediaTimingFunctionEaseInEaseOut
    kCAMediaTimingFunctionDefault

    kCAMediaTimingFunctionLinear选项创建了一个线性的计时函数,同样也是CAAnimationtimingFunction属性为空时候的默认函数。线性步调对于那些立即加速并且保持匀速到达终点的场景会有意义(例如射出枪膛的子弹),但是默认来说它看起来很奇怪,因为对大多数的动画来说确实很少用到。

    kCAMediaTimingFunctionEaseIn常量创建了一个慢慢加速然后突然停止的方法。对于之前提到的自由落体的例子来说很适合,或者比如对准一个目标的导弹的发射。

    kCAMediaTimingFunctionEaseOut则恰恰相反,它以一个全速开始,然后慢慢减速停止。它有一个削弱的效果,应用的场景比如一扇门慢慢地关上,而不是砰地一声。

    kCAMediaTimingFunctionEaseInEaseOut创建了一个慢慢加速然后再慢慢减速的过程。这是现实世界大多数物体移动的方式,也是大多数动画来说最好的选择。如果只可以用一种缓冲函数的话,那就必须是它了。那么你会疑惑为什么这不是默认的选择,实际上当使用UIView的动画方法时,他的确是默认的,但当创建CAAnimation的时候,就需要手动设置它了。

    最后还有一个kCAMediaTimingFunctionDefault,它和kCAMediaTimingFunctionEaseInEaseOut很类似,但是加速和减速的过程都稍微有些慢。它和kCAMediaTimingFunctionEaseInEaseOut的区别很难察觉,可能是苹果觉得它对于隐式动画来说更适合(然后对UIKit就改变了想法,而是使用kCAMediaTimingFunctionEaseInEaseOut作为默认效果),虽然它的名字说是默认的,但还是要记住当创建显式CAAnimation它并不是默认选项(换句话说,默认的图层行为动画用kCAMediaTimingFunctionDefault作为它们的计时方法)。

    你可以使用一个简单的测试工程来实验一下(清单10.1),在运行之前改变缓冲函数的代码,然后点击任何地方来观察图层是如何通过指定的缓冲移动的。

    清单10.1 缓冲函数的简单测试

    @interface ViewController ()

    @property (nonatomic, strong) CALayer *colorLayer;

    @end

    @implementation ViewController

    - (void)viewDidLoad
    {
    [super viewDidLoad];
    //create a red layer
    self.colorLayer = [CALayer layer];
    self.colorLayer.frame = CGRectMake(0, 0, 100, 100);
    self.colorLayer.position = CGPointMake(self.view.bounds.size.width/2.0, self.view.bounds.size.height/2.0);
    self.colorLayer.backgroundColor = [UIColor redColor].CGColor;
    [self.view.layer addSublayer:self.colorLayer];
    }

    - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
    {
    //configure the transaction
    [CATransaction begin];
    [CATransaction setAnimationDuration:1.0];
    [CATransaction setAnimationTimingFunction:[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut]];
    //set the position
    self.colorLayer.position = [[touches anyObject] locationInView:self.view];
    //commit transaction
    [CATransaction commit];
    }

    @end

    UIView的动画缓冲

    UIKit的动画也同样支持这些缓冲方法的使用,尽管语法和常量有些不同,为了改变UIView动画的缓冲选项,给options参数添加如下常量之一:

    UIViewAnimationOptionCurveEaseInOut 
    UIViewAnimationOptionCurveEaseIn
    UIViewAnimationOptionCurveEaseOut
    UIViewAnimationOptionCurveLinear

    它们和CAMediaTimingFunction紧密关联,UIViewAnimationOptionCurveEaseInOut是默认值(这里没有kCAMediaTimingFunctionDefault相对应的值了)。

    具体使用方法见清单10.2(注意到这里不再使用UIView额外添加的图层,因为UIKit的动画并不支持这类图层)。

    清单10.2 使用UIKit动画的缓冲测试工程

    @interface ViewController ()

    @property (nonatomic, strong) UIView *colorView;

    @end

    @implementation ViewController

    - (void)viewDidLoad
    {
    [super viewDidLoad];
    //create a red layer
    self.colorView = [[UIView alloc] init];
    self.colorView.bounds = CGRectMake(0, 0, 100, 100);
    self.colorView.center = CGPointMake(self.view.bounds.size.width / 2, self.view.bounds.size.height / 2);
    self.colorView.backgroundColor = [UIColor redColor];
    [self.view addSubview:self.colorView];
    }

    - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
    {
    //perform the animation
    [UIView animateWithDuration:1.0 delay:0.0
    options:UIViewAnimationOptionCurveEaseOut
    animations:^{
    //set the position
    self.colorView.center = [[touches anyObject] locationInView:self.view];
    }
    completion:NULL];

    }

    @end

    缓冲和关键帧动画

    或许你会回想起第八章里面颜色切换的关键帧动画由于线性变换的原因(见清单8.5)看起来有些奇怪,使得颜色变换非常不自然。为了纠正这点,我们来用更加合适的缓冲方法,例如kCAMediaTimingFunctionEaseIn,给图层的颜色变化添加一点脉冲效果,让它更像现实中的一个彩色灯泡。

    我们不想给整个动画过程应用这个效果,我们希望对每个动画的过程重复这样的缓冲,于是每次颜色的变换都会有脉冲效果。

    CAKeyframeAnimation有一个NSArray类型的timingFunctions属性,我们可以用它来对每次动画的步骤指定不同的计时函数。但是指定函数的个数一定要等于keyframes数组的元素个数减一,因为它是描述每一帧之间动画速度的函数。

    在这个例子中,我们自始至终想使用同一个缓冲函数,但我们同样需要一个函数的数组来告诉动画不停地重复每个步骤,而不是在整个动画序列只做一次缓冲,我们简单地使用包含多个相同函数拷贝的数组就可以了(见清单10.3)。

    运行更新后的代码,你会发现动画看起来更加自然了。

    清单10.3 对CAKeyframeAnimation使用CAMediaTimingFunction

    @interface ViewController ()

    @property (nonatomic, weak) IBOutlet UIView *layerView;
    @property (nonatomic, weak) IBOutlet CALayer *colorLayer;

    @end

    @implementation ViewController

    - (void)viewDidLoad
    {
    [super viewDidLoad];
    //create sublayer
    self.colorLayer = [CALayer layer];
    self.colorLayer.frame = CGRectMake(50.0f, 50.0f, 100.0f, 100.0f);
    self.colorLayer.backgroundColor = [UIColor blueColor].CGColor;
    //add it to our view
    [self.layerView.layer addSublayer:self.colorLayer];
    }

    - (IBAction)changeColor
    {
    //create a keyframe animation
    CAKeyframeAnimation *animation = [CAKeyframeAnimation animation];
    animation.keyPath = @"backgroundColor";
    animation.duration = 2.0;
    animation.values = @[
    (__bridge id)[UIColor blueColor].CGColor,
    (__bridge id)[UIColor redColor].CGColor,
    (__bridge id)[UIColor greenColor].CGColor,
    (__bridge id)[UIColor blueColor].CGColor ];
    //add timing function
    CAMediaTimingFunction *fn = [CAMediaTimingFunction functionWithName: kCAMediaTimingFunctionEaseIn];
    animation.timingFunctions = @[fn, fn, fn];
    //apply animation to layer
    [self.colorLayer addAnimation:animation forKey:nil];
    }

    @end
    收起阅读 »

    iOS - 图层时间 二

    9.2 层级关系时间在第三章“图层几何学”中,你已经了解到每个图层是如何相对在图层树中的父图层定义它的坐标系的。动画时间和它类似,每个动画和图层在时间上都有它自己的层级概念,相对于它的父亲来测量。对图层调整时间将会影响到它本身和子图层的动画,但不会影响到父图层...
    继续阅读 »

    9.2 层级关系时间

    在第三章“图层几何学”中,你已经了解到每个图层是如何相对在图层树中的父图层定义它的坐标系的。动画时间和它类似,每个动画和图层在时间上都有它自己的层级概念,相对于它的父亲来测量。对图层调整时间将会影响到它本身和子图层的动画,但不会影响到父图层。另一个相似点是所有的动画都被按照层级组合(使用CAAnimationGroup实例)。

    CALayer或者CAGroupAnimation调整durationrepeatCount/repeatDuration属性并不会影响到子动画。但是beginTimetimeOffsetspeed属性将会影响到子动画。然而在层级关系中,beginTime指定了父图层开始动画(或者组合关系中的父动画)和对象将要开始自己动画之间的偏移。类似的,调整CALayerCAGroupAnimationspeed属性将会对动画以及子动画速度应用一个缩放的因子。

    全局时间和本地时间

    CoreAnimation有一个全局时间的概念,也就是所谓的马赫时间(“马赫”实际上是iOS和Mac OS系统内核的命名)。马赫时间在设备上所有进程都是全局的--但是在不同设备上并不是全局的--不过这已经足够对动画的参考点提供便利了,你可以使用CACurrentMediaTime函数来访问马赫时间:

    CFTimeInterval time = CACurrentMediaTime();

    这个函数返回的值其实无关紧要(它返回了设备自从上次启动后的秒数,并不是你所关心的),它真实的作用在于对动画的时间测量提供了一个相对值。注意当设备休眠的时候马赫时间会暂停,也就是所有的CAAnimations(基于马赫时间)同样也会暂停。

    因此马赫时间对长时间测量并不有用。比如用CACurrentMediaTime去更新一个实时闹钟并不明智。(可以用[NSDate date]代替,就像第三章例子所示)。

    每个CALayerCAAnimation实例都有自己本地时间的概念,是根据父图层/动画层级关系中的beginTimetimeOffsetspeed属性计算。就和转换不同图层之间坐标关系一样,CALayer同样也提供了方法来转换不同图层之间的本地时间。如下:

    - (CFTimeInterval)convertTime:(CFTimeInterval)t fromLayer:(CALayer *)l; 
    - (CFTimeInterval)convertTime:(CFTimeInterval)t toLayer:(CALayer *)l;

    当用来同步不同图层之间有不同的speedtimeOffsetbeginTime的动画,这些方法会很有用。

    暂停,倒回和快进

    设置动画的speed属性为0可以暂停动画,但在动画被添加到图层之后不太可能再修改它了,所以不能对正在进行的动画使用这个属性。给图层添加一个CAAnimation实际上是给动画对象做了一个不可改变的拷贝,所以对原始动画对象属性的改变对真实的动画并没有作用。相反,直接用-animationForKey:来检索图层正在进行的动画可以返回正确的动画对象,但是修改它的属性将会抛出异常。

    如果移除图层正在进行的动画,图层将会急速返回动画之前的状态。但如果在动画移除之前拷贝呈现图层到模型图层,动画将会看起来暂停在那里。但是不好的地方在于之后就不能再恢复动画了。

    一个简单的方法是可以利用CAMediaTiming来暂停图层本身。如果把图层的speed设置成0,它会暂停任何添加到图层上的动画。类似的,设置speed大于1.0将会快进,设置成一个负值将会倒回动画。

    通过增加主窗口图层的speed,可以暂停整个应用程序的动画。这对UI自动化提供了好处,我们可以加速所有的视图动画来进行自动化测试(注意对于在主窗口之外的视图并不会被影响,比如UIAlertview)。可以在app delegate设置如下进行验证:

    self.window.layer.speed = 100;

    你也可以通过这种方式来减速,但其实也可以在模拟器通过切换慢速动画来实现。

    9.3 手动动画

    timeOffset一个很有用的功能在于你可以它可以让你手动控制动画进程,通过设置speed为0,可以禁用动画的自动播放,然后来使用timeOffset来来回显示动画序列。这可以使得运用手势来手动控制动画变得很简单。

    举个简单的例子:还是之前关门的动画,修改代码来用手势控制动画。我们给视图添加一个UIPanGestureRecognizer,然后用timeOffset左右摇晃。

    因为在动画添加到图层之后不能再做修改了,我们来通过调整layertimeOffset达到同样的效果(清单9.4)。

    清单9.4 通过触摸手势手动控制动画

    @interface ViewController ()

    @property (nonatomic, weak) UIView *containerView;
    @property (nonatomic, strong) CALayer *doorLayer;

    @end

    @implementation ViewController

    - (void)viewDidLoad
    {
    [super viewDidLoad];
    //add the door
    self.doorLayer = [CALayer layer];
    self.doorLayer.frame = CGRectMake(0, 0, 128, 256);
    self.doorLayer.position = CGPointMake(150 - 64, 150);
    self.doorLayer.anchorPoint = CGPointMake(0, 0.5);
    self.doorLayer.contents = (__bridge id)[UIImage imageNamed:@"Door.png"].CGImage;
    [self.containerView.layer addSublayer:self.doorLayer];
    //apply perspective transform
    CATransform3D perspective = CATransform3DIdentity;
    perspective.m34 = -1.0 / 500.0;
    self.containerView.layer.sublayerTransform = perspective;
    //add pan gesture recognizer to handle swipes
    UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc] init];
    [pan addTarget:self action:@selector(pan:)];
    [self.view addGestureRecognizer:pan];
    //pause all layer animations
    self.doorLayer.speed = 0.0;
    //apply swinging animation (which won't play because layer is paused)
    CABasicAnimation *animation = [CABasicAnimation animation];
    animation.keyPath = @"transform.rotation.y";
    animation.toValue = @(-M_PI_2);
    animation.duration = 1.0;
    [self.doorLayer addAnimation:animation forKey:nil];
    }

    - (void)pan:(UIPanGestureRecognizer *)pan
    {
    //get horizontal component of pan gesture
    CGFloat x = [pan translationInView:self.view].x;
    //convert from points to animation duration //using a reasonable scale factor
    x /= 200.0f;
    //update timeOffset and clamp result
    CFTimeInterval timeOffset = self.doorLayer.timeOffset;
    timeOffset = MIN(0.999, MAX(0.0, timeOffset - x));
    self.doorLayer.timeOffset = timeOffset;
    //reset pan gesture
    [pan setTranslation:CGPointZero inView:self.view];
    }

    @end

    这其实是个小诡计,也许相对于设置个动画然后每次显示一帧而言,用移动手势来直接设置门的transform会更简单。

    在这个例子中的确是这样,但是对于比如说关键这这样更加复杂的情况,或者有多个图层的动画组,相对于实时计算每个图层的属性而言,这就显得方便的多了。

    总结

    在这一章,我们了解了CAMediaTiming协议,以及Core Animation用来操作时间控制动画的机制。在下一章,我们将要接触缓冲,另一个用来使动画更加真实的操作时间的技术。

    收起阅读 »

    一个"水"按钮(滑水的水)

    🐳 前言 不知道大家平时有没有留意水滴落下的瞬间。 仔细去听,仔细去看,每一滴滴水珠落下泛起的涟漪都让人意向连篇。 一个个显现而消失的涟漪就像时光仿佛带走了什么,还是留下了什么,又似乎一切都没有变,却又感觉多了些什么,让人情不自禁想要点一个赞~~ 好了不开玩...
    继续阅读 »

    🐳 前言



    • 不知道大家平时有没有留意水滴落下的瞬间。

    • 仔细去听,仔细去看,每一滴滴水珠落下泛起的涟漪都让人意向连篇。

    • 一个个显现而消失的涟漪就像时光仿佛带走了什么,还是留下了什么,又似乎一切都没有变,却又感觉多了些什么,让人情不自禁想要点一个~~

    • 好了不开玩笑了我们来试试做这个涟漪按钮。


    water.gif


    🤽‍♂️ ToDoList



    • 一片静好

    • 蜻蜓点水

    • 阵阵微波


    🚿 Just Do It



    • 其实做一个这样的效果无非就是中间的按钮旁边会有两个渐渐变大的阴影,而当时间的推移,随着阴影范围变大也渐渐消失。


    🌱 一片静好



    • 我们先做一个平静的湖面,也就是我们的按钮。


    /** index.html **/
    <div class="waterButton">
    <div class="good">
    <div class="good_btn" id="waterButton">
    <img src="./good.png" alt="">
    </div>
    <span id="water1"></span>
    <span id="water2"></span>
    </div>
    </div>


    • 在基本布局中我们需要一个div包裹住一个点赞图片来表示一个按钮,另外还需要两个span标签来表示即将泛起涟漪,这个到后面会用到。


    /** button.css **/
    .waterButton {
    height: 27rem;
    display: flex;
    justify-content: center;
    align-items: center;
    }
    .good {
    width: 6rem;
    height: 6rem;
    position: relative;
    }
    .good_btn {
    width: 6rem;
    height: 6rem;
    display: flex;
    justify-content: center;
    align-items: center;
    border-radius: 50%;
    z-index: 3;
    cursor: pointer;
    box-shadow: .4rem .4rem .8rem #c8d0e7,-.4rem -.4rem .8rem #fff;
    }
    img{
    width: 50%;
    height: 50%;
    z-index: 4;
    }


    • 因为是模拟在水中的效果所以如果按钮的阴影特别单一相同就不好了,这时候我们可以让按钮上面白色阴影下面灰色阴影,在这里推荐一个网站给大家如果需要制作这些阴影可以在这里调试 Neumorphism.io


    image.png


    🍃 蜻蜓点水



    • 因为是按钮我们需要一个点击事件来模拟水滴滴入湖中的感觉。

    • 而水波荡漾的感觉其实可以做成一个动画,让一个跟按钮一样的元素逐渐缩放到两倍后慢慢消失,我们可以使用两个这样的元素来在视觉上产生水波一个接一个的感觉。


    .good_water-1, .good_water-2 {
    width: 6rem;
    height: 6rem;
    border-radius: 50%;
    z-index: -1;
    position: absolute;
    top: 0;
    left: 0 ;
    filter: blur(1px);
    }
    .good_water-1 {
    box-shadow: .4rem .4rem .8rem #c8d0e7,
    -.4rem -.4rem .8rem #fff;
    background: linear-gradient(to bottom right, #c8d0e7 0%, #fff 100%);
    animation: waves 2s linear;
    }
    .good_water-2 {
    box-shadow: .4rem .4rem .8rem #c8d0e7,
    -.4rem -.4rem .8rem #fff;
    animation: waves 2s linear 1s;
    }
    @keyframes waves {
    0% {
    transform: scale(1);
    opacity: 1;
    }
    50% {
    opacity: 1;
    }
    100% {
    transform: scale(2);
    opacity: 0;
    }
    }


    • 跟按钮一样我们给两个水波元素也加上不同的阴影,这样的感觉会更有立体感,而为了营造水波逐渐消失的感觉,我们需要给一个过渡属性filter: blur(1px)


    /** JS **/
    <script>
    let btn=document.getElementById('waterButton')
    let water1=document.getElementById('water1')
    let water2=document.getElementById('water2')
    let timer=''
    btn.addEventListener('click', ()=>{
    window.clearTimeout(timer)
    water1.classList.add("good_water-1");
    water2.classList.add("good_water-2");
    setTimeout(()=>{
    water1.classList.remove("good_water-1");
    water2.classList.remove("good_water-2");
    }, 3000)
    })
    </script>


    • 接下来我们设定点击事件来动态添加样式并在动画结束后移除样式,这样我们来看看效果吧~


    water1.gif


    💦 阵阵微波



    • 如果我们不希望水波这么快停下的话,我们也可以设置水波动画为无限循环,这样的话我们就不需要点击按钮的时候再加样式了,我们之间把样式加到水波上,然后给animation设置无限循环播放infinite


    .good_water-1 {
    ...
    animation: waves 2s linear infinite;

    }
    .good_water-2 {
    ...
    animation: waves 2s linear 1s infinite;
    }


    • 接下来我们来看看效果吧~是不是还不错呢。


    water2.gif


    👋 写在最后



    • 首先感谢大家看到这里,这次分享的只是学习css中的一些乐趣,对于业务上可能不太实用,但是图个乐嘛~上班这么累,多用前端做点好玩的事情。

    • 前端世界太过奇妙,只有细心的人才能发现其乐趣,希望可以帮到有需要的人。

    • 如果您觉得这篇文章有帮助到您的的话不妨🍉关注+点赞+收藏+评论+转发🍉支持一下哟~~😛您的支持就是我更新的最大动力。

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

    收起阅读 »

    【前端可视化】如何在React中优雅的使用ECharts

    这片文章由最近公司的一个可视化项目有感而发,随着前端的飞速发展,近年来数据可视化越来越火,有些公司的业务跟地图、位置、大数据等脱离不开关系,所以数据可视化甚至成了单独的一门前端行业,比如在杭州地区的前端可视化职位不但有一定的需求量且高薪, 至今为止,已经有很...
    继续阅读 »

    这片文章由最近公司的一个可视化项目有感而发,随着前端的飞速发展,近年来数据可视化越来越火,有些公司的业务跟地图、位置、大数据等脱离不开关系,所以数据可视化甚至成了单独的一门前端行业,比如在杭州地区的前端可视化职位不但有一定的需求量且高薪,


    截屏2021-08-25 下午10.57.37.png


    至今为止,已经有很多的可视化框架供我们选择,比如D3EChartsLeaflet....等等。


    本文使用的可视化框架为ECharts


    看完本文你可以学到什么?



    • 如何搭建react+ts+echarts项目

    • typescript基础使用

    • eCharts如何在react中更安全高效的使用

    • eCharts的使用

    • eCharts图表尺寸自适应

    • 启蒙可视化项目思想


    本文的源码地址:github.com/Gexle-Tuy/e…


    项目准备


    技术栈为:React+TypeScript+ECharts。既然提到优雅那肯定跟TS逃离不开关系,毕竟强大的类型系统能给我的🐶💩代码保驾护航,什么?我不会TS,我不看了,别急,本文不做过于复杂的类型检查,只在组件状态(state)、属性(props)上做基本使用,不会TS也能看的懂,废话不多说,咱们开始吧。


    使用的为react官方脚手架create-react-app,但是默认启动的为正常的js项目,如果想加上typescript类型检查,我们可以去它的仓库地址查看使用语法。在github上找到facebook/create-react-app。找到目录packages/cra-template-typescript。 在README中就可以看见启动命令create-react-app my-app --template typescript。
    image


    项目搭建完成之后看看src下的index文件的后缀名是否为tsx而不是jsx,为tsx就说明ts项目搭建成功了,就可以开始咱们的高雅之旅了~





    初探


    前面瞎吧啦半天完全跟我们本文的主角ECharts没有关系呀,伞兵作者?别急,这就开始,首先安装ECharts。


    npm i echarts

    安装好之后该干什么?当然是来个官方的入门例子感受一下了啦,打开官网->快速入手->绘制一个简单的图表。


    可以看到,每一个图表都需要一个DOM当作容器,在React中我们可以用ref来获取到DOM实例。


    image


    发现平时正常写的ref竟然报错了,这就是强大的ts发挥了作用,我们把鼠标放上去可以发现提示框有一大堆东西。


    不能将类型“RefObject<unknown>”分配给类型“LegacyRef<HTMLDivElement> | undefined”。
    不能将类型“RefObject<unknown>”分配给类型“RefObject<HTMLDivElement>”。
    不能将类型“unknown”分配给类型“HTMLDivElement”。
    .....

    可以根据它的提示来解决这个问题,将ref加上类型检查,本文不对ts做过多介绍,只使用简单的基础类型检查,我们直接给它加上一个:any。


    eChartsRef:any= React.createRef();

    这样报错就消失了,可以理解为any类型就是没有类型检查,跟普通的js一样没有区别。真正的重点不在这里,所以就直接使用any,其实应该按照它的提示加上真正的类型检查RefObject<HTMLDivElement>





    拿到实例之后,直接copy官方的配置项例子过来看看效果。


    import React, { PureComponent } from "react";
    import * as eCharts from "echarts";

    export default class App extends PureComponent {

    eChartsRef: any = React.createRef();

    componentDidMount() {
    const myChart = eCharts.init(this.eChartsRef.current);

    let option = {
    title: {
    text: "ECharts 入门示例",
    },
    tooltip: {},
    legend: {
    data: ["销量"],
    },
    xAxis: {
    data: ["衬衫", "羊毛衫", "雪纺衫", "裤子", "高跟鞋", "袜子"],
    },
    yAxis: {},
    series: [
    {
    name: "销量",
    type: "bar",
    data: [5, 20, 36, 10, 10, 20],
    },
    ],
    };

    myChart.setOption(option);
    }

    render() {
    return <div ref={this.eChartsRef} style={{
    width: 600,
    height: 400,
    margin: 100
    }}></div>;
    }
    }

    gif


    当图标的动态效果呈现在你眼前的时候是不是心动了,原来可视化这么简单,到这里你就会了最基本的使用了。





    接下来就开始本文的重点!如何在react里封装图表组件动态渲染并自适应移动端


    正文


    首先确定项目中我们要用到的图表,这里我选了四个最基本且常用的图表(折线图趋势图饼状图柱状图)。


    所有的图表都由无状态组件写(函数组件、Hooks),因为它们只负责拿到数据并渲染。并无自己维护的状态。


    接下来就是封装图表组件,这里就不把四个表的代码都贴出来了,只拿一个折线图举例子。可以把拉下源码看下其他的图。


    折线图:src/components/LineChart


    import React, { useEffect, useRef } from 'react';
    import { IProps } from "./type";
    import * as echarts from "echarts";

    const Index: React.FC<IProps> = (props) => {

    const chartRef:any = useRef(); //拿到DOM容器

    // 每当props改变的时候就会实时重新渲染
    useEffect(()=>{
    const chart = echarts.init(chartRef.current); //echart初始化容器
    let option = { //配置项(数据都来自于props)
    title: {
    text: props.title ? props.title : "暂无数据",
    },
    xAxis: {
    type: 'category',
    data: props.xData,
    },
    yAxis: {
    type: 'value'
    },
    series: [{
    data: props.seriesData,
    type: 'line'
    }]
    };

    chart.setOption(option);
    }, [props]);

    return <div ref={chartRef} className="chart"></div>
    }

    export default Index;

    同文件下新建一个type.ts,将要约束的props类型检查单独抽离出去,当然也可以直接写在index.tsx文件里面,看个人喜好。
    type.ts


    // 给props添加类型检查
    export interface IProps {
    title: string, //图表的标题(为string类型)
    xData: string[], //图表x轴数据的数组(数字里面每一项都为string类型)
    seriesData: number[], //跟x轴每个坐标点对应的数据(数字里面每一项都为number类型)
    }

    根据每张图表对应的配置项,选出你想要动态配置的属性,就可以写成props作为属性传递过来。(比如,一个项目里需要用到很多张折线图,但是每个图表的线条颜色是不一样的,就可以把color写成一个props作为属性值传递进来。)





    封装好之后,我们在App.tsx中引入使用一下。


    App.tsx


    import React, { PureComponent } from "react";
    import LineChart from "./components/LineChart/Index";
    import "./App.css";
    export default class App extends PureComponent {
    eChartsRef: any = React.createRef();

    state = {
    lineChartData: {
    //折线图模拟数据
    xData: [
    "2021/08/13",
    "2021/08/14",
    "2021/08/15",
    "2021/08/16",
    "2021/08/17",
    "2021/08/18",
    ],
    seriesData: [22, 19, 88, 66, 5, 90],
    },
    };

    componentDidMount() {}

    render() {
    return (
    <div className="homeWrapper">
    {/* 折线图 */}
    <div className="chartWrapper">
    <LineChart
    title="折线图模拟数据"
    xData={this.state.lineChartData.xData}
    seriesData={this.state.lineChartData.seriesData}
    />
    </div>
    </div>
    );
    }
    }

    如果使用LineChart组件的时候少传了任何一个属性,或者说属性传递的类型不对,那么就会直接报错,将报错扼杀在开发阶段,而不是运行代码阶段,而且还有一个好处就是,加上类型检查后会有强大的智能提示,普通的js项目写一个组件根本就不会提示你需要传递某些属性。


    忘记传递某个属性
    image


    传递的类型不符合类型检查
    image


    效果如下:


    gif


    这样一个基本的图表组件就完成了,但是都是我们模拟的数据,在真实的开发中数据都是来自于后端返回给我们,而且格式还不是我们想要的,那时候就需要我们自己处理下数据包装成需要的数据格式再传递。


    这样封装成函数组件还有一个好处就是每当props改变的时候就会进行重新渲染。比如我在componentDidMount中开启一个定时器定时添加数据来模拟实时数据。


    componentDidMount() {
    setInterval(() => {
    this.setState({
    lineChartData: {
    xData: [...this.state.lineChartData.xData, "2000/01/01"],
    seriesData: [...this.state.lineChartData.seriesData, Math.floor(Math.random() * 100)],
    }
    })
    }, 1500 );
    }

    gif


    这样就可以实现展示实时数据了,比如每秒的pv、uv数等等。我们把四个图表组件全部封装好之后的效果是这样的。


    gif


    前三个图表的数据都来自实时数据模拟,最后一张饼状图直接在组件中写死数据了,有兴趣的小伙伴可以拉下源码自行把它实现成实时的,可以看option中的配置哪些需要配置的,单独抽离出来写在type.ts文件中。





    移动端适配


    啥?echarts没做移动端适配?当然不是,echarts的官网中就介绍了移动端的相关优化:echarts.apache.org/zh/feature.… 当然也有跨平台使用。


    gif


    好像是那么回事,但感觉好像少了些什么,好像没有根据屏幕尺寸大小变化而自动发生调整尺寸。每次都要刷新一下也就是重新进入页面。


    别着急,在它的API文档中,有这么一个方法,echarts创建的实例也就是通过echarts.init()之后的对象会有一个resize的方法。


    我们可以监听窗口的变化,只要窗口尺寸变化了就调用resize方法。监听窗口的变化的方法很简单window.onresize可以在创建组件对象的时候都添加上一个window.onresize方法。





    注意:如果网页只有一个图表那么这么写是可以的,如果项目中图表不只一个的话,每个图表组件难道在后面都写一个window.onresize方法吗?这样写的话只有最后创建的组件会自适应屏幕尺寸大小,因为每创建一个组件都重新将window.onresize赋予为新的函数体了。





    解决:我们可以写一个公用方法,每一次创建组件的时候都加入到一个数组中,当屏幕尺寸变化的时候,都去循环遍历这个数组中的每一项,然后调用resize方法。


    src/util.js


    const echartsDom = [];  //所有echarts图表的数组
    /**
    * 当屏幕尺寸变化时,循环数组里的每一项调用resize方法来实现自适应。
    * @param {*} eDom
    */
    export function echartsResize(eDom) {
    echartsDom.push(eDom);
    window.onresize = () => {
    echartsDom.forEach((it)=>{
    it.resize();
    })
    };
    }

    写好方法之后,在每个图表组件设置好option之后将他添加到此数组内,然后当屏幕尺寸变化后就可以将每个图表变成自适应的了。





    这样之后每个图表就都可以自适应屏幕尺寸啦~


    gif


    结语


    本文主要介绍了如何在react中更安全高效的使用eCharts,所涉及的ts都为最基础的类型检查(有兴趣的同学可以自行拓展),只是为了给各位提供一个我在写一个eCharts项目的时候如何去做和管理项目,文章有错误的地方欢迎指出,大佬勿喷,大家伙儿有更好的思路和想法欢迎大家积极留言。感谢观看~




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

    收起阅读 »

    DIff算法看不懂就一起来砍我(带图)

    前言 面试官:"你了解虚拟DOM(Virtual DOM)跟Diff算法吗,请描述一下它们"; 我:"额,...鹅,那个",完了😰,突然智商不在线,没组织好语言没答好或者压根就答不出来; 所以这次我总结一下相关的知识点,让你可以有一个清晰的认知之余也会让你在今...
    继续阅读 »

    前言


    面试官:"你了解虚拟DOM(Virtual DOM)Diff算法吗,请描述一下它们";


    我:"额,...鹅,那个",完了😰,突然智商不在线,没组织好语言没答好或者压根就答不出来;


    所以这次我总结一下相关的知识点,让你可以有一个清晰的认知之余也会让你在今后遇到这种情况可以坦然自若,应付自如,游刃有余:




    相关知识点:



    • 虚拟DOM(Virtual DOM):


      • 什么是虚拟dom




      • 为什么要使用虚拟dom




      • 虚拟DOM库





    • DIFF算法:

      • snabbDom源码

        • init函数

        • h函数

        • patch函数

        • patchVnode函数

        • updateChildren函数








    虚拟DOM(Virtual DOM)


    什么是虚拟DOM


    一句话总结虚拟DOM就是一个用来描述真实DOM的javaScript对象,这样说可能不够形象,那我们来举个🌰:分别用代码来描述真实DOM以及虚拟DOM


    真实DOM:


    <ul class="list">
    <li>a</li>
    <li>b</li>
    <li>c</li>
    </ul>

    对应的虚拟DOM:



    let vnode = h('ul.list', [
    h('li','a'),
    h('li','b'),
    h('li','c'),
    ])

    console.log(vnode)

    控制台打印出来的Vnode:


    image.png


    h函数生成的虚拟DOM这个JS对象(Vnode)的源码:


    export interface VNodeData {
    props?: Props
    attrs?: Attrs
    class?: Classes
    style?: VNodeStyle
    dataset?: Dataset
    on?: On
    hero?: Hero
    attachData?: AttachData
    hook?: Hooks
    key?: Key
    ns?: string // for SVGs
    fn?: () => VNode // for thunks
    args?: any[] // for thunks
    [key: string]: any // for any other 3rd party module
    }

    export type Key = string | number

    const interface VNode = {
    sel: string | undefined, // 选择器
    data: VNodeData | undefined, // VNodeData上面定义的VNodeData
    children: Array<VNode | string> | undefined, //子节点,与text互斥
    text: string | undefined, // 标签中间的文本内容
    elm: Node | undefined, // 转换而成的真实DOM
    key: Key | undefined // 字符串或者数字
    }


    补充:

    上面的h函数大家可能有点熟悉的感觉但是一时间也没想起来,没关系我来帮大伙回忆;
    开发中常见的现实场景,render函数渲染:


    // 案例1 vue项目中的main.js的创建vue实例
    new Vue({
    router,
    store,
    render: h => h(App)
    }).$mount("#app");

    //案例2 列表中使用render渲染
    columns: [
    {
    title: "操作",
    key: "action",
    width: 150,
    render: (h, params) => {
    return h('div', [
    h('Button', {
    props: {
    size: 'small'
    },
    style: {
    marginRight: '5px',
    marginBottom: '5px',
    },
    on: {
    click: () => {
    this.toEdit(params.row.uuid);
    }
    }
    }, '编辑')
    ]);
    }
    }
    ]



    为什么要使用虚拟DOM



    • MVVM框架解决视图和状态同步问题

    • 模板引擎可以简化视图操作,没办法跟踪状态

    • 虚拟DOM跟踪状态变化

    • 参考github上virtual-dom的动机描述

      • 虚拟DOM可以维护程序的状态,跟踪上一次的状态

      • 通过比较前后两次状态差异更新真实DOM



    • 跨平台使用

      • 浏览器平台渲染DOM

      • 服务端渲染SSR(Nuxt.js/Next.js),前端是vue向,后者是react向

      • 原生应用(Weex/React Native)

      • 小程序(mpvue/uni-app)等



    • 真实DOM的属性很多,创建DOM节点开销很大

    • 虚拟DOM只是普通JavaScript对象,描述属性并不需要很多,创建开销很小

    • 复杂视图情况下提升渲染性能(操作dom性能消耗大,减少操作dom的范围可以提升性能)


    灵魂发问:使用了虚拟DOM就一定会比直接渲染真实DOM快吗?答案当然是否定的,且听我说:
    2c3559e204c5aae6a1c6bfdc8557efcd.jpeg


    举例:当一个节点变更时DOMA->DOMB


    image.png
    上述情况:
    示例1是创建一个DOMB然后替换掉DOMA;
    示例2创建虚拟DOM+DIFF算法比对发现DOMBDOMA不是相同的节点,最后还是创建一个DOMB然后替换掉DOMA;
    可以明显看出1是更快的,同样的结果,2还要去创建虚拟DOM+DIFF算啊对比
    所以说使用虚拟DOM比直接操作真实DOM就一定要快这个说法是错误的,不严谨的


    举例:当DOM树里面的某个子节点的内容变更时:


    image.png
    当一些复杂的节点,比如说一个父节点里面有多个子节点,当只是一个子节点的内容发生了改变,那么我们没有必要像示例1重新去渲染这个DOM树,这个时候虚拟DOM+DIFF算法就能够得到很好的体现,我们通过示例2使用虚拟DOM+Diff算法去找出改变了的子节点更新它的内容就可以了


    总结:复杂视图情况下提升渲染性能,因为虚拟DOM+Diff算法可以精准找到DOM树变更的地方,减少DOM的操作(重排重绘)




    虚拟dom库



    • Snabbdom

      • Vue.js2.x内部使用的虚拟DOM就是改造的Snabbdom

      • 大约200SLOC(single line of code)

      • 通过模块可扩展

      • 源码使用TypeScript开发

      • 最快的Virtual DOM之一



    • virtual-dom




    Diff算法


    在看完上述的文章之后相信大家已经对Diff算法有一个初步的概念,没错,Diff算法其实就是找出两者之间的差异;



    diff 算法首先要明确一个概念就是 Diff 的对象是虚拟DOM(virtual dom),更新真实 DOM 是 Diff 算法的结果。



    下面我将会手撕snabbdom源码核心部分为大家打开Diff的心,给点耐心,别关网页,我知道你们都是这样:


    src=http___img.wxcha.com_file_201905_17_f5a4d33d48.jpg&refer=http___img.wxcha.jpeg




    snabbdom的核心



    • init()设置模块.创建patch()函数

    • 使用h()函数创建JavaScript对象(Vnode)描述真实DOM

    • patch()比较新旧两个Vnode

    • 把变化的内容更新到真实DOM树


    init函数


    init函数时设置模块,然后创建patch()函数,我们先通过场景案例来有一个直观的体现:


    import {init} from 'snabbdom/build/package/init.js'
    import {h} from 'snabbdom/build/package/h.js'

    // 1.导入模块
    import {styleModule} from "snabbdom/build/package/modules/style";
    import {eventListenersModule} from "snabbdom/build/package/modules/eventListeners";

    // 2.注册模块
    const patch = init([
    styleModule,
    eventListenersModule
    ])

    // 3.使用h()函数的第二个参数传入模块中使用的数据(对象)
    let vnode = h('div', [
    h('h1', {style: {backgroundColor: 'red'}}, 'Hello world'),
    h('p', {on: {click: eventHandler}}, 'Hello P')
    ])

    function eventHandler() {
    alert('疼,别摸我')
    }

    const app = document.querySelector('#app')

    patch(app,vnode)

    当init使用了导入的模块就能够在h函数中用这些模块提供的api去创建虚拟DOM(Vnode)对象;在上文中就使用了样式模块以及事件模块让创建的这个虚拟DOM具备样式属性以及事件属性,最终通过patch函数对比两个虚拟dom(会先把app转换成虚拟dom),更新视图;


    image.png


    我们再简单看看init的源码部分:


    // src/package/init.ts
    /* 第一参数就是各个模块
    第二参数就是DOMAPI,可以把DOM转换成别的平台的API,
    也就是说支持跨平台使用,当不传的时候默认是htmlDOMApi,见下文
    init是一个高阶函数,一个函数返回另外一个函数,可以缓存modules,与domApi两个参数,
    那么以后直接只传oldValue跟newValue(vnode)就可以了*/
    export function init (modules: Array<Partial<Module>>, domApi?: DOMAPI) {

    ...

    return function patch (oldVnode: VNode | Element, vnode: VNode): VNode {}
    }



    h函数


    些地方也会用createElement来命名,它们是一样的东西,都是创建虚拟DOM的,在上述文章中相信大伙已经对h函数有一个初步的了解并且已经联想了使用场景,就不作场景案例介绍了,直接上源码部分:


    // h函数
    export function h (sel: string): VNode
    export function h (sel: string, data: VNodeData | null): VNode
    export function h (sel: string, children: VNodeChildren): VNode
    export function h (sel: string, data: VNodeData | null, children: VNodeChildren): VNode
    export function h (sel: any, b?: any, c?: any): VNode {
    var data: VNodeData = {}
    var children: any
    var text: any
    var i: number
    ...
    return vnode(sel, data, children, text, undefined) //最终返回一个vnode函数
    };

    // vnode函数
    export function vnode (sel: string | undefined,
    data: any | undefined,
    children: Array<VNode | string> | undefined,
    text: string | undefined,
    elm: Element | Text | undefined): VNode {
    const key = data === undefined ? undefined : data.key
    return { sel, data, children, text, elm, key } //最终生成Vnode对象
    }

    总结:h函数先生成一个vnode函数,然后vnode函数再生成一个Vnode对象(虚拟DOM对象)


    补充:


    在h函数源码部分涉及一个函数重载的概念,简单说明一下:



    • 参数个数或参数类型不同的函数()

    • JavaScript中没有重载的概念

    • TypeScript中有重载,不过重载的实现还是通过代码调整参数



    重载这个概念个参数相关,和返回值无关




    • 实例1(函数重载-参数个数)



    function add(a:number,b:number){

    console.log(a+b)

    }

    function add(a:number,b:number,c:number){

    console.log(a+b+c)

    }

    add(1,2)

    add(1,2,3)



    • 实例2(函数重载-参数类型)



    function add(a:number,b:number){

    console.log(a+b)

    }

    function add(a:number,b:string){

    console.log(a+b)

    }

    add(1,2)

    add(1,'2')




    patch函数(核心)


    src=http___shp.qpic.cn_qqvideo_ori_0_e3012t7v643_496_280_0&refer=http___shp.qpic.jpeg


    要是看完前面的铺垫,看到这里你可能走神了,醒醒啊,这是核心啊,上高地了兄弟;



    • pactch(oldVnode,newVnode)

    • 把新节点中变化的内容渲染到真实DOM,最后返回新节点作为下一次处理的旧节点(核心)

    • 对比新旧VNode是否相同节点(节点的key和sel相同)

    • 如果不是相同节点,删除之前的内容,重新渲染

    • 如果是相同节点,再判断新的VNode是否有text,如果有并且和oldVnodetext不同直接更新文本内容(patchVnode)

    • 如果新的VNode有children,判断子节点是否有变化(updateChildren,最麻烦,最难实现)


    源码:


    return function patch(oldVnode: VNode | Element, vnode: VNode): VNode {    
    let i: number, elm: Node, parent: Node
    const insertedVnodeQueue: VNodeQueue = []
    // cbs.pre就是所有模块的pre钩子函数集合
    for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i]()
    // isVnode函数时判断oldVnode是否是一个虚拟DOM对象
    if (!isVnode(oldVnode)) {
    // 若不是即把Element转换成一个虚拟DOM对象
    oldVnode = emptyNodeAt(oldVnode)
    }
    // sameVnode函数用于判断两个虚拟DOM是否是相同的,源码见补充1;
    if (sameVnode(oldVnode, vnode)) {
    // 相同则运行patchVnode对比两个节点,关于patchVnode后面会重点说明(核心)
    patchVnode(oldVnode, vnode, insertedVnodeQueue)
    } else {
    elm = oldVnode.elm! // !是ts的一种写法代码oldVnode.elm肯定有值
    // parentNode就是获取父元素
    parent = api.parentNode(elm) as Node

    // createElm是用于创建一个dom元素插入到vnode中(新的虚拟DOM)
    createElm(vnode, insertedVnodeQueue)

    if (parent !== null) {
    // 把dom元素插入到父元素中,并且把旧的dom删除
    api.insertBefore(parent, vnode.elm!, api.nextSibling(elm))// 把新创建的元素放在旧的dom后面
    removeVnodes(parent, [oldVnode], 0, 0)
    }
    }

    for (i = 0; i < insertedVnodeQueue.length; ++i) {
    insertedVnodeQueue[i].data!.hook!.insert!(insertedVnodeQueue[i])
    }
    for (i = 0; i < cbs.post.length; ++i) cbs.post[i]()
    return vnode
    }

    补充1: sameVnode函数


    function sameVnode(vnode1: VNode, vnode2: VNode): boolean { 通过key和sel选择器判断是否是相同节点
    return vnode1.key === vnode2.key && vnode1.sel === vnode2.sel
    }



    patchVnode



    • 第一阶段触发prepatch函数以及update函数(都会触发prepatch函数,两者不完全相同才会触发update函数)

    • 第二阶段,真正对比新旧vnode差异的地方

    • 第三阶段,触发postpatch函数更新节点


    源码:


    function patchVnode(oldVnode: VNode, vnode: VNode, insertedVnodeQueue: VNodeQueue) {
    const hook = vnode.data?.hook
    hook?.prepatch?.(oldVnode, vnode)
    const elm = vnode.elm = oldVnode.elm!
    const oldCh = oldVnode.children as VNode[]
    const ch = vnode.children as VNode[]
    if (oldVnode === vnode) return
    if (vnode.data !== undefined) {
    for (let i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
    vnode.data.hook?.update?.(oldVnode, vnode)
    }
    if (isUndef(vnode.text)) { // 新节点的text属性是undefined
    if (isDef(oldCh) && isDef(ch)) { // 当新旧节点都存在子节点
    if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue) //并且他们的子节点不相同执行updateChildren函数,后续会重点说明(核心)
    } else if (isDef(ch)) { // 只有新节点有子节点
    // 当旧节点有text属性就会把''赋予给真实dom的text属性
    if (isDef(oldVnode.text)) api.setTextContent(elm, '')
    // 并且把新节点的所有子节点插入到真实dom中
    addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
    } else if (isDef(oldCh)) { // 清除真实dom的所有子节点
    removeVnodes(elm, oldCh, 0, oldCh.length - 1)
    } else if (isDef(oldVnode.text)) { // 把''赋予给真实dom的text属性
    api.setTextContent(elm, '')
    }
    } else if (oldVnode.text !== vnode.text) { //若旧节点的text与新节点的text不相同
    if (isDef(oldCh)) { // 若旧节点有子节点,就把所有的子节点删除
    removeVnodes(elm, oldCh, 0, oldCh.length - 1)
    }
    api.setTextContent(elm, vnode.text!) // 把新节点的text赋予给真实dom
    }
    hook?.postpatch?.(oldVnode, vnode) // 更新视图
    }

    看得可能有点蒙蔽,下面再上一副思维导图:


    image.png




    题外话:diff算法简介


    传统diff算法



    • 虚拟DOM中的Diff算法

    • 传统算法查找两颗树每一个节点的差异

    • 会运行n1(dom1的节点数)*n2(dom2的节点数)次方去对比,找到差异的部分再去更新


    image.png


    snabbdom的diff算法优化



    • Snbbdom根据DOM的特点对传统的diff算法做了优化

    • DOM操作时候很少会跨级别操作节点

    • 只比较同级别的节点


    image.png


    src=http___img.wxcha.com_file_202004_03_1ed2e19e4f.jpg&refer=http___img.wxcha.jpeg


    下面我们就会介绍updateChildren函数怎么去对比子节点的异同,也是Diff算法里面的一个核心以及难点;




    updateChildren(核中核:判断子节点的差异)



    • 这个函数我分为三个部分,部分1:声明变量,部分2:同级别节点比较,部分3:循环结束的收尾工作(见下图);


    image.png



    • 同级别节点比较五种情况:

      1. oldStartVnode/newStartVnode(旧开始节点/新开始节点)相同

      2. oldEndVnode/newEndVnode(旧结束节点/新结束节点)相同

      3. oldStartVnode/newEndVnode(旧开始节点/新结束节点)相同

      4. oldEndVnode/newStartVnode(旧结束节点/新开始节点)相同

      5. 特殊情况当1,2,3,4的情况都不符合的时候就会执行,在oldVnodes里面寻找跟newStartVnode一样的节点然后位移到oldStartVnode,若没有找到在就oldStartVnode创建一个



    • 执行过程是一个循环,在每次循环里,只要执行了上述的情况的五种之一就会结束一次循环

    • 循环结束的收尾工作:直到oldStartIdx>oldEndIdx || newStartIdx>newEndIdx(代表旧节点或者新节点已经遍历完)


    为了更加直观的了解,我们再来看看同级别节点比较五种情况的实现细节:


    新开始节点和旧开始节点(情况1)


    image.png



    • 情况1符合:(从新旧节点的开始节点开始对比,oldCh[oldStartIdx]和newCh[newStartIdx]进行sameVnode(key和sel相同)判断是否相同节点)

    • 则执行patchVnode找出两者之间的差异,更新图;如没有差异则什么都不操作,结束一次循环

    • oldStartIdx++/newStartIdx++


    新结束节点和旧结束节点(情况2)


    image.png



    • 情况1不符合就判断情况2,若符合:(从新旧节点的结束节点开始对比,oldCh[oldEndIdx]和newCh[newEndIdx]对比,执行sameVnode(key和sel相同)判断是否相同节点)

    • 执行patchVnode找出两者之间的差异,更新视图,;如没有差异则什么都不操作,结束一次循环

    • oldEndIdx--/newEndIdx--


    旧开始节点/新结束节点(情况3)


    image.png



    • 情况1,2都不符合,就会尝试情况3:(旧节点的开始节点与新节点的结束节点开始对比,oldCh[oldStartIdx]和newCh[newEndIdx]对比,执行sameVnode(key和sel相同)判断是否相同节点)

    • 执行patchVnode找出两者之间的差异,更新视图,如没有差异则什么都不操作,结束一次循环

    • oldCh[oldStartIdx]对应的真实dom位移到oldCh[oldEndIdx]对应的真实dom

    • oldStartIdx++/newEndIdx--;


    旧结束节点/新开始节点(情况4)


    image.png



    • 情况1,2,3都不符合,就会尝试情况4:(旧节点的结束节点与新节点的开始节点开始对比,oldCh[oldEndIdx]和newCh[newStartIdx]对比,执行sameVnode(key和sel相同)判断是否相同节点)

    • 执行patchVnode找出两者之间的差异,更新视图,如没有差异则什么都不操作,结束一次循环

    • oldCh[oldEndIdx]对应的真实dom位移到oldCh[oldStartIdx]对应的真实dom

    • oldEndIdx--/newStartIdx++;


    新开始节点/旧节点数组中寻找节点(情况5)


    image.png



    • 从旧节点里面寻找,若寻找到与newCh[newStartIdx]相同的节点(且叫对应节点[1]),执行patchVnode找出两者之间的差异,更新视图,如没有差异则什么都不操作,结束一次循环

    • 对应节点[1]对应的真实dom位移到oldCh[oldStartIdx]对应的真实dom


    image.png



    • 若没有寻找到相同的节点,则创建一个与newCh[newStartIdx]节点对应的真实dom插入到oldCh[oldStartIdx]对应的真实dom

    • newStartIdx++


    379426071b8130075b11ba142f9468e2.jpeg




    下面我们再介绍一下结束循环的收尾工作(oldStartIdx>oldEndIdx || newStartIdx>newEndIdx):


    image.png



    • 新节点的所有子节点先遍历完(newStartIdx>newEndIdx),循环结束

    • 新节点的所有子节点遍历结束就是把没有对应相同节点的子节点删除


    image.png



    • 旧节点的所有子节点先遍历完(oldStartIdx>oldEndIdx),循环结束

    • 旧节点的所有子节点遍历结束就是在多出来的子节点插入到旧节点结束节点前;(源码:newCh[newEndIdx + 1].elm),就是对应的旧结束节点的真实dom,newEndIdx+1是因为在匹配到相同的节点需要-1,所以需要加回来就是结束节点


    最后附上源码:


    function updateChildren(parentElm, oldCh, newCh, insertedVnodeQueue) {
    let oldStartIdx = 0; // 旧节点开始节点索引
    let newStartIdx = 0; // 新节点开始节点索引
    let oldEndIdx = oldCh.length - 1; // 旧节点结束节点索引
    let oldStartVnode = oldCh[0]; // 旧节点开始节点
    let oldEndVnode = oldCh[oldEndIdx]; // 旧节点结束节点
    let newEndIdx = newCh.length - 1; // 新节点结束节点索引
    let newStartVnode = newCh[0]; // 新节点开始节点
    let newEndVnode = newCh[newEndIdx]; // 新节点结束节点
    let oldKeyToIdx; // 节点移动相关
    let idxInOld; // 节点移动相关
    let elmToMove; // 节点移动相关
    let before;


    // 同级别节点比较
    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
    if (oldStartVnode == null) {
    oldStartVnode = oldCh[++oldStartIdx]; // Vnode might have been moved left
    }
    else if (oldEndVnode == null) {
    oldEndVnode = oldCh[--oldEndIdx];
    }
    else if (newStartVnode == null) {
    newStartVnode = newCh[++newStartIdx];
    }
    else if (newEndVnode == null) {
    newEndVnode = newCh[--newEndIdx];
    }
    else if (sameVnode(oldStartVnode, newStartVnode)) { // 判断情况1
    patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue);
    oldStartVnode = oldCh[++oldStartIdx];
    newStartVnode = newCh[++newStartIdx];
    }
    else if (sameVnode(oldEndVnode, newEndVnode)) { // 情况2
    patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue);
    oldEndVnode = oldCh[--oldEndIdx];
    newEndVnode = newCh[--newEndIdx];
    }
    else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right情况3
    patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue);
    api.insertBefore(parentElm, oldStartVnode.elm, api.nextSibling(oldEndVnode.elm));
    oldStartVnode = oldCh[++oldStartIdx];
    newEndVnode = newCh[--newEndIdx];
    }
    else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left情况4
    patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue);
    api.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm);
    oldEndVnode = oldCh[--oldEndIdx];
    newStartVnode = newCh[++newStartIdx];
    }
    else { // 情况5
    if (oldKeyToIdx === undefined) {
    oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx);
    }
    idxInOld = oldKeyToIdx[newStartVnode.key];
    if (isUndef(idxInOld)) { // New element // 创建新的节点在旧节点的新节点前
    api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm);
    }
    else {
    elmToMove = oldCh[idxInOld];
    if (elmToMove.sel !== newStartVnode.sel) { // 创建新的节点在旧节点的新节点前
    api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm);
    }
    else {
    // 在旧节点数组中找到相同的节点就对比差异更新视图,然后移动位置
    patchVnode(elmToMove, newStartVnode, insertedVnodeQueue);
    oldCh[idxInOld] = undefined;
    api.insertBefore(parentElm, elmToMove.elm, oldStartVnode.elm);
    }
    }
    newStartVnode = newCh[++newStartIdx];
    }
    }
    // 循环结束的收尾工作
    if (oldStartIdx <= oldEndIdx || newStartIdx <= newEndIdx) {
    if (oldStartIdx > oldEndIdx) {
    // newCh[newEndIdx + 1].elm就是旧节点数组中的结束节点对应的dom元素
    // newEndIdx+1是因为在之前成功匹配了newEndIdx需要-1
    // newCh[newEndIdx + 1].elm,因为已经匹配过有相同的节点了,它就是等于旧节点数组中的结束节点对应的dom元素(oldCh[oldEndIdx + 1].elm)
    before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].elm;
    // 把新节点数组中多出来的节点插入到before前
    addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx, insertedVnodeQueue);
    }
    else {
    // 这里就是把没有匹配到相同节点的节点删除掉
    removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx);
    }
    }
    }



    key的作用



    • Diff操作可以更加快速;

    • Diff操作可以更加准确;(避免渲染错误)

    • 不推荐使用索引作为key


    以下我们看看这些作用的实例:


    Diff操作可以更加准确;(避免渲染错误):

    实例:a,b,c三个dom元素中的b,c间插入一个z元素


    没有设置key
    image.png
    当设置了key:


    image.png


    Diff操作可以更加准确;(避免渲染错误)

    实例:a,b,c三个dom元素,修改了a元素的某个属性再去在a元素前新增一个z元素


    没有设置key:


    image.png


    image.png


    因为没有设置key,默认都是undefined,所以节点都是相同的,更新了text的内容但还是沿用了之前的dom,所以实际上a->z(a原本打勾的状态保留了,只改变了text),b->a,c->b,d->c,遍历完毕发现还要增加一个dom,在最后新增一个text为d的dom元素


    设置了key:


    image.png


    image.png


    当设置了key,a,b,c,d都有对应的key,a->a,b->b,c->c,d->d,内容相同无需更新,遍历结束,新增一个text为z的dom元素


    不推荐使用索引作为key:

    设置索引为key:


    image.png


    这明显效率不高,我们只希望找出不同的节点更新,而使用索引作为key会增加运算时间,我们可以把key设置为与节点text为一致就可以解决这个问题:


    image.png




    最后


    如有描述错误或者不明的地方请在下方评论联系我,我会立刻更新,如有收获,请为我点个赞👍这是对我的莫大的支持,谢谢各位


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

    收起阅读 »

    产品经理说你能不能让词云动起来?我觉得配得上!!!

    ☀️ 前言 事情是这样的,前段时间拿到公司的数据大屏原型图让我一天内把一整个页面做出来。 简单看了看,就是一个3840 * 1840的大屏然后几个列表几个图例看起来也没有多复杂。 唰!很快啊加了一会班把整个页面调整好了信心十足拿给产品经理看。 产品经理皱了皱...
    继续阅读 »

    ☀️ 前言



    • 事情是这样的,前段时间拿到公司的数据大屏原型图让我一天内把一整个页面做出来。

    • 简单看了看,就是一个3840 * 1840的大屏然后几个列表几个图例看起来也没有多复杂。

    • 唰!很快啊加了一会班把整个页面调整好了信心十足拿给产品经理看。

    • 产品经理皱了皱眉头:你这词云不会动啊??


    🌤️ 之前的效果



    • 听到这话我发现情况不对,我寻思着这原型图的词云也看不出他要没要求我动啊,而且明明我做的是会动的呀!


    🎢 关系图



    • 一开始我用的是echartsgraph关系图,这种图的特点是一开始会因为每个词的斥力会互相分开,在一开始会有一些动态效果,但是因为力引导布局会在多次迭代后才会稳定,所以到后面就不会继续运动了。


    ciyun1.gif



    • 我:是吧我没骗人吧?确实是会动的。

    • 产品经理:这样效果不好,没有科技感,而且我要字体大小每个都不同的,明天要拿给客户看一版,比较急,算了你别做动的了就让他词云填满然后每个词的大小要不一样。


    WPS图片编辑.png


    🎠 词云图



    • 做不动词云的那不就简单了,直接使用echartswordCloud图啊,直接唰唰配置一下就好了。


    image.png



    • 产品经理:客户看完了,整体还不错,但是词云这块我还是想它动起来,这样吧,你想个办法整整。


    src=http___5b0988e595225.cdn.sohucs.com_images_20181108_0b031f4213f4403ca4cfca30c2b369ca.jpeg&refer=http___5b0988e595225.cdn.sohucs.jpg


    🚄 自己手写



    • 对于这个词云,我一开始真的是死脑筋了,认定要用echarts来做,但实际上wordCloud官网也没有提供资料了,好像确实也没有办法让它动起来。

    • 思量片刻....等会,词云要不同大小不同颜色然后要在区域内随机移动,既然我不熟canvas,那我是不是可以用jscss来写一个2d的呢,说白了就是一个词语在一个容器内随机运动然后每个词语都动起来撒,好像能行....开干。


    🚅 ToDoList



    • 准备容器和需要的配置项

    • 生成所有静态词云

    • 让词云动起来


    🚈 Just Do It



    • 由于我这边的技术栈是vue 2.x的所以接下来会用vue 2.x的语法来分享,但实际上换成原生js也没有什么难度,相信大家可以接受的。


    🚎 准备容器和需要的配置项



    • 首先建立一个容器来包裹我们要装的词云,我们接下来的所有操作都围绕这个容器进行。


    <template>
    <div class="wordCloud" ref="wordCloud">
    </div>
    </template>

    image.png



    • 因为我们的词云需要有不同的颜色我们需要实现准备一个词语列表和颜色列表,再准备一个空数组来存储之后生成的词语。


    ...
    data () {
    return {
    hotWord: ['万事如意', '事事如意 ', '万事亨通', '一帆风顺', '万事大吉', '吉祥如意', '步步高升', '步步登高', '三羊开泰', '得心应手', '财源广进', '陶未媲美', '阖家安康', '龙马精神'],
    color: [
    '#a18cd1', '#fad0c4', '#ff8177',
    '#fecfef', '#fda085', '#f5576c',
    '#330867', '#30cfd0', '#38f9d7'
    ],
    wordArr: []
    };
    }
    ...


    • 准备的这些词语都是想对现在在读文章的你说的~如果觉得我说得对的不妨读完文章后给一个 ~

    • 好了不开玩笑,现在准备工作完成了,开始生成我们的词云。


    🚒 生成所有静态词云



    • 我们如果想让一个容器里面充满词语,按照正常我们切图的逻辑来说,每个词语占一个span,那么就相当于一个div里面有n(hotWord数量)个词语,也就是容器里面有对应数量的span标签即可。

    • 如果需要不同的颜色和大小,再分别对span标签分别加不同样式即可。


    ...
    mounted () {
    this.init();
    },
    methods: {
    init () {
    this.dealSpan();
    },
    dealSpan () {
    const wordArr = [];
    this.hotWord.forEach((value) => {
    // 根据词云数量生成span数量设置字体颜色和大小
    const spanDom = document.createElement('span');
    spanDom.style.position = 'relative';
    spanDom.style.display = "inline-block";
    spanDom.style.color = this.randomColor();
    spanDom.style.fontSize = this.randomNumber(15, 25) + 'px';
    spanDom.innerHTML = value;
    this.$refs.wordCloud.appendChild(spanDom);
    wordArr.push(spanDom);
    });
    this.wordArr = wordArr;
    },
    randomColor () {
    // 获取随机颜色
    var colorIndex = Math.floor(this.color.length * Math.random());
    return this.color[colorIndex];
    },
    randomNumber (lowerInteger, upperInteger) {
    // 获得一个包含最小值和最大值之间的随机数。
    const choices = upperInteger - lowerInteger + 1;
    return Math.floor(Math.random() * choices + lowerInteger);
    }
    }
    ...


    • 我们对hotWord热词列表进行遍历,每当有一个词语就生成一个span标签,分别使用randomColor()randomSize()设置不同的随机颜色和大小。

    • 最后再将这些span都依次加入div容器中,那么完成后是这样的。


    image.png


    🚓 让词云动起来



    • 词语是添加完了,接下来我们需要让他们动起来,那么该怎么动呢,我们自然而然会想到transformtranslateXtranslateY属性,我们首先要让一个词语先动起来,接下来所有的都应用这种方式就可以了。


    先动一下x轴


    • 怎么动呢?我们现在要做的是一件无限循环的事情,就是一个元素无限的移动,既然是无限,在js中用定时器可不可以实现呢?确实是可以的,但是会巨卡,万一词语一多你的电脑会爆炸,在另一方面编写动画循环的关键是要知道延迟时间多长合适,如果太长或者太短都不合适所以不用定时器。

    • 然后一不小心发现了window.requestAnimationFrame这个APIrequestAnimationFrame不需要设置时间间隔。



    requestAnimationFrame 会把每一帧中的所有DOM操作集中起来,在一次重绘或回流中就完成,并且重绘或回流的时间间隔紧紧跟随浏览器的刷新频率,一般来说,这个频率为每秒60帧。




    • 也就是说当我们循环无限的让一个元素在x轴或者y轴移动,假设每秒向右移动10px那么它的translateX就是累加10px,每个元素都是如此那么我们需要给span元素新增一个属性来代表它的位置。


    data () {
    return {
    ...
    timer: null,
    resetTime: 0
    ...
    };
    }
    methods: {
    init () {
    this.dealSpan();
    this.render();
    },
    dealSpan () {
    const wordArr = [];
    this.hotWord.forEach((value) => {
    ...
    spanDom.local = {
    position: {
    x: 0,
    y: 0
    }
    };
    ...
    });
    this.wordArr = wordArr;
    },
    render () {
    if (this.resetTime < 100) {
    //防止“栈溢出”
    this.resetTime = this.resetTime + 1;
    this.timer = requestAnimationFrame(this.render.bind(this));
    this.resetTime = 0;
    }
    this.wordFly();
    },
    wordFly () {
    this.wordArr.forEach((value) => {
    //每次循环加1
    value.local.position.x += 1;
    // 给每个词云加动画过渡
    value.style.transform = 'translateX(' + value.local.position.x + 'px)';
    });
    },
    },
    destroyed () {
    // 组件销毁,关闭定时执行
    cancelAnimationFrame(this.timer);
    },


    • 这时候我们给每个元素加了个local属性里面有它的初始位置,每当我们执行一次requestAnimationFrame的时候它的初始位置+1,再把这个值给到translateX这样我们每次循环都相当于移动了1px,现在我们来看看效果。


    ciyun2.gif


    调整范围


    • 嘿!好家伙,动是动起来了,但是怎么还过头了呢?

    • 我们发现每次translateX+1了但是没有给一个停止的范围给他,所以我们需要给一个让他到容器的边缘就开始掉头的步骤。

    • 那怎么样让他掉头呢?既然我们可以让他每次往右移动1px那么我们是不是可以检测到当它的x轴位置大于这个容器的位置时x轴位置小于这个容器的位置时并且换个方向就好换个方向我们只需要用正负数来判断即可。


    init () {
    this.dealSpan();
    this.initWordPos();
    this.render();
    },
    dealSpan () {
    const wordArr = [];
    this.hotWord.forEach((value) => {
    ...
    spanDom.local = {
    position: {
    // 位置
    x: 0,
    y: 0
    },
    direction: {
    // 方向 正数往右 负数往左
    x: 1,
    y: 1
    }
    };
    ...
    });
    this.wordArr = wordArr;
    },
    wordFly () {
    this.wordArr.forEach((value) => {
    // 设置运动方向 大于边界或者小于边界的时候换方向
    if (value.local.realPos.minx + value.local.position.x < this.ContainerSize.leftPos.x) {
    value.local.direction.x = -value.local.direction.x;
    }
    if (value.local.realPos.maxx + value.local.position.x > this.ContainerSize.rightPos.x) {
    value.local.direction.x = -value.local.direction.x;
    }
    //每次右移1个单位 当方向为负数时就是-1个单位也就是向左移1个单位
    value.local.position.x += 1 * value.local.direction.x;
    // 给每个词云加动画过渡
    value.style.transform = 'translateX(' + value.local.position.x + 'px)';
    });
    },
    initWordPos () {
    // 计算每个词的真实位置和容器的位置
    this.wordArr.forEach((value) => {
    value.local.realPos = {
    minx: value.offsetLeft,
    maxx: value.offsetLeft + value.offsetWidth
    };
    });
    this.ContainerSize = this.getContainerSize();
    },
    getContainerSize () {
    // 判断容器大小控制词云位置
    const el = this.$refs.wordCloud;
    return {
    leftPos: {
    // 容器左侧的位置和顶部位置
    x: el.offsetLeft,
    y: el.offsetTop
    },
    rightPos: {
    // 容器右侧的位置和底部位置
    x: el.offsetLeft + el.offsetWidth,
    y: el.offsetTop + el.offsetHeight
    }
    };
    }


    • 我们一开始先用initWordPos来计算每个词语现在处于的位置并把它的位置保存起来,再使用getContainerSize获取我们的外部容器的最左侧最右侧最上最下的位置保存起来。

    • 给我们每个span添加一个属性direction 方向,当方向为负数则往左,方向为正则往右,注释我写在代码上了,大家如果不清除可以看一下。

    • 也就是说我们的词云会在容器里面反复横跳,那我们来看看效果。


    ciyun3.gif


    随机位移


    • 很不错,是我们想要的效果!!!

    • 当然我们每次位移不可能写死只位移1px我们要做到那种凌乱美,那就需要做一个随机位移。

    • 那怎么来做随机位移呢?可以看出我们的词语其实是在做匀速直线运动而匀速直线运动的公式大家还记得吗?

    • 如果不记得的话这边建议回去翻一下物理书~ 匀速直线运动的位移公式是 x=vt

    • 这个x就是我们需要的位移,而这个t我们就不用管了因为我上面也说了这个requestAnimationFrame会帮助我们设置时间,那我们只需要控制这个v初速度是随机的就可以了。


    dealSpan () {
    const wordArr = [];
    this.hotWord.forEach((value) => {
    ...
    spanDom.local = {
    velocity: {
    // 每次位移初速度
    x: -0.5 + Math.random(),
    y: -0.5 + Math.random()
    },
    };
    ...
    });
    this.wordArr = wordArr;
    },
    wordFly () {
    this.wordArr.forEach((value) => {
    ...
    //利用公式 x=vt
    value.local.position.x += value.local.velocity.x * value.local.direction.x;
    ...
    });
    },


    • 我们给每个词语span元素一个初速度,这个初速度可以为- 也可以为+代表向左或者向右移动,当我们处理这个translateX的时候他就会随机处理了,现在我们来看看效果。


    ciyun4.gif


    完善y轴


    • 现在x轴已经按照我们想的所完成了,想让词云们上下左右都动起来那么我们需要按照x轴的方法来配一下y轴即可。

    • 由于代码长度问题我就不放出来啦,我下面会给出源码,大家有兴趣可以去下载看看~我们直接来看看成品!小卢感谢您的阅读,那我就在这里祝您


    ciyun5.gif



    • 至此一个简单的词云动画就完啦,具体源码我放在这里。

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

    收起阅读 »

    贝塞尔曲线在前端,走近她,然后爱上她

    贝塞尔曲线在前端 css3的动画主要是 transition animation transition有transition-timing-function animation有animation-timing-function 以transition-t...
    继续阅读 »

    贝塞尔曲线在前端


    css3的动画主要是



    • transition

    • animation


    transition有transition-timing-function

    animation有animation-timing-function


    transition-timing-function为例


    image.png


    其内置 ease,linear,ease-in,ease-out,ease-in-out就是贝塞尔曲线函数, 作用是控制属性变化的速度。

    也可以自定义cubic-bizier(x1,y1,x2,y2), 这个嘛玩意呢,三阶贝塞尔曲线, x1,y1x2,y2是两个控制点。


    如图:
    x1, y1对应 P1点, x2,y2 对应P2点。

    要点:



    1. 曲线越陡峭,速度越快,反之,速度越慢!

    2. 控制点的位置会影响曲线形状


    image.png




    说道这里, 回想一下我们前端在哪些地方还会贝塞尔呢。



    • svg

    • canvas/webgl

    • css3 动画

    • animation Web API

      千万别以为JS就不能操作CSS3动画了


    这样说可能有些空洞,我们一起来看看曲线和实际的动画效果:

    红色ease和ease-out曲线前期比较陡峭,加速度明显比较快


    图片.png


    贝塞尔曲线运动-演示地址
    6af390fc619a4f1f8758a437d03e37c4~tplv-k3u1fbpfcp-watermark.image.gif




    什么是贝赛尔曲线


    贝塞尔曲线(Bézier curve),又称贝兹曲线或贝济埃曲线,是应用于二维图形应用程序的数学曲线。


    公式怎么理解呢?这里你可以假定



    • P0的坐标(0,0), 最终的点的坐标为(1,1)


    t从0不断的增长到1

    t的值和控制点的x坐标套入公式,得到一个新的x坐标值

    t的值和控制点的y坐标套入公式,得到一个新的y坐标值


    (新的x坐标值 , 新的y坐标值)坐标就是t时刻曲线的点的坐标。


    通用公式


    image.png


    线性公式


    无控制点,直线


    image.png


    二次方公式


    一个控制点


    image.png


    三次方公式


    两个控制点


    image.png


    这是我们的重点,因为css动画都是三次方程式


    P0作为起点,P3作为终点, 控制点是P1与P2, 因为我们一般会假定 P0 为 (0,0), 而 P3为(1,1)。


    控制点的变化,会影响整个曲线,我们一起来简单封装一下并进行实例操作。


    一阶二阶三阶封装


    我们基于上面公式的进行简单的封装,

    你传入需要的点数量和相应的控制点就能获得相应一组点的信息。


    class Bezier {
    getPoints(count = 100, ...points) {
    const len = points.length;
    if (len < 2 || len > 4) {
    throw new Error("参数points的长度应该大于等于2小于5");
    }
    const fn =
    len === 2
    ? this.firstOrder
    : len === 3
    ? this.secondOrder
    : this.thirdOrder;
    const retPoints = [];
    for (let i = 0; i < count; i++) {
    retPoints.push(fn.call(null, i / count, ...points));
    }
    return retPoints;
    }

    firstOrder(t, p0, p1) {
    const { x: x0, y: y0 } = p0;
    const { x: x1, y: y1 } = p1;
    const x = (x1 - x0) * t;
    const y = (y1 - y0) * t;
    return { x, y };
    }

    secondOrder(t, p0, p1, p2) {
    const { x: x0, y: y0 } = p0;
    const { x: x1, y: y1 } = p1;
    const { x: x2, x: y2 } = p2;
    const x = (1 - t) * (1 - t) * x0 + 2 * t * (1 - t) * x1 + t * t * x2;
    const y = (1 - t) * (1 - t) * y0 + 2 * t * (1 - t) * y1 + t * t * y2;
    return { x, y };
    }

    thirdOrder(t, p0, p1, p2, p3) {
    const { x: x0, y: y0 } = p0;
    const { x: x1, y: y1 } = p1;
    const { x: x2, y: y2 } = p2;
    const { x: x3, y: y3 } = p3;
    let x =
    x0 * Math.pow(1 - t, 3) +
    3 * x1 * t * (1 - t) * (1 - t) +
    3 * x2 * t * t * (1 - t) +
    x3 * t * t * t;
    let y =
    y0 * (1 - t) * (1 - t) * (1 - t) +
    3 * y1 * t * (1 - t) * (1 - t) +
    3 * y2 * t * t * (1 - t) +
    y3 * t * t * t;
    return { x, y };
    }
    }

    export default new Bezier();


    演示地址: xiangwenhu.github.io/juejinBlogs…


    一阶贝塞尔是一条直线:

    image.png


    二阶贝塞尔一个控制点:


    image.png


    三阶贝塞尔两个控制点:


    image.png


    贝塞尔曲线控制点


    回到最开始, animation和 transition都可以自定义三阶贝塞尔函数, 而需要的就是两个控制点的信息怎么通过测试曲线获得控制点呢?


    在线取三阶贝塞尔关键的方案早就有了。



    在线贝塞尔

    在线贝塞尔2



    但是不妨碍我自己去实现一个简单,加强理解。

    大致的实现思路



    • canvas 绘制效果

      canvas有bezierCurveTo方法,直接可以绘制贝塞尔曲线

    • 两个控制点用dom元素来显示


    逻辑



    • 点击时计算最近的点,同时修改最近点的坐标

    • 重绘


    当然这只是一个简单的版本。


    演示地址: xiangwenhu.github.io/juejinBlogs…

    截图:


    有了这个,你就可以通过曲线获得控制点了, 之前提到过,曲线的陡峭决定了速度的快慢,是不是很有用呢?


    当然,你可以自己加个贝塞尔的直线运动,查看实际的运动效果,其实都不难,难的是你不肯动手!!!


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

    收起阅读 »

    高级IOS开发进阶 - 自旋锁、互斥锁以及读写锁!(二)

    4.3 源码分析initWithCondition:保存了condition参数以及NSCondition的创建。lockWhenCondition:open func lock(whenCondition condition: Int) { let ...
    继续阅读 »

    4.3 源码分析

    • initWithCondition




    • 保存了condition参数以及NSCondition的创建。

    • lockWhenCondition

    open func lock(whenCondition condition: Int) {
    let _ = lock(whenCondition: condition, before: Date.distantFuture)
    }

    内部调用了lockWhenCondition: before:,默认值传的Date.distantFuture

    open func lock(whenCondition condition: Int, before limit: Date) -> Bool {
    _cond.lock()
    while _thread != nil || _value != condition {
    if !_cond.wait(until: limit) {
    _cond.unlock()
    return false
    }
    }
    _thread = pthread_self()
    _cond.unlock()
    return true
    }

    NSCondition加锁判断condition条件是否满足,不满足调用NSConditionwait waitUntilDate方法进入等待,超时后解锁返回false。满足的情况下赋值_thread解锁返回true

    • unlockWithCondition
    open func unlock(withCondition condition: Int) {
    _cond.lock()
    _thread = nil
    _value = condition
    _cond.broadcast()
    _cond.unlock()
    }

    加锁后释放_thread,更新condition,调用broadcast后解锁。

    • lock
    open func lock() {
    let _ = lock(before: Date.distantFuture)
    }

    open func lock(before limit: Date) -> Bool {
    _cond.lock()
    while _thread != nil {
    if !_cond.wait(until: limit) {
    _cond.unlock()
    return false
    }
    }
    _thread = pthread_self()
    _cond.unlock()
    return true
    }

    判断是否有其它任务阻塞,没有阻塞直接创建_thread返回true

    • unlock
    open func unlock() {
    _cond.lock()
    _thread = nil
    _cond.broadcast()
    _cond.unlock()
    }

    广播并且释放_thread

    4.4 反汇编分析

    initWithCondition



    • lockWhenCondition

    -(int)lockWhenCondition:(int)arg2 {
    r0 = [arg0 lockWhenCondition:arg2 beforeDate:[NSDate distantFuture]];
    return r0;
    }

    调用自己的lockWhenCondition: beforeDate :


    unlockWithCondition


    -(int)unlockWithCondition:(int)arg2 {
    r0 = object_getIndexedIvars(arg0);
    [*r0 lock];
    *(int128_t *)(r0 + 0x8) = 0x0;
    *(int128_t *)(r0 + 0x10) = arg2;
    [*r0 broadcast];
    r0 = *r0;
    r0 = [r0 unlock];
    return r0;
    }
    • lock
    int -[NSConditionLock lock](int arg0) {
    r0 = [arg0 lockBeforeDate:[NSDate distantFuture]];
    return r0;
    }
    lockBeforeDate



    unlock

    int -[NSConditionLock unlock]() {
    r0 = object_getIndexedIvars(r0);
    [*r0 lock];
    *(r0 + 0x8) = 0x0;
    [*r0 broadcast];
    r0 = *r0;
    r0 = [r0 unlock];
    return r0;
    }

    汇编、源码以及断点调试逻辑相同。
    NSConditionLock 内部封装了NSCondition。

    五、OSSpinLock & os_unfair_lock




    OSSpinLockAPI注释以及它自己的命名说明了这是一把自旋锁,自iOS10之后被os_unfair_lock替代。



    • os_unfair_lock必须以OS_UNFAIR_LOCK_INIT初始化。
    • 它是用来代替OSSpinLock的。
    • 它不是自旋锁(忙等),是被内核唤醒的(闲等)。



    可以看到这两个锁都是定义在libsystem_platform.dylib中的。可以在openSource中找到他们libplatform的源码libplatform,实现是在/src/os目录下的lock.c文件中。

    5.1 OSSpinLock 源码分析

    OSSpinLock的使用一般会用到以下API

    OSSpinLock hp_spinlock = OS_SPINLOCK_INIT;
    OSSpinLockLock(&hp_spinlock);
    OSSpinLockUnlock(&hp_spinlock);
    OSSpinLock
    typedef int32_t OSSpinLock OSSPINLOCK_DEPRECATED_REPLACE_WITH(os_unfair_lock);

    #define OS_SPINLOCK_INIT 0

    OSSpinLock本身是一个int32_t类型的值,初始化默认值为0

    5.1.1 OSSpinLockLock

    void
    OSSpinLockLock(volatile OSSpinLock *l)
    {
    OS_ATOMIC_ALIAS(spin_lock, OSSpinLockLock);
    OS_ATOMIC_ALIAS(_spin_lock, OSSpinLockLock);
    bool r = os_atomic_cmpxchg(l, 0, _OSSpinLockLocked, acquire);
    if (likely(r)) return;
    return _OSSpinLockLockSlow(l);
    }

    #if TARGET_OS_IPHONE && !TARGET_OS_SIMULATOR
    static const OSSpinLock _OSSpinLockLocked = 1;
    #else
    static const OSSpinLock _OSSpinLockLocked = -1;
    #endif

    OS_ATOMIC_ALIAS定义如下:

    #undef OS_ATOMIC_ALIAS
    #define OS_ATOMIC_ALIAS(n, o)
    static void _OSSpinLockLock(volatile OSSpinLock *l);

    这里相当于分了两条路径,通过_OSSpinLockLocked标记是否被锁定。在源码中并没有找到_OSSpinLockLock函数的实现。

    5.1.1.1 _OSSpinLockLockSlow

    #if OS_ATOMIC_UP
    void
    _OSSpinLockLockSlow(volatile OSSpinLock *l)
    {
    return _OSSpinLockLockYield(l); // Don't spin on UP
    }
    #elif defined(__arm64__)
    // Exclusive monitor must be held during WFE <rdar://problem/22300054>
    #if defined(__ARM_ARCH_8_2__)
    void
    _OSSpinLockLockSlow(volatile OSSpinLock *l)
    {
    uint32_t tries = OS_LOCK_SPIN_SPIN_TRIES;
    OSSpinLock lock;
    _spin:
    while (unlikely(lock = os_atomic_load_exclusive(l, relaxed))) {
    if (unlikely(lock != _OSSpinLockLocked)) {
    os_atomic_clear_exclusive();
    return _os_lock_corruption_abort((void *)l, (uintptr_t)lock);
    }
    if (unlikely(!tries--)) {
    os_atomic_clear_exclusive();
    return _OSSpinLockLockYield(l);
    }
    OS_LOCK_SPIN_PAUSE();
    }
    os_atomic_clear_exclusive();
    bool r = os_atomic_cmpxchg(l, 0, _OSSpinLockLocked, acquire);
    if (likely(r)) return;
    goto _spin;
    }
    #else // !__ARM_ARCH_8_2__
    void
    _OSSpinLockLockSlow(volatile OSSpinLock *l)
    {
    uint32_t tries = OS_LOCK_SPIN_SPIN_TRIES;
    OSSpinLock lock;
    os_atomic_rmw_loop(l, lock, _OSSpinLockLocked, acquire, if (unlikely(lock)){
    if (unlikely(lock != _OSSpinLockLocked)) {
    os_atomic_rmw_loop_give_up(return
    _os_lock_corruption_abort((void *)l, (uintptr_t)lock));
    }
    if (unlikely(!tries--)) {
    os_atomic_rmw_loop_give_up(return _OSSpinLockLockYield(l));
    }
    OS_LOCK_SPIN_PAUSE();
    continue;
    });
    }
    #endif // !__ARM_ARCH_8_2__
    #else // !OS_ATOMIC_UP
    void
    _OSSpinLockLockSlow(volatile OSSpinLock *l)
    {
    uint32_t tries = OS_LOCK_SPIN_SPIN_TRIES;
    OSSpinLock lock;
    while (unlikely(lock = *l)) {
    _spin:
    if (unlikely(lock != _OSSpinLockLocked)) {
    return _os_lock_corruption_abort((void *)l, (uintptr_t)lock);
    }
    if (unlikely(!tries--)) return _OSSpinLockLockYield(l);
    OS_LOCK_SPIN_PAUSE();
    }
    bool r = os_atomic_cmpxchgv(l, 0, _OSSpinLockLocked, &lock, acquire);
    if (likely(r)) return;
    goto _spin;
    }
    #endif // !OS_ATOMIC_UP

    可以看到内部有自转逻辑,这里直接分析_OSSpinLockLockYield

    5.1.1.2 _OSSpinLockLockYield

    static void
    _OSSpinLockLockYield(volatile OSSpinLock *l)
    {
    int option = SWITCH_OPTION_DEPRESS;
    mach_msg_timeout_t timeout = 1;
    uint64_t deadline = _os_lock_yield_deadline(timeout);
    OSSpinLock lock;
    while (unlikely(lock = *l)) {
    _yield:
    if (unlikely(lock != _OSSpinLockLocked)) {
    _os_lock_corruption_abort((void *)l, (uintptr_t)lock);
    }
    thread_switch(MACH_PORT_NULL, option, timeout);
    if (option == SWITCH_OPTION_WAIT) {
    timeout++;
    } else if (!_os_lock_yield_until(deadline)) {
    option = SWITCH_OPTION_WAIT;
    }
    }
    bool r = os_atomic_cmpxchgv(l, 0, _OSSpinLockLocked, &lock, acquire);
    if (likely(r)) return;
    goto _yield;
    }

    内部有超时时间以及线程切换逻辑。

    5.1.2 OSSpinLockUnlock

    void
    OSSpinLockUnlock(volatile OSSpinLock *l)
    {
    OS_ATOMIC_ALIAS(spin_unlock, OSSpinLockUnlock);
    OS_ATOMIC_ALIAS(_spin_unlock, OSSpinLockUnlock);
    return _os_nospin_lock_unlock((_os_nospin_lock_t)l);
    }

    内部调用了_os_nospin_lock_unlock

    5.1.2.1 _os_nospin_lock_unlock

    void
    _os_nospin_lock_unlock(_os_nospin_lock_t l)
    {
    os_lock_owner_t self = _os_lock_owner_get_self();
    os_ulock_value_t current;
    current = os_atomic_xchg(&l->oul_value, OS_LOCK_NO_OWNER, release);
    if (likely(current == self)) return;
    return _os_nospin_lock_unlock_slow(l, current);
    }

    _os_nospin_lock_unlock_slow

    static void
    _os_nospin_lock_unlock_slow(_os_nospin_lock_t l, os_ulock_value_t current)
    {
    os_lock_owner_t self = _os_lock_owner_get_self();
    if (unlikely(OS_ULOCK_OWNER(current) != self)) {
    return; // no unowned_abort for drop-in compatibility with OSSpinLock
    }
    if (current & OS_ULOCK_NOWAITERS_BIT) {
    __LIBPLATFORM_INTERNAL_CRASH__(current, "unlock_slow with no waiters");
    }
    for (;;) {
    int ret = __ulock_wake(UL_COMPARE_AND_WAIT | ULF_NO_ERRNO, l, 0);
    if (unlikely(ret < 0)) {
    switch (-ret) {
    case EINTR:
    continue;
    case ENOENT:
    break;
    default:
    __LIBPLATFORM_INTERNAL_CRASH__(-ret, "ulock_wake failure");
    }
    }
    break;
    }
    }

    5.2 os_unfair_lock 源码分析

    typedef struct os_unfair_lock_s {
    uint32_t _os_unfair_lock_opaque;
    } os_unfair_lock, *os_unfair_lock_t;

    初始化OS_UNFAIR_LOCK_INIT直接设置了默认值0

    #define OS_UNFAIR_LOCK_INIT ((os_unfair_lock){0})

    5.2.1 os_unfair_lock_lock


    void
    os_unfair_lock_lock(os_unfair_lock_t lock)
    {
    _os_unfair_lock_t l = (_os_unfair_lock_t)lock;
    os_lock_owner_t self = _os_lock_owner_get_self();
    bool r = os_atomic_cmpxchg(&l->oul_value, OS_LOCK_NO_OWNER, self, acquire);
    if (likely(r)) return;
    return _os_unfair_lock_lock_slow(l, OS_UNFAIR_LOCK_NONE, self);
    }

    _os_lock_owner_get_self


    OS_ALWAYS_INLINE OS_CONST
    static inline os_lock_owner_t
    _os_lock_owner_get_self(void)
    {
    os_lock_owner_t self;
    self = (os_lock_owner_t)_os_tsd_get_direct(__TSD_MACH_THREAD_SELF);
    return self;
    }

    _os_unfair_lock_lock_slow

    static void
    _os_unfair_lock_lock_slow(_os_unfair_lock_t l,
    os_unfair_lock_options_t options, os_lock_owner_t self)
    {
    os_unfair_lock_options_t allow_anonymous_owner =
    options & OS_UNFAIR_LOCK_ALLOW_ANONYMOUS_OWNER;
    options &= ~OS_UNFAIR_LOCK_ALLOW_ANONYMOUS_OWNER;
    if (unlikely(options & ~OS_UNFAIR_LOCK_OPTIONS_MASK)) {
    __LIBPLATFORM_CLIENT_CRASH__(options, "Invalid options");
    }
    os_ulock_value_t current, new, waiters_mask = 0;
    while (unlikely((current = os_atomic_load(&l->oul_value, relaxed)) !=
    OS_LOCK_NO_OWNER)) {
    _retry:
    if (unlikely(OS_ULOCK_IS_OWNER(current, self, allow_anonymous_owner))) {
    return _os_unfair_lock_recursive_abort(self);
    }
    new = current & ~OS_ULOCK_NOWAITERS_BIT;
    if (current != new) {
    // Clear nowaiters bit in lock value before waiting
    if (!os_atomic_cmpxchgv(&l->oul_value, current, new, &current,
    relaxed)){
    continue;
    }
    current = new;
    }
    int ret = __ulock_wait(UL_UNFAIR_LOCK | ULF_NO_ERRNO | options,
    l, current, 0);
    if (unlikely(ret < 0)) {
    switch (-ret) {
    case EINTR:
    case EFAULT:
    continue;
    case EOWNERDEAD:
    _os_unfair_lock_corruption_abort(current);
    break;
    default:
    __LIBPLATFORM_INTERNAL_CRASH__(-ret, "ulock_wait failure");
    }
    }
    if (ret > 0) {
    // If there are more waiters, unset nowaiters bit when acquiring lock
    waiters_mask = OS_ULOCK_NOWAITERS_BIT;
    }
    }
    new = self & ~waiters_mask;
    bool r = os_atomic_cmpxchgv(&l->oul_value, OS_LOCK_NO_OWNER, new,
    &current, acquire);
    if (unlikely(!r)) goto _retry;
    }

    内部是wait等待逻辑。

    5.2.2 os_unfair_lock_unlock

    void
    os_unfair_lock_unlock(os_unfair_lock_t lock)
    {
    _os_unfair_lock_t l = (_os_unfair_lock_t)lock;
    os_lock_owner_t self = _os_lock_owner_get_self();
    os_ulock_value_t current;
    current = os_atomic_xchg(&l->oul_value, OS_LOCK_NO_OWNER, release);
    if (likely(current == self)) return;
    return _os_unfair_lock_unlock_slow(l, self, current, 0);
    }

    内部调用了_os_unfair_lock_unlock_slow

    OS_NOINLINE
    static void
    _os_unfair_lock_unlock_slow(_os_unfair_lock_t l, os_lock_owner_t self,
    os_ulock_value_t current, os_unfair_lock_options_t options)
    {
    os_unfair_lock_options_t allow_anonymous_owner =
    options & OS_UNFAIR_LOCK_ALLOW_ANONYMOUS_OWNER;
    options &= ~OS_UNFAIR_LOCK_ALLOW_ANONYMOUS_OWNER;
    if (unlikely(OS_ULOCK_IS_NOT_OWNER(current, self, allow_anonymous_owner))) {
    return _os_unfair_lock_unowned_abort(OS_ULOCK_OWNER(current));
    }
    if (current & OS_ULOCK_NOWAITERS_BIT) {
    __LIBPLATFORM_INTERNAL_CRASH__(current, "unlock_slow with no waiters");
    }
    for (;;) {
    int ret = __ulock_wake(UL_UNFAIR_LOCK | ULF_NO_ERRNO, l, 0);
    if (unlikely(ret < 0)) {
    switch (-ret) {
    case EINTR:
    continue;
    case ENOENT:
    break;
    default:
    __LIBPLATFORM_INTERNAL_CRASH__(-ret, "ulock_wake failure");
    }
    }
    break;
    }
    }

    可以看到内部是有唤醒逻辑的。

    六、读写锁

    读操作可以共享,写操作是排他的,读可以有多个在读,写只有唯一个在写,同时写的时候不允许读。要实现读写锁核心逻辑是:

    • 多读单写
    • 写写互斥
    • 读写互斥
    • 写不能阻塞任务执行

    有两套方案:

    • 1.使用 栅栏函数 相关API
    • 2.使用pthread_rwlock_t相关API

    6.1 dispatch_barrier_async 实现多读单写

    写:通过栅栏函数可以实现写写互斥以及读写互斥,写使用async可以保证写逻辑不阻塞当前任务执行。
    读:使用dispatch_sync同步效果实现多读(放入并发队列中)。

    • 首先定义一个并发队列以及字典存储数据:
    @property (nonatomic, strong) dispatch_queue_t concurrent_queue;
    @property (nonatomic, strong) NSMutableDictionary *dataDic;

    //初始化
    self.concurrent_queue = dispatch_queue_create("rw_queue", DISPATCH_QUEUE_CONCURRENT);
    self.dataDic = [NSMutableDictionary dictionary];
    • 写入操作:
    - (void)safeSetter:(NSString *)name time:(int)time {
    dispatch_barrier_async(self.concurrent_queue, ^{
    sleep(time);
    [self.dataDic setValue:name forKey:@"HotpotCat"];
    NSLog(@"write name:%@,currentThread:%@",name,[NSThread currentThread]);
    });
    }

    为了方便测试key值写死,并且传入一个timebarrier保证了写之间互斥以及读写互斥。

    • 读取操作:
    - (NSString *)safeGetterWithTime:(int)time {
    __block NSString *result;
    //多条线程同时读,阻塞的是当前线程,多条线程访问就是多读了。同步使用concurrent_queue是为了配合栅栏函数读写互斥。
    dispatch_sync(self.concurrent_queue, ^{
    sleep(time);
    result = self.dataDic[@"HotpotCat"];
    });
    NSLog(@"result:%@,currentThread:%@,time:%@",result,[NSThread currentThread],@(time));
    return result;
    }

    使用同步函数配合栅栏函数(栅栏函数只能针对同一队列)实现读写互斥,当多条线程同时访问safeGetterWithTime时就实现了多读操作。

    • 写入验证:
    //调用
    [self safeSetter:@"1" time:4];
    [self safeSetter:@"2" time:1];
    [self safeSetter:@"3" time:2];
    [self safeSetter:@"4" time:1];

    输出:

    write name:1,currentThread:<NSThread: 0x281fea4c0>{number = 5, name = (null)}
    write name:2,currentThread:<NSThread: 0x281fea4c0>{number = 5, name = (null)}
    write name:3,currentThread:<NSThread: 0x281fea4c0>{number = 5, name = (null)}
    write name:4,currentThread:<NSThread: 0x281fea4c0>{number = 5, name = (null)}

    很明显写之间是互斥的,任务1没有执行完之前其它任务都在等待。


    • 读取验证:
    for (int i = 0; i < 5; i++) {
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
    NSString *result = [self safeGetterWithTime:5 - i];
    NSLog(@"result:%@",result);
    });
    }

    输出:


    result:4,currentThread:<NSThread: 0x281f80600>{number = 7, name = (null)},time:1
    result:4,currentThread:<NSThread: 0x281fce540>{number = 8, name = (null)},time:2
    result:4,currentThread:<NSThread: 0x281f80980>{number = 9, name = (null)},time:3
    result:4,currentThread:<NSThread: 0x281feb540>{number = 10, name = (null)},time:4
    result:4,currentThread:<NSThread: 0x281f80a80>{number = 11, name = (null)},time:5

    任务并行执行,顺序是由于设置了sleep时间,如果去掉时间或者时间一致,每次执行结果都不同了。

    6.2 pthread_rwlock_t 实现多读单写

    • 定义锁以及字典数据:
    {
    pthread_rwlock_t rw_lock;
    pthread_rwlockattr_t rw_lock_attr;
    }
    @property (nonatomic, strong) NSMutableDictionary *dataDic;

    pthread_rwlockattr_t读写属性有两种:lockkindpshared
    lockkind:读写策略,包括读取优先(默认属性)、写入优先。苹果系统里面没有提供 pthread_rwlockattr_getkind_np 与 pthread_rwlockattr_setkind_np 相关函数。
    psharedPTHREAD_PROCESS_PRIVATE(进程内竞争读写锁,默认属性)PTHREAD_PROCESS_SHARED(进程间竞争读写锁)

    • 初始化:
    self.dataDic = [NSMutableDictionary dictionary];
    //初始化
    pthread_rwlockattr_init(&rw_lock_attr);
    pthread_rwlock_init(&rw_lock, &rw_lock_attr);
    //进程内
    pthread_rwlockattr_setpshared(&rw_lock_attr, PTHREAD_PROCESS_PRIVATE);
    • 写入操作如下:
    - (void)safeSetter:(NSString *)name {
    //写锁
    pthread_rwlock_wrlock(&rw_lock);
    [self.dataDic setValue:name forKey:@"HotpotCat"];
    NSLog(@"write name:%@,currentThread:%@",name,[NSThread currentThread]);
    //释放
    pthread_rwlock_unlock(&rw_lock);
    }
    • 读取操作如下:
    - (NSString *)safeGetter {
    //读锁
    pthread_rwlock_rdlock(&rw_lock);
    NSString *result = self.dataDic[@"HotpotCat"];
    //释放
    pthread_rwlock_unlock(&rw_lock);
    NSLog(@"result:%@,currentThread:%@",result,[NSThread currentThread]);
    return result;
    }
    • 写入验证:
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
    [self safeSetter:@"1"];
    });
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
    [self safeSetter:@"2"];
    });
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
    [self safeSetter:@"3"];
    });
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
    [self safeSetter:@"4"];
    });

    输出:

    LockDemo[52251:5873172] write name:4,currentThread:<NSThread: 0x60000072e980>{number = 4, name = (null)}
    LockDemo[52251:5873177] write name:1,currentThread:<NSThread: 0x60000075d100>{number = 6, name = (null)}
    LockDemo[52251:5873170] write name:2,currentThread:<NSThread: 0x60000072f600>{number = 7, name = (null)}
    LockDemo[52251:5873178] write name:3,currentThread:<NSThread: 0x60000073d480>{number = 5, name = (null)}

    这里就与队列调度有关了,顺序不定,如果不加锁大量并发调用下则会crash

    • 读取验证:
    for (int i = 0; i < 5; i++) {
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
    NSString *result = [self safeGetter];
    });
    }

    输出:

    result:4,currentThread:<NSThread: 0x600001cdc200>{number = 5, name = (null)}
    result:4,currentThread:<NSThread: 0x600001cd1080>{number = 7, name = (null)}
    result:4,currentThread:<NSThread: 0x600001c95f40>{number = 6, name = (null)}
    result:4,currentThread:<NSThread: 0x600001c91ec0>{number = 3, name = (null)}
    result:4,currentThread:<NSThread: 0x600001c94d80>{number = 4, name = (null)}

    输出顺序也不一定。当然混合读写测试也可以,用数组更容易测试。


  • 对于读数据比修改数据频繁的应用,用读写锁代替互斥锁可以提高效率。因为使用互斥锁时,即使是读出数据(相当于操作临界区资源)都要上互斥锁,而采用读写锁,则可以在任一时刻允许多个读出者存在,提高了更高的并发度,同时在某个写入者修改数据期间保护该数据,以免任何其它读出者或写入者的干扰。

  • 获取一个读写锁用于读称为共享锁,获取一个读写锁用于写称为独占锁,因此对于某个给定资源的共享访问也称为共享-独占上锁






  • 作者:HotPotCat
    链接:https://www.jianshu.com/p/8f8e5f0d0b23





    收起阅读 »

    高级IOS开发进阶 - 自旋锁、互斥锁以及读写锁(一)

    一、锁的分类在分析其它锁之前,需要先区分清楚锁的区别,基本的锁包括了二类:互斥锁 和 自旋锁。1.1 自旋锁自旋锁:线程反复检查锁变量是否可用。由于线程在这一过程中保持执行, 因此是一种 忙等。一旦获取了自旋锁,线程会一直保持该锁...
    继续阅读 »

    一、锁的分类

    在分析其它锁之前,需要先区分清楚锁的区别,基本的锁包括了二类:互斥锁 和 自旋锁

    1.1 自旋锁

    自旋锁:线程反复检查锁变量是否可用。由于线程在这一过程中保持执行, 因此是一种 忙等。一旦获取了自旋锁,线程会一直保持该锁,直至显式释放自旋锁。 自旋锁避免了进程上下文的调度开销,因此对于线程只会阻塞很短时间的场合是有效的。

    自旋锁 = 互斥锁 + 忙等OSSpinLock就是自旋锁。

    1.2 互斥锁

    互斥锁:是一种用于多线程编程中,防止两条线程同时对同一公共资源(比如全局变量)进行读写的机制。该目的通过将代码切片成一个一个的临界区而达成。
    Posix Thread中定义有一套专⻔用于线程同步的mutex函数,mutex用于保证在任何时刻,都只能有一个线程访问该对象。 当获取锁操作失败时,线程会进入睡眠,等待锁释放时被唤醒(闲等)。

    创建和销毁:

    • POSIX定义了一个宏PTHREAD_MUTEX_INITIALIZER来静态初始化互斥锁。
    int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr)
    • pthread_mutex_destroy ()用于注销一个互斥锁。

    锁操作相关API

     int pthread_mutex_lock(pthread_mutex_t *mutex)
    int pthread_mutex_unlock(pthread_mutex_t *mutex)
    int pthread_mutex_trylock(pthread_mutex_t *mutex)
    • pthread_mutex_trylock()语义与pthread_mutex_lock()类似,不同的是在锁已经被占据时返回EBUSY而不是挂起等待。

    互斥锁 分为 递归锁 和 非递归锁

    • 递归锁:
      @synchronized:多线程可递归。
      NSRecursiveLock:不支持多线程可递归。
      pthread_mutex_t(recursive):多线程可递归。
    • 非递归锁:NSLockpthread_mutexdispatch_semaphoreos_unfair_lock
    • 条件锁:NSConditionNSConditionLock
    • 信号量(semaphore):是一种更高级的同步机制,互斥锁可以说是
      semaphore在仅取值0/1时的特例。信号量可以有更多的取值空间,用来实
      现更加复杂的同步,而不单单是线程间互斥。dispatch_semaphore

    1.2.1 读写锁

    读写锁实际是一种特殊的互斥锁,它把对共享资源的访问者划分成读者和写者,读者只对共享资源进行读访问,写者则需要对共享资源进行写操作。这种锁 相对于自旋锁而言,能提高并发性。因为在多处理器系统中,它允许同时有多个读者来访问共享资源,最大可能的读者数为实际的逻辑CPU 数。写者是排他性的,一个读写锁同时只能有一个写者或多个读者(与CPU数相关),但不能同时既有读者又有写者,在读写锁保持期间也是抢占失效的。

    如果读写锁当前没有读者,也没有写者,那么写者可以立刻获得读写锁,否则它必须自旋在那里, 直到没有任何写者或读者。如果读写锁没有写者,那么读者可以立即获得该读写锁,否则读者必须自旋在那里,直到写者释放该读写锁。

    一次只有一个线程可以占有写模式的读写锁,可以有多个线程同时占有读模式的读写锁。正是因为这个特性,当读写锁是写加锁状态时,在这个锁被解锁之前, 所有试图对这个锁加锁的线程都会被阻塞。当读写锁在读加锁状态时, 所有试图以读模式对它进行加锁的线程都可以得到访问权, 但是如果线程希望以写模式对此锁进行加锁, 它必须直到所有的线程释放锁。
    通常当读写锁处于读模式锁住状态时,如果有另外线程试图以写模式加锁,读写锁通常会阻塞随后的读模式锁请求,这样可以避免读模式锁⻓期占用而导致等待的写模式锁请求⻓期阻塞。
    读写锁适合于对数据结构的读次数比写次数多得多的情况。 因为读模式锁定可以共享,写模式锁住时意味着独占,所以读写锁又叫共享-独占锁

    创建和销毁API

    #include <pthread.h>
    int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr);
    int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);

    成功则返回0, 出错则返回错误编号。

    同互斥锁一样, 在释放读写锁占用的内存之前, 需要先通过pthread_rwlock_destroy对读写锁进行清理工作,释放由init分配的资源。

    锁操作相关API:

    #include <pthread.h>
    int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
    int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
    int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
    成功则返回0, 出错则返回错误编号。这3个函数分别实现获取读锁,获取写锁和释放锁的操作。获取锁的两个函数是阻塞操作,同样非阻塞的函数为:

    #include <pthread.h>
    int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
    int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);

    非阻塞的获取锁操作,如果可以获取则返回0, 否则返回错误的EBUSY

    二、NSLock & NSRecursiveLock 的应用以及原理

    2.1 案例一

    __block NSMutableArray *array;
    for (int i = 0; i < 10000; i++) {
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
    array = [NSMutableArray array];
    });
    }

    对于上面的代码运行会发生崩溃,常规处理是对它加一个锁,如下:

    __block NSMutableArray *array;
    self.lock = [[NSLock alloc] init];
    for (int i = 0; i < 10000; i++) {
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
    [self.lock lock];
    array = [NSMutableArray array];
    [self.lock unlock];
    });
    }

    这样就能解决array的创建问题了。

    2.2 案例二

    for (int i = 0; i < 10; i++) {
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
    static void (^testMethod)(int);
    testMethod = ^(int value){
    if (value > 0) {
    NSLog(@"current value = %d",value);
    testMethod(value - 1);
    }
    };
    testMethod(10);
    });
    }

    上面的例子中最终输出会错乱:



    可以在block调用前后加解锁解决:

    for (int i = 0; i < 10; i++) {
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
    static void (^testMethod)(int);
    testMethod = ^(int value){
    if (value > 0) {
    NSLog(@"current value = %d",value);
    testMethod(value - 1);
    }
    };
    [self.lock lock];
    testMethod(10);
    [self.lock unlock];
    });
    }

    但是在实际开发中锁往往是与业务代码绑定在一起的,如下:


    这个时候block在执行前会同一时间进入多次,相当于多次加锁了(递归),这样就产生了死锁。NSLog只会执行一次。

    NSLock改为NSRecursiveLock可以解决NSLock存在的死锁问题:


    for (int i = 0; i < 10; i++) {
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
    static void (^testMethod)(int);
    testMethod = ^(int value){
    [self.recursiveLock lock];
    if (value > 0) {
    NSLog(@"current value = %d",value);
    testMethod(value - 1);
    }
    [self.recursiveLock unlock];
    };
    testMethod(10);
    });
    }

    但是在执行testMethod一次(也有可能是多次)递归调用后没有继续输出:



    由于NSRecursiveLock不支持多线程可递归。所以改为@synchronized

        for (int i = 0; i < 10; i++) {
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
    static void (^testMethod)(int);
    testMethod = ^(int value){
    @synchronized (self) {
    if (value > 0) {
    NSLog(@"current value = %d",value);
    testMethod(value - 1);
    }
    }
    };
    testMethod(10);
    });
    }

    就能完美解决问题了。

    NSRecursiveLock 解决了 NSLock 递归问题,@synchronized 解决了 NSRecursiveLock 多线程可递归问题问题。

    2.3 原理分析

    NSLockNSRecursiveLock是定义在Foundation框架中的,Foundation框架并没有开源。有三种方式来探索:

    • 分析Foundation动态库的汇编代码。
    • 断点跟踪加锁解锁流程。
    • Swift Foundation源码分析。虽然Foundation框架本身没有,但是苹果开源了Swift Foundation的代码。原理是想通的。swift-corelibs-foundation

    当然有兴趣可以尝试编译可运行版本进行调试 swift-foundation 源码编译

    FoundationlockunlockNSLocking协议提供的方法:


    @protocol NSLocking
    - (void)lock;
    - (void)unlock;
    @end

    Swift Foundation源码中同样有NSLocking协议:

    public protocol NSLocking {
    func lock()
    func unlock()
    }

    2.3.1 NSLock 源码分析



    底层是对pthread_mutex_init的封装。lockunlock同样是对pthread_mutex_lockpthread_mutex_unlock的封装:



    通过宏定义可以看到Swift的跨平台支持。

    2.3.2 NSRecursiveLock 源码分析



    内部是对PTHREAD_MUTEX_RECURSIVE的封装。lockunlock同样是对pthread_mutex_lockpthread_mutex_unlock的封装。

    三、NSCondition 原理

    NSCondition实际上作为一个  和一个 线程检查器。锁主要为了当检测条件时保护数据源,执行条件引发的任务;线程检查器主要是根据条件决定是否继续运行线程,即线程是否被阻塞。

    • [condition lock]:一般用于多线程同时访问、修改同一个数据源,保证在同一 时间内数据源只被访问、修改一次,其他线程的命令需要在lock 外等待,只到 unlock后才可访问。
    • [condition unlock]:与lock同时使用。
    • [condition wait]:让当前线程处于等待状态。
    • [condition signal]CPU发信号告诉线程不用在等待,可以继续执行。

    3.1 生产者-消费者 案例

    - (void)testNSCondition {
    //创建生产-消费者
    for (int i = 0; i < 10; i++) {
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
    [self test_producer];
    });
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
    [self test_consumer];
    });

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
    [self test_consumer];
    });
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
    [self test_producer];
    });
    }
    }

    - (void)test_producer{
    [self.condition lock];
    self.ticketCount = self.ticketCount + 1;
    NSLog(@"生产 + 1 剩余: %zd",self.ticketCount);
    [self.condition signal]; // 信号
    [self.condition unlock];
    }

    - (void)test_consumer{
    [self.condition lock];
    if (self.ticketCount == 0) {
    NSLog(@"等待 剩余: %zd",self.ticketCount);
    [self.condition wait];
    }
    //消费行为,要在等待条件判断之后
    self.ticketCount -= 1;
    NSLog(@"消费 - 1 剩余: %zd ",self.ticketCount);
    [self.condition unlock];
    }

    输出:

    生产 + 1 剩余: 1
    消费 - 1 剩余: 0
    等待 剩余: 0
    生产 + 1 剩余: 1
    消费 - 1 剩余: 0
    等待 剩余: 0
    等待 剩余: 0
    等待 剩余: 0
    生产 + 1 剩余: 1
    消费 - 1 剩余: 0
    等待 剩余: 0
    生产 + 1 剩余: 1
    消费 - 1 剩余: 0
    等待 剩余: 0
    等待 剩余: 0
    生产 + 1 剩余: 1
    消费 - 1 剩余: 0
    生产 + 1 剩余: 1
    消费 - 1 剩余: 0
    生产 + 1 剩余: 1
    消费 - 1 剩余: 0
    等待 剩余: 0
    等待 剩余: 0
    生产 + 1 剩余: 1
    消费 - 1 剩余: 0
    生产 + 1 剩余: 1
    消费 - 1 剩余: 0
    等待 剩余: 0
    生产 + 1 剩余: 1
    消费 - 1 剩余: 0
    生产 + 1 剩余: 1
    消费 - 1 剩余: 0
    生产 + 1 剩余: 1
    生产 + 1 剩余: 2
    消费 - 1 剩余: 1
    消费 - 1 剩余: 0
    生产 + 1 剩余: 1
    生产 + 1 剩余: 2
    生产 + 1 剩余: 3
    消费 - 1 剩余: 2
    生产 + 1 剩余: 3
    消费 - 1 剩余: 2
    生产 + 1 剩余: 3
    消费 - 1 剩余: 2
    消费 - 1 剩余: 1
    生产 + 1 剩余: 2
    生产 + 1 剩余: 3
    消费 - 1 剩余: 2
    消费 - 1 剩余: 1
    消费 - 1 剩余: 0

    因为有condition的存在保证了消费行为是在对应的生产行为之后。在这个过程中会有消费等待行为,signal信号通知消费。

    • 生产和消费的加锁保证了各个事务的额安全。
    • waitsignal保证了事务之间的安全。

    3.2 源码分析




    内部也是对pthread_mutex_init的包装,多了一个pthread_cond_init

    open func lock() {
    pthread_mutex_lock(mutex)
    }

    open func unlock() {
    pthread_mutex_unlock(mutex)
    }

    open func wait() {
    pthread_cond_wait(cond, mutex)
    }

    open func signal() {
    pthread_cond_signal(cond)
    }

    open func broadcast() {
    pthread_cond_broadcast(cond)
    }

    代码中去掉了windows相关宏逻辑:

    • NSCondition:锁(pthread_mutex_t) + 线程检查器(pthread_cond_t
    • 锁(pthread_mutex_t):lock(pthread_mutex_lock) + unlock(pthread_mutex_unlock)
    • 线程检查器(pthread_cond_t):wait(pthread_cond_wait) + signal(pthread_cond_signal)

    四、NSConditionLock 使用和原理

    NSConditionLock也是锁,一旦一个线程获得锁,其他线程一定等待。它同样遵循NSLocking协议,相关API:

    - (void)lockWhenCondition:(NSInteger)condition;
    - (void)unlockWithCondition:(NSInteger)condition;
    - (BOOL)lockWhenCondition:(NSInteger)condition beforeDate:(NSDate *)limit;
    • [conditionLock lock]:表示 conditionLock 期待获得锁,如果没有其他线程获得锁(不需要判断内部的condition) 那它能执行此行以下代码,如果已经有其他线程获得锁(可能是条件锁,或者无条件锁),则等待,直至其他线程解锁。
    • [conditionLock lockWhenCondition:A条件]:表示如果没有其他线程获得该锁,但是该锁内部的 condition不等于A条件,它依然不能获得锁,仍然等待。如果内部的condition等于A条件,并且没有其他线程获得该锁,则进入代码区,同时设置它获得该锁,其他任何线程都将等待它代码的完成,直至它解锁。
    • [conditionLock unlockWithCondition:A条件]: 表示释放锁,同时把内部的condition设置为A条件。
    • return = [conditionLock lockWhenCondition:A条件 beforeDate:A时间]: 表示如果被锁定(没获得锁),并超过该时间则不再阻塞线程。注意:返回的值是NO,它没有改变锁的状态,这个函数的目的在于可以实现两种状态下的处理。
    • 所谓的condition就是整数,内部通过整数比较条件。

    4.1案例

    NSConditionLock *conditionLock = [[NSConditionLock alloc] initWithCondition:2];
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
    [conditionLock lockWhenCondition:1];
    NSLog(@"1");
    [conditionLock unlockWithCondition:0];
    });

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
    [conditionLock lockWhenCondition:2];
    sleep(1);
    NSLog(@"2");
    [conditionLock unlockWithCondition:1];
    });

    dispatch_async(dispatch_get_global_queue(0, 0), ^{
    [conditionLock lock];
    NSLog(@"3");
    [conditionLock unlock];
    });

    上面的案例2一定比1先执行,23之间无序。
    输出:3 2 1,如果任务2的优先级改为High则输出顺序变为2 1 3

    那么有以下疑问:

    • 1.NSConditionLock 与 NSCondition有关系么?
    • 2.NSConditionLock初始化的时候condition是什么?
    • 3.lockWhenCondition是如何控制的?
    • 4.unlockWithCondition是如何控制的?

    4.2 断点调试分析逻辑

    在拿不到源码以及拿不到动态库的情况下,断点分析调用流程是一个比较好的方案。
    分别在测试代码中打下以下断点:



    运行工程到达断点后下符号断点-[NSConditionLock initWithCondition:]过掉断点:



    这个时候就进入了initWithCondition的汇编实现。在汇编中对所有的b(跳转指令)下断点配合寄存器的值跟踪流程。

    • -[NSConditionLock initWithCondition:]:


    • 可以通过lldb读取寄存器的值,也可以查看全部寄存器中对应的值。

    过掉断点继续:






    最终返回了创建的NSConditionLock对象,它持有NSCondition对象以及初始化传的condition参数2
    -[NSConditionLock initWithCondition:]流程:

    -[NSConditionLock initWithCondition:]
    -[xxx init]
    -[NSConditionLock init]
    -[NSConditionLock zone]
    +[NSCondition allocWithZone:]
    -[NSCondition init]
    -[NSConditionLock lockWhenCondition:]
    同样添加-[NSConditionLock lockWhenCondition:]符号断点:






    调用了-[NSCondition unlock],这个时候继续过断点就又会回到线程4,调用逻辑和线程3相同。

    完整调用逻辑如下:

    线程4
    -[NSConditionLock lockWhenCondition:]
    +[NSDate distantFuture]
    -[NSConditionLock lockWhenCondition:beforeDate:]
    -[NSCondition lock]
    -[NSCondition waitUntilDate:]
    线程3
    -[NSConditionLock lockWhenCondition:]
    +[NSDate distantFuture]
    -[NSConditionLock lockWhenCondition:beforeDate:]
    -[NSCondition lock]
    -[NSCondition unlock]
    返回1(true)
    -[NSConditionLock unlockWithCondition:]
    -[NSCondition lock]
    -[NSCondition broadcast]
    -[NSCondition unlock]
    //回到线程4
    -[NSCondition unlock]
    返回1(true)
    -[NSConditionLock unlockWithCondition:]
    -[NSCondition lock]
    -[NSCondition broadcast]
    -[NSCondition unlock]


    流程总结:

    • 线程4调用[NSConditionLock lockWhenCondition:],此时因为不满足当前条件,所
      以会进入 waiting 状态,当前进入到 waiting 时,会释放当前的互斥锁。
    • 此时当前的线程2 调用[NSConditionLock lock:],本质上是调用[NSConditionLock lockBeforeDate:]这里不需要比对条件值,所以任务3会执行。
    • 接下来线程3 执行[NSConditionLock lockWhenCondition:],因为满足条件值,所以线任务2会执行,执行完成后会调用[NSConditionLock unlockWithCondition:],这个时候将
      condition 设置为 1,并发送 boradcast, 此时线程 4接收到当前的信号,唤醒执行并打印。
    • 这个时候任务执行顺序为任务3 -> 任务2 -> 任务1
    • [NSConditionLock lockWhenCondition:]会根据传入的 condition
      行对比,如果不相等,这里就会阻塞,进入线程池,否则的话就继续代码执行。
    • [NSConditionLock unlockWithCondition:] 会先更改当前的condition值,然后进行广播,唤醒当前的线程。


    作者:HotPotCat
    链接:https://www.jianshu.com/p/8f8e5f0d0b23
    收起阅读 »

    锁的原理(二):@synchronized

    3.1 SyncData存储结构#define LOCK_FOR_OBJ(obj) sDataLists[obj].lock #define LIST_FOR_OBJ(obj) sDataLists[obj].data static StripedMap<...
    继续阅读 »

    3.1 SyncData存储结构

    #define LOCK_FOR_OBJ(obj) sDataLists[obj].lock
    #define LIST_FOR_OBJ(obj) sDataLists[obj].data
    static StripedMap<SyncList> sDataLists;

    //本身也是 os_unfair_lock
    spinlock_t *lockp = &LOCK_FOR_OBJ(object);
    SyncData **listp = &LIST_FOR_OBJ(object);
    SyncData* result = NULL;
    可以看到锁和SyncData都是从sDataLists获取的(hash map结构,存储的是SyncList),SyncList定义如下:

    struct SyncList {
    SyncData *data;
    spinlock_t lock;
    constexpr SyncList() : data(nil), lock(fork_unsafe_lock) { }
    };

    StripedMap定义如下:

    class StripedMap {
    #if TARGET_OS_IPHONE && !TARGET_OS_SIMULATOR
    enum { StripeCount = 8 };
    #else
    enum { StripeCount = 64 };
    #endif

    struct PaddedT {
    T value alignas(CacheLineSize);
    };

    PaddedT array[StripeCount];
    ......
    }

    iOS真机上容量为8,其它平台容量为64SynData根据前面的分析是一个单向链表, 那么可以得到在哈希冲突的时候是采用拉链法解决的。

    增加以下验证代码:

    HPObject *obj = [HPObject alloc];
    HPObject *obj2 = [HPObject alloc];
    HPObject *obj3 = [HPObject alloc];
    dispatch_async(dispatch_queue_create("HotpotCat", DISPATCH_QUEUE_CONCURRENT), ^{
    @synchronized (obj) {
    NSLog(@"obj");
    @synchronized (obj2) {
    NSLog(@"obj2");
    @synchronized (obj3) {
    NSLog(@"obj3");
    }
    }
    }
    });
    • sDataLists包装了array,其中存储的是SyncList集合,SyncListdata中存储的是synData

    3.2 从 TLS 获取 SyncData

      bool fastCacheOccupied = NO;//后续存储的时候用
    //对 pthread_getspecific 的封装,针对线程中第一次调用 @synchronized 是获取不到数据的。
    SyncData *data = (SyncData *)tls_get_direct(SYNC_DATA_DIRECT_KEY);
    if (data) {
    fastCacheOccupied = YES;
    //判断要查找的与存储的object是不是同一个。
    if (data->object == object) {
    // Found a match in fast cache.
    uintptr_t lockCount;

    result = data;
    //获取当前线程对该对象锁了几次
    lockCount = (uintptr_t)tls_get_direct(SYNC_COUNT_DIRECT_KEY);
    if (result->threadCount <= 0 || lockCount <= 0) {
    _objc_fatal("id2data fastcache is buggy");
    }

    switch(why) {
    case ACQUIRE: {//enter 的时候 lockCount + 1,并且存储count到tls
    lockCount++;
    tls_set_direct(SYNC_COUNT_DIRECT_KEY, (void*)lockCount);
    break;
    }
    case RELEASE: //exit的时候 lockCount - 1,并且存储count到tls
    lockCount--;
    tls_set_direct(SYNC_COUNT_DIRECT_KEY, (void*)lockCount);
    //当 count 减少到 0 的情况下清除对应obj的SynData,这里并没有清空count,count在存储新objc的时候直接赋值为1
    if (lockCount == 0) {
    // remove from fast cache
    tls_set_direct(SYNC_DATA_DIRECT_KEY, NULL);
    // atomic because may collide with concurrent ACQUIRE
    //threadCount - 1
    OSAtomicDecrement32Barrier(&result->threadCount);
    }
    break;
    case CHECK:
    // do nothing
    break;
    }

    return result;
    }
    }

    • 通过tls_get_direct(是对_os_tsd_get_direct的封装)获取当前线程存储的SynData数据。
    • 在数据存在的情况下判断标记fastCacheOccupied存在。
    • 判断tls存储的数据是不是当前对象。是当前对象则进行进一步处理,否则结束tls逻辑。
    • 获取对象加锁的次数lockCount
    • enter逻辑:lockCount++并存储在tls
    • exit逻辑:lockCount--并存储在tls
      • lockCount0的时候释放SynData,直接在tls中置为NULL
      • 并且threadCount - 1

    线程局部存储(Thread Local Storage,TLS): 是操作系统为线程单独提供的私有空间,通常只有有限的容量。
    Linux系统下通常通过pthread库中的相关方法进行操作:
    pthread_key_create()
    pthread_getspecific()
    pthread_setspecific()
    pthread_key_delete()

    3.3 从 Cache 获取 SyncData

    tls中没有找到SynData的时候会去Cache中找:


        //获取线程缓存,参数NO 当缓存不存在的时候不进行创建。
    SyncCache *cache = fetch_cache(NO);
    if (cache) {
    unsigned int i;
    for (i = 0; i < cache->used; i++) {
    SyncCacheItem *item = &cache->list[i];
    //找到obj对应的 item
    if (item->data->object != object) continue;

    // Found a match.
    //获取SynData
    result = item->data;
    if (result->threadCount <= 0 || item->lockCount <= 0) {
    _objc_fatal("id2data cache is buggy");
    }

    switch(why) {
    case ACQUIRE://enter lockCount + 1
    item->lockCount++;
    break;
    case RELEASE://exit lockCount - 1
    item->lockCount--;
    if (item->lockCount == 0) {//lockCount = 0 的时候 从cache中移除i的元素,将最后一个元素存储到原先i的位置。used - 1。也就是最后一个位置被标记为未使用了。
    // remove from per-thread cache
    cache->list[i] = cache->list[--cache->used];
    // atomic because may collide with concurrent ACQUIRE
    //threadCount - 1
    OSAtomicDecrement32Barrier(&result->threadCount);
    }
    break;
    case CHECK:
    // do nothing
    break;
    }

    return result;
    }
    }
    • 通过fetch_cache(是对pthread_getspecific的封装)找SyncCache,由于是读取数据,所以找不到的情况下这里不创建。
    • 遍历cache已使用的空间找到obj对应的SyncCacheItem
    • enter的情况下item->lockCount++
    • exit情况下item->lockCount--
      • item->lockCount == 0的时候将cache中这个item替换为cache中最后一个,used -1标记cache中使用的数量,这样就将cache中数据释放了。
      • syndatathreadCount进行-1

    3.3.1 SyncCache

    typedef struct {
    SyncData *data;//数据
    unsigned int lockCount; // 被当前线程加锁次数
    } SyncCacheItem;

    typedef struct SyncCache {
    unsigned int allocated;//总容量
    unsigned int used;//已使用
    SyncCacheItem list[0];//列表
    } SyncCache;
    • SyncCache中存储的是SyncCacheItem的一个listallocated用于记录开辟的总容量,used记录已经使用的容量。
    • SyncCacheItem存储了一个SyncData以及lockCount。记录的是针对当前线程SyncData被锁了多少次。SyncCacheItem存储的对应于TSL快速缓存的SYNC_COUNT_DIRECT_KEYSYNC_DATA_DIRECT_KEY

    3.3.2 fetch_cache

    static SyncCache *fetch_cache(bool create)
    {
    _objc_pthread_data *data;
    //creat用来处理是否新建。
    data = _objc_fetch_pthread_data(create);
    //data不存在直接返回,create为YES的情况下data不会为空
    if (!data) return NULL;
    //syncCache不存在
    if (!data->syncCache) {
    if (!create) {//不允许创建直接返回 NULL
    return NULL;
    } else {
    //允许创建直接 calloc 创建,初始容量为4.
    int count = 4;
    data->syncCache = (SyncCache *)
    calloc(1, sizeof(SyncCache) + count*sizeof(SyncCacheItem));
    data->syncCache->allocated = count;
    }
    }

    // Make sure there's at least one open slot in the list.
    //存满的情况下扩容 2倍扩容。
    if (data->syncCache->allocated == data->syncCache->used) {
    data->syncCache->allocated *= 2;
    data->syncCache = (SyncCache *)
    realloc(data->syncCache, sizeof(SyncCache)
    + data->syncCache->allocated * sizeof(SyncCacheItem));
    }

    return data->syncCache;
    }

    通过_objc_fetch_pthread_data获取_objc_pthread_data_objc_pthread_data存储了SyncCache信息,当然不仅仅是它:




    • data
      不存在直接返回,createYES的情况下data不会为空。
    • syncCache不存在的情况下,允许创建则进行calloc(初始容量4,这里是创建syncCache),否则返回NULL
    • syncCache存满(通过allocatedused判断)的情况下进行2被扩容。

    _objc_fetch_pthread_data

    _objc_pthread_data *_objc_fetch_pthread_data(bool create)
    {
    _objc_pthread_data *data;
    //pthread_getspecific TLS_DIRECT_KEY
    data = (_objc_pthread_data *)tls_get(_objc_pthread_key);
    if (!data && create) {
    //允许创建的的情况下创建
    data = (_objc_pthread_data *)
    calloc(1, sizeof(_objc_pthread_data));
    //保存
    tls_set(_objc_pthread_key, data);
    }

    return data;
    }
    • 通过tls_get获取_objc_pthread_data,不存在并且允许创建的情况下进行calloc创建_objc_pthread_data
    • 创建后保存到tls

    这里的cache也是存储在tls,与tls_get_direct的区别要看二者存取的逻辑,一个调用的是tls_get_direct,一个是tls_get


    #if defined(__PTK_FRAMEWORK_OBJC_KEY0)
    # define SUPPORT_DIRECT_THREAD_KEYS 1
    # define TLS_DIRECT_KEY ((tls_key_t)__PTK_FRAMEWORK_OBJC_KEY0)
    # define SYNC_DATA_DIRECT_KEY ((tls_key_t)__PTK_FRAMEWORK_OBJC_KEY1)
    # define SYNC_COUNT_DIRECT_KEY ((tls_key_t)__PTK_FRAMEWORK_OBJC_KEY2)
    # define AUTORELEASE_POOL_KEY ((tls_key_t)__PTK_FRAMEWORK_OBJC_KEY3)
    # if SUPPORT_RETURN_AUTORELEASE
    # define RETURN_DISPOSITION_KEY ((tls_key_t)__PTK_FRAMEWORK_OBJC_KEY4)
    # endif
    #else
    # define SUPPORT_DIRECT_THREAD_KEYS 0
    #endif

    #if SUPPORT_DIRECT_THREAD_KEYS
    #define _objc_pthread_key TLS_DIRECT_KEY
    #else
    static tls_key_t _objc_pthread_key;
    #endif

    //key _objc_pthread_key
    static inline void *tls_get(tls_key_t k) {
    return pthread_getspecific(k);
    }

    //key SYNC_DATA_DIRECT_KEY 与 SYNC_COUNT_DIRECT_KEY
    static inline void *tls_get_direct(tls_key_t k)
    {
    ASSERT(is_valid_direct_key(k));

    if (_pthread_has_direct_tsd()) {
    return _pthread_getspecific_direct(k);
    } else {
    return pthread_getspecific(k);
    }
    }

    __header_always_inline int
    _pthread_has_direct_tsd(void)
    {
    #if TARGET_IPHONE_SIMULATOR
    return 0;
    #else
    return 1;
    #endif
    }

    __header_always_inline void *
    _pthread_getspecific_direct(unsigned long slot)
    {
    #if TARGET_IPHONE_SIMULATOR
    return pthread_getspecific(slot);
    #else
    return _os_tsd_get_direct(slot);
    #endif
    }

    __attribute__((always_inline))
    static __inline__ void*
    _os_tsd_get_direct(unsigned long slot)
    {
    return _os_tsd_get_base()[slot];
    }

  • _objc_pthread_data通过pthread_getspecific获取缓存数据,key的类型是tls_key_t
    • 如果支持SUPPORT_DIRECT_THREAD_KEYSkey__PTK_FRAMEWORK_OBJC_KEY0
    • 不支持SUPPORT_DIRECT_THREAD_KEYSkey_objc_pthread_key
  • TLS快速缓存通过tls_get_direct获取,keytls_key_t类型。
    • SynData对应的key__PTK_FRAMEWORK_OBJC_KEY1
    • lockCount对应的key__PTK_FRAMEWORK_OBJC_KEY2
    • iOS模拟器通过pthread_getspecific获取
    • 其它通过_os_tsd_get_direct获取,调用的是_os_tsd_get_base(),不同架构对应不同汇编指令:
  • __attribute__((always_inline, pure))
    static __inline__ void**
    _os_tsd_get_base(void)
    {
    #if defined(__arm__)
    uintptr_t tsd;
    __asm__("mrc p15, 0, %0, c13, c0, 3\n"
    "bic %0, %0, #0x3\n" : "=r" (tsd));
    /* lower 2-bits contain CPU number */
    #elif defined(__arm64__)
    uint64_t tsd;
    __asm__("mrs %0, TPIDRRO_EL0\n"
    "bic %0, %0, #0x7\n" : "=r" (tsd));
    /* lower 3-bits contain CPU number */
    #endif

    return (void**)(uintptr_t)tsd;
    }

    3.4 从sDataLists获取SynData

        //sDataLists 中找 Syndata
    {
    SyncData* p;
    SyncData* firstUnused = NULL;
    //从SynList链表中查找SynData
    for (p = *listp; p != NULL; p = p->nextData) {
    if ( p->object == object ) {
    result = p;//找到
    // atomic because may collide with concurrent RELEASE
    //threadCount + 1,由于在上面线程缓存和tls的查找中没有找到,但是在 sDataLists 中找到了。所以肯定不是同一个线程了(那也肯定就不是exit,而是enter了),线程数量+1。
    OSAtomicIncrement32Barrier(&result->threadCount);
    goto done;
    }
    //没有找到的情况下找到了空位。
    if ( (firstUnused == NULL) && (p->threadCount == 0) )
    firstUnused = p;
    }

    // no SyncData currently associated with object
    //是exit就直接跳转到done的逻辑
    if ( (why == RELEASE) || (why == CHECK) )
    goto done;

    // an unused one was found, use it
    //找到一个未使用的(也有可能是之前使用过,threadCount现在变为0了),直接存储当前objc数据(这里相当于释放了sDataLists中的旧数据)。
    if ( firstUnused != NULL ) {
    result = firstUnused;
    //替换object
    result->object = (objc_object *)object;
    result->threadCount = 1;
    goto done;
    }
    }

    • 遍历开始获取的SynListobj对应的SynData
    • 找到的情况下threadCount + 1,由于在tls(快速以及cache中)没有找到数据,但是在sDataLists中找到了,所以肯定不在同一个线程(那也肯定就不是exit,而是enter了)直接跳转到done
    • eixt的逻辑直接跳转到done
    • 没有找到但是找到了threadCount = 0Syndata,也就是找到了空位(之前使用过,threadCount现在变为0了)。
      • 直接存储当前objc数据到synData中(这里相当于释放了sDataLists中的旧数据)。threadCount标记为1

    3.5 创建 SyncData

    tls中没有快速缓存、也没cache、并且sDataLists中没有数据也没有空位

    posix_memalign((void **)&result, alignof(SyncData), sizeof(SyncData));
    //对象本身
    result->object = (objc_object *)object;
    //持有线程数初始化为1
    result->threadCount = 1;
    //创建锁
    new (&result->mutex) recursive_mutex_t(fork_unsafe_lock);
    //头插法
    result->nextData = *listp;
    //这里 sDataLists 中的 SynList就赋值了。
    *listp = result;
    • 开辟一个SyncData大小的内存并进行对齐。
    • 设置object以及threadCount
    • 创建mutex锁。
    • 头插法将创建的SynData插入SynList中。也就相当于将数据存入sDataLists中。nextData存在的情况是发生了哈希冲突。

    3.6 done 缓存存储逻辑

        //数据存储
    if (result) {//有result,无论是创建的还是从 sDataLists 获取的。
    // Only new ACQUIRE should get here.
    // All RELEASE and CHECK and recursive ACQUIRE are
    // handled by the per-thread caches above.
    if (why == RELEASE) {//exit不进行任何操作
    // Probably some thread is incorrectly exiting
    // while the object is held by another thread.
    return nil;
    }
    if (why != ACQUIRE) _objc_fatal("id2data is buggy");
    if (result->object != object) _objc_fatal("id2data is buggy");

    #if SUPPORT_DIRECT_THREAD_KEYS
    //TLS 快速缓存不存在,存储到快速缓存。
    if (!fastCacheOccupied) {//
    // Save in fast thread cache
    //存储Syndata
    tls_set_direct(SYNC_DATA_DIRECT_KEY, result);
    //存储count为1
    tls_set_direct(SYNC_COUNT_DIRECT_KEY, (void*)1);
    } else
    #endif
    //cache存储 不支持 tls 快速缓存 或者 tls快速缓存存在的情况下
    {
    // Save in thread cache
    //获取SyncCache,不存在的时候进行创建
    if (!cache) cache = fetch_cache(YES);
    //将result放入list的最后一个元素,SyncCacheItem 中存储 result 以及 lockCount
    cache->list[cache->used].data = result;
    cache->list[cache->used].lockCount = 1;
    cache->used++;
    }
    }

    • exit的时候不进行任何操作:
      • TLS快速缓存会在获取缓存的时候进行释放。并且threadCount -1
      • cache逻辑会进行替换数据(相当于释放),并且threadCount -1
      • sDataLists获取数据逻辑本身不释放,会根据threadCount = 0找到空位进行替换,相当于释放。
    • 在支持快速缓存并且快速缓存不存在的情况下,将创建的SynData以及lockCount = 1存储到TLS快速缓存中。
    • 在不支持快速缓存或者快速缓存已经有值了的情况下将SynData构造SyncCacheItem存入SyncCache中。
    • 也就是说SynData只会在快速缓存与Cache中存在一个,同时会存储在sDataLists中。

    3.7 验证

    3.7.1 @synchronized 数据结构

    根据源码分析@synchronized数据结构如下:



    3.7.2 验证

    有如下代码:

    HPObject *obj = [HPObject alloc];
    HPObject *obj2 = [HPObject alloc];
    HPObject *obj3 = [HPObject alloc];
    dispatch_async(dispatch_queue_create("HotpotCat", DISPATCH_QUEUE_CONCURRENT), ^{
    @synchronized (obj) {
    @synchronized (obj) {
    @synchronized (obj) {
    //obj lockCount = 3 threadCount = 1
    NSLog(@"1 = %p",obj);
    @synchronized (obj2) {
    //obj2 lockCount = 1 threadCount = 1,有可能存在拉链
    NSLog(@"2 = %p",obj2);
    @synchronized (obj3) {
    //obj3 threadCount = 1, lockCount = 1,必然存在拉链(为了方便验证源码强制修改StripeCount为2)
    NSLog(@"3 = %p",obj3);
    dispatch_async(dispatch_queue_create("HotpotCat1", DISPATCH_QUEUE_CONCURRENT), ^{
    @synchronized (obj) {
    //obj threadCount = 2,一个线程的 lockCount = 3 另外一个 lockCount = 1
    NSLog(@"4 = %p",obj);
    }
    });
    //为了让 @synchronized 不exit
    sleep(10);
    }
    }
    }
    }
    }
    });

    do {

    } while (1);
    由于源码是mac工程,在main函数中写一个死循环。为了方便验证将源码中StripeCount改为2


    NSLog@synchronized处断点验证。

    • 1处的验证结果:



    • lockCount = 3threadCount = 1,并且sDataLists中存储的与快速缓存中是同一个SynData地址。符合预期。

    • 2处验证结果:



    可以看到这个时候第二个元素已经进行了拉链,并且obj2在链表的头结点。

    • 3处结果验证:


    仍然进行了拉链obj3 -> obj2 -> obj

    • 4处验证结果:


    这个时候obj对应的SynDatathreadCount2了。

    所有验证结果符合分析预期。

    四、总结

    • 参数传nil没有做任何事情。传self在使用过程中不会被释放,并且同一个类中如果都用self底层只会存在一个SynData

    • @synchronized底层是封装的os_unfair_lock

    • objc_sync_enter中加锁,objc_sync_exit中解锁。

    • @synchronized加锁的数据信息都存储在sDataLists全局哈希表中。同时还有TLS快速缓存(一个SynData数据,通常是第一个,释放后会存放新的)以及线程缓存(一组SyncData数据)。这两个缓存互斥,同一个SyncData只存在其中一个)

    • id2data获取SynData流程:

      • TLS快速缓存获取(SYNC_COUNT_DIRECT_KEY),obj对应的SyncData存在的情况下获取SYNC_COUNT_DIRECT_KEY对应的lockCount
        • enterlockCount++并存储到SYNC_COUNT_DIRECT_KEY
        • exitlockCount--并存储到SYNC_COUNT_DIRECT_KEYlockCount == 0清空SYNC_DATA_DIRECT_KEYthreadCount -1
      • TLS cache缓存获取,遍历cache找到对应的SyncData
        • enterlockCount++
        • exitlockCount--lockCount == 0替换cache->list对应的值为最后一个,used -1threadCount -1
      • sDataLists全局哈希表获取SyncData:找到的情况下threadCount + 1进入缓存逻辑,没有找到并且存在threadCount = 0则替换object相当于存储了新值。
      • SyncData创建:创建SyncData,赋值objectthreadCount初始化为1,创建mutex锁。并且采用头插法将SyncData插入sDataLists对应的SynList头部。
      • SyncData数据缓存:sDataLists添加了或者更新了数据会走到缓存逻辑,缓存逻辑是往TLS快速缓存以及TLS cache缓存添加数据
        • enterTLS快速缓存不存在的情况下将SyncData存储快速缓存,否则存入cache缓存的尾部。
        • exit:直接return
    • lockCount是针对单个线程而言的,当lockCount = 0的时候对数据进行释放

      • TLS快速缓存是直接设置为NULL(只有一个SyncData
      • TLS cache缓存是直接用最后一个数据进行替换(一组SyncData),然后used -1进行释放
      • 同时threadCount - 1相当于当前线程释放。
    • threadCount是针对跨线程的,在threadCount = 0的时候并不立即释放,而是在下次插入数据的时候进行替换。sDataLists保存所有的数据。

    • lockCount@synchronized可重入可递归的原因,threadCount@synchronized可跨线程的原因。

    @synchronized数据之间关系:





    作者:HotPotCat
    链接:https://www.jianshu.com/p/a816e8cf3646
    收起阅读 »

    锁的原理(一):@synchronized

    一、性能分析网上很多对比八大锁性能的文章,时间大部分比较早。苹果对某些锁内部进行了优化。这篇文章找中会以10万次数据做对比对主流锁性能进行分析。1.1 调用情况模拟OSSpinLockOSSpinLock在iOS 10以后废弃了,不过还可以调用。需要导入头文件...
    继续阅读 »

    一、性能分析

    网上很多对比八大锁性能的文章,时间大部分比较早。苹果对某些锁内部进行了优化。这篇文章找中会以10万次数据做对比对主流锁性能进行分析。

    1.1 调用情况模拟

    OSSpinLock
    OSSpinLockiOS 10以后废弃了,不过还可以调用。需要导入头文件<libkern/OSAtomic.h>


    int hp_runTimes = 100000;
    /** OSSpinLock 性能 */
    {
    OSSpinLock hp_spinlock = OS_SPINLOCK_INIT;
    double_t hp_beginTime = CFAbsoluteTimeGetCurrent();
    for (int i = 0 ; i < hp_runTimes; i++) {
    OSSpinLockLock(&hp_spinlock);//解锁
    OSSpinLockUnlock(&hp_spinlock);
    }
    double_t hp_endTime = CFAbsoluteTimeGetCurrent();
    printf("OSSpinLock: %f ms\n",(hp_endTime - hp_beginTime) * 1000);
    }
    dispatch_semaphore_t
    信号量是GCD提供的:

    /** dispatch_semaphore_t 性能 */
    {
    dispatch_semaphore_t hp_sem = dispatch_semaphore_create(1);
    double_t hp_beginTime = CFAbsoluteTimeGetCurrent();
    for (int i = 0 ; i < hp_runTimes; i++) {
    dispatch_semaphore_wait(hp_sem, DISPATCH_TIME_FOREVER);
    dispatch_semaphore_signal(hp_sem);
    }
    double_t hp_endTime = CFAbsoluteTimeGetCurrent();
    printf("dispatch_semaphore_t: %f ms\n",(hp_endTime - hp_beginTime) * 1000);
    }

    os_unfair_lock
    os_unfair_lockiOS10推出的新类型的锁需要导入头文件<os/lock.h>

    /** os_unfair_lock_lock 性能 */
    {
    os_unfair_lock hp_unfairlock = OS_UNFAIR_LOCK_INIT;
    double_t hp_beginTime = CFAbsoluteTimeGetCurrent();
    for (int i = 0 ; i < hp_runTimes; i++) {
    os_unfair_lock_lock(&hp_unfairlock);
    os_unfair_lock_unlock(&hp_unfairlock);
    }
    double_t hp_endTime = CFAbsoluteTimeGetCurrent() ;
    printf("os_unfair_lock_lock: %f ms\n",(hp_endTime - hp_beginTime) * 1000);
    }

    pthread_mutex_t
    pthread_mutex_tlinux下提供的锁,需要导入头文件<pthread/pthread.h>:


    /** pthread_mutex_t 性能 */
    {
    pthread_mutex_t hp_metext = PTHREAD_MUTEX_INITIALIZER;
    double_t hp_beginTime = CFAbsoluteTimeGetCurrent();
    for (int i = 0 ; i < hp_runTimes; i++) {
    pthread_mutex_lock(&hp_metext);
    pthread_mutex_unlock(&hp_metext);
    }
    double_t hp_endTime = CFAbsoluteTimeGetCurrent();
    printf("pthread_mutex_t: %f ms\n",(hp_endTime - hp_beginTime) * 1000);
    }
    NSLock
    NSLockFoundation框架提供的锁:

    /** NSlock 性能 */
    {
    NSLock *hp_lock = [NSLock new];
    double_t hp_beginTime = CFAbsoluteTimeGetCurrent();
    for (int i = 0 ; i < hp_runTimes; i++) {
    [hp_lock lock];
    [hp_lock unlock];
    }
    double_t hp_endTime = CFAbsoluteTimeGetCurrent();
    printf("NSlock: %f ms\n",(hp_endTime - hp_beginTime) * 1000);
    }
    NSCondition

    /** NSCondition 性能 */
    {
    NSCondition *hp_condition = [NSCondition new];
    double_t hp_beginTime = CFAbsoluteTimeGetCurrent();
    for (int i = 0 ; i < hp_runTimes; i++) {
    [hp_condition lock];
    [hp_condition unlock];
    }
    double_t hp_endTime = CFAbsoluteTimeGetCurrent();
    printf("NSCondition: %f ms\n",(hp_endTime - hp_beginTime) * 1000);
    }

    pthread_mutex_t(recursive)

    /** PTHREAD_MUTEX_RECURSIVE 性能 */
    {
    pthread_mutex_t hp_metext_recurive;
    pthread_mutexattr_t attr;
    pthread_mutexattr_init (&attr);
    pthread_mutexattr_settype (&attr, PTHREAD_MUTEX_RECURSIVE);
    pthread_mutex_init (&hp_metext_recurive, &attr);

    double_t hp_beginTime = CFAbsoluteTimeGetCurrent();
    for (int i = 0 ; i < hp_runTimes; i++) {
    pthread_mutex_lock(&hp_metext_recurive);
    pthread_mutex_unlock(&hp_metext_recurive);
    }
    double_t hp_endTime = CFAbsoluteTimeGetCurrent();
    printf("PTHREAD_MUTEX_RECURSIVE: %f ms\n",(hp_endTime - hp_beginTime) * 1000);
    }
    NSRecursiveLock

    /** NSRecursiveLock 性能 */
    {
    NSRecursiveLock *hp_recursiveLock = [NSRecursiveLock new];
    double_t hp_beginTime = CFAbsoluteTimeGetCurrent();
    for (int i = 0 ; i < hp_runTimes; i++) {
    [hp_recursiveLock lock];
    [hp_recursiveLock unlock];
    }
    double_t hp_endTime = CFAbsoluteTimeGetCurrent();
    printf("NSRecursiveLock: %f ms\n",(hp_endTime - hp_beginTime) * 1000);
    }
    NSConditionLock

    /** NSConditionLock 性能 */
    {
    NSConditionLock *hp_conditionLock = [NSConditionLock new];
    double_t hp_beginTime = CFAbsoluteTimeGetCurrent();
    for (int i = 0 ; i < hp_runTimes; i++) {
    [hp_conditionLock lock];
    [hp_conditionLock unlock];
    }
    double_t hp_endTime = CFAbsoluteTimeGetCurrent() ;
    printf("NSConditionLock: %f ms\n",(hp_endTime - hp_beginTime) * 1000);
    }

    @synchronized

    /** @synchronized 性能 */
    {
    double_t hp_beginTime = CFAbsoluteTimeGetCurrent();
    for (int i = 0 ; i < hp_runTimes; i++) {
    @synchronized(self) {}
    }
    double_t hp_endTime = CFAbsoluteTimeGetCurrent();
    printf("@synchronized: %f ms\n",(hp_endTime - hp_beginTime) * 1000);
    }

    锁内部没有处理任何逻辑,都执行的空操作,在10万次循环后计算时间差值。

    1.2 验证

    iPhone 12 pro max 14.3真机测试数据如下:

    OSSpinLock: 1.366019 ms
    dispatch_semaphore_t: 1.923084 ms
    os_unfair_lock_lock: 1.502037 ms
    pthread_mutex_t: 1.694918 ms
    NSlock: 2.384901 ms
    NSCondition: 2.082944 ms
    PTHREAD_MUTEX_RECURSIVE: 3.449082 ms
    NSRecursiveLock: 3.075957 ms
    NSConditionLock: 7.895947 ms
    @synchronized: 3.794074 ms

    iPhone 12 pro max 14.3模拟器测试数据如下:

    OSSpinLock: 1.199007 ms
    dispatch_semaphore_t: 1.991987 ms
    os_unfair_lock_lock: 1.762986 ms
    pthread_mutex_t: 2.611995 ms
    NSlock: 2.719045 ms
    NSCondition: 2.544045 ms
    PTHREAD_MUTEX_RECURSIVE: 4.145026 ms
    NSRecursiveLock: 5.039096 ms
    NSConditionLock: 8.215070 ms
    @synchronized: 10.205030 ms



    大部分锁在真机上性能表现更好,@synchronized在真机与模拟器中表现差异巨大。也就是说苹果在真机模式下优化了@synchronized的性能。与之前相比目前@synchronized的性能基本能满足要求。

    判断一把锁的性能好坏,一般情况下是与pthread_mutex_t做对比(因为底层都是对它的封装)。

    二、@synchronized

    由于@synchronized使用比较简单,并且目前真机性能也不错。所以先分析它。

    2.1售票案例

    有如下代码:

    @property (nonatomic, assign) NSUInteger ticketCount;

    - (void)testTicket {
    self.ticketCount = 10;
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
    for (int i = 0; i < 5; i++) {
    [self saleTicket];
    }
    });

    dispatch_async(dispatch_get_global_queue(0, 0), ^{
    for (int i = 0; i < 2; i++) {
    [self saleTicket];
    }
    });

    dispatch_async(dispatch_get_global_queue(0, 0), ^{
    for (int i = 0; i < 3; i++) {
    [self saleTicket];
    }
    });

    dispatch_async(dispatch_get_global_queue(0, 0), ^{
    for (int i = 0; i < 5; i++) {
    [self saleTicket];
    }
    });
    }

    - (void)saleTicket {
    if (self.ticketCount > 0) {
    self.ticketCount--;
    sleep(0.1);
    NSLog(@"当前余票还剩:%lu张",(unsigned long)self.ticketCount);
    } else {
    NSLog(@"当前车票已售罄");
    }
    }

    模拟了多线程售票请款,输出如下:

    当前余票还剩:6张
    当前余票还剩:7张
    当前余票还剩:7张
    当前余票还剩:7张
    当前余票还剩:4张
    当前余票还剩:4张
    当前余票还剩:3张
    当前余票还剩:2张
    当前余票还剩:1张
    当前余票还剩:0张
    当前车票已售罄
    当前车票已售罄
    当前车票已售罄
    当前车票已售罄
    当前车票已售罄
    可以看到余票数量有重复以及顺序混乱。
    saleTicket加上@synchronized就能解决问题:

    - (void)saleTicket {
    @synchronized(self) {
    if (self.ticketCount > 0) {
    self.ticketCount--;
    sleep(0.1);
    NSLog(@"当前余票还剩:%lu张",(unsigned long)self.ticketCount);
    } else {
    NSLog(@"当前车票已售罄");
    }
    }
    }

    一般参数传递self。那么有以下疑问:

    • 为什么要传self呢?传nil行不行?
    • @synchronized是怎么实现加锁的效果的呢?
    • {}代码块究竟是什么呢?
    • 是否可以递归呢?
    • 底层是什么数据结构呢?

    2.2 clang 分析 @synchronized

    @synchronized是个系统关键字,那么通过clang还原它的底层实现,为了方便实现在main函数中调用它:

    int main(int argc, char * argv[]) {
    NSString * appDelegateClassName;
    @autoreleasepool {
    appDelegateClassName = NSStringFromClass([AppDelegate class]);
    @synchronized(appDelegateClassName) {

    }
    }
    return UIApplicationMain(argc, argv, nil, appDelegateClassName);
    }

    clang还原后代码如下:

    int main(int argc, char * argv[]) {
    NSString * appDelegateClassName;
    { __AtAutoreleasePool __autoreleasepool;
    appDelegateClassName = NSStringFromClass(((Class (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("AppDelegate"), sel_registerName("class")));
    {
    id _rethrow = 0;
    id _sync_obj = (id)appDelegateClassName;
    objc_sync_enter(_sync_obj);
    try {
    struct _SYNC_EXIT {
    _SYNC_EXIT(id arg) : sync_exit(arg) {}
    ~_SYNC_EXIT() {
    objc_sync_exit(sync_exit);
    }
    id sync_exit;
    } _sync_exit(_sync_obj);
    }
    catch (id e) {
    _rethrow = e;

    }
    {
    struct _FIN {
    _FIN(id reth) : rethrow(reth) {}
    ~_FIN() {
    if (rethrow) objc_exception_throw(rethrow);
    }
    id rethrow;
    } _fin_force_rethow(_rethrow);

    }
    }

    }
    return UIApplicationMain(argc, argv, __null, appDelegateClassName);
    }

    异常处理不关心,所以核心就是try的逻辑,精简后如下:

    id _sync_obj = (id)appDelegateClassName;
    objc_sync_enter(_sync_obj);
    struct _SYNC_EXIT {
    _SYNC_EXIT(id arg) : sync_exit(arg) {}
    ~_SYNC_EXIT() {
    objc_sync_exit(sync_exit);
    }
    id sync_exit;
    } _sync_exit(_sync_obj);
    _SYNC_EXIT是个结构体的定义,_sync_exit析构的实现是objc_sync_exit(sync_exit),所以@synchronized本质上等价于enter + exit
    //@synchronized(appDelegateClassName) {}
    //等价
    objc_sync_enter(appDelegateClassName);
    objc_sync_exit(appDelegateClassName);

    它们是定义在objc中的。当然也可以通过对@synchronized打断点查看汇编定位:



    2.3 源码分析

    2.3.1 objc_sync_enter

    int objc_sync_enter(id obj)
    {
    int result = OBJC_SYNC_SUCCESS;

    if (obj) {
    //obj存在的情况下 获取 SyncData
    SyncData* data = id2data(obj, ACQUIRE);
    ASSERT(data);
    //加锁
    data->mutex.lock();
    } else {
    // @synchronized(nil) does nothing
    if (DebugNilSync) {
    _objc_inform("NIL SYNC DEBUG: @synchronized(nil); set a breakpoint on objc_sync_nil to debug");
    }
    //不存在调用objc_sync_nil
    objc_sync_nil();
    }

    return result;
    }
    • obj存在的情况下通过id2data获取SyncData,参数是objACQUIRE
      • 然后通过mutex.lock()加锁。
    • objnil的情况下调用objc_sync_nil,根据注释does nothing是一个空实现。

    mutex
    mutexrecursive_mutex_t mutex类型,本质上是recursive_mutex_tt


    using recursive_mutex_t = recursive_mutex_tt<LOCKDEBUG>;
    class recursive_mutex_tt : nocopy_t {
    os_unfair_recursive_lock mLock;
    ......
    }

    typedef struct os_unfair_recursive_lock_s {
    os_unfair_lock ourl_lock;
    uint32_t ourl_count;
    } os_unfair_recursive_lock, *os_unfair_recursive_lock_t;

    os_unfair_recursive_lock是对os_unfair_lock的封装。所以 @synchronized 是对os_unfair_lock 的封装。

    objc_sync_nil
    objc_sync_nil的定义如下:

    BREAKPOINT_FUNCTION(
    void objc_sync_nil(void)
    );

    # define BREAKPOINT_FUNCTION(prototype) \
    OBJC_EXTERN __attribute__((noinline, used, visibility("hidden"))) \
    prototype { asm(""); }

    替换还原后如下:

    OBJC_EXTERN __attribute__((noinline, used, visibility("hidden"))) 
    void objc_sync_nil(void) {
    asm("");
    }

    也就是一个空实现。

    2.3.2 objc_sync_exit

    int objc_sync_exit(id obj)
    {
    int result = OBJC_SYNC_SUCCESS;//0
    if (obj) {
    //获取 SyncData
    SyncData* data = id2data(obj, RELEASE);
    if (!data) {//没有输出返回错误code - 1
    result = OBJC_SYNC_NOT_OWNING_THREAD_ERROR;
    } else {
    //获取到数据先解锁
    bool okay = data->mutex.tryUnlock();
    if (!okay) {//解锁失败返回-1
    result = OBJC_SYNC_NOT_OWNING_THREAD_ERROR;
    }
    }
    } else {
    // @synchronized(nil) does nothing
    }

    return result;
    }
    • obj存在的情况下通过id2data获取SyncData,参数是objRELEASE
    • 获取到数据进行解锁,解锁成功返回0,失败返回-1

    2.3.3 SyncData 数据结构

    SyncData是一个结构体:

    typedef struct alignas(CacheLineSize) SyncData {
    struct SyncData* nextData;//下一个节点
    DisguisedPtr<objc_object> object;//obj,@synchronized的参数
    int32_t threadCount; // number of THREADS using this block 线程数量
    recursive_mutex_t mutex;//锁
    } SyncData;
    • nextData指向下一个节点,SyncData是一个单向链表。
    • object存储的是@synchronized的参数,只不过进行了包装。
    • threadCount代表线程数量。支持多线程访问。
    • mutex创建的锁。递归锁只能递归使用不能多线程使用。

    三、id2data

    objc_sync_enterobjc_sync_exit中都调用了id2data获取数据,区别是第二个参数,显然id2data就是数据处理的核心了。

    进行代码块折叠后有如下逻辑:



    syndata要么从TLS获取,要么从cache获取。都没有的情况下进行创建。


    收起阅读 »

    Android模块化开发实践

    一、前言 随着业务的快速发展,现在的互联网App越来越大,为了提高团队开发效率,模块化开发已经成为主流的开发模式。正好最近完成了vivo官网App业务模块化改造的工作,所以本文就对模块化开发模式进行一次全面的介绍,并总结模块化改造经验,帮助兄弟项目避坑。 ...
    继续阅读 »

    一、前言


    随着业务的快速发展,现在的互联网App越来越大,为了提高团队开发效率,模块化开发已经成为主流的开发模式。正好最近完成了vivo官网App业务模块化改造的工作,所以本文就对模块化开发模式进行一次全面的介绍,并总结模块化改造经验,帮助兄弟项目避坑。


    二、什么是模块化开发


    首先我们搞清两个概念,Android客户端开发目前有两种模式:单工程开发模式模块化开发模式



    • **单工程开发模式:**早期业务少、开发人员也少,一个App对应一个代码工程,所有的代码都集中在这一个工程的一个module里。


    • **模块化开发模式:**简单来说,就是将一个App根据业务功能划分成多个独立的代码模块,整个App是由这些独立模块集成而成。



    在讲什么是模块化开发前,我们先定义清楚两个概念:组件和模块。



    • **组件:**指的是单一的功能组件,比如登录组件、分享组件;


    • **模块:**广义上来说是指功能相对独立、边界比较清晰的业务、功能等,本文如果单独出现模块这个词一般是该含义。狭义上是指一个业务模块,对应产品业务,比如商城模块、社区模块。



    模块和组件的本质思想是一样的,都是为了业务解耦和代码重用,组件相对模块粒度更细。在划分的时候,模块是业务导向,划分一个个独立的业务模块,组件是功能导向,划分一个个独立的功能组件。


    模块化开发模式又分为两种具体的开发模式:单工程多module模式多工程模式


    单工程多module模式


    所有代码位于一个工程中,模块以AndroidStudio的module形式存在,由一个App module和多个模块module组成。如图:



    多工程模式


    每个模块代码位于一个工程中,整个项目由一个主模块工程和多个子模块工程组成。其中主模块工程只有一个App module,用于集成子模块,进行整体调试、编包。子模块工程由一个App module和一个Library module组成,App module中是调试、测试代码,Library module中是业务、功能代码。如下图:




    下面我们来对比一下单工程多module模式和多工程模式的优缺点:



    通过上面的对比,我们可以看出来,多工程模式在代码管理、开发调试、业务并行等方面有明显优势,非常适合像vivo官网这种业务线多、工程大、开发人员多的App,所以vivo官网目前就采用的此模式。本文在讲解模块化开发时,一般也是指多工程模式。


    单工程多module模式,更适合开发人员少、业务并行程度低的项目。但是多工程模式也有两个缺点:代码仓较多、开发时需要打开多个工程,针对这两个缺点,我们也有解决方案。


    代码仓较多的问题


    要求我们在拆分模块时粒度不能太细,当一个模块膨胀到一定程度时再进行拆分,在模块化带来的效率提升与代码仓管理成本增加间保持平衡。


    要打开多个工程开发的问题


    我们基于Gradle插件开发了代码管理工具,可以方便的切换通过代码依赖子模块或者maven依赖子模块,实际开发体验跟单工程多module模式一样,如下图;



    模块化开发的流程也很简单:



    • 版本前期,每个模块由特定的开发人员负责,各子模块分别独立开发、调试;


    • 子模块开发完成后,集成到主模块工程进行整体调试;


    • 集成调试成功后,进入测试。



    三、模块化开发


    3.1 我们为什么要做模块化开发呢?


    这里我们说说单一工程开发模式的一些痛点。


    团队协作效率低



    • 项目早期业务少、开发人员也少,随着业务发展、团队扩张,由于代码都在同一个工程中,虽然各个人开发的功能不同,但是经常会修改同一处的代码,这时就需要相关开发人员沟通协调以满足各自需求,增加沟通成本;


    • 提交代码时,代码冲突也要沟通如何合并(否则可能引起问题),增加合代码成本;


    • 无法进行并行版本开发,或者勉强进行并行开发,代价是各个代码分支差异大,合并代码困难。



    代码维护成本高



    • 单一工程模式由于代码都在一起,代码耦合严重,业务与业务之间、业务与公共组件都存在很多耦合代码,可以说是你中有我、我中有你,任何修改都可能牵一发而动全身,随着版本的迭代,维护成本会越来越高。


    开发调试效率低



    • 任何一次的修改,即使是改一个字符,都需要编译整个工程代码,随着代码越来越多,编译也越来越慢,非常影响开发效率。


    3.2 如何解决问题


    说完单一工程开发模式的痛点,下面我们看看模块化开发模式怎么来解决这些问题的。


    提高团队协作效率



    • 模块化开发模式下,根据业务、功能将代码拆分成独立模块,代码位于不同的代码仓,版本并行开发时,各个业务线只在各自的模块代码仓中进行开发,互不干扰,对自己修改的代码负责;


    • 测试人员只需要重点测试修改过的功能模块,无需全部回归测试;


    • 要求产品层面要有明确的业务划分,并行开发的版本必须是不同业务模块。



    降低代码维护成本



    • 模块化开发对业务模块会划分比较明确的边界,模块间代码是相互独立的,对一个业务模块的修改不会影响其他模块;


    • 当然,这对开发人员也提出了要求,模块代码需要做到高内聚。



    提高编译速度



    • 开发阶段,只需要在自己的一个代码仓中开发、调试,无需集成完整App,编译代码量极少;


    • 集成调试阶段,开发的代码仓以代码方式依赖,其他不涉及修改的代码仓以aar方式依赖,整体的编译代码量也比较少。



    当然模块化开发也不是说全都是好处,也存在一些缺点,比如:



    1)业务单一、开发人员少的App不要模块化开发,那样反而会带来更多的维护成本;


    2)模块化开发会带来更多的重复代码;


    3)拆分的模块越多,需要维护的代码仓越多,维护成本也会升高,需要在拆分粒度上把握平衡。



    总结一下,模块化开发就像我们管理书籍一样,一开始只有几本书时,堆书桌上就可以了。随着书越来越多,有几十上百本时,我们需要一个书橱,按照类别放在不同的格子里。对比App迭代过程,起步时,业务少,单一工程模式效率最高,随着业务发展,我们要根据业务拆分不同的模块。


    所有这些目的都是为了方便管理、高效查找。


    四、模块化架构设计


    模块化架构设计的思路,我们总结为纵向和横向两个维度。纵向上根据与业务的紧密程度进行分层,横向上根据业务或者功能的边界拆分模块。


    下图是目前我们App的整体架构。



    4.1 纵向分层


    先看纵向分层,根据业务耦合度从上到下依次是业务层、组件层、基础框架层。



    • 业务层:位于架构最上层,根据业务模块划分(比如商城、社区等),与产品业务相对应;


    • 组件层:App的一些基础功能(比如登录、自升级)和业务公用的组件(比如分享、地址管理),提供一定的复用能力;


    • 基础框架层:完全与业务无关、通用的基础组件(比如网络请求、图片加载),提供完全的复用能力。



    框架层级从上往下,业务相关性越来越低,代码稳定性越来越高,代码入仓要求越来越严格(可以考虑代码权限收紧,越底层的代码,入仓要求越高)。


    4.2 横向分模块



    • 在每一层上根据一定的粒度和边界,拆分独立模块。比如业务层,根据产品业务进行拆分。组件层则根据功能进行拆分。


    • 大模块可以独立一个代码仓(比如商城、社区),小模块则多个模块组成一个代码仓(比如上图中虚线中的就是多个模块位于一个仓)。


    • 模块要高内聚低耦合,尽量减少与其他模块的依赖。



    面向对象设计原则强调组合优于继承,平行模块对应组合关系,上下层模块对应继承关系,组合的优点是封装性好,达到高内聚效果。所以在考虑框架的层级问题上,我们更偏向前者,也就是拆分的模块尽量平行,减少层级。


    层级多的问题在于,下层代码仓的修改会影响更多的上层代码仓,并且层级越多,并行开发、并行编译的程度越低。


    模块依赖规则:



    • 只有上层代码仓才能依赖下层代码仓,不能反向依赖,否则可能会出现循环依赖的问题;


    • 同一层的代码仓不能相互依赖,保证模块间彻底解耦。



    五、模块化开发需要解决哪些问题


    5.1 业务模块如何独立开发、调试?


    方式一:每个工程有一个App module和一个Library module,利用App module中的代码调试Library module中的业务功能代码。


    方式二:利用代码管理工具集成到主工程中调试,开发中的代码仓以代码方式依赖,其他模块以aar方式依赖。


    5.2 平行模块间如何实现页面跳转,包括Activity跳转、Fragment获取?


    根据模块依赖原则,平行模块间禁止相互依赖。隐式Intent虽然能解决该问题,但是需要通过Manifest集中管理,协作开发比较麻烦,所以我们选择了路由框架Arouter,Activity跳转和Fragment获取都能完美支持。另外Arouter的拦截器功能也很强大,比如处理跳转过程中的登录功能。


    5.3 平行模块间如何相互调用方法?


    Arouter服务参考——github.com/alibaba/ARo…


    5.4 平行模块间如何传递数据、驱动事件?


    Arouter服务、EventBus都可以做到,视具体情况定。


    六、老项目如何实施模块化改造


    老项目实施模块化改造非常需要耐心和细心,是一个循序渐进的过程。


    先看一下我们项目的模块化进化史,从单一工程逐步进化成纺锤形的多工程模块化模式。下图是进化的四个阶段,从最初的单个App工程到现在的4层多仓结构。





    注:此图中每个方块表示一个代码仓,上层代码仓依赖下层代码仓。


    早期项目都是采用单一工程模式的,随着业务的发展、人员的扩张,必然会面临将老项目进行模块化改造的过程。但是在模块化改造过程中,我们会面临很多问题,比如:



    • 代码逻辑复杂,缺乏文档、注释,不敢轻易修改,害怕引起功能异常;


    • 代码耦合严重,你中有我我中有你,牵一发动全身,拆分重构难度大;


    • 业务版本迭代与模块化改造并行,代码冲突频繁,影响项目进度;



    相信做模块化的人都会遇到这些问题,但是模块化改造势在必行,我们不可能暂停业务迭代,把人力都投入到模块化中来,一来业务方不可能同意,二来投入太多人反而会带来更多代码冲突。


    所以需要一个可行的改造思路,我们总结为先自顶向下划分,再自底向上拆分


    自顶向下



    • 从整体到细节逐层划分模块,先划分业务线,业务线再划分业务模块,业务模块中再划分功能组件,最终形成一个树状图。



    自底向上



    • 当我们把模块划分明确、依赖关系梳理清楚后,我们就需要自底向上,从叶子模块开始进行拆分,当我们把叶子模块都拆分完成后,枝干模块就可以轻松拆分,最后完成主干部分的拆分。


    • 另外整个模块化工作需要由专人统筹,整体规划,完成主要的改造工作,但是有复杂的功能也可以提需求给各模块负责人,协助完成改造。



    下面就讲讲我们在模块化改造路上打怪升级的一些经验。总的来说就是循序渐进,各个击破


    6.1 业务模块梳理


    这一步是自顶向下划分模块,也就是确定子模块代码仓。一个老项目必然经过多年迭代,经过很多人开发,你不一定要对所有的代码都很熟悉,但是你必须要基本了解所有的业务功能,在此基础上综合产品和技术规划进行初步的模块划分。


    此时的模块划分可以粒度粗一点,比如根据业务线或者大的业务模块进行划分,但是边界要清晰。一个App一般会有多个业务线,每个业务线下又会有多个业务模块,这时,我们梳理业务不需要太细,保持2层即可,否则过度的拆分会大大增加实施的难度。



    6.2 抽取公共组件


    划分完模块,但是如果直接按此来拆分业务模块,会有很大难度,并且会有很多重复代码,因为很多公共组件是每个业务模块都要依赖的(比如网络请求、图片加载、分享、登录)。所以模块化拆分的第一步就是要抽取、下沉这些公共组件。


    在这一步,我们在抽取公共组件时会遇到两类公共组件,一类是完全业务无关的基础框架组件(比如网络请求、图片加载),一类是业务相关的公共业务组件(比如分享、登录)。


    可以将这两类公共组件分成两层,便于后续的整体框架形成。比如我们的lib仓放的是基础框架组件和core仓放的是业务公共组件。如下图



    6.3 业务模块拆分


    抽取完公共组件后,我们要准备进行业务模块的拆分,这一步耗时最长,但也是效果最明显的,因为拆完我们就可以多业务并行开发了。


    确定要拆分的业务模块(比如下图的商城业务),先把代码仓拉出来,新功能直接在新仓开发。


    那老功能该怎么拆分迁移呢?我们不可能一口吃成大胖子,想一次把一个大业务模块全部拆分出来,难度太大。这时我们就要对业务模块内部做进一步的梳理,找出所有的子功能模块(比如商城业务中的支付、选购、商详等)。



    按照功能模块的独立程度,从易到难逐个拆分,比如支付的订单功能比较独立,那就先把订单功能的代码拆分到新仓。


    6.4 功能模块拆分


    在拆分具体功能时,我们依然使用Top-Down的逻辑来实施,首先找到入口类(比如Activity),迁移到新的代码仓中,此时你会发现一眼望去全是报红,就像拔草一样带出大量根须。依赖的布局、资源、辅助类等等都找不到,我们按照从易到难的顺序一个个解决,需要解决的依赖问题有以下几类:



    1)简单的依赖,比如字符串、图片。


    这类是最容易解决,直接把资源迁移过来即可。


    2)较复杂的依赖,比如布局文件、drawable。


    这类相对来说也比较容易解决,逐级迁移即可。比如布局依赖各种drawable、字符串、图片,drawable又依赖其他的drawable等,自顶向下逐个迁移就能解决。


    3)更复杂的依赖,类似A->B->C->D。


    对于这类依赖有两种解决方式,如果依赖的功能没有业务特性或只是简单封装系统 API,那可以考虑直接copy一份;如果依赖的代码是多个功能模块公用的或者多个功能模块需要保持一致,可以考虑将该功能代码抽取下沉到下一层代码仓。


    4)一时难以解决的依赖。


    可以先暂时注释掉,保证可以正常运行,后续理清逻辑再决定是进行解耦还是重构。斩断依赖链非常重要,否则可能坚持不下去。



    6.5 代码解耦


    下面介绍一下常用的代码解耦方法:



    公共代码抽取下沉


    比如:基础组件(eg.网络请求框架)、各模块需要保持功能一致的代码(eg.适配OS的动效);




    简单代码复制一份


    比如简单封装系统api(eg.获取packageName)、功能模块自用的自定义view(eg.提示弹窗);




    三个工具


    Arouter路由、Arouter服务、EventBus,能满足各种解耦场景。



    6.6 新老代码共存


    老项目模块化是一个长期的过程,新老代码共存也是一个长期的过程。经过上面改造后,一个功能模块就可以独立出来了,因为我们都是从老的App工程里拆分出来的,所以App工程依赖新仓后就可以正常运行。当我们持续从老工程中拆分出独立模块,最后老工程只需要保留一些入口功能,作为集成子模块的主工程。


    七、总结


    本文从模块化的概念模块化架构设计以及老项目如何实施模块化改造等几个方面介绍移动应用客户端模块化实践。当然模块化工作远不止这些,还包括模块aar管理、持续集成、测试、模块化代码管理、版本迭代流程等,本文就不一一赘述,希望这篇文章能给准备做模块化开发的项目提供帮助。



    作者:vivo互联网客户端团队-Wang Zhenyu


    收起阅读 »

    真·富文本编辑器的演进之路-Span的整体性控制

    时隔多日,终于又更新了。 在了解了Span的基本知识后,我们先来处理下「Span的整体性控制」,怎么理解呢?我们在编辑富文本的时候,经常会遇到一些整体内容的输入,例如「@用户」、输入话题「#什么话题#」、跳转链接「URL」,这些Span区别于普通文字,输入时...
    继续阅读 »

    时隔多日,终于又更新了。


    在了解了Span的基本知识后,我们先来处理下「Span的整体性控制」,怎么理解呢?我们在编辑富文本的时候,经常会遇到一些整体内容的输入,例如「@用户」、输入话题「#什么话题#」、跳转链接「URL」,这些Span区别于普通文字,输入时是整体输入,删除时,也是整体删除,而知中间也不能插入文字或者修改,这就是「Span的整体性控制」。


    所以,我们需要对Span做下面的限制:



    • 中间不允许光标插入

    • 增加时整体新增

    • 删除时整体删除


    对应这样的需求,我们有两种方式来处理,第一种是使用原本就是整体的Span,例如ImageSpan,这是最简单的方法,而且代码也非常简单,另一种是通过代码处理,让普通文本来实现整体性的功能。


    通过ImageSpan保证完整性


    将Span内容生成ImageSpan,从而实现整体性控制。这种方案简单易行,我们以新增「@用户」为例。



    1. 首先,创建一个ATSpan,继承自ImageSpan,附带@的数据信息

    2. 解析要添加的富文本数据,将要展示的内容,例如「@xuyisheng」,作为文本,创建一个TextView来承载

    3. 将生成的TextView转化为Drawable,设置给ATSpan,并传入@的相关数据信息

    4. 将ImageSpan插入Edittext,实现整体性Span的富文本插入


    可以发现,这种方案的实现步骤是比较简单的,但是它的确定也很明显:


    首先,由于是ImageSpan,所以在与普通文本的对齐方式上,始终会存在一些误差,这些误差有来自TextView-Drawable的转换过程,也有ImageSpan的对齐过程,所以,在样式上,对齐会有一些问题,同时,由于TextView-Drawable的整体性,一旦TextView有多行或者当前行剩余位置不够,那么第二行的剩余区域都将被View的矩形区域填满,从而导致这些区域无法再输入文本,如下所示。


    image-20210819162910988


    这是由于View的图形限制导致的问题,使用ImageSpan的话,是无法解决的问题,由此可见,ImageSpan虽然天生具有整体性,但是却只是一个妥协的方案,不能算是最好的实现方式。


    通过SpanWatcher控制


    第二种方案,我们使用普通文本,但是对普通文本增加Span标记,并对这个Span做整体性控制,这种方案复杂一点,要处理的地方也比较多,但是由于它使用的是普通文本,所以在样式上可以和其它普通文本完全保持一致,视觉样式应该是最好的。


    着色


    首先,我们来实现普通文本的变色功能,做一个蓝色的字色,这个比较简单,可以使用ClickableSpan或者其它Span来着色,为了方便我们富文本的输入和展示,这里直接选择ClickableSpan来实现。


    控制选中


    在讲解如何在普通文本中对Span做整体性控制前,我们先来考虑下选择的问题——如何让「整体性Span」的内部无法被选中。


    首先,我们要知道,Edittext的光标也是一种Span。也就是说,我们可以通过监听光标的移动事件,通过Selection实现当光标移动到Span内部时,让它重新移动到Span最近的边缘位置,从而让Span内部永远无法插入光标,这就是我们的主要思路。


    那么问题来了,我要怎么监听Edittext的光标呢?


    其实,Android的Span不仅功能非常强大,而且也提供了非常完善的管理API,在TextView和Edittext中,我们要监听Text的变化过程,可以使用TextWatcher,它可以在文本发生改变时进行回调,类似的,在SpannableStringBuidler中,也有类似的管理类——SpanWatcher,它同样可以用于在Span发生变化时进行回调。


    SpanWatcher,官方介绍如下。


    When an object of this type is attached to a Spannable, its methods will be called to notify it that other markup objects have been added, changed, or removed.

    在TextVIew的内部,它通过DynamicLayout来渲染Spannable数据,在其内部会设置SpanWatcher来监听Span的新增、修改和删除,当监听到变化后,会调用其内部的方法进行刷新。


    image-20210819165313706


    SpanWatcher和TextWatcher一样,都是继承自NoCopySpan,它们一个监听文本变化,一个监听Span变化。


    看完了SpanWatcher,再来看下Selection,Selection是为TextView和Edittext设计的一套管理选中态的工具类,借助Selection,可以在不依赖具体View的情况下,对Span做选中态的修改。


    Selection有两个状态,Start和End,而选择光标,就是Selection的两个状态,当两个状态重合时,就是光标的输入状态。


    现在我们的思路就很明显了,在SpanWatcher的onSpanChanged中监听Selection的Start和End状态即可,一旦Selection的Start和End在我们的「整体性Span」中,就将Selection光标移动到最近的Span标记处。


    image-20210819173317458


    那么SpanWatcher怎么使用呢?


    Edittext提供了Editable.Factory来自定义添加SpanWatcher,我们只需要在初始化的时候传入即可,代码如下所示。


    class ExEditableFactory(private val spans: List<NoCopySpan>) : Factory() {
    override fun newEditable(source: CharSequence): Editable {
    val spannableStringBuilder = RepairSpannableStringBuilder(source)
    for (span in spans) {
    spannableStringBuilder.setSpan(span, 0, source.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE or Spanned.SPAN_PRIORITY)
    }
    return spannableStringBuilder
    }
    }

    val watchers = ArrayList<NoCopySpan>()
    watchers.add(SelectionSpanWatcher(IntegratedSpan::class))
    setEditableFactory(ExEditableFactory(watchers))

    这样我们就完成了选中的整体性功能,当我们的Selection在「整体性Span」(通过IntegratedSpan来标记)中时,就自动修改Selection的位置,从而实现「整体性Span」中间无法插入光标。


    控制删除


    那么除了选中之外,剩下的一个问题就是删除的整体性控制。


    相比于选中来说,删除就比较简单了,我们可以通过setOnKeyListener来监听KeyEvent.KEYCODE_DEL和KeyEvent.ACTION_DOWN事件。


    当我们检测到这两个事件后,根据当前Selection的位置,拿到当前是否存在「整体性Span」,如果是「整体性Span」,那么在删除时则整体移除即可。



    这里有个很重要的地方,getSpan函数传入的Start和End,是闭区间,也就是说,即使光标现在在「整体性Span」的末尾,getSpan函数也是能拿到这个Span的。



    有了思路之后,我们的代码就很容易了,关键代码如下所示。


    image-20210820145414181



    其实这里除了对「整体性Span」进行整体性删除以为,你甚至可以使用removeSpan来移除「整体性Span」,从而将其恢复成普通文本,当然,这都是看你自己的需求了。



    好了,到此为止,我们又实现了富文本编辑器中的一个非常重要的功能——Span的整体性控制。

    收起阅读 »

    Flutter 安卓 Platform 与 Dart 端消息通信方式 Channel 源码解析

    背景 本系列前面已经分析了 Flutter 的很多知识,这一篇我们来看下 Flutter 平台通信相关原理。Flutter 官方提供三种 Platform 与 Dart 端消息通信方式,他们分别是 MethodChannel、BasicMessageChan...
    继续阅读 »


    背景


    本系列前面已经分析了 Flutter 的很多知识,这一篇我们来看下 Flutter 平台通信相关原理。Flutter 官方提供三种 Platform 与 Dart 端消息通信方式,他们分别是 MethodChannel、BasicMessageChannel、EventChannel,本文会继续延续前面系列对他们进行一个深度解析,源码依赖 Flutter 2.2.3 版本,Platform 选取熟悉的 Android 平台实现。


    对于 MethodChannel、BasicMessageChannel、EventChannel 三种官方消息通信方式来说,他们都是全双工通信,所以基于他们我们基本可以实现 Platform 与 Dart 的各种通信能力。他们各自适用场景如下:



    • MethodChanel:用于传递方法调用,MethodCallHandler 最终必须在 UI 线程通过result.success(x)方法返回结果,返回前自己可以异步新起线程做任意耗时操作。

    • BasicMessageChannel:用于传递字符串和半结构化的消息。

    • EventChannel:用于数据流的发送。


    基础使用技巧


    这些通信方式的基础用法我们这里就不再解释了,这里重点说下技巧,在编写 Platform 代码时有两个特别注意的点:



    • 对于 Mac 用户,如果你要通过 Mac 的 Android Studio 打开 Flutter 自动创建的.android 项目,记得吊起访达后通过快捷键Command + Shift + '.'显示隐藏目录即可。

    • 修改 Platform 端的代码后如果运行没生效则请关闭 app 重新编译,因为热部署对 Platform 无效。


    日常工作中我们使用最多的是 MethodChannel,但是他却不是类型安全的,为了解决这个问题官方推荐使用 Pigeon 包作为 MethodChannel 的替代品,它将生成以结构化类型安全方式发送消息的代码,但是他目前还不稳定。


    更多关于他们基础使用案例参见官方文档flutter.dev/docs/develo…


    消息收发传递源码分析


    下面源码分析我们依旧秉承以使用方式为入口,分 Platform、Engine、Dart 层各自展开。


    Platform 端收发实现流程


    在进行 Platform 端源码分析前请先记住下面这幅图,如下 Platform 的 Java 侧源码基于此图展开分析。 在这里插入图片描述 我们先分别看下 MethodChannel、BasicMessageChannel、EventChannel 在 Platform 端的构造成员源码:


    public class MethodChannel {
    private final BinaryMessenger messenger;
    private final String name;
    private final MethodCodec codec;
    //......
    private final class IncomingMethodCallHandler implements BinaryMessageHandler {
    private final MethodCallHandler handler;
    }
    }

    public final class BasicMessageChannel<T> {
    @NonNull private final BinaryMessenger messenger;
    @NonNull private final String name;
    @NonNull private final MessageCodec<T> codec;
    //......
    private final class IncomingMessageHandler implements BinaryMessageHandler {
    private final MessageHandler<T> handler;
    }
    }

    public final class EventChannel {
    private final BinaryMessenger messenger;
    private final String name;
    private final MethodCodec codec;
    //......
    private final class IncomingStreamRequestHandler implements BinaryMessageHandler {
    private final StreamHandler handler;
    }
    }

    可以看到,Platform 端无论哪种方式,他们都有三种重要的成员,分别是:



    • name:String 类型,唯一标识符代表 Channel 的名字,因为一个 Flutter 应用中存在多个 Channel,每个 Channel 在创建时必须指定一个独一无二的 name 作为标识,这点我们在前面系列源码分析中已经见过很多框架实现自己的 name 定义了。

    • messager:BinaryMessenger 类型,充当信使邮递员角色,消息的发送与接收工具人。

    • codec:MethodCodec 或MessageCodec<T>类型,充当消息的编解码器。


    所以,MethodChannel、BasicMessageChannel、EventChannel 的 Java 端源码其实自身是没有什么的,重点都在 BinaryMessenger,我们就不贴源码了(比较简单),整个 Java 端收发的流程(以 MethodChannel 为例)大致如下: 在这里插入图片描述 上面流程中的 DartMessenger 就是 BinaryMessenger 的实现,也就是 Platform 端与 Dart 端通信的信使,这一层通信使用的消息格式为二进制格式数据(ByteBuffer)。


    可以看到,当我们初始化一个 MethodChannel 实例并注册处理消息的回调 Handler 时会生成一个对应的 BinaryMessageHandler 实例,然后这个实例被放进信使的一个 Map 中,key 就是我们 Channel 的 name,当 Dart 端发送消息到 DartMessenger 信使时,信使会根据 name 找到对应 BinaryMessageHandler 调用,BinaryMessageHandler 中通过调用 MethodCodec 解码器进行二进制解码(默认 StandardMethodCodec 解码对应平台数据类型),接着我们就可以使用解码后的回调响应。


    当我们通过 Platform 调用 Dart 端方法时,也是先通过 MethodCodec 编码器对平台数据类型进行编码成二进制格式数据(ByteBuffer),然后通过 DartMessenger 信使调用 FlutterJNI 交给 Flutter Engine 调用 Dart 端对应实现。


    Dart Framework 端收发实现流程


    在进行 Dart 端源码分析前请先记住下面这幅图,如下源码基于此图展开分析。 在这里插入图片描述 是不是 Dart 端的像极了 Platform 端收发实现流程图,同理我们看下 Dart Framework 端对应 Channel 实现类成员:


    class MethodChannel {
    final String name;
    final MethodCodec codec;
    final BinaryMessenger? _binaryMessenger;
    //......
    }

    class BasicMessageChannel<T> {
    final String name;
    final MessageCodec<T> codec;
    final BinaryMessenger? _binaryMessenger;
    //......
    }

    class EventChannel {
    final String name;
    final MethodCodec codec;
    final BinaryMessenger? _binaryMessenger;
    //......
    }

    可以看到,Dart 端无论哪种方式,他们也都有三种重要的成员,分别是 name、codec、_binaryMessenger,而且他们的职责和 Platform 端完全一样。也就是说 Dart 端就是 Platform 端的一个镜像实现而已,框架设计到原理步骤完全一致,区别仅仅是实现语言的不同。


    所以,整个 Dart 端收发的流程(以 MethodChannel 为例)大致如下: 在这里插入图片描述 有了上图不用再贴代码了吧,和 Platform 端如出一辙,只是换了个语言实现而已。


    Flutter Engine C++ 收发实现流程


    上面 Platform 与 Dart 端的通信都分析完毕了,现在就差中间粘合层的 Engine 调用了,Engine 的分析我们依然依据调用顺序为主线查看。通过上面分析我们可以得到如下信息:



    • Platform 调用 Dart 时 Java 最终调用了 FlutterJNI 的private native void nativeDispatchPlatformMessage(long nativeShellHolderId, String channel, ByteBuffer message, int position, int responseId)方法传递到 Engine,Engine 最终调用了 Dart Framework 中hooks.dartvoid _dispatchPlatformMessage(String name, ByteData? data, int responseId)方法,然后层层传递到我们的 Widget 中的 MethodChannel。

    • Dart 调用 Platform 时 Dart 最终调用了 PlatformDispatcher 的String? _sendPlatformMessage(String name, PlatformMessageResponseCallback? callback, ByteData? data)方法(即native 'PlatformConfiguration_sendPlatformMessage')传递到 Engine,Engine 最终调用了 Platform 端 FlutterJNI 的public void handlePlatformMessage(final String channel, byte[] message, final int replyId)方法,然后层层传递到我们的 MethodChannel 设置的 MethodCallHandler 回调的 onMethodCall 方法中。


    因此我们顺着这两端的入口分析源码可以得到如下调用顺序图: 在这里插入图片描述 上图对应的 Engine C++ 代码调用及类所属文件都已经交代的很详细了,源码就不再贴片段了,相信你顺着这条链也能根懂源码。特别注意上面 Engine 在负责转发消息时的黄色 TaskRunner,其中 PlatformTaskRunner 就是平台层的主线程(安卓 UI 线程),所以 Channel 在安卓端的回调被切换运行在 UI 线程中,Channel 在 Dart 端的回调被切换运行在 Flutter Dart UI 线程(即 UITaskRunner 中)。


    消息编解码源码分析


    搞懂了 Channel 的收发流程,你可能对上面的编解码器还有疑惑,他是怎么做到 Dart 与不同平台语言类型间转换的? 我们都知道,一般跨语言或平台传输对象首选方案是通过 json 或 xml 格式,而 Flutter 也不例外,譬如他也提供了 JSONMessageCodec、JSONMethodCodec 等编解码器,同样也是将二进制字节流转换为 json 进行处理,像极了我们 http 请求中字节流转字符串转 json 转对象的机制,这样就抹平了平台差异。 对于 Flutter 的默认实现来说,最值得关注的就是 StandardMethodCodec 和 StandardMessageCodec,由于 StandardMethodCodec 是对 StandardMessageCodec 的一个包装,所以本质我们研究下 StandardMessageCodec 即可。如下:


    public class StandardMessageCodec implements MessageCodec<Object> {
    //把Java对象类型Object转为字节流ByteBuffer
    @Override
    public ByteBuffer encodeMessage(Object message) {
    //......
    final ExposedByteArrayOutputStream stream = new ExposedByteArrayOutputStream();
    writeValue(stream, message);
    final ByteBuffer buffer = ByteBuffer.allocateDirect(stream.size());
    buffer.put(stream.buffer(), 0, stream.size());
    return buffer;
    }
    //把字节流ByteBuffer转为Java对象类型Object
    @Override
    public Object decodeMessage(ByteBuffer message) {
    //......
    message.order(ByteOrder.nativeOrder());
    final Object value = readValue(message);
    //......
    return value;
    }
    //......
    }

    可以看到,在 Platform 端(Android Java)StandardMessageCodec 的作用就是字节流转 Java 对象类型,Java 对象类型转字节流,核心本质是 StandardMessageCodec 的 readValue 和 writeValue 方法,如下:


    protected void writeValue(ByteArrayOutputStream stream, Object value) {
    if (value == null || value.equals(null)) {
    stream.write(NULL);
    } else if (value instanceof Boolean) {
    stream.write(((Boolean) value).booleanValue() ? TRUE : FALSE);
    } else if (value instanceof Number) {
    if (value instanceof Integer || value instanceof Short || value instanceof Byte) {
    stream.write(INT);
    writeInt(stream, ((Number) value).intValue());
    } else if (value instanceof Long) {
    stream.write(LONG);
    writeLong(stream, (long) value);
    } else if (value instanceof Float || value instanceof Double) {
    stream.write(DOUBLE);
    writeAlignment(stream, 8);
    writeDouble(stream, ((Number) value).doubleValue());
    } else if (value instanceof BigInteger) {
    stream.write(BIGINT);
    writeBytes(stream, ((BigInteger) value).toString(16).getBytes(UTF8));
    } else {
    throw new IllegalArgumentException("Unsupported Number type: " + value.getClass());
    }
    } else if (value instanceof String) {
    stream.write(STRING);
    writeBytes(stream, ((String) value).getBytes(UTF8));
    } else if (value instanceof byte[]) {
    stream.write(BYTE_ARRAY);
    writeBytes(stream, (byte[]) value);
    } else if (value instanceof int[]) {
    stream.write(INT_ARRAY);
    final int[] array = (int[]) value;
    writeSize(stream, array.length);
    writeAlignment(stream, 4);
    for (final int n : array) {
    writeInt(stream, n);
    }
    } else if (value instanceof long[]) {
    stream.write(LONG_ARRAY);
    final long[] array = (long[]) value;
    writeSize(stream, array.length);
    writeAlignment(stream, 8);
    for (final long n : array) {
    writeLong(stream, n);
    }
    } else if (value instanceof double[]) {
    stream.write(DOUBLE_ARRAY);
    final double[] array = (double[]) value;
    writeSize(stream, array.length);
    writeAlignment(stream, 8);
    for (final double d : array) {
    writeDouble(stream, d);
    }
    } else if (value instanceof List) {
    stream.write(LIST);
    final List<?> list = (List) value;
    writeSize(stream, list.size());
    for (final Object o : list) {
    writeValue(stream, o);
    }
    } else if (value instanceof Map) {
    stream.write(MAP);
    final Map<?, ?> map = (Map) value;
    writeSize(stream, map.size());
    for (final Entry<?, ?> entry : map.entrySet()) {
    writeValue(stream, entry.getKey());
    writeValue(stream, entry.getValue());
    }
    } else {
    throw new IllegalArgumentException("Unsupported value: " + value);
    }
    }

    不用解释了吧,这不就是枚举一堆支持的类型然后按照字节位数截取转换的操作,所以这也就是为什么官方文档中明确枚举了 Channel 支持的数据类型,如下: 在这里插入图片描述 上面是 Platform 端对象类型与二进制之间的转换原理,对于 Dart 端我想你应该也就懂了,无非也是类似操作,不再赘述。


    总结


    上面全程都以 MethodChannel 进行了源码分析,其他 Channel 我们没有进行分析,但其实本质都一样,仅仅是一种封装而已,希望你有需求的时候知道怎么举一反三。

    收起阅读 »

    『Android』 AndroidStudio多版本共存指南

    当AndroidStudio最新版本,提供许多新功能的时候。为了提升开发效率,必须跟着谷歌官方走。但是为了防止,将原本的Studio直接升级到新版Studio,然后导入以前项目,出现问题。因此,考虑多种版本共存的问题。 搭建方法 采用多个版本的Stud...
    继续阅读 »

    当AndroidStudio最新版本,提供许多新功能的时候。为了提升开发效率,必须跟着谷歌官方走。但是为了防止,将原本的Studio直接升级到新版Studio,然后导入以前项目,出现问题。因此,考虑多种版本共存的问题。



    搭建方法


    采用多个版本的Studio(例如:AndroidStudio2.3 和3.0)开发同一个项目,当新版本出现问题后,为了避免拖延开发时间,可及时切换会旧版本继续开发。


    1.下载最新的版本或者需要的版本:


    ★ ★ ★ AndroidStudio的下载分为安装版(.exe)和无安装版本(zip)。


    原本已经存在的了AndroidStudio和配置好的SDK,不需要进行替换成最新的AndroidStudio3.0版本。 只需要下载无安装版本的AndroidStudio。如下图所示:


    1.png


    接下来,下载完成后,解压到指定的目录下,如下图所示:


    2.png


    2.配置下载好的Studio版本:


    在解压后的目录下–>bin目录–>打开studio64.exe程序,下图所示:


    4.png


    运行AndroidStudio3.0程序后,弹出Import Studio设置弹窗,如下图所示:


    3.png



    • 第一个选项:是导入旧版本的设置。选择该项后,可以直接与旧版的Studio共同开发原本项目,无需手动配置SDK,导入指定项目等操作。


    • 第二个选项:导入指定的配置,和第一个选项类似。


    • 第三个选项:不导入先前配置,这里需要手动配置SDK和导入项目的操作。若是为了体验最新版本的Studio,创建新项目,可以选该选项。



    选择第一个选项第二个选项是,多版本Studio共同开发同一个项目,无需下面操作,重要的事情强调三遍。


    本人这里不导入先前配置,因此选择do not import settings,接下来手动导入原本的SDK配置。


    点击OK后,出现正常安装界面,如下图:


    5.png


    点击Next后,在Install Type界面上,选择Custom选项,自定义配置,如下图所示:


    6.png


    点击Next后,在SDK Components Setup界面,在SDK Location选项中,选择原本旧版本studio下载好的SDK路径,如下图所示:


    7.png


    点击Next后,在Verify Settings界面,选择Cancel,不更SDK的配置,如下图:


    8.png


    最后,Welcome to Android Studio界面,如下图所示:


    9.png


    接下,是新创建项目,还是从版本托管拖拉项目,还是导入原本旧项目,取决个自己的需求。


    资源参考:


    Studio多版本共存:developer.android.google.cn/studio/prev…


    Studio下载: developer.android.google.cn/studio/inde…








    收起阅读 »