注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

SlideBar for Android 一个很好用的联系人快速索引。

SlideBarSlideBar for Android 一个很好用的联系人快速索引。Gif 展示引入Maven:<dependency> <groupId>com.king.view</groupId> <a...
继续阅读 »

SlideBar

SlideBar for Android 一个很好用的联系人快速索引。

Gif 展示


引入

Maven:

<dependency>
<groupId>com.king.view</groupId>
<artifactId>slidebar</artifactId>
<version>1.1.0</version>
<type>pom</type>
</dependency>

Gradle:

compile 'com.king.view:slidebar:1.1.0'

Lvy:

<dependency org='com.king.view' name='slidebar' rev='1.1.0'>
<artifact name='$AID' ext='pom'></artifact>
</dependency>
如果Gradle出现compile失败的情况,可以在Project的build.gradle里面添加如下:(也可以使用上面的GitPack来complie)
allprojects {
repositories {
maven { url 'https://dl.bintray.com/jenly/maven' }
}
}

具体实现详情请戳传送门

代码下载:SlideBar.zip

收起阅读 »

Android码表变化的旋转计数器动画控件

SpinCounterViewSpinCounterView for Android 一个类似码表变化的旋转计数器动画控件。Gif 展示引入Maven:<dependency> <groupId>com.king.view</...
继续阅读 »

SpinCounterView

SpinCounterView for Android 一个类似码表变化的旋转计数器动画控件。

Gif 展示

Image

引入

Maven:

<dependency>
<groupId>com.king.view</groupId>
<artifactId>spincounterview</artifactId>
<version>1.1.1</version>
<type>pom</type>
</dependency>

Gradle:

compile 'com.king.view:spincounterview:1.1.1'

Lvy:

<dependency org='com.king.view' name='spincounterview' rev='1.1.1'>
<artifact name='$AID' ext='pom'></artifact>
</dependency>

如果Gradle出现compile失败的情况,可以在Project的build.gradle里面添加如下:(也可以使用上面的GitPack来complie)

allprojects {
repositories {
//...
maven { url 'https://dl.bintray.com/jenly/maven' }
}
}

示例

布局

    <com.king.view.spincounterview.SpinCounterView
android:id="@+id/scv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:max="100"
app:maxValue="1000"/>

核心动画代码

spinCounterView.showAnimation(80);

代码下载:SpinCounterView.zip

收起阅读 »

Objective-C 消息转发深度理解(2)

4.1.3 forwarding_prep_0伪代码分析Hopper分析完毕后直接搜索forwarding_prep_0查看反汇编伪代码:int ___forwarding_prep_0___(int arg0, int arg1, int arg2, int...
继续阅读 »


4.1.3 forwarding_prep_0伪代码分析

Hopper分析完毕后直接搜索forwarding_prep_0查看反汇编伪代码:

int ___forwarding_prep_0___(int arg0, int arg1, int arg2, int arg3, int arg4, int arg5) {
//……
rax = ____forwarding___(&stack[0], 0x0);
if (rax != 0x0) {
rax = *rax;
}
else {
//arg0,arg1
rax = objc_msgSend(stack[0], stack[8]);
}
return rax;
}
  • 可以看到内部是对___forwarding___的调用。
  • ____forwarding___返回值不存在的时候调用的是objc_msgSend参数是arg0
    arg1

4.1.4 __forwarding__伪代码分析


点击进去查看___forwarding___的实现:


int ____forwarding___(int arg0, int arg1, int arg2, int arg3, int arg4, int arg5) {
r9 = arg5;
r8 = arg4;
rcx = arg3;
r13 = arg1;
r15 = arg0;
rax = COND_BYTE_SET(NE);
if (arg1 != 0x0) {
r12 = *_objc_msgSend_stret;
}
else {
r12 = *_objc_msgSend;
}
rbx = *(r15 + rax * 0x8);
rsi = *(r15 + rax * 0x8 + 0x8);
var_140 = rax * 0x8;
if (rbx >= 0x0) goto loc_115af7;

loc_115ac0:
//target pointer处理
rax = *_objc_debug_taggedpointer_obfuscator;
rax = *rax;
rcx = (rax ^ rbx) >> 0x3c & 0x7;
rax = ((rax ^ rbx) >> 0x34 & 0xff) + 0x8;
if (rcx != 0x7) {
rax = rcx;
}
if (rax == 0x0) goto loc_115ea6;

loc_115af7:
var_150 = r12;
var_138 = rsi;
var_148 = r15;
rax = object_getClass(rbx);
r15 = rax;
r12 = class_getName(rax);
//是否能响应 forwardingTargetForSelector,不能响应跳转 loc_115bab 否则继续执行 也就是forwardingTargetForSelector方法返回nil或者自身
if (class_respondsToSelector(r15, @selector(forwardingTargetForSelector:)) == 0x0) goto loc_115bab;

loc_115b38:
//rax返回值
rax = [rbx forwardingTargetForSelector:var_138];
//返回值是否存在,返回值是否等于自己 是则跳转 loc_115bab
if ((rax == 0x0) || (rax == rbx)) goto loc_115bab;

loc_115b55:
if (rax >= 0x0) goto loc_115b91;

loc_115b5a:
rcx = *_objc_debug_taggedpointer_obfuscator;
rcx = *rcx;
rdx = (rcx ^ rax) >> 0x3c & 0x7;
rcx = ((rcx ^ rax) >> 0x34 & 0xff) + 0x8;
if (rdx != 0x7) {
rcx = rdx;
}
if (rcx == 0x0) goto loc_115e95;

loc_115b91:
*(var_148 + var_140) = rax;
r15 = 0x0;
goto loc_115ef1;

loc_115ef1:
if (**___stack_chk_guard == **___stack_chk_guard) {
rax = r15;
}
else {
rax = __stack_chk_fail();
}
//返回 forwardingTargetForSelector 为消息的接收者
return rax;

loc_115e95:
rbx = rax;
r15 = var_148;
r12 = var_150;
goto loc_115ea6;

loc_115ea6:
if (dyld_program_sdk_at_least(0x7e30901ffffffff) != 0x0) goto loc_116040;

loc_115ebd:
r14 = _getAtomTarget(rbx);
*(r15 + var_140) = r14;
___invoking___(r12, r15, r15, 0x400, 0x0, r9, var_150, var_148, var_140, var_138, var_130, stack[-304], stack[-296], stack[-288], stack[-280], stack[-272], stack[-264], stack[-256], stack[-248], stack[-240]);
if (*r15 == r14) {
*r15 = rbx;
}
goto loc_115ef1;

loc_116040:
____forwarding___.cold.1();
rax = objc_opt_class(@class(NSInvocation));
*____forwarding___.invClass = rax;
rax = class_getInstanceSize(rax);
*____forwarding___.invClassSize = rax;
return rax;

loc_115bab:
var_140 = rbx;
//是否僵尸对象
if (strncmp(r12, "_NSZombie_", 0xa) == 0x0) goto loc_115f30;

loc_115bce:
r14 = var_140;
//是否能够响应 methodSignatureForSelector
if (class_respondsToSelector(r15, @selector(methodSignatureForSelector:)) == 0x0) goto loc_115f46;

loc_115bef:
rbx = var_138;
//调用
rax = [r14 methodSignatureForSelector:rbx];
if (rax == 0x0) goto loc_115fc1;

loc_115c0e:
r15 = rax;
rax = [rax _frameDescriptor];
r12 = rax;
if (((*(int16_t *)(*rax + 0x22) & 0xffff) >> 0x6 & 0x1) != r13) {
rax = sel_getName(rbx);
rcx = "";
if ((*(int16_t *)(*r12 + 0x22) & 0xffff & 0x40) == 0x0) {
rcx = " not";
}
r8 = "";
if (r13 == 0x0) {
r8 = " not";
}
_CFLog(0x4, @"*** NSForwarding: warning: method signature and compiler disagree on struct-return-edness of '%s'. Signature thinks it does%s return a struct, and compiler thinks it does%s.", rax, rcx, r8, r9, var_150);
}
//是否能够响应_forwardStackInvocation
if (class_respondsToSelector(object_getClass(r14), @selector(_forwardStackInvocation:)) == 0x0) goto loc_115d61;

loc_115c9a:
if (*____forwarding___.onceToken != 0xffffffffffffffff) {
dispatch_once(____forwarding___.onceToken, ^ {/* block implemented at ______forwarding____block_invoke */ } });
}
[NSInvocation requiredStackSizeForSignature:r15];
var_138 = r15;
rdx = *____forwarding___.invClassSize;
r13 = &var_150 - (rdx + 0xf & 0xfffffffffffffff0);
memset(r13, 0x0, rdx);
objc_constructInstance(*____forwarding___.invClass, r13);
var_150 = rax;
r15 = var_138;
[r13 _initWithMethodSignature:var_138 frame:var_148 buffer:&stack[-8] - (0xf + rax & 0xfffffffffffffff0) size:rax];
[var_140 _forwardStackInvocation:r13];
rbx = 0x1;
goto loc_115dce;

loc_115dce:
if (*(int8_t *)(r13 + 0x34) != 0x0) {
rax = *r12;
if (*(int8_t *)(rax + 0x22) < 0x0) {
rcx = *(int32_t *)(rax + 0x1c);
rdx = *(int8_t *)(rax + 0x20) & 0xff;
memmove(*(rdx + var_148 + rcx), *(rdx + rcx + *(r13 + 0x8)), *(int32_t *)(*rax + 0x10));
}
}
rax = [r15 methodReturnType];
r14 = rax;
rax = *(int8_t *)rax;
if ((rax != 0x76) && (((rax != 0x56) || (*(int8_t *)(r14 + 0x1) != 0x76)))) {
r15 = *(r13 + 0x10);
if (rbx != 0x0) {
r15 = [[NSData dataWithBytes:r15 length:var_150] bytes];
[r13 release];
rax = *(int8_t *)r14;
}
if (rax == 0x44) {
asm { fld tword [r15] };
}
}
else {
r15 = ____forwarding___.placeholder;
if (rbx != 0x0) {
r15 = ____forwarding___.placeholder;
[r13 release];
}
}
goto loc_115ef1;

loc_115d61:
var_138 = r12;
r12 = r14;
//forwardInvocation的判断,如果没有实现直接跳转loc_115f8e
if (class_respondsToSelector(object_getClass(r14), @selector(forwardInvocation:)) == 0x0) goto loc_115f8e;

loc_115d8d:
rax = [NSInvocation _invocationWithMethodSignature:r15 frame:var_148];
r13 = rax;
[r12 forwardInvocation:rax];
var_150 = 0x0;
rbx = 0x0;
r12 = var_138;
goto loc_115dce;

loc_115f8e:
//错误日志
r14 = @selector(forwardInvocation:);
____forwarding___.cold.4(&var_130, r12);
rcx = r14;
_CFLog(0x4, @"*** NSForwarding: warning: object %p of class '%s' does not implement methodSignatureForSelector: -- trouble ahead", var_140, rcx, r8, r9, var_150);
goto loc_115fba;

loc_115fba:
rbx = var_138;
goto loc_115fc1;

loc_115fc1:
rax = sel_getName(rbx);
r14 = rax;
rax = sel_getUid(rax);
if (rax != rbx) {
rcx = r14;
r8 = rax;
_CFLog(0x4, @"*** NSForwarding: warning: selector (%p) for message '%s' does not match selector known to Objective C runtime (%p)-- abort", var_138, rcx, r8, r9, var_150);
}
if (class_respondsToSelector(object_getClass(var_140), @selector(doesNotRecognizeSelector:)) == 0x0) goto loc_116034;

loc_11601b:
[var_140 doesNotRecognizeSelector:rdx];
asm { ud2 };
rax = loc_116034(rdi, rsi, rdx, rcx, r8, r9);
return rax;

loc_116034:
____forwarding___.cold.3(var_140);
goto loc_116040;

loc_115f46:
rbx = class_getSuperclass(r15);
r14 = object_getClassName(r14);
if (rbx == 0x0) {
rax = object_getClassName(var_140);
rcx = r14;
r8 = rax;
_CFLog(0x4, @"*** NSForwarding: warning: object %p of class '%s' does not implement methodSignatureForSelector: -- did you forget to declare the superclass of '%s'?", var_140, rcx, r8, r9, var_150);
}
else {
rcx = r14;
_CFLog(0x4, @"*** NSForwarding: warning: object %p of class '%s' does not implement methodSignatureForSelector: -- trouble ahead", var_140, rcx, r8, r9, var_150);
}
goto loc_115fba;

loc_115f30:
r14 = @selector(forwardingTargetForSelector:);
____forwarding___.cold.2(var_140, r12, var_138, rcx, r8);
goto loc_115f46;
}

可以看到汇编伪代码的调用流程与看到的API调用流程差不多。


4.1.5 __forwarding__伪代码还原


还原主要逻辑伪代码如下:


#include <stdio.h>

@interface NSInvocation(additions)

+ (unsigned long long)requiredStackSizeForSignature:(NSMethodSignature *)signature;

-(id)_initWithMethodSignature:(id)arg1 frame:(void*)arg2 buffer:(void*)arg3 size:(unsigned long long)arg4;

+(id)_invocationWithMethodSignature:(id)arg1 frame:(void*)arg2;

@end


@interface NSObject(additions)

- (void)_forwardStackInvocation:(NSInvocation *)invocation;

@end


void forwardingTargetForSelector(Class cls, SEL sel, const char * className, id obj);
void methodSignatureForSelector(Class cls, id obj, SEL sel);
void doesNotRecognizeSelector(id obj, SEL sel);
void _forwardStackInvocation(id obj,NSMethodSignature *signature);
void forwardInvocation(id obj,NSMethodSignature *signature);

int ____forwarding___(int arg0, int arg1, int arg2, int arg3, int arg4, int arg5) {
SEL sel = NULL;
id obj;
Class cls = object_getClass(obj);
const char * className = class_getName(cls);
forwardingTargetForSelector(cls,sel,className,obj);
return 0;
}

void forwardingTargetForSelector(Class cls, SEL sel, const char * className, id obj) {
//是否能响应 forwardingTargetForSelector,不能响应跳转 loc_115bab 否则继续执行 也就是forwardingTargetForSelector方法返回nil或者自身
if (class_respondsToSelector(cls, @selector(forwardingTargetForSelector:))) {
id obj = [cls forwardingTargetForSelector:sel];
if ((obj == nil) || (obj == cls)) {
methodSignatureForSelector(cls,obj,sel);
} else if (obj >= 0x0) {
//返回 forwardingTargetForSelector 备用消息接收者
// return obj;
} else {
//taggedpointer 处理
//返回NSInvocation size数据
}
} else {
//是否僵尸对象
if (strncmp(className, "_NSZombie_", 0xa)) {
methodSignatureForSelector(cls,obj,sel);
} else {
SEL currentSel = @selector(forwardingTargetForSelector:);
doesNotRecognizeSelector(obj,currentSel);
}
}
}


void methodSignatureForSelector(Class cls, id obj, SEL sel) {
if (class_respondsToSelector(cls, @selector(methodSignatureForSelector:))) {
NSMethodSignature *signature = [obj methodSignatureForSelector:sel];
if (signature) {
_forwardStackInvocation(obj,signature);
} else {
doesNotRecognizeSelector(obj,sel);
}
} else {
doesNotRecognizeSelector(obj,sel);
}
}

void _forwardStackInvocation(id obj,NSMethodSignature *signature) {
//是否能够响应_forwardStackInvocation
if (class_respondsToSelector(object_getClass(obj), @selector(_forwardStackInvocation:))) {
//执行dispatch_once相关逻辑
[NSInvocation requiredStackSizeForSignature:signature];
void *bytes;
// objc_constructInstance([NSInvocation class], bytes);
NSInvocation *invocation = [invocation _initWithMethodSignature:signature frame:NULL buffer:NULL size:bytes];
[obj _forwardStackInvocation:invocation];
const char * type = [signature methodReturnType];
//返回signature
} else {
forwardInvocation(obj,signature);
}
}

void forwardInvocation(id obj,NSMethodSignature *signature) {
//forwardInvocation的判断,如果没有实现直接跳转loc_115f8e
if (class_respondsToSelector(object_getClass(obj), @selector(forwardInvocation:))) {
NSInvocation *invocation = [NSInvocation _invocationWithMethodSignature:signature frame:NULL];
[obj forwardInvocation:invocation];
const char * type = [signature methodReturnType];
//返回signature
} else {
SEL sel = @selector(forwardInvocation:);
doesNotRecognizeSelector(obj,sel);
}
}

void doesNotRecognizeSelector(id obj, SEL sel) {
if (class_respondsToSelector(object_getClass(obj), @selector(doesNotRecognizeSelector:))) {
[obj doesNotRecognizeSelector:sel];
/*
____forwarding___.cold.1();
rax = objc_opt_class(@class(NSInvocation));
*____forwarding___.invClass = rax;
rax = class_getInstanceSize(rax);
*____forwarding___.invClassSize = rax;
return rax;
*/

} else {
/*
____forwarding___.cold.1();
rax = objc_opt_class(@class(NSInvocation));
*____forwarding___.invClass = rax;
rax = class_getInstanceSize(rax);
*____forwarding___.invClassSize = rax;
return rax;
*/

}
}
为了方便分析我这里class-dumpCoreFoundation头文件。手机端使用cycript进入SpringBoard应用,然后classdumpdyld导出CoreFoudation的头文件,最后拷贝到电脑端,具体操作如下:

cycript -p SpringBoard
@import net.limneos.classdumpdyld;
classdumpdyld.dumpBundle([NSBundle > bundleWithIdentifier:@"com.apple.CoreFoudation"]);
//输出导出头文件路径
@"Wrote all headers to /tmp/CoreFoundation"
//拷贝到电脑的相应目录
scp -r -P 12345 root@localhost:/tmp/CoreFoundation/ ./CoreFoundation_Headers/

伪代码流程图如下



反汇编流程与根据API分析的流程差不多。

  • forwardingTargetForSelector快速转发会对返回值会进行判断,如果是返回的自身或者nil直接进入下一流程(慢速转发)。
  • 如果返回taggedpointer有单独的处理。
  • methodSignatureForSelector慢速转发会先判断有没有实现_forwardStackInvocation(私有方法)。实现_forwardStackInvocation后不会再进入forwardInvocation流程,相当于_forwardStackInvocation是一个私有的前置条件。
  • methodSignatureForSelector如果没有返回签名信息不会继续进行下面的流程。
  • forwardInvocation没有实现就直接走到doesNotRecognizeSelector流程了。

4.2 流程分析


上篇文章分析resolveInstanceMethod在消息转发后还会调用一次resolveInstanceMethod(在日志文件中看到是在doesNotRecognizeSelector之前,methodSignatureForSelector之后)。那么实现对应的方法做下验证:

HPObject resolveInstanceMethod: HPObject-0x100008290-instanceMethod
-[HPObject forwardingTargetForSelector:] - instanceMethod
-[HPObject methodSignatureForSelector:] - instanceMethod
HPObject resolveInstanceMethod: HPObject-0x100008290-instanceMethod
-[HPObject doesNotRecognizeSelector:] - instanceMethod

证实是在methodSignatureForSelector之后,doesNotRecognizeSelector之前有一次进行了方法动态决议。那么为什么要这么处理呢?因为消息转发的过程中可能已经加入了对应的sel-imp,所以再给一次机会进行方法动态决议。这次决议后不会再进行消息转发。

但是在反汇编分析中并没有明确的再次进行动态方法决议的逻辑。


4.2.1 反汇编以及源码探究

那么在第二次调用resolveInstanceMethod前打断点查看下堆栈信息
macOS堆栈如下:

* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 5.1
frame #0: 0x0000000100300f53 libobjc.A.dylib`resolveMethod_locked(inst=0x0000000000000000, sel="instanceMethod", cls=HPObject, behavior=0) at objc-runtime-new.mm:6339:13
frame #1: 0x00000001002ffbd5 libobjc.A.dylib`lookUpImpOrForward(inst=0x0000000000000000, sel="instanceMethod", cls=HPObject, behavior=0) at objc-runtime-new.mm:6601:16
frame #2: 0x00000001002d6df9 libobjc.A.dylib`class_getInstanceMethod(cls=HPObject, sel="instanceMethod") at objc-runtime-new.mm:6210:5
* frame #3: 0x00007fff2e33fc68 CoreFoundation`__methodDescriptionForSelector + 282
frame #4: 0x00007fff2e35b57c CoreFoundation`-[NSObject(NSObject) methodSignatureForSelector:] + 38
frame #5: 0x0000000100003a21 HPObjcTest`-[HPObject methodSignatureForSelector:](self=0x0000000100706a30, _cmd="methodSignatureForSelector:", aSelector="instanceMethod") at HPObject.m:29:12 [opt]
frame #6: 0x00007fff2e327fc0 CoreFoundation`___forwarding___ + 408
frame #7: 0x00007fff2e327d98 CoreFoundation`__forwarding_prep_0___ + 120
frame #8: 0x0000000100003c79 HPObjcTest`main + 153
frame #9: 0x00007fff683fecc9 libdyld.dylib`start + 1
frame #10: 0x00007fff683fecc9 libdyld.dylib`start + 1
可以看到methodSignatureForSelector调用后进入了__methodDescriptionForSelector随后调用了class_getInstanceMethod。查看汇编确实在__methodDescriptionForSelector中调用了class_getInstanceMethod


那么系统是如何从methodSignatureForSelector调用到__methodDescriptionForSelector的?
当前的methodSignatureForSelector的实现是:

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
NSLog(@"%s - %@",__func__,NSStringFromSelector(aSelector));
return [super methodSignatureForSelector:aSelector];
}

如果改为返回nil呢?

HPObject resolveInstanceMethod: HPObject-0x100008288-instanceMethod
-[HPObject forwardingTargetForSelector:] - instanceMethod
-[HPObject methodSignatureForSelector:] - instanceMethod
-[HPObject doesNotRecognizeSelector:] - instanceMethod
这个时候发现没有第二次调用了,那也就是说核心逻辑在[super methodSignatureForSelector:aSelector]的实现中。
查看源码:

// Replaced by CF (returns an NSMethodSignature)
+ (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
_objc_fatal("+[NSObject methodSignatureForSelector:] "
"not available without CoreFoundation");
}

// Replaced by CF (returns an NSMethodSignature)
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
_objc_fatal("-[NSObject methodSignatureForSelector:] "
"not available without CoreFoundation");
}

注释说的已经很明显了实现在CoreFoundation中,直接搜索methodSignatureForSelector的反汇编实现:


/* @class NSObject */
-(void *)methodSignatureForSelector:(void *)arg2 {
rdx = arg2;
if ((rdx != 0x0) && (___methodDescriptionForSelector(objc_opt_class(), rdx) != 0x0)) {
rax = [NSMethodSignature signatureWithObjCTypes:rdx];
}
else {
rax = 0x0;
}
return rax;
}
  • sel不为nil的时候会调用___methodDescriptionForSelector。这样就串联起来了。

class_getInstanceMethod的实现如下:


Method class_getInstanceMethod(Class cls, SEL sel)
{
if (!cls || !sel) return nil;
lookUpImpOrForward(nil, sel, cls, LOOKUP_RESOLVER);
return _class_getMethod(cls, sel);
}

4.2.2 断点调试验证

既然上面已经清楚了resolveInstanceMethod第二次调用是methodSignatureForSelector之后调用的,那么不妨打个符号断点跟踪下methodSignatureForSelector:




显然只需要关心调用的函数以及跳转逻辑。

跟进去__methodDescriptionForSelector


这样通过断点也从methodSignatureForSelector定位到了resolveInstanceMethod

结论:

  • 实例方法 - methodSignatureForSelector-> ___methodDescriptionForSelector -> class_getInstanceMethod-> lookUpImpOrForward->resolveMethod_locked-> resolveInstanceMethod
  • 类方法 + methodSignatureForSelector -> ___methodDescriptionForSelector(传递的是元类) -> class_getInstanceMethod- lookUpImpOrForward->resolveMethod_locked-> resolveClassMethod

⚠️总结:

  1. 在methodSignatureForSelector内部调用了class_getInstanceMethod进行lookUpImpOrForward随后进入方法动态决议。这也就是class_getInstanceMethod调用第二次的来源入口。
  2. methodSignatureForSelector后第二次调用class_getInstanceMethod是为了再给一次进行消息查找和动态决议流程,因为消息转发流程过程中有可能实现了对应的sel-imp

动态方法决议以及消息转发整个流程如下:




五、消息发送查找总结

前面已经通过objc_msgSend分析整个消息缓存、查找、决议、转发整个流程。

  • 通过CacheLookup进行消息快速查找
    • 整个cache查找过程相当于是insert过程的逆过程,找到imp就解码跳转,否则进入慢速查找流程。
  • 通过lookUpImpOrForward进行消息慢速查找
    • 慢速查找涉及到递归查找,查找过程分为二分查找/循环查找。
    • 找到imp直接跳转,否则查找父类缓存。父类缓存依然找不到则在父类方法列表中查找,直到找到nil。查找到父类方法/缓存方法直接插入自己的缓存中。
  • imp找不到的时候进行方法动态决议
    • 当快速和慢速消息查找都没有找到imp的时候就进入了方法动态决议流程,在这个流程中主要是添加imp后再次进行快速慢速消息查找。
  • 之后进入本篇的消息转发流程,消息转发分为快速以及慢速。
    • 在动态方法决议没有返回imp的时候就进入到了消息转发阶段。
    • 快速消息转发提供一个备用消息接收者,返回值不能为nil与自身。这个过程不能修改参数和返回值。
    • 慢速消息转发需要提供消息签名,只要提供有效签名就可以解决消息发送错误问题。同时要实现forwardInvocation配合处理消息。
    • forwardInvocation配合处理消息,使target生效起作用。
    • 在慢速消息转发后系统会再进行一次慢速消息查找流程。这次不会再进行消息转发。
    • 消息转发仍然没有解决问题会进入doesNotRecognizeSelector,这个方法并不能处理错误,实现它仍然会报错。只是能拿到错误信息而已。

⚠️慢速消息转发后系统仍然给了一次机会进行 慢速消息查找!!!(并不仅仅是动态方法决议)。

整个流程如下:







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







收起阅读 »

iOS Hook原理 - 反hook& MonkeyDev

一、 反 hook 初探我们Hook别人的代码一般使用OC的MethodSwizzle,如果我们用fishhook将MethodSwizzle hook了,别人是不是就hook不了我们的代码了?1.1 创建主工程 AntiHookDemo创建一个工程AntiH...
继续阅读 »

一、 反 hook 初探

我们Hook别人的代码一般使用OCMethodSwizzle,如果我们用
fishhookMethodSwizzle hook了,别人是不是就hook不了我们的代码了?

1.1 创建主工程 AntiHookDemo

创建一个工程AntiHookDemo,页面中有两个按钮btn1btn2:



对应两个事件:

- (IBAction)btn1Click:(id)sender {
NSLog(@"click btn1");
}

- (IBAction)btn2Click:(id)sender {
NSLog(@"click btn2");
}

1.2 创建防护 HookManager (FrameWork 动态库)

这个时候要使用fishhook防护,在FrameWork中写防护代码。基于两点:

  1. Framework在主工程+ load执行之前执行+ load
  2. 别人注入的Framework也在防护代码之后。

创建一个HookManager Framework,文件结构下:




AntiHookManager.h

#import <Foundation/Foundation.h>
#import <objc/message.h>

//暴露给外界使用
CF_EXPORT void (*exchange_p)(Method _Nonnull m1, Method _Nonnull m2);

@interface AntiHookManager : NSObject

@end

AntiHookManager.m:

#import "AntiHookManager.h"
#import "fishhook.h"

@implementation AntiHookManager

+ (void)load {
//基本防护
struct rebinding exchange;
exchange.name = "method_exchangeImplementations";
exchange.replacement = hp_exchange;
exchange.replaced = (void *)&exchange_p;
struct rebinding bds[] = {exchange};
rebind_symbols(bds, 1);
}


//指回原方法
void (*exchange_p)(Method _Nonnull m1, Method _Nonnull m2);

void hp_exchange(Method _Nonnull m1, Method _Nonnull m2) {
//可以在这里进行上报后端等操作
NSLog(@"find Hook");
}

@end

HookManager.h中导出头文件:

#import <HookManager/AntiHookManager.h>

然后将AntiHookManager.h放入public Headers

修改主工程的ViewController.m如下:


#import <HookManager/HookManager.h>

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
[super viewDidLoad];
exchange_p(class_getInstanceMethod(self.class, @selector(btn2Click:)),class_getInstanceMethod(self.class, @selector(test)));
}

- (void)test {
NSLog(@"self Hook Success");
}

- (IBAction)btn1Click:(id)sender {
NSLog(@"click btn1");
}

- (IBAction)btn2Click:(id)sender {
NSLog(@"click btn2");
}

@end

在工程中Hook自己的方法,这个时候运行主工程:


AntiHookDemo[1432:149145] click btn1
AntiHookDemo[1432:149145] self Hook Success

btn2能够被自己正常Hook


1.3 创建注入工程 HookDemo

  1. 在根目录创建APP文件夹以及Payload文件夹,拷贝AntiHookDemo.appAPP/Payload目录,压缩zip -ry AntiHookDemo.ipa Payload/生成.ipa文件
  2. 拷贝appResign.sh重签名脚本以及yololib注入工具到根目录。
  3. 创建HPHook注入Framework

HPHook代码如下:


#import "HPInject.h"
#import <objc/message.h>

@implementation HPInject

+ (void)load {
method_exchangeImplementations(class_getInstanceMethod(objc_getClass("ViewController"), @selector(btn1Click:)), class_getInstanceMethod(self, @selector(my_click)));
}

- (void)my_click {
NSLog(@"inject Success");
}

@end

编译运行:

AntiHookDemo[1437:149999] find  Hook
AntiHookDemo[1437:149999] click btn1
AntiHookDemo[1437:149999] self Hook Success

首先是检测到了Hook,其次自己内部btn2 hook成功了,btn1 hook没有注入成功。到这里暴露给自己用和防止别人Hook都已经成功了。对于三方库中正常使用到的Hook可以在防护代码中做逻辑判断可以加白名单等调用回原来的方法。如果自己的库在image list最后一个那么三方库其实已经Hook完了。

当然只Hook method_exchangeImplementations不能完全防护,还需要Hook class_replaceMethod以及method_setImplementation

这种防护方式破解很容易,一般不这么处理:
1.在Hopper中可以找到method_exchangeImplementations,直接在MachO中修改这个字符串HookManager中就Hook不到了(这里会直接crash,因为viewDidLoad中调用了exchange_p,对于有保护逻辑的就可以绕过了,并且method_exchangeImplementations没法做混淆)


2.可以很容易定位到防护代码,直接在防护代码之前Hook,或者将fishhook中的一些系统函数Hook也能破解。本质上是不执行防护代码。


二、MonkeyDev

MonkeyDev是逆向开发中一个常用的工具 MonkeyDev。能够帮助我们进行重签名和代码注入。


2.1 安装 MonkeyDev

theos安装(Cydia Substrate就是 theos中的工具)

sudo git clone --recursive https://github.com/theos/theos.git /opt/theos

配置环境变量

#逆向相关配置
#export THEOS=/opt/theos

#写入环境变量
#export PATH=$THEOS/bin:$PATH

运行nic.pl查看theos信息。



[error] Cowardly refusing to make a project inside $THEOS (/opt/theos/)出现这个错误则是export配置有问题。

指定Xcode

sudo xcode-select -s /Applications/Xcode.app

安装命令

这里是安装Xcode插件。安装完成后重启XcodeXcode中会出现MonkeyDev对应的功能:



  • MonkeyApp:自动给第三方应用集成RevealCycript和注入dylib的模块,支持调试dylib和第三方应用,支持Pod给第三放应用集成SDK,只需要准备一个砸壳后的ipa或者app文件即可。
  • MonkeyPod:提供了创建Pod的项目。
  • CaptainHook Tweak:使用CaptainHook提供的头文件进行OC函数的Hook以及属性的获取。
  • Command-line Tool:可以直接创建运行于越狱设备的命令行工具。
  • Logos Tweak:使用theos提供的logify.pl工具将.xm文件转成.mm文件进行编译,集成了CydiaSubstrate,可以使用MSHookMessageExMSHookFunctionHook OC函数和指定地址。


错误处理
1.MonkeyDev 安装出现:Types.xcspec not found
添加一个软连接:
sudo ln -s /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/Library/Xcode/PrivatePlugIns/IDEOSXSupportCore.ideplugin/Contents/Resources /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/Library/Xcode/Specifications

2.2 重签名

创建一个MonkeyDemo工程:


工程目录如下:



在工程目录下有一个TargetApp目录,直接将微信8.0.2版本拖进去:


编译运行工程:

这个时候就重签名成功了。相比用脚本自己跑方便很多,也能避免很多异常。

2.3 MonkeyDev 代码注入



工程配置

MonkeyDemo注入一下AntiHookDemo,将AntiHookDemo编译生成的App加入MonkeyDemoTargetApp中:


代码注入

MonkeyDemo工程MonkeyDemoDylib->Logos目录,.xm文件可以写OCC++C




MonkeyDemoDylib.xmtype改为Objective-C++ Preprocessed Source

这里面的默认代码就是Logos语法:




.xm默认打开方式修改为Xcode后重启Xcode就能识别代码了,否则就还是默认文本文件。将默认的代码删除,写Hook btn1Click的代码:

#import <UIKit/UIKit.h>

//要hook的类
%hook ViewController

//要hook的方法
- (void)btn1Click:(id)sender {
NSLog(@"Monkey Hook Success");
//调用原来的方法
%orig;
}

%end

直接运行工程后点击btn1

AntiHookDemo[9306:5972601] find  Hook
AntiHookDemo[9306:5972601] find Hook
AntiHookDemo[9309:5973617] Monkey Hook Success
AntiHookDemo[9350:5987306] click btn1




这个时候就Hook成功了,并且检测到了Hook。这里没有防护住是因为Monkey中用的是getImpsetImp
AntiHookManager做下改进:
AntiHookManager .h:

#import <Foundation/Foundation.h>
#import <objc/message.h>

//暴露给外界使用
CF_EXPORT void (*exchange_p)(Method _Nonnull m1, Method _Nonnull m2);

CF_EXPORT IMP _Nonnull (*getImp_p)(Method _Nonnull m);

CF_EXPORT IMP _Nonnull(*setImp_p)(Method _Nonnull m, IMP _Nonnull imp);

@interface AntiHookManager : NSObject

@end

AntiHookManager .m:

#import "AntiHookManager.h"
#import "fishhook.h"

@implementation AntiHookManager

+ (void)load {
//基本防护
struct rebinding exchange;
exchange.name = "method_exchangeImplementations";
exchange.replacement = hp_exchange;
exchange.replaced = (void *)&exchange_p;

struct rebinding setIMP;
setIMP.name = "method_setImplementation";
setIMP.replacement = hp_setImp;
setIMP.replaced = (void *)&setImp_p;


struct rebinding getIMP;
getIMP.name = "method_getImplementation";
getIMP.replacement = hp_getImp;
getIMP.replaced = (void *)&getImp_p;

struct rebinding bds[] = {exchange,setIMP,getIMP};
rebind_symbols(bds, 3);
}


//指回原方法
void (*exchange_p)(Method _Nonnull m1, Method _Nonnull m2);

IMP _Nonnull (*getImp_p)(Method _Nonnull m);

IMP _Nonnull(*setImp_p)(Method _Nonnull m, IMP _Nonnull imp);

void hp_exchange(Method _Nonnull m1, Method _Nonnull m2) {
//可以在这里进行上报后端等操作
NSLog(@"find Hook");
}

void (hp_getImp)(Method _Nonnull m) {
NSLog(@"find Hook getImp");
}

void (hp_setImp)(Method _Nonnull m, IMP _Nonnull imp) {
NSLog(@"find Hook setImp");
}

@end

这个时候控制台输出:


AntiHookDemo[1488:207119] find  Hook getImp
AntiHookDemo[1488:207119] find Hook
AntiHookDemo[1488:207119] find Hook getImp
AntiHookDemo[1488:207119] find Hook
AntiHookDemo[1488:207119] click btn1

点击btn1也没有Hook到了。在这里运行时有可能CrashJSEvaluateScript的时候,直接删除App重新跑一次就可以了。
libsubstrate.dylib解析的,
其实这里.xm文件是被libsubstrate.dylib解析成MonkeyDemoDylib.mm中的内容(.xm代码是不参与编译的):



MSHookMessageEx底层用的是setImpgetImpOC进行Hook的。

错误问题
1.Signing for "MonkeyDemoDylib" requires a development team. Select a development team in the Signing & Capabilities editor.

直接在该targetbuild settings 中添加CODE_SIGNING_ALLOWED=NO





2.Failed to locate Logos Processor. Is Theos installed? If not, see https://github.com/theos/theos/wiki/Inst allation.
出现这个错误一般是theos没有安装好。或者路径配置的有问题。

3.library not found for -libstdc++
需要下载对应的库到XCode目录中。参考:https://github.com/longyoung/libstdc.6.0.9-if-help-you-give-a-star

4.The WatchKit app’s Info.plist must have a WKCompanionAppBundleIdentifier key set to the bundle identifier of the companion app.
删除DerivedData重新运行。

5.This application or a bundle it contains has the same bundle identifier as this application or another bundle that it contains. Bundle identifiers must be unique.
这种情况大概率是手机上之前安装过相同bundleIdApp安装不同版本导致,需要删除重新安装。还有问题的话删除DerivedDatabundleId

6.This app contains a WatchKit app with one or more Siri Intents app extensions that declare IntentsSupported that are not declared in any of the companion app's Siri Intents app extensions. WatchKit Siri Intents extensions' IntentsSupported values must be a subset of the companion app's Siri Intents extensions' IntentsSupported values.
需要删除com.apple.WatchPlaceholder(在/opt/MonkeyDev/Tools目录中修改pack.sh):


rm -rf "${TARGET_APP_PATH}/com.apple.WatchPlaceholder" || true

然后删除DerivedData重新运行。

  1. LLVM Profile Error: Failed to write file "default.profraw": Operation not permitted
    这个说明App内部做了反调试防护。直接在Monkey中开启sysctl
rebind_symbols((struct rebinding[1]){{"sysctl", my_sysctl, (void*)&orig_sysctl}},1);
8.Attempted to load Reveal Library twice. Are you trying to load dynamic library with Reveal Framework already linked?
直接删除dylibOther Linker Flags的设置即可(可能的原因是手机端已经导入了这个库):



⚠️遇见莫名其妙的错误建议删除DerivedData重启Xcode重新运行。


总结

  • Hook
    • 使用fishhook Hookmethod_exchangeImplementationsclass_replaceMethodmethod_setImplementation
    • 需要在动态库中添加防护代码。
    • 本地导出原函数IMP供自己项目使用,配合白名单。
    • 这种防护很容易破解,一般不推荐这么使用。
  • MonkeyDev:逆向开发中一个常用的工具。
    • 重签名:很容易,直接拖进去.ipa或者.app运行工程就可以了。
    • 代码注入:Logos主要是编写.xm文件。底层依然是getImpsetImp的调用。



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

收起阅读 »

iOS逆向 - fishhook

一、Hook概述HOOK中文译为挂钩或钩子。在iOS逆向中是指改变程序运行流程的一种技术。通过hook可以让别人的程序执行自己所写的代码。在逆向中经常使用这种技术。只有了解其原理才能够对恶意代码进行有效的防护。比如很久之前的微信自动抢红包插件:1.1Hook的...
继续阅读 »

一、Hook概述

HOOK中文译为挂钩钩子。在iOS逆向中是指改变程序运行流程的一种技术。通过hook可以让别人的程序执行自己所写的代码。在逆向中经常使用这种技术。只有了解其原理才能够对恶意代码进行有效的防护。

比如很久之前的微信自动抢红包插件:


1.1Hook的几种方式

iOSHOOK技术的大致上分为5种:Method SwizzlefishhookCydia Substratelibffiinlinehook

1.1.1 Method Swizzle (OC)

利用OCRuntime特性,动态改变SEL(方法编号)和IMP(方法实现)的对应关系,达到OC方法调用流程改变的目的。主要用于OC方法。

可以将SEL 和 IMP 之间的关系理解为一本书的目录SEL 就像标题IMP就像页码。他们是一一对应的关系。(书的目录不一定一一对应,可能页码相同,理解就行。)。

Runtime提供了交换两个SELIMP对应关系的函数:

OBJC_EXPORT void
method_exchangeImplementations(Method _Nonnull m1, Method _Nonnull m2)
OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);

通过这个函数交换两个SELIMP对应关系的技术,称之为Method Swizzle(方法欺骗)


runtime中有3种方式实现方法交换:

  • method_exchangeImplementations:在分类中直接交换就可以了,如果不在分类需要配合class_addMethod实现原方法的回调。
  • class_replaceMethod:直接替换原方法。
  • method_setImplementation:重新赋值原方法,通过getImpsetImp配合。

1.1.2 fishhook (外部函数)

Facebook提供的一个动态修改链接mach-O文件的工具。利用MachO文件加载原理,通过修改懒加载非懒加载两个表的指针达到C(系统C函数)函数HOOK的目的。fishhook

总结下来是:dyld 更新 Mach-O 二进制的 __DATA segment 的 __la_symbol_str 中的指针,使用 rebind_symbol方法更新两个符号位置来进行符号的重新绑定。


1.1.3 Cydia Substrate

Cydia Substrate 原名为 Mobile Substrate ,主要作用是针对OC方法C函数以及函数地址进行HOOK操作。并不仅仅针对iOS而设计,安卓一样可以用。Cydia Substrate官方

Cydia Substrate主要分为3部分:Mobile HookerMobileLoadersafe mode

Mobile Hooker

它定义了一系列的宏和函数,底层调用objcruntimefishhook来替换系统或者目标应用的函数。其中有两个函数:


void MSHookMessageEx(Class class, SEL selector, IMP replacement, IMP result) 
MSHookFunction :(inline hook)主要作用于CC++函数 MSHookFunction。 Logos语法的%hook就是对这个函数做了一层封装。

void MSHookFunction(voidfunction,void* replacement,void** p_original)

MobileLoader

MobileLoader用于加载第三方dylib在运行的应用程序。启动时MobileLoader会根据规则把指定目录的第三方的动态库加载进去,第三方的动态库也就是我们写的破解程序。

safe mode

破解程序本质是dylib寄生在别人进程里。 系统进程一旦出错,可能导致整个进程崩溃,崩溃后就会造成iOS瘫痪。所以CydiaSubstrate引入了安全模式,在安全模式下所有基于CydiaSubstratede的三方dylib都会被禁用,便于查错与修复。

1.1.4 libffi

基于libbfi动态调用C函数。使用libffi中的ffi_closure_alloc构造与原方法参数一致的"函数" (stingerIMP),以替换原方法函数指针;此外,生成了原方法和Block的调用的参数模板cifblockCif。方法调用时,最终会调用到void _st_ffi_function(ffi_cif *cif, void *ret, void **args, void *userdata), 在该函数内,可获取到方法调用的所有参数、返回值位置,主要通过ffi_call根据cif调用原方法实现和切面blockAOP库 StingerBlockHook就是使用libbfi做的。

1.1.5 inlinehook 内联钩子 (静态)

Inline Hook 就是在运行的流程中插入跳转指令来抢夺运行流程的一个方法。大体分为三步:

  • 将原函数的前 N 个字节搬运到 Hook 函数的前 N 个字节;
  • 然后将原函数的前 N 个字节填充跳转到 Hook 函数的跳转指令;
  • 在 Hook 函数末尾几个字节填充跳转回原函数 +N 的跳转指令;


MSHookFunction就是inline hook

基于 Dobby 的 Inline HookDobby 是通过插入 __zDATA 段和__zTEXT 段到 Mach-O 中。

  • __zDATA 用来记录 Hook 信息(Hook 数量、每个 Hook 方法的地址)、每个 Hook 方法的信息(函数地址、跳转指令地址、写 Hook 函数的接口地址)、每个 Hook 的接口(指针)。
  • __zText 用来记录每个 Hook 函数的跳转指令。

dobby
Dobby(原名:HOOKZz)是一个全平台的inlineHook框架,它用起来就和fishhook一样。
Dobby 通过 mmap 把整个 Mach-O 文件映射到用户的内存空间,写入完成保存本地。所以 Dobby 并不是在原 Mach-O上进行操作,而是重新生成并替换。



二 fishHook

2.1 fishhook的使用

fishhook源码.h文件中只提供了两个函数和一个结构体rebinding

rebind_symbols、rebind_symbols_image


FISHHOOK_VISIBILITY
int rebind_symbols(struct rebinding rebindings[], size_t rebindings_nel);

FISHHOOK_VISIBILITY
int rebind_symbols_image(void *header,
intptr_t slide,
struct rebinding rebindings[],
size_t rebindings_nel)
;

  • rebindings[]:存放rebinding结构体的数组(可以同时交换多个函数)。
  • rebindings_nelrebindings数组的长度。
  • slideASLR
  • headerimageHeader

只有两个函数重新绑定符号,两个函数的区别是一个指定image一个不指定。按照我们一般的理解放在前面的接口更常用,参数少的更简单。

rebinding

struct rebinding {
const char *name;//需要HOOK的函数名称,C字符串
void *replacement;//新函数的地址
void **replaced;//原始函数地址的指针!
};

  • name:要HOOK的函数名称,C字符串。
  • replacement:新函数的地址。(函数指针,也就是函数名称)。
  • replaced:原始函数地址的指针。(二级指针)。

2.1.1 Hook NSLog

现在有个需求,Hook系统的NSLog函数。
Hook代码:

- (void)hook_NSLog {
struct rebinding rebindNSLog;
rebindNSLog.name = "NSLog";
rebindNSLog.replacement = HP_NSLog;
rebindNSLog.replaced = (void *)&sys_NSLog;

struct rebinding rebinds[] = {rebindNSLog};

rebind_symbols(rebinds, 1);
}

//原函数,函数指针
static void (*sys_NSLog)(NSString *format, ...);

//新函数
void HP_NSLog(NSString *format, ...) {
format = [format stringByAppendingFormat:@"\n Hook"];
//调用系统NSLog
sys_NSLog(format);
}
调用:

    [self hook_NSLog];
NSLog(@"hook_NSLog");
输出:

hook_NSLog
Hook

这个时候就已经HookNSLog,走到了HP_NSLog中。
Hook代码调用完毕,sys_NSLog保存系统NSLog原地址,NSLog指向HP_NSLog

2.1.2 Hook 自定义 C 函数

Hook一下自己的C函数:

void func(const char * str) {
NSLog(@"%s",str);
}

- (void)hook_func {
struct rebinding rebindFunc;
rebindFunc.name = "func";
rebindFunc.replacement = HP_func;
rebindFunc.replaced = (void *)&original_func;

struct rebinding rebinds[] = {rebindFunc};

rebind_symbols(rebinds, 1);
}
//原函数,函数指针
static void (*original_func)(const char * str);

//新函数
void HP_func(const char * str) {
NSLog(@"Hook func");
original_func(str);
}
调用:

 [self hook_func];
func("HotPotCat");
输出:
HotPotCat

这个时候可以看到没有Hookfunc

结论:自定义的函数fishhook hook 不了,系统的可以hook

2.2 fishhook原理

fishHOOK可以HOOK C函数,但是我们知道函数是静态的,也就是说在编译的时候,编译器就知道了它的实现地址,这也是为什么C函数只写函数声明调用时会报错。那么为什么fishhook还能够改变C函数的调用呢?难道函数也有动态的特性存在?

是否意味着C Hook就必须修改调用地址?那意味着要修改二进制。(原理上使用汇编可以实现。fishhook不是这么处理的)

那么系统函数和本地函数区别到底在哪里?

2.2.1 符号 & 符号绑定 & 符号表 & 重绑定符号

NSLog函数的地址在编译的那一刻并不知道NSLog的真实地址。NSLogFoundation框架中。在运行时NSLog的地址在 共享缓存 中。在整个手机中只有dyld知道NSLog的真实地址。

LLVM编译器生成MachO文件时,如果让我们做就先空着系统函数的地址,等运行起来再替换。我们知道MachO中分为Text(只读)和Data(可读可写),那么显然这种方式行不通。

那么可行的方案是在Data段放一个 占位符(8字节)让代码编译的时候直接bl 占位符。在运行的时候dyld加载应用的时候将Data段的地址修改为NSLog真实地址,代码bl 占位符没有变 。这个技术就叫做 PIC(position independent code`)位置无关代码。(实际不是这么简单)

  • 占位符 就叫做 符号
  • dylddata段符号进行修改的这个过程叫做 符号绑定
  • 一个又一个的符号放在一起形成了一个列表,叫做 符号表

对于外部的C函数通过 符号 找 地址 也就给了我们机会动态的Hook外部C函数。OC是修改SELIMP对应的关系,符号 也是修改符号所对应的地址。这个动作叫做 重新绑定符号表。这也就是fishhook``hook的原理。

2.2.2验证

Hook NSLog前后分别调用NSLog:

    NSLog(@"before");
[self hook_NSLog];
NSLog(@"after");




MachO中我们能看到懒加载和非懒加载符号表,dyld绑定过程中绑定的是非懒加载符号和弱符号的。NSLog是懒加载符号,只有调用的时候才去绑定。

MachO中可以看到_NSLogData(值)是0000000100006960offset为:0x8010
在第一个NSLog处打个断点 运行查看:
主程序开始0x0000000100b24000ASLR0xb24000

0x0000000100b24000 + 0x8010中存储的内容为0x0100b2a960
0000000100006960 + 0xb24000 (ASLR) = 0x100B2A960
所以这里就对应上了。0x0100b2a960这个地址就是(符号表中的值其实是一个代码的地址,指向本地代码。)。



执行完第一个NSLog后(hook前):



符号表指向了HP_NSLog

这也就是fishhook能够Hook的真正原因(修改懒加载符号表)。

2.3 符号绑定过程(间接)

刚才在上面NSLog第一次执行之前我们拿到的地址0x0100b2a960实际上指向一段本地代码,加上ASLR后执行对应地址的代码然后就修改了懒加载符号表。

那么这个过程究竟是怎么做的呢?

先说明一些符号的情况:

  • 本地符号:只能本MachO用。
  • 全局符号:暴露给外面用。
  • 间接符号:当我们要调用外部函数/方法时,在编译时期地址是不知道的。比如系统的NSLog

间接符号专门有个符号表Indirect Symbols





比首地址大0x0000000100e0c000,所以这个地址在本MachO中。
0x100e12998 - 0x0000000100e0c000 = 0x6998

6998MachOSymbol Stubs中:





这个时候就对应上了:



这段代码的意思是执行桩中的代码找到符号表中代码跳转执行(0000000100006A28)。

6A28这段代码在__stub_helper中:



对应上了。实际上执行的是dyld_stub_binder。也就是说懒加载符号表里面的初始值都是执行符号绑定的函数

dyld_stub_binder是外部函数,那么怎么得到的dyld_stub_binder函数呢?



所以dyld_stub_binder是通过去非懒加载表中查找。
验证 :




验证确认,No-Lazy Symbol Pointers表中默认值是0

符号绑定过程:

  • 程序一运行,先绑定No-Lazy Symbol Pointers表中dyld_stub_binder的值。
  • 调用NSLog先找桩,执行桩中的代码。桩中的代码是找懒加载符号表中的代码去执行。
  • 懒加载符号表中的初始值是本地的源代码,这个代码去NoLazy表中找绑定函数地址。
  • 进入dyldbinder函数进行绑定。

binder函数执行完毕后就调用第一次的NSLog了。这个时候再看一下懒加载符号表中的符号:




符号已经变了。这个时候符号就已经绑定成功了。

接着执行第二次NSLog,这个时候依然是去找桩中的代码执行:



这个时候通过桩直接跳到了真实地址(还是虚拟的)。这个做的原因是符号表中保存地址执行代码,代码是保存在代码段的(桩)。





  • 外部函数调用时执行桩中的代码(__TEXT,__stubs)。
  • 桩中的代码去懒加载符号表中找地址执行(__DATA,__la_symbo_ptrl)。
    • 通过懒加载符号表中的地址去执行。要么直接调用函数地址(绑定过了),要么去__TEXT,__stubhelper中找绑定函数进行绑定。懒加载符号表中默认保存的是寻找binder的代码。
  • 懒加载中的代码去__TEXT,__stubhelper中执行绑定代码(binder函数)。
  • 绑定函数在非懒加载符号表中(__DATA._got),程序运行就绑定好了dyld

2.4 通过符号找字符串

上面使用fishhook的时候我们是通过rebindNSLog.name = "NSLog";告诉fishhookhook NSLog。那么fishhook通过NSLog怎么找到的符号的呢?

首先,我们清楚在绑定的时候是去Lazy Symbol中去找的NSLog对应的绑定代码:




找的是0x00008008这个地址,在Lazy SymbolNSLog排在第一个。

Indirect Symbols中可以看到顺序和Lazy Symbols中相同,也就是要找Lazy Symbols中的符号,只要找到Indirect Symbols中对应的第几个就可以了。




那么怎么确认Indirect Symbols中的第几个呢?
Indirect Symbolsdata对应值(十六进制)这里NSLog101,这个代表着NSLog在总的符号表(Symbols)中的角标:



在这里我们可以看到NSLogString Table中偏移为0x98(十六进制)。


通过偏移值计算得到0xCC38就确认到了_NSLog(长度+首地址)。

这里通过.隔开,函数名前面有_

这样我们就从Lazy Symbols -> Indirect Symbols -> Symbols - > String Table 通过符号找到了字符串。那么fishhook的过程就是这么处理的,通过遍历所有符号和要hook的数组中的字符串做对比。

fishhook中有一张图说明这个关系:




这里是通过符号查找close字符串。

  1. Lazy Symbol Pointer Tableclose index1061
  2. Indirect Symbol Table 1061 对应的角标为0X00003fd7(十进制16343)。
  3. Symbol Table找角标16343对应的字符串表中的偏移值70026
  4. String Table中找首地址+偏移值(70026)就找到了close
    字符串。

实际的原理还是通过传递的字符串找到符号进行替换:通过字符串找符号过程:

  1. String Table中找到字符串计算偏移值。
  2. 通过偏移值在Symbols中找到角标。
  3. 通过角标在Indirect Symbols中找到对应的符号。这个时候就能拿到这个符号的index了。
  4. 通过找到的indexLazy Symbols中找到对应index的符号。

2.5 去掉符号&恢复符号

符号本身在MachO文件中,占用包体积大小 ,在我们分析别人的App时符号是去掉的。

2.5.1 去除符号

符号基本分为:全局符号、间接符号(导出&导入)、本地符号。
对于App来说会去掉所有符号(间接符号除外)。对于动态库来说要保留全局符号(外部要调用)。

去掉符号在Build setting中设置:




  • Deployment Postprocessing:设置为YES则在编译阶段去符号,否则在打包阶段去符号。
  • Strip StyleAll Symbols去掉所有符号(间接除外),Non-Global Symbols去掉除全局符号外的符号。Debugging Symbols去掉调试符号。

设置Deployment PostprocessingYESStrip StyleAll Symbols。编译查看多了一个.bcsymbolmap文件,这个文件就是bitcode


这个时候的MachO文件中Symbols就只剩下间接符号表中的符号了:


其中
value为函数的实现地址(imp)。间接符号不会找到符号表中地址执行,是找Lazy Symbol Table中的地址。

代码中打断点就断不住了:




先计算出偏移值,下次直接ASLR+偏移值直接断点。这个也就是动态调试常用的方法。


2.5.2 恢复符号

前面动态调试下断点比较麻烦,如果能恢复符号的话就方便很多了。
在上面的例子中去掉所有符号后Symbol Table中只有间接符号了。虽然符号表中没有了,但是类列表和方法列表中依然存在。

这也就为我们提供了创建Symbol Table的机会。
可以通过restore-symbol工具恢复符号(只能恢复oc的,runtime机制导致):./restore-symbol 原始Macho文件 -o 恢复后文件

./restore-symbol FishHookDemo -o recoverDemo



这个时候就可以重签名后进行动态调试了。

2.6 fishhook源码解析

rebind_symbols
rebind_symbols的实现:

//第一次是拿dyld的回调,之后是手动拿到所有image去调用。这里因为没有指定image所以需要拿到所有的。
int rebind_symbols(struct rebinding rebindings[], size_t rebindings_nel) {
//prepend_rebindings的函数会将整个 rebindings 数组添加到 _rebindings_head 这个链表的头部
//Fishhook采用链表的方式来存储每一次调用rebind_symbols传入的参数,每次调用,就会在链表的头部插入一个节点,链表的头部是:_rebindings_head
int retval = prepend_rebindings(&_rebindings_head, rebindings, rebindings_nel);
//根据上面的prepend_rebinding来做判断,如果小于0的话,直接返回一个错误码回去
if (retval < 0) {
return retval;
}
//根据_rebindings_head->next是否为空判断是不是第一次调用。
if (!_rebindings_head->next) {
//第一次调用的话,调用_dyld_register_func_for_add_image注册监听方法.
//已经被dyld加载的image会立刻进入回调。之后的image会在dyld装载的时候触发回调。这里相当于注册了一个回调到 _rebind_symbols_for_image 函数。
_dyld_register_func_for_add_image(_rebind_symbols_for_image);
} else {
//不是第一次调用,遍历已经加载的image,进行的hook
uint32_t c = _dyld_image_count();//这个相当于 image list count
for (uint32_t i = 0; i < c; i++) {
//遍历重新绑定image header aslr
_rebind_symbols_for_image(_dyld_get_image_header(i), _dyld_get_image_vmaddr_slide(i));
}
}
return retval;
}
  • 首先通过prepend_rebindings函数生成链表,存放所有要Hook的函数。
  • 根据_rebindings_head->next是否为空判断是不是第一次调用,第一次调用走系统的回调,第二次则自己获取所有的image list进行遍历。
  • 最后都会走_rebind_symbols_for_image函数。

  • _rebind_symbols_for_image

    //两个参数 header  和 ASLR
    static void _rebind_symbols_for_image(const struct mach_header *header,
    intptr_t slide) {
    //_rebindings_head 参数是要交换的数据,head的头
    rebind_symbols_for_image(_rebindings_head, header, slide);
    }

    这里直接调用了rebind_symbols_for_image,传递了head链表地址。

    rebind_symbols_image

    int rebind_symbols_image(void *header,
    intptr_t slide,
    struct rebinding rebindings[],
    size_t rebindings_nel) {
    struct rebindings_entry *rebindings_head = NULL;
    int retval = prepend_rebindings(&rebindings_head, rebindings, rebindings_nel);
    //如果指定image就直接调用了 rebind_symbols_for_image,没有遍历了。
    rebind_symbols_for_image(rebindings_head, (const struct mach_header *) header, slide);
    if (rebindings_head) {
    free(rebindings_head->rebindings);
    }
    free(rebindings_head);
    return retval;
    }

    底层和rebind_symbols都调用到了rebind_symbols_for_image,由于给定了image所以不需要循环遍历。

    rebind_symbols_for_image

    //回调的最终就是这个函数! 三个参数:要交换的数组  、 image的头 、 ASLR的偏移
    static void rebind_symbols_for_image(struct rebindings_entry *rebindings,
    const struct mach_header *header,
    intptr_t slide) {

    /*dladdr() 可确定指定的address 是否位于构成进程的进址空间的其中一个加载模块(可执行库或共享库)内,如果某个地址位于在其上面映射加载模块的基址和为该加载模块映射的最高虚拟地址之间(包括两端),则认为该地址在加载模块的范围内。如果某个加载模块符合这个条件,则会搜索其动态符号表,以查找与指定的address 最接近的符号。最接近的符号是指其值等于,或最为接近但小于指定的address 的符号。
    */

    /*
    如果指定的address 不在其中一个加载模块的范围内,则返回0 ;且不修改Dl_info 结构的内容。否则,将返回一个非零值,同时设置Dl_info 结构的字段。
    如果在包含address 的加载模块内,找不到其值小于或等于address 的符号,则dli_sname 、dli_saddr 和dli_size字段将设置为0 ; dli_bind 字段设置为STB_LOCAL , dli_type 字段设置为STT_NOTYPE 。
    */


    // typedef struct dl_info {
    // const char *dli_fname; //image 镜像路径
    // void *dli_fbase; //镜像基地址
    // const char *dli_sname; //函数名字
    // void *dli_saddr; //函数地址
    // } Dl_info;

    Dl_info info;//拿到image的信息
    //dladdr函数就是在程序里面找header
    if (dladdr(header, &info) == 0) {
    return;
    }
    //准备从MachO里面去找!
    segment_command_t *cur_seg_cmd;//临时变量
    //这里与MachOView中看到的对应
    segment_command_t *linkedit_segment = NULL;//SEG_LINKEDIT
    struct symtab_command* symtab_cmd = NULL;//LC_SYMTAB 符号表地址
    struct dysymtab_command* dysymtab_cmd = NULL;//LC_DYSYMTAB 动态符号表地址
    //cur为了跳过header的大小,找loadCommands cur = 首地址 + mach_header大小
    uintptr_t cur = (uintptr_t)header + sizeof(mach_header_t);
    //循环load commands找对应的 SEG_LINKEDIT LC_SYMTAB LC_DYSYMTAB
    for (uint i = 0; i < header->ncmds; i++, cur += cur_seg_cmd->cmdsize) {
    cur_seg_cmd = (segment_command_t *)cur;
    //这里`SEG_LINKEDIT`获取和`LC_SYMTAB`与`LC_DYSYMTAB`不同是因为在`MachO`中分别对应`LC_SEGMENT_64(__LINKEDIT)`、`LC_SYMTAB`、`LC_DYSYMTAB`
    if (cur_seg_cmd->cmd == LC_SEGMENT_ARCH_DEPENDENT) {
    if (strcmp(cur_seg_cmd->segname, SEG_LINKEDIT) == 0) {
    linkedit_segment = cur_seg_cmd;
    }
    } else if (cur_seg_cmd->cmd == LC_SYMTAB) {
    symtab_cmd = (struct symtab_command*)cur_seg_cmd;
    } else if (cur_seg_cmd->cmd == LC_DYSYMTAB) {
    dysymtab_cmd = (struct dysymtab_command*)cur_seg_cmd;
    }
    }
    //有任何一项为空就直接返回,nindirectsyms表示间接符号表中符号数量,没有则直接返回
    if (!symtab_cmd || !dysymtab_cmd || !linkedit_segment ||
    !dysymtab_cmd->nindirectsyms) {
    return;
    }

    // Find base symbol/string table addresses
    //符号表和字符串表都属于data段中的linkedit,所以以linkedit基址+偏移量去获取地址(这里的偏移量不是整个macho的偏移量,是相对基址的偏移量)
    //链接时程序的基址 = __LINKEDIT.VM_Address -__LINKEDIT.File_Offset + silde的改变值
    uintptr_t linkedit_base = (uintptr_t)slide + linkedit_segment->vmaddr - linkedit_segment->fileoff;
    //printf("地址:%p\n",linkedit_base);
    //符号表的地址 = 基址 + 符号表偏移量
    nlist_t *symtab = (nlist_t *)(linkedit_base + symtab_cmd->symoff);
    //字符串表的地址 = 基址 + 字符串表偏移量
    char *strtab = (char *)(linkedit_base + symtab_cmd->stroff);

    // Get indirect symbol table (array of uint32_t indices into symbol table)
    //动态(间接)符号表地址 = 基址 + 动态符号表偏移量
    uint32_t *indirect_symtab = (uint32_t *)(linkedit_base + dysymtab_cmd->indirectsymoff);

    cur = (uintptr_t)header + sizeof(mach_header_t);
    for (uint i = 0; i < header->ncmds; i++, cur += cur_seg_cmd->cmdsize) {
    cur_seg_cmd = (segment_command_t *)cur;
    if (cur_seg_cmd->cmd == LC_SEGMENT_ARCH_DEPENDENT) {
    //寻找到load command 中的data【LC_SEGEMNT_64(__DATA)】,相当于拿到data段的首地址
    if (strcmp(cur_seg_cmd->segname, SEG_DATA) != 0 &&
    strcmp(cur_seg_cmd->segname, SEG_DATA_CONST) != 0) {
    continue;
    }

    for (uint j = 0; j < cur_seg_cmd->nsects; j++) {
    section_t *sect =
    (section_t *)(cur + sizeof(segment_command_t)) + j;
    //找懒加载表(lazy symbol table)
    if ((sect->flags & SECTION_TYPE) == S_LAZY_SYMBOL_POINTERS) {
    //找到直接调用函数 perform_rebinding_with_section,这里4张表就都已经找到了。传入要hook的数组、ASLR、以及4张表
    perform_rebinding_with_section(rebindings, sect, slide, symtab, strtab, indirect_symtab);
    }
    //非懒加载表(Non-Lazy symbol table)
    if ((sect->flags & SECTION_TYPE) == S_NON_LAZY_SYMBOL_POINTERS) {
    perform_rebinding_with_section(rebindings, sect, slide, symtab, strtab, indirect_symtab);
    }
    }
    }
    }
    }
    • 找到SEG_LINKEDITLC_SYMTABLC_DYSYMTABload commans

    SEG_LINKEDIT获取和LC_SYMTABLC_DYSYMTAB不同是因为在Load Commands中本来就不同,我们解析其它字段也要做类似操作
    • 根据linkedit和偏移值分别找到符号表的地址字符串表的地址以及间接符号表地址
    • 遍历load commandsdata段找到懒加载符号表非懒加载符号表
    • 找到表的同时就直接调用perform_rebinding_with_section进行hook替换函数符号。

    perform_rebinding_with_section

    //rebindings:要hook的函数链表,可以理解为数组
    //section:懒加载/非懒加载符号表地址
    //slide:ASLR
    //symtab:符号表地址
    //strtab:字符串标地址
    //indirect_symtab:动态(间接)符号表地址
    static void perform_rebinding_with_section(struct rebindings_entry *rebindings,
    section_t *section,
    intptr_t slide,
    nlist_t *symtab,
    char *strtab,
    uint32_t *indirect_symtab) {
    //nl_symbol_ptr和la_symbol_ptrsection中的reserved1字段指明对应的indirect symbol table起始的index。也就是第几个这里是和间接符号表中相对应的
    //这里就拿到了index
    uint32_t *indirect_symbol_indices = indirect_symtab + section->reserved1;
    //slide+section->addr 就是符号对应的存放函数实现的数组也就是我相应的__nl_symbol_ptr和__la_symbol_ptr相应的函数指针都在这里面了,所以可以去寻找到函数的地址。
    //indirect_symbol_bindings中是数组,数组中是函数指针。相当于lazy和non-lazy中的data
    void **indirect_symbol_bindings = (void **)((uintptr_t)slide + section->addr);
    //遍历section里面的每一个符号(懒加载/非懒加载)
    for (uint i = 0; i < section->size / sizeof(void *); i++) {
    //找到符号在Indrect Symbol Table表中的值
    //读取indirect table中的数据
    uint32_t symtab_index = indirect_symbol_indices[i];
    if (symtab_index == INDIRECT_SYMBOL_ABS || symtab_index == INDIRECT_SYMBOL_LOCAL ||
    symtab_index == (INDIRECT_SYMBOL_LOCAL | INDIRECT_SYMBOL_ABS)) {
    continue;
    }
    //以symtab_index作为下标,访问symbol table,拿到string table 的偏移值
    uint32_t strtab_offset = symtab[symtab_index].n_un.n_strx;
    //获取到symbol_name 首地址 + 偏移值。(char* 字符的地址)
    char *symbol_name = strtab + strtab_offset;
    //判断是否函数的名称是否有两个字符,因为函数前面有个_,所以方法的名称最少要1个
    bool symbol_name_longer_than_1 = symbol_name[0] && symbol_name[1];
    //遍历最初的链表,来判断名字进行hook
    struct rebindings_entry *cur = rebindings;
    while (cur) {
    for (uint j = 0; j < cur->rebindings_nel; j++) {
    //这里if的条件就是判断从symbol_name[1]两个函数的名字是否都是一致的,以及判断字符长度是否大于1
    if (symbol_name_longer_than_1 &&
    strcmp(&symbol_name[1], cur->rebindings[j].name) == 0) {
    //判断replaced的地址不为NULL 要替换的自己实现的方法和rebindings[j].replacement的方法不一致。
    if (cur->rebindings[j].replaced != NULL &&
    indirect_symbol_bindings[i] != cur->rebindings[j].replacement) {
    //让rebindings[j].replaced保存indirect_symbol_bindings[i]的函数地址,相当于将原函数地址给到你定义的指针的指针。
    *(cur->rebindings[j].replaced) = indirect_symbol_bindings[i];
    }
    //替换内容为自己自定义函数地址,这里就相当于替换了内存中的地址,下次桩直接找到lazy/non-lazy表的时候直接就走这个替换的地址了。
    indirect_symbol_bindings[i] = cur->rebindings[j].replacement;
    //替换完成跳转外层循环,到(懒加载/非懒加载)数组的下一个数据。
    goto symbol_loop;
    }
    }
    //没有找到就找自己要替换的函数数组的下一个函数。
    cur = cur->next;
    }
    symbol_loop:;
    }
    }
    • 首先通过懒加载/非懒加载符号表和间接符号表找到所有的index
    • 将懒加载/非懒加载符号表的data放入indirect_symbol_bindings数组中。
    indirect_symbol_bindings就是存放lazynon-lazy表中的data数组:
    • 遍历懒加载/非懒加载符号表。
      • 读取indirect_symbol_indices找到符号在Indrect Symbol Table表中的值放入symtab_index
      • symtab_index作为下标,访问symbol table,拿到string table的偏移值。
      • 根据 strtab_offset偏移值获取字符地址symbol_name,也就相当于字符名称。
      • 循环遍历rebindings也就是链表(自定义的Hook数据)
      • 判断&symbol_name[1]rebindings[j].name两个函数的名字是否都是一致的,以及判断字符长度是否大于1
      • 相同则先保存原地址到自定义函数指针(如果replaced传值的话,没有传则不保存)。并且用要Hook的目标函数replacement替换indirect_symbol_bindings,这里就完成了Hook
    • reserved1确认了懒加载和非懒加载符号在间接符号表中的index值。

    疑问点:懒加载和非懒加载怎么和间接符号表index对应的呢?
    直接Hook dyld_stub_binder以及NSLog看下index对应的值:




    在间接符号表中非懒加载符号从20开始供两个,懒加载从22开始,这也就对应上了。这也就验证了懒加载和非懒加载符号都在间接符号表中能对应上。

    总结


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



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

    收起阅读 »

    petite-vue源码分析:无虚拟DOM的极简版Vue

    vue
    最近发现Vue增加了一个petite-vue的仓库,大概看了一下,这是一个无虚拟DOM的mini版Vue,前身貌似是vue-lite(瞎猜的~),主要用于在服务端渲染的HTML页面中上"sprinkling"(点缀)一些Vue式的交互。颇有意思,于是看了下源码...
    继续阅读 »

    最近发现Vue增加了一个petite-vue的仓库,大概看了一下,这是一个无虚拟DOM的mini版Vue,前身貌似是vue-lite(瞎猜的~),主要用于在服务端渲染的HTML页面中上"sprinkling"(点缀)一些Vue式的交互。颇有意思,于是看了下源码(v0.2.3),整理了本文。



    起步


    开发调试环境


    整个项目的开发环境非常简单


    git clone git@github.com:vuejs/petite-vue.git

    yarn

    # 使用vite启动
    npm run dev

    # 访问http://localhost:3000/

    (不得不说,用vite来搭开发环境还是挺爽的~


    新建一个测试文件exmaples/demo.html,写点代码


    <script type="module">
    import { createApp, reactive } from '../src'

    createApp({
    msg: "hello"
    }).mount("#app")
    </script>

    <div id="app">
    <h1>{{msg}}</h1>
    </div>

    然后访问http://localhost:3000/demo.html即可


    目录结构


    从readme可以看见项目与标准vue的一些差异



    • Only ~5.8kb,体积很小

    • Vue-compatible template syntax,与Vue兼容的模板语法

    • DOM-based, mutates in place,基于DOM驱动,就地转换

    • Driven by @vue/reactivity,使用@vue/reactivity驱动


    目录结构也比较简单,使用ts编写,外部依赖基本上只有@vue/reactivity



    核心实现


    createContext


    从上面的demo代码可以看出,整个项目从createApp开始。


    export const createApp = (initialData?: any) => {
    // root context
    const ctx = createContext()
    if (initialData) {
    ctx.scope = reactive(initialData) // 将初始化数据代理成响应式
    }
    // app的一些接口
    return {
    directive(name: string, def?: Directive) {},
    mount(el?: string | Element | null) {},
    unmount() {}
    }
    }

    关于Vue3中的reactive,可以参考之前整理的:Vue3中的数据侦测reactive,这里就不再展开了。


    createApp中主要是通过createContext创建根context,这个上下文现在基本不陌生了,来看看createContext


    export const createContext = (parent?: Context): Context => {
    const ctx: Context = {
    ...parent,
    scope: parent ? parent.scope : reactive({}),
    dirs: parent ? parent.dirs : {}, // 支持的指令
    effects: [],
    blocks: [],
    cleanups: [],
    // 提供注册effect回调的接口,主要使用调度器来控制什么时候调用
    effect: (fn) => {
    if (inOnce) {
    queueJob(fn)
    return fn as any
    }
    // @vue/reactivity中的effect方法
    const e: ReactiveEffect = rawEffect(fn, {
    scheduler: () => queueJob(e)
    })
    ctx.effects.push(e)
    return e
    }
    }
    return ctx
    }

    稍微看一下queueJob就可以发现,还是Vue中熟悉的nextTick实现,



    • 通过一个全局变量queue队列保存回调

    • 在下一个微任务处理阶段,依次执行queue中的每一个回调,然后清空queue


    mount


    基本使用


    createApp().mount("#app")

    mount方法最主要的作用就是处理el参数,找到应用挂载的根DOM节点,然后执行初始化流程


    mount(el?: string | Element | null) {
    let roots: Element[]
    // ...根据el参数初始化roots
    // 根据el创建Block实例
    rootBlocks = roots.map((el) => new Block(el, ctx, true))
    return this
    }

    Block是一个抽象的概念,用于统一DOM节点渲染、插入、移除和销毁等操作。


    下图是依赖这个Block的地方,可以看见主要在初始化、iffor这三个地方使用



    看一下Block的实现


    // src/block.ts
    export class Block {
    template: Element | DocumentFragment
    ctx: Context
    key?: any
    parentCtx?: Context

    isFragment: boolean
    start?: Text
    end?: Text

    get el() {
    return this.start || (this.template as Element)
    }

    constructor(template: Element, parentCtx: Context, isRoot = false) {
    // 初始化this.template
    // 初始化this.ctx

    // 构建应用
    walk(this.template, this.ctx)
    }
    // 主要在新增或移除时使用,可以先不用关心实现
    insert(parent: Element, anchor: Node | null = null) {}
    remove() {}
    teardown() {}
    }

    这个walk方法,主要的作用是递归节点和子节点,如果之前了解过递归diff,这里应该比较熟悉。但petite-vue中并没有虚拟DOM,因此在walk中会直接操作更新DOM。


    export const walk = (node: Node, ctx: Context): ChildNode | null | void => {
    const type = node.nodeType
    if (type === 1) {
    // 元素节点
    const el = node as Element
    // ...处理 如v-if、v-for
    // ...检测属性执行对应的指令处理 applyDirective,如v-scoped、ref等

    // 先处理子节点,在处理节点自身的属性
    walkChildren(el, ctx)

    // 处理节点属性相关的自定,包括内置指令和自定义指令
    } else if (type === 3) {
    // 文本节点
    const data = (node as Text).data
    if (data.includes('{{')) {
    // 正则匹配需要替换的文本,然后 applyDirective(text)
    applyDirective(node, text, segments.join('+'), ctx)
    }
    } else if (type === 11) {
    walkChildren(node as DocumentFragment, ctx)
    }
    }

    const walkChildren = (node: Element | DocumentFragment, ctx: Context) => {
    let child = node.firstChild
    while (child) {
    child = walk(child, ctx) || child.nextSibling
    }
    }

    可以看见会根据node.nodeType区分处理处理



    • 对于元素节点,先处理了节点上的一些指令,然后通过walkChildren处理子节点。

      • v-if,会根据表达式决定是否需要创建Block然后执行插入或移除

      • v-for,循环构建Block,然后执行插入



    • 对于文本节点,替换{{}}表达式,然后替换文本内容


    v-if


    来看看if的实现,通过branches保存所有的分支判断,activeBranchIndex通过闭包保存当前位于的分支索引值。


    在初始化或更新时,如果某个分支表达式结算结果正确且与上一次的activeBranchIndex不一致,就会创建新的Block,然后走Block构造函数里面的walk。


    export const _if = (el: Element, exp: string, ctx: Context) => {
    const parent = el.parentElement!
    const anchor = new Comment('v-if')
    parent.insertBefore(anchor, el)

    // 存放条件判断的各种分支
    const branches: Branch[] = [{ exp,el }]

    // 定位if...else if ... else 等分支,放在branches数组中

    let block: Block | undefined
    let activeBranchIndex: number = -1 // 通过闭包保存当前位于的分支索引值

    const removeActiveBlock = () => {
    if (block) {
    parent.insertBefore(anchor, block.el)
    block.remove()
    block = undefined
    }
    }

    // 收集依赖
    ctx.effect(() => {
    for (let i = 0; i < branches.length; i++) {
    const { exp, el } = branches[i]
    if (!exp || evaluate(ctx.scope, exp)) {
    // 当判断分支切换时,会生成新的block
    if (i !== activeBranchIndex) {
    removeActiveBlock()
    block = new Block(el, ctx)
    block.insert(parent, anchor)
    parent.removeChild(anchor)
    activeBranchIndex = i
    }
    return
    }
    }
    // no matched branch.
    activeBranchIndex = -1
    removeActiveBlock()
    })

    return nextNode
    }

    v-for


    for指令的主要作用是循环创建多个节点,这里还根据key实现了类似于diff算法来复用Block的功能


    export const _for = (el: Element, exp: string, ctx: Context) => {
    // ...一些工具方法如createChildContexts、mountBlock

    ctx.effect(() => {
    const source = evaluate(ctx.scope, sourceExp)
    const prevKeyToIndexMap = keyToIndexMap
    // 根据循环项创建多个子节点的context
    ;[childCtxs, keyToIndexMap] = createChildContexts(source)
    if (!mounted) {
    // 首次渲染,创建新的Block然后insert
    blocks = childCtxs.map((s) => mountBlock(s, anchor))
    mounted = true
    } else {
    // 更新时
    const nextBlocks: Block[] = []
    // 移除不存在的block
    for (let i = 0; i < blocks.length; i++) {
    if (!keyToIndexMap.has(blocks[i].key)) {
    blocks[i].remove()
    }
    }
    // 根据key进行处理
    let i = childCtxs.length
    while (i--) {
    const childCtx = childCtxs[i]
    const oldIndex = prevKeyToIndexMap.get(childCtx.key)
    const next = childCtxs[i + 1]
    const nextBlockOldIndex = next && prevKeyToIndexMap.get(next.key)
    const nextBlock =
    nextBlockOldIndex == null ? undefined : blocks[nextBlockOldIndex]
    // 不存在旧的block,直接创建
    if (oldIndex == null) {
    // new
    nextBlocks[i] = mountBlock(
    childCtx,
    nextBlock ? nextBlock.el : anchor
    )
    } else {
    // 存在旧的block,复用,检测是否需要移动位置
    const block = (nextBlocks[i] = blocks[oldIndex])
    Object.assign(block.ctx.scope, childCtx.scope)
    if (oldIndex !== i) {
    if (blocks[oldIndex + 1] !== nextBlock) {
    block.insert(parent, nextBlock ? nextBlock.el : anchor)
    }
    }
    }
    }
    blocks = nextBlocks
    }
    })

    return nextNode
    }

    处理指令


    所有的指令都是通过applyDirectiveprocessDirective来处理的,后者是基于前者的二次封装,主要处理一些内置的指令快捷方式builtInDirectives


    export const builtInDirectives: Record<string, Directive<any>> = {
    bind,
    on,
    show,
    text,
    html,
    model,
    effect
    }

    每种指令都是基于ctx和el等来实现快速实现某些逻辑,具体实现可以参考对应源码。


    当调用app.directive注册自定义指令时,


    directive(name: string, def?: Directive) {
    if (def) {
    ctx.dirs[name] = def
    return this
    } else {
    return ctx.dirs[name]
    }
    },

    实际上是向contenx的dirs添加一个属性,当调用applyDirective时,就可以得到对应的处理函数


    const applyDirective = (el: Node,dir: Directive<any>,exp: string,ctx: Context,arg?: string,modifiers?: Record<string, true>) => {
    const get = (e = exp) => evaluate(ctx.scope, e, el)
    // 执行指令方法
    const cleanup = dir({
    el,
    get,
    effect: ctx.effect,
    ctx,
    exp,
    arg,
    modifiers
    })
    // 收集那些需要在卸载时清除的副作用
    if (cleanup) {
    ctx.cleanups.push(cleanup)
    }
    }

    因此,可以利用上面传入的这些参数来构建自定义指令


    app.directive("auto-focus", ({el})=>{
    el.focus()
    })

    小结


    整个代码看起来,确实非常精简



    • 没有虚拟DOM,就无需通过template构建render函数,直接递归遍历DOM节点,通过正则处理各种指令就行了

    • 借助@vue/reactivity,整个响应式系统实现的十分自然,除了在解析指令的使用通过ctx.effect()收集依赖,基本无需再关心数据变化的逻辑


    文章开头提到,petite-vue的主要作用是:在服务端渲染的HTML页面中上"sprinkling"(点缀)一些Vue式的交互。


    就我目前接触到的大部分服务端渲染HTML的项目,如果要实现一些DOM交互,一般使用



    • jQuery操作DOM,yyds

    • 当然Vue也是可以通过script + template的方式编写的,但为了一个div的交互接入Vue,又有点杀鸡焉用牛刀的感觉

    • 其他如React框架等同上


    petite-vue使用了与Vue基本一致的模板语法和响应式功能,开发体验上应该很不错。且其无需考虑虚拟DOM跨平台的功能,在源码中直接使用浏览器相关API操作DOM,减少了框架runtime运行时的成本,性能方面应该也不错。


    总结一下,感觉petite-vue结合了Vue标准版本的开发体验,以非常小的代码体积、良好的开发体验和还不错的运行性能,也许可以用来替代jQuery,用更现代的方式来操作DOM。


    该项目是6月30号提交的第一个版本,目前相关的功能和接口应该不是特别稳定,可能会有调整。但就exmples目录中的示例而言,应该能满足一些简单的需求场景了,也许可以尝试在一些比较小型的历史项目中使用。


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

    收起阅读 »

    【学不动了就回家喂猪】尤大大新活 petite-vue 尝鲜

    vue
    前言 打开尤大大的GitHub,发现多了个叫 petite-vue 的东西,好家伙,Vue3 和 Vite 还没学完呢,又开始整新东西了?本着学不死就往死里学的态度,咱还是来瞅瞅这到底是个啥东西吧,谁让他是咱的祖师爷呢! 简介 从名字来看可以知道 peti...
    继续阅读 »


    前言


    image.png


    打开尤大大的GitHub,发现多了个叫 petite-vue 的东西,好家伙,Vue3 和 Vite 还没学完呢,又开始整新东西了?本着学不死就往死里学的态度,咱还是来瞅瞅这到底是个啥东西吧,谁让他是咱的祖师爷呢!


    简介


    image.png


    从名字来看可以知道 petite-vue 是一个 mini 版的vue,大小只有5.8kb,可以说是非常小了。据尤大大介绍,petite-vue 是 Vue 的可替代发行版,针对渐进式增强进行了优化。它提供了与标准 Vue 相同的模板语法和响应式模型:



    • 大小只有5.8kb

    • Vue 兼容模版语法

    • 基于DOM,就地转换

    • 响应式驱动


    上活


    下面对 petite-vue 的使用做一些介绍。


    简单使用


    <body>
    <script src="https://unpkg.com/petite-vue" defer init></script>
    <div v-scope="{ count: 0 }">
    <button @click="count--">-</button>
    <span>{{ count }}</span>
    <button @click="count++">+</button>
    </div>
    </body>

    通过 script 标签引入同时添加 init ,接着就可以使用 v-scope 绑定数据,这样一个简单的计数器就实现了。



    了解过 Alpine.js 这个框架的同学看到这里可能有点眼熟了,两者语法之间是很像的。



    <!--  Alpine.js  -->
    <div x-data="{ open: false }">
    <button @click="open = true">Open Dropdown</button>
    <ul x-show="open" @click.away="open = false">
    Dropdown Body
    </ul>
    </div>

    除了用 init 的方式之外,也可以用下面的方式:


    <body>
    <div v-scope="{ count: 0 }">
    <button @click="count--">-</button>
    <span>{{ count }}</span>
    <button @click="count++">+</button>
    </div>
    <!-- 放在body底部 -->
    <script src="https://unpkg.com/petite-vue"></script>
    <script>
    PetiteVue.createApp().mount()
    </script>
    </body>

    或使用 ES module 的方式:


    <body>
    <script type="module">
    import { createApp } from 'https://unpkg.com/petite-vue?module'
    createApp().mount()
    </script>

    <div v-scope="{ count: 0 }">
    <button @click="count--">-</button>
    <span>{{ count }}</span>
    <button @click="count++">+</button>
    </div>
    </body>

    根作用域


    createApp 函数可以接受一个对象,类似于我们平时使用 data 和 methods 一样,这时 v-scope 不需要绑定值。


    <body>
    <script type="module">
    import { createApp } from 'https://unpkg.com/petite-vue?module'
    createApp({
    count: 0,
    increment() {
    this.count++
    },
    decrement() {
    this.count--
    }
    }).mount()
    </script>

    <div v-scope>
    <button @click="decrement">-</button>
    <span>{{ count }}</span>
    <button @click="increment">+</button>
    </div>
    </body>

    指定挂载元素


    <body>
    <script type="module">
    import { createApp } from 'https://unpkg.com/petite-vue?module'
    createApp({
    count: 0
    }).mount('#app')
    </script>

    <div id="app">
    {{ count }}
    </div>
    </body>

    生命周期


    可以监听每个元素的生命周期事件。


    <body>
    <script type="module">
    import { createApp } from 'https://unpkg.com/petite-vue?module'
    createApp({
    onMounted1(el) {
    console.log(el) // <span>1</span>
    },
    onMounted2(el) {
    console.log(el) // <span>2</span>
    }
    }).mount('#app')
    </script>

    <div id="app">
    <span @mounted="onMounted1($el)">1</span>
    <span @mounted="onMounted2($el)">2</span>
    </div>
    </body>

    组件


    在 petite-vue 里,组件可以使用函数的方式创建,通过template可以实现复用。


    <body>
    <script type="module">
    import { createApp } from 'https://unpkg.com/petite-vue?module'

    function Counter(props) {
    return {
    $template: '#counter-template',
    count: props.initialCount,
    increment() {
    this.count++
    },
    decrement() {
    this.count++
    }
    }
    }

    createApp({
    Counter
    }).mount()
    </script>

    <template id="counter-template">
    <button @click="decrement">-</button>
    <span>{{ count }}</span>
    <button @click="increment">+</button>
    </template>

    <!-- 复用 -->
    <div v-scope="Counter({ initialCount: 1 })"></div>
    <div v-scope="Counter({ initialCount: 2 })"></div>
    </body>

    全局状态管理


    借助 reactive 响应式 API 可以很轻松的创建全局状态管理


    <body>
    <script type="module">
    import { createApp, reactive } from 'https://unpkg.com/petite-vue?module'

    const store = reactive({
    count: 0,
    increment() {
    this.count++
    }
    })
    // 将count加1
    store.increment()
    createApp({
    store
    }).mount()
    </script>

    <div v-scope>
    <!-- 输出1 -->
    <span>{{ store.count }}</span>
    </div>
    <div v-scope>
    <button @click="store.increment">+</button>
    </div>
    </body>

    自定义指令


    这里来简单实现一个输入框自动聚焦的指令。


    <body>
    <script type="module">
    import { createApp } from 'https://unpkg.com/petite-vue?module'

    const autoFocus = (ctx) => {
    ctx.el.focus()
    }

    createApp().directive('auto-focus', autoFocus).mount()
    </script>

    <div v-scope>
    <input v-auto-focus />
    </div>
    </body>

    内置指令



    • v-model

    • v-if / v-else / v-else-if

    • v-for

    • v-show

    • v-html

    • v-text

    • v-pre

    • v-once

    • v-cloak



    注意:v-for 不需要key,另外 v-for 不支持 深度解构



    <body>
    <script type="module">
    import { createApp } from 'https://unpkg.com/petite-vue?module'

    createApp({
    userList: [
    { name: '张三', age: { a: 23, b: 24 } },
    { name: '李四', age: { a: 23, b: 24 } },
    { name: '王五', age: { a: 23, b: 24 } }
    ]
    }).mount()
    </script>

    <div v-scope>
    <!-- 支持 -->
    <li v-for="{ age } in userList">
    {{ age.a }}
    </li>
    <!-- 不支持 -->
    <li v-for="{ age: { a } } in userList">
    {{ a }}
    </li>
    </div>
    </body>

    不支持


    为了更轻量小巧,petite-vue 不支持以下特性:



    • ref()、computed

    • render函数,因为petite-vue 没有虚拟DOM

    • 不支持Map、Set等响应类型

    • Transition, KeepAlive, Teleport, Suspense

    • v-on="object"

    • v-is &

    • v-bind:style auto-prefixing


    总结


    以上就是对 petite-vue 的一些简单介绍和使用,抛砖引玉,更多新的探索就由你们去发现了。


    总的来说,prtite-vue 保留了 Vue 的一些基础特性,这使得 Vue 开发者可以无成本使用,在以往,当我们在开发一些小而简单的页面想要引用 Vue 但又常常因为包体积带来的考虑而放弃,现在,petite-vue 的出现或许可以拯救这种情况了,毕竟它真的很小,大小只有 5.8kb,大约只是 Alpine.js 的一半。


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

    10张脑图带你快速入门Vue3 | 附高清原图

    vue
    前言 这个月重新开始学习Vue3 目前已经完结第一部分:基础部分 我将所有内容吸收整理成10张脑图,一来快速入门Vue3,二来方便以后查看 脑图 应用实例和组件实例 模板语法 配置选项 计算属性和监听器 绑定class和style 条件渲染 列表渲...
    继续阅读 »

    前言


    这个月重新开始学习Vue3


    目前已经完结第一部分:基础部分


    我将所有内容吸收整理成10张脑图,一来快速入门Vue3,二来方便以后查看


    脑图


    应用实例和组件实例


    1应用实例和组件实例.png


    模板语法


    2模板语法.png


    配置选项


    3配置选项.png


    计算属性和监听器


    4计算属性和监听器.png


    绑定class和style


    5绑定class和style.png


    条件渲染


    6条件渲染.png


    列表渲染


    7列表渲染v-for.png


    事件处理


    8事件处理.png


    v-model及其修饰符


    9v-model及其修饰符.png


    组件的基本使用


    10组件的基本使用.png


    温馨小贴士



    1. 由于图片较多,为了避免一张张保存的麻烦


    我已将上述原图已上传githubgithub.com/jCodeLife/m…



    1. 如果需要更改图片,为了方便你按照自己的习惯进行修改


    我已将原始文件xmind上传github
    github.com/jCodeLife/m…



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

    收起阅读 »

    面试官问我CORS跨域,我直接一套操作斩杀!

    前言 我们都知道由于同源策略的存在,导致我们在跨域请求数据的时候非常的麻烦。首先阻挡我们的所谓同源到底是什么呢?,所谓同源就是浏览器的一个安全机制,不同源的客户端脚本没有在明确授权的情况下,不能读写对方资源。由于存在同源策略的限制,而又有需要跨域的业务,所以就...
    继续阅读 »

    前言


    我们都知道由于同源策略的存在,导致我们在跨域请求数据的时候非常的麻烦。首先阻挡我们的所谓同源到底是什么呢?,所谓同源就是浏览器的一个安全机制,不同源的客户端脚本没有在明确授权的情况下,不能读写对方资源。由于存在同源策略的限制,而又有需要跨域的业务,所以就有了CORS的出现。


    我们都知道,jsonp也可以跨域,那为什么还要使用CORS



    • jsonp只可以使用 GET 方式提交

    • 不好调试,在调用失败的时候不会返回任何状态码

    • 安全性,万一假如提供jsonp的服务存在页面注入漏洞,即它返回的javascript的内容被人控制的。那么结果是什么?所有调用这个jsonp的网站都会存在漏洞。于是无法把危险控制在一个域名下…所以在使用jsonp的时候必须要保证使用的jsonp服务必须是安全可信的。


    开始CORS


    CORS是一个W3C标准,全称是"跨域资源共享"(Cross-origin resource sharing),他允许浏览器向跨源服务器发送XMLHttpRequest请求,从而克服啦 AJAX 只能同源使用的限制


    CORS需要浏览器和服务器同时支持,整个 CORS通信过程,都是浏览器自动完成不需要用户参与,对于开发者来说,CORS的代码和正常的 ajax 没有什么差别,浏览器一旦发现跨域请求,就会添加一些附加的头信息,


    CORS这么好吗,难道就没有缺点嘛?


    答案肯定是NO,目前所有最新浏览器都支持该功能,但是万恶的IE不能低于10


    简单请求和非简单请求


    浏览器将CORS请求分成两类:简单请求和非简单请求


    简单请求


    凡是同时满足以下两种情况的就是简单请求,反之则非简单请求,浏览器对这两种请求的处理不一样



    • 请求方法是以下方三种方法之一

      • HEAD

      • GET

      • POST



    • HTTP的头信息不超出以下几种字段

      • Accept

      • Accept-Language

      • Content-Language

      • Last-Event-ID

      • Content-Type:只限于三个值 application/x-www-form-urlencodedmultipart/form-datatext/plain




    对于简单请求来说,浏览器之间发送CORS请求,具体来说就是在头信息中,增加一个origin字段,来看一下例子


    GET /cors? HTTP/1.1
    Host: localhost:2333
    Connection: keep-alive
    Origin: http://localhost:2332
    User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36
    Accept: */*
    Referer: http://localhost:2332/CORS.html
    Accept-Encoding: gzip, deflate, br
    Accept-Language: zh-CN,zh;q=0.9
    If-None-Match: W/"1-NWoZK3kTsExUV00Ywo1G5jlUKKs"

    上面的头信息中,Origin字段用来说名本次请求来自哪个源,服务器根据这个值,决定是否同意这次请求。


    如果Origin指定的源不在允许范围之内,服务器就会返回一个正常的HTTP回应,然后浏览器发现头信息中没有包含Access-Control-Allow-Origin 字段,就知道出错啦,然后抛出错误,反之则会出现这个字段(实例如下)


    Access-Control-Allow-Origin: http://api.bob.com
    Access-Control-Allow-Credentials: true
    Access-Control-Expose-Headers: FooBar
    Content-Type: text/html; charset=utf-8



    • Access-Control-Allow-Origin 这个字段是必须的,表示接受那些域名的请求(*为所有)




    • Access-Control-Allow-Credentials 该字段可选, 表示是否可以发送cookie




    • Access-Control-Expose-Headers 该字段可选,XHMHttpRequest对象的方法只能够拿到六种字段: Cache-ControlContent-LanguageContent-TypeExpiresLast-ModifiedPragma ,如果想拿到其他的需要使用该字段指定。




    如果你想要连带Cookie一起发送,是需要服务端和客户端配合的


    // 服务端
    Access-Control-Allow-Credentials: true
    // 客户端
    var xhr = new XMLHttpRequest();
    xhr.withCredentials = true;
    // 但是如果省略withCredentials属性的设置,有的浏览器还是会发送cookie的
    xhr.withCredentials = false;

    非简单请求


    非简单请求则是不满足上边的两种情况之一,比如请求的方式为 PUT,或者请求头包含其他的字段


    非简单请求的CORS请求是会在正式通信之前进行一次预检请求


    浏览器先询问服务器,当前网页所在的域名是否可以请求您的服务器,以及可以使用那些HTTP动词和头信息,只有得到正确的答复,才会进行正式的请求


    // 前端代码
    var url = 'http://localhost:2333/cors';
    var xhr = new XMLHttpRequest();
    xhr.open('PUT', url, true);
    xhr.setRequestHeader('X-Custom-Header', 'value');
    xhr.send();

    由于上面的代码使用的是 PUT 方法,并且发送了一个自定义头信息.所以是一个非简单请求,当浏览器发现这是一个非简单请求的时候,会自动发出预检请求,看看服务器可不可以接收这种请求,下面是"预检"HTTP 头信息


    OPTIONS /cors HTTP/1.1
    Origin: localhost:2333
    Access-Control-Request-Method: PUT // 表示使用的什么HTTP请求方法
    Access-Control-Request-Headers: X-Custom-Header // 表示浏览器发送的自定义字段
    Host: localhost:2332
    Accept-Language: zh-CN,zh;q=0.9
    Connection: keep-alive
    User-Agent: Mozilla/5.0...

    "预检"使用的请求方法是 OPTIONS , 表示这个请求使用来询问的,


    预检请求后的回应,服务器收到"预检"请求以后,检查了OriginAccess-Control-Request-MethodAccess-Control-Request-Headers字段以后,确认允许跨源请求,就可以做出回应。


    预检的响应头:


    HTTP/1.1 200 OK
    Date: Mon, 01 Dec 2008 01:15:39 GMT
    Server: Apache/2.0.61 (Unix)
    Access-Control-Allow-Origin: http://localhost:2332 // 表示http://localhost:2332可以访问数据
    Access-Control-Allow-Methods: GET, POST, PUT
    Access-Control-Allow-Headers: X-Custom-Header
    Content-Type: text/html; charset=utf-8
    Content-Encoding: gzip
    Content-Length: 0
    Keep-Alive: timeout=2, max=100
    Connection: Keep-Alive
    Content-Type: text/plain

    如果浏览器否定了"预检"请求,会返回一个正常的HTTP回应,但是没有任何CORS的头相关信息,这是浏览器就认定,服务器不允许此次访问,从而抛出错误


    预检之后的请求


    当预检请求通过之后发出正经的HTTP请求,还有一个就是一旦通过了预检请求就会,请求的时候就会跟简单请求,会有一个Origin头信息字段。


    通过预检之后的,浏览器发出发请求


    PUT /cors HTTP/1.1
    Origin: http://api.bob.com // 通过预检之后的请求,会自动带上Origin字段
    Host: api.alice.com
    X-Custom-Header: value
    Accept-Language: en-US
    Connection: keep-alive
    User-Agent: Mozilla/5.0...

    感谢


    谢谢你读完本篇文章,希望对你能有所帮助,如有问题欢迎各位指正。




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

    收起阅读 »

    「百毒不侵」面试官最喜欢问的13种Vue修饰符

    1.lazy lazy修饰符作用是,改变输入框的值时value不会改变,当光标离开输入框时,v-model绑定的值value才会改变 <input type="text" v-model.lazy="value"> <div>{{val...
    继续阅读 »

    image.png


    1.lazy


    lazy修饰符作用是,改变输入框的值时value不会改变,当光标离开输入框时,v-model绑定的值value才会改变


    <input type="text" v-model.lazy="value">
    <div>{{value}}</div>

    data() {
    return {
    value: '222'
    }
    }

    lazy1.gif


    2.trim


    trim修饰符的作用类似于JavaScript中的trim()方法,作用是把v-model绑定的值的首尾空格给过滤掉。


    <input type="text" v-model.trim="value">
    <div>{{value}}</div>

    data() {
    return {
    value: '222'
    }
    }

    number.gif


    3.number


    number修饰符的作用是将值转成数字,但是先输入字符串和先输入数字,是两种情况


    <input type="text" v-model.number="value">
    <div>{{value}}</div>

    data() {
    return {
    value: '222'
    }
    }


    先输入数字的话,只取前面数字部分



    trim.gif



    先输入字母的话,number修饰符无效



    number2.gif


    4.stop


    stop修饰符的作用是阻止冒泡


    <div @click="clickEvent(2)" style="width:300px;height:100px;background:red">
    <button @click.stop="clickEvent(1)">点击</button>
    </div>

    methods: {
    clickEvent(num) {
    不加 stop 点击按钮输出 1 2
    加了 stop 点击按钮输出 1
    console.log(num)
    }
    }

    5.capture


    事件默认是由里往外冒泡capture修饰符的作用是反过来,由外网内捕获


    <div @click.capture="clickEvent(2)" style="width:300px;height:100px;background:red">
    <button @click="clickEvent(1)">点击</button>
    </div>

    methods: {
    clickEvent(num) {
    不加 capture 点击按钮输出 1 2
    加了 capture 点击按钮输出 2 1
    console.log(num)
    }
    }

    6.self


    self修饰符作用是,只有点击事件绑定的本身才会触发事件


    <div @click.self="clickEvent(2)" style="width:300px;height:100px;background:red">
    <button @click="clickEvent(1)">点击</button>
    </div>

    methods: {
    clickEvent(num) {
    不加 self 点击按钮输出 1 2
    加了 self 点击按钮输出 1 点击div才会输出 2
    console.log(num)
    }
    }

    7.once


    once修饰符的作用是,事件只执行一次


    <div @click.once="clickEvent(2)" style="width:300px;height:100px;background:red">
    <button @click="clickEvent(1)">点击</button>
    </div>

    methods: {
    clickEvent(num) {
    不加 once 多次点击按钮输出 1
    加了 once 多次点击按钮只会输出一次 1
    console.log(num)
    }
    }

    8.prevent


    prevent修饰符的作用是阻止默认事件(例如a标签的跳转)


    <a href="#" @click.prevent="clickEvent(1)">点我</a>

    methods: {
    clickEvent(num) {
    不加 prevent 点击a标签 先跳转然后输出 1
    加了 prevent 点击a标签 不会跳转只会输出 1
    console.log(num)
    }
    }

    9.native


    native修饰符是加在自定义组件的事件上,保证事件能执行


    执行不了
    <My-component @click="shout(3)"></My-component>

    可以执行
    <My-component @click.native="shout(3)"></My-component>

    10.left,right,middle


    这三个修饰符是鼠标的左中右按键触发的事件


    <button @click.middle="clickEvent(1)"  @click.left="clickEvent(2)"  @click.right="clickEvent(3)">点我</button>

    methods: {
    点击中键输出1
    点击左键输出2
    点击右键输出3
    clickEvent(num) {
    console.log(num)
    }
    }

    11.passive


    当我们在监听元素滚动事件的时候,会一直触发onscroll事件,在pc端是没啥问题的,但是在移动端,会让我们的网页变卡,因此我们使用这个修饰符的时候,相当于给onscroll事件整了一个.lazy修饰符


    <div @scroll.passive="onScroll">...</div>

    12.camel


    不加camel viewBox会被识别成viewbox
    <svg :viewBox="viewBox"></svg>

    加了canmel viewBox才会被识别成viewBox
    <svg :viewBox.camel="viewBox"></svg>

    12.sync


    父组件传值进子组件,子组件想要改变这个值时,可以这么做


    父组件里
    <children :foo="bar" @update:foo="val => bar = val"></children>

    子组件里
    this.$emit('update:foo', newValue)

    sync修饰符的作用就是,可以简写:


    父组件里
    <children :foo.sync="bar"></children>

    子组件里
    this.$emit('update:foo', newValue)

    13.keyCode


    当我们这么写事件的时候,无论按什么按钮都会触发事件


    <input type="text" @keyup="shout(4)">

    那么想要限制成某个按键触发怎么办?这时候keyCode修饰符就派上用场了


    <input type="text" @keyup.keyCode="shout(4)">

    Vue提供的keyCode:


    //普通键
    .enter
    .tab
    .delete //(捕获“删除”和“退格”键)
    .space
    .esc
    .up
    .down
    .left
    .right
    //系统修饰键
    .ctrl
    .alt
    .meta
    .shift

    例如(具体的键码请看键码对应表


    按 ctrl 才会触发
    <input type="text" @keyup.ctrl="shout(4)">

    也可以鼠标事件+按键
    <input type="text" @mousedown.ctrl.="shout(4)">

    可以多按键触发 例如 ctrl + 67
    <input type="text" @keyup.ctrl.67="shout(4)">

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

    收起阅读 »

    iOS 自定义键盘

    很多项目中都使用自定义键盘,实现自定义键盘有很多方法,本文讲的是修改UITextField/UITextView的inputView来实现自定义键盘。如何修改已经知道了,但是怎么修改。有两种思路:自定义CustomTextField/CustomTextVie...
    继续阅读 »

    很多项目中都使用自定义键盘,实现自定义键盘有很多方法,本文讲的是修改UITextField/UITextView的inputView来实现自定义键盘。
    如何修改已经知道了,但是怎么修改。有两种思路:

    1. 自定义CustomTextField/CustomTextView,直接实现如下代码
    textField.inputView = customView;   
    textView.inputView = customView;

    但是这样写有个弊端,就是通用性不强。比如项目中可能要实现某个具体业务逻辑,这个textField/textView是继承ATextField/ATextView,其他地方又有用到的是继承BTextField/BTextView,那我们再写代码时候,可能需要写n个自定义textField/textView,用起来就非常麻烦了,所以这种方法不推荐。

    1. 使用分类来实现自定义键盘
      思路就是在分类中增加一个枚举,这个枚举定义了不同类型的键盘
    typedef NS_ENUM(NSUInteger, SJKeyboardType)
    {
    SJKeyboardTypeDefault, // 使用默认键盘
    SJKeyboardTypeNumber // 使用自定义数字键盘
    // 还可以根据需求 自定义其他样式...
    };

    写一个属性,来标记键盘类型

    @property (nonatomic, assign) SJKeyboardType sjKeyboardType;
    在.m文件中实现getter和setter方法

    static NSString *sjKeyboardTypeKey = @"sjKeyboardTypeKey";
    - (SJKeyboardType)sjKeyboardType
    {
    return [objc_getAssociatedObject(self, &sjKeyboardTypeKey) integerValue];
    }

    - (void)setSjKeyboardType:(SJKeyboardType)sjKeyboardType
    {
    objc_setAssociatedObject(self, &sjKeyboardTypeKey, @(sjKeyboardType), OBJC_ASSOCIATION_ASSIGN);
    [self setupKeyboard:sjKeyboardType];
    }

    在set方法中来实现自定义键盘视图设置及对应点击方法实现

    - (void)setupKeyboard:(SJKeyboardType)sjKeyboardType
    {

    switch (sjKeyboardType) {
    case SJKeyboardTypeDefault:
    break;
    case SJKeyboardTypeNumber: {
    SJCustomKeyboardView *numberInputView = [[[NSBundle mainBundle] loadNibNamed:@"SJCustomKeyboardView" owner:self options:nil] lastObject];
    numberInputView.frame = CGRectMake(0, 0, SJSCREEN_WIDTH, SJNumberKeyboardHeight + SJCustomKeyboardBottomMargin);
    self.inputView = numberInputView;
    numberInputView.textFieldReplacementString = ^(NSString * _Nonnull string) {
    BOOL canEditor = YES;
    if ([self.delegate respondsToSelector:@selector(textField:shouldChangeCharactersInRange:replacementString:)]) {
    canEditor = [self.delegate textField:self shouldChangeCharactersInRange:NSMakeRange(self.text.length, 0) replacementString:string];
    }

    if (canEditor) {
    [self replaceRange:self.selectedTextRange withText:string];
    }
    };
    numberInputView.textFieldShouldDelete = ^{
    BOOL canEditor = YES;
    if ([self.delegate respondsToSelector:@selector(textField:shouldChangeCharactersInRange:replacementString:)] && self.text.length) {
    canEditor = [self.delegate textField:self shouldChangeCharactersInRange:NSMakeRange(self.text.length - 1, 1) replacementString:@""];
    }
    if (canEditor) {
    [self deleteBackward];
    }
    };
    numberInputView.textFieldShouldClear = ^{
    BOOL canClear = YES;
    if ([self.delegate respondsToSelector:@selector(textFieldShouldClear:)]) {
    canClear = [self.delegate textFieldShouldClear:self];
    }
    if (canClear) {
    [self setText:@""];
    }
    };
    numberInputView.textFieldShouldReturn = ^{
    if ([self.delegate respondsToSelector:@selector(textFieldShouldReturn:)]) {
    [self.delegate textFieldShouldReturn:self];
    }
    };
    break;
    }
    }
    }
    之后就需要实现自定义键盘视图,这里需要注意一点,就是如果使用新建子类实现自定义键盘,个人感觉按钮响应用代理实现会看起来逻辑更清晰

    /* 用代理看的更清楚 但是分类不能实现代理 所以只能用block实现回调 如果自定义textField可以用代理 @protocol SJCustomKeyboardViewDelegate - (void)textFieldReplacementString:(NSString *_Nullable)string; - (BOOL)textFieldShouldDelete; - (BOOL)textFieldShouldClear; - (BOOL)textFieldShouldReturn; @end */

    但是分类不能实现代理,所以只能用block来实现回调


    @property (nonatomic, copy) void (^textFieldReplacementString)(NSString *string);
    @property (nonatomic, copy) void (^textFieldShouldDelete)(void);
    @property (nonatomic, copy) void (^textFieldShouldClear)(void);
    @property (nonatomic, copy) void (^textFieldShouldReturn)(void);

    .m中只需要实现按钮的点击方法和对应的回调方法即可。
    这样好处是只需要引入头文件,修改一个属性即可实现自定义键盘,不会影响项目中其他的业务逻辑。

    self.textField = [[UITextField alloc] initWithFrame:CGRectMake(20, 100, SJSCREEN_WIDTH - 40, 40)];  
    self.textField.placeholder = @"input";
    self.textField.borderStyle = UITextBorderStyleBezel;
    self.textField.delegate = self;
    [self.view addSubview:self.textField];

    self.textField.sjKeyboardType = SJKeyboardTypeNumber;





    收起阅读 »

    回顾 | Jetpack WindowManager 更新

    在今年年初,我们发布了 Jetpack WindowManager 库 alpha02 版本,这是一个较为重大的版本更新,并且包含部分已弃用的 API (目前已经发布到 1.0.0-alpha09 版),本文将为您回顾这次版本更新的内容。 Jetpack W...
    继续阅读 »

    在今年年初,我们发布了 Jetpack WindowManager 库 alpha02 版本,这是一个较为重大的版本更新,并且包含部分已弃用的 API (目前已经发布到 1.0.0-alpha09 版),本文将为您回顾这次版本更新的内容。


    Jetpack WindowManager 库可帮助您构建能够感知折叠和铰链等新设备功能的应用,使用以前不存在的新功能。在开发 Jetpack WindowManager 库时,我们结合了开发者的反馈意见,并且在 Alpha 版本中持续迭代 API,以提供一个更干净完整的 API 界面。我们一直在关注 WindowManager 空间中的不同领域以提供更多的功能,我们引入了 WindowMetrics,以便您可以在 Android 4.1 (API 级别 16) 及以上版本使用这些在 Android 11 加入的新 API


    首版发布后,我们用了大量时间来分析开发者反馈,并在 alpha02 版本中进行了大量的更新,接下来我们来看在 alpha02 版本中更新的具体内容!


    新建一个 WindowManager


    Alpha02 版本提供了一个简单的构造函数,这个构造函数只有一个参数,参数指向一个可见实体 (比如当前显示的 Activity) 的 Context:


    val windowManager = WindowManager(context: Context)

    原有的构造函数 仍可使用,但已被标记为废弃:


    @Deprecated
    val windowManager = WindowManager(context: Context, windowBackend: WindowBackend?)

    当您想在一个常见的设备或模拟器上使用一个自定义的 WindowBackend 模拟一个可折叠设备时,可使用原有的构造函数进行测试。这个 样例工程 中的实现可以供您参考。


    在 alpha02 版本,您仍可给参数 WindowBackend 传参为 null,我们计划在未来的版本中将 WindowBackend 设置为必填参数,移除 deprecation 标志,以推动此接口在测试时使用。


    添加 DisplayFeature 弃用 DeviceState


    另一个重大变化是弃用了 DeviceState 类,同时也弃用了使用它通知您应用的回调。之所以这样做,是因为我们希望提供更加通用的 API,这些通用的 API 允许系统向您的应用返回所有可用的 DisplayFeature 实例,而不是定义全局的设备状态。我们在 alpha06 的版本中已经将 DeviceState 从公共 API 中移除,请改用 FoldingFeature。


    alpha02 版本引入了带有更新了回调协议的新 DisplayFeature 类,以在 DisplayFeature 更改时通知您的应用。您可以注册、反注册回调来使用这些方法:


    registerLayoutChangeCallback(@NonNull Executor executor, @NonNull Consumer<WindowLayoutInfo> callback)

    unregisterLayoutChangeCallback(@NonNull Consumer<WindowLayoutInfo> callback)

    WindowLayoutInfo 包含了位于 window 内的 DisplayFeature 实例列表。


    FoldingFeature 类实现了 DisplayFeature 接口,其中包含了有关下列类型功能的信息:


    TYPE_FOLD(折叠类型)

    TYPE_HINGE(铰链类型)

    设备可能的折叠状态如下:


    △ DisplayFeature 可能的状态: 完全展开、半开、翻转


    △ DisplayFeature 可能的状态: 完全展开、半开、翻转


    需要注意的是这里没有与 DeviceState 中 POSTURE_UNKNOWN 和 POSTURE_CLOSED 姿态对应的状态。


    要获取最新的状态信息,您可以使用已注册回调返回的 FoldingFeature 信息:


    class LayoutStateChangeCallback : Consumer<WindowLayoutInfo> {
    override fun accept(newLayoutInfo: WindowLayoutInfo) {
    // 检查 newLayoutInfo. getDisplayFeatures() 的返回值,
    // 看它是否为 FoldingFeature 实例,并获取其中的信息。
    }
    }

    如何使用这些信息,请参阅: github.com/android/use…


    更好的回调注册


    上述示例代码的回调 API 也更加健壮了。在之前版本中,如果应用在 window 可用之前注册回调,将会抛出异常。


    在 aplha02 版本中我们修改了上述的行为。您可在对您应用设计有用的任何时候,注册这些回调,库会在 window 可用时发送初始 WindowLayoutInfo。


    R8 规则


    我们在库中添加了 R8 的 "keep" 规则,以保留那些因为内部模块的组织架构而可能被删除的方法或类。这些规则会自动合并到应用最终的 R8 规则中,这样可以防止应用出现如 alpha01 版本上的崩溃。


    WindowMetrics


    由于历史的命名习惯和各种可能的 Window Manager 状态,在 Android 上获取当前 window 的尺寸信息比较困难。Android 11 中一些被废弃的方法 (例如 Display#getSize 和 Display#getMetrics) 和在 window 尺寸新的 API 的使用,都凸显了可折叠设备从全屏到多窗口和自适应窗口这一上升的趋势。为了简化这一过渡过程,我们在 Android 11 中增加了 WindowMetrics API


    在第一次布局完成之前,WindowMetrics 可以让您轻松获取当前 window 状态信息,和系统当前状态下最大 Window 尺寸信息。例如像 Surface Duo 这样的设备,设备会有一个默认的配置决定应用从哪一个屏幕启动,但是也可以跨过设备的铰链扩展到两块屏幕上。在默认的状态,'getMaximumWindowMetrics' 方法返回应用当前所在屏幕的边界信息。当应用被移动到处于跨屏状态,'getMaximumWindowMetrics' 方法返回反映新状态的边界信息。这些信息最早在 onCreate 期间就会提供,您的 Activity 可以利用这些信息进行计算或者尽早做出决定,以便在第一时间选择正确的布局。


    API 返回的结果不包括系统 inset 信息,比如状态栏或导航栏,这是由于目前支持的所有 Android 版本中,在第一次布局完成之前,这些值对应的区域都不可用。关于使用 ViewCompat 去获取系统可用 inset 信息,Chris Banes 的文章 - 处理视觉冲突|手势导航 (二) 是非常好的资源。API 返回的边界信息也不会对布局填充时可能发生变化的布局参数作出响应。


    要访问这些 API,您需要像上文说明的那样先获取一个 WindowManager 对象:


    val windowManager = WindowManager(context: Context)

    现在您就可以访问 WindowMetrics API,并可轻松获取当前 window 的尺寸以及最大尺寸信息。


    windowManager.currentWindowMetrics

    windowManager.maximumWindowMetrics

    例如,如果您的应用在手机和平板电脑上的布局或导航模式截然不同,那么可以在视图填充之前依赖此信息对布局做出选择。如果您认为用户会对布局的明显变化感到疑惑,您可以忽略当前 window 尺寸信息的变化,选择部分信息作为常量。在选择填充哪些之前,您可以使用 window 最大尺寸信息。


    尽管 Android 11 平台已经包含了在 onCreate 期间获取 inset 信息的 API,但是我们还没有将这个 API 添加到 WindowManager 库中,这是因为我们想了解这些功能中哪些对开发者有用。您可以积极反馈,以便我们了解在您第一次布局之前,需要知道哪些能够使编写布局更为简便的值或抽象。


    我们希望这些可以用在 Android 低版本上的 API 能够帮助您构建响应 window 尺寸变化的应用,同时帮助您替换上文提到的已废弃 API。


    联系我们


    我们非常希望得到您对这些 API 的反馈,尤其是您认为缺少的那些,或者可让您开发变得更轻松的那些反馈。有一些使用场景我们可能没有考虑到,所以希望您在 public tracker 上向我们提交 bug 或功能需求。


    作者:Android_开发者
    链接:https://juejin.cn/post/6983867552841596942
    来源:掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    收起阅读 »

    Android so文件的加载原理

    so
    先说说so的编译类型 Android只支持3种cpu架构分为:arm,mips,x86,目前用的最多的是arm体系cpu,x86和mips体系的很少用到了。 arm体系中,又分32位和64位: armeabi/armeabi-v7a:这个架构是arm类型的,主...
    继续阅读 »



    1. 先说说so的编译类型
      Android只支持3种cpu架构分为:arm,mips,x86,目前用的最多的是arm体系cpu,x86和mips体系的很少用到了。
      arm体系中,又分32位和64位:

      armeabi/armeabi-v7a:这个架构是arm类型的,主要用于Android 4.0之后的,cpu是32位的,其中armeabi是相当老旧的一个版本, 缺少对浮点数的硬件支持,基本已经淘汰,可以不用考虑了。

      arm64-v8a:这个架构是arm类型的,主要是用于Android 5.0之后,cpu是64位的。平时项目中引入第三方的so文件时,第三方会根据cpu的架构编译成不同类型的so文件,项目引入这些so文件时,会将这些文件分别放入jniLibs目录下的arm64-v8a,armeabi-v7a等这些目录下,其实对于arm体系的so文件,没这个必要,因为arm体系是向下兼容的,比如32位的so文件是可以在64位的系统上运行的。Android上每启动一个app都会创建一个虚拟机,Android 64位的系统加载32位的so文件时,会创建一个64位的虚拟机的同时,还会创建一个32位的虚拟机,这样就能兼容32位的app应用了。鉴于兼容的原理,在app中,可以只保留armeabi-v7a版本的so文件就足够了。64位的操作系统会在32位的虚拟机上加载这个它。这样就极大的精简了app打包后的体积。虽然这样可以精简apk的体积,但是,在64位平台上运行32位版本的ART和Android组件,将丢失专为64位优化过的性能(ART,webview,media等等)所以,更好的方法是,为相应的abi打对应的apk包,这样就可以为不同abi版本生成不同的apk包。具体在build.gradle中的配置如下:



    android {

    ...

    splits {
    abi {
    enable true
    reset()
    include 'x86', 'x86_64', 'armeabi-v7a', 'arm64-v8a' //select ABIs to build APKs for
    universalApk true //generate an additional APK that contains all the ABIs
    }
    }

    // map for the version code
    project.ext.versionCodes = ['armeabi': 1, 'armeabi-v7a': 2, 'arm64-v8a': 3, 'mips': 5, 'mips64': 6, 'x86': 8, 'x86_64': 9]

    android.applicationVariants.all { variant ->
    // assign different version code for each output
    variant.outputs.each { output ->
    output.versionCodeOverride =
    project.ext.versionCodes.get(output.getFilter(com.android.build.OutputFile.ABI), 0) * 1000000 + android.defaultConfig.versionCode
    }
    }
    }


    1. so的加载流程
      可以通过以下命令来查看手机的cpu型号(以OPPO R7手机为例),在AS中的Terminal窗口中,输入如下命令


      C:\Users\xg\Desktop\AndroidSkill>adb shell
    shell@hwmt7:/ $ getprop ro.product.cpu.abilist
    arm64-v8a,armeabi-v7a,armeabi

    手机支持的种类存在一个abiList 的集合中,有个前后顺序,比如我的手机,支持三种类型, abiList 的集合中就有三个元素,第一个元素是arm64-v8a ,第二个元素是armeabi-v7a,第三个元素是armeabi 。按照这个先后顺序,我们遍历jniLib 目录,如果这个目录下有arm64-v8a子目录并且里面有so文件,那么接下来将加载arm64-v8a下的所有so文件,就不再去看其他子目录(比如armeabi-v7a)了,以此类推。在我的手机上,如果arm64-v8a 下有a.so,armeabi-v7a下有a.so和b.so那么我的手机只会加载arm64-v8a下的a.so,而永远不会加载到b.so,这时候就会抛出找不到b.so的异常,这是由Android 中的so加载算法导致的。因此,为了节省apk的体积,我们只能保存一份so文件,那就是armeabi-v7a下的so文件。32位的arm手机,肯定能加载到armeabi-v7a下的so文件。64位的arm手机,想要加载32位的so文件,千万不要在arm64 -v8a目录下放置任何so文件。把so文件都放在armeabi-v7a目录下就可以加载到了。


    下面举个例子来说明上面so的加载过程:
    32位的arm手机,如果项目的jniLibs目录下存在如下的so文件,
    jniLibs/arm64-v8a/libmsc.so
    jniLibs/armeabi-v7a/libmsc.so
    当要加载msc这个so文件时,就会直接到areabi-v7a目录下找。找到就加载, 找不到就报 couldn’t find “libmsc.so”
    如果armeabi-v7a这个目录都不存在时,也报 couldn’t find “libmsc.so”


    64位的arm手机,如果项目的jniLibs目录下存在如下的so文件,
    jniLibs/arm64-v8a/libmsc.so
    jniLibs/armeabi-v7a/libmsc.so
    当要加载msc.so文件时,就先到arm64-v8a目录下找,找到后,就不会去其他目录下找了。
    如果arm64-v8a目录下未找到,则到armeabi-v7a目录下找,找到就使用,找不到就去其他目录找,依次类推,如果都找到不到就报 couldn’t find “libmsc.so”。
    这个查找过程可以看下图:
    在这里插入图片描述



    1. so的加载方式
      方式一:System.loadLibrary方法,加载jniLibs目录下的so文件。例如,jniLibs目录下的arm64-v8a目录下有一个libHello.so文件,那么加载这个so文件是:


         System.loadLibray("Hello");//注意,没有lib前缀

    方式二:使用System.load方法,加载任意路径下的so文件,需要传入一个参数,这个参数就是so文件所在的完整路径。这两种方式最终都是调用的底层的dlopen方法加载so文件。但是方式二,由于可以传入so的路径,这样就可以实现动态加载so文件。so的插件化,就是使用的这种方式。动态加载so文件时,有时会出现 dlopen failed:libXXX.so is 32-bit instead of 64 bit 的异常。出现这个异常的原因是,手机的操作系统是64位的,这样加载这个32位的so文件时,会默认使用64位的虚拟机去加载,这样就报了这个异常。解决这个问题的方式,可以先在jniLibs目录下armeabi-v7a目录下,放入一个很简单的32位的libStub.so文件,在动态加载插件的so文件时,先去加载这个jniLibs/armeabi-v7a目录下的libStub.so文件,这样就会创建一个32位的虚拟机,当加载插件的32位的so文件时,就会使用这个32位的虚拟机来加载插件的so文件,这样也就不会报错了。


    注意,每个abi目录下的so文件数量要相同,因为,如果,在arm64-v8a目录下,存在a.so文件,在armeabi-v7a目录下,存在a.so和b.so文件,如果是在64位的arm系统的手机上加载a.so和b.so文件,由于先找a.so文件会先到arm64-v8a目录下找,找到后,后续的其他so文件就会都在这个目录下找了,有arm64-v8a目录下没有b.so文件,这样就会报couldn’t find "b.so"文件异常。所以,要保持每个abi目录下的so文件个数一致。


    关于加载插件中的so文件,是通过先创建加载插件的DexClassLoader,将插件中的so文件的路径传递给DecClassLoader的构造函数的第三个参数,这样,后续使用这个DexClassLoader去加载插件中的类或方法,插件中这些类或者方法中去加载插件的so文件。


    ————————————————
    版权声明:本文为CSDN博主「hujin2017」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
    原文链接:https://blog.csdn.net/hujin2017/article/details/102804883

    收起阅读 »

    探索 Android 消息机制

    1. Android 消息机制概述 Android 消息机制是由 Handler、Looper 和 MessageQueue 三者合作完成的,消息机制可以分为消息机制初始化、消息轮询、消息发送和消息处理 4 个过程来理解,消息机制是基于 Linux 的事...
    继续阅读 »

    1. Android 消息机制概述


    Android 消息机制.png


    Android 消息机制是由 HandlerLooperMessageQueue 三者合作完成的,消息机制可以分为消息机制初始化消息轮询消息发送消息处理 4 个过程来理解,消息机制是基于 Linux 的事件轮询机制 epoll 和用来通知事件的文件描述符 eventfd 来实现的 。


    消息机制初始化过程是从消息轮询器 Looper 的 prepare() 方法开始的,当线程调用 Looper 的 prepare() 方法时,prepare() 方法会调用 Looper 的构造函数创建一个 Looper ,并放到线程私有变量 ThreadLocal 中。Looper 的构造函数中会创建一个消息队列 MessageQueue ,而消息队列的构造方法会调用 nativeInit() JNI 方法初始化 Native 层的消息队列,在 Native 层消息队列的构造方法中,会调用 Native 层 Looper 的构造函数初始化 Native 层的 Looper ,而在 Native 层 Looper 的构造函数中会调用 rebuildEpollLocked() 方法,在 rebuildEpollLocked() 方法中会调用 epoll_create1() 系统调用创建一个 epoll 实例,然后再调用 epoll_ctl() 系统调用给 epoll 实例添加一个唤醒事件文件描述符,到这里消息机制的初始化就完成了。


    epoll 、select 和 poll 都是 Linux 中的一种 I/O 多路复用机制, poll 和 select 在每次调用时,都必须遍历所有被监视的文件描述符,文件描述符列表越大,性能就越差。而 epoll 则把监听注册从监听中分离了出来,这样就不需要每次调用时都遍历文件描述符列表了。创建 epoll 实例时,Linux 会创建一个 evnetpoll 结构体,这个结构体中有 rbrrdlist 两个成员,rbr 是红黑树的根节点,epoll 会用红黑树存储所有需要监控的事件 ,rdlist 则是存放着要通过 epoll_wait() 返回给用户的事件。


    唤醒事件文件描述符是一个 eventfd 对象,是 Linux 中的一个用来通知事件的文件描述符,与 pipe 相比,pipe 只能在进程/线程间使用,而 eventfd 是广播式的通知,可以多对多。eventfd 的结构体 eventfd_ctx 中有 wqhcount 两个成员,wqh 是一个等待队列的头结点,类型为 __wait_queue_head ,是一个自带自旋锁双向链表的节点,而 count 则是一个计数器


    消息轮询过程是从 Looper 的 loop() 方法开始的,当线程调用 Looper 的 loop() 方法后,loop() 方法中会调用 MessageQueuenext() 方法获取下一条要处理的消息,next() 方法中会通过 nativePollOnce() JNI 方法调检查当前消息队列中是否有新的消息要处理,nativePollOnce() 方法会调用 NativeMessageQueuepollOnce() 方法,NativeMessageQueue 的 pollOnce() 方法会调用 Native 层 Looper 的 pollOnce() 方法, Native 层 Looper 的 pollOnce() 方法中会把 timeout 参数传到 epoll_wait() 系统调用中,epoll_wait() 调用后会等待事件的产生,当 MessageQueue 中没有更多消息时,传到 epoll_wait() 中的 timeout 的值就是 -1 ,这时线程会一直被阻塞,直到有新的消息进来,这就是为什么 Looper 的死循环不会导致 CPU 飙高,因为主线程处于阻塞状态。当调用完 nativePollOnce() 方法后,MessageQueue 就会看下当前消息是不是同步屏障,是的话就找出并返回异步消息给 Looper ,不是的话则找出下一条到了发送时间的返回非异步消息。


    消息发送过程一般是从 Handler 的 sendMessage() 方法开始的,当我们调用 Handler 的 sendMessage() 或 sendEmptyMessage() 等方法时,Handler 会调用 MessageQueue 的 enqueueMessage() 方法把消息加入到消息队列中。消息 Message 并不是真正的队列结构,而是链表结构。MessageQueue 的enqueueMessage() 方法首先会判断消息的延时时间是否晚于当前链表中最后一个结点的发送时间,是的话则把该消息作为链表的最后一个结点。然后 enqueueMessage() 方法会判断是否需要唤醒消息轮询线程,是的话则通过 nativeWake() JNI 方法调用 NativeMessageQueue 的 wake() 方法。NativeMessageQueue 的 wake() 方法又会调用 Native 层 Looper 的 wake() 方法,在 Native 层 Looper 的 wake() 方法中,会通过 write() 系统调用写入一个 W 字符到唤醒事件文件描述符中,这时监听这个唤醒事件文件描述符的消息轮询线程就会被唤醒


    消息处理过程也是从 Looper 的 loop() 方法开始的,当 Looper 的 loop() 方法从 MessageQueue 的 next() 中获取到消息时,就会调用 Message 的 targetdispatchMessage() 的方法,Message 的 target 就是发送消息时用的 Handler ,Handler 的 dispatchMessage() 方法首先会判断 Message 是否设置了 callback 回调 ,比如用 post() 方法发送消息时,传入 post() 方法中的 Runnable 就是 Message 的 callback 回调,如果 Message 没有设置 callback ,则 dispatchMessage() 方法会调用 Handler 的 handleMessage() 方法,到这里消息处理过程就结束了。


    另外在使用消息 Message 的时候,建议使用 Message 的 obtain() 方法复用全局消息池中的消息。


    2. 消息机制初始化流程


    消息机制初始化流程就是 Handler、Looper 和 MessageQueue 三者的初始化流程,Handler 的初始化流程比较简单,而 Looper 的初始化流程则是从 prepare() 方法开始的,当 Looper 的 prepare() 方法被调用后,Looper 会创建一个消息队列 MessageQueue ,在 MessageQueue 的构造方法中会调用 nativeInit() JNI 方法初始化 Native 层的消息队列,在 NativeMessageQueue 的构造方法中会创建 Native 层的 Looper 实例,而在 Native 层的 Looper 的构造函数中,则会把唤醒事件的文件描述符监控请求的文件描述符添加到 epoll 的兴趣列表中。


    消息机制初始化流程.png


    1.1 Handler 初始化流程


    Handler 的初始化过程比较简单,这个过程中比较特别的两个点分别是不能在没有调用 Looper.prepare() 的线程创建 Handler以及异步 Handler


    Handler 中有好几个构造函数,其中不传 Looper 的构造函数在高版本的 SDK 中已经被声明为弃用了,也就是我们要创建主线程消息处理器的话,就要把 Looper.getMainLooper() 传到 Handler 的构造函数中。


    Handler 的构造函数有一个比较特别的一个 async 参数,async 为 true 时表示该 Handler 是一个异步消息处理器,使用这个 Handler 发送的消息会是异步消息,但是这个构造函数没有开放给我们使用,是系统组件自己用的。


    HandlerCode.png


    1.2 Looper 初始化流程


    之所以我们能在 Activity 中直接用 Handler 给主线程发消息 ,是因为 ActivityThread 的主函数 main() 中初始化了一个主线程专用的 Looper ,也正是这个 Looper 一直在轮询主线程要处理的消息。


    ActivityThread.png


    Looper 的 prepareMainLooper() 方法会调用 prepare() 方法创建一个新的 Looper , prepare() 是一个公共静态方法,如果我们也要开一个新的线程执行一个任务,这个任务也需要放在死循环中执行并等待消息,而我们又不想浪费 CPU 资源的话,就可以通过 Looper.prepare() 来创建线程的 Looper ,也可以直接使用 Android SDK 中 的 HandlerThread ,HandlerThread 内部也维护了一个 Looper。prepare() 方法会把创建好的 Looper 会放在线程局部变量 ThreadLocal 中。


    prepare() 方法可以传入一个 quitAllowed 参数,这个参数默认为 true ,用于指定是否允许退出,假如 quitAllowed 为 false 的话,那在 MessageQueue 的 quit() 方法被调用时就会抛出一个非法状态异常。


    Looper.png


    Looper 的构造函数中创建了 MessageQueue ,下面来看下 MessageQueue 的初始化流程。


    1.3 MessageQueue 初始化流程


    在 MessageQueue 的构造函数中调用了一个 JNI 方法 nativeInit() ,并且把初始化后的 NativeMessageQueue 的指针保存在 mPtr 中,发送消息的时候要用这个指针来唤醒消息轮询线程。


    MessageQueue.png


    nativeInit() 方法中调用了 NativeMessageQueue 的构造函数,在 NativeMessageQueue 的构造函数中创建了一个新的 Native 层的 Looper ,这个 Looper 跟 Java 层的 Looper 没有任何关系,只是在 Native 层实现了一套类似功能的逻辑。


    NativeMessageQueue 的构造函数中创建完 Looper 后,会通过 setForThread() 方法把它设置给当前线程,这个操作类似于把 Looper 放到 ThreadLocal 中。


    NativeMessageQueue.png


    在 Native 层的 Looper 的构造函数中,创建了一个新的唤醒事件文件描述符(eventfd)并赋值给 mWakeEventFd 变量,这个变量是一个唤醒事件描述符,然后再调用 rebuildEpollLocked() 方法重建 epoll 实例,新的事件文件描述符的初始值为 0 ,标志为 EFD_NONBLOCKEFD_CLOEXEC ,关于什么是文件描述符和这两个标志的作用在后面会讲到。


    NativeLooper.png


    rebuildEpollLocked() 方法的实现如下,关于什么是 epoll 后面会讲到,在 rebuildEpollLocked() 方法的最后会遍历请求列表,这个请求列表中的请求有很多地方会添加,比如输入分发器 InputDispatcherregisterInputChannel() 方法中也会添加一个请求到 Native 层 Looper 的请求列表中。


    rebuildEpollLocked().png


    1.4 Unix/Linux 体系架构


    由于 eventfd 和文件描述符都是 Linux 中的概念,所以下面来看一些 Linux 相关的知识。


    Linux 体系架构.png


    Linux 操作系统的体系架构分为用户态内核态(用户空间和内核空间),内核本质上看是一种软件,控制着计算机的硬件资源,并提供上层应用程序运行的环境。


    而用户态就是上层应用程序的活动空间,应用程序的执行,比如依托于内核提供的资源,包括 CPU 资源、存储资源、I/O 资源等,为了让上层应用能够访问这些资源,内核必须为上层应用提供访问的接口,也就是系统调用


    系统调用是受控的内核入口,借助这一机制,进程可以请求内核以自己的名义去执行某些动作,以 API 的形式,内核提供有一系列服务供程序访问,包括创建进程、执行 I/O 以及为进程间通信创建管道等。


    1.5 文件描述符


    Linux 继承了 UNIX 一切皆文件 的思想,在 Linux 中,所有执行 I/O 操作的系统调用都以文件描述符指代已打开的文件,包括管道(pipe)、FIFO、Socket、终端、设备和普通文件,文件描述符往往是数值很小的非负整数,获取文件描述符一般是通过系统调用 open() ,在参数中指定 I/O 操作目标文件的路径名。


    通常由 shell 启动的进程会继承 3 个已打开的文件描述符:



    • 描述符 0 :标准输入,指代为进程提供输入的文件


    • 描述符 1 :标准输出,指代供进程写入输出的文件


    • 描述符 2 :标准错误,指代进程写入错误消息或异常通告的文件



    文件描述符(File Descriptor) 是 Linux 中的一个索引值,系统在运行时有大量的文件操作,内核为了高效管理已被打开的文件会创建索引,用于指向被打开的文件,这个索引就是文件描述符


    1.6 事件文件描述符 eventfd


    eventfd 可以用于线程或父子进程间通信,内核通过 eventfd 也可以向用户空间发送消息,其核心实现是在内核空间维护一个计数器,向用户空间暴露一个与之关联的匿名文件描述符,不同线程通过读写该文件描述符通知或等待对方,内核则通过该文件描述符通知用户程序。


    在 Linux 中,很多程序都是事件驱动的,也就是通过 select/poll/epoll 等系统调用在一组文件描述符上进行监听,当文件描述符的状态发生变化时,应用程序就调用对应的事件处理函数,有的时候需要的只是一个事件通知,没有对应具体的实体,这时就可以使用 eventfd


    与管道(pipe)相比,管道是半双工的传统 IPC 方式,两个线程就需要两个 pipe 文件,而 eventfd 只要打开一个文件,而文件描述符又是非常宝贵的资源,linux 的默认值也只有 1024 个。eventfd 非常节省内存,可以说就是一个计数器,是自旋锁 + 唤醒队列来实现的,而管道一来一回在用户空间有多达 4 次的复制,内核还要为每个 pipe 至少分配 4K 的虚拟内存页,就算传输的数据长度为 0 也一样。这就是为什么只需要通知机制的时候优先考虑使用 eventfd 。


    eventfd 提供了一种非标准的同步机制,eventfd() 系统调用会创建一个 eventfd 对象,该对象拥有一个相关的由内核维护的 8 字节无符号整数,它返回一个指向该对象的文件描述符,向这个文件描述符中写入一个整数会把该整数加到对象值上,当对象值为 0 时,对该文件描述符的 read() 操作将会被阻塞,如果对象的值不是 0 ,那么 read() 会返回该值,并将对象值重置为 0 。


    struct eventfd_ctx {
    struct kref kref;
    wait_queue_head_t wqh;
    __u64 count;
    unsigned int flags;
    int id;
    };

    eventfd_ctx 结构体是 eventfd 实现的核心,其中 wqhcountflags 的作用如下。


    wqh 是等待队列头,所有阻塞在 eventfd 上的读进程挂在该等待队列上。


    count 是 eventfd 计数器,当用户程序在一个 eventfd 上执行 write 系统调用时,内核会把该值加在计数器上,用户程序执行 read 系统调用后,内核会把该值清 0 ,当计数器为 0 时,内核会把 read 进程挂在等待队列头 wqh 指向的队列上。


    有两种方式可以唤醒等待在 eventfd 上的进程,一个是用户态 write ,另一个是内核态的 eventfd_signal ,也就是 eventfd 不仅可以用于用户进程相互通信,还可以用作内核通知用户进程的手段。


    在一个 eventfd 上执行 write 系统调用,会向 count 加上被写入的值,并唤醒等待队列中输入的元素,内核中的 eventfd_signal 函数也会增加 count 的值并唤醒等待队列中的元素。


    flags 是决定用户 read 后内核的处理方式的标志,取值有EFD_SEMAPHOREEFD_CLOEXECEFD_NONBLOCK三个。


    EFD_SEMAPHORE表示把 eventfd 作为一个信号量来使用。


    EFD_NONBLOCK 表示该文件描述符是非阻塞的,在调用文件描述符的 read() 方法时,有该标志的文件描述符会直接返回 -1 ,在调用文件描述符的 write() 方法时,如果写入的值的和大于 0xFFFFFFFFFFFFFFFE ,则直接返回 -1 ,否则就会一直阻塞直到执行 read() 操作。


    EFD_CLOEXEC 表示子进程执行 exec 时会清理掉父进程的文件描述符。


    3. 事件轮询 epoll


    selectpollepoll都是 I/O 多路复用模型,可以同时监控多个文件描述符,当某个文件描述符就绪,比如读就绪或写就绪时,则立刻通知对应程序进行读或写操作,select/poll/epoll 都是同步 I/O ,也就是读写是阻塞的。


    1. epoll 简介

    epoll 是 Linux 中的事件轮询(event poll)机制,是为了同时监听多个文件描述符的 I/O 读写事件而设计的,epoll API 的优点有能高效检查大量文件描述符支持水平和边缘触发避免复杂的信号处理流程灵活性高四个。


    当检查大量的文件描述符时,epoll 的性能延展性比 select() 和 poll() 高很多


    epoll API 支持水平触发边缘触发,而 select() 和 poll() 只支持水平触发,信号驱动 I/O 则只支持边缘触发。


    epoll 可以避免复杂的信号处理流程,比如信号队列溢出时的处理。


    epoll 灵活性高,可以指定我们想检查的事件类型,比如检查套接字文件描述符的读就绪、写就绪或两者同时指定。


    2. 水平触发与边缘触发

    Linux 中的文件描述符准备就绪的通知有水平触发边缘触发两种模式。


    水平触发通知就是文件描述符上可以非阻塞地执行 I/O 调用,这时就认为它已经就绪。


    边缘触发通知就是文件描述符自上次状态检查以来有了新的 I/O 活动,比如新的输入,这时就要触发通知。


    3. epoll 实例

    epoll API 的核心数据结构称为 epoll 实例,它与一个打开的文件描述符关联,这个文件描述符不是用来做 I/O 操作的,而是内核数据结构的句柄,这些内核数据结构实现了记录兴趣列表维护就绪列表两个目的。


    这些内核数据结构记录了进程中声明过的感兴趣的文件描述符列表,也就是兴趣列表(interest list)


    这些内核数据结构维护了处于 I/O 就绪状态的文件描述符列表,也就是就绪列表(ready list),ready list 中的成员是兴趣列表的子集。


    4 epoll API 的 4 个系统调用

    epoll API 由以下 4 个系统调用组成。


    epoll_create() 创建一个 epoll 实例,返回代表该实例的文件描述符,有一个 size 参数,该参数指定了我们想通过 epoll 实例检查的文件描述符个数。


    epoll_creaet1() 的作用与 epoll_create() 一样,但是去掉了无用的 size 参数,因为 size 参数在 Linux 2.6.8 后就被忽略了,而 epoll_create1() 把 size 参数换成了 flag 标志,该参数目前只支持 EPOLL_CLOEXEC 一个标志。


    epoll_ctl() 操作与 epoll 实例相关联的列表,通过 epoll_ctl() ,我们可以增加新的描述符到列表中,把已有的文件描述符从该列表中移除,以及修改代表文件描述符上事件类型的掩码。


    epoll_wait()用于获取 epoll 实例中处于就绪状态的文件描述符。


    5. epoll_ctl()

    epoll_ctl.png


    epoll_ctl() 用于操作与 epoll 实例相关联的列表,成功返回 0 ,失败返回 -1,的 fd 参数指明了要修改兴趣列表中的哪一个文件描述符的设定,该参数可以是代表管道、FIFO、套接字等,甚至可以是另一个 epoll 实例的文件描述符。


    op 参数用于指定要执行的操作,可以选择的值如下。


    EPOLL_CTL_ADD 表示把描述符添加到 epoll 实例 epfd 的兴趣列表中。


    EPOLL_CTL_MOD 表示修改描述符上设定的事件。


    EPOLL_CTL_DEL 表示把文件描述符从 epfd 的兴趣列表中移除。


    6. epoll_wait()

    epoll_wait.png


    epoll_wait() 方法用于获取 epoll 实例中处于就绪状态的文件描述符,其中参数 timeout 就是 MessageQueue 的 next() 方法中的 nextPollTimeoutMillis ,timeout 参数用于确定 epoll_wait() 的阻塞行为,阻塞行为有如下几种。



    • -1 :调用将一直阻塞,直到兴趣列表中的文件描述符有事件产生,或者直到捕捉到一个信号为止

    • 0 :执行一次非阻塞式检查,看兴趣列表中的文件描述符上产生了哪个事件

    • 大于 0 :调用将阻塞至 timeout 毫秒,直到文件描述符上有事件发生,或者捕捉到一个信号为止


    7. epoll 事件

    下面是几个调用 epoll_ctl() 时可以在 ev.events 中指定的位掩码,以及由 epoll_wait() 返回的 evlist[].events 中的值。



    • EPOLLIN:可读取非高优先级的数据

    • EPOLLPRI:可读取高优先级的数据

    • EPOLLRDHUP:套接字对端关闭

    • EPOLLOUT:普通数据可写

    • EPOLLET:采用边缘触发事件通知

    • EPOLLONESHOT:在完成事件通知后禁用检查

    • EPOLLERR:在错误时发生

    • EPOLLHUP:出现挂断


    4. 消息轮询过程


    1. 消息轮询过程概述

    消息循环过程主要是由 Looper 的 loop() 方法、MessageQueue 的 next() 方法、Native 层 Looper 的 pollOnce() 这三个方法组成。


    消息轮询过程是从 Looper 的 loop() 方法开始的,loop() 方法中有一个死循环,死循环中会调用 MessageQueue 的 next() 方法,获取到消息后,loop() 方法就会调用 Message 的 target 的 dispatchMessage() 方法分发消息,target 其实就是最初发送 Message 的 Handler 。loop() 方法最后会调用 recycleUnchecked() 方法回收处理完的消息。


    在 MessageQueue 的 next() 方法中,首先会调用 nativePollOnce() JNI 方法检查队列中是否有新的消息要处理,没有时线程就会被阻塞。有的话就会尝试找出需要优先执行的异步线程,没有异步消息的话,就会判断消息是否到了要执行的时间,是的话就返回给 Looper 处理,否则重新计算消息的执行时间。


    2. Looper.loop()

    前面讲到了在 ActivityThread 的 main() 函数中会调用 Looper 的 loop() 方法让 Looper 开始轮询消息,loop() 方法中有一个死循环,死循环中会调用 MessageQueue 的 next() 方法获取下一条消息,获取到消息后,loop() 方法就会调用 Message 的 target 的 dispatchMessage() 方法,target 其实就是发送 Message 的 Handler 。最后就会调用 Message 的 recycleUnchecked() 方法回收处理完的消息。


    loop().png


    3. MessageQueue.next()

    在 MessageQueue 的 next() 方法中,首先会调用 nativePollOnce() JNI方法检查队列中是否有新的消息要处理,如果没有,那么当前线程就会在执行到 Native 层的 epoll_wait() 时阻塞。如果有消息,而且消息是同步屏障,那就会找出或等待需要优先执行的异步消息。调用完 nativePollOnce() 后,如果没有异步消息,就会判断当前消息是否到了要执行的时间,是的话则返回消息给 Looper 处理,不是的话就重新计算消息的执行时间(when)。在把消息返回给 Looper 后,下一次执行 nativePollOnce() 的 timeout 参数的值是默认的 0 ,所以进入 next() 方法时,如果没有消息要处理,next() 方法中还可以执行 IdleHandler。在处理完消息后,next() 方法最后会遍历 IdleHandler 数组,逐个调用 IdleHandler 的 queueIdle() 方法。


    下图是 MessageQueue 中找出异步消息后的链表变化。


    MessageQueue 异步消息处理机制.png


    光看 next() 方法的代码的话会觉得有点绕。ViewRootImpl 的 scheduleTraversals() 方法在很多地方都会被调用,当 scheduleTraversals() 方法被调用时,ViewRootImpl 就会调用 MessageQueuepostSyncBarrier() 方法插入一个同步屏障到消息链表中,然后再调用 ChoreographerpostCallback() 方法执行一个 View 遍历任务 ,然后再调用 MessageQueue 的 removeSyncBarrier() 方法移除同步屏障。Choreographer 的 postCallback() 方法会调用 postCallbackDelayedInternal() 方法,postCallbackDelayedInternal() 方法会调用 scheduleFrameLocked() 方法,scheduleFrameLock() 方法会从消息池中获取一条消息,并调用 Message 的 setAsynchronous() 方法把这条消息的标志 flags 设为异步标志 FLAG_ASYNCHRONOUS,然后调用内部类 FrameHandlersendMessageAtFrontOfQueue() 方法把异步消息添加到队列中。


    scheduleFrameLocked().png


    下面是 MessageQueue 的 next() 方法的具体实现代码。


    MessageQueue.next().png


    IdleHandler 可以用来做一些在主线程空闲的时候才做的事情,通过 Looper.myQueue().addIdleHandler() 就能添加一个 IdleHandler 到 MessageQueue 中,比如下面这样。


    addIdleHandler().png


    当 IdleHandler 的 queueIdle() 方法返回 false 时,那 MessageQueue 就会在执行完 queueIdle() 方法后把这个 IdleHandler 从数组中删除,下次不再执行。


    4. Looper.pollOnce()(Native 层)

    继续往下看。在 NativeMessageQueuepollOnce() 方法中,会调用 Native 层的 Looper 的 pollOnce() 方法。


    NativeMessageQueuePollOnce.png


    在 Looper 的 pollOnce() 方法中,首先会遍历了响应列表,如果响应的标识符(identifier)ident 值大于等 0 ,则返回标识符,响应是在 pollInner() 方法中添加的。


    NativeLooperPollOnce.png


    6. Looper.pollInner() (Native 层)

    在 pollInner() 方法中,首先会调用 epoll_wait() 获取可用事件,获取不到就阻塞当前线程,否则遍历可用事件数组 eventItems ,如果遍历到的事件的文件描述符是唤醒事件文件描述符 mWakeEventFd ,则调用 awoken()方法 唤醒当前线程。然后还会遍历响应数组信封数组,这两个数组是在 Native 层消息机制里用的,和我们上层用的关系不大,这里就不展开讲了。


    LooperPollInner.png


    awoken() 方法的实现很简单,只是调用了 read() 方法把 mWakeEventFd 的数据读取出来,mWakeEventFd 是一个 eventfd ,eventfd 的特点就是在读的时候它的 counter 的值会重置为 0


    awoken().png


    4. 消息发送机制



    当我们用 Handler 的 sendMessage()sendEmptyMessage()post() 等方法发送消息时, 最终都会走到 Handler 的 enqueueMessage() 方法。Handler 的 enqueueMessage() 又会调用 MessageQueue 的 enqueueMessage() 方法。


    ![sendMessage()](/Users/oushaoze/Documents/Projects/giteePages/assets/images/Android 消息机制/sendMessage().png)


    MessageQueue 的 enqueueQueue() 方法的实现如下。enqueueMessage() 首先会判断,当没有更多消息消息不是延时消息消息的发送时间早于上一条消息这三个条件其中一个成立时,就会把当前消息作为链表的头节点,然后如果 IdleHandler 都执行完的话,就会调用 nativeWake() JNI 方法唤醒消息轮询线程。


    如果把当前消息作为链表的头结点的条件不成立,就会遍历消息链表,当遍历到最后一个节点,或者发现了一条早于当前消息的发送时间的消息,就会结束遍历,然后把遍历结束的最后一个节点插入到链表中。如果在遍历链表的过程中发现了一条异步消息,就不会再调用 nativeWake() JNI 方法唤醒消息轮询线程。


    ![enqueueMessage()](/Users/oushaoze/Documents/Projects/giteePages/assets/images/Android 消息机制/enqueueMessage().png)


    nativeWake() 的实现如下,只是简单调用了 Native 层 Looper 的 wake() 方法。


    nativeWake().png


    Native 层 Looper 的 wake() 方法的实现如下,TEMP_FAILURE_RETRY 是一个用于重试,能返回 EINTR 的函数 ,write() 方法会向唤醒事件文件描述符写入一个 W 字符,这个操作唤醒被阻塞的消息循环线程 。


    LooperWake.png


    5. 消息处理过程


    消息处理过程是从 Looper 的 loop() 方法开始的,当 Looper 从 MessageQueue 中获取下一条要处理的消息后,就会调用 Message 的 target 的 dispatchMessage() 方法,而 target 其实就是发送消息的 Handler 。


    LooperLoop().png


    设置 Message 的 target 的地方就是在 HandlerenqueueMessage() 方法中。


    HandlerEnqueueMessage.png


    在 Handler 的 dispatchMessage() 方法中,如果消息是通过 post() 方法发送,那么 post() 传入的 Runnable 就会作为 msg 的 callback 字段。如果 callback 字段不为空,dispatchMessage() 方法就会调用 callback 的 run() 方法 ,否则调用 Handler 的 callback 或 Handler 本身的 handleMessage() 方法,Handler 的 callback 指的是在创建 Handler 时传入构造函数的 Callback


    dispatchMessage.png


    6. 消息 Message


    下面我们来看下 Message 的实现。Message 中的 what消息的标识符。而 arg1arg2objdata 分别是可以放在消息中的整型数据Object 类型数据Bundle 类型数据when 则是消息的发送时间


    sPool全局消息池,最多能存放 50 条消息,一般建议用 Message 的 obtain() 方法复用消息池中的消息,而不是自己创建一个新消息。如果在创建完消息后,消息没有被使用,想回收消息占用的内存,可以调用 recycle() 方法回收消息占用的资源。如果消息在 Looper 的 loop() 方法中处理了的话,Looper 就会调用 recycleUnchecked() 方法回收 Message 。


    Message.png


    参考资料




    作者:灯不利多
    链接:https://juejin.cn/post/6983598752837664781
    来源:掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    收起阅读 »

    比浏览器 F12 更好用的免费调试抓包工具 Fiddler 介绍

    身为一名前端搬砖工,长久以来有两个问题困扰着我,一个是做后台项目接口返回的数据都为空,不方便做更进一步的对数据的查改及测试;另一个是做移动端的项目,比如 uniapp,每次遇到接口问题都只能 console 在 HBuilder 进行调试,苦不堪言,后来发现我...
    继续阅读 »

    身为一名前端搬砖工,长久以来有两个问题困扰着我,一个是做后台项目接口返回的数据都为空,不方便做更进一步的对数据的查改及测试;另一个是做移动端的项目,比如 uniapp,每次遇到接口问题都只能 console 在 HBuilder 进行调试,苦不堪言,后来发现我司 TE 同学用 Fiddler 进行抓包测试,一问这软件还是免费的,遂进行了一番学习了解,发现可以直接解决刚刚提到的这两个问题,所以在这里做个分享。


    简介



    • Fiddler 是位于客户端和服务器端的 HTTP 代理

    • 目前最常用的 HTTP 抓包工具之一

    • 功能非常强大,是 web 调试的利器

      • 监控浏览器所有的 HTTP/HTTPS 流量

      • 查看、分析请求内容细节

      • 伪造客户端请求和服务器响应

      • 解密 HTTPS 的 web 会话

      • 全局、局部断点功能

      • 第三方插件



    • 使用场景

      • 接口的测试与调试

      • 线上环境调试

      • web 性能分析




    下载


    直接去官网下载 Fiddler Classic 即可:


    image.png


    原理


    学习一件新事物,最好是知其然亦知其所以然,这样遇到问题心里有底,才不容易慌,下面就介绍下 Fiddler 抓包的原理。


    Fiddler 是位于客户端和服务器端之间的 HTTP 代理。一旦启动 Fiddler,其会自动将代理服务器设置成本机,默认端口为 8888,并设置成系统代理(Act as system proxy on startup)。可以在 Fiddler 通过 'Tools -> Options -> Connections' 查看, 图示如下:


    image.png

    在 Fiddler 运行的情况下,以 Chrome 浏览器为例,可以在其 '设置 -> 高级 -> 系统 -> 打开您计算机的代理设置 -> 连接 -> 局域网(LAN)设置' 里看到,'代理服务器' 下的 '为 LAN 使用代理服务器' 选项被勾选了(如果没有运行 Fiddler,默认情况下是不会被勾选的),如下图:


    image (1).png

    点开 '高级',会发现 '要使用的代理服务器地址' 就是本机 ip,端口为 8888。如下图:


    image (2).png

    也就是说浏览器的 HTTP 请求/响应都被代理到了系统的 8888 端口,被 Fiddler 拦截了。


    界面介绍


    下面开始对整个 Fiddler 的界面进行一个庖丁解牛


    工具栏


    image.png
    主要介绍上图中几个标了号的我认为比较常用的功能:



    1. Replay:重放选中的那条请求,同时按下 shift + R 键,可以输入重复发送请求的次数(这些请求是串行发送的)。可以用来做重放攻击的测试。

    2. 删除会话(sessions)

    3. 继续打了断点的请求:打断点后请求会被拦截在 Fiddler,点击这个 Go 继续发送。打断点的方式是点击界面底部的空格,具体位置如下图所示:


    image (1).png



    1. 这个类似瞄准器的工具时用于选择抓取请求的应用:按住不放将鼠标拖放到目标应用即可

    2. 可用于查找某条请求,比如你知道请求参数里的某个字段,可以直接输入进行查找

    3. 编码解码工具,可以进行多种编码的转换,是个人觉得挺好用的一个工具,能够编码的格式包括但不限于 base64、md5 和 URLEncode 等

    4. 可以查看一些诸如本机 ip(包括 IPv4,IPv6) 等信息,就用不着去 cmd 里 输入ipconfig 查看了,如下图:


    image (2).png


    会话列表(Session List)


    位于软件界面的左半部的就是会话列表了,抓取到的每条 http 请求都列在这,每一条被称为一个 session,如下图所示:

    image (3).png


    每条会话默认包含的信息



    • 请求的状态码(result)

    • 协议(protocol)

    • 主机名(host)

    • URL

    • 请求大小(body,以字节为单位)

    • 缓存信息(caching)

    • 响应类型(content-type)

    • 发出请求的 Windows 进程及进程 ID(process)


    自定义列


    除了以上这些,我们还可以添加自定义列,比如想添加一列请求方法信息:



    1. 点击菜单栏 -> Rules -> Customize Rules 调出 Fiddler ScriptEditor 窗口

    2. 按下 ctrl + f 输入 static function Main() 进行查找

    3. 然后在找到的函数 Main 里添加:


    FiddlerObject.UI.lvSessions.AddBoundColumn("Method",60,getHTTPMethod );
    static function getHTTPMethod(oS: Session){
    if (null != oS.oRequest) return oS.oRequest.headers.HTTPMethod;
    else return String.Empty;
    }

    图示如下:


    image (4).png
    4. 按下 ctrl + s 保存。然后就可以在会话列表里看到多出了名为 Method 的一列,内容为请求方法。


    排序和移动



    1. 点击每一列的列表头,可以反向排序

    2. 按住列表头不放进行拖动,可以改变列表位置


    QuickExec 与状态栏


    位于软件界面底部的那条黑色的是 QuickExec,可用于快速执行输入的一些命令,具体命令可输入 help 跳转到官方的帮助页面查看。图示如下:


    image (5).png

    在 QuickExec 下面的就是状态栏,



    1. Capturing:代表目前 Fiddler 的代理功能是开启的,也就是是否进行请求响应的拦截,如果想关闭代理,只需要点击一下 Capturing 图标即可

    2. All Processes:选择抓取的进程,可以只选浏览器进程或是非浏览器进程等

    3. 断点:按一次是请求前断点,也就是请求从浏览器发出到 Fiddler 这停住;再按一次是响应后的断点,也就是响应从服务器发出,到Fiddler 这停住;再按一次就是不打断点

    4. 当前选中的会话 / 总会话数

    5. 附加信息


    辅助标签 + 工具


    位于软件界面右边的这一大块面板,即为辅助标签 + 工具,如下图所示,它拥有 10 个小标签,我们先从 Statistics 讲起,btw,这单词的发音是 [stəˈtɪstɪks],第 3 个字母 a 发 'ə' 的音,而不是 'æ'~


    image (6).png


    Statistics(统计)


    这个 tab 里都是些 http 请求的性能数据分析,如 DNS Lookup(DNS 解析时间)、 TCP/IP Connect(TCP/IP 连接时间)等。


    Inspectors(检查器)


    image.png

    以多种不同的方式查看请求的请求报文和响应报文,比如可以只看头部信息(Headers)、或者是查看请求的原始信息(Raw),再比如请求的参数是 x-www-form-urlencoded 的话,就能在 WebForms 里查看...


    AutoResponder(自动响应器)


    image (1).png

    这是一个我认为比较有用的功能了,它可以篡改从服务器返回的数据,达到欺骗浏览器的目的。


    实战案例


    我在做一个后台项目的时候,因为前台还没弄好,数据库都是没有数据的,在获取列表时,请求得到的都是如下图所示的空数组:


    image.png

    那么在页面上显示的也就是“暂无数据”,这就影响了之后一些删改数据的接口的对接。


    image (2).png

    此时,我们就可以通过 AutoResponder ,按照接口文档的返回实例,对返回的数据进行编辑,具体步骤如下:



    1. 勾选上 Enable rules(激活自动响应器) 和 Unmatched requests passthrough(放行所有不匹配的请求)


    image (3).png

    2. 在左侧会话列表里选中要修改响应的那条请求,按住鼠标直接拖动到 AutoResponder 的面板里,如下图红框所示:


    image (4).png

    3. 选中上图红框里的请求单机鼠标右键,选择 Edit Response...


    image (5).png

    4. 进入编辑面板选择 Raw 标签就可以直接进行编辑了,这里我按照接口文档的返回示例,给 items 数组添加了数据,如下图所示:


    image (6).png

    这样,浏览器接收到数据,页面就如下图所示有了内容,方便进行之后的操作


    image (7).png


    Composer(设计者)


    说完了对响应的篡改,现在介绍的 composer 就是用于对请求的篡改。这个单词的翻译是作曲家,按照我们的想法去修改一个请求,宛如作曲家谱一首乐曲一般。


    image.png

    用法与 AutoResponder 类似,也是可以从会话列表里直接拖拽一个请求到上图红框中,然后对请求的内容进行修改即可。应用场景之一就是可以绕过一些前端用 js 写的限制与验证,直接发送请求,通过返回的数据可以判断后端是否有做相关限制,测试系统的健壮性。


    Filters(过滤器)


    在默认情况下,Filters 会抓取一切能够抓取到的请求,统统列在左侧的会话列表里,如果我们是有目的对某些接口进行测试,就会觉得请求列表很杂乱,这时可以点开 Filters 标签,勾选 Use Filters,启动过滤工具,如下图:


    image.png

    接着就可以根据我们需要对左侧列表里展示的所抓取的接口进行过滤,比如根据 Hosts 进行过滤,只显示 Hosts 为 api.juejin.cn 的请求,就可以如下图在 Hosts 那选择 'Show only the following Hosts',然后点击右上角 Actions 里的 'Run Filterset now' 执行过滤。


    image.png

    过滤的筛选条件还有很多,比如据请求头字段里 URL 是否包含某个单词等,都很简单,一看便知,这里不再一一细说。


    HTTPS 抓包


    默认情况下,Fiddler 没办法显示 HTTPS 的请求,需要进行证书的安装:



    1. 点击 'Tools -> Options...' ,勾选上 'Decrypt HTTPS traffic' (解密HTTPS流量)


    image.png



    1. 点击 Actions 按钮,点击 'Reset All Certicicates' (重置所有证书),之后遇到弹出的窗口,就一直点击 '确定' 或 'yes' 就行了。


    image (1).png



    1. 查看证书是否安装成功:点击 'Open Windows Certificate Manager' 打开 Windows 证书管理器窗口


    image (2).png

    点击 '操作' 选择 '查找证书',在 '包含' 输入框输入 fiddler 进行查找


    image (3).png

    查找结果类似下图即安装证书成功


    image (4).png

    现在会话列表就能成功显示 https 协议的请求了。


    断点应用


    全局断点


    通过 'Rules -> Automatic Breakpoints' 可以给请求打断点,也就是中断请求,断点分为两种:



    1. Before Requests(请求前断点):请求发送给服务器之前进行中断

    2. After Responses(响应后断点):响应返回给客户端之前进行中断


    image.png

    打上断点之后,选中想要修改传输参数的那一条请求,按 R 进行重发,这条请求就会按要求在请求前或响应后被拦截,我们就可以根据需要进行修改,然后点击工具栏的 'Go',或者点击如下图所示的绿色按钮 'Run to Completion',继续完成请求。


    image (1).png

    这样打断点是全局断点,即所有请求都会被拦截,下面介绍局部断点。


    局部断点


    如果只想对某一条请求打断点,则可以在 QuickExec 输入相应的命令执行。



    • 请求前断点



    1. 在 QuickExec 输入 bpu query_adverts 。注意:query_adverts 为请求的 url 的一部分,这样就只有 url 中包含 query_adverts 的请求会被打上断点。


    image (2).png



    1. 按下 Enter 键,可以看到红框中显示 query_adverts 已经被 breakpoint 了,而且是 RequestURI


    image (3).png



    1. 选中 url 中带 query_adverts 的这条请求,按 R 再次发送,在发给服务器前就会被中断(原谅我又拿掘金的请求做例子~)


    image (4).png



    1. 取消断点:在 QuickExec 输入 bpu 按下 Enter 即可



    • 响应后断点


    与请求前断点步骤基本一致,区别在于输入的命令是 bpafter get_today_status
    按下 Enter 后在 'Composer' 标签下点击 'Execute' 执行,再次发送该请求则服务器的响应在发送给浏览器之前被截断,注意下红色的图标,跟之前的请求前断点的区别在于一个是向上的箭头,一个是向下的箭头。


    image (5).png

    取消拦截则是输入 bpafter 后回车,可以看到状态栏显示 'ResponseURI breakpoint cleared'


    image (6).png


    弱网测试


    Fiddler 还可以用于弱网测试,'Rules -> Performance -> 勾选 Simulate Modem Speeds' 即可


    image (7).png

    再次刷新网页会感觉回到了拨号上网的年代,可以测试网站在网速很低的情况下的表现。


    修改网速


    网速还可以修改,点击 'FiddlerScript' 标签,在下图绿框中搜索 simulateM,按几下回车找到 if (m_SimulateModem) 这段代码,可以修改上下传输的速度:


    image (8).png


    安卓手机抓包


    最后一部分主要内容是关于手机抓包的,我用的是小米手机 9,MIUI 12.5.1 稳定版,安卓版本为 11。



    1. 首先保证安装了 Fiddler 的电脑和手机连的是同一个 wifi

    2. 在 Fiddler 中,点击 'Tools -> Options...' ,在弹出的 Options 窗口选择 Connections 标签,勾选 'Allow remote computers to connect'


    image (9).png



    1. 手机打开 '设置 -> WLAN -> 连接的那个 WLAN 的设置' 进入如下图所示的页面


    image (10).png



    1. '代理' 选择 '手动','主机名' 填写电脑的主机名,端口则是 Fiddler 默认监听的 8888,然后点击左上角的 '打钩图标' 进行保存

    2. 下载证书:打开手机浏览器,输入 'http://192.168.1.1:8888' (注意:192.168.1.1 要替换成你电脑的 ip 地址),会出现如下页面


    image (11).png

    点击红框中链接进行证书的下载



    1. 安装证书:打开 '设置 -> 密码与安全 -> 系统安全 -> 加密与凭据 -> 安装证书(从存储设备安装证书)-> 证书 ' 找到刚刚下载的证书进行安装


    image (12).png



    1. 安装完成可以在 '加密与凭据 -> 信任的凭据' 下查看


    image (13).png



    1. 现在 Fiddler 就可以抓到手机里 app 发送的请求了

    2. 最后注意:测试完毕需要关闭手机的 WLAN 代理,否则手机就上不了网了~


    One More Thing


    几个常用快捷键



    • 双击某一条请求:打开该请求的 Inspectors 面板

    • ctrl + X:清除请求列表

    • R:选中某一条请求,按 R 键可重新发送该请求

    • shift+delete:删除除了选中那一条之外的请求



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

    收起阅读 »

    小程序自动化测试入门到实践

    背景 随着小程序项目越来越复杂,业务场景越来多,花费在回归测试上的时间会越来越多,前端自动化测试就非常有必要提上日程。 今天要带来的是: 小程序自动化测试入门教程。 环境 系统 :macOS 微信开发者工具版本: 1.05.2106300 什么是小程序自动化 ...
    继续阅读 »

    背景


    随着小程序项目越来越复杂,业务场景越来多,花费在回归测试上的时间会越来越多,前端自动化测试就非常有必要提上日程。


    今天要带来的是: 小程序自动化测试入门教程


    环境


    系统 :macOS

    微信开发者工具版本: 1.05.2106300


    什么是小程序自动化


    微信官方文档:小程序自动化


    使用小程序自动化 SDK miniprogram-automator,可以在帮助我们在小程序中完成一些事情,比如:控制小程序跳转到指定页面,获取小程序页面数据,获取小程序页面元素状态等。


    配合 jest 就可以实现小程序端自动化测试了。
    话不多说,我们开始吧


    准备




    1. 项目根目录 mini-auto-test-demo 里面准备两个目录 miniprogram 放小程序代码,和 test-e2e 放测试用例代码




     |— mini-auto-test-demo/  // 根目录
    |— miniprogram/ // 小程序代码
    |— pages/
    |— index/ // 测试文件
    |— test-e2e/ // 测试用例代码
    |— index.spec.js // 启动文件
    |— package.json

    index 文件夹下准备用于测试的页面

    <!--index.wxml-->
    <view class="userinfo">
    <view class="userinfo-avatar" bindtap="bindViewTap">
    <open-data type="userAvatarUrl"></open-data>
    </view>
    <open-data type="userNickName"></open-data>
    </view>

    /**index.wxss**/
    .userinfo {
    margin-top: 50px;
    display: flex;
    flex-direction: column;
    align-items: center;
    color: #aaa;
    }
    .userinfo-avatar {
    overflow: hidden;
    width: 128rpx;
    height: 128rpx;
    margin: 20rpx;
    border-radius: 50%;
    }

    // index.js
    // 获取应用实例
    const app = getApp()
    Page({
    data: {
    userInfo: {},
    },
    // 事件处理函数
    bindViewTap() {
    wx.navigateTo({
    url: '../logs/logs'
    })
    }
    })


    1. 微信开发者工具->设置-> 安全设置 -> 打卡服务端口


    image.png



    1. 安装npm包


    如果根目录没有 package.json 文件,先执行


    npm init

    如果根目录已经有 package.json 文件 ,执行以下命令:


    npm install miniprogram-automator jest --save-dev
    npm i jest -g

    安装需要的依赖



    1. 在根目录下新建index.spec.js 文件
    const automator = require('miniprogram-automator')

    automator.launch({
    cliPath: '/Applications/wechatwebdevtools.app/Contents/MacOS/cli', // 工具 cli 位置
    projectPath: '/Users/SONG/Documents/github/mini-auto-test-demo/miniprogram', // 项目文件地址
    }).then(async miniProgram => {
    const page = await miniProgram.reLaunch('/pages/index/index')
    await page.waitFor(500)
    const element = await page.$('.userinfo-avatar')
    console.log(await element.attribute('class'))
    await element.tap()
    await miniProgram.close()
    })

    这里要注意修改为自己的cli位置和项目文件地址:



    1. cliPath:


    可以在应用程序中找到微信开发者工具,点击右键点击"显示包内容"


    image.png


    找到cli后,快捷键 :command+option+c 复制路径, 就拿到了


    image.png



    1. projectPath:


    注意!!项目路径填写的是小程序文件夹miniprogram而不是mini-auto-test-demo


    启动


    写好路径后,在mac终端进入mini-auto-test-demo根目录或 vscode 终端根目录执行命令:


    node index.spec.js

    image.png


    你会发现微信开发者工具被自动打开,并执行了点击事件进入了log页面,终端输出了class的值。
    到此你已经感受到了自动化,接下来你要问了,自动化测试呢?别急,接着往下看。


    自动化测试


    在一开始准备的test-e2e 文件夹下新建integration.test.js文件,


    引入'miniprogram-automator, 连接自动化操作端口,把刚刚index.spec.js中的测试代码,放到 jest it 里,jest相关内容我们这里就不赘述了,大家可以自行学习(其实我也才入门 ̄□ ̄||)。

    const automator = require('miniprogram-automator');

    describe('index', () => {
    let miniProgram;
    let page;
    const wsEndpoint = 'ws://127.0.0.1:9420';
    beforeAll(async() => {
    miniProgram = await automator.connect({
    wsEndpoint: wsEndpoint
    });
    }, 30000);

    it('test index', async() => {
    page = await miniProgram.reLaunch('/pages/index/index')
    await page.waitFor(500)
    const element = await page.$('.userinfo-avatar')
    console.log(await element.attribute('class'))
    await element.tap()
    });
    });

    package.json scripts 添加命令


    "e2e": "jest ./test-e2e integration.test.js --runInBand"

    测试代码写好了,接下来如何运行呢?这里我们提另外一个方法。


    cli 命令行调用


    官方文档:命令行调用

    你一定会问,刚刚我们不是学习了启动运行,这么还要学另外一种方法 o(╥﹏╥)o
    大家都知道,一般团队里都是多人合作的,大家的项目路径都不一样,难道每次还要改projectPath吗?太麻烦了,使用cli就不需要考虑在哪里启动,项目地址在哪里,话不多说,干!


    打开终端进入放微信开发者工具cli文件夹(路径仅供参考):


    cd /Applications/wechatwebdevtools.app/Contents/MacOS 

    执行命令(如果你的微信开发者工具开着项目,先关掉)


    ./cli --auto  /Users/SONG/Documents/github/mini-auto-test-demo/miniprogram  --auto-port 9420

    微信开发者工具通过命令行启动


    image.png


    启动后在项目根目录下执行,可以看到测试通过


    npm run e2e

    image.png


    到此,我们已经可以写测试用例了。这只是入门系列,后续会持续更文,感谢大家的耐心阅读,如果你有任何问题都可以留言给我,摸摸哒



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

    面试官:能不能手写几道链表的基本操作

    反转链表 示例: 输入: 1->2->3->4->5->NULL 输出: 5->4->3->2->1->NULL 循环解决方案 这道题是链表中的经典题目,充分体现链表这种数据结构 操作思路简单 ,...
    继续阅读 »

    反转链表


    示例:


    输入: 1->2->3->4->5->NULL
    输出: 5->4->3->2->1->NULL


    • 循环解决方案


    这道题是链表中的经典题目,充分体现链表这种数据结构 操作思路简单 , 但是 实现上 并没有那么简单的特点。


    那在实现上应该注意一些什么问题呢?


    保存后续节点。作为新手来说,很容易将当前节点的 next 指针直接指向前一个节点,但其实当前节点下一个节点 的指针也就丢失了。因此,需要在遍历的过程当中,先将下一个节点保存,然后再操作 next指向。


    链表结构声定义如下:


    function ListNode(val) {
    this.val = val;
    this.next = null;
    }

    实现如下:

    /**
    * @param {ListNode} head
    * @return {ListNode}
    */
    let reverseList = (head) => {
    if (!head)
    return null;
    let pre = null,
    cur = head;
    while (cur) {
    // 关键: 保存下一个节点的值
    let next = cur.next;
    cur.next = pre;
    pre = cur;
    cur = next;
    }
    return pre;
    };


    • 递归解决方案


    let reverseList = (head) =>{
    let reverse = (pre, cur) => {
    if(!cur) return pre;
    // 保存 next 节点
    let next = cur.next;
    cur.next = pre;
    return reverse(cur, next);
    }
    return reverse(null, head);
    }

    2.区间反转


    反转从位置 m 到 n 的链表。请使用一趟扫描完成反转。


    说明: 1 ≤ m ≤ n ≤ 链表长度。


    示例:


    输入: 1->2->3->4->5->NULL, m = 2, n = 4
    输出: 1->4->3->2->5->NULL

    思路
    这一题相比上一个整个链表反转的题,其实是换汤不换药。我们依然有两种类型的解法:循环解法递归解法


    image.png
    关于前节点和后节点的定义,大家在图上应该能看的比较清楚了,后面会经常用到。


    反转操作上一题已经拆解过,这里不再赘述。值得注意的是反转后的工作,那么对于整个区间反转后的工作,其实就是一个移花接木的过程,首先将前节点的 next 指向区间终点,然后将区间起点的 next 指向后节点。因此这一题中有四个需要重视的节点: 前节点 、 后节点 、 区间起点 和 区间终点 。



    • 循环解法
    /**
    * @param {ListNode} head
    * @param {number} m
    * @param {number} n
    递归解法
    对于递归解法,唯一的不同就在于对于区间的处理,采用递归程序进行处理,大家也可以趁着复习一下
    递归反转的实现。
    * @return {ListNode}
    */
    var reverseBetween = function(head, m, n) {
    let count = n - m;
    let p = dummyHead = new ListNode();
    let pre, cur, start, tail;
    p.next = head;
    for(let i = 0; i < m - 1; i ++) {
    p = p.next;
    }
    // 保存前节点
    front = p;
    // 同时保存区间首节点
    pre = tail = p.next;
    cur = pre.next;
    // 区间反转
    for(let i = 0; i < count; i++) {
    let next = cur.next;
    cur.next = pre;
    pre = cur;
    cur = next;
    }
    // 前节点的 next 指向区间末尾
    front.next = pre;
    // 区间首节点的 next 指向后节点(循环完后的cur就是区间后面第一个节点,即后节点)
    tail.next = cur;
    return dummyHead.next;
    };


    • 递归解法
    var reverseBetween = function(head, m, n) {
    // 递归反转函数
    let reverse = (pre, cur) => {
    if(!cur) return pre;
    // 保存 next 节点
    let next = cur.next;
    cur.next = pre;
    return reverse(cur, next);
    }
    let p = dummyHead = new ListNode();
    dummyHead.next = head;
    let start, end; //区间首尾节点
    let front, tail; //前节点和后节点
    for(let i = 0; i < m - 1; i++) {
    p = p.next;
    }
    front = p; //保存前节点
    start = front.next;
    for(let i = m - 1; i < n; i++) {
    p = p.next;
    }
    end = p;
    tail = end.next; //保存后节点
    end.next = null;
    // 开始穿针引线啦,前节点指向区间首,区间首指向后节点
    front.next = reverse(null, start);
    start.next = tail;
    return dummyHead.next;
    }

    3.两个一组翻转链表


    给定一个链表,两两交换其中相邻的节点,并返回交换后的链表。


    你不能只是单纯的改变节点内部的值,而是需要实际的进行节点交换。


    示例


    给定 1->2->3->4, 你应该返回 2->1->4->3

    思路


    如图所示,我们首先建立一个虚拟头节点(dummyHead),辅助我们分析。


    image.png


    首先让 p 处在 dummyHead 的位置,记录下 p.next 和 p.next.next 的节点,也就是 node1 和
    node2。


    随后让 node1.next = node2.next, 效果:


    image.png


    然后让 node2.next = node1, 效果:


    image.png
    最后,dummyHead.next = node2,本次翻转完成。同时 p 指针指向node1, 效果如下:


    image.png
    依此循环,如果 p.next 或者 p.next.next 为空,也就是 找不到新的一组节点 了,循环结束。



    • 循环解决
    var swapPairs = function(head) {
    if(head == null || head.next == null)
    return head;
    let dummyHead = p = new ListNode();
    let node1, node2;
    dummyHead.next = head;
    while((node1 = p.next) && (node2 = p.next.next)) {
    node1.next = node2.next;
    node2.next = node1;
    p.next = node2;
    p = node1;
    }
    return dummyHead.next;
    };


    • 递归方式


    var swapPairs = function(head) {
    if(head == null || head.next == null)
    return head;
    let node1 = head, node2 = head.next;
    node1.next = swapPairs(node2.next);
    node2.next = node1;
    return node2;
    };

    4.K个一组翻转


    给你一个链表,每 k 个节点一组进行翻转,请你返回翻转后的链表。


    k 是一个正整数,它的值小于或等于链表的长度。


    如果节点总数不是 k 的整数倍,那么请将最后剩余的节点保持原有顺序。


    示例


    给定这个链表:1->2->3->4->5
    当 k = 2 时,应当返回: 2->1->4->3->5
    当 k = 3 时,应当返回: 3->2->1->4->5

    说明 :


    你的算法只能使用常数的额外空间。


    你不能只是单纯的改变节点内部的值,而是需要实际的进行节点交换。


    思路
    思路类似No.3中的两个一组翻转。唯一的不同在于两个一组的情况下每一组只需要反转两个节点,而在K 个一组的情况下对应的操作是将 K 个元素 的链表进行反转。



    • 递归解法
    /**
    * @param {ListNode} head
    * @param {number} k
    * @return {ListNode}
    */
    var reverseKGroup = function(head, k) {
    let pre = null, cur = head;
    let p = head;
    // 下面的循环用来检查后面的元素是否能组成一组
    for(let i = 0; i < k; i++) {
    if(p == null) return head;
    p = p.next;
    }
    for(let i = 0; i < k; i++){
    let next = cur.next;
    cur.next = pre;
    pre = cur;
    cur = next;
    }
    // pre为本组最后一个节点,cur为下一组的起点
    head.next = reverseKGroup(cur, k);
    return pre;
    };


    • 循环解法
    var reverseKGroup = function(head, k) {
    let count = 0;
    // 看是否能构成一组,同时统计链表元素个数
    for(let p = head; p != null; p = p.next) {
    if(p == null && i < k) return head;
    count++;
    }
    let loopCount = Math.floor(count / k);
    let p = dummyHead = new ListNode();
    dummyHead.next = head;
    // 分成了 loopCount 组,对每一个组进行反转
    for(let i = 0; i < loopCount; i++) {
    let pre = null, cur = p.next;
    for(let j = 0; j < k; j++) {
    let next = cur.next;
    cur.next = pre;
    pre = cur;
    cur = next;
    }
    // 当前 pre 为该组的尾结点,cur 为下一组首节点
    let start = p.next;// start 是该组首节点
    // 开始穿针引线!思路和2个一组的情况一模一样
    p.next = pre;
    start.next = cur;
    p = start;
    }
    return dummyHead.next;
    }


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

    收起阅读 »

    前端工程化实战 - 企业级 CLI 开发

    背景 先罗列一些小团队会大概率会遇到的问题: 规范 代码没有规范,每个人的风格随心所欲,代码交付质量不可控 提交 commit 没有规范,无法从 commit 知晓提交开发内容 流程 研发没有流程,没有 prd,没有迭代的需求管理,这个项目到底做了...
    继续阅读 »

    背景


    image.png


    先罗列一些小团队会大概率会遇到的问题:



    1. 规范

      • 代码没有规范,每个人的风格随心所欲代码交付质量不可控

      • 提交 commit 没有规范,无法从 commit 知晓提交开发内容



    2. 流程

      • 研发没有流程,没有 prd,没有迭代的需求管理,这个项目到底做了点啥也不知道



    3. 效率

      • 不断的重复工作,没有技术积累与沉淀



    4. 项目质量

      • 项目没有规范就一定没有质量

      • 测试功能全部靠人工发现与回归,费时费力



    5. 部署

      • 人工构建、部署,刀耕火种般的操作

      • 依赖不统一、人为不可控

      • 没有版本追踪、回滚等功能




    除了上述比较常见的几点外,其余的一些人为环境因素就不一一列举了,总结出来其实就是混乱 + 不舒服


    同时处在这样的一个团队中,团队自身的规划就不明确,个人就更难对未来有一个清晰的规划与目标,容易全部陷于业务不可自拔、无限循环。


    当你处在一个混乱的环境,遇事不要慌(乱世出英雄,为什么不能是你呢),先把事情捋顺,然后定个目标与规划,一步步走。


    工程化


    上述列举的这些问题可以通过引入工程化体系来解决,那么什么是工程化呢?


    广义上,一切以提高效率、降低成本、保障质量为目的的手段,都属于工程化的范畴。


    通过一系列的规范、流程、工具达到研发提效、自动化、保障质量、服务稳定、预警监控等等。


    对前端而言,在 Node 出现之后,可以借助于 Node 渗透到传统界面开发之外的领域,将研发链路延伸到整个 DevOps 中去,从而脱离“切图仔”成为前端工程师。


    image.png


    上图是一套简单的 DevOps 流程,技术难度与成本都比较适中,作为小型团队搭建工程化的起点,性价比极高。


    在团队没有制定规则,也没有基础建设的时候,通常可以先从最基础的 CLI 工具开始然后切入到整个工程化的搭建。


    所以先定一个小目标,完成一个团队、项目通用的 CLI 工具。


    CLI 工具分析


    小团队里面的业务一般迭代比较快,能抽出来提供开发基建的时间与机会都比较少,为了避免后期的重复工作,在做基础建设之前,一定要做好规划,思考一下当前最欠缺的核心与未来可能需要用到的功能是什么?



    Coding 永远不是最难的,最难的是不知道能使用 code 去做些什么有价值的事情。



    image.png


    参考上述的 DevOps 流程,本系列先简单规划出 CLI 的四个大模块,后续如果有需求变动再说。



    可以根据自己项目的实际情况去设计 CLI 工具,本系列仅提供一个技术架构参考。



    构建


    通常在小团队中,构建流程都是在一套或者多套模板里面准备多环境配置文件,再使用 Webpack Or Rollup 之类的构建工具,通过 Shell 脚本或者其他操作去使用模板中预设的配置来构建项目,最后再进行部署之类的。


    这的确是一个简单、通用的 CI/CD 流程,但问题来了,只要最后一步的发布配置不在可控之内,任意团队的开发成员都可以对发布的配置项做修改。


    即使构建成功,也有可能会有一些不可预见的问题,比如 Webpack 的 mode 选择的是 dev 模式、没有对构建代码压缩混淆、没有注入一些全局统一方法等等,此时对生产环境而言是存在一定隐患的


    所以需要将构建配置、过程从项目模板中抽离出来,统一使用 CLI 来接管构建流程,不再读取项目中的配置,而通过 CLI 使用统一配置(每一类项目都可以自定义一套标准构建配置)进行构建。


    避免出现业务开发同学因为修改了错误配置而导致的生产问题。


    质量


    与构建是一样的场景,业务开发的时候为了方便,很多时候一些通用的自动化测试以及一些常规的格式校验都会被忽略。比如每个人开发的习惯不同也会导致使用的 ESLINT 校验规则不同,会对 ESLINT 的配置做一些额外的修改,这也是不可控的一个点。一个团队还是使用同一套代码校验规则最好。


    所以也可以将自动化测试、校验从项目中剥离,使用 CLI 接管,从而保证整个团队的某一类项目代码格式的统一性。


    模板


    至于模板,基本上目前出现的博客中,只要是关于 CLI 的,就必然会有模板功能。


    因为这个一个对团队来说,快速、便捷初始化一个项目或者拉取代码片段是非常重要的,也是作为 CLI 工具来说产出最高、收益最明显的功能模块,但本章就不做过多的介绍,放在后面模板的博文统一写。


    工具合集


    既然是工具合集,那么可以放一些通用的工具类在里面,比如



    1. 图片压缩(png 压缩的更小的那种)、上传 CDN 等

    2. 项目升级(比如通用配置更新了,CLI 提供一键升级模板的功能)

    3. 项目部署、发布 npm 包等操作。

    4. 等等其他一些重复性的操作,也都可以放在工具合集里面


    CLI 开发


    前面介绍了 CLI 的几个模块功能设计,接下来可以正式进入开发对应的 CLI 工具的环节。


    搭建基础架构


    CLI 工具开发将使用 TS 作为开发语言,如果此时还没有接触过 TS 的同学,刚好可以借此项目来熟悉一下 TS 的开发模式。


    mkdir cli && cd cli // 创建仓库目录
    npm init // 初始化 package.json
    npm install -g typescript // 安装全局 TypeScript
    tsc --init // 初始化 tsconfig.json

    全局安装完 TypeScript 之后,初始化 tsconfig.json 之后再进行修改配置,添加编译的文件夹与输出目录。

    {
    "compilerOptions": {
    "target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', 'ES2021', or 'ESNEXT'. */
    "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
    "outDir": "./lib", /* Redirect output structure to the directory. */
    "strict": true, /* Enable all strict type-checking options. */
    "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
    "skipLibCheck": true, /* Skip type checking of declaration files. */
    "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
    },
    "include": [
    "./src",
    ]
    }

    上述是一份已经简化过的配置,但应对当前的开发已经足够了,后续有需要可以修改 TypeScript 的配置项。


    ESLINT


    因为是从 0 开发 CLI 工具,可以先从简单的功能入手,例如开发一个 Eslint 校验模块。


    npm install eslint --save-dev // 安装 eslint 依赖
    npx eslint --init // 初始化 eslint 配置

    直接使用 eslint --init 可以快速定制出适合自己项目的 ESlint 配置文件 .eslintrc.json

    {
    "env": {
    "browser": true,
    "es2021": true
    },
    "extends": [
    "plugin:react/recommended",
    "standard"
    ],
    "parser": "@typescript-eslint/parser",
    "parserOptions": {
    "ecmaFeatures": {
    "jsx": true
    },
    "ecmaVersion": 12,
    "sourceType": "module"
    },
    "plugins": [
    "react",
    "@typescript-eslint"
    ],
    "rules": {
    }
    }


    如果项目中已经有定义好的 ESlint,可以直接使用自己的配置文件,或者根据项目需求对初始化的配置进行增改。


    创建 ESlint 工具类


    第一步,对照文档 ESlint Node.js API,使用提供的 Node Api 直接调用 ESlint。


    将前面生成的 .eslintrc.json 的配置项按需加入,同时使用 useEslintrc:fase 禁止使用项目本身的 .eslintrc 配置,仅使用 CLI 提供的规则去校验项目代码。

    import { ESLint } from 'eslint'
    import { getCwdPath, countTime } from '../util'

    // 1. Create an instance.
    const eslint = new ESLint({
    fix: true,
    extensions: [".js", ".ts"],
    useEslintrc: false,
    overrideConfig: {
    "env": {
    "browser": true,
    "es2021": true
    },
    "parser": getRePath("@typescript-eslint/parser"),
    "parserOptions": {
    "ecmaFeatures": {
    "jsx": true
    },
    "ecmaVersion": 12,
    "sourceType": "module"
    },
    "plugins": [
    "react",
    "@typescript-eslint",
    ],
    },
    resolvePluginsRelativeTo: getDirPath('../../node_modules') // 指定 loader 加载路径
    });


    export const getEslint = async (path: string = 'src') => {
    try {
    countTime('Eslint 校验');
    // 2. Lint files.
    const results = await eslint.lintFiles([`${getCwdPath()}/${path}`]);

    // 3. Modify the files with the fixed code.
    await ESLint.outputFixes(results);

    // 4. Format the results.
    const formatter = await eslint.loadFormatter("stylish");

    const resultText = formatter.format(results);

    // 5. Output it.
    if (resultText) {
    console.log('请检查===》', resultText);
    }
    else {
    console.log('完美!');
    }
    } catch (error) {

    process.exitCode = 1;
    console.error('error===>', error);
    } finally {
    countTime('Eslint 校验', false);
    }
    }

    创建测试项目


    npm install -g create-react-app // 全局安装 create-react-app
    create-react-app test-cli // 创建测试 react 项目

    测试项目使用的是 create-react-app,当然你也可以选择其他框架或者已有项目都行,这里只是作为一个 demo,并且后期也还会再用到这个项目做测试。


    测试 CLI


    新建 src/bin/index.ts, demo 中使用 commander 来开发命令行工具。

    #!/usr/bin/env node // 这个必须添加,指定 node 运行环境
    import { Command } from 'commander';
    const program = new Command();

    import { getEslint } from '../eslint'

    program
    .version('0.1.0')
    .description('start eslint and fix code')
    .command('eslint')
    .action((value) => {
    getEslint()
    })
    program.parse(process.argv);

    修改 pageage.json,指定 bin 的运行 js(每个命令所对应的可执行文件的位置)


     "bin": {
    "fe-cli": "/lib/bin/index.js"
    },

    先运行 tsc 将 TS 代码编译成 js,再使用 npm link 挂载到全局,即可正常使用。



    commander 的具体用法就不详细介绍了,基本上市面大部分的 CLI 工具都使用 commander 作为命令行工具开发,也都有这方面的介绍。



    命令行进入刚刚的测试项目,直接输入命令 fe-cli eslint,就可以正常使用 Eslint 插件,输出结果如下:


    image.png


    美化输出


    可以看出这个时候,提示并没有那么显眼,可以使用 chalk 插件来美化一下输出。


    先将测试工程故意改错一个地方,再运行命令 fe-cli eslint


    image.png


    至此,已经完成了一个简单的 CLI 工具,对于 ESlint 的模块,可以根据自己的想法与规划定制更多的功能。


    构建模块


    配置通用 Webpack


    通常开发业务的时候,用的是 webpack 作为构建工具,那么 demo 也将使用 webpack 进行封装。


    先命令行进入测试项目中执行命令 npm run eject,暴露 webpack 配置项。


    image.png


    从上图暴露出来的配置项可以看出,CRA 的 webpack 配置还是非常复杂的,毕竟是通用型的脚手架,针对各种优化配置都做了兼容,但目前 CRA 使用的还是 webpack 4 来构建。作为一个新的开发项目,CLI 可以不背技术债务,直接选择 webpack 5 来构建项目。



    一般来说,构建工具替换不会影响业务代码,如果业务代码被构建工具绑架,建议还是需要去优化一下代码了。


    import path from "path"

    const HtmlWebpackPlugin = require('html-webpack-plugin')
    const postcssNormalize = require('postcss-normalize');
    import { getCwdPath, getDirPath } from '../../util'

    interface IWebpack {
    mode?: "development" | "production" | "none";
    entry: any
    output: any
    template: string
    }

    export default ({
    mode,
    entry,
    output,
    template
    }: IWebpack) => {
    return {
    mode,
    entry,
    target: 'web',
    output,
    module: {
    rules: [{
    test: /\.(js|jsx)$/,
    use: {
    loader: getRePath('babel-loader'),
    options: {
    presets: [
    ''@babel/preset-env',
    ],
    },
    },
    exclude: [
    getCwdPath('./node_modules') // 由于 node_modules 都是编译过的文件,这里做过滤处理
    ]
    },
    {
    test: /\.css$/,
    use: [
    'style-loader',
    {
    loader: 'css-loader',
    options: {
    importLoaders: 1,
    },
    },
    {
    loader: 'postcss-loader',
    options: {
    postcssOptions: {
    plugins: [
    [
    'postcss-preset-env',
    {
    ident: "postcss"
    },
    ],
    ],
    },
    }
    }
    ],
    },
    {
    test: /\.(woff(2)?|eot|ttf|otf|svg|)$/,
    type: 'asset/inline',
    },
    {
    test: [/\.bmp$/, /\.gif$/, /\.jpe?g$/, /\.png$/],
    loader: 'url-loader',
    options: {
    limit: 10000,
    name: 'static/media/[name].[hash:8].[ext]',
    },
    },
    ]
    },
    plugins: [
    new HtmlWebpackPlugin({
    template,
    filename: 'index.html',
    }),
    ],
    resolve: {
    extensions: [
    '',
    '.js',
    '.json',
    '.sass'
    ]
    },
    }
    }

    上述是一份简化版本的 webpack 5 配置,再添加对应的 commander 命令。


    program
    .version('0.1.0')
    .description('start eslint and fix code')
    .command('webpack')
    .action((value) => {
    buildWebpack()
    })

    现在可以命令行进入测试工程执行 fe-cli webpack 即可得到下述构建产物


    image.png


    image.png


    下图是使用 CRA 构建出来的产物,跟上图的构建产物对一下,能明显看出使用简化版本的 webpack 5 配置还有很多可优化的地方,那么感兴趣的同学可以再自行优化一下,作为 demo 已经完成初步的技术预研,达到了预期目标。


    image.png


    此时,如果熟悉构建这块的同学应该会想到,除了 webpack 的配置项外,构建中绝大部分的依赖都是来自测试工程里面的,那么如何确定 React 版本或者其他的依赖统一呢?


    常规操作还是通过模板来锁定版本,但是业务同学依然可以自行调整版本依赖导致不一致,并不能保证依赖一致性。


    既然整个构建都由 CLI 接管,只需要考虑将全部的依赖转移到 CLI 所在的项目依赖即可。


    解决依赖


    Webpack 配置项新增下述两项,指定依赖跟 loader 的加载路径,不从项目所在 node_modules 读取,而是读取 CLI 所在的 node_modules。


    resolveLoader: {
    modules: [getDirPath('../../node_modules')]
    }, // 修改 loader 依赖路径
    resolve: {
    modules: [getDirPath('../../node_modules')],
    }, // 修改正常模块依赖路径

    同时将 babel 的 presets 模块路径修改为绝对路径,指向 CLI 的 node_modules(presets 会默认从启动路劲读取依赖)。

    {
    test: /\.(js|jsx)$/,
    use: {
    loader: getRePath('babel-loader'),
    options: {
    presets: [
    getRePath('@babel/preset-env'),
    [
    getRePath("@babel/preset-react"),
    {
    "runtime": "automatic"
    }
    ],
    ],
    },
    },
    exclude: [
    [getDirPath('../../node_modules')]
    ]
    }

    完成依赖修改之后,一起测试一下效果,先将测试工程的依赖 node_modules 全部删除


    image.png


    再执行 fe-cli webpack,使用 CLI 依赖来构建此项目。


    image.png


    image.png


    可以看出,已经可以在项目不安装任何依赖的情况,使用 CLI 也可以正常构建项目了。


    那么目前所有项目的依赖、构建已经全部由 CLI 接管,可以统一管理依赖与构建流程,如果需要升级依赖的话可以使用 CLI 统一进行升级,同时业务开发同学也无法对版本依赖进行改动。



    这个解决方案要根据自身的实际需求来实施,所有的依赖都来源于 CLI 工具的话,版本升级影响会非常大也会非常被动,要做好兼容措施。比如哪些依赖可以取自项目,哪些依赖需要强制通用,做好取舍。



    写给迷茫 Coder 们的一段话


    如果遇到最开始提到那些问题的同学们,应该会经常陷入到业务中无法自拔,而且写这种基础项目,是真的很花时间也很枯燥。容易对工作厌烦,对 coding 感觉无趣。


    这是很正常的,绝大多数人都有这段经历与类似的想法,但还是希望你能去多想想,在枯燥、无味、重复的工作中去发现痛点、机会。只有接近业务、熟悉业务,才有机会去优化、革新、创造。


    所有的基建都是要依托业务才能发挥最大的作用


    每天抽个半小时思考一下今天的工作还能在哪些方面有所提高,提高效率的不仅仅是你的代码也可以是其他的工具或者是引入新的流程。


    同时也不要仅仅限制在思考阶段,有想法就争取落地,再多抽半小时进行 coding 或者找工具什么的,但凡能够提高个几分钟的效率,即使是个小工具、多几行代码、换个流程这种也值得去尝试一下。


    等你把这些零碎的小东西、想法一点点全部积累起来,到最后整合到一个体系中去,那么此时你会发现已经可以站在更高一层的台阶去思考、规划下一阶段需要做的事情,而这其中所有的经历都是你未来成长的基石。


    一直相信一句话:努力不会被辜负,付出终将有回报。此时敲下去的每一行代码在未来都将是你登高的一步步台阶。



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

    收起阅读 »

    完了,又火一个前端项目

    今天逛 GitHub 的时候,在趋势榜上看到一个项目,竟然短短一天的时间内,涨了 1000 多个星星! 就是这个名为 solid 的项目: 要知道日增上千 star 可是非常难得的,我不禁感到好奇,点进去看看这个项目到底有啥牛逼的? 啥是 Solid? 这是...
    继续阅读 »

    今天逛 GitHub 的时候,在趋势榜上看到一个项目,竟然短短一天的时间内,涨了 1000 多个星星!


    就是这个名为 solid 的项目:



    要知道日增上千 star 可是非常难得的,我不禁感到好奇,点进去看看这个项目到底有啥牛逼的?


    啥是 Solid?


    这是一个国外的前端项目,截止到发文前,已经收获了 8400 个 star。


    我总觉得这个项目很眼熟,好像之前也看到过,于是去 Star History 上搜了一下这个项目的 star 增长历史。好家伙,这几天的增速曲线几乎接近垂直,已经连续好几天增长近千了!


    项目 Star 增长曲线


    看到这个曲线,我想起来了,solid 是一个 JavaScript 框架,此前在一次 JavaScript 框架的性能测试中看到过它。


    要知道,现在的 JavaScript 开发框架基本就是 React、Vue、Angular 三分天下,还有就是新兴的 Svelte 框架潜力无限(近 5w star),其他框架想分蛋糕还是很难的。那么 Solid 到底有什么本事,能让他连续几天 star 数暴涨呢?


    描述


    打开官网,官方对 Solid 的描述是:一个用于构建用户界面的 声明性 JavaScript 库,特点是高效灵活。


    顺着官网往下看,Solid 有很多特点,比如压缩后的代码体积只有 6 kb;而且天然支持 TypeScript 以及 React 框架中经常编写的 JSX(JavaScript XML)。


    来看看官网给的示例代码:


    Solid 语法


    怎么样,他的语法是不是和 React 神似?


    性能


    但是,这些并不能帮助 Solid 框架脱颖而出,真正牛逼的一点是它 非常快


    有多快呢?第一够不够 !


    JS 框架性能测试对比


    有同学说了,你这不睁着眼睛说瞎话么?Solid 明明是第二,第一是 Vanilla 好吧!


    哈哈,但事实上,Vanilla 其实就是不使用任何框架的纯粹的原生 JavaScript,通常作为一个性能比较的基准。


    那么 Solid 为什么能做到这么快呢?甚至超越了我们引以为神的 Vue 和 React。


    这是因为 Solid 没有采用其他主流前端框架中的 Virtual DOM,而是直接被静态编译为真实的原生 DOM 节点,并且将更新控制在细粒度的局部范围内。从而让 runtime(运行时)更加轻小,也不需要所谓的脏检查和摘要循环带来的额外消耗,使得性能和原生 JavaScript 几乎无异。换句话说,编译后的 Solid 其实就是 JavaScript!



    其实 Solid 的原理和新兴框架 Svelte 的原理非常类似,都是编译成原生 DOM,但为啥他更快一点呢?


    为了搞清楚这个问题,我打开了百度来搜这玩意,但发现在国内根本搜不到几条和 Solid.js 有关的内容,基本全是一些乱七八糟的东西。后来还是在 Google 上搜索,才找到了答案,结果答案竟然还是来自于某乎的大神伊撒尔。。。


    要搞清楚为什么 Solid 比 Svelte 更快,就要看看同一段代码经过它们编译后,有什么区别。


    大神很贴心地举了个例子,比如这句代码:


    <div>{aaa}</div>

    经 Svelte 编译后的代码:

    let a1, a2
    a1 = document.creatElement('div')
    a2 = docment.createTextNode('')
    a2.nodeValue = ctx[0] // aaa
    a1.appendChild(a2)

    经 Solid 编译后的代码:

    let a1, a2
    let fragment = document.createElement('template')
    fragment.innerHTML = `<div>aaa</div>`
    a1 = fragment.firstChild
    a2 = a1.fristChild
    a2.nodeValue = data.aaa

    可以看到,在创建 DOM 节点时,原来 Solid 耍了一点小把戏,利用了 innerHTML 代替 createElement 来创建,从而进一步提升了性能。


    当然,抛去 Virtual DOM 不意味着就是 “银弹” 了,毕竟十年前各种框架出现前大家也都是写原生 JavaScript,轻 runtime 也有缺点,这里就不展开说了。


    除了快之外,Solid 还有一些其他的特点,比如语法精简、WebComponent 友好(可自定义元素)等。




    总的来说, 我个人还是非常看好这项技术的,日后说不定能和 Svelte 一起动摇一下三巨头的地位,给大家更多的选择呢?这也是技术选型好玩的地方,没有绝对最好的技术,只有最适合的技术。


    不禁感叹道:唉,技术发展太快了,一辈子学不完啊!(不过前端初学者不用关心那么多,老老实实学基础三件套 + Vue / React 就行了)


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

    收起阅读 »

    判断是否完全二叉树

    Hello: ? 今天又和小伙伴们见面啦,最近一直做二叉树相关的题目今天再和大家分享一道相关的题目《判断是不是完全二叉树》 判断是否是完全二叉树 查看全部源码:点击查看全部源码 介绍-什么是完全二叉树? 先看如下这一张图: ...
    继续阅读 »

    Hello:


    ? 今天又和小伙伴们见面啦,最近一直做二叉树相关的题目今天再和大家分享一道相关的题目《判断是不是完全二叉树》


    判断是否是完全二叉树


    查看全部源码:点击查看全部源码


    介绍-什么是完全二叉树?


    先看如下这一张图:










    这个一颗二叉树,如何区分该树是不是完全二叉树呢?



    • 当一个节点存在右子节点但是不存在左子节点这颗树视为非完全二叉树

    • 当一个节点的左子节点存在但是右子节点不存在视为完全二叉树

    • 如果没有子节点,那也是要在左侧开始到右侧依次没有子节点才视为完全二叉树,就像上图2中



    而上面第一张图这颗二叉树很明显是一颗非完全二叉树,因为在第三层也就是在节点2它并没有右子节点。在6和4节点中隔开了一个节点(2节点没有右子节点),所以不是完全二叉树


    再看第二张图,这颗树就是一个完全二叉树,虽然在这个颗节点3没有右子节点,但是6 4 5节点之间并没有空缺的子节点,这里就解释了上面说的第三条(如何没有子节点,那也是在左侧开始到右侧依次没有子节点才视为完全二叉树)



    流程


    这道题可以使用按层遍历的方式来解决:



    • 首先准备一个队列,按层遍历使用队列是最好的一种解决方法

    • 首先将头节点加入到队列里面(如果头节点为空,你可以认为它是一个非完全二叉树也可以认为它是完全二叉树)

    • 遍历该队列跳出遍历的条件是直到这个队列为空时

    • 这个时候需要准备一个Bool的变量,如果当一个节点的左子节点或者右子节点不存在时将其置成true

    • 当Bool变量为true并且剩余节点的左或右子节点不为空该树就是非完全二叉树

    • 当一树的左子节点不存在并且右子节点存在,该树也是非完全二叉树


    代码


    树节点


    type TreeNode struct {
    val string
    left *TreeNode
    right *TreeNode
    }

    测试代码


    func main() {
    root := &TreeNode{val: "1"}
    root.left = &TreeNode{val: "2"}
    root.left.left = &TreeNode{val: "4"}
    root.left.right = &TreeNode{val: "10"}
    root.left.left.left = &TreeNode{val: "7"}
    root.right = &TreeNode{val: "3"}
    root.right.left = &TreeNode{val: "5"}
    root.right.right = &TreeNode{val: "6"}
    if IsCompleteBt(root) {
    fmt.Println("是完全二叉树")
    } else {
    fmt.Println("不是完全二叉树")
    }
    }

    判断树是否为完全二叉树代码


    // IsCompleteBt 这里默认根节点为空属于完全二叉树,这个可以自已定义是否为完全二叉树/***/
    func IsCompleteBt(root *TreeNode) bool {
    if root == nil {
    return true
    }

    /**
    * 条件:
    * 1.当一个节点存在右子节点但是不存在左子节点这颗树视为非完全二叉树
    * 2.当一个节点的左子节点存在但是右子节点不存在视为完全二叉树
    */

    var tempNodeQueue []*TreeNode

    tempNodeQueue = append(tempNodeQueue, root)

    var tempNode *TreeNode
    isSingleNode := false
    for len(tempNodeQueue) != 0 {
    tempNode = tempNodeQueue[0]
    tempNodeQueue = tempNodeQueue[1:]

    if (isSingleNode && (tempNode.left != nil || tempNode.right != nil)) || (tempNode.left == nil && tempNode.right != nil){
    return false
    }

    if tempNode.left != nil{
    tempNodeQueue = append(tempNodeQueue,tempNode.left)
    }else{
    isSingleNode = true
    }

    if tempNode.right != nil {
    tempNodeQueue = append(tempNodeQueue, tempNode.right)
    }else{
    isSingleNode = true
    }
    }
    return true
    }

    代码解读


    这段代码里面没有多少好说的,就说下for里面第一个if判断叭


    这里看下上面流程中最后两个条件,当满足最后两个条件的时候才可以判断出来这颗树是否是完全二叉树.



    同样因为实现判断是否是完全二叉树是通过对树的按层遍历来处理的,因为对树的按层遍历通过队列是可以间单的实现的。所以这里使用到了队列



    至于这里为什么要单独创建一个isSingleNode变量:



    • 因为当有一个节点左侧节点或者是右侧的节点没有的时候,在这同一层后面如果还有不为空的节点时,那么这颗树便不是完全二叉树,看下图


    image-20210707163759637


    在这颗树的最后一层绿色涂鸭处是少一个节点的,所以我用了一个变量我标识当前节点(在上图表示节点2)的子节点是不是少一个,如果少了当前节点(在上图表示节点2)的下一个节点(在上图表示节点3)的子节点(在上图表示4和5)如果存在则不是完全二叉树,所以这就是创建了一个isSingleNode变量的作用


    运行结果


    image-20210707150308392


    作者:我与晚风同行
    链接:https://juejin.cn/post/6982109128395063304
    来源:掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    收起阅读 »

    Android 依赖注入 hilt 库的使用

    hilt官网 1-什么是控制反转和依赖注入? IOC(控制反转):全称是 Inverse of Control , 是一种思想.指的是让第3方去控制去创建对象. DI(依赖注入):全称是 Dependency Injection , 对象的创建是通过注入...
    继续阅读 »

    hilt官网


    1-什么是控制反转和依赖注入?


    IOC(控制反转):全称是 Inverse of Control , 是一种思想.指的是让第3方去控制去创建对象.


    DI(依赖注入):全称是 Dependency Injection , 对象的创建是通过注入的方式实现. 是IOC的一种具体实现.


    2- 为啥要用依赖注入?


    在java中我们创建对象都是通过new Object(), 或者是使用反射泛型进行创建, 需要指定泛型, 需要继承或者实现某接口, 不够灵活, 举个例子: 比如在使用MVVM模式进行网络请求时,我们通常在ViewModel定义Repository层,然后把Api传递给Repository层. 最后在ViewModel中发起接口请求


    // 定义网络接口
    interface MainApi {
    default void requestList() {}
    }

    // 仓库抽象类
    abstract class BaseRepo{}

    // 首页仓库
    class MainRepo extends BaseRepo {
    private MainApi api;
    public MainRepo(MainApi api) {
    this.api = api;
    }
    void requestList() {
    // 具体调用接口
    api.requestList();
    }
    }

    // 抽象ViewModel层
    abstract class BaseViewModel {}

    // ViewModel层
    class MainViewModel extends BaseViewModel {
    MainRepo repo = new MainRepo(new MainApi() {});

    void requestList(){
    // 通过repo请求接口
    repo.requestList();
    }
    }

    问题: 每次都要在Model层创建Repository对象和Api对象,这是重复且冗余的.


    解决方案: 通过在ViewModel层和Repo层指定泛型,然后反射创建


    // 定义网络接口
    interface MainApi {
    default void requestList() {
    }
    }

    // 仓库抽象类
    abstract class BaseRepo<Api> {
    private Api api;

    public Api getApi() {
    return api;
    }

    public void setApi(Api api) {
    this.api = api;
    }
    }

    // 首页仓库
    class MainRepo extends BaseRepo<MainApi> {
    void requestList() {
    // 具体调用接口
    getApi().requestList();
    }
    }

    // 抽象ViewModel层
    abstract class BaseViewModel<R extends BaseRepo> {
    private R repo;

    public BaseViewModel() {
    try {
    repo = crateRepoAndApi(this);
    } catch (Exception e) {
    e.printStackTrace();
    }
    }

    public R getRepo() {
    return repo;
    }
    // 反射创建Repo和Api
    public R crateRepoAndApi(BaseViewModel<R> model) throws Exception {
    Type repoType = ((ParameterizedType) model.getClass().getGenericSuperclass()).getActualTypeArguments()[0];
    R repo = (R) repoType.getClass().newInstance();
    Type apiType = ((ParameterizedType) repoType.getClass().getGenericSuperclass()).getActualTypeArguments()[0];
    String apiClassPath = apiType.getClass().toString().replace("class ", "").replace("interface ", "");
    repo.setApi(Class.forName(apiClassPath));
    return repo;
    }
    }

    // ViewModel层
    class MainViewModel extends BaseViewModel<MainRepo> {
    void requestList() {
    // 通过repo请求接口
    getRepo().requestList();
    }
    }

    通过反射可以避免在ViewModel里写new Repo()和new api()的代码. 除了反射还有没有更好的实现方式呢?


    image.png


    3-jectpack 中 hilt库的使用方法


    1-引入包


    1-在项目最外层build.gralde引入
    classpath 'com.google.dagger:hilt-android-gradle-plugin:2.37'

    2-在app模块顶部
    plugin "dagger.hilt.android.plugin"
    plugin "kotlin-kapt"

    3-在app模块内,最外层添加纠正错误类型
    kapt {
    correctErrorTypes true
    }

    4-添加依赖
    implementation 'com.google.dagger:hilt-android:2.37'
    kapt 'com.google.dagger:hilt-compiler:2.37'

    2-必须在Application子类上添加注解@HiltAndroidApp


    @HiltAndroidApp
    class MyApp : Application() {
    override fun onCreate() {
    super.onCreate()
    }
    }

    @HiltAndroidApp 创建一个容器.该容器遵循 Android 的生命周期类,目前支持的类型是: Activity, Fragment, View, Service, BroadcastReceiver @Inject


    使用 @Inject 来告诉 Hilt 如何提供该类的实例,常用于构造方法,非私有字段,方法中。


    Hilt 有关如何提供不同类型的实例信息也称之为绑定


    @Module


    module 是用来提供一些无法用 构造@Inject 的依赖,如第三方库,接口,build 模式的构造等。


    使用 @Module 注解的类,需要使用 @InstallIn 注解指定 module 的范围


    增加了 @Module 注解的类,其实代表的就是一个模块,并通过指定的组件来告诉在那个容器中可以使用绑定安装。


    @InstallIn


    使用 @Module 注入的类,需要使用 @InstallIn 注解指定 module 的范围。


    例如使用 @InstallIn(ActivityComponent::class) 注解的 module 会绑定到 activity 的生命周期上。


    @Provides


    常用于被 @Module 注解标记类的内部方法上。并提供依赖项对象。


    @EntryPoint Hilt 支持最常见的 Android 类 Application、Activity、Fragment、View、Service、BroadcastReceiver 等等,但是您可能需要在Hilt 不支持的类中执行依赖注入,在这种情况下可以使用 @EntryPoint 注解进行创建,Hilt 会提供相应的依赖。

    收起阅读 »

    用了postman,接口测试不用愁了

    Postman是一个功能强大的接口测试工具,不仅可以调用http接口也可以发送https请求,满足日常测试工作的需求。 一、下载 官网:https://www.postman.com 1.选择需要下载的版本号 2.双击下载的安装包,进入到用户登录和...
    继续阅读 »


    Postman是一个功能强大的接口测试工具,不仅可以调用http接口也可以发送https请求,满足日常测试工作的需求。


    一、下载


    官网:https://www.postman.com


    1.选择需要下载的版本号



    2.双击下载的安装包,进入到用户登录和注册的页面


    若个人使用,点击下方Skip and go to the app进入到postman的主页面。


    若企业或团队使用,可以先注册账号加入到团队工作区



    二、postman界面


    1.界面导航说明



    2.请求体选择


    form-data:是post请求当中常用的一种,将表单数据处理为一条消息,以标签为单元,用分隔符分开。既可以单独上传键值对,也可以直接上传文件(当上传字段是文件时,会有Content-Type来说明文件类型,但该文件不会作为历史保存,只能在每次需要发送请求的时候,重新添加文件)


    x-www-form-urlencoded:对应信息头
    application/x-www-form-urlencoded,将所表单中的数据转换成键值对的形式。


    raw:可以上传任意类型的文本,比如text、JavaScript、json、HTML、XML。一般输出为json格式,请求头为Content-Type:application/json 。使用时要用花括号{}将数据包裹起来,才能够正常转化成json格式。


    binary:对应请求头Content-Type:application/octet-stream,只能上传二进制文件且没有键值对,一次只能上传一个文件。



    三、请求方法


    GET:用于从API访问数据用于从API访问数据


    POST:创建新的数据


    PUT:更新数据,全部更新


    PATCH:更新数据,部分更新


    DELETE:删除现有数据



    四、发送一个http请求


    1.get请求


    在URL处填写请求的地址信息,有请求参数的填写在Params中,点击Send,就可以在下面的窗口中查看到响应的json数据。



    2.post请求


    在URL处填写请求的地址信息,选择请求体格式,输入json格式的数据,点击Send发送请求


    在这里插入图片描述


    3.接口响应数据解析


    其中Body和Status是做接口测试的重点,一般来说接口测试都会验证响应体中的数据和响应状态码是否正确。


    Test Results是在编写断言后,可以查看断言的执行结果。


    Time和Size在做性能测试时,可以根据这两个参数来对所测接口的性能做一个简单的判断。


    在这里插入图片描述


    Pretty:在postman中响应结果默认展示的是pretty,数据经过格式化后看起来更加直观,并且显示行号。


    Raw:返回的数据是文本格式,也就是未经处理过的原始数据。


    Preview:一般对返回HTML的页面效果比较明显,如请求百度后返回中可以直接看到页面。



    五、发送https请求设置


    主界面的右上面点击工具标志–选择“Setting”,进入到设置页面。



    在General选项中将SSL certificate verification设为ON,即打开https请求开关。



    在Certificate选项中将CA Certificate开关设置为ON,然后点击Add Certificate,进入到证书添加页面。



    填写请求的地址加端口号,上传CA证书秘钥,设置完成后回到主页面可以发起https请求了。



    六、接口管理(Collection)


    日常工作中接口测试涉及到一个或多个系统中的很多用例需要维护,那么就需要对用例进行分类管理。postman中的Collection可以实现这个功能。


    用例分类管理,方便后期维护


    可以批量执行用例,实现接口自动化测试


    1.创建集合目录


    在Collection选项中,点击“+”号,即可添加一个集合目录,右键可以对目录进行重命名、添加子目录或添加请求等。或者点击集合后面的“…”号,也可查看到更多操作。




    创建好的用例管理效果,如图显示:



    2.批量执行用例


    选中一个Collection,点击右上角的RUN,进入到Collection Runner界面,默认会把所有的用例选中,点击底部的Run Collection按钮执行用例。


    用了postman,接口测试不用愁了



    断言统计:左上角Passed和Failed都为0,表示当前Collection中断言执行的成功数和失败数,如果没有断言默认都为0。


    View Summary:运行结果总览,点击可以看到每个请求的具体断言详细信息。


    Run Again:将Collection中的用例重新运行一次


    New:返回到Runner界面,重新选择用例集合


    Export Results:导出运行结果,默认为json格式


    七、日志查看


    接口测试过程中报错时少不了去查看请求的日志信息,postman中提供了这个功能,可以方便定位问题。


    方法一:点击主菜单View–>Show Postman Console


    方法二:主界面左下角的“Console”按钮



    点击Show Postman Console,进入到日志界面,可以在搜索栏中输入搜索的URL,也可以过滤日志级别



    搜索框:通过输入URL或者请求的关键字进行查找。


    ALL Logs:这里有Log、Info、Warning、Error级别的日志。


    Show raw log:点开可以查看到原始请求的报文信息


    Show timestamps:勾选后展示请求的时间


    Hide network:把请求都隐藏掉,只查看输出日志


    八、断言


    断言是做自动化测试的核心,没有断言,那么只能叫做接口的功能测试,postman中提供的断言功能很强大,内置断言很多也很方便使用。


    点击主界面Tests,在右侧显示框中展示了所有内置断言。按接口响应的组成划分,有状态行、响应头、响应体。


    状态行断言:


    断言状态码:Status code: code is 200


    断言状态信息:Status code:code name has string


    响应头断言:


    断言响应头中包含:Response headers:Content-Type header check


    响应体断言:


    断言响应体中包含XXX字符串:Response body:Contains string


    断言响应体等于XXX字符串:Response body : is equal to a string


    断言响应体(json)中某个键名对应的值:Response body : JSON value check


    响应时间断言:


    断言响应时间:Response time is less than 200ms


    用了postman,接口测试不用愁了


    例如:


    点击右侧的状态码断言,显示在Tests下面的窗口中,点击send发送请求后,在返回的Test Results中可以查看到断言结果。




    以上是整理的postman中常用方法,掌握后对于接口测试来说非常方便,也有利于用例的维护。



    收起阅读 »

    Android集成开发google登录

    这是我参与新手入门的第2篇文章 背景 项目在要Google Play上架,并支持全球下载,加了google登录 一.准备 google登录集成地址 在google登录中创建并配置项目:console.developers.google...
    继续阅读 »

    这是我参与新手入门的第2篇文章


    背景



    项目在要Google Play上架,并支持全球下载,加了google登录



    一.准备


    google登录集成地址



    1. 在google登录中创建并配置项目:console.developers.google.com


    在控制面板选择Credentials → New Project,会提示创建项目名称和组织名称,如下图


    WX20210708-135551.png 2. 创建项目成功后开始创建OAuth client ID image.png 应用类型选择为Android


    image.png 根据系统提示,名称, packageName以及SHA-1值 获取SHA-1值的方式: keytool -keystore path-to-debug-or-production-keystore -list -v


    image.png


    创建成功后会生成一个Client ID 一定要保存好,集成的时候要用



    PS: 如果是通过集成文档创建成功的,会提示下载credentials.json文件,一定要下,不然可能会坑



    image.png


    二.集成开发



    PS: google登录需要运行在Android 4.1及以上且Google Play 服务 15.0.0及以上版本




    • 把刚才下载的credentials.json文件放入app路径的根目录

    • 检查项目顶级build.gradle中包含Maven库


    allprojects {
    repositories {
    google()

    // If you're using a version of Gradle lower than 4.1, you must instead use:
    // maven {
    // url 'https://maven.google.com'
    // }
    }
    }

    在app的build.gradle中引用google play服务


    dependencies {
    implementation 'com.google.android.gms:play-services-auth:19.0.0'
    }

    添加登录



    • 配置 Google Sign-in 和 GoogleSignInClient 对象


    var mGoogleSignInClient: GoogleSignInClient? = null
    private fun initGoogle() {
    val gso = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
    .requestIdToken(CommonConstants.GOOGLE_CLIENT_ID)
    .requestEmail()
    .build()
    mGoogleSignInClient = GoogleSignIn.getClient(requireActivity(), gso)
    }


    CommonConstants.GOOGLE_CLIENT_ID 为创建项目成功后的Client ID




    • 调起登录


    private fun signIn() {
    val signInIntent: Intent = mGoogleSignInClient?.signInIntent!!
    startActivityForResult(signInIntent, RC_SIGN_IN)
    }


    • onActivityResult中接收消息


        private val RC_SIGN_IN: Int = 3000
    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    if (requestCode == RC_SIGN_IN) {
    // The Task returned from this call is always completed, no need to attach
    // a listener.
    val task: Task<GoogleSignInAccount> = GoogleSignIn.getSignedInAccountFromIntent(data)
    handleSignInResult(task)
    }
    super.onActivityResult(requestCode, resultCode, data)
    }

    private fun handleSignInResult(completedTask: Task<GoogleSignInAccount>) {
    try {
    val account = completedTask.getResult(ApiException::class.java)
    // Signed in successfully, show authenticated UI.
    Log.e("handleSignInResult", account.toString())
    Log.e("handleSignInResult_displayName", account?.displayName!!)
    Log.e("handleSignInResult_email", account?.email!!)
    Log.e("handleSignInResult_familyName", account?.familyName!!)
    Log.e("handleSignInResult_givenName", account?.givenName!!)
    Log.e("handleSignInResult_id", account?.id!!)
    Log.e("handleSignInResult_idToken", account?.idToken!!)
    Log.e("handleSignInResult_isExpired", account?.isExpired.toString())
    Log.e("handleSignInResult_photoUrl", account?.photoUrl.toString())
    } catch (e: ApiException) {
    // The ApiException status code indicates the detailed failure reason.
    // Please refer to the GoogleSignInStatusCodes class reference for more information.
    Log.e("handleSignInResult", "signInResult:failed code=" + e.statusCode)
    }
    }

    检查现有用户是否登录


     val account = GoogleSignIn.getLastSignedInAccount(activity)
    updateUI(account);

    退出登录


    val gso = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
    .requestIdToken(CommonConstants.GOOGLE_CLIENT_ID)
    .requestEmail()
    .build()
    val mGoogleSignInClient = GoogleSignIn.getClient(activity, gso)
    mGoogleSignInClient.signOut().addOnCompleteListener(activity) { }

    拿到token信息后发送至自己的服务进行校验,至此google登录完成


    作者:是芝麻吖
    链接:https://juejin.cn/post/6982454523621015589
    来源:掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    收起阅读 »

    政策工具类-谷歌AndroidAppBundle(aab)政策海外发行

    作者 大家好,我是怡寶; 本人于18年毕业于湖南农业大学,于2021年6月加入37手游安卓团队; 目前负责于海外游戏发行安卓开发。 背景 根据Google Play的政策要求,自 2021 年 8 月起,Google Play 将开始要求新应用使用 ...
    继续阅读 »

    作者


    大家好,我是怡寶;


    本人于18年毕业于湖南农业大学,于2021年6月加入37手游安卓团队;


    目前负责于海外游戏发行安卓开发。


    背景


    根据Google Play的政策要求,自 2021 年 8 月起,Google Play 将开始要求新应用使用 Android App Bundle(以下简称aab) 进行发布。该格式将取代 APK 作为标准发布格式。


    想了解更多关于aab的介绍可以直接阅读android官方文档,有详细的说明developer.android.com/guide/app-b…


    juejin1


    正常情况:直接Android Studio上面点击打包或者用Gradle命令直接生成一个aab,交给运营提包到Google Play商店上面去,任务完成,下班~ 。


    存在问题:我没有工程,也没有源码,到我手上的就只有一个apk,走google提供的方案就不行了。


    思 考:我们常做的事情是把apk拿过来,反编译一下,修改修改代码,换换参数,然后重新打成新apk。 apk和aab都是同一个项目生成的,代码资源都一样,那么可不可以相互转化?


    查资料ing.....


    本文向大家介绍如何从apk一步步转化成aab,文末提供本文所使用到的工具&python脚本源码


    需要工具



    apk生成aab


    Android Studio打包可选Android App Bundle(aab),并提供详细教程,本文不再说明。


    解压apk


    通过apktool去解压apk包


    java -jar apktool_2.5.0.jar d test.apk -s -o decode_apk_dir

    解压apk后 decode_apk_dir 目录结构:


    ./decode_apk_dir
    ├── AndroidManifest.xml
    ├── apktool.yml
    ├── assets
    ├── classes2.dex
    ├── classes.dex
    ├── lib
    ├── original
    ├── res
    └── unknown

    编译资源


    编译资源使用aapt2编译生成 *.flat文件集合


    aapt2 compile --dir decode_apk_dir\res -o compiled_resources.zip

    生成compiled_resources.zip文件


    为什么要加.zip的后缀,不和谷歌官方文档一样直接生成compiled_resources文件,或者compiled_resources文件夹。此处为了windows能正常的编译打包,linux和mac随意~


    关联资源


    aapt2 link --proto-format -o base.apk -I android_30.jar \
    --min-sdk-version 19 --target-sdk-version 29 \
    --version-code 1 --version-name 1.0 \
    --manifest decode_apk_dir\AndroidManifest.xml \
    -R compiled_resources.zip --auto-add-overlay

    生成base.apk


    解压base.apk


    通过unzip解压到base文件夹,目录结构:


    ./base
    ├── AndroidManifest.xml
    ├── res
    └── resources.pb

    拷贝资源


    以base文件夹为根目录


    创建 base/manifest 将 base/AndroidManifest.xml 剪切过来


    拷贝assets , 将 ./temp/decode_apk_dir/assets 拷贝到 ./temp/base/assets


    拷贝lib, 将 ./temp/decode_apk_dir/lib 拷贝到 ./temp/base/lib


    拷贝unknown, 将 ./temp/decode_apk_dir/unknown 拷贝到 ./temp/base/root


    拷贝kotlin, 将 ./temp/decode_apk_dir/kotlin拷贝到 ./temp/base/root/kotlin


    拷贝META-INF,将./temp/decode_apk_dir/original/META-INF 拷贝到 ./temp/base/root/META-INF (删除签名信息***.RSA**、.SF.MF)


    创建./base/dex 文件夹,将 ./decode_apk_dir/*.dex(多个dex都要一起拷贝过来)


    base/manifest                        ============> base/AndroidManifest.xml
    decode_apk_dir/assets ============> base/assets
    decode_apk_dir/lib ============> base/lib
    decode_apk_dir/unknown ============> base/root
    decode_apk_dir/kotlin ============> base/root/kotlin
    decode_apk_dir/original/META-INF ============> base/root/META-INF
    decode_apk_dir/*.dex ============> base/dex/*.dex

    最终的目录结构


    base/
    ├── assets
    ├── dex
    ├── lib
    ├── manifest
    ├── res
    ├── resources.pb
    └── root

    压缩资源


    将base文件夹,压缩成base.zip 一定要zip格式


    编译aab


    打包app bundle需要使用bundletool


    java -jar bundletool-all-1.6.1.jar build-bundle \
    --modules=base.zip --output=base.aab

    aab签名


    jarsigner -digestalg SHA1 -sigalg SHA1withRSA \
    -keystore luojian37.jks \
    -storepass ****** \
    -keypass ****** \
    base.aab \
    ******

    注意:您不能使用 apksigner 为 aab 签名。签名aab的时候不需要使用v2签名,使用JDK的普通签名就行。


    测试


    此时我们已经拿到了一个aab的包,符合Google Play的上架要求,那么我们要确保这个aab的包是否正常呢?作为一个严谨的程序员还是得自己测一下。


    上传Google Play


    上传Google Play的内部测试,通过添加测试用户从Google Play去下载到手机测试。更加能模拟真实的用户环境。


    bundletool安装aab(推荐)


    每次都上传到Google Play上面去测试,成本太高了,程序员一般没上传权限,运营也不在就没法测试了。此时我们可以使用bundletool模拟aab的安装。


    连接好手机,调好adb,执行bundletool命令进行安装


    1.从 aab 生成一组 APK


    java -jar bundletool-all-1.6.1.jar build-apks \
    --bundle=base.aab \
    --output=base.apks \
    --ks=luojian37.jks \
    --ks-pass=pass:****** \
    --ks-key-alias=****** \
    --key-pass=pass:******

    2.将 APK 部署到连接的设备


    java -jar bundletool-all-1.6.1.jar install-apks --apks=base.apks

    还原成apk


    竟然apk可以转化成aab,同样aab也可以生成apk,而且更加简单


    java -jar bundletool-all-1.6.1.jar build-apks \
    --mode=universal \
    --bundle=base.aab \
    --output=test.apks \
    --ks=luojian37.jks \
    --ks-pass=pass:****** \
    --ks-key-alias=****** \
    --key-pass=pass:******

    此时就可以或得一个test.apks的压缩包,解压这个压缩包就有一个universal.apk,和开始转化的apk几乎一样。


    获取工具&源码


    github.com/37sy/build_…


    作者:37手游安卓团队
    链接:https://juejin.cn/post/6982111395621896229
    来源:掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    收起阅读 »

    一行代码实现欢迎引导页-GuidePage

    GuidePageGuidePage for Android 是一个App欢迎引导页。一般用于首次打开App时场景,通过引导页指南,概述App特色等相关信息功能介绍 链式调用,简单易用 自定义配置,满足各种需求引入Maven:<dep...
    继续阅读 »

    GuidePage

    GuidePage for Android 是一个App欢迎引导页。一般用于首次打开App时场景,通过引导页指南,概述App特色等相关信息

    功能介绍

    •  链式调用,简单易用
    •  自定义配置,满足各种需求


    引入

    Maven:

    <dependency>
    <groupId>com.king.guide</groupId>
    <artifactId>guidepage</artifactId>
    <version>1.0.0</version>
    <type>pom</type>
    </dependency>

    Gradle:

    //AndroidX
    implementation 'com.king.guide:guidepage:1.0.0'

    Lvy:

    <dependency org='com.king.guide' name='guidepage' rev='1.0.0'>
    <artifact name='$AID' ext='pom'></artifact>
    </dependency>
    如果Gradle出现compile失败的情况,可以在Project的build.gradle里面添加如下:(也可以使用上面的GitPack来complie)
    allprojects {
    repositories {
    //...
    maven { url 'https://dl.bintray.com/jenly/maven' }
    }
    }

    示例

    代码示例

        //简单调用示例
    GuidePage.load(intArrayOf(R.drawable.guide_page_1,R.drawable.guide_page_2,R.drawable.guide_page_3,R.drawable.guide_page_4))
    .pageDoneDrawableResource(R.drawable.btn_done)
    .start(this)//Activity or Fragment
          //Demo中的调用示例
    GuidePage.load(intArrayOf(R.drawable.guide_page_1,R.drawable.guide_page_2,R.drawable.guide_page_3,R.drawable.guide_page_4))
    .pageDoneDrawableResource(R.drawable.btn_done)
    // .indicatorDrawableResource(R.drawable.indicator_radius)
    // .indicatorSize(this,6f)//默认5dp
    .showSkip(v.id == R.id.btn1)//是否显示“跳过”
    .lastPageHideSkip(true)//最后一页是否隐藏“跳过”
    .onGuidePageChangeCallback(object : GuidePage.OnGuidePageChangeCallback{//引导页改变回调接口

    override fun onPageDone(skip: Boolean) {
    //TODO 当点击完成(立即体验)或者右上角的跳过时,触发此回调方法
    //这里可以执行您的逻辑,比如跳转到APP首页或者登陆页
    if(skip){
    Toast.makeText(this@MainActivity,"跳过",Toast.LENGTH_SHORT).show()
    }else{
    Toast.makeText(this@MainActivity,"立即体验",Toast.LENGTH_SHORT).show()
    }
    }

    })
    .start(this)//Activity or Fragment

    相关说明

    • 通过GuidePage链式调用,可以满足一些基本需求场景。
    • GuidePage中提供的配置无法满足需求时,可通过资源命名相同方式去自定义配置,即:资源覆盖方式。如dimensstyles等对应的资源。

    更多使用详情,请查看app中的源码使用示例

    代码下载:GuidePage.zip

    收起阅读 »

    Android通用的Adapter、Activity、Fragment、Dialog等-base

    BaseBase是针对于Android开发封装好一些常用的基类,主要包括通用的Adapter、Activity、Fragment、Dialog等、和一些常用的Util类,只为更简单。Base 3.x 在Base 2.x 的基础上进行了重构,最大的变化...
    继续阅读 »

    Base

    Base是针对于Android开发封装好一些常用的基类,主要包括通用的Adapter、Activity、Fragment、Dialog等、和一些常用的Util类,只为更简单。

    Base 3.x 在Base 2.x 的基础上进行了重构,最大的变化是将base-adapter和base-util提取了出来。

    单独提取library主要是为了模块化,使其更加独立。在使用时需要用哪个库就引入库,这样就能尽可能的减少引入库的体积。

    • base 主要是封装了常用的Activity、Fragment、DialogFragment、Dialog等作为基类,方便使用。
    • base-adapter 主要是封装了各种Adapter、简化自定义Adapter步骤,让写自定义适配器从此更简单。
    • base-util 主要是封装了一些常用的工具类。

    AndroidX version

    引入

    Maven:

    //base
    <dependency>
    <groupId>com.king.base</groupId>
    <artifactId>base</artifactId>
    <version>3.2.1</version>
    <type>pom</type>
    </dependency>

    //base-adapter
    <dependency>
    <groupId>com.king.base</groupId>
    <artifactId>adapter</artifactId>
    <version>3.2.1</version>
    <type>pom</type>
    </dependency>

    //base-util
    <dependency>
    <groupId>com.king.base</groupId>
    <artifactId>util</artifactId>
    <version>3.2.1</version>
    <type>pom</type>
    </dependency>

    Gradle:

    //---------- AndroidX 版本
    //base
    implementation 'com.king.base:base:3.2.1-androidx'

    //base-adapter
    implementation 'com.king.base:adapter:3.2.1-androidx'

    //base-util
    implementation 'com.king.base:util:3.2.1-androidx'


    //---------- Android 版本
    //base
    implementation 'com.king.base:base:3.2.1'

    //base-adapter
    implementation 'com.king.base:adapter:3.2.1'

    //base-util
    implementation 'com.king.base:util:3.2.1'

    Lvy:

    //base
    <dependency org='com.king.base' name='base' rev='3.2.1'>
    <artifact name='$AID' ext='pom'></artifact>
    </dependency>

    //base-adapter
    <dependency org='com.king.base' name='adapter' rev='3.2.1'>
    <artifact name='$AID' ext='pom'></artifact>
    </dependency>

    //base-util
    <dependency org='com.king.base' name='util' rev='3.2.1'>
    <artifact name='$AID' ext='pom'></artifact>
    </dependency>
    如果Gradle出现compile失败的情况,可以在Project的build.gradle里面添加如下:(也可以使用上面的GitPack来complie)
    allprojects {
    repositories {
    //...
    maven { url 'https://dl.bintray.com/jenly/maven' }
    }
    }

    引入的库:

    //---------- AndroidX 版本
    //base
    compileOnly 'androidx.appcompat:appcompat:1.0.0+'
    compileOnly 'com.king.base:util:3.2.1-androidx'

    //base-adapter
    compileOnly 'androidx.appcompat:appcompat:1.0.0+'
    compileOnly 'androidx.recyclerview:recyclerview:1.0.0+'

    //base-util
    compileOnly 'androidx.appcompat:appcompat:1.0.0+'
    //---------- Android 版本
    //base
    compileOnly 'com.android.support:appcompat-v7:28.0.0'
    compileOnly 'com.king.base:util:3.2.1'

    //base-adapter
    compileOnly 'com.android.support:appcompat-v7:28.0.0'
    compileOnly 'com.android.support:recyclerview-v7:28.0.0'

    //base-util
    compileOnly 'com.android.support:appcompat-v7:28.0.0'

    简要说明:

    Base主要实用地方体现在:出统一的代码风格,实用的各种基类,BaseActivity和BaseFragment里面还有许多实用的代码封装,只要用了Base,使用Fragment就感觉跟使用Activtiy基本是一样的。

    代码示例:

    通用的Adapter

    /**
    *
    * 只需继承通用的适配器(ViewHolderAdapter或ViewHolderRecyclerAdapter),简单的几句代码,妈妈再也不同担心我写自定义适配器了。
    */
    public class TestAdapter extends ViewHolderAdapter<String> {


    public TestAdapter(Context context, List<String> listData) {
    super(context, listData);
    }

    @Override
    public View buildConvertView(LayoutInflater layoutInflater,T t,int position, ViewGroup parent) {
    return inflate(R.layout.list_item,parent,false);
    }

    @Override
    public void bindViewDatas(ViewHolder holder, String s, int position) {
    holder.setText(R.id.tv,s);
    }
    }

    基类BaseActivity

    public class TestActivity extends BaseActivity {

    private TextView tv;
    private Button btn;

    @Override
    public void initUI() {
    //TODO:初始化UI
    setContentView(R.layout.activity_test);
    tv = findView(R.id.tv);
    btn = findView(R.id.btn);
    }

    @Override
    public void initData() {
    //TODO:初始化数据(绑定数据)
    tv.setText("text");
    }

    }

    GestureActivity

    public class TestGestureActivity extends GestureActivity {

    private TextView tv;
    private Button btn;

    @Override
    public void initUI() {
    //TODO:初始化UI
    setContentView(R.layout.activity_test);
    tv = findView(R.id.tv);
    btn = findView(R.id.btn);
    }

    @Override
    public void initData() {
    //TODO:初始化数据(绑定数据)
    tv.setText("text");
    }

    @Override
    public void onLeftFling() {
    //TODO:向左滑动
    }

    @Override
    public boolean onRightFling() {
    //TODO:向右滑动,默认执行finish,返回为true表示拦截事件。
    return false;
    }
    }

    SplashActivity

    public class TestSplashActivity extends SplashActivity {
    @Override
    public int getContentViewId() {
    return R.layout.activity_splash;
    }

    @Override
    public Animation.AnimationListener getAnimationListener() {
    return new Animation.AnimationListener() {
    @Override
    public void onAnimationStart(Animation animation) {

    }

    @Override
    public void onAnimationEnd(Animation animation) {
    //TODO: 启动动画结束,可执行跳转逻辑
    }

    @Override
    public void onAnimationRepeat(Animation animation) {

    }
    };
    }
    }

    BaseFragment

    public class TestFragment extends BaseFragment {
    @Override
    public int inflaterRootView() {
    return R.layout.fragment_test;
    }

    @Override
    public void initUI() {
    //TODO:初始化UI
    }

    @Override
    public void initData() {
    //TODO:初始化数据(绑定数据)
    }

    }

    BaseDialogFragment

    public class TestDialogFragment extends BaseDialogFragment {
    @Override
    public int inflaterRootView() {
    return R.layout.fragment_test_dialog;
    }

    @Override
    public void initUI() {
    //TODO:初始化UI
    }

    @Override
    public void initData() {
    //TODO:初始化数据(绑定数据)
    }


    }

    WebFragment

        WebFragment实现基本webView功能

    其他小功能

    使用Log: 统一控制管理Log

     LogUtils.v();

    LogUtils.d();

    LogUtils.i();

    LogUtils.w();

    LogUtils.e();

    LogUtils.twf();

    LogUtils.println();

    使用Toast

     showToast(CharSequence text);

    showToast(@StringRes int resId);

    使用Dialog

     showDialog(View v);
     showProgressDialog();

    showProgressDialog(@LayoutRes int resId);

    showProgressDialog(View v);

    App中的源码使用示例或直接查看API帮助文档。更多实用黑科技,请速速使用Base体会吧。

    代码下载:Base.zip

    收起阅读 »

    Android 路线规划和导航的地图帮助类库-MapHelper

    MapHelperMapHelper for Android 是一个整合了高德地图、百度地图、腾讯地图、谷歌地图等相关路线规划和导航的地图帮助类库。功能介绍 简单易用,一句代码实现 地图路线规划/导航 GCJ-02 /&...
    继续阅读 »


    MapHelper

    Image


    MapHelper for Android 是一个整合了高德地图、百度地图、腾讯地图、谷歌地图等相关路线规划和导航的地图帮助类库。

    功能介绍

    •  简单易用,一句代码实现
    •  地图路线规划/导航
    •  GCJ-02 / WGS-84 / BD09LL 等相关坐标系互转

    Gif 展示

    Image

    引入

    Maven:

    <dependency>
    <groupId>com.king.map</groupId>
    <artifactId>maphelper</artifactId>
    <version>1.0.0</version>
    <type>pom</type>
    </dependency>

    Gradle:

    implementation 'com.king.map:maphelper:1.0.0'

    Lvy:

    <dependency org='com.king.map' name='maphelper' rev='1.0.0'>
    <artifact name='$AID' ext='pom'></artifact>
    </dependency>
    如果Gradle出现compile失败的情况,可以在Project的build.gradle里面添加如下:(也可以使用上面的GitPack来complie)
    allprojects {
    repositories {
    //...
    maven { url 'https://dl.bintray.com/jenly/maven' }
    }
    }

    示例

    代码示例

    Kotlin 示例
        //调用相关地图线路/导航示例(params表示一些具体参数)

    //跳转到地图(高德、百度、腾讯、谷歌地图等)
    MapHelper.gotoMap(params)
    //跳转到高德地图
    MapHelper.gotoAMap(params)
    //跳转到百度地图
    MapHelper.gotoBaiduMap(params)
    //跳转腾讯地图
    MapHelper.gotoTencentMap(params)
    //跳转到谷歌地图
    MapHelper.gotoGoogleMap(params)
    //坐标系转换:WGS-84转GCJ-02(火星坐标系)
    MapHelper.wgs84ToGCJ02(lat,lng)
    //...更多示例详情请查看MapHelper
    Java 示例
        //调用相关地图线路/导航示例(params表示一些具体参数)

    //跳转到地图(高德、百度、腾讯、谷歌地图等)
    MapHelper.INSTANCE.gotoMap(params);
    //跳转到高德地图
    MapHelper.INSTANCE.gotoAMap(params);
    //跳转到百度地图
    MapHelper.INSTANCE.gotoBaiduMap(params);
    //跳转腾讯地图
    MapHelper.INSTANCE.gotoTencentMap(params);
    //跳转到谷歌地图
    MapHelper.INSTANCE.gotoGoogleMap(params);
    //坐标系转换:WGS-84转GCJ-02(火星坐标系)
    MapHelper.INSTANCE.wgs84ToGCJ02(lat,lng);
    //...更多示例详情请查看MapHelper

    更多使用详情,请查看app中的源码使用示例或直接查看API帮助文

    代码下载:MapHelper.zip

    收起阅读 »

    一文读懂JavaScript函数式编程重点-- 实践 总结

    什么是函数式编程?函数式编程是一种思维方式,函数式编程与命令式编程最大的不同其实在于:函数式编程关心数据的映射,命令式编程关心解决问题的步骤。函数式编程的初衷来, 也就是: 希望可以允许程序员用计算来表示程序, 用计算的组合来表达程序的组合, 而非函数式编程则...
    继续阅读 »

    什么是函数式编程?

    函数式编程是一种思维方式,函数式编程与命令式编程最大的不同其实在于:

    函数式编程关心数据的映射,命令式编程关心解决问题的步骤。函数式编程的初衷来, 也就是: 希望可以允许程序员用计算来表示程序, 用计算的组合来表达程序的组合, 而非函数式编程则习惯于用命令来表示程序, 用命令的顺序执行来表达程序的组合。

    好记性不如烂笔头,有时间将JS函数式编程,在JS方面毕竟有限,如果真要学习好函数式编程,建议学习下Haskell,本文就是将关于JS方面知识点尽可能总结全面。

    • 柯里化
    • 偏应用
    • 组合与管道
    • 函子
    • Monad

    1. 柯里化

    • 什么是柯里化呢?

    柯里化是把一个多参数函数转化为一个嵌套的一元函数的过程。下面我们用介绍柯里化时候很多文章都会使用的例子,加法例子(bad smile)。

    // 原始版本
    const add = (x,y) => x + y;

    // ES6 柯里化版本
    const addCurried = x => y => x + y;

    你没有看错,就是这么简单,柯里化就是将之前传入的多参数变为传入单参数,解释下,柯里化版本,其实当传入一个参数addCurried(1)时,实际会返回一个函数 y=>1+y,实际上是将add函数转化为含有嵌套的一元函数的addCurried函数。如果要调用柯里化版本,应该使用addCurried(1)(2)方式进行调用 会达到和add(1,2)一样的效果,n 个连续箭头组成的函数实际上就是柯里化了 n - 1次,前 n - 1 次调用,其实是提前将参数传递进去,并没有调用最内层函数体,最后一次调用才会调用最内层函数体,并返回最内层函数体的返回值。

    看到这里感觉是不是很熟悉,没错,React 中间件。

    以上是通过ES6箭头函数实现的,下面我们构建curryFn来实现这个过程。

    此函数应该比较容易理解,比较函数参数以及参数列表的长度,递归调用合并参数,当参数都为3,不满足,调用fn.apply(null, args)。

    例子: 使用以上的curryFn 数组元素平方函数式写法。

    const curryFn = (fn) => {
    if(typeof fn !== 'function'){
    throw Error ('Not Function');
    }
    return function curriedFn(...args){
    if(args.length < fn.length){
    return function(){
    return curriedFn.apply(null, args.concat(
    [].slice.call(arguments)
    ))
    }
    }
    return fn.apply(null, args);
    }
    }
    const map = (fn, arr) => arr.map(fn);
    const square = (x) => x * x;
    const squareFn = curryFn(map)(square)([1,2,3])

    从上例子可以观察出curryFn函数应用参数顺序是从左到右。如果想从右到左,下面一会会介绍。

    2. 偏应用

    上面柯里化我们介绍了我们对于传入多个参数变量的情况,如何处理参数关系,实际开发中存在一种情况,写一个方法,有些参数是固定不变的,即我们需要部分更改参数,不同于柯里化得全部应用参数。

    const partial = function (fn, ...partialArgs) {
    let args = partialArgs;
    return function(...fullArguments) {
    let arg = 0;
    for (let i = 0; i < args.length && arg < fullArguments.length; i++) {
    if (args[i] === null) {
    args[i] = fullArguments[arg++];
    }
    }
    return fn.apply(null, args)
    }
    }
    partial(JSON.stringify,null,null,2)({foo: 'bar', bar: 'foo'})


    应用起来 2 这个参数是不变的,相当于常量。简单解释下这个函数,args指向 [null, null, 2], fullArguments指向 [{foo:'bar', bar:'foo'}] ,当i==0时候 ,这样 args[0] ==fullArguments[0],所以args就为[{foo:'bar', bar:'foo'},null,2],然后调用,fn.apply(null, args)。

    3. 组合与管道

    组合

    组合与管道的概念来源于Unix,它提倡的概念大概就是每个程序的输出应该是另一个未知程序的输入。我们应该实现的是不应该创建新函数就可以通过compose一些纯函数解决问题。

    • 双函数情况
    const compose = (a, b) => c => a(b(c))

    我们来应用下:

    const toNumber = (num) => Number(num);
    const toRound = (num)=> Math.round(num);
    // 使用compose
    number = compose(toRound,toNumber)('4.67'); // 5
    • 多函数情况

    我们重写上面例子测试:

    const compose = (...fns) => (value) => fns.reverse().reduce((acc, fn) => fn(acc), value)
    const toNumber = (num) => Number(num);
    const toRound = (num)=> Math.round(num);
    const toString = (num) => num.toString();
    number = compose(toString,toRound,toNumber)('4.67'); // 字符串 '5'

    从上面多参数以及双参数情况,我们可以得出compose的数据流是从右到左的。那有没有一种数据流是从左到右的,答案是有的就是下面我们要介绍的管道。

    管道

    管道我们一般称为pipe函数,与compose函数相同,只不过是修改了数据流流向而已。

    const pipe = (...fns) => (value) => fns.reduce((acc, fn) => fn(acc), value);
    const toNumber = (num) => Number(num);
    const toRound = (num)=> Math.round(num);
    const toString = (num) => num.toString();
    number = compose(toString,toRound,toNumber)('4.67'); // 数字 5

    4. 函子

    函子(Functor)即用一种纯函数的方式帮我们处理异常错误,它是一个普通对象,并且实现了map函数,在遍历每个对象值得时候生成一个新对象。我们来看几个实用些的函子。

    • MayBe 函子
    • // MayBe 函数定义
      const MayBe = function (val) {
      this.value = val;
      }
      MayBe.of = function (val) {
      return new MayBe(val);
      }
      // MayBe map 函数定义
      MayBe.prototype.isNothing = function () {
      return (this.value === null || this.value === underfind)
      }
      MayBe.prototype.map = function (fn) {
      return this.isNothing() ? MayBe.of(null) : MayBe.of(fn(this.value));
      }


    MayBe并不关心参数是否为null或者underfind,因为它已经被MayBe函子抽象出来了,代码不会因为null或者underfind崩溃,可以看出,通过函子我们不需要关系那些特殊情况下的判断,程序也不会以为的崩溃。

    另外一点是,当都多个map链式调用时,如果第一个map参数是null或者underfind,并不会影响到第二个map正常运行,也就是说,任何map的链式调用都会调用到。

    MayBe.of('abc').map((x)=>x.toUpperCase()) // MayBe { value: 'ABC' }

    // 参数为null
    MayBe.of(null).map((x)=>x.toUpperCase()) // MayBe { value: null }

    // 链式调用中第一个参数为null
    MayBe.of('abc').map(()=>null).map((x)=> 'start' + x) // MayBe { value: null }


    • Either函子

    Either函子主要解决的是MayBe函子在执行失败时不能判断哪一只分支出问题而出现的,主要解决的分支扩展的问题。

    我们实现一下Either函子:

    const Nothing = function (val) {
    this.value = val;
    }
    Nothing.of = function (val) {
    return new Nothing(val);
    }
    Nothing.prototype.map = function (f) {
    return this;
    }
    const Some = function(val){
    this.value = val;
    }
    Some.of = function(val) {
    this.value = val;
    }
    Some.prototype.map = function(fn) {
    return Some.of(fn(this.value))
    }
    const Either = {
    Some: Some,
    Nothing: Nothing
    }


    实现包含两个函数,Nothing函数只返回函数自身,Some则会执行map部分,在实际应用中,可以将错误处理使用Nothing,需要执行使用Some,这样就可以分辨出分支出现的问题。

    5. Monad

    Monad应该是这几个中最难理解的概念了,因为本人也没有学过Haskell,所以也可能对Monad理解不是很准确,所以犹豫要不要写出来,打算学习Haskell,好吧,先记录下自己理解,永远不做无病呻吟,有自己感触与理解才会记录,学过之后再次补充。

    Monad就是一种设计模式,表示将一个运算过程,通过函数拆解成互相连接的多个步骤。你只要提供下一步运算所需的函数,整个运算就会自动进行下去。那么构成Monad 组成条件有哪些呢?

    • 类型构造器,因为Monad实际处理的是数据类型,而不是值,必须有一个类型构造器,这个类型构造器的作用就是如何从指定类型构造新的一元类型,比如Maybe<number>,定义Maybe<number>了基础类型的类型number,我们月可以把这种类型构造器理解为封装了一个值,这个值既可以是用数据结构进行封装,也可以使用函数,通过返回值表达封装的值,一般也说Monad是一个“未计算的值”、“包含在上下文(context)中的值”。
    • 提升函数。这个提升函数一般指的是return或者unit,说白了,提升函数就是将一个值封装进了Monad这个数据结构中,签名为 return :: a -> M a 。将unit基础类型的值包装到monad中的函数。对于Maybe monad,它将2类型number的值包装到类型的值Maybe(2)Maybe<number>。
    • 绑定函数bind。绑定函数就像一个管道,它解封一个Monad,将里面的值传到第二个参数表示的函数,生成另一个Monad。形式化定义为[公式](ma 为类型为[公式]的 Monad 实例,[公式]是转换函数)。此bind功能是不一样的Function.prototype.bind 功能。它用于创建具有绑定this值的部分应用函数或函数。

    就像一个盒子一样,放进盒子里面(提升函数),从盒子里面取出来(绑定函数),放进另外一个盒子里面(提升函数),本身这个盒子就是类型构造器。

    举一个常用的例子,这也是Monad for functional programming,里面除法的例子,实现一个求值函数evaluate,它可以接收类似[公式]

    function evaluate(e: Expr): Maybe<number> {
    if (e.type === 'value') return Maybe.just(<number>e.value);

    return evaluate((<DivisionExpr>e.value).left)
    .bind(left => evaluate((<DivisionExpr>e.value).right)
    .bind(right => safeDiv(left, right)));
    }

    在像JavaScript这样的面向对象语言中,unit函数可以表示为构造函数,函数可以表示为bind实例方法。

    还有三个遵守的monadic法则:

    1. bind(unit(x), f) ≡ f(x)
    2. bind(munit) ≡ m
    3. bind(bind(mf), g) ≡ bind(mx ⇒ bind(f(x), g))
    4. const unit = (value: number) => Maybe.just<number>(value);
      const f = (value: number) => Maybe.just<number>(value * 2);
      const g = (value: number) => Maybe.just<number>(value - 5);
      const ma = Maybe.just<number>(13);
      const assertEqual = (x: Maybe<number>, y: Maybe<number>) => x.value === y.value;

      // first law
      assertEqual(unit(5).bind(f), f(5));

      // second law
      assertEqual(ma.bind(unit), ma);

      // third law
      assertEqual(ma.bind(f).bind(g), ma.bind(value => f(value).bind(g)));


    前两个说这unit是一个中性元素。第三个说bind应该是关联的 - 绑定的顺序无关紧要。这是添加具有的相同属性:(8 + 4) + 2与...相同8 + (4 + 2)。

    举几个比较常见的Monad:

    1. Promise Monad

    没有想到吧,你平时使用的Promise就是高大上的Monad,它是如何体现的这三个特性呢?

    • 类型构造器就是Promise
    • unit提升函数 为x => Promise.resolve(x)
    • 绑定函数 为Promise.prototype.then
    fetch('xxx')
    .then(response => response.json())
    .then(o => fetch(`xxxo`))
    .then(response => response.json())
    .then(v => console.log(v));

    最简单的 P(A).then(B) 实现里,它的 P(A) 相当于 Monad 中的 unit 接口,能够把任意值包装到 Monad 容器里。支持嵌套的 Promise 实现中,它的 then 背后其实是 FP 中的 join 概念,在容器里还装着容器的时候,递归地把内层容器拆开,返回最底层装着的值。Promise 的链式调用背后,其实是 Monad 中的 bind 概念。你可以扁平地串联一堆 .then(),往里传入各种函数,Promise 能够帮你抹平同步和异步的差异,把这些函数逐个应用到容器里的值上。回归这节中最原始的问题,Monad 是什么呢?只要满足以上三个条件,我们就可以认为它是 Monad 了:正如我们已经看到的,Promise.resolve() 能够把任意值包装到 Promise 里,而 Promise/A+ 规范里的 Resolve 算法则实际上实现了 bind。因此,我们可以认为:Promise 就是一个 Monad。

    2. Continuation Monad

    continuation monad用于异步任务。幸运的是,ES6没有必要实现它 - Prmise对象是这个monad的一个实现。

    • Promise.resolve(value)包装一个值并返回一个promise(unit函数)。
    • Promise.prototype.then(onFullfill: value => Promise)将一个值转换为另一个promise并返回一个promise(bind函数)的函数作为参数。

    Promise为基本的continuation monad提供了几个扩展。如果then返回一个简单的值(而不是一个promise对象), 他将被视为Promise,解析为该值 自动将一个值包装在monad中。

    第二个区别在于错误传播。Continuation monad允许在计算步骤之间仅传递一个值。另一方面,Promise有两个不同的值 - 一个用于成功值,一个用于错误(类似于Either monad)。可以使用方法的第二个回调then或使用特殊。catch方法捕获错误。

    下面定义了一个简单的Monad类型,它单纯封装了一个值作为value属性:

    var Monad = function (v) {
    this.value = v;
    return this;
    };

    Monad.prototype.bind = function (f) {
    return f(this.value)
    };

    var lift = function (v) {
    return new Monad(v);
    };

    我们将一个除以2的函数应用的这个Monad:

    console.log(lift(32).bind(function (a) {
    return lift(a/2);
    }));

    // > Monad { value: 16 }

    连续应用除以2的函数:

    // 方便展示用的辅助函数,请忽视它是个有副作用的函数。
    var print = function (a) {
    console.log(a);
    return lift(a);
    };

    var half = function (a) {
    return lift(a/2);
    };

    lift(32)
    .bind(half)
    .bind(print)
    .bind(half)
    .bind(print);

    //output:
    // > 16
    // > 8


    收起阅读 »

    Vue路由懒加载

    vue
    Vue路由懒加载对于SPA单页应用,当打包构建时,JavaScript包会变得非常大,影响页面加载速度,将不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这就是路由的懒加载。实现方式#Vue异步组件#Vue允许以一个工厂函数的方式定...
    继续阅读 »

    Vue路由懒加载

    对于SPA单页应用,当打包构建时,JavaScript包会变得非常大,影响页面加载速度,将不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这就是路由的懒加载。

    实现方式#

    Vue异步组件#

    Vue允许以一个工厂函数的方式定义你的组件,这个工厂函数会异步解析你的组件定义。Vue只有在这个组件需要被渲染的时候才会触发该工厂函数,且会把结果缓存起来供未来重渲染。

    Vue.component("async-example", function (resolve, reject) {
    setTimeout(function() {
    // 向 `resolve` 回调传递组件定义
    resolve({
    template: "
    I am async!
    "
    })
    }, 1000)
    })


    这个工厂函数会收到一个resolve回调,这个回调函数会在你从服务器得到组件定义的时候被调用,当然也可以调用reject(reason)来表示加载失败,此处的setTimeout仅是用来演示异步传递组件定义用。将异步组件和webpackcode-splitting功能一起配合使用可以达到懒加载组件的效果。

    Vue.component("async-webpack-example", function (resolve) {
    // 这个特殊的 `require` 语法将会告诉 webpack
    // 自动将你的构建代码切割成多个包,这些包
    // 会通过 Ajax 请求加载
    require(["./my-async-component"], resolve)
    })

    也可以在工厂函数中返回一个Promise,把webpack 2ES2015语法加在一起。

    Vue.component(
    "async-webpack-example",
    // 这个动态导入会返回一个 `Promise` 对象。
    () => import("./my-async-component")
    )


    事实上我们在Vue-Router的配置上可以直接结合Vue的异步组件和Webpack的代码分割功能可以实现路由组件的懒加载,打包后每一个组件生成一个js文件。

    {
    path: "/example",
    name: "example",
    //打包后,每个组件单独生成一个chunk文件
    component: reslove => require(["@/views/example.vue"], resolve)
    }

    动态import#

    Webpack2中,可以使用动态import语法来定义代码分块点split point,官方也是推荐使用这种方法,如果使用的是Bable,需要添加syntax-dynamic-import插件, 才能使Babel可以正确的解析语法。

    //默认将每个组件,单独打包成一个js文件
    const example = () => import("@/views/example.vue")

    有时我们想把某个路由下的所有组件都打包在同一个异步块chunk中,需要使用命名chunk一个特殊的注释语法来提供chunk name,需要webpack > 2.4

    const example1 = () => import(/* webpackChunkName: "Example" */ "@/views/example1.vue")
    const example2 = () => import(/* webpackChunkName: "Example" */ "@/views/example2.vue");

    事实上我们在Vue-Router的配置上可以直接定义懒加载。

    {
    path: "/example",
    name: "example",
    //打包后,每个组件单独生成一个chunk文件
    component: () => import("@/views/example.vue")
    }

    webpack提供的require.ensure#

    使用webpackrequire.ensure,也可以实现按需加载,同样多个路由指定相同的chunkName也会合并打包成一个js文件。

    // require.ensure(dependencies: String[], callback: function(require), chunkName: String)
    {
    path: "/example1",
    name: "example1",
    component: resolve => require.ensure([], () => resolve(require("@/views/example1.vue")), "Example")
    },
    {
    path: "/example2",
    name: "example2",
    component: resolve => require.ensure([], () => resolve(require("@/views/example2.vue")), "Example")
    }




    收起阅读 »

    iOS MachO文件

    目标文件.aFramework可执行文件.dsym1.2.1 .out、可执行文件test.c文件,内容如下:#include int main() { printf("test\n"); return 0; }验证不指定默认生成...
    继续阅读 »

    一、MachO文件概述


    Mach-O(Mach Object)是mac以及iOS上的格式, 类似于windows上的PE格式 (Portable Executable ),linux上的elf格式 (Executable and Linking Format)。

    Mach-O是一种用于可执行文件目标代码动态库的文件格式。作为a.out格式的替代,Mach-O提供了更强的扩展性。


    1.1 MachO格式的常见文件

    • 目标文件.o
    • 库文件
    • .a
    • .dylib
    • Framework
    • 可执行文件
    • dyld
    • .dsym

    1.2 格式验证

    1.2.1 .o.out、可执行文件

    新建test.c文件,内容如下:

    #include 

    int main() {
    printf("test\n");
    return 0;
    }

    验证.o文件:

    clang -c  test.c
    //clang -c test.c -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk
    不指定-c默认生成a.out,如果报找不到'stdio.h' file not found,则可以指定-isysroot。文章最后有具体的解决方案,
    通过file指令查看文件格式:

    file test.o
    test.o: Mach-O 64-bit object x86_64

    验证a.out可执行文件:

    clang test.o
    file a.out
    a.out: Mach-O 64-bit executable x86_64
    ./a.out
    test

    验证可执行文件:

    clang -o test2 test.c 
    file test2
    test2: Mach-O 64-bit executable x86_64
    ./test2
    test

    至此再生成一个test3可执行文件:

    clang -o test3 test.o

    那么生成的a.outtest2test3一样么?



    可以看到生成的可执行文件md5相同。



    ⚠️原则上test3md5应该和test2a.out相同。源码没有变化,所以应该相同的。在指定-isysroot后生成的可能不同,推测和CommandLineTools有关(系统中一个,Xcode中一个)。

    再创建一个test1.c文件,内容如下:


    #include 

    void test1Func() {
    printf("test1 func \n");
    }

    修改test.c:

    #include 

    void test1Func();

    int main() {
    test1Func();
    printf("test\n");
    return 0;
    }

    这个时候相当于有多个文件了,编译生成可执行文件demodemo1demo2:


    clang -o demo  test1.c test.c 
    clang -c test1.c test.c
    clang -o demo1 test.o test1.o
    clang -o demo2 test1.o test.o



    这里demo1demo2``md5不同是因为test.otest1.o顺序不同。

    objdump --macho -d demo查看下macho:



    这也就解释了md5不同的原因。这里很像XcodeBuild Phases -> Compile Sources中源文件的顺序。

    ⚠️源文件顺序不同,编译出来的二进制文件不同( 大小相同),二进制排列顺序不同。


    1.2.2.a文件

    直接创建一个library库查看:

    //find /usr -name "*.a"
    file libTestLibrary.a
    libTestLibrary.a: current ar archive random library

    1.2.3. .dylib

    cd /usr/lib
    file dyld
    dyld: Mach-O universal binary with 2 architectures: [x86_64:Mach-O 64-bit dynamic linker x86_64] [i386:Mach-O dynamic linker i386]
    dyld (for architecture x86_64): Mach-O 64-bit dynamic linker x86_64
    dyld (for architecture i386): Mach-O dynamic linker i386

    1.2.4 dyld

    cd /usr/lib
    file dyld
    dyld: Mach-O universal binary with 2 architectures: [x86_64:Mach-O 64-bit dynamic linker x86_64] [i386:Mach-O dynamic linker i386]
    dyld (for architecture x86_64): Mach-O 64-bit dynamic linker x86_64
    dyld (for architecture i386): Mach-O dynamic linker i386

    这里需要注意的是dyld不是可执行文件,是一个dynamic linker。系统内核触发。

    1.2.5 .dsym

    file TestDsym.app.dSYM
    TestDsym.app.dSYM: directory

    cd TestDsym.app.dSYM/Contents/Resources/DWARF

    file TestDsym
    TestDsym: Mach-O 64-bit dSYM companion file arm64

    二、可执行文件

    创建一个工程,默认生成的文件就是可执行文件,查看对应的MachO:

    file TestDsym
    TestDsym: Mach-O 64-bit executable arm64
    可以看到是一个单一架构的可执行文件(⚠️11以上的系统都只支持64位架构,所以默认就没有32位的)。将Deployment Info改为iOS 10编译再次查看MachO

    file TestDsym
    TestDsym: Mach-O universal binary with 2 architectures: [arm_v7:Mach-O executable arm_v7] [arm64:Mach-O 64-bit executable arm64]
    TestDsym (for architecture armv7): Mach-O executable arm_v7
    TestDsym (for architecture arm64): Mach-O 64-bit executable arm64

    这个时候就有多个架构了。
    当然也可以在Xcode中直观的看到支持的架构:



    • Architectures:支持的架构。
    • Build Active Architecture Only:默认情况下debug模式下只编译当前设备架构,release模式下需要根据支持的设备。
    • $(ARCHS_STANDARD):环境变量,代表当前支持的架构。

    如果我们要修改架构直接在Architectures中配置(增加armv7s):



    编译再次查看MachO:

    file TestDsym
    TestDsym: Mach-O universal binary with 3 architectures: [arm_v7:Mach-O executable arm_v7] [arm_v7s:Mach-O executable arm_v7s] [arm64:Mach-O 64-bit executable arm64]
    TestDsym (for architecture armv7): Mach-O executable arm_v7
    TestDsym (for architecture armv7s): Mach-O executable arm_v7s
    TestDsym (for architecture arm64): Mach-O 64-bit executable arm64

    2.1通用二进制文件(Universal binary


    • 苹果公司提出的一种程序代码,能同时适用多种架构的二进制文件。
    • 同一个程序包中同时为多种架构提供最理想的性能。
    • 因为需要储存多种代码,通用二进制应用程序通常比单一平台二进制的程序要大。
    • 由于多种架构有共同的非执行资源(代码以外的),所以并不会达到单一版本的多倍之多(特殊情况下,只有少量代码文件的情况下有可能会大于多倍)。
    • 由于执行中只调用一部分代码,运行起来不需要额外的内存。


    当我们将通用二进制文件拖入Hopper时,能够看到让我们选择对应的架构:



    2.2lipo命令

    lipo是管理Fat File的工具,可以查看cpu架构,,提取特定架构,整合和拆分库文件。

    使用lipo -info 可以查看MachO文件包含的架构
    lipo -info MachO文件


    lipo -info TestDsym
    Architectures in the fat file: TestDsym are: armv7 armv7s arm64
    使用lifo –thin 拆分某种架构
    lipo MachO文件 –thin 架构 –output 输出文件路径

    lipo TestDsym -thin armv7 -output macho_armv7
    lipo TestDsym -thin arm64 -output macho_arm64
    file macho_armv7
    macho_armv7: Mach-O executable arm_v7
    file macho_arm64
    macho_arm64: Mach-O 64-bit executable arm64

    使用lipo -create 合并多种架构
    lipo -create MachO1 MachO2 -output 输出文件路径

    lipo -create macho_armv7 macho_arm64 -output  macho_v7_64

    file macho_v7_64
    macho_v7_64: Mach-O universal binary with 2 architectures: [arm_v7:Mach-O executable arm_v7] [arm64:Mach-O 64-bit executable arm64]
    macho_v7_64 (for architecture armv7): Mach-O executable arm_v7
    macho_v7_64 (for architecture arm64): Mach-O 64-bit executable arm64

    三、MachO文件结构


    Mach-O 的组成结构如图所示:

    • Header:包含该二进制文件的一般信息。

      • 字节顺序、架构类型、加载指令的数量等。
      • 快速确认一些信息,比如当前文件用于32位还是64位,对应的处理器是什么、文件类型是什么。
    • Load Commands:一张包含很多内容的表。

      • 内容包括区域的位置、符号表、动态符号表等。
    • Data:通常是对象文件中最大的部分。

      • 包含Segement的具体数据

    通用二进制文件就是包含多个这种结构。

    otool -f MachO文件查看Header信息:


    otool -f TestDsym

    Fat headers
    fat_magic 0xcafebabe
    nfat_arch 3
    architecture 0
    cputype 12
    cpusubtype 9
    capabilities 0x0
    offset 16384
    size 79040
    align 2^14 (16384)
    architecture 1
    cputype 12
    cpusubtype 11
    capabilities 0x0
    offset 98304
    size 79040
    align 2^14 (16384)
    architecture 2
    cputype 16777228
    cpusubtype 0
    capabilities 0x0
    offset 180224
    size 79760
    align 2^14 (16384)

    分析MachO最好的工具就是 MachOView了:



    otool的内容相同,对于多架构MachO会有一个Fat Header其中包含了CPU类型和架构。OffsetSize代表了每一个每一个架构在二进制文件中的偏移和大小。

    这里有个问题是16384+79040 = 95424 < 9830498304 - 16384 = 8192081920 / 4096 / 4 = 5,可以验证这里是以页对齐的。(iOS中一页16KMachO中都是以页为单位对齐的,这也就是为什么能在Load Commands中插入LC_LOAD_DYLIB的原因。)。

    MachO对应结构如下:




    3.1Header

    Header数据结构:



    对应dyld的定义如下(loader.h):

    struct mach_header {
    uint32_t magic; /* mach magic number identifier */
    cpu_type_t cputype; /* cpu specifier */
    cpu_subtype_t cpusubtype; /* machine specifier */
    uint32_t filetype; /* type of file */
    uint32_t ncmds; /* number of load commands */
    uint32_t sizeofcmds; /* the size of all the load commands */
    uint32_t flags; /* flags */
    };

    struct mach_header_64 {
    uint32_t magic; /* mach magic number identifier */
    cpu_type_t cputype; /* cpu specifier */
    cpu_subtype_t cpusubtype; /* machine specifier */
    uint32_t filetype; /* type of file */
    uint32_t ncmds; /* number of load commands */
    uint32_t sizeofcmds; /* the size of all the load commands */
    uint32_t flags; /* flags */
    uint32_t reserved; /* reserved */
    };
  • magic:魔数,快速定位属于64位还是32位。
  • cputypeCPU类型,比如ARM
  • cpusubtypeCPU具体类型,arm64armv7
  • filetype:文件类型,比如可执行文件,具体包含类型如下:

  • #define MH_OBJECT   0x1     /* relocatable object file */
    #define MH_EXECUTE 0x2 /* demand paged executable file */
    #define MH_FVMLIB 0x3 /* fixed VM shared library file */
    #define MH_CORE 0x4 /* core file */
    #define MH_PRELOAD 0x5 /* preloaded executable file */
    #define MH_DYLIB 0x6 /* dynamically bound shared library */
    #define MH_DYLINKER 0x7 /* dynamic link editor */
    #define MH_BUNDLE 0x8 /* dynamically bound bundle file */
    #define MH_DYLIB_STUB 0x9 /* shared library stub for static
    linking only, no section contents */

    #define MH_DSYM 0xa /* companion file with only debug
    sections */

    #define MH_KEXT_BUNDLE 0xb /* x86_64 kexts */
    #define MH_FILESET 0xc /* a file composed of other Mach-Os to
    be run in the same userspace sharing
    a single linkedit. */

  • ncmdsNumber of Load CommandsLoad Commands条数。
  • sizeofcmdsSize of Load CommandsLoad Commands大小。
  • flags:标识二进制文件支持的功能,主要是和系统加载、链接有关。
  • reservedarm64特有,保留字段。

  • 3.2 LoadCommands


    Load Commands指示dyld如何加载二进制文件。
    一个基本的Load Comands如下:




    空指针陷阱,目的是为了和32位指令完全分开。(32位地址在4G以下,64位地址大于4G 0xffffffff = 4G)。
    __PAGEZERO不占用数据(file size0),唯一有的是VM Sizearm64 4Garmv7比较小)。

    VM Addr : 虚拟内存地址
    VM Size: 虚拟内存大小。运行时刻在内存中的大小,一般情况下和File size相同,__PAGEZERO例外。
    File offset:数据在文件中偏移量。
    File size: 数据在文件中的大小。
    我们定位是看VM Addr + ASLR

  • __TEXT__DATA__LINKEDIT:将文件中(32位/64位)的段映射到进程地址空间中。
    分为三大块,分别对应DATA中的Section__TEXT + __DATA)、__LINKEDIT。告诉dyld占用多大空间。

  • LC_DYLD_INFO_ONLY:动态链接相关信息。




  • Rebase:重定向(ASLR)偏移地址和大小。从Rebase Info Offset + ASLR开始加载336个字节数据。
    Binding:绑定外部符号。
    Weak Binding:弱绑定。
    Lazy Binding:懒绑定,用到的时候再绑定。
    Export info:对外开放的函数。

  • LC_SYMTAB:符号表地址。





  • LC_DSYMTAB:动态符号表地址。


    LC_LOAD_DYLINKER:使用何种动态加载器。iOS使用的是dyld







    • LC_FUNCTION_DYLIB:函数起始地址表。

    • LC_DATA_IN_CODE:定义在代码段内的非指令的表。

    • LC_DATA_SIGNATURE:代码签名。


    3.3Data

    Data包含Section__TEXT + __DATA)、__LINKEDIT

    3.3.1__TEXT



    __TEXT代码段,就是我们的代码。

    • __text:主程序代码。开始是代码起始位置,和Compile Sources中文件顺序有关。

    __stubs & __stub_helper:用于符号绑定。



    这里65a0就是325a0,这里是循环做符号绑定。

    • __objc_methname:方法名称

    • __objc_classname:类名称

    • __objc_methtype:方法类型

    • __cstring:字符串常量


    3.3.2__DATA


    __DATA数据段。

    • __got & __la_symbol_ptr:外部符号有两张表Non-LazyLazy


    Lazy懒加载表,表中的指针一开始都指向 __stub_helper


    • __cfstring:程序中使用的 Core Foundation 字符串(CFStringRefs)。

    • __objc_classlist:类列表。

    • __objc_protolist: 原型。

    • __objc_imageinfo:镜像信息

    • __objc_selrefsself 引用

    • __objc_classrefs:类引用

    • __objc_superrefs:超类引用

    • __data:初始化过的可变数据。


    3.3.3 __LINKEDIT

  • Dynamic Loader Info:动态加载信息

  • Function Starts:入口函数

  • Symbol Table:符号表

  • Dynamic Symbol Table:动态库符号表

  • String Table:字符串表

  • Code Signature:代码签名


  • 总结

  • MachO属于一种文件格式。
    • 包含:可执行文件、静态库、动态库、dyld等。
    • 可执行文件:
      • 通用二进制文件(Fat):集成了多种架构。
      • lipo命令:-thin拆分架构,-creat合并架构。
  • MachO结构:
    • Header:快速确定该文件的CPU类型,文件类型等。
    • Load Commands:知识加载器(dyld)如何设置并加载二进制数据。
    • Data:存放数据,代码、数据、字符串常量、类、方法等。


  • 作者:HotPotCat
    链接:https://www.jianshu.com/p/9f6955575213


    收起阅读 »

    iOS面试可以怼HR的点-应用重签名

    首先理解一件事:签名是可以被替换的。签名:原始数据->hash->加密重签名:原始数据->hash->加密这也就是签名可以被替换的原因。一、codesign重签名codesign安装Xcode就有,Xcode也是用的这个工具。签名包含:...
    继续阅读 »

    首先理解一件事:签名是可以被替换的。
    签名:原始数据->hash->加密
    重签名:原始数据->hash->加密
    这也就是签名可以被替换的原因。


    一、codesign重签名

    codesign安装Xcode就有,Xcode也是用的这个工具。
    签名包含:
    资源文件
    macho文件
    framework
    ...


    1.1终端命令

    1.1.1查看签名信息

    codesign -vv -d xxx.app


    1.1.2列出钥匙串里可签名的证书

    security find-identity -v -p codesigning


    1.1.3otool分析macho文件信息并导出到指定文件

    otool -l xxx > ~/Desktop/machoMessage.txt

    其中cryptid0表示没有用到加密算法(也就是脱壳的), 其它则表示加密。



    也可以直接过滤查看是否砸壳:

    otool -l xxx | grep cryptid

    1.1.4强制替换签名


    codesign –fs “证书串” 文件名

    codesign -fs "Apple Development: xxx@qq.com (9AN9M5S786)" andromeda.framework

    1.1.5给文件添加权限

    chmod +x 可执行文件

    1.1.6查看描述文件


    security cms -D -i ../embedded.mobileprovision

    1.1.7macho签名

    codesign -fs “证书串” --no-strict --entitlements=权限文件.plist APP包

    1.1.8将输入文件压缩为输出文件

    zip –ry 输出文件 输入文件 

    1.1.9越狱的手机dump app 包

    // 建立连接
    sh usbConnect.sh
    //连接手机
    sh usbX.sh
    //查看进程
    ps -A
    //找到微信进程,拿到路径
    ps -A | grep WeChat
    //进入目标文件夹拷贝微信(这里是没有砸壳的)
    scp -r -p 12345 root@localhost:微信路径 ./

    1.2codesign命令重签名


    这里以砸过壳的微信(7.0.8)为例,使用免费开发者账号重签名微信,然后安装到非越狱手机上。

    1. 解压缩.ipa包,Payload中找到.app显示包内容。
      ⚠️由于免费证书没有办法签名PlugInsWatch,直接将这两个文件夹删除。

    2. 签名Frameworks
      逐个签名Frameworks目录下的framework(使用自己本机的免费证书)


    codesign -fs "Apple Development: xxx@qq.com (9AN9M5S786)" andromeda.framework



    1. 确保要签名的appmacho文件的可执行权限
    ➜  WeChat.app ls -l WeChat
    -rwxr-xr-x@ 1 zaizai staff 126048560 10 16 2019 WeChat
    4.获取免费账号对应的描述文件

    创建空工程使用免费账号&真机编译获运行取描述文件。


    这个时候描述文件已经拷贝到手机中去了,并且已经信任设备。
    将获取到的描述文件拷贝到 WeChatapp包中。

    5.修改bundleId
    找到WeChat info.plist修改BundleId为我们生成描述文件的BundleId





    1. 获取描述文件的权限
    security cms -D -i embedded.mobileprovision

    找到对应的权限Entitlements:

        <dict>

    <key>application-identifier</key>
    <string>S48J667P47.com.guazai.TestWeChat</string>

    <key>keychain-access-groups</key>
    <array>
    <string>S48J667P47.*</string>
    </array>

    <key>get-task-allow</key>
    <true/>

    <key>com.apple.developer.team-identifier</key>
    <string>S48J667P47</string>

    </dict>

    创建一个.plist文件,将权限内容粘贴进去:



    内容如下:

    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
    <!--
    Entitlements.plist
    TestWeChat

    Created by ZP on 2021/4/19.
    Copyright (c) 2021 ___ORGANIZATIONNAME___. All rights reserved.
    -->

    <plist version="1.0">
    <dict>

    <key>application-identifier</key>
    <string>S48J667P47.com.guazai.TestWeChat</string>

    <key>keychain-access-groups</key>
    <array>
    <string>S48J667P47.*</string>
    </array>

    <key>get-task-allow</key>
    <true/>

    <key>com.apple.developer.team-identifier</key>
    <string>S48J667P47</string>

    </dict>
    </plist>

    将权限文件(Entitlements.plist)拷贝到和PayloadWeChat.app同一目录


    1. 签名Wechat
    codesign -fs "Apple Development: xxx@qq.com (9AN9M5S786)" --no-strict --entitlements=entitlements.plist WeChat.app

    这里entitlments参数需要和上一步生成的权限文件名称对应上。




    这个时候通过XcodeWeChat.app包安装到手机就已经能正常安装了。

    通过debug->attach to process->WeChat就可以调试微信了:



    ⚠️这个时候不要用自己的常用账号登录重签名的微信(有可能被封号)。

    重签名步骤:

    1. 删除插件以及带有插件的.app包(如:watch
      PlugInsWatch文件夹
    2. Frameworks中的库重签名
      codesign -fs "Apple Development: xxx@qq.com (9AN9M5S786)" andromeda.framework
    3. 对可执行文件+X(可执行)权限
      chmod +x WeChat
    4. 添加描述文件(创建工程,真机编译得到,并且需要运行将描述文件安装到手机)
    5. 替换info.plist BundleIdBundleId要和描述文件中的一致)
    6. 通过授权文件(entitlments)重签名.app
      a.获取描述文件权限内容security cms -D -i embedded.mobileprovision
      b.将描述文件权限内容拷贝生成plist文件Entitlements.plist
      c.用全线文件签名App包:codesign -fs "Apple Development: xxx@qq.com (9AN9M5S786)" --no-strict --entitlements=entitlements.plist WeChat.app

    二、利用Xcode重签名调试三方应用

    1.新建和微信同名工程WeChat



    2.将空工程运行到真机上。

    3.解压.ipa包,并且删除WatchPlugIns文件夹

    4.重签名Frameworks

    5.修改BundleId

    6.将修改后的WeChat.app替换空工程的Products.app





    7.运行
    这个时候Products工程中有WeChat.appXcode认为有就直接使用这个了。这个时候就可以调试了(不需要attach



    ⚠️在某些系统下会出现启动重签名微信黑屏,建议通过脚本重签名。

    三、SHELL脚本

    shell是一种特殊的交互式工具,它为用户提供了启动程序、管理文件系统中文件以及运行在系统上的进程的途径。Shell一般是指命令行工具。它允许你输入文本命令,然后解释命令,并在内核中执行。
    Shell脚本,也就是用各类命令预先放入到一个文本文件中,方便一次性执行的一个脚本文件。


    脚本切换

    chsh -s /bin/zsh
    执行脚本的几种方式:
    有如下脚本:

    mkdir shell1
    cd shell1
    touch test.txt


  • source FileName  
    作用:在当前shell环境中读取并执行FileName中的命令
    特点:命令可以强行让一个脚本去立即影响当前的环境(一般用于加载配置文件)。
    命令会强制执行脚本中的全部命令,而忽略文件的权限。

  • bash FileNamezsh FileName  
    作用:重新建立一个子shell(进程),在子shell中执行脚本里面的句子。当前环境没有变化。

  • ./FileName
    作用:读取并执行文件中的命令。但有一个前提,脚本文件需要有可执行权限。


  • MAC中shell种类

    cd /private/etc
    cat shells





  • bashmacOS默认shell(老系统),新系统切换为zsh了。
  • csh:被tcsh替换了
  • dash:比bash小很多,效率高。
  • ksh:兼容bash
  • sh:已经被bash替换了
  • tcsh:整合了csh提供了更多功能
  • zsh:替换了bash

  • 四、用户组&文本权限

    UnixLinux都是多用户、多任务的系统,所以这样的系统里面就拥有了用户、组的概念。那么同样文件的权限也就有相应的所属用户和所属组。
    windows不同的是unixlinuxmacOS都是多用户的系统:





    4.1mac文件属性


    4.2权限

    权限有10位:

    drwx-r-xr-x

  • 1位文件类型d/-
    d目录(directory)
    -文件
  • 后面9位,文件权限:
    [r]:read,读
    [w]:write,写
    [x]:execute,执行
    ⚠️这三个权限的位置不会变,依次是rwx。出现-对应的位置代表没有此权限。
    • 一个文件的完整权限分为三组:
      第一组:文件所有者权限
      第二组:这一组其它用户权限
      第三组:非本组用户的权限


    4.3权限改变chmod

    文件权限的改变使用chmod命令。
    设置方法有两种:数字类型改变 和 符号类型改变。
    文件权限分为三种身份:[user][group][other]  
    三个权限:[read] [write] [execute]

    4.3.1数字类型


    各个权限数字对照:r:4(0100)  w:2(0010)  x:1(0001),这么设计的好处是可以按位或。与我们开发中位移枚举同理。
    如果一个文件权限为[!–rwxr-xr-x],则对应:
    User : 4+2+1 = 7
    Group: 4+0+1 = 5
    Other: 4+0+1 = 5
    命令为:chmod 755 文件名

    数字与权限对应表:


    0代表没有任何权限。

    4.3.2符号类型

    chmod [u(User)、g(Group)、o(Other)、a(All)] [+(加入)、-(除去)、=(设置)]  [r、w、x] 文件名称
    例:

    chmod a+x test.txt

    默认是all

    五、通过shell脚本自动重签名

    脚本实现逻辑和codesign逻辑相同。
    完整脚本如下:

    #临时解压目录
    TEMP_PATH="${SRCROOT}/Temp"
    #资源文件夹,我们提前在工程目录下新建一个APP文件夹,里面放ipa包(砸壳后的)
    ASSETS_PATH="${SRCROOT}/APP"
    #目标ipa包路径
    TARGET_IPA_PATH="${ASSETS_PATH}/*.ipa"


    #清空&创建Temp文件夹
    rm -rf TEMP_PATH
    mkdir -p TEMP_PATH


    # 1. 解压IPA到Temp目录下
    unzip -oqq "$TARGET_IPA_PATH" -d "$TEMP_PATH"
    # 拿到解压后的临时的APP的路径
    TEMP_APP_PATH=$(set -- "$TEMP_PATH/Payload/"*.app;echo "$1")

    #2. 将解压出来的.app拷贝进入工程下
    #2.1拿到当前工程目标Target路径
    # BUILT_PRODUCTS_DIR 工程生成的APP包的路径
    # TARGET_NAME target名称
    TARGET_APP_PATH="$BUILT_PRODUCTS_DIR/$TARGET_NAME.app"
    echo "app path:$TARGET_APP_PATH"

    #2.2删除工程本身的Target,将解压的Target拷贝到工程本身的路径
    rm -rf "$TARGET_APP_PATH"
    mkdir -p "$TARGET_APP_PATH"
    cp -rf "$TEMP_APP_PATH/" "$TARGET_APP_PATH"


    # 3. 删除extension和WatchAPP,个人证书没法签名Extention
    rm -rf "$TARGET_APP_PATH/PlugIns"
    rm -rf "$TARGET_APP_PATH/Watch"



    # 4. 更新info.plist文件 CFBundleIdentifier
    # 设置:"Set : KEY Value" "目标文件路径",PlistBuddy是苹果自带的。
    /usr/libexec/PlistBuddy -c "Set :CFBundleIdentifier $PRODUCT_BUNDLE_IDENTIFIER" "$TARGET_APP_PATH/Info.plist"

    #删除UISupportedDevices设备相关配置(越狱手机dump ipa包需要删除相关配置)
    /usr/libexec/PlistBuddy -c "Delete :UISupportedDevices" "$TARGET_APP_PATH/Info.plist"

    # 5. 给MachO文件上执行权限
    # 拿到MachO文件的名称
    APP_BINARY=`plutil -convert xml1 -o - $TARGET_APP_PATH/Info.plist|grep -A1 Exec|tail -n1|cut -f2 -d\>|cut -f1 -d\<`
    #上可执行权限
    chmod +x "$TARGET_APP_PATH/$APP_BINARY"



    # 6. 重签名第三方 FrameWorks
    TARGET_APP_FRAMEWORKS_PATH="$TARGET_APP_PATH/Frameworks"
    if [ -d "$TARGET_APP_FRAMEWORKS_PATH" ];
    then
    for FRAMEWORK in "$TARGET_APP_FRAMEWORKS_PATH/"*
    do
    #签名 --force --sign 就是-fs
    /usr/bin/codesign --force --sign "$EXPANDED_CODE_SIGN_IDENTITY" "$FRAMEWORK"
    done
    fi
    使用方式
    1.创建空工程,编译运行空工程至真机上(信任证书)。
    2.将appResign.sh脚本拷贝到工程根目录(要有可执行权限)。
    3.在工程根目录创建APP文件夹,并将微信.ipa拷贝到APP文件夹。
    4.配置脚本




    这个时候就可以调试微信了




    六、如何调试一个任意app?

    6.1获取对应ipa

    使用越狱手机dump ipa

    下载旧版本ipa包可以通过抓取iTunes的下载链接改版本号(后缀是app的版本,直接改版本)

    6.2砸壳

    砸壳后由于是dump越狱手机上的正版包,所以需要将info.plist中支持的设备信息(UISupportedDevices)删除。(当然可以写在脚本中)

    #删除UISupportedDevices设备相关配置(越狱手机dump ipa包需要删除相关配置)
    /usr/libexec/PlistBuddy -c "Delete :UISupportedDevices" "$TARGET_APP_PATH/Info.plist"

    删除完毕保存重新打包ipa

    zip -ry WeChat1.ipa Payload/

    总结

    • 重签名
      • codesign重签名
        • 删除不能签名的文件:Extensionwatch(包含了Extension
        • 重签名Frameworks(里面的库)
        • MachO添加可执行权限
        • 修改Info.plist文件(BundleID
        • 拷贝描述文件(该描述文件要在iOS真机中信任过)
        • 利用描述文件中的权限文件签名整个App
      • Xcode重签名
        • 删除不能签名的文件:Extensionwatch(包含了Extension
        • 重签名Frameworks(里面的库)
        • MachO添加可执行权限
        • 修改Info.plist文件(BundleID
        • App包拷贝进入Xcode工程目录中(剩下的交给Xcode
    • shell
      • 切换shell
        • $chsh -s shell路径
        • 现在macOSshell默认zsh(早期bash
        • 配置文件 zsh:.zshrc  bash:.bash_profile
      • 文件权限&用户组
        • 每个文件都有所属的用户、组、其它
        • 文件权限
          • 归属:用户、组、其它
          • 权限 :写、读、执行
        • 修改权限chmod
          • 数字:r:4 w:2 x:1
            • chmod 751 文件名
              • user4+2+1 = 7
              • group4+0+1 = 5
              • other0+0+1 = 1
          • 字符
            • 归属:u(用户) g(组) o(其它) a(所有)
            • +(添加) -(去掉) =(设置)
            • 默认achmod + x


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



    收起阅读 »

    iOS面试你需要了解的问题-应用签名

    一、代码签名代码签名是对可执行文件或脚本进行数字签名。用来确认软件在签名后未被修改或损坏的措施。和数字签名原理一样,只不过签名的数据是代码而已。1.1简单代码签名在iOS出来之前,以前的主流操作系统(Mac/Windows)软件随便从哪里下载都能运行,系统安全...
    继续阅读 »

    一、代码签名

    代码签名是对可执行文件或脚本进行数字签名。用来确认软件在签名后未被修改或损坏的措施。和数字签名原理一样,只不过签名的数据是代码而已。

    1.1简单代码签名

    iOS出来之前,以前的主流操作系统(Mac/Windows)软件随便从哪里下载都能运行,系统安全存在隐患,盗版软件、病毒入侵、静默安装等等。苹果希望解决这样的问题,要保证每一个安装到 iOS 上的 APP 都是经过苹果官方允许的,怎样保证呢?就是通过代码签名。

    如果要实现验证。最简单的方式就是通过苹果官方生成非对称加密的一对公私钥。在iOS的系统中内置一个公钥,私钥由苹果后台保存。我们传APPAppStore时,苹果后台用私钥对APP数据进行签名,iOS系统下载这个APP后,用公钥验证这个签名。若签名正确,这个APP肯定是由苹果后台认证的并且没有被修改过,也就达到了苹果的需求:保证安装的每一个APP都是经过苹果官方允许的。

    如果我们iOS设备安装APP只从App Store这一个入口这件事就简单解决了,没有任何复杂的东西,一个数字签名搞定。

    但是实际上iOS安装APP还有其他渠道。比如对于我们开发者iOSer而言,在开发APP时需要直接真机调试。而且苹果还开放了企业内部分发的渠道,企业证书签名的APP也是需要顺利安装的。苹果需要开放这些方式安装APP,这些需求就无法通过简单的代码签名来办到了。

    1.2苹果的需求

    • 安装包不需要上传到App Store,可以直接安装到手机上。
    • 苹果为了保证系统的安全性,必须对安装的APP有绝对的控制权:
      • 经过苹果允许才可以安装
      • 不能被滥用导致非开发APP也能被安装

    为了实现这些需求,iOS签名的复杂度也就开始增加了。苹果给出的方案是双层签名

    二、双层签名

    为了实现苹果验证应用的需求,苹果给出的方案是双层签名
    有两个角色:
    1.iOS系统
    2.Mac系统

    因为iOSAPP开发环境在Mac系统下。所以这个依赖关系成为了苹果双层签名的基础。


    2.1双层签名流程





    1. 在Mac系统中生成非对称加密算法的一对公钥\私钥(Xcode帮你代办了,钥匙串)。这里称为公钥M 私钥M ( M = Mac)。

    2. 苹果自己有固定的一对公私钥,和之前App Store原理一样,私钥在苹果后台,公钥在每个iOS系统中。这里称为公钥A ,私钥A。 (A=Apple)

    3. 公钥M 以及一些开发者的信息,传到苹果后台(这个就是CSR文件),用苹果后台里的私钥 A 去签名公钥M。得到一份数据包含了公钥M 以及其签名,把这份数据称为证书。这里苹果服务器就相当于认证服务器。

    4. 在开发时,编译完一个 APP 后,用本地的私钥 M(导出的P12) 对这个 APP 进行签名(证书p12是绑定在一起的),同时把第三步得到的证书一起打包进 APP 里,安装到手机上。

    5. 在安装时,iOS 系统取得证书,通过系统内置的公钥 A去验证证书的数字签名是否正确。

    6. 验证证书后确保了公钥 M是苹果认证过的,再用公钥 M 去验证 APP的签名(p12签名也就是私钥 M),这里就间接验证了这个 APP 安装行为是否经过苹果官方允许。(这里只验证安装行为,不验证APP 是否被改动,因为开发阶段 APP 内容总是不断变化的,苹果不需要管。)

    这里双层签名流程不是最终的iOS签名原理,在这个基础上还要加东西。

    有了双层签名过程,已经可以保证开发者的认证,和程序的安全性了。 但是,你要知道iOS的程序,主要渠道是要通过APP Store才能分发到用户设备的,如果只有上述的过程,那岂不是只要申请了一个证书,就可以安装到所有iOS设备了?

    三、描述文件的产生





    描述文件(Provisioning profile)一般包括三样东西:证书App ID设备。当我们在真机运行或者打包一个项目的时候,证书用来证明我们程序的安全性和合法性。

    苹果为了解决应用滥用的问题,所以苹果又加了两个限制。

    • 1.限制在苹果后台注册过的设备才可以安装。
    • 2.限制签名只能针对某一个具体的APP

    并且苹果还想控制App里面的iCloud/PUSH/后台运行/调试器附加这些权限,所以苹果把这些权限开关统一称为Entitlements(授权文件)。并将这个文件放在了一个叫做Provisioning Profile(描述文件)文件中。

    描述文件是在AppleDevelop网站创建的(在Xcode中填上AppleID它会代办创建),Xcode运行时会打包进入APP内。�所以我们使用CSR申请证书时,还要申请一个东西—就是描述文件!

    在开发时,编译完一个 APP 后,用本地的私钥M对这个APP进行签名,同时把从苹果服务器得到的 Provisioning Profile 文件打包进APP里,文件名为embedded.mobileprovision,把 APP 安装到手机上。最后系统进行验证。

    可以通过:

    security cms -D -i embedded.mobileprovision

    查看描述文件内容。

    资源文件签名:


    machoView签名:



    总结

    • 苹果签名原理
      • Mac电脑生成一对公钥 M私钥 M
        • 利用本地公钥 M创建CSR文件,请求证书
        • 钥匙串将证书本地私钥 Mp12证书)做关联
      • 苹果服务器利用本地私钥 A生成证书以及描述文件
        • 证书包含Mac电脑的公钥 M以及签名
        • 描述文件:设备列表AppID列表权限
      • iOS系统利用系统中的公钥 A(与苹果服务器私钥是一对)对App进行验证。
        • 验证描述文件是否与证书匹配
        • 验证App的安装行为(通过验证证书,拿出证书中的公钥MApp签名(p12 私钥M)进行验证)


    作者:HotPotCat
    链接:https://www.jianshu.com/p/0cd614e060ff
    来源:简书
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。




    收起阅读 »

    ArcSeekBar for Android 是一个弧形的拖动条进度控件

    ArcSeekBarArcSeekBar for Android 是一个弧形的拖动条进度控件,配置参数完全可定制化。ArcSeekBar 是基于 CircleProgressView 修改而来的库。 但青出于蓝而胜于蓝,所以&nb...
    继续阅读 »


    ArcSeekBar

    ArcSeekBar for Android 是一个弧形的拖动条进度控件,配置参数完全可定制化。

    ArcSeekBar 是基于 CircleProgressView 修改而来的库。 但青出于蓝而胜于蓝,所以 CircleProgressView 的大部分用法,ArcSeekBar基本都支持,而且可配置的参数更细致。

    之所以新造一个ArcSeekBar库,而不直接在CircleProgressView上面直接改,原因是CircleProgressView里面的部分动画效果对于SeekBar并不适用,所以ArcSeekBar是在CircleProgressView的基础上有所删减后,而再进行扩展增强的。 实际还需根据具体的需求而选择适合的。

    Gif 展示

    Image

    ArcSeekBar自定义属性说明(进度默认渐变色)

    属性值类型默认值说明
    arcStrokeWidthdimension12dp画笔描边的宽度
    arcStrokeCapenumROUND画笔的线冒样式
    arcNormalColorcolor#FFC8C8C8弧形正常颜色
    arcProgressColorcolor#FF4FEAAC弧形进度颜色
    arcStartAngleinteger270开始角度,默认十二点钟方向
    arcSweepAngleinteger360扫描角度范围
    arcMaxinteger100进度最大值
    arcProgressinteger0当前进度
    arcDurationinteger500动画时长
    arcLabelTextstring中间的标签文本,默认自动显示百分比
    arcLabelTextColorcolor#FF333333文本字体颜色
    arcLabelTextSizedimension30sp文本字体大小
    arcLabelPaddingTopdimension0dp文本居顶边内间距
    arcLabelPaddingBottomdimension0dp文本居底边内间距
    arcLabelPaddingLeftdimension0dp文本居左边内间距
    arcLabelPaddingRightdimension0dp文本居右边内间距
    arcShowLabelbooleantrue是否显示文本
    arcShowTickbooleantrue是否显示环刻度
    arcTickStrokeWidthdimension10dp刻度描边宽度
    arcTickPaddingdimension2dp环刻度与环间距
    arcTickSplitAngleinteger5刻度间隔的角度大小
    arcBlockAngleinteger1刻度的角度大小
    arcThumbStrokeWidthdimension8dp拖动按钮描边宽度
    arcThumbColorcolor#FFE8D30F拖动按钮颜色
    arcThumbRadiusdimension10dp拖动按钮半径
    arcThumbRadiusEnlargesdimension8dp触摸时按钮半径放大量
    arcShowThumbbooleantrue是否显示拖动按钮
    arcAllowableOffsetsdimension10dp触摸时可偏移距离:偏移量越大,触摸精度越小
    arcEnabledDragbooleantrue是否启用通过拖动改变进度
    arcEnabledSinglebooleantrue是否启用通过点击改变进度

    引入

    Maven:

    <dependency>
    <groupId>com.king.view</groupId>
    <artifactId>arcseekbar</artifactId>
    <version>1.0.2</version>
    <type>pom</type>
    </dependency>

    Gradle:

    implementation 'com.king.view:arcseekbar:1.0.2'

    Lvy:

    <dependency org='com.king.view' name='arcseekbar' rev='1.0.2'>
    <artifact name='$AID' ext='pom'></artifact>
    </dependency>
    如果Gradle出现compile失败的情况,可以在Project的build.gradle里面添加如下:(也可以使用上面的GitPack来complie)
    allprojects {
    repositories {
    maven { url 'https://dl.bintray.com/jenly/maven' }
    }
    }

    示例

    布局示例

        <com.king.view.arcseekbar.ArcSeekBar
    android:id="@+id/arcSeekBar"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"/>

    代码示例

        //进度改变监听
    arcSeekBar.setOnChangeListener(listener);
    //设置进度
    arcSeekBar.setProgress(progress);
    //显示进度动画(进度,动画时长)
    arcSeekBar.showAnimation(80,3000);

    更多使用详情,请查看app中的源码使用示例

    代码下载: ArcSeekBar.zip

    收起阅读 »

    ImageViewer for Android 是一个图片查看器

    ImageViewerImageViewer for Android 是一个图片查看器,一般用来查看图片详情或查看大图时使用。引入Maven:<dependency> <groupId>com.king.image</grou...
    继续阅读 »

    ImageViewer

    ImageViewer for Android 是一个图片查看器,一般用来查看图片详情或查看大图时使用。


    引入

    Maven:

    <dependency>
    <groupId>com.king.image</groupId>
    <artifactId>imageviewer</artifactId>
    <version>1.0.2</version>
    <type>pom</type>
    </dependency>

    Gradle:

    implementation 'com.king.image:imageviewer:1.0.2'

    Lvy:

    <dependency org='com.king.image' name='imageviewer' rev='1.0.2'>
    <artifact name='$AID' ext='pom'></artifact>
    </dependency>
    如果Gradle出现compile失败的情况,可以在Project的build.gradle里面添加如下:(也可以使用上面的GitPack来complie)
    allprojects {
    repositories {
    //...
    maven { url 'https://dl.bintray.com/jenly/maven' }
    }
    }

    示例

    代码示例

        //图片查看器 - 简单调用

    // data 可以多张图片List或单张图片,支持的类型可以是{@link Uri}, {@code url}, {@code path},{@link File}, {@link DrawableRes resId}…等
    ImageViewer.load(data)//要加载的图片数据,单张或多张
    .imageLoader(new GlideImageLoader())//加载器,imageLoader必须配置,目前内置的有GlideImageLoader或PicassoImageLoader,也可以自己实现
    .start(activity,sharedElement);//activity or fragment, 跳转时的共享元素视图
        //图片查看器

    // data 可以多张图片List或单张图片,支持的类型可以是{@link Uri}, {@code url}, {@code path},{@link File}, {@link DrawableRes resId}…等
    ImageViewer.load(data)//要加载的图片数据,单张或多张
    .selection(position)//当前选中位置,默认:0
    .indicator(true)//是否显示指示器,默认不显示
    .imageLoader(new GlideImageLoader())//加载器,imageLoader必须配置,目前内置的有GlideImageLoader或PicassoImageLoader,也可以自己实现
    .theme(R.style.ImageViewerTheme)//设置主题风格,默认:R.style.ImageViewerTheme
    .orientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT)//设置屏幕方向,默认:ActivityInfo.SCREEN_ORIENTATION_BEHIND
    .start(activity,sharedElement);//activity or fragment, 跳转时的共享元素视图

    相关说明

    • 使用 ImageViewer 时,必须配置一个实现的 ImageLoader
    • ImageViewer 一次可以查看多张图片或单张图片,支持的类型可以是 Uri、 url 、 path 、 File、 Drawable、 ImageDataSource 等
    • 目前内置默认实现的 ImageLoader 有和 PicassoImageLoader ,二者选其一即可,如果二者不满足您的需求,您也可以自己实现一个 ImageLoader
    • 为了保证 ImageViewer 体积最小化,和用户更多可能的选择性,并未将 Glide 和 Picasso 打包进 aar

    当您使用了 GlideImageLoader 时,必须依赖 Glide 库。

    当您使用了 PicassoImageLoader 时,必须依赖 Picasso 库。

    更多使用详情,请查看app中的源码使用示例

    ImageViewer.zip

    收起阅读 »

    环信新产品发布|千万级 MQTT 消息服务场景和架构解析

        2021 年随着 5G 商用的快速落地,其高可靠、低时延和大连接等特性将加速解锁下一代社交通信、智能硬件、物联网等多样化应用场景。物联网市场方兴未艾,根据 IDC 数据显示,2020 年全球物联网市场规模为 1.7 万亿美元。根据 ...
    继续阅读 »

        2021 年随着 5G 商用的快速落地,其高可靠、低时延和大连接等特性将加速解锁下一代社交通信、智能硬件、物联网等多样化应用场景。物联网市场方兴未艾,根据 IDC 数据显示,2020 年全球物联网市场规模为 1.7 万亿美元。根据 GSMA 预测,全球物联网连接数会从 2019 年的 120 亿增长至 2025 年的 246 亿,年复合增长率为 17%。同时,2021 年全球 MQTT 代理服务较 2020 年增长 40%,MQTT 作为轻量级、抗弱网、易集成的消息传输协议,将满足人与人之间、设备与人之间、设备与设备之间信令、即时消息等形式的互联网通信需求。

     

        MQTT是一个极其轻量级的发布/订阅消息传输协议,它解除时间与空间耦合,可以在应用内实现推送、通知等功能;它简约、轻量,极小的SDK空间占用,适用于嵌入Android、iOS、RTOS等多端平台;它数据包小、功耗低,适用于低带宽、高延迟或不可靠的网络环境。

     

        环信MQTT消息云的产品定位就是充分发挥MQTT协议优势,为开发者提供应用与应用之间、设备与应用之间、应用与平台之间的消息传输服务。为了让大家更深入了解MQTT协议优势,环信举办本次公开课,届时,我们将线上聆听:

     

    • 环信MQTT消息云核心功能有哪些?与IM的区别是什么?
    • 环信MQTT消息云实时交互服务背后有哪些关键的技术优势?
    • 环信MQTT消息云在不同行业领域有哪些典型的场景解决方案?
    • 环信MQTT消息云未来关注的技术发展趋势有哪些?
    • 环信MQTT如何让开发者更为轻松的应用?

     

    7月14日,环信新产品发布直播间,邀请环信MQTT产品经理、中科宏一教育集团Android主管,将围绕以上议题线上展开分享,我们直播间不见不散!


     

     

    从MQTT技术干货分享,到应用实践,再到行业最新案例实践……更多精彩值得期待!

    扫描上图二维码或点击链接即可报名参加。

    报名链接:https://mudu.tv/live/watch?id=o025ayrm

    收起阅读 »

    BaseUrlManager for Android 的设计初衷主要用于开发时,有多个环境需要打包APK的场景

    BaseUrlManagerBaseUrlManager for Android 的设计初衷主要用于开发时,有多个环境需要打包APK的场景,通过BaseUrlManager提供的BaseUrl动态设置入口,只需打一 次包,即可轻松随意的切换不同的开发环境或测试...
    继续阅读 »


    BaseUrlManager

    BaseUrlManager for Android 的设计初衷主要用于开发时,有多个环境需要打包APK的场景,通过BaseUrlManager提供的BaseUrl动态设置入口,只需打一 次包,即可轻松随意的切换不同的开发环境或测试环境。在打生产环境包时,关闭BaseUrl动态设置入口即可。

    妈妈再也不用担心因环境不同需要打多个包的问题,从此告别环境不同要写一堆配置的烦恼,真香。

    配合 RetrofitHelper 动态改变BaseUrl一起使用更香。

    Gif 展示

    Image

    引入

    Maven:

    <dependency>
    <groupId>com.king.base</groupId>
    <artifactId>base-url-manager</artifactId>
    <version>1.1.1</version>
    <type>pom</type>
    </dependency>

    Gradle:


    //AndroidX 版本
    implementation 'com.king.base:base-url-manager:1.1.1'

    //-----------------------v1.0.x以前的版本
    //AndroidX 版本
    implementation 'com.king.base:base-url-manager:1.0.1-androidx'

    //Android Support 版本
    implementation 'com.king.base:base-url-manager:1.0.1'

    Lvy:

    <dependency org='com.king.base' name='base-url-manager' rev='1.1.1'>
    <artifact name='$AID' ext='pom'></artifact>
    </dependency>
    如果Gradle出现implementation失败的情况,可以在Project的build.gradle里面添加如下:(也可以使用上面的GitPack来implementation)
    allprojects {
    repositories {
    maven { url 'https://dl.bintray.com/jenly/maven' }
    }
    }

    示例

    集成步骤代码示例 (示例出自于app中)

    Step.1 在您项目中的AndroidManifest.xml中通过配置meta-data来自定义全局配置

        <!-- 在你项目中添加注册如下配置 -->
    <activity android:name="com.king.base.baseurlmanager.BaseUrlManagerActivity"
    android:screenOrientation="portrait"
    android:theme="@style/BaseUrlManagerTheme"/>

    Step.2 在您项目Application的onCreate方法中初始化BaseUrlManager

        //获取BaseUrlManager实例(适用于v1.1.x版本)
    mBaseUrlManager = BaseUrlManager.getInstance();

    //获取BaseUrlManager实例(适用于v1.0.x旧版本)
    mBaseUrlManager = new BaseUrlManager(this);

    //获取baseUrl
    String baseUrl = mBaseUrlManager.getBaseUrl();

    Step.3 提供动态配置BaseUrl的入口(通过Intent跳转到BaseUrlManagerActivity界面)

    v.1.1.x 新版本写法

       BaseUrlManager.getInstance().startBaseUrlManager(this,SET_BASE_URL_REQUEST_CODE);

    v1.0.x 以前版本写法

        Intent intent = new Intent(this, BaseUrlManagerActivity.class);
    //BaseUrlManager界面的标题
    //intent.putExtra(BaseUrlManagerActivity.KEY_TITLE,"BaseUrl配置");
    //跳转到BaseUrlManagerActivity界面
    startActivityForResult(intent,SET_BASE_URL_REQUEST_CODE);

    Step.4 当配置改变了baseUrl时,在Activity或Fragment的onActivityResult方法中重新获取baseUrl即可


    //方式1:通过BaseUrlManager获取baseUrl
    String baseUrl = BaseUrlManager.getInstance().getBaseUrl();
    //方式2:通过data直接获取baseUrl
    UrlInfo urlInfo = BaseUrlManager.parseActivityResult(data);
    String baseUrl = urlInfo.getBaseUrl();

    更多使用详情,请查看app中的源码使用示例或直接查看API帮助文档

    BaseUrlManager.zip

    收起阅读 »

    Android沙雕操作之hook Toast

    一,背景 这是个沙雕操作,原因是:在小米手机的部分机型上,弹Toast时会在吐司内容前面带上app名称,如下: 此时产品经理发话了:为了统一风格,在小米手机上去掉Toast前的应用名。 网上有以下解决方案,比如:先给toast的message设置为空...
    继续阅读 »

    一,背景


    这是个沙雕操作,原因是:在小米手机的部分机型上,弹Toast时会在吐司内容前面带上app名称,如下:


    1.gif


    此时产品经理发话了:为了统一风格,在小米手机上去掉Toast前的应用名。


    网上有以下解决方案,比如:先给toastmessage设置为空,然后再设置需要提示的message,如下:


    Toast toast = Toast.makeText(context, “”, Toast.LENGTH_LONG);
    toast.setText(message);
    toast.show();

    但这些都不能从根本上解决问题,于是Hook Toast的方案诞生了。


    二,分析


    首先分析一下Toast的创建过程.


    Toast的简单使用如下:


    Toast.makeText(this,"abc",Toast.LENGTH_LONG).show();

    1,构造toast


    通过makeText()构造一个Toast,具体代码如下:


    public static Toast makeText(@NonNull Context context, @Nullable Looper looper,
    @NonNull CharSequence text, @Duration int duration)
    {
    if (Compatibility.isChangeEnabled(CHANGE_TEXT_TOASTS_IN_THE_SYSTEM)) {
    Toast result = new Toast(context, looper);
    result.mText = text;
    result.mDuration = duration;
    return result;
    } else {
    Toast result = new Toast(context, looper);
    View v = ToastPresenter.getTextToastView(context, text);
    result.mNextView = v;
    result.mDuration = duration;

    return result;
    }
    }

    makeText()中也就是设置了时长以及要显示的文本或自定义布局,对Hook什么帮助。


    2,展示toast


    接着看下Toast的show():


    public void show() {
    ...

    INotificationManager service = getService();
    String pkg = mContext.getOpPackageName();
    TN tn = mTN;
    tn.mNextView = mNextView;
    final int displayId = mContext.getDisplayId();

    try {
    if (Compatibility.isChangeEnabled(CHANGE_TEXT_TOASTS_IN_THE_SYSTEM)) {
    if (mNextView != null) {
    // It's a custom toast
    service.enqueueToast(pkg, mToken, tn, mDuration, displayId);
    } else {
    // It's a text toast
    ITransientNotificationCallback callback =
    new CallbackBinder(mCallbacks, mHandler);
    service.enqueueTextToast(pkg, mToken, mText, mDuration, displayId, callback);
    }
    } else {
    // 展示toast
    service.enqueueToast(pkg, mToken, tn, mDuration, displayId);
    }
    } catch (RemoteException e) {
    // Empty
    }
    }

    代码很简单,主要是通过serviceenqueueToast()enqueueTextToast()两种方式显示toast。


    service是一个INotificationManager类型的对象,INotificationManager是一个接口,这就为动态代理提供了可能。


    service是在每次show()时通过getService()获取,那就来看看getService():


    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
    private static INotificationManager sService;

    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
    static private INotificationManager getService() {
    if (sService != null) {
    return sService;
    }
    sService = INotificationManager.Stub.asInterface(
    ServiceManager.getService(Context.NOTIFICATION_SERVICE));
    return sService;
    }

    getService()最终返回的是sService,是一个懒汉式单例,因此可以通过反射获取到其实例。


    3,小结


    sService是一个单例,尅反射获取到其实例。


    sService实现了INotificationManager接口,因此可以动态代理。


    因此可以通过Hook来干预Toast的展示。


    三,撸码


    理清了上面的过程,实现就很简单了,直接撸码:


    1,获取sService的Field


    Class<Toast> toastClass = Toast.class;

    Field sServiceField = toastClass.getDeclaredField("sService");
    sServiceField.setAccessible(true);

    2,动态代理替换


    Object proxy = Proxy.newProxyInstance(Thread.class.getClassLoader(), new Class[]{INotificationManager.class}, new InvocationHandler() {
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

    return null;
    }
    });
    // 用代理对象给sService赋值
    sServiceField.set(null, proxy);

    3,获取sService原始对象


    因为动态代理不能影响被代理对象的原有流程,因此需要在第二步的InvocationHandler()invoke()中需要执行原有的逻辑,这就需要获取sService的原始对象。


    前面已经获取到了sService的Field,它是静态的,那直接通过sServiceField.get(null)获取不就可以了?然而并不能获取到,这是因为整个Hook操作是在应用初始化时,整个应用还没有执行过Toast.show()的操作,因此sService还没有初始化(因为它是一个懒汉单例)。


    既然不能直接获取,那就通过反射调用一下:


    Method getServiceMethod = toastClass.getDeclaredMethod("getService", null);
    getServiceMethod.setAccessible(true);
    Object service = getServiceMethod.invoke(null);

    接着完善一下第二步代码:


    Object proxy = Proxy.newProxyInstance(Thread.class.getClassLoader(), new Class[]{INotificationManager.class}, new InvocationHandler() {
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

    return method.invoke(service, args);
    }
    });

    到此,已经实现了对Toast的代理,Toast可以按照原始逻辑正常执行,但还没有加入额外逻辑。


    4,添加Hook逻辑


    InvocationHandlerinvoke()方法中添加额外逻辑:


    Object proxy = Proxy.newProxyInstance(Thread.class.getClassLoader(), new Class[]{INotificationManager.class}, new InvocationHandler() {
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    // 判断enqueueToast()方法时执行操作
    if (method.getName().equals("enqueueToast")) {
    Log.e("hook", method.getName());
    getContent(args[1]);
    }
    return method.invoke(service, args);
    }
    });

    args数组的第二个是TN类型的对象,其中有一个LinearLayout类型的mNextView对象,mNextView中有一个TextView类型的childView,这个childView就是展示toast文本的那个TextView,可以直接获取其文本内容,也可以对其赋值,因此代码如下:


    private static void getContent(Object arg) throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
    // 获取TN的class
    Class<?> tnClass = Class.forName(Toast.class.getName() + "$TN");
    // 获取mNextView的Field
    Field mNextViewField = tnClass.getDeclaredField("mNextView");
    mNextViewField.setAccessible(true);
    // 获取mNextView实例
    LinearLayout mNextView = (LinearLayout) mNextViewField.get(arg);
    // 获取textview
    TextView childView = (TextView) mNextView.getChildAt(0);
    // 获取文本内容
    CharSequence text = childView.getText();
    // 替换文本并赋值
    childView.setText(text.toString().replace("HookToast:", ""));
    Log.e("hook", "content: " + childView.getText());
    }

    最后看一下效果:


    2.gif


    四,总结


    这个一个沙雕操作,实际应用中这种需求也比较少见。通过Hook的方式可以统一控制,而且没有侵入性。大佬勿喷!!!



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

    未勾选用户协议、隐私政策实现抖动效果

    这是我参与新手入门的第2篇文章 产品看到别家的app,未勾选协议的时候,会给用户一个抖动效果的提示,感觉不错,然后看了看自家的app,不行,没有抖动,不能很明显表示,于是需求出来了,用户未勾选的时候,给个抖动效果。( 呵,都不能有点创新,当然不能说出来...
    继续阅读 »

    这是我参与新手入门的第2篇文章



    产品看到别家的app,未勾选协议的时候,会给用户一个抖动效果的提示,感觉不错,然后看了看自家的app,不行,没有抖动,不能很明显表示,于是需求出来了,用户未勾选的时候,给个抖动效果。( 呵,都不能有点创新,当然不能说出来了,只能内心暗说,哈哈,给自己加了点戏,)正事来了,开始。。。干,就完了。




    如果需要实现用户协议、隐私政策的代码,请看这篇文章:juejin.cn/post/698126…



    实现功能大概需要三个步骤:



    一、 用什么实现;二、实现的步骤;三、运行效果



    一、用什么实现



    其实实现起来很简单,用补间动画就行了。



    二、实现的步骤


    这里说下实现补间动画的步骤:总共需要以下几个步骤


    1.如果res目录下没有anim文件,就新建一个文件夹; image.png 2.在anim文件夹下创建一个名字叫translate_checkbox_shake.xml的文件,抖动动画


    <?xml version="1.0" encoding="utf-8"?>
    <translate xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="300"
    android:fromXDelta="0"
    android:interpolator="@anim/cyc"
    android:toXDelta="30">
    </translate>

    再在anim下创建一个插值器,名字叫cyc,这样会有抖动效果


    <?xml version="1.0" encoding="utf-8"?>
    <cycleInterpolator xmlns:android="http://schemas.android.com/apk/res/android"
    android:cycles="2">
    </cycleInterpolator>

    3.在translate_checkbox_shake.xml里写上需要的动画属性;


    android:duration="300"与android:cycles="2"联合表示在300毫秒内将动画执行2次,根据需求来设置就行了;


    属性toXDelta和fromXDelta是横向效果,toYDela和fromYDelta是竖向,感兴趣的可以尝试下。、


    4.在代码中使用 AnimationUtils.loadAnimation加载新创建的动画文件; image.png


     val animation = AnimationUtils.loadAnimation(this, R.anim.translate_checkbox_shake)

    5.在代码中使用View的startAnimation启动动画,完事


     binding.llShake.startAnimation(animation)

    三、效果如下:


    20210704160630743.gif


    作者:JasonYin

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

    电子厂里撂了挑子,我默默自学起了Android|2021年中总结

    大四那年我被骗到了电子厂,无法忍受流水线的工作,愤而撩了挑子。前途一片渺茫的时候,我连夜爬起自学起了Android,开启了我的Android开发之路。至今已毕业多年,一直在这条热爱的道路上坚持着,快乐、知足、感恩。 分享我的故事之前,先简单回顾一下我这半年都...
    继续阅读 »

    大四那年我被骗到了电子厂,无法忍受流水线的工作,愤而撩了挑子。前途一片渺茫的时候,我连夜爬起自学起了Android,开启了我的Android开发之路。至今已毕业多年,一直在这条热爱的道路上坚持着,快乐、知足、感恩。


    分享我的故事之前,先简单回顾一下我这半年都干了啥。


    这半年


    年初看到了一篇文章《我的 2020 年终总结》,深受感染。作者杰哥在2020一整年,始终坚持日更输出,拿到了多个平台的证书和奖杯。同时还学做了多道菜品,期间还坚持健身和旅游放松。一年同样是365天,别人竟过得如此充实、如此精彩!


    钦佩之余我不禁陷入了思考,联想到了自己。忽然意识到自高考以后,总是间歇性踌躇满志无疾而终,太久没有为一个目标而坚持了。 我想好好做成一件事情,我要给自己定个目标。我擅长Android开发,那就坚持写作,保证一两个礼拜输出一篇高质量文章。


    一则将自己用心打磨的东西分享出来,帮助别的开发者;二来利用持续的输出倒逼自己不断地摄入新知识,迫使我持续学习,养成终生学习、定期总结的好习惯。但分享给大家看的东西不比私人笔记,需要注意很多细节,诸如深入的理解、通俗的讲解、友好的排版等等。


    为此我做了很多准备,潜心学习了很多优质文章的行文风格、目录次序、MarkDown语言以及一堆作图工具。接着删除了手机、平板里的游戏和视频软件等一切时间杀手。另外收集了大量Android相关的优质话题。并买了个专业的待办事项App,用来随时记录新的灵感,高效地安排每篇文章的写作计划。万事俱备,一月底的时候就开始了半学习、半摸索的写作之路。


    写了一些文章


    半年不到的时间内我输出了十四篇技术文章和三篇随笔。技术文章主要聚焦在Android领域比较流行的话题,比如持续火爆的Jetpack框架,重大UI变革的Compose 工具包,即将发布的Android 12系统以及国人热捧的鸿蒙系统。



    被多个官方转载


    自High的文章没有价值,好在我不是自我感动,写的文章被多个官方平台转载。【深度解读Jetpack框架的基石-AppCompat】是第一篇被Google转载的文章,我很激动、也很意外。因为那是今年输出的第一篇文章,排版和措辞都略显粗糙。很感谢他们提供的平台,这些认同让我坚定了写作方向。


    2篇文章被Android官方公众号转载:



    3篇文章被CSDN官方公众号转载:



    1篇文章被搜狐技术公众号分享:



    1篇文章被掘金官方公众号转载:



    额外赞扬一波掘金平台,上面的高质量文章很多,技术氛围很好。我在这里读到了很多优质文章,也结识了很多优秀作者。而且相较其他平台,掘金对于新人更加友好,只要你的文章认真、质量过关,掘金不会吝啬曝光量。我入驻掘金的时间不长,但前两个月都闯进了移动端前二十的作者榜单,比心。



    特别感谢鸿神


    从事Android工作以来,拜读过鸿神的很多文章,但并不认识。写文章这段时间与鸿神有了多次交流,在钦佩他技术厉害的同时,更感受到他为人的Nice。很感激他的个人公众号转载过我多篇文章,给予的帮助。



    接受认可以及批评


    当然,输出文章的初衷还是希望对大家有所帮助。欣慰的是文章受到了很多积极的评价:有留下“全网最佳”评价的朋友,也有专门加我好友跟我道谢的朋友。你们的认可是我持续输出的最大动力。



    有赞扬自然也有批评,有些朋友说我某个知识点没提到、评价Demo难以理解、吐槽技术点过时。。。真的,我诚恳接受每个批评,将努力发掘和改正这些不足。


    我沉迷于将一个技术点一次性讲清楚,又常常选取一个大的话题,最终导致文章的篇幅都很大。这又需要准备很长时间,而这些时间都来源于工作、生活之余的零碎片段。思路非常很容易被打断,一不小心就错过某个细节,或者代码写得仓促,请大家多多包涵。


    那年高考


    回到文章的标题上来,回顾下我与Android结缘的心路历程,这还得从那年高考讲起。


    高考已过十年有余,那会儿的江苏高考已经很卷,一年一度的新政策搞得我们无所适从。还好我高二那年一鼓作气,势如破竹拿下小四门全A。可惜高考的时候还是大意了,即便我侥幸冲破了葛军神卷的围堵,还是栽在了语文作文上。不会出问题的化学还是出了问题,痛失了6A。在双重失利的情况下,艰难地挺过了一本线。


    与理想的211大学失之交臂后,只能在一众双非大学里碰碰运气了。路过江苏大学招生座位的时候,他们的老师对我兴趣十足,想跟我签订个志愿协议:保证能上他们学校的四个好专业之一,最终录取则要按照我定的顺序来。他提供了车辆工程机械工程电气工程电子信息工程这几个专业,事实上这个顺序已经按照分数线进行了由高到低的排名。


    爸爸和我在前一分钟还不知道江苏有个不在南京的江苏大学(散装江苏还真不是说笑的)。我们对于这个大学和这些专业完全不了解,彻底犯了难。不知道怎么选,更不知道怎么排序。在这重要的抉择时刻,爸爸把选择权交给了我,让我按照自己的想法来(内心OS:呐,你自己选哦,选错了别怨我)。


    面对这一众陌生又熟悉的名词,稚嫩的高三学生开始了他的内心戏:



    • 车辆工程?机械工程?是要学修车吗,还是做拖拉机,摩托车啥的,还是不要了吧

    • 电气工程?是学做电工吗,上电线杆修变压器的那种?但跟我喜欢的物理貌似有点关系,还不错

    • 电子信息工程?电子?电路?芯片?手机?手机能打电话、发短信、玩游戏,高端、有意思,就它了


    所以我在协议上郑重写下了:电子信息工程 > 电气工程 > 机械工程> 车辆工程。是的,我把顺序完美调了个头,哈哈。爸爸看到这个完全颠倒的顺序后,一脸疑惑,隐约不安。但确认了我坚定无比的眼神后,欲言又止,不想耽误我的远大前程。



    12-widget

    结果可想而知,毫无悬念地被江苏大学电子信息工程专业成功录取。进入学校后我才了解到这几个专业的真实情况后,心里直呼草率了,捂脸。


    我的大学



    电子信息工程专业确如我猜想的那样,跟芯片有关系。除此之外,还跟通信、操作系统密不可分。要学的知识点超级多:有令人头皮发麻的数电模电、单片机,需要记忆一堆公式的通信原理,C语言、Java语言和数据库。一句话,很多很散,复杂且枯燥。完全不是我想象中手机的有趣样子,自然是提不起一点兴趣。


    加上高中老师“认真学,到大学就解放了” 的反复洗脑深深地影响了我,便开始混日子。翘课是常有的事,连高等数学挂科了,都没激起我内心的一点涟漪。现在想来也不赖高中老师,这就是给自己的懒惰找的借口,哈哈。


    玩命地打工


    考研是不可能考研的,进大学的时候我就笃定了毕业后直接参加工作,去挣钱。工作需要什么?当时的我浅薄地以为,表达能力、处事能力这些社交素质才是最重要的。可这些本事,学校里不教啊。那就到社会中去,去打工,玩命地打工,还能挣到零花钱。


    在这样的“指导思想”下,大学的寒暑假,几乎都在打工中度过。前前后后在台湾仁宝代工厂做过工人,在日本妮飘面纸厂做过保安,在苏宁电器卖过步步高手机(那一整个暑假,耳朵都被宋慧乔的广告插曲统治着)。。。



    多份打工的体验,让我待人接物变得更加自信、接触新的环境也更加的从容,好像确实提升了所谓的社交素质。但让我感受最深的是,很多工作真的不容易,大学里不愁吃穿、只要顾好学习一件事情的生活真的太珍贵了,可那时候就是没有毅力去珍惜。


    肆意的青春


    大学里特别迷恋某位明星,就跟着一起痴迷Hipop文化。喜欢的歌以说唱为主,看的书都是日韩、港台潮流杂志,外在就更“嘻哈”了:染一头金色头发、打个“钻石”耳钉、戴个夸张的耳环、穿一套炸街的嘻哈服装。从里到外都很Real,简直就是学院里最靓的仔。那个时候Hipop没现在火,知道和接受的人很少,我在他们眼中特别另类,但我不Care。打工得来的大部分钱也都花在了置办这些行头上,在淘宝还不流行的年代买成了淘宝的五星买家。



    12-widget

    看似充实的大学生活,难掩空洞和无聊。除了帝国时代文明的陪伴,就通过画画、练字来排遣这无病呻吟的时光。


    大四了还去电子厂装电路板?


    浑浑噩噩地熬到了大四,终于到检验我社交才能的时候了。信心满满地参加了多个宣讲会,最后竟没有一家企业欣赏我“名企”的兼职经历,连笔试机会都不给啊。接连遭受企业的无情毒打,我才认识到专业成绩和基础仍然是企业最看重的东西。 可这就被动了,书本这一块早就被我放弃了。当年可是村里的高考状元啊,要是连工作都没找到就太丢人了!这种焦虑的状况持续了一个多月。



    12-widget

    工作还得继续找啊!痛定思痛,开始仔细地分析。恶补成绩和基础已经不可能了,那就去整点硬核的实习经验,在专业经验这块弯道超车。 恰好一个电子公司到学校招实习生,说是画PCB电路板子,还发正规的实习证书。这简直是雪中送炭,不拿工资我也得去啊。


    到了之后就傻眼了,压根不是想像中的电子公司,而是一家装配电瓶车充电器的电子厂。算嘞,既来之则安之,给我画电路图就行。可他们让我们一帮学生到流水线上组装电路板,就是左手拿电阻右手拿二极管,在快速转动的传送带上放元件!过分!


    才练习了半小时就得全部上流水线,我手忙脚乱地忙到几乎崩溃。联想到之前在代工厂的打工经历,心里直犯嘟哝:这哪是实习,分明就是打零工嘛,干上一年我还是找不到工作啊,简直就是在浪费时间! 我越想越气,越气装得越乱,越乱越被骂。情绪被逼到了极点,我甩开了电路板子,气呼呼地跟领班说:我,不干了!


    管不了工人们鄙视的眼神,我像逃兵一样跑了出来,钻上了回学校的公交。一路上都在跟自己较劲:你就这么跑了对吗?这点苦都受不了以后能干好什么?跟爸妈吹嘘的实习证书又该怎么办?


    复杂的情绪笼罩了一整天,直到晚上睡觉,还在为这事犯愁。


    Android给了我曙光


    躺在床上,思绪不禁回到了三年前。那时的我对手机兴趣满满,选择了这个专业。如今专业四年即将划上终点,而当初的梦想却未曾踏出半步。 惆怅之余看了眼身旁的HTC G14手机,突然想起店员曾说过它搭载了时下最火的Android智能系统。又回想起学校里曾经有过Android开发的培训广告,我不禁两眼放光:手机我有了,正好是这个最火的Android系统,那干嘛不开发个软件试试呢?如果能开发个完整的App,简历里、面试时不就有东西可说了嘛!


    想罢,立马从床上爬起来搜索关于Android开发的一切。那个年代Android Studio还没发布,开发资料更少得可怜。庆幸我学习能力还不错,顺利地装好了驱动、打开了开发者模式、搭好了EclipseSDK环境,这时候已经到了深夜。当G14成功运行了Hello world的时候,我情不自禁地炸了一句“Yeah”,气得舍友直骂娘。那一刻我兴奋不已,因为我感觉找对了方向。


    网上的资料少且零碎,第二天一早就去图书馆找相关书籍。谢天谢地,还真有一本Android相关的书。我抱着手里的“圣经”,虔诚地学习了各种控件的使用,小心翼翼地倒腾了两天,终于搞出了一个播放本地mp3的播放界面。看到这有模有样的成果,成就感爆棚。于是乘胜追击,加了很多小功能:音乐封面、上下首、播放模式、文件列表、主题切换、启动画面等等。


    大概又搞了一个礼拜,一个完整的音乐App成型了。我把杰作安装到G14上,随身携带。面试的时候时不时拿出来演示一番,顺带着复述着那些似懂非懂的API。 那个年代懂Android的人很少,我如愿以偿地找到了Android开发工作。我清晰地记得拿到Offer后,爸爸在电话那头的兴奋。在他们不看好的方向上获得成功、受到认可的感觉真得很棒!


    打那以后,我对Android的兴趣一发不可收拾。在学校的最后一点时光里,总忍不住开发个小Demo把玩把玩,时不时地刷个新Rom体验体验。G14很快就被折腾不行了,对我而言这是一部意义非凡的手机,多次搬家都不忍丢弃。 如今那个启蒙App早已找不着了,很想找来跑一跑,康康当时写的代码有多烂、界面有多丑,哈哈。


    社会人


    说不清是音乐App助我找到了工作,还是自学Android的热情打动了公司,给了我机会。


    我有幸一直从事品牌手机的ROM开发工作,从开发第三方App、到修改系统App、再到定制Framework;从面向Bug编程、到面向对象编程、再到面向产品编程,一晃已过了七年!


    临笔前特地到官网瞅了一眼这些年开发过的Android设备,有20多部。当这么多部造型各异的手机和平板,平铺在电脑面前时,回忆历历在目、感慨不已。


    成长为安卓老兵的同时,外在也不可抗拒地发生变化。发际线渐渐失守,眼镜戴上就摘不下来了,身形也渐渐走样。好像也不全是坏事,它们提醒着我在工作、生活、学习的同时,时刻关注身体健康。



    半山腰回望


    如果大四那年没有在电子厂里撂挑子,我大概率不会自学Android。可能最终也能找着工作,但极有可能不会从事我如今热爱的Android行业。


    我很荣幸参与和见证了这个行业的发展,这些年它变化太快,像是一场狂欢。从颠覆移动领域的变革时代,到移动互联的红利时代,再到如今内卷严重的存量时代,各方都在努力地维持或改变:



    • 巨头们在不断调整战略:Google通过GMS想方设法地控制Android系统,厂商们在同质化严重的Android设备里寻求亮点和突破,在传统设备以外持续探索和开发新的赛道。。。

    • 开发者们亦疲于奔命:应对各种快速迭代的新技术,应付各种碎片化ROM的适配,苦于前端、跨平台技术的蚕食。。。


    移动互联的落寞必然引发Android市场的紧缩,企业对于Android群体的要求将持续拉高,Android开发的内卷加剧则是不争的事实。 如果热爱Android、对Android仍有信心,时刻保持技术人的好奇心和探索欲吧,对新技术以及新领域:



    • AABJetpackKotlinComposeFlutter。。。

    • 革新的智能座舱、划时代的自动驾驶、万物互联的鸿蒙、一统Android和Chrome OS的Fuchsia。。。



    最后一点碎碎念



    I always knew what the right path was. Without exception, l knew, but l never took it. You know why ? lt was too damn hard.



    这是我最喜欢的电影《闻香识女人》里迈克中校的感人自白:“无一例外,我永远知道哪条路是对的。但我从来不走,因为太XX难了”。知易行难,这无疑是古今中外、亘古不变的难题。它关乎的东西太多:改变自律坚持成长,哪一个都不好对付。


    如今的我早已被生活磨平了棱角,渐渐丢掉了当年的那份冲劲和激情。但每每想起当年那个敢于说不、熬夜自学的我,感慨之余多了一份坚持。


    也许你也曾踌躇满志、无疾而终,记得想想最初的自己,你会找到那个答案。


    正值毕业季,祝福即将踏入社会的新朋友,以及社会中浮沉的老朋友,都有个淋漓尽致的人生!



    作者:TechMerger
    链接:https://juejin.cn/post/6982002538069360676
    来源:掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。


    收起阅读 »

    想搞懂Jetpack架构可以不搞懂生命周期知识吗?

    1. 前言 Activity生命周期真是一个非常古老的话题,无论是10年前,还是当下。不管是面试还是工作,经常会遇到与Activity生命周期相关的问题。比如“按下返回键和Home键,生命周期方法调用顺序”、“A启动B,它们的生命周期方法调用顺序”。工作中,...
    继续阅读 »

    1. 前言


    Activity生命周期真是一个非常古老的话题,无论是10年前,还是当下。不管是面试还是工作,经常会遇到与Activity生命周期相关的问题。比如“按下返回键和Home键,生命周期方法调用顺序”、“A启动B,它们的生命周期方法调用顺序”。工作中,Jetpack Lifecycle、LiveData、ViewModel等组件都是建立在生命周期之上。


    在我研究Jetpack Lifecycle、LiveData、ViewModel源码时,我发现它们与组件的生命周期有很大的关系。它们能够自动感知组件的生命周期变化。LiveData能够在onDestroy方法调用时自动将监听注销掉,ViewModel能够在Configuration发生改变时(比如旋转屏幕)自动保存数据,并且在Activity重建时恢复到Configuration发生改变之前。


    本文我将从几个场景详细介绍Activity的生命周期变化。


    2. 单Activity按返回按钮


    触发步骤:



    • 按返回按钮

    • 或者调用finish方法

    • 重新进入Activity


    该场景演示了用户启动,销毁,重新进入Activity的生命周期变化。调用顺序如图:


    状态管理:



    • onSaveInstanceState没有被调用,因为Activity被销毁,没有必要保存状态

    • 当Activity被重新进入时,onCreate方法bundle参数为null


    3. 单Activity按Home键


    触发步骤:



    • 用户按Home键

    • 或者切换至其它APP

    • 重新进入Activity


    该场景Activity会调用onStop方法,但是不会立即调用onDestroy方法。调用顺序如图:


    状态管理:


    当Activity进入Stopped状态,系统使用onSaveInstanceState保存app状态,以防系统将app进程杀死,重启后恢复状态。


    4. 单Activity旋转屏幕


    触发步骤:



    • Configuration发生改变, 比如旋转屏幕

    • 用户在多窗口模式下调整窗口大小


    当用户旋转屏幕,系统会保留旋转之前的状态,能很好的恢复到之前的状态。调用顺序如图:


    状态管理:



    • Activity被完全销毁掉,但是状态会被保存,而且会在新的Activity中恢复该状态

    • onCreate和onRestoreInstanceState方法中的bundle是一样的


    5. 单Activity弹出Dialog


    触发步骤:



    • 在API 24+上开启多窗口模式失去焦点时

    • 其它应用部分遮盖当前APP,比如弹出权限授权dialog

    • 弹出intent选择器时,比如弹出系统的分享dialog



    该场景不适用于以下情况:



    • 相同APP中弹dialog,比如弹出AlertDialog或者DialogFragment不会导致Activity onPause发生调用

    • 系统通知。当用户下拉系统通知栏时,不会导致下面的Activity onPause发生调用。


    6. 多个Activity跳转


    触发步骤:



    • activity1 跳转到activity2

    • 按返回按钮



    注意:activity1 跳转到activity2 正确的调用顺序是



    ->activity1.onPause


    ->activity2.onCreate


    ->activity2.onStart


    ->activity2.onResume


    ->activity1.onStop


    ->activity1.onSaveInstanceState



    在该场景下,当新的activity启动时,activity1处于STOPPED状态下(但是没有被销毁),这与用户按Home键有点类似。当用户按返回按钮时,activity2被销毁掉。


    状态管理:



    • onSaveInstanceState会被调用,但是onRestoreInstanceState不会。当activity2展示在前台时,如果发生了旋转屏幕,当activity1再次获得焦点时,它将会被销毁并且重建,这就是为什么activity1在失去焦点时为什么需要保存状态。

    • 如果系统杀死了app进程,该场景后面会介绍到


    7. 多个Activity跳转,并且旋转屏幕



    • activity1 跳转到activity2

    • 在activity2上旋转屏幕

    • 按返回按钮



    注意: 当返回activity1时,必须保证屏幕是保持旋转后的状态,否则并不会调用onDestroy方法。而且是在activity1回到前台时才会主动掉onDestroy


    状态管理:


    保存状态对所有的activity都非常重要,不仅仅是对前台activity。所有在后台栈中的activity在configuration发生改变时重建UI时都需要将保存的状态恢复回来。


    8. 多个Activity跳转,被系统kill掉app



    • activity1 跳转到activity2

    • 在activity2上按Home键

    • 系统资源不足kill app



    9. 总结


    本文主要是从Google大佬Jose Alcérreca的文章翻译过来。他假设的这7个关于activity的生命周期场景,对了解Lifecycle有非常大的帮助。甚至对于面试都是有非常大的帮助。


    后续我会写一系列关于Jetpack的文章。文风将会延续我的一贯风格,深入浅出,坚持走高质量创作路线。本文是我讲解Lifecycle的开篇之作。生命周期是Lifecycle、LiveDa、ViewModel等组件的基础。在对生命周期知识掌握不牢靠的情况,去研究那些组件,无异于空中楼阁。



    作者:字节小站
    链接:https://juejin.cn/post/6981965690014007327
    来源:掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。


    收起阅读 »

    KingPlayer 一个专注于 Android 视频播放器(IjkPlayer、ExoPlayer、VlcPlayer、SysPlayer)的基础库

    KingPlayerKingPlayer 一个专注于 Android 视频播放器(IjkPlayer、ExoPlayer、VlcPlayer、SysPlayer)的基础库,无缝切换内核。功能说明 主要播放相关核心功能 播放器无缝切换&nbs...
    继续阅读 »

    KingPlayer

    KingPlayer 一个专注于 Android 视频播放器(IjkPlayer、ExoPlayer、VlcPlayer、SysPlayer)的基础库,无缝切换内核。

    功能说明

    •  主要播放相关核心功能
    •  播放器无缝切换
      •  MediaPlayer封装实现(SysPlayer)
      •  IjkPlayer封装实现
      •  ExoPlayer封装实现
      •  vlc-android封装实现
    •  控制图层相关
      •  待补充...

    Gif 展示

    Image

    录制的gif效果有点不清晰,可以下载App查看详情。

    引入

    gradle:

    使用 SysPlayer (Android自带的MediaPlayer)

    //KingPlayer基础库,内置SysPlayer
    implementation 'com.king.player:king-player:1.0.0-beta1'

    使用 IjkPlayer

    //KingPlayer基础库(必须)
    implementation 'com.king.player:king-player:1.0.0-beta1'
    //IjkPlayer
    implementation 'com.king.player:ijk-player:1.0.0-beta1'

    // 根据您的需求选择ijk模式的so
    implementation 'tv.danmaku.ijk.media:ijkplayer-armv7a:0.8.8'
    // Other ABIs: optional
    implementation 'tv.danmaku.ijk.media:ijkplayer-armv5:0.8.8'
    implementation 'tv.danmaku.ijk.media:ijkplayer-arm64:0.8.8'
    implementation 'tv.danmaku.ijk.media:ijkplayer-x86:0.8.8'
    implementation 'tv.danmaku.ijk.media:ijkplayer-x86_64:0.8.8'

    使用 ExoPlayer

    //KingPlayer基础库(必须)
    implementation 'com.king.player:king-player:1.0.0-beta1'
    //ExoPlayer
    implementation 'com.king.player:exo-player:1.0.0-beta1'

    使用 VlcPlayer

    //KingPlayer基础库(必须)
    implementation 'com.king.player:king-player:1.0.0-beta1'
    //VlcPlayer
    implementation 'com.king.player:vlc-player:1.0.0-beta1'

    示例

    布局示例

        <com.king.player.kingplayer.view.VideoView
    android:id="@+id/videoView"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />

    代码示例

            //初始化一个视频播放器(IjkPlayer、ExoPlayer、VlcPlayer、SysPlayer)
    videoView.player = IjkPlayer(context)
    //初始化数据源
    val dataSource = DataSource(url)
    videoView.setDataSource(dataSource)

    videoView.setOnSurfaceListener(object : VideoView.OnSurfaceListener {
    override fun onSurfaceCreated(surface: Surface, width: Int, height: Int) {
    LogUtils.d("onSurfaceCreated: $width * $height")
    videoView.start()
    }

    override fun onSurfaceSizeChanged(surface: Surface, width: Int, height: Int) {
    LogUtils.d("onSurfaceSizeChanged: $width * $height")
    }

    override fun onSurfaceDestroyed(surface: Surface) {
    LogUtils.d("onSurfaceDestroyed")
    }

    })

    //缓冲更新监听
    videoView.setOnBufferingUpdateListener {
    LogUtils.d("buffering: $it")
    }
    //播放事件监听
    videoView.setOnPlayerEventListener { event, bundle ->

    }
    //错误事件监听
    videoView.setOnErrorListener { event, bundle ->

    }


            
    //------------ 控制相关
    //开始
    videoView.start()
    //暂停
    videoView.pause()
    //进度调整到指定位置
    videoView.seekTo(pos)
    //停止
    videoView.stop()
    //释放
    videoView.release()
    //重置
    videoView.reset()

    更多使用详情,请查看app中的源码使用示例或直接查看API帮助文档

    其他

    需使用JDK8+编译,在你项目中的build.gradle的android{}中添加配置:

    compileOptions {
    targetCompatibility JavaVersion.VERSION_1_8
    sourceCompatibility JavaVersion.VERSION_1_8
    }

    代码下载:KingPlayer.zip

    收起阅读 »

    KingKeyboard for Android 是一个自定义键盘

    KingKeyboardKingKeyboard for Android 是一个自定义键盘。内置了满足各种场景的键盘需求:包括但不限于混合、字母、数字、电话、车牌号等可输入场景。还支持自定义。集成简单,键盘可定制化。引入Maven:<dependency...
    继续阅读 »


    KingKeyboard

    KingKeyboard for Android 是一个自定义键盘。内置了满足各种场景的键盘需求:包括但不限于混合、字母、数字、电话、车牌号等可输入场景。还支持自定义。集成简单,键盘可定制化。


    引入

    Maven:

    <dependency>
    <groupId>com.king.keyboard</groupId>
    <artifactId>kingkeyboard</artifactId>
    <version>1.0.0</version>
    <type>pom</type>
    </dependency>

    Gradle:

    //AndroidX
    implementation 'com.king.keyboard:kingkeyboard:1.0.0'

    Lvy:

    <dependency org='com.king.keyboard' name='kingkeyboard' rev='1.0.0'>
    <artifact name='$AID' ext='pom'></artifact>
    </dependency>
    如果Gradle出现compile失败的情况,可以在Project的build.gradle里面添加如下:(也可以使用上面的GitPack来complie)
    allprojects {
    repositories {
    //...
    maven { url 'https://dl.bintray.com/jenly/maven' }
    }
    }

    自定义按键值


    /*
    * 在KingKeyboard的伴生对象中定义了一些核心的按键值,当您需要自定义键盘时,可能需要用到
    */

    //------------------------------ 下面是定义的一些公用功能按键值
    /**
    * Shift键 -> 一般用来切换键盘大小写字母
    */
    const val KEYCODE_SHIFT = -1
    /**
    * 模式改变 -> 切换键盘输入法
    */
    const val KEYCODE_MODE_CHANGE = -2
    /**
    * 取消键 -> 关闭输入法
    */
    const val KEYCODE_CANCEL = -3
    /**
    * 完成键 -> 长出现在右下角蓝色的完成按钮
    */
    const val KEYCODE_DONE = -4
    /**
    * 删除键 -> 删除输入框内容
    */
    const val KEYCODE_DELETE = -5
    /**
    * Alt键 -> 预留,暂时未使用
    */
    const val KEYCODE_ALT = -6
    /**
    * 空格键
    */
    const val KEYCODE_SPACE = 32

    /**
    * 无作用键 -> 一般用来占位或者禁用按键
    */
    const val KEYCODE_NONE = 0

    //------------------------------

    /**
    * 键盘按键 -> 返回(返回,适用于切换键盘后界面使用,如:NORMAL_MODE_CHANGE或CUSTOM_MODE_CHANGE键盘)
    */
    const val KEYCODE_MODE_BACK = -101

    /**
    * 键盘按键 ->返回(直接返回到最初,直接返回到NORMAL或CUSTOM键盘)
    */
    const val KEYCODE_BACK = -102

    /**
    * 键盘按键 ->更多
    */
    const val KEYCODE_MORE = -103

    //------------------------------ 下面是自定义的一些预留按键值,与共用按键功能一致,但会使用默认的背景按键

    const val KEYCODE_KING_SHIFT = -201
    const val KEYCODE_KING_MODE_CHANGE = -202
    const val KEYCODE_KING_CANCEL = -203
    const val KEYCODE_KING_DONE = -204
    const val KEYCODE_KING_DELETE = -205
    const val KEYCODE_KING_ALT = -206

    //------------------------------ 下面是自定义的一些功能按键值,与共用按键功能一致,但会使用默认背景颜色

    /**
    * 键盘按键 -> 返回(返回,适用于切换键盘后界面使用,如:NORMAL_MODE_CHANGE或CUSTOM_MODE_CHANGE键盘)
    */
    const val KEYCODE_KING_MODE_BACK = -251

    /**
    * 键盘按键 ->返回(直接返回到最初,直接返回到NORMAL或CUSTOM键盘)
    */
    const val KEYCODE_KING_BACK = -252

    /**
    * 键盘按键 ->更多
    */
    const val KEYCODE_KING_MORE = -253

    /*
    用户也可自定义按键值,primaryCode范围区间为-999 ~ -300时,表示预留可扩展按键值。
    其中-399~-300区间为功能型按键,使用Special背景色,-999~-400自定义按键为默认背景色
    */

    示例

    代码示例

        //初始化KingKeyboard
    kingKeyboard = KingKeyboard(this,keyboardParent)
    //然后将EditText注册到KingKeyboard即可
    kingKeyboard.register(editText,KingKeyboard.KeyboardType.NUMBER)

    /*
    * 如果目前所支持的键盘满足不了您的需求,您也可以自定义键盘,KingKeyboard对外提供自定义键盘类型。
    * 自定义步骤也非常简单,只需自定义键盘的xml布局,然后将EditText注册到对应的自定义键盘类型即可
    *
    * 1. 自定义键盘Custom,自定义方法setKeyboardCustom,键盘类型为{@link KeyboardType#CUSTOM}
    * 2. 自定义键盘CustomModeChange,自定义方法setKeyboardCustomModeChange,键盘类型为{@link KeyboardType#CUSTOM_MODE_CHANGE}
    * 3. 自定义键盘CustomMore,自定义方法setKeyboardCustomMore,键盘类型为{@link KeyboardType#CUSTOM_MORE}
    *
    * xmlLayoutResId 键盘布局的资源文件,其中包含键盘布局和键值码等相关信息
    */
    kingKeyboard.setKeyboardCustom(R.xml.keyboard_custom)
    // kingKeyboard.setKeyboardCustomModeChange(xmlLayoutResId)
    // kingKeyboard.setKeyboardCustomMore(xmlLayoutResId)
    kingKeyboard.register(et12,KingKeyboard.KeyboardType.CUSTOM)
     //获取键盘相关的配置信息
    var config = kingKeyboard.getKeyboardViewConfig()

    //... 修改一些键盘的配置信息

    //重新设置键盘配置信息
    kingKeyboard.setKeyboardViewConfig(config)

    //按键是否启用震动
    kingKeyboard.setVibrationEffectEnabled(isVibrationEffectEnabled)

    //... 等等,还有各种监听方法。更多详情,请直接使用。
        //在Activity或Fragment相应的生命周期中调用,如下所示

    override fun onResume() {
    super.onResume()
    kingKeyboard.onResume()
    }

    override fun onDestroy() {
    super.onDestroy()
    kingKeyboard.onDestroy()
    }

    相关说明

    • KingKeyboard主要采用Kotlin编写实现,如果您的项目使用的是Java编写,集成时语法上可能稍微有点不同,除了结尾没有分号以外,对应类伴生对象中的常量,需要通过点伴生对象才能获取。
      //Kotlin 写法
    var keyCode = KingKeyboard.KEYCODE_SHIFT
      //Java 写法
    int keyCode = KingKeyboard.Companion.KEYCODE_SHIFT;

    更多使用详情,请查看app中的源码使用示例

    代码下载:KeyboardVisibilityEvent.zip

    收起阅读 »

    WordPOI是一个将Word接口文档转换成JavaBean的工具库

    WordPOIWordPOI是一个将Word接口文档转换成JavaBean的工具库,主要目的是减少部分无脑的开发工作。核心功能:将文档中表格定义的实体转换成Java实体对象WordPOI特性说明支持解析doc格式和docx格式的Word文档支持批量解析Word...
    继续阅读 »


    WordPOI


    WordPOI是一个将Word接口文档转换成JavaBean的工具库,主要目的是减少部分无脑的开发工作。

    核心功能:将文档中表格定义的实体转换成Java实体对象

    WordPOI特性说明

    1. 支持解析doc格式和docx格式的Word文档
    2. 支持批量解析Word文档并转换成实体
    3. 解析配置支持自定义,详情请查看{@link ParseConfig}相关配置
    4. 虽然解析可配置,但因文档内容的不可控,解析转换也具有一定的局限性

    只要在文档上定义实体对象时,尽量满足示例文档的规则,就可以规避解析转换时的局限性。

    ParseConfig属性说明

    属性值类型默认值说明
    startTableint0开始表格
    startRowint1开始行
    startColumnint0开始列
    fieldNameColumnint0字段名称所在列
    fieldTypeColumnint1字段类型所在列
    fieldDescColumnint2字段注释说明所在列
    charsetNameStringUTF-8字符集编码
    genGetterAndSetterbooleantrue是否生成get和set方法
    genToStringbooleantrue是否生成toString方法
    useLombokbooleanfalse是否使用Lombok
    parseEntityNamebooleanfalse是否解析实体名称
    entityNameRowint0实体名称所在行
    entityNameColumnint0实体名称所在列
    serializablebooleanfalse是否实现Serializable序列化
    showHeaderbooleantrue是否显示头注释
    headerStringCreated by WordPOI头注释内容
    transformationsMap<String,String>需要转型的集合(自定义转型配置)

    引入

    Maven:

    <dependency>
    <groupId>com.king.poi</groupId>
    <artifactId>word-poi</artifactId>
    <version>1.0.1</version>
    <type>pom</type>
    </dependency>

    Gradle:

    compile 'com.king.poi:word-poi:1.0.1'

    Lvy:

    <dependency org='com.king.poi' name='word-poi' rev='1.0.1'>
    <artifact name='$AID' ext='pom'></artifact>
    </dependency>
    如果Gradle出现compile失败的情况,可以在Project的build.gradle里面添加如下:(也可以使用上面的GitPack来complie)
    allprojects {
    repositories {
    maven { url 'https://dl.bintray.com/jenly/maven' }
    }
    }

    引入的库:

    compile 'org.apache.poi:poi:4.1.0'
    compile 'org.apache.poi:poi-ooxml:4.1.0'
    compile 'org.apache.poi:poi-scratchpad:4.1.0'

    如想直接引入jar包可直接点击左上角的Download下载最新的jar,然后引入到你的工程即可。

    示例

    代码示例 (直接在main方法中调用即可)

            try {

    /**
    * 解析文档中的表格实体,表格包含了实体名称,只需配置 {@link ParseConfig#parseEntityName} 为 true 和相关对应行,即可开启自动解析实体名称,自动解析实体名称
    * {@link ParseConfig}中包含解析时需要的各种配置,方便灵活的支持文档中更多的表格样式
    */
    ParseConfig config = new ParseConfig.Builder().startRow(2).parseEntityName(true).build();
    WordPOI.wordToEntity(Test.class.getResourceAsStream("Api3.docx"),false,"C:/bean/","com.king.poi.bean",config);
    //解析文档docx格式 需要传生成的对象实体名称
    // WordPOI.wordToEntity(Test.class.getResourceAsStream("Api1.docx"),false,"C:/bean/","com.king.poi.bean","Result","PageInfo");
    //解析文档docx格式 需要传生成的对象实体名称
    // WordPOI.wordToEntity(Test.class.getResourceAsStream("Api2.doc"),true,"C:/bean/","com.king.poi.bean","TestBean");
    } catch (Exception e) {
    e.printStackTrace();
    }
    • 文档实体示例一(默认格式,见文档 Api1.docx)

    1.1. Result (响应结果实体)

    字段字段类型说明
    codeString0-代表成功,其它代表失败
    descString操作失败时的说明信息
    dataT返回对应的泛型实体对象

    1.2. PageInfo (页码信息实体)

    字段字段类型说明
    curPageInteger当前页码
    pageSizeInteger页码大小,每一页的记录条数
    totalPageInteger总页数
    hasNextBoolean是否有下一页
    dataList<T>泛型T为对应的数据记录实体
    • 文档实体示例二(自动解析实体名称格式,见文档 Api3.docx)

    1.1. 响应结果实体

    Result
    字段字段类型说明
    codeString0-代表成功,其它代表失败
    descString操作失败时的说明信息
    dataT返回对应的泛型<T>实体对象

    1.2. 页码信息实体

    PageInfo
    字段字段类型说明
    curPageInteger当前页码
    curPageInteger当前页码
    pageSizeInteger页码大小,每一页的记录条数
    totalPageInteger总页数
    hasNextBoolean是否有下一页
    dataList<T>泛型T为对应的数据记录实体

    更多使用详情,请查看Test中的源码使用示例或直接查看API帮助文档

    代码下载:WordPOI.zip

    收起阅读 »

    iOS 中的事件传递和响应机制 - 原理篇

    注:根据史上最详细的iOS之事件的传递和响应机制-原理篇重新整理(适当删减及补充)。在 iOS 中,只有继承了 UIReponder(响应者)类的对象才能接收并处理事件。其公共子类包括 UIView 、UIViewController 和 UIApplicat...
    继续阅读 »

    注:根据史上最详细的iOS之事件的传递和响应机制-原理篇重新整理(适当删减及补充)。

    在 iOS 中,只有继承了 UIReponder(响应者)类的对象才能接收并处理事件。其公共子类包括 UIView 、UIViewController 和 UIApplication 。

    UIReponder 类中提供了以下 4 个对象方法来处理触摸事件:

    /// 触摸开始
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {}
    /// 触摸移动
    override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {}
    /// 触摸取消(在触摸结束之前)
    /// 某个系统事件(例如电话呼入)会打断触摸过程
    override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {}
    /// 触摸结束
    override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {}

    注意:

    如果手指同时触摸屏幕,touches(_:with:) 方法只会调用一次,Set<UITouch> 包含两个对象;

    如果手指前后触摸屏幕,touches(_:with:) 会依次调用,且每次调用时 Set<UITouch> 只有一个对象

    iOS 中的事件传递

    事件传递和响应的整个流程

    触发事件后,系统会将该事件加入到一个由 UIApplication 管理的事件队列中;
    UIApplication 会从事件队列中取出最前面的事件,将之分发出去以便处理,通常,先发送事件给应用程序的主窗口( keyWindow );
    主窗口会在视图层次结构中<u>找到一个最适合的视图</u>来处理触摸事件;
    找到适合的视图控件后,就会调用该视图控件的 touches(_:with:) 方法;
    touches(_:with:) 的默认实现是将事件顺着响应者链(后面会说)一直传递下去,直到连 UIApplication 对象也不能响应事件,则将其丢弃。

    如何寻找最适合的控件来处理事件

    当事件触发后,系统会调用控件的 hitTest(_:with:) 方法来遍历视图的层次结构,以确定哪个子视图应该接收触摸事件,过程如下:

    调用自己的 hitTest(_:with:) 方法;
    判断自己能否触发事件、是否隐藏、alpha <= 0.01;
    调用 point(inside:with:) 来判断触摸点是否在自己身上;
    倒序遍历 subviews ,并重复前面三个步骤。直到找到包含触摸点的最上层视图,并返回这个视图,那么该视图就是那个最适合的处理事件的 view;
    如果没有符合条件的子控件,就认为自己最适合处理事件,也就是自己是最适合的 view;
    通俗一点来解释就是,其实系统也无法决定应该让哪个视图处理事件,那么就用遍历的方式,依次找到包含触摸点所在的最上层视图,则认为该视图最适合处理事件。

    注意:

    触摸事件传递的过程是从父控件传递到子控件的,如果父控件也不能接收事件,那么子控件就不可能接收事件。

    寻找最适合的的 view 的底层剖析

    hitTest(_:with:) 的调用时机

    事件开始产生时会调用;
    只要事件传递给一个控件,就会调用这个控件的 hitTest(_:with:) 方法(不管这个控件能否处理事件或触摸点是否自己身上)。
    hitTest(_:with:) 的作用

    返回一个最适合的 view 来处理触摸事件。

    注意:

    如果 hitTest(_:with:) 方法中返回 nil ,那么该控件本身和其 subview 都不是最适合的 view,而是该控件的父控件。

    在默认的实现中,如果确定最终父控件是最适合的 view,那么仍然会调用其子控件的 hitTest(_:with:) 方法(不然怎么知道有没有更适合的 view?参考 如何寻找最适合的控件来处理事件。)

    hitTest(_:with:) 的默认实现

    override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
    // 1. 判断自己能否触发事件
    if !self.isUserInteractionEnabled || self.isHidden || self.alpha <= 0.01 {
    return nil
    }
    // 2.判断触摸点是否在自己身上
    if !self.point(inside: point, with: event) {
    return nil
    }
    // 3. 倒序遍历 `subviews` ,并重复前面两个步骤;
    // 直到找到包含触摸点的最前面的视图,并返回这个视图,那么该视图就是那个最合适的接收事件的 view;
    for view in subviews.reversed() {
    // 把坐标转换成控件上的坐标
    let p = self.convert(point, to: view)
    if let hitView = view.hitTest(p, with: event) {
    return hitView
    }
    }

    return self
    }

    iOS 中的事件响应

    找到最适合的 view 接收事件后,如果不重写实现该 view 的 touches(_:with:) 方法,那么这些方法的默认实现是将事件顺着响应者链向下传递, 将事件交给下一个响应者去处理。


    可以说,响应者链是由多个响应者对象链接起来的链条。UIReponder 的一个对象属性 next 能够很好的解释这一规则。

    UIReponder().next

    返回响应者链中的下一个响应者,如果没有下一个响应者,则返回 nil 。

    例如,UIView 调用此属性会返回管理它的 UIViewController 对象(如果有),没有则返回它的 superview;UIViewController 调用此属性会返回其视图的 superview;UIWindow 返回应用程序对象;共享的 UIApplication 对象则通常返回 nil 。

    例如,我们可以通过 UIView 的 next 属性找到它所在的控制器:

    extension UIView {
    var next = self.next
    while next != nil { // 符合条件就一直循环
    if let viewController = next as? UIViewController {
    return viewController
    }
    // UIView 的下一个响应控件,直到找到控制器。
    next = next?.next
    }
    return nil
    }

    转自:https://www.jianshu.com/p/024f0c719715

    收起阅读 »

    iOS开发笔记(十)— Xcode、UITabbar、特殊机型问题分析

    前言本文分享iOS开发中遇到的问题,和相关的一些思考。正文一、Xcode10.1 import头文件无法索引【问题表现】如图,当import头文件的时候,索引无效,无法联想出正确的文件;【问题分析】通过多个文件尝试,发现并非完全不能索引头文件,而是只能索引和当...
    继续阅读 »

    前言

    本文分享iOS开发中遇到的问题,和相关的一些思考。

    正文

    一、Xcode10.1 import头文件无法索引
    【问题表现】如图,当import头文件的时候,索引无效,无法联想出正确的文件;


    【问题分析】通过多个文件尝试,发现并非完全不能索引头文件,而是只能索引和当前文件在同级目录的头文件;
    有点猜测是Xcode10.1的原因,但是在升级完的半年多时间里,都没有出现过索引。
    从已有的知识来分析,很可能是Xcode的头文件搜索路径有问题,于是尝试把工程文件下的路径设置递归搜索,结果又出现以下问题:


    【问题解决】在多次尝试无效之后,最终还是靠Google解决该问题。
    如下路径,修改设置
    Xcode --> File --> Workspace Settings --> Build System --> Legacy Build System


    二、NSAssert的断点和symbolic 断点

    【问题表现】NSAssert是常见的断言,可以在debug阶段快速暴露问题,但是在触发的时候无法保持上下文;
    【问题分析】NSAssert的本质就是抛出一个异常,可以通过Xcode添加一个Exception Breakpoint:


    如下,便可以NSAssert触发时捕获现场。


    同理,在Exception Breakpoint,还有Smybolic Breakpoint较为常用。
    以cookie设置接口为例,以下为一段设置cookies的代码
    [[NSHTTPCookieStorage sharedHTTPCookieStorage] setCookies];
    但是有时候设置cookies的地方可能较多,此时可以添加一个Smybolic Breakpoint并设置符号为cookies。
    如下,可以看到所有设置cookies的接口:


    三、.m文件改成.mm文件后编译失败

    【问题表现】Pointer is missing a nullability type specifier (_Nonnull, _Nullable, or _Null_unspecified)
    出错代码行: typedef void(^SSDataCallback)(NSError *error, id obj);
    手动给参数添加 nullable的声明并无法解决。

    【问题分析】
    首先确定的是,这个编译失败实际上是一个warning,只是因为工程设置了把warning识别为error;
    其次.m文件可以正常编译,并且.m文件也是开启了warning as error的设置;而从改成.mm就报错的表现和提示log来看,仍然是因为参数为空的原因导致。

    【问题解决】
    经过对比正常编译的.mm文件,找到一个解决方案:
    1,添加NS_ASSUME_NONNULL_BEGIN在代码最前面,NS_ASSUME_NONNULL_END在代码最后面;
    2、手动添加_Nullable到函数的参数;
    typedef void(^SSDataCallback)(NSError * _Nullable error, id _Nullable obj);

    四、UITabbar疑难杂症

    问题1、batItem的染色异常问题

    【问题表现】添加UITabBarItem到tabbar上,但是图片会被染成蓝色;
    【问题分析】tabbar默认会帮我们染色,所以我们创建的UITabBarItem默认会被tinkColor染色的影响。
    解决办法就是添加参数imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal,这样UITabBarItem的图片变不会受到tinkColor影响。

    UITabBarItem *item1 = [[UITabBarItem alloc] initWithTitle:@"商城" image:[UIImage imageNamed:@"tabbar_item_store"] selectedImage:[[UIImage imageNamed:@"tabbar_item_store_selected"] imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal]];

    问题2、tabbar的背景色问题

    【问题表现】设置tabbar的背景色是0xFFFFFF的白色,但是实际的效果确是灰白色,并不是全白色;
    【问题分析】tabbar默认是透明的(属性translucent),会对tabbar下面的视图进行高斯模糊,然后再与背景色混合。
    【问题解决】
    1、自由做法,addSubview:一个view到tabbar上,接下来自己绘制4个按钮;(可操作性强,缺点是tabbar的逻辑需要自己再实现一遍)
    2、改变tabbar透明度做法,设置translucent=YES,再修改背景色;(引入一个巨大的坑,导致UITabbarViewController上面的子VC的self.view属性高度会变化!)
    3、空白图做法,把背景图都用一张空白的图片替代,如下:(最终采纳的做法)

    self.tabBar.backgroundImage = [[UIImage alloc] init];
    self.tabBar.backgroundColor = [UIColor whiteColor];

    问题3、tabbar顶部的线条问题

    【问题表现】UITabbar默认在tabbar的顶部会有一条灰色的线,但是并没有一个属性可以修改其颜色。
    【问题分析】从Xcode的工具来看,这条线是一个UIImageView:


    再从UITabbar的头文件来看,这条线的图片可能是shadowImage。
    【问题解决】将shadowImage用一张空白的图片替代,然后自己再添加想要的线条大小和颜色。

    self.tabBar.shadowImage = [[UIImage alloc] init];
    UIView *lineView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, self.tabBar.width, 0.5)];
    lineView.backgroundColor = [UIColor colorWithHexString:@"e8e8e8"];
    [self.tabBar addSubview:lineView];

    五、特殊机型出现的异常现象

    1、iOS 11.4 充电时无法正常获取电量

    【问题表现】在某个场景需要获取电池,于是通过以下addObserverForName:UIDeviceBatteryLevelDidChangeNotification的方式监听电量的变化,在iOS 12的机型表现正常,但是在iOS 11.4的机型上会出现无法获取电量的原因。

    void (^block)(NSNotification *notification) = ^(NSNotification *notification) {
    SS_STRONG_SELF(self);
    NSLog(@"%@", self);
    self.batteryView.width = (self.batteryImageView.width - Padding_battery_width) * [UIDevice currentDevice].batteryLevel;
    };
    //监视电池剩余电量
    [[NSNotificationCenter defaultCenter] addObserverForName:UIDeviceBatteryLevelDidChangeNotification
    object:nil
    queue:[NSOperationQueue mainQueue]
    usingBlock:block];

    【问题分析】从电量获取的api开始入手分析,在获取电量之前,需要显式调用接口
    [UIDevice currentDevice].batteryMonitoringEnabled = YES;
    于是点击batteryMonitoringEnabled属性进入UIDevice.h,发现有个batteryState属性,里面有一个状态是充电UIDeviceBatteryStateCharging,但是对问题并无帮助;
    点击UIDeviceBatteryLevelDidChangeNotification发现还有一个通知是UIDeviceBatteryStateDidChangeNotification,猜测可能是充电状态下的回调有所不同;
    【问题解决】最终通过添加新通知的监听解决。该问题并不太难,但是养成多看.h文件相关属性的习惯,还是会有好处。

    [[NSNotificationCenter defaultCenter] addObserverForName:UIDeviceBatteryStateDidChangeNotification
    object:nil
    queue:[NSOperationQueue mainQueue]
    usingBlock:block];

    2、iOS 10.3的UILabel富文本排版异常

    【问题表现】有一段文本的显示需要设置首行缩进,所以用的富文本添加段落属性的方式;但是在iOS 10.3的6p机型上出现异常现象,如下:
    测试文本:contentStr=@"一年佛山电脑放山东难道是防空洞念佛"
    如下,最后的字符没有显示完全。
    实现方式是计算得到富文本,然后赋值给UILabel,再调用-sizeToFit的接口。


    以上的问题仅在一行的时候出现异常,两行又恢复正常。


    【问题分析】
    从表现来看,是sizeToFit的时候宽度结算出错;通过多次尝试,发现是少计算了大概两个空格的距离,也即是首行缩进的距离。
    【问题解决】
    方法1、去除首行缩进,每行增加两个空格;
    方法2、一行的时候,把宽度设置到最大;
    如何判断1行的情况,可以用以下的代码简短判断

    if (self.contentLabel.height < self.contentLabel.font.lineHeight * 2) { // 一行的情况
    self.contentLabel.width = self.width - 40;
    }

    总结

    日常开发遇到的问题,如果解决过程超过10分钟,我都会记录下来。
    这些问题有的很简单,仅仅是改个配置(如第一个Xcode索引问题),但是在解决过程中还是走了一些弯路,因为完全没想过可能会去改Workspace setting,都是在Build setting修改进行尝试。
    还有些问题纯粹是特定现象,比如说特殊机型问题,只是做一个备忘和提醒


    链接:https://www.jianshu.com/p/6c964411fc03

    收起阅读 »

    iOS 任务调度器:为 CPU 和内存减负

    GitHub 地址:YBTaskScheduler支持 cocopods,使用简便,效率不错,一个性能优化的基础组件。前言前些时间有好几个技术朋友问过笔者类似的问题:主线程需要执行大量的任务导致卡顿如何处理?异步任务量级过大导致 CPU 和内存压力过高如何优化...
    继续阅读 »

    GitHub 地址:YBTaskScheduler
    支持 cocopods,使用简便,效率不错,一个性能优化的基础组件。

    前言

    前些时间有好几个技术朋友问过笔者类似的问题:主线程需要执行大量的任务导致卡顿如何处理?异步任务量级过大导致 CPU 和内存压力过高如何优化?

    解决类似的问题可以用几个思路:降频、淘汰、优先级调度。

    本来解决这些问题并不需要很复杂的代码,但是涉及到一些 C 代码并且要注意线程安全的问题,所以笔者就做了这样一个轮子,以解决任务调度引发的性能问题。

    本文讲述 YBTaskScheduler 的原理,读者朋友需要有一定的 iOS 基础,了解一些性能优化的知识,基本用法可以先看看 GitHub README,DEMO 中也有一个相册列表的应用案例。

    一、需求分析

    就拿 DEMO 中的案例来说明,一个显示相册图片的列表:


    实现图中业务,必然考虑到几个耗时操作:

    1、从相册读取图片
    2、解压图片
    3、圆角处理
    4、绘制图片

    理所当然的想到处理方案(DEMO中有实现):

    1、异步读取图片
    2、异步裁剪图片为正方形(这个过程中就解压了)
    3、异步裁剪圆角
    4、回到主线程绘制图片

    一整套流程下来,貌似需求很好的解决了,但是当快速滑动列表时,会发现 CPU 和内存的占用会比较高(这取决于从相册中读取并显示多大的图片)。当然 DEMO 中按照屏幕的物理像素处理,就算不使用任务调度器组件快速滑动列表也基本不会有掉帧的现象。考虑到老旧设备或者技术人员的水平,很多时候这种需求会导致严重的 CPU 和内存负担,甚至导致闪退。

    以上处理方案可能存在的性能瓶颈:

    从相册读取图片、裁剪图片,处理圆角、主线程绘制等操作会导致 CPU 计算压力过大。
    同时解压的图片、同时绘制的图片过多导致内存峰值飙升(更不要说做了图片的缓存)。
    任何一种情况都可能导致客户端卡死或者闪退,结合业务来分析问题,会发现优化的思路还是不难找到:

    · 滑出屏幕的图片不会存在绘制压力,而当前屏幕中的图片会在一个 RunLoop 循环周期绘制,可能造成掉帧。所以可以减少一个 RunLoop 循环周期所绘制的图片数量。
    · 快速滑动列表,大量的异步任务直接交由 CPU 执行,然而滑出屏幕的图片已经没有处理它的意义了。所以可以提前删除掉已经滑出屏幕的异步任务,以此来降低 CPU 和内存压力。

    没错, YBTaskScheduler 组件就是替你做了这些事情 ,而且还不止于此。

    二、命令模式与 RunLoop

    想要管理这些复杂的任务,并且在合适的时机调用它们,自然而然的就想到了命令模式。意味着任务不能直接执行,而是把任务作为一个命令装入容器。

    在 Objective-C 中,显然 Block 代码块能解决延迟执行这个问题:

    [_scheduler addTask:^{
    /*
    具体任务代码
    解压图片、裁剪图片、访问磁盘等
    */
    }];

    然后组件将这些代码块“装起来”,组件由此“掌握”了所有的任务,可以自由的决定何时调用这些代码块,何时对某些代码块进行淘汰,还可以实现优先级调度。

    既然是命令模式,还差一个 Invoker (调用程序),即何时去触发这些任务。结合 iOS 的技术特点,可以监听 RunLoop 循环周期来实现:

    static void addRunLoopObserver() {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
    taskSchedulers = [NSHashTable weakObjectsHashTable];
    CFRunLoopObserverRef observer = CFRunLoopObserverCreate(CFAllocatorGetDefault(), kCFRunLoopBeforeWaiting | kCFRunLoopExit, true, 0xFFFFFF, runLoopObserverCallBack, NULL);
    CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
    CFRelease(observer);
    });
    }

    然后在回调函数中进行任务的调度。

    三、策略模式

    考虑到任务的淘汰策略和优先级调度,必然需要一些高效数据结构来支撑,为了提高处理效率,笔者直接使用了 C++ 的数据结构:deque和priority_queue。

    因为要实现任务淘汰,所以使用deque双端队列来模拟栈和队列,而不是直接使用stack和queue。使用priority_queue优先队列来处理自定义的优先级调度,它的缺点是不能删除低优先级节点,为了节约时间成本姑且够用。

    具体的策略:

    栈:后加入的任务先执行(可以理解为后加入的任务优先级高),优先淘汰先加入的任务。
    队列:先加入的任务先执行(可以理解为先加入的任务优先级高),优先淘汰后加入的任务。
    优先队列:自定义任务优先级,不支持任务淘汰。
    实际上组件是推荐使用栈和队列这两种策略,因为插入和取出的时间复杂度是常数级的,需要定制任务的优先级时才考虑使用优先队列,因为其插入复杂度是 O(logN) 的。

    至此,整个组件的业务是比较清晰了,组件需要让这三种处理方式可以自由的变动,所以采用策略模式来处理,下面是 UML 类图:


    嗯,这是个挺标准的策略模式。

    四、线程安全

    由于任务的调度可能在任意线程,所以必须要做好容器(栈、队列、优先队列)访问的线程安全问题,组件是使用pthread_mutex_t和dispatch_once来保证线程安全,同时笔者尽量减少临界区来提高性能。值得注意的是,如果不会存在线程安全的代码就不要去加锁了。

    后语

    部分技术细节就不多说了,组件代码量比较少,如果感兴趣可以直接看源码。实际上这个组件的应用场景并不是很多,在项目稳定需要做深度的性能优化时可能会比较需要它,并且希望使用它的人也能了解一些原理,做到胸有成竹,才能灵活的运用。

    转自:https://www.jianshu.com/p/f2a610c77d26

    收起阅读 »

    从 LiveData 迁移到 Kotlin 数据流

    LiveData 的历史要追溯到 2017 年。彼时,观察者模式有效简化了开发,但诸如 RxJava 一类的库对新手而言有些太过复杂。为此,架构组件团队打造了 LiveData: 一个专用于 Android 的具备自主生命周期感知能力的可观察的数据存储器类。L...
    继续阅读 »

    LiveData 的历史要追溯到 2017 年。彼时,观察者模式有效简化了开发,但诸如 RxJava 一类的库对新手而言有些太过复杂。为此,架构组件团队打造了 LiveData: 一个专用于 Android 的具备自主生命周期感知能力的可观察的数据存储器类。LiveData 被有意简化设计,这使得开发者很容易上手;而对于较为复杂的交互数据流场景,建议您使用 RxJava,这样两者结合的优势就发挥出来了。


    DeadData?


    LiveData 对于 Java 开发者、初学者或是一些简单场景而言仍是可行的解决方案。而对于一些其他的场景,更好的选择是使用 Kotlin 数据流 (Kotlin Flow)。虽说数据流 (相较 LiveData) 有更陡峭的学习曲线,但由于它是 JetBrains 力挺的 Kotlin 语言的一部分,且 Jetpack Compose 正式版即将发布,故两者配合更能发挥出 Kotlin 数据流中响应式模型的潜力。


    此前一段时间,我们探讨了 如何使用 Kotlin 数据流 来连接您的应用当中除了视图和 View Model 以外的其他部分。而现在我们有了 一种更安全的方式来从 Android 的界面中获得数据流,已经可以创作一份完整的迁移指南了。


    在这篇文章中,您将学到如何把数据流暴露给视图、如何收集数据流,以及如何通过调优来适应不同的需求。


    数据流: 把简单复杂化,又把复杂变简单


    LiveData 就做了一件事并且做得不错: 它在 缓存最新的数据 和感知 Android 中的生命周期的同时将数据暴露了出来。稍后我们会了解到 LiveData 还可以 启动协程创建复杂的数据转换,这可能会需要花点时间。


    接下来我们一起比较 LiveData 和 Kotlin 数据流中相对应的写法吧:


    #1: 使用可变数据存储器暴露一次性操作的结果


    这是一个经典的操作模式,其中您会使用协程的结果来改变状态容器:


    △ 将一次性操作的结果暴露给可变的数据容器 (LiveData)


    △ 将一次性操作的结果暴露给可变的数据容器 (LiveData)


    <!-- Copyright 2020 Google LLC.  
    SPDX-License-Identifier: Apache-2.0 -->

    class MyViewModel {
    private val _myUiState = MutableLiveData<Result<UiState>>(Result.Loading)
    val myUiState: LiveData<Result<UiState>> = _myUiState

    // 从挂起函数和可变状态中加载数据
    init {
    viewModelScope.launch {
    val result = ...
    _myUiState.value = result
    }
    }
    }

    如果要在 Kotlin 数据流中执行相同的操作,我们需要使用 (可变的) StateFlow (状态容器式可观察数据流):


    △ 使用可变数据存储器 (StateFlow) 暴露一次性操作的结果


    △ 使用可变数据存储器 (StateFlow) 暴露一次性操作的结果


    class MyViewModel {
    private val _myUiState = MutableStateFlow<Result<UiState>>(Result.Loading)
    val myUiState: StateFlow<Result<UiState>> = _myUiState

    // 从挂起函数和可变状态中加载数据
    init {
    viewModelScope.launch {
    val result = ...
    _myUiState.value = result
    }
    }
    }

    StateFlowSharedFlow 的一个比较特殊的变种,而 SharedFlow 又是 Kotlin 数据流当中比较特殊的一种类型。StateFlow 与 LiveData 是最接近的,因为:



    • 它始终是有值的。

    • 它的值是唯一的。

    • 它允许被多个观察者共用 (因此是共享的数据流)。

    • 它永远只会把最新的值重现给订阅者,这与活跃观察者的数量是无关的。



    当暴露 UI 的状态给视图时,应该使用 StateFlow。这是一种安全和高效的观察者,专门用于容纳 UI 状态。



    #2: 把一次性操作的结果暴露出来


    这个例子与上面代码片段的效果一致,只是这里暴露协程调用的结果而无需使用可变属性。


    如果使用 LiveData,我们需要使用 LiveData 协程构建器:


    △ 把一次性操作的结果暴露出来 (LiveData)


    △ 把一次性操作的结果暴露出来 (LiveData)


    class MyViewModel(...) : ViewModel() {
    val result: LiveData<Result<UiState>> = liveData {
    emit(Result.Loading)
    emit(repository.fetchItem())
    }
    }

    由于状态容器总是有值的,那么我们就可以通过某种 Result 类来把 UI 状态封装起来,比如加载中、成功、错误等状态。


    与之对应的数据流方式则需要您多做一点配置:


    △ 把一次性操作的结果暴露出来 (StateFlow)


    △ 把一次性操作的结果暴露出来 (StateFlow)


    class MyViewModel(...) : ViewModel() {
    val result: StateFlow<Result<UiState>> = flow {
    emit(repository.fetchItem())
    }.stateIn(
    scope = viewModelScope,
    started = WhileSubscribed(5000), //由于是一次性操作,也可以使用 Lazily
    initialValue = Result.Loading
    )
    }

    stateIn 是专门将数据流转换为 StateFlow 的运算符。由于需要通过更复杂的示例才能更好地解释它,所以这里暂且把这些参数放在一边。


    #3: 带参数的一次性数据加载


    比方说您想要加载一些依赖用户 ID 的数据,而信息来自一个提供数据流的 AuthManager:


    △ 带参数的一次性数据加载 (LiveData)


    △ 带参数的一次性数据加载 (LiveData)


    使用 LiveData 时,您可以用类似这样的代码:


    class MyViewModel(authManager..., repository...) : ViewModel() {
    private val userId: LiveData<String?> =
    authManager.observeUser().map { user -> user.id }.asLiveData()

    val result: LiveData<Result<Item>> = userId.switchMap { newUserId ->
    liveData { emit(repository.fetchItem(newUserId)) }
    }
    }

    switchMap 是数据变换中的一种,它订阅了 userId 的变化,并且其代码体会在感知到 userId 变化时执行。


    如非必须要将 userId 作为 LiveData 使用,那么更好的方案是将流式数据和 Flow 结合,并将最终的结果 (result) 转化为 LiveData。


    class MyViewModel(authManager..., repository...) : ViewModel() {
    private val userId: Flow<UserId> = authManager.observeUser().map { user -> user.id }

    val result: LiveData<Result<Item>> = userId.mapLatest { newUserId ->
    repository.fetchItem(newUserId)
    }.asLiveData()
    }

    如果改用 Kotlin Flow 来编写,代码其实似曾相识:


    △ 带参数的一次性数据加载 (StateFlow)


    △ 带参数的一次性数据加载 (StateFlow)


    class MyViewModel(authManager..., repository...) : ViewModel() {
    private val userId: Flow<UserId> = authManager.observeUser().map { user -> user.id }

    val result: StateFlow<Result<Item>> = userId.mapLatest { newUserId ->
    repository.fetchItem(newUserId)
    }.stateIn(
    scope = viewModelScope,
    started = WhileSubscribed(5000),
    initialValue = Result.Loading
    )
    }

    假如说您想要更高的灵活性,可以考虑显式调用 transformLatest 和 emit 方法:


    val result = userId.transformLatest { newUserId ->
    emit(Result.LoadingData)
    emit(repository.fetchItem(newUserId))
    }.stateIn(
    scope = viewModelScope,
    started = WhileSubscribed(5000),
    initialValue = Result.LoadingUser //注意此处不同的加载状态
    )

    #4: 观察带参数的数据流


    接下来我们让刚才的案例变得更具交互性。数据不再被读取,而是被观察,因此我们对数据源的改动会直接被传递到 UI 界面中。


    继续刚才的例子: 我们不再对源数据调用 fetchItem 方法,而是通过假定的 observeItem 方法获取一个 Kotlin 数据流。


    若使用 LiveData,可以将数据流转换为 LiveData 实例,然后通过 emitSource 传递数据的变化。


    △ 观察带参数的数据流 (LiveData)


    △ 观察带参数的数据流 (LiveData)


    class MyViewModel(authManager..., repository...) : ViewModel() {
    private val userId: LiveData<String?> =
    authManager.observeUser().map { user -> user.id }.asLiveData()

    val result = userId.switchMap { newUserId ->
    repository.observeItem(newUserId).asLiveData()
    }
    }

    或者采用更推荐的方式,把两个流通过 flatMapLatest 结合起来,并且仅将最后的输出转换为 LiveData:


    class MyViewModel(authManager..., repository...) : ViewModel() {
    private val userId: Flow<String?> =
    authManager.observeUser().map { user -> user?.id }

    val result: LiveData<Result<Item>> = userId.flatMapLatest { newUserId ->
    repository.observeItem(newUserId)
    }.asLiveData()
    }

    使用 Kotlin 数据流的实现方式非常相似,但是省下了 LiveData 的转换过程:


    △ 观察带参数的数据流 (StateFlow)


    △ 观察带参数的数据流 (StateFlow)


    class MyViewModel(authManager..., repository...) : ViewModel() {
    private val userId: Flow<String?> =
    authManager.observeUser().map { user -> user?.id }

    val result: StateFlow<Result<Item>> = userId.flatMapLatest { newUserId ->
    repository.observeItem(newUserId)
    }.stateIn(
    scope = viewModelScope,
    started = WhileSubscribed(5000),
    initialValue = Result.LoadingUser
    )
    }

    每当用户实例变化,或者是存储区 (repository) 中用户的数据发生变化时,上面代码中暴露出来的 StateFlow 都会收到相应的更新信息。


    #5: 结合多种源: MediatorLiveData -> Flow.combine


    MediatorLiveData 允许您观察一个或多个数据源的变化情况,并根据得到的新数据进行相应的操作。通常可以按照下面的方式更新 MediatorLiveData 的值:


    val liveData1: LiveData<Int> = ...
    val liveData2: LiveData<Int> = ...

    val result = MediatorLiveData<Int>()

    result.addSource(liveData1) { value ->
    result.setValue(liveData1.value ?: 0 + (liveData2.value ?: 0))
    }
    result.addSource(liveData2) { value ->
    result.setValue(liveData1.value ?: 0 + (liveData2.value ?: 0))
    }

    同样的功能使用 Kotlin 数据流来操作会更加直接:


    val flow1: Flow<Int> = ...
    val flow2: Flow<Int> = ...

    val result = combine(flow1, flow2) { a, b -> a + b }

    此处也可以使用 combineTransform 或者 zip 函数。


    通过 stateIn 配置对外暴露的 StateFlow


    早前我们使用 stateIn 中间运算符来把普通的流转换成 StateFlow,但转换之后还需要一些配置工作。如果现在不想了解太多细节,只是想知道怎么用,那么可以使用下面的推荐配置:


    val result: StateFlow<Result<UiState>> = someFlow
    .stateIn(
    scope = viewModelScope,
    started = WhileSubscribed(5000),
    initialValue = Result.Loading
    )

    不过,如果您想知道为什么会使用这个看似随机的 5 秒的 started 参数,请继续往下读。


    根据文档,stateIn 有三个参数:?


    @param scope 共享开始时所在的协程作用域范围

    @param started 控制共享的开始和结束的策略

    @param initialValue 状态流的初始值

    当使用 [SharingStarted.WhileSubscribed] 并带有 `replayExpirationMillis` 参数重置状态流时,也会用到 initialValue。

    started 接受以下的三个值:



    • Lazily: 当首个订阅者出现时开始,在 scope 指定的作用域被结束时终止。

    • Eagerly: 立即开始,而在 scope 指定的作用域被结束时终止。

    • WhileSubscribed: 这种情况有些复杂 (后文详聊)。


    对于那些只执行一次的操作,您可以使用 Lazily 或者 Eagerly。然而,如果您需要观察其他的流,就应该使用 WhileSubscribed 来实现细微但又重要的优化工作,参见后文的解答。


    WhileSubscribed 策略


    WhileSubscribed 策略会在没有收集器的情况下取消上游数据流。通过 stateIn 运算符创建的 StateFlow 会把数据暴露给视图 (View),同时也会观察来自其他层级或者是上游应用的数据流。让这些流持续活跃可能会引起不必要的资源浪费,例如一直通过从数据库连接、硬件传感器中读取数据等等。当您的应用转而在后台运行时,您应当保持克制并中止这些协程


    WhileSubscribed 接受两个参数:


    public fun WhileSubscribed(
    stopTimeoutMillis: Long = 0,
    replayExpirationMillis: Long = Long.MAX_VALUE
    )


    超时停止


    根据其文档:



    stopTimeoutMillis 控制一个以毫秒为单位的延迟值,指的是最后一个订阅者结束订阅与停止上游流的时间差。默认值是 0 (立即停止)。



    这个值非常有用,因为您可能并不想因为视图有几秒钟不再监听就结束上游流。这种情况非常常见——比如当用户旋转设备时,原来的视图会先被销毁,然后数秒钟内重建。


    liveData 协程构建器所使用的方法是 添加一个 5 秒钟的延迟,即如果等待 5 秒后仍然没有订阅者存在就终止协程。前文代码中的 WhileSubscribed (5000) 正是实现这样的功能:


    class MyViewModel(...) : ViewModel() {
    val result = userId.mapLatest { newUserId ->
    repository.observeItem(newUserId)
    }.stateIn(
    scope = viewModelScope,
    started = WhileSubscribed(5000),
    initialValue = Result.Loading
    )
    }

    这种方法会在以下场景得到体现:



    • 用户将您的应用转至后台运行,5 秒钟后所有来自其他层的数据更新会停止,这样可以节省电量。

    • 最新的数据仍然会被缓存,所以当用户切换回应用时,视图立即就可以得到数据进行渲染。

    • 订阅将被重启,新数据会填充进来,当数据可用时更新视图。


    数据重现的过期时间


    如果用户离开应用太久,此时您不想让用户看到陈旧的数据,并且希望显示数据正在加载中,那么就应该在 WhileSubscribed 策略中使用 replayExpirationMillis 参数。在这种情况下此参数非常适合,由于缓存的数据都恢复成了 stateIn 中定义的初始值,因此可以有效节省内存。虽然用户切回应用时可能没那么快显示有效数据,但至少不会把过期的信息显示出来。



    replayExpirationMillis 配置了以毫秒为单位的延迟时间,定义了从停止共享协程到重置缓存 (恢复到 stateIn 运算符中定义的初始值 initialValue) 所需要等待的时间。它的默认值是长整型的最大值 Long.MAX_VALUE (表示永远不将其重置)。如果设置为 0,可以在符合条件时立即重置缓存的数据。



    从视图中观察 StateFlow


    我们此前已经谈到,ViewModel 中的 StateFlow 需要知道它们已经不再需要监听。然而,当所有的这些内容都与生命周期 (lifecycle) 结合起来,事情就没那么简单了。


    要收集一个数据流,就需要用到协程。Activity 和 Fragment 提供了若干协程构建器:



    • Activity.lifecycleScope.launch : 立即启动协程,并且在本 Activity 销毁时结束协程。

    • Fragment.lifecycleScope.launch : 立即启动协程,并且在本 Fragment 销毁时结束协程。

    • Fragment.viewLifecycleOwner.lifecycleScope.launch : 立即启动协程,并且在本 Fragment 中的视图生命周期结束时取消协程。


    LaunchWhenStarted 和 LaunchWhenResumed


    对于一个状态 X,有专门的 launch 方法称为 launchWhenX。它会在 lifecycleOwner 进入 X 状态之前一直等待,又在离开 X 状态时挂起协程。对此,需要注意对应的协程只有在它们的生命周期所有者被销毁时才会被取消


    △ 使用 launch/launchWhenX 来收集数据流是不安全的


    △ 使用 launch/launchWhenX 来收集数据流是不安全的


    当应用在后台运行时接收数据更新可能会引起应用崩溃,但这种情况可以通过将视图的数据流收集操作挂起来解决。然而,上游数据流会在应用后台运行期间保持活跃,因此可能浪费一定的资源。


    这么说来,目前我们对 StateFlow 所进行的配置都是无用功;不过,现在有了一个新的 API。


    lifecycle.repeatOnLifecycle 前来救场


    这个新的协程构建器 (自 lifecycle-runtime-ktx 2.4.0-alpha01 后可用) 恰好能满足我们的需要: 在某个特定的状态满足时启动协程,并且在生命周期所有者退出该状态时停止协程。


    △ 不同数据流收集方法的比较


    △ 不同数据流收集方法的比较


    比如在某个 Fragment 的代码中:


    onCreateView(...) {
    viewLifecycleOwner.lifecycleScope.launch {
    viewLifecycleOwner.lifecycle.repeatOnLifecycle(STARTED) {
    myViewModel.myUiState.collect { ... }
    }
    }
    }

    当这个 Fragment 处于 STARTED 状态时会开始收集流,并且在 RESUMED 状态时保持收集,最终在 Fragment 进入 STOPPED 状态时结束收集过程。如需获取更多信息,请参阅: 使用更为安全的方式收集 Android UI 数据流


    结合使用 repeatOnLifecycle API 和上面的 StateFlow 示例可以帮助您的应用妥善利用设备资源的同时,发挥最佳性能。


    △ 该 StateFlow 通过 WhileSubscribed(5000) 暴露并通过 repeatOnLifecycle(STARTED) 收集


    △ 该 StateFlow 通过 WhileSubscribed(5000) 暴露并通过 repeatOnLifecycle(STARTED) 收集



    注意: 近期在 Data Binding 中加入的 StateFlow 支持 使用了 launchWhenCreated 来描述收集数据更新,并且它会在进入稳定版后转而使用 repeatOnLifecyle


    对于数据绑定,您应该在各处都使用 Kotlin 数据流并简单地加上 asLiveData() 来把数据暴露给视图。数据绑定会在 lifecycle-runtime-ktx 2.4.0 进入稳定版后更新。



    总结


    通过 ViewModel 暴露数据,并在视图中获取的最佳方式是:



    • ?? 使用带超时参数的 WhileSubscribed 策略暴露 StateFlow。[示例 1]

    • ?? 使用 repeatOnLifecycle 来收集数据更新。[示例 2]


    如果采用其他方式,上游数据流会被一直保持活跃,导致资源浪费:



    • ? 通过 WhileSubscribed 暴露 StateFlow,然后在 lifecycleScope.launch/launchWhenX 中收集数据更新。

    • ? 通过 Lazily/Eagerly 策略暴露 StateFlow,并在 repeatOnLifecycle 中收集数据更新。


    当然,如果您并不需要使用到 Kotlin 数据流的强大功能,就用 LiveData 好了 :)


    ManuelWojtekYigit、Alex Cook、FlorinaChris 致谢!




    作者:Android_开发者
    链接:https://juejin.cn/post/6979008878029570055
    来源:掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    收起阅读 »

    okhttp文件上传失败,居然是Android Studio背锅?太难了~

    1、前言 本案例是我本人遇到的真实案例,因查找原因的过程一度让我崩溃,我相信不少人也遇到过相同的问题,故将其记录下来,希望对大家有帮助,本案例使用RxHttp 2.6.4 + OkHttp 4.9.1版本,当然,如果你使用Retrofit等其它基于OkHtt...
    继续阅读 »

    1、前言


    本案例是我本人遇到的真实案例,因查找原因的过程一度让我崩溃,我相信不少人也遇到过相同的问题,故将其记录下来,希望对大家有帮助,本案例使用RxHttp 2.6.4 + OkHttp 4.9.1版本,当然,如果你使用Retrofit等其它基于OkHttp封装的框架,且用到监听上传进度功能,那么很大概率你也会遇到这个问题,请耐心看完,如果你想直接看到结果,划到文章末尾即可。


    2、问题描述


    事情是这样的,有一段文件上传的代码,如下:


    fun uploadFiles(fileList: List<File>) {
    RxHttp.postForm("/server/...")
    .add("key", "value")
    .addFiles("files", fileList)
    .upload {
    //上传进度回调
    }
    .asString()
    .subscribe({
    //成功回调
    }, {
    //失败回调
    })
    }

    这段代码在写完后很长一段时间内都是ok的,突然有一天,执行这段代码居然报错了,日志如下:


    image.png 这个异常是100%出现的,很熟悉的异常,具体原因就是,数据流被关闭了,但依然往里面写数据,来看看最后抛异常的地方,如下:


    image.png 可以看到,方法里面第一行代码就判断数据流是否已关闭,是的话,抛出异常。


    注:如果你是RxHttp使用者,正在尝试这段代码,发现没问题,也不要惊讶,因为这需要在Android Studio特定场景下执行才会出现,而且是相对高频使用的场景,请待我一步步揭晓答案


    3、一探究竟


    本着出现问题,先定位到自己代码的原则,打开ProgressRequestBody类76行看看,如下:


    public class ProgressRequestBody extends RequestBody {

    //省略相关代码
    private BufferedSink bufferedSink;
    @Override
    public void writeTo(BufferedSink sink) throws IOException {
    if (bufferedSink == null) {
    bufferedSink = Okio.buffer(sink(sink));
    }
    requestBody.writeTo(bufferedSink); //这里是76行
    bufferedSink.flush();
    }
    }

    ProgressRequestBody继承了okhttp3.RequestBody类,作用是监听上传进度;显然最后执行到这里时,数据流已经被关闭了,从日志里可以看到,最后一次调用ProgressRequestBody#writeTo(BufferedSink)方法的地方在CallServerInterceptor拦截器的59行,打开看看


    class CallServerInterceptor(private val forWebSocket: Boolean) : Interceptor {

    //省略相关代码
    @Throws(IOException::class)
    override fun intercept(chain: Interceptor.Chain): Response {
    //省略相关代码
    if (responseBuilder == null) {
    if (requestBody.isDuplex()) {
    exchange.flushRequest()
    val bufferedRequestBody = exchange.createRequestBody(request, true).buffer()
    requestBody.writeTo(bufferedRequestBody)
    } else {
    val bufferedRequestBody = exchange.createRequestBody(request, false).buffer()
    requestBody.writeTo(bufferedRequestBody) //这里是59行
    bufferedRequestBody.close() //数据写完,将数据流关闭
    }
    }
    }
    }

    熟悉OkHttp原理的同学应该知道,CallServerInterceptor拦截器是okhttp拦截器链的最后一个拦截器,将客户端数据写出到服务端,就是在这里实现的,也就是59行,那问题就来了,数据都还没写出去,数据流怎么就关闭了呢?这令我百思不得其解,毫无头绪。


    于是乎,我做了很多无用功,如:重新检查代码,看看是否有手动关闭数据流的地方,显然没有找到;接着,实在没有办法,代码回滚,回滚到最初写这段代码的版本,我满怀期待的以为,这下应该没问题了,可尝试过后,依旧报java.lang.IllegalStateException: closed,成年人的崩溃就在这一瞬间,我陷入了绝境,已经消耗5个小时在这个问题上,此时已晚上23:30,看来又是一个不眠夜。


    question1.jpeg


    习惯告诉我,一个问题很久没查出来,可以先放弃,好吧,拔手机关电脑,洗澡睡觉。


    半小时后,我躺在床上,很难受,于是我拿出手机,打开app,再试了试上传功能,惊奇的发现,可以了,上传成功了,这。。。。一脸懵逼,我找谁说理去,虽然没问题了,但问题没找到,作为一名初级程序员,这我无法接受。


    精神的力量把我从床上扶了起来,再次打开电脑,连上手机,这次,果然有了新的收获,也一下子刷新了我的世界观;当我再次打开app,尝试上传文件时,一样的错误出现在我眼前,What??? 刚才还好好的,连上电脑就不行了?


    question2.jpeg


    ok,我彻底没脾气了,拔掉手机,重启app,再试,没问题了,再次连上电脑,再试,问题又出来了。。


    此时,我的心态有了些许的好转,毕竟有了新的调查方向,我再次查看错误日志,发现了一个很奇怪的地方,如下: image.png


    com.android.tools.profiler.agent.okhttp.OkHttp3Interceptor是从哪冒出来的?在我的认知里,OkHttp3是没有这个拦截器的,为了验证我的认知,再次查看okhttp3源码,如下:


    image.png


    确定是没有添加这个拦截器的,仔细看日志发现,OkHttp3InterceptorCallServerInterceptor、ConnectInterceptor之间执行的,那就只有一个解释,OkHttp3Interceptor是通过addNetworkInterceptor方法添加,现在就好办了,全局搜索addNetworkInterceptor就知道是谁添加的,哪里添加的,很可惜,未找到调用此方法的源码,似乎又陷入了绝境。


    question.jpeg


    那就只能开启调试,看看OkHttp3Interceptor是否在OkHttpClient对象的networkInterceptors网络拦截器列表里,一调试,果然有发现,如下:


    image.png 调试点击下一步,神奇的事情就发生了,如下:


    image.png


    这怎么解释?networkInterceptors.size始终是0,interceptors.size是如何加1变为5的?再来看看,加的1是什么,如下:


    image.png


    很熟悉,就是我们之前提到的OkHttp3Interceptor,这是如何做到的?只有一个解释,OkHttpClient#networkInterceptors()方法被字节码插桩技术插入了新的代码,为了验证我的想法,我做了以下实验:


    image.png


    image.png


    可以看到,我直接new了一个OkHttpClient对象,啥也没配置,调用networkInterceptors()方法,就获取了OkHttp3Interceptor拦截器,但OkHttpClient对象里的networkInterceptors列表中是没有这个拦截器的,这就证实了我的想法。


    那现在的问题就是,OkHttp3Interceptor是谁注入的?跟文件上传失败是否有直接的关系?


    OkHttp3Interceptor是谁注入的?


    先来探索第一个问题,通过OkHttp3Interceptor类的包名class com.android.tools.profiler.agent.okhttp,我有以下3点猜测



    • 包名有com.android.tools,应该跟 Android 官方有关系


    • 包名有agent,又是拦截器,应该跟网络代理,也就是网络监控有关


    • 最后一点,也是最重要的,包名有profiler,这让我联想到了Android Studio(以下简称AS)里Profiler网络分析器



    果然,在Google的源码中,真找到了OkHttp3Interceptor类,看看相关代码:


    public final class OkHttp3Interceptor implements Interceptor {

    //省略相关代码
    @Override
    public Response intercept(Interceptor.Chain chain) throws IOException {
    Request request = chain.request();
    HttpConnectionTracker tracker = null;
    try {
    tracker = trackRequest(request); //1、追踪请求体
    } catch (Exception ex) {
    StudioLog.e("Could not track an OkHttp3 request", ex);
    }
    Response response;
    try {
    response = chain.proceed(request);
    } catch (IOException ex) {

    }
    try {
    if (tracker != null) {
    response = trackResponse(tracker, response); //2、追踪响应体
    }
    } catch (Exception ex) {
    StudioLog.e("Could not track an OkHttp3 response", ex);
    }
    return response;
    }

    可以确定它就是一个网络监控器,但它是不是AS的网络监听器,我却还持怀疑态度,因为我这个项目没开启Profiler分析器,但我最近在开发room数据库相关功能,开启了数据分析器Database Inspector,难道跟这个有关?我尝试关掉Database Inspector,并且重启app,再次尝试文件上传,居然成功了,是真的成功了,你能信?我也不信,于是,再次开启Database Inspector,再次尝试文件上传,失败了,异常跟之前的一模一样;接着,我关闭Database Inspector,并且打开Profiler分析器,再次尝试文件上传,一样失败了。


    我想到这里,基本可以认定OkHttp3Interceptor就是Profiler里面的网络监控器,但也好像缺乏直接证据,于是,我尝试改了下ProgressRequestBody类,如下:


    public class ProgressRequestBody extends RequestBody {

    //省略相关代码
    private BufferedSink bufferedSink;

    @Override
    public void writeTo(BufferedSink sink) throws IOException {
    //如果调用方是OkHttp3Interceptor,不写请求体,直接返回
    if (sink.toString().contains(
    "com.android.tools.profiler.support.network.HttpTracker$OutputStreamTracker"))
    return;
    if (bufferedSink == null) {
    bufferedSink = Okio.buffer(sink(sink));
    }
    requestBody.writeTo(bufferedSink);
    bufferedSink.flush();
    }
    }

    以上代码,仅仅加了一句if语句,这条语句可以判断当前调用方是不是OkHttp3Interceptor,是的话,不写请求体,直接返回;如果OkHttp3Interceptor就是Profiler里的网络监控器,那么此时Profiler里应该是看不到请求体的,也就是看不到请求参数,如下:


    image.png


    可以看到,Profiler里的网络监控器,没有监控到请求参数。


    这就证实了OkHttp3Interceptor的确是Profiler里的网络监控器,也就是AS动态注入的。


    OkHttp3Interceptor 与文件上传是否有直接的关系?


    通过上面的案例分析,显然是有直接关系的,当你未打开Database InspectorProfiler时,文件上传一切正常。


    OkHttp3Interceptor是如何影响文件上传的?


    回到正题,OkHttp3Interceptor是如何影响文件上传的?这个就需要继续分析OkHttp3Interceptor的源码,来看看追踪请求体的代码:


    public final class OkHttp3Interceptor implements Interceptor {

    private HttpConnectionTracker trackRequest(Request request) throws IOException {
    StackTraceElement[] callstack =
    OkHttpUtils.getCallstack(request.getClass().getPackage().getName());
    HttpConnectionTracker tracker =
    HttpTracker.trackConnection(request.url().toString(), callstack);
    tracker.trackRequest(request.method(), toMultimap(request.headers()));
    if (request.body() != null) {
    OutputStream outputStream =
    tracker.trackRequestBody(OkHttpUtils.createNullOutputStream());
    BufferedSink bufferedSink = Okio.buffer(Okio.sink(outputStream));
    request.body().writeTo(bufferedSink); // 1、将请求体写入到BufferedSink中
    bufferedSink.close(); // 2、关闭BufferedSink
    }
    return tracker;
    }

    }

    想到这里问题就很清楚了,上面备注的第一代码中request.body(),拿到的就是ProgressRequestBody对象,随后调用其writeTo(BufferedSink)方法,传入BufferedSink对象,方法执行完,就将BufferedSink对象关闭了,然而,ProgressRequestBody里却将BufferedSink声明为成员变量,并且为空时才会赋值,这就导致后续CallServerInterceptor调用其writeTo(BufferedSink)方法时,使用的还是上一个已关闭的BufferedSink对象,此时再往里面写数据,自然就java.lang.IllegalStateException: closed异常了。


    4、如何解决


    知道了具体的原因,就好解决,将ProgressRequestBody里面的BufferedSink对象改为局部变量即可,如下:


    public class ProgressRequestBody extends RequestBody {

    //省略相关代码
    @Override
    public void writeTo(BufferedSink sink) throws IOException {
    BufferedSink bufferedSink = Okio.buffer(sink(sink));
    requestBody.writeTo(bufferedSink);
    bufferedSink.colse();
    }
    }

    改完后,开启Profiler里的网络监控器,再次尝试文件上传,ok成功了,但又有一个新的问题,ProgressRequestBody是用于监听上传进度的,OkHttp3InterceptorCallServerInterceptor先后调用了其writeTo(BufferedSink)方法,这就会导致请求体写两次,也就是进度监听会收到两遍,而我们真正需要的是CallServerInterceptor调用的那次,咋整?好办,我们前面就判断过调用方是否OkHttp3Interceptor


    于是,做出如下更改:


    public class ProgressRequestBody extends RequestBody {

    //省略相关代码
    @Override
    public void writeTo(BufferedSink sink) throws IOException {
    //如果调用方是OkHttp3Interceptor,直接写请求体,不再通过包装类来处理请求进度
    if (sink.toString().contains(
    "com.android.tools.profiler.support.network.HttpTracker$OutputStreamTracker")) {
    requestBody.writeTo(bufferedSink);
    } else {
    BufferedSink bufferedSink = Okio.buffer(sink(sink));
    requestBody.writeTo(bufferedSink);
    bufferedSink.colse();
    }
    }
    }

    你以为这样就完了?相信很多人都会用到com.squareup.okhttp3:logging-interceptor日志拦截器,当你添加该日志拦截器后,再次上传文件,会发现,进度回调又执行了两遍,为啥?因为该日志拦截器,也会调用ProgressRequestBody#writeTo(BufferedSink)方法,看看代码:


    //省略部分代码
    class HttpLoggingInterceptor @JvmOverloads constructor(
    private val logger: Logger = Logger.DEFAULT
    ) : Interceptor {

    @Throws(IOException::class)
    override fun intercept(chain: Interceptor.Chain): Response {
    val request = chain.request()
    val requestBody = request.body

    if (logHeaders) {
    if (!logBody || requestBody == null) {
    logger.log("--> END ${request.method}")
    } else if (bodyHasUnknownEncoding(request.headers)) {
    logger.log("--> END ${request.method} (encoded body omitted)")
    } else if (requestBody.isDuplex()) {
    logger.log("--> END ${request.method} (duplex request body omitted)")
    } else if (requestBody.isOneShot()) {
    logger.log("--> END ${request.method} (one-shot body omitted)")
    } else {
    val buffer = Buffer()
    //1、这里调用了RequestBody的writeTo方法,并传入了Buffer对象
    requestBody.writeTo(buffer)
    }
    }

    val response: Response
    try {
    response = chain.proceed(request)
    } catch (e: Exception) {
    throw e
    }
    return response
    }

    }

    可以看到,HttpLoggingInterceptor内部也会调用RequestBody#writeTo方法,并传入Buffer对象,到这,我们就好办了,在ProgressRequestBody类增加一个Buffer的判断逻辑即可,如下:


    public class ProgressRequestBody extends RequestBody {

    //省略相关代码
    @Override
    public void writeTo(BufferedSink sink) throws IOException {
    //如果调用方是OkHttp3Interceptor,或者传入的是Buffer对象,直接写请求体,不再通过包装类来处理请求进度
    if (sink instanceof Buffer
    || sink.toString().contains(
    "com.android.tools.profiler.support.network.HttpTracker$OutputStreamTracker")) {
    requestBody.writeTo(bufferedSink);
    } else {
    BufferedSink bufferedSink = Okio.buffer(sink(sink));
    requestBody.writeTo(bufferedSink);
    bufferedSink.colse();
    }
    }
    }

    这样就完了?也不见得,如果后续又遇到什么拦截器调用其writeTo方法,还是会出现进度回调执行两遍的情况,只能在遇到这种情况时,加入对应的判断逻辑


    到这,也许有人会问,为啥不直接判断调用方是不是CallServerInterceptor,是的话监听进度回调,否则,直接写入请求体。想法很好,也是可行的,如下:


    public class ProgressRequestBody extends RequestBody {

    //省略相关代码
    @Override
    public void writeTo(BufferedSink sink) throws IOException {
    //如果调用方是CallServerInterceptor,监听上传进度
    if (sink.toString().contains("RequestBodySink(okhttp3.internal")) {
    BufferedSink bufferedSink = Okio.buffer(sink(sink));
    requestBody.writeTo(bufferedSink);
    bufferedSink.colse();
    } else {
    requestBody.writeTo(bufferedSink);
    }
    }
    }

    但是该方案有个致命的缺陷,如果okhttp未来版本更改了目录结构,ProgressRequestBody类就完全失效。


    两个方案就由大家自己去选择,这里给出ProgressRequestBody完整源码,需要自取


    5、小结


    本案例上传失败的直接原因就是在AS开启了Database Inspector数据库分析器或Profiler网络监控器时,AS就会通过字节码插桩技术,对OkHttpClient#networkInterceptors()方法注入新的字节码,使其多返回一个com.android.tools.profiler.agent.okhttp.OkHttp3Interceptor拦截器(用于监听网络),该拦截器会调用ProgressRequestBody#writeTo(BufferedSink)方法,并传入BufferedSink对象,writeTo方法执行完毕后,立即将BufferedSink对象关闭,在随后的CallServerInterceptor拦截又调用ProgressRequestBody#writeTo(BufferedSink)方法往已关闭的BufferedSink对象写数据,最终导致java.lang.IllegalStateException: closed异常。


    但有个有疑惑,我却未找到答案,那就是为啥开启Database Inspector也会导致AS去监听网络?有知道的小伙伴可以评论区留言。




    作者:不怕天黑
    链接:https://juejin.cn/post/6981210499815833631
    来源:掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    收起阅读 »

    面试必备:Kotlin线程同步的N种方法

    面试的时候经常会被问及多线程同步的问题,例如: “ 现有 Task1、Task2 等多个并行任务,如何等待全部执行完成后,执行 Task3。” 在 Kotlin 中我们有多种实现方式,本文将所有这些方式做了整理,建议收藏。 1. Thread.join ...
    继续阅读 »

    面试的时候经常会被问及多线程同步的问题,例如:


    “ 现有 Task1、Task2 等多个并行任务,如何等待全部执行完成后,执行 Task3。”


    在 Kotlin 中我们有多种实现方式,本文将所有这些方式做了整理,建议收藏。


    1. Thread.join
    2. Synchronized
    3. ReentrantLock
    4. BlockingQueue
    5. CountDownLatch
    6. CyclicBarrier
    7. CAS
    8. Future
    9. CompletableFuture
    10. Rxjava
    11. Coroutine
    12. Flow


    我们先定义三个Task,模拟上述场景, Task3 基于 Task1、Task2 返回的结果拼接字符串,每个 Task 通过 sleep 模拟耗时: image.png


    val task1: () -> String = {
    sleep(2000)
    "Hello".also { println("task1 finished: $it") }
    }

    val task2: () -> String = {
    sleep(2000)
    "World".also { println("task2 finished: $it") }
    }

    val task3: (String, String) -> String = { p1, p2 ->
    sleep(2000)
    "$p1 $p2".also { println("task3 finished: $it") }
    }



    1. Thread.join()


    Kotlin 兼容 Java,Java 的所有线程工具默认都可以使用。其中最简单的线程同步方式就是使用 Threadjoin()


    @Test
    fun test_join() {
    lateinit var s1: String
    lateinit var s2: String

    val t1 = Thread { s1 = task1() }
    val t2 = Thread { s2 = task2() }
    t1.start()
    t2.start()

    t1.join()
    t2.join()

    task3(s1, s2)

    }



    2. Synchronized


    使用 synchronized 锁进行同步


    	@Test
    fun test_synchrnoized() {
    lateinit var s1: String
    lateinit var s2: String

    Thread {
    synchronized(Unit) {
    s1 = task1()
    }
    }.start()
    s2 = task2()

    synchronized(Unit) {
    task3(s1, s2)
    }

    }

    但是如果超过三个任务,使用 synchrnoized 这种写法就比较别扭了,为了同步多个并行任务的结果需要声明n个锁,并嵌套n个 synchronized




    3. ReentrantLock


    ReentrantLock 是 JUC 提供的线程锁,可以替换 synchronized 的使用


    	@Test
    fun test_ReentrantLock() {

    lateinit var s1: String
    lateinit var s2: String

    val lock = ReentrantLock()
    Thread {
    lock.lock()
    s1 = task1()
    lock.unlock()
    }.start()
    s2 = task2()

    lock.lock()
    task3(s1, s2)
    lock.unlock()

    }

    ReentrantLock 的好处是,当有多个并行任务时是不会出现嵌套 synchrnoized 的问题,但仍然需要创建多个 lock 管理不同的任务,


    4. BlockingQueue


    阻塞队列内部也是通过 Lock 实现的,所以也可以达到同步锁的效果


    	@Test
    fun test_blockingQueue() {

    lateinit var s1: String
    lateinit var s2: String

    val queue = SynchronousQueue<Unit>()

    Thread {
    s1 = task1()
    queue.put(Unit)
    }.start()

    s2 = task2()

    queue.take()
    task3(s1, s2)
    }

    当然,阻塞队列更多是使用在生产/消费场景中的同步。




    5. CountDownLatch


    JUC 中的锁大都基于 AQS 实现的,可以分为独享锁和共享锁。ReentrantLock 就是一种独享锁。相比之下,共享锁更适合本场景。 例如 CountDownLatch,它可以让一个线程一直处于阻塞状态,直到其他线程的执行全部完成:


    	@Test
    fun test_countdownlatch() {

    lateinit var s1: String
    lateinit var s2: String
    val cd = CountDownLatch(2)
    Thread() {
    s1 = task1()
    cd.countDown()
    }.start()

    Thread() {
    s2 = task2()
    cd.countDown()
    }.start()

    cd.await()
    task3(s1, s2)
    }

    共享锁的好处是不必为了每个任务都创建单独的锁,即使再多并行任务写起来也很轻松




    6. CyclicBarrier


    CyclicBarrier 是 JUC 提供的另一种共享锁机制,它可以让一组线程到达一个同步点后再一起继续运行,其中任意一个线程未达到同步点,其他已到达的线程均会被阻塞。


    CountDownLatch 的区别在于 CountDownLatch 是一次性的,而 CyclicBarrier 可以被重置后重复使用,这也正是 Cyclic 的命名由来,可以循环使用


    	@Test
    fun test_CyclicBarrier() {

    lateinit var s1: String
    lateinit var s2: String
    val cb = CyclicBarrier(3)

    Thread {
    s1 = task1()
    cb.await()
    }.start()

    Thread() {
    s2 = task1()
    cb.await()
    }.start()

    cb.await()
    task3(s1, s2)

    }



    7. CAS


    AQS 内部通过自旋锁实现同步,自旋锁的本质是利用 CompareAndSwap 避免线程阻塞的开销。 因此,我们可以使用基于 CAS 的原子类计数,达到实现无锁操作的目的。


     	@Test
    fun test_cas() {

    lateinit var s1: String
    lateinit var s2: String

    val cas = AtomicInteger(2)

    Thread {
    s1 = task1()
    cas.getAndDecrement()
    }.start()

    Thread {
    s2 = task2()
    cas.getAndDecrement()
    }.start()

    while (cas.get() != 0) {}

    task3(s1, s2)

    }

    while 循环空转看起来有些浪费资源,但是自旋锁的本质就是这样,所以 CAS 仅仅适用于一些cpu密集型的短任务同步。




    volatile


    看到 CAS 的无锁实现,也许很多人会想到 volatile, 是否也能实现无锁的线程安全?


     	@Test
    fun test_Volatile() {
    lateinit var s1: String
    lateinit var s2: String

    Thread {
    s1 = task1()
    cnt--
    }.start()

    Thread {
    s2 = task2()
    cnt--
    }.start()

    while (cnt != 0) {
    }

    task3(s1, s2)

    }

    注意,这种写法是错误的 volatile 能保证可见性,但是不能保证原子性,cnt-- 并非线程安全,需要加锁操作




    8. Future


    上面无论有锁操作还是无锁操作,都需要定义两个变量s1s2记录结果非常不方便。 Java 1.5 开始,提供了 CallableFuture ,可以在任务执行结束时返回结果。


    @Test
    fun test_future() {

    val future1 = FutureTask(Callable(task1))
    val future2 = FutureTask(Callable(task2))

    Executors.newCachedThreadPool().execute(future1)
    Executors.newCachedThreadPool().execute(future2)

    task3(future1.get(), future2.get())

    }

    通过 future.get(),可以同步等待结果返回,写起来非常方便




    9. CompletableFuture


    future.get() 虽然方便,但是会阻塞线程。 Java 8 中引入了 CompletableFuture ,他实现了 Future 接口的同时实现了 CompletionStage 接口。 CompletableFuture 可以针对多个 CompletionStage 进行逻辑组合、实现复杂的异步编程。 这些逻辑组合的方法以回调的形式避免了线程阻塞:


    @Test
    fun test_CompletableFuture() {
    CompletableFuture.supplyAsync(task1)
    .thenCombine(CompletableFuture.supplyAsync(task2)) { p1, p2 ->
    task3(p1, p2)
    }.join()
    }



    10. RxJava


    RxJava 提供的各种操作符以及线程切换能力同样可以帮助我们实现需求: zip 操作符可以组合两个 Observable 的结果;subscribeOn 用来启动异步任务


    @Test
    fun test_Rxjava() {

    Observable.zip(
    Observable.fromCallable(Callable(task1))
    .subscribeOn(Schedulers.newThread()),
    Observable.fromCallable(Callable(task2))
    .subscribeOn(Schedulers.newThread()),
    BiFunction(task3)
    ).test().awaitTerminalEvent()

    }



    11. Coroutine


    前面讲了那么多,其实都是 Java 的工具。 Coroutine 终于算得上是 Kotlin 特有的工具了:


    @Test
    fun test_coroutine() {

    runBlocking {
    val c1 = async(Dispatchers.IO) {
    task1()
    }

    val c2 = async(Dispatchers.IO) {
    task2()
    }

    task3(c1.await(), c2.await())
    }
    }

    写起来特别舒服,可以说是集前面各类工具的优点于一身。




    12. Flow


    Flow 就是 Coroutine 版的 RxJava,具备很多 RxJava 的操作符,例如 zip:



    @Test
    fun test_flow() {

    val flow1 = flow<String> { emit(task1()) }
    val flow2 = flow<String> { emit(task2()) }

    runBlocking {
    flow1.zip(flow2) { t1, t2 ->
    task3(t1, t2)
    }.flowOn(Dispatchers.IO)
    .collect()

    }

    }

    flowOn 使得 Task 在异步计算并发射结果。




    总结


    上面这么多方式,就像茴香豆的“茴”字的四种写法,没必要都掌握。作为结论,在 Kotlin 上最好用的线程同步方案首推协程!



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

    被React Native插件狂虐2天之后,写下c++_share.so冲突处理心路历程

    为了应对活体检测客户 react-native 端的支持,需要开发 react-native 插件供客户使用。关于react-native 插件开发具体可以参考react官网: reactnative.cn/docs/native… reactnative....
    继续阅读 »

    为了应对活体检测客户 react-native 端的支持,需要开发 react-native 插件供客户使用。关于react-native 插件开发具体可以参考react官网:



    具体包含两部分



    1. ViewManager:包装原生的 view 供 react-native 的 js 部分使用

    2. NativeModule:提供原生的 api 能力供 react-native 的 js 部分调用


    心路历程


    参考着官方事例,插件代码很快就完成。开开心心把插件发布到 github 之后试用了一下就遇到了第一个问题


    image.png


    看错误很容易发现是 so 冲突了,也就是说 react-native 脚手架创建的项目原本就存在libc++_share.so,正好我们的活体检测 sdk 也存在 libc++_shared.so。冲突的解决方法也很简单,在 android 域中添加如下配置:


    packagingOptions {
    pickFirst 'lib/arm64-v8a/libc++_shared.so'
    pickFirst 'lib/armeabi-v7a/libc++_shared.so'
    pickFirst 'lib/x86/libc++_shared.so'
    pickFirst 'lib/x86_64/libc++_shared.so'
    }

    这边顺便解释下packagingOptions中几个关键字的意思和作用

    关键字含义实例
    doNotStrip可以设置某些动态库不被优化压缩doNotStrip '*/arm64-v8a/libc++_shared.so'
    pickFirst匹配到多个相同文件,只提取第一个pickFirst 'lib/arm64-v8a/libc++_shared.so'
    exclude过滤掉某些文件或者目录不添加到APK中exclude 'lib/arm64-v8a/libc++_shared.so'
    merge将匹配的文件合并添加到APK中merge 'lib/arm64-v8a/libc++_shared.so'

    上述例子中处理的方式是遇到冲突取第一个libc++_shared.so。冲突解决之后继续运行,打开摄像头过一会儿就崩溃了,报错如下:


    com.awesomeproject A/libc: Fatal signal 6 (SIGABRT), code -1 (SI_QUEUE) in tid 30755 (work), pid 30611 (.awesomeproject)

    从报错信息来看只知道错误的地方在jni部分,具体在什么位置?哪行代码?一概不知。从现象来看大致能猜到错误的入口,于是逐行代码屏蔽去试,最后定位到报错的代码竟然是:


    std::cout << "src: (" << h << ", " << w << ")" << std::endl;

    仅仅是简单的c++输出流,对功能本来没有影响。很好奇为什么会崩溃,查了好久一无所获。既然不影响功能就先删掉了这行代码,果然就不报错了,功能都能正常使用了,开开心心的交给测试回归。一切都是好好的,直到跑在arm64-v8a的设备上,出现了如下报错:


    1e14141e-51c8-42e7-a5c7-440905742247.png


    这次有明显的报错信息,意思是当运行opencv_java3.so的时候缺少_sfp_handler_exception函数,这个函数实际上是在c++_shared.so库中的。奇怪的是原生代码运行在arm64-v8a的设备上是好的,那怎么跑在react-native环境就会缺少_sfp_handler_exception函数了呢?
    直到我在原生用ndk20a编译代码报了同样的错误,才意识到一切问题的源头是pickFirst引起的。


    4a1a64b0-296d-4753-abc1-92da09d60cde.png


    a4d4f827-ccea-4817-9175-e47458f1c917.png


    可以明显的看到react-native和原生环境跑出来的apk包中c++_shared.so的大小是不同的。
    也就是说pickFirst是存在安全隐患的,就拿这个例子来说,假如两个c++_shared.so是用不同版本的ndk打出来的,其实内部的库函数是不一样的,pickFirst贸然选择第一个必然导致另外的库不兼容。那么是不是可以用merge合并两个c++_shared.so,试了一下针对so merge失效了,只能是另辟蹊径。
    如果我们的sdk只有一个库动态依赖于c++_shared.so,大可把c++_shared.so以静态库的方式打入,这样就不会有so冲突问题,同时也解决了上述问题。配置如下:


    externalNativeBuild {
    ndk {
    abiFilters "armeabi-v7a", "arm64-v8a"
    }
    cmake {
    cppFlags "-std=c++11 -frtti -fexceptions"
    arguments "-DANDROID_STL=c++_shared" //shared改为static
    }
    }

    可惜的是例子中的sdk不止一个库动态依赖于c++_shared.so,所以这条路也行不通。那么只能从react-native侧出发寻找方案。


    方案一(推荐)


    找出react-native这边的c++_shared.so是基于什么ndk版本打出来的,想办法把两端的ndk版本保持统一,问题也就迎刃而解了。


    b2d4115d-0316-47c5-a14f-3dd5daf167f9.png


    从react-native对应的android工程的蛛丝马迹中发现大概是基于ndk r20b打出来的。接下来就是改造sdk中c++_shared.so基于的ndk版本了。



    1. 基于ndk r20b版本重新编译opencv库

    2. 把opencv库连接到项目,基于ndk r20b版本重新编译alive_detected.so库


    把编译好的sdk重新导入插件升级,运行之后果然所有的问题得以解决。


    方案二


    去除react-native中的c++_shared.so库,react-native并不是一开始就引入了c++_shared.so。从React Native版本升级中去查看c++_shared.so是哪个版本被引入的,可以发现0.59之前的版本是没有c++_shared.so库的,详见对比:


    bd504920-1855-445d-8f8f-cf4b6e4feabd.png


    4a77bb53-f0ad-45b4-862c-2e264b88db9d.png


    那么我们把react-native版本降级为0.59以下也能解决问题,降级步骤如下:



    1. 进入工程


    cd Temple


    1. 指定版本


    npm install --save react-native@0.58.6


    1. 更新


    react-native upgrade


    1. 一路替换文件


    fdf99f54-b121-4321-8956-6e3bce7efb99.png


    总结


    Android开发会面临各种环境问题,遇到问题还是要从原理出发,理清问题发生的根源,这样问题就很好解决。



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

    收起阅读 »