注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

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

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

事件从起源

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

Activity.java

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

getWindow()实际返回的是PhoneWindow

PhoneWindow.java

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

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

DecorView.java

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

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

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

View的事件分发

View.java--伪代码

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

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

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

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

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

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

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

ViewGroup的事件分发

ViewGroup.java--伪代码

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

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

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

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

return handled;
}


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

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

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

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

View.java

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

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

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

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

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

收起阅读 »

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



收起阅读 »

高级线程应用之栅栏、信号量、调度组以及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开发进阶 - 自旋锁、互斥锁以及读写锁!(二)

    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


    收起阅读 »

    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

    收起阅读 »

    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

    收起阅读 »

    【开源项目】Compose仿豆瓣榜单客户端,了解一下~

    前言 Compose正式发布也有一段时间了,感觉要上手还是得实战一波。 所以借着空闲时间,参照豆瓣榜单页面的设计,开发了几个Compose版的豆瓣榜单页面 UI效果还是挺好看的,有兴趣的同学可以点个Star:Compose仿豆瓣榜单客户端 效果图 首先看...
    继续阅读 »

    前言


    Compose正式发布也有一段时间了,感觉要上手还是得实战一波。
    所以借着空闲时间,参照豆瓣榜单页面的设计,开发了几个Compose版的豆瓣榜单页面
    UI效果还是挺好看的,有兴趣的同学可以点个Star:Compose仿豆瓣榜单客户端


    效果图


    首先看下最终的效果图


    douban_compress.gif


    特性


    在项目中主要用到了以下几个特性,以美化UI及体验



    1. 支持设置沉浸式状态栏及状态栏颜色

    2. 支持水平方向滚动,竖直方向滚动等多种UI效果

    3. 支持给Image设置渐变滤镜,以美化显示效果

    4. 支持标题与列表页联动

    5. 通过Paging支持了分页加载


    主要实现


    具体源码可以直接查看,这里主要介绍一些主要功能的实现


    沉浸式状态栏设置


    状态栏主要是通过accompanist-insetsaccompanist-systemuicontroller库设置的
    accompanist上提供了一系列常用的,如状态栏,权限,FlowLayout,ViewPagerCompose
    如果有时你发现基础库里没有相应的内容,可以去这里查找下


    设置状态栏主要分为以下几步



    1. 设置沉浸时状态栏

    2. 获取状态栏高度

    3. 设置状态栏颜色


    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    // 1. 设置状态栏沉浸式
    WindowCompat.setDecorFitsSystemWindows(window, false)

    setContent {
    BD_ToolTheme {
    // 加入ProvideWindowInsets
    ProvideWindowInsets {
    // 2. 设置状态栏颜色
    rememberSystemUiController().setStatusBarColor(
    Color.Transparent, darkIcons = MaterialTheme.colors.isLight)
    Column {
    // 3. 获取状态栏高度并设置占位
    Spacer(modifier = Modifier
    .statusBarsHeight()
    .fillMaxWidth())
    Text(text = "首页\r\n首页1\r\n首页2\r\n首页3")
    }
    }
    }
    }
    }

    通过以上方法,就可以比较简单的实现沉浸状态栏的设置


    Image设置渐变滤镜


    豆瓣榜单页面都给Image设置了渐变滤镜,以美化UI效果
    其实实现起来也比较简单,给Image前添加一层渐变的蒙层即可


    @Composable
    fun TopRankItem(item: HomeTopRank) {
    Box(
    modifier = Modifier
    .size(180.dp, 220.dp)
    .padding(8.dp)
    .clip(RoundedCornerShape(10.dp))
    ) {
    // 1. 图片
    Image(
    painter = rememberCoilPainter(request = item.imgUrl),
    contentDescription = null,
    contentScale = ContentScale.Crop,
    modifier = Modifier.fillMaxSize()
    )
    Column(
    modifier = Modifier
    .fillMaxSize()
    // 渐变滤镜
    .background(
    brush = Brush.linearGradient(
    colors = listOf(Color(item.startColor), Color(item.endColor)),
    start = Offset(0f, Float.POSITIVE_INFINITY),
    end = Offset(Float.POSITIVE_INFINITY, 0f)
    )
    )
    .padding(8.dp)

    ) {
    //内容
    }
    }
    }

    如上所示,使用Box布局,给前景设置一个从左下到右上渐变的背景即可


    标题与列表联动


    具体效果可见上面的动图,即在列表滚动时标题会有一个渐现渐隐效果
    这个效果其实我们在Android View体系中也很常见,主要思路也很简单:



    1. 监听列表滚动,获取列表滚动offset

    2. 根据列表滚动offset设置Header效果,如背景或者高度变化等


    @Composable
    fun RankScreen(viewModel: RankViewModel = RankViewModel()) {
    val scrollState = rememberLazyListState()
    Box {
    // 1. 监听列表
    LazyColumn(state = scrollState) {
    //列表内容
    }
    RankHeader(scrollState)
    }
    }

    @Composable
    fun RankHeader(scrollState: LazyListState) {
    val target = LocalDensity.current.run {
    200.dp.toPx()
    }
    // 2. 根据列表偏移量计算比例
    val scrollPercent: Float = if (scrollState.firstVisibleItemIndex > 0) {
    1f
    } else {
    scrollState.firstVisibleItemScrollOffset / target
    }
    val activity = LocalContext.current as Activity
    val backgroundColor = Color(0xFF7F6351)
    Column() {
    Spacer(
    modifier = Modifier
    .fillMaxWidth()
    .statusBarsHeight()
    // 3. 根据比例设置Header的alpha,以实现渐变效果
    .alpha(scrollPercent)
    .background(backgroundColor)
    )
    //....
    }
    }

    如上所示,主要有三步:



    1. 监听列表

    2. 根据列表偏移量计算比例

    3. 根据比例设置Headeralpha,以实现渐变效果


    利用Paging实现分页


    目前Pagin3已经支持了Compose,我们可以利用Paging轻松实现分页效果
    主要分为以下几步:



    1. ViewModel中设置数据源

    2. 在页面中监听Paging数据

    3. 根据加载状态设置加载更多footr状态


    //1. 设置数据源
    class RankViewModel : ViewModel() {
    val rankItems: Flow<PagingData<RankDetail>> =
    Pager(PagingConfig(pageSize = 10, prefetchDistance = 1)) {
    MovieSource()
    }.flow
    }

    @Composable
    fun RankScreen(viewModel: RankViewModel = RankViewModel()) {
    val lazyMovieItems = viewModel.rankItems.collectAsLazyPagingItems()
    Box {
    LazyColumn(state = scrollState) {
    // 2. 在页面中监听paging
    items(lazyMovieItems) {
    it?.let {
    RankListItem(it)
    }
    }
    // 3. 根据paging状态设置加载更多footer状态等
    lazyMovieItems.apply {
    when (loadState.append) {
    is LoadState.Loading -> {
    item { LoadingItem() }
    }
    }
    }
    }
    }
    }

    通过以上步骤,就可以比较简单方便地实现分页了


    总结


    项目地址


    ComposeDouban
    开源不易,如果项目对你有所帮助,欢迎点赞,Star,收藏~

    收起阅读 »

    如何优雅的在业务中使用设计模式(代码如诗)

    前言 有段时间没写文章了,最近沉迷Rust,无法自拔,锈儿有毒;这真是门非常有趣的语言,很多地方的设计,真的是满足了我所有的向往。 当然,这也不是一门简单的语言,提出所有权的概念,引入了极多符号:mut、&mut、ref mut、&...
    继续阅读 »

    前言



    有段时间没写文章了,最近沉迷Rust,无法自拔,锈儿有毒;这真是门非常有趣的语言,很多地方的设计,真的是满足了我所有的向往。


    当然,这也不是一门简单的语言,提出所有权的概念,引入了极多符号:mut、&mut、ref mut、&、*、as_mut、as_ref。。。让人头秃。。。


    之前看到过一句话,觉得很不错:学习Rust并不会给你带来智商上的优越感,但或许会让你重新爱上编程。



    大家如果阅读过一些开源框架的源码,可能会发现其中数不尽的抽象类,设计模式拈手而来,在功能框架中,可以使用设计模式随心所欲的解耦;在实际的复杂业务中,当然也可以应用合适的设计模式。


    这篇文章,我会结合较为常见的实际业务场景,探讨如何使用合适的设计模式将业务解耦



    • 此处的应用绝不是生搬硬套,是我经过深思熟虑,并将较为复杂的业务进行全面重构后,得出的一套行之有效的思路历程

    • 任何一个设计模式都是一个伟大的经验及其思想总结,千人千面,如果对文章中内容,有不同的意见,希望你能在评论中提出,我们共同探讨,共同进步


    本文章是一篇弱代码类型文章,我会画大量的图片向大家展示,引用设计模式后,会对原有的业务流程,产生什么样的影响。


    前置知识


    这里,需要了解下基础知识,什么是责任链模式和策略模式


    责任链模式,在很多开源框架中都是有所应用,你如果听到啥啥拦截器,基本就是责任链模式,责任链模式的思想很简单,但是有很多种实现方式



    • 最简单的链表实现就和OkHttp的拦截器实现大相径庭

    • OkHttp的拦截器实现和Dio拦截器实现结构相同,但遍历方式不一样

    • 很多骚操作:我喜欢OkHttp的实现方式,喜欢dio的Api设计,结尾会给出一个结合这俩者思想的通用拦截器


    策略模式,或是天生适合业务,同一模块不同类型业务,如果行为相同,或许就可以考虑使用策略模式去解耦了


    责任链模式


    这边用Dart写一个简单的拦截器,dart和java非常像



    • 为了减少语言差异,我就不使用箭头语法了

    • 下划线表示私有


    用啥语言不重要,这边只是用代码简单演示下思想


    此处实现就用链表了;如果,使用数组的形式,需要多写很多逻辑,数组的优化写法在结尾给出,此处暂且不表


    结构



    • 责任链的结构,通常有俩种结构

      • 链表结构:链表构建责任链,十分便捷的就能和下一节点建立联系

      • 数组结构:数组,用通用的List即可,方便增删,不固定长度(别费劲的用固定长度Array了,例如:int[]、String[])



    责任链结构



    • 实现一个链表实体很简单


    abstract class InterceptChain<T> {
    InterceptChain? next;

    void intercept(T data) {
    next?.intercept(data);
    }
    }

    实现



    • 拦截器实现


    /// 该拦截器以最简单的链表实现
    abstract class InterceptChain<T> {
    InterceptChain? next;

    void intercept(T data) {
    next?.intercept(data);
    }
    }

    class InterceptChainHandler<T> {
    InterceptChain? _interceptFirst;

    void add(InterceptChain interceptChain) {
    if (_interceptFirst == null) {
    _interceptFirst = interceptChain;
    return;
    }

    var node = _interceptFirst!;
    while (true) {
    if (node.next == null) {
    node.next = interceptChain;
    break;
    }
    node = node.next!;
    }
    }

    void intercept(T data) {
    _interceptFirst?.intercept(data);
    }
    }


    • 使用

      • 调整add顺序,就调整了对应逻辑的节点,在整个责任链中的顺序

      • 去掉intercept重写方法中的super.intercept(data),就能实现拦截后续节点逻辑



    void main() {
    var intercepts = InterceptChainHandler<String>();
    intercepts.add(OneIntercept());
    intercepts.add(TwoIntercept());
    intercepts.intercept("测试拦截器");
    }

    class OneIntercept extends InterceptChain<String> {
    @override
    void intercept(String data) {
    data = "$data:OneIntercept";
    print(data);
    super.intercept(data);
    }
    }

    class TwoIntercept extends InterceptChain<String> {
    @override
    void intercept(String data) {
    data = "$data:TwoIntercept";
    print(data);
    super.intercept(data);
    }
    }


    • 打印结果


    测试拦截器:OneIntercept
    测试拦截器:OneIntercept:TwoIntercept

    策略模式


    结构



    • 策略模式最重要的:应该就是对抽象类的设计,对行为的抽象


    策略模式应用


    实现



    • 定义抽象类,抽象行为


    /// 结合适配器模式的接口适配:抽象必须实现行为,和可选实现行为
    abstract class BusinessAction {
    ///创建相应资源:该行为必须实现
    void create();

    ///可选实现
    void dealIO() {}

    ///可选实现
    void dealNet() {}

    ///可选实现
    void dealSystem() {}

    ///释放资源:该行为必须实现
    void dispose();
    }


    • 实现策略类


    //Net策略
    class NetStrategy extends BusinessAction {
    @override
    void create() {
    print("创建Net资源");
    }

    @override
    void dealNet() {
    print("处理Net逻辑");
    }

    @override
    void dispose() {
    print("释放Net资源");
    }
    }

    ///IO策略
    class IOStrategy extends BusinessAction {
    @override
    void create() {
    print("创建IO资源");
    }

    @override
    void dealIO() {
    print("处理IO逻辑");
    }

    @override
    void dispose() {
    print("释放IO资源");
    }
    }


    • 使用


    void main() {
    var type = 1;
    BusinessAction strategy;

    //不同业务使用不同策略
    if (type == 0) {
    strategy = NetStrategy();
    } else {
    strategy = IOStrategy();
    }

    //开始创建资源
    strategy.create();
    //......... 省略N多逻辑(其中某些场景,会有用到Net业务,和上面type是关联的)
    //IO业务:开始处理业务
    strategy.dealIO();
    //......... 省略N多逻辑
    //释放资源
    strategy.dispose();
    }


    • 结果


    创建IO资源
    处理IO逻辑
    释放IO资源

    适合的业务场景


    这边举一些适合上述设计模式的业务场景,这些场景是真实存在的!


    这些真实的业务,使用设计模式解耦和纯靠if else怼,完全是俩种体验!


    代码如诗,这并不是一句玩笑话。


    连环弹窗业务


    业务描述


    连环弹窗夺命call来袭。。。



    • A弹窗弹出:有确定和取消按钮

      • 确定按钮:B弹窗弹出(有查看详情和取消按钮)

        • 查看详情按钮:C弹窗弹出(有同意和拒绝按钮)

          • 同意按钮:D弹窗弹出(有查看和下一步按钮)

            • 查看按钮:E弹窗弹出(只有下一步按钮)

              • 下一步按钮:F弹窗弹出(结束)


            • 下一步按钮:F弹窗弹出(结束)


          • 拒绝按钮:流程结束


        • 取消按钮:流程结束


      • 取消按钮:流程结束



    好家伙,套娃真是无所不在,真不是我们代码套娃,实在是业务套娃,手动滑稽.png


    img



    • 图示弹窗业务


    连环弹窗业务1


    直接开搞


    看到这个业务,大家会去怎么做呢?



    • 有人可能会想,这么简单的业务还需要想吗?直接写啊!

      • A:在确定回调里面,跳转B弹窗

      • B:查看详情按钮跳转C弹窗

      • 。。。


    • 好一通套后,终于写完了



    产品来了,加需求


    B和C弹窗之间要加个预览G弹窗,点击B的查看详情按钮,跳转预览G弹窗;预览G弹窗只有一个确定按钮,点击后跳转C弹窗



    img



    • 你心里可能要想了,这特么不是坑爹?

      • 业务本来就超吉尔套,我B弹窗里面写的跳转代码要改,传参要改,而且还要加弹窗!


    • 先要去找产品撕比,撕完后

      • 然后继续在屎山上,小心翼翼的再拉了坨shit

      • 这座克苏鲁山初成规模



    连环弹窗业务2



    产品又来了,第一稿需求不合理,需要调整需求


    交换C和D弹窗位置,D弹窗点击下一步的时候,需要加一个校验请求,通过后才能跳转到C弹窗



    img



    • 你眉头一皱,发现事情没有表面这么简单

      • 由于初期图简单,几乎都写在一个文件里,眼花缭乱弹窗回调太多,而且弹窗样式也不一样

      • 现在改整个流程,导致你整个人脑子嗡嗡响


    • 心中怒气翻涌,找到产品说


    img



    • 回来,坐在椅子上,心里想:

      • 老夫写的代码天衣无缝,这什么几把需求

      • 可恶,这次测试,起码要给我多提十几个BUG



    image-20210822215435299



    • 克苏鲁山开始狰狞


    连环弹窗业务3



    产品飘来,加改需求:如此,如此,,,这般,这般,,,




    • 你....


    img



    产品:改下,,,然后,扔给你几十页的PRD



    你看了看这改了几十版的克苏鲁山,这几十个弹窗逻辑居然都写在一个文件里,快一万行的代码。。。



    • 心里不禁想:

      • 本帅比写的代码果然牛批,或许这就是艺术!艺术总是曲高和寡,难被人理解!而我的代码更牛批,连我自己都看不懂了!

      • 这代码行数!这代码结构!不得拍个照留念下,传给以后的孩子当传家宝供着!


    • 心里不禁嘚瑟:

      • 这块业务,除了我,还有谁敢动,成为头儿的心腹,指日可待!



    16c3-ikhvemy5945899



    • 但,转念深思后:事了拂衣去,深藏功与名


    img


    重构



    随着业务的逐渐复杂,最初的设计缺点会逐渐暴露;重构有缺陷的代码流程,变得势在必行,这会极大的降低维护成本



    如果心中对责任链模式有一些概念的话,会发现上面的业务,极其适合责任链模式!


    对上面的业务进行分析,可以明确一些事



    • 这个业务是一个链式的,有着明确的方向性:单向,从头到尾指向

    • 业务拆分开,可以将一个弹窗作为单颗粒度,一个弹窗作为节点

    • 上级的业务节点可以对下级节点拦截(点击取消,拒绝按钮,不再进行后续业务)


    重构上面的代码,只要明确思想和流程就行了



    第一稿业务




    • 业务流程


    连环弹窗业务1



    • 责任链


    责任链业务1



    • 代码:简写


    void main() {
    var intercepts = InterceptChainHandler<String>();
    intercepts.add(AIntercept());
    intercepts.add(BIntercept());
    intercepts.add(CIntercept());
    intercepts.add(DIntercept());
    intercepts.add(EIntercept());
    intercepts.add(FIntercept());
    intercepts.intercept("测试拦截器");
    }


    第二稿业务




    • 业务流程


    连环弹窗业务2



    • 责任链


    责任链业务2



    • 代码:简写


    void main() {
    var intercepts = InterceptChainHandler<String>();
    intercepts.add(AIntercept());
    intercepts.add(BIntercept());
    intercepts.add(GIntercept());
    intercepts.add(CIntercept());
    intercepts.add(DIntercept());
    intercepts.add(EIntercept());
    intercepts.add(FIntercept());
    intercepts.intercept("测试拦截器");
    }


    第三稿业务




    • 业务流程


    连环弹窗业务3



    • 责任链


    责任链业务3



    • 代码:简写


    void main() {
    var intercepts = InterceptChainHandler<String>();
    intercepts.add(AIntercept());
    intercepts.add(BIntercept());
    intercepts.add(GIntercept());
    intercepts.add(DIntercept());
    intercepts.add(CIntercept());
    intercepts.add(EIntercept());
    intercepts.add(FIntercept());
    intercepts.intercept("测试拦截器");
    }


    总结



    经过责任链模式重构后,业务节点被明确的区分开,整个流程从代码上看,都相当的清楚,维护将变的异常轻松;或许,此时能感受到一些,编程的乐趣了


    img


    花样弹窗业务


    业务描述


    来描述一个新的业务:这个业务场景真实存在某办公软件



    • 进入APP首页后,和后台建立一个长连接

    • 后台某些工单处理后,会通知APP处理,此时app会弹出处理工单的弹窗(app顶部)

    • 弹窗类型很多:工单处理弹窗,流程审批弹窗,邀请类型弹窗,查看工单详情弹窗,提交信息弹窗。。。

    • 弹窗弹出类型,是根据后台给的Type进行判断:从而弹出不同类型弹窗、点击其按钮,跳转不同业务,传递不同参数。


    花样弹窗业务


    分析



    确定设计



    这个业务,是一种渐变性的引导你搭建克苏鲁代码山



    • 在前期开发的时候,一般只有俩三种类型弹窗,前期十分好做;根本不用考虑如何设计,抬手一行代码,反手一行代码,就能搞定

    • 但是后来整个业务会渐渐的鬼畜,不同类型会慢慢加到几十种之多!!!


    首先这个业务,使用责任链模式,肯定是不合适的,因为弹窗之间的耦合性很低,并没有什么明确的上下游关系


    但是,这个业务使用策略模式非常的合适!



    • type明确:不同类型弹出不同弹窗,按钮执行不同逻辑

    • 抽象行为明确:一个按钮就是一种行为,不同行为的实现逻辑大相径庭



    抽象行为



    多样弹窗的行为抽象,对应其按钮就行了


    确定、取消、同意、拒绝、查看详情、我知道了、提交


    直接画图来表示吧


    花样弹窗业务-抽象行为


    实现


    来看下简要的代码实现,代码不重要,重要的是思想,这边简要的看下代码实现流程



    • 抽象基类


    /// 默认实现抛异常,可提醒未实现方法被误用
    abstract class DialogAction {
    ///确定
    void onConfirm() {
    throw 'DialogAction:not implement onConfirm()';
    }

    ///取消
    void onCancel() {
    throw 'DialogAction:not implement onCancel()';
    }

    ///同意
    void onAgree() {
    throw 'DialogAction:not implement onAgree()';
    }

    ///拒绝
    void onRefuse() {
    throw 'DialogAction:not implement onRefuse()';
    }

    ///查看详情
    void onDetail() {
    throw 'DialogAction:not implement onDetail()';
    }

    ///我知道了
    void onKnow() {
    throw 'DialogAction:not implement onKnow()';
    }

    ///提交
    void onSubmit() {
    throw 'DialogAction:not implement onSubmit()';
    }
    }


    • 实现逻辑类


    class OneStrategy extends DialogAction {
    @override
    void onConfirm() {
    print("确定");
    }

    @override
    void onCancel() {
    print("取消");
    }
    }

    class TwoStrategy extends DialogAction{
    @override
    void onAgree() {
    print("同意");
    }

    @override
    void onRefuse() {
    print("拒绝");
    }
    }

    //........省略其他实现


    • 使用


    void main() {
    //根据接口获取
    var type = 1;
    DialogAction strategy;
    switch (type) {
    case 0:
    strategy = DefaultStrategy();
    break;
    case 1:
    strategy = OneStrategy();
    break;
    case 2:
    strategy = TwoStrategy();
    break;
    case 3:
    strategy = ThreeStrategy();
    break;
    case 4:
    strategy = FourStrategy();
    break;
    case 5:
    strategy = FiveStrategy();
    break;
    default:
    strategy = DefaultStrategy();
    break;
    }

    //聚合弹窗按钮触发事件
    BusinessDialog(
    //确定按钮
    onConfirm: () {
    strategy.onConfirm();
    },
    //取消按钮
    onCancel: () {
    strategy.onCancel();
    },
    //同意按钮
    onAgree: () {
    strategy.onAgree();
    },
    //拒绝按钮
    onRefuse: () {
    strategy.onRefuse();
    },
    //查看详情按钮
    onDetail: () {
    strategy.onDetail();
    },
    //我知道了按钮
    onKnow: () {
    strategy.onKnow();
    },
    //提交按钮
    onSubmit: () {
    strategy.onSubmit();
    },
    );
    }


    • 图示


    花样弹窗业务-业务流程


    一个复杂业务场景的演变


    我们看下,一个简单的提交业务流,怎么逐渐变的狰狞


    我会逐渐给出一个合适的解决方案,如果大家有更好的想法,务必在评论区告诉鄙人


    业务描述:我们的车子因不可抗原因坏了,要去维修厂修车,工作人员开始登记这个损坏车辆。。。


    业务的演变



    第一稿


    初始业务



    登记一个维修车辆的流程,实际上还是满麻烦的



    • 登记一个新车,需要将车辆详细信息登记清楚:车牌、车架、车型号、车辆类型、进出场时间、油量、里程。。。

    • 还需要登记一下用户信息:姓名、手机号、是否隶属公司。。。

    • 登记车损程度:车顶、车底、方向盘、玻璃、离合器、刹车。。。

    • 车内物品:车座皮套、工具。。。

    • 以及其他我没想到的。。。

    • 最后:提交所有登记好的信息


    第一稿,业务流程十分清晰,细节复杂,但是做起来不难


    车辆登记-第一稿



    第二稿(实际是多稿聚合):增加下述几个流程


    外部登记:外部登记了一个维修车辆部分信息(后台,微信小程序,H5等等),需要在app上完善信息,提交接口不同(必带车牌号)


    快捷洗车:洗车业务极其常见,快捷生成对应信息,提交接口不同


    预约订单登记:预约好了车辆一部分一些信息,可快捷登记,提交接口不同(必带车牌号)



    因为登记维修车辆流程,登记车辆信息流程极其细致繁琐,我们决定复用登记新车模块



    • 因为此处逻辑大多涉及开头和结尾,中间登记车辆信息操作几乎未改动,复用想法是可行的

    • 如果增加车辆登记项,新的三个流程也必须提交这些信息;所以,复用势在必行


    因为这一稿需求,业务也变得愈加复杂


    车辆登记-第二稿



    第三稿


    现在要针对不同的车辆类型,做不同的处理;车类型分:个人车,集团车


    不同类型的登记,在提交的时候,需要校验不同的信息;校验不通过,需要提示用户,并且不能进行提交流程


    提交后,需要处理下通用业务,然后跳转到某个页面



    第三稿的描述不多,但是,大大的增加了复杂度



    • 尤其是不同类型校验过程还不同,还能中断后续提交流程

    • 提交流程后,还需要跳转通用页面


    车辆登记-第三稿


    开发探讨


    第一稿



    • 业务流程


    车辆登记-第一稿



    • 开发


    正常流程开发、、、


    第二稿



    • 业务流程


    车辆登记-第二稿



    • 思考


    对于第二稿业务,可以好好考虑下,怎么去设计?


    开头和结尾需要单独写判断,去处理不同流程的业务,这至少要写俩个大的判断模块,接受数据的入口模块可能还要写判断


    这样就非常适合策略模式去做了


    开头根据执行的流程,选择相应的策略对象,后续将逻辑块替换抽象的策略方法就OK了,大致流程如下


    车辆登记-第二稿(策略模式)


    第三稿



    业务流程



    车辆登记-第三稿



    探讨




    • 第三稿的需求,实际上,已经比较复杂了



      • 整个流程中掺杂着不同业务流程处理,不同流程逻辑又拥有阻断下游机制(绿色模块)

      • 下游逻辑又会合流(结尾)的多种变换


    • 在这一稿的需求



      • 使用策略模式肯定是可以的

      • 阻断那块(绿色模块)需要单独处理下:抽象方法应该拥有返回值,外层根据返回值,判断是否进行后续流程

      • 但!这!也太不优雅了!


    • 思考上面业务一些特性



      • 拦截下游机制

      • 上游到下游、方向明确

      • 随时可能插入新的业务流程。。。



    可以用责任链模式!但,需要做一些小改动!这地方,我们可以将频繁变动的模块用责任链模式全都隔离出来



    • 看下,使用责任链模式改造后流程图


    车辆登记-第三稿(责任链模式)



    浏览上述流程图可发现,本来是极度杂乱糅合的业务,可以被设计相对更加平行的结构




    • 对于上述流程,可以进一步分析,并进一步简化:对整体业务分析,我们需要去关注其变或不变的部分



      • 不变:整体业务变动很小的是,登记信息流程(主体逻辑这块),此处的相关变动是很小的,对所有流程也是共用的部分

      • 变:可以发现,开头和结尾是变动更加频繁的部分,我们可以对此处逻辑进行整体的抽象


    • 抽象多变的开头和结尾



    车辆登记-第三稿(责任链模式——简化)



    • 所以我们抽象拦截类,可以做一些调整


    abstract class InterceptChainTwice<T> {
    InterceptChainTwice? next;

    void onInit(T data) {
    next?.onInit(data);
    }

    void onSubmit(T data) {
    next?.onSubmit(data);
    }
    }


    来看下简要的代码实现,代码不重要,主要看看实现流程和思想




    • 抽象拦截器


    abstract class InterceptChainTwice<T> {
    InterceptChainTwice? next;

    void onInit(T data) {
    next?.onInit(data);
    }

    void onSubmit(T data) {
    next?.onSubmit(data);
    }
    }

    class InterceptChainTwiceHandler<T> {
    InterceptChainTwice? _interceptFirst;

    void add(InterceptChainTwice interceptChain) {
    if (_interceptFirst == null) {
    _interceptFirst = interceptChain;
    return;
    }

    var node = _interceptFirst!;
    while (true) {
    if (node.next == null) {
    node.next = interceptChain;
    break;
    }
    node = node.next!;
    }
    }

    void onInit(T data) {
    _interceptFirst?.onInit(data);
    }

    void onSubmit(T data) {
    _interceptFirst?.onSubmit(data);
    }
    }


    • 实现拦截器


    /// 开头通用拦截器
    class CommonIntercept extends InterceptChainTwice<String> {
    @override
    void onInit(String data) {
    //如果有车牌,请求接口,获取数据
    //.................
    //填充页面
    super.onInit(data);
    }
    }

    /// 登记新车拦截器
    class RegisterNewIntercept extends InterceptChainTwice<String> {
    @override
    void onInit(String data) {
    //处理开头针对登记新车的单独逻辑
    super.onInit(data);
    }

    @override
    void onSubmit(String data) {
    var isPass = false;
    //如果校验不过,拦截下游逻辑
    if (!isPass) {
    return;
    }
    // ......
    super.onSubmit(data);
    }
    }

    /// 省略其他实现


    • 使用


    void main() {
    var type = 0;
    var intercepts = InterceptChainTwiceHandler();

    intercepts.add(CommonIntercept());
    intercepts.add(CarTypeDealIntercept());
    if (type == 0) {
    //登记新车
    intercepts.add(CommonIntercept());
    } else if (type == 1) {
    //外部登记
    intercepts.add(OutRegisterIntercept());
    } else if (type == 2) {
    //快捷洗车
    intercepts.add(FastWashIntercept());
    } else {
    //预约订单登记
    intercepts.add(OrderRegisterIntercept());
    }
    intercepts.add(TailIntercept());

    //业务开始
    intercepts.onInit("传入数据源");

    //开始处理N多逻辑
    //............................................................
    //经历了N多逻辑

    //提交按钮触发事件
    SubmitBtn(
    //提交按钮
    onSubmit: () {
    intercepts.onSubmit("传入提交数据");
    },
    );
    }

    总结


    关于代码部分,关键的代码,我都写出来,用心看看,肯定能明白我写的意思


    也不用找我要完整代码了,这些业务demo代码写完后,就删了


    本栏目这个业务,实际上是非常常见的的一个业务,一个提交流程与很多其它的流程耦合,整个业务就会慢慢的变的鬼畜,充满各种判断,很容易让人陷入泥泞,或许,此时可以对已有业务进行思考,如何进行合理的优化


    该业务的演变历程,和开发改造是本人的一次思路历程,如大家有更好的思路,还请不吝赐教。


    通用拦截器


    我结合OkHttp的思想和Dio的API,封装了俩个通用拦截器,这边贴下代码,如果哪里有什么不足,请及时告知本人


    说明下:这是Dart版本的


    抽象单方法


    ///一层通用拦截器,T的类型必须一致
    abstract class InterceptSingle<T> {
    void intercept(T data, SingleHandler handler) => handler.next(data);
    }

    ///添加拦截器,触发拦截器方法入口
    class InterceptSingleHandler<T> {
    _InterceptSingleHandler _handler = _InterceptSingleHandler(
    index: 0,
    intercepts: [],
    );

    void add(InterceptSingle intercept) {
    //一种类型的拦截器只能添加一次
    for (var item in _handler.intercepts) {
    if (item.runtimeType == intercept.runtimeType) {
    return;
    }
    }

    _handler.intercepts.add(intercept);
    }

    void delete(InterceptSingle intercept) {
    _handler.intercepts.remove(intercept);
    }

    void intercept(T data) {
    _handler.next(data);
    }
    }

    ///------------实现不同处理器 参照 dio api设计 和 OkHttp实现思想---------------
    abstract class SingleHandler {
    next(dynamic data);
    }

    ///实现init处理器
    class _InterceptSingleHandler extends SingleHandler {
    List<InterceptSingle> intercepts;

    int index;

    _InterceptSingleHandler({
    required this.index,
    required this.intercepts,
    });

    @override
    next(dynamic data) {
    if (index >= intercepts.length) {
    return;
    }

    var intercept = intercepts[index];
    var handler =
    _InterceptSingleHandler(index: index + 1, intercepts: intercepts);

    intercept.intercept(data, handler);
    }
    }

    抽象双方法


    ///俩层通用拦截器,T的类型必须一致
    abstract class InterceptTwice<T> {
    void onInit(T data, TwiceHandler handler) => handler.next(data);

    void onSubmit(T data, TwiceHandler handler) => handler.next(data);
    }

    ///添加拦截器,触发拦截器方法入口
    class InterceptTwiceHandler<T> {
    _TwiceInitHandler _init = _TwiceInitHandler(index: 0, intercepts: []);
    _TwiceSubmitHandler _submit = _TwiceSubmitHandler(index: 0, intercepts: []);

    void add(InterceptTwice intercept) {
    //一种类型的拦截器只能添加一次
    for (var item in _init.intercepts) {
    if (item.runtimeType == intercept.runtimeType) {
    return;
    }
    }

    _init.intercepts.add(intercept);
    _submit.intercepts.add(intercept);
    }

    void delete(InterceptTwice intercept) {
    _init.intercepts.remove(intercept);
    _submit.intercepts.remove(intercept);
    }

    void onInit(T data) {
    _init.next(data);
    }

    void onSubmit(T data) {
    _submit.next(data);
    }
    }

    ///------------实现不同处理器 参照 dio api设计 和 OkHttp实现思想---------------
    abstract class TwiceHandler {
    next(dynamic data);
    }

    ///实现init处理器
    class _TwiceInitHandler extends TwiceHandler {
    List<InterceptTwice> intercepts;

    int index;

    _TwiceInitHandler({
    required this.index,
    required this.intercepts,
    });

    @override
    next(dynamic data) {
    if (index >= intercepts.length) {
    return;
    }

    var intercept = intercepts[index];
    var handler = _TwiceInitHandler(index: index + 1, intercepts: intercepts);

    intercept.onInit(data, handler);
    }
    }

    ///实现submit处理器
    class _TwiceSubmitHandler extends TwiceHandler {
    List<InterceptTwice> intercepts;

    int index;

    _TwiceSubmitHandler({
    required this.index,
    required this.intercepts,
    });

    @override
    next(dynamic data) {
    if (index >= intercepts.length) {
    return;
    }

    var intercept = intercepts[index];
    var handler = _TwiceSubmitHandler(index: index + 1, intercepts: intercepts);

    intercept.onSubmit(data, handler);
    }
    }

    最后


    第一次,写这种结合业务的文章


    如有收获,还请点个赞,让我感受一下,各位是否读有所获~~


    img



    感谢阅读,下次再会~~


    img

    收起阅读 »

    iOS逆向必学-logos语法

    一、概述Logos语法其实是CydiaSubstruct框架提供的一组宏定义。便于开发者使用宏进行HOOK操作。语法简单,功能强大且稳定,它是跨平台的。[logos] (http://iphonedevwiki.net/index.php/Logos)二、lo...
    继续阅读 »

    一、概述

    Logos语法其实是CydiaSubstruct框架提供的一组宏定义。便于开发者使用宏进行HOOK操作。语法简单,功能强大且稳定,它是跨平台的。[logos] (http://iphonedevwiki.net/index.php/Logos)

    二、logos语法

    logos语法分为3类。

    2.1、Block level

    这一类型的指令会开辟一个代码块,以%end结束。

    %group

    用来将代码分组。开发中hook代码会很多,这样方便管理Logos代码。所有的group都必须初始化,否则编译报错。


    #import <UIKit/UIKit.h>

    %group group1

    %hook RichTextView

    - (_Bool)setPrefixContent:(id)arg1 TargetContent:(NSString *)arg2 TargetParserString:(id)arg3 SuffixContent:(id)arg4 {
    //hook后要处理的方式1
    return %orig;
    }

    %end

    %end


    %group group2

    %hook RichTextView

    - (_Bool)setPrefixContent:(id)arg1 TargetContent:(NSString *)arg2 TargetParserString:(id)arg3 SuffixContent:(id)arg4 {
    //hook后要处理的方式2
    return %orig;
    }

    %end

    %end

    %group group3

    %hook RichTextView

    - (_Bool)setPrefixContent:(id)arg1 TargetContent:(NSString *)arg2 TargetParserString:(id)arg3 SuffixContent:(id)arg4 {
    //hook后要处理的方式3
    return %orig;
    }

    %end

    %end

    //使用group要配合ctor
    %ctor {
    //[[UIDevice currentDevice] systemVersion].doubleValue 可以用来判断版本或其它逻辑。
    if ([[UIDevice currentDevice] systemVersion].doubleValue >= 11.0) {
    //这里group3会覆盖group1,不会执行group1逻辑。
    %init(group1)%init(group3);
    } else {
    %init(group2);
    }
    }



    • group初始化在%ctor中,需要%init初始化。
    • 所有group必须初始化,否则编译报错。
    • 在一个逻辑中同时初始化多个group,后面的会覆盖前面的。
    • 在不添加group的情况下,默认有个_ungrouped组,会自动初始化。

    Begin a hook group with the name Groupname. Groups cannot be inside another [%group](https://iphonedev.wiki/index.php/Logos#.25group "Logos") block. All ungrouped hooks are in the implicit "_ungrouped" group. The _ungrouped group is initialized for you if there are no other groups. You can use the %init directive to initialize it manually. Other groups must be initialized with the %init(Groupname) directive

    %hook

    HOOK某个类里面的某个方法。

    %hook RichTextView

    - (_Bool)setPrefixContent:(id)arg1 TargetContent:(NSString *)arg2 TargetParserString:(id)arg3 SuffixContent:(id)arg4 {
    //hook后要处理的方式1
    return %orig;
    }

    %end

    %hook后面需要跟需要hook的类名。

    %new
    为某个类添加新方法,在%hook 和 %end 中使用。

    %hook RichTextView

    %new
    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {

    }

    %end

    %subclass

    %subclass Classname: Superclass <Protocol list>

    运行时创建子类,只能包含方法或者关联属性,不能包含属性。可以通过%c创建类实例。

    #import <UIKit/UIKit.h>

    @interface MyObject

    - (void)setSomeValue:(id)value;

    @end

    %subclass MyObject : NSObject

    - (id)init {
    self = %orig;
    [self setSomeValue:@"value"];
    return self;
    }

    %new
    - (id)someValue {
    return objc_getAssociatedObject(self, @selector(someValue));
    }

    %new
    - (void)setSomeValue:(id)value {
    objc_setAssociatedObject(self, @selector(someValue), value, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }

    %end

    %property

    %property (nonatomic|assign|retain|copy|weak|strong|getter|setter) Type name;

    subclass或者hook的类添加属性。必须在 %subclass 或%hook中。

    %property(nonatomic,assign) NSInteger age;

    %end

    与其它命令配对出现。

    2.2、Top level

    TopLevel指令不放在BlockLevel中。

    %config

    %config(Key=Value);

    logos设置标记。

    Configuration Flags

    keyvaluesnotes
    generatorMobileSubstrate生成的代码使用MobileSubstrate hook
    generatorinternal生成的代码只使用OC runtime方法hook
    warningsnone忽略所有警告
    warningsdefault没有致命的警告
    warningserror使所有警告报错
    dumpyamlYAML格式转储内部解析树

    %config(generator=internal);
    %config(warnings=error);
    %config(dump=yaml);

    %hookf

    hook函数,类似fishhook
    语法

    %hookf(rtype, symbolName, args...) { … }
    • rtype:返回值。
    • symbolName:原函数地址。
    • args...:参数。
      示例
    FILE *fopen(const char *path, const char *mode);
    %hookf(FILE *, fopen, const char *path, const char *mode) {
    NSLog(@"Hey, we're hooking fopen to deny relative paths!");
    if (path[0] != '/') {
    return NULL;
    }
    return %orig;
    }

    %ctor

    构造函数,用于确定加载那个组。和%init结合用。

    %dtor

    析构,做一些收尾工作。比如应用挂起的时候。

    2.3、Function level

    这一块的指令就放在方法中

    %init

    用来初始化某个组。

    %class

    %class Class;

    %class已经废弃了,不建议使用。

    %c

    类似getClass函数,获得一个类对象。一般用于调用类方法。

    //只是为了声明编译通过
    @interface MainViewController

    + (void)HP_classMethod;

    @end


    %hook MainViewController

    %new
    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    //方式一
    // [self.class HP_classMethod];
    //方式二
    // [NSClassFromString(@"MainViewController") HP_classMethod];
    //方式三
    [%c(MainViewController) HP_classMethod];
    }

    %new
    + (void)HP_classMethod {
    NSLog(@"HP_classMethod");
    }

    %end
    • %c 中没有引号。

    %orig

    保持原有的方法实现,如果原来的方法有返回值和参数,那么可以传递参数和接收返回值。

    %hook RichTextView

    - (_Bool)setPrefixContent:(id)arg1 TargetContent:(NSString *)arg2 TargetParserString:(id)arg3 SuffixContent:(id)arg4 {
    //传递参数&接收返回值。
    BOOL result1 = %orig(arg1,arg2,arg3,arg4);
    BOOL result2 = %orig;
    return %orig;
    }

    %end

    • %orig
      可以接收返回值。
    • 可以传递参数,不传就是传递该方法的默认参数。

    %log

    能够输出日志,输出方法调用的详细信息 。

    %hook RichTextView

    - (_Bool)setPrefixContent:(id)arg1 TargetContent:(NSString *)arg2 TargetParserString:(id)arg3 SuffixContent:(id)arg4 {
    %log;
    return %orig;
    }

    %end

    输出:

     WeChat[11309:6708938] -[<RichTextView: 0x15c4c9720> setPrefixContent:(null) TargetContent:钱已经借给你了。 TargetParserString:<contentMD5>0399062cd62208dad884224feae2aa30</contentMD5><fontsize>20.287109</fontsize><fwidth>240.000000</fwidth><parser><type>1</type><range>{0, 8}</range><info><![CDATA[<style><range>{0, 8}</range><rect>{{0, 0}, {135, 21}}</rect></style>]]></info></parser> SuffixContent:(null)]

    能够输出详细的日志信息,包含类、方法、参数、以及控件信息等详细信息。

    总结

    • logos语法其实是CydiaSubstruct框架提供的一组宏定义。
    • 语法
      • %hook%end勾住某个类,在一个代码块中直接写需要勾住的方法。
      • %group%end用于分组。
        • 每一组都需要%ctor()函数构造。
        • 通过%init(组名称)进行初始化。
      • %log输出方法的详细信息(调用者、方法名、方法参数)
      • %orig调用原始方法。可以传递参数,接收返回值。
      • %c类似getClass函数,获取一个类对象。
      • %new添加某个方法。
    • .xm文件代表该文件支持OCC/C++语法。
    • 编译该文件时需要导入头文件以便编译通过。.xm文件不参与代码的执行,编译后生成的.mm文件参与代码的执行。


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


    收起阅读 »

    lookUpImpOrForward 消息慢速查找(下)

    3.1.2 search_method_list_inlineALWAYS_INLINE static method_t * search_method_list_inline(const method_list_t *mlist, SEL sel) { ...
    继续阅读 »

    3.1.2 search_method_list_inline

    ALWAYS_INLINE static method_t *
    search_method_list_inline(const method_list_t *mlist, SEL sel)
    {
    //methodlist是否已经修复
    int methodListIsFixedUp = mlist->isFixedUp();
    //是否有序
    int methodListHasExpectedSize = mlist->isExpectedSize();

    if (fastpath(methodListIsFixedUp && methodListHasExpectedSize)) {
    //二分查找
    return findMethodInSortedMethodList(sel, mlist);
    } else {
    // Linear search of unsorted method list
    //无序,循环查找
    if (auto *m = findMethodInUnsortedMethodList(sel, mlist))
    return m;
    }
    return nil;
    }
    • 首先判断有序无序。
    • 有序进入二分查找findMethodInSortedMethodList
    • 无序进入循环查找findMethodInUnsortedMethodList

    ⚠️ 那么就有个问题,排序是什么时候完成的?
    既然是method_t相关类型那就进去搜一下sort相关的关键字。发现了如下方法:

     struct SortBySELAddress :
    public std::binary_function<const struct method_t::big&,
    const struct method_t::big&, bool>
    {
    bool operator() (const struct method_t::big& lhs,
    const struct method_t::big& rhs)
    { return lhs.name < rhs.name; }
    };



    是在_read_images类加载映射的时候注册调用的。又见到了_read_images,这个方法将在后面继续研究。

    结论:类在加载实例化的时候进行的排序,是按照sel address进行排序的。

    3.1.3 findMethodInSortedMethodList 二分查找

    findMethodInSortedMethodList会根据架构最终进入各自的findMethodInSortedMethodList方法,这里以x86为例进行分析:

    ALWAYS_INLINE static method_t *
    findMethodInSortedMethodList(SEL key, const method_list_t *list, const getNameFunc &getName)
    {
    ASSERT(list);

    auto first = list->begin();
    auto base = first;
    decltype(first) probe;

    uintptr_t keyValue = (uintptr_t)key;
    //method list count
    uint32_t count;
    //count >>= 1 相当于除以2。加入count为8
    for (count = list->count; count != 0; count >>= 1) {//7 >> 1 = 3,前一个已经比较了4,这里就完美的避开了4。
    //base是为了配合少查找
    //probe中间元素,第一次 probe = 0 + 8 >> 1 = 4
    probe = base + (count >> 1);
    //sel
    uintptr_t probeValue = (uintptr_t)getName(probe);
    //与要查找的sel是否匹配
    if (keyValue == probeValue) {
    // `probe` is a match.
    // Rewind looking for the *first* occurrence of this value.
    // This is required for correct category overrides.
    //查找分类同名sel。如果匹配了就找分类中的。因为分类是在前面的,所以一直找到最开始的位置。
    while (probe > first && keyValue == (uintptr_t)getName((probe - 1))) {
    probe--;
    }
    //匹配则返回。
    return &*probe;
    }
    //没有匹配
    if (keyValue > probeValue) {//大于的情况下,在后半部分
    //没有匹配的情况下,如果sel在后半部分,这里base指向了上次查找的后面一个位置。
    base = probe + 1;//5
    //count对应减1
    count--;//7 -- 操作为了少做比较,因为已经比较过了
    }
    //在前半部分不进行额外操作。
    }

    return nil;
    }
    • 首先是一个循环比较,条件是count >>= 1,这里是对count进行减半,相当于二分。
    • base是为了少做比较,相当于是一个基线,当要继续往后查找的时候base为当前查找元素的下一个元素。
    • 当要继续往后查找的时候count进行了--操作,这一步是为了count >>= 1不包含已经比较过的范围。
    • 找到值的时候会循环继续往前查找,因为存在分类与类中方法同名的情况(分类方法放在类中同名方法前面),一直找到不同名为止。

    ⚠️根据源码可以得到以下结论:
    1.分类方法调用先找类中方法,再逐次找到分类方法,直到找到第一个。
    2.因为判断条件是当前命中元素与前一个元素比较,sel相同的情况下才继续查找,那就说明分类的方法是插入类中方法列表中的,都在对应类中方法的前面。

    • 查找完毕后会返回lookUpImpOrForward

    这里以有8个方法为类来分析查找流程,过程如下:

    比较开始值:count = 8 base = 0 probe = 4
    - 第一次:比较 probe = 4
    - keyValue > probeValue count = 3(先--,再>>1) base = 5 probe = 6
    第二次: 比较 probe = 6
    - keyValue > probeValue count = 1(先--,再>>1) base = 7 probe = 7
    第三次:比较 probe = 7
    - keyValue > probeValue count = 0(先--,再>>1) base = 8 probe = 8count == 0
    - keyValue < probeValue count = 0>>1) base = 7 probe = 7count == 0
    - keyValue < probeValue count = 1>>1) base = 5 probe = 5
    第三次:比较 probe = 5
    - keyValue > probeValue count = 0(先--,再>>1) base = 6 probe = 6count == 0
    - keyValue < probeValue count = 0>>1) base = 5 probe = 5count == 0
    - keyValue < probeValue count = 4>>1) base = 0 probe = 2
    第二次:比较 probe = 2
    - keyValue > probeValue count = 1(先--,再>>1) base = 3 probe = 3
    第三次:比较 probe = 3
    - keyValue > probeValue count = 0(先--,再>>1) base = 4 --count == 0
    - keyValue < probeValue count = 0>>1) base = 3 --count == 0
    - keyValue < probeValue count = 2>>1) base = 1 probe = 1
    第三次:比较 probe = 1
    - keyValue > probeValue count = 0(先--,再>>1) base = 0 --count == 0
    - keyValue < probeValue count = 1>>1) base = 0 probe = 0
    第四次:比较 probe = 0
    - keyValue > probeValue count = 0(先--,再>>1) base = 1 --count == 0
    - keyValue < probeValue count = 0>>1) base = 0 --count == 0

    代码模拟:

    int testFindSortedMethods(int methodCount,int findKey) {
    int base = 0;
    int probe = 0;
    int round = 0;
    printf("查找key:%d\n",findKey);
    for (int count = methodCount; count != 0; count >>= 1) {
    round++;
    probe = base + (count >> 1);
    printf("\t第%d轮 scan count :%d, base:%d,probe:%d\n",round,count,base,probe);
    if (findKey == probe) {
    printf("\tfound prode:%d\n",probe);
    return probe;
    }
    if (findKey > probe) {
    base = probe + 1;
    count--;
    }
    }
    printf("\tnot found -1\n");
    return -1;
    }

    调用:

    testFindSortedMethods(8, 0);
    testFindSortedMethods(8, 1);
    testFindSortedMethods(8, 2);
    testFindSortedMethods(8, 3);
    testFindSortedMethods(8, 4);
    testFindSortedMethods(8, 5);
    testFindSortedMethods(8, 6);
    testFindSortedMethods(8, 7);
    testFindSortedMethods(8, 8);
    testFindSortedMethods(8, 9);

    输出:

    查找key:0
    第1轮 scan count :8, base:0,probe:4
    第2轮 scan count :4, base:0,probe:2
    第3轮 scan count :2, base:0,probe:1
    第4轮 scan count :1, base:0,probe:0
    found prode:0
    查找key:1
    第1轮 scan count :8, base:0,probe:4
    第2轮 scan count :4, base:0,probe:2
    第3轮 scan count :2, base:0,probe:1
    found prode:1
    查找key:2
    第1轮 scan count :8, base:0,probe:4
    第2轮 scan count :4, base:0,probe:2
    found prode:2
    查找key:3
    第1轮 scan count :8, base:0,probe:4
    第2轮 scan count :4, base:0,probe:2
    第3轮 scan count :1, base:3,probe:3
    found prode:3
    查找key:4
    第1轮 scan count :8, base:0,probe:4
    found prode:4
    查找key:5
    第1轮 scan count :8, base:0,probe:4
    第2轮 scan count :3, base:5,probe:6
    第3轮 scan count :1, base:5,probe:5
    found prode:5
    查找key:6
    第1轮 scan count :8, base:0,probe:4
    第2轮 scan count :3, base:5,probe:6
    found prode:6
    查找key:7
    第1轮 scan count :8, base:0,probe:4
    第2轮 scan count :3, base:5,probe:6
    第3轮 scan count :1, base:7,probe:7
    found prode:7
    查找key:8
    第1轮 scan count :8, base:0,probe:4
    第2轮 scan count :3, base:5,probe:6
    第3轮 scan count :1, base:7,probe:7
    not found -1
    查找key:9
    第1轮 scan count :8, base:0,probe:4
    第2轮 scan count :3, base:5,probe:6
    第3轮 scan count :1, base:7,probe:7
    not found -1

    可以看到输出与验证的结论一致。

    流程图:




    四、案例分析慢速查找流程

    定义一个HPObject以及它的子类HPSubObject
    HPObject定义和实现如下:

    @interface HPObject : NSObject

    - (void)instanceMethod1;

    - (void)instanceMethod2;

    + (void)classMethod;

    @end

    @implementation HPObject

    - (void)instanceMethod1 {
    NSLog(@"%s",__func__);
    }

    + (void)classMethod {
    NSLog(@"%s",__func__);
    }

    @end

    HPSubObject定义和实现如下:

    @interface HPSubObject : HPObject

    - (void)subInstanceMethod;

    @end

    @implementation HPSubObject

    - (void)subInstanceMethod {
    NSLog(@"%s",__func__);
    }

    @end

    根据前面分析的方法查找逻辑测试代码:

    #pragma clang diagnostic push
    #pragma clang diagnostic ignored "-Wundeclared-selector"
    HPSubObject *subObject = [HPSubObject new];
    //对象方法 根据慢速查找分析是能找到的
    [subObject subInstanceMethod];
    [subObject instanceMethod1];
    [subObject instanceMethod2];
    #pragma clang diagnostic pop

    输出:

    -[HPSubObject subInstanceMethod]
    -[HPObject instanceMethod1]
    -[HPSubObject instanceMethod2]: unrecognized selector sent to instance 0x1006addc0
    *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[HPSubObject instanceMethod2]: unrecognized selector sent to instance 0x1006addc0'
  • subInstanceMethodinstanceMethod1符合预期。
  • instanceMethod2找不到报错unrecognized selector sent to instance,为什么报这个错误呢?
    查看调用堆栈如下:



    • 那么去源码中搜索错误信息找到以下内容:
    // Replaced by CF (throws an NSException)
    + (void)doesNotRecognizeSelector:(SEL)sel {
    _objc_fatal("+[%s %s]: unrecognized selector sent to instance %p",
    class_getName(self), sel_getName(sel), self);
    }

    // Replaced by CF (throws an NSException)
    - (void)doesNotRecognizeSelector:(SEL)sel {
    _objc_fatal("-[%s %s]: unrecognized selector sent to instance %p",
    object_getClassName(self), sel_getName(sel), self);
    }

    // Default forward handler halts the process.
    __attribute__((noreturn, cold)) void
    objc_defaultForwardHandler(id self, SEL sel)
    {
    _objc_fatal("%c[%s %s]: unrecognized selector sent to instance %p "
    "(no message forward handler is installed)",
    class_isMetaClass(object_getClass(self)) ? '+' : '-',
    object_getClassName(self), sel_getName(sel), self);
    }
    void *_objc_forward_handler = (void*)objc_defaultForwardHandler;

    那么调用的是哪个呢?断点后并没有进入。根据上面的分析imp找不到的时候会有两个选项resolveMethod_locked或者_objc_msgForward_impcache
    _objc_msgForward_impcache的汇编实现如下:

    STATIC_ENTRY __objc_msgForward_impcache

    // No stret specialization.
    b __objc_msgForward

    END_ENTRY __objc_msgForward_impcache

    内部直接调用了__objc_msgForward

    ENTRY __objc_msgForward

    adrp x17, __objc_forward_handler@PAGE
    ldr p17, [x17, __objc_forward_handler@PAGEOFF]
    TailCallFunctionPointer x17

    END_ENTRY __objc_msgForward

    可以看到__objc_msgForward的实现是调用__objc_forward_handler,也就是:

    // Default forward handler halts the process.
    __attribute__((noreturn, cold)) void
    objc_defaultForwardHandler(id self, SEL sel)
    {
    _objc_fatal("%c[%s %s]: unrecognized selector sent to instance %p "
    "(no message forward handler is installed)",
    class_isMetaClass(object_getClass(self)) ? '+' : '-',
    object_getClassName(self), sel_getName(sel), self);
    }
    void *_objc_forward_handler = (void*)objc_defaultForwardHandler;

    这也就是报错信息的原因,里面进行了格式化的错误信息打印。

    接着增加一个NSObject的分类:

    @interface NSObject (Additions)

    - (void)categoryInstanceMethod;

    @end

    @implementation NSObject (Additions)

    - (void)categoryInstanceMethod {
    NSLog(@"%s",__func__);
    }

    @end
    调用:

    #pragma clang diagnostic push
    #pragma clang diagnostic ignored "-Wundeclared-selector"
    [HPSubObject classMethod];
    [HPSubObject performSelector:@selector(categoryInstanceMethod)];
    #pragma clang diagnostic pop
    输出:

    +[HPObject classMethod]
    -[NSObject(Additions) categoryInstanceMethod]
    • classMethod类方法能找到符合预期。
    • 为什么HPSubObject能调用categoryInstanceMethod实例方法?
      这就涉及到了类的继承链,NSObject元类的父类是NSObject类,所以能找到。

    再次说明OC的底层没有实例和类方法的区分,类方法和实例方法是人为加上去的。我们只是为了配合OC的演出视而不见。

    五、 总结

    慢速查找流程:

    • checkIsKnownClass检查注册类。
    • realizeAndInitializeIfNeeded_locked初始化类,为方法查找做好准备。
    • 递归查找imp,会涉及到动态缓存库的二次确认以及父类的快速慢速查找。
      • 查找过程会进行二分查找/递归查找。
      • 是否二分要看方法列表是否已经排序,排序操作是在类加载实例化的时候完成的。
      • 二分查找算法很经典,充分利用>>1以及--不多浪费一次机会。
    • 找到imp直接跳转返回。根据LOOKUP_NOCACHE判断是否插入缓存。
    • 没有找到则判断是否进行动态方法决议。
    • 不进行动态方法决议则判断是否要forward


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


    收起阅读 »

    lookUpImpOrForward 消息慢速查找(上)

    上篇文章分析到了_obje_msgSend查找cache消息快速查找,最终会从汇编代码进入_lookUpImpOrForward进行慢速查找。这篇文章将详细分析这个流程。一、汇编中找不到缓存在汇编代码中只有_lookUpImpOrForward的调用而没有实现...
    继续阅读 »

    上篇文章分析到了_obje_msgSend查找cache消息快速查找,最终会从汇编代码进入_lookUpImpOrForward进行慢速查找。这篇文章将详细分析这个流程。

    一、汇编中找不到缓存

    在汇编代码中只有_lookUpImpOrForward的调用而没有实现,代码中直接搜这个也是搜不到的。因为实现在c/c++代码中,需要搜索lookUpImpOrForward。声明如下:

    extern IMP lookUpImpOrForward(id obj, SEL, Class cls, int behavior);

    那么参数肯定也就是汇编中传过来的,汇编中调用如下:

    .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

    • 3个参数没有什么好说的,behaviorLOOKUP_INITIALIZE | LOOKUP_RESOLVER。那就证明lookUpImpOrForward是有查找模式的。
    • 调用完_lookUpImpOrForward后有mov x17, x0说明是有返回值的,与c/c++lookUpImpOrForward的声明对应上了。

    那么就有一个问题了,为什么cache查找要使用汇编?
    1.汇编更接近机器语言,执行速度快。为了快速找到方法,优化方法查找时间。
    2.消息发送参数是未知参数(比如可变参数),c参数必须明确,汇编相对能够更加动态化。
    3.更安全。


    二、 慢速查找流程

    慢速查找就是不断遍历methodlist的过程,遍历是一个耗时的过程,所以是使用c/c++来实现的。

    2.1 lookUpImpOrForward

    首先明确慢速查找流程的目标是找到sel对应的imp。所以核心就是lookUpImpOrForward中返回imp的逻辑,精简后源码如下:


    NEVER_INLINE
    IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
    {
    //forward_imp赋值
    const IMP forward_imp = (IMP)_objc_msgForward_impcache;
    //要返回的imp
    IMP imp = nil;
    //当前查找的cls
    Class curClass;

    //初始化的一些处理,如果类没有初始化behavior会增加 LOOKUP_NOCACHE,判断是否初始化取的是data()->flags的第29位。
    if (slowpath(!cls->isInitialized())) {
    behavior |= LOOKUP_NOCACHE;
    }

    //类是否已经注册,注册后会加入allocatedClasses表中
    checkIsKnownClass(cls);
    //初始化需要的类。由于要去类中查找方法,如果rw,ro没有准备好那就没有办法查了。也就是为后面的查找代码做好准备。LOOKUP_INITIALIZE用在了这里
    cls = realizeAndInitializeIfNeeded_locked(inst, cls, behavior & LOOKUP_INITIALIZE);
    //赋值要查找的类
    curClass = cls;
    //死循环,除非return/break
    for (unsigned attempts = unreasonableClassCount();;) {//……}

    //参数LOOKUP_RESOLVER用在了这里,动态方法决议
    if (slowpath(behavior & LOOKUP_RESOLVER)) {
    behavior ^= LOOKUP_RESOLVER;
    return resolveMethod_locked(inst, sel, cls, behavior);
    }

    done:
    //没有初始化LOOKUP_NOCACHE就有值了,也就是查完后不要插入缓存。在这个流程中是插入
    if (fastpath((behavior & LOOKUP_NOCACHE) == 0)) {
    #if CONFIG_USE_PREOPT_CACHES
    //共享缓存
    while (cls->cache.isConstantOptimizedCache(/* strict */true)) {
    cls = cls->cache.preoptFallbackClass();
    }
    #endif
    //填充缓存,这里填充的是`cls`。也就是父类如果有缓存也会被加进子类。
    log_and_fill_cache(cls, imp, sel, inst, curClass);
    }
    done_unlock:
    runtimeLock.unlock();
    //forward_imp 并且有 LOOKUP_NIL 的时候直接返回nil。也就是不进行forward_imp
    if (slowpath((behavior & LOOKUP_NIL) && imp == forward_imp)) {
    return nil;
    }
    return imp;
    }
    • 先给forward_imp赋值_objc_msgForward_impcache,这个函数的实现是在汇编中。
    • impcurClass定义。
    • cls->isInitialized()类没有初始化则behavior增加LOOKUP_NOCACHE,类有没有初始化时由data()->flags的第29位决定的。
        bool isInitialized() {
    //#define uint32_t RW_INITIALIZED (1<<29)
    return getMeta()->data()->flags & RW_INITIALIZED;
    }

    • checkIsKnownClass判断类是否已经注册,注册后会加入allocatedClasses表中。
    • realizeAndInitializeIfNeeded_locked初始化需要的类,由于要去类中查找方法,如果rw ro没有准备好那就没有办法查了(methods就存在其中)。也就是为后面的查找代码做好准备。汇编中调用的时候传递的behaviorLOOKUP_INITIALIZE用在了这里。它的流程会在后面介绍。
    • 进入for死循环查找imp,核心肯定就是找imp赋值的地方了。那么就只有breakreturngoto才能停止循环,否则一直查找。
    • 如果上面imp没有找到,LOOKUP_RESOLVER是有值的,会进入动态方法决议。
    • 如果找到imp会跳转到done,判断是否需要插入缓存会调用log_and_fill_cache最终调用到cache.insert。父类如果有缓存找到也会加入到子类,这里是因为写入的时候参数是cls
    • 根据LOOKUP_NIL判断是否需要forward,不需要直接返回nil,需要返回imp

    2.1.1 behavior 说明

    在从汇编调入lookUpImpOrForward的时候传入的behavior参数是LOOKUP_INITIALIZELOOKUP_RESOLVER
    behavior类型如下:

    /* method lookup */
    enum {
    LOOKUP_INITIALIZE = 1,
    LOOKUP_RESOLVER = 2,
    LOOKUP_NIL = 4,
    LOOKUP_NOCACHE = 8,
    };

    根据上面的分析可以得到大致结论:

    • LOOKUP_INITIALIZE: 控制是否去进行类的初始化。有值初始化,没有不初始化。
    • LOOKUP_RESOLVER:是否进行动态方法决议。有值决议,没有值不决议。
    • LOOKUP_NIL:是否进行forward。有值不进行,没有值进行。
    • LOOKUP_NOCACHE:是否插入缓存。有值不插入缓存,没有值插入。

    2.2 realizeAndInitializeIfNeeded_locked

    在这里主要进行类的实例化和初始化,有两个分支:RealizeInitialize

    2.2.1 Realize

    (这个分支一般在_read_images的时候就处理好了)
    在进行类的实例化的时候调用流程是这样的realizeAndInitializeIfNeeded_locked->realizeClassMaybeSwiftAndLeaveLocked->realizeClassMaybeSwiftMaybeRelock->realizeClassWithoutSwift,最终会调用realizeClassWithoutSwiftswift会调用realizeSwiftClass。这个不是这篇文章的重点,分析下主要代码如下:


    static Class realizeClassWithoutSwift(Class cls, Class previously)
    {
    class_rw_t *rw;
    Class supercls;
    Class metacls;
    auto ro = (const class_ro_t *)cls->data();
    auto isMeta = ro->flags & RO_META;
    if (ro->flags & RO_FUTURE) {
    // This was a future class. rw data is already allocated.
    rw = cls->data();
    ro = cls->data()->ro();
    ASSERT(!isMeta);
    cls->changeInfo(RW_REALIZED|RW_REALIZING, RW_FUTURE);
    } else {
    // Normal class. Allocate writeable class data.
    rw = objc::zalloc<class_rw_t>();
    rw->set_ro(ro);
    rw->flags = RW_REALIZED|RW_REALIZING|isMeta;
    cls->setData(rw);
    }
    //赋类和元类的操作
    supercls = realizeClassWithoutSwift(remapClass(cls->getSuperclass()), nil);
    metacls = realizeClassWithoutSwift(remapClass(cls->ISA()), nil);

    //关联类
    cls->setSuperclass(supercls);
    cls->initClassIsa(metacls);
    return cls;
    }
    • 对类的ro以及rw进行处理。
    • 循环调用了父类和元类的realizeClassWithoutSwift
    • 关联了父类和元类。

    当对象调用方法的时候判断类是否初始化,如果初始化了再判断类的父类以及元类,相当于是递归操作了,一直到NSObject->nil为止。也就是说只要有一个类进行初始化它的上层(也就是父类和元类)都会进行初始化,是一个连锁反应。

    ⚠️为什么这么操作?
    就是为了查找方法。类没有实例方法的话会找父类,类没有类方法会找元类,所以需要这么操作。

    2.2.2 Initialized

    realizeAndInitializeIfNeeded_locked->initializeAndLeaveLocked->initializeAndMaybeRelock->initializeNonMetaClass。在initializeNonMetaClass中调用了callInitialize(cls)


    void callInitialize(Class cls)
    {
    ((void(*)(Class, SEL))objc_msgSend)(cls, @selector(initialize));
    asm("");
    }


    系统直接objc_msgSend发送了initialize消息。所以initialize是在类第一个方法被调用的时候进行调用的。也就是发送第一个消息的时候:
    消息慢速查找开始前进行类初始化的时候发送的initialize消息

    三、循环查找

    对于慢速查找流程,我们想到的就是先查自己然后再查父类一直找到NSObject->nil
    慢速查找流程应该是这样:
    1.查自己methodlist->(sel,imp)。
    2.查父类->NSObject->nil ->跳出来

    查看源码:

    //死循环,除非return/break
    for (unsigned attempts = unreasonableClassCount();;) {
    //先去共享缓存查找,防止这个时候共享缓存中已经写入了该方法。
    if (curClass->cache.isConstantOptimizedCache(/* strict */true)) {
    #if CONFIG_USE_PREOPT_CACHES
    //这里也是调用到了`_cache_getImp`汇编代码,最终调用了`CacheLookup`查找共享缓存。
    imp = cache_getImp(curClass, sel);
    //找到后直接跳转done_unlock
    if (imp) goto done_unlock;
    curClass = curClass->cache.preoptFallbackClass();
    #endif
    } else {
    // curClass method list.进行循环查找
    Method meth = getMethodNoSuper_nolock(curClass, sel);
    //找到method
    if (meth) {
    //返回imp
    imp = meth->imp(false);
    //跳转done
    goto done;
    }
    //这里curClass 会赋值,直到找到 NSObject->nil就会返回forward_imp
    if (slowpath((curClass = curClass->getSuperclass()) == nil)) {
    imp = forward_imp;
    break;
    }
    }

    // Superclass cache.
    imp = cache_getImp(curClass, sel);
    if (slowpath(imp == forward_imp)) {

    break;
    }
    if (fastpath(imp)) {
    goto done;
    }
    }
    • 可以看到这是一个死循环。
    • 如果有共享缓存,先查找共享缓存,因为前面做了很多准备工作,防止这个时候共享缓存中已经写入了该方法(在汇编中已经查过一次了)。
    • 否则就进行二分查找流程,核心逻辑是在getMethodNoSuper_nolock中调用的,查找完成返回。
    • 如果找到method则获取imp跳转done,如果没有找到将父类赋值给curClass,父类不存在则imp = forward_imp;
      • 找到则进入找到imp done的逻辑。
        • log_and_fill_cache插入缓存,也就是调用cls->cache.insert与分析cache的时候逻辑对上了。
        • 返回imp
      • 没有找到则curClass赋值superclass,没有superclass也就是找到了NSObject->nil的情况下imp = forward_imp
      • 没有找到并且有父类的情况下通过cache_getImp去父类的cache中查找。这里与共享缓存的cache_getImp是一个逻辑,最终都是调用汇编_cache_getImp->CacheLookup

      父类也有快速和慢速查找。

    • 如果父类中也没有找到,则进入递归。直到imp找到或者变为forward_imp才结束循环。

    _cache_getImp 说明
    源码:

        STATIC_ENTRY _cache_getImp

    GetClassFromIsa_p16 p0, 0
    CacheLookup GETIMP, _cache_getImp, LGetImpMissDynamic, LGetImpMissConstant

    LGetImpMissDynamic:
    mov p0, #0
    ret

    LGetImpMissConstant:
    mov p0, p2
    ret

    END_ENTRY _cache_getImp

    最终也是调用CacheLookup进行缓存查找。但是第三个参数是LGetImpMissDynamic实现是mov p0, #0 ret也就是找不到就返回了。不会去走__objc_msgSend_uncached逻辑。

    ⚠️ 找到父类缓存会插入自己的缓存

    3.1 二分查找流程

    3.1.1 getMethodNoSuper_nolock

    首先进入的是getMethodNoSuper_nolock,实现如下:


    static method_t *
    getMethodNoSuper_nolock(Class cls, SEL sel)
    {
    //获取methods
    auto const methods = cls->data()->methods();
    //循环,这个时候找的是methodlist存的是method_list_t,有可能是二维数据。动态加载方法和类导致的
    for (auto mlists = methods.beginLists(),
    end = methods.endLists();
    mlists != end;
    ++mlists)
    {
    method_t *m = search_method_list_inline(*mlists, sel);
    if (m) return m;
    }
    return nil;
    }



    • 这里只是普通的循环,因为methods获取的数据类型是method_array_t,它存储的是method_list_t。这里的数据结构有可能是二维数据,因为动态加载方法和类导致。
    • 核心逻辑是调用search_method_list_inline实现的


    收起阅读 »

    为数不多的人知道的 Kotlin 技巧以及 原理解析

    Google 引入 Kotlin 的目的就是为了让 Android 开发更加方便,自从官宣 Kotlin 成为了 Android 开发的首选语言之后,已经有越来越多的人开始使用 Kotlin。结合着 Kotlin 的高级函数的特性可以让代码可读性更强,更加简洁...
    继续阅读 »

    Google 引入 Kotlin 的目的就是为了让 Android 开发更加方便,自从官宣 Kotlin 成为了 Android 开发的首选语言之后,已经有越来越多的人开始使用 Kotlin。

    结合着 Kotlin 的高级函数的特性可以让代码可读性更强,更加简洁,但是呢简洁的背后是有代价的,使用不当对性能可能会有损耗,这块往往很容易被我们忽略,这就需要我们去研究 kotlin 语法糖背后的魔法,当我们在开发的时候,选择合适的语法糖,尽量避免这些错误,关于 Kotlin 性能损失那些事,可以看一下我另外两篇文章。

    这两篇文章都分析了 Kotlin 使用不当对性能的影响,不仅如此 Kotlin 当中还有很多让人傻傻分不清楚的语法糖例如 run, with, let, also, apply 等等,这篇文章将介绍一种简单的方法来区分它们以及如何选择使用。

    通过这篇文章你将学习到以下内容,文中会给出相应的答案

    • 如何使用 plus 操作符对集合进行操作?
    • 当获取 Map 值为空时,如何设置默认值?
    • require 或者 check 函数做什么用的?
    • 如何区分 run, with, let, also and apply 以及如何使用?
    • 如何巧妙的使用 in 和 when 关键字?
    • Kotlin 的单例有几种形式?
    • 为什么 by lazy 声明的变量只能用 val?

    plus 操作符

    在 Java 中算术运算符只能用于基本数据类型,+ 运算符可以与 String 值一起使用,但是不能在集合中使用,在 Kotlin 中可以应用在任何类型,我们来看一个例子,利用 plus (+) 和 minus (-) 对 Map 集合做运算,如下所示。

    fun main() {
    val numbersMap = mapOf("one" to 1, "two" to 2, "three" to 3)

    // plus (+)
    println(numbersMap + Pair("four", 4)) // {one=1, two=2, three=3, four=4}
    println(numbersMap + Pair("one", 10)) // {one=10, two=2, three=3}
    println(numbersMap + Pair("five", 5) + Pair("one", 11)) // {one=11, two=2, three=3, five=5}

    // minus (-)
    println(numbersMap - "one") // {two=2, three=3}
    println(numbersMap - listOf("two", "four")) // {one=1, three=3}
    }

    其实这里用到了运算符重载,Kotlin 在 Maps.kt 文件里面,定义了一系列用关键字 operator 声明的 Map 的扩展函数。

    用 operator 关键字声明 plus 函数,可以直接使用 + 号来做运算,使用 operator 修饰符声明 minus 函数,可以直接使用 - 号来做运算,其实我们也可以在自定义类里面实现 plus (+) 和 minus (-) 做运算。


    data class Salary(var base: Int = 100){
    override fun toString(): String = base.toString()
    }

    operator fun Salary.plus(other: Salary): Salary = Salary(base + other.base)
    operator fun Salary.minus(other: Salary): Salary = Salary(base - other.base)

    val s1 = Salary(10)
    val s2 = Salary(20)
    println(s1 + s2) // 30
    println(s1 - s2) // -10

    Map 集合的默认值

    在 Map 集合中,可以使用 withDefault 设置一个默认值,当键不在 Map 集合中,通过 getValue 返回默认值。

    val map = mapOf(
    "java" to 1,
    "kotlin" to 2,
    "python" to 3
    ).withDefault { "?" }

    println(map.getValue("java")) // 1
    println(map.getValue("kotlin")) // 2
    println(map.getValue("c++")) // ?

    源码实现也非常简单,当返回值为 null 时,返回设置的默认值。

    internal inline fun <K, V> Map<K, V>.getOrElseNullable(key: K, defaultValue: () -> V): V {
    val value = get(key)
    if (value == null && !containsKey(key)) {
    return defaultValue()
    } else {
    @Suppress("UNCHECKED_CAST")
    return value as V
    }
    }

    但是这种写法和 plus 操作符在一起用,有一个 bug ,看一下下面这个例子。

    val newMap = map + mapOf("python" to 3)
    println(newMap.getValue("c++")) // 调用 getValue 时抛出异常,异常信息:Key c++ is missing in the map.

    这段代码的意思就是,通过 plus(+) 操作符合并两个 map,返回一个新的 map, 但是忽略了默认值,所以看到上面的错误信息,我们在开发的时候需要注意这点。

    使用 require 或者 check 函数作为条件检查

    // 传统的做法
    val age = -1;
    if (age <= 0) {
    throw IllegalArgumentException("age must not be negative")
    }

    // 使用 require 去检查
    require(age > 0) { "age must be negative" }

    // 使用 checkNotNull 检查
    val name: String? = null
    checkNotNull(name){
    "name must not be null"
    }

    那么我们如何在项目中使用呢,具体的用法可以查看我 GitHub 上的项目 DataBindingDialog.kt 当中的用法。

    如何区分和使用 run, with, let, also, apply

    感谢大神 Elye 的这篇文章提供的思路 Mastering Kotlin standard functions

    run, with, let, also, apply 都是作用域函数,这些作用域函数如何使用,以及如何区分呢,我们将从以下三个方面来区分它们。

    • 是否是扩展函数。
    • 作用域函数的参数(this、it)。
    • 作用域函数的返回值(调用本身、其他类型即最后一行)。

    是否是扩展函数

    首先我们来看一下 with 和 T.run,这两个函数非常的相似,他们的区别在于 with 是个普通函数,T.run 是个扩展函数,来看一下下面的例子。

    val name: String? = null
    with(name){
    val subName = name!!.substring(1,2)
    }

    // 使用之前可以检查它的可空性
    name?.run { val subName = name.substring(1,2) }?:throw IllegalArgumentException("name must not be null")

    在这个例子当中,name?.run 会更好一些,因为在使用之前可以检查它的可空性。

    作用域函数的参数(this、it)

    我们在来看一下 T.run 和 T.let,它们都是扩展函数,但是他们的参数不一样 T.run 的参数是 this, T.let 的参数是 it。

    val name: String? = "hi-dhl.com"

    // 参数是 this,可以省略不写
    name?.run {
    println("The length is ${this.length} this 是可以省略的 ${length}")
    }

    // 参数 it
    name?.let {
    println("The length is ${it.length}")
    }

    // 自定义参数名字
    name?.let { str ->
    println("The length is ${str.length}")
    }

    在上面的例子中看似 T.run 会更好,因为 this 可以省略,调用更加的简洁,但是 T.let 允许我们自定义参数名字,使可读性更强,如果倾向可读性可以选择 T.let。

    作用域函数的返回值(调用本身、其他类型)

    接下里我们来看一下 T.let 和 T.also 它们接受的参数都是 it, 但是它们的返回值是不同的 T.let 返回最后一行,T.also 返回调用本身。


    var name = "hi-dhl"

    // 返回调用本身
    name = name.also {
    val result = 1 * 1
    "juejin"
    }
    println("name = ${name}") // name = hi-dhl

    // 返回的最后一行
    name = name.let {
    val result = 1 * 1
    "hi-dhl.com"
    }
    println("name = ${name}") // name = hi-dhl.com

    从上面的例子来看 T.also 似乎没有什么意义,细想一下其实是非常有意义的,在使用之前可以进行自我操作,结合其他的函数,功能会更强大。

    fun makeDir(path: String) = path.let{ File(it) }.also{ it.mkdirs() }

    当然 T.also 还可以做其他事情,比如利用 T.also 在使用之前可以进行自我操作特点,可以实现一行代码交换两个变量,在后面会有详细介绍

    T.apply 函数

    通过上面三个方面,大致了解函数的行为,接下来看一下 T.apply 函数,T.apply 函数是一个扩展函数,返回值是它本身,并且接受的参数是 this。

    // 普通方法
    fun createInstance(args: Bundle) : MyFragment {
    val fragment = MyFragment()
    fragment.arguments = args
    return fragment
    }
    // 改进方法
    fun createInstance(args: Bundle)
    = MyFragment().apply { arguments = args }


    // 普通方法
    fun createIntent(intentData: String, intentAction: String): Intent {
    val intent = Intent()
    intent.action = intentAction
    intent.data=Uri.parse(intentData)
    return intent
    }
    // 改进方法,链式调用
    fun createIntent(intentData: String, intentAction: String) =
    Intent().apply { action = intentAction }
    .apply { data = Uri.parse(intentData) }

    汇总

    以表格的形式汇总,更方便去理解

    函数是否是扩展函数函数参数(this、it)返回值(调用本身、最后一行)
    with不是this最后一行
    T.runthis最后一行
    T.letit最后一行
    T.alsoit调用本身
    T.applythis调用本身

    使用 T.also 函数交换两个变量

    接下来演示的是使用 T.also 函数,实现一行代码交换两个变量?我们先来回顾一下 Java 的做法。

    int a = 1;
    int b = 2;

    // Java - 中间变量
    int temp = a;
    a = b;
    b = temp;
    System.out.println("a = "+a +" b = "+b); // a = 2 b = 1

    // Java - 加减运算
    a = a + b;
    b = a - b;
    a = a - b;
    System.out.println("a = " + a + " b = " + b); // a = 2 b = 1

    // Java - 位运算
    a = a ^ b;
    b = a ^ b;
    a = a ^ b;
    System.out.println("a = " + a + " b = " + b); // a = 2 b = 1

    // Kotlin
    a = b.also { b = a }
    println("a = ${a} b = ${b}") // a = 2 b = 1

    来一起分析 T.also 是如何做到的,其实这里用到了 T.also 函数的两个特点。

    • 调用 T.also 函数返回的是调用者本身。
    • 在使用之前可以进行自我操作。

    也就是说 b.also { b = a } 会先将 a 的值 (1) 赋值给 b,此时 b 的值为 1,然后将 b 原始的值(2)赋值给 a,此时 a 的值为 2,实现交换两个变量的目的。

    in 和 when 关键字

    使用 in 和 when 关键字结合正则表达式,验证用户的输入,这是一个很酷的技巧。

    // 使用扩展函数重写 contains 操作符
    operator fun Regex.contains(text: CharSequence) : Boolean {
    return this.containsMatchIn(text)
    }

    // 结合着 in 和 when 一起使用
    when (input) {
    in Regex("[0–9]") -> println("contains a number")
    in Regex("[a-zA-Z]") -> println("contains a letter")
    }

    in 关键字其实是 contains 操作符的简写,它不是一个接口,也不是一个类型,仅仅是一个操作符,也就是说任意一个类只要重写了 contains 操作符,都可以使用 in 关键字,如果我们想要在自定义类型中检查一个值是否在列表中,只需要重写 contains() 方法即可,Collections 集合也重写了 contains 操作符。

    val input = "kotlin"

    when (input) {
    in listOf("java", "kotlin") -> println("found ${input}")
    in setOf("python", "c++") -> println("found ${input}")
    else -> println(" not found ${input}")
    }

    Kotlin 的单例三种写法

    我汇总了一下目前 Kotlin 单例总共有三种写法:

    • 使用 Object 实现单例。
    • 使用 by lazy 实现单例。
    • 可接受参数的单例(来自大神 Christophe Beyls)。

    使用 Object 实现单例

    代码:

    object WorkSingleton

    Kotlin 当中 Object 关键字就是一个单例,比 Java 的一坨代码看起来舒服了很多,来看一下编译后的 Java 文件。

    public final class WorkSingleton {
    public static final WorkSingleton INSTANCE;

    static {
    WorkSingleton var0 = new WorkSingleton();
    INSTANCE = var0;
    }
    }

    通过 static 代码块实现的单例,优点:饿汉式且是线程安全的,缺点:类加载时就初始化,浪费内存。

    使用 by lazy 实现单例

    利用伴生对象 和 by lazy 也可以实现单例,代码如下所示。

    class WorkSingleton private constructor() {

    companion object {

    // 方式一
    val INSTANCE1 by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) { WorkSingleton() }

    // 方式二 默认就是 LazyThreadSafetyMode.SYNCHRONIZED,可以省略不写,如下所示
    val INSTANCE2 by lazy { WorkSingleton() }
    }
    }

    lazy 的延迟模式有三种:

    • 上面代码所示 mode = LazyThreadSafetyMode.SYNCHRONIZED,lazy 默认的模式,可以省掉,这个模式的意思是:如果有多个线程访问,只有一条线程可以去初始化 lazy 对象。

    • 当 mode = LazyThreadSafetyMode.PUBLICATION 表达的意思是:对于还没有被初始化的 lazy 对象,可以被不同的线程调用,如果 lazy 对象初始化完成,其他的线程使用的是初始化完成的值。

    • mode = LazyThreadSafetyMode.NONE 表达的意思是:只能在单线程下使用,不能在多线程下使用,不会有锁的限制,也就是说它不会有任何线程安全的保证以及相关的开销。

    通过上面三种模式,这就可以理解为什么 by lazy 声明的变量只能用 val,因为初始化完成之后它的值是不会变的。

    可接受参数的单例

    但是有的时候,希望在单例实例化的时候传递参数,例如:

    Singleton.getInstance(context).doSome()

    上面这两种形式都不能满足,来看看大神 Christophe Beyls 在这篇文章给出的方法 Kotlin singletons with argument 代码如下。

    class WorkSingleton private constructor(context: Context) {
    init {
    // Init using context argument
    }

    companion object : SingletonHolder<WorkSingleton, Context>(::WorkSingleton)
    }


    open class SingletonHolder<out T : Any, in A>(creator: (A) -> T) {
    private var creator: ((A) -> T)? = creator
    @Volatile
    private var instance: T? = null

    fun getInstance(arg: A): T {
    val i = instance
    if (i != null) {
    return i
    }

    return synchronized(this) {
    val i2 = instance
    if (i2 != null) {
    i2
    } else {
    val created = creator!!(arg)
    instance = created
    creator = null
    created
    }
    }
    }
    }

    有没有感觉这和 Java 中双重校验锁的机制很像,在 SingletonHolder 类中如果已经初始化了直接返回,如果没有初始化进入 synchronized 代码块创建对象,利用了 Kotlin 伴生对象提供的非常强大功能,它能够像其他任何对象一样从基类继承,从而实现了与静态继承相当的功能。 所以我们将 SingletonHolder 作为单例类伴随对象的基类,在单例类上重用并公开 getInstance()函数。

    参数传递给 SingletonHolder 构造函数的 creator,creator 是一个 lambda 表达式,将 WorkSingleton 传递给 SingletonHolder 类构造函数。

    并且不限制传入参数的类型,凡是需要传递参数的单例模式,只需将单例类的伴随对象继承于 SingletonHolder,然后传入当前的单例类和参数类型即可,例如:

    class FileSingleton private constructor(path: String) {

    companion object : SingletonHolder<FileSingleton, String>(::FileSingleton)

    }
    收起阅读 »

    解析android匿名共享内存几个关键函数

    基础知识当我们在分析android的键盘记录的时候就不得不和input进行打交道,那么input在系统中是怎么进行实现的? Android手机中默认携带input子系统,并且开在机就会产生默认的mouse和keyboard事件,这样使得用户开机就可以触屏点击和...
    继续阅读 »

    基础知识

    当我们在分析android的键盘记录的时候就不得不和input进行打交道,那么input在系统中是怎么进行实现的? Android手机中默认携带input子系统,并且开在机就会产生默认的mouse和keyboard事件,这样使得用户开机就可以触屏点击和使用按键。 android中键盘实现的系统源码位置\source\frameworks\base\cmds\input\src\com\android\commands\input\Input.java

    关键代码实现解析:java层代码

    Input类定义

    public class Input {
    //用于定义打印调试信息
    private static final String TAG = "Input";
    private static final String INVALID_ARGUMENTS = "Error: Invalid arguments for command: ";
    //用map方式实现关键字和标识对应
    private static final Map<String, Integer> SOURCES = new HashMap<String, Integer>() {{
    put("keyboard", InputDevice.SOURCE_KEYBOARD);
    put("dpad", InputDevice.SOURCE_DPAD);
    put("gamepad", InputDevice.SOURCE_GAMEPAD);
    put("touchscreen", InputDevice.SOURCE_TOUCHSCREEN);
    put("mouse", InputDevice.SOURCE_MOUSE);
    put("stylus", InputDevice.SOURCE_STYLUS);
    put("trackball", InputDevice.SOURCE_TRACKBALL);
    put("touchpad", InputDevice.SOURCE_TOUCHPAD);
    put("touchnavigation", InputDevice.SOURCE_TOUCH_NAVIGATION);
    put("joystick", InputDevice.SOURCE_JOYSTICK);
    }};

    sendKeyEvent 函数定义

    //函数功能:发送键盘事件
    private void sendKeyEvent(int inputSource, int keyCode, boolean longpress) {
    //获取从开机到现在的毫秒数
    long now = SystemClock.uptimeMillis();
    //注入键盘事件
    injectKeyEvent(new KeyEvent(now, now, KeyEvent.ACTION_DOWN, keyCode, 0, 0,
    KeyCharacterMap.VIRTUAL_KEYBOARD, 0, 0, inputSource));
    if (longpress) {
    injectKeyEvent(new KeyEvent(now, now, KeyEvent.ACTION_DOWN, keyCode, 1, 0,
    KeyCharacterMap.VIRTUAL_KEYBOARD, 0, KeyEvent.FLAG_LONG_PRESS,
    inputSource));
    }
    injectKeyEvent(new KeyEvent(now, now, KeyEvent.ACTION_UP, keyCode, 0, 0,
    KeyCharacterMap.VIRTUAL_KEYBOARD, 0, 0, inputSource));
    }

    sendSwipe 函数定义

    //函数功能:实现滑屏操作
    private void sendSwipe(int inputSource, float x1, float y1, float x2, float y2, int duration) {
    if (duration < 0) {
    duration = 300;
    }
    //获取从开机到现在的毫秒数
    long now = SystemClock.uptimeMillis();
    //注入触摸事件
    injectMotionEvent(inputSource, MotionEvent.ACTION_DOWN, now, x1, y1, 1.0f);
    //计算开始时间和结束时间
    long startTime = now;
    long endTime = startTime + duration;
    while (now < endTime) {
    long elapsedTime = now - startTime;
    float alpha = (float) elapsedTime / duration;
    injectMotionEvent(inputSource, MotionEvent.ACTION_MOVE, now, lerp(x1, x2, alpha),
    lerp(y1, y2, alpha), 1.0f);
    now = SystemClock.uptimeMillis();
    }
    injectMotionEvent(inputSource, MotionEvent.ACTION_UP, now, x2, y2, 0.0f);
    }

    injectKeyEvent 函数定义

    //函数功能:注入事件的实现
    private void injectKeyEvent(KeyEvent event) {
    //打印调试信息
    Log.i(TAG, "injectKeyEvent: " + event);
    //获取inputManager的实例事件
    InputManager.getInstance().injectInputEvent(event,
    InputManager.INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH);
    }

    injectMotionEvent 函数定义

    函数功能:注入触摸事件
    private void injectMotionEvent(int inputSource, int action, long when, float x, float y, float pressure) {
    final float DEFAULT_SIZE = 1.0f;
    final int DEFAULT_META_STATE = 0;
    final float DEFAULT_PRECISION_X = 1.0f;
    final float DEFAULT_PRECISION_Y = 1.0f;
    final int DEFAULT_DEVICE_ID = 0;
    final int DEFAULT_EDGE_FLAGS = 0;
    MotionEvent event = MotionEvent.obtain(when, when, action, x, y, pressure, DEFAULT_SIZE,
    DEFAULT_META_STATE, DEFAULT_PRECISION_X, DEFAULT_PRECISION_Y, DEFAULT_DEVICE_ID,
    DEFAULT_EDGE_FLAGS);
    event.setSource(inputSource);
    Log.i(TAG, "injectMotionEvent: " + event);
    InputManager.getInstance().injectInputEvent(event,
    InputManager.INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH);
    }

    sendMove 函数定义

    //函数功能:发送移动事件
    private void sendMove(int inputSource, float dx, float dy) {
    //获取时间
    long now = SystemClock.uptimeMillis();
    //调用注入触摸事件
    injectMotionEvent(inputSource, MotionEvent.ACTION_MOVE, now, dx, dy, 0.0f);
    }


    内存几个关键函数

    基础原理

    android系统在应用程序框架层中提供了两个C++类MemoryHeapBase和MemoryBase来创建和管理匿名共享内存。 如果一个进程需要与其他进程共享一块完整的匿名共享内存,那么就可以通过使用MemoryHeapBase类类创建这块匿名共享内存。如果一个进程创建一块匿名共享内存后,只希望与其他进程共享其中的一部分,那么就可以通过MemoryBase类来创建这块匿名共享内存。

    IMemory.h:定义内存相关类的接口,表示堆内存的类IMemoryHeap和BnMemoryHeap,表示一般内存的类IMemory和BnMemory。 MemoryHeapBase.h:定义类MemoryHeapBase,继承并实现BnMemoryHeap MemoryBase.h:定义类MemoryBase,继承并实现BnMemory。

    android系统在应用程序框架层中提供了java类MemoryFile来创建和管理匿名共享内存。使用java类MemoryFile创建的匿名共享内存可以在不同的Android应用程序之间进行共享。

    java代码解析

    匿名共享内存java类MemoryFile在系统中的source\frameworks\base\core\java\android\os\MemoryFile.java文件中实现。

    //匿名共享内存的构造函数,参数1表示创建匿名共享内存的名称,参数2表示创建匿名共享内存大小
    public MemoryFile(String name int length) throws IOException {
    mLength = length;
    if (length >= 0) {
    //通过调用jni的接口去打开匿名共享内存
    mFD = native_open(name length);
    } else {
    throw new IOException("Invalid length: " + length);
    }

    if (length > 0) {
    //进行映射
    mAddress = native_mmap(mFD length PROT_READ | PROT_WRITE);
    } else {
    mAddress = 0;
    }
    }

    C++关键函数解析

    //MemoryHeapBase构造函数的实现
    MemoryHeapBase::MemoryHeapBase(const char* device size_t size uint32_t flags)
    : mFD(-1) mSize(0) mBase(MAP_FAILED) mFlags(flags)
    mDevice(0) mNeedUnmap(false) mOffset(0)
    {
    int open_flags = O_RDWR;
    if (flags & NO_CACHING)
    open_flags |= O_SYNC;
    //通过调用open打开匿名共享内存设备文件
    int fd = open(device open_flags);
    ALOGE_IF(fd<0 "error opening %s: %s" device strerror(errno));
    if (fd >= 0) {
    //指定的匿名共享内存大小按页对齐
    const size_t pagesize = getpagesize();
    size = ((size + pagesize-1) & ~(pagesize-1));
    //匿名共享内存映射到当前进程地址空间
    if (mapfd(fd size) == NO_ERROR) {
    mDevice = device;
    }
    }
    }
    //MemoryHeapBase构造函数
    MemoryHeapBase::MemoryHeapBase(size_t size uint32_t flags char const * name)
    : mFD(-1) mSize(0) mBase(MAP_FAILED) mFlags(flags)
    mDevice(0) mNeedUnmap(false) mOffset(0)
    {
    //获得系统中页大小的内存
    const size_t pagesize = getpagesize();
    //内存页对齐
    size = ((size + pagesize-1) & ~(pagesize-1));
    //创建一块匿名共享内存
    int fd = ashmem_create_region(name == NULL ? "MemoryHeapBase" : name size);
    ALOGE_IF(fd<0 "error creating ashmem region: %s" strerror(errno));
    if (fd >= 0) {
    //创建的匿名共享内存映射到当前进程地址空间中
    if (mapfd(fd size) == NO_ERROR) {
    if (flags & READ_ONLY) {//如果地址映射成功,修改匿名共享内存的访问属性
    ashmem_set_prot_region(fd PROT_READ);
    }
    }
    }
    }

    初探android系统中input的java层实现

    基础知识

    当我们在分析android的键盘记录的时候就不得不和input进行打交道,那么input在系统中是怎么进行实现的? Android手机中默认携带input子系统,并且开在机就会产生默认的mouse和keyboard事件,这样使得用户开机就可以触屏点击和使用按键。 android中键盘实现的系统源码位置\source\frameworks\base\cmds\input\src\com\android\commands\input\Input.java

    关键代码实现解析:java层代码

    Input类定义

    public class Input {
    //用于定义打印调试信息
    private static final String TAG = "Input";
    private static final String INVALID_ARGUMENTS = "Error: Invalid arguments for command: ";
    //用map方式实现关键字和标识对应
    private static final Map<String, Integer> SOURCES = new HashMap<String, Integer>() {{
    put("keyboard", InputDevice.SOURCE_KEYBOARD);
    put("dpad", InputDevice.SOURCE_DPAD);
    put("gamepad", InputDevice.SOURCE_GAMEPAD);
    put("touchscreen", InputDevice.SOURCE_TOUCHSCREEN);
    put("mouse", InputDevice.SOURCE_MOUSE);
    put("stylus", InputDevice.SOURCE_STYLUS);
    put("trackball", InputDevice.SOURCE_TRACKBALL);
    put("touchpad", InputDevice.SOURCE_TOUCHPAD);
    put("touchnavigation", InputDevice.SOURCE_TOUCH_NAVIGATION);
    put("joystick", InputDevice.SOURCE_JOYSTICK);
    }};

    sendKeyEvent 函数定义

    //函数功能:发送键盘事件
    private void sendKeyEvent(int inputSource, int keyCode, boolean longpress) {
    //获取从开机到现在的毫秒数
    long now = SystemClock.uptimeMillis();
    //注入键盘事件
    injectKeyEvent(new KeyEvent(now, now, KeyEvent.ACTION_DOWN, keyCode, 0, 0,
    KeyCharacterMap.VIRTUAL_KEYBOARD, 0, 0, inputSource));
    if (longpress) {
    injectKeyEvent(new KeyEvent(now, now, KeyEvent.ACTION_DOWN, keyCode, 1, 0,
    KeyCharacterMap.VIRTUAL_KEYBOARD, 0, KeyEvent.FLAG_LONG_PRESS,
    inputSource));
    }
    injectKeyEvent(new KeyEvent(now, now, KeyEvent.ACTION_UP, keyCode, 0, 0,
    KeyCharacterMap.VIRTUAL_KEYBOARD, 0, 0, inputSource));
    }

    sendSwipe 函数定义

    //函数功能:实现滑屏操作
    private void sendSwipe(int inputSource, float x1, float y1, float x2, float y2, int duration) {
    if (duration < 0) {
    duration = 300;
    }
    //获取从开机到现在的毫秒数
    long now = SystemClock.uptimeMillis();
    //注入触摸事件
    injectMotionEvent(inputSource, MotionEvent.ACTION_DOWN, now, x1, y1, 1.0f);
    //计算开始时间和结束时间
    long startTime = now;
    long endTime = startTime + duration;
    while (now < endTime) {
    long elapsedTime = now - startTime;
    float alpha = (float) elapsedTime / duration;
    injectMotionEvent(inputSource, MotionEvent.ACTION_MOVE, now, lerp(x1, x2, alpha),
    lerp(y1, y2, alpha), 1.0f);
    now = SystemClock.uptimeMillis();
    }
    injectMotionEvent(inputSource, MotionEvent.ACTION_UP, now, x2, y2, 0.0f);
    }

    injectKeyEvent 函数定义

    //函数功能:注入事件的实现
    private void injectKeyEvent(KeyEvent event) {
    //打印调试信息
    Log.i(TAG, "injectKeyEvent: " + event);
    //获取inputManager的实例事件
    InputManager.getInstance().injectInputEvent(event,
    InputManager.INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH);
    }

    injectMotionEvent 函数定义

    函数功能:注入触摸事件
    private void injectMotionEvent(int inputSource, int action, long when, float x, float y, float pressure) {
    final float DEFAULT_SIZE = 1.0f;
    final int DEFAULT_META_STATE = 0;
    final float DEFAULT_PRECISION_X = 1.0f;
    final float DEFAULT_PRECISION_Y = 1.0f;
    final int DEFAULT_DEVICE_ID = 0;
    final int DEFAULT_EDGE_FLAGS = 0;
    MotionEvent event = MotionEvent.obtain(when, when, action, x, y, pressure, DEFAULT_SIZE,
    DEFAULT_META_STATE, DEFAULT_PRECISION_X, DEFAULT_PRECISION_Y, DEFAULT_DEVICE_ID,
    DEFAULT_EDGE_FLAGS);
    event.setSource(inputSource);
    Log.i(TAG, "injectMotionEvent: " + event);
    InputManager.getInstance().injectInputEvent(event,
    InputManager.INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH);
    }

    sendMove 函数定义

    //函数功能:发送移动事件
    private void sendMove(int inputSource, float dx, float dy) {
    //获取时间
    long now = SystemClock.uptimeMillis();
    //调用注入触摸事件
    injectMotionEvent(inputSource, MotionEvent.ACTION_MOVE, now, dx, dy, 0.0f);
    }

    初探android系统中input的java层实现

    基础知识

    当我们在分析android的键盘记录的时候就不得不和input进行打交道,那么input在系统中是怎么进行实现的? Android手机中默认携带input子系统,并且开在机就会产生默认的mouse和keyboard事件,这样使得用户开机就可以触屏点击和使用按键。 android中键盘实现的系统源码位置\source\frameworks\base\cmds\input\src\com\android\commands\input\Input.java

    关键代码实现解析:java层代码

    Input类定义

    public class Input {
    //用于定义打印调试信息
    private static final String TAG = "Input";
    private static final String INVALID_ARGUMENTS = "Error: Invalid arguments for command: ";
    //用map方式实现关键字和标识对应
    private static final Map<String, Integer> SOURCES = new HashMap<String, Integer>() {{
    put("keyboard", InputDevice.SOURCE_KEYBOARD);
    put("dpad", InputDevice.SOURCE_DPAD);
    put("gamepad", InputDevice.SOURCE_GAMEPAD);
    put("touchscreen", InputDevice.SOURCE_TOUCHSCREEN);
    put("mouse", InputDevice.SOURCE_MOUSE);
    put("stylus", InputDevice.SOURCE_STYLUS);
    put("trackball", InputDevice.SOURCE_TRACKBALL);
    put("touchpad", InputDevice.SOURCE_TOUCHPAD);
    put("touchnavigation", InputDevice.SOURCE_TOUCH_NAVIGATION);
    put("joystick", InputDevice.SOURCE_JOYSTICK);
    }};

    sendKeyEvent 函数定义

    //函数功能:发送键盘事件
    private void sendKeyEvent(int inputSource, int keyCode, boolean longpress) {
    //获取从开机到现在的毫秒数
    long now = SystemClock.uptimeMillis();
    //注入键盘事件
    injectKeyEvent(new KeyEvent(now, now, KeyEvent.ACTION_DOWN, keyCode, 0, 0,
    KeyCharacterMap.VIRTUAL_KEYBOARD, 0, 0, inputSource));
    if (longpress) {
    injectKeyEvent(new KeyEvent(now, now, KeyEvent.ACTION_DOWN, keyCode, 1, 0,
    KeyCharacterMap.VIRTUAL_KEYBOARD, 0, KeyEvent.FLAG_LONG_PRESS,
    inputSource));
    }
    injectKeyEvent(new KeyEvent(now, now, KeyEvent.ACTION_UP, keyCode, 0, 0,
    KeyCharacterMap.VIRTUAL_KEYBOARD, 0, 0, inputSource));
    }

    sendSwipe 函数定义

    //函数功能:实现滑屏操作
    private void sendSwipe(int inputSource, float x1, float y1, float x2, float y2, int duration) {
    if (duration < 0) {
    duration = 300;
    }
    //获取从开机到现在的毫秒数
    long now = SystemClock.uptimeMillis();
    //注入触摸事件
    injectMotionEvent(inputSource, MotionEvent.ACTION_DOWN, now, x1, y1, 1.0f);
    //计算开始时间和结束时间
    long startTime = now;
    long endTime = startTime + duration;
    while (now < endTime) {
    long elapsedTime = now - startTime;
    float alpha = (float) elapsedTime / duration;
    injectMotionEvent(inputSource, MotionEvent.ACTION_MOVE, now, lerp(x1, x2, alpha),
    lerp(y1, y2, alpha), 1.0f);
    now = SystemClock.uptimeMillis();
    }
    injectMotionEvent(inputSource, MotionEvent.ACTION_UP, now, x2, y2, 0.0f);
    }

    injectKeyEvent 函数定义

    //函数功能:注入事件的实现
    private void injectKeyEvent(KeyEvent event) {
    //打印调试信息
    Log.i(TAG, "injectKeyEvent: " + event);
    //获取inputManager的实例事件
    InputManager.getInstance().injectInputEvent(event,
    InputManager.INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH);
    }

    injectMotionEvent 函数定义

    函数功能:注入触摸事件
    private void injectMotionEvent(int inputSource, int action, long when, float x, float y, float pressure) {
    final float DEFAULT_SIZE = 1.0f;
    final int DEFAULT_META_STATE = 0;
    final float DEFAULT_PRECISION_X = 1.0f;
    final float DEFAULT_PRECISION_Y = 1.0f;
    final int DEFAULT_DEVICE_ID = 0;
    final int DEFAULT_EDGE_FLAGS = 0;
    MotionEvent event = MotionEvent.obtain(when, when, action, x, y, pressure, DEFAULT_SIZE,
    DEFAULT_META_STATE, DEFAULT_PRECISION_X, DEFAULT_PRECISION_Y, DEFAULT_DEVICE_ID,
    DEFAULT_EDGE_FLAGS);
    event.setSource(inputSource);
    Log.i(TAG, "injectMotionEvent: " + event);
    InputManager.getInstance().injectInputEvent(event,
    InputManager.INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH);
    }

    sendMove 函数定义

    //函数功能:发送移动事件
    private void sendMove(int inputSource, float dx, float dy) {
    //获取时间
    long now = SystemClock.uptimeMillis();
    //调用注入触摸事件
    injectMotionEvent(inputSource, MotionEvent.ACTION_MOVE, now, dx, dy, 0.0f);
    }


    收起阅读 »

    【插件&热修系列】ClassLoader方案设计

    引言 上一个阶段我们开始进入插件/热修的领域,了解了热修的前世今生,下面我们来学习下热修中的ClassLoader方案设计; ClassLoader主要是用来加载插件用的,在启动插件前首先要把插件加载进来,下面我们通过不同方案分析,了解加载的不同姿势~ ...
    继续阅读 »

    引言


    上一个阶段我们开始进入插件/热修的领域,了解了热修的前世今生,下面我们来学习下热修中的ClassLoader方案设计;


    ClassLoader主要是用来加载插件用的,在启动插件前首先要把插件加载进来,下面我们通过不同方案分析,了解加载的不同姿势~


    方案1:合并Dex(hook方式)


    谁用了这个方案?


    QQ团队的空间换肤功能


    原理


    将我们插件dex和宿主apk的class.dex合并,都放到宿主dexElements数组中。App每次启动从该数组中加载。


    实战流程


    1)获取宿主,dexElements


    2)获取插件,dexElements


    3)合并两个dexElements


    4)将新的dexElements 赋值到 宿主dexElements


    代码


    Class<?> clazz = Class.forName("dalvik.system.BaseDexClassLoader");
    Field pathListField = clazz.getDeclaredField("pathList");
    pathListField.setAccessible(true);

    Class<?> dexPathListClass = Class.forName("dalvik.system.DexPathList");
    Field dexElementsField = dexPathListClass.getDeclaredField("dexElements");
    dexElementsField.setAccessible(true);

    // 宿主的 类加载器
    ClassLoader pathClassLoader = context.getClassLoader();
    // DexPathList类的对象
    Object hostPathList = pathListField.get(pathClassLoader);
    // 宿主的 dexElements
    Object[] hostDexElements = (Object[]) dexElementsField.get(hostPathList);

    // 插件的 类加载器
    ClassLoader dexClassLoader = new DexClassLoader(apkPath, context.getCacheDir().getAbsolutePath(),null, pathClassLoader);
    // DexPathList类的对象
    Object pluginPathList = pathListField.get(dexClassLoader);
    // 插件的 dexElements
    Object[] pluginDexElements = (Object[]) dexElementsField.get(pluginPathList);

    // 宿主dexElements = 宿主dexElements + 插件dexElements
    // 创建一个新数组
    Object[] newDexElements = (Object[]) Array.newInstance(hostDexElements.getClass().getComponentType(),hostDexElements.length + pluginDexElements.length);
    // 拷贝
    System.arraycopy(hostDexElements, 0, newDexElements,0, hostDexElements.length);
    System.arraycopy(pluginDexElements, 0,newDexElements,hostDexElements.length, pluginDexElements.length);

    // 赋值
    dexElementsField.set(hostPathList, newDexElements);


    特点


    此乃单ClassLoader方案,插件和宿主程序的类全部都通过宿主的ClasLoader加载,存在的短板为“插件之间或者插件与宿主之间使用的类库有相同的时候,那么就会加载乱序等问题”


    方案2:替换 PathClassloader 的 parent


    谁用了这个方案?


    微店、Instant-Run


    知识基础


    安装在手机里的apk(宿主)的ClassLoader链路关系


    1)代码:


    ClassLoader classLoader = getClassLoader();
    ClassLoader parentClassLoader = classLoader.getParent();
    ClassLoader pParentClassLoader = parentClassLoader.getParent();

    2)关系:


    ==classLoader==:dalvik.system.PathClassLoader


    ==parentClassLoader==:java.lang.BootClassLoader


    ==pParentClassLoader==:null


    可以看出,当前的classLoader是PathClassLoader,parent的ClassLoader是BootClassLoader,而BootClassLoader没有parent的ClassLoader


    实现思想


    如何利用上面的宿主链路基础原理设计?


    ClassLoader的构造方法中有一个参数是parent; 如果把PathClassLoader的parent替换成我们==插件的classLoader==; 再把==插件的classLoader的parent==设置成BootClassLoader; 加上父委托的机制,查找插件类的过程就变成:BootClassLoader->==插件的classLoader==->PathClassLoader


    代码实现


    public static void loadApk(Context context, String apkPath) {
    File dexFile = context.getDir("dex", Context.MODE_PRIVATE);
    File apkFile = new File(apkPath);
    //找到 PathClassLoader
    ClassLoader classLoader = context.getClassLoader();
    //构建插件的 ClassLoader
    //PathClassLoader 的父亲 传递给 插件的ClassLoader
    //到这里,顺序为:BootClassLoader->插件的classLoader
    DexClassLoader dexClassLoader = new DexClassLoader(apkFile.getAbsolutePath(),dexFile.getAbsolutePath(), null,classLoader.getParent());
    try {
    //PathClassLoader 的父亲设置为 插件的ClassLoader
    //顺序为:BootClassLoader->插件的classLoader->PathClassLoader
    Field fieldClassLoader = ClassLoader.class.getDeclaredField("parent");
    if (fieldClassLoader != null) {
    fieldClassLoader.setAccessible(true);
    fieldClassLoader.set(classLoader, dexClassLoader);
    }
    } catch (Exception e) {
    e.printStackTrace();
    }
    }

    特点


    此乃单ClassLoader方案,插件和宿主程序的类全部都通过宿主的ClasLoader加载,存在的短板为“插件之间或者插件与宿主之间使用的类库有相同的时候,那么就会加载乱序等问题”


    方案3:利用LoadedApk的缓存机制


    谁用了这个方案?


    360的DroidPlugin


    实现原理


    java.lang.ClassLoader cl = r.packageInfo.getClassLoader();
    activity = mInstrumentation.newActivity(cl, component.getClassName(), r.intent);
    StrictMode.incrementExpectedActivityCount(activity.getClass());
    r.intent.setExtrasClassLoader(cl);

    上面代码做了两件事:


    1)系统用packageInfo.getClassLoader()来加载已安装app的Activity


    2)实例化的Activity


    其中packageInfo为LoadedApk类型,是APK文件在内存中的表示,Apk文件的相关信息,诸如Apk文件的代码和资源,甚至代码里面的Activity,Service等组件的信息我们都可以通过此对象获取。


    packageInfo怎么生成的?通过阅读源码得出:


    1)先在ActivityThread中的mPackages缓存(Map,key为包名,value为LoadedApk)中获取


    2)如果缓存没有,new LoadedApk 生成一个,然后放到缓存mPackages中


    基于上面系统的原理,实现的关键点步骤:


    1)构建插件 ApplicationInfo 信息


    ApplicationInfo applicationInfo = (ApplicationInfo) generateApplicationInfoMethod.invoke(packageParser,packageObj, 0, defaultPackageUserState);
    String apkPath = apkFile.getPath();
    applicationInfo.sourceDir = apkPath;
    applicationInfo.publicSourceDir = apkPath;

    2)构建 CompatibilityInfo


    Class<?> compatibilityInfoClass = Class.forName("android.content.res.CompatibilityInfo");
    Field defaultCompatibilityInfoField = compatibilityInfoClass.getDeclaredField("DEFAULT_COMPATIBILITY_INFO");
    defaultCompatibilityInfoField.setAccessible(true);
    Object defaultCompatibilityInfo = defaultCompatibilityInfoField.get(null);

    3)根据 ApplicationInfo 和 CompatibilityInfo,构建插件的 loadedApk


    Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
    Method getPackageInfoNoCheckMethod = activityThreadClass.getDeclaredMethod("getPackageInfoNoCheck", ApplicationInfo.class, compatibilityInfoClass);
    Object loadedApk = getPackageInfoNoCheckMethod.invoke(currentActivityThread, applicationInfo, defaultCompatibilityInfo);

    4)构建插件的ClassLoader,然后把它替换到插件loadedApk的ClassLoader中


    String odexPath = Utils.getPluginOptDexDir(applicationInfo.packageName).getPath();
    String libDir = Utils.getPluginLibDir(applicationInfo.packageName).getPath();
    ClassLoader classLoader = new DexClassLoader(apkFile.getPath(), odexPath, libDir, ClassLoader.getSystemClassLoader());
    Field mClassLoaderField = loadedApk.getClass().getDeclaredField("mClassLoader");
    mClassLoaderField.setAccessible(true);
    mClassLoaderField.set(loadedApk, classLoader);

    5)把插件loadedApk添加进ActivityThread的mPackages中


    // 先获取到当前的ActivityThread对象
    Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
    Method currentActivityThreadMethod = activityThreadClass.getDeclaredMethod("currentActivityThread");
    currentActivityThreadMethod.setAccessible(true);
    Object currentActivityThread = currentActivityThreadMethod.invoke(null);
    // 获取到 mPackages 这个静态成员变量, 这里缓存了dex包的信息
    Field mPackagesField = activityThreadClass.getDeclaredField("mPackages");
    mPackagesField.setAccessible(true);
    Map mPackages = (Map) mPackagesField.get(currentActivityThread);

    // 由于是弱引用, 因此我们必须在某个地方存一份, 不然容易被GC; 那么就前功尽弃了.
    sLoadedApk.put(applicationInfo.packageName, loadedApk);
    WeakReference weakReference = new WeakReference(loadedApk);
    mPackages.put(applicationInfo.packageName, weakReference);

    6)绕过系统检查,让系统觉得插件已经安装在系统上了


    private static void hookPackageManager() throws Exception {
    // 这一步是因为 initializeJavaContextClassLoader 这个方法内部无意中检查了这个包是否在系统安装
    // 如果没有安装, 直接抛出异常, 这里需要临时Hook掉 PMS, 绕过这个检查.
    Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
    Method currentActivityThreadMethod = activityThreadClass.getDeclaredMethod("currentActivityThread");
    currentActivityThreadMethod.setAccessible(true);
    Object currentActivityThread = currentActivityThreadMethod.invoke(null);

    // 获取ActivityThread里面原始的 sPackageManager
    Field sPackageManagerField = activityThreadClass.getDeclaredField("sPackageManager");
    sPackageManagerField.setAccessible(true);
    Object sPackageManager = sPackageManagerField.get(currentActivityThread);

    // 准备好代理对象, 用来替换原始的对象
    Class<?> iPackageManagerInterface = Class.forName("android.content.pm.IPackageManager");
    Object proxy = Proxy.newProxyInstance(iPackageManagerInterface.getClassLoader(),
    new Class<?>[] { iPackageManagerInterface },
    new IPackageManagerHookHandler(sPackageManager));

    // 1. 替换掉ActivityThread里面的 sPackageManager 字段
    sPackageManagerField.set(currentActivityThread, proxy);
    }


    特点


    1)自定义了插件的ClassLoader,并且绕开了Framework的检测


    2)Hook的地方也有点多:不仅需要Hook AMS和H,还需要Hook ActivityThread的mPackages和PackageManager!


    3)多ClassLoader构架,每一个插件都有一个自己的ClassLoader,隔离性好,如果不同的插件使用了同一个库的不同版本,它们相安无事


    4)真正完成代码的热加载!


    插件需要升级,直接重新创建一个自定的ClassLoader加载新的插件,然后替换掉原来的版本即可(Java中,不同ClassLoader加载的同一个类被认为是不同的类)


    单ClassLoader的话实现非常麻烦,有可能需要重启进程。


    方案4:自定义ClassLoader逻辑


    谁用了?


    腾讯视频等事业群中的Shadow热修框架


    实现原理


    1)先了解下宿主(已经安装App)的ClassLoader链路: BootClassLoader -> PathClassLoader


    2)插件可以加载宿主的类实现:


    构建插件的ClassLoader,名字为ApkClassLoader,其中父加载器传的是宿主的ClassLoader,代码片段为:


    class ApkClassLoader extends DexClassLoader {

    static final String TAG = "daviAndroid";
    private ClassLoader mGrandParent;
    private final String[] mInterfacePackageNames;

    @Deprecated
    ApkClassLoader(InstalledApk installedApk,
    ClassLoader parent,////parent = 宿主ClassLoader
    String[] mInterfacePackageNames,
    int grandTimes) {

    super(installedApk.apkFilePath, installedApk.oDexPath, installedApk.libraryPath, parent);

    在这个流程下,插件查找的流程变为: BootClassLoader -> PathClassLoader -> ApkClassLoader(其实就是双亲委托)


    3)插件不需要加载宿主的类实现:


    class ApkClassLoader extends DexClassLoader {

    ............
    //1)系统里面找
    Class<?> clazz = findLoadedClass(className);
    if (clazz == null) {
    //2)从自己的dexPath中查找
    clazz = findClass(className);
    if (clazz == null) {
    //3)从parent的parent找(BootClassLoader)ClassLoader中查找。
    clazz = mGrandParent.loadClass(className);
    }
    }

    ............
    }

    这个逻辑插件不需要加载宿主的类,所以加载逻辑中不会去加载宿主的类(也就是会经过PathClassLoader),这种情况下,即使插件和宿主用到了同一个类,那么插件加载的时候不会因为委托加载机制而去加载了宿主的,导致插件的加载错了;


    代码实现


    class ApkClassLoader extends DexClassLoader {
    private ClassLoader mGrandParent;
    private final String[] mInterfacePackageNames;

    ApkClassLoader(InstalledApk installedApk,
    ClassLoader parent, String[] mInterfacePackageNames, int grandTimes) {
    super(installedApk.apkFilePath, installedApk.oDexPath, installedApk.libraryPath, parent);
    ClassLoader grand = parent;
    for (int i = 0; i < grandTimes; i++) {
    grand = grand.getParent();
    }
    mGrandParent = grand;
    this.mInterfacePackageNames = mInterfacePackageNames;
    }

    @Override
    protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException {
    String packageName;
    int dot = className.lastIndexOf('.');
    if (dot != -1) {
    packageName = className.substring(0, dot);
    } else {
    packageName = "";
    }

    boolean isInterface = false;
    for (String interfacePackageName : mInterfacePackageNames) {
    if (packageName.equals(interfacePackageName)) {
    isInterface = true;
    break;
    }
    }

    if (isInterface) {
    return super.loadClass(className, resolve);
    } else {
    Class<?> clazz = findLoadedClass(className);

    if (clazz == null) {
    ClassNotFoundException suppressed = null;
    try {
    clazz = findClass(className);
    } catch (ClassNotFoundException e) {
    suppressed = e;
    }

    if (clazz == null) {
    try {
    clazz = mGrandParent.loadClass(className);
    } catch (ClassNotFoundException e) {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
    e.addSuppressed(suppressed);
    }
    throw e;
    }
    }
    }

    return clazz;
    }
    }

    /**
    * 从apk中读取接口的实现
    *
    * @param clazz 接口类
    * @param className 实现类的类名
    * @param <T> 接口类型
    * @return 所需接口
    * @throws Exception
    */
    <T> T getInterface(Class<T> clazz, String className) throws Exception {
    try {
    Class<?> interfaceImplementClass = loadClass(className);
    Object interfaceImplement = interfaceImplementClass.newInstance();
    return clazz.cast(interfaceImplement);
    } catch (ClassNotFoundException | InstantiationException
    | ClassCastException | IllegalAccessException e) {
    throw new Exception(e);
    }
    }

    }

    该代码实现不正常的双亲委派逻辑,既能和parent隔离类加载(和宿主),也能通过白名单复用一些宿主的类


    特点


    1)属于多ClassLoader方案


    2)插件可以选择加载宿主的类和绕过宿主加载,选择性强


    结尾


    哈哈,该篇就写到这里(一起体系化学习,一起成长)


    收起阅读 »

    Suspension(挂起/暂停) 在Kotlin coroutines里面到底是如何工作的?

    前言 挂起函数是Kotlin协程的标志。挂起功能也是其中最重要的功能,所有其他功能都建立在此基础上。这也是为什么在这篇文章中,我们目标是深入了解它的工作原理。 挂起一个协程(suspending a coroutine)意味着在其(代码块)执行过程中中断(挂起...
    继续阅读 »

    前言


    挂起函数是Kotlin协程的标志。挂起功能也是其中最重要的功能,所有其他功能都建立在此基础上。这也是为什么在这篇文章中,我们目标是深入了解它的工作原理。


    挂起一个协程(suspending a coroutine)意味着在其(代码块)执行过程中中断(挂起)它。和咱们停止玩电脑单机游戏很类似: 你保存并关闭了游戏,紧接着你和你的电脑又去干其他不同的事儿去了。然后,过了一段时间,你想继续玩游戏。所以你重新打开游戏,恢复之前保存的位置,继续从你之前玩的地方开始玩起了游戏。


    上面所讲的场景是协程的一个形象比喻。他们(任务/一段代码)可以被中断(挂起去执行去他任务),当他们要回来(任务执行完成)的时候,他们通过返回一个Continuation(指定了我们恢复到的位置)。我们可以用它(Continuation)来继续我们的任务从之前我们中断的地方。


    Resume(恢复)


    那么我们来看一下它(Resume)的实际效果。首先,我们需要一个协程代码块。创建协程的最简单方式是直接写一个suspend函数,下面这段代码是我们的起始点:


    suspend fun testCoroutine() {
    println("Before")

    println("After")
    }
    //依次输出
    //Before
    //After


    上面代码很简单:会依次输出“Before”和“After”。这个时候如果我们在两行代码中间挂起的话会发生什么?为了到达挂起的效果,我们可以使用kotlin标准库提供的suspendCoroutine方法:


    suspend fun testCoroutine() {
    println("Before")

    suspendCoroutine<Unit> {

    }

    println("After")
    }

    //依次输出
    //Before

    如果你调用上面的代码,你将不会看到”After“,而且这个代码将会一直运行下去(也就是说我们的testCoroutine方法不会结束)。这个协程在打印完”Before“后就被挂起了。我们的代码快被中断了,而且不会被恢复。所以?我们该怎么做呢?哪里有提到Continuation(可以主动恢复)吗?


    再看一下suspendCoroutine的调用, 而且注意它是以一个lambda表达式结尾。这个方法在挂起前给我们传递了一个参数,它的类型是Continuation


    uspend fun testCoroutine() {
    println("Before")

    suspendCoroutine<Unit> { continuation ->
    println("Before too")
    }

    println("After")
    }

    //依次输出
    //Before
    //Before too

    上面的代码添加了: 在lambda表达式里面调用了另外一个方法, 好吧,这不是啥新鲜事儿。这个就和letapply等类似。suspendCoroutine方法需要这样子设计以便在协程挂起之前就拿到了continuation。如果suspendCoroutine执行了,那就晚了,所以lambda表达式将会在挂起前被调用。这样子设计的好处就是可以在某些时机可以恢复或者存储continuation。so 我们可以让continuation立即恢复


    suspend fun testCoroutine() {
    println("Before")

    suspendCoroutine<Unit> { continuation ->
    continuation.resume(Unit)
    }

    println("After")
    }

    //依次输出
    //Before
    //After

    我们也可以用它来开启一个新的线程,而且还延迟了一会儿才恢复它:


    suspend fun testCoroutine() {
    println("Before")

    suspendCoroutine<Unit> { continuation ->
    thread {
    Thread.sleep(1000)
    continuation.resume(Unit)
    }
    }

    println("After")
    }

    //依次输出
    //Before
    //(1秒以后)
    //After

    这是一个重要的发现。注意,新启动一个线程的代码可以提到一个方法里面,而且恢复可以通过回调来触发。在这种情况下,continuation将被lambda表达式捕获:


    fun invokeAfterSecond(operation: () -> Unit) {
    thread {
    Thread.sleep(1000)
    operation.invoke()
    }
    }

    suspend fun testCoroutine() {
    println("Before")

    suspendCoroutine<Unit> { continuation ->
    invokeAfterSecond {
    continuation.resume(Unit)
    }
    }

    println("After")
    }

    //依次输出
    //Before
    //(1秒以后)
    //After


    这种机制是有效的,但是上面的代码我们没必要通过创建线程来做。线程是昂贵的,所以为啥子要浪费它们?一种更好的方式是设置一个闹钟。在JVM上面,我们可以使用ScheduledExecutorService。我们可以使用它来触发*continuation.resume(Unit)*在一定时间后:


    private val executor = Executors.newSingleThreadScheduledExecutor {
    Thread(it, "scheduler").apply { isDaemon = true }
    }

    suspend fun testCoroutine() {
    println("Before")


    suspendCoroutine<Unit> { continuation ->
    executor.schedule({
    continuation.resume(Unit)
    }, 1000, TimeUnit.MILLISECONDS)
    }

    println("After")
    }

    //依次输出
    //Before
    //(1秒以后)
    //After

    “挂起一定时间后恢复” 看起来像是一个很常用的功能。那我们就把它提到一个方法内,并且我们将这个方法命名为delay


    private val executor = Executors.newSingleThreadScheduledExecutor {
    Thread(it, "scheduler").apply { isDaemon = true }
    }

    suspend fun delay(time: Long) = suspendCoroutine<Unit> { cont ->
    executor.schedule({
    cont.resume(Unit)
    }, time, TimeUnit.MILLISECONDS)
    }

    suspend fun testCoroutine() {
    println("Before")

    delay(1000)

    println("After")
    }

    //依次输出
    //Before
    //(1秒以后)
    //After

    实际上上面的代码就是kotlin协程库delay的具体实现。我们的实现比较复杂,主要是为了支持测试,但是本质思想是一样的。


    Resuming with value(带值恢复)


    有件事可能一直让你感到疑惑:为啥我们调用resume方法的时候传递的是Unit?也有可能你会问为啥子我写suspendCoroutine方法的时候前面也带了Unit类型。实际上这两个是同一类型不是巧合:一个作为continuation恢复的时候入参类型,一个作为suspendCoroutine方法的返回值类型(指定我们要返回什么类型的值),这两个类型要保持一致:


    val ret: Unit =
    suspendCoroutine<Unit> { continuation ->
    continuation.resume(Unit)
    }

    当我们调用suspendCoroutine,我们决定了continuation恢复时候的数据类型,当然这个恢复时候返回的数据也作为了suspendCoroutine方法的返回值:


    suspend fun testCoroutine() {

    val i: Int = suspendCoroutine<Int> { continuation ->
    continuation.resume(42)
    }
    println(i)//42

    val str: String = suspendCoroutine<String> { continuation ->
    continuation.resume("Some text")
    }
    println(str)//Some text

    val b: Boolean = suspendCoroutine<Boolean> { continuation ->
    continuation.resume(true)
    }
    println(b)//true
    }

    上面这些代码好像和咱们之前聊得游戏有点不一样,没有任何一款游戏可以在恢复进度得时候你可以携带一些东西(除非你作弊或者谷歌了下知道下一个挑战是什么)。但是上面代码有返回值的设计方式对于协程来说却意义非凡。我们经常挂起是因为我们需要等待一些数据。比如,我们需要通过API网络请求获取数据,这是一个很常见的场景。一个线程正在处理业务逻辑,处理到某个点的时候,我们需要一些数据才能继续往下执行,这个时候我们通过网络库去请求数据并返回给我们。如果没有协程,这个线程则需要停下来等待。这是一个巨大的浪费---线程资源是非常昂贵的。尤其当这个线程是很重要的线程的时候,就像Android里面的Main Thread。但是有了协程就不一样了,这个网络请求只需要挂起,然后我们给网络请求库传递一个带有自我介绍的continuation:”一旦你获取到数据了,就将他们扔到我的resume方法里面“。然后这个线程就可以去做其他事儿了。一旦数据返回了,当前或其他方法(依赖于我们设置的dispatcher)就会从之前协程挂起的地方继续执行了。


    紧着我们实践一波,通过回调函数来模拟一下我们的网络库:


    data class User(val name: String)

    fun requestUser(callback: (User) -> Unit) {
    thread {
    Thread.sleep(1000)
    callback.invoke(User("hyy"))
    }
    }
    suspend fun testCoroutine() {
    println("Before")

    val user: User =
    suspendCoroutine<User> { continuation ->
    requestUser {
    continuation.resume(it)
    }
    }

    println(user)
    println("After")
    }

    //依次输出
    //Before
    //(1秒以后)
    //User(name=hyy)
    //After

    直接调用suspendCoroutine不是很方便,我们可以抽取一个挂起函数来替代:


    suspend fun requestUser(): User {
    return suspendCoroutine<User> { continuation ->
    requestUser {
    continuation.resume(it)
    }
    }
    }
    suspend fun testCoroutine() {
    println("Before")

    val user = requestUser()

    println(user)
    println("After")
    }

    现在,你很少需要包装回调函数以使其成为挂起函数,因为很多流行库(RetrofitRoom等)都已经支持挂起函数了。但从另方面来讲,我们已经对那些函数的底层实现有了一些了解。它就和我们刚才写的类似。不一样的是,底层使用的是suspendCancellableCoroutine函数(支持取消)。后面我们会讲到。


    suspend fun requestUser(): User {
    return suspendCancellableCoroutine<User> { continuation ->
    requestUser {
    continuation.resume(it)
    }
    }
    }

    你可能想知道如果API接口没给我们返回数据而是抛出了异常,比如服务死机或者返回一些错误。这种情况下,我们不能返回数据,相反我们需要在协程挂起的地方抛出异常。这是我们在异常情况下恢复地方。


    Resume with exception(异常恢复)


    我们调用的每个函数可能返回一些值也可能抛异常。就像suspendCoroutine: 当resume调用的时候返回正常值, 当resumeWithException调用的时候,则会在挂起点抛出异常:


    class MyException : Throwable("Just an exception")

    suspend fun testCoroutine() {

    try {
    suspendCoroutine<Unit> { continuation ->
    continuation.resumeWithException(MyException())
    }
    } catch (e: MyException) {
    println("Caught!")
    }
    }

    //Caught

    这种机制是为了处理各种不同的问题。比如,标识网络异常:


    suspend fun requestUser(): User {
    return suspendCancellableCoroutine<User> { cont ->
    requestUser { resp ->
    if (resp.isSuccessful) {
    cont.resume(resp.data)
    } else {
    val e = ApiException(
    resp.code,
    resp.message
    )
    cont.resumeWithException(e)
    }
    }
    }
    }

    翻译不动了。。。😂, 就差不多到这吧。。


    结尾


    我希望现在您可以从用户的角度清楚的了解挂起(暂停)是如何工作的。Best wishes!


    原文地址:kt.academy/article/cc-…


    作者:老炮儿丶狗二
    链接:https://juejin.cn/post/6999461797140889614
    来源:掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

    ConstraintLayout2.0一篇写不完之极坐标布局与动画

    相对于一般布局方式的笛卡尔坐标系,MotionLayout还拓展了ConstraintLayout中的相对中心布局方式,我们暂且称之为「极坐标布局」方式。 极坐标布局方式在某些场景下,比笛卡尔坐标系的建立更加方便,特别是涉及到一些圆周运动和相对中心点运动的场...
    继续阅读 »

    相对于一般布局方式的笛卡尔坐标系,MotionLayout还拓展了ConstraintLayout中的相对中心布局方式,我们暂且称之为「极坐标布局」方式。


    极坐标布局方式在某些场景下,比笛卡尔坐标系的建立更加方便,特别是涉及到一些圆周运动和相对中心点运动的场景。


    Rotational OnSwipe


    在OnSwipe的基础上,极坐标方式拓展了运动的方向,给dragDirection增加了dragClockwise和dragAnticlockwise参数,用于设置OnSwipe的顺时针滑动和逆时针滑动,这两个属性,在设置rotationCenterId后才会生效。那么借助这个,就可以很方便的实现一些圆形路径的滑动效果和动画。


    通过下面这个例子,我们来看下Rotational OnSwipe的使用方法。


    首先,极坐标的布局还是借助ConstraintLayout,代码如下所示。


    <?xml version="1.0" encoding="utf-8"?>
    <androidx.constraintlayout.motion.widget.MotionLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/motionLayout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#cfc"
    app:layoutDescription="@xml/motion_01_dial_scene"
    app:motionDebug="SHOW_ALL">

    <TextView
    android:id="@+id/number1"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="1"
    android:textSize="24sp"
    app:layout_constraintCircle="@id/dial"
    app:layout_constraintCircleAngle="73"
    app:layout_constraintCircleRadius="112dp"
    app:layout_constraintTag="hop" />

    ......

    <TextView
    android:id="@+id/number0"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="0"
    android:textSize="24sp"
    app:layout_constraintCircle="@id/dial"
    app:layout_constraintCircleAngle="172"
    app:layout_constraintCircleRadius="112dp"
    app:layout_constraintTag="hop" />

    <ImageView
    android:id="@+id/dial"
    android:layout_width="300dp"
    android:layout_height="300dp"
    android:src="@drawable/dial"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintHorizontal_bias="0.6"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTag="center"
    app:layout_constraintTop_toTopOf="parent"
    app:layout_constraintVertical_bias="0.8" />

    <ImageView
    android:id="@+id/dialhook"
    android:layout_width="70dp"
    android:layout_height="70dp"
    android:src="@drawable/dial_hook"
    app:layout_constraintCircle="@id/dial"
    app:layout_constraintCircleAngle="122"
    app:layout_constraintCircleRadius="112dp"
    app:layout_constraintTag="hop" />

    </androidx.constraintlayout.motion.widget.MotionLayout>


    极坐标布局就是借助layout_constraintCircle、layout_constraintCircleAngle、layout_constraintCircleRadius来确定圆心、角度和半径,从而实现极坐标的布局,接下来,再通过OnSwipe来实现圆形滑动效果。


    <?xml version="1.0" encoding="utf-8"?>
    <MotionScene xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:motion="http://schemas.android.com/apk/res-auto"
    motion:defaultDuration="2000">

    <ConstraintSet android:id="@+id/start">
    <Constraint android:id="@+id/dial">
    <Transform android:rotation="0" />
    </Constraint>
    </ConstraintSet>

    <ConstraintSet android:id="@+id/end">
    <Constraint android:id="@+id/dial">
    <Transform android:rotation="300" />
    </Constraint>
    </ConstraintSet>

    <Transition
    motion:autoTransition="animateToStart"
    motion:constraintSetEnd="@+id/end"
    motion:constraintSetStart="@+id/start"
    motion:duration="1000"
    motion:motionInterpolator="easeIn">

    <OnSwipe
    motion:dragDirection="dragClockwise"
    motion:dragScale=".9"
    motion:maxAcceleration="10"
    motion:maxVelocity="50"
    motion:onTouchUp="autoCompleteToStart"
    motion:rotationCenterId="@id/dial" />
    <KeyFrameSet>

    </KeyFrameSet>
    </Transition>
    </MotionScene>

    核心就在OnSwipe中,设置rotationCenterId后,再设置滑动的方向为顺时针即可,展示如下所示。


    image-20302


    Relative Animation


    在MotionLayout中,它进一步加强了在动画中对极坐标运动的支持,特别是一些极坐标的相对运动动画,可以通过MotionLayout,以非常简单的方式表现出来。我们举个简单的例子,一个行星环绕的动画,如下所示。


    image-208867


    我们可以发现,这个动画的轨迹是非常复杂的,太阳以自己为中心自传,地球绕着太阳旋转的同时还在自传,月球绕着地球旋转,卫星绕着地球旋转的同时,逐渐远离地球,靠近月球。


    这样一个复杂的极坐标动画效果,虽然借助ConstraintLayout可以很方便的实现定位布局,但是运动时,却无法继续保持极坐标的依赖关系,所以,这里需要使用MotionLayout来维持运动时的极坐标约束关系。


    首先,使用ConstraintLayout来完成起始布局的建立,代码如下所示。


    <?xml version="1.0" encoding="utf-8"?>
    <androidx.constraintlayout.motion.widget.MotionLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/motionLayout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#FF003b60"
    app:layoutDescription="@xml/motion_01_motion_scene"
    app:motionDebug="SHOW_ALL">

    <ImageView
    android:id="@+id/sun"
    android:layout_width="180dp"
    android:layout_height="180dp"
    android:src="@drawable/sun"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toBottomOf="parent" />

    <TextView
    android:id="@+id/rocket"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="?"
    android:textSize="28sp"
    app:layout_constraintCircle="@id/earth"
    app:layout_constraintCircleAngle="0"
    app:layout_constraintCircleRadius="60dp" />

    <androidx.constraintlayout.utils.widget.ImageFilterView
    android:id="@+id/moon"
    android:layout_width="16dp"
    android:layout_height="16dp"
    android:rotation="-240"
    android:src="@drawable/moon"
    app:layout_constraintCircle="@id/earth"
    app:layout_constraintCircleAngle="0"
    app:layout_constraintCircleRadius="180dp" />

    <androidx.constraintlayout.utils.widget.ImageFilterView
    android:id="@+id/earth"
    android:layout_width="160dp"
    android:layout_height="160dp"
    android:src="@drawable/earth"
    app:layout_constraintCircle="@id/sun"
    app:layout_constraintCircleAngle="315"
    app:layout_constraintCircleRadius="200dp" />

    </androidx.constraintlayout.motion.widget.MotionLayout>

    接下来,在Scene文件中,设置相对运动关系,代码如下所示。


    <ConstraintSet android:id="@+id/start">

    <Constraint android:id="@id/earth">
    <Motion motion:animateRelativeTo="@+id/sun" />
    </Constraint>

    <Constraint android:id="@id/moon">
    <Motion motion:animateRelativeTo="@+id/earth" />
    </Constraint>

    <Constraint android:id="@+id/rocket">
    <Motion
    motion:animateRelativeTo="@+id/earth"
    motion:motionPathRotate="45" />
    </Constraint>
    </ConstraintSet>

    借助animateRelativeTo来实现Motion中的相对中心点,使用motionPathRotate来设置旋转的角度。



    Motion标签中的motionPathRotate和Constraint标签中的transitionPathRotate的作用,都是让其相对于Path旋转一定角度。



    MotionLayout中新增的属性非常多,大家可以参考我的这些文章,从各个方面,逐个击破MotionLayout的各个难点。

    收起阅读 »

    Android分区存储常见问题解答

    要在 Google Play 上发布,开发者需要将应用的 目标 API 级别 (targetSdkVersion) 更新到 API 级别 30 (Android 11) 或者更高版本。针对新上架的应用,这个政策自 8 月开始生效;现有应用更新新的版本,这个政策...
    继续阅读 »

    要在 Google Play 上发布,开发者需要将应用的 目标 API 级别 (targetSdkVersion) 更新到 API 级别 30 (Android 11) 或者更高版本。针对新上架的应用,这个政策自 8 月开始生效;现有应用更新新的版本,这个政策的要求将自 11 月开始生效。


    API 30 所带来的一个巨大变更是,应用需要使用分区存储 (Scoped Storage)。


    变更之大,对于大型应用来说堪称恐怖。更糟糕的是,我们在网上看到的有关如何适配分区存储的建议,有一些建议十分令人迷惑,甚至会误导我们。


    为了帮您排忧解难,我们收集了一些有关分区存储的常见问题,同时也为如何适配您的应用提供了一些建议和可能的替代方案。


    Q: android:requestLegacyStorage 会被移除吗?


    A: 部分移除。


    如果您的应用当前已经设置了 android:requestLegacyStorage="true",就应该在 targetSdkVersion 设置为 30 后保持现状。该标记在 Android 11 设备中没有任何效果,但是可以继续让应用在 Android 10 设备上以旧的方式访问存储。


    如果您需要针对 Android 10 设备在 AndroidManifest.xml 中设置 android:requestLegacyStorage="true",那在应用的目标版本改为 Android 11 后应当保留此设置。它仍会在 Android 10 设备上生效。


    Q: android:preserveLegacyStorage 是如何工作的?


    A: 如果您的应用安装在 Android 10 设备上,并设置了 android:requestLegacyStorage="true",那在设备升级至 Android 11 后,此设置会继续保持旧的存储访问方式。


    ?? 如果应用被卸载,或者是第一次在 Android 11 上安装,那么就无法使用旧的存储访问方式。此标记仅适用于进一步帮助设备从传统存储升级到分区存储。


    Q: 如果我的应用没有访问照片、视频或音频文件,是否仍然需要请求 READ_EXTERNAL_STORAGE 权限?


    A: 不需要,从 Android 11 开始,仅在访问其他应用所属的媒体文件时才需要请求 READ_EXTERNAL_STORAGE 权限。如果您的应用仅使用自身创建的非媒体文件 (或自身创建的媒体文件),那么就不再需要请求该权限。


    如需在 Android 11 后停止请求该权限,仅需修改应用 AndroidManifest.xml 文件中的 <uses-permission> 标签,添加 android:maxSdkVersion="29" 即可:


    <uses-permission
    android:name="android.permission.READ_EXTERNAL_STORAGE"
    android:maxSdkVersion="29" />

    Q: 我想要访问不属于我应用的照片、视频或一段音频,我必须使用系统文件选择器吗?


    A: 不。但如果您想用就可以用,ACTION_OPEN_DOCUMENT 最早可支持至 Android KitKat (API 19),而 ACTION_GET_CONTENT 则支持至 API 1,二者使用的都是系统文件选择器。由于不需要任何权限,这仍然是首选的解决方案。


    如果您不想使用系统文件选择器,您仍然可以请求 READ_EXTERNAL_STORAGE 权限,它会使您的应用可以访问所有的照片、视频以及音频文件,同时也包含访问 File API 的权限!


    如果您需要使用 File API 访问媒体内容,记得设置 android:requestLegacyStorage="true",否则 File API 在 Android 10 中将无法工作。


    Q: 我想保存非媒体文件,但我不想在卸载我的应用时删除它们。我需要使用 SAF 吗?


    A: 也许需要。


    如果这些文件允许在应用外打开而无需通过您的应用,那么系统文件选择器是较好的选择。您可以使用 ACTION_CREATE_DOCUMENT 创建文件。当然也可以使用 ACTION_OPEN_DOCUMENT 来打开一个现有文件。


    如果应用曾经创建了一个目录用于存储所有这些文件,那最好的选择就是使用系统文件选择器和 ACTION_OPEN_DOCUMENT_TREE,以便用户可以选择要使用的特定文件夹。


    如果这些文件只对您的应用有意义,可以考虑在应用 AndroidManifest.xml 文件的 <application> 标签中设置 android:hasFragileUserData="true"。这将使用户可以保留这些数据,即使在卸载应用时亦是如此。


    上图为拥有 "脆弱用户数据" 应用的卸载对话框。对话框中包含了一个复选框,用于指示系统是否应该保留应用数据。


    △ 上图为拥有 "脆弱用户数据" 应用的卸载对话框。对话框中包含了一个复选框,用于指示系统是否应该保留应用数据。


    设置了该标记后,存储文件的最佳位置将取决于其内容。包含敏感或私人信息的文件应当存储在 Context#getFilesDir() 所返回的目录中;而不敏感的数据则应存储于 Context#getExternalFilesDir() 所返回的目录中。


    Q: 我可以将非媒体文件放置于其他文件夹中 (例如 Downloads 文件夹),而无需任何权限。这是一个 Bug 吗?


    A: 不是。应用可能会向这类集合提供文件,而且最好的方式是对非媒体文件同时使用 Downloads 和 Documents 集合。不过请记得,默认情况下只有创建该文件的应用才可以访问它们。其他应用需要通过系统文件选择器获得访问权限或者拥有对外部存储的广泛访问权限 (即: MANAGE_EXTERNAL_STORAGE 权限) 才行。


    ?? 对 MANAGE_EXTERNAL_STORAGE 权限的访问受到 Play 政策 监管。


    Q: 如果我需要保存一个文档,是否需要使用 SAF?


    A: 不用。应用可以向 Documents 与 Downloads 集合提供非媒体文件,而无需任何特殊权限。只要没被卸载,那么向这些集合提供文档的应用拥有这些文档的完全访问权限。


    ? 如果您的应用为了上面提到的方式保存文档而请求 READ_EXTERNAL_STORAGE 权限的话,在 Android 11 及更高版本中将不必再请求该权限。您可以参考下面的示例修改对该权限的请求 (设定 maxSdkVersion 为 API 版本 29):


    <uses-permission
    android:name="android.permission.READ_EXTERNAL_STORAGE"
    android:maxSdkVersion="29" />

    如要访问其他应用添加的文档,或者在您的应用卸载重装后访问其卸载前添加的文档,就需要通过 ACTION_OPEN_DOCUMENT Intent 来使用系统文件选择器。


    Q: 我想要与其他应用共享文件,是否需要使用 SAF?


    A: 不需要。如下是一些与其他应用共享文件的方式:



    • 直接分享: 使用 Intent.ACTION_SEND 可以让您的用户通过各种格式与设备上的其他应用共享数据。如果您使用这种方式,使用 AndroidX 的 FileProvider 来将 file:// Uri 自动转换为 content:// Uri 可能会对您有所帮助。

    • 创建您自己的 DocumentProvider: 这可以让您的应用继续处理应用的私有目录 ( Context#getFilesDirs() 或 Context#getExternalFilesDirs()) 中内容的同时,仍可以向使用系统文件选择器的其他应用提供访问权限。(请注意,可以在卸载应用后继续保存这些文件——参阅上文中的 android:hasFragileUserData="true" 设置来了解其使用方式。)


    最后的思考


    Scoped Storage 是一项旨在改善用户隐私保护的重大变更。不过仍然有很多方法可以处理不依赖使用存储访问框架 (Storage Access Framework) 的内容。


    如果要存储的数据仅适用于您的应用,那么我们强烈建议使用 应用特定目录


    如果数据是媒体文件,例如照片、视频或者音频,那么可以 使用 MediaStore。注意,从 Android 10 开始,提供内容 不再需要请求权限


    也别忘了可以通过 ACTION_SEND 来与 其他应用共享数据 (或允许它们 与您的应用共享数据)!


    收起阅读 »

    一文读懂 Android 主流屏幕适配方案

    公众号:字节数组,希望对你有所帮助 ?? 关于 Android 的屏幕适配现在已经有很多成熟的方案了,已经不是一个热门话题了。印象中 2018 年是讨论适配方案最火热的一段时间,那时候字节跳动技术团队发文介绍了其适配方案,之后就带动起了很多位大佬陆续发表...
    继续阅读 »

    公众号:字节数组,希望对你有所帮助 ??



    关于 Android 的屏幕适配现在已经有很多成熟的方案了,已经不是一个热门话题了。印象中 2018 年是讨论适配方案最火热的一段时间,那时候字节跳动技术团队发文介绍了其适配方案,之后就带动起了很多位大佬陆续发表文章进行讨论。当时我刚工作不久,在面试时也有被问到过关于屏幕适配的问题,因为对于一些概念认识不清导致回答得并不好 ?? 所以本篇文章就想要从头到尾讲清楚关于屏幕适配的主要知识点,希望对你有所帮助,有错误也希望读者能够指出来 ??


    一、ppi & dpi


    关于屏幕适配有两个绕不开的概念:ppi 和 dpi,两者在含义上很类似,很容易混淆,但其实是属于不同领域上的概念


    ppi


    ppi(Pixels Per Inch)即像素密度,指每英寸包含的物理像素的数量。ppi 是设备在物理上的属性值,取决于屏幕自身,计算公式如下所示。被除数和除数都属于客观不可改变的值,所以 ppi 也是无法修改的,是硬件上一个客观存在无法改变的值



    dpi


    dpi(Dots Per Inch)原先用于在印刷行业中描述每英寸包含有多少个点,在 Android 开发中则用来描述屏幕像素密度。屏幕像素密度决定了在软件概念上单位距离对应的像素总数,是手机在出厂时就会被写入系统配置文件中的一个属性值,一般情况下用户是无法修改该值的,但在开发者模式中有修改该值的入口,是软件上一个可以修改的值


    我们知道,在不同手机屏幕上 1 dp 所对应的 px 值可能是会有很大差异的。例如,在小屏幕手机上 1 dp 可能对应 1 px,在大屏幕手机上对应的可能是 3 px,这也是我们实现屏幕适配的基础原理。决定了在特定一台手机上 1 dp 对应多少 px 的正是该设备的 dpi 值,这可以通过 DisplayMetrics 来获取


    val displayMetrics = applicationContext.resources.displayMetrics
    Log.e("TAG", "densityDpi: " + displayMetrics.densityDpi)
    Log.e("TAG", "density: " + displayMetrics.density)
    Log.e("TAG", "widthPixels: " + displayMetrics.widthPixels)
    Log.e("TAG", "heightPixels: " + displayMetrics.heightPixels)

    TAG: densityDpi: 480
    TAG: density: 3.0
    TAG: widthPixels: 1080
    TAG: heightPixels: 2259

    从中就可以提取出几点信息:



    1. 屏幕像素密度为 480 dpi

    2. density 等于 3,说明在该设备上 1 dp 等于 3 px

    3. 屏幕宽高大小为 2259 x 1080 px,即 753 x 360 dp


    Android 系统定义的屏幕像素密度基准值是 160 dpi,该基准值下 1 dp 就等于 1 px,依此类推 480 dpi 下 1 dp 就等于 3 px,计算公式:


    px = dp * (dpi / 160)

    不同屏幕像素密度的设备就对应了不同的配置限定符。例如,在 320 到 480 dpi 之间的设备就对应 xxhdpi,该类型设备在取图片时就会优先从 drawable-xxhdpi 文件夹下取



    二、为什么要适配


    不管我们在布局文件中使用的是什么单位,最终系统在使用时都需要将其转换为 px,由于不同手机的屏幕像素尺寸会相差很大,我们自然不能在布局文件中直接使用 px 进行硬编码。因此 Google 官方也推荐开发者尽量使用 dp 作为单位值,因为系统会根据屏幕的实际情况自动完成 dp 与 px 之间的对应换算


    举个例子。假设设计师给出来的设计稿是按照 1080 x 1920 px,420 dpi 的标准来进行设计的,那么设计稿的宽高即 411 x 731 dp,那对于一个希望占据屏幕一半宽度的 ImageView 来说,在设计稿中的宽即 205.5 dp


    那么,对于一台 1440 x 2880 px,560 dpi 的真机来说,其宽高即 411 x 822 dp,此时我们在布局文件中就可以直接使用设计稿中给出来的宽度值,使得 ImageView 在这台真机上也占据了屏幕一半宽度。虽然设计稿和真机的屏幕像素并不相同,但由于屏幕像素密度的存在,使得两者的 dp 宽度是一样的,从而令开发者可以只使用同一套 dp 尺寸值就完成设计要求了


    既然有了 dp,那我们为什么还需要进行屏幕适配呢?当然也是因为 dp 只适用于大部分正常情况了。以上情况之所以能够完美适配,那也是因为举的例子刚好也是完美的:1440 / 1080 = 560 / 420 = 1.3333,设计稿和真机的 px 宽度和 dp 宽度刚好具有相同比例,此时使用 dp 才能刚好适用


    再来看一个不怎么完美的例子。以两台真机为例:



    • 华为 nova5:1080 x 2259 px,480 dpi,屏幕宽度为 1080 / (480 / 160) = 360 dp

    • 三星 Galaxy S10:1080 x 2137 px,420 dpi,屏幕宽度为 1080 / (420 / 160) = 411 dp


    可以看到,在像素宽度相同的情况下,不同手机的像素密度是有可能不一样的。手机厂家有可能是根据屏幕像素和屏幕尺寸来共同决定该值的大小,但不管怎样,这就造成了应用的实际效果与设计稿之间无法对应的情况:对于一个 180 dp 宽度的 View 来说,在华为 nova5 上能占据一半的屏幕宽度,但在三星 Galaxy S10 上却只有 180 / 411 = 0.43,这就造成了一定偏差


    以上情况就是直接使用 dp 值无法解决的问题,使用 dp 只能适配大部分宽高比例比较常规的机型,对于特殊机型就无能为力了……


    屏幕适配就是要来解决上述问题。对于屏幕适配,开发者希望实现的效果主要有两个:



    • 在声明宽高值时,能够直接套用设计稿上给出来的尺寸值,这个尺寸值映射到项目中可能是对应一个具体的值,也可能是对应多套 dimens 文件中的值,但不管是哪一种,在开发阶段都希望能够直接套用而无需再来进行手动计算。这关乎进行屏幕适配的效率

    • 适配后的界面最终在不同屏幕上的空间比例都能保持一致。这关乎进行屏幕适配的最终成效


    下面就来介绍三种当前比较主流或曾经是主流的的适配方案 ~~


    三、今日头条方案


    字节跳动技术团队曾经发布过一篇文章介绍了其适配方案:一种极低成本的Android屏幕适配方式


    其适配思路基于以下几条换算公式:



    • px = density * dp

    • density = dpi / 160

    • px = dp * (dpi / 160)


    在布局文件中声明的 dp 值,最终都需要通过 TypedValue 的 applyDimension 方法来转换为 px,转换公式即:density * dp


        public static float applyDimension(int unit, float value, DisplayMetrics metrics) {
    switch (unit) {
    case COMPLEX_UNIT_PX:
    return value;
    case COMPLEX_UNIT_DIP:
    return value * metrics.density;
    case COMPLEX_UNIT_SP:
    return value * metrics.scaledDensity;
    case COMPLEX_UNIT_PT:
    return value * metrics.xdpi * (1.0f/72);
    case COMPLEX_UNIT_IN:
    return value * metrics.xdpi;
    case COMPLEX_UNIT_MM:
    return value * metrics.xdpi * (1.0f/25.4f);
    }
    return 0;
    }

    那么,如果我们能够动态修改 density 值的大小,要求修改后计算出的屏幕宽度就等于设计稿的宽度,不就可以在布局文件中直接使用设计稿给出的各个 dp 宽高值,且使得 View 在不同手机屏幕上都能占据同样的比例吗?


    举个例子,假设设计师给出来的设计稿是按照 **1080 x 1920 px,density 2.625,420 dpi ** 的标准来进行设计的,设计稿的宽高即 411 x 731 dp。那么对于一个宽度为 100 dp 的 View,占据设计稿的宽度比例是:100 * 2.625 / 1080 = 0.2430


    用以下两台真机的数据为例,在适配前:



    • 华为 nova5:1080 x 2259 px,480 dpi。正常情况下其 density 为 3,View 占据的屏幕宽度比例是:100 x 3 / 1080 = 0.2777

    • Pixel 2 XL:1440 x 2800 px,560 dpi。正常情况下其 density 为 3.5,View 占据的屏幕宽度比例是:100 x 3.5 / 1440 = 0.2430


    采用字节跳动技术团队的方案动态改变 density 进行适配,适配后的 density = 设备真实宽度(单位 px) / 设计稿的宽度(单位 dp):



    • 华为 nova5:适配后 density 变成 1080 / 411 = 2.6277,View 占据的屏幕宽度比例是:100 x 2.6277 / 1080 = 0.2433

    • Pixel 2 XL:适配后 density 变成 1440 / 411 = 3.5036,View 占据的屏幕宽度比例是:100 x 3.5036 / 1440 = 0.2433


    可以看出来,虽然由于除法运算会导致一点点精度丢失,但完全可以忽略不计,只要我们能动态改变手机的 density,最终 View 在宽度上就都能保持和设计稿完全相同的比例了


    实际上 density 只是 DisplayMetrics 类中的一个 public 变量,不涉及任何私有 API,修改后理论上也不会影响到应用的稳定性。因此,只要我们在 Activity 的 onCreate 方法中完成对 density 和 densityDpi 的修改,我们就可以在布局文件中直接使用设计稿给出的 dp 值,不用准备多套 dimens 就能完成适配,十分简洁


        fun setCustomDensity(activity: Activity, application: Application, designWidthDp: Int) {
    val appDisplayMetrics = application.resources.displayMetrics
    val targetDensity = 1.0f * appDisplayMetrics.widthPixels / designWidthDp
    val targetDensityDpi = (targetDensity * 160).toInt()
    appDisplayMetrics.density = targetDensity
    appDisplayMetrics.densityDpi = targetDensityDpi
    val activityDisplayMetrics = activity.resources.displayMetrics
    activityDisplayMetrics.density = targetDensity
    activityDisplayMetrics.densityDpi = targetDensityDpi
    }

    override fun onCreate(savedInstanceState: Bundle?) {
    setCustomDensity(this, application, 420)
    super.onCreate(savedInstanceState)
    }


    字节跳动技术团队的文章只给出了示例代码,并没有给出最终落地可用的代码,但在 GitHub 上有一个挺出名的落地实践库,读者值得一看:AndroidAutoSize



    四、宽高限定符


    宽高限定符是系统原生支持的一种适配方案,通过穷举市面上所有 Android 手机的屏幕像素尺寸来实现适配。实现思路很简单,就是通过比例换算来为不同分辨率的屏幕分别生成一套 dimens 文件


    首先,以设计稿的尺寸作为基准分辨率,假设设计稿是 1920 x 1080 px,那么就可以先生成默认的 dimens 文件,生成规则:



    • 将屏幕宽度均分为 1080 份,每份 1 px,声明 1080 个 key 值,值从 1 px 开始递增,每次递增 1 px

    • 将屏幕高度均分为 1920 份,每份 1 px,声明 1920 个 key 值,值从 1 px 开始递增,每次递增 1 px


    最终 dimens 文件就像以下这样:


    <resources>
    <dimen name="x1">1px</dimen>
    <dimen name="x2">2px</dimen>
    ···
    <dimen name="x1080">1080px</dimen>

    <dimen name="y1">1px</dimen>
    <dimen name="y2">2px</dimen>
    ···
    <dimen name="y1920">1920px</dimen>
    </resources>

    类似地,再来为屏幕尺寸为 1440 x 720 px 的手机生成专属的 dimens 文件,生成规则:



    • 将屏幕宽度均分为 1080 份,每份 720 / 1080 = 0.666 px,声明 1080 个 key 值,值从 0.666 px 开始递增,每次递增 0.666 px

    • 将屏幕高度均分为 1920 份,每份 1440 / 1920 = 0.75 px,声明 1920 个 key 值,值从 0.75 px 开始递增,每次递增 0.75 px


    最终 dimens 文件就像以下这样:


    <resources>
    <dimen name="x1">0.666px</dimen>
    <dimen name="x2">1.332px</dimen>
    ···
    <dimen name="x1080">720px</dimen>

    <dimen name="y1">0.75px</dimen>
    <dimen name="y2">1.5px</dimen>
    ···
    <dimen name="y1920">1440px</dimen>
    </resources>

    最终,为市面上主流的屏幕尺寸均按照如上规则生成一套专属的 dimens 文件,每套文件均放到以像素尺寸进行命名的 value 文件夹下,就像以下这样:


    values
    values-1440x720
    values-1920x1080
    values-2400x1080
    values-2408x1080
    values-2560x1440

    之后,我们就可以直接套用设计稿中的像素尺寸进行开发了,设计稿写的是 100 x 200 px,那么我们在布局文件中就可以直接引用 x100 和 y200。当应用运行在不同分辨率的手机中时,应用会自动去引用相同分辨率的 dimens 文件,此时引用到的实际 px 值具有和设计稿相同的比例大小,这样就实现了适配需求了


    需要注意,宽高限定符方案有一个致命缺陷:需要精准命中分辨率才能实现适配。比如 1920 x 1080 px 的手机就一定要引用到 values-1920x1080文件夹内的 dimens 文件,否则就只能去引用默认的 values 文件夹,此时引用到的尺寸值就有可能和实际需求有很大出入,从而导致界面变形。而对于市面上层出不穷的各种分辨率,开发者想穷举完其实很麻烦,所以说,宽高限定符方案的容错率很低


    五、smallestWidth


    smallestWidth 也是系统原生支持的一种适配方案。smallestWidth 即最小宽度,指的是最短的那一个边长,而不考虑屏幕的方向,适配原理和宽高限定符方案一样,本质上都是通过比例换算来为不同尺寸的屏幕分别准备一套 dimens 文件,应用在运行时再去引用相匹配的 dimens 文件,以此来实现屏幕适配


    首先,我们要以设计稿的尺寸作为基准分辨率,假设设计师给出来的设计稿是按照 **1080 x 1920 px **的标准来进行设计的,那么基准分辨率就是设计稿的宽度 1080 px


    先为宽度为 360 dp 的设备生成 dimens 文件,生成规则:



    • 将 360 dp 均分为 1080 份,每份 360 / 1080 dp,声明 1080 个 key 值,值从 360 / 1080 dp 开始递增,每次递增 360 / 1080 dp


    最终 dimens 文件就像以下这样:


    <?xml version="1.0" encoding="utf-8"?>
    <resources>
    <dimen name="DIMEN_1PX">0.33dp</dimen>
    <dimen name="DIMEN_2PX">0.67dp</dimen>
    ···
    <dimen name="DIMEN_1078PX">359.33dp</dimen>
    <dimen name="DIMEN_1079PX">359.67dp</dimen>
    <dimen name="DIMEN_1080PX">360.00dp</dimen>
    </resources>

    类似地,我们再按照上述规则为宽度为 380 dp 的设备生成 dimens 文件:


    <?xml version="1.0" encoding="utf-8"?>
    <resources>
    <dimen name="DIMEN_1PX">0.35dp</dimen>
    <dimen name="DIMEN_2PX">0.70dp</dimen>
    ···
    <dimen name="DIMEN_1078PX">379.30dp</dimen>
    <dimen name="DIMEN_1079PX">379.65dp</dimen>
    <dimen name="DIMEN_1080PX">380.00dp</dimen>
    </resources>

    最终,为市面上主流的屏幕宽度均按照如上规则生成一套专属的 dimens 文件,每套文件均放到以宽度进行命名的 value 文件夹内,就像以下这样:


    values
    values-sw360dp
    values-sw380dp
    values-sw400dp
    values-sw420dp

    这样,我们就可以直接在布局文件中套用设计稿的 px 值了,应用在运行时就会自动去匹配最符合当前屏幕宽度的资源文件。例如,如果我们引用了 DIMEN_1080PX,那么不管是在宽度为 360 dp 还是 380 dp 的设备中,该引用对应的 dp 值都是刚好占满屏幕宽度,这样就实现了适配需求了


    smallestWidth 方案和宽高限定符方案最大的差别就在于容错率,smallestWidth 方案具有很高的容错率,即使应用中没有找到符合当前屏幕宽度的 dimens 文件,应用也会向下寻找并采用最接近当前屏幕宽度的 dimens 文件,只有都找不到时才会去引用默认的 dimens 文件。只要我们准备的 dimens 足够多,且每套 dimens 文件以 5 ~ 10 dp 作为步长递增,那么就能够很好地满足市面上的绝大部分手机了。此外,我们不仅可以使用设计稿的 px 宽度作为基准分辨率,也可以改为使用 dp 宽度,计算规则还是保持一致


    六、总结


    以上介绍的三种方案各有特点,这里来做个总结



    • 今日头条方案。优点:可以直接使用设计稿中的 dp 值,无需生成多套 dimens 文件进行映射,因此不会增大 apk 体积。此外,此方案的 UI 还原度在三种方案中应该是最高的了,其它两种方案都需要精准命中屏幕尺寸后才能达到此方案的还原度。缺点:由于此方案会影响到应用全局,因此如果我们引入了一些第三方库的话,三方库中的界面也会随之被影响到,可能会造成效果变形,此时就需要进行额外处理了

    • 宽高限定符方案。容错率太低,且需要准备很多套 dimens 文件,在 Android 刚兴起,屏幕类型还比较少的时候比较吃香,目前应该已经很少有项目采用此方案了,读者可以直接忽略

    • smallestWidth 方案。优点:容错率高,在 320 ~ 460 dp 之间每 10 dp 就提供一套 dimens 文件就足够使用了,想要囊括更多设备的话也可以再缩短步长,基本不用担心最终效果会与设计稿相差太多,且此方案不会影响到三方库。缺点:需要生成多套 dimens 文件,增大了 apk 体积


    需要强调下,以上三种方案其实都存在一个问题:我们只能实现对单个方向的适配,无法同时兼顾宽高。之所以只能单个方向,是因为当前手机屏幕的宽高比并不是按照一个固定的比例进行递增的,4 : 3、16 : 9、甚至其它宽高比都有,这种背景下我们要达到百分百还原设计稿是不现实的,我们只能选择一个维度来进行适配。幸运的是大部分情况下我们也只需要根据屏幕宽度来进行适配,以上方案已经能够满足我们绝大多数时候的开发需求了。对于少部分需要根据高度进行适配的页面,今日头条方案可以很灵活的进行切换,smallestWidth 方案就比较麻烦了,此时可以通过 ConstraintLayout 来精准按比例控制控件的宽高大小或者是位置,同样也能达到适配要求 ~


    此外,我看到网络上很多开发者都在说 dpi 的存在就是为了让大屏幕手机能够显示更多内容,屏幕适配导致 dpi 失去了其原有的意义,但我其实并不理解这和屏幕适配有什么关系。现在的现实背景就是存在某些屏幕像素宽度相同的手机,其 dpi 却不一样,如果单纯直接使用 dp 而不进行额外适配的话,那在这类机型下控件就会相比设计稿多出一些空白或者是超出屏幕范围,这是开发者不得不解决的问题。如果说显示更多内容指的是让控件在大屏幕手机上能够占据更多的物理空间,那么前提也是要让各个控件的大小和位置都符合设计稿的要求,屏幕适配要做到的就是这一点,同等比例下控件在大屏幕手机上自然就会有更多物理空间。而如果说显示更多内容指的是当在大屏幕手机上有剩余空间时就相比小屏幕多显示其它控件,那么我觉得不仅开发要疯,设计师都要疯……


    最后,这里再提供一份用于生成 dimens 文件的代码,基于 smallestWidth 方案,代码总的不到一百行,实现思路在前文讲的很清楚了。仅需要填入设计稿的宽高像素大小就可以,默认基于 1080 x 1920 px 的设计稿,生成范围从 320 到 460 dp 之间,步长 10 dp,读者可以按需调整



    有需要的同学自取:SmallestWidthGenerator

    收起阅读 »

    WMS在Activity启动中的职责 添加窗体(三)

    Context 获取系统服务在正式聊WMS之前,我们先来看看context.getSystemService其核心原理,才能找到WindowManager的实现类: @Override public Object getSystemService...
    继续阅读 »

    Context 获取系统服务

    在正式聊WMS之前,我们先来看看context.getSystemService其核心原理,才能找到WindowManager的实现类:

        @Override
    public Object getSystemService(String name) {
    return SystemServiceRegistry.getSystemService(this, name);
    }
    private static final HashMap<String, ServiceFetcher<?>> SYSTEM_SERVICE_FETCHERS =
    new HashMap<String, ServiceFetcher<?>>();

    public static Object getSystemService(ContextImpl ctx, String name) {
    ServiceFetcher<?> fetcher = SYSTEM_SERVICE_FETCHERS.get(name);
    return fetcher != null ? fetcher.getService(ctx) : null;
    }

    能看到是实际上所有的我们通过Context获取系统服务,是通过SYSTEM_SERVICE_FETCHERS这个提前存放在HashMap的服务集合中。这个服务是在静态代码域中提前注册。

            registerService(Context.WINDOW_SERVICE, WindowManager.class,
    new CachedServiceFetcher<WindowManager>() {
    @Override
    public WindowManager createService(ContextImpl ctx) {
    return new WindowManagerImpl(ctx);
    }});

    能看到此时实际上WindowManager的interface是由WindowManagerImpl实现的。

    这里先上一个WindowManager的UML类图。

    image.png

    我们能够从这个UML图能够看到,其实所有的事情都委托给WindowManagerGlobal工作。因此我们只需要看WindowManagerGlobal中做了什么。

    因此我们要寻求WindowManager的addView的方法,实际上就是看WindowManagerGlobal的addView方法。

    public void addView(View view, ViewGroup.LayoutParams params,
    Display display, Window parentWindow) {
    ...
    final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams) params;
    if (parentWindow != null) {
    parentWindow.adjustLayoutParamsForSubWindow(wparams);
    } else {
    // If there's no parent, then hardware acceleration for this view is
    // set from the application's hardware acceleration setting.
    final Context context = view.getContext();
    if (context != null
    && (context.getApplicationInfo().flags
    & ApplicationInfo.FLAG_HARDWARE_ACCELERATED) != 0) {
    wparams.flags |= WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED;
    }
    }

    ViewRootImpl root;
    View panelParentView = null;

    synchronized (mLock) {
    // Start watching for system property changes.
    ...
    int index = findViewLocked(view, false);
    if (index >= 0) {
    if (mDyingViews.contains(view)) {
    // Don't wait for MSG_DIE to make it's way through root's queue.
    mRoots.get(index).doDie();
    } else {
    throw new IllegalStateException("View " + view
    + " has already been added to the window manager.");
    }
    // The previous removeView() had not completed executing. Now it has.
    }

    // If this is a panel window, then find the window it is being
    // attached to for future reference.
    if (wparams.type >= WindowManager.LayoutParams.FIRST_SUB_WINDOW &&
    wparams.type <= WindowManager.LayoutParams.LAST_SUB_WINDOW) {
    final int count = mViews.size();
    for (int i = 0; i < count; i++) {
    if (mRoots.get(i).mWindow.asBinder() == wparams.token) {
    panelParentView = mViews.get(i);
    }
    }
    }

    root = new ViewRootImpl(view.getContext(), display);

    view.setLayoutParams(wparams);

    mViews.add(view);
    mRoots.add(root);
    mParams.add(wparams);

    // do this last because it fires off messages to start doing things
    try {
    root.setView(view, wparams, panelParentView);
    } catch (RuntimeException e) {
    // BadTokenException or InvalidDisplayException, clean up.
    if (index >= 0) {
    removeViewLocked(index, true);
    }
    throw e;
    }
    }
    }

    这里能够看到一个新的addView的时候,会找到是否有父Window。没有则继续往后走,判断新建窗体的type是否是子窗口类型,是则查找传进来的Binder对象和存储在缓存中的Binder对象又没有对应的Window。有则作为本次新建窗口的复窗口。

    最后能够看到我们熟悉的类ViewRootImpl。这个类可以说是所有View绘制的根部核心,这个类会在后面View绘制流程聊聊。最后会调用ViewRootImpl的setView进一步的沟通系统应用端。

    这里涉及到了几个有趣的宏,如WindowManager.LayoutParams.FIRST_SUB_WINDOW 。它们象征这当前Window处于什么层级。

    Window的层级

    Window的层级,我们大致可以分为3大类:System Window(系统窗口),Application Window(应用窗口),Sub Window(子窗口)

    Application Window(应用窗口)

    Application值得注意的有这么几个宏:

    type描述
    FIRST_APPLICATION_WINDOW = 1应用程序窗口初始值
    TYPE_BASE_APPLICATION = 1应用窗口类型初始值,其他窗口以此为基准
    TYPE_APPLICATION = 2普通应用程序窗口类型
    TYPE_APPLICATION_STARTING = 3应用程序的启动窗口类型,不是应用进程支配,当第一个应用进程诞生了启动窗口就会销毁
    TYPE_DRAWN_APPLICATION = 4应用显示前WindowManager会等待这种窗口类型绘制完毕,一般在多用户使用
    LAST_APPLICATION_WINDOW = 99应用窗口类型最大值

    因此此时我们能够清楚,应用窗口的范围在1~99之间。

    Sub Window(子窗口)

    type描述
    FIRST_SUB_WINDOW = 1000子窗口初始值
    TYPE_APPLICATION_PANEL = FIRST_SUB_WINDOW应用的panel窗口,在父窗口上显示
    TYPE_APPLICATION_MEDIA = FIRST_SUB_WINDOW + 1多媒体内容子窗口,在父窗口之下
    TYPE_APPLICATION_SUB_PANEL = FIRST_SUB_WINDOW + 2也是一种panel子窗口,位于所有TYPE_APPLICATION_PANEL之上
    TYPE_APPLICATION_ATTACHED_DIALOG = FIRST_SUB_WINDOW + 3dialog弹窗
    TYPE_APPLICATION_MEDIA_OVERLAY = FIRST_SUB_WINDOW + 4多媒体内容窗口的覆盖层
    TYPE_APPLICATION_ABOVE_SUB_PANEL = FIRST_SUB_WINDOW + 5位于子panel之上窗口
    LAST_SUB_WINDOW = 1999子窗口类型最大值

    能够看到子窗口的范围从1000~1999

    System Window(系统窗口)

    type描述
    FIRST_SYSTEM_WINDOW = 2000系统窗口初始值
    TYPE_STATUS_BAR = FIRST_SYSTEM_WINDOW系统状态栏
    TYPE_SEARCH_BAR = FIRST_SYSTEM_WINDOW+1搜索条窗口
    TYPE_PHONE = FIRST_SYSTEM_WINDOW+2通话窗口
    TYPE_SYSTEM_ALERT = FIRST_SYSTEM_WINDOW+3alert窗口,电量不足时警告
    TYPE_KEYGUARD = FIRST_SYSTEM_WINDOW+4屏保窗口
    TYPE_TOAST = FIRST_SYSTEM_WINDOW+5Toast提示窗口
    TYPE_SYSTEM_OVERLAY = FIRST_SYSTEM_WINDOW+6系统覆盖层窗口,这个层不会响应点击事件
    TYPE_PRIORITY_PHONE = FIRST_SYSTEM_WINDOW+7电话优先层,在屏保状态下显示通话
    TYPE_SYSTEM_DIALOG = FIRST_SYSTEM_WINDOW+8系统层级的dialog,比如RecentAppDialog
    TYPE_KEYGUARD_DIALOG= FIRST_SYSTEM_WINDOW+9屏保时候对话框(如qq屏保时候的聊天框)
    TYPE_SYSTEM_ERROR= FIRST_SYSTEM_WINDOW+10系统错误窗口
    TYPE_INPUT_METHOD= FIRST_SYSTEM_WINDOW+11输入法窗口
    TYPE_INPUT_METHOD_DIALOG= FIRST_SYSTEM_WINDOW+12输入法窗口上的对话框
    TYPE_WALLPAPER= FIRST_SYSTEM_WINDOW+13壁纸窗口
    TYPE_STATUS_BAR_PANEL = FIRST_SYSTEM_WINDOW+14滑动状态栏窗口
    LAST_SYSTEM_WINDOW = 2999系统窗口最大值

    常见的系统级别窗口主要是这几个。能够注意到系统窗口层级是从2000~2999。

    这些层级有什么用的?这些层级会作为参考,将会插入到显示栈的位置,层级值越高,越靠近用户。这个逻辑之后会聊到。

    ViewRootImpl setView

    ViewRootImpl里面包含了许多事情,主要是包含了我们熟悉的View的绘制流程,以及添加Window实例的流程。

    本文是关于WMS,因此我们只需要看下面这个核心函数

    这个方法有两个核心requestLayout以及addToDisplay。

    • 1.requestLayout实际上就是指View的绘制流程,并且最终会把像素数据发送到Surface底层。
    • 2.mWindowSession.addToDisplay 添加Window实例到WMS中。

    WindowManager的Session设计思想

    先来看看Session类:

    class Session extends IWindowSession.Stub implements IBinder.DeathRecipient

    得知此时Session实现了一个IWindowSession的Binder对象。并且实现了Binder的死亡监听。

    那么这个Session是从哪里来的呢?实际上是通过WMS通过跨进程通信把数据这个Binder对象传递过来的:

        @Override
    public IWindowSession openSession(IWindowSessionCallback callback, IInputMethodClient client,
    IInputContext inputContext) {
    if (client == null) throw new IllegalArgumentException("null client");
    if (inputContext == null) throw new IllegalArgumentException("null inputContext");
    Session session = new Session(this, callback, client, inputContext);
    return session;
    }

    通着这种方式,就能把一个Session带上WMS相关的环境送给客户端操作。这种方式和什么很相似,实际上和servicemanager查询服务Binder的思路几乎一模一样。

    @Override
    public int addToDisplay(IWindow window, int seq, WindowManager.LayoutParams attrs,
    int viewVisibility, int displayId, Rect outFrame, Rect outContentInsets,
    Rect outStableInsets, Rect outOutsets,
    DisplayCutout.ParcelableWrapper outDisplayCutout, InputChannel outInputChannel) {
    return mService.addWindow(this, window, seq, attrs, viewVisibility, displayId, outFrame,
    outContentInsets, outStableInsets, outOutsets, outDisplayCutout, outInputChannel);
    }

    很有趣的是,我们能够看到,按照道理我们需要添加窗体实例到WMS中。从逻辑上来讲,我们只需要做一次跨进程通信即可。但是为什么需要一个Session作为中转站呢?

    image.png

    能够看到实际上Session(会话)做的事情不仅仅只有沟通WMS这么简单。实际上它还同时处理了窗口上的拖拽,输入法等逻辑,更加重要的是Session面对着系统多个服务,但是通过这个封装,应用程序只需要面对这个Sesion接口,真的是名副其实的"会话"。

    这种设计想什么?实际上就是我们常说的门面设计模式。

    IWindow对象

    注意,这里面除了IWindowSession之外,当我们调用addWindow添加Window到WMS中的时候,其实还存在一个IWindow接口.这个IWindow是指PhoneWindow吗?

    很遗憾。并不是。PhoneWindow基础的接口只有Window接口。它并不是一个IBinder对象。我们转过头看看ViewRootImpl.

    public ViewRootImpl(Context context, Display display) {
    mContext = context;
    mWindowSession = WindowManagerGlobal.getWindowSession();
    mDisplay = display;
    mBasePackageName = context.getBasePackageName();
    mThread = Thread.currentThread();
    mLocation = new WindowLeaked(null);
    mLocation.fillInStackTrace();
    mWidth = -1;
    mHeight = -1;
    mDirty = new Rect();
    mTempRect = new Rect();
    mVisRect = new Rect();
    mWinFrame = new Rect();
    mWindow = new W(this);
    mTargetSdkVersion = context.getApplicationInfo().targetSdkVersion;
    mViewVisibility = View.GONE;
    mTransparentRegion = new Region();
    mPreviousTransparentRegion = new Region();
    mFirst = true; // true for the first time the view is added
    mAdded = false;
    mAttachInfo = new View.AttachInfo(mWindowSession, mWindow, display, this, mHandler, this,
    context);
    ...
    mViewConfiguration = ViewConfiguration.get(context);
    mDensity = context.getResources().getDisplayMetrics().densityDpi;
    mNoncompatDensity = context.getResources().getDisplayMetrics().noncompatDensityDpi;
    mFallbackEventHandler = new PhoneFallbackEventHandler(context);
    mChoreographer = Choreographer.getInstance();
    mDisplayManager = (DisplayManager)context.getSystemService(Context.DISPLAY_SERVICE);

    if (!sCompatibilityDone) {
    sAlwaysAssignFocus = mTargetSdkVersion < Build.VERSION_CODES.P;

    sCompatibilityDone = true;
    }

    loadSystemProperties();
    }

    能看到此时,实际上在ViewRootImpl的构造函数会对应当前生成一个W的内部类。这个内部类:

    static class W extends IWindow.Stub

    这个内部类实际上就是一个Binder类,里面回调了很多方法来操作当前的ViewRootImpl。换句话说,就是把当前的ViewRootImpl的代理W交给WMS去管理。

    那么我们可以总结,IWindow是WMS用来间接操作ViewRootImpl中的View,IWindowSession是App用来间接操作WMS。

    WMS.addWindow

    WMS的addWindow很长,因此我这边拆开成3部分聊

    添加窗体的准备步骤

    我们抛开大部分的校验逻辑。实际上可以把这个过程总结为以下几点:

    • 1.判断又没有相关的权限
    • 2.尝试着获取当前displayId对应的DisplayContent,没有则创建。其逻辑实际上和我上一篇说的创建DisplayContent一摸一样
    • 3.通过mWindowMap,判断当前IWindow是否被添加过,是的话说明已经存在这个Window,不需要继续添加
    • 4.如果当前窗口类型是子窗口,则会通过WindowToken.attrs参数中的token去查找当前窗口的父窗口是什么。
    • 5.如果有父窗口,则从DisplayContent中以父窗口的IWindow获取父窗口WindowToken的对象,否则尝试的获取当前窗口对应的WindowToken对象。

    我们稍微探索一下其中的几个核心:

    通过windowForClientLocked查找父窗口的WindowState

    final WindowState windowForClientLocked(Session session, IBinder client, boolean throwOnError) {
    WindowState win = mWindowMap.get(client);
    if (localLOGV) Slog.v(TAG_WM, "Looking up client " + client + ": " + win);
    if (win == null) {
    if (throwOnError) {
    throw new IllegalArgumentException(
    "Requested window " + client + " does not exist");
    }
    Slog.w(TAG_WM, "Failed looking up window callers=" + Debug.getCallers(3));
    return null;
    }
    if (session != null && win.mSession != session) {
    if (throwOnError) {
    throw new IllegalArgumentException("Requested window " + client + " is in session "
    + win.mSession + ", not " + session);
    }
    Slog.w(TAG_WM, "Failed looking up window callers=" + Debug.getCallers(3));
    return null;
    }

    return win;
    }

    实际上可以看到这里面是从mWindowMap通过IWindow获取WindowState对象。还记得我上篇说过很重要的数据结构吗?mWindowMap实际上是保存着WMS中IWindow对应WindowState对象。IWindow本质上是WMS控制ViewRootImpl的Binder接口。因此我们可以把WindowState看成应用进程的对应的对象也未尝不可。

    获取对应的WindowToken

                AppWindowToken atoken = null;
    final boolean hasParent = parentWindow != null;
    //从DisplayContent找到对应的WIndowToken
    WindowToken token = displayContent.getWindowToken(
    hasParent ? parentWindow.mAttrs.token : attrs.token);

    从这里面我们能够看到WindowToken,是通过DisplayContent获取到的。

    WindowToken getWindowToken(IBinder binder) {
    return mTokenMap.get(binder);
    }

    这样就能看到我前两篇提到过的很重要的数据结构:mTokenMap以及mWindowMap。这两者要稍微区分一下:
    mWindowMap是以IWindow为key,WindowState为value。
    mTokenMap是以WindowState的IBinder(一般为IApplicationToken)为key,WindowToken为value

    还记得mTokenMap在Activity的启动流程中做的事情吗?在创建AppWIndowContainer的时候,会同时创建AppWindowToken,AppWIndowToken的构造会把当前的IBinder作为key,AppWindowToken作为value添加到mTokenMap中。

    也就是说,如果系统想要通过应用进程给的IWindow找到真正位于WMS中Window的句柄,必须通过这两层变换才能真正找到。

    拆分情况获取对应的WindowToken和AppWindowToken

    这个时候就分为两种情况,一种是存在WindowToken,一种是不存在WindowToken。

                boolean addToastWindowRequiresToken = false;

    if (token == null) {
    //校验窗口参数是否合法
    ...

    final IBinder binder = attrs.token != null ? attrs.token : client.asBinder();
    final boolean isRoundedCornerOverlay =
    (attrs.privateFlags & PRIVATE_FLAG_IS_ROUNDED_CORNERS_OVERLAY) != 0;
    token = new WindowToken(this, binder, type, false, displayContent,
    session.mCanAddInternalSystemWindow, isRoundedCornerOverlay);
    } else if (rootType >= FIRST_APPLICATION_WINDOW && rootType <= LAST_APPLICATION_WINDOW) {
    atoken = token.asAppWindowToken();
    if (atoken == null) {
    return WindowManagerGlobal.ADD_NOT_APP_TOKEN;
    }
    ...
    } else if (atoken.removed) {
    ...
    } else if (type == TYPE_APPLICATION_STARTING && atoken.startingWindow != null) {
    ...

    }
    } else if (rootType == TYPE_INPUT_METHOD) {
    ...

    } else if (rootType == TYPE_VOICE_INTERACTION) {
    ...
    } else if (rootType == TYPE_WALLPAPER) {
    ...
    } else if (rootType == TYPE_DREAM) {
    ...
    } else if (rootType == TYPE_ACCESSIBILITY_OVERLAY) {
    ...
    } else if (type == TYPE_TOAST) {
    ....
    } else if (type == TYPE_QS_DIALOG) {
    ...
    } else if (token.asAppWindowToken() != null) {

    attrs.token = null;
    token = new WindowToken(this, client.asBinder(), type, false, displayContent,
    session.mCanAddInternalSystemWindow);
    }

    当我们通过mTokenMap获取WindowToken的时候,大致分为四种情况。WindowToken会尝试的获取父窗口对应的Token,找不到则使用WindowManager.LayoutParams中的WindowToken。一般来说我们找到的都有父亲的WindowToken。

    • 1.无关应用的找不到WindowToken
    • 2.有关应用找不到WindowToken。
    • 3.无关应用找到WindowToken
    • 4.有关应用找到WindowToken

    前两种情况解析

    实际上前两种情况,一旦发现找不到WindowToken,如果当前的窗口和应用相关的,就一定爆错误。如Toast,输入法,应用窗口等等。

    因此在Android 8.0开始,当我们想要显示Toast的时候,加入传入的Context是Application而不是Activity,此时一旦发现mTokenMap中找不到IApplicationToken对应的WindowToken就爆出了错误。正确的做法应该是需要获取Activity当前的Context。

    在上面的情况应用启动窗口,此时并没有启动Activity。因此不可能会被校验拦下,因此并没有异常抛出。就会自己创建一个WindowToken。

    后两种的解析

    当找到WindowToken,一般是指Activity启动之后,在AppWindowToken初始化后,自动加入了mTokenMap中。此时的情况稍微复杂了点。

    当是子窗口的时候,则会判断当前的WindowToken是不是AppWindowToken。不是,或者被移除等异常情况则报错。

    如果是壁纸,输入法,系统弹窗,toast等窗口模式,子窗口和父窗口的模式必须一致。

    当此时的AppWindowToken不为空的时候,说明在New的时候已经生成,且没有移除,将会生成一个新的WindowToken。

    为什么要生成一个新的windowToken?可以翻阅之前我写的文章,只要每一次调用一次构造函数将会把当前的WindowToken添加到mTokenMap中,实际上也是担心,对应的AppWindowToken出现的重新绑定的问题。

    添加WindowState实例到数据结构

    但是别忘了,我们这个时候还需要把相关的数据结构存储到全局。

                final WindowState win = new WindowState(this, session, client, token, parentWindow,
    appOp[0], seq, attrs, viewVisibility, session.mUid,
    session.mCanAddInternalSystemWindow);
    if (win.mDeathRecipient == null) {
    ...
    return WindowManagerGlobal.ADD_APP_EXITING;
    }

    if (win.getDisplayContent() == null) {
    ...
    return WindowManagerGlobal.ADD_INVALID_DISPLAY;
    }

    final boolean hasStatusBarServicePermission =
    mContext.checkCallingOrSelfPermission(permission.STATUS_BAR_SERVICE)
    == PackageManager.PERMISSION_GRANTED;
    mPolicy.adjustWindowParamsLw(win, win.mAttrs, hasStatusBarServicePermission);
    win.setShowToOwnerOnlyLocked(mPolicy.checkShowToOwnerOnly(attrs));

    res = mPolicy.prepareAddWindowLw(win, attrs);
    if (res != WindowManagerGlobal.ADD_OKAY) {
    return res;
    }
    // From now on, no exceptions or errors allowed!

    res = WindowManagerGlobal.ADD_OKAY;
    if (mCurrentFocus == null) {
    mWinAddedSinceNullFocus.add(win);
    }

    if (excludeWindowTypeFromTapOutTask(type)) {
    displayContent.mTapExcludedWindows.add(win);
    }

    origId = Binder.clearCallingIdentity();

    win.attach();
    //以IWindow为key,WindowState为value存放到WindowMap中
    mWindowMap.put(client.asBinder(), win);

    win.initAppOpsState();

    ....
    win.mToken.addWindow(win);

    因为完全可能出现新的WindowToken,因此干脆会创建一个新的WindowState。此时会对调用WindowState.attach方法

        void attach() {
    mSession.windowAddedLocked(mAttrs.packageName);
    }

    这方法挺重要的,Session做了一次添加锁定。

    void windowAddedLocked(String packageName) {
    mPackageName = packageName;
    mRelayoutTag = "relayoutWindow: " + mPackageName;
    if (mSurfaceSession == null) {
    if (WindowManagerService.localLOGV) Slog.v(
    TAG_WM, "First window added to " + this + ", creating SurfaceSession");
    mSurfaceSession = new SurfaceSession();
    if (SHOW_TRANSACTIONS) Slog.i(
    TAG_WM, " NEW SURFACE SESSION " + mSurfaceSession);
    mService.mSessions.add(this);
    if (mLastReportedAnimatorScale != mService.getCurrentAnimatorScale()) {
    mService.dispatchNewAnimatorScaleLocked(this);
    }
    }
    mNumWindow++;
    }

    此时的工作是什么?联系上下文,当我们新增了PhoneWindow,就会一个ViewRootImpl,也因此新增了Session。此时说明诞生一个新界面,此时已经诞生了相关的容器对象,但是相关的绘制到底层对象还没有创建出来。

    命名逻辑和Session很相似。Session是WMS给应用App的会话对象,SurfaceSession是SurfaceFlinger面向上层每一个WIndow需要绘制内容对象。

    这个SurfaceSession和SurfaceControl都是重点,联通到SurfaceFlinger很重要的对象。

    最后再添加到mWindowMap中。并且把WindowState添加到WindowToken中,让每一个WindowToken赋予状态的信息。我们稍微探索一下addWindow的方法。


    收起阅读 »

    Android自定义view之3D正方体

    前言在之前写了一篇关于3D效果的文章,借助传感器展示,有小伙伴问可不可以改成手势滑动操作(事件分发),所以出一篇文章传感器相关文章链接:Android 3D效果的实现一、小提相对于常见的自定义view而言,继承的GLSurfaceView只有两个构造函数。可以...
    继续阅读 »

    前言

    在之前写了一篇关于3D效果的文章,借助传感器展示,有小伙伴问可不可以改成手势滑动操作(事件分发),所以出一篇文章


    传感器相关文章链接:Android 3D效果的实现

    一、小提

    相对于常见的自定义view而言,继承的GLSurfaceView只有两个构造函数。可以理解为没有提供获取自定义属性的方法。

        public TouchSurfaceView(Context context) {
    super(context);
    }

    public TouchSurfaceView(Context context, AttributeSet attrs) {
    super(context, attrs);
    }

    二、将传感器改成事件分发机制

        @Override
    public boolean onTouchEvent(MotionEvent e) {
    float x = e.getX();
    float y = e.getY();
    switch (e.getAction()) {
    case MotionEvent.ACTION_MOVE:
    float dx = x - mPreviousX;
    float dy = y - mPreviousY;
    mRenderer.mAngleX += dx * TOUCH_SCALE_FACTOR;
    mRenderer.mAngleY += dy * TOUCH_SCALE_FACTOR;
    requestRender();
    }
    mPreviousX = x;
    mPreviousY = y;
    return true;
    }

    要注意还有一个滚动球事件

        @Override
    public boolean onTrackballEvent(MotionEvent e) {
    mRenderer.mAngleX += e.getX() * TRACKBALL_SCALE_FACTOR;
    mRenderer.mAngleY += e.getY() * TRACKBALL_SCALE_FACTOR;
    requestRender();
    return true;
    }

    三、在Activity中使用

      mGLSurfaceView = new TouchSurfaceView(this);
    setContentView(mGLSurfaceView);
    mGLSurfaceView.requestFocus();
    mGLSurfaceView.setFocusableInTouchMode(true);

    注意要在对应生命周期中处理

        @Override
    protected void onResume() {
    super.onResume();
    mGLSurfaceView.onResume();
    }

    @Override
    protected void onPause() {
    super.onPause();
    mGLSurfaceView.onPause();
    }

    四、源码

    TouchSurfaceView.java

    除去前面的修改部分,其他大多与链接文章相同,仅将传感器改成了事件分发。(代码中难点有注释)

    public class TouchSurfaceView extends GLSurfaceView {
    private final float TOUCH_SCALE_FACTOR = 180.0f / 320;
    private final float TRACKBALL_SCALE_FACTOR = 36.0f;
    private CubeRenderer mRenderer;
    private float mPreviousX;
    private float mPreviousY;

    public TouchSurfaceView(Context context) {
    super(context);
    mRenderer = new CubeRenderer();
    setRenderer(mRenderer);
    setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);
    }

    public TouchSurfaceView(Context context, AttributeSet attrs) {
    super(context, attrs);
    }


    @Override
    public boolean onTrackballEvent(MotionEvent e) {
    mRenderer.mAngleX += e.getX() * TRACKBALL_SCALE_FACTOR;
    mRenderer.mAngleY += e.getY() * TRACKBALL_SCALE_FACTOR;
    requestRender();
    return true;
    }

    @Override
    public boolean onTouchEvent(MotionEvent e) {
    float x = e.getX();
    float y = e.getY();
    switch (e.getAction()) {
    case MotionEvent.ACTION_MOVE:
    float dx = x - mPreviousX;
    float dy = y - mPreviousY;
    mRenderer.mAngleX += dx * TOUCH_SCALE_FACTOR;
    mRenderer.mAngleY += dy * TOUCH_SCALE_FACTOR;
    requestRender();
    }
    mPreviousX = x;
    mPreviousY = y;
    return true;
    }


    private class CubeRenderer implements GLSurfaceView.Renderer {

    private Cube mCube;
    public float mAngleX;
    public float mAngleY;
    public CubeRenderer() {
    mCube =new Cube();
    }

    public void onDrawFrame(GL10 gl) {
    // | GL10.GL_DEPTH_BUFFER_BIT
    gl.glClear(GL10.GL_COLOR_BUFFER_BIT);
    gl.glMatrixMode(GL10.GL_MODELVIEW);
    gl.glLoadIdentity();
    gl.glTranslatef(0, 0, -3.0f);
    gl.glRotatef(mAngleX, 0, 1, 0);
    gl.glRotatef(mAngleY, 1, 0, 0);
    gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
    gl.glEnableClientState(GL10.GL_COLOR_ARRAY);
    mCube.draw(gl);
    }


    @Override
    public void onSurfaceCreated(GL10 gl, javax.microedition.khronos.egl.EGLConfig config) {
    gl.glDisable(GL10.GL_DITHER);
    gl.glClearColor(1,1,1,1);
    }

    public void onSurfaceChanged(GL10 gl, int width, int height) {
    gl.glViewport(0, 0, width, height);
    //设置投影矩阵。但并不需要在每次绘制时都做,通常情况下,当视图调整大小时,需要设置一个新的投影。
    float ratio = (float) width / height;
    gl.glMatrixMode(GL10.GL_PROJECTION);
    gl.glLoadIdentity();
    gl.glFrustumf(-ratio, ratio, -1, 1, 1, 10);
    }

    }



    public class Cube {
    //opengl坐标系中采用的是3维坐标:
    private FloatBuffer mVertexBuffer;
    private FloatBuffer mColorBuffer;
    private ByteBuffer mIndexBuffer;

    public Cube() {
    final float vertices[] = {
    -1, -1, -1, 1, -1, -1,
    1, 1, -1, -1, 1, -1,
    -1, -1, 1, 1, -1, 1,
    1, 1, 1, -1, 1, 1,
    };

    final float colors[] = {
    0, 1, 1, 1, 1, 1, 1, 1,
    1, 1, 0, 1, 1, 1, 1, 1,
    1, 1, 1, 1, 0, 1, 1, 1,
    1, 1, 1, 1, 1, 1, 0, 1,
    };

    final byte indices[] = {
    0, 4, 5, 0, 5, 1,
    1, 5, 6, 1, 6, 2,
    2, 6, 7, 2, 7, 3,
    3, 7, 4, 3, 4, 0,
    4, 7, 6, 4, 6, 5,
    3, 0, 1, 3, 1, 2
    };

    ByteBuffer vbb = ByteBuffer.allocateDirect(vertices.length*4);
    vbb.order(ByteOrder.nativeOrder());
    mVertexBuffer = vbb.asFloatBuffer();
    mVertexBuffer.put(vertices);
    mVertexBuffer.position(0);

    ByteBuffer cbb = ByteBuffer.allocateDirect(colors.length*4);
    cbb.order(ByteOrder.nativeOrder());
    mColorBuffer = cbb.asFloatBuffer();
    mColorBuffer.put(colors);
    mColorBuffer.position(0);

    mIndexBuffer = ByteBuffer.allocateDirect(indices.length);
    mIndexBuffer.put(indices);
    mIndexBuffer.position(0);
    }

    public void draw(GL10 gl) {
    //启用服务器端GL功能。
    gl.glEnable(GL10.GL_CULL_FACE);
    //定义多边形的正面和背面。
    //参数:
    //mode——多边形正面的方向。GL_CW和GL_CCW被允许,初始值为GL_CCW。
    gl.glFrontFace(GL10.GL_CW);
    //选择恒定或光滑着色模式。
    //GL图元可以采用恒定或者光滑着色模式,默认值为光滑着色模式。当图元进行光栅化的时候,将引起插入顶点颜色计算,不同颜色将被均匀分布到各个像素片段。
    //参数:
    //mode——指明一个符号常量来代表要使用的着色技术。允许的值有GL_FLAT 和GL_SMOOTH,初始值为GL_SMOOTH。
    gl.glShadeModel(GL10.GL_SMOOTH);
    //定义一个顶点坐标矩阵。
    //参数:
    //
    //size——每个顶点的坐标维数,必须是2, 3或者4,初始值是4。
    //
    //type——指明每个顶点坐标的数据类型,允许的符号常量有GL_BYTE, GL_SHORT, GL_FIXED和GL_FLOAT,初始值为GL_FLOAT。
    //
    //stride——指明连续顶点间的位偏移,如果为0,顶点被认为是紧密压入矩阵,初始值为0。
    //
    //pointer——指明顶点坐标的缓冲区,如果为null,则没有设置缓冲区。
    gl.glVertexPointer(3, GL10.GL_FLOAT, 0, mVertexBuffer);
    //定义一个颜色矩阵。
    //size指明每个颜色的元素数量,必须为4。type指明每个颜色元素的数据类型,stride指明从一个颜色到下一个允许的顶点的字节增幅,并且属性值被挤入简单矩阵或存储在单独的矩阵中(简单矩阵存储可能在一些版本中更有效率)。
    gl.glColorPointer(4, GL10.GL_FLOAT, 0, mColorBuffer);
    //由矩阵数据渲染图元
    //可以事先指明独立的顶点、法线、颜色和纹理坐标矩阵并且可以通过调用glDrawElements方法来使用它们创建序列图元。
    gl.glDrawElements(GL10.GL_TRIANGLES, 36, GL10.GL_UNSIGNED_BYTE, mIndexBuffer);
    }
    }
    }

    MainActivity.java

    public class MainActivity extends AppCompatActivity {
    private GLSurfaceView mGLSurfaceView;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    mGLSurfaceView = new TouchSurfaceView(this);
    setContentView(mGLSurfaceView);
    mGLSurfaceView.requestFocus();
    mGLSurfaceView.setFocusableInTouchMode(true);
    }

    @Override
    protected void onResume() {
    super.onResume();
    mGLSurfaceView.onResume();
    }

    @Override
    protected void onPause() {
    super.onPause();
    mGLSurfaceView.onPause();
    }


    }

    总结

    收起阅读 »

    内存管理(MRC、ARC)

    一、 什么是内存管理程序在运行的过程中通常通过以下行为,来增加程序的的内存占用创建一个OC对象定义一个变量调用一个函数或者方法而一个移动设备的内存是有限的,每个软件所能占用的内存也是有限的当程序所占用的内存较多时,系统就会发出内存警告,这时就得回收一些不需要再...
    继续阅读 »

    一、 什么是内存管理

    • 程序在运行的过程中通常通过以下行为,来增加程序的的内存占用
      • 创建一个OC对象
      • 定义一个变量
      • 调用一个函数或者方法
    • 而一个移动设备的内存是有限的,每个软件所能占用的内存也是有限的
    • 当程序所占用的内存较多时,系统就会发出内存警告,这时就得回收一些不需要再使用的内存空间。比如回收一些不需要使用的对象、变量等
    • 如果程序占用内存过大,系统可能会强制关闭程序,造成程序崩溃、闪退现象,影响用户体验

    所以,我们需要对内存进行合理的分配内存、清除内存,回收那些不需要再使用的对象。从而保证程序的稳定性。

    那么,那些对象才需要我们进行内存管理呢?

    • 任何继承了NSObject的对象需要进行内存管理
    • 而其他非对象类型(int、char、float、double、struct、enum等) 不需要进行内存管理

    这是因为

    • 继承了NSObject的对象的存储在操作系统的里边。
    • 操作系统的:一般由程序员分配释放,若程序员不释放,程序结束时可能由OS回收,分配方式类似于链表
    • 非OC对象一般放在操作系统的里面
    • 操作系统的:由操作系统自动分配释放,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈(先进后出)
    • 示例:
    int main(int argc, const char * argv[])
    {
    @autoreleasepool {
    int a = 10; // 栈
    int b = 20; // 栈
    // p : 栈
    // Person对象(计数器==1) : 堆
    Person *p = [[Person alloc] init];
    }
    // 经过上面代码后, 栈里面的变量a、b、p 都会被回收
    // 但是堆里面的Person对象还会留在内存中,因为它是计数器依然是1
    return 0;
    }




    二、 内存管理模型

    提供给Objective-C程序员的基本内存管理模型有以下3种:

    • 自动垃圾收集(iOS运行环境不支持)
    • 手工引用计数和自动释放池(MRC)
    • 自动引用计数(ARC)

    三、MRC 手动管理内存(Manual Reference Counting)

    1. 引用计数器

    系统是根据对象的引用计数器来判断什么时候需要回收一个对象所占用的内存

    • 引用计数器是一个整数
    • 从字面上, 可以理解为”对象被引用的次数”
    • 也可以理解为: 它表示有多少人正在用这个对象
    • 每个OC对象都有自己的引用计数器
    • 任何一个对象,刚创建的时候,初始的引用计数为1
      • 当使用alloc、new或者copy创建一个对象时,对象的引用计数器默认就是1
    • 当没有任何人使用这个对象时,系统才会回收这个对象, 也就是说
      • 当对象的引用计数器为0时,对象占用的内存就会被系统回收
      • 如果对象的计数器不为0,那么在整个程序运行过程,它占用的内存就不可能被回收(除非整个程序已经退出 )

    2. 引用计数器操作

    • 为保证对象的存在,每当创建引用到对象需要给对象发送一条retain消息,可以使引用计数器值+1 ( retain 方法返回对象本身)
    • 当不再需要对象时,通过给对象发送一条release消息,可以使引用计数器值-1
    • 给对象发送retainCount消息,可以获得当前的引用计数器值
    • 当对象的引用计数为0时,系统就知道这个对象不再需要使用了,所以可以释放它的内存,通过给对象发送dealloc消息发起这个过程。
    • 需要注意的是:release并不代表销毁\回收对象,仅仅是计数器-1

    int main(int argc, const char * argv[]) {
    @autoreleasepool {
    // 只要创建一个对象默认引用计数器的值就是1
    Person *p = [[Person alloc] init];
    NSLog(@"retainCount = %lu", [p retainCount]); // 1

    // 只要给对象发送一个retain消息, 对象的引用计数器就会+1
    [p retain];

    NSLog(@"retainCount = %lu", [p retainCount]); // 2
    // 通过指针变量p,给p指向的对象发送一条release消息
    // 只要对象接收到release消息, 引用计数器就会-1
    // 只要一个对象的引用计数器为0, 系统就会释放对象

    [p release];
    // 需要注意的是: release并不代表销毁\回收对象, 仅仅是计数器-1
    NSLog(@"retainCount = %lu", [p retainCount]); // 1

    [p release]; // 0
    NSLog(@"--------");
    }
    // [p setAge:20]; // 此时对象已经被释放
    return 0;
    }

    3. dealloc方法

    • 当一个对象的引用计数器值为0时,这个对象即将被销毁,其占用的内存被系统回收
    • 对象即将被销毁时系统会自动给对象发送一条dealloc消息(因此,从dealloc方法有没有被调用,就可以判断出对象是否被销毁)
    • dealloc方法的重写
      • 一般会重写dealloc方法,在这里释放相关资源,dealloc就是对象的遗言
      • 一旦重写了dealloc方法,就必须调用[super dealloc],并且放在最后面调用

    - (void)dealloc
    {
    NSLog(@"Person dealloc");
    // 注意:super dealloc一定要写到所有代码的最后
    // 一定要写在dealloc方法的最后面
    [super dealloc];
    }

    • 使用注意
      • 不能直接调用dealloc方法
      • 一旦对象被回收了, 它占用的内存就不再可用,坚持使用会导致程序崩溃(野指针错误)

    4. 野指针和空指针

    • 只要一个对象被释放了,我们就称这个对象为 “僵尸对象(不能再使用的对象)”
    • 当一个指针指向一个僵尸对象(不可用内存),我们就称这个指针为野指针
    • 只要给一个野指针发送消息就会报错(EXC_BAD_ACCESS错误)
    int main(int argc, const char * argv[]) {
    @autoreleasepool {
    Person *p = [[Person alloc] init]; // 执行完引用计数为1

    [p release]; // 执行完引用计数为0,实例对象被释放
    [p release]; // 此时,p就变成了野指针,再给野指针p发送消息就会报错
    [p release];
    }
    return 0;
    }
    • 为了避免给野指针发送消息会报错,一般情况下,当一个对象被释放后我们会将这个对象的指针设置为空指针
    • 空指针
      • 没有指向存储空间的指针(里面存的是nil, 也就是0)
      • 给空指针发消息是没有任何反应的

    int main(int argc, const char * argv[]) {
    @autoreleasepool {
    Person *p = [[Person alloc] init]; // 执行完引用计数为1

    [p release]; // 执行完引用计数为0,实例对象被释放
    p = nil; // 此时,p变为了空指针
    [p release]; // 再给空指针p发送消息就不会报错了
    [p release];
    }
    return 0;
    }

    5. 内存管理规律

    单个对象内存管理规律
    • 谁创建谁release :
      • 如果你通过alloc、new、copy或mutableCopy来创建一个对象,那么你必须调用release或autorelease
    • 谁retain谁release:
      • 只要你调用了retain,就必须调用一次release
    • 总结一下就是
      • 有加就有减
      • 曾经让对象的计数器+1,就必须在最后让对象计数器-1
    多个对象内存管理规律

    因为多个对象之间往往是联系的,所以管理起来比较复杂。这里用一个玩游戏例子来类比一下。

    游戏可以提供给玩家(A类对象) 游戏房间(B类对象)来玩游戏。

    • 只要一个玩家想使用房间(进入房间),就需要对这个房间的引用计数器+1
    • 只要一个玩家不想再使用房间(离开房间),就需要对这个房间的引用计数器-1
    • 只要还有至少一个玩家在用某个房间,那么这个房间就不会被回收,引用计数至少为1




    下面来定义两个类 玩家类:Person 和 房间类:Room

    房间类:Room,房间类中有房间号

    #import <Foundation/Foundation.h>

    @interface Room : NSObject
    @property int no; // 房间号
    @end

    玩家类:Person

    #import <Foundation/Foundation.h>
    #import "Room.h"

    @interface Person : NSObject
    {
    Room *_room;
    }

    - (void)setRoom:(Room *)room;

    - (Room *)room;
    @end

    现在我们通过几个玩家使用房间的不同应用场景来逐步深入理解内存管理。

    1. 玩家没有使用房间,玩家和房间之间没有联系的情况
    int main(int argc, const char * argv[]) {
    @autoreleasepool {
    // 1.创建两个对象
    Person *p = [[Person alloc] init]; // 玩家 p
    Room *r = [[Room alloc] init]; // 房间 r
    r.no = 888; // 房间号赋值

    [r release]; // 释放房间
    [p release]; // 释放玩家
    }
    return 0;
    }

    上述代码执行完前3行

    // 1.创建两个对象
    Person *p = [[Person alloc] init]; // 玩家 p
    Room *r = [[Room alloc] init]; // 房间 r
    r.no = 888; // 房间号赋值

    之后在内存中的表现如下图所示:




    可见,Room实例对象和Person实例对象之间没有相互联系,所以各自释放不会报错。执行完4、5行代码

    [r release];    // 释放房间      
    [p release]; // 释放玩家

    后,将房间对象和玩家对象各自释放掉,在内存中的表现如下图所示:



    最后各自实例对象的内存就会被系统回收

    2. 一个玩家使用一个游戏房间,玩家和房间之间相关联的情况
    int main(int argc, const char * argv[]) {
    @autoreleasepool {
    // 1.创建两个对象
    Person *p = [[Person alloc] init]; // 玩家 p
    Room *r = [[Room alloc] init]; // 房间 r
    r.no = 888; // 房间号赋值

    // 将房间赋值给玩家,表示玩家在使用房间
    // 玩家需要使用这间房,只要玩家在,房间就一定要在
    p.room = r; // [p setRoom:r]

    [r release]; // 释放房间

    // 在这行代码之前,玩家都没有被释放,但是因为玩家还在,那么房间就不能销毁
    NSLog(@"-----");

    [p release]; // 释放玩家
    }
    return 0;
    }

    上边代码执行完前3行的时候和之前在内存中的表现一样,如图


    当执行完第4行代码p.room = r;时,因为调用了setter方法,将Room实例对象赋值给了Person的成员变量,不做其他设置的话,在内存中的表现如下图(做法不对):



    在调用setter方法的时候,因为Room实例对象多了一个Person对象引用,所以应将Room实例对象的引用计数+1才对,即setter方法应该像下边一样,对room进行一次retain操作。

    - (void)setRoom:(Room *)room // room = r
    {
    // 对房间的引用计数器+1
    [room retain];
    _room = room;
    }

    那么执行完第4行代码p.room = r;,在内存中的表现为:




    继续执行第5行代码[r release];,释放房间,Room实例对象引用计数-1,在内存中的表现如下图所示:



    然后执行第6行代码[p release];,释放玩家。这时候因为玩家不在房间里了,房间也没有用了,所以在释放玩家的时候,要把房间也释放掉,也就是在delloc里边对房间再进行一次release操作。

    这样对房间对象来说,每一次retain/alloc操作都对应一次release操作。

    - (void)dealloc
    {
    // 人释放了, 那么房间也需要释放
    [_room release];
    NSLog(@"%s", __func__);

    [super dealloc];
    }

    那么在内存中的表现最终如下图所示:



    最后实例对象的内存就会被系统回收

    3. 一个玩家使用一个游戏房间R后,换到另一个游戏房间R2,玩家和房间相关联的情况
    int main(int argc, const char * argv[]) {
    @autoreleasepool {
    // 1.创建两个对象
    Person *p = [[Person alloc] init]; // 玩家 p
    Room *r = [[Room alloc] init]; // 房间 r
    r.no = 888; // 房间号赋值

    // 2.将房间赋值给玩家,表示玩家在使用房间
    p.room = r; // [p setRoom:r]
    [r release]; // 释放房间 r

    // 3. 换房
    Room *r2 = [[Room alloc] init];
    r2.no = 444;
    p.room = r2;
    [r2 release]; // 释放房间 r2

    [p release]; // 释放玩家 p
    }
    return 0;
    }

    执行下边几行代码

    // 1.创建两个对象
    Person *p = [[Person alloc] init]; // 玩家 p
    Room *r = [[Room alloc] init]; // 房间 r
    r.no = 888; // 房间号赋值

    // 2.将房间赋值给玩家,表示玩家在使用房间
    p.room = r; // [p setRoom:r]
    [r release]; // 释放房间 r

    之后的内存表现为:



    接着执行换房操作而不进行其他操作的话,

    // 3. 换房
    Room *r2 = [[Room alloc] init];
    r2.no = 444;
    p.room = r2;

    内存的表现为:



    最后执行完

    [r2 release];    // 释放房间 r2
    [p release]; // 释放玩家 p

    内存的表现为:




    可以看出房间 r 并没有被释放,这是因为在进行换房的时候,并没有对房间 r 进行释放。所以应在调用setter方法的时候,对之前的变量进行一次release操作。具体setter方法代码如下:

    - (void)setRoom:(Room *)room // room = r
    {
    // 将以前的房间释放掉 -1
    [_room release];

    // 对房间的引用计数器+1
    [room retain];

    _room = room;
    }
    }

    这样在执行完p.room = r2;之后就会将 房间 r 释放掉,最终内存表现为:




    4. 一个玩家使用一个游戏房间,不再使用游戏房间,将游戏房间释放掉之后,再次使用该游戏房间的情况

    int main(int argc, const char * argv[]) {
    @autoreleasepool {
    // 1.创建两个对象
    Person *p = [[Person alloc] init];
    Room *r = [[Room alloc] init];
    r.no = 888;

    // 2.将房间赋值给人
    p.room = r; // [p setRoom:r]
    [r release]; // 释放房间 r

    // 3.再次使用房间 r
    p.room = r;
    [r release]; // 释放房间 r
    [p release]; // 释放玩家 p
    }
    return 0;
    }

    执行下面代码

    // 1.创建两个对象
    Person *p = [[Person alloc] init];
    Room *r = [[Room alloc] init];
    r.no = 888;

    // 2.将房间赋值给人
    p.room = r; // [p setRoom:r]
    [r release]; // 释放房间 r

    之后的内存表现为:



    然后再执行p.room = r;,因为setter方法会将之前的Room实例对象先release掉,此时内存表现为:




    此时_room、r 已经变成了一个野指针。之后再对野指针 r 发出retain消息,程序就会崩溃。所以我们在进行setter方法的时候,要先判断一下是否是重复赋值,如果是同一个实例对象,就不需要重复进行release和retain。换句话说,如果我们使用的还是之前的房间,那换房的时候就不需要对这个房间再进行release和retain。则setter方法具体代码如下:

    - (void)setRoom:(Room *)room // room = r
    {
    // 只有房间不同才需用release和retain
    if (_room != room) { // 0ffe1 != 0ffe1
    // 将以前的房间释放掉 -1
    [_room release];

    // 对房间的引用计数器+1
    [room retain];

    _room = room;
    }
    }

    因为retain不仅仅会对引用计数器+1, 而且还会返回当前对象,所以上述代码可最终简化成:

    - (void)setRoom:(Room *)room // room = r
    {
    // 只有房间不同才需用release和retain
    if (_room != room) { // 0ffe1 != 0ffe1
    // 将以前的房间释放掉 -1
    [_room release];

    _room = [room retain];
    }
    }

    以上就是setter方法的最终形式。

    6. @property参数

    • 在成员变量前加上@property,系统就会自动帮我们生成基本的setter/getter方法
    @property (nonatomic) int val;
    • 如果在property后边加上retain,系统就会自动帮我们生成getter/setter方法内存管理的代码,但是仍需要我们自己重写dealloc方法
    @property(nonatomic, retain) Room *room;
    • 如果在property后边加上assign,系统就不会帮我们生成set方法内存管理的代码,仅仅只会生成普通的getter/setter方法,默认什么都不写就是assign
    @property(nonatomic, retain) int val;

    7. 自动释放池

    当我们不再使用一个对象的时候应该将其空间释放,但是有时候我们不知道何时应该将其释放。为了解决这个问题,Objective-C提供了autorelease方法。

    • autorelease是一种支持引用计数的内存管理方式,只要给对象发送一条autorelease消息,会将对象放到一个自动释放池中,当自动释放池被销毁时,会对池子里面的所有对象做一次release操作

      注意,这里只是发送release消息,如果当时的引用计数(reference-counted)依然不为0,则该对象依然不会被释放。

    • autorelease方法会返回对象本身,且调用完autorelease方法后,对象的计数器不变

    Person *p = [Person new];
    p = [p autorelease];
    NSLog(@"count = %lu", [p retainCount]); // 计数还为1
    1. 使用AUTORELEASE有什么好处呢
    • 不用再关心对象释放的时间
    • 不用再关心什么时候调用release
    2. AUTORELEASE的原理实质上是什么?

    autorelease实际上只是把对release的调用延迟了,对于每一个autorelease,系统只是把该对象放入了当前的autorelease pool中,当该pool被释放时,该pool中的所有对象会被调用release。

    3. AUTORELEASE的创建方法
    1. 使用NSAutoreleasePool来创建
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; // 创建自动释放池
    [pool release]; // [pool drain]; 销毁自动释放池
    1. 使用@autoreleasepool创建
    @autoreleasepool
    { //开始代表创建自动释放池

    } //结束代表销毁自动释放池
    4. AUTORELEASE的使用方法

    NSAutoreleasePool *autoreleasePool = [[NSAutoreleasePool alloc] init];
    Person *p = [[[Person alloc] init] autorelease];
    [autoreleasePool drain];
    @autoreleasepool
    { // 创建一个自动释放池
    Person *p = [[Person new] autorelease];
    // 将代码写到这里就放入了自动释放池
    } // 销毁自动释放池(会给池子中所有对象发送一条release消息)
    5. AUTORELEASE的注意事项
    • 并不是放到自动释放池代码中,都会自动加入到自动释放池
    @autoreleasepool {
    // 因为没有调用 autorelease 方法,所以对象没有加入到自动释放池
    Person *p = [[Person alloc] init];
    [p run];
    }
    • 在自动释放池的外部发送autorelease 不会被加入到自动释放池中
      • autorelease是一个方法,只有在自动释 放池中调用才有效。
    @autoreleasepool {
    }
    // 没有与之对应的自动释放池, 只有在自动释放池中调用autorelease才会放到释放池
    Person *p = [[[Person alloc] init] autorelease];
    [p run];

    // 正确写法
    @autoreleasepool {
    Person *p = [[[Person alloc] init] autorelease];
    }

    // 正确写法
    Person *p = [[Person alloc] init];
    @autoreleasepool {
    [p autorelease];
    }

    6. 自动释放池的嵌套使用
    • 自动释放池是以栈的形式存在

    • 由于栈只有一个入口, 所以调用autorelease会将对象放到栈顶的自动释放池

      栈顶就是离调用autorelease方法最近的自动释放池


    @autoreleasepool { // 栈底自动释放池
    @autoreleasepool {
    @autoreleasepool { // 栈顶自动释放池
    Person *p = [[[Person alloc] init] autorelease];
    }
    Person *p = [[[Person alloc] init] autorelease];
    }
    }
    • 自动释放池中不适宜放占用内存比较大的对象
      • 尽量避免对大内存使用该方法,对于这种延迟释放机制,还是尽量少用
      • 不要把大量循环操作放到同一个 @autoreleasepool 之间,这样会造成内存峰值的上升
    // 内存暴涨
    @autoreleasepool {
    for (int i = 0; i < 99999; ++i) {
    Person *p = [[[Person alloc] init] autorelease];
    }
    }

    // 内存不会暴涨
    for (int i = 0; i < 99999; ++i) {
    @autoreleasepool {
    Person *p = [[[Person alloc] init] autorelease];
    }
    }

    7. AUTORELEASE错误用法
    • 不要连续调用autorelease
    @autoreleasepool {
    // 错误写法, 过度释放
    Person *p = [[[[Person alloc] init] autorelease] autorelease];
    }

    • 调用autorelease后又调用release(错误)
    @autoreleasepool {
    Person *p = [[[Person alloc] init] autorelease];
    [p release]; // 错误写法, 过度释放
    }

    8. MRC中避免循环retain

    定义两个类Person类和Dog类

    • Person类:
    #import <Foundation/Foundation.h>
    @class Dog;

    @interface Person : NSObject
    @property(nonatomic, retain)Dog *dog;
    @end
    • Dog类:
    #import <Foundation/Foundation.h>
    @class Person;

    @interface Dog : NSObject
    @property(nonatomic, retain)Person *owner;
    @end

    执行以下代码:

    int main(int argc, const char * argv[]) {
    Person *p = [Person new];
    Dog *d = [Dog new];

    p.dog = d; // retain
    d.owner = p; // retain assign

    [p release];
    [d release];

    return 0;
    }

    就会出现A对象要拥有B对象,而B对应又要拥有A对象,此时会形成循环retain,导致A对象和B对象永远无法释放

    那么如何解决这个问题呢?

    • 不要让A retain B,B retain A
    • 让其中一方不要做retain操作即可
    • 当两端互相引用时,应该一端用retain,一端用assign

    四、ARC 自动管理内存(Automatic Reference Counting)

    • Automatic Reference Counting,自动引用计数,即ARC,WWDC2011和iOS5所引入的最大的变革和最激动人心的变化。ARC是新的LLVM 3.0编译器的一项特性,使用ARC,可以说一 举解决了广大iOS开发者所憎恨的手动内存管理的麻烦。
    • 使用ARC后,系统会检测出何时需要保持对象,何时需要自动释放对象,何时需要释放对象,编译器会管理好对象的内存,会在何时的地方插入retain, release和autorelease,通过生成正确的代码去自动释放或者保持对象。我们完全不用担心编译器会出错

    1\ ARC的判断原则

    ARC判断一个对象是否需要释放不是通过引用计数来进行判断的,而是通过强指针来进行判断的。那么什么是强指针?

    • 强指针
      • 默认所有对象的指针变量都是强指针
      • 被__strong修饰的指针

    Person *p1 = [[Person alloc] init];
    __strong Person *p2 = [[Person alloc] init];
    • 弱指针
      • 被__weak修饰的指针
    __weak  Person *p = [[Person alloc] init];

    ARC如何通过强指针来判断?

    • 只要还有一个强指针变量指向对象,对象就会保持在内存中

    2. ARC的使用

    int main(int argc, const char * argv[]) {
    // 不用写release, main函数执行完毕后p会被自动释放
    Person *p = [[Person alloc] init];

    return 0;
    }

    3. ARC的注意点

    • 不允许调用对象的 release方法
    • 不允许调用 autorelease方法
    • 重写父类的dealloc方法时,不能再调用 [super dealloc];

    4. ARC下单对象内存管理

    • 局部变量释放对象随之被释放
    int main(int argc, const char * argv[]) {
    @autoreleasepool {
    Person *p = [[Person alloc] init];
    } // 执行到这一行局部变量p释放
    // 由于没有强指针指向对象, 所以对象也释放
    return 0;
    }

    • 清空指针对象随之被释放
    int main(int argc, const char * argv[]) {
    @autoreleasepool {
    Person *p = [[Person alloc] init];
    p = nil; // 执行到这一行, 由于没有强指针指向对象, 所以对象被释放
    }
    return 0;
    }
    • 默认清空所有指针都是强指针
    int main(int argc, const char * argv[]) {
    @autoreleasepool {
    // p1和p2都是强指针
    Person *p1 = [[Person alloc] init];
    __strong Person *p2 = [[Person alloc] init];
    }
    return 0;
    }

    • 弱指针需要明确说明
      • 注意: 千万不要使用弱指针保存新创建的对象
    int main(int argc, const char * argv[]) {
    @autoreleasepool {
    // p是弱指针, 对象会被立即释放
    __weak Person *p1 = [[Person alloc] init];
    }
    return 0;
    }

    5. ARC下多对象内存管理

    • ARC和MRC一样, 想拥有某个对象必须用强指针保存对象, 但是不需要在dealloc方法中release
    @interface Person : NSObject
    // MRC写法
    //@property (nonatomic, retain) Dog *dog;

    // ARC写法
    @property (nonatomic, strong) Dog *dog;
    @end

    6. ARC下@property参数

    • strong : 用于OC对象,相当于MRC中的retain
    • weak : 用于OC对象,相当于MRC中的assign
    • assign : 用于基本数据类型,跟MRC中的assign一样

    7. ARC下循环引用问题

    • ARC和MRC一样,如果A拥有B,B也拥有A,那么必须一方使用弱指针

    @interface Person : NSObject
    @property (nonatomic, strong) Dog *dog;
    @end

    @interface Dog : NSObject
    // 错误写法, 循环引用会导致内存泄露
    //@property (nonatomic, strong) Person *owner;

    // 正确写法, 当如果保存对象建议使用weak
    @property (nonatomic, weak) Person *owner;
    @end




    作者:NJKNJK
    链接:https://www.jianshu.com/p/af3d7700f280


    收起阅读 »

    『Blocks』基本使用

    本文用来介绍 iOS开发中 『Blocks』的基本使用。通过本文您将了解到:什么是 BlocksBlocks 变量语法Blocks 变量的声明与赋值Blocks 变量截获局部变量值特性使用 __block 说明符Blocks 变量的循环引用以及如何避...
    继续阅读 »

    本文用来介绍 iOS开发中 『Blocks』的基本使用。通过本文您将了解到:

    1. 什么是 Blocks
    2. Blocks 变量语法
    3. Blocks 变量的声明与赋值
    4. Blocks 变量截获局部变量值特性
    5. 使用 __block 说明符
    6. Blocks 变量的循环引用以及如何避免

    1. 什么是 Blocks ?

    一句话总结:Blocks 是带有 局部变量 的 匿名函数(不带名称的函数)。

    Blocks 也被称作 闭包代码块。展开来讲,Blocks 就是一个代码块,把你想要执行的代码封装在这个代码块里,等到需要的时候再去调用。

    下边我们先来理解 局部变量匿名函数 的含义。

    1.1 局部变量

    在 C 语言中,定义在函数内部的变量称为 局部变量。它的作用域仅限于函数内部, 离开该函数后就是无效的,再使用就会报错。

    int x, y; // x,y 为全局变量

    int fun(int a) {
    int b, c; //a,b,c 为局部变量
    return a+b+c;
    }

    int main() {
    int m, n; // m,n 为局部变量
    return 0;
    }

    从上边的代码中,我们可以看出:

    1. 我们在开始位置定义了变量 x 和 变量 y。 x 和 y 都是全局变量。它们的作用域默认是整个程序,也就是所有的源文件,包括 .c 和 .h 文件。
    2. 而我们在 fun() 函数中定义了变量 a、变量 b、变量 c。它们的作用域是 fun() 函数。只能在 fun() 函数内部使用,离开 fun() 函数就是无效的。
    3. 同理,main() 函数中的变量 m、变量 n 也只能在 main() 函数内部使用。

    1.2 匿名函数

    匿名函数指的是不带有名称的函数。但是 C 语言中不允许存在这样的函数。

    在 C 语言中,一个普通的函数长这样子:


    int fun(int a);

    fun 就是这个函数的名称,在调用的时候必须要使用该函数的名称 fun 来调用。

    int result = fun(10);
    在 C 语言中,我们还可以通过函数指针来直接调用函数。但是在给函数指针赋值的时候,同样也是需要知道函数的名称。

    int (*funPtr)(int) = &fun;
    int result = (*funPtr)(10);

    而我们通过 Blocks,可以直接使用函数,不用给函数命名。


    2. Blocks 变量语法

    我们使用 ^ 运算符来声明 Blocks 变量,并将 Blocks 对象主体部分包含在 {} 中,同时,句尾加 ; 表示结尾。

    下边来看一个官方的示例:

    int multiplier = 7;
    int (^ myBlock)(int)= ^(int num) {
    return num * multiplier;
    };
    这个 Blocks 示例中,myBlock 是声明的块对象,返回类型是 整型值,myBlock 块对象有一个 参数,参数类型为整型值,参数名称为 num。myBlock 块对象的 主体部分 为 return num * multiplier;,包含在 {} 中。

    参考上面的示例,我们可以将 Blocks 表达式语法表述为:

    ^ 返回值类型 (参数列表) { 表达式 };

    例如,我们可以写出这样的 Block 语法:

    ^ int (int count) { return count + 1; };

    Blocks 规定可以省略好多项目。例如:返回值类型参数列表。如果用不到,都可以省略。

    2.1 省略返回值类型:^ (参数列表) { 表达式 };

    上边的 Blocks 语法就可以写为:

    ^ (int count) { return count + 1; };

    表达式中,return 语句使用的是 count + 1 语句的返回类型。如果表达式中有多个 return 语句,则所有 return 语句的返回值类型必须一致。

    如果表达式中没有 return 语句,则可以用 void 表示,或者也省略不写。代码如下:。

    ^ void (int count)  { printf("%d\n", count); };    // 返回值类型使用 void
    ^ (int count) { printf("%d\n", count); }; // 省略返回值类型

    2.2 省略参数列表 ^ 返回值类型 (void) { 表达式 };

    如果表达式中,没有使用参数,则用 void 表示,也可以省略 void。


    ^ int (void) { return 1; };    // 参数列表使用 void
    ^ int { return 1; }; // 省略参数列表类型

    2.3 省略返回值类型、参数列表:^ { 表达式 };

    从上边 2.1 中可以看出,无论有无返回值,都可以省略返回值类型。并且,从 2.2 中可以看出,如果不需要参数列表的话,也可以省略参数列表。则代码可以简化为:

    ^ { printf("Blocks"); };

    3. Blocks 变量的声明与赋值

    3.1 Blocks 变量的声明与赋值语法

    Blocks 变量的声明与赋值语法可以总结为:

    返回值类型 (^变量名) (参数列表) = Blocks 表达式

    注意:此处返回值类型不可以省略,若无返回值,则使用 void 作为返回值类型。

    例如,定义一个变量名为 blk 的 Blocks 变量:


    int (^blk) (int)  = ^(int count) { return count + 1; };
    int (^blk1) (int); // 声明变量名为 blk1 的 Blocks 变量
    blk1 = blk; // 将 blk 赋值给 blk1

    Blocks 变量的声明语法有点复杂,其实我们可以和 C 语言函数指针的声明类比着来记。

    Blocks 变量的声明就是把声明函数指针类型的变量 * 变为 ^

    //  C 语言函数指针声明与赋值
    int func (int count) {
    return count + 1;
    }
    int (*funcptr)(int) = &func;

    // Blocks 变量声明与赋值
    int (^blk) (int) = ^(int count) { return count + 1; };

    3.2 Blocks 变量的声明与赋值的使用

    3.2.1 作为局部变量:返回值类型 (^变量名) (参数列表) = 返回值类型 (参数列表) { 表达式 };

    我们可以把 Blocks 变量作为局部变量,在一定范围内(函数、方法内部)使用。

    // Blocks 变量作为本地变量
    - (void)useBlockAsLocalVariable {
    void (^myLocalBlock)(void) = ^{
    NSLog(@"useBlockAsLocalVariable");
    };

    myLocalBlock();
    }
    3.2.2 作为带有 property 声明的成员变量:@property (nonatomic, copy) 返回值类型 (^变量名) (参数列表);

    作用类似于 delegate,实现 Blocks 回调。

    /* Blocks 变量作为带有 property 声明的成员变量 */
    @property (nonatomic, copy) void (^myPropertyBlock) (void);

    // Blocks 变量作为带有 property 声明的成员变量
    - (void)useBlockAsProperty {
    self.myPropertyBlock = ^{
    NSLog(@"useBlockAsProperty");
    };

    self.myPropertyBlock();
    }

    3.2.3 作为 OC 方法参数:- (void)someMethodThatTaksesABlock:(返回值类型 (^)(参数列表)) 变量名;

    可以把 Blocks 变量作为 OC 方法中的一个参数来使用,通常 blocks 变量写在方法名的最后。

    // Blocks 变量作为 OC 方法参数
    - (void)someMethodThatTakesABlock:(void (^)(NSString *)) block {
    block(@"someMethodThatTakesABlock:");
    }
    3.2.4 调用含有 Block 参数的 OC方法:[someObject someMethodThatTakesABlock:^返回值类型 (参数列表) { 表达式}];
    // 调用含有 Block 参数的 OC方法
    - (void)useBlockAsMethodParameter {
    [self someMethodThatTakesABlock:^(NSString *str) {
    NSLog(@"%@",str);
    }];
    }

    通过 3.2.3 和 3.2.4 中,Blocks 变量作为 OC 方法参数的调用,我们同样可以实现类似于 delegate 的作用,即 Blocks 回调(后边应用场景中会讲)。

    3.2.5 作为 typedef 声明类型:
    typedef 返回值类型 (^声明名称)(参数列表);
    声明名称 变量名 = ^返回值类型(参数列表) { 表达式 };
    // Blocks 变量作为 typedef 声明类型
    - (void)useBlockAsATypedef {
    typedef void (^TypeName)(void);

    // 之后就可以使用 TypeName 来定义无返回类型、无参数列表的 block 了。
    TypeName myTypedefBlock = ^{
    NSLog(@"useBlockAsATypedef");
    };

    myTypedefBlock();
    }

    4. Blocks 变量截获局部变量值特性

    先来看一个例子。

    // 使用 Blocks 截获局部变量值
    - (void)useBlockInterceptLocalVariables {
    int a = 10, b = 20;

    void (^myLocalBlock)(void) = ^{
    printf("a = %d, b = %d\n",a, b);
    };

    myLocalBlock(); // 打印结果:a = 10, b = 20

    a = 20;
    b = 30;

    myLocalBlock(); // 打印结果:a = 10, b = 20
    }

    为什么两次打印结果都是 a = 10, b = 20

    明明在第一次调用 myLocalBlock(); 之后已经重新给变量 a、变量 b 赋值了,为什么第二次调用 myLocalBlock(); 的时候,使用的还是之前对应变量的值?

    因为 Block 语法的表达式使用的是它之前声明的局部变量 a、变量 b。Blocks 中,Block 表达式截获所使用的局部变量的值,保存了该变量的瞬时值。所以在第二次执行 Block 表达式时,即使已经改变了局部变量 a 和 b 的值,也不会影响 Block 表达式在执行时所保存的局部变量的瞬时值。

    这就是 Blocks 变量截获局部变量值的特性。

    5. 使用 __block 说明符

    实际上,在使用 Block 表达式的时候,只能使用保存的局部变量的瞬时值,并不能直接对其进行改写。直接修改编译器会直接报错,如下图所示。



    那么如果,我们想要该写 Block 表达式中截获的局部变量的值,该怎么办呢?

    如果,我们想在 Block 表达式中,改写 Block 表达式之外声明的局部变量,需要在该局部变量前加上 __block 的修饰符。

    这样我们就能实现:在 Block 表达式中,为表达式外的局部变量赋值。


    // 使用 __block 说明符修饰,更改局部变量值
    - (void)useBlockQualifierChangeLocalVariables {
    __block int a = 10, b = 20;
    void (^myLocalBlock)(void) = ^{
    a = 20;
    b = 30;

    printf("a = %d, b = %d\n",a, b); // 打印结果:a = 20, b = 30
    };

    myLocalBlock();
    }

    可以看到,使用 __block 说明符修饰之后,我们在 Block表达式中,成功的修改了局部变量值。

    6. Blocks 变量的循环引用以及如何避免

    从上文中我们知道 Block 会对引用的局部变量进行持有。同样,如果 Block 也会对引用的对象进行持有,从而会导致相互持有,引起循环引用。


    /* —————— retainCycleBlcok.m —————— */   
    #import <Foundation/Foundation.h>
    #import "Person.h"
    int main() {
    Person *person = [[Person alloc] init];
    person.blk = ^{
    NSLog(@"%@",person);
    };

    return 0;
    }


    /* —————— Person.h —————— */
    #import <Foundation/Foundation.h>

    typedef void(^myBlock)(void);

    @interface Person : NSObject
    @property (nonatomic, copy) myBlock blk;
    @end


    /* —————— Person.m —————— */
    #import "Person.h"

    @implementation Person

    @end

    上面 retainCycleBlcok.m 中 main() 函数的代码会导致一个问题:person 持有成员变量 myBlock blk,而 blk 也同时持有成员变量 person,两者互相引用,永远无法释放。就造成了循环引用问题。

    那么,如何来解决这个问题呢?

    6.1 ARC 下,通过 __weak 修饰符来消除循环引用

    在 ARC 下,可声明附有 __weak 修饰符的变量,并将对象赋值使用。

    int main() {
    Person *person = [[Person alloc] init];
    __weak typeof(person) weakPerson = person;

    person.blk = ^{
    NSLog(@"%@",weakPerson);
    };

    return 0;
    }

    这样,通过 __weak,person 持有成员变量 myBlock blk,而 blk 对 person 进行弱引用,从而就消除了循环引用。

    6.2 MRC 下,通过 __block 修饰符来消除循环引用

    MRC 下,是不支持 weak 修饰符的。但是我们可以通过 block 来消除循环引用。

    int main() {
    Person *person = [[Person alloc] init];
    __block typeof(person) blockPerson = person;

    person.blk = ^{
    NSLog(@"%@", blockPerson);
    };

    return 0;
    }

    通过 __block 引用的 blockPerson,是通过指针的方式来访问 person,而没有对 person 进行强引用,所以不会造成循环引用。




    作者:NJKNJK
    链接:https://www.jianshu.com/p/c5561abe9dd8


    收起阅读 »

    2021 提升Android开发效率的实战技巧

    一 泛型 + 反射 我们创建Activity的时候 需要先设置布局setContentView(R.layout..) 如果使用了ViewModel,还得给每个Activity创建ViewModel. 如果项目中Activity过多,无疑是...
    继续阅读 »

    一 泛型 + 反射


    我们创建Activity的时候



    1. 需要先设置布局setContentView(R.layout..)

    2. 如果使用了ViewModel,还得给每个Activity创建ViewModel.


    如果项目中Activity过多,无疑是写很多模板代码的,借助Java的泛型机制,我们可以在BaseAct,封装上述逻辑。


    1.1 示例


    先创建BaseAct


    abstract class BaseAct<B : ViewDataBinding, VM : ViewModel> : AppCompatActivity() {
    private var mBinding: B? = null
    private lateinit var mModel: VM
    abstract val layoutId: Int
    abstract fun doBusiness(savedInstanceState: Bundle?)
    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    // 创建View
    setContentView(createViewBinding().root)
    // 创建ViewModel
    createViewModel()
    doBusiness(savedInstanceState)
    }

    fun getB(): B {
    return mBinding!!
    }

    fun getVM(): VM {
    return mModel
    }

    private fun createViewBinding(): B {
    mBinding = DataBindingUtil.inflate(LayoutInflater.from(this), layoutId, null, false)
    mBinding!!.lifecycleOwner = this
    return mBinding!!
    }

    private fun createViewModel() {
    val type = javaClass.genericSuperclass!! as ParameterizedType
    val argsType = type.actualTypeArguments
    val modelClass: Class<VM> = argsType[1] as Class<VM>
    val model = ViewModelProvider(this).get(modelClass)
    mModel = model
    }

    override fun onDestroy() {
    super.onDestroy()
    mBinding?.unbind()
    mBinding = null
    }
    }

    创建一个LoginAct的时候,可以这样写


    // 声明泛型类
    class LoginAct : BaseAct<LoginActBinding, LoginActViewModel>() {
    override val layoutId: Int = R.layout.login_act

    override fun doBusiness(savedInstanceState: Bundle?) {
    // 逻辑处理
    getVm() // 获取到的是 LoginActViewModel
    }
    }
    class LoginActViewModel : ViewModel() {

    }

    二 一次生成多个文件


    上面LoginAct的创建。我们一般得做以下几个步骤



    1. 创建一个xml布局

    2. new 一个 Kotlin Class/File创建LoginViewModel

    3. new 一个 Kotlin Class/File创建LoginAct

    4. LoginAct 继承 BaseAct,重写方法


    通过 templates模板,可以把上面步骤简化。


    2.1 as 版本4.1之前


    使用的是FreeMarker模板引擎


    2.1.1 把模板放到对应目录



    1. 新建文件夹mvvm_templates,放到目录**android Studio\plugins\android\lib\templates\activities **

    2. 把以下文件放到mvvm_templates文件夹里


    image.png


    2.1.2 模板文件介绍


    mvvm_templates
    |-- root // 文件
    |-- src
    |-- app_package
    |-- xx.kt // 期望生成的kt文件
    |-- xx.java // 期望生成的java文件
    |-- ...
    |-- res // 资源模板
    |-- xx.xml.ftl // 期望生成的xml
    |-- ...
    |-- globals.xml.ftl
    |-- recipe.xml.ftl // 管理所有的文件声明
    |-- template.xml // 模板控制台

    2.1.3 使用方法



    • 上面的ftl 描述执行模板的参数和指令

    • as启动后,Android Studio 会解析“  /templates ”文件夹的内容,向“ **New -> **”菜单界面添加模板名,当点击对应模板名,Android Studio会读取“ template.xml ”的内容,构建UI等。


    image.png


    image.png 我 as 升级了,无法截我自己的配置页面图,原理是一样的,你的模板配置了哪些选项,在上图中就可以选择。



    1. 我的mvvm_templates 模板下载地址


    这是我自己的配置,大家可以拿去参考修改。


    2.2 as 版本4.1后


    从 Android Studio 4.1 开始,Google 停止了对自定义 FreeMarker 模板的支持。 该功能对于我来讲是非常实用的,所以我在github上找到了另外一种解决方案1解决方案2


    很多人在谷歌的问题追踪里进行反馈,但到目前还在等待官方支持。


    三 一次生成一个文件


    Edit File Templates,创建单个xml、单个文件、文件头等模板


    3.1 创建xml布局


    image.png 步骤还是挺繁琐的,也需要点几下,创建出来的布局文件只有1个根布局。


    通过下面模板布局,可以简化上面步骤,并且可以设置一些常用的脚手架布局。


    3.2 创建xml模板布局


    3.2.1 配置模板



    1. 编辑模板

    2. 创建一个file

    3. 定义模板名字

    4. 定义文件后缀

    5. 把你的模板布局copy进去

    6. 完成


    image.png


    3.2.2 使用模板


    刚才配置的模板就会在这里显示,点击后就会生成对应的布局。 image.png


    配置布局会自动填充进来,可以根据不同场景,定义多种不同的模板。 image.png


    3.3 创建kt文件模板


    image.png 步骤和上面创建xml模板是一样的,只是该下文件后缀名。这里多了个File Header,创建步骤如下。


    3.4 创建File Header


    image.png


    四 单个文件快捷输出


    在AS 设置里 Live Templates


    4.1 示例


    如果我想让红色图片居中显示,必须得添加4行约束属性,这些属性对于咱们开发来讲是经常要写的。如果在xml里输入 cc 按下回车,就能生成这4行代码,是不是能节约点时间? image.png


    4.2 配置


    建议分组管理。



    • 在xml里的快捷键单独创建一个组。

    • 在kotlin的快捷键单独创建一个组。


    image.png


    image.png


    4.3 使用


    我设置了



    • cc显示4个约束属性

    • tt显示app:layout_constraintTop_toTopOf="parent"

    • 同样,在kotlin中,在java中,比如日志打印、if判断、初始化变量、更多使用场景等你挖掘。


    布局快捷键.gif


    4.4 我自己的模板


    image.png



    最终as会在该路径下生成上面我们的配置模板: C:\Users\userName\AppData\Roaming\Google\AndroidStudio4.1\templates



    五 AS 常用插件


    5.1 WiFi连接手机调试


    image.png


    5.2 Translation 翻译英文


    image.png


    5.3 其他



    • Alibaba Java Coding Guidelines 阿里Java代码规范

    • CodeGlance 在右边可以预览代码结构,实现快速定位

    • Database Navigator 数据库调试

    收起阅读 »

    Flutter 入门与实战:让模拟器和和邮递员(Postman)聊聊天

    前言 上一篇Flutter 入门与实战(五十五):和 Provider 一起玩 WebSocket我们讲了使用 socket_client_io 和 StreamProvider实现 WebSocket 通讯。本篇延续上一篇,来讲一下如何实现与其他用户进行即...
    继续阅读 »

    前言


    上一篇Flutter 入门与实战(五十五):和 Provider 一起玩 WebSocket我们讲了使用 socket_client_io 和 StreamProvider实现 WebSocket 通讯。本篇延续上一篇,来讲一下如何实现与其他用户进行即时聊天。


    Socket 消息推送


    在 与服务端Socket 通讯中,调用 socket.emit 方法时默认发送消息都是给当前连接的 socket的,如果要实现发送消息给其他用户,服务端需要做一下改造。具体的做法如下:



    • 在建立连接后,客户多发送消息将用户唯一标识符(例如用户名或 userId)与连接的 socket 对象进行绑定。

    • 当其他用户发送消息给该用户时,找到该用户绑定的 socket 对象,再通过该 socketemit 方法发送消息就可以搞定了。


    因此客户端需要发送一个注册消息到服务端以便与用户绑定,同时还应该有一个注销消息,以解除绑定(可选的,也可以通过断开连接来自动解除绑定)。整个聊天过程的时序图如下:


    时序图.png


    服务端代码已经好了,采用了一个简单的对象来存储用户相关的未发送消息和 socket 对象。可以到后端代码仓库拉取最新代码,


    消息格式约定


    Socket 可以发送字符串或Json 对象,这里我们约定消息聊天为 Json 对象,字段如下:



    • fromUserId:消息来源用户 id

    • toUserId:接收消息用户 id

    • contentType:消息类型,方便发送文本、图片、语音、视频等消息。目前只做了文本消息,其他消息其实可以在 content 中传对应的资源 id 后由App 自己处理就好了。

    • content:消息内容。


    StreamSocket 改造


    上一篇的 StreamSocket 改造我们只能发送字符串,为了扩大适用范围,将该类改造成泛型。这里需要注意,Socketemit 的数据会调用对象的 toJson 将对象转为 Json 对象发送,因此泛型的类需要实现 Map<String dynamic> toJson 方法。同时增加了如下属性和方法:



    • recvEvent:接收事件的名称

    • regsiter:注册方法,将用户 id发送到服务端与 socket 绑定,可以理解为上线通知;

    • unregister:注销方法,将用户 id 发送到服务端与 socket解绑,可以理解为下线通知。


    class StreamSocket<T> {
    final _socketResponse = StreamController<T>();

    Stream<T> get getResponse => _socketResponse.stream;

    final String host;
    final int port;
    late final Socket _socket;
    final String recvEvent;

    StreamSocket(
    {required this.host, required this.port, required this.recvEvent}) {
    _socket = SocketIO.io('ws://$host:$port', <String, dynamic>{
    'transports': ['websocket'],
    'autoConnect': true,
    'forceNew': true
    });
    }

    void connectAndListen() {
    _socket.onConnect((_) {
    debugPrint('connected');
    });

    _socket.onConnectTimeout((data) => debugPrint('timeout'));
    _socket.onConnectError((error) => debugPrint(error.toString()));
    _socket.onError((error) => debugPrint(error.toString()));
    _socket.on(recvEvent, (data) {
    _socketResponse.sink.add(data);
    });
    _socket.onDisconnect((_) => debugPrint('disconnect'));
    }

    void regsiter(String userId) {
    _socket.emit('register', userId);
    }

    void unregsiter(String userId) {
    _socket.emit('unregister', userId);
    }

    void sendMessage(String event, T message) {
    _socket.emit(event, message);
    }

    void close() {
    _socketResponse.close();
    _socket.disconnect().close();
    }
    }

    聊天页面


    新建一个 chat_with_user.dart 文件,实现聊天相关的代码,其中ChatWithUserPageStatefulWidget,以便在State 的生命周期管理 Socket的连接,注册和注销等操作。目前我们写死了 App 端的用户是 user1,发送消息给 user2


    class _ChatWithUserPageState extends State<ChatWithUserPage> {
    late final StreamSocket<Map<String, dynamic>> streamSocket;

    @override
    void initState() {
    super.initState();
    streamSocket =
    StreamSocket(host: '127.0.0.1', port: 3001, recvEvent: 'chat');
    streamSocket.connectAndListen();
    streamSocket.regsiter('user1');
    }

    @override
    Widget build(BuildContext context) {
    return Scaffold(
    appBar: AppBar(
    title: Text('即时聊天'),
    ),
    body: Stack(
    alignment: Alignment.bottomCenter,
    children: [
    StreamProvider<Map<String, dynamic>?>(
    create: (context) => streamSocket.getResponse,
    initialData: null,
    child: StreamDemo(),
    ),
    ChangeNotifierProvider<MessageModel>(
    child: MessageReplyBar(messageSendHandler: (message) {
    Map<String, String> json = {
    'fromUserId': 'user1',
    'toUserId': 'user2',
    'contentType': 'text',
    'content': message
    };
    streamSocket.sendMessage('chat', json);
    }),
    create: (context) => MessageModel(),
    ),
    ],
    ),
    );
    }

    @override
    void dispose() {
    streamSocket.unregsiter('user1');
    streamSocket.close();
    super.dispose();
    }
    }

    其他的和上一篇基本类似,只是消息对象由 String换成了 Map<String, dynamic>


    调试


    消息的对话界面本篇先不涉及,下一篇我们再来介绍。现在来看一下如何进行调试。目前 PostMan 的8.x 版本已经支持 WebSocket 调试了,我们拿PostMan 和手机模拟器进行联调。Postman 的 WebSocket 调试界面如下: image.png 使用起来比较简单,这里我们已经完成了如下操作:



    • 注册:使用 user2注册

    • 设置发送消息为 json,消息事件(event)为 chat,以便和 app、服务端 保持一致。


    现在来看看调试效果怎么样(PostMan 调起来有点手忙脚乱?)?


    屏幕录制2021-08-19 下午9.35.45.gif


    可以看到模拟器和 PostMan 直接的通讯是正常的。




    收起阅读 »

    Android修炼系列,图解抓包和弱网测试

    本节主要介绍下,如何使用 Charles 进行抓包和模拟弱网环境测试。Charles 能够帮助我们查看设备和 Internet 之间的所有 HTTP 和 SSL/HTTPS 通信,这包括请求、响应和 HTTP 头。 HTTP代理 我们要保证手机设备和电脑在...
    继续阅读 »

    本节主要介绍下,如何使用 Charles 进行抓包和模拟弱网环境测试。Charles 能够帮助我们查看设备和 Internet 之间的所有 HTTP 和 SSL/HTTPS 通信,这包括请求、响应和 HTTP 头。


    HTTP代理


    我们要保证手机设备和电脑在一个局域网下,这点要注意。


    安装完 Charles 软件后,首先要进行 HTTP 代理设置,默认端口号:8888


    抓包1.png


    接着查看电脑 IP,将手机 WIFI 网络选项也进行代理设置。通常步骤为 WIFI 高级选项 -> 代理手动 -> 输入IP与端口 -> 保存。如本例中将安卓设备的 WIFI 代理设置为 192.168.0.110


    抓包2.jpg


    随后会有连接成功的提示,点击允许。


    抓包3.jpg


    Allow 之后,我们就进入抓包界面了。请求信息会在界面的左侧展示。但是通过下图也能发现,https 的请求抓包乱码。


    抓包4.png


    针对HTTPS乱码的问题,我们需要设置 HTTPS 代理。


    HTTPS 代理


    要抓取 https 的接口的请求信息,那么 Charles 需要在电脑端安装证书。


    抓包5.png


    在电脑如下目录下,我们双击安装证书,并信任。


    抓包6.png


    证书安装完毕,Charles 还需要进行 SSL 代理配置。


    抓包7.png


    其中 Charles 的 Location 配置是支持通配符的,如不需要抓取特定域名,我们可直接填写 * 。Host的配置,ssl port 常规为 443。


    抓包8.png


    配置好SSL代理之后,我们同样需要给待测试手机安装证书,下证书载地址可通过如下方式查看。


    抓包9.png


    通过下图,我们知下载地址:chls.pro/ssl 我们打开手机浏览器,输入该地址下载手机证书。随后安装,并信任


    抓包10.png


    当我们操作完毕之后,我们就能抓取部分 HTTPS 的请求了。我实际测试有些 HTTPS 请求还是没办法脱码的。


    网速配置


    弱网环境测试就简单多了,在说之前,我们先来看下 Charles 工具栏中提供的快捷按钮:


    抓包11.jpg



    • 清除捕获到的所有请求


    • 红点状态说明正在捕获请求,灰色状态说明目前没有捕获请求。


    • 停止SSL代理


    • 灰色状态说明是没有开启网速节流,绿色状态说明开启了网速节流。


    • 灰色状态说明是没有开启断点,红色状态说明开启了断点。


    • 编辑修改请求,点击之后可以修改请求的内容。


    • 重复发送请求,点击之后选中的请求会被再次发送。


    • 验证选中的请求的响应。


    • 常用功能,包含了 Tools 菜单中的常用功能。


    • 常用设置,包含了 Proxy 菜单中的常用设置。



    这里的小乌龟图标就是我们所需要的啦。


    当然我们也可通过 Throttle Setting 来进行节流控制。其包括 Bandwidth:带宽、Utilistation:利用百分比、Round-trip:往返延迟、MTU:字节。这里选择 BandWidth(带宽)复选框来开启限速。


    抓包12.png



    好了,本文到此就结束了。知识无涯,勿焦虑。



    收起阅读 »

    字节跳动开源AndroidPLThook方案bhook

    字节 bhook 开源 github.com/bytedance/b… 字节的 Android PLT hook 方案 bhook 开源了。bhook 支持 Android 4.1 - 12 (API level 16 - 31),支持 armeabi-v...
    继续阅读 »

    字节 bhook 开源


    github.com/bytedance/b…


    字节的 Android PLT hook 方案 bhook 开源了。bhook 支持 Android 4.1 - 12 (API level 16 - 31),支持 armeabi-v7a, arm64-v8a, x86 和 x86_64,使用 MIT 许可证授权。


    字节的大多数 Android app 都在线上使用了 bhook 作为 PLT hook 方案。字节内部有 20 多个不同技术纬度的 SDK 使用了 bhook。bhook 在线上稳定性,功能性,性能等多个方面都达到了预期。


    Android native hook


    随着 Android app 开发的技术栈不断向 native 层扩展,native hook 已经被用于越来越多的技术场景中。Android native hook 的实现方式有很多种,其中使用最广泛,并且通用性最强的是 inline hook 和 PLT hook。


    inline hook 的功能无疑是最强大的,它受到的限制很少,几乎可以 hook 任何地方。inline hook 在线下场景中使用的比较多,业内现有的通用的 inline hook 开源方案或多或少都存在一些稳定性问题,而且基本都缺乏大规模的线上验证。


    PLT hook 的优点是稳定性可控,可以真正的在线上全量使用。但 PLT hook 只能 hook 通过 PLT 表跳转的函数调用,这在一定程度上限制了它的使用场景。


    在真实的线上环境中,经常是 PLT hook 和 inline hook 并存的,这样它们可以各自扬长避短,在不同的场景中发挥作用。


    ELF


    要弄清 Android PLT hook 的原理,需要了解 ELF 文件格式,以及 linker(动态连接器)加载 ELF 文件的过程。


    app_process 和 so 库(动态链接库)都是 ELF(Executable and Linkable Format)格式的文件。对于运行时 native hook 来说,我们主要关心最终的产物,即 ELF 文件。


    ELF 文件的起始处,有一个固定格式的定长的文件头。ELF 文件头中包含了 SHT(section header table)和 PHT(program header table)在当前 ELF 文件中的起始位置和长度。SHT 和 PHT 分别描述了 ELF 的“连接视图”和“执行视图”的基本信息。



    Execution View(执行视图)


    ELF 分为连接视图(Linking View)和执行视图(Execution View)。



    • 连接视图:ELF 未被加载到内存执行前,以 section 为单位的数据组织形式。

    • 执行视图:ELF 被加载到内存后,以 segment 为单位的数据组织形式。


    PLT hook 并不是修改磁盘上的 ELF 文件,而是在运行时修改内存中的数据,因此我们主要关心的是执行视图,即 ELF 被加载到内存后,ELF 中的数据是如何组织和存放的。


    linker 依据 ELF 文件执行视图中的信息,用 mmap 将 ELF 加载到内存中,执行 relocation(重定位)把外部引用的绝对地址填入 GOT 表和 DATA 中,然后设置内存页的权限,最后调用 init_array 中的各个初始化函数。


    PLT hook 执行的时机是在 linker 完全加载完 ELF 之后,我们需要解析内存中的 ELF 数据,然后修改 relocation 的结果。


    ELF 中可以包含很多类型的 section(节),下面介绍一些比较重要的,以及和 PLT hook 相关的 section。


    Dynamic section


    .dynamic 是专门为 linker 设计的,其中包含了 linker 解析和加载 ELF 时会用到的各项数据的索引。linker 在解析完 ELF 头和执行视图的内容后,就会开始解析 .dynamic


    Data(数据)



    • .bss:未初始化的数据。比如:没有赋初值的全局变量和静态变量。(.bss 不占用 ELF 文件体积)

    • .data:已初始化的非只读数据。比如:int g_value = 1;,或者 size_t (*strlen_ptr)(const char *) = strlen;(初始化过程需要 linker relocation 参与才能知道外部 strlen 函数的绝对地址)

    • .rodata:已初始化的只读数据,加载完成后所属内存页会被 linker 设置为只读。比如:const int g_value = 1;

    • .data.rel.ro:已初始化的只读数据,初始化过程需要 linker relocation 参与,加载完成后所属内存页会被 linker 设置为只读。比如:const size_t (*strlen)(const char *) = strlen;


    Code(代码)



    • .text:大多数函数被编译成二进制机器指令后,会存放在这里。

    • .init_array:有时候我们需要在 ELF 被加载后立刻自动执行一些逻辑,比如定义一个全局的 C++ 类的实例,这时候就需要在 .init_array 中调用这个类的构造函数。另外,也可以用 __attribute__((constructor)) 定义单独的 init 函数。

    • .plt:对外部或内部的符号的调用跳板,.plt 会从 .got.data.data.rel.ro 中查询符号的绝对地址,然后执行跳转。


    Symbol(符号)


    符号可以分为两类:“动态链接符号”和“内部符号(调试符号)”,这两个符号集合并不存在严格的相互包含关系,调试器一般会同时加载这两种符号。linker 只关心动态链接符号,内部符号并不会被 linker 加载到内存中。执行 PLT hook 时也只关心动态链接符号。



    • .dynstr:动态链接符号的字符串池,保存了动态链接过程中用到的所有字符串信息,比如:函数名,全局变量名。

    • .dynsym:动态链接符号的索引信息表,起到“关联”和“描述”的作用。


    动态链接符号分为“导入符号”和“导出符号”:



    • 导出符号:指当前 ELF 提供给外部使用的符号。比如:libc.so 中的 open 就是 libc.so 的导出符号。

    • 导入符号:指当前 ELF 需要使用的外部符号。比如:你自己的 libtest.so 如果用到了 open,那么 open 就会被定义为 libtest.so 的导入符号。


    顺便提一下,内部符号的信息包含在 .symtab.strtab.gnu_debugdata 中。


    hash table(哈希表)


    为了加速“动态链接符号的字符串”的查找过程,ELF 中包含了这些字符串的哈希表,通过查哈希表,可以快速确认 ELF 中是否存在某个动态链接符号,以及这个符号对应的信息项在 .dynsym 中的偏移位置。


    历史原因,Android ELF 中会存在两种格式的哈希表:



    • .hash:SYSV hash。其中包含了所有的动态链接符号。

    • .gnu.hash:GNU hash。只包含动态链接符号中的导出符号。


    ELF 中可能同时包含 .hash.gnu.hash,也可能只包含其中一个。具体看 ELF 编译时的静态链接参数 -Wl,--hash-style,可以设置为 sysvgnuboth。从 Android 6.0 开始,linker 支持了 .gnu.hash 的解析。


    linker(动态链接器)



    linker 在加载 ELF 时的最主要工作是 relocation(重定位),这个过程的目的是为当前 ELF 的每个“导入符号”找到对应的外部符号(函数或数据)的绝对地址。最终,这些地址会被写入以下几个地方:



    • .got.plt:保存外部函数的绝对地址。这就是我们经常会听到的 “GOT 表”。

    • .data.data.rel.ro:保存外部数据(包括函数指针)的绝对地址。


    要完成 relocation 过程,需要依赖于 ELF 中的以下信息:



    • .rel.plt.rela.plt:用于关联 .dynsym.got.plt。这就是我们经常会听到的 “PLT 表”。

    • .rel.dyn.rela.dyn.rel.dyn.aps2.rela.dyn.aps2:用于关联 .dynsym.data.data.rel.ro


    Android 只在 64 位实现中使用 RELA 格式,它比 REL 格式多了附加的 r_addend 字段。另外,Android 从 6.0 开始支持 aps2 格式的 .rel.dyn.rela.dyn 数据,这是一种 sleb128 编码格式的数据,读取时需要特别的解码逻辑。


    relocation 完成之后的函数调用关系如下:



    relocation 完成之后的数据引用关系如下:



    Android PLT hook


    PLT hook 基本原理


    了解了 ELF 格式和 linker 的 relocation 过程之后,PLT hook 的过程就不言自明了。它做了和 relocation 类似的事情。即:通过符号名,先在 hash table 中找到对应的符号信息(在 .dynsym 中),再找到对应的 PLT 信息(在 .rel.plt.rela.plt.rel.dyn.rela.dyn.rel.dyn.aps2.rela.dyn.aps2 中),最后找到绝对地址信息(在 .got.plt.data.data.rel.ro 中)。最后要做的就是修改这个绝对地址的值,改为我们需要的自己的“代理函数”的地址。


    要注意的是,在修改这个绝对地址之前,需要先用 mprotect 设置当前地址位置所在内存页为“可写”的,因为 linker 在做完 relocation 后会把 .got.plt.data.rel.ro 设置为只读的。修改完之后,需要用 __builtin___clear_cache 来清除该内存位置的 CPU cache,以使修改能立刻生效。


    xHook 的不足之处


    xHook 是一个开源较早的 Android PLT hook 方案,受到了很多的关注。xHook 比较好的实现了 ELF 解析和绝对地址替换的工作。但是作为一个工程化的 PLT hook 方案,xHook 存在很多不足之处,主要有:



    • native 崩溃兜底机制有缺陷,导致线上崩溃无法完全避免。

    • 无法自动对新加载的 ELF 执行 hook。(需要外部反复调用 refresh 来“发现”新加载的 ELF。但是在什么时机调用 refresh 呢?频率太高会影响性能,频率太低会导致 hook 不及时)

    • 由于依赖于链式调用的机制。如果一个调用点被多次 hook,在对某个 proxy 函数执行 unhook 后,链中后续的 proxy 函数就会丢失。

    • 只使用了读 maps 的方式来遍历 ELF。在高版本 Android 系统和部分机型中兼容性不好,经常会发生 hook 不到的情况。

    • API 设计中使用了正则来指定 hook 哪些目标 ELF,运行效率不佳。

    • 需要在真正执行 hook 前,注册完所有的 hook 点,一旦开始执行 hook(调用 refresh 后),不能再添加 hook 点。这种设计是很不友好的。

    • 无法适配 Android 8.0 引入 Linker Namespace 机制(同一个函数符号,在进程中可能存在多个实现)。


    由于存在上述这些稳定性、有效性、功能性上的问题,使 xHook 难以真正大规模的用于线上环境中。


    更完善的 Android PLT hook 方案


    我们迫切需要一个新的更完善的 Android PLT hook 方案,它应该是什么样子的呢?我认为它应该满足这些条件:



    • 要有一套真正可靠的 native 崩溃兜底机制,来避免可控范围内的 native 崩溃。

    • 可以随时 hook 和 unhook 单个、部分、全部的调用者 ELF。

    • 当新的 ELF 被加载到内存后,它应该自动的被执行所有预定的 hook 操作。

    • 多个使用方如果 hook 了同一个调用点,它们应该可以彼此独立的执行 unhook,相互不干扰。

    • 为了适配 Android linker namespace,应该可以指定 hook 函数的被调用者 ELF。

    • 能自动避免由于 hook 引起的意外的“递归调用”和“环形调用”。比如:open 的 proxy 函数中调用了 read,然后 read 的 proxy 函数中又调用了 open。如果这两个 proxy 存在于两个独立的 SDK 中,此时形成的环形调用将很难在 SDK 开发阶段被发现。如果在更多的 SDK 之间形成了一个更大的 proxy 函数调用环,情况将会失去控制。

    • proxy 函数中要能以正常的方式获取 backtrace(libunwind、libunwindstack、llvm libunwind、FP unwind 等)。有大量的业务场景是需要 hook 后在 proxy 函数中抓取和保存 backtrace,然后在特定的时机 dump 和聚合这些 backtrace,符号化后再将数据投递到服务端,从而监控和发现业务问题。

    • hook 管理机制本身带来的额外性能损耗要足够低。


    我们带着上面的这些目标设计和开发了 bhook。


    字节 bhook 介绍


    ELF 和 linker 前面已经介绍过了,下面介绍 bhook 中另外几个关键模块。



    DL monitor


    在 Android 系统中,动态加载 so 库最终是通过 dlopenandroid_dlopen_ext 完成的,通过 dlclose 则可以卸载 so 库。


    bhook 在内部 hook 了这三个函数调用。因此,当有新的 so 被加载到内存后,bhook 能立刻感知到,于是可以立刻对它执行预定的 hook 任务。当有 so 正在被卸载时,bhook 也能立刻感知到,并且会通过内部的读写锁机制与“ELF cache 和 hook 执行模块”同步,以此保证“正在被 hook 的 so 不会正在被卸载”。


    Android 从 7.0 开始不再允许 app 中 dlopen 系统库;从 8.0 开始引入了 linker namespace 机制,并且 libdl.so 不再是 linker 的虚拟入口,而成为了一个真实的 so 文件。对于 linker 来说,Android 7.0 和 8.0 是两个重要的版本。


    我们需要设法绕过系统对 app dlopen 系统库的限制,否则 hook dlopenandroid_dlopen_ext 之后,在代理函数中是无法直接调用原始的 dlopenandroid_dlopen_ext 函数的。


    这里我们参考了 ByteDance Raphael(github.com/bytedance/m… Android 7.0 开始,hook dlopenandroid_dlopen_ext 后不再调用原函数,而是通过调用 linker 和 libdl.so 内部函数的方式绕过了限制。主要用到了以下几个符号对应的内部函数:


    Android 7.x linker:


    __dl__ZL10dlopen_extPKciPK17android_dlextinfoPv
    __dl__Z9do_dlopenPKciPK17android_dlextinfoPv
    __dl__Z23linker_get_error_bufferv
    __dl__ZL23__bionic_format_dlerrorPKcS0_

    Android 8.0+ libdl.so:


    __loader_dlopen
    __loader_android_dlopen_ext

    trampoline


    简单的 PLT hook 方案(比如 xHook)是不需要 trampoline 的,只需要替换 .got.plt(和 .data.data.rel.ro)中的绝对地址就可以了。但是这种方式会导致“同一个 hook 点的多个 proxy 函数形成链式调用”(类似于 Linux 通过 sigaction 注册的 signal handler),如果其中一个 proxy 被 unhook 了,那么“链” 中后续的 proxy 也会丢失。xHook 就存在这个问题:



    当 proxy 1 被 unhook 后,proxy 2 也从调用链上消失了,因为 proxy 1 根本不知道 proxy 2 的存在,在 unhook proxy 1 时,会试图恢复最初的初始值,即 callee 的地址。


    为了解决这个问题,对于每个被 hook 的函数调用点,我们都需要一个对应的管理入口函数,我们改为在 GOT 表中写入这个管理入口函数的地址。同时,对于每个被 hook 的函数调用点,我们还需要维护一个 proxy 函数列表,在管理入口函数中,需要遍历和调用 proxy 函数列表中的每一个具体 proxy 函数。


    为了在运行时达到指定跳转的效果,我们需要用 mmapmprotect 来创建 shellcode。按照术语惯例,我们把这里创建的跳转逻辑称为 trampoline(蹦床):



    另外,为了检测和避免“环形调用”,每次 trampoline 开始执行时,都会开始记录 proxy 函数的执行栈,在 proxy 函数链中遍历执行时,会检测当前待执行的 proxy 函数是否已经在执行栈中出现过,如果出现过,就说明发生了“环形调用”,此时会忽略 proxy 函数链中后续所有的 proxy 函数,直接执行最后的“原函数”。


    trampoline 实现的难点在于性能。trampoline 给执行流程注入了额外的逻辑,在多线程环境中,proxy 调用链会被高频的遍历,其中保存的 proxy 函数可能随时会增加和减少,我们还需要保存 proxy 函数的执行栈。所有这些逻辑都不能加锁,否则 hook 高频函数时,性能损耗会比较明显。


    native 崩溃兜底


    执行 hook 操作时,需要直接计算很多的内存绝对地址,然后对这些内存位置进行读写,但这样做并不总是安全的,我们可能会遇到这些情况:



    • 在 DL monitor 初始化的过程中,对 dlclose 的 hook 尚未完成时,此时 linker 执行了 dlclose,恰恰 dlclose 了我们正在执行 dlclose hook 操作的 ELF。

    • ELF 文件可能意外损坏,导致 linker 加载了格式不正确的 ELF。


    这时候,对指定内存位置的读写可能会发生 sigsegv 或 sigbus,导致 native 崩溃。我们需要一种类似 Java / C++ try-catch 的机制来保护这种危险的操作,避免发生崩溃:


    int *p = NULL;

    TRY(SIGSEGV, SIGBUS) {
    *p = 1;
    } CATCH() {
    LOG("There was a problem, but it's okay.");
    } EXIT

    当崩溃发生时,因为我们明白在保护的代码区间中只有“内存读”或“单个内存写”操作,因此忽略这种崩溃并不会带来任何副作用。在 Java 虚拟机中,也有类似的机制用于检测 native 崩溃,并且创建合适的 Java 异常。


    bhook 通过注册 sigsegv 和 sigbus 信号处理函数来进行 native 崩溃兜底,在 try 块开头用 sigsetjmp 保存寄存器和 sigmask,当发生崩溃时,在信号处理函数中用 siglongjmp 跳转到 catch 块中并恢复 sigmask。


    值得注意的几个问题:



    • ART sigchain 代理了 sigactionsigprocmask 等函数,我们需要用 dlsym 在 libc.so 中找到原始的函数再调用它们。

    • bionic 和 ART sigchain 在某些 AOSP 版本上存在 bug,所以我们需要优先使用 sigaction64sigprocmask64,而不是 sigactionsigprocmask

    • 在正确的地方用正确的方式设置 sigmask 很重要。

    • 我们的 try-catch 机制运行于多线程环境中,所以需要以某种线程独立的方式来保存 sigjmp_buf

    • 考虑到性能和更多使用场景,整个机制需要无锁、无堆内存分配、无 TLS 操作、线程安全,异步信号安全。


    bhook 的 native 崩溃兜底模块经过了比较严格的压力测试和线上测试,如果正确的使用,可以达到预期的效果。如你在 bhook 的源码中所见,我们故意把这个模块设计成现在的样子(只有一个 .c 和 一个 .h 文件,并且没有任何外部依赖),这样做的好处是容易移植和复用。如果你想把这个模块用在自己的工程中,请注意以下几点:



    • native 崩溃兜底属于“高危”操作,可能引起不确定的难以排查的问题。所以能不用尽量不要用。

    • 纯业务类型的 native 库请不要使用 native 崩溃兜底。而是应该让崩溃暴露出来,然后修复问题。

    • try 块中的逻辑越少越好。比如兜底 sigsegv 和 sigbus 时,最好 try 块中只有一些内存地址的读操作和单个写操作,尽量不要调用外部函数(包括 mallocfreenewdelete 等)。

    • try 块中尽量不要使用 C++。某些 C++ 的语法封装,编译器会为它生成一些意外的逻辑(比如读写 C++ TLS 变量,编译器会生成 _emutls_get_address 调用,其中可能会调用 malloc)。

    • 在当前的设计中:try 块中请不要调用 return,否则会跳过 catch 或 exit 块中的回收逻辑,引起难以排查的问题。另外,在 try 块中不可以嵌套使用另一个“相同信号的 try”。
    收起阅读 »

    RecyclerView 添加分割线,ItemDecoration 的实用技巧

    官网解释: An ItemDecoration allows the application to add a special drawing and layout offset to specific item views from the adapter'...
    继续阅读 »

    官网解释: An ItemDecoration allows the application to add a special drawing and layout offset to specific item views from the adapter's data set. This can be useful for drawing dividers between items, highlights, visual grouping boundaries and more.


    我的理解:ItemDecoration 允许我们给 recyclerview 中的 item 添加专门的绘制和布局;比如分割线、强调和装饰等等。


    默认 recyclerview 的表现像下面这样


    image.png


    其实我想要的是这样


    image.png


    如果我们不使用这个的话,那么我们在编写 xml 文件的时候只能添加 layout_margin 这样的值,而且即便这样在有些场景下也是不好用的。其实也没关系我们可以使用代码控制,比如在 onBindViewHolder 中根据数据的位置写对应的逻辑,像我上面那种我需要把最后一个数据多对应的 layout_margin 给去掉,这样也是完全没问题的,只不过如果采用了这样的方式,首先如果我们把 layout_margin 设置到每一项上,那么将来要复用这个 xml 文件,由于间距不同,我们就没法复用,或者复用也需要在代码中控制。如果使用这个,就会非常的简单,并且不会在 adapter 中再使用代码控制了。


    使用这个需要进行两步:



    1. 实现自己的 ItemDecoration 子类;

    2. 添加到 recyclerView


    1. 实现自己的 ItemDecoration 子类


    这个类在 androidx.recyclerview.widget.RecyclerView.ItemDecoration 下:


    class ItemSeparatorDecoration: RecyclerView.ItemDecoration()

    这样就实现了,下面我们看看 ItemDecoration 的源代码,我把将要废弃的 API 都删掉:


    abstract class ItemDecoration {
    public void onDraw(Canvas c, RecyclerView parent, State state) {}
    public void onDrawOver(Canvas c, RecyclerView parent, State state) {}
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, State state) {}
    }

    发现我们可以重写这三个函数,下面说一下这三个的含义:


    1)void onDraw(Canvas c, RecyclerView parent, State state)


    参数的含义:



    • Canvas c 》 canvas 绘制对象

    • RecyclerView 》 parent RecyclerView 对象本身

    • State state 》 当前 RecyclerView 的状态


    作用就是绘制,可以在任何位置绘制,如果只是想绘制到每一项里面,那么就需要计算出对应的位置。


    2)void onDrawOver(Canvas c, RecyclerView parent, State state)


    跟上面一样,不同的地方在于绘制的总是在最上面,也就是绘制出来的不会被遮挡。


    3)void getItemOffsets(Rect outRect, View view, RecyclerView parent, State state)


    参数的含义:



    • Rect outRect 》item 四周的距离对象

    • View view 》 当前 view

    • RecyclerView 》 parent RecyclerView 本身

    • State state 》 RecyclerView 状态


    这里可以设置 itemRecyclerView 各边的距离。这里需要说明一下,我这里说的到各边的距离指的是啥?


    image.png


    2. 实现上面的间隔


    实现间隔是最简单的,因为我们只需要重写 getItemOffsets 函数,这个函数会在绘制每一项的时候调用,所以在这里我们只需要处理每一项的间隔,下面是重写代码,注意这里的单位并不是 dp ,而是 px ,所以如果需要使用 dp 的话,那么就需要自己转换一下,如果你不知道转换可以定义 dpdimen.xml 中,然后直接在代码中获取:


    context.resources.getDimensionPixelSize(R.dimen.test_16dp)

    其中 R.dimen.test_16dp 就是你定义好的值。


    下面看重写的 getItemOffsets 函数:


    override fun getItemOffsets(
    outRect: Rect,
    view: View,
    parent: RecyclerView,
    state: RecyclerView.State
    )
    {
    super.getItemOffsets(outRect, view, parent, state)
    if (parent.getChildLayoutPosition(view) != 0) {
    outRect.top = context.resources.getDimensionPixelSize(R.dimen.test_10dp)
    }
    }

    有没有发现很简单,这样就可以实现上边的效果,只不过最常见的应该还是分割线了。


    3. 实现分割线


    看代码:


    class MyItemDivider(val context: Context, orientation: Int) : RecyclerView.ItemDecoration() {
    companion object {
    // 分割线的 attr
    private val ATTRS = intArrayOf(android.R.attr.listDivider)
    const val HORIZONTAL_LIST = LinearLayoutManager.HORIZONTAL
    const val VERTICAL_LIST = LinearLayoutManager.VERTICAL
    }

    // 分割线绘制所需要的 Drawable ,当然也可以直接使用 Canvas 绘制,只不过我这里使用 Drawable
    private var mDivider: Drawable? = null
    private var mOrientation: Int? = null

    init {
    val a = context.obtainStyledAttributes(ATTRS)
    mDivider = a.getDrawable(0)
    a.recycle()
    setOrientation(orientation)
    }

    /**
    * 设置方向,如果是 RecyclerView 是上下方向,那么这里设置 VERTICAL_LIST ,否则设置 HORIZONTAL_LIST
    * @param orientation 方向
    */

    private fun setOrientation(orientation: Int) {
    // 传入的值必须是预先定义好的
    if (orientation != HORIZONTAL_LIST && orientation != VERTICAL_LIST) {
    throw IllegalArgumentException("invalid orientation")
    }
    mOrientation = orientation
    }

    /**
    * 开始绘制,这个函数只会执行一次,
    * 所以我们在绘制的时候需要在这里把所有项的都绘制,
    * 而不是只处理某一项
    */

    override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
    super.onDraw(c, parent, state)
    if (mOrientation == VERTICAL_LIST) {
    drawVertical(c, parent)
    } else {
    drawHorizontal(c, parent)
    }
    }

    private fun drawHorizontal(c: Canvas, parent: RecyclerView) {
    val top = parent.paddingTop
    val bottom = parent.height - parent.paddingBottom
    val childCount = parent.childCount
    for (i in 0 until childCount) {
    val child = parent.getChildAt(i)
    val params = child.layoutParams as RecyclerView.LayoutParams
    val left = child.right + params.rightMargin
    val right = left + (mDivider?.intrinsicWidth ?: 0)
    mDivider?.setBounds(left, top, right, bottom)
    mDivider?.draw(c)
    }
    }

    private fun drawVertical(c: Canvas, parent: RecyclerView) {
    // 左边的距离,
    // 意思是左边从哪儿开始绘制,
    // 对于每一项来说,
    // 肯定需要将 RecyclerView 的左边的 paddingLeft 给去掉
    val left = parent.paddingLeft
    // 右边就是 RecyclerView 的宽度减去 RecyclerView 右边设置的 paddingRight 值
    val right = parent.width - parent.paddingRight
    // 获取当前 RecyclerView 下总共有多少 Item
    val childCount = parent.childCount
    // 循环把每一项的都绘制完成,如果最后一项不需要,那么这里的循环就少循环一次
    for (i in 0 until childCount) {
    val child = parent.getChildAt(i)
    val params = child.layoutParams as RecyclerView.LayoutParams
    // 上边的距离就是当前 Item 下边再加上本身设置的 marginBottom
    val top = child.bottom + params.bottomMargin
    // 下边就简单了,就是上边 + 分割线的高度
    val bottom = top + (mDivider?.intrinsicHeight ?: 0)
    mDivider?.setBounds(left, top, right, bottom)
    mDivider?.draw(c)
    }
    }

    // 这个函数会被反复执行,执行的次数跟 Item 的个数相同
    override fun getItemOffsets(
    outRect: Rect,
    view: View,
    parent: RecyclerView,
    state: RecyclerView.State
    )
    {
    super.getItemOffsets(outRect, view, parent, state)
    // 由于在上面的距离绘制,但是实际上那里不会主动为我们绘制腾出空间,
    // 需要重写这个函数来手动调整空间,给上面的绘制不会被覆盖
    if (mOrientation == VERTICAL_LIST) {
    outRect.set(0, 0, 0, mDivider?.intrinsicHeight ?: 0)
    } else {
    outRect.set(0, 0, mDivider?.intrinsicWidth ?: 0, 0)
    }
    }
    }

    代码来源于刘望舒的三部曲,我对代码进行了解释和说明。大家可能在代码中的距离那一块不是很明白,直接看下面的图就很明白的。


    1629513919(1).png 注意 top 我只标注了距离当前 Item 的距离,其实不是,其实是距离最上面的距离,这里这样标注是跟代码保持统一;假如上面的红色方框是我们要画的分割线,那么我们要获取的值对应上面的标注。一般 onDrawgetItemOffsets 要配合使用,如果不的话,那么你绘制的也看不见,即便看见了也是不正常的。原因我在上面讲到了, onDraw 绘制会绘制到 Item 的下面,所以如果没有留足空间的话,那么结果就是看不见绘制的内容。


    内容还会补充,同时关于 RecyclerView 的将来陆续推出,真正做到完全攻略,从使用到问题解决再到源码分析。

    收起阅读 »

    Android输入系统之 的创建与启动

    今天趁着在公司摸鱼的时间,来更新一篇文章。 上一篇文章 InputManagerService的创建与启动 分析了 IMS 的创建与启动,这其中就伴随着 InputReader 的创建与启动,本文就着重分析这两点内容。 本文所涉及的文件路径如下 fram...
    继续阅读 »

    今天趁着在公司摸鱼的时间,来更新一篇文章。


    上一篇文章 InputManagerService的创建与启动 分析了 IMS 的创建与启动,这其中就伴随着 InputReader 的创建与启动,本文就着重分析这两点内容。


    本文所涉及的文件路径如下


    frameworks/native/services/inputflinger/InputManager.cpp frameworks/native/services/inputflinger/reader/EventHub.cpp frameworks/native/services/inputflinger/reader/InputReader.cpp frameworks/base/services/core/jni/com_android_server_input_InputManagerService.cpp frameworks/native/services/inputflinger/InputThread.cpp


    InputManagerService的创建与启动 可知,创建 InputReader 的代码如下


    InputManager::InputManager(
    const sp<InputReaderPolicyInterface>& readerPolicy,
    const sp<InputDispatcherPolicyInterface>& dispatcherPolicy) {

    // ...

    mReader = createInputReader(readerPolicy, mClassifier);
    }


    sp<InputReaderInterface> createInputReader(const sp<InputReaderPolicyInterface>& policy,
    const sp<InputListenerInterface>& listener)
    {
    // InputReader从EventHub中读取数据
    // policy 实现类是 NativeInputManager
    // listener的实现类其实是 InputClassifier
    return new InputReader(std::make_unique<EventHub>(), policy, listener);
    }

    创建 InputReader 需要三个参数。


    第一个参数的类型是 EventHub。正如名字所示,它是输入事件的中心,InputReader 会从 EventHub 中读取事件。这个事件分两类,一个是输入设备的输入事件,另一个是 EventHub 合成事件,用于表明设备的挂载与卸载。


    第二个参数的类型为 InputReaderPolicyInterface,由 InputManagerService的创建与启动 可知,它的实现类是 JNI 层的 NativeInputManager。


    第三个参数的类型为 InputListenerInterface, 由 InputManagerService的创建与启动 可知,它的实现类是 InputClassifier。InputReader 会的加工后的事件发送给 InputClassifier,而 InputClassifier 会针对触摸事件进行分类,再发送到 InputDispatcher。


    为止防止大家有所健忘,我把上一篇文章中,关于事件的流程图,再展示下


    graph TD
    EventHub --> InputReader
    InputReader --> NativeInputManager
    InputReader --> InputClassifer
    InputClassifer --> InputDispatcher
    InputDispatcher --> NativeInputManager
    NativeInputManager --> InputManagerService

    创建EventHub


    创建 InputReader 首先需要一个 EventHub 对象,因此我们首先得看下 EventHub 的创建过程


    EventHub::EventHub(void)
    : mBuiltInKeyboardId(NO_BUILT_IN_KEYBOARD),
    mNextDeviceId(1),
    mControllerNumbers(),
    mOpeningDevices(nullptr),
    mClosingDevices(nullptr),
    mNeedToSendFinishedDeviceScan(false),
    mNeedToReopenDevices(false),
    mNeedToScanDevices(true),
    mPendingEventCount(0),
    mPendingEventIndex(0),
    mPendingINotify(false) {
    ensureProcessCanBlockSuspend();

    // 创建epoll
    mEpollFd = epoll_create1(EPOLL_CLOEXEC);

    // 初始化inotify
    mINotifyFd = inotify_init();
    // 1. 使用inotify监听/dev/input目录下文件的创建与删除事件
    mInputWd = inotify_add_watch(mINotifyFd, DEVICE_PATH, IN_DELETE | IN_CREATE);
    // ...

    struct epoll_event eventItem = {};
    eventItem.events = EPOLLIN | EPOLLWAKEUP;
    eventItem.data.fd = mINotifyFd;
    // 2. epoll监听inotify可读事件
    int result = epoll_ctl(mEpollFd, EPOLL_CTL_ADD, mINotifyFd, &eventItem);

    // 3. 创建两个管道
    int wakeFds[2];
    result = pipe(wakeFds);

    mWakeReadPipeFd = wakeFds[0];
    mWakeWritePipeFd = wakeFds[1];

    result = fcntl(mWakeReadPipeFd, F_SETFL, O_NONBLOCK);

    result = fcntl(mWakeWritePipeFd, F_SETFL, O_NONBLOCK);

    eventItem.data.fd = mWakeReadPipeFd;
    // 4. epoll监听mWakeReadPipeFd可读事件
    result = epoll_ctl(mEpollFd, EPOLL_CTL_ADD, mWakeReadPipeFd, &eventItem);
    }

    第一步和第二步,初始化 inotify 监听 /dev/input/ 目录下的文件的创建和删除事件,然后使用 epoll 管理这个 inotify。


    为何要使用 inotify 监听 /dev/input/ 目录呢,因为当输入设备挂载和卸载时,内核会相应地在这个目录下创建和删除设备文件,因此监听这个目录可获知当前有哪些输入设备,然后才能监听这些设备的输入事件。


    第三步和第四步,创建了两个管道,其中一个管道也被 epoll 管理起来,这个管道是用来唤醒 InputReader 线程。例如当配置发生改变时,这个管道会被用来唤醒 InputReader 线程来处理配置的改变。



    另一个管道用于做什么呢?



    现在 epoll 已经管理了两个文件描述符,mINotifyFd 和 mWakeReadPipeFd。但是现在并没有启动 epoll 来监听它们的可读事件,这是因为 InputReader 还没有准备好,让我们继续往下看。



    本文不想浪费篇幅去介绍 Linux inotify 和 epoll 机制,这两个机制并不复杂,请大家自己去了解。



    创建 InputReader


    EventHub 已经创建完毕,现在来看下创建 InputReader 的过程


    InputReader::InputReader(std::shared_ptr<EventHubInterface> eventHub,
    const sp<InputReaderPolicyInterface>& policy,
    const sp<InputListenerInterface>& listener)
    : mContext(this), // ContextImpl mContext 是一个关于 InputReader 的环境
    mEventHub(eventHub),
    mPolicy(policy), // 由 NativeInputManager实现
    mGlobalMetaState(0),
    mGeneration(1),
    mNextInputDeviceId(END_RESERVED_ID),
    mDisableVirtualKeysTimeout(LLONG_MIN),
    mNextTimeout(LLONG_MAX),
    mConfigurationChangesToRefresh(0) {
    // 1. 创建QueuedInputListener对象
    // 事件的转换都是通过 InputListenerInterface 接口
    // QueuedInputListener 是一个继承并实现了 InputListenerInterface 接口的代理类
    // QueuedInputListener 把事件加入队列,并推迟发送事件直到调用它的flush()函数
    mQueuedListener = new QueuedInputListener(listener);

    { // acquire lock
    AutoMutex _l(mLock);
    // 2. 更新配置,保存到mConfig中
    refreshConfigurationLocked(0);
    // 根据已经映射的设备,更新 mGlobalMetaState 的值
    // 由于目前还没有映射设备,所以mGlobalMetaState值为0
    updateGlobalMetaStateLocked();
    } // release lock
    }

    InputReader 构造函数看似平平无奇,实际上有许多值得注意的地方。


    首先注意 mContext 变量,它的类型是 ContextImpl,这是一个表示 InputReader 的环境,由于 ContextImpl 是 InputReader 的友元类,因此透过 ContextImpl 可以访问 InputReader 的私有数据。


    那么这个 mContext 变量被谁所用呢? InputReader 会为物理输入设备建立一个映射类 InputDevice,这个 InputDevice 就会保存这个 mContext 变量,InputDevice 会通过 mContext,从 InputReader 中获取全局的设备状态以及参数。


    InputReader 构造函数使用了一个 InputClassifier 接口对象,由 InputManagerService的创建与启动 可知,InputListenerInterfac 接口的实现类是 InputClassifier。 实际上,InputListenerInterface 接口是专为传递事件设计的。因此只要你看到哪个类实现 ( 在c++中叫继承 ) 了 InputListenerInterface 这个接口,那么它肯定是传递事件中的一环。


    mQueuedListener 变量的类型是 QueuedInputListener ,恰好这个类也实现了 InputListenerInterface 接口,那么它肯定也传递事件。然而 QueuedInputListener 只是一个代理类,InputReader 会把事件存储到 QueuedInputListener 的队列中,然后直到 QueuedInputListener::flush() 函数被调用,QueuedInputListener 才把队列中的事件发送出去。发送给谁呢,就是 InputClassifier。


    那么现在我们来总结下,事件通过 InputListenerInterface 接口传递的关系图


    graph TD
    InputReader --> |InputListenerInterface|QueuedInputListener
    QueuedInputListener --> |InputListenerInterface|InputClassifier
    InputClassifier --> |InputListenerInterface|InputDispatcher

    最后,我们来一件挺烦琐的小事,InputReader 读取配置,它调用的是如下代码


    // 注意,参数 changes 值为0
    void InputReader::refreshConfigurationLocked(uint32_t changes) {
    // 从NativeInputManager中获取配置,保存到mConfig中
    mPolicy->getReaderConfiguration(&mConfig);
    // EventHub保存排除的输入设备
    mEventHub->setExcludedDevices(mConfig.excludedDeviceNames);

    if (changes) {
    // ...
    }
    }

    mPolicy 的实现类是 JNI 层 NativeInputManager,由 InputManagerService的创建与启动 可知, NativeInputManager 只是一个桥梁作用,那么它肯定是向上层的 InputManagerService 获取配置,是不是这样呢,来验证下。


    void NativeInputManager::getReaderConfiguration(InputReaderConfiguration* outConfig) {
    ATRACE_CALL();
    JNIEnv* env = jniEnv();

    // 0
    jint virtualKeyQuietTime = env->CallIntMethod(mServiceObj,
    gServiceClassInfo.getVirtualKeyQuietTimeMillis);
    if (!checkAndClearExceptionFromCallback(env, "getVirtualKeyQuietTimeMillis")) {
    outConfig->virtualKeyQuietTime = milliseconds_to_nanoseconds(virtualKeyQuietTime);
    }

    outConfig->excludedDeviceNames.clear();
    // 如下两个文件定义了排除的设备
    // /system/etc/excluded-input-devices.xml
    // /vendor/etc/excluded-input-devices.xml
    jobjectArray excludedDeviceNames = jobjectArray(env->CallStaticObjectMethod(
    gServiceClassInfo.clazz, gServiceClassInfo.getExcludedDeviceNames));
    if (!checkAndClearExceptionFromCallback(env, "getExcludedDeviceNames") && excludedDeviceNames) {
    jsize length = env->GetArrayLength(excludedDeviceNames);
    for (jsize i = 0; i < length; i++) {
    std::string deviceName = getStringElementFromJavaArray(env, excludedDeviceNames, i);
    outConfig->excludedDeviceNames.push_back(deviceName);
    }
    env->DeleteLocalRef(excludedDeviceNames);
    }

    // Associations between input ports and display ports
    // The java method packs the information in the following manner:
    // Original data: [{'inputPort1': '1'}, {'inputPort2': '2'}]
    // Received data: ['inputPort1', '1', 'inputPort2', '2']
    // So we unpack accordingly here.
    // 输入端口和显示端口绑定的关系,一种是静态绑定,来自于/vendor/etc/input-port-associations.xml
    // 而另一种是来自于运行时的动态绑定,并且动态绑定可以覆盖静态绑定。
    outConfig->portAssociations.clear();
    jobjectArray portAssociations = jobjectArray(env->CallObjectMethod(mServiceObj,
    gServiceClassInfo.getInputPortAssociations));
    if (!checkAndClearExceptionFromCallback(env, "getInputPortAssociations") && portAssociations) {
    jsize length = env->GetArrayLength(portAssociations);
    for (jsize i = 0; i < length / 2; i++) {
    std::string inputPort = getStringElementFromJavaArray(env, portAssociations, 2 * i);
    std::string displayPortStr =
    getStringElementFromJavaArray(env, portAssociations, 2 * i + 1);
    uint8_t displayPort;
    // Should already have been validated earlier, but do it here for safety.
    bool success = ParseUint(displayPortStr, &displayPort);
    if (!success) {
    ALOGE("Could not parse entry in port configuration file, received: %s",
    displayPortStr.c_str());
    continue;
    }
    outConfig->portAssociations.insert({inputPort, displayPort});
    }
    env->DeleteLocalRef(portAssociations);
    }

    // 下面这些都与悬浮点击有关系,如果触摸屏支持悬浮点击,可以研究下这些参数
    jint hoverTapTimeout = env->CallIntMethod(mServiceObj,
    gServiceClassInfo.getHoverTapTimeout);
    if (!checkAndClearExceptionFromCallback(env, "getHoverTapTimeout")) {
    jint doubleTapTimeout = env->CallIntMethod(mServiceObj,
    gServiceClassInfo.getDoubleTapTimeout);
    if (!checkAndClearExceptionFromCallback(env, "getDoubleTapTimeout")) {
    jint longPressTimeout = env->CallIntMethod(mServiceObj,
    gServiceClassInfo.getLongPressTimeout);
    if (!checkAndClearExceptionFromCallback(env, "getLongPressTimeout")) {
    outConfig->pointerGestureTapInterval = milliseconds_to_nanoseconds(hoverTapTimeout);

    // We must ensure that the tap-drag interval is significantly shorter than
    // the long-press timeout because the tap is held down for the entire duration
    // of the double-tap timeout.
    jint tapDragInterval = max(min(longPressTimeout - 100,
    doubleTapTimeout), hoverTapTimeout);
    outConfig->pointerGestureTapDragInterval =
    milliseconds_to_nanoseconds(tapDragInterval);
    }
    }
    }

    // 悬浮移动距离
    jint hoverTapSlop = env->CallIntMethod(mServiceObj,
    gServiceClassInfo.getHoverTapSlop);
    if (!checkAndClearExceptionFromCallback(env, "getHoverTapSlop")) {
    outConfig->pointerGestureTapSlop = hoverTapSlop;
    }

    // 如下mLocked的相关参数是在 NativeInputManager 的构造函数中初始化的
    // 但是这些参数都是可以通过 InputManagerService 改变的
    { // acquire lock
    AutoMutex _l(mLock);

    outConfig->pointerVelocityControlParameters.scale = exp2f(mLocked.pointerSpeed
    * POINTER_SPEED_EXPONENT);
    outConfig->pointerGesturesEnabled = mLocked.pointerGesturesEnabled;

    outConfig->showTouches = mLocked.showTouches;

    outConfig->pointerCapture = mLocked.pointerCapture;

    outConfig->setDisplayViewports(mLocked.viewports);

    outConfig->defaultPointerDisplayId = mLocked.pointerDisplayId;

    outConfig->disabledDevices = mLocked.disabledInputDevices;
    } // release lock
    }

    从这里可以看出,InputReader 获取配置的方式,是通过 JNI 层的 NativeInputManager 向 Java 层的 InputManagerService 获取的。


    但是这些配置并不是不变的,当Java层改变这些配置后,会通过 JNI 层的 NativeInputManager 通知 InputReader ( 注意,不是InputReader线程 ),然后通过 EventHub::wake() 函数通过管道唤醒 InputReader 线程来处理配置改变。这个过程可以在阅读完本文后,自行分析。


    启动 InputReader


    现在 InputReader 已经创建完毕,让我们继续看下它的启动过程。


    InputManagerService的创建与启动 可知,启动 InputReader 的代码如下


    status_t InputReader::start() {
    if (mThread) {
    return ALREADY_EXISTS;
    }
    // 创建线程并启动
    mThread = std::make_unique<InputThread>(
    "InputReader", [this]() { loopOnce(); }, [this]() { mEventHub->wake(); });
    return OK;
    }

    InputThread 封装了 c++ 的 Thread 类


    class InputThreadImpl : public Thread {
    public:
    explicit InputThreadImpl(std::function<void()> loop)
    : Thread(/* canCallJava */ true), mThreadLoop(loop) {
    }

    ~InputThreadImpl() {}

    private:
    std::function<void()> mThreadLoop;

    bool threadLoop() override {
    mThreadLoop();
    return true;
    }
    };

    当 InputThread 对象创建的时候,会启动一个线程


    InputThread::InputThread(std::string name, std::function<void()> loop, std::function<void()> wake)
    : mName(name), mThreadWake(wake) {
    mThread = new InputThreadImpl(loop);
    mThread->run(mName.c_str(), ANDROID_PRIORITY_URGENT_DISPLAY);
    }

    线程会循环调用 loopOnce() 函数,也就是 InputThread 构造函数的第二个参数,它的实现函数是 InputReader::loopOnce() 函数。


    我们注意到 InputThread 构造函数还有第三个参数,它是在 InputThread 析构函数调用的。


    InputThread::~InputThread() {
    mThread->requestExit();
    // mThreadWake 就是构造函数中的第三个参数
    if (mThreadWake) {
    mThreadWake();
    }
    mThread->requestExitAndWait();
    }

    那么什么时候会调用 InputThread 的析构函数呢,我觉得应该是 system_server 进程挂掉的时候,此时会调用 EventHub::wake() 来唤醒 InputReader 线程,从而退出 InputReader 线程。而这个唤醒的方式,就是使用刚才在 EventHub 中创建的一个管道。


    现在来分析下 InputReader::loopOnce() 函数,这里就是 InputReader 线程所做的事


    void InputReader::loopOnce() {
    int32_t oldGeneration;
    int32_t timeoutMillis;
    bool inputDevicesChanged = false;
    std::vector<InputDeviceInfo> inputDevices;

    // 1. 处理配置改变
    { // acquire lock
    AutoMutex _l(mLock);
    oldGeneration = mGeneration;
    timeoutMillis = -1;
    uint32_t changes = mConfigurationChangesToRefresh;
    if (changes) {
    mConfigurationChangesToRefresh = 0;
    timeoutMillis = 0;
    refreshConfigurationLocked(changes);
    } else if (mNextTimeout != LLONG_MAX) { // mNextTimeout 也属于配置
    nsecs_t now = systemTime(SYSTEM_TIME_MONOTONIC);
    timeoutMillis = toMillisecondTimeoutDelay(now, mNextTimeout);
    }
    } // release lock

    // 2. 读取事件
    size_t count = mEventHub->getEvents(timeoutMillis, mEventBuffer, EVENT_BUFFER_SIZE);

    // 3. 处理事件
    { // acquire lock
    AutoMutex _l(mLock);
    mReaderIsAliveCondition.broadcast();

    // 如果读到事件,就处理
    if (count) {
    processEventsLocked(mEventBuffer, count);
    }

    // 处理超时情况
    if (mNextTimeout != LLONG_MAX) {
    // ...
    }

    // mGeneration 表明输入设备改变
    if (oldGeneration != mGeneration) {
    inputDevicesChanged = true;
    // 对inputDevices填充inputDeviceInfo,而这个InputDeviceInfo是从InputDevice中获取
    getInputDevicesLocked(inputDevices);
    }
    } // release lock

    // 4. 通知设备改变
    if (inputDevicesChanged) {
    // mPolicy实现类为NativeInputManager
    mPolicy->notifyInputDevicesChanged(inputDevices);
    }

    // 5. 事件发送给 InputClassifier。
    mQueuedListener->flush();
    }

    我第一次看到这段代码时,头皮发麻,InputReader 做了这么多事情,我该怎么分析呢?不要紧,让我来梳理下思路。


    首先看第一步,这一步是处理配置改变。前面我们谈论过这个话题,当配置发生改变时,一般都通过 Java 层的 InputManagerService 发送信息给 JNI 层的 NativeInputManager ,然后再通知 InputReader (注意不是InputReader线程),InputReader 会通过 EventHub::wake() 函数来唤醒 InputReader 线程来处理配置改变。这就是第一步做的事件。鉴于篇幅原因,这个过程就不分析了。


    第二步,从 EventHub 获取数据。这个获取数据的过程其实分三种情况。



    1. 第一种情况,发生在系统首次启动,并且没有输入事件发生,例如手指没有在触摸屏上滑动。EventHub 会扫描输入设备,并建立与输入设备相应的数据结构,然后创建多个 EventHub 自己合成的事件,最后把这些事件返回给 InputReader 线程。为何要扫描设备,前面已经说过,是为了监听输入设备事件。


    2. 第二种情况,发生在系统启动完毕,然后有输入事件,例如手指在触摸屏上滑动。EventHub 会把 /dev/input/ 目录下的设备文件中的原始数据,包装成一个事件,发送给 InputReader 线程处理。


    3. 第三种情况,系统在运行的过程中,发生设备的挂载和卸载,EventHub 也会像第一种情况一样,合成自己的事件,并发送给 InputReader 线程处理。其实第一种情况和第三种情况下,InputReader 线程对事件的处理是类似的。因此后面的文章并不会分析这种情况。



    第三步,获取完事件后,就处理这些事件。


    第四步,通知监听者,设备发生改变。谁是监听者呢,就是上层的 InputManagerService。


    第五步,把InpuReader加工好的事件发送给 InputClassifier。


    事件发送关系图


    经过本文的分析,我们可以得出一张事件发送关系图,以及各个组件如何通信的关系图


    graph TD
    EventHub --> |EventHub::getEvent|InputReader
    InputReader --> |InputReaderPolicyInterface|NativeInputManager
    InputReader --> |InputListenerInterface|InputClassifer
    InputClassifer --> |InputListenerInterface|InputDispatcher
    InputDispatcher --> |InputDispatcherPolicyInterface|NativeInputManager
    NativeInputManager --> |mServiceObj|InputManagerService

    结束


    简简单单的 InputReader 的创建与启动就分析完了,而本文仅仅是描述了一个轮廓,但是我就问你复杂不复杂?复杂吧,不过没关系,只要我们理清思路,我们就一步一步来。那么下篇文章,我们来分析系统启动时,EventHub 是如何扫描设备并发送合成事件,以及 InputReader 线程是如何处理这些合成事件。


    收起阅读 »

    Android OpenGL ES 实现抖音传送带特效

    抖音 APP 真是个好东西,不过也容易上瘾,老实说你的抖音是不是反复卸载又反复安装了,后来我也发现我的几个 leader 都不刷抖音,这令我挺吃惊的。 我刷抖音主要是为了看新闻,听一些大 V 讲历史,研究抖音的一些算法特效,最重要的是抖音提供了一个年轻人的视...
    继续阅读 »

    抖音 APP 真是个好东西,不过也容易上瘾,老实说你的抖音是不是反复卸载又反复安装了,后来我也发现我的几个 leader 都不刷抖音,这令我挺吃惊的。


    我刷抖音主要是为了看新闻,听一些大 V 讲历史,研究抖音的一些算法特效,最重要的是抖音提供了一个年轻人的视角去观察世界。另外,自己感兴趣的内容看多了,反而训练抖音推送更多类似的优质内容,大家可以反向利用抖音的这一特点。


    至于我的 leader 老是强调刷抖音不好,对此我并不完全认同。


    实现抖音传送带特效 传送带


    抖音传送带特效原理


    抖音传送带特效推出已经很长一段时间了,前面也实现了下,最近把它整理出来了,如果你有仔细观测传送带特效,就会发现它的实现原理其实很简单。


    传送带原理.png


    通过仔细观察抖音的传送带特效,你可以发现左侧是不停地更新预览画面,右侧看起来就是一小格一小格的竖条状图像区域不断地向右移动,一直移动到右侧边界位置。


    预览的时候每次拷贝一小块预览区域的图像送到传送带,这就形成了源源不断地向右传送的效果。


    原理图进行了简化处理, 实际上右侧的竖条图像更多,效果会更流畅,每来一帧预览图像,首先拷贝更新左侧预览画面,然后从最右侧的竖条图像区域开始拷贝图像(想一想为什么?)。


    例如将区域 2 的像素拷贝到区域 3 ,然后将区域 1 的像素拷贝到区域 2,以此类推,最后将来源区域的像素拷贝到区域 0 。


    这样就形成了不断传送的效果,最后将拷贝好的图像更新到纹理,利用 OpenGL 渲染到屏幕上。


    抖音传送带特效实现


    抖音传送带特效实现


    上节原理分析时,将图像区域从左侧到右侧拷贝并不高效,可能会导致一些性能问题,好在 Android 相机出图都是横向的(旋转了 90 或 270 度),这样图像区域上下拷贝效率高了很多,最后渲染的时候再将图像旋转回来。


    Android 相机出图是 YUV 格式的,这里为了拷贝处理方便,先使用 OpenCV 将 YUV 图像转换为 RGBA 格式,当然为了追求性能直接使用 YUV 格式的图像问题也不大。


    cv::Mat mati420 = cv::Mat(pImage->height * 3 / 2, pImage->width, CV_8UC1, pImage->ppPlane[0]);
    cv::Mat matRgba = cv::Mat(m_SrcImage.height, m_SrcImage.width, CV_8UC4, m_SrcImage.ppPlane[0]);
    cv::cvtColor(mati420, matRgba, CV_YUV2RGBA_I420);

    用到的着色器程序就是简单的贴图:


    #version 300 es
    layout(location = 0) in vec4 a_position;
    layout(location = 1) in vec2 a_texCoord;
    uniform mat4 u_MVPMatrix;
    out vec2 v_texCoord;
    void main()
    {
    gl_Position = u_MVPMatrix * a_position;
    v_texCoord = a_texCoord;
    }

    #version 300 es
    precision mediump float;
    in vec2 v_texCoord;
    layout(location = 0) out vec4 outColor;
    uniform sampler2D u_texture;

    void main()
    {
    outColor = texture(u_texture, v_texCoord);
    }

    传送带的核心就是图像拷贝操作:


    memcpy(m_RenderImage.ppPlane[0], m_SrcImage.ppPlane[0], m_RenderImage.width * m_RenderImage.height * 4 / 2); //左侧预览区域像素拷贝

    int bannerHeight = m_RenderImage.height / 2 / m_bannerNum;//一个 banner 的高(小竖条)
    int bannerPixelsBufSize = m_RenderImage.width * bannerHeight * 4;//一个 banner 占用的图像内存

    uint8 *pBuf = m_RenderImage.ppPlane[0] + m_RenderImage.width * m_RenderImage.height * 4 / 2; //传送带分界线

    //从最右侧的竖条图像区域开始拷贝图像
    for (int i = m_bannerNum - 1; i >= 1; --i) {
    memcpy(pBuf + i*bannerPixelsBufSize, pBuf + (i - 1)*bannerPixelsBufSize, bannerPixelsBufSize);
    }

    //将来源区域的像素拷贝到竖条图像区域 0
    memcpy(pBuf, pBuf - bannerPixelsBufSize, bannerPixelsBufSize);

    渲染操作:


    glUseProgram (m_ProgramObj);

    glBindVertexArray(m_VaoId);

    glUniformMatrix4fv(m_MVPMatLoc, 1, GL_FALSE, &m_MVPMatrix[0][0]);

    //图像拷贝,传送带拷贝
    memcpy(m_RenderImage.ppPlane[0], m_SrcImage.ppPlane[0], m_RenderImage.width * m_RenderImage.height * 4 / 2);
    int bannerHeight = m_RenderImage.height / 2 / m_bannerNum;
    int bannerPixelsBufSize = m_RenderImage.width * bannerHeight * 4;

    uint8 *pBuf = m_RenderImage.ppPlane[0] + m_RenderImage.width * m_RenderImage.height * 4 / 2; //传送带分界线

    for (int i = m_bannerNum - 1; i >= 1; --i) {
    memcpy(pBuf + i*bannerPixelsBufSize, pBuf + (i - 1)*bannerPixelsBufSize, bannerPixelsBufSize);
    }
    memcpy(pBuf, pBuf - bannerPixelsBufSize, bannerPixelsBufSize);

    //更新纹理
    glActiveTexture(GL_TEXTURE0);
    glBindTexture(GL_TEXTURE_2D, m_TextureId);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, m_RenderImage.width, m_RenderImage.height, 0, GL_RGBA, GL_UNSIGNED_BYTE, m_RenderImage.ppPlane[0]);
    glBindTexture(GL_TEXTURE_2D, GL_NONE);

    glActiveTexture(GL_TEXTURE0);
    glBindTexture(GL_TEXTURE_2D, m_TextureId);
    GLUtils::setInt(m_ProgramObj, "u_texture", 0);
    glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_SHORT, (const void *)0);
    glBindVertexArray(GL_NONE);

    详细实现代码见项目:github.com/githubhaoha…

    收起阅读 »

    自动化检测 Android APP 非 SDK 接口使用,防止非预期异常发生!

    背景 从 Android 9(API 级别 28)开始,Android 平台对应用能使用的非 SDK 接口实施了限制,只要应用引用非 SDK 接口或尝试使用反射或 JNI 来获取其句柄,这些限制就适用,这些限制旨在帮助提升用户体验和开发者体验,为用户降低应用...
    继续阅读 »

    背景


    从 Android 9(API 级别 28)开始,Android 平台对应用能使用的非 SDK 接口实施了限制,只要应用引用非 SDK 接口或尝试使用反射或 JNI 来获取其句柄,这些限制就适用,这些限制旨在帮助提升用户体验和开发者体验,为用户降低应用发生崩溃的风险,同时为开发者降低紧急发布的风险。


    区分 SDK 接口和非 SDK 接口


    一般而言,公共 SDK 接口是在 Android 框架软件包索引中记录的那些接口,非 SDK 接口的处理是 API 抽象出来的实现细节,因此这些接口可能会在不另行通知的情况下随时发生更改。 


    为了避免发生崩溃和意外行为,应用应仅使用 SDK 中经过正式记录的类,这也意味着当您的应用通过反射等机制与类互动时,不应访问 SDK 中未列出的方法或字段。


    非 SDK API 名单


    为最大程度地降低非 SDK 使用限制对开发工作流的影响,Google 将非 SDK 接口分成了几个名单,这些名单界定了非 SDK 接口使用限制的严格程度(取决于应用的目标 API 级别):



    • greylist 无限制,可以正常使用

    • blacklist 无论什么版本的手机系统,使用这些api,系统将会抛出异常

    • greylist-max-o 受限制的灰名单,APP运行在 版本<=8.0的系统里 可以正常访问,targetSDK>8.0且运行在>8.0的手机会抛出异常

    • greylist-max-p 受限制的灰名单,APP运行在 版本<=9.0的系统里 可以正常访问,targetSDK>9.0且运行在>9.0的手机会抛出异常

    • greylist-max-q 受限制的灰名单,受限制的灰名单。APP运行在 版本<=10.0的系统里 可以正常访问,targetSDK>10.0且运行在>10.0的手机会抛出异常


    测试你的应用是否使用了非 SDK 接口


    这里我们通过veridex工具进行测试,veridex 工具会扫描 APK 的整个代码库(包括所有第三方库),并报告发现的所有使用非 SDK 接口的行为。


    不过veridex 工具存在以下局限性:



    • 它无法检测到通过 JNI 实现的调用

    • 它只能检测到一部分通过反射实现的调用

    • 它对非活动代码路径的分析仅限于 API 级别的检查

    • 它只能在支持 SSE4.2 和 POPCNT 指令的机器上运行


    我们以Mac系统为例,首先我们需要下载veridex 工具:android.googlesource.com/platform/pr…


    然后解压缩 appcompat.tar.gz 文件的内容,在解压缩的文件夹中,找到 veridex-mac.zip 文件并将其解压缩,转到解压缩的文件夹,然后运行下面的命令,其中 /path-from-root/your-app.apk 是你要测试的 APK 的路径,从系统的根目录开始:


    ./appcompat.sh --dex-file=/path-from-root/your-app.apk

    文件夹中的hiddenapi-flags.csv文件是需要根据targetAPI版本来更新的,不同的版本会有不同的检查清单,具体可参考:


    https://developer.android.google.cn/distribute/best-practices/develop/restrictions-non-sdk-interfaces#determine-list

    报告


    生成的报告如下图,我们主要关注红框部分的内容就可以了,如果存在blacklist的接口一定是需要修复的:


    图片

    收起阅读 »

    【Flutter 组件集录】SizedBox

    一、认识 SizedBox 组件 源码中对 SizedBox 的介绍为:一个指定尺寸的盒子。那 SizedBox 为什么可以限定尺寸?背后区域限定的原理又是什么? 本文通过 SizedBox 来一窥布局约束奥秘的冰山一角。 1.SizedBox 基...
    继续阅读 »
    一、认识 SizedBox 组件

    源码中对 SizedBox 的介绍为:一个指定尺寸的盒子。那 SizedBox 为什么可以限定尺寸?背后区域限定的原理又是什么? 本文通过 SizedBox 来一窥布局约束奥秘的冰山一角。





    1.SizedBox 基本信息

    下面是 SizedBox 组件类的定义构造方法,可以看出它继承自 SingleChildRenderObjectWidget。可接受一个子组件,和区域的宽高。





    2.SizedBox 的使用

    如下,是一个 100*50SizedBox ,通过 ColoredBox 涂上蓝色,效果如下:



    SizedBox(
    width: 100,
    height: 50,
    child: ColoredBox(
    color: Colors.blue.withAlpha(88)
    ),
    ),



    3.区域分析

    乍一看,不就是一个组件提供宽高来设置尺寸吗,似乎并没有什么好延伸的。但你有没有想过,为什么 SizedBox 有权力决定尺寸大小?它决定的区域一定有效吗?在分析之前,先了解一些前置知识:


    任何组件的占位区域绘制内容最终都取决于 RenderObject 。而并非所有的组件都和 RenderObject 有关,只有 RenderObjectWidget 负责维护 RenderObject 。像 StatelessWidgetStatefulWidget 这种都是基于已有组件进行组合,往深层去看,他们都是基于某些 RenderObjectWidget 实现。


    关于布局, RenderObject 有一个非常重要的属性: Constraints 类型的 constraints ,表示自身受到的区域约束限制。而 RenderBox 作为 RenderObject 的子类,拓展出了 size 的概念,绝大多数组件维护的渲染对象都是在 RenderBox 基础上进行拓展的。


    下面来打开组件树,一起来看一下:



    上面的 SizedBox 组件,它维护的 RenderObjectRenderConstrainedBox ,自身的约束为 [w(0,800) - h(0,600)] ,也就说明该渲染对象的大小必须在这此区间内。然后它会给子组件施加一个额外的约束 [w(100,100) - h(50,50)]


    这样对于 ColoredBox 对应的渲染对象 _RenderColoredBox ,由于父级施加的额外约束,自身的约束也就变成 [w(100,100) - h(50,50)] 。也就说明该渲染对象的大小必须在这此区间内,即 _RenderColoredBox 的尺寸被限定为 (100,50)


    _RenderColoredBoxsize 确定后,RenderConstrainedBox 会根据自身的约束和子节点的尺寸来确定自身的尺寸。这就是 SizedBox 的工作原理。




    4、约束测试

    为了更好地说明约束的作用,这里进行一下测试,在之前的案例的 SizedBox 外层通过 ConstrainedBox 组件添加添加一个 [w(20,20) - h(20,20)] 的强制约束。可以看出即使 SizedBox 设置了固定的宽高,但是在外层的约束之下,会优先满足父级约束。


    [推论1] SizedBox 的最终尺寸会受到父级约束的影响,并非一定为指定值。


    ConstrainedBox(
    constraints: BoxConstraints(
    minWidth: 20,
    maxWidth: 20,
    maxHeight: 20,
    minHeight: 20,
    ),
    child: SizedBox(
    width: 100,
    height: 50,
    child: ColoredBox(color: Colors.blue.withAlpha(88)),
    ),
    );



    我们再来看一下此时的组件树:
    可以看出 SizedBox 维护的 RenderConstrainedBox 本身的约束区域为 [w(20,20) - h(20,20)] ,为子节点施加的额外约束为 [w(100,100) - h(50,50)] 。在 ColoredBox 维护的 _RenderColoredBox 中,约束区域为 [w(20,20) - h(20,20)] ,这也就觉得了其尺寸为 (20,20)



    这样可以看出,渲染对象对子节点施加的额外约束 ,并不会完全作用于子节点。还会根据自身的约束情况,来确定子组件的最终约束。




    三、SizedBox 的源码分析


    SizedBox 继承自 SingleChildRenderObjectWidget ,就说明它需要维护一个 RenderObject 来实现功能。





    在前面我们通过组件树可以看出,它维护的渲染对象是 RenderConstrainedBox 。从源码中可以看出, RenderConstrainedBox 构造时需要传入一个约束对象 BoxConstraints 。这里通过 BoxConstraints.tightFor 构造使用 widthheight 创建一个紧约束。



    通过源码可以看出,这个构造的约束为: [w(width,width) - h(height,height)],也就是固定宽高约束。





    SizedBox 除了普通构造之外,还有三个命名构造。如果已经了解上面的用法,那这三个也非常简单,都逃离不了对宽高的初始化。比如 .expand 会创建一个无限的约束,这样由于 推论1 ,其约束的尺寸就可以在父级的约束下,尽可能的大 。 .shrink 就是一个 [w(0,0) - h(0,0)]的限制,同理,会在父级的约束下,尽可能的小。



    至于 RenderConstrainedBox 渲染对象的实现,将在后面的 ConstrainedBox 一文中进行介绍,毕竟 RenderConstrainedBox 的本命是 ConstrainedBox 。通过本文,你应该对 SizedBox 有了更深的认识,对布局约束、尺寸确定也认识了九牛一毛 。那本文到这里就结束了,谢谢观看,明天见~

    收起阅读 »

    Crash 防护系统 -- KVO 防护

    通过本文,您将了解到:KVO Crash 的主要原因KVO 防止 Crash 的常见方案我的 KVO 防护实现测试 KVO 防护效果1. KVO Crash 的常见原因KVO(Key Value Observing) 翻译过来就是键值对观察,是 iO...
    继续阅读 »

    通过本文,您将了解到:

    1. KVO Crash 的主要原因
    2. KVO 防止 Crash 的常见方案
    3. 我的 KVO 防护实现
    4. 测试 KVO 防护效果

    1. KVO Crash 的常见原因

    KVO(Key Value Observing) 翻译过来就是键值对观察,是 iOS 观察者模式的一种实现。KVO 允许一个对象监听另一个对象特定属性的改变,并在改变时接收到事件。但是 KVO API 的设计,我个人觉得不是很合理。被观察者需要做的工作太多,日常使用时稍不注意就会导致崩溃。

    KVO 日常使用造成崩溃的原因通常有以下几个:

    1. KVO 添加次数和移除次数不匹配:
      • 移除了未注册的观察者,导致崩溃。
      • 重复移除多次,移除次数多于添加次数,导致崩溃。
      • 重复添加多次,虽然不会崩溃,但是发生改变时,也同时会被观察多次。
    2. 被观察者提前被释放,被观察者在 dealloc 时仍然注册着 KVO,导致崩溃。
      例如:被观察者是局部变量的情况(iOS 10 及之前会崩溃)。
    3. 添加了观察者,但未实现 observeValueForKeyPath:ofObject:change:context: 方法,导致崩溃。
    4. 添加或者移除时 keypath == nil,导致崩溃。

    2. KVO 防止 Crash 常见方案

    为了避免上面提到的使用 KVO 造成崩溃的问题,于是出现了很多关于 KVO 的第三方库,比如最出名的就是 FaceBook 开源的第三方库 facebook / KVOController

    FBKVOController 对 KVO 机制进行了额外的一层封装,框架不但可以自动帮我们移除观察者,还提供了 block 或者 selector 的方式供我们进行观察处理。不可否认的是,FBKVOController 为我们的开发提供了很大的便利性。但是相对而言,这种方式对项目代码的侵入性比较大,必须考编码规范来强制约束团队人员使用这种方式。

    那么有没有一种对项目代码侵入性小,同时还能有效防护 KVO 崩溃的防护机制呢?

    网上有很多类似的方案可以参考一下。

    方案一:大白健康系统 — iOS APP 运行时 Crash 自动修复系统

    1. 首先为 NSObject 建立一个分类,利用 Method Swizzling,实现自定义的 BMP_addObserver:forKeyPath:options:context:BMP_removeObserver:forKeyPath:BMP_removeObserver:forKeyPath:context:BMPKVO_dealloc方法,用来替换系统原生的添加移除观察者方法的实现。
    2. 然后在观察者和被观察者之间建立一个 KVODelegate 对象,两者之间通过 KVODelegate 对象 建立联系。然后在添加和移除操作时,将 KVO 的相关信息例如 observerkeyPathoptionscontext保存为 KVOInfo 对象,并添加到 KVODelegate 对象 中对应 的 关系哈希表 中,对应原有的添加观察者。
      关系哈希表的数据结构:{keypath : [KVOInfo 对象1, KVOInfo 对象2, ... ]}
    3. 在添加和移除操作的时候,利用 KVODelegate 对象 做转发,把真正的观察者变为 KVODelegate 对象,而当被观察者的特定属性发生了改变,再由 KVODelegate 对象 分发到原有的观察者上。

    那么,BayMax 系统是如何避免 KVO 崩溃的呢?

    1. 添加观察者时:通过关系哈希表判断是否重复添加,只添加一次。

    2. 移除观察者时:通过关系哈希表是否已经进行过移除操作,避免多次移除。

    3. 观察键值改变时:同样通过关系哈希表判断,将改变操作分发到原有的观察者上。

      另外,为了避免被观察者提前被释放,被观察者在 dealloc 时仍然注册着 KVO 导致崩溃。BayMax 系统还利用 Method Swizzling 实现了自定义的 dealloc,在系统 dealloc 调用之前,将多余的观察者移除掉。

    方案二: ValiantCat / XXShield(第三方框架)

    XXShield 实现方案和 BayMax 系统类似。也是利用一个 Proxy 对象用来做转发, 真正的观察者是 Proxy,被观察者出现了通知信息,由 Proxy 做分发。不过不同点是 Proxy 里面保存的内容没有前者多。只保存了 _observed(被观察者) 和关系哈希表,这个关系哈希表中只维护了 keyPath 和 observer 的关系。

    关系哈希表的数据结构:{keypath : [observer1, observer2 , ...](NSHashTable)} 。

    XXShield 在 dealloc 中也做了类似将多余观察者移除掉的操作,是通过关系数据结构和 _observed ,然后调用原生移除观察者操作实现的。

    方案三: JackLee18 / JKCrashProtect(第三方框架)

    JKCrashProtect 相对于前两个方案来讲,看上去更加的简洁明了。他的不同点在于没有使用 delegate。而是直接在分类中建立了一个关系哈希表,用来保存 {keypath : [observer1, observer2 , ...](NSHashTable)} 的关系。

    添加的时候,如果关系哈希表中与 keyPath 对应的已经有了相关的观察者,就不再进行添加。同样移除观察者的时候,也在哈希表中进行查找,如果存在 observer、keyPath 的信息,就移除掉,否则就不进行移除操作。

    不过,这个框架并没有对被观察者在 dealloc 时仍然注册着 KVO ,造成崩溃的情况进行处理。


    3. 我的 KVO 防护实现

    参考了这几个方法的实现后,分别实现了一下之后,最终还是选择了 方案一、方案二 这两种方案的实现思路。

    1. 我使用了 YSCKVOProxy 对象,在 YSCKVOProxy 对象 中使用 {keypath : [observer1, observer2 , ...](NSHashTable)} 结构的 关系哈希表 进行 observerkeyPath 之间的维护。
    2. 然后利用 YSCKVOProxy 对象 对添加、移除、观察方法进行分发处理。
    3. 在分类中自定义了 dealloc 的实现,移除了多余的观察者。
    • 代码如下所示:
    #import "NSObject+KVODefender.h"
    #import "NSObject+MethodSwizzling.h"
    #import <objc/runtime.h>

    // 判断是否是系统类
    static inline BOOL IsSystemClass(Class cls){
    BOOL isSystem = NO;
    NSString *className = NSStringFromClass(cls);
    if ([className hasPrefix:@"NS"] || [className hasPrefix:@"__NS"] || [className hasPrefix:@"OS_xpc"]) {
    isSystem = YES;
    return isSystem;
    }
    NSBundle *mainBundle = [NSBundle bundleForClass:cls];
    if (mainBundle == [NSBundle mainBundle]) {
    isSystem = NO;
    }else{
    isSystem = YES;
    }
    return isSystem;
    }


    #pragma mark - YSCKVOProxy 相关

    @interface YSCKVOProxy : NSObject

    // 获取所有被观察的 keyPaths
    - (NSArray *)getAllKeyPaths;

    @end

    @implementation YSCKVOProxy
    {
    // 关系数据表结构:{keypath : [observer1, observer2 , ...](NSHashTable)}
    @private
    NSMutableDictionary<NSString *, NSHashTable<NSObject *> *> *_kvoInfoMap;
    }

    - (instancetype)init {
    self = [super init];
    if (self) {
    _kvoInfoMap = [NSMutableDictionary dictionary];
    }
    return self;
    }

    // 添加 KVO 信息操作, 添加成功返回 YES
    - (BOOL)addInfoToMapWithObserver:(NSObject *)observer
    forKeyPath:(NSString *)keyPath
    options:(NSKeyValueObservingOptions)options
    context:(void *)context {

    @synchronized (self) {
    if (!observer || !keyPath ||
    ([keyPath isKindOfClass:[NSString class]] && keyPath.length <= 0)) {
    return NO;
    }

    NSHashTable<NSObject *> *info = _kvoInfoMap[keyPath];
    if (info.count == 0) {
    info = [[NSHashTable alloc] initWithOptions:(NSPointerFunctionsWeakMemory) capacity:0];
    [info addObject:observer];

    _kvoInfoMap[keyPath] = info;

    return YES;
    }

    if (![info containsObject:observer]) {
    [info addObject:observer];
    }

    return NO;
    }
    }

    // 移除 KVO 信息操作, 添加成功返回 YES
    - (BOOL)removeInfoInMapWithObserver:(NSObject *)observer
    forKeyPath:(NSString *)keyPath {

    @synchronized (self) {
    if (!observer || !keyPath ||
    ([keyPath isKindOfClass:[NSString class]] && keyPath.length <= 0)) {
    return NO;
    }

    NSHashTable<NSObject *> *info = _kvoInfoMap[keyPath];

    if (info.count == 0) {
    return NO;
    }

    [info removeObject:observer];

    if (info.count == 0) {
    [_kvoInfoMap removeObjectForKey:keyPath];

    return YES;
    }

    return NO;
    }
    }

    // 添加 KVO 信息操作, 添加成功返回 YES
    - (BOOL)removeInfoInMapWithObserver:(NSObject *)observer
    forKeyPath:(NSString *)keyPath
    context:(void *)context {
    @synchronized (self) {
    if (!observer || !keyPath ||
    ([keyPath isKindOfClass:[NSString class]] && keyPath.length <= 0)) {
    return NO;
    }

    NSHashTable<NSObject *> *info = _kvoInfoMap[keyPath];

    if (info.count == 0) {
    return NO;
    }

    [info removeObject:observer];

    if (info.count == 0) {
    [_kvoInfoMap removeObjectForKey:keyPath];

    return YES;
    }

    return NO;
    }
    }

    // 实际观察者 yscKVOProxy 进行监听,并分发
    - (void)observeValueForKeyPath:(NSString *)keyPath
    ofObject:(id)object
    change:(NSDictionary<NSKeyValueChangeKey,id> *)change
    context:(void *)context {

    NSHashTable<NSObject *> *info = _kvoInfoMap[keyPath];

    for (NSObject *observer in info) {
    @try {
    [observer observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    } @catch (NSException *exception) {
    NSString *reason = [NSString stringWithFormat:@"KVO Warning : %@",[exception description]];
    NSLog(@"%@",reason);
    }
    }
    }

    // 获取所有被观察的 keyPaths
    - (NSArray *)getAllKeyPaths {
    NSArray <NSString *>*keyPaths = _kvoInfoMap.allKeys;
    return keyPaths;
    }

    @end


    #pragma mark - NSObject+KVODefender 分类

    @implementation NSObject (KVODefender)

    + (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{

    // 拦截 `addObserver:forKeyPath:options:context:` 方法,替换自定义实现
    [NSObject yscDefenderSwizzlingInstanceMethod: @selector(addObserver:forKeyPath:options:context:)
    withMethod: @selector(ysc_addObserver:forKeyPath:options:context:)
    withClass: [NSObject class]];

    // 拦截 `removeObserver:forKeyPath:` 方法,替换自定义实现
    [NSObject yscDefenderSwizzlingInstanceMethod: @selector(removeObserver:forKeyPath:)
    withMethod: @selector(ysc_removeObserver:forKeyPath:)
    withClass: [NSObject class]];

    // 拦截 `removeObserver:forKeyPath:context:` 方法,替换自定义实现
    [NSObject yscDefenderSwizzlingInstanceMethod: @selector(removeObserver:forKeyPath:context:)
    withMethod: @selector(ysc_removeObserver:forKeyPath:context:)
    withClass: [NSObject class]];

    // 拦截 `dealloc` 方法,替换自定义实现
    [NSObject yscDefenderSwizzlingInstanceMethod: NSSelectorFromString(@"dealloc")
    withMethod: @selector(ysc_kvodealloc)
    withClass: [NSObject class]];
    });
    }

    static void *YSCKVOProxyKey = &YSCKVOProxyKey;
    static NSString *const KVODefenderValue = @"YSC_KVODefender";
    static void *KVODefenderKey = &KVODefenderKey;

    // YSCKVOProxy setter 方法
    - (void)setYscKVOProxy:(YSCKVOProxy *)yscKVOProxy {
    objc_setAssociatedObject(self, YSCKVOProxyKey, yscKVOProxy, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }

    // YSCKVOProxy getter 方法
    - (YSCKVOProxy *)yscKVOProxy {
    id yscKVOProxy = objc_getAssociatedObject(self, YSCKVOProxyKey);
    if (yscKVOProxy == nil) {
    yscKVOProxy = [[YSCKVOProxy alloc] init];
    self.yscKVOProxy = yscKVOProxy;
    }
    return yscKVOProxy;
    }

    // 自定义 addObserver:forKeyPath:options:context: 实现方法
    - (void)ysc_addObserver:(NSObject *)observer
    forKeyPath:(NSString *)keyPath
    options:(NSKeyValueObservingOptions)options
    context:(void *)context {

    if (!IsSystemClass(self.class)) {
    objc_setAssociatedObject(self, KVODefenderKey, KVODefenderValue, OBJC_ASSOCIATION_RETAIN);
    if ([self.yscKVOProxy addInfoToMapWithObserver:observer forKeyPath:keyPath options:options context:context]) {
    // 如果添加 KVO 信息操作成功,则调用系统添加方法
    [self ysc_addObserver:self.yscKVOProxy forKeyPath:keyPath options:options context:context];
    } else {
    // 添加 KVO 信息操作失败:重复添加
    NSString *className = (NSStringFromClass(self.class) == nil) ? @"" : NSStringFromClass(self.class);
    NSString *reason = [NSString stringWithFormat:@"KVO Warning : Repeated additions to the observer:%@ for the key path:'%@' from %@",
    observer, keyPath, className];
    NSLog(@"%@",reason);
    }
    } else {
    [self ysc_addObserver:observer forKeyPath:keyPath options:options context:context];
    }
    }

    // 自定义 removeObserver:forKeyPath:context: 实现方法
    - (void)ysc_removeObserver:(NSObject *)observer
    forKeyPath:(NSString *)keyPath
    context:(void *)context {

    if (!IsSystemClass(self.class)) {
    if ([self.yscKVOProxy removeInfoInMapWithObserver:observer forKeyPath:keyPath context:context]) {
    // 如果移除 KVO 信息操作成功,则调用系统移除方法
    [self ysc_removeObserver:self.yscKVOProxy forKeyPath:keyPath context:context];
    } else {
    // 移除 KVO 信息操作失败:移除了未注册的观察者
    NSString *className = NSStringFromClass(self.class) == nil ? @"" : NSStringFromClass(self.class);
    NSString *reason = [NSString stringWithFormat:@"KVO Warning : Cannot remove an observer %@ for the key path '%@' from %@ , because it is not registered as an observer", observer, keyPath, className];
    NSLog(@"%@",reason);
    }
    } else {
    [self ysc_removeObserver:observer forKeyPath:keyPath context:context];
    }
    }

    // 自定义 removeObserver:forKeyPath: 实现方法
    - (void)ysc_removeObserver:(NSObject *)observer
    forKeyPath:(NSString *)keyPath {

    if (!IsSystemClass(self.class)) {
    if ([self.yscKVOProxy removeInfoInMapWithObserver:observer forKeyPath:keyPath]) {
    // 如果移除 KVO 信息操作成功,则调用系统移除方法
    [self ysc_removeObserver:self.yscKVOProxy forKeyPath:keyPath];
    } else {
    // 移除 KVO 信息操作失败:移除了未注册的观察者
    NSString *className = NSStringFromClass(self.class) == nil ? @"" : NSStringFromClass(self.class);
    NSString *reason = [NSString stringWithFormat:@"KVO Warning : Cannot remove an observer %@ for the key path '%@' from %@ , because it is not registered as an observer", observer, keyPath, className];
    NSLog(@"%@",reason);
    }
    } else {
    [self ysc_removeObserver:observer forKeyPath:keyPath];
    }

    }

    // 自定义 dealloc 实现方法
    - (void)ysc_kvodealloc {
    @autoreleasepool {
    if (!IsSystemClass(self.class)) {
    NSString *value = (NSString *)objc_getAssociatedObject(self, KVODefenderKey);
    if ([value isEqualToString:KVODefenderValue]) {
    NSArray *keyPaths = [self.yscKVOProxy getAllKeyPaths];
    // 被观察者在 dealloc 时仍然注册着 KVO
    if (keyPaths.count > 0) {
    NSString *reason = [NSString stringWithFormat:@"KVO Warning : An instance %@ was deallocated while key value observers were still registered with it. The Keypaths is:'%@'", self, [keyPaths componentsJoinedByString:@","]];
    NSLog(@"%@",reason);
    }

    // 移除多余的观察者
    for (NSString *keyPath in keyPaths) {
    [self ysc_removeObserver:self.yscKVOProxy forKeyPath:keyPath];
    }
    }
    }
    }


    [self ysc_kvodealloc];
    }

    @end

    4. 测试 KVO 防护效果

    这里提供一下相关崩溃的测试代码:


    /********************* KVOCrashObject.h 文件 *********************/
    #import <Foundation/Foundation.h>

    @interface KVOCrashObject : NSObject

    @property (nonatomic, copy) NSString *name;

    @end

    /********************* KVOCrashObject.m 文件 *********************/
    #import "KVOCrashObject.h"

    @implementation KVOCrashObject

    @end

    /********************* ViewController.m 文件 *********************/
    #import "ViewController.h"
    #import "KVOCrashObject.h"

    @interface ViewController ()

    @property (nonatomic, strong) KVOCrashObject *objc;

    @end

    @implementation ViewController

    - (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.

    self.objc = [[KVOCrashObject alloc] init];
    }

    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {

    // 1.1 移除了未注册的观察者,导致崩溃
    [self testKVOCrash11];

    // 1.2 重复移除多次,移除次数多于添加次数,导致崩溃
    // [self testKVOCrash12];

    // 1.3 重复添加多次,虽然不会崩溃,但是发生改变时,也同时会被观察多次。
    // [self testKVOCrash13];

    // 2. 被观察者 dealloc 时仍然注册着 KVO,导致崩溃
    // [self testKVOCrash2];

    // 3. 观察者没有实现 -observeValueForKeyPath:ofObject:change:context:导致崩溃
    // [self testKVOCrash3];

    // 4. 添加或者移除时 keypath == nil,导致崩溃。
    // [self testKVOCrash4];
    }

    /**
    1.1 移除了未注册的观察者,导致崩溃
    */

    - (void)testKVOCrash11 {
    // 崩溃日志:Cannot remove an observer XXX for the key path "xxx" from XXX because it is not registered as an observer.
    [self.objc removeObserver:self forKeyPath:@"name"];
    }

    /**
    1.2 重复移除多次,移除次数多于添加次数,导致崩溃
    */

    - (void)testKVOCrash12 {
    // 崩溃日志:Cannot remove an observer XXX for the key path "xxx" from XXX because it is not registered as an observer.
    [self.objc addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];
    self.objc.name = @"0";
    [self.objc removeObserver:self forKeyPath:@"name"];
    [self.objc removeObserver:self forKeyPath:@"name"];
    }

    /**
    1.3 重复添加多次,虽然不会崩溃,但是发生改变时,也同时会被观察多次。
    */

    - (void)testKVOCrash13 {
    [self.objc addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];
    [self.objc addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];
    self.objc.name = @"0";
    }

    /**
    2. 被观察者 dealloc 时仍然注册着 KVO,导致崩溃
    */

    - (void)testKVOCrash2 {
    // 崩溃日志:An instance xxx of class xxx was deallocated while key value observers were still registered with it.
    // iOS 10 及以下会导致崩溃,iOS 11 之后就不会崩溃了
    KVOCrashObject *obj = [[KVOCrashObject alloc] init];
    [obj addObserver: self
    forKeyPath: @"name"
    options: NSKeyValueObservingOptionNew
    context: nil];
    }

    /**
    3. 观察者没有实现 -observeValueForKeyPath:ofObject:change:context:导致崩溃
    */

    - (void)testKVOCrash3 {
    // 崩溃日志:An -observeValueForKeyPath:ofObject:change:context: message was received but not handled.
    KVOCrashObject *obj = [[KVOCrashObject alloc] init];

    [self addObserver: obj
    forKeyPath: @"title"
    options: NSKeyValueObservingOptionNew
    context: nil];

    self.title = @"111";
    }

    /**
    4. 添加或者移除时 keypath == nil,导致崩溃。
    */

    - (void)testKVOCrash4 {
    // 崩溃日志: -[__NSCFConstantString characterAtIndex:]: Range or index out of bounds
    KVOCrashObject *obj = [[KVOCrashObject alloc] init];

    [self addObserver: obj
    forKeyPath: @""
    options: NSKeyValueObservingOptionNew
    context: nil];

    // [self removeObserver:obj forKeyPath:@""];
    }


    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary*)change context:(void *)context {

    NSLog(@"object = %@, keyPath = %@", object, keyPath);
    }

    @end

    可以将示例项目 NSObject+KVODefender.m 中的 + (void)load; 方法注释掉或打开进行防护前后的测试。

    经测试可以发现,成功的拦截了这几种因为 KVO 使用不当导致的崩溃。



    作者:NJKNJK
    链接:https://www.jianshu.com/p/0d67bb7b96de


    收起阅读 »

    『Crash 防护系统』 一 Unrecognized Selector

    这个系列将会介绍如何设计一套 APP Crash 防护系统。这套系统采用 AOP(面向切面编程)的设计思想,利用 Objective-C语言的运行时机制,在不侵入原有项目代码的基础之上,通过在 APP 运行时阶段对崩溃因素的的拦截和处理,使得 APP 能够持续...
    继续阅读 »
    这个系列将会介绍如何设计一套 APP Crash 防护系统。这套系统采用 AOP(面向切面编程)的设计思想,利用 Objective-C语言的运行时机制,在不侵入原有项目代码的基础之上,通过在 APP 运行时阶段对崩溃因素的的拦截和处理,使得 APP 能够持续稳定正常的运行。

    通过本文,您将了解到:

    1. Crash 防护系统开篇
    2. 防护原理简介和常见 Crash
    3. Method Swizzling 方法的封装
    4. Unrecognized Selector 防护
      4.1 unrecognized selector sent to instance(找不到对象方法的实现)
      4.2 unrecognized selector sent to class(找不到类方法实现)

    1. Crash 防护系统开篇

    APP 的崩溃问题,一直以来都是开发过程中重中之重的问题。日常开发阶段的崩溃,发现后还能够立即处理。但是一旦发布上架的版本出现问题,就需要紧急加班修复 BUG,再更新上架新版本了。在这个过程中, 说不定会因为崩溃而导致关键业务中断、用户存留率下降、品牌口碑变差、生命周期价值下降等,最终导致流失用户,影响到公司的发展。

    当然,避免崩溃问题的最好办法就是不产生崩溃。在开发的过程中就要尽可能地保证程序的健壮性。但是,人又不是机器,不可能不犯错。不可能存在没有 BUG 的程序。但是如果能够利用一些语言机制和系统方法,设计一套防护系统,使之能够有效的降低 APP 的崩溃率,那么不仅 APP 的稳定性得到了保障,而且最重要的是可以减少不必要的加班。

    这套 Crash 防护系统被命名为:『YSCDefender(防卫者)』。Defender 也是路虎旗下最硬派的越野车系。在电影《Tomb Raider》里面,由 Angelina Jolie 饰演的英国女探险家 Lara Croft,所驾驶的就是一台 Defender。Defender 也是我比较喜欢的车之一。

    不过呢,这不重要。。。我就是为这个项目起了个花里胡哨的名字,并给这个名字赋予了一些无聊的意义。。。


    2. 防护原理简介和常见 Crash

    Objective-C 语言是一门动态语言,我们可以利用 Objective-C 语言的 Runtime 运行时机制,对需要 Hook 的类添加 Category(分类),在各个分类的 +(void)load; 中通过 Method Swizzling 拦截容易造成崩溃的系统方法,将系统原有方法与添加的防护方法的 selector(方法选择器) 与 IMP(函数实现指针)进行对调。然后在替换方法中添加防护操作,从而达到避免以及修复崩溃的目的。

    通过 Runtime 机制可以避免的常见 Crash :

    1. unrecognized selector sent to instance(找不到对象方法的实现)
    2. unrecognized selector sent to class(找不到类方法实现)
    3. KVO Crash
    4. KVC Crash
    5. NSNotification Crash
    6. NSTimer Crash
    7. Container Crash(集合类操作造成的崩溃,例如数组越界,插入 nil 等)
    8. NSString Crash (字符串类操作造成的崩溃)
    9. Bad Access Crash (野指针)
    10. Threading Crash (非主线程刷 UI)
    11. NSNull Crash

    这一篇我们先来讲解下 unrecognized selector sent to instance(找不到对象方法的实现) 和 unrecognized selector sent to class(找不到类方法实现) 造成的崩溃问题。


    3. Method Swizzling 方法的封装

    由于这几种常见 Crash 的防护都需要用到 Method Swizzling 技术。所以我们可以为 NSObject 新建一个分类,将 Method Swizzling 相关的方法封装起来。

    /********************* NSObject+MethodSwizzling.h 文件 *********************/

    #import <Foundation/Foundation.h>

    NS_ASSUME_NONNULL_BEGIN

    @interface NSObject (MethodSwizzling)

    /** 交换两个类方法的实现
    * @param originalSelector 原始方法的 SEL
    * @param swizzledSelector 交换方法的 SEL
    * @param targetClass 类
    */

    + (void)yscDefenderSwizzlingClassMethod:(SEL)originalSelector withMethod:(SEL)swizzledSelector withClass:(Class)targetClass;

    /** 交换两个对象方法的实现
    * @param originalSelector 原始方法的 SEL
    * @param swizzledSelector 交换方法的 SEL
    * @param targetClass 类
    */

    + (void)yscDefenderSwizzlingInstanceMethod:(SEL)originalSelector withMethod:(SEL)swizzledSelector withClass:(Class)targetClass;

    @end

    /********************* NSObject+MethodSwizzling.m 文件 *********************/

    #import "NSObject+MethodSwizzling.h"
    #import <objc/runtime.h>

    @implementation NSObject (MethodSwizzling)

    // 交换两个类方法的实现
    + (void)yscDefenderSwizzlingClassMethod:(SEL)originalSelector withMethod:(SEL)swizzledSelector withClass:(Class)targetClass {
    swizzlingClassMethod(targetClass, originalSelector, swizzledSelector);
    }

    // 交换两个对象方法的实现
    + (void)yscDefenderSwizzlingInstanceMethod:(SEL)originalSelector withMethod:(SEL)swizzledSelector withClass:(Class)targetClass {
    swizzlingInstanceMethod(targetClass, originalSelector, swizzledSelector);
    }

    // 交换两个类方法的实现 C 函数
    void swizzlingClassMethod(Class class, SEL originalSelector, SEL swizzledSelector) {

    Method originalMethod = class_getClassMethod(class, originalSelector);
    Method swizzledMethod = class_getClassMethod(class, swizzledSelector);

    BOOL didAddMethod = class_addMethod(class,
    originalSelector,
    method_getImplementation(swizzledMethod),
    method_getTypeEncoding(swizzledMethod));

    if (didAddMethod) {
    class_replaceMethod(class,
    swizzledSelector,
    method_getImplementation(originalMethod),
    method_getTypeEncoding(originalMethod));
    } else {
    method_exchangeImplementations(originalMethod, swizzledMethod);
    }
    }

    // 交换两个对象方法的实现 C 函数
    void swizzlingInstanceMethod(Class class, SEL originalSelector, SEL swizzledSelector) {
    Method originalMethod = class_getInstanceMethod(class, originalSelector);
    Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);

    BOOL didAddMethod = class_addMethod(class,
    originalSelector,
    method_getImplementation(swizzledMethod),
    method_getTypeEncoding(swizzledMethod));

    if (didAddMethod) {
    class_replaceMethod(class,
    swizzledSelector,
    method_getImplementation(originalMethod),
    method_getTypeEncoding(originalMethod));
    } else {
    method_exchangeImplementations(originalMethod, swizzledMethod);
    }
    }

    @end

    4. Unrecognized Selector 防护

    4.1 unrecognized selector sent to instance(找不到对象方法的实现)

    如果被调用的对象方法没有实现,那么程序在运行中调用该方法时,就会因为找不到对应的方法实现,从而导致 APP 崩溃。比如下面这样的代码:


    UIButton *testButton = [[UIButton alloc] init];
    [testButton performSelector:@selector(someMethod:)];

    testButton 是一个 UIButton 对象,而 UIButton 类中并没有实现 someMethod: 方法。所以向 testButoon 对象发送 someMethod: 方法,就会导致 testButoon 对象无法找到对应的方法实现,最终导致 APP 的崩溃。

    那么有办法解决这类因为找不到方法的实现而导致程序崩溃的方法吗?

    消息转发机制中三大步骤:消息动态解析消息接受者重定向消息重定向。通过这三大步骤,可以让我们在程序找不到调用方法崩溃之前,拦截方法调用。

    大致流程如下:

    1. 消息动态解析:Objective-C 运行时会调用 +resolveInstanceMethod: 或者 +resolveClassMethod:,让你有机会提供一个函数实现。我们可以通过重写这两个方法,添加其他函数实现,并返回 YES, 那运行时系统就会重新启动一次消息发送的过程。若返回 NO 或者没有添加其他函数实现,则进入下一步。
    2. 消息接受者重定向:如果当前对象实现了 forwardingTargetForSelector:,Runtime 就会调用这个方法,允许我们将消息的接受者转发给其他对象。如果这一步方法返回 nil,则进入下一步。
    3. 消息重定向:Runtime 系统利用 methodSignatureForSelector: 方法获取函数的参数和返回值类型。
      • 如果 methodSignatureForSelector: 返回了一个 NSMethodSignature 对象(函数签名),Runtime 系统就会创建一个 NSInvocation 对象,并通过 forwardInvocation: 消息通知当前对象,给予此次消息发送最后一次寻找 IMP 的机会。
      • 如果 methodSignatureForSelector: 返回 nil。则 Runtime 系统会发出 doesNotRecognizeSelector:消息,程序也就崩溃了。

    [图片上传失败...(image-5cdd82-1618276584627)]

    这里我们选择第二步(消息接受者重定向)来进行拦截。因为 -forwardingTargetForSelector 方法可以将消息转发给一个对象,开销较小,并且被重写的概率较低,适合重写。

    具体步骤如下:

    1. 给 NSObject 添加一个分类,在分类中实现一个自定义的 -ysc_forwardingTargetForSelector: 方法;
    2. 利用 Method Swizzling 将 -forwardingTargetForSelector: 和 -ysc_forwardingTargetForSelector: 进行方法交换。
    3. 在自定义的方法中,先判断当前对象是否已经实现了消息接受者重定向和消息重定向。如果都没有实现,就动态创建一个目标类,给目标类动态添加一个方法。
    4. 把消息转发给动态生成类的实例对象,由目标类动态创建的方法实现,这样 APP 就不会崩溃了。

    实现代码如下:


    #import "NSObject+SelectorDefender.h"
    #import "NSObject+MethodSwizzling.h"
    #import <objc/runtime.h>

    @implementation NSObject (SelectorDefender)

    + (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{

    // 拦截 `-forwardingTargetForSelector:` 方法,替换自定义实现
    [NSObject yscDefenderSwizzlingInstanceMethod:@selector(forwardingTargetForSelector:)
    withMethod:@selector(ysc_forwardingTargetForSelector:)
    withClass:[NSObject class]];

    });
    }

    // 自定义实现 `-ysc_forwardingTargetForSelector:` 方法
    - (id)ysc_forwardingTargetForSelector:(SEL)aSelector {

    SEL forwarding_sel = @selector(forwardingTargetForSelector:);

    // 获取 NSObject 的消息转发方法
    Method root_forwarding_method = class_getInstanceMethod([NSObject class], forwarding_sel);
    // 获取 当前类 的消息转发方法
    Method current_forwarding_method = class_getInstanceMethod([self class], forwarding_sel);

    // 判断当前类本身是否实现第二步:消息接受者重定向
    BOOL realize = method_getImplementation(current_forwarding_method) != method_getImplementation(root_forwarding_method);

    // 如果没有实现第二步:消息接受者重定向
    if (!realize) {
    // 判断有没有实现第三步:消息重定向
    SEL methodSignature_sel = @selector(methodSignatureForSelector:);
    Method root_methodSignature_method = class_getInstanceMethod([NSObject class], methodSignature_sel);

    Method current_methodSignature_method = class_getInstanceMethod([self class], methodSignature_sel);
    realize = method_getImplementation(current_methodSignature_method) != method_getImplementation(root_methodSignature_method);

    // 如果没有实现第三步:消息重定向
    if (!realize) {
    // 创建一个新类
    NSString *errClassName = NSStringFromClass([self class]);
    NSString *errSel = NSStringFromSelector(aSelector);
    NSLog(@"出问题的类,出问题的对象方法 == %@ %@", errClassName, errSel);

    NSString *className = @"CrachClass";
    Class cls = NSClassFromString(className);

    // 如果类不存在 动态创建一个类
    if (!cls) {
    Class superClsss = [NSObject class];
    cls = objc_allocateClassPair(superClsss, className.UTF8String, 0);
    // 注册类
    objc_registerClassPair(cls);
    }
    // 如果类没有对应的方法,则动态添加一个
    if (!class_getInstanceMethod(NSClassFromString(className), aSelector)) {
    class_addMethod(cls, aSelector, (IMP)Crash, "@@:@");
    }
    // 把消息转发到当前动态生成类的实例对象上
    return [[cls alloc] init];
    }
    }
    return [self ysc_forwardingTargetForSelector:aSelector];
    }

    // 动态添加的方法实现
    static int Crash(id slf, SEL selector) {
    return 0;
    }

    @end

    4.2 unrecognized selector sent to class(找不到类方法实现)

    同对象方法一样,如果被调用的类方法没有实现,那么同样也会导致 APP 崩溃。

    例如,有这样一个类,声明了一个 + (id)aClassFunc; 的类方法, 但是并没有实现,就像下边的 YSCObject 这样。

    /********************* YSCObject.h 文件 *********************/
    #import <Foundation/Foundation.h>

    @interface YSCObject : NSObject

    + (id)aClassFunc;

    @end

    /********************* YSCObject.m 文件 *********************/
    #import "YSCObject.h"

    @implementation YSCObject

    @end

    如果我们直接调用 [YSCObject aClassFunc]; 就会导致崩溃。

    找不到类方法实现的解决方法和之前类似,我们可以利用 Method Swizzling 将 +forwardingTargetForSelector:和 +ysc_forwardingTargetForSelector: 进行方法交换。


    #import "NSObject+SelectorDefender.h"
    #import "NSObject+MethodSwizzling.h"
    #import <objc/runtime.h>

    @implementation NSObject (SelectorDefender)

    + (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{

    // 拦截 `+forwardingTargetForSelector:` 方法,替换自定义实现
    [NSObject yscDefenderSwizzlingClassMethod:@selector(forwardingTargetForSelector:)
    withMethod:@selector(ysc_forwardingTargetForSelector:)
    withClass:[NSObject class]];
    });
    }

    // 自定义实现 `+ysc_forwardingTargetForSelector:` 方法
    + (id)ysc_forwardingTargetForSelector:(SEL)aSelector {
    SEL forwarding_sel = @selector(forwardingTargetForSelector:);

    // 获取 NSObject 的消息转发方法
    Method root_forwarding_method = class_getClassMethod([NSObject class], forwarding_sel);
    // 获取 当前类 的消息转发方法
    Method current_forwarding_method = class_getClassMethod([self class], forwarding_sel);

    // 判断当前类本身是否实现第二步:消息接受者重定向
    BOOL realize = method_getImplementation(current_forwarding_method) != method_getImplementation(root_forwarding_method);

    // 如果没有实现第二步:消息接受者重定向
    if (!realize) {
    // 判断有没有实现第三步:消息重定向
    SEL methodSignature_sel = @selector(methodSignatureForSelector:);
    Method root_methodSignature_method = class_getClassMethod([NSObject class], methodSignature_sel);

    Method current_methodSignature_method = class_getClassMethod([self class], methodSignature_sel);
    realize = method_getImplementation(current_methodSignature_method) != method_getImplementation(root_methodSignature_method);

    // 如果没有实现第三步:消息重定向
    if (!realize) {
    // 创建一个新类
    NSString *errClassName = NSStringFromClass([self class]);
    NSString *errSel = NSStringFromSelector(aSelector);
    NSLog(@"出问题的类,出问题的类方法 == %@ %@", errClassName, errSel);

    NSString *className = @"CrachClass";
    Class cls = NSClassFromString(className);

    // 如果类不存在 动态创建一个类
    if (!cls) {
    Class superClsss = [NSObject class];
    cls = objc_allocateClassPair(superClsss, className.UTF8String, 0);
    // 注册类
    objc_registerClassPair(cls);
    }
    // 如果类没有对应的方法,则动态添加一个
    if (!class_getInstanceMethod(NSClassFromString(className), aSelector)) {
    class_addMethod(cls, aSelector, (IMP)Crash, "@@:@");
    }
    // 把消息转发到当前动态生成类的实例对象上
    return [[cls alloc] init];
    }
    }
    return [self ysc_forwardingTargetForSelector:aSelector];
    }

    // 动态添加的方法实现
    static int Crash(id slf, SEL selector) {
    return 0;
    }

    @end

    将 4.1 和 4.2 结合起来就可以拦截所有未实现的类方法和对象方法了


    作者:NJKNJK
    链接:https://www.jianshu.com/p/bd8a2594b788





    收起阅读 »

    Crash拦截器 - 让unrecognized selector消失

    在本文中,我们将了解到如下内容:基础的消息转发流程unrecognized selector 拦截建议快速转发(Fast Forwarding)拦截unrecognized selector常规转发(Normal Forwarding)拦截unrecogniz...
    继续阅读 »

    在本文中,我们将了解到如下内容:

    1. 基础的消息转发流程
    2. unrecognized selector 拦截建议
    3. 快速转发(Fast Forwarding)拦截unrecognized selector
    4. 常规转发(Normal Forwarding)拦截unrecognized selector

    前言

    我们在第一天学习Objective-C这一门语言的时候,就被告知这是一门动态语言。
    C这样的编译语言,在编译阶段就确定了所有函数的调用链,如果函数没有被实现,编译就根本不过了。而基于动态语言的特性,在编译期间,我们无法确认程序在运行时要调用哪一个函数,某一个未被实现的函数是否会在运行时被实现。
    这样就可能会出现运行时发现调用的函数根本不存在的尴尬,这也就是我们收到unrecognized selector sent to XXX这样的崩溃的原因了(动态语言也有让人心累的地方,手动叹气)。

    这篇文章要讨论的就是如果遇到了这种尴尬情况的时候,我们该如何避免我们最最最讨厌的崩溃(是的,所有的崩溃都是最最最让人讨厌的)。

    消息转发流程

    我们知道在我们调用某一个方法之后,最终调用的是objc_msgSend()这样一个方法,发送消息(selector)给消息接收者(receiver)。这个方法会根据OC的消息发送机制在receiver中查找selector。如果没有查找到,就会出现上述的运行时调用了未实现的函数的尴尬局面了。

    不过为了缓解这种尴尬,我们还有机会来挣扎。这挣扎机会就是消息转发流程

    消息转发流程包含以下3个步骤:

    1. 动态方法解析:resolveInstanceMethod:resolveClassMethod:
    2. 消息转发
      • 快速转发:forwardingTargetForSelector:
      • 常规转发:methodSignatureForSelector:forwardInvocation:

    消息转发流程是以动态方法解析消息快速转发消息常规转发这样的顺序来执行的。如果其中任意一个步骤能使消息被执行,那么就不会出现unrecognized selector sent to XXX的崩溃

    动态方法解析

    resolveInstanceMethod:这个方法的作用是动态地为selector提供一个实例方法的实现。而resolveClassMethod:则是提供一个类方法的实现。

    所以我们可以在这两个方法中,为对象添加方法的实现,再返回YES告诉已经为selector添加了实现。这样就会重新在对象上查找方法,找到我们新添加的方法后就直接调用。从而避免掉unrecognized selector sent to XXX

    需要注意的是: 这两个方法会响应respondsToSelector:instancesRespondToSelector:

    消息快速转发

    forwardingTargetForSelector:的作用是将消息转发给其它对象去处理。
    我们可以在这个方法中,返回一个对象,让这个对象来响应消息。

    需要注意的是: 如果在这个方法中返回selfnil,则表示没有可响应的目标。

    消息常规转发

    forwardInvocation:的作用也是将消息转发给其它对象。不过与 消息快速转发 不同的是该方法需要手动的创建一个NSInvocation对象,并手动地将新消息发送给新的接收者。

    很显然,这种方式会比 消息快速转发 付出更大的消耗。

    如何选择拦截方案的建议

    对于以上的三个步骤,我们该选择哪一个步骤来进行拦截呢?

    • 动态方法解析 - 不建议
      1. 这个方法会为类添加本身不存在的方法,绝大多数情况下,这个方法时冗余的。
      2. respondsToSelector:instancesRespondToSelector:这两个方法都会调用到resolveInstanceMethod:,那么在我们需要使用这两个方法进行判断的时候,就会出现我们不想看到的情况。
    • 消息快速转发 - 推荐
      会拦截掉已经通过消息常规转发实现的消息转发,但是可以通过判断避开对NSObject子类的消息常规转发的拦截。
    • 消息常规转发 - 推荐
      这一步不会对原有的消息转发机制产生影响,缺点是更大的性能开销。

    快速转发拦截方案

    我们可以创建一个例如:crashPreventor的类,在forwardingTargetForSelector:中为crashPreventor添加selector,最后返回crashPreventor的实例。从而让crashPreventor的实例响应这个selector。具体代码如下:


    @implementation
    NSObject (SelectorPreventor)

    #pragma clang diagnostic push
    #pragma clang diagnostic ignored "-Wobjc-protocol-method-implementation"
    - (id)forwardingTargetForSelector:(SEL)aSelector{
    Class rootClass = NSObject.class;
    Class currentClass = self.class;
    return [self.class actionForwardingTargetForSelector:aSelector rootClass:rootClass currentClass:currentClass];
    }

    + (id)forwardingTargetForSelector:(SEL)aSelector {
    Class rootClass = objc_getMetaClass(class_getName(NSObject.class));
    Class currentClass = objc_getMetaClass(class_getName(self.class));
    return [self actionForwardingTargetForSelector:aSelector rootClass:rootClass currentClass:currentClass];
    }

    + (id)actionForwardingTargetForSelector:(SEL)aSelector rootClass:(Class)rootClass currentClass:(Class)currentClass {
    // 过滤掉内部对象
    NSString *className = NSStringFromClass(currentClass);
    if ([className hasPrefix:@"_"]) {
    return nil;
    }

    SEL methodSignatureSelector = @selector(methodSignatureForSelector:);
    IMP rootMethodSignatureMethod = class_getMethodImplementation(rootClass, methodSignatureSelector);
    IMP currentMethodSignatureMethod = class_getMethodImplementation(currentClass, methodSignatureSelector);
    if (rootMethodSignatureMethod != currentMethodSignatureMethod) {
    return nil;
    }

    NSString * selectorName = NSStringFromSelector(aSelector);

    // 上报异常
    // unrecognized selector sent to class XXX
    // unrecognized selector sent to instance XXX
    NSLog(@"unrecognized selector crash:%@:%@", className, selectorName);

    // 创建crashPreventor类
    NSString *targetClassName = @"crashPreventor";
    Class cls = NSClassFromString(targetClassName);
    if (!cls) {
    // 如果要注册类,则必须要先判断class是否已经存在,否则会产生崩溃
    // 如果不注册类,则可以重复创建class
    cls = objc_allocateClassPair(NSObject.class, targetClassName.UTF8String, 0);
    objc_registerClassPair(cls);
    }

    // 如果类没有对应的方法,则动态添加一个
    if (!class_getInstanceMethod(cls, aSelector)) {
    Method method = class_getInstanceMethod(currentClass, @selector(crashPreventor));
    class_addMethod(cls, aSelector, method_getImplementation(method), method_getTypeEncoding(method));
    }

    return [cls new];
    }

    #pragma clang diagnostic pop

    - (id)crashPreventor {
    return nil;
    }

    @end

    这里有几个点需要提一下:

    1. - (id)forwardingTargetForSelector:(SEL)aSelector;+ (id)forwardingTargetForSelector:(SEL)aSelector;都要在NSObject的分类中重写。前者对应实例方法,后者对应类方法。
    2. 过滤掉一些系统内部对象,否则在启动的时候就会有一些奇怪的异常被捕获到。
    3. 我们需要判断当前类是否实现了methodSignatureForSelector:方法,如果实现了该方法,就认为当前类已经实现了自己的消息转发机制,我们不对其进行拦截。
    4. 细心的我们肯定有注意到,不管是类方法还是实例方法,我们都是向crashPreventor中添加实例方法。这是因为,我们的响应对象时crashPreventor实例,而selector不区分实例方法还是类方法。我们这么处理最终对方法执行来说不会有什么差别。

    常规转发拦截方案

    实现比较简单,我们直接上代码:

    @implementation NSObject (SelectorPreventor)

    #pragma clang diagnostic push
    #pragma clang diagnostic ignored "-Wobjc-protocol-method-implementation"
    - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    return [NSMethodSignature signatureWithObjCTypes:"@"];
    }

    - (void)forwardInvocation:(NSInvocation *)anInvocation {
    NSLog(@"forwardInvocation------");
    }

    + (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    return [NSMethodSignature signatureWithObjCTypes:"@"];
    }

    + (void)forwardInvocation:(NSInvocation *)anInvocation {
    NSLog(@"forwardInvocation------");
    }

    #pragma clang diagnostic pop

    @end

    同样的,类方法和实例方法我们都需要重写。
    methodSignatureForSelector:中我们返回一个返回值为voidNSMethodSignature,在forwardInvocation:中我们不做任何事情。这样将性能消耗减到最小。

    以上:我们可以选择其中一种方式来实现我们对unrecognized selector的拦截,跟unrecognized selector彻底说拜拜啦(手动微笑)。



    作者:一纸苍白
    链接:https://www.jianshu.com/p/90b04882c595
    收起阅读 »