注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

自定义KVO(四)

四、KVOController上面Hook系统kvo相关方法的方式侵入太严重了,我们要做的其实只是需要对自己的调用负责而已,可以通过中间类来完成。这块有很多第三方框架,其中Facebook提供的KVOController是很优秀的一个框架。在这篇文章中将对这个...
继续阅读 »


四、KVOController

上面Hook系统kvo相关方法的方式侵入太严重了,我们要做的其实只是需要对自己的调用负责而已,可以通过中间类来完成。这块有很多第三方框架,其中Facebook提供的KVOController是很优秀的一个框架。在这篇文章中将对这个库进行简单分析。

4.1 KVOController 的使用

#import <KVOController/KVOController.h>


- (void)viewDidLoad {
[super viewDidLoad];

self.KVOController = [FBKVOController controllerWithObserver:self];
[self.KVOController observe:self.obj keyPath:@"name" options:NSKeyValueObservingOptionNew block:^(id _Nullable observer, id _Nonnull object, NSDictionary<NSString *,id> * _Nonnull change) {
NSLog(@"change:%@",change);
}];

[self.KVOController observe:self.obj keyPath:@"name" options:NSKeyValueObservingOptionNew block:^(id _Nullable observer, id _Nonnull object, NSDictionary<NSString *,id> * _Nonnull change) {
NSLog(@"change:%@",change);
}];

[self.KVOController observe:self.obj keyPath:@"nickName" options:NSKeyValueObservingOptionNew action:@selector(hp_NickNameChange:object:)];
}

- (void)hp_NickNameChange:(NSDictionary *)change object:(id)object {
NSLog(@"change:%@ object:%@",change,object);
}

输出:

change:{
FBKVONotificationKeyPathKey = name;
kind = 1;
new = HP111;
}
change:{
kind = 1;
new = cat111;
} object:<HPObject: 0x6000022c91d0>
  • vc持有FBKVOController实例KVOController。在NSObject+FBKVOController.h的关联属性。
  • 通过FBKVOController实例进行注册。注册方式提供了多种。
  • 对于重复添加会进行判断直接返回。
  • 会自动进行移除操作。

4.2 KVOController 实现分析

KVOController主要是使用了中介者模式,官方kvo使用麻烦的点在于使用需要三部曲。KVOController核心就是将三部曲进行了底层封装,上层只需要关心业务逻辑。

FBKVOController会进行注册、移除以及回调的处理(回调包括blockaction以及兼容系统的observe回调)。是对外暴露的交互类。使用FBKVOController分为两步:

  1. 使用 controllerWithObserver 初始化FBKVOController实例。
  2. 使用observe:进行注册。

4.2.1 FBKVOController 初始化

controllerWithObserver
controllerWithObserver最终会调用到initWithObserver中:

- (instancetype)initWithObserver:(nullable id)observer retainObserved:(BOOL)retainObserved
{
self = [super init];
if (nil != self) {
_observer = observer;
NSPointerFunctionsOptions keyOptions = retainObserved ? NSPointerFunctionsStrongMemory|NSPointerFunctionsObjectPointerPersonality : NSPointerFunctionsWeakMemory|NSPointerFunctionsObjectPointerPersonality;
_objectInfosMap = [[NSMapTable alloc] initWithKeyOptions:keyOptions valueOptions:NSPointerFunctionsStrongMemory|NSPointerFunctionsObjectPersonality capacity:0];
pthread_mutex_init(&_lock, NULL);
}
return self;
}

  • _observer是观察者,FBKVOController的属性。
@property (nullable, nonatomic, weak, readonly) id observer;

weak类型,因为FBKVOController本身被观察者持有了。

  • _objectInfosMap根据retainObserved进行NSMapTable内存管理初始化配置,FBKVOController的成员变量。其中保存的是一个被观察者对应多个_FBKVOInfo(也就是被观察对象对应多个keyPath):
  NSMapTable<id, NSMutableSet<_FBKVOInfo *> *> *_objectInfosMap;

这里_FBKVOInfo是放在NSMutableSet中的,说明是去重的。

4.2.2 FBKVOController 注册

由于各个observe方式的原理差不多,这里只分析block的形式。

- (void)observe:(nullable id)object keyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options block:(FBKVONotificationBlock)block
{
NSAssert(0 != keyPath.length && NULL != block, @"missing required parameters observe:%@ keyPath:%@ block:%p", object, keyPath, block);
if (nil == object || 0 == keyPath.length || NULL == block) {
return;
}

// create info
_FBKVOInfo *info = [[_FBKVOInfo alloc] initWithController:self keyPath:keyPath options:options block:block];

// observe object with info
[self _observe:object info:info];
}
  • 首先一些条件容错判断。
  • 构造_FBKVOInfo。保存FBKVOControllerkeyPathoptions以及block
  • 调用_observe:(id)object info:(_FBKVOInfo *)info

4.2.2.1 _FBKVOInfo

@implementation _FBKVOInfo
{
@public
__weak FBKVOController *_controller;
NSString *_keyPath;
NSKeyValueObservingOptions _options;
SEL _action;
void *_context;
FBKVONotificationBlock _block;
_FBKVOInfoState _state;
}
  • _FBKVOInfo中保存了相关数据信息。

并且重写了isEqualhash方法:

- (NSUInteger)hash
{
return [_keyPath hash];
}

- (BOOL)isEqual:(id)object
{
if (nil == object) {
return NO;
}
if (self == object) {
return YES;
}
if (![object isKindOfClass:[self class]]) {
return NO;
}
return [_keyPath isEqualToString:((_FBKVOInfo *)object)->_keyPath];
}

说明只要_keyPath相同就认为是同一对象。

4.2.2.2 _observe: info:

- (void)_observe:(id)object info:(_FBKVOInfo *)info
{
// lock
pthread_mutex_lock(&_lock);

//从TableMap中获取 object(被观察者) 对应的 set
NSMutableSet *infos = [_objectInfosMap objectForKey:object];

// check for info existence
//判断对应的keypath info 是否存在
_FBKVOInfo *existingInfo = [infos member:info];
if (nil != existingInfo) {
//存在直接返回,这里就相当于对于同一个观察者排除了相同的keypath
// observation info already exists; do not observe it again

// unlock and return
pthread_mutex_unlock(&_lock);
return;
}

// lazilly create set of infos
//TableMap数据为空进行创建设置
if (nil == infos) {
infos = [NSMutableSet set];
//<被观察者 - keypaths info>
[_objectInfosMap setObject:infos forKey:object];
}

// add info and oberve
//keypaths info添加 keypath info
[infos addObject:info];

// unlock prior to callout
pthread_mutex_unlock(&_lock);
//注册
[[_FBKVOSharedController sharedController] observe:object info:info];
}
  • 首先判断kayPath是否已经被注册了,注册了直接返回,这里也就进行了去重处理。
  • 将构造的_FBKVOInfo信息添加进_objectInfosMap中。
  • 调用_FBKVOSharedController进行真正的注册。

member:说明
member会调用到_FBKVOInfo中的hash以及isEqual进行判断对象是否存在,也就是判断keyPath对应的对象是否存在。








官方API说明:







源码实现:

+ (NSUInteger)hash {
return _objc_rootHash(self);
}

- (NSUInteger)hash {
return _objc_rootHash(self);
}

+ (BOOL)isEqual:(id)obj {
return obj == (id)self;
}

- (BOOL)isEqual:(id)obj {
return obj == self;
}

uintptr_t
_objc_rootHash(id obj)
{
return (uintptr_t)obj;
}
  • hash默认实现将对象地址转换为uintptr_t类型返回。
  • isEqual:直接判断地址是否相同。
  • member:根据汇编可以看到大概逻辑是先计算参数的hash,然后集合中的元素调用isEqual参数是hash值。

4.2.2.3 _unobserve:info:

- (void)_unobserve:(id)object info:(_FBKVOInfo *)info
{
// lock
pthread_mutex_lock(&_lock);

// get observation infos
NSMutableSet *infos = [_objectInfosMap objectForKey:object];

// lookup registered info instance
_FBKVOInfo *registeredInfo = [infos member:info];

if (nil != registeredInfo) {
[infos removeObject:registeredInfo];

// remove no longer used infos
if (0 == infos.count) {
[_objectInfosMap removeObjectForKey:object];
}
}

// unlock
pthread_mutex_unlock(&_lock);

// unobserve
[[_FBKVOSharedController sharedController] unobserve:object info:registeredInfo];
}

- (void)_unobserve:(id)object
{
// lock
pthread_mutex_lock(&_lock);

NSMutableSet *infos = [_objectInfosMap objectForKey:object];

// remove infos
[_objectInfosMap removeObjectForKey:object];

// unlock
pthread_mutex_unlock(&_lock);

// unobserve
[[_FBKVOSharedController sharedController] unobserve:object infos:infos];
}

- (void)_unobserveAll
{
// lock
pthread_mutex_lock(&_lock);

NSMapTable *objectInfoMaps = [_objectInfosMap copy];

// clear table and map
[_objectInfosMap removeAllObjects];

// unlock
pthread_mutex_unlock(&_lock);

_FBKVOSharedController *shareController = [_FBKVOSharedController sharedController];

for (id object in objectInfoMaps) {
// unobserve each registered object and infos
NSSet *infos = [objectInfoMaps objectForKey:object];
[shareController unobserve:object infos:infos];
}
}

  • _unobserve提供了3个方法进行移除。分别对应keyPathobserverd(被观察对象)、observer(观察者)。
  • 最终都是通过_FBKVOSharedControllerunobserve进行移除。

4.2.3 _FBKVOSharedController

[[_FBKVOSharedController sharedController] observe:object info:info];

4.2.3.1 sharedController

_FBKVOSharedController是个单例,有成员变量_infos:

 NSHashTable<_FBKVOInfo *> *_infos;
不设计FBKVOController为单例是因为它被观察者持有,它是单例观察者就无法释放了。这里_infos存储的是所有类的_FBKVOInfo信息

- (instancetype)init
{
self = [super init];
if (nil != self) {
NSHashTable *infos = [NSHashTable alloc];
#ifdef __IPHONE_OS_VERSION_MIN_REQUIRED
_infos = [infos initWithOptions:NSPointerFunctionsWeakMemory|NSPointerFunctionsObjectPointerPersonality capacity:0];
#elif defined(__MAC_OS_X_VERSION_MIN_REQUIRED)
if ([NSHashTable respondsToSelector:@selector(weakObjectsHashTable)]) {
_infos = [infos initWithOptions:NSPointerFunctionsWeakMemory|NSPointerFunctionsObjectPointerPersonality capacity:0];
} else {
// silence deprecated warnings
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
_infos = [infos initWithOptions:NSPointerFunctionsZeroingWeakMemory|NSPointerFunctionsObjectPointerPersonality capacity:0];
#pragma clang diagnostic pop
}

#endif
pthread_mutex_init(&_mutex, NULL);
}
return self;
}

  • infos的初始化是weak的,也就是它不影响_FBKVOInfo的引用计数。

4.2.3.2 observe: info:

- (void)observe:(id)object info:(nullable _FBKVOInfo *)info
{
if (nil == info) {
return;
}

// register info
pthread_mutex_lock(&_mutex);
[_infos addObject:info];
pthread_mutex_unlock(&_mutex);

// add observer
//被观察者调用官方kvo进行注册,context 传递的是 _FBKVOInfo 信息。
[object addObserver:self forKeyPath:info->_keyPath options:info->_options context:(void *)info];

if (info->_state == _FBKVOInfoStateInitial) {
//状态变为Observing
info->_state = _FBKVOInfoStateObserving;
} else if (info->_state == _FBKVOInfoStateNotObserving) {
// this could happen when `NSKeyValueObservingOptionInitial` is one of the NSKeyValueObservingOptions,
// and the observer is unregistered within the callback block.
// at this time the object has been registered as an observer (in Foundation KVO),
// so we can safely unobserve it.
//当状态变为不在观察时移除
[object removeObserver:self forKeyPath:info->_keyPath context:(void *)info];
}
}
  • 首先自己持有了传进来的info信息。
  • observe: info:中调用系统kvo方法观察注册。context传递的是_FBKVOInfo信息。
  • 对于系统而言观察者是_FBKVOSharedController

4.2.3.3 unobserve: info:

- (void)unobserve:(id)object info:(nullable _FBKVOInfo *)info
{
if (nil == info) {
return;
}

// unregister info
pthread_mutex_lock(&_mutex);
[_infos removeObject:info];
pthread_mutex_unlock(&_mutex);

// remove observer
if (info->_state == _FBKVOInfoStateObserving) {
[object removeObserver:self forKeyPath:info->_keyPath context:(void *)info];
}
info->_state = _FBKVOInfoStateNotObserving;
}
  • 调用系统的removeObserver移除观察。

4.2.3.4 observeValueForKeyPath

既然是在4.2.3_FBKVOSharedController中进行的注册,那么系统的回调observeValueForKeyPath必然由它实现:

- (void)observeValueForKeyPath:(nullable NSString *)keyPath
ofObject:(nullable id)object
change:(nullable NSDictionary<NSString *, id> *)change
context:(nullable void *)context
{
NSAssert(context, @"missing context keyPath:%@ object:%@ change:%@", keyPath, object, change);

_FBKVOInfo *info;

{
// lookup context in registered infos, taking out a strong reference only if it exists
pthread_mutex_lock(&_mutex);
info = [_infos member:(__bridge id)context];
pthread_mutex_unlock(&_mutex);
}

if (nil != info) {

// take strong reference to controller
FBKVOController *controller = info->_controller;
if (nil != controller) {

// take strong reference to observer
//观察者
id observer = controller.observer;
if (nil != observer) {

// dispatch custom block or action, fall back to default action
if (info->_block) {
NSDictionary<NSString *, id> *changeWithKeyPath = change;
// add the keyPath to the change dictionary for clarity when mulitple keyPaths are being observed
if (keyPath) {
//将keypath加入字典中
NSMutableDictionary<NSString *, id> *mChange = [NSMutableDictionary dictionaryWithObject:keyPath forKey:FBKVONotificationKeyPathKey];
[mChange addEntriesFromDictionary:change];
changeWithKeyPath = [mChange copy];
}
info->_block(observer, object, changeWithKeyPath);
} else if (info->_action) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[observer performSelector:info->_action withObject:change withObject:object];
#pragma clang diagnostic pop
} else {
[observer observeValueForKeyPath:keyPath ofObject:object change:change context:info->_context];
}
}
}
}
}
  • info中获取观察者,info信息是context传递过来的。
  • _FBKVOInfo存在的情况下根据类型(blockaction、系统原始回调)进行了回调。block回调的过程中添加了keyPath

4.2.4 自动移除观察者

FBKVOControllerdealloc中调用了unobserveAll进行移除:

- (void)dealloc
{
[self unobserveAll];
pthread_mutex_destroy(&_lock);
}

由于FBKVOController的实例是被观察者持有的,所以当观察者dealloc的时候FBKVOController实例也就dealloc了。在这里调用就相当于在观察者dealloc中调用了移除。

FBKVOController流程




五、通过gnustep探索

kvokvc相关的代码苹果并没有开源,对于它们的探索可以通过gnustep查看原理,gnustep中有一些苹果早期底层的实现。



5.1 addObserver


  • setup()中是对一些表的初始化。
  • replacementForClass创建并注册kvo类。
  • 创建GSKVOInfo信息加入Map中。然后进行isa替换。
  • 重写setter方法。





  • 根据是否开启自动回调决定是否调用willChangeValueForKey以及didChangeValueForKey

didChangeValueForKey




最终调用了notifyForKey发送通知。

notifyForKey:ofInstance:prior:



收起阅读 »

自定义KVO(三)

三、系统kvo容错处理在上面自定义kvo中处理了自动移除观察者逻辑,以及将回调使用block实现。在实际使用系统kvo的时候有以下问题:1.多次添加同一观察者会进行多次回调。2.某个属性没有被观察,在dealloc中移除会造成crash。3.多次移除观察者也会...
继续阅读 »

三、系统kvo容错处理

在上面自定义kvo中处理了自动移除观察者逻辑,以及将回调使用block实现。在实际使用系统kvo的时候有以下问题:
1.多次添加同一观察者会进行多次回调。
2.某个属性没有被观察,在dealloc中移除会造成crash
3.多次移除观察者也会造成crash
4.不移除观察者有可能造成crash。(观察者释放后被观察者调用回调)

那么要避免就要在添加和移除以及dealloc过程中做容错处理。

NSObject(NSKeyValueObservingCustomization)中发现了observationInfo


/*
Take or return a pointer that identifies information about all of the observers that are registered with the receiver, the options that were used at registration-time, etc.
The default implementation of these methods store observation info in a global dictionary keyed by the receivers' pointers. For improved performance, you can override these methods to store the opaque data pointer in an instance variable.
Overrides of these methods must not attempt to send Objective-C messages to the passed-in observation info, including -retain and -release.
*/

@property (nullable) void *observationInfo NS_RETURNS_INNER_POINTER;
根据注释可以看到默认情况下observationInfo中保存了所有观察者信息。
那么observationInfo保存在哪里呢?直接代码验证下:


NSLog(@"observed before %@",self.obj.observationInfo);
NSLog(@"observe before %@",self.observationInfo);
[self.obj addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];
NSLog(@"observe after %@",self.observationInfo);
NSLog(@"observed after %@",self.obj.observationInfo);
输出

observed before (null)
observe before (null)
observe after (null)
observed after <NSKeyValueObservationInfo 0x60000100c700> (
<NSKeyValueObservance 0x600001ee0c90: Observer: 0x7fd6eb112cb0, Key path: name, Options: <New: YES, Old: NO, Prior: NO> Context: 0x0, Property: 0x600001ee1050>
)

可以看到在注册后存入到了被观察者中,类型是NSKeyValueObservationInfo,它是一个私有类。NSKeyValueObservationInfo中保存的是NSKeyValueObservance
NSKeyValueObservationInfo保存了NSKeyValueObservance集合,NSKeyValueObservance中保存了观察者注册的时候的信息。
既然是在Foundation框架中,那么dump一下这个动态库的头文件(越狱手机使用classdump-dyld导出头文件)。

NSKeyValueObservationInfo头文件:

@class NSArray;
@interface NSKeyValueObservationInfo : NSObject {
NSArray* _observances;
unsigned long long _cachedHash;
BOOL _cachedIsShareable;
}
@property (nonatomic,readonly) BOOL containsOnlyInternalObservationHelpers;
-(void)dealloc;
-(unsigned long long)hash;
-(id)_initWithObservances:(id*)arg1 count:(unsigned long long)arg2 hashValue:(unsigned long long)arg3 ;
-(id)description;
-(BOOL)containsOnlyInternalObservationHelpers;
-(BOOL)isEqual:(id)arg1 ;
-(id)_copyByAddingObservance:(id)arg1 ;
@end

NSKeyValueObservance头文件:

@class NSObject, NSKeyValueProperty;
@interface NSKeyValueObservance : NSObject {
NSObject* _observer;
NSKeyValueProperty* _property;
void* _context;
NSObject* _originalObservable;
unsigned _options : 6;
unsigned _cachedIsShareable : 1;
unsigned _isInternalObservationHelper : 1;
}
-(id)_initWithObserver:(id)arg1 property:(id)arg2 options:(unsigned long long)arg3 context:(void*)arg4 originalObservable:(id)arg5 ;
-(unsigned long long)hash;
-(id)description;
-(BOOL)isEqual:(id)arg1 ;
-(void)observeValueForKeyPath:(id)arg1 ofObject:(id)arg2 change:(id)arg3 context:(void*)arg4 ;
@end
那么基本可以确定_observances中保存的是NSKeyValueObservance
代码验证:



它的定义如下:

@class NSKeyValueContainerClass, NSString;
@interface NSKeyValueProperty : NSObject <NSCopying> {
NSKeyValueContainerClass* _containerClass;
NSString* _keyPath;
}
-(Class)isaForAutonotifying;
-(id)_initWithContainerClass:(id)arg1 keyPath:(id)arg2 propertiesBeingInitialized:(CFSetRef)arg3 ;
-(id)dependentValueKeyOrKeysIsASet:(BOOL*)arg1 ;
-(void)object:(id)arg1 withObservance:(id)arg2 didChangeValueForKeyOrKeys:(id)arg3 recurse:(BOOL)arg4 forwardingValues:(SCD_Struct_NS48)arg5 ;
-(BOOL)object:(id)arg1 withObservance:(id)arg2 willChangeValueForKeyOrKeys:(id)arg3 recurse:(BOOL)arg4 forwardingValues:(SCD_Struct_NS48*)arg5 ;
-(void)object:(id)arg1 didAddObservance:(id)arg2 recurse:(BOOL)arg3 ;
-(id)restOfKeyPathIfContainedByValueForKeyPath:(id)arg1 ;
-(void)object:(id)arg1 didRemoveObservance:(id)arg2 recurse:(BOOL)arg3 ;
-(BOOL)matchesWithoutOperatorComponentsKeyPath:(id)arg1 ;
-(id)copyWithZone:(NSZone*)arg1 ;
-(id)keyPath;
-(void)dealloc;
-(id)keyPathIfAffectedByValueForKey:(id)arg1 exactMatch:(BOOL*)arg2 ;
-(id)keyPathIfAffectedByValueForMemberOfKeys:(id)arg1 ;
@end
  • _observer存储在NSKeyValueObservance 中。
  • _keyPath存储在NSKeyValueObservance_propertyNSKeyValueProperty)中。

3.1Hook注册和移除方法

要对系统方法进行容错处理那么最好的办法就是Hook了,直接对添加和移除的3个方法进行Hook处理:

+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[self hp_methodSwizzleWithClass:self oriSEL:@selector(addObserver:forKeyPath:options:context:) swizzledSEL:@selector(hp_addObserver:forKeyPath:options:context:) isClassMethod:NO];
[self hp_methodSwizzleWithClass:self oriSEL:@selector(removeObserver:forKeyPath:context:) swizzledSEL:@selector(hp_removeObserver:forKeyPath:context:)isClassMethod:NO];
[self hp_methodSwizzleWithClass:self oriSEL:@selector(removeObserver:forKeyPath:) swizzledSEL:@selector(hp_removeObserver:forKeyPath:)isClassMethod:NO];
});
}
  • 由于removeObserver:forKeyPath:底层调用的不是removeObserver:forKeyPath:context:所以两个方法都要Hook

那么核心逻辑就是怎么判断observer对应的keyPath是否存在。由于observationInfo存储的是私有类,那么直接通过kvc获取值:

- (BOOL)keyPathIsExist:(NSString *)sarchKeyPath observer:(id)observer {
BOOL findKey = NO;
id info = self.observationInfo;
if (info) {
NSArray *observances = [info valueForKeyPath:@"_observances"];
for (id observance in observances) {
id tempObserver = [observance valueForKey:@"_observer"];
if (tempObserver == observer) {
NSString *keyPath = [observance valueForKeyPath:@"_property._keyPath"];
if ([keyPath isEqualToString:sarchKeyPath]) {
findKey = YES;
break;
}
}
}
}
return findKey;
}

Hook的具体实现:

- (void)hp_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context {
if ([self keyPathIsExist:keyPath observer:observer]) {//observer 观察者已经添加了对应key的观察,再次添加不做处理。
return;
}
[self hp_addObserver:observer forKeyPath:keyPath options:options context:context];
}

- (void)hp_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(nullable void *)context {
if ([self keyPathIsExist:keyPath observer:observer]) {//key存在才移除
[self hp_removeObserver:observer forKeyPath:keyPath context:context];
}
}

- (void)hp_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath {
if ([self keyPathIsExist:keyPath observer:observer]) {//key存在才移除
[self hp_removeObserver:observer forKeyPath:keyPath];
}
}

  • 这样就解决了重复添加和移除的问题。

3.2 自动移除观察者

3.1中解决了重复添加和移除的问题,还有一个问题是dealloc的时候自动移除。这块思路与自定义kvo相同,通过Hook观察者的的dealloc实现。

- (void)hp_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context {
if ([self keyPathIsExist:keyPath observer:observer]) {//observer 观察者已经添加了对应key的观察,再次添加不做处理。
return;
}
NSString *className = NSStringFromClass([self class]);
NSString *newClassName = [NSString stringWithFormat:@"NSKVONotifying_%@",className];
Class newClass = NSClassFromString(newClassName);
if (!newClass) {//类不存在的时候进行 hook 观察者 dealloc
//hook dealloc
[[observer class] hp_methodSwizzleWithClass:[observer class] oriSEL:NSSelectorFromString(@"dealloc") swizzledSEL:@selector(hp_dealloc) isClassMethod:NO];
}
[self hp_addObserver:observer forKeyPath:keyPath options:options context:context];
}

- (void)hp_dealloc {
[self hp_removeSelfAllObserverd];
[self hp_dealloc];
}
  • kvo子类已经存在的时候证明已经hook过了。

deallocself.observationInfo是获取不到信息的,因为observationInfo是存储在被观察者中的。所以还需要自己存储信息。
修改如下:

static NSString *const kHPSafeKVOObserverdAssiociateKey = @"HPSafeKVOObserverdAssiociateKey";

@interface HPSafeKVOObservedInfo : NSObject

@property (nonatomic, weak) id observerd;
@property (nonatomic, copy) NSString *keyPath;
@property (nonatomic, strong) id context;

@end

@implementation HPSafeKVOObservedInfo

- (instancetype)initWitObserverd:(NSObject *)observerd forKeyPath:(NSString *)keyPath context:(nullable void *)context {
if (self=[super init]) {
_observerd = observerd;
_keyPath = keyPath;
_context = (__bridge id)(context);
}
return self;
}

@end

- (void)hp_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context {
if ([self keyPathIsExist:keyPath observer:observer]) {//observer 观察者已经添加了对应key的观察,再次添加不做处理。
return;
}
NSString *className = NSStringFromClass([self class]);
NSString *newClassName = [NSString stringWithFormat:@"NSKVONotifying_%@",className];
Class newClass = NSClassFromString(newClassName);
if (!newClass) {//类不存在的时候进行 hook 观察者 dealloc
//hook dealloc
[[observer class] hp_methodSwizzleWithClass:[observer class] oriSEL:NSSelectorFromString(@"dealloc") swizzledSEL:@selector(hp_dealloc) isClassMethod:NO];
}

//保存被观察者信息
HPSafeKVOObservedInfo *kvoObservedInfo = [[HPSafeKVOObservedInfo alloc] initWitObserverd:self forKeyPath:keyPath context:context];
NSMutableArray *observerdArray = objc_getAssociatedObject(observer, (__bridge const void * _Nonnull)(kHPSafeKVOObserverdAssiociateKey));
if (!observerdArray) {
observerdArray = [NSMutableArray arrayWithCapacity:1];
}
[observerdArray addObject:kvoObservedInfo];
objc_setAssociatedObject(observer, (__bridge const void * _Nonnull)(kHPSafeKVOObserverdAssiociateKey), observerdArray, OBJC_ASSOCIATION_RETAIN_NONATOMIC);

//调用原始方法
[self hp_addObserver:observer forKeyPath:keyPath options:options context:context];
}

hp_dealloc中主动调用移除方法:

- (void)hp_dealloc {
[self hp_removeSelfAllObserverd];
[self hp_dealloc];
}

- (void)hp_removeSelfAllObserverd {
NSMutableArray *observerdArray = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kHPSafeKVOObserverdAssiociateKey));
for (HPSafeKVOObservedInfo *info in observerdArray) {
if (info.observerd) {
//调用系统方法,已经hook了,走hook逻辑。
if (info.context) {
[info.observerd removeObserver:self forKeyPath:info.keyPath context:(__bridge void * _Nullable)(info.context)];
} else {
[info.observerd removeObserver:self forKeyPath:info.keyPath];
}
}
}
}

这样在dealloc的时候就主动清空了已经释放掉的observer

3.3 问题处理

上面这样处理后在退出页面的时候发生了crash(非必现),堆栈如下:



UIScreen 观察了 CADisplay 的 cloned。但是在释放的过程中UIScreen却没有调用到Hookhp_dealloc中,对应的汇编实现:

int -[UIScreen dealloc](int arg0) {
[r0 _invalidate];
__UIScreenWriteDisplayConfiguration(r0, 0x0, 0x0);
r0 = [[&stack[0] super] dealloc];
return r0;
}

int -[UIScreen _invalidate](int arg0) {
var_10 = r20;
stack[-24] = r19;
r31 = r31 + 0xffffffffffffffe0;
saved_fp = r29;
stack[-8] = r30;
r19 = arg0;
*(int16_t *)(arg0 + 0xb0) = *(int16_t *)(arg0 + 0xb0) & 0xffffffffffffffcf;
r0 = [NSNotificationCenter defaultCenter];
r0 = [r0 retain];
[r0 removeObserver:r19];
[r0 release];
if ([r19 _isCarScreen] == 0x0) goto loc_7495b4;

loc_749570:
r0 = __UIInternalPreferenceUsesDefault_751e78(0x19080b0, @"ApplySceneUserInterfaceStyleToCarScreen", 0xec7178);
if (((*(int8_t *)0x19080b4 & 0x1) == 0x0) || (r0 != 0x0)) goto loc_74959c;

loc_7495b4:
[r19 _endObservingBacklightLevelNotifications];
[r19 _setSoftwareDimmingWindow:0x0];
r0 = *(r19 + 0x90);
r0 = [r0 _setScreen:0x0];
return r0;

loc_74959c:
CFNotificationCenterRemoveObserver(CFNotificationCenterGetDarwinNotifyCenter(), r19, @"CarPlayUserInterfaceStyleDidChangeNotification", 0x0);
goto loc_7495b4;
}
那么意味着是否没有替换成功?
_UIScreenWriteDisplayConfiguration中确实先移除后添加:



是否进行注册是通过rax控制的。也就是__UIScreenIsCapturedValueOverride.isCapturedValue控制的。
经过测试只要在系统自动调用UIScreen initialize之前调用一个UIScreen相关方法就不走kvo设置逻辑了,比如:

[UIScreen mainScreen]
//[UIScreen class]

目前不清楚原因。所以处理这个问题有两个思路:

  1. + load进行方法交换的时候先调用[UIScreen class]
  1. 在注册的时候对系统类或者自己的类进行过滤。
  • 2.1只排除UIScreen
if ([observer isKindOfClass:[UIScreen class]]) {
[self hp_addObserver:observer forKeyPath:keyPath options:options context:context];
return;
}
  • 2.2排除系统类
NSString *className = NSStringFromClass([observer class]);
if ([className hasPrefix:@"NS"] || [className hasPrefix:@"UI"]) { //排除某些系统类。
[self hp_addObserver:observer forKeyPath:keyPath options:options context:context];
return;
}
  • 2.3 只处理自己的类
NSString *className = NSStringFromClass([observer class]);
if (![className hasPrefix:@"HP"]) { //排除某些系统类。
[self hp_addObserver:observer forKeyPath:keyPath options:options context:context];
return;
}


收起阅读 »

自定义KVO(二)

2.2.2 优化Hook逻辑上面在+ load中Hook dealloc方法是在NSObject分类中处理的,那么意味着所有的类的dealloc方法都被Hook了。显然这么做是不合理的。逻辑就是仅对需要的类进行Hook dealloc方法,所以将Hook延迟到...
继续阅读 »

2.2.2 优化Hook逻辑

上面在+ loadHook dealloc方法是在NSObject分类中处理的,那么意味着所有的类的dealloc方法都被Hook了。显然这么做是不合理的。
逻辑就是仅对需要的类进行Hook dealloc方法,所以将Hook延迟到addObserver中:

- (void)hp_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath block:(HPKVOBlock)block {
……
//hook dealloc
[[observer class] hp_methodSwizzleWithClass:[observer class] oriSEL:NSSelectorFromString(@"dealloc") swizzledSEL:@selector(hp_dealloc) isClassMethod:NO];
}

但是只应该对dealloc hook一次,否则又交换回来了。要么做标记,要么在创建kvo子类的时候进行hook。显然在创建子类的时候更合适。修改逻辑如下:
//申请类-注册类-添加方法
- (Class)_creatKVOClassWithKeyPath:(NSString *)keyPath observer:(NSObject *)observer {
//这里重写class后kvo子类也返回的是父类的名字
NSString *superClassName = NSStringFromClass([self class]);
NSString *newClassName = [NSString stringWithFormat:@"%@%@",kHPBlockKVOClassPrefix,superClassName];
Class newClass = NSClassFromString(newClassName);
//类是否存在
if (!newClass) {//不存在需要创建类
//1:申请类 父类、新类名称、额外空间
newClass = objc_allocateClassPair([self class], newClassName.UTF8String, 0);
//2:注册类
objc_registerClassPair(newClass);
//3:添加class方法,class返回父类信息 这里是`-class`
SEL classSEL = NSSelectorFromString(@"class");
Method classMethod = class_getInstanceMethod([self class], classSEL);
const char *classTypes = method_getTypeEncoding(classMethod);
class_addMethod(newClass, classSEL, (IMP)_hp_class, classTypes);

//hook dealloc
[[observer class] hp_methodSwizzleWithClass:[observer class] oriSEL:NSSelectorFromString(@"dealloc") swizzledSEL:@selector(hp_dealloc) isClassMethod:NO];
}
//4:添加setter方法
SEL setterSEL = NSSelectorFromString(_setterForGetter(keyPath));
Method setterMethod = class_getInstanceMethod([self class], setterSEL);
const char *setterTypes = method_getTypeEncoding(setterMethod);
class_addMethod(newClass, setterSEL, (IMP)_hp_setter, setterTypes);

return newClass;
}

完整实现代码:

typedef void(^HPKVOBlock)(id observer,NSString *keyPath,id oldValue,id newValue);

@interface NSObject (HP_KVO_Block)

- (void)hp_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath block:(HPKVOBlock)block;

- (void)hp_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;

@end

#import "NSObject+HP_KVO_Block.h"
#import <objc/runtime.h>
#import <objc/message.h>
#import "HPKVOInfo.h"

static NSString *const kHPBlockKVOClassPrefix = @"HPKVONotifying_";
static NSString *const kHPBlockKVOAssiociateKey = @"HPKVOAssiociateKey";

static NSString *const kHPBlockKVOObserverdAssiociateKey = @"HPKVOObserverdAssiociateKey";

@interface HPKVOBlockInfo : NSObject

@property (nonatomic, weak) id observer;
@property (nonatomic, copy) NSString *keyPath;
@property (nonatomic, copy) HPKVOBlock handleBlock;

@end

@implementation HPKVOBlockInfo

- (instancetype)initWitObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath handleBlock:(HPKVOBlock)block {
if (self=[super init]) {
_observer = observer;
_keyPath = keyPath;
_handleBlock = block;
}
return self;
}

@end

@interface HPKVOObservedInfo : NSObject

@property (nonatomic, weak) id observerd;
@property (nonatomic, copy) NSString *keyPath;

@end

@implementation HPKVOObservedInfo

- (instancetype)initWitObserverd:(NSObject *)observerd forKeyPath:(NSString *)keyPath {
if (self=[super init]) {
_observerd = observerd;
_keyPath = keyPath;
}
return self;
}

@end

@implementation NSObject (HP_KVO_Block)


- (void)hp_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath block:(HPKVOBlock)block {
//1.参数判断 以及 setter检查
if (!observer || !keyPath) return;
BOOL result = [self _handleSetterMethodFromKeyPath:keyPath];
if (!result) return;

//2.isa_swizzle 申请类-注册类-添加方法
Class newClass = [self _creatKVOClassWithKeyPath:keyPath observer:observer];

//3.isa 指向子类
object_setClass(self, newClass);
//4.setter逻辑处理
//保存观察者信息-数组
HPKVOBlockInfo *kvoInfo = [[HPKVOBlockInfo alloc] initWitObserver:observer forKeyPath:keyPath handleBlock:block];
NSMutableArray *observerArray = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kHPBlockKVOAssiociateKey));
if (!observerArray) {
observerArray = [NSMutableArray arrayWithCapacity:1];
}
[observerArray addObject:kvoInfo];
objc_setAssociatedObject(self, (__bridge const void * _Nonnull)(kHPBlockKVOAssiociateKey), observerArray, OBJC_ASSOCIATION_RETAIN_NONATOMIC);

//保存被观察者信息
HPKVOObservedInfo *kvoObservedInfo = [[HPKVOObservedInfo alloc] initWitObserverd:self forKeyPath:keyPath];
NSMutableArray *observerdArray = objc_getAssociatedObject(observer, (__bridge const void * _Nonnull)(kHPBlockKVOObserverdAssiociateKey));
if (!observerdArray) {
observerdArray = [NSMutableArray arrayWithCapacity:1];
}
[observerdArray addObject:kvoObservedInfo];
objc_setAssociatedObject(observer, (__bridge const void * _Nonnull)(kHPBlockKVOObserverdAssiociateKey), observerdArray, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (void)hp_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath {
NSMutableArray *observerArray = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kHPBlockKVOAssiociateKey));
if (observerArray.count <= 0) {
return;
}

NSMutableArray *tempArray = [observerArray mutableCopy];
for (HPKVOInfo *info in tempArray) {
if ([info.keyPath isEqualToString:keyPath]) {
if (info.observer) {
if (info.observer == observer) {
[observerArray removeObject:info];
}
} else {
[observerArray removeObject:info];
}
}
}
objc_setAssociatedObject(self, (__bridge const void * _Nonnull)(kHPBlockKVOAssiociateKey), observerArray, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
//已经全部移除了
if (observerArray.count <= 0) {
//isa指回给父类
Class superClass = [self class];
object_setClass(self, superClass);
}
}

- (BOOL)_handleSetterMethodFromKeyPath:(NSString *)keyPath {
SEL setterSeletor = NSSelectorFromString(_setterForGetter(keyPath));
Method setterMethod = class_getInstanceMethod(object_getClass(self), setterSeletor);
NSAssert(setterMethod, @"%@ setter is not exist",keyPath);
return setterMethod ? YES : NO;
}

// 从get方法获取set方法的名称 key -> setKey
static NSString *_setterForGetter(NSString *getter) {
if (getter.length <= 0) { return nil;}
NSString *firstString = [[getter substringToIndex:1] uppercaseString];
NSString *otherString = [getter substringFromIndex:1];
return [NSString stringWithFormat:@"set%@%@:",firstString,otherString];
}

//申请类-注册类-添加方法
- (Class)_creatKVOClassWithKeyPath:(NSString *)keyPath observer:(NSObject *)observer {
//这里重写class后kvo子类也返回的是父类的名字
NSString *superClassName = NSStringFromClass([self class]);
NSString *newClassName = [NSString stringWithFormat:@"%@%@",kHPBlockKVOClassPrefix,superClassName];
Class newClass = NSClassFromString(newClassName);
//类是否存在
if (!newClass) {//不存在需要创建类
//1:申请类 父类、新类名称、额外空间
newClass = objc_allocateClassPair([self class], newClassName.UTF8String, 0);
//2:注册类
objc_registerClassPair(newClass);
//3:添加class方法,class返回父类信息 这里是`-class`
SEL classSEL = NSSelectorFromString(@"class");
Method classMethod = class_getInstanceMethod([self class], classSEL);
const char *classTypes = method_getTypeEncoding(classMethod);
class_addMethod(newClass, classSEL, (IMP)_hp_class, classTypes);

//hook dealloc
[[observer class] hp_methodSwizzleWithClass:[observer class] oriSEL:NSSelectorFromString(@"dealloc") swizzledSEL:@selector(hp_dealloc) isClassMethod:NO];
}
//4:添加setter方法
SEL setterSEL = NSSelectorFromString(_setterForGetter(keyPath));
Method setterMethod = class_getInstanceMethod([self class], setterSEL);
const char *setterTypes = method_getTypeEncoding(setterMethod);
class_addMethod(newClass, setterSEL, (IMP)_hp_setter, setterTypes);

return newClass;
}

//返回父类信息
Class _hp_class(id self,SEL _cmd) {
return class_getSuperclass(object_getClass(self));
}

static void _hp_setter(id self,SEL _cmd,id newValue) {
//自动开关判断,省略
//保存旧值
NSString *keyPath = _getterForSetter(NSStringFromSelector(_cmd));
id oldValue = [self valueForKey:keyPath];
//1.调用父类的setter(也可以通过performSelector调用)
void (*hp_msgSendSuper)(void *,SEL , id) = (void *)objc_msgSendSuper;
struct objc_super super_struct = {
.receiver = self,
.super_class = class_getSuperclass(object_getClass(self)),
};
hp_msgSendSuper(&super_struct,_cmd,newValue);

//2.通知观察者
NSMutableArray *observerArray = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kHPBlockKVOAssiociateKey));
for (HPKVOBlockInfo *info in observerArray) {//循环调用,可能添加多次。
if ([info.keyPath isEqualToString:keyPath] && info.handleBlock && info.observer) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
info.handleBlock(info.observer, keyPath, oldValue, newValue);
});
}
}
}

//获取getter
static NSString *_getterForSetter(NSString *setter){
if (setter.length <= 0 || ![setter hasPrefix:@"set"] || ![setter hasSuffix:@":"]) { return nil;}
NSRange range = NSMakeRange(3, setter.length - 4);
NSString *getter = [setter substringWithRange:range];
NSString *firstString = [[getter substringToIndex:1] lowercaseString];
return [getter stringByReplacingCharactersInRange:NSMakeRange(0, 1) withString:firstString];
}

+ (void)hp_methodSwizzleWithClass:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swizzledSEL isClassMethod:(BOOL)isClassMethod {
if (!cls) {
NSLog(@"class is nil");
return;
}
if (!swizzledSEL) {
NSLog(@"swizzledSEL is nil");
return;
}
//类/元类
Class swizzleClass = isClassMethod ? object_getClass(cls) : cls;
Method oriMethod = class_getInstanceMethod(swizzleClass, oriSEL);
Method swiMethod = class_getInstanceMethod(swizzleClass, swizzledSEL);
if (!oriMethod) {//原始方法没有实现
// 在oriMethod为nil时,替换后将swizzledSEL复制一个空实现
class_addMethod(swizzleClass, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
//添加一个空的实现
method_setImplementation(swiMethod, imp_implementationWithBlock(^(id self, SEL _cmd){
NSLog(@"imp default null implementation");
}));
}
//自己没有则会添加成功,自己有添加失败
BOOL success = class_addMethod(swizzleClass, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(oriMethod));
if (success) {//自己没有方法添加一个,添加成功则证明自己没有。
class_replaceMethod(swizzleClass, swizzledSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
} else { //自己有直接进行交换
method_exchangeImplementations(oriMethod, swiMethod);
}
}

- (void)hp_dealloc {
NSMutableArray *observerdArray = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kHPBlockKVOObserverdAssiociateKey));
for (HPKVOObservedInfo *info in observerdArray) {
if (info.observerd) {
[info.observerd hp_removeObserver:self forKeyPath:info.keyPath];
}
}
[self hp_dealloc];
}

@end


收起阅读 »

自定义KVO(一)

kvo1.1 hp_addObserver由于只有属性才有效,所以先进行容错处理。1.1.2 isa_swizzle动态生成子类static NSString *const kHPKVOClassPrefix = @"HPKVONotifying_"; //...
继续阅读 »

实现一个简单的kvo

@interface NSObject (HP_KVO)

- (void)hp_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
- (void)hp_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(nullable void *)context;
- (void)hp_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;

@end

1.1 hp_addObserver

1.1.1参数检查

- (void)hp_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context {
//1.参数判断 以及 setter检查
if (!observer || !keyPath) return;
BOOL result = [self handleSetterMethodFromKeyPath:keyPath];
if (!result) return;
}

- (BOOL)handleSetterMethodFromKeyPath:(NSString *)keyPath {
SEL setterSeletor = NSSelectorFromString(setterForGetter(keyPath));
Method setterMethod = class_getInstanceMethod(object_getClass(self), setterSeletor);
NSAssert(setterMethod, @"%@ setter is not exist",keyPath);
return setterMethod ? YES : NO;
}

// 从get方法获取set方法的名称 key -> setKey
static NSString *setterForGetter(NSString *getter) {
if (getter.length <= 0) { return nil;}
NSString *firstString = [[getter substringToIndex:1] uppercaseString];
NSString *otherString = [getter substringFromIndex:1];
return [NSString stringWithFormat:@"set%@%@:",firstString,otherString];
}

由于只有属性才有效,所以先进行容错处理。

1.1.2 isa_swizzle动态生成子类

static NSString *const kHPKVOClassPrefix = @"HPKVONotifying_";

//申请类-注册类-添加方法
- (Class)creatKVOClassWithKeyPath:(NSString *)keyPath {
//这里重写class后kvo子类也返回的是父类的名字
NSString *superClassName = NSStringFromClass([self class]);
NSString *newClassName = [NSString stringWithFormat:@"%@%@",kHPKVOClassPrefix,superClassName];
Class newClass = NSClassFromString(newClassName);
//类是否存在
if (!newClass) {//不存在需要创建类
//1:申请类 父类、新类名称、额外空间
newClass = objc_allocateClassPair([self class], newClassName.UTF8String, 0);
//2:注册类
objc_registerClassPair(newClass);
//3:添加class方法,class返回父类信息 这里是`-class`
SEL classSEL = NSSelectorFromString(@"class");
Method classMethod = class_getInstanceMethod([self class], classSEL);
const char *classTypes = method_getTypeEncoding(classMethod);
class_addMethod(newClass, classSEL, (IMP)hp_class, classTypes);
}
//4:添加setter方法
SEL setterSEL = NSSelectorFromString(setterForGetter(keyPath));
Method setterMethod = class_getInstanceMethod([self class], setterSEL);
const char *setterTypes = method_getTypeEncoding(setterMethod);
class_addMethod(newClass, setterSEL, (IMP)hp_setter, setterTypes);

return newClass;
}

//返回父类信息
Class hp_class(id self,SEL _cmd){
return class_getSuperclass(object_getClass(self));
}

static void hp_setter(id self,SEL _cmd,id newValue){

}
  • 根据类名字拼接kvo类名字,判断是否已经存在。(superClassName由于class重写了,即使二次进入也获取到的是父类的名字)。
  • newClass不存在则调用objc_allocateClassPair创建kvo子类。并且重写- class方法。
  • 添加对应的setter方法。

当然也可以写+class,写入元类中。在objc_allocateClassPair后元类就存在了:




1.1.3 isa 指向子类

object_setClass(self, newClass);
  • 直接调用object_setClass设置objisa为新创建的kvo子类。

object_setClass源码:


Class object_setClass(id obj, Class cls)
{
if (!obj) return nil;
if (!cls->isFuture() && !cls->isInitialized()) {
lookUpImpOrNilTryCache(nil, @selector(initialize), cls, LOOKUP_INITIALIZE);
}

return obj->changeIsa(cls);
}

源码中就是修改对象的isa指向。


1.1.4 setter逻辑

在进行了上面逻辑的处理后,这个时候调用如下代码:

[self.obj hp_addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];
self.obj.name = @"HP";

会进入hp_setter函数。目前从HPObjectsetterName替换到了HPKVONotifying_ HPObjecthp_setter函数中。

hp_setter主要逻辑分两部分:调用父类方法以及发送通知

static void hp_setter(id self,SEL _cmd,id newValue) {
//自动开关判断,省略
//1.调用父类的setter(也可以通过performSelector调用)
void (*hp_msgSendSuper)(void *,SEL , id) = (void *)objc_msgSendSuper;
struct objc_super super_struct = {
.receiver = self,
.super_class = class_getSuperclass(object_getClass(self)),
};
hp_msgSendSuper(&super_struct,_cmd,newValue);

//2.通知观察者
[observer hp_observeValueForKeyPath:getterForSetter(_cmd) ofObject:self change:@{} context:NULL];
}
  • 调用父类方法可以通过objc_msgSendSuper实现。
  • 通知观察者keypath可以通过_cmd转换获取,objectselfchange也可以获取到,context可以先不传。那么核心就是observer的获取。

通知观察者
首先想到的是用属性存储observer,那么有个问题在类已经创建后就无法添加了。所以关联属性明显更合适。在hp_addObserver中添加关联对象:

static NSString *const kHPKVOAssiociateKey = @"HPKVOAssiociateKey";

objc_setAssociatedObject(self, (__bridge const void * _Nonnull)(kHPKVOAssiociateKey), observer, OBJC_ASSOCIATION_RETAIN_NONATOMIC);

通知逻辑实现:

//2.通知观察者
id observer = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kHPKVOAssiociateKey));
[observer hp_observeValueForKeyPath:getterForSetter(NSStringFromSelector(_cmd)) ofObject:self change:@{@"kind":@1,@"new":newValue} context:NULL];

//获取getter
static NSString *getterForSetter(NSString *setter){
if (setter.length <= 0 || ![setter hasPrefix:@"set"] || ![setter hasSuffix:@":"]) { return nil;}
NSRange range = NSMakeRange(3, setter.length - 4);
NSString *getter = [setter substringWithRange:range];
NSString *firstString = [[getter substringToIndex:1] lowercaseString];
return [getter stringByReplacingCharactersInRange:NSMakeRange(0, 1) withString:firstString];
}

这个时候在hp_observeValueForKeyPath中就有回调了:

change:{
kind = 1;
new = HP;
}

1.1.5 观察者信息保存

上面的逻辑虽然简单实现了,但是存在一个严重问题,观察多个属性的时候以及新旧值都要观察以及传递了context的情况就无效了。
那么就需要保存观察者相关的信息,创建一个新类HPKVOInfo实现如下:

typedef NS_OPTIONS(NSUInteger, HPKeyValueObservingOptions) {
HPKeyValueObservingOptionNew = 0x01,
HPKeyValueObservingOptionOld = 0x02,
};

@interface HPKVOInfo : NSObject

@property (nonatomic, weak) id observer;
@property (nonatomic, copy) NSString *keyPath;
@property (nonatomic, assign) HPKeyValueObservingOptions options;
@property (nonatomic, strong) id context;

- (instancetype)initWitObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(HPKeyValueObservingOptions)options context:(nullable void *)context;

@end

@implementation HPKVOInfo

- (instancetype)initWitObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(HPKeyValueObservingOptions)options context:(nullable void *)context {
self = [super init];
if (self) {
self.observer = observer;
self.keyPath = keyPath;
self.options = options;
self.context = (__bridge id _Nonnull)(context);
}
return self;
}

@end

hp_addObserver中信息保存修改如下:

//保存观察者信息-数组
HPKVOInfo *kvoInfo = [[HPKVOInfo alloc] initWitObserver:observer forKeyPath:keyPath options:options context:context];
NSMutableArray *observerArray = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kHPKVOAssiociateKey));
if (!observerArray) {
observerArray = [NSMutableArray arrayWithCapacity:1];
}
[observerArray addObject:kvoInfo];
objc_setAssociatedObject(self, (__bridge const void * _Nonnull)(kHPKVOAssiociateKey), observerArray, OBJC_ASSOCIATION_RETAIN_NONATOMIC);

hp_setter逻辑修改如下:

static void hp_setter(id self,SEL _cmd,id newValue) {
//自动开关判断,省略
//保存旧值
NSString *keyPath = getterForSetter(NSStringFromSelector(_cmd));
id oldValue = [self valueForKey:keyPath];
//1.调用父类的setter(也可以通过performSelector调用)
void (*hp_msgSendSuper)(void *,SEL , id) = (void *)objc_msgSendSuper;
struct objc_super super_struct = {
.receiver = self,
.super_class = class_getSuperclass(object_getClass(self)),
};
hp_msgSendSuper(&super_struct,_cmd,newValue);

//2.通知观察者
NSMutableArray *observerArray = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kHPKVOAssiociateKey));
for (HPKVOInfo *info in observerArray) {//循环调用,可能添加多次。
if ([info.keyPath isEqualToString:keyPath]) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSMutableDictionary<NSKeyValueChangeKey,id> *change = [NSMutableDictionary dictionaryWithCapacity:1];
//对新旧值进行处理
if (info.options & HPKeyValueObservingOptionNew) {
[change setObject:newValue forKey:NSKeyValueChangeNewKey];
}
if (info.options & HPKeyValueObservingOptionOld) {
if (oldValue) {
[change setObject:oldValue forKey:NSKeyValueChangeOldKey];
} else {
[change setObject:@"" forKey:NSKeyValueChangeOldKey];
}
}
[change setObject:@1 forKey:@"kind"];
//消息发送给观察者
[info.observer hp_observeValueForKeyPath:keyPath ofObject:self change:change context:(__bridge void * _Nullable)(info.context)];
});
}
}
}
  • 在调用父类之前先获取旧值。
  • 取出关联对象数组数据,循环判断调用hp_observeValueForKeyPath通知观察者。

这个时候观察多个属性以及多次观察就都没问题了。

1.2 hp_removeObserver

观察者对象是保存在关联对象中,所以在移除的时候也需要删除关联对象,并且当没有观察者时就要回复isa指向了。

- (void)hp_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath {
[self hp_removeObserver:observer forKeyPath:keyPath context:NULL];
}

- (void)hp_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(void *)context {
NSMutableArray *observerArray = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kHPKVOAssiociateKey));
if (observerArray.count <= 0) {
return;
}

NSMutableArray *tempArray = [observerArray mutableCopy];
for (HPKVOInfo *info in tempArray) {
if ([info.keyPath isEqualToString:keyPath]) {
if (info.observer) {
if (info.observer == observer) {
if (context != NULL) {
if (info.context == context) {
[observerArray removeObject:info];
}
} else {
[observerArray removeObject:info];
}
}
} else {
if (context != NULL) {
if (info.context == context) {
[observerArray removeObject:info];
}
} else {
[observerArray removeObject:info];
}
}
}
}
objc_setAssociatedObject(self, (__bridge const void * _Nonnull)(kHPKVOAssiociateKey), observerArray, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
//已经全部移除了
if (observerArray.count <= 0) {
//isa指回给父类
Class superClass = [self class];
object_setClass(self, superClass);
}
}
  • 通过keyPath以及observercontext确定要移除的关联对象数据。
  • 当关联对象中没有数据的时候isa进行指回。

完整代码如下:

@interface NSObject (HP_KVO)

- (void)hp_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(HPKeyValueObservingOptions)options context:(nullable void *)context;
- (void)hp_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(nullable void *)context;
- (void)hp_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;

- (void)hp_observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change context:(nullable void *)context;

@end

#import "NSObject+HP_KVO.h"
#import
#import

static NSString *const kHPKVOClassPrefix = @"HPKVONotifying_";
static NSString *const kHPKVOAssiociateKey = @"HPKVOAssiociateKey";

@implementation NSObject (HP_KVO)

- (void)hp_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(HPKeyValueObservingOptions)options context:(nullable void *)context {
//1.参数判断 以及 setter检查
if (!observer || !keyPath) return;
BOOL result = [self handleSetterMethodFromKeyPath:keyPath];
if (!result) return;

//2.isa_swizzle 申请类-注册类-添加方法
Class newClass = [self creatKVOClassWithKeyPath:keyPath];

//3.isa 指向子类
object_setClass(self, newClass);
//4.setter逻辑处理
//保存观察者信息-数组
HPKVOInfo *kvoInfo = [[HPKVOInfo alloc] initWitObserver:observer forKeyPath:keyPath options:options context:context];
NSMutableArray *observerArray = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kHPKVOAssiociateKey));
if (!observerArray) {
observerArray = [NSMutableArray arrayWithCapacity:1];
}
[observerArray addObject:kvoInfo];
objc_setAssociatedObject(self, (__bridge const void * _Nonnull)(kHPKVOAssiociateKey), observerArray, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (void)hp_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath {
[self hp_removeObserver:observer forKeyPath:keyPath context:NULL];
}

- (void)hp_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(void *)context {
NSMutableArray *observerArray = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kHPKVOAssiociateKey));
if (observerArray.count <= 0) {
return;
}

NSMutableArray *tempArray = [observerArray mutableCopy];
for (HPKVOInfo *info in tempArray) {
if ([info.keyPath isEqualToString:keyPath]) {
if (info.observer) {
if (info.observer == observer) {
if (context != NULL) {
if (info.context == context) {
[observerArray removeObject:info];
}
} else {
[observerArray removeObject:info];
}
}
} else {
if (context != NULL) {
if (info.context == context) {
[observerArray removeObject:info];
}
} else {
[observerArray removeObject:info];
}
}
}
}
objc_setAssociatedObject(self, (__bridge const void * _Nonnull)(kHPKVOAssiociateKey), observerArray, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
//已经全部移除了
if (observerArray.count <= 0) {
//isa指回给父类
Class superClass = [self class];
object_setClass(self, superClass);
}
}

- (BOOL)handleSetterMethodFromKeyPath:(NSString *)keyPath {
SEL setterSeletor = NSSelectorFromString(setterForGetter(keyPath));
Method setterMethod = class_getInstanceMethod(object_getClass(self), setterSeletor);
NSAssert(setterMethod, @"%@ setter is not exist",keyPath);
return setterMethod ? YES : NO;
}

// 从get方法获取set方法的名称 key -> setKey
static NSString *setterForGetter(NSString *getter) {
if (getter.length <= 0) { return nil;}
NSString *firstString = [[getter substringToIndex:1] uppercaseString];
NSString *otherString = [getter substringFromIndex:1];
return [NSString stringWithFormat:@"set%@%@:",firstString,otherString];
}

//申请类-注册类-添加方法
- (Class)creatKVOClassWithKeyPath:(NSString *)keyPath {
//这里重写class后kvo子类也返回的是父类的名字
NSString *superClassName = NSStringFromClass([self class]);
NSString *newClassName = [NSString stringWithFormat:@"%@%@",kHPKVOClassPrefix,superClassName];
Class newClass = NSClassFromString(newClassName);
//类是否存在
if (!newClass) {//不存在需要创建类
//1:申请类 父类、新类名称、额外空间
newClass = objc_allocateClassPair([self class], newClassName.UTF8String, 0);
//2:注册类
objc_registerClassPair(newClass);
//3:添加class方法,class返回父类信息 这里是`-class`
SEL classSEL = NSSelectorFromString(@"class");
Method classMethod = class_getInstanceMethod([self class], classSEL);
const char *classTypes = method_getTypeEncoding(classMethod);
class_addMethod(newClass, classSEL, (IMP)hp_class, classTypes);
}
//4:添加setter方法
SEL setterSEL = NSSelectorFromString(setterForGetter(keyPath));
Method setterMethod = class_getInstanceMethod([self class], setterSEL);
const char *setterTypes = method_getTypeEncoding(setterMethod);
class_addMethod(newClass, setterSEL, (IMP)hp_setter, setterTypes);

return newClass;
}

//返回父类信息
Class hp_class(id self,SEL _cmd) {
return class_getSuperclass(object_getClass(self));
}

static void hp_setter(id self,SEL _cmd,id newValue) {
//自动开关判断,省略
//保存旧值
NSString *keyPath = getterForSetter(NSStringFromSelector(_cmd));
id oldValue = [self valueForKey:keyPath];
//1.调用父类的setter(也可以通过performSelector调用)
void (*hp_msgSendSuper)(void *,SEL , id) = (void *)objc_msgSendSuper;
struct objc_super super_struct = {
.receiver = self,
.super_class = class_getSuperclass(object_getClass(self)),
};
hp_msgSendSuper(&super_struct,_cmd,newValue);

//2.通知观察者
NSMutableArray *observerArray = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kHPKVOAssiociateKey));
for (HPKVOInfo *info in observerArray) {//循环调用,可能添加多次。
if ([info.keyPath isEqualToString:keyPath]) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSMutableDictionary<NSKeyValueChangeKey,id> *change = [NSMutableDictionary dictionaryWithCapacity:1];
//对新旧值进行处理
if (info.options & HPKeyValueObservingOptionNew) {
[change setObject:newValue forKey:NSKeyValueChangeNewKey];
}
if (info.options & HPKeyValueObservingOptionOld) {
if (oldValue) {
[change setObject:oldValue forKey:NSKeyValueChangeOldKey];
} else {
[change setObject:@"" forKey:NSKeyValueChangeOldKey];
}
}
[change setObject:@1 forKey:@"kind"];
//消息发送给观察者
[info.observer hp_observeValueForKeyPath:keyPath ofObject:self change:change context:(__bridge void * _Nullable)(info.context)];
});
}
}
}

//获取getter
static NSString *getterForSetter(NSString *setter){
if (setter.length <= 0 || ![setter hasPrefix:@"set"] || ![setter hasSuffix:@":"]) { return nil;}
NSRange range = NSMakeRange(3, setter.length - 4);
NSString *getter = [setter substringWithRange:range];
NSString *firstString = [[getter substringToIndex:1] lowercaseString];
return [getter stringByReplacingCharactersInRange:NSMakeRange(0, 1) withString:firstString];
}

@end

二、kvo函数式编程

上面的自定义自定义kvo与系统的kvo实现都有一个问题,都需要三步曲。代码是分离的可读性并不好。

2.1 注册与回调绑定

可以定义一个block用来处理回调,这样就不需要回调方法了,注册和回调就可以在一起处理了。
直接修改注册方法为block实现:

typedef void(^HPKVOBlock)(id observer,NSString *keyPath,id oldValue,id newValue);

- (void)hp_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath block:(HPKVOBlock)block {
……
//保存观察者信息-数组
HPKVOBlockInfo *kvoInfo = [[HPKVOBlockInfo alloc] initWitObserver:observer forKeyPath:keyPath handleBlock:block];
……
}
  • block实现也保存在HPKVOBlockInfo中,这样在回调的时候直接执行block实现就可以了。

修改回调逻辑:

NSMutableArray *observerArray = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kHPBlockKVOAssiociateKey));
for (HPKVOBlockInfo *info in observerArray) {//循环调用,可能添加多次。
if ([info.keyPath isEqualToString:keyPath] && info.handleBlock) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
info.handleBlock(info.observer, keyPath, oldValue, newValue);
});
}
}

  • 在回调的时候直接将新值与旧值一起返回。

注册调用逻辑:

[self.obj hp_addObserver:self forKeyPath:@"name" block:^(id  _Nonnull observer, NSString * _Nonnull keyPath, id  _Nonnull oldValue, id  _Nonnull newValue) {
NSLog(@"block: oldValue:%@,newValue:%@",oldValue,newValue);
}];

这样就替换了回调函数为block实现了,注册和回调逻辑在一起了。

2.2 kvo自动销毁

上面虽然实现了注册和回调绑定,但是在观察者dealloc的时候仍然需要remove
那么怎么能自动释放不需要主动调用呢?

removeObserver的过程中主要做了两件事,移除关联对象数组中的数据以及指回isa。关联对象不移除的后果是会继续调用回调,那么在调用的时候判断下observer存不存在来处理是否回调就可以了。核心就在指回isa了。

2.2.1 Hook dealloc

首先想到的就是Hook dealloc方法:

+ (void)hp_methodSwizzleWithClass:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swizzledSEL isClassMethod:(BOOL)isClassMethod {
if (!cls) {
NSLog(@"class is nil");
return;
}
if (!swizzledSEL) {
NSLog(@"swizzledSEL is nil");
return;
}
//类/元类
Class swizzleClass = isClassMethod ? object_getClass(cls) : cls;
Method oriMethod = class_getInstanceMethod(swizzleClass, oriSEL);
Method swiMethod = class_getInstanceMethod(swizzleClass, swizzledSEL);
if (!oriMethod) {//原始方法没有实现
// 在oriMethod为nil时,替换后将swizzledSEL复制一个空实现
class_addMethod(swizzleClass, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
//添加一个空的实现
method_setImplementation(swiMethod, imp_implementationWithBlock(^(id self, SEL _cmd){
NSLog(@"imp default null implementation");
}));
}
//自己没有则会添加成功,自己有添加失败
BOOL success = class_addMethod(swizzleClass, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(oriMethod));
if (success) {//自己没有方法添加一个,添加成功则证明自己没有。
class_replaceMethod(swizzleClass, swizzledSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
} else { //自己有直接进行交换
method_exchangeImplementations(oriMethod, swiMethod);
}
}

+ (void)load {
[self hp_methodSwizzleWithClass:[self class] oriSEL:NSSelectorFromString(@"dealloc") swizzledSEL:@selector(hp_dealloc) isClassMethod:NO];
}

- (void)hp_dealloc {
// [self.obj hp_removeObserver:self forKeyPath:@""];
[self hp_dealloc];
}
hp_dealloc中调用hp_removeObserver移除观察者。这里有个问题是被观察者和keypath从哪里来?这里相当于是观察者的dealloc中调用。所以可以通过在注册的时候对观察者添加关联对象保存被观察者和keyPath

static NSString *const kHPBlockKVOObserverdAssiociateKey = @"HPKVOObserverdAssiociateKey";

@interface HPKVOObservedInfo : NSObject

@property (nonatomic, weak) id observerd;
@property (nonatomic, copy) NSString *keyPath;

@end

@implementation HPKVOObservedInfo

- (instancetype)initWitObserverd:(NSObject *)observerd forKeyPath:(NSString *)keyPath {
if (self=[super init]) {
_observerd = observerd;
_keyPath = keyPath;
}
return self;
}

@end


- (void)hp_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath block:(HPKVOBlock)block {
……
//保存被观察者信息
HPKVOObservedInfo *kvoObservedInfo = [[HPKVOObservedInfo alloc] initWitObserverd:self forKeyPath:keyPath];
NSMutableArray *observerdArray = objc_getAssociatedObject(observer, (__bridge const void * _Nonnull)(kHPBlockKVOObserverdAssiociateKey));
if (!observerdArray) {
observerdArray = [NSMutableArray arrayWithCapacity:1];
}
[observerdArray addObject:kvoObservedInfo];
objc_setAssociatedObject(observer, (__bridge const void * _Nonnull)(kHPBlockKVOObserverdAssiociateKey), observerdArray, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

  • kvoObservedInfo中保存的是self也就是被观察者。
  • 关联对象关联在observer也就是观察者身上。

这个时候在dealloc中遍历对其进行移除:

- (void)hp_dealloc {
NSMutableArray *observerdArray = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kHPBlockKVOObserverdAssiociateKey));
for (HPKVOObservedInfo *info in observerdArray) {
if (info.observerd) {
[info.observerd hp_removeObserver:self forKeyPath:info.keyPath];
}
}
[self hp_dealloc];
}

当然这里的方法执行只针对被观察者没有释放的情况,释放了observerd就不存在了不需要调用remove逻辑了。

篇幅有限 下片继续



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








收起阅读 »

Android 11源码分析:从Activity的setContent方法看渲染流初识Window

在上一篇的分析中,我们已经知道DecorView以下的部分弄的很明白了,但是对于DecorView是如何显示在我们的屏幕上还是不太清楚。所以接着分析DecorView与PhoneWindow与Activity具体是如何建立联系的。 我们先弄清楚两个问题:Dec...
继续阅读 »

在上一篇的分析中,我们已经知道DecorView以下的部分弄的很明白了,但是对于DecorView是如何显示在我们的屏幕上还是不太清楚。所以接着分析DecorView与PhoneWindow与Activity具体是如何建立联系的。 我们先弄清楚两个问题:

  1. DecorView何时绘制到屏幕中
  2. DecorView如何绘制到屏幕中(addView,removeView,upDateViewLayout)
  3. Activity是如何得到Touch事件的

DecorView何时绘制到屏幕中

Activity的启动流程中提到执行Activity的performLaunchActivity方法执行的时,先是通过反射创建了Activity,然后会调用Activity的attach方法,在上一篇我们知道attach方法做了PhoneWindow的初始化操作,再然后才是执行生命周期onCreate,而setContent方法又是在onCreate执行后,看起来一切都很合理。

先创建一个Activity,然后再为这个Activity创建一个PhoneWindow,执行onCreate,最后再把我们写的XML设置进去。

一切都准备就绪,但是我们到目前还是看不到界面,因为我们知道,Activity真正可见是在执行onResume的时候。所以对于DecorView何时绘制到屏幕中问题,其实答案已经出来了,接下来需要去代码里进行验证。

看过我之前文章的人知道找Activity不同生命周期具体执行代码要去TransactionExecutor里看performLifecycleSequence方法。

Activity的onRemune具体执行是在Activity的handleResumeActivity方法中,我们去看看这里是怎么处理PhoneWindow的。

android.app.ActivityThread

@Override
public void handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward,
String reason) {
......
// TODO Push resumeArgs into the activity for consideration
performResumeActivity内会触发Activity的onResume生命周期
final ActivityClientRecord r = performResumeActivity(token, finalStateRequest, reason);
......
r.window = r.activity.getWindow();
//获取到当前PhoneWindow的DecorView
View decor = r.window.getDecorView();
decor.setVisibility(View.INVISIBLE);
ViewManager wm = a.getWindowManager();
......
关键代码
wm.addView(decor, l);
......
}

关键代码只有简单的一句wm.addView(decor, l);

找出WindowManagerImpl

这个wm是ViewManager类型,然后获取他的方法是Activity的WindowManager。先来搞清楚一下WindowManager和ViewManager是什么关系。

假装分析一波,能用ViewManager去接收WindowManager,说明WindowManager要么是ViewManager的子类,要么是实现了ViewManager接口。而且会使用wm.addView。就说明这个addView方法ViewManager里被定义了,在WindowManager中没有,否则不会这么去写。

其实也不用想这么多,各自点进去看看就知道了。

android.view.ViewManager

public interface ViewManager
{
public void addView(View view, ViewGroup.LayoutParams params);
public void updateViewLayout(View view, ViewGroup.LayoutParams params);
public void removeView(View view);
}

android.view.WindowManager
@SystemService(Context.WINDOW_SERVICE)
public interface WindowManager extends ViewManager {
......
}

ViewManager是个接口,里面虽然只有三个方法,但是我们却非常熟悉,这三个方法的重要性就不用多说了。

让我没想到的是WindowManager居然也是个接口......那addView具体的实现在哪呢。只能去getWindowManager具体找找了。

android.app.Activity
......
private WindowManager mWindowManager;
......
public WindowManager getWindowManager() {
return mWindowManager;
}

而的赋值操作在attach里

   final void attach(......){
mWindow = new PhoneWindow(this, window, activityConfigCallback);
mWindow.setWindowControllerCallback(mWindowControllerCallback);
mWindow.setCallback(this);
mWindow.setOnWindowDismissedCallback(this);
mWindow.getLayoutInflater().setPrivateFactory(this);
......
给PhoneWindow设置一个WindowManager
mWindow.setWindowManager((WindowManager)context.getSystemService(Context.WINDOW_SERVICE),
mToken, mComponent.flattenToString(),
(info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);
if (mParent != null) {
mWindow.setContainer(mParent.getWindow());
}
取出WindowManager来赋值给mWindowManager
mWindowManager = mWindow.getWindowManager();
}

所以想知道mWindowManager到底是啥还得看mWindow.getWindowManager。而mWindow.getWindowManager返回的又是上面的一行mWindow.setWindowManager,所以去setWindowManager看看吧

android.view.Window

......
private WindowManager mWindowManager;
......
public void setWindowManager(WindowManager wm, IBinder appToken, String appName,
boolean hardwareAccelerated) {
mAppToken = appToken;
mAppName = appName;
mHardwareAccelerated = hardwareAccelerated;
if (wm == null) {
wm = (WindowManager)mContext.getSystemService(Context.WINDOW_SERVICE);
}
关键代码
mWindowManager = ((WindowManagerImpl)wm).createLocalWindowManager(this);
}
......
public WindowManager getWindowManager() {
return mWindowManager;
}

真相浮出水面

mWindowManager的真实对象是WindowManagerImpl

addView

这一小节我们来看看Window的添加过程

找了这么久我们就是为了弄明白wm.addView(decor, l);的wm到底是个啥。下面去看看addView到底把我们的DecorView怎么了。

android.view.WindowManagerImpl

public final class WindowManagerImpl implements WindowManager {
@UnsupportedAppUsage
private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();
......
@Override
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
applyDefaultToken(params);
mGlobal.addView(view, params, mContext.getDisplayNoVerify(), mParentWindow,
mContext.getUserId());
}
......
}

WindowManagerImpl居然又是个空壳,大部分操作都给了单例类WindowManagerGlobal去处理,

android.view.WindowManagerGlobal

private final ArrayList<View> mViews = new ArrayList<View>();

private final ArrayList<ViewRootImpl> mRoots = new ArrayList<ViewRootImpl>();

private final ArrayList<WindowManager.LayoutParams> mParams =
new ArrayList<WindowManager.LayoutParams>();
private final ArraySet<View> mDyingViews = new ArraySet<View>();

public void addView(View view, ViewGroup.LayoutParams params,Display display,
Window parentWindow, int userId) {
......
ViewRootImpl root;
View panelParentView = null;
synchronized (mLock) {
进行参数检查
if (view == null) {
throw new IllegalArgumentException("view must not be null");
}
if (display == null) {
throw new IllegalArgumentException("display must not be null");
}
if (!(params instanceof WindowManager.LayoutParams)) {
throw new IllegalArgumentException("Params must be WindowManager.LayoutParams");
}

final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams) params;
如果是子Windowh还需要进行布局参数的调整
if (parentWindow != null) {
parentWindow.adjustLayoutParamsForSubWindow(wparams);
}
......
创建ViewRootImpl
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 {
//把DecorView交给ViewRootImpl
root.setView(view, wparams, panelParentView, userId);
} catch (RuntimeException e) {
// BadTokenException or InvalidDisplayException, clean up.
if (index >= 0) {
removeViewLocked(index, true);
}
throw e;
}
}
}

首先我们看到WindowManagerGlobal维护了4个集合

  • mViews :存储了所有Window所对应的View

  • mRoots :存储的是所以Window所对应的ViewRootImpl

  • mParams:存储的是所有Window所对应的布局参数

  • mDyingViews:存储的是那些正在被删除的View

在addView中后面的关键代码创建了ViewRootImpl,并将我们的DecorView交给了它。

android.view.ViewRootImpl

public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView,
int userId) {
synchronized (this) {
if (mView == null) {
mView = view;
}
......
触发一次屏幕刷新
requestLayout();
try {
将最后的操作给mWindowSession
res = mWindowSession.addToDisplayAsUser(mWindow, mSeq, mWindowAttributes,
getHostVisibility(), mDisplay.getDisplayId(), userId, mTmpFrame,
mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
mAttachInfo.mDisplayCutout, inputChannel,
mTempInsets, mTempControls);
setFrame(mTmpFrame);
} ......
// Set up the input pipeline.
输入事件处理
CharSequence counterSuffix = attrs.getTitle();
mSyntheticInputStage = new SyntheticInputStage();
InputStage viewPostImeStage = new ViewPostImeInputStage(mSyntheticInputStage);
InputStage nativePostImeStage = new NativePostImeInputStage(viewPostImeStage,
"aq:native-post-ime:" + counterSuffix);
InputStage earlyPostImeStage = new EarlyPostImeInputStage(nativePostImeStage);
InputStage imeStage = new ImeInputStage(earlyPostImeStage,
"aq:ime:" + counterSuffix);
InputStage viewPreImeStage = new ViewPreImeInputStage(imeStage);
InputStage nativePreImeStage = new NativePreImeInputStage(viewPreImeStage,
"aq:native-pre-ime:" + counterSuffix);
......
}
@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
scheduleTraversals();
}
}

setView方法先是做了个刷新布局的操作,内部执行的scheduleTraversals() 就是View绘制的入口,调用此方法后 ViewRootImpl 所关联的 View 也执行 measure - layout - draw 操作,确保在 View 被添加到 Window 上显示到屏幕之前,已经完成测量和绘制操作。至此由ViewRootImpl完成了添加View到Window的操作。 然后将调用mWindowSession的addToDisplayAsUser方法来完成Window的添加过程,内部真实执行的地方在WMS(WindowManagerService)

View的事件如何反馈到Activity

对于后面的输入事件处理的代码我也不是很明白,但是之前好像在哪里看过,所有对于利用管道和系统底层通信的机制有点印象。 这一块的简单的理解就是:这里是设置了一系列的输入通道。因为一个触屏事件的发生是肯定是由屏幕发起,再经过驱动层一系列的计算处理最后通过 Socket 跨进程通知 Android Framework 层。 其实看到这一块我一开始是不打算继续深入了,因为的也知道我的能力差不多就这了,但是抱着好奇的心点了进去想看看ViewPostImeInputStage是个啥。结果却有了意想不到的收获。

android.view.ViewRootImpl
final class ViewPostImeInputStage extends InputStage {
public ViewPostImeInputStage(InputStage next) {
super(next);
}

@Override
protected int onProcess(QueuedInputEvent q) {
if (q.mEvent instanceof KeyEvent) {
return processKeyEvent(q);
} else {
final int source = q.mEvent.getSource();
if ((source & InputDevice.SOURCE_CLASS_POINTER) != 0) {
只看这里,其实下面的两个都是类似
return processPointerEvent(q);
} else if ((source & InputDevice.SOURCE_CLASS_TRACKBALL) != 0) {
return processTrackballEvent(q);
} else {
return processGenericMotionEvent(q);
}
}
}
}
private int processPointerEvent(QueuedInputEvent q) {
final MotionEvent event = (MotionEvent)q.mEvent;

mAttachInfo.mUnbufferedDispatchRequested = false;
mAttachInfo.mHandlingPointerEvent = true;
boolean handled = mView.dispatchPointerEvent(event);
......
return handled ? FINISH_HANDLED : FORWARD;
}

注意一下参数的传递就知道这个mView其实就是一开始我们DecorView。但是dispatchPointerEvent是在View定义的。

android.view.View
public final boolean dispatchPointerEvent(MotionEvent event) {
if (event.isTouchEvent()) {
return dispatchTouchEvent(event);
} else {
return dispatchGenericMotionEvent(event);
}
}

返回了dispatchTouchEvent和dispatchGenericMotionEvent的执行。所以去DecorView看看

com.android.internal.policy.DecorView

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
final Window.Callback cb = mWindow.getCallback();
return cb != null && !mWindow.isDestroyed() && mFeatureId < 0
? cb.dispatchTouchEvent(ev) : super.dispatchTouchEvent(ev);
}

@Override
public boolean dispatchTrackballEvent(MotionEvent ev) {
final Window.Callback cb = mWindow.getCallback();
return cb != null && !mWindow.isDestroyed() && mFeatureId < 0
? cb.dispatchTrackballEvent(ev) : super.dispatchTrackballEvent(ev);
}

@Override
public boolean dispatchGenericMotionEvent(MotionEvent ev) {
final Window.Callback cb = mWindow.getCallback();
return cb != null && !mWindow.isDestroyed() && mFeatureId < 0
? cb.dispatchGenericMotionEvent(ev) : super.dispatchGenericMotionEvent(ev);
}

我们发现不管是哪个方法,最后都是从mWindow获取一个回调cb,然后把事回调出去。不知道读者对之前在Activity的attach里的时候的代码 mWindow.setCallback(this);还有没有印象,方法接收的参数类型是Window内部的一个接口Callback而我们的Activity实现了这个接口。所以mWindow.setCallback就是把Activity设置了进去,所以这个cb就是我们的Activity,所以DecorView的事件,就这样传递给了Activity

removeView

android.view.WindowManagerGlobal

@UnsupportedAppUsage
public void removeView(View view, boolean immediate) {
if (view == null) {
throw new IllegalArgumentException("view must not be null");
}

synchronized (mLock) {
找到需要删除View的索引
int index = findViewLocked(view, true);
View curView = mRoots.get(index).getView();
真的执行删除操作
removeViewLocked(index, immediate);
if (curView == view) {
return;
}

throw new IllegalStateException("Calling with view " + view
+ " but the ViewAncestor is attached to " + curView);
}
}

Window的删除过程页是在WindowManagerGlobal中。先是找到需要删除View的索引,然后传递到removeViewLocked方法里。

   private void removeViewLocked(int index, boolean immediate) {
ViewRootImpl root = mRoots.get(index);
View view = root.getView();

if (root != null) {
root.getImeFocusController().onWindowDismissed();
}
boolean deferred = root.die(immediate);
if (view != null) {
view.assignParent(null);
if (deferred) {
mDyingViews.add(view);
}
}
}

removeViewLocked内部也是通过ViewRootImpl來完成刪除操作的。 在removeView方法里我们就看到了mRoots.get(index).getView(),里面又有View view = root.getView();ViewRootImpl的getView返回的View其实就是上面setView方法里,传进来的我们的顶层视图DecorView。

看看ViewRootImpl的die方法

Params:immediate – True, do now if not in traversal. False, put on queue and do later.
Returns:True, request has been queued. False, request has been completed.
boolean die(boolean immediate) {
// Make sure we do execute immediately if we are in the middle of a traversal or the damage
// done by dispatchDetachedFromWindow will cause havoc on return.
if (immediate && !mIsInTraversal) {
doDie();
return false;
}

if (!mIsDrawing) {
destroyHardwareRenderer();
} else {
Log.e(mTag, "Attempting to destroy the window while drawing!\n" +
" window=" + this + ", title=" + mWindowAttributes.getTitle());
}
mHandler.sendEmptyMessage(MSG_DIE);
return true;
}

参数immediates表示是否立即删除,返回false表示删除已完成,返回true表示加入待删除的队列里。看removeViewLocked的最后,将返回true加入了待删除的列表mDyingViews就明白了。 如果是需要立即删除则执行doDie,如果是是异步删除,则发送个消息,ViewRootImpl内部Handle接收消息的处理还是执行doDie。

现在看来doDie才是真正执行删除操作的地方。

void doDie() {
checkThread();
if (LOCAL_LOGV) Log.v(mTag, "DIE in " + this + " of " + mSurface);
synchronized (this) {
if (mRemoved) {
return;
}
mRemoved = true;
if (mAdded) {
关键代码
dispatchDetachedFromWindow();
}
......
全局单例WindowManagerGlobal也执行对应方法
WindowManagerGlobal.getInstance().doRemoveView(this);
}
void dispatchDetachedFromWindow() {
内部完成视图的移除
mView.dispatchDetachedFromWindow();
try {
内部通过WMA完成Window的删除
mWindowSession.remove(mWindow);
} catch (RemoteException e) {}
}

在dispatchDetachedFromWindow方法内部通过View的dispatchDetachedFromWindow方法完成View的删除,同时在通过mWindowSession来完成Window的删除

updateViewLayout

android.view.WindowManagerGlobal
public void updateViewLayout(View view, ViewGroup.LayoutParams params) {
if (view == null) {
throw new IllegalArgumentException("view must not be null");
}
if (!(params instanceof WindowManager.LayoutParams)) {
throw new IllegalArgumentException("Params must be WindowManager.LayoutParams");
}

final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams)params;
更新View参数
view.setLayoutParams(wparams);

synchronized (mLock) {
int index = findViewLocked(view, true);
ViewRootImpl root = mRoots.get(index);
mParams.remove(index);
mParams.add(index, wparams);
更新ViewRootImpl参数
root.setLayoutParams(wparams, false);
}
}

updateViewLayout内部做的事情比较简单,先是更新View的参数,然后更新ViewRootImpl的参数。

  • setLayoutParams内部会触发scheduleTraversals来对View重新布局,scheduleTraversals一旦触发,就会执行relayoutWindow方法,触发WMS来更新Window视图。我就不贴代码了,太多了,附上调用路径吧。
  • scheduleTraversals->mTraversalRunnable(TraversalRunnable)->doTraversal->performTraversals->relayoutWindow->内部执行mWindowSession.relayout通知WMS更新Window

总结

  1. DecorView在Activity的handleResumeActivity方法执行通过wm.addView完成操作。执行时机是在onResume后面一点点
  2. DecorView存在于PhoneWindow中,DecorView的添加删除更新曹组是由ViewRootImpl负责,而PhoneWindow的添加删除更新操作由WMS负责
  3. PhoneWindow的事件通过Callback接口回调给Activity
收起阅读 »

[Android翻译]解除对WindowManager的束缚

原文地址:medium.com/androiddeve…原文作者:medium.com/@pmaggi发布时间:2021年8月20日 - 6分钟阅读为可折叠设备和大屏幕设备优化应用程序Android的屏幕尺寸正在迅速变化,随着平板电脑和可折叠设备的不断普及,了...
继续阅读 »

原文地址:medium.com/androiddeve…

原文作者:medium.com/@pmaggi

发布时间:2021年8月20日 - 6分钟阅读

为可折叠设备和大屏幕设备优化应用程序

Android的屏幕尺寸正在迅速变化,随着平板电脑和可折叠设备的不断普及,了解你的应用程序的窗口尺寸和状态对于开发一个响应式的UI至关重要。Jetpack WindowManager现在处于测试阶段,它是一个库和API,提供类似于Android框架WindowManager的功能,包括对响应式UI的支持、检测屏幕变化的回调适配器以及窗口测试API。但Jetpack WindowManager还提供了对新型设备的支持,如可折叠设备和Chrome OS等窗口环境。

新的WindowManager APIs包括以下内容。

  • WindowLayoutInfo:包含了一个窗口的显示特征,例如窗口是否包含了折叠或铰链
  • FoldingFeature:使你能够监测可折叠设备的折叠状态,以确定设备的姿势
  • WindowMetrics:提供当前窗口的指标或整体显示的指标

Jetpack WindowManager与安卓系统没有捆绑,允许更快地迭代API,以快速支持快速发展的设备市场,并使应用程序开发人员能够采用库的更新,而不必等待最新的安卓版本。

现在该库已经进入测试阶段,我们鼓励所有的开发者采用Jetpack WindowManager,它具有设备无关的API,测试API,以及带来WindowMetrics,使你能够轻松应对窗口尺寸的变化。逐步过渡到测试版意味着你可以对你所采用的API有信心,使你可以完全专注于在这些设备上建立令人兴奋的体验。Jetpack WindowManager支持低至API 14的功能检测。

该库

Jetpack WindowManager是一个现代的、以Kotlin为首的库,它支持新的设备形态因素,并提供 "类似AppCompat "的功能,以构建具有响应式用户界面的应用程序。

折叠状态

这个库所提供的最明显的功能是对可折叠设备的支持。当设备的折叠状态发生变化时,应用程序可以接收事件,允许更新用户界面以支持新的用户互动。

1.gif

三星Galaxy Z Fold2上的Google Duo

请看这个Google Duo案例研究,它介绍了如何为可折叠设备添加支持。

有两种可能的折叠状态:平面半开放。对于FLAT,你可以认为表面是完全平坦地打开的,尽管在某些情况下它可能被铰链分割。对于HALF_OPENED,窗口至少有两个逻辑区域。下面,我们有图片说明每种状态的可能性。

image.png

折叠状态。平坦和半开放

当应用程序处于活动状态时,应用程序可以通过收集Kotlin流的事件来接收关于折叠状态变化的信息。 为了开始和停止事件收集,我们可以使用一个生命周期范围,正如 repeatOnLifeCycle API设计故事博文和下面的代码示例中所解释的。

lifecycleScope.launch(Dispatchers.Main) {
// The block passed to repeatOnLifecycle is executed when the lifecycle
// is at least STARTED and is cancelled when the lifecycle is STOPPED.
// It automatically restarts the block when the lifecycle is STARTED again.
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
// Safely collects from windowInfoRepository when the lifecycle is STARTED
// and stops collection when the lifecycle is STOPPED.
windowInfoRepository.windowLayoutInfo
.collect { newLayoutInfo ->
updateStateLog(newLayoutInfo)
updateCurrentState(newLayoutInfo)
}
}
}

然后,应用程序可以使用收到的WindowLayoutInfo对象中的可用信息,在应用程序对用户可见时更新其布局。

FoldingFeature包括铰链方向和折叠功能是否创建两个逻辑屏幕区域(isSeparating属性)等信息。我们可以使用这些值来检查设备是否处于桌面模式(半开,铰链水平)。

image.png

设备处于TableTop模式

private fun isTableTopMode(foldFeature: FoldingFeature) =
foldFeature.isSeparating &&
foldFeature.orientation == FoldingFeature.Orientation.HORIZONTAL

或处于书本模式(半开,铰链垂直)。

image.png

设备在书本模式下

private fun isBookMode(foldFeature: FoldingFeature) =
foldFeature.isSeparating &&
foldFeature.orientation == FoldingFeature.Orientation.VERTICAL

你可以在《可折叠设备上的桌面模式》一文中看到一个例子,说明如何为一个媒体播放器应用程序做到这一点。

注意:在主/UI线程上收集这些事件很重要,以避免UI和处理这些事件之间的同步问题。

对响应式UI的支持

由于安卓系统中的屏幕尺寸变化非常频繁,因此开始设计完全自适应和响应式的UI非常重要。WindowManager库中包含的另一个功能是能够检索当前和最大的窗口度量信息。这与API 30中包含的框架WindowMetrics API提供的信息类似,但它向后兼容到API 14。

Jetpack WindowManager提供了两种检索WindowMetrics信息的方式,作为流事件流或通过WindowMetricsCalculator类同步进行。

当在视图中写代码时,异步的API可能太难处理(比如onMeasure),就使用WindowMetricsCalculator。

val windowMetrics = 
WindowMetricsCalculator.getOrCreate().computeCurrentWindowMetrics(activity)

另一个用例是在测试中(见下面的测试)。

对于应用程序UI的更高层次的处理,使用WindowInfoRepository#currentWindowMetrics来获得库的通知,当有一个窗口大小的变化时,独立于这个变化是否触发了配置的变化。

下面是一个如何根据你的可用区域的大小来切换你的布局的例子。

// Create a new coroutine since repeatOnLifecycle is a suspend function
lifecycleScope.launch(Dispatchers.Main) {
// The block passed to repeatOnLifecycle is executed when the lifecycle
// is at least STARTED and is cancelled when the lifecycle is STOPPED.
// It automatically restarts the block when the lifecycle is STARTED again.
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
// Safely collect from currentWindowMetrics when the lifecycle is STARTED
// and stops collection when the lifecycle is STOPPED
windowInfoRepository.currentWindowMetrics
.collect { windowMetrics ->
val currentBounds = windowMetrics.bounds
Log.i(TAG, "New bounds: {$currentBounds}")
// We can update the layout if needed from here
}
}
}

回调适配器 要在Java编程语言中使用这个库,或者使用回调接口,请在你的应用程序中包含androidx.window:window-java依赖项。该工件提供了WindowInfoRepositoryCallbackAdapter,你可以用它来注册(和取消注册)一个回调,以接收设备姿态和窗口度量信息的更新。

public class SplitLayoutActivity extends AppCompatActivity {

private WindowInfoRepositoryCallbackAdapter windowInfoRepository;
private ActivitySplitLayoutBinding binding;
private final LayoutStateChangeCallback layoutStateChangeCallback =
new LayoutStateChangeCallback();

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

binding = ActivitySplitLayoutBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());

windowInfoRepository =
new WindowInfoRepositoryCallbackAdapter(WindowInfoRepository.getOrCreate(this));
}

@Override
protected void onStart() {
super.onStart();
windowInfoRepository.addWindowLayoutInfoListener(Runnable::run, layoutStateChangeCallback);
}

@Override
protected void onStop() {
super.onStop();
windowInfoRepository.removeWindowLayoutInfoListener(layoutStateChangeCallback);
}

class LayoutStateChangeCallback implements Consumer<WindowLayoutInfo> {
@Override
public void accept(WindowLayoutInfo windowLayoutInfo) {
binding.splitLayout.updateWindowLayout(windowLayoutInfo);
}
}
}

测试

我们从开发者那里听说,更强大的测试API对于维持长期支持至关重要。让我们来谈谈如何在正常设备上测试可折叠的姿势。

到目前为止,我们已经看到Jetpack WindowManager库在设备姿态发生变化时通知你的应用程序,这样你就可以修改应用程序的布局。

该库在androidx.window:window-testing中提供了WindowLayoutInfoPublisherRule,它使你可以在测试FoldingFeature的支持下发布WindowInfoLayout。

import androidx.window.testing.layout.FoldingFeature
import androidx.window.testing.layout.WindowLayoutInfoPublisherRule

我们可以用它来创建一个假的FoldingFeature,在我们的测试中使用。

val feature = FoldingFeature(
activity = activity,
center = center,
size = 0,
orientation = VERTICAL,
state = HALF_OPENED
)
val expected =
WindowLayoutInfo.Builder().setDisplayFeatures(listOf(feature)).build()

publisherRule.overrideWindowLayoutInfo(expected)

然后使用WindowLayoutInfoPublisherRule来发布它。

val publisherRule = WindowLayoutInfoPublisherRule()

publisherRule.overrideWindowLayoutInfo(expected)

最后一步是使用可用的Espresso匹配器检查我们正在测试的活动的布局是否符合预期。

下面是一个测试发布FoldingFeature的例子,它在屏幕中心有一个HALF_OPENED的垂直铰链。

@Test
fun testDeviceOpen_Vertical(): Unit = testScope.runBlockingTest {
activityRule.scenario.onActivity { activity ->
val feature = FoldingFeature(
activity = activity,
orientation = VERTICAL,
state = HALF_OPENED
)
val expected =
WindowLayoutInfo.Builder().setDisplayFeatures(listOf(feature)).build()

val value = testScope.async {
activity.windowInfoRepository().windowLayoutInfo.first()
}
publisherRule.overrideWindowLayoutInfo(expected)
runBlockingTest {
Assert.assertEquals(
expected,
value.await()
)
}
}

// Checks that start_layout is on the left of end_layout with a vertical folding feature.
// This requires to run the test on a big enough screen to fit both views on screen
onView(withId(R.id.start_layout))
.check(isCompletelyLeftOf(withId(R.id.end_layout)))
}

请看它的运行情况。代码样本

GitHub上的一个最新样本显示了如何使用Jetpack WindowManager库来检索显示姿势信息,从WindowLayoutInfo流中收集信息或通过WindowInfoRepositoryCallbackAdapter注册一个回调。

该样本还包括一些测试,可以在任何设备或模拟器上运行。

在你的应用程序中采用WindowManager

可折叠和双屏设备不再是实验性的或未来主义的--大的显示区域和额外的姿势具有被证实的用户价值,而且现在有更多的设备可以供你的用户使用。可折叠设备和双屏设备代表了智能手机的自然进化。对于安卓开发者来说,他们提供了进入一个正在增长的高端市场的机会,这也得益于设备制造商的重新关注。

我们去年推出了Jetpack WindowManager alpha01。从那时起,该库有了稳定的发展,针对早期的反馈有了一些很大的改进。该库现在已经接受了Android的Kotlin优先理念,从回调驱动的模型过渡到coroutines和flow。随着WindowManager现在处于测试阶段,该API已经稳定,我们强烈建议采用。 而更新并不限于此。我们计划为该库添加更多的功能,并将其发展成一个用于系统UI的非捆绑式AppCompat,使开发者能够在所有的Android设备上轻松实现现代的、响应式的UI。


收起阅读 »

Android组件化开发笔记

Modularization什么是组件化组件化就是将一个app拆分成不同的组件,每一个组件都是一个独立的module。组件化的意义组件化能降低耦合性,而耦合性低就能提高维护性。于此同时由于组件间是独立的,所以组件与组件间耦合性低,所以我们在团队开发的时候可以以...
继续阅读 »

Modularization

什么是组件化

组件化就是将一个app拆分成不同的组件,每一个组件都是一个独立的module。

组件化的意义

组件化能降低耦合性,而耦合性低就能提高维护性。

于此同时由于组件间是独立的,所以组件与组件间耦合性低,所以我们在团队开发的时候可以以组件为分割单位,这样就能提高开发效率。

如何进行组件化

组件化是依靠gradle实现的。所以不会gradle的得去学学基础语法。当然也可以不学,只是说看别人写的代码看的半懂不懂的。

第一步创建Module

起点是一个全新的Project

image-20210818192909793

创建Module有好几种方法。

  • 右击new Module

    image-20210818193028977

    这个得注意位置哦,不然new出来全在app包下不是很好,通常我们的module是和app平级的。也就是说在大project下面。

    在我的这个demo中就是Modularization下面

  • 点击File new一个Module

    image-20210818193107528

这里我创建了3个module,一个lib

image-20210818193618429

注意命名,module是module_模块名,lib是lib__库名称.

简单区分以下module和lib,lib就是不显示页面的模块,module就是一个页面模块的集合。

将版本信息配置到一个gradle文件中

Tips:
fileName :app_versions.gradle

//applicationIds
def appIds = [:]
appIds.module_main = "com.example.module_main"
appIds.module_one = "com.example.module_one"
appIds.module_two = "com.example.module_two"
appIds.app = "com.example.modularization"
ext.appIds = appIds

将该gradle文件配置到project的build.gradle中

// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
apply from: 'app_versions.gradle'
ext{
app_configs = "$rootDir/app_config.gradle"
}
repositories {
google()
mavenCentral()
}
dependencies {
classpath "com.android.tools.build:gradle:7.0.0"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.20"

// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}

task clean(type: Delete) {
delete rootProject.buildDir
}

配置gradle的编译插件

先在gradle.properties,加入一个变量判断是否是发行状态

组件化中有两种状态,一种是debug状态,一个是发行状态,

  • debug状态也就是开发阶段,这个阶段每一个模块都是一个独立的app,可以独立运行
  • release发行状态,这个状态下只有app模块可以独立运行,其他的模块都是lib,依托于app模块。

image-20210818194002652

然后创建了一个app_config.gradle文件

image-20210818194518257

编写代码使得module_XX能在lib和app中切换状态。

这里有一点需要注意我们在gradle.propergies虽然写了一个isRelease的bool变量但是其实gradle这里获取的是一个string,得用toBoolean()进行转化。

image-20210818194838453

if (isRelease.toBoolean()){
if (project.name != 'app') apply plugin: 'com.android.library'
else apply plugin: 'com.andorid.application'
}
else{
if (project.name.matches('module_.+') || project.name == 'app') apply plugin: 'com.android.application'
else if (project.name.matches('lib_.+')) apply plugin: 'com.android.library'
}

然后加入必要的依赖

apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt'

配置Manifest文件

android{
`````

sourceSets{
main{
if (isRelease.toBoolean()){
manifest.srcFile "src/main/AndroidManifest.xml"
}else {
if (project.name.matches('module_.+')){
manifest.srcFile "src/main/manifest/AndroidManifest.xml"
}else if (project.name.matches('lib_.+') || project.name == 'app'){
manifest.srcFile "src/main/AndroidManifest.xml"
}
}
}
}


`````
}

然后对module的manifest文件进行一点变动

在main文件夹下创建manifest文件夹,然后把debug状态的manifest的文件放进去。

image-20210818200716961

debug状态下的文件(这个状态下编译的文件是apk所以需要配置启动页和一些application的选项)

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.module_main">


<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.Modularization">

<activity
android:name=".MainMainActivity"
android:exported="true">

<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>

</manifest>

release状态下的manifest文件(这个状态下的编译文件是aar文件所以只需要注册一个activity,其余的都不需要)

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.module_main">


<application>
<activity
android:name=".MainMainActivity"
android:exported="true" />

</application>

</manifest>

其余的module_XX,模块也是按照这样改。

配置applicationId

由于application才有applicationId,所以lib是没有applicationId的,而module在application和module之间疯狂切换,说以是有必要进行设置的。

if (isRelease.toBoolean()) {
applicationId "com.example.modularization"
}else {
if (project.name.matches('module_.+') || project.name == 'app') {
applicationId appIds[project.name]
}
}

之后在project的build.gradle中加点变量方便其gradle文件访问app_config.gradle文件(注意要加等号‘=’)

buildscript {
ext{
app_config = "$rootDir/app_config.gradle"
}
repositories {
google()
mavenCentral()
}
dependencies {
classpath "com.android.tools.build:gradle:7.0.0"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.20"

// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}

module引用app_config.gradle

之前

plugins {
id 'com.android.application'
id 'kotlin-android'
}

android {
compileSdk 30

defaultConfig {
applicationId "com.example.module_main"
minSdk 21
targetSdk 30
versionCode 1
versionName "1.0"

testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}

buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
}

dependencies {

implementation 'androidx.core:core-ktx:1.3.2'
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'com.google.android.material:material:1.3.0'
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
testImplementation 'junit:junit:4.+'
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
}

之后

apply from: app_configs

dependencies {

implementation 'androidx.core:core-ktx:1.3.2'
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'com.google.android.material:material:1.3.0'
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
testImplementation 'junit:junit:4.+'
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
}

其余的lib和module都是这样

把android闭包下的一些属性进行抽离


def android_versions = [:]
android_versions.sdk_version = 30
android_versions.min_version = 21
android_versions.target_version = 30
android_versions.version_code = 1
android_versions.version_name = "1.0"

def kotlin_options = [:]
kotlin_options.jvm_target = '1.8'
android_versions.kotlin_options = kotlin_options

ext.android_versions = android_versions

app_config.gradle的内容

if (isRelease.toBoolean()){
if (project.name != 'app') apply plugin: 'com.android.library'
else apply plugin: 'com.andorid.application'
}
else{
if (project.name.matches('module_.+') || project.name == 'app') apply plugin: 'com.android.application'
else if (project.name.matches('lib_.+')) apply plugin: 'com.android.library'
}

apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt'

android {
compileSdk android_versions.sdk_version

defaultConfig {
if (isRelease.toBoolean()) {
applicationId "com.example.modularization"
}else {
if (project.name.matches('module_.+') || project.name == 'app') {
applicationId appIds[project.name]
}
}

minSdk android_versions.min_version
targetSdk android_versions.target_version
versionCode android_versions.version_code
versionName android_versions.version_name

testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}

sourceSets{
main{
if (isRelease.toBoolean()){
manifest.srcFile "src/main/AndroidManifest.xml"
}else {
if (project.name.matches('module_.+')){
manifest.srcFile "src/main/manifest/AndroidManifest.xml"
}else if (project.name.matches('lib_.+') || project.name == 'app'){
manifest.srcFile "src/main/AndroidManifest.xml"
}
}
}
}

buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = android_versions.kotlin_options.jvm_target
}
}
收起阅读 »

Android数据库高手秘籍,如何在Kotlin中更好地使用LitePal

前言 自从 LitePal 在 2.0.0 版本中全面支持了 Kotlin 之后,我也一直在思考如何让 LitePal 更好地融入和适配 Kotlin 语言,而不仅仅停留在简单的支持层面。 Kotlin 确实是一门非常出色的语言,里面有许多优秀的特性是在 ...
继续阅读 »

前言


自从 LitePal 在 2.0.0 版本中全面支持了 Kotlin 之后,我也一直在思考如何让 LitePal 更好地融入和适配 Kotlin 语言,而不仅仅停留在简单的支持层面。


Kotlin 确实是一门非常出色的语言,里面有许多优秀的特性是在 Java 中无法实现的。因此,在 LitePal 全面支持了 Kotlin 之后,我觉得如果我还视这些优秀特性而不见的话,就有些太暴殄天物了。所以在最新的 LitePal 3.0.0 版本里面,我准备让 LitePal 更加充分地利用 Kotlin 的一些语言特性,从而让我们的开发更加轻松。


本篇文章除了介绍 LitePal 3.0.0 版本的升级内容之外,还会讲解一些 Kotlin 方面的高级知识。


首先还是来看如何升级。


升级的方式


为什么这次的版本号跨度如此之大,直接从 2.0 升到了 3.0 呢?因为这次 LitePal 在结构上面有了一个质的变化。


为了更好地兼容 Kotlin 语言,LitePal 现在不再只是一个库了,而是变成了两个库,根据你使用的语言不同,需要引入的库也不同。如果你使用的是 Java,那么就在 build.gradle 中引入如下配置:


dependencies {
implementation 'org.litepal.android:java:3.0.0'
}


而如果你使用的是 Kotlin,那么就在 build.gradle 中引入如下配置:


dependencies {
implementation 'org.litepal.android:kotlin:3.0.0'
}


好了,接下来我们就一起看一看 LitePal 3.0.0 版本到底变更了哪些东西。


不得不说,其实 LitePal 的泛型设计一直都不是很友好,尤其在异步查询的时候格外难受,比如我们看下如下代码:


在异步查询的onFinish()回调中,我们直接得到的并不是查询的对象,而是一个泛型 T 对象,还需要再经过一次强制转型才能得到真正想要查询的对象。


如果你觉得这还不算难受的话,那么再来看看下面这个例子:



可以看到,这次查询返回的是一个List<T>,我们必须要对整个 List 进行强制转型。不仅要多写一行代码,关键是开发工具还会给出一个很丑的警告。


这样的设计无论如何都算不上友好。


这里非常感谢 xiazunyang 这位朋友在 GitHub 上提出的这个 Issue(github.com/LitePalFram… 3.0.0 版本在泛型方面的优化很大程度上是基于他的建议。


那么我们现在来看看,到了 LitePal 3.0.0 版本,同样的功能可以怎么写:


LitePal.findAsync(Song.class, 1).listen(new FindCallback<Song>() {
@Override
public void onFinish(Song song) {

}
});


可以看到,这里在FindCallback接口上声明了泛型类型为Song,那么在onFinish()方法回调中的参数就可以直接指定为Song类型了,从而避免了一次强制类型转换。


那么同样地,在查询多条数据的时候就可以这样写:


LitePal.where("duration > ?", "100").findAsync(Song.class).listen(new FindMultiCallback<Song>() {
@Override
public void onFinish(List<Song> list) {

}
});


这次就清爽多了吧,在onFinish()回调方法中,我们直接拿到的就是一个List<Song>集合,而不会再出现那个丑丑的警告了。


而如果这段代码使用 Kotlin 来编写的话,将会更加的精简:


LitePal.where("duration > ?", "100").findAsync(Song::class.java).listen { list ->

}


得益于 Kotlin 出色的 lambda 机制,我们的代码可以得到进一步精简。在上述代码中,行尾的list参数就是查询出来的List<Song>集合了。


那么关于泛型优化的讲解就到这里,下面我们来看另一个主题,监听数据库的创建和升级。


没错,LitePal 3.0.0 版本新增了监听数据库的创建和升级功能。


加入这个功能是因为 JakeHao 这位朋友在 GitHub 上提了一个 Issue(github.com/LitePalFram…


)


要实现这个功能肯定要添加新的接口了,而我对于添加新接口保持着一种比较谨慎的态度,因为要考虑到接口的易用性和对整体框架的影响。


LitePal 的每一个接口我都要尽量将它设计得简单好用,因此大家应该也可以猜到了,监听数据库创建和升级这个功能会非常容易,只需要简单几行代码就可以了实现了:


LitePal.registerDatabaseListener(new DatabaseListener() {
@Override
public void onCreate() {
}

@Override
public void onUpgrade(int oldVersion, int newVersion) {
}
});


需要注意的是,registerDatabaseListener()方法一定要确保在任何其他数据库操作之前调用,然后当数据库创建的时候,onCreate()方法就会得到回调,当数据库升级的时候onUpgrade()方法就会得到回调,并且告诉通过参数告诉你之前的老版本号,以及升级之后的新版本号。


Kotlin 版的代码也是类似的,但是由于这个接口有两个回调方法,因此用不了 Kotlin 的单抽象方法 (SAM) 这种语法糖,只能使用实现接口的匿名对象这种写法:


LitePal.registerDatabaseListener(object : DatabaseListener {
override fun onCreate() {
}

override fun onUpgrade(oldVersion: Int, newVersion: Int) {
}
})


这样我们就将监听数据库创建和升级这部分内容也快速介绍完了,接下来即将进入到本篇文章的重头戏内容。


从上述文章中我们都可以看出,Kotlin 版的代码普遍都是比 Java 代码要更简约的,Google 给出的官方统计是,使用 Kotlin 开发可以减少大约 25% 以上的代码。


但是处处讲究简约的 Kotlin,却在有一处用法上让我着实很难受。比如使用 Java 查询 song 表中 id 为 1 的这条记录是这样写的:


Song song = LitePal.find(Song.class, 1);


而同样的功能在 Kotlin 中却需要这样写:


val song = LitePal.find(Song::class.java, 1)


由于 LitePal 必须知道要查询哪个表当中的数据,因此一定要传递一个 Class 参数给 LitePal 才行。在 Java 中我们只需要传入Song.class即可,但是在 Kotlin 中的写法却变成了Song::class.java,反而比 Java 代码更长了,有没有觉得很难受?


当然,很多人写着写着也就习惯了,这并不是什么大问题。但是随着我深入学习 Kotlin 之后,我发现 Kotlin 提供了一个相当强大的机制可以优化这个问题,这个机制叫作泛型实化。接下来我会对泛型实化的概念和用法做个详细的讲解。


要理解泛型实化,首先你需要知道泛型擦除的概念。


不管是 Java 还是 Kotlin,只要是基于 JVM 的语言,泛型基本都是通过类型擦除来实现的。也就是说泛型对于类型的约束只在编译时期存在,运行时期是无法直接对泛型的类型进行检查的。例如,我们创建一个List<String>集合,虽然在编译时期只能向集合中添加字符串类型的元素,但是在运行时期 JVM 却并不能知道它本来只打算包含哪种类型的元素,只能识别出来它是个List


Java 的泛型擦除机制,使得我们不可能使用if (a instanceof T),或者是T.class这样的语法。


而 Kotlin 也是基于 JVM 的语言,因此 Kotlin 的泛型在运行时也是会被擦除的。但是 Kotlin 中提供了一个内联函数的概念,内联函数中的代码会在编译的时候自动被替换到调用它的地方,这就使得原有方法调用时的形参声明和实参传递,在编译之后直接变成了同一个方法内的变量调用。这样的话也就不存在什么泛型擦除的问题了,因为 Kotlin 在编译之后会直接使用实参替代内联方法中泛型部分的代码。


简单点来说,就是 Kotlin 是允许将内联方法中的泛型进行实化的。


泛型实化


那么具体该怎么写才能将泛型实化呢?首先,该方法必须是内联方法才行,也就是要用inline关键字来修饰该方法。其次,在声明泛型的地方还必须加上reified关键字来表示该泛型要进行实化。示例代码如下所示:


inline fun <reified T> instanceOf(value: Any) {

}


上述方法中的泛型 T 就是一个被实化的泛型,因为它满足了内联函数和reified关键字这两个前提条件。那么借助泛型实化,我们到底可以实现什么样的效果呢?从方法名上就可以看出来了,这里我们借助泛型来实现一个 instanceOf 的效果,代码如下所示:


inline fun <reified T> instanceOf(value: Any) = value is T


虽然只有一行代码,但是这里实现了一个 Java 中完全不可能实现的功能 —— 判断参数的类型是不是属于泛型的类型。这就是泛型实化不可思议的地方。


那么我们如何使用这个方法呢?在 Kotlin 中可以这么写:


val result1 = instanceOf<String>("hello")
val result2 = instanceOf<String>(123)



可以看到,第一行代码指定的泛型是String,参数是字符串"hello",因此最后的结果是true。而第二行代码指定泛型是String,参数却是数字123,因此最后的结果是false


除了可以做类型判断之外,我们还可以直接获取到泛型的 Class 类型。看一下下面的代码:


inline fun <reified T> genericClass() = T::class.java


这段代码就更加不可思议了,genericClass()方法直接返回了当前指定泛型的 class 类型。T.class这样的语法在 Java 中是不可能的,而在 Kotlin 中借助泛型实化功能就可以使用T::class.java这样的语法了。


然后我们就可以这样调用:


val result1 = genericClass<String>()
val result2 = genericClass<Int>()



可以看到,我们如果指定了泛型String,那么最终就可以得到java.lang.String的 Class,如果指定了泛型Int,最终就可以得到java.lang.Integer的 Class。


关于 Kotlin 泛型实化这部分的讲解就到这里,现在我们重新回到 LitePal 上面。讲了这么多泛型实化方面的内容,那么 LitePal 到底如何才能利用这个特性进行优化呢?


回顾一下,刚才我们查询 song 表中 id 为 1 的这条记录是这样写的:


val song = LitePal.find(Song::class.java, 1)


这里需要传入Song::class.java是因为要告知 LitePal 去查询 song 这张表中的数据。而通过刚才泛型实化部分的讲解,我们知道 Kotlin 中是可以使用T::class.java这样的语法的,因此我在 LitePal 3.0.0 中扩展了这部分特性,允许通过指定泛型来声明查询哪张表中的内容。于是代码就可以优化成这个样子了:


val song = LitePal.find<Song>(1)


怎么样,有没有觉得代码瞬间清爽了很多?看起来比 Java 版的查询还要更加简约。


另外得益于 Kotlin 出色的类型推导机制,我们还可以将代码改为如下写法:


val song: Song? = LitePal.find(1)


这两种写法效果是一模一样的,因为如果我在song变量的后面声明了Song?类型,那么find()方法就可以自动推导出泛型类型,从而不需要再手动进行<Song>的泛型指定了。


除了find()方法之外,我还对 LitePal 中几乎全部的公有 API 都进行了优化,只要是原来需要传递 Class 参数的接口,我都增加了一个通过指定泛型来替代 Class 参数的扩展方法。注意,这里我使用的是扩展方法,而不是修改了原有方法,这样的话两种写法你都可以使用,全凭自己的喜好,如果是直接修改原有方法,那么项目升级之后就可能会造成大面积报错了,这是谁都不想看到的。


那么这里我再向大家演示另外几种 CRUD 操作优化之后的用法吧,比如我想使用 where 条件查询的时候就可以这样写:


val list = LitePal.where("duration > ?", "100").find<Song>()


这里在最后的 find() 方法中指定了泛型<Song>,得到的结果会是一个List<Song>集合。


想要删除 song 表中 id 为 1 的这条数据可以这么写:


LitePal.delete<Song>(1)


想要统计 song 表中的记录数量可以这么写:


val count = LitePal.count<Song>()


其他一些方法的优化也都是类似的,相信大家完全可以举一反三,就不再一一演示了。


这样我们就将 LitePal 新版本中的主要功能都介绍完了。当然,除了这些新功能之外,我还修复了一些已知的 bug,提升了整体框架的稳定性,如果这些正是你所需要的话,那就赶快升级吧。


收起阅读 »

从精准化测试看ASM在Android中的强势插入-JaCoco初探

ASM
在Java技术栈上,基本上提到覆盖率,大家就会想到JaCoco「Java Code Coverage的缩写」,几乎所有的覆盖率项目,都是使用JaCoco,可想而知它的影响力有多大,我们在Android项目中,也集成了JaCoco,官网文档如下。 docs.g...
继续阅读 »

在Java技术栈上,基本上提到覆盖率,大家就会想到JaCoco「Java Code Coverage的缩写」,几乎所有的覆盖率项目,都是使用JaCoco,可想而知它的影响力有多大,我们在Android项目中,也集成了JaCoco,官网文档如下。


docs.gradle.org/current/use…


但是这里的JaCoco是与单元测试配合使用的,与一般的业务测试场景不太一样,所以,我们需要自己依赖JaCoco来做改造。


初探


官网镇楼


http://www.eclemma.org/jacoco/


从官网上就能看出这是一个极具历史感的项目。最后生成的覆盖率文件,是在 源代码的基础上,用颜色标记不同的执行状态。


image-20210716171811946


在上面这张图中,绿色代表已执行, 红色代表未执行, 黄色代表执行了一部分,这样就可以算出代码的覆盖率数据。


使用全量报表


JaCoco默认的插桩方式是全部插桩,在Android项目中,要使用JaCoco的全量报表功能非常简单,因为JaCoco插件已经集成在Gradle中了,所以我们只需要开启JaCoco即可。


首先,在根目录gradle文件中加入JaCoco的依赖


classpath "org.jacoco:org.jacoco.core:0.8.4"

然后在App的gradle文件中增加插件的依赖。


apply plugin: 'jacoco'

并在android标签中,增加开关。


testCoverageEnabled = true

接下来引入JaCoco的Report模块,同时exclude掉core,因为其在gradle中已经有依赖了。


implementation('org.jacoco:org.jacoco.report:0.8.4') {
exclude group: 'org.jacoco', module: 'org.jacoco.core'
}

创建生成Report的Task


def coverageSourceDirs = ['../xxxx/src/main/java']

task jacocoTestReport(type: JacocoReport) {
group = "Reporting"
description = "Generate Jacoco coverage reports after running tests."
reports {
xml.enabled = true
html.enabled = true
}
classDirectories.setFrom(fileTree(
dir: './build/intermediates/javac/xxxxx',
excludes: ['**/R*.class']))
sourceDirectories.setFrom(files(coverageSourceDirs))
executionData.setFrom(files("$buildDir/outputs/code-coverage/connected/coverage.exec"))
doFirst {new File("$buildDir/intermediates/javac/masterDebug/classes/com/qidian/QDReader").eachFileRecurse { file ->
if (file.name.contains('$$')) {
file.renameTo(file.path.replace('$$', '$'))
}
}
}
}

在项目中合适的地方来调用这两个方法,分别用来创建JaCoco的Exec文件和写入Exec文件。


private void createExecFile() {
String DEFAULT_COVERAGE_FILE_PATH = "/mnt/sdcard/" + getPackageName();
String DEFAULT_COVERAGE_FILE = DEFAULT_COVERAGE_FILE_PATH + "/coverage.ec";
File file_path = new File(DEFAULT_COVERAGE_FILE_PATH);
File file = new File(DEFAULT_COVERAGE_FILE);
Log.d(TAG, "file_path = " + file_path);
if (!file.exists()) {
try {
file_path.mkdirs();
file.createNewFile();
} catch (IOException e) {
e.printStackTrace();
}
}
}

private void writeExecFile() {
OutputStream out = null;
try {
out = new FileOutputStream("/mnt/sdcard/" + getPackageName() + "/coverage.ec", true);
Object agent = Class.forName("org.jacoco.agent.rt.RT")
.getMethod("getAgent")
.invoke(null);
out.write((byte[]) agent.getClass().getMethod("getExecutionData", boolean.class)
.invoke(agent, false));
} catch (Exception e) {
Log.d(TAG, e.toString(), e);
e.printStackTrace();
} finally {
if (out != null) {
try {
out.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}

在创建Exec文件后,进行测试,然后写入Exec文件,等测试完毕后,把生成的Exec文件通过ADB pull到本地,再执行jacocoTestReport这个Task即可生成全量的JaCoco覆盖率报告。


花了这么长时间写了这么多,其实并没什么卵用,只是让大家看下如何来使用JaCoco的标准用法。


JaCoco插桩原理


JaCoco在Android上只能使用Offline mode,它的实现机制其实很简单,我们反编译一下它插入的代码。


image-20210617135224018


可以发现,实际上JaCoco就是用一个Boolean数组来标记每句可执行代码,只要执行过相应的语句,当前位就被标记为True,这个标记,官方称之为「探针」(Probe)。


JaCoco对代码的修改主要体现在下面几个地方:



  • 在Class中增加




    j


    a


    c


    o


    c


    o


    D


    a


    t


    a


    属性和



    jacocoData属性和


    jacocoInit方法

  • 在Method中增加了$jacocoInit数字并初始化

  • 增加了对数组的修改


当然,这只是JaCoco最基本的原理,实际的实现细节会更加复杂,例如条件、选择语句、方法函数的探针插入等等,这里不详细深入讨论,感兴趣的朋友可以参考JaCoco的源码:


github.com/jacoco/jaco…


性能影响


由于JaCoco只是插入一个探针数组,所以对代码执行的性能开销影响不大,但是由于插入大量的探针代码,所以代码体积会增大不少,一般情况下,Android会在测试包中做插入,而在正式包中去除插入逻辑。



当然,借助JaCoco还能玩一些骚操作,比如发到线上,实时统计代码中有哪些代码从未执行过,用于发现潜在的垃圾代码。



探针插桩策略


JaCoco的核心逻辑就是要决定,到底在哪插入探针代码。官网文档上对插桩策略写的比较清楚,涉及到字节码的一些原理,所以这里就不深入讲解了,感兴趣的朋友可以通过下面的链接查看。


http://www.jacoco.org/jacoco/trun…


关键代码类


JaCoco对代码的探针插入分析,主要是利用了下面这些计数器:



  • 指令计数器(CounterImpl)

  • 行计数器(LineImpl)

  • 方法计算节点(MethodCoverageImpl)

  • 类计算节点(ClassCoverageImpl)

  • Package计算节点(PackageCoverageImpl)

  • Module计算节点(BundleCoverageImpl)


这里面包含了JaCoco的覆盖率数据。


JaCoco的使用其实非常简单,原理也很简单,但要做的好,稳定运行这么多年没有Bug,还是很难的,所以现在市面上做覆盖率的很多软件都逐渐被历史所淘汰了,而剩下的就是经历过时间检验的真金。

收起阅读 »

JetpackSplashscreen解析助力新生代IT农民工事半功倍

公众号:ByteCode,致力于分享最新技术原创文章,涉及 Kotlin、Jetpack、译文、系统源码、 LeetCode / 剑指 Offer / 多线程 / 国内外大厂算法题 等等一系列文章。 Jetpack 家族迎来了一位新的成员 Core Sp...
继续阅读 »

公众号:ByteCode,致力于分享最新技术原创文章,涉及 Kotlin、Jetpack、译文、系统源码、 LeetCode / 剑指 Offer / 多线程 / 国内外大厂算法题 等等一系列文章。



Jetpack 家族迎来了一位新的成员 Core Splashscreen,所以我也要重新开始写 Jetpack 系列文章了,在这之前写过一系列 Jetpack 文章以及配套的实战应用,包含 App StartupPaging3HiltDataStoreViewBinding 等等实战项目,点击下方链接前去查看。



而今天这篇文章主要介绍 Google 新库 Core Splashscreen ,众所周知在 Android 12 中增加了一个改善用户体验的功能 SplashScreen API,它可为所有应用添加启动画面。包括启动时进入应用的启动动画,以及退出动画。


通过这篇文章你将学习到以下内容



  • Core Splashscreen 解决了什么问题?

  • Core Splashscreen 工作原理?

  • 针对不同的场景,如何在项目中使用 Core Splashscreen?

  • Core Splashscreen 源码分析?


Core Splashscreen 实战项目地址,可以前往 GitHub 查看示例项目 Splashscreen。 github.com/hi-dhl/Andr…


Core Splashscreen


Core Splashscreen 解决了什么问题?


在 Android 启动过程中会出现白屏 / 黑屏,为了改善这一体验,因此添加启动画面,从而改善视觉上的体验,为了实现这一功能,市面上也有很多实现方法,都有各自的优缺点,因此并不能保证在所有设备上都能够流畅的运行。


其次有的时候需要从本地磁盘或者网络异步加载数据,等待数据加载完之后,才会去渲染 View, 大多数时候,希望将数据加载提前,尽量保证用户进入到首页之后,看到数据,减少用户的等待时间。


在 Android 12 上新增的 SplashScreen API,可以解决这一系列问题,但是缺点是仅限于 Android 12。


Core Splashscreen 因此而诞生了,为 Android 12 新增的 SplashScreen API 提供了向后兼容,可以在 Android 5.0 (API 21) ~ Android 12 (API 31)所有的 API 上使用。来看一下 Google 提供的动画效果。



Core Splashscreen 工作原理


Core Splashscreen 为 Android 12 新增的 SplashScreen API 提供了向后兼容,但是仅仅在以下情况下才会显示启动画面:



  • 冷启动:用户打开 APP 时 APP 进程尚未运行

  • 温启动:APP 进程正在运行,但是 Activity 尚未创建


启动动画只有在以上情况才会显示,但是在热启动期间是不会显示启动画面。



  • 热启动:APP 进程正在运行,Activity 也已经创建,也就说用户按下 Home 键退到后台,直到 Activity 被销毁之前,是不会显示启动画面


如何使用 Core Splashscreen


因为 Core Splashscreen 兼容了 Android 12 新增的 SplashScreen API, 因此需要将 compileSdkVersion 更新到 31 及其以上。



如果你的 SDK 还没有更新到 Android 12, 请先更新。SDK Manager -> 选择 Android 12



android {
compileSdkVersion 31
}

在模块级别的 build.gradle 文件中添加以下依赖。


implementation 'androidx.core:core-splashscreen:1.0.0-alpha01'

当添加完依赖之后就可以开始使用 Core Splashscreen,只需要三步即可实现显示启动画面。


1. 在 res/values/themes.xml 文件下添加新的主题 Theme.AppSplashScreen


<style name="Theme.AppSplashScreen" parent="Theme.SplashScreen">
<item name="windowSplashScreenBackground">@color/purple_200</item>
<item name="windowSplashScreenAnimatedIcon">@mipmap/ic_launcher</item>
<item name="postSplashScreenTheme">@style/Theme.AppTheme</item>
</style>

<!-- Base application theme. -->
<style name="Theme.AppTheme" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<!-- 添加 APP 默认主题 -->
</style>


  • android:windowSplashScreenBackground : 设置背景颜色

  • windowSplashScreenAnimatedIcon : 设置显示在屏幕中间的图标, 如果是通过 AnimationDrawableAnimatedVectorDrawable 创建的对象,可呈现动画效果,则会在页面显示的时候,播放动画

  • postSplashScreenTheme : 设置显示动画不可见时,使用 APP 的默认主题


2. 在 application 节点中,设置上一步添加主题 Theme.AppSplashScreen


<application
android:theme="@style/Theme.AppSplashScreen">
</application>

3. 在调用 setContentView() 方法之前调用 installSplashScreen()


class MainActivity : AppCompatActivity() {
private val binding: ActivityMainBinding by viewbind()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
installSplashScreen()
with(binding) {
// init view
}
}
}

调用 installSplashScreen() 方法主要将 Activity 与我们添加的主题相关联。这一步完成之后,就可以在 APP 启动过程中,看到刚才设置的图标或者动画了。


扩展功能


让启动动画持久一点


默认情况下当应用绘制第一帧后,启动画面会立即关闭,但是有的时候需要从本地磁盘或者网络异步加载数据,这个时候,希望启动画面能够等到数据加载完回来才结束。可以通过以下方法实现。


splashScreen.setKeepVisibleCondition { !appReady }

// 模拟从本地磁盘或者网络异步加载数据的耗时操作
Handler(Looper.getMainLooper())
.postDelayed({ appReady = true }, 3000)

调用以上方法,可以让应用暂停绘制第一帧这样启动画面就不会结束,当数据加载完之后,通过更新变量 appReady 来控制是否结束启动画面。


实现退出动画


当然我们也可以添加启动画面的退出动画,即从启动画面优雅的回到应用主界面。


splashScreen.setOnExitAnimationListener { splashScreenViewProvider ->
......
// 自定义退出动画
val translationY = ObjectAnimator.ofFloat(......)
translationY.doOnEnd { splashScreenViewProvider.remove() }
translationY.start()
}

效果可以前往 GitHub 查看示例项目 Splashscreen。


GitHub 示例项目:https://github.com/hi-dhl/AndroidX-Jetpack-Practice


Core Splashscreen 源码解析


Core Splashscreen 源码很简单,总共就只有两个类。



  • SplashScreen :主要为实现 SplashScreen API 提供了向后兼容性,用于将 Activity 与主题相关联。

  • SplashScreenViewProvider : 用于控制退出动画(启动画面 -> 应用主界面),当退出动画结束时需要手动调用 SplashScreenViewProvider#remove() 方法


初始化 SplashScreen


通过调用 SplashScreen#installSplashScreen() 方法来进行初始化,将 Activity 与添加的主题相关联。 androidx/core/splashscreen/SplashScreen.kt


public companion object {
@JvmStatic
public fun Activity.installSplashScreen(): SplashScreen {
val splashScreen = SplashScreen(this)
splashScreen.install()
return splashScreen
}
}

private fun install() {
impl.install()
}

最终都是通过调用 impl.install() 方法来进行初始化,一起来看看成员变量 impl 是如何初始化的。


private val impl = when {
SDK_INT >= 31 -> Impl31(activity)
SDK_INT == 30 && PREVIEW_SDK_INT > 0 -> Impl31(activity)
SDK_INT >= 23 -> Impl23(activity)
else -> Impl(activity)
}

到这里我们知道了 Google 为了向后兼容,针对于不同版本的系统,分别对应有不同的实现类。最终都是调用 install() 方法来进行初始化的,在 install() 方法内通过解析我们添加的主题,最后通过 activity.setTheme() 方法,将添加的主题和 Activity 关联在一起。


如何让启动动画持久一点


在代码中,我们通过调用 SplashScreen#setKeepVisibleCondition() 方法,让启动动画持久一点,等待数据加完之后,才结束启动动画。一起来看看这个方法。 androidx/core/splashscreen/SplashScreen.kt


public fun setKeepVisibleCondition(condition: KeepOnScreenCondition) {
// impl:针对于不同版本的系统,分别对应有不同的实现类
impl.setKeepVisibleCondition(condition)
}

open fun setKeepVisibleCondition(keepOnScreenCondition: KeepOnScreenCondition) {
......
observer.addOnPreDrawListener(object : OnPreDrawListener {
override fun onPreDraw(): Boolean {
if (splashScreenWaitPredicate.shouldKeepOnScreen()) {
return false
}
contentView.viewTreeObserver.removeOnPreDrawListener(this)
// 当开始绘制时,会调用 dispatchOnExitAnimation 方法,结束启动动画
mSplashScreenViewProvider?.let(::dispatchOnExitAnimation)
return true
}
})
}

最后通过 ViewTreeObserver 来监听视图的变化,当视图将要开始绘制时,会回调 OnPreDrawListener#onPreDraw() 方法。最后调用 dispatchOnExitAnimation 方法,结束启动动画。


实现退出动画


最后一起来看一下,源码中是如何实现退出动画,即从启动画面优雅的回到应用主界面,源码中只是提供了一个 OnExitAnimationListener 接口,将退出动画交给了开发者去实现,一起来看一下SplashScreen#setOnExitAnimationListener() 方法。 androidx/core/splashscreen/SplashScreen.kt


Android 12 以上


override fun setOnExitAnimationListener(
exitAnimationListener: OnExitAnimationListener
) {
activity.splashScreen.setOnExitAnimationListener {
val splashScreenViewProvider = SplashScreenViewProvider(it, activity)
exitAnimationListener.onSplashScreenExit(splashScreenViewProvider)
}
}

在 Android 12 中是通过系统源码提供的接口 activity.splashScreen.setOnExitAnimationListener ,回调对外暴露的接口 OnExitAnimationListener 让开发者去实现退出动画的效果。


Android 12 以下


open fun setOnExitAnimationListener(exitAnimationListener: OnExitAnimationListener) {
animationListener = exitAnimationListener
val splashScreenViewProvider = SplashScreenViewProvider(activity)
......
splashScreenViewProvider.view.addOnLayoutChangeListener(
object : OnLayoutChangeListener {
override fun onLayoutChange(......) {
......
dispatchOnExitAnimation(splashScreenViewProvider)
}
})
}

fun dispatchOnExitAnimation(splashScreenViewProvider: SplashScreenViewProvider) {
......
splashScreenViewProvider.view.postOnAnimation {
finalListener.onSplashScreenExit(splashScreenViewProvider)
}
}

通过向屏幕中显示的 View 添加 addOnLayoutChangeListener 方法,来监听布局的变化,当布局会发生改变时,会回调 onLayoutChange 方法,最后通过回调对外暴露的接口 OnExitAnimationListener 让开发者去实现退出动画。


不过这里需要注意的是,最后都需要调用 SplashScreenViewProvider#remove() 方法在合适的时机移除动画,可以在退出动画结束时,调用这个方法。


总结


本文从不同的角度分别分析了 Core Splashscreen。如何在项目中使用 Core Splashscreen,可以前往 GitHub 查看示例项目 Splashscreen。


仓库地址:https://github.com/hi-dhl/AndroidX-Jetpack-Practice


另外 KtKit 是用 Kotlin 语言编写的小巧而实用工具库,包含了项目中常用的一系列工具,我添加了许多新的功能,包含了很多 Kotlin 技巧。文章分析可前往查看 为数不多的人知道的 Kotlin 技巧以及解析(三)


监听 EditText


将 Flow 通过 lifecycleScope 将 EditText 与 Activity / Fragment 的生命周期绑定在一起,在 Activity / Fragment 生命周期结束时,会结束 flow , flow 结束时会断开它们之间的引用,有效的避免内存泄漏。


......
// 监听 TextWatcher#onTextChanged 的回调函数
editText.textChange(lifecycleScope) {
Log.e(TAG, "textChange = $it")
}

// 监听 TextWatcher#beforeTextChanged 的回调函数
editText.textChangeWithbefore(lifecycleScope) {
Log.e(TAG, "textChangeWithbefore = $it")
}

// 监听 TextWatcher#afterTextChanged 的回调函数
editText.textChangeWithAfter(lifecycleScope) {
Log.e(TAG, "textChangeWithbefore = $it")
}
......

监听蜂窝网络变化


lifecycleScope.launch {
listenCellular().collect {
Log.e(TAG, "listenNetwork = $it")
}
}

监听 wifi 网络的变化


lifecycleScope.launch {
listenWifi().collect {
Log.e(TAG, "listenNetwork = $it")
}
}

监听蓝牙网络的变化


lifecycleScope.launch {
listenNetworkFlow().collect {
Log.e(TAG, "listenNetwork = $it")
}
}

更多 API 使用方式点击这里前往查看:



如果这个仓库对你有帮助,请在仓库右上角帮我 star 一下,非常感谢你的支持,同时也欢迎你提交 PR ??????




如果有帮助 点个赞 就是对我最大的鼓励


代码不止,文章不停


欢迎关注公众号:ByteCode,持续分享最新的技术










收起阅读 »

Flow操作符shareIn和stateIn使用须知

Flow.shareIn 与 Flow.stateIn 操作符可以将冷流转换为热流: 它们可以将来自上游冷数据流的信息广播给多个收集者。这两个操作符通常用于提升性能: 在没有收集者时加入缓冲;或者干脆作为一种缓存机制使用。 注意 : 冷流 是按需创建的...
继续阅读 »

Flow.shareInFlow.stateIn 操作符可以将冷流转换为热流: 它们可以将来自上游冷数据流的信息广播给多个收集者。这两个操作符通常用于提升性能: 在没有收集者时加入缓冲;或者干脆作为一种缓存机制使用。



注意 : 冷流 是按需创建的,并且会在它们被观察时发送数据;热流 则总是活跃,无论是否被观察,它们都能发送数据。



本文将会通过示例帮您熟悉 shareIn 与 stateIn 操作符。您将学到如何针对特定用例配置它们,并避免可能遇到的常见陷阱。


底层数据流生产者


继续使用我 之前文章 中使用过的例子——使用底层数据流生产者发出位置更新。它是一个使用 callbackFlow 实现的 冷流。每个新的收集者都会触发数据流的生产者代码块,同时也会将新的回调加入到 FusedLocationProviderClient。


class LocationDataSource(
private val locationClient: FusedLocationProviderClient
) {
val locationsSource: Flow<Location> = callbackFlow<Location> {
val callback = object : LocationCallback() {
override fun onLocationResult(result: LocationResult?) {
result ?: return
try { offer(result.lastLocation) } catch(e: Exception) {}
}
}
requestLocationUpdates(createLocationRequest(), callback, Looper.getMainLooper())
.addOnFailureListener { e ->
close(e) // in case of exception, close the Flow
}
// 在 Flow 结束收集时进行清理
awaitClose {
removeLocationUpdates(callback)
}
}
}

让我们看看在不同的用例下如何使用 shareIn 与 stateIn 优化 locationsSource 数据流。


shareIn 还是 stateIn?


我们要讨论的第一个话题是 shareInstateIn 之间的区别。shareIn 操作符返回的是 SharedFlowstateIn 返回的是 StateFlow



注意 : 要了解有关 StateFlowSharedFlow 的更多信息,可以查看 我们的文档



StateFlow 是 SharedFlow 的一种特殊配置,旨在优化分享状态: 最后被发送的项目会重新发送给新的收集者,并且这些项目会使用 Any.equals 进行合并。您可以在 StateFlow 文档 中查看更多相关信息。


两者之间的最主要区别,在于 StateFlow 接口允许您通过读取 value 属性同步访问其最后发出的值。而这不是 SharedFlow 的使用方式。


提升性能


通过共享所有收集者要观察的同一数据流实例 (而不是按需创建同一个数据流的新实例),这些 API 可以为我们提升性能。


在下面的例子中,LocationRepository 消费了 LocationDataSource 暴露的 locationsSource 数据流,同时使用了 shareIn 操作符,从而让每个对用户位置信息感兴趣的收集者都从同一数据流实例中收集数据。这里只创建了一个 locationsSource 数据流实例并由所有收集者共享:


class LocationRepository(
private val locationDataSource: LocationDataSource,
private val externalScope: CoroutineScope
) {
val locations: Flow<Location> =
locationDataSource.locationsSource.shareIn(externalScope, WhileSubscribed())
}

WhileSubscribed 共享策略用于在没有收集者时取消上游数据流。这样一来,我们便能在没有程序对位置更新感兴趣时避免资源的浪费。



Android 应用小提醒! 在大部分情况下,您可以使用 WhileSubscribed(5000),当最后一个收集者消失后再保持上游数据流活跃状态 5 秒钟。这样在某些特定情况 (如配置改变) 下可以避免重启上游数据流。当上游数据流的创建成本很高,或者在 ViewModel 中使用这些操作符时,这一技巧尤其有用。



缓冲事件


在下面的例子中,我们的需求有所改变。现在要求我们保持监听位置更新,同时要在应用从后台返回前台时在屏幕上显示最后的 10 个位置:


class LocationRepository(
private val locationDataSource: LocationDataSource,
private val externalScope: CoroutineScope
) {
val locations: Flow<Location> =
locationDataSource.locationsSource
.shareIn(externalScope, SharingStarted.Eagerly, replay = 10)
}

我们将参数 replay 的值设置为 10,来让最后发出的 10 个项目保持在内存中,同时在每次有收集者观察数据流时重新发送这些项目。为了保持内部数据流始终处于活跃状态并发送位置更新,我们使用了共享策略 SharingStarted.Eagerly,这样就算没有收集者,也能一直监听更新。


缓存数据


我们的需求再次发生变化,这次我们不再需要应用处于后台时 持续 监听位置更新。不过,我们需要缓存最后发送的项目,让用户在获取当前位置时能在屏幕上看到一些数据 (即使数据是旧的)。针对这种情况,我们可以使用 stateIn 操作符。


class LocationRepository(
private val locationDataSource: LocationDataSource,
private val externalScope: CoroutineScope
) {
val locations: Flow<Location> =
locationDataSource.locationsSource.stateIn(externalScope, WhileSubscribed(), EmptyLocation)
}

Flow.stateIn 可以缓存最后发送的项目,并重放给新的收集者。


注意!不要在每个函数调用时创建新的实例


切勿 在调用某个函数调用返回时,使用 shareIn 或 stateIn 创建新的数据流。这样会在每次函数调用时创建一个新的 SharedFlow 或 StateFlow,而它们将会一直保持在内存中,直到作用域被取消或者在没有任何引用时被垃圾回收。


class UserRepository(
private val userLocalDataSource: UserLocalDataSource,
private val externalScope: CoroutineScope
) {
// 不要像这样在函数中使用 shareIn 或 stateIn
// 这将在每次调用时创建新的 SharedFlow 或 StateFlow,而它们将不会被复用。
fun getUser(): Flow<User> =
userLocalDataSource.getUser()
.shareIn(externalScope, WhileSubscribed())

// 可以在属性中使用 shareIn 或 stateIn
val user: Flow<User> =
userLocalDataSource.getUser().shareIn(externalScope, WhileSubscribed())
}

需要入参的数据流


需要入参 (如 userId) 的数据流无法简单地使用 shareInstateIn 共享。以开源项目——Google I/O 的 Android 应用 iosched 为例,您可以在 源码中 看到,从 Firestore 获取用户事件的数据流是通过 callbackFlow 实现的。由于其接收 userId 作为参数,因此无法简单使用 shareInstateIn 操作符对其进行复用。


class UserRepository(
private val userEventsDataSource: FirestoreUserEventDataSource
) {
// 新的收集者会在 Firestore 中注册为新的回调。
// 由于这一函数依赖一个 `userId`,所以在这个函数中
// 数据流无法通过调用 shareIn 或 stateIn 进行复用.
// 这样会导致每次调用函数时,都会创建新的 SharedFlow 或 StateFlow
fun getUserEvents(userId: String): Flow<UserEventsResult> =
userLocalDataSource.getObservableUserEvents(userId)
}

如何优化这一用例取决于您应用的需求:



  • 您是否允许同时从多个用户接收事件?如果答案是肯定的,您可能需要为 SharedFlowStateFlow 实例创建一个 map,并在 subscriptionCount 为 0 时移除引用并退出上游数据流。

  • 如果您只允许一个用户,并且收集者需要更新为观察新的用户,您可以向一个所有收集者共用的 SharedFlowStateFlow 发送事件更新,并将公共数据流作为类中的变量。


shareInstateIn 操作符可以与冷流一同使用来提升性能,您可以使用它们在没有收集者时添加缓冲,或者直接将其作为缓存机制使用。小心使用它们,不要在每次函数调用时都创建新的数据流实例——这样会导致资源的浪费及预料之外的问题!


收起阅读 »

Android超简单实现验证码倒计时,页面关闭不中断,杀掉进程也不中断

在日常开发中,获取验证码是一个常见的功能,通常验证码倒计时的实现思路都是使用CountDownTimer来实现,但是存在一个问题就是当页面关闭之后重新进入页面,倒计时是不会继续进行的,如果后端验证码接口做了时间限制,那么我们再次请求的时候就会报错,用户体验不好...
继续阅读 »

在日常开发中,获取验证码是一个常见的功能,通常验证码倒计时的实现思路都是使用CountDownTimer来实现,但是存在一个问题就是当页面关闭之后重新进入页面,倒计时是不会继续进行的,如果后端验证码接口做了时间限制,那么我们再次请求的时候就会报错,用户体验不好。 作为一名CV工程师,在一番百度之后,唯一找到的一个实现方案还需要花钱下载,哎。。。无奈只能自己想了。

实现思路

其实实现思路很简单,使用CountDownTimer进行倒计时,在开始倒计时的时候,把 当前时间+倒计时总时间 持久化存储,再次打开页面的时候判断一下当前时间是否在倒计时时间的范围内,如果在就继续倒计时。

效果如下

在这里插入图片描述

倒计时工具类

持久化存储我使用了腾讯的MMKV,当前你也可以使用SharedPreference 如果你使用的也是MMKV,别忘了在Application中初始化

package com.lzk.jetpacktest.code

import android.os.CountDownTimer
import com.tencent.mmkv.MMKV

/**
* @Author: LiaoZhongKai
* @Date: 2021/8/17 16:38
* @Description: 验证码倒计时工具类
*/
object CodeCountDownUtil {
private const val KEY_TIME = "TotalTimeMills"
private val mMkv: MMKV = MMKV.mmkvWithID("CodeCountDownUtil")
private var mTotalTimeMills = 0L
private var mListener: OnCountDownListener? = null

private val mCountDownTimer: CountDownTimer
get() {
return object : CountDownTimer(getCountTimeMills(),1000){
override fun onTick(millisUntilFinished: Long) {
mListener?.onTick(millisUntilFinished/1000)
}

override fun onFinish() {
mListener?.onFinish()
}

}
}

/**
* 开始倒计时
* [totalTimeMills] 倒计时时间毫秒值
*/
fun start(totalTimeMills: Long){
mTotalTimeMills = totalTimeMills+System.currentTimeMillis()
mMkv.encode(KEY_TIME, mTotalTimeMills)
mCountDownTimer.start()
}

/**
* 继续倒计时
*/
fun continueCount(){
mCountDownTimer.start()
}

/**
* 取消倒计时
*/
fun cancel(){
mCountDownTimer.cancel()
}

/**
* 获取当前的倒计时时间毫秒值
*/
fun getCountTimeMills(): Long{
mTotalTimeMills = mMkv.decodeLong(KEY_TIME)
return mTotalTimeMills - System.currentTimeMillis()
}

/**
* 当前时间是否在倒计时范围内
* @return true: 倒计时正在进行 false:倒计时未进行
*/
fun isCounting(): Boolean{
mTotalTimeMills = mMkv.decodeLong(KEY_TIME)
return System.currentTimeMillis() < mTotalTimeMills
}

/**
* 设置监听器
*/
fun setOnCountDownListener(listener: OnCountDownListener){
mListener = listener
}

interface OnCountDownListener{
/**
* 倒计时
* [seconds] 倒计时剩余时间,秒为单位
*/
fun onTick(seconds: Long)

/**
* 倒计时结束
*/
fun onFinish()
}
}

使用示例

package com.lzk.jetpacktest.code

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.Toast
import androidx.databinding.DataBindingUtil
import com.lzk.jetpacktest.R
import com.lzk.jetpacktest.databinding.ActivityVerificationCodeBinding

/**
* @Author: LiaoZhongKai
* @Date: 2021/8/18 9:22
* @Description: 验证码页面
*/
class VerificationCodeActivity : AppCompatActivity() {

companion object{
//验证码倒计时总时间
//60秒
private const val COUNT_DOWN_TIME = 60*1000L
}

private lateinit var mBinding: ActivityVerificationCodeBinding

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mBinding = DataBindingUtil.setContentView(this,R.layout.activity_verification_code)
initView()
initEvent()
}

//千万不要忘记在onDestroy中取消倒计时
//千万不要忘记在onDestroy中取消倒计时
//千万不要忘记在onDestroy中取消倒计时
override fun onDestroy() {
super.onDestroy()
CodeCountDownUtil.cancel()
}

private fun initView(){
//如果当前时间仍在倒计时范围内,则显示倒计时
if (CodeCountDownUtil.isCounting()){
CodeCountDownUtil.continueCount()
mBinding.btn.isEnabled = false
}else{//否则显示获取验证码
mBinding.btn.text = "获取验证码"
}
}

private fun initEvent(){
//设置监听器
CodeCountDownUtil.setOnCountDownListener(object : CodeCountDownUtil.OnCountDownListener{
override fun onTick(seconds: Long) {
mBinding.btn.text = "$seconds 秒后重新获取"
}

override fun onFinish() {
mBinding.btn.isEnabled = true
mBinding.btn.text = "获取验证码"
}

})

//获取验证码按钮
mBinding.btn.setOnClickListener {
//模拟验证码发送成功
Toast.makeText(this,"验证码已发送",Toast.LENGTH_SHORT).show()
//按照正常流程,以下代码应该在验证码发送成功之后再调用
//开始倒计时
CodeCountDownUtil.start(COUNT_DOWN_TIME)
mBinding.btn.isEnabled = false
}
}

}
xml
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">


<data>

</data>

<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".code.VerificationCodeActivity">


<Button
android:id="@+id/btn"
android:layout_width="150dp"
android:layout_height="50dp"
android:layout_gravity="center"
android:text="获取验证码" />

</FrameLayout>
</layout>
收起阅读 »

四大组件之Service|Android开发系列

概述  Service 是一种可在后台执行长时间运行操作而不提供界面的应用组件,与Activity不同,Activity是现实图形用户界面的,而Service的运行是不可见的。服务可由其他应用组件启动,即使切换到其他的应用,Service仍将在后台继...
继续阅读 »

概述

  Service 是一种可在后台执行长时间运行操作而不提供界面的应用组件,与Activity不同,Activity是现实图形用户界面的,而Service的运行是不可见的。服务可由其他应用组件启动,即使切换到其他的应用,Service仍将在后台继续运行。此外,组件可通过绑定到服务与之进行交互,甚至是执行进程间通信 (IPC)。例如,服务可在后台处理网络事务、播放音乐,执行文件 I/O 或与内容提供程序进行交互。

Sercvvice类型

前台Service

  前台服务执行一些用户能注意到的操作。例如,音频应用会使用前台服务来播放音频曲目。前台服务必须显示Notification。即使用户停止与应用的交互,前台服务仍会继续运行。

后台Service

  后台服务执行用户不会直接注意到的操作。例如,如果应用使用某个服务来压缩其存储空间,则此服务通常是后台服务。

注意:如果您的应用面向 API 级别 26 或更高版本,当应用本身未在前台运行时,系统会对运行后台服务施加限制。在诸如此类的大多数情况下,您的应用应改为使用计划作业

绑定Service

   绑定Service方式核心在于bindService()绑定服务会提供客户端-服务器接口,以便组件与服务进行交互、发送请求、接收结果,甚至是利用进程间通信 (IPC) 跨进程执行这些操作。仅当与另一个应用组件绑定时,绑定服务才会运行。多个组件可同时绑定到该服务,但全部取消绑定后,该服务即会被销毁。

Service生命周期

image.png

public class MyService extends Service {

/**
* 首次创建服务时,系统会(在调用 onStartCommand() 或 onBind() 之前)调用此方法来执行一次性设置程序。如果服务已在运行,则不会调用此方法。
*/
@Override
public void onCreate() {
super.onCreate();
}

/**
* 当另一个组件(如 Activity)请求启动服务时,系统会通过调用 startService() 来调用此方法。执行此方法时,服务即会启动并可在后台无限期运行。
* 如果您实现此方法,则在服务工作完成后,您需负责通过调用 stopSelf() 或 stopService() 来停止服务。(如果您只想提供绑定,则无需实现此方法。)
* @param intent
* @param flags
* @param startId
* @return
*/
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
return super.onStartCommand(intent, flags, startId);
}

/**
* 当另一个组件想要与服务绑定(例如执行 RPC)时,系统会通过调用 bindService() 来调用此方法。在此方法的实现中,您必须通过返回 IBinder 提供一个接口,
* 以供客户端用来与服务进行通信。请务必实现此方法;但是,如果您并不希望允许绑定,则应返回 null。
* @param intent
* @return
*/
@Override
public IBinder onBind(Intent intent) {
return null;
}

/**
* 取消绑定时调用
* @param intent
* @return
*/
@Override
public boolean onUnbind(Intent intent) {
return super.onUnbind(intent);
}

/**
* 当不再使用服务且准备将其销毁时,系统会调用此方法。服务应通过实现此方法来清理任何资源,如线程、注册的侦听器、接收器等。这是服务接收的最后一个调用。
*/
@Override
public void onDestroy() {
super.onDestroy();
}
}

创建启动和绑定Service

   常见的需要定义一个Service的子类,重写其生命周期方法,当创建一个Service后,必须将这个Service在AndroidManifest.xml中进行注册。

<service
android:name=".MyService"
android:enabled="true"
android:exported="true" />
public class MainActivity extends AppCompatActivity implements View.OnClickListener {

private Button mStartService;
private Button mBindService;
private Button mUnBindService;
private Button mStopService;
private ServiceConnection serviceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {

}

@Override
public void onServiceDisconnected(ComponentName name) {

}
};

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mStartService = findViewById(R.id.start_service);
mBindService = findViewById(R.id.onBind_service);
mUnBindService = findViewById(R.id.unbind_service);
mStopService = findViewById(R.id.stop_service);
mStartService.setOnClickListener(this);
mBindService.setOnClickListener(this);
mUnBindService.setOnClickListener(this);
mStopService.setOnClickListener(this);
}

@Override
public void onClick(View v) {
switch (v.getId()){
case R.id.start_service:
Intent startIntent = new Intent(this,MyService.class);
startService(startIntent);
break;
case R.id.onBind_service:
Intent bindIntent = new Intent(this,MyService.class);
bindService(bindIntent, serviceConnection,BIND_AUTO_CREATE);
break;
case R.id.unbind_service:
unbindService(serviceConnection);
break;
case R.id.stop_service:
Intent stopIntent = new Intent();
stopService(stopIntent);
break;
}
}
}
  • 如果服务存在,binderService方法只能是onBind方法被调用,而unBindService方法只能被onUnbind被调用
收起阅读 »

Android Binder 学习笔记-未完结

读者须知:在跟随大佬学习的历程中,不断地跟着源码查看,主要参阅的博客我会在文章开头着重声明一遍。由于自己能力有限,在总结过程中发现很多东西没有想象的简单,该文章暂时处于烂尾模式....重启计划暂定于一年后。 Binder 真的太深了(个人感觉,短时间内没办法总...
继续阅读 »

读者须知:

在跟随大佬学习的历程中,不断地跟着源码查看,主要参阅的博客我会在文章开头着重声明一遍。由于自己能力有限,在总结过程中发现很多东西没有想象的简单,该文章暂时处于烂尾模式....

重启计划暂定于一年后。 Binder 真的太深了(个人感觉,短时间内没办法总结好)

如果有看到该篇文章想学Binder知识的同学,可参考以下三篇文章

Binder设计原理

写给 Android 应用工程师的 Binder 原理剖析

Binder源码解析

当然在学习图中为了能看到那个部分比喻,也重新温习了 互联网协议 和 DNS 原理 DNS原理 和 互联网协议

Binder学习笔记 1

前言

Android系统中,每个应用程序是由Android的Activity,Service,Broadcast,ContentProvider这四组件的中一个或多个组合而成,这四组件所涉及的多进程间的通信底层都是依赖于Binder IPC机制。 此处借用大佬的一段话来诠释Binder Binder设计原理

Android作为一个开放式,拥有众多开发者的的平台,应用程序的来源广泛,确保智能终端的安全是非常重要的。终端用户不希望从网上下载的程序在不知情的情况下偷窥隐私数据,连接无线网络,长期操作底层设备导致电池很快耗尽等等。传统IPC没有任何安全措施,完全依赖上层协议来确保。首先传统IPC的接收方无法获得对方进程可靠的UID/PID(用户ID/进程ID),从而无法鉴别对方身份。Android为每个安装好的应用程序分配了自己的UID,故进程的UID是鉴别进程身份的重要标志。使用传统IPC只能由用户在数据包里填入UID/PID,但这样不可靠,容易被恶意程序利用。可靠的身份标记只有由IPC机制本身在内核中添加。其次传统IPC访问接入点是开放的,无法建立私有通道。比如命名管道的名称,system V的键值,socket的ip地址或文件名都是开放的,只要知道这些接入点的程序都可以和对端建立连接,不管怎样都无法阻止恶意程序通过猜测接收方地址获得连接。

基于以上原因,Android需要建立一套新的IPC机制来满足系统对通信方式,传输性能和安全性的要求,这就是Binder。Binder基于Client-Server通信模式,传输过程只需一次拷贝,为发送发添加UID/PID身份,既支持实名Binder也支持匿名Binder,安全性高。

Binder使用Client-Server通信方式:一个进程作为Server提供诸如视频/音频解码,视频捕获,地址本查询,网络连接等服务;多个进程作为Client向Server发起服务请求,获得所需要的服务。要想实现Client-Server通信据必须实现以下两点:一是server必须有确定的访问接入点或者说地址来接受Client的请求,并且Client可以通过某种途径获知Server的地址;二是制定Command-Reply协议来传输数据。例如在网络通信中Server的访问接入点就是Server主机的IP地址+端口号,传输协议为TCP协议。对Binder而言,Binder可以看成Server提供的实现某个特定服务的访问接入点, Client通过这个‘地址’向Server发送请求来使用该服务;对Client而言,Binder可以看成是通向Server的管道入口,要想和某个Server通信首先必须建立这个管道并获得管道入口。

与其它IPC不同,Binder使用了面向对象的思想来描述作为访问接入点的Binder及其在Client中的入口:Binder是一个实体位于Server中的对象,该对象提供了一套方法用以实现对服务的请求,就象类的成员函数。遍布于client中的入口可以看成指向这个binder对象的‘指针’,一旦获得了这个‘指针’就可以调用该对象的方法访问server。在Client看来,通过Binder‘指针’调用其提供的方法和通过指针调用其它任何本地对象的方法并无区别,尽管前者的实体位于远端Server中,而后者实体位于本地内存中。‘指针’是C++的术语,而更通常的说法是引用,即Client通过Binder的引用访问Server。而软件领域另一个术语‘句柄’也可以用来表述Binder在Client中的存在方式。从通信的角度看,Client中的Binder也可以看作是Server Binder的‘代理’,在本地代表远端Server为Client提供服务。

IPC 简单概要理解

进程间通信(inter-process communication或interprocess communication,简写IPC)是指两个或两个以上进程(或线程)之间进行数据或信号交互的技术方案。

11.webp

每个Android的进程,只能运行在自己进程所拥有的虚拟地址空间。举例 对应一个4GB的虚拟地址空间,其中3GB是用户空间,1GB是内核空间,当然内核空间的大小是可以通过参数配置调整的。对于用户空间,不同进程之间彼此是不能共享的,而内核空间却是可共享的。Client进程向Server进程通信,恰恰是利用 进程间可共享的内核内存空间 来完成底层通信工作的,Client端与Server端进程往往采用ioctl等方法跟内核空间的驱动进行交互。

Binder 原理

sdk ver : Android 6.0

Gityuan 博客学习地址 Binder 部分为本人你学习其博客的笔记与总结。

22.webp

Binder通信采用C/S架构,从组件视角来说,包含Client、Server、ServiceManager以及binder驱动,其中ServiceManager用于管理系统中的各种服务。架构图如上所示。

可以看出无论是注册服务和获取服务的过程都需要ServiceManager,需要注意的是此处的Service Manager是指Native层的ServiceManager(C++)。ServiceManager是整个Binder通信机制的管家,是Android进程间通信机制Binder的守护进程,来负责 查询 和 注册服务 。和DNS类似,ServiceManager的作用是将字符形式的Binder名字转化成Client中对该Binder的引用,使得Client能够通过Binder名字获得对Server中Binder实体的引用。

图中Client/Server/ServiceManage之间的相互通信都是基于Binder机制。既然基于Binder机制通信,那么同样也是C/S架构,则图中的3大步骤都有相应的Client端与Server端。

  1. 注册服务(addService):Server进程要先注册Service到ServiceManager。该过程:Server是客户端,ServiceManager是服务端。
  2. 获取服务(getService):Client进程使用某个Service前,须先向ServiceManager中获取相应的Service。该过程:Client是客户端,ServiceManager是服务端。
  3. 使用服务:Client根据得到的Service信息建立与Service所在的Server进程通信的通路,然后就可以直接与Service交互。该过程:client是客户端,server是服务端。

上图中的 Client,Server,Service Manager 之间交互都是虚线表示,是由于它们彼此之间不是直接交互的,而是都通过与Binder驱动进行交互的,从而实现IPC通信方式。其中Binder驱动位于内核空间,Client,Server,Service Manager位于用户空间。Binder驱动和Service Manager可以看做是Android平台的基础架构,而Client和Server是Android的应用层,开发人员只需自定义实现client、Server端,借助Android的基本平台架构便可以直接进行IPC通信。

Binder 学习笔记 2

Binder 中 ServerManage启动

SMgr是一个进程,Server是另一个进程,Server向SMgr注册Binder必然会涉及进程间通信。当前实现的是进程间通信却又要用到进程间通信,这就好象蛋可以孵出鸡前提却是要找只鸡来孵蛋。Binder的实现比较巧妙:预先创造一只鸡来孵蛋:ServiceManager和其它进程同样采用Binder通信,ServiceManager是Server端,有自己的Binder对象(实体),其它进程都是Client,需要通过这个Binder的引用来实现Binder的注册,查询和获取。ServiceManager提供的Binder比较特殊,它没有名字也不需要注册,当一个进程使用BINDER_SET_CONTEXT_MGR命令将自己注册成ServiceManager时Binder驱动会自动为它创建Binder实体(这就是那只预先造好的鸡)。其次这个Binder的引用在所有Client中都固定为0而无须通过其它手段获得。也就是说,一个Server若要向ServiceManager注册自己Binder就必需通过0这个引用号和ServiceManager的Binder通信。

↓↓↓↓↓↓ ServerManage 启动流程图

33.webp

ServiceManager是Binder IPC通信过程中的守护进程,本身也是一个Binder服务,但并没有采用libbinder中的多线程模型来与Binder驱动通信,而是自行编写了binder.c直接和Binder驱动来通信,并且只有一个循环binder_loop来进行读取和处理事务,接下来我们通过源码看下 ServiceManager怎么玩的。

通过查看官网 得知ServiceManager 是由init进程通过解析init.rc文件而创建的,其所对应的可执行程序/system/bin/servicemanager,所对应的源文件是service_manager.c。通过 service_manager.c 中的main() 函数为入口 查看整个启动 ServiceManager的流程。

主要方法都在 main函数中出现,后续是对部分方法的深入查看

// 主方法
int main(int argc, char **argv) {
struct binder_state *bs;
//第一步 打开binder驱动,申请128k字节大小的内存空间
bs = binder_open(128*1024);
...

//第二步 成为上下文管理者
if (binder_become_context_manager(bs)) {
return -1;
}

//selinux权限 判断进程 是否有权利注册或者查看
selinux_enabled = is_selinux_enabled();
sehandle = selinux_android_service_context_handle();
selinux_status_open(true);

if (selinux_enabled > 0) {
if (sehandle == NULL) {
abort(); //无法获取sehandle
}
if (getcon(&service_manager_context) != 0) {
abort(); //无法获取service_manager上下文
}
}
...

//第三步 进入无限循环,处理client端发来的请求。svcmgr_handler 主要提供服务注册和查找
binder_loop(bs, svcmgr_handler);
return 0;
}

第一步 打开binder驱动

对应上图中的 第一步到第四步

  1. 先调用open()打开binder设备,open()方法经过系统调用,进入Binder驱动,然后调用方法binder_open(),该方法会在Binder驱动层创建一个binder_proc对象,再将binder_proc对象赋值给fd->private_data,同时放入全局链表binder_procs。

  2. 再通过ioctl()检验当前binder版本与Binder驱动层的版本是否一致。

  3. 调用mmap()进行内存映射,同理mmap()方法经过系统调用,对应于Binder驱动层的binder_mmap()方法,该方法会在Binder驱动层创建Binder_buffer对象,并放入当前binder_proc的proc->buffers链表。

第二步 注册成为binder服务的大管家

对应上图中的 第五到 第七步

根据 main() 函数中的 第二步代码 引用链为 binder_become_context_manager(struct binder_state) -> binder_ioctl(bs->fd, BINDER_SET_CONTEXT_MGR, 0) -> binder_ioctl_set_ctx_mgr(struct file *filp)

在 binder_ioctl_set_ctx_mgr()方法中 创建了全局的单例binder_node对象binder_context_mgr_node,并将binder_context_mgr_node的强弱引用各加1.部分代码块如下:

static int binder_ioctl_set_ctx_mgr(struct file *filp)
{
int ret = 0;
struct binder_proc *proc = filp->private_data;
kuid_t curr_euid = current_euid();
//保证只创建一次mgr_node对象
if (binder_context_mgr_node != NULL) {
ret = -EBUSY;
goto out;
}
if (uid_valid(binder_context_mgr_uid)) {
...
} else {
//设置当前线程euid作为Service Manager的uid
binder_context_mgr_uid = curr_euid;
}
//创建ServiceManager实体 终于找到了
binder_context_mgr_node = binder_new_node(proc, 0, 0);
...
binder_context_mgr_node->local_weak_refs++;
binder_context_mgr_node->local_strong_refs++;
binder_context_mgr_node->has_strong_ref = 1;
binder_context_mgr_node->has_weak_ref = 1;
out:
return ret;
}

通过跟踪 我们发现了 binder_new_node () 代码块如下:


static struct binder_node *binder_new_node(struct binder_proc *proc,
binder_uintptr_t ptr,
binder_uintptr_t cookie)
{
struct rb_node **p = &proc->nodes.rb_node;
struct rb_node *parent = NULL;
struct binder_node *node;
//首次进来为空
while (*p) {
parent = *p;
node = rb_entry(parent, struct binder_node, rb_node);

if (ptr < node->ptr)
p = &(*p)->rb_left;
else if (ptr > node->ptr)
p = &(*p)->rb_right;
else
return NULL;
}

//给新创建的binder_node 分配内核空间
node = kzalloc(sizeof(*node), GFP_KERNEL);
if (node == NULL)
return NULL;
binder_stats_created(BINDER_STAT_NODE);
// 将新创建的node对象添加到proc红黑树;
rb_link_node(&node->rb_node, parent, p);
rb_insert_color(&node->rb_node, &proc->nodes);
node->debug_id = ++binder_last_id;
node->proc = proc;
node->ptr = ptr;
node->cookie = cookie;
node->work.type = BINDER_WORK_NODE; //设置binder_work的type
INIT_LIST_HEAD(&node->work.entry);
INIT_LIST_HEAD(&node->async_todo);
return node;
}

在Binder驱动层创建binder_node结构体对象,并将当前binder_proc加入到binder_node的node->proc。并创建binder_node的async_todo和binder_work两个队列。

第三步 无限循环,处理client端发来的请求

对应上图中的 第九到 第十三步

void binder_loop(struct binder_state *bs, binder_handler func) {
int res;
struct binder_write_read bwr;
uint32_t readbuf[32];
bwr.write_size = 0;
bwr.write_consumed = 0;
bwr.write_buffer = 0;
readbuf[0] = BC_ENTER_LOOPER;

//将BC_ENTER_LOOPER命令发送给binder驱动,让Service Manager进入循环
binder_write(bs, readbuf, sizeof(uint32_t));
for (;;) {
bwr.read_size = sizeof(readbuf);
bwr.read_consumed = 0;
bwr.read_buffer = (uintptr_t) readbuf;
//进入循环,不断地binder读写过程
res = ioctl(bs->fd, BINDER_WRITE_READ, &bwr);
if (res < 0) {
break;
}
// 解析binder信息
res = binder_parse(bs, 0, (uintptr_t) readbuf, bwr.read_consumed, func);
if (res == 0) {
break;
}
if (res < 0) {
break;
}
}
}

进入循环读写操作,由main()方法传递过来的参数func指向svcmgr_handler。

binder_write通过ioctl()将BC_ENTER_LOOPER命令发送给binder驱动,此时bwr只有write_buffer有数据,进入binder_thread_write()方法。 接下来进入for循环,执行ioctl(),此时bwr只有read_buffer有数据,那么进入binder_thread_read()方法。

我们通过 binder_write() -> ioctl(bs->fd, BINDER_WRITE_READ, &bwr) -> binder_ioctl_write_read() -> binder_thread_write()

binder_thread_write 方法

static int binder_thread_write(struct binder_proc *proc, struct binder_thread *thread, binder_uintptr_t binder_buffer, size_t size, binder_size_t *consumed) {
uint32_t cmd;
void __user *buffer = (void __user *)(uintptr_t)binder_buffer;
void __user *ptr = buffer + *consumed;
void __user *end = buffer + size;

while (ptr < end && thread->return_error == BR_OK) {
get_user(cmd, (uint32_t __user *)ptr); //获取命令
switch (cmd) {
case BC_ENTER_LOOPER:
//设置该线程的looper状态
thread->looper |= BINDER_LOOPER_STATE_ENTERED;
break;
case ...;
}
}
}

从bwr.write_buffer拿出cmd数据,此处为BC_ENTER_LOOPER. 可见上层本次调用binder_write()方法,主要是完成设置当前线程的looper状态为BINDER_LOOPER_STATE_ENTERED。

第四部分 Binder消息的处理


int binder_parse(struct binder_state *bs, struct binder_io *bio,
uintptr_t ptr, size_t size, binder_handler func)
{
int r = 1;
uintptr_t end = ptr + (uintptr_t) size;

while (ptr < end) {
uint32_t cmd = *(uint32_t *) ptr;
ptr += sizeof(uint32_t);
switch(cmd) {
case BR_NOOP: //无操作,退出循环
break;
case BR_TRANSACTION_COMPLETE:
break;
case BR_INCREFS:
case BR_ACQUIRE:
case BR_RELEASE:
case BR_DECREFS:
ptr += sizeof(struct binder_ptr_cookie);
break;
case BR_TRANSACTION: {
struct binder_transaction_data *txn = (struct binder_transaction_data *) ptr;
...
binder_dump_txn(txn);
if (func) {
unsigned rdata[256/4];
struct binder_io msg;
struct binder_io reply;
int res;
// 对 binder_io 进行初始化设置
bio_init(&reply, rdata, sizeof(rdata), 4);
//从txn解析并复制给binder_io信息
bio_init_from_txn(&msg, txn);
//
res = func(bs, txn, &msg, &reply);
// 向 binder 驱动通信
binder_send_reply(bs, &reply, txn->data.ptr.buffer, res);
}
ptr += sizeof(*txn);
break;
}
case BR_REPLY: {
struct binder_transaction_data *txn = (struct binder_transaction_data *) ptr;
...
binder_dump_txn(txn);
if (bio) {
bio_init_from_txn(bio, txn);
bio = 0;
}
ptr += sizeof(*txn);
r = 0;
break;
}
case BR_DEAD_BINDER: {
struct binder_death *death = (struct binder_death *)(uintptr_t) *(binder_uintptr_t *)ptr;
ptr += sizeof(binder_uintptr_t);
// binder死亡消息
death->func(bs, death->ptr);
break;
}
case BR_FAILED_REPLY:
r = -1;
break;
case BR_DEAD_REPLY:
r = -1;
break;
default:
return -1;
}
}
return r;
}

binder_parse方法,先调用svcmgr_handler(),再然后执行binder_send_reply过程。该方法会调用binder_write进入binder驱动后,将BC_FREE_BUFFER和BC_REPLY命令协议发送给Binder驱动,向client端发送reply. 其中data的数据区中保存的是TYPE为HANDLE.

ServiceManager启动总结

  • 打开binder驱动,并调用binder.c 文件中的mmap()方法分配128k的内存映射空间:binder_open();
  • 通知binder驱动使其成为守护进程:binder_become_context_manager();通过binder驱动中的binder_new_node()方法 创建 ServiceManager 对象实体类
  • 验证selinux权限,判断进程是否有权注册或查看指定服务;
  • 进入循环状态,等待Client端的请求:binder_loop()。
  • 注册服务的过程,根据服务名称,但同一个服务已注册,重新注册前会先移除之前的注册信息;
  • 死亡通知: 当binder所在进程死亡后,会调用binder_release方法,然后调用binder_node_release.这个过程便会发出死亡通知的回调.

--------------------------------------------------- 章节分割线 ---------------------------------------------------

Binder 的一些理解

Binder优点

通过看完上文的 Binder驱动中的工作,我们不妨找一下Binder为何能成为Android 进程交互中流砥柱。

暂且撇开Binder,考虑一下传统的IPC方式中,数据是怎样从发送端到达接收端的呢?通常的做法是,发送方将准备好的数据存放在缓存区中,调用API通过系统调用进入内核中。内核服务程序在内核空间分配内存,将数据从发送方缓存区复制到内核缓存区中。接收方读数据时也要提供一块缓存区,内核将数据从内核缓存区拷贝到接收方提供的缓存区中并唤醒接收线程,完成一次数据发送。这种存储-转发机制有两个缺陷:首先是效率低下,需要做两次拷贝:用户空间->内核空间->用户空间。Linux使用copy_from_user()和copy_to_user()实现这两个跨空间拷贝,在此过程中如果使用了高端内存(high memory),这种拷贝需要临时建立/取消页面映射,造成性能损失。其次是接收数据的缓存要由接收方提供,可接收方不知道到底要多大的缓存才够用,只能开辟尽量大的空间或先调用API接收消息头获得消息体大小,再开辟适当的空间接收消息体。两种做法都有不足,不是浪费空间就是浪费时间。

Binder采用一种全新策略:由Binder驱动负责管理数据接收缓存。我们注意到Binder驱动实现了mmap()系统调用,这对字符设备是比较特殊的,因为mmap()通常用在有物理存储介质的文件系统上,而象Binder这样没有物理介质,纯粹用来通信的字符设备没必要支持mmap()。Binder驱动当然不是为了在物理介质和用户空间做映射,而是用来创建数据接收的缓存空间。先看mmap()是如何使用的:

fd = open("/dev/binder", O_RDWR);

mmap(NULL, MAP_SIZE, PROT_READ, MAP_PRIVATE, fd, 0);

这样Binder的接收方就有了一片大小为MAP_SIZE的接收缓存区。mmap()的返回值是内存映射在用户空间的地址,不过这段空间是由驱动管理,用户不必也不能直接访问(映射类型为PROT_READ,只读映射)。

接收缓存区映射好后就可以做为缓存池接收和存放数据了。前面说过,接收数据包的结构为binder_transaction_data,但这只是消息头,真正的有效负荷位于data.buffer所指向的内存中。这片内存不需要接收方提供,恰恰是来自mmap()映射的这片缓存池。在数据从发送方向接收方拷贝时,驱动会根据发送数据包的大小,使用最佳匹配算法从缓存池中找到一块大小合适的空间,将数据从发送缓存区复制过来。要注意的是,存放binder_transaction_data结构本身以及表4中所有消息的内存空间还是得由接收者提供,但这些数据大小固定,数量也不多,不会给接收方造成不便。映射的缓存池要足够大,因为接收方的线程池可能会同时处理多条并发的交互,每条交互都需要从缓存池中获取目的存储区,一旦缓存池耗竭将产生导致无法预期的后果。

有分配必然有释放。接收方在处理完数据包后,就要通知驱动释放data.buffer所指向的内存区。在介绍Binder协议时已经提到,这是由命令BC_FREE_BUFFER完成的。

通过上面介绍可以看到,驱动为接收方分担了最为繁琐的任务:分配/释放大小不等,难以预测的有效负荷缓存区,而接收方只需要提供缓存来存放大小固定,最大空间可以预测的消息头即可。在效率上,由于mmap()分配的内存是映射在接收方用户空间里的,所有总体效果就相当于对有效负荷数据做了一次从发送方用户空间到接收方用户空间的直接数据拷贝,省去了内核中暂存这个步骤,提升了一倍的性能。顺便再提一点,Linux内核实际上没有从一个用户空间到另一个用户空间直接拷贝的函数,需要先用copy_from_user()拷贝到内核空间,再用copy_to_user()拷贝到另一个用户空间。为了实现用户空间到用户空间的拷贝,mmap()分配的内存除了映射进了接收方进程里,还映射进了内核空间。所以调用copy_from_user()将数据拷贝进内核空间也相当于拷贝进了接收方的用户空间,这就是Binder只需一次拷贝的‘秘密’

Binder 中的线程管理

Binder通信实际上是位于不同进程中的线程之间的通信。假如进程S是Server端,提供Binder实体,线程T1从Client进程C1中通过Binder的引用向进程S发送请求。S为了处理这个请求需要启动线程T2,而此时线程T1处于接收返回数据的等待状态。T2处理完请求就会将处理结果返回给T1,T1被唤醒得到处理结果。在这过程中,T2仿佛T1在进程S中的代理,代表T1执行远程任务,而给T1的感觉就是象穿越到S中执行一段代码又回到了C1。为了使这种穿越更加真实,驱动会将T1的一些属性赋给T2,特别是T1的优先级nice,这样T2会使用和T1类似的时间完成任务。很多资料会用‘线程迁移’来形容这种现象,容易让人产生误解。一来线程根本不可能在进程之间跳来跳去,二来T2除了和T1优先级一样,其它没有相同之处,包括身份,打开文件,栈大小,信号处理,私有数据等。

对于Server进程S,可能会有许多Client同时发起请求,为了提高效率往往开辟线程池并发处理收到的请求。怎样使用线程池实现并发处理呢?这和具体的IPC机制有关。拿socket举例,Server端的socket设置为侦听模式,有一个专门的线程使用该socket侦听来自Client的连接请求,即阻塞在accept()上。这个socket就象一只会生蛋的鸡,一旦收到来自Client的请求就会生一个蛋 – 创建新socket并从accept()返回。侦听线程从线程池中启动一个工作线程并将刚下的蛋交给该线程。后续业务处理就由该线程完成并通过这个单与Client实现交互。

可是对于Binder来说,既没有侦听模式也不会下蛋,怎样管理线程池呢?一种简单的做法是,先创建一堆线程,每个线程都用BINDER_WRITE_READ命令读Binder。这些线程会阻塞在驱动为该Binder设置的等待队列上,一旦有来自Client的数据驱动会从队列中唤醒一个线程来处理。这样做简单直观,省去了线程池,但一开始就创建一堆线程有点浪费资源。于是Binder协议引入了专门命令或消息帮助用户管理线程池,包括:

· INDER_SET_MAX_THREADS // 设置最大线程数

· BC_REGISTER_LOOP // 注册

· BC_ENTER_LOOP // 进入

· BC_EXIT_LOOP // 退出

· BR_SPAWN_LOOPER // 通知线程即将不够使用,创建指令

首先要管理线程池就要知道池子有多大,应用程序通过INDER_SET_MAX_THREADS告诉驱动最多可以创建几个线程。以后每个线程在创建,进入主循环,退出主循环时都要分别使用BC_REGISTER_LOOP,BC_ENTER_LOOP,BC_EXIT_LOOP告知驱动,以便驱动收集和记录当前线程池的状态。每当驱动接收完数据包返回读Binder的线程时,都要检查一下是不是已经没有闲置线程了。如果是,而且线程总数不会超出线程池最大线程数,就会在当前读出的数据包后面再追加一条BR_SPAWN_LOOPER消息,告诉用户线程即将不够用了,请再启动一些,否则下一个请求可能不能及时响应。新线程一启动又会通过BC_xxx_LOOP告知驱动更新状态。这样只要线程没有耗尽,总是有空闲线程在等待队列中随时待命,及时处理请求。

关于工作线程的启动,Binder驱动还做了一点小小的优化。当进程P1的线程T1向进程P2发送请求时,驱动会先查看一下线程T1是否也正在处理来自P2某个线程请求但尚未完成(没有发送回复)。这种情况通常发生在两个进程都有Binder实体并互相对发时请求时。假如驱动在进程P2中发现了这样的线程,比如说T2,就会要求T2来处理T1的这次请求。因为T2既然向T1发送了请求尚未得到返回包,说明T2肯定(或将会)阻塞在读取返回包的状态。这时候可以让T2顺便做点事情,总比等在那里闲着好。而且如果T2不是线程池中的线程还可以为线程池分担部分工作,减少线程池使用率。

↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓ 以下为未完成篇幅

Binder 学习笔记 5

Binder 中服务注册流程

服务注册过程(addService)核心功能:在服务所在进程创建binder_node,在servicemanager进程创建binder_ref。 其中binder_ref的desc再同一个进程内是唯一的:

  • 每个进程binder_proc所记录的binder_ref的handle值是从1开始递增的;
  • 所有进程binder_proc所记录的handle=0的binder_ref都指向service manager;
  • 同一个服务的binder_node在不同进程的binder_ref的handle值可以不同;

Media服务注册的过程涉及到MediaPlayerService(作为Client进程)和Service Manager(作为Service进程),通信流程图如下所示:

444.webp

过程分析:

  1. MediaPlayerService进程调用ioctl()向Binder驱动发送IPC数据,该过程可以理解成一个事务binder_transaction(记为T1),执行当前操作的线程binder_thread(记为thread1),则T1->from_parent=NULL,T1->from = thread1,thread1->transaction_stack=T1。其中IPC数据内容包含:
  • Binder协议为BC_TRANSACTION;
  • Handle等于0;
  • RPC代码为ADD_SERVICE;
  • RPC数据为”media.player”。
  1. Binder驱动收到该Binder请求,生成BR_TRANSACTION命令,选择目标处理该请求的线程,即ServiceManager的binder线程(记为thread2),则 T1->to_parent = NULL,T1->to_thread = thread2。并将整个binder_transaction数据(记为T2)插入到目标线程的todo队列;

  2. Service Manager的线程thread2收到T2后,调用服务注册函数将服务”media.player”注册到服务目录中。当服务注册完成后,生成IPC应答数据(BC_REPLY),T2->form_parent = T1,T2->from = thread2, thread2->transaction_stack = T2。

  3. Binder驱动收到该Binder应答请求,生成BR_REPLY命令,T2->to_parent = T1,T2->to_thread = thread1, thread1->transaction_stack = T2。 在MediaPlayerService收到该命令后,知道服务注册完成便可以正常使用。

整个过程中,BC_TRANSACTION和BR_TRANSACTION过程是一个完整的事务过程;BC_REPLY和BR_REPLY是一个完整的事务过程。

收起阅读 »

熟悉又陌生的Handler

熟悉又陌生的Handler-3nativeInit:在上文中,关于Handler三件套的创建流程,第一个涉及到的JNI调用就是MessageQueue的nativeInit方法。MessageQueue(boolean quitAllowed) { m...
继续阅读 »


熟悉又陌生的Handler-3

nativeInit:

在上文中,关于Handler三件套的创建流程,第一个涉及到的JNI调用就是MessageQueue的nativeInit方法。

MessageQueue(boolean quitAllowed) {
mQuitAllowed = quitAllowed;
mPtr = nativeInit();
}

具体实现在framework/base/core/jni/android_os_MessageQueue.cpp中:

static jlong android_os_MessageQueue_nativeInit(JNIEnv* env, jclass clazz) {
// 首先创建NativeMessageQueue对象,该对象持有Native侧的Looper对象
NativeMessageQueue* nativeMessageQueue = new NativeMessageQueue();
//如果创建失败,直接异常
if (!nativeMessageQueue) {
jniThrowRuntimeException(env, "Unable to allocate native queue");
return 0;
}
// 增加NativeMessageQueue的引用计数
nativeMessageQueue->incStrong(env);
// 返回nativeMessageQueue这个指针给Java层
return reinterpret_cast<jlong>(nativeMessageQueue);
}

在NativeMessageQueue的构造函数中:

NativeMessageQueue::NativeMessageQueue() :
mPollEnv(NULL), mPollObj(NULL), mExceptionObj(NULL) {
// 通过TLS(Thread Local Storage线程局部存储)获取当前线程的Native的Looper对象
mLooper = Looper::getForThread();
if (mLooper == NULL) {
// 如果没有,那么会去创建一个Native的Looper对象
mLooper = new Looper(false);
// 并将创建的Looper对象保存到TLS中
Looper::setForThread(mLooper);
}
}

需要注意这里的Looper对象和Java层的Looper对象并没有什么直接关联。在其构造方法中,最关联的两件事:

Looper::Looper(bool allowNonCallbacks)
: mAllowNonCallbacks(allowNonCallbacks),
mSendingMessage(false),
mPolling(false),
mEpollRebuildRequired(false),
mNextRequestSeq(0),
mResponseIndex(0),
mNextMessageUptime(LLONG_MAX) {
// 初始化表示唤醒事件的文件描述符mWakeEventFd
// eventfd这个系统调用用于创建或者打开一个eventfd的文件,类似于文件的open操作
// 这里传入的初始值为0,然后设置的标志位为
// EFD_CLOEXEC:FD_CLOEXEC,简单说就是fork子进程时不继承,对于多线程的程序设上这个值不会有错的。
// EFD_NONBLOCK:
// 文件会被设置成O_NONBLOCK(非阻塞IO,读取不到数据的时候或写入缓冲区满了会return -1),
// 而不是阻塞调用,一般要设置。
mWakeEventFd.reset(eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC));
AutoMutex _l(mLock);
// 重新创建当前Looper的Epoll事件
rebuildEpollLocked();
}

rebuildEpollLocked实现如下:

void Looper::rebuildEpollLocked() {
// 如果当前Looper已经有了EpollFd,即已经有了旧的epoll实例,那么先重置一下
if (mEpollFd >= 0) {
mEpollFd.reset();
}
// 创建新的Epoll实例
mEpollFd.reset(epoll_create1(EPOLL_CLOEXEC));
struct epoll_event eventItem;
// 初始化eventItem占用的内存空间
memset(& eventItem, 0, sizeof(epoll_event)); // zero out unused members of data field union
// EPOLLIN :表示对应的文件描述符可以读
eventItem.events = EPOLLIN;
eventItem.data.fd = mWakeEventFd.get();
// 调用epoll_ctl操作mEpollFd对应的Epoll实例,将mWakeEventFd(唤醒事件)
// 添加到mEpoll对应的epoll实例上
int result = epoll_ctl(mEpollFd.get(), EPOLL_CTL_ADD, mWakeEventFd.get(), &eventItem);
for (size_t i = 0; i < mRequests.size(); i++) {
const Request& request = mRequests.valueAt(i);
struct epoll_event eventItem;
request.initEventItem(&eventItem);
// 将Request也一并添加到epoll实例上
int epollResult = epoll_ctl(mEpollFd.get(), EPOLL_CTL_ADD, request.fd, &eventItem);
if (epollResult < 0) {
ALOGE("Error adding epoll events for fd %d while rebuilding epoll set: %s",
request.fd, strerror(errno));
}
}
}

epoll_event结构体如下:

struct epoll_event {
uint32_t events;
epoll_data_t data;
}

typedef union epoll_data {
void* ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;

events成员变量:可以是以下几个宏的集合:

  • EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
  • EPOLLOUT:表示对应的文件描述符可以写;
  • EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
  • EPOLLERR:表示对应的文件描述符发生错误;
  • EPOLLHUP:表示对应的文件描述符被挂断;
  • EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。
  • EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里。

既然Java层的MessageQueue在创建的时候,有创建Native层的MessageQueue,那么同样的Java层MQ在销毁的时候,也会触发NativeMessageQueue的销毁,Native层MQ的销毁比较简单,实质上就是Native层一个对象的清除:

  1. 移除对象的引用关系。
  2. delete调用清除对象的内存空间。

nativeWake:

从上文分析中,我们知道,在Java层当我们调用MessageQueue.enqueueMessage的时候,在Java层觉得需要唤醒消息队列的时候,会调用nativeWake这个native方法:

static void android_os_MessageQueue_nativeWake(JNIEnv* env, jclass clazz, jlong ptr) {
NativeMessageQueue* nativeMessageQueue = reinterpret_cast<NativeMessageQueue*>(ptr);
// 调用NativeMessageQueue的wake方法。
nativeMessageQueue->wake();
}

NativeMessageQueue的wake方法就是调Native Looper的wake方法:

void Looper::wake() {
uint64_t inc = 1;
// 向管道mWakeEventFd写入字符1
ssize_t nWrite = TEMP_FAILURE_RETRY(write(mWakeEventFd.get(), &inc, sizeof(uint64_t)));
if (nWrite != sizeof(uint64_t)) {
if (errno != EAGAIN) {
LOG_ALWAYS_FATAL("Could not write wake signal to fd %d (returned %zd): %s",
mWakeEventFd.get(), nWrite, strerror(errno));
}
}
}

TEMP_FAILURE_RETRY这个宏表达式的作用是,对传入的表达式求值,当传入的表达式求值为-1,则表示失败,当表达式返回值-1且设置了错误码为EINITR(4),那么他会一直重试,直到成功。

nativePollOnce:

从上文分析中,我们知道,在Java层消息队列处理Message之前,会先调用nativePollOnce,处理Native层的消息:

nativePollOnce(ptr, nextPollTimeoutMillis);

ptr是之前在Native层创建的MessageQueue的“指针”,nextPollTimeoutMillis表示下一条消息要被取出的时间。

在android_os_MessageQueue.cpp中,nativePollOnce实现如下:

static void android_os_MessageQueue_nativePollOnce(JNIEnv* env, jobject obj,
jlong ptr, jint timeoutMillis) {
// 将Java层传递过来的mPtr转换为NativeMessageQueue指针
NativeMessageQueue* nativeMessageQueue = reinterpret_cast<NativeMessageQueue*>(ptr);
// 调用nativeMessageQueue的pollOnce方法
nativeMessageQueue->pollOnce(env, obj, timeoutMillis);
}

pollOnce:

void NativeMessageQueue::pollOnce(JNIEnv* env, jobject pollObj, int timeoutMillis) {
// ...
// 调用到Looper的pollOnce方法
mLooper->pollOnce(timeoutMillis);
// ...
}

Looper的pollOnce最终实现在system/core/libutils/Looper.cpp:

int Looper::pollOnce(int timeoutMillis, int* outFd, int* outEvents, void** outData) {
int result = 0;
for (;;) {
// 首先处理Native层的Response
while (mResponseIndex < mResponses.size()) {
const Response& response = mResponses.itemAt(mResponseIndex++)
// 当ident>=0的时候,表示没有callback
int ident = response.request.ident;
if (ident >= 0) {
int fd = response.request.fd;
int events = response.events;
void* data = response.request.data;
if (outFd != nullptr) *outFd = fd;
if (outEvents != nullptr) *outEvents = events;
if (outData != nullptr) *outData = data;
return ident;
}
}
// 如果有result,那么就退出了
if (result != 0) {
if (outFd != nullptr) *outFd = 0;
if (outEvents != nullptr) *outEvents = 0;
if (outData != nullptr) *outData = nullptr;
return result;
}
// 调用pollInner
result = pollInner(timeoutMillis);
}
}

Response和Request的结构体如下:

    struct Request {
// request关联的文件描述符
int fd;
// requestId,当为POLL_CALLBACK(-2)的时候,表示有callback
int ident;
int events;
int seq;
// request的处理回调
sp<LooperCallback> callback;
void* data;
void initEventItem(struct epoll_event* eventItem) const;
};
struct Response {
int events;
Request request;
};

Looper::pollInner的实现如下,内部首先是调用epoll_wait这个阻塞方法,获取Epoll事件发生的数量,然后根据这个数量,

int Looper::pollInner(int timeoutMillis) {
// Adjust the timeout based on when the next message is due.
if (timeoutMillis != 0 && mNextMessageUptime != LLONG_MAX) {
nsecs_t now = systemTime(SYSTEM_TIME_MONOTONIC);
int messageTimeoutMillis = toMillisecondTimeoutDelay(now, mNextMessageUptime);
if (messageTimeoutMillis >= 0
&& (timeoutMillis < 0 || messageTimeoutMillis < timeoutMillis)) {
timeoutMillis = messageTimeoutMillis;
}
}

// Poll.
int result = POLL_WAKE;
// 上面的分析已知,在pollInner被调用之前,mResponses已经都被处理完了
mResponses.clear();
mResponseIndex = 0;
// 即将开始epoll轮询。
mPolling = true;
struct epoll_event eventItems[EPOLL_MAX_EVENTS];
// 等待epoll_wait系统调用返回,返回timeoutMillis时间内文件描述符mEpollFd上发生的epoll事件数量
int eventCount = epoll_wait(mEpollFd.get(), eventItems, EPOLL_MAX_EVENTS, timeoutMillis);
// 轮询结束,下面开始处理收到的事件。
mPolling = false;
// Acquire lock.
mLock.lock();

// 比如发生了什么异常,需要重新创建Epoll机制
if (mEpollRebuildRequired) {
mEpollRebuildRequired = false;
rebuildEpollLocked();
goto Done;
}

// 当epoll事件个数小于0的时候,即认为发生了异常,跳转到Done处继续执行
if (eventCount < 0) {
if (errno == EINTR) {
goto Done;
}
result = POLL_ERROR;
goto Done;
}

// 当epoll事件等于0的时候,表示轮询超时了,直接跳转到Done处继续执行
if (eventCount == 0) {
result = POLL_TIMEOUT;
goto Done;
}
// 开始循环遍历,处理所有的event
for (int i = 0; i < eventCount; i++) {
// 获取一个事件的FD
int fd = eventItems[i].data.fd;
uint32_t epollEvents = eventItems[i].events;
// 如果是唤醒事件
if (fd == mWakeEventFd.get()) {
if (epollEvents & EPOLLIN) {
// 此时已经被唤醒了,读取并清空管道中的数据
awoken();
} else {
ALOGW("Ignoring unexpected epoll events 0x%x on wake event fd.", epollEvents);
}
} else {
// 通过文件描述符,找到对应的Request索引
ssize_t requestIndex = mRequests.indexOfKey(fd);
if (requestIndex >= 0) {
int events = 0;
if (epollEvents & EPOLLIN) events |= EVENT_INPUT;
if (epollEvents & EPOLLOUT) events |= EVENT_OUTPUT;
if (epollEvents & EPOLLERR) events |= EVENT_ERROR;
if (epollEvents & EPOLLHUP) events |= EVENT_HANGUP;
// 将对应的request封装成Response对象并push到mRequests这和Vector中
pushResponse(events, mRequests.valueAt(requestIndex));
} else {
ALOGW("Ignoring unexpected epoll events 0x%x on fd %d that is "
"no longer registered.", epollEvents, fd);
}
}
}
Done: ;
// Response事件都处理完了,接下来处理Native的Message事件。
mNextMessageUptime = LLONG_MAX;
// mMessageEnvelopes是一个Vector,MessageEnvelopes如其名消息信封
// 封装了Message和MessageHandler对象
// Message表示消息,MessageHandler定义了一个handleMessage方法
// 通过调用Looper::sendMessageXX可以发送一条Native Message
while (mMessageEnvelopes.size() != 0) {
nsecs_t now = systemTime(SYSTEM_TIME_MONOTONIC);
const MessageEnvelope& messageEnvelope = mMessageEnvelopes.itemAt(0);
if (messageEnvelope.uptime <= now) {
// 取出来的一条消息到了可以被处理的时间,那么就移除并执行
// 对应MessageHandler的handleMessage方法。
{ // obtain handler
sp<MessageHandler> handler = messageEnvelope.handler;
Message message = messageEnvelope.message;
mMessageEnvelopes.removeAt(0);
mSendingMessage = true;
mLock.unlock();
handler->handleMessage(message);
} // release handler
mLock.lock();
mSendingMessage = false;
result = POLL_CALLBACK;
} else {
// 而如果队列头部的消息尚未到达需要被处理的时间
// 那么队列需要挂起到这个头部消息能被处理时候为止
mNextMessageUptime = messageEnvelope.uptime;
break;
}
}
// Release lock.
mLock.unlock();
// 接着处理上面push进来的mResponses,即Request
for (size_t i = 0; i < mResponses.size(); i++) {
Response& response = mResponses.editItemAt(i);
// 有callback
if (response.request.ident == POLL_CALLBACK) {
int fd = response.request.fd;
int events = response.events;
void* data = response.request.data;
// 执行callback
int callbackResult = response.request.callback->handleEvent(fd, events, data)
if (callbackResult == 0) {
removeFd(fd, response.request.seq);
}
// 清除callback的引用
response.request.callback.clear();
result = POLL_CALLBACK;
}
}
return result;
}

整个Native层Looper机制的重中之重就是Looper::pollInner中的epoll_wait这个系统调用,这个调用在消息队列没有工作需要处理的时候,会阻塞当前线程,释放系统资源,也就是说,Looper的死循环机制并不会一直占用系统资源,在没有任务需要处理的时候,主线程是阻塞状态的,因此并不会造成资源占用过高。

或者更通俗易懂的,我们看到的Looper.loop开启了一个死循环,这个死循环的的确确就是一个死循环。但是特别之处在于,不同于我们写一个无限循环,CPU会一直执行,然后导致资源占用激增,Looper.loop这个死循环,在没有消息需要处理的时候,就会阻塞停止,不再往epoll_wait后面执行。

而我们感受不到主线程停止,是因为,我们写的一个代码,执行都是被动的,我们在子线程post一个message,MessageQueue接收消息,主线程的Looper.loop执行代码,执行完代码后取下一条Message,没有Message,主线程继续阻塞。我们写的代码执行了,当然是感受不到主线程阻塞的。

小结:

通过对Native的pollOnce的分析,我们知道Android的消息处理机制实际上纵跨Java层和Native层的。

Java层和Native层,通过上面的一些JNI调用以及mPtr这个关键指针,将Java层的MessageQueue和Native的MessageQueue进行关联,这样在Java层的消息机制进行运转的时候,Native层的消息机制也能一起运转。

消息处理的流程是先处理Native的Message,然后再处理Native的Request,最后才会是pollOnce结束之后,处理Java层的Message,所以有时候Java层的消息并不多但是响应时间比较慢可能是Native层的消息机制导致的。

收起阅读 »

解析android源码中dex文件的几个关键函数

dex简介dex文件作为android的的主要格式,它是可以直接在Dalvik虚拟机中加载运行的文件。 dex 文件可以分为3个模块,头文件(header)、索引区(xxxx_ids)、数据区(data)。 我们在进行对android加固和脱壳的时候都需要进行...
继续阅读 »

dex简介

dex文件作为android的的主要格式,它是可以直接在Dalvik虚拟机中加载运行的文件。 dex 文件可以分为3个模块,头文件(header)、索引区(xxxx_ids)、数据区(data)。 我们在进行对android加固和脱壳的时候都需要进行和dex文件格式打交道。 它在系统的定义是定义在/art/runtime/dex_file文件中的。下面对dex文件格式的几个关键函数进行分析。

CheckMagicAndVersion 函数

//判断dex文件中魔法值dex后面所跟的版本
bool DexFile::CheckMagicAndVersion(std::string* error_msg) const {
//dex文件魔法值是存在在头文件header中的,所有通过header就可以获取魔法值
if (!IsMagicValid(header_->magic_)) {
std::ostringstream oss;
oss << "Unrecognized magic number in " << GetLocation() << ":"
<< " " << header_->magic_[0]
<< " " << header_->magic_[1]
<< " " << header_->magic_[2]
<< " " << header_->magic_[3];
*error_msg = oss.str();
return false;
}
//判断获取的魔法值是否有效
if (!IsVersionValid(header_->magic_)) {
std::ostringstream oss;
oss << "Unrecognized version number in " << GetLocation() << ":"
<< " " << header_->magic_[4]
<< " " << header_->magic_[5]
<< " " << header_->magic_[6]
<< " " << header_->magic_[7];
*error_msg = oss.str();
return false;
}
return true;
}

DexFile函数

//获取解析dex文件格式
DexFile::DexFile(const byte* base, size_t size,
const std::string& location,
uint32_t location_checksum,
MemMap* mem_map)

: begin_(base),
size_(size),
location_(location),
location_checksum_(location_checksum),
mem_map_(mem_map),
header_(reinterpret_cast<const Header*>(base)),
string_ids_(reinterpret_cast<const StringId*>(base + header_->string_ids_off_)),
type_ids_(reinterpret_cast<const TypeId*>(base + header_->type_ids_off_)),
field_ids_(reinterpret_cast<const FieldId*>(base + header_->field_ids_off_)),
method_ids_(reinterpret_cast<const MethodId*>(base + header_->method_ids_off_)),
proto_ids_(reinterpret_cast<const ProtoId*>(base + header_->proto_ids_off_)),
class_defs_(reinterpret_cast<const ClassDef*>(base + header_->class_defs_off_)),
find_class_def_misses_(0),
class_def_index_(nullptr),
build_class_def_index_mutex_("DexFile index creation mutex") {
CHECK(begin_ != NULL) << GetLocation();
CHECK_GT(size_, 0U) << GetLocation();
}

FindClassDef 函数

//查找dex文件中的class_ids数据
const DexFile::ClassDef* DexFile::FindClassDef(uint16_t type_idx) const {
//获取class的数量
size_t num_class_defs = NumClassDefs();
for (size_t i = 0; i < num_class_defs; ++i) {
const ClassDef& class_def = GetClassDef(i);
if (class_def.class_idx_ == type_idx) {
return &class_def;
}
}
return NULL;
}

FindFieldId 函数

//查找dex文件中的fileId
const DexFile::FieldId* DexFile::FindFieldId(const DexFile::TypeId& declaring_klass,
const DexFile::StringId& name,
const DexFile::TypeId& type) const {
// Binary search MethodIds knowing that they are sorted by class_idx, name_idx then proto_idx
const uint16_t class_idx = GetIndexForTypeId(declaring_klass);
const uint32_t name_idx = GetIndexForStringId(name);
const uint16_t type_idx = GetIndexForTypeId(type);
int32_t lo = 0;
int32_t hi = NumFieldIds() - 1;
while (hi >= lo) {
int32_t mid = (hi + lo) / 2;
//获取FileId数据
const DexFile::FieldId& field = GetFieldId(mid);
if (class_idx > field.class_idx_) {
lo = mid + 1;
} else if (class_idx < field.class_idx_) {
hi = mid - 1;
} else {
if (name_idx > field.name_idx_) {
lo = mid + 1;
} else if (name_idx < field.name_idx_) {
hi = mid - 1;
} else {
if (type_idx > field.type_idx_) {
lo = mid + 1;
} else if (type_idx < field.type_idx_) {
hi = mid - 1;
} else {
//成功返回获取到的fileId数据
return &field;
}
}
}
}
return NULL;
}

FindMethodId 函数

该函数多所对应的是dex文件中的Method table数据

//查找dex文件中的FindMethodId数据
const DexFile::MethodId* DexFile::FindMethodId(const DexFile::TypeId& declaring_klass,
const DexFile::StringId& name,
const DexFile::ProtoId& signature) const {
// Binary search MethodIds knowing that they are sorted by class_idx, name_idx then proto_idx
const uint16_t class_idx = GetIndexForTypeId(declaring_klass);
const uint32_t name_idx = GetIndexForStringId(name);
const uint16_t proto_idx = GetIndexForProtoId(signature);
int32_t lo = 0;
//获取dex文件中的所有MethodIds的数量
int32_t hi = NumMethodIds() - 1;
//当数量大于0
while (hi >= lo) {
//折半的方式进去一个个获取
int32_t mid = (hi + lo) / 2;
const DexFile::MethodId& method = GetMethodId(mid);
if (class_idx > method.class_idx_) {
lo = mid + 1;
} else if (class_idx < method.class_idx_) {
hi = mid - 1;
} else {
if (name_idx > method.name_idx_) {
lo = mid + 1;
} else if (name_idx < method.name_idx_) {
hi = mid - 1;
} else {
if (proto_idx > method.proto_idx_) {
lo = mid + 1;
} else if (proto_idx < method.proto_idx_) {
hi = mid - 1;
} else {
return &method;
}
}
}
}
return NULL;
}

FindStringId 函数

该函数对应的是在dex文件中的String table上

//查找dex文件中的StringId数据
const DexFile::StringId* DexFile::FindStringId(const char* string) const {
int32_t lo = 0;
//获取dex文件中的所有StringId是数量
int32_t hi = NumStringIds() - 1;
while (hi >= lo) {
int32_t mid = (hi + lo) / 2;
const DexFile::StringId& str_id = GetStringId(mid);
const char* str = GetStringData(str_id);
int compare = CompareModifiedUtf8ToModifiedUtf8AsUtf16CodePointValues(string, str);
if (compare > 0) {
lo = mid + 1;
} else if (compare < 0) {
hi = mid - 1;
} else {
return &str_id;
}
}
return NULL;
}

收起阅读 »

Android Java 虚拟机

1. 概述 1.1 Java 虚拟机家族1.1.1 HotSpot VMOracle JDK 和 OpenJDK 中自带的虚拟机,最主流和使用范围最广的 Java 虚拟机。1.1.2 J9 VM1.1.3 Zing VM1.2 Java 虚拟机执行流...
继续阅读 »

1. 概述

image.png image.png

1.1 Java 虚拟机家族

1.1.1 HotSpot VM

Oracle JDK 和 OpenJDK 中自带的虚拟机,最主流和使用范围最广的 Java 虚拟机。

1.1.2 J9 VM

1.1.3 Zing VM

1.2 Java 虚拟机执行流程

当执行一个 Java 程序时,它的执行流程图如下:

  • 编译时环境
  • 运行时环境

image.png

2. Java 虚拟机结构

image.png

2.1 Class 文件格式

image.png

Class 文件格式: image.png

2.2 类的生命周期

image.png

image.png

2.3 类加载系统

  • Bootstrap ClassLoader(引导类加载器)
  • Extensions ClassLoader(拓展类加载器)
  • Application ClassLoader(应用程序类加载器)/ System ClassLoader(系统类加载器)

image.png

image.png

2.4 运行时数据区域

这些数据区域分别为程序计数器、Java 虚拟机栈、本地方法栈、Java 堆和方法区。

2.4.1 程序计数器

image.png

image.png

2.4.2 Java 虚拟机栈

image.png

2.4.3 本地方法栈

image.png

2.4.4 Java 堆

image.png

image.png

2.4.5 方法区

image.png

2.4.6 运行时常量池

image.png

3. 对象的创建

当虚拟机接收到一个 new 指令时,它会做如下的操作:

image.png

image.png

4. 对象的堆内存布局

image.png

image.png

5. oop-klass 模型

image.png

image.png

image.png

image.png

6. 垃圾标记算法

image.png

6.1 Java 中的引用

  • 强引用
  • 软引用
  • 弱引用
  • 虚引用

image.png

image.png

6.2 引用计数法

image.png

image.png

image.png

缺点:引用计数算法没有解决对象之间相互循环引用的问题。

6.3 根搜索算法

image.png

image.png

优点:解决了已经死亡的对象因为相互引用而不能被回收。

7. Java 对象在虚拟机中的生命周期

image.png

image.png

8. 垃圾收集算法

8.1 标记-清除算法

image.png

image.png

8.2 复制算法

image.png

image.png

8.3 标记-压缩算法

image.png

image.png

8.4 分代收集算法

image.png

8.4.1 分代收集

image.png

image.png

收起阅读 »

Android 多返回栈技术详解

用户通过系统返回按钮导航回去的一组页面,在开发中被称为返回栈 (back stack)。多返回栈即一堆 "返回栈",对多返回栈的支持是在 Navigation 2.4.0-alpha01 和 Fragment 1.4.0-alpha01 中开始的。本文将为您展...
继续阅读 »

用户通过系统返回按钮导航回去的一组页面,在开发中被称为返回栈 (back stack)。多返回栈即一堆 "返回栈",对多返回栈的支持是在 Navigation 2.4.0-alpha01  Fragment 1.4.0-alpha01 中开始的。本文将为您展开多返回栈的技术详解。

系统返回按钮的乐趣

无论您在使用 Android 全新的 手势导航 还是传统的导航栏,用户的 "返回" 操作是 Android 用户体验中关键的一环,把握好返回功能的设计可以使应用更加贴近整个生态系统。

在最简单的应用场景中,系统返回按钮仅仅 finish 您的 Activity。在过去您可能需要覆写 Activity 的 onBackPressed() 方法来自定义返回操作,而在 2021 年您无需再这样操作。我们已经在 OnBackPressedDispatcher 中提供了 针对自定义返回导航的 API。实际上这与 FragmentManager 和 NavController 中 已经 添加的 API 相同。

这意味着当您使用 Fragments 或 Navigation 时,它们会通过 OnBackPressedDispatcher 来确保您调用了它们返回栈的 API,系统的返回按钮会将您推入返回栈的页面逐层返回。

多返回栈不会改变这个基本逻辑。系统的返回按钮仍然是一个单向指令 —— "返回"。这对多返回栈 API 的实现机制有深远影响。

Fragment 中的多返回栈

在 surface 层级,对于 多返回栈的支持 貌似很直接,但其实需要额外解释一下 "Fragment 返回栈" 到底是什么。FragmentManager 的返回栈其实包含的不是 Fragment,而是由 Fragment 事务组成的。更准确地说,是由那些调用了 addToBackStack(String name) API 的事务组成的。

这就意味着当您调用 commit() 提交了一个调用过 addToBackStack() 方法的 Fragment 事务时,FragmentManager 会执行所有您在事务中所指定的操作 (比如 替换操作),从而将每个 Fragment 转换为预期的状态。然后 FragmentManager 会将该事务作为它返回栈的一部分。

当您调用 popBackStack() 方法时 (无论是直接调用,还是通过系统返回键以 FragmentManager 内部机制调用),Fragment 返回栈的最上层事务会从栈中弹出 -- 比如新添加的 Fragment 会被移除,隐藏的 Fragment 会显示。这会使得 FragmentManager 恢复到最初提交 Fragment 事务之前的状态。

作者注: 这里有一个非常重要的事情需要大家注意,在同一个 FragmentManager 中绝对不应该将含有 addToBackStack() 的事务和不含的事务混在一起: 返回栈的事务无法察觉返回栈之外的 Fragment 事务的修改 —— 当您从堆栈弹出一个非常不确定的元素时,这些事务从下层替换出来的时候会撤销之前未添加到返回栈的修改。

也就是说 popBackStack() 变成了销毁操作: 任何已添加的 Fragment 在事务被弹出的时候都会丢失它的状态。换言之,您会失去视图的状态,任何所保存的实例状态 (Saved Instance State),并且任何绑定到该 Fragment 的 ViewModel 实例都会被清除。这也是该 API 和新的 saveBackStack() 方法之间的主要区别。saveBackStack() 可以实现弹出事务所实现的返回效果,此外它还可以确保视图状态、已保存的实例状态,以及 ViewModel 实例能够在销毁时被保存。这使得 restoreBackStack() API 后续可以通过已保存的状态重建这些事务和它们的 Fragment,并且高效 "重现" 已保存的全部细节。太神奇了!

而实现这个目的必须要解决大量技术上的问题。

排除 Fragment 在技术上的障碍

虽然 Fragment 总是会保存 Fragment 的视图状态,但是 Fragment 的 onSaveInstanceState() 方法只有在 Activity 的 onSaveInstanceState() 被调用时才会被调用。为了能够保证调用 saveBackStack() 时 SavedInstanceState 会被保存,我们  需要在 Fragment 生命周期切换 的正确时机注入对 onSaveInstanceState() 的调用。我们不能调用得太早 (您的 Fragment 不应该在 STARTED 状态下保存状态),也不能调用得太晚 (您需要在 Fragment 被销毁之前保存状态)。

这样的前提条件就开启了需要 解决 FragmentManager 转换到对应状态的问题,以此来保障有一个地方能够将 Fragment 转换为所需状态,并且处理可重入行为和 Fragment 内部的状态转换。

在 Fragment 的重构工作进行了 6 个月,进行了 35 次修改时,发现 Postponed Fragment 功能已经严重损坏,这一问题使得被推迟的事务处于一个中间状态 —— 既没有被提交也并不是未被提交。之后的 65 个修改和 5 个月的时间里,我们几乎重写了 FragmentManager 管理状态、延迟状态切换和动画的内部代码,具体请参见我们之前的文章《全新的 Fragment: 使用新的状态管理器》。

Fragment 中值得期待的地方

随着技术问题的逐步解决,包括更加可靠和更易理解的 FragmentManager,我们新增加了两个 API: saveBackStack() 和 restoreBackStack()

如果您不使用这些新增 API,则一切照旧: 单个 FragmentManager 返回栈和之前的功能相同。现有的 addToBackStack() 保持不变 —— 您可以将 name 赋值为 null 或者任意 name。然而,当您使用多返回栈时,name 的作用就非常重要了: 在您调用 saveBackStack() 和之后的 restoreBackStack() 方法时,它将作为 Fragment 事务的唯一的 key。

举个例子,会更容易理解。比如您已经添加了一个初始的 Fragment 到 Activity,然后提交了两个事务,每个事务中包含一个单独的 replace 操作:

// 这是用户看到的初始的 Fragment
fragmentManager.commit {
setReorderingAllowed(true)
replace(R.id.fragment_container)
}
// 然后,响应用户操作,我们在返回栈中增加了两个事务
fragmentManager.commit {
setReorderingAllowed(true)
replace(R.id.fragment_container)
addToBackStack(“profile”)
}
fragmentManager.commit {
setReorderingAllowed(true)
replace(R.id.fragment_container)
addToBackStack(“edit_profile”)
}

也就是说我们的 FragmentManager 会变成这样:

△ 提交三次之后的 FragmentManager 的状态

△ 提交三次之后的 FragmentManager 的状态

比如说我们希望将 profile 页换出返回栈,然后切换到通知 Fragment。这就需要调用 saveBackStack() 并且紧跟一个新的事务:

fragmentManager.saveBackStack("profile")
fragmentManager.commit {
setReorderingAllowed(true)
replace(R.id.fragment_container)
addToBackStack("notifications")
}

现在我们添加 ProfileFragment 的事务和添加 EditProfileFragment 的事务都保存在 "profile" 关键字下。这些 Fragment 已经完全将状态保存,并且 FragmentManager 会随同事务状态一起保持它们的状态。很重要的一点: 这些 Fragment 的实例并不在内存中或者在 FragmentManager 中 —— 存在的仅仅只有状态 (以及任何以 ViewModel 实例形式存在的非配置状态)。

△ 我们保存 profile 返回栈并且添加一个新的 commit 后的 FragmentManager 状态

△ 我们保存 profile 返回栈并且添加一个新的 commit 后的 FragmentManager 状态

替换回来非常简单: 我们可以在 "notifications" 事务中同样调用 saveBackStack() 操作,然后调用 restoreBackStack():

fragmentManager.saveBackStack(“notifications”)
fragmentManager.restoreBackStack(“profile”)

这两个堆栈项高效地交换了位置:

△ 交换堆栈项后的 FragmentManager 状

△ 交换堆栈项后的 FragmentManager 状态

维持一个单独且活跃的返回栈并且将事务在其中交换,这保证了当返回按钮被点击时,FragmentManager 和系统的其他部分可以保持一致的响应。实际上,整个逻辑并未改变,同之前一样,仍然弹出 Fragment 返回栈的最后一个事务。

这些 API 都特意按照最小化设计,尽管它们会产生潜在的影响。这使得开发者可以基于这些接口设计自己的结构,而无需通过任何非常规的方式保存 Fragment 的视图状态、已保存的实例状态、非配置的状态。

当然了,如果您不希望在这些 API 之上构建您的框架,那么可以使用我们所提供的框架进行开发。

使用 Navigation 将多返回栈适配到任意屏幕类型

Navigation Component 最初 是作为通用运行时组件进行开发的,其中不涉及 View、Fragment、Composable 或者其他屏幕显示相关类型及您可能会在 Activity 中实现的 "目的地界面"。然而,NavHost 接口 的实现中需要考虑这些内容,通过它添加一个或者多个 Navigator 实例时,这些实例 确实 清楚如何与特定类型的目的地进行交互。

这也就意味着与 Fragment 的交互逻辑全部封装在了 navigation-fragment 开发库和它其中的 FragmentNavigator 与 DialogFragmentNavigator 中。类似的,与 Composable 的交互逻辑被封装在完全独立的 navigation-compose 开发库和它的 ComposeNavigator 中。这里的抽象设计意味着如果您希望仅仅通过 Composable 构建您的应用,那么当您使用 Navigation Compose 时无需任何涉及到 Fragment 的依赖。

该级别的分离意味着 Navigation 中有两个层次来实现多返回栈:

  • 保存独立的 NavBackStackEntry 实例状态,这些实例组成了 NavController 返回栈。这是属于 NavController 的职责。
  • 保存 Navigator 针对每个 NavBackStackEntry 的特定状态 (比如与 FragmentNavigator 目的地相关联的 Fragment)。这是属于 Navigator 的职责。

仍需特别注意那些 尚未 更新的 Navigator,它们无法支持保存自身状态。底层的 Navigator API 已经整体重写来支持状态保存 (您需要覆写新增的 navigate() 和 popBackStack() API 的重载方法,而不是覆写之前的版本),即使 Navigator 并未更新,NavController 仍会保存 NavBackStackEntry 的状态 (在 Jetpack 世界中向后兼容是非常重要的)。

备注: 通过绑定 TestNavigatorState 使其成为一个 mini-NavController 可以实现在新的 Navigator API 上更轻松、独立地测试您自定义的 Navigator

如果您仅仅在应用中使用 Navigation,那么 Navigator 这个层面更多的是实现细节,而不是您需要直接与之交互的内容。可以这么说,我们已经完成了将 FragmentNavigator 和 ComposeNavigator 迁移到新的 Navigator API 的工作,使其能够正确地保存和恢复它们的状态,在这个层面上您无需再做任何额外工作。

在 Navigation 中启用多返回栈

如果您正在使用 NavigationUI,它是用于连接您的 NavController 到 Material 视图组件的一系列专用助手,您会发现对于菜单项、BottomNavigationView (现在叫 NavigationRailView) 和 NavigationView,多返回栈是 默认启用 的。这就意味着结合 navigation-fragment 和 navigation-ui 使用就可以。

NavigationUI API 是基于 Navigation 的其他公共 API 构建的,确保您可以准确地为自定义组件构建您自己的版本。保证您可以构建所需的自定义组件。启用保存和恢复返回栈的 API 也不例外,在 Navigation XML 中通过 NavOptions 上的新 API,也就是 navOptions Kotlin DSL,以及 popBackStack() 的重载方法可以帮助您指定 pop 操作保存状态或者指定 navigate 操作来恢复之前已保存的状态。

比如,在 Compose 中,任何全局的导航模式 (无论是底部导航栏、导航边栏、抽屉式导航栏或者任何您能想到的形式) 都可以使用我们在与 底部导航栏集成 所介绍的相同的技术,并且结合 saveState 和 restoreState 属性一起调用 navigate():

onClick = {
navController.navigate(screen.route) {
// 当用户选择子项时在返回栈中弹出到导航图中的起始目的地
// 来避免太过臃肿的目的地堆栈
popUpTo(navController.graph.findStartDestination().id) {
saveState = true
}

// 当重复选择相同项时避免相同目的地的多重拷贝
launchSingleTop = true
// 当重复选择之前已经选择的项时恢复状态
restoreState = true
}
}

保存状态,锁定用户

对用户来说,最令人沮丧的事情之一便是丢失之前的状态。这也是为什么 Fragment 用一整页来讲解 保存与 Fragment 相关的状态,而且也是我非常乐于更新每个层级来支持多返回栈的原因之一:

  • Fragments (比如完全不使用 Navigation Component): 通过使用新的 FragmentManager API,也就是 saveBackStack 和 restoreBackStack

  • 核心的 Navigation 运行时: 添加可选的新的 NavOptions 方法用于 restoreState(恢复状态) 和 saveState (保存状态) 以及新的 popBackStack() 的重载方法,它同样可以传入一个布尔型的 saveState 参数 (默认是 false)。

  • 通过 Fragment 实现 Navigation: FragmentNavigator 现在利用新的 NavigatorAPI,通过使用 Navigation 运行时 API 将 Navigation 运行时 API 转换为 Fragment API。

  • NavigationUI: 每当它们弹出返回栈时,onNavDestinationSelected()、NavigationBarView.setupWithNavController() 和 NavigationView.setupWithNavController() 现在默认使用 restoreState 和 saveState 这两个新的 NavOption。也就意味着 当升级到 Navigation 2.4.0-alpha01 或者更高版本后,任何使用 NavigationUI API 的应用无需修改代码即可实现多返回栈

如果您希望了解 更多使用该 API 的示例,请参考 NavigationAdvancedSample (它是最新更新的,且不包含任何用于支持多返回栈的 NavigationExtensions 代码)。

收起阅读 »

Android 面试准备进行曲-Java基础篇

虚拟机 基础jvm 参考文章JVM内存管理JVM执行Java程序的过程:Java源代码文件(.java)会被Java编译器编译为字节码文件(.class),然后JVM中的类加载器加载各个类的字节码文件,加载完毕之后,交由JVM执行引擎执行。运行时数据区被分为&...
继续阅读 »

虚拟机 基础

jvm 参考文章

JVM内存管理

JVM执行Java程序的过程:Java源代码文件(.java)会被Java编译器编译为字节码文件(.class),然后JVM中的类加载器加载各个类的字节码文件,加载完毕之后,交由JVM执行引擎执行。

1.webp

运行时数据区被分为 线程私有数据区 和 线程共享数据区 两大类:

线程私有数据区包含:程序计数器、虚拟机栈、本地方法栈 线程共享数据区包含:Java堆、方法区(内部包含常量池)

线程私有数据区包含:

  • 程序计数器:是当前线程所执行的字节码的行号指示器
  • 虚拟机栈:是Java方法执行的内存模型
  • 本地方法栈:是虚拟机使用到的Native方法服务

线程共享数据区包含:

  • Java堆:用于存放几乎所有的对象实例和数组;是垃圾收集器管理的主要区域,也被称做“GC堆”;是Java虚拟机所管理的内存中最大的一块

  • 方法区:用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据;Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放

Java堆和栈的区别

  • 堆内存 用于存储Java中的对象和数组,当我们new一个对象或者创建一个数组的时候,就会在堆内存中开辟一段空间给它,用于存放。特点: 先进先出,后进后出。堆可以动态地分配内存大小,由于要在运行时动态分配内存,存取速度较慢。

  • 栈内存

主要是用来执行程序用的,比如:基本类型的变量和对象的引用变量。特点:先进后出,后进先出,存取速度比堆要快,仅次于寄存器,栈数据可以共享,但缺点是,存在栈中的数据大小与生存期必须是确定的,缺乏灵活性

垃圾回收机制/ 回收算法

判定对象可回收有两种方法:

  • 引用计数算法:给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的。然而在主流的Java虚拟机里未选用引用计数算法来管理内存,主要原因是它难以解决对象之间相互循环引用的问题,所以出现了另一种对象存活判定算法。

  • 可达性分析法:通过一系列被称为『GC Roots』的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。其中可作为GC Roots的对象:虚拟机栈中引用的对象,主要是指栈帧中的本地变量、本地方法栈中Native方法引用的对象、方法区中类静态属性引用的对象、方法区中常量引用的对象

回收算法

分代收集算法:是当前商业虚拟机都采用的一种算法,根据对象存活周期的不同,将Java堆划分为新生代和老年代,并根据各个年代的特点采用最适当的收集算法。

  • 新生代:多数对象死去,少量存活。使用『复制算法』,只需复制少量存活对象即可。

    • 复制算法:把可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用尽后,把还存活着的对象『复制』到另外一块上面,再将这一块内存空间一次清理掉。
  • 老年代:对象存活率高。使用『标记—清理算法』或者『标记—整理算法』,只需标记较少的回收对象即可。

    • 标记-清除算法:首先『标记』出所有需要回收的对象,然后统一『清除』所有被标记的对象。
    • 标记-整理算法:首先『标记』出所有需要回收的对象,然后进行『整理』,使得存活的对象都向一端移动,最后直接清理掉端边界以外的内存。

参考文章

Java基础

Java 引用类型

  • 强引用(StrongReference):具有强引用的对象不会被GC;即便内存空间不足,JVM宁愿抛出OutOfMemoryError使程序异常终止,也不会随意回收具有强引用的对象。

  • 软引用(SoftReference):具有软引用的对象,会在内存空间不足的时候被GC;软引用常用来实现内存敏感的高速缓存。

  • 弱引用(WeakReference):被弱引用关联的对象,无论当前内存是否足够都会被GC;强度比软引用更弱,常用于描述非必需对象;常用于解决内存泄漏的问题(Handle 中Context 部分)

  • 虚引用(PhantomReference):仅持有虚引用的对象,在任何时候都可能被GC;常用于跟踪对象被GC回收的活动;必须和引用队列 (ReferenceQueue)联合使用,当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。

类加载机制

类加载机制:是虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,形成可被虚拟机直接使用的Java类型的过程。另外,类型的加载、连接和初始化过程都是在程序运行期完成的,从而通过牺牲一些性能开销来换取Java程序的高度灵活性。主要阶段:

  • 加载(Loading):通过类的全限定名来获取定义此类的二进制字节流;将该二进制字节流所代表的静态存储结构转化为方法区的运行时数据结构,该数据存储数据结构由虚拟机实现自行定义;在内存中生成一个代表这个类的java.lang.Class对象,它将作为程序访问方法区中的这些类型数据的外部接口

  • 验证(Verification):确保Class文件的字节流中包含的信息符合当前虚拟机的要求,包括文件格式验证、元数据验证、字节码验证和符号引用验证

  • 准备(Preparation):为类变量分配内存,因为这里的变量是由方法区分配内存的,所以仅包括类变量而不包括实例变量,后者将会在对象实例化时随着对象一起分配在Java堆中;设置类变量初始值,通常情况下零值

  • 解析(Resolution):虚拟机将常量池内的符号引用替换为直接引用的过程

  • 初始化(Initialization):是类加载过程的最后一步,会开始真正执行类中定义的Java字节码。而之前的类加载过程中,除了在『加载』阶段用户应用程序可通过自定义类加载器参与之外,其余阶段均由虚拟机主导和控制

内存模型

22.webp 主内存是所有变量的存储位置,每条线程都有自己的工作内存,用于保存被该线程使用到的变量的主内存副本拷贝。为了获取更好的运行速度,虚拟机可能会让工作内存优先存储于寄存器和高速缓存中。

并发过程中的原子性 时序性

  • 原子性

可直接保证的原子性变量操作有:read、load、assign、use、store和write,因此可认为基本数据类型的访问读写是具备原子性的。

若需要保证更大范围的原子性,可通过更高层次的字节码指令monitorenter和monitorexit来隐式地使用lock和unlock这两个操作,反映到Java代码中就是同步代码块synchronized关键字。

  • 可见性(一个线程修改了共享变量的值,其他线程能够立即得知这个修改)

通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值这种依赖主内存作为传递媒介的方式来实现。

提供三个关键字保证可见性:volatile能保证新值能立即同步到主内存,且每次使用前立即从主内存刷新;synchronized对一个变量执行unlock操作之前可以先把此变量同步回主内存中;被final修饰的字段在构造器中一旦初始化完成且构造器没有把this的引用传递出去,就可以在其他线程中就能看见final字段的值。

  • 有序性(按照指令顺序执行)

如果在本线程内观察,所有的操作都是有序的,指“线程内表现为串行的语义”;如果在一个线程中观察另一个线程,所有的操作都是无序的,指“指令重排序”现象和“工作内存与主内存同步延迟”现象。

提供两个关键字保证有序性:volatile 本身就包含了禁止指令重排序的语义;synchronized保证一个变量在同一个时刻只允许一条线程对其进行lock操作,使得持有同一个锁的两个同步块只能串行地进入。

设计模式 (使用过的)

  • 单一职责原则:一个类只负责一个功能领域中的相应职责

  • 开放封闭原则:对扩展开放,对修改关闭

  • 依赖倒置原则:抽象不应该依赖于细节,细节应当依赖于抽象。换言之,要针对接口编程,而不是针对实现编程

  • 迪米特法则:应该尽量减少对象之间的交互,如果两个对象之间不必彼此直接通信,那么这两个对象就不应当发生任何直接的相互作用,如果其中的一个对象需要调用另一个对象的某一个方法的话,可以通过第三者转发这个调用

  • 合成/聚合复用原则:要尽量使用合成/聚合,尽量不要使用继承

延伸:Android 中源码使用的设计模式,自己使用过的设计模式

View事件分发: 责任链模式 BitmapFactory加载图片: 工厂模式

ListAdapter: 适配器模式
DialogBuilder: 建造者模式 Adapter.notifyDataSetChanged(): 观察者模式 Binder机制: 代理模式

平时经常使用的 设计模式

单例模式

初级版

public class Singleton {
private static Singleton instance;
private Singleton (){}

public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
复制代码

进阶版

public class Singleton {
private volatile static Singleton singleton;//代码1
private Singleton (){}
public static Singleton getSingleton() {
if (singleton == null) {//代码2
synchronized (Singleton.class) {
if (singleton == null) {//代码3
singleton = new Singleton();//代码4
}
}
}
return singleton;
}
}
复制代码

  在多线程中 两个线程可能同时进入代码2, synchronize保证只有一个线程能进入下面的代码,   此时一个线程A进入一个线程B在外等待, 当线程A完成代码3 和代码4之后, 线程B进入synchronized下面的方法, 线程B在代码3的时候判断不过,从而保证了多线程下 单例模式的线程安全,   另外要慎用单例模式,因为单例模式一旦初始化后 只有进程退出才有可能被回收,如果一个对象不经常被使用,尽量不要使用单例,否则为了几次使用,一直让单例存在占用内存

优点:

  • 内存中只存在一个对象,节省了系统资源。
  • 避免对资源的多重占用,例如一个文件操作,由于只有一个实例存在内存中,避免对同一资源文件的同时操作。

缺点:

  • 单例对象如果持有Context,那么很容易引发内存泄露。
  • 单例模式一般没有接口,扩展很困难,若要扩展,只能修改代码来实现。

Builder 模式

参考文章

33.webp 具体的产品类

public class Computer {
private String mCPU;
private String mMemory;
private String mHD;

public void setCPU(String CPU) {
mCPU = CPU;
}

public void setMemory(String memory) {
mMemory = memory;
}

public void setHD(String HD) {
mHD = HD;
}
}
复制代码

抽象建造者

public abstract class Builder {
public abstract void buildCPU(String cpu);//组装CPU

public abstract void buildMemory(String memory);//组装内存

public abstract void buildHD(String hd);//组装硬盘

public abstract Computer create();//返回组装好的电脑
}
复制代码

创建者实现类

public class ConcreteBuilder extends Builder {
//创建产品实例
private Computer mComputer = new Computer();

@Override
public void buildCPU(String cpu) {//组装CPU
mComputer.setCPU(cpu);
}

@Override
public void buildMemory(String memory) {//组装内存
mComputer.setMemory(memory);
}

@Override
public void buildHD(String hd) {//组装硬盘
mComputer.setHD(hd);
}

@Override
public Computer create() {//返回组装好的电脑
return mComputer;
}
}
复制代码

调用者

public class Director {
private Builder mBuild = null;

public Director(Builder build) {
this.mBuild = build;
}

//指挥装机人员组装电脑
public void Construct(String cpu, String memory, String hd) {
mBuild.buildCPU(cpu);
mBuild.buildMemory(memory);
mBuild.buildHD(hd);
}
}
复制代码

调用


Director direcror = new Director(new ConcreteBuilder());//创建指挥者实例,并分配相应的建造者,(老板分配任务)
direcror.Construct("i7-6700", "三星DDR4", "希捷1T");//组装电脑
复制代码

Builder 模式 优缺点

优点

  • 封装性良好,隐藏内部构建细节。
  • 易于解耦,将产品本身与产品创建过程进行解耦,可以使用相同的创建过程来得到不同的产品。也就说细节依赖抽象。
  • 易于扩展,具体的建造者类之间相互独立,增加新的具体建造者无需修改原有类库的代码。
  • 易于精确控制对象的创建,由于具体的建造者是独立的,因此可以对建造过程逐步细化,而不对其他的模块产生任何影响。

缺点

  • 产生多余的Build对象以及Dirextor类。
  • 建造者模式所创建的产品一般具有较多的共同点,其组成部分相似;如果产品之间的差异性很大,则不适合使用建造者模式,因此其使用范围受到一定的限制

源码中使用的 比如: Dialog.Builder

工厂模式

44.webp

抽象产品类

    public abstract class Product {
public abstract void show();
}
复制代码

具体产品类

    public class ProductA extends Product {
@Override
public void show() {
System.out.println("product A");
}
}
//具体产品类B
public class ProductB extends Product {
@Override
public void show() {
System.out.println("product B");
}
}
复制代码

创建抽象工厂类

	 //抽象工厂类
public abstract class Factory {
public abstract Product create();
}
复制代码

创建具体工厂类,继承抽象工厂类

	public class FactoryA extends Factory {
@Override
public Product create() {
return new ProductA();//创建ProductA
}
}
//具体工厂类B
public class FactoryB extends Factory {
@Override
public Product create() {
return new ProductB();//创建ProductB
}
}
复制代码

测试代码

		Factory factoryA = new FactoryA();
Product productA = factoryA.create();
productA.show();
//产品B
Factory factoryB = new FactoryB();
Product productB = factoryB.create();
productB.show();
复制代码

优点:

  • 符合开放封闭原则。新增产品时,只需增加相应的具体产品类和相应的工厂子类即可。
  • 符合单一职责原则。每个具体工厂类只负责创建对应的产品。

缺点:

  • 增加新产品时,还需增加相应的工厂类,系统类的个数将成对增加,增加了系统的复杂度和性能开销。

源码中 使用的 比如 ThreadFactory

观察者模式

参考文章

含义: 定义对象间的一种一个对多的依赖关系,当一个对象的状态发送改变时,所以依赖于它的对象都得到通知并被自动更新。

55.webp

备注:

  • Subject(抽象主题):又叫抽象被观察者,把所有观察者对象的引用保存到一个集合里,每个主题都可以有任何数量的观察者。抽象主题提供一个接口,可以增加和删除观察者对象。

  • ConcreteSubject(具体主题):又叫具体被观察者,将有关状态存入具体观察者对象;在具体主题内部状态改变时,给所有登记过的观察者发出通知。

  • Observer (抽象观察者):为所有的具体观察者定义一个接口,在得到主题通知时更新自己。

  • ConcrereObserver(具体观察者):实现抽象观察者定义的更新接口,当得到主题更改通知时更新自身的状态。

代码实现

抽象观察者

	public interface Observer {//抽象观察者
public void update(String message);//更新方法
}
复制代码

具体观察者

public class Boy implements Observer {

private String name;//名字
public Boy(String name) {
this.name = name;
}
@Override
public void update(String message) {//男孩的具体反应
System.out.println(name + ",收到了信息:" + message+"屁颠颠的去取快递.");
}
}
复制代码

创建抽象主题

	public interface  Observable {//抽象被观察者
void add(Observer observer);//添加观察者

void remove(Observer observer);//删除观察者

void notify(String message);//通知观察者
}
复制代码

创建具体主题

	public class Postman implements  Observable{//快递员

private List<Observer> personList = new ArrayList<Observer>();//保存收件人(观察者)的信息
@Override
public void add(Observer observer) {//添加收件人
personList.add(observer);
}

@Override
public void remove(Observer observer) {//移除收件人
personList.remove(observer);

}

@Override
public void notify(String message) {//逐一通知收件人(观察者)
for (Observer observer : personList) {
observer.update(message);
}
}
}
复制代码

测试代码

	Observable postman=new Postman();

Observer boy1=new Boy("路飞");
Observer boy2=new Boy("乔巴");
postman.notify("快递到了,请下楼领取.");
复制代码

优点:

  • 解除观察者与主题之间的耦合。让耦合的双方都依赖于抽象,而不是依赖具体。从而使得各自的变化都不会影响另一边的变化。
  • 易于扩展,对同一主题新增观察者时无需修改原有代码。

缺点:

  • 使用观察者模式时需要考虑一下开发效率和运行效率的问题,程序中包括一个被观察者、多个观察者,开发、调试等内容会比较复杂,而且在Java中消息的通知一般是顺序执行,那么一个观察者卡顿,会影响整体的执行效率,在这种情况下,一般会采用异步实现。
  • 可能会引起多余的数据通知。

Android 源码中的观察者模式:Listener

其他设计模式

由于本人涉猎较少,有些只能说简单了解。这里分享一个 不错的 系列博客,感谢前人栽树。

设计模式系列教程 !!!

源码设计

接口和抽象类

  • 抽象类可以提供成员方法的实现细节,而接口中只能存在 public 抽象方法,没有实现,(JDK8以后可以有)
  • 抽象类中的成员变量可以是各种类型的,而接口中的成员变量只能是 public static final 类型的;
  • 接口的成员变量只能是静态常量,没有构造函数,也没有代码块,但抽象类都可以有。
  • 一个类只能继承一个抽象类,而一个类却可以实现多个接口;

抽象类访问速度比接口速度要快,因为接口需要时间去寻找在类中具体实现的方法;

  • 如果你往抽象类中添加新的方法,你可以给它提供默认的实现。因此你不需要改变你现在的代码。如果你往接口中添加方法,那么你必须改变实现该接口的类。

所以:抽象类强调的是重用,接口强调的是解耦。

收起阅读 »

Android 面试准备进行曲-Android基础进阶

View相关View的绘制流程自定义控件: 1、组合控件。这种自定义控件不需要我们自己绘制,而是使用原生控件组合成的新控件。如标题栏。 2、继承原有的控件。这种自定义控件在原生控件提供的方法外,可以自己添加一些方法。如制作圆角,圆形图片。 3、完全自定义控件:...
继续阅读 »

View相关

View的绘制流程

自定义控件: 1、组合控件。这种自定义控件不需要我们自己绘制,而是使用原生控件组合成的新控件。如标题栏。 2、继承原有的控件。这种自定义控件在原生控件提供的方法外,可以自己添加一些方法。如制作圆角,圆形图片。 3、完全自定义控件:这个View上所展现的内容全部都是我们自己绘制出来的。比如说制作水波纹进度条。

View的绘制流程:OnMeasure()——>OnLayout()——>OnDraw()

第一步:OnMeasure():测量视图大小。从顶层父View到子View递归调用measure方法,measure方法又回调OnMeasure。

第二步:OnLayout():确定View位置,进行页面布局。从顶层父View向子View的递归调用view.layout方法的过程,即父View根据上一步measure子View所得到的布局大小和布局参数,将子View放在合适的位置上。

第三步:OnDraw():绘制视图。ViewRoot创建一个Canvas对象,然后调用OnDraw()。

1.webp

View,ViewGroup事件分发

Touch事件分发中只有两个主角:ViewGroup和View。ViewGroup包含onInterceptTouchEvent、dispatchTouchEvent、onTouchEvent三个相关事件。View包含dispatchTouchEvent、onTouchEvent两个相关事件。其中ViewGroup又继承于View。

2.ViewGroup和View组成了一个树状结构,根节点为Activity内部包含的一个ViwGroup。

3.触摸事件由Action_Down、Action_Move、Aciton_UP组成,其中一次完整的触摸事件中,Down和Up都只有一个,Move有若干个,可以为0个。

4.当Acitivty接收到Touch事件时,将遍历子View进行Down事件的分发。ViewGroup的遍历可以看成是递归的。分发的目的是为了找到真正要处理本次完整触摸事件的View,这个View会在onTouchuEvent结果返回true。

5.当某个子View返回true时,会中止Down事件的分发,同时在ViewGroup中记录该子View。接下去的Move和Up事件将由该子View直接进行处理。由于子View是保存在ViewGroup中的,多层ViewGroup的节点结构时,上级ViewGroup保存的会是真实处理事件的View所在的ViewGroup对象:如ViewGroup0-ViewGroup1-TextView的结构中,TextView返回了true,它将被保存在ViewGroup1中,而ViewGroup1也会返回true,被保存在ViewGroup0中。当Move和UP事件来时,会先从ViewGroup0传递至ViewGroup1,再由ViewGroup1传递至TextView。

6.当ViewGroup中所有子View都不捕获Down事件时,将触发ViewGroup自身的onTouch事件。触发的方式是调用super.dispatchTouchEvent函数,即父类View的dispatchTouchEvent方法。在所有子View都不处理的情况下,触发Acitivity的onTouchEvent方法。

7.onInterceptTouchEvent有两个作用:1.拦截Down事件的分发。2.中止Up和Move事件向目标View传递,使得目标View所在的ViewGroup捕获Up和Move事件。

2.webp

view 事件分发流程

3.webp

ViewGroup 时间分发流程

4.webp

整体Activity - ViewGroup - view 分发流程

5.webp

View 事件分发及源码讲解

MeasureSpec 相关知识

MeasureSpec 是一个32位int值,高2位代表SpecMode(测量模式),低30位代表SpecSize( 某种测量模式下的规格大小)。通过宽测量值widthMeasureSpec和高测量值heightMeasureSpec决定View的大小 SpecMode 代表的三种测量模式分别为:

  1. UNSPECIFIED:父容器不对View有任何限制,要多大有多大。常用于系统内部。

  2. EXACTLY(精确模式):父视图为子视图指定一个确切的尺寸SpecSize。对应LyaoutParams中的match_parent或具体数值。

  3. AT_MOST(最大模式):父容器为子视图指定一个最大尺寸SpecSize,View的大小不能大于这个值。对应LayoutParams中的wrap_content。

决定因素:值由子View的布局参数LayoutParams和父容器的MeasureSpec值共同决定。见下图:

6.webp

参考图片 及讲解地址

SurfaceView和View的区别

SurfaceView是从View基类中派生出来的显示类,他和View的区别有:

  • View需要在UI线程对画面进行刷新,而SurfaceView可在子线程进行页面的刷新,View适用于主动更新的情况,View频繁刷新会阻塞主线程,导致界面卡顿

  • SurfaceView在底层已实现双缓冲机制,而View没有,因此SurfaceView更适用于被动更新,需要频繁刷新、刷新时数据处理量很大的页面,而SurfaceView适用于

invalidate()和postInvalidate()的区别

invalidate()与postInvalidate()都用于刷新View,主要区别是invalidate()在主线程中调用,若在子线程中使用需要配合handler;而postInvalidate()可在子线程中直接调用。 我们通过 postInvalidate 如何在子线程中更新的

	// 系统代码
public void postInvalidateDelayed(long delayMilliseconds) {
// We try only with the AttachInfo because there's no point in invalidating
// if we are not attached to our window
final AttachInfo attachInfo = mAttachInfo;
if (attachInfo != null) {
attachInfo.mViewRootImpl.dispatchInvalidateDelayed(this, delayMilliseconds);
}
}

复制代码

接下来我们看下

	// 系统代码
public void dispatchInvalidateDelayed(View view, long delayMilliseconds) {
Message msg = mHandler.obtainMessage(MSG_INVALIDATE, view);
mHandler.sendMessageDelayed(msg, delayMilliseconds);
}

复制代码

我们可以看到 postInvalidate它是向主线程发送个Message,然后handleMessage时,调用了invalidate()函数。(系统帮我们 写好了 Handle部分)

Android 动画

Android中的几种动画

帧动画:指通过指定每一帧的图片和播放时间,有序的进行播放而形成动画效果,比如想听的律动条。

补间动画:指通过指定View的初始状态、变化时间、方式,通过一系列的算法去进行图形变换,从而形成动画效果,主要有Alpha、Scale、Translate、Rotate四种效果。注意:只是在视图层实现了动画效果,并没有真正改变View的属性,比如滑动列表,改变标题栏的透明度。

属性动画:在Android3.0的时候才支持,通过不断的改变View的属性,不断的重绘而形成动画效果。相比于视图动画,View的属性是真正改变了。比如view的旋转,放大,缩小。

属性动画和补间动画区别

  • 补间动画仅仅是 Parents View 对子View里面的画布进行操作,新位置并不响应点击事件,原位置响应。
  • 属性动画是通过修改view属性实现动画,新位置响应点击事件

属性动画为何在新位置还能响应事件

ViewGroup 在 getTransformedMotionEvent() 方法中会通过子 View 的 hasIdentityMatrix() 方法来判断子 View 是否应用过位移、缩放、旋转之类的属性动画。如果应用过的话,那还会调用子 View 的 getInverseMatrix() 做「反平移」操作,然后再去判断处理后的触摸点是否在子 View 的边界范围内。

属性动画点击解密

属性动画原理

属性动画要求 动画作用的对象提供该属性的set方法,属性动画根据你传递的该熟悉的初始值和最终值,以动画的效果多次去调用set方法,每次传递给set方法的值都不一样,确切来说是随着时间的推移,所传递的值越来越接近最终值。如果动画的时候没有传递初始值,那么还要提供get方法,因为系统要去拿属性的初始值。

// 系统代码
void setAnimatedValue(Object target) {
if (mProperty != null) {
mProperty.set(target, getAnimatedValue());
}
if (mSetter != null) {
try {
mTmpValueArray[0] = getAnimatedValue();
mSetter.invoke(target, mTmpValueArray);
} catch (InvocationTargetException e) {
Log.e("PropertyValuesHolder", e.toString());
} catch (IllegalAccessException e) {
Log.e("PropertyValuesHolder", e.toString());
}
}

复制代码

属性动画源码解析

Handler 详解

Handler的原理

Android中主线程是不能进行耗时操作的,子线程是不能进行更新UI的。所以就有了handler,它的作用就是实现线程之间的通信。 handler整个流程中,主要有四个对象,handlerMessage,MessageQueue,Looper。当应用创建的时候,就会在主线程中创建handler对象。

对于Message :

在线程之间传递的消息,它的内部持有Handler和Runnable的引用以及消息类型。可以使用what、arg1、arg2字段携带一些整型数据,使用obj字段携带Object对象;其中有一个obtain()方法,该方法的内部是先通过消息池获取消息,没有再创建,实现了对message对象的复用。其内部有一个target引用,就是对Handler对象的引用,在Looper.loop方法中的消息处理就是通过message的target引用来调用Handler的dispatchMessage()方法来实现消息的处理。

对于Message Queue:

指的是消息队列,是通过一个 单链表 的数据结构维护消息列表的,在插入和删除有优势。其中主要包括两个操作:插入和读取,读取操作本身伴随着删除操作。插入操作是enqueueMessage()方法,就是插入一条消息到MessageQueue中;读取操作是next()方法,它是一个无限循环,如果有消息就返回并从单链表中移除;没有消息就一直阻塞(此时主线程会释放CPU进入休眠状态)。

对于Looper:

Looper在消息机制中进行消息循环,像一个泵,不断地从MessageQueue中查看是否有新消息并提取,交给handler处理。Handler机制一定要Looper,在线程中通过Looper.prepare()为当前线程创建一个Looper,并使用Looper.loop()来开启消息的读取。为什么在平常Activity主线程使用时没有使用到Looper呢?因为对于主线程(UI线程),会自动创建一个Looper 驱动消息队列获取消息,所以Looper可以通过getMainLooper获取到主线程的Looper。 通过quit/quitSafely可以退出Looper,区别在于quit会直接退出,quitSafely会把消息队列已有的消息处理完毕后才退出Looper。

对于Handler

Handler可以发送和接收消息。发送消息(就是往MessageQueue里面插入一条Message)通过post方法和send方法,而post方法最终也是通过send方法来发送的,最终就会调用sendMessageAtTime这个方法(内部就是调用MessageQueue的enqueueMessage()方法,往MessageQueue里面插入一条消息),同时也会给msg的target赋值为handler本身,进入MessageQueue中。处理消息就是Looper调用loop()方法进入无限循环,获取到消息后就会调用msg.target(Handler本身)的dispatchMessage()方法,进而调用handlerMessage()方法处理消息。

Handler导致内存泄露问题

一般我们写Handler:

Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
mImageView.setImageBitmap(mBitmap);
}
}
复制代码

当使用内部类(包括匿名类)来创建Handler的时候,Handler对象会隐式地持有一个外部类对象(通常是一个Activity)的引用,而常常在Activity退出后,消息队列还有未被处理完的消息,此时activity依然被handler引用,导致内存无法回收而内存泄露。

在Handler中增加一个对Activity的弱引用(WeakReference):

static class MyHandler extends Handler {
WeakReference mActivityReference;

MyHandler(Activity activity) {
mActivityReference= new WeakReference(activity);
}

@Override
public void handleMessage(Message msg) {
final Activity activity = mActivityReference.get();
if (activity != null) {
mImageView.setImageBitmap(mBitmap);
}
}
}

复制代码

如果在非自定义 Handler 情况下,还可以通过 Activity 生命周期来及时清除消息,从而及时回收 Activity

override fun onDestroy() {
super.onDestroy()
if (mHandler != null){
mHandler.removeCallbacksAndMessages(null)
}
}
复制代码

Handler的post方法原理

mHandler.post(new Runnable()
{
@Override
public void run()
{
Log.e(“TAG”, Thread.currentThread().getName());
mTxt.setText(“yoxi”);
}
});
复制代码

然后run方法中可以写更新UI的代码,其实这个Runnable并没有创建什么线程,而是发送了一条消息,下面看源码:

public final boolean post(Runnable r)
{
return sendMessageDelayed(getPostMessage(r), 0);
}
复制代码

最终和handler.sendMessage一样,调用了sendMessageAtTime,然后调用了enqueueMessage方法,给msg.target赋值为handler,最终加入MessagQueue.

Handler 其他问题

  1. Looper.loop()和MessageQueue.next()取延时消息时,主线程中使用死循环为什么不会卡死?

    答: 在MessageQueue在取消息时,如果是延时消息就会计算得到该延时消息还需要延时多久nextPollTimeoutMillis。然后再继续循环的时候,发现nextPollTimeoutMillis不等于0,就会执行nativePollOnce阻塞线程nextPollTimeoutMillis毫秒,而阻塞了之后被唤醒的时机就是阻塞的时间到了或者又有新的消息添加进来执行enqueueMessage方法调用nativeWake唤醒阻塞线程,再继续执行获取消息的代码,如果有消息就返回,如果还是需要延时就继续和上边一样阻塞。而Android所有的事件要在主线程中改变的都会通过主线程的Handler发送消息处理,所以就完全保证了不会卡死。

    其中nativePollOnce的位置也有考究,刚好在synchronized的外边,所以在阻塞的时候也能保证添加消息是可以执行的,而取消息 时添加消息就需要等待。

  2. MessageQueue是队列吗? 答: MessageQueue不是队列,它内部使用一个Message链表实现消息的存和取。

  3. Handler的postDelay,时间准吗?它用的是system.currentTime吗? 答: 不准,因为looper里边从MessageQueue里取出来的消息执行也是串行的,如果前一个消息是比较耗时的,那么等到执行之前延时的消息时时间难免可能会超过延时的时间。postDelay时用的是System.uptimeMillis,也就是开机时间。

  4. 子线程run方法中如何使用Handler? 答 : 先要使用Lopper.prepare方法,然后使用该looper创建一个Handler,最后调用Looper.loop方法;Looper.loop方法之后就不要执行写代码了,因为是loop是死循环除非退出,所以Handler的创建也必须写在loop之前。

  5. ThreadLocal是如何实现一个线程一个Looper的? 答: Looper的使用最终都需要执行loop方法,而loop方法中去获取的Looper是从sThreadLocal中获取的,所以Looper就需要和sThreadLocal建立关系,在不考虑反射的情况下,就只能通过Looper的prepare方法进行关联,这里边就会引入一个threadLocalMap,该对象又是和thread一一对应,而threadLocal的get方法实际使用的就是threadLocalMap的get方法,而key就是Looper中的静态变量sThreadLocal,value则就是当前looper对象,而prepare方法只能被执行一次,也就保证了一个线程只有一个looper。ThreadLocalMap对key和value的存取和hashMap类似。

  6. 假设先 postDelay 10ms, 再postDelay 1ms,这两个消息会有什么不同的经历。 答: 先传入一个延时为10ms的消息进入MessageQueue中,因为该消息延时,假设当前消息队列中没有消息,则会直接将消息放入队列,因为loop一直在取消息,但是这里有延时就会阻塞10ms,当然这不考虑代码执行的时间;然后延时1ms的消息进入时,会和之前的10ms的消息进行比较,根据延时的大小进行排序插入,延时小的在前边,所以这时候就把1ms的消息放在10ms的前边,然后唤醒,不阻塞,继续执行取消息的操作,发现还是有延时1ms,所以也会继续阻塞1ms,直到阻塞1ms之后或者又有新的消息进入队列唤醒,直到获取到1ms延时消息,在loop中,通过调用handler的dispatchMessage方法,判断消息的callback或者Handler的callback不为null就回调对应的callback,否则就执行handler的handleMessage方法,我们就可以根据情况处理消息了;10ms的延时消息的处理也是一致,延时的时间到了就交给返回给looper,然后给handler处理。

  7. HandlerThread ? 答: HandlerThread是Thread的一个子类,只是内部创建了一个Handler,这个Handler是子线程的handler,其中子线程的looper的创建和管理也提供了方法方便使用。

  8. 你对 Message.obtain() 了解吗? 答: Message.obtain其实是从缓冲的消息池中取出第一个消息来使用,避免消息对象的频繁创建和销毁;消息池其实是使用Message链表结构实现,在消息在loop中被handler分发消费之后会执行回收的操作,将该消息内部数据清空并添加到消息链表最前边。

多线程相关问题

如何创建多线程

  1. 继承Thread类,重写run函数方法
  2. 实现Runnable接口,重写run函数方法
  3. 实现Callable接口,重写call函数方法
  4. HandlerThread
  5. AsyncTask很老的一种= =

多线程间同步问题

  1. volatile关键字,在get和set的场景下是可以的,由于get和set的时候都加了读写内存屏障,在数据可见性上保证数据同步。但是对于++这种非原子性操作,数据会出现不同步

  2. synchronized对代码块或方法加锁,结合wait,notify调度保证数据同步

  3. reentrantLock加锁结合Condition条件设置,在线程调度上保障数据同步

  4. CountDownLatch简化版的条件锁,在线程调度上保障数据同步

  5. cas=compare and swap(set), 在保证操作原子性上,确保数据同步

  6. 参照UI线程更新UI的思路,使用handler把多线程的数据更新都集中在一个线程上,避免多线程出现脏读

  7. 当然如果只是部分变量存在多线程修改的可能性 建议使用 原子类AtomicInteger AtomicBoolean等 这样会更方便一点。

Android 优化

OOM

  1. 根据java的内存模型会出现内存溢出的内存有堆内存、方法区内存、虚拟机栈内存、native方法区内存;一般说的OOM基本都是针对堆内存;

  2. 对于堆内存溢出主的根本原因有两种 (1)app进程内存达到上限 (2)手机可用内存不足,这种情况并不是我们app消耗了很多内存,而是整个手机内存不足

而我们需要解决的主要是 app的内存达到上限

  1. 对于app内存达到上限只有两种情况

(1)申请内存的速度超出gc释放内存的速度 (2)内存出现泄漏,gc无法回收泄漏的内存,导致可用内存越来越少

  1. 对于申请内存速度超出gc释放内存的速度主要有2种情况

(1)往内存中加载超大文件 (2)循环创建大量对象

  1. 一般申请内存的速度超出gc释放内存基本不会出现,内存泄漏才是出现问题的关键所在

导致内存泄漏情况

内存泄漏的根本原因在于生命周期长的对象持有了生命周期短的对象的引用

  1. 资源对象没关闭造成的内存泄漏(如: Cursor、File等)

  2. 全局集合类强引用没清理造成的内存泄漏( static 修饰的集合)

  3. 接收器、监听器注册没取消造成的内存泄漏,如广播,eventsbus

  4. Activity 的 Context 造成的泄漏,可以使用 ApplicationContext

  5. Handler 造成的内存泄漏问题(一般由于 Handler 生命周期比其外部类的生命周期长引起的)

注1:ListView 的 Adapter 中缓存用的 ConvertView ,主要缓存的是 移除屏幕外的View,就算没有复用,暂时 只会 内存溢出,和泄漏还是有区别的。

注2 :Bitmap 对象到底要不要调用 recycle() 释放内存。结论 Android 3.0 以前需要,因为像素数据与对象本身分开存储,像素数据存储在native层;对象存储在java层。 3.0之后 像素数据与Bitmap对象数据一起关联存储在Dalvik堆中。所以,这个时候,就可以考虑用GC来自动回收。所以我们不用的时候直接 将Bitmap对象设置为Null 即可。参考博客地址

我们列举了 大部分常见的 内存泄漏出现的时机,那么我也简要的列举下 常见的避免内存泄漏的方法(仅供参考);

  1. 为应用申请更大内存,把manifest上的largdgeheap设置为true

  2. 减少内存的使用 ①使用优化后的集合对象,比如SpaseArray;

②使用微信的mmkv替代sharedpreference; ③对于经常打log的地方使用StringBuilder来组拼,替代String拼接 ④统一带有缓存的基础库,特别是图片库,如果用了两套不一样的图片加载库就会出现2个图片各自维护一套图片缓存 ⑤给ImageView设置合适尺寸的图片,列表页显示缩略图,查看大图显示原图 ⑥优化业务架构设计,比如省市区数据分批加载,需要加载省就加载省,需要加载市就加载失去,避免一下子加载所有数据

  1. 避免内存泄漏

    编码规范上:

    ①资源对象用完一定要关闭,最好加finally

②静态集合对象用完要清理 ③接收器、监听器使用时候注册和取消成对出现 ④context使用注意生命周期,如果是静态类引用直接用ApplicationContext ⑤使用静态内部类 ⑥结合业务场景,设置软引用,弱引用,确保对象可以在合适的时机回收

建设内存监控体系:

线下监控:

①使用ArtHook检测图片尺寸是否超出imageview自身宽高的2倍

②编码阶段Memery Profile看app的内存使用情况,是否存在内存抖动,内存泄漏,结合Mat分析内存泄漏

线上监控:

①上报app使用期间待机内存、重点模块内存、OOM率

②上报整体及重点模块的GC次数,GC时间

③使用LeakCannery自动化内存泄漏分析

ANR

Android系统中,AMS和WMS会检测App的响应时间,如果App在主线程进行耗时操作,导致用户的操作没得到及时的响应 就会报出 Application Not Response 的问题 即ANR 。

  1. activity 、键盘输入事件和触摸事件超过五秒
  2. 前台广播10秒没有完成 后台60秒
  3. 服务前台20秒 后台200秒

主要的 规避方案

解决笼统一下尽量使用 子线程,避免死锁 的出现,使用子线程来处理耗时操作或阻塞任务。服务内容提供者尽量不要执行太长时间的任务。

收起阅读 »

Activity的启动方法

在 Android 中,界面的跳转通常是通过启动不同的 Activity 来实现的,下面介绍一下 Activity 的启动方法。显式调用显式调用,字面意思即”明显的调用“,我们可以在调用方法中明确的知道我们即将启动的 Activity,显示调用的具体方法如下:...
继续阅读 »

在 Android 中,界面的跳转通常是通过启动不同的 Activity 来实现的,下面介绍一下 Activity 的启动方法。

显式调用

显式调用,字面意思即”明显的调用“,我们可以在调用方法中明确的知道我们即将启动的 Activity,显示调用的具体方法如下:

val intent = Intent(this, SecondActivity::class.java)
// SecondActivity::class.java 相当于Java中的 SecondActivity.class
startActivity(intent)

我们需要构建一个 Intent 对象,第一个参数传入 this 即当前 Activity 的上下文,第二个对象传入 SecondActivity::class.java 作为目标 Activity,这样我们的意图就很明显,即我们想跳转到 SecondActivity 这个界面,我们只需要调用 startActivity 这个函数就可以达到我们的目的了。

隐式调用

隐式调用与显式调用相反,它没有明确的说明要跳转到哪个 Activity,而是通过 action, category [ˈkætəɡəri] 和 data 这三个过滤信息由系统去匹配复合条件的 Activity。

为 Activity 设置过滤信息

如果我们想要通过隐式调用去启动一个 Activity,我们首先要为这个 Activity 设置过滤信息,否则是不能通过隐式调用去启动这个 Activity 的。过滤信息在 AndroidMainfest.xml 文件中注册 Activity 时设置,通过在 标签下配置 的内容,可以指定当前Activity能够响应的action,category和data。

<activity
   android:name=".activity.SplashActivity"
   android:theme="@style/SplashTheme">
   <intent-filter>
       <action android:name="android.intent.action.VIEW" />
       <category android:name="android.intent.category.DEFAULT" />
       <category android:name="android.intent.category.BROWSABLE" />
       <data
           android:host="room.join"
           android:scheme="bjhlliveapp" />
   </intent-filter>
</activity>

当使用隐式调用启动 Activity 时,需要同时匹配 action,category 和 data,否则匹配失败。一个 下的 action,category 和 data 可以有多个,一个 activity 可以有多个 标签,只要成功匹配其中任意一个中的信息即可匹配成功。

下面详细说明一下各种信息的匹配规则。

action

action 是一个字符串 (区分大小写) ,系统已经为我们预定义了一些 action,如 android.intent.action.MAIN 等,同时我们也可以自己定义 action,一个 下可以有多个 action。

当我们使用隐式调用时 Intent 中必须指定 action,当 action 和 activity 的 中任意一个 action 相同 (字符串的值相同) 时,匹配成功。

category

category 和 action 一样是一个字符串,同时系统中有定义的 category,我们也可以自己定义 category,但是如果想让一个 activity 支持隐式调用,那么必须在 中指定 “android.intent.category.DEFAULT” 这个 category。

category 的匹配规则与 action 不同,隐式调用时 Intent 中必须指定 action,但可以没有 category,但是如果指定了 category (可以是一个或多个) ,那么所有指定的 category 都要和 中的 category 相同,否则匹配失败。

用通俗的话来讲,category 你可以不指定 (如果不指定一定可以匹配成功) ,但是一旦你指定了,你就要保证你指定的这些 category 都要和你即将启动的 activity 中某一个 中的category 相同。

为什么不指定反而可以匹配成功呢?因为前面说了如果 activity 支持隐式调用,则一定要有 “android.intent.category.DEFAULT” 这个 category,系统在调用 startActivity 或者 startActivityForResult 的时候会默认为 Intent 加上 “android.intent. category.DEFAULT” 这个category,所以可以匹配成功。

data

data 的语法结构如下:

<data android:scheme="string"
     android:host="string"
     android:port="string"
     android:path="string"
     android:pathPattern="string"
     android:pathPrefix="string"
     android:mimeType="string" />

data 由两部分组成,mimeType 和 URI。mimeType 指媒体类型,比如image/jpeg、audio/mpeg4-generic 和 video/* 等,可以表示图片、文本、视频等不同的媒体格式,而URI中包含的数据就比较多了,下面是URI的结构:

<scheme>://<host>:<port>/[<path>|<pathPrefix>|<pathPattern>]

例如:

content://com.example.project:200/folder/subfolder/etc
http://www.baidu.com:80/search/info

URI 中每个数据的含义如下:

  • Scheme:URI 的模式,比如http、file、content 等,如果 URI 中没有指定scheme,那么整个 URI 的其他参数无效,这也意味着 URI 是无效的。
  • Host:URI 的主机名,比如 http://www.baidu.com,如果 host 未指定,那么整个 URI 中的其他参数无效,这也意味着 URI 是无效的。
  • Port:URI 中的端口号,比如80,仅当 URI 中指定了 scheme 和 host 参数的时候 port 参数才是有意义的。
  • path、pathPattern 和 pathPrefix:这三个参数表述路径信息,其中 path 表示完整的路径信息;pathPattern 也表示完整的路径信息,但是它里面可以包含通配符 “”,“” 表示0个或多个任意字符,需要注意的是,由于正则表达式的规范,如果想表示真实的字符串,那么“*” 要写成“*”, “\”要写成“ \\”;pathPrefix 表示路径的前缀信息。

data 的匹配规则较为复杂,但总的来说就是 Intent 中的 data 必须和 中的一致,如果 中没有,则 Intent 也没有,如果 中有,则 Intent 中必须有切必须和 中相同。

调用方法

val intent = Intent("android.intent.action.CPW")
// intent 构造函数指定 action
intent.addCategory("android.intent.category.DEFAULT")
// addCategory 方法指定category
intent.setDataAndType(Uri.parse("file://abc"), "text/plain")
// setDataAndType 方法指定data,参数为 URI 和 mimeType
startActivity(intent)

以上就是关于 Activity 启动方法的全部内容了!

收起阅读 »

kotlin 作用域函数

在Kotlin标准库(Standard.kt)中定义了几个作用域函数,其中包含let、run、with、apply和also。这几个函数有一个共同点就是在一个对象的上下文中执行代码块。 当对一个对象调用一个函数并提供一个lambda表达式时,它会形成一...
继续阅读 »

在Kotlin标准库(Standard.kt)中定义了几个作用域函数,其中包含letrunwithapplyalso。这几个函数有一个共同点就是在一个对象的上下文中执行代码块。



当对一个对象调用一个函数并提供一个lambda表达式时,它会形成一个临时作用域。在此作用域中,可以访问该对象而无需其名称。这样的函数称之为作用域函数



这些函数使用起来比较相似,主要区别在于两个方面:



  • 应用上下文对象的方式

  • 返回值


let


public inline fun <T, R> T.let(block: (T) -> R): R 	

let声明为扩展函数,上下文对象作为lambda表达式的参数(默认为it,也可以自定义名称),返回值是lambda表达式的结果。


val result = "a".let {
123
// return@let 123
}
print(result) // 123

上面的代码会输出123,在lambda表达式中可以省略return语句,默认最后一行代码为返回值。


let函数经常用于对象非空执行代码块的情况。例如下面情况使用let是非常方便的。


val str: String? = "Hello" 
//processNonNullString(str) // 编译错误:str 可能为空
val length = str?.let {
println("let() called on $it")
processNonNullString(it) // 编译通过:'it' 在 '?.let { }' 中必不为空
it.length
}

当运行时str不为空才会执行let后面的代码块,相比Java中需要对str进行非空判断就非常便捷了。


run


public inline fun <R> run(block: () -> R): R 

public inline fun <T, R> T.run(block: T.() -> R): R

再标准库中定义了两个run函数,其中第一个run函数可以独立运行一个代码块,并将lambda表达式的返回值作为run函数的返回值。例如:


val hexNumberRegex = run {
val digits = "0-9"
val hexDigits = "A-Fa-f"
val sign = "+-"

Regex("[$sign]?[$digits$hexDigits]+")
}

for (match in hexNumberRegex.findAll("+1234 -FFFF not-a-number")) {
println(match.value)
}

第二个run函数是一个扩展函数,上下文对象作为接收者(this) 来访问,返回值是lambda表达式结果。


val service = MultiportService("https://example.kotlinlang.org", 80)

val result = service.run {
port = 8080
query(prepareRequest() + " to port $port")
}

// 同样的代码如果用 let() 函数来写:
val letResult = service.let {
it.port = 8080
it.query(it.prepareRequest() + " to port ${it.port}")
}

可以看出这个run函数与let类似,区别在于run中可以直接使用上下文对象的属性和方法,而let需要通过it来调用上下文对象的属性和方法。


with


public inline fun <T, R> with(receiver: T, block: T.() -> R): R

with函数是一个非扩展函数,将上下文对象作为参数传递,并接收一个lambda表达式,在lambda表达式内部可以直接引用上下文对象的属性和方法,并将lambda表达式结果作为with函数的返回值。


val numbers = mutableListOf("one", "two", "three")
with(numbers) {
println("'with' is called with argument $this")
println("It contains $size elements")
}

with函数可以理解为“对于这个对象执行以下操作”。在使用with函数时建议使用 with 来调用上下文对象上的函数,而不使用 lambda 表达式结果。


apply


public inline fun <T> T.apply(block: T.() -> Unit): T

apply函数是一个扩展函数,上下文对象 作为接收者(this)来访问。 返回值 是上下文对象本身。


apply 的常见情况是对象配置。这样的调用可以理解为“将以下赋值操作应用于对象”。


val adam = Person("Adam").apply {
age = 32
city = "London"
}
println(adam)

also


public inline fun <T> T.also(block: (T) -> Unit): T

also函数是一个扩展函数,上下文对象作为 lambda 表达式的参数(it)来访问。 返回值是上下文对象本身。


also 对于执行一些将上下文对象作为参数的操作很有用。 对于需要引用对象而不是其属性与函数的操作,或者不想屏蔽来自外部作用域的 this 引用时,请使用 also


当你在代码中看到 also 时,可以将其理解为“并且用该对象执行以下操作”。


val numbers = mutableListOf("one", "two", "three")
numbers
.also { println("The list elements before adding new one: $it") }
.add("four")

总结


对于各个函数之间的区别可以参考下面的表格。根据使用场景选择合适的函数。

















































函数 对象引用 返回值 是否时扩展函数
let it Lambda 表达式结果
run this Lambda 表达式结果
run - Lambda 表达式结果 不是:调用无需上下文对象
with this Lambda 表达式结果 不是:把上下文对象当做参数
apply this 上下文对象
also it 上下文对象

以下是根据预期目的选择作用域函数的简短指南:



  • 对一个非空(non-null)对象执行 lambda 表达式:let

  • 将表达式作为变量引入为局部作用域中:let

  • 对象配置:apply

  • 对象配置并且计算结果:run

  • 在需要表达式的地方运行语句:非扩展的 run

  • 附加效果:also

  • 一个对象的一组函数调用:with


参考


Kotlin语言中文站


收起阅读 »

Recyclerview EditText 引发的问题与解决方案

问题 我使用简单的魔法就让各位大佬的财富值减少了,我们来看看谷歌公司是怎样做到的。 我们知道 Recyclerview 是有复用机制的,一般复用的个数是一个屏幕多一点的数量,比如我这里就是 16 。 默认情况,找到产生问题的原因 也就是我们不做任何...
继续阅读 »



问题


GIF 2021-8-5 7-27-00.gif


我使用简单的魔法就让各位大佬的财富值减少了,我们来看看谷歌公司是怎样做到的。


我们知道 Recyclerview 是有复用机制的,一般复用的个数是一个屏幕多一点的数量,比如我这里就是 16


默认情况,找到产生问题的原因


也就是我们不做任何修改,只看文本监听里面输出内容看看打印的日志,先看监听代码:


input?.addTextChangedListener(object : TextWatcher {
init {
Log.i(TAG, "Holder init: -------------------------------")
}
override fun beforeTextChanged(s: CharSequence?,start: Int,count: Int,after: Int) {}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
Log.i(TAG, "onTextChanged ${d.name}: $s")
}
override fun afterTextChanged(s: Editable?) {}
})

现在我们看初始化的日志:


I/吴敬悦: Holder init: -------------------------------
I/吴敬悦: Holder init: -------------------------------
I/吴敬悦: Holder init: -------------------------------
I/吴敬悦: Holder init: -------------------------------

现在我滑动到底看看对应的日志输出:


I/吴敬悦: Holder init: -------------------------------
I/吴敬悦: Holder init: -------------------------------
I/吴敬悦: onTextChanged 杰夫·贝索斯: 618
I/吴敬悦: Holder init: -------------------------------
I/吴敬悦: onTextChanged 埃隆·马斯克: 602
I/吴敬悦: Holder init: -------------------------------
I/吴敬悦: onTextChanged 伯纳德·阿尔诺及家族: 595
I/吴敬悦: Holder init: -------------------------------
I/吴敬悦: onTextChanged 比尔·盖茨: 590
I/吴敬悦: Holder init: -------------------------------
I/吴敬悦: onTextChanged 马克·扎克伯格: 553
I/吴敬悦: Holder init: -------------------------------
I/吴敬悦: onTextChanged 沃伦·巴菲特: 530
I/吴敬悦: Holder init: -------------------------------
I/吴敬悦: onTextChanged 拉里·埃里森: 519
I/吴敬悦: Holder init: -------------------------------
I/吴敬悦: onTextChanged 拉里·佩奇: 505
I/吴敬悦: Holder init: -------------------------------
I/吴敬悦: onTextChanged 谢尔盖·布林: 499
I/吴敬悦: Holder init: -------------------------------
I/吴敬悦: onTextChanged 穆克什·安巴尼: 484
I/吴敬悦: Holder init: -------------------------------
I/吴敬悦: onTextChanged 阿曼西奥·奥特加: 464
I/吴敬悦: Holder init: -------------------------------
I/吴敬悦: onTextChanged 弗朗索瓦丝·贝当古·迈耶斯及家族: 464
I/吴敬悦: Holder init: -------------------------------
I/吴敬悦: onTextChanged 钟睒睒: 454
I/吴敬悦: Holder init: -------------------------------
I/吴敬悦: onTextChanged 史蒂夫·鲍尔默: 451
I/吴敬悦: Holder init: -------------------------------
I/吴敬悦: onTextChanged 马化腾: 441
I/吴敬悦: Holder init: -------------------------------
I/吴敬悦: Holder init: -------------------------------
I/吴敬悦: onTextChanged 杰夫·贝索斯: 418
I/吴敬悦: onTextChanged 艾丽斯·沃尔顿: 418
I/吴敬悦: Holder init: -------------------------------
I/吴敬悦: onTextChanged 埃隆·马斯克: 392
I/吴敬悦: onTextChanged 吉姆·沃尔顿: 392
I/吴敬悦: Holder init: -------------------------------
I/吴敬悦: onTextChanged 伯纳德·阿尔诺及家族: 390
I/吴敬悦: onTextChanged 罗伯·沃尔顿: 390
I/吴敬悦: Holder init: -------------------------------
I/吴敬悦: onTextChanged 比尔·盖茨: 382
I/吴敬悦: onTextChanged 迈克尔·布隆伯格: 382
I/吴敬悦: Holder init: -------------------------------
I/吴敬悦: onTextChanged 马克·扎克伯格: 377
I/吴敬悦: onTextChanged 黄峥: 377
I/吴敬悦: Holder init: -------------------------------
I/吴敬悦: onTextChanged 沃伦·巴菲特: 369
I/吴敬悦: onTextChanged 麦肯齐·斯科特: 369
I/吴敬悦: Holder init: -------------------------------
I/吴敬悦: onTextChanged 拉里·埃里森: 356
I/吴敬悦: onTextChanged 丹尼尔·吉尔伯特: 356
I/吴敬悦: Holder init: -------------------------------
I/吴敬悦: onTextChanged 拉里·佩奇: 351
I/吴敬悦: onTextChanged 高塔姆·阿达尼及家族: 351
I/吴敬悦: Holder init: -------------------------------
I/吴敬悦: onTextChanged 谢尔盖·布林: 345
I/吴敬悦: onTextChanged 菲尔·耐特及家族: 345
I/吴敬悦: Holder init: -------------------------------
I/吴敬悦: onTextChanged 穆克什·安巴尼: 345
I/吴敬悦: onTextChanged 马云: 345
I/吴敬悦: Holder init: -------------------------------
I/吴敬悦: onTextChanged 阿曼西奥·奥特加: 337
I/吴敬悦: onTextChanged 查尔斯·科赫: 337
I/吴敬悦: Holder init: -------------------------------
I/吴敬悦: onTextChanged 弗朗索瓦丝·贝当古·迈耶斯及家族: 335
I/吴敬悦: onTextChanged 茱莉亚·科赫及家族: 335
I/吴敬悦: Holder init: -------------------------------
I/吴敬悦: onTextChanged 卡洛斯·斯利姆·埃卢及家族: 330
I/吴敬悦: Holder init: -------------------------------
I/吴敬悦: onTextChanged 史蒂夫·鲍尔默: 320
I/吴敬悦: onTextChanged 迈克尔·戴尔: 320
I/吴敬悦: Holder init: -------------------------------
I/吴敬悦: onTextChanged 马化腾: 317
I/吴敬悦: onTextChanged 柳井正及家族: 317
I/吴敬悦: Holder init: -------------------------------
I/吴敬悦: onTextChanged 弗朗索瓦·皮诺特及家族: 313
I/吴敬悦: Holder init: -------------------------------
I/吴敬悦: onTextChanged 杰夫·贝索斯: 313
I/吴敬悦: onTextChanged 艾丽斯·沃尔顿: 313
I/吴敬悦: onTextChanged 大卫·汤姆森及家族: 313
I/吴敬悦: Holder init: -------------------------------
I/吴敬悦: onTextChanged 埃隆·马斯克: 296
I/吴敬悦: onTextChanged 吉姆·沃尔顿: 296
I/吴敬悦: onTextChanged 贝亚特·海斯特和小卡尔·阿尔布雷希特: 296
I/吴敬悦: Holder init: -------------------------------

可以看到默认情况下是只执行实例化的操作,而文本改变的监听却没有,那我滚动的时候发现文本监听触发了,其实我并没有改变文本,那为啥会这样子呢,当然就是因为复用导致的,由于复用所以原本已经被赋值的还会被赋值,这个时候就会触发文本改变监听,同时由于每一个监听器被多个数据使用,所以这里的财富所对应的名字也是不同的。在滑动过程中我们也发现 TextWatcher 被多次实例化,但又不是跟数据条数所对应。我们知道如果 TextWatcher 的个数跟数据量相同,是不是就可以解决数据乱的问题呢,我们尝试让每一项数据都有独一无二的 TextWatcher


我新建了一个类:


class OwnTextWatcher(private val name: String): TextWatcher {
init {
Log.i(Adapter.TAG, "init name: $name-------------------")
}
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}

override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
Log.i(Adapter.TAG, "onTextChanged ${name}: $s")
}

override fun afterTextChanged(s: Editable?) {}
}

初始化的日志:


I/吴敬悦: init name: 杰夫·贝索斯-------------------
I/吴敬悦: init name: 埃隆·马斯克-------------------
I/吴敬悦: init name: 伯纳德·阿尔诺及家族-------------------
I/吴敬悦: init name: 比尔·盖茨-------------------
I/吴敬悦: init name: 马克·扎克伯格-------------------
I/吴敬悦: init name: 沃伦·巴菲特-------------------
I/吴敬悦: init name: 拉里·埃里森-------------------
I/吴敬悦: init name: 拉里·佩奇-------------------
I/吴敬悦: init name: 谢尔盖·布林-------------------
I/吴敬悦: init name: 穆克什·安巴尼-------------------
I/吴敬悦: init name: 阿曼西奥·奥特加-------------------
I/吴敬悦: init name: 弗朗索瓦丝·贝当古·迈耶斯及家族-------------------

这是的个数刚好差不多是一屏的数量,再看我滑动的日志输出:


I/吴敬悦: init name: 钟睒睒-------------------
I/吴敬悦: init name: 史蒂夫·鲍尔默-------------------
I/吴敬悦: init name: 马化腾-------------------
I/吴敬悦: init name: 卡洛斯·斯利姆·埃卢及家族-------------------
I/吴敬悦: onTextChanged 杰夫·贝索斯: 618
I/吴敬悦: init name: 艾丽斯·沃尔顿-------------------
I/吴敬悦: onTextChanged 埃隆·马斯克: 602
I/吴敬悦: init name: 吉姆·沃尔顿-------------------
I/吴敬悦: onTextChanged 伯纳德·阿尔诺及家族: 595
I/吴敬悦: init name: 罗伯·沃尔顿-------------------
I/吴敬悦: onTextChanged 比尔·盖茨: 590
I/吴敬悦: init name: 迈克尔·布隆伯格-------------------
I/吴敬悦: onTextChanged 马克·扎克伯格: 553
I/吴敬悦: init name: 黄峥-------------------
I/吴敬悦: onTextChanged 沃伦·巴菲特: 530
I/吴敬悦: init name: 麦肯齐·斯科特-------------------
I/吴敬悦: onTextChanged 拉里·埃里森: 519
I/吴敬悦: init name: 丹尼尔·吉尔伯特-------------------
I/吴敬悦: onTextChanged 拉里·佩奇: 505
I/吴敬悦: init name: 高塔姆·阿达尼及家族-------------------
I/吴敬悦: onTextChanged 谢尔盖·布林: 499
I/吴敬悦: init name: 菲尔·耐特及家族-------------------
I/吴敬悦: init name: 马云-------------------
I/吴敬悦: onTextChanged 阿曼西奥·奥特加: 464
I/吴敬悦: init name: 查尔斯·科赫-------------------
I/吴敬悦: onTextChanged 弗朗索瓦丝·贝当古·迈耶斯及家族: 464
I/吴敬悦: init name: 茱莉亚·科赫及家族-------------------
I/吴敬悦: onTextChanged 钟睒睒: 454
I/吴敬悦: init name: 孙正义-------------------
I/吴敬悦: onTextChanged 史蒂夫·鲍尔默: 451
I/吴敬悦: init name: 迈克尔·戴尔-------------------
I/吴敬悦: onTextChanged 马化腾: 441
I/吴敬悦: init name: 柳井正及家族-------------------
I/吴敬悦: onTextChanged 卡洛斯·斯利姆·埃卢及家族: 423
I/吴敬悦: init name: 弗朗索瓦·皮诺特及家族-------------------
I/吴敬悦: onTextChanged 穆克什·安巴尼: 418
I/吴敬悦: init name: 大卫·汤姆森及家族-------------------
I/吴敬悦: onTextChanged 埃隆·马斯克: 392
I/吴敬悦: onTextChanged 吉姆·沃尔顿: 392
I/吴敬悦: init name: 贝亚特·海斯特和小卡尔·阿尔布雷希特-------------------
I/吴敬悦: onTextChanged 杰夫·贝索斯: 390
I/吴敬悦: onTextChanged 艾丽斯·沃尔顿: 390
I/吴敬悦: init name: 王卫-------------------
I/吴敬悦: onTextChanged 比尔·盖茨: 382
I/吴敬悦: onTextChanged 迈克尔·布隆伯格: 382
I/吴敬悦: init name: 米丽娅姆·阿德尔森-------------------
I/吴敬悦: onTextChanged 马克·扎克伯格: 377
I/吴敬悦: onTextChanged 黄峥: 377
I/吴敬悦: init name: 何享健及家族-------------------
I/吴敬悦: onTextChanged 沃伦·巴菲特: 369
I/吴敬悦: onTextChanged 麦肯齐·斯科特: 369
I/吴敬悦: init name: 迪特尔·施瓦茨-------------------
I/吴敬悦: onTextChanged 拉里·埃里森: 356
I/吴敬悦: onTextChanged 丹尼尔·吉尔伯特: 356
I/吴敬悦: init name: 张一鸣-------------------
I/吴敬悦: onTextChanged 拉里·佩奇: 351
I/吴敬悦: onTextChanged 高塔姆·阿达尼及家族: 351
I/吴敬悦: init name: 乔瓦尼·费列罗-------------------
I/吴敬悦: onTextChanged 谢尔盖·布林: 345
I/吴敬悦: onTextChanged 菲尔·耐特及家族: 345
I/吴敬悦: init name: 阿兰·韦特海默-------------------
I/吴敬悦: onTextChanged 马云: 345
I/吴敬悦: init name: 杰拉德·韦特海默-------------------
I/吴敬悦: onTextChanged 阿曼西奥·奥特加: 337
I/吴敬悦: onTextChanged 查尔斯·科赫: 337
I/吴敬悦: init name: 李嘉诚-------------------
I/吴敬悦: onTextChanged 弗朗索瓦丝·贝当古·迈耶斯及家族: 335
I/吴敬悦: onTextChanged 茱莉亚·科赫及家族: 335
I/吴敬悦: init name: 秦英林-------------------
I/吴敬悦: onTextChanged 钟睒睒: 330
I/吴敬悦: onTextChanged 孙正义: 330
I/吴敬悦: init name: 丁磊-------------------
I/吴敬悦: onTextChanged 史蒂夫·鲍尔默: 320
I/吴敬悦: onTextChanged 迈克尔·戴尔: 320
I/吴敬悦: init name: 莱恩·布拉瓦特尼克-------------------
I/吴敬悦: onTextChanged 马化腾: 317
I/吴敬悦: onTextChanged 柳井正及家族: 317
I/吴敬悦: init name: 李兆基-------------------
I/吴敬悦: onTextChanged 卡洛斯·斯利姆·埃卢及家族: 313
I/吴敬悦: onTextChanged 弗朗索瓦·皮诺特及家族: 313
I/吴敬悦: init name: 杰奎琳·马尔斯-------------------
I/吴敬悦: onTextChanged 穆克什·安巴尼: 313
I/吴敬悦: onTextChanged 大卫·汤姆森及家族: 313
I/吴敬悦: init name: 约翰·马尔斯-------------------
I/吴敬悦: onTextChanged 埃隆·马斯克: 296
I/吴敬悦: onTextChanged 吉姆·沃尔顿: 296
I/吴敬悦: onTextChanged 贝亚特·海斯特和小卡尔·阿尔布雷希特: 296
I/吴敬悦: init name: 杨惠妍及家族-------------------

我核对了初始化的数量,发现跟数据是相同的,说明的确初始化了这么多,那为啥还是有这种现象呢,我们知道的是其实输入框的节点对象并不是跟数据量相同,而是要看复用了多少,其实对于一个手机来说基本上每次初始化相同列表所实例化的是相同或相似的(我没有验证)。既然如此那么即便我们 TextWatcher 的数量是跟数据量相同,但由于本身 EditText 的数量就只有那么几个,要同时保存那么多数量的 TextWatcher 是不现实的,如果真要保存,那么只能是一个 EditText 实例保存了多份 TextWatcher 。我们可以去看一下 addTextChangedListener 的源码:


public void addTextChangedListener(TextWatcher watcher) {
if (mListeners == null) {
mListeners = new ArrayList<TextWatcher>();
}

mListeners.add(watcher);
}

我们发现果然是添加,并不是替换,也就是一个 EditText 是可以对应多个 TextWatcher 的,于是我就想为啥这样设计呢,其实我觉得原因就是有这样的需求,就是有可能一个文本的改变有多处监听,这也是普遍的需求。我们假设如果这个地方只有一个监听,也就是一对一的关系,那么我们这里是不是可以实现我们想要的功能呢,答案的否定的,如果真是这样的话,那么只会有那么几个是有效的,而且当后面的监听把前面的代替以后,前面的压根就不能正常工作。


寻找我们想要的答案


根据前面的分析与理解,我们知道产生这种问题的原因,现在我们的目标就是对症下药。


我们知道总是只有那么几个实例,只要数量多到达到复用的情况,那么就会出现一对多的情况,其实在复用的情况下我们是不希望一对多的,毕竟我们改变一个的时候就是改变一个,既然这样,那么我们可以尝试让监听的数量刚好跟 EditText 的实例数量相同;我们知道每一个 RecyclerView.ViewHolder 实例化都会执行 init ,而且在这里面总是跟 RecyclerView.ViewHolder 的数量相同,所以我们把监听的工作放到这里面进行。但是又会出现一个新的问题,也就是刚才我们说的如果只有一个的话,那么数据的一对一怎么保证呢,我想到一个方法,因为我们知道数据每一次渲染都会执行 onBindViewHolder 这个函数,也就是每一次数据都会在这里改变,那么我使用一个全局的变量保存数据,只要执行了 onBindViewHolder 这个函数,那么就更新数据,这样就解决问题了,下面看代码:


class Adapter: RecyclerView.Adapter<Adapter.Holder>() {
companion object {
const val TAG = "吴敬悦"
private var currentData: Data? = null
}
var list: ArrayList<Data> = arrayListOf()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Holder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.item_test_adapter, parent, false)
return Holder(view)
}

override fun onBindViewHolder(holder: Holder, position: Int) {
holder.bind(list[position], position)
}

override fun getItemCount(): Int = list.size

inner class Holder(view: View): RecyclerView.ViewHolder(view) {
private var text: TextView? = null
private var input: EditText? = null
init {
text = view.findViewById(R.id.titleText)
input = view.findViewById(R.id.input)
input?.addTextChangedListener(object : TextWatcher {
init {
Log.i(TAG, "Holder init: -------------------------------")
}
override fun beforeTextChanged(s: CharSequence?,start: Int,count: Int,after: Int) {}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
Log.i(TAG, "onTextChanged ${currentData?.name}: $s")
}
override fun afterTextChanged(s: Editable?) {}
})
}
fun bind(d: Data, position: Int) {
currentData = d
text?.text = d.name
input?.setText(d.wealth.toString())
}
}
}

下面看一下日志输出,当初始化时:


I/吴敬悦: Holder init: -------------------------------
I/吴敬悦: onTextChanged 杰夫·贝索斯: 1770
I/吴敬悦: Holder init: -------------------------------
I/吴敬悦: onTextChanged 埃隆·马斯克: 1510
I/吴敬悦: Holder init: -------------------------------
I/吴敬悦: onTextChanged 伯纳德·阿尔诺及家族: 1500
I/吴敬悦: Holder init: -------------------------------
I/吴敬悦: onTextChanged 比尔·盖茨: 1240
I/吴敬悦: Holder init: -------------------------------
I/吴敬悦: onTextChanged 马克·扎克伯格: 970
I/吴敬悦: Holder init: -------------------------------
I/吴敬悦: onTextChanged 沃伦·巴菲特: 960
I/吴敬悦: Holder init: -------------------------------
I/吴敬悦: onTextChanged 拉里·埃里森: 930
I/吴敬悦: Holder init: -------------------------------
I/吴敬悦: onTextChanged 拉里·佩奇: 915
I/吴敬悦: Holder init: -------------------------------
I/吴敬悦: onTextChanged 谢尔盖·布林: 890
I/吴敬悦: Holder init: -------------------------------
I/吴敬悦: onTextChanged 穆克什·安巴尼: 845
I/吴敬悦: Holder init: -------------------------------
I/吴敬悦: onTextChanged 阿曼西奥·奥特加: 770
I/吴敬悦: Holder init: -------------------------------
I/吴敬悦: onTextChanged 弗朗索瓦丝·贝当古·迈耶斯及家族: 736

当我们滑到底的日志:


I/吴敬悦: Holder init: -------------------------------
I/吴敬悦: onTextChanged 钟睒睒: 689
I/吴敬悦: Holder init: -------------------------------
I/吴敬悦: onTextChanged 史蒂夫·鲍尔默: 687
I/吴敬悦: Holder init: -------------------------------
I/吴敬悦: onTextChanged 马化腾: 658
I/吴敬悦: Holder init: -------------------------------
I/吴敬悦: onTextChanged 卡洛斯·斯利姆·埃卢及家族: 628
I/吴敬悦: onTextChanged 艾丽斯·沃尔顿: 618
I/吴敬悦: onTextChanged 吉姆·沃尔顿: 602
I/吴敬悦: onTextChanged 罗伯·沃尔顿: 595
I/吴敬悦: onTextChanged 迈克尔·布隆伯格: 590
I/吴敬悦: onTextChanged 黄峥: 553
I/吴敬悦: onTextChanged 麦肯齐·斯科特: 530
I/吴敬悦: onTextChanged 丹尼尔·吉尔伯特: 519
I/吴敬悦: Holder init: -------------------------------
I/吴敬悦: onTextChanged 高塔姆·阿达尼及家族: 505
I/吴敬悦: onTextChanged 菲尔·耐特及家族: 499
I/吴敬悦: onTextChanged 马云: 484
I/吴敬悦: onTextChanged 查尔斯·科赫: 464
I/吴敬悦: onTextChanged 茱莉亚·科赫及家族: 464
I/吴敬悦: onTextChanged 孙正义: 454
I/吴敬悦: onTextChanged 迈克尔·戴尔: 451
I/吴敬悦: onTextChanged 柳井正及家族: 441
I/吴敬悦: onTextChanged 弗朗索瓦·皮诺特及家族: 423
I/吴敬悦: onTextChanged 大卫·汤姆森及家族: 418
I/吴敬悦: onTextChanged 贝亚特·海斯特和小卡尔·阿尔布雷希特: 392
I/吴敬悦: onTextChanged 王卫: 390
I/吴敬悦: onTextChanged 米丽娅姆·阿德尔森: 382
I/吴敬悦: onTextChanged 何享健及家族: 377
I/吴敬悦: onTextChanged 迪特尔·施瓦茨: 369
I/吴敬悦: onTextChanged 张一鸣: 356
I/吴敬悦: onTextChanged 乔瓦尼·费列罗: 351
I/吴敬悦: onTextChanged 阿兰·韦特海默: 345
I/吴敬悦: onTextChanged 杰拉德·韦特海默: 345
I/吴敬悦: onTextChanged 李嘉诚: 337
I/吴敬悦: onTextChanged 秦英林: 335
I/吴敬悦: onTextChanged 丁磊: 330
I/吴敬悦: onTextChanged 莱恩·布拉瓦特尼克: 320
I/吴敬悦: onTextChanged 李兆基: 317
I/吴敬悦: onTextChanged 杰奎琳·马尔斯: 313
I/吴敬悦: onTextChanged 约翰·马尔斯: 313
I/吴敬悦: onTextChanged 杨惠妍及家族: 296

我们发现达到了我们想要的目标

收起阅读 »

给Android应用设置DeviceOwner权限遇到的问题及解决方案

背景 Android手机品牌和型号众多,特别是国产手机系统时常添加各种中国特色功能,因此其与设备管理员模式的兼容性或多或少存在一些问题,今天专门来讲讲我遇到的一些常见机型兼容性问题。 注意事项 设备管理员模式不需要反复连接电脑设置,只需要配置一次,重启或...
继续阅读 »



背景


Android手机品牌和型号众多,特别是国产手机系统时常添加各种中国特色功能,因此其与设备管理员模式的兼容性或多或少存在一些问题,今天专门来讲讲我遇到的一些常见机型兼容性问题。


注意事项


设备管理员模式不需要反复连接电脑设置,只需要配置一次,重启或升级系统都没有影响。


但是在执行命令之前需要对手机进行一些设置,具体如下:



  • 小米用户需要开启「USB 调试(安全设置)」关闭「MIUI 优化」

  • 所有手机进入「设置 - 帐户」,删除所有的帐户,包括你的 Google、小米、华为、OPPO、vivo等系统帐号(像OPPO和vivo这样安装需要登录账户的可以之后再登录回来)

  • 如果你之前设置过多用户或开启过手机自带的访客模式、应用双开等,也需要一并关闭或删除(之后可以再打开)


常见问题


问题1:提示 “Not allowed to … already several accounts on the device”


说明手机上的账户没有删干净,这时候需要注销手机上的所有账户,包括 Google 账号和系统自带的如小米账号、华为账号、OPPO/vivo账号等,另外索尼手机需要拔掉 SIM 卡重启。


问题2:提示 “Not allowed to … already several users on the device”


说明手机的多用户或应用双开没删干净或者关闭,请删除或关闭所有的多用户、访客模式以及应用双开。


问题3:提示 “Trying to set the device owner, but device owner is already set.”


说明手机已经设置了其他 APP 为设备管理员,一台手机上只能有一个设备管理员。


问题4:MIUI 用户提示 “Neither user xxx nor current process has android.permission.MANAGE_DEVICE_ADMINS”


这个时候需要手动在系统设置- 开发者设置里开启「USB 调试(安全设置)」,如果任然不可以,那么就关闭 MIUI 优化重试。


问题5:尝试完以上步骤还是无法设置DeviceOwner权限


但是在有些机型上即便重置了手机,发现还是设置不了DeviceOwner权限,那就说明这台手机存在隐藏账户或者用户了,这时候我们可以通过adb命令来获取手机账户信息从而查看设置不了权限的原因。


查看手机账户(Account):


adb shell dumpsys account

图片


如果账户数目大于0,则请查看手机账户管理,是否有账户存在,存在的账户要退出或者删除;如果没有看到账户,那可能是隐藏账户,需要重置手机,然后再重新设置权限,如下图是重置手机后的结果:


图片


查看手机用户(User):


adb shell dumpsys user

图片


Android 6.0以后,设置DeviceOwner会检测手机里面user数目,如果大于1个则不能设置DeviceOwner权限。


问题6:手机重置之后仍然无法设置DeviceOwner权限


重置手机的时候需要注意,在系统初始化设置的时候,初始化界面上有一些选项(比如智能助手、智能桌面、用户体验计划等),能不选的就都别勾选,因为勾选了这些选项之后系统就会创建一个隐藏的账户。


问题7:提示:java.lang.IllegalStateException: Unexpected @ProvisioningPreCondition 99


这个问题暂时解决不了,据了解OPPO以及Realme 最新的几款机型已经修改了底层源码,不支持设置DeviceOwner了。


截至发稿,我已经在小米、红米、华为、荣耀、三星、魅族、一加、HTC、努比亚、vivo这几款主流机型上验证过了将近200个机型都是可以正常激活DeviceOwner权限的,另外早期的几款OPPO手机型号也是可以的。

收起阅读 »

Android 状态机源码解析

概述如果流程围绕失误的状态流转,这时候就要用到状态机,状态机描述一个事务,有多种状态,不同的动作作用再状态上导致抓状态的转换,这里面有三个重点状态 : 睡觉,工作,吃饭事件 : 起床,饥饿,疲惫动作 : 比如说闹铃触发了起床事件导致状态 从睡觉->工作(...
继续阅读 »

概述

如果流程围绕失误的状态流转,这时候就要用到状态机,状态机描述一个事务,有多种状态,不同的动作作用再状态上导致抓状态的转换,这里面有三个重点

  • 状态 : 睡觉,工作,吃饭
  • 事件 : 起床,饥饿,疲惫
  • 动作 : 比如说闹铃触发了起床事件导致状态 从睡觉->工作(可以省略)

总体就是,首先触发某个事件,导致了状态的改变, 闹铃触发起床事件,导致状态的改变睡觉-->工作

而Android中提供了状态机,在frameworks层源码frameworks/base/core/java/com/android/internal/util,如果项目中需要使用可以把对应的三个类拷贝到项目中StateMachine.java、State、IState

源码分析

IState

public interface IState {
/**
* Returned by processMessage to indicate the the message was processed.
* 由 processMessage 返回以指示消息已处理。
*/

static final boolean HANDLED = true;
/**
* Returned by processMessage to indicate the the message was NOT processed.
* 由 processMessage 返回以指示消息未被处理。
*/

static final boolean NOT_HANDLED = false;
/**
* Called when a state is entered.
* 进入状态时调用
*
*/

void enter();
/**
* Called when a state is exited.
* 退出一个状态时调用
*/

void exit();
/**
* Called when a message is to be processed by the
* state machine.
*
* This routine is never reentered thus no synchronization
* is needed as only one processMessage method will ever be
* executing within a state machine at any given time. This
* does mean that processing by this routine must be completed
* as expeditiously as possible as no subsequent messages will
* be processed until this routine returns.
*
* @param msg to process
* @return HANDLED if processing has completed and NOT_HANDLED
* if the message wasn't processed.
*/

boolean processMessage(Message msg);
/**
* Name of State for debugging purposes.
*
* @return name of state.返回状态的名字
*/

String getName();
}

状态的接口,定义了基本的方法,State实现了IState,我们自定义的状态需要直接继承State

StateMachine

构造方法

  private void initStateMachine(String name, Looper looper) {
mName = name;
mSmHandler = new SmHandler(looper, this);
}

protected StateMachine(String name) {
mSmThread = new HandlerThread(name);
mSmThread.start();
Looper looper = mSmThread.getLooper();
initStateMachine(name, looper);
}

public StateMachine(String name, Looper looper) {
initStateMachine(name, looper);
}

private void initStateMachine(String name, Looper looper) {
mName = name;
mSmHandler = new SmHandler(looper, this);
}

有三个构造方法,可以外部传入Looper,如果外部不传入就自动 new HandlerThread,最终创建SmHandler,他是StateMachine的内部类,他的角色相当于上面说的动作

addState

  private HashMap<State, StateInfo> mStateInfo =new HashMap<State, StateInfo>();

private final StateInfo addState(State state, State parent) {
if (mDbg) {
Log.d(TAG, "addStateInternal: E state=" + state.getName()
+ ",parent=" + ((parent == null) ? "" : parent.getName()));
}
StateInfo parentStateInfo = null;
if (parent != null) {
parentStateInfo = mStateInfo.get(parent);
if (parentStateInfo == null) {
// Recursively add our parent as it's not been added yet.
parentStateInfo = addState(parent, null);
}
}
StateInfo stateInfo = mStateInfo.get(state);
if (stateInfo == null) {
stateInfo = new StateInfo();
mStateInfo.put(state, stateInfo);
}
// Validate that we aren't adding the same state in two different hierarchies.
if ((stateInfo.parentStateInfo != null) &&
(stateInfo.parentStateInfo != parentStateInfo)) {
throw new RuntimeException("state already added");
}
stateInfo.state = state;
stateInfo.parentStateInfo = parentStateInfo;
stateInfo.active = false;
if (mDbg) Log.d(TAG, "addStateInternal: X stateInfo: " + stateInfo);
return stateInfo;
}


private class StateInfo {
/** The state */
State state;
/** The parent of this state, null if there is no parent */
StateInfo parentStateInfo;
/** True when the state has been entered and on the stack */
boolean active;
/**
* Convert StateInfo to string
*/

@Override
public String toString() {
return "state=" + state.getName() + ",active=" + active
+ ",parent=" + ((parentStateInfo == null) ?
"null" : parentStateInfo.state.getName());
}
}

像状态机添加状态,可以看到最外层使用HashMap存储key=State,value=StateInfo,而StateInfo中储存了当前状态和是否激活,和当前状态的父节点

image.png

假如说目前有六个状态,A->B->C 和 D->E->F,C是B的父节点,B是A的父节点

setInitialState 设置除初始化状态

 public final void setInitialState(State initialState) {
mSmHandler.setInitialState(initialState);
}

private final void setInitialState(State initialState) {
if (mDbg) Log.d(TAG, "setInitialState: initialState=" + initialState.getName());
mInitialState = initialState;
}

假如现在设的初始状态为 C

状态机开始

 public void start() {
// mSmHandler can be null if the state machine has quit.
if (mSmHandler == null) return;
/** Send the complete construction message */
mSmHandler.completeConstruction();
}

private final void completeConstruction() {
//首先拿到状态树的最大深度
int maxDepth = 0;
for (StateInfo si : mStateInfo.values()) {
int depth = 0;
for (StateInfo i = si; i != null; depth++) {
i = i.parentStateInfo;
}
if (maxDepth < depth) {
maxDepth = depth;
}
}
if (mDbg) Log.d(TAG, "completeConstruction: maxDepth=" + maxDepth);
//根据最大深度初始化状态栈,和临时状态栈
mStateStack = new StateInfo[maxDepth];
mTempStateStack = new StateInfo[maxDepth];
setupInitialStateStack();
/** Sending SM_INIT_CMD message to invoke enter methods asynchronously */
//发送初始化消息给Handler
sendMessageAtFrontOfQueue(obtainMessage(SM_INIT_CMD, mSmHandlerObj));
if (mDbg) Log.d(TAG, "completeConstruction: X");
}

//根据初始状态填充mTempStateStack 临时栈
private final void setupInitialStateStack() {
if (mDbg) {
Log.d(TAG, "setupInitialStateStack: E mInitialState="
+ mInitialState.getName());
}
StateInfo curStateInfo = mStateInfo.get(mInitialState);
for (mTempStateStackCount = 0; curStateInfo != null; mTempStateStackCount++) {
mTempStateStack[mTempStateStackCount] = curStateInfo;
curStateInfo = curStateInfo.parentStateInfo;
}
// Empty the StateStack
mStateStackTopIndex = -1;
moveTempStateStackToStateStack();
}

//然后把mTempStateStack翻转填充mStateStack
private final int moveTempStateStackToStateStack() {
int startingIndex = mStateStackTopIndex + 1;
int i = mTempStateStackCount - 1;
int j = startingIndex;
while (i >= 0) {
if (mDbg) Log.d(TAG, "moveTempStackToStateStack: i=" + i + ",j=" + j);
mStateStack[j] = mTempStateStack[i];
j += 1;
i -= 1;
}
mStateStackTopIndex = j - 1;
if (mDbg) {
Log.d(TAG, "moveTempStackToStateStack: X mStateStackTop="
+ mStateStackTopIndex + ",startingIndex=" + startingIndex
+ ",Top=" + mStateStack[mStateStackTopIndex].state.getName());
}
return startingIndex;
}

这里一共做了一下几件事情

  • 计算出状态树的最大深度
  • 根据最大深度初始化俩个数组
  • 然后根据初始的State 填充数组

image.png

此时数组状态,也就是说从mStateStack按照mStateStackTopIndex取出的状态是C

Handler处理初始化

public final void handleMessage(Message msg) {
if (mDbg) Log.d(TAG, "handleMessage: E msg.what=" + msg.what);
/** Save the current message */
mMsg = msg;
if (mIsConstructionCompleted) {
/** Normal path */
processMsg(msg);
} else if (!mIsConstructionCompleted &&
(mMsg.what == SM_INIT_CMD) && (mMsg.obj == mSmHandlerObj)) {
/** Initial one time path. */
//第一次初始化走这里
mIsConstructionCompleted = true;
invokeEnterMethods(0);
} else {
throw new RuntimeException("StateMachine.handleMessage: " +
"The start method not called, received msg: " + msg);
}
//处理状态的切换
performTransitions();
if (mDbg) Log.d(TAG, "handleMessage: X");
}

private final void invokeEnterMethods(int stateStackEnteringIndex) {
for (int i = stateStackEnteringIndex; i <= mStateStackTopIndex; i++) {
if (mDbg) Log.d(TAG, "invokeEnterMethods: " + mStateStack[i].state.getName());
mStateStack[i].state.enter();
mStateStack[i].active = true;
}
}

第一次初始化做了俩件事情

  • 首先把`mIsConstructionCompleted = true;
  • 然后把invokeEnterMethods(0)方法,由于传入的是0,所以把mStateStack中所有的状态都调用mStateStack[i].state.enter();mStateStack[i].active = true;全部激活

如果已经初始化完成了调用processMsg

   private final void processMsg(Message msg) {
//首先从mStateStack取出顶部状态
StateInfo curStateInfo = mStateStack[mStateStackTopIndex];
if (mDbg) {
Log.d(TAG, "processMsg: " + curStateInfo.state.getName());
}
if (isQuit(msg)) {
transitionTo(mQuittingState);
} else {
//调用状态的processMessage方法,如果没处理就调用父节点,如果父节点也不处理,就提跳出循环
while (!curStateInfo.state.processMessage(msg)) {
/**
* Not processed
*/

curStateInfo = curStateInfo.parentStateInfo;
if (curStateInfo == null) {
/**
* No parents left so it's not handled
*/

mSm.unhandledMessage(msg);
break;
}
if (mDbg) {
Log.d(TAG, "processMsg: " + curStateInfo.state.getName());
}
}

}

这个就做了俩件事情

  • 首先从mStateStack取出顶部状态(目前来说就是取出了C)
  • 调用State的processMessage方法,如果没处理就调用父节点,如果父节点也不处理,就提跳出循环

怎么切换状态呢?

 private final void transitionTo(IState destState) {
mDestState = (State) destState;
if (mDbg) Log.d(TAG, "transitionTo: destState=" + mDestState.getName());
}

用这个方法切换状态,参数就是目标状态,我们看到再handleMessage中,除了调用StateprocessMessage方法,还调用了performTransitions来处理状态的切换,看下这个方法

 private void performTransitions() {
/**
* If transitionTo has been called, exit and then enter
* the appropriate states. We loop on this to allow
* enter and exit methods to use transitionTo.
*/

State destState = null;
while (mDestState != null) {
if (mDbg) Log.d(TAG, "handleMessage: new destination call exit");
/**
* Save mDestState locally and set to null
* to know if enter/exit use transitionTo.
*/

destState = mDestState;
mDestState = null;
/**
* Determine the states to exit and enter and return the
* common ancestor state of the enter/exit states. Then
* invoke the exit methods then the enter methods.
*/

StateInfo commonStateInfo = setupTempStateStackWithStatesToEnter(destState);
invokeExitMethods(commonStateInfo);
int stateStackEnteringIndex = moveTempStateStackToStateStack();
invokeEnterMethods(stateStackEnteringIndex);
/**
* Since we have transitioned to a new state we need to have
* any deferred messages moved to the front of the message queue
* so they will be processed before any other messages in the
* message queue.
*/

moveDeferredMessageAtFrontOfQueue();
}

}

假如目标状态为F,先走setupTempStateStackWithStatesToEnter

 private final StateInfo setupTempStateStackWithStatesToEnter(State destState) {
/**
* Search up the parent list of the destination state for an active
* state. Use a do while() loop as the destState must always be entered
* even if it is active. This can happen if we are exiting/entering
* the current state.
*/

mTempStateStackCount = 0;
StateInfo curStateInfo = mStateInfo.get(destState);
do {
mTempStateStack[mTempStateStackCount++] = curStateInfo;
curStateInfo = curStateInfo.parentStateInfo;
} while ((curStateInfo != null) && !curStateInfo.active);
if (mDbg) {
Log.d(TAG, "setupTempStateStackWithStatesToEnter: X mTempStateStackCount="
+ mTempStateStackCount + ",curStateInfo: " + curStateInfo);
}
return curStateInfo;
}

这个就是按照顺序把destState和他的父节点依次填入mTempStateStack,这里返回值为null,因为新的状态都没有被激活过,此时mTemStateStack数据为

image.png

然后调用invokeExitMethods(commonStateInfo);

  private final void invokeExitMethods(StateInfo commonStateInfo) {
while ((mStateStackTopIndex >= 0) &&
(mStateStack[mStateStackTopIndex] != commonStateInfo)) {
State curState = mStateStack[mStateStackTopIndex].state;
if (mDbg) Log.d(TAG, "invokeExitMethods: " + curState.getName());
curState.exit();
mStateStack[mStateStackTopIndex].active = false;
mStateStackTopIndex -= 1;
}
}

这里表示把之前mStateStack数据exit,并且active = false,此时mStateStack状态为

image.png

接下来调用moveTempStateStackToStateStack

 private final int moveTempStateStackToStateStack() {
int startingIndex = mStateStackTopIndex + 1;
int i = mTempStateStackCount - 1;
int j = startingIndex;
while (i >= 0) {
if (mDbg) Log.d(TAG, "moveTempStackToStateStack: i=" + i + ",j=" + j);
mStateStack[j] = mTempStateStack[i];
j += 1;
i -= 1;
}
mStateStackTopIndex = j - 1;
if (mDbg) {
Log.d(TAG, "moveTempStackToStateStack: X mStateStackTop="
+ mStateStackTopIndex + ",startingIndex=" + startingIndex
+ ",Top=" + mStateStack[mStateStackTopIndex].state.getName());
}
return startingIndex;
}

这个就是把mTempStateStack翻转填充mStateStack,此时mStateStack状态为,此时返回值为0

image.png

最后调用

 private final void invokeEnterMethods(int stateStackEnteringIndex) {
for (int i = stateStackEnteringIndex; i <= mStateStackTopIndex; i++) {
if (mDbg) Log.d(TAG, "invokeEnterMethods: " + mStateStack[i].state.getName());
mStateStack[i].state.enter();
mStateStack[i].active = true;
}
}

把mStateStack中的状态激活,此时抓状态就装换完毕了,下次handle处理数据 StateInfo curStateInfo = mStateStack[mStateStackTopIndex]就是拿到的新的状态

这只是讨论其中一种情况切换到了状态F,如果切换到状态B呢?有些区差异,但基本差不多

使用


public class MyStateMachine extends StateMachine {


private static final String TAG = "mmm";

//设置状态改变事件
public static final int MSG_WAKEUP = 1; // 消息:醒
public static final int MSG_TIRED = 2; // 消息:困
public static final int MSG_HUNGRY = 3; // 消息:饿
private static final int MSG_HALTING = 4; // 状态机暂停消息

//创建状态
private State mBoringState = new BoringState();// 默认状态
private State mWorkState = new WorkState(); // 工作
private State mEatState = new EatState(); // 吃
private State mSleepState = new SleepState(); // 睡

/**
* 构造方法
*
* @param name
*/

MyStateMachine(String name) {
super(name);
//加入状态,初始化状态
addState(mBoringState, null);
addState(mSleepState, mBoringState);
addState(mWorkState, mBoringState);
addState(mEatState, mBoringState);

// sleep状态为初始状态
setInitialState(mSleepState);
}

/**
* @return 创建启动person 状态机
*/

public static MyStateMachine makePerson() {
MyStateMachine person = new MyStateMachine("Person");
person.start();
return person;
}


@Override
public void onHalting() {
synchronized (this) {
this.notifyAll();
}
}


/**
* 定义状态:无聊
*/

class BoringState extends State {
@Override
public void enter() {
Log.e(TAG, "############ enter Boring ############");
}

@Override
public void exit() {
Log.e(TAG, "############ exit Boring ############");
}

@Override
public boolean processMessage(Message msg) {
Log.e(TAG, "BoringState processMessage.....");
return true;
}
}

/**
* 定义状态:睡觉
*/

class SleepState extends State {
@Override
public void enter() {
Log.e(TAG, "############ enter Sleep ############");
}

@Override
public void exit() {
Log.e(TAG, "############ exit Sleep ############");
}

@Override
public boolean processMessage(Message msg) {
Log.e(TAG, "SleepState processMessage.....");
switch (msg.what) {
// 收到清醒信号
case MSG_WAKEUP:
Log.e(TAG, "SleepState MSG_WAKEUP");
// 进入工作状态
transitionTo(mWorkState);
//...
//...
//发送饿了信号...
sendMessage(obtainMessage(MSG_HUNGRY));
break;
case MSG_HALTING:
Log.e(TAG, "SleepState MSG_HALTING");

// 转化到暂停状态
transitionToHaltingState();
break;
default:
return false;
}
return true;
}
}


/**
* 定义状态:工作
*/

class WorkState extends State {
@Override
public void enter() {
Log.e(TAG, "############ enter Work ############");
}

@Override
public void exit() {
Log.e(TAG, "############ exit Work ############");
}

@Override
public boolean processMessage(Message msg) {
Log.e(TAG, "WorkState processMessage.....");
switch (msg.what) {
// 收到 饿了 信号
case MSG_HUNGRY:
Log.e(TAG, "WorkState MSG_HUNGRY");
// 吃饭状态
transitionTo(mEatState);
//...
//...
// 发送累了信号...
sendMessage(obtainMessage(MSG_TIRED));
break;
default:
return false;
}
return true;
}
}

/**
* 定义状态:吃
*/

class EatState extends State {
@Override
public void enter() {
Log.e(TAG, "############ enter Eat ############");
}

@Override
public void exit() {
Log.e(TAG, "############ exit Eat ############");
}

@Override
public boolean processMessage(Message msg) {
Log.e(TAG, "EatState processMessage.....");
switch (msg.what) {
// 收到 困了 信号
case MSG_TIRED:
Log.e(TAG, "EatState MSG_TIRED");
// 睡觉
transitionTo(mSleepState);
//...
//...
// 发出结束信号...
sendMessage(obtainMessage(MSG_HALTING));
break;
default:
return false;
}
return true;
}

}
}

调用

 	// 获取 状态机引用
MyStateMachine personStateMachine = MyStateMachine.makePerson();
// 初始状态为SleepState,发送消息MSG_WAKEUP
personStateMachine.sendMessage(MyStateMachine.MSG_WAKEUP);

日志

2021-08-12 18:20:03.137 6035-8981/com.example.myapplication E/mmm: ############ enter Boring ############
2021-08-12 18:20:03.137 6035-8981/com.example.myapplication E/mmm: ############ enter Sleep ############
2021-08-12 18:20:03.137 6035-8981/com.example.myapplication E/mmm: SleepState processMessage.....
2021-08-12 18:20:03.137 6035-8981/com.example.myapplication E/mmm: SleepState MSG_WAKEUP
2021-08-12 18:20:03.137 6035-8981/com.example.myapplication E/mmm: ############ exit Sleep ############
2021-08-12 18:20:03.137 6035-8981/com.example.myapplication E/mmm: ############ enter Work ############
2021-08-12 18:20:03.137 6035-8981/com.example.myapplication E/mmm: WorkState processMessage.....
2021-08-12 18:20:03.137 6035-8981/com.example.myapplication E/mmm: WorkState MSG_HUNGRY
2021-08-12 18:20:03.137 6035-8981/com.example.myapplication E/mmm: ############ exit Work ############
2021-08-12 18:20:03.137 6035-8981/com.example.myapplication E/mmm: ############ enter Eat ############
2021-08-12 18:20:03.137 6035-8981/com.example.myapplication E/mmm: EatState processMessage.....
2021-08-12 18:20:03.137 6035-8981/com.example.myapplication E/mmm: EatState MSG_TIRED
2021-08-12 18:20:03.137 6035-8981/com.example.myapplication E/mmm: ############ exit Eat ############
2021-08-12 18:20:03.137 6035-8981/com.example.myapplication E/mmm: ############ enter Sleep ############
2021-08-12 18:20:03.137 6035-8981/com.example.myapplication E/mmm: SleepState processMessage.....
2021-08-12 18:20:03.137 6035-8981/com.example.myapplication E/mmm: SleepState MSG_HALTING
2021-08-12 18:20:03.137 6035-8981/com.example.myapplication E/mmm: ############ exit Sleep ############
2021-08-12 18:20:03.137 6035-8981/com.example.myapplication E/mmm: ############ exit Boring ############

这里最重要的是要分清楚 状态和事件,首先触发某个事件,导致了状态的改变, 闹铃触发起床事件,导致状态的改变睡觉-->工作

这里首先把所有的状态都加入到了状态机,然后设置初始状态是为Sleep,然后就调用了start

所以开始就会把Sleep和其父节点加入状态栈中,然后调用enter,然后调用personStateMachine.sendMessage(MyStateMachine.MSG_WAKEUP);这里可以这样理解,sendMessage表示都动作,MyStateMachine.MSG_WAKEUP表示事件,然后SleepState接收事件情,触发状态的改变 transitionTo(mWorkState);

也就是说当前状态SleepState只接受接收事件MSG_WAKEUP,如果是其他事件,当前状态不接受,也就不会改变状态,比如当前状态时睡觉,触发事件吃饭,睡觉时不能吃饭,所以是个无效事件

收起阅读 »

Android 多返回栈技术详解

用户通过系统返回按钮导航回去的一组页面,在开发中被称为返回栈 (back stack)。多返回栈即一堆 "返回栈",对多返回栈的支持是在 Navigation 2.4.0-alpha01 和 Fragment 1.4.0-alpha01 中开始的。本文将为您展...
继续阅读 »

用户通过系统返回按钮导航回去的一组页面,在开发中被称为返回栈 (back stack)。多返回栈即一堆 "返回栈",对多返回栈的支持是在 Navigation 2.4.0-alpha01Fragment 1.4.0-alpha01 中开始的。本文将为您展开多返回栈的技术详解。


系统返回按钮的乐趣


无论您在使用 Android 全新的 手势导航 还是传统的导航栏,用户的 "返回" 操作是 Android 用户体验中关键的一环,把握好返回功能的设计可以使应用更加贴近整个生态系统。


在最简单的应用场景中,系统返回按钮仅仅 finish 您的 Activity。在过去您可能需要覆写 Activity 的 onBackPressed() 方法来自定义返回操作,而在 2021 年您无需再这样操作。我们已经在 OnBackPressedDispatcher 中提供了 针对自定义返回导航的 API。实际上这与 FragmentManagerNavController已经 添加的 API 相同。


这意味着当您使用 Fragments 或 Navigation 时,它们会通过 OnBackPressedDispatcher 来确保您调用了它们返回栈的 API,系统的返回按钮会将您推入返回栈的页面逐层返回。


多返回栈不会改变这个基本逻辑。系统的返回按钮仍然是一个单向指令 —— "返回"。这对多返回栈 API 的实现机制有深远影响。


Fragment 中的多返回栈


在 surface 层级,对于 多返回栈的支持 貌似很直接,但其实需要额外解释一下 "Fragment 返回栈" 到底是什么。FragmentManager 的返回栈其实包含的不是 Fragment,而是由 Fragment 事务组成的。更准确地说,是由那些调用了 addToBackStack(String name) API 的事务组成的。


这就意味着当您调用 commit() 提交了一个调用过 addToBackStack() 方法的 Fragment 事务时,FragmentManager 会执行所有您在事务中所指定的操作 (比如 替换操作),从而将每个 Fragment 转换为预期的状态。然后 FragmentManager 会将该事务作为它返回栈的一部分。


当您调用 popBackStack() 方法时 (无论是直接调用,还是通过系统返回键以 FragmentManager 内部机制调用),Fragment 返回栈的最上层事务会从栈中弹出 -- 比如新添加的 Fragment 会被移除,隐藏的 Fragment 会显示。这会使得 FragmentManager 恢复到最初提交 Fragment 事务之前的状态。



作者注: 这里有一个非常重要的事情需要大家注意,在同一个 FragmentManager 中绝对不应该将含有 addToBackStack() 的事务和不含的事务混在一起: 返回栈的事务无法察觉返回栈之外的 Fragment 事务的修改 —— 当您从堆栈弹出一个非常不确定的元素时,这些事务从下层替换出来的时候会撤销之前未添加到返回栈的修改。



也就是说 popBackStack() 变成了销毁操作: 任何已添加的 Fragment 在事务被弹出的时候都会丢失它的状态。换言之,您会失去视图的状态,任何所保存的实例状态 (Saved Instance State),并且任何绑定到该 Fragment 的 ViewModel 实例都会被清除。这也是该 API 和新的 saveBackStack() 方法之间的主要区别。saveBackStack() 可以实现弹出事务所实现的返回效果,此外它还可以确保视图状态、已保存的实例状态,以及 ViewModel 实例能够在销毁时被保存。这使得 restoreBackStack() API 后续可以通过已保存的状态重建这些事务和它们的 Fragment,并且高效 "重现" 已保存的全部细节。太神奇了!


而实现这个目的必须要解决大量技术上的问题。


排除 Fragment 在技术上的障碍


虽然 Fragment 总是会保存 Fragment 的视图状态,但是 Fragment 的 onSaveInstanceState() 方法只有在 Activity 的 onSaveInstanceState() 被调用时才会被调用。为了能够保证调用 saveBackStack() 时 SavedInstanceState 会被保存,我们 需要在 Fragment 生命周期切换 的正确时机注入对 onSaveInstanceState() 的调用。我们不能调用得太早 (您的 Fragment 不应该在 STARTED 状态下保存状态),也不能调用得太晚 (您需要在 Fragment 被销毁之前保存状态)。


这样的前提条件就开启了需要 解决 FragmentManager 转换到对应状态的问题,以此来保障有一个地方能够将 Fragment 转换为所需状态,并且处理可重入行为和 Fragment 内部的状态转换。


在 Fragment 的重构工作进行了 6 个月,进行了 35 次修改时,发现 Postponed Fragment 功能已经严重损坏,这一问题使得被推迟的事务处于一个中间状态 —— 既没有被提交也并不是未被提交。之后的 65 个修改和 5 个月的时间里,我们几乎重写了 FragmentManager 管理状态、延迟状态切换和动画的内部代码,具体请参见我们之前的文章《全新的 Fragment: 使用新的状态管理器》。


Fragment 中值得期待的地方


随着技术问题的逐步解决,包括更加可靠和更易理解的 FragmentManager,我们新增加了两个 API: saveBackStack()restoreBackStack()


如果您不使用这些新增 API,则一切照旧: 单个 FragmentManager 返回栈和之前的功能相同。现有的 addToBackStack() 保持不变 —— 您可以将 name 赋值为 null 或者任意 name。然而,当您使用多返回栈时,name 的作用就非常重要了: 在您调用 saveBackStack() 和之后的 restoreBackStack() 方法时,它将作为 Fragment 事务的唯一的 key。


举个例子,会更容易理解。比如您已经添加了一个初始的 Fragment 到 Activity,然后提交了两个事务,每个事务中包含一个单独的 replace 操作:


// 这是用户看到的初始的 Fragment
fragmentManager.commit {
setReorderingAllowed(true)
replace<HomeFragment>(R.id.fragment_container)
}
// 然后,响应用户操作,我们在返回栈中增加了两个事务
fragmentManager.commit {
setReorderingAllowed(true)
replace<ProfileFragment>(R.id.fragment_container)
addToBackStack(“profile”)
}
fragmentManager.commit {
setReorderingAllowed(true)
replace<EditProfileFragment>(R.id.fragment_container)
addToBackStack(“edit_profile”)
}

也就是说我们的 FragmentManager 会变成这样:


△ 提交三次之后的 FragmentManager 的状态


△ 提交三次之后的 FragmentManager 的状态


比如说我们希望将 profile 页换出返回栈,然后切换到通知 Fragment。这就需要调用 saveBackStack() 并且紧跟一个新的事务:


fragmentManager.saveBackStack("profile")
fragmentManager.commit {
setReorderingAllowed(true)
replace<NotificationsFragment>(R.id.fragment_container)
addToBackStack("notifications")
}

现在我们添加 ProfileFragment 的事务和添加 EditProfileFragment 的事务都保存在 "profile" 关键字下。这些 Fragment 已经完全将状态保存,并且 FragmentManager 会随同事务状态一起保持它们的状态。很重要的一点: 这些 Fragment 的实例并不在内存中或者在 FragmentManager 中 —— 存在的仅仅只有状态 (以及任何以 ViewModel 实例形式存在的非配置状态)。


△ 我们保存 profile 返回栈并且添加一个新的 commit 后的 FragmentManager 状态


△ 我们保存 profile 返回栈并且添加一个新的 commit 后的 FragmentManager 状态


替换回来非常简单: 我们可以在 "notifications" 事务中同样调用 saveBackStack() 操作,然后调用 restoreBackStack():


fragmentManager.saveBackStack(“notifications”)
fragmentManager.restoreBackStack(“profile”)

这两个堆栈项高效地交换了位置:


△ 交换堆栈项后的 FragmentManager 状


△ 交换堆栈项后的 FragmentManager 状态


维持一个单独且活跃的返回栈并且将事务在其中交换,这保证了当返回按钮被点击时,FragmentManager 和系统的其他部分可以保持一致的响应。实际上,整个逻辑并未改变,同之前一样,仍然弹出 Fragment 返回栈的最后一个事务。


这些 API 都特意按照最小化设计,尽管它们会产生潜在的影响。这使得开发者可以基于这些接口设计自己的结构,而无需通过任何非常规的方式保存 Fragment 的视图状态、已保存的实例状态、非配置的状态。


当然了,如果您不希望在这些 API 之上构建您的框架,那么可以使用我们所提供的框架进行开发。


使用 Navigation 将多返回栈适配到任意屏幕类型


Navigation Component 最初 是作为通用运行时组件进行开发的,其中不涉及 View、Fragment、Composable 或者其他屏幕显示相关类型及您可能会在 Activity 中实现的 "目的地界面"。然而,NavHost 接口 的实现中需要考虑这些内容,通过它添加一个或者多个 Navigator 实例时,这些实例 确实 清楚如何与特定类型的目的地进行交互。


这也就意味着与 Fragment 的交互逻辑全部封装在了 navigation-fragment 开发库和它其中的 FragmentNavigatorDialogFragmentNavigator 中。类似的,与 Composable 的交互逻辑被封装在完全独立的 navigation-compose 开发库和它的 ComposeNavigator 中。这里的抽象设计意味着如果您希望仅仅通过 Composable 构建您的应用,那么当您使用 Navigation Compose 时无需任何涉及到 Fragment 的依赖。


该级别的分离意味着 Navigation 中有两个层次来实现多返回栈:



  • 保存独立的 NavBackStackEntry 实例状态,这些实例组成了 NavController 返回栈。这是属于 NavController 的职责。

  • 保存 Navigator 针对每个 NavBackStackEntry 的特定状态 (比如与 FragmentNavigator 目的地相关联的 Fragment)。这是属于 Navigator 的职责。


仍需特别注意那些 尚未 更新的 Navigator,它们无法支持保存自身状态。底层的 Navigator API 已经整体重写来支持状态保存 (您需要覆写新增的 navigate()popBackStack() API 的重载方法,而不是覆写之前的版本),即使 Navigator 并未更新,NavController 仍会保存 NavBackStackEntry 的状态 (在 Jetpack 世界中向后兼容是非常重要的)。



备注: 通过绑定 TestNavigatorState 使其成为一个 mini-NavController 可以实现在新的 Navigator API 上更轻松、独立地测试您自定义的 Navigator



如果您仅仅在应用中使用 Navigation,那么 Navigator 这个层面更多的是实现细节,而不是您需要直接与之交互的内容。可以这么说,我们已经完成了将 FragmentNavigatorComposeNavigator 迁移到新的 Navigator API 的工作,使其能够正确地保存和恢复它们的状态,在这个层面上您无需再做任何额外工作。


在 Navigation 中启用多返回栈


如果您正在使用 NavigationUI,它是用于连接您的 NavController 到 Material 视图组件的一系列专用助手,您会发现对于菜单项、BottomNavigationView (现在叫 NavigationRailView) 和 NavigationView,多返回栈是 默认启用 的。这就意味着结合 navigation-fragmentnavigation-ui 使用就可以。


NavigationUI API 是基于 Navigation 的其他公共 API 构建的,确保您可以准确地为自定义组件构建您自己的版本。保证您可以构建所需的自定义组件。启用保存和恢复返回栈的 API 也不例外,在 Navigation XML 中通过 NavOptions 上的新 API,也就是 navOptions Kotlin DSL,以及 popBackStack() 的重载方法可以帮助您指定 pop 操作保存状态或者指定 navigate 操作来恢复之前已保存的状态。


比如,在 Compose 中,任何全局的导航模式 (无论是底部导航栏、导航边栏、抽屉式导航栏或者任何您能想到的形式) 都可以使用我们在与 底部导航栏集成 所介绍的相同的技术,并且结合 saveStaterestoreState 属性一起调用 navigate():


onClick = {
navController.navigate(screen.route) {
// 当用户选择子项时在返回栈中弹出到导航图中的起始目的地
// 来避免太过臃肿的目的地堆栈
popUpTo(navController.graph.findStartDestination().id) {
saveState = true
}

// 当重复选择相同项时避免相同目的地的多重拷贝
launchSingleTop = true
// 当重复选择之前已经选择的项时恢复状态
restoreState = true
}
}

保存状态,锁定用户


对用户来说,最令人沮丧的事情之一便是丢失之前的状态。这也是为什么 Fragment 用一整页来讲解 保存与 Fragment 相关的状态,而且也是我非常乐于更新每个层级来支持多返回栈的原因之一:



  • Fragments (比如完全不使用 Navigation Component): 通过使用新的 FragmentManager API,也就是 saveBackStackrestoreBackStack


  • 核心的 Navigation 运行时: 添加可选的新的 NavOptions 方法用于 restoreState(恢复状态) 和 saveState (保存状态) 以及新的 popBackStack() 的重载方法,它同样可以传入一个布尔型的 saveState 参数 (默认是 false)。


  • 通过 Fragment 实现 Navigation: FragmentNavigator 现在利用新的 NavigatorAPI,通过使用 Navigation 运行时 API 将 Navigation 运行时 API 转换为 Fragment API。


  • NavigationUI: 每当它们弹出返回栈时,onNavDestinationSelected()、NavigationBarView.setupWithNavController()NavigationView.setupWithNavController() 现在默认使用 restoreState 和 saveState 这两个新的 NavOption。也就意味着 当升级到 Navigation 2.4.0-alpha01 或者更高版本后,任何使用 NavigationUI API 的应用无需修改代码即可实现多返回栈



如果您希望了解 更多使用该 API 的示例,请参考 NavigationAdvancedSample (它是最新更新的,且不包含任何用于支持多返回栈的 NavigationExtensions 代码)。


对于 Navigation Compose 的示例,请参考 Tivi。


如果您遇到任何问题,请使用官方的问题追踪页面提交关于 Fragment 或者 Navigation 的 bug,我们会尽快处理。


收起阅读 »

『ios』NSProxy解决NStimer循环引用的思考

1.nstimer为什么回循环引用2.NSObject如何解决NStimer循环引用3.NSProxy如何解决NStimer循环引用4.为什么要用NSProxy,优势在哪围绕上面几个问题我们来思考一下1.nstimer为什么回循环引用self.timer = ...
继续阅读 »

1.nstimer为什么回循环引用
2.NSObject如何解决NStimer循环引用
3.NSProxy如何解决NStimer循环引用
4.为什么要用NSProxy,优势在哪
围绕上面几个问题我们来思考一下

1.nstimer为什么回循环引用

self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(timerTest) userInfo:nil repeats:YES];
self强引用timer,timer强引用self.
那是否可以改为

__weak typeof(self)weakSelf = self;
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:weakSelf selector:@selector(timerTest) userInfo:nil repeats:YES];
答案也是不行的。target其实里面做的是一个赋值操作。
我们可以从gnu里面的源码中发现这个问题。所以在外面设置weakSelf是不能解决这个问题,如果是block那是可以解决的。所以我们现在就要添加一个中间变量,来解开这个环。



2.NSObject如何解决NStimer循环引用
这里我们可以利用消息转发阶段的forwardingTargetForSelector函数来解决这个问题。

self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:[XHObjectProxy proxyWithTarget:self] selector:@selector(timerTest) userInfo:nil repeats:YES];

@interface XHObjectProxy : NSObject
+ (instancetype)proxyWithTarget:(id)target;
@property (weak, nonatomic) id target;
@end

@implementation XHObjectProxy
+ (instancetype)proxyWithTarget:(id)target
{
XHObjectProxy *proxy = [[XHObjectProxy alloc] init];
proxy.target = target;
return proxy;
}
- (id)forwardingTargetForSelector:(SEL)aSelector
{
return self.target;
}
@end

设置XHObjectProxy这个中间件,然后传入target,并且用一个弱引用的target来接受他。从而实现解开循环。
当timer调用timerTest方法的时候,很显然XHObjectProxy没有这个方法,所以就进入了消息转发阶段。
当到达forwardingTargetForSelector 这个阶段的时候,直接返回self.target也就是[XHObjectProxy proxyWithTarget:self] 中的self。所以可以解决这个问题。

3.NSProxy如何解决NStimer循环引用
NSProxy跟NSObject一样,也是一个基类,是一个实现<NSObject>协议的基类。

@class NSMethodSignature, NSInvocation;

NS_ASSUME_NONNULL_BEGIN

NS_ROOT_CLASS
@interface NSProxy <NSObject> {
__ptrauth_objc_isa_pointer Class isa;
}

+ (id)alloc;
+ (id)allocWithZone:(nullable NSZone *)zone NS_AUTOMATED_REFCOUNT_UNAVAILABLE;
+ (Class)class;

- (void)forwardInvocation:(NSInvocation *)invocation;
- (nullable NSMethodSignature *)methodSignatureForSelector:(SEL)sel NS_SWIFT_UNAVAILABLE("NSInvocation and related APIs not available");
- (void)dealloc;
- (void)finalize;
@property (readonly, copy) NSString *description;
@property (readonly, copy) NSString *debugDescription;
+ (BOOL)respondsToSelector:(SEL)aSelector;

- (BOOL)allowsWeakReference API_UNAVAILABLE(macos, ios, watchos, tvos);
- (BOOL)retainWeakReference API_UNAVAILABLE(macos, ios, watchos, tvos);

// - (id)forwardingTargetForSelector:(SEL)aSelector;

@end

里面有消息转发第三阶段的两个方法

- (void)forwardInvocation:(NSInvocation *)invocation;
- (nullable NSMethodSignature *)methodSignatureForSelector:(SEL)sel

所以可以这么写

@interface XHRealProxy : NSProxy
+ (instancetype)proxyWithTarget:(id)target;
@property (weak, nonatomic) id target;
@end
@implementation XHRealProxy

+ (instancetype)proxyWithTarget:(id)target
{
// NSProxy对象不需要调用init,因为它本来就没有init方法
XHRealProxy *proxy = [XHRealProxy alloc];
proxy.target = target;
return proxy;
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel
{
return [self.target methodSignatureForSelector:sel];
}

- (void)forwardInvocation:(NSInvocation *)invocation
{
[invocation invokeWithTarget:self.target];
}
@end

经过测试我们发现在同时写forwardingTargetForSelector 和 methodSignatureForSelector 这两个方法的情况下,不会执行第二阶段的转发,直接进入methodSignatureForSelector。

4.为什么要用NSProxy,优势在哪
我们一般的方法调用都是通过objc_msgSend来进行的,然后经过消息转发,一步一步的进行。
这张图是NSObject的方法调用





  ViewController *vc = [[ViewController alloc] init];

XHRealProxy *proxy1 = [XHRealProxy proxyWithTarget:vc];

XHObjectProxy *proxy2 = [XHObjectProxy proxyWithTarget:vc];
通过代码进行测试,发现继承NSProxy的类,直接执行了methodSignatureForSelector方法,而继承NSObject的类,直接执行了forwardingTargetForSelector方法。
然后又进了nstimer的方法进行测试。发现不管是NSProxy 还是 NSObject 都会先直接走forwardingTargetForSelector这个方法。
觉得很奇怪,然后去gnu上去看源码。发现里面重写了isKindOfClass这个方法,然后里面直接调用了methodSignatureForSelector

/**
* Calls the -forwardInvocation: method to determine if the 'real' object
* referred to by the proxy is an instance of the specified class.
* Returns the result.<br />
* NB. The default operation of -forwardInvocation: is to raise an exception.
*/

- (BOOL) isKindOfClass: (Class)aClass
{
NSMethodSignature *sig;
NSInvocation *inv;
BOOL ret;

sig = [self methodSignatureForSelector: _cmd];
inv = [NSInvocation invocationWithMethodSignature: sig];
[inv setSelector: _cmd];
[inv setArgument: &aClass atIndex: 2];
[self forwardInvocation: inv];
[inv getReturnValue: &ret];
return ret;
}

/**
* Calls the -forwardInvocation: method to determine if the 'real' object
* referred to by the proxy is an instance of the specified class.
* Returns the result.<br />
* NB. The default operation of -forwardInvocation: is to raise an exception.
*/

- (BOOL) isMemberOfClass: (Class)aClass
{
NSMethodSignature *sig;
NSInvocation *inv;
BOOL ret;

sig = [self methodSignatureForSelector: _cmd];
inv = [NSInvocation invocationWithMethodSignature: sig];
[inv setSelector: _cmd];
[inv setArgument: &aClass atIndex: 2];
[self forwardInvocation: inv];
[inv getReturnValue: &ret];
return ret;
}

- (BOOL) conformsToProtocol: (Protocol*)aProtocol
{
NSMethodSignature *sig;
NSInvocation *inv;
BOOL ret;

sig = [self methodSignatureForSelector: _cmd];
inv = [NSInvocation invocationWithMethodSignature: sig];
[inv setSelector: _cmd];
[inv setArgument: &aProtocol atIndex: 2];
[self forwardInvocation: inv];
[inv getReturnValue: &ret];
return ret;
}

为什么NSProxy的效率更高?因为他是直接走消息转发第三步methodSignatureForSelector,而nsobject需要走objc_msgSend整个流程,所以效率更高。


作者:butterflyer
链接:https://www.jianshu.com/p/a079fd0f7d61
收起阅读 »

MQTT通信协议介绍

一:MQTT协议介绍MQTT(Message Queuing Telemetry Transport,消息队列遥测传输)是IBM开发的一个即时通讯协议,它是一种轻量级的、基于代理的“发布/订阅”模式的消息传输协议。其具有协议简洁、小巧、可扩展性强、省流量、等优...
继续阅读 »

一:MQTT协议介绍

MQTT(Message Queuing Telemetry Transport,消息队列遥测传输)是IBM开发的一个即时通讯协议,它是一种轻量级的、基于代理的“发布/订阅”模式的消息传输协议。其具有协议简洁小巧可扩展性强省流量、等优点。可在不可靠的网络环境中进行扩展,适用于设备硬件存储空间或网络带宽有限的场景。使用MQTT协议,消息发送者与接收者不受时间和空间的限制。物联网平台支持设备使用MQTT协议接入。

二:MQTT协议的主要特点

1、使用发布/订阅消息模式,提供一对多的消息发布,解除应用程序耦合

2、使用 TCP/IP 提供网络连接

3、有三种级别的消息发布服务质量QoS(Quality of Service)

  • “至多一次”(Qos = 0),消息发布完全依赖底层 TCP/IP 网络。会发生消息丢失或重复。这一级别可用于如下情况,环境传感器数据,丢失一次读记录无所谓,因为不久后还会有第二次发送。

  • “至少一次”(Qos = 1),确保消息到达,但消息重复可能会发生。

  • “只有一次”(Qos = 2),确保消息到达一次。消息丢失和重复都是不可接受的,使用这个服务质量等级会有额外的开销。

三:MQTT协议的核心角色

MQTT 协议主要有三大核心角色:发布者(Publisher)、Broker代理服务器(转发者) 、订阅者(Subscriber) 。其中消息的发布者和订阅者都是客户端(Client)角色,消息代理是服务器,消息发布者可以同时是订阅者。 当Client发布某个主题的消息时,Broker会将该消息分发给任何已订阅该主题的Client。

image.png

MQTT服务器

MQTT服务端通常是一台服务器。它是MQTT信息传输的枢纽,负责将MQTT客户端发送来的信息传递给MQTT客户端。MQTT服务端还负责管理MQTT客户端。确保客户端之间的通讯顺畅,保证MQTT消息得以正确接收和准确投递。

MQTT客户端

MQTT客户端可以向服务端发布信息,也可以从服务端收取信息。我们把客户端发送信息的行为成为“发布”信息。而客户端要想从服务端收取信息,则首先要向服务端“订阅”信息。“订阅”信息这一操作很像我们在微信中订阅的公众号,当公众号更新时,微信会向订阅了该公众号的用户发送信息,告诉他们有文章更新了。

MQTT主题

刚刚我们在讲解MQTT客户端订阅信息时,使用了用户在微信中订阅公众号的这个例子。在MQTT通讯中,客户端所订阅的肯定不是一个个公众号,而是一个个“主题”。MQTT服务端在管理MQTT信息通讯时,就是使用“主题”来控制的。

四:连接MQTT服务端

MQTT客户端要想通讯,必须经过MQTT服务端。因此MQTT客户端无论是发布消息还是订阅消息,首先都要连接MQTT服务端。下面我们看一下MQTT客户端连接服务端的详细过程。

1、首先MQTT客户端将会向服务端发送连接请求。该请求实际上是一个包含有连接请求信息的数据包。这个数据包的官方名称为CONNECT image.png 2、MQTT服务端收到客户端连接请求后,会向客户端发送连接确认。同样的,该确认也是一个数据包。这个数据包官方名称为CONNACK

image.png

以上就是MQTT客户端在连接服务端的两步操作。接下来,我们一起来了解一下客户端在连接服务端时所发送的CONNECT报文内容。

clientId – 客户端ID

ClientId是MQTT客户端的标识。MQTT服务端用该标识来识别客户端。因此ClientId必须是独立的。如果两个MQTT客户端使用相同ClientId标识,服务端会把它们当成同一个客户端来处理。通常ClientId是由一串字符所构成的

username(用户名)和password(密码)

这里的用户名和密码是用于客户端连接服务端时进行认证需要的。有些MQTT服务端需要客户端在连接时提供用户名和密码。只有客户端正确提供了用户名和密码后,才能连接服务端。否则服务端将会拒绝客户端连接,那么客户端也就无法发布和订阅消息了。

username(用户名)和password(密码)是可选的CONNECT信息。也就是说,有些服务端开启了客户端用户密码认证,这种服务端需要客户端在连接时正确提供认证信息才能连接。当然,那些没有开启用户密码认证的服务端无需客户端提供用户名和密码认证信息

cleanSession – 清除会话

要说明cleanSession的具体含义,首先要从MQTT网络环境讲起。MQTT客户端与服务端的连接可能不是非常稳定,在不稳定的网络环境下,要想保证所有信息传输都能够做到准确无误,这是非常困难的。

为了保证重要的MQTT报文可以被客户端准确无误的收到。在服务端向客户端发送报文后,客户端会向服务端返回一个确认报文。如果服务端没有收到客户端返回的确认报文,那么服务端就会认为刚刚发送给客户端的报文没有被准确无误的送达。在这种情况下,服务端将会执行以下两个操作:

1、将尚未被客户端确认的报文保存起来

2、再次尝试向客户端发送报文,并且再次等待客户端发来确认信息。

如果 cleanSession 被设置为“true”。那么服务端不需要客户端确认收到报文,也不会保存任何报文。在这种情况下,即使客户端错过了服务端发来的报文,也没办法让服务端再次发送报文。 反过来,如果我们将 cleanSession 设置为”false”。那么服务端就知道,后续通讯中,客户端可能会要求我保存没有收到的报文。

keepAlive – 心跳时间间隔

MQTT服务端运行过程中,当有客户端因为某种原因断开了与服务端的连接,服务端需要实时了解这一情况。KeepAlive正是用于服务端了解客户端连接情况的。

客户端在没有向服务端发送信息时,可以定时向服务端发送一条消息。这条用于心跳机制的消息也被称作心跳请求。

心跳请求的作用正是用于告知服务端,当前客户端依然在线。服务端在收到客户端的心跳请求后,会回复一条消息。这条回复消息被称作心跳响应。 image.png

客户端在心跳间隔时间内,如果有消息发布,那就直接发布消息而不发布心跳请求,但是在心跳间隔时间内,客户端没有消息发布,那么它就会发布一条心跳请求给服务端,这个心跳请求的目的就是为了告诉服务端,我还在线。

五: SUBSCRIBE – 订阅主题

当客户端连接到服务端后,除了可以发布消息,也可以接收消息。所有MQTT消息都有主题。客户端要想接收消息,首先要订阅该消息的主题。这样,当有客户端向该主题发布消息后,订阅了该主题的客户端就能接收到消息了。

客户端要想订阅主题,首先要向服务端发送主题订阅请求。客户端是通过向服务端发送SUBSCRIBE报文来实现这一请求的。客户端在订阅主题时也可以明确QoS。服务端会根据SUBSCRIBE中的QoS来提供相应的服务保证。

收起阅读 »

WorkManager :工作链

工作链工作链也是WorkManager的一个非常重要的功能。你可以使用WorkManager创建工作链并将其加入队列。工作链用于指定多个依存任务并定义这些任务的运行顺序。当需要以特定顺序运行多个任务时,此功能尤其有用。例如,假设您的应用有三个 OneTimeW...
继续阅读 »

工作链

工作链也是WorkManager的一个非常重要的功能。

你可以使用WorkManager创建工作链并将其加入队列。工作链用于指定多个依存任务并定义这些任务的运行顺序。当需要以特定顺序运行多个任务时,此功能尤其有用。

例如,假设您的应用有三个 OneTimeWorkRequest对象:workAworkB 和 workC。这些任务必须按该顺序运行。如需对这些任务进行排队,请使用 WorkManager.beginWith(OneTimeWorkRequest)方法创建一个序列,并传递第一个 OneTimeWorkRequest对象;该方法会返回一个 WorkContinuation对象,以定义一个任务序列。然后,使用 WorkContinuation.then(OneTimeWorkRequest)依次添加剩余的 OneTimeWorkRequest 对象;最后,使用 WorkContinuation.enqueue()对整个序列进行排队:

WorkManager.getInstance(myContext)
  .beginWith(workA)
       // Note: WorkManager.beginWith() returns a
       // WorkContinuation object; the following calls are
       // to WorkContinuation methods
  .then(workB)    // FYI, then() returns a new WorkContinuation instance
  .then(workC)
  .enqueue();

A-B-C.png WorkManager会根据每个任务的指定约束,按请求的顺序运行任务。如果有任务返回Result.failure(),整个序列结束。

您还可以将多个 OneTimeWorkRequest对象传递给任何 beginWith(List)和 then(List)调用。如果您向单个方法调用传递多个 OneTimeWorkRequest对象,WorkManager会并行运行所有这些任务,然后再运行序列中的其他任务。例如:

WorkManager.getInstance(myContext)
   // First, run all the A tasks (in parallel):
  .beginWith(Arrays.asList(workA1, workA2, workA3))
   // ...when all A tasks are finished, run the single B task:
  .then(workB)
   // ...then run the C tasks (in parallel):
  .then(Arrays.asList(workC1, workC2))
  .enqueue();

A1A2A3-B-C1C2.png

您可以使用 WorkContinuation.combine(List)方法联接多个任务链来创建更为复杂的序列。例如,假设您要运行像这样的序列:

workmanager-chain.svg

如需设置该序列,请创建两个单独的链,然后将它们联接成第三个链:

WorkContinuation chain1 = WorkManager.getInstance(myContext)
  .beginWith(workA)
  .then(workB);
WorkContinuation chain2 = WorkManager.getInstance(myContext)
  .beginWith(workC)
  .then(workD);
WorkContinuation chain3 = WorkContinuation
  .combine(Arrays.asList(chain1, chain2))
  .then(workE);
chain3.enqueue();

在这种情况下,WorkManager会在 workB 之前运行 workA。它还会在 workD 之前运行 workC。在 workB 和 workD 都完成后,WorkManager会运行 workE

注意:虽然 WorkManager会按顺序运行各个子链,但并不保证 chain1 中的任务如何与 chain2 中的任务重叠。例如,workB 可能会在 workC 的前面或后面运行,或者两者也可能会同时运行。唯一可以保证的就是每个子链中的任务将按顺序运行,即 workB 会在 workA 完成之后再启动。

上面我们介绍了工作链的基本知识和基本的方法。下面我们来看一个示例。在本例中,有 3 个不同的工作器作业配置为运行(可能并行运行)。然后这些工作器的结果将联接起来,并传递给正在缓存的工作器作业。最后,该作业的输出将传递到上传工作器,由上传工作器将结果上传到远程服务器。


WorkManager.getInstance(myContext)
  // Candidates to run in parallel
  .beginWith(Arrays.asList(plantName1, plantName2, plantName3))
  // Dependent work (only runs after all previous work in chain)
  .then(cache)
  .then(upload)
  // Call enqueue to kick things off
  .enqueue();

输入合并器

当您链接 OneTimeWorkRequest 实例时,父级工作请求的输出将作为子级的输入传入。因此,在上面的示例中,plantName1plantName2 和 plantName3 的输出将作为 cache 请求的输入传入。

为了管理来自多个父级工作请求的输入,WorkManager 使用 InputMerger。

WorkManager 提供两种不同类型的 InputMerger

  • OverwritingInputMerger会尝试将所有输入中的所有键添加到输出中。如果发生冲突,它会覆盖先前设置的键。
  • ArrayCreatingInputMerger会尝试合并输入,并在必要时创建数组。

OverwritingInputMerger

OverwritingInputMerger 是默认的合并方法。如果合并过程中存在键冲突,键的最新值将覆盖生成的输出数据中的所有先前版本。

例如,如果每种植物的输入都有一个与其各自变量名称("plantName1""plantName2" 和 "plantName3")匹配的键,传递给 cache 工作器的数据将具有三个键值对。

chaining-overwriting-merger-example.png 如果存在冲突,那么最后一个工作器将在争用中“取胜”,其值将传递给 cache

chaining-overwriting-merger-conflict.png 由于工作请求是并行运行的,因此无法保证其运行顺序。在上面的示例中,plantName1 可以保留值 "tulip" 或 "elm",具体取决于最后写入的是哪个值。如果有可能存在键冲突,并且您需要在合并器中保留所有输出数据,那么 ArrayCreatingInputMerger 可能是更好的选择。

ArrayCreatingInputMerger

对于上面的示例,假设我们要保留所有植物名称工作器的输出,则应使用 ArrayCreatingInputMerger

OneTimeWorkRequest cache = new OneTimeWorkRequest.Builder(PlantWorker.class)
      .setInputMerger(ArrayCreatingInputMerger.class)
      .setConstraints(constraints)
      .build();

ArrayCreatingInputMerger 将每个键与数组配对。如果每个键都是唯一的,您会得到一系列一元数组。

chaining-array-merger-example.png 如果存在任何键冲突,那么所有对应的值会分组到一个数组中。

chaining-array-merger-conflict.png

链接和工作状态

只要工作成功完成(即,返回 Result.success()),OneTimeWorkRequest 链便会按顺序执行。运行时,工作请求可能会失败或被取消,这会对依存工作请求产生下游影响。

当第一个 OneTimeWorkRequest 被加入工作请求链队列时,所有后续工作请求会被屏蔽,直到第一个工作请求的工作完成为止。

chaining-enqueued-all-blocked.png

在加入队列且满足所有工作约束后,第一个工作请求开始运行。如果工作在根 OneTimeWorkRequest 或 List<OneTimeWorkRequest> 中成功完成(即返回 Result.success()),系统会将下一组依存工作请求加入队列。

chaining-enqueued-in-progress.png

只要每个工作请求都成功完成,工作请求链中的剩余工作请求就会遵循相同的运行模式,直到链中的所有工作都完成为止。这是最简单的用例,通常也是首选用例,但处理错误状态同样重要。

如果在工作器处理工作请求时出现错误,您可以根据您定义的退避政策来重试该请求。重试请求链中的某个请求意味着,系统将使用提供给该请求的输入数据仅对该请求进行重试。并行运行的所有其他作业均不会受到影响。

chaining-enqueued-retry.png

如果该重试政策未定义或已用尽,或者您以其他方式已达到 OneTimeWorkRequest 返回 Result.failure() 的某种状态,该工作请求和所有依存工作请求都会被标记为 FAILED.

chaining-enqueued-failed.png

OneTimeWorkRequest 被取消时遵循相同的逻辑。任何依存工作请求也会被标记为 CANCELLED,并且无法执行其工作。

chaining-enqueued-cancelled.png

请注意,如果要向已失败或已取消工作请求的链附加更多工作请求,新附加的工作请求也会分别标记为 FAILED 或 CANCELLED。如果您想扩展现有链的工作,需要ExistingWorkPolicy中的 APPEND_OR_REPLACE

下面我们验证下工作状态: (一)

        Data data1 = new Data.Builder().putString("key", "1").build();
       OneTimeWorkRequest uploadWorkRequest1 =
               new OneTimeWorkRequest.Builder(UploadWorker.class)
                      .setInputData(data1)
                      .addTag("work")
                       // Additional configuration
                      .build();

       Data data2 = new Data.Builder().putString("key", "2").build();
       OneTimeWorkRequest uploadWorkRequest2 =
               new OneTimeWorkRequest.Builder(UploadWorker.class)
                      .setInputData(data2)
                      .addTag("work")
                       // Additional configuration
                      .build();

       Data data3 = new Data.Builder().putString("key", "3").build();
       OneTimeWorkRequest uploadWorkRequest3 =
               new OneTimeWorkRequest.Builder(UploadWorker.class)
                      .setInputData(data3)
                      .addTag("work_work")
                       // Additional configuration
                      .build();
       Log.d(TAG, "WorkRequest 3 id is " + uploadWorkRequest3.getId());

       Data data4 = new Data.Builder().putString("key", "4").build();
       OneTimeWorkRequest uploadWorkRequest4 =
               new OneTimeWorkRequest.Builder(UploadWorker.class)
                      .setInputData(data4)
                      .addTag("work_work")
                       // Additional configuration
                      .build();
       Log.d(TAG, "WorkRequest 4 id is " + uploadWorkRequest4.getId());

       WorkManager workManager = WorkManager.getInstance(MainActivity.this);

       workManager.beginWith(Arrays.asList(uploadWorkRequest1,uploadWorkRequest2))
              .then(uploadWorkRequest3)
              .then(uploadWorkRequest4)
              .enqueue();

代码如上,打印出来的Log如下:

2021-01-12 23:17:05.106 30630-30665/com.example.myapplication D/MainActivity: UploadWorker doWork and key is 1
2021-01-12 23:17:05.110 30630-30666/com.example.myapplication D/MainActivity: UploadWorker doWork and key is 2
2021-01-12 23:17:05.194 30630-30669/com.example.myapplication D/MainActivity: UploadWorker doWork and key is 3
2021-01-12 23:17:05.222 30630-30670/com.example.myapplication D/MainActivity: UploadWorker doWork and key is 4

通过上面的Log发现如下:

①:的确先执行的1和2,然后执行的3再是4。

(二)

如果仅仅把OneTimeWorkRequest1添加一个延时,把代码变成如下:

        OneTimeWorkRequest uploadWorkRequest1 =
               new OneTimeWorkRequest.Builder(UploadWorker.class)
                      .setInitialDelay(10, TimeUnit.SECONDS)
                      .setInputData(data1)
                      .addTag("work")
                       // Additional configuration
                      .build();
.................

执行的Log如下:

2021-01-12 23:24:02.731 31097-31130/com.example.myapplication D/MainActivity: UploadWorker doWork and key is 2
2021-01-12 23:24:12.652 31097-31149/com.example.myapplication D/MainActivity: UploadWorker doWork and key is 1
2021-01-12 23:24:12.730 31097-31150/com.example.myapplication D/MainActivity: UploadWorker doWork and key is 3
2021-01-12 23:24:12.763 31097-31151/com.example.myapplication D/MainActivity: UploadWorker doWork and key is 4

通过上面的Log:

①:发现2已经先执行了,这说明1、2的执行没有先后顺序。

②:3和4等1执行完毕之后才执行。

(三)

再次修正上面的代码,在把Worker提交给系统的下一行,添加对于OneTimeWorkRequest1的cancel

................
       workManager.beginWith(Arrays.asList(uploadWorkRequest1,uploadWorkRequest2))
              .then(uploadWorkRequest3)
              .then(uploadWorkRequest4)
              .enqueue();

       workManager.cancelWorkById(uploadWorkRequest1.getId());

执行的Log如下:

2021-01-12 23:29:10.977 31585-31618/com.example.myapplication D/MainActivity: UploadWorker doWork and key is 2

通过Log发现:

①:当把OneTimeWorkRequest1给cancel了,在工作链后面执行的OneTimeWorkRequest3和OneTimeWorkRequest4也被cancel掉了。

(四)

接下来我想验证下ExistingWorkPolicy.APPEND_OR_REPLACE.

修改代码如下:

..............
       WorkContinuation chain1 = workManager.beginUniqueWork("work&work", ExistingWorkPolicy.APPEND_OR_REPLACE,
Arrays.asList(uploadWorkRequest1,uploadWorkRequest2));

       WorkContinuation chain2 = workManager.beginUniqueWork("work&work", ExistingWorkPolicy.APPEND_OR_REPLACE,
Arrays.asList(uploadWorkRequest3,uploadWorkRequest4))
          .then(uploadWorkRequest5);

       chain1.enqueue();
       chain2.enqueue();

        workManager.cancelWorkById(uploadWorkRequest1.getId());

执行的Log如下:

2021-01-13 03:52:08.084 10457-10457/com.example.myapplication D/MainActivity: WorkRequest 3 is cancelled
2021-01-13 03:52:08.084 10457-10457/com.example.myapplication D/MainActivity: WorkRequest 2 is enqueued
2021-01-13 03:52:08.084 10457-10457/com.example.myapplication D/MainActivity: WorkRequest 4 is cancelled
2021-01-13 03:52:08.085 10457-10457/com.example.myapplication D/MainActivity: WorkRequest 1 is cancelled
2021-01-13 03:52:08.094 10457-10491/com.example.myapplication D/MainActivity: UploadWorker doWork and key is 2
2021-01-13 03:52:08.123 10457-10457/com.example.myapplication D/MainActivity: WorkRequest 2 is succeeded

通过上面的Log发现:

①:由于先执行了WorkContinuaton1, 然后又把WorkContinuaton1中的WorkRequest1给Cancel了, 所以后续的WorkRequest3、4都受影响, 被cancel了。

②: 而WorkRequest2 由于APPEND_OR_REPLACE的影响, 能够正常执行。

(五)

如果把chain1.enqueue()和chain2.enqueue()的执行顺序调换下. 再次执行.

Log如下:

2021-01-13 04:04:33.951 10938-10938/com.example.myapplication D/MainActivity: WorkRequest 1 is cancelled
2021-01-13 04:04:33.951 10938-10938/com.example.myapplication D/MainActivity: WorkRequest 3 is enqueued
2021-01-13 04:04:33.951 10938-10938/com.example.myapplication D/MainActivity: WorkRequest 4 is enqueued
2021-01-13 04:04:33.962 10938-11023/com.example.myapplication D/MainActivity: UploadWorker doWork and key is 3
2021-01-13 04:04:33.968 10938-11024/com.example.myapplication D/MainActivity: UploadWorker doWork and key is 4
2021-01-13 04:04:34.011 10938-10938/com.example.myapplication D/MainActivity: WorkRequest 3 is succeeded
2021-01-13 04:04:34.011 10938-10938/com.example.myapplication D/MainActivity: WorkRequest 5 is enqueued
2021-01-13 04:04:34.011 10938-10938/com.example.myapplication D/MainActivity: WorkRequest 4 is succeeded
2021-01-13 04:04:34.020 10938-11026/com.example.myapplication D/MainActivity: UploadWorker doWork and key is 5
2021-01-13 04:04:34.046 10938-10938/com.example.myapplication D/MainActivity: WorkRequest 5 is running
2021-01-13 04:04:34.057 10938-11027/com.example.myapplication D/MainActivity: UploadWorker doWork and key is 2
2021-01-13 04:04:34.057 10938-10938/com.example.myapplication D/MainActivity: WorkRequest 5 is succeeded
2021-01-13 04:04:34.057 10938-10938/com.example.myapplication D/MainActivity: WorkRequest 2 is enqueued
2021-01-13 04:04:34.074 10938-10938/com.example.myapplication D/MainActivity: WorkRequest 2 is succeeded

通过上面的Log发现:

①: 在WorkContinuation2先执行的情况下, WorkContinuation2这边的工作链不受到WorkRequest1的cancel的影响。

②:WorkContinuation1的WorkRequest2因为APPEND_OR_REPLACE, 不受到WorkRequest1的cancel影响。

※上面的示例(四)和(五)在实际使用过程中需要特别注意, 在使用多个工作链的时候, 需要注意前一个执行的工作链的状态对后执行的工作链的影响。

收起阅读 »

一文带你理解Kotlin协程本质核心

1. 协程是什么协程是编译器的能力,因为协程并不需要操作系统和硬件的支持(线程需要),是编译器为了让开发者写代码更简单方便, 提供了一些关键字, 并在内部自动生成了处理字节码线程和协程的目的差异线程的目的是提高CPU资源使用率, 使多个任务得以并行的运行,是为...
继续阅读 »

1. 协程是什么

  • 协程是编译器的能力,因为协程并不需要操作系统和硬件的支持(线程需要),是编译器为了让开发者写代码更简单方便, 提供了一些关键字, 并在内部自动生成了处理字节码

线程和协程的目的差异

  • 线程的目的是提高CPU资源使用率, 使多个任务得以并行的运行,是为了服务于机器的.
  • 协程的目的是为了让多个任务之间更好的协作,主要体现在代码逻辑上,是为了服务开发者 (能提升资源的利用率, 但并不是原始目的)

线程和协程的调度差异

  • 线程的调度是系统完成的,一般是抢占式的,根据优先级来分配
  • 协程的调度是开发者根据程序逻辑指定好的,在不同的时期把资源合理的分配给不同的任务.

协程与线程的关系

  • 协程并不是取代线程,而且抽象于线程之上,线程是被分割的CPU资源,协程是组织好的代码流程,协程需要线程来承载运行,线程是协程的资源

2. 基本使用

2.1. CoroutineScope.launch

  • launch函数可以启动新协程而不将结果返回给调用方

2.1.1. 代码实现

//获取一个协程作用域用于创建协程
private val mScope = MainScope()

mScope.launch(Dispatchers.IO) {
//IO线程执行getStringInfo()方法,返回结果
var res = getStringInfo()
//获取结果后主线程提示更新
withContext(Dispatchers.Main) {
Alerter.create(this@LearnCoroutineActivity).setTitle("Result").setText(res).show()
}
}

private suspend fun getStringInfo(): String {
return withContext(Dispatchers.IO) {
//在这1000毫秒内该协程所处的线程不会阻塞
delay(1000)
"Coroutine-launch"
}
}

//在onDestroy生命周期方法之中要手动取消
override fun onDestroy() {
super.onDestroy()
mScope.cancel()
}

2.1.2. 步骤

  1. 获取一个协程作用域用于创建协程
  2. 通过协程作用域.launch方法启动新的协程任务
    1. 启动时可以指定执行线程
    2. 内部通过withContext()方法实现切换线程
  3. 在onDestroy生命周期方法之中要手动取消

2.2. CoroutineScope.async

  • async函数实现返回值处理或者并发处理

2.2.1. 返回值处理

private fun asyncReturn() {
mScope.launch(Dispatchers.Main) {
//新开一个协程去执行协程体,父协程的代码会接着往下走
var deferred = async(Dispatchers.IO) {
delay(1000)
"Coroutine-Async"
}
//等待async执行完成获取返回值,并不会阻塞线程,而是挂起,将线程的执行权交出去
//直到async的协程体执行完毕后,会恢复协程继续执行
val data = deferred.await()
Alerter.create(this@LearnCoroutineActivity).setTitle("Result").setText(data).show()
}
}

2.2.2. 并发处理

private fun asyncConcurrent() {
//coroutineContext的创建下文会有分析
var coroutineContext = Job() +
Dispatchers.Main +
CoroutineExceptionHandler { coroutineContext, throwable ->
Log.e(
"CoroutineException",
"CoroutineExceptionHandler: $throwable"
)
} +
CoroutineName("asyncConcurrent")
mScope.launch(coroutineContext) {
val job1 = async(Dispatchers.IO) {
delay(1000)
"job1-finish"
}
val job2 = async(Dispatchers.IO) {
delay(2000)
"job2-finish"
}
val job3 = async(Dispatchers.IO) {
delay(500)
"job3-finish"
}
//等待各job执行完 将结果合并
Alerter.create(this@LearnCoroutineActivity).setTitle("Result")
.setText("job1:${job1.await()},job2:${job2.await()},job3:${job3.await()}").show()
}
}

2.3. 协程作用域

  • MainScope是协程默认提供的作用域,但是还有其他作用域更为方便
  • 可使用lifecycleScope或者viewModelScope,这两种作用域会自动取消
  • 在UI组件中使用LifecycleOwner.lifecycleScope,在ViewModel中使用ViewModel.viewModelScope

3. CoroutineContext

  • CoroutineContext是一个特殊的集合,同时包含了Map和Set的特点

  • 集合内部的元素Element是根据key去对应(Map特点),但是不允许重复(Set特点)

  • Element之间可以通过+号进行组合

  • Element有如下四类,共同组成了CoroutineContext

    • Job:协程的唯一标识,用来控制协程的生命周期(new、active、completing、completed、cancelling、cancelled)
    • CoroutineDispatcher:指定协程运行的线程(IO、Default、Main、Unconfined)
    • CoroutineName: 指定协程的名称,默认为coroutine
    • CoroutineExceptionHandler: 指定协程的异常处理器,用来处理未捕获的异常

3.1. CoroutineDispatcher Element

  • 用于指定协程的运行线程
  • kotlin已经内置了CoroutineDispatcher的4个实现,可以通过Dispatchers的Default、IO、Main、Unconfined字段分别返回使用
public actual object Dispatchers {
@JvmStatic
public actual val Default: CoroutineDispatcher = createDefaultDispatcher()
@JvmStatic
public val IO: CoroutineDispatcher = DefaultScheduler.IO
@JvmStatic
public actual val Unconfined: CoroutineDispatcher = kotlinx.coroutines.Unconfined
@JvmStatic
public actual val Main: MainCoroutineDispatcher get() = MainDispatcherLoader.dispatcher
}

3.1.1. Default,IO

Default,IO其实内部用的是一个线程池,下面逐个解析,看实现原理

3.1.1.1. default
  • Default会根据useCoroutinesScheduler属性(默认为true)去获取对应的线程池
    • DefaultScheduler(useCoroutinesScheduler=ture):kotlin自己实现的线程池逻辑
    • CommonPool(useCoroutinesScheduler=false):java类库中的Executor实现线程池逻辑
internal actual fun createDefaultDispatcher(): CoroutineDispatcher =
if (useCoroutinesScheduler) DefaultScheduler else CommonPool
internal object DefaultScheduler : ExperimentalCoroutineDispatcher() {
.....
}
//委托类
public open class ExperimentalCoroutineDispatcher(
private val corePoolSize: Int,
private val maxPoolSize: Int,
private val idleWorkerKeepAliveNs: Long,
private val schedulerName: String = "CoroutineScheduler"
) : ExecutorCoroutineDispatcher() {
}
//java类库中的Executor实现线程池逻辑
internal object CommonPool : ExecutorCoroutineDispatcher() {}
//共同父类,定义行为
public abstract class ExecutorCoroutineDispatcher: CoroutineDispatcher(), Closeable {}
ExperimentalCoroutineDispatcher
  • DefaultScheduler的主要实现都在它的父类ExperimentalCoroutineDispatcher中
public open class ExperimentalCoroutineDispatcher(
private val corePoolSize: Int,
private val maxPoolSize: Int,
private val idleWorkerKeepAliveNs: Long,
private val schedulerName: String = "CoroutineScheduler"
) : ExecutorCoroutineDispatcher() {
public constructor(
corePoolSize: Int = CORE_POOL_SIZE,
maxPoolSize: Int = MAX_POOL_SIZE,
schedulerName: String = DEFAULT_SCHEDULER_NAME
) : this(corePoolSize, maxPoolSize, IDLE_WORKER_KEEP_ALIVE_NS, schedulerName)
....//省略一些供测试的方法,更好的跟踪同步状态
}
3.1.1.2. IO
  • IO的实现其实是LimitingDispatcher
val IO: CoroutineDispatcher = LimitingDispatcher(
this,
systemProp(IO_PARALLELISM_PROPERTY_NAME, 64.coerceAtLeast(AVAILABLE_PROCESSORS)),
"Dispatchers.IO",
TASK_PROBABLY_BLOCKING
)
LimitingDispatcher
  • IO的实现类会有一些最大请求限制,以及队列处理
private class LimitingDispatcher(
private val dispatcher: ExperimentalCoroutineDispatcher,
private val parallelism: Int,
private val name: String?,
override val taskMode: Int
) : ExecutorCoroutineDispatcher(), TaskContext, Executor {
//同步阻塞队列
private val queue = ConcurrentLinkedQueue<Runnable>()
//cas计数
private val inFlightTasks = atomic(0)

override fun dispatch(context: CoroutineContext, block: Runnable) = dispatch(block, false)

private fun dispatch(block: Runnable, tailDispatch: Boolean) {
var taskToSchedule = block
while (true) {

if (inFlight <= parallelism) {
//LimitingDispatcher的dispatch方法委托给了DefaultScheduler的dispatchWithContext方法
dispatcher.dispatchWithContext(taskToSchedule, this, tailDispatch)
return
}
..//省略了一些队列处理逻辑
}
}
}

3.1.2. CoroutineScheduler

  • Default、IO其实都是共享CoroutineScheduler线程池,Kotlin实现了一套线程池两种调度策略
  • 通过内部的mode区分
fun dispatch(block: Runnable, taskContext: TaskContext = NonBlockingContext, tailDispatch: Boolean = false) {
......
if (task.mode == TASK_NON_BLOCKING) {
if (skipUnpark) return
signalCpuWork()
} else {
signalBlockingWork(skipUnpark = skipUnpark)
}
}
Mode
TypeMode
DefaultTASK_NON_BLOCKING
IOTASK_PROBABLY_BLOCKING
处理策略
TypeMode
DefaultCoroutineScheduler最多有corePoolSize个线程被创建,corePoolSize它的取值为max(2, CPU核心数)
即它会尽量的等于CPU核心数
IO创建比corePoolSize更多的线程来运行IO型任务,但不能大于maxPoolSize
1.公式:max(corePoolSize, min(CPU核心数 * 128, 2^21 - 2)),即大于corePoolSize,小于2^21 - 2
2.2^21 - 2是一个很大的数约为2M,但是CoroutineScheduler是不可能创建这么多线程的,所以就需要外部限制提交的任务数
3.Dispatchers.IO构造时就通过LimitingDispatcher默认限制了最大线程并发数parallelism为max(64, CPU核心数),即最多只能提交parallelism个任务到CoroutineScheduler中执行,剩余的任务被放进队列中等待。
适合场景
TypeMode
Default1.CPU密集型任务的特点是执行任务时CPU会处于忙碌状态,任务会消耗大量的CPU资源
2.复杂计算、视频解码等,如果此时线程数太多,超过了CPU核心数,那么这些超出来的线程是得不到CPU的执行的,只会浪费内存资源
3.因为线程本身也有栈等空间,同时线程过多,频繁的线程切换带来的消耗也会影响线程池的性能
4.对于CPU密集型任务,线程池并发线程数等于CPU核心数才能让CPU的执行效率最大化
IO1.IO密集型任务的特点是执行任务时CPU会处于闲置状态,任务不会消耗大量的CPU资源
2.网络请求、IO操作等,线程执行IO密集型任务时大多数处于阻塞状态,处于阻塞状态的线程是不占用CPU的执行时间
3.此时CPU就处于闲置状态,为了让CPU忙起来,执行IO密集型任务时理应让线程的创建数量更多一点,理想情况下线程数应该等于提交的任务数,对于这些多创建出来的线程,当它们闲置时,线程池一般会有一个超时回收策略,所以大部分情况下并不会占用大量的内存资源
4.但也会有极端情况,所以对于IO密集型任务,线程池并发线程数应尽可能地多才能提高CPU的吞吐量,这个尽可能地多的程度并不是无限大,而是根据业务情况设定,但肯定要大于CPU核心数。

3.1.3. Unconfined

  • 任务执行在默认的启动线程。之后由调用resume的线程决定恢复协程的线程。
internal object Unconfined : CoroutineDispatcher() {
//为false为不需要dispatch
override fun isDispatchNeeded(context: CoroutineContext): Boolean = false

override fun dispatch(context: CoroutineContext, block: Runnable) {
// 只有当调用yield方法时,Unconfined的dispatch方法才会被调用
// yield() 表示当前协程让出自己所在的线程给其他协程运行
val yieldContext = context[YieldContext]
if (yieldContext != null) {
yieldContext.dispatcherWasUnconfined = true
return
}
throw UnsupportedOperationException("Dispatchers.Unconfined.dispatch function can only be used by the yield function. " +
"If you wrap Unconfined dispatcher in your code, make sure you properly delegate " +
"isDispatchNeeded and dispatch calls.")
}
}
  • 每一个协程都有对应的Continuation实例,其中的resumeWith用于协程的恢复,存在于DispatchedContinuation
DispatchedContinuation
  • 我们重点看resumeWith的实现以及类委托
internal class DispatchedContinuation<in T>(
@JvmField val dispatcher: CoroutineDispatcher,
@JvmField val continuation: Continuation<T>
) : DispatchedTask<T>(MODE_UNINITIALIZED), CoroutineStackFrame, Continuation<T> by continuation {
.....
override fun resumeWith(result: Result<T>) {
val context = continuation.context
val state = result.toState()
if (dispatcher.isDispatchNeeded(context)) {
_state = state
resumeMode = MODE_ATOMIC
dispatcher.dispatch(context, this)
} else {
executeUnconfined(state, MODE_ATOMIC) {
withCoroutineContext(this.context, countOrElement) {
continuation.resumeWith(result)
}
}
}
}
....
}

解析如下:

  1. DispatchedContinuation通过类委托实现了在resumeWith()方法之前的代码逻辑添加

  2. 通过isDispatchNeeded(是否需要dispatch,Unconfined=false,default,IO=true)判断做不同处理

    1. true:调用协程的CoroutineDispatcher的dispatch方法
    2. false:调用executeUnconfined方法
    private inline fun DispatchedContinuation<*>.executeUnconfined(
    contState: Any?, mode: Int, doYield: Boolean = false,
    block: () -> Unit
    ): Boolean {
    assert { mode != MODE_UNINITIALIZED }
    val eventLoop = ThreadLocalEventLoop.eventLoop
    if (doYield && eventLoop.isUnconfinedQueueEmpty) return false
    return if (eventLoop.isUnconfinedLoopActive) {
    _state = contState
    resumeMode = mode
    eventLoop.dispatchUnconfined(this)
    true
    } else {
    runUnconfinedEventLoop(eventLoop, block = block)
    false
    }
    }
    1. 从threadlocal中取出eventLoop(eventLoop和当前线程相关的),判断是否在执行Unconfined任务
      1. 如果在执行则调用EventLoop的dispatchUnconfined方法把Unconfined任务放进EventLoop中
      2. 如果没有在执行则直接执行
    internal inline fun DispatchedTask<*>.runUnconfinedEventLoop(
    eventLoop: EventLoop,
    block: () -> Unit
    ) {
    eventLoop.incrementUseCount(unconfined = true)
    try {
    block()
    while (true) {
    if (!eventLoop.processUnconfinedEvent()) break
    }
    } catch (e: Throwable) {
    handleFatalException(e, null)
    } finally {
    eventLoop.decrementUseCount(unconfined = true)
    }
    }
    1. 执行block()代码块,即上文提到的resumeWith()
    2. 调用processUnconfinedEvent()方法实现执行剩余的Unconfined任务,知道全部执行完毕跳出循环
EventLoop
  • EventLoop是存放与threadlocal,所以是跟当前线程相关联的,而EventLoop也是CoroutineDispatcher的一个子类
internal abstract class EventLoop : CoroutineDispatcher() {
.....
//双端队列实现存放Unconfined任务
private var unconfinedQueue: ArrayQueue<DispatchedTask<*>>? = null
//从队列的头部移出Unconfined任务执行
public fun processUnconfinedEvent(): Boolean {
val queue = unconfinedQueue ?: return false
val task = queue.removeFirstOrNull() ?: return false
task.run()
return true
}
//把Unconfined任务放进队列的尾部
public fun dispatchUnconfined(task: DispatchedTask<*>) {
val queue = unconfinedQueue ?:
ArrayQueue<DispatchedTask<*>>().also { unconfinedQueue = it }
queue.addLast(task)
}
.....
}

解析如下:

  1. 内部通过双端队列实现存放Unconfined任务
    1. EventLoop的dispatchUnconfined方法用于把Unconfined任务放进队列的尾部
    2. rocessUnconfinedEvent方法用于从队列的头部移出Unconfined任务执行

3.1.4. Main

  • 是把协程运行在平台相关的只能操作UI对象的Main线程,但是根据不同平台有不同的实现
平台实现
kotlin/jskotlin对JavaScript的支持,提供了转换kotlin代码,kotlin标准库的能力,npm包管理能力
在kotlin/js上Dispatchers.Main等效于Dispatchers.Default
kotlin/native将kotlin代码编译为无需虚拟机就可运行的原生二进制文件的技术, 它的主要目的是允许对不需要或不可能使用虚拟机的平台进行编译,例如嵌入式设备或iOS
在kotlin/native上Dispatchers.Main等效于Dispatchers.Default
kotlin/JVM需要虚拟机才能编译的平台,例如Android就是属于kotlin/JVM,对于kotlin/JVM我们需要引入对应的dispatcher,例如Android就需要引入kotlinx-coroutines-android库,它里面有Android对应的Dispatchers.Main实现,其实就是把任务通过Handler运行在Android的主线程

3.2. CoroutineName Element

  • 协程名称,可以自定义,方便调试分析
public data class CoroutineName(
val name: String
) : AbstractCoroutineContextElement(CoroutineName) {

public companion object Key : CoroutineContext.Key<CoroutineName>

override fun toString(): String = "CoroutineName($name)"
}

3.3. CoroutineExceptionHandler Element

  • 协程异常处理器,默认创建的协程都会有一个异常处理器,也可以手动指定。

    var coroutineContext = Job() +
    Dispatchers.Main +
    //手动添加指定异常处理器
    CoroutineExceptionHandler { coroutineContext, throwable ->
    Log.e(
    "CoroutineException",
    "CoroutineExceptionHandler: $throwable"
    )
    } +
    CoroutineName("asyncConcurrent")
  • 但是只对launch方法启动的根协程有效,而对async启动的根协程无效

    async启动的根协程默认会捕获所有未捕获异常并把它放在Deferred中,等到用户调用Deferred的await方法才抛出,也就是需要手动加try-catch

CASE

协程的使用场景变化自如,异常处理的情况也就比较多

  1. 非SupervisorJob情况下,字协程抛出的异常会委托给父协程的CoroutineExceptionHandler处理
    1. 子协程的CoroutineExceptionHandler并不会执行
  2. SupervisorJob情况下,不会产生异常传播,即自己的CoroutineExceptionHandler可以接收到异常
  3. 子协程同时抛出多个异常时,CoroutineExceptionHandler只会捕捉第一个异常,后续的异常存于第一个异常的suppressed数组之中
  4. 取消协程时会抛出CancellationException,但是所有的CoroutineExceptionHandler不会接收到,只能通过try-catch实现捕获

3.4. CoroutineContext结构

CoroutineContext.pngfold方法

  • 提供了从left到right遍历CoroutineContext中每一个Element的能力,并对每一个Element做operation操作
//operation是一个函数指针,可以执行函数引用
public override fun <R> fold(initial: R, operation: (R, Element) -> R): R =
//对left做fold操作,把left做完fold操作的的返回结果和element做operation操作
operation(left.fold(initial, operation), element)
minusKey方法
  • 返回一个新的CoroutineContext,这个CoroutineContext删除了Key对应的Element
public override fun minusKey(key: Key<*>): CoroutineContext {
//element是否匹配,如果是则直接返回供删除,即匹配成功
element[key]?.let { return left }
//没有匹配成功则从left开始寻找
val newLeft = left.minusKey(key)
return when {
//如果left中不存在目标element,则当前CombinedContext肯定不包含目标元素,直接返回当前
newLeft === left -> this
//如果left之中存在目标element,删除目标element后,left等于空,返回当前CombinedContext的element
newLeft === EmptyCoroutineContext -> element
//如果left之中存在目标element,删除目标element后,left不等于空,创建新的CombinedContext并返回
else -> CombinedContext(newLeft, element)
}
}
结构图

CoroutineContext结构.png

  • 整体像链表,left就是指向下一个结点的指针,
  • get、minusKey操作逻辑流程都是先访问当前element,不满足,再访问left的element,顺序都是从right到left
  • fold的操作逻辑流程是先访问left,直到递归到最后的element,然后再从left到right的返回,从而访问了所有的element。

plus方法

此方法是CoroutineContext的实现,内部分为元素合并和拦截器处

  • puls方法最终返回的CoroutineContext是不存在key相同的element的,新增元素会覆盖CoroutineContext中的含有相同key的元素,这像是Set的特性

  • 拦截器的处理就是为了每次添加完成后保持ContinuationInterceptor为CoroutineContext中的最后一个元素,目的是在执行协程之前做前置操作

    CoroutineDispatcher就继承自ContinuationInterceptor

    • 通过把ContinuationInterceptor放在最后面,协程在查找上下文的element时,总能最快找到拦截器,避免了递归查找,从而让拦截行为前置执行

4. Job Element

  • 每一个所创建的协程 (通过 launch 或者 async),会返回一个 Job实例,该实例是协程的唯一标识,并且负责管理协程的生命周期

4.1. Job状态

Job在执行的过程中,包含了一系列状态,虽然开发者没办法直接获取所有状态,但是Job之中有如下三个属性

  • isActive(是否活动)
  • isCompleted(是否已完成)
  • isCancelled(是否已取消)

根据属性就可以推断出Job的所处状态,状态如下

  • 新创建 (New)
    • 当一个协程创建后就处于新建(New)状态
  • 活跃 (Active)
    • 当调用Job的start/join方法后协程就处于活跃(Active)状态
  • 完成中 (Completing)
    • 当协程执行完成后或者调用CompletableJob(CompletableJob是Job的一个子接口)的complete方法都会让当前协程进入完成中(Completing)状态
  • 已完成 (Completed)
    • 处于完成中状态的协程会等所有子协程都完成后才进入完成(Completed)状态
  • 取消中 (Cancelling)
    • 当运行出错或者调用Job的cancel方法都会将当前协程置为取消中(Cancelling)状态
  • 已取消 (Cancelled)
    • 处于取消中状态的协程会等所有子协程都完成后才进入取消 (Cancelled)状态
StateisActiveisCompletedisCancelled
New (optional initial state)falsefalsefalse
Active (default initial state)truefalsefalse
Completing (transient state)truefalsefalse
Cancelling (transient state)falsefalsetrue
Cancelled (final state)falsetruetrue
Completed (final state)falsetruefalse
                                      wait children
+-----+ start +--------+ complete +-------------+ finish +-----------+
| New | -----> | Active | ---------> | Completing | -------> | Completed |
+-----+ +--------+ +-------------+ +-----------+
| cancel / fail |
| +----------------+
| |
V V
+------------+ finish +-----------+
| Cancelling | --------------------------------> | Cancelled |
+------------+ +-----------+

4.2. Job方法

fun start(): Boolean
  • 调用该函数来启动这个 Coroutine
  • 如果当前 Coroutine 还没有执行调用该函数返回 true
  • 如果当前 Coroutine 已经执行或者已经执行完毕,则调用该函数返回 false
fun cancel(cause: CancellationException? = null)
  • 通过可选的取消原因取消Job
fun invokeOnCompletion(handler: CompletionHandler): DisposableHandle
  • 通过这个函数可以给 Job 设置一个完成通知,当 Job 执行完成的时候会同步执行这个通知函数。 回调的通知对象类型为:typealias CompletionHandler = (cause: Throwable?) -> Unit.
  • CompletionHandler 参数代表了 Job 是如何执行完成的。 cause 有下面三种情况:
    • 如果 Job 是正常执行完成的,则 cause 参数为 null
    • 如果 Job 是正常取消的,则 cause 参数为 CancellationException 对象。这种情况不应该当做错误处理,这是任务正常取消的情形。所以一般不需要在错误日志中记录这种情况。
    • 其他情况表示 Job 执行失败了。
  • 这个函数的返回值为 DisposableHandle 对象,如果不再需要监控 Job 的完成情况了, 则可以调用 DisposableHandle.dispose 函数来取消监听。如果 Job 已经执行完了, 则无需调用 dispose 函数了,会自动取消监听。
suspend fun join()(suspend函数)
  • 用来在另外一个 Coroutine 中等待 job 执行完成后继续执行。

4.3. Job异常传播

  • 协程是有父子级的概念,如果子Job在运行过程之中发生异常,那么父Job就会感知到并抛出异常。如果要抑制这种行为就需要使用SupervisorJob

    除了CancellationException以外的异常

SupervisorJob
fun main(){
val parentJob = GlobalScope.launch {
//childJob是一个SupervisorJob
val childJob = launch(SupervisorJob()){
throw NullPointerException()
}
childJob.join()
println("parent complete")
}
Thread.sleep(1000)
}

此时childJob抛出异常并不会影响parentJob的运行,parentJob会继续运行并输出parent complete。


5. CoroutineScope

  • CoroutineScope是用于提供CoroutineContext的容器,但是制定了代码边界,去全局管控所有内部作用域中的CoroutineContext,源码如下:
public interface CoroutineScope {
public val coroutineContext: CoroutineContext
}

这里只需要一个CoroutineContext,保证CoroutineContext能在整个协程运行中传递下去,约束CoroutineContext的作用边界

5.1. lifecycleScope

  • lifecycleScope可以让协程具有与Activity一样的生命周期意
  • 源码解析如下:
    1. 通过创建LifecycleCoroutineScopeImpl实现CoroutineScope接口
    2. 通过SupervisorJob控制异常传播
    3. 通过Dispatchers.Main控制线程类型
    4. 在register方法中通过launch创建协程,通过lifecycle的状态监听Activity的生命周期,在合适的时机调用cancel方法
    5. 通过扩展实现取消策略

5.2. 其他方法

  • lifecycleScope还扩展出了其他作用域范围的控制函数
lifecycleScope.launchWhenCreated {  }
lifecycleScope.launchWhenStarted { }
lifecycleScope.launchWhenResumed { }

6. ContinuationInterceptor

  • ContinuationInterceptor继承于CoroutineContext.Element,也就是CoroutineContext
  • ContinuationInterceptor提供了interceptContinuation方法,实现了拦截,源码分析如下:

7. Suspend|协程状态机

  • 被suspend关键字修饰的方法为协程方法,其本质如下:

7.1. CPS机制

  • 通过CPS(Continuation-Passing-Style)机制。使得每一个suspend修饰的方法或者lambda表达式都会在代码调用的时候为其额外添加Continuation类型的参数7.2. 协程状态机

协程通过suspend来标识挂起点,但真正的挂起点还需要通过是否返回COROUTINE_SUSPENDED来判断,而代码体现是通过状态机来处理协程的挂起与恢复。在需要挂起的时候,先保留现场与设置下一个状态点,然后再通过退出方法的方式来挂起协程。在挂起的过程中并不会阻塞当前的线程。对应的恢复通过resumeWith来进入状态机的下一个状态,同时在进入下一个状态时会恢复之前挂起的现场

  • 我们结合kotlin字节码文件分析协程状态机
  1. getCOROUTINE_SUSPENDED方法也就是上文说的COROUTINE_SUSPENDED标识用于标识挂起
  2. 通过label实现不同状态的处理,在对应的case返回挂起标识
  3. 当返回了COROUTINE_SUSPENDED也就会跳出方法,此时协程就被挂起。当前线程也就可以执行其它的逻辑,并不会被协程的挂起所阻塞
  4. 最终等到下个状态,执行对应的代码
  5. 在label进入case2状态,会在对应时间字之后实现唤醒


收起阅读 »

Compose版FlowLayout了解一下~

前言 FlowLayout是开发中常用的一种布局,并且在面试时,如何自定义FlowLayout也是一个高频问题 最近Compose发布正式版了,本文主要是以FlowLayout为例,熟悉Compose自定义Layout的主要流程 本文主要要实现以下效果: ...
继续阅读 »

前言


FlowLayout是开发中常用的一种布局,并且在面试时,如何自定义FlowLayout也是一个高频问题
最近Compose发布正式版了,本文主要是以FlowLayout为例,熟悉Compose自定义Layout的主要流程
本文主要要实现以下效果:



  1. 自定义Layout,从左向右排列,超出一行则换行显示

  2. 支持设置子View间距及行间距

  3. 当子View高度不一致时,支持一行内居上,居中,居下对齐


效果


首先来看下最终的效果


Compose自定义Layout流程


Android View体系中,自定义Layout一般有以下几步:



  1. 测量子View宽高

  2. 根据测量结果确定父View宽高

  3. 根据需要确定子View放置位置


Compose中其实也是大同小异的
我们一般使用Layout来测量和布置子项,以实现自定义Layout,我们首先来实现一个自定义的Column,如下所示:


@Composable
fun MyBasicColumn(
modifier: Modifier = Modifier,
content: @Composable () -> Unit
)
{
Layout(
modifier = modifier,
children = content
) { measurables, constraints ->
// 测量布置子项
}
}

Layout中有两个参数,measurables 是需要测量的子项的列表,而constraints是来自父项的约束条件


@Composable
fun MyBasicColumn(
modifier: Modifier = Modifier,
content: @Composable () -> Unit
)
{
Layout(
modifier = modifier,
content = content
) { measurables, constraints ->
//需要测量的子项
val placeables = measurables.map { measurable ->
// 1.测量子项
measurable.measure(constraints)
}

// 2.设置Layout宽高
layout(constraints.maxWidth, constraints.maxHeight) {
var yPosition = 0

// 在父Layout中定位子项
placeables.forEach { placeable ->
// 3.在屏幕上定位子项
placeable.placeRelative(x = 0, y = yPosition)

// 记录子项的y轴位置
yPosition += placeable.height
}
}
}
}

以上主要就是做了三件事:



  1. 测量子项

  2. 测量子项后,根据结果设置父Layout宽高

  3. 在屏幕上定位子项,设置子项位置


然后一个简单的自定义Layout也就完成了,可以看到,这跟在View体系中也没有什么区别
下面我们来看下怎么实现一个FlowLayout


自定义FlowLayout


我们首先来分析下,实现一个FlowLayou需要做些什么?



  1. 首先我们应该确定父Layout的宽度

  2. 遍历测量子项,如果宽度和超过父Layout则换行

  3. 遍历时同时记录每行的最大高度,最后高度即为每行最大高度的和

  4. 经过以上步骤,宽高都确定了,就可以设置父Layout的宽高了,测量步骤完成

  5. 接下来就是定位,遍历测量后的子项,根据之前测量的结果确定其位置


流程大概就是上面这些了,我们一起来看看实现


遍历测量,确定宽高


    Layout(
modifier = modifier,
content = content
) { measurables, constraints ->
val parentWidthSize = constraints.maxWidth
var lineWidth = 0
var totalHeight = 0
var lineHeight = 0
// 测量子View,获取FlowLayout的宽高
measurables.mapIndexed { i, measurable ->
// 测量子view
val placeable = measurable.measure(constraints)
val childWidth = placeable.width
val childHeight = placeable.height
//如果当前行宽度超出父Layout则换行
if (lineWidth + childWidth > parentWidthSize) {
//记录总高度
totalHeight += lineHeight
//重置行高与行宽
lineWidth = childWidth
lineHeight = childHeight
totalHeight += lineSpacing.toPx().toInt()
} else {
//记录每行宽度
lineWidth += childWidth + if (i == 0) 0 else itemSpacing.toPx().toInt()
//记录每行最大高度
lineHeight = maxOf(lineHeight, childHeight)
}
//最后一行特殊处理
if (i == measurables.size - 1) {
totalHeight += lineHeight
}
}

//...设置宽高
layout(parentWidthSize, totalHeight) {

}
}

以上就是确定宽高的代码,主要做了以下几件事



  1. 循环测量子项

  2. 如果当前行宽度超出父Layout则换行

  3. 每次换行都记录每行最大高度

  4. 根据测量结果,最后确定父Layout的宽高


记录每行的子项与每行最大高度


上面我们已经测量完成了,明确了父Layout的宽高
不过为了实现当子项高度不一致时居中对齐的效果,我们还需要将每行的子项与每行的最大高度记录下来


    Layout(
modifier = modifier,
content = content
) { measurables, constraints ->
val mAllPlaceables = mutableListOf<MutableList<Placeable>>() // 所有子项
val mLineHeight = mutableListOf<Int>() //每行的最高高度
var lineViews = mutableListOf<Placeable>() //每行放置的内容
// 测量子View,获取FlowLayout的宽高
measurables.mapIndexed { i, measurable ->
// 测量子view
val placeable = measurable.measure(constraints)
val childWidth = placeable.width
val childHeight = placeable.height
//如果行宽超出Layout宽度则换行
if (lineWidth + childWidth > parentWidthSize) {
//每行最大高度添加到列表中
mLineHeight.add(lineHeight)
//二级列表,存放所有子项
mAllPlaceables.add(lineViews)
//重置每行子项列表
lineViews = mutableListOf()
lineViews.add(placeable)
} else {
//每行高度最大值
lineHeight = maxOf(lineHeight, childHeight)
//每行的子项添加到列表中
lineViews.add(placeable)
}
//最后一行特殊处理
if (i == measurables.size - 1) {
mLineHeight.add(lineHeight)
mAllPlaceables.add(lineViews)
}
}
}

上面主要做了三件事



  1. 每行的最大高度添加到列表中

  2. 每行的子项添加到列表中

  3. lineViews列表添加到mAllPlaceables中,存放所有子项


定位子项


上面我们已经完成了测量,并且获得了所有子项的列表,现在可以遍历定位了


@Composable
fun ComposeFlowLayout(
modifier: Modifier = Modifier,
itemSpacing: Dp = 0.dp,
lineSpacing: Dp = 0.dp,
gravity: Int = Gravity.TOP,
content: @Composable () -> Unit
)
{
Layout(
modifier = modifier,
content = content
) { measurables, constraints ->
layout(parentWidthSize, totalHeight) {
var topOffset = 0
var leftOffset = 0
//循环定位
for (i in mAllPlaceables.indices) {
lineViews = mAllPlaceables[i]
lineHeight = mLineHeight[i]
for (j in lineViews.indices) {
val child = lineViews[j]
val childWidth = child.width
val childHeight = child.height
// 根据Gravity获取子项y坐标
val childTop = getItemTop(gravity, lineHeight, topOffset, childHeight)
child.placeRelative(leftOffset, childTop)
// 更新子项x坐标
leftOffset += childWidth + itemSpacing.toPx().toInt()
}
//重置子项x坐标
leftOffset = 0
//子项y坐标更新
topOffset += lineHeight + lineSpacing.toPx().toInt()
}
}
}
}

private fun getItemTop(gravity: Int, lineHeight: Int, topOffset: Int, childHeight: Int): Int {
return when (gravity) {
Gravity.CENTER -> topOffset + (lineHeight - childHeight) / 2
Gravity.BOTTOM -> topOffset + lineHeight - childHeight
else -> topOffset
}
}

要定位一个子项,其实就是确定它的坐标,以上主要做了以下几件事



  1. 遍历所有子项

  2. 根据位置确定子项XY坐标

  3. 根据Gravity可使子项居上,居中,居下对齐


综上,一个简单的ComposeFlowLayout就完成了


总结


本文主要实现了一个支持设置子View间距及行间距,支持子View居上,居中,居左对齐的FlowLayout,了解了Compose自定义Layout的基本流程
后续更多Compose相关知识点,敬请期待~


本文的所有相关代码


Compose版FlowLayout


收起阅读 »

View.post和Handler.post的关系

前言 View.post和Handler.post是Android开发中经常使用到的两个”post“方法,我们经常通过前者去获取一些View在运行时的渲染数据,或者测量页面的渲染时间。而后者则是Android的核心Handler的一个方法,它会向对应线程的M...
继续阅读 »

前言


View.post和Handler.post是Android开发中经常使用到的两个”post“方法,我们经常通过前者去获取一些View在运行时的渲染数据,或者测量页面的渲染时间。而后者则是Android的核心Handler的一个方法,它会向对应线程的MessageQueue中插入一条Message,在未来的某个事件点得到执行.....


为什么要拿这二者来比较?


首先,这二者的名字相同


其次,是View.post()的调用时机和整个View的绘制和渲染有着千丝万缕的联系。而这一切的基础,正是主线程的Handler.post(),理清这二者的关系,能够加深我们对View渲染、绘制的流程的理解。


View的渲染起点


宏观上来说,当DecorView被”attach“到Window之上后,程序能够收到系统分配给各个Activity的同步信号时,View就会开始渲染了,当每个同步信号到来时,ChoreoGrapher将会派发出一个信号通知ViewRootImpl进行视图的渲染,因此,从系统上来看,每次释放的Vsync同步信号应该是视图绘制的起点。


从App端来说,当ScheduleTravesals被调用时,会先向MessageQueue中插入一个消息屏障,此时会阻隔其他的同步消息的通过,允许异步消息的进入。然后mChoreoGrapher,向MessageQueue中插入一个视图更新的信号,最终会走到doTraversals()方法中,在该方法的执行过程中,将会先取消掉同步屏障,然后紧接着执行performTraversals()方法。显然,消息屏障的作用就是提升peformTraversals的优先级,确保视图的优先绘制。


不难发现,真正的进行渲染的起点是perfromTraversals()方法:


图片1.png


View.post的执行流程


View.post在不同版本的Android系统中,有着不同的实现,在API24以前,View.post所做的是:当View.post被调用时,直接向ViewRootImpl的mRunQueue中插入一个Runnable,然后在performTraversals()过程中,统一进行处理,这样一来,View.post()就会按照View.post()的调用顺序在”未来的某个时间点“进行执行,这说明:在这一系列的Android版本中,View.post的执行顺序就是本身调用View.post()的顺序




  1. 处理:这里的处理并非直接执行Runnable,而是统一插入到主线程的MessageQueue中去执行;

  2. “未来的某个时间点”,这个未来的某个时间点指的是perfromTraversals()中将ViewRootImpl中mRunQueue中的所有Runnable插入到MessageQueue之后的某个时间点。必然在performTraversals()之后。



图片2.png


如上图,必须得等到整个perfromTraversals方法体执行完成(包括)后,才有可能执行下一个Message(这里标注为了Runnable),而perfromTraversals()方法体中,会顺序地调用performMeasure()、performLayout()、performDraw()方法,这三个方法走完,意味着视图已经完成了渲染,此时的View.post()执行,必然是能落在视图创建之后


而API24及之后的版本中,View.post所做的事情发生了改变,当View.post()调用时,Runnable被插入到View各自的mRunQueue当中,也就是说,每个View都含有一个mRunQueue,当performTraversals()中,也没有统一处理了,而是根据 performTraversals()->dispatchAttachedToWindows()递归地调用到子View时,子View将自己的mRunQueue插入到主线程的MessageQueue,这意味着:在高版本的执行过程中,View.post()的执行顺序是按照视图被迭代到的顺序。


不变的是View.post()执行,必然是能落在视图创建之后,这也是为什么能够调用View.post()来获取一些屏幕上的View的数据的原因。


Handler.post()能像View.post()一样获取到宽、高数据吗?


Activity为我们暴露了三个常用的生命周期函数:onCreate()、onStart()、onResume()。通常我们对一些事件的监听、View的初始化设置都会在这三个生命周期函数中实现,以最后执行的onReumse()为例,我们在其中使用主线程的Handler.post()获取一个视图的数据,我们可以看看结果:


    override fun onResume(){
super.onResume()
Handler(Looper.getMainLooper()).post{
Log.d("getHeight",textView.height.toString())
}
}

  D/getHeight: 0


显然,失败了。


我们知道,一个的Activity的创建初期,DecorView并不会直接就和Activity建立联系,建立联系的过程在handleResumeActivity()当中,此时的DecorView被attach到了Activity之上。但是,我们需要明确一点:一个View如果没有和Activity建立联系,那么它将收不到系统的同步信号,也就无法更新(更新也没有意义,因为它没有地方去显示),我们看看handleResumeActiivty的执行方法体,可以发现,先走了onResume()的回调,再走了a.mDecor = decor这一步骤,上文我们提到,视图更新的事件是以Message的形式,在MessageQueue中”排队“的,如果我们在onResume()中插入一个消息去获取渲染之后的宽高数据,那么这时的MessageQueue大概是这样:


image.png


当前正在执行的是黄色的Message,这是一个从ActivityThread.java中H类发出的调度方法,它将会调用到handleResumeActivity中的一系列方法,最终走到onResume这,我们使用Handler.post(),我们会发现消息被插在了黄色的Message之后,但是此时的a.mDecor = decor还没有执行,更不可能已经发生绘制了,这也就意味着压根没渲染,没视图,自然也没数据,完整的流程如下:


image.png


end~


收起阅读 »

搞懂Socket通信(一)

搞懂Socket通信(一) Socket 在编程中并不陌生,即时通信、推送的应用场景也都是用到它。对于做Android的小伙伴来说,自己也很少的去写底层的逻辑,相应的是去使用第三方的开源框架,当然在一般场景下,第三方基本都能满足需求了,但对于极端的场景...
继续阅读 »



搞懂Socket通信(一)



Socket 在编程中并不陌生,即时通信、推送的应用场景也都是用到它。对于做Android的小伙伴来说,自己也很少的去写底层的逻辑,相应的是去使用第三方的开源框架,当然在一般场景下,第三方基本都能满足需求了,但对于极端的场景,当第三方框架满足不了的时候,还是需要自己去了解它。



一、七层协议


开放式系统互联通信参考模型(英语:Open System Interconnection Reference Model,缩写为 OSI),简称为OSI模型,是国际标准化组织提出。


在OSI模型中,通信分为七层模型


简单一点,也可以分为TCP/IP四层模型,或者五层模型,看图更方便理解。


1.png


图片来源


我们知道:IP协议对应网络层、TCP协议和UDP协议对应传输层、HTTP协议对应应用层


那么Socket在哪里?


2.jpg


图片来源


二、Socket 是什么


Socket 不是协议,是应用层与TCP/IP协议族通信的中间软件抽象层,是一组接口。


它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。


我们仅需要操作Socket来处理数据,而无需关心复杂的TCP/IP


三、有哪些基于Socket的协议


Socket是应用层与TCP/IP协议族通信的中间软件抽象层。


所以基于TCP/IP的协议都是基于Socket


比如HTTP协议,是基于TCP/IP协议之上的应用层协议


3.1 疑问


既然都是基于TCP/IP,那么我们知道Http访问完毕之后,是一次性访问,为什么我们用Socket实现TCP却是长连接,以流的形式发送数据呢。


3.2 解答


这是因为Http在请求时去建立TCP连接,完毕之后,就释放了TCP连接。


下面这个例子可以诠释http的流程。


例如:在浏览器地址栏键入URL,按下回车之后会经历以下流程:



  1. 浏览器向 DNS 服务器请求解析该 URL 中的域名所对应的 IP 地址;


  2. 解析出 IP 地址后,根据该 IP 地址和默认端口 80,和服务器建立TCP连接;


  3. 浏览器发出读取文件(URL 中域名后面部分对应的文件)的HTTP 请求,该请求报文作为 TCP 三次握手的第三个报文的数据发送给服务器;


  4. 服务器对浏览器请求作出响应,并把对应的 html 文本发送给浏览器;


  5. 释放 TCP连接;


  6. 浏览器将该 html 文本并显示内容;  



三、Android中长连接框架



  1. Apache MINA


  2. OKHttp中的 webSocket


  3. socketIo



...


这是笔者在开发安卓中使用过的长连接框架


后来才开始自己封装Socket,自己实现心跳机制、断线重连机制。


下一篇讲如何实现。

收起阅读 »

图文记录HTTPS知识点

底层网络监测工具:Wireshark一、名词全称HTTPSHTTP Secure/HTTP over SSL / HTTP over TLSSSLSecure Socket Layer :安全套接字层TLSTransport Layer Security:安全...
继续阅读 »

TLS链接建立时序图.jpg

底层网络监测工具:Wireshark

一、名词全称

  • HTTPS

HTTP Secure/HTTP over SSL / HTTP over TLS

  • SSL

Secure Socket Layer :安全套接字层

  • TLS

Transport Layer Security:安全传输层

TLS 是 SSL的前身,在HTTPS中指的是TLS

二、HTTP+加TLS后的传输层级示意

发送:HTTP -> TLS -> TCP -> IP -> LINK

接受:LINK -> IP-> TCP -> TLS -> HTTP

简单总结:传输加一层TLS加密层,接受加一层TLS解密层

三、本质

在客户端和服务器之间使用非对称加密协商出一套对称密钥,每次发送信息之前对内容进行加密,收到之后进行解密,实现加密传输。

  • 为什么不直接使用非对称加密?

非对称加密属于复杂型算法,会严重降低接受和发送的性能,降低处理效率。

四、TLS 链接流程

1.Client发送:Client Hello

  • 1.TLS 版本
  • 2.加密套件:对称加密算法、非对称加密算法、hash 算法
  • 3.客户端随机数
  • 4.其他信息

2.Server发送: ServerHello

  • 1.选出可匹配的TLS版本
  • 2.选出加密套件:对称加密算法、非对称加密算法、hash 算法
  • 3.其他

3.Server发送:服务器证书

  • 1.服务器证书(包含证书签名,公钥、主机名,地区等信息)
  • 2.证书签发机构证书信息(证书公钥,与公钥签名)
  • 3.指定根证书信息
  • 4.其他信息

验证规则:

  • 1.使用证书签发机构的私钥,对服务器的公钥的hash值进行签名。对应证书签发机构的公钥可以解开这个签名,确认服务器证书安全可信。
  • 2.使用指定的根证书的公钥,可以解开证书签发机构证书公钥的签名,确认签发机构安全可信
  • 到达客户端后,客户端验证链:
    • 1.客户端向系统确认服务器指定的根证书是否可信,可信则使用根证书中的公钥,解密服务器证书的签发机构证书的公钥签名
    • 2.使用签发机构的公钥解密服务器证书的公钥的签名,确认服务器证书安全可信

以上有一个验证环节未通过,那么TLS链接就建立失败。

服务器的证书只是证明网站或域名所有者,而不能证明是否合法

用Wireshark对建立HTTPS链接传输信息展示: image.png

4.Client 连发3个消息:

  • Pre-master secret(使用服务器公钥加密)
  • 将开始传输加密消息
  • Finished(加密后的握手信息)

image.png

服务端使用Pre-master secret按照相同的计算方式得出跟客户端一样的加密密钥

到此阶段,两端都将会持有以下信息:

1.客户端随机数

2.服务器随机数

3.Pre-master secret 随机数

4.Master secret(两端使用相同的算法,计算上面的三个信息,得出相同的结果)

用Master secret计算出下面的对称密钥:

5.客户端加密密钥(对称密钥)

6.服务器加密密钥(对称密钥)

7.客户端MAC secret

8.服务端MAC secret

到此两方都创建出了用于对称加密的传输的密钥

5.Server 连发2个消息:

  • 将开始传输加密消息
  • Finished(加密后的握手信息)

时序上并不一定在客户端最后两条消息之后

image.png

将上面确认的信息 通过用加密方式发送给客户端,客户端对数据进行验证,确认两方一致。


至此一共五个大步骤,都顺利完成TLS链接就建立成功,可以使用对应的对称加密密钥进行加密传输了。

五、问:

1.服务器随机数作用?

避免replay attack:重放攻击 虽然解不开加密信息,但是将发送的加密数据拦截后重复发送给服务器,来搞破坏,比如拦截的是转账请求,换个时间段继续重放请求,多次转账。 每次链接后拿到的服务器随机数都不一样,避免被重放攻击

2.既然是对称加密,为什么客户端于服务端要用不同的加密密钥?

为了避免发送出去的数据被原封不动的又被发回来,发和收使用不同的密钥,避免这种攻击

3.既然HTTPS都用上了,为什么有时候项目上还要内部再搞一套内部加密逻辑

这个问题是个大坑,以至于从理解上就容易产生误区。

  • 第一如果使用的证书不能100%信任,比如证书签发机构可能出卖用户、卖国、被恶势力控制。这时候整个HTTPS就彻底被瓦解,毫无意义。

逻辑是这么个逻辑,但在国内基本是无稽之谈!国外就不知道了。

真的不信任的话用私有证书,比起自己订一套不如TLS安全的加密逻辑来得保险得多。

当然这只是我个人的理解,也许有其他的知识面,欢迎评论提出。

  • 第二HTTPS 只是传输层的加密,防的是中间人攻击。所以服务器很被动,终端伪造证书被根证书信任后都可以访问他。

所以C端个体被破解后,可以对服务端造成小范围破坏,或者有大范围破坏的风险

比如HTTPS抓包,导致单用户可授权访问的接口请求参数,返回参数都被窃取,造成数据安全,和特定接口被攻击的风险。

因此服务端需要鉴别基于某些端信任私有证书后在业务层面的非法请求,以及单用户的重放攻击(replay attack)

那么可以考虑:

1.使用私有证书,但是这会给C端带来极高的运营成本,毕竟每次更新证书都要更新C端版本

2.使用对称加密,每次请求都生成一个新的请求签名。这样第一可以防重放攻击,第二服务器可以鉴别访问者是否合法。

3.使用非对称加密先加密原始数据,再丢给HTTPS进行传输。这样抓包也不担心,破解也不担心。只需权衡性能方面的损失,通常用于支付等极度敏感的数据交互场景上。


END

收起阅读 »

LiveData奇思妙用总结

前言 本文不涉及LiveData的基本使用方式。 阅读本文之前,强推推荐先看官方文档 LiveData的概览,官方文档写的非常好,并且很详细。 本文是一篇总结文,自己的一些使用结总结以及网上的学习归纳。 一、LiveData结...
继续阅读 »

前言



  • 本文不涉及LiveData的基本使用方式。


  • 阅读本文之前,强推推荐先看官方文档 LiveData的概览,官方文档写的非常好,并且很详细。


  • 本文是一篇总结文,自己的一些使用结总结以及网上的学习归纳。



一、LiveData结合ActivityResult


对 Activity Results Api不怎么了解的,可以先看下官方文档:


developer.android.com/training/ba…


1.1 调用系统相机


场景


调用系统相机,获取拍照后返回的照片


示例代码


// MainActivity.kt
private var takePhotoLiveData: TakePhotoLiveData = TakePhotoLiveData(activityResultRegistry, "key")

// 点击拍照按钮
mBinding.btTakePhoto.setOnClickListener {
takePhotoLiveData.takePhoto()
}

// 拍照返回的照片
takePhotoLiveData.observe(this) { bitmap ->
mBinding.imageView.setImageBitmap(bitmap)
}

几行代码搞定调用系统相机并且返回拍照后的图片。


封装示例


class TakePhotoLiveData(private val registry: ActivityResultRegistry, private val key: String) :
LiveData<Bitmap>() {

private lateinit var takePhotoLauncher: ActivityResultLauncher<Void?>

override fun onActive() {
takePhotoLauncher = registry.register(key, ActivityResultContracts.TakePicturePreview()) { result ->
value = result
}
}

override fun onInactive() = takePhotoLauncher.unregister()

fun takePhoto() = takePhotoLauncher.launch(null)

}

同理,请求权限也可以类似封装:


1.2 请求权限


场景


请求系统权限,例如GPS定位


示例代码


private var requestPermissionLiveData = RequestPermissionLiveData(activityResultRegistry, "key")

mBinding.btRequestPermission.setOnClickListener {
requestPermissionLiveData.requestPermission(Manifest.permission.RECORD_AUDIO)
}

requestPermissionLiveData.observe(this) { isGranted ->
toast("权限RECORD_AUDIO请求结果 $isGranted")
}

封装的代码跟上面类似,就不列出来了。


二、LiveData实现全局定时器


场景


一个全局计数器,Activity销毁时,计时器停止,不会导致内存泄露,Activity激活时,计时器开始,自动获取最新的计时。


示例代码


// 开启计时器
TimerGlobalLiveData.get().startTimer()

// 停止计时器
TimerGlobalLiveData.get().cancelTimer()

// 全局监听
TimerGlobalLiveData.get().observe(this) {
Log.i(TAG, "GlobalTimer value: == $it")
}

封装示例


class TimerGlobalLiveData : LiveData<Int>() {

private val handler: Handler = Handler(Looper.getMainLooper())

private val timerRunnable = object : Runnable {
override fun run() {
postValue(count++)
handler.postDelayed(this, 1000)
}
}

fun startTimer() {
count = 0
handler.postDelayed(timerRunnable, 1000)
}

fun cancelTimer() {
handler.removeCallbacks(timerRunnable)
}

companion object {
private lateinit var sInstance: TimerGlobalLiveData

private var count = 0

@MainThread
fun get(): TimerGlobalLiveData {
sInstance = if (::sInstance.isInitialized) sInstance else TimerGlobalLiveData()
return sInstance
}
}

}

三、共享数据


场景



  • 多个Fragment之间共享数据


  • Activity和Fragment共享数据


  • Activity/Fragment和自定义View共享数据



获取ViewModel实例时都用宿主Activity的引用即可。


示例代码


// Activity中
private val mViewModel by viewModels<ApiViewModel>()

// Fragment中
private val mViewModel by activityViewModels<ApiViewModel>()

// 自定义View中
fun setHost(activity: BaseActivity) {
var viewModel = ViewModelProvider(activity).get(ApiViewModel::class.java)
}

四、对于自定义View


关于自定义View,提一下我常用的方式。


通过ViewMode跟LiveData把自定义view从Activity中独立开来,自成一体,减少在Activity中到处调用自定义View的引用。


场景


Activity中有一个EndTripView自定义View,这个自定义View中有很多的小view,最右下角是一个按钮,点击按钮,调用结束行程的网络请求。


img


以前的做法是自定义View通过callback回调的方式将点击事件传递给Activity,在Activity中请求结束行程的接口,然后Activity中收到回调后,拿着自定义View的引用进行相应的ui展示


示例伪代码


// TestActivity
class TestActivity{
private lateinit var endTripView : EndTripView
private val endTripViewModel by viewModels<EndTripViewModel>()

fun onCreate{
endTripView = findViewById(R.id.view_end_trip)
endTripView.setListener{

onClickEndTrip(){
endTripViewModel.endTrip()
}
}
endTripViewModel.endTripLiveData.observer(this){ isSuccess ->
if(isSuccess){
endTripView.showEndTripSuccessUi()
}else {
endTripView.showEndTripFailedUi()
}
}
}
}

从上面伪代码中可以看到:



  • 操作逻辑都在Activity中,Activity中存在很多自定义View的回调,并且Activity中很多地方都有EndTripView的引用。


  • 自定义EndTripView需要定义很多的回调和公开很多的操作方法。


  • 如果业务很复杂,那么Activity会变得很臃肿并且不好维护。


  • 并且自定义EndTripView也严重依赖Activity,如果想在其他地方用,需要copy一份代码。



优化后伪代码


// Activity中代码
fun onCreate{
endTripView = findViewById(R.id.view_end_trip)
endTripView.setHost(this)
endTripViewModel.endTripLiveData.observer(this){ isSuccess ->
// 更新Activity的其它ui操作
}
}

// 自定义View中
class EndTripView : LinearLayout{

private var endTripViewModel: EndTripViewModel? = null

fun setHost(activity: BaseActivity) {
endTripViewModel = ViewModelProvider(activity).get(EndTripViewModel::class.java)
endTripViewModel.endTripLiveData.observer(this){ isSuccess ->
if(isSuccess){
showEndTripSuccessUi()
}else {
showEndTripFailedUi()
}
}
}

private fun clickEndTrip{
endTripViewModel?.endTrip()
}

private fun showEndTripSuccessUi(){...}

private fun showEndTripFailedUi(){...}
}

把自定义View相关的逻辑封装在自定义View里面,让自定义View成为一片独立的小天地,不再依赖Activity,这样Activity中的代码就非常简单了,自定义View也可以将方法都私有,去掉一些callback回调,实现高内聚。


并且由于LiveData本身的特效,跟Activity的生命周期想关联,并且点击结束行程按钮,Activity中如果注册了相应的LiveData,也可以执行相应的操作。


这样就把跟结束行程有关的自定义View的操作和ui更新放在自定义View中,Activity有关的操作在Activity中,相互隔离开来。


如果Activity中的逻辑不复杂,这种方式看不出特别的优势,但是如果Activity中逻辑复杂代码很多,这种方式的优点就很明显了。


五、LiveData实现自动注册和取消注册


利用LiveDatake可以感受Activity生命周期的优点,在Activity销毁时自动取消注册,防止内存泄露。


场景


进入Activity时请求定位,Activity销毁时移除定位,防止内存泄露


以前的方式


// 伪代码··
class MainActiviy {

override fun onStart() {
super.onStart()
LocationManager.register(this)
}

override fun onStop() {
super.onStop()
LocationManager.unRegister(this)
}
}

示例代码


val locationLiveData = LocationLiveData()
locationLiveData.observe(this){location ->
Log.i(TAG,"$location")
}

封装示例


class LocationLiveData : LiveData<Location>() {

private var mLocationManager =
BaseApp.instance.getSystemService(LOCATION_SERVICE) as LocationManager

private var gpsLocationListener: LocationListener = object : LocationListener {
override fun onLocationChanged(location: Location) {
postValue(location)
}

override fun onProviderDisabled(provider: String) = Unit
override fun onProviderEnabled(provider: String) = Unit
override fun onStatusChanged(provider: String, status: Int, extras: Bundle) = Unit
}

override fun onActive() {
mLocationManager.requestLocationUpdates(
LocationManager.GPS_PROVIDER, minTimeMs, minDistanceM, gpsLocationListener
)
}

override fun onInactive() {
mLocationManager.removeUpdates(gpsLocationListener)
}
}

当然,使用自定义的LifecycleObserver是一样的


class LocationObserver : LifecycleObserver {

@OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
fun startLoaction() {

}

@OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
fun stopLocation() {
...
}
}

myLifecycleOwner.getLifecycle().addObserver(LocationObserver())

具体见官方文档:


developer.android.com/topic/libra…


查看下LiveData的源码就知道,匿名内部类里面也是继承LifecycleObserver


六、LiveData 结合 BroadcastReceiver


场景


可以实现BroadcastReceiver的自动注册和取消注册,减少重复代码。


封装代码


class NetworkWatchLiveData : LiveData<NetworkInfo?>() {
private val mContext = BaseApp.instance
private val mNetworkReceiver: NetworkReceiver = NetworkReceiver()
private val mIntentFilter: IntentFilter = IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION)

override fun onActive() {
mContext.registerReceiver(mNetworkReceiver, mIntentFilter)
}

override fun onInactive() = mContext.unregisterReceiver(mNetworkReceiver)

private class NetworkReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val manager =
context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
val activeNetwork = manager.activeNetworkInfo
get().postValue(activeNetwork)
}
}

companion object {

private lateinit var sInstance: NetworkWatchLiveData

@MainThread
fun get(): NetworkWatchLiveData {
sInstance = if (::sInstance.isInitialized) sInstance else NetworkWatchLiveData()
return sInstance
}
}
}

七、LiveEventBus


场景


封装LiveData替换EventBus,实现消息总线,可以减少引入第三方库。


项目地址


github.com/JeremyLiao/…


实现原理


Android消息总线的演进之路:用LiveDataBus替代RxBus、EventBus


八、LiveData数据倒灌解决


发生原因


什么是LiveData数据倒灌?为什么会导致数据倒灌?


附上我以前写的一篇文章?


Activity销毁重建导致LiveData数据倒灌


解决办法



九、Application级别的ViewModel


场景


ViewModel不属于Activity或者Fragment所有,属于Application级别的


示例代码


protected <T extends ViewModel> T getApplicationScopeViewModel(@NonNull Class<T> modelClass) {
if (mApplicationProvider == null) {
mApplicationProvider = new ViewModelProvider((BaseApplication) this.getApplicationContext(),
getAppFactory(this));
}
return mApplicationProvider.get(modelClass);
}

private ViewModelProvider.Factory getAppFactory(Activity activity) {
Application application = checkApplication(activity);
return ViewModelProvider.AndroidViewModelFactory.getInstance(application);
}

项目地址


具体见KunMin大神的:


github.com/KunMinX/Jet…


十、LiveData的转换


场景


获取用户信息的接口返回的是一个User对象,但是页面上只需要显示用户的名字UserName,这样就没必要把整个User对象抛出去。


private val userLiveData: LiveData<User> = UserLiveData()
val userName: LiveData<String> = Transformations.map(userLiveData) {
user -> "${user.name} ${user.lastName}"
}

摘自官方文档:developer.android.com/topic/libra…


此外,还有一种转换方式:Transformations.switchMap(),具体见官方文档。


十一、合并多个LiveData数据源


场景


如果界面中有可以从本地数据库或网络更新的 LiveData 对象,则可以向 MediatorLiveData 对象添加以下源:



  • 与存储在数据库中的数据关联的 LiveData 对象。

  • 与从网络访问的数据关联的 LiveData 对象。


来自官方文档:developer.android.com/topic/libra…


示例代码


// 数据库来的结果
private val dbLiveData = StateLiveData<List<WxArticleBean>>()
// api网络请求的结果
private val apiLiveData = StateLiveData<List<WxArticleBean>>()
// 将上面两个结果进行合并,只有有一个更新,mediatorLiveDataLiveData就会收到
val mediatorLiveDataLiveData = MediatorLiveData<ApiResponse<List<WxArticleBean>>>().apply {
this.addSource(apiLiveData) {
this.value = it
}
this.addSource(dbLiveData) {
this.value = it
}
}

代码地址


github.com/ldlywt/Fast…


鸣谢


本文是一片总结文,会长期不定时更新。


如果有其他的LiveData奇思妙用,请留言,非常感谢。


最后,感谢网上各路大神的无私奉献。


收起阅读 »

autojs正经的交互-安卓与webview

效果展示缘起我一直觉得现在的autojs和webview交互不正经,监听弹框监听console日志监听网页title监听url尤其是监听弹框, 直接dismiss, 那那些需要弹框的网页怎么办?环境Autojs版本: 9.0.4Android版本: 8.0.0...
继续阅读 »

效果展示

效果.gif

缘起

我一直觉得现在的autojs和webview交互不正经,

  • 监听弹框
  • 监听console日志
  • 监听网页title
  • 监听url

尤其是监听弹框, 直接dismiss, 那那些需要弹框的网页怎么办?

环境

Autojs版本: 9.0.4

Android版本: 8.0.0

思路

  • autojs作为发出命令方, webview作为执行命令方, 使用方法: webView.evaluateJavascript
  • webview作为发出命令方, autojs作为执行命令方, 使用方法: webView.addJavascriptInterface

你将学到以下知识点

  • 获取随机颜色
  • 改变网页背景色
  • 改变按钮背景色
  • evaluateJavascript的回调函数
  • addJavascriptInterface的回调函数
  • @JavascriptInterface注解的使用
  • java类的内部interface

代码讲解

1. 创建类JSInterface, 然后打包成dex给autojs调用
package com.yashu.simple;

import android.webkit.JavascriptInterface;

public class JSInterface {
private JSCallback jsCallback;

public JSInterface setJsCallback(JSCallback jsCallback) {
this.jsCallback = jsCallback;
return this;
}


@JavascriptInterface
public void share(String callback) {
if (jsCallback != null) {
jsCallback.jsShare(callback);
}
}


public interface JSCallback {
void jsShare(String callback);
}
}
2. 网页
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<script language="javascript">
function onButtonClick() {
function randomHexColor() {
//随机生成十六进制颜色
return "#" + ("00000" + ((Math.random() * 0x1000000) << 0).toString(16)).substr(-6);
}
let color = randomHexColor();
jsInterface.share(color);
}
</script>
</head>

<body>
<img
id="image"
width="328"
height="185"
src="https://www.baidu.com/img/PCtm_d9c8750bed0b3c7d089fa7d55720d6cf.png"
/>

<button type="button" style="width: 328px; height: 185px; font-size: 40px" onclick="onButtonClick()">
改变安卓按钮颜色
</button>
</body>
</html>
3. 加载dex并导入类JSInterface
let dexPath = files.path("./classes2.dex");
runtime.loadDex(dexPath);
importClass(android.webkit.JavascriptInterface);
importClass(android.webkit.WebViewClient);
importClass(android.webkit.ValueCallback);
importClass(android.webkit.WebChromeClient);
importClass(com.yashu.simple.JSInterface);
4. UI界面
ui.layout(
<vertical>
<text text="牙叔教程 简单易懂" textSize="28sp" textColor="#fbfbfe" bg="#00afff" w="*" gravity="center"></text>
<webview id="webview" />
<button id="button" text="改变网页body颜色" />
</vertical>
);
5. 设置webview属性
let webView = ui.findById("webview");
webView.getSettings().setJavaScriptEnabled(true);
6. 设置按钮点击事件
// 发出命令方: Android
// 执行命令方: Html
// Html返回值: body背景色
ui.button.click(function () {
function test() {
function randomHexColor() {
//随机生成十六进制颜色
return "#" + ("00000" + ((Math.random() * 0x1000000) << 0).toString(16)).substr(-6);
}
document.body.bgColor = randomHexColor();
return "牙叔教程, " + document.body.bgColor;
}
let js = test.toString() + ";test();";
let valueCallback = new ValueCallback({
onReceiveValue: function (value) {
toastLog("网页返回值: " + value);
},
});
webView.evaluateJavascript(js, valueCallback);
});
7.网页日志日志打印到控制台, 方便查看错误
webView.setWebChromeClient(
new JavaAdapter(WebChromeClient, {
onConsoleMessage: function (message) {
message.message && log("h5: " + message.message());
},
})
);
8. JSInterface的使用方法
// 重点, 这里给html注册了一个全局对象: jsInterface
webView.addJavascriptInterface(new JSInterface().setJsCallback(new JSCallback()), "jsInterface");
9. 加载网页
html = files.path("./index.html");
webView.loadUrl("file://" + html);

名人名言

收起阅读 »

Objective-C 动态方法决议

一、动态方法决议当imp没有找到的时候的时候会赋值libobjc.A.dylib_objc_msgForward_impcache`,首先会进入如下代码逻辑: if (slowpath(behavior & LOOKUP_RESOLVER)) { ...
继续阅读 »

一、动态方法决议

imp没有找到的时候的时候会赋值libobjc.A.dylib_objc_msgForward_impcache`,首先会进入如下代码逻辑:

  if (slowpath(behavior & LOOKUP_RESOLVER)) {
behavior ^= LOOKUP_RESOLVER;
//要查找的对象,方法,类,1
return resolveMethod_locked(inst, sel, cls, behavior);
}
  • 这其实可以理解为一个单类,相同流程只会进入一次。
  • behavior上篇文章已经分析,值中有LOOKUP_INITIALIZE|LOOKUP_RESOLVER进入后异或LOOKUP_INITIALIZE|LOOKUP_RESOLVER^ LOOKUP_RESOLVER = LOOKUP_INITIALIZE,相当于清空了LOOKUP_RESOLVER
  • resolveMethod_locked参数最后一个是LOOKUP_INITIALIZE

resolveMethod_locked的源码如下:


static NEVER_INLINE IMP
resolveMethod_locked(id inst, SEL sel, Class cls, int behavior)
{
runtimeLock.assertLocked();
ASSERT(cls->isRealized());

runtimeLock.unlock();

if (! cls->isMetaClass()) {
//这里的cls是类
resolveInstanceMethod(inst, sel, cls);
}
else {
resolveClassMethod(inst, sel, cls);
if (!lookUpImpOrNilTryCache(inst, sel, cls)) {
//这里的cls是元类
resolveInstanceMethod(inst, sel, cls);
}
}

//又会去查找一次,既然这里又会去查找一次,那么肯定有什么地方会加入之前查找不存在的方法。
return lookUpImpOrForwardTryCache(inst, sel, cls, behavior);
}
  • 当快速和慢速消息查找都没有找到的时候进入了resolveMethod_locked
  • 查找的是实例方法则进行对象方法动态决议resolveInstanceMethod
  • 查找的是类方法则先进行类方法动态决议resolveClassMethod,再执行resolveInstanceMethod(这里resolveInstanceMethod调用与实例方法的resolveInstanceMethod参数不同。)。
  • 最后会调用lookUpImpOrForwardTryCache查找。

核心问题是最后要返回imp,那么先看下lookUpImpOrForwardTryCache进行的操作:

IMP lookUpImpOrForwardTryCache(id inst, SEL sel, Class cls, int behavior)
{
return _lookUpImpTryCache(inst, sel, cls, behavior);
}

只是一个简单的调用,继续排查:

ALWAYS_INLINE
static IMP _lookUpImpTryCache(id inst, SEL sel, Class cls, int behavior)
{
runtimeLock.assertUnlocked();

//是否初始化,正常情况下是已经初始化了。
if (slowpath(!cls->isInitialized())) {
// see comment in lookUpImpOrForward
//这就是慢速消息查找流程,与之前的区别是 behavior = LOOKUP_INITIALIZE,没有动态方法决议参数了。
return lookUpImpOrForward(inst, sel, cls, behavior);
}
//缓存查找
IMP imp = cache_getImp(cls, sel);
//找到直接跳转done
if (imp != NULL) goto done;
#if CONFIG_USE_PREOPT_CACHES
//动态共享缓存查找
if (fastpath(cls->cache.isConstantOptimizedCache(/* strict */true))) {
imp = cache_getImp(cls->cache.preoptFallbackClass(), sel);
}
#endif
//imp不存在继续慢速消息查找流程
if (slowpath(imp == NULL)) {
return lookUpImpOrForward(inst, sel, cls, behavior);
}

done:
//是否消息转发
if ((behavior & LOOKUP_NIL) && imp == (IMP)_objc_msgForward_impcache) {
return nil;
}
//返回imp
return imp;
}

  • isInitialized正常情况是不会进入的。
  • 先去缓存查找对应的imp,找到直接返回。
  • 没有找到会去动态共享缓存查找(如果支持)。
  • 仍然没有会进行lookUpImpOrForward也就是再进行一次慢速消息查找。

既然这个函数也是进行快速和慢速消息查找的,那么就说明resolveInstanceMethodresolveClassMethod可以在某个时机将方法加入类中。这样后面方法的调用才有意义。

二、对象方法动态决议 resolveInstanceMethod

通过源码分析发现在进行了快速与慢速消息查找后如果找不到imp,苹果仍然给了机会进行resolveInstanceMethod处理,那么核心肯定是要给类中添加imp,源码如下:

static void resolveInstanceMethod(id inst, SEL sel, Class cls)
{
runtimeLock.assertUnlocked();
ASSERT(cls->isRealized());
SEL resolve_sel = @selector(resolveInstanceMethod:);
//先进行元类查找是否实现了`resolveInstanceMethod`实例方法,也就是类的类方法。没有实现直接返回,这里不会返回,因为NSobject默认实现了。
if (!lookUpImpOrNilTryCache(cls, resolve_sel, cls->ISA(/*authenticated*/true))) {
// Resolver not implemented.
return;
}

BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
//系统自动发送了`resolveInstanceMethod`消息,由于消息的接受者是类,所以是+方法。
bool resolved = msg(cls, resolve_sel, sel);

// Cache the result (good or bad) so the resolver doesn't fire next time.
// +resolveInstanceMethod adds to self a.k.a. cls
//快速慢速查找
IMP imp = lookUpImpOrNilTryCache(inst, sel, cls);

if (resolved && PrintResolving) {
if (imp) {
_objc_inform("RESOLVE: method %c[%s %s] "
"dynamically resolved to %p",
cls->isMetaClass() ? '+' : '-',
cls->nameForLogging(), sel_getName(sel), imp);
}
else {
// Method resolver didn't add anything?
_objc_inform("RESOLVE: +[%s resolveInstanceMethod:%s] returned YES"
", but no new implementation of %c[%s %s] was found",
cls->nameForLogging(), sel_getName(sel),
cls->isMetaClass() ? '+' : '-',
cls->nameForLogging(), sel_getName(sel));
}
}
}
  • 先进行元类resolveInstanceMethod的查找和缓存。
  • 系统自动给类发送了resolveInstanceMethod消息。既然是类调用的,那么就是+方法。
  • 接着进行了快速慢速方法查找imp,但是没有返回imp(为什么不返回?这里只是缓存,如果有的话)。
  • lookUpImpOrNilTryCachelookUpImpOrForwardTryCache唯一的区别是是否进行动态转发。这里不进行动态转发。
  • 可以看到返回的resolved只是进行了日志打印。也就是resolved返回YES/NO对功能没有影响。

那么就有个问题?
既然查找了imp为什么不进行返回操作?而resolveInstanceMethod调用结束后还查了一次?

2.1 + (BOOL)resolveInstanceMethod 调试分析

resolveInstanceMethod源码跟踪流程如下:

  • cls->ISA元类也就是HPObject元类中查找有没有实现resolveInstanceMethod-imp,最终会找到NSObject元类然后将resolveInstanceMethod-imp缓存写入HPObject元类的缓存。(NSObject默认实现了)。
  • 给类发送resolveInstanceMethod消息。
  • HPObject查找instanceMethod有没有实现,没有实现会将instanceMethod-_objc_msgForward_impcache(IMP)写入HPObject缓存。这个时候由于LOOKUP_NIL的存在返回的是`nil。
  • 如果lookUpImpOrNilTryCache没有找到imp会返回继续执行lookUpImpOrForwardTryCache继续进行缓存->消息慢速查找流程(消息慢速查找不会执行)。因为前面已经写入了对应缓存。这次会从缓存中获取到imp_objc_msgForward_impcacheimp。不会进入消息慢速查找流程,直接进行了消息转发。

这就说明resolveInstanceMethod中首先元类查找resolveInstanceMethod,目的是将resolveInstanceMethod写入缓存。然后类发送resolveInstanceMethod消息。接着lookUpImpOrNilTryCache调用是将的imp加入缓存中(无论是否找到,找不到会存入_objc_msgForward_impcache)。返回后lookUpImpOrForwardTryCache从缓存中找方法返回。

结论:resolveInstanceMethod中lookUpImpOrNilTryCache只是将方法插入缓存,返回后lookUpImpOrForwardTryCache从缓存中获取imp 这也是调用两次的原因。

2.2 + (BOOL)resolveInstanceMethod 实现

既然系统已经给了+ (BOOL)resolveInstanceMethod:(SEL)sel进行容错处理,那么就实现下:


+ (BOOL)resolveInstanceMethod:(SEL)sel {
NSLog(@"resolveInstanceMethod: %@-%@",self,NSStringFromSelector(sel));
return [super resolveInstanceMethod:sel];
}


调用后发现这个方法调用了两次:


resolveInstanceMethod: HPObject-instanceMethod
resolveInstanceMethod: HPObject-instanceMethod


  • HPObject的元类中能找到resolveInstanceMethod方法,缓存的直接是自己的imp了。
  • 仍然是没有命中进行了消息转发。

消息转发会进入class_getInstanceMethod

Method class_getInstanceMethod(Class cls, SEL sel)
{
if (!cls || !sel) return nil;

// This deliberately avoids +initialize because it historically did so.

// This implementation is a bit weird because it's the only place that
// wants a Method instead of an IMP.

#warning fixme build and search caches

// Search method lists, try method resolver, etc.
lookUpImpOrForward(nil, sel, cls, LOOKUP_RESOLVER);

#warning fixme build and search caches

return _class_getMethod(cls, sel);
}

又进行了一次lookUpImpOrForward所以这也是调用了两次的原因。但是这次不进行消息转发了,所以不会造成死循环。

总结:第一次没有命中后,再进行消息转发后又会进行一次lookUpImpOrForward消息慢速查找流程,所以resolveInstanceMethod会执行两次。

那么如果实现中添加了imp就肯定只调用一次了。
修改代码如下:


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

+ (BOOL)resolveInstanceMethod:(SEL)sel {
NSLog(@"HPObject resolveInstanceMethod: %@-%@",self,NSStringFromSelector(sel));
if (sel == @selector(instanceMethod)) {
IMP instanceMethod1 = class_getMethodImplementation(self, @selector(instanceMethod1));
Method method = class_getInstanceMethod(self, @selector(instanceMethod1));
const char *type = method_getTypeEncoding(method);
return class_addMethod(self, sel, instanceMethod1, type);
}
return NO;
}

按照源码理解在lookUpImpOrNilTryCache调用中只是增加到了缓存中,后面lookUpImpOrForwardTryCache会从缓存中查找,找到imp然后执行。

+ (BOOL)resolveInstanceMethod:(SEL)sel返回NO/YES根据源码来看只是打印日志相关的内容,应该是没有影响的。经过调试验证确实没有影响。

结论:

  • resolveInstanceMethod 调用中只是对方法的缓存,lookUpImpOrNilTryCache 从缓存中再次查找方法。这也是为什么会查找两次的原因。
  • resolveInstanceMethod 执行两次的原因是,在方法没有命中的时候消息转发过程中会再次进行lookUpImpOrForward(消息慢速查找),这就是执行两次的原因。
  • + (BOOL)resolveInstanceMethod:(SEL)sel 返回值不会影响功能,只是对日志打印有影响,并且默认情况下是不打印日志的。

三、类方法动态决议resolveClassMethod

在上面最开始分析的时候类方法动态决议会先调用resolveClassMethod,如果没有命中那么就会调用resolveInstanceMethod


resolveClassMethod(inst, sel, cls);
if (!lookUpImpOrNilTryCache(inst, sel, cls)) {
resolveInstanceMethod(inst, sel, cls);
}

resolveClassMethod的实现如下:


static void resolveClassMethod(id inst, SEL sel, Class cls)
{
runtimeLock.assertUnlocked();
ASSERT(cls->isRealized());
ASSERT(cls->isMetaClass());
//不会进入这里,先查找元类是否实现`resolveClassMethod`
if (!lookUpImpOrNilTryCache(inst, @selector(resolveClassMethod:), cls)) {
// Resolver not implemented.
return;
}
//类方法存在元类中,操作元类防止没有实现。
Class nonmeta;
{
mutex_locker_t lock(runtimeLock);
nonmeta = getMaybeUnrealizedNonMetaClass(cls, inst);
// +initialize path should have realized nonmeta already
if (!nonmeta->isRealized()) {
_objc_fatal("nonmeta class %s (%p) unexpectedly not realized",
nonmeta->nameForLogging(), nonmeta);
}
}
BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
//非元类调用,也就是类方法
bool resolved = msg(nonmeta, @selector(resolveClassMethod:), sel);

// Cache the result (good or bad) so the resolver doesn't fire next time.
// +resolveClassMethod adds to self->ISA() a.k.a. cls
IMP imp = lookUpImpOrNilTryCache(inst, sel, cls);

if (resolved && PrintResolving) {//......}
}

  • 第一个lookUpImpOrNilTryCache查找元类是否实现,先将resolveClassMethod插入HPObject元类的缓存中。resolveClassMethodNSObject默认实现了。
  • 操作元类,防止元类没有实现。
  • 元类中是以对象方法存在,所以在类中实现类方法就可以了。系统主动给类方法发送+ resolveClassMethod消息。这里细节的一点是通过nonmeta来发送消息。
  • lookUpImpOrNilTryCache查找目标imp,先缓存后慢速。查找到后将imp插入缓存,没有找到则将_objc_msgForward_impcache插入缓存。

实现如下:

+ (BOOL)resolveClassMethod:(SEL)sel {
NSLog(@"resolveClassMethod: %@-%@",self,NSStringFromSelector(sel));
return [super resolveClassMethod:sel];
}

调用后发现打印了8次:

resolveClassMethod: HPObject-encodeWithOSLogCoder:options:maxLength:
resolveClassMethod: HPObject-encodeWithOSLogCoder:options:maxLength:
resolveClassMethod: HPObject-encodeWithOSLogCoder:options:maxLength:
resolveClassMethod: HPObject-classMethod
resolveClassMethod: HPObject-encodeWithOSLogCoder:options:maxLength:
resolveClassMethod: HPObject-encodeWithOSLogCoder:options:maxLength:
resolveClassMethod: HPObject-encodeWithOSLogCoder:options:maxLength:
resolveClassMethod: HPObject-classMethod
  • 其中encodeWithOSLogCoder与我们无关,classMethod出现两次符合预期(另外一次消息转发过程中调用)。
  • 当调用resolveClassMethod没有实现的时候,就调用resolveInstanceMethod去查找(这里的cls参数是元类,与查找实例方法不同),仍然没有找到就执行lookUpImpOrForwardTryCache
  • 最后在消息转发的时候会再执行一次方法动态决议。

修改实现:

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

+ (BOOL)resolveClassMethod:(SEL)sel {
NSLog(@"resolveClassMethod: %@-%@",self,NSStringFromSelector(sel));
if (sel == @selector(classMethod)) {
IMP classMethod1 = class_getMethodImplementation(objc_getMetaClass("HPObject"), @selector(classMethod1));
Method method = class_getClassMethod(self, @selector(classMethod1));
const char *type = method_getTypeEncoding(method);
return class_addMethod(objc_getMetaClass("HPObject"), sel, classMethod1, type);
}
return [super resolveClassMethod:sel];
}

这个时候就调用一次了。

既然resolveClassMethod找不到的时候会执行一次resolveInstanceMethod,那意味者可以在resolveInstanceMethod中对类方法进行处理。

+ (BOOL)resolveInstanceMethod:(SEL)sel {
NSLog(@"HPObject resolveInstanceMethod: %@-%@",self,NSStringFromSelector(sel));
return NO;
}

+ (BOOL)resolveClassMethod:(SEL)sel {
NSLog(@"HPObject resolveClassMethod: %@-%@",self,NSStringFromSelector(sel));
return [super resolveClassMethod:sel];
}

这个时候调试发现resolveInstanceMethod并没有执行。为什么?因为这里是HPObject元类调用resolveInstanceMethod

根据isa的走位图,NSObject同时也是元类,那么元类调用+方法就要存到元类的元类中也就是存在根元类的元类,那么就是NSObject自己,通过NSObjectresolveInstanceMethod方法就可以实现了。
添加一个NSObject的分类,实现方法:


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

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

+ (BOOL)resolveInstanceMethod:(SEL)sel {
NSLog(@"resolveInstanceMethod: %@-%p-%@",self,self,NSStringFromSelector(sel));
if (sel == @selector(instanceMethod)) {
IMP instanceMethod1 = class_getMethodImplementation(self, @selector(instanceMethod1));
Method method = class_getInstanceMethod(self, @selector(instanceMethod1));
const char *type = method_getTypeEncoding(method);
return class_addMethod(self, sel, instanceMethod1, type);
} else if (sel == @selector(classMethod)) {
IMP classMethod1 = class_getMethodImplementation(objc_getMetaClass("HPObject"), @selector(classMethod1));
Method method = class_getInstanceMethod(objc_getMetaClass("HPObject"), @selector(classMethod1));
const char *type = method_getTypeEncoding(method);
return class_addMethod(objc_getMetaClass("HPObject"), sel, classMethod1, type);
}
return NO;
}

分别调用instanceMethodclassMethod输出如下:

HPObject:0x1000082c8, HPMetaObject:0x1000082a0, NSObject:0x100358140, NSMetaObject:0x1003580f0

resolveInstanceMethod: HPObject-0x1000082c8-instanceMethod //类
-[NSObject(Additions) instanceMethod1]
resolveInstanceMethod: HPObject-0x1000082a0-classMethod //元类
HPObjcTest[59242:11857560] +[NSObject(Additions) classMethod1]

这样就在NSObjectresolveInstanceMethod中即处理了类方法也处理了实例方法。两次调用参数不同,一次是类调用,一次是元类调用。

⚠️如果两个都实现在HPObject类中,则都是类调用。

总结

  • resolveClassMethod 调用中只是对方法的缓存,lookUpImpOrNilTryCache会从缓存中再次查找方法,这也是为什么会查找两次的原因。
  • resolveClassMethod 执行两次的原因是在方法没有命中的时候消息转发过程中会再次进行lookUpImpOrForward(消息慢速查找),再次走这个流程。这就是执行两次的原因。LOOKUP_NIL有值,所以不会再次消息转发,不会造成死循环。)
  • resolveClassMethod 没有命中的时候会先调用resolveInstanceMethod(这里的cls是元类),再次调用时因为NSObject是元类的父类。
  • 这里resolveInstanceMethod由于是元类调用,所以只能实现在NSObject的分类中。(根元类的元类是自己,它的父类是NSObject
  • + (BOOL)resolveClassMethod:(SEL)sel 返回值不会影响功能,只是对日志打印有影响。

三、aop & oop

那么动态方法决议的意义在哪里呢?
这是苹果在sel查找imp找不到的时候给的一次解决错误的机会。有什么意义呢?在NSObject的分类中,所有找不到的OC方法都能在resolveInstanceMethod中监听到。
那么在自己的工程中可以根据类名前缀、模块以及事物进行区分prefix_ module_traffic。当发现有问题的时候可以进行容错处理并且上报错误信息。 比如HP_Setting_didClickLogin出现问题的时候进行上报,当超过阈值时进行报警。

这种方式就是aop切面编程。我们比较习惯的方式是oop

oop
oop分工非常明确,耦合度小,冗余代码。一般情况下会提取公共的类,但是遵循后会对它有强依赖,强耦合。
这些其实不是我们关心的,我们更关心业务的内容,所以公共类尽量少侵入,最好无侵入。通过动态方式注入代码,对原始方法没有影响。这就相当于整个切面切入了,要切入的方法和类就是切点。aopoop的延伸。

aop
aop的缺点在上面的例子中是if-else过多冗余。正如上面看到的那样,方法会调用很多次浪费了相应的性能。如果命中还好,没有命中会走多次,会有性能消耗。它是消息转发机制的前一个阶段。意味着如果在这里做了容错处理,后面的流程就被切掉了。苹果写转发流程就没有意义了。

如果其它模块也做了相应处理,重复了这块不一定会执行到。所以在后面的流程做aop更合理。



作者:HotPotCat
链接:https://www.jianshu.com/p/7daa33b95cd3
收起阅读 »

GCD底层分析 - 队列、同步异步函数

一、GCD 简介1.1 GCDGCD(Grand Central Dispatch)本质是 将任务添加到队列,并且指定执行任务的函数。GCD是纯C语言实现,提供了非常强大的函数。GCD的优势:是苹果公司为多核的并行运算提出的解决方案。会自动利用更多的...
继续阅读 »

一、GCD 简介

1.1 GCD

GCDGrand Central Dispatch)本质是 将任务添加到队列,并且指定执行任务的函数

GCD是纯C语言实现,提供了非常强大的函数。GCD的优势:

  • 是苹果公司为多核的并行运算提出的解决方案。
  • 会自动利用更多的CPU内核(比如双核、四核)。
  • 会自动管理线程的生命周期(创建线程、调度任务、销毁线程)。
  • 程序员只需要告诉 GCD 想要执行什么任务,不需要编写任何线程管理代码。

最简单的一个例子:

dispatch_block_t block = ^{
NSLog(@"Hello GCD");
};
//串行队列
dispatch_queue_t quene = dispatch_queue_create("com.HotpotCat.zai", NULL);
//函数
dispatch_async(quene, block);
  • 任务使用block封装,这个block块就是任务。任务的block没有参数也没有返回值
  • quene是创建的串行队列。
  • 通过函数将任务和队列关联在一起。

1.2 GCD 的作用



二、函数与队列

2.1 同步与异步

  • dispatch_async异步执行任务的函数。
    • 不用等待当前语句执行完毕,就可以执行下一条语句。
    • 会开启线程执行block的任务。
    • 异步是多线程的代名词。
  • dispatch_sync同步函数。
    • 必须等待当前语句执行完毕,才会执行下一条语句。
    • 不会开启线程。
    • 在当前执行block任务。
  • block块是在函数内部执行的。

有如下案例:

- (void)test {
CFAbsoluteTime time = CFAbsoluteTimeGetCurrent();
dispatch_queue_t queue = dispatch_queue_create("com.HotpotCat.zai", DISPATCH_QUEUE_SERIAL);

dispatch_async(queue, ^{
NSLog(@"1: %f",CFAbsoluteTimeGetCurrent() - time);
method();
});

dispatch_sync(queue, ^{
NSLog(@"2: %f",CFAbsoluteTimeGetCurrent() - time);
method();
});

method();
NSLog(@"3: %f",CFAbsoluteTimeGetCurrent() - time);
}

void method() {
sleep(3);
}

输出:

1: 0.000055
2: 3.000264
3: 9.001459

说明同步异步是一个耗时的操作。

2.2 串行队列与并发队列




  • 队列:是一种数据结构,支持FIFO原则。
  • 串行队列:一次只能进一个任务,任务之间需要排队,DFQ_WIDTH = 1。在上图中任务一比任务二先执行,队列中的任务按顺序执行。
  • 并发队列:一次能调度多个任务(调度多个并不是执行多个,队列不具备执行任务能力,线程才能执行任务),任务一先调度不一定比任务二先执行,得看线程池的调度情况(先调度不一定先执行)。

⚠️ 队列与线程没有任何关系,队列存储任务,线程执行任务

2.2.1 案例1

有如下代码:

dispatch_queue_t queue = dispatch_queue_create("com.HotpotCat.zai", DISPATCH_QUEUE_CONCURRENT);
NSLog(@"1");
dispatch_async(queue, ^{
NSLog(@"2");
dispatch_sync(queue, ^{
NSLog(@"3");
});
NSLog(@"4");
});
NSLog(@"5");

输出:1 5 2 3 4
分析:queue是并发队列,dispatch_async是异步函数。所以先执行1 5, 在dispatch_asyncblock内部先执行2,由于dispatch_sync同步函数导致3执行完才能执行4。 所以输出1 5 2 3 4

2.2.2 案例2

将上面的例子中DISPATCH_QUEUE_CONCURRENT改为NULL(DISPATCH_QUEUE_SERIAL):





这个时候在执行到dispatch_sync的时候发生了死锁。
对于同步函数会进行护犊子,堵塞的是block之后的代码。queue中的任务如下(这里为了简单没有写外层异步函数的任务块):



由于queue是串行队列并且支持FIFO,在queue中块任务为同步函数需要保证任务3执行,但是queue是串行队列,任务3的执行依赖于任务4,而任务4以来块任务所以发生了循环等待,造成死锁。如果改为并发队列(34可以一起执行)或者任务3为异步函数调用则就不会发生死锁了。

2.2.3 案例3

继续修改代码,将任务4删除,代码如下:

- (void)test{
dispatch_queue_t queue = dispatch_queue_create("com.HotpotCat.zai", DISPATCH_QUEUE_SERIAL);
NSLog(@"1");
dispatch_async(queue, ^{
NSLog(@"2");
dispatch_sync(queue, ^{
NSLog(@"3");
});
});
NSLog(@"5");
}

这个时候仍然发生了死锁。对于queue而言有两个任务块以及一个任务2





任务块2阻塞了任务块1的执行完毕,任务块2的执行依赖于任务3的执行,任务3的执行完毕以来于任务块1。这样就造成了死锁。

2.2.4 案例4

- (void)test {
dispatch_queue_t queue = dispatch_queue_create("com.HotpotCat.zai", DISPATCH_QUEUE_CONCURRENT);

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

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

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

NSLog(@"0");

dispatch_async(queue, ^{
NSLog(@"7");
});
dispatch_async(queue, ^{
NSLog(@"8");
});
dispatch_async(queue, ^{
NSLog(@"9");
});

}

输出选项:

A: 1230789
B: 1237890
C: 3120798
D: 2137890

queue是并发队列,任务3是同步函数执行的。所以任务3后面的任务会被阻塞。那么任务0肯定在任务3之后执行,任务7、8、9肯定在任务0之后执行。所以就有1、 2、 3 — 0 — 7、 8、 9。而由于本身是并发队列,所以1、2、3之间是无序的,7、8、9之间也是无序的。所以A、C符合。

dispatch_async中增加耗时操作,修改如下:

- (void)test {
dispatch_queue_t queue = dispatch_queue_create("com.HotpotCat.zai", DISPATCH_QUEUE_CONCURRENT);

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

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

dispatch_sync(queue, ^{
sleep(1);
NSLog(@"3");
});

NSLog(@"0");

dispatch_async(queue, ^{
sleep(3);
NSLog(@"7");
});
dispatch_async(queue, ^{
sleep(2);
NSLog(@"8");
});
dispatch_async(queue, ^{
sleep(1);
NSLog(@"9");
});
}

当任务中有耗时操作时1、2就不一定在0之前执行了,核心点在于dispatch_sync只能保证任务3先执行不能保证1、2先执行。而0仍然在7、8、9之前。
所以只能保证:30之前执行。07、8、9之前执行(1、2、7、8、9无序,31、2之间无序)。此时上面的代码输出3098721

修改queue为串行队列DISPATCH_QUEUE_SERIAL后,1、2、3以及7、8、9就是按顺序执行了1230789就选A了。

小结:

  • 同步函数串行队列:
    • 不会开启线程,在当前线程执行任务。
    • 任务串行执行,一个接着一个。
    • 会产生阻塞。
  • 同步函数并发队列:
    • 不会开启线程,在当前线程执行任务。
    • 任务一个接着一个。
  • 异步函数串行队列:
    • 开启线程,一条新线程。
    • 任务一个接着一个。
  • 异步函数并发队列:
    • 开启线程,在当前线程执行任务。
    • 任务异步执行,没有顺序,与CPU调用有关。

三、主队列与全局并发队列

通过GCD创建队列,一般有以下4种方式:

- (void)test {
//串行队列
dispatch_queue_t serial = dispatch_queue_create("com.HotpotCat.cat", DISPATCH_QUEUE_SERIAL);
//并发队列
dispatch_queue_t concurrent = dispatch_queue_create("com.HotpotCat.zai", DISPATCH_QUEUE_CONCURRENT);
//主队列
dispatch_queue_t mainQueue = dispatch_get_main_queue();
//全局队列
dispatch_queue_t globalQueue = dispatch_get_global_queue(0, 0);
NSLog(@"serial:%@\nconcurrent:%@\nmainQueue:%@\nglobalQueue:%@",serial,concurrent,mainQueue,globalQueue);
}

3.1 主队列

dispatch_get_main_queue声明如下:




3.1.1 dispatch_get_main_queue 源码分析

那么dispatch_get_main_queue具体是在什么时机创建的呢?
main_queueblock中打断点bt查看堆栈定位到调用是在libdispatch中:




dispatch_get_main_queue
dispatch_get_main_queue定义如下:

dispatch_queue_main_t
dispatch_get_main_queue(void)
{
//dispatch_queue_main_t 是类型,真正的对象是_dispatch_main_q
return DISPATCH_GLOBAL_OBJECT(dispatch_queue_main_t, _dispatch_main_q);
}

dispatch_get_main_queue返回了DISPATCH_GLOBAL_OBJECT,参数是dispatch_queue_main_t以及_dispatch_main_q

DISPATCH_GLOBAL_OBJECT的宏定义如下:

#define DISPATCH_GLOBAL_OBJECT(type, object) ((OS_OBJECT_BRIDGE type)&(object))

可以看到dispatch_queue_main_t是一个类型,真正的对象是object也就是_dispatch_main_q

_dispatch_main_q
_dispatch_main_q的函数搜索不到,此时通过赋值可以直接定位到:




当然也可以通过label com.apple.main-thread搜索定位到。

struct dispatch_queue_static_s _dispatch_main_q = {
DISPATCH_GLOBAL_OBJECT_HEADER(queue_main),
#if !DISPATCH_USE_RESOLVERS
.do_targetq = _dispatch_get_default_queue(true),
#endif
.dq_state = DISPATCH_QUEUE_STATE_INIT_VALUE(1) |
DISPATCH_QUEUE_ROLE_BASE_ANON,
.dq_label = "com.apple.main-thread",
//DQF_WIDTH(1) 串行队列
.dq_atomic_flags = DQF_THREAD_BOUND | DQF_WIDTH(1),
.dq_serialnum = 1,
};

  • DISPATCH_GLOBAL_OBJECT_HEADER(queue_main)传递的参数是queue_main
  • DQF_WIDTH区分串行并行队列。并不是通过serialnum
  • 最终返回的类型是dispatch_queue_main_t。在使用过程中都是使用dispatch_queue_t接收的。

dispatch_queue_static_s定义如下:

typedef struct dispatch_lane_s {
DISPATCH_LANE_CLASS_HEADER(lane);
/* 32bit hole on LP64 */
} DISPATCH_ATOMIC64_ALIGN *dispatch_lane_t;

// Cache aligned type for static queues (main queue, manager)
struct dispatch_queue_static_s {
struct dispatch_lane_s _as_dl[0]; \
DISPATCH_LANE_CLASS_HEADER(lane);
} DISPATCH_CACHELINE_ALIGN;

内部实际上是dispatch_lane_s

3.2 全局队列

dispatch_get_global_queue的实现如下:

dispatch_queue_global_t
dispatch_get_global_queue(intptr_t priority, uintptr_t flags)
{
dispatch_assert(countof(_dispatch_root_queues) ==
DISPATCH_ROOT_QUEUE_COUNT);

//过量使用直接返回0
if (flags & ~(unsigned long)DISPATCH_QUEUE_OVERCOMMIT) {
return DISPATCH_BAD_INPUT;
}
//根据优先级返回qos
dispatch_qos_t qos = _dispatch_qos_from_queue_priority(priority);
#if !HAVE_PTHREAD_WORKQUEUE_QOS
if (qos == QOS_CLASS_MAINTENANCE) {
qos = DISPATCH_QOS_BACKGROUND;
} else if (qos == QOS_CLASS_USER_INTERACTIVE) {
qos = DISPATCH_QOS_USER_INITIATED;
}
#endif
if (qos == DISPATCH_QOS_UNSPECIFIED) {
return DISPATCH_BAD_INPUT;
}
//调用 _dispatch_get_root_queue
return _dispatch_get_root_queue(qos, flags & DISPATCH_QUEUE_OVERCOMMIT);
}
  • 返回dispatch_queue_global_t
  • 通过_dispatch_get_root_queue获取队列。

_dispatch_get_root_queue实现:

static inline dispatch_queue_global_t
_dispatch_get_root_queue(dispatch_qos_t qos, bool overcommit)
{
if (unlikely(qos < DISPATCH_QOS_MIN || qos > DISPATCH_QOS_MAX)) {
DISPATCH_CLIENT_CRASH(qos, "Corrupted priority");
}
return &_dispatch_root_queues[2 * (qos - 1) + overcommit];
}
  • 先对优先级进行验证。
  • _dispatch_root_queues集合中取数据。

_dispatch_root_queues实现:

//静态变量集合,随时调用随时取。
struct dispatch_queue_global_s _dispatch_root_queues[] = {
#define _DISPATCH_ROOT_QUEUE_IDX(n, flags) \
((flags & DISPATCH_PRIORITY_FLAG_OVERCOMMIT) ? \
DISPATCH_ROOT_QUEUE_IDX_##n##_QOS_OVERCOMMIT : \
DISPATCH_ROOT_QUEUE_IDX_##n##_QOS)

#define _DISPATCH_ROOT_QUEUE_ENTRY(n, flags, ...) \
[_DISPATCH_ROOT_QUEUE_IDX(n, flags)] = { \
DISPATCH_GLOBAL_OBJECT_HEADER(queue_global), \
.dq_state = DISPATCH_ROOT_QUEUE_STATE_INIT_VALUE, \
.do_ctxt = _dispatch_root_queue_ctxt(_DISPATCH_ROOT_QUEUE_IDX(n, flags)), \
.dq_atomic_flags = DQF_WIDTH(DISPATCH_QUEUE_WIDTH_POOL), \
.dq_priority = flags | ((flags & DISPATCH_PRIORITY_FLAG_FALLBACK) ? \
_dispatch_priority_make_fallback(DISPATCH_QOS_##n) : \
_dispatch_priority_make(DISPATCH_QOS_##n, 0)), \
__VA_ARGS__ \
}

_DISPATCH_ROOT_QUEUE_ENTRY(MAINTENANCE, 0,
.dq_label = "com.apple.root.maintenance-qos",
.dq_serialnum = 4,
),
_DISPATCH_ROOT_QUEUE_ENTRY(MAINTENANCE, DISPATCH_PRIORITY_FLAG_OVERCOMMIT,
.dq_label = "com.apple.root.maintenance-qos.overcommit",
.dq_serialnum = 5,
),
_DISPATCH_ROOT_QUEUE_ENTRY(BACKGROUND, 0,
.dq_label = "com.apple.root.background-qos",
.dq_serialnum = 6,
),
_DISPATCH_ROOT_QUEUE_ENTRY(BACKGROUND, DISPATCH_PRIORITY_FLAG_OVERCOMMIT,
.dq_label = "com.apple.root.background-qos.overcommit",
.dq_serialnum = 7,
),
_DISPATCH_ROOT_QUEUE_ENTRY(UTILITY, 0,
.dq_label = "com.apple.root.utility-qos",
.dq_serialnum = 8,
),
_DISPATCH_ROOT_QUEUE_ENTRY(UTILITY, DISPATCH_PRIORITY_FLAG_OVERCOMMIT,
.dq_label = "com.apple.root.utility-qos.overcommit",
.dq_serialnum = 9,
),
_DISPATCH_ROOT_QUEUE_ENTRY(DEFAULT, DISPATCH_PRIORITY_FLAG_FALLBACK,
.dq_label = "com.apple.root.default-qos",
.dq_serialnum = 10,
),
_DISPATCH_ROOT_QUEUE_ENTRY(DEFAULT,
DISPATCH_PRIORITY_FLAG_FALLBACK | DISPATCH_PRIORITY_FLAG_OVERCOMMIT,
.dq_label = "com.apple.root.default-qos.overcommit",
.dq_serialnum = 11,
),
_DISPATCH_ROOT_QUEUE_ENTRY(USER_INITIATED, 0,
.dq_label = "com.apple.root.user-initiated-qos",
.dq_serialnum = 12,
),
_DISPATCH_ROOT_QUEUE_ENTRY(USER_INITIATED, DISPATCH_PRIORITY_FLAG_OVERCOMMIT,
.dq_label = "com.apple.root.user-initiated-qos.overcommit",
.dq_serialnum = 13,
),
_DISPATCH_ROOT_QUEUE_ENTRY(USER_INTERACTIVE, 0,
.dq_label = "com.apple.root.user-interactive-qos",
.dq_serialnum = 14,
),
_DISPATCH_ROOT_QUEUE_ENTRY(USER_INTERACTIVE, DISPATCH_PRIORITY_FLAG_OVERCOMMIT,
.dq_label = "com.apple.root.user-interactive-qos.overcommit",
.dq_serialnum = 15,
),
};
  • 根据下标取对应的队列。
  • _dispatch_root_queues是一个静态变量集合,随时调用随时获取。
  • 在使用过程中都是使用dispatch_queue_t接收。
  • DISPATCH_GLOBAL_OBJECT_HEADER(queue_global)传递的参数是queue_global

小结:
主队列(dispatch_get_main_queue()

  • 专⻔用来在主线程上调度任务的串行队列。
  • 不会开启线程。
  • 如果当前主线程正在有任务执行,那么无论主队列中当前被添加了什么任务,都不会被调度。

全局并发队列

  • 为了方便使用,提供了全局队列 dispatch_get_global_queue(0, 0)
  • 全局队列是一个并发队列。
  • 在使用多线程开发时,如果对队列没有特殊需求,在执行异步任务时,可以直接使用全局队列。

四、dispatch_queue_create

普通的串行队列以及并发队列是通过dispatch_queue_create创建的,它的实现如下

dispatch_queue_t
dispatch_queue_create(const char *label, dispatch_queue_attr_t attr)
{
return _dispatch_lane_create_with_target(label, attr,
DISPATCH_TARGET_QUEUE_DEFAULT, true);
}

返回dispatch_queue_t类型,内部直接调用_dispatch_lane_create_with_target

4.1 _dispatch_lane_create_with_target


  • _dispatch_queue_attr_to_info根据dqa(串行/并行)创建dqai
  • 返回的dqai进行优先级相关的处理,进行准备工作。
  • 初始化队列
    • 根据dqai_concurrent串行并行获取vtable(class name)。参数是queue_concurrent或者queue_serial
    • _dispatch_object_alloc开辟空间。
    • _dispatch_queue_init初始化,根据dqai_concurrent传递DISPATCH_QUEUE_WIDTH_MAX 或者 1区分是串行还是并行。
    • label赋值。
    • 优先级设置。
  • _dispatch_trace_queue_create追踪返回_dq,重点在dq

这里创建queue并不是通过queue相关对象进行创建和接收。

4.2 _dispatch_queue_attr_to_info


  • 串行队列(dqa 为空)直接返回。
  • 并行队列进行一系列配置。

4.3 _dispatch_object_alloc

_dispatch_object_alloc传递的参数是vtable以及sizeof(struct dispatch_lane_s),那么意味着实际上也是dispatch_lane_s类型。


void *
_dispatch_object_alloc(const void *vtable, size_t size)
{
#if OS_OBJECT_HAVE_OBJC1
const struct dispatch_object_vtable_s *_vtable = vtable;
dispatch_object_t dou;
dou._os_obj = _os_object_alloc_realized(_vtable->_os_obj_objc_isa, size);
dou._do->do_vtable = vtable;
return dou._do;
#else
return _os_object_alloc_realized(vtable, size);
#endif
}
  • 调用_os_object_alloc_realized申请开辟内存。

4.3.1 _os_object_alloc_realized

inline _os_object_t
_os_object_alloc_realized(const void *cls, size_t size)
{
_os_object_t obj;
dispatch_assert(size >= sizeof(struct _os_object_s));
while (unlikely(!(obj = calloc(1u, size)))) {
_dispatch_temporary_resource_shortage();
}
obj->os_obj_isa = cls;
return obj;
}

内部直接调用calloc开辟空间。

4.4 _dispatch_queue_init

根据是否并行队列传递DISPATCH_QUEUE_WIDTH_MAX1

#define DISPATCH_QUEUE_WIDTH_FULL           0x1000ull
#define DISPATCH_QUEUE_WIDTH_POOL (DISPATCH_QUEUE_WIDTH_FULL - 1)
#define DISPATCH_QUEUE_WIDTH_MAX (DISPATCH_QUEUE_WIDTH_FULL - 2)


  • 根据width配置状态。
  • 根据width配置队列串行/并行以及dq_serialnum标识。dq_serialnum通过_dispatch_queue_serial_numbers赋值。
unsigned long volatile _dispatch_queue_serial_numbers =
DISPATCH_QUEUE_SERIAL_NUMBER_INIT;
// skip zero
// 1 - main_q
// 2 - mgr_q
// 3 - mgr_root_q
// 4,5,6,7,8,9,10,11,12,13,14,15 - global queues
// 17 - workloop_fallback_q
// we use 'xadd' on Intel, so the initial value == next assigned
#define DISPATCH_QUEUE_SERIAL_NUMBER_INIT 17
extern unsigned long volatile _dispatch_queue_serial_numbers;
  • 0跳过。
  • 1主队列。
  • 2管理队列。
  • 3管理队列的目标队列。
  • 4~15全局队列。根据qos优先级指定不同队列。
  • 17自动创建相关返回队列。

os_atomic_inc_orig传递的参数是_dispatch_queue_serial_numbersp)以及relaxedm):


#define os_atomic_inc_orig(p, m) \
os_atomic_add_orig((p), 1, m)

#define os_atomic_add_orig(p, v, m) \
_os_atomic_c11_op_orig((p), (v), m, add, +)

//##是连接符号,编译后会被去掉
#define _os_atomic_c11_op_orig(p, v, m, o, op) \
atomic_fetch_##o##_explicit(_os_atomic_c11_atomic(p), v, \
memory_order_##m)
  • 最终调用的是atomic_fetch_add_explicit(_os_atomic_c11_atomic(17), 1, memory_order_relaxed)。原子替换(p + 1 -> p), 并返回p之前的值。也就相当于是i++这是原子操作。
  • 这里多层宏定义处理是为了兼容不同的c/c++版本。

  • C atomic_fetch_add_explicit(volatile A * obj,M arg,memory_order order);

    enum memory_order {
    memory_order_relaxed, //不对执行顺序做保证,只保证此操作是原子的
    memory_order_consume, // 本线程中,所有后续的有关本原子类型的操作,必须>在本条原子操作完成之后执行
    memory_order_acquire, //本线程中,所有后续的读操作必须在本条原子操作完>成后执行
    memory_order_release, //本线程中,所有之前的写操作完成后才能执行本条原子操作
    memory_order_acq_rel, //同时包含 memory_order_acquire 和 memory_order_release
    memory_order_seq_cst //全部存取都按顺序执行
    };

    4.5 _dispatch_trace_queue_create


    #define _dispatch_trace_queue_create _dispatch_introspection_queue_create

    dispatch_queue_class_t
    _dispatch_introspection_queue_create(dispatch_queue_t dq)
    {
    dispatch_queue_introspection_context_t dqic;
    size_t sz = sizeof(struct dispatch_queue_introspection_context_s);

    if (!_dispatch_introspection.debug_queue_inversions) {
    sz = offsetof(struct dispatch_queue_introspection_context_s,
    __dqic_no_queue_inversion);
    }
    dqic = _dispatch_calloc(1, sz);
    dqic->dqic_queue._dq = dq;
    if (_dispatch_introspection.debug_queue_inversions) {
    LIST_INIT(&dqic->dqic_order_top_head);
    LIST_INIT(&dqic->dqic_order_bottom_head);
    }
    dq->do_introspection_ctxt = dqic;

    _dispatch_unfair_lock_lock(&_dispatch_introspection.queues_lock);
    LIST_INSERT_HEAD(&_dispatch_introspection.queues, dqic, dqic_list);
    _dispatch_unfair_lock_unlock(&_dispatch_introspection.queues_lock);

    DISPATCH_INTROSPECTION_INTERPOSABLE_HOOK_CALLOUT(queue_create, dq);
    if (DISPATCH_INTROSPECTION_HOOK_ENABLED(queue_create)) {
    _dispatch_introspection_queue_create_hook(dq);
    }
    return upcast(dq)._dqu;
    }

    五、dispatch_queue_t

    由于所有的队列都是通过dispatch_queue_t来接收的,直接研究dispatch_queue_t是一个不错的入口。
    dispatch_queue_t的点击会跳转到:


    DISPATCH_DECL(dispatch_queue);

    5.1 DISPATCH_DECL 源码分析

    DISPATCH_DECL宏定义:

    #define DISPATCH_DECL(name) OS_OBJECT_DECL_SUBCLASS(name, dispatch_object)
    真实类型是OS_OBJECT_DECL_SUBCLASS,在源码中它的定义有两个,一个是针对objc的,定义如下:
    #define OS_OBJECT_DECL_SUBCLASS(name, super) \
    OS_OBJECT_DECL_IMPL(name, NSObject, <OS_OBJECT_CLASS(super)>)

    OS_OBJECT_DECL_IMPL定义如下:

    #define OS_OBJECT_DECL_IMPL(name, adhere, ...) \
    OS_OBJECT_DECL_PROTOCOL(name, __VA_ARGS__) \
    typedef adhere<OS_OBJECT_CLASS(name)> \
    * OS_OBJC_INDEPENDENT_CLASS name##_t

    OS_OBJECT_DECL_PROTOCOL定义如下:

    #define OS_OBJECT_DECL_PROTOCOL(name, ...) \
    @protocol OS_OBJECT_CLASS(name) __VA_ARGS__ \
    @end
    • 也就是定义了一个@protocol

    OS_OBJC_INDEPENDENT_CLASS定义如下:

    #if __has_attribute(objc_independent_class)
    #define OS_OBJC_INDEPENDENT_CLASS __attribute__((objc_independent_class))
    #endif // __has_attribute(objc_independent_class)
    #ifndef OS_OBJC_INDEPENDENT_CLASS
    #define OS_OBJC_INDEPENDENT_CLASS
    #endif

    为了方便分析,这里假设它走的是OS_OBJC_INDEPENDENT_CLASS为空。

    OS_OBJECT_CLASS定义如下:

    #define OS_OBJECT_CLASS(name) OS_##name
    • 本质上是OS_拼接。

    在整个宏定义中参数 name: dispatch_queue super: dispatch_object,整个宏定义替换完成后如下:

    @protocol OS_dispatch_queue <OS_dispatch_object>
    @end
    typedef NSObject<OS_dispatch_queue> *dispatch_queue_t

    当然在源码中搜索#define DISPATCH_DECL可以搜到多个,其中有一个定义如下:

    #define DISPATCH_DECL(name) \
    typedef struct name##_s : public dispatch_object_s {} *name##_t

    替换后如下:

    typedef struct dispatch_queue_s : public dispatch_object_s {} *dispatch_queue_t
    • dispatch_queue_t是一个结构体来自于dispatch_queue_s
    • dispatch_queue_s继承自dispatch_object_s
    • dispatch_queue_t -> dispatch_queue_s -> dispatch_object_s类似于class -> objc_class -> objc_object
    • 本质上dispatch_queue_tdispatch_queue_s结构体类型。

    5.2 dispatch_queue_s 分析

    要研究dispatch_queue_t那么就要研究它的类型dispatch_queue_s
    dispatch_queue_s定义如下:


    struct dispatch_queue_s {
    DISPATCH_QUEUE_CLASS_HEADER(queue, void *__dq_opaque1);
    /* 32bit hole on LP64 */
    } DISPATCH_ATOMIC64_ALIGN;


    内部继承于_DISPATCH_QUEUE_CLASS_HEADER


    _DISPATCH_QUEUE_CLASS_HEADER内部又继承自DISPATCH_OBJECT_HEADER


    • 这里就对接到了dispatch_object_s类型。

    DISPATCH_OBJECT_HEADER内部又使用_DISPATCH_OBJECT_HEADER

    #define _DISPATCH_OBJECT_HEADER(x) \
    struct _os_object_s _as_os_obj[0]; \
    OS_OBJECT_STRUCT_HEADER(dispatch_##x); \
    struct dispatch_##x##_s *volatile do_next; \
    struct dispatch_queue_s *do_targetq; \
    void *do_ctxt; \
    union { \
    dispatch_function_t DISPATCH_FUNCTION_POINTER do_finalizer; \
    void *do_introspection_ctxt; \
    }

    也就是最终使用的是_os_object_s类型。

    OS_OBJECT_STRUCT_HEADER类型:

    #define OS_OBJECT_STRUCT_HEADER(x) \
    _OS_OBJECT_HEADER(\
    const struct x##_vtable_s *__ptrauth_objc_isa_pointer do_vtable, \
    do_ref_cnt, \
    do_xref_cnt)
    #endif

    _OS_OBJECT_HEADER3个成员变量:

    #define _OS_OBJECT_HEADER(isa, ref_cnt, xref_cnt) \
    isa; /* must be pointer-sized and use __ptrauth_objc_isa_pointer */ \
    int volatile ref_cnt; \
    int volatile xref_cnt

    至此整个继承链为:dispatch_queue_t -> dispatch_queue_s -> dispatch_object_s -> _os_object_s

    5.3 dispatch_object_s 分析



    • dispatch_object_t是一个联合体,那么意味着它可以是里面数据类型的任意一种。
    • 其中有dispatch_object_s类型,那么意味者它底层实际上是dispatch_object_t类型。
    • _os_object_s也与上面的分析相对应。

    总结: 整个继承链为:dispatch_queue_t -> dispatch_queue_s -> dispatch_object_s -> _os_object_s -> dispatch_object_t



    作者:HotPotCat
    链接:https://www.jianshu.com/p/1b2202ecb964
    收起阅读 »

    iOS LLDB(Low Lever Debug)

    一、概述LLDB(Low Lever Debug这里的low指轻量级)默认内置于Xcode中的动态调试工具。标准的 LLDB 提供了一组广泛的命令,旨在与老版本的 GDB 命令兼容。 除了使用标准配置外,还可以很容易地自...
    继续阅读 »

    一、概述

    LLDB(Low Lever Debug这里的low指轻量级)默认内置于Xcode中的动态调试工具。标准的 LLDB 提供了一组广泛的命令,旨在与老版本的 GDB 命令兼容。 除了使用标准配置外,还可以很容易地自定义 LLDB 以满足实际需要。

    二、LLDB语法

    <command> [<subcommand> [<subcommand>...]] <action> [-options [option-value]] [argument [argument...]]
    • command:命令
    • subcommand:子命令
    • action:执行命令的操作
    • options:命令选项
    • arguement:参数
    • []:表示可选

    例子:

    //command     action    option    arguement
    breakpoint set -n test1
    唯一匹配原则:根据n个字母已经能唯一匹配到某个命令,则只写n个字母等效于完整的命令(大小写敏感)。也就是说只要能识别出来命令唯一就可以:

    br s -n test1

    help

    直接在LLDB中输入help可以查看所有的LLDB命令。
    查看某一个命令help /help :

    help breakpoint
    help breakpoint set

    apropos

    apropos可以用来搜索命令相关信息。

    //将所有breakpoint命令搜索出来
    apropos breakpoint

    三、lldb常用命令

    3.1 lldb断点设置

    3.1.1 breakpoint

    breakpoint set

    • set 是子命令
    • -n 是选项--name 的缩写。

    根据方法名设置断点
    breakpoint set -n test1,相当于对符号test1下断点,所有的test1都会被断住:

    Breakpoint 4: where = LLDBTest`test1 at ViewController.m:17, address = 0x000000010be80d00

    这和在Xcode中设置符号断点是一样的:



    区别是前者重新启动后就失效了。

    设置组断点
    breakpoint set -n "[ViewController click1:]" -n "[ViewController click2:]" -n "[ViewController click3:]"相当于下了一组断点:

    (lldb) breakpoint set -n "[ViewController click1:]" -n "[ViewController click2:]" -n "[ViewController click3:]"
    Breakpoint 6: 3 locations.
    (lldb) breakpoint list 6
    6: names = {'[ViewController click1:]', '[ViewController click1:]', '[ViewController click1:]', '[ViewController click2:]', '[ViewController click2:]', '[ViewController click2:]', '[ViewController click3:]', '[ViewController click3:]', '[ViewController click3:]'}, locations = 3, resolved = 3, hit count = 0
    6.1: where = LLDBTest`-[ViewController click1:] at ViewController.m:31, address = 0x000000010be80e00, resolved, hit count = 0
    6.2: where = LLDBTest`
    -[ViewController click2:] at ViewController.m:35, address = 0x000000010be80e40, resolved, hit count = 0
    6.3: where = LLDBTest`-[ViewController click3:] at ViewController.m:40, address = 0x000000010be80e80, resolved, hit count = 0

    可以同时启用和禁用组断点。

    使用-f指定文件

    (lldb) br set -f ViewController.m -n click1:
    Breakpoint 12: where = LLDBTest`-[ViewController click1:] at ViewController.m:31, address = 0x000000010be80e00

    使用-l指定某一行设置断点

    (lldb) br set -f ViewController.m -l 40
    Breakpoint 13: where = LLDBTest`-[ViewController click3:] at ViewController.m:40, address = 0x000000010be80e80
    使用-c设置条件断点
    只要计算的结果是个bool型或整型数值就可以。test2:方法接收一个布尔值参数,则当传入的值为YES时才断住:

    (lldb) br set -n test2: -c enable==YES
    Breakpoint 1: where = LLDBTest`-[ViewController test2:] at ViewController.m:45, address = 0x0000000100dc1e80

    使用-F设置函数全名

    (lldb) br set -F test1
    Breakpoint 1: where = LLDBTest`test1 at ViewController.m:17, address = 0x000000010762ecb0

    使用-a设置地址断点

    (lldb)  br set -a 0x000000010762ecb0
    Breakpoint 2: where = LLDBTest`test1 at ViewController.m:17, address = 0x000000010762ecb0

    使用--selector设置断点

    (lldb) br set -n touchesBegan:withEvent:
    Breakpoint 2: 97 locations.
    (lldb) br set --selector touchesBegan:withEvent:
    Breakpoint 3: 97 locations.

    --selector在这里和-n等价都是全部匹配。不过-n是针对符号,--selector针对OC的方法。

    使用-r模糊匹配

    (lldb) br set -f ViewController.m -r test
    Breakpoint 4: 2 locations.
    (lldb) br list
    Current breakpoints:
    4: regex = 'test', locations = 2, resolved = 2, hit count = 0
    4.1: where = LLDBTest`test1 at ViewController.m:17, address = 0x000000010762ecb0, resolved, hit count = 0
    4.2: where = LLDBTest`-[ViewController test2:] at ViewController.m:45, address = 0x000000010762ee80, resolved, hit count = 0

    使用-i设置忽略次数

    (lldb) br set -f ViewController.m -r test -i 3
    Breakpoint 1: 2 locations.

    这里的次数是这组所有断点加起来的次数。

    breakpoint list

    查看断点列表:

    (lldb) br l
    Current breakpoints:
    7: names = {'[ViewController click1:]', '[ViewController click1:]', '[ViewController click1:]', '[ViewController click2:]', '[ViewController click2:]', '[ViewController click2:]', '[ViewController click3:]', '[ViewController click3:]', '[ViewController click3:]'}, locations = 3, resolved = 3, hit count = 0
    7.1: where = LLDBTest`-[ViewController click1:] at ViewController.m:31, address = 0x000000010be80e00, resolved, hit count = 0
    7.2: where = LLDBTest`-[ViewController click2:] at ViewController.m:35, address = 0x000000010be80e40, resolved, hit count = 0
    7.3: where = LLDBTest`-[ViewController click3:] at ViewController.m:40, address = 0x000000010be80e80, resolved, hit count = 0

    查看某一个/某一组断点:

    (lldb) br l
    Current breakpoints:
    7: names = {'[ViewController click1:]', '[ViewController click1:]', '[ViewController click1:]', '[ViewController click2:]', '[ViewController click2:]', '[ViewController click2:]', '[ViewController click3:]', '[ViewController click3:]', '[ViewController click3:]'}, locations = 3, resolved = 3, hit count = 0
    7.1: where = LLDBTest`-[ViewController click1:] at ViewController.m:31, address = 0x000000010be80e00, resolved, hit count = 0
    7.2: where = LLDBTest`-[ViewController click2:] at ViewController.m:35, address = 0x000000010be80e40, resolved, hit count = 0
    7.3: where = LLDBTest`-[ViewController click3:] at ViewController.m:40, address = 0x000000010be80e80, resolved, hit count = 0

    8: name = 'test1', locations = 1, resolved = 1, hit count = 0
    8.1: where = LLDBTest`test1 at ViewController.m:17, address = 0x000000010be80d00, resolved, hit count = 0

    (lldb) br l 8
    8: name = 'test1', locations = 1, resolved = 1, hit count = 0
    8.1: where = LLDBTest`test1 at ViewController.m:17, address = 0x000000010be80d00, resolved, hit count = 0

    (lldb) br l 7
    7: names = {'[ViewController click1:]', '[ViewController click1:]', '[ViewController click1:]', '[ViewController click2:]', '[ViewController click2:]', '[ViewController click2:]', '[ViewController click3:]', '[ViewController click3:]', '[ViewController click3:]'}, locations = 3, resolved = 3, hit count = 0
    7.1: where = LLDBTest`-[ViewController click1:] at ViewController.m:31, address = 0x000000010be80e00, resolved, hit count = 0
    7.2: where = LLDBTest`-[ViewController click2:] at ViewController.m:35, address = 0x000000010be80e40, resolved, hit count = 0
    7.3: where = LLDBTest`-[ViewController click3:] at ViewController.m:40, address = 0x000000010be80e80, resolved, hit count = 0

    breakpoint disable/enable/delete

    breakpoint disable

    (lldb) br dis 7.1
    1 breakpoints disabled.


    breakpoint enable

    (lldb) br en 7.1
    1 breakpoints enabled.

    breakpoint delete
    只能删除指定组断点,不能删除组里面的某一个。

    (lldb) br del 7.2
    0 breakpoints deleted; 1 breakpoint locations disabled.
    (lldb) br del 7
    1 breakpoints deleted; 0 breakpoint locations disabled.
    breakpoint delete
    只能删除指定组断点,不能删除组里面的某一个。
    (lldb) br del 7.2
    0 breakpoints deleted; 1 breakpoint locations disabled.
    (lldb) br del 7
    1 breakpoints deleted; 0 breakpoint locations disabled.

    breakpoint delete删除所有断点


    (lldb) breakpoint delete
    About to delete all breakpoints, do you want to do that?: [Y/n] y
    All breakpoints removed. (3 breakpoints)

    ⚠️breakpoint组在一次运行过程中是一直递增的。多次添加断点只会断住一次。

    3.1.2 watchpoint 内存断点/地址断点

    breakpoint是对方法生效的断点,watchpoint是对地址生效的断点。如果地址里中的数据改变了,就让程序中断。

    watchpoint set

    watchpoint set variable


    (lldb) watchpoint set variable item1->_name
    Watchpoint created: Watchpoint 1: addr = 0x600002ce8868 size = 8 state = enabled type = w
    declare @ '/Users/zaizai/LLDBTest/LLDBTest/ViewController.m:28'
    watchpoint spec = 'item1->_name'
    new value: 0x000000010ad37038

    改变name值的时候就会断住(即使值没有变):

    Watchpoint 1 hit:
    old value: 0x0000000109319038
    new value: 0x0000000109319038

    watchpoint set variable传入的是变量名,不接受方法。所以不能使用watchpoint set variable item1.name

    watchpoint set expression

    (lldb) p item1->_name
    (__NSCFConstantString *) $0 = 0x000000010a0d0038 @"1"
    (lldb) p &item1->_name
    (NSString **) $1 = 0x00006000033bec48
    (lldb) watchpoint set expression 0x00006000033bec48
    Watchpoint created: Watchpoint 1: addr = 0x6000033bec48 size = 8 state = enabled type = w
    new value: 4463591480

    breakpoint类似,watchpoint也有watchpoint listwatchpoint disablewatchpoint enablewatchpoint delete

    3.2 lldb代码执行 expression、p、print、call、po

    expression执行一个表达式,并将表达式返回的结果输出。


    expression <cmd-options> -- <expr>
    • cmd-options:命令选项,一般使用默认。
    • --:命令选项结束符。
    • expr:表达式。

    pprintcall 都是expression --的别名:

    • print: 打印某个东西,可以是变量/表达式,pprint的缩写。
    • call: 调用某个方法。

    poexpression -O --的别名。调用的是description或者debugDescription方法。

    进制转换p/x、p/o、p/t
    p除了打印还有常量的进制转换功能。


    //默认10进制打印
    (lldb) p 100
    (int) $0 = 100
    //16进制打印
    (lldb) p/x 100
    (int) $1 = 0x00000064
    //8进制打印
    (lldb) p/o 100
    (int) $2 = 0144
    //2进制打印
    (lldb) p/t 100
    (int) $3 = 0b00000000000000000000000001100100
    //字符串转换为10进制
    (lldb) p/d 'A'
    (char) $4 = 65
    //10进制转换为字符
    (lldb) p/c 65
    (int) $5 = A\0\0\0

    浮点数转换

    (lldb) p/x (double) 180.0
    (double) $6 = 0x4066800000000000

    (lldb) p/f 0x4066800000000000
    (long) $1 = 180

    (lldb) e -f f -- 0x4066800000000000
    (long) $2 = 180

    x/nuf


    (lldb) x self
    0x600002c12180: 29 8a 00 00 01 80 1d 00 00 00 00 00 00 00 00 00 )...............
    0x600002c12190: 0e 00 00 00 00 00 00 00 00 5e 75 01 00 60 00 00 .........^u..`..
    (lldb) x/4gx self
    0x600002c12180: 0x001d800100008a29 0x0000000000000000
    0x600002c12190: 0x000000000000000e 0x0000600001755e00
    (lldb) x/4gw self
    0x600002c12180: 0x00008a29 0x001d8001 0x00000000 0x00000000

    x/nuf
    x就是 memory read 内存读取。

    • nn表示要打印的内存单元的个数。
    • uu表示一个地址单元的长度。iOS是小端模式。
      • b表示单字节
      • h表示双字节
      • w表示四字节
      • g表示八字节。
    • ff表示显示方式。
      • x按十六进制格式显示变量
      • d按十进制显示
      • u按十进制显示无符号整形
      • o按八进制显示变量
      • t按二进制显示变量
      • a按十六进制显示变量
      • i指令地址格式
      • c按字符格式显示变量
      • f按浮点数格式显示变量
    • addr:地址/数据。

    3.3查看堆栈信息

    bt(thread backtrace)

     thread #1, queue = 'com.apple.main-thread', stop reason = step over
    * frame #0: 0x000000010e60fa06 LLDBTest`-[ViewController touchesBegan:withEvent:](self=0x00007fce43806030, _cmd="touchesBegan:withEvent:", touches=1 element, event=0x0000600002168540) at ViewController.m:46:23
    frame #1: 0x00007fff246ca70f UIKitCore`forwardTouchMethod + 321
    frame #2: 0x00007fff246ca5bd UIKitCore`-[UIResponder touchesBegan:withEvent:] + 49
    frame #3: 0x00007fff246d95b5 UIKitCore`-[UIWindow _sendTouchesForEvent:] + 622
    frame #4: 0x00007fff246db6c7 UIKitCore`-[UIWindow sendEvent:] + 4774
    frame #5: 0x00007fff246b5466 UIKitCore`-[UIApplication sendEvent:] + 633
    frame #6: 0x00007fff24745f04 UIKitCore`__processEventQueue + 13895
    frame #7: 0x00007fff2473c877 UIKitCore`__eventFetcherSourceCallback + 104
    frame #8: 0x00007fff2039038a CoreFoundation`__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 17
    frame #9: 0x00007fff20390282 CoreFoundation`__CFRunLoopDoSource0 + 180
    frame #10: 0x00007fff2038f764 CoreFoundation`__CFRunLoopDoSources0 + 248
    frame #11: 0x00007fff20389f2f CoreFoundation`__CFRunLoopRun + 878
    frame #12: 0x00007fff203896d6 CoreFoundation`CFRunLoopRunSpecific + 567
    frame #13: 0x00007fff2c257db3 GraphicsServices`GSEventRunModal + 139
    frame #14: 0x00007fff24696cf7 UIKitCore`-[UIApplication _run] + 912
    frame #15: 0x00007fff2469bba8 UIKitCore`UIApplicationMain + 101
    frame #16: 0x000000010e60fff2 LLDBTest`main(argc=1, argv=0x00007ffee15efd20) at main.m:17:12
    frame #17: 0x00007fff2025a3e9 libdyld.dylib`start + 1

    up、down、frame select

    (lldb) up
    frame #3: 0x00007fff246d95b5 UIKitCore`-[UIWindow _sendTouchesForEvent:] + 622
    UIKitCore`-[UIWindow _sendTouchesForEvent:]:
    -> 0x7fff246d95b5 <+622>: lea rax, [rip + 0x628a699c] ; UIApp
    0x7fff246d95bc <+629>: mov rdi, qword ptr [rax]
    0x7fff246d95bf <+632>: mov rsi, qword ptr [rbp - 0x170]
    0x7fff246d95c6 <+639>: mov rdx, r12
    0x7fff246d95c9 <+642>: mov rcx, rbx
    0x7fff246d95cc <+645>: mov r8, r14
    0x7fff246d95cf <+648>: call r13
    0x7fff246d95d2 <+651>: mov ecx, 0x1
    (lldb) down
    frame #2: 0x00007fff246ca5bd UIKitCore`-[UIResponder touchesBegan:withEvent:] + 49
    UIKitCore`-[UIResponder touchesBegan:withEvent:]:
    -> 0x7fff246ca5bd <+49>: mov rdi, rbx
    0x7fff246ca5c0 <+52>: pop rbx
    0x7fff246ca5c1 <+53>: pop r12
    0x7fff246ca5c3 <+55>: pop r14
    0x7fff246ca5c5 <+57>: pop r15
    0x7fff246ca5c7 <+59>: pop rbp
    0x7fff246ca5c8 <+60>: jmp qword ptr [rip + 0x5bf063e2] ; (void *)0x00007fff2018f760: objc_release

    UIKitCore`forwardTouchMethod:
    0x7fff246ca5ce <+0>: push rbp
    (lldb) frame select 10
    frame #10: 0x00007fff2038f764 CoreFoundation`__CFRunLoopDoSources0 + 248
    CoreFoundation`__CFRunLoopDoSources0:
    -> 0x7fff2038f764 <+248>: mov r13d, eax
    0x7fff2038f767 <+251>: jmp 0x7fff2038f7dc ; <+368>
    0x7fff2038f769 <+253>: xor r13d, r13d
    0x7fff2038f76c <+256>: jmp 0x7fff2038f802 ; <+406>
    0x7fff2038f771 <+261>: mov rbx, r14
    0x7fff2038f774 <+264>: mov rdi, qword ptr [rbp - 0x38]
    0x7fff2038f778 <+268>: call 0x7fff20312bcc ; CFArrayGetCount
    0x7fff2038f77d <+273>: mov r15, rax

    这3个命令只是方便我们查看堆栈信息,寄存器还是在断点处。

    frame variable
    查看当前frame参数

    (lldb) frame variable
    (ViewController *) self = 0x00007f8c59405600
    (SEL) _cmd = "touchesBegan:withEvent:"
    (BOOL) enable = NO

    在已经执行过的frame中修改参数不会影响后面的结果。

    thread return

    thread return可以接受一个表达式,调用命令之后直接从当前的frame返回表达式的值。直接返回不执行后面的代码。相当于回滚(相当于直接到bl跳转的下一行汇编代码)。当然修改pc寄存器的值也能达到相同的效果。

    3.4 command指令

    给断点添加命令的命令。
    breakpoint command add


    (lldb) b test2:
    Breakpoint 1: where = LLDBTest`-[ViewController test2:] at ViewController.m:65, address = 0x0000000103e7db40
    (lldb) br command add 1
    Enter your debugger command(s). Type 'DONE' to end.
    > frame variable
    > DONE

    当断点断住的时候执行frame variable指令。
    当然也可以只添加一条指令:


    br command add -o "po self" 1

    多次对同一个断点添加命令,后面命令会将前面命令覆盖


    breakpoint command list
    查看某个断点已有的命令(list 后必须有断点编号)

    (lldb) breakpoint command list 1
    Breakpoint 1:
    Breakpoint commands:
    po self


    3.5 target stop-Hook指令

    target stop-hook命令可以在每次stop的时候去执行一些命令


    (lldb) target stop-hook add -o "frame variable"
    Stop hook #1 added.
    command不同的是它对所有断点生效。相当于对程序下钩子。
    display命令等价:

    (lldb) display frame variable
    Stop hook #2 added.
    target stop-hook只对breakpointwatchpointstop生效,直接点击Xcode上的pause或者debug view hierarchy不会生效

    target stop-hook list


    (lldb) target stop-hook list
    Hook: 1
    State: enabled
    Commands:
    frame variable

    Hook: 2
    State: enabled
    Commands:
    expr -- frame variable

    target stop-hook disable / enable
    暂时让某个stop-hook失效/生效,不传id则代表全部。

    target stop-hook delete / undisplay
    删除stop-hook


    (lldb) target stop-hook delete
    Delete all stop hooks?: [Y/n] y

    delete可以不传idundisplay必须传id


    3.6 image(target modules)指令

    image lookup --address

    查找某个地址具体对应的文件位置,可以使用image lookup --address(image lookup -a)
    比如有一个crash:

    2021-05-19 18:19:45.833183+0800 LLDBTest[41719:24239029] *** Terminating app due to uncaught exception 'NSRangeException', reason: '*** -[__NSArrayM objectAtIndexedSubscript:]: index 5 beyond bounds [0 .. 2]'
    *** First throw call stack:
    (
    0 CoreFoundation 0x00007fff20421af6 __exceptionPreprocess + 242
    1 libobjc.A.dylib 0x00007fff20177e78 objc_exception_throw + 48
    2 CoreFoundation 0x00007fff2049e77f _CFThrowFormattedException + 194
    3 CoreFoundation 0x00007fff20320825 -[__NSArrayM removeAllObjects] + 0
    4 LLDBTest 0x0000000107c469f7 -[ViewController touchesBegan:withEvent:] + 151

    从上面的堆栈可以看到是-[ViewController touchesBegan:withEvent:]中的调用发生了crash,但是并不知道在ViewController.m的哪一行。使用image lookup -a就可以具体定位到确定的行数:


    (lldb) image lookup -a 0x0000000107c469f7
    Address: LLDBTest[0x00000001000019f7] (LLDBTest.__TEXT.__text + 807)
    Summary: LLDBTest`-[ViewController touchesBegan:withEvent:] + 151 at ViewController.m:48:28

    image lookup --name

    查找方法或者符号的信息可以使用image lookup --nameimage lookup -n):

    (lldb) image lookup -n test2:
    1 match found in /Users/zaizai/Library/Developer/Xcode/DerivedData/LLDBTest-enxwhkxlnnynraafdlfrcoxaibzm/Build/Products/Debug-iphonesimulator/LLDBTest.app/LLDBTest:
    Address: LLDBTest[0x0000000100001b30] (LLDBTest.__TEXT.__text + 1120)
    Summary: LLDBTest`-[ViewController test2:] at ViewController.m:65

    image lookup --type

    可以使用image lookup --type(image lookup -t)查看类型:

    (lldb) image lookup -t ViewController
    Best match found in /Users/zaizai/Library/Developer/Xcode/DerivedData/LLDBTest-enxwhkxlnnynraafdlfrcoxaibzm/Build/Products/Debug-iphonesimulator/LLDBTest.app/LLDBTest:
    id = {0x100000033}, name = "ViewController", byte-size = 16, decl = ViewController.h:10, compiler_type = "@interface ViewController : UIViewController{
    NSMutableArray * _items;
    }
    @property(nonatomic, readwrite, getter = items, setter = setItems:) NSMutableArray *items;
    @end"


    作者:HotPotCat
    链接:https://www.jianshu.com/p/5f435b1faf4b
    收起阅读 »

    IO系列 字节、字符流|Java基础

    字节缓冲流介绍BufferOutputStream:该类实现缓冲输出流。 通过设置这样的输出流,应用程序可以向底层输出流写入字节,而不必为写入的每个字节导致底层系统的调用BufferedInputStream:创建BufferedInputStream将创建一...
    继续阅读 »

    字节缓冲流介绍

    • BufferOutputStream:该类实现缓冲输出流。 通过设置这样的输出流,应用程序可以向底层输出流写入字节,而不必为写入的每个字节导致底层系统的调用

    • BufferedInputStream:创建BufferedInputStream将创建一个内部缓冲区数组。 当从流中读取或跳过字节时,内部缓冲区将根据需要从所包含的输入流中重新填充,一次多个

    构造方法

    方法名说明
    BufferedOutputStream(OutputStream out)创建字节缓冲输出流对象
    BufferedInputStream(InputStream in)创建字节缓冲输入流对象
    public class BufferStreamLearn1 {
    public static void main(String[] args) throws IOException {
    //字节缓冲输出流:BufferedOutputStream(OutputStream out)

    BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("File\shixf.txt"));
    //写数据
    bos.write("hello\r\n".getBytes());
    bos.write("world\r\n".getBytes());
    //释放资源
    bos.close();


    //字节缓冲输入流:BufferedInputStream(InputStream in)
    BufferedInputStream bis = new BufferedInputStream(new FileInputStream("File\shixf.txt"));

    //一次读取一个字节数据
    /*int by;
    while ((by=bis.read())!=-1) {
    System.out.print((char)by);
    }*/


    //一次读取一个字节数组数据
    byte[] bys = new byte[1024];
    int len;
    while ((len=bis.read(bys))!=-1) {
    System.out.print(new String(bys,0,len));
    }

    //释放资源
    bis.close();
    }
    }

    文件输出image.png

    控制台输出image.png

    字符流

    字符流介绍

    • 字符流的介绍

      由于字节流操作中文不是特别的方便,所以Java就提供字符流

      字符流 = 字节流 + 编码表

    • 中文的字节存储方式

      用字节流复制文本文件时,文本文件也会有中文,但是没有问题,原因是最终底层操作会自动进行字节拼接成中文,如何识别是中文的呢?

      汉字在存储的时候,无论选择哪种编码存储,第一个字节都是负数

    编码表

    • 什么是字符集

    是一个系统支持的所有字符的集合,包括各国家文字、标点符号、图形符号、数字等

    l计算机要准确的存储和识别各种字符集符号,就需要进行字符编码,一套字符集必然至少有一套字符编码。常见字符集有ASCII字符集、GBXXX字符集、Unicode字符集等

    • 常见的字符集

      • ASCII字符集:

        lASCII:是基于拉丁字母的一套电脑编码系统,用于显示现代英语,主要包括控制字符(回车键、退格、换行键等)和可显示字符(英文大小写字符、阿拉伯数字和西文符号)

        基本的ASCII字符集,使用7位表示一个字符,共128字符。ASCII的扩展字符集使用8位表示一个字符,共256字符,方便支持欧洲常用字符。是一个系统支持的所有字符的集合,包括各国家文字、标点符号、图形符号、数字等

      • GBXXX字符集:

        GBK:最常用的中文码表。是在GB2312标准基础上的扩展规范,使用了双字节编码方案,共收录了21003个汉字,完全兼容GB2312标准,同时支持繁体汉字以及日韩汉字等

      • Unicode字符集:

        UTF-8编码:可以用来表示Unicode标准中任意字符,它是电子邮件、网页及其他存储或传送文字的应用 中,优先采用的编码。互联网工程工作小组(IETF)要求所有互联网协议都必须支持UTF-8编码。它使用一至四个字节为每个字符编码

        编码规则:

        128个US-ASCII字符,只需一个字节编码

        拉丁文等字符,需要二个字节编码

        大部分常用字(含中文),使用三个字节编码

        其他极少使用的Unicode辅助字符,使用四字节编码

    字符串中的编码解码问题

    方法名说明
    byte[] getBytes()使用平台的默认字符集将该 String编码为一系列字节
    byte[] getBytes(String charsetName)使用指定的字符集将该 String编码为一系列字节
    String(byte[] bytes)使用平台的默认字符集解码指定的字节数组来创建字符串
    String(byte[] bytes, String charsetName)通过指定的字符集解码指定的字节数组来创建字符串
    public class StringLearn {
    public static void main(String[] args) throws UnsupportedEncodingException {
    //定义一个字符串
    String s = "中国";

    //byte[] bys = s.getBytes(); //[-28, -72, -83, -27, -101, -67]
    //byte[] bys = s.getBytes("UTF-8"); //[-28, -72, -83, -27, -101, -67]
    byte[] bys = s.getBytes("GBK"); //[-42, -48, -71, -6]
    System.out.println(Arrays.toString(bys));

    //String ss = new String(bys);
    //String ss = new String(bys,"UTF-8");
    String ss = new String(bys,"GBK");
    System.out.println(ss);
    }
    }

    image.png

    字符流中的编码解码

    • InputStreamReader:是从字节流到字符流的桥梁

      它读取字节,并使用指定的编码将其解码为字符

      它使用的字符集可以由名称指定,也可以被明确指定,或者可以接受平台的默认字符集

    • OutputStreamWriter:是从字符流到字节流的桥梁

      是从字符流到字节流的桥梁,使用指定的编码将写入的字符编码为字节

      它使用的字符集可以由名称指定,也可以被明确指定,或者可以接受平台的默认字符集

    方法名说明
    InputStreamReader(InputStream in)使用默认字符编码创建InputStreamReader对象
    InputStreamReader(InputStream in,String chatset)使用指定的字符编码创建InputStreamReader对象
    OutputStreamWriter(OutputStream out)使用默认字符编码创建OutputStreamWriter对象
    OutputStreamWriter(OutputStream out,String charset)使用指定的字符编码创建OutputStreamWriter对象
    public class ConversionStreamLearn {
    public static void main(String[] args) throws IOException {
    //OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream("myCharStream\osw.txt"));
    OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream("File\shixf.txt"),"GBK");
    osw.write("中国");
    osw.close();

    //InputStreamReader isr = new InputStreamReader(new FileInputStream("myCharStream\osw.txt"));
    try (InputStreamReader isr = new InputStreamReader(new FileInputStream("File\shixf.txt"), "GBK")) {
    //一次读取一个字符数据
    int ch;
    while ((ch = isr.read()) != -1) {
    System.out.print((char) ch);
    }
    isr.close();
    }
    }
    }

    文件输出image.png

    控制台输出image.png

    字符流写数据的5种方式

    方法名说明
    void write(int c)写一个字符
    void write(char[] cbuf)写入一个字符数组
    void write(char[] cbuf, int off, int len)写入字符数组的一部分
    void write(String str)写一个字符串
    void write(String str, int off, int len)写一个字符串的一部分
     
    方法名说明
    flush()刷新流,之后还可以继续写数据
    close()关闭流,释放资源,但是在关闭之前会先刷新流。一旦关闭,就不能再写数据
    public class OutputStreamWriterLearn {
    public static void main(String[] args) throws IOException {
    OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream("File\shixf.txt"));

    //void write(int c):写一个字符
    // osw.write(97);
    // osw.write(98);
    // osw.write(99);

    //void writ(char[] cbuf):写入一个字符数组
    char[] chs = {'a', 'b', 'c', 'd', 'e'};
    // osw.write(chs);

    //void write(char[] cbuf, int off, int len):写入字符数组的一部分
    // osw.write(chs, 0, chs.length);
    // osw.write(chs, 1, 3);

    //void write(String str):写一个字符串
    // osw.write("abcde");

    //void write(String str, int off, int len):写一个字符串的一部分
    // osw.write("abcde", 0, "abcde".length());
    osw.write("abcde", 1, 3);

    //释放资源
    osw.close();
    }
    }

    字符流读数据的2种方式

    方法名说明
    int read()一次读一个字符数据
    int read(char[] cbuf)一次读一个字符数组数据
    public class InputStreamReaderLearn {
    public static void main(String[] args) throws IOException {
    // shixf.txt -> {b,c,d}
    InputStreamReader isr = new InputStreamReader(new FileInputStream("File\shixf.txt"));

    //int read():一次读一个字符数据
    // int ch;
    // while ((ch=isr.read())!=-1) {
    // System.out.print((char)ch);
    // }

    //int read(char[] cbuf):一次读一个字符数组数据
    char[] chs = new char[1024];
    int len;
    while ((len = isr.read(chs)) != -1) {
    System.out.print(new String(chs, 0, len));
    }

    //释放资源
    isr.close();
    }
    }

    控制台输出

    image.png

    字符缓冲流

    字符缓冲流介绍

    • BufferedWriter:将文本写入字符输出流,缓冲字符,以提供单个字符,数组和字符串的高效写入,可以指定缓冲区大小,或者可以接受默认大小。默认值足够大,可用于大多数用途

    • BufferedReader:从字符输入流读取文本,缓冲字符,以提供字符,数组和行的高效读取,可以指定缓冲区大小,或者可以使用默认大小。 默认值足够大,可用于大多数用途

    方法名说明
    BufferedWriter(Writer out)创建字符缓冲输出流对象
    BufferedReader(Reader in)创建字符缓冲输入流对象
    public class BufferedStreamLearn {
    public static void main(String[] args) throws IOException {
    //BufferedWriter(Writer out)
    BufferedWriter bw = new BufferedWriter(new FileWriter("File\ShixfBW.txt"));
    bw.write("hello\r\n");
    bw.write("world\r\n");
    bw.close();

    //BufferedReader(Reader in)
    BufferedReader br = new BufferedReader(new FileReader("File\ShixfBW.txt"));

    //一次读取一个字符数据
    // int ch;
    // while ((ch=br.read())!=-1) {
    // System.out.print((char)ch);
    // }

    //一次读取一个字符数组数据
    char[] chs = new char[1024];
    int len;
    while ((len=br.read(chs))!=-1) {
    System.out.print(new String(chs,0,len));
    }

    br.close();
    }
    }

    文件输出image.png

    控制台输出image.png

    字符缓冲流特有功能

    BufferedWriter

    方法名说明
    void newLine()写一行行分隔符,行分隔符字符串由系统属性定义

    BufferedReader:

    方法名说明
    String readLine()读一行文字。 结果包含行的内容的字符串,不包括任何行终止字符如果流的结尾已经到达,则为null
    public class BufferedStreamLearn {
    public static void main(String[] args) throws IOException {

    //创建字符缓冲输出流
    BufferedWriter bw = new BufferedWriter(new FileWriter("File\ShixfBW.txt"));

    //写数据
    for (int i = 0; i < 10; i++) {
    bw.write("hello" + i);
    //bw.write("\r\n");
    bw.newLine();
    bw.flush();
    }

    //释放资源
    bw.close();

    //创建字符缓冲输入流
    BufferedReader br = new BufferedReader(new FileReader("File\ShixfBW.txt"));

    String line;
    while ((line=br.readLine())!=null) {
    System.out.println(line);
    }

    br.close();
    }
    }

    输出

    image.png

    小结

    image.pngimage.png


    收起阅读 »

    【JVM入门食用指南】JVM内存管理

    📑即将学会JVM的内存管理的相关知识点,JVM对内存管理进行了哪些规范Java从编译到执行.java文件经过javac编译成.class文件 .class文件通过类加载器(ClassLoader)加载到方法区 jvm执行引擎执行 把字节码翻译成机器码...
    继续阅读 »

    📑即将学会

    JVM的内存管理的相关知识点,JVM对内存管理进行了哪些规范

    Java从编译到执行

    image.png

    .java文件经过javac编译成.class文件 .class文件通过类加载器(ClassLoader)加载到方法区 jvm执行引擎执行 把字节码翻译成机器码

    解释执行与JIT执行

    解释执行

    JVM 是C++ 写的 需要通过C++ 解释器进行解释

    解释执行优缺点

    通过JVM解释 速度相对慢一些

    JIT (just-in-time compilation 即时编译)(hotspot)

    方法、一段代码 循环到一定次数 后端1万多 代码会走hotspot编译 JIT执行(hotspot)(JIT) java代码 直接翻译成(不经解释器) 汇编码 机器码

    JIT执行优缺点

    速度快 但是编译需要一定时间

    JVM是一种规范

    JVM两种特性 跨平台 语言无关性

    • 跨平台
      • 相同的代码在不同的平台有相同的执行效果
    • JVM语言无关性
      • 只识别.class文件 只要把相关的语言文件编译成.class文件 就可以通过JVM执行
        • 像groove、kotlin、java语言 本质上和语言没有关系,因此,只要符合JVM规范,就可以执行 语言层面上 只是将.java .kt等文件编译成.class文件

    因此 JVM是一种规范

    JVM 内存管理规范

    运行时数据区域

    Java虚拟机在执行Java程序的过程中会把它所管理的内存 划分为若干个不同的数据区域 数据划分 image.png 而数据划分这块 依据线程私有 和 线程共享这两种进行划分

    • 线程共享区
      • 线程共享区 分为方法区 和 堆
    • 方法区 (永久代(JDk1.7 前) 元空间(JDK1.8) hotspot实现下称呼 )
      • 在JVM规范中,统称为方法区
        • 只是hotSpot VM 这块产品用得比较多 。hotSpot利用永久代 或者 元空间 实现method区 Hotspot不同的版本实现而已

     几乎所有对象都会在这里分配

    线程私有区 每启动一个线程划分的一个区域

    直接内存 堆外内存

    • JVM会在运行时把管理的区域进行虚拟化 new 对象()

    通过JVM内存管理,将对象放入堆中,使用的时候只需要找到对象的引用 就可以直接使用,比直接通过分配内存,地址寻址 计算偏移量,偏移长度更方便。

    • 而这块数据没有经过内存虚拟化 (运行时外的内存 内存8G JVM 占用5G 堆外内存3G)

    可以通过某种方法 进行申请、使用、释放。不过比较麻烦,涉及分配内存、分配地址等

    java方法的运行与虚拟机栈

    虚拟机栈

    栈的结构 存储当前线程运行Java方法所需要的数据、指令、返回地址

    public static void main(String[] args) {
    A();
    }

    private static void A() {
    B();
    }

    private static void B() {
    C();
    }

    private static void C() {

    }

    比如以上代码,当我们运行main方法时,会启动一个线程,这个时候,JVM会在运行时数据区创建一个虚拟机栈。 在栈中 运行方法 每运行一个方法 ,会压入一个栈帧

    image.png

    • 虚拟机栈大小限制 Xss参数指定

    image.png

    -Xsssize
    设置线程堆栈大小(以字节为单位)。k或k表示KB, m或m表示MB, g或g表示GB。默认值取决于虚拟内存。
    下面的示例以不同的单位将线程堆栈大小设置为1024kb:
    -Xss1m
    -Xss1024k
    -Xss1048576
    这个选项相当于-XX:ThreadStackSize。

    栈帧

    栈帧内主要包含

    • 局部变量表
    • 操作数栈
    • 动态连接
    • 完成出口

    栈帧对内存区域的影响

    以以下代码为例

    public class Apple {
    public int grow() throws Exception {
    int x = 1;
    int y = 2;
    int z = (x + y) * 10;
    return z;
    }
    public static void main(String[] args) throws Exception {
    Apple apple = new Apple();
    apple.grow();
    apple.hashCode();
    }
    }

    因为JVM识别的.class文件,而不是.java文件。因此,我们需要拿到其字节码,可以通过ASM plugin插件 右键获取 或者通过 javap -v xxx.class 获取 (本文通过javap 方式获取) 其字节码如下

    Classfile /XXX/build/classes/java/mainXXX/Apple.class
    Last modified 2021-8-11; size 668 bytes
    MD5 checksum d10da1235fad7eba906f5455db2c5d8b
    Compiled from "Apple.java"
    public class Apple
    minor version: 0
    major version: 51
    flags: ACC_PUBLIC, ACC_SUPER
    Constant pool:
    #1 = Methodref #6.#29 // java/lang/Object."<init>":()V
    #2 = Class #30 // Apple
    #3 = Methodref #2.#29 // Apple."<init>":()V
    #4 = Methodref #2.#31 // Apple.grow:()I
    #5 = Methodref #6.#32 // java/lang/Object.hashCode:()I
    #6 = Class #33 // java/lang/Object
    #7 = Utf8 <init>
    #8 = Utf8 ()V
    #9 = Utf8 Code
    #10 = Utf8 LineNumberTable
    #11 = Utf8 LocalVariableTable
    #12 = Utf8 this
    #13 = Utf8 Apple;
    #14 = Utf8 grow
    #15 = Utf8 ()I
    #16 = Utf8 x
    #17 = Utf8 I
    #18 = Utf8 y
    #19 = Utf8 z
    #20 = Utf8 Exceptions
    #21 = Class #34 // java/lang/Exception
    #22 = Utf8 main
    #23 = Utf8 ([Ljava/lang/String;)V
    #24 = Utf8 args
    #25 = Utf8 [Ljava/lang/String;
    #26 = Utf8 apple
    #27 = Utf8 SourceFile
    #28 = Utf8 Apple.java
    #29 = NameAndType #7:#8 // "<init>":()V
    #30 = Utf8 Apple
    #31 = NameAndType #14:#15 // grow:()I
    #32 = NameAndType #35:#15 // hashCode:()I
    #33 = Utf8 java/lang/Object
    #34 = Utf8 java/lang/Exception
    #35 = Utf8 hashCode
    {
    public Apple();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
    stack=1, locals=1, args_size=1
    0: aload_0
    1: invokespecial #1 // Method java/lang/Object."<init>":()V
    4: return
    LineNumberTable:
    line 3: 0
    LocalVariableTable:
    Start Length Slot Name Signature
    0 5 0 this Apple;

    public int grow() throws java.lang.Exception;
    descriptor: ()I
    flags: ACC_PUBLIC
    Code:
    stack=2, locals=4, args_size=1
    0: iconst_1
    1: istore_1
    2: iconst_2
    3: istore_2
    4: iload_1
    5: iload_2
    6: iadd
    7: bipush 10
    9: imul
    10: istore_3
    11: iload_3
    12: ireturn
    LineNumberTable:
    line 5: 0
    line 6: 2
    line 7: 4
    line 8: 11
    LocalVariableTable:
    Start Length Slot Name Signature
    0 13 0 this Apple;
    2 11 1 x I
    4 9 2 y I
    11 2 3 z I
    Exceptions:
    throws java.lang.Exception

    public static void main(java.lang.String[]) throws java.lang.Exception;
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
    stack=2, locals=2, args_size=1
    0: new #2 // class Apple
    3: dup
    4: invokespecial #3 // Method "<init>":()V
    7: astore_1
    8: aload_1
    9: invokevirtual #4 // Method grow:()I
    12: pop
    13: aload_1
    14: invokevirtual #5 // Method java/lang/Object.hashCode:()I
    17: pop
    18: return
    LineNumberTable:
    line 11: 0
    line 12: 8
    line 13: 13
    line 14: 18
    LocalVariableTable:
    Start Length Slot Name Signature
    0 19 0 args [Ljava/lang/String;
    8 11 1 apple Lcom/enjoy/ann/Apple;
    Exceptions:
    throws java.lang.Exception
    }
    SourceFile: "Apple.java"

    从其字节码中 我们可以看到这么一段

      public Apple();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
    stack=1, locals=1, args_size=1
    0: aload_0
    1: invokespecial #1 // Method java/lang/Object."<init>":()V
    4: return
    LineNumberTable:
    line 3: 0
    LocalVariableTable:
    Start Length Slot Name Signature
    0 5 0 this Apple;

    这是Apple的构造方法,虽然我们没有写,但是默认有无参构造方法实现

    回到正文 下面我们对grow()方法做解析

     public int grow() throws java.lang.Exception;
    descriptor: ()I
    flags: ACC_PUBLIC
    Code:
    stack=2, locals=4, args_size=1
    0: iconst_1
    1: istore_1
    2: iconst_2
    3: istore_2
    4: iload_1
    5: iload_2
    6: iadd
    7: bipush 10
    9: imul
    10: istore_3
    11: iload_3
    12: ireturn
    LineNumberTable:
    line 5: 0
    line 6: 2
    line 7: 4
    line 8: 11
    LocalVariableTable:
    Start Length Slot Name Signature
    0 13 0 this Apple;
    2 11 1 x I
    4 9 2 y I
    11 2 3 z I
    Exceptions:
    throws java.lang.Exception

    我们可以看到其code代码区域,有 0 1 2 3 既

    image.png

    这是grow栈帧中的字节码地址(相对于改方法的偏移量)表,当程序运行的时候,程序计数器中的数会被调换为运行这个方法的字节码的行号 0 1 2 3 [字节码行号] 而字节码的行号 对应JVM 字节码指令助记符 下面对字节码地址表中涉及的字节码行号 进行理解

             0: iconst_1
    1: istore_1
    2: iconst_2
    3: istore_2
    4: iload_1
    5: iload_2
    6: iadd
    7: bipush 10
    9: imul
    10: istore_3
    11: iload_3
    12: ireturn

    首先 进入grow方法 记录进入方法时所在main()中的行号 作为完成出口,如main方法中grow方法字节码地址为3 方法完成后,接着执行完成出口的下一行字节码地址,所有的操作都在操作数栈中完成

    进入grow()栈帧中。程序计数器将计数器置为0,如果该类是静态方法,则局部变量表不变,如果该类不是静态方法,则在局部变量量中加入该对象实例this。类似下图

    image.png

    • 0: iconst_1
      • i 表示int const 表示常量 后面的1 表示值 ,这里表示创建int常量 1 ,放入操作数栈。

    image.png 然后code代码运行下一行

    • 1: istore_1
      • 这里将程序计数器count值改为1,然后 i 表示 int ,store表示 存储, 1 表示存储下标 存储到局部变量表中1的位置 ,我们这里将操作数中值为1的int出栈放到局部变量中 存储。

    image.png

    上面两条字节码 对应 int X = 1

    i_const_1 对应右边 定义1
    i_store_1 对应左边 用一个变量X存储 1 int y = 2参考上面分析

    下面我们来看看 int z = (x + y) * 10; x 和 y都在本地布局变量中有存储,因此,执行这条代码的时候,我们不需要上述步骤了,我们可以通过4: iload_1,将布局变量中1位置的数据加载到操作数栈中

    image.png

    下面执行code中 6: iadd,将操作数栈中的数据弹出两个操作数,再将结果存入栈顶,这个时候结果仅仅保留在操作数栈

    image.png 这个时候我们已经完成了 (X + y)这步 ,接下来看 * 10这步,这个时候我们跳到 7: bipush 10这个值也是常量值,但是比较大 操作指令有点不一样,0-5用const,其它值JVM采用bipush指令将常量压入栈中。 10对应要压入的值 。

    image.png 然后我们跳到下一行 9: imul .这是一个加法操作指令。我们可以看到操作号直接从7变成了9.这是因为bipush指令过大,占用了2个操作指令长度。

    image.png 这个时候我们已经得到了计算结果,还需要将其赋值局部变量z进行变量存储.

    image.png 此时,我们已经完成了 z = (x + y) * 10的操作了。 此时执行最后一行 return z;首先 ,取出z,将其load进操作数栈,然后利用ireturn返回结果。该方法结束。这个时候,完成出口存储的上一方法中的程序计数器的值,回到上一方法中正确的位置。

    补充

    0 1 2 3 4 7 9 字节码偏移量 针对本方法的偏移

    程序计数器只存储自己这个方法的值
    动态连接 确保多线程执行程序的正确性

    本地方法栈

    本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,其区别不过是虚拟机栈为JVM执行 Java 方法(也就是字节码)服务,而本地方法栈则是为JVM使用到的 Native方法服务。在hotSpot中,本地方法栈与虚拟机栈是一体

    在本地方法栈中,程序计数器为null,因为,本地方法栈中运行的形式不是字节码


    线程共享区

    下面还是来一段代码

    public class Learn {
    static int NUMBER = 18; //静态变量 基本数据类型
    static final int SEX = 1; //常量 基本数据类型
    static final Learn LERARN = new Learn(); //成员变量 指向 对象
    private boolean isYou = true; //成员变量


    public static void main(String[] args) {
    int x = 18;//局部变量
    long y = 1;
    Learn learn = new Learn(); //局部变量 对象
    learn.isYou = false;//局部变量 改变值
    learn.hashCode(); //局部变量调用native 方法
    ByteBuffer byteBuffer = ByteBuffer.allocateDirect(128 * 1024 * 1024);//分配128M直接内存
    }
    }

    类加载过程中

    Learn 加载到方法区

    类中的 静态变量、常量加载到方法区。

    方法区

    是可供各条线程共享的运行时内存区域。它存储了每一个类的结构信息,

    我们申请的几乎所有的对象,都是在这里存储的。我们常说的垃圾回收,操作的对象就是堆。 随着对象的频繁创建,堆空间占用的越来越多,就需要不定期的对不再使用的对象进行回收。这个在 Java 中,就叫作 GC(Garbage Collection)。

    创建的时候,到底是在堆上分配,还是在栈上分配呢?

    这和两个方面有关:对象和在 Java 类中存在的位置。 对于普通对象来说,JVM 会首先在堆上创建对象,然后在其他地方使用的其实是它的引用。比如,把这个引用保存在虚拟机栈的局部变量表中。 对于基本数据类型来说(byte、short、int、long、float、double、char),有两种情况。 当你在方法体内声明了基本数据类型的对象,它就会在栈上直接分配。其他情况,都是在堆上分配。 image.png

    JVM内存处理

    先来一段代码进行后续分析

    public class JVMObject {
    public final static String MAN_TYPE = "man"; // 常量
    public static String WOMAN_TYPE = "woman"; // 静态变量
    public static void main(String[] args)throws Exception {
    Teacher T1 = new Teacher();
    T1.setName("A");
    T1.setSexType(MAN_TYPE);
    T1.setAge(36);
    for(int i =0 ;i < 15 ;i++){
    //每触发一次gc(),age+1 记录age的字段是4位 最大1111 对应15
    System.gc();//主动触发GC 垃圾回收 15次--- T1存活 T1要进入老年代
    }
    Teacher T2 = new Teacher();
    T2.setName("B");
    T2.setSexType(MAN_TYPE);
    T2.setAge(18);
    Thread.sleep(Integer.MAX_VALUE);//线程休眠 后续进行观察 T2还是在新生代
    }
    }

    class Teacher{
    String name;
    String sexType;
    int age;

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

    public String getSexType() {
    return sexType;
    }
    public void setSexType(String sexType) {
    this.sexType = sexType;
    }
    public int getAge() {
    return age;
    }
    public void setAge(int age) {
    this.age = age;
    }
    }

    1. JVM申请内存
    2. 初始化运行时数据区
    3. 类加载

    image.png

    • 执行方法(加载后运行main方法)

    image.png 4.创建对象

    image.png

    流程

    JVM 启动,申请内存,先进行运行时数据区的初始化,然后把类加载到方法区,最后执行方法。方法的执行和退出过程在内存上的体现上就是虚拟机栈中栈帧的入栈和出栈。 同时在方法的执行过程中创建的对象一般情况下都是放在堆中,堆中的对象最后通过垃圾回收处理。

    • 堆空间分代划分

    image.png

    通过HSDB查看堆空间划分 及内存分配

    • 先运行相关类

    • CMD命令行 运行jps 查看相关进程

      • image.png
    • 找到JDK安装目录 java8u292\bin bin 目录下点击HSDB.exe运行程序

    • 通过File下 点击 下图 进行 进程绑定

      • image.png
      • 将之前通过jps获取的进程号输入 输入框
      • image.png
      • 绑定后界面为 该进程下进程信息
      • image.png

    • 通过 Tools栏下的heap parameter 可以观察堆分配情况
      • image.png
      • 我们可以看到堆分区的划分和之前的是类似的,这样可以直观的看到堆的地址,也可以让我们对JVM将内存虚拟化有更直观的认知。
      • image.png

    • 对象的地址分配
      • 我们也可以通过object histogram查看对象的分配情况

      • image.png

      • 进入后界面如下所示

      • image.png

      • 我们可以通过全类名搜索相关类

      • image.png

      • 找到自己想要的查看的类后,可以看到 第一行表示这个类所有对象的size ,count 数量是多少个。比如标红的表示,Teacher类所有对象占用48,一共两个对象。双击这一栏,进入详细页面

      • image.png

      • 点击对应条目,点击下方insperctor查看详细信息,将其与之前堆内存地址分配对比,发现一个主动调用gc()从新生代慢慢进入老年代,这个A已经进入老年代了,而另一个B还在新生代。

      • image.png

    通过HSDB查看栈

    可以在HSDB绑定进程时,查看所有列出的线程信息,点击想要查看的线程,如main线程。点击浮窗菜单栏上的第二个 我们可以查看主线程的栈内存情况 ,如下图所示。

    image.png

    有兴趣的朋友可以去玩玩 这个工具

    内存溢出

    栈溢出 StackOverflowError

    方法调用方法 递归

    堆溢出

    OOM 申请分配内存空间 超出堆最大内存空间

    可以通过设置运行设置 进行模拟

    image.png

    image.png 可以通过设置 VM options进行设置JVM 相关参数配置参考相关链接第一个 可以通过 调大 -Xms,-Xmx参数避免栈溢出

    方法区溢出

    (1) 运行时常量池溢出

    (2)方法区中保存的Class对象没有被及时回收掉或者Class信息占用的内存超过了我们配置。

    class回收条件
    • 该类所有的实例都已经被回收,堆中不存在该类的任何实例
    • 加载该类的ClassLoader已经被回收
    • 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

    直接内存溢出

    直接内存的容量可以通过MaxDirectMemorySize来设置(默认与堆内存最大值一样)


    收起阅读 »

    Jetpack Compose 快来学学吧!

    简介官方入门文档:developer.android.com/jetpack/com…Jetpack Compose 是 Google 在2019年 Google I/O 大会上公布的全新的 Android 原生 UI 开发框架,历时两年2021年7月29日,...
    继续阅读 »

    简介

    官方入门文档:developer.android.com/jetpack/com…

    Jetpack Compose 是 Google 在2019年 Google I/O 大会上公布的全新的 Android 原生 UI 开发框架,历时两年2021年7月29日,正式版终于问世。官方介绍有以下特点:

    • 更少的代码

      • 使用更少的代码实现更多的功能,并且可以避免各种错误,从而使代码简洁且易于维护。
    • 直观的 Kotlin API

      • 只需描述界面,Compose 会负责处理剩余的工作。应用状态变化时,界面会自动更新。
    • 加快应用开发

      • 兼容现有的所有代码,方便随时随地采用。借助实时预览和全面的 Android Studio 支持,实现快速迭代。
    • 功能强大

      • 凭借对 Android 平台 API 的直接访问和对于 Material Design、深色主题、动画等的内置支持,创建精美的应用。

    如何理解“全新 UI 框架”?全新在于它直接抛弃了我们写了 N 年的 View 和 ViewGroup 那一套东西,从上到下撸了一整套全新的 UI 框架。就是说,它的渲染机制、布局机制、触摸算法以及 UI 的具体写法,全都是新的。

    个人总结 Compose 特点有三个:代码写 UI、声明式 UI、全新 UI框架。

    Motivation

    参考自:深入详解 Jetpack Compose | 优化 UI 构建

    解耦

    目前 Android 写界面的方式,布局文件写在 layout.xml 中,而视图模型(也就是代码逻辑部分)写在 ViewModel 或者 Presenter 中,通过某些 API (例如 findViewById) 建立两者之间的联系。这两者之间耦合十分紧密,例如在 XML 中修改了 id 或者 View 的类型,需要在视图模型中修改对应代码;此外如果动态删除或增加了某个 View,布局 XML 不会更新,因此需要在视图模型手动维护。

    造成上述现象的原因是因为 XML 布局和视图模型就应该是一体的。那能不能直接用代码写布局文件呢?当然是可以的,但肯定不是现在这样 new 一个 ViewGroup,然后 addView 的方式。比较容易想到的是通过 kotlin DSL,需要注意由于在不同情况下显示的 UI 可能不同,所以 DSL 一定含有逻辑。

    历史包袱

    Android已经十年多了,传统的Android UI 有很多历史遗留问题,而有些官方也很难修改。比如View.java有三万多行代码,ListView 已经废弃了。

    为了避免重蹈覆辙,在 Jetpack Compose 的世界中使用函数替代了类型,用组合替代继承,抛弃原有Android View System,操作canvas直接进行绘制:

    重点目标是解决耦合问题,解决基础类 View.java 爆炸问题。基于组合优于继承的思想,重新设计一套解偶的UI框架。

    快速入门

    环境准备

    参考自官方文档:developer.android.com/jetpack/com…

    1. 下载 Android Studio Arctic Fox:developer.android.com/studio (注意一定要使用2021.7.29日后发布的AS)
    2. 加入依赖
    android {
    buildFeatures {
    compose true
    }
    // compose_version = '1.0.0'
    composeOptions {
    kotlinCompilerExtensionVersion compose_version
    kotlinCompilerVersion '1.5.10'
    }
    }

    dependencies {
    implementation "androidx.compose.ui:ui:$compose_version"
    implementation "androidx.compose.material:material:$compose_version"
    implementation "androidx.compose.ui:ui-tooling:$compose_version"
    implementation 'androidx.activity:activity-compose:1.3.0'
    }
    1. 在代码中使用 Compose

    Activity

    class MainActivity : ComponentActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContent {
    HelloWorld()
    }
    }
    }

    Fragment

    class MainFragment: Fragment() {

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
    return ComposeView(requireContext()).apply {
    setContent {
    HelloWorld()
    }
    }
    }
    }

    XML

    <androidx.compose.ui.platform.ComposeView
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/compose"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />
    findViewById<ComposeView>(R.id.compose).setContent {
    HelloWorld()
    }

    小试牛刀

    我第一次写 Compose ,复刻大力辅导我的页面大约花费 2.5 小时(仅 UI,不含逻辑)。实际感受学习成本不高,如果大家有 flutter 基础,可以说非常容易上手。

    设计稿Compose 还原图

    展示一下头部帐户信息区域代码:

    @Composable
    fun AccountArea() {
    Row(
    verticalAlignment = Alignment.CenterVertically,
    modifier = Modifier
    .height(64.dp)
    .fillMaxWidth()
    ) {
    Image(
    painter = painterResource(id = R.drawable.avatar),
    contentDescription = "我的头像",
    modifier = Modifier
    .size(64.dp)
    .clip(shape = CircleShape)
    )

    Spacer(modifier = Modifier.width(12.dp))
    Column(
    modifier = Modifier.align(Alignment.CenterVertically)
    ) {
    Text(text = "华Lena爱生活", style = MyTabTheme.typography.h2, color = MyTabTheme.colors.textPrimary)
    Spacer(modifier = Modifier.height(10.dp))
    Row {
    GradeOrIdentity(text = "选择年级")
    Spacer(modifier = Modifier.width(12.dp))
    GradeOrIdentity(text = "选择身份")
    }

    }

    Spacer(modifier = Modifier.weight(1f))
    Image(
    painter = painterResource(id = R.drawable.icon_header_more),
    contentDescription = "进入个人信息设置",
    modifier = Modifier
    .size(16.dp)
    )
    }
    }


    @Composable
    fun GradeOrIdentity(text: String) {
    Row(
    horizontalArrangement = Arrangement.Center,
    verticalAlignment = Alignment.CenterVertically,
    modifier = Modifier
    .size(74.dp, 22.dp)
    .clip(MyTabTheme.shapes.medium)
    .background(MyTabTheme.colors.background)
    ) {
    Text(text = text, style = MyTabTheme.typography.caption, color = MyTabTheme.colors.textSecondary)
    Spacer(modifier = Modifier.width(2.dp))
    Image(
    painter = painterResource(id = R.drawable.icon_small_more),
    contentDescription = text,
    modifier = Modifier
    .size(8.dp)
    )
    }
    }

    大致结构是通过组合 Row 和 Column,再加上 Text、Image、Spacer 等一些“控件”,和 Flutter 很像。Flutter 中万物皆 Widget,Column、ListView、GestureDetector、Padding 都是 Widget,Compose 呢?

    @Composable 函数

    可能直观上大家会觉得 Row/Text/Image 是某种 “View”?然而并不是,其实它们都是函数,唯一特殊的是带有了 @Composable 注解。该函数有个规则:@Composeable 函数必须在另一个 @Composeable 函数中被调用(和协程关键字 suspend 类似),我们自己写的函数也需要加上该注解。所以有种说法是,Compose 中一切皆函数,开发者通过组合 @Composeable 函数达到想要的效果。

    // Composable
    fun Example(a: () -> Unit, b: @Composable () -> Unit) {
    a() // 允许
    b() // 不允许
    }

    @Composable
    fun Example(a: () -> Unit, b: @Composable () -> Unit) {
    a() // 允许
    b() // 允许
    }

    Modifier

    现有 Android View 体系中设置宽高、Margin、点击事件、背景色是通过设置 View 的某个属性来实现的。而 Flutter 通过包裹一层Widget实现(例如 Padding,SizedBox,GestureDetector、DecoratedBox)。刚刚我们说过,Compose 中一切皆函数,那 Compose 是选择为每个 @Composable 函数提供 height/width/margin 等参数,还是再包一层 @Composable 函数呢?答案是前者,Compose 把它们统一抽象成了 modifier 参数,通过串联 Modifier 设置大小、行为、外观。Modifier 功能很强大,除了设置宽高、点击、Padding 、背景色之外,还能设置宽高比、滚动、weight,甚至 draw、layout、拖拽、缩放都能做。

    (大家如果熟悉 Flutter 应该知道,Flutter 为人诟病的一点是它的 Widget 层级极深,很难方便得找到想要的 Widget。而在 Compose 中不会有此问题,因为 Modifier 的设计,是的它可以做到和现有 XML 布局的层级是一样的。)

    开发调试

    现有 XML 布局的预览功能是很强大的,开发时能快速看到效果,Compose 作为 Google 下一代 UI 框架在这点上的支持也相当强大。(这里吐槽一下 Flutter,必须编译到手机上才能看到效果)。加上 @Preview 注解可实现预览,如下图。点击预览图上的控件也可以直接定位到代码。

    除了静态预览之外,Compose 还支持简单的点击交互和调试动画。能实现这些的原因是 Compose 确确实实编译出了可执行的代码。(有些跨平台的影子)

     

    常用 Composable

    这里做个总结,大部分 Android 已有的能力使用 Compose 都能实现。

    AndroidComposeFlutter说明
    TextViewTextText
    EditTextTextFieldTextField
    ImageViewImageImage如果是加载网络图片,Android/Compose 需使用三方库
    LinearLayoutColumn/RowColumn/Row
    ListView/RecyclerViewLazyColumn/LazyRowListView
    GridView/RecyclerViewLazyVerticalGrid(实验性)GridView
    ConstraintLayoutConstraintLayout
    FrameLayoutBoxStack
    ScrollViewModifier.verticalScroll()SingleChildScrollView
    NestedScrollViewModifier.nestedScroll()NestedScrollViewCompose 通过 modifier 实现
    ViewPagerPageViewCompose 有开源方案:google.github.io/accompanist…
    padding/marginModifier.padding()Padding
    layout_height/layout_widthModifier.size()/height()/width()/fillMaxWidth()SizedBox
    backgroundModifier.drawbehind{}DecoratedBox

    Text

    详见:Compose Text

    @Composable
    fun BoldText() {
    Text("Hello World", color = Color.Blue,
    fontSize = 30.sp, fontWeight = FontWeight.Bold,
    modifier = Modifier.padding(10.dp)
    )
    }

    TextField

    @Composable
    fun StyledTextField() {
    var value by remember { mutableStateOf("Hello\nWorld\nInvisible") }
    TextField(
    value = value,
    onValueChange = { value = it },
    label = { Text("Enter text") },
    maxLines = 2,
    textStyle = TextStyle(color = Color.Blue, fontWeight = FontWeight.Bold),
    modifier = Modifier.padding(20.dp)
    )
    }

    Image

    Image(
    painter = painterResource(R.drawable.header),
    contentDescription = null,
    modifier = Modifier
    .height(180.dp)
    .fillMaxWidth(),
    contentScale = ContentScale.Crop
    )

    Column / Row

    Column 表示纵向排列,Row 表示横行排列。以 Column 为例代码如下:

    Column(modifier = Modifier.background(Color.LightGray).height(400.dp).width(200.dp)) {
    Text(text = "FIRST LINE", fontSize = 26.sp)
    Divider()
    Text(text = "SECOND LINE", fontSize = 26.sp)
    Divider()
    Text(text = "THIRD LINE", fontSize = 26.sp)
    }

    Box

    Box(Modifier.background(Color.Yellow).size(width = 150.dp, height = 70.dp)) {
    Text(
    "Modifier sample",
    Modifier.offset(x = 25.dp, y = 30.dp)
    )
    Text(
    "Layout offset",
    Modifier.offset(x = 0.dp, y = 0.dp)
    )
    }

    LazyColumn / LazyRow

    LazyColumn(modifier = modifier) {
    items(items = names) { name ->
    Greeting(name = name)
    Divider(color = Color.Black)
    }
    }

    ConstraintLayout

    详细介绍可参考:medium.com/android-dev…

    需要额外引入依赖:

    implementation "androidx.constraintlayout:constraintlayout-compose:1.0.0-beta01"
    @Composable
    fun ConstraintLayoutContent() {
    ConstraintLayout {
    val (button1, button2, text) = createRefs()
    Button(
    onClick = { /* Do something */ } ,
    modifier = Modifier.constrainAs(button1) {
    top.linkTo(parent.top, margin = 16.dp)
    }
    ) {
    Text("Button 1")
    }

    Text("Text", Modifier.constrainAs(text) {
    top.linkTo(button1.bottom, margin = 16.dp)
    centerAround(button1.end)
    } )

    val barrier = createEndBarrier(button1, text)
    Button(
    onClick = { /* Do something */ } ,
    modifier = Modifier.constrainAs(button2) {
    top.linkTo(parent.top, margin = 16.dp)
    start.linkTo(barrier)
    }) {
    Text("Button 2")
    }
    }
    }

    在声明式 UI 中是无法获取一个 “View” 的 id,但是在 ConstraintLayout 中似乎有所例外,因为需要 id 来描述相对位置。

    Scroll(滚动)

    详见:developer.android.com/jetpack/com…

    在 Compose 中只需加入 Modifier.scroll() / Modifier.verticalScroll() / Modifier.horizontalScroll() 即可实现。

    Column(     
    modifier = Modifier
    .background(Color.LightGray)
    .size(100.dp)
    .verticalScroll(rememberScrollState())
    ) {
    repeat(10) {
    Text("Item $it", modifier = Modifier.padding(2.dp))
    }
    }

    NestedScroll

    详见:developer.android.com/jetpack/com…

    简单的嵌套滚动只需要加上了 Modifier.scroll() 即可实现

    val gradient = Brush.verticalGradient(0f to Color.Gray, 1000f to Color.White)
    Box(
    modifier = Modifier
    .background(Color.LightGray)
    .verticalScroll(rememberScrollState())
    .padding(32.dp)
    ) {
    Column {
    repeat(6) {
    Box(
    modifier = Modifier
    .height(128.dp)
    .verticalScroll(rememberScrollState())
    ) {
    Text(
    "Scroll here",
    modifier = Modifier
    .border(12.dp, Color.DarkGray)
    .background(brush = gradient)
    .padding(24.dp)
    .height(150.dp)
    )
    }
    }
    }
    }

    如果是复杂的嵌套滚动需要使用 Modifier.nestedScroll()

    val toolbarHeight = 48.dp
    val toolbarHeightPx = with(LocalDensity.current) { toolbarHeight.roundToPx().toFloat() }
    val toolbarOffsetHeightPx = remember { mutableStateOf(0f) }
    val nestedScrollConnection = remember {
    object : NestedScrollConnection {
    override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
    val delta = available.y
    val newOffset = toolbarOffsetHeightPx.value + delta
    toolbarOffsetHeightPx.value = newOffset.coerceIn(-toolbarHeightPx, 0f)
    return Offset.Zero
    }
    }
    }

    Box(
    Modifier
    .fillMaxSize()
    .nestedScroll(nestedScrollConnection)
    ) {
    // our list with build in nested scroll support that will notify us about its scroll
    LazyColumn(contentPadding = PaddingValues(top = toolbarHeight)) {
    items(100) { index ->
    Text("I'm item $index", modifier = Modifier
    .fillMaxWidth()
    .padding(16.dp))
    }
    }

    TopAppBar(
    modifier = Modifier
    .height(toolbarHeight)
    .offset { IntOffset(x = 0, y = toolbarOffsetHeightPx.value.roundToInt()) } ,
    title = { Text("toolbar offset is ${toolbarOffsetHeightPx.value}") }
    )
    }

    声明式 UI

    假设要修改一个 View 的颜色,如果使用命令式的开发方式首先需要借助 findViewById 等获得此 View 的句柄,然后通过调用其方法实现UI的变化。

    // Imperative style
    View b = findViewById(...)
    b.setColor(red)

    命令式UI需要持有全功能UI句柄,然后通过调用相关方法对UI进行变更

    如果使用声明式的方式完成同样效果,只需要在声明 View 的同时将 state 关联到对应的 UI 属性,然后通过 state 的变化,驱动 View 的重新绘制

    // Declarative style
    View {
    color: state.color,
    }

    声明式UI仅仅描述当前的UI状态,当状态变化时框架自动更新UI

    声明式UI中无法通过获取View的句柄对其属性进行修改,这间接保证了UI属性的不可变性(immutable),state作为唯一的“Source of Truth”,任何时刻保持与UI状态的绝对一致。因此声明式UI不可绕开的是状态管理。

    状态管理

    参考:developer.android.com/jetpack/com…

    如果使用过声明式框架,应该对状态管理并不陌生。Flutter 中有 StatelessWidget 和 StatefulWidget,StatefulWidget 用于管理状态。而 Compose 使用 State 管理状态(大部分情况还需要结合 remember())。在下面的例子中,每当 text 值变化时,会触发重组,更新UI。

    重组:在数据发生变化时重新运行某些函数以达到改变 UI

    为什么修改 text 就会触发更新 UI 呢?这是 Compose 赋予 State 的能力(mutableStateOf()返回的是一个可变的 State),State 表示该变量是 Compose 需要关心的“状态”,当状态变化后需要被感知并触发重组。实际使用中,State 一般会配合 remember 函数使用,remember 是指当重组发生时可以记住上一次的 State。在下面例子中,当 Foo() 函数由于重组而被重新执行时,若没有 remember 该函数的局部变量 text 会被赋值成初始值(即空字符串),这显然与我们的预期不符,因此诞生了 remember。

    @Composable
    fun Foo() {
    var text: String by remember { mutableStateOf("") }
    Button(onClick = { text = "$text\n$text" }) {
    Text(text)
    }
    }

    上面的代码和 Flutter 中的 MobX 很像,但是相比于 MobX,Compose 的重组范围细粒度更精准,它会自动分析出最小的重组范围。例如上述代码只要重组的范围是 Button 的 lambda 部分

    推荐架构

    这些 State 应该放在哪里好呢?是分布在各个 @Composable 函数中吗?官方提供了一种推荐的架构:用 ViewModel 持有 LiveData,UI 界面观察 LiveData。

    事件从下往上,状态从上往下(单向数据流)

    其他

    作为一个 UI 框架,还需要包括主题(Theme)、动画(Animation),这部分大家自行参考官方文档

    实际使用呢?

    实际使用 Compose 可能会关心这些数据

    包大小

    参考自:Jetpack compose before and after

    结论:包大小有显著缩小

    包大小
    方法数
    编译速度

    性能

    理论上由于没有了 XML -> Java 的转换所以对复杂布局有优势。

    实际使用中,没有找到比较权威的数据,官方给的 Demo 刷着挺流畅的。

    但是在 GitHub 上会也看到很多 issue 吐槽 Compose 的性能:github.com/android/com…

    上手门槛&开发效率

    优势:更容易写动画、自定义View、预览功能很强大。

    劣势:需要适应声明式写法

    android-developers.googleblog.com/2021/05/mer… 该文章声称 Compose 提高了 56% 的开发效率。如果有 Flutter 基础可以更快上手,大家可以尝试用 Compose 实现一个界面,用起来会挺顺。

    经典 View 中写动画、自定义View、自定义 ViewGroup 都有一定门槛,但在 Compose 中使用系统提供的API,例如 animateXXXAsState、Canvas、Layout 实现相同功能会简单很多。

    现有库的兼容性

    实际使用时可能会遇到 Compose 基建不完善的情况。当然,很多三方库已经有了 Compose 的兼容版本,例如 Glide、ViewPager、SwipeRefresh 、RxJava等,但不得不承认更多库是没有 Compose 版本的,例如 Fresco、TTVideoPlayer、WebView、CoordinatorLayout 等都没有。官方也提供了一种解决方法 AndroidView,写法如下:

    @Composable
    fun CustomView() {
    val selectedItem = remember { mutableStateOf(0) }
    // Adds view to Compose
    AndroidView(
    modifier = Modifier.fillMaxSize(), // Occupy the max size in the Compose UI tree
    factory = { context ->
    // Creates custom view
    CustomView(context).apply {
    // Sets up listeners for View -> Compose communication
    myView.setOnClickListener {
    selectedItem.value = 1
    }
    }
    } ,
    update = { view ->
    // View's been inflated or state read in this block has been updated
    // Add logic here if necessary
    view.coordinator.selectedItem = selectedItem.value
    }
    )
    }

    我尝试把 QrCodeView(一个二维码扫描 View) 用上述方式包装成 Compose ,遇到了一些问题。

    1. 混写命令式和声明式,很不舒服

    2. 不熟悉 Compose 导致的代码错误

      1. 例如:生命周期问题,composable 函数会在传统 View.onAttachToWindow() 时才会执行,所以在 Activity.onCreate/onStart/onResume 想要对此 AndroidView 做一些操作需要一些技巧。很多时候需要翻阅官方提供的 Sample 才能找到答案。
    3. 其他奇奇怪怪没有考虑 Compose 时的问题。很多三方库会有各种和 Compose 不兼容问题,需要一点点踩坑。

    如果是一个无 Compose 版本的 ViewGroup,例如瀑布流布局,就别想着迁移了,还是用 Compose 重写个相同功能的 Composable 函数比较现实,Compose 提供了 Layout() 函数对自定义布局会比较简单的。

    是否可以跨平台?

    根据 Kotlin 跨平台特性,Compose 其实是有跨平台潜力的,官方也给了 Demo,当然都处于非常原始的状态。

    Compose For Desktop:Compose for Desktop UI Framework

    Compose For Web:Technology Preview: Jetpack Compose for Web | The Kotlin Blog


    收起阅读 »

    Android Studio中的 Image Asset Studio(图标生成工具)

    Android 图标在线生成Android Studio 包含一个名为 Image Asset Studio 的工具,它可以帮我们把自定义图像、系统图标素材、文本字符串自动生成适配系统的应用图标。它为你的应用程序支持的每个像素密度生成一组适当分辨率的图标。Im...
    继续阅读 »

    Android 图标在线生成

    Android Studio 包含一个名为 Image Asset Studio 的工具,它可以帮我们把自定义图像系统图标素材文本字符串自动生成适配系统的应用图标。它为你的应用程序支持的每个像素密度生成一组适当分辨率的图标。Image Asset Studio 将新生成的图标放置res/在项目目录下的特定文件夹中(例如 mipmap/ 或 drawable/)。在运行时,Android 根据运行应用的设备的屏幕密度使用适当的资源。

    Image Asset Studio 可帮助您生成以下图标类型:

    • 启动图标(Launcher icons)

      • Launcher Icons(Adaptive and Legacy):AS 3.0后新增,用于自适应启动图标,兼容新旧版系统;
      • Launcher Icons(Legacy only):用于非自适应的启动图标,仅限旧版系统(Android 8.0之前);
    • 操作栏和选项卡图标(Action bar and tab icons)

    • 通知图标(Notification icons)

    • TV Banners

    • TV Channel lcons

    Image Asset 是什么

    Image Asset Studio 可帮助您创建不同密度的各种类型的图标,并准确显示它们在项目中的放置位置。以下部分描述了您可以创建的图标类型以及您可以使用的图像和文本输入。

    Launcher icons

    Image Asset Studio 将启动图标放置在目录中的适当位置res/mipmap-density/。它还创建了适合 Google Play 商店的 512 x 512 像素图像。

    Action bar and tab icons

    Image Asset Studio 将图标放置在res/drawable-density/目录中的适当位置 。

    我们建议操作栏和选项卡图标使用 Material Design 风格。作为 Image Asset Studio 的替代方案,您可以使用 Vector Asset Studio创建操作栏和选项卡图标。矢量绘图适用于简单的图标,可以减少应用程序的大小。

    Notification icons

    通知是您可以在应用程序的正常 UI 之外向用户显示的消息。Image Asset Studio 将通知图标放置在目录中的适当位置 :res/drawable-density/

    • Android 2.2(API 级别 8)及更低版本的图标放置在目录中。res/drawable-density/
    • Android 2.3 到 2.3.7(API 级别 9 到 10)的图标放置在 目录中。res/drawable-density-v9/
    • Android 3(API 级别 11)及更高版本的图标放置在目录中。res/drawable-density-v11/
    • 如果你的应用程序支持 Android 2.3 到 2.3.7(API 级别 9 到 10),Image Asset Studio 会生成一个灰色版本的图标。后来的 Android 版本使用 Image Asset Studio 生成的白色图标。

    Clip Art(剪贴画)

    Image Asset Studio 使您可以轻松导入 VectorDrawable 和 PNG 格式的 Google Material 图标:只需从对话框中选择一个图标即可。

    Images(图片)

    您可以导入自己的图像并根据图标类型对其进行调整。Image Asset Studio 支持以下文件类型:PNG(首选)、JPG(可接受)和 GIF(不可用)。

    Text(文本)

    Image Asset Studio 允许您以各种字体键入文本字符串,并将其放置在图标上。它将基于文本的图标转换为不同密度的 PNG 文件。你可以使用计算机上安装的字体。

    使用 Image Asset Studio

    要启动 Image Asset Studio,请按照下列步骤操作:

    • 在Project窗口中,选择 Android view。
    • 右键单击res文件夹并选择 New > Image Asset。

    • Image Asset Studio 中的自适应和旧式图标向导。

    继续执行以下步骤:

    • 如果您的应用支持 Android 8.0及以上,请创建自适应和旧版启动器图标
    • 如果您的应用支持不高于 Android 7.1 的版本,请创建旧版启动器图标
    • 创建操作栏或选项卡图标。
    • 创建通知图标。

    创建Launcher Icons(Adaptive and Legacy)

    打开Image Asset Studio,你可以通过以下步骤添加图标:

    • Icon Type 中, 选择Launcher Icons (Adaptive and Legacy)

    • Foreground Layer选项卡中,选择Asset Type,然后在下方的字段中指定asset

      • 选择Image以指定图像文件的路径。
      • 选择Clip Art 以从Material Design 图标集中指定一个图像 。
      • 选择Text以指定文本字符串并选择字体。  文章上面有各自选择的教程
    • Background Layer选项卡中,选择Asset Type,然后在下方的字段中指定Asset。你可以选择一种颜色或指定要用作背景层的image。

    • Options选项卡中,查看默认设置并确认您要生成 Legacy、Round 和 Google Play Store 图标。

    • (可选)更改每个Foreground Layer和Background Layer选项卡的名称和显示设置:

      • Name:如果不想使用默认名称,请键入新名称。如果该资源名称已存在于项目中,如向导底部的错误所示,它将被覆盖。名称只能包含小写字符、下划线和数字
      • Trim:要调整源资产中图标图形和边框之间的边距,请选择Yes。此操作去除透明空间,同时保留纵横比。要保持源资产不变,请选择No
      • Color:要更改Clip Art or Text图标的颜色,请单击该字段。在"选择颜色"对话框中,指定一种颜色,然后单击"选择"。新值出现在该字段中。
      • Resize:使用滑块指定比例因子以调整Image, Clip Art, or Text图标的大小。当您指定颜色资源类型时,background layer的此控件将被禁用。
    • 单击Next。

    • 或者,更改资源目录:选择要添加图像资产的资源源集:src/main/res、 src/debug/res、src/release/res或自定义源集。要定义新的源集,请选择 File > Project Structure > app > Build Types. 例如,您可以定义一个 Beta 源集并创建一个图标版本,在右下角包含文本“BETA”。有关更多信息,

    • 单击Finish。Image Asset Studio 将图像添加到不同密度的 mipmap文件夹中。

    创建Launcher Icons(Legacy only)

    新增:

    • Scaling:要适合图标大小,请选择Crop或 Shrink to Fit。使用Crop,图像边缘可以被剪掉,而使用Shrink to Fit,则不会。如果源资产仍然不适合,您可以根据需要调整填充。
    • Shape:要在源资产后面放置背景,请选择一个形状,圆形、正方形、垂直矩形或水平矩形之一。对于透明背景,选择None。

    • Effect:如果要在正方形或矩形形状的右上角添加狗耳朵效果,请选择DogEar。否则,选择None。

    创建Action bar and tab icons

    创建Notification icons

    其他情况基本大同小异,这里就不多做介绍,浪费大家时间了。

    收起阅读 »

    Android面试题之Activity和Fragment生命周期 一次性记忆

    每当我们换工作面试之前,总是会不由自主的刷起面试题,大部分题我们反反复复不知道刷了多少遍,但是今天记住了,等下一次面试的时候又刷着相同的面试题,我就想问在座的各位,Activity的生命周期,你们到底刷过多少遍 [哭笑] 作为一名程序员 把时间浪费在重复性劳动...
    继续阅读 »

    每当我们换工作面试之前,总是会不由自主的刷起面试题,大部分题我们反反复复不知道刷了多少遍,但是今天记住了,等下一次面试的时候又刷着相同的面试题,我就想问在座的各位,Activity的生命周期,你们到底刷过多少遍 [哭笑] 作为一名程序员 把时间浪费在重复性劳动上是极其不能忍受的 因此 为了让自己省去不必要的脑力开销 我给自己总结了一份面试相关的记忆技巧,在这里分享给大家 记忆不是目的 把知识变成自己的才最关键

    前提

    需要熟悉Activity的生命周期 通过Activity的周期去对比理解和记忆Fragment生命周期

    Activity的生命周期

    假设你已经非常熟悉Activity的生命周期了,那么接下来咱们看Fragment的生命周期图

    Fragment的生命周期

    找出他和Activity的相同之处

    这部分完全和Activity一模一样 可以不用记忆它,咱们来看不同的地方

    其实这部分才是人们最容易搞混和记不住的地方 那咱们来分析一下:

    Fragment比Activity多了几个生命周期的回调方法

    • onAttach(Activity) 当Fragment与Activity发生关联的时候调用

    • onCreateView(LayoutInflater, ViewGroup, Bundle) 创建该F

    • onActivityCreated(Bundle) 当Activity的onCreated方法返回时调用

    • onDestroyView() 与onCreateView方法相对应,当该Fragment的视图被移除时调用

    • onDetach() 与onAttach方法相对应,当Fragment与Activity取消关联时调用 PS:注意:除了onCreateView,其他的所有方法如果你重写了,必须调用父类对于该方法的实现

      这些方法理解起来并不费劲 但是要完美记在脑子里 还是需要花上一番功夫的

      那咱们一个一个来 先从创建开始:

      1.首先 onAttach方法: 和Activity进行关联的时候调用 这个放在第一个 应该好理解

      2.我们知道 Activity在onCreate方法中需要调用setContentVIew()进行布局的加载,那么在Fragment中onCreateView就相当于Activity中的setContentVIew

      3.onActivityCreate是一个额外的方法 为了告诉Fragment当前Activity的创建执行情况 方便Fragment的后续操作

      先后顺序

      已知Fragment是依赖Activity而存在的 它们都有着相同的生命周期方法 那么先调用Activity的还是Fragment的呢? 这里分两种情况

      • 如果是创建 那么先创建Activity 后创建Fragment

      • 如果是销毁 那么先销毁Fragment 后销毁Activity

        网上有很多文章说Activity的onCreate方法在Fragment的onCreateView之后执行,这是不正确的 Fragment一般都是在Activity的onCreate()中创建 要么通过布局加载的方式 要么通过new创建Fragment对象的方式 如果没有Activity的onCreate 哪来的Fragment

        总结

        上面的理解好后,咱们再整理记忆一下

        一句话 Activity是老子 Fragment是小子 进门先让老子进 滚蛋先让小子滚 加载布局createView 老子完事吱一声(ActivityCreated)

        希望有帮到你

    收起阅读 »

    Android JNI 原理

    JNI:Java Native Interface1. 系统源码中的 JNI2. MediaRecorder 框架中的 JNIMediaRecorder,用于录音和录像。2.1 Java Framework 层的 MediaRecorder2.2 JNI 层的...
    继续阅读 »
    • JNI:Java Native Interface

    image.png

    1. 系统源码中的 JNI

    image.png

    2. MediaRecorder 框架中的 JNI

    MediaRecorder,用于录音和录像。

    image.png

    2.1 Java Framework 层的 MediaRecorder

    image.png

    image.png

    2.2 JNI 层的 MediaRecorder

    image.png

    2.3 Native 方法注册

    Native 方法注册分为静态注册和动态注册,其中静态注册多用于 NDK 开发,而动态注册多用于 Framework 开发。

    2.3.1 静态注册

    image.png

    image.png

    静态注册就是 Java 的 Native 方法通过方法指针来与 JNI 进行关联,如果 Java 的 Native 方法知道它在 JNI 中对应的函数指针,就可以避免上述的缺点,这就是动态注册。

    2.3.2 动态注册

    image.png

    image.png

    image.png

    3. 数据类型的转换

    Java 的数据类型到了 JNI 层就需要转换为 JNI 层的数据类型。

    3.1 基本数据类型的转换

    image.png

    3.2 引用数据类型的转换

    image.png

    image.png

    image.png

    4. 方法签名

    JNI 的方法签名的格式为: (参数签名格式...)返回值签名格式

    image.png

    image.png

    5. 解析 JNIEnv

    • JNIEnv
    • Java VM
    • JNINativeInterface
    • JNIInvokeInterface
    • AttachCurrentThread
    • DetachCurrentThread

    image.png

    image.png

    5.1 jfieldID 和 jmethodID

    image.png

    image.png

    image.png

    5.2 使用 jfieldID 和 jmethodID

    image.png

    image.png

    6. 引用类型

    • 本地引用(Local References)
    • 全局引用(Global References)
    • 弱全局引用(Weak Global References)

    6.1 本地引用

    image.png

    image.png

    • FindClass
    • DeleteLocalRef

    6.2 全局引用

    全局引用和本地引用几乎是相反的,它主要有以下特点:

    image.png

    image.png

    • NewGlobalRef
    • DeleteGlobalRef

    6.3 弱全局引用

    image.png

    image.png

    • NewWeakGlobalRef
    • DeleteWeakGlobalRef
    • IsSameObject
    收起阅读 »

    超详细的android so库的逆向调试

    好久没有写博客了,最近的精力全放在逆向上面。目前也只是略懂皮毛。android java层的逆向比较简单,主要就是脱壳 、反编译源码,通过xposed进行hook。接下来介绍一下,如何去调试hook native层的源码,也就是hook so文件。应用环境准备...
    继续阅读 »

    好久没有写博客了,最近的精力全放在逆向上面。目前也只是略懂皮毛。

    android java层的逆向比较简单,主要就是脱壳 、反编译源码,通过xposed进行hook。
    接下来介绍一下,如何去调试hook native层的源码,也就是hook so文件。

    应用环境准备

    首先,为了方便学习,一上来就hook第三方app难度极大,因此我们自己来创建一个native的项目,自己来hook自己的项目作为学习的练手点。

    创建默认的native application

    打开as,选择File -> new project -> naive c++ 创建包含c++的原生工程。

    hook1.png

    默认的native工程,帮我们实现了stringFromJNI方法,那我们就来探索如何hook这个stringFromJNI,并修改他的值。

    修改stringFromJNI方法,便于调试

    as默认实现的stringFromJNI只有在Activity onCreate的时候调用,为了便于调试,我们增加一个点击事件,每次点击重新调用,并且返回一个随机的值。

    java代码增加如下方法:

    	binding.sampleText.setOnClickListener {
    Log.e("MainActivity", "stringFromJNI")
    binding.sampleText.text = stringFromJNI()
    }

    修改native-lib.cpp代码:

    #include <jni.h>
    #include <string>

    using namespace std;

    int max1(int num1, int num2);
    #define random(x) rand()%(x)

    extern "C" JNIEXPORT jstring JNICALL
    Java_com_noober_naticeapplication_MainActivity_stringFromJNI(
    JNIEnv* env,
    jobject /* this */) {
    int result = max1(random(100), random(100));
    string hello = "Hello from C++";
    string hello2 = hello.append(to_string(result));
    return env->NewStringUTF(hello2.c_str());
    }


    int max1(int num1, int num2)
    {
    // 局部变量声明
    int result;

    if (num1 > num2)
    result = num1;
    else
    result = num2;

    return result;
    }

    修改的代码很简单,相信不会 c++ 的同学也看得懂,就是随机输入两个数,取其中小的那一位拼接在“Hello from C++”后面,并返回。主要目的是让我们每次点击的时候,返回内容可以动态。

    修改androidManifest文件

    在application中增加下面两行代码:

        android:extractNativeLibs="true"
    android:debuggable="true"

    android:debuggable: 让我们可以对apk进行调试,如果是第三方已经打包好了app,我们需要对其manifest文件进行修改,增加这行代码,然后进行重打包,否则无法进行so的调试。

    android:extractNativeLibs: 很多人在进行调试的时候发现ida pro一切正常,但是却一直没有加载我们的libnative -lib.so, 是因为缺少这行代码。如果不加,可能会使so直接自身的base.apk进行加载,导致ida pro无法识别。

    修改CMakeLists.txt

    在cmakelists中增加下面代码。so文件生成路径,这样编译之后就可以在main-cpp-jniLibs目录下找到生产的so文件。

    set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR}/jniLibs/${ANDROID_ABI})

    编译运行,获取so

    上述工作做好之后,直接编译运行,同时会生成4个so文件,我们取手机运行时对应使用的那个so进行hook。
    我这边使用的是arm64-v8a目录下的libnative-lib.so。

    hook2.png

    hook环境准备

    • 系统:windows 10 64位
    • 工具ida pro 7.5
    • java8环境
    • android sdk tools和adb工具
    • arm64-v8a目录下的libnative-lib.so
    • android 真机

    使用ida pro进行hook

    adb与手机的准备

    1. 首先找到ida pro的dbgsrv文件夹,里面有很多server文件

    hook3.png

    64代表的含义是64位,否则就是32位,我们根据我们需要调试的so的指令集进行选择。因为我这边调试的是arm64-v8a,这里我们就选择android_server64的文件。连接真机后,打开cmd,输入以下指令:

    adb push "\\Mac\Home\Desktop\IDA PRO 7.5 (x86, x64, ARM, ARM64)\dbgsrv\android_server64"  /data/local/tmp
    1. 如果是真机,则需要输入su,模拟器不需要

       #真机
      su
    2. 修改权限

       chmod 777 /data/local/tmp/android_server64
    3. 运行

       /data/local/tmp/android_server64

    hook9.png

    1. 新打开一个cmd,在本地执行adb 做端口转发

       adb forward tcp:23946 tcp:23946

    ida pro的工作准备

    1. 打开ida pro,因为我们的so是64位的,所以打开ida64.exe。点击new,选择libnative-lib.so。

    2. 选择debugger-select debugger

    hook4.png

    1. 选择Remote ARM Linux/Android debugger

    hook5.png

    1. 点击debugger-Debugger options

    勾选Suspend on process entry point ,也就是在断点处进行挂起暂停

    hook6.png

    1. 点击debugger-Process options

    填写hostname为localhost

    hook10.png

    1. 找到exports标签,ctrl+f,搜索java关键字,找到我们要hook的函数。

    hook7.png

    1. 双击打开,按F5,进行反汇编操作。这样就可以看到反汇编之后的c ++代码了。然后我们随便加上断点进行调试。

    hook8.png

    1. 执行adb命令,进入调试状态,也就是打开我们要调试的app的启动activity,我这边如下:

       adb shell am start -D -n com.noober.naticeapplication/com.noober.naticeapplication.MainActivity
    2. 点击debugger-Attach to process

    选择我们需要调试的进程。

    hook11.png

    1. adb 执行如下命令,关联运行的so与本地要调试的so。

      jdb -connect com.sun.jdi.SocketAttach:hostname=127.0.0.1,port=8700
    2. 此时ida卡在libc.so的位置,点击继续执行,弹出如下界面,关联so到本地,选择same。如果没有弹出则需要通过快捷键ctrl+s, 打开所有已经加载的so,找到我们的libnative-lib.so

    hook14.png

    1. 此时就会自动进入断点。

    hook1.png

    使用ida pro进行调试

    ida pro 常用调试快捷键

    • F2下断点
    • F7单步步入
    • F8单步步过
    • F9执行到下个断点
    • G调到函数地址
    • Debugger-debugger windows-locals 查看变量

    进行调试

    1. 简单分析反汇编代码,我们发现返回值是v5,通过f8,执行到return的上一行。打开locals, 获取所有变量的值。

    locals.png

    1. 复制bytes的地址0x7FFE2CDEB9LL,切换到代码界面,输入快捷键g,输入地址跳转。这样我们便从内存中得到了数据结果,可以看出本次返回的值就是"Hello from c++89"

    result.png

    1. 当然我们也可以在locals中直接修改值,这样就达到了我们hook so动态修改数据的目的。

    收起阅读 »

    Android 自动化交互实践

    Android 自动化交互可以代替人工完成重复性的工作,包括通过自动操作 App 进行黑盒测试和第三方 App 的自动运行。常见的自动化交互包含启动 App、view 的点击、拖拽和文本输入等。随着 App 安防能力的提升,要想实现完整流程的自动化交互变的越来...
    继续阅读 »

    Android 自动化交互可以代替人工完成重复性的工作,包括通过自动操作 App 进行黑盒测试和第三方 App 的自动运行。常见的自动化交互包含启动 App、view 的点击、拖拽和文本输入等。随着 App 安防能力的提升,要想实现完整流程的自动化交互变的越来越困难,本文主要探讨目前常见的自动化交互方案以及不同方案的优劣和应用场景。

    1 传统执行脚本方案

    ADB 是 Google 提供的能够和 Android 设备进行交互的命令行工具,我们可以编写脚本按照事先设计好的顺序,一个一个执行各个事件。ADB 执行操作需要事先获取界面元素的坐标(获取坐标方法可以利用 uiautomator 或者 dump xml 的方法,这里不是讨论的重点),然后把坐标传入作为命令行参数。

    adb shell input tap 100 500

    上面命令是模拟点击屏幕坐标为(100, 500)处的控件。

    adb shell input swipe 100 500 200 600

    上面命令是模拟手指在屏幕上向右下方滑动的一个操作。

    adb shell input keyevent "KEYCODE_BACK"

    上面命令模拟返回按键的点击。

    一次完整的自动化交互流程可由上面一系列命令顺序执行。

    ADB 脚本方式的优点

    1. 实现简单,只需要获取目标元素的坐标等简单信息即可完成相关操作
    2. 可以实现对 webview 的自动化交互\

    ADB 脚本方式的缺点

    1. 灵活度不够,依赖于写死的坐标,App 界面变更引起的 view 位置变换会让脚本中相关命令无法执行,需要重新分析页面坐标
    2. 需要建立 ADB 链接或套接字链接,交互过程中网络状况的变化会影响自动化交互效果\

    ADB 脚本方式应用场景

    1. 交互简单、迭代频率低,安防级别比较低的 App
    2. webview 页面,flutter 开发的 App

    2 Android 原生方法实现自动化交互

    我们可以借助各种插件化框架来控制 App 页面的界面元素,其中一种思路就是在插件中借助 ActivityLifecycleCallbacks 来监听各个 activity 的生命周期。

    public class MyApplication extends Application {
    private static final String TAG = "MyApplication";
    //声明一个监听Activity们生命周期的接口
    private ActivityLifecycleCallbacks activityLifecycleCallbacks = new ActivityLifecycleCallbacks() {
    /**
    * application下的每个Activity声明周期改变时,都会触发以下的函数。
    */
    @Override
    public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
    //如何区别参数中activity代表你写的哪个activity。
    if (activity.getClass() == MainActivity.class)
    Log.d(TAG, "MainActivityCreated.");
    else if(activity.getClass()== SecondActivity.class)
    Log.d(TAG, "SecondActivityCreated.");
    }

    @Override
    public void onActivityStarted(Activity activity) {
    Log.d(TAG, "onActivityStarted.");
    }

    @Override
    public void onActivityResumed(Activity activity) {
    Log.d(TAG, "onActivityResumed.");
    }

    @Override
    public void onActivityPaused(Activity activity) {
    Log.d(TAG, "onActivityPaused.");
    }

    @Override
    public void onActivityStopped(Activity activity) {
    Log.d(TAG, "onActivityStopped.");
    }

    @Override
    public void onActivitySaveInstanceState(Activity activity, Bundle outState) {
    }

    @Override
    public void onActivityDestroyed(Activity activity) {
    Log.d(TAG, "onActivityDestroyed.");
    }
    };

    @Override
    public void onCreate() {
    super.onCreate();
    //注册自己的Activity的生命周期回调接口。![Alt text](./WechatIMG59.png)

    registerActivityLifecycleCallbacks(activityLifecycleCallbacks);
    }

    @Override
    public void onTerminate() {
    //注销这个接口。
    unregisterActivityLifecycleCallbacks(activityLifecycleCallbacks);
    super.onTerminate();
    }
    }

    监听到 activity 的活动后,可以借助 uiautomator 分析 activity 界面元素的 viewId 以及属性,不同情况的界面 view 可以采用不同的自动化方法。

    2.1 简单 view 的处理方式

    如下图: image.png

    像这类 view,可以直接获取到 resource id ,并且确认可点击属性为 true,操作方式比较简单, 可以在监听到的 activity 生命周期中执行如下操作:

    int fl_btn = activity.getResources().getIdentifier("dashboard_title",         "id", "com.android.settings");
    View v = activity.findViewById(fl_btn);

    v.performClick();

    2.2 隐藏属性的 view 的处理方式

    在一些对 view 的属性进行隐藏,特别是利用 React Native 等混合开发的页面,上面的方法不再生效,如下图所示的 view: image.png

    如图,选中的 viewgroup 及其子 view 的 clickable 属性均为 false,并且无法获取到 view 的 resource id,这时候可以利用图中 dump 出的布局信息,借助 Xpath 元素定位工具来获取到界面的 view,由于这些 view 的点击属性为 false,因此通过调用 performClick 来实现点击的方法已经无效,此时考虑在 click 更底层的与触摸事件传递相关的比较重要的类:MotionEvent, MotionEvent 可以仿真几乎所有的交互事件,包括点击,滑动,双指操作等。以单击为例:

        private void simulateClick(View view, float x, float y) {
    long time = SystemClock.uptimeMillis();//必须是 SystemClock.uptimeMillis()。

    MotionEvent downEvent = MotionEvent.obtain(time, time, MotionEvent.ACTION_DOWN, x, y, 0);

    time += 500;

    MotionEvent upEvent = MotionEvent.obtain(time, time, MotionEvent.ACTION_UP, x, y, 0);

    view.onTouchEvent(downEvent);
    view.onTouchEvent(upEvent);
    }

    如果是滑动操作,可以在起始位置中间添加若干 ACTION_MOVE 类型的 MotionEvent. 综上所述,借助系统原生方法时间交互自动化的优缺点大致如下:

    借助插件框架实现自动化交互的优点

    1. 可维护性强,因为可以直接获取到界面的 view 对象,因此即使页面布局发生变化,只要 view 还存在,就不需要对代码进行修改
    2. 仿真效果更好,比脚本方式更接近真人操作

    借助插件框架实现自动化交互的不足

    1. 对 webview、flutter 框架 App 支持不好

    应用场景

    1. 版本迭代频繁的 App
    2. 非 flutter 框架开发的 App

    上面分析了两种常用的模拟真实用户进行自动化操作的方法,脚本方式和调用原生方法的方式,这两种方法基本上可以完成简单的交互流程,在此基础上,我们还可以去深究一些更深层次的交互实现,比如自动化过各种验证等,也可以基于这两种方法来完成。

    收起阅读 »

    Android 面试准备进行曲-Android 基础知识

    基础部分Activity生命周期onCreate() -> onStart() -> onResume() -> onPause() -> onStop() -> onDetroy() 图片简要说明启动 onCreate...
    继续阅读 »

    基础部分

    Activity生命周期

    onCreate() -> onStart() -> onResume() -> onPause() -> onStop() -> onDetroy()

    11.webp 图片简要说明

    • 启动 onCreate -> onStart -> onResume
    • 被覆盖/ 回到当前界面 onPause -> / -> onResume
    • 在后台 onPause -> onStop
    • 后退回到 onRestart -> onStart -> onResume
    • 退出 onPause -> onStop -> onDestory

    另外说一下 其他两个比较重要的 方法

    • onSaveInstanceState : (1)Activity被覆盖或退居后台,系统资源不足将其杀死,此方法会被调用;(2)在用户改变屏幕方向时,此方法会被调用 (系统先销毁当前的Activity,然后再重建一个新的,调用此方法时,我们可以保存一些临时数据);(3)在当前Activity跳转到其他Activity或者按Home键回到主屏,自身退居后台时, 系统调用此方法是为了保存当前窗口各个View组件的状态。onSaveInstanceState该方法调用在onStop之前,但和onPause没有时序关系。 不过一般onSaveInstanceState() 保存临时数据为主,而 onPause() 适用于对数据的持久化保存。

    • onRestoreInstanceState : onRestoreInstanceState的调用顺序是在onStart之后。主要用于 恢复一些onSaveInstanceState 方法中保存的数据

    onStart()和onResume()/onPause()和onStop()的区别

    onStart()与onStop()是从Activity是否可见这个角度调用的 onResume()和onPause()是从Activity是否显示在前台这个角度来回调的 在实际使用没其他明显区别。

    Activity A 跳转 Activity B的问题

    Activity A启动另一个Activity B会回调的方法: Activity A的onPause() -->Activity B的onCreate()-->onStart()-->onResume()-->Activity A的onStop();

    如果Activity B是完全透明的,则最后不会调用Activity A的onStop();如果是对话框Activity,则最后不会调用Activity A的onStop();

    Activity 启动流程

    22.webp 调用startActivity()后经过重重方法会转移到ActivityManagerService的startActivity(),并通过一个IPC回到ActivityThread的内部类ApplicationThread中,并调用其scheduleLaunchActivity()将启动Activity的消息发送并交由Handler H处理。 Handler H对消息的处理会调用handleLaunchActivity()->performLaunchActivity()得以完成Activity对象的创建和启动。

    参考地址:Activity启动流程

    Fragment 生命周期

    Fragment从创建到销毁整个生命周期中涉及到的方法依次为: onAttach()->onCreate()-> onCreateView()->onActivityCreated()->onStart()->onResume()->onPause()->onStop()->onDestroyView()->onDestroy()->onDetach(), 其中和Activity有不少名称相同作用相似的方法,而不同的方法有:

    onAttach():当Fragment和Activity建立关联时调用

    onCreateView():当Fragment创建视图时调用

    onActivityCreated():当与Fragment相关联的Activity完成onCreate()之后调用

    onDestroyView():在Fragment中的布局被移除时调用

    onDetach():当Fragment和Activity解除关联时调用

    Activity 与 Fragment 通信

    1. 对于Activity和Fragment之间的相互调用

    (1)Activity调用Fragment 直接调用就好,Activity一般持有Fragment实例,或者通过Fragment id 或者tag获取到Fragment实例 (2)Fragment调用Activity 通过activity设置监听器到Fragment进行回调,或者是直接在fragment直接getActivity获取到activity实例

    1. Activity如果更好的传递参数给Fragment

    如果直接通过普通方法的调用传递参数的话,那么在fragment回收后恢复不能恢复这些数据。google给我们提供了一个方法 setArguments(bundle) 可以通过这个方法传递参数给fragment,然后在fragment中用getArguments获取到。能保证在fragment销毁重建后还能获取到数据

    Service 启动及生命周期

    service 启动方式

    • 不可通信Service 。 通过startService()启动,不跟随调用者关闭而关闭
    • 可通信Service 。 通过bindService()方式进行启动。跟随调用者关闭而关闭

    以上两种Servcie 默认都存在于调用者一样的进程中,如果想要设置不一样的进程中则需要在 AndroidManifest.xml 中 配置 android:process = Remote 属性

    生命周期 :

    • 通过startService()这种方式启动的service,生命周期 :startService() --> onCreate()--> onStartConmon()--> onDestroy()。

    需要注意几个问题

    1. 当我们通过startService被调用以后,多次在调用startService(),onCreate()方法也只会被调用一次,
    2. 而onStartConmon()会被多次调用当我们调用stopService()的时候,onDestroy()就会被调用,从而销毁服务。
    2. 当我们通过startService启动时候,通过intent传值,在onStartConmon()方法中获取值的时候,一定要先判断intent是否为null。
    • 通过bindService()方式进行绑定,这种方式绑定service,生命周期走法:bindService-->onCreate()-->onBind()-->unBind()-->onDestroy()

    bindService的优点 这种方式进行启动service好处是更加便利activity中操作service,比如加入service中有几个方法,a,b ,如果要在activity中调用,在需要在activity获取ServiceConnection对象,通过ServiceConnection来获取service中内部类的类对象,然后通过这个类对象就可以调用类中的方法,当然这个类需要继承Binder对象

    Service 通信方式

    1. 创建继承Binder的内部类,重写Service的onBind方法 返回 Binder 子类,重写ServiceConnection,onServiceConnected时调用逻辑方法 绑定服务。

    2. 通过接口Iservice调用Service方法

    IntentService对比Service

    IntentService是Service的子类,是一个异步的,会自动停止的服务,很好解决了传统的Service中处理完耗时操作忘记停止并销毁Service的问题

    优点:

    • 所有请求处理完成后,IntentService会自动停止,无需调用stopSelf()方法停止
    • IntentService不会阻塞UI线程,而普通Serveice会导致ANR异常
    • Intentservice若未执行完成上一次的任务,将不会新开一个线程,是等待之前的任务完成后,再执行新的任务,等任务完成后再次调用stopSelf()
    • 为Service的onBind()提供默认实现,返回null;

    提高service的优先级

    1. 在AndroidManifest.xml文件中对于intent-filter可以通过android:priority = “1000”这个属性设置最高优先级,1000是最高值,如果数字越小则优先级越低,同时实用于广播。
    2. onStartCommand方法,手动返回START_STICKY。
    3. 监听系统广播判断Service状态。
    4. Application加上Persistent属性。
    5. 在onStartCommand里面调用 startForeground()方法把Service提升为前台进程级别,然后再onDestroy里面调用stopForeground ()方法。
    6. 在onDestroy方法里发广播重启service。

    service +broadcast 方式,就是当service走ondestory的时候,发送一个自定义的广播,当收到广播的时候,重新启动service。

    延伸:进程保活(毒瘤)

    黑色保活:不同的app进程,用广播相互唤醒(包括利用系统提供的广播进行唤醒)
    白色保活:启动前台Service
    灰色保活:利用系统的漏洞启动前台Service

    黑色保活 所谓黑色保活,就是利用不同的app进程使用广播来进行相互唤醒。举个3个比较常见的场景: 场景1:开机,网络切换、拍照、拍视频时候,利用系统产生的广播唤醒app 场景2:接入第三方SDK也会唤醒相应的app进程,如微信sdk会唤醒微信,支付宝sdk会唤醒支付宝。由此发散开去,就会直接触发了下面的 场景3 场景3:假如你手机里装了支付宝、淘宝、天猫、UC等阿里系的app,那么你打开任意一个阿里系的app后,有可能就顺便把其他阿里系的app给唤醒了。(只是拿阿里打个比方,其实BAT系都差不多)

    白色保活 白色保活手段非常简单,就是调用系统api启动一个前台的Service进程,这样会在系统的通知栏生成一个Notification,用来让用户知道有这样一个app在运行着,哪怕当前的app退到了后台。如下方的LBE和QQ音乐这样:

    灰色保活 灰色保活,这种保活手段是应用范围最广泛。它是利用系统的漏洞来启动一个前台的Service进程,与普通的启动方式区别在于,它不会在系统通知栏处出现一个Notification,看起来就如同运行着一个后台Service进程一样。这样做带来的好处就是,用户无法察觉到你运行着一个前台进程(因为看不到Notification),但你的进程优先级又是高于普通后台进程的。那么如何利用系统的漏洞呢,大致的实现思路和代码如下: 思路一:API < 18,启动前台Service时直接传入new Notification(); 思路二:API >= 18,同时启动两个id相同的前台Service,然后再将后启动的Service做stop处理 熟悉Android系统的童鞋都知道,系统出于体验和性能上的考虑,app在退到后台时系统并不会真正的kill掉这个进程,而是将其缓存起来。打开的应用越多,后台缓存的进程也越多。在系统内存不足的情况下,系统开始依据自身的一套进程回收机制来判断要kill掉哪些进程,以腾出内存来供给需要的app。这套杀进程回收内存的机制就叫 Low Memory Killer ,它是基于Linux内核的 OOM Killer(Out-Of-Memory killer)机制诞生。

    思路二:后台播放无声音频,模拟前台服务,提高等级

    思路三:1像素界面

    思路四:在Activity的onDestroy()通过发送广播,并在广播接收器的onReceive()中启动Service

    进程的重要性,划分5级: 前台进程 (Foreground process) 可见进程 (Visible process) 服务进程 (Service process) 后台进程 (Background process) 空进程 (Empty process)

    什么是oom_adj?它是linux内核分配给每个系统进程的一个值,代表进程的优先级,进程回收机制就是根据这个优先级来决定是否进行回收。对于oom_adj的作用,你只需要记住以下几点即可: 进程的oom_adj越大,表示此进程优先级越低,越容易被杀回收;越小,表示进程优先级越高,越不容易被杀回收 普通app进程的oom_adj>=0,系统进程的oom_adj才可能<0 有些手机厂商把这些知名的app放入了自己的白名单中,保证了进程不死来提高用户体验

    Broadcast注册方式与区别

    Broadcast广播,注册方式主要有两种.

    • 第一种是静态注册,也可成为常驻型广播,这种广播需要在Androidmanifest.xml中进行注册,这中方式注册的广播,不受页面生命周期的影响,即使退出了页面,也可以收到广播这种广播一般用于想开机自启动啊等等,由于这种注册的方式的广播是常驻型广播,所以会占用CPU的资源。

    • 第二种是动态注册,而动态注册的话,是在代码中注册的,这种注册方式也叫非常驻型广播,收到生命周期的影响,退出页面后,就不会收到广播,我们通常运用在更新UI方面。这种注册方式优先级较高。最后需要解绑,否则会内存泄露

    广播是分为有序广播和无序广播。

    Broadcast 有几种形式

    普通广播:一种完全异步执行的广播,在广播发出之后,所有的广播接收器几乎都会在同一时刻接收到这条广播消息,因此它们接收的先后是随机的。

    有序广播:一种同步执行的广播,在广播发出之后,同一时刻只会有一个广播接收器能够收到这条广播消息,当这个广播接收器中的逻辑执行完毕后,广播才会继续传递,所以此时的广播接收器是有先后顺序的,且优先级(priority)高的广播接收器会先收到广播消息。有序广播可以被接收器截断使得后面的接收器无法收到它。

    本地广播:发出的广播只能够在应用程序的内部进行传递,并且广播接收器也只能接收本应用程序发出的广播。

    粘性广播:这种广播会一直滞留,当有匹配该广播的接收器被注册后,该接收器就会收到此条广播。

    部分Broadcast 之间的区别

    BroadcastReceiver: 是可以跨应用广播,利用Binder机制实现,支持动态和静态两种方式注册方式。

    LocalBroadcastReceiver: 是应用内广播,利用Handler实现,利用了IntentFilter的match功能,提供消息的发布与接收功能,实现应用内通信,效率和安全性比较高,仅支持动态注册。

    OrderedBroadcast : 调用sendOrderedBroadcast()发送,接收者会按照priority优先级从大到小进行排序,如优先级相同,先注册,先处理 广播接收者还能对广播进行截断和修改

    ContentProvider

    作为四大组件之一,ContentProvider主要负责存储和共享数据。与文件存储、SharedPreferences存储、SQLite数据库存储这几种数据存储方法不同的是,后者保存下的数据只能被该应用程序使用,而前者可以让不同应用程序之间进行数据共享,它还可以选择只对哪一部分数据进行共享,从而保证程序中的隐私数据不会有泄漏风险。

    app中有几个Context对象

    先看一下源码的解释

    /**
    * Interface to global information about an application environment. This is
    * an abstract class whose implementation is provided by
    * the Android system. It
    * allows access to application-specific resources and classes, as well as
    * up-calls for application-level operations such as launching activities,
    * broadcasting and receiving intents, etc.
    */
    public abstract class Context {
    /**
    * File creation mode: the default mode, where the created file can only
    * be accessed by the calling application (or all applications sharing the
    * same user ID).
    * <a href="http://www.jobbole.com/members/heydee@qq.com">@see</a> #MODE_WORLD_READABLE
    * <a href="http://www.jobbole.com/members/heydee@qq.com">@see</a> #MODE_WORLD_WRITEABLE
    */
    public static final int MODE_PRIVATE = 0x0000;

    public static final int MODE_WORLD_WRITEABLE = 0x0002;

    public static final int MODE_APPEND = 0x8000;

    public static final int MODE_MULTI_PROCESS = 0x0004;

    }

    源码中的注释是这么来解释Context的:Context提供了关于应用环境全局信息的接口。它是一个抽象类,它的执行被Android系统所提供。它允许获取以应用为特征的资源和类型,是一个统领一些资源(应用程序环境变量等)的上下文。就是说,它描述一个应用程序环境的信息(即上下文);是一个抽象类,Android提供了该抽象类的具体实现类;通过它我们可以获取应用程序的资源和类(包括应用级别操作,如启动Activity,发广播,接受Intent等)。

    3.webp 从上面的关系图我们已经可以得出答案了,在应用程序中Context的具体实现子类就是:Activity,Service,Application。那么Context数量=Activity数量+Service数量+1。当然如果你足够细心,可能会有疑问:我们常说四大组件,这里怎么只有Activity,Service持有Context,那Broadcast Receiver,Content Provider呢?Broadcast Receiver,Content Provider并不是Context的子类,他们所持有的Context都是其他地方传过去的,所以并不计入Context总数。

    Application Context 启动问题

    如果我们用ApplicationContext去启动一个LaunchMode为standard的Activity的时候会报错android.util.AndroidRuntimeException: Calling startActivity from outside of an Activity context requires the FLAG_ACTIVITY_NEW_TASK flag. Is this really what you want?这是因为非Activity类型的Context并没有所谓的任务栈,所以待启动的Activity就找不到栈了。解决这个问题的方法就是为待启动的Activity指定FLAG_ACTIVITY_NEW_TASK标记位,这样启动的时候就为它创建一个新的任务栈,而此时Activity是以singleTask模式启动的。所有这种用Application启动Activity的方式不推荐使用,Service同Application。

    如何获取 Context对象

    1:View.getContext,返回当前View对象的Context对象,通常是当前正在展示的Activity对象。

    2:Activity.getApplicationContext,获取当前Activity所在的(应用)进程的Context对象,通常我们使用Context对象时,要优先考虑这个全局的进程Context。

    4:Activity.this 返回当前的Activity实例,如果是UI控件需要使用Activity作为Context对象,但是默认的Toast实际上使用ApplicationContext也可以。

    如何避免因为Context 造成内存泄漏

    一般Context造成的内存泄漏,几乎都是当Context销毁的时候,却因为被引用导致销毁失败,而Application的Context对象可以理解为随着进程存在的,所以我们总结出使用Context的正确姿势:

    1:当Application的Context能搞定的情况下,并且生命周期长的对象,优先使用Application的Context。

    2:不要让生命周期长于Activity的对象持有到Activity的引用。

    3:尽量不要在Activity中使用非静态内部类,因为非静态内部类会隐式持有外部类实例的引用,如果使用静态内部类,将外部实例引用作为弱引用持有。

    getApplication()和getApplicationContext() 区别

    其实我们通过程序打印 两个方法获得的对象 Application本身就是一个Context,所以这里获取getApplicationContext()得到的结果就是Application本身的实例。那么问题来了,既然这两个方法得到的结果都是相同的,那么Android为什么要提供两个功能重复的方法呢?

    实际上这两个方法在作用域上有比较大的区别。getApplication()方法的语义性非常强,一看就知道是用来获取Application实例的,但是这个方法只有在Activity和Service中才能调用的到。那么也许在绝大多数情况下我们都是在Activity或者Service中使用Application的,但是如果在一些其它的场景,比如BroadcastReceiver中也想获得Application的实例,这时就可以借助getApplicationContext()方法了。

    理解Activity,View,Window三者关系

    Activity像一个工匠(控制单元),Window像窗户(承载模型),View像窗花(显示视图)LayoutInflater像剪刀,Xml配置像窗花图纸。 1:Activity构造的时候会初始化一个Window,准确的说是PhoneWindow。 2:这个PhoneWindow有一个“ViewRoot”,这个“ViewRoot”是一个View或者说ViewGroup,是最初始的根视图。 3:“ViewRoot”通过addView方法来一个个的添加View。比如TextView,Button等 4:这些View的事件监听,是由WindowManagerService来接受消息,并且回调Activity函数。比如onClickListener,onKeyDown等。

    四种LaunchMode及其使用场景

    此处延伸:栈(First In Last Out)与队列(First In First Out)的区别 栈与队列的区别:

    队列先进先出,栈先进后出 对插入和删除操作的"限定"。 栈是限定只能在表的一端进行插入和删除操作的线性表。 队列是限定只能在表的一端进行插入和在另一端进行删除操作的线性表。 遍历数据速度不同

    standard 模式 这是默认模式,每次激活Activity时都会创建Activity实例,并放入任务栈中。使用场景:大多数Activity。 singleTop 模式 如果在任务的栈顶正好存在该Activity的实例,就重用该实例( 会调用实例的 onNewIntent() ),否则就会创建新的实例并放入栈顶,即使栈中已经存在该Activity的实例,只要不在栈顶,都会创建新的实例。使用场景如新闻类或者阅读类App的内容页面。 singleTask 模式 如果在栈中已经有该Activity的实例,就重用该实例(会调用实例的 onNewIntent() )。重用时,会让该实例回到栈顶,因此在它上面的实例将会被移出栈。如果栈中不存在该实例,将会创建新的实例放入栈中。使用场景如浏览器的主界面。不管从多少个应用启动浏览器,只会启动主界面一次,其余情况都会走onNewIntent,并且会清空主界面上面的其他页面。 singleInstance 模式 在一个新栈中创建该Activity的实例,并让多个应用共享该栈中的该Activity实例。一旦该模式的Activity实例已经存在于某个栈中,任何应用再激活该Activity时都会重用该栈中的实例( 会调用实例的 onNewIntent() )。其效果相当于多个应用共享一个应用,不管谁激活该 Activity 都会进入同一个应用中。使用场景如闹铃提醒,将闹铃提醒与闹铃设置分离。singleInstance不要用于中间页面,如果用于中间页面,跳转会有问题,比如:A -> B (singleInstance) -> C,完全退出后,在此启动,首先打开的是B。

    数据存储

    Android中提供哪些数据持久存储的方法?

    File 文件存储:写入和读取文件的方法和 Java中实现I/O的程序一样。

    SharedPreferences存储:一种轻型的数据存储方式,常用来存储一些简单的配置 信息,本质是基于XML文件存储key-value键值对数据。

    SQLite数据库存储:一款轻量级的关系型数据库,它的运算速度非常快,占用资源很少,在存储大量复杂的关系型数据的时可以使用。

    ContentProvider:四大组件之一,用于数据的存储和共享,不仅可以让不同应用程序之间进行数据共享,还可以选择只对哪一部分数据进行共享,可保证程序中的隐私数据不会有泄漏风险。

    SharePreferences 相关问题

    1. SharePreferences是一种轻型的数据存储方式,适用于存储一些简单的配置信息,如int、string、boolean、float和long。由于系统对SharedPreferences的读/写有一定的缓存策略,即在内存中有一份该文件的缓存,因此在多进程模式下,其读/写会变得不可靠,甚至丢失数据。

    2. context.getSharedPreferences()开始追踪的话,可以去到ContextImpl的getSharedPreferences(),最终发现SharedPreferencesImpl这个SharedPreferences的实现类,在代码中可以看到读写操作时都有大量的synchronized,因此它是线程安全

    3. 由于进程间是不能内存共享的,每个进程操作的SharedPreferences都是一个单独的实例,这导致了多进程间通过SharedPreferences来共享数据是不安全的,这个问题只能通过多进程间其它的通信方式或者是在确保不会同时操作SharedPreferences数据的前提下使用SharedPreferences来解决。

    SharePreferences 注意事项及优化办法

    1. 第一次getSharePreference会读取磁盘文件,异步读取,写入到内存中,后续的getSharePreference都是从内存中拿了。
    2. 第一次读取完毕之前 所有的get/set请求都会被卡住 等待读取完毕后再执行,所以第一次读取会有ANR风险。
    3. 所有的get都是从内存中读取。
    4. 提交都是 写入到内存和磁盘中 。apply跟commit的区别在于

    apply 是内存同步 然后磁盘异步写入任务放到一个单线程队列中 等待调用。方法无返回 即void commit 内存同步 只不过要等待磁盘写入结束才返回 直接返回写入成功状态 true or false 5. 从 Android N 开始, 不再支持 MODE_WORLD_READABLE & MODE_WORLD_WRITEABLE. 一旦指定, 会抛异常 。也不要用MODE_MULTI_PROCESS 迟早被放弃。 8.每次commit/apply都会把全部数据一次性写入到磁盘,即没有增量写入的概念 。 所以单个文件千万不要太大 否则会严重影响性能。

    建议用微信的第三方MMKV来替代SharePreference

    SP源码解析

    SQLite 相关问题

    • 使用事务做批量操作:

    使用SQLiteDatabase的beginTransaction()方法开启一个事务,将批量操作SQL语句转化成SQLiteStatement并进行批量操作,结束后endTransaction()

    • 及时关闭Cursor,避免内存泄漏

    • 耗时操作异步化:数据库的操作属于本地IO,通常比较耗时,建议将这些耗时操作放入异步线程中处理

    • ContentValues的容量调整:ContentValues内部采用HashMap来存储Key-Value数据,ContentValues初始容量为8,扩容时翻倍。因此建议对ContentValues填入的内容进行估量,设置合理的初始化容量,减少不必要的内部扩容操作

    • 使用索引加快检索速度:对于查询操作量级较大、业务对要求查询要求较高的推荐使用索引


    收起阅读 »

    性能优化2 - 内存、启动速度、卡顿、布局优化

    性能优化是在充分了解项目+java、android基础知识牢固的基础上的。内存优化基础知识回顾(看前面文章JVM详解):jVM内存模型,除了程序计数器以外,别的都会出现 OOM。JAVA对象的生命周期,创建、运行、死亡。GC对象可回收的判定:可达性分析。GC ...
    继续阅读 »

    性能优化是在充分了解项目+java、android基础知识牢固的基础上的。

    内存优化

    基础知识回顾(看前面文章JVM详解):

    jVM内存模型,除了程序计数器以外,别的都会出现 OOM。

    image.png

    JAVA对象的生命周期,创建、运行、死亡。
    GC对象可回收的判定:可达性分析。GC root(除了堆里的对象,虚拟机栈里的引用、方法区里的引用)。
    强软弱虚四种引用。
    GC回收算法:复制算法、标记清楚算法、标记整理算法。

    image.png

    App内存组成以及限制

    Android给每个App分配一个VM,让App运行在dalvik上,这样即使App崩溃也不会影响到系统。系统给VM分配了一定的内存大小,App可以申请使用的内存大小不能超过此硬性逻辑限制,就算物理内存富余,如果应用超出VM最大内存,就会出现内存溢出crash

    由程序控制操作的内存空间在heap上,分java heapsizenative heapsize

    • Java申请的内存在vm heap上,所以如果java申请的内存大小超过VM的逻辑内存限制,就会出现内存溢出的异常。
    • native层内存申请不受其限制,native层受native process对内存大小的限制

    总结:
    app运行在虚拟机上,手机给虚拟机分配内存是固定的,超出就oom。
    分配的内存大部分都是在堆上。分为java堆和native层的堆。
    java层的堆超过VM给的就oom。理论上native层无限制,但是底层实现native process对内存大小是有限制的。

    查看系统给App分配的内存限制

    不同手机给app分配的内存大小其实是不一样大的。

    1. 通过cmd指令 adb shell cat /system/build.prop

    image.png

    1. 通过代码 activityManager.getMemoryClass();
    ActivityManager activityManager =(ActivityManager)context.getSystemService(Context.ACTIVITY_SERVICE)

    activityManager.getMemoryClass();//以m为单位

    这些限制其实在 AndroidRuntime.cpp的startVm里,我们改不了,手机厂商可以改

    image.png

    Android低内存杀进程机制

    默认五个级别:空进程、后台进程、服务进程、可见进程、前台进程
    所以在保活里有一个做法就是给app提升优先级,给他整成前台、可见这样的。

    AMS里有一个oom_adj,会给各个进程进行评分,数字越大越容易被回收,前台进程是0,系统进程是负数,别的是正数。 image.png

    内存三大问题

    1、内存抖动
    profiler -> 内存波动图形呈 锯齿张、GC导致卡顿。
    原因是内存碎片很严重。因为android虚拟机的GC算法是标记清楚算法,所以频繁的申请内存、释放内存会让内存碎片化很严重,连续的内存越来越少,让GC非常频繁,导致了内存抖动。案例:在自定义View onDraw()里new对象

    2、内存泄漏 在当前应用周期内不再使用的对象被GC Roots引用,导致不能回收,使实际可使用内存变小。内存泄露如果越来越严重的话,最终会导致OOM。案例:context被长生命周期的东西引用。没有释放listener

    3、内存溢出 即OOM,OOM时会导致程序异常。Android设备出厂以后,java虚拟机对单个应用的最大内存分配就确定下来了,超出这个值就会OOM。案例:加载大图、内存泄露、内存抖动

    除了程序计数器以外 别的JVM部分都会OOM
    image.png

    常见分析工具

    1. Memory Analyzer Tools --- MAT

    MAT是Eclipse下的内存分析工具。
    用法:用cmd指令或者android studio 自带的profiler 截取一段数据,然后下载下来。得到一个.hprof

    image.png

    image.png
    然后用AMT打开,就能看预测泄露地方,有一个图表,基本没用。还有看哪个线程调用这个对象、深浅堆等等。挺难用的。

    image.png

    1. android studio自带的profiler

    谷歌官网:developer.android.google.cn/studio/prof…

    官网超级详细,还有视频,直接看官网的就行。 image.png

    选中一段区域,就能查看这段区域内存被具体哪个对象用了多少等等。
    我感觉还是用来看大势的,大的内存上涨、下落、起伏图。

    1. LeakCanary

    超级推荐LeakCanary!!!永远滴神
    具体内存泄露的检测,细节还得用LeakCanary。

    集成:
    build.gradle里添,跟集成第三方一样。
    debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.3'

    然后直接跑APP,跑完以后一顿点,点完以后会给推送。写了发现几个对象有泄露。点一下推送蓝会自动下载下来。

    image.png

    比如这就是我的问题,期初我认为是activity的context没有释放,其实是在dialog里使用了动画animation,但是动画的listener没有释放。 image.png

    Android内存泄漏常见场景以及解决方案

    1、资源性对象未关闭
    对于资源性对象不再使用时,应该立即调用它的close()函数,将其关闭,然后再置为null。例如Bitmap 等资源未关闭会造成内存泄漏,此时我们应该在Activity销毁时及时关闭。

    2、注册对象未注销
    例如BraodcastReceiver、EventBus未注销造成的内存泄漏,我们应该在Activity销毁时及时注销。

    3、类的静态变量持有大数据对象
    尽量避免使用静态变量存储数据,特别是大数据对象,建议使用数据库存储。

    4.单例造成的内存泄漏
    优先使用Application的Context,如需使用Activity的Context,可以在传入Context时使用弱引用进行封 装,然后,在使用到的地方从弱引用中获取Context,如果获取不到,则直接return即可。

    5、非静态内部类的静态实例
    该实例的生命周期和应用一样长,这就导致该静态实例一直持有该Activity的引用,Activity的内存资源 不能正常回收。此时,我们可以将该内部类设为静态内部类或将该内部类抽取出来封装成一个单例,如 果需要使用Context,尽量使用Application Context,如果需要使用Activity Context,就记得用完后置 空让GC可以回收,否则还是会内存泄漏。

    6、Handler临时性内存泄漏
    Message发出之后存储在MessageQueue中,在Message中存在一个target,它是Handler的一个引用,Message在Queue中存在的时间过长,就会导致Handler无法被回收。如果Handler是非静态的, 则会导致Activity或者Service不会被回收。并且消息队列是在一个Looper线程中不断地轮询处理消息, 当这个Activity退出时,消息队列中还有未处理的消息或者正在处理的消息,并且消息队列中的Message 持有Handler实例的引用,Handler又持有Activity的引用,所以导致该Activity的内存资源无法及时回 收,引发内存泄漏。解决方案如下所示:
    1、使用一个静态Handler内部类,然后对Handler持有的对象(一般是Activity)使用弱引用,这 样在回收时,也可以回收Handler持有的对象。
    2、在Activity的Destroy或者Stop时,应该移除消息队列中的消息,避免Looper线程的消息队列中 有待处理的消息需要处理
    (Handler那篇有讲)

    7、容器中的对象没清理造成的内存泄漏
    在退出程序之前,将集合里的东西clear,然后置为null,再退出程序

    8、WebView
    WebView都存在内存泄漏的问题,在应用中只要使用一次WebView,内存就不会被释放掉。我们可以为 WebView开启一个独立的进程,使用AIDL与应用的主进程进行通信,WebView所在的进程可以根据业 务的需要选择合适的时机进行销毁,达到正常释放内存的目的。

    9、使用ListView时造成的内存泄漏
    在构造Adapter时,使用缓存的convertView。

    10、Bitmap
    80%的内存泄露都是Bitmap造成的,Bitmap有Recycle()方法,不用时要及时回收。但是如果遇到要用Bitmap直接用Glide就完事了,自己写10有8.9得出错。

    启动速度优化

    app启动流程

    ①点击桌面App图标,Launcher进程采用Binder IPC向AMS进程发起startActivity请求;

    ②AMS接收到请求后,向zygote进程发送创建进程的请求;

    ③Zygote进程fork出新的子进程,即App进程;

    ④App进程,通过Binder IPC向AMS进程发起attachApplication请求;

    ⑤AMS进程在收到请求后,进行一系列准备工作后,再通过binder IPC向App进程发送scheduleLaunchActivity请求;

    ⑥App进程的binder线程(ApplicationThread)在收到请求后,通过handler向主线程发送LAUNCH_ACTIVITY消息;

    ⑦主线程在收到Message后,通过反射机制创建目标Activity,并回调Activity.onCreate()等方法。

    ⑧到此,App便正式启动,开始进入Activity生命周期,执行完onCreate/onStart/onResume方法,UI渲染结束后便可以看到App的主界面。

    image.png

    启动状态

    APP启动状态分为:冷启动、热启动、温启动。
    冷启动:什么都没有,点击桌面图标,启动App。Zygote fork进程...
    热启动:app挂在后台,在点击图标切换回来。
    温启动:两者之间。

    启动耗时统计

    1.打log。displayed。有显示

    image.png

    2.cmd命令 adb shell am start -S -W +包名+Activity名

    CPU Profile

    具体怎么优化呢?得用到Android Studio自带的分析的工具CPU Profile。

    1. 打开CPU Profile,trace java Methods

    image.png

    image.png

    1. 跑项目,截开始的那段cpu运行图,得到启动的数据

    image.png

    1. 得到具体哪个方法执行时间的情况图

    Call Chart:黄、蓝、绿三色。黄色=系统、绿色=自己的、蓝色=第三方。自上而下表示调用顺序。越长就说明执行的时间越长。

    如:我发现极光的初始化时间还是挺长的,如果要优化可以将极光延迟初始化。
    image.png

    还有onCreat占用时间也挺长,主要在setContentView里,看看能不能将布局优化一下。
    image.png

    Flame Chart:跟Call Chart差不多,就是反过来的图,又称火焰图。 image.png

    Top Down Tree:这个就不是图标了,是方法直接的调用关系,每个方法的调用时间。一直往下点,可以找到占用时间较长的,可以优化的地方。从上往下的调用。

    Bottom Up Tree:跟top反向从下往上。

    image.png

    启动白屏

    在主题里配置一个背景。
    image.png

    StrictMode严苛模式

    可以在application里配置严苛模式,这样不规范的操作就会有log提示,或者闪退。

    image.png

    启动优化的点

    1). 合理的使用异步初始化、延迟初始化、懒加载机制。
    2). 启动过程避免耗时操作,如数据库 I/O操作不要放在主线程执行。
    3). 类加载优化:提前异步执行类加载。
    4). 合理使用IdleHandler进行延迟初始化。
    5). 简化布局

    卡顿优化

    分析工具

    1. Systrace是Android提供的一款工具,需要运行在Python环境下。

    配置:http://www.jianshu.com/p/e73768e66…

    Systrace主要是分析掉帧的情况的。帧:android手机一般都是60帧,所以1帧的时间=1000毫秒/60=16.6毫秒。也就是android手机16.6毫秒刷新一次,超过这个数就是掉帧了

    会有三色球,绿=正常,黄=一点不正常,红=掉帧严重。
    少几个没啥事,大面积的出现红、黄,就需要研究为啥掉帧了。 image.png

    可以看上面有一条CPU的横轴,绿色=正在执行,蓝色=等待,可以运行。紫色=休眠。白色=休眠阻塞。如果是出现了紫色就说明IO等耗时操作导致掉帧。如果紫+蓝比较多,说明cpu拿不到时间片,cpu很忙。

    CPU Profile

    这个就能看那个方法运行多少时间等,所以可以直接用android studio自带的分析。

    一般是在top down里一直点,耗时较多的,然后点到自己熟悉的地方,挨个分析。
    这是一个漫长的耗时的过程,可能找半天只找到几个地方能优化,然后每个几毫秒,加起来也没有直观的变快,但是性能优化就是这样的一个过程,积少成多。
    如:我经过查找就发现adapter的 notifyDataSetChanged因为不小心,有些地方多次调用了。
    甚至还有没有在线程进行io操作。 image.png

    布局优化

    经过上面的一顿操作,发现占时间大块的少不了setContentView。说明布局渲染视图还是挺费时的。

    减少层级

    自定义Viewmeasure、layout、draw这三个过程,都需要对整个视图树自顶向下的遍历,而且很多情况都会多次触发整个遍历过程(Linearlayout 的 weight等),所以如果层级太深,就会让整个绘制过程变慢,从而造成启动速度慢、卡顿等问题。
    而onDraw在频繁刷新时可能多次触发,因此 onDraw更不能做耗时操作,同时需要注意内存抖动。对于布局性能的检测,依然可以使用systrace与traceview按 照绘制流程检查绘制耗时函数。

    工具Layout Inspector
    DecorView开始。content往下才是自己写的布局。
    image.png

    重复的布局使用include。
    一个布局+到另一个上,如果加上以后,有两一样的ViewGroup,可以把被加的顶层控件的ViewGroup换成merge
    ViewStub:失败提示框等。需要展示的时候才创建,放在那不占位置。

    过度渲染

    一块区域内(一个像素),如果被绘制了好几次,就是过度渲染。
    过度绘制不可避免,但是应该尽量的减少。
    手机->开发者模式->GPU 过度绘制 打开以后能看到不同颜色的块,越红就说明过度绘制的越严重。对于严重的地方得减少过度绘制。

    1.移除布局中不需要的背景。
    2.降低透明度。
    3.使视图层次结构扁平化。

    布局加载优化

    异步加载布局,视情况而用。

    implementation"androidx.asynclayoutinflater:asynclayoutinflater:1.0.0"

    new AsyncLayoutInflater(this)
    .inflate(R.layout.activity_main, null, new AsyncLayoutInflater.OnInflateFinishedListener() {
    @Override
    public void onInflateFinished(@NonNull View view, int resid, @Nullable ViewGroup parent) {
    setContentView(view);
    //......
    }
    });
    收起阅读 »

    iOS 调试:通过 Safari/Chrome 调试 WebView

    iOS 调试:通过 Safari/Chrome 调试 WebView主要汇总通过 Safari 和 Chrome 调试网页的步骤Safari 调试 WebView1、真机/模拟器开启 Safari 调试开关如果需要远程调试iOS Safari,必须启用Web检...
    继续阅读 »

    iOS 调试:通过 Safari/Chrome 调试 WebView

    主要汇总通过 Safari 和 Chrome 调试网页的步骤

    Safari 调试 WebView

    1、真机/模拟器开启 Safari 调试开关

    如果需要远程调试iOS Safari,必须启用Web检查功能

    • 设置 -> Safari -> 高级 -> 启动”Web检查“



    2、Safari 开启调试模式

    • Safari浏览器 -> 偏好设置 -> 高级 -> 勾选“在菜单栏中显示开发菜单”



    3、开始调试 WebView

    • 将手机通过数据线连接到mac上

    • 打开 Safari 浏览器,运行手机 app 中的 Web 界面

    • 在 Safari -> 开发 中选择连接的手机,并找到调试的网页




    Chrome 调试 WebView

    1、准备工作

    • 安装部署ios-webkit-debug-proxy,在终端中输入
    brew install ios-webkit-debug-proxy
    • 启动 proxy,在终端输入以下命令
    ios_webkit_debug_proxy -f chrome-devtools://devtools/bundled/inspector.html

    运行结果如下所示



    2、调试

    • 在 Chrome 中打开 localhost:9221 ,可以看到当前已连接的设备列表




    在app中打开Web页面,并在Chrome中点击local进入新页面,并右键转到该连接的页面





    最后在Web页面中,右键,选择检查即可






    作者:Style_月月
    链接:https://www.jianshu.com/p/99b52270c59d

    收起阅读 »