注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

设计模式-代理模式(Proxy Pattern)

定义为其他对象提供一种代理以控制对这个对象的访问按照代理的创建时期,代理类可以分为两种: 静态代理:由程序员创建代理类或特定工具自动生成源代码再对其编译。在程序运行前代理类的.class文件就已经存在了。动态代理:在程序运行时运用反射机制动态创建而成...
继续阅读 »

定义

为其他对象提供一种代理以控制对这个对象的访问

按照代理的创建时期,代理类可以分为两种: 

  • 静态代理:由程序员创建代理类或特定工具自动生成源代码再对其编译。在程序运行前代理类的.class文件就已经存在了。

  • 动态代理:在程序运行时运用反射机制动态创建而成。

使用场景

主要作用:控制对象访问

  • 扩展目标对象的功能:例如演员(目标对象),有演戏的功能,找一个经纪人(代理),会额外提供收费的功能,实际上是代理的功能,而不是演员的功能。
  • 限制目标对象的功能:例如经纪人对收费不满意,只让演员演一场戏,对演员的功能进行了部分限制。

类图

  • Subject:抽象主题角色,主要是声明代理类和被代理类共同的接口方法
  • RealSubject:具体主题角色(被代理角色),执行具体的业务逻辑
  • Proxy:代理类,持有一个被代理对象的引用,负责在被代理对象方法调用的前后做一些额外操作

4、优点

  • 职责清晰,被代理角色只实现实际的业务逻辑,代理对象实现附加的处理逻辑
  • 扩展性高,可以更换不同的代理类,实现不同的代理逻辑

静态代理

编译时期就已经存在,一般首先需要定义接口,而被代理的对象和代理对象一起实现相同的接口。

1、接口定义:

public interface Play {
//唱歌
void sing(int count);
//演出
void show();
}

2、演员(被代理对象):

public class Actor implements Play {
@Override
public void sing(int count) {
System.out.print("唱了" + count + "首歌");
}

@Override
public void show() {
System.out.print("进行演出");
}
}

被代理对象提供了几个具体方法实现

3、经纪人(代理对象):

public class Agent implements Play {
//被代理对象
private Play player;
private long money;

public void setMoney(long money){
this.money = money;
}

/**
* @param player
* @param money 收费
*/

public Agent(Play player, long money) {
this.player = player;
this.money = money;
}

@Override
public void sing(int count) {
player.sing(count);
}
//控制了被代理对象的访问
@Override
public void show() {
if (money > 100) {
player.show();
} else {
System.out.println("baibai...");
}
}
}

4、使用

public class PlayTest {
public static void main(String[] args){
Actor actor = new Actor();
Agent agent = new Agent(actor, 50);
agent.sing(2);
agent.show();
agent.setMoney(200);
agent.show();
}
}

代理对象通过自身的逻辑处理对目标对象的功能进行控制。

动态代理

动态一般指的是在运行时的状态,是相对编译时的静态来区分,就是在运行时生成一个代理对象帮我们做一些逻辑处理。主要使用反射技术获得类的加载器并且创建实例。
动态代理可以在运行时动态创建一个类,实现一个或多个接口,可以在不修改原有类的基础上动态为通过该类获取的对象添加方法、修改行为。

1、生成动态代理类:

InvocationHandler是动态代理接口,动态代理类需要实现该接口,并在invoke方法中对代理类的方法进行处理

public interface InvocationHandler {
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable;
}

参数说明:

  • Object proxy:被代理的对象
  • Object[] args:要调用的方法
  • Object[] args:方法调用所需要的参数

2、创建动态代理类

Proxy类可以通过newProxyInstance创建一个代理对象

public static Object newProxyInstance(ClassLoader loader,
Class<?>[] interfaces,
InvocationHandler h)

throws IllegalArgumentException {
if (h == null) {
throw new NullPointerException();
}
Class<?> cl = getProxyClass0(loader, interfaces);
try {
//通过反射完成了代理对象的创建
final Constructor<?> cons = cl.getConstructor(constructorParams);
return newInstance(cons, h);
} catch (NoSuchMethodException e) {
throw new InternalError(e.toString());
}
}

参数说明:

  • ClassLoader loader:类加载器
  • Class<?>[] interfaces:所有的接口
  • InvocationHandler h:实现InvocationHandler接口的子类

3、动态代理demo:

(1)定义动态代理类

public class ActorProxy implements InvocationHandler {
private Play player;

public ActorProxy(Play player) {
this.player = player;
}

/**
* 获取动态代理对象
*/

public Object getDynamicProxy() {
return Proxy.newProxyInstance(player.getClass().getClassLoader(), player.getClass().getInterfaces(), this);
}

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//处理被代理对象的方法实现
if ("show".equals(method.getName())) {
System.out.println("代理处理show....");
return method.invoke(player, null);
} else if ("sing".equals(method.getName())) {
System.out.println("代理处理sing....");
return method.invoke(player, 2);
}
return null;
}
}

代理类实现InvocationHandler接口,在invoke方法中对player(被代理对象)做相应的逻辑处理。

(2)使用

public class ProxyTest {

public static void main(String[] args) {

ActorProxy actorProxy = new ActorProxy(new Actor());
//通过调用Proxy.newProxyInstance方法生成代理对象
Play proxy = (Play) actorProxy.getDynamicProxy();
//调用代理类相关方法
proxy.show();
proxy.sing(3);
}
}

四、Android中的代理模式

Retrofit代理模式

(1)Retrofit使用: 定义接口

public interface MyService {
@GET("users/{user}/list")
Call<String> getMyList(@Path("user") String user);
}

新建retrofit对象,然后产生一个接口对象,然后调用具体方法去完成请求。

Retrofit retrofit = new Retrofit.Builder()
.baseUrl("http://xxx.com")
.build();
MyService myService = retrofit.create(MyService.class);
Call<String> myList = myService.getMyList("my");

retrofit.create方法就是通过动态代理的方式传入一个接口,返回了一个对象

(2)动态代理分析:

public <T> T create(final Class<T> service) {
//判断是否为接口
Utils.validateServiceInterface(service);
if (validateEagerly) {
eagerlyValidateMethods(service);
}
//创建请求接口的动态代理对象
return (T) Proxy.newProxyInstance(service.getClassLoader(), new Class<?>[] { service },
new InvocationHandler() {
private final Platform platform = Platform.get();
private final Object[] emptyArgs = new Object[0];

@Override public Object invoke(Object proxy, Method method, @Nullable Object[] args)
throws Throwable {
// If the method is a method from Object then defer to normal invocation.
if (method.getDeclaringClass() == Object.class) {
return method.invoke(this, args);
}
if (platform.isDefaultMethod(method)) {
return platform.invokeDefaultMethod(method, service, proxy, args);
}
//将接口中方法传入返回了ServiceMethod
return loadServiceMethod(method).invoke(args != null ? args : emptyArgs);
}
});
}

通过Proxy.newProxyInstance,该动态代理对象可以拿到请求接口实例上所有注解,然后通过代理对象进行网络请求。

收起阅读 »

iOS RXSwift 5.5

iOS
deferred直到订阅发生,才创建 Observable,并且为每位订阅者创建全新的 Observabledeferred 操作符将等待观察者订阅它,才创建一个 Observable,它会通过一个构建函数为每一位订阅者创建新的 Observable。看上去每...
继续阅读 »

deferred

直到订阅发生,才创建 Observable,并且为每位订阅者创建全新的 Observable

deferred 操作符将等待观察者订阅它,才创建一个 Observable,它会通过一个构建函数为每一位订阅者创建新的 Observable。看上去每位订阅者都是对同一个 Observable 产生订阅,实际上它们都获得了独立的序列。

在一些情况下,直到订阅时才创建 Observable 是可以保证拿到的数据都是最新的。

debug

打印所有的订阅,事件以及销毁信息


演示

let disposeBag = DisposeBag()

let sequence = Observable<String>.create { observer in
observer.onNext("🍎")
observer.onNext("🍐")
observer.onCompleted()
return Disposables.create()
}

sequence
.debug("Fruit")
.subscribe()
.disposed(by: disposeBag)

输出结果:

2017-11-06 20:49:43.187: Fruit -> subscribed
2017-11-06 20:49:43.188: Fruit -> Event next(🍎)
2017-11-06 20:49:43.188: Fruit -> Event next(🍐)
2017-11-06 20:49:43.188: Fruit -> Event completed
2017-11-06 20:49:43.189: Fruit -> isDisposed

debounce

过滤掉高频产生的元素

debounce 操作符将发出这种元素,在 Observable 产生这种元素后,一段时间内没有新元素产生。

收起阅读 »

iOS RXSwift 5.4

iOS
connect通知 ConnectableObservable 可以开始发出元素了ConnectableObservable 和普通的 Observable 十分相似,不过在被订阅后不会发出元素,直到 ...
继续阅读 »


connect

通知 ConnectableObservable 可以开始发出元素了

ConnectableObservable 和普通的 Observable 十分相似,不过在被订阅后不会发出元素,直到 connect 操作符被应用为止。这样一来你可以等所有观察者全部订阅完成后,才发出元素。


演示

let intSequence = Observable<Int>.interval(1, scheduler: MainScheduler.instance)
.publish()

_ = intSequence
.subscribe(onNext: { print("Subscription 1:, Event: \($0)") })

DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
_ = intSequence.connect()
}

DispatchQueue.main.asyncAfter(deadline: .now() + 4) {
_ = intSequence
.subscribe(onNext: { print("Subscription 2:, Event: \($0)") })
}

DispatchQueue.main.asyncAfter(deadline: .now() + 6) {
_ = intSequence
.subscribe(onNext: { print("Subscription 3:, Event: \($0)") })
}

输出结果:

Subscription 1:, Event: 0
Subscription 1:, Event: 1
Subscription 2:, Event: 1
Subscription 1:, Event: 2
Subscription 2:, Event: 2
Subscription 1:, Event: 3
Subscription 2:, Event: 3
Subscription 3:, Event: 3
Subscription 1:, Event: 4
Subscription 2:, Event: 4
Subscription 3:, Event: 4
Subscription 1:, Event: 5
Subscription 2:, Event: 5
Subscription 3:, Event: 5
Subscription 1:, Event: 6
Subscription 2:, Event: 6
Subscription 3:, Event: 6
...

create

通过一个构建函数完整的创建一个 Observable

create 操作符将创建一个 Observable,你需要提供一个构建函数,在构建函数里面描述事件(nexterrorcompleted)的产生过程。

通常情况下一个有限的序列,只会调用一次观察者的 onCompleted 或者 onError 方法。并且在调用它们后,不会再去调用观察者的其他方法。


演示

创建一个 [0, 1, ... 8, 9] 的序列:

let id = Observable<Int>.create { observer in
observer.onNext(0)
observer.onNext(1)
observer.onNext(2)
observer.onNext(3)
observer.onNext(4)
observer.onNext(5)
observer.onNext(6)
observer.onNext(7)
observer.onNext(8)
observer.onNext(9)
observer.onCompleted()
return Disposables.create()
}
收起阅读 »

iOS RXSwift 5.3

iOS
concat让两个或多个 Observables 按顺序串连起来concat 操作符将多个 Observables 按顺序串联起来,当前一个 Observable 元素发送完毕后,后一个&n...
继续阅读 »

concat

让两个或多个 Observables 按顺序串连起来

concat 操作符将多个 Observables 按顺序串联起来,当前一个 Observable 元素发送完毕后,后一个 Observable 才可以开始发出元素。

concat 将等待前一个 Observable 产生完成事件后,才对后一个 Observable 进行订阅。如果后一个是“热” Observable ,在它前一个 Observable 产生完成事件前,所产生的元素将不会被发送出来。

startWith 和它十分相似。但是startWith不是在后面添加元素,而是在前面插入元素。

merge 和它也是十分相似。merge并不是将多个 Observables 按顺序串联起来,而是将他们合并到一起,不需要 Observables 按先后顺序发出元素。


演示

let disposeBag = DisposeBag()

let subject1 = BehaviorSubject(value: "🍎")
let subject2 = BehaviorSubject(value: "🐶")

let variable = Variable(subject1)

variable.asObservable()
.concat()
.subscribe { print($0) }
.disposed(by: disposeBag)

subject1.onNext("🍐")
subject1.onNext("🍊")

variable.value = subject2

subject2.onNext("I would be ignored")
subject2.onNext("🐱")

subject1.onCompleted()

subject2.onNext("🐭")

输出结果:

next(🍎)
next(🍐)
next(🍊)
next(🐱)
next(🐭)


concatMap

将 Observable 的元素转换成其他的 Observable,然后将这些 Observables 串连起来

concatMap 操作符将源 Observable 的每一个元素应用一个转换方法,将他们转换成 Observables。然后让这些 Observables 按顺序的发出元素,当前一个 Observable 元素发送完毕后,后一个 Observable 才可以开始发出元素。等待前一个 Observable 产生完成事件后,才对后一个 Observable 进行订阅。


演示

let disposeBag = DisposeBag()

let subject1 = BehaviorSubject(value: "🍎")
let subject2 = BehaviorSubject(value: "🐶")

let variable = Variable(subject1)

variable.asObservable()
.concatMap { $0 }
.subscribe { print($0) }
.disposed(by: disposeBag)

subject1.onNext("🍐")
subject1.onNext("🍊")

variable.value = subject2

subject2.onNext("I would be ignored")
subject2.onNext("🐱")

subject1.onCompleted()

subject2.onNext("🐭")

输出结果:

next(🍎)
next(🍐)
next(🍊)
next(🐱)
next(🐭)
收起阅读 »

iOS RXSwift 5.2

iOS
buffer缓存元素,然后将缓存的元素集合,周期性的发出来buffer 操作符将缓存 Observable 中发出的新元素,当元素达到某个数量,或者经过了特定的时间,它就会将这个元素集合发送出来。catchError从一个错误事件...
继续阅读 »

buffer

缓存元素,然后将缓存的元素集合,周期性的发出来

buffer 操作符将缓存 Observable 中发出的新元素,当元素达到某个数量,或者经过了特定的时间,它就会将这个元素集合发送出来。

catchError

从一个错误事件中恢复,将错误事件替换成一个备选序列

catchError 操作符将会拦截一个 error 事件,将它替换成其他的元素或者一组元素,然后传递给观察者。这样可以使得 Observable 正常结束,或者根本都不需要结束。

这里存在其他版本的 catchError 操作符。


演示

let disposeBag = DisposeBag()

let sequenceThatFails = PublishSubject<String>()
let recoverySequence = PublishSubject<String>()

sequenceThatFails
.catchError {
print("Error:", $0)
return recoverySequence
}
.subscribe { print($0) }
.disposed(by: disposeBag)

sequenceThatFails.onNext("😬")
sequenceThatFails.onNext("😨")
sequenceThatFails.onNext("😡")
sequenceThatFails.onNext("🔴")
sequenceThatFails.onError(TestError.test)

recoverySequence.onNext("😊")

输出结果:

next(😬)
next(😨)
next(😡)
next(🔴)
Error: test
next(😊)

catchErrorJustReturn

catchErrorJustReturn 操作符会将error 事件替换成其他的一个元素,然后结束该序列。


演示

let disposeBag = DisposeBag()
let sequenceThatFails = PublishSubject<String>()

sequenceThatFails
.catchErrorJustReturn("😊")
.subscribe { print($0) }
.disposed(by: disposeBag)

sequenceThatFails.onNext("😬")
sequenceThatFails.onNext("😨")
sequenceThatFails.onNext("😡")
sequenceThatFails.onNext("🔴")
sequenceThatFails.onError(TestError.test)

输出结果:

next(😬)
next(😨)
next(😡)
next(🔴)
next(😊)
completed



combineLatest

当多个 Observables 中任何一个发出一个元素,就发出一个元素。这个元素是由这些 Observables 中最新的元素,通过一个函数组合起来的

combineLatest 操作符将多个 Observables 中最新的元素通过一个函数组合起来,然后将这个组合的结果发出来。这些源 Observables 中任何一个发出一个元素,他都会发出一个元素(前提是,这些 Observables 曾经发出过元素)。


演示

tips: 可与 zip 比较学习

let disposeBag = DisposeBag()

let first = PublishSubject<String>()
let second = PublishSubject<String>()

Observable.combineLatest(first, second) { $0 + $1 }
.subscribe(onNext: { print($0) })
.disposed(by: disposeBag)

first.onNext("1")
second.onNext("A")
first.onNext("2")
second.onNext("B")
second.onNext("C")
second.onNext("D")
first.onNext("3")
first.onNext("4")

输出结果:

1A
2A
2B
2C
2D
3D
4D
收起阅读 »

Compose 仅50行代码轻松定制下滑刷新

目前有一个正在进行的 Jetpack Compose中文手册 项目,旨在帮助开发者更好的理解和掌握Compose框架,目前仍还在开荒中,欢迎大家进行关注与加入! 这篇文章由本人撰写,目前文章已经发布到该手册中,欢迎进行查阅。 下滑刷新效果展...
继续阅读 »

目前有一个正在进行的 Jetpack Compose中文手册 项目,旨在帮助开发者更好的理解和掌握Compose框架,目前仍还在开荒中,欢迎大家进行关注与加入! 这篇文章由本人撰写,目前文章已经发布到该手册中,欢迎进行查阅。


下滑刷新效果展示


像下滑刷新这样涉及到嵌套滑动的手势行为就需要使用 nestedScroll 修饰符来完成。接下来,就让我们先来介绍一下 nestedScroll 修饰符是什么,并且该怎么用。





nestedScroll 修饰符


nestedScroll 修饰符主要用于处理嵌套滑动的场景,为父布局劫持消费子布局滑动手势提供了可能。


使用 nestedScroll 参数列表中有一个必选参数 connection 和一个可选参数 dispatcher


connection: 嵌套滑动手势处理的核心逻辑,内部回调可以在子布局获得滑动事件前预先消费掉部分或全部手势偏移量,也可以获取子布局消费后剩下的手势偏移量。


dispatcher:调度器,内部包含用于父布局的 NestedScrollConnection , 可以调用 dispatch* 方法来通知父布局发生滑动


fun Modifier.nestedScroll(
connection: NestedScrollConnection,
dispatcher: NestedScrollDispatcher? = null
)

NestedScrollConnection


NestedScrollConnection 提供了四个回调方法。


interface NestedScrollConnection {
fun onPreScroll(available: Offset, source: NestedScrollSource): Offset = Offset.Zero

fun onPostScroll(
consumed: Offset,
available: Offset,
source: NestedScrollSource
): Offset = Offset.Zero

suspend fun onPreFling(available: Velocity): Velocity = Velocity.Zero

suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
return Velocity.Zero
}
}

onPreScroll


方法描述:预先劫持滑动事件,消费后再交由子布局。


参数列表:



  • available:当前可用的滑动事件偏移量

  • source:滑动事件的类型


返回值:当前组件消费的滑动事件偏移量,如果不想消费可返回Offset.Zero




onPostScroll


方法描述:获取子布局处理后的滑动事件


参数列表:



  • consumed:之前消费的所有滑动事件偏移量

  • available:当前剩下还可用的滑动事件偏移量

  • source:滑动事件的类型


返回值:当前组件消费的滑动事件偏移量,如果不想消费可返回 Offset.Zero ,则剩下偏移量会继续交由当前布局的父布局进行处理




onPreFling


方法描述:获取 Fling 开始时的速度。


参数列表:



  • available:Fling 开始时的速度


返回值:当前组件消费的速度,如果不想消费可返回 Velocity.Zero




onPostFling


方法描述:获取 Fling 结束时的速度信息。


参数列表:




  • consumed:之前消费的所有速度




  • available:当前剩下还可用的速度




返回值:当前组件消费的速度,如果不想消费可返回Velocity.Zero,剩下速度会继续交由当前布局的父布局进行处理。



Fling含义:当我们手指在滑动列表时,如果是快速滑动并抬起,则列表会根据惯性继续飘一段距离后停下,这个行为就是 Fling onPreFling 在你手指刚抬起时便会回调,而 onPostFling 会在飘一段距离停下后回调。



实现下滑刷新


像下滑刷新这样涉及到嵌套滑动的手势行为就可以使用 nestedScroll 修饰符来完成。


示例介绍


在这个示例中存在着加载动画和列表数据。当我们手指向下滑时,此时如果列表顶部没有数据则会逐渐出现加载动画。与之相反,当我们手指向上滑时,此时如果加载动画还在,则加载动画逐渐向上消失,直到加载动画完全消失后,列表才会被向下滑动。


设计实现方案


为实现这个滑动刷新的需求,我们可以设计如下方案。我们首先需要将加载动画和列表数据放到一个父布局中统一管理。




  1. 当我们手指向下滑时,我们希望滑动手势首先交给子布局中的列表进行处理,如果列表已经滑到顶部说明此时滑动手势事件没有被消费,此时再交由父布局进行消费。父布局可以消费列表消费剩下的滑动手势事件(为加载动画增加偏移)。




  2. 当我们手指向上滑时,我们希望滑动手势首先被父布局消费(为加载动画减小偏移),如果加载动画本身仍未出现时,则不进行消费。然后将剩下的滑动手势交给子布局列表进行消费。




NestedScrollConnection 实现


使用 nestedScroll 修饰符最重要的就是根据自己的业务场景来定制 NestedScrollConnection 的实现,接下来我们就逐个分析 NestedScrollConnection 重的借口该如何进行实现。


实现 onPostScroll


向我们之前设计的实现方案一样,当我们手指向下滑时,我们希望滑动手势首先交给子布局中的列表进行处理,如果列表已经滑到顶部说明此时滑动手势事件没有被消费,此时再交由父布局进行消费。 onPostScroll 回调时机是符合我们的需求的。


我们首先需要判断该滑动事件是不是拖动事件,通过 available.y > 0 判断是否是下滑手势,如果都没问题时,通知加载动画增加偏移量。返回值 Offset(x = 0f, y = available.y) 意味着将剩下的所有偏移量全部消费调,不再向外层父布局继续传播了。


override fun onPostScroll(
consumed: Offset,
available: Offset,
source: NestedScrollSource
): Offset {
if (source == NestedScrollSource.Drag && available.y > 0) {
state.updateOffsetDelta(available.y)
return Offset(x = 0f, y = available.y)
} else {
return Offset.Zero
}
}

实现 onPreScroll


与上面相反,此时我们希望下滑收回加载动画,当我们手指向上滑时,我们希望滑动手势首先被父布局消费(为加载动画减小偏移),如果加载动画本身仍未出现时,则不进行消费。然后将剩下的滑动手势交给子布局列表进行消费。onPreScroll 回调时机是符合这个需求的。


我们首先需要判断该滑动事件是不是拖动事件,通过 available.y < 0 判断是否是上滑手势。此时可能加载动画本身未出现,所以需要额外进行判断。如果未出现则返回 Offset.Zero 不消费,如果出现了则返回 Offset(x = 0f, y = available.y) 进行消费。


override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
if (source == NestedScrollSource.Drag && available.y < 0) {
state.updateOffsetDelta(available.y)
return if (state.isSwipeInProgress) Offset(x = 0f, y = available.y) else Offset.Zero
} else {
return Offset.Zero
}
}

实现 onPreFling


接下来,我们需要一个松手时的吸附效果。如果拉过加载动画高度的一般则进行加载,否则就收缩回初始状态。前问我提到了 onPreFling 在松手时回调,即符合我们当前这个的场景。



即使松手时速度很慢或静止,onPreFlingonPostFling都会回调,只是速度数值很小。



这里我们只需要吸引效果,并不希望消费速度,所以返回 Velocity.Zero 即可


override suspend fun onPreFling(available: Velocity): Velocity {
if (state.indicatorOffset > height / 2) {
state.animateToOffset(height)
state.isRefreshing = true
} else {
state.animateToOffset(0.dp)
}
return Velocity.Zero
}

实现 onPreFling


由于我们的下滑刷新手势处理不涉及 onPreFling 回调时机,所以不进行额外的实现。


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

Flutter ListView懒加载(滑动不加载,停止滑动加载)

前言:为了更好的减小网络的带宽,使得列表更加流畅,我们需要了解懒加载,也称延迟加载。 面试真题:flutter如何实现懒加载? 关于上一章的登录界面,各位属实难为我了,我也在求ui小姐姐,各位点点赞给我点动力吧~ 懒加载也叫延迟加载,指的是在长网页中延迟...
继续阅读 »

前言:为了更好的减小网络的带宽,使得列表更加流畅,我们需要了解懒加载,也称延迟加载。 面试真题:flutter如何实现懒加载?


关于上一章的登录界面,各位属实难为我了,我也在求ui小姐姐,各位点点赞给我点动力吧~


5e3c9f11dc5c8d53c46907cf16e2b5e.jpg
ca.png


image.png


懒加载也叫延迟加载,指的是在长网页中延迟加载图像,是一种很好优化网页性能的方式。用户滚动到它们之前,可视区域外的图像不会加载。这与图像预加载相反,在长网页上使用延迟加载将使网页加载更快。在某些情况下,它还可以帮助减少服务器负载。常适用图片很多,页面很长的电商网站场景中。


对ListView优化就那么几点:(后面都会写出来):


1.Flutter ListView加载图片优化(懒加载)


2.Flutter ListView加载时使用图片使用缩略图,对图片进行缓存


3.Flutter 减少build()的耗时


本章,我们会实现wechat朋友圈的优化功能,即当页面在滑动时不加载图片,在界面停止滑动时加载图片。

效果图:

tt0.top-288153.gif


1.了解widget通知监听:NotificationListener


NotificationListener属性:




  • child:widget



  • onNotification:NotificationListenerCallback<Notification>

    返回值true表示消费掉当前通知不再向上一级NotificationListener传递通知,false则会再向上一级NotificationListener传递通知;这里需要注意的是通知是由下而上去传递的,所以才会称作冒泡通知!




2.需要一个bool来控制是否加载


///加载图片的标识
bool isLoadingImage = true;

3.编写传递通知的方法,使其作用于NotificationListener


bool notificationFunction(Notification notification) {
 ///通知类型
 switch (notification.runtimeType) {
   case ScrollStartNotification:
     print("开始滚动");

     ///在这里更新标识 刷新页面 不加载图片
     isLoadingImage = false;
     break;
   case ScrollUpdateNotification:
     print("正在滚动");
     break;
   case ScrollEndNotification:
     print("滚动停止");

     ///在这里更新标识 刷新页面 加载图片
     setState(() {
       isLoadingImage = true;
    });
     break;
   case OverscrollNotification:
     print("滚动到边界");
     break;
}
 return true;
}

4.根据bool值加载不同的组件


ListView buildListView() {
 return ListView.separated(
   itemCount: 1000, //子条目个数
   ///构建每个条目
   itemBuilder: (BuildContext context, int index) {
     if (isLoadingImage) {
       ///这时将子条目单独封装在了一个StatefulWidget中
       return Image.network(
         netImageUrl,
         width: 100,
         height: 100,
         fit: BoxFit.fitHeight,
      );
    } else {
       return Container(
         height: 100,
         width: 100,
         child: Text("加载中..."),
      ); //占位
    }
  },

   ///构建每个子Item之间的间隔Widget
   separatorBuilder: (BuildContext context, int index) {
     return new Divider();
  },
);
}

完整代码:


class ScrollHomePageState extends State {
 ///加载图片的标识
 bool isLoadingImage = true;

 ///网络图片地址
 String netImageUrl =
     "https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0a4ce25d48b8405cbf5444b6195928d4~tplv-k3u1fbpfcp-no-mark:0:0:0:0.awebp";

 @override
 Widget build(BuildContext context) {
   return Scaffold(
     appBar: new AppBar(
       title: Text("详情"),
    ),
     ///列表
     body: NotificationListener(
       ///子Widget中的滚动组件滑动时就会分发滚动通知
       child: buildListView(),
       ///每当有滑动通知时就会回调此方法
       onNotification: notificationFunction,
    ),
  );
}

 bool notificationFunction(Notification notification) {
   ///通知类型
   switch (notification.runtimeType) {
     case ScrollStartNotification:
       print("开始滚动");

       ///在这里更新标识 刷新页面 不加载图片
       isLoadingImage = false;
       break;
     case ScrollUpdateNotification:
       print("正在滚动");
       break;
     case ScrollEndNotification:
       print("滚动停止");

       ///在这里更新标识 刷新页面 加载图片
       setState(() {
         isLoadingImage = true;
      });
       break;
     case OverscrollNotification:
       print("滚动到边界");
       break;
  }
   return true;
}

 ListView buildListView() {
   return ListView.separated(
     itemCount: 1000, //子条目个数
     ///构建每个条目
     itemBuilder: (BuildContext context, int index) {
       if (isLoadingImage) {
         ///这时将子条目单独封装在了一个StatefulWidget中
         return Image.network(
           netImageUrl,
           width: 100,
           height: 100,
           fit: BoxFit.fitHeight,
        );
      } else {
         return Container(
           height: 100,
           width: 100,
           child: Text("加载中..."),
        ); //占位
      }
    },

     ///构建每个子Item之间的间隔Widget
     separatorBuilder: (BuildContext context, int index) {
       return new Divider();
    },
  );
}
}

是不是很简单,但是懒加载确实是面试真题,你了解了吗?


ad.png


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

为什么 Compose 没有布局嵌套问题?

前言 做过布局性能优化的同学都知道,为了优化界面加载速度,要尽可能的减少布局的层级。这主要是因为布局层级的增加,可能会导致测量时间呈指数级增长。 而Compose却没有这个问题,它从根本上解决了布局层级对布局性能的影响: Compose界面只允许一次测量。这意...
继续阅读 »

前言


做过布局性能优化的同学都知道,为了优化界面加载速度,要尽可能的减少布局的层级。这主要是因为布局层级的增加,可能会导致测量时间呈指数级增长。

Compose却没有这个问题,它从根本上解决了布局层级对布局性能的影响: Compose界面只允许一次测量。这意味着随着布局层级的加深,测量时间也只是线性增长的.

下面我们就一起来看看Compose到底是怎么只测量一次就把活给干了的,本文主要包括以下内容:



  1. 布局层级过深为什么影响性能?

  2. Compose为什么没有布局嵌套问题?

  3. Compose测量过程源码分析


1. 布局层级过深为什么影响性能?


我们总说布局层级过深会影响性能,那么到底是怎么影响的呢?主要是因为在某些情况下ViewGroup会对子View进行多次测量

举个例子


<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:orientation="vertical">

<View
android:layout_width="match_parent"
android:layout_height="100dp"
android:background="@android:color/holo_red_dark" />

<View
android:layout_width="100dp"
android:layout_height="100dp"
android:background="@android:color/black" />
</LinearLayout>


  1. LinearLayout宽度为wrap_content,因此它将选择子View的最大宽度为其最后的宽度

  2. 但是有个子View的宽度为match_parent,意思它将以LinearLayout的宽度为宽度,这就陷入死循环了

  3. 因此这时候, LinearLayout 就会先以0为强制宽度测量一下子View,并正常地测量剩下的其他子View,然后再用其他子View里最宽的那个的宽度,二次测量这个match_parent的子 View,最终得出它的尺寸,并把这个宽度作为自己最终的宽度。

  4. 这是对单个子View的二次测量,如果有多个子View写了match_parent ,那就需要对它们每一个都进行二次测量。

  5. 除此之外,如果在LinearLayout中使用了weight会导致测量3次甚至更多,重复测量在Android中是很常见的


上面介绍了为什么会出现重复测量,那么会有什么影响呢?不过是多测量了几次,会对性能有什么大的影响吗?

之所以需要避免布局层级过深是因为它对性能的影响是指数级的



  1. 如果我们的布局有两层,其中父View会对每个子View做二次测量,那它的每个子View一共需要被测量 2 次

  2. 如果增加到三层,并且每个父View依然都做二次测量,这时候最下面的子View被测量的次数就直接翻倍了,变成 4 次

  3. 同理,增加到 4 层的话会再次翻倍,子 View 需要被测量 8 次



也就是说,对于会做二次测量的系统,层级加深对测量时间的影响是指数级的,这就是Android官方文档建议我们减少布局层级的原因


2. Compose为什么没有布局嵌套问题?


我们知道,Compose只允许测量一次,不允许重复测量。

如果每个父组件对每个子组件只测量一次,那就直接意味着界面中的每个组件只会被测量一次



这样即使布局层级加深,测量时间却没有增加,把组件加载的时间复杂度从O(2ⁿ) 降到了 O(n)


那么问题就来了,上面我们已经知道,多次测量有时是必要的,但是为什么Compose不需要呢?

Compose中引入了固有特性测量(Intrinsic Measurement)


固有特性测量即Compose允许父组件在对子组件进行测量之前,先测量一下子组件的「固有尺寸」

我们上面说的,ViewGroup的二次测量,也是先进行这种「粗略测量」再进行最终的「正式测量」,使用固有特性测量可以产生同样的效果


而使用固有特性测量之所以有性能优势,主要是因为其不会随着层级的加深而加倍,固有特性测量也只进行一次

Compose会先对整个组件树进行一次Intrinsic测量,然后再对整体进行正式的测量。这样开辟两个平行的测量过程,就可以避免因为层级增加而对同一个子组件反复测量所导致的测量时间的不断加倍了。



总结成一句话就是,在Compose里疯狂嵌套地写界面,和把所有组件全都写进同一层里面,性能是一样的!所以Compose没有布局嵌套问题


2.1 固有特性测量使用


假设我们需要创建一个可组合项,该可组合项在屏幕上显示两个用分隔线隔开的文本,如下所示:


p14.png


为了实现分隔线与最高的文本一样高,我们可以怎么做呢?


@Composable
fun TwoTexts(
text1: String,
text2: String,
modifier: Modifier = Modifier
) {
Row(modifier = modifier.height(IntrinsicSize.Min)) {
Text(
modifier = Modifier
.weight(1f)
.padding(start = 4.dp)
.wrapContentWidth(Alignment.Start),
text = text1
)
Divider(
color = Color.Black,
modifier = Modifier
.fillMaxHeight()
.width(1.dp)
)
Text(
modifier = Modifier
.weight(1f)
.padding(end = 4.dp)
.wrapContentWidth(Alignment.End),
text = text2
)
}
}

注意,这里给Rowheight设置为了IntrinsicSize.Min,IntrinsicSize.Min会递归查询它子项的最小高度,其中两个Text的最小高度即文本的宽度,而Divider的最小高度为0
因此最后Row的高度即为最长的文本的高度,而Divider的高度为fillMaxHeight,也就跟最高的文本一样高了

如果我们这里不设置高度为IntrinsicSize.Min的话,Divider的高度是占满屏幕的,如下所示


p13.png


3. Compose测量过程源码分析


上面我们介绍了固有特性测量是什么,及固有特性测量的使用,下面我们来看看Compose的测量究竟是怎么实现的


3.1 测量入口


我们知道,在Compose中自定义Layout是通过Layout方法实现的


@Composable inline fun Layout(
content: @Composable () -> Unit,
modifier: Modifier = Modifier,
measurePolicy: MeasurePolicy
)

主要传入3个参数



  1. content:自定义布局的子项,我们后续需要对它们测量和定位

  2. modifier: 对Layout添加的一些修饰modifier

  3. measurePolicy: 即测量规则,这个是我们主要需要处理的地方


measurePolicy中主要有五个接口


fun interface MeasurePolicy {
fun MeasureScope.measure(measurables: List<Measurable>,constraints: Constraints): MeasureResult

fun IntrinsicMeasureScope.minIntrinsicWidth(measurables: List<IntrinsicMeasurable>,height: Int): Int

fun IntrinsicMeasureScope.minIntrinsicHeight(measurables: List<IntrinsicMeasurable>,width: Int): Int

fun IntrinsicMeasureScope.maxIntrinsicWidth(measurables: List<IntrinsicMeasurable>,height: Int): Int

fun IntrinsicMeasureScope.maxIntrinsicHeight(measurables: List<IntrinsicMeasurable>,width: Int): Int
}

可以看出:



  1. 使用固有特性测量的时候,会调用对应的IntrinsicMeasureScope方法,如使用Modifier.height(IntrinsicSize.Min),就会调用minIntrinsicHeight方法

  2. 父项测量子项时,就是在MeasureScope.measure方法中调用measure.meausre(constraints),但是具体是怎么实现的呢?我们来看个例子


@Composable
fun MeasureTest() {
Row() {
Layout(content = { }, measurePolicy = { measurables, constraints ->
measurables.forEach {
it.measure(constraints)
}
layout(100, 100) {

}
})
}
}

一个简单的例子,我们在measure方法中打个断点,如下图所示:



  1. 如下图所示,是由RowMeasurePolicy中开始测量子项,rowColumnMeasurePolicy我们定义为ParentPolicy

  2. 然后调用到LayoutNode,OuterMeasurablePlaceable,InnerPlaceablemeasure方法

  3. 最后再由InnerPlaceable中调用到子项的MeasurePolicy,即我们自定义Layout实现的部分,我们定义它为ChildPolicy

  4. 子项中也可能会测量它的子项,在这种情况下它就变成了一个ParentPolicy,然后继续后续的测量



综上所述,父项在测量子项时,子项的测量入口就是LayoutNode.measure,然后经过一系列调用到子项自己的MeasurePolicy,也就是我们自定义Layout中自定义的部分


3.2 LayoutNodeWrapper链构建


上面我们说了,测量入口是LayoutNode,后续还要经过OuterMeasurablePlaceable,InnerPlaceablemeasure方法,那么问题来了,这些东西是怎么来的呢?

首先给出结论



  1. 子项都是以LayoutNode的形式,存在于Parentchildren中的

  2. Layout的设置的modifier会以LayoutNodeWrapper链的形式存储在LayoutNode中,然后后续做相应变换


由于篇幅原因,关于第一点就不在这里详述了,有兴趣的同学可以参考:Jetpack Compose 测量流程源码分析

我们这里主要看下LayoutNodeWrapper链是怎么构建的


  internal val innerLayoutNodeWrapper: LayoutNodeWrapper = InnerPlaceable(this)
private val outerMeasurablePlaceable = OuterMeasurablePlaceable(this, innerLayoutNodeWrapper)
override fun measure(constraints: Constraints) = outerMeasurablePlaceable.measure(constraints)
override var modifier: Modifier = Modifier
set(value) {
// …… code
field = value
// …… code


// 创建新的 LayoutNodeWrappers 链
// foldOut 相当于遍历 modifier
val outerWrapper = modifier.foldOut(innerLayoutNodeWrapper) { mod /*📍 modifier*/ , toWrap ->
var wrapper = toWrap
if (mod is OnGloballyPositionedModifier) {
onPositionedCallbacks += mod
}
if (mod is RemeasurementModifier) {
mod.onRemeasurementAvailable(this)
}

val delegate = reuseLayoutNodeWrapper(mod, toWrap)
if (delegate != null) {
wrapper = delegate
} else {
// …… 省略了一些 Modifier判断
if (mod is KeyInputModifier) {
wrapper = ModifiedKeyInputNode(wrapper, mod).assignChained(toWrap)
}
if (mod is PointerInputModifier) {
wrapper = PointerInputDelegatingWrapper(wrapper, mod).assignChained(toWrap)
}
if (mod is NestedScrollModifier) {
wrapper = NestedScrollDelegatingWrapper(wrapper, mod).assignChained(toWrap)
}
// 布局相关的 Modifier
if (mod is LayoutModifier) {
wrapper = ModifiedLayoutNode(wrapper, mod).assignChained(toWrap)
}
if (mod is ParentDataModifier) {
wrapper = ModifiedParentDataNode(wrapper, mod).assignChained(toWrap)
}

}
wrapper
}

outerWrapper.wrappedBy = parent?.innerLayoutNodeWrapper
outerMeasurablePlaceable.outerWrapper = outerWrapper

……
}

如上所示:



  1. 默认的LayoutNodeWrapper链即由LayoutNode , OuterMeasurablePlaceable, InnerPlaceable 组成

  2. 当添加了modifier时,LayoutNodeWrapper链会更新,modifier会作为一个结点插入到其中


举个例子,如果我们给Layout设置一些modifier:


Modifier.size(100.dp).padding(10.dp).background(Color.Blue)

那么对应的LayoutNodeWrapper链如下图所示



这样一个接一个链式调用下一个的measure,直到最后一个结点InnerPlaceable

那么InnerPlaceable又会调用到哪儿呢?



InnerPlaceable最终调用到了我们自定义Layout时写的measure方法


3.3 固有特性测量是怎样实现的?


上面我们介绍了固有特性测量的使用,也介绍了LayoutNodeWrapper链的构建,那么固有特性测量是怎么实现的呢?

其实固有特性测量就是往LayoutNodeWrapper链中插入了一个Modifier


@Stable
fun Modifier.height(intrinsicSize: IntrinsicSize) = when (intrinsicSize) {
IntrinsicSize.Min -> this.then(MinIntrinsicHeightModifier)
IntrinsicSize.Max -> this.then(MaxIntrinsicHeightModifier)
}

private object MinIntrinsicHeightModifier : IntrinsicSizeModifier {
override fun MeasureScope.measure(
measurable: Measurable,
constraints: Constraints
): MeasureResult {
//正式测量前先根据固有特性测量获得一个约束
val contentConstraints = calculateContentConstraints(measurable, constraints)
//正式测量
val placeable = measurable.measure(
if (enforceIncoming) constraints.constrain(contentConstraints) else contentConstraints
)
return layout(placeable.width, placeable.height) {
placeable.placeRelative(IntOffset.Zero)
}
}

override fun MeasureScope.calculateContentConstraints(
measurable: Measurable,
constraints: Constraints
): Constraints {
val height = measurable.minIntrinsicHeight(constraints.maxWidth)
return Constraints.fixedHeight(height)
}

override fun IntrinsicMeasureScope.maxIntrinsicHeight(
measurable: IntrinsicMeasurable,
width: Int
) = measurable.minIntrinsicHeight(width)
}

如上所示:



  1. IntrinsicSize.Min其实也是个Modifier

  2. MinIntrinsicHeightModifier会在测量之间,先调用calculateContentConstraints计算约束

  3. calculateContentConstraints中则会递归地调用子项的minIntrinsicHeight,并找出最大值,这样父项的高度就确定了

  4. 固有特性测量完成后,再调用measurable.measure,开始真正的递归测量


3.4 测量过程小结


准备阶段

子项在声明时,会生成LayoutNode添加到父项的chindredn中,同时子项的modifier也将构建成LayoutNodeWrapper链,保存在LayoutNode

值得注意的是,如果使用了固有特性测量,将会添加一个IntrinsicSizeModifierLayoutNodeWrapper链中


测量阶段

父容器在其测量策略MeasurePolicymeasure函数中会执行childmeasure函数。

childmeasure方法按照构建好的LayoutNodeWrapper链一步步的执行各个节点的measure函数,最终走到InnerPlaceablemeasure函数,在这里又会继续它的children进行测量,此时它的children 就会和它一样进行执行上述流程,一直到所有children测量完成。


用下面这张图总结一下上述流程。


总结


本文主要介绍了以下内容



  1. Android中布局层级过深为什么会对性能有影响?

  2. Compose中为什么没有布局嵌套问题?

  3. 什么是固有特性测量及固有特性测量的使用

  4. Compose测量过程源码分析及固有特性测量到底是怎样实现的?


如果本文对你有所帮助,欢迎点赞收藏~


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

JavaScript 中有了Object 为什么还需要 Map 呢

众所周知,Map 是用于存储键值对的,而 JavaScript 中对象也是由键值对组成的,那么 Map 存在的意义是什么呢? 别把对象当 Map 1、可能通过原型链访问到未定义的属性 假设现有场景,开发一个网站,需要提供日语、汉语、韩语三种语言,我们可以定义一...
继续阅读 »

众所周知,Map 是用于存储键值对的,而 JavaScript 中对象也是由键值对组成的,那么 Map 存在的意义是什么呢?


别把对象当 Map


1、可能通过原型链访问到未定义的属性


假设现有场景,开发一个网站,需要提供日语、汉语、韩语三种语言,我们可以定义一个字典去管理。


const dictionary = {
'ja': {
'Ninjas for hire': '忍者を雇う',
},
'zh': {
'Ninjas for hire': '忍者出租',
},
'ko': {
'Ninjas for hire': '고용 닌자',
}
}

console.log(dictionary.ja['Ninjas for hire']) // 忍者を雇う
console.log(dictionary.zh['Ninjas for hire']) // 忍者出租
console.log(dictionary.ko['Ninjas for hire']) // 고용 닌자

这样我们就把不同语言的字典管理起来了。但是,当我们试图访问 constroctor 属性,问题就出现了。


console.log(dictionary.ko['constructor']) // ƒ Object() { [native code] }

对于不存在的属性,我们期望得到 undefined,结果却通过原型链访问到了未定义的属性,原型对象的 constructor 属性,指向构造函数。


此处有一个解决办法是把原型设置为 null


Object.setPrototypeOf(dictionary.ko, null)
console.log(dictionary.ko['constructor']) // undefined

2、对象的 Key 只能是字符串


假设需要将对象的 key 映射为 html 节点。我们写如下代码:


/* html部分
<div id="firstElement"></div>
<div id="secondElement"></div>
*/

const firstElement = document.getElementById('firstElement')
const secondElement = document.getElementById('secondElement')

const map = {}

map[firstElement] = {
data: 'firstElement'
}
map[secondElement] = {
data: 'secondElement'
}

console.log(map[firstElement].data) // secondElement
console.log(map[secondElement].data) // secondElement


第一个元素的数据被覆盖了,原因是对象中的 key 只能是字符串类型,当我们没有使用字符串类型时,它会隐式调用 toString() 函数进行转换。于是两个 html 元素都被转为字符串 [object HTMLDivElement]


使用 Map


1、Map 常用操作


Map 可以使用任何 JavaScript 数据类型作为键


function People(name) {
this.name = name
}
const zhangsan = new People('zhangsan')
const xiaoming = new People('xiaoming')
const lihua = new People('lihua')
// 创建 Map
const map = new Map()
// 创建 Map 并进行初始化
const map1 = new Map([
['key1', 'val1'],
['key2', 'val2'],
])
// 设置键值映射关系
map.set(zhangsan, {
region: 'HB'
})
map.set(xiaoming, {
region: 'HN'
})
// 根据 key 获取对应值
console.log(map.get(zhangsan)) // { region: 'HB' }
console.log(map.get(xiaoming)) // { region: 'HN' }
// 获取不存在的 key 得到 undefined
console.log(map.get(lihua)) // undefined
// 通过 has 函数判断指定 key 是否存在
console.log(map.has(lihua)) // false
console.log(map.has(xiaoming)) // true
// map存储映射个数
console.log(map.size) // 2
// delete 删除 key
map.delete(xiaoming)
console.log(map.has(xiaoming)) // false
console.log(map.size) // 1
// clear 清空 map
map.clear()
console.log(map.size) // 0

2、遍历 Map


Map 可以确保遍历的顺序和插入的顺序一致


const zhangsan = { name: 'zhangsan' }
const xiaoming = { name: 'xiaoming' }
const map = new Map()
map.set(zhangsan, { region: 'HB' })
map.set(xiaoming, { region: 'HN' })

for (let item of map) { // = for (let item of map.entries()) {
console.log(item)
}
// 每个键值对返回的是 [key, value] 的数组
// [ { name: 'zhangsan' }, { region: 'HB' } ]
// [ { name: 'xiaoming' }, { region: 'HN' } ]
for (let key of map.keys()) {
console.log(key)
}
// 遍历 key
// { name: 'zhangsan' }
// { name: 'xiaoming' }
for (let key of map.values()) {
console.log(key)
}
// 遍历 value
// { region: 'HB' }
// { region: 'HN' }

3、Map 中判断 key 相等


Map 内部使用 SameValueZero 比较操作。


关于SameValue 和 SameValueZero


SameValue (Object.is()) 和严格相等(===)相比,对于 NaN+0-0 的处理不同


Object.is(NaN, NaN) // true
Object.is(0, -0) // false

SameValueZero 与 SameValue 的区别主要在于 0-0 是否相等。


map.set(NaN, 0)
map.set(0, 0)
console.log(map.has(NaN)) // true
console.log(map.has(-0)) // true

4、Map 序列化


感谢 @一条鱼的心事 的提醒。


Map 无法被序列化,如果试图用 JSON.stringify 获得 MapJSON 的话,只会得到 "{}"


由于 Map 的键可以是任意数据类型,而 JSON 仅允许将字符串作为键,所以一般情况下无法将 Map 转为 JSON


不过可以通过下面的方式去尝试序列化一个 Map


// 初始化 Map(1) {"key1" => "val1"}
const originMap = new Map([['key1', 'val1']])
// 序列化 "[[\"key1\",\"val1\"]]"
const mapStr = JSON.stringify(Array.from(originMap.entries()))
// 反序列化 Map(1) {"key1" => "val1"}
const cloneMap = new Map(JSON.parse(mapStr))

Map 和 Object 的性能差异



  1. 内存占用


不同浏览器的情况不同,但给定固定大小的内存,Map 大约可以比 Object 多存储 50% 的键/值对。



  1. 插入性能


Map 略快,如果涉及大量操作,建议使用 Map



  1. 查找速度


性能差异极小,但如果只包含少量键/值对,则 Object 有时候速度更快。Object 作为数组使用时浏览器会进行优化。如果涉及大量查找操作,选择 Object 会更好一些。



  1. 删除性能


如果代码涉及大量的删除操作,建议选择 Map



作者:我不吃饼干呀
链接:https://juejin.cn/post/7012036506994868255

收起阅读 »

CSS实现瀑布流的两种方式

瀑布流又称瀑布流式布局,是比较流行的一种网站页面布局方式。在手机端进行多图片展示时会经常用到。即多行等宽元素排列,后面的元素依次添加到其后,等宽不等高,根据图片原比例缩放直至宽度达到我们的要求,依次按照规则放入指定位置。 那么瀑布流式布局有哪些实现方式呢? c...
继续阅读 »

瀑布流又称瀑布流式布局,是比较流行的一种网站页面布局方式。在手机端进行多图片展示时会经常用到。即多行等宽元素排列,后面的元素依次添加到其后,等宽不等高,根据图片原比例缩放直至宽度达到我们的要求,依次按照规则放入指定位置。


那么瀑布流式布局有哪些实现方式呢?


column 多行布局实现瀑布流



column 实现瀑布流主要依赖两个属性。


column-count 属性,是控制屏幕分为多少列。


column-gap 属性,是控制列与列之间的距离。



<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>瀑布流布局-column</title>
<style>
.box {
margin: 10px;
column-count: 3;
column-gap: 10px;
}
.item {
margin-bottom: 10px;
}
.item img{
width: 100%;
height:100%;
}
</style>
</head>
<body>
<div class="box">
<div class="item">
<img src="./imgs/1.jpg" alt="1" />
</div>
<div class="item">
<img src="./imgs/2.jpg" alt="2" />
</div>
<div class="item">
<img src="./imgs/3.jpg" alt="3" />
</div>
<div class="item">
<img src="./imgs/1.jpg" alt="1" />
</div>
<div class="item">
<img src="./imgs/2.jpg" alt="2" />
</div>
<div class="item">
<img src="./imgs/3.jpg" alt="3" />
</div>
<div class="item">
<img src="./imgs/1.jpg" alt="1" />
</div>
<div class="item">
<img src="./imgs/2.jpg" alt="2" />
</div>
<div class="item">
<img src="./imgs/3.jpg" alt="3" />
</div>
<div class="item">
<img src="./imgs/1.jpg" alt="1" />
</div>
<div class="item">
<img src="./imgs/2.jpg" alt="2" />
</div>
<div class="item">
<img src="./imgs/3.jpg" alt="3" />
</div>
<div class="item">
<img src="./imgs/1.jpg" alt="1" />
</div>
<div class="item">
<img src="./imgs/2.jpg" alt="2" />
</div>
<div class="item">
<img src="./imgs/3.jpg" alt="3" />
</div>
<div class="item">
<img src="./imgs/1.jpg" alt="1" />
</div>
<div class="item">
<img src="./imgs/2.jpg" alt="2" />
</div>
<div class="item">
<img src="./imgs/3.jpg" alt="3" />
</div>
<div class="item">
<img src="./imgs/1.jpg" alt="1" />
</div>
<div class="item">
<img src="./imgs/2.jpg" alt="2" />
</div>
<div class="item">
<img src="./imgs/3.jpg" alt="3" />
</div>
<div class="item">
<img src="./imgs/1.jpg" alt="1" />
</div>
<div class="item">
<img src="./imgs/2.jpg" alt="2" />
</div>
<div class="item">
<img src="./imgs/3.jpg" alt="3" />
</div>
</div>
</body>
</html>

展示效果如下


column.png


flex 弹性布局实现瀑布流



flex 实现瀑布流需要将最外层元素设置为 display: flex,使用弹性布局


flex-flow:column wrap 使其纵向排列并且换行换行


设置 height: 100vh 填充屏幕的高度,也可以设置为单位为 px 的高度,来容纳子元素。


每一列的宽度可用 calc 函数来设置,即 width: calc(100%/3 - 20px)。分成等宽的 3 列减掉左右两遍的 margin 距离。



<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>瀑布流布局-flex</title>
<style>
.box {
display: flex;
flex-flow: column wrap;
height: 100vh;
}
.item {
margin: 10px;
width: calc(100%/3 - 20px);
}
.item img{
width: 100%;
height:100%;
}
</style>
</head>
<body>
<div class="box">
<div class="item">
<img src="./imgs/1.jpg" alt="1" />
</div>
<div class="item">
<img src="./imgs/2.jpg" alt="2" />
</div>
<div class="item">
<img src="./imgs/3.jpg" alt="3" />
</div>
<div class="item">
<img src="./imgs/1.jpg" alt="1" />
</div>
<div class="item">
<img src="./imgs/2.jpg" alt="2" />
</div>
<div class="item">
<img src="./imgs/3.jpg" alt="3" />
</div>
<div class="item">
<img src="./imgs/1.jpg" alt="1" />
</div>
<div class="item">
<img src="./imgs/2.jpg" alt="2" />
</div>
<div class="item">
<img src="./imgs/3.jpg" alt="3" />
</div>
<div class="item">
<img src="./imgs/1.jpg" alt="1" />
</div>
<div class="item">
<img src="./imgs/2.jpg" alt="2" />
</div>
<div class="item">
<img src="./imgs/3.jpg" alt="3" />
</div>
<div class="item">
<img src="./imgs/1.jpg" alt="1" />
</div>
</div>
</body>
</html>

展示效果如下


flex.png



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

收起阅读 »

学透CSS-:focus-within 仿掘金登录小人动画

兼容性 作为:focus的好兄弟,在兼容性上也还是不错的。主流的浏览器基本都已经支持这个属性。 :focus-within 和 :focus 的区 :focus-within 表示一个元素自身获取焦点,以及子元素获取焦点后的效果。 :focus 表...
继续阅读 »

兼容性


作为:focus的好兄弟,在兼容性上也还是不错的。主流的浏览器基本都已经支持这个属性。
image.png


:focus-within 和 :focus 的区


:focus-within 表示一个元素自身获取焦点,以及子元素获取焦点后的效果。


:focus 表示元素自身获取到焦点后的效果。


示例


定义一个form表单,背景颜色是green。


form{          
padding: 50px;
background-color:green ;
}
<form action="">
<input type="text">
</form>

image.png


定义获取焦点后的效果


form:focus-within{
background-color: aqua;
}
input:focus{
background-color: red;
}

当input标签获取到焦点后,背景颜色变成了red,同时form的背景颜色变成aqea
image.png


应用场景- form表单输入(掘金登录页面)


掘金在登录输入密码的时候,这个小人会挡住自己的眼睛,有很多作者用各种方法实现这个效果,:focu-within有同样可以实现这个效果。
image.png


首先实现登陆前的画面(比较丑)


<div class="login">
<form action="">
<div class="panfish"></div>
<div><label for=""> 账号</label> <input type="text" /></div>
<div><label for=""> 密码</label> <input type="text" /></div>
</form>
</div>

.login {
position: relative;
padding: 2rem;
width: 20rem;
font-size: 1.167rem;
background-color: #fff;
border-radius: 2px;
box-sizing: border-box;
}
.panfish {
background: url(https://lf3-cdn-tos.bytescm.com/obj/static/xitu_juejin_web/ad7fa76844a2df5c03151ead0ce65ea6.svg);
z-index: 1;
padding-top: 50px;
width: 20rem;
height:50px;
position: absolute;
background-repeat: no-repeat;

top: -60px;
}
input:focus {
background-color: red;
}

image.png


使用:fous-within


form:focus-within > .panfish {
background: url(https://lf3-cdn-tos.bytescm.com/obj/static/xitu_juejin_web/4f6f6f316cde4398d201cd67e44ddea3.svg);
background-repeat: no-repeat;

}

获取焦点后的效果


image.png


GIF


focuswithin.gif


作者:前端picker
链接:https://juejin.cn/post/7012171045155110942

收起阅读 »

给女友写的,每日自动推送暖心消息

起因是因为刷到一则给女友发的每日提醒消息的沸点,每天自动定时发送消息,感觉很有趣,刚好最近在学习egg,里面有用到定时任务,于是决定尝试一把 egg 实现 环境准备 操作系统:支持 macOS,Linux,Windows 运行环境:建议选择 node LTS ...
继续阅读 »

起因是因为刷到一则给女友发的每日提醒消息的沸点,每天自动定时发送消息,感觉很有趣,刚好最近在学习egg,里面有用到定时任务,于是决定尝试一把 egg 实现


环境准备


操作系统:支持 macOS,Linux,Windows


运行环境:建议选择 node LTS 版本,最低要求 8.x。


创建egg项目和目录结构介绍


快速入门


目录结构


运行


本地开发


$ npm i
$ npm run dev
$ open http://localhost:7001/

部署生产


$ npm start
$ npm stop

控制器


class HomeController extends Controller {
async send() {
const { ctx, app } = this;
ctx.body = app.config;
const result = await ctx.service.sendmsg.sendOut();
ctx.logger.info('主动触发,发送模板消息 结果: %j', result);
ctx.body = result;
ctx.set('Content-Type', 'application/json');
}
}

service服务层


 // 时间处理
const moment = require('moment');
class sendmsg extends Service {
// 发送模板消息给媳妇儿
async sendOut() {
const { ctx, app } = this;
const token = await this.getToken();
const data = await this.getTemplateData();
ctx.logger.info('获取token 结果: %j', token);
// 模板消息接口文档
const users = app.config.weChat.users;
const promise = users.map(id => {
ctx.logger.info('--------------开始发送每日提醒-----------------------------------------------: %j', id);
data.touser = id;
return this.toWechart(token, data);
});
const results = await Promise.all(promise);
ctx.logger.info('--------------结束发送每日提醒->结果-----------------------------------------------: %j', results);
return results;
}
// 通知微信接口
async toWechart(token, data) {
...
}
// 获取token
async getToken() {
...
}
// 组装模板消息数据
async getTemplateData() {
...
}
// 获取天气
async getWeather(city = '深泽') {
...
}
// 获取 下次发工资 还有多少天
getWageDay() {
...
}
// 获取距离 下次结婚纪念日还有多少天
getMarryDay() {
...
}
// 获取 距离 下次生日还有多少天
getbirthday() {
...
}
// 获取 相恋天数
getLoveDay() {
...
}
// 获取 相恋几年了
getLoveYear() {
...
}
// 获取是第几个生日
getBirthYear() {
...
}
// 获取是第几个结婚纪念日
getMarryYear() {
...
}
// 获取 每日一句
async getOneSentence() {
...
}
// 获取时间日期
getDatetime() {
...
}
}

发送模板消息


  async toWechart(token, data) {
// 模板消息接口文档
const url = 'https://api.weixin.qq.com/cgi-bin/message/template/send?access_token=' + token;
const result = await this.ctx.curl(url, {
method: 'POST',
data,
dataType: 'json',
headers: {
'Content-Type': 'application/json',
},
});
return result;
}

获取Access token


  async getToken() {
const { app } = this;
const url = 'https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=' + app.config.weChat.appld + '&secret=' + app.config.weChat.secret;
const result = await this.ctx.curl(url, {
method: 'get',
dataType: 'json',
});
if (result.status === 200) {
return result.data.access_token;
}
}

组装模板消息数据


  async getTemplateData() {
const { app } = this;
// 判断所需 模板
// 发工资模板 getWageDay == 0 wageDay
// 结婚纪念日模板 getMarryDay == 0 marry
// 生日 模板 getbirthday == 0 birthday
// 正常模板 daily

const wageDay = this.getWageDay();
const marry = this.getMarryDay();
const birthday = this.getbirthday();
const data = {
topcolor: '#FF0000',
data: {},
};
// 发工资模板
if (!wageDay) {
data.template_id = app.config.weChat.wageDay;
data.data = {
dateTime: {
value: this.getDatetime(),
color: '#cc33cc',
},
};
} else if (!marry) {
// 结婚纪念日模板
data.template_id = app.config.weChat.marry;
data.data = {
dateTime: {
value: this.getDatetime(),
color: '#cc33cc',
},
anniversary: {
value: this.getMarryYear(),
color: '#ff3399',
},
year: {
value: this.getLoveYear(),
color: '#ff3399',
},
};
} else if (!birthday) {
// 生日模板
data.template_id = app.config.weChat.birthday;
data.data = {
dateTime: {
value: this.getDatetime(),
color: '#cc33cc',
},
individual: {
value: this.getBirthYear(),
color: '#ff3399',
},
};
} else {
// 正常模板
data.template_id = app.config.weChat.daily;
// 获取天气
const getWeather = await this.getWeather();
// 获取每日一句
const message = await this.getOneSentence();
data.data = {
dateTime: {
value: this.getDatetime(),
color: '#cc33cc',
},
love: {
value: this.getLoveDay(),
color: '#ff3399',
},
wage: {
value: wageDay,
color: '#66ff00',
},
birthday: {
value: birthday,
color: '#ff0033',
},
marry: {
value: marry,
color: '#ff0033',
},
wea: {
value: getWeather.wea,
color: '#33ff33',
},
tem: {
value: getWeather.tem,
color: '#0066ff',
},
airLevel: {
value: getWeather.air_level,
color: '#ff0033',
},
tem1: {
value: getWeather.tem1,
color: '#ff0000',
},
tem2: {
value: getWeather.tem2,
color: '#33ff33',
},
win: {
value: getWeather.win,
color: '#3399ff',
},
message: {
value: message,
color: '#8C8C8C',
},
};
}
return data;
}

获取天气


  async getWeather(city = '石家庄') {
const { app } = this;
const url = 'https://www.tianqiapi.com/api?unescape=1&version=v6&appid=' + app.config.weather.appid + '&appsecret=' + app.config.weather.appsecret + '&city=' + city;
const result = await this.ctx.curl(url, {
method: 'get',
dataType: 'json',
});
console.log(result.status);
// "wea": "多云",
// "tem": "27", 实时温度
// "tem1": "27", 高温
// "tem2": "17", 低温
// "win": "西风",
// "air_level": "优",
if (result && result.status === 200) {
return result.data;
}
return {
city,
wea: '未知',
tem: '未知',
tem1: '未知',
tem2: '未知',
win: '未知',
win_speed: '未知',
air_level: '未知',
};
}

获取 下次发工资 还有多少天


  getWageDay() {
const { app } = this;
const wage = app.config.time.wageDay;
// 获取日期 day
// 如果在 wage号之前或等于wage时 那么就用 wage-day
// 如果在 wage号之后 那么就用 wage +(当前月总天数-day)
// 当日 日期day
const day = moment().date();
// 当月总天数
const nowDayTotal = moment().daysInMonth();
// // 下个月总天数
// const nextDayTotal = moment().month(moment().month() + 1).daysInMonth();
let resultDay = 0;
if (day <= wage) {
resultDay = wage - day;
} else {
resultDay = wage + (nowDayTotal - day);
}
return resultDay;
}

获取距离 下次结婚纪念日还有多少天


  getMarryDay() {
const { app } = this;
const marry = app.config.time.marry;
// 获取当前时间戳
const now = moment(moment().format('YYYY-MM-DD')).valueOf();
// 获取纪念日 月-日
const mmdd = moment(marry).format('-MM-DD');
// 获取当年
const y = moment().year();
// 获取今年结婚纪念日时间戳
const nowTimeNumber = moment(y + mmdd).valueOf();
// 判断 今天的结婚纪念日 有没有过,如果已经过去(now>nowTimeNumber),resultMarry日期为明年的结婚纪念日
// 如果还没到,则 结束日期为今年的结婚纪念日
let resultMarry = nowTimeNumber;
if (now > nowTimeNumber) {
// 获取明年纪念日
resultMarry = moment((y + 1) + mmdd).valueOf();
}
return moment(moment(resultMarry).format()).diff(moment(now).format(), 'day');
}

获取 距离 下次生日还有多少天



getbirthday() {
const { app } = this;
const birthday = app.config.time.birthday[moment().year()];
// 获取当前时间戳
const now = moment(moment().format('YYYY-MM-DD')).valueOf();
// 获取纪念日 月-日
const mmdd = moment(birthday).format('-MM-DD');
// 获取当年
const y = moment().year();
// 获取今年生日 时间戳
const nowTimeNumber = moment(y + mmdd).valueOf();
// 判断 生日 有没有过,如果已经过去(now>nowTimeNumber),resultBirthday日期为明年的生日 日期
// 如果还没到,则 结束日期为今年的目标日期
let resultBirthday = nowTimeNumber;
if (now > nowTimeNumber) {
// 获取明年目标日期
resultBirthday = moment(app.config.time.birthday[y + 1]).valueOf();
}
return moment(moment(resultBirthday).format()).diff(moment(now).format(), 'day');
}

获取 相恋天数


  getLoveDay() {
const { app } = this;
const loveDay = app.config.time.love;
return moment(moment().format('YYYY-MM-DD')).diff(loveDay, 'day');
}

获取 相恋几年了


  getLoveYear() {
const { app } = this;
const loveDay = app.config.time.love;
return moment().year() - moment(loveDay).year();
}

获取是第几个生日


  getBirthYear() {
const { app } = this;
const birthYear = app.config.time.birthYear;
return moment().year() - birthYear;
}

获取是第几个结婚纪念日


  getMarryYear() {
const { app } = this;
const marry = app.config.time.marry;
return moment().year() - moment(marry).year();
}

获取 每日一句


  async getOneSentence() {
const url = 'https://v1.hitokoto.cn/';
const result = await this.ctx.curl(url, {
method: 'get',
dataType: 'json',
});
if (result && result.status === 200) {
return result.data.hitokoto;
}
return '今日只有我爱你!';
}

获取时间日期


  getDatetime() {
console.log('moment().weekday()', moment().weekday());
const week = {
1: '星期一',
2: '星期二',
3: '星期三',
4: '星期四',
5: '星期五',
6: '星期六',
0: '星期日',
};
return moment().format('YYYY年MM月DD日 ') + week[moment().weekday()];
}


定时任务和主动触发


定时任务


设置规则 请参考文档


└── app
└── schedule
└── update_cache.js

class UpdateCache extends Subscription {
// 通过 schedule 属性来设置定时任务的执行间隔等配置
static get schedule() {
return {
cron: '0 30 7 * * *', // 每天的7点30分0秒执行
// interval: '1m', // 1 分钟间隔
type: 'all', // 指定所有的 worker 都需要执行
};
}

// subscribe 是真正定时任务执行时被运行的函数
async subscribe() {
const { ctx } = this;
const result = await ctx.service.sendmsg.send();
ctx.logger.info('定时任务执行消息提醒 结果: %j', result);
}
}

日志中 可以查看 定时任务的 执行记录


└── logs
└── serves
└── serves-web

主动发送


请求或浏览器访问 http://localhost:7001/send


image


配置文件说明


└── logs
└── config.default.js

天气秘钥


注册地址


// 天气接口配置
config.weather = {
appid: '*******',
appsecret: '*******',
};

特殊 时间点设置


下方是 birthday生日,因老家都是过阴历生日,不好处理,暂时写死的


// 时间
config.time = {
wageDay: 15, // 工资日
love: '2017-06-09', // 相爱日期
marry: '2021-11-27', // 结婚纪念日
birthday: {
2021: '2021-04-17',
2022: '2022-04-06',
2023: '2023-04-25',
2024: '2024-04-14',
2025: '2025-04-03',
2026: '2026-04-22',
}, // 每年生日 阳历
birthYear: '1995-03-06',
};

微信公众号 配置


因个人只能申请订阅号,而订阅号不支持发送模板消息,所以在此使用的测试的微信公众号,有微信号都可以申请,免注册,扫码登录


无需公众帐号、快速申请接口测试号


直接体验和测试公众平台所有高级接口


申请地址


// 测试 微信公众号
config.weChat = {
appld: '**********',
secret: '**********',
// 用户的openid
users: [
'**********************',
'**********************',
'**********************',
'**********************'
],
daily: '************', // 普通模板
marry: ''************',', // 结婚纪念日模板
wageDay: ''************',', // 工资日模板
birthday: ''************',', // 生日模板
};

微信消息模板


这个需要在 上文提到的 微信公众平台测试账号 单独设置


以下是 我用的模板


正常模板


{{dateTime.DATA}}
今天是 我们相恋的第{{love.DATA}}天
距离上交工资还有{{wage.DATA}}天
距离你的生日还有{{birthday.DATA}}天
距离我们结婚纪念日还有{{marry.DATA}}天
今日天气 {{wea.DATA}}
当前温度 {{tem.DATA}}度
最高温度 {{tem1.DATA}}度
最低温度 {{tem2.DATA}}度
空气质量 {{airLevel.DATA}}
风向 {{win.DATA}}
每日一句
{{message.DATA}}

发工资模板


{{dateTime.DATA}}
老婆大人,今天要发工资了,预计晚九点前会准时上交,记得查收!

生日 模板


{{dateTime.DATA}}
听说今天是你人生当中第 {{individual.DATA}} 个生日?天呐,
我差点忘记!因为岁月没有在你脸上留下任何痕迹。
尽管,日历告诉我:你又涨了一岁,但你还是那个天真可爱的小妖女,生日快乐!

结婚纪念日


{{dateTime.DATA}}
今天是结婚{{anniversary.DATA}}周年纪念日,在一起{{year.DATA}}年了,
经历了风风雨雨,最终依然走在一起,很幸运,很幸福!我们的小家庭要一直幸福下去。

展示效果


d086745c18c811ef17488c004f64cb0.jpg


81c079890d8acd86cf02aeb22c1ff4b.jpg


0fb7faaa548c212ae6c31bf5d9ce816.jpg


作者:iwhao
链接:https://juejin.cn/post/7012171027790692388

收起阅读 »

如何小程序上绘制树状图

前言 现有的移动端图可视化技术有Antv旗下的F2、F6。F2主要专注于数据分析的统计图,而F6专注与各种场景的关系图。两者各有侧重。F6 是一个简单、易用、完备的移动端图可视化引擎,它在高定制能力的基础上,提供了一系列设计优雅、便于使用的图可视化解决方案。能...
继续阅读 »

前言


现有的移动端图可视化技术有Antv旗下的F2、F6。F2主要专注于数据分析的统计图,而F6专注与各种场景的关系图。两者各有侧重。F6 是一个简单、易用、完备的移动端图可视化引擎,它在高定制能力的基础上,提供了一系列设计优雅、便于使用的图可视化解决方案。能帮助开发者搭建属于自己的图可视化、图分析、或图编辑器应用。如果您希望将内容通过流程图、知识图谱、思维导图等形式进行输出,并希望可以方便的实现对图的操控,那么建议您一定要尝试一下F6。



欢迎star和提交issue


github.com/antvis/F6



什么是树图?


树图,表现形式如下,


具体原理可以参考 emr.cs.iit.edu/~reingold/t… ,由根节点不断的派生,形成一个树状结构,是一种可以很好表达层级关系的可视化方法。
举个例子:
Kapture 2021-09-22 at 23.40.34.gif


什么场景下使用































































名称应用
分解树在人口调查中,将人口样本分解为人口统计信息
关键质量特性树将顾客的需求转化为产品的可测量参数和过程特性
决策树或逻辑图绘制出思维过程以便于决策
树干图在产品的设计和开发阶段,用于识别产品的特性
故障树分析识别故障的潜在原因
装配图在制造过程中,描绘产品零部件的装配
方法-方法图解决问题
工作或任务分析识别一项工作或任务的要求
组织图识别管理和汇报间的关联水平
过程决策程序图确定潜在的问题和复杂计划中的对策
需求测量树确定顾客、需求以及对测量产品或服务的测量
原因一原因图或five whys识别问题的根本原因
生产分类结构(WBS)识别项目的所有方面,分解成具体工作包水平

可见树图在实际场景中有很多应用,不论是在日常生活中,还是在生产中都有多种用途。我们最熟悉的脑图(mind map)也是树图的一种形式,


F6中如何绘制


演示示例可以参考f6.antv.vision/zh/docs/exa…
本节代码已经开源,感兴趣可以查看



支付宝中


首先安装


npm install @antv/f6 @antv/f6-alipay -S


index.json


{
"defaultTitle": "紧凑树",
"usingComponents": {
"f6-canvas": "@antv/f6-alipay/es/container/container"
}
}


index.js


import F6 from '@antv/f6';
import TreeGraph from '@antv/f6/dist/extends/graph/treeGraph';

import data from './data.js';

/**
* 紧凑树
*/

Page({
canvas: null,
ctx: null, // 延迟获取的2d context
renderer: '', // mini、mini-native等,F6需要,标记环境
isCanvasInit: false, // canvas是否准备好了
graph: null,

data: {
width: 375,
height: 600,
pixelRatio: 2,
forceMini: false,
},

onLoad() {
// 注册自定义树,节点等
F6.registerGraph('TreeGraph', TreeGraph);

// 同步获取window的宽高
const { windowWidth, windowHeight, pixelRatio } = my.getSystemInfoSync();

this.setData({
width: windowWidth,
height: windowHeight,
pixelRatio,
});
},

/**
* 初始化canvas回调,缓存获得的context
* @param {*} ctx 绘图context
* @param {*} rect 宽高信息
* @param {*} canvas canvas对象,在render为mini时为null
* @param {*} renderer 使用canvas 1.0还是canvas 2.0,mini | mini-native
*/
handleInit(ctx, rect, canvas, renderer) {
this.isCanvasInit = true;
this.ctx = ctx;
this.renderer = renderer;
this.canvas = canvas;
this.updateChart();
},

/**
* canvas派发的事件,转派给graph实例
*/
handleTouch(e) {
this.graph && this.graph.emitEvent(e);
},

updateChart() {
const { width, height, pixelRatio } = this.data;

// 创建F6实例
this.graph = new F6.TreeGraph({
context: this.ctx,
renderer: this.renderer,
width,
height,
pixelRatio,
fitView: true,
modes: {
default: [
{
type: 'collapse-expand',
onChange: function onChange(item, collapsed) {
const model = item.getModel();
model.collapsed = collapsed;
return true;
},
},
'drag-canvas',
'zoom-canvas',
],
},
defaultNode: {
size: 26,
anchorPoints: [
[0, 0.5],
[1, 0.5],
],
},
defaultEdge: {
type: 'cubic-horizontal',
},
layout: {
type: 'compactBox',
direction: 'LR',
getId: function getId(d) {
return d.id;
},
getHeight: function getHeight() {
return 16;
},
getWidth: function getWidth() {
return 16;
},
getVGap: function getVGap() {
return 10;
},
getHGap: function getHGap() {
return 100;
},
},
});

this.graph.node(function(node) {
return {
label: node.id,
labelCfg: {
offset: 10,
position: node.children && node.children.length > 0 ? 'left' : 'right',
},
};
});

this.graph.data(data);
this.graph.render();
this.graph.fitView();
},
});


index.axml


<f6-canvas
width="{{width}}"
height="{{height}}"
forceMini="{{forceMini}}"
pixelRatio="{{pixelRatio}}"
onTouchEvent="handleTouch"
onInit="handleInit"
></f6-canvas>

微信中


首先安装


npm install @antv/f6-wx -S

@antv/f6-wx 由于微信对npm包不是很友好,所以我们封装了 @antv/f6-wx 帮助用户简化操作。


index.json


{
"defaultTitle": "紧凑树",
"usingComponents": {
"f6-canvas": "@antv/f6-wx/canvas/canvas"
}
}


index.wxml


<f6-canvas
width="{{width}}"
height="{{height}}"
forceMini="{{forceMini}}"
pixelRatio="{{pixelRatio}}"
bind:onTouchEvent="handleTouch"
bind:onInit="handleInit"
></f6-canvas>


index.js


import F6 from '@antv/f6-wx';
import TreeGraph from '@antv/f6-wx/extends/graph/treeGraph';

import data from './data.js';
Page({
canvas: null,
ctx: null, // 延迟获取的2d context
renderer: '', // mini、mini-native等,F6需要,标记环境
isCanvasInit: false, // canvas是否准备好了
graph: null,

data: {
width: 375,
height: 600,
pixelRatio: 2,
forceMini: false,
},

onLoad() {
// 注册自定义树,节点等
F6.registerGraph('TreeGraph', TreeGraph);
// 同步获取window的宽高
const { windowWidth, windowHeight, pixelRatio } = wx.getSystemInfoSync();

this.setData({
width: windowWidth * pixelRatio,
height: windowHeight * pixelRatio,
pixelRatio,
});
},

/**
* 初始化canvas回调,缓存获得的context
* @param {*} ctx 绘图context
* @param {*} rect 宽高信息
* @param {*} canvas canvas对象,在render为mini时为null
* @param {*} renderer 使用canvas 1.0还是canvas 2.0,mini | mini-native
*/
handleInit(event) {
const { ctx, rect, canvas, renderer } = event.detail;
this.isCanvasInit = true;
this.ctx = ctx;
this.renderer = renderer;
this.canvas = canvas;
this.updateChart();
},

/**
* canvas派发的事件,转派给graph实例
*/
handleTouch(e) {
this.graph && this.graph.emitEvent(e.detail);
},

updateChart() {
const { width, height, pixelRatio } = this.data;

// 创建F6实例
this.graph = new F6.TreeGraph({
context: this.ctx,
renderer: this.renderer,
width,
height,
pixelRatio,
fitView: true,
modes: {
default: [
{
type: 'collapse-expand', // 点击后展开/收缩
onChange: function onChange(item, collapsed) {
const model = item.getModel();
model.collapsed = collapsed;
return true;
},
},
'drag-canvas',
'zoom-canvas',
],
},
defaultNode: {
size: 26,
anchorPoints: [
[0, 0.5],
[1, 0.5],
],
},
defaultEdge: {
type: 'cubic-horizontal',
},
layout: {
type: 'compactBox',
direction: 'LR',
getId: function getId(d) {
return d.id;
},
getHeight: function getHeight() {
return 16;
},
getWidth: function getWidth() {
return 16;
},
getVGap: function getVGap() {
return 10;
},
getHGap: function getHGap() {
return 100;
},
},
});

this.graph.node(function(node) {
return {
label: node.id,
labelCfg: {
offset: 10,
position: node.children && node.children.length > 0 ? 'left' : 'right',
},
};
});

this.graph.data(data);
this.graph.render();
this.graph.fitView();
},
});



作者:AntCredit
链接:https://juejin.cn/post/7011374414394556452

收起阅读 »

Android-activity的启动流程

需要结合Application的启动流程。 juejin.cn/post/701209…//查看栈顶可见activity是否正等待 if (normalMode) { try { if (mStackSupervisor.at...
继续阅读 »


需要结合Application的启动流程。 juejin.cn/post/701209…

//查看栈顶可见activity是否正等待
if (normalMode) {
try {
if (mStackSupervisor.attachApplicationLocked(app)) {
didSomething = true;
}
} catch (Exception e) {
Slog.wtf(TAG, "Exception thrown launching activities in " + app, e);
badApp = true;
}
}

进入ActivityStackSupervisor中的

boolean attachApplicationLocked(ProcessRecord app) throws RemoteException {
final String processName = app.processName;
boolean didSomething = false;
for (int displayNdx = mActivityDisplays.size() - 1; displayNdx >= 0; --displayNdx) {
final ActivityDisplay display = mActivityDisplays.valueAt(displayNdx);
for (int stackNdx = display.getChildCount() - 1; stackNdx >= 0; --stackNdx) {
final ActivityStack stack = display.getChildAt(stackNdx);
if (!isFocusedStack(stack)) {
continue;
}
//从activityStack(Activity栈)把所有的activity添加给mTmpActivityList
stack.getAllRunningVisibleActivitiesLocked(mTmpActivityList);
//返回当前应用最顶端的activity
final ActivityRecord top = stack.topRunningActivityLocked();
final int size = mTmpActivityList.size();
//遍历所有的activity
for (int i = 0; i < size; i++) {
final ActivityRecord activity = mTmpActivityList.get(i);
f (activity.app == null && app.uid == activity.info.applicationInfo.uid
&& processName.equals(activity.processName)) {
try {
if (realStartActivityLocked(activity, app,
top == activity /* andResume */, true /* checkConfig */)) {
didSomething = true;
}
} catch (RemoteException e) {

throw e;
}
}
}
}
}
if (!didSomething) {
ensureActivitiesVisibleLocked(null, 0, !PRESERVE_WINDOWS);
}
return didSomething;
}

进入realStartActivityLocked方法

final boolean realStartActivityLocked(ActivityRecord r, ProcessRecord app,
boolean andResume, boolean checkConfig) throws RemoteException {
...
//创建activity启动事务
final ClientTransaction clientTransaction = ClientTransaction.obtain(app.thread,
r.appToken);
//添加回调
clientTransaction.addCallback(LaunchActivityItem.obtain(new Intent(r.intent),
System.identityHashCode(r), r.info,
mergedConfiguration.getGlobalConfiguration(),
mergedConfiguration.getOverrideConfiguration(), r.compat,
r.launchedFromPackage, task.voiceInteractor, app.repProcState, r.icicle,
r.persistentState, results, newIntents, mService.isNextTransitionForward(),
profilerInfo));

final ActivityLifecycleItem lifecycleItem;
if (andResume) {
lifecycleItem = ResumeActivityItem.obtain(mService.isNextTransitionForward());
} else {
lifecycleItem = PauseActivityItem.obtain();
}
clientTransaction.setLifecycleStateRequest(lifecycleItem);
//提交事务
mService.getLifecycleManager().scheduleTransaction(clientTransaction);
...
}

进入ClientLifecycleManager


void scheduleTransaction(ClientTransaction transaction) throws RemoteException {
final IApplicationThread client = transaction.getClient();
transaction.schedule();
if (!(client instanceof Binder)) {
// If client is not an instance of Binder - it's a remote call and at this point it is
// safe to recycle the object. All objects used for local calls will be recycled after
// the transaction is executed on client in ActivityThread.
transaction.recycle();
}
}

进入ClientTransaction这个类的schedule方法

public void schedule() throws RemoteException {
mClient.scheduleTransaction(this);
}

mClient其实就是IApplicationThread

public static ClientTransaction obtain(IApplicationThread client, IBinder activityToken) {
ClientTransaction instance = ObjectPool.obtain(ClientTransaction.class);
if (instance == null) {
instance = new ClientTransaction();
}
instance.mClient = client;
instance.mActivityToken = activityToken;

return instance;
}

回到ActivityThread中的scheduleTransaction方法中

public void scheduleTransaction(ClientTransaction transaction) throws RemoteException {
ActivityThread.this.scheduleTransaction(transaction);
}

进入activitythread父类ClientTransactionHandler中

void scheduleTransaction(ClientTransaction transaction) {
transaction.preExecute(this);
sendMessage(ActivityThread.H.EXECUTE_TRANSACTION, transaction);
}

回到activitythread中handlerMessage中处理

case EXECUTE_TRANSACTION:
final ClientTransaction transaction = (ClientTransaction) msg.obj;
mTransactionExecutor.execute(transaction);
if (isSystem()) {
// Client transactions inside system process are recycled on the client side
// instead of ClientLifecycleManager to avoid being cleared before this
// message is handled.
transaction.recycle();
}
// TODO(lifecycler): Recycle locally scheduled transactions.
break;

进入TransactionExecutor类中执行execute方法

public void execute(ClientTransaction transaction) {
final IBinder token = transaction.getActivityToken();
log("Start resolving transaction for client: " + mTransactionHandler + ", token: " + token);

executeCallbacks(transaction);

executeLifecycleState(transaction);
mPendingActions.clear();
log("End resolving transaction");
}

到executeCallbacks方法中

public void executeCallbacks(ClientTransaction transaction) {
final List<ClientTransactionItem> callbacks = transaction.getCallbacks();
if (callbacks == null) {
// No callbacks to execute, return early.
return;
}
log("Resolving callbacks");

final IBinder token = transaction.getActivityToken();
ActivityClientRecord r = mTransactionHandler.getActivityClient(token);

// In case when post-execution state of the last callback matches the final state requested
// for the activity in this transaction, we won't do the last transition here and do it when
// moving to final state instead (because it may contain additional parameters from server).
final ActivityLifecycleItem finalStateRequest = transaction.getLifecycleStateRequest();
final int finalState = finalStateRequest != null ? finalStateRequest.getTargetState()
: UNDEFINED;
// Index of the last callback that requests some post-execution state.
final int lastCallbackRequestingState = lastCallbackRequestingState(transaction);
//遍历事务管理器中的所有窗体请求对象
final int size = callbacks.size();
for (int i = 0; i < size; ++i) {
//获得的是LaunchActivityItem
final ClientTransactionItem item = callbacks.get(i);
log("Resolving callback: " + item);
final int postExecutionState = item.getPostExecutionState();
final int closestPreExecutionState = mHelper.getClosestPreExecutionState(r,
item.getPostExecutionState());
if (closestPreExecutionState != UNDEFINED) {
cycleToPath(r, closestPreExecutionState);
}
//进行窗体创建请求
item.execute(mTransactionHandler, token, mPendingActions);
item.postExecute(mTransactionHandler, token, mPendingActions);
if (r == null) {
// Launch activity request will create an activity record.
r = mTransactionHandler.getActivityClient(token);
}

if (postExecutionState != UNDEFINED && r != null) {
// Skip the very last transition and perform it by explicit state request instead.
final boolean shouldExcludeLastTransition =
i == lastCallbackRequestingState && finalState == postExecutionState;
cycleToPath(r, postExecutionState, shouldExcludeLastTransition);
}
}
}

进入到LaunchActivityItem中的execute方法

@Override
public void execute(ClientTransactionHandler client, IBinder token,
PendingTransactionActions pendingActions) {
Trace.traceBegin(TRACE_TAG_ACTIVITY_MANAGER, "activityStart");
//创建一个ActivityClientRecord对象,用于activity的实例化
ActivityClientRecord r = new ActivityClientRecord(token, mIntent, mIdent, mInfo,
mOverrideConfig, mCompatInfo, mReferrer, mVoiceInteractor, mState, mPersistentState,
mPendingResults, mPendingNewIntents, mIsForward,
mProfilerInfo, client);
//回调给activityThread
client.handleLaunchActivity(r, pendingActions, null /* customIntent */);
Trace.traceEnd(TRACE_TAG_ACTIVITY_MANAGER);
}

回到activityThread类中的handleLaunchActivity方法

public Activity handleLaunchActivity(ActivityClientRecord r,
PendingTransactionActions pendingActions, Intent customIntent) {
...
//根据传递过来的activityClientRecord创建一个activity
final Activity a = performLaunchActivity(r, customIntent);
...
}

进入performLaunchActivity方法

private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
...
try {
//通过反射创建activity对象
java.lang.ClassLoader cl = appContext.getClassLoader();
activity = mInstrumentation.newActivity(
cl, component.getClassName(), r.intent);
StrictMode.incrementExpectedActivityCount(activity.getClass());
r.intent.setExtrasClassLoader(cl);
r.intent.prepareToEnterProcess();
if (r.state != null) {
r.state.setClassLoader(cl);
}
} catch (Exception e) {
if (!mInstrumentation.onException(activity, e)) {
throw new RuntimeException(
"Unable to instantiate activity " + component
+ ": " + e.toString(), e);
}
}
...
}

进入Instrumentation类中的newActivity方法

public Activity newActivity(ClassLoader cl, String className,
Intent intent)
throws InstantiationException, IllegalAccessException,
ClassNotFoundException {
String pkg = intent != null && intent.getComponent() != null
? intent.getComponent().getPackageName() : null;
return getFactory(pkg).instantiateActivity(cl, className, intent);
}

进入AppComponentFactory类中

public @NonNull Activity instantiateActivity(@NonNull ClassLoader cl, @NonNull String className,
@Nullable Intent intent)
throws InstantiationException, IllegalAccessException, ClassNotFoundException {
return (Activity) cl.loadClass(className).newInstance();
}

到此activity就被创建出来了。 继续回到activityThread类中

activity.mCalled = false;
if (r.isPersistable()) {
//通过mInstrumentation调用activity的生命周期方法
mInstrumentation.callActivityOnCreate(activity, r.state, r.persistentState);
} else {
mInstrumentation.callActivityOnCreate(activity, r.state);
}

进入Instrumentation类中的callActivityOnCreate方法

public void callActivityOnCreate(Activity activity, Bundle icicle,
PersistableBundle persistentState) {
prePerformCreate(activity);
activity.performCreate(icicle, persistentState);
postPerformCreate(activity);
}

进入activity中的performCreate方法中

final void performCreate(Bundle icicle, PersistableBundle persistentState) {
mCanEnterPictureInPicture = true;
restoreHasCurrentPermissionRequest(icicle);
if (persistentState != null) {
onCreate(icicle, persistentState);
} else {
onCreate(icicle);
}
writeEventLog(LOG_AM_ON_CREATE_CALLED, "performCreate");
mActivityTransitionState.readState(icicle);

mVisibleFromClient = !mWindow.getWindowStyle().getBoolean(
com.android.internal.R.styleable.Window_windowNoDisplay, false);
mFragments.dispatchActivityCreated();
mActivityTransitionState.setEnterActivityOptions(this, getActivityOptions());


收起阅读 »

Kotlin系列三:空指针检查

Android系统上崩溃率最高的异常类型就是空指针异常(NullPointerException)。public void doStudy(Study study) { if (study != null) { study.readBo...
继续阅读 »

Android系统上崩溃率最高的异常类型就是空指针异常(NullPointerException)。

public void doStudy(Study study) {
if (study != null) {
study.readBooks();
study.doHomework();
}
}

这种java里常见的判空检查容易陷入判空地狱的灾难。Kotlin提供了很好的解决思路。

1 可空类型(?)

Kotlin在编译时就进行判空检查,这会导致代码变得相对难写些,因为你得实时考虑到对象的为空与否。

一个判空举例:

fun doStudy(study: Study) {
study.readBooks()
study.doHomework()
}

如果你尝试向doStudy()函数传入一个null参数,则会提示错误:

那么可为空的类型系统是什么样的呢?很简单,就是在类名的后面加上一个问号:

为什么会出现红色报错:由于我们将参数改成了可为空的Study?类型,此时调用参数的readBooks()和doHomework()方法都可能造成空指针异常过。如何解决呢:

fun doStudy(study: Study?) {
if (study != null) {
study.readBooks()
study.doHomework()
}
}

2 判空辅助工具

2.1 ?.操作符

?.操作符:当对象不为空时正常调用相应的方法,当对象为空时则什么都不做(相当于外部包裹了 !=null 的一个判断了):

fun doStudy(study: Study?) {
study?.readBooks()
study?.doHomework()
}

2.1 ?:操作符

?:操作符:操作符的左右两边都接收一个表达式,如果左边表达式的结果不为空就返回左边表达式的结果,否则就返回右边表达式的结果。

val c = if (a ! = null) {
a
} else {
b
}

这段代码的逻辑使用?:操作符就可以简化成:

val c = a ?: b

比如现在我们要编写一个函数用来获得一段文本的长度,使用传统的写法就可以这样写:

fun getTextLength(text: String?): Int {
if (text != null) {
return text.length
}
return 0
}

改进:

fun getTextLength(text: String?) = text?.length ?: 0

2.2 !!操作符

不过Kotlin的空指针检查机制也并非总是那么智能,有的时候我们可能从逻辑上已经将空指针异常处理了,但是Kotlin的编译器并不知道,这个时候它还是会编译失败。

观察如下的代码示例:

var content: String? = "hello"

fun main() {
if (content != null) {
printUpperCase()
}
}

fun printUpperCase() {
val upperCase = content.toUpperCase()
println(upperCase)
}

看上去好像逻辑没什么问题,但这段代码一定是无法运行的。因为printUpperCase()函数并不知道外部已经对content变量进行了非空检查,在调用toUpperCase()方法时,还认为这里存在空指针风险,从而无法编译通过。在这种情况下,如果我们想要强行通过编译,可以使用非空断言工具,写法是在对象的后面加上!!,如下所示:

fun printUpperCase() {
val upperCase = content!!.toUpperCase()
println(upperCase)
}

这种写法意在告诉Kotlin,我非常确信这里的对象不会为空,所以不用你来帮我做空指针检查了,如果出现问题,你可以直接抛出空指针异常,后果由我自己承担。

2.3 let函数

let函数属于Kotlin中的标准函数,这个函数提供了函数式API的编程接口,并将原始调用对象作为参数传递到Lambda表达式中。示例代码如下:

obj.let { obj2 ->
// 编写具体的业务逻辑
}
结合doStudy()函数:

fun doStudy(study: Study?) {
study?.readBooks()
study?.doHomework()
}

虽然这段代码我们通过?.操作符优化之后可以正常编译通过,但其实这种表达方式是有点啰嗦的,如果将这段代码准确翻译成使用if判断语句的写法,对应的代码如下:

fun doStudy(study: Study?) {
if (study != null) {
study.readBooks()
}
if (study != null) {
study.doHomework()
}
}

也就是说,本来我们进行一次if判断就能随意调用study对象的任何方法,但受制于?.操作符的限制,现在变成了每次调用study对象的方法时都要进行一次if判断。

这个时候就可以结合使用?.操作符和let函数来对代码进行优化了,如下所示:

fun doStudy(study: Study?) {
study?.let { stu ->
stu.readBooks()
stu.doHomework()
}
}

我来简单解释一下上述代码,?.操作符表示对象为空时什么都不做,对象不为空时就调用let函数,而let函数会将study对象本身作为参数传递到Lambda表达式中,此时的study对象肯定不为空了,我们就能放心地调用它的任意方法了。

另外还记得Lambda表达式的语法特性吗?当Lambda表达式的参数列表中只有一个参数时,可以不用声明参数名,直接使用it关键字来代替即可,那么代码就可以进一步简化成:

fun doStudy(study: Study?) {
study?.let {
it.readBooks()
it.doHomework()
}
}
收起阅读 »

iOS RXSwift 5.1

iOS
如何选择操作符?下面这个决策树可以帮助你找到需要的操作符。决策树我想要创建一个 Observable产生特定的一个元素:just经过一段延时:timer从一个序列拉取元素:from重复的产生某一个元素:repeatElement存在自定义逻辑:cre...
继续阅读 »

如何选择操作符?

下面这个决策树可以帮助你找到需要的操作符。


决策树

我想要创建一个 Observable

  • 产生特定的一个元素:just
    • 经过一段延时:timer
  • 从一个序列拉取元素:from
  • 重复的产生某一个元素:repeatElement
  • 存在自定义逻辑:create
  • 每次订阅时产生:deferred
  • 每隔一段时间,发出一个元素:interval
    • 在一段延时后:timer
  • 一个空序列,只有一个完成事件:empty
  • 一个任何事件都没有产生的序列:never

我想要创建一个 Observable 通过组合其他的 Observables

  • 任意一个 Observable 产生了元素,就发出这个元素:merge
  • 让这些 Observables 一个接一个的发出元素,当上一个 Observable 元素发送完毕后,下一个 Observable 才能开始发出元素:concat
  • 组合多个 Observables 的元素
    • 当每一个 Observable 都发出一个新的元素:zip
    • 当任意一个 Observable 发出一个新的元素:combineLatest

我想要转换 Observable 的元素后,再将它们发出来

  • 对每个元素直接转换:map
  • 转换到另一个 ObservableflatMap
    • 只接收最新的元素转换的 Observable 所产生的元素:flatMapLatest
    • 每一个元素转换的 Observable 按顺序产生元素:concatMap
  • 基于所有遍历过的元素: scan

我想要将产生的每一个元素,拖延一段时间后再发出:delay

我想要将产生的事件封装成元素发送出来

我想要忽略掉所有的 next 事件,只接收 completed 和 error 事件:ignoreElements

我想创建一个新的 Observable 在原有的序列前面加入一些元素:startWith

我想从 Observable 中收集元素,缓存这些元素之后在发出:buffer

我想将 Observable 拆分成多个 Observableswindow

  • 基于元素的共同特征:groupBy

我想只接收 Observable 中特定的元素

  • 发出唯一的元素:single

我想重新从 Observable 中发出某些元素

  • 通过判定条件过滤出一些元素:filter
  • 仅仅发出头几个元素:take
  • 仅仅发出尾部的几个元素:takeLast
  • 仅仅发出第 n 个元素:elementAt
  • 跳过头几个元素
    • 跳过头 n 个元素:skip
    • 跳过头几个满足判定的元素:skipWhileskipWhileWithIndex
    • 跳过某段时间内产生的头几个元素:skip
    • 跳过头几个元素直到另一个 Observable 发出一个元素:skipUntil
  • 只取头几个元素
    • 只取头几个满足判定的元素:takeWhiletakeWhileWithIndex
    • 只取某段时间内产生的头几个元素:take
    • 只取头几个元素直到另一个 Observable 发出一个元素:takeUntil
  • 周期性的对 Observable 抽样:sample
  • 发出那些元素,这些元素产生后的特定的时间内,没有新的元素产生:debounce
  • 直到元素的值发生变化,才发出新的元素:distinctUntilChanged
  • 在开始发出元素时,延时后进行订阅:delaySubscription

我想要从一些 Observables 中,只取第一个产生元素的 Observableamb

我想评估 Observable 的全部元素

  • 并且对每个元素应用聚合方法,待所有元素都应用聚合方法后,发出结果:reduce
  • 并且对每个元素应用聚合方法,每次应用聚合方法后,发出结果:scan

我想把 Observable 转换为其他的数据结构:as...

我想在某个 Scheduler 应用操作符:subscribeOn

我想要 Observable 发生某个事件时, 采取某个行动:do

我想要 Observable 发出一个 error 事件:error

  • 如果规定时间内没有产生元素:timeout

我想要 Observable 发生错误时,优雅的恢复

  • 如果规定时间内没有产生元素,就切换到备选 Observable :timeout
  • 如果产生错误,将错误替换成某个元素 :catchErrorJustReturn
  • 如果产生错误,就切换到备选 Observable :catchError
  • 如果产生错误,就重试 :retry

我创建一个 Disposable 资源,使它与 Observable 具有相同的寿命:using

我创建一个 Observable,直到我通知它可以产生元素后,才能产生元素:publish

  • 并且,就算是在产生元素后订阅,也要发出全部元素:replay
  • 并且,一旦所有观察者取消观察,他就被释放掉:refCount
  • 通知它可以产生元素了:connect


amb

在多个源 Observables 中, 取第一个发出元素或产生事件的 Observable,然后只发出它的元素

当你传入多个 Observables 到 amb 操作符时,它将取其中一个 Observable:第一个产生事件的那个 Observable,可以是一个 nexterror 或者 completed 事件。 amb 将忽略掉其他的 Observables

收起阅读 »

iOS RXSwift 4.9

iOS
Schedulers - 调度器Schedulers 是 Rx 实现多线程的核心模块,它主要用于控制任务在哪个线程或队列运行。如果你曾经使用过 GCD, 那你对以下代码应该不会陌生:// 后台取得数据,主线程处理结果 D...
继续阅读 »

Schedulers - 调度器

Schedulers 是 Rx 实现多线程的核心模块,它主要用于控制任务在哪个线程或队列运行。

如果你曾经使用过 GCD, 那你对以下代码应该不会陌生:

// 后台取得数据,主线程处理结果
DispatchQueue.global(qos: .userInitiated).async {
let data = try? Data(contentsOf: url)
DispatchQueue.main.async {
self.data = data
}
}

如果用 RxSwift 来实现,大致是这样的:

let rxData: Observable<Data> = ...

rxData
.subscribeOn(ConcurrentDispatchQueueScheduler(qos: .userInitiated))
.observeOn(MainScheduler.instance)
.subscribe(onNext: { [weak self] data in
self?.data = data
})
.disposed(by: disposeBag)

使用 subscribeOn

我们用 subscribeOn 来决定数据序列的构建函数在哪个 Scheduler 上运行。以上例子中,由于获取 Data 需要花很长的时间,所以用 subscribeOn 切换到 后台 Scheduler 来获取 Data。这样可以避免主线程被阻塞。

使用 observeOn

我们用 observeOn 来决定在哪个 Scheduler 监听这个数据序列。以上例子中,通过使用 observeOn 方法切换到主线程来监听并且处理结果。

一个比较典型的例子就是,在后台发起网络请求,然后解析数据,最后在主线程刷新页面。你就可以先用 subscribeOn 切到后台去发送请求并解析数据,最后用 observeOn 切换到主线程更新页面。


MainScheduler

MainScheduler 代表主线程。如果你需要执行一些和 UI 相关的任务,就需要切换到该 Scheduler 运行。

SerialDispatchQueueScheduler

SerialDispatchQueueScheduler 抽象了串行 DispatchQueue。如果你需要执行一些串行任务,可以切换到这个 Scheduler 运行。

ConcurrentDispatchQueueScheduler

ConcurrentDispatchQueueScheduler 抽象了并行 DispatchQueue。如果你需要执行一些并发任务,可以切换到这个 Scheduler 运行。

OperationQueueScheduler

OperationQueueScheduler 抽象了 NSOperationQueue

它具备 NSOperationQueue 的一些特点,例如,你可以通过设置 maxConcurrentOperationCount,来控制同时执行并发任务的最大数量。

Error Handling - 错误处理

一旦序列里面产出了一个 error 事件,整个序列将被终止。RxSwift 主要有两种错误处理机制:

  • retry - 重试
  • catch - 恢复

retry - 重试

retry 可以让序列在发生错误后重试:

// 请求 JSON 失败时,立即重试,
// 重试 3 次后仍然失败,就将错误抛出

let rxJson: Observable<JSON> = ...

rxJson
.retry(3)
.subscribe(onNext: { json in
print("取得 JSON 成功: \(json)")
}, onError: { error in
print("取得 JSON 失败: \(error)")
})
.disposed(by: disposeBag)

以上的代码非常直接 retry(3) 就是当发生错误时,就进行重试操作,并且最多重试 3 次。

retryWhen

如果我们需要在发生错误时,经过一段延时后重试,那可以这样实现:

// 请求 JSON 失败时,等待 5 秒后重试,

let retryDelay: Double = 5 // 重试延时 5 秒

rxJson
.retryWhen { (rxError: Observable<Error>) -> Observable<Int> in
return Observable.timer(retryDelay, scheduler: MainScheduler.instance)
}
.subscribe(...)
.disposed(by: disposeBag)

这里我们需要用到 retryWhen 操作符,这个操作符主要描述应该在何时重试,并且通过闭包里面返回的 Observable 来控制重试的时机:

.retryWhen { (rxError: Observable<Error>) -> Observable<Int> in
...
}

闭包里面的参数是 Observable<Error> 也就是所产生错误的序列,然后返回值是一个 Observable。当这个返回的 Observable 发出一个元素时,就进行重试操作。当它发出一个 error 或者 completed 事件时,就不会重试,并且将这个事件传递给到后面的观察者。

如果需要加上一个最大重试次数的限制:

// 请求 JSON 失败时,等待 5 秒后重试,
// 重试 4 次后仍然失败,就将错误抛出

let maxRetryCount = 4 // 最多重试 4 次
let retryDelay: Double = 5 // 重试延时 5 秒

rxJson
.retryWhen { (rxError: Observable<Error>) -> Observable<Int> in
return rxError.flatMapWithIndex { (error, index) -> Observable<Int> in
guard index < maxRetryCount else {
return Observable.error(error)
}
return Observable<Int>.timer(retryDelay, scheduler: MainScheduler.instance)
}
}
.subscribe(...)
.disposed(by: disposeBag)

我们这里要实现的是,如果重试超过 4 次,就将错误抛出。如果错误在 4 次以内时,就等待 5 秒后重试:

...
rxError.flatMapWithIndex { (error, index) -> Observable<Int> in
guard index < maxRetryCount else {
return Observable.error(error)
}
return Observable<Int>.timer(retryDelay, scheduler: MainScheduler.instance)
}
...

我们用 flatMapWithIndex 这个操作符,因为它可以给我们提供错误的索引数 index。然后用这个索引数判断是否超过最大重试数,如果超过了,就将错误抛出。如果没有超过,就等待 5 秒后重试。


catchError - 恢复

catchError 可以在错误产生时,用一个备用元素或者一组备用元素将错误替换掉:

searchBar.rx.text.orEmpty
...
.flatMapLatest { query -> Observable<[Repository]> in
...
return searchGitHub(query)
.catchErrorJustReturn([])
}
...
.bind(to: ...)
.disposed(by: disposeBag)

我们开头的 Github 搜索就用到了catchErrorJustReturn。当错误产生时,就返回一个空数组,于是就会显示一个空列表页。

你也可以使用 catchError,当错误产生时,将错误事件替换成一个备选序列:

// 先从网络获取数据,如果获取失败了,就从本地缓存获取数据

let rxData: Observable<Data> = ... // 网络请求的数据
let cahcedData: Observable<Data> = ... // 之前本地缓存的数据

rxData
.catchError { _ in cahcedData }
.subscribe(onNext: { date in
print("获取数据成功: \(date.count)")
})
.disposed(by: disposeBag)

Result

如果我们只是想给用户错误提示,那要如何操作呢?

以下提供一个最为直接的方案,不过这个方案存在一些问题:

// 当用户点击更新按钮时,
// 就立即取出修改后的用户信息。
// 然后发起网络请求,进行更新操作,
// 一旦操作失败就提示用户失败原因

updateUserInfoButton.rx.tap
.withLatestFrom(rxUserInfo)
.flatMapLatest { userInfo -> Observable<Void> in
return update(userInfo)
}
.observeOn(MainScheduler.instance)
.subscribe(onNext: {
print("用户信息更新成功")
}, onError: { error in
print("用户信息更新失败: \(error.localizedDescription)")
})
.disposed(by: disposeBag)

这样实现是非常直接的。但是一旦网络请求操作失败了,序列就会终止。整个订阅将被取消。如果用户再次点击更新按钮,就无法再次发起网络请求进行更新操作了。

为了解决这个问题,我们需要选择合适的方案来进行错误处理。例如,使用系统自带的枚举 Result

public enum Result<Success, Failure> where Failure : Error {
case success(Success)
case failure(Failure)
}

然后之前的代码需要修改成:

updateUserInfoButton.rx.tap
.withLatestFrom(rxUserInfo)
.flatMapLatest { userInfo -> Observable<Result<Void, Error>> in
return update(userInfo)
.map(Result.success) // 转换成 Result
.catchError { error in Observable.just(Result.failure(error)) }
}
.observeOn(MainScheduler.instance)
.subscribe(onNext: { result in
switch result { // 处理 Result
case .success:
print("用户信息更新成功")
case .failure(let error):
print("用户信息更新失败: \(error.localizedDescription)")
}
})
.disposed(by: disposeBag)

这样我们的错误事件被包装成了 Result.failure(Error) 元素,就不会终止整个序列。即便网络请求失败了,整个订阅依然存在。如果用户再次点击更新按钮,也是能够发起网络请求进行更新操作的。

另外你也可以使用 materialize 操作符来进行错误处理。这里就不详细介绍了,如你想了解如何使用 materialize 可以参考这篇文章 How to handle errors in RxSwift!

收起阅读 »

iOS RXSwift 4.9

iOS
Disposable - 可被清除的资源通常来说,一个序列如果发出了 error 或者 completed 事件,那么所有内部资源都会被释放。如果你需要提前释放这些资源或取消订阅的话,那么你可以对返回的 可被清...
继续阅读 »

Disposable - 可被清除的资源

通常来说,一个序列如果发出了 error 或者 completed 事件,那么所有内部资源都会被释放。如果你需要提前释放这些资源或取消订阅的话,那么你可以对返回的 可被清除的资源(Disposable) 调用 dispose 方法:

var disposable: Disposable?

override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)

self.disposable = textField.rx.text.orEmpty
.subscribe(onNext: { text in print(text) })
}

override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)

self.disposable?.dispose()
}

调用 dispose 方法后,订阅将被取消,并且内部资源都会被释放。通常情况下,你是不需要手动调用 dispose 方法的,这里只是做个演示而已。我们推荐使用 清除包(DisposeBag) 或者 takeUntil 操作符 来管理订阅的生命周期。

DisposeBag - 清除包

因为我们用的是 Swift ,所以我们更习惯于使用 ARC 来管理内存。那么我们能不能用 ARC 来管理订阅的生命周期了。答案是肯定了,你可以用 清除包(DisposeBag) 来实现这种订阅管理机制:

var disposeBag = DisposeBag()

override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)

textField.rx.text.orEmpty
.subscribe(onNext: { text in print(text) })
.disposed(by: self.disposeBag)
}

override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)

self.disposeBag = DisposeBag()
}

当 清除包 被释放的时候,清除包 内部所有 可被清除的资源(Disposable) 都将被清除。在输入验证中我们也多次看到 清除包 的身影:

var disposeBag = DisposeBag() // 来自父类 ViewController

override func viewDidLoad() {
super.viewDidLoad()

...

usernameValid
.bind(to: passwordOutlet.rx.isEnabled)
.disposed(by: disposeBag)

usernameValid
.bind(to: usernameValidOutlet.rx.isHidden)
.disposed(by: disposeBag)

passwordValid
.bind(to: passwordValidOutlet.rx.isHidden)
.disposed(by: disposeBag)

everythingValid
.bind(to: doSomethingOutlet.rx.isEnabled)
.disposed(by: disposeBag)

doSomethingOutlet.rx.tap
.subscribe(onNext: { [weak self] in self?.showAlert() })
.disposed(by: disposeBag)
}

这个例子中 disposeBag 和 ViewController 具有相同的生命周期。当退出页面时, ViewController 就被释放,disposeBag 也跟着被释放了,那么这里的 5 次绑定(订阅)也就被取消了。这正是我们所需要的。

takeUntil

另外一种实现自动取消订阅的方法就是使用 takeUntil 操作符,上面那个输入验证的演示代码也可以通过使用 takeUntil 来实现:

override func viewDidLoad() {
super.viewDidLoad()

...

_ = usernameValid
.takeUntil(self.rx.deallocated)
.bind(to: passwordOutlet.rx.isEnabled)

_ = usernameValid
.takeUntil(self.rx.deallocated)
.bind(to: usernameValidOutlet.rx.isHidden)

_ = passwordValid
.takeUntil(self.rx.deallocated)
.bind(to: passwordValidOutlet.rx.isHidden)

_ = everythingValid
.takeUntil(self.rx.deallocated)
.bind(to: doSomethingOutlet.rx.isEnabled)

_ = doSomethingOutlet.rx.tap
.takeUntil(self.rx.deallocated)
.subscribe(onNext: { [weak self] in self?.showAlert() })
}

这将使得订阅一直持续到控制器的 dealloc 事件产生为止。

注意⚠️:这里配图中所使用的 Observable 都是“热” Observable,它可以帮助我们理解订阅的生命周期。如果你想要了解 “冷热” Observable 之间的区别,可以参考官方文档 Hot and Cold Observables

收起阅读 »

iOS RXSwift 4.8

iOS
Operator - 操作符操作符可以帮助大家创建新的序列,或者变化组合原有的序列,从而生成一个新的序列。我们之前在输入验证例子中就多次运用到操作符。例如,通过 map 方法将输入的用户名,转换为用户名是否有效。然后用这个转化后来的序列来控...
继续阅读 »

Operator - 操作符

操作符可以帮助大家创建新的序列,或者变化组合原有的序列,从而生成一个新的序列。

我们之前在输入验证例子中就多次运用到操作符。例如,通过 map 方法将输入的用户名,转换为用户名是否有效。然后用这个转化后来的序列来控制红色提示语是否隐藏。我们还通过 combineLatest 方法,将用户名是否有效密码是否有效合并成两者是否同时有效。然后用这个合成后来的序列来控制按钮是否可点击。

这里 map 和 combineLatest 都是操作符,它们可以帮助我们构建所需要的序列。现在,我们再来看几个例子:

filter - 过滤

你可以用 filter 创建一个新的序列。这个序列只发出温度大于 33 度的元素。

map - 转换

你可以用 map 创建一个新的序列。这个序列将原有的 JSON 转换成 Model 。这种转换实际上就是解析 JSON 。

zip - 配对

你可以用 zip 来合成一个新的序列。这个序列将汉堡序列的元素和薯条序列的元素配对后,生成一个新的套餐序列。

如何使用操作符

使用操作符是非常容易的。你可以直接调用实例方法,或者静态方法:

  • 温度过滤

    // 温度
    let rxTemperature: Observable<Double> = ...

    // filter 操作符
    rxTemperature.filter { temperature in temperature > 33 }
    .subscribe(onNext: { temperature in
    print("高温:\(temperature)度")
    })
    .disposed(by: disposeBag)
  • 解析 JSON

    // JSON
    let json: Observable<JSON> = ...

    // map 操作符
    json.map(Model.init)
    .subscribe(onNext: { model in
    print("取得 Model: \(model)")
    })
    .disposed(by: disposeBag)
  • 合成套餐

    // 汉堡
    let rxHamburg: Observable<Hamburg> = ...
    // 薯条
    let rxFrenchFries: Observable<FrenchFries> = ...

    // zip 操作符
    Observable.zip(rxHamburg, rxFrenchFries)
    .subscribe(onNext: { (hamburg, frenchFries) in
    print("取得汉堡: \(hamburg) 和薯条:\(frenchFries)")
    })
    .disposed(by: disposeBag)

决策树

Rx 提供了充分的操作符来帮我们创建序列。当然如果内置操作符无法满足你的需求时,你还可以创建自定义的操作符。

如果你不确定该如何选择操作符,可以参考 决策树。它会引导你找出合适的操作符。

操作符列表

26个英文字母我都认识,可是连成一个句子我就不怎么认得了...

这里提供一个操作符列表,它们就好比是26个英文字母。你如果要将它们的作用全部都发挥出来,是需要学习如何将它们连成一个句子的:

收起阅读 »

Flutter 入门与实战(八十):使用GetX构建更优雅的页面结构

前言 App 的大部分页面都会涉及到数据加载、错误、无数据和正常几个状态,在一开始的时候我们可能数据获取的状态枚举用 if...else 或者 switch 来显示不同的 Widget,这种方式会显得代码很丑陋,譬如下面这样的代码: if (PersonalC...
继续阅读 »

前言


App 的大部分页面都会涉及到数据加载、错误、无数据和正常几个状态,在一开始的时候我们可能数据获取的状态枚举用 if...else 或者 switch 来显示不同的 Widget,这种方式会显得代码很丑陋,譬如下面这样的代码:


if (PersonalController.to.loadingStatus == LoadingStatus.loading) {
return Center(
child: Text('加载中...'),
);
}
if (PersonalController.to.loadingStatus == LoadingStatus.failed) {
return Center(
child: Text('请求失败'),
);
}
// 正常状态
PersonalEntity personalProfile = PersonalController.to.personalProfile;
return Stack(
...
);

这种情况实在是不够优雅,在 GetX 中提供了一种 StateMixin 的方式来解决这个问题。


StateMixin


StateMixin 是 GetX 定义的一个 mixin,可以在状态数据中混入页面数据加载状态,包括了如下状态:



  • RxStatus.loading():加载中;

  • RxStatus.success():加载成功;

  • RxStatus.error([String? message]):加载失败,可以携带一个错误信息 message

  • RxStatus.empty():无数据。


StateMixin 的用法如下:


class XXXController extends GetxController
with StateMixin<T> {
}

其中 T 为实际的状态类,比如我们之前一篇 PersonalEntity,可以定义为:


class PersonalMixinController extends GetxController
with StateMixin<PersonalEntity> {
}

然后StateMixin 提供了一个 change 方法用于传递状态数据和状态给页面。


void change(T? newState, {RxStatus? status})

其中 newState 是新的状态数据,status 就是上面我们说的4种状态。这个方法会通知 Widget 刷新。


GetView


GetX 提供了一个快捷的 Widget 用来访问容器中的 controller,即 GetViewGetView是一个继承 StatelessWidget的抽象类,实现很简单,只是定义了一个获取 controllerget 属性。


abstract class GetView<T> extends StatelessWidget {
const GetView({Key? key}) : super(key: key);

final String? tag = null;

T get controller => GetInstance().find<T>(tag: tag)!;

@override
Widget build(BuildContext context);
}

通过继承 GetView,就可以直接使用controller.obx构建界面,而 controller.obx 最大的特点是针对 RxStatus 的4个状态分别定义了四个属性:


Widget obx(
NotifierBuilder<T?> widget, {
Widget Function(String? error)? onError,
Widget? onLoading,
Widget? onEmpty,
})


  • NotifierBuilder<T?> widget:实际就是一个携带状态变量,返回正常状态界面的函数,NotifierBuilder<T?>的定义如下。通过这个方法可以使用状态变量构建正常界面。


typedef NotifierBuilder<T> = Widget Function(T state);


  • onError:错误时对应的 Widget构建函数,可以使用错误信息 error

  • onLoading:加载时对应的 Widget

  • onEmpty:数据为空时的 Widget



通过这种方式可以自动根据 change方法指定的 RxStatus 来构建不同状态的 UI 界面,从而避免了丑陋的 if...elseswitch 语句。例如我们的个人主页,可以按下面的方式来写,是不是感觉更清晰和清爽了?


class PersonalHomePageMixin extends GetView<PersonalMixinController> {
PersonalHomePageMixin({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return controller.obx(
(personalEntity) => _PersonalHomePage(personalProfile: personalEntity!),
onLoading: Center(
child: CircularProgressIndicator(),
),
onError: (error) => Center(
child: Text(error!),
),
onEmpty: Center(
child: Text('暂无数据'),
),
);
}
}

对应的PersonalMixinController的代码如下:


class PersonalMixinController extends GetxController
with StateMixin<PersonalEntity> {
final String userId;
PersonalMixinController({required this.userId});

@override
void onReady() {
getPersonalProfile(userId);
super.onReady();
}

void getPersonalProfile(String userId) async {
change(null, status: RxStatus.loading());
var personalProfile = await JuejinService().getPersonalProfile(userId);
if (personalProfile != null) {
change(personalProfile, status: RxStatus.success());
} else {
change(null, status: RxStatus.error('获取个人信息失败'));
}
}
}

Controller 的构建


从 GetView 的源码可以看到,Controller 是从容器中获取的,这就需要使用 GetX 的容器,在使用 Controller 前注册到 GetX 容器中。


Get.lazyPut<PersonalMixinController>(
() => PersonalMixinController(userId: '70787819648695'),
);

总结


本篇介绍了使用GetXStateMixin方式构建更优雅的页面结构,通过controller.obx 的参数配置不同状态对应不同的组件。可以根据 RxStatus 状态自动切换组件,而无需写丑陋的 if...elseswitch 语句。当然,使用这种方式的前提是需要在 GetX 的容器中构建 controller 对象,本篇源码已上传至:GetX 状态管理源码。实际上使用容器能够带来其他的好处,典型的应用就是依赖注入(Dependency Injection,简称DI),接下来我们会使用两篇来介绍依赖注入的概念和具体应用。


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

落地西瓜视频埋点方案,埋点从未如此简单

前言 目前,几乎每个商用应用都有数据埋点的需求。你的 App 是怎么做埋点的呢,有遇到让你 “难顶” 的问题吗? 在这篇文章里,我将带你建立数据埋点的基本认识,还会介绍西瓜视频团队的前端埋点方案,最后为你带来我的落地实现 EasyTrack。如果能帮上忙,请...
继续阅读 »

前言



  • 目前,几乎每个商用应用都有数据埋点的需求。你的 App 是怎么做埋点的呢,有遇到让你 “难顶” 的问题吗?

  • 在这篇文章里,我将带你建立数据埋点的基本认识,还会介绍西瓜视频团队的前端埋点方案,最后为你带来我的落地实现 EasyTrack。如果能帮上忙,请务必点赞加关注,这真的对我非常重要。




目录





1. 数据埋点概述


1.1 为什么要埋点?


“除了上帝,任何人都必须用数据说话”,在数据时代,使用数据驱动产品迭代已经称为行业共识。在分析应用数据之前,首先需要获得数据,这就需要前端或服务端进行数据埋点。


1.2 数据需求的工作流程


首先,你需要了解数据需求的工作流程,需求是如何产生,又是如何流转的,主要分为以下几个环节:



  • 1、需求产生: 产品需求引起产品形态变化,产生新的数据需求;

  • 2、事件设计: 数据产品设计埋点事件并更新数据字典文档,提出埋点评审;

  • 3、埋点开发: 开发进行数据埋点开发;

  • 4、埋点测试: 测试进行数据埋点测试,确保数据质量;

  • 5、数据消费: 数据分析师进行数据分析,推荐系统工程师进行模型训练,赋能产品运营决策。



1.3 数据消费的经典场景



























消费场景需求描述技术需求
渗透率分析统计 DAU/PV/UV/VV 等准确的上报时机
归因分析分析前因后果准确上报上下文 (如场景、会话、来源页面)
1. A / B 测试
2. 个性化推荐
分析用户特征、产品特征等准确上报事件属性

可以看到,在归因分析中,除了需要上报事件本身的属性之外,还需要上报事件产生时的上下文信息,例如当前页面、来源页面、会话等。


1.4 埋点数据采集的基本模型


数据采集是指在前端或服务端收集需要上报的事件属性的过程。为了满足复杂、高效的数据消费需求,需要科学合理地设计端侧的数据采集逻辑,基本可以总结为 “4W + 1H” 模型:





































模型描述举例
1、WHAT什么行为事件名
2、WHEN行为产生的时间时间戳
3、WHO行为产生的对象对象唯一标识 (例如用户 ID、设备 ID)
4、WHERE行为产生的环境设备所处的环境 (例如 IP、操作系统、网络)
5、HOW行为的特征上下文信息 (例如当前页面、来源页面、会话)



2. 如何实现数据埋点?


2.1 埋点方案总结


目前,业界已经存在多种埋点方案,主要分为全埋点、前端代码埋点和服务端代码埋点三种,优缺点和适用场景总结如下:































全埋点前端埋点服务端埋点
优势开发成本低完整采集上下文信息不依赖于前端版本
劣势数据量大,无法获取上下文数据,数据质量低前端开发成本较高服务端开发成本较高、获取上下文信息依赖于接口传值
适用场景通用基础事件(如启动/退出、浏览、点击)核心业务流程(如登录、注册、收藏、购买)核心业务结果事件(如支付成功)



  • 1、全埋点: 指通过编译时插桩、运行时动态代理等 AOP 手段实现自动埋点和上报,无须开发者手动进行埋点,因此也称为 “无埋点”;




  • 2、前端埋点: 指前端 (包括客户端) 开发者手动编码实现埋点,虽然可以通过埋点工具或者脚本简化埋点开发工作,但总体上还是需要手动操作;




  • 3、服务端埋点: 指服务端手动编码实现埋点,缺点是需要客户端需要侵入接口来保留上下文参数。




2.2 全埋点方案的局限性


表面上看,全埋点方案的优势很明显:客户端和服务端只需要一次开发,就能实现所有页面、所有路径的曝光和点击事件埋点,节省了研发人力,也不用担心埋点逻辑会侵入正常业务逻辑。然而,不可能存在完美的解决方案,全埋点方案还是存在一些局限性:




  • 1、资源消耗较大: 全场景上报会产生大量无用数据,网络传输、数据存储和数据计算需要消耗大量资源;




  • 2、页面稳定性要求较高: 需要保持页面视图结构相对稳定,一旦页面视图结果变化,历史录入的埋点数据就会失效;




  • 3、无法采集上下文信息: 无法采集事件产生时的上下文信息,也就无法满足复杂的数据消费需求。




2.3 埋点设计的整体方案


考虑的不同方案都存在优缺点,单纯采用一种埋点方案是不切实际的,需要根据不同业务场景和不同数据消费需要而采用不同的埋点方案:




  • 1、全埋点: 作为全局兜底方案,可以满足粗粒度的统计需求;




  • 2、前端埋点: 作为全埋点的补充方案,可以自定义埋点参数,主要处理核心业务流程事件,例如(如登录、注册、收藏、购买);




  • 3、服务端埋点: 核心业务结果事件,例如订单支付成功。






3. 前端埋点中的困难


3.1 一个简单的埋点场景


现在,我们通过一个具体的埋点场景,试着发现在做埋点需求时会遇到的困难或痛点。我直接使用西瓜视频中的一个埋点场景:



—— 图片引用自西瓜视频技术博客


这个产品场景很简单,左边是西瓜视频的推荐流列表,点击 “电影卡片” 会进入右边的 “电影详情页” 。两个页面中都有 “收藏按钮”,现在的数据需求是采集不同页面中 “收藏按钮” 的点击事件,以便分析用户收藏影片的行为,优化影片的推荐模型。



  • 1、在推荐列表页中上报点击事件:


“event_name" : "click_favorite", // 事件名
"cur_page" : "feed", // 当前页面
"video_id" : "123", // 影片 ID
"video_name" : "影片名", // 影片名
"video_type" : "1", // 影片类型
"$user_id" : "10000", // 用户 ID
"$device_id" : "abc" // 设备 ID
... // 其他预置属性


  • 2、在电影详情页中上报点击事件:


“event_name" : "click_favorite", // 事件名
"from_page" : "feed"
"cur_page" : "video_detail", // 当前页面
"video_id" : "123", // 影片 ID
"video_name" : "影片名", // 影片名
"video_type" : "1", // 影片类型
"$user_id" : "10000", // 用户 ID
"$device_id" : "abc" // 设备 ID
... // 其他预置属性

3.2 现状分析


理解了这个埋点场景之后,我们先梳理出目前遇到的困难:




  • 1、埋点参数分散: 需要上报的埋点参数位于不同 UI 容器或不同业务模块,代码跨度很大(例如:Activity、Fragment、ViewHolder、自定义 View);




  • 2、组件复用: 组件抽象复用后在多个页面使用(例如通用的 ViewHolder 或自定义 View);




  • 3、数据模型不一致: 不同场景 / 页面下描述状态的数据模型不一致,需要额外的转换适配过程(例如有的模型用 video_type 表示影片类型,另一些模型用 videoType 表示影片类型)。




3.3 评估标准


理解了问题和现状,现在我们开始尝试找到解决方案。为此,我们需要想清楚理想中的解决方案,应该满足什么标准:



  • 1、准确性: 这是核心目标,能够在保证不同场景 / 页面下准确收集埋点数据;

  • 2、简洁性: 使用方法尽可能简单,收敛模板代码;

  • 3、可用性: 尽可能高效稳定,不容易出错,性能开销小。


3.4 常规解决方案


1、逐级传递 —— 通过面向对象的关系逐级传递埋点参数:


通过 Android 框架支持的 Activity / Fragment 参数传递方式和面向对象程序设计,逐级将埋点参数传递到最深层的收藏按钮。例如:




  • 列表页: Activity -> ViewModel -> FeedFragment (推荐) -> Adapter -> ViewHolder (电影卡片) -> CollectButton (收藏按钮)




  • 详情页: Activity -> ViewModel -> DetailBottomFragment(底部功能区) -> CollectButton (收藏按钮)




缺点 (参数传递困难) :传递数据需要编写大量重复模板代码,工程代码膨胀,增大维护难度。再叠加上组件复用的情况,逐级传递会让代码复杂度非常高,很明显不是一个合理的解决方案。


2、Bean 传递 —— 在 Java Bean 中增加字段来收集埋点参数:


缺点 (违背单一职责原则):Java Bean 中侵入了与业务无关的埋点参数,同时会造成 Java Bean 数据冗余,增大维护难度。


3、全局单例 —— 通过全局单例对象来收集埋点参数:


这个方案与 “Bean 传递 ” 类似,区别在于埋点参数从 Java Bean 中移动到全局单例中,但缺点还是很明显:


缺点 (写入和清理时机):单例会被多个位置写入,一旦被覆盖就无法被恢复,容易导致上报错误;另外清理的时机也难以把握,清理过早会导致埋点参数丢失,清理过晚会污染后面的埋点事件。




4. 西瓜视频方案


理解了数据埋点开发中的困难,有没有什么方案可以简化埋点过程中的复杂度呢?我们来讨论下西瓜视频团队分享的一个思路:基于视图树收集埋点参数。




—— 图片引用自西瓜视频技术博客


通过分析数据与视图节点的关系可以发现,事件的埋点数据正好分布在视图树的不同节点中。当 “收藏按钮” 触发事件时,只需要沿着视图树逐级向上查找 (通过 View#getParent()) 就可以收集到所有数据。


并且,树的分支天然地支持为参数设置不同的值。例如 “推荐 Fragment” 需要上报 “channel : recomment”,而 “电影 Fragment” 需要上报 “channel : film”。因为 Fragment 的根布局对应有视图树中的不同节点,所以在不同 Fragment 中触发的事件最终收集到的 “channel” 参数值也就不同了。Nice~




5. EasyTrack 埋点框架


思路 Get 到了,现在我们来讨论如何应用这个思路来解决问题。贴心的我已经帮你实现为一个框架 EasyTrack。源码地址:github.com/pengxurui/E…


5.1 添加依赖



  • 1、依赖 JitPack 仓库


在项目级 build.gradle 声明远程仓库:


allprojects {
repositories {
google()
mavenCentral()
// JitPack 仓库
maven { url "https://jitpack.io" }
}
}


  • 2、依赖 EasyTrack 框架


在模块级 build.gradle 中依赖类库:


dependencies {
...
// 依赖 EasyTrack 框架
implementation 'com.github.pengxurui:EasyTrack:v1.0.1'
// 依赖 Kotlin 工具(非必须)
implementation 'com.github.pengxurui:KotlinUtil:1.0.1'
}

5.2 依附埋点参数到视图树


ITrackModel接口定义了一个数据填充能力,你可以创建它的实现类来定义一个数据节点,并在 fillTrackParams() 方法中声明参数。例如:MyGoodsViewHolder 实现了 ITrackMode 接口,在 fillTrackParams() 方法中声明参数(goods_id / goods_name)。


随后,通过 View 的扩展函数View.trackModel()将其依附到视图节点上。扩展函数 View.trackModel() 内部基于 View#setTag() 实现。


MyGoodsViewHolder.kt


class MyGoodsViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView), ITrackModel {

private var mItem: GoodsItem? = null

init {
// Java:EasyTrackUtilsKt.setTrackModel(itemView, this);
itemView.trackModel = this
}

override fun fillTrackParams(params: TrackParams) {
mItem?.let {
params.setIfNull("goods_id", it.id)
params.setIfNull("goods_name", it.goods_name)
}
}
}

EasyTrackUtils.kt


/**
* Attach track model on the view.
*/
var View.trackModel: ITrackModel?
get() = this.getTag(R.id.tag_id_track_model) as? ITrackModel
set(value) {
this.setTag(R.id.tag_id_track_model, value)
}

ITrackModel.kt


/**
* 定义数据填充能力
*/
interface ITrackModel : Serializable {
/**
* 数据填充
*/
fun fillTrackParams(params: TrackParams)
}

5.3 触发事件埋点


在需要埋点的地方,直接通过定义在 View 上的扩展函数 trackEvent(事件名)触发埋点事件,它会以该扩展函数的接收者对象为起点,逐级向上层视图节点收集参数。另外,它还有多个定义在 Activity、Fragment、ViewHolder 上的扩展函数,但最终都会调用到 View.trackEvent。


class MyGoodsViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
fun bind(item: GoodsItem) {
...
trackEvent(GOODS_EXPOSE)
}
}

EasyTrackUtils.kt


@JvmOverloads
fun Activity?.trackEvent(eventName: String, params: TrackParams? = null) =
findRootView(this)?.doTrackEvent(eventName, params)

@JvmOverloads
fun Fragment?.trackEvent(eventName: String, params: TrackParams? = null) =
this?.requireView()?.doTrackEvent(eventName, params)

@JvmOverloads
fun RecyclerView.ViewHolder?.trackEvent(eventName: String, params: TrackParams? = null) {
this?.itemView?.let {
if (null == it.parent) {
it.post { it.doTrackEvent(eventName, params) }
} else {
it.doTrackEvent(eventName, params)
}
}
}

@JvmOverloads
fun View?.trackEvent(eventName: String, params: TrackParams? = null): TrackParams? =
this?.doTrackEvent(eventName, params)

查看 logcat 日志,可以看到以下日志,显示埋点并没有生效。这是因为没有为 EasyTrack 配置埋点数据上报和统计分析的能力。


logcat 日志


EasyTrackLib: Try track event goods_expose, but the providers is Empty.

5.4 实现 ITrackProvider 接口


EasyTrack 的职责在于收集分散的埋点数据,本身没有提供埋点数据上报和统计分析的能力。因此,你需要实现 ITrackProvider 接口进行依赖注入。例如,这里模拟实现友盟数据埋点提供器,在 onInit() 方法中进行初始化,在 onEvent() 方法中调用友盟 SDK 事件上报方法。


MockUmengProvider.kt


/**
* 模拟友盟数据上报
*/
class MockUmengProvider : ITrackProvider() {

companion object {
const val TAG = "Umeng"
}

/**
* 是否启用
*/
override var enabled = true

/**
* 名称
*/
override var name = TAG

/**
* 初始化
*/
override fun onInit() {
Log.d(TAG, "Init Umeng provider.")
}

/**
* 执行事件上报
*/
override fun onEvent(eventName: String, params: TrackParams) {
Log.d(TAG, params.toString())
}
}

5.5 配置 EasyTrack


在应用初始化时,进行 EasyTrack 的初始化配置。我们可以将相关的初始化代码单独封装起来,例如:


StatisticsUtils.kt


// 模拟友盟数据统计提供器
val umengProvider by lazy {
MockUmengProvider()
}

// 模拟神策数据统计提供器
val sensorProvider by lazy {
MockSensorProvider()
}

/**
* 初始化 EasyTrack,在 Application 初始化时调用
*/
fun init(context: Context) {
configStatistics(context)
registerProviders(context)
}

/**
* 配置
*/
private fun configStatistics(context: Context) {
// 调试开关
EasyTrack.debug = BuildConfig.DEBUG
// 页面间参数映射
EasyTrack.referrerKeyMap = mapOf(
CUR_PAGE to FROM_PAGE,
CUR_TAB to FROM_TAB
)
}

/**
* 注册提供器
*/
private fun registerProviders(context: Context) {
EasyTrack.registerProvider(umengProvider)
EasyTrack.registerProvider(sensorProvider)
}

EventConstants.java


public static final String FROM_PAGE = "from_page";
public static final String CUR_PAGE = "cur_page";
public static final String FROM_TAB = "from_tab";
public static final String CUR_TAB = "cur_tab";


























配置类型描述
debugBoolean调试开关
referrerKeyMapMap<String,String>全局页面间参数映射
registerProvider()ITrackProvider底层数据埋点能力

以上步骤是 EasyTrack 的必选步骤,完成后重新执行 trackEvent() 后可以看到以下日志:


logcat 日志


/EasyTrackLib:  
onEvent:goods_expose
goods_id= 10000
goods_name = 商品名
Try track event goods_expose with provider Umeng.
Try track event goods_expose with provider Sensor.
------------------------------------------------------

5.6 页面间参数映射


上一节中有一个referrerKeyMap配置项,定义了全局的页面间参数映射。 举个例子,在分析不同入口的转化率时,不仅仅需要上报当前页面的数据,还需要上报来源页面的信息。这样我们才能分析用户经过怎样的路径来到当前页面,并最终触发了某个行为。


需要注意的是,来源页面的参数往往不能直接添加到当前页面的埋点参数中,这里一般会有一定的转换规则 / 映射关系。例如:来源页面的 cur_page 参数,在当前页面应该映射为 from_page 参数。 在这个例子里,我们配置的映射关系是:



  • 来源页面的 cur_page 映射为当前页面的 from_page;

  • 来源页面的 cur_tab 映射为当前页面的 from_tab。


因此,假设来源页面传递给当前页面的参数是 A,则当前页面在触发事件时的收集参数是 B:


A (来源页面):
{
"cur_page" : "list"
...
}

B (当前页面):
{
"cur_page" : "detail",
"from_page" : "list",
...
}

BaseTrackActivity 实现了页面间参数映射,你可以创建 BaseActivity 类并继承于 BaseTrackActivity,或者将其内部的逻辑迁移到你的 BaseActivity 中。这一步是可选的,如果你不使用页面间参数映射的特性,你那大可不必使用 BaseTrackActivity。



















操作描述
定义映射关系1、EasyTrack.referrerKeyMap 配置项
2、重写 BaseTrackActivity #referrerKeyMap() 方法
传递页面间参数Intent.referrerSnapshot(TrackParams) 扩展函数

MyGoodsDetailActivity.java


public class MyGoodsDetailActivity extends MyBaseActivity {

private static final String EXTRA_GOODS = "extra_goods";

public static void start(Context context, GoodsItem item, TrackParams params) {
Intent intent = new Intent(context, GoodsDetailActivity.class);
intent.putExtra(EXTRA_GOODS, item);
EasyTrackUtilsKt.setReferrerSnapshot(intent, params);
context.startActivity(intent);
}

@Nullable
@Override
protected String getCurPage() {
return GOODS_DETAIL_NAME;
}

@Nullable
@Override
public Map<String, String> referrerKeyMap() {
Map<String, String> map = new HashMap<>();
map.put(STORE_ID, STORE_ID);
map.put(STORE_NAME, STORE_NAME);
return map;
}
}

需要注意的是,BaseTrackActivity 不会将来源页面的全部参数都添加到当前页面的参数中,只有在全局 referrerKeyMap 配置项或 referrerKeyMap() 方法中定义了映射关系的参数,才会添加到当前页面。 例如:MyGoodsDetailActivity 继承于 BaseActivity,并重写 referrerKeyMap() 定义了感兴趣的参数(STORE_ID、STORE_NAME)。最终触发埋点时的日志如下:


logcat 日志


/EasyTrackLib:  
onEvent:goods_detail_expose
goods_id= 10000
goods_name = 商品名
store_id = 10000
store_name = 商店名
from_page = Recommend
cur_page = goods_detail
Try track event goods_expose with provider Umeng.
Try track event goods_expose with provider Sensor.
------------------------------------------------------

在一般的埋点模型中,每个 Activity (页面) 都有对应一个唯一的 page_id,因此你可以重写 fillTrackParams() 方法追加这些固定的参数。例如:MyBaseActivity 定义了 getCurPage() 方法,子类可以通过重写 getCurPage() 来设置 page_id。


MyBaseActivity.java


abstract class MyBaseActivity : BaseTrackActivity() {

@CallSuper
override fun fillTrackParams(params: TrackParams) {
super.fillTrackParams(params)
// 填充页面统一参数
getCurPage()?.also {
params.setIfNull(CUR_PAGE, it)
}
}

protected open fun getCurPage(): String? = null
}

5.7 TrackParams 参数容器


TrackParams 是 EasyTrack 收集参数的中间容器,最终会分发给 ITrackProvider 使用。



























方法描述
set(key: String, value: Any?)设置参数,无论无何都覆盖
setIfNull(key: String, value: Any?)设置参数,如果已经存在该参数则丢弃
get(key: String): String?获取参数值,参数不存在则返回 null
get(key: String, default: String?)获取参数值,参数不存在则返回默认值 default

5.8 使用 Kotlin 委托依附参数


如果你觉得每次定义 ITrackModel 数据节点后都需要调用 View.trackModel,你可以使用我定义的 Kotlin 委托 “跳过” 这个步骤,例如:


MyFragment.kt


private val trackNode by track()

EasyTrackUtils.kt


fun <F : Fragment> F.track(): TrackNodeProperty<F> = FragmentTrackNodeProperty()

fun RecyclerView.ViewHolder.track(): TrackNodeProperty<RecyclerView.ViewHolder> =
LazyTrackNodeProperty() viewFactory@{
return@viewFactory itemView
}

fun View.track(): TrackNodeProperty<View> = LazyTrackNodeProperty() viewFactory@{
return@viewFactory it
}

如果你还不了解委托属性,可以看下我之前写过的一篇文章,这里不解释其原理了:Android | ViewBinding 与 Kotlin 委托双剑合璧




6. EasyTrack 核心源码


这一节,我简单介绍下 EasyTrack 的核心源码,最核心的部分在入口类 EasyTrack 中:


6.1 doTrackEvent()


doTrackEvent() 是触发埋点的主方法,主要流程是调用 fillTrackParams() 收集埋点参数,再将参数分发给有效的 ITrackProvider。


internal fun Any.doTrackEvent(eventName: String, otherParams: TrackParams? = null): TrackParams? {
1. 检查是否有有效的 ITrackProvider
2. 基于视图树递归收集埋点参数(fillTrackParams)
3. 日志
4. 将收集到的埋点参数分发给有效的 ITrackProvider
}

6.2 fillTrackParams()


-> 基于视图树递归收集埋点参数
internal fun fillTrackParams(node: Any?, params: TrackParams? = null): TrackParams {
val result = params ?: TrackParams()
var curNode = node
while (null != curNode) {
when (curNode) {
is View -> {
// 1. 视图节点
if (android.R.id.content == curNode.id) {
// 1.1 Activity 节点
val activity = getActivityFromView(curNode)
if (activity is IPageTrackNode) {
// 1.1.1 IPageTrackNode节点(处理页面间参数映射)
activity.fillTrackParams(result)
curNode = activity.referrerSnapshot()
} else {
// 1.1.2 终止
curNode = null
}
} else {
// 1.2 Activity 视图子节点
curNode.trackModel?.fillTrackParams(result)
curNode = curNode.parent
}
}
is ITrackNode -> {
// 2. 非视图节点
curNode.fillTrackParams(result)
curNode = curNode.parent
}
else -> {
// 3. 终止
curNode = null
}
}
}
return result
}

主要逻辑:从入参 node 为起点,循环获取依附在视图节点上的 ITrackModel 数据节点并调用 fillTrackParams() 方法收集参数,并将循环指针指向 parent。




7. 总结


EasyTrack 框架的源码我已经放在 Github 上了,源码地址:github.com/pengxurui/E… 我也写了一个简单的 Sample Demo,你可以直接运行体验下。欢迎批评,欢迎 Issue~


说说目前遇到的问题,在处理页面间参数传递时,我们需要依赖 Intent extras 参数。这就导致我们需要在大量创建 Intent 的地方都加入来源页面的埋点参数(注意:即使你不使用 EasyTrack,你也要这么做)。目前我还没有想到比较好的方法,你觉得呢?说说你的看法吧。


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

【Flutter 状态管理】第一论: 对状态管理的看法与理解

前言 由 编程技术交流圣地[-Flutter群-] 发起的 状态管理研究小组,将就 状态管理 相关相关话题进行为期 两个月 的讨论。小组将于两个月后解散,并发布相关讨论成果。 目前只有内定的 5 个人参与讨论,如果你对状态管理有什么独特的见解,或想参与其中。...
继续阅读 »
前言

编程技术交流圣地[-Flutter群-] 发起的 状态管理研究小组,将就 状态管理 相关相关话题进行为期 两个月 的讨论。小组将于两个月后解散,并发布相关讨论成果。



目前只有内定的 5 个人参与讨论,如果你对状态管理有什么独特的见解,或想参与其中。可以发表一篇自己对状态管理的认知文章,作为入群的“门票”,欢迎和我们共同交流。




前两周进行第一个话题的探讨 :


你对状态管理的看法与理解



状态管理,状态管理。顾名思义是状态+管理,那问题来了,到底什么是状态?为什么要管理呢?


一、何谓状态


1. 对状态概念的思考

其实要说明一个东西是什么,是非常困难的。这并不像数学中能给出具体的定义,比如


平行四边形: 是在同一个二维平面内,由两组平行线段组成的闭合图形
三角形: 是由同一平面内不在同一直线上的三条线段首尾顺次连接所组成的封闭图形

如果具有明确定义的概念,我们可以很容易理解它的特性和作用。但对于 状态 这种含义比较笼统的词汇,那就仁者见仁,智者见智 了。我查了一下,对于状态而言有如下解释:


状态是人或事物表现出来的形态。是指现实(或虚拟)事物处于生成、生存、发展、消亡时期
或各转化临界点时的形态或事物态势。

如果影射到编程上,状态就是界面各个时期的表现,状态的改变,通过刷新后会导致界面的变化。那 界面状态 有什么区别和联系呢?


比如说一颗种子发芽、长大、开花、结果、枯萎,这是外在的表征,是外界所看到的形态变化。但从根本上来说,这些变化是种子与外界的资源交换,导致的内部数据变化,而产生的结果。也就是一个是 面子 ,一个是 里子


看花人并不会在意种子的内部的变化逻辑,他们只需满足看花的需求就行了。 也就是说 界面是表现 ,是用来给用户看的;状态是本质 ,是需要编程者去维护的。如果一个开发者只能看到 面子 ,而忽略我们本身就是那颗种子,还谈什么状态,想什么管理?。




2.状态、交互与界面

对一个应用而言,最根本的目的在于: 用户 通过操作界面, 可以进行正确的逻辑处理,并得到一定的响应反馈





从用户的角度来看,应用内部运作机制是个 黑盒,用户不需要、也没必要了解细节。但这个黑盒内部逻辑处理需要编程者进行实现,我们是无法逃避的。



拿我们最熟悉的计数器而言,点击按钮,修改状态信息,重新构建后,实现界面上数字变化的效果。





二、为什么需要管理


说到 管理 一词,你觉得什么情况下需要管理?是 复杂,只有 复杂 才有管理的必要。那管理有什么好处?


比如张三开了一家餐馆,雇了四个人,他们各干各的,都要同时进行招乎食客、烧菜、送快递、清洁等任务,那效率将非常低下。如果菜里吃出了不明生物 (bug),也不容易定位问题根源。这很像什么东西都塞在一个 XXXState 里去完成,其中不仅需要处理组件构建逻辑,还掺杂着大量的业务逻辑


如果将复杂的事务,分层次地交由不同人进行处理,各司其职,要比四个人各干各的要高效。而管理的目的就是分层级提高地 处理任务。




1.状态的作用范围

首先来思考一个问题:是不是所有的状态都需要管理?比如说下面的 FloatingActionButton ,在点击时会有水波纹的效果,界面的变化就意味着存在着状态的变化



FloatingActionButton 组件继承自 StatelessWidget,也就是说它并没有改变自身状态的能力。那点击时,为什么状态会发生变化呢?因为它在 build 中使用了 RawMaterialButton 组件,RawMaterialButton 中使用了 InkWell ,而 InkWell 继承自 InkResponseInkResponsebuild 中使用了_InkResponseStateWidget ,这个组件中维护了水波纹在手势中的状态变化逻辑。


class FloatingActionButton extends StatelessWidget{

---->[FloatingActionButton#build]----
Widget result = RawMaterialButton(
onPressed: onPressed,
mouseCursor: mouseCursor,
elevation: elevation,
focusElevation: focusElevation,
hoverElevation: hoverElevation,
highlightElevation: highlightElevation,
disabledElevation: disabledElevation,
constraints: sizeConstraints,
materialTapTargetSize: materialTapTargetSize,
fillColor: backgroundColor,
focusColor: focusColor,
hoverColor: hoverColor,
splashColor: splashColor,
textStyle: extendedTextStyle,
shape: shape,
clipBehavior: clipBehavior,
focusNode: focusNode,
autofocus: autofocus,
enableFeedback: enableFeedback,
child: resolvedChild,
);



也就是说:点击时,水波纹的变化效果,被封装在 _InkResponseStateWidget 组件状态中。像这种私有的状态,我们并不需要进行管理,因为它能够独立完成自己任务,而且外界并不需要了解这些状态。比如水波纹的圆心半径等会变化的状态信息,在外界是不关心的。

Flutter 中的 State 本身就是一种状态管理的手段。因为:


1. State 具有根据状态信息,构建组件的能力
2. State 具有重新构建组件的能力

所有的 StatefulWidget 都是这样,变化逻辑及状态量都会被封装在对应的 XXXState 类中。是局部的,私有的,外界无需了解内部状态的信息变化,也没有可以直接访问的途径。这一般用于对组件的封装,将复杂且相对独立的状态变化,封装起来,简化用户使用。




2.状态的共享及修改同步

上面说的 State 管理状态虽然非常小巧,方便。但同时也会存在不足之处,因为状态量被维护在 XXXState 内部,外界很难访问修改。比如下面 page1 中,C 是数字信息,跳转到 page2 时,也要显示这个数值,且按下 R 按钮能要让 page1page2 的数字都重置为 0。这就存在着状态存在共享及修改同步更新,该如何实现呢?





我们先来写个如下的设置界面:



class SettingPage extends StatelessWidget {
const SettingPage({Key? key}) : super(key: key);

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('设置界面'),),
body: Container(
height: 54,
color: Colors.white,
child: Row(
children: [
const SizedBox(width: 10,),
Text('当前计数为:'),
Spacer(),
ElevatedButton(child: Text('重置'),onPressed: (){} ),
const SizedBox(width: 10,)
],
),
),
);
}
}

那如何知道当前的数值,以及如何将 重置 操作点击时,影响 page1 的数字状态呢?其实 构造入参回调函数 可以解决一切的数据共享和修改同步问题。




3.代码实现 - setState 版:源码位置

在点击重置时 ,由于 page2 的计数也要清空,这就说明其状态量需要变化,要用 StatefulWidget 维护状态。在构造时,通过构造方法传入 initialCounter ,让 page2 的数字可以与 page1 一致。通过 onReset 回调函数来监听重置按钮的触发,以此来重置 page1 的数字状态,让 page1 的数字可以与 page2 一致。这就是让两个界面的同一状态量保持一致。如下图:


class SettingPage extends StatefulWidget {
final int initialCounter;
final VoidCallback onReset;

const SettingPage({
Key? key,
required this.initialCounter,
required this.onReset,
}) : super(key: key);

@override
State<SettingPage> createState() => _SettingPageState();
}














跳转到设置页设置页重置

class _SettingPageState extends State<SettingPage> {
int _counter = 0;

@override
void initState() {
super.initState();
_counter = widget.initialCounter;
}

//构建同上, 略...

void _onReset() {
widget.onReset();
setState(() {
_counter = 0;
});
}

_SettingPageState 中维护 _counter 状态量,在点击 重置 时执行 _onReset 方法,触发 onReset 回调。在 界面1 中监听 onReset ,来重置 界面1 的数字状态。这样通过 构造入参回调函数 ,就能保证两个界面 数字状态信息 的同步。


---->[界面1 跳转代码]-----
Navigator.push(context,
MaterialPageRoute(builder: (context) => SettingPage(
initialCounter: _counter,
onReset: (){
setState(() {
_counter=0;
});
},
)));

但这样,确定也很明显,数据传来传去,调来调去,非常麻烦,乱就容易出错。如果再多几个需要共享的信息,或者在其他界面里还需要共享这个状态,那代码里将会更加混乱。




4.代码实现 - ValueListenableBuilder 版:源码位置

上面的 setState 版实现 数据共享和修改同步,除了代码混乱之外,还有一些其他的缺点。首先,在 SettingPage 中我们又维护了一个状态信息,两个界面的信息虽然相同,却是两份一样的。如果状态信息是比较大的对象,这未免会造成不必要的内存浪费。





其次,就是深为大家诟病的 setState 重构范围。State#setState 执行后,会触发 build 方法重新构建组件。比如在 page1 中,_MyHomePageState#build 构建的是 Scaffold ,当状态变化时触发 setState ,其下的所有组件都会被构建一遍,重新构建的范围过大。

大家可以想一下,这里为什么不把 Scaffold 提到外面去?原因是:FloatingActionButton 组件需要修改状态量 _counter 并执行重新构建,所以不得不扩大构建的范围,来包含住 FloatingActionButton





其实 Flutter 中有个组件可以解决上面两个问题,那就是 ValueListenableBuilder 。使用方式很简单,先创建一个 ValueNotifier 的可监听对象 _counter


class _MyHomePageState extends State<MyHomePage> {

final ValueNotifier<int> _counter = ValueNotifier(0);

@override
void dispose() {
super.dispose();
_counter.dispose();
}

void _incrementCounter() {
_counter.value++;
}

如下使用 ValueListenableBuilder 组件,监听 _counter 对象,当该可监听对象的数值变化时,会可以通知监听者,重新构建 builder 方法里的组件。这样最大的好处在于:不需要 通过 _MyHomePageState#setState 对内部整体进行构建,仅对需要改变的局部 进行重新构建。


ValueListenableBuilder(
valueListenable: _counter,
builder: (ctx, int value, __) => Text(
'$value',
style: Theme.of(context).textTheme.headline4,
),
),



可以将 对于_counter 可见听对象传入 page2 中,同样通过 ValueListenableBuilder 监听 counter。这就相当于观察者模式中,两个订阅者 同时监听一个发布者 。在 page2 中让发布者信息变化,也会通知两个订阅者,比如执行 counter.value =0 ,两处的 ValueListenableBuilder 都会触发局部重建。



这样就能达到和 setState 版 一样的效果,通过 ValueListenableBuilder 简化了入参和回调通知,并具有局部重构组件的能力。可以说 State状态的共享及修改同步 方面是被 ValueListenableBuilder 完胜的。但话说回来, State 本来就不是做这种事的,它更注重于私有状态的处理。比如ValueListenableBuilder 的本质,就是一个通过 State 实现的私有状态封装 ,所以没有什么好不好,只有适合或不适合。





三、使用状态管理工具


1. 状态管理工具的必要性

其实前面的 ValueListenableBuilder 的效果以及不错了,但是在某些场合仍存在不足。因为 _counter 需要通过构造方法进行传递,如果状态量过多,或共享场合变多、传递层级过深,也会使代码处理比较复杂。最致命的一点是:业务逻辑处理界面组件都耦合在 _MyHomePageState 中,这对于拓展维护而言并不是件好事。所以 管理 对于 复杂逻辑性下的状态的共享及修改同步 是有必要的。





2.通过 flutter_bloc 实现状态管理: 源码位置

我们前面说过,状态管理的目的在于:让状态可以共享及在更新状态时可以同步更新相关组件显示,且将状态变化逻辑界面构建进行分离。flutter_bloc 是实现状态管理的工具之一,它的核心是:通过 BlocEvent 操作转化成 State;同时通过 BlocBuilder 监听状态的变化,进行局部组件构建。


通过这种方式,编程者可以将 状态变化逻辑 集中在 Bloc 中处理。当事件触发时,通过发送 Event 指令,让 Bloc 驱动 State 进行变化。就这个小案例而言,主要有两个事件: 自加重置 。像这样不需要参数的 Event , 通过枚举进行区分即可,比如定义事件:


enum CountEvent {
add, // 自加
reset, // 重置
}



状态,就是界面构建需要依赖的信息。这里定义 CountState ,持有 value 数值。


class CountState {
final int value;
const CountState({this.value = 0});
}



最后是 Bloc ,新版的 flutter_bloc 通过 on 监听事件,通过 emit 产出新状态。如下在构造中通过 on 来监听 CountEvent 事件,通过 _onCountEvent 方法进行处理,进行 CountState 的变化。当 event == CountEvent.add 时,会产出一个原状态 +1 的新 CountState 对象。


class CountBloc extends Bloc<CountEvent, CountState> {
CountBloc() : super(const CountState()){
on<CountEvent>(_onCountEvent);
}

void _onCountEvent(CountEvent event, Emitter<CountState> emit) {
if (event == CountEvent.add) {
emit(CountState(value: state.value + 1));
}

if (event == CountEvent.reset) {
emit (const CountState(value: 0));
}
}
}

画一个简单的示意图,如下:点击 _incrementCounter 时,只需要触发 CountEvent.add 指令即可。核心的状态处理逻辑会在 CountBloc 中进行,并生成新的状态,且通过 BlocBuilder 组件 触发局部更新 。这样,状态变化的逻辑界面构建的逻辑就能够很好地分离。



// 发送自加事件指定
void _incrementCounter() {
BlocProvider.of<CountBloc>(context).add(CountEvent.add);
}

//构建数字 Text 处使用 BlocBuilder 局部更新:
BlocBuilder<CountBloc, CountState>(
builder: _buildCounterByState,
),

Widget _buildCounterByState(BuildContext context, CountState state) {
return Text(
'${state.value}',
style: Theme.of(context).textTheme.headline4,
);
}



这样,设置界面的 重置 按钮也是类似,只需要发出 CountEvent.reset 指令即可,核心的状态处理逻辑会在 CountBloc 中进行,并生成新的状态,且通过 BlocBuilder 组件 触发局部更新





由于 BlocProvider.of<CountBloc>(context) 获取 Bloc 对象,需要上级的上下文存在该 BlocProvider ,可以在最顶层进行提供。这样在任何界面中都可以获取该 Bloc 及对其状态进行共享。



这是个比较小的案例,可能无法体现 Bloc 的精髓,但作为一个入门级的体验还是挺不错的。你需要自己体会一下:


[1]. 状态的 [共享] 及 [修改状态] 时同步更新。
[2]. [状态变化逻辑] 和 [界面构建逻辑] 的分离。

个人认为,这两点是状态管理的核心。也许每个人都会有各自的认识,但至少你不能在不知道自己要管理什么的情况下,做着表面上认为是状态管理的事。最后总结一下我的观点:状态就是界面构建需要依赖的信息;而管理,就是通过分工,让这些状态信息可以更容易维护更便于共享更好同步变化更'高效'地运转flutter_bloc 只是 状态管理 的工具之一,而其他的工具,也不会脱离这个核心。




四、官方案例 - github_search 解读


1. 案例介绍:源码位置

为了让大家对 flutter_bloc 在逻辑分层上有更深的认识,这里选取了 flutter_bloc 官方的一个案例进行解读。下面先简单看一下界面效果:


[1] 输入字符进行搜索,界面显示 github 项目
[2] 在不同的状态下显示不同的界面,如未输入、搜索中、搜索成功、无数据。
[3] 输入时防抖 debounce。避免每输入一个字符都请求接口。

注: debounce : 当调用动作 n 毫秒后,才会执行该动作,若在这 n 毫秒内又调用此动作则将重新计算执行时间。















搜索状态变化无数据时状态显示

项目结构


├── bloc         # 处理状态变化逻辑
├── view # 处理视图构建
├── repository # 处理数据获取逻辑
└── main.dart # 程序入口



2.仓储层 repository

我们先来看一下仓储层 repository ,这是将数据获取逻辑单独抽离出来,其中包含model 包下相关数据实体类 ,和 api 包下数据获取操作。



有人可能会问,业务逻辑都放在 Bloc 里处理不就行了吗,为什么非要搞个 repository 层。其实很任意理解,Bloc 核心是处理状态的变化,如果接口请求代码都放在 Bloc 里就显得非常臃肿。更重要的有点是: repository 层是相对独立的,你完全可以单独对进行测试,保证数据获取逻辑的正确性。


这样能带来另一个好处,当数据模型确定后。repository 层和界面层完全可以同步进行开发,最后通过 Bloc 层将 repository界面 进行整合。分层是进行管理的一种手段,就像不同部门来处理不同的事务,一旦出错,就很容易定位是哪个环节出了问题。当一个部门的进行拓展升级,也能尽可能不波及其他部门。



repository 层也是通用的,不管是 Bloc 也好、Provider 也好,都只是管理的一种手段。repository 层作为数据的获取方式是完全独立的,比如 todo 的案例,Bloc 版和 Provider 可以共用一个 repository 层,因为即使框架的使用方式有差异,但数据的获取方式是不变的。




下面来简单看一下repository 层的逻辑,GithubRepository 依赖两个对象,只有一个 search 方法。其中 GithubCache 类型 cache 对象用于记录缓存,在查询时首先从缓存中查看,如果已存在,则返回缓存数据。否则使用 GithubClient 类型的 client 对象进行搜索。





GithubClient 主要通过 http 获取网络数据。





GithubClient 就是通过一个 Map 维护搜索字符搜索结果的映射。这了处理的比较简单,完全可以基于此进行拓展:比如设置一个缓存数量上限,不然随着搜索缓存会一直加入;或将缓存加入数据库,支持离线缓存。将 repository 层独立出来后,这些功能的拓展就能和界面层解耦。因为界面只关心数据本身,并不关心数据如何缓存、如何获取。





3. bloc 层

首先来看事件,整个搜索功能只有一个事件:文字输入时的TextChanged,事件触发时需要附带搜索的信息字符串。


abstract class GithubSearchEvent extends Equatable {
const GithubSearchEvent();
}

class TextChanged extends GithubSearchEvent {
const TextChanged({required this.text});

final String text;

@override
List<Object> get props => [text];

@override
String toString() => 'TextChanged { text: $text }';
}



至于状态,整个过程中有四类状态:



  • [1]. SearchStateEmpty : 输入字符为空时的状态,无维护数据。

  • [2]. SearchStateLoading : 从请求开始到响应中的等待状态,无维护数据。

  • [3]. SearchStateSuccess: 请求成功的状态,维护 SearchResultItem 条目列表。

  • [4]. SearchStateError:失败状态,维护错误信息字符串。





最后是 Bloc,用于整合状态变化的逻辑。在 构造方法 中通过 onTextChanged 事件进行监听,触发 _onTextChanged 产出状态。比如 searchTerm.isEmpty 说明无字符输入,产出 SearchStateEmpty 状态。在 githubRepository.search 获取数据前,产出 SearchStateLoading 表示等待状态。请求成功则产出 SearchStateSuccess 状态,且内含结果数据,失败则产出 SearchStateError 状态。


class GithubSearchBloc extends Bloc<GithubSearchEvent, GithubSearchState> {
GithubSearchBloc({required this.githubRepository})
: super(SearchStateEmpty()) {
on<TextChanged>(_onTextChanged);
}

final GithubRepository githubRepository;

void _onTextChanged(
TextChanged event,
Emitter<GithubSearchState> emit,
) async {
final searchTerm = event.text;

if (searchTerm.isEmpty) return emit(SearchStateEmpty());

emit(SearchStateLoading());

try {
final results = await githubRepository.search(searchTerm);
emit(SearchStateSuccess(results.items));
} catch (error) {
emit(error is SearchResultError
? SearchStateError(error.message)
: const SearchStateError('something went wrong'));
}
}
}

到这里,整个业务逻辑就完成了,不同时刻的状态变化也已经完成,接下来只需要通过 BlocBuilder 监听状态变化,构建组件即可。另外说明一下 debounce 的作用:如果不进行防抖处理,每次输入字符都会触发请求获取数据,这样会造成请求非常频繁,而且过程中的输入大多数是无用的。这种情况,就可以使用 debounce 进行处理,比如,输入 300 ms 后才进行请求操作,如果在此期间有新的输入,就重新计时。
其本质是对流的转换操作,在 stream_transform 插件中有相关处理,在 pubspec.yaml 中添加依赖


stream_transform: ^2.0.0



on<TextChanged>transformer 参数中可以指定事件流转换器,这样就能完成防抖效果:


const Duration _duration = Duration(milliseconds: 300);

EventTransformer<Event> debounce<Event>(Duration duration) {
return (events, mapper) => events.debounce(duration).switchMap(mapper);
}

class GithubSearchBloc extends Bloc<GithubSearchEvent, GithubSearchState> {
GithubSearchBloc({required this.githubRepository})
: super(SearchStateEmpty()) {
// 使用 debounce 进行转换
on<TextChanged>(_onTextChanged, transformer: debounce(_duration));
}



4.界面层

界面层的处理非常简单,通过 BlocBuilder 监听状态变化,根据不同的状态构建不同的界面元素即可。





事件的触发,是在文字输入时。输入框被单独封装成 SearchBar 组件,在 TextFieldonChanged 方法中,触发 _githubSearchBlocTextChanged 方法,这样驱动点,让整个状态变化的“齿轮组”运转了起来。


---->[search_bar.dart]----
@override
void initState() {
super.initState();
_githubSearchBloc = context.read<GithubSearchBloc>();
}

return TextField(
//....
onChanged: (text) {
_githubSearchBloc.add(TextChanged(text: text));
},

这样一个简单的搜索需求就完成了,flutter_bloc 还通过了非常多的实例、文档,有兴趣的可以自己多研究研究。




五、小结


这里小结一下我对状态管理的理解:


[1]. [状态] 是界面构建需要依赖的信息。
[2]. [管理] 是对复杂场景的分层处理,使[状态变化逻辑]独立于[视图构建逻辑]。

再回到那个最初的问题,是所有的状态都需要管理吗?如何区分哪些状态需要管理?就像前端 redux 状态管理,在 You Might Not Need Redux (可自行百度译文) 中说到:人们常常在正真需要 Redux 之前,就选择使用它 。对于状态管理,其实都是这样,往往初学者 "趋之若鹜" ,不明白为什么要状态管理,为什么一个很简单的功能,非要弯弯绕绕一大圈来实现。就是看到别用了,使用我也要用,这是不理智的。


我们在使用前应该明白:


[1]. 状态是否需要被共享和修改同步。如果否,也许通过 [State] 封装为内部状态是更好的选择。
[2]. [业务逻辑] 和[界面状态变化] 是否复杂到有分层的必要。如果不是非常复杂,
FutureBuilder、ValueListenableBuilder 这种小巧的局部构建组件也许是更好的选择。

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

[JS基础回顾] 闭包 又双叒叕来~~~

闭包是基于词法作用域书写代码时所产生的自然结果,你甚至不需要为了利用它们而有意识地创建闭包 MDN的解释闭包是函数和声明该函数的词法环境的组合。 Tips: 词法作用域和词法环境 1,此时函数还没被执行,所以使用的是词法作用域即静态作用域.2, 此时函...
继续阅读 »

闭包是基于词法作用域书写代码时所产生的自然结果,你甚至不需要为了利用它们而有意识地创建闭包




MDN的解释闭包是函数声明该函数的词法环境的组合。




Tips: 词法作用域词法环境 1,此时函数还没被执行,所以使用的是词法作用域即静态作用域.2, 此时函数被执行,此时词法作用域就会变成词法环境(包含静态作用域与动态作用域)



以上的解释 个人感觉还是不够清晰
我这样理解

  1. 闭包就是突破了函数作用域
  2. 闭包就是函数嵌套函数子函数可以访问父函数的变量(也就是所谓的自由变量), 所以,此变量不会被回收.


闭包暴露``函数作用域3种方式:


1) 通过外部函数的参数进行暴露



闭包内 调用外部函数 通过外部函数的参数 暴露 闭包内 自由变量.



function fn() { 
var a = 2;
function innerFn() {
outerFn(a) //通过外部函数的参数进行暴露
}
innerFn();
};
function outerFn(val) {
console.log(val); // 2
}
fn(); // 2

2) 通过外部作用域的变量进行暴露



其中val为全局变量



function fn() { 
var a = 1;
function innerFn() {
val = a; //通过外部作用域的变量进行暴露
}
innerFn();
};

fn();
console.log(val); // 1


3) 通过return直接将整个函数进行暴露


function fn() { 
var a = 1;
function innerFn() {
console.log(a);
}
return innerFn; //通过return直接将整个函数进行暴露
};

let a = fn();
a(); // 1

关于闭包的内存泄露



首先必须声明一点:使用闭包并不一定会造成内存泄露,只有使用闭包不当才可能会造成内存泄露.




为什么闭包可能会造成内存泄露呢?原因就是上面提到的,因为它一般会暴露自身的作用域给外部使用.如果使用不当,就可能导致该内存一直被占用,无法被JS的垃圾回收机制回收.就造成了内存泄露.




注意: 即使闭包里面什么都没有,闭包仍然会隐式地引用它所在作用域里的所用变量. 正因为这个隐藏的特点,闭包经常会发生不易发现的内存泄漏问题.



常见哪些情况使用闭包会造成内存泄露:





    1. 使用定时器未及时清除.因为计时器只有先停止才会被回收.所以决办法很简单,将定时器及时清除,并将造成内存的变量赋值为null(变成空指针)





    1. 相互循环引用.这是经常容易犯的错误,并且也不容易发现.





    1. 闭包引用到全局变量上.因为全局变量是只有当页面被关闭的时候才会被回收.




四 循环和闭包


1) 同步循环打印 正确的值


for (var i=1; i<5; i++) { 
console.log( i );
}
// 1 2 3 4

2) 同步中嵌套异步任务(中的宏任务)循环打印 错误的值



当执行 console 时, 循环已经完成, 同步任务执行完成后,执行宏任务,此时 i 已经是 5.所以打印5个5.



for (var i=1; i<5; i++) { 
setTimeout( function timer() {
console.log( i );
}, i*1000 );
}
// 打印出 5 个 5

3) 创造5个独立的函数作用域,但是 i 也全都是对外部作用域的引用 错误的值



它的最终值仍然是5个5.为什么?我们来分析下,它用了一个匿名函数包裹了定时器,并立即执行.在进行for循环时,会创造5个独立的函数作用域(由匿名函数创建的,因为它是闭包函数).但是这5个独立的函数作用域里的i也全都是对外部作用域的引用.即它们访问的都是i的最终值5.这并不是我们想要的,我们要的是5个独立的作用域,并且每个作用域都保存一个"当时"i的值.



for (var i=1; i<5; i++) { 
(function() {
setTimeout( function timer() {
console.log( i );
}, i*1000 );
})();
}
// 打印出 5 个 5

4) 通过匿名函数创建独立的函数作用域,并且通过 变量 保存独立的 i 值


for (var i=1; i<5; i++) { 
(function () {
var x=i;
console.log(x*1000); // 1000 2000 3000 4000
setTimeout( function timer() {
console.log( x );
}, x*1000 );
})();
}

// 1 2 3 4

5) 通过匿名函数创建独立的函数作用域,并且通过 参数 保存独立的 i 值


for (var i=1; i<5; i++) { 
(function (x) {
console.log(x*1000); // 1000 2000 3000 4000
setTimeout( function timer() {
console.log( x );
}, x*1000 );
})(i);
}

// 1 2 3 4

注意

  • 使用定时器未及时清除.因为计时器只有先停止才会被回收.所以决办法很简单,将定时器及时清除,并将造成内存的变量赋值为null(变成空指针)
  • 闭包引用到全局变量上.因为全局变量是只有当页面被关闭的时候才会被回收.
  • 闭包就是函数嵌套函数子函数可以访问父函数的变量(也就是所谓的自由变量), 所以,此变量不会被回收.


  • 作者:无限循环无限
    链接:https://juejin.cn/post/7011805931201642533

    收起阅读 »

    JS箭头函数 什么时候用 ,什么时候不能用,我总结出了4点

    箭头函数的定义 箭头函数定义包括一个参数列表(零个或多个参数,如果参数个数不是一个的话要用 ( .. ) 包围起来),然后是标识 =>,函数体放在最后。 箭头函数与普通函数的区别 箭头函数 let arrowSum = (a, b) => { ...
    继续阅读 »

    箭头函数的定义



    箭头函数定义包括一个参数列表(零个或多个参数,如果参数个数不是一个的话要用 ( .. ) 包围起来),然后是标识 =>,函数体放在最后。



    箭头函数与普通函数的区别


    箭头函数


    let arrowSum = (a, b) => { 
    return a + b
    }

    普通函数


    let zz = function(a, b){
    return a + b
    }

    箭头函数的用法


    我们打印fn函数的原型,我们会发现箭头函数本身没有this;


    var fn = (a, b) => {
    console.log(this, fn.prototype);
    //window, undefined
    var fn2 = () => {
    console.log(this, '测试');
    // window
    };
    fn2();
    }
    fn()

    箭头函数的arguments
    我们会发现这样写会报语法错误


    var fn = (a) => {
    console.log(a.arguments)
    }
    fn();
    // TypeError:Cannot read property 'arguments' of undefined

    我们换一种情况,我们看代码会发现箭头函数argemnets指向了上一个函数



    箭头函数不会创建自己的this,它只会从自己的作用域链的上一层继承this。




    var z = function(a){
    console.log(arguments);
    bb();
    function bb() {
    console.log(arguments);
    let ac = () => {
    console.log(arguments);
    //arguments 指向第二层函数
    };
    ac();
    }
    }
    z()

    什么时候不能用箭头函数


    1. 通过构造函数调用


    let Foo = () =>  {

    }
    let result = new Foo();
    //TypeError: Foo is not a constructor

    2. 需要使用prototype


    let foo = () =>  {

    }
    console.log(foo.prototype)
    //underfind

    3. 没有super



    连原型都没有,自然也不能通过 super 来访问原型的属性,所以箭头函数也是没有 super 的,不过跟 this、arguments、new.target 一样,这些值由外围最近一层非箭头函数决定



    总结




    • 如果你有一个简单语句的在线函数表达式,其中唯一的语句是return某个计算出的值,而且这个函数内部没有this引用,且没有自身引用(比如递归,事件绑定/解绑定),且不会要求函数执行这些,那么我们可以安全的把它重构为=>箭头函数




    • 如果你的内层函数表达式依赖于它的函数中调用 let self= this 或者.bind(this)来确保适当的this绑定,那么内层函数表达式可以转换为=>箭头函数




    • 如果你的内函数表达式依赖于封装函数像 let args = Array.prototype.slice.call
      (arguments)的词法复制,那么这个内层函数表达式应该可以安全的转换=>箭头函数




    • 所有的其他情况——函数声明,较长的多函数表达式,需要词法名称标识符(比如递归 , 构造函数)的函数,以及任何不符合以上几点特征的函数一般都应该避免=>箭头函数





    关于this arguments 和 super 的词法绑定。这是利用es6的特性来修正一些常见的问题,而不是bug或者错误。


    作者:zz
    链接:https://juejin.cn/post/7011270097721360421
    收起阅读 »

    ?Map和Set巧解力扣算法问题

    问题一:什么是Map和Set? ES6以前,在JavaScript中实现“键/值”式存储可以使用Object来方便高效的完成,也就是使用对象属性作为键,再使用属性来引用值,像下面这样 let student = { name: '啊呜', se...
    继续阅读 »

    问题一:什么是Map和Set?


    ES6以前,在JavaScript中实现“键/值”式存储可以使用Object来方便高效的完成,也就是使用对象属性作为键,再使用属性来引用值,像下面这样


    let student = {
    name: '啊呜',
    sex: 'male',
    age: 18
    }

    但是这种实现并非没有问题,这里的键只能是对象的属性,于是就出现了Map这一新的集合类型,为JavaScript带来了真正的键/值存储机制,我们可以这样初始化映射:


    const map = new Map([
    ['key1','value1'],
    ['key2','value2'],
    ['key3','value3'],
    ])

    ES6还新增了Set这一种新的集合类型,Set在很多方面都像是加强的Map,这是因为它们的大多数API和行为都是共有的。Set集合类型的特点是不能存储重复元素,成员值都是唯一且没有重复的值


    问题二:Map和Set的基本API怎么用?


    Map的API:




    • get() :返回键值对




    • set() :添加键值对,返回实例




    • delete() :删除键值对,返回布尔




    • has() :检查键值对,返回布尔




    • clear() :清除所有成员




    • keys() :返回以键为遍历器的对象




    • values() :返回以值为遍历器的对象




    • entries() :返回以键和值为遍历器的对象




    • forEach() :使用回调函数遍历每个成员




    Set的API:




    • add() :添加值,返回实例




    • delete() :删除值,返回布尔




    • has() :检查值,返回布尔




    • clear() :清除所有成员




    • keys() :返回以属性值为遍历器的对象




    • values() :返回以属性值为遍历器的对象




    • entries() :返回以属性值和属性值为遍历器的对象




    • forEach() :使用回调函数遍历每个成员





    好啦,到这,相信你对JS中的Map和Set有了一定的了解,我们现在尝试使用这两种集合类型,在LeetCode中大显身手~



    LeetCode20:有效的括号



    给定一个只包括 '(',')','{','}','[',']' 的字符串 s ,判断字符串是否有效。



    示例1:



    输入: s = "()"

    输出: true



    示例2:



    输入: s = "()[]{}"

    输出: true



    示例 3:



    输入: s = "(]"

    输出: false



    这题的思路是使用栈+Map来解决,直接上代码:


    carbon (2).png


    LeetCode141:环形链表



    给定一个链表,判断链表中是否有环。



    示例:


    circularlinkedlist.png



    输入: head = [3,2,0,-4], pos = 1

    输出: true



    这题我的思路是使用Set来解决,当然还有一种方法,用快慢指针来解决,但是比较难想到,而且比较反人类,我们这里只介绍Set,清晰易懂~


    carbon (3).png



    作者:_啊呜
    链接:https://juejin.cn/post/7011710641807294477
    收起阅读 »

    深入理解 redux 数据流和异步过程管理

    前端框架的数据流 前端框架实现了数据驱动视图变化的功能,我们用 template 或者 jsx 描述好了数据和视图的绑定关系,然后就只需要关心数据的管理了。 数据在组件和组件之间、组件和全局 store 之间传递,叫做前端框架的数据流。 一般来说,除了某部分状...
    继续阅读 »

    前端框架的数据流


    前端框架实现了数据驱动视图变化的功能,我们用 template 或者 jsx 描述好了数据和视图的绑定关系,然后就只需要关心数据的管理了。


    数据在组件和组件之间、组件和全局 store 之间传递,叫做前端框架的数据流。


    一般来说,除了某部分状态数据是只有某个组件关心的,我们会把状态数据放在组件内以外,业务数据、多个组件关心的状态数据都会放在 store 里面。组件从 store 中取数据,当交互的时候去通知 store 改变对应的数据。


    这个 store 不一定是 redux、mobox 这些第三方库,其实 react 内置的 context 也可以作为 store。但是 context 做为 store 有一个问题,任何组件都能从 context 中取出数据来修改,那么当排查问题的时候就特别困难,因为并不知道是哪个组件把数据改坏的,也就是数据流不清晰。


    正是因为这个原因,我们几乎见不到用 context 作为 store,基本都是搭配一个 redux。


    所以为什么 redux 好呢?第一个原因就是数据流清晰,改变数据有统一的入口。



    组件里都是通过 dispatch 一个 action 来触发 store 的修改,而且修改的逻辑都是在 reducer 里面,组件再监听 store 的数据变化,从中取出最新的数据。


    这样数据流动是单向的,清晰的,很容易管理。


    这就像为什么我们在公司里想要什么权限都要走审批流,而不是直接找某人,一样的道理。集中管理流程比较清晰,而且还可以追溯。


    异步过程的管理


    很多情况下改变 store 数据都是一个异步的过程,比如等待网络请求返回数据、定时改变数据、等待某个事件来改变数据等,那这些异步过程的代码放在哪里呢?


    组件?


    放在组件里是可以,但是异步过程怎么跨组件复用?多个异步过程之间怎么做串行、并行等控制?


    所以当异步过程比较多,而且异步过程与异步过程之间也不独立,有串行、并行、甚至更复杂的关系的时候,直接把异步逻辑放组件内不行。


    不放组件内,那放哪呢?


    redux 提供的中间件机制是不是可以用来放这些异步过程呢?


    redux 中间件


    先看下什么是 redux 中间件:


    redux 的流程很简单,就是 dispatch 一个 action 到 store, reducer 来处理 action。那么如果想在到达 store 之前多做一些处理呢?在哪里加?


    改造 dispatch!中间件的原理就是层层包装 dispatch。


    下面是 applyMiddleware 的源码,可以看到 applyMiddleware 就是对 store.dispatch 做了层层包装,最后返回修改了 dispatch 之后的 store。


    function applyMiddleware(middlewares) {
    let dispatch = store.dispatch
    middlewares.forEach(middleware =>
    dispatch = middleware(store)(dispatch)
    )
    return { ...store, dispatch}
    }

    所以说中间件最终返回的函数就是处理 action 的 dispatch:


    function middlewareXxx(store) {
    return function (next) {
    return function (action) {
    // xx
    };
    };
    };
    }

    中间件会包装 dispatch,而 dispatch 就是把 action 传给 store 的,所以中间件自然可以拿到 action、拿到 store,还有被包装的 dispatch,也就是 next。


    比如 redux-thunk 中间件的实现:


    function createThunkMiddleware(extraArgument) {
    return ({ dispatch, getState }) => next => action => {
    if (typeof action === 'function') {
    return action(dispatch, getState, extraArgument);
    }

    return next(action);
    };
    }

    const thunk = createThunkMiddleware();

    它判断了如果 action 是一个函数,就执行该函数,并且把 store.dispath 和 store.getState 传进去,否则传给内层的 dispatch。


    通过 redux-thunk 中间件,我们可以把异步过程通过函数的形式放在 dispatch 的参数里:


    const login = (userName) => (dispatch) => {
    dispatch({ type: 'loginStart' })
    request.post('/api/login', { data: userName }, () => {
    dispatch({ type: 'loginSuccess', payload: userName })
    })
    }
    store.dispatch(login('guang'))

    但是这样解决了组件里的异步过程不好复用、多个异步过程之间不好做并行、串行等控制的问题了么?


    没有,这段逻辑依然是在组件里写,只不过移到了 dispatch 里,也没有提供多个异步过程的管理机制。


    解决这个问题,需要用 redux-saga 或 redux-observable 中间件。


    redux-saga


    redux-saga 并没有改变 action,它会把 action 透传给 store,只是多加了一条异步过程的处理。



    redux-saga 中间件是这样启用的:


    import { createStore, applyMiddleware } from 'redux'
    import createSagaMiddleware from 'redux-saga'
    import rootReducer from './reducer'
    import rootSaga from './sagas'

    const sagaMiddleware = createSagaMiddleware()
    const store = createStore(rootReducer, {}, applyMiddleware(sagaMiddleware))
    sagaMiddleware.run(rootSaga)

    要调用 run 把 saga 的 watcher saga 跑起来:


    watcher saga 里面监听了一些 action,然后调用 worker saga 来处理:


    import { all, takeLatest } from 'redux-saga/effects'

    function* rootSaga() {
    yield all([
    takeLatest('login', login),
    takeLatest('logout', logout)
    ])
    }
    export default rootSaga

    redux-saga 会先把 action 透传给 store,然后判断下该 action 是否是被 taker 监听的:


    function sagaMiddleware({ getState, dispatch }) {
    return function (next) {
    return function (action) {
    const result = next(action);// 把 action 透传给 store

    channel.put(action); //触发 saga 的 action 监听流程

    return result;
    }
    }
    }

    当发现该 action 是被监听的,那么就执行相应的 taker,调用 worker saga 来处理:


    function* login(action) {
    try {
    const loginInfo = yield call(loginService, action.account)
    yield put({ type: 'loginSuccess', loginInfo })
    } catch (error) {
    yield put({ type: 'loginError', error })
    }
    }

    function* logout() {
    yield put({ type: 'logoutSuccess'})
    }

    比如 login 和 logout 会有不同的 worker saga。


    login 会请求 login 接口,然后触发 loginSuccess 或者 loginError 的 action。


    logout 会触发 logoutSuccess 的 action。


    redux saga 的异步过程管理就是这样的:先把 action 透传给 store,然后判断 action 是否是被 taker 监听的,如果是,则调用对应的 worker saga 进行处理。


    redux saga 在 redux 的 action 流程之外,加了一条监听 action 的异步处理的流程。


    其实整个流程还是比较容易理解的。理解成本高一点的就是 generator 的写法了:


    比如下面这段代码:


    function* xxxSaga() {
    while(true) {
    yield take('xxx_action');
    //...
    }
    }

    它就是对每一个监听到的 xxx_action 做同样的处理的意思,相当于 takeEvery:


    function* xxxSaga() {
    yield takeEvery('xxx_action');
    //...
    }

    但是因为有一个 while(true),很多同学就不理解了,这不是死循环了么?


    不是的。generator 执行后返回的是一个 iterator,需要另外一个程序调用 next 方法才会继续执行。所以怎么执行、是否继续执行都是由另一个程序控制的。


    在 redux-saga 里面,控制 worker saga 执行的程序叫做 task。worker saga 只是告诉了 task 应该做什么处理,通过 call、fork、put 这些命令(这些命令叫做 effect)。


    然后 task 会调用不同的实现函数来执行该 worker saga。


    为什么要这样设计呢?直接执行不就行了,为啥要拆成 worker saga 和 task 两部分,这样理解成本不就高了么?


    确实,设计成 generator 的形式会增加理解成本,但是换来的是可测试性。因为各种副作用,比如网络请求、dispatch action 到 store 等等,都变成了 call、put 等 effect,由 task 部分控制执行。那么具体怎么执行的就可以随意的切换了,这样测试的时候只需要模拟传入对应的数据,就可以测试 worker saga 了。


    redux saga 设计成 generator 的形式是一种学习成本和可测试性的权衡。


    还记得 redux-thunk 有啥问题么?多个异步过程之间的并行、串行的复杂关系没法处理。那 redux-saga 是怎么解决的呢?


    redux-saga 提供了 all、race、takeEvery、takeLatest 等 effect 来指定多个异步过程的关系:


    比如 takeEvery 会对多个 action 的每一个做同样的处理,takeLatest 会对多个 action 的最后一个做处理,race 会只返回最快的那个异步过程的结果,等等。


    这些控制多个异步过程之间关系的 effect 正是 redux-thunk 所没有的,也是复杂异步过程的管理必不可少的部分。


    所以 redux-saga 可以做复杂异步过程的管理,而且具有很好的可测试性。


    其实异步过程的管理,最出名的是 rxjs,而 redux-observable 就是基于 rxjs 实现的,它也是一种复杂异步过程管理的方案。


    redux-observable


    redux-observable 用起来和 redux-saga 特别像,比如启用插件的部分:


    const epicMiddleware = createEpicMiddleware();

    const store = createStore(
    rootReducer,
    applyMiddleware(epicMiddleware)
    );

    epicMiddleware.run(rootEpic);

    和 redux saga 的启动流程是一样的,只是不叫 saga 而叫 epic。


    但是对异步过程的处理,redux saga 是自己提供了一些 effect,而 redux-observable 是利用了 rxjs 的 operator:


    import { ajax } from 'rxjs/ajax';

    const fetchUserEpic = (action$, state$) => action$.pipe(
    ofType('FETCH_USER'),
    mergeMap(({ payload }) => ajax.getJSON(`/api/users/${payload}`).pipe(
    map(response => ({
    type: 'FETCH_USER_FULFILLED',
    payload: response
    }))
    )
    );

    通过 ofType 来指定监听的 action,处理结束返回 action 传递给 store。


    相比 redux-saga 来说,redux-observable 支持的异步过程的处理更丰富,直接对接了 operator 的生态,是开放的,而 redux-saga 则只是提供了内置的几个 effect 来处理。


    所以做特别复杂的异步流程处理的时候,redux-observable 能够利用 rxjs 的操作符的优势会更明显。


    但是 redux-saga 的优点还有基于 generator 的良好的可测试性,而且大多数场景下,redux-saga 提供的异步过程的处理能力就足够了,所以相对来说,redux-saga 用的更多一些。


    总结


    前端框架实现了数据到视图的绑定,我们只需要关心数据流就可以了。


    相比 context 的混乱的数据流,redux 的 view -> action -> store -> view 的单向数据流更清晰且容易管理。


    前端代码中有很多异步过程,这些异步过程之间可能有串行、并行甚至更复杂的关系,放在组件里并不好管理,可以放在 redux 的中间件里。


    redux 的中间件就是对 dispatch 的层层包装,比如 redux-thunk 就是判断了下 action 是 function 就执行下,否则就是继续 dispatch。


    redux-thunk 并没有提供多个异步过程管理的机制,复杂异步过程的管理还是得用 redux-saga 或者 redux-observable。


    redux-saga 透传了 action 到 store,并且监听 action 执行相应的异步过程。异步过程的描述使用 generator 的形式,好处是可测试性。比如通过 take、takeEvery、takeLatest 来监听 action,然后执行 worker saga。worker saga 可以用 put、call、fork 等 effect 来描述不同的副作用,由 task 负责执行。


    redux-observable 同样监听了 action 执行相应的异步过程,但是是基于 rxjs 的 operator,相比 saga 来说,异步过程的管理功能更强大。


    不管是 redux-saga 通过 generator 来组织异步过程,通过内置 effect 来处理多个异步过程之间的关系,还是 redux-observable 通过 rxjs 的 operator 来组织异步过程和多个异步过程之间的关系。它们都解决了复杂异步过程的处理的问题,可以根据场景的复杂度灵活选用。


    作者:zxg_神说要有光
    链接:https://juejin.cn/post/7011835078594527263

    收起阅读 »

    【JavaScript】async await 更优雅的错误处理

    背景 团队来了新的小伙伴,发现我们的团队代码规范中,要给 async await 添加 try...catch。他感觉很疑惑,假如有很多个(不集中),那不是要加很多个地方?那不是很不优雅? 为什么要错误处理 JavaScript 是一个单线程的语言,假如不加...
    继续阅读 »

    背景


    团队来了新的小伙伴,发现我们的团队代码规范中,要给 async await 添加 try...catch。他感觉很疑惑,假如有很多个(不集中),那不是要加很多个地方?那不是很不优雅?


    为什么要错误处理


    JavaScript 是一个单线程的语言,假如不加 try ...catch ,会导致直接报错无法继续执行。当然不意味着你代码中一定要用 try...catch 包住,使用 try...catch 意味着你知道这个位置代码很可能出现报错,所以你使用了 try...catch 进行捕获处理,并让程序继续执行。


    我理解我们一般在执行 async await 的时候,一般运行在异步的场景下,这种场景一般不应该阻塞流程的进行,所以推荐使用了 try...catch 的处理。


    async await 更优雅的错误处理


    但确实如那位同事所说,加 try...catch 并不是一个很优雅的行为。所以我 Google 了一下,发现 How to write async await without try-catch blocks in Javascript 这篇文章中提到了一种更优雅的方法处理,并封装成了一个库——await-to-js。这个库只有一个 function,我们完全可以将这个函数运用到我们的业务中,如下所示:


    /**
    * @param { Promise } promise
    * @param { Object= } errorExt - Additional Information you can pass to the err object
    * @return { Promise }
    */
    export function to<T, U = Error> (
    promise: Promise<T>,
    errorExt?: object
    ): Promise<[U, undefined] | [null, T]> {
    return promise
    .then<[null, T]>((data: T) => [null, data]) // 执行成功,返回数组第一项为 null。第二个是结果。
    .catch<[U, undefined]>((err: U) => {
    if (errorExt) {
    Object.assign(err, errorExt);
    }

    return [err, undefined]; // 执行失败,返回数组第一项为错误信息,第二项为 undefined
    });
    }

    export default to;

    这里需要有一个前置的知识点:await 是在等待一个 Promise 的返回值


    正常情况下,await 命令后面是一个 Promise 对象,返回该对象的结果。如果不是 Promise 对象,就直接返回对应的值。


    所以我们只需要利用 Promise 的特性,分别在 promise.thenpromise.catch 中返回不同的数组,其中 fulfilled 的时候返回数组第一项为 null,第二个是结果。rejected 的时候,返回数组第一项为错误信息,第二项为 undefined。使用的时候,判断第一项是否为空,即可知道是否有错误,具体使用如下:


    import to from 'await-to-js';
    // If you use CommonJS (i.e NodeJS environment), it should be:
    // const to = require('await-to-js').default;

    async function asyncTaskWithCb(cb) {
    let err, user, savedTask, notification;

    [ err, user ] = await to(UserModel.findById(1));
    if(!user) return cb('No user found');

    [ err, savedTask ] = await to(TaskModel({userId: user.id, name: 'Demo Task'}));
    if(err) return cb('Error occurred while saving task');

    if(user.notificationsEnabled) {
    [ err ] = await to(NotificationService.sendNotification(user.id, 'Task Created'));
    if(err) return cb('Error while sending notification');
    }

    if(savedTask.assignedUser.id !== user.id) {
    [ err, notification ] = await to(NotificationService.sendNotification(savedTask.assignedUser.id, 'Task was created for you'));
    if(err) return cb('Error while sending notification');
    }

    cb(null, savedTask);
    }

    小结


    async await 中添加错误处理个人认为是有必要的,但方案不仅仅只有 try...catch。利用 async awaitPromise 的特性,我们可以更加优雅的处理 async await 的错误。


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

    收起阅读 »

    国内知名Wchat团队荣誉出品顶级IM通讯聊天系统

    iOS
    国内知名Wchat团队荣誉出品顶级IM通讯聊天系统团队言语在先:想低价购买者勿扰(团队是在国内首屈一指的通信公司离职后组建,低价购买者/代码代码贩子者/同行勿扰/)。想购买劣质低等产品者勿扰(行业鱼龙混杂,想购买类似低能协议xmpp者勿扰)。想购买由类似ope...
    继续阅读 »



    国内知名Wchat团队荣誉出品顶级IM通讯聊天系统



    团队言语在先:

    想低价购买者勿扰(团队是在国内首屈一指的通信公司离职后组建,低价购买者/代码代码贩子者/同行勿扰/)

    。想购买劣质低等产品者勿扰(行业鱼龙混杂,想购买类似低能协议xmpp者勿扰)

    。想购买由类似openfire第三方开源改造而来的所谓第三方通信server者勿扰

    。想购买没有做任何安全加密场景者勿扰(随便一句api 一个接口就构成了红包收发/转账/密码设置等没有任何安全系数可言的低质产品)

    。想购买非运营级别通信系统勿扰(到处呼喊:最稳定/真正可靠/大并发/真正安全!所有一切都需要实际架构支撑以及理论数值测验)

    。想购买无保障/无支撑者勿扰(1W/4W/10W低质产品不可谓没有,必须做到:大并发支持合同保障/合作支持运维保障/在线人数支持架构保障)

    。想购买消息丢包者勿扰(满天飞的所谓消息确认机制,最简单的测验既是前端支持消息收发demo测试环境,低质产品一秒收发百条消息必丢必崩,

    别提秒发千条/万条,更低质产品可测验:同时发九张图片/根据数字12345678910发送出去,必丢!android vs ios)

    。想购买大容量群uer者勿扰(随便宣传既是万人大群/几千大群/群组无限,小团队产品群组上线用户超过4000群消息体量不用很大手机前端必卡)

    。最重要一点:口口声声说要运营很大的系统 却想出十几个money的人群勿扰,买产品做系统一要稳定二要长久用三要抛开运维烦恼,预算有限那就干脆

    别买,买了几万的系统你一样后面用不起来会烂掉!

    。产品体系包括:android ios server adminweb maintenance httpapi h5 webpc (支持server压测/前端消息收发压测/httpapi压测)

    。。支持源码,但需要您拿去做一个伟大的系统出来!

    。。团队产品目前国内没有同质化,客户集中在国外,有求高质量产品的个人或团队可通过以下方式联系到我们(低价者勿扰!)

    。。。球球:383189941 q 513275129

    。。。。产品不多介绍直接加我 测试产品更直接

    。。。。。创新从未停止 更新不会终止 大陆唯一一家支持大并发保障/支持合同费用包含运维支撑的团队 

    收起阅读 »

    Android 高级UI5 画笔Paint的基本用法

    1.setStyle(Paint.Style style)设置画笔样式,取值有Paint.Style.FILL :填充内部Paint.Style.FILL_AND_STROKE :填充内部和描边Paint.Style.STROKE :仅描边代码实例:publi...
    继续阅读 »

    1.setStyle(Paint.Style style)

    设置画笔样式,取值有
    Paint.Style.FILL :填充内部
    Paint.Style.FILL_AND_STROKE :填充内部和描边
    Paint.Style.STROKE :仅描边

    代码实例:


    public class PaintViewBasic extends View {
    private Paint mPaint;

    public PaintViewBasic(Context context) {
    super(context);
    mPaint = new Paint();
    }

    public PaintViewBasic(Context context, @Nullable AttributeSet attrs) {
    super(context, attrs);
    mPaint = new Paint();
    }

    @Override
    protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    drawStyle(canvas);
    }

    private void drawStyle( Canvas canvas ) {

    mPaint.setColor(Color.RED);//设置画笔的颜色
    mPaint.setTextSize(60);//设置文字大小
    mPaint.setStrokeWidth(5);//设置画笔的宽度
    mPaint.setAntiAlias(true);//设置抗锯齿功能 true表示抗锯齿 false则表示不需要这功能

    mPaint.setStyle(Paint.Style.STROKE);
    canvas.drawCircle(200,200,160,mPaint);

    mPaint.setStyle(Paint.Style.FILL);
    canvas.drawCircle(200,600,160,mPaint);

    mPaint.setStyle(Paint.Style.FILL_AND_STROKE);
    canvas.drawCircle(200,1000,160,mPaint);

    }

    }

    2.setStrokeCap(Paint.Cap cap)

    设置线冒样式,取值有
    Paint.Cap.BUTT(无线冒)
    Paint.Cap.ROUND(圆形线冒)
    Paint.Cap.SQUARE(方形线冒)
    注意:冒多出来的那块区域就是线帽!就相当于给原来的直线加上一个帽子一样,所以叫线帽


        private void drawStrokeCap(Canvas canvas) {
    Paint paint = new Paint();

    paint.setAntiAlias(true);
    paint.setStrokeWidth(200);
    paint.setColor(Color.parseColor("#00ff00"));
    paint.setStrokeCap(Paint.Cap.BUTT); // 线帽,即画的线条两端是否带有圆角,butt,无圆角
    canvas.drawLine(200, 200, 500, 200, paint);

    paint.setColor(Color.parseColor("#ff0000"));
    paint.setStrokeCap(Paint.Cap.ROUND); // 线帽,即画的线条两端是否带有圆角,ROUND,圆角
    canvas.drawLine(200, 500, 500, 500, paint);

    paint.setColor(Color.parseColor("#0000ff"));
    paint.setStrokeCap(Paint.Cap.SQUARE); // 线帽,即画的线条两端是否带有圆角,SQUARE,矩形
    canvas.drawLine(200, 800, 500, 800, paint);
    }

    3.setStrokeJoin(Paint.Join join)

    设置线段连接处样式,取值有:
    Paint.Join.MITER(结合处为锐角)
    Paint.Join.Round (结合处为圆弧)
    Paint.Join.BEVEL (结合处为直线)


      private void drawStrokeJoin( Canvas canvas ) {
    Paint paint = new Paint();

    paint.setAntiAlias( true );
    paint.setStrokeWidth( 80 );
    paint.setStyle(Paint.Style.STROKE ); // 默认是填充 Paint.Style.FILL
    paint.setColor( Color.parseColor("#0000ff") );

    Path path = new Path();
    path.moveTo(100, 100);
    path.lineTo(400, 100);
    path.lineTo(100, 300);
    paint.setStrokeJoin(Paint.Join.MITER);
    canvas.drawPath(path, paint);

    path.moveTo(100, 500);
    path.lineTo(400, 500);
    path.lineTo(100, 700);
    paint.setStrokeJoin(Paint.Join.ROUND);
    canvas.drawPath(path, paint);

    path.moveTo(100, 900);
    path.lineTo(400, 900);
    path.lineTo(100, 1100);
    paint.setStrokeJoin(Paint.Join.BEVEL);
    canvas.drawPath(path, paint);
    }

    }

    4.setPathEffect(PathEffect effect)

    设置绘制路径的效果,如点画线等

    CornerPathEffect:

    这个类的作用就是将Path的各个连接线段之间的夹角用一种更平滑的方式连接,类似于圆弧与切线的效果。
    一般的,通过CornerPathEffect(float radius)指定一个具体的圆弧半径来实例化一个CornerPathEffect。

    DashPathEffect:

    这个类的作用就是将Path的线段虚线化。
    构造函数为DashPathEffect(float[] intervals, float offset),其中intervals为虚线的ON和OFF数组,该数组的length必须大于等于2,phase为绘制时的偏移量。

    DiscretePathEffect:

    这个类的作用是打散Path的线段,使得在原来路径的基础上发生打散效果。
    一般的,通过构造DiscretePathEffect(float segmentLength,float deviation)来构造一个实例,其中,segmentLength指定最大的段长,deviation指定偏离量。

    PathDashPathEffect:

    这个类的作用是使用Path图形来填充当前的路径,其构造函数为PathDashPathEffect (Path shape, float advance, float phase,PathDashPathEffect.Stylestyle)。
    shape则是指填充图形,advance指每个图形间的间距,phase为绘制时的偏移量,style为该类自由的枚举值,有三种情况:Style.ROTATE、Style.MORPH和
    Style.TRANSLATE。其中ROTATE的情况下,线段连接处的图形转换以旋转到与下一段移动方向相一致的角度进行转转,MORPH时图形会以发生拉伸或压缩等变形的情况与下一段相连接,TRANSLATE时,图形会以位置平移的方式与下一段相连接。

    ComposePathEffect:

    组合效果,这个类需要两个PathEffect参数来构造一个实例,ComposePathEffect (PathEffect outerpe,PathEffect innerpe),表现时,会首先将innerpe表现出来,然后再在innerpe的基础上去增加outerpe的效果。

    SumPathEffect:

    叠加效果,这个类也需要两个PathEffect作为参数SumPathEffect(PathEffect first,PathEffect second),但与ComposePathEffect不同的是,在表现时,会分别对两个参数的效果各自独立进行表现,然后将两个效果简单的重叠在一起显示出来。

    关于参数phase

    在存在phase参数的两个类里,如果phase参数的值不停发生改变,那么所绘制的图形也会随着偏移量而不断的发生变动,这个时候,看起来这条线就像动起来了一样。


    private float phase;
    private PathEffect[] effects;
    private int[] colors;

    private void drawPathEffect(Canvas canvas) {
    mPaint.setStyle(Paint.Style.STROKE);
    mPaint.setStrokeWidth(4);
    // 创建,并初始化Path
    Path path = new Path();
    path.moveTo(0, 0);
    for (int i = 1; i <= 35; i++) {
    // 生成15个点,随机生成它们的坐标,并将它们连成一条Path
    path.lineTo(i * 20, (float) Math.random() * 60);
    }
    // 初始化七个颜色
    colors = new int[]{Color.BLACK, Color.BLUE, Color.CYAN, Color.GREEN, Color.MAGENTA, Color.RED,
    Color.GRAY};


    // 将背景填充成白色
    canvas.drawColor(Color.WHITE);
    effects = new PathEffect[7];
    // -------下面开始初始化7中路径的效果
    // 使用路径效果
    effects[0] = null;
    // 使用CornerPathEffect路径效果
    effects[1] = new CornerPathEffect(10);
    // 初始化DiscretePathEffect
    effects[2] = new DiscretePathEffect(3.0f, 5.0f);
    // 初始化DashPathEffect
    effects[3] = new DashPathEffect(new float[]{20, 10, 5, 10}, phase);
    // 初始化PathDashPathEffect
    Path p = new Path();
    p.addRect(0, 0, 8, 8, Path.Direction.CCW);
    effects[4] = new PathDashPathEffect(p, 12, phase, PathDashPathEffect.Style.ROTATE);
    // 初始化PathDashPathEffect
    effects[5] = new ComposePathEffect(effects[2], effects[4]);
    effects[6] = new SumPathEffect(effects[4], effects[3]);
    // 将画布移到8,8处开始绘制
    canvas.translate(8, 8);
    // 依次使用7中不同路径效果,7种不同的颜色来绘制路径
    for (int i = 0; i < effects.length; i++) {
    mPaint.setPathEffect(effects[i]);
    mPaint.setColor(colors[i]);
    canvas.drawPath(path, mPaint);
    canvas.translate(0, 200);
    }
    // 改变phase值,形成动画效果
    phase += 1;
    invalidate();
    }

    5.setShadowLayer(float radius, float dx, float dy, int shadowColor)

    阴影制作:包括各种形状(矩形,圆形等等),以及文字等等都能设置阴影。


        private void drawShadowLayer(Canvas canvas) {
    // 建立Paint 物件
    Paint paint1 = new Paint();
    paint1.setTextSize(100);
    // 设定颜色
    paint1.setColor(Color.BLACK);
    // 设定阴影(柔边, X 轴位移, Y 轴位移, 阴影颜色)
    paint1.setShadowLayer(10, 5, 5, Color.GRAY);
    // 实心矩形& 其阴影
    canvas.drawText("我爱你", 20,100,paint1);
    Paint paint2 = new Paint();
    paint2.setTextSize(100);
    paint2.setColor(Color.GREEN);
    paint2.setShadowLayer(10, 6, 6, Color.GRAY);
    canvas.drawText("你真傻", 20,200,paint2);

    //cx和cy为圆点的坐标
    int radius = 80;
    int offest = 40;
    int startX = radius + offest;
    int startY = radius + offest + 200;

    Paint paint3 = new Paint();
    //如果不关闭硬件加速,setShadowLayer无效
    setLayerType(LAYER_TYPE_SOFTWARE, null);
    paint3.setShadowLayer(20, -20, 10, Color.DKGRAY);
    canvas.drawCircle(startX, startY, radius, paint3);
    paint3.setStyle(Paint.Style.STROKE);
    paint3.setStrokeWidth(5);
    canvas.drawCircle(startX + radius * 2 + offest, startY, radius, paint3);
    }

    6.setXfermode(Xfermode xfermode)

    Xfermode国外有大神称之为过渡模式,这种翻译比较贴切但恐怕不易理解,大家也可以直接称之为图像混合模式,因为所谓的“过渡”其实就是图像混合的一种,这个方法跟我们上面讲到的setColorFilter蛮相似的。查看API文档发现其果然有三个子类:AvoidXfermode, PixelXorXfermode和PorterDuffXfermode,这三个子类实现的功能要比setColorFilter的三个子类复杂得多。

    由于AvoidXfermode, PixelXorXfermode都已经被标注为过时了,所以这次主要研究的是仍然在使用的PorterDuffXfermode:

    PorterDuffXfermode

    该类同样有且只有一个含参的构造方法PorterDuffXfermode(PorterDuff.Mode mode),虽说构造方法的签名列表里只有一个PorterDuff.Mode的参数,但是它可以实现很多酷毙的图形效果!!而PorterDuffXfermode就是图形混合模式的意思,其概念最早来自于SIGGRAPH的Tomas Proter和Tom Duff,混合图形的概念极大地推动了图形图像学的发展,延伸到计算机图形图像学像Adobe和AutoDesk公司著名的多款设计软件都可以说一定程度上受到影响,而我们PorterDuffXfermode的名字也来源于这俩人的人名组合PorterDuff,那PorterDuffXfermode能做些什么呢?我们先来看一张API DEMO里的图片:

    这张图片从一定程度上形象地说明了图形混合的作用,两个图形一圆一方通过一定的计算产生不同的组合效果,在API中Android为我们提供了18种(比上图多了两种ADD和OVERLAY)模式:

    ADD:饱和相加,对图像饱和度进行相加,不常用

    CLEAR:清除图像

    DARKEN:变暗,较深的颜色覆盖较浅的颜色,若两者深浅程度相同则混合

    DST:只显示目标图像

    DST_ATOP:在源图像和目标图像相交的地方绘制【目标图像】,在不相交的地方绘制【源图像】,相交处的效果受到源图像和目标图像alpha的影响

    DST_IN:只在源图像和目标图像相交的地方绘制【目标图像】,绘制效果受到源图像对应地方透明度影响

    DST_OUT:只在源图像和目标图像不相交的地方绘制【目标图像】,在相交的地方根据源图像的alpha进行过滤,源图像完全不透明则完全过滤,完全透明则不过滤

    DST_OVER:将目标图像放在源图像上方

    LIGHTEN:变亮,与DARKEN相反,DARKEN和LIGHTEN生成的图像结果与Android对颜色值深浅的定义有关

    MULTIPLY:正片叠底,源图像素颜色值乘以目标图像素颜色值除以255得到混合后图像像素颜色值

    OVERLAY:叠加

    SCREEN:滤色,色调均和,保留两个图层中较白的部分,较暗的部分被遮盖

    SRC:只显示源图像

    SRC_ATOP:在源图像和目标图像相交的地方绘制【源图像】,在不相交的地方绘制【目标图像】,相交处的效果受到源图像和目标图像alpha的影响

    SRC_IN:只在源图像和目标图像相交的地方绘制【源图像】

    SRC_OUT:只在源图像和目标图像不相交的地方绘制【源图像】,相交的地方根据目标图像的对应地方的alpha进行过滤,目标图像完全不透明则完全过滤,完全透明则不过滤

    SRC_OVER:将源图像放在目标图像上方

    XOR:在源图像和目标图像相交的地方之外绘制它们,在相交的地方受到对应alpha和色值影响,如果完全不透明则相交处完全不绘制


    public class PorterDuffView extends View {

    Paint mPaint;
    Context mContext;
    int BlueColor;
    int PinkColor;
    int mWith;
    int mHeight;
    public PorterDuffView(Context context) {
    super(context);
    init(context);
    }
    public PorterDuffView(Context context, AttributeSet attrs) {
    super(context, attrs);
    init(context);
    }

    public PorterDuffView(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    init(context);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    mHeight = getMeasuredHeight();
    mWith = getMeasuredWidth();
    }

    private void init(Context context) {
    mContext = context;
    BlueColor = ContextCompat.getColor(mContext, R.color.colorPrimary);
    PinkColor = ContextCompat.getColor(mContext, R.color.colorAccent);
    mPaint = new Paint();
    mPaint.setStyle(Paint.Style.FILL);
    mPaint.setAntiAlias(true);
    }
    private Bitmap drawRectBm(){
    Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
    paint.setColor(BlueColor);
    paint.setStyle(Paint.Style.FILL);
    paint.setAntiAlias(true);
    Bitmap bm = Bitmap.createBitmap(200,200, Bitmap.Config.ARGB_8888);
    Canvas cavas = new Canvas(bm);
    cavas.drawRect(new RectF(0,0,70,70),paint);
    return bm;
    }
    private Bitmap drawCircleBm(){
    Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
    paint.setColor(PinkColor);
    paint.setStyle(Paint.Style.FILL);
    paint.setAntiAlias(true);
    Bitmap bm = Bitmap.createBitmap(200,200, Bitmap.Config.ARGB_8888);
    Canvas cavas = new Canvas(bm);
    cavas.drawCircle(70,70,35,paint);
    return bm;
    }
    @Override
    protected void onDraw(Canvas canvas) {
    mPaint.setFilterBitmap(false);
    mPaint.setStyle(Paint.Style.FILL);
    mPaint.setTextSize(20);
    RectF recf = new RectF(20,20,60,60);
    mPaint.setColor(BlueColor);
    canvas.drawRect(recf,mPaint);
    mPaint.setColor(PinkColor);
    canvas.drawCircle(100,40,20,mPaint);
    @SuppressLint("WrongConstant") int sc = canvas.saveLayer(0, 0,mWith,mHeight, null, Canvas.MATRIX_SAVE_FLAG |
    Canvas.CLIP_SAVE_FLAG |
    Canvas.HAS_ALPHA_LAYER_SAVE_FLAG |
    Canvas.FULL_COLOR_LAYER_SAVE_FLAG |
    Canvas.CLIP_TO_LAYER_SAVE_FLAG);
    int y = 180;
    int x = 50;
    for(PorterDuff.Mode mode : PorterDuff.Mode.values()){
    if(y >= 900){
    y = 180;
    x += 200;
    }
    mPaint.setXfermode(null);
    canvas.drawText(mode.name(),x + 100,y,mPaint);
    canvas.drawBitmap(drawRectBm(),x,y,mPaint);
    mPaint.setXfermode(new PorterDuffXfermode(mode));
    canvas.drawBitmap(drawCircleBm(),x,y,mPaint);
    y += 120;
    }
    mPaint.setXfermode(null);
    // 还原画布
    canvas.restoreToCount(sc);
    }
    }

    收起阅读 »

    android音视频基础

    一、编码目的编码的目的:压缩,各种音视频的编码方式就是为了让视频体积更小,有利于存储和传输。编码的核心四想就是去除冗余信息。二、编码思路1.空间冗余图像内部相邻元素之间存在较强的相关性,造成信息的冗余。(一块区域颜色一样)2.时间冗余相邻视频帧具有较大的相关性...
    继续阅读 »

    一、编码目的

    编码的目的:压缩,各种音视频的编码方式就是为了让视频体积更小,有利于存储和传输。编码的核心四想就是去除冗余信息。

    二、编码思路

    1.空间冗余

    图像内部相邻元素之间存在较强的相关性,造成信息的冗余。(一块区域颜色一样)

    2.时间冗余

    相邻视频帧具有较大的相关性,造成信息的冗余。(第一帧和第二帧绝大多数数据一样)

    3. 视觉冗余

    人类不敏感的信息可以去除。(红色偏点橘色)

    4.信息熵冗余 == 熵编码-哈夫曼算法

    也称编码冗余,人们用于表达某一信息所需要的比特数总比理论上表示该信息所需要的最少比特数要大,它们之间的差距就是信息熵冗余,或称编码冗余。

    5.知识冗余 == 人类(头 身体 腿),汽车,房子 不需要记录

    是指在有些图像中还包含与某些验证知识有关的信息。

    6.I帧、P帧、B帧压缩思路

    I帧:帧内编码帧,关键帧,I帧可以看作一个图像经过压缩之后的产物,可以单独解码出一个完整的图像;(压缩率最低)

    P帧:前向预测/参考 编码帧,记录了本帧跟之前的一个关键帧(或P帧)的差别,解码时需要用之前缓存的画面叠加上本帧定义的差别,生成最终画面。 (压缩率比I帧高,比B帧低 属于 适中情况)

    B帧:双向预测/参考 编码帧,记录了本帧与前后帧的差别,解码需要参考前面一个I帧或者P帧,同时也需要后面的P帧才能解码一张完整的图像。 (参考前后的预测得到的,压缩率是最高,但是耗时)

    image.png

    三、编码标准

    1.组织

    • 国际电信联盟:H.264、H.265
    • MPEG系列标准:MPEG1、MPEG2、MPEG4、AVC

    AVC == H.264

    HEVC == H.265

    2.视频编码概念

    通过指定的压缩技术,把某一种视频格式文件,转换成另一种视频文件格式文件的方式。

    3. H.264分层结构(VCL和NAL)

    • VCL

      VCL(viedo coding layer,视频编码层):负责高效的视频内容展示。

      VCL数据:编码处理的输出,被压缩编码后的视频数据序列。

    • NAL

      NAL(Network Abstraction Layer,网络提取层):以网络所要求的恰当方式对数据进行打包传送,是传输层。不管是网络还是本地都需要通过这一层来传输。

    NAL = 一个字节的片头 + 若干的片数据

    image.png

    传输的是NAL

    4. H.264的输出结构

    H.264编码器默认的输出为:起始码+NALU。

    起始码:0x00000001和0x000001

    0x00000001:NALU里有狠多片

    0x000001:NALU里只有一片。

    5.举例分析H.264文件格式。

    image.png

    SPS 序列参数集(记录有多少I帧,多少B帧,多少P帧,帧是如何排列) == 7
    00 00 00 01 670x67 ---> 2进制01100111 ---> 取低五位 00000111 ---> 十六进制 0x07


    PPS 图像参数集(图像宽高信息等) == 8
    00 00 00 01 68, 0x68 ---> 2进制01101000---> 取低五位 00001000 ---> 十六进制 0x08


    SEI补充信息单元(可以记录坐标信息,人员信息, 后面解码的时候,可以通过代码获取此信息)https://blog.csdn.net/y601500359/article/details/80943990
    00 00 01 06 , 0x06 ---> 2进制00000110---> 取低五位00000110 ---> 十六进制 0x06

    I帧
    00 00 00 65, 0x65 ---> 2进制01100101---> 取低五位00000101 ---> 十六进制 0x05
    最终是 5 I帧完整画面出来

    P帧
    61 -->0x01 重要P帧
    41 -->0x01 非重要P帧

    B帧
    01 -->0x01 B帧

    image.png

    6.PTS和DTS

    DTS:解码时间戳,在什么时候解码这一帧的数据。

    PTS:显示时间戳,在什么时候显示这一帧数据。

    在没有B帧的时候,DTS和PTS是一样的顺序。

    因为B帧的解码需要靠前一帧和后一帧,只要有B帧DTS和PTS就一定会乱。

    image.png

    GOP:I帧+ 下一个I帧之前的所有B帧和P帧。

    i帧=GOP是什么理解的?
    SPS PPS I P B P B P B P B I 一组   SPS PPS I P B P B P B P B I 二组
    收起阅读 »

    Android 是怎么捕捉 java 异常的

    val default = Thread.getDefaultUncaughtExceptionHandler() Thread.setDefaultUncaughtExceptionHandler { t, e ->    //...
    继续阅读 »
     val default = Thread.getDefaultUncaughtExceptionHandler()

    Thread.setDefaultUncaughtExceptionHandler { t, e ->
       // 处理异常
       Log.e("Uncaught", "exception message : "+ e.message)
       // 将异常回执给原注册的 handler
       default.uncaughtException(t, e)
    }

    以上是很简单的一段代码,经常被用于 java 异常全局捕捉,但我的疑问是,他是怎么实现全局捕捉的,带着这样的疑问,我们来扒一下代码看看。


    顺藤摸瓜,我们看看静态方法 getDefaultUncaughtExceptionHandler 是被谁调用的,看了下所有的类调用的类,唯有 ThreadGroup 最靠谱:


    image.png


    在 parent 为空的情况下,就会调用 getDefaultUncaughtExceptionHandler 来回调异常,然后继续顺藤摸瓜,看看 ThreadGroup 的 uncaughtException 是被谁触发的,搜了一个圈,没有一个靠谱的。在我踌躇时,顺带瞄了一眼注释,奇迹发现:


    -   Called by the Java Virtual Machine when a thread in this
    - thread group stops because of an uncaught exception, and the thread
    - does not have a specific {[@link ](/link%20)Thread.UncaughtExceptionHandler}
    - installed.

    意思是:当一个未捕获的异常导致线程组中的线程停止时,JVM 会调用该方法。那我们就去搜搜 jvm 的源码,看看是怎么触发这个方法的。


    在 Hotspot 虚拟机源码的 thread.cpp 中的 JavaThread::exit 方法发现了这样的一段代码,并且还给出了注释:


    image.png


    在线程调用 exit 退出时,如果有未捕获的异常,则会调用 Thread.dispatchUncaughtException 方法,然后我们继续跟踪该方法:


    image.png


    然后调用当前线程的 uncaughtException 分发异常:


    image.png


    有意思的来了,如果我们没有给当前线程设置 UncaughtExceptionHandler ,则会将这个异常交给当前线程的 ThreadGroup 处理。如果我们给当前线程设置了 UncaughtExceptionHandler,则当前线程发生了异常,永远也不会抛给 getDefaultUncaughtExceptionHandler,该功能适合捕捉当前线程异常来用。


    终于回到了我们起初看到的 ThreadGroup.UncaughtExceptionHandler 方法,贴回原来的图继续分析:


    image.png


    这个地方会继续判断 parent 是否为空,parent 是个 ThreadGroup,ThreadGroup 实现了 Thread.UncaughtExceptionHandler 接口。这里我就直接说答案了,后面再说 ThreadGroup 和 Thread 的关系,最终会走到 system 的 ThreadGroup,system 的 parent 是个空,这时候走 else 分支,获取 Thread 中的 getDefaultUncaughtExceptionHandler 静态变量,触发 uncaughtException 方法,由于我们在 Activity 中设置了这个静态变量,所以,我们收到了这个异常通知。


    小知识


    1、如何捕获异常不退出


    val default = Thread.getDefaultUncaughtExceptionHandler()

    Log.e("Uncaught", "Uncaught handler: "+ default)
    // Uncaught handler: com.android.internal.os.RuntimeInit$KillApplicationHandler@21f02a3

    Thread.setDefaultUncaughtExceptionHandler { t, e ->
       // 将异常回执给原注册的 handler
       // default.uncaughtException(t, e)
    }

    捕获异常后,什么都不处理。但这样做显得非常不地道,这样会导致其他框架无法通过之前设置的静态变量捕获到异常上报。我打印了一下 default 是 RuntimeInit,该类在捕获到异常后,会做 killProcess。


    2、如何捕获指定线程异常:


    val thread = Thread {
         val a = 1/0
    }
    thread.setUncaughtExceptionHandler { t, e ->
           Log.e("Uncaught", "Uncaught trace: "+ e.message)
    }
    thread.start()

    3、ThreadGroup 和 Thread 的关系结构


    image.png



    • Thread 的 parent 是在 new Thread 的时候指定的,构造可传自定义的 ThreadGroup,默认是使用创建当前线程的 ThreadGroup

    • Thread 添加进 ThreadGroup 的 Thread[] 数组时机是在调用 start 启动线程的时候做的

    • ThreadGroup 的 parent 是在 new ThreadGroup 的时候指定的,构造可传自定义的 ThreadGroup,默认是使用当前线程的 ThreadGroup

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

    iOS RXSwift 4.7

    iOS
    ReplaySubjectReplaySubject 将对观察者发送全部的元素,无论观察者是何时进行订阅的。这里存在多个版本的 ReplaySubject,有的只会将最新的 n 个元素发送给观察者,有的只会将限制时间段内最新的元素发送给观察...
    继续阅读 »

    ReplaySubject

    ReplaySubject 将对观察者发送全部的元素,无论观察者是何时进行订阅的。

    这里存在多个版本的 ReplaySubject,有的只会将最新的 n 个元素发送给观察者,有的只会将限制时间段内最新的元素发送给观察者。

    如果把 ReplaySubject 当作观察者来使用,注意不要在多个线程调用 onNextonError 或 onCompleted。这样会导致无序调用,将造成意想不到的结果。


    演示

    let disposeBag = DisposeBag()
    let subject = ReplaySubject<String>.create(bufferSize: 1)

    subject
    .subscribe { print("Subscription: 1 Event:", $0) }
    .disposed(by: disposeBag)

    subject.onNext("🐶")
    subject.onNext("🐱")

    subject
    .subscribe { print("Subscription: 2 Event:", $0) }
    .disposed(by: disposeBag)

    subject.onNext("🅰️")
    subject.onNext("🅱️")

    输出结果:

    Subscription: 1 Event: next(🐶)
    Subscription: 1 Event: next(🐱)
    Subscription: 2 Event: next(🐱)
    Subscription: 1 Event: next(🅰️)
    Subscription: 2 Event: next(🅰️)
    Subscription: 1 Event: next(🅱️)
    Subscription: 2 Event: next(🅱️)

    BehaviorSubject

    当观察者对 BehaviorSubject 进行订阅时,它会将源 Observable 中最新的元素发送出来(如果不存在最新的元素,就发出默认元素)。然后将随后产生的元素发送出来。

    如果源 Observable 因为产生了一个 error 事件而中止, BehaviorSubject 就不会发出任何元素,而是将这个 error 事件发送出来。


    演示

    let disposeBag = DisposeBag()
    let subject = BehaviorSubject(value: "🔴")

    subject
    .subscribe { print("Subscription: 1 Event:", $0) }
    .disposed(by: disposeBag)

    subject.onNext("🐶")
    subject.onNext("🐱")

    subject
    .subscribe { print("Subscription: 2 Event:", $0) }
    .disposed(by: disposeBag)

    subject.onNext("🅰️")
    subject.onNext("🅱️")

    subject
    .subscribe { print("Subscription: 3 Event:", $0) }
    .disposed(by: disposeBag)

    subject.onNext("🍐")
    subject.onNext("🍊")

    输出结果:

    Subscription: 1 Event: next(🔴)
    Subscription: 1 Event: next(🐶)
    Subscription: 1 Event: next(🐱)
    Subscription: 2 Event: next(🐱)
    Subscription: 1 Event: next(🅰️)
    Subscription: 2 Event: next(🅰️)
    Subscription: 1 Event: next(🅱️)
    Subscription: 2 Event: next(🅱️)
    Subscription: 3 Event: next(🅱️)
    Subscription: 1 Event: next(🍐)
    Subscription: 2 Event: next(🍐)
    Subscription: 3 Event: next(🍐)
    Subscription: 1 Event: next(🍊)
    Subscription: 2 Event: next(🍊)
    Subscription: 3 Event: next(🍊)

    Variable (已弃用)

    Variable 是早期添加到 RxSwift 的概念,通过 “setting” 和 “getting”, 他可以帮助我们从原先命令式的思维方式,过渡到响应式的思维方式

    但这只是我们一厢情愿的想法。许多开发者滥用 Variable,来构建 重度命令式 系统,而不是 Rx 的 声明式 系统。这对于新手很常见,并且他们无法意识到,这是代码的坏味道。所以在 RxSwift 4.x 中 Variable 被轻度弃用,仅仅给出一个运行时警告。

    在 RxSwift 5.x 中,他被官方的正式的弃用了,并且在需要时,推荐使用 BehaviorRelay 或者 BehaviorSubject


    ControlProperty

    ControlProperty 专门用于描述 UI 控件属性的,它具有以下特征:

    • 不会产生 error 事件
    • 一定在 MainScheduler 订阅(主线程订阅)
    • 一定在 MainScheduler 监听(主线程监听)
    • 共享附加作用
    收起阅读 »

    Kotlin协程实现原理概述

    协程的顶层实现-CPS 现有如下代码: fun test(a: Int, b: Int) { // 求和 var result = a + b // 乘以2 result = result shl 1 // 加2 ...
    继续阅读 »

    协程的顶层实现-CPS


    现有如下代码:


    fun test(a: Int, b: Int) {
    // 求和
    var result = a + b
    // 乘以2
    result = result shl 1
    // 加2
    result += 2
    // 打印结果
    println(result)
    }

    我们来将代码SRP一下(单一职责):


    // 加法
    fun sum(a: Int,b: Int) = a + b
    // x乘以2
    fun double(x: Int) = x shl 1
    // x加2
    fun add2(x: Int) = x + 2

    // 最终的test
    fun test(a: Int, b: Int) {
    // 从内层依次调用,最终打印
    println(add2(double(sum(a,b))))
    }

    可以看到,我们将原来一坨的方法,抽离成了好几个方法,每个方法干一件事,虽然提高了可读性和可维护性,但是代码复杂了,我们来让它更复杂一点。


    上述代码是 让内层方法的返回值 作为参数 传递给外层方法,现在我们 把外层方法作为接口回调 传递给 内层方法:


    // 加法,next是加法做完的回调,会传入相加的结果
    fun sum(a: Int, b: Int, next: (Int) -> Unit) = a + b
    // x乘以2
    fun double(x: Int, next: (Int) -> Unit) = x shl 1
    // x加2
    fun add2(x: Int, next: (Int) -> Unit) = x + 2

    // 最终的test
    fun test2(a: Int, b: Int) {
    // 执行加法
    sum(a, b) { sum ->
    // 加完执行乘法
    double(sum) { double ->
    // 乘完就加2
    add2(double) { result ->
    // 最后打印
    println(result)
    }
    }
    }
    }

    这就是CPS的代码风格:通过接口回调的方式来实现的


    假设: 我们上述的几个方法: sum()/double()/add2()都是挂起函数,那么最终也会编译为CPS风格的回调函数方式,也就是:原来看起来同步的代码,经过编译器的"修改",变成了异步的方法,也就是:CPS化了,这就是kotlin协程的顶层实现逻辑。


    现在,让我们来验证一下,我们定义一个suspend函数,反编译看下是否真的CPS化了。


    // 定义挂起函数
    suspend fun test(id: String): String = "hello"

    反编译结果如下:


    // 参数添加了一个Continuation参数
    public final Object test(@NotNull String id, @NotNull Continuation $completion) {
    return "hello";
    }

    可以看到,多了个Continuation参数,这是个接口,是在本次函数执行完毕后执行的回调,内容如下:


    public interface Continuation<in T> {
    // 保存上下文(比如变量状态)
    public val context: CoroutineContext

    // 方法执行结束的回调,参数是个范型,用来传递方法执行的结果
    public fun resumeWith(result: Result<T>)
    }

    好,现在我们知道了suspend函数 是通过添加Continuation来实现的,我们来看个具体的业务:


    // 根据id获取token
    suspend fun getToken(id: String): String = "token"

    // 根据token获取info
    suspend fun getInfo(token: String): String = "info"

    // 测试
    suspend fun test() {
    // 先获取token,这是耗时请求
    val token = getToken("123")
    // 再根据token获取info,这也是个耗时请求
    val info = getInfo(token)
    // 打印
    println(info)
    }

    上述的业务代码很简单,但是前两步都是耗时操作,线程会卡在那里wait吗?显然不会,既然是suspend函数,那么就可以CPS化,等价的CPS代码如下:


    // 跟上述相同,传递了Continuation回调
    fun getToken(id: String, callback: Continuation<String>): String = "token"

    // 跟上述相同,传递了Continuation回调
    fun getInfo(token: String, callback: Continuation<String>): String = "info"

    // 测试(只写了主线代码)
    fun test() {
    // 先获取token,传入回调
    getToken("123", object : Continuation<String> {
    override fun resumeWith(result: Result<String>) {
    // 用token获取info,传入回调
    val token = result.getOrNull()
    getInfo(token!!, object : Continuation<String> {
    override fun resumeWith(result: Result<String>) {
    // 打印结果
    val info = result.getOrNull()
    println(info)
    }
    })
    }
    })
    }

    上述就是无suspend的CPS风格代码,通过传入接口回调来实现协程的同步代码风格。


    接下来我们来反编译suspend风格代码,看下它里面是怎么调度的。


    协程的底层实现-状态机


    我们先来简单修改下suspend test函数:


    // 没变化
    suspend fun getToken(id: String): String = "token"
    // 没变化
    suspend fun getInfo(token: String): String = "info"

    // 添加了局部变量a,看下suspend怎么保存a这个变量
    suspend fun test() {
    val token = getToken("123") // 挂起点1
    var a = 10 // 这里是10
    val info = getInfo(token) // 挂起点2,需要将前面的数据保存(比如a),在挂起点之后恢复
    println(info)
    println(a
    }

    每个suspend函数调用点,都会生成一个挂起点,在挂起点我们要保存当前的运行状态,比如局部变量等。


    反编译后的代码大致如下:


    public final Object getToken(String id, Continuation completion) {
    return "token";
    }

    public final Object getInfo(String token, Continuation completion) {
    return "info";
    }

    // 重点函数(伪代码)
    public final Object test(Continuation<String>: continuation) {
    Continuation cont = new ContinuationImpl(continuation) {
    int label; // 保存状态
    Object result; // 保存中间结果,还记得那个Result<T>吗,是个泛型,因为泛型擦除,所以为Object,用到就强转
    int tempA; // 保存上下文a的值,这个是根据具体代码产生的
    };
    switch(cont.label) {
    case 0 : {
    cont.label = 1; //更新label

    getToken("123",cont) // 执行对应的操作,注意cont,就是传入的回调
    break;
    }

    case 1 : {
    cont.label = 2; // 更新label

    // 这是一个挂起点,我们要保存上下文数据,这里就保存a的值
    int a = 10;
    cont.tempA = a; // 保存a的值

    // 获取上一步的结果,因为泛型擦除,需要强转
    String token = (Object)cont.result;
    getInfo(token, cont); // 执行对应的操作
    break;
    }

    case 2 : {
    String info = (Object)cont.result; // 获取上一步的结果
    println(info); // 执行对应的操作

    // 在挂起点之后,恢复a的值
    int a = cont.tempA;
    println(a);

    return;
    }
    }
    }

    我们可以将每个case理解为一个状态,每个case分支对应的语句,理解为一个Continuation实现。


    上述伪代码大致描述了协程的调度流程:



    • 1 调用test函数时,需要传入一个Continuation接口,我们会对它进行二次装饰。

    • 2 装饰就是根据函数具体逻辑,在内部添加额外的上下文数据和状态信息(也就是label)。

    • 3 每个状态对应一个Continuation接口,里面会执行对应的业务逻辑。

    • 4 每个状态都会: 保存上下文信息 -> 获取上一个状态的结果 -> 执行本状态业务逻辑 -> 恢复上下文信息。

    • 5 直到最后一个状态对应的逻辑执行完毕。


    总结


    综上,我们可以归纳以下几点:



    • 1 Kotlin协程没有很"频繁"的切换线程,它是在顶层通过调度方式实现的,所以效率是比较高的。

    • 2 Kotlin中,每个suspend方法,都需要一个Continuation接口实现,用来执行下一个状态的操作;并且,每个suspend方法的调用点都会产生一个挂起点。

    • 3 每个挂起点,都会产生一个label,对应于状态机的一个状态,不同的状态之间,通过Continuation来切换。

    • 4 Kotlin协程会在每个挂起点保存当前的上下文数据,并且在挂起点之后进行恢复。这样,每个状态之间就是相互独立的,可以独立调度。

    • 5 协程的切换,只不过是从一种状态切换到另一种状态,因为不同状态是相互独立的,所以在合适的时机,再切换回来也不会对结果造成影响。

    作者:奔波儿灞取经
    链接:https://juejin.cn/post/7011011123814072327
    来源:掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

    iOS RXSwift 4.6

    iOS
    Observable & Observer 既是可监听序列也是观察者在我们所遇到的事物中,有一部分非常特别。它们既是可监听序列也是观察者。例如:textField的当前文本。它可以看成是由用户输入,而产生的一个文本序列。也可以是由外部文本序列,来控制当...
    继续阅读 »

    Observable & Observer 既是可监听序列也是观察者

    在我们所遇到的事物中,有一部分非常特别。它们既是可监听序列也是观察者

    例如:textField的当前文本。它可以看成是由用户输入,而产生的一个文本序列。也可以是由外部文本序列,来控制当前显示内容的观察者

    // 作为可监听序列
    let observable = textField.rx.text
    observable.subscribe(onNext: { text in show(text: text) })
    // 作为观察者
    let observer = textField.rx.text
    let text: Observable<String?> = ...
    text.bind(to: observer)

    有许多 UI 控件都存在这种特性,例如:switch的开关状态,segmentedControl的选中索引号,datePicker的选中日期等等。

    参考

    另外,框架里面定义了一些辅助类型,它们既是可监听序列也是观察者。如果你能合适的应用这些辅助类型,它们就可以帮助你更准确的描述事物的特征:


    AsyncSubject

    AsyncSubject 将在源 Observable 产生完成事件后,发出最后一个元素(仅仅只有最后一个元素),如果源 Observable 没有发出任何元素,只有一个完成事件。那 AsyncSubject 也只有一个完成事件。

    它会对随后的观察者发出最终元素。如果源 Observable 因为产生了一个 error 事件而中止, AsyncSubject 就不会发出任何元素,而是将这个 error 事件发送出来。


    演示

    let disposeBag = DisposeBag()
    let subject = AsyncSubject<String>()

    subject
    .subscribe { print("Subscription: 1 Event:", $0) }
    .disposed(by: disposeBag)

    subject.onNext("🐶")
    subject.onNext("🐱")
    subject.onNext("🐹")
    subject.onCompleted()

    输出结果:

    Subscription: 1 Event: next(🐹)
    Subscription: 1 Event: completed

    PublishSubject

    PublishSubject 将对观察者发送订阅后产生的元素,而在订阅前发出的元素将不会发送给观察者。如果你希望观察者接收到所有的元素,你可以通过使用 Observable 的 create 方法来创建 Observable,或者使用 ReplaySubject

    如果源 Observable 因为产生了一个 error 事件而中止, PublishSubject 就不会发出任何元素,而是将这个 error 事件发送出来。


    演示

    let disposeBag = DisposeBag()
    let subject = PublishSubject<String>()

    subject
    .subscribe { print("Subscription: 1 Event:", $0) }
    .disposed(by: disposeBag)

    subject.onNext("🐶")
    subject.onNext("🐱")

    subject
    .subscribe { print("Subscription: 2 Event:", $0) }
    .disposed(by: disposeBag)

    subject.onNext("🅰️")
    subject.onNext("🅱️")

    输出结果:

    Subscription: 1 Event: next(🐶)
    Subscription: 1 Event: next(🐱)
    Subscription: 1 Event: next(🅰️)
    Subscription: 2 Event: next(🅰️)
    Subscription: 1 Event: next(🅱️)
    Subscription: 2 Event: next(🅱️)
    收起阅读 »

    Flutter跨进程混合栈渲染的实践——子进程WebView

    前言 首先祝大家中秋节快乐,而明天又要上班啦~ 哈哈哈。不过,立此之处,国庆可期矣~ 好了,书归正传,在此我想分享一下关于我在Flutter 安卓端的跨进程渲染所做的一些实践。 起因 随着项目不断的迭代,功能日益复杂,内存占用也与日俱增。在压测过程中,app的...
    继续阅读 »

    前言


    首先祝大家中秋节快乐,而明天又要上班啦~ 哈哈哈。不过,立此之处,国庆可期矣~


    好了,书归正传,在此我想分享一下关于我在Flutter 安卓端跨进程渲染所做的一些实践。


    起因


    随着项目不断的迭代,功能日益复杂,内存占用也与日俱增。在压测过程中,app的崩溃也多是因为各种原因的内存泄漏异常抖动并最终引发OOM而被系统杀死。按技术栈划分主要集中以下两端:




    1. 原生端本身的代码质量(不当设计、图片加载、对象未释放等)所造成,这点通过回溯及找到组内对应同学修复便可快速解决。




    2. 前端的代码质量(亦如上)所引起,这点则需要找到前端组的同学进行修复,但是跨组/部门的无力感我想大家或多或少都会有一些。




    不管原因几何,结果都是App崩了,我们一方面找到负责的同学抓紧修复外,另一方面也在思考如何从原生解决(至少隔绝)H5导致的App崩溃问题。


    分析


    有一定原生开发经验的我们,便想到了子进程。而通过子进程去分担主进程的内存压力,在各大厂也均有应用,可证明它是一个比较成熟的方案,而就单进程Web-View来说,市面上也有不少成功的Android框架及技术方案的分享。


    纯原生(Android)应用来讲,因为栈的统一,接入一个子进程web-view,还是比较方便的,大致开启一个子进程,然后startActivity即可,无需关心栈的管理。但是Flutter应用则分为两种栈:


    1 Android栈 (管理activity)

    2 Flutter栈 (管理flutter的route)

    在实际应用中,H5与原生均有复杂的交互,这里不仅体现在功能上的,还包括UI上的。就算不考虑跳转动画的问题,Flutter栈内的叠加(Flutter和H5)就需要一个单独的栈管理器来处理(如Flutter Boost)。


    在考虑到投入产出成本以及问题的本质并非栈管理器可以解决的情况下(如 Flutter页面部分是H5等情况),我决定用Flutter自带的Texture Widget进行H5的显示,这样统一了栈的管理,同时Texture Widget可以自由调整大小,做到任意Flutter页面的(部分)嵌入。Texture Widget需要一个Surface,而Surface又具有天然的跨进程属性这无疑大大方便了开发。


    实践及结果


    经过一段时间的研究和设计,最终有了一个Alpha版的框架,在此我对架构做一下简单的介绍:


    flutter_remote_view_framework.png


    按进程划分


    主要分为两部分:


    1. 主进程包含Flutter及相应的平台部分,承担surface的创建、展示、交互等的发起方。

    2. 子进程主要包含zygote activity , webview 等。

    进程之间通过Binder进行通信。


    按流程划分


    主要分为三部分:


    1. Flutter侧,主要发起创建指令并最终消费子进程的渲染数据。

    2. 平台侧,主要承担Flutter与子进程的web-view的通信转发功能,同时承担surface的创建功能,
    也是真正与子进程通信的模块。

    3. 子进程,主要负责webview的创建,并使用主进程所提供的surface进行H5内容的输出。

    所遇到的一些难点


    系统弹窗的权限问题


    在子进程中使用web-view,并渲染在指定surface 上需要借助virtual displaypresentation,但是如果presentation的创建不是基于activity context,那么则需要一个系统权限才可以正常工作,这对于我们的需求来说,是不可接受的。


    为此便创建了一个Zygote activity,它工作于后台,主要责任就是提供一个context和部分presentation的创建工作。同时借助内存泄漏以尽可能长的保留它的存活时间。


    交互


    由于系统事件(如 触摸)是分发到当前(前台)activity stack的栈顶activity,那么当Zygote activity工作于后台的时候,我们的触摸事件是分发到了Main activity,h5则无法响应任何交互。因此我们需要在主进程做事件的分拣并通过binder转发到子进程,以此来让H5消费到属于它的事件。


    触摸事件的分发及错位问题


    上面的问题细分后,可以明确我们需要解决Flutter端的H5页面在非栈顶的情况下不能消费事件,因为Flutter所接受的事件由Main Activity提供,所以事件的分发也在此处处理,为此我增加了一个栈协调器(相对于栈管理要简单一些),以获取当前Flutter端的栈情况,并做出正确的分发。


    经过实际实践,效果还是不错的,但也发现一个问题:点击坐标错位。经过研究发现,这主要是Flutter端布局和web view端布局不一致导致的,换言之需要计算在Flutter点击时的position相对于那个Texture widget内的相对位置,并做转换再进行分发。


    通信


    客观的说,这里并没有什么难点,但比较,因为操作涉及到UI,所以不仅要考虑到进程间的通信、线程切换还有各进程的主、子线程的切换。并且按领域进行划分话,又分为共有和私有通信,为此增加了communicate hub以区分各领域的通信。


    结果


    在一些主要问题解决后,得到了最终的效果图(debug mode):


    small.gif


    这个Demo并不满足生产,但是验证了它的可行性,而就真正的上线来说,还是有一部分工作要做的,如坐标转换器优化(下一个版本要做的)、协调器、垃圾回收、兜底策略等等。


    到此我的分享就结束了,希望对大家有所帮助,同时也殷切希望有大佬能指出设计的不足,谢谢大家的阅读。


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

    iOS RXSwift 4.5

    iOS
    Observer - 观察者观察者 是用来监听事件,然后它需要这个事件做出响应。例如:弹出提示框就是观察者,它对点击按钮这个事件做出响应。响应事件的都是观察者在 Observable 章节,我们举了个几个例子来介绍什么是可监听序列...
    继续阅读 »

    Observer - 观察者

    观察者 是用来监听事件,然后它需要这个事件做出响应。例如:弹出提示框就是观察者,它对点击按钮这个事件做出响应。

    响应事件的都是观察者

    在 Observable 章节,我们举了个几个例子来介绍什么是可监听序列。那么我们还是用这几个例子来解释一下什么是观察者

    • 当室温高于 33 度时,打开空调降温

      1

      打开空调降温就是观察者 Observer<Double>

    • 当《海贼王》更新一集时,我们就立即观看这一集

      1

      观看这一集就是观察者 Observer<OnePieceEpisode>

    • 当取到 JSON 时,将它打印出来

      1

      将它打印出来就是观察者 Observer<JSON>

    • 当任务结束后,提示用户任务已完成

      1

      提示用户任务已完成就是观察者 Observer<Void>

    如何创建观察者

    现在我们已经知道观察者主要是做什么的了。那么我们要怎么创建它们呢?

    和 Observable 一样,框架已经帮我们创建好了许多常用的观察者。例如:view 是否隐藏,button 是否可点击, label 的当前文本,imageView 的当前图片等等。

    另外,有一些自定义的观察者是需要我们自己创建的。这里介绍一下创建观察者最基本的方法,例如,我们创建一个弹出提示框的的观察者

    tap.subscribe(onNext: { [weak self] in
    self?.showAlert()
    }, onError: { error in
    print("发生错误: \(error.localizedDescription)")
    }, onCompleted: {
    print("任务完成")
    })

    创建观察者最直接的方法就是在 Observable 的 subscribe 方法后面描述,事件发生时,需要如何做出响应。而观察者就是由后面的 onNextonErroronCompleted的这些闭包构建出来的。

    以上是创建观察者最常见的方法。当然你还可以通过其他的方式来创建观察者,可以参考一下 AnyObserver 和 Binder

    特征观察者

    和 Observable 一样,观察者也存特征观察者,例如:


    AnyObserver

    AnyObserver 可以用来描叙任意一种观察者。

    例如:


    打印网络请求结果:

    URLSession.shared.rx.data(request: URLRequest(url: url))
    .subscribe(onNext: { data in
    print("Data Task Success with count: \(data.count)")
    }, onError: { error in
    print("Data Task Error: \(error)")
    })
    .disposed(by: disposeBag)

    可以看作是:

    let observer: AnyObserver<Data> = AnyObserver { (event) in
    switch event {
    case .next(let data):
    print("Data Task Success with count: \(data.count)")
    case .error(let error):
    print("Data Task Error: \(error)")
    default:
    break
    }
    }

    URLSession.shared.rx.data(request: URLRequest(url: url))
    .subscribe(observer)
    .disposed(by: disposeBag)

    用户名提示语是否隐藏:

    usernameValid
    .bind(to: usernameValidOutlet.rx.isHidden)
    .disposed(by: disposeBag)

    可以看作是:

    let observer: AnyObserver<Bool> = AnyObserver { [weak self] (event) in
    switch event {
    case .next(let isHidden):
    self?.usernameValidOutlet.isHidden = isHidden
    default:
    break
    }
    }

    usernameValid
    .bind(to: observer)
    .disposed(by: disposeBag)

    下一节将介绍 Binder 以及 usernameValidOutlet.rx.isHidden 的由来。


    Binder

    Binder 主要有以下两个特征:

    • 不会处理错误事件
    • 确保绑定都是在给定 Scheduler 上执行(默认 MainScheduler

    一旦产生错误事件,在调试环境下将执行 fatalError,在发布环境下将打印错误信息。


    示例

    在介绍 AnyObserver 时,我们举了这样一个例子:

    let observer: AnyObserver<Bool> = AnyObserver { [weak self] (event) in
    switch event {
    case .next(let isHidden):
    self?.usernameValidOutlet.isHidden = isHidden
    default:
    break
    }
    }

    usernameValid
    .bind(to: observer)
    .disposed(by: disposeBag)

    由于这个观察者是一个 UI 观察者,所以它在响应事件时,只会处理 next 事件,并且更新 UI 的操作需要在主线程上执行。

    因此一个更好的方案就是使用 Binder

    let observer: Binder<Bool> = Binder(usernameValidOutlet) { (view, isHidden) in
    view.isHidden = isHidden
    }

    usernameValid
    .bind(to: observer)
    .disposed(by: disposeBag)

    Binder 可以只处理 next 事件,并且保证响应 next 事件的代码一定会在给定 Scheduler 上执行,这里采用默认的 MainScheduler


    复用

    由于页面是否隐藏是一个常用的观察者,所以应该让所有的 UIView 都提供这种观察者:

    extension Reactive where Base: UIView {
    public var isHidden: Binder<Bool> {
    return Binder(self.base) { view, hidden in
    view.isHidden = hidden
    }
    }
    }
    usernameValid
    .bind(to: usernameValidOutlet.rx.isHidden)
    .disposed(by: disposeBag)

    这样你不必为每个 UI 控件单独创建该观察者。这就是 usernameValidOutlet.rx.isHidden 的由来,许多 UI 观察者 都是这样创建的:

    • 按钮是否可点击 button.rx.isEnabled

      extension Reactive where Base: UIControl {
      public var isEnabled: Binder<Bool> {
      return Binder(self.base) { control, value in
      control.isEnabled = value
      }
      }
      }
    • label 的当前文本 label.rx.text

      extension Reactive where Base: UILabel {
      public var text: Binder<String?> {
      return Binder(self.base) { label, text in
      label.text = text
      }
      }
      }

    你也可以用这种方式来创建自定义的 UI 观察者

    收起阅读 »

    iOS RXSwift 4.4

    iOS
    SignalSignal 和 Driver 相似,唯一的区别是,Driver 会对新观察者回放(重新发送)上一个元素,而 Signal 不会对新观察者回放上一个元素。他有如下特性:不会产生 ...
    继续阅读 »

    Signal

    Signal 和 Driver 相似,唯一的区别是,Driver 对新观察者回放(重新发送)上一个元素,而 Signal 不会对新观察者回放上一个元素。

    他有如下特性:

    • 不会产生 error 事件
    • 一定在 MainScheduler 监听(主线程监听)
    • 共享附加作用

    现在,我们来看看以下代码是否合理:

    let textField: UITextField = ...
    let nameLabel: UILabel = ...
    let nameSizeLabel: UILabel = ...

    let state: Driver<String?> = textField.rx.text.asDriver()

    let observer = nameLabel.rx.text
    state.drive(observer)

    // ... 假设以下代码是在用户输入姓名后运行

    let newObserver = nameSizeLabel.rx.text
    state.map { $0?.count.description }.drive(newObserver)

    这个例子只是将用户输入的姓名绑定到对应的标签上。当用户输入姓名后,我们创建了一个新的观察者,用于订阅姓名的字数。那么问题来了,订阅时,展示字数的标签会立即更新吗?

    嗯、、、 因为 Driver 会对新观察者回放上一个元素(当前姓名),所以这里是会更新的。在对他进行订阅时,标签的默认文本会被刷新。这是合理的。

    那如果我们用 Driver 来描述点击事件呢,这样合理吗?

    let button: UIButton = ...
    let showAlert: (String) -> Void = ...

    let event: Driver<Void> = button.rx.tap.asDriver()

    let observer: () -> Void = { showAlert("弹出提示框1") }
    event.drive(onNext: observer)

    // ... 假设以下代码是在用户点击 button 后运行

    let newObserver: () -> Void = { showAlert("弹出提示框2") }
    event.drive(onNext: newObserver)

    当用户点击一个按钮后,我们创建一个新的观察者,来响应点击事件。此时会发生什么?Driver 会把上一次的点击事件回放给新观察者。所以,这里的 newObserver 在订阅时,就会接受到上次的点击事件,然后弹出提示框。这似乎不太合理。

    因此像这类型的事件序列,用 Driver 建模就不合适。于是我们就引入了 Signal:

    ...

    let event: Signal<Void> = button.rx.tap.asSignal()

    let observer: () -> Void = { showAlert("弹出提示框1") }
    event.emit(onNext: observer)

    // ... 假设以下代码是在用户点击 button 后运行

    let newObserver: () -> Void = { showAlert("弹出提示框2") }
    event.emit(onNext: newObserver)

    在同样的场景中,Signal 不会把上一次的点击事件回放给新观察者,而只会将订阅后产生的点击事件,发布给新观察者。这正是我们所需要的。

    结论

    一般情况下状态序列我们会选用 Driver 这个类型,事件序列我们会选用 Signal 这个类型。

    参考


    ControlEvent

    ControlEvent 专门用于描述 UI 控件所产生的事件,它具有以下特征:

    • 不会产生 error 事件
    • 一定在 MainScheduler 订阅(主线程订阅)
    • 一定在 MainScheduler 监听(主线程监听)
    • 共享附加作用
    收起阅读 »

    你知道如何批量创建一批邮箱吗?

    1.前期准备 搭建邮件服务器需要一些“基础建设”,包括如下 一台服务器 推荐centos 一个域名 1.1 配置细节 邮件服务器是通过SMTP协议进行通信,为了让服务器能够成功接收邮件,我们需要打开25这个端口,并允许访问25端口。同时如果你需要使用像类似...
    继续阅读 »

    1.前期准备


    搭建邮件服务器需要一些“基础建设”,包括如下



    • 一台服务器 推荐centos

    • 一个域名


    1.1 配置细节


    邮件服务器是通过SMTP协议进行通信,为了让服务器能够成功接收邮件,我们需要打开25这个端口,并允许访问25端口。同时如果你需要使用像类似foxmail这种客户端接发收邮件,还需要支持POP3协议,需要打开110端口。换句话说为了保证邮件服务的正常使用,需要开启25和110这两个端口



    关于 POP3协议(Post Office Protocol 3):协议主要用于支持使用客户端远程管理在服务器上的电子邮件,将电子邮件存储到本地主机



    下图是阿里云服务器配置安全策略组的规则,在其中加入一条访问规则


    image.png


    接下来是域名,需要配置域名解析,配置主机记录


    如下图是域名的解析配置,主要包括几个记录数值




    • MX类:增加 MX 记录,类型选择 MX记录,值可以填写主机名,也可以填写你的公网ip地址也可以是mail.example.com。如果配置的是域名,还需要新增一条A类型的记录,主机记录定义为:mail,具体看下图




    • A类:该配置主要用来支持客户端接收邮件(比如:foxmail)分别添加smtp、imap、pop等配置,记录值为 ip




    配置完如下图所示,可以在列表中看到配置好的,


    image.png


    2 服务器安装


    2.1 Postfix



    关于 postfix:Postfix 是实现 SMTP 协议的软件,也叫做邮件发送服务器,负责对邮件进行转发,具体的转发规则,就需要我们对postfix的配置进行修改



    我使用的是阿里云的服务器,首先我们安装邮件服务`postfix'



    • 安装


    yum install postfix // 服务器安装 


    • 配置


    安装成功之后,修改配置,通过vi /etc/postfix/main.cf 命令行修改以下配置


    myhostname =  email.example.com //  设置系统的主机名

    mydomain = example.com  // 设置域名(我们将让此处设置将成为E-mail地址“@”后面的部分)

    myorigin = $mydomain  // 将发信地址“@”后面的部分设置为域名(非系统主机名)

    inet_interfaces = all  // 接受来自所有网络的请求

    mydestination = $myhostname, localhost.$mydomain, localhost, $mydomain  // 指定发给本地邮件的域名

    home_mailbox = Maildir/  // 指定用户邮箱目录

    # 规定邮件最大尺寸为10M
    message_size_limit = 10485760
    # 规定收件箱最大容量为1G
    mailbox_size_limit = 1073741824
    # SMTP认证
    smtpd_sasl_type = dovecot
    smtpd_sasl_path = private/auth
    smtpd_sasl_auth_enable = yes
    smtpd_sasl_security_options = noanonymous
    smtpd_sasl_local_domain = $myhostname
    smtpd_recipient_restrictions = permit_mynetworks,permit_auth_destination,permit_sasl_authenticated,reject


    下图是postfix中主要的参数
    image.png



    • 启动


    配置完postfix的,启动服务


    postfix check   // 检查配置文件是否正确
    systemctl start postfix //开启postfix服务
    systemctl enable postfix //设置postfix服务开机启动

    完成postfix的配置,接下来我们还需要安装dovecot


    2.2 Dovecot



    关于 Dovecot:是一款能够为Linux系统提供IMAP和POP3电子邮件服务的开源服务程序,安全性极高,配置简单,执行速度快,而且占用的服务器硬件资源也较少。上文提到POP3/IMAP是从邮件服务器中读取邮件时使用的协议




    • 安装


    yum install dovecot // 服务器安装 


    • 配置


    安装成功之后,修改配置,通过vi /etc/dovecot/dovecot.conf 命令行修改以下配置


    protocols = imap pop3 lmtp listen = *, 

    #新添加以下配置 #

    !include conf.d/10-auth.conf

    ssl = no

    disable_plaintext_auth = no

    mail_location = maildir:~/Maildir



    • 启动


    systemctl start dovecot   //开启dovecot服务
    systemctl enable dovecot //置dovecot服务开机启动

    完成以上两个服务的配置,你离成功就近一步了!



    啊乐同学:postfix与dovecot这两个其实有什么区别?



    答:postfix主要做发送邮件使用,而dovecot主要做接收使用,两者结合才能完成一个完整的邮件服务


    3 新建用户


    搭建完邮件服务器之后,我们需要创建用户来完成 邮件的接收和发送



    • 如何创建用户


    useradd tree/ 新增用户
    passwd tree // 设置用户密码


    啊乐同学:如果这样我创建100个邮箱用户,岂不是很浪费时间?



    莫慌,我们写个shell脚本,批量创建就可以解决你这个问题


    创建一个文件,createUser.sh 内容如下


    /bash
    #user.txt 为需要创建的用户的文件passwd.txt为随机生成密码
    USER_FILE=user.txt
    pass_FILE=passwd.txt
    for user in `cat user.txt`
    do
    id $user &> /dev/null #查看用户是否存在
    if [ $? -eq 0 ]
    then
    echo "The $user already exist"
    else
    useradd $user #创建用户
    if [ $? -eq 0 ]
    then
    echo "$user create sucessful"
    PASSWD=$(echo $RANDOM |md5sum |cut -c 1-8) #随机生成数字
    echo $PASSWD |passwd --stdin $user &>/dev/null #修改用户密码
    echo -e "$user\'$PASSWD'\'$(date +%Y%m%d)'" >> $pass_FILE #将用户,密码,日期输入到文件中
    fi
    fi
    done

    前提需要建立一个user.txt 来维护我们要创建的用户,比如


    tree
    shujiang

    脚本会根据我们列出的用户名去批量生成用户


    4.测试邮箱


    搭建好服务以及完成用户的创建,接下来就是测试邮件是否正常接收环节了


    我使用的是foxmail来做验证


    image.png


    这个用户名就是我们上一节创建的用户名称,完成创建之后,我们通过发送邮件来测试是否能够成功接收


    image.png


    还有一种方式就是借助telnet去做测试,这里不做大篇幅介绍。最原始的方式



    阿乐同学:如果我每个新建的邮箱用户,我都得去配置一个客户端去接收邮寄,岂不是很费劲,有没有其他方式?



    有的,换个角度思考,你可以通过配置邮件转发,将所有邮件接收都转发到某一个用户的邮箱中去,你就可以只在该邮箱查阅邮件(我开始怀疑你的动机,是不是搞什么批量注册!)


    具体如下,需要配置下第二节中提到的postfix配置文件,在文件最后添加


    virtual_alias_domains = ensbook.com  mail.ensbook.com
    virtual_alias_maps = hash:/etc/postfix/virtual

    完成配置之后,我查阅网上一些资料,需要配置/etc/postfix/virtual文件,该文件主要用来管理电子邮件转发规则的


    于是我尝试修改/etc/postfix/virtual文件,并添加一下信息


    image.png


    这条规则的含义是:所有邮件发送至 @ensbook.com 转发到 qq邮箱


    发现竟然没有生效,最后是创建一个virtual的用户实现转发接收的。如果你看得出问题,记得在评论区告诉我



    阿乐同学:我接收不到邮箱,又不知道什么问题,如何排查?



    你可以通过tail -n /var/log/maillog查看邮件日志


    image.png


    最后


    通过上文的了解,我们不难看到,一个域名邮件服务器的创建其实很简单,而且技术很老。但是无论老不老,能够解决我们的需求就好。如果你有其他方式实现,欢迎在评论区留言。



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

    收起阅读 »

    JavaScript深浅拷贝的实现

    前置知识 对象类型在赋值的过程中其实是复制了地址,从而会导致改变了一方其他也都被改变的情况 let a = { age: 1 } let b = a a.age = 2 console.log(b.a...
    继续阅读 »

    前置知识



    • 对象类型在赋值的过程中其实是复制了地址,从而会导致改变了一方其他也都被改变的情况


        let a = {
    age: 1
    }
    let b = a
    a.age = 2
    console.log(b.age) // 2

    浅拷贝



    • Object.assign : 拷贝所有的属性值到新的对象中,如果属性值是对象的话,拷贝的是地址,所以并不是深拷贝


        let a = {
    age: 1
    }
    let b = Object.assign({}, a)
    a.age = 2
    console.log(b.age) // 1


    • 通过展开运算符 ... 来实现浅拷贝


        let a = {
    age: 1
    }
    let b = {...a}
    a.age = 2
    console.log(b.age) // 1

    深拷贝



    • JSON.parse(JSON.stringify(object))

      • 会忽略 undefined

      • 会忽略 symbol

      • 不能序列化函数

      • 不能解决循环引用的对象,会报错抛出异常




        let a = {
    age: 1,
    jobs: {
    first: 'FE'
    }
    }
    let b = JSON.parse(JSON.stringify(a))
    a.jobs.first = 'native'
    console.log(b.jobs.first) // FE

    let a = {
    age: undefined,
    sex: Symbol('male'),
    jobs: function() {},
    name: 'yck'
    }
    let b = JSON.parse(JSON.stringify(a))
    console.log(b) // {name: "yck"}


    • 递归


        function isObject(obj) {
    //Object.prototype.toString.call(obj) === '[object Object]'要保留数组形式,用在这里并不合适
    return typeof obj === 'object' && obj != null
    }

    function cloneDeep1(obj){
    if(!isObject(obj)) return obj
    var newObj = Array.isArray(obj)? [] : {}
    for (var key in obj) {
    if (obj.hasOwnProperty(key)) {
    newObj[key] = isObject(obj[key])? cloneDeep1(obj[key]) : obj[key]
    }
    }
    return newObj
    }


    • 问题:递归方法最大的问题在于爆栈,当数据的层次很深是就会栈溢出,例如循环引用



    var a = {
    name: "muyiy",
    a1: undefined,
    a2: null,
    a3: 123,
    book: {title: "You Don't Know JS", price: "45"}
    }
    a.circleRef = a

    // TypeError: Converting circular structure to JSON
    JSON.parse(JSON.stringify(a))

    //Uncaught RangeError: Maximum call stack size exceeded at Object.hasOwnProperty (<anonymous>)
    cloneDeep1(a)



    • 解决方法:循环检测(设置一个数组或者哈希表存储已拷贝过的对象,当检测到当前对象已存在于哈希表中时,取出该值并返回即可)


    //哈希表
    function cloneDeep3(source, hash = new WeakMap()) {

    if (!isObject(source)) return source;
    if (hash.has(source)) return hash.get(source); // 新增代码,查哈希表

    var target = Array.isArray(source) ? [] : {};
    hash.set(source, target); // 新增代码,哈希表设值

    for(var key in source) {
    if (Object.prototype.hasOwnProperty.call(source, key)) {
    if (isObject(source[key])) {
    target[key] = cloneDeep3(source[key], hash); // 新增代码,传入哈希表
    } else {
    target[key] = source[key];
    }
    }
    }
    return target;
    }

    //数组
    function cloneDeep3(source, uniqueList) {

    if (!isObject(source)) return source;
    if (!uniqueList) uniqueList = []; // 新增代码,初始化数组

    var target = Array.isArray(source) ? [] : {};

    // 数据已经存在,返回保存的数据
    var uniqueData = find(uniqueList, source);
    if (uniqueData) {
    return uniqueData.target;
    };

    // 数据不存在,保存源数据,以及对应的引用
    uniqueList.push({
    source: source,
    target: target
    });

    for(var key in source) {
    if (Object.prototype.hasOwnProperty.call(source, key)) {
    if (isObject(source[key])) {
    target[key] = cloneDeep3(source[key], uniqueList); // 新增代码,传入数组
    } else {
    target[key] = source[key];
    }
    }
    }
    return target;
    }

    // 新增方法,用于查找
    function find(arr, item) {
    for(var i = 0; i < arr.length; i++) {
    if (arr[i].source === item) {
    return arr[i];
    }
    }
    return null;
    }

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

    收起阅读 »

    为什么 Vue2 this 能够直接获取到 data 和 methods ? 源码揭秘!

    1. 前言 写相对很难的源码,耗费了自己的时间和精力,也没收获多少阅读点赞,其实是一件挺受打击的事情。从阅读量和读者受益方面来看,不能促进作者持续输出文章。 所以转变思路,写一些相对通俗易懂的文章。其实源码也不是想象的那么难,至少有很多看得懂。歌德曾说:读一...
    继续阅读 »

    1. 前言



    写相对很难的源码,耗费了自己的时间和精力,也没收获多少阅读点赞,其实是一件挺受打击的事情。从阅读量和读者受益方面来看,不能促进作者持续输出文章。
    所以转变思路,写一些相对通俗易懂的文章。其实源码也不是想象的那么难,至少有很多看得懂。歌德曾说:读一本好书,就是在和高尚的人谈话。
    同理可得:读源码,也算是和作者的一种学习交流的方式。



    本文源于一次源码共读群里群友的提问,请问,“为什么 data 中的数据可以用 this 直接获取到啊”,当时我翻阅源码做出了解答。想着如果下次有人再次问到,我还需要回答一次。当时打算有空写篇文章告诉读者自己探究原理,于是就有了这篇文章。


    阅读本文,你将学到:


    1. 如何学习调试 vue2 源码
    2. data 中的数据为什么可以用 this 直接获取到
    3. methods 中的方法为什么可以用 this 直接获取到
    4. 学习源码中优秀代码和思想,投入到自己的项目中

    本文不难,用过 Vue 的都看得懂,希望大家动手调试和学会看源码。


    看源码可以大胆猜测,最后小心求证。


    2. 示例:this 能够直接获取到 data 和 methods


    众所周知,这样是可以输出我是若川的。好奇的人就会思考为啥 this 就能直接访问到呢。


    const vm = new Vue({
    data: {
    name: '我是若川',
    },
    methods: {
    sayName(){
    console.log(this.name);
    }
    },
    });
    console.log(vm.name); // 我是若川
    console.log(vm.sayName()); // 我是若川

    那么为什么 this.xxx 能获取到data里的数据,能获取到 methods 方法。


    我们自己构造写的函数,如何做到类似Vue的效果呢。


    function Person(options){

    }

    const p = new Person({
    data: {
    name: '若川'
    },
    methods: {
    sayName(){
    console.log(this.name);
    }
    }
    });

    console.log(p.name);
    // undefined
    console.log(p.sayName());
    // Uncaught TypeError: p.sayName is not a function

    如果是你,你会怎么去实现呢。带着问题,我们来调试 Vue2源码学习。


    3. 准备环境调试源码一探究竟


    可以在本地新建一个文件夹examples,新建文件index.html文件。
    <body></body>中加上如下js


    <script src="https://unpkg.com/vue@2.6.14/dist/vue.js"></script>
    <script>
    const vm = new Vue({
    data: {
    name: '我是若川',
    },
    methods: {
    sayName(){
    console.log(this.name);
    }
    },
    });
    console.log(vm.name);
    console.log(vm.sayName());
    </script>

    再全局安装npm i -g http-server启动服务。


    npm i -g http-server
    cd examples
    http-server .
    // 如果碰到端口被占用,也可以指定端口
    http-server -p 8081 .

    这样就能在http://localhost:8080/打开刚写的index.html页面了。


    对于调试还不是很熟悉的读者,可以看这篇文章《前端容易忽略的 debugger 调试技巧》,截图标注的很详细。



    调试:在 F12 打开调试,source 面板,在例子中const vm = new Vue({打上断点。



    如下图所示


    刷新页面后按F11进入函数,这时断点就走进了 Vue 构造函数。


    3.1 Vue 构造函数


    function Vue (options) {
    if (!(this instanceof Vue)
    ) {
    warn('Vue is a constructor and should be called with the `new` keyword');
    }
    this._init(options);
    }
    // 初始化
    initMixin(Vue);
    stateMixin(Vue);
    eventsMixin(Vue);
    lifecycleMixin(Vue);
    renderMixin(Vue);

    值得一提的是:if (!(this instanceof Vue)){} 判断是不是用了 new 关键词调用构造函数。
    一般而言,我们平时应该不会考虑写这个。


    当然看源码库也可以自己函数内部调用 new 。但 vue 一般一个项目只需要 new Vue() 一次,所以没必要。


    jQuery 源码的就是内部 new ,对于使用者来说就是无new构造。


    jQuery = function( selector, context ) {
    // 返回new之后的对象
    return new jQuery.fn.init( selector, context );
    };

    因为使用 jQuery 经常要调用。
    其实 jQuery 也是可以 new 的。和不用 new 是一个效果。


    如果不明白 new 操作符的用处,可以看我之前的文章。面试官问:能否模拟实现JS的new操作符



    调试:继续在this._init(options);处打上断点,按F11进入函数。



    3.2 _init 初始化函数


    进入 _init 函数后,这个函数比较长,做了挺多事情,我们猜测跟datamethods相关的实现在initState(vm)函数里。


    // 代码有删减
    function initMixin (Vue) {
    Vue.prototype._init = function (options) {
    var vm = this;
    // a uid
    vm._uid = uid$3++;

    // a flag to avoid this being observed
    vm._isVue = true;
    // merge options
    if (options && options._isComponent) {
    // optimize internal component instantiation
    // since dynamic options merging is pretty slow, and none of the
    // internal component options needs special treatment.
    initInternalComponent(vm, options);
    } else {
    vm.$options = mergeOptions(
    resolveConstructorOptions(vm.constructor),
    options || {},
    vm
    );
    }

    // expose real self
    vm._self = vm;
    initLifecycle(vm);
    initEvents(vm);
    initRender(vm);
    callHook(vm, 'beforeCreate');
    initInjections(vm); // resolve injections before data/props
    // 初始化状态
    initState(vm);
    initProvide(vm); // resolve provide after data/props
    callHook(vm, 'created');
    };
    }


    调试:接着我们在initState(vm)函数这里打算断点,按F8可以直接跳转到这个断点,然后按F11接着进入initState函数。



    3.3 initState 初始化状态


    从函数名来看,这个函数主要实现功能是:


    初始化 props
    初始化 methods
    监测数据
    初始化 computed
    初始化 watch

    function initState (vm) {
    vm._watchers = [];
    var opts = vm.$options;
    if (opts.props) { initProps(vm, opts.props); }
    // 有传入 methods,初始化方法
    if (opts.methods) { initMethods(vm, opts.methods); }
    // 有传入 data,初始化 data
    if (opts.data) {
    initData(vm);
    } else {
    observe(vm._data = {}, true /* asRootData */);
    }
    if (opts.computed) { initComputed(vm, opts.computed); }
    if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch);
    }
    }


    我们重点来看初始化 methods,之后再看初始化 data




    调试:在 initMethods 这句打上断点,同时在initData(vm)处打上断点,看完initMethods函数后,可以直接按F8回到initData(vm)函数。
    继续按F11,先进入initMethods函数。



    3.4 initMethods 初始化方法


    function initMethods (vm, methods) {
    var props = vm.$options.props;
    for (var key in methods) {
    {
    if (typeof methods[key] !== 'function') {
    warn(
    "Method \"" + key + "\" has type \"" + (typeof methods[key]) + "\" in the component definition. " +
    "Did you reference the function correctly?",
    vm
    );
    }
    if (props && hasOwn(props, key)) {
    warn(
    ("Method \"" + key + "\" has already been defined as a prop."),
    vm
    );
    }
    if ((key in vm) && isReserved(key)) {
    warn(
    "Method \"" + key + "\" conflicts with an existing Vue instance method. " +
    "Avoid defining component methods that start with _ or $."
    );
    }
    }
    vm[key] = typeof methods[key] !== 'function' ? noop : bind(methods[key], vm);
    }
    }

    initMethods函数,主要有一些判断。


    判断 methods 中的每一项是不是函数,如果不是警告。
    判断 methods 中的每一项是不是和 props 冲突了,如果是,警告。
    判断 methods 中的每一项是不是已经在 new Vue实例 vm 上存在,而且是方法名是保留的 _ $ (在JS中一般指内部变量标识)开头,如果是警告。

    除去这些判断,我们可以看出initMethods函数其实就是遍历传入的methods对象,并且使用bind绑定函数的this指向为vm,也就是new Vue的实例对象。


    这就是为什么我们可以通过this直接访问到methods里面的函数的原因


    我们可以把鼠标移上 bind 变量,按alt键,可以看到函数定义的地方,这里是218行,点击跳转到这里看 bind 的实现。


    3.4.1 bind 返回一个函数,修改 this 指向


    function polyfillBind (fn, ctx) {
    function boundFn (a) {
    var l = arguments.length;
    return l
    ? l > 1
    ? fn.apply(ctx, arguments)
    : fn.call(ctx, a)
    : fn.call(ctx)
    }

    boundFn._length = fn.length;
    return boundFn
    }

    function nativeBind (fn, ctx) {
    return fn.bind(ctx)
    }

    var bind = Function.prototype.bind
    ? nativeBind
    : polyfillBind;

    简单来说就是兼容了老版本不支持 原生的bind函数。同时兼容写法,对参数多少做出了判断,使用callapply实现,据说是因为性能问题。


    如果对于call、apply、bind的用法和实现不熟悉,可以查看我在面试官问系列中写的面试官问:能否模拟实现JS的call和apply方法
    面试官问:能否模拟实现JS的bind方法



    调试:看完了initMethods函数,按F8回到上文提到的initData(vm)函数断点处。



    3.5 initData 初始化 data


    initData 函数也是一些判断。主要做了如下事情:


    先给 _data 赋值,以备后用。
    最终获取到的 data 不是对象给出警告。
    遍历 data ,其中每一项:
    如果和 methods 冲突了,报警告。
    如果和 props 冲突了,报警告。
    不是内部私有的保留属性,做一层代理,代理到 _data 上。
    最后监测 data,使之成为响应式的数据。

    function initData (vm) {
    var data = vm.$options.data;
    data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {};
    if (!isPlainObject(data)) {
    data = {};
    warn(
    'data functions should return an object:\n' +
    'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
    vm
    );
    }
    // proxy data on instance
    var keys = Object.keys(data);
    var props = vm.$options.props;
    var methods = vm.$options.methods;
    var i = keys.length;
    while (i--) {
    var key = keys[i];
    {
    if (methods && hasOwn(methods, key)) {
    warn(
    ("Method \"" + key + "\" has already been defined as a data property."),
    vm
    );
    }
    }
    if (props && hasOwn(props, key)) {
    warn(
    "The data property \"" + key + "\" is already declared as a prop. " +
    "Use prop default value instead.",
    vm
    );
    } else if (!isReserved(key)) {
    proxy(vm, "_data", key);
    }
    }
    // observe data
    observe(data, true /* asRootData */);
    }

    3.5.1 getData 获取数据


    是函数时调用函数,执行获取到对象。


    function getData (data, vm) {
    // #7573 disable dep collection when invoking data getters
    pushTarget();
    try {
    return data.call(vm, vm)
    } catch (e) {
    handleError(e, vm, "data()");
    return {}
    } finally {
    popTarget();
    }
    }

    3.5.2 proxy 代理


    其实就是用 Object.defineProperty 定义对象


    这里用处是:this.xxx 则是访问的 this._data.xxx


    /**
    * Perform no operation.
    * Stubbing args to make Flow happy without leaving useless transpiled code
    * with ...rest (https://flow.org/blog/2017/05/07/Strict-Function-Call-Arity/).
    */
    function noop (a, b, c) {}
    var sharedPropertyDefinition = {
    enumerable: true,
    configurable: true,
    get: noop,
    set: noop
    };

    function proxy (target, sourceKey, key) {
    sharedPropertyDefinition.get = function proxyGetter () {
    return this[sourceKey][key]
    };
    sharedPropertyDefinition.set = function proxySetter (val) {
    this[sourceKey][key] = val;
    };
    Object.defineProperty(target, key, sharedPropertyDefinition);
    }

    3.5.3 Object.defineProperty 定义对象属性


    Object.defineProperty 算是一个非常重要的API。还有一个定义多个属性的API:Object.defineProperties(obj, props) (ES5)


    Object.defineProperty 涉及到比较重要的知识点,面试也常考。


    value——当试图获取属性时所返回的值。
    writable——该属性是否可写。
    enumerable——该属性在for in循环中是否会被枚举。
    configurable——该属性是否可被删除。
    set()——该属性的更新操作所调用的函数。
    get()——获取属性值时所调用的函数。

    详细举例见此链接


    3.6 文中出现的一些函数,最后统一解释下


    3.6.1 hasOwn 是否是对象本身拥有的属性


    调试模式下,按alt键,把鼠标移到方法名上,可以看到函数定义的地方。点击可以跳转。


    /**
    * Check whether an object has the property.
    */
    var hasOwnProperty = Object.prototype.hasOwnProperty;
    function hasOwn (obj, key) {
    return hasOwnProperty.call(obj, key)
    }

    hasOwn({ a: undefined }, 'a') // true
    hasOwn({}, 'a') // false
    hasOwn({}, 'hasOwnProperty') // false
    hasOwn({}, 'toString') // false
    // 是自己的本身拥有的属性,不是通过原型链向上查找的。

    3.6.2 isReserved 是否是内部私有保留的字符串$ 和 _ 开头


    /**
    * Check if a string starts with $ or _
    */
    function isReserved (str) {
    var c = (str + '').charCodeAt(0);
    return c === 0x24 || c === 0x5F
    }
    isReserved('_data'); // true
    isReserved('$options'); // true
    isReserved('data'); // false
    isReserved('options'); // false

    4. 最后用60余行代码实现简化版


    function noop (a, b, c) {}
    var sharedPropertyDefinition = {
    enumerable: true,
    configurable: true,
    get: noop,
    set: noop
    };
    function proxy (target, sourceKey, key) {
    sharedPropertyDefinition.get = function proxyGetter () {
    return this[sourceKey][key]
    };
    sharedPropertyDefinition.set = function proxySetter (val) {
    this[sourceKey][key] = val;
    };
    Object.defineProperty(target, key, sharedPropertyDefinition);
    }
    function initData(vm){
    const data = vm._data = vm.$options.data;
    const keys = Object.keys(data);
    var i = keys.length;
    while (i--) {
    var key = keys[i];
    proxy(vm, '_data', key);
    }
    }
    function initMethods(vm, methods){
    for (var key in methods) {
    vm[key] = typeof methods[key] !== 'function' ? noop : methods[key].bind(vm);
    }
    }

    function Person(options){
    let vm = this;
    vm.$options = options;
    var opts = vm.$options;
    if(opts.data){
    initData(vm);
    }
    if(opts.methods){
    initMethods(vm, opts.methods)
    }
    }

    const p = new Person({
    data: {
    name: '若川'
    },
    methods: {
    sayName(){
    console.log(this.name);
    }
    }
    });

    console.log(p.name);
    // 未实现前: undefined
    // '若川'
    console.log(p.sayName());
    // 未实现前:Uncaught TypeError: p.sayName is not a function
    // '若川'

    5. 总结


    本文涉及到的基础知识主要有如下:


    构造函数
    this 指向
    call、bind、apply
    Object.defineProperty
    等等基础知识。

    本文源于解答源码共读群友的疑惑,通过详细的描述了如何调试 Vue 源码,来探寻答案。


    解答文章开头提问:


    通过this直接访问到methods里面的函数的原因是:因为methods里的方法通过 bind 指定了this为 new Vue的实例(vm)。


    通过 this 直接访问到 data 里面的数据的原因是:data里的属性最终会存储到new Vue的实例(vm)上的 _data对象中,访问 this.xxx,是访问Object.defineProperty代理后的 this._data.xxx


    Vue的这种设计,好处在于便于获取。也有不方便的地方,就是propsmethodsdata三者容易产生冲突。


    文章整体难度不大,但非常建议读者朋友们自己动手调试下。调试后,你可能会发现:原来 Vue 源码,也没有想象中的那么难,也能看懂一部分。


    启发:我们工作使用常用的技术和框架或库时,保持好奇心,多思考内部原理。能够做到知其然,知其所以然。就能远超很多人。


    你可能会思考,为什么模板语法中,可以省略this关键词写法呢,内部模板编译时其实是用了with。有余力的读者可以探究这一原理。


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

    收起阅读 »

    webpack-dev-server 从入门到实战

    古有云:“工欲善其事,必先利其器”。作为一个前端开发,搭建一个便捷的开发环境,将会为我们的开发工作带来极大的效率提升。而Webpack作为如今前端工程打包必不可少的工具,很多人却不知道从Webpack 4开始提供的DevServer功能。 让我们一起来学习下吧...
    继续阅读 »

    古有云:“工欲善其事,必先利其器”。作为一个前端开发,搭建一个便捷的开发环境,将会为我们的开发工作带来极大的效率提升。而Webpack作为如今前端工程打包必不可少的工具,很多人却不知道从Webpack 4开始提供的DevServer功能。


    让我们一起来学习下吧!


    1 什么是webpack-dev-server


    DevServerWebpack 3开放的一个实验功能,使用webpack-dev-middleware中间件,提供热更新的开发服务器,旨在帮助开发者在开发阶段快速进行环境搭建。


    最新Webpack 5还支持反向代理、防火墙、Socketgzip压缩等功能。


    2 反向代理配置


    Nginx类似,webpack-dev-server也是通过url正则匹配的方式进行url代理配置,常用配置参考如下代码:


    {
    "/rest/": {
    "target": "http://127.0.0.1:8080",
    "secure": false
    }
    }

    还可以通过用JavaScript定义此配置,把多个条目代理到同一个目标。将代理配置文件设置为proxy.conf.js(代替proxy.conf.json),并指定如下例子中的配置文件。


    module.exports = {
        //...
        devServer: {
            proxy: [
                {
                    context: ['/auth', '/api'],
                    target: 'http://localhost:3000',
                },
            ],
        },
    };

    2.1 基本配置项介绍



    • proxydevServer代理配置

    • /api: 表示需要代理的请求url

    • target:反向代理的地址

    • pathRewrite:请求地址重写,类似NginxRewite功能


    其他写法参考:


    "pathRewrite": {
      "^/old/api": "/new/api"
    }

     // remove path
    pathRewrite: {
    '^/remove/api': ''
    }

    // add base path
    pathRewrite: {
    '^/': '/basepath/'
    }

    // custom rewriting
    pathRewrite: function (path, req) {
    return path.replace('/api', '/base/api');
    }

    // custom rewriting, returning Promise
    pathRewrite: async function (path, req) {
    const should_add_something = await httpRequestToDecideSomething(path);
    if (should_add_something) path += 'something';
    return path;
    }

    2.2 其他配置参考



    • logLevel:日志打印等级,支持['debug', 'info', 'warn', 'error', 'silent']silent不打印日志

    • logProvider: 自定义日志打印中间件

    • secure:是否关闭https安全认证

    • changeOrigin:修改代理请求host

    • protocolRewrite:协议重写,httphttps请求互转

    • cookieDomainRewrite:修改cookieDomain的值

    • headers:给所有请求添加headers配置

    • proxyTimeout:请求超时时间


    2.3 高级代理机制



    • onError:  对请求状态码进行处理


    function onError(err, req, res, target) {
        res.writeHead(500, {
            'Content-Type': 'text/plain',
        });
        res.end('Something went wrong. And we are reporting a custom error message.');
    }


    • onProxyRes: 对代理接口的Response处理,这里常用来获取cookie、重定向等


    function onProxyRes(proxyRes, req, res) {
        proxyRes.headers['x-added'] = 'foobar'; // 添加一个header
        delete proxyRes.headers['x-removed']; // 删除一个header
    }


    • onProxyReq:对代理接口request处理,执行在请求前,常用来设置cookieheader等操作


    function onProxyReq(proxyReq, req, res) {
        // add custom header to request
        proxyReq.setHeader('x-added', 'foobar');
        // or log the req
    }

    3 域名白名单配置


    配置该配置后,只有匹配的host地址才可以访问该服务,常用于开发阶段模拟网络网络防火墙对访问IP进行限制。当该配置项被配置为all时,会跳过host检查,但不建议这样做,因为有DNS攻击的风险。



    1. webpack配置项配置


    module.exports = {
      //...
      devServer: {
        allowedHosts: [
          'host.com',
          'subdomain.host.com',
          'subdomain2.host.com',
          'host2.com',
        ],
      },
    };


    1. cli 启动命令配置


    npx webpack serve --allowed-hosts .host.com --allowed-hosts host2.com

    4 端口配置



    1. webpack配置项配置


    module.exports = {
      //...
      devServer: {
        port: 8080,
      },
    };


    1. cli 启动命令配置


       npx webpack serve --port 8080

    5 Angular 实战 —— 通过webpack devServer代理REST接口到本地服务器


    在Angular框架中,由于对webpack进行了封装,proxy配置文件默认使用的是proxy.config.json。(js格式配置文件需要到angular.json配置文件中修改),这里以proxy.config.json为例。



    1. 代理所有以/rest/开头的接口到127.0.0.1:8080,并且将/rest/请求地址转为/


    {
      "/rest/": {
        "target": "http://127.0.0.1:8080",
        "secure": false,
        "pathRewrite": {
          "/rest/": "/"
        },
        "changeOrigin": true,
        "logLevel": "debug",
        "proxyTimeout": 3000
      }
    }

    访问启动地址测试{{ host地址}}/rest/testApi



    1. 给所有的/rest/接口加上cftk的header


    这个需要使用js格式的proxy配置文件,修改angular.json中的proxyConfig为 proxy.config.js,在proxy.config.js中添加如下内容:


    const PROXY_CONFIG = [
        {
            "target": "http://127.0.0.1:8080",
            "secure": false,
            "pathRewrite": {
                "/rest/": "/"
            },
            "changeOrigin": true,
            "logLevel": "debug",
            "proxyTimeout": 3000,
            "onProxyReq": (request, req, res) => {
                request.setHeader('cftk', 'my cftk');
            }
        },
    ];
    module.exports = PROXY_CONFIG;

    6 webpack-dev-server 与 nginx 的对比



    作者:DevUI团队
    链接:https://juejin.cn/post/7010571347705200671

    收起阅读 »

    Android性能优化—StrictMode的使用

    概述StrictMode是Android开发过程中一个必不可缺的性能检测工具,他能帮助开发检测出一些不合理的代码块。策略分类StrictMode分为线程策略(ThreadPolicy)和虚拟机策略(VmPolicy)线程策略(ThreadPolicy)线程策略...
    继续阅读 »

    概述

    StrictMode是Android开发过程中一个必不可缺的性能检测工具,他能帮助开发检测出一些不合理的代码块。

    策略分类

    StrictMode分为线程策略(ThreadPolicy)和虚拟机策略(VmPolicy)

    线程策略(ThreadPolicy)

    线程策略主要包含了以下几个方面

    • detectNetwork:监测主线程使用网络(重要)
    • detectCustomSlowCalls:监测自定义运行缓慢函数
    • penaltyLog:输出日志
    • penaltyDialog:监测情况时弹出对话框
    • detectDiskReads:检测在UI线程读磁盘操作 (重要)
    • detectDiskWrites:检测在UI线程写磁盘操作(重要)
    • detectResourceMismatches:检测发现资源不匹配 (api>22)
    • detectAll:检测所有支持检测等项目(如果太懒,不想一一列出来,可以通过这个方式)
    • permitDiskReads:允许UI线程在磁盘上读操作

    虚拟机策略(VmPolicy)

    虚拟机策略主要包含了以下几个方面

    • detectActivityLeaks:检测Activity 的内存泄露情况(重要)(api>10)
    • detectCleartextNetwork:检测明文的网络 (api>22)
    • detectFileUriExposure:检测file://或者是content:// (api>17)
    • detectLeakedClosableObjects:检测资源没有正确关闭(重要)(api>10)
    • detectLeakedRegistrationObjects:检测BroadcastReceiver、ServiceConnection是否被释放 (重要)(api>15)
    • detectLeakedSqlLiteObjects:检测数据库资源是否没有正确关闭(重要)(api>8)
    • setClassInstanceLimit:设置某个类的同时处于内存中的实例上限,可以协助检查内存泄露(重要)
    • penaltyLog:输出日志
    • penaltyDeath:一旦检测到应用就会崩溃

    代码

        private void enabledStrictMode() {
    //开启Thread策略模式
    StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder().detectNetwork()//监测主线程使用网络io
    .detectCustomSlowCalls()//监测自定义运行缓慢函数
    .detectDiskReads() // 检测在UI线程读磁盘操作
    .detectDiskWrites() // 检测在UI线程写磁盘操作
    .penaltyLog() //写入日志
    .penaltyDialog()//监测到上述状况时弹出对话框
    .build());
    //开启VM策略模式
    StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder().detectLeakedSqlLiteObjects()//监测sqlite泄露
    .detectLeakedClosableObjects()//监测没有关闭IO对象
    .setClassInstanceLimit(MainActivity.class, 1) // 设置某个类的同时处于内存中的实例上限,可以协助检查内存泄露
    .detectActivityLeaks()
    .penaltyLog()//写入日志
    .penaltyDeath()//出现上述情况异常终止
    .build());
    }

    案例1

    public class MainActivity extends Activity {

    private Handler mHandler = new Handler();

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    if (BuildConfig.DEBUG) {
    enabledStrictMode();
    }
    mHandler.postDelayed(new Runnable() {
    @Override
    public void run() {
    Log.d("MainActivity", "我来了");
    }
    }, 10 * 1000);
    TextView tv = new TextView(this);
    tv.setText("不错啊");
    }

    private void enabledStrictMode() {
    //开启Thread策略模式
    StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder().detectNetwork()//监测主线程使用网络io
    .detectCustomSlowCalls()//监测自定义运行缓慢函数
    .detectDiskReads() // 检测在UI线程读磁盘操作
    .detectDiskWrites() // 检测在UI线程写磁盘操作
    .penaltyLog() //写入日志
    .penaltyDialog()//监测到上述状况时弹出对话框
    .build());
    //开启VM策略模式
    StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder().detectLeakedSqlLiteObjects()//监测sqlite泄露
    .detectLeakedClosableObjects()//监测没有关闭IO对象
    .setClassInstanceLimit(MainActivity.class, 1) // 设置某个类的同时处于内存中的实例上限,可以协助检查内存泄露
    .detectActivityLeaks()
    .penaltyLog()//写入日志
    .penaltyDeath()//出现上述情况异常终止
    .build());
    }
    }

    如代码所示,我在MainActivity(启动模式为singleTask且为app的启动Activity)中创建一个Handler(非静态),然后执行一个delay了10s的任务。
    现在我不断的启动和退出MainActivity,结果发现如下图所示

    可以看出MainActivity创建了多份实例(此图使用了MAT中的OQL,以后的章节会详细的讲解),我们的预期是只能有一个这样的MainActivity实例。将其中某个对象实例引用路径列出来,见下图。

    通过上图我们可以发现,是Handler持有了此MainActivity实例,导致这个MainActivity无法被释放。

    改造

    public class MainActivity extends Activity {

    private static class InnerHandler extends Handler {
    private final WeakReference<MainActivity> mWeakreference;

    InnerHandler(MainActivity activity) {
    mWeakreference = new WeakReference<>(activity);
    }

    @Override
    public void handleMessage(Message msg) {
    super.handleMessage(msg);
    final MainActivity activity = mWeakreference.get();
    if (activity == null) {
    return;
    }
    Log.d("MainActivity","执行msg");
    }
    }

    private Handler mHandler = new InnerHandler(this);

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    if (BuildConfig.DEBUG) {
    enabledStrictMode();
    }
    mHandler.postDelayed(new Runnable() {
    @Override
    public void run() {
    Log.d("MainActivity", "我来了");
    }
    }, 10 * 1000);
    TextView tv = new TextView(this);
    tv.setText("我来了");
    setContentView(tv);
    }

    @Override
    protected void onDestroy() {
    mHandler.removeCallbacksAndMessages(null);
    super.onDestroy();
    }

    private void enabledStrictMode() {
    //开启Thread策略模式
    StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder().detectNetwork()//监测主线程使用网络io
    .detectCustomSlowCalls()//监测自定义运行缓慢函数
    .detectDiskReads() // 检测在UI线程读磁盘操作
    .detectDiskWrites() // 检测在UI线程写磁盘操作
    .penaltyLog() //写入日志
    .penaltyDialog()//监测到上述状况时弹出对话框
    .build());
    //开启VM策略模式
    StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder().detectLeakedSqlLiteObjects()//监测sqlite泄露
    .detectLeakedClosableObjects()//监测没有关闭IO对象
    .setClassInstanceLimit(MainActivity.class, 1) // 设置某个类的同时处于内存中的实例上限,可以协助检查内存泄露
    .detectActivityLeaks()
    .penaltyLog()//写入日志
    .build());
    }
    }

    将Handler实现为静态内部类,且通过弱引用的方式将当前Activity持有,在onDestory出调用removeCallbacksAndMessages(null)方法,此处填null,表示将Handler中所有的消息都清空掉。
    运行代码后,通过MAT分析见下图

    由图可见,当前有且仅有一个MainActivity,达到代码设计预期。

    备注

    这个案例在我们分析过程中,会爆出android instances=2; limit=1字样的StrictMode信息,原因是由于我们在启动退出MainActivity的过程中,系统正在回收MainActivity的实例(回收是需要时间的),即此对象正在被FinalizerReference引用,而我们正在启动另外一项MainActivity,故报两个实例。

    案例2

    public class MainActivity extends Activity {

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    if (BuildConfig.DEBUG) {
    enabledStrictMode();
    }
    TextView tv = new TextView(this);
    tv.setText("我来了");
    setContentView(tv);
    newThread();
    takeTime();
    }

    private void newThread() {
    for (int i = 0; i < 50; i++) {
    new Thread(new Runnable() {
    @Override
    public void run() {
    takeTime();
    }
    }).start();
    }
    }

    private void takeTime() {
    try {
    File file = new File(getCacheDir(), "test");
    if (file.exists()) {
    file.delete();
    }
    file.createNewFile();
    FileOutputStream fileOutputStream = new FileOutputStream(file);
    final String content = "hello 我来了";
    StringBuffer buffer = new StringBuffer();
    for (int i = 0; i < 100; i++) {
    buffer.append(content);
    }
    fileOutputStream.write(buffer.toString().getBytes());
    fileOutputStream.flush();
    } catch (Exception e) {
    e.printStackTrace();
    }
    }

    @Override
    protected void onDestroy() {
    super.onDestroy();
    }

    private void enabledStrictMode() {
    //开启Thread策略模式
    StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder().detectNetwork()//监测主线程使用网络io
    .detectCustomSlowCalls()//监测自定义运行缓慢函数
    .detectDiskReads() // 检测在UI线程读磁盘操作
    .detectDiskWrites() // 检测在UI线程写磁盘操作
    .penaltyLog() //写入日志
    .penaltyDialog()//监测到上述状况时弹出对话框
    .build());
    //开启VM策略模式
    StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder().detectLeakedSqlLiteObjects()//监测sqlite泄露
    .detectLeakedClosableObjects()//监测没有关闭IO对象
    .setClassInstanceLimit(MainActivity.class, 1) // 设置某个类的同时处于内存中的实例上限,可以协助检查内存泄露
    .detectActivityLeaks()
    .penaltyLog()//写入日志
    .build());
    }
    }

    运行以上代码,弹出警告对话框

    点击确定后,查看StrictMode日志见附图

    从日志信息我们可以得到,程序在createNewFile、openFile、writeFile都花了75ms时间,这对于程序来说是一个较为耗时的操作。接着继续我们的日志

    从字面上意思我们知道,是文件流没有关闭,通过日志我们能很快的定位问题点:

          fileOutputStream.write(buffer.toString().getBytes());
    fileOutputStream.flush();

    文件流flush后,没有执行close方法,这样会导致这个文件资源一直被此对象持有,资源得不到释放,造成内存及资源浪费。

    总结

    StrictMode除了上面的案例情况,还可以检测对IO、网络、数据库等相关操作,而这些操作恰恰是Android开发过程中影响App性能最常见因素(都比较耗时、CPU占用时间、占用大量内存),所以在开发过程中时刻关注StrictMode变化是一个很好的习惯——一方面可以检测项目组员代码质量,另一方面也可以让自己在Android开发过程中形成一些良好的写代码的思维方式。在StrictMode检测过程中,我们要时刻关注日志的变换(如方法执行时间长短),尤其要对那些红色的日志引起注意,因为这些方法引发的问题是巨大的。

    收起阅读 »

    写个图片加载框架

    假如让你自己写个图片加载框架,你会考虑哪些问题?首先,梳理一下必要的图片加载框架的需求:异步加载:线程池切换线程:Handler,没有争议吧缓存:LruCache、DiskLruCache防止OOM:软引用、LruCache、图片压缩、Bitmap像素存储位置...
    继续阅读 »

      假如让你自己写个图片加载框架,你会考虑哪些问题?

      首先,梳理一下必要的图片加载框架的需求:

      • 异步加载:线程池
      • 切换线程:Handler,没有争议吧
      • 缓存:LruCache、DiskLruCache
      • 防止OOM:软引用、LruCache、图片压缩、Bitmap像素存储位置
      • 内存泄露:注意ImageView的正确引用,生命周期管理
      • 列表滑动加载的问题:加载错乱、队满任务过多问题

      当然,还有一些不是必要的需求,例如加载动画等。

      2.1 异步加载:

      线程池,多少个?

      缓存一般有三级,内存缓存、硬盘、网络。

      由于网络会阻塞,所以读内存和硬盘可以放在一个线程池,网络需要另外一个线程池,网络也可以采用Okhttp内置的线程池。

      读硬盘和读网络需要放在不同的线程池中处理,所以用两个线程池比较合适。

      Glide 必然也需要多个线程池,看下源码是不是这样

      public final class GlideBuilder {
      ...
      private GlideExecutor sourceExecutor; //加载源文件的线程池,包括网络加载
      private GlideExecutor diskCacheExecutor; //加载硬盘缓存的线程池
      ...
      private GlideExecutor animationExecutor; //动画线程池

      Glide使用了三个线程池,不考虑动画的话就是两个。

      2.2 切换线程:

      图片异步加载成功,需要在主线程去更新ImageView,

      无论是RxJava、EventBus,还是Glide,只要是想从子线程切换到Android主线程,都离不开Handler。

      看下Glide 相关源码:

          class EngineJob<R> implements DecodeJob.Callback<R>,Poolable {
      private static final EngineResourceFactory DEFAULT_FACTORY = new EngineResourceFactory();
      //创建Handler
      private static final Handler MAIN_THREAD_HANDLER =
      new Handler(Looper.getMainLooper(), new MainThreadCallback());

      问RxJava是完全用Java语言写的,那怎么实现从子线程切换到Android主线程的? 依然有很多3-6年的开发答不上来这个很基础的问题,而且只要是这个问题回答不出来的,接下来有关于原理的问题,基本都答不上来。

      有不少工作了很多年的Android开发不知道鸿洋、郭霖、玉刚说,不知道掘金是个啥玩意,内心估计会想是不是还有叫掘银掘铁的(我不知道有没有)。

      我想表达的是,干这一行,真的是需要有对技术的热情,不断学习,不怕别人比你优秀,就怕比你优秀的人比你还努力,而你却不知道

      2.3 缓存

      我们常说的图片三级缓存:内存缓存、硬盘缓存、网络。

      2.3.1 内存缓存

      一般都是用LruCache

      Glide 默认内存缓存用的也是LruCache,只不过并没有用Android SDK中的LruCache,不过内部同样是基于LinkHashMap,所以原理是一样的。

      // -> GlideBuilder#build
      if (memoryCache == null) {
      memoryCache = new LruResourceCache(memorySizeCalculator.getMemoryCacheSize());
      }

      既然说到LruCache ,必须要了解一下LruCache的特点和源码:

      为什么用LruCache?

      LruCache 采用最近最少使用算法,设定一个缓存大小,当缓存达到这个大小之后,会将最老的数据移除,避免图片占用内存过大导致OOM。

      LruCache 源码分析
          public class LruCache<K, V> {
      // 数据最终存在 LinkedHashMap 中
      private final LinkedHashMap<K, V> map;
      ...
      public LruCache(int maxSize) {
      if (maxSize <= 0) {
      throw new IllegalArgumentException("maxSize <= 0");
      }
      this.maxSize = maxSize;
      // 创建一个LinkedHashMap,accessOrder 传true
      this.map = new LinkedHashMap<K, V>(0, 0.75f, true);
      }
      ...

      LruCache 构造方法里创建一个LinkedHashMap,accessOrder 参数传true,表示按照访问顺序排序,数据存储基于LinkedHashMap。

      先看看LinkedHashMap 的原理吧

      LinkedHashMap 继承 HashMap,在 HashMap 的基础上进行扩展,put 方法并没有重写,说明LinkedHashMap遵循HashMap的数组加链表的结构

      LinkedHashMap重写了 createEntry 方法。

      看下HashMap 的 createEntry 方法

      void createEntry(int hash, K key, V value, int bucketIndex) {
      HashMapEntry<K,V> e = table[bucketIndex];
      table[bucketIndex] = new HashMapEntry<>(hash, key, value, e);
      size++;
      }

      HashMap的数组里面放的是HashMapEntry 对象

      看下LinkedHashMap 的 createEntry方法

      void createEntry(int hash, K key, V value, int bucketIndex) {
      HashMapEntry<K,V> old = table[bucketIndex];
      LinkedHashMapEntry<K,V> e = new LinkedHashMapEntry<>(hash, key, value, old);
      table[bucketIndex] = e; //数组的添加
      e.addBefore(header); //处理链表
      size++;
      }

      LinkedHashMap的数组里面放的是LinkedHashMapEntry对象

      LinkedHashMapEntry

      private static class LinkedHashMapEntry<K,V> extends HashMapEntry<K,V> {
      // These fields comprise the doubly linked list used for iteration.
      LinkedHashMapEntry<K,V> before, after; //双向链表

      private void remove() {
      before.after = after;
      after.before = before;
      }

      private void addBefore(LinkedHashMapEntry<K,V> existingEntry) {
      after = existingEntry;
      before = existingEntry.before;
      before.after = this;
      after.before = this;
      }

      LinkedHashMapEntry继承 HashMapEntry,添加before和after变量,所以是一个双向链表结构,还添加了addBeforeremove 方法,用于新增和删除链表节点。

      LinkedHashMapEntry#addBefore
      将一个数据添加到Header的前面

      private void addBefore(LinkedHashMapEntry<K,V> existingEntry) {
      after = existingEntry;
      before = existingEntry.before;
      before.after = this;
      after.before = this;
      }

      existingEntry 传的都是链表头header,将一个节点添加到header节点前面,只需要移动链表指针即可,添加新数据都是放在链表头header 的before位置,链表头节点header的before是最新访问的数据,header的after则是最旧的数据。

      再看下LinkedHashMapEntry#remov

      在Bitmap构造方法创建了一个 BitmapFinalizer类,重写finalize 方法,在java层Bitmap被回收的时候,BitmapFinalizer 对象也会被回收,finalize 方法肯定会被调用,在里面释放native层Bitmap对象。

      6.0 之后做了一些变化,BitmapFinalizer 没有了,被NativeAllocationRegistry取代。

      例如 8.0 Bitmap构造方法

          Bitmap(long nativeBitmap, int width, int height, int density,
      boolean isMutable, boolean requestPremultiplied,
      byte[] ninePatchChunk, NinePatch.InsetStruct ninePatchInsets) {

      ...
      mNativePtr = nativeBitmap;
      long nativeSize = NATIVE_ALLOCATION_SIZE + getAllocationByteCount();
      // 创建NativeAllocationRegistry这个类,调用registerNativeAllocation 方法
      NativeAllocationRegistry registry = new NativeAllocationRegistry(
      Bitmap.class.getClassLoader(), nativeGetNativeFinalizer(), nativeSize);
      registry.registerNativeAllocation(this, nativeBitmap);
      }

      NativeAllocationRegistry 就不分析了, 不管是BitmapFinalizer 还是NativeAllocationRegistry,目的都是在java层Bitmap被回收的时候,将native层Bitmap对象也回收掉。 一般情况下我们无需手动调用recycle方法,由GC去盘它即可。

      上面分析了Bitmap像素存储位置,我们知道,Android 8.0 之后Bitmap像素内存放在native堆,Bitmap导致OOM的问题基本不会在8.0以上设备出现了(没有内存泄漏的情况下),那8.0 以下设备怎么办?赶紧升级或换手机吧~

      我们换手机当然没问题,但是并不是所有人都能跟上Android系统更新的步伐,所以,问题还是要解决~

      Fresco 之所以能跟Glide 正面交锋,必然有其独特之处,文中开头列出 Fresco 的优点是:“在5.0以下(最低2.3)系统,Fresco将图片放到一个特别的内存区域(Ashmem区)” 这个Ashmem区是一块匿名共享内存,Fresco 将Bitmap像素放到共享内存去了,共享内存是属于native堆内存。

      Fresco 关键源码在 PlatformDecoderFactory 这个类

      8.0 先不看了,看一下 4.4 以下是怎么得到Bitmap的,看下GingerbreadPurgeableDecoder这个类有个获取Bitmap的方法

      //GingerbreadPurgeableDecoder
      private Bitmap decodeFileDescriptorAsPurgeable(
      CloseableReference<PooledByteBuffer> bytesRef,
      int inputLength,
      byte[] suffix,
      BitmapFactory.Options options) {
      // MemoryFile :匿名共享内存
      MemoryFile memoryFile = null;
      try {
      //将图片数据拷贝到匿名共享内存
      memoryFile = copyToMemoryFile(bytesRef, inputLength, suffix);
      FileDescriptor fd = getMemoryFileDescriptor(memoryFile);
      if (mWebpBitmapFactory != null) {
      // 创建Bitmap,Fresco自己写了一套创建Bitmap方法
      Bitmap bitmap = mWebpBitmapFactory.decodeFileDescriptor(fd, null, options);
      return Preconditions.checkNotNull(bitmap, "BitmapFactory returned null");
      } else {
      throw new IllegalStateException("WebpBitmapFactory is null");
      }
      }
      }

      捋一捋,4.4以下,Fresco 使用匿名共享内存来保存Bitmap数据,首先将图片数据拷贝到匿名共享内存中,然后使用Fresco自己写的加载Bitmap的方法。

      Fresco对不同Android版本使用不同的方式去加载Bitmap,至于4.4-5.0,5.0-8.0,8.0 以上,对应另外三个解码器,大家可以从PlatformDecoderFactory 这个类入手,自己去分析,思考为什么不同平台要分这么多个解码器,8.0 以下都用匿名共享内存不好吗?期待你在评论区跟大家分享~

      2.5 ImageView 内存泄露

      曾经在Vivo驻场开发,带有头像功能的页面被测出内存泄漏,原因是SDK中有个加载网络头像的方法,持有ImageView引用导致的。

      当然,修改也比较简单粗暴,将ImageView用WeakReference修饰就完事了。

      事实上,这种方式虽然解决了内存泄露问题,但是并不完美,例如在界面退出的时候,我们除了希望ImageView被回收,同时希望加载图片的任务可以取消,队未执行的任务可以移除。

      Glide的做法是监听生命周期回调,看 RequestManager 这个类

      public void onDestroy() {
      targetTracker.onDestroy();
      for (Target<?> target : targetTracker.getAll()) {
      //清理任务
      clear(target);
      }
      targetTracker.clear();
      requestTracker.clearRequests();
      lifecycle.removeListener(this);
      lifecycle.removeListener(connectivityMonitor);
      mainHandler.removeCallbacks(addSelfToLifecycle);
      glide.unregisterRequestManager(this);
      }

      在Activity/fragment 销毁的时候,取消图片加载任务,细节大家可以自己去看源码。

      2.6 列表加载问题

      图片错乱

      由于RecyclerView或者LIstView的复用机制,网络加载图片开始的时候ImageView是第一个item的,加载成功之后ImageView由于复用可能跑到第10个item去了,在第10个item显示第一个item的图片肯定是错的。

      常规的做法是给ImageView设置tag,tag一般是图片地址,更新ImageView之前判断tag是否跟url一致。

      当然,可以在item从列表消失的时候,取消对应的图片加载任务。要考虑放在图片加载框架做还是放在UI做比较合适。

      线程池任务过多

      列表滑动,会有很多图片请求,如果是第一次进入,没有缓存,那么队列会有很多任务在等待。所以在请求网络图片之前,需要判断队列中是否已经存在该任务,存在则不加到队列去。

      总结

      本文通过Glide开题,分析一个图片加载框架必要的需求,以及各个需求涉及到哪些技术和原理。

      • 异步加载:最少两个线程池
      • 切换到主线程:Handler
      • 缓存:LruCache、DiskLruCache,涉及到LinkHashMap原理
      • 防止OOM:软引用、LruCache、图片压缩没展开讲、Bitmap像素存储位置源码分析、Fresco部分源码分析
      • 内存泄露:注意ImageView的正确引用,生命周期管理

    收起阅读 »

    Android 高级UI 事件传递机制

    1.View的事件分发流程dispatchTouchEvent():onTouchListener--->onTouch方法onTouchEventonClickListener--->onClick方法ListenerInfo static...
    继续阅读 »

    1.View的事件分发

    流程
    1. dispatchTouchEvent():
    2. onTouchListener--->onTouch方法
    3. onTouchEvent
    4. onClickListener--->onClick方法

    ListenerInfo


        static class ListenerInfo {
    /**
    * Listener used to dispatch focus change events.
    * This field should be made private, so it is hidden from the SDK.
    * {@hide}
    */

    protected OnFocusChangeListener mOnFocusChangeListener;

    /**
    * Listeners for layout change events.
    */

    private ArrayList<OnLayoutChangeListener> mOnLayoutChangeListeners;

    protected OnScrollChangeListener mOnScrollChangeListener;

    /**
    * Listeners for attach events.
    */

    private CopyOnWriteArrayList<OnAttachStateChangeListener> mOnAttachStateChangeListeners;

    /**
    * Listener used to dispatch click events.
    * This field should be made private, so it is hidden from the SDK.
    * {@hide}
    */

    public OnClickListener mOnClickListener;

    /**
    * Listener used to dispatch long click events.
    * This field should be made private, so it is hidden from the SDK.
    * {@hide}
    */

    protected OnLongClickListener mOnLongClickListener;

    /**
    * Listener used to dispatch context click events. This field should be made private, so it
    * is hidden from the SDK.
    * {@hide}
    */

    protected OnContextClickListener mOnContextClickListener;

    /**
    * Listener used to build the context menu.
    * This field should be made private, so it is hidden from the SDK.
    * {@hide}
    */

    protected OnCreateContextMenuListener mOnCreateContextMenuListener;

    private OnKeyListener mOnKeyListener;

    private OnTouchListener mOnTouchListener;

    private OnHoverListener mOnHoverListener;

    private OnGenericMotionListener mOnGenericMotionListener;

    private OnDragListener mOnDragListener;

    private OnSystemUiVisibilityChangeListener mOnSystemUiVisibilityChangeListener;

    OnApplyWindowInsetsListener mOnApplyWindowInsetsListener;

    OnCapturedPointerListener mOnCapturedPointerListener;
    }

    dispatchTouchEvent


        public boolean dispatchTouchEvent(MotionEvent event) {
    // If the event should be handled by accessibility focus first.
    if (event.isTargetAccessibilityFocus()) {
    // We don't have focus or no virtual descendant has it, do not handle the event.
    if (!isAccessibilityFocusedViewOrHost()) {
    return false;
    }
    // We have focus and got the event, then use normal event dispatch.
    event.setTargetAccessibilityFocus(false);
    }

    boolean result = false;

    if (mInputEventConsistencyVerifier != null) {
    mInputEventConsistencyVerifier.onTouchEvent(event, 0);
    }

    final int actionMasked = event.getActionMasked();
    if (actionMasked == MotionEvent.ACTION_DOWN) {
    // Defensive cleanup for new gesture
    stopNestedScroll();
    }

    if (onFilterTouchEventForSecurity(event)) {
    if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
    result = true;
    }
    //noinspection SimplifiableIfStatement
    ListenerInfo li = mListenerInfo;
    if (li != null && li.mOnTouchListener != null
    && (mViewFlags & ENABLED_MASK) == ENABLED
    && li.mOnTouchListener.onTouch(this, event)) {
    result = true;
    }

    if (!result && onTouchEvent(event)) {
    result = true;
    }
    }

    if (!result && mInputEventConsistencyVerifier != null) {
    mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
    }

    // Clean up after nested scrolls if this is the end of a gesture;
    // also cancel it if we tried an ACTION_DOWN but we didn't want the rest
    // of the gesture.
    if (actionMasked == MotionEvent.ACTION_UP ||
    actionMasked == MotionEvent.ACTION_CANCEL ||
    (actionMasked == MotionEvent.ACTION_DOWN && !result)) {
    stopNestedScroll();
    }

    return result;
    }

    onTouchEvent

    结论:
    1. 控件的Listener事件触发的顺序是onTouch,再onClick
    2. 控件的onTouch返回true,将会使onClick的事件没有了---阻止了事件的传递。返回false,才会传递onClick事件 。
    3. 如果onTouchListener的onTouch方法返回了true,那么view里面的onTouchEvent就不会被调用了。顺序dispatchTouchEvent-->onTouchListener---return false-->onTouchEvent
    4. 如果view为disenable,则:onTouchListener里面不会执行,但是会执行onTouchEvent(event)方法
    5. onTouchEvent方法中的ACTION_UP分支中触发onclick事件监听
      onTouchListener-->onTouch方法返回true,消耗次事件。down,但是up事件是无法到达onClickListener.
      onTouchListener-->onTouch方法返回false,不会消耗此事件

    2.ViewGroup+View的事件分发

    ViewGroup继承View

    1. dispatchTouchEvent()
    2. onInterceptTouchEvent() (拦截触摸,ViewGroup独有)
    3. onTouchEvent()
    dispatchTouchEvent

      @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
    if (mInputEventConsistencyVerifier != null) {
    mInputEventConsistencyVerifier.onTouchEvent(ev, 1);
    }
    if (!handled && mInputEventConsistencyVerifier != null) {
    mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1);
    }
    return handled;
    }
    onInterceptTouchEvent

      public boolean onInterceptTouchEvent(MotionEvent ev) {
    if (ev.isFromSource(InputDevice.SOURCE_MOUSE)
    && ev.getAction() == MotionEvent.ACTION_DOWN
    && ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY)
    && isOnScrollbarThumb(ev.getX(), ev.getY())) {
    return true;
    }
    return false;
    }
    示例

    import android.content.Context;
    import android.util.AttributeSet;
    import android.util.Log;
    import android.view.MotionEvent;
    import android.widget.RelativeLayout;

    /**
    * Created by Xionghu on 2018/6/6.
    * Desc:
    */


    public class MyRelativeLayout extends RelativeLayout {
    public MyRelativeLayout(Context context) {
    super(context);
    }

    public MyRelativeLayout(Context context, AttributeSet attrs) {
    super(context, attrs);
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
    Log.i("kpioneer", "dispatchTouchEvent:action--"+ev.getAction()+"---view:MyRelativeLayout");
    return super.dispatchTouchEvent(ev);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
    Log.i("kpioneer", "onInterceptTouchEvent:action--"+ev.getAction()+"---view:MyRelativeLayout");
    return super.onInterceptTouchEvent(ev);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
    Log.i("kpioneer", "onTouchEvent:action--"+event.getAction()+"---view:MyRelativeLayout");
    return super.onTouchEvent(event);
    }
    }

    点击Button


    06-06 11:05:18.340 27438-27438/com.haocai.eventdemo I/kpioneer: dispatchTouchEvent:action--0---view:MyRelativeLayout
    06-06 11:05:18.340 27438-27438/com.haocai.eventdemo I/kpioneer: onInterceptTouchEvent:action--0---view:MyRelativeLayout
    06-06 11:05:18.340 27438-27438/com.haocai.eventdemo I/kpioneer: dispatchTouchEvent:action--0
    06-06 11:05:18.340 27438-27438/com.haocai.eventdemo I/kpioneer: OnTouchListener:acton--0----view:com.haocai.eventdemo.MyButton{8e32527 VFED..C.. ........ 0,42-264,186 #7f070022 app:id/button1}
    06-06 11:05:18.340 27438-27438/com.haocai.eventdemo I/kpioneer: onTouchEvent:action--0
    06-06 11:05:18.370 27438-27438/com.haocai.eventdemo I/kpioneer: dispatchTouchEvent:action--2---view:MyRelativeLayout
    06-06 11:05:18.370 27438-27438/com.haocai.eventdemo I/kpioneer: onInterceptTouchEvent:action--2---view:MyRelativeLayout
    06-06 11:05:18.370 27438-27438/com.haocai.eventdemo I/kpioneer: dispatchTouchEvent:action--2
    06-06 11:05:18.370 27438-27438/com.haocai.eventdemo I/kpioneer: OnTouchListener:acton--2----view:com.haocai.eventdemo.MyButton{8e32527 VFED..C.. ...P.... 0,42-264,186 #7f070022 app:id/button1}
    06-06 11:05:18.380 27438-27438/com.haocai.eventdemo I/kpioneer: onTouchEvent:action--2
    06-06 11:05:18.390 27438-27438/com.haocai.eventdemo I/kpioneer: dispatchTouchEvent:action--2---view:MyRelativeLayout
    06-06 11:05:18.390 27438-27438/com.haocai.eventdemo I/kpioneer: onInterceptTouchEvent:action--2---view:MyRelativeLayout
    06-06 11:05:18.390 27438-27438/com.haocai.eventdemo I/kpioneer: dispatchTouchEvent:action--2
    06-06 11:05:18.390 27438-27438/com.haocai.eventdemo I/kpioneer: OnTouchListener:acton--2----view:com.haocai.eventdemo.MyButton{8e32527 VFED..C.. ...P.... 0,42-264,186 #7f070022 app:id/button1}
    06-06 11:05:18.390 27438-27438/com.haocai.eventdemo I/kpioneer: onTouchEvent:action--2
    06-06 11:05:18.400 27438-27438/com.haocai.eventdemo I/kpioneer: dispatchTouchEvent:action--2---view:MyRelativeLayout
    06-06 11:05:18.400 27438-27438/com.haocai.eventdemo I/kpioneer: onInterceptTouchEvent:action--2---view:MyRelativeLayout
    06-06 11:05:18.400 27438-27438/com.haocai.eventdemo I/kpioneer: dispatchTouchEvent:action--2
    06-06 11:05:18.400 27438-27438/com.haocai.eventdemo I/kpioneer: OnTouchListener:acton--2----view:com.haocai.eventdemo.MyButton{8e32527 VFED..C.. ...P.... 0,42-264,186 #7f070022 app:id/button1}
    06-06 11:05:18.410 27438-27438/com.haocai.eventdemo I/kpioneer: onTouchEvent:action--2
    06-06 11:05:18.410 27438-27438/com.haocai.eventdemo I/kpioneer: dispatchTouchEvent:action--1---view:MyRelativeLayout
    06-06 11:05:18.410 27438-27438/com.haocai.eventdemo I/kpioneer: onInterceptTouchEvent:action--1---view:MyRelativeLayout
    06-06 11:05:18.410 27438-27438/com.haocai.eventdemo I/kpioneer: dispatchTouchEvent:action--1
    06-06 11:05:18.410 27438-27438/com.haocai.eventdemo I/kpioneer: OnTouchListener:acton--1----view:com.haocai.eventdemo.MyButton{8e32527 VFED..C.. ...P.... 0,42-264,186 #7f070022 app:id/button1}
    06-06 11:05:18.410 27438-27438/com.haocai.eventdemo I/kpioneer: onTouchEvent:action--1
    06-06 11:05:18.410 27438-27438/com.haocai.eventdemo I/kpioneer: OnClickListener----view:com.haocai.eventdemo.MyButton{8e32527 VFED..C.. ...P.... 0,42-264,186 #7f070022 app:id/button1}
    该例子中Button事件点击:
    1. 先接触到事件的是父容器
    2. ViewGroup顺序:dispatchTouchEvent--->onInterceptTouchevent-->dispatchTouchEvent(Button)-->OnTouchListener(Button) --->return false---> onTouchEvent(Button)(消耗事件) ----- onTouchevent(该示例父布局并没调用)
    收起阅读 »

    Android View post 方法

    解析View.post方法。分析一下这个方法的流程。 说起post方法,我们很容易联想到Handler的post方法,都是接收一个Runnable对象。那么这两个方法有啥不同呢? Handler的post方法 先来简单看一下Handler的post(Runna...
    继续阅读 »

    解析View.post方法。分析一下这个方法的流程。


    说起post方法,我们很容易联想到Handlerpost方法,都是接收一个Runnable对象。那么这两个方法有啥不同呢?


    Handler的post方法


    先来简单看一下Handlerpost(Runnable)方法。这个方法是将一个Runnable加到消息队列中,并且会在这个handler关联的线程里执行。


    下面是关联的部分源码。可以看到传入的Runnable对象,装入Message后,被添加进了queue队列中。


    Handler 有关的部分源码


        // android.os Handler 有关的部分源码
    public final boolean post(@NonNull Runnable r) {
    return sendMessageDelayed(getPostMessage(r), 0);
    }

    private static Message getPostMessage(Runnable r) {
    Message m = Message.obtain();
    m.callback = r;
    return m;
    }

    public final boolean sendMessageDelayed(@NonNull Message msg, long delayMillis) {
    if (delayMillis < 0) {
    delayMillis = 0;
    }
    return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
    }

    public boolean sendMessageAtTime(@NonNull Message msg, long uptimeMillis) {
    MessageQueue queue = mQueue;
    if (queue == null) {
    RuntimeException e = new RuntimeException(
    this + " sendMessageAtTime() called with no mQueue");
    Log.w("Looper", e.getMessage(), e);
    return false;
    }
    return enqueueMessage(queue, msg, uptimeMillis);
    }

    private boolean enqueueMessage(@NonNull MessageQueue queue, @NonNull Message msg,
    long uptimeMillis) {
    msg.target = this;
    msg.workSourceUid = ThreadLocalWorkSource.getUid();

    if (mAsynchronous) {
    msg.setAsynchronous(true);
    }
    return queue.enqueueMessage(msg, uptimeMillis);
    }

    具体流程,可以看handler介绍


    View的post方法


    我们直接跟着post的源码走。


    public boolean post(Runnable action) {
    final AttachInfo attachInfo = mAttachInfo;
    if (attachInfo != null) {
    return attachInfo.mHandler.post(action);
    }

    // Postpone the runnable until we know on which thread it needs to run.
    // Assume that the runnable will be successfully placed after attach.
    getRunQueue().post(action);
    return true;
    }

    private HandlerActionQueue getRunQueue() {
    if (mRunQueue == null) {
    mRunQueue = new HandlerActionQueue();
    }
    return mRunQueue;
    }

    可以看到一开始就查询是否有attachInfo,如果有,则用attachInfo.mHandler来执行这个任务。


    如果没有attachInfo,则添加到View自己的mRunQueue中。确定运行的线程后,再执行任务。


    post(Runnable action)的返回boolean值,如果为true,表示任务被添加到消息队列中了。
    如果是false,通常表示消息队列关联的looper正在退出。


    那么我们需要了解AttachInfoHandlerActionQueue


    AttachInfo


    AttachInfoView的静态内部类。View关联到父window后,用这个类来存储一些信息。


    AttachInfo存储的一部分信息如下:



    • WindowId mWindowId window的标志

    • View mRootView 最顶部的view

    • Handler mHandler 这个handler可以用来处理任务


    HandlerActionQueue


    View还没有handler的时候,拿HandlerActionQueue来缓存任务。HandlerAction是它的静态内部类,存储Runnable与延时信息。


    public class HandlerActionQueue {
    private HandlerAction[] mActions;

    public void post(Runnable action)
    public void executeActions(Handler handler)
    // ...

    private static class HandlerAction {
    final Runnable action;
    final long delay;
    // ...
    }
    }

    View的mRunQueue


    将任务(runnable)排成队。当View关联上窗口并且有handler后,再执行这些任务。


    /**
    * Queue of pending runnables. Used to postpone calls to post() until this
    * view is attached and has a handler.
    */
    private HandlerActionQueue mRunQueue;

    这个mRunQueue里存储的任务啥时候被执行?我们关注dispatchAttachedToWindow方法。


    void dispatchAttachedToWindow(AttachInfo info, int visibility) {
    // ...
    // Transfer all pending runnables.
    if (mRunQueue != null) {
    mRunQueue.executeActions(info.mHandler);
    mRunQueue = null;
    }
    // ...
    }

    这个方法里调用了mRunQueue.executeActions


    executeActions(Handler handler)方法实际上是用传入的handler处理队列中的任务。


    而这个dispatchAttachedToWindow会被ViewGroup中被调用。


    或者是ViewRootImpl中调用


    host.dispatchAttachedToWindow(mAttachInfo, 0);

    小结


    View的post方法,实际上是使用了AttachInfohandler


    如果View当前还没有AttachInfo,则把任务添加到了View自己的HandlerActionQueue队列中,然后在dispatchAttachedToWindow中把任务交给传入的AttachInfohandler。也可以这样认为,View.post用的就是handler.post


    我们在获取View的宽高时,会利用View的post方法,就是等View真的关联到window再拿宽高信息。


    流程图归纳如下


    post-flow1.png


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

    【开源项目】简单易用的Compose版StateLayout,了解一下~

    前言 在页面中常常需要展示网络请求状态,以带来更好的用户体验,具体来说通常有加载中,加载失败,加载为空,加载成功等状态. 在XML中我们通常用一个ViewGroup封装各种状态来实现,那么使用Compose该如何实现这种效果呢? 本文主要介绍Compose如何...
    继续阅读 »

    前言


    在页面中常常需要展示网络请求状态,以带来更好的用户体验,具体来说通常有加载中加载失败加载为空加载成功等状态.

    XML中我们通常用一个ViewGroup封装各种状态来实现,那么使用Compose该如何实现这种效果呢?

    本文主要介绍Compose如何封装一个简单易用的StateLayout,有兴趣的同学可以点个Star : Compose版StateLayout


    效果图


    首先看下最终的效果图


    特性



    1. 支持配置全局默认布局,如默认加载中,默认成功失败等

    2. 支持自定义默认样式文案,图片等细节

    3. 支持完全自定义样式,如自定义加载中样式

    4. 支持自定义处理点击重试事件

    5. 完全使用数据驱动,使用简单,接入方便


    使用


    接入


    第 1 步:在工程的build.gradle中添加:


    allprojects {
    repositories {
    ...
    mavenCentral()
    }
    }

    第2步:在应用的build.gradle中添加:


    dependencies {
    implementation 'io.github.shenzhen2017:compose-statelayout:1.0.0'
    }

    简单使用


    定义全局样式


    在框架中没有指定任何默认样式,因此你需要自定义自己的默认加载中,加载失败等页面样式

    同时需要自定义传给自定义样式的数据结构类型,方便数据驱动


    data class StateData(
    val tipTex: String? = null,
    val tipImg: Int? = null,
    val btnText: String? = null
    )

    @Composable
    fun DefaultStateLayout(
    modifier: Modifier = Modifier,
    pageStateData: PageStateData,
    onRetry: OnRetry = { },
    loading: @Composable (StateLayoutData) -> Unit = { DefaultLoadingLayout(it) },
    empty: @Composable (StateLayoutData) -> Unit = { DefaultEmptyLayout(it) },
    error: @Composable (StateLayoutData) -> Unit = { DefaultErrorLayout(it) },
    content: @Composable () -> Unit = { }
    ) {
    ComposeStateLayout(
    modifier = modifier,
    pageStateData = pageStateData,
    onRetry = onRetry,
    loading = { loading(it) },
    empty = { empty(it) },
    error = { error(it) },
    content = content
    )
    }

    如上所示,初始化时我们主要需要做以下事



    1. 自定义默认加载中,加载失败,加载为空等样式

    2. 自定义StateData,即传给默认样式的数据结构,比如文案,图片等,这样后续需要修改的时候只需修改StateData即可


    直接使用


    如果我们直接使用默认样式,直接如下使用即可


    @Composable
    fun StateDemo() {
    var pageStateData by remember {
    mutableStateOf(PageState.CONTENT.bindData())
    }
    DefaultStateLayout(
    modifier = Modifier.fillMaxSize(),
    pageStateData = pageStateData,
    onRetry = {
    pageStateData = PageState.LOADING.bindData()
    }
    ) {
    //Content
    }
    }

    如上所示,可以直接使用,如果需要修改状态,修改pageStateData即可


    自定义文案


    如果我们需要自定义文案或者图片等细节,可简单直接修改StateData即可


    fun StateDemo() {
    var pageStateData by remember {
    mutableStateOf(PageState.CONTENT.bindData())
    }
    //....
    pageStateData = PageState.LOADING.bindData(StateData(tipTex = "自定义加载中文案"))
    }

    自定义布局


    有时页面的加载中样式与全局的并不一样,这就需要自定义布局样式了


    @Composable
    fun StateDemo() {
    var pageStateData by remember {
    mutableStateOf(PageState.CONTENT.bindData())
    }
    DefaultStateLayout(
    modifier = Modifier.fillMaxSize(),
    pageStateData = pageStateData,
    loading = { CustomLoadingLayout(it) },
    onRetry = {
    pageStateData = PageState.LOADING.bindData()
    }
    ) {
    //Content
    }
    }

    主要原理


    其实Compose要实现不同的状态非常简单,传入不同的数据即可,如下所示:


        Box(modifier = modifier) {
    when (pageStateData.status) {
    PageState.LOADING -> loading()
    PageState.EMPTY -> empty()
    PageState.ERROR -> error()
    PageState.CONTENT -> content()
    }
    }

    其实代码非常简单,但是这段代码是个通用逻辑,如果每个页面都要写这一段代码可能也挺烦的

    所以这段代码其实是模板代码,我们想到Scaffold脚手架,提供了组合各个组件的API,包括标题栏、底部栏、SnackBar(类似吐司功能)、浮动按钮、抽屉组件、剩余内容布局等,让我们可以快速定义一个基本的页面结构。


    仿照Scaffold,我们也可以定义一个模板组件,用户可以传入自定义的looading,empty,error,content等组件,再将它们组合起来,这样就形成了ComposeStateLayout


    data class PageStateData(val status: PageState, val tag: Any? = null)

    data class StateLayoutData(val pageStateData: PageStateData, val retry: OnRetry = {})

    typealias OnRetry = (PageStateData) -> Unit

    @Composable
    fun ComposeStateLayout(
    modifier: Modifier = Modifier,
    pageStateData: PageStateData,
    onRetry: OnRetry = { },
    loading: @Composable (StateLayoutData) -> Unit = {},
    empty: @Composable (StateLayoutData) -> Unit = {},
    error: @Composable (StateLayoutData) -> Unit = {},
    content: @Composable () -> Unit = { }
    ) {
    val stateLayoutData = StateLayoutData(pageStateData, onRetry)
    Box(modifier = modifier) {
    when (pageStateData.status) {
    PageState.LOADING -> loading(stateLayoutData)
    PageState.EMPTY -> empty(stateLayoutData)
    PageState.ERROR -> error(stateLayoutData)
    PageState.CONTENT -> content()
    }
    }
    }

    如上所示,代码很简单,主要需要注意以下几点:



    1. PageStateDatatag即传递给自定义loading等页面的信息,为Any类型,没有任何限制,用户可灵活处理

    2. 自定义loading等页面也传入了OnRetry,因此我们也可以处理自定义点击事件


    总结


    本文主要实现了一个Compose版的StateLayout,它具有以下特性



    1. 支持配置全局默认布局,如默认加载中,默认成功失败等

    2. 支持自定义默认样式文案,图片等细节

    3. 支持完全自定义样式,如自定义加载中样式

    4. 支持自定义处理点击重试事件

    5. 完全使用数据驱动,使用简单,接入方便


    项目地址


    简单易用的Compose版StateLayout

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


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

    iOS RXSwift 4.3

    iOS
    MaybeMaybe 是 Observable 的另外一个版本。它介于 Single 和 Completable 之间,它要么只能发出一个元素,要么产生一个 completed&n...
    继续阅读 »

    Maybe

    Maybe 是 Observable 的另外一个版本。它介于 Single 和 Completable 之间,它要么只能发出一个元素,要么产生一个 completed 事件,要么产生一个 error 事件。

    • 发出一个元素或者一个 completed 事件或者一个 error 事件
    • 不会共享附加作用

    如果你遇到那种可能需要发出一个元素,又可能不需要发出时,就可以使用 Maybe

    如何创建 Maybe

    创建 Maybe 和创建 Observable 非常相似:

    func generateString() -> Maybe<String> {
    return Maybe<String>.create { maybe in
    maybe(.success("RxSwift"))

    // OR

    maybe(.completed)

    // OR

    maybe(.error(error))

    return Disposables.create {}
    }
    }

    之后,你可以这样使用 Maybe

    generateString()
    .subscribe(onSuccess: { element in
    print("Completed with element \(element)")
    }, onError: { error in
    print("Completed with an error \(error.localizedDescription)")
    }, onCompleted: {
    print("Completed with no element")
    })
    .disposed(by: disposeBag)

    你同样可以对 Observable 调用 .asMaybe() 方法,将它转换为 Maybe

    Driver

    Driver(司机?) 是一个精心准备的特征序列。它主要是为了简化 UI 层的代码。不过如果你遇到的序列具有以下特征,你也可以使用它:

    • 不会产生 error 事件
    • 一定在 MainScheduler 监听(主线程监听)
    • 共享附加作用

    这些都是驱动 UI 的序列所具有的特征。

    为什么要使用 Driver ?

    我们举个例子来说明一下,为什么要使用 Driver

    这是文档简介页的例子:

    let results = query.rx.text
    .throttle(0.3, scheduler: MainScheduler.instance)
    .flatMapLatest { query in
    fetchAutoCompleteItems(query)
    }

    results
    .map { "\($0.count)" }
    .bind(to: resultCount.rx.text)
    .disposed(by: disposeBag)

    results
    .bind(to: resultsTableView.rx.items(cellIdentifier: "Cell")) {
    (_, result, cell) in
    cell.textLabel?.text = "\(result)"
    }
    .disposed(by: disposeBag)

    这段代码的主要目的是:

    • 取出用户输入稳定后的内容
    • 向服务器请求一组结果
    • 将返回的结果绑定到两个 UI 元素上:tableView 和 显示结果数量的label

    那么这里存在什么问题?

    • 如果 fetchAutoCompleteItems 的序列产生了一个错误(网络请求失败),这个错误将取消所有绑定,当用户输入一个新的关键字时,是无法发起新的网络请求。
    • 如果 fetchAutoCompleteItems 在后台返回序列,那么刷新页面也会在后台进行,这样就会出现异常崩溃。
    • 返回的结果被绑定到两个 UI 元素上。那就意味着,每次用户输入一个新的关键字时,就会分别为两个 UI 元素发起 HTTP 请求,这并不是我们想要的结果。

    一个更好的方案是这样的:

    let results = query.rx.text
    .throttle(0.3, scheduler: MainScheduler.instance)
    .flatMapLatest { query in
    fetchAutoCompleteItems(query)
    .observeOn(MainScheduler.instance) // 结果在主线程返回
    .catchErrorJustReturn([]) // 错误被处理了,这样至少不会终止整个序列
    }
    .share(replay: 1) // HTTP 请求是被共享的

    results
    .map { "\($0.count)" }
    .bind(to: resultCount.rx.text)
    .disposed(by: disposeBag)

    results
    .bind(to: resultsTableView.rx.items(cellIdentifier: "Cell")) {
    (_, result, cell) in
    cell.textLabel?.text = "\(result)"
    }
    .disposed(by: disposeBag)

    在一个大型系统内,要确保每一步不被遗漏是一件不太容易的事情。所以更好的选择是合理运用编译器和特征序列来确保这些必备条件都已经满足。

    以下是使用 Driver 优化后的代码:

    let results = query.rx.text.asDriver()        // 将普通序列转换为 Driver
    .throttle(0.3, scheduler: MainScheduler.instance)
    .flatMapLatest { query in
    fetchAutoCompleteItems(query)
    .asDriver(onErrorJustReturn: []) // 仅仅提供发生错误时的备选返回值
    }

    results
    .map { "\($0.count)" }
    .drive(resultCount.rx.text) // 这里改用 `drive` 而不是 `bindTo`
    .disposed(by: disposeBag) // 这样可以确保必备条件都已经满足了

    results
    .drive(resultsTableView.rx.items(cellIdentifier: "Cell")) {
    (_, result, cell) in
    cell.textLabel?.text = "\(result)"
    }
    .disposed(by: disposeBag)

    首先第一个 asDriver 方法将 ControlProperty 转换为 Driver

    然后第二个变化是:

    .asDriver(onErrorJustReturn: [])

    任何可监听序列都可以被转换为 Driver,只要他满足 3 个条件:

    • 不会产生 error 事件
    • 一定在 MainScheduler 监听(主线程监听)
    • 共享附加作用

    那么要如何确定条件都被满足?通过 Rx 操作符来进行转换。asDriver(onErrorJustReturn: []) 相当于以下代码:

    let safeSequence = xs
    .observeOn(MainScheduler.instance) // 主线程监听
    .catchErrorJustReturn(onErrorJustReturn) // 无法产生错误
    .share(replay: 1, scope: .whileConnected)// 共享附加作用
    return Driver(raw: safeSequence) // 封装

    最后使用 drive 而不是 bindTo

    drive 方法只能被 Driver 调用。这意味着,如果你发现代码所存在 drive,那么这个序列不会产生错误事件并且一定在主线程监听。这样你可以安全的绑定 UI 元素。

    收起阅读 »

    iOS RXSwift 4.2

    iOS
    SingleSingle 是 Observable 的另外一个版本。不像 Observable 可以发出多个元素,它要么只能发出一个元素,要么产生一个 error 事件。发出一个元素,或一个...
    继续阅读 »

    Single

    Single 是 Observable 的另外一个版本。不像 Observable 可以发出多个元素,它要么只能发出一个元素,要么产生一个 error 事件。

    一个比较常见的例子就是执行 HTTP 请求,然后返回一个应答错误。不过你也可以用 Single 来描述任何只有一个元素的序列。

    如何创建 Single

    创建 Single 和创建 Observable 非常相似:

    func getRepo(_ repo: String) -> Single<[String: Any]> {

    return Single<[String: Any]>.create { single in
    let url = URL(string: "https://api.github.com/repos/\(repo)")!
    let task = URLSession.shared.dataTask(with: url) {
    data, _, error in

    if let error = error {
    single(.error(error))
    return
    }

    guard let data = data,
    let json = try? JSONSerialization.jsonObject(with: data, options: .mutableLeaves),
    let result = json as? [String: Any] else {
    single(.error(DataError.cantParseJSON))
    return
    }

    single(.success(result))
    }

    task.resume()

    return Disposables.create { task.cancel() }
    }
    }

    之后,你可以这样使用 Single

    getRepo("ReactiveX/RxSwift")
    .subscribe(onSuccess: { json in
    print("JSON: ", json)
    }, onError: { error in
    print("Error: ", error)
    })
    .disposed(by: disposeBag)

    订阅提供一个 SingleEvent 的枚举:

    public enum SingleEvent<Element> {
    case success(Element)
    case error(Swift.Error)
    }
    • success - 产生一个单独的元素
    • error - 产生一个错误

    你同样可以对 Observable 调用 .asSingle() 方法,将它转换为 Single

    Completable

    Completable 是 Observable 的另外一个版本。不像 Observable 可以发出多个元素,它要么只能产生一个 completed 事件,要么产生一个 error 事件。

    • 发出零个元素
    • 发出一个 completed 事件或者一个 error 事件
    • 不会共享附加作用

    Completable 适用于那种你只关心任务是否完成,而不需要在意任务返回值的情况。它和 Observable<Void> 有点相似。

    如何创建 Completable

    创建 Completable 和创建 Observable 非常相似:

    func cacheLocally() -> Completable {
    return Completable.create { completable in
    // Store some data locally
    ...
    ...

    guard success else {
    completable(.error(CacheError.failedCaching))
    return Disposables.create {}
    }

    completable(.completed)
    return Disposables.create {}
    }
    }

    之后,你可以这样使用 Completable

    cacheLocally()
    .subscribe(onCompleted: {
    print("Completed with no error")
    }, onError: { error in
    print("Completed with an error: \(error.localizedDescription)")
    })
    .disposed(by: disposeBag)

    订阅提供一个 CompletableEvent 的枚举:

    public enum CompletableEvent {
    case error(Swift.Error)
    case completed
    }
    • completed - 产生完成事件
    • error - 产生一个错误
    收起阅读 »

    iOS RXSwift 4.1

    iOS
    Observable - 可监听序列所有的事物都是序列之前我们提到,Observable 可以用于描述元素异步产生的序列。这样我们生活中许多事物都可以通过它来表示,例如:Observable<Double> 温度你可以将温度看作...
    继续阅读 »

    Observable - 可监听序列

    1

    所有的事物都是序列

    之前我们提到,Observable 可以用于描述元素异步产生的序列。这样我们生活中许多事物都可以通过它来表示,例如:

    • Observable<Double> 温度

      你可以将温度看作是一个序列,然后监测这个温度值,最后对这个值做出响应。例如:当室温高于 33 度时,打开空调降温。

      1

    • Observable<OnePieceEpisode> 《海贼王》动漫

      你也可以把《海贼王》的动漫看作是一个序列。然后当《海贼王》更新一集时,我们就立即观看这一集。

      1

    • Observable<JSON> JSON

      你可以把网络请求的返回的 JSON 看作是一个序列。然后当取到 JSON 时,将它打印出来。

      1

    • Observable<Void> 任务回调

      你可以把任务回调看作是一个序列。当任务结束后,提示用户任务已完成。

      1

    如何创建序列

    现在我们已经可以把生活中的许多事物看作是一个序列了。那么我们要怎么创建这些序列呢?

    实际上,框架已经帮我们创建好了许多常用的序列。例如:button的点击,textField的当前文本,switch的开关状态,slider的当前数值等等。

    另外,有一些自定义的序列是需要我们自己创建的。这里介绍一下创建序列最基本的方法,例如,我们创建一个 [0, 1, ... 8, 9] 的序列:

    1

    let numbers: Observable<Int> = Observable.create { observer -> Disposable in

    observer.onNext(0)
    observer.onNext(1)
    observer.onNext(2)
    observer.onNext(3)
    observer.onNext(4)
    observer.onNext(5)
    observer.onNext(6)
    observer.onNext(7)
    observer.onNext(8)
    observer.onNext(9)
    observer.onCompleted()

    return Disposables.create()
    }

    创建序列最直接的方法就是调用 Observable.create,然后在构建函数里面描述元素的产生过程。 observer.onNext(0) 就代表产生了一个元素,他的值是 0。后面又产生了 9 个元素分别是 1, 2, ... 8, 9 。最后,用 observer.onCompleted() 表示元素已经全部产生,没有更多元素了。

    你可以用这种方式来封装功能组件,例如,闭包回调:

    1

    typealias JSON = Any

    let json: Observable<JSON> = Observable.create { (observer) -> Disposable in

    let task = URLSession.shared.dataTask(with: ...) { data, _, error in

    guard error == nil else {
    observer.onError(error!)
    return
    }

    guard let data = data,
    let jsonObject = try? JSONSerialization.jsonObject(with: data, options: .mutableLeaves)
    else {
    observer.onError(DataError.cantParseJSON)
    return
    }

    observer.onNext(jsonObject)
    observer.onCompleted()
    }

    task.resume()

    return Disposables.create { task.cancel() }
    }

    在闭包回调中,如果任务失败,就调用 observer.onError(error!)。如果获取到目标元素,就调用 observer.onNext(jsonObject)。由于我们的这个序列只有一个元素,所以在成功获取到元素后,就直接调用 observer.onCompleted() 来表示任务结束。最后 Disposables.create { task.cancel() } 说明如果数据绑定被清除(订阅被取消)的话,就取消网络请求。

    这样一来我们就将传统的闭包回调转换成序列了。然后可以用 subscribe 方法来响应这个请求的结果:

    json
    .subscribe(onNext: { json in
    print("取得 json 成功: \(json)")
    }, onError: { error in
    print("取得 json 失败 Error: \(error.localizedDescription)")
    }, onCompleted: {
    print("取得 json 任务成功完成")
    })
    .disposed(by: disposeBag)

    这里subscribe后面的onNext,onErroronCompleted 分别响应我们创建 json 时,构建函数里面的onNext,onErroronCompleted 事件。我们称这些事件为 Event:

    Event - 事件

    public enum Event<Element> {
    case next(Element)
    case error(Swift.Error)
    case completed
    }
    • next - 序列产生了一个新的元素
    • error - 创建序列时产生了一个错误,导致序列终止
    • completed - 序列的所有元素都已经成功产生,整个序列已经完成

    你可以合理的利用这些 Event 来实现业务逻辑。

    决策树

    现在我们知道如何用最基本的方法创建序列。你还可参考 决策树 来选择其他的方式创建序列。

    特征序列

    我们都知道 Swift 是一个强类型语言,而强类型语言相对于弱类型语言的一个优点是更加严谨。我们可以通过类型来判断出,实例有哪些特征。同样的在 RxSwift 里面 Observable 也存在一些特征序列,这些特征序列可以帮助我们更准确的描述序列。并且它们还可以给我们提供语法糖,让我们能够用更加优雅的方式书写代码,他们分别是:

    ℹ️ 提示:由于可被观察的序列(Observable)名字过长,很多时候会增加阅读难度,所以笔者在必要时会将它简写为:序列

    收起阅读 »

    iOS RXSwift 4

    iOS
    数据绑定(订阅)在 RxSwift 里有一个比较重要的概念就是数据绑定(订阅)。就是指将可监听序列绑定到观察者上:我们对比一下这两段代码:let image: UIImage = UIImage(named: ...) imageView....
    继续阅读 »

    数据绑定(订阅)

    在 RxSwift 里有一个比较重要的概念就是数据绑定(订阅)。就是指将可监听序列绑定到观察者上:

    我们对比一下这两段代码:

    let image: UIImage = UIImage(named: ...)
    imageView.image = image
    let image: Observable<UIImage> = ...
    image.bind(to: imageView.rx.image)

    第一段代码我们非常熟悉,它就是将一个单独的图片设置到imageView上。

    第二段代码则是将一个图片序列 “同步” 到imageView上。这个序列里面的图片可以是异步产生的。这里定义的 image 就是上图中蓝色部分(可监听序列),imageView.rx.image就是上图中橙色部分(观察者)。而这种 “同步机制” 就是数据绑定(订阅)

    RxSwift 核心

    这一章主要介绍 RxSwift 的核心内容:

    // Observable<String>
    let text = usernameOutlet.rx.text.orEmpty.asObservable()

    // Observable<Bool>
    let passwordValid = text
    // Operator
    .map { $0.characters.count >= minimalUsernameLength }

    // Observer<Bool>
    let observer = passwordValidOutlet.rx.isHidden

    // Disposable
    let disposable = passwordValid
    // Scheduler 用于控制任务在那个线程队列运行
    .subscribeOn(MainScheduler.instance)
    .observeOn(MainScheduler.instance)
    .bind(to: observer)


    ...

    // 取消绑定,你可以在退出页面时取消绑定
    disposable.dispose()

    下面几节会详细介绍这几个组件的功能和用法。

    ℹ️ 提示:这一章主要介绍一些偏理论方面的知识。你如果觉得阅读起来比较乏味的话,可以先快速地浏览一遍,了解 RxSwift 的核心组件大概有哪些内容。待以后遇到实际问题时,在回来查询。你可以直接跳到 更多例子 章节,去了解如何应用 RxSwift


    收起阅读 »