注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

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…








    收起阅读 »

    开发者实践丨盲水印插件:用户端的实时视频溯源保护

    本文作者是 RTE 2021 创新编程挑战赛获奖者董章晔团队。在实时音视频领域,视频内容会需要得到版权保护,而盲水印则是保护的措施之一。这支参赛团队基于声网 SDK 开发了一款应用于用户端的实时视频盲水印插件。其他使用声网 SDK 的开发者,也同样可以在自己的...
    继续阅读 »

    本文作者是 RTE 2021 创新编程挑战赛获奖者董章晔团队。在实时音视频领域,视频内容会需要得到版权保护,而盲水印则是保护的措施之一。这支参赛团队基于声网 SDK 开发了一款应用于用户端的实时视频盲水印插件。其他使用声网 SDK 的开发者,也同样可以在自己的应用中使用该插件。访问『阅读原文』,可以查看该项目的源码。

    项目介绍

    视频盲水印技术是将标识信息直接嵌入视频 RGB 或 YUV 的频域中,基本不影响原视频的观看质量,也不容易被人觉察或注意。通过这些隐藏在载体中的信息,可确认内容创建者、使用者或者判断视频是否被篡改。该技术通常由专业的版权保护服务商提供,用于广播电视版权保护,商业性较强。

    本项目基于声网的 SDK 开发了一款用户端的实时视频盲水印的插件,同时,配套提供了一款基于个人 PC 端的水印识别软件,用于水印验证。降低了使用盲水印服务的专业门槛,为个人用户的隐私保护和作品防盗版提供了便捷的解决方案。

    实现原理

    盲水印的实现原理是在频域上完成信息叠加,变换的方法包括离散傅立叶变换、小波变换等,比如采用傅里叶变换,在实部和虚部完成文字图像叠加,再通过逆变换显示视频帧。

    图片

    对视频帧提取水印的方法是对视频帧截图,对截图再进行一次傅里叶变换,得到频域数据,对频域幅度,即能量进行显示,得出频域幅度图,就会显示之前叠加的文字。

    图片

    快速傅里叶变换复杂度为 O(nlog(n)),原理上可以在视频处理过程中实现盲水印的实时叠加。

    设计实现

    程序设计包括声网 SDK 对接和盲水印开发两部分,盲水印开发分为 Android 端叠加水印和 Windows 提取水印两部分。分别是灰色、黄色和橙色三块。由于是演示 Demo,所以仅在本地视频预览上完成盲水印的处理,未来可扩展到视频显示上。

    图片

    该方案设计重点考虑 SDK 衔接和第三方兼容两大方面。主要是少拷贝YUV数据、视频处理串行化、第三方兼容性和场景泛化等方面。

    核心代码

    叠加水印的主流程:

    图片

    opencv 的调用函数:

    图片

    主要是傅立叶变换和叠加文字两个函数,声网 SDK 与 OpenCV 开源库兼容效果良好。

    效果展示

    图片

    第一幅图是原始视频,输入水印文字比如 wm,第二幅图是叠加盲水印的视频,可见视频效果基本不受影响,最后一幅图是将第二幅图上传到 PC 后,用户自己提取水印的图像,可见图像中有明显的 wm 文字。至此完成了验证。

    未来展望

    下一步计划主要从提高水印的鲁棒性、扩展水印的应用场景、丰富水印的数据维度等方面进行考虑。在水印鲁棒性方面,计划空域上进行网格化分割,针对不同分割区域进行频域水印叠加;采用不同的变换方法,例如 DWT,以求最佳效果;对水印本身进行冗余编码,提升水印辨识度,增加水印的隐蔽性。在扩展水印应用方面,在实时视频显示端,进行水印叠加,达到偷拍溯源的目的。在丰富数据维度方面,在音频处理上,可扩展声纹水印;结合视频内容特征,可扩展特征编码等。


    收起阅读 »

    Jetpact Compose状态管理简单理解

    概览所谓的状态可以简单的理解为应用中的某个值的变化,比如可以是一个布尔值、数组放在业务的场景中,可以是 TextField 中的文字、动画执行的状态、用户收藏的商品都是状态我们知道 compose 是声明式的 ui,每次我们重组页面的时候都会把组件重组,此时就...
    继续阅读 »

    概览

    所谓的状态可以简单的理解为应用中的某个值的变化,比如可以是一个布尔值、数组

    放在业务的场景中,可以是 TextField 中的文字、动画执行的状态、用户收藏的商品都是状态

    我们知道 compose 是声明式的 ui,每次我们重组页面的时候都会把组件重组,此时就需要引入状态进行管理,例如:

    我们在商品的 item 里面点击按钮收藏了商品,此时商品的收藏状态发生了改变,我们需要重组 ui 将商品变为已收藏状态,这个时候就需要用 remember 扩展方法保存重组状态,如果使用 boolean 这个基本类型保存那么就无法在重组 ui 后正常的设置组件的状态。

    代码举例(抄官方代码):

    @Composable
    fun HelloContent() {
    Column(modifier = Modifier.padding(16.dp)) {
    OutlinedTextField(
    value = "输入的值",
    onValueChange = { },
    label = { Text("Name") }
    )
    }
    }

    运行上面的代码,我们会发现无论我们如何在 TextField 中输入内容,TextFile 的内容都不会变,这就是因为无法保存状态导致的,以下代码示例可以正常的改变 TextField 中的内容


    @Composable
    fun textFieldStateHasTextShow(){
    var value by remember {//这里就是对TextField中展示的文字进行状态保存的操作
    mutableStateOf("")
    }
    Box(modifier = Modifier.fillMaxSize(1f),contentAlignment = Alignment.Center) {
    OutlinedTextField(
    value = value,
    onValueChange = {
    value=it//每次输入内容的时候,都回调这个更新状态,从而刷新重组ui
    },
    label = { Text("Name") }
    )
    }
    }

    状态管理的常用方法

    remember 重组中保存状态

    组合函数可以通过remember记住单个对象,系统会在初始化期间将remember初始的值存储在组合中。重组的时候可以返回对象值,remember既可以用来存储可变对象又可以存储不可变的对象

    当可组合项被移除后,会忘记 remember 存储的对象。

    mutableStateOf

    mutableStateOf 会创建可观察的 MutableState<T>,例如如下代码: data 就是一个MutableState对象

    每当data.value值发生改变的时候,系统就会重组ui。

    var data = remember {
    mutableStateOf("")
    }

    注:mutableStateOf 必须使用 remember 嵌套才能在数据更改的时候重组界面

    rememberSaveable 保存配置

    remember可以帮助我们在界面重组的时候保存状态,而rememberSaveable可以帮助我们存储配置更改(重新创建activity或进程)时的状态。

    Livedata、Flow、RxJava 转换为状态

    这三个框架是安卓常用的三个响应式开发框架,都支持转化为State对象,以 Flow 举例,如下代码可以转化为一个 State:

      val favorites = MutableStateFlow<Set<String>>(setOf())
    val state = favorites.collectAsState()

    状态管理

    有状态和无状态

    使用 remember、rememberSaveState 方法保存状态的组合项是有状态组合

    反之是无状态组合

    状态提升

    如下代码是官方关于状态提升的代码:

    本例代码中 HelloContent 是无状态的,它的状态被提升到了 HelloScreen 中,HelloContent 有nameonNameChange两个参数,name 是状态,通过 HelloScreen 组合项传给 HelloContent

    而 HelloContent 中发生的更改它也不能自己进行处理,必须将更改传给HelloScreen进行处理并重组界面。

    以上的逻辑叫做:状态下降,事件上升

    @Composable
    fun HelloScreen() {
    var name by rememberSaveable { mutableStateOf("") }

    HelloContent(name = name, onNameChange = { name = it })
    }

    @Composable
    fun HelloContent(name: String, onNameChange: (String) -> Unit) {
    Column(modifier = Modifier.padding(16.dp)) {
    Text(
    text = "Hello, $name",
    modifier = Modifier.padding(bottom = 8.dp),
    style = MaterialTheme.typography.h5
    )
    OutlinedTextField(
    value = name,
    onValueChange = onNameChange,
    label = { Text("Name") }
    )
    }
    }

    存储状态的方式

    前面的介绍中我们知道使用rememberSaveable方法我们可以通过 Bundle 的方式保存状态,那么如果我们要保存的状态不方便用 Bundle 的情况下该何如处理呢?

    以下三种方式,可以实现对非 Bundle 的数据的保存(配置更改后的保存)

    Parcelize

    代码示例:

    @Parcelize
    data class City(val name: String, val country: String) : Parcelable

    @Composable
    fun CityScreen() {
    var selectedCity = rememberSaveable {
    mutableStateOf(City("Madrid", "Spain"))
    }
    }

    MapSaver

    data class City(val name: String, val country: String)

    val CitySaver = run {
    val nameKey = "Name"
    val countryKey = "Country"
    mapSaver(
    save = { mapOf(nameKey to it.name, countryKey to it.country) },
    restore = { City(it[nameKey] as String, it[countryKey] as String) }
    )
    }

    @Composable
    fun CityScreen() {
    var selectedCity = rememberSaveable(stateSaver = CitySaver) {
    mutableStateOf(City("Madrid", "Spain"))
    }
    }

    ListSaver

    data class City(val name: String, val country: String)

    val CitySaver = listSaver<City, Any>(
    save = { listOf(it.name, it.country) },//数组中保存的值和City中的属性是顺序对应的
    restore = { City(it[0] as String, it[1] as String) }
    )

    @Composable
    fun CityScreen() {
    var selectedCity = rememberSaveable(stateSaver = CitySaver) {
    mutableStateOf(City("Madrid", "Spain"))
    }
    }

    状态管理源码分析

    remember

    初次阅读 remember 的源码,可能有理解不对的地方(但总得有人先去看不是),多多见谅,欢迎指正

    • remember 方法调用的主流程

    remember方法返回的是一个MutableState对象,MutableState可以在数据更新的时候通知系统重组ui

     rememberedValue 就是数据转换的逻辑

    • rememberedValue 方法解析

    inserting:如果我们正在将新的节点插入到视图数中,那么 inserting=true

    reusing:意为正在重用,我的理解是当前正在重新使用这个状态,所以避免多次获取

    • reader.next 方法 晒一段源码
      fun next(): Any? {
    if (emptyCount > 0 || currentSlot >= currentSlotEnd) return Composer.Empty
    return slots[currentSlot++]
    }

    slots是一个数组,currentSlot表示我们要获取到的状态在数组中的索引,compose 构建页面是单线程的,索引每次我们调用remember方法的时候如果状态已经存在就从slots中获取数据,然后把currentSlot索引加 1,这样当我们调用了最后一个remember方法的时候currentSlot索引刚好等于slots数组.length-1


    收起阅读 »

    我用index作为key也没啥问题啊,为什么面试还有人diao我???

    所有熟悉 Vue 技术栈的小伙伴,都知道在列表渲染的场景下,不能使用 index 或 random 作为 key。 也有很多小伙伴在面试的时候会被面试官比较详细的追问,假如使用 index 作为 key 会有什么问题?假如使用 random 作为 key 会有...
    继续阅读 »

    所有熟悉 Vue 技术栈的小伙伴,都知道在列表渲染的场景下,不能使用 indexrandom 作为 key


    也有很多小伙伴在面试的时候会被面试官比较详细的追问,假如使用 index 作为 key 会有什么问题?假如使用 random 作为 key 会有什么问题?假如使用一个唯一不变的 id 作为 key 有什么好处呢?


    这道题目,表面上看起来是考察我们对同级比较过程中 diff 算法的理解,唯一不变的 key 可以帮助我们更快的找到可复用的 VNode,节省性能开销,使用 index 作为 key 有可能造成 VNode 错误的复用,从而产生 bug ,而使用 random 作为 key 会导致VNode 始终无法复用,极大的影响性能。


    这么回答有问题么?没有问题。


    但是假如这道题目满分100,我只能给你99分。


    还有 1分,涉及到 Vue 更新流程中的一点点细节,若不理清,可能在实际的业务场景中给我们造成困扰。


    啥困扰呢?


    举个栗子


    直奔主题,看一段代码,index 作为 key ,假如我们删除某一条,结果会是啥呢?


    <template>
     <div id="app">
       <div v-for="(item, index) in data" :key="index">
         <Child />
         <button @click="handleDelete(index)">删除这一行</button>
       </div>
     </div>
    </template>

    <script>

    export default {
     name: "App",
     components: {
       Child: {
         template: '<span>{{name}}{{Math.floor(Math.random() * 1000)}}</span>',
         props: ['name']
      }
    },
     data() {
       return {
         data: [
          { name: "小明" },
          { name: "小红" },
          { name: "小蓝" },
          { name: "小紫" },
        ]
      };
    },
     methods: {
       handleDelete(index) {
         this.data.splice(index, 1);
      },
    }
    };
    </script>

    看结果



    可以观察到,虽然我们删除的不是最后一条,但最终却是最后一条被删除了,看起来很奇怪,但是假如你了解过 Vuediff 流程,这个结果应该是可以符合你的预期的。


    diff


    大段的列源码,会增加我们的理解负担,所以我把 Vue更新流程简化成一张图:



    通常来讲,我们说 Vuediff 流程,指的就是 patchVnode ,其中 updateChildren 就是我们说的同层比较,其实就是比较新旧两个 Vnode 数组。


    Vue 会声明四个指针变量,分别记录新旧 Vnode 数组的首尾索引,通过首尾索引指针的移动,根据新头旧头、新尾旧尾、旧头新尾、旧尾新头的顺序,依次比较新旧 Vnode ,若不能命中 sameVnode,则将oldVnode.key 维护成一个 map, 继续查询是否包含newVnode.key ,若命中 sameVnode ,则递归执行 patchVnode。若最终无法命中,说明无可复用的 Vnode ,创建新的 dom 节点。


    newVnode 的首尾指针先相遇,说明 newVnode 已经遍历完成,直接移除 oldVnode 多余部分,若 oldVnode 的首尾指针先相遇,说明 oldVnode 已经遍历完成,直接新增 newVnode 的多余部分。


    这种直接的文字描述会显得比较苍白,所以我给大家准备了个动画


    第一步:



    第二步:



    第三步:



    第四步:



    第五步:



    第六步:



    理论上,只要你滑动的足够快,这几张图就可以动起来😊



    上面描述updateChildren过程的图片均摘自 Vue技术揭秘 组件更新章节,建议大家翻阅原文


    我尝试了半天实在做不出来动画,同时感觉这几张图已经可以带给我们足够直观的感受了,所以直接搬运了


    侵删



    使用 index 作为 key 会有什么问题


    上面我们讲,判断新旧 Vnode 是否可以复用,取决于 sameNode 方法,这个方法非常简单,就是比对 Vnode 的部分属性,其中 key 是最关键的因素


    function sameVnode (a, b) {
       return (
         a.key === b.key &&
         a.asyncFactory === b.asyncFactory && (
          (
             a.tag === b.tag &&
             a.isComment === b.isComment &&
             isDef(a.data) === isDef(b.data) &&
             sameInputType(a, b)
          ) || (
             isTrue(a.isAsyncPlaceholder) &&
             isUndef(b.asyncFactory.error)
          )
        )
      )
    }

    我们再回到上面的栗子,看看是哪里出了问题


    上面代码生成的 VNode 大约是这样的:


    [
    {
       tag: 'div',
    key: 0,
    children: [
    {
    tag: VueComponent,
           elm: 408, // 这个Vnode对应的真实dom是408
    },
    {
    tag: 'button'
    }
    ]
    },
    {
       tag: 'div',
    key: 1,
    children: [
    {
    tag: VueComponent,
           elm: 227, // 这个Vnode对应的真实dom是227
    },
    {
    tag: 'button'
    }
    ]
    }
     ...
    ]

    我们删除第一条数据,新的 VNode 大约是这样的:


    [
    {
       tag: 'div',
    key: 0,
    children: [
    {
    tag: VueComponent,
           elm: 227, // 这个Vnode对应的真实dom是227
    },
    {
    tag: 'button'
    }
    ]
    },
    {
       tag: 'div',
    key: 1,
    children: [
    {
    tag: VueComponent,
           elm: 324, // 这个Vnode对应的真实dom是324
    },
    {
    tag: 'button'
    }
    ]
    }
     ...
    ]

    我们人肉逻辑 一下这两个 Vnode 数组,由于 key 都是0,所以比较第一条的时候,就会命中 sameNode ,导致错误复用,然后 updateChildren ,子节点的 Vnode 依然会命中 sameVnode ,同理,第二、三条均会命中 sameVnode ,而直接错误复用其关联的真实 dom 节点,所以我们明明删除的是第一条,UI表现却是最后一条被删除了。


    那么到这里就结束了么?


    当然没有,因为很多小伙伴在刚接触 Vue 的时候,也用过 index 作为 key ,部分牛逼的项目甚至已经上线了,似乎也没人来找麻烦


    why?


    为什么我用 index 作为 key 没出现问题


    如果我把代码改成这样,再删除某一条,会是什么结果呢?


    <template>
     <div id="app">
       <div v-for="(item, index) in data" :key="index">
         <Child :name="`${item.name}`" />
         <button @click="handleDelete(index)">删除这一行</button>
       </div>
     </div>
    </template>

    看结果



    法克,我们明明把 Vue更新流程捋清楚了,用 index 作为 key 会导致 Vnode 错误复用啊,怎么这里表现却正常了呢?


    我们再看一下更新流程简化图:



    组件类型的 Vnode ,在 patchVnode 的过程中会执行 prePatch 钩子函数,给组件的 propsData 重新赋值,从而触发 setter ,假如 propsData 的值有变化,则会触发 update ,重新渲染组件


    我们可以再人肉逻辑 一下,这次我们删除的是第二条,因为key 一致,新的 Vnode 数组依然会复用旧的 Vnode 数组的前三条,第一条 Vnode 是正确复用,组件的 propsData 未发生变化,不会触发 update ,直接复用其关联的真实 dom 节点,但是第二条 Vnode 是错误复用,但是组件的 propsData 发生变化,由小红变成了小蓝,触发了 update ,组件重新渲染,因此我们看到其实连 random 都发生了变化,第三条同理。


    呼~


    到这里,总算是搞明白了,我可真是个小机灵鬼


    那么到这里就结束了么?


    其实还没有,比如我们再改一下代码


    <template>
     <div id="app">
       <div v-for="(item, index) in data" :key="index">
         <span>{{item.name}}</span>
         <button @click="handleDelete(index)">删除这一行</button>
       </div>
     </div>
    </template>

    看结果



    这次我们没有组件类型 Vnode ,不会执行 prePatch,为啥表现还是正常的呢?


    再观察一下上面的更新流程图,文本类型的 Vnode ,新旧文本不同的时候是会直接覆盖的。


    到这里,我们已经完全明白,列表渲染的场景下,为什么推荐使用唯一不变的 id 作为 key了。抛开代码规范不谈,即使某些场景下,问题并未以 bug 的形式暴露出来,但是不能复用、或者错误复用 Vnode ,都会导致组件重新渲染,这部分的性能包袱还是非常沉重的!


    最后的1分


    纸上得来终觉浅,绝知此事要躬行


    我第一次读完 Vue2 源码的时候,以为自己已经清晰的明白了这部分知识,直到团队里的小伙伴拿着一个纯文本类型的列表来质问我


    不得已仔细 debug 了一遍更新流程,才算解开了心中疑惑,补上了这 1分 的缺口



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

    收起阅读 »

    Android 10 启动分析之Init篇 (一)

    按下电源键时,android做了啥?当我们按下电源键时,手机开始上电,并从地址0x00000000处开始执行,而这个地址通常是Bootloader程序的首地址。bootloader是一段裸机程序,是直接与硬件打交道的,其最终目的是“初始化并检测硬件设备,准备好...
    继续阅读 »

    按下电源键时,android做了啥?

    当我们按下电源键时,手机开始上电,并从地址0x00000000处开始执行,而这个地址通常是Bootloader程序的首地址。

    bootloader是一段裸机程序,是直接与硬件打交道的,其最终目的是“初始化并检测硬件设备,准备好软件环境,最后调用操作系统内核”。除此之外,bootloader还有保护功能,部分品牌的手机对bootloader做了加锁操作,防止boot分区和recovery分区被写入。

    或许有人会问了,什么是boot分区,什么又是recovery分区?

    我们先来认识一下Android系统的常见分区:

    /boot

    这个分区上有Android的引导程序,包括内核和内存操作程序。没有这个分区设备就不能被引导。恢复系统的时候会擦除这个分区,并且必须重新安装引导程序和ROM才能重启系统。

    /recovery

    recovery分区被认为是另一个启动分区,你可以启动设备进入recovery控制台去执行高级的系统恢复和管理操作。

    /data

    这个分区保存着用户数据。通讯录、短信、设置和你安装的apps都在这个分区上。擦除这个分区相当于恢复出厂设置,当你第一次启动设备的时候或者在安装了官方或者客户的ROM之后系统会自动重建这个分区。当你执行恢复出厂设置时,就是在擦除这个分区。

    /cache

    这个分区是Android系统存储频繁访问的数据和app的地方。擦除这个分区不影响你的个人数据,当你继续使用设备时,被擦除的数据就会自动被创建。

    /apex

    Android Q新增特性,将系统功能模块化,允许系统按模块来独立升级。此分区用于存放apex 相关的内容。

    为什么需要bootloader去拉起linux内核,而不把bootloader这些功能直接内置在linux内核中呢?这个问题不在此做出回答,留给大家自行去思考。

    bootloader完成初始化工作后,会载入 /boot 目录下面的 kernel,此时控制权转交给操作系统。操作系统将要完成的存储管理、设备管理、文件管理、进程管理、加载驱动等任务的初始化工作,以便进入用户态。

    内核启动完成后,将会寻找init文件(init文件位于/system/bin/init),启动init进程,也就是android的第一个进程。

    我们来关注一下内核的common/init/main.c中的kernel_init方法。

    static int __ref kernel_init(void *unused)
    {
    ...

    if (execute_command) {
    ret = run_init_process(execute_command);
    if (!ret)
    return 0;
    }
    if (CONFIG_DEFAULT_INIT[0] != '\0') {
    ret = run_init_process(CONFIG_DEFAULT_INIT);
    if (ret)
    pr_err("Default init %s failed (error %d)\n",CONFIG_DEFAULT_INIT, ret);
    else
    return 0;
    }

    if (!try_to_run_init_process("/sbin/init") ||!try_to_run_init_process("/etc/init") ||
    !try_to_run_init_process("/bin/init") ||!try_to_run_init_process("/bin/sh"))
    return 0;
    }

    可以看到,在init_kernel的最后,会调用run_init_process方法来启动init进程。

    static int run_init_process(const char *init_filename){
    const char *const *p;
    argv_init[0] = init_filename;
    return kernel_execve(init_filename, argv_init, envp_init);
    }

    kernel_execve是内核空间调用用户空间的应用程序的函数。

    接下来我们来重点分析init进程。

    init进程解析

    我们从system/core/init/main.cpp 这个文件开始看起。

    int main(int argc, char** argv) {
    #if __has_feature(address_sanitizer)
    __asan_set_error_report_callback(AsanReportCallback);
    #endif

    if (!strcmp(basename(argv[0]), "ueventd")) {
    return ueventd_main(argc, argv);
    }

    if (argc > 1) {
    if (!strcmp(argv[1], "subcontext")) {
    android::base::InitLogging(argv, &android::base::KernelLogger);
    const BuiltinFunctionMap function_map;

    return SubcontextMain(argc, argv, &function_map);
    }

    if (!strcmp(argv[1], "selinux_setup")) {
    return SetupSelinux(argv);
    }

    if (!strcmp(argv[1], "second_stage")) {
    return SecondStageMain(argc, argv);
    }
    }

    return FirstStageMain(argc, argv);
    }

    第一个参数argc表示参数个数,第二个参数是参数列表,也就是具体的参数。

    main函数有四个参数入口:

    • 一是参数中有ueventd,进入ueventd_main

    • 二是参数中有subcontext,进入InitLogging 和SubcontextMain

    • 三是参数中有selinux_setup,进入SetupSelinux

    • 四是参数中有second_stage,进入SecondStageMain

    main的执行顺序如下:

    1.  FirstStageMain  启动第一阶段

    2. SetupSelinux    加载selinux规则,并设置selinux日志,完成SELinux相关工作

    3. SecondStageMain  启动第二阶段

    4.  ueventd_main    init进程创建子进程ueventd,并将创建设备节点文件的工作托付给ueventd。

    FirstStageMain

    我们来从FirstStageMain的源码看起,源码位于/system/core/init/first_stage_init.cpp

    int FirstStageMain(int argc, char** argv) {

    boot_clock::time_point start_time = boot_clock::now();

    #define CHECKCALL(x) \
    if (x != 0) errors.emplace_back(#x " failed", errno);

    // Clear the umask.
    umask(0);

    //初始化系统环境变量
    CHECKCALL(clearenv());
    CHECKCALL(setenv("PATH", _PATH_DEFPATH, 1));
    // 挂载及创建基本的文件系统,并设置合适的访问权限
    CHECKCALL(mount("tmpfs", "/dev", "tmpfs", MS_NOSUID, "mode=0755"));
    CHECKCALL(mkdir("/dev/pts", 0755));
    CHECKCALL(mkdir("/dev/socket", 0755));
    CHECKCALL(mount("devpts", "/dev/pts", "devpts", 0, NULL));
    #define MAKE_STR(x) __STRING(x)
    CHECKCALL(mount("proc", "/proc", "proc", 0, "hidepid=2,gid=" MAKE_STR(AID_READPROC)));
    #undef MAKE_STR
    // 不要将原始命令行公开给非特权进程
    CHECKCALL(chmod("/proc/cmdline", 0440));
    gid_t groups[] = {AID_READPROC};
    CHECKCALL(setgroups(arraysize(groups), groups));
    CHECKCALL(mount("sysfs", "/sys", "sysfs", 0, NULL));
    CHECKCALL(mount("selinuxfs", "/sys/fs/selinux", "selinuxfs", 0, NULL));

    CHECKCALL(mknod("/dev/kmsg", S_IFCHR | 0600, makedev(1, 11)));

    if constexpr (WORLD_WRITABLE_KMSG) {
    CHECKCALL(mknod("/dev/kmsg_debug", S_IFCHR | 0622, makedev(1, 11)));
    }

    //创建linux随机伪设备文件
    CHECKCALL(mknod("/dev/random", S_IFCHR | 0666, makedev(1, 8)));
    CHECKCALL(mknod("/dev/urandom", S_IFCHR | 0666, makedev(1, 9)));

    //log wrapper所必须的,需要在ueventd运行之前被调用
    CHECKCALL(mknod("/dev/ptmx", S_IFCHR | 0666, makedev(5, 2)));
    CHECKCALL(mknod("/dev/null", S_IFCHR | 0666, makedev(1, 3)));

    ...

    //将内核的stdin/stdout/stderr 全都重定向/dev/null,关闭默认控制台输出
    SetStdioToDevNull(argv);
    // tmpfs已经挂载到/dev上,同时我们也挂载了/dev/kmsg,我们能够与外界开始沟通了
    //初始化内核log
    InitKernelLogging(argv);

    //检测上面的操作是否发生了错误
    if (!errors.empty()) {
    for (const auto& [error_string, error_errno] : errors) {
    LOG(ERROR) << error_string << " " << strerror(error_errno);
    }
    LOG(FATAL) << "Init encountered errors starting first stage, aborting";
    }

    LOG(INFO) << "init first stage started!";

    auto old_root_dir = std::unique_ptr<DIR, decltype(&closedir)>{opendir("/"), closedir};
    if (!old_root_dir) {
    PLOG(ERROR) << "Could not opendir("/"), not freeing ramdisk";
    }

    struct stat old_root_info;

    ...

    //挂载 system、cache、data 等系统分区
    if (!DoFirstStageMount()) {
    LOG(FATAL) << "Failed to mount required partitions early ...";
    }

    ...

    //进入下一步,SetupSelinux
    const char* path = "/system/bin/init";
    const char* args[] = {path, "selinux_setup", nullptr};
    execv(path, const_cast<char**>(args));

    return 1;
    }

    我们来总结一下,FirstStageMain到底做了哪些重要的事情:

    1. 挂载及创建基本的文件系统,并设置合适的访问权限

    2. 关闭默认控制台输出,并初始化内核级log。

    3. 挂载 system、cache、data 等系统分区

    SetupSelinux

    这个模块主要的工作是设置SELinux安全策略,本章内容主要聚焦于android的启动流程,selinux的内容在此不做展开。

    int SetupSelinux(char** argv) {

    ...

    const char* path = "/system/bin/init";
    const char* args[] = {path, "second_stage", nullptr};
    execv(path, const_cast<char**>(args));

    return 1;
    }

    SetupSelinux的最后,进入了init的第二阶段SecondStageMain。

    SecondStageMain

    不多说,先上代码。

    int SecondStageMain(int argc, char** argv) {
    // 禁止OOM killer 杀死该进程以及它的子进程
    if (auto result = WriteFile("/proc/1/oom_score_adj", "-1000"); !result) {
    LOG(ERROR) << "Unable to write -1000 to /proc/1/oom_score_adj: " << result.error();
    }

    // 启用全局Seccomp,Seccomp是什么请自行查阅资料
    GlobalSeccomp();

    // 设置所有进程都能访问的会话密钥
    keyctl_get_keyring_ID(KEY_SPEC_SESSION_KEYRING, 1);

    // 创建 /dev/.booting 文件,就是个标记,表示booting进行中
    close(open("/dev/.booting", O_WRONLY | O_CREAT | O_CLOEXEC, 0000));

    //初始化属性服务,并从指定文件读取属性
    property_init();

    ...

    // 进行SELinux第二阶段并恢复一些文件安全上下文
    SelinuxSetupKernelLogging();
    SelabelInitialize();
    SelinuxRestoreContext();

    //初始化Epoll,android这里对epoll做了一层封装
    Epoll epoll;
    if (auto result = epoll.Open(); !result) {
    PLOG(FATAL) << result.error();
    }

    //epoll 中注册signalfd,主要是为了创建handler处理子进程终止信号
    InstallSignalFdHandler(&epoll);

    ...

    //epoll 中注册property_set_fd,设置其他系统属性并开启系统属性服务
    StartPropertyService(&epoll);
    MountHandler mount_handler(&epoll);

    ...

    ActionManager& am = ActionManager::GetInstance();
    ServiceList& sm = ServiceList::GetInstance();
    //解析init.rc等文件,建立rc文件的action 、service,启动其他进程,十分关键的一步
    LoadBootScripts(am, sm);

    ...

    am.QueueBuiltinAction(SetupCgroupsAction, "SetupCgroups");

    //执行rc文件中触发器为 on early-init 的语句
    am.QueueEventTrigger("early-init");

    // 等冷插拔设备初始化完成
    am.QueueBuiltinAction(wait_for_coldboot_done_action, "wait_for_coldboot_done");

    am.QueueBuiltinAction(MixHwrngIntoLinuxRngAction, "MixHwrngIntoLinuxRng");
    am.QueueBuiltinAction(SetMmapRndBitsAction, "SetMmapRndBits");
    am.QueueBuiltinAction(SetKptrRestrictAction, "SetKptrRestrict");

    // 设备组合键的初始化操作
    Keychords keychords;
    am.QueueBuiltinAction(
    [&epoll, &keychords](const BuiltinArguments& args) -> Result<Success> {
    for (const auto& svc : ServiceList::GetInstance()) {
    keychords.Register(svc->keycodes());
    }
    keychords.Start(&epoll, HandleKeychord);
    return Success();
    },
    "KeychordInit");
    am.QueueBuiltinAction(console_init_action, "console_init");

    // 执行rc文件中触发器为on init的语句
    am.QueueEventTrigger("init");

    // Starting the BoringSSL self test, for NIAP certification compliance.
    am.QueueBuiltinAction(StartBoringSslSelfTest, "StartBoringSslSelfTest");

    // Repeat mix_hwrng_into_linux_rng in case /dev/hw_random or /dev/random
    // wasn't ready immediately after wait_for_coldboot_done
    am.QueueBuiltinAction(MixHwrngIntoLinuxRngAction, "MixHwrngIntoLinuxRng");


    am.QueueBuiltinAction(InitBinder, "InitBinder");

    // 当设备处于充电模式时,不需要mount文件系统或者启动系统服务,充电模式下,将charger设为执行队列,否则把late-init设为执行队列
    std::string bootmode = GetProperty("ro.bootmode", "");
    if (bootmode == "charger") {
    am.QueueEventTrigger("charger");
    } else {
    am.QueueEventTrigger("late-init");
    }

    // 基于属性当前状态 运行所有的属性触发器.
    am.QueueBuiltinAction(queue_property_triggers_action, "queue_property_triggers");

    while (true) {
    //开始进入死循环状态
    auto epoll_timeout = std::optional<std::chrono::milliseconds>{};

    //执行关机重启流程
    if (do_shutdown && !shutting_down) {
    do_shutdown = false;
    if (HandlePowerctlMessage(shutdown_command)) {
    shutting_down = true;
    }
    }

    if (!(waiting_for_prop || Service::is_exec_service_running())) {
    am.ExecuteOneCommand();
    }
    if (!(waiting_for_prop || Service::is_exec_service_running())) {
    if (!shutting_down) {
    auto next_process_action_time = HandleProcessActions();

    // If there's a process that needs restarting, wake up in time for that.
    if (next_process_action_time) {
    epoll_timeout = std::chrono::ceil<std::chrono::milliseconds>(
    *next_process_action_time - boot_clock::now());
    if (*epoll_timeout < 0ms) epoll_timeout = 0ms;
    }
    }

    // If there's more work to do, wake up again immediately.
    if (am.HasMoreCommands()) epoll_timeout = 0ms;
    }

    // 循环等待事件发生
    if (auto result = epoll.Wait(epoll_timeout); !result) {
    LOG(ERROR) << result.error();
    }
    }

    return 0;
    }

    总结一下,第二阶段做了以下这些比较重要的事情:

    1. 初始化属性服务,并从指定文件读取属性
    2. 初始化epoll,并注册signalfd和property_set_fd,建立和init的子进程以及部分服务的通讯桥梁
    3. 初始化设备组合键,使系统能够对组合键信号做出响应
    4. 解析init.rc文件,并按rc里的定义去启动服务
    5. 开启死循环,用于接收epoll的事件

    在第二阶段,我们需要重点关注以下问题:

    init进程是如何通过init.rc配置文件去启动其他的进程的呢?

    init.rc 解析

    我们从 LoadBootScripts(am, sm)这个方法开始看起,一步一部来挖掘init.rc 的解析流程。

    static void LoadBootScripts(ActionManager& action_manager, ServiceList& service_list) {
    //初始化ServiceParse、ActionParser、ImportParser三个解析器
    Parser parser = CreateParser(action_manager, service_list);

    std::string bootscript = GetProperty("ro.boot.init_rc", "");
    if (bootscript.empty()) {
    //bootscript为空,进入此分支
    parser.ParseConfig("/init.rc");
    if (!parser.ParseConfig("/system/etc/init")) {
    late_import_paths.emplace_back("/system/etc/init");
    }
    if (!parser.ParseConfig("/product/etc/init")) {
    late_import_paths.emplace_back("/product/etc/init");
    }
    if (!parser.ParseConfig("/product_services/etc/init")) {
    late_import_paths.emplace_back("/product_services/etc/init");
    }
    if (!parser.ParseConfig("/odm/etc/init")) {
    late_import_paths.emplace_back("/odm/etc/init");
    }
    if (!parser.ParseConfig("/vendor/etc/init")) {
    late_import_paths.emplace_back("/vendor/etc/init");
    }
    } else {
    parser.ParseConfig(bootscript);
    }
    }

    我们可以看到这句话,Parse开始解析init.rc文件,在深入下去之前,让我们先来认识一下init.rc。

     parser.ParseConfig("/init.rc")

    init.rc是一个可配置的初始化文件,负责系统的初步建立。它的源文件的路径为 /system/core/rootdir/init.rc

    init.rc文件有着固定的语法,由于内容过多,限制于篇幅的原因,在此另外单独开了一篇文章进行讲解:

    Android 10 启动分析之init语法

    了解了init.rc的语法后,我们来看看init.rc文件里的内容。

    import /init.environ.rc  //导入全局环境变量
    import /init.usb.rc //adb 服务、USB相关内容的定义
    import /init.${ro.hardware}.rc //硬件相关的初始化,一般是厂商定制
    import /vendor/etc/init/hw/init.${ro.hardware}.rc
    import /init.usb.configfs.rc
    import /init.${ro.zygote}.rc //定义Zygote服务

    我们可以看到,在/system/core/init目录下,存在以下四个zygote相关的文件

    image.png

    怎样才能知道我们当前的手机用的是哪个配置文件呢?

    答案是通过adb shell getprop | findstr ro.zygote命令,看看${ro.zygote}这个环境变量具体的值是什么,笔者所使用的华为手机的ro.zygote值如下所示:

    image.png

    什么是Zygote,Zygote的启动过程是怎样的,它的启动配置文件里又做了啥,在这里我们不再做进一步探讨, 只需要知道init在一开始在这个文件中对Zygote服务做了定义,而上述的这些问题将留到 启动分析之Zygote篇 再去说明。

    on early-init
    # Disable sysrq from keyboard
    write /proc/sys/kernel/sysrq 0

    # Set the security context of /adb_keys if present.
    restorecon /adb_keys

    # Set the security context of /postinstall if present.
    restorecon /postinstall

    mkdir /acct/uid

    # memory.pressure_level used by lmkd
    chown root system /dev/memcg/memory.pressure_level
    chmod 0040 /dev/memcg/memory.pressure_level
    # app mem cgroups, used by activity manager, lmkd and zygote
    mkdir /dev/memcg/apps/ 0755 system system
    # cgroup for system_server and surfaceflinger
    mkdir /dev/memcg/system 0550 system system

    start ueventd

    # Run apexd-bootstrap so that APEXes that provide critical libraries
    # become available. Note that this is executed as exec_start to ensure that
    # the libraries are available to the processes started after this statement.
    exec_start apexd-bootstrap

    紧接着是一个Action,Action的Trigger 为early-init,在这个 Action中,我们需要关注最后两行,它启动了ueventd服务和apex相关服务。还记得什么是ueventd和apex吗?不记得的读者请往上翻越再自行回顾一下。

    ueventd服务的定义也可以在init.rc文件的结尾找到,具体代码及含义如下:

    service ueventd    //ueventd服务的可执行文件的路径为 /system/bin/ueventd
    class core //ueventd 归属于 core class,同样归属于core class的还有adbd 、console等服务
    critical //表明这个Service对设备至关重要,如果Service在四分钟内退出超过4次,则设备将重启进入恢复模式。
    seclabel u:r:ueventd:s0 //selinux相关的配置
    shutdown critical //ueventd服务关闭行为

    然而,early-init 这个Trigger到底什么时候触发呢?

    答案是通过init.cpp代码调用触发。

    我们可以在init.cpp 代码中找到如下代码片段:

    am.QueueEventTrigger("early-init");

    QueueEventTrigger这个方法的实现机制我们稍后再进行探讨,目前我们只需要了解, ActionManager 这个类中的 QueueEventTrigger方法,负责触发init.rc中的Action。

    我们继续往下看init.rc的内容。

    on init

    ...

    # Start logd before any other services run to ensure we capture all of their logs.
    start logd

    # Start essential services.
    start servicemanager
    ...

    在Trigger 为init的Action中,我们只需要关注以上的关键内容。在init的action中启动了一些核心的系统服务,这些服务具体的含义为 :

    服务名含义
    logdAndroid L加入的服务,用于保存Android运行期间的日志
    servicemanagerandroid系统服务管理者,负责查询和注册服务

    接下来是late-init Action:

    on late-init
    //启动vold服务(管理和控制Android平台外部存储设备,包括SD插拨、挂载、卸载、格式化等)
    trigger early-fs
    trigger fs
    trigger post-fs
    trigger late-fs

    //挂载/data , 启动 apexd 服务
    trigger post-fs-data

    # 读取持久化属性或者从/data 中读取并覆盖属性
    trigger load_persist_props_action

    //启动zygote服务!!在启动zygote服务前会先启动netd服务(专门负责网络管理和控制的后台守护进程)
    trigger zygote-start

    //移除/dev/.booting 文件
    trigger firmware_mounts_complete

    trigger early-boot
    trigger boot //初始化网络环境,设置系统环境和守护进程的权限

    最后,我们用流程图来总结一下上述的启动过程:

    first_stage
    second_stage
    设置SetupSelinux安全策略
    挂载system、cache、data等系统分区
    初始化内核log
    初始化基本的文件系统
    初始化系统环境变量
    开启死循环,用于接收epoll的事件
    启动zygote服务
    启动netd服务
    挂载/data,启动apexd服务
    启动vold服务
    启动servicemanager服务
    启动logd服务
    启动ueventd服务
    解析init.rc文件
    初始化设备组合键
    初始化epoll
    初始化属性服务
    Click Power Button
    bootloader
    linux kernel
    init进程
    收起阅读 »

    Android 10 启动分析之servicemanager篇 (二)

    上一篇文章:Android 10 启动分析之Init篇 (一)在前文提到,init进程会在在Trigger 为init的Action中,启动servicemanager服务,这篇文章我们就来具体分析一下servicemanager服务,它到底做了哪些事情。se...
    继续阅读 »

    上一篇文章:

    Android 10 启动分析之Init篇 (一)

    在前文提到,init进程会在在Trigger 为init的Action中,启动servicemanager服务,这篇文章我们就来具体分析一下servicemanager服务,它到底做了哪些事情。

    servicemanager服务的源码位于/frameworks/native/cmds/servicemanager/service_manager.c,我们将从这个类的入口开始看起。

    int main(int argc, char** argv)
    {
    struct binder_state *bs;
    union selinux_callback cb;
    char *driver;

    if (argc > 1) {
    driver = argv[1];
    } else {
    //启动时默认无参数,走这个分支
    driver = "/dev/binder";
    }

    //打开binder驱动,并设置mmap的内存大小为128k
    bs = binder_open(driver, 128*1024);

    ...

    if (binder_become_context_manager(bs)) {
    ALOGE("cannot become context manager (%s)\n", strerror(errno));
    return -1;
    }

    cb.func_audit = audit_callback;
    selinux_set_callback(SELINUX_CB_AUDIT, cb);
    #ifdef VENDORSERVICEMANAGER
    cb.func_log = selinux_vendor_log_callback;
    #else
    cb.func_log = selinux_log_callback;
    #endif
    selinux_set_callback(SELINUX_CB_LOG, cb);

    #ifdef VENDORSERVICEMANAGER
    sehandle = selinux_android_vendor_service_context_handle();
    #else
    sehandle = selinux_android_service_context_handle();
    #endif
    selinux_status_open(true);

    if (sehandle == NULL) {
    ALOGE("SELinux: Failed to acquire sehandle. Aborting.\n");
    abort();
    }

    if (getcon(&service_manager_context) != 0) {
    ALOGE("SELinux: Failed to acquire service_manager context. Aborting.\n");
    abort();
    }


    /* binder_loop已封装如下步骤:
    while (1)
    {
    /* read data */
    /* parse data, and process */
    /* reply */
    }
    */
    binder_loop(bs, svcmgr_handler);

    return 0;
    }

    从main函数中可以看出,它主要做了三件事情:

    1. 打开/dev/binder设备,并在内存中映射128K的空间。
    2. 通知Binder设备,把自己变成context_manager,其他用户进程都通过0号句柄访问ServiceManager。
    3. 进入循环,不停的去读Binder设备,看是否有对service的请求,如果有的话,就去调用svcmgr_handler函数回调处理请求。

    我们再来看看svcmgr_handler函数的实现:

    int svcmgr_handler(struct binder_state *bs,
    struct binder_transaction_data_secctx *txn_secctx,
    struct binder_io *msg,
    struct binder_io *reply)
    {
    struct svcinfo *si;
    uint16_t *s;
    size_t len;
    uint32_t handle;
    uint32_t strict_policy;
    int allow_isolated;
    uint32_t dumpsys_priority;

    struct binder_transaction_data *txn = &txn_secctx->transaction_data;

    if (txn->target.ptr != BINDER_SERVICE_MANAGER)
    return -1;

    if (txn->code == PING_TRANSACTION)
    return 0;

    ...

    switch(txn->code) {
    case SVC_MGR_GET_SERVICE:
    case SVC_MGR_CHECK_SERVICE:
    s = bio_get_string16(msg, &len);
    if (s == NULL) {
    return -1;
    }
    handle = do_find_service(s, len, txn->sender_euid, txn->sender_pid,
    (const char*) txn_secctx->secctx);
    if (!handle)
    break;
    bio_put_ref(reply, handle);
    return 0;

    case SVC_MGR_ADD_SERVICE:
    s = bio_get_string16(msg, &len);
    if (s == NULL) {
    return -1;
    }
    handle = bio_get_ref(msg);
    allow_isolated = bio_get_uint32(msg) ? 1 : 0;
    dumpsys_priority = bio_get_uint32(msg);
    if (do_add_service(bs, s, len, handle, txn->sender_euid, allow_isolated, dumpsys_priority,
    txn->sender_pid, (const char*) txn_secctx->secctx))
    return -1;
    break;

    case SVC_MGR_LIST_SERVICES: {
    uint32_t n = bio_get_uint32(msg);
    uint32_t req_dumpsys_priority = bio_get_uint32(msg);

    if (!svc_can_list(txn->sender_pid, (const char*) txn_secctx->secctx, txn->sender_euid)) {
    ALOGE("list_service() uid=%d - PERMISSION DENIED\n",
    txn->sender_euid);
    return -1;
    }
    si = svclist;
    // walk through the list of services n times skipping services that
    // do not support the requested priority
    while (si) {
    if (si->dumpsys_priority & req_dumpsys_priority) {
    if (n == 0) break;
    n--;
    }
    si = si->next;
    }
    if (si) {
    bio_put_string16(reply, si->name);
    return 0;
    }
    return -1;
    }
    default:
    ALOGE("unknown code %d\n", txn->code);
    return -1;
    }

    bio_put_uint32(reply, 0);
    return 0;
    }

    我们先来认识一下binder的数据传输载体binder_transaction_data:

    struct binder_transaction_data {
    union {
    /* 当binder_transaction_data是由用户空间的进程发送给Binder驱动时,
    handle是该事务的发送目标在Binder驱动中的信息,即该事务会交给handle来处理;
    handle的值是目标在Binder驱动中的Binder引用。*/
    __u32 handle;

    /* 当binder_transaction_data是有Binder驱动反馈给用户空间进程时,
    ptr是该事务的发送目标在用户空间中的信息,即该事务会交给ptr对应的服务来处理;
    ptr是处理该事务的服务的服务在用户空间的本地Binder对象。*/
    binder_uintptr_t ptr;

    } target; // 该事务的目标对象(即,该事务数据包是给该target来处理的)

    // 只有当事务是由Binder驱动传递给用户空间时,cookie才有意思,它的值是处理该事务的ServerC++层的本地Binder对象
    binder_uintptr_t cookie;
    // 事务编码。如果是请求,则以BC_开头;如果是回复,则以BR_开头。
    __u32 code;

    /* General information about the transaction. */
    __u32 flags;

    //表示事务发起者的pid和uid。
    pid_t sender_pid;
    uid_t sender_euid;

    // 数据大小
    binder_size_t data_size;

    //数据偏移量
    binder_size_t offsets_size;

    //data是一个共用体,当通讯数据很小的时,可以直接使用buf[8]来保存数据。当够大时,只能用指针buffer来描述一个申请的数据缓冲区。
    union {
    struct {
    /* transaction data */
    binder_uintptr_t buffer;

    binder_uintptr_t offsets;
    } ptr;
    __u8 buf[8];
    } data;
    };

    可以看到,svcmgr_handler函数中对binder data的事务编码进行了判断,并分别对SVC_MGR_GET_SERVICE(SVC_MGR_CHECK_SERVICE)SVC_MGR_ADD_SERVICESVC_MGR_LIST_SERVICES三种类型的事务编码做了业务处理。

    获取服务

      case SVC_MGR_CHECK_SERVICE:  
            s = bio_get_string16(msg, &len);  
            ptr = do_find_service(bs, s, len);  
            if (!ptr)  
                break;  
            bio_put_ref(reply, ptr);  
            return 0;

    do_find_service函数中主要执行service的查找,并把找到的服务句柄写入reply,返回给客户端。

    uint32_t do_find_service(const uint16_t *s, size_t len, uid_t uid, pid_t spid, const char* sid)
    {
    struct svcinfo *si = find_svc(s, len);

    ...

    return si->handle;
    }

    我们继续看find_svc函数:

    struct svcinfo *find_svc(const uint16_t *s16, size_t len)
    {
    struct svcinfo *si;

    for (si = svclist; si; si = si->next) {
    if ((len == si->len) &&
    !memcmp(s16, si->name, len * sizeof(uint16_t))) {
    return si;
    }
    }
    return NULL;
    }

    svclist 是一个单向链表,储存了所有向servicemanager注册的服务信息。find_svc遍历svclist链表,通过服务名称作为索引条件,最终找到符合条件的服务。

    注册服务

        case SVC_MGR_ADD_SERVICE:
    s = bio_get_string16(msg, &len);
    if (s == NULL) {
    return -1;
    }
    handle = bio_get_ref(msg);
    allow_isolated = bio_get_uint32(msg) ? 1 : 0;
    dumpsys_priority = bio_get_uint32(msg);
    if (do_add_service(bs, s, len, handle, txn->sender_euid, allow_isolated, dumpsys_priority,
    txn->sender_pid, (const char*) txn_secctx->secctx))
    return -1;

    我们继续看do_add_service函数中做了哪些事情。

    在该函数中,首先会去检查客户端是否有权限注册service,如果没有权限就直接返回,不能注册。

     if (!svc_can_register(s, len, spid, sid, uid)) {
    ALOGE("add_service('%s',%x) uid=%d - PERMISSION DENIED\n",
    str8(s, len), handle, uid);
    return -1;
    }

    然后会去检查该service是否已经注册过了,如果已经注册过,那么就不能再注册了。

     si = find_svc(s, len);
    if (si) {
    if (si->handle) {
    ALOGE("add_service('%s',%x) uid=%d - ALREADY REGISTERED, OVERRIDE\n",
    str8(s, len), handle, uid);
    svcinfo_death(bs, si);
    }
    si->handle = handle;
    }

    再判断内存是否足够。

     si = malloc(sizeof(*si) + (len + 1) * sizeof(uint16_t));
    if (!si) {
    ALOGE("add_service('%s',%x) uid=%d - OUT OF MEMORY\n",
    str8(s, len), handle, uid);
    return -1;
    }

    如果都没什么问题,会注册该service,并加入到svcList链表中。

    综上所述,servicemanager主要负责查询和注册其他的系统服务,是系统服务的管理者。

    文章的最后,留给大家一个问题进行思考:

    为什么Android需要设计servicemanager做中转来添加和获取系统服务,而不直接让客户端去获取服务端句柄?
    收起阅读 »

    Android 10 启动分析之Zygote篇 (三)

    上一篇文章:# Android 10 启动分析之servicemanager篇 (二)app_main在init篇中有提到,init进程会在在Trigger 为late-init的Action中,启动Zygote服务,这篇文章我们就来具体分析一下Zygote服...
    继续阅读 »

    上一篇文章:

    # Android 10 启动分析之servicemanager篇 (二)

    app_main

    在init篇中有提到,init进程会在在Trigger 为late-init的Action中,启动Zygote服务,这篇文章我们就来具体分析一下Zygote服务,去挖掘一下Zygote负责的工作。

    Zygote服务的启动入口源码位于 /frameworks/base/cmds/app_process/app_main.cpp,我们将从这个文件的main方法开始解析。

    int main(int argc, char* const argv[])
    {

    //声明AppRuntime类的实例runtime,在AppRuntime类的构造方法中初始化的skia图形引擎
    AppRuntime runtime(argv[0], computeArgBlockSize(argc, argv));

    ...

    bool zygote = false;
    bool startSystemServer = false;
    bool application = false;
    String8 niceName;
    String8 className;

    ++i; // Skip unused "parent dir" argument.
    while (i < argc) {
    const char* arg = argv[i++];
    if (strcmp(arg, "--zygote") == 0) {
    zygote = true;
    //对于64位系统nice_name为zygote64; 32位系统为zygote
    niceName = ZYGOTE_NICE_NAME;
    } else if (strcmp(arg, "--start-system-server") == 0) {
    //是否需要启动system server
    startSystemServer = true;
    } else if (strcmp(arg, "--application") == 0) {
    //启动进入独立的程序模式
    application = true;
    } else if (strncmp(arg, "--nice-name=", 12) == 0) {
    //niceName 为当前进程别名,区别abi型号
    niceName.setTo(arg + 12);
    } else if (strncmp(arg, "--", 2) != 0) {
    className.setTo(arg);
    break;
    } else {
    --i;
    break;
    }
    }

    ...

    }

    可以看到,app_main根据启动时传入参数的区别,分为zygote 模式和application模式。

    我们可以从init.zygote64_32.rc文件中看到zygote的启动参数为:

    -Xzygote /system/bin --zygote --start-system-server --socket-name=zygote

    我们接着往下看:

    Vector<String8> args;
    if (!className.isEmpty()) {
    // We're not in zygote mode, the only argument we need to pass
    // to RuntimeInit is the application argument.
    //
    // The Remainder of args get passed to startup class main(). Make
    // copies of them before we overwrite them with the process name.
    args.add(application ? String8("application") : String8("tool"));
    runtime.setClassNameAndArgs(className, argc - i, argv + i);

    if (!LOG_NDEBUG) {
    String8 restOfArgs;
    char* const* argv_new = argv + i;
    int argc_new = argc - i;
    for (int k = 0; k < argc_new; ++k) {
    restOfArgs.append(""");
    restOfArgs.append(argv_new[k]);
    restOfArgs.append("" ");
    }
    ALOGV("Class name = %s, args = %s", className.string(), restOfArgs.string());
    }
    } else {
    // We're in zygote mode.
    //初始化Dalvik虚拟机Cache目录和权限
    maybeCreateDalvikCache();

    if (startSystemServer) {
    //附加上start-system-serve 的arg
    args.add(String8("start-system-serve 的argr"));
    }

    char prop[PROP_VALUE_MAX];
    if (property_get(ABI_LIST_PROPERTY, prop, NULL) == 0) {
    LOG_ALWAYS_FATAL("app_process: Unable to determine ABI list from property %s.",
    ABI_LIST_PROPERTY);
    return 11;
    }

    String8 abiFlag("--abi-list=");
    abiFlag.append(prop);
    args.add(abiFlag);

    // In zygote mode, pass all remaining arguments to the zygote
    // main() method.
    for (; i < argc; ++i) {
    args.add(String8(argv[i]));
    }
    }

    if (!niceName.isEmpty()) {
    runtime.setArgv0(niceName.string(), true /* setProcName */);
    }

    if (zygote) {
    //进入此分支
    runtime.start("com.android.internal.os.ZygoteInit", args, zygote);
    } else if (className) {
    runtime.start("com.android.internal.os.RuntimeInit", args, zygote);
    } else {
    fprintf(stderr, "Error: no class name or --zygote supplied.\n");
    app_usage();
    LOG_ALWAYS_FATAL("app_process: no class name or --zygote supplied.");
    }

    结合传入的启动参数来看,代码将从if语句的else分支继续往下执行,进入zygote模式。至于application模式我们暂时先忽略它,等我们分析app的启动过程时再来说明。

    上述代码最后将通过 runtime.start("com.android.internal.os.ZygoteInit", args, zygote);语句,将控制权限转交给AppRuntime类去继续执行。

    继续从AppRuntime的start函数看起:

    void AndroidRuntime::start(const char* className, const Vector<String8>& options, bool zygote)
    {

    ...

    // 虚拟机创建及启动,主要是关于虚拟机参数的设置
    JniInvocation jni_invocation;
    jni_invocation.Init(NULL);
    JNIEnv* env;
    if (startVm(&mJavaVM, &env, zygote) != 0) {
    return;
    }
    onVmCreated(env);

    //注册JNI方法
    if (startReg(env) < 0) {
    ALOGE("Unable to register all android natives\n");
    return;
    }

    /*
    * We want to call main() with a String array with arguments in it.
    * At present we have two arguments, the class name and an option string.
    * Create an array to hold them.
    */
    jclass stringClass;
    jobjectArray strArray;
    jstring classNameStr;

    //等价于strArray[0] = "com.android.internal.os.ZygoteInit"
    stringClass = env->FindClass("java/lang/String");
    assert(stringClass != NULL);
    strArray = env->NewObjectArray(options.size() + 1, stringClass, NULL);
    assert(strArray != NULL);
    classNameStr = env->NewStringUTF(className);
    assert(classNameStr != NULL);
    env->SetObjectArrayElement(strArray, 0, classNameStr);

    //strArray[1] = "start-system-server";
    //strArray[2] = "--abi-list=xxx";
    //其中xxx为系统响应的cpu架构类型,比如arm64-v8a.
    for (size_t i = 0; i < options.size(); ++i) {
    jstring optionsStr = env->NewStringUTF(options.itemAt(i).string());
    assert(optionsStr != NULL);
    env->SetObjectArrayElement(strArray, i + 1, optionsStr);
    }

    /*
    * Start VM. This thread becomes the main thread of the VM, and will
    * not return until the VM exits.
    */
    //将"com.android.internal.os.ZygoteInit"转换为"com/android/internal/os/ZygoteInit
    char* slashClassName = toSlashClassName(className != NULL ? className : "");
    jclass startClass = env->FindClass(slashClassName);
    if (startClass == NULL) {
    ALOGE("JavaVM unable to locate class '%s'\n", slashClassName);
    /* keep going */
    } else {
    //找到这个类后就继续找成员函数main方法的Mehtod ID
    jmethodID startMeth = env->GetStaticMethodID(startClass, "main",
    "([Ljava/lang/String;)V");
    if (startMeth == NULL) {
    ALOGE("JavaVM unable to find main() in '%s'\n", className);
    /* keep going */
    } else {
    // 通过Jni调用ZygoteInit.main()方法
    env->CallStaticVoidMethod(startClass, startMeth, strArray);

    #if 0
    if (env->ExceptionCheck())
    threadExitUncaughtException(env);
    #endif
    }
    }
    free(slashClassName);

    ALOGD("Shutting down VM\n");
    if (mJavaVM->DetachCurrentThread() != JNI_OK)
    ALOGW("Warning: unable to detach main thread\n");
    if (mJavaVM->DestroyJavaVM() != 0)
    ALOGW("Warning: VM did not shut down cleanly\n");
    }

    start()函数主要做了三件事情,一调用startVm开启虚拟机,二调用startReg注册JNI方法,三就是使用JNI把Zygote进程启动起来。

    ZygoteInit

    通过上述分析,代码进入了ZygoteInit.java中的main方法继续执行。从这里开始,就真正的启动了Zygote进程。我们从/frameworks/base/core/java/com/android/internal/os/ZygoteInit.java这个文件继续往下看。

    public static void main(String argv[]) {
    //ZygoteServer 是Zygote进程的Socket通讯服务端的管理类
    ZygoteServer zygoteServer = null;

    // 标记zygote启动开始,调用ZygoteHooks的Jni方法,确保当前没有其它线程在运行
    ZygoteHooks.startZygoteNoThreadCreation();

    //设置pid为0,Zygote进入自己的进程组
    try {
    Os.setpgid(0, 0);
    } catch (ErrnoException ex) {
    throw new RuntimeException("Failed to setpgid(0,0)", ex);
    }

    Runnable caller;
    try {

    ...

    //开启DDMS(Dalvik Debug Monitor Service)功能
    RuntimeInit.enableDdms();

    //解析app_main.cpp - start()传入的参数
    boolean startSystemServer = false;
    String zygoteSocketName = "zygote";
    String abiList = null;
    boolean enableLazyPreload = false;
    for (int i = 1; i < argv.length; i++) {
    if ("start-system-server".equals(argv[i])) {
    //启动zygote时,传入了参数:start-system-server,会进入此分支
    startSystemServer = true;
    } else if ("--enable-lazy-preload".equals(argv[i])) {
    //启动zygote_secondary时,才会传入参数:enable-lazy-preload
    enableLazyPreload = true;
    } else if (argv[i].startsWith(ABI_LIST_ARG)) {
    abiList = argv[i].substring(ABI_LIST_ARG.length());
    } else if (argv[i].startsWith(SOCKET_NAME_ARG)) {
    //SOCKET_NAME_ARG 为 zygote 或zygote_secondary,具体请参考 init.zyoget64_32.rc文件
    zygoteSocketName = argv[i].substring(SOCKET_NAME_ARG.length());
    } else {
    throw new RuntimeException("Unknown command line argument: " + argv[i]);
    }
    }

    // 根据传入socket name来决定是创建socket还是zygote_secondary
    final boolean isPrimaryZygote = zygoteSocketName.equals(Zygote.PRIMARY_SOCKET_NAME);

    if (abiList == null) {
    throw new RuntimeException("No ABI list supplied.");
    }

    // In some configurations, we avoid preloading resources and classes eagerly.
    // In such cases, we will preload things prior to our first fork.
    // 在第一次zygote启动时,enableLazyPreload为false,执行preload
    if (!enableLazyPreload) {
    bootTimingsTraceLog.traceBegin("ZygotePreload");
    EventLog.writeEvent(LOG_BOOT_PROGRESS_PRELOAD_START,
    SystemClock.uptimeMillis());
    // 加载进程的资源和类
    preload(bootTimingsTraceLog);
    EventLog.writeEvent(LOG_BOOT_PROGRESS_PRELOAD_END,
    SystemClock.uptimeMillis());
    bootTimingsTraceLog.traceEnd(); // ZygotePreload
    } else {
    Zygote.resetNicePriority();
    }

    // Do an initial gc to clean up after startup
    bootTimingsTraceLog.traceBegin("PostZygoteInitGC");
    gcAndFinalize();
    bootTimingsTraceLog.traceEnd(); // PostZygoteInitGC

    bootTimingsTraceLog.traceEnd(); // ZygoteInit
    // Disable tracing so that forked processes do not inherit stale tracing tags from
    // Zygote.
    Trace.setTracingEnabled(false, 0);


    Zygote.initNativeState(isPrimaryZygote);

    ZygoteHooks.stopZygoteNoThreadCreation();

    // 调用ZygoteServer 构造函数,创建socket Server端,会根据传入的参数,
    // 创建两个socket:/dev/socket/zygote 和 /dev/socket/zygote_secondary
    zygoteServer = new ZygoteServer(isPrimaryZygote);

    if (startSystemServer) {
    //fork出system server进程
    Runnable r = forkSystemServer(abiList, zygoteSocketName, zygoteServer);

    // {@code r == null} in the parent (zygote) process, and {@code r != null} in the
    // child (system_server) process.
    if (r != null) {
    // 启动SystemServer
    r.run();
    return;
    }
    }

    Log.i(TAG, "Accepting command socket connections");

    // ZygoteServer进入无限循环,处理请求
    caller = zygoteServer.runSelectLoop(abiList);
    } catch (Throwable ex) {
    Log.e(TAG, "System zygote died with exception", ex);
    throw ex;
    } finally {
    if (zygoteServer != null) {4
    zygoteServer.closeServerSocket();
    }
    }

    // We're in the child process and have exited the select loop. Proceed to execute the
    // command.
    if (caller != null) {
    caller.run();
    }
    }

    main方法中主要做了以下几件事:

    1. 加载进程的资源和类。
    2. 根据传入socket name来创建socket server。
    3. fork SystemServer 进程。

    preload

    既然preload方法是负责加载进程的资源和类,那么它究竟加载了哪些资源和哪些类呢,这些资源又位于什么位置呢?

    我们先来看看preload方法里具体做了什么:

    static void preload(TimingsTraceLog bootTimingsTraceLog) {

    beginPreload();
    //预加载类
    preloadClasses();

    cacheNonBootClasspathClassLoaders();
    //加载图片、颜色等资源文件
    preloadResources();
    //加载HAL相关内容
    nativePreloadAppProcessHALs();
    //加载图形驱动
    maybePreloadGraphicsDriver();
    // 加载 android、compiler_rt、jnigraphics等library
    preloadSharedLibraries();
    //用于初始化文字资源
    preloadTextResources();
    //用于初始化webview;
    WebViewFactory.prepareWebViewInZygote();
    endPreload();
    warmUpJcaProviders();


    sPreloadComplete = true;
    }

    preloadClasses

     private static void preloadClasses() {
    final VMRuntime runtime = VMRuntime.getRuntime();


    } catch (IOException e) {
    Log.e(TAG, "Error reading " + PRELOADED_CLASSES + ".", e);
    } finally {
    ...
    }
    }

    可以看到,preloadClasses方法读取/system/etc/preloaded-classes文件的内容,并通过Class.forName初始化类。那么在/system/etc/preloaded-classes文件具体有哪些类呢?

    由于内容过多,我这里只截取部分截图让大家看看具体装载是什么类。

    image.png

    image.png

    image.png

    从装载列表中,我们可以看到很多熟悉的类,实际上,装载的类都是我们应用程序运行时可能用到的java类。

    preloadResources

    private static void preloadResources() {
    final VMRuntime runtime = VMRuntime.getRuntime();


    mResources.finishPreloading();
    } catch (RuntimeException e) {
    Log.w(TAG, "Failure preloading resources", e);
    }
    }

    从上述代码可以看到,preloadResources加载了特定的图片资源和颜色资源。这些资源的路径又具体在哪里呢?

    com.android.internal.R.array.preloaded_drawables的路径位于/frameworks/base/core/res/res/values/arrays.xml中,其他的资源路径也可以类似找到。各位读者可以自行去该路径下去看看所包含的资源文件到底是什么样的。

    preloadSharedLibraries

    private static void preloadSharedLibraries() {
    Log.i(TAG, "Preloading shared libraries...");
    System.loadLibrary("android");
    System.loadLibrary("compiler_rt");
    System.loadLibrary("jnigraphics");
    }

    preloadSharedLibraries里的内容很简单,主要是加载位于/system/lib目录下的libandroid.so、libcompiler_rt.so、libjnigraphics.so三个so库。


    我们不妨想一下,为什么android要在Zygote中将资源先进行预加载,这么做有什么好处?

    这个问题留给各位读者去自行思考,在这里便不再回答了。

    forkSystemServer

     private static Runnable forkSystemServer(String abiList, String socketName,
    ZygoteServer zygoteServer) {
    ...


    return null;
    }

    forkSystemServer方法只是fork了一个Zygote的子进程,而handleSystemServerProcess方法构造了一个Runnable对象,创建一个子线程用于启动SystemServer的逻辑。

    private static Runnable handleSystemServerProcess(ZygoteArguments parsedArgs) {
    Os.umask(S_IRWXG | S_IRWXO);

    if (parsedArgs.mNiceName != null) {
    //nicename 为 system_server
    Process.setArgV0(parsedArgs.mNiceName);
    }

    ...

    if (parsedArgs.mInvokeWith != null) {
    String[] args = parsedArgs.mRemainingArgs;
    // If we have a non-null system server class path, we'll have to duplicate the
    // existing arguments and append the classpath to it. ART will handle the classpath
    // correctly when we exec a new process.
    if (systemServerClasspath != null) {
    String[] amendedArgs = new String[args.length + 2];
    amendedArgs[0] = "-cp";
    amendedArgs[1] = systemServerClasspath;
    System.arraycopy(args, 0, amendedArgs, 2, args.length);
    args = amendedArgs;
    }

    WrapperInit.execApplication(parsedArgs.mInvokeWith,
    parsedArgs.mNiceName, parsedArgs.mTargetSdkVersion,
    VMRuntime.getCurrentInstructionSet(), null, args);

    throw new IllegalStateException("Unexpected return from WrapperInit.execApplication");
    } else {
    //parsedArgs.mInvokeWith 为null,会进入此分支
    createSystemServerClassLoader();
    ClassLoader cl = sCachedSystemServerClassLoader;
    if (cl != null) {
    Thread.currentThread().setContextClassLoader(cl);
    }

    /*
    * Pass the remaining arguments to SystemServer.
    */
    return ZygoteInit.zygoteInit(parsedArgs.mTargetSdkVersion,
    parsedArgs.mRemainingArgs, cl);
    }

    /* should never reach here */
    }

    继续从ZygoteInit.zygoteInit看起:

    public static final Runnable zygoteInit(int targetSdkVersion, String[] argv,
    ClassLoader classLoader) {
    ...

    RuntimeInit.commonInit();
    //注册两个jni函数
    //android_internal_os_ZygoteInit_nativePreloadAppProcessHALs
    //android_internal_os_ZygoteInit_nativePreloadGraphicsDriver
    ZygoteInit.nativeZygoteInit();
    return RuntimeInit.applicationInit(targetSdkVersion, argv, classLoader);
    }

    RuntimeInit.applicationInit

    protected static Runnable applicationInit(int targetSdkVersion, String[] argv,
    ClassLoader classLoader) {
    //true代表应用程序退出时不调用AppRuntime.onExit(),否则会在退出前调用
    nativeSetExitWithoutCleanup(true);

    //设置虚拟机的内存利用率参数值为0.75
    VMRuntime.getRuntime().setTargetHeapUtilization(0.75f);
    VMRuntime.getRuntime().setTargetSdkVersion(targetSdkVersion);

    final Arguments args = new Arguments(argv);

    // Remaining arguments are passed to the start class's static main
    return findStaticMain(args.startClass, args.startArgs, classLoader);
    }

    继续看findStaticMain:

     protected static Runnable findStaticMain(String className, String[] argv,
    ClassLoader classLoader) {
    Class<?> cl;

    }

    这里通过反射获得了 com.android.server.SystemServer 类中的main方法,并传递给MethodAndArgsCaller用于构造一个Runnable。只要执行此Runnable,就会开始调用com.android.server.SystemServer 类中的main方法。

    到此,Zygote的逻辑已经全部执行完毕,android启动进入了SystemServer的阶段。

    最后,我们再用一个流程图来总结一下Zygote的业务逻辑:

    app_mainAppRuntimeZygoteInit进入Zygote模式创建及启动Dalvik注册Jni方法预加载进程的资源和类Zygote创建socket Server端fork SystemServer子进程载入SystemServer逻辑进入无限循环,处理请求app_mainAppRuntimeZygoteInit

    收起阅读 »

    iOS 图层时间 一

    图层时间时间和空间最大的区别在于,时间不能被复用 -- 弗斯特梅里克在上面两章中,我们探讨了可以用CAAnimation和它的子类实现的多种图层动画。动画的发生是需要持续一段时间的,所以计时对整个概念来说至关重要。在这一章中,我们来看看CAMedia...
    继续阅读 »

    图层时间

    时间和空间最大的区别在于,时间不能被复用 -- 弗斯特梅里克

    在上面两章中,我们探讨了可以用CAAnimation和它的子类实现的多种图层动画。动画的发生是需要持续一段时间的,所以计时对整个概念来说至关重要。在这一章中,我们来看看CAMediaTiming,看看Core Animation是如何跟踪时间的。

    9.1 CAMediaTiming协议

    CAMediaTiming协议定义了在一段动画内用来控制逝去时间的属性的集合,CALayerCAAnimation都实现了这个协议,所以时间可以被任意基于一个图层或者一段动画的类控制。

    持续和重复

    我们在第八章“显式动画”中简单提到过durationCAMediaTiming的属性之一),duration是一个CFTimeInterval的类型(类似于NSTimeInterval的一种双精度浮点类型),对将要进行的动画的一次迭代指定了时间。

    这里的一次迭代是什么意思呢?CAMediaTiming另外还有一个属性叫做repeatCount,代表动画重复的迭代次数。如果duration是2,repeatCount设为3.5(三个半迭代),那么完整的动画时长将是7秒。

    durationrepeatCount默认都是0。但这不意味着动画时长为0秒,或者0次,这里的0仅仅代表了“默认”,也就是0.25秒和1次,你可以用一个简单的测试来尝试为这两个属性赋多个值,如清单9.1,图9.1展示了程序的结果。

    清单9.1 测试durationrepeatCount

    @interface ViewController ()

    @property (nonatomic, weak) IBOutlet UIView *containerView;
    @property (nonatomic, weak) IBOutlet UITextField *durationField;
    @property (nonatomic, weak) IBOutlet UITextField *repeatField;
    @property (nonatomic, weak) IBOutlet UIButton *startButton;
    @property (nonatomic, strong) CALayer *shipLayer;

    @end

    @implementation ViewController

    - (void)viewDidLoad
    {
    [super viewDidLoad];
    //add the ship
    self.shipLayer = [CALayer layer];
    self.shipLayer.frame = CGRectMake(0, 0, 128, 128);
    self.shipLayer.position = CGPointMake(150, 150);
    self.shipLayer.contents = (__bridge id)[UIImage imageNamed: @"Ship.png"].CGImage;
    [self.containerView.layer addSublayer:self.shipLayer];
    }

    - (void)setControlsEnabled:(BOOL)enabled
    {
    for (UIControl *control in @[self.durationField, self.repeatField, self.startButton]) {
    control.enabled = enabled;
    control.alpha = enabled? 1.0f: 0.25f;
    }
    }

    - (IBAction)hideKeyboard
    {
    [self.durationField resignFirstResponder];
    [self.repeatField resignFirstResponder];
    }

    - (IBAction)start
    {
    CFTimeInterval duration = [self.durationField.text doubleValue];
    float repeatCount = [self.repeatField.text floatValue];
    //animate the ship rotation
    CABasicAnimation *animation = [CABasicAnimation animation];
    animation.keyPath = @"transform.rotation";
    animation.duration = duration;
    animation.repeatCount = repeatCount;
    animation.byValue = @(M_PI * 2);
    animation.delegate = self;
    [self.shipLayer addAnimation:animation forKey:@"rotateAnimation"];
    //disable controls
    [self setControlsEnabled:NO];
    }

    - (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag
    {
    //reenable controls
    [self setControlsEnabled:YES];
    }

    @end

    图9.1

    图9.1 演示durationrepeatCount的测试程序

    创建重复动画的另一种方式是使用repeatDuration属性,它让动画重复一个指定的时间,而不是指定次数。你甚至设置一个叫做autoreverses的属性(BOOL类型)在每次间隔交替循环过程中自动回放。这对于播放一段连续非循环的动画很有用,例如打开一扇门,然后关上它(图9.2)。

    图9.2

    图9.2 摆动门的动画

    对门进行摆动的代码见清单9.2。我们用了autoreverses来使门在打开后自动关闭,在这里我们把repeatDuration设置为INFINITY,于是动画无限循环播放,设置repeatCountINFINITY也有同样的效果。注意repeatCountrepeatDuration可能会相互冲突,所以你只要对其中一个指定非零值。对两个属性都设置非0值的行为没有被定义。

    清单9.2 使用autoreverses属性实现门的摇摆

    @interface ViewController ()

    @property (nonatomic, weak) UIView *containerView;

    @end

    @implementation ViewController

    - (void)viewDidLoad
    {
    [super viewDidLoad];
    //add the door
    CALayer *doorLayer = [CALayer layer];
    doorLayer.frame = CGRectMake(0, 0, 128, 256);
    doorLayer.position = CGPointMake(150 - 64, 150);
    doorLayer.anchorPoint = CGPointMake(0, 0.5);
    doorLayer.contents = (__bridge id)[UIImage imageNamed: @"Door.png"].CGImage;
    [self.containerView.layer addSublayer:doorLayer];
    //apply perspective transform
    CATransform3D perspective = CATransform3DIdentity;
    perspective.m34 = -1.0 / 500.0;
    self.containerView.layer.sublayerTransform = perspective;
    //apply swinging animation
    CABasicAnimation *animation = [CABasicAnimation animation];
    animation.keyPath = @"transform.rotation.y";
    animation.toValue = @(-M_PI_2);
    animation.duration = 2.0;
    animation.repeatDuration = INFINITY;
    animation.autoreverses = YES;
    [doorLayer addAnimation:animation forKey:nil];
    }

    @end

    相对时间

    每次讨论到Core Animation,时间都是相对的,每个动画都有它自己描述的时间,可以独立地加速,延时或者偏移。

    beginTime指定了动画开始之前的的延迟时间。这里的延迟从动画添加到可见图层的那一刻开始测量,默认是0(就是说动画会立刻执行)。

    speed是一个时间的倍数,默认1.0,减少它会减慢图层/动画的时间,增加它会加快速度。如果2.0的速度,那么对于一个duration为1的动画,实际上在0.5秒的时候就已经完成了。

    timeOffsetbeginTime类似,但是和增加beginTime导致的延迟动画不同,增加timeOffset只是让动画快进到某一点,例如,对于一个持续1秒的动画来说,设置timeOffset为0.5意味着动画将从一半的地方开始。

    beginTime不同的是,timeOffset并不受speed的影响。所以如果你把speed设为2.0,把timeOffset设置为0.5,那么你的动画将从动画最后结束的地方开始,因为1秒的动画实际上被缩短到了0.5秒。然而即使使用了timeOffset让动画从结束的地方开始,它仍然播放了一个完整的时长,这个动画仅仅是循环了一圈,然后从头开始播放。

    可以用清单9.3的测试程序验证一下,设置speedtimeOffset滑块到随意的值,然后点击播放来观察效果(见图9.3)

    清单9.3 测试timeOffsetspeed属性

    @interface ViewController ()

    @property (nonatomic, weak) IBOutlet UIView *containerView;
    @property (nonatomic, weak) IBOutlet UILabel *speedLabel;
    @property (nonatomic, weak) IBOutlet UILabel *timeOffsetLabel;
    @property (nonatomic, weak) IBOutlet UISlider *speedSlider;
    @property (nonatomic, weak) IBOutlet UISlider *timeOffsetSlider;
    @property (nonatomic, strong) UIBezierPath *bezierPath;
    @property (nonatomic, strong) CALayer *shipLayer;

    @end

    @implementation ViewController

    - (void)viewDidLoad
    {
    [super viewDidLoad];
    //create a path
    self.bezierPath = [[UIBezierPath alloc] init];
    [self.bezierPath moveToPoint:CGPointMake(0, 150)];
    [self.bezierPath addCurveToPoint:CGPointMake(300, 150) controlPoint1:CGPointMake(75, 0) controlPoint2:CGPointMake(225, 300)];
    //draw the path using a CAShapeLayer
    CAShapeLayer *pathLayer = [CAShapeLayer layer];
    pathLayer.path = self.bezierPath.CGPath;
    pathLayer.fillColor = [UIColor clearColor].CGColor;
    pathLayer.strokeColor = [UIColor redColor].CGColor;
    pathLayer.lineWidth = 3.0f;
    [self.containerView.layer addSublayer:pathLayer];
    //add the ship
    self.shipLayer = [CALayer layer];
    self.shipLayer.frame = CGRectMake(0, 0, 64, 64);
    self.shipLayer.position = CGPointMake(0, 150);
    self.shipLayer.contents = (__bridge id)[UIImage imageNamed: @"Ship.png"].CGImage;
    [self.containerView.layer addSublayer:self.shipLayer];
    //set initial values
    [self updateSliders];
    }

    - (IBAction)updateSliders
    {
    CFTimeInterval timeOffset = self.timeOffsetSlider.value;
    self.timeOffsetLabel.text = [NSString stringWithFormat:@"%0.2f", timeOffset];
    float speed = self.speedSlider.value;
    self.speedLabel.text = [NSString stringWithFormat:@"%0.2f", speed];
    }

    - (IBAction)play
    {
    //create the keyframe animation
    CAKeyframeAnimation *animation = [CAKeyframeAnimation animation];
    animation.keyPath = @"position";
    animation.timeOffset = self.timeOffsetSlider.value;
    animation.speed = self.speedSlider.value;
    animation.duration = 1.0;
    animation.path = self.bezierPath.CGPath;
    animation.rotationMode = kCAAnimationRotateAuto;
    animation.removedOnCompletion = NO;
    [self.shipLayer addAnimation:animation forKey:@"slide"];
    }

    @end

    图9.3

    图9.3 测试时间偏移和速度的简单的应用程序

    fillMode

    对于beginTime非0的一段动画来说,会出现一个当动画添加到图层上但什么也没发生的状态。类似的,removeOnCompletion被设置为NO的动画将会在动画结束的时候仍然保持之前的状态。这就产生了一个问题,当动画开始之前和动画结束之后,被设置动画的属性将会是什么值呢?

    一种可能是属性和动画没被添加之前保持一致,也就是在模型图层定义的值(见第七章“隐式动画”,模型图层和呈现图层的解释)。

    另一种可能是保持动画开始之前那一帧,或者动画结束之后的那一帧。这就是所谓的填充,因为动画开始和结束的值用来填充开始之前和结束之后的时间。

    这种行为就交给开发者了,它可以被CAMediaTimingfillMode来控制。fillMode是一个NSString类型,可以接受如下四种常量:

    kCAFillModeForwards 
    kCAFillModeBackwards
    kCAFillModeBoth
    kCAFillModeRemoved

    默认是kCAFillModeRemoved,当动画不再播放的时候就显示图层模型指定的值剩下的三种类型向前,向后或者即向前又向后去填充动画状态,使得动画在开始前或者结束后仍然保持开始和结束那一刻的值。

    这就对避免在动画结束的时候急速返回提供另一种方案(见第八章)。但是记住了,当用它来解决这个问题的时候,需要把removeOnCompletion设置为NO,另外需要给动画添加一个非空的键,于是可以在不需要动画的时候把它从图层上移除。

    收起阅读 »

    iOS 显示动画 二

    8.3 过渡有时候对于iOS应用程序来说,希望能通过属性动画来对比较难做动画的布局进行一些改变。比如交换一段文本和图片,或者用一段网格视图来替换,等等。属性动画只对图层的可动画属性起作用,所以如果要改变一个不能动画的属性(比如图片),或者从层级关系中添加或者移...
    继续阅读 »

    8.3 过渡

    有时候对于iOS应用程序来说,希望能通过属性动画来对比较难做动画的布局进行一些改变。比如交换一段文本和图片,或者用一段网格视图来替换,等等。属性动画只对图层的可动画属性起作用,所以如果要改变一个不能动画的属性(比如图片),或者从层级关系中添加或者移除图层,属性动画将不起作用。

    于是就有了过渡的概念。过渡并不像属性动画那样平滑地在两个值之间做动画,而是影响到整个图层的变化。过渡动画首先展示之前的图层外观,然后通过一个交换过渡到新的外观。

    为了创建一个过渡动画,我们将使用CATransition,同样是另一个CAAnimation的子类,和别的子类不同,CATransition有一个typesubtype来标识变换效果。type属性是一个NSString类型,可以被设置成如下类型:

    kCATransitionFade 
    kCATransitionMoveIn
    kCATransitionPush
    kCATransitionReveal

    到目前为止你只能使用上述四种类型,但你可以通过一些别的方法来自定义过渡效果,后续会详细介绍。

    默认的过渡类型是kCATransitionFade,当你在改变图层属性之后,就创建了一个平滑的淡入淡出效果。

    我们在第七章的例子中就已经用到过kCATransitionPush,它创建了一个新的图层,从边缘的一侧滑动进来,把旧图层从另一侧推出去的效果。

    kCATransitionMoveInkCATransitionRevealkCATransitionPush类似,都实现了一个定向滑动的动画,但是有一些细微的不同,kCATransitionMoveIn从顶部滑动进入,但不像推送动画那样把老土层推走,然而kCATransitionReveal把原始的图层滑动出去来显示新的外观,而不是把新的图层滑动进入。

    后面三种过渡类型都有一个默认的动画方向,它们都从左侧滑入,但是你可以通过subtype来控制它们的方向,提供了如下四种类型:

    kCATransitionFromRight 
    kCATransitionFromLeft
    kCATransitionFromTop
    kCATransitionFromBottom

    一个简单的用CATransition来对非动画属性做动画的例子如清单8.11所示,这里我们对UIImageimage属性做修改,但是隐式动画或者CAPropertyAnimation都不能对它做动画,因为Core Animation不知道如何在插图图片。通过对图层应用一个淡入淡出的过渡,我们可以忽略它的内容来做平滑动画(图8.4),我们来尝试修改过渡的type常量来观察其它效果。

    清单8.11 使用CATransition来对UIImageView做动画

    @interface ViewController ()

    @property (nonatomic, weak) IBOutlet UIImageView *imageView;
    @property (nonatomic, copy) NSArray *images;

    @end

    @implementation ViewController

    - (void)viewDidLoad
    {
    [super viewDidLoad];
    //set up images
    self.images = @[[UIImage imageNamed:@"Anchor.png"],
    [UIImage imageNamed:@"Cone.png"],
    [UIImage imageNamed:@"Igloo.png"],
    [UIImage imageNamed:@"Spaceship.png"]];
    }


    - (IBAction)switchImage
    {
    //set up crossfade transition
    CATransition *transition = [CATransition animation];
    transition.type = kCATransitionFade;
    //apply transition to imageview backing layer
    [self.imageView.layer addAnimation:transition forKey:nil];
    //cycle to next image
    UIImage *currentImage = self.imageView.image;
    NSUInteger index = [self.images indexOfObject:currentImage];
    index = (index + 1) % [self.images count];
    self.imageView.image = self.images[index];
    }

    @end

    你可以从代码中看出,过渡动画和之前的属性动画或者动画组添加到图层上的方式一致,都是通过-addAnimation:forKey:方法。但是和属性动画不同的是,对指定的图层一次只能使用一次CATransition,因此,无论你对动画的键设置什么值,过渡动画都会对它的键设置成“transition”,也就是常量kCATransition

    图8.4

    图8.4 使用CATransition对图像平滑淡入淡出

    隐式过渡

    CATransision可以对图层任何变化平滑过渡的事实使得它成为那些不好做动画的属性图层行为的理想候选。苹果当然意识到了这点,并且当设置了CALayercontent属性的时候,CATransition的确是默认的行为。但是对于视图关联的图层,或者是其他隐式动画的行为,这个特性依然是被禁用的,但是对于你自己创建的图层,这意味着对图层contents图片做的改动都会自动附上淡入淡出的动画。

    我们在第七章使用CATransition作为一个图层行为来改变图层的背景色,当然backgroundColor属性可以通过正常的CAPropertyAnimation来实现,但这不是说不可以用CATransition来实行。

    对图层树的动画

    CATransition并不作用于指定的图层属性,这就是说你可以在即使不能准确得知改变了什么的情况下对图层做动画,例如,在不知道UITableView哪一行被添加或者删除的情况下,直接就可以平滑地刷新它,或者在不知道UIViewController内部的视图层级的情况下对两个不同的实例做过渡动画。

    这些例子和我们之前所讨论的情况完全不同,因为它们不仅涉及到图层的属性,而且是整个图层树的改变--我们在这种动画的过程中手动在层级关系中添加或者移除图层。

    这里用到了一个小诡计,要确保CATransition添加到的图层在过渡动画发生时不会在树状结构中被移除,否则CATransition将会和图层一起被移除。一般来说,你只需要将动画添加到被影响图层的superlayer

    在清单8.2中,我们展示了如何在UITabBarController切换标签的时候添加淡入淡出的动画。这里我们建立了默认的标签应用程序模板,然后用UITabBarControllerDelegate-tabBarController:didSelectViewController:方法来应用过渡动画。我们把动画添加到UITabBarController的视图图层上,于是在标签被替换的时候动画不会被移除。

    清单8.12 对UITabBarController做动画

    #import "AppDelegate.h"
    #import "FirstViewController.h"
    #import "SecondViewController.h"
    #import
    @implementation AppDelegate
    - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
    {
    self.window = [[UIWindow alloc] initWithFrame: [[UIScreen mainScreen] bounds]];
    UIViewController *viewController1 = [[FirstViewController alloc] init];
    UIViewController *viewController2 = [[SecondViewController alloc] init];
    self.tabBarController = [[UITabBarController alloc] init];
    self.tabBarController.viewControllers = @[viewController1, viewController2];
    self.tabBarController.delegate = self;
    self.window.rootViewController = self.tabBarController;
    [self.window makeKeyAndVisible];
    return YES;
    }
    - (void)tabBarController:(UITabBarController *)tabBarController didSelectViewController:(UIViewController *)viewController
    {
    //set up crossfade transition
    CATransition *transition = [CATransition animation];
    transition.type = kCATransitionFade;
    //apply transition to tab bar controller's view
    [self.tabBarController.view.layer addAnimation:transition forKey:nil];
    }
    @end

    自定义动画

    我们证实了过渡是一种对那些不太好做平滑动画属性的强大工具,但是CATransition的提供的动画类型太少了。

    更奇怪的是苹果通过UIView +transitionFromView:toView:duration:options:completion:+transitionWithView:duration:options:animations:方法提供了Core Animation的过渡特性。但是这里的可用的过渡选项和CATransitiontype属性提供的常量完全不同UIView过渡方法中options参数可以由如下常量指定:

    UIViewAnimationOptionTransitionFlipFromLeft 

    UIViewAnimationOptionTransitionFlipFromRight UIViewAnimationOptionTransitionCurlUp UIViewAnimationOptionTransitionCurlDown UIViewAnimationOptionTransitionCrossDissolve UIViewAnimationOptionTransitionFlipFromTop UIViewAnimationOptionTransitionFlipFromBottom

    除了UIViewAnimationOptionTransitionCrossDissolve之外,剩下的值和CATransition类型完全没关系。你可以用之前例子修改过的版本来测试一下(见清单8.13)。

    清单8.13 使用UIKit提供的方法来做过渡动画

    @interface ViewController ()
    @property (nonatomic, weak) IBOutlet UIImageView *imageView;
    @property (nonatomic, copy) NSArray *images;
    @end
    @implementation ViewController
    - (void)viewDidLoad
    {
    [super viewDidLoad]; //set up images
    self.images = @[[UIImage imageNamed:@"Anchor.png"],
    [UIImage imageNamed:@"Cone.png"],
    [UIImage imageNamed:@"Igloo.png"],
    [UIImage imageNamed:@"Spaceship.png"]];
    - (IBAction)switchImage
    {
    [UIView transitionWithView:self.imageView duration:1.0
    options:UIViewAnimationOptionTransitionFlipFromLeft
    animations:^{
    //cycle to next image
    UIImage *currentImage = self.imageView.image;
    NSUInteger index = [self.images indexOfObject:currentImage];
    index = (index + 1) % [self.images count];
    self.imageView.image = self.images[index];
    }
    completion:NULL];
    }

    @end

    文档暗示过在iOS5(带来了Core Image框架)之后,可以通过CATransitionfilter属性,用CIFilter来创建其它的过渡效果。然是直到iOS6都做不到这点。试图对CATransition使用Core Image的滤镜完全没效果(但是在Mac OS中是可行的,也许文档是想表达这个意思)。

    因此,根据要实现的效果,你只用关心是用CATransition还是用UIView的过渡方法就可以了。希望下个版本的iOS系统可以通过CATransition很好的支持Core Image的过渡滤镜效果(或许甚至会有新的方法)。

    但这并不意味着在iOS上就不能实现自定义的过渡效果了。这只是意味着你需要做一些额外的工作。就像之前提到的那样,过渡动画做基础的原则就是对原始的图层外观截图,然后添加一段动画,平滑过渡到图层改变之后那个截图的效果。如果我们知道如何对图层截图,我们就可以使用属性动画来代替CATransition或者是UIKit的过渡方法来实现动画。

    事实证明,对图层做截图还是很简单的。CALayer有一个-renderInContext:方法,可以通过把它绘制到Core Graphics的上下文中捕获当前内容的图片,然后在另外的视图中显示出来。如果我们把这个截屏视图置于原始视图之上,就可以遮住真实视图的所有变化,于是重新创建了一个简单的过渡效果。

    清单8.14演示了一个基本的实现。我们对当前视图状态截图,然后在我们改变原始视图的背景色的时候对截图快速转动并且淡出,图8.5展示了我们自定义的过渡效果。

    为了让事情更简单,我们用UIView -animateWithDuration:completion:方法来实现。虽然用CABasicAnimation可以达到同样的效果,但是那样的话我们就需要对图层的变换和不透明属性创建单独的动画,然后当动画结束的是哦户在CAAnimationDelegate中把coverView从屏幕中移除。

    清单8.14 用renderInContext:创建自定义过渡效果

    @implementation ViewController
    - (IBAction)performTransition
    {
    //preserve the current view snapshot
    UIGraphicsBeginImageContextWithOptions(self.view.bounds.size, YES, 0.0);
    [self.view.layer renderInContext:UIGraphicsGetCurrentContext()];
    UIImage *coverImage = UIGraphicsGetImageFromCurrentImageContext();
    //insert snapshot view in front of this one
    UIView *coverView = [[UIImageView alloc] initWithImage:coverImage];
    coverView.frame = self.view.bounds;
    [self.view addSubview:coverView];
    //update the view (we'll simply randomize the layer background color)
    CGFloat red = arc4random() / (CGFloat)INT_MAX;
    CGFloat green = arc4random() / (CGFloat)INT_MAX;
    CGFloat blue = arc4random() / (CGFloat)INT_MAX;
    self.view.backgroundColor = [UIColor colorWithRed:red green:green blue:blue alpha:1.0];
    //perform animation (anything you like)
    [UIView animateWithDuration:1.0 animations:^{
    //scale, rotate and fade the view
    CGAffineTransform transform = CGAffineTransformMakeScale(0.01, 0.01);
    transform = CGAffineTransformRotate(transform, M_PI_2);
    coverView.transform = transform;
    coverView.alpha = 0.0;
    } completion:^(BOOL finished) {
    //remove the cover view now we're finished with it
    [coverView removeFromSuperview];
    }];
    }
    @end

    图8.5

    图8.5 使用renderInContext:创建自定义过渡效果

    这里有个警告:-renderInContext:捕获了图层的图片和子图层,但是不能对子图层正确地处理变换效果,而且对视频和OpenGL内容也不起作用。但是用CATransition,或者用私有的截屏方式就没有这个限制了。


    8.4 在动画过程中取消动画

    之前提到过,你可以用-addAnimation:forKey:方法中的key参数来在添加动画之后检索一个动画,使用如下方法:

    - (CAAnimation *)animationForKey:(NSString *)key;

    但并不支持在动画运行过程中修改动画,所以这个方法主要用来检测动画的属性,或者判断它是否被添加到当前图层中。

    为了终止一个指定的动画,你可以用如下方法把它从图层移除掉:

    - (void)removeAnimationForKey:(NSString *)key;

    或者移除所有动画:

    - (void)removeAllAnimations;

    动画一旦被移除,图层的外观就立刻更新到当前的模型图层的值。一般说来,动画在结束之后被自动移除,除非设置removedOnCompletionNO,如果你设置动画在结束之后不被自动移除,那么当它不需要的时候你要手动移除它;否则它会一直存在于内存中,直到图层被销毁。

    我们来扩展之前旋转飞船的示例,这里添加一个按钮来停止或者启动动画。这一次我们用一个非nil的值作为动画的键,以便之后可以移除它。-animationDidStop:finished:方法中的flag参数表明了动画是自然结束还是被打断,我们可以在控制台打印出来。如果你用停止按钮来终止动画,它会打印NO,如果允许它完成,它会打印YES

    清单8.15是更新后的示例代码,图8.6显示了结果。

    清单8.15 开始和停止一个动画

    @interface ViewController ()

    @property (nonatomic, weak) IBOutlet UIView *containerView;
    @property (nonatomic, strong) CALayer *shipLayer;

    @end

    @implementation ViewController

    - (void)viewDidLoad
    {
    [super viewDidLoad];
    //add the ship
    self.shipLayer = [CALayer layer];
    self.shipLayer.frame = CGRectMake(0, 0, 128, 128);
    self.shipLayer.position = CGPointMake(150, 150);
    self.shipLayer.contents = (__bridge id)[UIImage imageNamed: @"Ship.png"].CGImage;
    [self.containerView.layer addSublayer:self.shipLayer];
    }

    - (IBAction)start
    {
    //animate the ship rotation
    CABasicAnimation *animation = [CABasicAnimation animation];
    animation.keyPath = @"transform.rotation";
    animation.duration = 2.0;
    animation.byValue = @(M_PI * 2);
    animation.delegate = self;
    [self.shipLayer addAnimation:animation forKey:@"rotateAnimation"];
    }

    - (IBAction)stop
    {
    [self.shipLayer removeAnimationForKey:@"rotateAnimation"];
    }

    - (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag
    {
    //log that the animation stopped
    NSLog(@"The animation stopped (finished: %@)", flag? @"YES": @"NO");
    }

    @end

    图8.6

    图8.6 通过开始和停止按钮控制的旋转动画

    总结

    这一章中,我们涉及了属性动画(你可以对单独的图层属性动画有更加具体的控制),动画组(把多个属性动画组合成一个独立单元)以及过度(影响整个图层,可以用来对图层的任何内容做任何类型的动画,包括子图层的添加和移除)。

    在第九章中,我们继续学习CAMediaTiming协议,来看一看Core Animation是怎样处理逝去的时间。

    收起阅读 »

    call, call.call, call.call.call, 你也许还不懂这疯狂的call

    Function.prototype.call 我想大家都觉得自己很熟悉了,手写也没问题!! 你确认这个问题之前, 首先看看 三千文字,也没写好 Function.prototype.call, 看完,你感觉还OK,那么再看一道题: 请问如下的输出结果 fun...
    继续阅读 »

    Function.prototype.call 我想大家都觉得自己很熟悉了,手写也没问题!!

    你确认这个问题之前, 首先看看 三千文字,也没写好 Function.prototype.call,


    看完,你感觉还OK,那么再看一道题:

    请问如下的输出结果


    function a(){ 
    console.log(this,'a')
    };
    function b(){
    console.log(this,'b')
    }
    a.call.call(b,'b')

    如果,你也清晰的知道,结果,对不起,大佬, 打扰了,我错了!


    本文起源:

    一个掘友加我微信,私聊问我这个问题,研究后,又请教了 阿宝哥

    觉得甚有意思,遂与大家分享!


    结果


    结果如下: 惊喜还是意外,还是淡定呢?


    String {"b"} "b"

    再看看如下的代码:2个,3个,4个,更多个的call,输出都会是String {"b"} "b"


    function a(){ 
    console.log(this,'a')
    };
    function b(){
    console.log(this,'b')
    }
    a.call.call(b,'b') // String {"b"} "b"
    a.call.call.call(b,'b') // String {"b"} "b"
    a.call.call.call.call(b,'b') // String {"b"} "b"

    看完上面,应该有三个疑问?



    1. 为什么被调用的是b函数

    2. 为什么thisString {"b"}

    3. 为什么 2, 3, 4个call的结果一样


    结论:

    两个以上的call,比如call.call(b, 'b'),你就简单理解为用 b.call('b')


    分析


    为什么 2, 3, 4个call的结果一样


    a.call(b) 最终被调用的是a,

    a.call.call(b), 最终被调用的 a.call

    a.call.call.call(b), 最终被执行的 a.call.call


    看一下引用关系


    a.call === Function.protype.call  // true
    a.call === a.call.call // true
    a.call === a.call.call.call // true

    基于上述执行分析:

    a.call 被调用的是a

    a.call.calla.call.call.call 本质没啥区别, 被调用的都是Function.prototype.call


    为什么 2, 3, 4个call的结果一样,到此已经真相


    为什么被调用的是b函数


    看本质就要返璞归真,ES 标准对 Funtion.prototye.call 的描述



    Function.prototype.call (thisArg , ...args)


    When the call method is called on an object func with argument, thisArg and zero or more args, the following steps are taken:



    1. If IsCallable(func) is false, throw a TypeError exception.

    2. Let argList be an empty List.

    3. If this method was called with more than one argument then in left to right order, starting with the second argument, append each argument as the last element of argList.

    4. Perform PrepareForTailCall().

    5. Return Call(functhisArgargList).



    中文翻译一下



    1. 如果不可调用,抛出异常

    2. 准备一个argList空数组变量

    3. 把第一个之后的变量按照顺序添加到argList

    4. 返回 Call(functhisArgargList)的结果


    这里的Call只不是是一个抽象的定义, 实际上是调用函数内部 [[Call]] 的方法, 其也没有暴露更多的有用的信息。


    实际上在这里,我已经停止了思考:


    a is a function, then what a.call.call really do? 一文的解释,有提到 Bound Function Exotic Objects , MDN的 Function.prototype.bind 也有提到:



    The bind() function creates a new bound function, which is an exotic function object (a term from ECMAScript 2015) that wraps the original function object. Calling the bound function generally results in the execution of its wrapped function.



    Function.prototype.call 相反,并没有提及!!! 但不排查在调用过程中有生成。


    Difference between Function.call, Function.prototype.call, Function.prototype.call.call and Function.prototype.call.call.call 一文的解释,我觉得是比较合理的


    function my(p) { console.log(p) }
    Function.prototype.call.call(my, this, "Hello"); // output 'Hello'


    Function.prototype.call.call(my, this, "Hello"); means:


    Use my as this argument (the function context) for the function that was called. In this case Function.prototype.call was called.


    So, Function.prototype.call would be called with my as its context. Which basically means - it would be the function to be invoked.


    It would be called with the following arguments: (this, "Hello"), where this is the context to be set inside the function to be called (in this case it's my), and the only argument to be passed is "Hello" string.



    重点标出:

    So, Function.prototype.call would be called with my as its context. Which basically means - it would be the function to be invoked.


    It would be called with the following arguments: (this, "Hello"), where this is the context to be set inside the function to be called (in this case it's my), and the only argument to be passed is "Hello" string


    翻译一下:

    Function.prototype.call.call(my, this, "Hello")表示: 用my作为上下文调用Function.prototype.call,也就是说my是最终被调用的函数。


    my带着这些 (this, "Hello") 被调用, this 作为被调用函数的上下文,此处是作为my函数的上下文, 唯一被传递的参数是 "hello"字符串。


    基于这个理解, 我们简单验证一下, 确实是这样的表象


    // case 1:
    function my(p) { console.log(p) }
    Function.prototype.call.call(my, this, "Hello"); // output 'Hello'

    // case 2:
    function a(){
    console.log(this,'a')
    };
    function b(){
    console.log(this,'b')
    }
    a.call.call(b,'b') // String {"b"} "b"

    为什么被调用的是b函数, 到此也真相了。


    其实我依旧不能太释怀, 但是这个解释可以接受,表象也是正确的, 期望掘友们有更合理,更详细的解答。


    为什么thisString {"b"}


    在上一节的分析中,我故意遗漏了Function.prototype.call的两个note



    NOTE 1: The thisArg value is passed without modification as the this value. This is a change from Edition 3, where an undefined or null thisArg is replaced with the global object and ToObject is applied to all other values and that result is passed as the this value. Even though the thisArg is passed without modification, non-strict functions still perform these transformations upon entry to the function.




    NOTE 2: If func is an arrow function or a bound function then the thisArg will be ignored by the function [[Call]] in step 5.



    注意这一句:



    This is a change from Edition 3, where an undefined or null thisArg is replaced with the global object and ToObject is applied to all other values and that result is passed as the this value



    两点:



    1. 如果thisArgundefined 或者null, 会用global object替换


    这里的前提是 非严格模式


    "use strict"

    function a(m){
    console.log(this, m); // undefined, 1
    }

    a.call(undefined, 1)


    1. 其他的所有类型,都会调用 ToObject进行转换


    所以非严格模式下, this肯定是个对象, 看下面的代码:


    Object('b') // String {"b"}

    note2的 ToObject 就是答案


    到此, 为什么thisSting(b) 这个也真相了


    万能的函数调用方法


    基于Function.prototype.call.call的特性,我们可以封装一个万能函数调用方法


    var call = Function.prototype.call.call.bind(Function.prototype.call);

    示例


    var person = {
    hello() {
    console.log('hello', this.name)
    }
    }

    call(person.hello, {"name": "tom"}) // hello tom

    写在最后


    如果你觉得不错,你的一赞一评就是我前行的最大动力。




    作者:云的世界
    链接:https://juejin.cn/post/6999781802923524132

    收起阅读 »

    iOS 显式动画 一

    显式动画如果想让事情变得顺利,只有靠自己 -- 夏尔·纪尧姆上一章介绍了隐式动画的概念。隐式动画是在iOS平台创建动态用户界面的一种直接方式,也是UIKit动画机制的基础,不过它并不能涵盖所有的动画类型。在这一章中,我们将要研究一下显式动画,它能够对一些属性做...
    继续阅读 »

    显式动画

    如果想让事情变得顺利,只有靠自己 -- 夏尔·纪尧姆

    上一章介绍了隐式动画的概念。隐式动画是在iOS平台创建动态用户界面的一种直接方式,也是UIKit动画机制的基础,不过它并不能涵盖所有的动画类型。在这一章中,我们将要研究一下显式动画,它能够对一些属性做指定的自定义动画,或者创建非线性动画,比如沿着任意一条曲线移动。


    8.1 属性动画

    CAAnimationDelegate在任何头文件中都找不到,但是可以在CAAnimation头文件或者苹果开发者文档中找到相关函数。在这个例子中,我们用-animationDidStop:finished:方法在动画结束之后来更新图层的backgroundColor

    当更新属性的时候,我们需要设置一个新的事务,并且禁用图层行为。否则动画会发生两次,一个是因为显式的CABasicAnimation,另一次是因为隐式动画,具体实现见订单8.3。

    清单8.3 动画完成之后修改图层的背景色

    @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 new random color
    CGFloat red = arc4random() / (CGFloat)INT_MAX;
    CGFloat green = arc4random() / (CGFloat)INT_MAX;
    CGFloat blue = arc4random() / (CGFloat)INT_MAX;
    UIColor *color = [UIColor colorWithRed:red green:green blue:blue alpha:1.0];
    //create a basic animation
    CABasicAnimation *animation = [CABasicAnimation animation];
    animation.keyPath = @"backgroundColor";
    animation.toValue = (__bridge id)color.CGColor;
    animation.delegate = self;
    //apply animation to layer
    [self.colorLayer addAnimation:animation forKey:nil];
    }

    - (void)animationDidStop:(CABasicAnimation *)anim finished:(BOOL)flag
    {
    //set the backgroundColor property to match animation toValue
    [CATransaction begin];
    [CATransaction setDisableActions:YES];
    self.colorLayer.backgroundColor = (__bridge CGColorRef)anim.toValue;
    [CATransaction commit];
    }

    @end

    CAAnimation而言,使用委托模式而不是一个完成块会带来一个问题,就是当你有多个动画的时候,无法在在回调方法中区分。在一个视图控制器中创建动画的时候,通常会用控制器本身作为一个委托(如清单8.3所示),但是所有的动画都会调用同一个回调方法,所以你就需要判断到底是那个图层的调用。

    考虑一下第三章的闹钟,“图层几何学”,我们通过简单地每秒更新指针的角度来实现一个钟,但如果指针动态地转向新的位置会更加真实。

    我们不能通过隐式动画来实现因为这些指针都是UIView的实例,所以图层的隐式动画都被禁用了。我们可以简单地通过UIView的动画方法来实现。但如果想更好地控制动画时间,使用显式动画会更好(更多内容见第十章)。使用CABasicAnimation来做动画可能会更加复杂,因为我们需要在-animationDidStop:finished:中检测指针状态(用于设置结束的位置)。

    动画本身会作为一个参数传入委托的方法,也许你会认为可以控制器中把动画存储为一个属性,然后在回调用比较,但实际上并不起作用,因为委托传入的动画参数是原始值的一个深拷贝,从而不是同一个值。

    当使用-addAnimation:forKey:把动画添加到图层,这里有一个到目前为止我们都设置为nilkey参数。这里的键是-animationForKey:方法找到对应动画的唯一标识符,而当前动画的所有键都可以用animationKeys获取。如果我们对每个动画都关联一个唯一的键,就可以对每个图层循环所有键,然后调用-animationForKey:来比对结果。尽管这不是一个优雅的实现。

    幸运的是,还有一种更加简单的方法。像所有的NSObject子类一样,CAAnimation实现了KVC(键-值-编码)协议,于是你可以用-setValue:forKey:-valueForKey:方法来存取属性。但是CAAnimation有一个不同的性能:它更像一个NSDictionary,可以让你随意设置键值对,即使和你使用的动画类所声明的属性并不匹配。

    这意味着你可以对动画用任意类型打标签。在这里,我们给UIView类型的指针添加的动画,所以可以简单地判断动画到底属于哪个视图,然后在委托方法中用这个信息正确地更新钟的指针(清单8.4)。

    清单8.4 使用KVC对动画打标签

    @interface ViewController ()

    @property (nonatomic, weak) IBOutlet UIImageView *hourHand;
    @property (nonatomic, weak) IBOutlet UIImageView *minuteHand;
    @property (nonatomic, weak) IBOutlet UIImageView *secondHand;
    @property (nonatomic, weak) NSTimer *timer;

    @end

    @implementation ViewController

    - (void)viewDidLoad
    {
    [super viewDidLoad];
    //adjust anchor points
    self.secondHand.layer.anchorPoint = CGPointMake(0.5f, 0.9f);
    self.minuteHand.layer.anchorPoint = CGPointMake(0.5f, 0.9f);
    self.hourHand.layer.anchorPoint = CGPointMake(0.5f, 0.9f);
    //start timer
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(tick) userInfo:nil repeats:YES];
    //set initial hand positions
    [self updateHandsAnimated:NO];
    }

    - (void)tick
    {
    [self updateHandsAnimated:YES];
    }

    - (void)updateHandsAnimated:(BOOL)animated
    {
    //convert time to hours, minutes and seconds
    NSCalendar *calendar = [[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar];
    NSUInteger units = NSHourCalendarUnit | NSMinuteCalendarUnit | NSSecondCalendarUnit;
    NSDateComponents *components = [calendar components:units fromDate:[NSDate date]];
    CGFloat hourAngle = (components.hour / 12.0) * M_PI * 2.0;
    //calculate hour hand angle //calculate minute hand angle
    CGFloat minuteAngle = (components.minute / 60.0) * M_PI * 2.0;
    //calculate second hand angle
    CGFloat secondAngle = (components.second / 60.0) * M_PI * 2.0;
    //rotate hands
    [self setAngle:hourAngle forHand:self.hourHand animated:animated];
    [self setAngle:minuteAngle forHand:self.minuteHand animated:animated];
    [self setAngle:secondAngle forHand:self.secondHand animated:animated];
    }

    - (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];
    [self updateHandsAnimated:NO];
    animation.keyPath = @"transform";
    animation.toValue = [NSValue valueWithCATransform3D:transform];
    animation.duration = 0.5;
    animation.delegate = self;
    [animation setValue:handView forKey:@"handView"];
    [handView.layer addAnimation:animation forKey:nil];
    } else {
    //set transform directly
    handView.layer.transform = transform;
    }
    }

    - (void)animationDidStop:(CABasicAnimation *)anim finished:(BOOL)flag
    {
    //set final position for hand view
    UIView *handView = [anim valueForKey:@"handView"];
    handView.layer.transform = [anim.toValue CATransform3DValue];
    }

    我们成功的识别出每个图层停止动画的时间,然后更新它的变换到一个新值,很好。

    不幸的是,即使做了这些,还是有个问题,清单8.4在模拟器上运行的很好,但当真正跑在iOS设备上时,我们发现在-animationDidStop:finished:委托方法调用之前,指针会迅速返回到原始值,这个清单8.3图层颜色发生的情况一样。

    问题在于回调方法在动画完成之前已经被调用了,但不能保证这发生在属性动画返回初始状态之前。这同时也很好地说明了为什么要在真实的设备上测试动画代码,而不仅仅是模拟器。

    我们可以用一个fillMode属性来解决这个问题,下一章会详细说明,这里知道在动画之前设置它比在动画结束之后更新属性更加方便。

    关键帧动画

    CABasicAnimation揭示了大多数隐式动画背后依赖的机制,这的确很有趣,但是显式地给图层添加CABasicAnimation相较于隐式动画而言,只能说费力不讨好。

    CAKeyframeAnimation是另一种UIKit没有暴露出来但功能强大的类。和CABasicAnimation类似,CAKeyframeAnimation同样是CAPropertyAnimation的一个子类,它依然作用于单一的一个属性,但是和CABasicAnimation不一样的是,它不限制于设置一个起始和结束的值,而是可以根据一连串随意的值来做动画。

    关键帧起源于传动动画,意思是指主导的动画在显著改变发生时重绘当前帧(也就是关键帧),每帧之间剩下的绘制(可以通过关键帧推算出)将由熟练的艺术家来完成。CAKeyframeAnimation也是同样的道理:你提供了显著的帧,然后Core Animation在每帧之间进行插入。

    我们可以用之前使用颜色图层的例子来演示,设置一个颜色的数组,然后通过关键帧动画播放出来(清单8.5)

    清单8.5 使用CAKeyframeAnimation应用一系列颜色的变化

    - (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 ];
    //apply animation to layer
    [self.colorLayer addAnimation:animation forKey:nil];
    }

    注意到序列中开始和结束的颜色都是蓝色,这是因为CAKeyframeAnimation并不能自动把当前值作为第一帧(就像CABasicAnimation那样把fromValue设为nil)。动画会在开始的时候突然跳转到第一帧的值,然后在动画结束的时候突然恢复到原始的值。所以为了动画的平滑特性,我们需要开始和结束的关键帧来匹配当前属性的值。

    当然可以创建一个结束和开始值不同的动画,那样的话就需要在动画启动之前手动更新属性和最后一帧的值保持一致,就和之前讨论的一样。

    我们用duration属性把动画时间从默认的0.25秒增加到2秒,以便于动画做的不那么快。运行它,你会发现动画通过颜色不断循环,但效果看起来有些奇怪。原因在于动画以一个恒定的步调在运行。当在每个动画之间过渡的时候并没有减速,这就产生了一个略微奇怪的效果,为了让动画看起来更自然,我们需要调整一下缓冲,第十章将会详细说明。

    提供一个数组的值就可以按照颜色变化做动画,但一般来说用数组来描述动画运动并不直观。CAKeyframeAnimation有另一种方式去指定动画,就是使用CGPathpath属性可以用一种直观的方式,使用Core Graphics函数定义运动序列来绘制动画。

    我们来用一个宇宙飞船沿着一个简单曲线的实例演示一下。为了创建路径,我们需要使用一个三次贝塞尔曲线,它是一种使用开始点,结束点和另外两个控制点来定义形状的曲线,可以通过使用一个基于C的Core Graphics绘图指令来创建,不过用UIKit提供的UIBezierPath类会更简单。

    我们这次用CAShapeLayer来在屏幕上绘制曲线,尽管对动画来说并不是必须的,但这会让我们的动画更加形象。绘制完CGPath之后,我们用它来创建一个CAKeyframeAnimation,然后用它来应用到我们的宇宙飞船。代码见清单8.6,结果见图8.1。

    清单8.6 沿着一个贝塞尔曲线对图层做动画

    @interface ViewController ()

    @property (nonatomic, weak) IBOutlet UIView *containerView;

    @end

    @implementation ViewController

    - (void)viewDidLoad
    {
    [super viewDidLoad];
    //create a path
    UIBezierPath *bezierPath = [[UIBezierPath alloc] init];
    [bezierPath moveToPoint:CGPointMake(0, 150)];
    [bezierPath addCurveToPoint:CGPointMake(300, 150) controlPoint1:CGPointMake(75, 0) controlPoint2:CGPointMake(225, 300)];
    //draw the path using a CAShapeLayer
    CAShapeLayer *pathLayer = [CAShapeLayer layer];
    pathLayer.path = bezierPath.CGPath;
    pathLayer.fillColor = [UIColor clearColor].CGColor;
    pathLayer.strokeColor = [UIColor redColor].CGColor;
    pathLayer.lineWidth = 3.0f;
    [self.containerView.layer addSublayer:pathLayer];
    //add the ship
    CALayer *shipLayer = [CALayer layer];
    shipLayer.frame = CGRectMake(0, 0, 64, 64);
    shipLayer.position = CGPointMake(0, 150);
    shipLayer.contents = (__bridge id)[UIImage imageNamed: @"Ship.png"].CGImage;
    [self.containerView.layer addSublayer:shipLayer];
    //create the keyframe animation
    CAKeyframeAnimation *animation = [CAKeyframeAnimation animation];
    animation.keyPath = @"position";
    animation.duration = 4.0;
    animation.path = bezierPath.CGPath;
    [shipLayer addAnimation:animation forKey:nil];
    }

    @end

    图8.1

    图8.1 沿着一个贝塞尔曲线移动的宇宙飞船图片

    运行示例,你会发现飞船的动画有些不太真实,这是因为当它运动的时候永远指向右边,而不是指向曲线切线的方向。你可以调整它的affineTransform来对运动方向做动画,但很可能和其它的动画冲突。

    幸运的是,苹果预见到了这点,并且给CAKeyFrameAnimation添加了一个rotationMode的属性。设置它为常量kCAAnimationRotateAuto(清单8.7),图层将会根据曲线的切线自动旋转(图8.2)。

    清单8.7 通过rotationMode自动对齐图层到曲线

    - (void)viewDidLoad
    {
    [super viewDidLoad];
    //create a path
    ...
    //create the keyframe animation
    CAKeyframeAnimation *animation = [CAKeyframeAnimation animation];
    animation.keyPath = @"position";
    animation.duration = 4.0;
    animation.path = bezierPath.CGPath;
    animation.rotationMode = kCAAnimationRotateAuto;
    [shipLayer addAnimation:animation forKey:nil];
    }

    图8.2

    图8.2 匹配曲线切线方向的飞船图层

    虚拟属性

    之前提到过属性动画实际上是针对于关键路径而不是一个键,这就意味着可以对子属性甚至是虚拟属性做动画。但是虚拟属性到底是什么呢?

    考虑一个旋转的动画:如果想要对一个物体做旋转的动画,那就需要作用于transform属性,因为CALayer没有显式提供角度或者方向之类的属性,代码如清单8.8所示

    清单8.8 用transform属性对图层做动画

    @interface ViewController ()

    @property (nonatomic, weak) IBOutlet UIView *containerView;

    @end

    @implementation ViewController

    - (void)viewDidLoad
    {
    [super viewDidLoad];
    //add the ship
    CALayer *shipLayer = [CALayer layer];
    shipLayer.frame = CGRectMake(0, 0, 128, 128);
    shipLayer.position = CGPointMake(150, 150);
    shipLayer.contents = (__bridge id)[UIImage imageNamed: @"Ship.png"].CGImage;
    [self.containerView.layer addSublayer:shipLayer];
    //animate the ship rotation
    CABasicAnimation *animation = [CABasicAnimation animation];
    animation.keyPath = @"transform";
    animation.duration = 2.0;
    animation.toValue = [NSValue valueWithCATransform3D: CATransform3DMakeRotation(M_PI, 0, 0, 1)];
    [shipLayer addAnimation:animation forKey:nil];
    }

    @end

    这么做是可行的,但看起来更因为是运气而不是设计的原因,如果我们把旋转的值从M_PI(180度)调整到2 * M_PI(360度),然后运行程序,会发现这时候飞船完全不动了。这是因为这里的矩阵做了一次360度的旋转,和做了0度是一样的,所以最后的值根本没变。

    现在继续使用M_PI,但这次用byValue而不是toValue。也许你会认为这和设置toValue结果一样,因为0 + 90度 == 90度,但实际上飞船的图片变大了,并没有做任何旋转,这是因为变换矩阵不能像角度值那样叠加。

    那么如果需要独立于角度之外单独对平移或者缩放做动画呢?由于都需要我们来修改transform属性,实时地重新计算每个时间点的每个变换效果,然后根据这些创建一个复杂的关键帧动画,这一切都是为了对图层的一个独立做一个简单的动画。

    幸运的是,有一个更好的解决方案:为了旋转图层,我们可以对transform.rotation关键路径应用动画,而不是transform本身(清单8.9)。

    清单8.9 对虚拟的transform.rotation属性做动画

    @interface ViewController ()

    @property (nonatomic, weak) IBOutlet UIView *containerView;

    @end

    @implementation ViewController

    - (void)viewDidLoad
    {
    [super viewDidLoad];
    //add the ship
    CALayer *shipLayer = [CALayer layer];
    shipLayer.frame = CGRectMake(0, 0, 128, 128);
    shipLayer.position = CGPointMake(150, 150);
    shipLayer.contents = (__bridge id)[UIImage imageNamed: @"Ship.png"].CGImage;
    [self.containerView.layer addSublayer:shipLayer];
    //animate the ship rotation
    CABasicAnimation *animation = [CABasicAnimation animation];
    animation.keyPath = @"transform.rotation";
    animation.duration = 2.0;
    animation.byValue = @(M_PI * 2);
    [shipLayer addAnimation:animation forKey:nil];
    }

    @end

    结果运行的特别好,用transform.rotation而不是transform做动画的好处如下:

    • 我们可以不通过关键帧一步旋转多于180度的动画。
    • 可以用相对值而不是绝对值旋转(设置byValue而不是toValue)。
    • 可以不用创建CATransform3D,而是使用一个简单的数值来指定角度。
    • 不会和transform.position或者transform.scale冲突(同样是使用关键路径来做独立的动画属性)。

    transform.rotation属性有一个奇怪的问题是它其实并不存在。这是因为CATransform3D并不是一个对象,它实际上是一个结构体,也没有符合KVC相关属性,transform.rotation实际上是一个CALayer用于处理动画变换的虚拟属性。

    你不可以直接设置transform.rotation或者transform.scale,他们不能被直接使用。当你对他们做动画时,Core Animation自动地根据通过CAValueFunction来计算的值来更新transform属性。

    CAValueFunction用于把我们赋给虚拟的transform.rotation简单浮点值转换成真正的用于摆放图层的CATransform3D矩阵值。你可以通过设置CAPropertyAnimationvalueFunction属性来改变,于是你设置的函数将会覆盖默认的函数。

    CAValueFunction看起来似乎是对那些不能简单相加的属性(例如变换矩阵)做动画的非常有用的机制,但由于CAValueFunction的实现细节是私有的,所以目前不能通过继承它来自定义。你可以通过使用苹果目前已经提供的常量(目前都是和变换矩阵的虚拟属性相关,所以没太多使用场景了,因为这些属性都有了默认的实现方式)。


    8.2 动画组

    动画组

    CABasicAnimationCAKeyframeAnimation仅仅作用于单独的属性,而CAAnimationGroup可以把这些动画组合在一起。CAAnimationGroup是另一个继承于CAAnimation的子类,它添加了一个animations数组的属性,用来组合别的动画。我们把清单8.6那种关键帧动画和调整图层背景色的基础动画组合起来(清单8.10),结果如图8.3所示。

    清单8.10 组合关键帧动画和基础动画

    - (void)viewDidLoad
    {
    [super viewDidLoad];
    //create a path
    UIBezierPath *bezierPath = [[UIBezierPath alloc] init];
    [bezierPath moveToPoint:CGPointMake(0, 150)];
    [bezierPath addCurveToPoint:CGPointMake(300, 150) controlPoint1:CGPointMake(75, 0) controlPoint2:CGPointMake(225, 300)];
    //draw the path using a CAShapeLayer
    CAShapeLayer *pathLayer = [CAShapeLayer layer];
    pathLayer.path = bezierPath.CGPath;
    pathLayer.fillColor = [UIColor clearColor].CGColor;
    pathLayer.strokeColor = [UIColor redColor].CGColor;
    pathLayer.lineWidth = 3.0f;
    [self.containerView.layer addSublayer:pathLayer];
    //add a colored layer
    CALayer *colorLayer = [CALayer layer];
    colorLayer.frame = CGRectMake(0, 0, 64, 64);
    colorLayer.position = CGPointMake(0, 150);
    colorLayer.backgroundColor = [UIColor greenColor].CGColor;
    [self.containerView.layer addSublayer:colorLayer];
    //create the position animation
    CAKeyframeAnimation *animation1 = [CAKeyframeAnimation animation];
    animation1.keyPath = @"position";
    animation1.path = bezierPath.CGPath;
    animation1.rotationMode = kCAAnimationRotateAuto;
    //create the color animation
    CABasicAnimation *animation2 = [CABasicAnimation animation];
    animation2.keyPath = @"backgroundColor";
    animation2.toValue = (__bridge id)[UIColor redColor].CGColor;
    //create group animation
    CAAnimationGroup *groupAnimation = [CAAnimationGroup animation];
    groupAnimation.animations = @[animation1, animation2];
    groupAnimation.duration = 4.0;
    //add the animation to the color layer
    [colorLayer addAnimation:groupAnimation forKey:nil];
    }

    图8.3

    图8.3 关键帧路径和基础动画的组合

    收起阅读 »

    Vue3的7种和Vue2的12种组件通信,年轻人?还不收藏在等什么!!!

    Vue2.x组件通信12种方式写在后面了,先来 Vue3 的 奥力给! Vue3 组件通信方式 props $emit expose / ref $attrs v-model provide / inject Vuex Vue3 通信使用写法 props ...
    继续阅读 »

    Vue2.x组件通信12种方式写在后面了,先来 Vue3 的


    奥力给!


    Vue3 组件通信方式



    • props

    • $emit

    • expose / ref

    • $attrs

    • v-model

    • provide / inject

    • Vuex


    Vue3 通信使用写法


    props


    用 props 传数据给子组件有两种方法,如下


    方法一,混合写法


    // Parent.vue 传送
    <child :msg1="msg1" :msg2="msg2"></child>
    <script>
    import child from "./child.vue"
    import { ref, reactive } from "vue"
    export default {
    data(){
    return {
    msg1:"这是传级子组件的信息1"
    }
    },
    setup(){
    // 创建一个响应式数据

    // 写法一 适用于基础类型 ref 还有其他用处,下面章节有介绍
    const msg2 = ref("这是传级子组件的信息2")

    // 写法二 适用于复杂类型,如数组、对象
    const msg2 = reactive(["这是传级子组件的信息2"])

    return {
    msg2
    }
    }
    }
    </script>

    // Child.vue 接收
    <script>
    export default {
    props: ["msg1", "msg2"],// 如果这行不写,下面就接收不到
    setup(props) {
    console.log(props) // { msg1:"这是传给子组件的信息1", msg2:"这是传给子组件的信息2" }
    },
    }
    </script>

    方法二,纯 Vue3 写法


    // Parent.vue 传送
    <child :msg2="msg2"></child>
    <script setup>
    import child from "./child.vue"
    import { ref, reactive } from "vue"
    const msg2 = ref("这是传给子组件的信息2")
    // 或者复杂类型
    const msg2 = reactive(["这是传级子组件的信息2"])
    </script>

    // Child.vue 接收
    <script setup>
    // 不需要引入 直接使用
    // import { defineProps } from "vue"
    const props = defineProps({
    // 写法一
    msg2: String
    // 写法二
    msg2:{
    type:String,
    default:""
    }
    })
    console.log(props) // { msg2:"这是传级子组件的信息2" }
    </script>

    注意:


    如果父组件是混合写法,子组件纯 Vue3 写法的话,是接收不到父组件里 data 的属性,只能接收到父组件里 setup 函数里传的属性


    如果父组件是纯 Vue3 写法,子组件混合写法,可以通过 props 接收到 data 和 setup 函数里的属性,但是子组件要是在 setup 里接收,同样只能接收到父组件中 setup 函数里的属性,接收不到 data 里的属性


    官方也说了,既然用了 3,就不要写 2 了,所以不推荐混合写法。下面的例子,一律只用纯 Vue3 的写法,就不写混合写法了


    $emit


    // Child.vue 派发
    <template>
    // 写法一
    <button @click="emit('myClick')">按钮</buttom>
    // 写法二
    <button @click="handleClick">按钮</buttom>
    </template>
    <script setup>

    // 方法一 适用于Vue3.2版本 不需要引入
    // import { defineEmits } from "vue"
    // 对应写法一
    const emit = defineEmits(["myClick","myClick2"])
    // 对应写法二
    const handleClick = ()=>{
    emit("myClick", "这是发送给父组件的信息")
    }

    // 方法二 不适用于 Vue3.2版本,该版本 useContext()已废弃
    import { useContext } from "vue"
    const { emit } = useContext()
    const handleClick = ()=>{
    emit("myClick", "这是发送给父组件的信息")
    }
    </script>

    // Parent.vue 响应
    <template>
    <child @myClick="onMyClick"></child>
    </template>
    <script setup>
    import child from "./child.vue"
    const onMyClick = (msg) => {
    console.log(msg) // 这是父组件收到的信息
    }
    </script>

    expose / ref


    父组件获取子组件的属性或者调用子组件方法


    // Child.vue
    <script setup>
    // 方法一 不适用于Vue3.2版本,该版本 useContext()已废弃
    import { useContext } from "vue"
    const ctx = useContext()
    // 对外暴露属性方法等都可以
    ctx.expose({
    childName: "这是子组件的属性",
    someMethod(){
    console.log("这是子组件的方法")
    }
    })

    // 方法二 适用于Vue3.2版本, 不需要引入
    // import { defineExpose } from "vue"
    defineExpose({
    childName: "这是子组件的属性",
    someMethod(){
    console.log("这是子组件的方法")
    }
    })
    </script>

    // Parent.vue 注意 ref="comp"
    <template>
    <child ref="comp"></child>
    <button @click="handlerClick">按钮</button>
    </template>
    <script setup>
    import child from "./child.vue"
    import { ref } from "vue"
    const comp = ref(null)
    const handlerClick = () => {
    console.log(comp.value.childName) // 获取子组件对外暴露的属性
    comp.value.someMethod() // 调用子组件对外暴露的方法
    }
    </script>

    attrs


    attrs:包含父作用域里除 class 和 style 除外的非 props 属性集合


    // Parent.vue 传送
    <child :msg1="msg1" :msg2="msg2" title="3333"></child>
    <script setup>
    import child from "./child.vue"
    import { ref, reactive } from "vue"
    const msg1 = ref("1111")
    const msg2 = ref("2222")
    </script>

    // Child.vue 接收
    <script setup>
    import { defineProps, useContext, useAttrs } from "vue"
    // 3.2版本不需要引入 defineProps,直接用
    const props = defineProps({
    msg1: String
    })
    // 方法一 不适用于 Vue3.2版本,该版本 useContext()已废弃
    const ctx = useContext()
    // 如果没有用 props 接收 msg1 的话就是 { msg1: "1111", msg2:"2222", title: "3333" }
    console.log(ctx.attrs) // { msg2:"2222", title: "3333" }

    // 方法二 适用于 Vue3.2版本
    const attrs = useAttrs()
    console.log(attrs) // { msg2:"2222", title: "3333" }
    </script>

    v-model


    可以支持多个数据双向绑定


    // Parent.vue
    <child v-model:key="key" v-model:value="value"></child>
    <script setup>
    import child from "./child.vue"
    import { ref, reactive } from "vue"
    const key = ref("1111")
    const value = ref("2222")
    </script>

    // Child.vue
    <template>
    <button @click="handlerClick">按钮</button>
    </template>
    <script setup>

    // 方法一 不适用于 Vue3.2版本,该版本 useContext()已废弃
    import { useContext } from "vue"
    const { emit } = useContext()

    // 方法二 适用于 Vue3.2版本,不需要引入
    // import { defineEmits } from "vue"
    const emit = defineEmits(["key","value"])

    // 用法
    const handlerClick = () => {
    emit("update:key", "新的key")
    emit("update:value", "新的value")
    }
    </script>

    provide / inject


    provide / inject 为依赖注入


    provide:可以让我们指定想要提供给后代组件的数据或


    inject:在任何后代组件中接收想要添加在这个组件上的数据,不管组件嵌套多深都可以直接拿来用


    // Parent.vue
    <script setup>
    import { provide } from "vue"
    provide("name", "沐华")
    </script>

    // Child.vue
    <script setup>
    import { inject } from "vue"
    const name = inject("name")
    console.log(name) // 沐华
    </script>

    Vuex


    // store/index.js
    import { createStore } from "vuex"
    export default createStore({
    state:{ count: 1 },
    getters:{
    getCount: state => state.count
    },
    mutations:{
    add(state){
    state.count++
    }
    }
    })

    // main.js
    import { createApp } from "vue"
    import App from "./App.vue"
    import store from "./store"
    createApp(App).use(store).mount("#app")

    // Page.vue
    // 方法一 直接使用
    <template>
    <div>{{ $store.state.count }}</div>
    <button @click="$store.commit('add')">按钮</button>
    </template>

    // 方法二 获取
    <script setup>
    import { useStore, computed } from "vuex"
    const store = useStore()
    console.log(store.state.count) // 1

    const count = computed(()=>store.state.count) // 响应式,会随着vuex数据改变而改变
    console.log(count) // 1
    </script>

    Vue2.x 组件通信方式


    Vue2.x 组件通信共有12种



    1. props

    2. $emit / v-on

    3. .sync

    4. v-model

    5. ref

    6. $children / $parent

    7. $attrs / $listeners

    8. provide / inject

    9. EventBus

    10. Vuex

    11. $root

    12. slot


    父子组件通信可以用:



    • props

    • $emit / v-on

    • $attrs / $listeners

    • ref

    • .sync

    • v-model

    • $children / $parent


    兄弟组件通信可以用:



    • EventBus

    • Vuex

    • $parent


    跨层级组件通信可以用:



    • provide/inject

    • EventBus

    • Vuex

    • $attrs / $listeners

    • $root


    Vue2.x 通信使用写法


    下面把每一种组件通信方式的写法一一列出


    1. props


    父组件向子组件传送数据,这应该是最常用的方式了


    子组件接收到数据之后,不能直接修改父组件的数据。会报错,所以当父组件重新渲染时,数据会被覆盖。如果子组件内要修改的话推荐使用 computed


    // Parent.vue 传送
    <template>
    <child :msg="msg"></child>
    </template>

    // Child.vue 接收
    export default {
    // 写法一 用数组接收
    props:['msg'],
    // 写法二 用对象接收,可以限定接收的数据类型、设置默认值、验证等
    props:{
    msg:{
    type:String,
    default:'这是默认数据'
    }
    },
    mounted(){
    console.log(this.msg)
    },
    }

    2. .sync


    可以帮我们实现父组件向子组件传递的数据 的双向绑定,所以子组件接收到数据后可以直接修改,并且会同时修改父组件的数据


    // Parent.vue
    <template>
    <child :page.sync="page"></child>
    </template>
    <script>
    export default {
    data(){
    return {
    page:1
    }
    }
    }

    // Child.vue
    export default {
    props:["page"],
    computed(){
    // 当我们在子组件里修改 currentPage 时,父组件的 page 也会随之改变
    currentPage {
    get(){
    return this.page
    },
    set(newVal){
    this.$emit("update:page", newVal)
    }
    }
    }
    }
    </script>

    3. v-model


    和 .sync 类似,可以实现将父组件传给子组件的数据为双向绑定,子组件通过 $emit 修改父组件的数据


    // Parent.vue
    <template>
    <child v-model="value"></child>
    </template>
    <script>
    export default {
    data(){
    return {
    value:1
    }
    }
    }

    // Child.vue
    <template>
    <input :value="value" @input="handlerChange">
    </template>
    export default {
    props:["value"],
    // 可以修改事件名,默认为 input
    model:{
    event:"updateValue"
    },
    methods:{
    handlerChange(e){
    this.$emit("input", e.target.value)
    // 如果有上面的重命名就是这样
    this.$emit("updateValue", e.target.value)
    }
    }
    }
    </script>

    4. ref


    ref 如果在普通的DOM元素上,引用指向的就是该DOM元素;


    如果在子组件上,引用的指向就是子组件实例,然后父组件就可以通过 ref 主动获取子组件的属性或者调用子组件的方法


    // Child.vue
    export default {
    data(){
    return {
    name:"沐华"
    }
    },
    methods:{
    someMethod(msg){
    console.log(msg)
    }
    }
    }

    // Parent.vue
    <template>
    <child ref="child"></child>
    </template>
    <script>
    export default {
    mounted(){
    const child = this.$refs.child
    console.log(child.name) // 沐华
    child.someMethod("调用了子组件的方法")
    }
    }
    </script>

    5. $emit / v-on


    子组件通过派发事件的方式给父组件数据,或者触发父组件更新等操作


    // Child.vue 派发
    export default {
    data(){
    return { msg: "这是发给父组件的信息" }
    },
    methods: {
    handleClick(){
    this.$emit("sendMsg",this.msg)
    }
    },
    }
    // Parent.vue 响应
    <template>
    <child v-on:sendMsg="getChildMsg"></child>
    // 或 简写
    <child @sendMsg="getChildMsg"></child>
    </template>

    export default {
    methods:{
    getChildMsg(msg){
    console.log(msg) // 这是父组件接收到的消息
    }
    }
    }

    6. $attrs / $listeners


    多层嵌套组件传递数据时,如果只是传递数据,而不做中间处理的话就可以用这个,比如父组件向孙子组件传递数据时


    $attrs:包含父作用域里除 class 和 style 除外的非 props 属性集合。通过 this.$attrs 获取父作用域中所有符合条件的属性集合,然后还要继续传给子组件内部的其他组件,就可以通过 v-bind="$attrs"


    $listeners:包含父作用域里 .native 除外的监听事件集合。如果还要继续传给子组件内部的其他组件,就可以通过 v-on="$linteners"


    使用方式是相同的


    // Parent.vue
    <template>
    <child :name="name" title="1111" ></child>
    </template
    export default{
    data(){
    return {
    name:"沐华"
    }
    }
    }

    // Child.vue
    <template>
    // 继续传给孙子组件
    <sun-child v-bind="$attrs"></sun-child>
    </template>
    export default{
    props:["name"], // 这里可以接收,也可以不接收
    mounted(){
    // 如果props接收了name 就是 { title:1111 },否则就是{ name:"沐华", title:1111 }
    console.log(this.$attrs)
    }
    }

    7. $children / $parent


    $children:获取到一个包含所有子组件(不包含孙子组件)的 VueComponent 对象数组,可以直接拿到子组件中所有数据和方法等


    $parent:获取到一个父节点的 VueComponent 对象,同样包含父节点中所有数据和方法等


    // Parent.vue
    export default{
    mounted(){
    this.$children[0].someMethod() // 调用第一个子组件的方法
    this.$children[0].name // 获取第一个子组件中的属性
    }
    }

    // Child.vue
    export default{
    mounted(){
    this.$parent.someMethod() // 调用父组件的方法
    this.$parent.name // 获取父组件中的属性
    }
    }

    8. provide / inject


    provide / inject 为依赖注入,说是不推荐直接用于应用程序代码中,但是在一些插件或组件库里却是被常用,所以我觉得用也没啥,还挺好用的


    provide:可以让我们指定想要提供给后代组件的数据或方法


    inject:在任何后代组件中接收想要添加在这个组件上的数据或方法,不管组件嵌套多深都可以直接拿来用


    要注意的是 provide 和 inject 传递的数据不是响应式的,也就是说用 inject 接收来数据后,provide 里的数据改变了,后代组件中的数据不会改变,除非传入的就是一个可监听的对象


    所以建议还是传递一些常量或者方法


    // 父组件
    export default{
    // 方法一 不能获取 methods 中的方法
    provide:{
    name:"沐华",
    age: this.data中的属性
    },
    // 方法二 不能获取 data 中的属性
    provide(){
    return {
    name:"沐华",
    someMethod:this.someMethod // methods 中的方法
    }
    },
    methods:{
    someMethod(){
    console.log("这是注入的方法")
    }
    }
    }

    // 后代组件
    export default{
    inject:["name","someMethod"],
    mounted(){
    console.log(this.name)
    this.someMethod()
    }
    }

    9. EventBus


    EventBus 是中央事件总线,不管是父子组件,兄弟组件,跨层级组件等都可以使用它完成通信操作


    定义方式有三种


    // 方法一
    // 抽离成一个单独的 js 文件 Bus.js ,然后在需要的地方引入
    // Bus.js
    import Vue from "vue"
    export default new Vue()

    // 方法二 直接挂载到全局
    // main.js
    import Vue from "vue"
    Vue.prototype.$bus = new Vue()

    // 方法三 注入到 Vue 根对象上
    // main.js
    import Vue from "vue"
    new Vue({
    el:"#app",
    data:{
    Bus: new Vue()
    }
    })

    使用如下,以方法一按需引入为例


    // 在需要向外部发送自定义事件的组件内
    <template>
    <button @click="handlerClick">按钮</button>
    </template>
    import Bus from "./Bus.js"
    export default{
    methods:{
    handlerClick(){
    // 自定义事件名 sendMsg
    Bus.$emit("sendMsg", "这是要向外部发送的数据")
    }
    }
    }

    // 在需要接收外部事件的组件内
    import Bus from "./Bus.js"
    export default{
    mounted(){
    // 监听事件的触发
    Bus.$on("sendMsg", data => {
    console.log("这是接收到的数据:", data)
    })
    },
    beforeDestroy(){
    // 取消监听
    Bus.$off("sendMsg")
    }
    }

    10. Vuex


    Vuex 是状态管理器,集中式存储管理所有组件的状态。这一块内容过长,如果基础不熟的话可以看这个Vuex,然后大致用法如下


    比如创建这样的文件结构


    微信图片_20210824003500.jpg


    index.js 里内容如下


    import Vue from 'vue'
    import Vuex from 'vuex'
    import getters from './getters'
    import actions from './actions'
    import mutations from './mutations'
    import state from './state'
    import user from './modules/user'

    Vue.use(Vuex)

    const store = new Vuex.Store({
    modules: {
    user
    },
    getters,
    actions,
    mutations,
    state
    })
    export default store

    然后在 main.js 引入


    import Vue from "vue"
    import store from "./store"
    new Vue({
    el:"#app",
    store,
    render: h => h(App)
    })

    然后在需要的使用组件里


    import { mapGetters, mapMutations } from "vuex"
    export default{
    computed:{
    // 方式一 然后通过 this.属性名就可以用了
    ...mapGetters(["引入getters.js里属性1","属性2"])
    // 方式二
    ...mapGetters("user", ["user模块里的属性1","属性2"])
    },
    methods:{
    // 方式一 然后通过 this.属性名就可以用了
    ...mapMutations(["引入mutations.js里的方法1","方法2"])
    // 方式二
    ...mapMutations("user",["引入user模块里的方法1","方法2"])
    }
    }

    // 或者也可以这样获取
    this.$store.state.xxx
    this.$store.state.user.xxx

    11. $root


    $root 可以拿到 App.vue 里的数据和方法


    12. slot


    就是把子组件的数据通过插槽的方式传给父组件使用,然后再插回来


    // Child.vue
    <template>
    <div>
    <slot :user="user"></slot>
    </div>
    </template>
    export default{
    data(){
    return {
    user:{ name:"沐华" }
    }
    }
    }

    // Parent.vue
    <template>
    <div>
    <child v-slot="slotProps">
    {{ slotProps.user.name }}
    </child>
    </div>
    </template>

    结语


    写作不易,你的一赞一评,就是我前行的最大动力。


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

    收起阅读 »

    前端9种图片格式基础知识, 你应该知道的

    彩色深度 彩色深度标准通常有以下几种: 8位色,每个像素所能显示的彩色数为2的8次方,即256种颜色。 16位增强色,16位彩色,每个像素所能显示的彩色数为2的16次方,即65536种颜色。 24位真彩色,每个像素所能显示的彩色数为24位,即2的24次方,约...
    继续阅读 »

    彩色深度


    彩色深度标准通常有以下几种:



    • 8位色,每个像素所能显示的彩色数为2的8次方,即256种颜色。

    • 16位增强色,16位彩色,每个像素所能显示的彩色数为2的16次方,即65536种颜色。

    • 24位真彩色,每个像素所能显示的彩色数为24位,即2的24次方,约1680万种颜色。

    • 32位真彩色,即在24位真彩色图像的基础上再增加一个表示图像透明度信息的Alpha通道。

      32位真彩色并非是2的32次方的色数,它其实也是1677万多色,不过它增加了256阶颜色的灰度,为了方便称呼,就规定它为32位色


    图的分类


    光栅图和矢量图


    对于图片,一般分光栅图和矢量图。



    • 光栅图:是基于 pixel像素构成的图像。JPEG、PNG,webp等都属于此类

    • 矢量图:使用点,线和多边形等几何形状来构图,具有高分辨率和缩放功能. SVG就是一种矢量图。


    无压缩, 无损压缩, 有损压缩


    另一种分类




    • 无压缩。无压缩的图片格式不对图片数据进行压缩处理,能准确地呈现原图片。BMP格式就是其中之一。




    • 无损压缩。压缩算法对图片的所有的数据进行编码压缩,能在保证图片的质量的同时降低图片的尺寸。png是其中的代表。




    • 有损压缩。压缩算法不会对图片所有的数据进行编码压缩,而是在压缩的时候,去除了人眼无法识别的图片细节。因此有损压缩可以在同等图片质量的情况下大幅降低图片的尺寸。其中的代表是jpg。




    前端9种图片格式


    诞生时间


    对于超过30岁的程序员来说,她们都很年轻,真的是遇到好时光!


    85年前,人们都在干嘛呢?



    1. GIF - 1987

    2. Base64- 1987

    3. JPEG - 1992

    4. PNG - 1996

    5. SVG - 1999

    6. JPEG2000 - 1997 to 2000

    7. APNG - 2004

    8. WebP - 2010


    ico: 1985年??

    查阅文档说ico文件格式是伴随着 Windows 1.0 发行诞生的。


    GIF


    GIF是一种索引色模式图片,所以GIF每帧图所表现的颜色最多为256种。GIF能够支持动画,也能支持背景透明,这点连古老的IE6都支持,所以在以前想要在项目中使用背景透明图片,其中一种方案就是生成GIF图片。


    优点



    • 支持动画和透明背景

    • 兼容性好

    • 灰度图像表现佳

    • 支持交错

      部分接收到的文件可以以较低的质量显示。这在网络连接缓慢时特别有用。


    缺点



    • 最多支持 8 位 256 色,色阶过渡糟糕,图片具有颗粒感

    • 支持透明,但不支持半透明,边缘有杂边


    适用场景



    • 色彩简单的logo、icon、线框图适合采用gif格

    • 动画


    JPG/JPEG


    这里提个问题: jpg和jpeg有啥区别


    平常我们大部分见到的静态图基本都是这种图片格式。这种格式的图片能比较好的表现各种色彩,主要在压缩的时候会有所失真,也正因为如此,造就了这种图片格式体积的轻量。


    优点



    • 压缩率高

    • 兼容性好

    • 色彩丰富


    缺点



    • JPEG不适合用来存储企业Logo、线框类的这种高清图

    • 不支持动画、背景透明


    JPEG 2000 (了解即可)


    JPEG 2000是基于小波变换的图像压缩标准,由Joint Photographic Experts Group组织创建和维护。JPEG 2000通常被认为是未来取代JPEG(基于离散余弦变换)的下一代图像压缩标准。JPEG 2000文件的副档名通常为.jp2,MIME类型是image/jp2。


    JPEG2000的压缩比更高,而且不会产生原先的基于离散余弦变换的JPEG标准产生的块状模糊瑕疵。JPEG2000同时支持有损压缩无损压缩


    目前就safari支持,can is use-png2000支持18%。


    优点



    • 支持有损和无损压缩


    缺点



    • 支持率太低了


    ICO


    ICO (Microsoft Windows 图标)文件格式是微软为 Windows 系统的桌面图标设计的。网站可以在网站的根目录中提供一个名为 favicon.ICO, 在收藏夹菜单中显示的图标,以及其他一些有用的标志性网站表示形式。

    一个 ICO 文件可以包含多个图标,并以列出每个图标详细信息的目录开始。


    其主要用来做网站图标,现在png也是可以用来做网站图标的。


    PNG


    PNG格式是有三种版本的,分别为PNG-8,PNG-24,PNG-32,所有这些版本都不支持动画的。PNG-8跟GIF类似的属性是相似的,都是索引色模式,而且都支持背景透明。相对比GIF格式好的特点在与背景透明时,图像边缘没有什么噪点,颜色表现更优秀。PNG-24其实就是无损压缩的JPEG。而PNG-32就是在PNG-24的基础上,增加了透明度的支持。


    如果没有动画需求推荐使用png-8来替代gif


    优点



    1. 不失真的情况下尽可能压缩图像文件的大小

    2. 像素丰富

    3. 支持透明(alpha通道)


    缺点



    1. 文件大


    这里额外提一下,gif和jpg有渐进,png有交错,都是在没有完全下载图片的时候,能看到图片全貌。


    具体可以看在线示例: png正常,png交错,jpg渐进


    APNG:Animated PNG


    APNG(Animated Portable Network Graphics)顾名思义是基于 PNG 格式扩展的一种动画格式,增加了对动画图像的支持,同时加入了 24 位图像和 8 位 Alpha 透明度的支持,这意味着动画将拥有更好的质量,其诞生的目的是为了替代老旧的 GIF 格式,但它目前并没有获得 PNG 组织官方的认可。


    从Can I Use上查看,除了IE系列, chrome, firefox, safari均已支持。2021-08月的时候支持达到94%。


    相对GIF来说



    • 色彩丰富

    • 支持透明

    • 向下兼容 PNG

    • 支持动画


    缺点



    • 生成比较繁琐

    • 未标准化


    webP


    有损 WebP 图像平均比视觉上类似压缩级别的 JPEG 图像小25-35% 。无损耗的 WebP 图像通常比 PNG 格式的相同图像小26% 。WebP 还支持动画: 在有损的 WebP 文件中,图像数据由 VP8位流表示,该位流可能包含多个帧。


    包括体积小、色彩表现足够、支持动画。 简直了就是心中的完美女神!!


    can i use - webp上看,支持率95%。 主要是Safari低版本和IE低版本不兼容。


    优点



    • 同等质量更小

    • 压缩之后质量无明显变化

    • 支持无损图像

    • 支持动画


    缺点



    • 兼容性吧,相对jpg,png,gif来说


    SVG


    SVG 是一种基于 xml 的矢量图形格式,它将图像的内容指定为一组绘图命令,这些命令创建形状、线条、应用颜色、过滤器等等。SVG 文件是理想的图表,图标和其他图像,可以准确地绘制在任何大小。因此,SVG 是现代 Web 设计中用户界面元素的流行选择。


    优点



    • 可伸缩性

      你可以随心所欲地把它们做大或者做小,而不用牺牲质量



    • Svg 平均比 GIF、 JPEG、 PNG 小得多,甚至在极高的分辨率下也是如此

    • 支持动画

      更灵活,质量无与伦比

    • 与DOM无缝衔接

      Svg 可以直接使用 HTML、 CSS 和 JavaScript (例如动画)来操作


    缺点



    • SVG复杂度高会减慢渲染速度

    • 不适合游戏类等高互动动画


    base64


    图片的 base64 编码就是可以将一副图片数据编码成一串字符串,使用该字符串代替图像地址,图片随着 HTML 的下载同时下载到本地,不再单独消耗一个http来请求图片。


    优点



    • 无额外请求

    • 对于极小或者极简单图片

    • 可像单独图片一样使用,比如背景图片重复使用等

    • 没有跨域问题,无需考虑缓存、文件头或者cookies问题  


    缺点



    • 相比其他格式,体积会至少大1/3

    • 编码解码有额外消耗


    一些对比


    PNG, GIF, JPG 比较


    大小比较:通常地,PNG ≈ JPG > GIF 8位的PNG完全可以替代掉GIF

    透明性:PNG > GIF > JPG

    色彩丰富程度:JPG > PNG >GIF

    兼容程度:GIF ≈ JPG > PNG

    gif, jpg, png, web优缺点和使用场景



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

    收起阅读 »

    Why | 为什么需要虚拟内存?

    冯-诺依曼老爷子告诉过我们,算术逻辑单元和控制器单元组成的 CPU 负责进行运算以及程序流程的控制。运算所需要的指令和数据由 内存 来提供。 那么,如果让你作为操作系统的顶层设计者,你会提供一种什么机制,让 CPU 可以从内存中获取指令和数据呢? 用 ...
    继续阅读 »

    冯-诺依曼老爷子告诉过我们,算术逻辑单元和控制器单元组成的 CPU 负责进行运算以及程序流程的控制。运算所需要的指令和数据由 内存 来提供。


    Von_Neumann_Architecture.png


    那么,如果让你作为操作系统的顶层设计者,你会提供一种什么机制,让 CPU 可以从内存中获取指令和数据呢?


    用 C 语言写一个 Hello World,通过 objdump 查看汇编代码。我随便截取一行。


    mov    0x200aed(%rip),%rax        # 200fe8 <__gmon_start__>

    这一行汇编代码中包含了一个内存地址。这个内存地址是物理内存中的真实地址吗?


    我们假设它就是真实的物理地址,但是程序员在编程时是无法得知要运行的设备的内存信息的,所以针对不同的操作系统,得在编译期将程序中的地址转换为真实物理地址。这在单道编程的情况下可行,对于多道编程呢?不同的程序之间如何确定各自在内存中的位置?


    从单道编程到多道编程是计算机发展前进的一大步。CPU 通过轮询时间片的方式让多个程序仿佛在同时运行。显然,在程序中使用真实的物理地址会打破这一幻像,不同的程序之间不得而知对方用的是哪一块物理内存,各自的内存完全无法得到保护。


    所以,程序中的地址不能是真实的物理地址,但又要与真实的物理地址存在一定的映射关系 。我们把程序中的地址称为 虚拟地址 ,它至少应该具备以下特性:



    • 能通过一定的机制映射到真实的物理地址

    • 保证不同的程序(进程) 映射的真实物理地址之间互相独立

    • 它应该是自动工作的,对于程序开发者来说是透明的


    基于这三个特性,我们一起来探究一下 虚拟内存 的工作方式。


    一个萝卜一个坑,分段


    最直观的解决方案,给每个程序分配一块独立的内存空间,如下图所示。


    动态定位.png


    对于每个程序来说,它的虚拟内存空间都从 0 开始,基址寄存器 中存储其在物理内存空间的起始地址。所以,物理地址和虚拟地址之间就存在这样的关系:


    物理地址 = 虚拟地址 + 基址

    这样的地址转换由叫做 内存管理单元(Memory Management Unit,MMU) 的硬件负责完成。


    界限寄存器 可以存储程序占用内存的大小,也可以存储界限的物理地址,它提供基本的内存访问保护。如果 MMU 转换出的物理地址超过了界限,将会触发异常。每个 CPU 都有一对基址寄存器和界限寄存器,当发生进程切换时,更新寄存器的值,这样就做到了进程间内存独立。


    乍一看,基本满足了虚拟内存的三个特性,但事实上基本没有操作系统会这么干。由于它需要在虚拟内存和物理内存中分别分配一块连续的内存空间,再进行内存映射。这样的缺点很明显。


    第一,容易造成内存碎片。假设内存经过一段时间的使用,还剩下两块 128 MB 的小块,但此时用户需要运行一个内存占用 129 MB 的程序,在此机制下就无法成功分配内存。虽然可以通过内存交换,将内存拾掇拾掇,和磁盘换来换去,把空余内存拼接起来,但是这么大一块数据,磁盘读写的速度实在太慢了,性能上根本无法接受。


    第二,浪费了很多内存空间。如果把二八法则搬到计算机上,一个程序最经常运行的代码可能两成都占不到。而上面的方案在一开始就要分配好整个程序需要的内存空间,堆和栈之间有一大块的内存是空闲的。


    上面的方案暂且可以看成一种特殊的 “分段”。我们可以试着把段分的更细一些。


    典型的 Linux 进程用户空间内存包含栈、共享库、堆、数据、代码等。我们可以按这些基本类型来分段,为了方便演示,下图中仅分为 栈、堆、代码 三个段。


    分段.png


    将程序按逻辑分为一段一段,放入内存中对应的段区域内,这样避免了之前的方案中堆和栈之间的空间浪费,真正需要内存的时候才会去申请。同时顺带实现了共享。对于一些可以公用的系统基本库,在之前的方案中仍然需要拷贝到各个进程独立的空间中。而分段的方案中,只需要一份拷贝就行,不同进程间的虚拟地址映射到这一份物理拷贝就可以了。


    但是由于各个段的大小不一致,内存碎片的问题可能并不比上一个方案好到哪里去。


    另外,上面提到的所有方案都没有考虑到程序大小的问题。如果程序大小大于物理内存,你再怎么分段也没有办法解决问题。


    把段再分细一点,分页


    为了解决分段产生的内存碎片问题,我们把段分的再细一些,细成一个一个固定大小的页面,虚拟内存和固定内存都是如此。这个固定大小在当前主流操作系统中一般是 4 KB ,部分系统也支持 8 KB、16 KB、64 KB。


    将虚拟页和物理页一一对应起来,虚拟地址到物理地址的转换就不是难事了。


    不论是虚拟内存还是物理内存,在分页之后,给每页拟定一个 页号,再根据 页内偏移量 就可以取到数据了。由于虚拟页和物理页的页大小是一致的,所以页内偏移量无需转换,只需要把虚拟页号转换为物理页号就可以了。


    而虚拟地址正是由 虚拟页号页内偏移量 组成。


    操作系统将虚拟页号到物理页号的映射关系保存在 页表 中,页表是一个 页表项(PTE) 的数组,页表项包含了有效位,物理地址等数据。页表直接使用虚拟页号作为索引,找到对应的页表项。


    分页.png


    上图中的第 3 个虚拟页被映射到了第 2 个物理页。其实 虚拟页可以被映射到任意物理页,连续的虚拟页也不需要对应连续的物理页,这给了操作系统很大的自由。不仅相对减少了内存碎片的产生,也能更方便的实现进程间的数据共享,只要将不同进程的虚拟页映射到同样的物理页就行了。


    为了能直接使用虚拟页号作为索引检索到页表项,页表中的所有页表项必须连续的,并且要提前创建好。那么问题来了,页表有多大?


    以 32 位操作系统为例,最大寻址空间为 2 ^ 32 = 4 GB,页的大小为 4 KB,所以共需要 1 M 个页表项。每个页表项大小为 4 个字节,所以一个页表的大小为 1 M * 4 B = 4 MB 。为实现进程隔离,每个进程又必须有自己独立的页表。顺手看一下你的操作系统,至少都得有上百个进程在同时运行,光页表就得占用几百兆内存,这显然是不合适的。


    实际上,对大多数程序来说,并不需要占用全部的 4 GB 虚拟内存,所以没有必要在一开始就分配完整个页表。使用多级页表可以解决这个问题。


    时间换空间,多级页表


    还是以 32 位操作系统为例,来看个简单的二级页表。


    二级页表.png


    第一级叫 页目录项 ,共有 1 K 项。每一个页目录项又对应着 1 K 个 页表项,总共 1 K * 1 K = 1 M 个页表项,正好对应着 4 GB 的寻址空间。


    对于 32 位的虚拟地址来说,正好 10 位对应着 1 K 个页目录项索引,10 位对应着指定页目录项下的 1 K 个页表项索引,剩下 12 位正好对应页大小 4 KB 的页内偏移量。


    算一下二级页表的大小。1 K 个一级页目录项一共 4 KB,1 M 个二级页表项一共 4 MB ,加起来一共 4.004 MB


    所以,二级页表比普通页表占用的内存还要大?其实并不然。


    首先得明确一点,不管是几级页表,都必须要能覆盖整个虚拟空间。对于只有一级的普通页表来说,一上来就得初始化所有页表项,才能覆盖到整个虚拟空间地址。而对于二级页表来说,1 K 个一级的页目录项就可以足以覆盖,二级页表项只有在需要的时候才被创建。这样就可以节省相当一部分内存。


    另外,二级页表可以不存储在内存中,而是存在磁盘中。这倒并不是专门为多级页表而设计的,这是虚拟内存分页的特性,也正因如此,程序的大小可以大于实际物理内存的大小。


    页命中和缺页


    回想一下之前描述的寻址过程。虚拟地址经过内存管理单元 MMU 的处理,找到对应的页表项 PTE ,转换为物理地址,然后在物理内存中定位到对应的数据。这种理想的情况叫做 页命中 ,根据虚拟地址直接就可以在内存中获取到数据。


    但是,并不是任何时候都可以直接根据 PTE 在内存中拿到数据的。最典型的情况,程序的大小大于物理内存,必然会有数据不存在内存中。另外,由于多级页表并不是开始就创建,所以 PTE 对应的数据可能也不在内存中。


    在任意时刻,虚拟内存页都可以分为三个状态:



    • 未分配的:还未分配(或创建)的页。没有任何数据与其关联,不占用任何磁盘空间

    • 已缓存的:当前已缓存在物理内存中的已分配页

    • 未缓存的:未缓存在物理内存中的已分配页


    只有已缓存的虚拟页可以发生页命中,实际上 PTE 会有一个有效位来表示页表是否有效,假设 0 表示有效,1 表示无效。


    有效位为 0,表示 PTE 可用,直接读数据即可。有效位为 1,在不考虑非法内存地址的情况下,可以认为是未分配或者未缓存,无法直接从内存中读取数据,这种情况称为 缺页


    一旦发生缺页,将由系统的缺页异常处理程序来接管,它会根据特定算法从内存中寻找一个 牺牲页,如果该牺牲页数据被修改过,要先写回磁盘,然后将需要的页换到该牺牲页的位置,并更新 PTE。当异常处理程序返回时,它会重新执行之前导致缺页的命令,也就是之前的寻址操作,这次就直接页命中了。


    看到这,你会发现缺页是一个非常昂贵的操作,操作系统必须尽量减少缺页的发生,所以如何寻找合适的牺牲页是个大问题。如果你替换了一个即将要访问的页,那么一会又得把它换回来,这样频繁的换来换去是无法接受的。关于具体的替换算法,可以阅读 《操作系统导论》第22章 超越物理内存:策略


    缺页.png


    给页表加一层缓存,TLB


    再说回到页表,将虚拟地址转换为物理地址,如果使用未分级的普通页表只需要一次内存访问,但占用内存较大。大多数操作系统使用的是多级页表,例如目前的 64 位 Linux 操作系统,使用的是 四级页表,内存占用小了很多,但付出的代价是要访问四次内存。其实这就是一个 时间换空间 的策略。


    另外,程序执行时的一连串指令的虚拟地址是连续的,相连几个虚拟地址通常是在一个虚拟页中,自然而然它们都对应着同一个物理页。但是无论页表如何设计,访问相邻的虚拟地址,每次仍然都要去访问页表。这里是一个可以优化的点。


    计算机科学领域里的任何问题,都可以通过引入一个中间层来解决。


    既要保留多级页表的低内存特性,又要避免多余的内存访问,那就再加一层 缓存 吧。


    TLB(Translation Lookaside Buffer) ,有的资料翻译成 翻译后备缓冲器,有的翻译成 地址变换高速缓存,且不纠结这个名字。TLB 是封装在 CPU 里的一块缓存芯片,它就是页表的缓存,存储了虚拟地址和页表项的映射关系。


    当进行地址转换时,第一步就是根据虚拟地址从 TLB 中查询是否存在对应的页表项 PTE 。如果 TLB 命中,就不用访问页表了,直接根据 TLB 中缓存的物理地址去 CPU Cache 或者内存取数据。如果 TLB 未命中,和缺页的处理流程类似,通过抛出一个异常,让 TLB 的异常处理程序来接手,它会去访问页表,找到对应的页表项,然后更新 TLB 。当异常处理程序执行完后,会再次执行原来的指令,这时候会命中 TLB 。可想而知, TLB 的命中率直接影响了操作系统运行的效率。


    TLB.png


    总结


    先说说为什么写了这么一篇文章。


    最近在读 《深入理解 Android 内核设计思想》Binder 相关章节的时候,发现对 mmap 没有一个深度认识的话,就很难理解 Binder 只复制一次的底层逻辑。而如果对虚拟内存机制又没有一个很好的理解的话,也很难去理解 mmap 的实现原理。算是一环扣一环,倒逼学习了一波。


    其实编程领域的很多问题,归根到底都是针对计算机操作系统的特性,做出的解决方案和妥协。打好操作系统的扎实基础,对学习任何编程相关的知识,都是大有裨益的。但另一方面,操作系统的知识也多且杂,我也不敢保证我这篇文章没有任何错误。如果你对上面的内容有不同意见,欢迎评论区和我探讨。


    最后,一张图总结虚拟内存的工作机制。



    虚拟内存.png

    收起阅读 »