注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

重谈Handler的内存泄漏

Handler 的内存泄漏问题 在多线程操作中,handler会使用的非常多,但是每次使用handler你有没有考虑内存泄漏的问题。 如果你使用handler进行操作时,你会发现出现以下提示 This Handler class should be stati...
继续阅读 »

Handler 的内存泄漏问题


在多线程操作中,handler会使用的非常多,但是每次使用handler你有没有考虑内存泄漏的问题。


如果你使用handler进行操作时,你会发现出现以下提示
This Handler class should be static or leaks might occur (anonymous android.os.Handler)这样的提示。翻译:
由于此Handler被声明为内部类,因此可能会阻止外部类被垃圾回收。 如果Handler使用Looper或MessageQueue作为主线程以外的线程,则没有问题。 如果Handler正在使用主线程的Looper或MessageQueue,则需要修复Handler声明,如下所示:将Handler声明为静态类; 在外部类中,实例化外部类的WeakReference,并在实例化Handler时将此对象传递给Handler; 使用WeakReference对象对外部类的成员进行所有引用。



警告原因:handler没有设置为静态类,声明内部类可能会阻止被GC回收,从而导致内存泄漏



那么为什么会造成内存泄漏呢。
首先来说下什么是内存泄漏
内存泄漏(Memory Leak):指的是程序已经动态分配的堆内存由于某种原因程序未释放或者无法释放,造成系统资源浪费,会造成程序运行缓慢甚至系统崩溃等严重后果。
问题代码:


public class MainActivity extends AppCompatActivity {
private Handler mHandler = new Handler();
private TextView mTextView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mTextView = (TextView) findViewById(R.id.tv);
//模拟内存泄漏
mHandler.postDelayed(new Runnable() {
@Override
public void run() {
mTextView.setText("yiyi");
}
}, 1000);
}

内存泄漏原因


从上面问题代码,可以看出这里通过内部类方式创建handler,而在java中,非静态内部类会持有外部类的引用,这里的postDelayed是一个延迟处理消息,将一个handler装入到message中,将消息放进消息队列messageQueueLooper进行取消息进行处理。如果此时activity要退出了,想要调用**destroy**销毁,但是此时Looper正在处理消息,**Looper**的生命周期明显比activity长,这将使得activity无法被**GC**回收,最终造成内存泄漏。并且此时handler还持有activity的引用,也是造成内存泄漏的一个原因(不是根本原因)。



但是我觉得真正handler造成内存泄漏的根本原因是生命周期比activity长,比如TextView也是内部类创建的,那么它怎么没有造成内存泄漏,它也持有外部类Activity的引用,根本原因是它的生命周期比Activity短,Activity销毁时候,它可以被GC回收



总结


当handler有没有处理的消息或者正在处理消息,此时Handler的生命周期明显比Activity长,GC持有Activity与handler两者的引用,导致Activity无法被GC回收,造成内存泄漏。而handler是不是内部类,并不是造成内存泄漏的根本原因。


解决方案


静态内部类+弱引用



将Handler的子类设置成 静态内部类,并且可加上 使用WeakReference弱引用持有Activity实例



原因:弱引用的对象拥有短暂的生命周期。而垃圾回收器不管内存是否充足都会回收弱引用对象。


public class HandlerActivity extends AppCompatActivity  {
private static class MyHandler extends Handler {
private final WeakReference<HandlerActivity> mActivity;
public MyHandler(HandlerActivity activity) {
mActivity = new WeakReference<HandlerActivity>(activity);
}

@Override
public void handleMessage(Message msg) {
HandlerActivity activity = mActivity.get();
if (activity != null) {
}
}

private final MyHandler mHandler = new MyHandler(this);
private static final Runnable mRunnable = new Runnable() {
@Override
public void run() { }
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mHandler.postDelayed(mRunnable, 1000 * 60 * 1);
finish();
}
}
复制代码

Activity生命周期结束时,清空消息队列
只需在Activity的onDestroy()方法中调用mHandler.removeCallbacksAndMessages(null);就行了。


@Override
protected void onDestroy() {
super.onDestroy();
if(handler!=null){
handler.removeCallbacksAndMessages(null);
handler = null;
}
}

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

OAuth2.0原理图解:第三方网站为什么可以使用微信登录

1 文章概述假设小明开发了一个A网站,需要支持微信登陆和淘宝账号登陆。如果你是微信或者淘宝开发人员,你会怎么设计这个功能?本文结合淘宝开放平台官方文档以淘宝账号为例。从最简单视角去思考,用户在网站A输入淘宝用户名和密码,网站A调用淘宝接口校验输入信息,校验通过...
继续阅读 »

1 文章概述

假设小明开发了一个A网站,需要支持微信登陆和淘宝账号登陆。如果你是微信或者淘宝开发人员,你会怎么设计这个功能?本文结合淘宝开放平台官方文档以淘宝账号为例。

从最简单视角去思考,用户在网站A输入淘宝用户名和密码,网站A调用淘宝接口校验输入信息,校验通过则登陆成功,整体流程如下图:


01 第三方登陆简单思路.jpg


上述思路存在什么问题?最显著问题就是信息安全问题。问题第一个方面是用户需要将淘宝用户名和密码输入网站A,这样会带来用户名和密码泄露风险。问题第二个方面是如果用户不信任网站A,那么也不会输入淘宝用户名和密码,影响网站A业务开展。


2 OAuth2.0

第三方登陆信息安全问题应该如何解决?OAuth是一种流行标准。如果执行这行这个标准,那么用户可以在不告知A网站淘宝用户名和密码情况下,使用淘宝账号登陆A网站。

目前已经发展到OAuth2.0版本,相较于1.0版本更加关注客户端开发者简易性,而且为桌面应用、web应用、手机设备提供专门认证流程。


2.1 四种角色

OAuth2.0标准定义了四种角色:

  • 客户端(Client)
  • 资源所有者(Resource Owner)
  • 资源服务器(Resource Server)
  • 授权服务器(Authorization Server)

四种角色交互流程:

02 OAuth2_四种角色_01.jpg

本文场景对应四种角色:

02 OAuth2_四种角色_02.jpg


2.2 四种模式

OAuth2.0标准定义了以下四种授权模式:

  • 授权码模式(authorization code)
  • 隐式模式(implicit)
  • 密码模式(password)
  • 客户端模式(client credentials)

四种授权模式中最常用的是授权码模式,例如微信开发平台文档介绍对于网站应用微信OAuth2.0授权登录目前支持授权码模式,所以本文只介绍授权码模式,后续文章会详细比较四种模式。


2.3 实现流程

第一个流程是创建应用,A网站开发者首先去淘宝开放平台创建应用,开放平台会生成一个client_id作为A网站唯一标识。

第二个流程是授权流程,用户在A网站点击使用淘宝账号登陆时,实际上跳转至A网站拼接授权URL页面,这个页面由淘宝提供。用户在授权页面输入淘宝用户名和密码,校验成功后跳转至A网站回调地址,这时A网站会拿到一个code,后台再使用code去获取access_token。

第三个流程是获取信息,获取到access_token相当于获取到一把钥匙,再按照规范调用淘宝对外提供接口就可以获取到用户数据。


03 oauth2_整体流程.jpg


2.4 为什么安全

第一个方面A网站开发人员需要在淘宝开放平台进行申请,需要输入个人信息或者公司信息,这样A网站可靠性有了一定程度保证。

第二个方面在第一章节方案用户需要在A网站输入淘宝用户名和密码,但是在OAuth2.0方案2.4步骤虽然也要输入淘宝用户名密码,但是这个页面由淘宝官方提供,安全性得到了保证。

第三个方面access_token(令牌)并没有在浏览器中传递,而是需要A网站在获取到code之后去后台程序换取,避免了钥匙泄露风险。

第四个方面code(授权码)在浏览器传递有一定风险,但是具有两个特性一定程度保证了安全:

(1) code具有效期,超期未使用需要重新按授权流程获取

(2) code只能使用一次,使用后需要重新按授权流程获取


3 OpenID Connect

3.1 授权与认证

在第二章节详细分析了OAuth2.0协议,在实现流程章节分析了创建应用、授权流程、获取信息三个流程,我们发现一个问题:客户端在获取到令牌之后,还需要调用资源服务器接口获取用户信息,有没有一种协议可以在返回令牌时同时将用户是谁返回呢?回答这个问题之前首先对比一组概念:授权与认证。

授权关注通信实体具有什么权限,认证关注通信实体是谁。OAuth2.0只有授权流程,返回令牌之后授权流程已经完成,OpenID connect在此基础上进行了扩展,这样客户端能够通过认证来识别用户。


3.2 三种角色

OpenID Connect定义了三种角色:

  • 最终用户(End User)
  • 依赖方(Relying Party)
  • 身份认证提供商(Identity Provider)

三种角色交互流程:

04 OIDC_三种角色_01.jpg

本文场景对应三种角色:

04 OIDC_三种角色_02.jpg


3.3 整体流程

05 OIDC_整体流程.jpg


4 相关文档

淘宝开放平台用户授权介绍

网站应用微信登录开发指南


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

Java线程池必知必会

1、线程数使用开发规约阿里巴巴开发手册中关于线程和线程池的使用有如下三条强制规约【强制】创建线程或线程池时请指定有意义的线程名称,方便出错时回溯。正例:自定义线程工厂,并且根据外部特征进行分组,比如,来自同一机房的调用,把机房编号赋值给whatFeatureO...
继续阅读 »

1、线程数使用开发规约

阿里巴巴开发手册中关于线程和线程池的使用有如下三条强制规约

【强制】创建线程或线程池时请指定有意义的线程名称,方便出错时回溯。

正例:自定义线程工厂,并且根据外部特征进行分组,比如,来自同一机房的调用,把机房编号赋值给whatFeatureOfGroup


public class UserThreadFactory implements ThreadFactory {

private final String namePrefix;

private final AtomicInteger nextId = new AtomicInteger(1);

/**

* 定义线程组名称,在利用 jstack 来排查问题时,非常有帮助

*/


UserThreadFactory(String whatFeatureOfGroup) {

namePrefix = "From UserThreadFactory's " + whatFeatureOfGroup + "-";

}

@Override

public Thread newThread(Runnable task) {

String name = namePrefix + nextId.getAndIncrement();

Thread thread = new Thread(null, task, name, 0);

System.out.println(thread.getName());

return thread;

}

}

【强制】线程资源必须通过线程池提供,不允许在应用中自行显式创建线程。

说明:线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源的开销,解决资源不足的问题。

如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题。

【强制】线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这

样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。

说明:Executors 返回的线程池对象的弊端如下:

1) FixedThreadPool 和 SingleThreadPool:

允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。

2) CachedThreadPool:

允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。

2、 ThreadPoolExecutor源码

1. 构造函数

UML图: image.png ThreadPoolExecutor的构造函数共有四个,但最终调用的都是同一个:

image.png

2.核心参数

  1. corePoolSize => 线程池核心线程数量

  2. maximumPoolSize => 线程池最大数量

  3. keepAliveTime => 线程池的工作线程空闲后,保持存活的时间。如果任务多而且任务的执行时间比较短,可以调大keepAliveTime,提高线程的利用率。

  4. unit => 时间单位

  5. workQueue => 线程池所使用的缓冲队列,队列类型有:

    • ArrayBlockingQueue,基于数组结构的有界阻塞队列,按FIFO(先进先出)原则对任务进行排序。使用该队列,线程池中能创建的最大线程数为maximumPoolSize

    • LinkedBlockingQueue,基于链表结构的无界阻塞队列,按FIFO(先进先出)原则对任务进行排序,吞吐量高于ArrayBlockingQueue。使用该队列,线程池中能创建的最大线程数为corePoolSize。静态工厂方法 Executor.newFixedThreadPool()使用了这个队列。

    • SynchronousQueue,一个不存储元素的阻塞队列。添加任务的操作必须等到另一个线程的移除操作,否则添加操作一直处于阻塞状态。静态工厂方法 Executor.newCachedThreadPool()使用了这个队列。

    • PriorityBlokingQueue:一个支持优先级的无界阻塞队列。使用该队列,线程池中能创建的最大线程数为corePoolSize。

  6. threadFactory => 线程池创建线程使用的工厂

  7. handler => 线程池对拒绝任务的处理策略,主要有4种类型的拒绝策略:

    • AbortPolicy:无法处理新任务时,直接抛出异常,这是默认策略。

    • CallerRunsPolicy:用调用者所在的线程来执行任务。

    • DiscardOldestPolicy:丢弃阻塞队列中最靠前的一个任务,并执行当前任务。

    • DiscardPolicy:直接丢弃任务。

3.execute()方法

image.png

  1. 如果当前运行的线程少于corePoolSize,则创建新的工作线程来执行任务(执行这一步骤需要获取全局锁)。

  2. 如果当前运行的线程大于或等于corePoolSize,而且BlockingQueue未满,则将任务加入到BlockingQueue中。

  3. 如果BlockingQueue已满,而且当前运行的线程小于maximumPoolSize,则创建新的工作线程来执行任务(执行这一步骤需要获取全局锁)。

  4. 如果当前运行的线程大于或等于maximumPoolSize,任务将被拒绝,并调用RejectExecutionHandler.rejectExecution()方法。即调用饱和策略对任务进行处理。

3、线程池的工作流程

image.png

image.png

执行逻辑说明:

  1. 判断核心线程数是否已满,核心线程数大小和corePoolSize参数有关,未满则创建线程执行任务

  2. 若核心线程池已满,判断队列是否满,队列是否满和workQueue参数有关,若未满则加入队列中

  3. 若队列已满,判断线程池是否已满,线程池是否已满和maximumPoolSize参数有关,若未满创建线程执行任务

  4. 若线程池已满,则采用拒绝策略处理无法执执行的任务,拒绝策略和handler参数有关

4、Executors创建返回ThreadPoolExecutor对象(不推荐)

Executors创建返回ThreadPoolExecutor对象的方法共有三种:

1. Executors#newCachedThreadPool => 创建可缓存的线程池

  • corePoolSize => 0,核心线程池的数量为0

  • maximumPoolSize => Integer.MAX_VALUE,可以认为最大线程数是无限的

  • keepAliveTime => 60L

  • unit => 秒

  • workQueue => SynchronousQueue

弊端:maximumPoolSize => Integer.MAX_VALUE可能会导致OOM

2. Executors#newSingleThreadExecutor => 创建单线程的线程池

SingleThreadExecutor是单线程线程池,只有一个核心线程:

  • corePoolSize => 1,核心线程池的数量为1

  • maximumPoolSize => 1,只可以创建一个非核心线程

  • keepAliveTime => 0L

  • unit => 毫秒

  • workQueue => LinkedBlockingQueue

弊端:LinkedBlockingQueue是长度为Integer.MAX_VALUE的队列,可以认为是无界队列,因此往队列中可以插入无限多的任务,在资源有限的时候容易引起OOM异常

3. Executors#newFixedThreadPool => 创建固定长度的线程池

  • corePoolSize => 1,核心线程池的数量为1

  • maximumPoolSize => 1,只可以创建一个非核心线程

  • keepAliveTime => 0L

  • unit => 毫秒

  • workQueue => LinkedBlockingQueue

它和SingleThreadExecutor类似,唯一的区别就是核心线程数不同,并且由于使用的是LinkedBlockingQueue,在资源有限的时候容易引起OOM异常

5、线程池的合理配置

从以下几个角度分析任务的特性:

  1. 任务的性质:CPU 密集型任务、IO 密集型任务和混合型任务。

  2. 任务的优先级:高、中、低。

  3. 任务的执行时间:长、中、短。

  4. 任务的依赖性:是否依赖其他系统资源,如数据库连接。

任务性质不同的任务可以用不同规模的线程池分开处理。可以通过 Runtime.getRuntime().availableProcessors()方法获得当前设备的 CPU 个数。

  • CPU 密集型任务:配置尽可能小的线程,如配置 cpu核心数+1 个线程的线程池。

  • IO 密集型任务 :由于线程并不是一直在执行任务,则配置尽可能多的线程,如2 ∗ Ncpu

  • 混合型任务:如果可以拆分,则将其拆分成一个 CPU 密集型任务和一个 IO 密集型任务。只要这两个任务执行的时间相差不是太大,那么分解后执行的吞吐率要高于串行执行的吞吐率;如果这两个任务执行时间相差太大,则没必要进行分解。

优先级不同的任务可以使用优先级队列 PriorityBlockingQueue 来处理,它可以让优先级高的任务先得到执行。但是,如果一直有高优先级的任务加入到阻塞队列中,那么低优先级的任务可能永远不能执行。

执行时间不同的任务可以交给不同规模的线程池来处理,或者也可以使用优先级队列,让执行时间短的任务先执行。

依赖数据库连接池的任务,因为线程提交 SQL 后需要等待数据库返回结果,线程数应该设置得较大,这样才能更好的利用 CPU。

建议使用有界队列,有界队列能增加系统的稳定性和预警能力。可以根据需要设大一点,比如几千。使用无界队列,线程池的队列就会越来越大,有可能会撑满内存,导致整个系统不可用。

处理拒绝策略有以下几种比较推荐:

在程序中捕获RejectedExecutionException异常,在捕获异常中对任务进行处理。针对默认拒绝策略使用CallerRunsPolicy拒绝策略,该策略会将任务交给调用execute的线程执行【一般为主线程】,此时主线程将在一段时间内不能提交任何任务,从而使工作线程处理正在执行的任务。此时提交的线程将被保存在TCP队列中,TCP队列满将会影响客户端,这是一种平缓的性能降低自定义拒绝策略,只需要实现RejectedExecutionHandler接口即可如果任务不是特别重要,使用DiscardPolicy和DiscardOldestPolicy拒绝策略将任务丢弃也是可以的如果使用Executors的静态方法创建ThreadPoolExecutor对象,可以通过使用Semaphore对任务的执行进行限流也可以避免出现OOM异常。

6、拒绝策略

有以下几种比较推荐:

  • 在程序中捕获RejectedExecutionException异常,在捕获异常中对任务进行处理。针对默认拒绝策略

  • 使用CallerRunsPolicy拒绝策略,该策略会将任务交给调用execute的线程执行【一般为主线程】,此时主线程将在一段时间内不能提交任何任务,从而使工作线程处理正在执行的任务。此时提交的线程将被保存在TCP队列中,TCP队列满将会影响客户端,这是一种平缓的性能降低

  • 自定义拒绝策略,只需要实现RejectedExecutionHandler接口即可

  • 如果任务不是特别重要,使用DiscardPolicy和DiscardOldestPolicy拒绝策略将任务丢弃也是可以的如果使用Executors的静态方法创建ThreadPoolExecutor对象,可以通过使用Semaphore对任务的执行进行限流也可以避免出现OOM异常。

  • 参考文章:8大拒绝策略

7、线程池的五种运行状态

线程状态:

image.png

不同于线程状态,线程池也有如下几种 状态:

image.png

• RUNNING :该状态的线程池既能接受新提交的任务,又能处理阻塞队列中任务。

• SHUTDOWN:该状态的线程池不能接收新提交的任务,但是能处理阻塞队列中的任务。(政府服务大厅不在允许群众拿号了,处理完手头的和排队的政务就下班)


处于 RUNNING 状态时,调用 shutdown()方法会使线程池进入到该状态。

注意:finalize() 方法在执行过程中也会隐式调用shutdown()方法。

• STOP:该状态的线程池不接受新提交的任务,也不处理在阻塞队列中的任务,还会中断正在执行的任务。(政府服务大厅不再进行服务了,拿号、排队、以及手头工作都停止了。)


在线程池处于 RUNNINGSHUTDOWN 状态时,调用shutdownNow() 方法会使线程池进入到该状态;

• TIDYING:如果所有的任务都已终止,workerCount (有效线程数)=0。


线程池进入该状态后会调用 terminated() 钩子方法进入TERMINATED 状态。

• TERMINATED:在terminated()钩子方法执行完后进入该状态,默认terminated()钩子方法中什么也没有做。


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

算法题每日一练---第37天:打家劫舍

一、问题描述你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装...
继续阅读 »

一、问题描述

你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。

给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。

题目链接:打家劫舍

二、题目要求

样例1

输入: [1,2,3,1]
输出: 4
解释: 偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。
偷窃到的最高金额 = 1 + 3 = 4

样例2

输入: [2,7,9,3,1]
输出: 12
解释: 偷窃 1 号房屋 (金额 = 2), 偷窃 3 号房屋 (金额 = 9),接着偷窃 5 号房屋 (金额 = 1)。
偷窃到的最高金额 = 2 + 9 + 1 = 12 。

数据范围

  • 1 <= nums.length <= 100`
  • 0 <= nums[i] <= 400`

考察

1.动态规划中等题型
2.建议用时5~15min

三、问题分析

这也是一道比较典型的动态规划问题,动态规划没做过的可以看这一篇入门题解:

算法题每日一练---第34天: 青蛙跳台阶

还是用我们的三步走,老套路:

第一步含义搞懂:

首先,使用动态规划一维数组就可以解决问题,那么这个dp[i]到底代表什么?

看看题目问什么,在不触犯警报的情况下,偷到的最大金额数,那么dp[i]就代表从截止到第i个房子,最大的金额数。

第二步变量初始:

假如房子数目为1,那么dp[0]=nums[0]
假如房子数目为2,那么dp[1]=max(nums[0],nums[1])

第三步规律归纳:

那么到底有什么规律呢?我把样例2详细列出来你看一下:

打家劫舍.gif

从第三个数开始,dp[i]是不是满足

dp[i]=max(dp[i−2]+nums[i],dp[i−1])关系式

三步走,打完收工!

四、编码实现

#include<iostream>
#include<algorithm>
using namespace std;
int main()
{
int n,nums[105],i,dp[105];//初始化
cin>>n;//输入数组的大小,n为0或1力扣要判断的,我这里省去了
for(i=1;i<=n;i++)//输入数组的元素
cin>>nums[i];
dp[1]=nums[1],dp[2]=max(nums[1],nums[2]);//初始化动态规划前两位
for(i=3;i<=n;i++)//第三位开始循环
{
dp[i]=max(dp[i-1],nums[i]+dp[i-2]);//找到规律
}
cout<<dp[n];//输出结果
return 0;
}

五、测试结果

2.png


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

Flutter - 这么炫酷的App你见过吗??

前言:今天是1024,先祝各位兄弟们节日快乐,永不脱发,永无Bug😜。说正事:在前几天,我发现了一个动画特别炫酷的一个Flutter项目,一款习惯养成类的App,看了后就真的是爱不释手,功能很丰富,所以我立刻找到了开源作者,向他申请了写作权限。然后开始了对项目...
继续阅读 »

前言:今天是1024,先祝各位兄弟们节日快乐,永不脱发,永无Bug😜。说正事:在前几天,我发现了一个动画特别炫酷的一个Flutter项目,一款习惯养成类的App,看了后就真的是爱不释手,功能很丰富,所以我立刻找到了开源作者,向他申请了写作权限。然后开始了对项目的分析(求个赞!!!相信我,看完这篇你会有收获的👍)

我对他项目的代码进行了部分修改,修改的源代码在文章最后~

开源项目地址:github.com/designDo/fl…

先上效果图:

tt0.top-432794.gif tt0.top-795301.gif

还有很多的功能大家自己下载源码(觉得好的话给开源作者点个star哦,人家不容易!)

本文分析重点:

  • 登录界面的动画、输入框处理以及顶部弹出框
  • 底部导航栏的动画处理
  • 首页动画以及环形进度条处理
  • 适配深色模式(分析一下作者的全局状态管理)

1.登录界面的动画、输入框处理以及顶部弹出框

  • 动画处理

    这里一共有3处动画,输入框的缩放动画,验证码按钮的平移动画,登录界面的缩放动画。

    当我们使用动画时,我们需要定义一个Controller来控制管理动画

    AnimationController _animationController;

    当然使用动画时我们的State是需要混入SingleTickerProviderStateMixin这个类的

    在效果图中我们也不难看出动画直接是有时间间距的,所以我们整个界面仅用一个Controller来控制,使其从上到下逐步显示。

    关于缩放动画呢,在flutter我们需要使用ScaleTransition,其中最重要的一点便是:

    Animation<double> scale //控制widget缩放

    来看看详细使用:

    ScaleTransition(
    //控制缩放从0到1
    scale: Tween<double>(begin: 0, end: 1).animate(CurvedAnimation(
    //控制动画的Controller
    parent: _animationController,
    //0,0.3是动画运行的时间
    //curve用来描述曲线运动的动画
    curve: Interval(0, 0.3, curve: Curves.fastOutSlowIn),
    )),
    child:...
    )

    这里关于其他动画也差不多,区别就在于动画和动画的运行时间

    关键区别:

    验证码的输入框:

    curve: Interval(0.3, 0.6, curve: Curves.fastOutSlowIn),

    获取验证码按钮:

    这里主要区别是position用于处理初始时的绝对位置

    SlideTransition(
    //大家可以将begin: Offset(2, 0)的数据更改,这样就会清晰的体验到它的功能
    position: Tween<Offset>(begin: Offset(2, 0), end: Offset.zero)
    .animate(CurvedAnimation(
    parent: _animationController,
    curve:
    Interval(0.6, 0.8, curve: Curves.fastOutSlowIn))),child:...)

    登录按钮:

    ScaleTransition(
    scale: Tween<double>(begin: 0, end: 1).animate(CurvedAnimation(
    parent: _animationController,
    curve: Interval(0.8, 1, curve: Curves.fastOutSlowIn),
    )),child:...)

    关于动画的实现就是这样,是不是非常的简单~

  • 手机号输入框的限制处理

登录输入框处理.png

我觉得这个样式很炫酷,主要是在平时不是很常见,就分析一下

这里我们封装了一个CustomEditField输入框,可以更好的做动画的处理

动画定义

///文本内容
String _value = '';
TextEditingController editingController;
AnimationController numAnimationController;
Animation<double> numAnimation;

且该组件需要混入(Mixin)TickerProviderStateMixin与AutomaticKeepAliveClientMixin,因为AnimationController需要调用TickerProvider里的createTicker方法(感兴趣可以查看flutter源码)

with TickerProviderStateMixin, AutomaticKeepAliveClientMixin

初始化时:

@override
void initState() {
_value = widget.initValue;
//初始化controller
editingController = TextEditingController(text: widget.initValue);
//初始化限制框的控制器与动画
numAnimationController =
AnimationController(duration: Duration(milliseconds: 500), vsync: this);
numAnimation = CurvedAnimation(
parent: numAnimationController, curve: Curves.easeOutBack);
if (widget.initValue.length > 0) {
numAnimationController.forward(from: 0.3);
}
super.initState();
}

销毁时:

@override
void dispose() {
editingController.dispose();
numAnimationController.dispose();
super.dispose();
}

UI: 使用Stack用于包裹一个输入框和限制框

Stack(
children:[
TextField(),
//限制框的动画,所以在外面套一层ScaleTransition
ScaleTransition(
child:Padding()
)
]
)

使用这个封装的组件时,我们主要处理numDecoration

此处的颜色为全局管理的处理,直接复制该代码需要修改

numDecoration: BoxDecoration(
shape: BoxShape.rectangle,
color: AppTheme.appTheme.cardBackgroundColor(),
borderRadius: BorderRadius.all(Radius.circular(15)),
boxShadow: AppTheme.appTheme.containerBoxShadow()),
numTextStyle: AppTheme.appTheme
.themeText(fontWeight: FontWeight.bold, fontSize: 15),
  • 顶部弹出框的处理

1634777618(1).png

使用了flash这个插件,一个高度可定制、功能强大且易于使用的警告框

为了代码的复用,在这里进行了封装处理

class FlashHelper {
static Future<T> toast<T>(BuildContext context, String message) async {
return showFlash<T>(
context: context,
//显示两秒
duration: Duration(milliseconds: 2000),
builder: (context, controller) {
//弹出框
return Flash.bar(
margin: EdgeInsets.only(left: 24, right: 24),
position: FlashPosition.top,
brightness: AppTheme.appTheme.isDark()
? Brightness.light
: Brightness.dark,
backgroundColor: Colors.transparent,
controller: controller,
child: Container(
alignment: Alignment.center,
padding: EdgeInsets.all(16),
height: 80,
decoration: BoxDecoration(
shape: BoxShape.rectangle,
borderRadius: BorderRadius.all(Radius.circular(16)),
gradient: AppTheme.appTheme.containerGradient(),
boxShadow: AppTheme.appTheme.coloredBoxShadow()),
child: Text(
//显示的文字
message,
style: AppTheme.appTheme.headline1(
textColor: Colors.white,
fontWeight: FontWeight.normal,
fontSize: 16),
),
));
});
}
}

2.底部导航栏的动画处理

tt0.top-150276.gif

这里真的是惊艳到我了,Icon都是画出来的,作者真的是脑洞大开,点赞!

  • Icon的绘制

    房子:

static final home = FluidFillIconData([
//房子
ui.Path()..addRRect(RRect.fromLTRBXY(-10, -2, 10, 10, 2, 2)),
ui.Path()
..moveTo(-14, -2)
..lineTo(14, -2)
..lineTo(0, -16)
..close(),
]);

四个正方形:

static final window = FluidFillIconData([
//正方形
ui.Path()..addRRect(RRect.fromLTRBXY(-12, -12, -2, -2, 2, 2)),
ui.Path()..addRRect(RRect.fromLTRBXY(2, -12, 12, -2, 2, 2)),
ui.Path()..addRRect(RRect.fromLTRBXY(-12, 2, -2, 12, 2, 2)),
ui.Path()..addRRect(RRect.fromLTRBXY(2, 2, 12, 12, 2, 2)),
]);

趋势图:

static final progress = FluidFillIconData([
//趋势图
ui.Path()
..moveTo(-10, -10)
..lineTo(-10, 8)
..arcTo(Rect.fromCircle(center: Offset(-8, 8), radius: 2), -1 * math.pi,
-0.5 * math.pi, true)
..moveTo(-8, 10)
..lineTo(10, 10),
ui.Path()
..moveTo(-6.5, 2.5)
..lineTo(0, -5)
..lineTo(4, 0)
..lineTo(10, -9),
]);

我的:

static final user = FluidFillIconData([
//我的
ui.Path()..arcTo(Rect.fromLTRB(-5, -16, 5, -6), 0, 1.9 * math.pi, true),
ui.Path()..arcTo(Rect.fromLTRB(-10, 0, 10, 20), 0, -1.0 * math.pi, true),
]);

大佬的思路就是强👍

  • 切换时的波浪动画

    这里主要是两个部分,一个是点击切换时的波浪动画,一个是动画结束后的凹凸效果

    这样的效果我们需要通过CustomPainter来进行绘制

    我们需要定义一些参数(指展示最重要的)

    final double _normalizedY;final double _x;

    然后进行绘制

 @override
void paint(canvas, size) {
// 使用基于“_normalizedY”值的各种线性插值绘制两条三次bezier曲线
final norm = LinearPointCurve(0.5, 2.0).transform(_normalizedY) / 2;
final radius = Tween<double>(
begin: _radiusTop,
end: _radiusBottom
).transform(norm);
// 当动画结束后的凹凸效果
final anchorControlOffset = Tween<double>(
begin: radius * _horizontalControlTop,
end: radius * _horizontalControlBottom
).transform(LinearPointCurve(0.5, 0.75).transform(norm));
final dipControlOffset = Tween<double>(
begin: radius * _pointControlTop,
end: radius * _pointControlBottom
).transform(LinearPointCurve(0.5, 0.8).transform(norm));


final y = Tween<double>(
begin: _topY,
end: _bottomY
).transform(LinearPointCurve(0.2, 0.7).transform(norm));
final dist = Tween<double>(
begin: _topDistance,
end: _bottomDistance
).transform(LinearPointCurve(0.5, 0.0).transform(norm));
final x0 = _x - dist / 2;
final x1 = _x + dist / 2;

//绘制工程
final path = Path()
..moveTo(0, 0)
..lineTo(x0 - radius, 0)
..cubicTo(x0 - radius + anchorControlOffset, 0, x0 - dipControlOffset, y, x0, y)
..lineTo(x1, y) //背景的宽高
..cubicTo(x1 + dipControlOffset, y, x1 + radius - anchorControlOffset, 0, x1 + radius, 0)
//背景的宽高
..lineTo(size.width, 0)
..lineTo(size.width, size.height)
..lineTo(0, size.height);

final paint = Paint()
..color = _color;

canvas.drawPath(path, paint);
}

@override
bool shouldRepaint(_BackgroundCurvePainter oldPainter) {
return _x != oldPainter._x
|| _normalizedY != oldPainter._normalizedY
|| _color != oldPainter._color;
}

这样带波浪动画的背景就完成啦~

  • 按钮的弹跳动画

    其实实现方式与波浪动画相同,也是通过CustomPainter来进行绘制

    (只展示核心代码)

//绘制其他无状态的按钮
final paintBackground = Paint()
..style = PaintingStyle.stroke
..strokeWidth = 2.4
..strokeCap = StrokeCap.round
..strokeJoin = StrokeJoin.round
..color = AppTheme.iconColor;
//绘制点击该按钮时的颜色
final paintForeground = Paint()
..style = PaintingStyle.stroke
..strokeWidth = 2.4
..strokeCap = StrokeCap.round
..strokeJoin = StrokeJoin.round
..color = AppTheme.appTheme.selectColor();

Icon的背景以及跳跃我们需要定义AnimationController与Animation,进行跳跃动画的绘制

在初始化时处理动画

@override
void initState() {
_animationController = AnimationController(
duration: const Duration(milliseconds: 1666),
reverseDuration: const Duration(milliseconds: 833),
vsync: this);
_animation = Tween<double>(begin: 0.0, end: 1.0).animate(_animationController)
..addListener(() {
setState(() {
});
});
_startAnimation();

super.initState();
}
final offsetCurve = _selected ? ElasticOutCurve(0.38) : Curves.easeInQuint;
final scaleCurve = _selected ? CenteredElasticOutCurve(0.6) : CenteredElasticInCurve(0.6);

final progress = LinearPointCurve(0.28, 0.0).transform(_animation.value);

final offset = Tween<double>(
begin: _defaultOffset,
end: _activeOffset
).transform(offsetCurve.transform(progress));
final scaleCurveScale = 0.50;
final scaleY = 0.5 + scaleCurve.transform(progress) * scaleCurveScale + (0.5 - scaleCurveScale / 2);

用于控制动画的运行与销毁:

@override
void didUpdateWidget(oldWidget) {
setState(() {
_selected = widget._selected;
});
_startAnimation();
super.didUpdateWidget(oldWidget);
}

void _startAnimation() {
if (_selected) {
_animationController.forward();
} else {
_animationController.reverse();
}
}

ui布局:

return GestureDetector(
onTap: _onPressed,
behavior: HitTestBehavior.opaque,
child: Container(
constraints: BoxConstraints.tight(ne),
alignment: Alignment.center,
child: Container(
margin: EdgeInsets.all(ne.width / 2 - _radius),
constraints: BoxConstraints.tight(Size.square(_radius * 2)),
decoration: ShapeDecoration(
color: AppTheme.appTheme.cardBackgroundColor(),
shape: CircleBorder(),
),
transform: Matrix4.translationValues(0, -offset, 0),
//Icon的绘制
child: FluidFillIcon(
_iconData,
LinearPointCurve(0.25, 1.0).transform(_animation.value),
scaleY,
),
),
),
);

这样底部导航栏就完成啦!

3.首页动画以及环形进度条处理

  • 首页整体列表动画处理

    这一部分数据是最为复杂的

    与其他动画相同,我们需要一个controller来控制,在此页面,我们还需要一个List来存放数据

    final AnimationController mainScreenAnimationController;
    final Animation<dynamic> mainScreenAnimation;
    final List<Habit> habits;

    数据存储在此文章暂时不分析,大家可以自己运行源码~

    初始化动画:

@override
void initState() {
animationController = AnimationController(
duration: const Duration(milliseconds: 2000), vsync: this);
super.initState();
}

因为使用到动画的组件很多,所以我们根节点使用AnimatedBuilder,主要使用的动画FadeTransition与Transform,做法于上面相同,在此就不多赘述了。

  • 环形进度条

    我们封装了一个CircleProgressBar用户绘制圆形进度条

    这部分的ui很简单,主要是动画的绘制较为复杂

屏幕截图 2021-10-23 140905.jpg

ui:

return AspectRatio(
aspectRatio: 1,
child: AnimatedBuilder(
animation: this.curve,
child: Container(),
builder: (context, child) {
final backgroundColor =
this.backgroundColorTween?.evaluate(this.curve) ??
this.widget.backgroundColor;
final foregroundColor =
this.foregroundColorTween?.evaluate(this.curve) ??
this.widget.foregroundColor;

return CustomPaint(
child: child,
//重点是这个封装组件,这里是圆形里面的进度条
foregroundPainter: CircleProgressBarPainter(
backgroundColor: backgroundColor,
foregroundColor: foregroundColor,
percentage: this.valueTween.evaluate(this.curve),
strokeWidth: widget.strokeWidth
),
);
},
),
);

详细的绘制:

@override
void paint(Canvas canvas, Size size) {
final Offset center = size.center(Offset.zero);
final Size constrainedSize =
size - Offset(this.strokeWidth, this.strokeWidth);
final shortestSide =
Math.min(constrainedSize.width, constrainedSize.height);
final foregroundPaint = Paint()
..color = this.foregroundColor
..strokeWidth = this.strokeWidth
..strokeCap = StrokeCap.round
..style = PaintingStyle.stroke;
final radius = (shortestSide / 2);

// Start at the top. 0 radians represents the right edge
final double startAngle = -(2 * Math.pi * 0.25);
final double sweepAngle = (2 * Math.pi * (this.percentage ?? 0));

// Don't draw the background if we don't have a background color
if (this.backgroundColor != null) {
final backgroundPaint = Paint()
..color = this.backgroundColor
..strokeWidth = this.strokeWidth
..style = PaintingStyle.stroke;
canvas.drawCircle(center, radius, backgroundPaint);
}

canvas.drawArc(
Rect.fromCircle(center: center, radius: radius),
startAngle,
sweepAngle,
false,
foregroundPaint,
);
}

这里还有一个很实用的功能:

时间定义和欢迎词

屏幕截图 2021-10-23 142038.jpg

这个demo包含了大部分对时间的处理

屏幕截图 2021-10-23 142440.jpg 例如:

///根据当前时间获取,[monthIndex]个月的开始结束日期
static Pair<DateTime> getMonthStartAndEnd(DateTime now, int monthIndex) {
DateTime start = DateTime(now.year, now.month - monthIndex, 1);
DateTime end = DateTime(now.year, now.month - monthIndex + 1, 0);
return Pair<DateTime>(start, end);
}

强烈推荐大家学习,开发中比较常用!

关于此app的大部分动画ui都分析完成了,其他都是在复用,大家觉得还不错的话可以自己下载体验一下,养成好习惯~

4.适配深色模式(分析一下作者的全局状态管理)

作者在这里使用了Bloc用于状态管理

///  theme mode
enum AppThemeMode {
Light,
Dark,
}
///字体模式
enum AppFontMode {
///默认字体
Roboto,
///三方字体
MaShanZheng,
}
///颜色模式,特定view背景颜色
enum AppThemeColorMode {
Indigo, Orange, Pink, Teal, Blue, Cyan, Purple }

在此基础上,定义了颜色,样式,例如:

String fontFamily(AppFontMode fontMode) {
switch (fontMode) {
case AppFontMode.MaShanZheng:
return 'MaShanZheng';
}
return 'Roboto';
}

然后在使用样式时多用三元判断,这样就很简单的实现了状态管理

这样对这个项目的ui已经动画就分析完成了,大家也可以通过这个项目来学习本地存储,看到这里了,不妨点个赞吧😘


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

kotlin 协程 + Retrofit 搭建网络请求方案对比

近期在调研使用Kotlin协程 + Retrofit做网络请求方案的实践,计划后面会引入到新项目中,Retrofit的使用非常的简单,基本上看个文档就能立马接入,也在github上找了大量的Demo来看别人是怎么写的,看了大量网上的文章,但发现很多文章看下来也...
继续阅读 »

近期在调研使用Kotlin协程 + Retrofit做网络请求方案的实践,计划后面会引入到新项目中,Retrofit的使用非常的简单,基本上看个文档就能立马接入,也在github上找了大量的Demo来看别人是怎么写的,看了大量网上的文章,但发现很多文章看下来也只是一个简单的接入Demo,不能满足我当下的业务需求。以下记录近期调研的结果和我们的使用。 首先我们先对比从网上找到的几种方案:

方案一

代码摘自这里 这是一篇非常好的Kotlin 协程 + Retrofit 入门的文章,其代码如下:

  1. 服务的定义
interface ApiService {
@GET("users")
suspend fun getUsers(): List

}
  1. Retrofit Builder
object RetrofitBuilder {

private const val BASE_URL = "https://5e510330f2c0d300147c034c.mockapi.io/"

private fun getRetrofit(): Retrofit {
return Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build() //Doesn't require the adapter
}

val apiService: ApiService = getRetrofit().create(ApiService::class.java)
}
  1. 一些中间层
class ApiHelper(private val apiService: ApiService) {

suspend fun getUsers() = apiService.getUsers()
}
class MainRepository(private val apiHelper: ApiHelper) {

suspend fun getUsers() = apiHelper.getUsers()
}
  1. 在ViewModel中获取网络数据
class MainViewModel(private val mainRepository: MainRepository) : ViewModel() {

fun getUsers() = liveData(Dispatchers.IO) {
emit(Resource.loading(data = null))
try {
emit(Resource.success(data = mainRepository.getUsers()))
} catch (exception: Exception) {
emit(Resource.error(data = null, message = exception.message ?: "Error Occurred!"))
}
}
}

这段代码能够与服务端通信,满足基本的要求,并且也有异常的处理机制。但存在以下问题:

  1. 对异常的处理粒度过大。如果需要对不同的异常进行差异化的处理,就会比较麻烦。
  2. 在每一个调用的地方都需要进行try...catch操作。
  3. 不支持从reponse中获取响应头部, http code 信息。但其实很多APP通常也没有要求做这些处理,如果没有拿到数据,给一个通用的提示就完。所以这种方案在某些情况下是可以直接使用的。

方案二

从Github上找了一个Demo, 链接在这里 和方案一相比,作者在的BaseRepository里面,对接口的调用统一进行了try...catch的处理,这样对于调用方,就不用每一个都添加try...catch了。相关的代码如下:

open class BaseRepository {

suspend fun apiCall(call: suspend () -> WanResponse): WanResponse {
return call.invoke()
}

suspend fun safeApiCall(call: suspend () -> Result, errorMessage: String): Result {
return try {
call()
} catch (e: Exception) {
// An exception was thrown when calling the API so we're converting this to an IOException
Result.Error(IOException(errorMessage, e))
}
}

suspend fun executeResponse(response: WanResponse, successBlock: (suspend CoroutineScope.() -> Unit)? = null,
errorBlock: (suspend CoroutineScope.() -> Unit)? = null)
: Result {
return coroutineScope {
if (response.errorCode == -1) {
errorBlock?.let { it() }
Result.Error(IOException(response.errorMsg))
} else {
successBlock?.let { it() }
Result.Success(response.data)
}
}
}

}

在Repository里面这样写

class HomeRepository : BaseRepository() {

suspend fun getBanners(): Result> {
return safeApiCall(call = {requestBanners()},errorMessage = "")
}

private suspend fun requestBanners(): Result> =
executeResponse(WanRetrofitClient.service.getBanner())

}

方案三

在网上看到这个博客, 作者利用一个CallAdapter进行转换,将http错误转换成异常抛出来(后面我自己的方案一也是按照这个思路来的)。核心代码如下:

class ApiResultCallAdapter(private val type: Type) : CallAdapter>> {
override fun responseType(): Type = type

override fun adapt(call: Call): Call> {
return ApiResultCall(call)
}
}

class ApiResultCall(private val delegate: Call) : Call> {
/**
* 该方法会被Retrofit处理suspend方法的代码调用,并传进来一个callback,如果你回调了callback.onResponse,那么suspend方法就会成功返回
* 如果你回调了callback.onFailure那么suspend方法就会抛异常
*
* 所以我们这里的实现是永远回调callback.onResponse,只不过在请求成功的时候返回的是ApiResult.Success对象,
* 在失败的时候返回的是ApiResult.Failure对象,这样外面在调用suspend方法的时候就不会抛异常,一定会返回ApiResult.Success 或 ApiResult.Failure
*/

override fun enqueue(callback: Callback>) {
//delegate 是用来做实际的网络请求的Call对象,网络请求的成功失败会回调不同的方法
delegate.enqueue(object : Callback {

/**
* 网络请求成功返回,会回调该方法(无论status code是不是200)
*/

override fun onResponse(call: Call, response: Response) {
if (response.isSuccessful) {//http status 是200+
//这里担心response.body()可能会为null(还没有测到过这种情况),所以做了一下这种情况的处理,
// 处理了这种情况后还有一个好处是我们就能保证我们传给ApiResult.Success的对象就不是null,这样外面用的时候就不用判空了
val apiResult = if (response.body() == null) {
ApiResult.Failure(ApiError.dataIsNull.errorCode, ApiError.dataIsNull.errorMsg)
} else {
ApiResult.Success(response.body()!!)
}
callback.onResponse(this@ApiResultCall, Response.success(apiResult))
} else {//http status错误
val failureApiResult = ApiResult.Failure(ApiError.httpStatusCodeError.errorCode, ApiError.httpStatusCodeError.errorMsg)
callback.onResponse(this@ApiResultCall, Response.success(failureApiResult))
}

}

/**
* 在网络请求中发生了异常,会回调该方法
*
* 对于网络请求成功,但是业务失败的情况,我们也会在对应的Interceptor中抛出异常,这种情况也会回调该方法
*/

override fun onFailure(call: Call, t: Throwable) {
val failureApiResult = if (t is ApiException) {//Interceptor里会通过throw ApiException 来直接结束请求 同时ApiException里会包含错误信息
ApiResult.Failure(t.errorCode, t.errorMsg)
} else {
ApiResult.Failure(ApiError.unknownException.errorCode, ApiError.unknownException.errorMsg)
}

callback.onResponse(this@ApiResultCall, Response.success(failureApiResult))
}

})
}
...
}

作者有提供一个Demo, 如果想拿来用,需要自己再新增一个返回数据的包装类。该方案的缺点是不能获取响应体中的header,还是那句话,毕竟这个需求不常见,可以忽略。

总结一下,当前网上的这些方案可能有的局限:

  1. 如果服务器出错了,不能拿到具体的错误信息。比如,如果服务器返回401, 403,这些方案中的网络层不能将这些信息传递出去。
  2. 如果服务端通过header传递数据给前端,这些方案是不满足需求的。

针对上面的两个问题,我们来考虑如何完善框架的实现。

调整思路

我们期望一个网络请求方案能满足如下目标:

  1. 与服务器之间的正常通信
  2. 能拿到响应体中的header数据
  3. 能拿到服务器的出错信息(http code,message)
  4. 方便的异常处理

调整后的方案

以下代码的相关依赖库版本

implementation 'com.squareup.retrofit2:retrofit:2.8.1'
implementation "com.squareup.retrofit2:converter-gson:2.8.1"

//Coroutine
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.6"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.6"
  1. 约定常见的错误类型

我们期望ApiException中也能够返回HTTP Code, 为此约定,错误信息的code从20000开始,这样就不会和HTTP的Code有冲突了。

  • ApiError
object ApiError {
var unknownError = Error(20000, "unKnown error")
var netError = Error(20001, "net error")
var emptyData = Error(20002, "empty data")
}

data class Error(var errorCode: Int, var errorMsg: String)
  1. 返回数据的定义ApiResult.kt

用来承载返回的数据,成功时返回正常的业务数据,出错时组装errorCode, errorMsg, 这些数据会向上抛给调用方。

sealed class ApiResult() {
data class Success(val data: T):ApiResult()
data class Failure(val errorCode:Int,val errorMsg:String):ApiResult()
}
data class ApiResponse(var errorCode: Int, var errorMsg: String, val data: T)

方案一

该方案支持获取HTTP Code,并返回给调用方, 不支持从HTTP Response中提取header的数据。

  1. 服务的定义WanAndroidApi
interface WanAndroidApi {
@GET("/banner/json")
suspend fun getBanner(): ApiResult>>
}
  1. 定义一个ApiCallAdapterFactory.kt

在这里面会对响应的数据进行过滤,对于出错的情况,向外抛出错误。

class ApiCallAdapterFactory : CallAdapter.Factory() {
override fun get(returnType: Type, annotations: Array, retrofit: Retrofit): CallAdapter<*, *>? {=
check(getRawType(returnType) == Call::class.java) { "$returnType must be retrofit2.Call." }
check(returnType is ParameterizedType) { "$returnType must be parameterized. Raw types are not supported" }

val apiResultType = getParameterUpperBound(0, returnType)
check(getRawType(apiResultType) == ApiResult::class.java) { "$apiResultType must be ApiResult." }
check(apiResultType is ParameterizedType) { "$apiResultType must be parameterized. Raw types are not supported" }

val dataType = getParameterUpperBound(0, apiResultType)
return ApiResultCallAdapter(dataType)
}
}
class ApiResultCallAdapter(private val type: Type) : CallAdapter>> {
override fun responseType(): Type = type

override fun adapt(call: Call): Call> {
return ApiResultCall(call)
}
}

class ApiResultCall(private val delegate: Call) : Call> {

override fun enqueue(callback: Callback>) {
delegate.enqueue(object : Callback {

override fun onResponse(call: Call, response: Response) {
if (response.isSuccessful) {
val apiResult = if (response.body() == null) {
ApiResult.Failure(ApiError.emptyData.errorCode, ApiError.emptyData.errorMsg)
} else {
ApiResult.Success(response.body()!!)
}
callback.onResponse(this@ApiResultCall, Response.success(apiResult))
} else {
val failureApiResult = ApiResult.Failure(response.code(), response.message())
callback.onResponse(this@ApiResultCall, Response.success(failureApiResult))
}

}

override fun onFailure(call: Call, t: Throwable) {
//Interceptor里会通过throw ApiException 来直接结束请求 同时ApiException里会包含错误信息
val failureApiResult = if (t is ApiException) {
ApiResult.Failure(t.errorCode, t.errorMessage)
} else {
ApiResult.Failure(ApiError.netError.errorCode, ApiError.netError.errorMsg)
}
callback.onResponse(this@ApiResultCall, Response.success(failureApiResult))
}
})
}

override fun clone(): Call> = ApiResultCall(delegate.clone())

override fun execute(): Response> {
throw UnsupportedOperationException("ApiResultCall does not support synchronous execution")
}


override fun isExecuted(): Boolean {
return delegate.isExecuted
}

override fun cancel() {
delegate.cancel()
}

override fun isCanceled(): Boolean {
return delegate.isCanceled
}

override fun request(): Request {
return delegate.request()
}

override fun timeout(): Timeout {
return delegate.timeout()
}
}
  1. 在Retrofit 初始化时指定CallAdapterFactory, 定义文件ApiServiceCreator.kt 如下:
object ApiServiceCreator {

private const val BASE_URL = "https://www.wanandroid.com/"
var okHttpClient: OkHttpClient = OkHttpClient().newBuilder().build()

private fun getRetrofit() = Retrofit.Builder()
.baseUrl(BASE_URL)
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(ApiCallAdapterFactory())
.build()

fun create(serviceClass: Class): T = getRetrofit().create(serviceClass)
inline fun create(): T = create(T::class.java)
}
  1. 在ViewModel中使用如下:
viewModelScope.launch {
when (val result = api.getBanner()) {
is ApiResult.Success<*> -> {
var data = result.data as ApiResponse>
Log.i("API Response", "--------->data size:" + data.data.size)
}
is ApiResult.Failure -> {
Log.i("API Response","errorCode: ${result.errorCode} errorMsg: ${result.errorMsg}")

}
}
}

方案二

该方案在方案一的基础之上,支持从HTTP Response Header中获取数据。

  1. 服务的定义WanAndroidApi
interface WanAndroidApi {
@GET("/banner/json")
fun getBanner2(): Call>>
}

需要注意此处的getBanner2()方法前面没有suspend关键字,返回的是一个Call类型的对象,这个很重要。

  1. 定义一个CallWait.kt文件, 为Call类添加扩展方法awaitResult, 该方法内部有部份逻辑和上面的CallAdapter中的实现类似。CallWait.kt文件也是借鉴了这段代码
suspend fun  Call.awaitResult(): ApiResult {
return suspendCancellableCoroutine { continuation ->
enqueue(object : Callback {
override fun onResponse(call: Call?, response: Response) {
continuation.resumeWith(runCatching {
if (response.isSuccessful) {
var data = response.body();
if (data == null) {
ApiResult.Failure(ApiError.emptyData.errorCode, ApiError.emptyData.errorMsg)
} else {
ApiResult.Success(data!!)
}
} else {
ApiResult.Failure(response.code(), response.message())
}
})
}

override fun onFailure(call: Call, t: Throwable) {
// Don't bother with resuming the continuation if it is already cancelled.
if (continuation.isCancelled) return
if (t is ApiException) {
ApiResult.Failure(t.errorCode, t.errorMessage)
} else {
ApiResult.Failure(ApiError.netError.errorCode, ApiError.netError.errorMsg)
}
}
})
}
}
  1. Retrofit的初始化

和方案一不一样,在Retrofit 初始化时不需要指定CallAdapterFactory, 定义文件ApiServiceCreator.kt

object ApiServiceCreator {

private const val BASE_URL = "https://www.wanandroid.com/"
var okHttpClient: OkHttpClient = OkHttpClient().newBuilder().build()

private fun getRetrofit() = Retrofit.Builder()
.baseUrl(BASE_URL)
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create())
.build()

fun create(serviceClass: Class): T = getRetrofit().create(serviceClass)
inline fun create(): T = create(T::class.java)
}
  1. ViewModel中使用, 和方法一基本一致,只是这里需要调用一下awaitResult方法
viewModelScope.launch {
when (val result = api.getBanner2().awaitResult()) {
is ApiResult.Success<*> -> {
var data = result.data as ApiResponse>
Log.i("API Response", "--------->data size:" + data.data.size)
}
is ApiResult.Failure -> {
Log.i("API Response","errorCode: ${result.errorCode} errorMsg: ${result.errorMsg}")

}
}
}
  1. 如果我们想从reponse的header里面拿数据, 可以使用Retrofit提供的扩展函数awaitResponse, 如下:
try {
val result = api.getBanner2().awaitResponse()
//拿HTTP Header中的数据
Log.i("API Response", "-----header---->Server:" + result.headers().get("Server"))

if (result.isSuccessful) {
var data = result.body();
if (data != null && data is ApiResponse>) {
Log.i("API Response", "--------->data:" + data.data.size)
}
} else {
//拿HTTP Code
Log.i("API Response","errorCode: ${result.code()}")
}
} catch (e: Exception) {
Log.i("API Response","exception: ${e.message}");
}

方案三

如果我们用Java去实现一套

  • 定义服务
public interface WanAndroidApiJava {
@GET("/banner/json")
public Call>> getBanner();
}
  • ApiException中去封装错误信息
public class ApiException extends Exception {
private int errorCode;
private String errorMessage;

public ApiException(int errorCode, String message) {
this.errorCode = errorCode;
this.errorMessage = message;
}

public ApiException(int errorCode, String message, Throwable e) {
super(e);
this.errorCode = errorCode;
this.errorMessage = message;
}

public String getErrorMessage() {
return this.errorMessage;
}

public int getErrorCode() {
return this.errorCode;
}

interface Code {
int ERROR_CODE_DATA_PARSE = 20001;
int ERROR_CODE_SEVER_ERROR = 20002;
int ERROR_CODE_NET_ERROR = 20003;
}

public static final ApiException PARSE_ERROR = new ApiException(Code.ERROR_CODE_DATA_PARSE, "数据解析出错");
public static final ApiException SERVER_ERROR = new ApiException(Code.ERROR_CODE_SEVER_ERROR, "服务器响应出错");
public static final ApiException NET_ERROR = new ApiException(Code.ERROR_CODE_NET_ERROR, "网络连接出错");
}
  • NetResult封装服务器的响应
public class NetResult {
private T data;
private int code;
private String errorMsg;
...//省略get/set
}
  • 自定义一个Callback去解析数据
public abstract class RetrofitCallbackEx implements Callback> {

@Override
public void onResponse(Call> call, Response> response) {
//如果返回成功
if (response.isSuccessful()) {
NetResult data = response.body();
//返回正确, 和后端约定,返回的数据中code == 0 代表业务成功
if (data.getCode() == 0) {
try {
onSuccess(data.getData());
} catch (Exception e) {
//数据解析出错
onFail(ApiException.PARSE_ERROR);
}
} else {
onFail(ApiException.SERVER_ERROR);
}
} else {
//服务器请求出错
Log.i("API Response", "code:" + response.code() + " message:" + response.message());
onFail(ApiException.SERVER_ERROR);
}
}

@Override
public void onFailure(Call> call, Throwable t) {
onFail(ApiException.NET_ERROR);
}

protected abstract void onSuccess(T t);

protected abstract void onFail(ApiException e);

}
  1. 使用
api.getBanner().enqueue(new RetrofitCallbackEx>() {
@Override
protected void onSuccess(List banners) {
if (banners != null) {
Log.i("API Response", "data size:" + banners.size());
}
}

@Override
protected void onFail(ApiException e) {
Log.i("API Response", "exception code:" + e.getErrorCode() + " msg:" + e.getErrorMessage() + " root cause: " + e.getMessage());
}
});

其它

  1. 在实际项目中,可能经常会碰到需要对HTTP Code进行全局处理的,比如当服务器返回401的时候,引导用户去登录页,这种全局的拦截直接放到interceptor 里面去做就好了。
  2. 架构的方案是为了满足业务的需求,这里也只是针对自己碰到的业务场景来进行梳理调研。当然实际项目中通常会有更多的要求,比如环境的切换导致域名的不同,网络请求的通用配置,业务异常的上报等等,一个完整的网络请求方案需要再添加更多的功能。
  3. Kotlin语言非常的灵活,扩展函数的使用能使代码非常的简洁。Kotlin在我们项目中用的不多, 不是非常精通,协程 + Retrofit应该会有更优雅的写法,欢迎交流。


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

Google 如何看待 Kotlin 与 Android

先进 简洁 安全。 在语法表现上,Kotlin够简洁明了。不防看看:你应该切换到Kotlin开发,它包含了零默认值和不可变性的安全特性,使你的Android应用程序在默认情况下是安全的 并且性能是良好的。 代码更安全 编写更安全的代码,并在应用程序中避免 发生...
继续阅读 »

先进 简洁 安全。


在语法表现上,Kotlin够简洁明了。不防看看:你应该切换到Kotlin开发,它包含了零默认值和不可变性的安全特性,使你的Android应用程序在默认情况下是安全的 并且性能是良好的。


代码更安全


编写更安全的代码,并在应用程序中避免 发生Nullpointerexception。


var output: String
output = null // Compilation error==================================val name: String? = null // Nullable type
println(name.length()) // Compilation error

语法更易读和简洁


Data Classes


更加专注于表达你自己的代码创意设计,无需编写更多的样板代码。


// Create a POJO with getters, setters, equals(), hashCode(), toString(), and copy() with a single line:
data class User(val name: String, val email: String)

Lambdas语法


使用lambda来简化你的代码。


button.setOnClickListener(new View.OnClickListener(){
@Override
public void onClick(View v){
doSomething();
}
});

button.setOnClickListener { doSomething() }    

默认的命名参数


通过使用默认参数减少重载函数的数量。使用命名参数调用函数,使自己的代码更具有可读性。


fun format(str: String,
normalizeCase: Boolean = true,
upperCaseFirstLetter: Boolean = true,
divideByCamelHumps: Boolean = false,
wordSeparator: Char = ' ') {

}==================================// Call function with named arguments.
format(str, normalizeCase = true, upperCaseFirstLetter = true)

和 findViewById 说再见


在你自己的代码中避免findViewById() 调用。专注于写你的逻辑,而不需要那么繁琐。


import kotlinx.android.synthetic.main.content_main.*class MainActivity : AppCompatActivity() {   override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// No need to call findViewById(R.id.textView) as TextView
textView.text = "Kotlin for Android rocks!" }
}

扩展功能, 而不是用继承


扩展函数和属性使你可以轻松地扩展类的功能,而无需继承它们。调用代码是可读和自然的。


// Extend ViewGroup class with inflate function
fun ViewGroup.inflate(layoutRes: Int): View {
return LayoutInflater.from(context).inflate(layoutRes, this, false)
}==================================// Call inflate directly on the ViewGroup instance
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val v = parent.inflate(R.layout.view_item)
return ViewHolder(v)
}

100%的和Java可互操作性


在你非常不是想用Java的情况下,尽量多地使用Kotlin。Kotlin是一种与Java完全可互操作的JVM语言。


// Calling Java code from Kotlin
class KotlinClass {
fun kotlinDoSomething() {
val javaClass = JavaClass()
javaClass.javaDoSomething()
println(JavaClass().prop)
}
}==================================// Calling Kotlin code from Java
public class JavaClass {
public String getProp() { return "Hello"; }
public void javaDoSomething() {
new KotlinClass().kotlinDoSomething();
}
}

强大的开发工具支持


Android Studio 3.0 提供了不错的工具来帮助你开始使用Kotlin开发。在将Java代码粘贴到Kotlin文件时,可以转换整个Java文件或转换一段代码片段。很稳!


image.png


Kotlin 是开放的


与Android一样,Kotlin是Apache 2.0下的一个开源项目。Google对 Kotlin 的选择重申了Android对开发者 开放生态系统的承诺,随着 Google 的发展和 Android平台的发展,Google 希望 kotlin 语言的发展, 也很高兴看到 kotlin 语言的发展。


image.png


Tamic的一些话


Java 10 的 新特性也刚好( Java 10 新特性解密)迎合kotlin的某些特性一样,以后即将用var 来定义变量和类。 因此我们发现Koltin将来必定是开发者所关注的一名语言趋势,假如有一天,Google像抛弃 Eclispe,投坏Android Studio一样,放弃对Java的支持,到时候,至少你还能掌握kotlin开发,不然,你是要转行吗?


相关视频


Android进阶开发:函数与方法有本质区别 你知道吗


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

超难面试题:Android 为什么设计只有主线程更新UI

选择方案的选择 单线程更新UI 多线程更新UI 从问题本身考虑就两个方案不是单线程就是多线程。 下面是个人根据具体情况分析,如果有什么不对的地方,欢迎指正。 从开发效率来看,开发一个 单线程UI 库,会显得非常容易,而且,每个控件的运行效率肯定会比多线...
继续阅读 »

选择方案的选择



  1. 单线程更新UI

  2. 多线程更新UI


从问题本身考虑就两个方案不是单线程就是多线程。



下面是个人根据具体情况分析,如果有什么不对的地方,欢迎指正。



从开发效率来看,开发一个 单线程UI 库,会显得非常容易,而且,每个控件的运行效率肯定会比多线程的效率高,比如我们单线程可以使用HashMap,多线程就需要使用JUC 框架下的类库了,这个效率肯定比 HashMap低很多,这样就很好理解。编写一个多线程的UI库,很可能每个控件,都会加锁,控件本身效率就低了,但是这样还不够 ,后面会解释。


还有一个简单方案,就是对真个UI库,加锁,而不是具体某个控件,就是确保同一时刻,只能有一个线程,对整个UI系统更新,这个已经有点单线程更新UI的意思了 。但是锁的粒度会很大,如果一个页面100个控件,相当于每个控件都加锁了。


这个方案实现起来倒是不复杂,只需要设计一个boolean变量就可以,任何线程需要更新UI 都会访问这个变量获取锁,这个方案会造成所有的线程都竞争同一把锁,单从运行效率分析,应该是很高的,但是这个竞争特别激烈,可能造成的问题就是,事件响应不够及时,


单线程更新UI方案简单成熟


单线程更新UI方案,从上面的分析来看,优势就很明显,整体设计可能是最简单的,每个控件的设计只需要考虑单线程运行就可以,完全不必关系其他线程更新UI。


而且这套方案非常成熟,在Android 之前,swing qt windows 几乎绝大部分图形界面api 都会使用这个单线程方案。


从执行效率看


前面说了,如果一个加锁的api 和不加锁的api 比较,那肯定不加锁效率高对吧,但是,这么说确实很笼统,如果合理设计一套多线程更新ui 的库,整体性能未必会比单线程差,只是想实现这样一套系统的复杂程度,可能不只是翻倍那么简单,设计越复杂,带来的问题是 潜在bug 可能会多,但是这些,在设计ui系统 的时候未必是这样考虑的,如果业务复杂,效果会更好,那么我相信大部分企业还是会设计一个复杂的系统的。


综合考虑?


多线程更新UI,不管如何设计都会绕不开一个问题,就是竞争,而这个竞争,是整个UI系统的,而不是单独一个控件,大部分情况下,一个线程可能同时更新的是过个控件,而要确保我一次更新的所有控件是同步更新的,所以要保证这个逻辑,其实我们就要确保一个问题,同一时刻。永远只允许一个线程去更新UI。不能保证这一点,就会造成业务逻辑可能各种问题,甚至各种死锁。


既然同一个时刻只能一个线程更新,那设计成单线程是不是就更好呢,到这里,其实还是不够全面的,还有个因素就是事件相应。如果多线程更新的情况下,其实这个是不容易实现的, 反而单线程,就好实现一些。


总结


通过分析总结几个点。



  1. 一般UI还是要保证同一时刻只有一个线程在更新,所以效率不会更高。

  2. 多线程更新UI实现上会复杂一些,Java的内部人员发布过文章也说过这个几乎不可实现。

  3. 从响应速度角度分析,单线程可以设计出更好的响应速度的api

  4. 单线程更新,也是一个被证明效果非常好的方案。


从过个角度分析 Android 为什么设计只有主线程更新UI 都是最好的选择。


不过回答这个问题需要理解的不全是结论,而是对这个问题,和图形界面开发的理解。
如果说效率高,安全,也需要回答出来为什么。这些不是凭空说的。真的效率高吗?高在哪里?都需要说清楚,可能会有不正确的地方。但是只要把需要考虑的点表达清晰就好


引用


负责Swing开发的一个大师的一篇博客《Multithreaded toolkits: A failed dream?》


也有人说单新ui 效率会高,因为多线程会加锁。如果有人能把这个细节解释清楚呢,希望留言。因为正常设计也只是锁更新那一行代码而已,我的总结就是效率不分伯仲,希望大家探讨吧。


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

IDEA 中玩转 Git

Git
Git 有很多客户端工具,不过感觉还是命令行操作最好用,方便又快捷,不过命令行操作需要小伙伴们对 Git 命令比较熟练,这可能对有的人来说会有一些难度,所以,客户端工具有时候也不能抛弃,如果非要推荐一个 Git 客户端工具,我觉得还是 IDEA 中的 Git ...
继续阅读 »

Git 有很多客户端工具,不过感觉还是命令行操作最好用,方便又快捷,不过命令行操作需要小伙伴们对 Git 命令比较熟练,这可能对有的人来说会有一些难度,所以,客户端工具有时候也不能抛弃,如果非要推荐一个 Git 客户端工具,我觉得还是 IDEA 中的 Git 插件吧。其他的 Git 客户端工具松哥之前也有体验过一些,不过感觉还是 IDEA 中的用起来更加省事。


今天这篇文章算是我第二次教大家在开发工具中使用 Git 了,刚毕业的时候,松哥写过一篇文章,教大家在 Eclipse 中使用 Git,那时候在 Eclipse 中使用 Git 是真的麻烦,光是插件就要安装半天,刚刚翻了一下那篇文章,已经是七年前的事情了。



七年之后,Eclipse 也没了往日的风光,IDEA 逐渐成了开发的主流工具,咱们今天就来捋一捋 IDEA 中使用 Git。


1. 基本配置


首先你要安装 Git,这个就不需要我多说了,IDEA 上默认也是安装了 Git 插件,可以直接使用。


为了给小伙伴们演示方便,我这里使用 GitHub 作为远程仓库,如果还有人不清楚 GitHub 和 Git 的区别,可以在公众号江南一点雨底部菜单栏查看 Git 教程,看完了就明白了。


从 2021.08.13 号开始,IDEA 上配置 GitHub 有一个小小的变化,即不能使用用户名密码的方式登录了,如果你尝试用用户名/密码的方式登录 GitHub 提交代码,会收到如下提示:


Support for password authentication was removed on August 13, 2021. 
Please use a personal access token instead.

在 IDEA 上使用用户名/密码的方法登录 GitHub 也会报如下错误:



需要我们点击右上角的 Use Token,使用令牌的方式登录 GitHub,令牌的生成方式如下:



  1. 网页上登录你的 GitHub 账号。

  2. 点击右上角,选择 Settings:




  1. 拉到最下方,选择左边的 Developer settings:




  1. 选择左边的 Personal access tokens,然后点击右上角的 Generate new token:




  1. 填一下基本信息,选一下权限即可(权限需要选择 repo 和 gist,其他根据自己的需求选择):




  1. 最后会生成一个令牌,拷贝到 IDEA 中即可,如下:




这就是基本配置。


小伙伴们在公司做开发,一般是不会将 GitHub 作为远程仓库的,那么这块根据自己实际情况来配置就行了。


2. clone


头一天上班,首先上来要先 clone 项目下来,IDEA 中有对应的 clone 工具,我们直接使用即可:




这块也可以直接选择下面的 GitHub,然后直接从自己的 GitHub 仓库上拉取新代码。


clone 完成之后,IDEA 会提示是否打开该项目,选择 yes 即可。


代码 clone 下来之后,就可以根据松哥前文介绍的 Git Flow 开始开发了。


3. 分支


假设我们先创建 develop 和 release 分支,创建方式如下,选中当前工程,右键单击,然后依次选择 Git->Repository->Branches...



或者依次点击顶部的 VCS->Git->Branches...



当然两个方式都比较麻烦,直接点击 IDEA 的右下角最为省事,也是最常用的办法,如下图:



选择 New Branch,然后创建新的分支,勾选上 Checkout 表示分支创建成功后,切换到该分支上,如下:



选择一个分支,然后点击 Checkout,可以切换到该分支上:



接下来我们把 develop 分支提交到远程仓库,如下:




我们没有修改代码,所以直接点击 Push 按钮提交即可。


提交完成后,develop 后面多了 origin 前缀,Remote Branches 中也多了 develop 分支,说明提交成功。



现在假设我们想从 develop 上拉一个名为 feature-login 的分支,来完成登录功能,如下:




从创建的日志中,我们能看到 feature-login 确实是来自 develop:



好啦,接下来我们就可以愉快的开启一天的工作啦~


feature-login 上的功能开发完成后,首先点击 IDEA 的右上角完成本地仓库的提交,如下图:




填入提交的 Message,下方也能看到不同版本的内容对比,点击右下角完成代码提交,注意这个只是提交到本地仓库。


由于我们并不会将 feature-login 提交到远程仓库,所以接下来我们要将 feature-login 合并到 develop 然后将最新的 develop push 到远程仓库,操作方式如下:



  1. 切换回 develop 分支。

  2. 选择 feature-login->Merge into Current 进行合并。



合并完成后,如需删除 feature-login 分支,也可以在 IDEA 日志中顺手删除:



不过上面介绍的合并是快速合并,即让 develop 的指针指向了 feature-login,很多时候我们可能需要加上 --no-ff 参数来合并,那么步骤如下:


从 feature-login 切换回 develop 分支,然后如下:




此时我们看一眼提交日志,如下:



从这日志中也可以看出,此时不是快速合并模式了!


最后,选择 develop->Push,将代码提交到远程仓库。


4. pull


在 IDEA 中,如需从远程仓库中更新代码,点击右上角的按钮即可,如下图:



好啦,这就是一个大致的流程。


当然 Git 博大精深,IDEA 中支持的功能也非常多,其他功能就需要小伙伴们自己来摸索了,有不明白的欢迎留言讨论。


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

求你别自己瞎写工具类了,Spring 自带的这些他不香吗?

断言 断言是一个逻辑判断,用于检查不应该发生的情况 Assert 关键字在 JDK1.4 中引入,可通过 JVM 参数-enableassertions开启 SpringBoot 中提供了 Assert 断言工具类,通常用于数据合法性检查 // ...
继续阅读 »

断言



  1. 断言是一个逻辑判断,用于检查不应该发生的情况

  2. Assert 关键字在 JDK1.4 中引入,可通过 JVM 参数-enableassertions开启

  3. SpringBoot 中提供了 Assert 断言工具类,通常用于数据合法性检查


// 要求参数 object 必须为非空(Not Null),否则抛出异常,不予放行  
// 参数 message 参数用于定制异常信息。  
void notNull(Object object, String message)  
// 要求参数必须空(Null),否则抛出异常,不予『放行』。  
// 和 notNull() 方法断言规则相反  
void isNull(Object object, String message)  
// 要求参数必须为真(True),否则抛出异常,不予『放行』。  
void isTrue(boolean expression, String message)  
// 要求参数(List/Set)必须非空(Not Empty),否则抛出异常,不予放行  
void notEmpty(Collection collection, String message)  
// 要求参数(String)必须有长度(即,Not Empty),否则抛出异常,不予放行  
void hasLength(String text, String message)  
// 要求参数(String)必须有内容(即,Not Blank),否则抛出异常,不予放行  
void hasText(String text, String message)  
// 要求参数是指定类型的实例,否则抛出异常,不予放行  
void isInstanceOf(Class type, Object obj, String message)  
// 要求参数 `subType` 必须是参数 superType 的子类或实现类,否则抛出异常,不予放行  
void isAssignable(Class superType, Class subType, String message)  

对象、数组、集合


ObjectUtils



  1. 获取对象的基本信息


// 获取对象的类名。参数为 null 时,返回字符串:"null"   
String nullSafeClassName(Object obj)  
// 参数为 null 时,返回 0  
int nullSafeHashCode(Object object)  
// 参数为 null 时,返回字符串:"null"  
String nullSafeToString(boolean[] array)  
// 获取对象 HashCode(十六进制形式字符串)。参数为 null 时,返回 0   
String getIdentityHexString(Object obj)  
// 获取对象的类名和 HashCode。 参数为 null 时,返回字符串:""   
String identityToString(Object obj)  
// 相当于 toString()方法,但参数为 null 时,返回字符串:""  
String getDisplayString(Object obj)  


  1. 判断工具


// 判断数组是否为空  
boolean isEmpty(Object[] array)  
// 判断参数对象是否是数组  
boolean isArray(Object obj)  
// 判断数组中是否包含指定元素  
boolean containsElement(Object[] array, Object element)  
// 相等,或同为 null时,返回 true  
boolean nullSafeEquals(Object o1, Object o2)  
/*  
判断参数对象是否为空,判断标准为:  
   Optional: Optional.empty()  
      Array: length == 0  
CharSequence: length == 0  
 Collection: Collection.isEmpty()  
        Map: Map.isEmpty()  
*/  
boolean isEmpty(Object obj)  


  1. 其他工具方法


// 向参数数组的末尾追加新元素,并返回一个新数组  
<A, O extends A> A[] addObjectToArray(A[] array, O obj)  
// 原生基础类型数组 --> 包装类数组  
Object[] toObjectArray(Object source)  

StringUtils



  1. 字符串判断工具


// 判断字符串是否为 null,或 ""。注意,包含空白符的字符串为非空  
boolean isEmpty(Object str)  
// 判断字符串是否是以指定内容结束。忽略大小写  
boolean endsWithIgnoreCase(String str, String suffix)  
// 判断字符串是否已指定内容开头。忽略大小写  
boolean startsWithIgnoreCase(String str, String prefix)   
// 是否包含空白符  
boolean containsWhitespace(String str)  
// 判断字符串非空且长度不为 0,即,Not Empty  
boolean hasLength(CharSequence str)  
// 判断字符串是否包含实际内容,即非仅包含空白符,也就是 Not Blank  
boolean hasText(CharSequence str)  
// 判断字符串指定索引处是否包含一个子串。  
boolean substringMatch(CharSequence str, int index, CharSequence substring)  
// 计算一个字符串中指定子串的出现次数  
int countOccurrencesOf(String str, String sub)  


  1. 字符串操作工具


// 查找并替换指定子串  
String replace(String inString, String oldPattern, String newPattern)  
// 去除尾部的特定字符  
String trimTrailingCharacter(String str, char trailingCharacter)   
// 去除头部的特定字符  
String trimLeadingCharacter(String str, char leadingCharacter)  
// 去除头部的空白符  
String trimLeadingWhitespace(String str)  
// 去除头部的空白符  
String trimTrailingWhitespace(String str)  
// 去除头部和尾部的空白符  
String trimWhitespace(String str)  
// 删除开头、结尾和中间的空白符  
String trimAllWhitespace(String str)  
// 删除指定子串  
String delete(String inString, String pattern)  
// 删除指定字符(可以是多个)  
String deleteAny(String inString, String charsToDelete)  
// 对数组的每一项执行 trim() 方法  
String[] trimArrayElements(String[] array)  
// 将 URL 字符串进行解码  
String uriDecode(String source, Charset charset)  


  1. 路径相关工具方法


// 解析路径字符串,优化其中的 “..”   
String cleanPath(String path)  
// 解析路径字符串,解析出文件名部分  
String getFilename(String path)  
// 解析路径字符串,解析出文件后缀名  
String getFilenameExtension(String path)  
// 比较两个两个字符串,判断是否是同一个路径。会自动处理路径中的 “..”   
boolean pathEquals(String path1, String path2)  
// 删除文件路径名中的后缀部分  
String stripFilenameExtension(String path)   
// 以 “. 作为分隔符,获取其最后一部分  
String unqualify(String qualifiedName)  
// 以指定字符作为分隔符,获取其最后一部分  
String unqualify(String qualifiedName, char separator)  

CollectionUtils



  1. 集合判断工具


// 判断 List/Set 是否为空  
boolean isEmpty(Collection<?> collection)  
// 判断 Map 是否为空  
boolean isEmpty(Map<?,?> map)  
// 判断 List/Set 中是否包含某个对象  
boolean containsInstance(Collection<?> collection, Object element)  
// 以迭代器的方式,判断 List/Set 中是否包含某个对象  
boolean contains(Iterator<?> iterator, Object element)  
// 判断 List/Set 是否包含某些对象中的任意一个  
boolean containsAny(Collection<?> source, Collection<?> candidates)  
// 判断 List/Set 中的每个元素是否唯一。即 List/Set 中不存在重复元素  
boolean hasUniqueObject(Collection<?> collection)  


  1. 集合操作工具


// 将 Array 中的元素都添加到 List/Set 中  
<E> void mergeArrayIntoCollection(Object array, Collection<E> collection)    
// 将 Properties 中的键值对都添加到 Map 中  
<K,V> void mergePropertiesIntoMap(Properties props, Map<K,V> map)  
// 返回 List 中最后一个元素  
<T> T lastElement(List<T> list)    
// 返回 Set 中最后一个元素  
<T> T lastElement(Set<T> set)   
// 返回参数 candidates 中第一个存在于参数 source 中的元素  
<E> E findFirstMatch(Collection<?> source, Collection<E> candidates)  
// 返回 List/Set 中指定类型的元素。  
<T> T findValueOfType(Collection<?> collection, Class<T> type)  
// 返回 List/Set 中指定类型的元素。如果第一种类型未找到,则查找第二种类型,以此类推  
Object findValueOfType(Collection<?> collection, Class<?>[] types)  
// 返回 List/Set 中元素的类型  
Class<?> findCommonElementType(Collection<?> collection)  

文件、资源、IO 流


FileCopyUtils



  1. 输入


// 从文件中读入到字节数组中  
byte[] copyToByteArray(File in)  
// 从输入流中读入到字节数组中  
byte[] copyToByteArray(InputStream in)  
// 从输入流中读入到字符串中  
String copyToString(Reader in)  


  1. 输出


// 从字节数组到文件  
void copy(byte[] in, File out)  
// 从文件到文件  
int copy(File in, File out)  
// 从字节数组到输出流  
void copy(byte[] in, OutputStream out)   
// 从输入流到输出流  
int copy(InputStream in, OutputStream out)   
// 从输入流到输出流  
int copy(Reader in, Writer out)  
// 从字符串到输出流  
void copy(String in, Writer out)  

ResourceUtils



  1. 从资源路径获取文件


// 判断字符串是否是一个合法的 URL 字符串。  
static boolean isUrl(String resourceLocation)  
// 获取 URL  
static URL getURL(String resourceLocation)   
// 获取文件(在 JAR 包内无法正常使用,需要是一个独立的文件)  
static File getFile(String resourceLocation)  


  1. Resource


// 文件系统资源 D:...  
FileSystemResource  
// URL 资源,如 file://... http://...  
UrlResource  
// 类路径下的资源,classpth:...  
ClassPathResource  
// Web 容器上下文中的资源(jar 包、war 包)  
ServletContextResource  

// 判断资源是否存在  
boolean exists()  
// 从资源中获得 File 对象  
File getFile()  
// 从资源中获得 URI 对象  
URI getURI()  
// 从资源中获得 URI 对象  
URL getURL()  
// 获得资源的 InputStream  
InputStream getInputStream()  
// 获得资源的描述信息  
String getDescription()  

StreamUtils



  1. 输入


void copy(byte[] in, OutputStream out)  
int copy(InputStream in, OutputStream out)  
void copy(String in, Charset charset, OutputStream out)  
long copyRange(InputStream in, OutputStream out, long start, long end)  


  1. 输出


byte[] copyToByteArray(InputStream in)  
String copyToString(InputStream in, Charset charset)  
// 舍弃输入流中的内容  
int drain(InputStream in)   

反射、AOP


ReflectionUtils



  1. 获取方法


// 在类中查找指定方法  
Method findMethod(Class<?> clazz, String name)   
// 同上,额外提供方法参数类型作查找条件  
Method findMethod(Class<?> clazz, String name, Class<?>... paramTypes)   
// 获得类中所有方法,包括继承而来的  
Method[] getAllDeclaredMethods(Class<?> leafClass)   
// 在类中查找指定构造方法  
Constructor<T> accessibleConstructor(Class<T> clazz, Class<?>... parameterTypes)   
// 是否是 equals() 方法  
boolean isEqualsMethod(Method method)   
// 是否是 hashCode() 方法   
boolean isHashCodeMethod(Method method)   
// 是否是 toString() 方法  
boolean isToStringMethod(Method method)   
// 是否是从 Object 类继承而来的方法  
boolean isObjectMethod(Method method)   
// 检查一个方法是否声明抛出指定异常  
boolean declaresException(Method method, Class<?> exceptionType)   


  1. 执行方法


// 执行方法  
Object invokeMethod(Method method, Object target)    
// 同上,提供方法参数  
Object invokeMethod(Method method, Object target, Object... args)   
// 取消 Java 权限检查。以便后续执行该私有方法  
void makeAccessible(Method method)   
// 取消 Java 权限检查。以便后续执行私有构造方法  
void makeAccessible(Constructor<?> ctor)   


  1. 获取字段


// 在类中查找指定属性  
Field findField(Class<?> clazz, String name)   
// 同上,多提供了属性的类型  
Field findField(Class<?> clazz, String name, Class<?> type)   
// 是否为一个 "public static final" 属性  
boolean isPublicStaticFinal(Field field)   


  1. 设置字段


// 获取 target 对象的 field 属性值  
Object getField(Field field, Object target)   
// 设置 target 对象的 field 属性值,值为 value  
void setField(Field field, Object target, Object value)   
// 同类对象属性对等赋值  
void shallowCopyFieldState(Object src, Object dest)  
// 取消 Java 的权限控制检查。以便后续读写该私有属性  
void makeAccessible(Field field)   
// 对类的每个属性执行 callback  
void doWithFields(Class<?> clazz, ReflectionUtils.FieldCallback fc)   
// 同上,多了个属性过滤功能。  
void doWithFields(Class<?> clazz, ReflectionUtils.FieldCallback fc,   
                 ReflectionUtils.FieldFilter ff)   
// 同上,但不包括继承而来的属性  
void doWithLocalFields(Class<?> clazz, ReflectionUtils.FieldCallback fc)   

AopUtils



  1. 判断代理类型


// 判断是不是 Spring 代理对象  
boolean isAopProxy()  
// 判断是不是 jdk 动态代理对象  
isJdkDynamicProxy()  
// 判断是不是 CGLIB 代理对象  
boolean isCglibProxy()  


  1. 获取被代理对象的 class


// 获取被代理的目标 class  
Class<?> getTargetClass()  

AopContext



  1. 获取当前对象的代理对象


Object currentProxy()

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

使用MyBatis拦截器后,摸鱼时间又长了。?

场景 在后端服务开发时,现在很流行的框架组合就是SSM(SpringBoot + Spring + MyBatis),在我们进行一些业务系统开发时,会有很多的业务数据表,而表中的信息从新插入开始,整个生命周期过程中可能会进行很多次的操作。 比如,我们在某网站购...
继续阅读 »

场景


在后端服务开发时,现在很流行的框架组合就是SSM(SpringBoot + Spring + MyBatis),在我们进行一些业务系统开发时,会有很多的业务数据表,而表中的信息从新插入开始,整个生命周期过程中可能会进行很多次的操作。


比如,我们在某网站购买一件商品,会生成一条订单记录,在支付完金额后订单状态会变为已支付,等最后我们收到订单商品,这个订单状态会变成已完成等。


假设我们的订单表t_order结果如下:



当订单创建时,需要设置insert_byinsert_timeupdate_byupdate_time的值;


在进行订单状态更新时,则只需要更新update_byupdate_time的值。


那应该如何处理呢?


麻瓜做法


最简单的做法,也是最容易想到的做法,就是在每个业务处理的代码中,对相关的字段进行处理。


比如订单创建的方法中,如下处理:


public void create(Order order){
// ...其他代码
// 设置审计字段
Date now = new Date();
order.setInsertBy(appContext.getUser());
order.setUpdateBy(appContext.getUser());
order.setInsertTime(now);
order.setUpdateTime(now);
orderDao.insert(order);
}

订单更新方法则只设置updateByupdateTime


public void update(Order order){
// ...其他代码

// 设置审计字段
Date now = new Date();
order.setUpdateBy(appContext.getUser());
order.setUpdateTime(now);
orderDao.insert(order);
}

这种方式虽然可以完成功能,但是存在一些问题:



  • 需要在每个方法中按照不同的业务逻辑决定设置哪些字段;

  • 在业务模型变多后,每个模型的业务方法中都要进行设置,重复代码太多。


那我们知道这种方式存在问题以后,就得找找有什么好方法对不对,往下看!


优雅做法


因为我们持久层框架更多地使用MyBatis,那我们就借助于MyBatis的拦截器来完成我们的功能。


首先我们来了解一下,什么是拦截器?


什么是拦截器?


MyBatis的拦截器顾名思义,就是对某些操作进行拦截。通过拦截器可以对某些方法执行前后进行拦截,添加一些处理逻辑。


MyBatis的拦截器可以对Executor、StatementHandler、PameterHandler和ResultSetHandler 接口进行拦截,也就是说会对这4种对象进行代理。


拦截器设计的初衷就是为了让用户在MyBatis的处理流程中不必去修改MyBatis的源码,能够以插件的方式集成到整个执行流程中。


比如MyBatis中的ExecutorBatchExecutorReuseExecutorSimpleExecutorCachingExecutor,如果这几种实现的query方法都不能满足你的需求,我们可以不用去直接修改MyBatis的源码,而通过建立拦截器的方式,拦截Executor接口的query方法,在拦截之后,实现自己的query方法逻辑。


在MyBatis中的拦截器通过Interceptor接口表示,该接口中有三个方法。


public interface Interceptor {

Object intercept(Invocation invocation) throws Throwable;

Object plugin(Object target);

void setProperties(Properties properties);

}

plugin方法是拦截器用于封装目标对象的,通过该方法我们可以返回目标对象本身,也可以返回一个它的代理。


当返回的是代理的时候我们可以对其中的方法进行拦截来调用intercept方法,当然也可以调用其他方法。


setProperties方法是用于在Mybatis配置文件中指定一些属性的。


使用拦截器更新审计字段


那么我们应该如何通过拦截器来实现我们对审计字段赋值的功能呢?


在我们进行订单创建和修改时,本质上是通过MyBatis执行insert、update语句,MyBatis是通过Executor来处理的。


我们可以通过拦截器拦截Executor,然后在拦截器中对要插入的数据对象根据执行的语句设置insert_by,insert_time,update_by,update_time等属性值就可以了。


自定义拦截器


自定义Interceptor最重要的是要实现plugin方法和intercept方法。


plugin方法中我们可以决定是否要进行拦截进而决定要返回一个什么样的目标对象。


intercept方法就是要进行拦截的时候要执行的方法。


对于plugin方法而言,其实Mybatis已经为我们提供了一个实现。Mybatis中有一个叫做Plugin的类,里面有一个静态方法wrap(Object target,Interceptor interceptor),通过该方法可以决定要返回的对象是目标对象还是对应的代理。


但是这里还存在一个问题,就是我们如何在拦截器中知道要插入的表有审计字段需要处理呢?


因为我们的表中并不是所有的表都是业务表,可能有一些字典表或者定义表是没有审计字段的,这样的表我们不需要在拦截器中进行处理。


也就是说我们要能够区分出哪些对象需要更新审计字段


这里我们可以定义一个接口,让需要更新审计字段的模型都统一实现该接口,这个接口起到一个标记的作用。


public interface BaseDO {
}

public class Order implements BaseDO{

private Long orderId;

private String orderNo;

private Integer orderStatus;

private String insertBy;

private String updateBy;

private Date insertTime;

private Date updateTime;
//... getter ,setter
}

接下来,我们就可以实现我们的自定义拦截器了。


@Component("ibatisAuditDataInterceptor")
@Intercepts({@Signature(method = "update", type = Executor.class, args = {MappedStatement.class, Object.class})})
public class IbatisAuditDataInterceptor implements Interceptor {

private Logger logger = LoggerFactory.getLogger(IbatisAuditDataInterceptor.class);

@Override
public Object intercept(Invocation invocation) throws Throwable {
// 从上下文中获取用户名
String userName = AppContext.getUser();

Object[] args = invocation.getArgs();
SqlCommandType sqlCommandType = null;

for (Object object : args) {
// 从MappedStatement参数中获取到操作类型
if (object instanceof MappedStatement) {
MappedStatement ms = (MappedStatement) object;
sqlCommandType = ms.getSqlCommandType();
logger.debug("操作类型: {}", sqlCommandType);
continue;
}
// 判断参数是否是BaseDO类型
// 一个参数
if (object instanceof BaseDO) {
if (SqlCommandType.INSERT == sqlCommandType) {
Date insertTime = new Date();
BeanUtils.setProperty(object, "insertedBy", userName);
BeanUtils.setProperty(object, "insertTimestamp", insertTime);
BeanUtils.setProperty(object, "updatedBy", userName);
BeanUtils.setProperty(object, "updateTimestamp", insertTime);
continue;
}
if (SqlCommandType.UPDATE == sqlCommandType) {
Date updateTime = new Date();
BeanUtils.setProperty(object, "updatedBy", userName);
BeanUtils.setProperty(object, "updateTimestamp", updateTime);
continue;
}
}
// 兼容MyBatis的updateByExampleSelective(record, example);
if (object instanceof ParamMap) {
logger.debug("mybatis arg: {}", object);
@SuppressWarnings("unchecked")
ParamMap<Object> parasMap = (ParamMap<Object>) object;
String key = "record";
if (!parasMap.containsKey(key)) {
continue;
}
Object paraObject = parasMap.get(key);
if (paraObject instanceof BaseDO) {
if (SqlCommandType.UPDATE == sqlCommandType) {
Date updateTime = new Date();
BeanUtils.setProperty(paraObject, "updatedBy", userName);
BeanUtils.setProperty(paraObject, "updateTimestamp", updateTime);
continue;
}
}
}
// 兼容批量插入
if (object instanceof DefaultSqlSession.StrictMap) {
logger.debug("mybatis arg: {}", object);
@SuppressWarnings("unchecked")
DefaultSqlSession.StrictMap<ArrayList<Object>> map = (DefaultSqlSession.StrictMap<ArrayList<Object>>) object;
String key = "collection";
if (!map.containsKey(key)) {
continue;
}
ArrayList<Object> objs = map.get(key);
for (Object obj : objs) {
if (obj instanceof BaseDO) {
if (SqlCommandType.INSERT == sqlCommandType) {
Date insertTime = new Date();
BeanUtils.setProperty(obj, "insertedBy", userName);
BeanUtils.setProperty(obj, "insertTimestamp", insertTime);
BeanUtils.setProperty(obj, "updatedBy", userName);
BeanUtils.setProperty(obj, "updateTimestamp", insertTime);
}
if (SqlCommandType.UPDATE == sqlCommandType) {
Date updateTime = new Date();
BeanUtils.setProperty(obj, "updatedBy", userName);
BeanUtils.setProperty(obj, "updateTimestamp", updateTime);
}
}
}
}
}
return invocation.proceed();
}

@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}

@Override
public void setProperties(Properties properties) {
}
}

通过上面的代码可以看到,我们自定义的拦截器IbatisAuditDataInterceptor实现了Interceptor接口。


在我们拦截器上的@Intercepts注解,type参数指定了拦截的类是Executor接口的实现,method 参数指定拦截Executor中的update方法,因为数据库操作的增删改操作都是通过update方法执行。


配置拦截器插件


在定义好拦截器之后,需要将拦截器指定到SqlSessionFactoryBeanplugins中才能生效。所以要按照如下方式配置。


<bean id="transSqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="transDataSource" />
<property name="mapperLocations">
<array>
<value>classpath:META-INF/mapper/*.xml</value>
</array>
</property>
<property name="plugins">
<array>
<!-- 处理审计字段 -->
<ref bean="ibatisAuditDataInterceptor" />
</array>
</property>

到这里,我们自定义的拦截器就生效了,通过测试你会发现,不用在业务代码中手动设置审计字段的值,会在事务提交之后,通过拦截器插件自动对审计字段进行赋值。


小结


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

当Synchronized遇到这玩意儿,有个大坑,要注意!

你好呀,我是歪歪。 前几天在某技术平台上看到别人提的关于 Synchronized 的一个用法问题,我觉得挺有意思的,这个问题其实也是我三年前面试某公司的时候遇到的一个真题,当时不知道面试官想要考什么,没有回答的特别好,后来研究了一下就记住了。 所以看到这个问...
继续阅读 »

你好呀,我是歪歪。


前几天在某技术平台上看到别人提的关于 Synchronized 的一个用法问题,我觉得挺有意思的,这个问题其实也是我三年前面试某公司的时候遇到的一个真题,当时不知道面试官想要考什么,没有回答的特别好,后来研究了一下就记住了。


所以看到这个问题的时候觉得特别亲切,准备分享给你一起看看:



首先为了方便你看文章的时候复现问题,我给你一份直接拿出来就能跑的代码,希望你有时间的话也把代码拿出来跑一下:


public class SynchronizedTest {

    public static void main(String[] args) {
        Thread why = new Thread(new TicketConsumer(10), "why");
        Thread mx = new Thread(new TicketConsumer(10), "mx");
        why.start();
        mx.start();
    }
}

class TicketConsumer implements Runnable {

    private volatile static Integer ticket;

    public TicketConsumer(int ticket) {
        this.ticket = ticket;
    }

    @Override
    public void run() {
        while (true) {
            System.out.println(Thread.currentThread().getName() + "开始抢第" + ticket + "张票,对象加锁之前:" + System.identityHashCode(ticket));
            synchronized (ticket) {
                System.out.println(Thread.currentThread().getName() + "抢到第" + ticket + "张票,成功锁到的对象:" + System.identityHashCode(ticket));
                if (ticket > 0) {
                    try {
                        //模拟抢票延迟
                        TimeUnit.SECONDS.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "抢到了第" + ticket-- + "张票,票数减一");
                } else {
                    return;
                }
            }
        }
    }
}

程序逻辑也很简单,是一个模拟抢票的过程,一共 10 张票,开启两个线程去抢票。


票是共享资源,且有两个线程来消费,所以为了保证线程安全,TicketConsumer 的逻辑里面用了 synchronized 关键字。


这是应该是大家在初学 synchronized 的时候都会写到的例子,期望的结果是 10 张票,两个人抢,每张票只有一个人能抢到。


但是实际运行结果是这样的,我只截取开始部分的日志:



截图里面有三个框起来的部分。


最上面的部分,就是两个人都在抢第 10 张票,从日志输出上看也完全没有任何毛病,最终只有一个人抢到了票,然后进入到第 9 张票的争夺过程。


但是下面被框起来的第 9 张票的争夺部分就有点让人懵逼了:


why抢到第9张票,成功锁到的对象:288246497
mx抢到第9张票,成功锁到的对象:288246497

为什么两个人都抢到了第 9 张票,且成功锁到的对象都一样的?


这玩意,超出认知了啊。


这两个线程怎么可能拿到同一把锁,然后去执行业务逻辑呢?


所以,提问者的问题就浮现出来了。



  • 1.为什么 synchronized 没有生效?

  • 2.为什么锁对象 System.identityHashCode 的输出是一样的?


为什么没有生效?


我们先来看一个问题。


首先,我们从日志的输出上已经非常明确的知道,synchronized 在第二轮抢第 9 张票的时候失效了。


经过理论知识支撑,我们知道 synchronized 失效,肯定是锁出问题了。


如果只有一把锁,多个线程来竞争同一把锁,synchronized 绝对是不会有任何毛病的。


但是这里两个线程并没有达成互斥的条件,也就是说这里绝对存在的不止一把锁。


这是我们可以通过理论知识推导出来的结论。



先得出结论了,那么我怎么去证明“锁不止一把”呢?


能进入 synchronized 说明肯定获得了锁,所以我只要看各个线程持有的锁是什么就知道了。


那么怎么去看线程持有什么锁呢?


jstack 命令,打印线程堆栈功能,了解一下?


这些信息都藏在线程堆栈里面,我们拿出来一看便知。


在 idea 里面怎么拿到线程堆栈呢?


这就是一个在 idea 里面调试的小技巧了,我之前的文章里面应该也出现过多次。


首先为了方便获取线程堆栈信息,我把这里的睡眠时间调整到 10s:



跑起来之后点击这里的“照相机”图标:



点击几次就会有对应点击时间点的几个 Dump 信息



由于我需要观察前两次锁的情况,而每次线程进入锁之后都会等待 10s 时间,所以我就在项目启动的第一个 10s 和第二个 10s 之间各点击一次就行。


为了更直观的观察数据,我选择点击下面这个图标,把 Dump 信息复制下来:



复制下来的信息很多,但是我们只需要关心 why 和 mx 这两个线程即可。


这是第一次 Dump 中的相关信息:



mx 线程是 BLOCKED 状态,它在等待地址为 0x000000076c07b058 的锁。


why 线程是 TIMED_WAITING 状态,它在 sleeping,说明它抢到了锁,在执行业务逻辑。而它抢到的锁,你说巧不巧,正是 mx 线程等待的 0x000000076c07b058。


从输出日志上来看,第一次抢票确实是 why 线程抢到了:



从 Dump 信息看,两个线程竞争的是同一把锁,所以第一次没毛病。


好,我们接着看第二次的 Dump 信息:



这一次,两个线程都在 TIMED_WAITING,都在 sleeping,说明都拿到了锁,进入了业务逻辑。


但是仔细一看,两个线程拿的锁是不相同的锁。


mx 锁的是 0x000000076c07b058。


why 锁的是 0x000000076c07b048。


由于不是同一把锁,所以并不存在竞争关系,因此都可以进入 synchronized 执行业务逻辑,所以两个线程都在 sleeping,也没毛病。


然后,我再把两次 Dump 的信息放在一起给你看一下,这样就更直观了:



如果我用“锁一”来代替 0x000000076c07b058,“锁二”来代替 0x000000076c07b048。


那么流程是这样的:


why 加锁一成功,执行业务逻辑,mx 进入锁一等待状态。


why 释放锁一,等待锁一的 mx 被唤醒,持有锁一,继续执行业务。


同时 why 加锁二成功,执行业务逻辑。


从线程堆栈中,我们确实证明了 synchronized 没有生效的原因是锁发生了变化。


同时,从线程堆栈中我们也能看出来为什么锁对象 System.identityHashCode 的输出是一样的。



第一次 Dump 的时候,ticket 都是 10,其中 mx 没有抢到锁,被 synchronized 锁住。


why 线程执行了 ticket-- 操作,ticket 变成了 9,但是此时 mx 线程被锁住的 monitor 还是 ticket=10 这个对象,它还在 monitor 的 _EntryList 里面等着的,并不会因为 ticket 的变化而变化。


所以,当 why 线程释放锁之后,mx 线程拿到锁继续执行,发现 ticket=9。


而 why 也搞到一把新锁,也可以进入 synchronized 的逻辑,也发现 ticket=9。


好家伙,ticket 都是 9, System.identityHashCode 能不一样吗?


按理来说,why 释放锁一后应该继续和 mx 竞争锁一,但是却不知道它在哪搞到一把新锁。


那么问题就来了:锁为什么发生了变化呢?



谁动了我的锁?


经过前面一顿分析,我们坐实了锁确实发生了变化,当你分析出这一点的时候勃然大怒,拍案而起,大喊一声:是哪个瓜娃子动了我的锁?这不是坑爹吗?



按照我的经验,这个时候不要急着甩锅,继续往下看,你会发现小丑竟是自己:



抢完票之后,执行了 ticket-- 的操作,而这个 ticket 不就是你的锁对象吗?


这个时候你把大腿一拍,恍然大悟,对着围观群众说:问题不大,手抖而已。


于是大手一挥,把加锁的地方改成这样:


synchronized (TicketConsumer.class)

利用 class 对象来作为锁对象,保证了锁的唯一性。


经过验证也确实没毛病,非常完美,打完收工。


但是,真的就收工了吗?



其实关于锁对象为什么发生了变化,还隔了一点点东西没有说出来。


它就藏在字节码里面。


我们通过 javap 命令,反查字节码,可以看到这样的信息:



Integer.valueOf 这是什么玩意?



让人熟悉的 Integer 从 -128 到 127 的缓存。


也就是说我们的程序里面,会涉及到拆箱和装箱的过程,这个过程中会调用到 Integer.valueOf 方法。具体其实就是 ticket-- 的这个操作。


对于 Integer,当值在缓存范围内的时候,会返回同一个对象。当超过缓存范围,每次都会 new 一个新对象出来。


这应该是一个必备的八股文知识点,我在这里给你强调这个是想表达什么意思呢?


很简单,改动一下代码就明白了。


我把初始化票数从 10 修改为 200,超过缓存范围,程序运行结果是这样的:



很明显,从第一次的日志输出来看,锁都不是同一把锁了。


这就是我前面说的:因为超过缓存范围,执行了两次 new Integer(200) 的操作,这是两个不同的对象,拿来作为锁,就是两把不一样的锁。


再修改回 10,运行一次,你感受一下:



从日志输出来看,这个时候只有一把锁,所以只有一个线程抢到了票。


因为 10 是在缓存范围内的数字,所以每次是从缓存中获取出来,是同一个对象。


我写这一小段的目的是为了体现 Integer 有缓存这个知识点,大家都知道。但是当它和其他东西揉在一起的时候因为这个缓存会带来什么问题,你得分析出来,这比直接记住干瘪的知识点有效一点。


但是...


我们的初始票是 10,ticket-- 之后票变成了 9,也是在缓存范围内的呀,怎么锁就变了呢?


如果你有这个疑问的话,那么我劝你再好好想想。


10 是 10,9 是 9。


虽然它们都在缓存范围内,但是本来就是两个不同的对象,构建缓存的时候也是 new 出来的:



为什么我要补充这一段看起来很傻的说明呢?


因为我在网上看到其他写类似问题的时候,有的文章写的不清楚,会让读者误认为“缓存范围内的值都是同一个对象”,这样会误导初学者。


总之一句话:请别用 Integer 作为锁对象,你把握不住。


但是...



stackoverflow


但是,我写文章的时候在 stackoverflow 上也看到了一个类似的问题。


这个哥们的问题在于:他知道 Integer 不能做为锁对象,但是他的需求又似乎必须把 Integer 作为锁对象。



stackoverflow.com/questions/6…




我给你描述一下他的问题。


首先看标号为 ① 的地方,他的程序其实就是先从缓存中获取,如果缓存中没有则从数据库获取,然后在放到缓存里面去。


非常简单清晰的逻辑。


但是他考虑到并发的场景下,如果有多个线程同一时刻都来获取同一个 id,但是这个 id 对应的数据并没有在缓存里面,那么这些线程都会去执行查询数据库并维护缓存的动作。


对应查询和存储的动作,他用的是 fairly expensive 来形容。


就是“相当昂贵”的意思,说白了就是这个动作非常的“重”,最好不要重复去做。


所以只需要让某一个线程来执行这个 fairly expensive 的操作就好了。


于是他想到了标号为 ② 的地方的代码。


用 synchronized 来把 id 锁一下,不幸的是,id 是 Integer 类型的。


在标号为 ③ 的地方他自己也说了:不同的 Integer 对象,它们并不会共享锁,那么 synchronized 也没啥卵用。


其实他这句话也不严谨,经过前面的分析,我们知道在缓存范围内的 Integer 对象,它们还是会共享同一把锁的,这里说的“共享”就是竞争的意思。


但是很明显,他的 id 范围肯定比 Integer 缓存范围大。


那么问题就来了:这玩意该咋搞啊?


我看到这个问题的时候想到的第一个问题是:上面这个需求我好像也经常做啊,我是怎么做的来着?


想了几秒恍然大悟,哦,现在都是分布式应用了,我特么直接用的是 Redis 做锁呀。


根本就没有考虑过这个问题。


如果现在不让用 Redis,就是单体应用,那么怎么解决呢?


在看高赞回答之前,我们先看看这个问题下面的一个评论:



开头三个字母:FYI。


看不懂没关系,因为这个不是重点。


但是你知道的,我的英语水平 very high,所以我也顺便教点英文。


FYI,是一个常用的英文缩写,全称是 for your information,供参考的意思。


所以你就知道,他后面肯定是给你附上一个资料了,翻译过来就是: Brian Goetz 在他的 Devoxx 2018 演讲中提到,我们不应该把 Integer 作为锁。



你可以通过这个链接直达这一部分的讲解,只有不到 30s秒的时间,随便练练听力:http://www.youtube.com/watch?v=4r2…



那么问题又来了?


Brian Goetz 是谁,凭什么他说的话看起来就很权威的样子?



Java Language Architect at Oracle,开发 Java 语言的,就问你怕不怕。


同时,他还是我多次推荐过的《Java并发编程实践》这本书的作者。


好了,现在也找到大佬背书了,接下来带你看看高赞回答是怎么说的。



前部分就不详说了,其实就是我们前面提到的那一些点,不能用 Integer ,涉及到缓存内、缓存外巴拉巴拉的...


关注划线的部分,我加上自己的理解给你翻译一下:


如果你真的必须用 Integer 作为锁,那么你需要搞一个 Map 或 Integer 的 Set,通过集合类做映射,你就可以保证映射出来的是你想要的明确的一个实例。而这个实例,就那可以拿来做锁。


然后他给出了这样的代码片段:



就是用 ConcurrentHashMap 然后用 putIfAbsent 方法来做一个映射。


比如多次调用 locks.putIfAbsent(200, 200),在 map 里面也只有一个值为 200 的 Integer 对象,这是 map 的特性保证的,无需过多解释。


但是这个哥们很好,为了防止有人转不过这个弯,他又给大家解释了一下。


首先,他说你也可以这样的写:



但这样一来,你就会多产生一个很小成本,就是每次访问的时候,如果这个值没有被映射,你都会创建一个 Object 对象。


为了避免这一点,他只是把整数本身保存在 Map 中。这样做的目的是什么?这与直接使用整数本身有什么不同呢?


他是这样解释的,其实就是我前面说的“这是 map 的特性保证的”:



当你从 Map 中执行 get() 时,会用到 equals() 方法比较键值。


两个相同值的不同 Integer 实例,调用 equals() 方法是会判定为相同的 。



因此,你可以传递任何数量的 "new Integer(5)" 的不同 Integer 实例作为 getCacheSyncObject 的参数,但是你将永远只能得到传递进来的包含该值的第一个实例。


就是这个意思:



汇总一句话:就是通过 Map 做了映射,不管你 new 多少个 Integer 出来,这多个 Integer 都会被映射为同一个 Integer,从而保证即使超出 Integer 缓存范围时,也只有一把锁。


除了高赞回答之外,还有两个回答我也想说一下。


第一个是这个:



不用关心他说的内容是什么,只是我看到这句话翻译的时候虎躯一震:



skin this cat ???


太残忍了吧。



我当时就觉得这个翻译肯定不太对,这肯定是一个小俚语。于是考证了一下,原来是这个意思:



免费送你一个英语小知识,不用客气。


第二个应该关注的回答排在最后:



这个哥们叫你看看《Java并发编程实战》的第 5.6 节的内容,里面有你要寻找的答案。


巧了,我手边就有这本书,于是我翻开看了一眼。


第 5.6 节的名称叫做“构建高效且可伸缩的结果缓存”:



好家伙,我仔细一看这一节,发现这是宝贝呀。


你看书里面的示例代码:



不就和提问题的这个哥们的代码如出一辙吗?



都是从缓存中获取,拿不到再去构建。


不同的地方在于书上把 synchronize 加在了方法上。但是书上也说了,这是最差的解决方案,只是为了引出问题。


随后他借助了 ConcurrentHashMap、putIfAbsent 和 FutureTask 给出了一个相对较好的解决方案。


你可以看到完全是从另外一个角度去解决问题的,根本就没有在 synchronize 上纠缠,直接第二个方法就拿掉了 synchronize。


看完书上的方案后我才恍然大悟:好家伙,虽然前面给出的方案可以解决这个问题,但是总感觉怪怪的,又说不出来哪里怪。原来是死盯着 synchronize 不放,思路一开始就没打开啊。


书里面一共给出了四段代码,解决方案层层递进,具体是怎么写的,由于书上已经写的很清楚了,我就不赘述了,大家去翻翻书就行了。


没有书的直接在网上搜“构建高效且可伸缩的结果缓存”也能搜出原文。


我就指个路,看去吧。


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

Flutter之GetX依赖注入Bindings使用详解

作用Bindings 主要配合 GetX 路由和依赖一起使用,作用是在路由跳转页面加载时注入当前页面所需的依赖关系。Bindings 的好处是能统一管理页面的依赖关系,当业务复杂时可能一个页面需要注入大量的依赖,此时使用 Bindings 能更方便的维护页面的...
继续阅读 »

作用

Bindings 主要配合 GetX 路由和依赖一起使用,作用是在路由跳转页面加载时注入当前页面所需的依赖关系。Bindings 的好处是能统一管理页面的依赖关系,当业务复杂时可能一个页面需要注入大量的依赖,此时使用 Bindings 能更方便的维护页面的依赖关系。

使用

前面说了 Bindings 需要结合 GetX 路由一起使用,而 GetX 路由分为普通路由别名路由,接下来分别看看如何使用。

首选创建一个自定义 Bindings 继承自 Bindings,比如计数器界面,创建一个 CounterBindings 在 dependencies 方法中注入 CounterController, 代码如下:

class CounterBinding extends Bindings {
@override
void dependencies() {
Get.lazyPut(() => CounterController());
}
}

上面通过 lazyPut 懒加载方式注入的,也可以使用前面讲到的其他注入方式注入。

普通路由

普通路由使用 Bindings 很简单,在路由跳转时加上 binding 参数传入创建的自定义 Bindings 对象即可:

Get.to(CounterPage(), binding: CounterBinding());

Get.off(CounterPage(), binding: CounterBinding());

Get.offAll(CounterPage(), binding: CounterBinding());

这样通过路由进入 CounterPage 时就会自动调用 CounterBinding 的 dependencies 方法初始化注入对应的依赖,在 CounterPage 中就能正常使用 Get.find 获取到注入的 CounterController 对象。

别名路由

Flutter应用框架搭建(一)GetX集成及使用详解 一文中介绍了别名路由的使用,需要先创建 GetPage 确定别名与页面的关系并配置到 GetMaterialApp 的 getPages 中,使用时通过 Get.toNamed 进行路由跳转,而 Get.toNamed 方法并没有 binding 参数用于传入 Bindings。

使用别名路由时需要在创建 GetPage 时就传入 Bindings 对象,如下:

GetPage(name: "/counter", page: () => CounterPage(), binding: CounterBinding());

跳转时正常使用 Get.toNamed 就能达到同样的效果。

Get.toNamed("/counter");

别名路由与普通路由对于 Bindings 的使用上还有一个区别,普通路由只有一个 binding 参数,只能传入一个 Bindings 对象,而别名路由除了 binding 参数以外还有一个 bindings 参数,可传入 Bindings 数组。使用如下:

GetPage(
name: "/counter",
page: () => CounterPage(),
binding: CounterBinding(),
bindings: [PageABinding(), PageBBinding(), PageCBinding()]);

那 bindings 的作用是什么呢?为什么需要传入一个数组?

通常一个页面只需要一个 Bindings 用来管理页面的依赖,但是当使用到 ViewPager 等嵌套组件或者存在页面嵌套时,因为页面中嵌套的页面不是通过路由加载出来的所以无法自动调用 Bindings 的 dependencies 方法来初始化依赖关系,而嵌套的页面有可能也需要单独显示,为了提高页面的复用性也会为嵌套页面创建 Bindings ,这样当页面嵌套使用时就可以把嵌套页面的 Bindings 传入到主页面路由的 bindings 中,使用如下:

/// ViewPager 页面路由
GetPage(
name: "/viewpager",
page: () => ViewPagerPage(),
binding: ViewPagerBinding(),
bindings: [PageABinding(), PageBBinding(), PageCBinding()]);

/// 单独 PageA pageB pageC 路由
GetPage(
name: "/pageA",
page: () => PageAPage(),
binding: PageABinding(),);
GetPage(
name: "/pageB",
page: () => PageBPage(),
binding: PageBBinding(),);
GetPage(
name: "/pageC",
page: () => PageCPage(),
binding: PageCBinding(),);

/// 使用
Get.toNamed("/viewpager");

Get.toNamed("/pageA");
Get.toNamed("/pageB");
Get.toNamed("/pageC");

这样就能实现,当在 ViewPager 中使用时也能初始化 ViewPager 中嵌套页面的依赖,单独使用某个 Page 时也能正常加载依赖。

原理

前面讲了 Bindings 的作用和使用方法,下面通过源码简单分析一下 Bindings 的原理。

Bindings 是一个抽象类,只有一个 dependencies 抽象方法,源码如下:

abstract class Bindings {
void dependencies();
}

在页面路由中注册 Bindings 后,页面初始化时会调用 Bindings 的 dependencies 方法,初始化页面依赖,其调用是在 GetPageRoute 的 buildContent 中,而 GetPageRoute 是继承至 Flutter 的 PageRoute 即在路由跳转加载页面内容时调用, 核心源码如下:

Widget _getChild() {
if (_child != null) return _child!;
final middlewareRunner = MiddlewareRunner(middlewares);

/// 获取 Bindings
final localbindings = [
if (bindings != null) ...bindings!,
if (binding != null) ...[binding!]
];
/// 调用中间件的 onBindingsStart 方法
final bindingsToBind = middlewareRunner.runOnBindingsStart(localbindings);

/// 调用 Bindings 的 dependencies 方法
if (bindingsToBind != null) {
for (final binding in bindingsToBind) {
binding.dependencies();
}
}

final pageToBuild = middlewareRunner.runOnPageBuildStart(page)!;
_child = middlewareRunner.runOnPageBuilt(pageToBuild());
return _child!;
}

@override
Widget buildContent(BuildContext context) {
return _getChild();
}

源码核心代码就是在创建页面 Widget 时获取路由传入的 Bindings ,然后依次调用 Bindings 的 dependencies 方法。

其中:

  /// 获取 Bindings
final localbindings = [
if (bindings != null) ...bindings!,
if (binding != null) ...[binding!]
];
/// 调用中间件的 onBindingsStart 方法
final bindingsToBind = middlewareRunner.runOnBindingsStart(localbindings);

/// 调用 Bindings 的 dependencies 方法
if (bindingsToBind != null) {
for (final binding in bindingsToBind) {
binding.dependencies();
}
}

就是将路由中传入的 bindings 和 binding 取出放入同一个数组。然后依次调用 dependencies 方法,其中 binding 就是路由或 GetPage 中传入的 binding 参数,而 bindings 就是使用别名路由时在 ``GetPage 中传入的 Bindings 数组。

总结

本文通过介绍在 GetX 依赖注入中 Bindings 的作用以及使用方法,再结合 GetX 的源码分析了 Bindings 的实现原理,更进一步了解了 Bindings 为什么能实现页面依赖注入的管理,希望通过源码让大家更好的理解 GetX 中的 Bindings ,从而在开发中灵活使用 Bindings 管理页面所需的依赖。


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

收起阅读 »

解决 Flutter 嵌套过深,是选择函数还是自定义类组件?

前言 初学 Flutter 的时候,一个很大的感受就是组件嵌套层级很深,写下来的代码找对应的括号都找不到。比如下面这种情况,从最外层的 Scaffold 到最里层的 Image.asset,一共有7层组件嵌套。这还不算多的,最夸张是见过一个表单页面写了10多层...
继续阅读 »

前言


初学 Flutter 的时候,一个很大的感受就是组件嵌套层级很深,写下来的代码找对应的括号都找不到。比如下面这种情况,从最外层的 Scaffold 到最里层的 Image.asset,一共有7层组件嵌套。这还不算多的,最夸张是见过一个表单页面写了10多层,代码的阅读体验非常糟糕,而且如果不小心删除了一个括号要找半天才对应得上。当然,通过 VSCode 彩虹括号(Rainbow Brackets)这个插件能够一定程度上解决括号对称查找得问题,但是代码的可维护性、阅读体验还是很差。自然而然,大家会想到拆分。拆分有两种方式,一种是使用返回Widget 的函数,另一种是使用 StatelessWidget,那这两种该如何选择呢?


image.png


拆分原则


在关于这个问题的讨论上,2年前 StackOverflow 有一个经典的回答:使用函数和使用类来构建可复用得组件有什么区别?,大家可以去看看。其中提到得一个关键因素是 Flutter 框架能够检测组件树的类对象,从而提高复用性。而对于私有的方法来说 Flutter 在更新的时候并不知道该如何处理。


image.png


答主也对比了使用类和函数的优劣势。使用类构建的方式:



  • 支持性能优化,比如使用 const 构造方法,更细颗粒度的刷新;

  • 两个不同的布局切换时,能够正确地销毁对应得资源。这个我们在上篇讲 StatefulWidget 的时候有介绍过。

  • 保证正确的方式进行热重载,而使用函数可能破坏热重载。

  • 在 Widget Inspector 中可以查看得到,从而可以方便我们定位和调试问题。

  • 更友好的错误提示。当组件树出现错误时,框架会给出当前构建得组件名称,而如果使用函数的话则得不到清晰得名词。

  • 可以使用 key 提高性能。

  • 可以使用 context 提供的方法(函数式组件除非显示地传递 context)。


使用函数构建组件唯一的优势就是代码量会更少(这可以通过 functional_widget 插件解决,functional_widget 是一个通过注解将和函数式组件构建方式自动转换为类组件的代码生成插件)。


示例对比


下面我们看一段没有拆分的代码,这个仅仅是示例代码,没有任何实际意义。


class _MyStatefulWidgetState extends State<MyStatefulWidget> {
int _counter = 0;

@override
Widget build(BuildContext context) {
return Row(
children: [
Text('Counter: $_counter'),
Container(
child: Column(
children: [
Text('Hello'),
Row(
children: [
Text('there'),
Text('world!'),
],
),
],
),
),
],
);
}
}

括号有点多,对吧,一眼看过去都懵圈了 —— 这也是很多初次接触 Flutter 的人吐槽地方,可以说让不少人直接放弃了! 最直接的方式就是将部分代码抽离成为一个私有方法,比如像下面这样。


class _MyStatefulWidgetState extends State<MyStatefulWidget> {
int _counter = 0;

Widget _buildNonsenseWidget() {
return Container(
child: Column(
children: [
Text('Hello'),
Row(
children: [
Text('there'),
Text('world!'),
],
),
],
),
);
}

@override
Widget build(BuildContext context) {
return Row(
children: [
Text('Counter: $_counter'),
_buildNonsenseWidget(),
],
);
}
}

将深度嵌套的组件代码单独抽成了一个返回 Widget 的私有方法,看起来确实让代码简洁不少。
那么问题就解决了吗?我们来看一下当状态改变的时候会发生什么。
我们知道,当状态变量_counter改变后,Flutter 会调用 build 方法刷新组件。这会导致 _buildNonsenseWidget 这个方法在刷新的时候每次都会被调用,意味着每次都会创建新的组件来替换旧的组件,即便两个组件没有任何改变。而事实上,我们应该只重建那些变化的组件,从而提高性能。
现在再来看使用类组件的方式,实际上有代码模板的情况下,编写一个 StatelessWidget 非常简单。使用类组件后的代码如下所示。代码确实会比函数的方式多,但是实际上大部分不需要我们手敲。


class _MyStatefulWidgetState extends State<MyStatefulWidget> {
int _counter = 0;

@override
Widget build(BuildContext context) {
return Row(
children: [
Text('Counter: $_counter'),

// The deeply nesting widget is now refactored into a
// stateless const widget. No more needless rebuilding!
const _NonsenseWidget(),
],
);
}
}

class _NonsenseWidget extends StatelessWidget {
const _NonsenseWidget();

@override
Widget build(BuildContext context) {
return Container(
child: Column(
children: [
Text('Hello'),
Row(
children: [
Text('there'),
Text('world!'),
],
),
],
),
);
}
}

这里注意,以为这个_NonsenseWidget 在组件得声明周期不会改变,因此使用了 const 的构造方法。这样在刷新过程中,就不会重新构建了!关于 const 可以参考之前的两篇文章。


关于 StatefulWidget,你不得不知道的原理和要点!


解密 Flutter 的 const 关键字


总结


相比使用函数构建复用的组件代码,请尽可能地使用类组件的方式,而且尽可能地将组件拆分为小一点的单元。这样一方面可以提供精确的刷新,另一方面则是可以将组件复用到其他页面中。如果你不想改变自己得习惯,那么可以考虑使用 functional_widget 这个插件来自动生成类组件。


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

一天一个经典算法:桶排序

桶排序也叫箱排序,工作的原理是将数组分到有限数量的桶里。每个桶再进行排序排序,可能再使用别的排序算法或者是以递归的方式继续使用桶排序进行排序,桶排序是鸽巢排序的一种归纳结果。 当输入在一个范围内均匀分布时,桶排序非常好用。 例如:对范围从0.0到1.0且均匀分...
继续阅读 »

桶排序也叫箱排序,工作的原理是将数组分到有限数量的桶里。每个桶再进行排序排序,可能再使用别的排序算法或者是以递归的方式继续使用桶排序进行排序,桶排序是鸽巢排序的一种归纳结果。


当输入在一个范围内均匀分布时,桶排序非常好用。


例如:对范围从0.0到1.0且均匀分布在该范围内的大量浮点数进行排序。


创建桶算法的方法:



  1. 创建n个空桶(列表)。

  2. 对每个数组元素arr[i]插入bucket[n*array[i]]

  3. 使用插入排序对各个桶进行排序

  4. 连接所有的排序桶


Java示例:


import java.util.*;
import java.util.Collections;

class GFG {

// 使用桶排序对大小为 n 的 arr[] 进行排序
static void bucketSort(float arr[], int n)
{
if (n <= 0)
return;

// 1) 创建 n 个空桶
@SuppressWarnings("unchecked")
Vector<Float>[] buckets = new Vector[n];

for (int i = 0; i < n; i++) {
buckets[i] = new Vector<Float>();
}

// 2) 将数组元素放在不同的桶中
for (int i = 0; i < n; i++) {
float idx = arr[i] * n;
buckets[(int)idx].add(arr[i]);
}

// 3) 对单个存储桶进行排序
for (int i = 0; i < n; i++) {
Collections.sort(buckets[i]);
}

// 4) 将所有桶连接到 arr[]
int index = 0;
for (int i = 0; i < n; i++) {
for (int j = 0; j < buckets[i].size(); j++) {
arr[index++] = buckets[i].get(j);
}
}
}

public static void main(String args[])
{
float arr[] = { (float)0.897, (float)0.565,
(float)0.656, (float)0.1234,
(float)0.665, (float)0.3434 };

int n = arr.length;
bucketSort(arr, n);

System.out.println("排序后的数组为 ");
for (float el : arr) {
System.out.print(el + " ");
}
}
}


输出


排序后的数组为
0.1234 0.3434 0.565 0.656 0.665 0.897

性能


时间复杂度: 如果我们假设在桶中插入需要 O(1) 时间,那么上述算法的第 1 步和第 2 步显然需要 O(n) 时间。如果我们使用链表来表示桶,O(1) 很容易实现。第 4 步也需要 O(n) 时间,因为所有桶中都会有 n 个项目。 
分析的主要步骤是步骤 3。如果所有数字均匀分布,这一步平均也需要 O(n) 时间。


包含负数的情况


上面的例子是桶排序时在对大于零的数组进行排序,对于包含负数的情况需要用下述的方法解决。



  1. 将数组拆分为两部分创建两个空向量 Neg[], Pos[](分别存正数和负数)通过转换将所有负,元素存储在 Neg[],变为正数(Neg[i] = -1 * Arr[i]),将所有 +ve 存储在 pos[] (pos[i] = Arr[i])

  2. 调用函数bucketSortPositive(Pos, pos.size()),调用函数 bucketSortPositive(Neg, Neg.size()),bucketSortPositive(arr[], n)

  3. 创建n个空桶(或列表)。

  4. 将每个数组元素 arr[i] 插入 bucket[n*array[i]]

  5. 使用插入排序对单个桶进行排序。

  6. 连接所有排序的桶。


Java示例


import java.util.*;
class GFG
{

// 使用桶排序对大小为 n 的 arr[] 进行排序
static void bucketSort(Vector<Double> arr, int n)
{

// 1) 创建 n 个空桶
@SuppressWarnings("unchecked")
Vector<Double> b[] = new Vector[n];
for (int i = 0; i < b.length; i++)
b[i] = new Vector<Double>();

// 2) 将数组元素放在不同的桶中
for (int i = 0; i < n; i++)
{
int bi = (int)(n*arr.get(i)); // 桶中索引
b[bi].add(arr.get(i));
}

// 3) 对单个存储桶进行排序
for (int i = 0; i < n; i++)
Collections.sort(b[i]);

// 4) 将所有桶连接到 arr[]
int index = 0;
arr.clear();
for (int i = 0; i < n; i++)
for (int j = 0; j < b[i].size(); j++)
arr.add(b[i].get(j));
}

// 这个函数主要是把数组一分为二,然后对两个数组调用bucketSort()。
static void sortMixed(double arr[], int n)
{
Vector<Double>Neg = new Vector<>();
Vector<Double>Pos = new Vector<>();

// 遍历数组元素
for (int i = 0; i < n; i++)
{
if (arr[i] < 0)

// 通过转换为 +ve 元素来存储 -Ve 元素
Neg.add (-1 * arr[i]) ;
else

// 存储 +ve 元素
Pos.add (arr[i]) ;
}
bucketSort(Neg, (int)Neg.size());
bucketSort(Pos, (int)Pos.size());

// 首先通过转换为 -ve 存储 Neg[] 数组的元素
for (int i = 0; i < Neg.size(); i++)
arr[i] = -1 * Neg.get( Neg.size() -1 - i);

// 排序
for(int j = Neg.size(); j < n; j++)
arr[j] = Pos.get(j - Neg.size());
}

public static void main(String[] args)
{
double arr[] = {-0.897, 0.565, 0.656,
-0.1234, 0, 0.3434};
int n = arr.length;
sortMixed(arr, n);

System.out.print("排序后的数组: \n");
for (int i = 0; i < n; i++)
System.out.print(arr[i] + " ");
}0
}

**输出: **


排序后的数组:
-0.897 -0.1234 0 0.3434 0.565 0.656

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

面试官:你都工作3年了,这个算法题都不会?

前言 金三银四,又到了换工作的最佳时机,我幻想着只要跳个槽,就能离开这个”鸟地方“,拿着更多的钱,干着最爽的事... 然而现实总是残酷的,最近有个学妹在换工作,面试前什么手写Priomise、vue双向绑定原理,webpack优化方式,准备了一大堆,本以...
继续阅读 »

前言



金三银四,又到了换工作的最佳时机,我幻想着只要跳个槽,就能离开这个”鸟地方“,拿着更多的钱,干着最爽的事...




然而现实总是残酷的,最近有个学妹在换工作,面试前什么手写Priomisevue双向绑定原理,webpack优化方式,准备了一大堆,本以为成竹在胸,结果却在算法上吃了大亏,心仪的offer没有拿到,一度怀疑人生。到底是什么算法题能让面试官对妹子说出你都工作3年了,这个算法题都不会?这样的狠话?



有效的括号问题



这是一道leetcode上的原题,本意是在考察候选人对数据结构的掌握。来看看题目



给定一个只包括 '(',')','{','}','[',']' 的字符串 s ,判断字符串是否有效。
有效字符串需满足:



  1. 左括号必须用相同类型的右括号闭合。

  2. 左括号必须以正确的顺序闭合。


示例



示例 1:
输入:s = "()"
输出:true

示例 2:
输入:s = "()[]{}"
输出:true

示例 3:
输入:s = "(]"
输出:false

示例 4:
输入:s = "([)]"
输出:false

示例 5:
输入:s = "{[]}"
输出:true


解题信息



如果咱们确实没有刷过算法,不知道那么多套路,通过题目和示例尽可能的获取到更多的信息就很重要了。



根据题目推断出:



  1. 字符串s的长度一定是偶数,不可能是奇数(一对对匹配)。

  2. 右括号前面一定跟着左括号,才符合匹配条件,具备对称性。

  3. 右括号前面如果不是左括号,一定不是有效的括号。


暴力消除法



得到了以上这些信息后,胖头鱼想既然是[]{}()成对的出现,我能不能把他们都挨个消除掉,如果最后结果是空字符串,那不就意味着符合题意了吗?



举个例子


输入:s = "{[()]}"

第一步:可以消除()这一对,结果s还剩{[]}

第二步: 可以消除[]这一对,结果s还剩{}

第三步: 可以消除{}这一对,结果s还剩'' 所以符合题意返回true


代码实现


const isValid = (s) => {
while (true) {
let len = s.length
// 将字符串按照匹配对,挨个替换为''
s = s.replace('{}', '').replace('[]', '').replace('()', '')
// 有两种情况s.length会等于len
// 1. s匹配完了,变成了空字符串
// 2. s无法继续匹配,导致其长度和一开始的len一样,比如({],一开始len是3,匹配完还是3,说明不用继续匹配了,结果就是false
if (s.length === len) {
return len === 0
}
}
}


暴力消除法最终还是可以通过leetcode的用例,就是性能差了点,哈哈


image.png


栈解题法



解题信息中的第2条强调对称性,而栈(后入先出)入栈和出栈恰好是反着来,形成了鲜明的对称性。



入栈:abc,出栈:cba


abc
cba


所以可以试试从的角度来解析:


输入:s = "{[()]}"

第一步:读取ch = {,属于左括号,入栈,此时栈内有{
第二步:读取ch = [,属于左括号,入栈,此时栈内有{[
第三步:读取ch = (,属于左括号,入栈,此时栈内有{[(
第四步:读取ch = ),属于右括号,尝试读取栈顶元素(和)正好匹配,将(出栈,此时栈内还剩{[
第五步:读取ch = ],属于右括号,尝试读取栈顶元素[和]正好匹配,将[出栈,此时栈内还剩{
第六步:读取ch = },属于右括号,尝试读取栈顶元素{和}正好匹配,将{出栈,此时栈内还剩''
第七步:栈内只能'',s = "{[()]}"符合有效的括号定义,返回true


代码实现


const isValid = (s) => {
// 空字符串符合条件
if (!s) {
return true
}

const leftToRight = {
'(': ')',
'[': ']',
'{': '}'
}
const stack = []

for (let i = 0, len = s.length; i < len; i++) {
const ch = s[i]
// 左括号
if (leftToRight[ch]) {
stack.push(ch)
} else {
// 右括号开始匹配
// 1. 如果栈内没有左括号,直接false
// 2. 有数据但是栈顶元素不是当前的右括号
if (!stack.length || leftToRight[ stack.pop() ] !== ch) {
return false
}
}
}

// 最后检查栈内还有没有元素,有说明还有未匹配则不符合
return !stack.length
}


暴力解法虽然符合我们日常的思维,但是果然还是栈结构解法好了不少。


image.png


结尾



面试中,算法到底该不该成为考核候选人的重要指标咱们不吐槽,但是近几年几乎每个大厂都将算法放进了前端面试的环节,为了获得心仪的offer,重温数据结构,刷刷题还是很有必要的,愿你我都被算法温柔以待。


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

庖丁解牛:Android stuido中 git 操作详解

Git
前言 在开发flutter或android项目,选择用Android stuido是比较方便的,在git的可视化使用上,Android studio已经非常方便了,但是还是有很多的小伙伴,依旧用其他工具来管理git。那么今天我就来详细的介绍一下 Android...
继续阅读 »

前言


在开发flutter或android项目,选择用Android stuido是比较方便的,在git的可视化使用上,Android studio已经非常方便了,但是还是有很多的小伙伴,依旧用其他工具来管理git。那么今天我就来详细的介绍一下 Android stuido的git使用与操作。


一、基本认知


git是采用分布式版本库机制。


工作区


项目目录下的文件可以称之为工作区


暂存区


增加文件,执行add操作则是把文件添加到暂存区


基本操作


git add 是将文件放到暂缓区
git commit 则是把文件添加到本地仓库
git push 则是提交到远程仓库
git status 是查看现有版本库中的文件状态


head指针


head表示的是当前版本,并不是任何分支的最新版本


二、Android studio中的git


文件样式与对应关系


在这里插入图片描述
文件1是git忽略文件
文件2是与本地分支版本一致
文件3是咱为提交到本地分支,并做了修改


假如文件是红色的样子,表示并没提交到暂存区


界面与操作


在这里插入图片描述



  1. commit 提交到本地分支 (基本操作,不做说明)

  2. add 添加到暂存区 (基本操作,不做说明)

  3. .git/info/exclude (添加到 忽略文件,是为了让文件脱离git管理,不会上传到git仓库)

  4. Annotate with Git Blame (显示每行代码的作者,如下图)


在这里插入图片描述
5. show diff (故名思义differences 差别)
6. compare with reversion(与某个版本比较)
7. compare with branch(与某个分支比较)
8. show history (查看历史)
9. show current revision (显示当前行最新修订历史版本、提示)
10. Rollback.. 在没有提交到本地库之前,丢弃工作区内容。
11. push.. 推到线上分支
12. pull.. 线上拉到本地并合并
13. fetch 线上拉到本地
14. merge.. 选择分支进行合并
15. rebase.. 选择分支进行合并,如有有合并冲突,会提示整理为一条commit直线
16. branches.. 创建分支与查看分支(这块下面快捷操作具体介绍)
17. new branch 会按照当前本地提交版本,来创建新的分支
18. new tag 给某一次提交增加个可识别的名字
19. reset HEAD
注意:Git中,用HEAD表示当前版本
在这里插入图片描述
有三个选项
Mixed:参数为默认值,暂存区的内容和工作区的内容都在工作区,提交上的都会还原到工作区
Soft:工作区的内容依旧在工作区,暂存区的内容还在暂存区(不被git管理的文件会变为新增文件)
Hard:工作区和暂存区的内容全部丢失(需要谨慎操作,一步就啥也木有了)


to commit 选择你要还原的commit号
20. stash changes..
git中如果本地有文件改动未提交、且该文件和服务器最新版本有冲突,pull更新会提示错误,无法更新:要么先commit自己的改动然后再通过pull拉取代码,stash的好处是可以先将你的改动暂存到本地仓库中,随时可以取出来再用,但是不用担心下次push到服务器时,把不想提交的改动也push到服务器上,因为Stash Changes的内容不参与commit和push。
21. unstash changes
在这里插入图片描述
View:查看
Drop:删除
Clear:清理
pop stash:移除stash
reinstate index
22. manager remote
查看git remote内容


右下角快捷操作


在这里插入图片描述
在这里插入图片描述
merge into current 合并到当前分支
rebase current onto selected 合并到当前rebase模式
chechout and rebase onto current 切换分支,并将分支合并到当前切换的分支


git面板快捷操作


在这里插入图片描述
在这里插入图片描述


compact references view 简洁引用视图
简洁引用:
在这里插入图片描述
align references to left 将引用向左对齐
引用左右对齐配置:
在这里插入图片描述


Show tag names 显示标签名称
设置是否显示标签:


在这里插入图片描述


Show long Edges 显示长线
在这里插入图片描述


Turn Intellisort On 打开intelli 排序
incase of merge show incoming commits first (directly below merge commit)
在合并的情况下,首先显示传入的提交 直接合并在下边

收起阅读 »

排序算法的基础&进阶

类型平均情况下,时间复杂度最好情况下,时间复杂度最坏情况下,时间复杂度空间复杂度稳定性冒泡排序O(n²)O(n)有序情况O(n²)无序情况O(1)稳定快速排序O(nlogn)O(nlogn)O(n²)有序情况O(logn)不稳定插入排序O(n²)O(n)有序情...
继续阅读 »
类型平均情况下,时间复杂度最好情况下,时间复杂度最坏情况下,时间复杂度空间复杂度稳定性
冒泡排序O(n²)O(n)有序情况O(n²)无序情况O(1)稳定
快速排序O(nlogn)O(nlogn)O(n²)有序情况O(logn)不稳定
插入排序O(n²)O(n)有序情况O(n²)无序情况O(1)稳定
选择排序O(n²)O(n²)O(n²)O(1)不稳定
归并排序O(nlogn)O(nlogn)O(nlogn)O(n)稳定
希尔排序O(nlog²n)O(nlog²n)O(nlog²n)O(1)不稳定

关键词含义


n:数据规模


时间复杂度


算法运行过程中所耗费的时间。


空间复杂度


算法运行过程中临时占用存储空间的大小。例如:O(1)表示所需空间大小为常量,与数据量n无关。


稳定性含义



  • 稳定:在排序之前,如果两个数相等,那么排序之后,这两个数的先后顺序不变。如排序前,a=b,a在b的前面;那么排序后,a依旧在b的前面。

  • 不稳定:在排序之前,如果两个数相等,那么排序之后,这两个数的先后顺序改变。如排序前,a=b,a在b的前面;那么排序后,a在b的后面。


冒泡排序


原理步骤



  1. 比较相邻的两个数,如果前面的数大于后面的数,就交换这两个数。

  2. 相邻的最前一对数和最后一对数都要进行比较,这样最后一个数就是最大的数。

  3. 每个元素重复以上步骤,除了最后一个数。

  4. 重复1-3的步骤。


代码实现


private static int[] bubbleSort(int array[]) {
if (array.length == 0) {
return array;
}
// 第1个for循环相当于步骤4
for (int i = 0; i < array.length; i++) {
// 第2个for循环相当于步骤3
// array.length -1 是因为后面有j+1,先-1是为了避免数组越界
// array.length -1 - i,之所以减i(已经排过1遍,就减1;如果已经排过i遍,就减i),是为了不比较排在最后且已经排好序的数,相当于步骤3的最后一句话
for (int j = 0; j < array.length - 1 - i; j++) {
int temp;
// if判断语句相当于步骤1和步骤2
if (array[j] > array[j + 1]) {
temp = array[j];
array[j] = array[j + 1];
array[j + 1] = temp;
}
}
}
return array;
}

快速排序


原理步骤



  1. 取数组中的一个数作为key。

  2. 从后往前获取数组的数,并将其与key进行对比。

  3. 如果其中一个数小于key,那么就将这个数和key交换位置。

  4. 交换位置之后,从前往后获取数组的数,并将其与key对比。

  5. 如果其中一个数大于key,那么就将这个数和key交换位置。

  6. 重复2-5的过程,直到key前面的数都比key小,key后面的数都比key大,这样就完成一次排序。

  7. 以key为中心,对key前面的数组和后面的数组执行1-6的过程,直到数组完全有序。


代码实现


private static void quickSort(int[] array, int left, int right) {
if (left >= right) {
return;
}
int i, j, x;
i = left;
j = right;
x = array[i];
while (i < j) {
while (i < j && array[j] > x) {
j--;
}
if (i < j) {
array[i] = array[j];
i++;
}
while (i < j && array[i] < x) {
i++;
}
if (i < j) {
array[j] = array[i];
j--;
}
}
// j=i
array[j] = x;
quickSort(array, j + 1, right);
quickSort(array, left, j - 1);
}

插入排序


原理



  • 每一步将一个待排序的数插入到已经排好序的序列中,直到插完所有数据。


代码实现


private static int[] insertSort(int array[]) {          
if (array.length == 0) {
return array;
}    
int i, j, temp;
// 注释①
for (i = 1; i < array.length; i++) {
// 注释②
temp = array[i];
// 注释③
for (j = i - 1; j >= 0 && array[j] > temp; j--) {
// 注释④
array[j+1] = array[j];           
}                  
// 注释⑤                  
array[j+1] = temp;           
}                  
return array;
}

注释①



  • 默认数组第一个数(i=0的数)是有序的。


注释②



  • array[i]为待排序的数据。


注释③



  • array[i]前面的数与array[i]进行排序。


注释④



  • arr[j]相当于前数,arr[j+1]相当于后数。

  • 如果前数比后数大,交换位置,前数放到后数的位置。


注释⑤



  • 如果for循环内前数和后数交换了位置(即前数挪到了后数的位置),那么注释⑤处的代码,就是将后数挪到前数的位置,实现交换。

  • 如果缺少注释⑤处的代码,那么前数的位置就会“空缺”,或者说依旧是原来的数,并没有实现交换。

  • 如果for循环内两数并没有交换(即跳出了for循环),此时j=i-1,j+1=i,与tmp=a[i]效果是一样的。


选择排序


原理步骤:



  1. 从未排序的序列中取出最小(最大)的数,放入已排序序列的初始位置;

  2. 继续从未排序序列剩余的数中取最小(最大)的数,放在已排序序列的末尾。

  3. 持续执行②的步骤,直到整个序列有序。


代码实现


private static int[] selectionSort(int[] array) {    
if (array.length == 0) {
return array;
}
for (int i = 0; i < array.length; i++) {
int min = i;
for (int j = i; j < array.length; j++) {
// 从未排序序列中获取最小值
if (array[j] < array[min]) {
min = j;
}
}
// 把获取的最小值放入已排序序列的末尾(此时i代表末尾的索引)
int temp = array[i];
array[i] = array[min];
array[min] = temp;
}
return array;
}

总结1:



  • 插入排序和选择排序可以划为一类排序算法来理解和掌握。

  • 它们都具有相同点——将数组划分为已排序、未排序两个部分,然后将未排序的部分逐个迁移到已排序的部分,最终使整个数组实现完全有序。

  • 而不同点在于从未排序合入到已排序的方式。插入排序会将未排序的数据在已排序的数组中执行直接插入排序;而选择排序会先在未排序的数组中选出最小值,当这个值合入到已排序的数组中时,不需要再进行比较,直接放到已排序数组的末尾就可以了。


希尔排序


原理步骤



  • 把一个数组按增量进行分组。(增量指分组数量)

  • 每个分组采用直接插入排序进行排序。

  • 然后减小增量,每个分组的元素数目增加,直到增量为1,整个文件变为一组,算法结束。


代码实现


private static int[] shellSort(int[] array) {    
if (array.length == 0) {
return array;
}
// gap为分组数目
for (int gap = array.length / 2; gap > 0; gap = gap / 2) {
// i为索引,对每组进行排序
for (int i = gap; i < array.length; i++) {
// j为临时变量
int j = i;
// 分组内元素的个数可能大于2个,因此使用while循环
while (j - gap >= 0 && array[j] < array[j - gap]) {
// 在同一个分组中,如果后面的数(j)比前面的(j-gap)大,就交换它们的位置
int temp = array[j];
array[j] = array[j - gap];
array[j - gap] = temp;
j = j - gap;
}
}
}
return array;
}

归并排序


原理步骤



  • 将一个数组分为左子数组和右子数组,两个子数组的长度为n/2(n为数组的总长度)。

  • 在两个子数组间进行归并排序(即每个子数组划分为更小的左子数组和右子数组,直到无法再分时,对两个数组进行排序,详情见代码)。

  • 将两个有序的子数组合并为一个最终的有序数组。


代码实现


private static int[] mergeSort(int[] array) {    
// 数组只有一个元素或没有元素,直接返回。
// 脱离递归的条件
if (array.length < 2) {
return array;
}
// 将数组分为两半,分别进行排序
int[] left = Arrays.copyOfRange(array, 0, array.length / 2);
int[] right = Arrays.copyOfRange(array, array.length / 2, array.length);
return merge(mergeSort(left),mergeSort(right));
}

/**
* 将左数组与右数组合并为一个有序数组
* 注意:此时左数组、右数组已经有序
* @param left
* @param right
* @return
*/
private static int[] merge(int[] left, int[] right) {
// 合并后的有序数组
int[] result = new int[left.length + right.length];
for (int index = 0, i = 0, j = 0; index < result.length; index++) {
if (i >= left.length) {
// 如果左数组已经遍历结束,就插入右数组的值
result[index] = right[j];
j++;
} else if (j >= right.length) {
// 如果右数组已经遍历结束,就插入左数组的值
result[index] = left[i];
i++;
} else if (left[i] > right[j]) {
// 左数组与右数组的值同时存在时,就对两数进行比较
// 如果右数组的值比较小,就插入右数组的值。
result[index] = right[j];
j++;
} else {
// 左数组与右数组的值同时存在时,就对两数进行比较
// 如果左数组的值比较小,就插入左数组的值。
result[index] = left[i];
i++;
}
}
return result;
}

总结2:



  • 希尔排序和归并排序可以划为一类排序算法来理解和掌握。

  • 它们都具有相同点——先将整个大的数组分为不同的小组,然后对小组的数据进行排序,最终将所有小组合并为一个有序数组。

  • 而不同点在于分组后的排序方式不同。希尔排序会针对一个小组内的数据执行直接插入排序,而归并排序会直接将两个小组合并为一个有序数组。


排序算法进阶



  • 以上冒泡、快排、插入、选择、希尔、归并这六种排序算法都是基础的排序算法,很多中等、困难难度的算法题一般都是基于上述算法进行解决。(比如《合并两个有序数组》其实就是归并算法的某一部分)

  • 推荐《最小K个数》、《数组中的第K个最大元素》作为进阶学习。(它们都是基于快排实现,类似的变形有最大K个数等)

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

把EditText交给ViewModel管理

Android小萌新今天在做项目的时候遇到一个小问题,来记录一下~ 在做一个登录界面的时候,想使用DataBinding+ViewModel+LiveData 但是怎样让ViewModel拿到EditText控件的实例呢?一开始想到把DataBinding对象...
继续阅读 »

Android小萌新今天在做项目的时候遇到一个小问题,来记录一下~


在做一个登录界面的时候,想使用DataBinding+ViewModel+LiveData


但是怎样让ViewModel拿到EditText控件的实例呢?一开始想到把DataBinding对象从Activity传入ViewModel,后来发现不可行,因为DataBinding在初始化的时候需要传入owner参数,而这个owner参数传的是Activity本身,也就是说DataBinding持有了Activity的引用,这时候如果把DataBinding传给ViewModel不就成了ViewModel持有Activity的引用了吗?内存泄漏!不行!


image.png


解决办法是通过DataBinding双向绑定(View可以操作数据,数据变化时通知View),让EditText的内容直接对应到ViewModel中的LiveData上,这样的话在输入框输入的同时LiveData也在随时变化。


一些收获的经验:


1. @={}和@{}


我发现EditText的text属性要使用@={...}而不是像TextView直接使用@{...}来和Livedata绑定,多出来的这个"="我个人认为是TextView和LiveData绑定仅仅只是get数据,而EditText和数据绑定需要get和实时set数据,所以"="可以理解为赋值


<EditText
...
android:text="@={viewModel.inputAccount}"
... />

<EditText
...
android:text="@={viewModel.inputVerify}"
... />

<Button
...
android:onClick="@{(v)->viewModel.onLogin()}"
... />

2. 为什么在账号EditText输入一个数,getInputAccount()会被调用两次呢?


public class TemporaryLoginViewModel extends ViewModel {

private static final String TAG = "TemporaryLoginViewModel";
MutableLiveData<String> mInputAccount;
MutableLiveData<String> mInputVerify;

public MutableLiveData<String> getInputAccount() {
// TODO:为什么EditText输入一个数,getInputAccount()会调用两次?
Log.d(TAG, "getInputAccount: Entrance");
//双检锁
if (mInputAccount == null)
synchronized (TemporaryLoginViewModel.class) {
if (mInputAccount == null)
mInputAccount = new MutableLiveData<>();
}
//只是TextView展示的话可以返回不可变的LiveData,这里因为是EditText所以只能返回可变的MutableLivedata
return mInputAccount;
}

public MutableLiveData<String> getInputVerify() {
...
}

public void onLogin() {
Log.d(TAG, "onLogin: 账号:" + mInputAccount.getValue() + " 验证码:" + mInputVerify.getValue());
}
}

这就要进入源码去看一眼了,在getInputAccount()上选择findUsages
发现有两处地方调用了它


image.png
第一处在一个回调方法的onChange()中,我们打个断点查看虚拟机栈的栈帧,在第一次执行到断点的时候,虚拟机栈是这样的:


image.png


onChange()内部是这样的:


image.png


也就是说你在输入框里打字使得EditText数据改变的时候,首先回调到onChange()中,在这个onChange()中通过getInputAccount()得到LiveData再给它set一个字符串值


第二处是在executeBindings()中,这个方法是什么时候执行呢?我们让程序继续执行,在下一次执行到断点的时候,虚拟机栈是这样的:


image.png
可以看到在第二次执行到断点的时候,程序从executeBindings()方法中企图调用getInputAccount()


继续向下追踪,就可以看到这样的一个描述


image.png


意思是当View所绑定的数据发生变更的时候,执行此方法


总结


走到这里就很清晰了,整个流程是首先在输入框中输入,当监听到输入后先回调onChange(),在onChange()中通过getInputAccount()得到LiveData,然后修改了LiveData的值;LiveData一但修改,就会重新执行executeBindings(),所以又会调用一次getInputAccount()


到现在就明白了为什么ViewModel中的getInputAccount()会被执行两次啦~


3. getInputAccount()只能返回MutableLiveData


第三个问题也很好理解,为了安全嘛,我一开始试图让getInputAccount()返回一个不可修改的LiveData,然后报错了!


image.png
从第二个问题的分析不难看出,人家内部还要给get到的LiveData执行setValue()呢,所以返回的LiveData一定是可变的MutableLiveData啦~


4. 程序启动时会额外执行一次getInputAccount()


当我查看Activity中的setLifecycleOwner(this)方法时发现它设置了一个LifecycleObserver


image.png
进入这个Observer
image.png
它观察到Activity处于onStart状态的时候会调用executePendingBindings()


进入executePendingBindings()瞅瞅


image.png
又要去调用executeBindingsInternal(),这不就是我们上面在虚拟机栈中看到的调用步骤吗?也就是说在Activity在onStart状态时会执行一次getInputAccount()



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

Android卡顿优化思路

卡顿优化思路 卡顿原理分析 卡顿流程flow 卡顿概貌分析 卡顿实际数据收集 卡顿优化细节 卡顿原因 屏幕刷新频率高于帧率,帧率低于30 每帧执行流程 Choreographer中维护着四个队列callbacks 输入事件队列 动画队列 绘制队列 app...
继续阅读 »

卡顿优化思路



  • 卡顿原理分析

  • 卡顿流程flow

  • 卡顿概貌分析

  • 卡顿实际数据收集

  • 卡顿优化细节


卡顿原因


屏幕刷新频率高于帧率,帧率低于30


每帧执行流程


Choreographer中维护着四个队列callbacks



  • 输入事件队列

  • 动画队列

  • 绘制队列

  • app添加的frameCallback队列


vysnc信号由SurfaceFlinger中创建HWC触发,通过bitTube技术发送到目标进程,目标进程vsync信号到来时,执行Choreographer中的onVsync回调,最终触发doFrame顺序执行这四条队列中的消息。


bitTube


在linux/unix中,bitTube技术成为socketPair,它通过dup技术复制socket的句柄,传递到目标进程,开启socket的全双工通信。


句柄


在内核中,每一个进程都有一个私有的“打开文件表”,这个表是一个指针数组,每一个元素都指向一个内核的打开文件对象。而fd,就是这个表的下标。当用户打开一个文件时,内核会在内部生成一个打开文件对象,并在这个表里找到一个空项,让这一项指向生成的打开文件对象,并返回这一项的下标作为fd.


ui优化



  • 多余Bg移除

  • ui重叠区域优化 cancas.clipRect

  • 减少ui层级

  • 耗时方法分析与优化

  • 多样式布局采用单一rv处理


webview优化


webview的加载流程


image


webiew初始化



  • 目的是初始化并启动浏览器内核。

  • 提前初始化webview并隐藏 优化126ms


webview 单独进程



  • 单独进程 activity配置

  • 单独进程的交互 webview.addJavascriptInterface(),webview.evalute()


安全性



  • addJavaScriptInterface添加的java对象的方法,需要添加@addJavascriptInterface注解,避免xss攻击


卡顿收集策略


开发卡顿检测StrictMode


private void initStrictMode() {
if (isDebug()) {
StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
.detectCustomSlowCalls() //API等级11,使用StrictMode.noteSlowCode
.detectDiskReads()
.detectDiskWrites()
.detectNetwork() // or .detectAll() for all detectable problems
.penaltyDialog() //弹出违规提示对话框
.penaltyLog() //在Logcat 中打印违规异常信息
.penaltyFlashScreen() //API等级11
.build());
StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder()
.detectLeakedSqlLiteObjects()
.detectLeakedClosableObjects() //API等级11
.penaltyLog()
.penaltyDeath()
.build());
}
}

线下卡顿检测



  • adb shell dumpsys gfxinfo [packagename]


Applications Graphics Acceleration Info:
Uptime: 205237819 Realtime: 436545102

** Graphics info for pid 5842 [xxxx] **

Stats since: 198741999784549ns
Total frames rendered: 653
Janky frames: 157 (24.04%)
50th percentile: 9ms
90th percentile: 34ms
95th percentile: 53ms
99th percentile: 200ms
Number Missed Vsync: 46
Number High input latency: 268
Number Slow UI thread: 76
Number Slow bitmap uploads: 3
Number Slow issue draw commands: 8
Number Frame deadline missed: 92


  • 通过gpu绘制条形柱分析


条形柱共分为8种颜色,绿色和蓝色部分是异步应用能够优化的部分。包括其他处理 - 输入 - 动画 - travel
image


BlockCanary检测卡顿


在ActivityThread.main中的Looper大循环中,Looper.looponce会不断从消息队列中取出消息派发出去,并在前后通过logging打印了两个日志,我们通过设置自定义的logger,在两部分日志的时间差与30ms做对比,如果超过30ms,认为是卡顿。


logging.println(">>>>> Dispatching to " + msg.target + " "
+ msg.callback + ": " + msg.what);
msg.target.dispatchMessage(msg);
logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);

卡顿分析信息收集



  • Debug.startMethodTracing 收集具体的卡顿方法

  • 查看trace文件 根据bottomup分析具体的耗时方法

  • 火焰图,横轴是调用方法耗时,纵轴是调用深度

  • 调用图,调用链以及方法耗时


线上卡顿分析与收集


在ActivityThread.main中的Looper大循环中,Looper.looponce会不断从消息队列中取出消息派发出去,并在前后通过logging打印了两个日志,我们通过设置自定义的logger,在两部分日志的时间差与30ms做对比,如果超过30ms,认为是卡顿。将主线程堆栈信息写入到缓存文件并异步发送到日志后台。


常见的卡顿问题


sharepreference



  • 首次读取写入会loadxml到内存

  • sp文件修改是全量读写的

  • commit异步写入,通过CountdownLatch阻塞等待结果

  • apply延迟100ms写入,无返回结果

  • 主线程ANR,sp的修改会先体现在内存中,然后往QueueWorker中加入磁盘异步写数据的任务,但是会在Activity.onResume以及Service.onstartCommand等方法中增加waitToFinish等待磁盘写入完成的代码。

  • 解决方案使用MMKV

  • 尽量拆分小的xml


主线程操作文件


主线程网络操作


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

Silhouette——更方便的Shape/Selector实现方案

写在前面 首先祝大家新年快乐,开工大吉。 最新刚换了工作,大部分精力还是放到新工作上面,所以这次还是先给大家带来一个小而实用的库:Silhouette。另外,考虑到Kotlin越来越普及,作者在开发过程中也切实感受到Kotlin相较于Java带来的便利,后续的...
继续阅读 »

写在前面


首先祝大家新年快乐,开工大吉。

最新刚换了工作,大部分精力还是放到新工作上面,所以这次还是先给大家带来一个小而实用的库:Silhouette。另外,考虑到Kotlin越来越普及,作者在开发过程中也切实感受到Kotlin相较于Java带来的便利,后续的IM系列文章及项目考虑用Kotlin重写,而且考虑到由于工作业务需求过多可能出现断更的情况,所以打算一次性写完再放出来,避免大家学习不方便。

废话不多说,直接开始吧。


Silhouette是什么?


Silhouette意为“剪影”,取名并没有特别的含义,只是单纯地觉得意境较美。例如上一篇文章Shine——更简单的Android网络请求库封装的网络请求库:Shine即意为“闪耀”,也没有特别的含义,只是作者认为开源库起名较难,特意找一些比较优美的单词。

Silhouette是一系列基于GradientDrawableStateListDrawable封装的组件集合,主要用于实现在Android Layout XML中直接支持Shape/Selector等功能。

我们都知道在Android开发中,不同的TextViewButton各种样式(形状、背景色、描边、圆角、渐变等)的传统实现方式是在drawable文件夹中编写各种shape/selector等文件,这种方式至少会存在以下几种弊端:



  1. shape/selector文件过多,项目体积增大;

  2. shape/selector文件命名困难,命名规范时往往会存在功能重复的文件;

  3. 功能存在局限性:例如gradient渐变色。传统shape方式只支持三种颜色过渡(startColor/centerColor/endColor),如果设计稿存在四种以上颜色渐变,shape gradient无能为力。再比如TextView在常态和按下态需要同时改变背景色及文字颜色时,传统方式只能在代码中动态设置等。

  4. 开发效率低;

  5. 难以维护等;


综上所述,我们迫切需要一个库来解决以上问题,Silhouette正具备这些能力。接下来,我们来具体看看Silhouette能做什么吧。


Silhouette能做什么?


上面说到Silhouette是一系列组件集合,具体包含以下组件:




  • SleTextButton

    基于AppCompatTextView封装;

    具备定义各种样式(形状、背景色、描边、圆角、渐变等)的能力 ;

    具备不同状态(常态、按下态、不可点击态)下文字颜色指定等。




  • SleImageButton

    基于ShapeableImageView封装;

    通过指定sle_ib_type属性使ImageView支持按下态遮罩层、透明度改变、自定义图片,同时支持CheckBox功能;

    通过指定sle_ib_style属性使ImageView支持Normal、圆角、圆形等形状。




  • SleConstraintLayout

    基于ConstraintLayout封装;

    具备定义各种样式(形状、背景色、描边、圆角、渐变等)的功能。




  • SleRelativeLayout

    基于RelativeLayout封装;

    具备定义各种样式(形状、背景色、描边、圆角、渐变等)的功能。




  • SleLinearLayout

    基于LinearLayout封装;

    具备定义各种样式(形状、背景色、描边、圆角、渐变等)的功能。




  • SleFrameLayout

    基于FrameLayout封装;

    具备定义各种样式(形状、背景色、描边、圆角、渐变等)的功能。




设计、封装思路及原理




  • 项目结构

    com.freddy.silhouette



    • config(配置相关,存放全局注解及公共常量、默认值等)

    • extkotlin扩展相关,可选择用或不用)

    • utils(工具类相关,可选择用或不用)

    • widget(控件相关)

      • button

      • layout




    由此可见,项目结构非常简单,所以Silhouette也是一个比较轻量级的库。




  • 封装思路及原理

    由于该库非常简单,实际上就是根据Shape/Selector进行自定义属性,从而利用GradientDrawableStateListDrawable提供的API进行封装,不存在什么难度,在此就不展开讲了。


    下面贴一下代码片段,基本上几个组件的实现原理都大同小异,都是利用GradientDrawableStateListDrawable实现组件的ShapeSelector功能:




private fun init() {
val normalDrawable =
getDrawable(normalBackgroundColor, normalStrokeColor, normalGradientColors)
var pressedDrawable: GradientDrawable? = null
var disabledDrawable: GradientDrawable? = null
var selectedDrawable: GradientDrawable? = null
when (type) {
TYPE_MASK -> {
pressedDrawable = getDrawable(
normalBackgroundColor,
normalStrokeColor,
normalGradientColors
).apply {
colorFilter =
PorterDuffColorFilter(maskBackgroundColor, PorterDuff.Mode.SRC_ATOP)
}
disabledDrawable =
getDrawable(disabledBackgroundColor, disabledBackgroundColor)
}
TYPE_SELECTOR -> {
pressedDrawable =
getDrawable(pressedBackgroundColor, pressedStrokeColor, pressedGradientColors)
disabledDrawable = getDrawable(
disabledBackgroundColor,
disabledStrokeColor,
disabledGradientColors
)
}
}
selectedDrawable = getDrawable(
selectedBackgroundColor,
selectedStrokeColor,
selectedGradientColors
)
setTextColor(normalTextColor)
background = StateListDrawable().apply {
if (type != TYPE_NONE) {
addState(intArrayOf(android.R.attr.state_pressed), pressedDrawable)
}
addState(intArrayOf(-android.R.attr.state_enabled), disabledDrawable)
addState(intArrayOf(android.R.attr.state_selected), selectedDrawable)
addState(intArrayOf(), normalDrawable)
}

setOnTouchListener(this)
}

private fun getDrawable(
backgroundColor: Int,
strokeColor: Int,
gradientColors: IntArray? = null
): GradientDrawable {
// 背景色相关
val drawable = GradientDrawable()
setupColor(drawable, backgroundColor)

// 形状相关
(drawable.mutate() as GradientDrawable).shape = shape
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
drawable.innerRadius = innerRadius
if (innerRadiusRatio > 0f) {
drawable.innerRadiusRatio = innerRadiusRatio
}
drawable.thickness = thickness
if (thicknessRatio > 0f) {
drawable.thicknessRatio = thicknessRatio
}
}

// 描边相关
if (strokeColor != 0) {
(drawable.mutate() as GradientDrawable).setStroke(
strokeWidth,
strokeColor,
dashWidth,
dashGap
)
}

// 圆角相关
setupCornersRadius(
drawable,
cornersRadius,
cornersTopLeftRadius,
cornersTopRightRadius,
cornersBottomRightRadius,
cornersBottomLeftRadius
)

// 渐变相关
(drawable.mutate() as GradientDrawable).gradientType = gradientType
if (gradientCenterX != 0.0f || gradientCenterY != 0.0f) {
(drawable.mutate() as GradientDrawable).setGradientCenter(
gradientCenterX,
gradientCenterY
)
}
gradientColors?.let { colors ->
(drawable.mutate() as GradientDrawable).colors = colors
}
var orientation: GradientDrawable.Orientation? = null
when (gradientOrientation) {
GRADIENT_ORIENTATION_TOP_BOTTOM -> {
orientation = GradientDrawable.Orientation.TOP_BOTTOM
}
GRADIENT_ORIENTATION_TR_BL -> {
orientation = GradientDrawable.Orientation.TR_BL
}
GRADIENT_ORIENTATION_RIGHT_LEFT -> {
orientation = GradientDrawable.Orientation.RIGHT_LEFT
}
GRADIENT_ORIENTATION_BR_TL -> {
orientation = GradientDrawable.Orientation.BR_TL
}
GRADIENT_ORIENTATION_BOTTOM_TOP -> {
orientation = GradientDrawable.Orientation.BOTTOM_TOP
}
GRADIENT_ORIENTATION_BL_TR -> {
orientation = GradientDrawable.Orientation.BL_TR
}
GRADIENT_ORIENTATION_LEFT_RIGHT -> {
orientation = GradientDrawable.Orientation.LEFT_RIGHT
}
GRADIENT_ORIENTATION_TL_BR -> {
drawable.orientation = GradientDrawable.Orientation.TL_BR
}
}
orientation?.apply {
(drawable.mutate() as GradientDrawable).orientation = this
}
return drawable
}

感兴趣的同学可以到官方文档了解GradientDrawableStateListDrawable的原理。


自定义属性列表


自定义属性分为通用属性特有属性




  • 通用属性



    • 类型



















    属性名称类型说明备注
    sle_typeenum类型
    mask:遮罩
    selector:自定义样式
    none:无
    默认值:mask
    默认的mask为90%透明度黑色,可通过sle_maskBackgroundColors属性设置
    若不指定为selector,则自定义样式无效


    • 形状相关











































    属性名称类型说明备注
    sle_shapeenum形状
    rectangle:矩形
    oval:椭圆形
    line:线性形状
    ring:环形
    默认值:rectangle
    sle_innerRadiusdimension|reference尺寸,内环的半径shape="ring"可用
    sle_innerRadiusRatiofloat以环的宽度比率来表示内环的半径shape="ring"可用
    sle_thicknessdimension|reference尺寸,环的厚度shape="ring"可用
    sle_thicknessRatiofloat以环的宽度比率来表示环的厚度shape="ring"可用


    • 背景色相关





































    属性名称类型说明备注
    sle_normalBackgroundColorcolor|reference常态背景颜色/
    sle_pressedBackgroundColorcolor|reference按下态背景颜色/
    sle_disabledBackgroundColorcolor|reference不可点击态背景颜色默认值:#CCCCCC
    sle_selectedBackgroundColorcolor|reference选中态背景颜色/


    • 描边相关























































    属性名称类型说明备注
    sle_normalStrokeColorcolor|reference常态描边颜色/
    sle_pressedStrokeColorcolor|reference按下态描边颜色/
    sle_disabledStrokeColorcolor|reference不可点击态描边颜色/
    sle_selectedStrokeColorcolor|reference选中态描边颜色/
    sle_strokeWidthdimension|reference描边宽度/
    sle_dashWidthdimension|reference虚线宽度/
    sle_dashGapdimension|reference虚线间隔/


    • 圆角相关











































    属性名称类型说明备注
    sle_cornersRadiusdimension|reference总圆角半径/
    sle_cornersTopLeftRadiusdimension|reference左上角圆角半径/
    sle_cornersTopRightRadiusdimension|reference右上角圆角半径/
    sle_cornersBottomLeftRadiusdimension|reference左下角圆角半径/
    sle_cornersBottomRightRadiusdimension|reference右下角圆角半径/


    • 渐变相关



































































    属性名称类型说明备注
    sle_normalGradientColorsreference常态渐变背景色支持在res/array下定义数组实现多个颜色渐变
    sle_pressedGradientColorsreference按下态渐变背景色支持在res/array下定义数组实现多个颜色渐变
    sle_disabledGradientColorsreference不可点击态渐变背景色支持在res/array下定义数组实现多个颜色渐变
    sle_selectedGradientColorsreference选中态渐变背景色支持在res/array下定义数组实现多个颜色渐变
    sle_gradientOrientationenum渐变方向
    TOP_BOTTOM:从上到下
    TR_BL:从右上到左下
    RIGHT_LEFT:从右到左
    BR_TL:从右下到左上
    BOTTOM_TOP:从下到上
    BL_TR:从左下到右上
    LEFT_RIGHT:从左到右
    TL_BR:从左上到右下
    /
    sle_gradientTypeenum渐变类型
    linear:线性渐变
    radial:圆形渐变,起始颜色从gradientCenterX、gradientCenterY点开始
    sweep:A sweeping line gradient
    /
    sle_gradientCenterXfloat渐变中心放射点x坐标注意,这里的坐标是整个背景的百分比的点,并不是确切点,0.2就是20%的点
    sle_gradientCenterYfloat渐变中心放射点y坐标注意,这里的坐标是整个背景的百分比的点,并不是确切点,0.2就是20%的点
    sle_gradientRadiusdimension|reference渐变半径需要配合gradientType=radial使用,如果设置gradientType=radial而没有设置gradientRadius,将会报错


    • 其它

























    属性名称类型说明备注
    sle_maskBackgroundColorcolor|reference当sle_type=mask时,按钮按下状态的遮罩颜色默认值:90%透明度黑色(#1A000000)
    sle_cancelOffsetdimension|reference用于解决手指移出控件区域判断为cancel的偏移量默认值:8dp



  • 特有属性



    • SleConstraintLayout/SleRelativeLayout/SleFrameLayout/SleLinearLayout



















    属性名称类型说明备注
    sle_interceptTypeenum事件拦截类型
    intercept_super:return super
    intercept_true:return true
    intercept_false:return false
    Layout组件设置此值,可实现是否拦截事件,如果设置为intercept_true,事件将不传递到子控件,在某些场景比较实用


    • SleTextButton





































    属性名称类型说明备注
    sle_normalTextColorcolor|reference常态文字颜色/
    sle_pressedTextColorcolor|reference按下态文字颜色/
    sle_disabledTextColorcolor|reference不可点击态文字颜色/
    sle_selectedTextColorcolor|reference选中态文字颜色/


    • SleImageButton









































































    属性名称类型说明备注
    sle_ib_typeenum类型
    mask:图片遮罩
    alpha:图片透明度改变
    selector:自定义图片
    checkBox:CheckBox场景
    none:无
    1.指定为mask时,自定义图片资源无效;
    2.指定为alpha时,sle_pressedAlpha/sle_disabledAlpha生效;
    3.指定为selector时,sle_normalResId/sle_pressedResId/sle_disabledResId生效;
    4.指定为checkBox时,sle_checkedResId/sle_uncheckedResId/sle_isChecked生效;
    5.指定为none时,图片资源均不生效,圆角相关配置有效
    sle_ib_styleenumImageView形状
    normal:普通形状
    rounded:圆角
    oval:圆形
    默认值:normal
    sle_normalResIdcolor|reference常态图片资源/
    sle_pressedResIdcolor|reference按下态图片资源/
    sle_disabledResIdcolor|reference不可点击态图片资源/
    sle_checkedResIdcolor|reference选中态checkBox图片资源/
    sle_uncheckedResIdcolor|reference非选中态checkBox图片资源/
    sle_isCheckedbooleanCheckBox是否选中默认值:false
    sle_pressedAlphafloat按下态图片透明度默认值:70%
    sle_disabledAlphafloat不可点击态图片透明度默认值:30%



使用方式



  1. 添加依赖


implementation "io.github.freddychen:silhouette:$lastest_version"

Note:最新版本可在maven central silhouette中找到。



  1. 使用


由于自定义属性太多,在此就不一一列举了。下面给出几种常见的场景示例,大家可以根据自定义属性表自行编写:



  • 常态


Silhouette Normal



  • 按下态


Silhouette Pressed


以上布局代码为:


<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/black"
android:gravity="center_horizontal"
android:orientation="vertical">

<com.freddy.silhouette.widget.button.SleTextButton
android:id="@+id/stb_1"
android:layout_width="match_parent"
android:layout_height="54dp"
android:layout_marginHorizontal="48dp"
android:layout_marginTop="14dp"
android:gravity="center"
android:text="SleTextButton1"
android:textSize="20sp"
app:sle_cornersRadius="28dp"
app:sle_normalBackgroundColor="#f88789"
app:sle_normalTextColor="@color/white"
app:sle_type="mask" />

<com.freddy.silhouette.widget.button.SleTextButton
android:id="@+id/stb_2"
android:layout_width="match_parent"
android:layout_height="54dp"
android:layout_marginHorizontal="48dp"
android:layout_marginTop="14dp"
android:gravity="center"
android:text="SleTextButton2"
android:textSize="20sp"
app:sle_cornersBottomRightRadius="24dp"
app:sle_cornersTopLeftRadius="14dp"
app:sle_normalBackgroundColor="#338899"
app:sle_normalTextColor="@color/white"
app:sle_pressedBackgroundColor="#aeeacd"
app:sle_type="selector" />

<com.freddy.silhouette.widget.button.SleTextButton
android:id="@+id/stb_3"
android:layout_width="120dp"
android:layout_height="120dp"
android:layout_marginHorizontal="48dp"
android:layout_marginTop="14dp"
android:enabled="false"
android:gravity="center"
android:text="SleTextButton2"
android:textSize="14sp"
app:sle_cornersBottomRightRadius="24dp"
app:sle_cornersTopLeftRadius="14dp"
app:sle_normalBackgroundColor="#cc688e"
app:sle_normalTextColor="@color/white"
app:sle_pressedBackgroundColor="#34eeac"
app:sle_shape="oval"
app:sle_type="selector" />

<com.freddy.silhouette.widget.button.SleImageButton
android:id="@+id/sib_1"
android:layout_width="84dp"
android:layout_height="84dp"
android:layout_marginTop="14dp"
app:sle_ib_type="mask"
app:sle_normalResId="@drawable/ic_launcher_background" />

<com.freddy.silhouette.widget.button.SleImageButton
android:id="@+id/sib_2"
android:layout_width="128dp"
android:layout_height="128dp"
android:layout_marginTop="14dp"
app:sle_ib_type="alpha"
app:sle_normalResId="@drawable/ic_launcher_background" />

<com.freddy.silhouette.widget.button.SleImageButton
android:id="@+id/sib_3"
android:layout_width="72dp"
android:layout_height="72dp"
android:layout_marginTop="14dp"
app:sle_ib_type="selector"
app:sle_normalResId="@mipmap/ic_launcher"
app:sle_pressedResId="@drawable/ic_launcher_foreground" />

<com.freddy.silhouette.widget.layout.SleConstraintLayout
android:id="@+id/scl_1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="48dp"
android:layout_marginTop="14dp"
android:paddingHorizontal="14dp"
android:paddingVertical="8dp"
app:sle_cornersRadius="10dp"
app:sle_interceptType="intercept_super"
app:sle_normalBackgroundColor="@color/white">

<ImageView
android:layout_width="72dp"
android:layout_height="48dp"
android:scaleType="centerCrop"
android:src="@mipmap/ic_launcher_round" />

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="UserName"
android:textColor="@color/black"
android:textSize="18sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</com.freddy.silhouette.widget.layout.SleConstraintLayout>

<com.freddy.silhouette.widget.layout.SleLinearLayout
android:id="@+id/sll_1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="48dp"
android:layout_marginTop="14dp"
android:gravity="center_vertical"
android:paddingHorizontal="14dp"
app:sle_type="selector"
android:paddingVertical="8dp"
app:sle_cornersTopRightRadius="24dp"
app:sle_cornersBottomRightRadius="18dp"
app:sle_interceptType="intercept_true"
app:sle_pressedBackgroundColor="#fe9e87"
app:sle_normalBackgroundColor="#aee949">

<ImageView
android:layout_width="72dp"
android:layout_height="48dp"
android:scaleType="centerCrop"
android:src="@mipmap/ic_launcher_round" />

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="14dp"
android:text="UserName"
android:textColor="@color/black"
android:textSize="18sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</com.freddy.silhouette.widget.layout.SleLinearLayout>
</LinearLayout>

Note:需要给组件设置setOnClickListener才能看到效果。

至于更多的功能,就让大家去试试吧,篇幅有限,就不一一列举了。有任何疑问,欢迎通过QQ群微信公众号联系我。


版本记录






















版本号修改时间版本说明
0.0.12022.02.10首次提交
0.0.22022.02.12修改minSdk为19

写在最后


终于写完了,Shape/Selector在每个项目中基本都会用到,而且频率还不算低。Silhouette原理虽然简单,但确实能解决很多问题,这些都是平时开发中的积累,希望对大家能有所帮助。欢迎大家starfork,让我们为Android开发共同贡献一份力量。另外如果有疑问欢迎加入我的QQ群:1015178804,同时也欢迎大家关注我的公众号:FreddyChen,让我们共同进步和成长。


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

Android UI适配方案

大纲 使用dp而不是px 尽量使用自动适配布局,而不要指定分辨率 使用宽高限定符 values-1080x1920,以1080P为基准计算每种常见分辨率对应的尺寸。 需要尽可能全的添加各种设备的分辨率(有工具) 容错性不足,如果设备分辨率不能精确匹配对应限...
继续阅读 »

大纲



  1. 使用dp而不是px

  2. 尽量使用自动适配布局,而不要指定分辨率

  3. 使用宽高限定符

    1. values-1080x1920,以1080P为基准计算每种常见分辨率对应的尺寸。

    2. 需要尽可能全的添加各种设备的分辨率(有工具)

    3. 容错性不足,如果设备分辨率不能精确匹配对应限定符,会默认使用统一默认的dimens



  4. 第三方自动适配UI框架

    1. 原理:自定义RelativeLayout,在onMeasure中对控件分辨率做变换

    2. 第三方框架,维护性很成问题

    3. 一些自定义View,处理比较麻烦



  5. 最小宽度限定符,类似宽高限定符

    1. values-sw240dp,同样以某一dp宽度为基准计算其他宽度dp的值

    2. values-sw360dp、values-sw480dp

    3. 相比宽高限定符,最小宽度限定符不进行精确匹配,会遵循就近原则,可以较好的解决容错问题。

    4. 如:设备宽364dp,系统会自动就近配置values-sw360dp下的dimens,显示效果相差不会很大



  6. 今日头条——修改density值

    1. 原理:px = dp x (dpi/160) = dp x density

    2. 既然如此,将density

    3. 需要UI出设计图时以统一的dp为基准

    4. mp.weixin.qq.com/s/d9QCoBP6k…




基本概念



  • 像素——px

  • 密度独立像素——dp或dip

  • 像素密度——dpi,单位面积内的像素数。

    • 软件系统的概念。

    • 在系统出厂时,配置文件中的固定值。

    • 通常的取值有:160、240、360、480等。

    • 不同于物理概念上的屏幕密度ppi,如ppi为415、430和470时,dpi可能会统一设置为480。



  • density——当dpi=160时,1px = 1pd,此时denstiy的值为1,dpi=240时,1.5px = 1dp,density的值为1.5。

  • 上述值的关系:

    • denstiy = dpi / 160;

    • px = dp x density = dp x (dpi / 160)




Android设备的碎片化极为严重,各种尺寸和分辨率的设备无比繁多。使得在Android开发中,UI适配变成了开发过程中极为重要的一步。为此Google提出了密度独立像素dip或dp的概率,旨在更友好的处理Android UI适配问题。


但是效果嘛,只能说差强人意,可以解决大部分的业务场景,但是剩下的个别情况就搞死人了,原因在于Android设备碎片化实在太严重了,存在各种分辨率和dpi的设备。


比如两台设备A和B,分辨率是1920x1080,dpi分别为420和480,在布局中编写一个100dp宽的ImageView,按照上面的公式ImageView的显示宽度分别为:100dp x 420 / 160 = 262.5100dp x 480 / 160 = 300,ImageView在B设备上明显显示要大一些。差异可能还不明显,我们把宽度改为360dp呢,A设备显示宽度为:948px,B设备显示宽度为:1080px。这就扯淡了,一个宽度填充满屏幕,一个不满。这种情况肯定是需要开发来背锅解决的。


适配方案


虽然上面提到了使用dp无法解决全部业务场景,但是相对于直接使用px已经可以解决大部分场景下的适配问题了。


所以UI适配的第一条就是:


1. 使用dp代替px来编写布局。


又因为上面无法适配的个别场景,所以UI适配的第二条是:


2.尽量使用自动适配布局,而不要指定分辨率


这一条也很好理解,尽量使用ConstraintLayout 约束布局和LinearLayout等父布局,不要写死分辨率,比如上面的例子如果使用match_parent而不是360dp,也可以避免出现显示不一致问题(但是仅限于上列)。


限定符


Google同样意识到dp满足所以业务场景的需要,所以提供了宽度限定符的概念。



虽然您的布局应始终通过拉伸其视图内部和周围的空间来应对不同的屏幕尺寸,但这可能无法针对每种屏幕尺寸提供最佳用户体验。例如,您为手机设计的界面或许无法在平板电脑上提供良好的体验。因此,您的应用还应提供备用布局资源,以针对特定屏幕尺寸优化界面设计。



最小宽度限定符



使用“最小宽度”屏幕尺寸限定符,您可以为具有最小宽度(以密度无关像素 dp 或 dip 为度量单位)的屏幕提供备用布局。


通过将屏幕尺寸描述为密度无关像素的度量值,Android 允许您创建专为非常具体的屏幕尺寸而设计的布局,同时让您不必对不同的像素密度有任何担心。



通俗一点翻译就是:可用通过xxxx-swXXXdp的方式定义一些最小限定符的资源文件,比如:values-sw400dp、values-sw600dp,系统会自动匹配如屏幕宽度相近资源文件夹。


我们再来看上面的例子两台设备A和B,分辨率是1920x1080,dpi分别为360和400。我们简化下问题比如设计图给的是1920x1080 360dpi,包含一个22.5px * 22.5px = 10dp * 10dp的图片。按经验布局应该如下编写:


<ImageView
android:id="@+id/img_iv"
android:layout_width="10dp"
android:layout_height="10dp"
android:background="@mipmap/ic_launcher"/>

在不同设备上运行的结果:



  • 1280 x 720 240dpi的设备,图片显示为15px * 15px;

  • 1920 x1080 360dpi的A设备,图片显示为22.5px * 22.5px;

  • 1920 x1080 400dpi的B设备,图片显示为25px * 25px;


可以看到B设备图片显示是有问题的,为了解决这个问题,我们使用最小宽度限定符定义两个资源文件夹:values-sw360dp和values-sw400dp。


在values-sw360dp中添加dimen.xml内容如下:


<?xml version="1.0" encoding="utf-8"?>
<resources>
<dimen name="dp_1">1dp</dimen>
<dimen name="dp_2">2dp</dimen>
<dimen name="dp_3">3dp</dimen>
<dimen name="dp_4">4dp</dimen>
<dimen name="dp_5">5dp</dimen>
<dimen name="dp_6">6dp</dimen>
<dimen name="dp_7">7dp</dimen>
<!-- 省略其他值 -->
<dimen name="dp_360">360dp</dimen>
<!-- 因为设计图是360dpi,所以控件尺寸通常不会超过360dp,定义最大360dp的值足够使用 -->
</resources>

在values-sw420dp中添加dimen.xml,文件中的dimen值很容易换算出来:在360dpi中dp_1 = 1dp,那么在400dpi中dp_1 = 360 / 400 = 0.9dp,文件内容如下:


<?xml version="1.0" encoding="utf-8"?>
<resources>
<dimen name="dp_1">0.9dp</dimen>
<dimen name="dp_2">1.8dp</dimen>
<dimen name="dp_3">2.7dp</dimen>
<dimen name="dp_4">3.6dp</dimen>
<dimen name="dp_5">4.5dp</dimen>
<dimen name="dp_6">5.4dp</dimen>
<dimen name="dp_7">6.3dp</dimen>
<!-- 省略其他值 -->
</resources>

注意要在values文件夹下添加默认dimen.xml,文件内容与values-sw360dp中添加dimen.xml一致(因为设计图恰好是360dpi的)。


布局中的ImageView自然要改写为:


<ImageView
android:id="@+id/img_iv"
android:layout_width="@dimen/dp_10"
android:layout_height="@dimen/dp_10"
android:background="@mipmap/ic_launcher"/>

我们再来看一下不同设备运行结果:



  • 1280 x 720 240dpi的设备,未匹配到限定符使用values中的dimen,dp_10 = 10dp, px = 10 * 240 / 160 = 15px,图片显示尺寸为15px * 15px。

  • 1920 x1080 360dpi的A设备,匹配到sw360dp限定符,dp_10 = 10dp, px = 10 * 360 / 160 = 20px,图片显示尺寸为22.5px * 22.5px。

  • 1920 x1080 400dpi的B设备,匹配到sw420dp限定符,dp_10 = 9dp, px = 9 * 400 / 160 = 20px,图片显示尺寸为22.5px * 22.5px。


完美的解决了设备A和B的显示问题,所以UI适配的第三条是:


3. 使用最小(可用)宽度限定符,解决同样分辨率不同dpi的设备适配问题。


这种方案看似完美,但是也有一些隐含的问题:此方案只能解决同样分辨率不同dpi设备的适配问题:



  • 一旦出现不同分辨率相同dpi的情况就无效了(当然这种情况的可能性不高)。

  • 以上举例只是基于1920x1080这一种分辨率为例说明,试想一下如果1280x720的设备存在240dpi和280dpi的情况呢?我们只能针对特殊情况适配处理,无法解决全部场景适配问题。


宽高限定符


类似于上面说的最小宽度限定符,但是需要精确指定要匹配的设备宽高,values-1920x1080、values-1280x720等。配置与使用方式也与上面类似,如设计图尺寸为1920x1080 360dpi,那么只需要以1920x1080为基准计算所有分辨率对应的尺寸就可以了,布局编写时按照给的尺寸一一对应就可以,比如:给出的ImageView是20px*20px的,那在布局中同样指定width和height为@dimen/dp_20就可以了。


values-1920x1080中dimens.xml如下:


<resources>
<dimen name="dp_1">1px</dimen>
<dimen name="dp_2">2px</dimen>
<dimen name="dp_3">3px</dimen>
<dimen name="dp_4">4px</dimen>
<dimen name="dp_5">5px</dimen>
<dimen name="dp_6">6px</dimen>
<dimen name="dp_7">7px</dimen>
<dimen name="dp_8">8px</dimen>
<dimen name="dp_9">9px</dimen>
<!-- 省略其他 -->
<dimen name="dp_1920">1920px</dimen>
</resources>

values-1280x720中dimens.xml换算为:


<?xml version="1.0" encoding="utf-8"?>
<resources>
<dimen name="dp_1">0.66px</dimen>
<dimen name="dp_2">1.33px</dimen>
<dimen name="dp_3">2.0px</dimen>
<dimen name="dp_4">2.66px</dimen>
<dimen name="dp_5">3.33px</dimen>
<dimen name="dp_6">4.0px</dimen>
<dimen name="dp_7">4.66px</dimen>
<!-- 省略其他 -->
</resources>

同样需要在values添加默认尺寸dimen.xml,内容同基准分辨率文件。


因为不是所有设备屏幕都是16:9的,也可以按照宽高拆分成两个dimens.xml文件dimen_x.xml和dimen_y.xml,按照宽高:1920x1080分别换算得到x和y的值,但是页面设计通常是竖屏可滑动的,所以对高度不敏感,只需要根据一个维度计算统一值就可以了。


以上计算方式比较简单了,不需要自己编写换算可以通过代码工具或者自己写个类实现。(网上有好多,找一下应该可以找到)。


理论上只要尽可能多的枚举所有设备分辨率,就可以完美的解决屏幕适配问题,所以UI适配的第四条是:


4.使用宽高限定符,精确匹配屏幕分辨率。


这种方案已经近乎完美了,一度成为比较热门的解决方案,也有很多团队使用过此方案。但是之前也说过Android设备的碎片化太严重了,综合考虑基本不可能在项目中枚举所有的屏幕尺寸进行适配,如果设备没有匹配到对应尺寸会使用values下的默认尺寸文件,可能会出现严重的UI适配问题。


但是不可否认此种方案实现简单,对于编写布局也很友好(直接填入设计图的尺寸值就行,不需要换算),可以解决绝大多数的设备适配问题,是一种很友好的解决方案。


第三方UI适配框架


有很多第三方库的解决方案,是从ViewGroup入手的,要么重写常用的如:RelativeLayout、LinearLayout和FrameLayout等在控件内部做转换来适配不同尺寸的设备,要么提供新的Layout如:Google的PercentLayout布局。但是这些方案基本都不在维护了,这里就不详细展开了,感兴趣的可以自行搜索了解。


UI适配的第五条是:


5. 使用第三方自适配框架,解决UI适配问题。


感兴趣的可以参考以下文档:



其他适配方案


参考字节的实现方案:


一种极低成本的Android屏幕适配方式


这篇文章着实属于拾人牙慧了,起因是因为看到了这篇博客Android 目前最稳定和高效的UI适配方案。所以想着确实应该把这部分知识梳理一下,所以写了这篇文档加了一些自己的里面,主要也是为了梳理知识点加深理解。


文中列举的几种UI适配方案,没有严格的优劣之分,可以根据自己的业务需求选择,也可以选择几种搭配使用,比如笔者目前主要做智能电视(盒子)的应用开发,Android电视不同于手机,碎片化没有那么严重,电视分辨率种类屈指可数,所以在日常项目中基本选择使用宽高限定符的方案进行适配,效果也是极好的。


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

sleep()为什么要 try catch

前言 当我们在 Java 中使用 sleep() 让线程休眠的时候,总是需要使用 try catch 去包含它: try { sleep(1000); } catch (InterruptedExcept...
继续阅读 »

前言


当我们在 Java 中使用 sleep() 让线程休眠的时候,总是需要使用 try catch 去包含它:


        try {
sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}

但是,我们却很少在 catch 中执行其它代码,仿佛这个 try catch 是理所当然一样,但其实,存在即合理,不可能无缘无故地多出一个 try catch。


在给出原因之前,我先要讲讲另外一个知识点,那就是如何去停止线程。


如何停止线程


stop()


最直接了断的方法就是调用 stop(),它能直接结束线程的执行,就如:


        // 开启线程循环输出当前的时间
Thread thread = new Thread(){
@Override
public void run() {
while (true){
System.out.println(System.currentTimeMillis());
}
}
};
thread.start();
// 睡眠两秒后停止线程
try {
sleep(2000);
thread.stop();
} catch (InterruptedException e) {
e.printStackTrace();
}

输出结果:


···
1643730391073
1643730391073
1643730391073
1643730391073
1643730391073

Process finished with exit code 0

很明显是能够把线程暂停掉的。但是,该方法现在被标记遗弃状态。



大概意思就是:


这个方法原本是设计用来停止线程并抛出线程死亡的异常,但是实际上它是不安全的,因为它是直接终止线程解放锁,这很难正确地抛出线程死亡的异常进行处理,所以,更好的方式是设置一个判断条件,然后在线程执行的过程中去判断该条件以去决定是否要进行停止线程,这样就能进行处理并有序地退出这个线程。假如一个线程等待很久的话,那才会直接中断线程并抛出异常。


按照上面所说,我们可以设置一个变量来进行控制,当然,我们可以声明一个 bool 类型进行判断,但是更好的方式是使用 interrupt()。


interrupt()


源码:


    public void interrupt() {
if (this != Thread.currentThread())
checkAccess();

synchronized (blockerLock) {
Interruptible b = blocker;
if (b != null) {
interrupt0(); // Just to set the interrupt flag
b.interrupt(this);
return;
}
}
interrupt0();
}

Just to set the interrupt flag 这里很明显就是说明只是进行了一个中断标记而已,并不会直接中断线程,所以,需要使用 interrupt() 的时候,我们还要在线程中进行 interrupt 的状态判断:



// 开启线程循环输出当前的时间
// 每次循环前都去判断该线程是否被中断了
Thread thread = new Thread(){
@Override
public void run() {
while (true){
if(isInterrupted()){
return;
}
System.out.println(System.currentTimeMillis());
}
}
};
thread.start();
// 睡眠两秒后标记中断线程
try {
sleep(2000);
thread.interrupt();
} catch (InterruptedException e) {
e.printStackTrace();
}

输出结果:


···
1643731839919
1643731839919
1643731839919

Process finished with exit code 0

这样也能正常的中断线程。



好了,如何中断线程到这里讲完了,大家拜拜~~


喂!等下,这文章是不是还有东西没讲 (#`O′),sleep()为什么要 try catch 还没说。



好像是这样。


中断等待


我们再看看 stop() 被标记为遗弃的说明:


 If the target thread waits for long periods (on a condition variable, for example),
the interrupt method should be used to interrupt the wait.

也就是说,当线程在等待过久的时候,interrupt() 应该去中断这个等待。


所以,原因就找到了,要加 try catch,是因为当线程在 sleep() 的时候,调用线程的 interrupt() 方法,就会直接中断线程的 sleep 状态,抛出 InterruptedException。


因为调用 interrupt() 的时候,其实就是想尽快地结束线程,所以,继续的 sleep 是没有意义的,应该尽快结束。


        // 开启线程
// 线程睡眠 10 秒
Thread thread = new Thread(){
@Override
public void run() {
try {
sleep(10000);
} catch (InterruptedException e) {
System.out.println("sleep 状态被中断了!");
e.printStackTrace();
}
}
};
thread.start();
// 睡眠两秒后标记中断线程
try {
sleep(2000);
thread.interrupt();
} catch (InterruptedException e) {
e.printStackTrace();
}

输出结果:


sleep 状态被中断了!
java.lang.InterruptedException: sleep interrupted
at java.base/java.lang.Thread.sleep(Native Method)
at com.magic.vstyle.TestMain$1.run(TestMain.java:16)

Process finished with exit code 0

这时,我们就可以 catch 到这个异常后进行额外操作,例如回收资源等。这时,停止线程就是一种可控的行为了。


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

【解惑】App处于前台,Activity就不会被回收了?

昨天在康 KunMinX 大佬的:《重学安卓:Activity 生命周期的 3 个辟谣》,在加餐处看到这段:转换后的理解:单进程场景,Activity被回收只可能是因为进程被系统回收了。感觉不太对?因为在很久以前,遇到过这样一个场景:App...
继续阅读 »

昨天在康 KunMinX 大佬的:《重学安卓:Activity 生命周期的 3 个辟谣》,在加餐处看到这段:

转换后的理解:单进程场景,Activity被回收只可能是因为进程被系统回收了

感觉不太对?因为在很久以前,遇到过这样一个场景:

App打开多个Activity,然后手机晾一边,过一段时间后(屏幕常亮),点击回退,之前的Activity空白,然后重新加载了。

App在前台,不在栈顶的Activity却被干掉,但进程还健在,如果真是这样,就和上面的理解有些出入了。

立马写个代码验证下,大概流程如下:

写个父类Activity,生命周期回调加日志打印,接着打开一个Activity,包含一个按钮,点击后依次打开多个Activity,最后一个加个按钮,点一下就申请一个大一点的ByteArray来模拟内存分配,看内存不足时是否会回收Activity。

测试结果如下:

App宁愿OOM,也不愿意回收Activity,鬼使神差地加上 android:largeHeap="true" ,结果一样。

em...难道是我记错了???

等等!!!我好像混淆了两个东西:系统可用内存不足 和 应用可用内存不足

0x1、系统可用内存不足

LMK机制

Android系统中,进程的生命周期由系统控制,处于体验和性能考虑,在APP中点击Home键或Back回退操作,并不会真的杀掉APP,进程依旧存在于内存中,这样下次启动此APP时就能更加快速。随着系统运行时间增长,打开APP越来越多,内存中的进程随着增多,系统的可用内存会越来越少。咋办,总不能让用户自己去杀进程吧,所以系统内置一套 回收机制,当系统可用内存达到一个 阈值,系统会根据 进程优先级 来杀掉一部分进程,释放内存供后续启动APP使用。

Android的这套回收机制,是基于Linux内核的OOM规则改进而来的,叫 Low Memory Killer,简称 LMK

阈值 & 杀谁

通过下述两个文件配合完成,不同手机数值可能不同,以我的老爷机 魅蓝E2 为例 (Android 11的Mix2S一直说没权限打开此文件):

# /sys/module/lowmemorykiller/parameters/minfree
# 单位:Page页,1Page = 4KB
18432,23040,27648,46080,66560,97280

# /sys/module/lowmemorykiller/parameters/adj
0,58,117,176,529,1000

Android系统会为每个进程维护一个 adj(优先级)

  • Android 6及以前称为:oom_adj,值范围:[-17,16],LMK要换算*1000/17
  • Android 7后称为:oom_score_adj,值范围:[-1000,1000]

然后,上面两个文件的值,其实是以一一对应的,比如:

66560  * 4 / 1024 = 260MB → 当系统可用内存减少到260MB时,会杀掉adj值大于529的进程;
18432 * 4 / 1024 = 72MB → 当系统可用内存减少到72MB,杀掉ajd值大于0的进程;

adj怎么看

直接通过命令行查看:

可以看到,adj是动态变化的,当App状态及四大组件生命周期发生改变时,都会改变它的值。常见ADJ级别如下:

  • NATIVE_ADJ → -1000,init进程fork出来的native进程,不受system管控
  • SYSTEM_ADJ → -900,system_server进程
  • PERSISTENT_PROC_ADJ → -800,系统persistent进程,一般不会被杀,杀了或者Carsh系统也会重新拉起
  • PERSISTENT_SERVICE_ADJ → -700,关联着系统或persistent进程
  • FOREGROUND_APP_ADJ → 0,前台进程
  • VISIBLE_APP_ADJ → 100,可见进程
  • PERCEPTIBLE_APP_ADJ → 200,可感知进程,比如后台音乐播放
  • BACKUP_APP_ADJ → 300,执行bindBackupAgent()过程的备份进程
  • HEAVY_WEIGHT_APP_ADJ → 400,重量级进程,system/rootdir/init.rc文件中设置
  • SERVICE_ADJ → 500,服务进程
  • HOME_APP_ADJ → 600,Home进程,类型为ACTIVITY_TYPE_HOME的应用,如Launcher
  • PREVIOUS_APP_ADJ → 700,用户上一个使用的App进程
  • SERVICE_B_ADJ → 800,B List中的Service
  • CACHED_APP_MIN_ADJ → 900,不可见进程 的adj最小值
  • CACHED_APP_MAX_ADJ → 906,不可见进程的adj最大值
  • UNKNOWN_ADJ → 1001,一般指将要会缓存进程,无法获取确定值

关于ADJ计算的详细算法分析可见Gityuan大佬的:《解读Android进程优先级ADJ算法》,干货多多,顺带从总结处捞一波进程保活伎俩:

  • UI进程与Service进程分离,包含Activity的Service进程,一进后台ADJ>=900,随时可能被系统回收,分离的话ADJ=500,被杀的可能性降低,尤其是系统允许自启动的服务进程,必须做UI分离,避免消耗较大内存;
  • 真正需要用户可感知的应用,调用startForegroundService()启用前台服务,ADJ=200;
  • 进程中的Service工作完,务必主动调用stopService或stopSelf来停止服务,避免占用内存,浪费系统资源;
  • 不要长时间绑定其他进程的service或者provider,每次使用完成后应立刻释放,避免其他进程常驻于内存;
  • APP应该实现接口onTrimMemory()和onLowMemory(),根据TrimLevel适当地将非必须内存在回调方法中加以释放,当系统内存紧张时会回调该接口,减少系统卡顿与杀进程频次;
  • 更应在优化内存上下功夫,相同ADJ级别,系统会优先杀内存占用的进程;

:能否把自己的App的ADJ值设置为-1000,让其杀不死? :不可以,要有root权限才能修改adj,而且改了重启手机还是恢复的。

扯得有点远了,回到问题上:

系统内存不足时,会在内核层直接查杀进程,不会在Framework层还跟你叨逼叨看回收哪个Activity。

所以在系统这个层面,单进程场景,Activity被回收只可能是因为进程被系统回收了,这句话是没毛病的,但在应用层面就不一定了。


0x2、应用可用内存不足

APP进程(虚拟机)的内存分配实际上是对 堆的分配和释放,为了整个系统的内存控制需要,会为每个应用程序设置一个 堆的限制阈值,如果应用使用内存接近阈值还尝试分配内存,就很容易引起OOM。

当然,不会那么蠢,还要开发仔自己在APP里回收内存,虚拟机自带 GC,这里就不向去卷具体的回收算法了

假设应用内存不足真的会回收Activity,那该怎么设计?一种解法如下:

应用启动时,开一个子线程,定时轮询当前可用内存是否超过阈值,超过的话干掉Activity

那就来跟下Android是不是也是这样设计的?

Activity回收机制

跟下应用启动入口:ActivityThread → main()

跟下 attach()

这里就非常像,run()中计算:已用内存 > 3/4最大内存,就执行 releaseSomeActivities(),跟下:

所以 getService() 是获取了 IActivityTaskManager.aidl接口,具体的实现类是 ActivityTaskManangerService

继续往下跟: RootActivityContainer → releaseSomeActivitiesLocked()

跟下:WindowProcessController → getReleaseSomeActivitiesTasks()

然后再往下走就是释放Activity的代码了:ActivityStack → releaseSomeActivitiesLocked()

具体咋释放,就不往下跟了哈,接着跟下是怎么监控的~

内存监控机制

跟回:BinderInternal.addGcWatcher()

这里可能看得你有点迷,但是当你理解了就会觉得很妙了:

虚拟机GC会干掉 WeakReference 的对象,在释放内存前,会调用对象的 finalize(),而这里有创建了一个新的 WeakReference 实例。下次GC,又会走一遍这里的代码,啧啧啧,相比起轮询高效多了

到此,应用内存不足回收Activity的流程就大概缕清了,接着可以写个代码验证下是否真的这样。

Demo验证

先试下两个Task的:

模拟内存分配的页面,然后一直点~

宁愿OOM,也不回收,试试三个~

好家伙,onDestory()了,此时按Back回退这些页面,发现走了onCreate(),即回收了,接着试试四个的情况:

可以,每次只回收一个Task,到此验证完毕了~

0x3、结论

  • 系统内存不足时,直接在内核层查杀(回收)进程,并不会考虑回收哪个Activity;
  • 进程内存不足时,如果此进程 Activity Task数 >= 3 且 使用内存超过3/4,会对 不可见 Task进行回收,每次回收 1个 Task,回收时机为每次gc;

参考文献


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

收起阅读 »

Androd Gradle 使用技巧之模块依赖替换

背景 我们在多模块项目开发过程中,会遇到这样的场景,工程里依赖了一个自己的或者其他同事的 aar 模块,有时候为了开发调试方便,经常会把 aar 改为本地源码依赖,开发完毕并提交的时候,会再修改回 aar 依赖,这样就会很不方便,开发流程图示如下: 解决 一...
继续阅读 »

背景


我们在多模块项目开发过程中,会遇到这样的场景,工程里依赖了一个自己的或者其他同事的 aar 模块,有时候为了开发调试方便,经常会把 aar 改为本地源码依赖,开发完毕并提交的时候,会再修改回 aar 依赖,这样就会很不方便,开发流程图示如下:


截屏2022-02-09 下午4.56.16.png


解决


一开始我们通过在 appbuild.gradle 里的 dependency 判断如果是需要本地依赖的 aar,就替换为 implementation project 依赖,伪代码如下:


dependencies {
if(enableLocalModule) {
implementation 'custom:test:0.0.1'
} else {
implementation project(path: ':test')
}
}

这样就可以不用每次提交代码还要修改回 aar 依赖,但是如果其他模块如果也依赖了该 aar 模块,就会出现问题,虽然可以继续修改其他模块里的依赖方式,但是这样就会有侵入性,而且不能彻底解决问题,仍然有可能出现本地依赖和 aar 依赖的代码不一致问题。


Gradle 官方针对这种场景提供了更好的解决方式 DependencySubstitution,使用方式如下:


步骤1:在 settting.gradle,添加如下代码:


// 加载本地 module
if (file("local.properties").exists()) {
def properties = new Properties()
def inputStream = file("local.properties").newDataInputStream()
properties.load( inputStream )
def moduleName = properties.getProperty("moduleName")
def modulePath = properties.getProperty("modulePath")
if (moduleName != null && modulePath != null) {
include moduleName
project(moduleName).projectDir = file(modulePath)
}
}

步骤2:在 appbuild.gradle 添加以下代码


configurations.all {
resolutionStrategy.dependencySubstitution.all { DependencySubstitution dependency ->
// use local module
if (dependency.requested instanceof ModuleComponentSelector && dependency.requested.group == "custom") {
def targetProject = findProject(":test")
if (targetProject != null) {
dependency.useTarget targetProject
}
}
}
}

步骤3::在 local.properties


moduleName=:test
modulePath=../AndroidStudioProjects/TestProject/testModule

到这里就大功告成了,后续只需要在 local.properties 里开启和关闭,即可实现 aar 模块本地依赖调试,提交代码也不用去手动修改回 aar 依赖。


参考



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

Android 使用 Retrofit 发送网络请求

简介 在Android应用中,如果不是单机的话,应该都有请求后端接口API的情况,本篇文章就介绍下Retrofit在Android中如何进行使用的 相关代码 我们以一个简单的登录接口为例 完整代码GitHub上有:github.com/lw124392545…...
继续阅读 »

简介


在Android应用中,如果不是单机的话,应该都有请求后端接口API的情况,本篇文章就介绍下Retrofit在Android中如何进行使用的


相关代码


我们以一个简单的登录接口为例


完整代码GitHub上有:github.com/lw124392545…


仅做代码参考,目前数据监控上传是有了,但界面这些还很粗糙,没有完善


相关的依赖引入


首先我们在工程中引入相关的依赖:


    implementation 'com.squareup.okhttp3:okhttp:4.5.0'
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'

相关的手机权限开启


需要在文件中开启网络权限:


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

<uses-permission android:name="android.permission.INTERNET"/>

<application
......
</application>

</manifest>

配置Retrofit Client


Client的相关配置:单例,配置基于OKHTTP,Gson序列化;OKHTTP中添加了请求拦截器


@Data
public class RetrofitClient {

private static final RetrofitClient instance = new RetrofitClient();

private final Retrofit retrofit = new Retrofit.Builder()
.baseUrl(HttpConfig.ADDRESS) //基础url,其他部分在GetRequestInterface里
.client(httpClient())
.addConverterFactory(GsonConverterFactory.create()) //Gson数据转换器
.build();

public static RetrofitClient getInstance() {
return instance;
}

private OkHttpClient httpClient() {
return new OkHttpClient.Builder()
.addInterceptor(new AccessTokenInterceptor())
.connectTimeout(20, TimeUnit.SECONDS)
.build();
}
}

配置通用的请求拦截器


比如在请求中,带上Authorization等


public class AccessTokenInterceptor implements Interceptor {

@NonNull
@Override
public Response intercept(@NonNull Chain chain) throws IOException {
if (UserCache.getInstance().getToken() == null) {
return chain.proceed(chain.request());
}

Request original = chain.request();
Request.Builder requestBuilder = original.newBuilder()
.addHeader("Authorization", UserCache.getInstance().getToken());
Request request = requestBuilder.build();
return chain.proceed(request);
}
}

Retrofit接口定义


登录请求的接口定义:


public interface UserApi {

/**
* 用户登录
**/
@POST("auth/user/login")
Call<ApiResponse> login(@Body LoginUser user);
}

Retrofit Request具体请求编写


我们首先定义一个抽象类,在其中持有我们的RetrofitClient全局类,在其中发起请求,由于Android UI的形式,请求是异步的


public abstract class Request {

final Retrofit retrofit;

public Request() {
this.retrofit = RetrofitClient.getInstance().getRetrofit();
}

/**
* 发送网络请求(异步)
* @param call call
*/
void sendRequest(Call<ApiResponse> call, Consumer<? super Object> success, Consumer<? super Object> failed) {
call.enqueue(new Callback<ApiResponse>() {
@Override
public void onResponse(Call<ApiResponse> call, Response<ApiResponse> response) {
if (response.code() != 200) {
Log.w("Http Response", "请求响应错误");
failed.accept(response.raw().message());
return;
}
if (response.body() == null || response.body().getData() == null) {
success.accept(null);
return;
}
Object res = response.body().getData();
if (String.valueOf(res).isEmpty()) {
success.accept(null);
return;
}
success.accept(res);
}

@Override
public void onFailure(Call<ApiResponse> call, Throwable t) {
System.out.println("GetOutWarehouseList->onFailure(MainActivity.java): "+t.toString() );
}
});
}
}

如上所示,请求成功就执行success相关的逻辑,失败则执行failed相关的逻辑


登录请求的具体逻辑如下:构造Retrofit Interface,发起请求


public class UserRequest extends Request {

public void login(LoginUser user, Consumer<? super Object> success, Consumer<? super Object> failed) {
UserApi request = retrofit.create(UserApi.class);
Call<ApiResponse> call = request.login(user);
sendRequest(call, success, failed);
}
}

Android UI中进行调动


使用示例如下,点击一个登录按钮后触发


public class LoginFragment extends Fragment {

private final UserRequest userRequest = new UserRequest();

@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View rootView = inflater.inflate(R.layout.fragment_login, container, false);
Button loginButton = rootView.findViewById(R.id.login_button);
loginButton.setOnClickListener(view -> {
EditText email = rootView.findViewById(R.id.login_email_edit);
EditText password = rootView.findViewById(R.id.login_password_edit);

final LoginUser user = LoginUser.builder()
.email(email.getText().toString())
.password(password.getText().toString())
.build();

// 获取相关的用户名和密码后,调用登录接口
userRequest.login(user, (token) -> {
UserCache.getInstance().initUser(email.getText().toString(), token.toString());
final SharedPreferences preferences = requireContext().getSharedPreferences("userInfo", Context.MODE_PRIVATE);
final SharedPreferences.Editor edit = preferences.edit();
edit.putString("username", email.getText().toString());
edit.putString("password", password.getText().toString());
edit.apply();
Snackbar.make(view, "登录成功:" + token.toString(), Snackbar.LENGTH_LONG)
.setAction("Action", null).show();
}, failedMessage -> {
Snackbar.make(view, "登录失败:" + failedMessage, Snackbar.LENGTH_LONG)
.setAction("Action", null).show();
});
});
return rootView;
}

@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
final SharedPreferences preferences = getActivity().getSharedPreferences("userInfo", Activity.MODE_PRIVATE);
final String userName = preferences.getString("username", "");
final String password = preferences.getString("password", "");
final EditText emailEdit = getView().findViewById(R.id.login_email_edit);
final EditText passwordEdit = getView().findViewById(R.id.login_password_edit);
emailEdit.setText(userName);
passwordEdit.setText(password);
}
}

总结


本篇文章中介绍了如Android学习中如何使用Retrofit发起网络请求


但由于吃初学,虽然感觉能用,但有点繁琐,不知道在实际的Android开发中,网络请求的最近实践是怎么样的,如果有的话,大佬可以在评论区告知下,感谢


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

Flutter之GetX依赖注入使用详解

put 为了验证依赖注入的功能,首先创建两个测试页面:PageA 和 PageB ,PageA 添加两个按钮 toB 和 find ,分别为跳转 PageB 和获取依赖;在 PageB 中通过 put 方法注入依赖对象,然后调用按钮触发 find 获取依赖。关...
继续阅读 »

put


为了验证依赖注入的功能,首先创建两个测试页面:PageA 和 PageB ,PageA 添加两个按钮 toBfind ,分别为跳转 PageB 和获取依赖;在 PageB 中通过 put 方法注入依赖对象,然后调用按钮触发 find 获取依赖。关键源码如下:


PageA


TextButton(
child: const Text("toB"),
onPressed: (){
/// Navigator.push(context, MaterialPageRoute(builder: (context) => const PageB()));
/// Get.to(const PageB());
},
),

TextButton(
child: const Text("find"),
onPressed: () async {
User user = Get.find();
print("page a username : ${user.name} id: ${user.id}");
})


PageB:


Get.put(User.create("张三", DateTime.now().millisecondsSinceEpoch)));

User user = Get.find();

TextButton(
child: const Text("find"),
onPressed: (){
User user = Get.find();
print("${DateTime.now()} page b username : ${user.name} id: ${user.id}");
})

其中 User 为自定义对象,用于测试注入,源码如下:


User:


class User{
final String? name;
final int? id;

factory User.create(String name, int id){
print("${DateTime.now()} create User");
return User(name, id);
}
}

Navigator 路由跳转


首先使用 Flutter 自带的路由管理从 PageA 跳转 PageB, 然后返回 PageA 再点击 find 按钮获取 User 依赖:


Navigator.push(context, MaterialPageRoute(builder: (context) => const PageB()));

流程:PageA -> PageB -> put -> find -> PageA -> find


输出结果:


/// put
I/flutter (31878): 2022-01-27 19:18:20.851800 create User
[GETX] Instance "User" has been created

/// page b find
I/flutter (31878): 2022-01-27 19:18:22.170133 page b username : 张三 id: 1643282300139

/// page a find
I/flutter (31878): 2022-01-27 19:18:25.554667 page a username : 张三 id: 1643282300139

通过输出结果发现,在 PageB 注入的依赖 User,在返回 PageA 后通过 find 依然能获取,并且是同一个对象。通过 Flutter 通过源码一步一步剖析 Getx 依赖管理的实现 这篇文章知道,在页面销毁的时候会回收依赖,但是这里为什么返回 PageA 后还能获取到依赖对象呢?是因为在页面销毁时回收有个前提是使用 GetX 的路由管理页面,使用官方的 Navigator 进行路由跳转时页面销毁不会触发回收依赖。


GetX 路由跳转


接下来换成使用 GetX 进行路由跳转进行同样的操作,再看看输出结果:


Get.to(const PageB());

流程:PageA -> PageB -> put -> find -> PageA -> find


输出结果:


[GETX] GOING TO ROUTE /PageB

/// put
I/flutter (31878): 2022-01-27 19:16:32.014530 create User
[GETX] Instance "User" has been created

/// page b find
I/flutter (31878): 2022-01-27 19:16:34.043144 page b username : 张三 id: 1643282192014
[GETX] CLOSE TO ROUTE /PageB
[GETX] "User" deleted from memory

/// page a find error
E/flutter (31878): [ERROR:flutter/lib/ui/ui_dart_state.cc(209)] Unhandled Exception: "User" not found. You need to call "Get.put(User())" or "Get.lazyPut(()=>User())"

发现在 PageB 中获取是正常,关闭 PageB 时输出了一句 "User" deleted from memory 即在 PageB 注入的 User 被删除了,此时在 PageA 再通过 find 获取 User 就报错了,提示需要先调用 put 或者 lazyPut 先注入依赖对象。这就验证了使用 GetX 路由跳转时,使用 put 默认注入依赖时,当页面销毁依赖也会被回收。


permanent


put 还有一个 permanent 参数,在 Flutter应用框架搭建(一)GetX集成及使用详解 这篇文章里介绍过,permanent 的作用是永久保留,默认为 false,接下来在 put 时设置 permanent 为 true,并同样使用 GetX 的路由跳转重复上面的流程。


关键代码:


Get.put(User.create("张三", DateTime.now().millisecondsSinceEpoch), permanent: true);

流程:PageA -> PageB -> put -> find -> PageA -> find


输出结果:


[GETX] GOING TO ROUTE /PageB
/// put
I/flutter (31878): 2022-01-27 19:15:16.110403 create User
[GETX] Instance "User" has been created

/// page b find
I/flutter (31878): 2022-01-27 19:15:18.667360 page b username : 张三 id: 1643282116109
[GETX] CLOSE TO ROUTE /PageB
[GETX] "User" has been marked as permanent, SmartManagement is not authorized to delete it.

/// page a find success
I/flutter (31878): page a username : 张三 id: 1643282116109

设置 permanent 为 true 后,返回 PageA 同样能获取到依赖对象,说明依赖并没有因为页面销毁而回收,GetX 的日志输出也说明了 User 被标记为 permanent 而不会被删除:"User" has been marked as permanent, SmartManagement is not authorized to delete it.


lazyPut


lazyPut 为延迟初始化依赖对象 :


Get.lazyPut(() => User.create("张三", DateTime.now().millisecondsSinceEpoch));

TextButton(
child: const Text("find"),
onPressed: () async {
User user = Get.find();
print("${DateTime.now()} page b username : ${user.name} id: ${user.id}");
})

流程:PageA -> PageB -> put -> find -> find -> PageA -> find, 从 PageA 跳转 PageB,先通过 lazyPut 注入依赖,然后点击 find 获取依赖,过 3 秒再点击一次,然后返回 PageA 点击 find 获取一次。


输出结果:


[GETX] GOING TO ROUTE /PageB
/// lazyPut

/// page b find 1
I/flutter (31878): 2022-01-27 17:38:49.590295 create User
[GETX] Instance "User" has been created
I/flutter (31878): 2022-01-27 17:38:49.603063 page b username : 张三 id: 1643276329589

/// page b find 2
I/flutter (31878): 2022-01-27 17:38:52.297049 page b username : 张三 id: 1643276329589
[GETX] CLOSE TO ROUTE /PageB
[GETX] "User" deleted from memory

/// page a find error
E/flutter (31878): [ERROR:flutter/lib/ui/ui_dart_state.cc(209)] Unhandled Exception: "User" not found. You need to call "Get.put(User())" or "Get.lazyPut(()=>User())"

通过日志发现 User 对象是在第一次调用 find 时进行初始化话的,第二次 find 时不会再次初始化 User;同样的 PageB 销毁时依赖也会被回收,导致在 PageA 中获取会报错。


fenix


lazyPut 还有一个 fenix 参数默认为 false,作用是当销毁时,会将依赖移除,但是下次 find 时又会重新创建依赖对象。


lazyPut 添加 fenix 参数 :


 Get.lazyPut(() => User.create("张三", DateTime.now().millisecondsSinceEpoch), fenix: true);

流程:PageA -> PageB -> put -> find -> find -> PageA -> find,与上面流程一致。


输出结果:


[GETX] GOING TO ROUTE /PageB
/// lazyPut

/// page b find 1
[GETX] Instance "User" has been created
I/flutter (31878): 2022-01-27 17:58:58.321564 create User
I/flutter (31878): 2022-01-27 17:58:58.333369 page b username : 张三 id: 1643277538321

/// page b find 2
I/flutter (31878): 2022-01-27 17:59:01.647629 page b username : 张三 id: 1643277538321
[GETX] CLOSE TO ROUTE /PageB

/// page a find success
I/flutter (31878): 2022-01-27 17:59:07.666929 create User
[GETX] Instance "User" has been created
I/flutter (31878): page a username : 张三 id: 1643277547666

通过输出日志分析,在 PageB 中的表现与不加 fenix 表现一致,但是返回 PageA 后获取依赖并没有报错,而是重新创建了依赖对象。这就是 fenix 的作用。


putAsync


putAsyncput 基本一致,不同的是传入依赖可以异步初始化。测试代码修改如下:


print("${DateTime.now()} : page b putAsync User");
Get.putAsync(() async {
await Future.delayed(const Duration(seconds: 3));
return User.create("张三", DateTime.now().millisecondsSinceEpoch);
});

使用 Future.delayed 模拟耗时操作。


流程:PageA -> PageB -> put -> find -> PageA -> find


输出结果:


[GETX] GOING TO ROUTE /PageB

/// putAsync
I/flutter (31878): 2022-01-27 18:48:34.280337 : page b putAsync User

/// create user
I/flutter (31878): 2022-01-27 18:48:37.306073 create User
[GETX] Instance "User" has been created

/// page b find
I/flutter (31878): 2022-01-27 18:48:40.264854 page b username : 张三 id: 1643280517305
[GETX] CLOSE TO ROUTE /PageB
[GETX] "User" deleted from memory

/// page a find error
E/flutter (31878): [ERROR:flutter/lib/ui/ui_dart_state.cc(209)] Unhandled Exception: "User" not found. You need to call "Get.put(User())" or "Get.lazyPut(()=>User())"

通过日志发现,put 后确实是过了 3s 才创建 User。


create


createpermanent 参数默认为 true,即永久保留,但是通过 Flutter应用框架搭建(一)GetX集成及使用详解 这篇源码分析知道,create 内部调用时 isSingleton 设置为 false,即每次 find 时都会重新创建依赖对象。


Get.create(() => User.create("张三", DateTime.now().millisecondsSinceEpoch));

流程:PageA -> PageB -> put -> find -> find -> PageA -> find


输出结果:


[GETX] GOING TO ROUTE /PageB
/// create

/// page b find 1
I/flutter (31878): 2022-01-27 18:56:10.520961 create User
I/flutter (31878): 2022-01-27 18:56:10.532465 page b username : 张三 id: 1643280970520

/// page b find 2
I/flutter (31878): 2022-01-27 18:56:18.933750 create User
I/flutter (31878): 2022-01-27 18:56:18.934188 page b username : 张三 id: 1643280978933

[GETX] CLOSE TO ROUTE /PageB

/// page a find success
I/flutter (31878): 2022-01-27 18:56:25.319224 create User
I/flutter (31878): page a username : 张三 id: 1643280985319

通过日志发现,确实是每次 find 时都会重新创建 User 对象,并且退出 PageB 后还能通过 find 获取依赖对象。


总结


通过代码调用不同的注入方法,设置不同的参数,分析输出日志,详细的介绍了 putlazyPutputAsynccreate 以及 permanentfenix 参数的具体作用,开发中可根据实际业务场景灵活使用不同注入方式。关于注入的 tag 参数将在后续文章中详细介绍。


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

图解 ArrayDeque 比 LinkedList 快

在之前的两篇文章中主要分析了 Java 栈的缺点 ,为什么不推荐使用 Java 栈 ,以及 为什么不推荐直接使用 ArrayDeque 代替 Java Stack 。更多内容点击下方链接前去查看。 算法动画图解 | 被 "废弃" 的 Java 栈,为什么还在...
继续阅读 »

在之前的两篇文章中主要分析了 Java 栈的缺点为什么不推荐使用 Java 栈 ,以及 为什么不推荐直接使用 ArrayDeque 代替 Java Stack 。更多内容点击下方链接前去查看。



接口 Deque 的子类 ArrayDeque ,作为栈使用时比 Stack 快,因为原来的 Java 的 Stack 继承自 Vector,而 Vector 在每个方法中都加了锁,而 Deque 的子类 ArrayDeque 并没有锁的开销。


接口 Deque 还有另外一个子类 LinkedListLinkedList 基于双向链表实现的双端队列,ArrayDeque 作为队列使用时可能比 LinkedList 快。


而这篇文章主要来分析,为什么 ArrayDequeLinkedList 快。在开始分析之前,我们需要简单的了解一下它们的数据结构的特点。


接口 Deque


接口 Deque 继承自 Queue 即队列, 在 Java 中队列有两种形式,单向队列( AbstractQueue ) 和 双端队列( Deque ),单向队列效果如下所示,只能从一端进入,另外一端出去。



而今天主要介绍双端队列( Deque ), Deque 是双端队列的线性数据结构, 可以在两端进行插入和删除操作,效果如下所示。



双端队列( Deque )的子类分别是 ArrayDequeLinkedListArrayDeque 基于数组实现的双端队列,而 LinkedList 基于双向链表实现的双端队列,它们的继承关系如下图所示。



接口 DequeQueue 提供了两套 API ,存在两种形式,分别为抛出异常,和不抛出异常,返回一个特殊值 null 或者布尔值 ( true | false )。



























操作类型抛出异常返回特殊值
插入addXXX(e)offerXXX(e)
移除removeXXX()pollXXX()
查找element()peekXXX()

ArrayDeque


ArrayDeque 是基于(循环)数组的方式实现双端队列,数组初始化容量为 16(JDK 8),结构图如下所示。



ArrayDeque 具有以下特点:



  • 因为双端队列只能在头部和尾部插入或者删除元素,所以时间复杂度为 O(1),但是在扩容的时候需要批量移动元素,其时间复杂度为 O(n)

  • 扩容的时候,将数组长度扩容为原来的 2 倍,即 n << 1

  • 数组采用连续的内存地址空间,所以查询的时候,时间复杂度为 O(1)

  • 它是非线程安全的集合


LinkedList


LinkedList 基于双向链表实现的双端队列,它的结构图如下所示。



LinkedList 具有以下特点:



  • LinkedList 是基于双向链表的结构来存储元素,所以长度没有限制,因此不存在扩容机制

  • 由于链表的内存地址是非连续的,所以只能从头部或者尾部查找元素,查询的时间复杂为 O(n),但是 JDK 对 LinkedList 做了查找优化,当我们查找某个元素时,若 index < (size / 2),则从 head 往后查找,否则从 tail 开始往前查找 , 但是我们在计算时间复杂度的时候,常数项可以省略,故时间复杂度 O(n)


Node<E> node(int index) {
// size >> 1 等价于 size / 2
if (index < (size >> 1)) {
// form head to tail
} else {
// form tail to head
}
}


  • 链表通过指针去访问各个元素,所以插入、删除元素只需要更改指针指向即可,因此插入、删除的时间复杂度 O(1)

  • 它是非线程安全的集合


最后汇总一下 ArrayDequeLinkedList 的特点如下所示:































集合类型数据结构初始化及扩容插入/删除时间复杂度查询时间复杂度是否是线程安全
ArrqyDeque循环数组初始化:16
扩容:2 倍
0(n)0(1)
LinkedList双向链表0(1)0(n)

为什么 ArrayDeque 比 LinkedList 快


了解完数据结构特点之后,接下来我们从两个方面分析为什么 ArrayDeque 作为队列使用时可能比 LinkedList 快。




  • 从速度的角度:ArrayDeque 基于数组实现双端队列,而 LinkedList 基于双向链表实现双端队列,数组采用连续的内存地址空间,通过下标索引访问,链表是非连续的内存地址空间,通过指针访问,所以在寻址方面数组的效率高于链表。




  • 从内存的角度:虽然 LinkedList 没有扩容的问题,但是插入元素的时候,需要创建一个 Node 对象, 换句话说每次都要执行 new 操作,当执行 new 操作的时候,其过程是非常慢的,会经历两个过程:类加载过程 、对象创建过程。




    • 类加载过程



      • 会先判断这个类是否已经初始化,如果没有初始化,会执行类的加载过程

      • 类的加载过程:加载、验证、准备、解析、初始化等等阶段,之后会执行 <clinit>() 方法,初始化静态变量,执行静态代码块等等




    • 对象创建过程



      • 如果类已经初始化了,直接执行对象的创建过程

      • 对象的创建过程:在堆内存中开辟一块空间,给开辟空间分配一个地址,之后执行初始化,会执行 <init>() 方法,初始化普通变量,调用普通代码块






接下来我们通过 算法动画图解 | 被 "废弃" 的 Java 栈,为什么还在用 文章中 LeetCode 算法题:有效的括号,来验证它们的执行速度,以及在内存方面的开销,代码如下所示:


class Solution {
public boolean isValid(String s) {

// LinkedList VS ArrayDeque

// Deque<Character> stack = new LinkedList<Character>();
Deque<Character> stack = new ArrayDeque<Character>();

// 开始遍历字符串
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
// 遇到左括号,则将其对应的右括号压入栈中
if (c == '(') {
stack.push(')');
} else if (c == '[') {
stack.push(']');
} else if (c == '{') {
stack.push('}');
} else {
// 遇到右括号,判断当前元素是否和栈顶元素相等,不相等提前返回,结束循环
if (stack.isEmpty() || stack.poll() != c) {
return false;
}
}
}
// 通过判断栈是否为空,来检查是否是有效的括号
return stack.isEmpty();
}
}

正如你所看到的,核心算法都是一样的,通过接口 Deque 来访问,只是初始化接口 Deque 代码不一样。


// 通过 LinkedList 初始化     
Deque<Character> stack = new LinkedList<Character>();

// 通过 ArrayDeque 初始化
Deque<Character> stack = new ArrayDeque<Character>();


结果如上所示,无论是在执行速度、还是在内存开销上 ArrayDeque 的性能都比 LinkedList 要好。





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


代码不止,文章不停


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







最后推荐长期更新和维护的项目:




  • 个人博客,将所有文章进行分类,欢迎前去查看 hi-dhl.com




  • KtKit 小巧而实用,用 Kotlin 语言编写的工具库,欢迎前去查看 KtKit




  • 计划建立一个最全、最新的 AndroidX Jetpack 相关组件的实战项目 以及 相关组件原理分析文章,正在逐渐增加 Jetpack 新成员,仓库持续更新,欢迎前去查看 AndroidX-Jetpack-Practice




  • LeetCode / 剑指 offer / 国内外大厂面试题 / 多线程 题解,语言 Java 和 kotlin,包含多种解法、解题思路、时间复杂度、空间复杂度分析






近期必读热门文章



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

Kotlin常用的by lazy你真的了解吗

前言 在使用Kotlin语言进行开发时,我相信很多开发者都信手拈来地使用by或者by lazy来简化你的属性初始化,但是by lazy涉及的知识点真的了解吗 假如让你实现这个功能,你会如何设计。 正文 话不多说,我们从简单的属性委托by来说起。 委托属性 什...
继续阅读 »

前言


在使用Kotlin语言进行开发时,我相信很多开发者都信手拈来地使用by或者by lazy来简化你的属性初始化,但是by lazy涉及的知识点真的了解吗 假如让你实现这个功能,你会如何设计。


正文


话不多说,我们从简单的属性委托by来说起。


委托属性


什么是委托属性呢,比较官方的说法就是假如你想实现一个比较复杂的属性,它们处理起来比把值保存在支持字段中更复杂,但是却不想在每个访问器都重复这样的逻辑,于是把获取这个属性实例的工作交给了一个辅助对象,这个辅助对象就是委托。


比如可以把这个属性的值保存在数据库中,一个Map中等,而不是直接调用其访问器。


看完这个委托属性的定义,假如你不熟悉Kotlin也可以理解,就是我这个类的实例由另一个辅助类对象来提供,但是这时你可能会疑惑,上面定义中说的支持字段和访问器是什么呢,这里顺便给不熟悉Kotlin的同学普及一波。


Java的属性

当你定义一个Java类时,在定义字段时并不是所有字段都是属性,比如下面代码:


//Java类
public class Phone {

//3个字段
private String name;
private int price;
private int color;

//name字段访问器
private String getName() {
return name;
}

private void setName(String name){
this.name = name;
}

//price字段访问器
private int getPrice() {
return price;
}

private void setPrice(int price){
this.price = price;
}
}

上面我在Phone类中定义了3个字段,但是只有name和price是Phone的属性,因为这2个字段有对应的get和set,也只有符合有getter和setter的字段才叫做属性。


这也能看出Java类的属性值是保存在字段中的,当然你也可以定义setXX函数和getXX函数,既然XX属性没有地方保存,XX也是类的属性。


Kotlin的属性

而对于Kotlin的类来说,属性定义就非常简单了,比如下面类:


class People(){
val name: String? = null
var age: Int? = null
}

在Kotlin的类中只要使用val/var定义的字段,它就是类的属性,然后会自带getter和setter方法(val属性相当于Java的final变量,是没有set方法的),比如下面:


val people = People()
//调用name属性的getter方法
people.name
//调用age属性的setter方法
people.age = 12

这时就有了疑问,为什么上面代码定义name时我在后面给他赋值了即null值,和Java一样不赋值可以吗 还有个疑问就是在Java中是把属性的值保存在字段中,那Kotlin呢,比如name这个属性的值就保存给它自己吗


带着问题,我们继续分析。


Kotlin属性访问器

前面我们可知Java中的属性是保存在字段中,或者不要字段,其实Kotlin也可以,这个就是给属性定义自定义setter方法和getter方法,如下代码:


class People(){
val name: String? = null
var age: Int = 0
//定义了isAbove18这个属性
var isAbove18: Boolean = false
get() = age > 18
}

比如这里自定义了get访问器,当再访问这个属性时,便会调用其get方法,然后进行返回值。


Kotlin属性支持字段field

这时一想那Kotlin的属性值保存在哪里呢,Kotlin会使用一个field的支持字段来保存属性。如下代码:


class People{
val name: String? = null
var age: Int = 0
//返回field的值
get() = field
//设置field的值
set(value){
Log.i("People", "旧值是$field 新值是$value ")
field = value
}

var isAbove18: Boolean = false
get() = age > 18
}

可以发现每个属性都会有个支持字段field来保存属性的值。


好了,为了介绍为什么Kotlin要有委托属性这个机制,假如我在一个类中,需要定义一个属性,这时获取属性的值如果使用get方法来获取,会在多个类都要写一遍,十分不符合代码设计,所以委托属性至关重要。


委托属性的实现


在前面说委托属性的概念时就说了,这个属性的值需要由一个新类来代理处理,这就是委托属性,那我们也可以大概猜出委托属性的底层逻辑,大致如下面代码:


class People{
val name: String? = null
var age: Int = 0
val isAbove18: Boolean = false
//email属性进行委托,把它委托给ProduceEmail类
var email: String by ProduceEmail()
}

假如People的email属性需要委托,上面代码编译器会编译成如下:


class People{
val name: String? = null
var age: Int = 0
val isAbove18: Boolean = false
//委托类的实例
private val productEmail = ProduceEmail()
//委托属性
var email: String
//访问器从委托类实例获取值
get() = productEmail.getValue()
//设置值把值设置进委托类实例
set(value) = productEmail.setValue(value)
}

当然上面代码是编译不过的,只是说一下委托的实现大致原理。那假如想使ProduceEmail类真的具有这个功能,需要如何实现呢。


by约定

其实我们经常使用 by 关键字它是一种约定,是对啥的约定呢 是对委托类的方法的约定,关于啥是约定,一句话说明白就是简化函数调用,具体可以查看我之前的文章:


# Kotlin invoke约定,让Kotlin代码更简洁


那这里的by约定简化了啥函数调用呢 其实也就是属性的get方法和set方法,当然委托类需要定义相应的函数,也就是下面这2个函数:


//by约定能正常使用的方法
class ProduceEmail(){

private val emails = arrayListOf("111@qq.com")

//对应于被委托属性的get函数
operator fun getValue(people: People, property: KProperty<*>): String {
Log.i("zyh", "getValue: 操作的属性名是 ${property.name}")
return emails.last()
}

//对于被委托属性的get函数
operator fun setValue(people: People, property: KProperty<*>, s: String) {
emails.add(s)
}

}

定义完上面委托类,便可以进行委托属性了:


class People{
val name: String? = null
var age: Int = 0
val isAbove18: Boolean = false
//委托属性
var email: String by ProduceEmail()
}

然后看一下调用地方:


val people = People()
Log.i("zyh", "onCreate: ${people.email}")
people.email = "222@qq.com"
Log.i("zyh", "onCreate: ${people.email}")

打印如下:


image.png


会发现每次调用email属性的访问器方法时,都会调用委托类的方法。


关于委托类中的方法,当你使用by关键字时,IDE会自动提醒,提醒如下:


image.png


比如getValue方法中的参数,第一个就是接收者了,你这个要委托的属性是哪个类的,第二个就是属性了,关于KProperty不熟悉的同学可以查看文章:


# Kotlin反射全解析3 -- 大展身手的KProperty


它就代表这属性,可以调用其中的一些方法来获取属性的信息。


而且方法必须使用operator关键字修饰,这是重载操作符的必须步骤,想使用约定,就必须这样干。


by lazy的实现


由前面明白了by的原理,我们接着来看一下我们经常使用的by lazy是个啥,直接看代码:


//这里使用by lazy惰性初始化一个实例
val instance: DataStoreManager by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) {
DataStoreManager(store) }

比如上面代码,使用惰性初始化初始了一个实例,我们来看一下这个by的实现:


//by代码
@kotlin.internal.InlineOnly
public inline operator fun <T> Lazy<T>.getValue(thisRef: Any?, property: KProperty<*>): T = value

哦,会发现它是Lazy类的一个扩展函数,按照前面我们对by的理解,它就是把被委托的属性的get函数和getValue进行配对,所以可以想象在Lazy< T >类中,这个value便是返回的值,我们来看一下:


//惰性初始化类
public interface Lazy<out T> {

//懒加载的值,一旦被赋值,将不会被改变
public val value: T

//表示是否已经初始化
public fun isInitialized(): Boolean
}

到这里我们注意一下 by lazy的lazy,这个就是一个高阶函数,来创建Lazy实例的,lazy源码:


//lazy源码
public actual fun <T> lazy(mode: LazyThreadSafetyMode, initializer: () -> T): Lazy<T> =
when (mode) {
LazyThreadSafetyMode.SYNCHRONIZED -> SynchronizedLazyImpl(initializer)
LazyThreadSafetyMode.PUBLICATION -> SafePublicationLazyImpl(initializer)
LazyThreadSafetyMode.NONE -> UnsafeLazyImpl(initializer)
}

这里会发现第一个参数便是线程同步的模式,第二个参数是初始化器,我们就直接看一下最常见的SYNCHRONIZED的模式代码:


//线程安全模式下的单例
private class SynchronizedLazyImpl<out T>(initializer: () -> T, lock: Any? = null) : Lazy<T>, Serializable {
private var initializer: (() -> T)? = initializer
//用来保存值,当已经被初始化时则不是默认值
@Volatile private var _value: Any? = UNINITIALIZED_VALUE
//锁
private val lock = lock ?: this

override val value: T
//见分析1
get() {
//第一次判空,当实例存在则直接返回
val _v1 = _value
if (_v1 !== UNINITIALIZED_VALUE) {
@Suppress("UNCHECKED_CAST")
return _v1 as T
}
//使用锁进行同步
return synchronized(lock) {
//第二次判空
val _v2 = _value
if (_v2 !== UNINITIALIZED_VALUE) {
@Suppress("UNCHECKED_CAST") (_v2 as T)
} else {
//真正初始化
val typedValue = initializer!!()
_value = typedValue
initializer = null
typedValue
}
}
}

//是否已经完成
override fun isInitialized(): Boolean = _value !== UNINITIALIZED_VALUE

override fun toString(): String = if (isInitialized()) value.toString() else "Lazy value not initialized yet."

private fun writeReplace(): Any = InitializedLazyImpl(value)
}

分析1:这个单例实现是不是有点眼熟,没错它就是双重校验锁实现的单例,假如你对双重校验锁的实现单例方式还不是很明白可以查看文章:


# Java双重校验锁单例原理 赶快看进来


这里实现懒加载单例的模式就是双重校验锁,2次判空以及volatile关键字都是有作用的,这里不再赘述。


总结


先搞明白by的原理,再理解by lazy就非常好理解了,虽然这些关键字我们经常使用,不过看一下其源码实现还是很舒爽的,尤其是Kotlin的高阶函数的一些SDK写法还是很值的学习。


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

Flutter 外接纹理

背景 在Flutter开发中需要用到视频播放的功能,Flutter对视频播放的支持并不是很友好所以Google提供了TextureLayer让Flutter端能够使用原生端的渲染,这样我们原生端很多优秀的视频播放组件能够在Flutter程序上使用了 Textu...
继续阅读 »

背景


在Flutter开发中需要用到视频播放的功能,Flutter对视频播放的支持并不是很友好所以Google提供了TextureLayer让Flutter端能够使用原生端的渲染,这样我们原生端很多优秀的视频播放组件能够在Flutter程序上使用了


Texture的创建


Texture是Platform端创建的,创建是会生成一个textureId,textureId可以映射获取到Texture



  • Androaid端FlutterRenderer创建SurfaceTexture


  @Override
public SurfaceTextureEntry createSurfaceTexture() {
  Log.v(TAG, "Creating a SurfaceTexture.");
  //创建SurfaceTexture
  final SurfaceTexture surfaceTexture = new SurfaceTexture(0);
  surfaceTexture.detachFromGLContext();
  final SurfaceTextureRegistryEntry entry =
      new SurfaceTextureRegistryEntry(nextTextureId.getAndIncrement(), surfaceTexture);
  Log.v(TAG, "New SurfaceTexture ID: " + entry.id());
  // 映射 textureId/entry.id() 和 SurfaceTexture的关系
  registerTexture(entry.id(), entry.textureWrapper());
  return entry;
}


  • Flutter Engine中 platform_view_android_jni_impl.cc


static void RegisterTexture(JNIEnv* env,
                          jobject jcaller,
                          jlong shell_holder,
                          jlong texture_id,
                          jobject surface_texture) {
ANDROID_SHELL_HOLDER->GetPlatformView()->RegisterExternalTexture(
    static_cast<int64_t>(texture_id),                       //
    fml::jni::JavaObjectWeakGlobalRef(env, surface_texture) //
);
}


  • android_shell_holder.cc


fml::WeakPtr<PlatformViewAndroid> AndroidShellHolder::GetPlatformView() {
FML_DCHECK(platform_view_);
return platform_view_;
}


  • platform_view_android.cc


void PlatformViewAndroid::RegisterExternalTexture(
  int64_t texture_id,
  const fml::jni::JavaObjectWeakGlobalRef& surface_texture) {
  //AndroidExternalTextureGL 即 Texture
RegisterTexture(std::make_shared<AndroidExternalTextureGL>(
    texture_id, surface_texture, std::move(jni_facade_)));
}


  • platform_view.cc


void PlatformView::RegisterTexture(std::shared_ptr<flutter::Texture> texture) {
delegate_.OnPlatformViewRegisterTexture(std::move(texture));
}


  • shell.cc


// |PlatformView::Delegate|
void Shell::OnPlatformViewRegisterTexture(
  std::shared_ptr<flutter::Texture> texture) {
FML_DCHECK(is_setup_);
FML_DCHECK(task_runners_.GetPlatformTaskRunner()->RunsTasksOnCurrentThread());

task_runners_.GetRasterTaskRunner()->PostTask(
    [rasterizer = rasterizer_->GetWeakPtr(), texture] {
      if (rasterizer) {
        if (auto* registry = rasterizer->GetTextureRegistry()) {
          registry->RegisterTexture(texture);
        }
      }
    });
}


  • texture.cc


void TextureRegistry::RegisterTexture(std::shared_ptr<Texture> texture) {
if (!texture) {
  return;
}
mapping_[texture->Id()] = texture;
}

通过上述流程Flutter Engine层最终会把SurfaceTexture存到mapping_中


Texture的获取


Texture的获取是在Flutter端,通过textureId获取到mapping_中保存的Texture并且创建出一个TextureLayer映射到Flutter framework层



  • Flutter端TextureLayer


  @override
void addToScene(ui.SceneBuilder builder, [ Offset layerOffset = Offset.zero ]) {
  final Rect shiftedRect = layerOffset == Offset.zero ? rect : rect.shift(layerOffset);
  builder.addTexture(
    textureId,
    offset: shiftedRect.topLeft,
    width: shiftedRect.width,
    height: shiftedRect.height,
    freeze: freeze,
    filterQuality: filterQuality,
  );
}


  • SceneBuilder


  void _addTexture(double dx, double dy, double width, double height, int textureId, bool freeze,
    int filterQuality) native 'SceneBuilder_addTexture'; // 调用Engine中的方法


  • scene_builder.cc


void SceneBuilder::addTexture(double dx,
                            double dy,
                            double width,
                            double height,
                            int64_t textureId,
                            bool freeze,
                            int filterQualityIndex) {
auto sampling = ImageFilter::SamplingFromIndex(filterQualityIndex);
auto layer = std::make_unique<flutter::TextureLayer>(
    SkPoint::Make(dx, dy), SkSize::Make(width, height), textureId, freeze,
    sampling);
AddLayer(std::move(layer));
}


  • texture_layer.cc


//GPU线程绘制时会调用该方法
void TextureLayer::Paint(PaintContext& context) const {
TRACE_EVENT0("flutter", "TextureLayer::Paint");
FML_DCHECK(needs_painting(context));

//获取texture_registry中注册好的texture
std::shared_ptr<Texture> texture =
    context.texture_registry.GetTexture(texture_id_);
if (!texture) {
  TRACE_EVENT_INSTANT0("flutter", "null texture");
  return;
}
texture->Paint(*context.leaf_nodes_canvas, paint_bounds(), freeze_,
                context.gr_context, sampling_);
}

image-20220126174016419


Texture的使用


Texture是封装的TextureLayer,通过上述流程分析后再来使用TextureLayer就比较简单了,可以通过MethodChannel的方式让Platform端创建一个Texture,最终返回一个textureId到Flutter端,Flutter端通过textureId的映射获取到Flutter Engine层创建好的Texture并包装成一个TextureLayer返回到Flutter framework层。



  1. 创建MethodChannel

  2. 创建SurfaceTexture

  3. 获取textureId创建Texture


以下是以获取摄像头预览为例:



  • Android 端示例代码


import android.Manifest;
import android.content.Context;
import android.content.pm.PackageManager;
import android.graphics.SurfaceTexture;
import android.hardware.camera2.CameraAccessException;
import android.hardware.camera2.CameraCaptureSession;
import android.hardware.camera2.CameraDevice;
import android.hardware.camera2.CameraManager;
import android.hardware.camera2.CaptureRequest;
import android.os.Handler;
import android.util.Log;
import android.view.Surface;


import androidx.annotation.NonNull;
import androidx.core.app.ActivityCompat;

import java.util.Arrays;

import io.flutter.embedding.android.FlutterActivity;
import io.flutter.embedding.engine.FlutterEngine;
import io.flutter.plugin.common.MethodChannel;
import io.flutter.view.TextureRegistry;

public class MainActivity extends FlutterActivity {
  public static final String TAG = "MainActivity";
  MethodChannel channel;
  private Handler backgroundHandler;

  @Override
  public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) {
      super.configureFlutterEngine(flutterEngine);
      backgroundHandler = new Handler();
      channel = new MethodChannel(flutterEngine.getDartExecutor()
              .getBinaryMessenger(), "flutter/texture/channel");

      channel.setMethodCallHandler((call, result) -> {
          switch (call.method) {
              case "createTexture":
                  createTexture(flutterEngine,result);
                  break;
          }
      });
  }

  private void createTexture(FlutterEngine flutterEngine,MethodChannel.Result result) {
      CameraManager cameraManager = (CameraManager) getSystemService(Context.CAMERA_SERVICE);
      if (ActivityCompat.checkSelfPermission(this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
          result.success(-1);
          ActivityCompat.requestPermissions(this,new String[]{Manifest.permission.CAMERA},1);
          return;
      }
      TextureRegistry.SurfaceTextureEntry entry =
              flutterEngine.getRenderer().createSurfaceTexture();
      SurfaceTexture surfaceTexture = entry.surfaceTexture();
      Surface surface = new Surface(surfaceTexture);
      try {
          cameraManager.openCamera(
                  "0",
                  new CameraDevice.StateCallback() {
                      @Override
                      public void onOpened(@NonNull CameraDevice device) {
                          result.success(entry.id());
                          CaptureRequest.Builder previewRequestBuilder = null;
                          try {
                              previewRequestBuilder = device.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
                              previewRequestBuilder.addTarget(surface);
                              startPreview(result,device,surface,previewRequestBuilder,backgroundHandler);
                          } catch (CameraAccessException e) {
                              e.printStackTrace();
                          }

                      }

                      @Override
                      public void onDisconnected(@NonNull CameraDevice camera) {
                           
                      }

                      @Override
                      public void onError(@NonNull CameraDevice cameraDevice, int errorCode) {
                          Log.i(TAG, "open | onError");
                          result.success(-1);
                      }
                  },
                  backgroundHandler);
      } catch (CameraAccessException e) {
          e.printStackTrace();
      }


  }

  private void startPreview(MethodChannel.Result result,CameraDevice device, Surface surface, CaptureRequest.Builder previewRequestBuilder, Handler backgroundHandler) {
      try {
          device.createCaptureSession(Arrays.asList(surface), new CameraCaptureSession.StateCallback() {
              @Override
              public void onConfigured(@NonNull CameraCaptureSession session) {
                  Log.i(TAG, "startPreview");
                  try {
                      session.setRepeatingRequest(previewRequestBuilder.build(),null,backgroundHandler );
                  } catch (CameraAccessException e) {
                      e.printStackTrace();
                  }
              }

              @Override
              public void onConfigureFailed(@NonNull CameraCaptureSession session) {
                  Log.i(TAG, "startPreview Failed");
              }
          },backgroundHandler);
      } catch (CameraAccessException e) {
          e.printStackTrace();
      }
  }
}


  • Flutter端示例代码


import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

MethodChannel methodChannel = MethodChannel('flutter/texture/channel');

void main() {
runApp(MyApp());
}

class MyApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
  return MaterialApp(
    title: 'Flutter Demo',
    theme: ThemeData(
      primarySwatch: Colors.blue,
    ),
    home: MyHomePage(title: 'Flutter Demo Home Page'),
  );
}
}

class MyHomePage extends StatefulWidget {
MyHomePage({Key? key, required this.title}) : super(key: key);
 
final String title;
@override
_MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
int textureId = -1;

Future<void> _createTexture() async {
  print('textureId = $textureId');
  if (textureId < 0) {
    methodChannel.invokeMethod('createTexture').then((value) {
      textureId = value;
      setState(() {
        print('textureId ==== $textureId');
      });
    });
  }

}

@override
Widget build(BuildContext context) {

  return Scaffold(
    appBar: AppBar(
      title: Text(widget.title),
    ),
    body: Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: <Widget>[
          if (textureId > -1)
            Container(
              width: 300,
              height: 400,
              child: Texture(
                textureId: textureId,
              ),
            )
        ],
      ),
    ),
    floatingActionButton: FloatingActionButton(
      onPressed: _createTexture,
      tooltip: 'createTexture',
      child: Icon(Icons.add),
    ),
  );
}
}

上述代码在Android并未创建View,而是创建了SurfaceTexture与camera绑定后通过Texture的形式在Flutter端显示


Texture和PlatformView的区别


PlatformView是Flutter中嵌套Platform中的View,如:TextView。它们的区别在Texture是渲染层的东西,而PlatformView本质是一个View它拥有View所有的属性。


总结


本文通过源码分析以及简单的摄像头预览示例讲解了Flutter外接纹理的原理和使用方式,希望能够帮助部分刚接触Flutter开发的同学加深对Flutter外接纹理的认识。


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

【MPFlutter浅尝】使用flutter写一个微信小程序

前言:12份左右flutter发布2.8.0 flutter对于桌面端和web端的支持越来越完善 想着这玩意能不能写微信小程序呢 一搜还真有 由一兜糖团队开发的MPFlutter项目开源了 本着尝鲜的心态看了下已上线的一兜糖小程序 哎呀妈呀 感觉还不错的样子 ...
继续阅读 »

前言:12份左右flutter发布2.8.0 flutter对于桌面端和web端的支持越来越完善 想着这玩意能不能写微信小程序呢 一搜还真有 由一兜糖团队开发的MPFlutter项目开源了 本着尝鲜的心态看了下已上线的一兜糖小程序 哎呀妈呀 感觉还不错的样子 话不多嗦 vscode启动


MPFlutter项目地址


MPFlutter Gayhub地址


所以 我应该写个啥玩意项目好呢

开始我想的是仿站 或者把公司项目直接用MPFlutter写一遍小程序 这样我接口什么的都有现成的
但是我是一个Apex游戏玩家 我要干票大的 我看刑


第一步借鉴ui
打开WeGame 打开小黑盒
简单查看后先借鉴下小黑盒 ps:不得不说小黑盒数据非常全 基本所有Steam游戏都囊括了
开始设计ui
简单的画两笔 差不多就这样


7f6f64b563b2a3eaf18908190bf22a8.jpg


第二步 收集数据
apex英雄这个游戏是一个大逃杀类型游戏 数十个英雄 二三十把枪械 数以千记的皮肤 边框 动作 数据太多自己搜集太麻烦这部分 但是小黑盒的数据非常全 嘿嘿嘿
简简单单写个后台填上数据


第三步 开始绘制前端
因为我flutter写的还行但是我从未写过微信小程序。但是好在MPFLutter的文档写的很详细。属于我奶奶来了都能把环境搭好的那种


mpflutter 应该是类似mpvue的类型。是一个跨平台 Flutter 开发框架,可用于各种小程序、H5、原生应用开发。开发者可以通过 Dart 语言开发 App,一套代码同时在多个平台运行 微信 京东 钉钉等等


Apex项目启动

环境搭建


windows配置环境


macOS中配置环境


这里部分和flutter配置环境是一样的
。但是我们需要


克隆模板工程

git clone https://github.com/mpflutter/mpflutter_template.git apex_wechat


安装依赖

./mpflutter packages get



注意,这里用的是 ./mpflutter 而不是 flutter



这里模板工程克隆下来之后按F5就可以成功运行 你可以看到演示的demo 。这里继续看文档 要把模板工程变成这里的项目。我们需要


初始化应用信息

dart scripts/help.dart


这将出现以下信息,help.dart 是 MPFlutter 的帮助中心,可帮助你完成应用的初始化和构建工作。


image.png


我们选择初始化 MPFlutter 模板工程,并根据提示输入工程名称、输出目标。




  • 是否移除模板工程自带的 Git 源? (y/N)



    • 对于新克隆的模板工程,选 y 即可,移除自带的 Git 源,后续可以添加自己的 Git 源。




  • 请输入工程名称,合法字符为全小写英文和下划线:



    • 输入一个合法的工程名称,如 awesome_project,这将会同步修改 pubspec.yaml 中的 name 值。




  • 该工程需要输出到 Web 吗?(如果选择否,将删除 Web 目录。) (y/N)



    • 如果你不需要输出到 Web (HTML5) 可以选择否,一般情况下,我们会选 y 保留该目标。




  • 该工程需要输出到微信小程序吗?(如果选择否,将删除 weapp 目录。) (y/N)



    • 如果你不需要输出到微信小程序可以选择否。




......


在命令行执行 ./mpflutter packages get


这里你的项目就成功跑起来了


然后就开始我们的apex启动


目前MPFlutter支持的第三库有GetX Provider 富文本 Bloc等等 我这里简单使用了Provider 和GetX
用法和写FLutter项目没有差别 正常引入后./mpflutter packages get就行


提一嘴就是Flutter很多组件都是经过MPFlutter二次封装过的 material组件包含的是不可用的 MPFlutter提供了大部分替换类的组件,但是还是有一小部分需要自己实现。就意味这部分ui需要自己写 pub.dev上的第三方ui组件差不多也是不能用的 算了 写就写吧 又不是不能看 丑就丑点吧


花了大概两个星期总算是把前后数据都通了完成了百科 商店 头条页面 上线的话目前还没有想法 过完年不懒了再说 现在我只想打Apex 效果图如下


image.png


image.png


image.png


具体代码地址 小孩子不懂事写着玩的 轻点喷


看到这里可能有小伙伴要问了 我应该如何调用原生小程序的APi 登录授权 查看大图等等。不用担心 MPFlutter早已经把这部分的问题解决了 提供了所有的原生小程序的APi调用 具体说明


总的来说经过使用MPFlutter写了一个微信小程序感觉就是有点回归写html的时候的样子 自己想要的ui效果得自己写 但是像一些基础组件还是可以用的 包括我在使用过程中遇到各种问题也一直在请教MPFLutter作者 @PonyCui。大佬也一直很有耐心的解答我的各种奇葩问题和解决我的奇葩需求。
再次感谢 @PonyCui的开源 希望flutter社区越来越好 一统我的技术栈


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

Flutter线上监控说明

概要移动端Apm系统作用:1、我们可以快速定位到线上App的实际使用情况,了解到App的奔溃、异常数据,从而针对潜在的风险问题进行预警,并进行相应的处理。2、了解App的真实使用信息,提高用户使用黏性。一、移动端常用apm指标1、崩溃率崩溃分析,是将 Andr...
继续阅读 »

概要

移动端Apm系统作用:

1、我们可以快速定位到线上App的实际使用情况,了解到App的奔溃、异常数据,从而针对潜在的风险问题进行预警,并进行相应的处理。

2、了解App的真实使用信息,提高用户使用黏性。

一、移动端常用apm指标

1、崩溃率

崩溃分析,是将 Android 和 iOS 平台常见的 APP 崩溃问题进行归类分析,帮助企业根据崩溃指标快速发现、定位问题。


2、UI卡顿

拿Android来说:大多数用户感知到的卡顿等性能问题的最主要根源都是因为渲染性能。Android系统每隔大概16.6ms发出VSYNC信号,触发对UI进行渲染,如果每次渲染都成功,这样就能够达到流畅的画面所需要的60fps,为了能够实现60fps,这意味着程序的大多数操作都必须在16ms内完成。

 

3、线上日志

可以快速定位某个用户的日志数据,及时根据用户反馈的情况进行快速排查。

4、网络监控

由于网络环境错综复杂,对于网络接口性能方面需要关注 接口响应时间,网络错误,http状态码,网络劫持等

 

三、Flutter apm现状

闲鱼自研(未开方源码)

再无其他第三方

  

收起阅读 »

Flow 操作符 shareIn 和 stateIn 使用须知

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

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

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

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

底层数据流生产者

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

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

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

shareIn 还是 stateIn?

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

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

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

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

提升性能

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

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

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

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

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

缓冲事件

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

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

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

缓存数据

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

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

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

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

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

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

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

需要入参的数据流

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

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

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

  • 您是否允许同时从多个用户接收事件?如果答案是肯定的,您可能需要为 SharedFlow 或 StateFlow 实例创建一个 map,并在 subscriptionCount 为 0 时移除引用并退出上游数据流。
  • 如果您只允许一个用户,并且收集者需要更新为观察新的用户,您可以向一个所有收集者共用的 SharedFlow 或 StateFlow 发送事件更新,并将公共数据流作为类中的变量。

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


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

同一个app不同activity显示多任务(仿微信小程序切换效果)

如题,这种效果类似微信小程序显示的效果,就是打开微信跳一跳后,切换安卓多任务窗口(就是清理内存窗口),会看到如下页面 微信小程序会在其中显示两个单独的页面,点击跳一跳会进入跳一跳小程序,点击后面的微信,即会进入微信聊天主页面。在安卓中如何实现呢?这里...
继续阅读 »

如题,这种效果类似微信小程序显示的效果,就是打开微信跳一跳后,切换安卓多任务窗口(就是清理内存窗口),会看到如下页面 多任务图1.jpg

微信小程序会在其中显示两个单独的页面,点击跳一跳会进入跳一跳小程序,点击后面的微信,即会进入微信聊天主页面。

在安卓中如何实现呢?

这里有两种方法实现:

第一种:代码动态实现

Intent intent = new Intent(this, SecondActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT);
intent.addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK);
startActivity(intent);

添加上面的两个Flag即可,有些文章说关闭的时候要使用

finishAndRemoveTask();

方法,我这边没使用该方法也没发现问题,如果存在潜在问题,知道的人麻烦告知下,谢谢!!!

第二种:在AndroidManifest.xml中配置属性

参考链接:在近期任务列表显示单个APP的多个Activity

第二种方法由于需要写死配置,可能对于我来说作用不大,所以也没有测试,需要了解的人可以查看上面地址。

注意:这里来说下处理第一种方法的问题

使用上面的方法确实是实现了微信小程序多任务窗口的效果,但你会发现两个窗口在文章开头的图中的地方显示的是相同的名字,即你APP的名字,这里就跟小程序有区别了,下面来说下如何实现这种效果:

首先:经过测试,在manifest.xml中给要显示的activity设置android:lable,这种方法是可行的,但会相当于是固定了,不可变了。

然后:在manifest.xml中给该activity设置android:icon也是可以的,这样就实现了显示"跳一跳"文字和logo了。

最后:当然还是同样需要在代码中动态设置,不然固定死对于程序员来说有瑕疵。

在需要显示的activity中调用下面的代码即可显示不同文字

setTaskDescription(new ActivityManager.TaskDescription("跳一跳"));

聪明的程序员都会看下该方法的源码以及需要参数的构造方法,所以同时显示图片和文字以及需要适配就需要用下面的代码了

if( Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP){
setTaskDescription(new ActivityManager.TaskDescription("跳一跳", mBitmap));
}

没错,需要5.0以上才能实现,参数的构造就需要传入bitmap才能显示图片了。

最终效果图:

最终效果图.png

存在的问题:当添加flag打开activity之后,如果切换了任务窗口,这时返回是不能返回到之前调用startActivity的方法的页面了,如果没有切换就不会存在这个问题,微信也是一样,像微信大佬都没有解决(也可能没这个需求),反正我是没有办法滴。


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

Android Activity Result API

最近准备开始新的项目,在编写base类复写onActivityResult方法时,发现已经提示deprecation了。于是去官网查找了一下,发现现在官方推荐做法是使用 Activity Result API。本篇文章用来记录一下 Activity Resul...
继续阅读 »

最近准备开始新的项目,在编写base类复写onActivityResult方法时,发现已经提示deprecation了。于是去官网查找了一下,发现现在官方推荐做法是使用 Activity Result API。

本篇文章用来记录一下 Activity Result API 如何使用。

以往的实现方式

以往,A Activity获取B Activity的返回值的实现方法是,A通过startActivityForResult()来启动B,然后B在finish()之前通过setResult()方法来设置结果值,A就可以在onActivityResult()方法中获取到B在setResult()方法中设置的参数。简单示例如下:

class A : Activity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
startActivityForResult(Intent(this,B::class.java))
}

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
//从B返回后这里就可以获取到resultCode为Activity.RESULT_OK
}
}

class B: Activity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setResult(Activity.RESULT_OK)
finish()
}
}

Activity Result API

现在,在Activity和Fragment中,Activity Result API提供了registerForActivityResult()方法,该方法用于注册获取结果的回调。

registerForActivityResult可以传入ActivityResultContract、ActivityResultRegistry、ActivityResultCallback等3个参数,并返回ActivityResultLauncher。

  • ActivityResultContract:ActivityResult合约,约定输入的参数和输出的参数。包含默认合约和自定义合约。
  • ActivityResultRegistry:存储已注册的ActivityResultCallback的记录表,可以在非Activity和Fragment的类中借用此类获取ActivityResult。
  • ActivityResultCallback:ActivityResult回调,用于获取返回结果。
  • ActivityResultLauncher:启动器,根据合约规定的输入参数来启动页面。

Activity Result API的简单使用示例:

class A : Activity() {

//默认合约
var forActivityResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { activityResult ->
launcher?.text="lanuncher callback value : resultCode:$resultCode data$data"
}

//自定义合约(输入输出类型均为String)
var forActivityResultLauncher1 = registerForActivityResult(object : ActivityResultContract<String, String>() {
override fun createIntent(context: Context, input: String?): Intent {
return Intent(this@AActivity, B::class.java).apply {
putExtra("inputParams", input)
}
}

override fun parseResult(resultCode: Int, intent: Intent?): String {
return if (resultCode == Activity.RESULT_OK) {
intent?.getStringExtra("result") ?: "empty result"
} else {
""
}
}
}) { resultString ->
launcher1?.text = "lanuncher1 callback value : reslutString:$reslutString"
}

var launcher: TextView? = null
var launcher1: TextView? = null

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.layout_a_activity)
launcher = findViewById(R.id.tv_launcher_callback)
launcher1 = findViewById(R.id.tv_launcher_callback1)
val btnLauncher = findViewById<Button>(R.id.launcher)
val btnLauncher1 = findViewById<Button>(R.id.launcher1)
btnLauncher.setOnClickListener {
//默认合约
forActivityResultLauncher.launch(Intent(this@AActivity, B::class.java))
}

btnLauncher1.setOnClickListener {
//自定义合约
forActivityResultLauncher1.launch("inputParams from A")
}
}
}

class B: Activity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.layout_b_activity)
val btnBack = findViewById<Button>(R.id.back)
val inputParams = intent.getStringExtra("inputParams")
btnBack.setOnClickListener {
if (TextUtils.isEmpty(inputParams)) {
setResult(Activity.RESULT_OK, Intent())
} else {
setResult(Activity.RESULT_OK, Intent().apply {
putExtra("result", "result from A")
})
}
finish()
}
}
}

示例效果图: 1642499246411691.gif

在非Activity和Fragment的类中接收ActivityResult

示例代码如下:

class A : Activity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val myActivityResultObserver = MyActivityResultObserver(activityResultRegistry)
lifecycle.addObserver(myActivityResultObserver)
}
}

class MyActivityResultObserver(private val registry: ActivityResultRegistry) : DefaultLifecycleObserver {

override fun onCreate(owner: LifecycleOwner) {
super.onCreate(owner)
registry.register("MyActivityResultReceiver", owner, ActivityResultContracts.StartActivityForResult()) {

}
}
}

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

Android LiveData原理分析

前言官方介绍:LiveData 是一种可观察的数据存储器类。与常规的可观察类不同,LiveData 具有生命周期感知能力,意指它遵循其他应用组件(如 Activity、Fragment 或 Service)的生命周期。这种感知能力可确保 LiveData 仅更...
继续阅读 »

前言

官方介绍:LiveData 是一种可观察的数据存储器类。与常规的可观察类不同,LiveData 具有生命周期感知能力,意指它遵循其他应用组件(如 Activity、Fragment 或 Service)的生命周期。这种感知能力可确保 LiveData 仅更新处于活跃生命周期状态的应用组件观察者。 它有以下的优势:

  • 确保界面符合数据状态
  • 不会发生内存泄露
  • 不会因Activity停止而导致崩溃
  • 不再需要手动处理生命周期
  • 数据始终保持最新状态
  • 适当的配置修改
  • 共享资源

接下来我们通过基本使用,一步一步的探究LiveData是如何实现这些优势的。

使用

创建 LiveData 对象

public class CoursePreviewModel extends ViewModel {

/**
* view状态
*/
private MutableLiveData<List<CoursePreviewBean.DataBean>> mStateLiveData;

public MutableLiveData<List<CoursePreviewBean.DataBean>> viewStateLive() {
if (mStateLiveData == null) {
mStateLiveData = new MutableLiveData<>();
}
return mStateLiveData;
}
}

观察 LiveData 对象

class CoursePreviewActivity : AppCompatActivity() {

// Use the 'by viewModels()' Kotlin property delegate
// from the activity-ktx artifact
private val mViewModel: CoursePreviewModel by viewModels()

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 监听
mViewModel.viewStateLive().observe(this, target -> {
});
}
}

原理分析

这里我们先提出几个问题:

  1. LiveData怎么绑定到应用组件的生命周期呢
  2. 为什么不需要我们手动处理生命周期,为什么不会因Activity停止而导致崩溃
  3. 数据变化又是怎么触发的呢

带着这些问题,我们逐步往里看

一、应用组件生命周期的绑定

当我们需要观察数据变化时,需要调用LiveData的observe接口,这也是LiveData与Activity或Fragment产生关联的地方:

    @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);
}

这个方法需要接收两个参数:

  • LifecycleOwner:生命周期所有者
  • Observer:观察者,用于观察获取变化后的数据

通常我们在Activity或Fragment中使用LiveData,看下androidx包提供的AppCompatActivity和Fragment都是实现了LifecycleOwner接口。所以直接将this作为第一个参数即可。

继续分析上边的observe方法

  • 这里会首先判断是否在主线程执行,假如不是即会抛出异常
assertMainThread("observe");
static void assertMainThread(String methodName) {
if (!ArchTaskExecutor.getInstance().isMainThread()) {
throw new IllegalStateException("Cannot invoke " + methodName + " on a background"
+ " thread");
}
}
  • 假如是应用组件的生命周期已经是destory的状态,即不会继续往下执行
if (owner.getLifecycle().getCurrentState() == DESTROYED) {
// ignore
return;
}
  • 通过LifecycleBoundObserver进行真正的逻辑处理,这里我们继续往下走,待会再回头分析这块
  • 判断相同的observer不能被不同的LifecycleOwner处理
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;
}

这里主要用到SafeIterableMap这个结构保存owner和observer的关系:

// put之前先判断是否已经包含了该key,假如是则直接返回相应的value
public V putIfAbsent(@NonNull K key, @NonNull V v) {
Entry<K, V> entry = get(key);
if (entry != null) {
return entry.mValue;
}
put(key, v);
return null;
}
  • 最后,将生命周期所有者与observer绑定起来,这样子observer即可接收到相应的生命周期
owner.getLifecycle().addObserver(wrapper);

可以看到,这里不是直接add传递进来的Observer,而是上边提到的包装了owner和Observer的LifecycleBoundObserver。所以接下来我们好好分析下它:

class LifecycleBoundObserver extends ObserverWrapper implements LifecycleEventObserver {
@NonNull
final LifecycleOwner mOwner;

LifecycleBoundObserver(@NonNull LifecycleOwner owner, Observer<? super T> observer) {
super(observer);
mOwner = owner;
}

@Override
boolean shouldBeActive() {
return mOwner.getLifecycle().getCurrentState().isAtLeast(STARTED);
}

@Override
public void onStateChanged(@NonNull LifecycleOwner source,
@NonNull Lifecycle.Event event) {
if (mOwner.getLifecycle().getCurrentState() == DESTROYED) {
removeObserver(mObserver);
return;
}
activeStateChanged(shouldBeActive());
}

@Override
boolean isAttachedTo(LifecycleOwner owner) {
return mOwner == owner;
}

@Override
void detachObserver() {
mOwner.getLifecycle().removeObserver(this);
}
}

可以看到,LifecycleBoundObserver是实现了LifecycleEventObserver接口,而LifecycleEventObserver接口是继承于LifecycleObserver接口的,因此可以看出主要是在LifecycleBoundObserver这里完成生命周期的处理。

二、为什么不需要手动处理生命周期

经过上边的分析,我们发现其实主要的生命周期处理工作是在LifecycleBoundObserver里边完成的。我们继续看它的源码,有这么两个方法:

    @Override
public void onStateChanged(@NonNull LifecycleOwner source,
@NonNull Lifecycle.Event event) {
if (mOwner.getLifecycle().getCurrentState() == DESTROYED) {
removeObserver(mObserver);
return;
}
activeStateChanged(shouldBeActive());
}

@Override
void detachObserver() {
mOwner.getLifecycle().removeObserver(this);
}

我们先看detachObserver,这里明显就是用于解除绑定的。我们找下哪里调用了这个方法:

@MainThread
public void removeObserver(@NonNull final Observer<? super T> observer) {
assertMainThread("removeObserver");
ObserverWrapper removed = mObservers.remove(observer);
if (removed == null) {
return;
}
removed.detachObserver();
removed.activeStateChanged(false);
}

主要是在LiveData的removeObserver,那继续找下该方法的调用。发现又回到了LifecycleBoundObserver本身,不错就是在onStateChanged里边,它会接收生命周期的变化通知,当发现mOwner.getLifecycle().getCurrentState() == DESTROYED即组件处于destory状态时,自动移除相应的观察者,这样子当activity或fragment销毁时,不会再收到相应的事件通知

三、数据变化怎么触发的

我们继续将核心放在LifecycleBoundObserver的onStateChanged方法上。当组件还没销毁,即会继续往下跑activeStateChanged(shouldBeActive());,该方法定义在ObserverWrapper类里边(LifecycleBoundObserver继承于它)

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(this);
}
}

当处于活跃状态,即mActive为true时,会走到dispatchingValue(this);。我们继续看

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方法

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;
observer.mObserver.onChanged((T) mData);
}

直接看到最后一句,这里直接调用了observer.mObserver.onChanged((T) mData);,这里就会触发数据变化回调。而这里的mObserver即是我们在刚开始传递进来的。

其他

一、observeForever

其实,LiveData除了提供observe用于方法,还提供了一个observeForever方法

@MainThread
public void observeForever(@NonNull Observer<? super T> observer) {
assertMainThread("observeForever");
AlwaysActiveObserver wrapper = new AlwaysActiveObserver(observer);
ObserverWrapper existing = mObservers.putIfAbsent(observer, wrapper);
if (existing instanceof LiveData.LifecycleBoundObserver) {
throw new IllegalArgumentException("Cannot add the same observer"
+ " with different lifecycles");
}
if (existing != null) {
return;
}
wrapper.activeStateChanged(true);
}

这里可以看到,没有跟生命周期绑定,也不再使用LifecycleBoundObserver进行包装,而是使用AlwaysActiveObserver:

private class AlwaysActiveObserver extends ObserverWrapper {

AlwaysActiveObserver(Observer<? super T> observer) {
super(observer);
}

@Override
boolean shouldBeActive() {
return true;
}
}

AlwaysActiveObserver和LifecycleBoundObserver都继承于ObserverWrapper,但是前者没有重写它的detachObserver方法,因此它不会被自动移除监听。只能通过手动调用removeObserver进行移除。

二、postValue和setValue

两个方法都可以用于更新值,分析下区别:

protected void postValue(T value) {
boolean postTask;
synchronized (mDataLock) {
postTask = mPendingData == NOT_SET;
mPendingData = value;
}
if (!postTask) {
return;
}
ArchTaskExecutor.getInstance().postToMainThread(mPostValueRunnable);
}

@MainThread
protected void setValue(T value) {
assertMainThread("setValue");
mVersion++;
mData = value;
dispatchingValue(null);
}
  1. setValue必须在主线程调用,否则会抛出异常
  2. postValue用于在其他线程更新值,核心在:ArchTaskExecutor.getInstance().postToMainThread(mPostValueRunnable);,这会切回到主线程执行。
  3. 由于postValue是通过handler的消息派发进行处理,而setValue直接设值,因此这种情况需要注意:
// 源码提示
Posts a task to a main thread to set the given value. So if you have a following code executed in the main thread:
liveData.postValue("a");
liveData.setValue("b");

The value "b" would be set at first and later the main thread would override it with the value "a".
  1. 如果在主线程执行已发布任务之前多次调用此方法,则只会调度最后一个值。这个是怎么实现的呢?我们看下postValue里边的处理
protected void postValue(T value) {
boolean postTask;
synchronized (mDataLock) {
postTask = mPendingData == NOT_SET;
mPendingData = value;
}
if (!postTask) {
return;
}
ArchTaskExecutor.getInstance().postToMainThread(mPostValueRunnable);
}

private final Runnable mPostValueRunnable = new Runnable() {
@SuppressWarnings("unchecked")
@Override
public void run() {
Object newValue;
synchronized (mDataLock) {
newValue = mPendingData;
mPendingData = NOT_SET;
}
setValue((T) newValue);
}
};

第一次,mPendingData的值为NOT_SET,因此postTask为true,而mPendingData为设置的value。直到mPostValueRunnable被执行时,mPendingData才被重新赋值为NOT_SET。假如在主线程执行前,不断的调用postValue,postTask一直为false,mPendingData会被更新到最新设置的值,但是mPostValueRunnable不会被重复执行。


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

Kotlin 中的contract 到底有什么用?

我们在开发中肯定会经常用Kotlin提供的一些通用拓展函数,当我们进去看源码的时候会发现许多函数里面有contract {}包裹的代码块,那么这些代码块到底有什么作用呢??测试接下来用以下两个我们常用的拓展函数作为例子public inline fun <...
继续阅读 »

我们在开发中肯定会经常用Kotlin提供的一些通用拓展函数,当我们进去看源码的时候会发现许多函数里面有contract {}包裹的代码块,那么这些代码块到底有什么作用呢??

测试

接下来用以下两个我们常用的拓展函数作为例子

public inline fun <T, R> T.run(block: T.() -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return block()
}

public inline fun CharSequence?.isNullOrEmpty(): Boolean {
contract {
returns(false) implies (this@isNullOrEmpty != null)
}

return this == null || this.length == 0
}

runisNullOrEmpty我相信大家在开发中是经常见到的。

不知道那些代码有什么作用,那么我们就把那几行代码去掉,然后看看函数使用起来有什么区别。

public inline fun <T, R> T.runWithoutContract(block: T.() -> R): R {
return block()
}

public inline fun CharSequence?.isNullOrEmptyWithoutContract(): Boolean {
return this == null || this.length == 0
}

上面是去掉了contract{}代码块后的两个函数 调用看看

fun test() {
var str1: String = ""
var str2: String = ""

runWithoutContract {
str1 = "jayce"
}
run {
str2 = "jayce"
}

println(str1) //jayce
println(str2) //jayce
}

经过测试发现,看起来好像没什么问题,run代码块都能都正常执行,做了赋值的操作。

那么如果是这样呢

将str的初始值去掉,在run代码块里面进行初始化操作

@Test
fun test() {
var str1: String
var str2: String

runWithoutContract {
str1 = "jayce"
}
run {
str2 = "jayce"
}

println(str1) //编译不通过 (Variable 'str1' must be initialized)
println(str2) //编译通过
}

??????

我们不是在runWithoutContract做了初始化赋值的操作了吗?怎么IDE还报错,难道是IDE出了什么问题?好 有问题就重启,我去,重启还没解决。。。。好重装。不不不!!别急 会不会Contract代码块就是干这个用的?是不是它悄悄的跟IDE说了什么话 以至于它能正常编译通过?

好 这个问题先放一放 我们再看看没contract版本的isNullOrEmpty对比有contract的有什么区别

fun test() {
val str: String? = "jayce"

if (!str.isNullOrEmpty()) {
println(str) //jayce
}
if (!str.isNullOrEmptyWithoutContract()) {
println(str) //jayce
}
}

发现好像还是没什么问题。相信大家根据上面遇到的问题可以猜测,这其中肯定也有坑。

比如这种情况

fun test() {
val str: String? = "jayce"

if (!str.isNullOrEmpty()) {
println(str.length) // 编译通过
}

if (!str.isNullOrEmptyWithoutContract()) {
println(str.length) // 编译不通过(Only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type String?)
}
}

根据错误提示可以看出,在isNullOrEmptyWithoutContract判断为flase之后的代码块,str这个字段还是被IDE认为是一个可空类型,必须要进行空检查才能通过。然而在isNullOrEmpty返回flase之后的代码块,IDE认为str其实已经是非空了,所以使用前就不需要进行空检查。

查看 contract 函数

public inline fun contract(builder: ContractBuilder.() -> Unit) { }

点进去源码,我们可以看到contract是一个内联函数,接收一个函数类型的参数,该函数是ContractBuilder的一个拓展函数(也就是说在这个函数体里面拥有ContractBuilder的上下文)

看看ContractBuilder给我们提供了哪些函数(主要就是依靠这些函数来约定我们自己写的lambda函数)

public interface ContractBuilder {
//描述函数正常返回,没有抛出任何异常的情况。
@ContractsDsl public fun returns(): Returns

//描述函数以value返回的情况,value可以取值为 true|false|null。
@ContractsDsl public fun returns(value: Any?): Returns

//描述函数以非null值返回的情况。
@ContractsDsl public fun returnsNotNull(): ReturnsNotNull

//描述lambda会在该函数调用的次数,次数用kind指定
@ContractsDsl public fun <R> callsInPlace(lambda: Function<R>, kind: InvocationKind = InvocationKind.UNKNOWN): CallsInPlace
}
returns

其中 returns() returns(value) returnsNotNull() 都会返回一个继承于SimpleEffectReturns 接下来看看SimpleEffect

public interface SimpleEffect : Effect {
//接收一个Boolean值的表达式 改函数用来表示当SimpleEffect成立之后 保证Boolean值的表达式返回值为true
//表达式可以传判空代码块(`== null`, `!= null`)判断实例语句 (`is`, `!is`)。
public infix fun implies(booleanExpression: Boolean): ConditionalEffect
}

可以看到SimpleEffect里面有一个中缀函数implies 。可以使用ContractBuilder的函数指定某种返回的情况 然后用implies来声明传入的表达式为true。

看到这里 那么我们应该就知道 isNullOrEmpty() 加的contract是什么意思了

public inline fun CharSequence?.isNullOrEmpty(): Boolean {
contract {
//返回值为false的情况 returns(false)
//意味着 implies
//调用该函数的对象不为空 (this@isNullOrEmpty != null)
returns(false) implies (this@isNullOrEmpty != null)
}

return this == null || this.length == 0
}

因为isNullOrEmpty里面加了contract代码块,告诉IDE说:返回值为false的情况意味着调用该函数的对象不为空。所以我们就可以直接在判断语句后直接使用非空的对象了。

有些同学可能还是不理解,这里再举一个没什么用的例子(运行肯定会crash哈。。。)

@ExperimentalContracts //因为该特性还在试验当中 所以需要加上这个注解
fun CharSequence?.isNotNull(): Boolean {
contract {
//返回值为true returns(true)
//意味着implies
//调用该函数的对象是StringBuilder (this@isNotNull is StringBuilder)
returns(true) implies (this@isNotNull is StringBuilder)
}

return this != null
}

fun test() {
val str: String? = "jayce"

if (str.isNotNull()) {
str.append("")//String可是没有这个函数的,因为我们用contract让他强制转换成StringBuilder了 所以才有了这个函数
}
}

是的 这样IDE居然没有报错,因为经过我们contract的声明,只要这个函数返回true,调用函数的对象就是一个StringBuilder。

callsInPlace
//描述lambda会在该函数调用的次数,次数用kind指定
@ContractsDsl public fun <R> callsInPlace(lambda: Function<R>, kind: InvocationKind = InvocationKind.UNKNOWN): CallsInPlace

可以知道callsInPlace是用来指定lambda函数调用次数的

kind有四种取值

  • InvocationKind.AT_MOST_ONCE:最多调用一次
  • InvocationKind.AT_LEAST_ONCE:最少调用一次
  • InvocationKind.EXACTLY_ONCE:调用一次
  • InvocationKind.UNKNOWN:未知,不指定的默认值

我们再看回去之前run函数里面的contract声明了什么

public inline fun <T, R> T.run(block: T.() -> R): R {
contract {
//block这个函数,刚好调用一次
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return block()
}

看到这里 应该就知道为什么我们自己写的runWithoutContract会报错(Variable 'str1' must be initialized),而系统的run却不会报错了,因为run声明了lambda会调用一次,所以就一定会对str2做初始化操作,然而runWithoutContract却没有声明,所以IDE就会报错(因为有可能不会调用,所以就不会做初始化操作了)。

总结

  1. Kotlin提供了一些自动转换的功能,例如平时判空和判断是否为某个实例的时候,Kotlin都会为我们自动转换。但是如果这个判断被提取到其他函数的时候,这个转换会失效。所以提供了contract给我们在函数体添加声明,编译器会遵守我们的约定。
  2. 当使用一个高阶函数的时候,可以使用callsInPlace指定该函数会被调用的次。例如在函数体里面做初始化,如果申明为EXACTLY_ONCE的时候,IDE就不会报错,因为编译器会遵守我们的约定。

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

跟我学企业级flutter项目:dio网络框架增加公共请求参数&header

前言跟我学flutter系列:跟我学flutter:我们来举个例子通俗易懂讲解dart 中的 mixin跟我学flutter:我们来举个例子通俗易懂讲解异步(一)ioslate跟我学flutter:我们来举个例子通俗易懂讲解异步(二)ioslate循环机制企业...
继续阅读 »

前言

跟我学flutter系列:
跟我学flutter:我们来举个例子通俗易懂讲解dart 中的 mixin
跟我学flutter:我们来举个例子通俗易懂讲解异步(一)ioslate
跟我学flutter:我们来举个例子通俗易懂讲解异步(二)ioslate循环机制
企业级篇目:
跟我学企业级flutter项目:用bloc手把手教你搭建用户认证系统
跟我学企业级flutter项目:dio网络框架增加公共请求参数&header
跟我学企业级flutter项目:如何用dio封装一套企业级可扩展高效的网络层
跟我学企业级flutter项目:如何封装一套易用,可扩展的Hybrid混合开发webview
跟我学企业级flutter项目:手把手教你制作一款低耦合空页面widget

在开发过程中,我们经常会用到网络请求,在flutter框架中,dio框架非常不错,所以今天的文章在dio的基础上搭建一套网络持久化框架,那么在flutter项目中如何搭建一套,高可用性,维护性较高的dio公共请求参数框架呢?

搭建前夕准备

一、基本认知

  1. 要持久化那么必然要有存储设备
  2. 持久化的数据在app启动后要即使填充到项目中
  3. 由于项目中网络请求地址繁多,类型不同,需要持久化的位置不同

二、基于基本认知来找合适的工具&必要点

2.1 持久化的工具:

我的推荐

  1. mmkv
  2. share_prefresence

今天主要用讲解mmkv版本

2.2必要点:

dio拦截器

拦截器是搭建这套持久化的关键

三、准备好如上技能,我们来搭建这套持久化网络框架吧

1、首先要知道有几种类型的公共

  //请求中url后追加公共请求
static const int urlPresistent = 1;
//请求头中追加公共请求
static const int headerPresistent = 2;
//全部都追加公共请求
static const int allPresistent = 3;

2、构建缓存参数(为了快速获取)

  static Map> headerPersistent = Map();
static Map> urlPersistent = Map();

3、构建mmkv存储结构(加密存储)

static MMKV _store(String baseUrl, int type) => MMKVStore.sysSafeMMKV(name: '${SysConfig.sysPersistent}${baseUrl}_${type.toString()}');

4、构建基本函数

单健值对存储

static void setPersistent(String baseUrl,String key,String? value,{int type = allPresistent}){
if (type == allPresistent || type == headerPresistent) {
if (!headerPersistent.containsKey(baseUrl)) {
headerPersistent[baseUrl] = Map();
}
var keyMap = headerPersistent[baseUrl]!;
keyMap[key] = value;
_store(baseUrl, headerPresistent).encodeString(key, value??"");
}
if (type == allPresistent || type == urlPresistent) {
if (!urlPersistent.containsKey(baseUrl)) {
urlPersistent[baseUrl] = Map();
}
var keyMap = urlPersistent[baseUrl]!;
keyMap[key] = value;
_store(baseUrl, urlPresistent).encodeString(key, value??"");
}
}

多健值对存储

static void setPersistentMap(String baseUrl,Map map,{int type = allPresistent}){
if (type == allPresistent || type == headerPresistent) {
if (!headerPersistent.containsKey(baseUrl)) {
headerPersistent[baseUrl] = Map();
}
var keyMap = headerPersistent[baseUrl]!;
keyMap.addAll(map);
keyMap.forEach((key, value) {
_store(baseUrl, headerPresistent).encodeString(key, value??"");
});
}
if (type == allPresistent || type == urlPresistent) {
if (!urlPersistent.containsKey(baseUrl)) {
urlPersistent[baseUrl] = Map();
}
var keyMap = urlPersistent[baseUrl]!;
keyMap.addAll(map);
keyMap.forEach((key, value) {
_store(baseUrl, urlPresistent).encodeString(key, value??"");
});
}
}

参数获取:

  static Map? getPersistent(String baseUrl, {int type = allPresistent}) {
Map? map;
if (type == allPresistent || type == headerPresistent) {
Map? headerMap;
if (headerPersistent.containsKey(baseUrl)) {
headerMap = headerPersistent[baseUrl];
} else {
headerMap = null;
}
if (headerMap != null) {
if (map == null) {
map = Map();
}
map.addAll(headerMap);
}
}
if (type == allPresistent || type == urlPresistent) {
Map? urlMap;
if (urlPersistent.containsKey(baseUrl)) {
urlMap = urlPersistent[baseUrl];
} else {
urlMap = null;
}

if (urlMap != null) {
if (map == null) {
map = Map();
}
map.addAll(urlMap);
}
}
return map;
}

刷新当前缓存(应用启动刷新)

  static Map _all(String baseurl, int type) {
var mmkv= _store(baseurl, type);
var keys = mmkv.allKeys;
var map = Map();
keys.forEach((element) {
var value = mmkv.decodeString(element);
map[element] = value;
});
return map;
}

static void flushPersistent(String baseurl, {int type = allPresistent}) {
if (type == allPresistent || type == headerPresistent) {
var map = _all(baseurl, headerPresistent);
headerPersistent[baseurl]?.clear();

if (!headerPersistent.containsKey(baseurl)) {
headerPersistent[baseurl] = Map();
}
var keyMap = headerPersistent[baseurl]!;
keyMap.addAll(map);
}
if (type == allPresistent || type == urlPresistent) {
var map = _all(baseurl, urlPresistent);
urlPersistent[baseurl]?.clear();
if (!urlPersistent.containsKey(baseurl)) {
urlPersistent[baseurl] = Map();
}
var keyMap = urlPersistent[baseurl]!;
keyMap.addAll(map);
}

}

退出登陆移除持久化

static void removeAllPersistent(String baseurl, {int type = allPresistent}) {
if (type == allPresistent || type == headerPresistent) {
headerPersistent[baseurl]?.clear();
_store(baseurl, headerPresistent).clearAll();
}
if (type == allPresistent || type == urlPresistent) {
urlPersistent[baseurl]?.clear();
_store(baseurl, urlPresistent).clearAll();
}
}

拦截器实现(dio请求拦截管理)

class PresistentInterceptor extends Interceptor {

@override
void onRequest(RequestOptions options, RequestInterceptorHandler handler)
{
var urlPersitents = HttpPersistent.getPersistent(options.baseUrl,type: HttpPersistent.urlPresistent);
var headerPersitents = HttpPersistent.getPersistent(options.baseUrl,
type: HttpPersistent.headerPresistent);
headerPersitents?.forEach((key, value) {
options.headers[key] = value;
});

urlPersitents?.forEach((key, value) {
options.queryParameters[key] = value;
});
super.onRequest(options, handler);
}

}

四、整体代码&事件调用逻辑

整体代码


class HttpPersistent{

static const int urlPresistent = 1;
static const int headerPresistent = 2;
static const int allPresistent = 3;

static MMKV _store(String baseUrl, int type) => MMKVStore.sysSafeMMKV(name: '${SysConfig.sysPersistent}${baseUrl}_${type.toString()}');
static Map> headerPersistent = Map();
static Map> urlPersistent = Map();

static void setPersistent(String baseUrl,String key,String? value,{int type = allPresistent}){
if (type == allPresistent || type == headerPresistent) {
if (!headerPersistent.containsKey(baseUrl)) {
headerPersistent[baseUrl] = Map();
}
var keyMap = headerPersistent[baseUrl]!;
keyMap[key] = value;
_store(baseUrl, headerPresistent).encodeString(key, value??"");
}
if (type == allPresistent || type == urlPresistent) {
if (!urlPersistent.containsKey(baseUrl)) {
urlPersistent[baseUrl] = Map();
}
var keyMap = urlPersistent[baseUrl]!;
keyMap[key] = value;
_store(baseUrl, urlPresistent).encodeString(key, value??"");
}
}

static void setPersistentMap(String baseUrl,Map map,{int type = allPresistent}){
if (type == allPresistent || type == headerPresistent) {
if (!headerPersistent.containsKey(baseUrl)) {
headerPersistent[baseUrl] = Map();
}
var keyMap = headerPersistent[baseUrl]!;
keyMap.addAll(map);
keyMap.forEach((key, value) {
_store(baseUrl, headerPresistent).encodeString(key, value??"");
});
}
if (type == allPresistent || type == urlPresistent) {
if (!urlPersistent.containsKey(baseUrl)) {
urlPersistent[baseUrl] = Map();
}
var keyMap = urlPersistent[baseUrl]!;
keyMap.addAll(map);
keyMap.forEach((key, value) {
_store(baseUrl, urlPresistent).encodeString(key, value??"");
});
}
}

static Map? getPersistent(String baseUrl, {int type = allPresistent}) {
Map? map;
if (type == allPresistent || type == headerPresistent) {
Map? headerMap;
if (headerPersistent.containsKey(baseUrl)) {
headerMap = headerPersistent[baseUrl];
} else {
headerMap = null;
}
if (headerMap != null) {
if (map == null) {
map = Map();
}
map.addAll(headerMap);
}
}
if (type == allPresistent || type == urlPresistent) {
Map? urlMap;
if (urlPersistent.containsKey(baseUrl)) {
urlMap = urlPersistent[baseUrl];
} else {
urlMap = null;
}

if (urlMap != null) {
if (map == null) {
map = Map();
}
map.addAll(urlMap);
}
}
return map;
}

static Map _all(String baseurl, int type) {
var mmkv= _store(baseurl, type);
var keys = mmkv.allKeys;
var map = Map();
keys.forEach((element) {
var value = mmkv.decodeString(element);
map[element] = value;
});
return map;
}

static void flushPersistent(String baseurl, {int type = allPresistent}) {
if (type == allPresistent || type == headerPresistent) {
var map = _all(baseurl, headerPresistent);
headerPersistent[baseurl]?.clear();

if (!headerPersistent.containsKey(baseurl)) {
headerPersistent[baseurl] = Map();
}
var keyMap = headerPersistent[baseurl]!;
keyMap.addAll(map);
}
if (type == allPresistent || type == urlPresistent) {
var map = _all(baseurl, urlPresistent);
urlPersistent[baseurl]?.clear();
if (!urlPersistent.containsKey(baseurl)) {
urlPersistent[baseurl] = Map();
}
var keyMap = urlPersistent[baseurl]!;
keyMap.addAll(map);
}

}

static void removeAllPersistent(String baseurl, {int type = allPresistent}) {
if (type == allPresistent || type == headerPresistent) {
headerPersistent[baseurl]?.clear();
_store(baseurl, headerPresistent).clearAll();
}
if (type == allPresistent || type == urlPresistent) {
urlPersistent[baseurl]?.clear();
_store(baseurl, urlPresistent).clearAll();
}
}
}



class PresistentInterceptor extends Interceptor {

@override
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
var urlPersitents = HttpPersistent.getPersistent(options.baseUrl,type: HttpPersistent.urlPresistent);
var headerPersitents = HttpPersistent.getPersistent(options.baseUrl,
type: HttpPersistent.headerPresistent);
headerPersitents?.forEach((key, value) {
options.headers[key] = value;
});

urlPersitents?.forEach((key, value) {
options.queryParameters[key] = value;
});
super.onRequest(options, handler);
}

}

1、登陆后,调用存储 HttpPersistent.setPersistent("http://www.baidu.com","token","123",HttpPersistent.headerPresistent)

 2、退出登陆后,调用移除 HttpPersistent.removeAllPersistent("http://www.baidu.com",,type: HttpPersistent.headerPresistent); 

3、应用启动后刷新缓存
HttpPersistent.flushPersistent("http://www.baidu.com", type: HttpPersistent.headerPresistent);

五、大功告成

如上就构建出一套可靠性高,维护性高的网络持久化框架

更多flutter教程请关注我的IMGeek:http://www.imgeek.org/people/3369…


收起阅读 »

熬夜再战Android之修炼Kotlin-【Kotlin的static是什么】

👉关于作者众所周知,人生是一个漫长的流程,不断克服困难,不断反思前进的过程。在这个过程中会产生很多对于人生的质疑和思考,于是我决定将自己的思考,经验和故事全部分享出来,以此寻找共鸣!!!专注于Android/Unity和各种游戏开发技巧,以及各种资源分享(网站...
继续阅读 »

👉关于作者

众所周知,人生是一个漫长的流程,不断克服困难,不断反思前进的过程。在这个过程中会产生很多对于人生的质疑和思考,于是我决定将自己的思考,经验和故事全部分享出来,以此寻找共鸣!!!

专注于Android/Unity和各种游戏开发技巧,以及各种资源分享(网站、工具、素材、源码、游戏等)

欢迎关注公众号【空名先生】获取更多资源和交流!

👉前提

前面我们学了Kotlin语言,趁热打铁我们试试Kotlin在Android中的应用。

如果是新手,请先学完Android基础。

推荐先看小空之前写的熬夜Android系列,再来尝试。

👉实践过程

😜方式一

Java中有static关键字,而且我们常用,在Kotlin中是伴生对象,使用方式如下:

class LoginFragment : Fragment() {
    companion object {
        //默认无权限修饰符的话,就public类型
        const val APP_Name = "空名先生"
        var APP_Name_Change = "空名先生"
        private const val APP_Author = "芝麻粒儿"
    }
}
class MainActivity : AppCompatActivity(), View.OnClickListener {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        Log.e("TAG", "onCreate: " + LoginFragment.APP_Name)
        // Log.e("TAG", "onCreate: "+LoginFragment.APP_Author ) //无法调用,因为是私有类型private
        LoginFragment.APP_Name_Change="我修改了你的名字"
        Log.e("TAG", "onCreate: " + LoginFragment.APP_Name_Change)
}
}

输出结果:
2021-10-19 16:04:26.574 22369-22369/cn.appstudy E/TAG: onCreate: 空名先生
2021-10-19 16:04:26.577 22369-22369/cn.appstudy E/TAG: onCreate: 我修改了你的名字

上面是关于变量的使用,那方法呢?Java中方法加上【static】关键字就是静态再加上public就是公开的了,哪都能用。Kotlin呢?

companion object {
fun myWork() {
Log.e("TAG", "方法:我的工作是研发")
}
//默认无权限修饰符的话,就public类型
const val APP_Name = "空名先生"
var APP_Name_Change = "空名先生"
private const val APP_Author = "芝麻粒儿"
}

如上在里面正常些函数,其他的kt文件中就能调用。

难道就这么简单?

注意,重点来了。如果在Java中调用Kotlin呢?

我们创建个【TextActivity】,调用下:

public class TextActivity extends AppCompatActivity {
    public static String myName = "";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_text);
        //LoginFragment.myWork();  //无法调用
        //LoginFragment.setAPP_Name_Change("修改名字"); //无法调用
        LoginFragment.Companion.myWork();
        LoginFragment.Companion.setAPP_Name_Change("修改名字");
    }
}

从实践中我们得知,在KT的companion object中做的任何声明,在Java中不能直接调用,而是利用【Companion】实体调用出来的,这就相当于new个类,调用实例方法了,而非静态方法。

所以需要这样:

companion object {

        @JvmStatic
        fun myWork() {
            Log.e("TAG", "方法:我的工作是研发")
        }

        //默认无权限修饰符的话,就public类型
        const val APP_Name = "空名先生"
 
        @JvmStatic
        var APP_Name_Change = "空名先生"
        private const val APP_Author = "芝麻粒儿"
        //非const类型的常量 val修饰的,要想Java中作为静态引用需要@JvmField
        @JvmField
        val MY_APP = "我的作品"
}
public class TextActivity extends AppCompatActivity {

    public static String myName = "";

    @Override
    protected void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_text);
        //LoginFragment.myWork();  //无法调用
        //LoginFragment.setAPP_Name_Change("修改名字"); //无法调用
        Log.e("TAG", "onCreate: " + LoginFragment.APP_Name);
        //这些使用均正常,即使添加了@JvmStatic,仍然可以用Companion实例形式调用
        LoginFragment.Companion.myWork();
        LoginFragment.Companion.setAPP_Name_Change("修改名字");
        LoginFragment.myWork();
        LoginFragment.getAPP_Name_Change();
        Log.e("TAG", "onCreate: " + LoginFragment.MY_APP);
        Log.e("TAG", "onCreate: " + LoginFragment.APP_Name);
    }
}

运行后你再试试,会发现,哎?真的,没有那么多吐司了,真的好啊。

要想实现Java中直接点出来的静态形式

  • var类型的要添加@JvmStatic
  • const修饰的不用管
  • 方法使用@JvmStatic
  • 非const修饰却为val类型的使用@JvmField

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

Flutter进阶-key的原理

在之前的篇幅介绍中,我们在构造函数中都没有使用key,只要继承于Widget的都默认有这个key属性,这是一个可选属性。下面我们通过案例来研究一个key的作用。const MyApp({Key? key}) : super(key: key);Stateful...
继续阅读 »

在之前的篇幅介绍中,我们在构造函数中都没有使用key,只要继承于Widget的都默认有这个key属性,这是一个可选属性。下面我们通过案例来研究一个key的作用。

const MyApp({Key? key}) : super(key: key);

StatefulWidget中的key

先搭建一个页面,在页面的中间位置随机创建不同颜色的正方形

class _MyHomePageState extends State<MyHomePage> {
List<SquareItem1> list = [
const SquareItem1('上上上', key: ValueKey(111),),
const SquareItem1('中中中', key: ValueKey(222),),
const SquareItem1('下下下', key: ValueKey(333),)
];

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: list,
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
setState(() {
list.removeAt(0);
});
},
child: const Icon(Icons.add),
), // This trailing comma makes auto-formatting nicer for build methods.
);
}
}

其中SquareItem1的构造方法如下:const SquareItem1(this.title, {Key? key}) : super(key: key);

import 'dart:math';
import 'package:flutter/material.dart';
class SquareItem1 extends StatefulWidget {
final String title;
const SquareItem1(this.title, {Key? key}) : super(key: key);

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

class _SquareItem1State extends State<SquareItem1> {
final color = Color.fromRGBO(
Random().nextInt(256), Random().nextInt(256), Random().nextInt(256), 1.0);
@override
Widget build(BuildContext context) {
return Container(
width: 100,
height: 100,
color: color,
child: Text(widget.title),
);
}
}

在点击按钮的时候,一次删除数组中的第一个元素 仔细看,通过实验对比可以明显的发现,此时顺序似乎是有点问题,那么问题出在哪里?

image.png

  • 先看文字:文字的话似乎顺序没有问题,每一次都删除最上面的
  • 再看颜色:颜色话倒像从是后面开始删除的,每次把最后一个删掉

带着这个疑问,我们再来看看StatelessWidget中的key

StatelessWidget中的key

这次继承的是StatelessWidget,其中SquareItem1的构造方法如下:SquareItem(this.title, {Key? key}) : super(key: key);

import 'dart:math';
import 'package:flutter/material.dart';

class SquareItem extends StatelessWidget {
final String title;
SquareItem(this.title, {Key? key}) : super(key: key);

final color = Color.fromRGBO(
Random().nextInt(256), Random().nextInt(256), Random().nextInt(256), 1.0);
@override
Widget build(BuildContext context) {
return Container(
width: 100,
height: 100,
color: color,
child: Text(title),
);
}
}

image.png

经过观察发现,此次的remove的顺序没有问题,颜色和文字都能一一对应上。通过两边的代码对比发现唯一的差别可能就是color初始化的位置区别,一个是在State中初始化,一个是在Widget中初始化,那是不是就是这个原因呢,我们接着研究。

key的使用

StatefulWidget中我们可以通过给key赋值来区分不同的Widget,示例

  List<SquareItem1> list = [
const SquareItem1(
'上上上',
key: ValueKey(111),
),
const SquareItem1(
'中中中',
key: ValueKey(222),
),
const SquareItem1(
'下下下',
key: ValueKey(333),
)
];

此时再运行发现颜色+文字删除的顺序正确了。所以我们有理由合理大胆的猜测,之所以在Stateful中数据紊乱是因为对应关系出了问题,那么到底是不是呢,我们看下API。StatelessWidget -> Widget 这里有一个方法canUpdate

  static bool canUpdate(Widget oldWidget, Widget newWidget) {
return oldWidget.runtimeType == newWidget.runtimeType
&& oldWidget.key == newWidget.key;
}

Flutter是增量渲染,哪些需要更新的通过上面的那个方法来判断。所以当两个Widget都StatefulWidget的时候,如果不指定key的话,如果结构一样那么此时这个方法就会返回True

image.png

  • Widget中保存的是Text
  • Element中保存的是Color
  • 在删除的时候,虽然我们删除了文字,但是由于没有指定key,所以canUpdate = true所以此时第一个Element的颜色指向了Widget的第二个,这也就是示例一中出现的问题。

当然如果再新增一个跟上面一模一样的小部件,此时没有用到了Element下有了新的指向就不会删除了。也就是说新增的color= Element下的颜色

Key的原理

Key本身是一个抽象类,有一个工厂构造方法,创建ValueKey,其直接子类主要有:LocalKeyGlobalKey

  1. GlobalKey:帮助访问某一个Widget的信息
  2. LocalKey: 用来区别哪个Element需要保留
    • const ValueKey(this.value);// 以值作为参数,数字、字符串
    • const ObjectKey(this.value);// 以对象作为参数
    • UniqueKey();// 创建唯一标识

GlobalKey的使用

import 'package:flutter/material.dart';

class GlobalDemo extends StatelessWidget {
const GlobalDemo({Key? key}) : super(key: key);

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Home Page'),
),
body: const BodyCenter(),
floatingActionButton: FloatingActionButton(
onPressed: () {
//外层需要调用内层的count++
},
child: const Icon(Icons.add),
),
);
}
}

class BodyCenter extends StatefulWidget {
const BodyCenter({Key? key}) : super(key: key);

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

class _BodyCenterState extends State<BodyCenter> {
int count = 0;
String title = 'hello';

@override
Widget build(BuildContext context) {
return Center(
child: Column(
children: [Text(count.toString()), Text(title)],
mainAxisAlignment: MainAxisAlignment.center,
),
);
}
}

在下面的设计中,外层是一个StatelessWidget中间的body是一个StatefulWidget,我们在外层onPress的时候,正常情况是无法更新内存的count的,此时用GlobalKey可以解决

image.png

  1. 在外层初始化一个GlobalKey同时指定需要跟哪个State绑定final GlobalKey<_BodyCenterState> _globalKey = GlobalKey();
  2. 内存小部件初始化的时候同步绑定body: BodyCenter(key: _globalKey),
  3. 使用方式:
onPressed: () {
_globalKey.currentState!.setState(() {
_globalKey.currentState!.title =
'上一次count=' + _globalKey.currentState!.count.toString();
_globalKey.currentState!.count++;
});
},

image.png

只要是属于当前子部件的树状结构中,这种方式都管用,都可以拿到子部件的数据。


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

哪怕不学Gradle,这些常见操作,你也值得掌握

Gradle 是每个 Android 同学都逃不开的一个话题。你是否看到别人的 Gradle 文件干净又卫生?而自己的又是一团乱麻🏷不用怕,本篇将结合我的开发日常,将一些常用的操作分享出来,希望可以帮到像我一样...
继续阅读 »

Gradle 是每个 Android 同学都逃不开的一个话题。

你是否看到别人的 Gradle 文件干净又卫生?而自己的又是一团乱麻🏷

不用怕,本篇将结合我的开发日常,将一些常用的操作分享出来,希望可以帮到像我一样不怎么会[玩]Gradle 的同学,相信会对大家有所帮助。

模板代码提取

这是最基础的操作了,对于一个普通 model.gradle ,默认的配置如下:

image-20220117094502841

如果我们每个 model 都这样写,那岂不是很麻烦,那么让我们提取通用代码:

优化步骤

新建一个 gradle 文件,命名为 xxx.gradle ,复制上述 model 里的配置,放到你的项目中,可以自定义修改一些通用内容,在其他model 中依赖即可,如下所示:

这是一个播放器model

// 这就是刚才新建的默认gradle文件,
// 注意:如果你的default.gradle是在项目目录下,请使用../,如果仅在app下,请使用./
apply from: "../default.gradle"
import xxx.*

android {
// 用于隔离不同model的资源文件
resourcePrefix "lc_play_"
}


dependencies {
compileOnly project(path: ':common')
api xxx
}

上述的 android{} , dependencies{}

其内部的内容都会在 default.gradle 的基础上叠加,对于唯一的键值对,会进行替换。

定义统一的config配置

在项目中,你是如何去写你的版本号等其他默认配置呢?

image-20220113100724957

对于一个新项目,其默认的配置如下所示,每次新创建 model ,也需要定义其默认参数,如果每次都直接在这里去改动,那么如果版本变化,意味着我们需要修改多次,这并不是我们想看到的效果。

优化步骤

新建 config.gradle ,内容如下:

// 一些配置文件的保存

// 使用git的commit记录当做versionCode
static def gitVersionCode() {
def cmd = 'git rev-list HEAD --count'
return cmd.execute().text.trim().toInteger()
}

static def releaseBuildTime() {
return new Date().format("yyyy.MM.dd", TimeZone.getTimeZone("UTC"))
}

ext {
android = [compileSdkVersion: 30,
applicationId : "com.xxx.xxx",
minSdkVersion : 21,
targetSdkVersion : 30,
buildToolsVersion: "30.0.2",
buildTime : releaseBuildTime(),
versionCode : gitVersionCode(),
versionName : "1.x.x"]
}

使用时:

android {
def android = rootProject.ext.android
defaultConfig {
multiDexEnabled true
minSdk android.minSdkVersion
compileSdk android.compileSdkVersion
targetSdk android.targetSdkVersion
versionCode android.versionCode
versionName android.versionName
}
}

配置你的build

配置不同build类型

在开发中,我们一般会有多个环境,比如 开发环境 ,测试环境线上环境

buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}

dev{
// initWith代表的是允许从其他build类型进行复制操作,然后配置我们想更改的设置
// 这里代表的是从release复制build配置
initWith release
// 清单占位符
manifestPlaceholders = [hostName:"com.petterp.testgradle.dev"]
// 会在你原包名后面新增.test
applicationIdSuffix ".dev"
}
}

如上所述,dev 是我们新增的 build类型 ,当新增之后,我们就可以在命令行使用如下匹配的指令,或者点击As 最右侧,gradle图标,选择app(根据自己build的配置位置而定,一般默认是app-model),选择other,即可看到多了如下几个指令:

image-20220114095904490

当然你也可以选择如下命令行执行,以便在 Jenkins 或者 CI 下 build 时执行:

gradlew buildDev
gradlew assembleDev

注意,mac下是gradlew开头,windows下可能是./gradlew


配置变体

对于开发中,我们一般都有多渠道的需求,一般而言,如果仅仅是多渠道我们可以选择使用第三方 walle 去做,如果我们可能还有更精细的设置,比如针对这个 build类型,我们很可能对应了不同的默认配置等,比如配置不同的 applicationId ,资源。

如下所示:

// 变体风味名,如果只设置一个,则所有变体会自动使用,如果存在两个及以上,需要在变体中指定,并且变体需要与分组匹配。
// 风味名,类似于风格,分组的意思。
flavorDimensions "channel"
// flavorDimensions ("channel","api")
productFlavors {
demo1 {
// 每一个变体都必须存在一个风味,默认使用flavorDimensions(仅限其为单个时)的值,否则如果没提供,则会报错。
dimension "channel"
// appid后缀,会覆盖了我们build类型中的applicationIdSuffix
applicationIdSuffix ".demo"
// 版本后缀
versionNameSuffix "-demo"
}
demo2 {
dimension "channel"
applicationIdSuffix ".demo2"
versionNameSuffix "-demo2"
}
}

然后查看我们的 build Variants:

image-20220115110605931

Gradle 会根据我们的 变体 和 build类型 自动创建多个build变种,按照 变体名-build类型名 方式命名。

在配置变体时,我们也可以替换在 build类型 中设置的所有默认值,具体原因是,在添加 build类型时,默认的 defaultConfig 配置其实是属于 ProductFlavors 类,所以我们也可以在任意变体中替换所有默认值。


组合多个变体

在某些场景下,我们可能想将多个产品的变体组合在一起,比如我们想增加一个 api30 的变体,并且针对这个变体,我们想让demo1和demo2与分别也能与其组合在一起 ,即也就是当channel是demo1时api30下对应的包。

理解起来有些拗口,示例如下所示,我们更改上面的配置:

  flavorDimensions("channel", "api")
productFlavors {
demo1 {
dimension "channel"
applicationIdSuffix ".demo"
versionNameSuffix "-demo"
}
demo2 {
dimension "channel"
applicationIdSuffix ".demo2"
versionNameSuffix "-demo2"
}
minApi23 {
dimension "api"
minSdk 23
applicationIdSuffix ".minapi23"
versionNameSuffix "-minapi23"
}
}

最终如下所示,左侧是 gralde 生成的 build变种 ,右侧对应其中 demo1MinApi23Debug 打包后的产物具体信息:

image-20220115114123883

所以我们可以总结为:

最终我们在打包时,我们的包名和版本名会根据多个变体混合生成,具体如上图所示,然后分别使用了两者都具有的配置,当配置出现重复时,优先以开头的变体配置作为基准。

比如如果我们给demo1变体也配置了最低sdk版本是21,那么最终打出来的包minSdk也会是21,而不是minApi23中的minSdk配置,这点需要注意。


解疑

那么 变体 和 build类型 两者到底应该怎么选?似乎两者好像很是相似?

其实不难理解,如下所示:

比如你新增了一个变体 firDev ,那么默认情况下就会有如下的 build命令 生成

firDevDebug
firDevRelase
firDevXXX(xxx是你自定义的build类型)

需要注意的是 debug 和 relase 是默认就会存在的,我们可以选择覆盖,否则就算移除,其也会选择默认设置存在

即也就是最终 gradle 会帮我们每个变体都生成相应的 build类型 对应的命令,变体就相当于不同的渠道,而 build类型 就相当于针对这个渠道,存在着多种环境,比如 debug,relase,你自定义的更多build类型。

  • 所以如果你的场景仅仅是想对应几个不同环境,那么直接配置 build类型 即可;
  • 如果你可能希望区分不同的包下的依赖项或者资源配置,那么配置变体即可。

过滤变体

Gradle 会为我们配置的 所有变体 和 build类型 每一种可能组合都创建一个 build变种 。当然有些变种,我们并不需要,所以我们可以在相应模块的 build.gradle 中创建 变体过滤器 ,以便移除某些不需要的变体配置。

android{
...
variantFilter { variant ->
def names = variant.flavors*.name
if (names.contains("demo2")) {
setIgnore(true)
}
}
...
}

效果如下:

image-20220115120754881

针对变体配置依赖项

我们也可以针对上面这些变体,进行不同的依赖。比如:

 demo1Implementation  xxx
minApi23Implementation xxxx

常见技巧

关于依赖管理

对于一些环境下,我们并不想在线上依赖某些库或者 model ,如果是三方库,一般都会有 relase 下依赖的版本。

如果是本地model,目前已经引用到了,所以就需要对于线上环境做null包处理,只留有相应的包名与入口,具体的实现都为null.

限制依赖条件为build类型

debugImplementation project(":dev")
releaseImplementation project(":dev_noop")

有一点需要注意,当我们使用默认的 debugImplementation 和 releaseImplementation 进行依赖时,最终打包时是否会依赖其中,取决于我们 使用的build命令中build类型是不是debug或者relase ,如果使用的是自定义的 dev ,那么上述的两个 model 也都不会依赖,很好理解。

限制依赖条件为变体

相应的,如果我们希望当前的依赖的库或者model 不受 build类型 限制,仅受 变体 限制,我们也可以使用我们的 变体-Implementation 进行依赖,如下所示:

demo1Implementation project(":dev")

这个意思是,如果我们打包时使用demo1相应的gradle命令,比如assembleDemo1Debug,那么无论当前build类型是debug还是release或者其他,其都会参与依赖。

排除传递的依赖项

开发中,我们经常会遇见依赖冲突,对于第三方库导致的依赖冲突,比较好解决,我们只需要使用 exclude 解决即可,如下所示:

dependencies {
implementation("androidx.lifecycle:lifecycle-extensions:2.2.0") {
exclude group: 'androidx.lifecycle', module: 'lifecycle-process'
}
}

统一全局的依赖版本

有时候,某些库会存在好多个版本,虽然 Gradle 会默认选用最高的版本,但是依然不免有时候还是会提示报错,此时我们就可以通过配置全局统一的版本限制:

android{
defaultConfig {
configurations.all {
resolutionStrategy {
force AndroidX.Core
force AndroidX.Ktx.Core
force AndroidX.Work_Runtime
}
}
}
}

简化你的BuildConfig配置

开发中,我们常见的都会将一些配置信息,写入到 BuildConfig 中,以便我们在开发中使用,这也是最常用的手段之一了。

配置方式1

最简单的方式就是,我们可以在执行 applicationVariants task任务时,将我们的 config 写入配置中,示例如下:

app/ build.gradle

android.applicationVariants.all { variant ->
if ("release" == variant.buildType.getName()) {
variant.buildConfigField "String", "baseUrl", "\"xxx\""
} else if ("preReleaseDebug" == variant.buildType.getName()) {
variant.buildConfigField "String", "baseUrl", "\"xxx\""
} else {
variant.buildConfigField "String", "baseUrl", "\"xxx\""
}
variant.buildConfigField "String", "buglyAppId", "\"xx\""
variant.buildConfigField "String", "xiaomiAppId", "\"xx\""
...
}

在写入时,我们也可以通过判断当前的 build类型 从而决定到底写入哪些。

优化配置

如果配置很少的话,上述方式写还可以接收,那如果配置参数很多,成百呢?此时就需要我们将其抽离出来了。

所以我们可以新建一个 build_config.gradle ,将上述代码复制到其中。

image-20220115155803121

然后在需要的 模块 里,依赖一下即可。

apply from: "build_config.gradle"

这样做的好处就是,可以减少我们 app-build.gradle 里的逻辑,通过增加统一的入口,来提高效率和可读性。


配置方式2

当然也有另一种方式,相当于我们自己定义两个方法,在 buildType 里自行调用,相应的我们将 config配置 按照规则写入一个文件中去管理。

示例代码:

app/ build.gradle

buildTypes {
// 读取 ./build_extras 下的所有配置
def configBuildExtras = { com.android.build.gradle.internal.dsl.BuildType type ->
// This closure reads lines from "build_extras" file and feeds its content to BuildConfig
// Nothing but a better way of storing magic numbers
def buildExtras = new FileInputStream(file("./build_extras"))
buildExtras.eachLine {
def keyValue = it == null ? null : it.split(" -> ")
if (keyValue != null && keyValue.length == 2) {
type.buildConfigField("String", keyValue[0].toUpperCase(), "\"${keyValue[1]}\"")
}
}
}
release {
...
configBuildExtras(delegate)
...
}
debug{
...
configBuildExtras(delegate)
...
}
}

build_extras

...
baseUrl -> xxx
buglyId -> xxx
...

上述两种配置方式,我们可以根据需要自行决定,我个人是比较喜欢方式1,毕竟看着更简单,但其实两者的实现方式也是大差不大,具体看个人习惯吧。

管理全局插件的依赖

某些时候,我们所有的model,可能都需要集成一个插件,此时我们就可以通过在 项目build.gradle 里全局统一管理,而避免到每一个Gradle 下去集成:

// 管理全局插件的依赖
subprojects { subproject ->
// 默认应用所有子项目中
apply plugin: xxx
// 如果想应用到某个子项目中,可以通过 subproject.name 来判断应用在哪个子项目中
// subproject.name 是你子项目的名字,示例如下
// 官方文档地址:https://guides.gradle.org/creating-multi-project-builds/#add_documentation
// if (subproject.name == "app") {
// apply plugin: 'com.android.application'
// apply plugin: 'kotlin-android'
// apply plugin: 'kotlin-android-extensions'
// }
}

动态调整你的组件开关

对于一些组件,在 debug 开发时如果依赖,对我们的编译时间可能会有影响,那么此时,如果我们增加相应的开关控制,就会比较好:

buildscript {
ext.enableBooster = flase
ext.enableBugly = flase

if (enableBooster)
classpath "com.didiglobal.booster:booster-gradle-plugin:$booster_version"
}

如果每次都是静态控制,那么当我们使用 CI 来打包时,就会没法操作。所以相应的,我们可以更改一下逻辑:

我们创建一个文件夹,里面放的是相应的忽略文件,如下所示:

image-20220115134753904

然后我们更改一下相应的 buildscript 逻辑:

buildscript {
ext.enableBooster = !file("ignore/.boosterignore").exists()
ext.enableBugly = !file("ignore/.buglyignore").exists()

if (enableBooster)
classpath "com.didiglobal.booster:booster-gradle-plugin:$booster_version"
}

通过判断相应的插件对应的文件是否存在,来决定插件在CI打包时的启用状态。在CI打包时,我们只需要通过shell删除相应的配置ignore文件或者通过gradle执行相应命令即可。因为本篇是讲gradle的一些操作,所以我们就主要演示一下gradle的命令示例。

定义自己的gradle插件

我们先简单写一个最入门的插件,用来移除相应的文件,来达到开关插件的目的。

task checkIgnore {
println "-------checkIgnore--------开始->"
removeIgnore("enableBugly", ".buglyignore")
removeIgnore("enableGms", ".gmsignore")
removeIgnore("enableByteTrack", ".bytedancetrackerignore")
removeIgnore("enableSatrack", ".satrackerignore")
removeIgnore("enableBooster", ".boosterignore")
removeIgnore("enableHms", ".hmsignore")
removeIgnore("enablePrivacy", ".privacyignore")
println "-------checkIgnore--------结束->"
}

def removeIgnore(String name, ignoreName) {
if (project.hasProperty(name)) {
delete "../ignore/$ignoreName"
def sdkName = name.replaceAll("enable", "")
println "--------已打开$sdkName" + "组件"
}
}

这个插件的作用很简单,就是通过我们 Gradle 命令 携带的参数 来移除相应的插件文件。

gradlew app:assembleRoyalFinalDebug  -PenableBugly=true

image-20220115143720964

具体如图所示:在 CI-build 时,我们就可以通过传递相应的值,来动态决定是否启用某插件。


优化版

上述方式虽然方便,但是看着依然很麻烦,那么有没有更简单,单纯利用 Gradle 即可。其实如果稍微懂一点 Gradle 生命周期,这个问题就能轻松解决。

我们可以在 settings.gradle 里监听一下 Gradle 的 生命周期 ,然后在项目结构加载完成时,也就是 projectsLoaded 执行时,去判断一下,如果存在某个参数,那么就打开相应的组件,否则关闭。

示例:

settings.gradle

gradle.projectsLoaded { proj ->
println 'projectsLoaded()->项目结构加载完成(初始化阶段结束)'
def rootProject = proj.gradle.rootProject
rootProject.ext.enableBugly = rootProject.findProperty("enableBugly") ?: false
rootProject.ext.enableBooster = rootProject.findProperty("enableBooster") ?: false
rootProject.ext.enableGms = rootProject.findProperty("enableGms") ?: false
rootProject.ext.enableBytedance = rootProject.findProperty("enableBytedance") ?: false
rootProject.ext.enableSadance = rootProject.findProperty("enableSadance") ?: false
rootProject.ext.enableHms = rootProject.findProperty("enableHms") ?: false
rootProject.ext.enablePrivacy = rootProject.findProperty("enablePrivacy") ?: false
}

执行build命令时携带相应参数即可:

gradlew assembleDebug -PenablePrivacy=true 

参考

Android开发者-配置你的build

我是Petterp,一个三流开发,如果本文对你有所帮助,欢迎点赞支持。


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

超实用的算法小技巧

本篇文章我们将介绍一些超级实用的算法小技巧,灵活使用这些算法小技巧可以帮助我们更好的解决遇到的问题,让我们的时间复杂度,空间复杂度大大降低,有效的提高我们的编程能力。1 严格定义函数名称,出入参我们在一开始拿到算法题,读懂题之后,就需要根据题意定义我们的函数名...
继续阅读 »

本篇文章我们将介绍一些超级实用的算法小技巧,灵活使用这些算法小技巧可以帮助我们更好的解决遇到的问题,让我们的时间复杂度,空间复杂度大大降低,有效的提高我们的编程能力。

1 严格定义函数名称,出入参

我们在一开始拿到算法题,读懂题之后,就需要根据题意定义我们的函数名称,以及入参,函数的返回类型。日常的企业项目开发也是一样,我们在拿到需求之后,需要去定义接口,入参,出参。

我们一定要给处理函数起一个能够明确表达函数功能的名字比如:排序sort,搜索search,统一用英文表示(入参也是如此)。

leetcode技巧: 变量名称定义简单,可以提高算法执行速度,这也是在好多人在刷leetcode参数定义不那么规范的原因,往往在周赛或者一些比赛中一点点优势,就能助我们取得胜利。

2 严进宽出,边界判断

严进宽出就是说我们要对算法的入参进行严格的验证,比如我们经常要对数组、字符串进行非空校验,还有一些需要对边界值进行校验。

对于不符合规范的直接返回,很好的把控边界也可提升我们的算法效率。

笔试小技巧: 优雅严格的边界值判断,往往能给面试官留下很好的印象。

// 入参校验
if (nums == null || nums.length == 0) {
return -1;
}
// 边界值(不相等时,我们让左边指针移动到二分处,并且+1就很细节,因为中间点已经不符合,所以我们+1可以少循环一次)
while (l <= r) {
mid = l + (r - l) / 2;
   if (nums[mid] == target) {
  return mid;
  } else if (nums[mid] < target) {
       l = mid + 1;
  } else if (nums[mid] > target) {
       r = mid - 1;
  }
}

3 暴力解法

没有经过算法训练的同学,一般在解决算法问题时,只能想到暴力解法,常常就是多层嵌套函数,定义额外的空间。

往往时间复杂度都是O(N)、O(N²) 虽然也能解决算法问题,但是往往因为算法的执行效率过低,代码不够优雅让Offer与我们失之交臂。

不过暴力解法虽然效率不高,但是是我们必须掌握的,写出来永远比什么都写不出来要强的多,暴力解法就要求我们灵活应用每种数据结构的遍历,并加入条件判断逻辑。

以下几个技巧则可以帮助我们优化算法,提供算法的执行效率。

4 双指针(Two Pointers)

双指针是一种算法小套路,我们在好多地方可以见到双指针的妙用,比如二分查找,确定链表是否成环等,接下来我们就来一起探究一下双指针的妙用。

image.png

双指针一般有以下几种形式。

  • 普通双指针

    两个指针往同一个方向移动

        /**
        * 冒泡排序
        * @param nums
        */
       public static void sort(int [] nums) {
           if (nums == null || nums.length == 0) {
               return;
          }
           int temp = 0;
           for (int i = 0; i < nums.length - 1; i++) {
               for (int j = i + 1; j < nums.length; j++) {
                   if (nums[i] > nums[j]) {
                       temp = nums[i];
                       nums[i] = nums[j];
                       nums[j] = temp;
                  }
              }
          }
      }
  • 对撞双指针

    两个指针从两端向对方移动

    /**
    * 力扣704 二分查找
    * 二分查找算法是借助二分的思想,结合双指针来实现的一种搜索算法
    * @author zhj
    */
    public class Test704 {
       public static void main(String[] args) {
           int[] nums = {-1, 0, 3, 5, 9, 12};
           int index = search(nums, 5);
           System.out.println(index);
      }

       private static int search(int[] nums, int target) {
           if (nums == null || nums.length == 0) {
               return -1;
          }
           int l = 0;
           int r = nums.length - 1;
           int mid;
           while (l <= r) {
               mid = l + (r - l) / 2;
               if (nums[mid] == target) {
                   return mid;
              } else if (nums[mid] < target) {
                   l = mid + 1;
              } else if (nums[mid] > target) {
                   r = mid - 1;
              }
          }
           return -1;
      }
    }
  • 快慢双指针

    慢指针+快指针 解决环形链表问题

    /**
    * 力扣 141 环形链表
    * 给定一个链表,判断链表中是否有环
    * @author zhj
    */
    public class Test141 {
       public static void main(String[] args) {
           ListNode node = new ListNode(1);
           ListNode node1 = new ListNode(2);
           ListNode node2 = new ListNode(3);
           ListNode node3 = new ListNode(4);
           ListNode node4 = new ListNode(5);
           node.next = node1;
           node1.next = node2;
           node2.next = node3;
           node3.next = node4;
           node4.next = node2;
           System.out.println(isRing(node));
      }

       private static boolean isRing(ListNode node) {
           if (node == null || node.next == null) {
               return false;
          }
           ListNode s = node;
           ListNode f = node;
           while (f != null && f.next != null) {
               s = s.next;
               f = f.next.next;
               if (s == f) {
                   return true;
              }
          }
           return false;
      }
    }

5 滑动窗口

滑动窗口也是一种算法小技巧,可以极大的减少重叠部分计算量,尤其是当重叠部分比较大的时候,效果格外明显。滑动窗口主要解决的是连续定长子数组的问题,但是有一些非定长的也可以通过滑动窗口的思想来解决。

当我们移动窗口时只需要排除移除的一个数据,在加入移入的一个数据,窗口内其它数据是不需要做出改变的。

image.png

非定长一般需要通过加入内层循环来解决。

/**
* 力扣209 长度最小的子数组
* @author zhj
*/
public class Test209 {

   public static void main(String[] args) {
       int[] nums = {2,3,1,2,4,3};
       System.out.println(mumsLength(nums, 7));
  }

   private static int mumsLength(int[] nums, int sum) {
       if (nums == null || nums.length == 0) {
           return 0;
      }
       int res = nums.length + 1;
       int total = 0;
       int i = 0;
       int j = 0;
       while (j < nums.length) {
           total = total + nums[j];
           j++;
           while (total >= sum) {
               res = res < j-i ? res : j-i;
               total = total - nums[i];
               i = i + 1;
          }
      }
       if (res == nums.length + 1) {
           return 0;
      }
       return res;
  }
}

6 递归

一些复杂的问题,我们往往可以通过递归去简化。

特点:自己调自己,根据特点条件可以返回,不会陷入死循环。

递归的四个要素:

  • 参数
  • 返回值
  • 终止条件
  • 递归拆解

经典问题斐波那契数列

0,1,1,2,3,5... f(0) = 0 f(1) = 1 f(n) = f(n-1) + f(n-2)

int recursion(int n) {
   if (n < 2) {
       return n == 1 ? 1 : 0;
  }
   return recursion(n-1) + recursion(n-2);
}

7 高阶算法

除了上边一些简单的小技巧之外,还有许多高阶的算法,比如由递归引发的分治法、回溯法;还有树和图的遍历方法,深度优先遍历,广度优先遍历;还有经典的算法贪心算法、动态规划等等,本文将不做讲解,后续会单独更新在算法这一专栏中,希望大家持续关注。


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

收起阅读 »

揭秘 Kotlin 中的 == 和 ===

这篇文章我们主要来分析 Kotlin 中的操作符 == 和 === 的区别,以及它们分别在什么场景下使用。这些操作符在实际项目和开源项目中,使用的频率非常的高。主要包含以下内容:Java 中的 == ...
继续阅读 »

这篇文章我们主要来分析 Kotlin 中的操作符 == 和 === 的区别,以及它们分别在什么场景下使用。这些操作符在实际项目和开源项目中,使用的频率非常的高。主要包含以下内容:

  • Java 中的 == 和 equals 的区别?
  • Kotlin 提供的操作符 == 和 === 分别做什么用?
    • 比较对象的结构是否相等 ( == 或者 equals )
    • 比较对象的引用是否相等 ( === )
  • Kotlin 中的操作符在以下场景中的使用
    • 基本数据类型
    • 包装类
    • 普通类
    • 数据类

在开始分析之前,我们先来简单回顾一下 Java 中的操作符 == 和 equals 的区别。

Java 中的操作符 == 和 equals 的区别

操作符 ==

  • 如果是基本数据类型比较的是值
  • 如果是引用数据类型比较的是地址

操作符 equals

  • 默认情况下在不重写 equals 方法时,等价于 ==,比较的是地址
public boolean equals(Object obj) {
return (this == obj);
}
  • 重写 equals 方法时,一般用于比较结构是否相等,例如 String
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = value.length;
if (n == anotherString.value.length) {
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}

但是需要注意的是重写 equals 方法时,需要重写 hashCode() 方法,否则无法和 hash 集合类一起正常工作,可以通过快捷键自动生成 equals() 、hashCode() 、 toString() 等等方法。

  • Mac: Cmd + N
  • Win/Linux: Alt+Insert

更多 AndroidStudio 快捷键使用技巧查看下列文章:

关于 Java 的操作符介绍就到这里了,接下来重点来分析 Kotlin 中的操作符。

Kotlin 中的操作符 == 和 === 及 equals

Kotlin 提供了两种方式用于对象的比较。

  • 比较对象的结构是否相等( == 或者 equals )

    Kotlin 中的操作符 == 等价于 equals 用于比较对象的结构是否相等, 很多情况下使用的是 ==,因为对于浮点类型 Float 和 Double,其实现方法 equals 不遵循 IEEE 754 浮点运算标准。

  • 比较对象的引用是否相等 ( === )

    Kotlin 中的操作符 === 用于比较对象的引用是否指向同一个对象,运行时如果是基本数据类型 === 等价于 ==

我们知道了基本概念之后,接下来一起来看一下这些操作符( == 和 === 及 equals ),在以下场景中的使用。

  • 基本数据类型
  • 包装类
  • 普通类
  • 数据类

基本数据类型

我们先来看一个例子:

val a1 = -0
val a2 = 0
println(a1 == a2) // true
println(a1.equals(a2)) // true
println(a1 === a2) // true

a1 = 100
a2 = 100
println(a1 == a2) // true
println(a1.equals(a2)) // true
println(a1 === a2) // true

运行时,对于基本数据类型 === 等价于 == 比较的是值(即对象的结构是否相等),如果比较基本数据类型时使用 ===,编译器就会给出一个警告,不建议使用。

但是 equals 比较特殊, 对于浮点类型 Float 和 Double 却有不同的表现,代码如下所示。

val a3 = -0f
val a4 = 0f
println(a3 == a4) // true
println(a3.equals(a4)) // false
println(a3 === a4) // true

正如你所看到的 a3.equals(a4) 结果为 false,那么为什么会这样呢,一起来查看反编译后的 Java 代码都做了什么。Tools → Kotlin → Show Kotlin Bytecode 。

float a3 = -0.0F;
float a4 = 0.0F;
boolean var2 = Float.valueOf(a3).equals(a4);
boolean var3 = false;
System.out.println(var2);

将 float 转换为包装类型 Float,调用其 equals 方法来进行比较,来看一下 equals 方法。

运行结果正如源码注释高亮部分一样,使用 equals 方法比较 +0.0f 和 -0.0f 其结果为 false, 如果使用操作符 ==结果为 true。

在 equals 方法中调用了 floatToIntBits 方法,在这个方法中是根据 IEEE 754 浮点算法标准,返回指定浮点值的表示形式,结果是一个整数,如下所示:

System.out.println(Float.floatToIntBits(-0f));  // -2147483648
System.out.println(Float.floatToIntBits(0f)); // 0

正如你所见,Float.floatToIntBits(-0f) 计算出来的结果,是整数的最小值 -2147483648,从结果来看它不遵循 IEEE 754 浮点运算标准,一起来看一下官方是如何解释的,更多信息点击查看 IEEE 754 浮点运算标准

对于浮点类型 Float 和 Double,其实现方法 equals 不遵循 IEEE 754 浮点运算标准

  • NaN 被认为和它自身相等
  • NaN 被认为比包括正无穷在内的任何其他元素都大
  • -0.0 小于 +0.0

因此在 Kotlin 中如果使用 equals 方法进行比较的时候,需要注意这个情况。

包装类

无论是 Java 还是 Kotlin 每一种基本类型都会对应一个唯一的包装类,只不过它们的区分方式不一样。

基本数据类型包装类
byteByte
shortShort
intInteger
longLong
floatFloat
doubleDouble
charCharacter
booleanBoolean
val a5 = Integer(10)    
val a6 = Integer(10)
println(a5 == a6) // true
println(a5.equals(a6)) // true
println(a5 === a6) // false

因为包装类重写了 equals 方法,所以使用操作符 == 和 equals 比较的是对象的结构是否相等,所以结果为 true。而操作符 === 比较的是对象的引用,是否指向同一个对象,因为是不同的对象,所以结果为 false。

普通的类

普通的类其实就是我们自己新建的类,并没有重写 equals 方法,一起来看一下这三种操作符的运行结果。

class Person1(val name: String, val age: Int)

val p1 = Person1(name = "hi-dhl", age = 10)
val p2 = Person1(name = "hi-dhl", age = 10)
println(p1 == p2) // false
println(p1.equals(p2)) // false
println(p1 === p2) // false

println(p1.name == p2.name) // true
println(p1.name.equals(p2.name)) // true
println(p1.name === p2.name) // true

因为普通的类 Person1 并没有实现 equals 方法,所以使用操作符 == 和 equals 比较的结果为 false,而 p1 和 p2 是不同的对象所以操作符 === 的结果为 false。

参数 name 是 String 类型,在上文分析过了 String 重写了 equals 方法,操作符 == 和 equals 比较的结果为 true。而 p1.name === p2.name 结果为 true , 是因为会先去常量池中查找是否存在 "hi-dhl",如果存在直接返回常量池中的引用。

数据类

最后我们在来看一下这三种操作符在数据类中的表现。

data class Person2(val name: String, val age: Int)

val p3 = Person2(name = "ByteCode", age = 10)
val p4 = Person2(name = "ByteCode", age = 10)
println(p3 == p4) // true
println(p3.equals(p4)) // true
println(p3 === p4) // false

println(p3.name == p4.name) // true
println(p3.name.equals(p4.name))// true
println(p3.name === p4.name) // true

因为编译器会根据数据类中的参数,自动生成 equals 、 hashCode 、 toString 等等方法,编译后的代码如下所示。

public int hashCode() {
String var10000 = this.name;
return (var10000 != null ? var10000.hashCode() : 0) * 31 + Integer.hashCode(this.age);
}

public boolean equals(@Nullable Object var1) {
if (this != var1) {
if (var1 instanceof Person2) {
Person2 var2 = (Person2)var1;
if (Intrinsics.areEqual(this.name, var2.name) && this.age == var2.age) {
return true;
}
}

return false;
} else {
return true;
}
}

所以使用操作符 == 和 equals,输出结果为 true,但是 p3 和 p4 是不同的对象所以操作符 === 的结果为 false。

总结

Java 中的操作符

操作符 ==

  • 如果是基本数据类型比较的是值
  • 如果是引用数据类型比较的是地址

操作符 equals

  • 默认情况下在不重写 equals 方法时,等价于 ==,比较的是地址
  • 重写 equals 方法时,常用于比较结构是否相等,可以通过快捷键自动生成 equals() 、hashCode() 、 toString() 等等方法。
    • Mac: Cmd + N
    • Win/Linux: Alt+Insert

Kotlin 中的操作符

Kotlin 提供了两种方式用于对象的比较。

  • 比较对象的结构是否相等( == 或者 equals )

    Kotlin 中的操作符 == 等价于 equals 用于比较对象的结构是否相等, 很多情况下使用的是 ==,因为对于浮点类型 Float 和 Double,其实现方法 equals 不遵循 IEEE 754 浮点运算标准。

  • 比较对象的引用是否相等 ( === )

    Kotlin 中的操作符 === 用于比较对象的引用是否指向同一个对象,运行时如果是基本数据类型 === 等价于 ==

全文到这里就结束了,最后附上文章的精简示例,你能够在不运行程序的情况下,说出下面代码的运行结果吗?

class Person1(val name: String, val age: Int)
data class Person2(val name: String, val age: Int)

fun main() {
val a1 = -0
val a2 = 0
println(a1 == a2)
println(a1.equals(a2)

val a3 = -0f
val a4 = 0f
println(a3 == a4)
println(a3.equals(a4))

//-------------

val p1 = Person1(name = "hi-dhl", age = 10)
val p2 = Person1(name = "hi-dhl", age = 10)
println(p1 == p2)
println(p1.equals(p2))
println(p1 === p2)
println(p1.name === p2.name)

//-------------

val p3 = Person2(name = "ByteCode", age = 10)
val p4 = Person2(name = "ByteCode", age = 10)
println(p3 == p4)
println(p3.equals(p4))
println(p3 === p4)
println(p3.name === p4.name)

}

运行结果如下所示:

a1 == a2        true
a1.equals(a2) true
a3 == a4 true
a3.equals(a4) false
--------------------------
p1 == p2 false
p1.equals(p2) false
p1 === p2 false
p1.name === p2.name true
--------------------------
p3 == p4 true
p3.equals(p4) true
p3 === p4) false
p3.name === p4.name true

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

leetcode-零钱兑换

周末一直在下雨,甚至看天气预报,年前就一直是这样的天气了。不过这样的天气也有好处,反正哪儿也去不了,就在家看看书,也算是难得精心。 题目 给你一个整数数组 coins ,表示不同面额的硬币;以及一个整数 amount ,表示总金额。 计算并返回可以凑成总金额所...
继续阅读 »

周末一直在下雨,甚至看天气预报,年前就一直是这样的天气了。不过这样的天气也有好处,反正哪儿也去不了,就在家看看书,也算是难得精心。


题目


给你一个整数数组 coins ,表示不同面额的硬币;以及一个整数 amount ,表示总金额。
计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回 -1 。
你可以认为每种硬币的数量是无限的。



示例 1:

输入:coins = [1, 2, 5], amount = 11

输出: 3

解释: 11 = 5 + 5 + 1




示例 2:

输入:coins = [2], amount = 3

输出: -1




示例 3:

输入:coins = [1], amount = 0

输出: 0




示例 4:

输入:coins = [1], amount = 11

输出: 1




示例 5:

输入:coins = [1], amount = 2

输出: 2



思路


这也是一个背包问题,要求刚好把背包装满,选用尽量少的件数。

定义一个一维数组dp,dp[n]代表总金额为n的情况下,最少的硬币个数,如果无法刚好凑成n,那么可以用一个特殊的固定值。dp[n] = min(dp[n-coin[k]]) + 1

怎么理解呢?

我们可以这么考虑:刚好组成总金额n的硬币中,可能包含coin[0]~coin[len-1]各有ci枚,当然,ci可以是0。如果ci > 0,那么我们可以先去掉这一枚,这样,dp[n] = dp[n-coin[k]] + 1;因为有len枚硬币,这里就可能存在len种情况,所以,dp[n] = min(dp[n-coin[0]]+1...dp[n-coin[len-1]]+1),整理一下,就得到了上面的状态转移方程。

边界条件,amount为0的时候,我们可以不用任何1枚硬币,所以dp[0] = 0。

另外,对于n无法刚好凑成的情况,本来我们可以初始化dp[n] = Integer.MAX_VALUE,由于有+1这个操作,会导致溢出,所以我们选择把dp[n]初始化成amount+1,因为面值是整数,至少是1,所以对于能凑成的n,硬币个数一定小于amount+1,等于amount+1的,就代表无法凑成。


Java版本代码


class Solution {
public int coinChange(int[] coins, int amount) {
int[] dp = new int[amount+1];
Arrays.fill(dp, amount + 1);
dp[0] = 0;
for (int i = 1; i <= amount; i++) {
for (int coin : coins) {
if (i >= coin) {
dp[i] = Integer.min(dp[i], dp[i-coin] + 1);
}
}
}
if (dp[amount] > amount) {
return -1;
}
return dp[amount];
}
}

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

不知不觉到了 Hero 动画

其实在我们的开发过程中,我们可能已经看见过 Hero 动画了,比如像电商类 App 的一个典型场景,商品列表页到商品详情页,列表页的缩略图需要带到详情页,并且带的过程中可能有大小,位置等的变化。在 Flutter 中,这种页面简直共有元素的动画就叫做 Hero...
继续阅读 »

其实在我们的开发过程中,我们可能已经看见过 Hero 动画了,比如像电商类 App 的一个典型场景,商品列表页到商品详情页,列表页的缩略图需要带到详情页,并且带的过程中可能有大小,位置等的变化。在 Flutter 中,这种页面简直共有元素的动画就叫做 Hero 动画 ,有的时候也叫做 共享元素动画


Flutter 官方的每周组件也介绍了 Hero 组件:👉 Hero 组件介绍


本文就演示如何构建标准的 Hero 动画,以及在页面过渡过程中将图像从圆形转换为方形的 Hero 动画。


可以使用 Hero 组件来创建这种动画。随着Hero动画从源路由到目标路由,目标路由也会在这一过程中淡入到视图上。一般来说,Hero 组件是两个页面 UI 的一部分,比如图片等等。从用户体验的角度来说,Hero 组件是从源路由飞到了目标路由。我们就用代码实现下面的 Hero 效果。


Standard hero animations


标准的 Hero 动画是 Hero 元素从一个页面到另一个页面,并且一般情况下位置和尺寸会有变化。比如是这样的:


standard (1).gif


第一个页面图片是在中间的,到了第二个蓝色页面,图片的位置和大小都发生了变化。从第二个页面到第一个页面,图片又还原到最初的样子。


Radial hero animations


radial hero 动画中, 随着页面的过渡,Hero的形状会发生变化从圆形到矩形。比如下面的效果:


radial (1).gif


上面的效果就是一个radial hero 动画,底部的三个元素,依次展示到第二个页面的中间,并且形状从圆形到矩形。从第二个页面回到第一个页面,图片元素还原到最初的样子。


Hero 动画的基本结构




  • 在不同的 Route 声明两个 Hero 组件,两个 Hero 组件的 tag 要一致。

  • Navigator 管理应用的路由栈

  • 路由的 Push 或者 Pop 触发 Hero 动画

  • 边框效果是由 RectTween 实现的,从源路由到目标路由的过程中,这个效果值会变化。也许你可能会有疑问,为啥第二个路由还没显示呢,作为页面的一部分的 Hero 却可以显示? 因为在过渡期间,Hero 是放在应用的 Overlay 上的,所以它才可以显示在所有的 Route 上。



Hero 动画是由两个 Hero 组件实现的,一个在源路由中,一个在目标路由中。虽然从用户体验的角度,两个 UI 是共享的,只是样子变化了。这都不重要,因为只需要我们程序知道怎么计算的就可以了😭。


这里注意一点,Hero 动画不能加到 Dialog 上



Hero 动画主要是下面几部分:




  1. 在源路由定义一个 Hero 组件,这个组件叫做 源 hero,需要给 源hero 设置两个参数,待添加动画的组件,比如图片等等,和动画的唯一标示 tag




  2. 在目标路由定义一个 Hero 组件,这个组件叫做目标 hero,这个目标Hero需要和源Hero的tag一样,这也是最重要的一点,并且目标Hero也需要包裹一个带添加动画的组件。为了动画的效果达到最佳,目标Hero源Hero包裹的内容最好一样




  3. 创建一个包含 目标Hero 的路由,路由定义的树会在动画结束时渲染出来




  4. Navigator 的 push 或者 pop 操作会触发 Hero 动画,会去匹配 Hero 动画的 tag





Flutter 会计算 Hero 动画从开始到结束的补间,补间就是效果比如尺寸大小和位置摆放。真正承载动画效果的是在 overlay 中,而不是源或者目标路由中。


幕后工作


下面我们就介绍 Flutter 是怎么执行 Hero 的。


image.png


在执行动画之前,源 Hero 在 源路由的 Widget 树上。目标路由还不存在,Overlay 也是空的。


image.png


我们使用 Navigator Push 一个 路由,就会触发动画的执行。在动画开始的时刻,也就是 t=0.0, Flutter 就会执行下面的动作:




  • 现在 Flutter 已经知道 Hero 动画到哪里停止,它会计算 Hero 动画的路径,动画的效果是 Material 运动的设计规范,这里注意一点,动画是不依附任何页面的




  • 把 目标Hero 和 源Hero 都放在 Overlay 上,他们的大小和尺寸都是我们给他设置的。在Overlay 上进行动画效果,所以可以在页面之上显示效果




  • 页面之上进行动画




image.png


当 Hero 动画移动的时候,边框效果使用 Tween ,具体的实现是 Hero 刻的 createRectTween 方法。默认情况下,Flutter 使用的 MaterialRectArcTween 效果。


image.png


动画完成之后:



  • Flutter 会把 Overlay 上的目标Hero,移动到目标路由(页面)上,Overlay 就是空的了。

  • 目标Hero 就出现在了页面上最终的位置

  • 源Hero就存储在了页面上




Push 页面 Hero 动画会前进,Pop 页面会让 Hero 动画反向执行。


关键类


Hero 动画的实现需要使用到下面的类:




  • Hero 是一个动画组件,会让子组件从源路由动画到目标路由,使用的时候需要指定相同的tag属性。Flutter 会用 tag 匹对 Hero。




  • Inkwell 用于手势识别,onTap() 的执行的时候 push 个新的页面,触发 Hero 动画。




  • Navigator 管理路由栈,可以 Push 或者 Pop




  • Route 承载一个页面,一般情况下,一个 Route 代表了 一个页面。大多数应用都是多路由的。




标准的 Hero 动画


关键点




  • 使用 MaterialPageRoute、 CupertinoPageRoute、 自定义 PageRouteBuilder 指定路由,案例用的是 MaterialPageRoute




  • 使用 SizedBox 组件包裹 Image 组件,实现页面切换时,尺寸动画的效果




  • 把图片组件放在目标页面的 Widget 树上,源页面和目标页面的 Widget 树不同,Image 组件在树中的位置也不同。




继续写代码


从一个页面到另一页面的动画可以使用 Flutter 的 Hero 组件,如果目标路由是 MaterialPageRoute ,那么动画的效果会使用 👉Material的效果


Create a new Flutter example  使用代码👉 hero_animation.


按着下面的步骤运行:




  • 点击主页页面的图片,会打开新页面,新页面会呈现一个不同尺寸和位置的图片




  • 点击图像或者物理返回会返回到前一个路由




  • 可以使用 timeDilation 属性让动画的速度降下来




PhotoHero 类


自定义的 PhotoHero 类维护这个 Hero、尺寸、图片和点击的行为,代码如下:


class PhotoHero extends StatelessWidget {
const PhotoHero({ Key key, this.photo, this.onTap, this.width }) : super(key: key);

final String photo;
final VoidCallback onTap;
final double width;

Widget build(BuildContext context) {
return SizedBox(
width: width,
child: Hero(
tag: photo,
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: onTap,
child: Image.asset(
photo,
fit: BoxFit.contain,
),
),
),
),
);
}
}

代码的关键信息:



  • InkWell 包裹了 Image 组件,让源路由和目标路由的手势添加变得简单了。

  • 代码中的 MaterialColors.transparent 的效果是,当图片动画到目的地之后,图像可以从背景中 “pop out” (弹出来)。

  • SizedBox 的含义是指定Hero的大小

  • Image 的 fit 属性是为了让图片在容器内尽可能大,这个尽可能大是指不改变宽高比。可以看这里👉图文组件


PhotoHero 的树结构是:


photohero-class.png


HeroAnimation 类


PhotoHero 类是显示类,HeroAnimation 类是动画类,这个类创建了源路由和目标路由,并且关联了动画。


代码如下:


class HeroAnimation extends StatelessWidget {
Widget build(BuildContext context) {
timeDilation = 5.0; // 1.0 means normal animation speed.

return Scaffold(
appBar: AppBar(
title: const Text('Basic Hero Animation'),
),
body: Center(
child: PhotoHero(
photo: 'images/flippers-alpha.png',
width: 300.0,
onTap: () {
Navigator.of(context).push(MaterialPageRoute(
builder: (BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Flippers Page'),
),
body: Container(
// The blue background emphasizes that it's a new route.
color: Colors.lightBlueAccent,
padding: const EdgeInsets.all(16.0),
alignment: Alignment.topLeft,
child: PhotoHero(
photo: 'images/flippers-alpha.png',
width: 100.0,
onTap: () {
Navigator.of(context).pop();
},
),
),
);
}
));
},
),
),
);
}
}

关键信息:




  • 用户点击图片创建一个 MaterialPageRoute 的路由,并且使用 Navigator 把路由添加到栈中




  • Container 容器让 PhotoHero 放置在页面的左上角,当然在 AppBar 的下面




  • onTap() 触发页面切换和动画




  • timeDilation 属性让动画变慢了




动画的效果:
standard (1).gif




Radial hero animations


关键点




  • radial 效果是把圆形的边框动画成方形边框




  • 从源路由到目标路由,Hero 执行径向的转换。




  • MaterialRectCenterArcTween 定义了径向效果




  • 使用 PageRouteBuilder 定义目标路由




进行页面跳转的同时进行形状的变化,会让动画更加的流畅。为了实现这一效果,代码会动画两个形状的交集:圆形和正方形。在整个动画过程中,圆形的裁剪从 minRadius 到 maxRadius,方形的裁剪始终保持同一个大小。同时,图片也从源路由动画到目标路由的指定位置。


动画可能看起来很复杂(确实很复杂),但开发者可以根据需要定制所提供的示例。一般性的代码已经完成了。


继续写代码


下面的算法展示了图片的裁剪过程,从开始的(t = 0.0)到结束的(t = 1.0)。


Radial transformation from beginning to end


蓝色的渐变代表图片,表示裁剪形状的交点。在动画的开始,相交的结果是一个圆形。在动画过程中,ClipOvalminRadius 缩放到 maxRadius,而 ClipRect 保持恒定的大小。在动画的最后,圆形和矩形的交集会生成一个矩形,这个矩形与 Hero 组件的大小相同。也就是说,在动画结束时,图像不再被裁剪。


动画的代码在这里👉radial_hero_animation


按着下面的步骤操作:




  • 点击三个圆形缩略图中的一个,使图像动画到一个更大的正方形,正方形在目标路由的中间




  • 点击图像返回到上一个源路由,也可以物理返回




  • 使用 timeDilation 属性慢放动画




Photo class


The Photo class builds the widget tree that holds the image:


content_copy


class Photo extends StatelessWidget {
Photo({ Key key, this.photo, this.color, this.onTap }) : super(key: key);

final String photo;
final Color color;
final VoidCallback onTap;

Widget build(BuildContext context) {
return Material(
// Slightly opaque color appears where the image has transparency.
color: Theme.of(context).primaryColor.withOpacity(0.25),
child: InkWell(
onTap: onTap,
child: Image.asset(
photo,
fit: BoxFit.contain,
)
),
);
}
}

关键点:




  • Inkwell 组件捕捉点击事件,执行的动作是构造方法传进来的回调




  • 动画期间,InkWell 会使用第一个 Material 祖先节点的效果,比如水波纹等等




  • Material 组件有一个稍微不透明的背景色,这样即使是图片透明的部分也会有一个背景色。确保了圆形到方形的过渡很容易被看到。




  • Photo 类中没有包含 Hero 组件,为了让动画生效,Hero 包装了 RadialExpansion 组件。




RadialExpansion class


RadialExpansion 组件是 Demo 的核心,构建了 裁剪图片的 Widget树。裁剪的形状是圆形和矩形的交集,圆形是随着动画正向变大,反向变小的,矩形的大小是不变的。


代码如下:


class RadialExpansion extends StatelessWidget {
RadialExpansion({
Key key,
this.maxRadius,
this.child,
}) : clipRectSize = 2.0 * (maxRadius / math.sqrt2),
super(key: key);

final double maxRadius;
final clipRectSize;
final Widget child;

@override
Widget build(BuildContext context) {
return ClipOval(
child: Center(
child: SizedBox(
width: clipRectSize,
height: clipRectSize,
child: ClipRect(
child: child, // Photo
),
),
),
);
}
}

上面代码形成的节点树:


radial-expansion-class.png


关键点:




  • Hero 组件包裹了 RadialExpansion 组件




  • 在动画的过程中,它的尺寸和 RadialExpansion 的尺寸都会改变




  • RadialExpansion 动画是被两个重叠的裁剪组件创建的




  • 案例使用 MaterialRectCenterArcTween 定义了补间的插值,默认的动画路径使用 Hero 角度的计算值(sqrt)来进行插值。这种方法会在径向变化期间会影响Hero的长宽比。因此径向动画使用 MaterialRectCenterArcTween 来使用 Hero的中心点和角度计算进行差值。




代码如下:


   static RectTween _createRectTween(Rect begin, Rect end) {
return MaterialRectCenterArcTween(begin: begin, end: end);
}

完成的代码是这样的:


import 'dart:math' as math;

import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart' show timeDilation;

class Photo extends StatelessWidget {
const Photo({Key? key, required this.photo, this.onTap}) : super(key: key);

final String photo;
final VoidCallback? onTap;

@override
Widget build(BuildContext context) {
return Material(
// Slightly opaque color appears where the image has transparency.
color: Theme.of(context).primaryColor.withOpacity(0.25),
child: InkWell(
onTap: onTap,
child: LayoutBuilder(
builder: (BuildContext context, BoxConstraints size) {
return Image.asset(
photo,
fit: BoxFit.contain,
);
},
),
),
);
}
}

class RadialExpansion extends StatelessWidget {
const RadialExpansion({
Key? key,
required this.maxRadius,
this.child,
}) : clipRectSize = 2.0 * (maxRadius / math.sqrt2),
super(key: key);

final double maxRadius;
final double clipRectSize;
final Widget? child;

@override
Widget build(BuildContext context) {
return ClipOval(
child: Center(
child: SizedBox(
width: clipRectSize,
height: clipRectSize,
child: ClipRect(
child: child,
),
),
),
);
}
}

class RadialExpansionDemo extends StatelessWidget {
const RadialExpansionDemo({Key? key}) : super(key: key);

static double kMinRadius = 32.0;
static double kMaxRadius = 128.0;
static Interval opacityCurve =
const Interval(0.0, 0.75, curve: Curves.fastOutSlowIn);

static RectTween _createRectTween(Rect? begin, Rect? end) {
return MaterialRectCenterArcTween(begin: begin, end: end);
}

static Widget _buildPage(
BuildContext context, String imageName, String description) {
return Container(
color: Theme.of(context).canvasColor,
child: Center(
child: Card(
elevation: 8.0,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(
width: kMaxRadius * 2.0,
height: kMaxRadius * 2.0,
child: Hero(
createRectTween: _createRectTween,
tag: imageName,
child: RadialExpansion(
maxRadius: kMaxRadius,
child: Photo(
photo: imageName,
onTap: () {
Navigator.of(context).pop();
},
),
),
),
),
Text(
description,
style: const TextStyle(fontWeight: FontWeight.bold),
textScaleFactor: 3.0,
),
const SizedBox(height: 16.0),
],
),
),
),
);
}

Widget _buildHero(
BuildContext context, String imageName, String description) {
return SizedBox(
width: kMinRadius * 2.0,
height: kMinRadius * 2.0,
child: Hero(
createRectTween: _createRectTween,
tag: imageName,
child: RadialExpansion(
maxRadius: kMaxRadius,
child: Photo(
photo: imageName,
onTap: () {
Navigator.of(context).push(
PageRouteBuilder(
pageBuilder: (BuildContext context,
Animation animation,
Animation secondaryAnimation) {
return AnimatedBuilder(
animation: animation,
builder: (BuildContext context, Widget? child) {
return Opacity(
opacity: opacityCurve.transform(animation.value),
child: _buildPage(context, imageName, description),
);
});
},
),
);
},
),
),
),
);
}

@override
Widget build(BuildContext context) {
timeDilation = 5.0; // 1.0 is normal animation speed.

return Scaffold(
appBar: AppBar(
title: const Text('Radial Transition Demo'),
),
body: Container(
padding: const EdgeInsets.all(32.0),
alignment: FractionalOffset.bottomLeft,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
_buildHero(context, 'images/chair-alpha.png', 'Chair'),
_buildHero(context, 'images/binoculars-alpha.png', 'Binoculars'),
_buildHero(context, 'images/beachball-alpha.png', 'Beach ball'),
],
),
),
);
}
}

void main() {
runApp(
const MaterialApp(
home: RadialExpansionDemo(),
),
);
}

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

每个 Flutter 开发者都应该知道的框架总览

本篇文章翻译自官方的👉总览文章,这篇文章基本上把 Flutter 介绍清楚了,如果想从总体上知道 Flutter 是咋回事,本篇文章是最好的教程了。 以下是正文 本文旨在从高层级提供一个 Flutter 框架结构的总体概览,介绍一些其设计上的核心原则和概念。...
继续阅读 »

本篇文章翻译自官方的👉总览文章,这篇文章基本上把 Flutter 介绍清楚了,如果想从总体上知道 Flutter 是咋回事,本篇文章是最好的教程了。


以下是正文




本文旨在从高层级提供一个 Flutter 框架结构的总体概览,介绍一些其设计上的核心原则和概念。


Flutter 是一个跨平台的 UI 工具包,目的是一份代码可以运行在不同的操作系统上,比如 Android、IOS等等,同时也可以让应用直接和底层的平台服务交互。我们的目标是:尽量用一份代码,开发者就可以在不用的平台上开发出高性能、高保真的 APP。拥抱差异,更少代码,更高性能


在开发阶段,Flutter 运行在虚拟机上,虚拟机提供了 hot reload 的功能,可以加载每次开发者改动的差异代码,而不需要全代码的编译。在正式版本上,Flutter 应用是直接编译成了机器码:Intel x64、ARM、JavaScript等等。这里说的开发阶段和正式版本是指 Flutter 产物的模式,Flutter 的产物有三种模式 debug、release、profile。Flutter 的 framework 是开源的,开源的协议是 BSD 协议,并且有活跃繁荣的第三方库社区,这些优秀的第三方库很好的补充了 Flutter 的能力。


本文的总览分为以下几个部分:



  1. 分层模型: Flutter 的组成部分

  2. 响应式 UI : Flutter UI 开发的核心概念

  3. Widgets 介绍: Flutter UI 代码构建的基础

  4. 渲染流程: Flutter 是如何将 UI 代码转化为屏幕像素点的

  5. 平台嵌入 的总览: 让移动端和桌面系统运行 Flutter 应用

  6. 用其他代码集成 Flutter: 介绍 Flutter 可用的不同的技术信息

  7. Web 的支持: 总结 Flutter 在浏览器环境中的特点


框架分层


从设计上来看,Flutter 框架是可扩展的、分层的。Flutter 由一系列的单独的依赖包组成,而且这些依赖包依赖底层。上层没有权限访问下层,并且框架层的每个部分都是可插拔的。


image.png


对于底层的操作系统来说,Flutter 应用被打成的应用包和其他的 Native 应用是一样。平台特定的嵌入层提供了一个入口:协调底层操作系统来访问一些底层的服务,比如渲染桌面、可访问性、输入等,管理事件循环。这个嵌入层是被特定的平台语言开发的,Android 系统是 Java 和 C++,iOS 和 macOS 系统是 Objective-C/Objective-C++,Windows 和 Linux 系统是 C++。正是由于这一层的存在,Flutter 代码可以集成进已经存在的应用,也可以直接使用 Flutter 代码打包整个应用。Flutter 为通用的平台提供了一些嵌入器,其他的嵌入器也是存在的


Flutter 的核心是 Flutter engine,engine 是 C++ 开发的,并且 Flutter 应用提供最
原始的支持,比如协议、通道等等。当新的一帧需要被绘制的时候,Flutter engine 就会栅格化代码合成的绘制信息。Engine 也为上层封装了访问底层的 API:图形图像化、文本布局、文件和网络 I/O、访问性支持、插件架构、Dart运行时、Dart编译工具链等等。


Flutter engine 暴漏是通过 dart:ui 这个库来暴漏给上一层的,这个库用 Dart 类包装了底层的 C++ 代码。像上面说的 engine 的功能,这个库包含了驱动输入、图形化、文本渲染系统等功能。


Typically, developers interact with Flutter through the Flutter framework, which provides a modern, reactive framework written in the Dart language. It includes a rich set of platform, layout, and foundational libraries, composed of a series of layers. Working from the bottom to the top, we have:
一般来说,开发者通过 Flutter framework 来和 Flutter 交互,这一层是 Dart 代码,提供了现代的、响应式的 Flutter 框架。这一层包括了和平台、布局、基础相关的库,并且也是分层的,自底向上以次有:




  • 必要的基础类以及常用的底层代码块的抽象,比如动画绘制手势




  • 处理布局的rendering layer,在这一层,可以构建一棵渲染对象的节点树,你也可以动态的操作这些节点,那么布局就会自动响应你的改变。




  • 合成抽象的 widgets layer,渲染层的每一个渲染对象在 Widget 层都会有一个 Widget 对象与之对应。另外,在这一层开发者也可以定义一些可以复用的组合类,就是这这一层引入了响应式框架。




  • [Material] 和 [Cupertino] 库, 提供了全套的 Material和 iOS 风格的原始组件。




Flutter 框架是相对来说比较小的,一些开发者用到的高级功能大多是以包的形式实现的,比如像 camerawebview 这样的平台插件,像 charactershttpanimations 这样的平台无关的包,平台无关的包可以完全依赖 Dart 和 Flutter依赖。这些高级包有一些是生态共建的,比如支付、苹果证书、动画等等。


下面就从响应式 UI 编程以此向下层展开描述。主要内容有,介绍 Widget 是怎么结合到一起的,Widget 是怎么转化为渲染对象的,介绍 Flutter 是怎么集成以及互操作平台代码的,最后简要总结Flutter 的 Web支持。


响应式 UI


总体上来说,Flutter 是一个响应式的非声明式的UI 框架,这意味着开发者仅仅需要提供程序状态与程序 UI 的映射,框架会在应用状态改变的时候自动完成 UI 的刷新。这种设计的灵感得益于 Facebook 的 React 框架,React 框架对很多传统的设计原则进行了重新思考。


在大多数传统的 UI 框架中,UI 的初始化状态只被描述一次,然后为了响应各种事件会单独的更新。这种方法的一个痛点是,随着应用复杂性的增长,开发者需要时刻注意状态的改变是怎么层叠地贯穿整个 UI 的。比如,考虑下面的 UI:


image.png


上面有许多状态可以改变的地方:Color box、色带 Slider、Radio按钮等等。只要用户和 UI 交互,那么改变必须被响应到每一个地方。更麻烦的是,页面一个很小的改动,比如拖动一下色带,可能会导致一系列连锁的反应,进而影响到很多看似不相干的代码。比如色带的拖动,文本框里面也要改变。


一种解决的方案是像 MVC 这样的代码开发架构,通过 controller 将数据的改变推送到 model,然后,model 通过 controller 将新的状态 推送给 view。但是,这样的方式其实也是有瑕疵的,因为创建和更新 UI 元素是两个单独的步骤,可能会导致不同步。


沿着其他响应式框架的脚步👣,Flutter 通过 UI 与底层状态彻底的解耦来解决这个问题。在响应式的 API 开发背景下,开发者仅仅创建 UI 的描述,framework 会在运行时使用我们的描述创建或者更新界面。


在 Flutter 中,我们所说的组件是 Widget,并且 Widget 是不可变的,可以形成 Widget 树形结构。这些组件用于管理独立的布局对象树,布局树用与管理独立的合成对象树。Widget 树到布局树再到合成树。Flutter的核心就是,确保可以有效的修改树中部分节点:把上层树转化成低层级的树(Widget到布局),并且在这些树上传递改变。


在Flutter中,小部件(类似于React中的组件)由用于配置对象树的不可变类表示。这些小部件用于管理用于布局的独立对象树,然后用于管理用于合成的独立对象树。Flutter的核心是一系列机制,可以有效地在树的修改部分行走,将对象树转换为较低级的对象树,并在这些树之间传播变化。


开发者需要在 Widget 的 build() 方法中将状态转化为 UI:


UI = f(state)

在 Flutter 设计中,build() 方法执行起来会很快,并且没啥副作用,framework 会在需要调用的时候调用它。


这种响应式的框架需要一些特定的语言特征(对象快速实例化和删除),而 Dart 就很符合干这件事


Widgets


正如前面所提,Flutter 着重强调 Widget 是合成的一个单元。Flutter 应用的 UI 就是 Widget 构建块,并且每一个 Widget 都是一份不可变的描述。


Widget 在组合的基础上形成一个体系结构。每一个 Widget 都嵌套在它的父节点里面,并且从父节点接受上下文。这种结构一直延伸到根 Widget,像下面的简单代码:


import 'package:flutter/material.dart';

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);

@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: const Text('My Home Page'),
),
body: Center(
child: Builder(
builder: (BuildContext context)
{
return Column(
children: [
const Text('Hello World'),
const SizedBox(height: 20),
ElevatedButton(
onPressed: ()
{
print('Click!');
},
child: const Text('A button'),
),
],
)
;
},
),
),
),
);

在这个代码中,所有的类都是 Widget。


用户交互的时候会生成事件,App会更新 UI 来响应事件。更新的方式是告诉 framework 用另一个 Widget 来替换 Widget。framework 就会比较 新的和旧的 Widget,并且有效的更新 UI。


Flutter 有其自己的实现机制来控制 UI,而不是按照系统所提供的方式:比如说,这段代码是iOS Switch controlAndroid control纯 Dart 的实现。


这种方式有以下几点好处:




  • 无限的可扩展性。比如开发者想要一个 Switch 组件,那么可以用任意的方式来创建,不需要局限于操作系统所提供的




  • 避免性能瓶颈。这种方式运行 Flutter 一次就合成整个屏幕信息,而不需要在 Flutter 代码和 平台代码之间来回切换




  • 将应用的执行和操作系统的依赖解耦。Flutter 应用在操作系统的所有版本上运行的效果是一样的,即使操作系统改变了他自己的一些控件实现。




组合先与继承


Widget 通常由许多更小的 、用途更单一的 Widget 组成,组合起来的 Widget 往往可以产生更加有力的效果。


理想的效果,设计上概念的数量尽可能少,然而实际的总量表要尽可能大。比如说,在 Widget 层,概念只有一个,那就是 Widget,表示屏幕绘制、布局、用户交互、状态管理、主题定制、动画、路由导航等等。在动画层,只有两个概念:Animation 和 Tween。在渲染层,只有一个概念 RenderObject,用于描述布局、绘制、点击、可访问。而这些层级中,每一层都有大量的具体实现来具化概念,比如有几百个 widget 和 render对象,几十个动画和插值类型。


Flutter 有意地将类的继承体系设计的浅而宽,目的是最大化组合的数量。每一个小粒度的 可组合的Widget尽量聚焦做好自己的功能。核心的功能是抽象的,即使是像间距、对齐这样的基础功能,也被设计成具体的 Widget,而不是把这些功能添加到基础的 Widget 中。因此,假如我们想要居中一个组件,不是添加 Align 属性,而是使用 Center 组件包裹。


间距、对齐、横向排列、竖向排列,表格等等都是 Widget,布局类型的 Widget 并没有它自己本身可视的样子。但是呢,它们就是控制其他组件的布局。Flutter 也包括了一些功能性组件,这些功能组件也利用这种组合的方法。


比如,Container 是非常常用的组件,它本身是有负责布局、绘制、定位、尺寸的几个小 Widget 组成,具体来说,Container 由 LimitedBoxConstrainedBoxAlignPaddingDecoratedBoxTransform 组成。Flutter 的一个很明显的特征是,我们可以深入源码,去看去检查源码。因此,我们不需要泛化一个 Container 来实现自定义的效果,我们可以把它和另外一些 Widget 进行组合,或者参考 Container 写一个新的 Widget。


构建 Widget


正如前面提到的,build() 方法返回的内容就是页面上显示的内容,返回的元素会形成一颗 Widget 树,这个树以更具体的方式来表示 UI 的一部分。比如说,toolbar Widget 的 build 方法 构建了横向的布局,包括了 文本、按钮等等。根据需要,framework 会递归的 build 每一个 Widget 直到 Widget 树可以被更具化的渲染对象完全描述下来。framework 会将渲染对象拼合成一颗渲染树。


Widget 的 build 方法应该是无副作用的。只要方法被调用了,那么不管旧的 Widget 树是什么,一颗新的 Widget 树都会被创建。framework 做了大量的工作,来决定哪一个 Widget 的 build 方法需要被执行。具体的过程可以参考Inside Flutter topic


在每一个渲染帧,Flutter 仅仅会再次创建 UI 中需要创建的部分,这一部分就是状态变化的地方,创建的方式是执行 build 方法。所以,build 方法耗时应该非常小。一些比较重的计算应该放在异步中,将计算的结果作为状态的一部分,build 方法只是用数据。


虽然这种方式相对有点直白,但是这种自动比较的方式很高效,能够保正高性能、高保真。而且,build 方法的这种设计可以简化代码,让开发者聚焦在 Widget 的声明上,脱离状态与 UI 复杂的交互。


Widget state


框架里面有两个最主要的 Widget 类:StatefulWidget 和 StatelessWidget


许多 Widget 都是无状态的:它们的属性不随着时间改变。这样的 Widget 是 StatelessWidget 的子类。


但是呢,如果 Widget 的某个特征需要根据用户交互或者其他因素发生改变,那么这种 Widget 是 StatefulWidget。比如说,如果一个 Widget 有一个计数器,当用户点击按钮的时候,计数器需要变化,那么计数器就是 Widget 的 State。当值改变的时候,Widget 需要被重新构建来更新部分 UI(显示数字的那部分)。这样的 Widget 就是 StatefulWidget,因为 Widget 本身是不可变的,所以把状态存储在 可变的 State 子类中。StatefulWidget 没有 build 方法,相反,它的 UI 构建放到了 State 对象中。


只要想要改变 State 对象的状态,那么就调用 setState() 方法来告诉 framework : 你应该调用我的 build 方法来更新 UI 来。


虽然 StatefulWidget 既有 State 对象又有Widget 对象,但是其他 Widget 可以像使用 StatelessWidget 一样使用 StatefulWidget,担心状态丢失等问题。父节点在需要的时候可以随时创建子组件,不需要保留前一个 state 对象,framework 做了查找和重用状态对象的所有工作。


状态管理


因此,如果保持状态的 Widget 非常的多,那么状态是怎么管理的呢?是怎么更好的在应用内传递呢?


像其他的类一样,开发者可以在 Widget 构造方法中初始化它的数据, build() 方法可以确保其用的数据已经初始化了:


@override
Widget build(BuildContext context)
{
return ContentWidget(importantState);
}

随着节点树越来越深,状态的向上向下查找就变的十分糟糕了。因此,另一种类型的 Widget —— InheritedWidget 就应运而生了。这种类型的 Widget 提供了一个很容易的方式来获取祖先节点的数据。可以使用 InheritedWidget 来创建一个 StatefulWidget 祖先节点,就像下面一样:


image.png


Whenever one of the ExamWidget or ExamWidget objects needs data from StudentState, it can now access it with a command such as:
只要 ExamWidget 或者 ExamWidget 需要 StudentState 的数据,那么可以使用下面的方式:


final studentState = StudentState.of(context);

of(context) 从 context 开始向上查找,找到最近的指定类型的祖先节点。这里的类型是StudentStateInheritedWidget 也提供了一个 updateShouldNotify() 方法,这个方法决定了当状态改变的时候,是否来触发使用数据的子节点的更新重建。


Flutter 本身就广泛的使用 InheritedWidget 来共享状态,比如我们熟知的主题。MaterialAppbuild() 方法中插入了一个 theme 节点,并为 theme 填充了数据,这样 比theme 节点更深的节点就可以通过 .of() 来找到 theme 节点,并使用数据。比如:


Container(
color: Theme.of(context).secondaryHeaderColor,
child: Text(
'Text with a background color',
style: Theme.of(context).textTheme.headline6,
),
);

Navigator 也用了这种方式,我们经常使用 Navigator 的 of 方法来 push 或者 pop 路由。MediaQuery 也用这种方式让开发者可以很快捷的获取屏幕相关信息,尺寸、方向、密度、模式等等。


随着应用的增长,更加先进高级的状态管理方案更加符合生产环境的开发,可以减少 StatefulWidget 的使用。许多 Flutter 应用使用 provider 这样的开源库。前面提到 Flutter 的分层结构可以无限扩展,flutter_hooks 这个第三方库提供了另外一种将状态转为 UI 的方式。


渲染与布局


这一节主要描述渲染管线,渲染管线包括了几个重要的步骤,将 Widget 真正的转化为实际的绘制像素。


Flutter 渲染模型


你可能会好奇:既然 Flutter 是一个跨平台的框架,它是怎么做到和单平台框架相当的性能效果呢?


我们先想一下传统的 Android app 是怎么运行的。当需要绘制的时候,开发者需要首先调用 Android 框架的 Java 代码。Android 系统提供的组件负责在 Canvas 对象中绘制,Android 使用 Skia 进行渲染。Skia 是 C/C++ 开发的图形库,会调用 CPU 或者 GPU 完成设备屏幕的绘制。


跨平台框架通常的做法是:在原生的 Android and iOS UI 层上创建一个抽象层,来尝试磨平每个平台的差异性。应用的代码一般是解释语言——JavaScript,必须和Java/Objective-C反复的交互来显示 UI。这些都增加了高额的负担,尤其是 UI 层和逻辑层有大量交互的时候。


Flutter 另辟蹊径,Flutter 最小化了这些抽象,避开系统提供的 UI,它自己有丰富的 Widget 库。绘制 Flutter 的 Dart 代码最终转为 native 代码,而这些 native 代码会使用 Skia 进行渲染。 Flutter 把 Skia 作为 引擎的一部分,这样开发者可以始终让应用保持到最新版本,而 Android 设备不需要更新。IOS等其他的设备也是相同的道理。


从用户输入到 GPU


Flutter 渲染管线的总原则是:简单就是快,Flutter 有一个简单明了的数据传输管道,沿着这个通道用户的输入流到了系统。正如下面所示:


image.png


下面我们来看更多的细节。


Build: 从 Widget 到 Element


Consider this code fragment that demonstrates a widget hierarchy:
思考一下下面的 Widget 体系代码片段:


Container(
color: Colors.blue,
child: Row(
children: [
Image.network('https://www.example.com/1.png'),
const Text('A'),
],
),
);

当 Flutter 需要渲染这个片段的时候,会调用 build 方法,返回了反应当前程序状态的 Widget 树,然后去渲染 UI。在这个过程中,build() 方法可能会构造新的 Widget。比如,前面的代码中,
Container 有 color 和 child 属性。但是在 Container 的源码中,如果 color 不是null,那么会插入一个代表颜色的 ColoredBox 组件:


if (color != null)
current = ColoredBox(color: color!, child: current);

同样地,Image 和 Text 组件也插入了 RawImage 和 RichText 组件在 build 过程中。所以最终的 Widget 树可能会比代码更深,比如:


image.png


这就解释了为啥我们在 Flutter inspector 看到的节点要远远深于我们的原始代码。


在 build 阶段,Flutter 会将 widget 树 转为 element 树,每一个 Element 都对应一个 Widget。每一个 Element 表示一个指定位置的特定 Widget 实例。有两个不同类型的 Element:



  • ComponentElement, 承载其他 Element 的 Element

  • RenderObjectElement, 参与布局和绘制阶段的 Element


image.png


RenderObjectElement 是它的 Widget 和 背后的 RenderObject 的中介,这个后面再说。


Widget 的 Element 可以通过 BuildContext 来引用到,同时 BuildContext 也表示树的位置信息。比如 Theme.of(context) 的参数就是 BuildContext,并且 BuildContext 是 build 方法的参数。


因为 Widget 是不变的,所以 Widget 树的任意改变都会导致一组新的 Widget 要被创建,即使是像 Text('A') 到 Text('B') 这样的变化。但是,Widget 的重新构建,并不意味着背后的 Element、RenderObject 也要重新构建。Element 树是持久化的在帧与帧之间,Flutter 的高性能很大一原因就是这个持久化的设计。Flutter 会缓存 Element等背后的对象,所以完全舍弃旧的Widget 也没啥问题。通过只遍历已经修改的 Widget,Flutter可以仅仅重建需要重建的 Element 树。


布局和渲染


仅仅绘制一个 Widget 的应用几乎是不存在的。因此,框架需要高效的布局 Widget 层次树,并且在渲染在屏幕上之前,也要高效的计算 Element 的尺寸,标定 Element 的位置。


渲染对象的基类是 RenderObject,这个类定义了布局和绘制的通用抽象模型,像多维、极坐标这样的需要自定义渲染模型。每个  RenderObject 知道它的父节点是谁,但是子节点的信息知道的很少,仅仅知道怎么去 visit 子节点和子节点布局约束。但是对于抽象来说这就够了,RenderObject 可以处理各种用例。


在 build 阶段,Flutter 会创建或者更新 element 树上每一个 RenderObjectElement 背后的 RenderObject 对象。
RenderObject 是原始的抽象类:RenderParagraph 渲染文本,RenderImage 渲染图像,RenderTransform 会在子节点绘制之前应用位置等信息。


image.png


大多数 Flutter Widget 背后的渲染对象是 RenderBox 的子类,RenderBox 将模型定义为盒子模型——固定大小的二维笛卡尔坐标。RenderBox 提供基本的 盒子约束,每一个 Widget 都放在由最大最小宽度、最大最小高度限制的盒子内。


为了执行布局过程,Flutter 向下👇传递约束。向上👆传递尺寸。父节点设置位置


image.png


布局的遍历完成之后,每个对象都有了符合父节点约束的尺寸,就会调用 paint() 方法进行绘制。


盒子约束模型非常棒,布局的过程时间复杂度仅是*O(n)*的:




  • 父节点可以将最大值和最小值设置为相同的,这样子节点的大小就是固定的了。比如说,最根部的节点就强制其子节点为屏幕大小。(子节点可以选择如何使用这一部分空间,比如,子节点可以在空间内居中摆放)




  • 父节点可以让设置子节点的宽度,但是让高度灵活可变。或者设置高度,让宽度灵活可变。比如文本组件,文本组件的宽度是灵活可变的,高度是固定的。




除了上述描述:子节点要根据可用空间的多少来展示自己的显示也是可行的。使用 LayoutBuilder 可以达到这样的效果,子节点检查父节点传进来的约束,然后使用约束的信息来展示自己的内容,比如:


Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
if (constraints.maxWidth < 600) {
return const OneColumnLayout();
} else {
return const TwoColumnLayout();
}
},
);
}

布局和约束更加详细的内容可以看这一篇文章👉深入理解Flutter布局约束


所有 RenderObject 的根节点是 RenderView,它就代表一颗渲染树。当平台决定绘制新的一帧时,就会调用 RenderViewcompositeFrame() 方法,方法内部创建了 SceneBuilder 去更新 scene。当 scene 准备完成之后,RenderView 对象会将合成的 scene 传递给
dart:ui 内的 Window.render() 方法,然后 GPU 就会渲染合成信息。


合成和光栅化阶段的更多细节可以参考 👉Flutter 渲染管线


Platform embedding


正如我们所示,Flutter 不像 React Native,把自己的组件转为 OS 的组件,让 OS 去渲染,它 是自己完成 build、布局、合成、绘制。获取纹理和 App 生命周期的机制也会因为平台的原因有所不同,比如 Android 的纹理和 IOS 的纹理在实现上就有所不同。Flutter 引擎是平台无关的,表现出来是 👉应用二进制接口,提供了一种平台嵌入器,可以安装和使用 Flutter。


平台嵌入器是一个原生系统的应用程序,承载着 Flutter 的所有的内容,并且充当了原生操作系统与 Flutter 之间的粘合剂。当我们打开 Flutter 应用的时候,嵌入器提供了一个入口:初始化 Flutter 引擎,获得 UI 线程和 光栅,创建 Flutter 纹理。嵌入器也负责:应用的生命周期、手势输入、窗口尺寸、线程管理和平台消息。Flutter 包含了 Android、iOS、Windows、macOS、Linux。开发者也可以自定义平台嵌入器,有两个比较不错的案例 👉 案例1 和 👉 案例2——Raspberry Pi


每一个平台尤其本身的API和约束。一些简明的平台原则如下:




  • 在 iOS 和 macOS,Flutter 是作为 UIViewController 或者 NSViewController 而被加载进嵌入器的。平台嵌入器创建了 FlutterEngine,而引擎可以当作 Dart VM 和 Flutter 运行时的宿主。FlutterViewControllerFlutterEngine 相绑定,将 UIKit 或者 Cocoa 输入事件传递给 Flutter,并且使用 Metal 或者 OpenGL 来渲染帧。




  • 在 Android 上,Flutter 默认加载到 Activity 中,视图就是 FlutterViewFlutterView 可以渲染 Flutter 的内容(ui 或者 纹理,取决于合成信息和 z 轴顺序),




  • 在 Windows 上,Flutter 被装载在传统的 Win32 应用中。Flutter 内容使用 ANGLE 渲染,




这个库可以将 OpenGL API 转为与之等价的 DirectX 11。目前正在做的事情是,提供一个使用 UWP 应用模型的 Windows 嵌入器,以及使用更加高效直接的方式将 DirectX 12 到 GPU,替换现有的 ANGLE。


集成其他代码


Flutter 提供了一些互操作的机制,访问 Kotlin 或者 Swift 编写的代码,调用 基于 C 的本地 API,在 Flutter 中嵌入原生组件,在既有应用中嵌入 Flutter。


Platform channels


对于移动端和桌面 App,通过 platform channel 机制,Flutter 可以让开发者调用自定义代码。platform channel 是 Dart 代码和 App 宿主平台代码通信的机制。通过创建一个通用的 channel (指定名字和编解码器),开发者能够在 Dart 和平台之间发送和接受消息。数据会被序列化,比如 Dart 的 Map 就是 Kotlin 中的 HashMap,Swift 的 Dictionary


image.png


下面是简单的事件处理器的代码,Android 是 Kotlin,iOS 是 Swift,Dart 调用原生的方法,并获取数据:


// Dart side
const channel = MethodChannel('foo');
final String greeting = await channel.invokeMethod('bar', 'world');
print(greeting);

// Android (Kotlin)
val channel = MethodChannel(flutterView, "foo")
channel.setMethodCallHandler { call, result ->
when (call.method) {
"bar" -> result.success("Hello, ${call.arguments}")
else -> result.notImplemented()
}
}

// iOS (Swift)
let channel = FlutterMethodChannel(name: "foo", binaryMessenger: flutterView)
channel.setMethodCallHandler {
(call: FlutterMethodCall, result: FlutterResult) -> Void in
switch (call.method) {
case "bar": result("Hello, (call.arguments as! String)")
default: result(FlutterMethodNotImplemented)
}
}

像这样的 channel 代码,可以在 flutter/plugins 仓库中找到,里面也有响应的 macOS 的实现。一些通用的场景大概有几千个可用插件,从广告到相机、蓝牙这样的硬件设备。


外部方法接口


对于 C基础的 API(包含 Rust、Go 生产的代码),Dart 也提供了直接的调用机制,可以使用 dart:ffi 依赖库来绑定 native 代码。Foreign function interface (FFI) 模型 没有数据数据序列化过程,所以它比上面的 channel 更快。Dart 运行时提供了在堆内存上分配内存的能力,堆上的内存是 Dart 对象内存,并且可以调用静态和动态的链接库。FFI 可用在除 web 之外的所有平台上,因为 js package 提供了相同的能力。


要使用 FFI 的话,可以创建一个 typedef 为每一个 Dart 的非管理的方法签名,并且指定 Dart VM 做了映射。下面是一个案例,调用 Win32 MessageBox() 的 API:


typedef MessageBoxNative = Int32 Function(
IntPtr hWnd,
Pointer
lpText,
Pointer
lpCaption,
Int32 uType,
)
;

typedef MessageBoxDart = int Function(
int hWnd,
Pointer
lpText,
Pointer
lpCaption,
int uType,
)
;

void exampleFfi() {
final user32 = DynamicLibrary.open('user32.dll');
final messageBox =
user32.lookupFunction('MessageBoxW');

final result = messageBox(
0, // No owner window
'Test message'.toNativeUtf16(), // Message
'Window caption'.toNativeUtf16(), // Window title
0, // OK button only
);
}

在 Flutter 应用中渲染原生组件


因为 Flutter 内容是被绘制在纹理上的,并且 组件树完全是内部的。像 Android view 存在在 Flutter 内部模型中,或者在 Flutter 组件交错渲染,这些情况我们咩有看到。如果不能支持的话,是有问题的,比如一些原生组件不能用的话,就很麻烦。比如 WebView。


Flutter 解决这种问题是通过平台视图 Widget 的方式(AndroidView 和 UiKitView)。这些组件可以嵌入平台原生组件。Platform Widget 可以和其他的 Flutter 内容一起集成,并且充当着与底层操作系统的中介。比如,在Android上,AndroidView 有三个主要的功能:




  • 复制原生视图的图形纹理,并把纹理作为 Flutter 合成渲染的一部分,所以在每一帧的时候都会进行这样的合成绘制。




  • 响应手势,并且把手势转为等价于 Native 的输入。




  • 创建一个可访问性树的模拟,并且在原生和 Flutter 层之间传递和响应命令




显而易见的,每帧的合成都是相当耗时的,像音视频也非常耗内存。所以,这种方法一般会在复杂交互的时候采用,比如 Google Maps 这样的,Flutter 不太具有生产实践意义的。


通常,一个 Flutter 应用也是在 build() 方法中实例化这些组件,比如,google_maps_flutter创建了地图插件:


if (defaultTargetPlatform == TargetPlatform.android) {
return AndroidView(
viewType: 'plugins.flutter.io/google_maps',
onPlatformViewCreated: onPlatformViewCreated,
gestureRecognizers: gestureRecognizers,
creationParams: creationParams,
creationParamsCodec: const StandardMessageCodec(),
);
} else if (defaultTargetPlatform == TargetPlatform.iOS) {
return UiKitView(
viewType: 'plugins.flutter.io/google_maps',
onPlatformViewCreated: onPlatformViewCreated,
gestureRecognizers: gestureRecognizers,
creationParams: creationParams,
creationParamsCodec: const StandardMessageCodec(),
);
}
return Text(
'$defaultTargetPlatform is not yet supported by the maps plugin');

AndroidView 或者 UiKitView 使用的我们前面提到的 platform channel 机制来和 原生代码交互。


目前,Platform Widget 在桌面平台上是不可用的,但是这不是架构上的限制,后面可能会添加。


宿主 App 接入 Flutter


和前面Flutter 嵌入 native 相反,这一节介绍 既有应用中嵌入 Flutter。前面我们提到了 Flutter 是被 Android 的 Activity,iOS 的 UIViewController 承载的,Flutter 的内容可以用相同的方式被嵌入。


Flutter module 模版很容易被嵌入,开发者可以使用 Gradle 或者 Xcode 进行源码依赖,也可以产物依赖。产物依赖的方式的好处就是项目组的成员不需要每个人都安装 Flutter 环境。


Flutter 引擎需要一点的时间初始化,因为需要加载 Flutter 依赖库,初始化 Dart 运行时,创建和运行 Dart 线程,绑定渲染 surface 到 UI。为了最小化上面提到的时间,减少呈现 Flutter UI 的延迟,最好的处理方式是在程序初始化的时候,初始化 Flutter 引擎,至少在第一个 第一个Flutter屏幕之前,这样用户不就会在显示 Flutter 第一个页面的时候,出现短暂的白屏或者黑屏。


关于接入的更多信息,可以在 👉Load sequence, performance and memory topic找到。


Flutter web support


一些通用的框架概念适用于 Flutter 支持的所有平台,但是呢,Flutter’s web 有一些值得讨论的独特的点。


自JavaScript语言存在以来,Dart就一直在编译JavaScript,并为开发和生产目的优化了工具链。许多重要的应用程序从Dart编译到JavaScript,并在今天的生产中运行,包括谷歌Ads的广告商工具。因为Flutter框架是用Dart编写的,所以将它编译成JavaScript相对简单。


从 Dart 语言面世以来,Dart 就一直在支持编译成 JavaScript,并且持续的为开发和生产优化工具链。许多重要的程序从 Dart 编译成 JavaScript,并在今天一直在运行,比如 👉advertiser tooling for Google Ads。因为 Flutter 框架是 Dart 开发的,把 Dart 编译成 JavaScript 相对来说是简单直接的。


However, the Flutter engine, written in C++, is designed to interface with the underlying operating system rather than a web browser. A different approach is therefore required. On the web, Flutter provides a reimplementation of the engine on top of standard browser APIs. We currently have two options for rendering Flutter content on the web: HTML and WebGL. In HTML mode, Flutter uses HTML, CSS, Canvas, and SVG. To render to WebGL, Flutter uses a version of Skia compiled to WebAssembly called CanvasKit. While HTML mode offers the best code size characteristics, CanvasKit provides the fastest path to the browser’s graphics stack, and offers somewhat higher graphical fidelity with the native mobile targets5.
然而,C++ 开发的 Flutter 引擎是操作系统底层的接口,而不是浏览器。因此,需要采取一个不同的方法。在 web 上,Flutter 在标准浏览器 API 之上 提供了重新实现。目前,在 Web 上渲染 Flutter 有两个方案:HTML 和 WebGL。HTML 模式下,Flutter 使用 HTML、 CSS、
Canvas 和 SVG。WebGL 模式下,Flutter 使用 CanvasKit 编译成 WebAssembly。
HTML 模式的包体积会很小,而 CanvasKit 的渲染会更快、渲染效果更佳高保真。


Web 版本的架构图是下面的:


Flutter web<br />
architecture


和其他 Flutter 平台相比,最显著的区别是:不需要提供一个 Dart 的运行时。相反,Flutter 的framework 被编译成了 JavaScript。在 Dart 的众多模式中,比如 JIT、AOT、native、web,语言语义上的差别很小,开发者也不会在开发的过程中体验到差异。


在开发期间,Flutter web 使用 dartdevc,它支持增量编译,这就可以 hot restart 了。相反,如果想要创建一个线上正式版本的 web,就会使用 dart2js 编译器,这是一款高性能的 JavaScript 编译器,会将 Flutter 核心和框架与应用程序打包为可部署到任何 web 服务器的小型源文件。deferred imports 可以将代码封装为一个文件,或者分割为多个文件。


展望


如果想要更加深入了解 Flutter 内部的机制,那么可以看 Inside Flutter 文章。


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

业务开发时,接口不能对外暴露怎么办?

0 - 前言 在业务开发的时候,经常会遇到某一个接口不能对外暴露,只能内网服务间调用的实际需求。面对这样的情况,我们该如何实现呢?今天,我们就来理一理这个问题,从几个可行的方案中,挑选一个来实现。 1 - 可行方案 目前,想到的方案有三种:内外网接口通过微服务...
继续阅读 »

0 - 前言


在业务开发的时候,经常会遇到某一个接口不能对外暴露,只能内网服务间调用的实际需求。面对这样的情况,我们该如何实现呢?今天,我们就来理一理这个问题,从几个可行的方案中,挑选一个来实现。


1 - 可行方案


目前,想到的方案有三种:内外网接口通过微服务隔离、redis配合网关实现接口白名单机制、网关加AOP在业务侧判断访问权限。


1.1 方案一 内外网接口微服务隔离


将对外暴露的接口和对内暴露的接口分别放到两个微服务上,一个服务里所有的接口均对外暴露,另一个服务的接口只能内网服务间调用。


该方案需要额外编写一个只对内部暴露接口的微服务,将所有只能对内暴露的业务接口聚合到这个微服务里,通过这个聚合的微服务,分别去各个业务侧获取资源。


该方案,新增一个微服务做请求转发,增加了系统的复杂性,增大了调用耗时以及后期的维护成本。


1.2 方案二 网关 + redis 实现白名单机制


在 redis 里维护一套接口白名单列表,外部请求到达网关时,从 redis 获取接口白名单,在白名单内的接口放行,反之拒绝掉。


该方案的好处是,对业务代码零侵入,只需要维护好白名单列表即可;


不足之处在于,白名单的维护是一个持续性投入的工作,在很多公司,业务开发无法直接触及到 redis,只能提工单申请,增加了开发成本;另外,每次请求进来,都需要判断白名单,增加了系统响应耗时,考虑到正常情况下外部进来的请求大部分都是在白名单内的,只有极少数恶意请求才会被白名单机制所拦截,所以该方案的性价比很低。


1.3 方案三 网关 + AOP


相比于方案二对接口进行白名单判断而言,方案三是对请求来源进行判断,并将该判断下沉到业务侧。避免了网关侧的逻辑判断,从而提升系统响应速度。


我们知道,外部进来的请求一定会经过网关再被分发到具体的业务侧,内部服务间的调用是不用走外部网关的(走 k8s 的 service)。根据这个特点,我们可以对所有经过网关的请求的header里添加一个字段,业务侧接口收到请求后,判断header里是否有该字段,如果有,则说明该请求来自外部,没有,则属于内部服务的调用,再根据该接口是否属于内部接口来决定是否放行该请求。


该方案将内外网访问权限的处理分布到各个业务侧进行,消除了由网关来处理的系统性瓶颈;同时,开发者可以在业务侧直接确定接口的内外网访问权限,提升开发效率的同时,增加了代码的可读性。


当然该方案会对业务代码有一定的侵入性,不过可以通过注解的形式,最大限度的降低这种侵入性。


image.png


2 - 具体实操


下面就方案三,进行具体的代码演示。


首先在网关侧,需要对进来的请求header添加外网标识符: from=public


@Component
public class AuthFilter implements GlobalFilter, Ordered {
@Override
public Mono < Void > filter ( ServerWebExchange exchange, GatewayFilterChain chain ) {
return chain.filter(
exchange.mutate().request(
exchange.getRequest().mutate().header("id", "").header("from", "public").build())
.build()
);
}

@Override
public int getOrder () {
return 0;
}
}

接着,编写内外网访问权限判断的AOP和注解


@Aspect
@Component
@Slf4j
public class OnlyIntranetAccessAspect {
@Pointcut ( "@within(org.openmmlab.platform.common.annotation.OnlyIntranetAccess)" )
public void onlyIntranetAccessOnClass () {}
@Pointcut ( "@annotation(org.openmmlab.platform.common.annotation.OnlyIntranetAccess)" )
public void onlyIntranetAccessOnMethed () {
}

@Before ( value = "onlyIntranetAccessOnMethed() || onlyIntranetAccessOnClass()" )
public void before () {
HttpServletRequest hsr = (( ServletRequestAttributes ) RequestContextHolder.getRequestAttributes()) .getRequest ();
String from = hsr.getHeader ( "from" );
if ( !StringUtils.isEmpty( from ) && "public".equals ( from )) {
log.error ( "This api is only allowed invoked by intranet source" );
throw new MMException ( ReturnEnum.C_NETWORK_INTERNET_ACCESS_NOT_ALLOWED_ERROR);
}
}
}

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

最后,在只能内网访问的接口上加上@OnlyIntranetAccess注解即可


@GetMapping ( "/role/add" )
@OnlyIntranetAccess
public String onlyIntranetAccess() {
return "该接口只允许内部服务调用";
}

以上。


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

为什么ConcurrentHashMap是线程安全的?

ConcurrentHashMap 是 HashMap 的多线程版本,HashMap 在并发操作时会有各种问题,比如死循环问题、数据覆盖等问题。而这些问题,只要使用 ConcurrentHashMap 就可以完美解决了,那问题来了,ConcurrentHash...
继续阅读 »

ConcurrentHashMap 是 HashMap 的多线程版本,HashMap 在并发操作时会有各种问题,比如死循环问题、数据覆盖等问题。而这些问题,只要使用 ConcurrentHashMap 就可以完美解决了,那问题来了,ConcurrentHashMap 是如何保证线程安全的?它的底层又是如何实现的?接下来我们一起来看。


JDK 1.7 底层实现


ConcurrentHashMap 在不同的 JDK 版本中实现是不同的,在 JDK 1.7 中它使用的是数组加链表的形式实现的,而数组又分为:大数组 Segment 和小数组 HashEntry。 大数组 Segment 可以理解为 MySQL 中的数据库,而每个数据库(Segment)中又有很多张表 HashEntry,每个 HashEntry 中又有多条数据,这些数据是用链表连接的,如下图所示:
image.png


JDK 1.7 线程安全实现


了解了 ConcurrentHashMap 的底层实现,再看它的线程安全实现就比较简单了。
接下来,我们通过添加元素 put 方法,来看 JDK 1.7 中 ConcurrentHashMap 是如何保证线程安全的,具体实现源码如下:


final V put(K key, int hash, V value, boolean onlyIfAbsent) {
// 在往该 Segment 写入前,先确保获取到锁
HashEntry<K,V> node = tryLock() ? null : scanAndLockForPut(key, hash, value);
V oldValue;
try {
// Segment 内部数组
HashEntry<K,V>[] tab = table;
int index = (tab.length - 1) & hash;
HashEntry<K,V> first = entryAt(tab, index);
for (HashEntry<K,V> e = first;;) {
if (e != null) {
K k;
// 更新已有值...
}
else {
// 放置 HashEntry 到特定位置,如果超过阈值则进行 rehash
// 忽略其他代码...
}
}
} finally {
// 释放锁
unlock();
}
return oldValue;
}
复制代码

从上述源码我们可以看出,Segment 本身是基于 ReentrantLock 实现的加锁和释放锁的操作,这样就能保证多个线程同时访问 ConcurrentHashMap 时,同一时间只有一个线程能操作相应的节点,这样就保证了 ConcurrentHashMap 的线程安全了。
也就是说 ConcurrentHashMap 的线程安全是建立在 Segment 加锁的基础上的,所以我们把它称之为分段锁或片段锁,如下图所示:
image.png


JDK 1.8 底层实现


在 JDK 1.7 中,ConcurrentHashMap 虽然是线程安全的,但因为它的底层实现是数组 + 链表的形式,所以在数据比较多的情况下访问是很慢的,因为要遍历整个链表,而 JDK 1.8 则使用了数组 + 链表/红黑树的方式优化了 ConcurrentHashMap 的实现,具体实现结构如下:
image.png
链表升级为红黑树的规则:当链表长度大于 8,并且数组的长度大于 64 时,链表就会升级为红黑树的结构。



PS:ConcurrentHashMap 在 JDK 1.8 虽然保留了 Segment 的定义,但这仅仅是为了保证序列化时的兼容性,不再有任何结构上的用处了。



JDK 1.8 线程安全实现


在 JDK 1.8 中 ConcurrentHashMap 使用的是 CAS + volatile 或 synchronized 的方式来保证线程安全的,它的核心实现源码如下:


final V putVal(K key, V value, boolean onlyIfAbsent) { if (key == null || value == null) throw new NullPointerException();
int hash = spread(key.hashCode());
int binCount = 0;
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh; K fk; V fv;
if (tab == null || (n = tab.length) == 0)
tab = initTable();
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) { // 节点为空
// 利用 CAS 去进行无锁线程安全操作,如果 bin 是空的
if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value)))
break;
}
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else if (onlyIfAbsent
&& fh == hash
&& ((fk = f.key) == key || (fk != null && key.equals(fk)))
&& (fv = f.val) != null)
return fv;
else {
V oldVal = null;
synchronized (f) {
// 细粒度的同步修改操作...
}
}
// 如果超过阈值,升级为红黑树
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
addCount(1L, binCount);
return null;
}

从上述源码可以看出,在 JDK 1.8 中,添加元素时首先会判断容器是否为空,如果为空则使用 volatile 加 CAS 来初始化。如果容器不为空则根据存储的元素计算该位置是否为空,如果为空则利用 CAS 设置该节点;如果不为空则使用 synchronize 加锁,遍历桶中的数据,替换或新增节点到桶中,最后再判断是否需要转为红黑树,这样就能保证并发访问时的线程安全了。
我们把上述流程简化一下,我们可以简单的认为在 JDK 1.8 中,ConcurrentHashMap 是在头节点加锁来保证线程安全的,锁的粒度相比 Segment 来说更小了,发生冲突和加锁的频率降低了,并发操作的性能就提高了。而且 JDK 1.8 使用的是红黑树优化了之前的固定链表,那么当数据量比较大的时候,查询性能也得到了很大的提升,从之前的 O(n) 优化到了 O(logn) 的时间复杂度,具体加锁示意图如下:
image.png


总结


ConcurrentHashMap 在 JDK 1.7 时使用的是数据加链表的形式实现的,其中数组分为两类:大数组 Segment 和小数组 HashEntry,而加锁是通过给 Segment 添加 ReentrantLock 锁来实现线程安全的。而 JDK 1.8 中 ConcurrentHashMap 使用的是数组+链表/红黑树的方式实现的,它是通过 CAS 或 synchronized 来实现线程安全的,并且它的锁粒度更小,查询性能也更高。


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

对雪花算法的初识到热恋

分库分表常见主键id生产策略讲解 引入什么技术都是会存在一定的风险,分库分表也不会是例外。在不同的数据节点生成一个唯一主键是一个难题,一张逻辑表x_order会被拆分成多个真实表x_order_n,然后这些表又被分散到不同的库中db_0、db1、db2各个表自...
继续阅读 »

分库分表常见主键id生产策略讲解


引入什么技术都是会存在一定的风险,分库分表也不会是例外。在不同的数据节点生成一个唯一主键是一个难题,一张逻辑表x_order会被拆分成多个真实表x_order_n,然后这些表又被分散到不同的库中db_0、db1、db2各个表自增键由于没有办法相互的感应会产生重复的主键,这就没有办法满足分库分表对主键的全局唯一的要求了。


尽管可以使用分片表的自增主键初始值和步长来解决,但是这样会导致运维成本提高,可扩展性差,这种方式不太可取。


业界常用ID解决方案


UUID: 性能非常高,没有网络消耗。缺点就是无序的字符串,不具备有趋势自增的特性,UUID太长也不便于存储,浪费存储空间,所以很多的场景是不使用的


Redsi发号器: 利用Redis的INCR和INCRBY来实现,原子操作,线程安全,性能比Mysql强劲。缺点就是需要占用网络资源,增加系统复杂度。


Snowflake雪花算法: 它是twitter开源式的分布式ID生成算法,代码实现简单,而且不占用宽带,数据的迁移不会受到影响,生成的id里包含有时间戳,所以生成的id按照时间进行递增,部署多台服务器的话,需要保证系统时间是一样的。缺点就是依赖系统时钟。


UUID


进入UUID的主键生成实现类,发现它只有一个规则,那就是UUID.randomUUID(),可以看出是多么的牛啊。UUID虽然可以做到全局的唯一性,但还是不推荐作为主键,因为我们知道,在实际业务中主键一般为整型,而UUID是生成32位的字符串。它对MYSQL的性能消耗会比较大,而且MYSQL也建议主键要尽量的短,UUID主键的无序性还会导致数据位置的频繁变动,严重的影响了性能。


public final class UUIDShardingKeyGenerator implements ShardingKeyGenerator {
    private Properties properties = new Properties();

    public UUIDShardingKeyGenerator() {
    }

    public String getType() {
        return "UUID";
    }

    public synchronized Comparable<?> generateKey() {
        return UUID.randomUUID().toString().replaceAll("-", "");
    }

    public Properties getProperties() {
        return this.properties;
    }

    public void setProperties(Properties properties) {
        this.properties = properties;
    }
}

Snowflake(雪花算法)


雪花算法是默认使用主键生成方案,生成一个64bit的长整型数据。sharding-jdbc中雪花算法生成的主键主要是由四个部分组成,1bit符号位、41bit时间戳位、10bit机器id,12bit序列号。


图片


符号位(1bit)


在Java当中,Long型最高的位是符号位,也就是正数是0,负数是1,一般id生成都为正数,所以的话默认是0。


时间戳位(41bit)


41位的时间戳占41个bit,毫秒数是2的41次幂,而一年的总毫秒数就为1000L * 60 * 60 * 24 * 365,为服务上线的时间毫秒级时间戳(当前时间-服务第一次上线的时间)。


工作机器id(10bit)


表示一个唯一的工作进程,它的默认值是0,可以通过key-generator.props.worker.id来进行设置


序列号位(12bit)


可以允许同一毫秒生成2^12=4096个id,理论上一秒就可以生成400万个id。


时钟回拨


了解了雪花算法主键的组成后可以发现,这是一种严重的依赖服务器的时间算法,这时候依赖服务器时间就会遇到一个棘手的问题就是-时钟回拨。


为啥会出现时钟回拨这种现象


据我们所知,在互联网中有一种网络的时间协议叫ntp,专门是用来进行同步或者是用来校准网络计算机的时间。这就是手机现在不用手动校对时间的原因。


当硬件因为某一些原因导致时间快或者是慢了,这个时候就需要使用ntp服务来对时间进行校准,在做校准的时候就有可能发生服务器时钟的跳跃或者是回拨这些问题。


雪花算法怎么样解决时钟回拨问题


上面提到服务器时钟回拨问题可能会导致重复的id产生,所以在SNOWFLAKE方案对雪花算法进行了改进,添加了一个最大的容忍时钟回拨毫秒数。


当时钟回拨的时间超过最大容忍毫秒数的阀值的话,就直接程序报错。如果在可以容忍的范围内的话,就默认使用了分布式的主键生成器,等待时钟同步到最后一次生成主键时间后才继续开始工作。


最大容忍的时钟回拨毫秒数默认是0,max.tolerate.time.difference.milliseconds来设置。


public final class SnowflakeShardingKeyGenerator implements ShardingKeyGenerator{
    @Getter
    @Setter
    private Properties properties = new Properties();
    
    public String getType() {
        return "SNOWFLAKE";
    }
    
    public synchronized Comparable<?> generateKey() {
     /**
      * 当前系统时间毫秒数 
      */ 
        long currentMilliseconds = timeService.getCurrentMillis();
        /**
         * 判断是否需要等待容忍时间差,如果需要,则等待时间差过去,然后再获取当前系统时间 
         */ 
        if (waitTolerateTimeDifferenceIfNeed(currentMilliseconds)) {
            currentMilliseconds = timeService.getCurrentMillis();
        }
        /**
         * 如果最后一次毫秒与 当前系统时间毫秒相同,即还在同一毫秒内 
         */
        if (lastMilliseconds == currentMilliseconds) {
         /**
          * &位与运算符:两个数都转为二进制,如果相对应位都是1,则结果为1,否则为0
          * 当序列为4095时,4095+1后的新序列与掩码进行位与运算结果是0
          * 当序列为其他值时,位与运算结果都不会是0
          * 即本毫秒的序列已经用到最大值4096,此时要取下一个毫秒时间值
          */
            if (0L == (sequence = (sequence + 1) & SEQUENCE_MASK)) {
                currentMilliseconds = waitUntilNextTime(currentMilliseconds);
            }
        } else {
         /**
          * 上一毫秒已经过去,把序列值重置为1 
          */
            vibrateSequenceOffset();
            sequence = sequenceOffset;
        }
        lastMilliseconds = currentMilliseconds;
        
        /**
         * XX......XX XX000000 00000000 00000000 时间差 XX
         *    XXXXXX XXXX0000 00000000 机器ID XX
         *               XXXX XXXXXXXX 序列号 XX
         *  三部分进行|位或运算:如果相对应位都是0,则结果为0,否则为1
         */
        return ((currentMilliseconds - EPOCH) << TIMESTAMP_LEFT_SHIFT_BITS) | (getWorkerId() << WORKER_ID_LEFT_SHIFT_BITS) | sequence;
    }
    
    /**
     * 判断是否需要等待容忍时间差
     */
    @SneakyThrows
    private boolean waitTolerateTimeDifferenceIfNeed(final long currentMilliseconds) {
     /**
      * 如果获取ID时的最后一次时间毫秒数小于等于当前系统时间毫秒数,属于正常情况,则不需要等待 
      */
        if (lastMilliseconds <= currentMilliseconds) {
            return false;
        }
        /**
         * ===>时钟回拨的情况(生成序列的时间大于当前系统的时间),需要等待时间差 
         */
        /**
         * 获取ID时的最后一次毫秒数减去当前系统时间毫秒数的时间差 
         */
        long timeDifferenceMilliseconds = lastMilliseconds - currentMilliseconds;
        /**
         * 时间差小于最大容忍时间差,即当前还在时钟回拨的时间差之内 
         */
        Preconditions.checkState(timeDifferenceMilliseconds < getMaxTolerateTimeDifferenceMilliseconds(), 
                "Clock is moving backwards, last time is %d milliseconds, current time is %d milliseconds", lastMilliseconds, currentMilliseconds);
        /**
         * 线程休眠时间差 
         */
        Thread.sleep(timeDifferenceMilliseconds);
        return true;
    }
    
    // 配置的机器ID
    private long getWorkerId() {
        long result = Long.valueOf(properties.getProperty("worker.id", String.valueOf(WORKER_ID)));
        Preconditions.checkArgument(result >= 0L && result < WORKER_ID_MAX_VALUE);
        return result;
    }
    
    private int getMaxTolerateTimeDifferenceMilliseconds() {
        return Integer.valueOf(properties.getProperty("max.tolerate.time.difference.milliseconds", String.valueOf(MAX_TOLERATE_TIME_DIFFERENCE_MILLISECONDS)));
    }
    
    private long waitUntilNextTime(final long lastTime) {
        long result = timeService.getCurrentMillis();
        while (result <= lastTime) {
            result = timeService.getCurrentMillis();
        }
        return result;
    }
}

可以看出最后的主键生成时间(lastMilliseconds)与当前(currentMilliseconds)做比较,如果生成的时间大于当前时间的话,这就说明了时钟回调了。那么这时会接着判断两个时间的差,看下是否在设置在最大的容忍时间范围内,在范围内就睡眠差值的时间,大于差值那就直接报异常。


百度UidGenerator


UidGenerator算法是对雪花算法的改进版。UidGenerator组成是由:sign(1bit)+delta seconds(28bits)+worker node id(22bits)+sequence(13bits)。


UidGenerator可以保证指定的机器同一时间某一并发序列是唯一的,并由此生成一个64bits的唯一id。


UidGenerator跟雪花算法不一样,它可以自定义时间戳,工作机器id与序列号各部位的位数,用于不同的场景。


1.sign:固定的符号标识,生成的UID为正数。


2.delta seconds:当前的时间


3.worker id:机器id,内置实现是在启动的时候由数据库分配,默认分配策略为:用后就弃掉。


4.sequence:每秒下的并发序列,13bits可以支持每一秒8192个并发。


UidGenerator两种方式


DefaultUidGenerator: 通过DefaultUidGenerator来实现的,对于时钟回拨问题比较的简单,根据业务情况来调整字段占用的位数。


CachedUidGenerator: CachedUidGenerator是在DefaultUidGenerator进行改进的,利用了RingBuffer,它的本质是一个数组,数组里的每一个项都叫slot。而CachedUidGenerator是设计两个RingBuffer,一个是用于保存唯一id,一个保存flag。


CachedUidGenerator主要是通过以下的集中方式规避了时钟的问题和增强了唯一性


1、自增列:在每一次重启的时候workerid都会初始化,且它就是数据库自增的id,这样完美的实现每一个实例获取到的workerid都不一样,不会造成冲突


2、RingBuffer:不需要在每次取得ID都要计算分布式ID,而是利用RingBuffer数据结构预先生成若干个分布式ID来保存


3、时间递增:像雪花算法都是通过currentTimeMillis来获取时间,并比较,这样的话是很依赖服务器的时间,但是CachedUidGenerator的时间类型是AtomicLong,通过incrementAndGet方法来获取下一次的时间,这样就避免了对服务器时间的依赖,也就是时钟回拨的问题可以得到解决。


CachedUidGenerator通过缓存的这种方式来预先生成唯一ID列表,这种可以解决唯一ID所消耗的时间,但是也有不好的点就是,需要耗费内存来缓存这一部分ID的数据,如果访问量不是很大的情况下,提前生成的UID的时间戳可能是很早以前的。


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

flutter架构:Repository设计模式

在软件开发中,我们可以使用设计模式有效的解决我们软件设计中的常见问题。而在app的架构中,structural设计模式可以帮助我们很好的划分应用结构。在本文,我们将使用Repository设计模式,访问各种来源的数据,如后端的API,蓝牙等等。并将这些数据转化...
继续阅读 »

在软件开发中,我们可以使用设计模式有效的解决我们软件设计中的常见问题。而在app的架构中,structural设计模式可以帮助我们很好的划分应用结构。

在本文,我们将使用Repository设计模式,访问各种来源的数据,如后端的API,蓝牙等等。并将这些数据转化成类型安全的实体类提供给上层(领域层),即我们业务逻辑所在的位置。

本文中我们将详细讲解Repository设计模式,包含以下部分:

  • Repository设计模式是什么以及何时使用它
  • 使用具体抽象类的实现以及如何权衡使用
  • 如何使用Repository设计模式单元测试

1.什么是Repository设计模式

为了帮助我们理解,我们先看看下面的app架构设计图:

Untitled.png

在这张图中,repositories位于 数据层(data layer),它的作用是:

  • 将领域模型(或实体)与数据源(data sources)的实现细节隔离开来。
  • 将数据源的数据对象转换为领域层(domain layer)中使用的实体或模型
  • (可选)执行数据缓存等操作。

上图仅展示了构建APP的其中一种架构模式。如果使用其他的架构模式,例如 MVC、MVVM 或 Clean Architecture,虽然看起来不一样,但repository设计模式的应用都一样。

还要注意在**表示层(UI或Presentation)**中,widget是需要与业务逻辑或网络等是无关的。

如果在Widget中直接使用来自REST API 或远程数据库的key-value,这样做是有很大风险的。换句话说:不要将业务逻辑与您的 UI 代码混合,这会使你的代码更难测试、调试和推理。

2.什么时候使用Repository设计模式

如果你的APP有一个复杂的数据层,包含许多不同的数据来源,并且这些来源返回非结构化数据(例如 JSON),这样需要将其与其他部分隔离,这时候使用Repository设计模式非常方便。

如果说更具体的话,下面这些场景我认为Repository设计模式更合适:

  • 与 REST API 交互
  • 与本地或远程数据库(例如 Sembast、Hive、Firestore 等)交互
  • 与设备的 API(例如权限、摄像头、位置等)交互

这样做的最大的好处是,如果任何第三方API 发生重大更改,我们只需要更新Repository的代码

仅仅这一点就我就觉得使Repository模式 是100% 值得我们在实际中使用的。💯

下面我们就看看如何使用吧!🚀

3.Repository设计模式在实际中的使用

我们以OpenWeatherMap(https://openweathermap.org/api)提供的天气查询API为例,做一个简单的天气查询APP。

我们先看看API 文档openweathermap.org/current),先了解需要如何调用 API,以及响应数据的JSON 格式。

我们通过Repository设计模式能非常快速的抽象出所有网络相关和 JSON 序列化代码。下面,我们就来具体实现吧。

首先,我们为repository定义一个抽象类:

abstract class WeatherRepository {
Future<Weather> getWeather({required String city});
}
复制代码

我们的WeatherRepository现在只添加了一个方法,但是在实际应用中我们可能会有很多个,根据需求决定。

接下来,我们还需要一个具体的实现类,来实现API调用以及数据出局等:

import 'package:http/http.dart' as http;

class HttpWeatherRepository implements WeatherRepository {
HttpWeatherRepository({required this.api, required this.client});
// custom class defining all the API details
final OpenWeatherMapAPI api;
// client for making calls to the API
final http.Client client;

// implements the method in the abstract class
Future<Weather> getWeather({required String city}) {
// TODO: send request, parse response, return Weather object or throw error
}
}

这些具体的细节在data layer实现,其他层就不需要关心数据是如何来的。

3.1数据解析

我们需要定义一个具体的model(或者entity),用来接收和解析api返回的json数据。

class Weather {
// TODO: declare all the properties we need
factory Weather.fromJson(Map<String, dynamic> json) {
// TODO: parse JSON and return validated Weather object
}
}

api返回的字段可能很多,我们这里只需要解析我们使用到的字段。

json解析有很多方法,ide(vscode、android studio)提供了很多插件,帮助我们快速的实现fromJson,感兴趣的同学可以自己去找找。

3.2 初始化repository

repository定义后,我们需要在一个合适的时机进行初始化,以便app其他层能够访问。

如何进行repository的初始化,我们需要根据我们选择的状态管理工具来决定。

例如,我们使用get_it(pub.dev/packages/ge…)来进行管理:

import 'package:get_it/get_it.dart';

GetIt.instance.registerLazySingleton<WeatherRepository>(
() => HttpWeatherRepository(api: OpenWeatherMapAPI(), client: http.Client(),
);

或者也可使用Riverpod

import 'package:flutter_riverpod/flutter_riverpod.dart';

final weatherRepositoryProvider = Provider<WeatherRepository>((ref) {
return HttpWeatherRepository(api: OpenWeatherMapAPI(), client: http.Client());
});

或者是使用bloc:

import 'package:flutter_bloc/flutter_bloc.dart';

RepositoryProvider<WeatherRepository>(
create: (_) => HttpWeatherRepository(api: OpenWeatherMapAPI(), client: http.Client()),
child: MyApp(),
))

不管使用哪种方式,我们的目的是repository初始化一次全局都可以使用。

4.抽象还是具体?

当创建一个repository的时候,我们也许会有疑惑,我们需要创建一个抽象类吗?还是只需要一个具体类?如果添加的方法越来越多,可能会觉得工作越来越多,如下:

abstract class WeatherRepository {
Future<Weather> getWeather({required String city});
Future<Forecast> getHourlyForecast({required String city});
Future<Forecast> getDailyForecast({required String city});
// and so on
}

class HttpWeatherRepository implements WeatherRepository {
HttpWeatherRepository({required this.api, required this.client});
// custom class defining all the API details
final OpenWeatherMapAPI api;
// client for making calls to the API
final http.Client client;

Future<Weather> getWeather({required String city}) { ... }
Future<Forecast> getHourlyForecast({required String city}) { ... }
Future<Forecast> getDailyForecast({required String city}) { ... }
// and so on
}

到底需不需要,答案就像软件设计中的给出的一样:视情况而定。那么,我们就来分析下两种方法的优缺点。

4.1 使用抽象类

  • 优点:提供了统一的接口,不关心具体实现,使用时比较统一。
  • 优点  完全可以使用不同的实现 ****,替换时只需要更改初始化时的一行代码。
  • 缺点**:**当我们在IDE点击“跳转到引用”时只能到抽象类中的方法定义而不是具体类中的实现。
  • 缺点:会写更多代码。

4.2只有具体类

  • 优点:更少的代码。
  • 优点:IDE中点击“跳转到引用”能跳转到正确的方法。
  • 缺点:如果我们repository名字,需要多处修改。

但是呢,具体如何选择,我们还有一个重要的参考标准,就是我们需要为它添加单元测试。

5.repository的单元测试

单元测试时,我们需要mock掉网络调用的部分,是我们的测试更快更准确。

这样的话,我们使用抽象类就没有任何优势,因为在Dart中所有类都有一个隐式接口,如下,我们可以这样mock数据:

// note: in Dart we can always implement a concrete class
class FakeWeatherRepository implements HttpWeatherRepository {

// just a fake implementation that returns a value immediately
Future<Weather> getWeather({required String city}) {
return Future.value(Weather(...));
}
}

所以在单元测试中,我们完全没必要需要抽象类。我们在单测中,可以使用mocktail这样的包:

import 'package:mocktail/mocktail.dart';

class MockWeatherRepository extends Mock implements HttpWeatherRepository {}

final mockWeatherRepository = MockWeatherRepository();
when(() => mockWeatherRepository.getWeather('London'))
.thenAnswer((_) => Future.value(Weather(...)));

在测试里,我们可以mock HttpWeatherRepository,也可以mock HttpClient,

import 'package:http/http.dart' as http;
import 'package:mocktail/mocktail.dart';

class MockHttpClient extends Mock implements http.Client {}

void main() {
test('repository with mocked http client', () async {
// setup
final mockHttpClient = MockHttpClient();
final api = OpenWeatherMapAPI();
final weatherRepository =
HttpWeatherRepository(api: api, client: mockHttpClient);
when(() => mockHttpClient.get(api.weather('London')))
.thenAnswer((_) => Future.value(/* some valid http.Response */));
// run
final weather = await weatherRepository.getWeather(city: 'London');
// verify
expect(weather, Weather(...));
});
}

具体的是mock Repository还是HttpClient,可以根据你需要测试的内容来定。

最后,对于Repository到底需不需要抽象类,我觉得是没必要的,对于Repository我们只需要一个具体的实现,而且每个Repository是不一样的。

Repository的扩展

这里我们只实例了一个库,但是随着业务的增长,我们的应用功能越来越多,在一个Repository里添加所有api显然不是一个明智的选择。

所有,我们可以根据场景划分不同的Repository,将相关的方法放在同一个Repository中。比如在电商app中,我们划分为产品列表、购物车、订单管理、身份验证、结算等Repository。

总结

所有事情保持简单是最好的,我希望这篇概述能够激发大家更清晰地去思考App的架构,以及分层(UI层、领域和数据层)的重要性。


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

收起阅读 »