注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

聊一聊ThreadLocal,终于搞明白了

ThreadLocal是什么? 试想以下情况: 在多线程的情况下,对一同一个变量操作可能会出现冲突,解决的办法就是对这个变量加锁。但是我们有时候其实就是想要一个全局变量,不想让多个线程去干扰。那么能不能有一个变量,名字相同,但是多个线程操作的时候又不会相互影响...
继续阅读 »

ThreadLocal是什么?


试想以下情况:


在多线程的情况下,对一同一个变量操作可能会出现冲突,解决的办法就是对这个变量加锁。但是我们有时候其实就是想要一个全局变量,不想让多个线程去干扰。那么能不能有一个变量,名字相同,但是多个线程操作的时候又不会相互影响呢?


从另外一个角度来说,对于一个变量,在一个线程的任何一个地方都可能需要用到,但是通过传参的方式又比较麻烦,有没有一个变量是贯穿整个线程,我们想取就能取到的呢。


ThreadLocal就是这么一个变量,那么这个变量是怎么实现的呢?


ThreadLocal源码分析


ThreadLocal github地址


首先看用法


public class Client {
private static final ThreadLocal<String> myThreadLocal = ThreadLocal.withInitial(() -> "This is the initial value");

public static void main(String[] args) {

for (int i = 0; i < 6; i++){
new Thread(new MyRunnable(), "线程"+i).start();
}

}

public static class MyRunnable implements Runnable {

@Override
public void run() {
String name = Thread.currentThread().getName();
System.out.println(name + "的threadLocal"+ ",设置为" + name);
myThreadLocal.set(name);
try {
Thread.sleep(1000);
} catch (InterruptedException ignored) {}
System.out.println(name + ":" + myThreadLocal.get());
}

}
}

------

线程0的threadLocal,设置为线程0
线程3的threadLocal,设置为线程3
线程4的threadLocal,设置为线程4
线程2的threadLocal,设置为线程2
线程1的threadLocal,设置为线程1
线程5的threadLocal,设置为线程5
线程0:线程0
线程4:线程4
线程5:线程5
线程2:线程2
线程1:线程1
线程3:线程3

例子中有六个线程,myThreadLocal里存的都是自己线程独有的变量。这样就实现了变量的线程隔离,而且如果不向传参数,在另外一个函数里直接就能get到这个变量,这对于很多场景下都非常有用。


我们下面来分析一下源码:



  1. 首先每一个Thread,都有一个ThreadLocalMap,变量名字叫做threadLocas,里面保存的是多个ThreadLocal,所以每一个线程才能保存属于自己线程的值。

  2. ThreadLocal封装了getMap()、Set()、Get()、Remove()4个核心方法。主要是对ThreadLocalMap来进行操作。

  3. ThreadLocalMap是一个ThreadLocal的内部类,它实现了一个自定义的Map,ThreadLocalMap中的Entry[]数组存储数据。

  4. Entry的键是threadLocal变量本身,值就是设置的变量的值。Entry的key是对ThreadLocal的弱引用,当ThreadLocal不再有强引用的时候,就会清理掉这个key,防止内存泄漏(然而并不能,后面会说)


5abe86d1459c394b7552c1ef9d7e370c.png


get方法


public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}

ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}



  1. 获取当前的Thread对象,通过getMap获取Thread内的ThreadLocalMap,ThreadLocalMap的定义如下:ThreadLocal.ThreadLocalMap threadLocals = null;

  2. 如果map已经存在,以当前的ThreadLocal为键,获取Entry对象,并从从Entry中取出值

  3. 否则,调用setInitialValue进行初始化。


setInitialValue


private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}



  1. 首先是调用initialValue生成一个初始的value值,深入initialValue函数,我们可知它就是返回一个null;

  2. 然后还是在get以下Map,如果map存在,则直接map.set,这个函数会放在后文说;

  3. 如果不存在则会调用createMap创建ThreadLocalMap,这里正好可以先说明下ThreadLocalMap了。


ThreadLocalMap


createMap


void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}

static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
private static final int INITIAL_CAPACITY = 16;

private Entry[] table;

private int size = 0;

private int threshold; // Default to 0

private void setThreshold(int len) {
threshold = len * 2 / 3;
}

private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}

private static int prevIndex(int i, int len) {
return ((i - 1 >= 0) ? i - 1 : len - 1);
}
...
}


  1. 首先是Entry的定义,前面已经说过;

  2. 初始的容量为INITIAL_CAPACITY = 16

  3. 主要数据结构就是一个Entry的数组table;

  4. size用于记录Map中实际存在的entry个数;

  5. threshold是扩容上限,当size到达threashold时,需要resize整个Map,threshold的初始值为len * 2 / 3

  6. nextIndex和prevIndex则是为了安全的移动索引,后面的函数里经常用到。


map.getEntry


private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
return e;
else
return getEntryAfterMiss(key, i, e);
}



  1. 计算索引位置

  2. 获取Entry,如果Entry存在,且key和threadLocal相等,那么返回

  3. 否则,调用getEntryAfterMiss。


getEntryAfterMiss


private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;

while (e != null) {
ThreadLocal<?> k = e.get();
if (k == key)
return e;
if (k == null)
expungeStaleEntry(i);
else
i = nextIndex(i, len);
e = tab[i];
}
return null;
}


  1. 如果k==key,那么代表找到了这个所需要的Entry,直接返回;

  2. 如果k==null,那么证明这个Entry中key已经为null,那么这个Entry就是一个过期对象,这里调用expungeStaleEntry清理该Entry。



为什么会需要清理呢?


如果说ThreadLocal变量被人为的置为null了,ThreadLocal对象只有一个弱引用指着,就会被GC,Entry的key没有了,value可能会内存泄漏。ThreadLocal在每一个get,set的时候都会清理这种过期的key。


为什么需要循环查找key?


这是一种解决hash冲突的手段,这里用的是开放地址法,既有冲突之后,把要插入的元素放在要插入的位置后面为null的地方。HashMap则采用的是链地址法。



expungeStaleEntry


private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;

// expunge entry at staleSlot
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;

// Rehash until we encounter null
Entry e;
int i;
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
if (k == null) {
e.value = null;
tab[i] = null;
size--;
} else {
int h = k.threadLocalHashCode & (len - 1);
if (h != i) {
tab[i] = null;

// Unlike Knuth 6.4 Algorithm R, we must scan until
// null because multiple entries could have been stale.
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i;
}


  1. expunge entry at staleSlot:这段主要是将i位置上的Entry的value设为null,Entry的引用也设为null,那么系统GC的时候自然会清理掉这块内存;

  2. Rehash until we encounter null: 这段就是扫描位置staleSlot之后,null之前的Entry数组,清除每一个key为null的Entry,同时若是key不为空,做rehash,调整其位置。



这里rehash的作用是什么?


我们清理的过程中会把某个值设置为null,如果之前这个值后面的区域是和前两连起来的,那么下次循环查找的时候,就会只查到null为止。比如三个hash值碰撞的key,中间的那个被删除了,那么第三个key在查找的时候会从第一个开始查找,查找到第二个就停止了,第三个就查不到了。



set


public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}

map.set


private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);

for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();

if (k == key) {
e.value = value;
return;
}

if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}

tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}


  1. 首先还是根据key计算出位置i,然后查找i位置上的Entry,

  2. 若是Entry已经存在并且key等于传入的key,那么这时候直接给这个Entry赋新的value值。

  3. 若是Entry存在,但是key为null,则调用replaceStaleEntry来更换这个key为空的Entry

  4. 不断循环检测,直到遇到为null的地方,这时候要是还没在循环过程中return,那么就在这个null的位置新建一个Entry,并且插入,同时size增加1。

  5. 最后调用cleanSomeSlots,这个函数就不细说了,你只要知道内部还是调用了上面提到的expungeStaleEntry函数清理key为null的Entry就行了,最后返回是否清理了Entry,接下来再判断sz>thresgold,这里就是判断是否达到了rehash的条件,达到的话就会调用rehash函数。


rehash


private void rehash() {
expungeStaleEntries();

// Use lower threshold for doubling to avoid hysteresis
if (size >= threshold - threshold / 4)
resize();
}


private void resize() {
Entry[] oldTab = table;
int oldLen = oldTab.length;
int newLen = oldLen * 2;
Entry[] newTab = new Entry[newLen];
int count = 0;

for (int j = 0; j < oldLen; ++j) {
Entry e = oldTab[j];
if (e != null) {
ThreadLocal<?> k = e.get();
if (k == null) {
e.value = null; // Help the GC
} else {
int h = k.threadLocalHashCode & (newLen - 1);
while (newTab[h] != null)
h = nextIndex(h, newLen);
newTab[h] = e;
count++;
}
}
}

setThreshold(newLen);
size = count;
table = newTab;
}


  1. 首先,size大于threshold的时候才会rehash。

  2. 清理空key,如果size大于3/4的threshold,调用resize()函数。

  3. 每次扩容大小扩展为原来的2倍,然后再一个for循环里,清除空key的Entry,同时重新计算key不为空的Entry的hash值,把它们放到正确的位置上,再更新ThreadLocalMap的所有属性。


remove


private void remove(ThreadLocal<?> key) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
if (e.get() == key) {
e.clear();
expungeStaleEntry(i);
return;
}
}
}

先计算出hash值,若是第一次没有命中,就循环直到null,在此过程中也会调用expungeStaleEntry清除空key节点。


什么是内存泄漏?


当程序分配了空间但是却忘了回收导致以后的程序都无法或暂时无法使用这块空间,就发生了内存泄漏。和内存溢出不一样,内存溢出是内存不足的时候出现的。这块要理解清楚,才能明白ThreadLocal为什么会导致内存泄漏。


Java 引用类型


要说到ThreadLocal引起内存泄漏,还得从java的四种引用类型说起。


java中有四种引用类型,分别是强软弱虚。


强引用


一个对象被强引用,那么他就不会被回收。


软引用


如果一个对象只具有软引用,那么它的性质属于可有可无的那种。如果此时内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。


软引用可以和一个引用队列联合使用,如果软件用所引用的对象被垃圾回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中。


    Object obj = new Object();
ReferenceQueue queue = new ReferenceQueue();
SoftReference reference = new SoftReference(obj, queue);
//强引用对象滞空,保留软引用
obj = null;
当内存不足时,软引用对象被回收时,reference.get()为null,此时软引用对象的作用已经发挥完毕,这时将其添加进ReferenceQueue 队列中

如果要判断哪些软引用对象已经被清理:


    SoftReference ref = null;
while ((ref = (SoftReference) queue.poll()) != null) {
//清除软引用对象
}

弱引用


弱引用和软引用的区别就是,如果一个对象只有弱引用,那么只要GC,不管内存够不够,都会回收他的内存。注意这里的”只有弱引用“。如果这个对象还被其他变量强引用,那么他是不会被回收的。


虚引用


如果一个对象与虚引用关联,则跟没有引用与之关联一样,在任何时候都可能被垃圾回收器回收。虚引用主要用来跟踪对象被垃圾回收的活动。





































引用类型被垃圾回收时间用途生存时间
强引用从来不会对象的一般状态JVM停止运行时终止
软引用在内存不足时对象缓存内存不足时终止
弱引用在垃圾回收时对象缓存垃圾回收时终止
虚引用UnkonwnUnkonwnUnkonwn


为什么要有四种引用类型?



  1. 可以让程序员通过代码的方式来决定某个对象的生命周期。

  2. 有利于垃圾回收

  3. 能够实现一些复杂的数据结构。



ThreadLocal什么情况下会出现内存泄漏?


threadlocal里面使用了一个存在弱引用的map,当释放掉threadlocal的强引用以后,map里面的value却没有被回收.而这块value永远不会被访问到了. 所以存在着内存泄露。


如果这个时候还会去调用get set方法,那么这块内存可能会被清理掉。


如果没有去调用get set方法,如果这个线程很快销毁了,那么也不会内存泄漏。


最坏的情况就是,threadLocal对象设置成null了,然后使用线程池,这个线程被重复使用了,但是有一直没有调用get set方法,这个期间就会发生真正的内存泄漏。


其实ThreadLocal发生内存泄露的条件还是比较苛刻的,只要是使用规范,那么就没有什么问题。


ThreadLocal最佳实践



  1. 每次使用完手动调用remove函数,删除不再使用的ThreadLocal.

  2. 可以将ThreadLocal设置成private static的,这样ThreadLocal会尽量和线程本身一起消亡。


ThreadLocal应用案例



管理数据库连接。


  假如A类的方法a中,会调用B类的方法b和C类的方法c,a方法开启了事务,b方法和c方法会去操作数据库。我们知道,要想实现事务,那么b方法和c方法中所使用的的数据库连接一定是同一个连接,那怎么才能实现所用的是同一个数据库连接呢?答案就是通过ThreadLocal来管理。


MDC日志链路追踪。


MDC(Mapped Diagnostic Contexts)主要用于保存每次请求的上下文参数,同时可以在日志输出的格式中直接使用 %X{key} 的方式,将上下文中的参数输出至每一行日志中。而保存上下文信息主要就是通过ThreadLocal来实现的。
假如在交易流程每个环节的日志中,你都想打印全局流水号transId,流程可能涉及多个系统、多个线程、多个方法。有一些环节中,全局流水号并不能当做参数传递,那你怎么才能获取这个transId参数呢,这里就是利用了Threadlocal特性。每个系统或者线程在接收到请求时,都会将transId存放到ThreadLocal中,在输出日志时,将transId获取出来,进行打印。这样,我们就可以通过transId,在日志文件中查询全链路的日志信息了。



InheritableThreadLocal


使用ThreadLocal时,子线程获取不到父线程通过set方法保存的数据,要想使子线程也可以获取到,可以使用InheritableThreadLocal类。


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

【墙裂推荐】球球了,RPC之间调用别再使用 if-else做参数校验了

RPC
RPC调用时使用 @Validated进行参数校验不起作用 球球了,RPC之间调用别再使用 if-else做参数校验了。众所周知,@Validated 是一款非常好用的参数校验工具。但在RPC调用时不可用,在当前的微服务大环境下,微服务之间的调用怎么做到优雅...
继续阅读 »

RPC调用时使用 @Validated进行参数校验不起作用


球球了,RPC之间调用别再使用 if-else做参数校验了。众所周知,@Validated 是一款非常好用的参数校验工具。但在RPC调用时不可用,在当前的微服务大环境下,微服务之间的调用怎么做到优雅的参数校验呢?


话不多说,直接上干货


引包


<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>

1. 参数校验 这里我们先要定义一个注解来代替来继承 @Validated


import org.springframework.validation.annotation.Validated;

@Validated
public @interface RPCValidated {
}

2. 然后使用AOP来解析参数,进行参数校验。


import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.hibernate.validator.messageinterpolation.ResourceBundleMessageInterpolator;
import org.hibernate.validator.resourceloading.PlatformResourceBundleLocator;
import org.springframework.stereotype.Component;
import wangjubao.base.common.extend.Response;

import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;

@Aspect
@Component
@Slf4j
public class ValidatedAop {
private static Validator validator;

static {
validator = Validation.byDefaultProvider().configure()
.messageInterpolator(new ResourceBundleMessageInterpolator(
new PlatformResourceBundleLocator("validationMessages")))
.buildValidatorFactory().getValidator();
}

@Around("@annotation(com.qiaoba.annotation.RPCValidated))")
public Object around(ProceedingJoinPoint joinPoint) {
Object[] args = joinPoint.getArgs();
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
// 执行方法参数的校验
Set<ConstraintViolation<Object>> constraintViolations = validator.forExecutables().validateParameters(joinPoint.getThis(), signature.getMethod(), args);
List<String> messages = new ArrayList<>();
for (ConstraintViolation<Object> error : constraintViolations) {
messages.add(error.getMessage());
}
if (!messages.isEmpty()) {
return Response.paramError("参数错误:", messages.get(0));
}
try {
return joinPoint.proceed(args);
} catch (Throwable e) {
e.printStackTrace();
return Response.error("操作失败:", e.getMessage());
}
}
}

3. 使用方法,在接口Impl实现类加上定义的@RPCValidated


@Override
@RPCValidated
public Response create(MessageSmsRechargeDto dto) {
//todo:业务逻辑......
}

4. 在Interfaces接口层加上@Valid注解


Response create(@Valid MessageSmsRechargeDto params);

5. 实体类


@Data
public class MessageSmsRechargeDto implements Serializable {


/**
* 充值公司
*/
@NotNull(message = "充值公司不能为空 ")
private Long companyId;

/**
* 充值备注
*/
@NotEmpty(message = "充值备注不能为空 ")
private String rechargeRemark;
}

--- 至此,整个流程完成,加上自定义的参数校验注解@RPCValidated后,就可以优雅的进行参数校验,不用再写各种if-else 做参数校验了


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

Flutter APP 前期准备工作

组件库可参考:flutter.dev、bruno(贝壳开源组件库) 以下从部分GetX文档转载 用于记录。 框架: Flutter GetX GetX 是 Flutter 上的一个轻量且强大的解决方案:高性能的状态管理、智能的依赖注入和便捷的路由管理。 Get...
继续阅读 »

组件库可参考:flutter.dev、bruno(贝壳开源组件库)


以下从部分GetX文档转载 用于记录。
框架: Flutter GetX


GetX 是 Flutter 上的一个轻量且强大的解决方案:高性能的状态管理、智能的依赖注入和便捷的路由管理。


GetX 有3个基本原则:


性能: GetX 专注于性能和最小资源消耗。GetX 打包后的apk占用大小和运行时的内存占用与其他状态管理插件不相上下。效率: GetX 的语法非常简捷,并保持了极高的性能,能极大缩短你的开发时长。 结构: GetX 可以将界面、逻辑、依赖和路由完全解耦,用起来更清爽,逻辑更清晰,代码更容易维护。 GetX 并不臃肿,却很轻量。


三大功能


状态管理


目前,Flutter有几种状态管理器。但是,它们中的大多数都涉及到使用ChangeNotifier来更新widget,这对于中大型应用的性能来说是一个很糟糕的方法。你可以在Flutter的官方文档中查看到,ChangeNotifier应该使用1个或最多2个监听器,这使得它们实际上无法用于任何中等或大型应用。


Get 并不是比任何其他状态管理器更好或更差,而是说你应该分析这些要点以及下面的要点来选择只用Get,还是与其他状态管理器结合使用。


Get不是其他状态管理器的敌人,因为Get是一个微框架,而不仅仅是一个状态管理器,既可以单独使用,也可以与其他状态管理器结合使用。


Get有两个不同的状态管理器:简单的状态管理器(GetBuilder)和响应式状态管理器(GetX)。


响应式状态管理器


响应式编程可能会让很多人感到陌生,因为觉得它很复杂,但是GetX将响应式编程变得非常简单。



  • 你不需要创建StreamControllers.

  • 你不需要为每个变量创建一个StreamBuilder。

  • 你不需要为每个状态创建一个类。

  • 你不需要为一个初始值创建一个get。


使用 Get 的响应式编程就像使用 setState 一样简单。


让我们想象一下,你有一个名称变量,并且希望每次你改变它时,所有使用它的小组件都会自动刷新。


这就是你的计数变量。


var name = 'Jonatas Borges';

要想让它变得可观察,你只需要在它的末尾加上".obs"。


var name = 'Jonatas Borges'.obs;

而在UI中,当你想显示该值并在值变化时更新页面,只需这样做。


Obx(() => Text("${controller.name}"));

这就是全部,就这么简单。


关于状态管理的更多细节


关于状态管理更深入的解释请查看这里。在那里你将看到更多的例子,以及简单的状态管理器和响应式状态管理器之间的区别


你会对GetX的能力有一个很好的了解。


路由管理


如果你想免上下文(context)使用路由/snackbars/dialogs/bottomsheets,GetX对你来说也是极好的,来吧展示:


在你的MaterialApp前加上 "Get",把它变成GetMaterialApp。


GetMaterialApp( // Before: MaterialApp(
home: MyHome(),
)

导航到新页面


Get.to(NextScreen());

用别名导航到新页面。查看更多关于命名路由的详细信息这里


Get.toNamed('/details');

要关闭snackbars, dialogs, bottomsheets或任何你通常会用Navigator.pop(context)关闭的东西。


Get.back();

进入下一个页面,但没有返回上一个页面的选项(用于闪屏页,登录页面等)。


Get.off(NextScreen());

进入下一个页面并取消之前的所有路由(在购物车、投票和测试中很有用)。


Get.offAll(NextScreen());

注意到你不需要使用context来做这些事情吗?这就是使用Get路由管理的最大优势之一。有了它,你可以在你的控制器类中执行所有这些方法,而不用担心context在哪里。


关于路由管理的更多细节


关于别名路由,和对路由的低级控制,请看这里


依赖管理


Get有一个简单而强大的依赖管理器,它允许你只用1行代码就能检索到与你的Bloc或Controller相同的类,无需Provider context,无需inheritedWidget。


Controller controller = Get.put(Controller()); // 而不是 Controller controller = Controller();


  • 注意:如果你使用的是Get的状态管理器,请多注意绑定api,这将使你的界面更容易连接到你的控制器。


你是在Get实例中实例化它,而不是在你使用的类中实例化你的类,这将使它在整个App中可用。 所以你可以正常使用你的控制器(或类Bloc)。


提示:  Get依赖管理与包的其他部分是解耦的,所以如果你的应用已经使用了一个状态管理器(任何一个,都没关系),你不需要全部重写,你可以使用这个依赖注入。


controller.fetchApi();

想象一下,你已经浏览了无数条路由,现在你需要拿到一个被遗留在控制器中的数据,那你需要一个状态管理器与Provider或Get_it一起使用来拿到它,对吗?用Get则不然,Get会自动为你的控制器找到你想要的数据,而你甚至不需要任何额外的依赖关系。


Controller controller = Get.find();
//是的,它看起来像魔术,Get会找到你的控制器,并将其提供给你。你可以实例化100万个控制器,Get总会给你正确的控制器。

然后你就可以恢复你在后面获得的控制器数据。


Text(controller.textFromApi);

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

Android中的类加载器

类的生命周期加载阶段加载阶段可以细分如下加载类的二进制流数据结构转换,将二进制流所代表的静态存储结构转化成方法区的运行时的数据结构生成java.lang.Class对象,作为方法区这个类的各种数据的访问入口加载类的二进制流的方法从zip包中读取。我们常见的JA...
继续阅读 »

类的生命周期

image.png

加载阶段

加载阶段可以细分如下

  • 加载类的二进制流
  • 数据结构转换,将二进制流所代表的静态存储结构转化成方法区的运行时的数据结构
  • 生成java.lang.Class对象,作为方法区这个类的各种数据的访问入口

加载类的二进制流的方法

  • 从zip包中读取。我们常见的JAR、AAR依赖
  • 运行时动态生成。我们常见的动态代理技术,在java.reflect.Proxy中就是用ProxyGenerateProxyClass来为特定的接口生成代理的二进制流
验证

验证是连接阶段的第一步,这一阶段的目的是为了确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。

  1. 文件格式验证:如是否以魔数 0xCAFEBABE 开头、主、次版本号是否在当前虚拟机处理范围之内、常量合理性验证等。
    此阶段保证输入的字节流能正确地解析并存储于方法区之内,格式上符合描述一个 Java类型信息的要求。
  2. 元数据验证:是否存在父类,父类的继承链是否正确,抽象类是否实现了其父类或接口之中要求实现的所有方法,字段、方法是否与父类产生矛盾等。
    第二阶段,保证不存在不符合 Java 语言规范的元数据信息。
  3. 字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。例如保证跳转指令不会跳转到方法体以外的字节码指令上。
  4. 符号引用验证:在解析阶段中发生,保证可以将符号引用转化为直接引用。

可以考虑使用 -Xverify:none 参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。

准备

类变量分配内存并设置类变量初始值,这些变量所使用的内存都将在方法区中进行分配。

解析

虚拟机将常量池内的符号引用替换为直接引用的过程。
解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符 7 类符号引用进行

初始化

到初始化阶段,才真正开始执行类中定义的 Java 程序代码,此阶段是执行 () 方法的过程。

类加载的时机

虚拟机规范规定了有且只有 5 种情况必须立即对类进行“初始化”(而加载、验证、准备自然需要在此之前开始)

  1. 遇到new、getstatic 和 putstatic 或 invokestatic 这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。对应场景是:使用 new 实例化对象、读取或设置一个类的静态字段(被 final 修饰、已在编译期把结果放入常量池的静态字段除外)、以及调用一个类的静态方法。

  2. 对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。

  3. 当初始化类的父类还没有进行过初始化,则需要先触发其父类的初始化。(而一个接口在初始化时,并不要求其父接口全部都完成了初始化)

  4. 虚拟机启动时,用户需要指定一个要执行的主类(包含 main() 方法的那个类),
    虚拟机会先初始化这个主类。

  5. 当使用 JDK 1.7 的动态语言支持时,如果一个 java.lang.invoke.MethodHandle 实例最后的解析结果 REF_getStatic、REF_putStatic、REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。

注意:

  1. 通过子类引用父类的静态字段,不会导致子类初始化。
  2. 通过数组定义来引用类,不会触发此类的初始化。MyClass[] cs = new MyClass[10];
  3. 常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。

类加载器

把实现类加载阶段中的“通过一个类的全限定名来获取描述此类的二进制字节流”这个动作的代码模块称为“类加载器”。

将 class 文件二进制数据放入方法区内,然后在堆内(heap)创建一个 java.lang.Class 对象,Class 对象封装了类在方法区内的数据结构,并且向开发者提供了访问方法区内的数据结构的接口。

类的唯一性

对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性。

即使两个类来源于同一个 Class 文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那这两个类也不相等。
这里所指的“相等”,包括代表类的 Class 对象的 equals() 方法、 isAssignableFrom() 方法、isInstance() 方法的返回结果,也包括使用 instanceof 关键字做对象所属关系判定等情况

双亲委托机制

image.png

如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。

    protected Class loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
// First, check if the class has already been loaded
//先从缓存中加没加载这个类
Class c = findLoadedClass(name);
if (c == null) {
try {
if (parent != null) {
//从parent中加载
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
//加载不到,就自己加载
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
c = findClass(name);
}
}
return c;
}


好处
  • 避免重复加载,当父加载器已经加载了该类的时候,就没有必要子ClassLoader再加载一次。
  • 安全性考虑,防止核心API库被随意篡改。

Android中ClassLoader

image.png

  • ClassLoader是一个抽象类,定义了ClassLoader的主要功能
  • BootClassLoader是ClassLoader的子类(注意不是内部类,有些材料上说是内部类,是不对的),用于加载一些系统Framework层级需要的类,是Android平台上所有的ClassLoader的最终parent
  • SecureClassLoader扩展了ClassLoader类,加入了权限方面的功能,加强了安全性
  • URLClassLoader继承SecureClassLoader,用来通过URI路径从jar文件和文件夹中加载类和资源,在Android中基本无法使用
  • BaseDexClassLoader是实现了Android ClassLoader的大部分功能
  • PathClassLoader加载应用程序的类,会加载/data/app目录下的dex文件以及包含dex的apk文件或者java文件(有些材料上说他也会加载系统类,我没有找到,这里存疑)
  • DexClassLoader可以加载自定义dex文件以及包含dex的apk文件或jar文件,支持从SD卡进行加载。我们使用插件化技术的时候会用到
  • InMemoryDexClassLoader用于加载内存中的dex文件

ClassLoader的加载流程源码分析

-> ClassLoader.java 类

protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized(this.getClassLoadingLock(name)) {
//先查找class是否已经加载过,如果加载过直接返回
Class c = this.findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();

try {
if (this.parent != null) {
//委托给parent加载器进行加载 ClassLoader parent;
c = this.parent.loadClass(name, false);
} else {
//当执行到顶层的类加载器时,parent = null
c = this.findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException var10) {
}

if (c == null) {
long t1 = System.nanoTime();
c = this.findClass(name);
PerfCounter.getParentDelegationTime().addTime(t1 - t0);
PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
//如果parent加载器中没有找到,
PerfCounter.getFindClasses().increment();
}
}

if (resolve) {
this.resolveClass(c);
}

return c;
}
}

由子类实现

protected Class findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}

BaseDexClassLoader类中findClass方法

protected Class findClass(String name) throws ClassNotFoundException {
List suppressedExceptions = new ArrayList();
// pathList是DexPathList,是具体存放代码的地方。
Class c = pathList.findClass(name, suppressedExceptions);
if (c == null) {
ClassNotFoundException cnfe = new ClassNotFoundException(
"Didn't find class "" + name + "" on path: " + pathList);
for (Throwable t : suppressedExceptions) {
cnfe.addSuppressed(t);
}
throw cnfe;
}
return c;
}

public Class findClass(String name, List suppressed) {
for (Element element : dexElements) {
Class clazz = element.findClass(name, definingContext, suppressed);
if (clazz != null) {
return clazz;
}
}

if (dexElementsSuppressedExceptions != null) {
suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
}
return null;
}

public Class findClass(String name, ClassLoader definingContext,
List suppressed) {
return dexFile != null ? dexFile.loadClassBinaryName(name, definingContext, suppressed) : null;
}

public Class loadClassBinaryName(String name, ClassLoader loader, List suppressed) {
return defineClass(name, loader, mCookie, this, suppressed);
}

private static Class defineClass(String name, ClassLoader loader, Object cookie,
DexFile dexFile, List suppressed) {
Class result = null;
try {
result = defineClassNative(name, loader, cookie, dexFile);
} catch (NoClassDefFoundError e) {
if (suppressed != null) {
suppressed.add(e);
}
} catch (ClassNotFoundException e) {
if (suppressed != null) {
suppressed.add(e);
}
}
return result;
}

// 调用 Native 层代码
private static native Class defineClassNative(String name, ClassLoader loader, Object cookie, DexFile dexFile)

image.png

本文转自 juejin.cn/post/703847…,如有侵权,请联系删除。


作者:Gaga246
链接:https://juejin.cn/post/7040625001266937869
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

收起阅读 »

Android 多线程-IntentService详解

IntentService 一、IntentService概述   上一篇我们聊到了HandlerThread,本篇我们就来看看HandlerThread在IntentService中的应用,看本篇前建议先看看上篇的HandlerThread,有助于我们更好掌...
继续阅读 »

IntentService


一、IntentService概述


  上一篇我们聊到了HandlerThread,本篇我们就来看看HandlerThread在IntentService中的应用,看本篇前建议先看看上篇的HandlerThread,有助于我们更好掌握IntentService。同样地,我们先来看看IntentService的特点:



  • 它本质是一种特殊的Service,继承自Service并且本身就是一个抽象类

  • 它可以用于在后台执行耗时的异步任务,当任务完成后会自动停止

  • 它拥有较高的优先级,不易被系统杀死(继承自Service的缘故),因此比较适合执行一些高优先级的异步任务

  • 它内部通过HandlerThread和Handler实现异步操作

  • 创建IntentService时,只需实现onHandleIntent和构造方法,onHandleIntent为异步方法,可以执行耗时操作


二、IntentService的常规使用套路


  大概了解了IntentService的特点后,我们就来了解一下它的使用方式,先看个案例:

IntentService实现类如下:


package com.zejian.handlerlooper;

import android.app.IntentService;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.IBinder;
import android.os.Message;

import com.zejian.handlerlooper.util.LogUtils;

import java.io.BufferedInputStream;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URL;

/**
* Created by zejian
* Time 16/9/3.
* Description:
*/
public class MyIntentService extends IntentService {
public static final String DOWNLOAD_URL="download_url";
public static final String INDEX_FLAG="index_flag";
public static UpdateUI updateUI;


public static void setUpdateUI(UpdateUI updateUIInterface){
updateUI=updateUIInterface;
}

public MyIntentService(){
super("MyIntentService");
}

/**
* 实现异步任务的方法
* @param intent Activity传递过来的Intent,数据封装在intent中
*/
@Override
protected void onHandleIntent(Intent intent) {

//在子线程中进行网络请求
Bitmap bitmap=downloadUrlBitmap(intent.getStringExtra(DOWNLOAD_URL));
Message msg1 = new Message();
msg1.what = intent.getIntExtra(INDEX_FLAG,0);
msg1.obj =bitmap;
//通知主线程去更新UI
if(updateUI!=null){
updateUI.updateUI(msg1);
}
//mUIHandler.sendMessageDelayed(msg1,1000);

LogUtils.e("onHandleIntent");
}
//----------------------重写一下方法仅为测试------------------------------------------
@Override
public void onCreate() {
LogUtils.e("onCreate");
super.onCreate();
}

@Override
public void onStart(Intent intent, int startId) {
super.onStart(intent, startId);
LogUtils.e("onStart");
}

@Override
public int onStartCommand(Intent intent, int flags, int startId) {
LogUtils.e("onStartCommand");
return super.onStartCommand(intent, flags, startId);

}

@Override
public void onDestroy() {
LogUtils.e("onDestroy");
super.onDestroy();
}

@Override
public IBinder onBind(Intent intent) {
LogUtils.e("onBind");
return super.onBind(intent);
}


public interface UpdateUI{
void updateUI(Message message);
}


private Bitmap downloadUrlBitmap(String urlString) {
HttpURLConnection urlConnection = null;
BufferedInputStream in = null;
Bitmap bitmap=null;
try {
final URL url = new URL(urlString);
urlConnection = (HttpURLConnection) url.openConnection();
in = new BufferedInputStream(urlConnection.getInputStream(), 8 * 1024);
bitmap= BitmapFactory.decodeStream(in);
} catch (final IOException e) {
e.printStackTrace();
} finally {
if (urlConnection != null) {
urlConnection.disconnect();
}
try {
if (in != null) {
in.close();
}
} catch (final IOException e) {
e.printStackTrace();
}
}
return bitmap;
}

}

  通过代码可以看出,我们继承了IntentService,这里有两个方法是必须实现的,一个是构造方法,必须传递一个线程名称的字符串,另外一个就是进行异步处理的方法onHandleIntent(Intent intent) 方法,其参数intent可以附带从activity传递过来的数据。这里我们的案例主要利用onHandleIntent实现异步下载图片,然后通过回调监听的方法把下载完的bitmap放在message中回调给Activity(当然也可以使用广播完成),最后通过Handler去更新UI。下面再来看看Acitvity的代码:


activity_intent_service.xml


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

<ImageView
android:id="@+id/image"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>

IntentServiceActivity.java


package com.zejian.handlerlooper.util;

import android.app.Activity;
import android.content.Intent;
import android.graphics.Bitmap;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.widget.ImageView;

import com.zejian.handlerlooper.MyIntentService;
import com.zejian.handlerlooper.R;

/**
* Created by zejian
* Time 16/9/3.
* Description:
*/
public class IntentServiceActivity extends Activity implements MyIntentService.UpdateUI{
/**
* 图片地址集合
*/
private String url[] = {
"https://img-blog.csdn.net/20160903083245762",
"https://img-blog.csdn.net/20160903083252184",
"https://img-blog.csdn.net/20160903083257871",
"https://img-blog.csdn.net/20160903083257871",
"https://img-blog.csdn.net/20160903083311972",
"https://img-blog.csdn.net/20160903083319668",
"https://img-blog.csdn.net/20160903083326871"
};

private static ImageView imageView;
private static final Handler mUIHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
imageView.setImageBitmap((Bitmap) msg.obj);
}
};

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_intent_service);
imageView = (ImageView) findViewById(R.id.image);

Intent intent = new Intent(this,MyIntentService.class);
for (int i=0;i<7;i++) {//循环启动任务
intent.putExtra(MyIntentService.DOWNLOAD_URL,url[i]);
intent.putExtra(MyIntentService.INDEX_FLAG,i);
startService(intent);
}
MyIntentService.setUpdateUI(this);
}

//必须通过Handler去更新,该方法为异步方法,不可更新UI
@Override
public void updateUI(Message message) {
mUIHandler.sendMessageDelayed(message,message.what * 1000);
}
}

  代码比较简单,通过for循环多次去启动IntentService,然后去下载图片,注意即使我们多次启动IntentService,但IntentService的实例只有一个,这跟传统的Service是一样的,最终IntentService会去调用onHandleIntent执行异步任务。这里可能我们还会担心for循环去启动任务,而实例又只有一个,那么任务会不会被覆盖掉呢?其实是不会的,因为IntentService真正执行异步任务的是HandlerThread+Handler,每次启动都会把下载图片的任务添加到依附的消息队列中,最后由HandlerThread+Handler去执行。好~,我们运行一下代码:



每间隔一秒去更新图片,接着我们看一组log:



从Log可以看出onCreate只启动了一次,而onStartCommand和onStart多次启动,这就证实了之前所说的,启动多次,但IntentService的实例只有一个,这跟传统的Service是一样的,最后任务都执行完成后,IntentService自动销毁。以上便是IntentService德使用方式,怎么样,比较简单吧。接着我们就来分析一下IntentService的源码,其实也比较简单只有100多行代码。


三、IntentService源码解析


我们先来看看IntentService的onCreate方法:


@Override
public void onCreate() {
// TODO: It would be nice to have an option to hold a partial wakelock
// during processing, and to have a static startService(Context, Intent)
// method that would launch the service & hand off a wakelock.

super.onCreate();
HandlerThread thread = new HandlerThread("IntentService[" + mName + "]");
thread.start();

mServiceLooper = thread.getLooper();
mServiceHandler = new ServiceHandler(mServiceLooper);
}

  当第一启动IntentService时,它的onCreate方法将会被调用,其内部会去创建一个HandlerThread并启动它,接着创建一个ServiceHandler(继承Handler),传入HandlerThread的Looper对象,这样ServiceHandler就变成可以处理异步线程的执行类了(因为Looper对象与HandlerThread绑定,而HandlerThread又是一个异步线程,我们把HandlerThread持有的Looper对象传递给Handler后,ServiceHandler内部就持有异步线程的Looper,自然就可以执行异步任务了),那么IntentService是怎么启动异步任务的呢?其实IntentService启动后还会去调用onStartCommand方法,而onStartCommand方法又会去调用onStart方法,我们看看它们的源码:


@Override
public void onStart(Intent intent, int startId) {
Message msg = mServiceHandler.obtainMessage();
msg.arg1 = startId;
msg.obj = intent;
mServiceHandler.sendMessage(msg);
}

/**
* You should not override this method for your IntentService. Instead,
* override {@link #onHandleIntent}, which the system calls when the IntentService
* receives a start request.
* @see android.app.Service#onStartCommand
*/
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
onStart(intent, startId);
return mRedelivery ? START_REDELIVER_INTENT : START_NOT_STICKY;
}

  从源码我们可以看出,在onStart方法中,IntentService通过mServiceHandler的sendMessage方法发送了一个消息,这个消息将会发送到HandlerThread中进行处理(因为HandlerThread持有Looper对象,所以其实是Looper从消息队列中取出消息进行处理,然后调用mServiceHandler的handleMessage方法),我们看看ServiceHandler的源码:


private final class ServiceHandler extends Handler {
public ServiceHandler(Looper looper) {
super(looper);
}

@Override
public void handleMessage(Message msg) {
onHandleIntent((Intent)msg.obj);
stopSelf(msg.arg1);
}
}

  这里其实也说明onHandleIntent确实是一个异步处理方法(ServiceHandler本身就是一个异步处理的handler类),在onHandleIntent方法执行结束后,IntentService会通过 stopSelf(int startId)方法来尝试停止服务。这里采用stopSelf(int startId)而不是stopSelf()来停止服务,是因为stopSelf()会立即停止服务,而stopSelf(int startId)会等待所有消息都处理完后才终止服务。最后看看onHandleIntent方法的声明:


protected abstract void onHandleIntent(Intent intent);

  到此我们就知道了IntentService的onHandleIntent方法是一个抽象方法,所以我们在创建IntentService时必须实现该方法,通过上面一系列的分析可知,onHandleIntent方法也是一个异步方法。这里要注意的是如果后台任务只有一个的话,onHandleIntent执行完,服务就会销毁,但如果后台任务有多个的话,onHandleIntent执行完最后一个任务时,服务才销毁。最后我们要知道每次执行一个后台任务就必须启动一次IntentService,而IntentService内部则是通过消息的方式发送给HandlerThread的,然后由Handler中的Looper来处理消息,而Looper是按顺序从消息队列中取任务的,也就是说IntentService的后台任务时顺序执行的,当有多个后台任务同时存在时,这些后台任务会按外部调用的顺序排队执行,我们前面的使用案例也很好说明了这点。最后贴一下到IntentService的全部源码,大家再次感受一下:


/*
* Copyright (C) 2008 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package android.app;

import android.annotation.WorkerThread;
import android.content.Intent;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.IBinder;
import android.os.Looper;
import android.os.Message;

/**
* IntentService is a base class for {@link Service}s that handle asynchronous
* requests (expressed as {@link Intent}s) on demand. Clients send requests
* through {@link android.content.Context#startService(Intent)} calls; the
* service is started as needed, handles each Intent in turn using a worker
* thread, and stops itself when it runs out of work.
*
* <p>This "work queue processor" pattern is commonly used to offload tasks
* from an application's main thread. The IntentService class exists to
* simplify this pattern and take care of the mechanics. To use it, extend
* IntentService and implement {@link #onHandleIntent(Intent)}. IntentService
* will receive the Intents, launch a worker thread, and stop the service as
* appropriate.
*
* <p>All requests are handled on a single worker thread -- they may take as
* long as necessary (and will not block the application's main loop), but
* only one request will be processed at a time.
*
* <div>
* <h3>Developer Guides</h3>
* <p>For a detailed discussion about how to create services, read the
* <a href="{@docRoot}guide/topics/fundamentals/services.html">Services</a> developer guide.</p>
* </div>
*
* @see android.os.AsyncTask
*/
public abstract class IntentService extends Service {
private volatile Looper mServiceLooper;

private volatile ServiceHandler mServiceHandler;
private String mName;
private boolean mRedelivery;

private final class ServiceHandler extends Handler {
public ServiceHandler(Looper looper) {
super(looper);
}

@Override
public void handleMessage(Message msg) {
onHandleIntent((Intent)msg.obj);
stopSelf(msg.arg1);
}
}

/**
* Creates an IntentService. Invoked by your subclass's constructor.
*
* @param name Used to name the worker thread, important only for debugging.
*/
public IntentService(String name) {
super();
mName = name;
}

/**
* Sets intent redelivery preferences. Usually called from the constructor
* with your preferred semantics.
*
* <p>If enabled is true,
* {@link #onStartCommand(Intent, int, int)} will return
* {@link Service#START_REDELIVER_INTENT}, so if this process dies before
* {@link #onHandleIntent(Intent)} returns, the process will be restarted
* and the intent redelivered. If multiple Intents have been sent, only
* the most recent one is guaranteed to be redelivered.
*
* <p>If enabled is false (the default),
* {@link #onStartCommand(Intent, int, int)} will return
* {@link Service#START_NOT_STICKY}, and if the process dies, the Intent
* dies along with it.
*/
public void setIntentRedelivery(boolean enabled) {
mRedelivery = enabled;
}

@Override
public void onCreate() {
// TODO: It would be nice to have an option to hold a partial wakelock
// during processing, and to have a static startService(Context, Intent)
// method that would launch the service & hand off a wakelock.

super.onCreate();
HandlerThread thread = new HandlerThread("IntentService[" + mName + "]");
thread.start();

mServiceLooper = thread.getLooper();
mServiceHandler = new ServiceHandler(mServiceLooper);
}

@Override
public void onStart(Intent intent, int startId) {
Message msg = mServiceHandler.obtainMessage();
msg.arg1 = startId;
msg.obj = intent;
mServiceHandler.sendMessage(msg);
}

/**
* You should not override this method for your IntentService. Instead,
* override {@link #onHandleIntent}, which the system calls when the IntentService
* receives a start request.
* @see android.app.Service#onStartCommand
*/
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
onStart(intent, startId);
return mRedelivery ? START_REDELIVER_INTENT : START_NOT_STICKY;
}

@Override
public void onDestroy() {
mServiceLooper.quit();
}

/**
* Unless you provide binding for your service, you don't need to implement this
* method, because the default implementation returns null.
* @see android.app.Service#onBind
*/
@Override
public IBinder onBind(Intent intent) {
return null;
}

/**
* This method is invoked on the worker thread with a request to process.
* Only one Intent is processed at a time, but the processing happens on a
* worker thread that runs independently from other application logic.
* So, if this code takes a long time, it will hold up other requests to
* the same IntentService, but it will not hold up anything else.
* When all requests have been handled, the IntentService stops itself,
* so you should not call {@link #stopSelf}.
*
* @param intent The value passed to {@link
* android.content.Context#startService(Intent)}.
*/
@WorkerThread
protected abstract void onHandleIntent(Intent intent);
}

此IntentService的源码就分析完了,嗯,本篇完结。


作者:如果声音不记得
链接:https://juejin.cn/post/7040651369497231374
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Android onSaveInstanceState/onRestoreInstanceState 原来要这么理解

前些天,有位小伙伴兴匆匆地跑过来给我展示一个现象:Activity 里有个EditText,点击该EditText 输入一些文字。此时,转动手机方向,Activity 变成横屏了,而EditText 上的文字依然保留。 问我:为啥EditText上文字能够恢复...
继续阅读 »

前些天,有位小伙伴兴匆匆地跑过来给我展示一个现象:Activity 里有个EditText,点击该EditText 输入一些文字。此时,转动手机方向,Activity 变成横屏了,而EditText 上的文字依然保留。

问我:为啥EditText上文字能够恢复?

我说:你Activity 配置了横竖屏切换时不重建Activity。

他立马给我展示了:Activity 重建的日志。

我说:系统会在重建Activity 的时候恢复整个ViewTree吧。

他又给我展示了:ImageView 横竖屏时没有恢复之前的图像。

我:...

不服输的我开始了默默地研究,于是有了这篇总结以解心中困惑。

通过本篇文章,你将了解到:



1、onSaveInstanceState/onRestoreInstanceState 作用。

2、onSaveInstanceState/onRestoreInstanceState 原理分析

3、onSaveInstanceState/onRestoreInstanceState 触发场景。

4、onSaveInstanceState/onRestoreInstanceState 为啥不能存放大数据?

5、与Jetpack ViewModel 区别。



1、onSaveInstanceState/onRestoreInstanceState 作用


EditText/ImageView 横竖屏地表现



tt0.top-423136.gif


可以看出,从竖屏到横屏再恢复到竖屏,EditText 内容没有变化。而从竖屏到横屏时,ImageView 内容已经丢失了。

都是系统控件,咱们也没有进行其它的额外区别处理,为啥表现不一致呢?

View.java 里有两个方法:


#View.java
protected Parcelable onSaveInstanceState() {...}

protected void onRestoreInstanceState(Parcelable state){...}

官方注释上写的比较清楚了:



1、onSaveInstanceState 是个钩子方法,View.java 的子类可以重写该方法,在方法里面存储一些子类的内部状态,用以下次重建时恢复。

2、onRestoreInstanceState 也是个钩子方法,用以恢复在onSaveInstanceState 里保存的状态。



既然是View的方法,分别查看EditText 与ImageView 对它们的重写情况:


#TextView.java
public Parcelable onSaveInstanceState() {
Parcelable superState = super.onSaveInstanceState();
...
if (freezesText || hasSelection) {
SavedState ss = new SavedState(superState);

if (freezesText) {
if (mText instanceof Spanned) {
final Spannable sp = new SpannableStringBuilder(mText);
...
ss.text = sp;
} else {
//将TextView 内容存储在SavedState里
ss.text = mText.toString();
}
}
...
return ss;
}

//返回存储的对象
return superState;
}

public void onRestoreInstanceState(Parcelable state) {
if (!(state instanceof SavedState)) {
super.onRestoreInstanceState(state);
return;
}

SavedState ss = (SavedState) state;
super.onRestoreInstanceState(ss.getSuperState());

if (ss.text != null) {
//取出TextView 内容,并设置
setText(ss.text);
}
...
}

由此可见,TextView 重写这俩方法,先是在onSaveInstanceState 里存储文本内容,再在onRestoreInstanceState 里恢复文本内容。

而通过查看ImageView 发现它并没有重写这俩方法,当然就不能恢复了。其实这也比较容易理解,毕竟对于ImageView,Bitmap 是它的内容,暂存这个Bitmap 很耗内存。


需要注意的是:想要onSaveInstanceState 被调用,则需要给该控件设置id。因为系统是根据View id将状态存储在SparseArray 里


Activity 横竖屏的处理


现在的问题是:谁调用了View 的onSaveInstanceState/onRestoreInstanceState ? 在前一篇分析过Activity 和View的关系:Android Activity 与View 的互动思考

可知,Activity 通过Window 控制View,我们子类继承自EditText,并重写 onSaveInstanceState/onRestoreInstanceState,然后在横竖屏切换时查看这俩方法的调用栈:



image.png


第一个红色框表示EditText子类里的方法(onSaveInstanceState),而第二个红框表示Activity 子类里重写的方法(onSaveInstanceState)。

由此可知,当横竖屏切换时调用了Activity.onSaveInstanceState(xx) 方法。


#Activity.java
protected void onSaveInstanceState(@NonNull Bundle outState) {
//saveHierarchyState 调用整个ViewTree 的onSaveInstanceState 方法
outState.putBundle(WINDOW_HIERARCHY_TAG, mWindow.saveHierarchyState());
...
//告知生命周期回调方法状态已保存
dispatchActivitySaveInstanceState(outState);
}

同样的对于onRestoreInstanceState:


#Activity.java
protected void onRestoreInstanceState(@NonNull Bundle savedInstanceState) {
if (mWindow != null) {
Bundle windowState = savedInstanceState.getBundle(WINDOW_HIERARCHY_TAG);
if (windowState != null) {
//恢复整个ViewTree 状态
mWindow.restoreHierarchyState(windowState);
}
}
}


当横竖屏切换时,会调用到Activity onSaveInstanceState/onRestoreInstanceState 方法,进而会调用整个ViewTree onSaveInstanceState/onRestoreInstanceState 方法来保存与恢复必要的状态。



Activity 数据保存与恢复


Activity 的onSaveInstanceState/onRestoreInstanceState 方法 除了触发View 的状态保存与恢复外,还可以将Activity 用到的一些重要的数据保存下来,待下次Activity 重建时恢复。

重写两者:


    @Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putString("say", "hello world");
}

@Override
protected void onRestoreInstanceState(Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
String restore = savedInstanceState.getString("say");
Log.d("fish", restore);
}

此时我们注意到onSaveInstanceState 的入参是Bundle 类型,往outState 写入数据,在onRestoreInstanceState 将数据取出,outState/savedInstanceState 必然不为空。


总结 onSaveInstanceState/onRestoreInstanceState 作用



1、保存与恢复View 的状态。

2、保存与恢复Activity 自定义数据。



2、onSaveInstanceState/onRestoreInstanceState 原理分析。


onSaveInstanceState 调用时机


之前 Android Activity 生命周期详解及监听 有详细分析了Activity 各个阶段的调用情况,现在结合生命周期来分析onSaveInstanceState(xx)在生命周期中的哪个阶段被调用的。

调用栈如下:



image.png


看看上图标黄色的方法,这方法很眼熟,在Activity 生命周期中分析过,它是Activity.onStop()方法的调用者:


#ActivityThread.java
private void callActivityOnStop(ActivityClientRecord r, boolean saveState, String reason) {
// Before P onSaveInstanceState was called before onStop, starting with P it's
// called after. Before Honeycomb state was always saved before onPause.
//这句话翻译过来:
//如果目标设备是Android 9之前,那么onSaveInstanceState 在onStop 之前调用
//如果在Android 9 之后,那么onSaveInstanceState 在onStop 之后调用
//Honeycomb 指的是Android 3.0 现在基本可以忽略了。
//r.activity.mFinished 表示Activity 是否即将被销毁
final boolean shouldSaveState = saveState && !r.activity.mFinished && r.state == null
&& !r.isPreHoneycomb();
final boolean isPreP = r.isPreP();
//Android p 之前先于onStop 之前执行
if (shouldSaveState && isPreP) {
callActivityOnSaveInstanceState(r);
}
try {
//最终执行到Activity.onStop()方法
r.activity.performStop(r.mPreserveWindow, reason);
} catch (SuperNotCalledException e) {
...
}
//标记Stop状态
r.setState(ON_STOP);

if (shouldSaveState && !isPreP) {
//调用onSave 保存
callActivityOnSaveInstanceState(r);
}
}

以上注释比较详细了,小结一下:



1、在Android 9之前,onSaveInstanceState 在onStop 之前调用(至于在onPause 之前还是之后调用,这个时机不确定);在Android 9(包含)之后,onSaveInstanceState 在onStop 之后调用。

2、如果Activity 即将被销毁,则onSaveInstanceState 不会被调用。



对于第二句的理解,举个简单例子:



Activity 在前台时,此时按Home键回到桌面,会执行onSaveInstanceState;若是按back键/主动finish,此时虽然会执行到onStop,但是不会执行onSaveInstanceState。



onRestoreInstanceState 调用时机


现在已经弄清楚onSaveInstanceState 调用时机,接着来分析 onRestoreInstanceState 什么时候执行。

调用栈如下:



image.png


黄色部分的方法也很眼熟,它是Activity.onStart()方法的调用者:


    public void handleStartActivity(ActivityClientRecord r,
PendingTransactionActions pendingActions) {
final Activity activity = r.activity;
...
//最终执行到Activity.onStart()
activity.performStart("handleStartActivity");
r.setState(ON_START);
...
if (pendingActions.shouldRestoreInstanceState()) {
if (r.isPersistable()) {
//从持久化存储里恢复数据
if (r.state != null || r.persistentState != null) {
mInstrumentation.callActivityOnRestoreInstanceState(activity, r.state,
r.persistentState);
}
} else if (r.state != null) {
//从内存里恢复数据
mInstrumentation.callActivityOnRestoreInstanceState(activity, r.state);
}
}
...
}

小结:



1、onRestoreInstanceState 在onStart()方法之后执行。

2、pendingActions.shouldRestoreInstanceState() 返回值是执行 onRestoreInstanceState()方法的关键,它是在哪赋值的呢?接下来会分析。

3、r.state 不能为空,毕竟没数据无法恢复。



通过以上分析,结合Activity生命周期,onSaveInstanceState /onRestoreInstanceState 调用时机如下:



image.png


onRestoreInstanceState 与onCreate 参数差异


onCreate参数也是Bundle类型,实际上这个参数就是onSaveInstanceState里保存的Bundle,这个Bundle分别传递给了onCreate和onRestoreInstanceState,而onCreate里的Bundle可能为空(新建非重建的情况下),onRestoreInstanceState 里的Bundle必然不为空。

官方注释也说了在onRestoreInstanceState里处理数据的恢复更灵活。


3、onSaveInstanceState/onRestoreInstanceState 触发场景


横竖屏触发的场景


在前面的分析中,与Activity 生命周期关联可能会让人有种印象:

onSaveInstanceState 调用之后onRestoreInstanceState 就会被调用。

而事实并非如此,举个简单例子:

Activity 处在前台时,此时退回到桌面,onSaveInstanceState 会被执行。而后再让Activity 回到前台,onStart()方法执行后,发现onRestoreInstanceState 并没有被调用。



也就是说onSaveInstanceState/onRestoreInstanceState 的调用不一定是成对出现的。



还记得在分析onRestoreInstanceState 遗留了个问题: pendingActions.shouldRestoreInstanceState() 返回值如何确定的 ?

在横竖屏切换时,onRestoreInstanceState 被调用了,说明 pendingActions.shouldRestoreInstanceState() 在横竖屏切换时返回了true,接着来看看其来龙去脉:


#PendingTransactionActions.java
//判断是否需要执行onRestoreInstanceState 方法
public boolean shouldRestoreInstanceState() {
return mRestoreInstanceState;
}

//设置标记
public void setRestoreInstanceState(boolean restoreInstanceState) {
mRestoreInstanceState = restoreInstanceState;
}

只需要找到setRestoreInstanceState()在何处调用即可。

直接说结论:



ActivityThread.handleLaunchActivity() 里设置了setRestoreInstanceState(true)



而handleLaunchActivity()在两种情况下被调用:



image.png


横竖屏时属于重建 Activity,因此onRestoreInstanceState 能被调用。

而从后台返回到前台,并没有新建Activity也没有重建Activity,因此onRestoreInstanceState 不会被调用。

又引申出另一个问题:为啥新建Activity 时onRestoreInstanceState 没被调用?

答案:因为新建Activity 时,ActivityClientRecord 是全新的对象,它所持有的Bundle state 对象为空,因此不会调用到onRestoreInstanceState。


其它配置项更改的场景


除了横竖屏切换时会重建Activity,还有以下配置项更改会重建Activity:



image.png


当然,还有一些不常涉及的配置项,比如所在地区更改等。


重建Activity 的细节



image.png


当需要重建Activity 时,AMS 发出指令,会执行到 ActivityThread.handleRelaunchActivity()方法。


#ActivityThread.java
public void handleRelaunchActivity(ActivityClientRecord tmp,
PendingTransactionActions pendingActions) {
...
//从Map 里获取缓存的ActivityClientRecord
ActivityClientRecord r = mActivities.get(tmp.token);
...
//将ActivityClientRecord 传递下去
handleRelaunchActivityInner(r, configChanges, tmp.pendingResults, tmp.pendingIntents,
pendingActions, tmp.startsNotResumed, tmp.overrideConfig, "handleRelaunchActivity");
...
}

mActivities 定义如下:


final ArrayMap<IBinder, ActivityClientRecord> mActivities = new ArrayMap<>();

以IBinder 为key,存储ActivityClientRecord。

当新建Activity 时,存入ActivityClientRecord,当销毁Activity 时,移除 ActivityClientRecord。


再来分析handleRelaunchActivityInner():


#ActivityThread.java
private void handleRelaunchActivityInner(...) {
...
if (!r.paused) {
//最终执行到onPause
performPauseActivity(r, false, reason, null /* pendingActions */);
}
if (!r.stopped) {
//最终执行到onStop
callActivityOnStop(r, true /* saveState */, reason);
}
//最终执行到onDestroy
handleDestroyActivity(r.token, false, configChanges, true, reason);
//创建新的Activity 实例
handleLaunchActivity(r, pendingActions, customIntent);
}

通过分析Activity 重建的细节,有以下结论:



1、Activity 重建过程中,先将原来的Activity 进行销毁(从onResume--onStop-->onDestroy 的生命周期)。

2、虽然是不同的Activity 对象,但重建时使用的ActivityClientRecord 却是相同的,而ActivityClientRecord 最终是被ActivityThread 持有,它是全局的。这也是 onSaveInstanceState/onRestoreInstanceState 能够存储与恢复数据的本质原因。



当然也可以通过配置告诉系统在配置项变更时不重建Activity:


<activity android:name=".viewmodel.ViewModelActivity" android:configChanges="orientation|screenSize"></activity>

比如以上配置,当横竖屏切换时,不会重建Activity,而配置项的变更会通过 Activity.onConfigurationChanged()方法回调。


4、onSaveInstanceState/onRestoreInstanceState 为啥不能存放大数据?


onSaveInstanceState/onRestoreInstanceState 的参数都是Bundle 类型,思考一下为什么需要定义为Bundle类型呢?

Android IPC 精讲系列 中有提到过,Android 进程间通信方式大多时候使用的是Binder,而要想自定义数据能够通过Binder传输则需要实现Parcelable 接口,Bundle 实现了Parcelable 接口。


由此我们推测,onSaveInstanceState/onRestoreInstanceState 可能涉及到进程间通信,才会用Bundle 来修饰形参。但之前说的ActivityClientRecord是存储在当前进程的啊,貌似和其它进程没有关联呢?

要分析这个问题,实际上只需要在onSaveInstanceState 存储一个比较大的数据,看看报错时的堆栈。


    protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putString("say", "hello world");
//存储2M 数据
outState.putByteArray("big", new byte[1024*1024*2]);
}

保存2M 的数据,通常来说这是超出了Binder的限制,当调用onSaveInstanceState 时会有报错信息:



image.png


果然还是crash了。

找到 PendingTransactionActions ,它实现了Runnable 接口,在其run方法里:


#PendingTransactionActions.java
public void run() {
try {
//提交给ActivityTaskManagerService 处理,属于进程间通信
//mState 即是onSaveInstanceState 保存的数据
ActivityTaskManager.getService().activityStopped(
mActivity.token, mState, mPersistentState, mDescription);
} catch (RemoteException ex) {
...
}
}

而在ActivityThread.java 里有个方法:


    public void reportStop(PendingTransactionActions pendingActions) {
mH.post(pendingActions.getStopInfo());
}

该方法用于告知系统,咱们的Activity 已经变为Stop状态了,最终会执行到PendingTransactionActions.run()方法。

小结一下:



onSaveInstanceState 存储的数据,在onStop执行后,当前进程需要将Stop状态传递给ATM(ActivityTaskManagerService 运行在system_server进程),因为跨进程传递(Binder)有大小限制,因此onSaveInstanceState 不能传递大量数据。



5、与Jetpack ViewModel 区别


onSaveInstanceState 与 ViewModel 都是将数据放在ActivityClientRecord 的不同字段里。



image.png



1、onSaveInstanceState 用Bundle存储数据便于跨进程传递,而ViewModel 是Object存储数据,不需要跨进程,因此它没有大小限制。

2、onSaveInstanceState 在onStop 之后调用,比较频繁。而ViewModel 存储数据是onDestroy 之后。

3、onSaveInstanceState 可以选择是否持久化数据到文件里(该功能由ATM 实现,存储到xml里),而ViewModel 没有这功能。



更多的区别后续分析 ViewModel 时会提到。


本文基于Android 10.0。


作者:小鱼人爱编程
链接:https://juejin.cn/post/7040819115874844709
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Spring Boot + Redis 解决重复提交问题,还有谁不会??

前言 在实际的开发项目中,一个对外暴露的接口往往会面临很多次请求,我们来解释一下幂等的概念:任意多次执行所产生的影响均与一次执行的影响相同。按照这个含义,最终的含义就是 对数据库的影响只能是一次性的,不能重复处理。如何保证其幂等性,通常有以下手段: 1、数据库...
继续阅读 »

前言


在实际的开发项目中,一个对外暴露的接口往往会面临很多次请求,我们来解释一下幂等的概念:任意多次执行所产生的影响均与一次执行的影响相同。按照这个含义,最终的含义就是 对数据库的影响只能是一次性的,不能重复处理。如何保证其幂等性,通常有以下手段:


1、数据库建立唯一性索引,可以保证最终插入数据库的只有一条数据。


2、token机制,每次接口请求前先获取一个token,然后再下次请求的时候在请求的header体中加上这个token,后台进行验证,如果验证通过删除token,下次请求再次判断token。


3、悲观锁或者乐观锁,悲观锁可以保证每次for update的时候其他sql无法update数据(在数据库引擎是innodb的时候,select的条件必须是唯一索引,防止锁全表)


4、先查询后判断,首先通过查询数据库是否存在数据,如果存在证明已经请求过了,直接拒绝该请求,如果没有存在,就证明是第一次进来,直接放行。


redis 实现自动幂等的原理图:



搭建 Redis 服务 API


1、首先是搭建redis服务器。


2、引入springboot中到的redis的stater,或者Spring封装的jedis也可以,后面主要用到的api就是它的set方法和exists方法,这里我们使用springboot的封装好的redisTemplate。


推荐一个 Spring Boot 基础教程及实战示例:
github.com/javastacks/…


/**
* redis工具类
*/
@Component
public class RedisService {

@Autowired
private RedisTemplate redisTemplate;

/**
* 写入缓存
* @param key
* @param value
* @return
*/
public boolean set(finalString key, Object value) {
boolean result = false;
try {
ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
operations.set(key, value);
result = true;
} catch (Exception e) {
e.printStackTrace();
}
return result;
}

/**
* 写入缓存设置时效时间
* @param key
* @param value
* @return
*/
public boolean setEx(finalString key, Object value, Long expireTime) {
boolean result = false;
try {
ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
operations.set(key, value);
redisTemplate.expire(key, expireTime, TimeUnit.SECONDS);
result = true;
} catch (Exception e) {
e.printStackTrace();
}
return result;
}

/**
* 判断缓存中是否有对应的value
* @param key
* @return
*/
public boolean exists(finalString key) {
return redisTemplate.hasKey(key);
}

/**
* 读取缓存
* @param key
* @return
*/
public Objectget(finalString key) {
Object result = null;
ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
result = operations.get(key);
return result;
}

/**
* 删除对应的value
* @param key
*/
public boolean remove(finalString key) {
if (exists(key)) {
Boolean delete = redisTemplate.delete(key);
return delete;
}
returnfalse;

}

}

自定义注解 AutoIdempotent


自定义一个注解,定义此注解的主要目的是把它添加在需要实现幂等的方法上,凡是某个方法注解了它,都会实现自动幂等。后台利用反射如果扫描到这个注解,就会处理这个方法实现自动幂等,使用元注解ElementType.METHOD表示它只能放在方法上,etentionPolicy.RUNTIME表示它在运行时。


@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface AutoIdempotent {

}

token 创建和检验


token服务接口:我们新建一个接口,创建token服务,里面主要是两个方法,一个用来创建token,一个用来验证token。创建token主要产生的是一个字符串,检验token的话主要是传达request对象,为什么要传request对象呢?主要作用就是获取header里面的token,然后检验,通过抛出的Exception来获取具体的报错信息返回给前端。


publicinterface TokenService {

/**
* 创建token
* @return
*/
public String createToken();

/**
* 检验token
* @param request
* @return
*/
public boolean checkToken(HttpServletRequest request) throws Exception;

}

token的服务实现类:token引用了redis服务,创建token采用随机算法工具类生成随机uuid字符串,然后放入到redis中(为了防止数据的冗余保留,这里设置过期时间为10000秒,具体可视业务而定),如果放入成功,最后返回这个token值。checkToken方法就是从header中获取token到值(如果header中拿不到,就从paramter中获取),如若不存在,直接抛出异常。这个异常信息可以被拦截器捕捉到,然后返回给前端。


@Service
publicclass TokenServiceImpl implements TokenService {

@Autowired
private RedisService redisService;

/**
* 创建token
*
* @return
*/
@Override
public String createToken() {
String str = RandomUtil.randomUUID();
StrBuilder token = new StrBuilder();
try {
token.append(Constant.Redis.TOKEN_PREFIX).append(str);
redisService.setEx(token.toString(), token.toString(),10000L);
boolean notEmpty = StrUtil.isNotEmpty(token.toString());
if (notEmpty) {
return token.toString();
}
}catch (Exception ex){
ex.printStackTrace();
}
returnnull;
}

/**
* 检验token
*
* @param request
* @return
*/
@Override
public boolean checkToken(HttpServletRequest request) throws Exception {

String token = request.getHeader(Constant.TOKEN_NAME);
if (StrUtil.isBlank(token)) {// header中不存在token
token = request.getParameter(Constant.TOKEN_NAME);
if (StrUtil.isBlank(token)) {// parameter中也不存在token
thrownew ServiceException(Constant.ResponseCode.ILLEGAL_ARGUMENT, 100);
}
}

if (!redisService.exists(token)) {
thrownew ServiceException(Constant.ResponseCode.REPETITIVE_OPERATION, 200);
}

boolean remove = redisService.remove(token);
if (!remove) {
thrownew ServiceException(Constant.ResponseCode.REPETITIVE_OPERATION, 200);
}
returntrue;
}
}

拦截器的配置


web配置类,实现WebMvcConfigurerAdapter,主要作用就是添加autoIdempotentInterceptor到配置类中,这样我们到拦截器才能生效,注意使用@Configuration注解,这样在容器启动是时候就可以添加进入context中。


@Configuration
publicclass WebConfiguration extends WebMvcConfigurerAdapter {

@Resource
private AutoIdempotentInterceptor autoIdempotentInterceptor;

/**
* 添加拦截器
* @param registry
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(autoIdempotentInterceptor);
super.addInterceptors(registry);
}
}

拦截处理器:主要的功能是拦截扫描到AutoIdempotent到注解到方法,然后调用tokenService的checkToken()方法校验token是否正确,如果捕捉到异常就将异常信息渲染成json返回给前端。


/**
* 拦截器
*/
@Component
publicclass AutoIdempotentInterceptor implements HandlerInterceptor {

@Autowired
private TokenService tokenService;

/**
* 预处理
*
* @param request
* @param response
* @param handler
* @return
* @throws Exception
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

if (!(handler instanceof HandlerMethod)) {
returntrue;
}
HandlerMethod handlerMethod = (HandlerMethod) handler;
Method method = handlerMethod.getMethod();
//被ApiIdempotment标记的扫描
AutoIdempotent methodAnnotation = method.getAnnotation(AutoIdempotent.class);
if (methodAnnotation != null) {
try {
return tokenService.checkToken(request);// 幂等性校验, 校验通过则放行, 校验失败则抛出异常, 并通过统一异常处理返回友好提示
}catch (Exception ex){
ResultVo failedResult = ResultVo.getFailedResult(101, ex.getMessage());
writeReturnJson(response, JSONUtil.toJsonStr(failedResult));
throw ex;
}
}
//必须返回true,否则会被拦截一切请求
returntrue;
}

@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {

}

@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {

}

/**
* 返回的json值
* @param response
* @param json
* @throws Exception
*/
private void writeReturnJson(HttpServletResponse response, String json) throws Exception{
PrintWriter writer = null;
response.setCharacterEncoding("UTF-8");
response.setContentType("text/html; charset=utf-8");
try {
writer = response.getWriter();
writer.print(json);

} catch (IOException e) {
} finally {
if (writer != null)
writer.close();
}
}

}

测试用例


模拟业务请求类,首先我们需要通过/get/token路径通过getToken()方法去获取具体的token,然后我们调用testIdempotence方法,这个方法上面注解了@AutoIdempotent,拦截器会拦截所有的请求,当判断到处理的方法上面有该注解的时候,就会调用TokenService中的checkToken()方法,如果捕获到异常会将异常抛出调用者,下面我们来模拟请求一下:


@RestController
publicclass BusinessController {

@Resource
private TokenService tokenService;

@Resource
private TestService testService;

@PostMapping("/get/token")
public String getToken(){
String token = tokenService.createToken();
if (StrUtil.isNotEmpty(token)) {
ResultVo resultVo = new ResultVo();
resultVo.setCode(Constant.code_success);
resultVo.setMessage(Constant.SUCCESS);
resultVo.setData(token);
return JSONUtil.toJsonStr(resultVo);
}
return StrUtil.EMPTY;
}

@AutoIdempotent
@PostMapping("/test/Idempotence")
public String testIdempotence() {
String businessResult = testService.testIdempotence();
if (StrUtil.isNotEmpty(businessResult)) {
ResultVo successResult = ResultVo.getSuccessResult(businessResult);
return JSONUtil.toJsonStr(successResult);
}
return StrUtil.EMPTY;
}
}

使用postman请求,首先访问get/token路径获取到具体到token:



利用获取到到token,然后放到具体请求到header中,可以看到第一次请求成功,接着我们请求第二次:



第二次请求,返回到是重复性操作,可见重复性验证通过,再多次请求到时候我们只让其第一次成功,第二次就是失败:



总结


本篇博客介绍了使用springboot和拦截器、redis来优雅的实现接口幂等,对于幂等在实际的开发过程中是十分重要的,因为一个接口可能会被无数的客户端调用,如何保证其不影响后台的业务处理,如何保证其只影响数据一次是非常重要的,它可以防止产生脏数据或者乱数据,也可以减少并发量,实乃十分有益的一件事。而传统的做法是每次判断数据,这种做法不够智能化和自动化,比较麻烦。而今天的这种自动化处理也可以提升程序的伸缩性。


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

FlutterWeb初体验

FlutterWeb初体验 [toc] 背景 因为最近业务需求的变动,在APP的某一部分页面会经常性发生变动,一般情况下来说,这种不稳定的页面不应该由原生来承担,修改发版的成本太大了,最合理的做法是由H5来承担,由原生提供必要的bridge来调用原生方法,但是...
继续阅读 »

FlutterWeb初体验


[toc]


背景


因为最近业务需求的变动,在APP的某一部分页面会经常性发生变动,一般情况下来说,这种不稳定的页面不应该由原生来承担,修改发版的成本太大了,最合理的做法是由H5来承担,由原生提供必要的bridge来调用原生方法,但是由于种种历史债务,还是没有如此实现,经历了痛苦的发版以及等待审核后,我在想flutterWeb是不是可以解决这个问题?


想法


页面进入流程


screenshot-20211210-211936.png


项目架构想法


整个项目转为支持FlutterWeb


整个项目转为flutterweb,可以打包成web文件直接部署在服务器,而app依旧打包成apk和ipa,但是在路由监听处留下开关,当有页面需要紧急修复或者紧急更改的情况下,下发配置,跳转的时候根据路由配置跳转WebView或者原生页面。


抽离出某个模块,单个模块支持web


抽离出一个module,由一个壳工程引用,这个壳工程用于把该module打包成web;同时该模块依然被app工程引用,作为一个功能模块,而部署的时候只部署了这个模块的web产物。


因为目前app集成了一定数量的原生端的第三方sdk,直接支持flutterweb工程量较大,所以先尝试第二个方法。


壳工程结构图

1924616-18f5d8ee85f0f330.png


其中


flutter_libs 是基础的lib库,封装了基础的网络请求,持久化存储,状态管理等基础,壳工程和app工程也会引用


ly_income是功能module,也是我们主要开发需求的模块,它会被壳工程引用作为web的打包内容,也会被app工程引用作为原生的页面展示。


实践


打包问题处理


因为是新建的项目工程,打包成flutterWeb并不会有那么多障碍。


开启web支持


执行 flutter config查看目前的配置信息,如果看到


Settings:
enable-web: true
enable-macos-desktop: true

那就是已经开启了,如果还没,可以使用flutter config --enable-web开启配置


打包模式选择

而flutterWeb打包也有两种模式可以选择:html模式和CanvasKit模式


它们两者各自的特别是:


html模式


flutter build web --web-renderer html


当我们采用html渲染模式时,flutter会采用HTML的custom element,CSS,Canvas和SVG来渲染UI元素


优点是:体积比较小


缺点是:渲染性能比较差,跨端一致性可能不受保障



CanvasKit模式


flutter build web --web-renderer canvaskit


当我们采用canvaskit渲染模式时,flutter将 Skia 编译成 WebAssembly 格式,并使用 WebGL 渲染。应用在移动和桌面端保持一致,有更好的性能,以及降低不同浏览器渲染效果不一致的风险。但是应用的大小会增加大约 2MB。


优点是:跨端一致性受保障,渲染性能更好


缺点是:体积比较大,load页面时间会更久



跨域问题处理


之前一直是做app开发,跨域这个词只听过,还没见识过。


了解跨域

跨域是指浏览器的不执行其他网站脚本的,由于浏览器的同源策略造成,是对JavaScript的一种安全限制


说白点理解,当你通过浏览器向其他服务器发送请求时,不是服务器不响应,而是服务器返回的结果被浏览器限制了。


而什么是同源策略的同源



同源指的是协议、域名、端口 都要保持一致


http://www.123.com:8080/index.html (http协议,http://www.123.com 域名、8080 端口 ,只要这三个有一项不一样的都是跨域,这里不一一举例子)


http://www.123.com:8080/matsh.html(…


http://www.123.com:8081/matsh.html(…


注意:localhost 和127.0.0.1 虽然都指向本机,但也属于跨域。



而跨域的解决方法也暂时不适用我:



  1. JSONP方式 (我们项目的请求都是post请求)

  2. 反向代理,ngixn (ngixn小白)

  3. 配置浏览器 (好像不太适用,应该,大概,也许,可能,或许)

  4. 项目配置跨域 (因为只是尝试项目,需要后台和运维支持的话,需要跨部门沟通,太麻烦了)


摘自网络 什么是跨域,侵删歉


常规做法



  1. 本地调试的时候修改代码,支持跨域请求


    在上图红框中添加代码--disable-web-security




1924616-e444ef62f7776b1e.png


1924616-fddf6a72c3a43965.png


然后删除以下两个文件,执行flutter doctor生成新的一份,再尝试run起来,你会发现浏览器已经支持跨域了,你可以很开心地在浏览器run接口了。但是仅支持本地调试!!!



  1. ngixn做转发,但是这个... 我没有怎么用过ngixn,而且需要在周末做完调研给出可行性报告,也没有时间去学习,先搁置,后续再拿起来看看

  2. 后端和运维同学帮忙调试跨域,因为是尝试而已,没有必要用到其他部门的资源,先搁置,后续如果可实际应用,再要求他们协助。


骚操作


保命前提:



  1. 这个其实就是配置转发的做法,但是这块我没什么经验,时间紧任务重所以就先这么尝试做了

  2. 其实这个就是类似于openfeign之类的想法,但是我并不知道后台开发的FeignClient,而且也有点危险,还是调用开发的接口更加稳妥

  3. 纯个人做法,肯定还会有更好的方法,但是这个是我当时最快的达成方案,勿喷。



如果说我要求不了后台服务做跨域,那可不可以我自己要求我自己做跨域呢?


比如:


我请求我的服务器,我的服务器再去请求后台服务,我访问后台服务跨域而已,我的服务器访问后台服务可不跨域,我的服务器跨域又咋样,自己的东西随便拿捏。



  1. 新建一个springboot项目

  2. 搭建一个controller,参数是url全路径以及参数json字符串,配置好header之后请求后台服务并返回信息


@CrossOrigin
@RestController
@RequestMapping("api/home")
public class GatewayController {

@PostMapping("/gatewayApi")
public String gatewayApi(@RequestParam("url") String url, @RequestParam("params") String json) {
try {
JSONObject jsonObject = JSONObject.parseObject(json);
JSONObject result = doPost(jsonObject, url);
if (result != null) {
return result.toString();
} else {
return errMsg().toString();
}
} catch (Exception e) {
return errMsg(e.getMessage()).toString();
}
}
}


  1. 配置跨域信息


@SpringBootConfiguration
public class WebGlobalConfig {

@Bean
public CorsFilter corsFilter() {

//创建CorsConfiguration对象后添加配置
CorsConfiguration config = new CorsConfiguration();
//设置放行哪些原始域
config.addAllowedOriginPattern("*");
//放行哪些原始请求头部信息
config.addAllowedHeader("*");
//暴露哪些头部信息
config.addExposedHeader("*");
//放行哪些请求方式
config.addAllowedMethod("GET"); //get
config.addAllowedMethod("PUT"); //put
config.addAllowedMethod("POST"); //post
config.addAllowedMethod("DELETE"); //delete
//corsConfig.addAllowedMethod("*"); //放行全部请求

//是否发送Cookie
config.setAllowCredentials(true);

//2. 添加映射路径
UrlBasedCorsConfigurationSource corsConfigurationSource =
new UrlBasedCorsConfigurationSource();
corsConfigurationSource.registerCorsConfiguration("/**", config);
//返回CorsFilter
return new CorsFilter(corsConfigurationSource);
}
}


  1. 打包后部署到服务器

  2. module里的接口不再请求后台服务,而是请求我的服务器,因为只是转发,所以没有改动任何数据结构,只需要请求地址改动下

  3. 可以跨域了


与原生交互问题


设想中web的页面可以有三种方式:



  1. 集成在app里面作为原生页面,这个的交互没什么好说的。

  2. 打包成web项目,通过webview进行加载,那需要额外处理持久化信息的获取与写入,以及与原生页面的跳转交互

  3. 只有url,测试人员可以通过url路径传参之类的切换账号,方便测试


针对业务来说,页面的加载流程应该是这样的:


screenshot-20211210-211914.png


不同场景做不同的操作

原生

通过持久化工具类获取用户基础信息,然后读取接口判断身份,根据身份去做不同展示,点击跳转时间也是直接的通过路由跳转


通过webview加载

通过js交互,从原生模块拿到用户基础信息(存疑,是否直接读接口?,这样避免对原生api的依赖,如果有需求修改的话可以尽量不依赖),然后读取接口判断身份,根据身份不同去做不同展示,如果是dialog之类的交互可以直接实现,如果是跳转页面之类的,可以通过js交互进行原生操作


通过url加载的

通过url的参数串获取到对应的用户id,读取接口获取用户信息,其他操作如上,但是页面没有跳转之类的交互


实现

从链接上面获取参数

比如url为:```xxx.yyy.zzz/value


要如何拿到value值?


因为项目里刚好使用了Get做状态管理,而刚好Get已经实现了这一块,世间上的事情就是这么刚好。(好像navigator2已经支持这个了,不过还没仔细看过)




  1. 配置路由表



    class RouterConf {
    static const String appIncomeArgs = '/app/inCome/:fromApp';
    static const String appIncome = '/app/inCome/';
    static List<GetPage> _getPages = [];
    static List<GetPage> get getPages {
    _getPages = [
    GetPage(name: appIncomeArgs, page: () => const StoreKeeperInComePage()),
    ];
    return _getPages;
    }
    }

    这里appIncome配置了两个路由名


    但是实际使用时以没带**:fromApp为准的,fromApp我觉得可以理解成一个占位符,也就是fromApp=value**




  2. 获取对应的value


    在base类里面定义一个bool值,在init的回调里面去做获取操作


      bool ifFromApp = false;
    Map<String, String?> _args = Get.parameters;
    if (_args.isNotEmpty && _args.containsKey('fromApp')) {
    String? _fromAppFlag = Get.parameters['fromApp'];
    if ((_fromAppFlag?.isNotEmpty ?? false)) {
    ifFromApp = _fromAppFlag == "1";
    }
    }



根据不同情景做操作

以在webview打开为例,在页面加载时通过js交互获取用户信息,拿到用户信息后替换cache类里缓存的id,token之类的,因为拦截器里面会读取这些值用于拼接通用参数


  @override
void onReady() {
if (ifFromApp) {
initUserInfo();
js.context['getUserInfoCallback'] = getUserInfoCallback;
}else{
_loadInterface();
}

super.onReady();
}

void initUserInfo() {
js.context.callMethod("callFlutterMethod", [
json.encode({
"api": "getUserInfo",
"data": {
"name": 'getUserInfo',
"needCallback": true,
"needToken": true,
"callbackName": 'getUserInfoCallback',
"callbackArgs": 'info'
},
})
]);
}

void getUserInfoCallback(msg, info) {
Map<String, dynamic> _args = {};
if (info != null) {
if (info is String) {
_args = jsonDecode(info);
} else {
_args = info;
}
if (_args.containsKey("info")) {
dynamic _realInfo = _args['info'];
if (_realInfo is String) {
_args = jsonDecode(_realInfo);
} else {
_args = _realInfo;
}
}
if (_args.containsKey('name')) {
debugPrint(' _args[name]---------${_args['name']}');
CacheManager.instance.oName = _args['name'];
}
if (_args.containsKey('uId')) {
debugPrint(' _args[uId]---------${_args['uId']}');

CacheManager.instance.userId = _args['uId'];
}
if (_args.containsKey('oId')) {
debugPrint(' _args[oId]---------${_args['oId']}');
CacheManager.instance.userOId = _args['oId'];
}
if (_args.containsKey('token')) {
debugPrint(' _args[token]---------${_args['token']}');

CacheManager.instance.userToken = _args['token'];
}
if (_args.containsKey('headImg')) {
debugPrint(' _args[headImg]---------${_args['headImg']}');
CacheManager.instance.headImgUrl = _args['headImg'];
}
state.userName = CacheManager.instance.oName;
state.userHeaderImg = CacheManager.instance.headImgUrl;
_loadInterface();
}
}

每次都做这个判断是真的恶心,应该把这些东西抽离出来,通过中间件去实现,避免页面上耦合了这个判断。


接下去就是正常的请求接口渲染页面的流程了。


与原生的交互

这里借鉴的是这位大佬的文章 flutterweb与flutter的交互 侵删歉


唯一需要注意的就是在web项目里面增加一个js


1924616-af650f09d9300f88.png
在app里面也要做一点操作:


class NativeBridge implements JavascriptChannel {
BuildContext context; //来源于当前widget, 便于操作UI
Future<WebViewController> _controller; //当前webView 的 controller

NativeBridge(this.context, this._controller);

// api 与具体函数的映射表,可通过 _functions[key](data) 调用函数
get _functions => <String, Function>{
"getUserInfo": _getUserInfo,
"incomeDetail": _incomeDetail,
"incomeHistory": _incomeHistory,
};

@override
String get name =>
"nativeBridge"; // js 通过 nativeBridge.postMessage(msg); 调用flutter

// 处理js请求
@override
get onMessageReceived => (msg) async {
// 将收到的string数据转为json
Map<String, dynamic> message = json.decode(msg.message);
// 异步是因为有些api函数实现可能为异步,如inputText,等待UI相应
// 根据 api 字段,调用具体函数
final data = await _functions[message["api"]](message["data"]);
};

//拿token
_getUserInfo(data) async {
handlerCallback(data);
} //拿token

_incomeDetail(data) async {
Get.toNamed(RouterConf.OLD_STOREKEEPER_INCOME_LIST);
}

_incomeHistory(data) async {
Get.toNamed(RouterConf.STORE_KEEPER_INCOME_HISTORY);
}

handlerCallback(data) async {
LoginModel? _login = await UserManager.getLoginModel();
UserInfoModel? _user = await UserManager.getUserInfo();
String? _name = _user?.resultData?.organization?.organizationName;
String? _uId = _user?.resultData?.user?.userId?.toString() ?? "";
String? _oId =
_user?.resultData?.organization?.organizationId?.toString() ?? "";
String? _token = _login?.resultData?.xAUTHTOKEN;
String? _img = _user?.resultData?.user?.portraitUrl;
_img = ImgSize.getImgUrlThumbnail(_img);
Map<String, dynamic> _infos = {
"name": _name,
"uId": _uId,
"oId": _oId,
"token": _token,
"headImg": _img,
};

if (data['needCallback']) {
var args = data['callbackArgs'];
if (data['needToken']) {
args = "'${data['callbackArgs']}','${jsonEncode(_infos)}'";
}
doCallback(data['callbackName'], args);
}
}

doCallback(name, args) {
_controller.then((value) => value.evaluateJavascript("$name($args)"));
}
}

在webview里面设置channels:


 javascriptChannels: <JavascriptChannel>[
NativeBridge(context, widget.controller!.future)
].toSet(),

结尾


目前来说好像这个方案是可行的,把一个app页面通过网页跑起来确实是挺爽的,但是慢也是真的慢,


也可能因为我的服务器是丐版中的丐版,加载起来是真的慢:


1924616-5860a92dde710996.png


1924616-71b2de0857dcbc1e.png


但是挺好玩的,虽然代码很烂,但是开心就是了。


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

一步一步完成Flutter应用开发-掘金App文章详情, 悬浮,标题动画

这边文章主要将掘金app的文章详情界面的内容构造和效果的实现,也是完结篇,或者有兴趣的同学们可以谈论想要去实现哪个页面的或者功能都可以谈论起来。一起进步。一个人终究没有一群人会走得远。 标题部分 看了一下掘金app文章详情的效果,我的思路是自定义一个appba...
继续阅读 »

这边文章主要将掘金app的文章详情界面的内容构造和效果的实现,也是完结篇,或者有兴趣的同学们可以谈论想要去实现哪个页面的或者功能都可以谈论起来。一起进步。一个人终究没有一群人会走得远。


标题部分


看了一下掘金app文章详情的效果,我的思路是自定义一个appbar然后,左半部分是一个返回按钮,右部分是点击弹出分享的悬浮窗口,中间部分根据内容列表的滑动进行改变,大体思路就是通过pageView构建中间部分,禁止手势滑动,使用主动触发滑动效果,触发机制是内容滑动改变的距离
效果如下:


tutieshi_640x1343_4s.gif


import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';

class DetailPage extends StatefulWidget {
DetailPage({Key key}) : super(key: key);

@override
_DetailPageState createState() => _DetailPageState();
}

class _DetailPageState extends State<DetailPage> {
PageController controller = new PageController();
@override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: [
Container(
width: Get.width,
height: 80,
decoration: BoxDecoration(color: Colors.white),
padding: EdgeInsets.only(
top: Get.context.mediaQueryPadding.top, left: 10, right: 10),
child: Row(
children: [
InkWell(
child: Icon(
CupertinoIcons.back,
color: Color.fromRGBO(38, 38, 40, 1),
),
onTap: () {
print('点击了返回');
Get.back();
},
),
Expanded(
child: PageView.builder(
itemBuilder: (context, index) {
if (index == 0) {
return Container(
alignment: Alignment.center,
child: Text('一步一步完成Flutter应用开发-掘金文章详情页面'),
);
}
return Container(
child: Row(
children: [
Container(
height: 20,
width: 20,
decoration: BoxDecoration(
color: Colors.red,
borderRadius: BorderRadius.circular(10)),
)
],
),
);
},
itemCount: 2,
scrollDirection: Axis.vertical,
physics: NeverScrollableScrollPhysics(),
controller: controller,
)),
InkWell(
child: Icon(
Icons.list,
color: Color.fromRGBO(38, 38, 40, 1),
),
onTap: () {
controller.animateTo(40,
duration: Duration(milliseconds: 500),
curve: Curves.ease);
},
),
],
),
)
],
));
}
}

基于这个思想接下来完成下面的内容


内容部分的构建


这部分想要知道掘金内容的返回格式是什么,是markdown内容或者是html内容,如果是html内容传送门,可以参考一下。
效果:


tutieshi_640x1343_5s.gif
这块主要是通过markdown形式展示详情内容引入


flutter_markdown: ^0.5.2

在上述代码中加入详情内容代码


ScrollController _scrollController = new ScrollController();

@override
void initState() {
super.initState();
_scrollController
..addListener(() {
setState(() {
if (_scrollController.offset > 88 && _scrollController.offset < 100) {
controller.animateTo(30,
duration: Duration(milliseconds: 500), curve: Curves.ease);
} else if (_scrollController.offset <= 0) {
controller.animateTo(0,
duration: Duration(milliseconds: 500), curve: Curves.ease);
}
});
});
}

@override
Widget build(BuildContext context) {
...省略上述代码
//_markdownData为内容常量
Expanded(
child: Markdown(
data: _markdownData,
controller: _scrollController,
))
}


悬浮弹窗


使用getX的Get.dialog进行展示悬浮弹窗
效果:


tutieshi_640x1343_4s.gif


代码如下:


renderItem(title) {
return Column(
children: [
Container(
height: 40,
width: 40,
margin: EdgeInsets.only(top: 10),
decoration: BoxDecoration(
color: Colors.red, borderRadius: BorderRadius.circular(20)),
),
Padding(padding: EdgeInsets.only(top: 8)),
Material(
child: Text(title),
)
],
);
}

//调用方法
Get.dialog(
UnconstrainedBox(
alignment: Alignment.bottomCenter,
child: Container(
width: Get.width,
height: Get.height * 0.6,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(20),
topRight: Radius.circular(20))),
child: Column(
children: [
Padding(padding: EdgeInsets.only(top: 40)),
Row(
mainAxisAlignment:
MainAxisAlignment.spaceAround,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
renderItem('卡片分享'),
renderItem('微信分享'),
renderItem('朋友圈分享'),
renderItem('微博分享'),
],
),
Padding(padding: EdgeInsets.only(top: 20)),
Divider(
height: 2,
color: Colors.grey,
),
Padding(padding: EdgeInsets.only(top: 20)),
Row(
mainAxisAlignment:
MainAxisAlignment.spaceAround,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
renderItem('卡片分享'),
renderItem('微信分享'),
renderItem('朋友圈分享'),
renderItem('微博分享'),
],
),
Row(
mainAxisAlignment:
MainAxisAlignment.spaceAround,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
renderItem('卡片分享'),
renderItem('微信分享'),
renderItem('朋友圈分享'),
renderItem('微信分享'),
],
),
Expanded(child: Container()),
GestureDetector(
onTap: () {
Get.back();
},
child: Container(
margin: EdgeInsets.only(
bottom: Get
.context.mediaQueryPadding.bottom),
width: Get.width,
height: 40,
alignment: Alignment.center,
child: Material(
child: Text(
'取消',
),
),
),
),
],
)),
),
useRootNavigator: false,
useSafeArea: false);

over ~~~~


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

Android论网络加载框架(Android-async-http,afinal,xUtils,Volley,okhttp,Retrofit)的特点和优缺点

一:HTTP,TCP,UDP,Socket简要介绍 1、TCP TCP简要介绍 TCP是面向连接的、传输可靠(保证数据正确性且保证数据顺序)、用于传输大量数据(流模式)、速度慢,建立连接需要开销较多(时间,系统资源)。 TCP三次握手 建立一个TCP连接时,需...
继续阅读 »

一:HTTP,TCP,UDP,Socket简要介绍


1、TCP


TCP简要介绍


TCP是面向连接的、传输可靠(保证数据正确性且保证数据顺序)、用于传输大量数据(流模式)、速度慢,建立连接需要开销较多(时间,系统资源)。


TCP三次握手


建立一个TCP连接时,需要客户端和服务器总共发送3个包。


  三次握手的目的是连接服务器指定端口,建立TCP连接,并同步连接双方的序列号和确认号并交换 TCP 窗口大小信息.在 Socket 编程中,客户端执行connect()时。将触发三次握手。


首先了解一下几个标志,SYN(synchronous),同步标志,ACK (Acknowledgement),即确认标志,seq是Sequence Number(序列号)。


  第一次握手:客户端发送一个TCP的SYN标志位置1的包指明客户打算连接的服务器的端口,以及初始序号X,保存在包头的序列号(Sequence Number)字段里。


  第二次握手:服务器发回确认包(ACK)应答。即SYN标志位和ACK标志位均为1同时,将确认序号(Acknowledgement Number)设置为客户的序列号加1以,即X+1。


  第三次握手:客户端再次发送确认包(ACK) SYN标志位为0,ACK标志位为1。并且把服务器发来ACK的序号字段+1,放在确定字段中发送给对方.并且在数据段放写序列号的+1。


tcp四次挥手


TCP的连接的拆除需要发送四个包,因此称为四次挥手(four-way handshake)。


为什么连接的时候是三次握手,关闭的时候却是四次挥手?


因为当Server端收到Client端的SYN连接请求报文后,可以直接发送SYN+ACK报文。其中ACK报文是用来应答的,SYN报文是用来 同步的。但是关闭连接时,当Server端收到FIN报文时,很可能并不会立即关闭SOCKET,所以只能先回复一个ACK报文,告诉Client端,” 你发的FIN报文我收到了”。只有等到我Server端所有的报文都发送完了,我才能发送FIN报文,因此不能一起发送。故需要四步握手。


TCP的优缺点


优点:


可靠,稳定 TCP的可靠体现在TCP在传递数据之前,会有三次握手来建立连接,而且在数据传递时,有确认、窗口、重传、拥塞控制机制,在数据传完后,还会断开连接用来节约系统资源。


缺点:


慢,效率低,占用系统资源高,易被攻击 TCP在传递数据之前,要先建连接,这会消耗时间,而且在数据传递时,确认机制、重传机制、拥塞控制机制等都会消耗大量的时间,而且要在每台设备上维护所有的传输连接,事实上,每个连接都会占用系统的CPU、内存等硬件资源。 而且,因为TCP有确认机制、三次握手机制,这些也导致TCP容易被人利用,实现DOS、DDOS、CC等攻击。


2、UDP:


面向非连接、传输不可靠、用于传输少量数据(数据包模式)、速度快。


UDP的优点: 快,比TCP稍安全 UDP没有TCP的握手、确认、窗口、重传、拥塞控制等机制,UDP是一个无状态的传输协议,所以它在传递数据时非常快。没有TCP的这些机制,UDP较TCP被攻击者利用的漏洞就要少一些。但UDP也是无法避免攻击的,比如:UDP Flood攻击…… UDP的


UDP缺点:不可靠,不稳定,因为UDP没有TCP那些可靠的机制,在数据传递时,如果网络质量不好,就会很容易丢包。


3、HTTP


(1) HTTP简要介绍


HTTP协议即超文本传送协议(HypertextTransfer Protocol ),是Web联网的基础,也是手机联网常用的协议之一,HTTP协议是建立在TCP协议之上的一种应用。


(2) HTTP特点


  HTTP连接最显著的特点是客户端发送的每次请求都需要服务器回送响应,在请求结束后,会主动释放连接,从建立连接到关闭连接的过程称为“一次连接”,因此HTTP连接是一种“短连接”



  • 在HTTP 1.0中,客户端的每次请求都要求建立一次单独的连接,在处理完本次请求后,就自动释放连接。

  • 在HTTP 1.1中则可以在一次连接中处理多个请求,并且多个请求可以重叠进行,不需要等待一个请求结束后再发送下一个请求。 


HTTP是基于客户端/服务端(C/S)的架构模型


  客户端发送一个HTTP请求到服务器的请求消息包括以下格式:请求行(request line)、请求头部(header)、空行和请求数据四个部分组成,


HTTP响应也由四个部分组成,分别是:状态行、消息报头、空行和响应正文。


(3) HTTP优缺点


优点:



  • 基于应用级的接口使用方便

  • 程序员开发水平要求不高,容错性强


缺点:



  • 传输速度慢,数据包大(Http协议中包含辅助应用信息)

  • 如实时交互,服务器性能压力大。

  • 数据传输安全性差


4、Socket


(1) Socket简要介绍


网络上的两个程序通过一个双向的通信连接实现数据的交换,这个连接的一端称为一个socket。


建立网络通信连接至少要一对端口号(socket)。socket本质是编程接口(API),对TCP/IP的封装,TCP/IP也要提供可供程序员做网络开发所用的接口,这就是Socket编程接口;HTTP是轿车,提供了封装或者显示数据的具体形式;Socket是发动机,提供了网络通信的能力。


(2) Socket优缺点


优点:



  • 传输数据为字节级,传输数据可自定义,数据量小(对于手机应用讲:费用低)

  • 传输数据时间短,性能高

  • 适合于客户端和服务器端之间信息实时交互

  • 可以加密,数据安全性强


Socket缺点:



  • 需对传输的数据进行解析,转化成应用级的数据

  • 对开发人员的开发水平要求高

  • 相对于Http协议传输,增加了开发量


5、TCP HTTP UDP三者的关系:



  • TCP/IP是个协议组,可分为四个层次:网络接口层、网络层、传输层和应用层。

  • 在网络层有:IP协议、ICMP协议、ARP协议、RARP协议和BOOTP协议。

  • 在传输层中有:TCP协议与UDP协议。

  • 在应用层有:FTP、HTTP、TELNET、SMTP、DNS等协议。

  • 因此,HTTP本身就是一个协议,是从Web服务器传输超文本到本地浏览器的传送协议。


二:HttpURLConnection和httpclient


在Android开发中网络请求是最常用的操作之一, Android SDK中对HTTP(超文本传输协议)也提供了很好的支持,这里包括两种接口:



  • 标准Java接口(java.NET) —-HttpURLConnection,可以实现简单的基于URL请求、响应功能;

  • Apache接口(org.appache.http)—-HttpClient,使用起来更方面更强大。


但在android API23的SDK中Google将HttpClient移除了。Google建议使用httpURLconnection进行网络访问操作。


HttpURLconnection是基于http协议的,支持get,post,put,delete等各种请求方式,最常用的就是get和post,下面针对这两种请求方式进行讲解。


1、HttpURLConnection


在JDK的java.net包中已经提供了访问HTTP协议的基本功能的类:HttpURLConnection。


HttpURLConnection是Java的标准类,它继承自URLConnection,可用于向指定网站发送GET请求、POST请求。


2、httpclient


HttpClient 是Apache Jakarta Common 下的子项目,可以用来提供高效的、最新的、功能丰富的支持 HTTP 协议的客户端编程工具包,并且它支持 HTTP 协议最新的版本和建议。


三:android常用网络框架


1、Android-async-http


Android-async-http简要介绍


Android-async-http 是一个强大的网络请求库,这个网络请求库是基于 Apache HttpClient 库之上的一个异步网络请求处理库,网络处理均基于 Android 的非 UI 线程,通过回调方法处理请求结果。可惜的是 Android 6.0 (api 23) SDK,不再提供 org.apache.http.* (只保留几个类)。


Android-async-http优点


优点:



  • 在匿名回调中处理请求结果

  • 在 UI 线程外进行 http 请求

  • 文件断点上传

  • 智能重试

  • 默认 gzip 压缩

  • 支持解析成 Json 格式

  • 可将 Cookies 持久化到 SharedPreference


2、afinal


afinal简要介绍


afinal是一个开源的android的orm和ioc应用开发框架,其特点是小巧灵活,代码入侵量少。在android应用开发中,通过afinal的ioc框架,诸如ui绑定,事件绑定,通过注解可以自动绑定。通过afinal的orm框架,无需任何配置信息,一行代码就可以对android的sqlite数据库进行增删改查操作。同时,afinal内嵌了finalHttp等简单易用的工具,可以轻松的对http请求进行操作。


afinal主要组件



  • FinalHttp:用于请求http数据,直接ajax方式请求,文件上传, 断点续传下载文件等

  • FinalBitmap:用于显示bitmap图片,无需考虑线程并发和oom等问题。

  • FinalActivity:完全可以通过注解方式绑定控件和事件,无需编写代码。

  • FinalDb:android中sqlite的orm框架,一行代码搞定增删改查。


afinal特点



  • 设计简单小巧灵活

  • orm零配置,但可以配置,可以通过灵活的注解配置达到更加强大的功能

  • 数据库查询支持DbModel,可以轻松的进行各种复杂的查询

  • android的ui和事件绑定完全通过注解的方式,无需编写一行代码

  • http请求支持ajax方式请求

  • 体积小(不到100KB),不依赖第三方jar包


afinal优缺点


优点
android中的orm框架,一行代码就可以进行增删改查。支持一对多,多对一等查询。


缺点
目前暂时不支持复合主键,并且对SQL语句的支持也非常有限,一些比较复杂的业务逻辑实现非常麻烦!


3、xUtils


xUtils简要介绍


xUtils是基于Afinal开发的目前功能比较完善的一个Android开源框架,最近又发布了xUtil3.0,在增加新功能的同时又提高了框架的性能。


下面来看看官方(github.com/wyouflf/xUt…)对xUtils3的介绍:



  • xUtils包含了很多实用的android工具;

  • xUtils支持超大文件(超过2G)上传,更全面的http请求协议支持(11种谓词),拥有更加灵活的ORM,更多的事件注解支持且不受混淆影响;

  • xUitls最低兼容android 2.2 (api level 8)!

  • xUtils3变化较多所以建立了新的项目不在旧版(github.com/wyouflf/xUtils)上继续维护, 相对于旧版本:

  • HTTP实现替换HttpClient为UrlConnection, 自动解析回调泛型, 更安全的断点续传策略;

  • 支持标准的Cookie策略, 区分domain, path;

  • 事件注解去除不常用的功能, 提高性能;

  • 数据库api简化提高性能, 达到和greenDao一致的性能;

  • 图片绑定支持gif(受系统兼容性影响, 部分gif文件只能静态显示), webp; 支持圆角, 圆形, 方形等裁剪, 支持自动旋转。


xUtils主要组件


目前xUtils主要有四大模块:
ViewUtils模块:



  • android中的ioc(控制倒转)框架,完全注解方式就可以进行UI,资源和事件绑定;

  • 新的事件绑定方式,使用混淆工具混淆后仍可正常工作;

  • 目前支持常用的20种事件绑定,参见ViewCommonEventListener类和包com.lidroid.xutils.view.annotation.event。


HttpUtils模块:



  • 支持同步,异步方式的请求;

  • 支持大文件上传,上传大文件不会oom;

  • 支持GET,POST,PUT,MOVE,COPY,DELETE,HEAD,OPTIONS,TRACE,CONNECT请求;

  • 下载支持301/302重定向,支持设置是否根据Content-Disposition重命名下载的文件;

  • 返回文本内容的请求(默认只启用了GET请求)支持缓存,可设置默认过期时间和针对当前请求的过期时间。


BitmapUtils模块:



  • 加载bitmap的时候无需考虑bitmap加载过程中出现的oom和android容器快速滑动时候出现的图片错位等现象;

  • 支持加载网络图片和本地图片;

  • 内存管理使用lru算法,更好的管理bitmap内存;

  • 可配置线程加载线程数量,缓存大小,缓存路径,加载显示动画等…


DbUtils模块:



  • android中的orm(对象关系映射)框架,一行代码就可以进行增删改查;

  • 支持事务,默认关闭;

  • 可通过注解自定义表名,列名,外键,唯一性约束,NOT NULL约束,CHECK约束等(需要混淆的时候请注解表名和列名);

  • 支持绑定外键,保存实体时外键关联实体自动保存或更新;

  • 自动加载外键关联实体,支持延时加载;

  • 支持链式表达查询,更直观的查询语义


4、Volley框架


Volley简要介绍


在2013年Google I/O大会上推出了一个新的网络通信框架Volley。Volley既可以访问网络取得数据,也可以加载图片,并且在性能方面也进行了大幅度的调整,它的设计目标就是非常适合去进行数据量不大,但通信频繁的网络操作,而对于大数据量的网络操作,比如说下载文件等,Volley的表现就会非常糟糕。在使用Volley前请下载Volley库并放在libs目录下并add到工程中。


Volley的主要特点



  • 扩展性强。Volley 中大多是基于接口的设计,可配置性强。

  • 一定程度符合 Http 规范,包括返回 ResponseCode(2xx、3xx、4xx、5xx)的处理,请求头的处理,缓存机制的支持等。并支持重试及优先级定义。

  • 默认 Android2.3 及以上基于 HttpURLConnection,2.3 以下基于 HttpClient 实现,这两者的区别及优劣在4.2.1 Volley中具体介绍。

  • 提供简便的图片加载工具。


Volley提供的功能



  • JSON,图像等的异步下载;

  • 网络请求的排序(scheduling)

  • 网络请求的优先级处理

  • 缓存

  • 多级别取消请求

  • 和Activity和生命周期的联动(Activity结束时同时取消所有网络请求)


Volley优缺点


优点



  • 非常适合进行数据量不大,但通信频繁的网络操作

  • 可直接在主线程调用服务端并处理返回结果

  • 可以取消请求,容易扩展,面向接口编程

  • 网络请求线程NetworkDispatcher默认开启了4个,可以优化,通过手机CPU数量

  • 通过使用标准的HTTP缓存机制保持磁盘和内存响应的一致

  • 通信更快、更稳定、更简单


缺点



  • 使用的是HttpClient的,HttpURLConnection类

  • 6.0不支持的HttpClient了,如果想支持得添加org.apache.http.legacy.jar

  • 对大文件下载Volley的表现非常糟糕

  • 只支持HTTP请求

  • 图片加载性能一般

  • 不适合进行大数据的上传和下载

  • 不能下载文件:这也是它最致命的地方


为什么使用Volley:



  • 高效的的Get/Post方式的数据请求交互

  • 网络图片的加载和缓存

  • 谷歌官方推出

  • 性能稳定和强劲


5、okhttp


okhttp简介


一个处理网络请求的开源项目,是安卓端最火热的轻量级框架,由移动支付Square公司贡献(该公司还贡献了Picasso),用于替代HttpUrlConnection和Apache HttpClient(android API23 6.0里已移除HttpClient)


okhttp优势



  • 支持HTTP2/SPDY(SPDY是Google开发的基于TCP的传输层协议,用以最小化网络延迟,提升网络速度,优化用户的网络使用体验。),可以合并多个到同一个主机的请求

  • 允许连接到同一个主机地址的所有请求,提高请求效率

  • socket自动选择最好路线,并支持自动重连,拥有自动维护的socket连接池,减少握手次数,减少了请求延迟,共享Socket,减少对服务器的请求次数

  • 基于Headers的缓存策略减少重复的网络请求。

  • 缓存响应数据来减少重复的网络请求

  • 减少了对数据流量的消耗

  • 自动处理GZip压缩

  • OkHttp使用Okio来大大简化数据的访问与存储,Okio是一个增强 java.io 和 java.nio的库。

  • OkHttp还处理了代理服务器问题和SSL握手失败问题。


okhttp流程图


\


okhttp功能



  • PUT,DELETE,POST,GET等请求

  • 基于Http的文件上传

  • 文件的上传下载

  • 上传下载的进度回调

  • 加载图片(内部会图片大小自动压缩)

  • 支持请求回调,直接返回对象、对象集合

  • 支持session的保持

  • 支持自签名网站https的访问,提供方法设置下证书就行

  • 支持取消某个请求


6、Retrofit


Retrofit简介


Retrofit与okhttp共同出自于Square公司,retrofit就是对okhttp做了一层封装。把网络请求都交给给了Okhttp,我们只需要通过简单的配置就能使用retrofit来进行网络请求了,主要作者是Android大神JakeWharton


Retrofit特性



  • 将rest API封装为java接口,我们根据业务需求来进行接口的封装,实际开发可能会封装多个不同的java接口以满足业务需求。(注意:这里会用到Retrofit的注解:比如get,post)

  • 使用Retrofit提供的封装方法将我们的生成我们接口的实现类,这个真的很赞,不用我们自己实现,通过注解Retrofit全部帮我们自动生成好了。

  • 调用我们实现类对象的接口方法。


为什么要用Retrofit




  • 在处理HTTP请求的时候,因为不同场景或者边界情况等比较难处理。你需要考虑网络状态,需要在请求失败后重试,需要处理HTTPS等问题,二这些事情让你很苦恼,而Retrofit可以将你从这些头疼的事情中解放出来。




  • 当然你也可以选择android-async-http和Volley,但为什么选择Retrofit?首先效率高,其次Retrofit强大且配置灵活,其次是和OkHttp无缝衔接。




  • 在Retrofit2之前,OkHttp是一个可选的客户端。Retrofit2中,Retrofit与OkHttp强耦合,使得更好地利用OkHttp,包括使用OkHttp解决一些棘手的问题。




Retrofit流程图


\


Retrofit优缺点


优点:



  • 可以配置不同HTTP client来实现网络请求,如okhttp、httpclient等

  • 请求的方法参数注解都可以定制

  • 支持同步、异步和RxJava

  • 超级解耦

  • 可以配置不同的反序列化工具来解析数据,如json、xml等

  • 使用非常方便灵活

  • 框架使用了很多设计模式(感兴趣的可以看看源码学习学习)


缺点:



  • 不能接触序列化实体和响应数据

  • 执行的机制太严格

  • 使用转换器比较低效

  • 只能支持简单自定义参数类型 

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

Android查看第三方库的依赖树汇总

项目的开发过程中,我们或多或少都会引入第三方库,引入的库越多,越容易产生库之间的依赖冲突。 下面就拿我遇到的问题还原一下: 之前接人容联客服系统的时候,集成完成后进入客服页面发生闪退,我们回顾一下错误信息: 我们关键看一下报错代码: java.lang.No...
继续阅读 »

项目的开发过程中,我们或多或少都会引入第三方库,引入的库越多,越容易产生库之间的依赖冲突。


下面就拿我遇到的问题还原一下:


之前接人容联客服系统的时候,集成完成后进入客服页面发生闪退,我们回顾一下错误信息:


122.jpg


我们关键看一下报错代码:


java.lang.NoSuchMethodError: No virtual method into (Landroid/widget/ImageView;)Lcom/bumptech/glide/request/target/Target; in class Lcom/a/a/i; or its super classes (declaration of 'com.a.a.i' appears in/data/app/com.sami91sami.h5-1/base.apk)
复制代码

我们可以根据报错,跳到报错的地方:


133.jpg


该报错的意思就是:没有


into(Landroid/widget/ImageView)
复制代码

的方法,代码能编译通过,说明项目中肯定是添加依赖了,那怎么还会报这个错误呢?还没添加依赖之前,项目中也是使用的Glide进行图片的加载,会不会是项目中的Glide与容联Demo中的Glide有冲突呢。


我们可以根据报错的地方into方法,点进入看源码:


144.jpg


可以看到容联Demo使用的Glide版本是3.7.0。


再来看看项目中Glide使用的版本:


155.jpg


可以看到项目中使用的Glide版本是4.5.0。


这时就想到真的很大概率是两者的Glide版本有冲突了。


果然将容联Demo中的Glide版本改成4.5.0之后,编译运行进入客服界面后,没有报错了,完美解决。


这就是我之前遇到的库冲突的问题,这个问题有错误信息可以定位到是Glide库依赖的问题,要是遇到其它错误信息没那么显著的,那是不是就头疼了呢。


当时遇到这个问题,我并没有使用查看依赖树的方式,而是直接查看了源码,因为当时我并不知道还能这么干,幸运的是很快就定位到了问题所在,所以当我们升级第三方库或者引入新的第三方库时,库与库之间依赖冲突,我们需要知道每个第三方依赖库的依赖树,知道依赖树就清楚哪里冲突啦。


下面就记录下几种查看依赖树的方式:


方案一: Gradle task工具查看


1、点击Android studio面板右上角“Gradle”,如图所示:


1639041944906-gzb.png


2、按照如图目录找到dependencise双击,会在Run控制台输出打印,如图所示:


222.png


3、打印如图所示:


333.png


方案二:使用Gradle View插件


1、快捷键Ctrl+Alt+s,打开settings,然后点击按钮Plugins


444.png


2、搜索 Gradle View,然后安装,并重启Android Studio,我这是已经安装成功后的截图


555.png


3、点击菜单栏上View -> Tool Windows -> Gradle View,然后等待一会,就可以查看了。


666.png


如图所示:


777.png


方案三:Terminal控制台查看


在windows上Android studio Terminal中使用这个命令:


gradlew :app:dependencies(“app”为module名称)
复制代码

在MacOS中使用下面的命令:


./gradlew :app:dependencies(“app”为module名称)
复制代码

这个命令会将gradle执行的各个步骤都打印出来,包括releaseUnitTestRuntimeClasspath,releaseUnitTestCompileClasspath,releaseRuntimeClasspath,releaseCompileClasspath,lintClassPath,debugUnitTestRuntimeClasspath等等。


那么,我们可以配置configuration 参数只查看其中一个的依赖树就够了。


 ./gradlew :app:dependencies --configuration compile
复制代码

在Window系统下,无需使用./开头,直接使用gradlew即可。


执行app模块下的dependencies任务;额外配置compile,编译环境下的依赖项。


888.png


通过查看依赖树,我们就能看到哪些依赖有冲突,比如某个框架的support包冲突,只要在moudle的gradle文件下找到该冲突的依赖用括号括住,在后面加:


{
exclude group:'com.android.support'
}
复制代码

这要就可以把该框架的support包移除啦。


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

Android Canvas画布解析

1.简介在开发中,我们经常需要自定义View去实现各种各样的效果,在这个过程中经常需要用到Canvas画布去绘制各种各样的图形和图案,因此,熟练地掌握Canvas的各种使用方法,就显得尤为重要。本文将简要介绍Canvas的各种用法,加深大家的理解。2.绘制各种...
继续阅读 »

1.简介

在开发中,我们经常需要自定义View去实现各种各样的效果,在这个过程中经常需要用到Canvas画布去绘制各种各样的图形和图案,因此,熟练地掌握Canvas的各种使用方法,就显得尤为重要。本文将简要介绍Canvas的各种用法,加深大家的理解。

2.绘制各种图形

Canvas提供了很多绘制方法,基于这些方法,我们可以绘制出各种各样的图形,下面我们就开始介绍这些绘制方法。

2.1 drawARGB

此方法可以用ARGB颜色绘制一个颜色背景,方法如下:

//a:颜色的alpha部分,取值0--255
//r:颜色的red部分,取值0--255
//g:颜色的green部分,取值0--255
//b:颜色的blue部分,取值0--255
public void drawARGB(int a, int r, int g, int b)

现在使用此方法绘制一个纯色背景,示例如下:

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawARGB(255,150,100,100);
}

2.2 drawArc

先介绍其中的一个方法,方法如下:

//left:左边到父布局左边的距离
//top:顶边到父布局顶边的距离
//right:右边到父布局左边的距离
//bottom:底边到父布局顶边的距离
//startAngle:弧开始的角度
//sweepAngle:顺时针方向扫描的角度
//useCenter:是否使用中心
//paint:绘制弧的画笔,这个值不能为null
public void drawArc(float left, float top, float right, float bottom, float startAngle, float sweepAngle, boolean useCenter, @NonNull Paint paint)

此方法用来绘制弧形,如果起始角度是负值或大于等于360,起始角度取360的模。如果扫描角度大于等于360,椭圆形将会被完全地绘制,如果扫描角度是负值,扫描角度取360的模。弧的绘制是顺时针方向,0度对应着钟表的3点钟方向。useCenter为true时的示例如下:

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawArc(50,50,300,300,0,300,true,paint);
}

useCenter为false时的示例如下:

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawArc(50,50,300,300,0,300,false,paint);
}

drawArc的另一个重载方法如下:

//oval:用来定义弧形的形状和大小的椭圆的边界,这个值不能为null
//startAngle:弧开始的角度
//sweepAngle:顺时针方向扫描的角度
//useCenter:是否使用中心
//paint:绘制弧的画笔,这个值不能为null
public void drawArc(@NonNull RectF oval, float startAngle, float sweepAngle, boolean useCenter, @NonNull Paint paint)

此方法useCenter为true时的示例如下:

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
RectF rectF = new RectF(50,50,300,300);
canvas.drawArc(rectF,0,200,true,paint);
}

此方法useCenter为false时的示例如下:

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
RectF rectF = new RectF(50,50,300,300);
canvas.drawArc(rectF,0,200,false,paint);
}

2.3 drawBitmap

这个方法是用来绘制位图的,这个方法有很多重载,先看其中的一个方法:

//bitmap:要绘制的位图
//matrix:用来变换位图的矩阵
//paint:绘制位图的画笔,可能为null
public void drawBitmap(@NonNull Bitmap bitmap, @NonNull Matrix matrix, @Nullable Paint paint)

此方法绘制位图的示例如下:

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.test);
Matrix matrix = new Matrix();
canvas.drawBitmap(bitmap,matrix,paint);
}

看一个drawBitmap的重载方法如下:

//bitmap:要绘制的位图
//src:要绘制的位图的子集,即绘制的是全部或者部分位图,可能为null
//dst:位图将要通过缩放和转换去适应的矩形
//paint:绘制位图的画笔,可能为null
public void drawBitmap(@NonNull Bitmap bitmap, @Nullable Rect src, @NonNull Rect dst,
@Nullable Paint paint)

此方法用来绘制位图,通过自动缩放和转换去适应目标矩形,示例如下:

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.test);
Rect srcRect = new Rect(0,0,300,300);
Rect dstRect = new Rect(0,0,600,600);
canvas.drawBitmap(bitmap,srcRect,dstRect,paint);
}

再来看另外一个重载方法如下:

//bitmap:要绘制的位图
//src:要绘制的位图的子集,即绘制的是全部或者部分位图,可能为null
//dst:位图将要通过缩放和转换去适应的矩形
//paint:绘制位图的画笔,可能为null
public void drawBitmap(@NonNull Bitmap bitmap, @Nullable Rect src, @NonNull RectF dst,
@Nullable Paint paint)

此方法也是用来绘制通过自动缩放和转换去适应目标矩形的位图,示例如下:

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.test);
Rect srcRect = new Rect(0,0,300,300);
RectF dstRectF = new RectF(0,0,600,600);
canvas.drawBitmap(bitmap,srcRect,dstRectF,paint);
}

再来看绘制位图的一个方法如下:

//bitmap:要绘制的位图
//left:位图左边的位置
//top:位图顶边的位置
//paint:绘制位图的画笔,可能为null
public void drawBitmap(@NonNull Bitmap bitmap, float left, float top, @Nullable Paint paint)

这个方法绘制左上角在(x,y)的位图,如果位图和画布拥有不同的密度,将会自动缩放位图,以和画布相同的密度绘制,示例如下:

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.test);
canvas.drawBitmap(bitmap,50,100,paint);
}

2.4 drawCircle

此方法使用画笔paint绘制圆,如果半径小于等于0将不会绘制任何东西,基于画笔的样式,圆将会被填充或者绘制的是轮廓,方法如下:

//cx:圆心的x坐标
//cy:圆心的y坐标
//radius:圆的半径
//paint:绘制圆的画笔
public void drawCircle(float cx, float cy, float radius, @NonNull Paint paint)

此方法的示例如下:

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawCircle(200,200,150,paint);
}

2.5 drawColor

此方法使用颜色填充整个画布canvas的位图,方法如下:

//color:绘制在画布上的颜色
public void drawColor(@ColorInt int color)

示例如下:

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawColor(Color.GREEN);
}

再来看一个重载方法如下:

//color:绘制在画布上的颜色
//mode:应用到颜色上的porter-duff模式
public void drawColor(@ColorInt int color, @NonNull PorterDuff.Mode mode)

此方法使用颜色和porter-duff模式填充整个画布canvas的位图,示例如下:

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawColor(Color.GREEN, PorterDuff.Mode.DARKEN);
}

2.6 drawLine

此方法使用画笔paint和开始及终止的x,y坐标绘制线段,由于线总是轮廓式的,画笔paint的样式将会被忽略,方法如下:

//startX:线段起始点的x坐标
//startY:线段起始点的y坐标
//stopX:线段结束点的x坐标
//stopY:线段结束点的y坐标
//paint:绘制线段的画笔
public void drawLine(float startX, float startY, float stopX, float stopY,
@NonNull Paint paint)

示例如下:

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawLine(50,50,300,300,paint);
}

2.7 drawLines

此方法绘制一系列线段,每条线需要pts数组中4个连续的值。因此,绘制一条线,数组必须至少包括4个值。逻辑上和绘制下面的数组一样,先使用pts[0]、pts[1]、pts[2]、pts[3]绘制线,接着使用[4]、pts[5]、pts[6]、pts[7]绘制线,以此类推。方法如下:

//pts:要绘制的点的数组,如[x0,y0,x1,y1,x2,y2...]
//offset:绘制前在数组中要跳过的值的个数
//count:在跳过偏移量后,要处理的数组中值的个数
//paint:绘制的画笔
public void drawLines(@Size(multiple = 4) @NonNull float[] pts, int offset, int count,
@NonNull Paint paint)

此方法的示例如下:

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
float[] pts = {50,10,100,200,300,200,200,400};
canvas.drawLines(pts,0,8,paint);
}

再来看另一个重载方法如下:

//pts:要绘制的点的数组,如[x0,y0,x1,y1,x2,y2...]
//paint:绘制的画笔
public void drawLines(@Size(multiple = 4) @NonNull float[] pts, @NonNull Paint paint)

示例如下:

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
float[] pts = {50,10,100,200,300,200,200,400};
canvas.drawLines(pts,paint);
}

2.8 drawOval

此方法使用画笔paint绘制椭圆,椭圆被填充或是轮廓由画笔paint的样式决定,方法如下:

//left:左边到父布局左边的距离
//top:顶边到父布局顶边的距离
//right:右边到父布局左边的距离
//bottom:底边到父布局顶边的距离
//paint:绘制的画笔
public void drawOval(float left, float top, float right, float bottom, @NonNull Paint paint)

示例如下:

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawOval(50,50,600,300,paint);
}

再来看一个重载方法:

//oval:椭圆的矩形边界,这个值不能为null
//paint:绘制的画笔,不能为null
public void drawOval(@NonNull RectF oval, @NonNull Paint paint)

示例如下:

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
RectF rectF = new RectF(50,50,600,300);
canvas.drawOval(rectF,paint);
}

2.9 drawPaint

此方法使用画笔paint填充整个画布的位图,方法如下:

//paint:在画布上绘制的画笔
public void drawPaint(@NonNull Paint paint)

2.10 drawPath

此方法使用画笔paint绘制路径,路径被填充或是轮廓由画笔paint的样式决定,方法如下:

//path:被绘制的路径
//paint:绘制路径的画笔
public void drawPath(@NonNull Path path, @NonNull Paint paint)

示例如下:

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
Path path = new Path();
path.moveTo(50,50);
path.lineTo(200,100);
path.lineTo(200,400);
path.lineTo(150,500);
canvas.drawPath(path,paint);
}

2.11 drawPoint

此方法用来绘制一个点,方法如下:

//x:点的x坐标
//y:点的y坐标
//paint:绘制点的画笔
public void drawPoint(float x, float y, @NonNull Paint paint)

示例如下:

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawPoint(100,100,paint);
}

2.12 drawPoints

此方法绘制一系列点,每个点位于被pts[]确定的坐标的中心,点的直径由画笔的笔画宽度确定,点的形状由画笔的Cap类型确定,点的形状是正方形的,除非当Cap类型是Round的时候,点的形状是圆形的,方法如下:

//pts:要绘制的点的数组[x0,y0,x1,y1,x2,y2...]
//offset:绘制前跳过的值的个数
//count:跳过偏移量之后要处理的值的个数
//paint:绘制点的画笔
public void drawPoints(@Size(multiple = 2) float[] pts, int offset, int count,
@NonNull Paint paint)

示例如下:

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
float[] pts = {50,50,100,200,300,200,200,400};
canvas.drawPoints(pts,0,8,paint);
}

再来看另外一个重载方法如下:

//pts:要绘制的点的数组[x0,y0,x1,y1,x2,y2...]
//paint:绘制点的画笔
public void drawPoints(@Size(multiple = 2) @NonNull float[] pts, @NonNull Paint paint)

示例如下:

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
float[] pts = {50,50,100,200,300,200,200,400};
canvas.drawPoints(pts,paint);
}

2.13 drawRGB

此方法使用RGB颜色填充整个画布的位图,方法如下:

//r:颜色的red部分,取值0--255
//g:颜色的green部分,取值0--255
//b:颜色的blue部分,取值0--255
public void drawRGB(int r, int g, int b)

示例如下:

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawRGB(200,100,100);
}

2.14 drawRect

此方法使用画笔绘制矩形,矩形被填充或者显示轮廓由画笔的样式确定,方法如下:

//left:矩形的左边
//top:矩形的顶边
//right:矩形的右边
//bottom:矩形的底边
//paint:绘制矩形的画笔
public void drawRect(float left, float top, float right, float bottom, @NonNull Paint paint)

示例如下:

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawRect(50,100,500,300,paint);
}

看一个重载方法如下:

//r:要绘制的矩形
//paint:绘制矩形的画笔
public void drawRect(@NonNull Rect r, @NonNull Paint paint)

示例如下:

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
Rect rect = new Rect(50,100,500,300);
canvas.drawRect(rect,paint);
}

再来看另外一个重载方法如下:

//rect:要绘制的矩形
//paint:绘制矩形的画笔
public void drawRect(@NonNull RectF rect, @NonNull Paint paint)

此方法的示例如下:

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
RectF rectF = new RectF(50,100,500,300);
canvas.drawRect(rectF,paint);
}

2.15 drawRoundRect

此方法使用画笔绘制圆角矩形,矩形被填充或者显示轮廓由画笔的样式确定,方法如下:

//rect:圆角矩形的矩形边界
//rx:圆角的x半径
//ry:圆角的y半径
//paint:绘制圆角矩形的画笔
public void drawRoundRect(@NonNull RectF rect, float rx, float ry, @NonNull Paint paint)

示例如下:

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
RectF rectF = new RectF(50,100,500,300);
canvas.drawRoundRect(rectF,20,20,paint);
}

2.16 drawText

此方法用来绘制文本,原点在(x,y),原点和画笔paint中的对齐设置有关,方法如下:

//text:被绘制的文本
//x:文本的原点的x坐标
//y:文本基线的y坐标
//paint:绘制文本的画笔,可以进行颜色、大小、样式等设置
public void drawText(@NonNull String text, float x, float y, @NonNull Paint paint)

示例如下:

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
String text = "Android Canvas";
canvas.drawText(text,200,300,paint);
}

再看一个重载方法如下:

//text:被绘制的文本
//start:要绘制的文本中第一个字符的索引
//end:(end-1)是要绘制的文本中最后一个字符的索引
//x:文本的原点的x坐标
//y:文本基线的y坐标
//paint:绘制文本的画笔,可以进行颜色、大小、样式等设置
public void drawText(@NonNull String text, int start, int end, float x, float y,
@NonNull Paint paint)

示例如下:

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
String text = "Android Canvas";
canvas.drawText(text,3,11,200,300,paint);
}

2.17 drawTextOnPath

此方法使用画笔paint沿着路径绘制文本,画笔的对齐方式决定从何处沿着路径开始文本的绘制,方法如下:

//text:被绘制的文本
//path:文本应该遵循的路径
//hOffset:沿着路径文本开始位置偏移的距离
//hOffset:文本在路径之上或之下的偏移的距离,可以为正值或负值
//paint:绘制文本的画笔,可以进行颜色、大小、样式等设置
public void drawTextOnPath(@NonNull String text, @NonNull Path path, float hOffset,
float vOffset, @NonNull Paint paint)

示例如下:

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
String text = "Android Canvas";
Path path = new Path();
path.moveTo(50,50);
path.lineTo(200,100);
path.lineTo(400,400);
canvas.drawTextOnPath(text,path,0,0,paint);
}

3.总结

在自定义View的时候,将会经常用到Canvas,因此熟练地掌握和运用这些绘制方法就显得比较重要。使用Canvas可以绘制点、线、矩形、圆、椭圆、文本、路径、位图等各种各样的图形图案,本文详细地介绍了Canvas的各种方法,并给出了示例代码,灵活运用这些方法进行组合,就能绘制出各种各样的图案和效果。

收起阅读 »

写动画不用愁,Lottie 已经支持 Jetpack Compose 啦!

概述 Lottie 是一款优秀的移动应用动画效果框架,支持为原生应用添加动画效果。Lottie 在不需要对代码进行重写的情况下让工程师更加方便的创建更丰富的动画效果,有了 Lottie 就不再需要使用 Gif 动画来展现效果,在移动开发领域 Lottie 已经...
继续阅读 »

概述


Lottie 是一款优秀的移动应用动画效果框架,支持为原生应用添加动画效果。Lottie 在不需要对代码进行重写的情况下让工程师更加方便的创建更丰富的动画效果,有了 Lottie 就不再需要使用 Gif 动画来展现效果,在移动开发领域 Lottie 已经广为人知。 伴随着 Jetpack Compose 1.0 的正式发布,Lottie 也同样支持了 Jetpack Compose。这篇文章将指引你如何在 Jeptack Compose 中使用 Lottie 动画。这篇文章所使用的 Lottie 动画文件来自 Lottie 官方网站 ,你可以在这里找到更多免费的 Lottie 动画文件。


添加 Lottie 依赖项


你需要 build.gradle(app) 脚本文件中,添加依赖项目。



implementation "com.airbnb.android:lottie-compose:4.0.0"



配置 Lottie 资源


你可以通过 Lottie 官方网站 或其他途径获取到你想要添加的Lottie动画对应静态 json 资源,或者你也可以使用URL方式。


如果你使用的是静态 json 文件方式,你可以将其放入 res/raw 目录下。


如果你使用的是URL方式,后续需要加载 lottie 时,你可以选用 URL 方式。


创建 Lottie 动画


首先,我们创建两个 mutableState 用于描述动画的速度与开始暂停状态。


var isPlaying by remember {
mutableStateOf(true)
}
var speed by remember {
mutableStateOf(1f)
}

下一步,我们需要加载我们预先准备好的 Lottie资源。 这里我选择使用本地res/raw目录下静态资源的方式。


val lottieComposition by rememberLottieComposition(
spec = LottieCompositionSpec.RawRes(R.raw.lottie),
)

当然 Lottie 还为你提供了其他加载方式。


sealed interface LottieCompositionSpec {
// 加载 res/raw 目录下的静态资源
inline class RawRes(@androidx.annotation.RawRes val resId: Int) : LottieCompositionSpec

// 加载 URL
inline class Url(val url: String) : LottieCompositionSpec

// 加载手机目录下的静态资源
inline class File(val fileName: String) : LottieCompositionSpec

// 加载 asset 目录下的静态资源
inline class Asset(val assetName: String) : LottieCompositionSpec

// 直接加载 json 字符串
inline class JsonString(val jsonString: String) : LottieCompositionSpec
}

再接下来,我们还需要描述 Lottie 的动画状态。


val lottieAnimationState by animateLottieCompositionAsState (
composition = lottieComposition, // 动画资源句柄
iterations = LottieConstants.IterateForever, // 迭代次数
isPlaying = isPlaying, // 动画播放状态
speed = speed, // 动画速度状态
restartOnPlay = false // 暂停后重新播放是否从头开始
)

最后,我们仅需要把动画资源句柄和动画状态提供给 LottieAnimation Composable 即可。


LottieAnimation(
lottieComposition,
lottieAnimationState,
modifier = Modifier.size(400.dp)
)

效果展示





源代码


@Preview
@Composable
fun LottieDemo() {
var isPlaying by remember {
mutableStateOf(true)
}
var speed by remember {
mutableStateOf(1f)
}

val lottieComposition by rememberLottieComposition(
spec = LottieCompositionSpec.RawRes(R.raw.lottie),
)

val lottieAnimationState by animateLottieCompositionAsState (
composition = lottieComposition,
iterations = LottieConstants.IterateForever,
isPlaying = isPlaying,
speed = speed,
restartOnPlay = false
)


Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Column {
Text(
text = "Lottie Animation In Jetpack Compose",
fontSize = 30.sp
)
Spacer(modifier = Modifier.height(30.dp))
LottieAnimation(
lottieComposition,
lottieAnimationState,
modifier = Modifier.size(400.dp)
)

Row(
horizontalArrangement = Arrangement.SpaceAround,
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
Row(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Button(
onClick = {
speed = max(speed - 0.25f, 0f)
},
colors = ButtonDefaults.buttonColors(
backgroundColor = Color(0xFF0F9D58)
)
) {
Text(
text = "-",
color = Color.White,
fontWeight = FontWeight.Bold,
fontSize = 20.sp,
)
}

Text(
text = "Speed ( $speed ) ",
color = Color.Black,
fontWeight = FontWeight.Bold,
fontSize = 15.sp, modifier = Modifier.padding(horizontal = 10.dp)

)
Button(
onClick = {
speed += 0.25f
},
colors = ButtonDefaults.buttonColors(
backgroundColor = Color(0xFF0F9D58)
)
) {
Text(
text = "+",
color = Color.White,
fontWeight = FontWeight.Bold,
fontSize = 20.sp
)
}
}

Button(
onClick = {
isPlaying = !isPlaying
},
colors = ButtonDefaults.buttonColors(
backgroundColor = Color(0xFF0F9D58)
)
) {
Text(
text = if (isPlaying) "Pause" else "Play",
color = Color.White
)
}
}
}
}
}

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

Flutter | 启动,渲染,setState 流程

前言 用了这么久 Flutter 了,居然都不知道他的启动过程,真的是学之有愧啊,今天我们来分析一下 Flutter 的启动流程,以及他的渲染过程,对其做一个简单的剖析。 启动流程 Flutter 的启动入口在 lib/main.dart 里的 main() ...
继续阅读 »

前言


用了这么久 Flutter 了,居然都不知道他的启动过程,真的是学之有愧啊,今天我们来分析一下 Flutter 的启动流程,以及他的渲染过程,对其做一个简单的剖析。


启动流程


Flutter 的启动入口在 lib/main.dart 里的 main() 函数中,他是 Dart 应用程序的起点,main 函数中最简单的实现如下:


void main() => runApp(MyApp());

可以看到,main 函数中只调用了 runApp() 方法,我们看看它里面都干了什么:


void runApp(Widget app) {
WidgetsFlutterBinding.ensureInitialized()
..scheduleAttachRootWidget(app)
..scheduleWarmUpFrame();
}

接收了一个 widget 参数,它是 Flutter 启动后要展示的第一个组件,而 WidgetsFlutterBinding 正是绑定 widgetFlutter 引擎的桥梁,定义如下:


/// 基于 Widgets 框架的应用程序的具体绑定。
class WidgetsFlutterBinding extends BindingBase with GestureBinding, SchedulerBinding, ServicesBinding, PaintingBinding, SemanticsBinding, RendererBinding, WidgetsBinding {

static WidgetsBinding ensureInitialized() {
if (WidgetsBinding.instance == null)
WidgetsFlutterBinding();
return WidgetsBinding.instance!;
}
}

可以看到 WidgetsFlutterBinding 继承自 BindingBase ,并且混入了很多 Binding,在介绍这些 Binding 之前我们先介绍一下 Window ,下面是 Window 的官方解释:



The most basic interface to the host operating system's user interface.


主机操作系统用户界面的最基本界面。



很明显,Window 正是 Flutter Framework 连接宿主操作系统的接口,


我们看一下 Window 类的部分定义


@Native("Window,DOMWindow")
class Window extends EventTarget implements WindowEventHandlers, WindowBase GlobalEventHandlers,
_WindowTimers, WindowBase64 {

// 当前设备的DPI,即一个逻辑像素显示多少物理像素,数字越大,显示效果就越精细保真。
// DPI是设备屏幕的固件属性,如Nexus 6的屏幕DPI为3.5
double get devicePixelRatio => _devicePixelRatio;

// Flutter UI绘制区域的大小
Size get physicalSize => _physicalSize;

// 当前系统默认的语言Locale
Locale get locale;

// 当前系统字体缩放比例。
double get textScaleFactor => _textScaleFactor;

// 当绘制区域大小改变回调
VoidCallback get onMetricsChanged => _onMetricsChanged;
// Locale发生变化回调
VoidCallback get onLocaleChanged => _onLocaleChanged;
// 系统字体缩放变化回调
VoidCallback get onTextScaleFactorChanged => _onTextScaleFactorChanged;
// 绘制前回调,一般会受显示器的垂直同步信号VSync驱动,当屏幕刷新时就会被调用
FrameCallback get onBeginFrame => _onBeginFrame;
// 绘制回调
VoidCallback get onDrawFrame => _onDrawFrame;
// 点击或指针事件回调
PointerDataPacketCallback get onPointerDataPacket => _onPointerDataPacket;
// 调度Frame,该方法执行后,onBeginFrame和onDrawFrame将紧接着会在合适时机被调用,
// 此方法会直接调用Flutter engine的Window_scheduleFrame方法
void scheduleFrame() native 'Window_scheduleFrame';
// 更新应用在GPU上的渲染,此方法会直接调用Flutter engine的Window_render方法
void render(Scene scene) native 'Window_render';

// 发送平台消息
void sendPlatformMessage(String name,
ByteData data,
PlatformMessageResponseCallback callback) ;
// 平台通道消息处理回调
PlatformMessageCallback get onPlatformMessage => _onPlatformMessage;

... //其它属性及回调

}

可以看到 Window 中包含了当前设备和系统的一些信息和 Flutter Engine 的一些回调。


现在回过头来看一下 WidgetsFlutterBinding 混入的各种 Binding。通过查看这些 Binding 的源码,我们可以发现这些 Binding 中基本都是监听并处理 Window 对象中的一些事件,然后将这些事件安装 Framework 的模型进行包装,抽象后然后进行分发。可以看到 WidgetsFlutterBinding 正是粘连 Flutter engine 与上层 Framework 的胶水。




  • GestureBinding:提供了 window.onPointerDataPacket 回调,绑定 Fragment 手势子系统,是 Framework 事件模型与底层事件的绑定入口。


    mixin GestureBinding on BindingBase implements HitTestable, HitTestDispatcher, HitTestTarget {
    @override
    void initInstances() {
    super.initInstances();
    _instance = this;
    window.onPointerDataPacket = _handlePointerDataPacket;
    }
    }
    复制代码



  • ServiceBinidng:提供了 window.onPlatformMessage 回调,用户绑定平台消息通道(message channel) ,主要处理原生和 Flutter 通信。


    mixin SchedulerBinding on BindingBase {
    @override
    void initInstances() {
    super.initInstances();
    _instance = this;
    if (!kReleaseMode) {
    addTimingsCallback((List<FrameTiming> timings) {
    timings.forEach(_profileFramePostEvent);
    });
    }
    }



  • SchedulerBinding:提供了 window.onBeginFramewindow.onDrawFrame 回调,监听刷新事件,绑定 Framework 绘制调度子系统。




  • PaintingBinding :绑定绘制库,主要用户处理图片缓存




  • SemanticsBidning:语义化层与 Flutter engine 的桥梁,主要是辅助功能的底层支持。




  • RendererBinding:提供了 window.onMetricsChangedwindow.onTextScaleFactorChanged 等回调。他是渲染树与 Flutter engine 的桥梁。




  • WidgetsBinding:提供了 window.onLocaleChangeonBulidScheduled 等回调。他是 Flutter widget 层与 engine 的桥梁。




widgetsFlutterBinding.ensureInitiallized() 负责初始化一个 widgetsBinding 的全局单例,紧接着会调用 WidgetBindingattachRootwWidget 方法,该方法负责将根 Widget 添加到 RenderView 上,代码如下:


void scheduleAttachRootWidget(Widget rootWidget) {
Timer.run(() {
attachRootWidget(rootWidget);
});
}

void attachRootWidget(Widget rootWidget) {
final bool isBootstrapFrame = renderViewElement == null;
_readyToProduceFrames = true;
_renderViewElement = RenderObjectToWidgetAdapter<RenderBox>(
container: renderView,
debugShortDescription: '[root]',
child: rootWidget,
).attachToRenderTree(buildOwner!, renderViewElement as RenderObjectToWidgetElement<RenderBox>?);
if (isBootstrapFrame) {
SchedulerBinding.instance!.ensureVisualUpdate();
}
}

注意,代码中有 renderViewrenderViewElement 两个变量,renderView 是一个 Renderobject ,他是渲染树的根。而 renderViewElement 是 renderView 对应的 Element 对象。


可见该方法主要完成了根 widget 到根RenderObject 再到根 Element 的整个关联过程,我们在看看 attachToRenderTree 的源码实现过程:


RenderObjectToWidgetElement<T> attachToRenderTree(BuildOwner owner, [ RenderObjectToWidgetElement<T>? element ]) {
if (element == null) {
owner.lockState(() {
element = createElement();
assert(element != null);
element!.assignOwner(owner);
});
owner.buildScope(element!, () {
element!.mount(null, null);
});
} else {
element._newWidget = this;
element.markNeedsBuild();
}
return element!;
}

该方法负责创建根 element,即 RenderObjectToWidgetElement ,并且将 element 与 widget 进行关联,即创建出 widget 树对应的 element 树。


如果 element 创建过了,则将根 element 中关联的 widget 设为新的,由此可以看出 element 只会创建一次,后面会进行复用。那么 BuildOwner 是什么呢?,其实他就是 widget framework 的管理类,它跟踪哪些 widget 需要重新构建。


组件树在构建完毕后,回到 runApp 的实现中,当调完 attachRootWidget 后,最后一行会调用 WidgetsFlutterBainding 实例的 scheduleWarmUpFrame() 方法,该方法的是现在 SchedulerBinding 中,他被调用后会立即进行一次绘制,在此次绘制结束前,该方法就会锁定事件分发,也就是说在本次绘制结束完成之前 Flutter 不会响应各种事件,这可以保证在绘制过程中不会触发新的重绘。


总结


通过上面上面的分析我们可以知道 WidgetsFlutterBinding 就像是一个胶水,它里面会监听并处理 window 对象的事件,并且将这些事件按照 framework的模型进行包装并且分发。所以说 widgetsFlutterBinding 正是连接 Flutter engine 与上传 Framework 的胶水。


  WidgetsFlutterBinding.ensureInitialized()
..scheduleAttachRootWidget(app)
..scheduleWarmUpFrame();


  • ensureInitialized :负责初始化 WidgetsFlutterBinding ,并且监听 window 的事件进行包装分发。

  • scheduleAttachRootWidget:在该方法的后续中,会创建根 Element ,调用 mount 完成 elementRenderObject 树的创建

  • scheduleWarmUpFrame:开始绘制第一帧


渲染官线


Frame


一次绘制过程,我们可以将其称为一帧(frame),我们知道 flutter 可以实现 60 fps,就是指 1 秒中可以进行60次重绘,FPS 越大,界面就会越流畅。


这里需要说明的是 Flutter 中的 frame 并不等于屏幕的刷新帧,因为 Flutter UI 框架并不是每次屏幕刷新都会触发,这是因为,如果 UI 在一段时间不变,那么每次重新走一遍渲染流程是不必要的,因此 Flutter 在第一帧渲染结束后会采取一种主动请求 frame 的方式来实现只有当 UI 可能会改变时才会重新走渲染流程。


1,Flutter 会在 window 上注册一个 onBeginFrame 和一个 onDrawFrame回调,在 onDrawFrame 回调中最终会调用 drawFrame


2,当我们调用 window.scheduleFrame 方法之后,Flutter 引擎会在合适时机(可以认为是在屏幕下一次刷新之前,具体取决于 Flutter 引擎实现) 来调用 onBeginFrame 和 onDrawFrame


在调用 window.scheduleFrame 之前会对 onBeginFrame 和 onDrawFrame 进行注册,如下所示:


void scheduleFrame() {
if (_hasScheduledFrame || !framesEnabled)
return;
assert(() {
if (debugPrintScheduleFrameStacks)
debugPrintStack(label: 'scheduleFrame() called. Current phase is $schedulerPhase.');
return true;
}());
ensureFrameCallbacksRegistered();
window.scheduleFrame();
_hasScheduledFrame = true;
}

void ensureFrameCallbacksRegistered() {
window.onBeginFrame ??= _handleBeginFrame;
window.onDrawFrame ??= _handleDrawFrame;
}

可以看见,只有主动调用 scheduleFrame 之后,才会调用 drawFrame(该方法是注册的回调)。


所以我们在 Flutter 中提到 frame 时,如无特别说明,则是和 drawFrame() 相互对应,而不是和屏幕的刷新相对应。


Frame 处理流程


当有新的 frame 到来时,开始调用 SchedulerBinding.handleDrawFrame 来处理 frame,具体过程就是执行四个任务队列:transientCallbacks,midFrameMicotasks,persistentCallbacks,postFrameCallbacks。当四个任务队列执行完毕后当前 frame 结束。


综上,Flutter 将整个生命周期分为 5 种状态,通过 SchedulerPhase 来表示他们:


enum SchedulerPhase {
/// 空闲状态,并没有 frame 在处理,这种状态表示页面未发生变化,并不需要重新渲染
/// 如果页面发生变化,需要调用 scheduleFrame 来请求 frame。
/// 注意,空闲状态只是代表没有 frame 在处理。通常微任务,定时器回调或者用户回调事件都有可能被执行
/// 比如监听了 tap 事件,用户点击后我们 onTap回调就是在 onTap 执行的
idle,

/// 执行 临时 回调任务,临时回调任务只能被执行一次,执行后会被移出临时任务队列。
/// 典型代表就是动画回调会在该阶段执行
transientCallbacks,

/// 在执行临时任务是可能会产生一下新的微任务,比如在执行第一个临时任务时创建了一个 Fluture,
/// 且这个 Future 在所有任务执行完毕前就已经 resolve
/// 这种情况 Future 的回调将会在 [midFrameMicrotasks] 阶段执行
midFrameMicrotasks,

/// 执行一些持久的任务(每一个 frame 都要执行的任务),比如渲染官线(构建,布局,绘制)
/// 就是在该任务队列执行的
persistentCallbacks,

/// 在当前 frame 在结束之前将会执行 postFrameCallbacks,通常进行一些清理工作和请求新的 frame
postFrameCallbacks,
}

需要注意,接下来需要重点介绍的渲染管线就是在 persistentCallbacks 中执行的。


渲染管线(rendering pipline)


当我们页面需要发生变化时,我们需要调用 scheduleFrame() 方法去请求 frame,该方法中会注册 _handleBeginFrame_handleDrawFrame。 当 frame 到来时就会执行 _handleDrawFrame,代码如下:


void _handleDrawFrame() {
//判断当前 frame 是否需要推迟,这里的推迟原因是当前坑是预热帧
if (_rescheduleAfterWarmUpFrame) {
_rescheduleAfterWarmUpFrame = false;
//添加一个回调,该回调会在当前帧结束后执行
addPostFrameCallback((Duration timeStamp) {
_hasScheduledFrame = false;
//重新请求 frame。
scheduleFrame();
});
return;
}
handleDrawFrame();
}

void handleDrawFrame() {
assert(_schedulerPhase == SchedulerPhase.midFrameMicrotasks);
Timeline.finishSync(); // end the "Animate" phase
try {
// 切换当前生命周期状态
_schedulerPhase = SchedulerPhase.persistentCallbacks;
// 执行持久任务的回调,
for (final FrameCallback callback in _persistentCallbacks)
_invokeFrameCallback(callback, _currentFrameTimeStamp!);

// postFrame 回调
_schedulerPhase = SchedulerPhase.postFrameCallbacks;
final List<FrameCallback> localPostFrameCallbacks =
List<FrameCallback>.from(_postFrameCallbacks);
_postFrameCallbacks.clear();
for (final FrameCallback callback in localPostFrameCallbacks)
_invokeFrameCallback(callback, _currentFrameTimeStamp!);
} finally {
// 将状态改为空闲状态
_schedulerPhase = SchedulerPhase.idle;
Timeline.finishSync(); // end the Frame
//....
_currentFrameTimeStamp = null;
}
}

在上面的代码中,对持久任务进行了遍历,并且进行回调,对应的是 _persistentCallbacks ,通过对调用栈的分析,发现该回调是在初始化 RendererBinding 的时候被添加到 _persistentCallbacks 中的:


mixin RendererBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureBinding, SemanticsBinding, HitTestable {
@override
void initInstances() {
super.initInstances();
//添加持久任务回调......
addPersistentFrameCallback(_handlePersistentFrameCallback);
initMouseTracker();
if (kIsWeb) {
//添加 postFrame 任务回调
addPostFrameCallback(_handleWebFirstFrame);
}
}
void addPersistentFrameCallback(FrameCallback callback) {
_persistentCallbacks.add(callback);
}

所以最终的回调就是 _handlePersistentFrameCallback


void _handlePersistentFrameCallback(Duration timeStamp) {
drawFrame();
_scheduleMouseTrackerUpdate();
}

在上面代码中,调用到了 drawFrame 方法。




通过上面的分析之后,我们知道了当 frame 到来时,会调用到 drawFrame 中,由于 drawFrame 有一个实现方法,所以首先会调用到 WidgetsBinding 的 drawFrame() 方法,如下:


void drawFrame() {
.....//省略无关
try {
if (renderViewElement != null)
buildOwner!.buildScope(renderViewElement!); // 1.重新构建widget树
super.drawFrame();
buildOwner!.finalizeTree();
}
}

最终的调用如下:


void drawFrame() {
assert(renderView != null);
pipelineOwner.flushLayout(); // 2.更新布局
pipelineOwner.flushCompositingBits();//3.更新“层合成”信息
pipelineOwner.flushPaint(); // 4.重绘
if (sendFramesToEngine) {
renderView.compositeFrame(); // 5. 上屏,会将绘制出的bit数据发送给GPU
...../////
}
}

可以到上面代码主要做了五件事:


1,重新构建 widget 树(buildScope())


2,更新布局(flushLayout())


3,更新"层合成"信息(flushCompositingBits())


4,重绘(flushPaint())


5,上屏:将绘制的产物显示在屏幕上


上面的五部我们称为 rendering pipline ,中文翻译为 “渲染流水线” 或者 “渲染管线”,而这五个步骤便是重中之重。下面我们以 setState 的更新流程为例先对整个更新流程有一个比较深的印象。


setState 执行流


void setState(VoidCallback fn) {
assert(fn != null);
//执行 callback,返回值不能是 future
final Object? result = fn() as dynamic;
assert(() {
if (result is Future) {
throw ...//
}
}());
_element!.markNeedsBuild();
}

void markNeedsBuild() {
....//
//标注该 element 需要重建
_dirty = true;
owner!.scheduleBuildFor(this);
}

void scheduleBuildFor(Element element) {
//注释1
if (!_scheduledFlushDirtyElements && onBuildScheduled != null) {
_scheduledFlushDirtyElements = true;
onBuildScheduled!();
}
//注释2
_dirtyElements.add(element);
element._inDirtyList = true;
}

当调用 setState 后:


1,首先调用 markNeedsBuild 方法,将 element 的 dirty 标记为 true,表示需要重建


2,接着调用 scheduleBuildFor ,将当前的 element 添加到 _dirtyElements 列表中(注释2)


下面我们着重看一下 注释1的代码,


首先判断 _scheduledFlushDirtyElements 如果为 false,该字段值初始值默认就是 false,接着判断 onBuildScheduled 不为 null,其实 onBuildScheduled 在 WidgetBinding初始化的时候就已经创建了,所以他是不会为 null 的。


当条件成立后,就会直接执行 onBuildScheduled 回调。我们跟踪一下:


mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureBinding, RendererBinding, SemanticsBinding {
@override
void initInstances() {
super.initInstances();
...///
buildOwner!.onBuildScheduled = _handleBuildScheduled
}

void _handleBuildScheduled() {
...///
ensureVisualUpdate();
}

根据上面代码我们可以知道 onBuildScheduled 确实是在 WidgetsBinding 的初始化方法中进行初始化的。并且他的实现中调用了 ensureVisualUpdate 方法,我们继续跟进一下:


void ensureVisualUpdate() {
switch (schedulerPhase) {
case SchedulerPhase.idle:
case SchedulerPhase.postFrameCallbacks:
scheduleFrame();
return;
case SchedulerPhase.transientCallbacks:
case SchedulerPhase.midFrameMicrotasks:
case SchedulerPhase.persistentCallbacks:
return;
}
}

上面代码中,判断了 schedulerPhase 的状态,如果是 idle 和 postFrameCallbacks 状态的时候,就开始调用 scheduleFrame。



对于上面每种状态所代表的意义,在文章上面已经说过了,这里就不在赘述。值得一提的是,在每次 frame 流程完成的时候,在 finally 代码块中将状态又改为了 idle 。这也侧面说明如果你频繁的 setState 的时候,如果上次的渲染流程没有完成,则不会发起新的渲染。



接着继续看 scheduleFrame:


void scheduleFrame() {
//判断流程是否已经开始了
if (_hasScheduledFrame || !framesEnabled)
return;
// 注释1
ensureFrameCallbacksRegistered();
// 注释2
window.scheduleFrame();
_hasScheduledFrame = true;
}

注释1:注册 onBeginFrame 和 onDrawFrame ,这两个函数类型的字段在上面的 "渲染管线中已经说过了"。


注释2:flutter framework 想 Flutter Engine 发起一个请求,接着 Flutter 引擎会在合适的时机去调用 onBeginFrame 和 onDrawFrame。这个时机可以认为是屏幕下一次刷新之前,具体取决于 Flutter 引擎实现。


到此,setState 中最核心的就是触发了一个 请求,在下一次屏幕刷新的时候就会回调 onBeginFrame,执行完成之后才会调用 onDrawFrame 方法。




void handleBeginFrame(Duration? rawTimeStamp) {
...///
assert(schedulerPhase == SchedulerPhase.idle);
_hasScheduledFrame = false;
try {
Timeline.startSync('Animate', arguments: timelineArgumentsIndicatingLandmarkEvent);
//将生命周期改为 transientCallbacks,表示正在执行一些临时任务的回调
_schedulerPhase = SchedulerPhase.transientCallbacks;
final Map<int, _FrameCallbackEntry> callbacks = _transientCallbacks;
_transientCallbacks = <int, _FrameCallbackEntry>{};
callbacks.forEach((int id, _FrameCallbackEntry callbackEntry) {
if (!_removedIds.contains(id))
_invokeFrameCallback(callbackEntry.callback, _currentFrameTimeStamp!, callbackEntry.debugStack);
});
_removedIds.clear();
} finally {
_schedulerPhase = SchedulerPhase.midFrameMicrotasks;
}
}

上面代码主要是执行了_transientCallbacks 的回调方法。执行完成后将生命周期改为了 midFrameMicrotasks。


接下来就是执行 handlerDrawFrame 方法了。该方法在上面已经分析过了,已经知道它最终就会走到 drawFrame 方法中。


# WidgetsBindign.drawFrame()
void drawFrame() {
.....//省略无关
try {
if (renderViewElement != null)
buildOwner!.buildScope(renderViewElement!); // 1.重新构建widget树
super.drawFrame();
buildOwner!.finalizeTree();
}
}
# RendererBinding.drawFrame()
void drawFrame() {
assert(renderView != null);
pipelineOwner.flushLayout(); // 2.更新布局
pipelineOwner.flushCompositingBits();//3.更新“层合成”信息
pipelineOwner.flushPaint(); // 4.重绘
if (sendFramesToEngine) {
renderView.compositeFrame(); // 5. 上屏,会将绘制出的bit数据发送给GPU
...../////
}
}

以上,便是 setState 调用的大概过程,实际的流程会更加复杂一点,例如在这个过程中不允许再次调用 setState,还有在 frame 中会涉及到动画的调度,以及如何进行布局更新,重绘等。通过上面的分析,我们需要对整个流程有一个比较深的印象。


至于上面 drawFrame 中的绘制流程,我们放在下一篇文章中介绍。


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

其他都是错的,只有这一篇正确解决:Flutter Textfield长按报错修复:NosuchMethodError: The getter ‘pasterBu

正确解决:Flutter Textfield长按报错修复:NosuchMethodError: The getter 'pasterButtonLabel' was ca ????????? 为什么叫正确解决?? 关于这个问题,我在百度上看过很多人的答案,基本...
继续阅读 »

正确解决:Flutter Textfield长按报错修复:NosuchMethodError: The getter 'pasterButtonLabel' was ca ?????????


为什么叫正确解决??
关于这个问题,我在百度上看过很多人的答案,基本无一例外都是,说:“Cupertino缺少了对应的非英文版本的支持”。
大家真的看过源码吗?真的是缺少Cupertino么?我是真不相信的,flutter出了这么多年,连个中文都不支持?然后我就查阅了源码:
我发现了这个类 GlobalCupertinoLocalizations
有木有很眼熟,他和
GlobalMaterialLocalizations & GlobalWidgetsLocalizations 没啥区别


class _GlobalCupertinoLocalizationsDelegate extends LocalizationsDelegate {
const _GlobalCupertinoLocalizationsDelegate();

@override
bool isSupported(Locale locale) => kCupertinoSupportedLanguages.contains(locale.languageCode);

static final Map> _loadedTranslations = >{};

@override
Future load(Locale locale) {
assert(isSupported(locale));
return _loadedTranslations.putIfAbsent(locale, () {
util.loadDateIntlDataIfNotLoaded();

final String localeName = intl.Intl.canonicalizedLocale(locale.toString());
assert(
locale.toString() == localeName,
'Flutter does not support the non-standard locale form $locale (which '
'might be $localeName',
);

late intl.DateFormat fullYearFormat;
late intl.DateFormat dayFormat;
late intl.DateFormat mediumDateFormat;
// We don't want any additional decoration here. The am/pm is handled in
// the date picker. We just want an hour number localized.
late intl.DateFormat singleDigitHourFormat;
late intl.DateFormat singleDigitMinuteFormat;
late intl.DateFormat doubleDigitMinuteFormat;
late intl.DateFormat singleDigitSecondFormat;
late intl.NumberFormat decimalFormat;

void loadFormats(String? locale) {
fullYearFormat = intl.DateFormat.y(locale);
dayFormat = intl.DateFormat.d(locale);
mediumDateFormat = intl.DateFormat.MMMEd(locale);
// TODO(xster): fix when https://github.com/dart-lang/intl/issues/207 is resolved.
singleDigitHourFormat = intl.DateFormat('HH', locale);
singleDigitMinuteFormat = intl.DateFormat.m(locale);
doubleDigitMinuteFormat = intl.DateFormat('mm', locale);
singleDigitSecondFormat = intl.DateFormat.s(locale);
decimalFormat = intl.NumberFormat.decimalPattern(locale);
}

if (intl.DateFormat.localeExists(localeName)) {
loadFormats(localeName);
} else if (intl.DateFormat.localeExists(locale.languageCode)) {
loadFormats(locale.languageCode);
} else {
loadFormats(null);
}

return SynchronousFuture(getCupertinoTranslation(
locale,
fullYearFormat,
dayFormat,
mediumDateFormat,
singleDigitHourFormat,
singleDigitMinuteFormat,
doubleDigitMinuteFormat,
singleDigitSecondFormat,
decimalFormat,
)!);
});
}

@override
bool shouldReload(_GlobalCupertinoLocalizationsDelegate old) => false;

@override
String toString() => 'GlobalCupertinoLocalizations.delegate(${kCupertinoSupportedLanguages.length} locales)';
}

源码中加载语言也没说不支持中文啊!!
还有网上很多配置本地化时候都是这么写的:


          GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,

仔细看了源码,我想说:
这么写不香么??
GlobalMaterialLocalizations.delegates



/// A value for [MaterialApp.localizationsDelegates] that's typically used by
/// internationalized apps.
///
/// ## Sample code
///
/// To include the localizations provided by this class and by
/// [GlobalWidgetsLocalizations] in a [MaterialApp],
/// use [GlobalMaterialLocalizations.delegates] as the value of
/// [MaterialApp.localizationsDelegates], and specify the locales your
/// app supports with [MaterialApp.supportedLocales]:
///
/// ```dart
/// new MaterialApp(
/// localizationsDelegates: GlobalMaterialLocalizations.delegates,
/// supportedLocales: [
/// const Locale('en', 'US'), // English
/// const Locale('he', 'IL'), // Hebrew
/// ],
/// // ...
/// )
/// ```
static const List> delegates = >[
GlobalCupertinoLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
];
}

仅此一篇文章,我希望大家认真阅读源码,提升水平

收起阅读 »

实现穿梭栈帧的魔法--协程

1. 协程-穿梭栈帧的魔法 协程的特性是代码中调用一次,实际会执行2次,第一次如果不满足条件先return一个状态,当满足条件的时候线程池会回调该方法执行第二次,而且还具有单例特性(指该函数第一次和第二次执行期间共享同一个上下文),妥妥的操作栈帧的魔法师。 2...
继续阅读 »

1. 协程-穿梭栈帧的魔法


协程的特性是代码中调用一次,实际会执行2次,第一次如果不满足条件先return一个状态,当满足条件的时候线程池会回调该方法执行第二次,而且还具有单例特性(指该函数第一次和第二次执行期间共享同一个上下文),妥妥的操作栈帧的魔法师。


2. 如何实现协程


前提:本文仅探讨kotlin协程实现


其实在反编译suspend函数反编译后就能知道协程的实现原理(以下)


github.com/yujinyan/ko…


//协程代码
//suspend fun foo() :Any{
// delay(3000L)
// val value =getCurrentTime()
// Log.e("TAG", "result is $value")
//}
//等价代码
@suspend fun foo() {
foo(object : Continuation<Any> {
override fun resumeWith(result: Result<Any>) {
val value = result.getOrThrow()
Log.e("TAG", "result is $value")
}
})
}

@suspend fun foo(continuation: Continuation<Any>): Any {
class FooContinuation : Continuation<Any> {
var label: Int = 0

override fun resumeWith(result: Result<Any>) {
val outcome = invokeSuspend()
if (outcome === COROUTINE_SUSPENDED) return
continuation.resume(result.getOrThrow())
}

fun invokeSuspend(): Any {
return foo(this)
}
}

val cont = (continuation as? FooContinuation) ?: FooContinuation()
return when (cont.label) {
0 -> {
cont.label++
//异步延时任务
AppExecutors.newInstance().otherIO.execute {
Thread.sleep(3000L)
val value = getCurrentTime()
cont.resume(value)
}
COROUTINE_SUSPENDED
}
1 -> 1 // return 1
else -> error("shouldn't happen")
}
}

核心就是函数内匿名内部类的巧用,真的很妙



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

构建Java IO框架体系

IO框架 Java IO的学习是一件非常艰巨的任务。 它的挑战是来自于要覆盖所有的可能性。不仅存在各种I/O源端还有想要和他通信的接收端(文件/控制台/网络链接),而且还需要以不同的方式与他们进行通信(顺序/随机存取/缓冲/二进制/字符/行/字 等等)这...
继续阅读 »

IO框架


	Java IO的学习是一件非常艰巨的任务。

它的挑战是来自于要覆盖所有的可能性。不仅存在各种I/O源端还有想要和他通信的接收端(文件/控制台/网络链接),而且还需要以不同的方式与他们进行通信(顺序/随机存取/缓冲/二进制/字符/行/字 等等)这些情况综合起来就给我们带来了大量的学习任务,大量的类需要学习。


我们要学会所有的这些java 的IO是很难的,因为我们没有构建一个关于IO的体系,要构建这个体系又需要深入理解IO库的演进过程,所以,我们如果缺乏历史的眼光,很快我们会对什么时候应该使用IO中的哪些类,以及什么时候不该使用它们而困惑。


所以,在开发者的眼中,IO很乱,很多类,很多方法,很迷茫。


IO简介


数据流是一组有序,有起点和终点的字节的数据序列。包括输入流和输出流。


流序列中的数据既可以是未经加工的原始二进制数据,也可以是经一定编码处理后符合某种格式规定的特定数据。因此Java中的流分为两种: **1) 字节流:**数据流中最小的数据单元是字节 **2) 字符流:**数据流中最小的数据单元是字符, Java中的字符是Unicode编码,一个字符占用两个字节。


Java.io包中最重要的就是5个类和一个接口。5个类指的是File、OutputStream、InputStream、Writer、Reader;一个接口指的是Serializable。掌握了这些就掌握了Java I/O的精髓了。


Java I/O主要包括如下3层次:


  1. 流式部分——最主要的部分。如:OutputStream、InputStream、Writer、Reader等

  2. 非流式部分——如:File类、RandomAccessFile类和FileDescriptor等类

  3. 其他——文件读取部分的与安全相关的类,如:SerializablePermission类,以及与本地操作系统相关的文件系统的类,如:FileSystem类和Win32FileSystem类和WinNTFileSystem类。


[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uFYWs0jZ-1638951173815)(F:\001_优秀课题\29_Java IO\IO图谱.png)]


IO详细介绍


在Android 平台,从应用的角度出发,我们最需要关注和研究的就是 字节流(Stream)字符流(Reader/Writer)和 File/ RandomAccessFile。当我们需要的时候再深入研究也未尝不是一件好事。关于字符和字节,例如文本文件,XML这些都是用字符流来读取和写入。而如RAR,EXE文件,图片等非文本,则用字节流来读取和写入。面对如此复杂的类关系,有一个点是我们必须要首先掌握的,那就是设计模式中的修饰模式,学会并理解修饰模式是搞懂流必备的前提条件哦。


字节流的学习


在具体的学习流之前,我们必须要学的一个设计模式是装饰模式。因为从流的整个发展历史,出现的各种类之间的关系看,都是沿用了修饰模式,都是一个类的功能可以用来修饰其他类,然后组合成为一个比较复杂的流。比如说:


     	DataOutputStream out = new DataOutputStream(
new BufferedOutputStream(
new FileOutputStream(file)));

从上面的代码块中大家不难看出这些类的关系:为了向文件中写入数据,首先需要创建一个FileOutputStream,然后为了提升访问的效率,所以将它发送给具备缓存功能的BufferedOutput-Stream,而为了实现与机器类型无关的java基本类型数据的输出,所以,我们将缓存的流传递给了DataOutputStream。从上面的关系,我们可以看到,其根本目的都是为outputSteam添加额外的功能。而这种额外功能的添加就是采用了装饰模式来构建的代码。因此,学习流,必须要学好装饰模式。


下面的图是一个关于字节流的图谱,这张图谱比较全面的概况了我们字节流中间的各个类以及他们之间的关系。


输入输出流.jpg


字节流的学习过程


为什么要按照一个学习路线来呢?原因是他们的功能决定的。


OutputStream -> FileOutputStream/FilterOutputStream ->DataOutputStream->bufferedOutputStream


相应的学习InputStream方法就好了。


从学习的角度来,我们应该先掌握FilterOutputStream, 以及FileOutputStream,这两个类是基本的类,从继承关系可以不难发现他们都是对 abstract 类 OutputStream的拓展,是它的子类。然而,伴随着 对 Stream流的功能的拓展,所以就出现了 DataOutputStream,(将java中的基础数据类型写入数据字节输出流中、保存在存储介质中、然后可以用DataOutputStream从存储介质中读取到程序中还原成java基础类型)。这里多提一句、DataOutputStream、FilterOutputStream三个类的关系的这种设计既使用了装饰器模式 避免了类的爆炸式增长。


为了提升Stream的执行效率,所以出现了bufferedOutputStream。bufferedOutputStream就是将本地添加了一个缓存的数组。在使用bufferedOutputStream之前每次从磁盘读入数据的时候都是需要访问多少byte数据就向磁盘中读多少个byte的数据,而出现bufferedOutputSteam之后,策略就改了,会先读取整个缓存空间相应大小的数据,这样就是从磁盘读取了一块比较大的数据,然后缓存起来,从而减少了对磁盘的访问的次数以达到提升性能的目的。


另外一方面,我们知道了outputStream(输出流)的发展历史后,我们便可以知道如何使用outpuSteam了,同样的方法,我们可以运用到inputStream中来,这样对称的解释就出现到了inputStream相关的中来了,于是,我们对整个字节流就有了全方位的理解,所以这样子我们就不会感觉到流的复杂了。这个时候对于其他的一些字节流的使用(byteArrayOutputStream/PipeOutputStream/ObjectOutputStream)的学习就自需要在使用的时候看看API即可。


字符流的学习


下图则是一个关于字符流的图谱,这张图谱比较全面的概况了我们字符流中间的各个类以及他们之间的关系。


字符输入输出流.jpg


字符流的学习和字节流的学习是一样的,它和字节流有着同样的发展过程,只是,字节流面向的是我们未知或者即使知道了他们的编码格式也意义不大的文件(png,exe, zip)的时候是采用字节,而面对一些我们知道文件构造我们就能够搞懂它的意义的文件(json,xml)等文件的时候我们还是需要以字符的形式来读取,所以就出现了字符流。reader 和 Stream最大的区别我认为是它包含了一个readline()接口,这个接口标明了,一行数据的意义,这也是可以理解的,因为自有字符才具备行的概念,相反字节流中的行也就是一个字节符号。


字符流的学习历程:


Writer- >FilterWriter->BufferedWriter->OutputStreamWriter->FileWriter->其他


同时类比着学习Reader相关的类。


FilterWriter/FilterReader

字符过滤输出流、与FilterOutputStream功能一样、只是简单重写了父类的方法、目的是为所有装饰类提供标准和基本的方法、要求子类必须实现核心方法、和拥有自己的特色。这里FilterWriter没有子类、可能其意义只是提供一个接口、留着以后的扩展。。。本身是一个抽象类。


BufferedWriter/BufferedReader

BufferedWriter是 Writer类的一个子类。他的功能是为传入的底层字符输出流提供缓存功能、同样当使用底层字符输出流向目的地中写入字符或者字符数组时、每写入一次就要打开一次到目的地的连接、这样频繁的访问不断效率底下、也有可能会对存储介质造成一定的破坏、比如当我们向磁盘中不断的写入字节时、夸张一点、将一个非常大单位是G的字节数据写入到磁盘的指定文件中的、没写入一个字节就要打开一次到这个磁盘的通道、这个结果无疑是恐怖的、而当我们使用BufferedWriter将底层字符输出流、比如FileReader包装一下之后、我们可以在程序中先将要写入到文件中的字符写入到BufferedWriter的内置缓存空间中、然后当达到一定数量时、一次性写入FileReader流中、此时、FileReader就可以打开一次通道、将这个数据块写入到文件中、这样做虽然不可能达到一次访问就将所有数据写入磁盘中的效果、但也大大提高了效率和减少了磁盘的访问量!


OutputStreamWriter/InputStreamReader

输入字符转换流、是输入字节流转向输入字符流的桥梁、用于将输入字节流转换成输入字符流、通过指定的或者默认的编码将从底层读取的字节转换成字符返回到程序中、与OutputStreamWriter一样、本质也是使用其内部的一个类来完成所有工作:StreamDecoder、使用默认或者指定的编码将字节转换成字符;OutputStreamWriter/ InputStreamReader只是对StreamDecoder进行了封装、isr内部所有方法核心都是调用StreamDecoder来完成的、InputStreamReader只是对StreamDecoder进行了封装、使得我们可以直接使用读取方法、而不用关心内部实现。


	OutputStreamWriter、InputStreamReader分别为InputStream、OutputStream的低级输入输出流提供将字节转换成字符的桥梁、他们只是外边的一个门面、真正的核心:

OutputStreamWriter中的StreamEncoder:


         1、使用指定的或者默认的编码集将字符转码为字节        

2、调用StreamEncoder自身实现的写入方法将转码后的字节写入到底层字节输出流中。

InputStreamReader中的StreamDecoder:


        1、使用指定的或者默认的编码集将字节解码为字符         

2、调用StreamDecoder自身实现的读取方法将解码后的字符读取到程序中。

在理解这两个流的时候要注意:java——io中只有将字节转换成字符的类、没有将字符转换成字节的类、原因很简单——字符流的存在本来就像对字节流进行了装饰、加工处理以便更方便的去使用、在使用这两个流的时候要注意:由于这两个流要频繁的对读取或者写入的字节或者字符进行转码、解码和与底层流的源和目的地进行交互、所以使用的时候要使用BufferedWriter、BufferedReader进行包装、以达到最高效率、和保护存储介质。


FileReader/FileWriter

FileReader和FileWriter 继承于InputStreamReader/OutputStreamWriter。


从源码可以发现FileWriter 文件字符输出流、主要用于将字符写入到指定的打开的文件中、其本质是通过传入的文件名、文件、或者文件描述符来创建FileOutputStream、然后使用OutputStreamWriter使用默认编码将FileOutputStream转换成Writer(这个Writer就是FileWriter)。如果使用这个类的话、最好使用BufferedWriter包装一下、高端大气上档次、低调奢华有内涵!


FileReader 文件字符输入流、用于将文件内容以字符形式读取出来、一般用于读取字符形式的文件内容、也可以读取字节形式、但是因为FileReader内部也是通过传入的参数构造InputStreamReader、并且只能使用默认编码、所以我们无法控制编码问题、这样的话就很容易造成乱码。所以读取字节形式的文件还是使用字节流来操作的好、同样在使用此流的时候用BufferedReader包装一下、就算冲着BufferedReader的readLine()方法去的也要使用这个包装类、不说他还能提高效率、保护存储介质。


字节流与字符流的关系


那么字节输入流和字符输入流之间的关系是怎样的呢?请看下图


字节与字符输入流.jpg


同样的字节与字符输出流字节的关系也如下图所示


字节与字符输出流.jpg


字节流与字符流的区别


字节流和字符流使用是非常相似的,那么除了操作代码的不同之外,还有哪些不同呢?


  字节流在操作的时候本身是不会用到缓冲区(内存)的,是与文件本身直接操作的,而字符流在操作的时候是使用到缓冲区的字节流在操作文件时,即使不关闭资源(close方法),文件也能输出,但是如果字符流不使用close方法的话,则不会输出任何内容,说明字符流用的是缓冲区,并且可以使用flush方法强制进行刷新缓冲区,这时才能在不close的情况下输出内容


  那开发中究竟用字节流好还是用字符流好呢?

  在所有的硬盘上保存文件或进行传输的时候都是以字节的方法进行的,包括图片也是按字节完成,而字符是只有在内存中才会形成的,所以使用字节的操作是最多的。


  如果要java程序实现一个拷贝功能,应该选用字节流进行操作(可能拷贝的是图片),并且采用边读边写的方式(节省内存)。


字节流与字符流的转换


虽然Java支持字节流和字符流,但有时需要在字节流和字符流两者之间转换。InputStreamReader和OutputStreamWriter,这两个为类是字节流和字符流之间相互转换的类。


  InputSreamReader用于将一个字节流中的字节解码成字符:

  有两个构造方法: 


   InputStreamReader(InputStream in);

  功能:用默认字符集创建一个InputStreamReader对象


   InputStreamReader(InputStream in,String CharsetName);

  功能:接收已指定字符集名的字符串,并用该字符创建对象


  OutputStream用于将写入的字符编码成字节后写入一个字节流。

  同样有两个构造方法


  OutputStreamWriter(OutputStream out);

  功能:用默认字符集创建一个OutputStreamWriter对象;   


  OutputStreamWriter(OutputStream out,String  CharSetName);

  功能:接收已指定字符集名的字符串,并用该字符集创建OutputStreamWrite对象


为了避免频繁的转换字节流和字符流,对以上两个类进行了封装。


  BufferedWriter类封装了OutputStreamWriter类;


  BufferedReader类封装了InputStreamReader类;


  封装格式


  BufferedWriter out=new BufferedWriter(new OutputStreamWriter(System.out));
BufferedReader in= new BufferedReader(new InputStreamReader(System.in);

  利用下面的语句,可以从控制台读取一行字符串:


  BufferedReader in=new BufferedReader(new InputStreamReader(System.in));
String line=in.readLine();


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

kotlin 与java 互操作

简介 大多数情况下,你不需要关注这个问题。但是,如果你的代码中包含了部分Java代码,了解这些可能帮你解决一些棘手的问题,同时让你设计的Api更加可靠 互操作性与可空性 Java世界里所有对象都可能是null,当一个kotlin函数返回string类型值,你不...
继续阅读 »

简介


大多数情况下,你不需要关注这个问题。但是,如果你的代码中包含了部分Java代码,了解这些可能帮你解决一些棘手的问题,同时让你设计的Api更加可靠


互操作性与可空性


Java世界里所有对象都可能是null,当一个kotlin函数返回string类型值,你不能想当然地认为它的返回值就能符合kotlin关于空值的规定


kotlin


fun main() {
val my = MyClass()
val value = my.getCanNullValue()
println(value?.capitalize())
}

java


public class MyClass {
public String value;

public String getCanNullValue(){
return value;
}
}

类型映射


代码运行时,所有的映射类型都会重新映射回对应的java类型


fun main() {
val my = MyClass()
my.value = "a123"
val value = my.getCanNullValue()
println(value.javaClass)
}

结果为:class java.lang.String


属性访问


不需要调用相关setter方法,你可以使用赋值语法来设置一个java字段值了


val my = MyClass()
my.value = "a123"

@JvmName


这个注解可以改变字节码中生成的类名或方法名称,如果作用在顶级作用域(文件中),则会改变生成对应Java类的名称。如果作用在方法上,则会改变生成对应Java方法的名称。


kotlin


@file:JvmName("FooKt")
@JvmName("foo1")
fun foo() {
println("Hello, Jvm...")
}

java


// 相当于下面的Java代码
public final class FooKt {
public static final void foo1() {
String var0 = "Hello, Jvm...";
System.out.println(var0);
}
}

第一个注解@file:JvmName("FooKt")的作用是使生成的类名变为FooKt,第二个注解的作用是使生成的方法名称变为foo1


@JvmField


Kotlin编译器默认会将类中声明的成员变量编译成私有变量,Java语言要访问该变量必须通过其生成的getter方法。而使用上面的注解可以向Java暴露该变量,即使其访问变为公开(修饰符变为public)。


Kotlin


class JavaToKotlin {
@JvmField
val info = "Hello"
}

@JvmOverloads


由于Kotlin语言支持方法参数默认值,而实现类似功能Java需要使用方法重载来实现,这个注解就是为解决这个问题而生的,添加这个注解会自动生成重载方法


Kotlin


@JvmOverloads
fun prinltInfo(name: String, age: Int = 1) {
println("$name $age")
}

java


 public static void main(String[] args) {
MyKotlin.prinltInfo("arrom");
MyKotlin.prinltInfo("arrom", 20);
}

@JvmStatic


@JvmStatic注解的作用类似于@JvmField,可以直接调用伴生对象里的函数


class JavaToKotlin {
@JvmField
val info = "Hello"

companion object {
@JvmField
val max: Int = 200

@JvmStatic
fun loadConfig(): String {
return "loading config"
}
}
}

@Throws


由于Kotlin语言不支持CE(Checked Exception),所谓CE,即方法可能抛出的异常是已知的。Java语言通过throws关键字在方法上声明CE。为了兼容这种写法,Kotlin语言新增了@Throws注解,该注解的接收一个可变参数,参数类型是多个异常的KClass实例。Kotlin编译器通过读取注解参数,在生成的字节码中自动添加CE声明。


Kotlin


@Throws(IllegalArgumentException::class)
fun div(x: Int, y: Int): Float {
return x.toFloat() / y
}

Java


// 生成的代码相当于下面这段Java代码
public static final float div(int x, int y) throws IllegalArgumentException {
return (float)x / (float)y;
}

添加了@Throws(IllegalArgumentException::class)注解后,在生成的方法签名上自动添加了可能抛出的异常声明(throws IllegalArgumentException),即CE。


@Synchronized


用于产生同步方法。Kotlin语言不支持synchronized关键字,处理类似Java语言的并发问题,Kotlin语言建议使用同步方法进行处理


Kotlin


@Synchronized
fun start() {
println("Start do something...")
}

java


// 生成的代码相当于下面这段Java代码
public static final synchronized void start() {
String var0 = "Start do something...";
System.out.println(var0);
}

函数类型操作


Java中没有函数类型,所以,在Java里,kotlin函数类型使用FunctionN这样的名字的接口来表示,N代表入参的个数,一共有24个这样的接口,从Function0到Function23,每个接口都包含一个invoke函数,调用匿名函数需要调用invoke


kotlin:


val funcp:(String) -> String = {
it.capitalize()
}

java:


Function1 funcp = ArromKt.getFuncp();
funcp.invoke("arrom");

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

LiveData学习记

LiveData 使用 var liveData: MutableLiveData<String>? = null //初始化 liveData = MutableLiveData() // 设置 observe liveData?.observe...
继续阅读 »

LiveData 使用


var liveData: MutableLiveData<String>? = null
//初始化
liveData = MutableLiveData()
// 设置 observe
liveData?.observe(this, {
Log.e("Main2", "2界面接收数据 = $it")
Toast.makeText(this, "2界面接收数据 = $it", Toast.LENGTH_LONG).show()
})
// 发送值
liveData?.value = "2界面发送数据 $indexValue"

LiveData 是针对同一个界面数据相互传递, 配合 MVVM 使用


如果想跨界面使用 比如 Activity1 想传值 给 Activity2 可以把LiveData 下沉(二次封装)


package com.one_hour.test_livedata
import androidx.lifecycle.MutableLiveData
object LiveDataBusBeta{
//创建一个Map 管理 LiveData
private val liveDataMap: MutableMap<String, MutableLiveData<Any>> = HashMap()
// 设置一个 key
fun <T> getLiveData(key: String) : MutableLiveData<T>? {
if (!liveDataMap.containsKey(key)) {
liveDataMap.put(key, MutableLiveData<Any>())
}
return liveDataMap[key] as MutableLiveData<T>
}
fun removeMapLiveData(key : String) {
liveDataMap.remove(key)
}
}

像这样下沉后会出现 Bug, 如场景:当界面Activity1 向未创建的Activity2 发送消息时,会在Activity2 创建时 出现从界面1传过来的数据,这是我们不需要的。(现象出现叫 消息粘性)


什么是粘性事件

即发射的事件如果早于注册,那么注册之后依然可以接收到的事件称为粘性事件


消息粘性 咋个形成的 ?
先创建 new MutableLiveData -> setValue -> observe(绑定监听)


LiveData 绑定(observe)源码

    @MainThread
public void observe(@NonNull LifecycleOwner owner, @NonNull Observer<? super T> observer) {
assertMainThread("observe");
if (owner.getLifecycle().getCurrentState() == DESTROYED) {
// ignore
return;
}
LifecycleBoundObserver wrapper = new LifecycleBoundObserver(owner, observer);
ObserverWrapper existing = mObservers.putIfAbsent(observer, wrapper);
if (existing != null && !existing.isAttachedTo(owner)) {
throw new IllegalArgumentException("Cannot add the same observer"
+ " with different lifecycles");
}
if (existing != null) {
return;
}
owner.getLifecycle().addObserver(wrapper);
}


1.


owner.getLifecycle() 获取的是 Lifecycle 监听Activity 生命周期变化的流程
androidx.appcompat.app.AppCompatActivity (继承)-> androidx.fragment.app.FragmentActivity (继承)-> androidx.activity.ComponentActivity (继承)->androidx.core.app.ComponentActivity( 实现 LifecycleOwner) -> 现在 实例化 private LifecycleRegistry mLifecycleRegistry = new LifecycleRegistry(this);


androidx.core.app.ComponentActivity( 实现 LifecycleOwner)


@CallSuper
@Override
protected void onSaveInstanceState(@NonNull Bundle outState) {
//添加一个 mLifecycleRegistry 状态管理
mLifecycleRegistry.markState(Lifecycle.State.CREATED);
super.onSaveInstanceState(outState);
}

androidx.activity.ComponentActivity( 实现 LifecycleOwner)


    @CallSuper
@Override
protected void onSaveInstanceState(@NonNull Bundle outState) {
Lifecycle lifecycle = getLifecycle();
//设置 lifecycle 当前状态
if (lifecycle instanceof LifecycleRegistry) {
((LifecycleRegistry) lifecycle).setCurrentState(Lifecycle.State.CREATED);
}
super.onSaveInstanceState(outState);
mSavedStateRegistryController.performSave(outState);
}

androidx.fragment.app.FragmentActivity


final LifecycleRegistry mFragmentLifecycleRegistry = new LifecycleRegistry(this);
//开始绑定什么周期 调用 handleLifecycleEvent 绑定状态
mFragmentLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.xxx);

2.


** owner.getLifecycle().addObserver(wrapper); 中 addObserver 调用了 androidx.lifecycle.LifecycleRegistry的 addObserver,而LifecycleRegistry是在FragmentActivity类中实例化获取**


    @Override
public void addObserver(@NonNull LifecycleObserver observer) {
State initialState = mState == DESTROYED ? DESTROYED : INITIALIZED;
ObserverWithState statefulObserver = new ObserverWithState(observer, initialState);
ObserverWithState previous = mObserverMap.putIfAbsent(observer, statefulObserver);

if (previous != null) {
return;
}
LifecycleOwner lifecycleOwner = mLifecycleOwner.get();
if (lifecycleOwner == null) {
// it is null we should be destroyed. Fallback quickly
return;
}

boolean isReentrance = mAddingObserverCounter != 0 || mHandlingEvent;
State targetState = calculateTargetState(observer);
mAddingObserverCounter++;
while ((statefulObserver.mState.compareTo(targetState) < 0
&& mObserverMap.contains(observer))) {
pushParentState(statefulObserver.mState);
statefulObserver.dispatchEvent(lifecycleOwner, upEvent(statefulObserver.mState));
popParentState();
// mState / subling may have been changed recalculate
targetState = calculateTargetState(observer);
}
、、、、、省略代码
}

statefulObserver.dispatchEvent(lifecycleOwner, upEvent(statefulObserver.mState)); 在循环中一直调用


    static class ObserverWithState {
State mState;
LifecycleEventObserver mLifecycleObserver;

ObserverWithState(LifecycleObserver observer, State initialState) {
mLifecycleObserver = Lifecycling.lifecycleEventObserver(observer);
mState = initialState;
}

void dispatchEvent(LifecycleOwner owner, Event event) {
State newState = getStateAfter(event);
mState = min(mState, newState);
mLifecycleObserver.onStateChanged(owner, event);
mState = newState;
}
}

LifecycleBoundObserver wrapper = new LifecycleBoundObserver(owner, observer); 监听状态改变 并且
在ObserverWithState 中调用了 mLifecycleObserver.onStateChanged(owner, event); -》mLifecycleObserver 指的就是 LifecycleBoundObserver


class LifecycleBoundObserver extends ObserverWrapper implements GenericLifecycleObserver


        @Override
public void onStateChanged(LifecycleOwner source, Lifecycle.Event event) {
if (mOwner.getLifecycle().getCurrentState() == DESTROYED) {
removeObserver(mObserver);
return;
}
// 这里是如果状态 是可见的 那么就发送消息
// 就调用 class LifecycleBoundObserver extends ObserverWrapper 父类 ObserverWrapper 的方法
//shouldBeActive() 获取 mOwner.getLifecycle().getCurrentState().isAtLeast(STARTED); 状态 是否可见
activeStateChanged(shouldBeActive());
}

class LifecycleBoundObserver extends ObserverWrapper 父类 ObserverWrapper 的方法 并分发 dispatchingValue 值


        void activeStateChanged(boolean newActive) {
if (newActive == mActive) {
return;
}
// immediately set active state, so we'd never dispatch anything to inactive
// owner
mActive = newActive;
boolean wasInactive = LiveData.this.mActiveCount == 0;
LiveData.this.mActiveCount += mActive ? 1 : -1;
if (wasInactive && mActive) {
onActive();
}
if (LiveData.this.mActiveCount == 0 && !mActive) {
onInactive();
}
if (mActive) {
// 调用 dispatchingValue 回到 abstract class LiveData<T> 类里面的 dispatchingValue 方法
dispatchingValue(this);
}
}
}

dispatchingValue 都调用了相同的函数 considerNotify


    void dispatchingValue(@Nullable ObserverWrapper initiator) {
if (mDispatchingValue) {
mDispatchInvalidated = true;
return;
}
mDispatchingValue = true;
do {
mDispatchInvalidated = false;
if (initiator != null) {
considerNotify(initiator);
initiator = null;
} else {
for (Iterator<Map.Entry<Observer<? super T>, ObserverWrapper>> iterator =
mObservers.iteratorWithAdditions(); iterator.hasNext(); ) {
considerNotify(iterator.next().getValue());
if (mDispatchInvalidated) {
break;
}
}
}
} while (mDispatchInvalidated);
mDispatchingValue = false;
}

considerNotify 中 observer.mObserver.onChanged 回调数据


    private void considerNotify(ObserverWrapper observer) {
if (!observer.mActive) {
return;
}
// Check latest state b4 dispatch. Maybe it changed state but we didn't get the event yet.
//
// we still first check observer.active to keep it as the entrance for events. So even if
// the observer moved to an active state, if we've not received that event, we better not
// notify for a more predictable notification order.
if (!observer.shouldBeActive()) {
observer.activeStateChanged(false);
return;
}
if (observer.mLastVersion >= mVersion) {
return;
}
observer.mLastVersion = mVersion;
//noinspection unchecked
observer.mObserver.onChanged((T) mData);
}

解决粘性代码


  • 方法1


import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Observer


class BaseLiveData<T> : MutableLiveData<T>() {
private var isSticky: Boolean = false
private var mStickyData: T? = null
private var mVersion = 0

override fun observe(owner: LifecycleOwner, observer: Observer<in T>) {
if (isSticky) {
super.observe(owner, observer)
} else {
super.observe(owner, CustomObserver<T>(this, observer))
}

}

/**
* 发送非粘性数据
*/
override fun setValue(value: T) {
mVersion++
isSticky = false
super.setValue(value)
}

override fun postValue(value: T) {
mVersion++
isSticky = false
super.postValue(value)
}

/**
* 发送粘性数据
*/
fun setStickyData(data: T?) {
mStickyData = data
isSticky = true
setValue(data!!)
}

fun postStickyData(mStickyData: T?) {
this.mStickyData = mStickyData
isSticky = true
super.postValue(mStickyData!!)
}

inner class CustomObserver<T>(val mLiveData: BaseLiveData<T>, var mObserver: Observer<in T>?,
var isSticky: Boolean = false) : Observer<T> {

private var mLastVersion = mLiveData.mVersion

override fun onChanged(t: T) {
if (mLastVersion >= mLiveData.mVersion) {
if (isSticky && mLiveData.mStickyData != null) {
mObserver?.onChanged(mLiveData.mStickyData)
}
return
}
mLastVersion = mLiveData.mVersion
mObserver?.onChanged(t)

}

}
}


  • 方法2


利用反射 修改 observer.mLastVersion 值
observer.mLastVersion 的 获取值的调用链 :
observer.mLastVersion -》considerNotify (iterator.next().getValue()) -> mObservers (SafeIterableMap<Observer<? super T>, ObserverWrapper> mObservers = new SafeIterableMap<>()) -> ObserverWrapper(int mLastVersion = START_VERSION;) (子类LifecycleBoundObserver, 但是只有父类 ObserverWrapper 才有 mLastVersion, 所以获取父类的 mLastVersion 进行修改)


import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Observer
import java.lang.reflect.Field
import java.lang.reflect.Method


class BaseUnStickyLiveData<T> : MutableLiveData<T>() {

private var isSticky = false

override fun observe(owner: LifecycleOwner, observer: Observer<in T>) {
super.observe(owner, observer)
if (!isSticky) {
hookClass(observer)
}
}

override fun setValue(value: T) {
isSticky = false
super.setValue(value)
}

override fun postValue(value: T) {
isSticky = false
super.postValue(value)
}

fun setStickyValue(value: T) {
isSticky = true
super.setValue(value)
}

fun setStickyPostValue(value: T) {
isSticky = true
super.postValue(value)
}

private fun hookClass(observer: Observer<in T>) {
val liveDataClass = LiveData::class.java
try {
//获取field private SafeIterableMap<Observer<T>, ObserverWrapper> mObservers
val mObservers: Field = liveDataClass.getDeclaredField("mObservers")
mObservers.setAccessible(true)

//获取SafeIterableMap集合mObservers
val observers: Any = mObservers.get(this)

//获取SafeIterableMap的get(Object obj)方法
val observersClass: Class<*> = observers.javaClass
val methodGet: Method = observersClass.getDeclaredMethod("get", Any::class.java)
methodGet.setAccessible(true)

//获取到observer在集合中对应的ObserverWrapper对象
val objectWrapperEntry: Any = methodGet.invoke(observers, observer)
var objectWrapper: Any? = null
if (objectWrapperEntry is Map.Entry<*, *>) {
objectWrapper = objectWrapperEntry.value
}
if (objectWrapper == null) {
//throw NullPointerException("ObserverWrapper can not be null")
return
}

// 获取ListData的mVersion
val mVersion: Field = liveDataClass.getDeclaredField("mVersion")
mVersion.setAccessible(true)
val mVersionValue: Any = mVersion.get(this)

//获取ObserverWrapper的Class对象 LifecycleBoundObserver extends ObserverWrapper
val wrapperClass: Class<*> = objectWrapper.javaClass.superclass

//获取ObserverWrapper的field mLastVersion
val mLastVersion: Field = wrapperClass.getDeclaredField("mLastVersion")
mLastVersion.setAccessible(true)

//把当前ListData的mVersion赋值给 ObserverWrapper的field mLastVersion
mLastVersion.set(objectWrapper, mVersionValue)
} catch (e: Exception) {
if (BuildConfig.DEBUG) {
throw RuntimeException(e)
} else {
e.printStackTrace()
}
}
}
}

配合二次 封装的 LiveDataBusBeta 使用


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

看一遍就理解:动态规划详解

前言 我们刷leetcode的时候,经常会遇到动态规划类型题目。动态规划问题非常非常经典,也很有技巧性,一般大厂都非常喜欢问。今天跟大家一起来学习动态规划的套路,文章如果有不正确的地方,欢迎大家指出哈,感谢感谢~ 什么是动态规划? 动态规划的核心思想 一个例...
继续阅读 »

前言


我们刷leetcode的时候,经常会遇到动态规划类型题目。动态规划问题非常非常经典,也很有技巧性,一般大厂都非常喜欢问。今天跟大家一起来学习动态规划的套路,文章如果有不正确的地方,欢迎大家指出哈,感谢感谢~



  • 什么是动态规划?

  • 动态规划的核心思想

  • 一个例子走进动态规划

  • 动态规划的解题套路

  • leetcode案例分析



公众号:捡田螺的小男孩


什么是动态规划?


动态规划(英语:Dynamic programming,简称 DP),是一种在数学、管理科学、计算机科学、经济学和生物信息学中使用的,通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法。动态规划常常适用于有重叠子问题和最优子结构性质的问题。



dynamic programming is a method for solving a complex problem by breaking it down into a collection of simpler subproblems.



以上定义来自维基百科,看定义感觉还是有点抽象。简单来说,动态规划其实就是,给定一个问题,我们把它拆成一个个子问题,直到子问题可以直接解决。然后呢,把子问题答案保存起来,以减少重复计算。再根据子问题答案反推,得出原问题解的一种方法。



一般这些子问题很相似,可以通过函数关系式递推出来。然后呢,动态规划就致力于解决每个子问题一次,减少重复计算,比如斐波那契数列就可以看做入门级的经典动态规划问题。



动态规划核心思想


动态规划最核心的思想,就在于拆分子问题,记住过往,减少重复计算


动态规划在于记住过往


我们来看下,网上比较流行的一个例子:




  • A : "1+1+1+1+1+1+1+1 =?"

  • A : "上面等式的值是多少"

  • B : 计算 "8"

  • A : 在上面等式的左边写上 "1+" 呢?

  • A : "此时等式的值为多少"

  • B : 很快得出答案 "9"

  • A : "你怎么这么快就知道答案了"

  • A : "只要在8的基础上加1就行了"

  • A : "所以你不用重新计算,因为你记住了第一个等式的值为8!动态规划算法也可以说是 '记住求过的解来节省时间'"



一个例子带你走进动态规划 -- 青蛙跳阶问题


暴力递归



leetcode原题:一只青蛙一次可以跳上1级台阶,也可以跳上2级台阶。求该青蛙跳上一个 10 级的台阶总共有多少种跳法。



有些小伙伴第一次见这个题的时候,可能会有点蒙圈,不知道怎么解决。其实可以试想:




  • 要想跳到第10级台阶,要么是先跳到第9级,然后再跳1级台阶上去;要么是先跳到第8级,然后一次迈2级台阶上去。

  • 同理,要想跳到第9级台阶,要么是先跳到第8级,然后再跳1级台阶上去;要么是先跳到第7级,然后一次迈2级台阶上去。

  • 要想跳到第8级台阶,要么是先跳到第7级,然后再跳1级台阶上去;要么是先跳到第6级,然后一次迈2级台阶上去。



假设跳到第n级台阶的跳数我们定义为f(n),很显然就可以得出以下公式:


f(10) = f(9)+f(8)
f (9) = f(8) + f(7)
f (8) = f(7) + f(6)
...
f(3) = f(2) + f(1)

即通用公式为: f(n) = f(n-1) + f(n-2)

那f(2) 或者 f(1) 等于多少呢?



  • 当只有2级台阶时,有两种跳法,第一种是直接跳两级,第二种是先跳一级,然后再跳一级。即f(2) = 2;

  • 当只有1级台阶时,只有一种跳法,即f(1)= 1;


因此可以用递归去解决这个问题:


class Solution {
public int numWays(int n) {
if(n == 1){
return 1;
}
if(n == 2){
return 2;
}
return numWays(n-1) + numWays(n-2);
}
}

去leetcode提交一下,发现有问题,超出时间限制了



为什么超时了呢?递归耗时在哪里呢?先画出递归树看看:




  • 要计算原问题 f(10),就需要先计算出子问题 f(9) 和 f(8)

  • 然后要计算 f(9),又要先算出子问题 f(8) 和 f(7),以此类推。

  • 一直到 f(2) 和 f(1),递归树才终止。


我们先来看看这个递归的时间复杂度吧:


递归时间复杂度 = 解决一个子问题时间*子问题个数


  • 一个子问题时间 = f(n-1)+f(n-2),也就是一个加法的操作,所以复杂度是 O(1);

  • 问题个数 = 递归树节点的总数,递归树的总节点 = 2^n-1,所以是复杂度O(2^n)。


因此,青蛙跳阶,递归解法的时间复杂度 = O(1) * O(2^n) = O(2^n),就是指数级别的,爆炸增长的,如果n比较大的话,超时很正常的了。


回过头来,你仔细观察这颗递归树,你会发现存在大量重复计算,比如f(8)被计算了两次,f(7)被重复计算了3次...所以这个递归算法低效的原因,就是存在大量的重复计算


既然存在大量重复计算,那么我们可以先把计算好的答案存下来,即造一个备忘录,等到下次需要的话,先去备忘录查一下,如果有,就直接取就好了,备忘录没有才开始计算,那就可以省去重新重复计算的耗时啦!这就是带备忘录的解法。


带备忘录的递归解法(自顶向下)


一般使用一个数组或者一个哈希map充当这个备忘录



  • 第一步,f(10)= f(9) + f(8),f(9) 和f(8)都需要计算出来,然后再加到备忘录中,如下:




  • 第二步, f(9) = f(8)+ f(7),f(8)= f(7)+ f(6), 因为 f(8) 已经在备忘录中啦,所以可以省掉,f(7),f(6)都需要计算出来,加到备忘录中~



第三步, f(8) = f(7)+ f(6),发现f(8),f(7),f(6)全部都在备忘录上了,所以都可以剪掉。



所以呢,用了备忘录递归算法,递归树变成光秃秃的树干咯,如下:



备忘录的递归算法,子问题个数=树节点数=n,解决一个子问题还是O(1),所以带备忘录的递归算法的时间复杂度是O(n)。接下来呢,我们用带备忘录的递归算法去撸代码,解决这个青蛙跳阶问题的超时问题咯~,代码如下:


public class Solution {
//使用哈希map,充当备忘录的作用
Map<Integer, Integer> tempMap = new HashMap();
public int numWays(int n) {
// n = 0 也算1种
if (n == 0) {
return 1;
}
if (n <= 2) {
return n;
}
//先判断有没计算过,即看看备忘录有没有
if (tempMap.containsKey(n)) {
//备忘录有,即计算过,直接返回
return tempMap.get(n);
} else {
// 备忘录没有,即没有计算过,执行递归计算,并且把结果保存到备忘录map中,对1000000007取余(这个是leetcode题目规定的)
tempMap.put(n, (numWays(n - 1) + numWays(n - 2)) % 1000000007);
return tempMap.get(n);
}
}
}

去leetcode提交一下,如图,稳了:



其实,还可以用动态规划解决这道题。


自底向上的动态规划


动态规划跟带备忘录的递归解法基本思想是一致的,都是减少重复计算,时间复杂度也都是差不多。但是呢:



  • 带备忘录的递归,是从f(10)往f(1)方向延伸求解的,所以也称为自顶向下的解法。

  • 动态规划从较小问题的解,由交叠性质,逐步决策出较大问题的解,它是从f(1)往f(10)方向,往上推求解,所以称为自底向上的解法。


动态规划有几个典型特征,最优子结构、状态转移方程、边界、重叠子问题。在青蛙跳阶问题中:



  • f(n-1)和f(n-2) 称为 f(n) 的最优子结构

  • f(n)= f(n-1)+f(n-2)就称为状态转移方程

  • f(1) = 1, f(2) = 2 就是边界啦

  • 比如f(10)= f(9)+f(8),f(9) = f(8) + f(7) ,f(8)就是重叠子问题。


我们来看下自底向上的解法,从f(1)往f(10)方向,想想是不是直接一个for循环就可以解决啦,如下:



带备忘录的递归解法,空间复杂度是O(n),但是呢,仔细观察上图,可以发现,f(n)只依赖前面两个数,所以只需要两个变量a和b来存储,就可以满足需求了,因此空间复杂度是O(1)就可以啦



动态规划实现代码如下:


public class Solution {
public int numWays(int n) {
if (n<= 1) {
return 1;
}
if (n == 2) {
return 2;
}
int a = 1;
int b = 2;
int temp = 0;
for (int i = 3; i <= n; i++) {
temp = (a + b)% 1000000007;
a = b;
b = temp;
}
return temp;
}
}

动态规划的解题套路


什么样的问题可以考虑使用动态规划解决呢?



如果一个问题,可以把所有可能的答案穷举出来,并且穷举出来后,发现存在重叠子问题,就可以考虑使用动态规划。



比如一些求最值的场景,如最长递增子序列、最小编辑距离、背包问题、凑零钱问题等等,都是动态规划的经典应用场景。


动态规划的解题思路


动态规划的核心思想就是拆分子问题,记住过往,减少重复计算。 并且动态规划一般都是自底向上的,因此到这里,基于青蛙跳阶问题,我总结了一下我做动态规划的思路:



  • 穷举分析

  • 确定边界

  • 找出规律,确定最优子结构

  • 写出状态转移方程


1. 穷举分析



  • 当台阶数是1的时候,有一种跳法,f(1) =1

  • 当只有2级台阶时,有两种跳法,第一种是直接跳两级,第二种是先跳一级,然后再跳一级。即f(2) = 2;

  • 当台阶是3级时,想跳到第3级台阶,要么是先跳到第2级,然后再跳1级台阶上去,要么是先跳到第 1级,然后一次迈 2 级台阶上去。所以f(3) = f(2) + f(1) =3

  • 当台阶是4级时,想跳到第3级台阶,要么是先跳到第3级,然后再跳1级台阶上去,要么是先跳到第 2级,然后一次迈 2 级台阶上去。所以f(4) = f(3) + f(2) =5

  • 当台阶是5级时......


自底向上的动态规划


2. 确定边界


通过穷举分析,我们发现,当台阶数是1的时候或者2的时候,可以明确知道青蛙跳法。f(1) =1,f(2) = 2,当台阶n>=3时,已经呈现出规律f(3) = f(2) + f(1) =3,因此f(1) =1,f(2) = 2就是青蛙跳阶的边界。


3. 找规律,确定最优子结构


n>=3时,已经呈现出规律 f(n) = f(n-1) + f(n-2) ,因此,f(n-1)和f(n-2) 称为 f(n) 的最优子结构。什么是最优子结构?有这么一个解释:



一道动态规划问题,其实就是一个递推问题。假设当前决策结果是f(n),则最优子结构就是要让 f(n-k) 最优,最优子结构性质就是能让转移到n的状态是最优的,并且与后面的决策没有关系,即让后面的决策安心地使用前面的局部最优解的一种性质



4, 写出状态转移方程


通过前面3步,穷举分析,确定边界,最优子结构,我们就可以得出状态转移方程啦:



5. 代码实现


我们实现代码的时候,一般注意从底往上遍历哈,然后关注下边界情况,空间复杂度,也就差不多啦。动态规划有个框架的,大家实现的时候,可以考虑适当参考一下:


dp[0][0][...] = 边界值
for(状态1 :所有状态1的值){
for(状态2 :所有状态2的值){
for(...){
//状态转移方程
dp[状态1][状态2][...] = 求最值
}
}
}

leetcode案例分析


我们一起来分析一道经典leetcode题目吧



给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。



示例 1:


输入:nums = [10,9,2,5,3,7,101,18]
输出:4
解释:最长递增子序列是 [2,3,7,101],因此长度为 4 。

示例 2:


输入:nums = [0,1,0,3,2,3]
输出:4

我们按照以上动态规划的解题思路,



  • 穷举分析

  • 确定边界

  • 找规律,确定最优子结构

  • 状态转移方程


1.穷举分析


因为动态规划,核心思想包括拆分子问题,记住过往,减少重复计算。 所以我们在思考原问题:数组num[i]的最长递增子序列长度时,可以思考下相关子问题,比如原问题是否跟子问题num[i-1]的最长递增子序列长度有关呢?


自顶向上的穷举

这里观察规律,显然是有关系的,我们还是遵循动态规划自底向上的原则,基于示例1的数据,从数组只有一个元素开始分析。



  • 当nums只有一个元素10时,最长递增子序列是[10],长度是1.

  • 当nums需要加入一个元素9时,最长递增子序列是[10]或者[9],长度是1。

  • 当nums再加入一个元素2时,最长递增子序列是[10]或者[9]或者[2],长度是1。

  • 当nums再加入一个元素5时,最长递增子序列是[2,5],长度是2。

  • 当nums再加入一个元素3时,最长递增子序列是[2,5]或者[2,3],长度是2。

  • 当nums再加入一个元素7时,,最长递增子序列是[2,5,7]或者[2,3,7],长度是3。

  • 当nums再加入一个元素101时,最长递增子序列是[2,5,7,101]或者[2,3,7,101],长度是4。

  • 当nums再加入一个元素18时,最长递增子序列是[2,5,7,101]或者[2,3,7,101]或者[2,5,7,18]或者[2,3,7,18],长度是4。

  • 当nums再加入一个元素7时,最长递增子序列是[2,5,7,101]或者[2,3,7,101]或者[2,5,7,18]或者[2,3,7,18],长度是4.


分析找规律,拆分子问题

通过上面分析,我们可以发现一个规律


如果新加入一个元素nums[i], 最长递增子序列要么是以nums[i]结尾的递增子序列,要么就是nums[i-1]的最长递增子序列。看到这个,是不是很开心,nums[i]的最长递增子序列已经跟子问题 nums[i-1]的最长递增子序列有关联了。


原问题数组nums[i]的最长递增子序列 = 子问题数组nums[i-1]的最长递增子序列/nums[i]结尾的最长递增子序列

是不是感觉成功了一半呢?但是如何把nums[i]结尾的递增子序列也转化为对应的子问题呢?要是nums[i]结尾的递增子序列也跟nums[i-1]的最长递增子序列有关就好了。又或者nums[i]结尾的最长递增子序列,跟前面子问题num[j](0=<j<i)结尾的最长递增子序列有关就好了,带着这个想法,我们又回头看看穷举的过程:



nums[i]的最长递增子序列,不就是从以数组num[i]每个元素结尾的最长子序列集合,取元素最多(也就是长度最长)那个嘛,所以原问题,我们转化成求出以数组nums每个元素结尾的最长子序列集合,再取最大值嘛。哈哈,想到这,我们就可以用dp[i]表示以num[i]这个数结尾的最长递增子序列的长度啦,然后再来看看其中的规律:



其实,nums[i]结尾的自增子序列,只要找到比nums[i]小的子序列,加上nums[i] 就可以啦。显然,可能形成多种新的子序列,我们选最长那个,就是dp[i]的值啦




  • nums[3]=5,以5结尾的最长子序列就是[2,5],因为从数组下标0到3遍历,只找到了子序列[2]5小,所以就是[2]+[5]啦,即dp[4]=2

  • nums[4]=3,以3结尾的最长子序列就是[2,3],因为从数组下标0到4遍历,只找到了子序列[2]3小,所以就是[2]+[3]啦,即dp[4]=2

  • nums[5]=7,以7结尾的最长子序列就是[2,5,7][2,3,7],因为从数组下标0到5遍历,找到2,5和3都比7小,所以就有[2,7],[5,7],[3,7],[2,5,7]和[2,3,7]这些子序列,最长子序列就是[2,5,7]和[2,3,7],它俩不就是以5结尾和3结尾的最长递增子序列+[7]来的嘛!所以,dp[5]=3 =dp[3]+1=dp[4]+1



很显然有这个规律:一个以nums[i]结尾的数组nums



  • 如果存在j属于区间[0,i-1],并且num[i]>num[j]的话,则有,dp(i) =max(dp(j))+1,


最简单的边界情况


当nums数组只有一个元素时,最长递增子序列的长度dp(1)=1,当nums数组有两个元素时,dp(2) =2或者1,
因此边界就是dp(1)=1。


确定最优子结构


从穷举分析,我们可以得出,以下的最优结构:


dp(i) =max(dp(j))+1,存在j属于区间[0,i-1],并且num[i]>num[j]。

max(dp(j)) 就是最优子结构。


状态转移方程


通过前面分析,我们就可以得出状态转移方程啦:



所以数组num[i]的最长递增子序列就是:


最长递增子序列 =max(dp[i])

代码实现


class Solution {
public int lengthOfLIS(int[] nums) {
if (nums.length == 0) {
return 0;
}
int[] dp = new int[nums.length];
//初始化就是边界情况
dp[0] = 1;
int maxans = 1;
//自底向上遍历
for (int i = 1; i < nums.length; i++) {
dp[i] = 1;
//从下标0到i遍历
for (int j = 0; j < i; j++) {
//找到前面比nums[i]小的数nums[j],即有dp[i]= dp[j]+1
if (nums[j] < nums[i]) {
//因为会有多个小于nums[i]的数,也就是会存在多种组合了嘛,我们就取最大放到dp[i]
dp[i] = Math.max(dp[i], dp[j] + 1);
}
}
//求出dp[i]后,dp最大那个就是nums的最长递增子序列啦
maxans = Math.max(maxans, dp[i]);
}
return maxans;
}
}

参考与感谢



  • leetcode官网

  • 《labuladong算法小抄》

作者:捡田螺的小男孩
链接:https://juejin.cn/post/6951922898638471181
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

如何进一步提高flutter内存表现

前言 性能稳定性是App的生命,Flutter带了很多创新与机遇,然而团队在享受Flutter带来的收益同时也迎接了很多新事物带来的挑战。 本文就内存优化过程中一些实践经验跟大家做一个分享。 Flutter 上线之后 闲鱼使用一套混合栈管理的方案将Flutte...
继续阅读 »

前言


性能稳定性是App的生命,Flutter带了很多创新与机遇,然而团队在享受Flutter带来的收益同时也迎接了很多新事物带来的挑战。


本文就内存优化过程中一些实践经验跟大家做一个分享。


Flutter 上线之后


闲鱼使用一套混合栈管理的方案将Flutter嵌入到现有的App中。在产品体验上我们取得了优于Native的体验。主要得益于Flutter的在跨平台渲染方面的优势,部分原因则是因为我们用Dart语言重新实现的页面抛弃了很多历史的包袱轻装上阵。


上线之后各方面技术指标,都达到甚至超出了部分预期。而我们最为担心的一些稳定性指标,比如crash也在稳定的范围之内。但是在一段时间后我们发现由于内存过高而被系统杀死的abort率数据有比较明显的异常。性能稳定性问题是非常关键的,于是我们火速开展了问题排查。


问题定位与排查


显然问题出在了过大的内存消耗上。内存消耗在App中构成比较复杂,如何在复杂的业务中去定位到罪魁祸首呢?稍加观察,我们确定Flutter问题相对比价明显。工欲善其事必先利其器,需要更好地定位内存的问题,善用已经的工具是非常有帮助的。好在我们在Native层和Dart层都有足够多的性能分析工具进行使用。


工具分析


这里简单介绍我们如何使用的工具去观察手机数据以便于分析问题。需要注意的是,本文的重点不是工具的使用方法介绍,所以只是简单列举部分使用到的常见工具。


Xcode Instruments


Instruments是iOS内存排查的利器,可以比较便捷地观察实时内存使用情况,自然不必多说。


Xcode MemGraph + VMMap


XCode 8之后推出的MEMGraph是Xcode的内存调试利器,可以看到实时的可视化的内存。更为方便的是,你可以将MemGraph导出,配合命令行工具更好的得到结构化的信息。


Dart Observatory


这是Dart语言官方的调试工具,里面也包含了类似于Xcode的Instruments的工具。在Debug模式下Dart VM启动以后会在特定的端口接受调试请求。官方文档


观察结果


在整个过程中我进行了大量的观察,这里分享一部分典型的数据表现。


通过Xcode Instruments排查的话,我们观察到CG Raster Data这个数据有些高。这个Raster Data呢其实是图片光栅化的时候的内存消耗。


我们将App内存异常的场景的MemGraph导出来,对其执行VMMap指令得出的结果:


vmmap --summary Runner[40957].memgraph

vmmap Runner[40957].memgraph | grep 'IOKit'

vmmap Summary


vmmap address


我们主要关注resident和dirty的内存。发现IOKit占用了大量的内存。


结合Xcode Raster Data还有IOKit的大量内存消耗,我们开始怀疑问题是图内存泄漏导致的。经过进一步通过Dart Observatory观察Dart Image对象的内存情况。

Dart image instance

观察结果显示,在内存较高的场景下在Dart层的确同时存在了较多Image(如图中270)的对象。现在基本可以确定内存问题跟Dart层的图片有很大的关系。


这个结果,我估计很多人都已经想到了,App有明显的内存问题很有可能就是跟多媒体资源有关系。通过工具得出的准确数据线索,我们得到一个大致的方向去深入研究。


诡异的Dart图片数量爆炸


图片对象泄漏?


前面我们用工具观察到Dart层的Image对象数量过多直接导致了非常大的内存压力,我们起初怀疑存在图片的内存泄漏。但是我们在经过进一步确认以后发现图片其实并没有真正的泄漏。


Dart语言采用垃圾回收机制(Garbage Collection 下面开始简称GC)来管理分配的内存,VM层面的垃圾回收应该大多数情况下是可信的。但是从实际观察来看,图片数量的爆炸造成的较大的内存峰值直观感觉上GC来得有些不及时。在Debug模式下我们使用Dart Observatory手动触发GC,最终这些图片对象在没有引用的情况下最终还是会被回收。


至此,我们基本可以确认,图片对象不存在泄漏。那是什么导致了GC的反应迟钝呢,难道是Dart语言本身的问题吗?


Garbage Collection 不及时?


为此我需要了解一下Dart内存管理机制垃圾回收的实现,关于详细的内存问题我团队的 @匠修 同学已经发过一篇相关文章可以参考:内存文章


我这里不详细讨论Dart垃圾回收实现细节,只聊一聊Flutter与Dart相关的一些内容。


关于Flutter我需要首先明确几个概念:




  1. Framework(Dart)(跟iOS平台连接的库Flutter.framework要区别开)特指由Dart编写的Flutter相关代码。




  2. Dart VM执行Dart代码的Dart语言相关库,它是以C实现的Dart SDk形式提供的。对外主要暴露了C接口Dart Api。里面主要包含了Dart的编译器,运行时等等。




  3. FLutter Engine C++实现的Flutter驱动引擎。他主要负责跨平台的绘制实现,包含Skia渲染引擎的接入;Dart语言的集成;以及跟Native层的适配和Embeder相关的一些代码。简单理解,iOS平台上面Flutter.framework, Android平台上的Flutter.jar便是引擎代码构建后的产物。




在Dart代码里面对于GC是没有感知的。


对于Dart SDK也就是Dart语言我们可以做的很有限,因为Dart语言本身是一种标准,如果Dart真的有问题我们需要和Dart维护团队协作推进问题的解决。Dart语言设计的时候初衷也是希望GC对于使用者是透明的,我们不应该依赖GC实现的具体算法和策略。不过我们还是需要通过Dart SDK的源码去理解GC的大致情况。


既然我们前面已经确认并非内存泄漏,所以我们在对GC延迟的问题的调查主要放在Flutter Engine以及Dart CG入口上。


Flutter与Dart Garbage Collection


既然感觉GC不及时,先撇开消耗,我们至少可以尝试多触发几次GC来减轻内存峰值压力。但是我在仔细查阅dart_api.h(/src/third_party/dart/runtime/include/dart_api.h )接口文件后,但是并没有找到显式提供触发GC的接口。


但是找到了如下这个方法Dart_NotifyIdle


/**
* Notifies the VM that the embedder expects to be idle until |deadline|. The VM
* may use this time to perform garbage collection or other tasks to avoid
* delays during execution of Dart code in the future.
*
* |deadline| is measured in microseconds against the system's monotonic time.
* This clock can be accessed via Dart_TimelineGetMicros().
*
* Requires there to be a current isolate.
*/

DART_EXPORT void Dart_NotifyIdle(int64_t deadline);

这个接口意思是我们可以在空闲的时候显式地通知Dart,你接下来可以利用这些时间(dealine之前)去做GC。注意,这里的GC不保证会马上执行,可以理解我们请求Dart去做GC,具体做不做还是取决于Dart本身的策略。


另外,我还找到一个方法叫做Dart_NotifyLowMemory:


/**
* Notifies the VM that the system is running low on memory.
*
* Does not require a current isolate. Only valid after calling Dart_Initialize.
*/

DART_EXPORT void Dart_NotifyLowMemory();

不过这个Dart_NotifyLowMemory方法其实跟GC没有太大关系,它其实是在低内存的情况下把多余的isolate去终止掉。你可以简单理解,把一些不是必须的线程给清理掉。


在研究Flutter Engine代码后你会发现,Flutter Engine其实就是通过Dart_NotifyIdle去跟Dart层进行GC方面的协作的。我们可以在Flutter Engine源码animator.cc看到以下代码:


  
//Animator负责刷新和通知帧的绘制
if (!frame_scheduled_) {
// We don't have another frame pending, so we're waiting on user input
// or I/O. Allow the Dart VM 100 ms.
delegate_.OnAnimatorNotifyIdle(*this, dart_frame_deadline_ + 100000);
}


//delegate 最终会调用到这里
bool RuntimeController::NotifyIdle(int64_t deadline) {
if (!root_isolate_) {
return false;
}

tonic::DartState::Scope scope(root_isolate_.get());
//Dart api接口
Dart_NotifyIdle(deadline);
return true;
}

这里的逻辑比较直观:如果当前没有帧渲染的任务时候就通过NotifyIdle告诉Dart层可以进行GC操作了。注意,这里并不是说只有在这种情况下Dart才回去做GC,Flutter只是通过这种方式尽可能利用空闲去做GC,配合Dart以更合理的时间去做GC。


看到这里,我们有足够的理由去尝试一下这个接口,于是我们在一些内存压力比较大的场景进行了手动请求GC的操作。线上的Abort虽然有明显好转,但是内存峰值并没有因此得到改善。我们需要进一步找到根本原因。


图片数量爆炸的真相


为了确定图片大量囤积释放不及时的问题,我们需要跟踪Flutter图片从初始化到销毁的整个流程。


我们从Dart层开始去追寻Image对象的生命周期,我们可以看到Flutter里面所以的图片都是经过ImageProvider来获取的,ImageProvider在获取图片的时候会调用一个Resolve接口,而这个接口会首先查询ImageCache去读取图片,如果不存在缓存就new Image的实例出来。


关键代码:


  ImageStream resolve(ImageConfiguration configuration) {
assert(configuration != null);
final ImageStream stream = new ImageStream();
T obtainedKey;
obtainKey(configuration).then((T key) {
obtainedKey = key;
stream.setCompleter(PaintingBinding.instance.imageCache.putIfAbsent(key, () => load(key)));
}).catchError(
(dynamic exception, StackTrace stack) async {
FlutterError.reportError(new FlutterErrorDetails(
exception: exception,
stack: stack,
library: 'services library',
context: 'while resolving an image',
silent: true, // could be a network error or whatnot
informationCollector: (StringBuffer information) {
information.writeln('Image provider: $this');
information.writeln('Image configuration: $configuration');
if (obtainedKey != null)
information.writeln('Image key: $obtainedKey');
}
));
return null;
}
);
return stream;
}

大致的逻辑



  1. Resolve 请求获取图片.

  2. 查询是否存在于ImageCache.Yes->3 NO->4

  3. 返回已经存在的图片对象

  4. 生成新的Image对象并开始加载
    看起来没有特别复杂的逻辑,不过这里我要提一下Flutter ImageCache的实现。


Flutter ImageCache


Flutter ImageCache最初的版本其实非常简单,用Map实现的基于LRU算法缓存。这个算法和实现没有什么问题,但是要注意的是ImageCache缓存的是ImageStream对象,也就是缓存的是一个异步加载的图片的对象。而且缓存没有对占用内存总量做限制,而是采用默认最大限制1000个对象(Flutter在0.5.6 beta中加入了对内存大小限制的逻辑)。缓存异步加载对象的一个问题是,在图片加载解码完成之前,无法知道到底将要消耗多少内存,至少在Flutter这个Cache实现中没有处理这个问题。具体的实现感兴趣的朋友可以阅读ImageCache.dart源码。


其实Flutter本身提供了定制化Cache的能力,所以优化ImageCache的第一步就是要根据机型的物理内存去做缓存大小的适配,设置ImageCache的合理限制。关于ImageCache的问题,可以参考官方文档和这个issue,我这里不展开去聊了。


Flutter Image生命周期


回到我们的Image对象跟踪,很明显,在缓存没有命中的情况下会有新的Image产生。继续深入代码会发现Image对象是由这段代码产生的:



Future instantiateImageCodec(Uint8List list) {
return _futurize(
(_Callback callback) => _instantiateImageCodec(list, callback, null)
);
}

String _instantiateImageCodec(Uint8List list, _Callback callback, _ImageInfo imageInfo)
native 'instantiateImageCodec';

这里有个native关键字,这是Dart调用C代码的能力,我们查看具体的源码可以发现这个最终初始化的是一个C++的codec对象。具体的代码在Flutter Engine codec.cc。它大致的过程就是先在IO线程中启动了一个解码任务,在IO完成之后再把最终的图片对象发回UI线程。关于Flutter线程的详细介绍,我在另外一篇文章中已经有介绍,这里附上链接给有兴趣的朋友。深入理解Flutter Engine线程模型。经过来这些代码和线程分析,我们得到大致的流程图:


图片爆炸流程图


也就是说,解码任务在IO线程进行,IO任务队列里面都是C++ lambda表达式,持有了实际的解码对象,也就持有了内存资源。当IO线程任务过多的时候,会有很多IO任务在等待执行,这些内存资源也被闭包所持有而等待释放。这就是为什么直观上会有内存释放不及时而造成内存峰值的问题。这也解释了为什么之前拿到的vmmap虚拟内存数据里面IOKit是大头。


这样我们找到了关键的线索,在缓存不命中的情况下,大量初始化Image对象,导致IO线程任务繁重,而IO又持有大量的图片解码所用的内存资源。带这个推论,我在Flutter Engine的Task Runner加入了任务数量和C++ image对象的监控代码,证实了的确存在IO任务线程过载的情况,峰值在极端情况下瞬时达到了100+IO操作。


IO Runner监控


到这里问题似乎越来越明了了,但是为什么会有这么IO任务触发呢?上述逻辑虽然可能会有IO线程过载的情况下占用大量内存的情况。上层要求生成新的图片对象,这种请求是没有错误的,设计就是如此。就好比主线程阻塞大量的任务,必然会导致界面卡顿,但者却不是主线程本身的问题。我们需要从源头找到导致新对象创建暴涨真正导致IO线程过载的原因。


大量请求的根源


在前面的线索之下,我们继续寻找问题的根源。我们在实际App操作的过程当中发现,页面Push的越多,图片生成的速度越来越快。也就是说页面越多请求越快,看起来没有什么大问题。但是可见的图片其实总是在一定数量范围之内的,不应该随着页面增多而加快对象创建的频率。我们下意识的开始怀疑是否存在不可见的Image Widget也在不断请求图片的情况。最终导致了Cache无法命中而大量生成新的图片的场景。


我开始调查每个页面的图片加载请求,我们知道Flutter里面万物皆Widget,页面都是是Widget,由Navigator管理。我在Widget的生命周期方法(详细见Flutter官方文档)中加入监控代码,如我所料,在Navigator栈底下不可见的页面也还在不停的Resolve Image,直接导致了image对象暴涨而导致IO线程过载,导致了内存峰值。


看起来,我们终于找到了根本原因。解决方案并不难。在页面不可见的时候没必要发出多余的图片加载请求,峰值也就随之降下来了。再经过一番代码优化和测试以后问题得到了根本上的解决。优化上线以后,我们看到了数据发生了质的好转。
有朋友可能想问,为什么不可见的Widget也会被调用到相关的生命周期方法。这里我推荐阅读Flutter官方文档关于Widget相关的介绍,篇幅有限我这里不展开介绍了。widgets


至此,我们已经解决了一个较为严重的内存问题。内存优化情况复杂,可以点也比较多,接下来我继续简要分享在其它一些方面的优化方案。


截图缓存优化


文件缓存+预加载策略


我们是采用嵌入式Flutter并使用一套混合栈模式管理Native和Flutter页面相互跳转的逻辑。由于FlutterView在App中是单例形式存在的,我们为了更好的用户体验,在页面切换的过程中使用的截图的方式来进行过渡。


大家都知道,图片是非常占用内存的对象,我们如何在不降低用户体验的同时获得最小的内存消耗呢?假如我们每push一个页面都保存一张截图,那么内存是以线性复杂度增长的,这显然不够好。


内存和空间在大多数情况下是一个互相转换的关系,优化很多时候其实是找一个合理的折中点。
最终我采用了预加载+缓存的策略,在页面最多只在内存中同时存在两个截图,其它的存文件,在需要的时候提前进行预加载。
简要流程图:


简要流程图


这样的话就做到了不影响用户体验的前提下,将空间复杂度从O(n)降低到了O(1)。
这个优化进一步节省了不必要的内存开销。


截图额外的优化



  • 针对当前设备的内存情况,自适应调整截图的分辨率,争取最小的内存消耗。

  • 在极端的内存情况下,把所有截图都从内存中移除存(存文件可恢复),采用PlaceHolder的形式。极端情况下避免被杀,保证可用性的体验降级策略。


页面兜底策略


对于电商类App存在一个普遍的问题,用户会不断的push页面到栈里面,我们不能阻止用户这种行为。我们当然可以把老页面干掉,每次回退的时候重新加载,但是这种用户体验跟Web页一样,是用户不可接受的。我们要维持页面的状态以保证用户体验。这必然会导致内存的线性增长,最终肯定难免要被杀。我们优化的目的是提高用户能够push的极限页面数量。


对于Flutter页面优化,除了在优化每一个页面消耗的内存之外,我们做了降级兜底策略去保证App的可用性:在极端情况下将老页面进行销毁,在需要的时候重新创建。这的确降低了用户体验,在极端情况下,降级体验还是比Crash要好一些。



FlutterViewController 单例析构


另外我想讨论的一个话题是关于FlutterViewController的。目前Flutter的设计是按照单例模式去运行的,这对于完全用Flutterc重新开发的App没有太大的问题。但是对于混合型App,多出来的常驻内存确实是一个问题。


实际上,Flutter Engine底层实现是考虑到了析构这个问题,有相关的接口。但是在Embeder这一层(具体FlutterViewController Message Channels这一层),在实现过程中存在一些循环引用,导致在Native层就算没有引用FlutterViewController的时候也无法释放.


FlutterViewController引用图


我在经过一段时间的尝试后,算是把循环引用解除了。这些循环引用主要集中在FlutterChannel这一块。在解除之后我顺利的释放了FlutterViewController,可以明显看到常驻内存得到了释放。但是我发现释放FlutterViewController的时候会导致一部分Skia Image对象泄漏,因为Skia Objects必须在它创建的线程进行释放(详情请参考skia_gpu_object.cc源码),线程同步的问题。关于这个问题我在GitHub上面有一个issue大家可以参考。FlutterViewController释放issue


目前,这个优化我们已经反馈给Flutter团队,期待他们官方支持。希望大家可以一起探索研究。


进一步探讨


除此之外,Flutter内存方面其实还有比较多方面可以去研究。我这里列举几个目前观察到的问题。




  1. 我在内存分析的时候发现Flutter底层使用的boring ssl库有可以确定的内存泄漏。虽然这个泄漏比较缓慢,但是对于App长期运行还是有影响的。我在GitHub上面提了个issue跟进,目前已有相关的人员进行跟进。SSL leak issue




  2. 关于图片渲染,目前Flutter还是有优化空间的,特别是图片的按需剪裁。大多数情况下是没有不要将整一个bitmap解压到内存中的,我们可以针对显示的区域大小和屏幕的分辨率对图片进行合理的缩放以取得最好的性能消耗。




  3. 在分析Flutter内存的MemGraph的时候,我发现Skia引擎当中对于TextLayout消耗了大量的内存.目前我没有找到具体的原因,可能存在优化的空间。




结语


在这篇文章里,我简要的聊了一下目前团队在Flutter应用内存方面做出的尝试和探索。短短一篇文章无法包含所有内容,只能推出了几个典型的案例来作分析,希望可以跟大家一起探讨研究。欢迎感兴趣的朋友一起研究,如有更好的想法方案,我非常乐意看到你的分享。


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

Flutter动画实现粒子漂浮效果

要问2019年最火的移动端框架,肯定非Google的Flutter莫属。 本着学习的态度,基本的Dart语法(个人感觉语法风格接近Java+JS)过完之后,开始撸代码练手。 效果图 (这里为了方便录制gif,动画设置的较快;如果将动画的Duration设...
继续阅读 »

要问2019年最火的移动端框架,肯定非Google的Flutter莫属。

image

本着学习的态度,基本的Dart语法(个人感觉语法风格接近Java+JS)过完之后,开始撸代码练手。




效果图


image


(这里为了方便录制gif,动画设置的较快;如果将动画的Duration设置成20s,看起来就是浮动的效果了)
粒子碰撞的效果参考了张风捷特列 大佬的Flutter动画之粒子精讲


1. Flutter的动画原理



在任何系统的UI框架中,动画实现的原理都是相同的,即:在一段时间内,快速地多次改变UI外观;由于人眼会产生视觉暂留,所以最终看到的就是一个“连续”的动画,这和电影的原理是一样的。我们将UI的一次改变称为一个动画帧,对应一次屏幕刷新,而决定动画流畅度的一个重要指标就是帧率FPS(Frame Per Second),即每秒的动画帧数。



简而言之,就是逐帧绘制,只要屏幕刷新的足够快,我们就会觉得这是个连续的动画。
设想一个小球从屏幕顶端移动到底端的动画,为了完成这个动画,我们需要哪些数据呢?



  • 小球的运动轨迹,即起始点s、终点e和中间任意一点p

  • 动画持续时长t


只有这两个参数够吗?明显是不够的,因为小球按照给定的轨迹运动,可能是匀速、先快后慢、先慢后快、甚至是一会儿快一会慢的交替地运动,只要在时间t内完成,都是可能的。所以我们应该再指定一个参数c来控制动画的速度。


1.1 vsync探究


废话不多说,我们看看Flutter中是动画部分的代码:


AnimationController controller = AnimationController(
vsync: this,
duration: Duration(seconds: 2),
)..addListener(() {
//_renderBezier();
print(controllerG.value);
print('这是第${++count}次回调');
});
复制代码

简要分析一下,AnimationController,顾名思义,控制器,用来控制动画的播放。传入的参数中,duration我们知道是前面提到过的动画持续时长t,那这个vsync是啥参数呢?打过游戏的同学可能对这个单词有印象,vsync 就是 垂直同步 。那什么是垂直同步呢?



垂直同步又称场同步(Vertical Hold),从CRT显示器的显示原理来看,单个像素组成了水平扫描线,水平扫描线在垂直方向的堆积形成了完整的画面。显示器的刷新率受显卡DAC控制,显卡DAC完成一帧的扫描后就会产生一个垂直同步信号。我们平时所说的打开垂直同步指的是将该信号送入显卡3D图形处理部分,从而让显卡在生成3D图形时受垂直同步信号的制约。



简而言之就是,显卡在完成渲染后,将画面数据送往显存中,而显示器从显存中一行一行从上到下取出画面数据,进行显示。但是屏幕的刷新率和显卡渲染数据的速度很多时候是不匹配的,试想一下,显示器刚扫描显示完屏幕上半部分的画面,正准备从显存取下面的画面数据时,显卡送来了下一帧的图像数据覆盖了原来的显存,这个时候显示器取出的下面部分的图像就和上面的不匹配,造成画面撕裂。


为了避免这种情况,我们引入垂直同步信号,只有在显示器完整的扫描显示完一帧的画面后,显卡收到垂直同步信号才能刷新显存。
可是这个物理信号跟我们flutter动画有啥关系呢?vsync对应的参数是this,我们继续分析一下this对应的下面的类。


class _RunBallState extends State<RunBall> with TickerProviderStateMixin 
复制代码

with关键字是使用该类的方法而不继承该类,Mixin是类似于Java的接口,区别在于Mixin中的方法不是抽象方法而是已经实现了的方法。



这个TickerProviderStateMixin到底是干啥的呢???经过哥们儿Google的帮助,在网上找到了



关于动画的驱动,在此简单的说一下,Ticker是被SchedulerBinding所驱动。SchedulerBinding则是监听着Window.onBeginFrame回调。
Window.onBeginFrame的作用是什么呢,是告诉应用该提供一个scene了,它是被硬件的VSync信号所驱动的。



于是我们终于发现了,绕了一圈,归根到底还是真正的硬件产生的垂直同步信号在驱动着Flutter的动画的进行。


..addListener(() {
//_renderBezier();
print(controllerG.value);
print('这是第${++count}次回调');
});

复制代码

注意到之前的代码中存在一个动画控制器的监听器,动画在执行时间内,函数回调controller.value会生成一个从0到1的double类型的数值。我们在控制台打印出结果如下:

image


image


经过观察,两次试验,在2s的动画执行时间内,该回调函数分别被执行了50次,53次,并不是一个固定值。也就是说硬件(模拟器)的屏幕刷新率大概维持在(25~26.5帧/s)。


结论:硬件决定动画刷新率


1.2 动画动起来


搞懂了动画的原理之后,我们接下来就是逐帧的绘制了。关于Flutter的自定义View,跟android原生比较像。


image


继承CustomPainter类,重写paint和shouldRepaint方法,具体实现可以看代码.


class Ball {
double aX;
double aY;
double vX;
double vY;
double x;
double y;
double r;
Color color;}

复制代码

小球Ball具有圆心坐标、半径、颜色、速度、加速度等属性,通过数学表达式计算速度和加速度的关系,就可以实现匀加速的效果。


//运动学公式,看起来少了个时间t;实际上这些函数在动画过程中逐帧回调,把每帧刷新周期当成单位时间,相当于t=1
_ball.x += _ball.vX;//位移=速度*时间
_ball.y += _ball.vY;
_ball.vX += _ball.aX;//速度=加速度*时间
_ball.vY += _ball.aY;

复制代码

控制器使得函数不断回调,在回调函数函数里改变小球的相关参数,并且调用setState()函数,使得UI重新绘制;小球的轨迹坐标不断地变化,逐帧绘制的小球看起来就在运动了。你甚至可以在添加效果使得小球在撞到边界时变色或者半径变小(参考文章开头的粒子碰撞效果图)。


2. 小球随机浮动的思考


问题来了,我想要一个漂浮的效果呢?最好是随机的轨迹,就像气泡在空中飘乎不定,于是引起了我的思考;匀速然后方向随机?感觉不够优雅,于是去网上搜了一下,发现了思路!



首先随机生成一条贝塞尔曲线作为轨迹,等小球运动到终点,再生成新的贝塞尔曲线轨迹



生成二阶贝塞尔曲线的公式如下:


//二次贝塞尔曲线轨迹坐标,根据参数t返回坐标;起始点p0,控制点p1,终点p2
Offset quadBezierTrack(double t, Offset p0, Offset p1, Offset p2) {
var bx = (1 - t) * (1 - t) * p0.dx + 2 * t * (1 - t) * p1.dx + t * t * p2.dx;
var by = (1 - t) * (1 - t) * p0.dy + 2 * t * (1 - t) * p1.dy + t * t * p2.dy;

return Offset(bx, by);
}
复制代码

很巧的是,这里需要传入一个0~1之间double类型的参数t,恰好前面我们提过,animationController会在给定的时间内,生成一个0~1的value;这太巧了。


起始点的坐标不用说,接下来就剩解决控制点p1和p2,当然是随机生成这两点,但是如果同时有多个小球呢?比如5个小球同时进行漂浮,每个小球都对应一组三个坐标的信息,给小球Ball添加三个坐标的属性?不,这个时候,我们可以巧妙地利用带种子参数的随机数。



我们知道随机数在生成的时候,如果种子相同的话,每次生成的随机数也是相同的。



每个小球对象在创建的时候自增地赋予一个整形的id,作为随机种子;比如5个小球,我们起始的id为:2,4,6,8,10;


    Offset p0 = ball.p0;//起点坐标
Offset p1 = _randPosition(ball.id);
Offset p2 = _randPosition(ball.id + 1);
复制代码

rand(2),rand(2+1)为第一个小球的p1和p2坐标;当所有小球到达终点时,此时原来的终点p2为新一轮贝塞尔曲线的起点;此时相应的id也应增加,为了防止重复,id应增加小球数量5 *2,即第二轮运动开始时,5个小球的id为:12,14,16,18,20。
这样就保证了每轮贝塞尔曲线运动的时候,对于每个小球而言,p0,p1,p2是确定的;新一轮的运动所需要的随机的三个坐标点,只需要改变id的值就好了。


Path quadBezierPath(Offset p0, Offset p1, Offset p2) {
Path path = new Path();
path.moveTo(p0.dx, p0.dy);
path.quadraticBezierTo(p1.dx, p1.dy, p2.dx, p2.dy);
return path;
}
复制代码

这个时候,我们还可以利用Flutter自带的api画出二次贝塞尔曲线的轨迹,看看小球的运动是否落在轨迹上。


image


2.1 一些细节


animation = CurvedAnimation(parent: controllerG, curve: Curves.bounceOut);
复制代码

这里的Curve就是前面提到的,控制动画过程的参数,flutter自带了挺多效果,我最喜欢这个bounceOut(弹出效果)

image


 animation.addStatusListener((status) {
switch (status) {
case AnimationStatus.dismissed:
// TODO: Handle this case.
break;
case AnimationStatus.forward:
// TODO: Handle this case.
break;
case AnimationStatus.reverse:
// TODO: Handle this case.
break;
case AnimationStatus.completed:
// TODO: Handle this case.
controllerG.reset();
controllerG.forward();
break;
}
});

复制代码

监听动画过程的状态,当一轮动画结束时,status状态为AnimationStatus.completed;此时,我们将控制器reset重置,再forward重新启动,此时就会开始新一轮的动画效果;如果我们选的是reverse,则动画会反向播放。




GestureDetector(
child: Container(
width: double.infinity,
height: 200,
child: CustomPaint(
painter: FloatBallView(_ballsF, _areaF),
),
),
onTap: () {
controllerG.forward();
},
onDoubleTap: () {
controllerG.stop();
},
),
复制代码

为了方便控制,我还加了个手势监听器,单击控制动画运行,双击暂停动画。


3 完结


水平有限,文中如有错误还请各位指出,我是梦龙Dragon


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

GC回收机制与分代回收策略

GC回收机制一、前言垃圾回收:Garbage Collection,简写 GC。JVM 中的垃圾回收器会自动回收无用的对象。但是 GC 自动回收的代价是:当这种自动化机制出错,我们就需要深入理解 GC 回收机制,甚至需要对这些 自动化 的技术实施必要的监控与...
继续阅读 »



GC回收机制

一、前言

垃圾回收Garbage Collection,简写 GCJVM 中的垃圾回收器会自动回收无用的对象。

但是 GC 自动回收的代价是:当这种自动化机制出错,我们就需要深入理解 GC 回收机制,甚至需要对这些 自动化 的技术实施必要的监控与调节。

在虚拟机中,程序计数器、虚拟机栈、本地方法栈 3 个区域随线程而生,随线程而灭,栈中的栈帧随着方法的进入和退出而执行着出栈和入栈操作。所以这几个区域不需要考虑回收的问题。

而在 堆和方法区 中,一个接口中的多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存也可能不一样。这部分的只有在程序运行期间才会知道需要创建哪些对象,这部分的内存的创建和回收是动态的,也是垃圾回收器重点关注的地方。

二、什么是垃圾

垃圾 就是 内存中已经没有用的对象。既然是 垃圾回收,就必须知道哪些对象是垃圾。

Java 虚拟机中使用了一种叫做 可达性分析 的算法 来决定对象是否可以被回收

GCRoot示意图

上图中 A、B、C、D、E 与 GCRoot 直接或间接产生引用链,所以 GC 扫描到这些对象时,并不会执行回收操作;J、K、M虽然之间有引用链,但是并没有与 GCRoot 存在引用链,所以当 GC 扫描到他们时会将他们回收。

注意的是,上图中所有的对象,包括 GCRoot,都是内存中的引用。

作为 GCRoot 的几种对象
  1. Java虚拟机栈(局部变量表)中的引用的对象;

  2. 方法区中静态引用指向的对象;

  3. 仍处于存活状态中的线程对象;

  4. Native方法中 JNI 引用的对象;

三、什么时候回收

不同的虚拟机实现有着不同的 GC 实现机制,但一般情况下都会存在下面两种情况:

  1. Allocation Failure:在堆内存分配中,如果因为可用剩余空间不足导致对象内存分配失败,这时系统会触发一次 GC。

  2. System.gc():在应用层,Java开发工程师可以主动调用此API来请求一次 GC。

四、验证GCRoot的几种情况

在验证之前,先了解Java命令时的参数。

-Xms:初始分配 JVM 运行时的内存大小,如果不指定则默认为物理内存的 1/64

举个小例子

// 表示从物理内存中分配出 200M 空间给 JVM 内存
java -Xms200m HelloWorld
1.验证虚拟机栈(栈帧中的局部变量)中引用的对象作为 GCRoot
// 验证代码
public class GCRootLocalVariable {

  private int _10MB = 10 * 1024 * 1024;
  private byte[] memory = new byte[8 * _10MB];

  public static void main(String[] args) {
      System.out.println("开始时:");
      printMemory();
      method();
      System.gc();
      System.out.println("第二次GC完成");
      printMemory();
  }

  public static void method() {
      GCRootLocalVariable gc = new GCRootLocalVariable();
      System.gc();
      System.out.println("第一次GC完成");
      printMemory();
  }

  // 打印出当前JVM剩余空间和总的空间大小
  public static void printMemory() {
      long freeMemory = Runtime.getRuntime().freeMemory();
      long totalMemory = Runtime.getRuntime().totalMemory();
      System.out.println("剩余空间:" + freeMemory / 1024 / 1024 + "M");
      System.out.println("总共空间:" + totalMemory / 1024 / 1024 + "M");
  }
}
// 打印日志:
开始时:
剩余空间:119M
总共空间:123M
第一次GC完成
剩余空间:40M
总共空间:123M
第二次GC完成
剩余空间:120M
总共空间:123M

从上述代码中可以看到:

第一次打印内存信息,分别为 119M 和 123M;

第二次打印内存信息,分别为 40M 和 123M;剩余空间小了 80M,是因为在 method() 方法中创建了局部变量 gc(位于栈帧中的局部变量),并且这个 gc 对象会被作为 GCRoot。虽然创建的对象未被使用并且调用了 System.gc(),但是因为该方法未结束,所以创建的对象不能被回收。

第三次打印内存信息,分别为 120M 和 123M;method() 方法已经结束,创建的对象 gc 也随方法消失,不再有引用类型指向该 80M 对象。

【值得注意的是】

private int _10MB = 10 * 1024 * 1024;
private byte[] memory = new byte[8 * _10MB];

上面 2 行代码是必须的,如果去掉,那么 3 次打印结果将会一致,idea 也会出现Instantiation of utility class 警告信息,说这个类只存在静态方法,没必要创建这个对象。

这也就是说为什么创建 GCRootLocalVariable() 会需要 80M 的大小,是因为 GCRootLocalVariable 在创建时就会为其内部变量 memory 确定 80M 的大小。

2.验证方法区中的静态变量引用的对象作为 GCRoot
public class GCRootStaticVariable {
  private static int _10M = 10 * 1024 * 1024;
  private byte[] memory;
  private static GCRootStaticVariable staticVariable;

  public GCRootStaticVariable(int size) {
      memory = new byte[size];
  }

  public static void main(String[] args) {
      System.out.println("程序开始:");
      printMemory();
      GCRootStaticVariable g = new GCRootStaticVariable(2 * _10M);
      g.staticVariable = new GCRootStaticVariable(4 * _10M);
      // 将g设置为null,调用GC时可以回收此对象内存
      g = null;
      System.gc();
      System.out.println("GC完成");
      printMemory();
  }

  // 打印JVM剩余空间和总空间
  private static void printMemory() {
      long freeMemory = Runtime.getRuntime().freeMemory();
      long totalMemory = Runtime.getRuntime().totalMemory();
      System.out.println("剩余空间" + freeMemory/1024/1024 + "M");
      System.out.println("总共空间" + totalMemory/1024/1024 + "M");
  }
}

打印结果:
程序开始:
剩余空间119M
总共空间123M
GC完成
剩余空间81M
总共空间123M

通过上述打印结果可知:

  1. 程序刚开始时打印结果为 119M;

  2. 当创建 g 对象时分配 20M 内存,又为静态变量 staticVariable 分配 40M 内存;

  3. 当调用 gc 回收时,非静态变量 memory 分配的 20M 内存被回收;

  4. 但是作为 GCRoot 的静态变量 staticVariable 不会被回收,所以最终打印结果少了 40M 内存。

3.验证活跃线程作为GCRoot
public class GCRootThread {

  private int _10M = 10 * 1024 * 1024;
  private byte[] memory = new byte[8 * _10M];

  public static void main(String[] args) throws InterruptedException {
      System.out.println("程序开始:");
      printMemory();
      AsyncTask asyncTask = new AsyncTask(new GCRootThread());
      Thread thread = new Thread(asyncTask);
      thread.start();
      System.gc();
      System.out.println("main方法执行完成,执行gc");
      printMemory();
      thread.join();
      asyncTask = null;
      System.gc();
      System.out.println("线程代码执行完成,执行gc");
      printMemory();
  }

  private static void printMemory() {
      long freeMemory = Runtime.getRuntime().freeMemory();
      long totalMemory = Runtime.getRuntime().totalMemory();
      System.out.println("剩余内存:" + freeMemory / 1024 / 1024 + "M");
      System.out.println("总共内存:" + totalMemory / 1024 / 1024 + "M");
  }

  private static class AsyncTask implements Runnable {

      private GCRootThread gcRootThread;

      public AsyncTask(GCRootThread gcRootThread) {
          this.gcRootThread = gcRootThread;
      }

      @Override
      public void run() {
          try {
              Thread.sleep(500);
          } catch (InterruptedException e) {
              e.printStackTrace();
          }
      }
  }
}
打印结果:
程序开始:
剩余内存:119M
总共内存:123M
main方法执行完成,执行gc
剩余内存:41M
总共内存:123M
线程代码执行完成,执行gc
剩余内存:120M
总共内存:123M

通过上述打印结果可知:

  1. 程序刚开始时可用内存为 119M;

  2. 第一次调用 gc 时,线程并没有执行结束,并且它作为 GCRoot ,所以它所引用的 80M 内存不会被 GC 回收掉;

  3. thread.join() 保证线程结束后再调用后续代码,所以当第二次调用 GC 时,线程已经执行完毕并被置为 null;

  4. 这时线程已经销毁,所以该线程所引用的 80M 内存被 GC 回收掉。

4.测试成员变量是否可作为GCRoot
public class GCRootClassVariable {
  private static int _10M = 10 * 1024 * 1024;
  private byte[] memory;
  private GCRootClassVariable gcRootClassVariable;

  public GCRootClassVariable(int size) {
      memory = new byte[size];
  }

  public static void main(String[] args) {
      System.out.println("程序开始:");
      printMemory();
      GCRootClassVariable g = new GCRootClassVariable(2 * _10M);
      g.gcRootClassVariable = new GCRootClassVariable(4 * _10M);
      g = null;
      System.gc();
      System.out.println("GC完成");
      printMemory();
  }

  private static void printMemory() {
      long freeMemory = Runtime.getRuntime().freeMemory();
      long totalMemory = Runtime.getRuntime().totalMemory();
      System.out.println("剩余内存:" + freeMemory / 1024 / 1024 + "M");
      System.out.println("总共内存:" + totalMemory / 1024 / 1024 + "M");
  }
}
打印结果:
程序开始:
剩余内存:119M
总共内存:123M
GC完成
剩余内存:121M
总共内存:123M

上述打印结果可知:

  1. 第一次打印结果与第二次打印结果一致:全局变量 gcRootClassVariable 随着 g=null 后被销毁。

  2. 所以全局变量并不能作为 GCRoot。

五、如何回收垃圾(常见的几种垃圾回收算法)

1.标记清除算法(Mark and Sweep GC)

从 “GCRoots” 集合开始,将内存整个遍历一次,保留所有可以被 GCRoots 直接或间接引用到的对象,而剩下的对象都当做垃圾对待并回收。

上述整个过程分为两步:

  1. Mark标记阶段:找到内存中的所有 GC Root 对象,只要是和 GC Root 对象直接或间接相连则标记为灰色(存活对象),否则标记为黑色(垃圾对象)。

  2. Sweep清楚阶段:当遍历完所有的 GC Root 之后,则将标记为垃圾的对象直接清楚。

标记清除算法示意图

标记清除算法优缺点

【优点】

实现简单,不需要将对象进行移动。

【缺点】

需要中断进程内其他组件的执行,并且可能产生内存碎片,提高了垃圾回收的频率。

2.复制算法(Copying)
  1. 将现有的内存空间分为两块,每次只使用其中一块;

  2. 在垃圾回收时将正在使用的内存中的存活对象复制到未被使用的内存块中;

  3. 之后清除正在使用的内存块中的所有对象;

  4. 交换两个内存的角色,完成垃圾回收(目前使用A,B是空闲,算法完成后A为空闲,设置B为使用状态)。

复制算法复制前示意图

复制算法复制后示意图

复制算法优缺点

【优点】

按顺序分配内存即可;实现简单、运行高效,不用考虑内存碎片问题。

【缺点】

可用的内存大小缩小为原来的一半,对象存活率高时会频繁进行复制。

3.标记压缩算法(Mark-Compact)
  1. 需要先从根节点开始对所有可达对象做一次标记;

  2. 之后并不简单地清理未标记的对象,而是将所有的存活对象压缩到内存的一端;

  3. 最后清理边界外所有的空间。

所有,标记压缩也分为两步完成:

  1. Mark标记阶段:找到内存中的所有 GC Root 对象,只要和 GC Root 对象直接或间接相连则标记为灰色(存活对象),否则标记为黑色(垃圾对象)

  2. Compact压缩阶段:将剩余存活对象按顺序压缩到内存的某一端

标记压缩算法示意图

标记压缩算法优缺点

【优点】

避免了碎片产生,又不需要两块相同的内存空间,性价比较高。

【缺点】

所谓压缩操作,仍需要进行局部对象移动,一定程度上还是降低了效率。

分代回收策略

Java 虚拟机根据对象存活的周期不同,把堆内存划分为 新生代老年代,这就是 JVM 的内存分代策略。

注意:在 HotSpot 中除了 新生代老年代,还有 永久代

分代回收的中心思想:对于新创建的对象会在新生代中分配内存,此区域的对象生命周期一般较短,如果经过多次回收仍然存活下来,则将它们转移到老年代中。

一、年轻代

新生成的对象优先存放在新生代中,存活率很低

新生代中,常规应用进行一次垃圾收集一般可以回收 70% ~ 95% 的空间,回收效率很高。所以一般采用的 GC 回收算法是 复制算法

新生代也可细分3部分:Eden、Survivor0(简称 S0)、Survivor1(简称 S1),这 3 部分按照 8:1:1 的比例来划分新生代。

新生代老年代示意图

新生成的对象会存放在 Eden 区。

新生代老年代示意图

当 Eden 区满时,会触发垃圾回收,回收掉垃圾之后,将剩下存活的对象存放到 S0 区。当下一次 Eden 区满时,再次触发垃圾回收,这时会将 Eden 区 和 S0 区存活的对象全部复制到 S1 区,并清空 Eden 区和 S0 区。

新生代老年代示意图

上述步骤重复 15 次之后,依然存活下来的对象存放到 老年区

二、老年代

一个对象如果在新生代存活了足够长的时间而没有被清理掉,则会被复制到老年代。

老年代的内存大小一般比新生代大,能存放更多的对象。如果对象比较大(比如长字符串或大数组),并且新生代的剩余空间不足,则这个大对象会直接被分配到老年代上。

可以使用 -XX:PretenureSizeThreshold 来控制直接升入老年代的对象大小。

因为老年代对象的生命周期较长,不需要过多的复制操作,所以一般采用标记压缩的回收算法。

【注意的是】

有这么一种情况,老年代中的对象会引用新生代中的对象,这时如果要执行新生代的 GC,则可能要查询整个老年代引用新生代的情况,这种效率是极低的。所以老年代中维护了一个 512byte 的 table,所有老年代对象引用新生代对象的引用都记录在这里。这样新生代 GC 时只需要查询这个表即可。

三、GC log分析

为了让上层应用开发人员更加方便调试 Java 程序,JVM 提供了相应的 GC 日志,在 GC 执行垃圾回收事件中,会有各种相应的 log 被打印出来。

新生代和老年代打印的日志是有区别的:

【新生代GC:轻GC】这一区域的 GC 叫做 Minor GC。因为 Java 对象大多都具备朝生夕灭的特性,所以 Minor GC 非常频繁,一般回收速度比较快。

【老年代GC:重GC】发生在这一区域的 GC 叫做 Major GC 或者 Full GC,当出现 Major GC,经常会伴随至少一次 Minor GC

Major GCFull GC 在有些虚拟机中还是有区别的:前者是仅回收老年代中的垃圾对象,后者是回收整个堆中的垃圾对象。

常用的 GC 命令参数
命令参数功能描述
-verbose:gc显示 GC 的操作内容
-Xms20M初始化堆大小为 20M
-Xmx20M设置堆最大分配内存 20M
-Xmn10M设置新生代的内存大小为 10M
-XX:+PrintGCDetails打印GC的详细log日志
-XX:SurvivorRatio=8新生代中 Eden 区域与 Survivor 区域的大小比值为 8:1:1

添加 VM Options 参数:分配堆内存 20M,10M给新生代,10M给老年代

// VM args: -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8
public class MinorGCTest {

  private static final int _1M = 1024 * 1024;

  public static void main(String[] args) {
      byte[] a, b, c, d;
      a = new byte[2 * _1M];
      b = new byte[2 * _1M];
      c = new byte[2 * _1M];
      d = new byte[_1M];
  }
}
打印结果:(这里测试是第二次修改后的运行效果)
[GC (Allocation Failure) [PSYoungGen: 7820K->840K(9216K)] 7820K->6992K(19456K), 0.0072302 secs] [Times: user=0.02 sys=0.00, real=0.01 secs]
[Full GC (Ergonomics) [PSYoungGen: 840K->0K(9216K)] [ParOldGen: 6152K->6759K(10240K)] 6992K->6759K(19456K), [Metaspace: 3198K->3198K(1056768K)], 0.0087734 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
Heap
PSYoungGen     total 9216K, used 1190K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
eden space 8192K, 14% used [0x00000000ff600000,0x00000000ff7298d8,0x00000000ffe00000)
from space 1024K, 0% used [0x00000000ffe00000,0x00000000ffe00000,0x00000000fff00000)
to   space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000)
ParOldGen       total 10240K, used 6759K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
object space 10240K, 66% used [0x00000000fec00000,0x00000000ff299cd0,0x00000000ff600000)
Metaspace       used 3205K, capacity 4496K, committed 4864K, reserved 1056768K
class space   used 351K, capacity 388K, committed 512K, reserved 1048576K

上述字段意思代表如下:

字段代表含义
PSYoungGen新生代
eden新生代中的 Eden 区
from新生代中的 S0 区
to新生代中的 S1 区
ParOldGen老年代
  1. 第一次运行效果后,因为 Eden 区 8M,S0 和 S1 各 1M。所以 a、b、c、d 共有 7M 空间都会在 Eden 区。

  2. 修改 d = new byte[2 * _1M],再次运行;

  3. JVM 会将 a/b/c 存放到 Eden 区,Eden 占有 6M 空间,无法再分配 2M 空间给 d;

  4. 因此会执行一次轻 GC,并尝试将 a/b/c 复制到 S1 区;

  5. 但是因为 S1 区只有 1M 空间,所以没办法存储 a/b/c 三者任一对象。

  6. 这种情况下,JVM 将 a/b/c 转移到老年代,将 d 保存在 Eden 区。

【最终结果】

Eden区 占用 2M 空间(d),老年代占用 6M 空间(a,b,c)

四、引用

通过 GC Roots 的引用可达性来判断对象是否存活,JVM 中的引入关系有以下四种:

引用英文名GC回收机制使用示例
强引用Strong Reference如果一个对象具有强引用,那么垃圾回收期绝不会回收它Object obj = new Object();
软引用Soft Reference在内存实在不足时,会对软引用进行回收SoftReference softObj = new SoftReference();
弱引用Weak Reference第一次GC回收时,如果垃圾回收器遍历到此弱引用,则将其回收WeakReference weakObj = new WeakReference();
虚引用Phantom Reference一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来获取一个对象的实例不会使用
软引用的用法
public class SoftReferenceNormal {

  static class SoftObject {
      byte[] data = new byte[120 * 1024 * 1024]; // 120M
  }

  public static void main(String[] args) {
      SoftReference<SoftObject> softObj = new SoftReference<>(new SoftObject());
      System.out.println("第一次GC前,软引用:" + softObj.get());
      System.gc();
      System.out.println("第一次GC后,软引用:" + softObj.get());
      SoftObject obj = new SoftObject();
      System.out.println("分配100M强引用,软引用:" + softObj.get());
  }
}

添加 VM Option 参数:-Xmx200M 给堆内存分配最大200M内存

第一次 GC 前,软引用:SoftReferenceNormal$SoftObject@1b6d3586
第一次 GC 后,软引用:SoftReferenceNormal$SoftObject@1b6d3586
分配 100M 强引用,软引用:null
  1. 添加参数后位堆内存分配最大 200M 空间,分配给 softObj 对象 120M。

  2. 第一次 GC 后,因为剩余内存任然够,所以软引用并没有被回收。

  3. 当分配 100M 强引用后,堆内存空间不够,会触发GC回收,回收掉软引用。

软引用隐藏的问题

【注意】

被软引用对象关联的对象会自动被垃圾回收器回收,但是软引用对象本身也是一个对象,这些创建的软引用并不会自动被垃圾回收器回收掉。

public class SoftReferenceTest {

  static class SoftObject {
      byte[] data = new byte[1024]; // 占用1k空间
  }

  private static final int _100K = 100 * 1024;
  // 静态集合保存软引用,会导致这些软引用对象本身无法被垃圾回收器回收
  private static Set<SoftReference<SoftObject>> cache = new HashSet<>(_100K);

  public static void main(String[] args) {
      for (int i = 0; i < _100K; i++) {
          SoftObject obj = new SoftObject();
          cache.add(new SoftReference(obj));
          if (i * 10000 == 0) {
              System.out.println("cache size is " + cache.size());
          }
      }
      System.out.println("END");
  }
}

添加 VM Option 参数:-Xms4m -Xmx4m -Xmn2m

// 打印结果:
cache size is 1
Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded
at SoftReferenceTest$SoftObject.<init>(SoftReferenceTest.java:8)
at SoftReferenceTest.main(SoftReferenceTest.java:17)

程序崩溃,崩溃的原因并不是堆内存溢出,而是超出了 GC 开销限制。

这里错误的原因是:JVM 不停的回收软引用中的对象,回收次数过快,回收内存较小,占用资源过高了。

【解决方案】注册一个引用队列,将这个对象从 Set 中移除掉。

public class SoftReferenceTest {

  static class SoftObject {
      byte[] data = new byte[1024]; // 占用1k空间
  }

  private static final int _100K = 100 * 1024;
  // 静态集合保存软引用,会导致这些软引用对象本身无法被垃圾回收器回收
  private static Set<SoftReference<SoftObject>> cache = new HashSet<>(_100K);
  // 解决方案:注册一个引用队列,将要移除的对象从中删除
  private static ReferenceQueue<SoftObject> queue = new ReferenceQueue<>();
  // 记录清空次数
  private static int removeReferenceIndex = 0;

  public static void main(String[] args) {
      for (int i = 0; i < _100K; i++) {
          SoftObject obj = new SoftObject();
          cache.add(new SoftReference(obj, queue));
          // 清除掉软引用
          removeSoft();
          if (i * 10000 == 0) {
              System.out.println("cache size is " + cache.size());
          }
      }
      System.out.println("END removeReferenceIndex: " + removeReferenceIndex);
  }

  private static void removeSoft() {
      Reference<? extends SoftObject> poll = queue.poll();
      while (poll != null) {
          if (cache.remove(poll)) {
              removeReferenceIndex++;
          }
          poll = queue.poll();
      }
  }
}
// 打印结果:
cache size is 1
END removeReferenceIndex: 101745

作者:沅兮
来源:https://juejin.cn/post/7037330678731505672


收起阅读 »

String还有长度限制?是多少?

前言 话说Java中String是有长度限制的,听到这里很多人不禁要问,String还有长度限制?是的有,而且在JVM编译中还有规范,而且有的家人们在面试的时候也遇到了。 String 首先要知道String的长度限制我们就需要知道String是怎么存储字符串...
继续阅读 »

前言


话说Java中String是有长度限制的,听到这里很多人不禁要问,String还有长度限制?是的有,而且在JVM编译中还有规范,而且有的家人们在面试的时候也遇到了。


String


首先要知道String的长度限制我们就需要知道String是怎么存储字符串的,String其实是使用的一个char类型的数组来存储字符串中的字符的。



那么String既然是数组存储那数组会有长度的限制吗?是的有限制,但是是在有先提条件下的,我们看看String中返回length的方法。



由此我们看到返回值类型是int类型,Java中定义数组是可以给数组指定长度的,当然不指定的话默认会根据数组元素来指定:


int[] arr1 = new int[10]; // 定义一个长度为10的数组
int[] arr2 = {1,2,3,4,5}; // 那么此时数组的长度为5
复制代码

整数在java中是有限制的,我们通过源码来看看int类型对应的包装类Integer可以看到,其长度最大限制为2^31 -1,那么说明了数组的长度是0~2^31-1,那么计算一下就是(2^31-1 = 2147483647 = 4GB)



看到这我们尝试通过编码来验证一下上述观点。



以上是我通过定义字面量的形式构造的10万个字符的字符串,编译之后虚拟机提示报错,说我们的字符串长度过长,不是说好了可以存21亿个吗?为什么才10万个就报错了呢?


其实这里涉及到了JVM编译规范的限制了,其实JVM在编译时,如果我们将字符串定义成了字面量的形式,编译时JVM是会将其存放在常量池中,这时候JVM对这个常量池存储String类型做出了限制,接下来我们先看下手册是如何说的。



常量池中,每个 cp_info 项的格式必须相同,它们都以一个表示 cp_info 类型的单字节 “tag”项开头。后面 info[]项的内容 由tag 的类型所决定。



我们可以看到 String类型的表示是 CONSTANT_String ,我们来看下CONSTANT_String具体是如何定义的。



这里定义的 u2 string_index 表示的是常量池的有效索引,其类型是CONSTANT_Utf8_info 结构体表示的,这里我们需要注意的是其中定义的length我们看下面这张图。



在class文件中u2表示的是无符号数占2个字节单位,我们知道1个字节占8位,2个字节就是16位 ,那么2个字节能表示的范围就是2^16- 1 = 65535 。范中class文件格式对u1、u2的定义的解释做了一下摘要:


#这里对java虚拟机规摘要部分


##1、class文件中文件内容类型解释


定义一组私有数据类型来表示 Class 文件的内容,它们包括 u1,u2 和 u4,分别代 表了 1、2 和 4 个字节的无符号数。


每个 Class 文件都是由 8 字节为单位的字节流组成,所有的 16 位、32 位和 64 位长度的数 据将被构造成 2 个、4 个和 8 个 8 字节单位来表示。


##2、程序异常处理的有效范围解释


start_pc 和 end_pc 两项的值表明了异常处理器在 code[]数组中的有效范围。


start_pc 必须是对当前 code[]数组中某一指令的操作码的有效索引,end_pc 要 么是对当前 code[]数组中某一指令的操作码的有效索引,要么等于 code_length 的值,即当前 code[]数组的长度。start_pc 的值必须比 end_pc 小。


当程序计数器在范围[start_pc, end_pc)内时,异常处理器就将生效。即设 x 为 异常句柄的有效范围内的值,x 满足:start_pc ≤ x < end_pc


实际上,end_pc 值本身不属于异常处理器的有效范围这点属于 Java 虚拟机历史上 的一个设计缺陷:如果 Java 虚拟机中的一个方法的 code 属性的长度刚好是 65535 个字节,并且以一个 1 个字节长度的指令结束,那么这条指令将不能被异常处理器 所处理。


不过编译器可以通过限制任何方法、实例初始化方法或类初始化方法的code[]数组最大长度为 65534,这样可以间接弥补这个 BUG。



注意:这里对个人认为比较重要的点做了标记,首先第一个加粗说白了就是说数组有效范围就是【0-65565】但是第二个加粗的地方又解释了,因为虚拟机还需要1个字节的指令作为结束,所以其实真正的有效范围是【0-65564】,这里要注意这里的范围仅限编译时期,如果你是运行时拼接的字符串是可以超出这个范围的。



接下来我们通过一个小实验来测试一下我们构建一个长度为65534的字符串,看看是否就能编译通过。0期阶段汇总


首先通过一个for循环构建65534长度的字符串,在控制台打印后,我们通过自己度娘的一个在线字符统计工具计算了一下确实是65534个字符,如下:




然后我们将字符复制后以定义字面量的形式赋值给字符串,可以看到我们选择这些字符右下角显示的确实是65534,于是乎运行了一波,果然成功了。




#看到这里我们来总结一下:


##字符串有长度限制吗?是多少?


首先字符串的内容是由一个字符数组 char[] 来存储的,由于数组的长度及索引是整数,且String类中返回字符串长度的方法length() 的返回值也是int ,所以通过查看java源码中的类Integer我们可以看到Integer的最大范围是2^31 -1,由于数组是从0开始的,所以数组的最大长度可以使【0~2^31】通过计算是大概4GB。


但是通过翻阅java虚拟机手册对class文件格式的定义以及常量池中对String类型的结构体定义我们可以知道对于索引定义了u2,就是无符号占2个字节,2个字节可以表示的最大范围是2^16 -1 = 65535。


其实是65535,但是由于JVM需要1个字节表示结束指令,所以这个范围就为65534了。超出这个范围在编译时期是会报错的,但是运行时拼接或者赋值的话范围是在整形的最大范围。


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

ASM字节码插桩

ASM字节码插桩 一、什么是插桩 QQ空间曾经发布的热修复解决方案中利用Javaassist库实现向类的构造函数中插入一段代码解决CLASS_ISPREVERIFIED 问题。包括了Instant Run的实现以及参照Instant Run实现的热修复美团Ro...
继续阅读 »

ASM字节码插桩


一、什么是插桩


QQ空间曾经发布的热修复解决方案中利用Javaassist库实现向类的构造函数中插入一段代码解决CLASS_ISPREVERIFIED 问题。包括了Instant Run的实现以及参照Instant Run实现的热修复美团Robus等都利用到了插桩技术。


插桩就是将一段代码插入或者替换原本的代码。字节码插桩顾名思义就是在我们编写的源码编译成字节码(Class)后,在Android下生成dex之前修改Class文件,修改或者增强原有代码逻辑的操作。


插桩前.png


插桩后.png


我们需要查看方法执行耗时,如果每一个方法都需要自己手动去加入这些内容,当不需要时也需要一个个删去相应的代码。1个、两个方法还好,如果有10个、20个得多麻烦!所以可以利用注解来标记需要插桩的方法,结合编译后操作字节码来帮助我们自动插入,当不需要时关掉插桩即可。这种AOP思想让我们只需要关注插桩代码本身。


二、字节码操作框架


上面我们提到QQ空间使用了Javaassist来进行字节码插桩,除了Javaassist之外还有一个应用更为广泛的ASM框架同样也是字节码操作框架,Instant Run包括AspectJ就是借助ASM来实现各自的功能。


我们非常熟悉的JSON格式数据是基于文本的,我们只需要知道它的规则就能够轻松的生成、修改JSON数据。同样的Class字节码也有其自己的规则(格式)。操作JSON可以借助GSON来非常方便的生成、修改JSON数据。而字节码Class,同样可以借助Javassist/ASM来实现对其修改。



字节码操作框架的作用在于生成或者修改Class文件,因此在Android中字节码框架本身是不需要打包进入APK的,只有其生成/修改之后的Class才需要打包进入APK中。它的工作时机在上图Android打包流程中的生成Class之后,打包dex之前。


三、ASM的使用


由于ASM具有相对于Javassist更好的性能以及更高的灵活行,我们这篇文章以使用ASM为主。在真正利用到Android中之前,我们可以先在Java程序中完成对字节码的修改测试。


3.1、在AS中引入ASM


ASM可以直接从jcenter()仓库中引入,所以我们可以进入:bintray.com/进行搜索



点击图中标注的工件进入,可以看到最新的正式版本为:7.1。



因此,我们可以在AS中加入:


引入ASM.png


同时,需要注意的是:我们使用testImplementation引入,这表示我们只能在Java的单元测试中使用这个框架,对我们Android中的依赖关系没有任何影响。



AS中使用gradle的Android工程会自动创建Java单元测试与Android单元测试。测试代码分别在test与androidTest。



3.2、准备待插桩Class


test/java下面创建一个Java类:


public class InjectTest {

public static void main(String[] args) {

}
}
</pre>

由于我们操作的是字节码插桩,所以可以进入test/java下面使用javac对这个类进行编译生成对应的class文件。


javac InjectTest.java

3.3、执行插桩


因为main方法中没有任何输出代码,我们输入命令:java InjectTest执行这个Class不会有任何输出。那么我们接下来利用ASM,向main方法中插入一开始图中的记录函数执行时间的日志输出。


在单元测试中写入测试方法


<pre spellcheck="false" lang="java" cid="n37" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;"> /**
* 1、准备待分析的class
*/
FileInputStream fis = new FileInputStream
("xxxxx/test/java/InjectTest.class");

/**
* 2、执行分析与插桩
*/
//class字节码的读取与分析引擎
ClassReader cr = new ClassReader(fis);
// 写出器 COMPUTE_FRAMES 自动计算所有的内容,后续操作更简单
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
//分析,处理结果写入cw EXPAND_FRAMES:栈图以扩展格式进行访问
cr.accept(new ClassAdapterVisitor(cw), ClassReader.EXPAND_FRAMES);


/**
* 3、获得结果并输出
*/
byte[] newClassBytes = cw.toByteArray();
File file = new File("xxx/test/java2/");
file.mkdirs();

FileOutputStream fos = new FileOutputStream
("xxx/test/java2/InjectTest.class");
fos.write(newClassBytes);

fos.close();</pre>

关于ASM框架本身的设计,我们这里先不讨论。上面的代码会获取上一步生成的class,然后由ASM执行完插桩之后,将结果输出到test/java2目录下。其中关键点就在于第2步中,如何进行插桩。


把class数据交给ClassReader,然后进行分析,类似于XML解析,分析结果会以事件驱动的形式告知给accept的第一个参数ClassAdapterVisitor


<pre spellcheck="false" lang="java" cid="n41" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">public class ClassAdapterVisitor extends ClassVisitor {

public ClassAdapterVisitor(ClassVisitor cv) {
super(Opcodes.ASM7, cv);
}

@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature,
String[] exceptions) {
System.out.println("方法:" + name + " 签名:" + desc);

MethodVisitor mv = super.visitMethod(access, name, desc, signature,
exceptions);
return new MethodAdapterVisitor(api,mv, access, name, desc);
}
}</pre>

分析结果通过ClassAdapterVisitor获得,一个类中会存在方法、注解、属性等,因此ClassReader会将调用ClassAdapterVisitor中对应的visitMethodvisitAnnotationvisitField这些visitXX方法。


我们的目的是进行函数插桩,因此重写visitMethod方法,在这个方法中我们返回一个MethodVisitor方法分析器对象。一个方法的参数、注解以及方法体需要在MethodVisitor中进行分析与处理。


<pre spellcheck="false" lang="java" cid="n45" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">package com.enjoy.asminject.example;

import com.enjoy.asminject.ASMTest;

import org.objectweb.asm.AnnotationVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Type;
import org.objectweb.asm.commons.AdviceAdapter;
import org.objectweb.asm.commons.Method;

/**
* AdviceAdapter: 子类
* 对methodVisitor进行了扩展, 能让我们更加轻松的进行方法分析
*/
public class MethodAdapterVisitor extends AdviceAdapter {

private boolean inject;

protected MethodAdapterVisitor(int api, MethodVisitor methodVisitor, int access, String name, String descriptor) {
super(api, methodVisitor, access, name, descriptor);
}


/**
* 分析方法上面的注解
* 在这里干嘛???
* <p>
* 判断当前这个方法是不是使用了injecttime,如果使用了,我们就需要对这个方法插桩
* 没使用,就不管了。
*
* @param desc
* @param visible
* @return
*/
@Override
public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
if (Type.getDescriptor(ASMTest.class).equals(desc)) {
System.out.println(desc);
inject = true;
}
return super.visitAnnotation(desc, visible);
}

private int start;

@Override
protected void onMethodEnter() {
super.onMethodEnter();
if (inject) {
//执行完了怎么办? 记录到本地变量中
invokeStatic(Type.getType("Ljava/lang/System;"),
new Method("currentTimeMillis", "()J"));

start = newLocal(Type.LONG_TYPE); //创建本地 LONG类型变量
//记录 方法执行结果给创建的本地变量
storeLocal(start);
}
}

@Override
protected void onMethodExit(int opcode) {
super.onMethodExit(opcode);
if (inject){
invokeStatic(Type.getType("Ljava/lang/System;"),
new Method("currentTimeMillis", "()J"));
int end = newLocal(Type.LONG_TYPE);
storeLocal(end);

getStatic(Type.getType("Ljava/lang/System;"),"out",Type.getType("Ljava/io" +
"/PrintStream;"));

//分配内存 并dup压入栈顶让下面的INVOKESPECIAL 知道执行谁的构造方法创建StringBuilder
newInstance(Type.getType("Ljava/lang/StringBuilder;"));
dup();
invokeConstructor(Type.getType("Ljava/lang/StringBuilder;"),new Method("<init>","()V"));


visitLdcInsn("execute:");
invokeVirtual(Type.getType("Ljava/lang/StringBuilder;"),new Method("append","(Ljava/lang/String;)Ljava/lang/StringBuilder;"));

//减法
loadLocal(end);
loadLocal(start);
math(SUB,Type.LONG_TYPE);


invokeVirtual(Type.getType("Ljava/lang/StringBuilder;"),new Method("append","(J)Ljava/lang/StringBuilder;"));
invokeVirtual(Type.getType("Ljava/lang/StringBuilder;"),new Method("toString","()Ljava/lang/String;"));
invokeVirtual(Type.getType("Ljava/io/PrintStream;"),new Method("println","(Ljava/lang/String;)V"));

}
}
}</pre>

MethodAdapterVisitor继承自AdviceAdapter,其实就是MethodVisitor 的子类,AdviceAdapter封装了指令插入方法,更为直观与简单。


上述代码中onMethodEnter进入一个方法时候回调,因此在这个方法中插入指令就是在整个方法最开始加入一些代码。我们需要在这个方法中插入long s = System.currentTimeMillis();。在onMethodExit中即方法最后插入输出代码。


<pre spellcheck="false" lang="java" cid="n48" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">@Override
protected void onMethodEnter() {
super.onMethodEnter();
if (inject) {
//执行完了怎么办? 记录到本地变量中
invokeStatic(Type.getType("Ljava/lang/System;"),
new Method("currentTimeMillis", "()J"));

start = newLocal(Type.LONG_TYPE); //创建本地 LONG类型变量
//记录 方法执行结果给创建的本地变量
storeLocal(start);
}
}</pre>

这里面的代码怎么写?其实就是long s = System.currentTimeMillis();这句代码的相对的指令。我们可以先写一份代码


<pre spellcheck="false" lang="java" cid="n50" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">void test(){
//插入的代码
long s = System.currentTimeMillis();
/**
* 方法实现代码....
*/
//插入的代码
long e = System.currentTimeMillis();
System.out.println("execute:"+(e-s)+" ms.");
}</pre>

然后使用javac编译成Class再使用javap -c查看字节码指令。也可以借助插件来查看,就不需要我们手动执行各种命令。


插件安装.png


安装完成之后,可以在需要插桩的类源码中点击右键:


查看字节码.png


点击ASM Bytecode Viewer之后会弹出


字节码.png


所以第20行代码:long s = System.currentTimeMillis();会包含两个指令:INVOKESTATICLSTORE


再回到onMethodEnter方法中


<pre spellcheck="false" lang="java" cid="n59" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">@Override
protected void onMethodEnter() {
super.onMethodEnter();
if (inject) {
//invokeStatic指令,调用静态方法
invokeStatic(Type.getType("Ljava/lang/System;"),
new Method("currentTimeMillis", "()J"));
//创建本地 LONG类型变量
start = newLocal(Type.LONG_TYPE);
//store指令 将方法执行结果从操作数栈存储到局部变量
storeLocal(start);
}
}</pre>

而`onMethodExit`也同样根据指令去编写代码即可。最终执行完插桩之后,我们就可以获得修改后的class数据。

四、Android中的实现


在Android中实现,我们需要考虑的第一个问题是如何获得所有的Class文件来判断是否需要插桩。Transform就是干这件事情的。


相关视频


Android项目实战 微信Matrix卡顿监控方案,函数自动埋点实践


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

Flutter 单例的实现

和谐学习!不急不躁!!我是你们的老朋友小青龙~ 前言 回顾iOS,单例的写法如下: static JXWaitingView *shared; +(JXWaitingView*)sharedInstance{ static dispatch_once_t...
继续阅读 »

和谐学习!不急不躁!!我是你们的老朋友小青龙~


前言


回顾iOS,单例的写法如下:


static JXWaitingView *shared;

+(JXWaitingView*)sharedInstance{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
shared=[[JXWaitingView alloc]initWithTitle:nil];
});
return shared;
}

其目的是通过dispatch_once来控制【初始化方法】只会执行一次,然后用static修饰的对象来接收并返回它。所以核心是只会执行一次初始化


创建单例


创建单例的案例


class Student {
String? name;
int? age;
//构造方法
Student({this.name, this.age});

// 单例方法
static Student? _dioInstance;
static Student instanceSingleStudent() {
if (_dioInstance == null) {
_dioInstance = Student();
}
return _dioInstance!;
}
}

测试单例效果


测试一


import 'package:flutter_async_programming/Student.dart';

void main() {
Student studentA = Student.instanceSingleStudent();
studentA.name = "张三";
Student studentB = Student.instanceSingleStudent();
print('studentA姓名是${studentA.name}');
print('studentB姓名是${studentB.name}');
}

运行效果


image.png


测试二


import 'package:flutter_async_programming/Student.dart';

void main() {
Student studentA = Student.instanceSingleStudent();
studentA.name = "张三";
Student studentB = Student.instanceSingleStudent();
studentB.name = "李四";
print('studentA姓名是${studentA.name}');
print('studentB姓名是${studentB.name}');
}

运行效果


image.png


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

Android 图形处理 —— Matrix 原理剖析

Matrix 简介 Android 图形库中的 android.graphics.Matrix 是一个 3×3 的 float 矩阵,其主要作用是坐标变换 它的结构大概是这样的 其中每个位置的数值作用和其名称所代表的的含义是一一对应的 MSCALE_X、M...
继续阅读 »

Matrix 简介


Android 图形库中的 android.graphics.Matrix 是一个 3×3 的 float 矩阵,其主要作用是坐标变换


它的结构大概是这样的


matrix


其中每个位置的数值作用和其名称所代表的的含义是一一对应的



  • MSCALE_X、MSCALE_Y:控制缩放

  • MTRANS_X、MTRANS_Y:控制平移

  • MSKEW_X、MSKEW_X:控制错切

  • MSCALE_X、MSCALE_Y、MSKEW_X、MSKEW_X:控制旋转

  • MPERSP_0、MPERSP_1、MPERSP_2:控制透视


matrix_1


在 Android 中,我们直接实例化一个 Matrix,内部的矩阵长这样:


matrix_3


是一个左上到右下为 1,其余为 0 的矩阵,也叫单位矩阵,一般数学上表示为 I


Matrix 坐标变换原理


前面说到 Matirx 主要的作用就是处理坐标的变换,而坐标的基本变换有:平移、缩放、旋转和错切



这里所说的基本变换,也称仿射变换 ,透视不属于仿射变化,关于透视相关的内容不在本文的范围内



当矩阵的最后一行是 0,0,1 代表该矩阵是仿射矩阵,下文中所有的矩阵默认都是仿射矩阵


线性代数中的矩阵乘法


在正式介绍 Matrix 是如何控制坐标变换的原理之前,我们先简单复习一下线性代数中的矩阵乘法,详细的讲解可参见维基百科或者翻翻大学的《线性代数》,这里只做最简单的介绍




  • 两个矩阵相乘,前提是第一个矩阵的列数等于第二个矩阵的行数




  • 若 A 为 m × n 的矩阵,B 为 n × p 的矩阵,则他们的乘积 AB 会是一个 m × p 的矩阵,表达可以写为





  • 由定义计算,AB 中任意一点(a,b)的值为 A 中第 a 行的数和 B 中第 b 列的数的乘积的和







了解矩阵乘法的基本方法之后,我们还需要记住几个性质,对后续的分析有用



  • 满足结合律,即 A(BC)=(AB)C

  • 满足分配律,即 A(B + C) = AB + AC (A + B)C = AC + BC

  • 不满足交换律,即 AB != BA

  • 单位矩阵 I 与任意矩阵相乘,等于矩阵本身,即 IA = ABI = B


缩放(Scale)


我们先想想,让我们实现把一个点 (x0, y0) 的 x 轴和 y 轴分别缩放 k1 和 k2 倍,我们会怎么做,很简单


val x = k1 * x0
val y = k2 * y0

那如果用矩阵怎么实现呢,前面我们讲到 Matrix 中 MSCALE_XMSCALE_Y 是用来控制缩放的,我们在这里填分别设置为 k1 和 k2,看起来是这样的


image-20211109103257621

而点 (x0, y0) 用矩阵表示是这样的


image-20211109103824496

有些人会疑问,最后一行这里不是还有一个 1 吗,这是使用了齐次坐标系的缘故,在数学中我们的点和向量都是这样表示的 (x, y),两者看起来一样,计算机无法区分,为了让计算机也可以区分它们,增加了一个标志位,即


(x, y, 1) -> 点
(x, y, 0) -> 向量

现在 Matrix 和点都可以用矩阵表示了,接下来我们看看怎么通过这两个矩阵得到一个缩放之后的点 (x, y). 前面我们已经介绍过矩阵的乘法,让我们看看把上面两个矩阵相乘会得到什么结果


image-20211109104922576

可以看到,矩阵相乘得到了一个(k1x0, k2y0,1)的矩阵,上面说过,计算机中,这个矩阵就代表点 (k1x0, k2y0), 而这个点刚好就是我们要的缩放之后的点


以上所有过程用代码来实现,看起来就是像下面这样


val xy = FloatArray(x0, y0)
Matrix().apply {
setScale(k1, k2)
mapPoints(xy)
}

平移(Translate)


平移和缩放也是类似的,实现平移,我们一般可写为


val x = x0 + deltaX
val y = y0 + deltaY

而用矩阵来实现则是


val xy = FloatArray(x0, y0)
Matrix().apply {
setTranslate(k1, k2)
mapPoints(xy)
}

换成数学表示


translate


根据矩阵乘法


x = 1 × x0 + 0 × y0 + deltaX × 1 = x0 + deltaX
y = 0 × x0 + 1 × y0 + deltaY × 1 = y0 + deltaY

可得和一开始的实现也是效果一致的


错切(Skew)


错切相对于平移和缩放,可能大部分人对这个名词比较陌生,直接看三张图大家可能会比较直观


水平错切


x = x0 + ky0
y = y0

矩阵表示



水平错切


垂直错切


x = x0
y = kx0 + y0

矩阵表示




复合错切


x = x0 + k1y0
y = k2x0 + y0

矩阵表示




旋转(Rotate)


旋转相对以上三种变化又有一点复杂,这里涉及一些三角函数的计算,忘记的可以去维基百科 先复习下



image-20211108215739508

同样我们先自己实现一下旋转,假设一个点 A(x0, y0), 距离原点的距离为 r,与水平夹角为 α,现绕原点顺时针旋转 θ 度,旋转之后的点为 B(x, y)



用矩阵表示




Matrix 复合操作原理


前面介绍了四种基本变换,如果我们需要同时应用上多种变化,比如先绕原点顺时针旋转 90° 再 x 轴平移 100,y 轴平移 100, 最后 x、y 轴缩放0.5 倍,那么就需要用到复合操作


还是先用自己的实现来实现一下


x = ((x0 · cosθ - y0 · sinθ) + 100) · 0.5
y = ((y0 · cosθ + x0 · sinθ) + 100) · 0.5

矩阵表示


image-20211206155715836


按照前面的方式逐个推导,最终也能得到和上述一样的结果


到此,我们可以对 Matrix 做出一个基本的认识:Matrix 基于矩阵计算的原理,解决了计算机中坐标映射和变化的问题


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

Glide线程池

hello:大家好我是 小小小小小鹿,一枚菜鸡Android程序猿。最近正在阅读Glide源码,今天我们要研究的部分是Glide 线程池的配置。 本次代码阅读主要有两个目标 弄清楚Glide是如何做线程池配置的 Glide如何进行优先级加载 Glide用来...
继续阅读 »

hello:大家好我是 小小小小小鹿,一枚菜鸡Android程序猿。最近正在阅读Glide源码,今天我们要研究的部分是Glide 线程池的配置。 本次代码阅读主要有两个目标



  1. 弄清楚Glide是如何做线程池配置的

  2. Glide如何进行优先级加载


Glide用来进行图片加载,我们知道当页面暂停的时候,glide可以根据页面的生命周期,来暂停当前页面的请求,但是如果当前页面通过滑动加载大量图片,那么Glide是怎么进行图片加载的呢?是先调用的加载在前还是后调用的加载在前面呢?如果某个页面的部分图片需要优先被加载,那么Glide又该如何处理呢?


Glide线程池的使用


Glide DecodeJob 的工作过程我们知道Glide在进行一次完成的数据加载会经历 ResourceCacheGenerator --> DataCacheGenerator --> SourceGenerator 的三个过程变化。而在这个过程变化中会涉及到两个线程池的使用。




  1. EngineJob#start开始本次请求


    public synchronized void start(DecodeJob<R> decodeJob) {
     this.decodeJob = decodeJob;
       //如果是从 缓存中获取图片使用 diskCacheExecutor
     GlideExecutor executor = decodeJob.willDecodeFromCache()
         ? diskCacheExecutor
        : getActiveSourceExecutor();
     executor.execute(decodeJob);
    }

    private GlideExecutor getActiveSourceExecutor() {
        //如果useUnlimitedSourceGeneratorPool 为true 使用无限制的线程池
        //如果useAnimationPool 为true且如果useUnlimitedSourceGeneratorPool为false 使用动画线程池 否则使用sourceExecutor
       return useUnlimitedSourceGeneratorPool
           ? sourceUnlimitedExecutor : (useAnimationPool ? animationExecutor : sourceExecutor);
    }



  2. EngineJob#reschedule重新进行调度


    @Override
    public void reschedule(DecodeJob<?> job) {
     //此时线程池的使用逻辑和EngineJob#start不在文件中加载数据一致
     getActiveSourceExecutor().execute(job);
    }



Glide线程池的配置


Glide Excutor参数初始化来自于GlideBuilder#build 而这些在不额外设置的情况下都来自于GlideExecutor。而GlideExecutor的所有线程池都是通过配置ThreadPoolExecutor来完成的。


初识ThreadPoolExecutor


ExecutorService是最初的线程池接口,ThreadPoolExecutor类是对线程池的具体实现,它通过构造方法来配置线程池的参数。


public ThreadPoolExecutor(int corePoolSize,
                             int maximumPoolSize,
                             long keepAliveTime,
                             TimeUnit unit,
                             BlockingQueue<Runnable> workQueue,
                             ThreadFactory threadFactory,
                             RejectedExecutionHandler handler) {
       if (corePoolSize < 0 ||
           maximumPoolSize <= 0 ||
           maximumPoolSize < corePoolSize ||
           keepAliveTime < 0)
           throw new IllegalArgumentException();
       if (workQueue == null || threadFactory == null || handler == null)
           throw new NullPointerException();
       this.corePoolSize = corePoolSize;
       this.maximumPoolSize = maximumPoolSize;
       this.workQueue = workQueue;
       this.keepAliveTime = unit.toNanos(keepAliveTime);
       this.threadFactory = threadFactory;
       this.handler = handler;
  }

参数解释:


corePoolSize,线程池中核心线程的数量,默认情况下,即使核心线程没有任务在执行它也存在的,我们固定一定数量的核心线程且它一直存活这样就避免了一般情况下CPU创建和销毁线程带来的开销。我们如果将ThreadPoolExecutor的allowCoreThreadTimeOut属性设置为true,那么闲置的核心线程就会有超时策略,这个时间由keepAliveTime来设定,即keepAliveTime时间内如果核心线程没有回应则该线程就会被终止。allowCoreThreadTimeOut默认为false,核心线程没有超时时间。 maximumPoolSize,线程池中的最大线程数,当任务数量超过最大线程数时其它任务可能就会被阻塞。最大线程数=核心线程+非核心线程。非核心线程只有当核心线程不够用且线程池有空余时才会被创建,执行完任务后非核心线程会被销毁。 keepAliveTime,非核心线程的超时时长,当闲置时间超过这个时间时,非核心线程就会被回收。当allowCoreThreadTimeOut设置为true时,此属性也作用在核心线程上。 unit,枚举时间单位,TimeUnit。 workQueue,线程池中的任务队列,我们提交给线程池的runnable会被存储在这个对象上。 线程池的分配遵循这样的规则:


当线程池中的核心线程数量未达到最大线程数时,启动一个核心线程去执行任务; 如果线程池中的核心线程数量达到最大线程数时,那么任务会被插入到任务队列中排队等待执行; 如果在上一步骤中任务队列已满但是线程池中线程数量未达到限定线程总数,那么启动一个非核心线程来处理任务; 如果上一步骤中线程数量达到了限定线程总量,那么线程池则拒绝执行该任务,且ThreadPoolExecutor会调用RejectedtionHandler的rejectedExecution方法来通知调用者。


threadFactory:线程工厂,为线程池提供创建新线程的能力。


DiskCacheExecutor的配置过程


GlideExecutor提供了三个创建DiskCacheExecutor的方法,最终都会调用到有三个参数那个


public static GlideExecutor newDiskCacheExecutor(
   int threadCount, String name, UncaughtThrowableStrategy uncaughtThrowableStrategy) {
 return new GlideExecutor(
     new ThreadPoolExecutor(
         threadCount /* corePoolSize */,
         threadCount /* maximumPoolSize */,
         0 /* keepAliveTime */,
         TimeUnit.MILLISECONDS,
         new PriorityBlockingQueue<Runnable>(),
         new DefaultThreadFactory(name, uncaughtThrowableStrategy, true)));
}

在默认创建的时候,调用的是无参数的那个,threadCount 值为1 即DiskCacheExecutor是一个核心线程数为1,没有非核心线程的线程池,所有任务在线程池中串行执行,Runnable的存储对象是PriorityBlockingQueue。


SourceExecutor的配置过程


public static GlideExecutor newSourceExecutor() {
 return newSourceExecutor(
     calculateBestThreadCount(),
     DEFAULT_SOURCE_EXECUTOR_NAME,
     UncaughtThrowableStrategy.DEFAULT);
}

public static GlideExecutor newSourceExecutor(
     int threadCount, String name, UncaughtThrowableStrategy uncaughtThrowableStrategy) {
   return new GlideExecutor(
       new ThreadPoolExecutor(
           threadCount /* corePoolSize */,
           threadCount /* maximumPoolSize */,
           0 /* keepAliveTime */,
           TimeUnit.MILLISECONDS,
           new PriorityBlockingQueue<Runnable>(),
           new DefaultThreadFactory(name, uncaughtThrowableStrategy, false)));
}

可以看到SourceExecutor的构建过程和基本一致,不同的地方在于核心线程的数量是通过calculateBestThreadCount来动态计算的。


if (bestThreadCount == 0) {
   //如果cpu核心数超过4则核心线程数为4 如果Cpu核心数小于4那么使用Cpu核心数作为核心线程数量
 bestThreadCount =
     Math.min(MAXIMUM_AUTOMATIC_THREAD_COUNT, RuntimeCompat.availableProcessors());
}
return bestThreadCount;

UnlimitedSourceExecutor无限制的线程池


public static GlideExecutor newUnlimitedSourceExecutor() {
 return new GlideExecutor(new ThreadPoolExecutor(
     0,
     Integer.MAX_VALUE,
     KEEP_ALIVE_TIME_MS,
     TimeUnit.MILLISECONDS,
     new SynchronousQueue<Runnable>(),
     new DefaultThreadFactory(
         SOURCE_UNLIMITED_EXECUTOR_NAME,
         UncaughtThrowableStrategy.DEFAULT,
         false)));
}

UnlimitedSourceExecutor没有核心线程,非核心线程数量无限大。


AnimationExecutor


public static GlideExecutor newAnimationExecutor() {
 int bestThreadCount = calculateBestThreadCount();
 int maximumPoolSize = bestThreadCount >= 4 ? 2 : 1;
 return newAnimationExecutor(maximumPoolSize, UncaughtThrowableStrategy.DEFAULT);
}

public static GlideExecutor newAnimationExecutor(
     int threadCount, UncaughtThrowableStrategy uncaughtThrowableStrategy) {
    return new GlideExecutor(
       new ThreadPoolExecutor(
           0 /* corePoolSize */,
           threadCount,
           KEEP_ALIVE_TIME_MS,
           TimeUnit.MILLISECONDS,
           new PriorityBlockingQueue<Runnable>(),
           new DefaultThreadFactory(
               ANIMATION_EXECUTOR_NAME,
               uncaughtThrowableStrategy,
               true)));
}

AnimationExecutor没有核心线程,非核心线程数量根据Cpu核心数来决定,当Cpu核心数大于等4时 非核心线程数为2,否则为1。


Glide线程池总结


DiskCacheExecutor和SourceExecutor 采用固定核心线程数固定,适用于处理CPU密集型的任务,但是没有非核心线程。确保CPU在长期被工作线程使用的情况下,尽可能的少的分配线程,即适用执行长期的任务。


UnlimitedSourceExecutor采用无核心线程,非核心线程无限大适用于并发执行大量短期的小任务。在空闲的时候消耗资源非常少。


AnimationExecutor没有核心线程,非核心线程有限,同UnlimitedSourceExecutor的区别就是核心线程数量和工作队列不一致。第一次看到这么用。


Glide如何实现加载优先级


除了UnlimitedSourceExecutor其余的都是使用的PriorityBlockingQueue。PriorityBlockingQueue是一个具有优先级的无界阻塞队列。也就是说优先级越高越先执行。


我们知道图片的加载是在线程池中执行的DecodeJob,DecodeJob实现了Runnable和Comparable接口。当DecodeJob被提交到线程池的时候,如果需要加入工作队列会通过compareTo比较Decodejob优先级


@Override
public int compareTo(@NonNull DecodeJob<?> other) {
 //先比较 Priority  
 int result = getPriority() - other.getPriority();
 //如果 Priority优先级一致 ,比较order order是一个自增的int 每一次初始化DecodeJob 都会执行++ 因此后初始化的DecodeJob比先初始化的优先级高。
 if (result == 0) {
   result = order - other.order;
}
 return result;
}
作者:小小小小小鹿
链接:https://juejin.cn/post/7038795986482757669
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Kotlin开发中的一些Tips

作用域函数选择 目前有let、run、with、apply 和 also五个作用域函数。 官方文档有张表来说明它们之间的区别:   总结一下有几点区别: 1、apply和also返回上下文对象。 2、let、run 和with返回lambda 结果。 3、l...
继续阅读 »

作用域函数选择


目前有letrunwithapply 和 also五个作用域函数。


官方文档有张表来说明它们之间的区别: 



 总结一下有几点区别:


1、applyalso返回上下文对象。


2、letrun 和with返回lambda 结果。


3、letalso引用对象是it ,其余是this


1.letrun是我日常使用最多的两个,它们之间很类似。


private var textView: TextView? = null

textView?.let {
it.text = "Kotlin"
it.textSize = 14f
}

textView?.run {
text = "Kotlin"
textSize = 14f
}

相比较来说使用run显得比较简洁,但let的优势在于可以将it重命名,提高代码的可读性,也可以避免作用域函数嵌套时导致混淆上下文对象的情况。


2.对于可空对象,使用let比较方便。对于非空对象可以使用with


3.applyalso也非常相似,文档给出的建议是如果是对象配置操作使用apply,额外的处理使用also。例如:


val numberList = mutableListOf()
numberList.also { println("Populating the list") }
.apply {
add(2.71)
add(3.14)
add(1.0)
}
.also { println("Sorting the list") }
.sort()

简单说就是符合单词的含义使用,提高代码可读性。


总的来说,这几种函数有许多重叠的部分,因此可以根据开发中的具体情况来使用。以上仅做参考。


Sequence


我们经常会使用到kotlin的集合操作符,比如 map 和 filter 等。


list.map {
it * 2
}.filter {
it % 3 == 0
}

老规矩,看一下反编译后的代码: 



就干了这么点事情,创建了两个集合,循环了两遍。效率太低,这还不如自己写个for循环,一个循环就处理完了。看一下map的源码:


public inline fun  Iterable.map(transform: (T) -> R): List {
return mapTo(ArrayList(collectionSizeOrDefault(10)), transform)
}

public inline fun > Iterable.mapTo(destination: C, transform: (T) -> R): C {
for (item in this)
destination.add(transform(item))
return destination
}

内部实现确实如此,难道这些操作符不香了?


其实这时就可以使用Sequences(序列),用法很简单,只需要在集合后添加一个asSeqence() 方法。


list.asSequence().map {
it * 2
}.filter {
it % 3 == 0
}

反编译:


SequencesKt.filter(SequencesKt.map(CollectionsKt.asSequence((Iterable)list), (Function1)null.INSTANCE), (Function1)null.INSTANCE);

有两个Function1,其实就是lambda表达式,这是因为Sequence没有使用内联导致的。我们先看看SequencesKt.map源码:


public fun  Sequence.map(transform: (T) -> R): Sequence {
return TransformingSequence(this, transform)
}

internal class TransformingSequence
constructor(private val sequence: Sequence, private val transformer: (T) -> R) : Sequence {
override fun iterator(): Iterator = object : Iterator {
val iterator = sequence.iterator()
override fun next(): R {
return transformer(iterator.next())
}

override fun hasNext(): Boolean {
return iterator.hasNext()
}
}

internal fun flatten(iterator: (R) -> Iterator): Sequence {
return FlatteningSequence(sequence, transformer, iterator)
}
}

可以看到没有创建中间集合去循环,只是创建了一个Sequence对象,里面实现了迭代器。SequencesKt.filter方法也是类似。细心的话你会发现,这都只是创建Sequence对象,所以要想真正拿到处理后的集合,需要添加toList()这种末端操作。


map 和 filter 这类属于中间操作,返回的是一个新Sequence,里面有数据迭代时的实际处理。而 toList和first这类属于末端操作用来返回结果。


所以Sequence是延迟执行的,这也就是它为何不会出现我们一开始提到的问题,一次循环就处理完成了。


总结一下Sequence的使用场景:


1、有多个集合操作符时,建议使用Sequence。


2、数据量大的时候,这样可以避免重复创建中间集合。这个数据量大,怎么也是万以上的级别了。


所以对于一般Android开发中来说,不使用Sequence其实差别不大。。。哈哈。。


协程


有些人会错误理解kotlin的协程,觉得它的性能更高,是一种“轻量级”的线程,类似go语言的协程。但是如果你细想一下,这是不太可能的,最终它都是要在JVM上运行,java都没有的东西,你就实现了,你这不是打java的脸嘛。


所以对于JVM平台,kotlin的协程只能是对Thread API的封装,和我们用的Executor类似。所以对于协程的性能,我个人也认为差别不大。只能说kotlin借助语言简洁的优势,让操作线程变的更加简单。


之所以上面说JVM,是因为kotlin还有js和native平台。对于它们来说,或许可以实现真正的协程。


推荐扔物线大佬关于协程的文章,帮你更好的理解kotlin的协程:到底什么是「非阻塞式」挂起?协程真的更轻量级吗?


Checked Exception


这对熟悉Java的同学并不陌生,Checked Exception 是处理异常的一种机制,如果你的方法中声明了它可能会抛出的异常,编译器就会强制开发者对异常进行处理,否则编译不会通过。我们需要使用 try catch 捕获异常或者使用 throws 抛出异常处理它。


但是Kotlin中并不支持这个机制,也就是说不会强制你去处理抛出的异常。至于Checked Exception 好不好,争议也不少。这里就不讨论各自的优缺点了。


既然Kotlin中没有这个机制已经是既成事实,那么我们在使用中就需要考虑它带来的影响。比如我们开发中在调用一些方法时,要注意看一下源码中是否有指定异常抛出,然后做相应处理,避免不必要的崩溃。


例如常用的json解析:


private fun test() {
val jsonObject = JSONObject("{...}")
jsonObject.getString("id")
...
}

在java中我们需要处理JSONException,kotlin中因为没有Checked Exception,如果我们像上面这样直接使用,虽然程序可以运行,可是一但解析出现异常,程序就会崩溃。


Intrinsics检查


如果你经常观察反编译后的java代码,会发现有许多类似Intrinsics.checkXXX这样的代码。


fun test(str: String) {
println(str)
}

反编译: 



 比如图中的checkParameterIsNotNull就是用了检查参数是否为空。虽然我们的参数是不可控的,但是考虑到方法会被Java调用,Kotlin会默认的增加checkParameterIsNotNull校验。如果kotlin方法是私有的,也就不会有此行检查。


checkParameterIsNotNull并不会有性能问题,相反这种提前判断参数是否正确,可以避免程序向后执行导致不必要的资源消耗。


当然如果你想去除它,可以添加下面的配置到你的gradle文件,这样就会在编译时去除它。


kotlinOptions {
freeCompilerArgs = [
'-Xno-param-assertions',
'-Xno-call-assertions',
'-Xno-receiver-assertions'
]
}

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

一图秒懂CDN原理

CDN
前些天,线上灰度了一个功能,下午接到一些业务上报国外用户访问时图片无法显示,但是国内访问都是正常,所以怀疑是国外CDN问题导致。 先了说明下现状: 图片保存在阿里OSS中 国内使用了阿里云CDN 国外使用Akamai(全球CDN厂商) 按理说,CDN都有...
继续阅读 »

前些天,线上灰度了一个功能,下午接到一些业务上报国外用户访问时图片无法显示,但是国内访问都是正常,所以怀疑是国外CDN问题导致。


先了说明下现状:



  1. 图片保存在阿里OSS中

  2. 国内使用了阿里云CDN

  3. 国外使用Akamai(全球CDN厂商)



按理说,CDN都有,图片不应该访问不到。于是,在脑子中根据CDN的原理,先思考下可能的问题



CDN原理


CDN全称是Content Delivery Network,即内容分发网络,也称为内容传送网络。CDN是构建在现有网络基础之上的智能虚拟网络,依靠部署在各地的边缘服务器,使用户就近获取所需内容,降低网络拥塞,提高用户访问响应速度和命中率。


cdn.jpg


如上图CDN的逻辑主要分为两步:DNS解析请求边缘节点



用dig看下DNS解析结果:



$ dig juejin.cn

; <<>> DiG 9.10.6 <<>> juejin.cn
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 63296
;; flags: qr rd ra; QUERY: 1, ANSWER: 9, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 4096
;; QUESTION SECTION:
;juejin.cn. IN A

;; ANSWER SECTION:
juejin.cn. 412 IN CNAME juejin.cn.w.cdngslb.com.
juejin.cn.w.cdngslb.com. 60 IN A 112.85.251.229
juejin.cn.w.cdngslb.com. 60 IN A 112.85.251.227
juejin.cn.w.cdngslb.com. 60 IN A 112.85.251.231
juejin.cn.w.cdngslb.com. 60 IN A 112.85.251.224
juejin.cn.w.cdngslb.com. 60 IN A 112.85.251.225
juejin.cn.w.cdngslb.com. 60 IN A 112.85.251.230
juejin.cn.w.cdngslb.com. 60 IN A 112.85.251.226
juejin.cn.w.cdngslb.com. 60 IN A 112.85.251.228

;; Query time: 9 msec
;; SERVER: 192.168.3.1#53(192.168.3.1)
;; WHEN: Sat May 15 14:26:26 CST 2021
;; MSG SIZE rcvd: 203

在ANSWER SECTION列表可以看出



  1. juejin.cn为cname记录指向juejin.cn.w.cdngslb.com

  2. juejin.cn.w.cdngslb.com返回了7条A记录,这7个ip 信息是江苏 徐州 联通,我所在地是上海,联通,可以看出返回的都是就近节点。实际上CDN是有非常多的边缘节点。



用tcpdump来监控下DNS的UDP数据包




  1. 在一个窗口输入sudo tcpdump -n -s 1500 udp and port 53

  2. 在另一个窗口输入ping juejin.cn


监控到的UDP数据包如下:


21:49:13.960212 IP 192.168.3.201.52647 > 192.168.3.1.53: 37581+ A? juejin.cn. (27)
21:49:13.975290 IP 192.168.3.1.53 > 192.168.3.201.52647: 37581 9/0/0 CNAME juejin.cn.w.cdngslb.com., A 112.85.251.229, A 112.85.251.230, A 112.85.251.226, A 112.85.251.228, A 112.85.251.224, A 112.85.251.231, A 112.85.251.225, A 112.85.251.227 (192)

其中,192.168.3.1为路由器IP。也就是本机向路由器询问DNS解析,如果路由器已经缓存了,就会直接返回。


复现问题


我们回到问题中,如果CDN返回的边缘节点如果不出问题,图片应该是可以很快访问到的,CDN厂商不至于出现这个问题。那么问题在那里呢?


在公司环境无法复现问题,就要找一个最接近客户场景的环境来测试,于是想办法搞到一台香港window系统的测试机,远程上去一看,还果真有问题。



图片在界面中不显示,但是直接在浏览器访问是正常的,开发者模式下发现访问图片时出现跨域错误



一张正常显示的图片请求返回的http头是这样的:


Response Headers:
accept-ranges: bytes
access-control-allow-origin: *
etag: "A31F477F3232DA431D3B77543C3EBF92"
last-modified: Thu, 25 Jul 2019 01:37:03 GMT
...省略

1. access-control-allow-origin


通配符 * 表示允许被任何网站引用。如果想让资源只被指定域名访问,只需把*改为域名就行了,如下:


access-control-allow-origin: `https://juejin.cn`

2. etag


etag是http协议缓存逻辑中的一个属性。CDN的目的就是减少网络访问,因为缓存是必须要用的功能。


而无法显示的图片,返回的请求头是这样的:


Response Headers:
accept-ranges: bytes
etag: "A31F477F3232DA431D3B77543C3EBF92"
last-modified: Thu, 25 Jul 2019 01:37:03 GMT
...省略

没有access-control-allow-origin这一项,导致页面中无法加载。


浏览器边缘节点请求图片命中缓存,返回图片响应头中没有CORS属性抛出CORS异常,图片不渲染浏览器边缘节点


解决办法很简单,在CDN后台配置返回access-control-allow-origin信息即可


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

Flutter: 完成一个图片APP

自从 Flutter 推出之后, 一直是备受关注, 有看好的也有不看好的, 作为移动开发人员自然是要尝试一下的(但是它的嵌套写法真的难受), 本着学一个东西, 就一定要动手的态度, 平时又喜欢看一些猫狗的图片, 就想着做一个加载猫狗图片你的 APP, 界面图如...
继续阅读 »

自从 Flutter 推出之后, 一直是备受关注, 有看好的也有不看好的, 作为移动开发人员自然是要尝试一下的(但是它的嵌套写法真的难受), 本着学一个东西, 就一定要动手的态度, 平时又喜欢看一些猫狗的图片, 就想着做一个加载猫狗图片你的 APP, 界面图如下(界面不是很好看).






主要模块



NetWork

api.dart文件中, 分别定义了DogApi, CatApi两个类, 一个用于处理获取猫的图片的类, 一个用于处理狗的图片的类.


http_request.dart文件封装了Http请求, 用于发送和接收数据.


url.dart文件封装了需要用到的Api接口, 主要是为了方便和统一管理而编写.


Models文件夹下分别定义不同API接口返回数据的模型.


图片页

瀑布流使用的flutter_staggered_grid_view库, 作者自定义了Delegate计算布局, 使用起来非常简单.


Widget scene = new StaggeredGridView.countBuilder(
physics: BouncingScrollPhysics(),
itemCount: this.breedImgs != null ? this.breedImgs.urls.length : 0,
mainAxisSpacing: 4.0,
crossAxisSpacing: 4.0,
crossAxisCount: 3,
itemBuilder: (context, index) {
return new GestureDetector(
onTapUp: (TapUpDetails detail) {
// 展示该品种的相关信息
dynamic breed = this.breeds[this.selectedIdx].description;
// TODO: 取出当前点击的然后所有往后的
List<String> unreadImgs = new List<String>();
for (int i = index; i < this.breedImgs.urls.length; i++) {
unreadImgs.add(this.breedImgs.urls[i]);
}
AnimalImagesPage photoPage = new AnimalImagesPage(
listImages: unreadImgs,
breed: this.breeds[this.selectedIdx].name,
imgType: "Cat",
petInfo: this.breeds[this.selectedIdx],
);
Navigator.of(context)
.push(new MaterialPageRoute(builder: (context) {
return photoPage;
}));
},
child: new Container(
width: 100,
height: 100,
color: Color(0xFF2FC77D), //Colors.blueAccent,
child: new CachedNetworkImage(
imageUrl: this.breedImgs.urls[index],
fit: BoxFit.fill,
placeholder: (context, index) {
return new Center(child: new CupertinoActivityIndicator());
},
),
),
);
},
// 该属性可以控制当前 Cell 占用的空间大小, 用来实现瀑布的感觉
staggeredTileBuilder: (int index) =>
new StaggeredTile.count(1, index.isEven ? 1.5 : 1),
);

  • 组装PickerView


系统默认的 PickerView 在每一次切换都会回调, 而且没有确定和取消事件,
如果直接使用会造成频繁的网络请求, 内存消耗也太快, 所以组装了一下, 增加确定和取消才去执行网络请求, 这样就解决了这个问题.


    Widget column = Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
new Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
new Container(
width: MediaQuery.of(context).size.width,
height: 40,
child: new Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
new Padding(
padding: EdgeInsets.only(left: 10.0),
child: new GestureDetector(
onTapUp: (detail) {
// 点击了确定按钮, 退出当前页面
Navigator.of(context).pop();
// 回调操作
this.submit(this.selectedIndex);
},
child: new Text(
"确定",
style: TextStyle(
decoration: TextDecoration.none,
color: Colors.white,
fontSize: 18),
),
),
),
new Padding(
padding: EdgeInsets.only(right: 10.0),
child: new GestureDetector(
onTapUp: (detail) {
// 点击了确定按钮, 退出当前页面
Navigator.of(context).pop();
},
child: new Text(
"取消",
style: TextStyle(
decoration: TextDecoration.none,
color: Colors.white,
fontSize: 18),
),
),
)
],
),
),
],
),
new Container(
height: 1,
color: Colors.white,
),
// Picker
new Expanded(
child: new CupertinoPicker.builder(
backgroundColor: Colors.transparent,
itemExtent: 44,
childCount: this.names.length,
onSelectedItemChanged: (int selected) {
this.selectedIndex = selected;
this.onSelected(selected);
},
itemBuilder: (context, index) {
return new Container(
width: 160,
height: 44,
alignment: Alignment.center,
child: new Text(
this.names[index],
textAlign: TextAlign.right,
style: new TextStyle(
color: Colors.white,
fontSize: 16,
decoration: TextDecoration.none),
),
);
}),
)
],
);
详情页


  • Column 包含 ListView


详情页中, 上方是一个图片, 下方是关于品种的相关信息, 下方是通过 API获取到的属性进行一个展示, 需要注意一点是, 如果Column封装了MainAxis相同方向的滚动控件, 必须设置Width/Height, 同理, Row也是需要注意这一点的.


我在这里的做法是通过一个Container包裹 ListView.


new Container(
margin: EdgeInsets.only(bottom: 10, top: 10),
height: MediaQuery.of(context).size.height - MediaQuery.of(context).size.width / 1.2 - 80,
width: MediaQuery.of(context).size.width,
child: listView,
),

  • 图片动画


这一部分稍微复杂一些, 首先需要监听滑动的距离, 来对图片进行变换, 最后根据是否达到阈值来进行切换动画, 这里我没有实现在最后一张和第一张图片进行切换以至于可以无限循环滚动, 我在边界阈值上只是阻止了下一步动画.


动画我都是通过Matrix4来设置不同位置的属性, 它也能模拟出 3D 效果,


动画的变换都是Tween来管理.


  void _initAnimation() {
// 透明度动画
this.opacityAnimation = new Tween(begin: 1.0, end: 0.0).animate(
new CurvedAnimation(
parent: this._nextAnimationController, curve: Curves.decelerate))
..addListener(() {
this.setState(() {
// 通知 Fluter Engine 重绘
});
});
// 翻转动画
// 第三个值是角度
var startTrans = Matrix4.identity()..setEntry(3, 2, 0.006);
var endTrans = Matrix4.identity()
..setEntry(3, 2, 0.006)
..rotateX(3.1415927);
this.transformAnimation = new Tween(begin: startTrans, end: endTrans)
.animate(new CurvedAnimation(
parent: this._nextAnimationController, curve: Curves.easeIn))
..addListener(() {
this.setState(() {});
});
// 缩放
var saveStartTrans = Matrix4.identity()..setEntry(3, 2, 0.006);
// 平移且缩放
var saveEndTrans = Matrix4.identity()
..setEntry(3, 2, 0.006)
..scale(0.1, 0.1)
..translate(-20.0, 20.0); // MediaQuery.of(context).size.height
this.saveToPhotos = new Tween(begin: saveStartTrans, end: saveEndTrans)
.animate(new CurvedAnimation(
parent: this._saveAnimationController, curve: Curves.easeIn))
..addListener(() {
this.setState(() {});
});
}

Widget引用这个属性来执行动画.


Widget pet = new GestureDetector(
onVerticalDragUpdate: nextUpdate,
onVerticalDragStart: nextStart,
onVerticalDragEnd: next,
child: new Transform(
transform: this.dragUpdateTransform,
child: Container(
child: new Transform(
alignment: Alignment.bottomLeft,
transform: transform,
child: new Opacity(
opacity: opacity,
child: Container(
width: MediaQuery.of(context).size.width / 1.2,
height: MediaQuery.of(context).size.width / 1.5 - 30,
child: new Padding(
padding: EdgeInsets.all(0),
child: new CachedNetworkImage(
imageUrl: this.widget.listImages[item],
fit: BoxFit.fill,
placeholder: (context, content) {
return new Container(
width: MediaQuery.of(context).size.width / 2.0 - 40,
height: MediaQuery.of(context).size.width / 2.0 - 60,
color: Color(0xFF2FC77D),
child: new Center(
child: new CupertinoActivityIndicator(),
),
);
},
),
),
),
),
),
),
),
);
Firebase_admob

注意: 这里需要去 firebase 官网注册 APP, 然后分别下载 iOS, Android 的配置文件放到指定的位置, 否则程序启动的时候会闪退.


iOS info.plist: GADApplicationIdentifier也需要配置, 虽然在 Dart 中会启动的时候就注册ID, 但是这里也别忘了配置.


Android Manifst.xml 也需要配置


<meta-data
android:name="com.google.android.gms.ads.APPLICATION_ID"
android:value=""/>

这里说一下我因为个人编码导致的问题, 我尝试自己来控制广告展示, 加了一个读秒跳过按钮(想强制观看一段时间), 点击跳过设置setState, 但是在 build 方法中又请求了广告, 导致了一个死循环, 最后由于请求次数过多还没有设置自己的设备为测试设备也不是使用的测试ID, 账号被暂停了, 所以大家使用的时候要避免这个问题, 尽量还是将自己的设备添加到测试设备中.


使用的话比较简单(官方的演示代码直接复制也可以用).


class AdPage {
MobileAdTargetingInfo targetingInfo;

InterstitialAd interstitial;

BannerAd banner;

void initAttributes() {
if (this.targetingInfo == null) {
this.targetingInfo = MobileAdTargetingInfo(
keywords: ["some keyword for your app"],
// 防止被Google 认为是无效点击和展示.
testDevices: ["Your Phone", "Simulator"]);

bool android = Platform.isAndroid;

this.interstitial = InterstitialAd(
adUnitId: InterstitialAd.testAdUnitId,
targetingInfo: this.targetingInfo,
listener: (MobileAdEvent event) {
if (event == MobileAdEvent.closed) {
// 点击关闭
print("InterstitialAd Closed");
this.interstitial.dispose();
this.interstitial = null;
} else if (event == MobileAdEvent.clicked) {
// 关闭
print("InterstitialAd Clicked");
this.interstitial.dispose();
this.interstitial = null;
} else if (event == MobileAdEvent.loaded) {
// 加载
print("InterstitialAd Loaded");
}
print("InterstitialAd event is $event");
},
);

// this.banner = BannerAd(
// targetingInfo: this.targetingInfo,
// size: AdSize.smartBanner,
// listener: (MobileAdEvent event) {
// if (event == MobileAdEvent.closed) {
// // 点击关闭
// print("InterstitialAd Closed");
// this.interstitial.dispose();
// this.interstitial = null;
// } else if (event == MobileAdEvent.clicked) {
// // 关闭
// print("InterstitialAd Clicked");
// this.interstitial.dispose();
// this.interstitial = null;
// } else if (event == MobileAdEvent.loaded) {
// // 加载
// print("InterstitialAd Loaded");
// }
// print("InterstitialAd event is $event");
// });
}
}

@override
void show() {
// 初始化数据
this.initAttributes();
// 然后控制跳转
if (this.interstitial != null) {
this.interstitial.load();
this.interstitial.show(
anchorType: AnchorType.bottom,
anchorOffset: 0.0,
);
}
}
}

项目比较简单, 但是编写的过程中也遇到了许多问题, 慢慢解决的过程也学到了挺多.


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

Flutter页面传值的几种方式

今天来聊聊Flutter页面传值的几种方式: InheritWidget Notification Eventbus (当前Flutter版本:2.0.4) InheritWidget 如果看过Provider的源码的同学都知道,Provider跨组件传值...
继续阅读 »

今天来聊聊Flutter页面传值的几种方式:



  1. InheritWidget

  2. Notification

  3. Eventbus


(当前Flutter版本:2.0.4)


InheritWidget


如果看过Provider的源码的同学都知道,Provider跨组件传值的原理就是根据系统提供的InheritWidget实现的,让我们来看一下这个组件。
InheritWidget是一个抽象类,我们写一个保存用户信息的类UserInfoInheritWidget继承于InheritWidget:


class UserInfoInheritWidget extends InheritedWidget {

UserInfoBean userInfoBean;
UserInfoInheritWidget({Key key, this.userInfoBean, Widget child}) : super (child: child);

static UserInfoWidget of(BuildContext context){
return context.dependOnInheritedWidgetOfExactType<UserInfoWidget>();
}

@override
bool updateShouldNotify(UserInfoInheritWidget oldWidget) {
return oldWidget.userInfoBean != userInfoBean;
}
}

我们在这里面定义了一个静态方法:of,并且传入了一个context,根据context获取当前类,拿到当前类中的UserInfoBean,其实获取主题数据也是根据InheritWidget这种方式获取Theme.of(context),关于of方法后面重点讲一下,updateShouldNotify是刷新机制,什么时候刷新数据


还有一个用户信息的实体:


class UserInfoBean {
String name;
String address;
UserInfoBean({this.name, this.address});
}

我们做两个页面,第一个页面显示用户信息,还有一个按钮,点击按钮跳转到第二个页面,同样也是显示用户信息:


class Page19PassByValue extends StatefulWidget {
@override
_Page19PassByValueState createState() => _Page19PassByValueState();
}

class _Page19PassByValueState extends State<Page19PassByValue> {

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('PassByValue'),
),
body: DefaultTextStyle(
style: TextStyle(fontSize: 30, color: Colors.black),
child: Column(
children: [
Text(UserInfoWidget.of(context)!.userInfoBean.name),
Text(UserInfoWidget.of(context)!.userInfoBean.address),
SizedBox(height: 40),
TextButton(
child: Text('点击跳转'),
onPressed: (){
Navigator.of(context).push(CupertinoPageRoute(builder: (context){
return DetailPage();
}));
},
)
],
),
),
);
}
}

class DetailPage extends StatefulWidget {
@override
_DetailPageState createState() => _DetailPageState();
}

class _DetailPageState extends State<DetailPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Detail'),
),
body: DefaultTextStyle(
style: TextStyle(fontSize: 30, color: Colors.black),
child: Center(
child: Column(
children: [
Text(UserInfoWidget.of(context).userInfoBean.name),
Text(UserInfoWidget.of(context).userInfoBean.address),
TextButton(
onPressed: () {
setState(() {
UserInfoWidget.of(context)!.updateBean('wf123','address123');
});
},
child: Text('点击修改'))
],
),
),
)
);
}
}

由于我们这里是跨组件传值,需要把UserInfoWidget放在MaterialApp的上层,并给UserInfoBean一个初始值:


class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return UserInfoWidget(
userInfoBean: UserInfoBean(name: 'wf', address: 'address'),
child: MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
),
);
}
}

这样就实现了一个跨组件传值,但是还有个问题,我们给UserInfoWidget赋值的时候是在最顶层,在真实业务场景中,如果我们把UserInfo的赋值放在MaterialApp上面,这时候我们还没拿到用户数据呢,所以就要有一个可以更新UserInfo的方法,并且修改后立即刷新,我们可以借助setState,把我们上面定义的UserInfoWidget改个名字然后封装在StatefulWidget 中:


class _UserInfoInheritWidget extends InheritedWidget {

UserInfoBean userInfoBean;
Function update;
_UserInfoInheritWidget({Key key, this.userInfoBean, this.update, Widget child}) : super (child: child);

updateBean(String name, String address){
update(name, address);
}

@override
bool updateShouldNotify(_UserInfoInheritWidget oldWidget) {
return oldWidget.userInfoBean != userInfoBean;
}
}

class UserInfoWidget extends StatefulWidget {
UserInfoBean userInfoBean;
Widget child;
UserInfoWidget({Key key, this.userInfoBean, this.child}) : super (key: key);

static _UserInfoInheritWidget of(BuildContext context){
return context.dependOnInheritedWidgetOfExactType<_UserInfoInheritWidget>();
}
@override
State<StatefulWidget> createState() => _UserInfoState();
}

class _UserInfoState extends State <UserInfoWidget> {

_update(String name, String address){
UserInfoBean bean = UserInfoBean(name: name, address: address);
widget.userInfoBean = bean;
setState(() {});
}
@override
Widget build(BuildContext context) {
return _UserInfoInheritWidget(
child: widget.child,
userInfoBean: widget.userInfoBean,
update: _update,
);
}
}

上面把继承自InheritWidget的类改了一个名字:_UserInfoInheritWidget,对外只暴露用StatefulWidget封装过的UserInfoWidget,向_UserInfoInheritWidget传入了包含setState的更新数据方法,更新数据的时候通过UserInfoWidget.of(context)获取到继承于InheritWidget_UserInfoInheritWidget类,调用updateBean方法实际上就调用了包含setState的方法,所以做到了数据更新和页面刷新


1.gif


下面重点说一下UserInfoWidget.of(context)是如何获取到继承于InheritWidget类的对象的,通过查看类似的方法:Theme.of(context)发现是根据dependOnInheritedWidgetOfExactType,于是我们也照着它的样子获取到了_UserInfoInheritWidget,点到dependOnInheritedWidgetOfExactType源码中看一下,发现跳转到了BuildContext中定义了这个方法:


  T? dependOnInheritedWidgetOfExactType<T extends InheritedWidget>({ Object? aspect });
复制代码

了解WidgetElementRenderObject三只之间关系的同学都知道,其实contextElement的一个实例,BuildContext的注释也提到了这一点:


image.png
我们可以在Element中找到这个方法的实现:


@override
T? dependOnInheritedWidgetOfExactType<T extends InheritedWidget>({Object? aspect}) {
assert(_debugCheckStateIsActiveForAncestorLookup());
final InheritedElement? ancestor = _inheritedWidgets == null ? null : _inheritedWidgets![T];
if (ancestor != null) {
assert(ancestor is InheritedElement);
return dependOnInheritedElement(ancestor, aspect: aspect) as T;
}
_hadUnsatisfiedDependencies = true;
return null;
}

_inheritedWidgets是从哪来的,我们搜索一下在Element中发现


void _updateInheritance() {
assert(_lifecycleState == _ElementLifecycle.active);
_inheritedWidgets = _parent?._inheritedWidgets;
}

再看一下_updateInheritance方法是什么时候调用的


@mustCallSuper
void mount(Element? parent, dynamic newSlot) {
...
...省略无关代码
_parent = parent;
_slot = newSlot;
_lifecycleState = _ElementLifecycle.active;
_depth = _parent != null ? _parent!.depth + 1 : 1;
if (parent != null) // Only assign ownership if the parent is non-null
_owner = parent.owner;
final Key? key = widget.key;
if (key is GlobalKey) {
key._register(this);
}
_updateInheritance();//这里调用了一次
}

还有:


@mustCallSuper
void activate() {
...
...已省略无关代码
final bool hadDependencies = (_dependencies != null && _dependencies!.isNotEmpty) || _hadUnsatisfiedDependencies;
_lifecycleState = _ElementLifecycle.active;
_dependencies?.clear();
_hadUnsatisfiedDependencies = false;
_updateInheritance();//这里又调用了一次
if (_dirty)
owner!.scheduleBuildFor(this);
if (hadDependencies)
didChangeDependencies();
}

从上面代码我们可以看到每个页面的Element都会通过_parent向下级传递父级信息,而我们的UserInfoWidget就保存在_parent中的_inheritedWidgets集合中:
Map<Type, InheritedElement>? _inheritedWidgets;,当_inheritedWidgets在页面树中向下传递的时候,如果当前WidgetInheritWidget,在当前Widget对应的Element中先看_parent传过来的_inheritedWidgets是否为空,如果为空就新建一个集合,把自己存到这个集合中,以当前的类型作为key(这也是为什么调用of方法中的context.dependOnInheritedWidgetOfExactType方法为什么要传当前类型的原因),从_inheritedWidgets集合中去取值;如果不为空直接把自己存进去,这就是of的原理了。


Notification


上面讲的InheritWidget一般是根部组建向子级组件传值,Notification是从子级组件向父级组件传值,下面我们来看一下它的用法


class Page19PassByValue extends StatefulWidget {
@override
_Page19PassByValueState createState() => _Page19PassByValueState();
}

class _Page19PassByValueState extends State<Page19PassByValue> {
UserInfoBean userInfoBean = UserInfoBean(name: 'wf', address: 'address');

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('PassByValue'),
),
body: Center(
child: NotificationListener<MyNotification>(
onNotification: (MyNotification data) {
userInfoBean = data.userInfoBean;
setState(() {});
///这里需要返回一个bool值,true表示阻止事件继续向上传递,false表示事件可以继续向上传递到父级组件
return true;
},
child: Builder(
///这里用了一个Builder包装了一下,为的是能取到
///NotificationListener的context
builder: (context) {
return Column(
children: [
Text(userInfoBean.name),
Text(userInfoBean.address),
Container(
child: FlatButton(
child: Text('点击传值'),
onPressed: () {
MyNotification(userInfoBean: UserInfoBean(name: 'wf123', address: 'address123')).dispatch(context);
},
),
)
],
);
},
),
),
),
);
}
}

///Notification是一个抽象类,
///使用Notification需要自定义一个class继承Notification
class MyNotification extends Notification {
UserInfoBean userInfoBean;
MyNotification({this.userInfoBean}) : super();
}

我们到源码中看一下这个dispatch方法:


void dispatch(BuildContext target) {
// The `target` may be null if the subtree the notification is supposed to be
// dispatched in is in the process of being disposed.
target?.visitAncestorElements(visitAncestor);
}

target就是我们传进来的context,也就是调用了BuildContextvisitAncestorElements方法,并且把visitAncestor方法作为一个参数传过去,visitAncestor方法返回一个bool值:


  @protected
@mustCallSuper
bool visitAncestor(Element element) {
if (element is StatelessElement) {
final StatelessWidget widget = element.widget;
if (widget is NotificationListener<Notification>) {
if (widget._dispatch(this, element)) // that function checks the type dynamically
return false;
}
}
return true;
}

我们进入Element内部看一下visitAncestorElements方法的实现:


@override
void visitAncestorElements(bool visitor(Element element)) {
assert(_debugCheckStateIsActiveForAncestorLookup());
Element? ancestor = _parent;
while (ancestor != null && visitor(ancestor))
ancestor = ancestor._parent;
}

当有父级节点,并且visitor方法返回true的时候执行while循环,visitorNotification类传进来的方法,回过头再看visitor方法的实现,当Elementvisitor方法传递的ancestorNotificationListener类的情况下,再判断widget._dispatch方法,而widget._dispatch方法:


final NotificationListenerCallback<T>? onNotification;

bool _dispatch(Notification notification, Element element) {
if (onNotification != null && notification is T) {
final bool result = onNotification!(notification);
return result == true; // so that null and false have the same effect
}
return false;
}

就是我们在外面写的onNotification方法的实现,我们在外面实现的onNotification方法返回true(即阻止事件继续向上传递),上面的while循环主要是为了执行我们onNotification里面的方法.


总结一下:MyNotification执行dispatch方法,传递context,根据当前context向父级查找对应NotificationListener,并且执行NotificationListener里面的onNotification方法,返回true,则事件不再向上级传递,如果返回false则事件继续向上一个NotificationListener传递,并执行里面对应的方法。Notification主要用在同一个页面中,子级向父级传值,比较轻量级,不过如果我们用了Provider可能就就直接借助Provider传值了。


Eventbus


Eventbus用于两个不同的页面,可以跨多级页面传值,用法也比较简单,我创建了一个EventBusUtil来创建一个单例


import 'package:event_bus/event_bus.dart';
class EventBusUtil {
static EventBus ? _instance;
static EventBus getInstance(){
if (_instance == null) {
_instance = EventBus();
}
return _instance!;
}
}

在第一个页面监听:


class Page19PassByValue extends StatefulWidget {
@override
_Page19PassByValueState createState() => _Page19PassByValueState();
}

class _Page19PassByValueState extends State<Page19PassByValue> {
UserInfoBean userInfoBean = UserInfoBean(name: 'wf', address: 'address');
@override
void initState() {
super.initState();
EventBusUtil.getInstance().on<UserInfoBean>().listen((event) {
setState(() {
userInfoBean = event;
});
});
}

@override
void dispose() {
super.dispose();
//不用的时候记得关闭
EventBusUtil.getInstance().destroy();
}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('PassByValue'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(userInfoBean.name),
Text(userInfoBean.address),
TextButton(onPressed: (){
Navigator.of(context).push(CupertinoPageRoute(builder: (_){
return EventBusDetailPage();
}));
}, child: Text('点击跳转'))

],
),
),
);
}
}

在第二个页面发送事件:


class EventBusDetailPage extends StatefulWidget {
@override
_EventBusDetailPageState createState() => _EventBusDetailPageState();
}

class _EventBusDetailPageState extends State<EventBusDetailPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('EventBusDetail'),
),
body: Center(
child: TextButton(onPressed: (){
EventBusUtil.getInstance().fire(UserInfoBean(name: 'name EventBus', address: 'address EventBus'));
}, child: Text('点击传值')),
),
);
}
}

我们看一下EventBus的源码,发现只有几十行代码,他的内部是创建了一个StreamController,通过StreamController来实现跨组件传值,我们也可以直接使用一下这个StreamController实现页面传值:


class Page19PassByValue extends StatefulWidget {
@override
_Page19PassByValueState createState() => _Page19PassByValueState();
}

StreamController controller = StreamController();

class _Page19PassByValueState extends State<Page19PassByValue> {

//设置一个初始值
UserInfoBean userInfoBean = UserInfoBean(name: 'wf', address: 'address');
@override
void initState() {
super.initState();
controller.stream.listen((event) {
setState(() {
userInfoBean = event;
});
});
}

@override
void dispose() {
super.dispose();
//页面销毁的时候记得关闭
controller.close();
}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('PassByValue'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(userInfoBean.name),
Text(userInfoBean.address),
TextButton(onPressed: (){
Navigator.of(context).push(CupertinoPageRoute(builder: (_){
return MyStreamControllerDetail();
}));
}, child: Text('点击跳转'))
],
),
)
);
}
}

class MyStreamControllerDetail extends StatefulWidget {
@override
State<StatefulWidget> createState() {
return _MyStreamControllerDetailState();
}
}
class _MyStreamControllerDetailState extends State <MyStreamControllerDetail> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('StreamController'),
),
body: Center(
child: TextButton(onPressed: (){
//返回上个页面,会发现页面的数据已经变了
controller.sink.add(UserInfoBean(name: 'StreamController pass name: 123', address: 'StreamController pass address 123'));
}, child: Text('点击传值'),),
),
);
}
}

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

Android组件化第一步壳工程配置

传统项目开发中,我们都是通过集成化的方式来搭建项目的架构。什么叫做集成化,我的理解,就是整个project有一个module,根据功能的需要来创建不同的library库,通过gradle的方式来实现依赖。 什么叫做组件化,我的理解就是,一个project中,将...
继续阅读 »

传统项目开发中,我们都是通过集成化的方式来搭建项目的架构。什么叫做集成化,我的理解,就是整个project有一个module,根据功能的需要来创建不同的library库,通过gradle的方式来实现依赖。


什么叫做组件化,我的理解就是,一个project中,将会有多个module,并且这个module可以在需要的时候切换身份,变成library,作为主module的依赖,主 module 就是我们的壳工程。


为什么会想尝试一下组件化呢?有两个愿景:


1.在开发中,可以不用打包整个app。实现测试同学的测试包和开发人员的自测包分离


2.减少开发自测时的打包时间。


以上就是我对组件化目前的理解,对于路由那块,分篇讨论。为了解决上面愿景中的两个问题,我们可以做如下配置。


开始步骤1,2,3...

1.在项目的build.gradle中创建一个boolean变量,用来确定编译项目为集成化模式还是组件化模式。同时配置buildconfig。方便在代码中进行判断处理。


image2021-3-5_9-28-45.png


这儿说一下,gradle的引入机制,没有根据文档,只是主观推断。gradle会先从项目的build.gradle中进行读取,通过ext来定义整个工程的变量,通过apply from 来引入其他的gradle配置文件,在project中配置的功能和变量,将会在整个工程中都可以使用。


2.在要做成组件化的library中进行配置,主要是切换plugin是library还是module,以及是否在default中展示application Id,这儿有可能因为依赖的库太多,需要配置mutidex,来解决65535的问题。def用于定义子module内部的变量。


image2021-3-5_9-29-7.png


image2021-3-5_9-29-18.png


3.在依赖该library的地方,也就是主module地方,进行配置。如果是集成化的配置,也就是isRelease为true,才可以依赖,否则会在编译时产生依赖重复引入异常,无法编译通过。同时在defaultConfig里面配置buildconfig变量,方便代码中使用,进行功能切换


image2021-3-5_9-29-35.png


// 如果是集成化模式,做发布版本时。各个模块都不能独立运行了

if (isRelease) {

implementation project(':YoungWear')

}

4.配置两个AndroidManifest,作为module时候是有Application的,同时按照mutidex的配置方案配置module,接下来是一些核心的代码配置

// 配置资源路径,方便测试环境,打包不集成到正式环境

sourceSets {

main {

if (!isRelease) {

// 如果是组件化模式,需要单独运行时

manifest.srcFile 'src/main/debug/AndroidManifest.xml'

} else {

// 集成化模式,整个项目打包apk

manifest.srcFile 'src/main/AndroidManifest.xml'

java {

// release 时 debug 目录下文件不需要合并到主工程

exclude '**/debug/**'

}}}}

image2021-3-5_9-30-16.png


windows下,分别给project里的build.gradle赋值true和false,terminal中输入gradlew compileDebugSource --stacktrace -info ,查看是否可以编译成功,当作为module的方式,可以在AS中,看到如下图的图标正常,就证明配置成功了,直接安装apk到手机就可以了。


image2021-3-5_9-30-36.png


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

Android开发:实现滑动退出 Fragment + Activity 二合一

前言 能否在不包含侧滑菜单的时候,添加一个侧滑返回,边缘finish当前Fragment? 今天把这项工作完成了,做成了单独的SwipeBackFragment库以及Fragmentation-SwipeBack拓展库 特性: 1、SwipeBackFra...
继续阅读 »

前言



能否在不包含侧滑菜单的时候,添加一个侧滑返回,边缘finish当前Fragment?



今天把这项工作完成了,做成了单独的SwipeBackFragment库以及Fragmentation-SwipeBack拓展库


特性:

1、SwipeBackFragment , SwipeBackActivity二合一:当Activity内的Fragment数大于1时,滑动finish的是Fragment,如果小于等于1时,finish的是Activity。


2、支持左、右、左&右滑动(未来可能会增加更多滑动区域)


3、支持Scroll中的滑动监听


4、帮你处理了app被系统强杀后引起的Fragment重叠的情况


效果



效果图


谈谈实现


拖拽部分大部分是靠ViewDragHelper来实现的,ViewDragHelper帮我们处理了大量Touch相关事件,以及对速度、释放后的一些逻辑监控,大大简化了我们对触摸事件的处理。(本篇不对ViewDragHelper做详细介绍,有不熟悉的小伙伴可以自行查阅相关文档)


对Fragment以及Activiy的滑动退出,原理是一样的,都是在Activity/Fragment的视图上,添加一个父View:SwipeBackLayout,该Layout里创建ViewDragHelper,控制Activity/Fragment视图的拖拽。


1、Activity的实现


对于Activity的SwipeBack实现,网上有大量分析,这里我简要介绍下原理,如下图:



我们只要保证SwipeBackLayout、DecorView和Window的背景是透明的,这样拖拽Activity的xml布局时,可以看到上个Activity的界面,把布局滑走时,再finish掉该Activity即可。


public void attachToActivity(FragmentActivity activity) {
...
ViewGroup decor = (ViewGroup) activity.getWindow().getDecorView();
ViewGroup decorChild = (ViewGroup) decor.getChildAt(0);
decorChild.setBackgroundResource(background);
decor.removeView(decorChild); // 移除decorChild
addView(decorChild); // 添加decorChild到SwipeBackLayout(FrameLayout)
setContentView(decorChild);
decor.addView(this);} // 把SwipeBackLayout添加到DecorView下


2、Fragment的实现


重点来了,Fragment的实现!

在实现前,我先说明Fragment的几个相关知识点:


1、Fragment的视图部分其实就是在onCreateView返回的View;


2、同一个Activity里的多个通过add装载的Fragment,他们在视图层是叠加上去的:

hide()并不销毁视图,仅仅让视图不可见,即View.setVisibility(GONE);

show()让视图变为可见,即View.setVisibility(VISIBLE);



add+show/hide的情况


3、通过replace装载的Fragment,他们在视图层是替换的,replace()会销毁当前的Fragment视图,即回调onDestoryView,返回时,重新创建视图,即回调onCreateView;



replace的情况


4、不管add还是replace,Fragment对象都会被FragmentManager保存在内存中,即使app在后台因系统资源不足被强杀,FragmentManager也会为你保存Fragment,当重启app时,我们可以从FragmentManager中获取这些Fragment。


分析:


Fragment之间的启动无非下图中的2种:



而这个库我并没有考虑replace的情况,因为我们的SwipeBackFragment应该是在"流式"使用的场景(FragmentA -> FragmentB ->....),而这种场景下结合上面的2、3、4条,add+show(),hide()无疑更优于replace,性能更佳、响应更快、我们app的代码逻辑更简单。


add+hide的方式的实现


从第1条,我们可以知道onCreateView的View就是需要放入SwipeBackLayout的子View,我们给该子View一个背景色,然后SwipeBackLayout透明,这样在拖拽时,即可看到"上个Fragment"。


当我们拖拽时,上个Fragment A的View是GONE状态,所以我们要做的就是当判断拖拽发生时,Fragment A的View设置为VISIBLE状态,这样拖拽的时候,上个Fragment A就被完好的显示出来了。


核心代码:


@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(...);
return attachToSwipeBack(view);
}

protected View attachToSwipeBack(View view) {
mSwipeBackLayout.addView(view);
mSwipeBackLayout.setFragment(this, view);
return mSwipeBackLayout;
}


但是相比Activity,上个Activity的视图状态是VISIBLE的,而我们的上个Fragment的视图状态是GONE的,所以我们需要FragmentA.getView().setVisibility(VISIBLE),但是时机是什么时候呢?


最好的方案是开始拖拽前的那一刻,我是在ViewDragHelper里的tryCaptureView方法处理的:


@Override
public boolean tryCaptureView(View child, int pointerId) {
boolean dragEnable = mHelper.isEdgeTouched(ViewDragHelper.EDGE_LEFT);
if (mPreFragment == null) {
if (dragEnable && mFragment != null) {
...省略获取上一个Fragment代码
mPreFragment = fragment;
mPreFragment.getView().setVisibility(VISIBLE);
break;
}
} else {
View preView = mPreFragment.getView();
if (preView != null && preView.getVisibility() != VISIBLE) {
preView.setVisibility(VISIBLE);
}
}
return dragEnable;
}


通过上面代码,我们拖拽当前Fragment前的一瞬间,PreFragment的视图会被VISIBLE,同时完全不会影响onHiddenChanged方法,完美。(到这之前可能有小伙伴想到,只通过add不hide上个Fragment的思路怎么样?很明显是不行的,因为这样的话onHiddenChanged方法不会被回调,而我们使用add的方式,主要通过onHiddenChanged来作为“生命周期”来实现我们的逻辑的)


还一种情况需要注意,当我已经开始拖拽FragmentB打算pop时,拖拽到一半我放弃了,这时FragmentA的视图已经是VISIBLE状态,我又从B进入到Fragment C,这是我们应该把A的视图GONE掉:


SwipeBackFragment里:
@Override
public void onHiddenChanged(boolean hidden) {
super.onHiddenChanged(hidden);
if (hidden && mSwipeBackLayout != null) {
mSwipeBackLayout.hiddenFragment();
}
}

SwipeBackLayout里:
public void hiddenFragment() {
if (mPreFragment != null && mPreFragment.getView() != null) {
mPreFragment.getView().setVisibility(GONE);
}
}


坑点


1、触摸事件冲突


当我们所拖拽的边缘区域中的子View,有其他Touch事件,比如Click事件,这时我们会发现我们的拖拽失效了,这是因为,如果子View不消耗事件,那么整个Touch流程直接走onTouchEvent,在onTouchEvent的DOWN的时候就确定了CaptureView。如果子View消耗事件,那么就会先走onInterceptTouchEvent方法,判断是否可以捕获,而在这过程中会去判断另外两个回调的方法:getViewHorizontalDragRange和getViewVerticalDragRange,只有这两个方法返回大于0的值才能正常的捕获;


并且你需要考虑当前拖拽的页面下是有2个SwipeBackLayout:当前Fragment的和Activity的,最后代码如下:


@Override
public int getViewHorizontalDragRange(View child) {
if (mFragment != null) {
return 1;
} else {
if (mActivity != null && mActivity.getSupportFragmentManager().getBackStackEntryCount() == 1) {
return 1;
}
}
return 0;
}


这样的话,一方面解决了事件冲突,一方面完成了Activity内Fragment数量大于1时,拖拽的是Fragment,等于1时拖拽的是Activity。


2、动画


我们需要在拖拽完成时,将Fragment/Activity移出屏幕,紧接着关闭,最重要的是要保证当前Fragment/Actiivty关闭和上一个Fragment/Activity进入时是无动画的!


对于Activity这项工作很简单:Activity.overridePendingTransition(0, 0)即可。


对于Fragment,如果本身在Fragment跳转时,就不为其设置转场动画,那就可以直接使用了;

如果你使用了setCustomAnimations(enter,exit)或者setCustomAnimations(enter,exit,popenter,popexit),你可以这样处理:


SwipeBackLayout里:
{
mPreFragment.mLocking = true;
mFragment.mLocking =true;
mFragment.getFragmentManager().popBackStackImmediate();
mFragment.mLocking = false;
mPreFragment.mLocking = false;
}

SwipeBackFragment里:
@Override
public Animation onCreateAnimation(int transit, boolean enter, int nextAnim) {
if(mLocking){
return mNoAnim;
}
return super.onCreateAnimation(transit, enter, nextAnim);
}


3、启动新Fragment时,不要调用show()


getSupportFragmentManager().beginTransaction()
.setCustomAnimations(xxx)
.add(xx, B)
// .show(B)
.hide(A)
.commit();


请不要调用上述代码里的show(B)

一方面是新add的B本身就是可见状态,不管你是show还是不调用show,都不会回调B的onHiddenChanged方法;

另一方面,如果你调用了show,滑动返回会后出现异常行为,回到PreFragment时,PreFragment的视图会是GONE状态;如果你非要调用show的话,请按下面的方式处理:(没必要的话,还是不要调用show了,下面的代码可能会产生闪烁)


@Overridepublic void onHiddenChanged(boolean hidden) {
super.onHiddenChanged(hidden);
if (!hidden && getView().getVisibility() != View.VISIBLE) {
getView().post(new Runnable() {
@Override
public void run() {
getView().setVisibility(View.VISIBLE);
}
});
}
}


最后


我为什么把这个库做成2个,一个单独使用的SwipeBackFragment和一个Fragmentation-SwipeBack拓展库呢?


原因在于:

SwipeBackFragment库是一个仅实现Fragment&Activity拖拽返回的基础库,适合轻度使用Fragment的小伙伴(项目属于多Activity+多Fragment,Fragment之间没有复杂的逻辑),当然你也可以随意拓展。


Fragmentation主要是在项目结构为 单Activity+多Fragment,或者重度使用Fragment的多Activity+多Fragment结构时的一个Fragment帮助库,Fragment-SwipeBack是在其基础上拓展的一个库,用于实现滑动返回功能,可以用于各种项目结构。


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

HashMap源码解析

带着问题看HashMap源码(基于JDK8) HashMap由于涉及到多个数据结构,所以变成了面试题的常客,下面带着以下几个面试常见问题去阅读JDK8中HashMap的源码 HashMap底层数据结构 HashMap的put过程 HashMap的get过程...
继续阅读 »

带着问题看HashMap源码(基于JDK8)



  • HashMap由于涉及到多个数据结构,所以变成了面试题的常客,下面带着以下几个面试常见问题去阅读JDK8中HashMap的源码

    1. HashMap底层数据结构

    2. HashMap的put过程

    3. HashMap的get过程

    4. HashMap如何扩容,扩容为啥是之前的2倍

    5. HashMap在JDK8中为啥要改成尾插法




1、HashMap底层数据结构



  • HashMap的数据结构是数组 + 链表 + 红黑树

    • 默认是存储的Node节点的数组


    Node<K,V>[] table;

    static class Node<K,V> implements Map.Entry<K,V> {
    final int hash; // 存储的key的hash值
    final K key; // key键
    V value; // value值
    Node<K,V> next; // 链表指向的下一个节点


    • 当Node节点中链表(next)长度超过8时会将链表转换为红黑树TreeNode(Node的子类)以提高查询效率


    static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
    TreeNode<K,V> parent; // red-black tree links
    TreeNode<K,V> left;
    TreeNode<K,V> right;
    TreeNode<K,V> prev; // needed to unlink next upon deletion
    boolean red;


  • Node[]数组的初始长度默认为16,并且必须为2^n的形式(具体原因下面会有解释)


/**
* The default initial capacity - MUST be a power of two.
* 默认初始容量为16,并且必须为2的幂数
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16


  • HashMap默认的阈值threshold = 负载因子loadFactor(默认为0.75)*容量capacity,即初始时默认为16 * 0.75 = 12

    • 表示当hashMap中存储的元素超过该阈值时,为了减少hash碰撞,会对hashMap的容量Capacity进行resize扩容,每次扩容都是之前的2倍,扩容后会重新计算hash值即重新计算在新的存放位置并插入


    /**
    * The load factor used when none specified in constructor.
    * 当没有在构造中指定loadFactor加载因子时,默认值为0.75
    */
    static final float DEFAULT_LOAD_FACTOR = 0.75f;



2、HashMap的put过程


put & putIfAbsent


/**
* Associates the specified value with the specified key in this map.
* If the map previously contained a mapping for the key, the old
* value is replaced.
* 将指定的值与此映射中的指定键相关联。如果映射以前包含键的映射,则旧的值被替换
*
* @param key key with which the specified value is to be associated key值
* @param value value to be associated with the specified key key对应的Value值
* @return the previous value associated with key, or null if there was no mapping for key
* 当hashmap中已有当前key覆盖更新并返回旧的Value,如果没有返回null
*/
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}

// onlyIfAbsent参数为true,表示仅在不包含该key时会插入,已包含要插入的key时则不会覆盖更新
@Override
public V putIfAbsent(K key, V value) {
return putVal(hash(key), key, value, true, true);
}

hash方法计算key的hash值


// 通过key计算hash值
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

putVal相关代码


/**
* Implements Map.put and related methods
*
* @param hash hash for key key的hash值,通过hash方法获取
* @param key the key 键
* @param value the value to put 值
* @param onlyIfAbsent if true, don't change existing value 当已有key时是否覆盖更新
* @param evict if false, the table is in creation mode.
* @return previous value, or null if none 返回旧的值,如果没有相同的key返回null
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
// 1、第一次put时table为null,就会触发resize,将初始化工作延迟到第一次添加元素时,懒加载
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 2、将hash值与size-1进行&运算得出数组存放的位置;当此位置上还未存放Node时
// 直接初始化创建一个Node(hash,key,value,null)并放置在该位置
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;e
// 3、假如该位置已经有值,但存储的key完全相同时,直接将原来的值赋值给临时e
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
// 4、假如该位置有值,key值也不同,先判断该Node是不是一个TreeNode类型(红黑树,Node的子类)
// 就调用putTreeVal方法执行红黑树的插入操作
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
// 5、假如该位置有值,key值也不同,Node也不是一个TreeNode红黑树类型,
// 便会对链表进行遍历并对链表长度进行计数,遍历到链表中有相同key的节点会跳出遍历
// 当链表长度计数的值超过8(包含数组本身上的Node)时
// 就会触发treeifyBin操作即将链表转化为红黑树
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
// 这里主要针对相同的key做处理,当onlyIfAbsent为true时就不覆盖,为false时覆盖更新
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
// 6、当hashMap存储的元素数量超过阈值就会触发resize扩容
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}

resize扩容相关代码


/**
* Initializes or doubles table size. If null, allocates in
* accord with initial capacity target held in field threshold.
* Otherwise, because we are using power-of-two expansion, the
* elements from each bin must either stay at same index, or move
* with a power of two offset in the new table.
*
* @return the table
*/
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;、
// 针对后续扩容
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
// 这里针对构造器中自行设置了initialCapacity的情况
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
// 针对第一次put时,Node数组相关参数初始化
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
// 链表数组初始化
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
// 扩容时将旧的Node移到新的数组操作
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
// 判断高位是1还是0
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}

put大致的流程总结



  1. 第一次put元素时会触发resize方法,其实是将hashMap的Node[]数组初始化工作进行了类似懒加载的处理

  2. 将hash值与capacity-1进行&运算计算出当前key要放置在数组中的位置;当该位置无值时就会直接初始化创建一个Node(hash,key,value,null)并放置在该位置,如果已有值就先判断存储和插入的key是否相等,相等的话通过onlyIfAbsent参数判定是否要覆盖更新并返回旧值

  3. 如果已有值并且与要存储的key不等,就先判定该Node是否是一个TreeNode(红黑树,Node的子类),是的话就调用putTreeVal方法执行红黑树的插入操作

  4. 如果已有值并且与要存储的key不等也不是一个红黑树节点TreeNode就会对Node链表进行遍历操作,遍历到链表中有相同key就跳出根据onlyIfAbsent参数判定是否要覆盖更新,如果没有便新建Node,放置在Node链表的Next位置;如果链表长度超过8时便会将链表转化为红黑树并重新插入

  5. 最后判断HashMap存储的元素是否超过了阈值,超过阈值便会执行resize扩容操作,并且每次扩容都是之前的2倍。扩容后重新进行hash&(capacity-1)计算元素的插入位置重新插入


image.png


3、HashMap的get过程


get方法执行



  • 实质上是调用的getNode方法


public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}

getNode方法


/**
* Implements Map.get and related methods
*
* @param hash hash for key
* @param key the key
* @return the node, or null if none
*/
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
// 先判断Node数组是否为空或length为0或是否存储的值本身为null,如果是直接返回null
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
// 当匹配到节点数组上的Node的hash和key都相同时直接返回该Node
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
if ((e = first.next) != null) {
// 判断Node.next,如果为TreeNode红黑树类型就利用getTreeNode方法进行红黑树的查找
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
do {
// 不是红黑树结构就是链表结构,进行链表遍历操作,直至找到链表中hash和key值都相等
// 的元素便返回该Node
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}

get大致的流程总结



  1. get方法实质调用的是getNode方法

  2. 首先通过hash(key)方法计算出key的hash值,再通过hash&(capacity-1)计算出要查找的Node数组中的元素位置

  3. 假如Node数组为null或者数组length为0或者该位置本身存储的元素就是null就直接返回null

  4. 假如该位置存储的元素不为null,直接对该位置的Node的hash和key进行匹配,假如都相等便匹配成功返回该Node

  5. 假如该数组上的Node不匹配就获取该Node的next元素,首先判断该元素是否是一个TreeNode红黑树节点类型的Node,如果是就利用getTreeNode方法进行红黑树的查找,找到返回该节点,找不到返回null

  6. 如果next节点的Node不是TreeNode表明是一个链表结构,直接循环遍历该链表,直至找到该值,或最后一个链表元素仍然不匹配就跳出循环返回null


4、HashMap如何扩容,扩容为啥是之前的2倍



  • HashMap中当存储的元素数量超过阈值时就会触发扩容,每次扩容后容量会变成之前的2倍

  • 因为扩容为2倍时,capacity-1转换成2进制后每一位都为1,使得hash&(capacity-1)计算得出要存放的新位置要么是之前的位置要么是之前的位置+ 之前的capacity,使得在扩容时,不需要重新计算元素的hash了,只需要判断最高位是1还是0就好了(hash&oldCapacity),一方面降低了hash冲突,一方面提升了扩容后重新插入的效率


image.png


5、HashMap在JDK8中为啥要改成尾插法



  • 参考:juejin.cn/post/684490…

  • HashMap在jdk1.7中采用头插入法,在扩容时会改变链表中元素原本的顺序,以至于在并发场景下导致链表成环的问题。而在jdk1.8中采用尾插入法,在扩容时会保持链表元素原本的顺序,就不会出现链表成环的问题了

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

Flutter2.0快速体验写一个macos应用

1.简介 Flutter2.0将桌面端的开发支持加入到了stable分支中,这对于我一个移动开发小码农,产生了巨大的兴趣(/手动狗头),于是开始了我的第一个macos应用的开发(FTools),简单的说:开发桌面应用真的不要太简单了吧!下面是应用的截图,多图警...
继续阅读 »

1.简介


Flutter2.0将桌面端的开发支持加入到了stable分支中,这对于我一个移动开发小码农,产生了巨大的兴趣(/手动狗头),于是开始了我的第一个macos应用的开发(FTools),简单的说:开发桌面应用真的不要太简单了吧!下面是应用的截图,多图警告


2.屏幕截图



  • 明亮模式:







  • 深色模式:






3.MacOS应用开发


看到上面,是不是也是像我一样,想自己也写一个macos的工具应用,不要着急,下面来教大家如何创建和生成MacOS应用


1.配置环境


首先,确保你的FlutterSDK为2.0,我使用的是beta分支,也可以在stable分支下面查看到相同的版本号,至于Flutter的环境搭建,网上已经有很多相关的文章了,这里就直接省略了
image.png


2.配置可开发MacOS应用


运行下面命令即可


flutter config --enable-macos-deaktop

3.创建项目


我一般使用的是Android Studio,所以,按照步骤:


Create New Flutter Project


->选择 Flutter Application -> 点击 Next


->输入项目名Project Name -> 点击Next


->输入包名Package Name -> 点击Finish


-> 等待创建完毕(如果卡住了,可以试试设置代理,百度搜索:Flutter设置国内镜像)


-> 因为Android Studio 给我们创建的项目只能运行AndroidIOS,我们需要再命令行下切换到项目的根目录下,运行flutter create .命令即可,完成后,可以看到macos文件夹



4.运行项目


这里,我们需要给Android Studio 升级Flutter插件到最新的版本,然后选择macOS点击绿色三角按钮进行运行即可





4.FTools后续开发


这个应用目前只耗时了两天,后续还会继续维护并免费上架到AppStore,如果你想这个应用有哪些功能(用户面向于开发者),欢迎评论区留言给我,在能够实现并且时间充足的话会安排在开发计划当中。目前计划安排!



  1. Json To Table (JSON 转表格)

  2. Json To Create SQLite (JSON 转Sqlite创建)

  3. App Icon Make (应用图标制作)

  4. ...欢迎留言



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

Flutter 入门路线图

本文是为那些渴望开始学习 flutter 的人们而准备的,这是一个适合初学者从所有必要资源中逐步学习的路线图。 什么是 flutter Flutter 是 Google 的 UI 工具包,可通过单个代码库为移动设备,web 和桌面系统构建漂亮的,本机编译的应用...
继续阅读 »

本文是为那些渴望开始学习 flutter 的人们而准备的,这是一个适合初学者从所有必要资源中逐步学习的路线图。


什么是 flutter


Flutter 是 Google 的 UI 工具包,可通过单个代码库为移动设备,web 和桌面系统构建漂亮的,本机编译的应用程序。


下面两个视频很好地介绍了 flutter;


Introducing Flutter1


What's new in Flutter 20192



[1] https://youtu.be/fq4N0hgOWzU


[2] https://youtu.be/5VbAwhBBHsg


为什么是 flutter?


我们已经知道有很多框架可以提供跨平台功能,那么在这场激烈竞争中,是什么让 flutter 显得特别呢?


快速开发


Flutter 的热加载功能可帮助您快速轻松地进行实验,构建用户界面,添加功能并更快地修复错误。在 iOS 和 Android 的模拟器和硬件上体验亚秒级的重新加载时间,而不会丢失状态。


富有表现力的精美用户界面


Flutter 内置的精美 Material Design 和 Cupertino(iOS-flavor)小部件,丰富的运动 API,流畅的自然滚动以及对平台的了解,可为您的用户带来更多惊喜。


native 级别的性能


Flutter 的小部件结合了所有重要的平台差异,例如滚动,导航,图标和字体,以在 iOS 和 Android 上提供完整的 native 性能。


查看 Flutter 的功能


以下是全球开发人员构建的 Flutter 应用程序的展示。


Apps take flight with Flutter3


An open list of apps built with Flutter4


Flutter Awesome5


Start Flutter | Forever free, open source, and easy to use.6



[3]https://flutter.dev/showcase


[4]https://itsallwidgets.com/


[5]https://flutterawesome.com/


[6]https://startflutter.com/


首先要做什么?


Flutter 既快速又容易,如果您熟悉 Java 或任何面向对象的语言,那么很不错,但是我强烈建议您具备 Dart 的基本知识。


以下是一些可能对您有所帮助的视频。


Dart Programming for Flutter7


Dart Programming Tutorial - Full Course8


Introduction to Dart for Beginners9


Dart: Basics of Dart Part - 1/2 | Flutter10



[7]https://youtu.be/5rtujDjt50I?list=PLlxmoA0rQ-LyHW9voBdNo4gEEIh0SjG-q


[8]https://youtu.be/Ej_Pcr4uC2Q


[9]https://youtu.be/8F2uemqLwvE?list=PLJbE2Yu2zumDjfrfu8kisK9lQVcpMDDzZ


[10]https://youtu.be/DFRl4UyS7c8?list=PLR2qQy0Zxs_W4a6P70VYtzna7jwl3-lxI


对于那些不喜欢看视频的人,可以查看以下站点


Tutorials11


Dart Programming Tutorial12


Learn Dart In A Week With These Free Resources13



[11]https://dart.dev/tutorials


[12]https://www.tutorialspoint.com/dart_programming/index.htm


[13]https://hackernoon.com/learn-dart-in-a-week-with-these-free-resources-b892e5265220


是什么使 Dart 如此典型,为什么 flutter 会使用它?


为什么 Flutter 使用 Dart?


可以查看以下文章和视频


Why Flutter Uses Dart?14


视频:Why Flutter Uses Dart?15



[14]https://hackernoon.com/why-flutter-uses-dart-dd635a054ebf


[15]https://youtu.be/5F-6n_2XWR8


Flutter 底层是如何工作的?


由于 iOS 不允许动态编译,因此您的 Dart 代码会使用 AOT 直接编译为本地代码。


要了解更多信息,请在下面查看这些资源:


Technical overview16


How to Dart and Flutter Work Together?17


What's Revolutionary about Flutter18


How Flutter reners Widgets19


How is Flutter different for app development20



[16]https://flutter.dev/docs/resources/technical-overview


[17]https://youtu.be/iVYpeEd3Jes


[18]https://hackernoon.com/whats-revolutionary-about-flutter-946915b09514


[19]https://youtu.be/996ZgFRENMs


[20]https://youtu.be/l-YO9CmaSUM


Flutter快速且易于使用,现在让我们看看如何安装它。


如何安装Flutter?


这是开发人员文档的链接,您可以在其中找到在现有的操作系统中安装Flutter。


Install21



[21]https://flutter.dev/docs/get-started/install


解决安装过程中的问题


如果您在安装 flutter 时遇到任何问题,并且 flutter 无法正常工作,那么这就是出现了一些问题。


设置 flutter 路径时遇到麻烦-找不到flutter命令22


Flutter Doctor无法识别Android Studio flutter和dart插件,但已安装插件23


Flutter和Dart插件未在Flutter Doctor中安装警告24


安装 flutter 时的一些常见问题。25



[22]https://stackoverflow.com/questions/49268297/having-trouble-setting-flutter-path-flutter-commands-not-found


[23]https://github.com/flutter/flutter/issues/21881


[24]https://github.com/flutter/flutter/issues/11940


[25]https://github.com/flutter/flutter/wiki/Workarounds-for-common-issues


设置Flutter的编辑器


Set up an editor26



[26]https://flutter.dev/docs/get-started/editor


创建您的Flutter项目


通过以下命令创建 flutter 项目


flutter create <project-name>

或者您可以使用IDE(Intellij,Android Studio等)


项目概况


当您创建 flutter 应用程序时,您会看到这些文件和文件夹,大多数代码是用 dart 编写在 lib 文件夹中,native 代码放在 android 和 ios 目录下。


Jay Tillu的一篇文章解释了该项目的结构。


Flutter Project Structure27



[27]https://dev.to/jay_tillu/flutter-project-structure-1lhe


运行你的第一个 App


Test drive28


或者你可以使用以下命令来运行您的第一个应用程序


flutter run

当您启动第一个应用程序时,一定会感到很兴奋(从技术上说,这不是您的应用程序,代码已经在那里😜)。 我也很兴奋🎉。


创建flutter应用程序时,您会看到计数器应用程序已经有代码了。


运行代码时,您将看到此信息。这是一个简单的计数器应用程序,其中有一个FAB(FloatingActionButton)和 Text 来指示已按下 FAB 多少次。



[28]https://flutter.dev/docs/get-started/test-drive


flutter 中的 widget


如果看到代码,您将看到 StatefulWidget 和 StatelessWidget。在深入探讨之前,我们先来了解一下什么是 Widget。


Introduction to widget29


基本上,在 flutter 应用程序中看到的所有内容都是一个小部件。


我发现 What is a Widget in Flutter30 一文中的解释非常准确


Flutter小组还提供了一个YouTube播放列表(Widget of the week31),该列表仅讨论flutter中的Widget。



[29]https://flutter.dev/docs/development/ui/widgets-intro


[30]https://stackoverflow.com/questions/50958238/what-is-a-widget-in-flutter


[31]https://youtu.be/b_sQ9bMltGU?list=PLjxrf2q8roU23XGwz3Km7sQZFTdB996iG


什么是有状态和无状态小部件?


在 Stateless Widget 中,其所有属性都是不可变的,这意味着 StatelessWidget 永远不会自行重建(但可以从外部事件重建),而 StatefulWidget 可以。


Intro to Flutter - Stateful and Stateless Widgets, Widget Tree - Part One32


Flutter: Stateful vs Stateless Widget33


How to Create Stateless Widgets - Flutter Widgets 101 Ep. 134


How Stateful Widgets Are Used Best - Flutter Widgets 101 Ep. 235


Google's Flutter Tutorials | 6 - Stateless & Stateful Widgets | Android & iOS | Dart36



[32]https://youtu.be/-QRQIKtPTlI


[33]https://medium.com/flutter-community/flutter-stateful-vs-stateless-db325309deae


[34]https://www.youtube.com/watch?v=wE7khGHVkYY&feature=emb_title


[35]https://www.youtube.com/watch?v=AqCMFXEmf3w&feature=emb_title


[36]https://www.youtube.com/watch?list=PLR2qQy0Zxs_UdqAcaipPR3CG1Ly57UlhV&v=VnWHOogtDk8&feature=emb_title


让我们创建第一个Flutter应用


Google已经提供了一个 Codelab,您可以从那里开始学习如何构建自己的第一个 Flutter 应用程序。


Write Your First Flutter App, part 137


Write Your First Flutter App, part 238


Flutter Tutorial Part 1: Build a Flutter app from scratch39


1.3 Flutter Hello World Tutorial: Create First Flutter Application: Flutter Dart Tutorial40


1.4 First Flutter Application using Dart: PART-2 Flutter Tutorial for Beginners using Dart41



[37]https://codelabs.developers.google.com/codelabs/first-flutter-app-pt1/#4


[38]https://codelabs.developers.google.com/codelabs/first-flutter-app-pt2/#0


[39]https://medium.com/aviabird/flutter-tutorial-how-to-build-an-app-from-scratch-b88d4e0e10d7


[40]https://www.youtube.com/watch?list=PLlxmoA0rQ-Lw6tAs2fGFuXGP13-dWdKsB&v=dsyucuytW2k


[41]https://www.youtube.com/watch?list=PLlxmoA0rQ-Lw6tAs2fGFuXGP13-dWdKsB&v=ycHX8QtV08c


如何在 Flutter 中创建 UI?


为了使 UI 更加流畅,您需要基本了解布局以及如何使用它们。


Layouts in Flutter42


Flutter layout Cheat Sheet43



[42]https://flutter.dev/docs/development/ui/layout


[43]https://medium.com/flutter-community/flutter-layout-cheat-sheet-5363348d037e


如何在您的应用中添加交互?


在 flutter 中,您不能只是分配一个值并留下它


例如


String value="Hello";
------------------------------
Text(value);
---SOMEWHERE IN THE CODE------
onTap(){
value="How are you?";
}

如果您认为文本将要更改,那么您错了🙅‍♂️,您将不得不使用 setState()。


onTap(){
setState({
value="How are you?";
});
}

添加 setState() 将重建小部件并显示更改。


Adding interactivity to your Flutter app44


我建议您跟进有关开发的 Flutter 官方文档


Development45


flutter 中的所有内容都是小部件,您可以自行创建任何自定义小部件,但是已经有通过 flutter 定义的小部件。


Widget catalog46



[44]https://flutter.dev/docs/development/ui/interactive


[45]https://flutter.dev/docs/development


[46]https://flutter.dev/docs/development/ui/widgets


Flutter 中的 JSON 解析


JSON and serialization47


Parsing JSON in Flutter48


Parsing complex JSON in Flutter49


Working with APIs in Flutter50


Handling Network Calls like a Pro in Flutter51


Flutter - Build An App To Fetch Data Online Using HTTP GET | Android & iOS52


Testing, JSON serialization, and immutables (The Boring Flutter Development Show, Ep. 2)53



[47]https://flutter.dev/docs/development/data-and-backend/json


[48]https://medium.com/flutterdevs/parsing-complex-json-in-flutter-b7f991611d3e


[49]https://medium.com/flutter-community/parsing-complex-json-in-flutter-747c46655f51


[50]https://medium.com/flutter-community/working-with-apis-in-flutter-8745968103e9


[51]https://medium.com/flutter-community/handling-network-calls-like-a-pro-in-flutter-31bd30c86be1


[52]https://www.youtube.com/watch?list=PLR2qQy0Zxs_UdqAcaipPR3CG1Ly57UlhV&v=aIJU68Phi1w


[53]https://www.youtube.com/watch?v=TiCA0CEePyE


在 Flutter 中使用数据库


SQLite


Persist data with SQLite54


Data Persistence with SQLite | Flutter55


4.1 Flutter SQFLite Database Tutorial: Implement SQLite database with example: Section Overview56


Moor (Room for Flutter) #1 – Tables & Queries – Fluent SQLite Database57



[54]https://flutter.dev/docs/cookbook/persistence/sqlite


[55]https://medium.com/flutterdevs/data-persistence-with-sqlite-flutter-47a6f67b973f


[56]https://www.youtube.com/watch?list=PLDQl6gZtjvFu5l20K5KTEBLCjfRjHowLj&v=1BwjNEKD8g8


[57]https://www.youtube.com/watch?v=zpWsedYMczM


SharedPreferences


Shared preferences plugin58


Using SharedPreferences in Flutter59


Store key-value data on disk60


Making use of Shared Preferences, Flex Widgets and Dismissibles with Dart's Flutter framework61



[58]https://pub.dev/packages/shared_preferences


[59]https://medium.com/flutterdevs/using-sharedpreferences-in-flutter-251755f07127


[60]https://flutter.dev/docs/cookbook/persistence/key-value


[61]https://www.youtube.com/watch?v=IvrAAMQnj4k


使用Firebase


将 Firebase 添加到您的 Flutter 应用62


Firebase for Flutter63


Flutter - Firestore introduction64



[62]https://firebase.google.com/docs/flutter/setup


[63]https://codelabs.developers.google.com/codelabs/flutter-firebase/#0


[64]https://www.youtube.com/watch?list=PLgGjX33Qsw-Ha_8ks9im86sLIihimuYrr&v=LzEbpALmRlc


其他学习 Flutter 的资源


以下是其他开发人员和Flutter团队提供的一些资源:


Technical overview65


Resources to learn Flutter66


Free resources to learn and advance in Flutter67


Flutter Community68


My Favourite List of Flutter Resources69


awesome-flutter70


londonappbrewery/Flutter-Course-Resources71


A Searchable List of Flutter Resources72


FlutterDevs73



[65]https://flutter.dev/docs/resources/technical-overview


[66]https://medium.com/flutter-community/resources-to-learn-flutter-2ade7aa73305


[67]https://medium.com/flutter-community/free-resources-to-learn-and-advance-in-flutter-e07875ffc825


[68]https://medium.com/flutter-community


[69]https://medium.com/coding-with-flutter/my-favourite-list-of-flutter-resources-523adc611cbe


[70]https://github.com/Solido/awesome-flutter


[71]https://github.com/londonappbrewery/Flutter-Course-Resources


[72]https://flutterx.com/


[73]https://medium.com/flutterdevs


关于 Flutter 的问题


FAQ74


Answering Questions on Flutter App Development75


Flutter Vs. React Native: FAQs for Every Developer76



[74]https://flutter.dev/docs/resources/faq


[75]https://medium.com/@dev.n/answering-questions-on-flutter-app-development-6d50eb7223f3


[76]https://hackernoon.com/flutter-vs-react-native-faqs-for-every-developer-yjp329z


本文仅适用于初学者。


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

Kotlin + Flow 实现的 Android 应用初始化任务启动库

特性 Kotlin + Flow 实现的 Android 应用初始化任务启动库。 支持模块化,按模块加载任务 可指定工作进程名称,main 表示仅在主进程运行,all 表示在所有进程运行,默认值all 可指定任务仅在工作线程执行 可指定任务仅在调试模式执行 ...
继续阅读 »

特性


Kotlin + Flow 实现的 Android 应用初始化任务启动库。



  • 支持模块化,按模块加载任务

  • 可指定工作进程名称,main 表示仅在主进程运行,all 表示在所有进程运行,默认值all

  • 可指定任务仅在工作线程执行

  • 可指定任务仅在调试模式执行

  • 可指定任务在满足合规条件后执行

  • 可指定任务优先级,决定同模块内无依赖同步任务的执行顺序

  • 可指定依赖任务列表,能检测循环依赖

  • 使用 Flow 调度任务

  • 仅200多行代码,简单明了

  • 有耗时统计


引入依赖


项目地址:github.com/czy1121/ini…


repositories { 
maven { url "https://gitee.com/ezy/repo/raw/android_public/"}
}
dependencies {
implementation "me.reezy.init:init:0.9.0"
kapt "me.reezy.init:init-compiler:0.9.0"

// 使用 init-startup 代替 init 可以利用 Jetpack Startup 库自动初始化
// 无需在 Application.onCreate 调用 InitManager.init()
implementation "me.reezy.init:init-startup:0.9.0"
}

使用


AndroidManifest.xml<application> 里添加模块


<meta-data android:name="modules" android:value="app" />

通过注解 @InitInitTask 接口定义一个任务


@Init
class OneInit : InitTask {
override fun execute(app: Application) {
Log.e(TAG, "this is ${javaClass.simpleName} in ${Thread.currentThread().name}")
}
}

通过注解 @Init 的参数配置任务信息


@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.SOURCE)
annotation class Init(
val process: String = "all", // 指定工作进程名称,main 表示仅在主进程运行,all 表示在所有进程运行
val background: Boolean = false, // 是否在工作线程执行任务
val debugOnly: Boolean = false, // 是否仅在 DEBUG 模式执行任务
val compliance: Boolean = false, // 是否需要合规执行
val depends: Array<String> = [], // 依赖的任务列表
val priority: Short = 0 //
)

APT会按模块收集任务信息并生成任务加载器(InitLoader_$moduleName),任务加载器用于添加任务到TaskList


class Task(
val name: String, // APT收集的任务名称格式为 "$moduleName:${clazz.simpleName}"
val background: Boolean = false, // 是否在工作线程执行任务
val priority: Int = 0, // 进程运行的优先级,值小的先执行
val depends: Set<String> = setOf(), // 依赖的任务列表,同模块只需指定"${clazz.simpleName}",跨模块需要指定 "$moduleName:${clazz.simpleName}"
val block: () -> Unit = {}, // 待执行的任务
) {
val children: MutableSet<Task> = mutableSetOf() // 子任务列表
}

核心类



  • TaskList 负责持有和添加任务

  • TaskManager 负责调度任务,支持添加开关任务(没有业务仅作为开关,可手动触发完成,并偿试执行其子任务)

    • 无依赖的异步任务,在子线程并行执行

    • 无依赖的同步任务,在主线程顺序执行

    • 有依赖的任务,确保无循环依赖,且被依赖的任务先执行



  • InitManager 负责找到各模块的任务加载器并开始启动初始化,它使用了一个合规开关来使相关任务在确定合规后执行


可以不使用 InitManager 收集任务


val taskList = TaskList(app).apply {
add("task1") {
}
add("task2", depends = setOf("t1")) {
}
add("task3", depends = setOf("task1")) {
}
}

val manager = TaskManager(taskList, setOf("t1"))
manager.start()

// ...

// 完成开关任务t1
manager.trigger("t1")

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

Android | 彻底理解 View 的坐标

Android | 彻底理解 View 的坐标前言如果你是一位从事 Android 原生开发的工程师,那么肯定会对 View 的各种坐标感到迷惑,不理解他们的真正含义。因为曾经我也和你们一样,面对他们时感到陌生和害怕。现在我将这些知识点整理成文,希望可以给大家...
继续阅读 »

Android | 彻底理解 View 的坐标

前言

如果你是一位从事 Android 原生开发的工程师,那么肯定会对 View 的各种坐标感到迷惑,不理解他们的真正含义。因为曾经我也和你们一样,面对他们时感到陌生和害怕。现在我将这些知识点整理成文,希望可以给大家一些帮助。

View 的坐标分为四大类:位置坐标,内容滚动坐标,平移坐标,触摸坐标。 通过阅读本文,读者能够在理解各种 View 坐标的基础上,今后在面对动画和触摸事件的处理会更加的游刃有余。

预备知识

如果你对以下知识有过了解,阅读本文将会很轻松。

  1. 了解 View 的属性动画;View 触摸事件的分发;View 的测量、布局过程
  2. 了解 Kotlin 基础语法

环境

文中所有的代码和运行截图,基于以下开发环境。
Android Studio 4.1.1
Kotlin 1.4.20
程序运行系统 Android 5.1

View 的位置坐标

View 的位置坐标是我们日常开发中最常见的一类坐标,分别是左、上、右、下,获取他们的值也很简单。

getLeft()
getTop()
getRight()
getBottom()

如上所示,通过 View 的上面四个方法,就可以获取到 View 的位置坐标了。需要注意以下三点。

1. (left,top) 代表 View 的左上角坐标,(right,bottom) 代表 View 的右下角坐标
2. 位置坐标是一种相对于父容器的坐标,即坐标系原点是父容器的左上角
3. 位置坐标不会因为 View 的内容滚动、View 的平移而改变,他们在 View 的测量、布局过程结束后就确定了

View 的内容滚动坐标

通过 View 的下面两个方法,可以得到 View 内容滚动后的左上角坐标。

getScrollX()
getScrollY()

需要注意 View 的内容滚动坐标是相对于 View 自身的坐标,即坐标系原点是 View 自身的左上角,并不是父容器。 如下伪代码和运行截图所示,绿色区域是一个 TextView ,当我们点击绿色区域的时候,TextView 的内容会向右滚动 100px的距离,根据运行后的截图,可以得出如下结论。

1. View 的位置坐标并不会因为 View 的内容滚动后而发生改变,这在上面已经说明过
2. 当一个 View 的内容从左向右滚动时,getScrollX() 是负值,同理当一个 View 的内容从上往下滚动时,getScrollY()也是负值。反之,从右向左,从下往上就是正值

viewBinding.tvScroll.scrollTo(-100,0)

初始坐标.png

向右滚动100px.png

View 的平移坐标

读者在实际开发中,或多或少都接触过 View 的属性动画,大概是平移、旋转、缩放三种。而平移的运用场景在 Android 中实在是太多了,基本你能看到的 View 滑动效果,都是通过属性动画的平移来实现的。通过下面两个方法,可以得到 View 左上角的平移坐标。需要注意 View 的平移坐标同样是相对于 View 自身的坐标,即坐标系原点是 View 自身的左上角,并不是父容器

getTranslationX()
getTranslationY()

当然在你需要的时候,通过 View 的下面两个方法,仍然可以获得 View 相对于父容器的平移坐标。

getX()
getY()

他们两者之间的数学关系如下

getX() = getLeft() + getTranslationX()
getY() = getTop() + getTranslationY()

如下图伪代码和运行截图所示,绿色区域仍然是一个 TextView,当我们点击绿色区域的时候,使用属性动画,让 TextView 向右平移 100px,根据运行后的截图,可以得出如下结论。

1. View 的位置坐标不会随 View的平移而改变
2. 和 View 的内容滚动不一样,View 的平移是整个 View 都向右平移
3. 向右平移 getTranslationX() 是正值,同理向下平移 getTranslationY()也是正值。反之就是负值

val translationXAnim = ObjectAnimator.ofFloat(viewBinding.tvScroll,"translationX",0f,100f).setDuration(2*1000)
translationXAnim.start()

初始坐标.png

向右平移100px.png

这里读者思考一个问题,如果让你实现一个 View 的滑动效果时,选择内容滚动还是属性动画平移? 很显然,平移相对内容滚动有诸多优点,首先是平移坐标的正负值符合人们的直观感受,其次平移是整个 View 的平移,实际应用场景更多,没有明显的缺点。

View 的触摸坐标

View 的触摸坐标和触摸事件是相关联的,获取触摸坐标有如下两组方法。

第一组,获取 View 触摸点相对于 View 自身左上角的坐标

eventX = event.getX()
eventY = event.getY()

第二组,获取 View 触摸点相对于设备屏幕左上角的坐标

rawX = event.getRawX()
rawY = event.getRawY()

注意上面伪代码中 event 的类型是 MotionEvent,读者可以通过调用 View 的 setOnTouchListener 方法,或重写 View 的 onTouchEvent 方法,来使用这个对象。

如下运行截图所示,绿色区域仍然是一个 TextView,不过这里为它设置了 50px 的左边距和 50px 的上边距。点击绿色区域的任意一处,你都会看到,rawX 和 eventX 始终相差 50px,rawY 和 eventY 始终相差 75px。 根据上面的分析 rawY 和 eventY 也应该相差 50px 才对?其实多出来的 25px 是屏幕状态栏的高度,这证实了上面的结论。

1. event.getX() 和 event.getY() 是 View 触摸点相对于 View 自身左上角的坐标
2. event.getRawX() 和 event.getRawY() 是 View 触摸点相对于设备屏幕左上角的坐标

触摸坐标.png

通过下图,读者或许能够更好理解。

触摸坐标

写在最后

本文是对 View 坐标的一次实践与总结,希望本文能够给读者一点帮助。

原文链接:https://juejin.cn/post/7037320714935861284?utm_source=gold_browser_extension

收起阅读 »

Android asm加注解实现自动Log打印

Android asm加注解实现自动Log打印前言在Android开发中有时候调试问题要给方法加很多的log,很麻烦,所以结合asm用注解的方式来自动在方法中插入log,这样方便开发时候调试。当然通过asm插入的log应该需要包含方法的参数,方法的返回值,有时...
继续阅读 »

Android asm加注解实现自动Log打印

前言

在Android开发中有时候调试问题要给方法加很多的log,很麻烦,所以结合asm用注解的方式来自动在方法中插入log,这样方便开发时候调试。当然通过asm插入的log应该需要包含方法的参数,方法的返回值,有时候也需要获取对象里面的变量值等。

hanno

_    _
| | | |
| |__| | __ _ _ __ _ __ ___
| __ |/ _` | '_ \| '_ \ / _ \
| | | | (_| | | | | | | | (_) |
|_| |_|\__,_|_| |_|_| |_|\___/
复制代码

通过字节码插件实现注解打印log,注解可以加在类上面,也可以加在方法上面,当加在类上面时会打印全部方法的log,当加在方法上面时打印当前方法的log

使用方法

1、类中全部方法打印log

@HannoLog
class MainActivity : AppCompatActivity() {
// ...
}
复制代码

只要在类上面加上@HannoLog注解就可以在编译的时候给这个类中所有的方法插入log,运行时输出log。

2、给类中的某些方法加log

class MainActivity : AppCompatActivity() {
@HannoLog(level = Log.INFO, enableTime = false,watchField=true)
private fun test(a: Int = 3, b: String = "good"): Int {
return a + 1
}
}
复制代码

通过在方法上面添加注解可以在当前方法中插入log。 3、打印的log

//D/MainActivity: ┌───────────────────────────────────------───────────────────────────────────------
//D/MainActivity: │ method: onCreate(android.os.Bundle)
//D/MainActivity: │ params: [{name='savedInstanceState', value=null}]
//D/MainActivity: │ time: 22ms
//D/MainActivity: │ fields: {name='a', value=3}{name='b', value=false}{name='c', value=ccc}
//D/MainActivity: │ thread: main
//D/MainActivity: └───────────────────────────────────------───────────────────────────────────------
复制代码

其中method是当前方法名,params是方法的参数名和值,time方法的执行时间,fields是当前对象的fields值,thread当前方法执行的线程。

HannoLog参数解释

可以通过level来设置log的级别,level的设置可以调用Log里面的INFO,DEBUG,ERROR等。enableTime用来设置是否打印方法执行的时间,默认是false,如果要打印设置enableTime=true. tagName用于设置log的名称,默认是当前类名,也可以通过这个方法进行设置。

1、level控制log打印的等级,默认是log.d,可以通过@HannoLog(level = Log.INFO)来设置等级,支持Log.DEBUG,Log.ERROR等。

2、enableTime控制是否输出方法的执行时间,默认是false,如果要打印可以通过@HannoLog(enableTime=true)来设置。

3、tagName设置tag的名称,默认是当前类名,也可以通过 @HannoLog(tagName = "test")来设置。

4、watchField用于观察对象中的field值,通过@HannoLog(watchField = true)设置,由于静态方法中不能调用非静态的field所以这个参数在静态方法上统一不生效。

重要的类

1、HannoLog HannoLog是注解类,里面提供了控制参数。对应上面的HannoLog参数解释

/**
*
*
*
* create by 胡汉君
* date 2021/11/10 17:38
* 定义一个注解,用于标注当前方法需要打印log
*/

@Retention(RetentionPolicy.CLASS)
@Target({ElementType.METHOD, ElementType.CONSTRUCTOR, ElementType.TYPE})
public @interface HannoLog {
//定义一下log的级别,默认是3,debug级别
int level() default Log.DEBUG;
/**
* @return 打印方法的运行时间
*/

boolean enableTime() default false;

/**
* @return tag的名称,默认是类名,也可以设置
*/

String tagName() default "";

/**
* @return 是否观察field的值,如果观察就会就拿到对象里面全部的field值
*/

boolean watchField() default false;
}
复制代码

2、HannoExtension

public class HannoExtension {
//控制是否使用Hanno
boolean enable;
//控制是否打印log
boolean openLog = true;

public boolean isEnableModule() {
return enableModule;
}

public void setEnableModule(boolean enableModule) {
this.enableModule = enableModule;
}

//设置这个值为true可以给整个module的方法增加log
boolean enableModule = false;

public boolean isEnable() {
return enable;
}

public boolean isOpenLog() {
return openLog;
}

public void setOpenLog(boolean openLog) {
this.openLog = openLog;
}

public void setEnable(boolean enable) {
this.enable = enable;
}
}
复制代码

HannoExtension提供gradle.build文件是否开启plugin 和打印执行plugin的log 默认情况下添加HannoLog之后会进行asm插装,也可以通过在module的build.gradle文件中添加以下配置使在编译时不执行字节码插装提高编译速度

apply plugin: 'com.hanking.hanno'
hannoExtension{
enable=false
openLog=false
}
复制代码

实现原理

hanno是通过asm字节码插桩方式来实现的。Android项目的编译过程如下图: 在这里插入图片描述 java编译器会将.java类编译生成.class类,asm可以用来修改.class类,通过对.class类的修改就可以达到往已有的类中加入代码的目的。一个.java文件经过Java编译器(javac)编译之后会生成一个.class文件。 在.class文件中,存储的是字节码(ByteCode)数据,如下图所示。 在这里插入图片描述 ASM所的操作对象是是字节码(ByteCode)的类库。ASM处理字节码(ByteCode)数据的流程是这样的:

第一步,将.class文件拆分成多个部分;

第二步,对某一个部分的信息进行修改;

第三步,将多个部分重新组织成一个新的.class文件。

ClassFile

.class文件中,存储的是ByteCode数据。但是,这些ByteCode数据并不是杂乱无章的,而是遵循一定的数据结构。

ClassFile {
u4 magic;
u2 minor_version;
u2 major_version;
u2 constant_pool_count;
cp_info constant_pool[constant_pool_count-1];
u2 access_flags;
u2 this_class;
u2 super_class;
u2 interfaces_count;
u2 interfaces[interfaces_count];
u2 fields_count;
field_info fields[fields_count];
u2 methods_count;
method_info methods[methods_count];
u2 attributes_count;
attribute_info attributes[attributes_count];
}
复制代码

字节码的类库和ClassFile之间关系 在这里插入图片描述

asm的组成

从组成结构上来说,ASM分成两部分,一部分为Core API,另一部分为Tree API。

  • 其中,Core API包括asm.jar、asm-util.jar和asm-commons.jar;
  • 其中,Tree API包括asm-tree.jar和asm-analysis.jar。

在这里插入图片描述

asm中重要的类

  • ClassReader类,负责读取.class文件里的内容,然后拆分成各个不同的部分。
  • ClassVisitor类,负责对.class文件中某一部分里的信息进行修改。
  • ClassWriter类,负责将各个不同的部分重新组合成一个完整的.class文件。

在这里插入图片描述

.class文件 --> ClassReader --> byte[] --> 经过各种转换 --> ClassWriter --> byte[] --> .class文件
复制代码

ClassVisitor类

ClassVisitor是一个抽象类,实现类有ClassWriter类(Core API)和ClassNode类(Tree API)。

public abstract class ClassVisitor {
protected final int api;
protected ClassVisitor cv;
}
复制代码
  • api字段:int类型的数据,指出了当前使用的ASM API版本。
  • cv字段:ClassVisitor类型的数据,它的作用是将多个ClassVisitor串连起来

在这里插入图片描述

classVisitor的方法

visit()、visitField()、visitMethod()和visitEnd()。

visitXxx()方法与ClassFile ClassVisitor的visitXxx()方法与ClassFile之间存在对应关系。在ClassVisitor中定义的visitXxx()方法,并不是凭空产生的,这些方法存在的目的就是为了生成一个合法的.class文件,而这个.class文件要符合ClassFile的结构,所以这些visitXxx()方法与ClassFile的结构密切相关。 1、visit()方法 用于生成类或者接口的定义,如下生成一个为printField的类,因为如果类默认继承的父类是Object类,所以superName是” java/lang/Object “。

cw.visit(52, ACC_PUBLIC + ACC_SUPER, "com/hank/test/PrintField", null, "java/lang/Object", null);
复制代码

2、visitField()方法 对应classFile中的field_info,用于生成对象里面的属性值。通过visitField生成一个属性,如下:

FieldVisitor fv;
{
fv = cw.visitField(ACC_PRIVATE + ACC_FINAL + ACC_STATIC, "a", "I", null, new Integer(2));
fv.visitEnd();
}
复制代码

3、visitMethod()方法 用于生成一个方法,对应classFile中的method_info

ClassWriter类

ClassWriter的父类是ClassVisitor,因此ClassWriter类继承了visit()、visitField()、visitMethod()和visitEnd()等方法。 toByteArray方法 在ClassWriter类当中,提供了一个toByteArray()方法。这个方法的作用是将对visitXxx()的调用转换成byte[],而这些byte[]的内容就遵循ClassFile结构。 在toByteArray()方法的代码当中,通过三个步骤来得到byte[]:

  • 第一步,计算size大小。这个size就是表示byte[]的最终的长度是多少。
  • 第二步,将数据填充到byte[]当中。
  • 第三步,将byte[]数据返回。

3、使用ClassWriter类 使用ClassWriter生成一个Class文件,可以大致分成三个步骤:

  • 第一步,创建ClassWriter对象。
  • 第二步,调用ClassWriter对象的visitXxx()方法。
  • 第三步,调用ClassWriter对象的toByteArray()方法。
import org.objectweb.asm.ClassWriter;

import static org.objectweb.asm.Opcodes.*;

public class GenerateCore {
public static byte[] dump () throws Exception {
// (1) 创建ClassWriter对象
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
// (2) 调用visitXxx()方法
cw.visit();
cw.visitField();
cw.visitMethod();
cw.visitEnd(); // 注意,最后要调用visitEnd()方法

// (3) 调用toByteArray()方法
byte[] bytes = cw.toByteArray();
return bytes;
}
}
复制代码

Hanno源码分析

上面已经先回顾一下asm相关的基础知识,下面对hanno源码进行分析。主要针对三个方面:

1、如何在方法中插入Log语句。

2、如何获取对象中的field值。

3、如何获取到方法的参数


原文链接:https://juejin.cn/post/7037369790100406309?utm_source=gold_browser_extension

收起阅读 »

Android - 依赖统一管理

#前言 前段时间自己在搭建组件化框架时候遇到了多人协作 Moudle 版本依赖冲突以及重复导包和同一个包导入不同版本的情况,针对这个问题对依赖统一这块做了一次比较详细的学习和总结 目前Android依赖统一管理的方式有以下几种方式,接下来我们一起慢慢分析一下各...
继续阅读 »

#前言


前段时间自己在搭建组件化框架时候遇到了多人协作 Moudle 版本依赖冲突以及重复导包和同一个包导入不同版本的情况,针对这个问题对依赖统一这块做了一次比较详细的学习和总结


目前Android依赖统一管理的方式有以下几种方式,接下来我们一起慢慢分析一下各种方式优缺点



  1. groovy ext扩展函数(也有称之为:"循环优化")

  2. kotlin+buildSrc

  3. composing builds

  4. catalog

  5. 自定义插件+includeBuild


Groovy ext扩展函数


这种方式可能是大家最开始或者说是比较常见的一种依赖配置方式:
iShot2021-12-02 15.17.09.png


示例代码


然后在项目根build.gradle(即root路径下)


apply from:"config.gradle"


引入的方式有两种一种是循环遍历:


iShot2021-12-03 10.26.55.png


iShot2021-12-03 10.32.12.png


总结:


优点:


1:后续添加依赖不需要改动build.gradle,直接在config.gradle


2:精简了build.gradle的长度


缺点:


1:不支持代码提醒


2:不支持点击跳转


3:多moudle 开发时,不同module的依赖需要ctrl+c/v 导致开发的效率降低


kotlin+buildSrc


buildSrc


The directory buildSrc is treated as an included build. Upon discovery of the directory, Gradle automatically compiles and tests this code and puts it in the classpath of your build script. For multi-project builds there can be only one buildSrc directory, which has to sit in the root project directory. buildSrc should be preferred over script plugins as it is easier to maintain, refactor and test the code.


这是来自gradle官方文档对buildSrc的解释:


当运行 Gradle 时会检查项目中是否存在一个名为 buildSrc 的目录。然后 Gradle 会自动编译并测试这段代码,并将其放入构建脚本的类路径中, 对于多项目构建,只能有一个 buildSrc 目录,该目录必须位于根项目目录中, buildSrc 是 Gradle 项目根目录下的一个目录,它可以包含我们的构建逻辑,与脚本插件相比,buildSrc 应该是首选,因为它更易于维护、重构和测试代码


通过上面的介绍,大家或许对buildsrc 有一定理解了,那么我们就看下他怎么和kotlin一起使用达到项目统一依赖管理的


iShot2021-12-03 13.44.50.png


如上图所示我们首先创建一个名为buildSrc的module,gradle 构建的时候会先检查工程中是否有buildSrc命名的目录然后会自动编译和测试这段代码并写入到构建脚本的类路径中,所以无需在setting.gradle 做任何配置有关buildSrc的配置信息


官方的配置信息


iShot2021-12-03 14.19.27.png


iShot2021-12-03 14.21.33.png


这是我的项目中配置信息


这种方式管理依赖优点和缺点如下:


优点:


1:但这种方式支持IDE,输入代码会有提示,会自动完成,所以非常推荐使用这种方式来管理项目中的依赖包


2:支持 AndroidStudio 单击跳转


缺点:


来自gradle文档


A change in buildSrc causes the whole project to become out-of-date. Thus, when making small incremental changes, the --no-rebuild command-line option is often helpful to get faster feedback. Remember to run a full build regularly or at least when you’re done, though.


更改buildSrc会导致整个项目过时。因此,在进行小的增量更改时,--no-rebuild命令行选项通常有助于获得更快的反馈。不过,请记住定期或至少在完成后运行完整构建。


从官网的解释我们可以得出结论:


buildSrc 是对全局的所有 module 的配置依赖更新会重新构建整个项目,项目越大,重新构建的时间就越长,造成不必要的时间浪费。


阅读到这里我们可能会思考那么有没有一种方式是在部分module 需要修改依赖版本的时候而不会重新构建整个项目的方式呢,探索极致是每一个研发人员毕生所追求的,那么***"includeBuild"***这种方式应运而生


composing builds


那么我们开始一步一步实现这种方式:


1:首先创建一个library 的module <对于使用kotlin 或者 java>就要看自己的比较中意哪种语言喽


iShot2021-12-03 14.46.44.png


2:就是在library 配置插件等信息


apply plugin: 'kotlin'
apply plugin: 'java-gradle-plugin'

buildscript {
repositories {
// https://developer.aliyun.com/mvn/guide
//todo error:"Using insecure protocols with repositories, without explicit opt-in,"
google()
mavenCentral()
maven { url 'https://maven.aliyun.com/repository/public' }
maven { url 'https://maven.aliyun.com/repository/google' }
maven { url 'https://maven.aliyun.com/repository/gradle-plugin' }
}

dependencies {
// 因为使用的 Kotlin 需要需要添加 Kotlin 插件
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.21"
}
}




repositories {
// 需要添加 jcenter 否则会提示找不到 gradlePlugin
repositories {
google()
mavenCentral()
maven { url 'https://maven.aliyun.com/repository/public' }
maven { url 'https://maven.aliyun.com/repository/google' }
maven { url 'https://maven.aliyun.com/repository/gradle-plugin' }
}

}

dependencies {
implementation gradleApi()
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8"
}


compileKotlin {
kotlinOptions {
jvmTarget = "1.8"
}
}

compileTestKotlin {
kotlinOptions {
jvmTarget = "1.8"
}
}

gradlePlugin{
plugins {
version{
// 在 app 模块需要通过 id 引用这个插件
id = 'com.bimromatic.version.plugin'
// 实现这个插件的类的路径
implementationClass = 'com.bimromatic.plugin.VersionPlugin'
}
}
}
复制代码

3:在项目路径下建立一个在.gradle 配置的类名实现Plugin 这个接口


/*
* Copyright 2009 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.gradle.api;

/**
* <p>A <code>Plugin</code> represents an extension to Gradle. A plugin applies some configuration to a target object.
* Usually, this target object is a {@link org.gradle.api.Project}, but plugins can be applied to any type of
* objects.</p>
*
* @param <T> The type of object which this plugin can configure.
*/
public interface Plugin<T> {
/**
* Apply this plugin to the given target object.
*
* @param target The target object
*/
void apply(T target);
}
复制代码

4:在settings.gradle添加


iShot2021-12-03 15.01.56.png


5:在需要用的地方添加插件名


iShot2021-12-03 15.03.33.png


详细配置请移步我们的项目查看


因为时间的原因,这次项目管理依赖就讲到这里,后续会把google在孵化器期 Catalog统一配置依赖版本 讲解一下,然后我们再把各种依赖管理方式用在编辑器跑一下试试看看那种方式构建速度最快。


如果你们觉得写得不错的随手给我点个关注,后期会持续做移动端技术文章的分享,或者给我的github 点个start 后期会上传一些干货。


对了如果文章中有讲的什么不对的地方咱们评论区见,或者提上你们宝贵的issue

作者:bimromatic
链接:https://juejin.cn/post/7037371592816459806
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

收起阅读 »

Android实战——RecyclerView条目曝光埋点

一、概要 100行代码实现recyclerview条目曝光埋点设计 二、设计思路 条目露出来一半以上视为该条目曝光。 在rv滚动过程中或者数据变更回调OnGlobalLayoutListener时,将符合条件1的条目记录在曝光列表、上传埋点集合里。 滚动状态...
继续阅读 »

一、概要


100行代码实现recyclerview条目曝光埋点设计


二、设计思路



  1. 条目露出来一半以上视为该条目曝光。

  2. 在rv滚动过程中或者数据变更回调OnGlobalLayoutListener时,将符合条件1的条目记录在曝光列表、上传埋点集合里。

  3. 滚动状态变更和OnGlobalLayoutListener回调时,且列表状态为idle状态,触发上报埋点。


三、容错性



  1. 滑动过快时,视为未曝光

  2. 数据变更时,重新检测曝光

  3. 曝光过的条目,不会重复曝光


四、接入影响



  1. 对业务代码零侵入

  2. 对列表滑动体验无影响


五、代码实现


import android.graphics.Rect
import android.view.View
import androidx.recyclerview.widget.RecyclerView
import java.util.*

class RVItemExposureListener(
private val mRecyclerView: RecyclerView,
private val mExposureListener: IOnExposureListener?
) {
interface IOnExposureListener {
fun onExposure(position: Int)
fun onUpload(exposureList: List<Int>?): Boolean
}

private val mExposureList: MutableList<Int> = ArrayList()
private val mUploadList: MutableList<Int> = ArrayList()
private var mScrollState = 0

var isEnableExposure = true
private var mCheckChildViewExposure = true

private val mViewVisible = Rect()
fun checkChildExposeStatus() {
if (!isEnableExposure) {
return
}
val length = mRecyclerView.childCount
if (length != 0) {
var view: View?
for (i in 0 until length) {
view = mRecyclerView.getChildAt(i)
if (view != null) {
view.getLocalVisibleRect(mViewVisible)
if (mViewVisible.height() > view.height / 2 && mViewVisible.top < mRecyclerView.bottom) {
checkExposure(view)
}
}
}
}
}

private fun checkExposure(childView: View): Boolean {
val position = mRecyclerView.getChildAdapterPosition(childView)
if (position < 0 || mExposureList.contains(position)) {
return false
}
mExposureList.add(position)
mUploadList.add(position)
mExposureListener?.onExposure(position)
return true
}

private fun uploadList() {
if (mScrollState == RecyclerView.SCROLL_STATE_IDLE && mUploadList.size > 0 && mExposureListener != null) {
val success = mExposureListener.onUpload(mUploadList)
if (success) {
mUploadList.clear()
}
}
}

init {
mRecyclerView.viewTreeObserver.addOnGlobalLayoutListener {
if (mRecyclerView.childCount == 0 || !mCheckChildViewExposure) {
return@addOnGlobalLayoutListener
}
checkChildExposeStatus()
uploadList()
mCheckChildViewExposure = false
}
mRecyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(
recyclerView: RecyclerView,
newState: Int
) {
super.onScrollStateChanged(recyclerView, newState)
mScrollState = newState
uploadList()
}

override fun onScrolled(
recyclerView: RecyclerView,
dx: Int,
dy: Int
) {
super.onScrolled(recyclerView, dx, dy)
if (!isEnableExposure) {
return
}

// 大于50视为滑动过快
if (mScrollState == RecyclerView.SCROLL_STATE_SETTLING && Math.abs(dy) > 50) {
return
}
checkChildExposeStatus()
}
})
}
}


六、使用


RVItemExposureListener(yourRecyclerView, object : RVItemExposureListener.IOnExposureListener {
override fun onExposure(position: Int) {
// 滑动过程中出现的条目
Log.d("exposure-curPosition:", position.toString())
}

override fun onUpload(exposureList: List<Int>?): Boolean {
Log.d("exposure-positionList", exposureList.toString())
// 上报成功后返回true
return true
}

})

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

聊一聊线程池和Kotlin协程

目前很多开发组都用上协程来处理异步任务了,但是有的地方协程提供的原生API还是不足以应付,比方说一些SDK提供了传入Executor的接口(以便复用调用者的线程池来执行异步任务),这时候可以用JDK提供的线程池,或者封装一下协程也可以满足需求。 协程提供了Di...
继续阅读 »

目前很多开发组都用上协程来处理异步任务了,但是有的地方协程提供的原生API还是不足以应付,比方说一些SDK提供了传入Executor的接口(以便复用调用者的线程池来执行异步任务),这时候可以用JDK提供的线程池,或者封装一下协程也可以满足需求。


协程提供了Dispatchers.DefaultDispatchers.IO 分别用于 计算密集型 任务和 IO密集型 任务,类似于RxJava的 Schedulers.computation()Schedulers.io()

但两者有所差异,比如RxJava的 Schedulers.io() 不做并发限制,而 Dispatchers.io() 做了并发限制:



It defaults to the limit of 64 threads or the number of cores (whichever is larger)



考虑到当前移动设备的CPU核心数都不超过64,所以可以认为协程的 Dispatchers.IO 的最大并发为64。

Dispatchers.Default 的并发限制为:



By default, the maximal level of parallelism used by this dispatcher is equal to the number of CPU cores, but is at least two



考虑到目前Android设备核心数都在2个以上,所以可以认为 Dispatchers.Default 的最大并发为 CPU cores。

Dispatchers.DefaultDispatchers.IO 是共享协程自己的线程池的,二者可以复用线程。

不过目前这两个Dispatchers 并未完全满足项目中的需求,有时我们需要一些自定义的并发限制,其中最常见的是串行。


RxJava有Schedulers.single() ,但这个Schedulers.single()和AsyncTask的SERAIL_EXECOTOR一样,是全局串行,不同的任务处在同一个串行队列,会相互堵塞,因而可能会引发问题。


或许也是因为这个原因,kotlin协程没有定义“Dispatchers.Single"。

对于需要串行的场景,可以这样实现:


val coroutineContext: CoroutineContext =
Executors.newSingleThreadExecutor().asCoroutineDispatcher()

这样可以实现局部的串行,但和协程的线程池是相互独立的,不能复用线程。

线程池的好处:



  1. 提高响应速度:任务到达时,无需等待线程创建即可立即执行。

  2. 降低资源消耗:通过池化技术重复利用已创建的线程,降低线程创建和销毁造成的损耗。

  3. 提高线程的可管理性:线程是稀缺资源,如果无限制创建,不仅会消耗系统资源,还会因为线程的不合理分布导致资源调度失衡,降低系统的稳定性。使用线程池可以进行统一的分配、调优和监控。


然彼此独立创建线程池的话,会大打折扣。

如何既复用协程的线程池,又自主控制并发呢?

一个办法就是套队列来控制并发,然后还是任务还是执行在线程池之上。

AsyncTask 就是这样实现的:


private static class SerialExecutor implements Executor {
final ArrayDeque<Runnable> mTasks = new ArrayDeque<Runnable>();
Runnable mActive;

public synchronized void execute(final Runnable r) {
mTasks.offer(new Runnable() {
public void run() {
try {
r.run();
} finally {
scheduleNext();
}
}
});
if (mActive == null) {
scheduleNext();
}
}

protected synchronized void scheduleNext() {
if ((mActive = mTasks.poll()) != null) {
THREAD_POOL_EXECUTOR.execute(mActive);
}
}
}


用SerialExecutor的execute的任务会先进入队列,当mActive为空时从队列获取任务赋值给mActive然后通过线程池 THREAD_POOL_EXECUTOR执行。

当然AsyncTask 的SerialExecutor是全局唯一的,所以会有上面提到的各种任务相互堵塞的问题。可以通过创建不同是的SerialExecutor实例来达到各业务各自串行。


在Kotlin环境下,我们可以利用协程和Channel来实现:


fun Channel<Any>.runBlock(block: suspend CoroutineScope.() -> Unit) {
CoroutineScope(Dispatchers.Unconfined).launch {
send(0)
CoroutineScope(Dispatchers.IO).launch {
block()
receive()
}
}
}

// 使用方法
private val serialChannel = Channel<Any>(1)
serialChannel.runBlock {
// do somthing
}


添加Log编写测试如下:


private val a = AtomicInteger(0)
private val b = AtomicInteger(0)
fun Channel<Any>.runBlock(block: suspend CoroutineScope.() -> Unit) {
CoroutineScope(Dispatchers.Unconfined).launch {
Log.d("MyTag", "before send " + a.getAndIncrement() + getTime())
send(0)
Log.i("MyTag", "after send " + b.getAndIncrement() + getTime())
CoroutineScope(Dispatchers.Default).launch {
block()
receive()
}
}
}

private fun test() {
// 并发限制为1,串行执行任务
val channel = Channel<Any>(1)
val t1 = System.currentTimeMillis()
repeat(4) { x ->
channel.runBlock {
Thread.sleep(1000L)
Log.w("MyTag", "$x done job" + getTime())
}
}

CoroutineScope(Dispatchers.Default).launch {
while (!channel.isEmpty) {
delay(200)
}
val t2 = System.currentTimeMillis()
Log.d("MyTag", "Jobs all done, use time:" + (t2 - t1))
}
}


执行结果:



第一个任务可以顺利通过send(), 而随后的任务被suspend, 直到前面的任务执行完(执行block),调用recevie(), 然后下一个任务通过send() ……依此类推。

最终,消耗4s完成任务。


如果Channel的参数改成2,则能有两个任务可以通过send() :



最终,消耗2s完成任务。


关于参数可以参考Channel的构造函数:


public fun <E> Channel(capacity: Int = RENDEZVOUS): Channel<E> =
when (capacity) {
RENDEZVOUS -> RendezvousChannel()
UNLIMITED -> LinkedListChannel()
CONFLATED -> ConflatedChannel()
BUFFERED -> ArrayChannel(CHANNEL_DEFAULT_CAPACITY)
else -> ArrayChannel(capacity)
}

在前面的实现中, 我们关注UNLIMITED, BUFFERED 以及 capacity > 0 的情况即可:



  • UNLIMITED: 不做限制;

  • BUFFERED: 并发数由 kotlin "kotlinx.coroutines.channels.defaultBuffer"决定,目前测试得到8;

  • capacity > 0, 则并发数由 capacity 决定;

  • 特别地,当capacity = 1,为串行调度。


不过,[Dispatchers.IO] 本身有并发限制(目前版本是64),

所有对于 Channel.UNLIMITED 和 capacity > 64 的情况,和capacity=64的情况是相同的。

我们可以为不同的业务创建不同的Channel实例,从而各自控制并发且最终在协程的线程池上执行任务。

简要示意图如下:



为了简化,我们假设Dispatchers的并发限制为4。



  • 不同Channel有各自的buffer, 当任务小于capacity时进入buffer, 大于capacity时新任务被suspend。

  • Dispatchers 不断地执行任务然后调用receive(), 上面的实现中,receive并非要取什么信息,仅仅是让channel空出buffer, 好让被suspend的任务可以通过send()然后进入Dispatchers的调度。

  • 极端情况下(进入Disptachers的任务大于并发限制时),任务进入Dispatchers也不会被立即执行,这个设定可以避免开启的线程太多而陷于线程上下文频繁切换的困境。


通过Channel可以实现并发的控制,但是日常开发中有的地方并不是简单地执行个任务,而是需要一个ExecutorService或者Executor。


为此,我们可以实现一个ExecutorService。

当然了,不是直接implement ExecutorService, 而是像ThreadPoolExecutor一样继承AbstractExecutorService, 这样只需要实现几个方法即可。



最终完整代码如下:


fun Channel<Any>.runBlock(block: suspend CoroutineScope.() -> Unit) {
CoroutineScope(Dispatchers.Unconfined).launch {
send(0)
CoroutineScope(Dispatchers.IO).launch {
block()
receive()
}
}
}

class ChannelExecutorService(capacity: Int) : AbstractExecutorService() {
private val channel = Channel<Any>(capacity)

override fun execute(command: Runnable) {
channel.runBlock {
command.run()
}
}

fun isEmpty(): Boolean {
return channel.isEmpty || channel.isClosedForReceive
}

override fun shutdown() {
channel.close()
}

override fun shutdownNow(): MutableList<Runnable> {
shutdown()
return mutableListOf()
}

@ExperimentalCoroutinesApi
override fun isShutdown(): Boolean {
return channel.isClosedForSend
}

@ExperimentalCoroutinesApi
override fun isTerminated(): Boolean {
return channel.isClosedForReceive
}

override fun awaitTermination(timeout: Long, unit: TimeUnit): Boolean {
var millis = unit.toMillis(timeout)
while (!isTerminated && millis > 0) {
try {
Thread.sleep(200L)
millis -= 200L
} catch (ignore: Exception) {
}
}
return isTerminated
}
}

需要简单地控制并发的地方,直接定义Channel然后调用runBlock即可;


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

Android 编译速度提升黑科技 - RocketX

怎么做编译优化,当时说了个方案,就是编译时将所有的模块依赖修改为 aar,然后每次编译将变动的模块改成源码依赖,同时编译完成再将修改模块上传为 aar,这样可以始终做到仅有最少的模块参与源码编译,从而提升编译速度。 当然说起来轻松,做起来没有那么容易,终于有位...
继续阅读 »

怎么做编译优化,当时说了个方案,就是编译时将所有的模块依赖修改为 aar,然后每次编译将变动的模块改成源码依赖,同时编译完成再将修改模块上传为 aar,这样可以始终做到仅有最少的模块参与源码编译,从而提升编译速度。


当然说起来轻松,做起来没有那么容易,终于有位小伙伴将上述描述开发成一个开源方案了,非常值得大家学习和借鉴。


1.背景描述


在项目体量越来越大的情况下,编译速度也随着增长,有时候一个修改需要等待长达好几分钟的编译时间。


基于这种普遍的情况,推出了 RocketX ,通过在编译流程动态修改项目依赖关系, 动态 替换 module 为 aar,做到只编译改动模块,其他模块不参与编译,无需改动原有项目任何代码,提高全量编译的速度。


2.效果展示


2.1、测试项目介绍

目标项目一共 3W+ 个类与资源文件,全量编译 4min 左右(测试使用 18 年 mbp 8代i7 16g)。


通过 RocketX 全量增速之后的效果(每一个操作取 3 次平均值)。


image.png


项目依赖关系如下图,app 依赖 bm 业务模块,bm 业务模块依赖顶层 base/comm 模块。


image.png


依赖关系


• 当 base/comm 模块改动,底部的所有模块都必须参与编译。因为 app/bmxxx 模块可能使用了 base 模块中的接口或变量等,并且不知道是否有改动到。(那么速度就非常慢)


• 当 bmDiscover 做了改动,只需要 app 模块和 bmDiscover 两个模块参与编译。(速度较快)


• rx(RocketX) 在无论哪一个模块的编译速度基本都是在控制在 30s 左右,因为只编译 app 和 改动的模块,其他模块是 aar 包不参与编译。


顶层模块速度提升 300%+


3.思路问题分析与模块搭建


3.1、思路问题分析

需要通过 gradle plugin 的形式动态修改没有改动过的 module 依赖为 相对应的 aar 依赖,如果 module 改动,退化成 project 工程依赖,这样每次只有改动的 module 和 app 两个模块编译。


需要把 implement/api moduleB,修改为implement/api aarB。


需要构建 local maven 存储未被修改的 module 对应的 aar。(也可以通过 flatDir 代替速度更快)


编译流程启动,需要找到哪一个 module 做了修改。


需要遍历每一个 module 的依赖关系进行置换, module 依赖怎么获取?一次性能获取到所有模块依赖,还是分模块各自回调?修改其中一个模块依赖关系会阻断后面模块依赖回调?


每一个 module 换变成 aar 之后,自身依赖的 child 依赖 (网络依赖,aar),给到 parent module (如何找到所有 parent module) ? 还是直接给 app module ? 有没有 app 到 module 依赖断掉的风险?这里需要出一个技术方案。


需要hook 编译流程,完成后置换 loacal maven 中被修改的 aar。


提供 AS 状态栏 button, 实现开启关闭功能,加速编译还是让开发者使用已经习惯性的三角形 run 按钮。


3.2、模块搭建

依照上面的分析,虽然问题很多,但是大致可以把整个项目分成以下几块:


image.png


4.问题解决与实现


4.1、implement 源码实现入口在 DynamicAddDependencyMethods 中的 tryInvokeMethod 方法。他是一个动态语言的 methodMissing 功能。

tryInvokeMethod 代码分析:


 public DynamicInvokeResult tryInvokeMethod(String name, Object... arguments) {       //省略部分代码 ...       return DynamicInvokeResult.found(this.dependencyAdder.add(configuration, normalizedArgs.get(0), (Closure)null)); }
复制代码

dependencyAdder 实现是一个 DirectDependencyAdder。


private class DirectDependencyAdder implements DependencyAdder<Dependency> {    private DirectDependencyAdder() {    }    public Dependency add(Configuration configuration, Object dependencyNotation, @Nullable Closure configureAction) {        return DefaultDependencyHandler.this.doAdd(configuration, dependencyNotation, configureAction);    }}
复制代码

最后是在 DefaultDependencyHandler.this.doAdd 进行添加进去,而 DefaultDependencyHandler 在 project可以获取。


  DependencyHandler getDependencies(); 
复制代码

通过以上的分析,添加相对应的 aar/jar 可以通过以下代码实现。


fun addAarDependencyToProject(aarName: String, configName: String, project: Project) {    //添加 aar 依赖 以下代码等同于 api/implementation/xxx (name: 'libaccount-2.0.0', ext: 'aar'),源码使用 linkedMap    if (!File(FileUtil.getLocalMavenCacheDir() + aarName + ".aar").exists()) return    val map = linkedMapOf<String, String>()    map.put("name", aarName)    map.put("ext", "aar")    // TODO: 2021/11/5 改变依赖 这里后面需要修改成    //project.dependencies.add(configName, "com.${project.name}:${project.name}:1.0")    project.dependencies.add(configName, map)}
复制代码

4.2、localMave 优先使用 flatDir 实现通过指定一个缓存目录把生成 aar/jar 包丢进去,依赖修改时候通过找寻进行替换。

fun flatDirs() {    val map = mutableMapOf<String, File>()    map.put("dirs", File(getLocalMavenCacheDir()))    appProject.rootProject.allprojects {        it.repositories.flatDir(map)    }}
复制代码

4.3、编译流程启动,需要找到哪一个 module做了修改。

使用遍历整个项目的文件的 lastModifyTime 去做实现。


以每一个 module 为一个粒度,递归遍历当前 module 的文件,把每个文件的 lastModifyTime 整合计算得出一个唯一标识 countTime。


通过 countTime 与上一次的作对比,相同说明没改动,不同则改动. 并需要同步计算后的 countTime 到本地缓存中。


整体 3W 个文件耗时 1.2s 可以接受。


4.4、 module 依赖关系获取。

通过以下代码可以找到生成整个项目的依赖关系图时机,并在此处生成依赖图解析器。


 project.gradle.addListener(DependencyResolutionListener listener)
复制代码

4.5、 module 依赖关系 project 替换成 aar 技术方案

每一个 module 依赖关系替换的遍历顺序是无序的,所以技术方案需要支持无序的替换。


目前使用的方案是:如果当前模块 A 未改动,需要把 A 通过 localMaven 置换成 A.aar,并把 A.aar 以及 A 的 child 依赖,给到第一层的 parent module 即可。(可能会质疑如果 parent module 也是 aar 怎么办,其实这块也是没有问题的,这里就不展开说了,篇幅太长)


为什么要给到 parent 不能直接给到 app ,下图一个简单的示例如果 B.aar 不给 A 模块的话,A 使用 B 模块的接口不见了,会导致编译不过。


image.png


给出整体项目替换的技术方案演示:


image.png


4.5、hook 编译流程,完成后置换 loacal maven 中被修改的 aar。

点击三角形 run,执行的命令是 app:assembleDebug , 需要在 assembleDebug 后面补一个 uploadLocalMavenTask, 通过 finalizedBy 把我们的 task 运行起来去同步修改后的 aar


4.6、提供 AS 状态栏 button,小火箭按钮一个喷火一个没有喷火,代表 enable/disable , 一个 扫把clean rockectx 的缓存。

image.png


5一天一个小惊喜


5.1、发现点击 run 按钮 ,执行的命令是 app:assembleDebug ,各个子 module 在 output 并没有打包出 aar。

解决:通过研究 gradle 源码发现打包是由 bundleFlavor{Flavor}{BuildType}Aar 这个task执行出来,那么只需要将各个模块对应的 task 找到并注入到 app:assembleDebug 之后运行即可。


5.2、发现运行起来后存在多个 jar 包重复问题。

解决:implementation fileTree(dir: "libs", include: ["*.jar"]) jar 依赖不能交到 parent module,jar 包会打进 aar 中的lib 可直接剔除。通过以下代码可以判断:


// 这里的依赖是以下两种: 无需添加在 parent ,因为 jar 包直接进入 自身的 aar 中的libs 文件夹//    implementation rootProject.files("libs/xxx.jar")//    implementation fileTree(dir: "libs", include: ["*.jar"])childDepency.files is DefaultConfigurableFileCollection || childDepency.files is DefaultConfigurableFileTree
复制代码

5.3、发现 aar/jar 存在多种依赖方式。

implementation (name: 'libXXX', ext: 'aar')


implementation files("libXXX.aar")


解决:使用第一种,第二种会合并进aar,导致类重复问题.


5.4、发现 aar 新姿势依赖。

configurations.maybeCreate("default")artifacts.add("default", file('lib-xx.aar'))
复制代码

上面代码把 aar 做了一个单独的 module 给到其他 module 依赖,default config 其实是 module 最终输出 aar 的持有者,default config 可以持有一个 列表的aar ,所以把 aar 手动添加到 default config,也相当于当前 module 打包出来的产物。


解决:通过 childProject.configurations.maybeCreate("default").artifacts 找到所有添加进来的 aar ,单独发布 localmaven。


5.5、发现 android module 打包出来可以是 jar。

解决:通过找到名字叫做 jar 的task,并且在 jar task 后面注入 uploadLocalMaven task。


5.6、发现 arouter 有 bug,transform 没有通过 outputProvider.deleteAll() 清理旧的缓存。

解决:详情查看 issue,结果arouter 问题是解决了,代码也是合并了。但并没有发布新的插件版本到 mavenCentral,于是先自行帮 arouter 解决一下。


github.com/alibaba/ARo…



关注我,每天分享知识干货!

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

Android CameraX结合LibYUV和GPUImage自定义相机滤镜

前言 之前使用Camera实现了一个自定义相机滤镜(Android自定义相机滤镜 ),但是运行起来有点卡顿,这次用Camerax来实现一样的效果发现很流畅,在此记录一下,也希望能帮到有需要的同学。 实现效果 实现步骤 1.引入依赖库 这里我引入的依赖库有Ca...
继续阅读 »

前言


之前使用Camera实现了一个自定义相机滤镜(Android自定义相机滤镜 ),但是运行起来有点卡顿,这次用Camerax来实现一样的效果发现很流畅,在此记录一下,也希望能帮到有需要的同学。


实现效果



实现步骤


1.引入依赖库

这里我引入的依赖库有CameraXGPUImage(滤镜库)、Utilcodex(一款好用的工具类)


// CameraX core library using camera2 implementation
    implementation "androidx.camera:camera-camera2:1.0.1"
// CameraX Lifecycle Library
    implementation "androidx.camera:camera-lifecycle:1.0.1"
// CameraX View class
    implementation "androidx.camera:camera-view:1.0.0-alpha27"

    implementation'jp.co.cyberagent.android.gpuimage:gpuimage-library:1.4.1'
    implementation 'com.blankj:utilcodex:1.30.6'

2.引入libyuv

这里我用的是这个案例(github.com/theeasiestw…



3.编写CameraX预览代码

布局代码如下


<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">
    <androidx.camera.view.PreviewView
        android:id="@+id/viewFinder"
        android:layout_width="0dp"
        android:layout_height="0dp" />
</FrameLayout>

Activity中开启相机预览代码如下,基本都是Google官方提供的案例代码


class MainActivity : AppCompatActivity() {
    private lateinit var cameraExecutor: ExecutorService
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        cameraExecutor = Executors.newSingleThreadExecutor()
        // Request camera permissions
        if (allPermissionsGranted()) {
            startCamera()
        } else {
            ActivityCompat.requestPermissions(
                this, REQUIRED_PERMISSIONS, REQUEST_CODE_PERMISSIONS)
        }
    }

    private fun allPermissionsGranted() = REQUIRED_PERMISSIONS.all {
        ContextCompat.checkSelfPermission(
            baseContext, it) == PackageManager.PERMISSION_GRANTED
    }

    override fun onRequestPermissionsResult(
        requestCode: Int, permissions: Array<String>, grantResults:
        IntArray) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)
        if (requestCode == REQUEST_CODE_PERMISSIONS) {
            if (allPermissionsGranted()) {
                startCamera()
            } else {
                Toast.makeText(this,
                    "Permissions not granted by the user.",
                    Toast.LENGTH_SHORT).show()
                finish()
            }
        }
    }
    private fun startCamera() {
        val cameraProviderFuture = ProcessCameraProvider.getInstance(this)
        cameraProviderFuture.addListener(Runnable {
            val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()
            val preview = Preview.Builder()
                .build()
                .also {
                    it.setSurfaceProvider(viewFinder.surfaceProvider)
                }
            val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
            try {
                cameraProvider.unbindAll()
                cameraProvider.bindToLifecycle(
                    this, cameraSelector, preview)
            } catch(exc: Exception) {
                Log.e(TAG, "Use case binding failed", exc)
            }
        }, ContextCompat.getMainExecutor(this))
    }

    override fun onDestroy() {
        super.onDestroy()
        cameraExecutor.shutdown()
    }

    companion object {
        private const val TAG = "CameraXBasic"
        private const val REQUEST_CODE_PERMISSIONS = 10
        private val REQUIRED_PERMISSIONS = arrayOf(Manifest.permission.CAMERA)

    }
}

到这里就可以实现相机预览了



4.增加相机数据回调

我们要增加滤镜效果就必须对相机的数据进行操作,这里我们通过获取相机数据回调来获取可修改的数据


val imageAnalyzer = ImageAnalysis.Builder()
                //设置回调数据的比例为16:9
                .setTargetAspectRatio(AspectRatio.RATIO_16_9)
                .build()
                .also {
                    it.setAnalyzer(cameraExecutor,this@MainActivity)
                }

这里我们还需要进行绑定



除此之外我们还需要在Activity中实现ImageAnalysis.Analyzer接口,数据的获取就在此接口的回调方法中获取,如下所示,其中ImageProxy就包含了图像数据


override fun analyze(image: ImageProxy) {

}

5.对回调数据进行处理

我们在相机数据回调的方法中对图像进行处理并添加滤镜,当然在此之前我们还需要创建GPUImage对象并设置滤镜类型


private var bitmap:Bitmap? = null
private var gpuImage:GPUImage? = null
//创建GPUImage对象并设置滤镜类型,这里我使用的是素描滤镜
private fun initFilter() {
        gpuImage = GPUImage(this)
        gpuImage!!.setFilter(GPUImageSketchFilter())
    }
@SuppressLint("UnsafeOptInUsageError")
    override fun analyze(image: ImageProxy) {
        //将Android的YUV数据转为libYuv的数据
        var yuvFrame = yuvUtils.convertToI420(image.image!!)
        //对图像进行旋转(由于回调的相机数据是横着的因此需要旋转90度)
        yuvFrame = yuvUtils.rotate(yuvFrame, 90)
        //根据图像大小创建Bitmap
        bitmap = Bitmap.createBitmap(yuvFrame.width, yuvFrame.height, Bitmap.Config.ARGB_8888)
        //将图像转为Argb格式的并填充到Bitmap上
        yuvUtils.yuv420ToArgb(yuvFrame,bitmap!!)
        //利用GpuImage给图像添加滤镜
        bitmap = gpuImage!!.getBitmapWithFilterApplied(bitmap)
        //由于这不是UI线程因此需要在UI线程更新UI
        img.post {
            img.setImageBitmap(bitmap)
            //关闭ImageProxy,才会回调下一次的数据
            image.close()
        }

    }

6.拍摄照片

这里我们加一个拍照的按钮


<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">
    <androidx.camera.view.PreviewView
        android:id="@+id/viewFinder"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
    <ImageView
        android:id="@+id/img"
        android:scaleType="centerCrop"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>
    <Button
        android:id="@+id/bt_takepicture"
        android:layout_gravity="center_horizontal|bottom"
        android:layout_marginBottom="100dp"
        android:text="拍照"
        android:layout_width="70dp"
        android:layout_height="70dp"/>
</FrameLayout>

然后我们在Activity中添加拍照的逻辑,其实就是将Bitmap转为图片保存到SD卡,这里我们使用了之前引入的Utilcodex工具,当我们点击按钮的时候isTakePhoto 会变为true,然后在相机的回调中就会进行保存图片的处理


bt_takepicture.setOnClickListener {
            isTakePhoto = true
        }

并且我们加入变量控制,在拍照的时候不处理回调数据


@SuppressLint("UnsafeOptInUsageError")
    override fun analyze(image: ImageProxy) {
        if(!isTakePhoto){
            //将Android的YUV数据转为libYuv的数据
            var yuvFrame = yuvUtils.convertToI420(image.image!!)
            //对图像进行旋转(由于回调的相机数据是横着的因此需要旋转90度)
            yuvFrame = yuvUtils.rotate(yuvFrame, 90)
            //根据图像大小创建Bitmap
            bitmap = Bitmap.createBitmap(yuvFrame.width, yuvFrame.height, Bitmap.Config.ARGB_8888)
            //将图像转为Argb格式的并填充到Bitmap上
            yuvUtils.yuv420ToArgb(yuvFrame,bitmap!!)
            //利用GpuImage给图像添加滤镜
            bitmap = gpuImage!!.getBitmapWithFilterApplied(bitmap)
            //由于这不是UI线程因此需要在UI线程更新UI
            img.post {
                img.setImageBitmap(bitmap)
                if(isTakePhoto){
                    takePhoto()
                }
                //关闭ImageProxy,才会回调下一次的数据
                image.close()
            }
        }else{
            image.close()
        }
    }
 /**
     * 拍照
     */
    private fun takePhoto() {
        Thread{
            val filePath = File(getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),"${System.currentTimeMillis()}save.png")
            ImageUtils.save(bitmap,filePath.absolutePath,Bitmap.CompressFormat.PNG)
            ToastUtils.showShort("拍摄成功")
            isTakePhoto = false
        }.start()
    }

效果如下



保存的图片在如下目录



保存的图片如下



只有不断的学习进步,才能不被时代淘汰。关注我,每天分享知识干货!


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

探究Android属性动画执行过程

1.引言属性动画作为Android动画功能的一个重要组成部分,可以实现很多有趣的动画效果,理解属性动画的执行过程有助于我们更好地使用属性动画去实现需求。本文将从源码的角度去探索属性动画的实现过程,加深大家对其的认知和理解。2.属性动画相关的类2.1 Value...
继续阅读 »

1.引言

属性动画作为Android动画功能的一个重要组成部分,可以实现很多有趣的动画效果,理解属性动画的执行过程有助于我们更好地使用属性动画去实现需求。本文将从源码的角度去探索属性动画的实现过程,加深大家对其的认知和理解。

2.属性动画相关的类

2.1 ValueAnimator

这个类是实现属性动画的一个重要的类,通过ValueAnimator.ofFloat()、ValueAnimator.ofInt()、ValueAnimator.ofObject()、ValueAnimator.ofArgb()、ValueAnimator.ofPropertyValuesHolder()等方法可以获得ValueAnimator的对象,然后可以通过对这个对象的操作去实现动画。使用ValueAnimator实现属性动画,需要实现ValueAnimator.AnimatorUpdateListener()接口,并在onAnimationUpdate()方法内为要添加动画的对象设置属性值。

2.2 ObjectAnimator

ObjectAnimator是ValueAnimator的子类,可以操作目标对象的动画属性,这个类的构造函数支持采用参数的形式传入要使用动画的目标对象和属性名。

3.属性动画的实现过程

ObjectAnimator objectAnimator = ObjectAnimator.ofFloat(iv, "alpha", 1.0f, 0f);
objectAnimator.setDuration(3000);
objectAnimator.start();

这是一段简单的代码,它使用属性动画实现了一张图片的透明度渐变的效果,我们从这一段代码入手,去分析属性动画的实现过程。

3.1 创建属性动画

/**
* target:添加动画效果的目标对象
* propertyName:动画效果的属性名
* values:动画将会在这个时间之间执行的数值集合
*/
public static ObjectAnimator ofFloat(Object target, String propertyName, float... values){
ObjectAnimator anim = new ObjectAnimator(target, propertyName);
anim.setFloatValues(values);
return anim;
}

这个方法返回了一个属性动画对象,第一个参数是产生动画效果的目标对象,第二个参数是属性名,目标对象的属性名应该有与之对应的set()方法,例如我们传入属性名"alpha",那么这个目标对象也应该有setAlpha()方法。参数values传一个值的时候,这个值是动画的结束值,传两个数值的时候,第一个值是开始值,第二个值是结束值,多于两个值的时候,第一个值是开始值,最后一个值是结束值。

private ObjectAnimator(Object target, String propertyName) {
setTarget(target);
setPropertyName(propertyName);
}

这个是属性动画的构造函数,里面执行了两个方法setTarget(target)和setPropertyName(propertyName)。

@Override
public void setTarget(@Nullable Object target) {
final Object oldTarget = getTarget();
if (oldTarget != target) {
if (isStarted()) {
cancel();
}
mTarget = target == null ? null : new WeakReference<Object>(target);
// New target should cause re-initialization prior to starting
mInitialized = false;
}
}
public void setPropertyName(@NonNull String propertyName) {
// mValues could be null if this is being constructed piecemeal. Just record the
// propertyName to be used later when setValues() is called if so.
if (mValues != null) {
PropertyValuesHolder valuesHolder = mValues[0];
String oldName = valuesHolder.getPropertyName();
valuesHolder.setPropertyName(propertyName);
mValuesMap.remove(oldName);
mValuesMap.put(propertyName, valuesHolder);
}
mPropertyName = propertyName;
// New property/values/target should cause re-initialization prior to starting
mInitialized = false;
}

mValues是一个PropertyValuesHolder数组,PropertyValuesHolder持有动画的属性名和属性值信息,mValuesMap是一个hashmap数组,用来管理PropertyValuesHolder对象,在调用getAnimatedValue(String)方法的时候,这个map通过属性名去查找动画执行的数值。当mValues不为空的时候,将属性名信息放入mValuesMap。

//ObjectAnimator
@Override
public void setFloatValues(float... values) {
if (mValues == null || mValues.length == 0) {
// No values yet - this animator is being constructed piecemeal. Init the values with
// whatever the current propertyName is
if (mProperty != null) {
setValues(PropertyValuesHolder.ofFloat(mProperty, values));
} else {
setValues(PropertyValuesHolder.ofFloat(mPropertyName, values));
}
} else {
super.setFloatValues(values);
}
}

mValues为null或者数组元素个数为0的时候,调用其父类ValueAnimator的setValues()方法,在setValues()内执行了初始化mValues和mValuesMap的操作,并将PropertyValuesHolder放入mValuesMap。当mValues不为null且元素个数不为0的时候,调用其父类ValueAnimator的setFloatValues()方法,在setFloatValues()方法内满足条件又会调用到PropertyValuesHolder的setFloatValues()方法。

//PropertyValuesHolder
public void setFloatValues(float... values) {
mValueType = float.class;
mKeyframes = KeyframeSet.ofFloat(values);
}

这里的mValueType指的是提供的值的类型,mKeyframes是定义这个动画的关键帧集合。

//KeyframeSet
public static KeyframeSet ofFloat(float... values) {
boolean badValue = false;
int numKeyframes = values.length;
FloatKeyframe keyframes[] = new FloatKeyframe[Math.max(numKeyframes,2)];
if (numKeyframes == 1) {
keyframes[0] = (FloatKeyframe) Keyframe.ofFloat(0f);
keyframes[1] = (FloatKeyframe) Keyframe.ofFloat(1f, values[0]);
if (Float.isNaN(values[0])) {
badValue = true;
}
} else {
keyframes[0] = (FloatKeyframe) Keyframe.ofFloat(0f, values[0]);
for (int i = 1; i < numKeyframes; ++i) {
keyframes[i] =
(FloatKeyframe) Keyframe.ofFloat((float) i / (numKeyframes - 1), values[i]);
if (Float.isNaN(values[i])) {
badValue = true;
}
}
}
if (badValue) {
Log.w("Animator", "Bad value (NaN) in float animator");
}
return new FloatKeyframeSet(keyframes);
}

在这个方法内新建了一个FloatKeyframe数组,数组的元素至少为2个,FloatKeyframe是Keyframe的内部子类,持有这个动画的时间值对,Keyframe类被ValueAnimator用来定义整个动画过程中动画目标的数值,当时间从一帧到另一帧,目标对象的值也会从上一帧的值运动到下一帧的值。

/**
* fraction:取值范围0到1之间,表示全部动画时长中已经执行的时间部分
* value:关键帧中与时间相对应的数值
*/
public static Keyframe ofFloat(float fraction, float value) {
return new FloatKeyframe(fraction, value);
}

此方法使用给定的时间和数值创建一个关键帧对象,到这里,属性动画的创建过程基本完成。

3.2 属性动画执行过程

//ObjectAnimator
@Override
public void start() {
AnimationHandler.getInstance().autoCancelBasedOn(this);
if (DBG) {
Log.d(LOG_TAG, "Anim target, duration: " + getTarget() + ", " + getDuration());
for (int i = 0; i < mValues.length; ++i) {
PropertyValuesHolder pvh = mValues[i];
Log.d(LOG_TAG, " Values[" + i + "]: " +
pvh.getPropertyName() + ", " + pvh.mKeyframes.getValue(0) + ", " +
pvh.mKeyframes.getValue(1));
}
}
super.start();
}

在代码中调用objectAnimator.start()的时候动画开始执行,内部调用了其父类ValueAnimator的start()方法。

//ValueAnimator
private void start(boolean playBackwards) {
if (Looper.myLooper() == null) {
throw new AndroidRuntimeException("Animators may only be run on Looper threads");
}
mReversing = playBackwards;
mSelfPulse = !mSuppressSelfPulseRequested;
// Special case: reversing from seek-to-0 should act as if not seeked at all.
if (playBackwards && mSeekFraction != -1 && mSeekFraction != 0) {
if (mRepeatCount == INFINITE) {
// Calculate the fraction of the current iteration.
float fraction = (float) (mSeekFraction - Math.floor(mSeekFraction));
mSeekFraction = 1 - fraction;
} else {
mSeekFraction = 1 + mRepeatCount - mSeekFraction;
}
}
mStarted = true;
mPaused = false;
mRunning = false;
mAnimationEndRequested = false;
// Resets mLastFrameTime when start() is called, so that if the animation was running,
// calling start() would put the animation in the
// started-but-not-yet-reached-the-first-frame phase.
mLastFrameTime = -1;
mFirstFrameTime = -1;
mStartTime = -1;
addAnimationCallback(0);

if (mStartDelay == 0 || mSeekFraction >= 0 || mReversing) {
// If there's no start delay, init the animation and notify start listeners right away
// to be consistent with the previous behavior. Otherwise, postpone this until the first
// frame after the start delay.
startAnimation();
if (mSeekFraction == -1) {
// No seek, start at play time 0. Note that the reason we are not using fraction 0
// is because for animations with 0 duration, we want to be consistent with pre-N
// behavior: skip to the final value immediately.
setCurrentPlayTime(0);
} else {
setCurrentFraction(mSeekFraction);
}
}
}

在这个方法内进行了一些赋值操作,addAnimationCallback(0)和startAnimation()是比较重要的操作。

//ValueAnimator
private void addAnimationCallback(long delay) {
if (!mSelfPulse) {
return;
}
getAnimationHandler().addAnimationFrameCallback(this, delay);
}

这个方法内执行了AnimationHandler的addAnimationFrameCallback()方法注册回调,我们继续看看addAnimationFrameCallback()方法。

//AnimationHandler
public void addAnimationFrameCallback(final AnimationFrameCallback callback, long delay) {
if (mAnimationCallbacks.size() == 0) {
getProvider().postFrameCallback(mFrameCallback);
}
if (!mAnimationCallbacks.contains(callback)) {
mAnimationCallbacks.add(callback);
}

if (delay > 0) {
mDelayedCallbackStartTime.put(callback, (SystemClock.uptimeMillis() + delay));
}
}

这个方法添加了一个AnimationFrameCallback回调,AnimationFrameCallback是AnimationHandler的一个内部接口,其中有两个重要的方法doAnimationFrame()和commitAnimationFrame()。

//AnimationHandler
interface AnimationFrameCallback {
boolean doAnimationFrame(long frameTime);

void commitAnimationFrame(long frameTime);
}

AnimationFrameCallback是可以收到动画执行时间和帧提交时间通知的回调,内有两个方法,doAnimationFrame()和commitAnimationFrame()。

//AnimationHandler
private class MyFrameCallbackProvider implements AnimationFrameCallbackProvider {

final Choreographer mChoreographer = Choreographer.getInstance();

@Override
public void postFrameCallback(Choreographer.FrameCallback callback) {
mChoreographer.postFrameCallback(callback);
}

@Override
public void postCommitCallback(Runnable runnable) {
mChoreographer.postCallback(Choreographer.CALLBACK_COMMIT, runnable, null);
}

@Override
public long getFrameTime() {
return mChoreographer.getFrameTime();
}

@Override
public long getFrameDelay() {
return Choreographer.getFrameDelay();
}

@Override
public void setFrameDelay(long delay) {
Choreographer.setFrameDelay(delay);
}
}

前面的getProvider()方法获得了MyFrameCallbackProvider的一个实例,MyFrameCallbackProvider是AnimationHandler的一个内部类,实现了AnimationFrameCallbackProvider接口,使用Choreographer作为计时脉冲的提供者,去发送帧回调。Choreographer从显示器子系统获得时间脉冲,postFrameCallback()方法发送帧回调。

//AnimationHandler
public interface AnimationFrameCallbackProvider {
void postFrameCallback(Choreographer.FrameCallback callback);
void postCommitCallback(Runnable runnable);
long getFrameTime();
long getFrameDelay();
void setFrameDelay(long delay);
}
//AnimationHandler
private final Choreographer.FrameCallback mFrameCallback = new Choreographer.FrameCallback() {
@Override
public void doFrame(long frameTimeNanos) {
doAnimationFrame(getProvider().getFrameTime());
if (mAnimationCallbacks.size() > 0) {
getProvider().postFrameCallback(this);
}
}
};

在这个回调内执行了doAnimationFrame()方法,如果mAnimationCallbacks的个数大于0,AnimationFrameCallbackProvider就继续发送帧回调,继续重复执行doAnimationFrame()。

//AnimationHandler   
private void doAnimationFrame(long frameTime) {
long currentTime = SystemClock.uptimeMillis();
final int size = mAnimationCallbacks.size();
for (int i = 0; i < size; i++) {
final AnimationFrameCallback callback = mAnimationCallbacks.get(i);
if (callback == null) {
continue;
}
if (isCallbackDue(callback, currentTime)) {
callback.doAnimationFrame(frameTime);
if (mCommitCallbacks.contains(callback)) {
getProvider().postCommitCallback(new Runnable() {
@Override
public void run() {
commitAnimationFrame(callback, getProvider().getFrameTime());
}
});
}
}
}
cleanUpList();
}

在这个方法内开启了一个循环,里面执行了callback.doAnimationFrame(),这个操作会触发ValueAnimator类中的doAnimationFrame()。

//ValueAnimator
private void startAnimation() {
if (Trace.isTagEnabled(Trace.TRACE_TAG_VIEW)) {
Trace.asyncTraceBegin(Trace.TRACE_TAG_VIEW, getNameForTrace(),
System.identityHashCode(this));
}

mAnimationEndRequested = false;
initAnimation();
mRunning = true;
if (mSeekFraction >= 0) {
mOverallFraction = mSeekFraction;
} else {
mOverallFraction = 0f;
}
if (mListeners != null) {
notifyStartListeners();
}
}

startAnimation()方法内调用了initAnimation()初始化动画。

//ValueAnimator
public final boolean doAnimationFrame(long frameTime) {
//省略部分代码
...
final long currentTime = Math.max(frameTime, mStartTime);
boolean finished = animateBasedOnTime(currentTime);

if (finished) {
endAnimation();
}
return finished;
}

这个方法在执行动画的过程中会被多次调用,其中重要的操作是animateBasedOnTime(currentTime)。

//ValueAnimator
boolean animateBasedOnTime(long currentTime) {
boolean done = false;
if (mRunning) {
final long scaledDuration = getScaledDuration();
final float fraction = scaledDuration > 0 ?
(float)(currentTime - mStartTime) / scaledDuration : 1f;
final float lastFraction = mOverallFraction;
final boolean newIteration = (int) fraction > (int) lastFraction;
final boolean lastIterationFinished = (fraction >= mRepeatCount + 1) &&
(mRepeatCount != INFINITE);
if (scaledDuration == 0) {
// 0 duration animator, ignore the repeat count and skip to the end
done = true;
} else if (newIteration && !lastIterationFinished) {
// Time to repeat
if (mListeners != null) {
int numListeners = mListeners.size();
for (int i = 0; i < numListeners; ++i) {
mListeners.get(i).onAnimationRepeat(this);
}
}
} else if (lastIterationFinished) {
done = true;
}
mOverallFraction = clampFraction(fraction);
float currentIterationFraction = getCurrentIterationFraction(
mOverallFraction, mReversing);
animateValue(currentIterationFraction);
}
return done;
}

animateBasedOnTime()方法计算了已经执行的动画时长和动画分数,并调用animateValue()方法计算动画值。

//ValueAnimator
void animateValue(float fraction) {
fraction = mInterpolator.getInterpolation(fraction);
mCurrentFraction = fraction;
int numValues = mValues.length;
for (int i = 0; i < numValues; ++i) {
mValues[i].calculateValue(fraction);
}
if (mUpdateListeners != null) {
int numListeners = mUpdateListeners.size();
for (int i = 0; i < numListeners; ++i) {
mUpdateListeners.get(i).onAnimationUpdate(this);
}
}
}

ValueAnimator的animateValue()方法内部首先根据动画分数得到插值分数,再根据插值分数计算动画值,并调用了AnimatorUpdateListener的onAnimationUpdate()方法通知更新。

//ObjectAnimator
@Override
void animateValue(float fraction) {
final Object target = getTarget();
if (mTarget != null && target == null) {
// We lost the target reference, cancel and clean up. Note: we allow null target if the
/// target has never been set.
cancel();
return;
}

super.animateValue(fraction);
int numValues = mValues.length;
for (int i = 0; i < numValues; ++i) {
mValues[i].setAnimatedValue(target);
}
}

ObjectAnimator的animateValue()方法不仅调用了父类的animateValue()方法,还在循环内调用了PropertyValuesHolder的setAnimatedValue()方法,传入的参数是产生动画效果的目标对象。

//PropertyValuesHolder
@Override
void setAnimatedValue(Object target) {
if (mFloatProperty != null) {
mFloatProperty.setValue(target, mFloatAnimatedValue);
return;
}
if (mProperty != null) {
mProperty.set(target, mFloatAnimatedValue);
return;
}
if (mJniSetter != 0) {
nCallFloatMethod(target, mJniSetter, mFloatAnimatedValue);
return;
}
if (mSetter != null) {
try {
mTmpValueArray[0] = mFloatAnimatedValue;
mSetter.invoke(target, mTmpValueArray);
} catch (InvocationTargetException e) {
Log.e("PropertyValuesHolder", e.toString());
} catch (IllegalAccessException e) {
Log.e("PropertyValuesHolder", e.toString());
}
}
}

在PropertyValuesHolder的setAnimatedValue()方法内部,先通过JNI去修改目标对象的属性值,如果通过JNI找不到对应的方法,则通过使用反射机制修改目标对象的属性值。

4.总结

属性动画的功能相当强大,可以为视图对象和非视图对象添加动画效果,属性动画是通过改变要添加动画的目标对象的属性值实现的,ValueAnimator基于动画时长和已经执行的时长计算得出动画分数,然后根据设置的时间插值器TimeInterpolator计算得出动画的插值分数,再调用对应的估值器TypeEvaluator根据插值分数、起始值和结束值计算得出对象的属性值,ObjectAnimator类在计算出动画的新值后自动地更新对象的属性值,ValueAnimator类则需要手动地去设置对象的属性值。


作者:QiShare
链接:https://juejin.cn/post/7036986935545430030
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

收起阅读 »

桌面上的Flutter:Electron又多了个对手

从本质上看,Flutter 是一个独立的二进制可执行文件。它不仅改变了移动设备的玩法,在桌面设备上也同样不可小觑。一次编写,可在 Android、iOS、Windows、Mac 和 Linux 上进行原生部署,并通过 AngularDart 将所有的业务逻辑共...
继续阅读 »

从本质上看,Flutter 是一个独立的二进制可执行文件。它不仅改变了移动设备的玩法,在桌面设备上也同样不可小觑。一次编写,可在 Android、iOS、Windows、Mac 和 Linux 上进行原生部署,并通过 AngularDart 将所有的业务逻辑共享到 Web 上,这也是它的一大特点。


原生桌面客户端加速移动开发

在进入真实的原生桌面应用程序之前,先让我们看看在桌面上运行的 Flutter 可以为开发移动设备的人们带来哪些好处。


 启动时间


首先是启动 Android 模拟器和运行 Gradle。


下面的动图记录了模拟器冷启动并运行默认的 Flutter 应用程序。我只截取了其中的 2 分 40 秒,可以看出来在那段时间内可以发生很多事情。



但如果我告诉你,你可以在不到 10 秒的时间内启动并运行应用程序,你会怎么想?


运行原生应用程序可以省去启动 Android 模拟器和运行 Gradle 的全部开销。


看看这个:



请注意,你不必离开 IntelliJ。我们开发了将 Flutter 作为原生应用程序所需的工具,它适用于所有的 Flutter IDE。


 在运行时调整大小

与其他应用程序一样,你需要测试不同大小的布局,那么你需要做些什么?


你要求你的朋友使用不同的手机或者创建一组模拟器,以确保你的布局在每台设备上都是正常的。


这对我来说是个麻烦事。我们能更简单一点吗?


可以!



 使用 PC 上的资源

在开发和测试需要与手机上的资源发生交互的应用程序时,首先需要将所有测试文件移动到模拟器或设备上,这样可能会非常烦人。


如果只需要使用原生文件选择器来选择你想要的文件会不会更好?



 热重载和调试

热重载和调试功能是每个高效率工程师所必须的。



 内存占用

对于使用笔记本电脑或配置不太好的电脑的人来说,内存是非常重要的。


Android 模拟器占用大约 1GB 的内存。现在想象一下,为了测试一个聊天应用程序或类似的程序,需要启动 IntelliJ 和狂吃内存的 Chrome。



因为嵌入器是以原生的方式运行,所以不需要 Android 模拟器。这使它的内存占用变得更小。



原生桌面应用

只是在桌面上运行一个 Flutter 应用程序对于可立即发布的成熟桌面应用程序来说是远远不够的。这样做感觉上就像在桌面上运行移动应用程序。


少了什么东西?很多!


悬停、光标变化、滚轮交互,等等。


我们设法在不改变任何平台代码的情况下实现这些功能——它是一个独立的软件包,可以被包含在任何普通的 Flutter 应用程序中。但是,当与桌面嵌入器一起使用时,奇迹就发生了!



这是在 Android 模拟器运行完全相同的代码的结果。



同时开发 Android 和桌面应用程序。



桌面小部件展示

悬停:



光标:



开发一个真正的跨平台应用——包括桌面
 小部件

你创建的大多数小部件都是普遍可用的,如按钮、加载指标器等。


那些需要根据平台呈现不同外观的小部件可以通过 TargetPlatform 属性进行封装,非常容易。


像 CursorWidget 这样的小部件也可以被包含在 Android 版本中。


 页面

根据平台和屏幕尺寸的不同,页面也会有很大差异。不过它们大多只是布局不同,而不是功能差异。


使用 PageLayoutWidget 可以轻松地为每个平台创建准确的布局。



默认情况下对平板电脑也提供了很好的支持。


 插件

使用同时支持桌面嵌入器的插件时,不需要修改 Flutter 代码。


 代码什么时候发布?

很快。不过这个项目仍然处于测试阶段,在不久的将来很可能会发生一些变化。


我们的目标是在不久的将来发布易于安装、设置和使用的产品。


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

高效开发:分享 `extension` 有趣的用法

前言 extension 可以在不更改类或创建子类的情况下,向类添加扩展功能的一种方式。灵活使用 extension 对基础类进行扩展,对开发效率有显著提升。 举个栗子🌰,对 int 类型扩展 小轰在开发项目中碰到需求:将单位为分的数值转换成单位为元的字符串 ...
继续阅读 »

前言


extension 可以在不更改类或创建子类的情况下,向类添加扩展功能的一种方式。灵活使用 extension 对基础类进行扩展,对开发效率有显著提升。


举个栗子🌰,对 int 类型扩展


小轰在开发项目中碰到需求:将单位为分的数值转换成单位为元的字符串


/// 通常的写法,封装转换方法

///封装方法:金额转字符串 保留两位小数
String convertPointToUnit(int num){
return (num.toDouble() / 100).toStringAsFixed(2);
}

///使用
void main(){
int num = 100;
var result = convertPointToUnit(num);
print(result); //打印结果为 1.00
}

同样的功能,使用 extension 进行开发,会更加简洁,如下:


/// 使用 extension 对 int 类进行扩展,添加方法 moneyString
extension ExInt on int {
/// 金额转字符串 保留两位小数
/// 100 => 1.00
String get moneyString => (this.toDouble() / 100).toStringAsFixed(2);
}

import ../ExInt.dart;
///使用
void main(){
int num = 100;
print(num.moneyString);
}

扩展后,直接作为该类型的成员方法来被使用。extension 就像是基因赋值,直接将能力(方法)对宿主进行赠与。


各种场景的扩展演示



  • 对枚举进行扩展实现


enum FruitEnum { apple, banana }

extension ExFruitEnum on FruitEnum {
String get name {
switch (this) {
case FruitEnum.apple:
return "apple";
case FruitEnum.banana:
return "banana";
}
}
}

///字符串匹配枚举
FruitEnum generateFruit (String fruitType){
if(fruitType == FruitEnum.apple.name){
return FruitEnum.apple;
} else if(fruitType == FruitEnum.banana.name){
return FruitEnum.banana;
}
}


  • 扩展作用于泛型:


//扩展list的方法
extension ExList<T> on List<T> {
//扩展操作符
List<T> operator -() => reversed.toList();
//一个链表分割成两个
List<List<T>> split(int at) => <List<T>>[sublist(0, at), sublist(at)];
}


  • 扩展在 Widget 控件中的应用


我们会有类似的控件


Column(
children: <Widget>[
Container(
paddint: const EdgeInsets.all(10)
child: AWidget(),
),
Container(
paddint: const EdgeInsets.all(10)
child: BWidget(),
),
Container(
paddint: const EdgeInsets.all(10)
child: CWidget(),
),
]
)

代码中有很多的冗余对吧?我们用 extension 进行扩展一下:


extension ExWidget on Widget {
Widget paddingAll(double padding) {
return Container(
paddint: const EdgeInsets.all(padding)
child: this,
);
}
}

之后我们就可以改成:


Column(
children: <Widget>[
AWidget().paddingAll(10),
BWidget().paddingAll(10),
CWidget().paddingAll(10),
]
)

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

Android程序员如何从设计角度思考HTTPS

typora-root-url: img typora-copy-images-to: img 从设计角度思考HTTPS 我们了解了HTTP协议的内容后,明白HTTP存在很多安全隐患,所以后来推出了安全协议-HTTPS,我们不妨站在设计角度来设计一个安全的HT...
继续阅读 »
typora-root-url: img
typora-copy-images-to: img

从设计角度思考HTTPS


我们了解了HTTP协议的内容后,明白HTTP存在很多安全隐患,所以后来推出了安全协议-HTTPS,我们不妨站在设计角度来设计一个安全的HTTP连接协议,看看HTTP存在哪些问题,我们该如何设计能保证安全性,从而了解HTTPS的安全协议是如何保障的HTTP安全

首先我们需要考虑一下,实现HTTPS所谓的安全,我们需要保证那些地方的安全:


1.首先我们需要保证服务端和客户端之间发送的消息是安全的


2.其次我们要保证服务端和客户端之间的连接是安全的


3.最后我们还要保证服务端不会被其他的伪造客户端连接,并且通过此方式破解加密方式


服务端/客户端信息交互的安全


首先我们先来考虑一下,有什么方法可以保证客户端发送的消息给服务端并且服务端返回结果,这个过程是安全的,大概的过程如下:


image.png


这个时候我们最先想到的方案--加密,我们使用加密算法给数据加密了不就行了吗,那么该选择什么加密算法呢?开发过程中最常见的加密算法如:MD5、SHA1这样的摘要算法或者aes、des这样的对称加密算法,又或者rsa这样的非对称算法,看起来每一种都可以实现数据加密传输,但是我们不要忘记了,第三点中我们希望能保证其他客户端连接不会破解此种加密方式,要知道在互联网中,不是仅仅一台客户端和一个服务端交互,可以有无数台客户端同时与服务端交互,这个时候我们如果要防止别的客户端破解加密,看起来摘要算法这种不可逆的算法刚好合适,但是我们不要忘记了,客户端和服务端需要完成交互的,那么也就是说这个加密不能为不可逆算法,否则客户端也无法对服务端数据进行处理,服务端也无法处理客户端数据,那么只能是对称加密算法或者非对称加密算法能满足了,我们继续思考,如果是多台客户端同时连接服务端,如下图:


image.png


那么似乎哪一种加密都能满足,那么我们不禁有个问题,万一有黑客(恶意机器)拦截了我们的请求,并且充当了中间的传输者,我们的这两种加密算法还安全吗?如下图:


image.png


可以看到,我们的客户端和服务端中间被未知的恶意机器拦截转发了请求,那么我们之前的加密方式如果是直接传递的加密方式和密钥,如果是对称加密那么结局可想而知,对于中间机器来说,依然可以解密出客户端和服务端的消息,对于黑客来说依然是透明的,安全性仅仅比不加密强上一点点,完全不可以称之为可信任的安全协议,那么使用非对称加密呢?我们都知道非对称加密是一堆密钥,每一端持有自己的私钥,对外公开公钥,而公钥加密仅仅使用私钥才可以解密,这样即使有中间机器拦截,也仅仅能拿到客户端和服务端的公钥,但是我们不要忘记了,客户端应该是持有服务端的公钥,用公钥加密传输给服务端,服务端私钥解密,响应的过程即是客户端的私钥解密服务端持有的客户端公钥,中间机器即使拦截了双方的公钥,也无法解密双方公钥自身加密的信息,这样的话,客户端和服务端数据传输安全的问题似乎完美解决了


新隐患-公钥传输方式


刚刚我们经过对比,确定了使用公私钥方式的非对称加密来作为客户端-服务端传输的加密方式,看起来应该高枕无忧了,那么事实真的如此吗?其实和对称加密一样,非对称加密这样直接传输加密,也仅仅是提高了一点点安全性而已,如果遇到的黑客在拦截到客户端的请求后,将自身的公钥传递给服务端以及客户端,而将客户端/服务端的公钥持有会如何?是的,细极思恐,那样中间机器将拥有解密双端消息的能力!为什么会这样?试想一下,客户端使用所谓服务端的公钥加密消息,发送,被中间机器拦截后,这所谓的服务端公钥是中间机器的,那么私钥岂不是可以解密拿到明文信息?然后再伪装使用拦截到的真实的客户端的公钥加密,转发给服务端,同理,服务端的所谓客户端公钥加密对于中间机器完全形同虚设,那么这种问题如何解决呢?我们可不可以更换一种公钥传输方式,尽量绕开中间机器的拦截,保证安全性呢?


我们可以想下,大概有如下两种方法传输公钥:


1.服务端把公钥发送给每一个连接进来的客户端


2.将公钥放到一个地方(比如独立的服务器,或者文件系统),客户端需要获取公钥的时候,访问这个地方的公钥来和服务端进行匹配


而第一个方案,即我们刚刚推翻的方案,很明显会存在被拦截调包的可能,那么似乎我们只能使用第二个方案来传输公钥?那么我们不禁有个问题,即客户端是如何知道存放公钥的远程服务器地址以及认证加密方式,而且每次建立连接都要来获取一次,对服务器的抗压能力也有一定的考验?还有如何保证黑客等恶意访问的用户不能通过此种方式拿到公钥,所以安全也是个比较麻烦的问题


引入第三方CA机构


由于上述提到的问题,所以对于个人而言,如果在开发网站的同时,还要再花费大量金钱和精力在开发公钥服务上,是很不合理的,那么有木有专门做这个的公司,我们托管给这个公司帮我们完成,只需要付出金钱的代价就能体验到服务不可以吗?于是,专门负责证书认证的第三方CA机构出现了,我们只需要提前申请好对应的服务端信息,并且提交对应资料,付出报酬,CA就会给我们提供对应的服务端认证服务,大大减少我们的操作和复杂度,但是这个时候我们不禁又有个问题,CA机构能保证只有客户端拿到认证的证书,并且认证通过,拦截对应的非正常客户端吗?如果不能的话,那岂不是黑客也可以拿到认证?现在的问题开始朝着如何认证用户真伪方向发展了


验证证书有效性


其实想要解决认证的问题,我们可以从生活中寻找一些灵感,我们每个人都有一个唯一的id,证明身份,这样可以保证识别出id和对应的人,也能识别不法分子,那么,既然计算机来源于生活,设计出来的东西也应该遵循正常的逻辑,我们何不给每个证书设置类似id的唯一编号呢?当然计算机是死的,没办法简单的将机器和证书编号进行绑定,那么就需要设计一个符合逻辑的证书验证过程。我们不妨思考下,平时开发的软件为了识别被人篡改的软件,我们是如何做的,相信大脑里有个词会一闪而过,MD5/SHA1(签名)?没错,那么我们证书的认证可否按照这个思路设计?


现在我们假设,客户端拿到证书后,能够从证书上拿到公钥信息、证书签名hash和有效期等信息,也就是说证书内置了计算整个证书的签名hash值,如果此时我们根据客户端的签名算法进行一次加签计算,和证书默认计算好的hash比较,发现不一致,那么就说明证书被修改了,肯定不是第三方发布的正式证书,如果一致,说明证书是真实的,没有被篡改,我们可以尝试与服务端连接了,因为证书拿到了,也有了公钥,后续的就是加密通信的过程了


至此,似乎一个安全的加密https简陋的设计出来了,也似乎解决了这些安全问题,但是不得不提的一点是,我们上面有个很重要的一点,即存放证书的服务器一定要保证安全性,第三方机构算不算绝对安全呢?答案是否定的,因为在https至今的历史上,发生过第三方机构被黑客攻击成功,黑客使用的也是正版的证书的事件,只能说计算机的世界不存在绝对安全,而是相对来说,安全系数提高了太多


HTTPS认证过程


前面我们设计了简陋版的HTTPS,那么,我们接下来看看,正版的HTTPS大体认证过程是如何的,首先我们从申请证书开始:


image.png


可以看到,申请证书的时候,需要提供很多内容,其中域名、签名hash算法、加密算法是最重要的,通过这三项计算生成证书以及确定加密认证算法,并且在这个过程中还需要提供服务端自己的公钥,用来生成证书,CA机构使用自己的私钥加密证书,生成证书传递给服务端


2.证书申请拿到以后,客户端tcp三次握手连接(会携带一个随机数client-random),这个时候服务端将证书信息(包含过期时间、签名算法、当前证书的hash签名、服务端公钥、颁发证书机构等信息)传递给客户端,并且传递一个random随机数


3.客户端收到证书后,使用浏览器内置的CA认证,对证书的颁发机构逐个/逐层校验,确定证书来源正常,并且校验证书过期时间,确定是否可用,最后根据证书的签名算法,计算出对应的签名hash,和证书内置的签名hash比较,确定是否是未篡改的证书,完全认证通过后,证书认证环节结束


4.客户端生成随机对称密钥( pre-master ),将双端随机数组合通过证书的公钥(服务端的公钥)加密后,发送给服务端,服务端收到后,根据双方生成的随机数组合验证击进行http通信


以上就是HTTPS认证的大体流程,另外需要注意的是,HTTPS使用了签名算法(MD5/SHA256等)、对称加密以及非对称加密完成了整个交互过程,在认证过程中仅仅使用了签名算法和非对称加密保证建立通道的安全稳定,在通道建立过程中,维持了一个sessionid,用来防止频繁创建通道大量消耗资源,尽可能保证通道长期连接复用,并且我们需要知道一点,非对称加密虽然安全,但是相比较对称加密,加密解密步骤复杂导致时间会更久,所以HTTPS在建立通道以后,会选择双端协议使用对称加密来完成后续的数据交互,而上述提到的双方的随机对称密钥组合是用来在建立连接后的第一次交互的过程中,二次确认握手过程是否被篡改(客户端把Client.random + sever.random + pre-master组合后使用公钥加密,并且把握手消息根据证书的签名算法计算hash,发送给服务端确认握手过程是否被窜改),完成校验后,确定当前是安全连接后,双端之间就会使用约定好的对称加密算法进行数据加密解密传输,至此一个完整的HTTPS协议完成


相关视频


Android程序员中高级进阶学习/OkHttp原理分析


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

做一个短链接系统需要考虑这么多

什么是短链接短链接顾名思义,就是一个比较短的链接(我好像说了个废话),我们平时看到的链接可能长这样:mp.weixin.qq.com/s?biz=MzU5M…又臭又长有没有(没错,这是我的WX公众号链接,可以关注一下),那如果我们需要将某个链接发在某个文章或者...
继续阅读 »



什么是短链接

短链接顾名思义,就是一个比较短的链接(我好像说了个废话),我们平时看到的链接可能长这样:

mp.weixin.qq.com/s?biz=MzU5M…

又臭又长有没有(没错,这是我的WX公众号链接,可以关注一下),那如果我们需要将某个链接发在某个文章或者推广给别人的时候,这么长看着也太不爽了,而短链接的出现就是用一个很短的URL来替代这个很长的家伙,当用户访问短链接的时候,会重定向到原来的链接。比如长下面这样:

sourl.cn/CsTkky

你如果平时有注意的话,各种商业短信上的链接也是会转成特别短的:

img

这个特别短的URL就是短链接。

为什么需要URL短链接

URL短链接用于为长URL创建较短的别名,我们称这些缩短的别名为“短链接”;当用户点击这些短链接时,会被重定向到原始URL;短链接在显示、打印、发送消息时可节省大量空间。

例如,如果我们通过sourl缩短以下URL:

juejin.cn/user/211951…

我们可以得到一个短链接:

sourl.cn/R99fbj

缩短的URL几乎是实际URL大小的三分之一。

URL缩写经常用于优化设备之间的链接,跟踪单个链接以分析受众,衡量广告活动的表现,或隐藏关联的原始URL。

如果你以前没有使用过sourl,可以尝试创建一个新的URL短链接,并花一些时间浏览一下他们的服务提供的各种选项。可以让你更好的理解这篇文章。

系统的要求和目标

在完成一个功能或者开发一个系统时,先确定系统的定位和要达到的目标是一个好的习惯,这可以让你在设计和开发过程中有更清晰的思路。

我们的短链接系统应满足以下要求:

功能要求:

  • 给定一个URL,我们的服务应该为其生成一个较短且唯一的别名,这叫做短链接,此链接应足够短,以便于复制和粘贴到应用程序中;

  • 当用户访问短链接时,我们的服务应该将他们重定向到原始链接;

  • 用户应该能够选择性地为他们的URL选择一个自定义的短链接;

  • 链接可以在指定时间跨度之后过期,用户应该能够指定过期时间。

非功能要求:

  • 系统必须高度可用。如果我们的服务关闭,所有URL重定向都将开始失败。

  • URL重定向的延迟情况应该足够小;

  • 短链接应该是不可猜测的。

扩展要求:

  • 支持分析和统计,例如短链接的访问次数;

  • 其他服务也应该可以通过RESTAPI访问我们的服务。

容量要求和限制

我们的系统将会有很大的访问量。会有对短链接的读取请求和创建短链接的写入请求。假设读写比例为100:1。

访问量预估:

假设我们每个月有5亿个新增短链接,读写比为100:1,我们可以预计在同一时间内有500亿重定向:

100 * 5亿 => 500亿

我们系统的QPS(每秒查询数量)是多少?每秒的新短链接为:

5亿/ (30天 * 24小时 * 3600 秒) ≈ 200 URLs/s

考虑到100:1读写比,每秒URL重定向将为:

100 * 200 URLs/s = 20000/s

存储预估:

假设我们将每个URL缩短请求(以及相关的缩短链接)存储5年。由于我们预计每个月将有5亿个新URL,因此我们预计存储的对象总数将为300亿:

5亿 * 5 年 * 12 月 = 300亿 

假设每个存储的对象大约有500个字节(这只是一个估算值)。我们将需要15TB的总存储:

300亿*500bytes≈15TB

带宽预估:

对于写请求,由于我们预计每秒有200个新的短链接创建,因此我们服务的总传入数据为每秒100KB:

200*500bytes≈100KB/s

对于读请求,预计每秒约有20,000个URL重定向,因此我们服务的总传出数据将为每秒10MB:

20000 * 500 bytes ≈10 MB/s

内存预估:

对于一些热门访问的URL为了提高访问速率,我们需要进行缓存,需要多少内存来存储它们?如果我们遵循二八原则,即20%的URL产生80%的流量,我们希望缓存这20%的热门URL。

由于我们每秒有20,000个请求,因此我们每天将收到17亿个请求:

20000 * 24 * 3600 ≈ 17亿

要缓存这些请求中的20%,我们需要170 GB的内存:

17亿 * 0.2 * 500bytes ≈ 170GB

这里需要注意的一件事是,由于将会有许多来自相同URL的重复请求,因此我们的实际内存使用量可能达不到170 GB。

整体来说,假设每月新增5亿个URL,读写比为100:1,我们的预估数据大概是下面这样:

类型预估数值
新增短链接200/s
短链接重定向20000/s
传入数据100KB/s
传出数据10 MB/s
存储5年容量15 TB
内存缓存容量170 GB

系统API设计

一旦我们最终确定了需求,就可以定义系统的API了,这里则是要明确定义我们的系统能提供什么服务。

我们可以使用REST API来公开我们服务的功能。以下是用于创建和删除URL的API的定义:

创建短链接接口

String createURL(api_dev_key, original_url, custom_alias=None, user_name=None, expire_date=None)
复制代码

参数列表:

api_dev_key:分配给注册用户的开发者密钥,可以根据该值对用户的创建短链接数量进行限制;

original_url:需要生成短链接的原始URL;

custom_alias :用户对于URL自定义的名称;

user_name :可以用在编码中的用户名;

expire_date :短链接的过期时间;

返回值:

成功生成短链接将返回短链接URL;否则,将返回错误代码。

删除短链接接口

String deleteURL(api_dev_key, url_key)
复制代码

其中url_key是表示要删除的短链接字符串;成功删除将返回delete success

如何发现和防止短链接被滥用?

恶意用户可以通过使用当前设计中的所有URL密钥来对我们进行攻击。为了防止滥用,我们可以通过用户的api_dev_key来限制用户。每个api_dev_key可以限制为每段时间创建一定数量的URL和重定向(可以根据开发者密钥设置不同的持续时间)。

数据模型设计

在开发之前完成数据模型的设计将有助于理解各个组件之间的数据流。

在我们短链接服务系统中的数据,存在以下特点:

  • 需要存储十亿条数据记录;

  • 存储的每个对象都很小(小于1K);

  • 除了存储哪个用户创建了URL之外,记录之间没有任何关系;

  • 我们的服务会有大量的读取请求。

我们需要创建两张表,一张用于存储短链接数据,一张用于存储用户数据;

img

应该使用怎样的数据库?

因为我们预计要存储数十亿行,并且不需要使用对象之间的关系-所以像mongoDB、Cassandra这样的NoSQL存储是更好的选择。选择NoSQL也更容易扩展。

基本系统设计与算法

现在需要解决的问题是如何为给定的URL生成一个简短且唯一的密钥。主要有两种解决方案:

  • 对原URL进行编码

  • 提前离线生成秘钥

对原URL编码

可以计算给定URL的唯一HASH值(例如,MD5或SHA256等)。然后可以对HASH进行编码以供显示。该编码可以是base36([a-z,0-9])base62([A-Z,a-z,0-9]),如果我们加上+/,就可以使用Base64编码。需要考虑的一个问题是短链接的长度应该是多少?6个、8个或10个字符?

使用Base64编码,6个字母的长密钥将产生64^6≈687亿个可能的字符串; 使用Base64编码,8个字母长的密钥将产生64^8≈281万亿个可能的字符串。

按照我们预估的数据,687亿对于我们来说足够了,所以可以选择6个字母。

如果我们使用MD5算法作为我们的HASH函数,它将产生一个128位的HASH值。在Base64编码之后,我们将得到一个超过21个字符的字符串(因为每个Base64字符编码6位HASH值)。

现在我们每个短链接只有6(或8)个字符的空间,那么我们将如何选择我们的密钥呢?

我们可以取前6(或8)个字母作为密钥,但是这样导致链接重复;要解决这个问题,我们可以从编码字符串中选择一些其他字符或交换一些字符。

我们的解决方案有以下问题:

解决办法:

我们可以将递增的序列号附加到每个输入URL以使其唯一,然后生成其散列。不过,我们不需要将此序列号存储在数据库中。此方法可能存在的问题是序列号不断增加会导致溢出。添加递增的序列号也会影响服务的性能。

另一种解决方案可以是将用户ID附加到输入URL。但是,如果用户尚未登录,我们将不得不要求用户选择一个唯一的key。即使这样也有可能有冲突,需要不断生成直到得到唯一的密钥。

离线生成秘钥

可以有一个独立的密钥生成服务,我们就叫它KGS(Key Generation Service),它预先生成随机的六个字母的字符串,并将它们存储在数据库中。每当我们想要生成短链接时,都去KGS获取一个已经生成的密钥并使用。这种方法更简单快捷。我们不仅不需要对URL进行编码,而且也不必担心重复或冲突。KGS将确保插入到数据库中的所有密钥都是唯一的。

会存在并发问题吗?

密钥一旦使用,就应该在数据库中进行标记,以确保不会再次使用。如果有多个服务器同时读取密钥,我们可能会遇到两个或多个服务器尝试从数据库读取相同密钥的情况。如何解决这个并发问题呢?

KGS可以使用两个表来存储密钥:一个用于尚未使用的密钥,一个用于所有已使用的密钥。

一旦KGS将密钥提供给其中一个服务器,它就可以将它们移动到已使用的秘钥表中;可以始终在内存中保留一些密钥,以便在服务器需要时快速提供它们。

为简单起见,一旦KGS将一些密钥加载到内存中,它就可以将它们移动到Used Key表中。这可确保每台服务器都获得唯一的密钥。

如果在将所有加载的密钥分配给某个服务器之前KGS重启或死亡,我们将浪费这些密钥,考虑到我们拥有的秘钥很多,这种情况也可以接受。

还必须确保KGS不将相同的密钥提供给多个服务器,因此,KGS将秘钥加载到内存和将秘钥移动到已使用表的动作需要时同步的,或者加锁,然后才能将秘钥提供给服务器。

KGS是否存在单点故障?

要解决KGS单点故障问题,我们可以使用KGS的备用副本。当主服务器死机时,备用服务器可以接管以生成和提供密钥。

每个应用服务器是否可以换成一些Key?

可以,这样可以减少对KGS的访问,不过,在这种情况下,如果应用服务器在使用所有密钥之前死亡,我们最终将丢失这些密钥。但是因为我们的秘钥数量很多,这点可以接受。

如何完成秘钥查找?

我们可以在数据库中查找密钥以获得完整的URL。如果它存在于数据库中,则向浏览器发回一个“HTTP302 Redirect”状态,将存储的URL传递到请求的Location字段中。如果密钥不在我们系统中,则发出HTTP 404 Not Found状态或将用户重定向回主页。

数据分区和复制

因为我们要存储十亿个URL数据,那么一个数据库节点在存储上可能不满足要求,并且单节点也不能支撑我们读取的要求。

因此,我们需要开发一种分区方案,将数据划分并存储到不同的数据库服务中。

基于范围分区:

我们可以根据短链接的第一个字母将URL存储在不同的分区中。因此,我们将所有以字母'A/a'开头的URL保存在一个分区中,将以字母‘B/b’开头的URL保存在另一个分区中,以此类推。这种方法称为基于范围的分区。我们甚至可以将某些不太频繁出现的字母合并到一个数据库分区中。

基于hash值分区:

在此方案中,我们对要存储的对象进行Hash计算。然后,我们根据Hash结果计算使用哪个分区。在我们的例子中,我们可以使用短链接的Hash值来确定存储数据对象的分区。

Hash函数会将URL随机分配到不同的分区中(例如,Hash函数总是可以将任何‘键’映射到[1…256]之间的一个数字,这个数字将表示我们在其中存储对象的分区。

这种方式有可能导致有些分区数据超载,可以使用一致性哈希算法解决。

缓存

对于频繁访问的热点URL我们可以进行缓存。缓存的方案可以使用现成的解决方案,比如使用memcached,Redis等,因此,应用服务器在查找数据库之前可以快速检查高速缓存是否具有所需的URL。

如果确定缓存容量?

可以从每天20%的流量开始,并根据客户端的使用模式调整所需的缓存服务器数量。如上所述,我们需要170 GB内存来缓存20%的日常流量。可以使用几个较小的服务器来存储所有这些热门URL。

选择哪种淘汰策略?

淘汰策略是指当缓存已满时,如果我们想用更热点的URL替换链接,我们该如何选择?

对于我们的系统来说,最近最少使用(LRU)是一个合理的策略。在此策略下,我们首先丢弃最近最少使用的URL;我们可以使用一个短链接或短链接的HASH值作为key的Hash Map或类似的数据结构来存储URL和访问次数。

如何更新缓存?

每当出现缓存未命中时,我们的服务器都会命中后端数据库。每次发生这种情况,我们都可以更新缓存并将新条目传递给所有缓存副本。每个副本都可以通过添加新条目来更新其缓存。如果副本已经有该条目,它可以简单地忽略它。

负载均衡

可以在系统中的三个位置添加负载均衡层:

  • 在客户端和应用程序服务器之间;

  • 在应用程序服务器和数据库服务器之间;

  • 在应用程序服务器和缓存服务器之间。

可以使用简单的循环调度方法,在后端服务器之间平均分配传入的请求。这种负载均衡方式实现起来很简单,并且不会带来任何开销。此方法的另一个好处是,如果服务器死机,负载均衡可以让其退出轮换,并停止向其发送任何流量。

循环调度的一个问题是没有考虑服务器过载情况。因此,如果服务器过载或速度慢,不会停止向该服务器发送新请求。要处理此问题,可以放置一个更智能的解决方案,定期查询后端服务器的负载并基于此调整流量。

数据清除策略

数据应该永远保留,还是应该被清除?如果达到用户指定的过期时间,短链接应该如何处理?

  • 持续扫描数据库,清除过期数据。

  • 懒惰删除策略

如果我们选择持续查询过期链接来删除,将会给数据库带来很大的压力;可以慢慢删除过期的链接,并进行懒惰的方式清理。服务确保只有过期的链接将被删除,尽管一些过期的链接可以在数据库保存更长时间,但永远不会返回给用户。

  • 每当用户尝试访问过期链接时,我们都可以删除该链接并向用户返回错误;

  • 单独的清理服务可以定期运行,从存储和缓存中删除过期的链接;

  • 此服务应该非常轻量级,并计划仅在预期用户流量较低时运行;

  • 我们可以为每个短链接设置默认的到期时间(例如两年);

  • 删除过期链接后,我们可以将密钥放回KGS的数据库中重复使用。

结语

以上就是开发一个短链接服务系统要做的方方面面,可能还存在一些小黑没有考虑到的地方,欢迎留言区交流!如果对你有一点点帮助,点个赞鼓励一下。

作者:小黑说Java
来源:https://juejin.cn/post/7034325565431611406

收起阅读 »