注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

环信FAQ

环信FAQ

集成常见问题及答案
RTE开发者社区

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

一点Andorid开发小建议:妥善使用和管理Handler

在Android开发中,我们经常使用Android SDK提供的Handler类来完成线程间通信的任务,但是项目代码中,经常看到Handler过于随意地使用,这些使用可能有一些隐患,本文记录下这些问题,并给出相关建议。 问题一:使用默认无参构造来创建Handl...
继续阅读 »

在Android开发中,我们经常使用Android SDK提供的Handler类来完成线程间通信的任务,但是项目代码中,经常看到Handler过于随意地使用,这些使用可能有一些隐患,本文记录下这些问题,并给出相关建议。


问题一:使用默认无参构造来创建Handler


Handler有个无参构造方法,有的时候偷懒在Activity中直接通过无参构造方法来创建Handler对象,例如:

private final Handler mHandler = new Handler();

那么这个Handler对象会使用当前线程的Looper,这在Activity或自定义View中可能没问题,因为主线程是存在Looper的,但是在子线程中就会出现异常,因为子线程很可能没有进行过Looper.prepare()。另外一个隐患是,new Handler()使用“当前线程”的Looper,可能预期是在子线程,但是一开始的外部调用是在主线程,那么这个使用可能影响主线程的交互体验。



  • 建议:创建Handler对象时必须传递Looper参数来确保Looper的存在并显式控制其线程。


问题二:Looper.getMainLooper()的滥用


在Activity中我们可以使用runOnUiThread方法把一个过程放在主线程执行,但是在其他地方,通过Looper.getMainLooper()也是一个简单的方法,例如:

new Handler(Looper.getMainLooper()).post(() -> {
//do something ...
});

因为太方便了,所以到处都可以用,那么就存在了一个隐患:任何线程都可以通过这种方式来影响主线程。有可能在配置较差的手机出现意料之外的卡顿,而且这种卡顿可能有一定随机性,不容易复现,给排查问题的时候造成一定难度。



  • 建议:避免在线程外部创建该线程的Handler,例如,尽量避免在ActivityFragment和自定义View以外的地方创建主线程的Handler


问题三:在业务功能层面直接创建Hanlder


在开发中,有时候需要利用Handler来切换线程,或者利用Handler的消息功能,然后直接使用Handler,例如下面这段代码:

new Thread(() -> {
presenter.doTaskInBackground();
new Handler(Looper.getMainLooper()).post(() -> {
updateViewInMainThread();
});
}).start()

这个代码槽点太多,这里主要讲下关于Handler的。其实程序的结果并没有什么问题,不足之处在于把Handler这种平台相关的概念混到了业务功能代码里,就好像一个人在读诗朗诵,念完“两个黄鹂鸣翠柳”,然后下面冒出一句“我先喝口水”,喝完水然后继续念。



  • 建议:将Handler的创建和销毁放到框架层面,甚至可以封装一套使用的接口,而不是直接使用postsend等方法。


问题四:没有及时移除Hanlder的消息和回调


HandlerpostDelayedpostAtTime是两个便利的方法,但是这个方法并没有和组件的生命周期绑定,很容易造成Activity或其他大型对象无法及时释放。



  • 建议:不需要的时候,及时调用removeCallbacksAndMessages方法

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

日常开发中,提升技术的13个建议

前言 大家好,我是田螺。 最近有位读者问我:田螺哥,日常开发中,都是在做业务需求,如何提升自己的技术呢? 所以,本文田螺哥整理了提升技术的13个建议,小伙伴们,一起加油。 1. 打好基础,深入学习语言特性 比如,对于Java程序员来说,要了解Java语言的基...
继续阅读 »

前言


大家好,我是田螺


最近有位读者问我:田螺哥,日常开发中,都是在做业务需求,如何提升自己的技术呢? 所以,本文田螺哥整理了提升技术的13个建议,小伙伴们,一起加油。



1. 打好基础,深入学习语言特性


比如,对于Java程序员来说,要了解Java语言的基本概念和核心特性,包括面向对象编程、集合框架、异常处理、多线程等等。可以通过阅读Java的官方文档、教程、参考书籍或在线资源来学习。


如果最基本的基础都不扎实,就不要谈什么提升技术啦。 比如说:



  • 你知道HashMap和ConcurrentHashMap的区别嘛?

  • 在什么时候使用ConcurrentHashMap?操作文件的时候

  • 你知道在finally块中释放资源嘛?

  • 你知道在哪些场景适合用泛型嘛?


因此,要提升自身技术,首先就是要把基础打扎实。 有些小伙伴说,上班没时间学基础呀,其实不是这样的,基础这玩意,每天地铁上下班看看,下班后回到家在看看,周末在家看看,多点写写代码,一般一两个月,你的基础就很好啦。


又有些小伙伴说,如何提升Java基础呢? 可以:



  • 阅读Java相关书籍或教程,如Java编程思想、Java核心技术、Java虚拟机、菜鸟教程等

  • 阅读Java博客和社区参与讨论:关注Java领域的博客、论坛和社区,了解最新的技术动态和解决方案,与其他开发者交流。

  • 多实践,多敲代码:在B站找个Java基础视频看,平时多实践、多敲代码



2. 熟悉掌握常用的开发工具


工欲善其事,必先利其器. 所以一位好的程序员,往往编码效率就更高。而提升编码效率,一般要求熟悉并灵活应用工具.比如Eclipse、IntelliJ IDEA、Maven、Navicat等。熟悉运用这些工具,可以提高开发效率。


我举个例子,比如你熟悉掌握IntelliJ IDEA的快捷键,三两下就把实体类的setter和getter方法生成了,而有些的程序员,还在一行一行慢慢敲。。



3. 日常工作中,总结你踩过的坑


优秀的程序员,之所以优秀,是因为他会总结踩过的坑,避免重蹈覆辙。所以,田螺哥建议你,日常开发中,如果你踩了哪些坑,就需要总结下来.茶余饭后,再温习温习.


比如,你知道:



  • Redis分布式锁使用,可能会有哪些坑嘛?

  • 线程池使用有哪些坑?

  • Java日期处理又又哪些坑嘛?

  • Arrays.asList使用可能有哪些坑?


如果一时间忘记的话,可以看下我以前的这些文章:



这些都是我工作总结出来的,也希望你们日常开发中,遇到哪些坑,都总结下来哈。



4.工作中,阅读你项目优秀的代码和设计文档


孔子说,三人行,必有我师。大家平时在看代码的时候,不要总吐槽着项目的烂代码。其实,可以多点关注写得优秀的代码,然后看懂别人为什么这些写,仿造着来写。


当然,一些好的设计文档也是:人家为什么这么设计,好处在哪里,不足又在哪里,如果是你来设计,你如何思考等等。把好的设计,读懂后,记录下来,变成自己的知识.



5.日常工作中,总结一些通用的技术方案.


在日常工作中呢,注意整理一些通用的技术方案。


比如幂等设计、分布式锁如何设计、分布式事务设计、接口优化、限流设计、分库分表设计、深分页问题解决等等. 大家可以看下我之前的一些通用方案设计的文章哈:



当然,田螺哥也建议你,日常开发中,把自己遇到的一些通用设计方案总结下来,熟悉掌握这些通用技术方案。



6.参与技术讨论,积极技术分享


参与技术讨论和交流,可以有助于你与其他Java开发者分享经验、解决问题和学习新知识。进行技术分享,可以加深自己的理解、建立专业声誉、促进个人成长、为技术社区做贡献等等。


比如你做需求遇到的一些难题,都可以跟有经验的同事、或者技术leader讨论讨论。一些常见的难题,讨论完可以记录下来,然后做技术分享



7. 主人翁意识,积极攻克项目的难题


作为一名开发工程师,具备主人翁意识并积极攻克项目的难题,是非常重要的。遇到项目中的比较棘手问题时,先不管是谁的问题,我们都要持有主人翁意识,积极主动地找到解决方案并采取行动。


而在技术找解决方案的过程,我们也就成长了。当攻克问题后,你也获得领导的认可,好绩效不远了,一举多得



8. 思考项目中,哪些可以提升效率


日常开发中,几乎大多数程序员都是在进行增删改查。如何如何避免自己成为平凡的增删改查程序员呢。


我觉得可以这样做:平时工作中,思考项目中,有哪些可以提升的效率。包括熟悉开发工具、掌握适当的调试技巧、熟悉常用框架、持续学习和关注技术发展等等。


比如:



  • 好的的debug调试技巧,可以让你快速找到问题

  • 再比如一个插件easyyapi可以一键让你快速生成yapi接口文档,而不用一个一个字段手工敲接口文档。


当然,日常开发中,还有很多可以提升效率的技巧/工具,等待我们去发现



9. 熟悉你的业务,让自己不容易被替代


我们普通程序员,多数都是做业务的。一般工作个五年以上,水平差不了太多。如何避免自己被淘汰呢?我个人建议是,尽量做到熟悉你们做的业务,让你变得不容易被替代。



10. 多看看你的系统,可能存在哪些问题,如接口耗时、慢SQL等等


一般的系统,多多少少都有些问题。比如接口耗时过长、慢SQL、fullGC频繁等等。


首先需要掌握这些技能,比如如何优化接口,如何优化慢SQl、fullGC如何排查等等。大家可以看下这几篇文章哈:



11. 学以致用,将理论知识应用到实际项目中


很多小伙伴说,看过很多计算机相关的书,阅读过很多博客,背了很多八股文,依然做不好一个系统。


我觉得,大家可以多点思考,把平时积累的东西,应用到实际项目中。背八股文不是没用,你可以把它应用到实际开发中的。比如说,你看了田螺哥的文章,IO模型详解


这个表面看起来就是一个常见的八股文知识点,工作中似乎没用到。但是我在工作中,就用到这种类似的异步思想



比如发起一笔批量转账,但是批量转账处理比较耗时,这时候后端可以先告知前端转账提交成功,等到结果处理完,再通知前端结果即可。



再比如,你看完田螺哥的:MySQL索引15连问,抗住!,你是不是可以回头看看,你的系统中,那些sql的索引加的是否合理呢?是不是可以思考一下如何优化,对吧。因此,就是要学以致用



12. 阅读一些优秀框架的源码,如spring、rockectMq等等


如果你有空余的时间,就建议你看看一些优化框架的源码,比如spring、rockectMq等等。


对于spring源码的话,可以按模块来呀,比如aop,控制反转,spring事务等,你先写个demo,然后debug跟踪流程,通过调试器逐步跟踪源码执行过程,观察各个方法的调用关系和数据变化。最好是结合电子书一起,如(Spring源码深度解析这本书一起)


优秀框架的源码,我们可以学习到很多编码思想的,加油。



13. 多编码,少偷懒,养成编程的好习惯


作为程序员,一定要多打代码,不要偷懒,代码敲多了,你就会了。还有就是,少点偷懒,坚持!努力!养成热爱编程的好习惯


总之,提升技术需要不断学习、实践、总结和积累经验



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

来这公司一年碰到的问题比我过去10年都多

无意间发现我们 Kafka 管理平台的服务的 open files 和 CPU 监控异常,如下图,有一台机器 CPU 和 opfen files 指标持续在高位,尤其是 open files 达到了4w+。 原因分析 第一反应是这个服务请求很高?但是这个服...
继续阅读 »

无意间发现我们 Kafka 管理平台的服务的 open files 和 CPU 监控异常,如下图,有一台机器 CPU 和 opfen files 指标持续在高位,尤其是 open files 达到了4w+。




原因分析


第一反应是这个服务请求很高?但是这个服务是一个管理服务不应该有很高的请求量才对,打开监控一看,QPS少的可怜。



既然机器还在就找 devops 同学帮忙使用 Arthas 简单看下是什么线程导致的,竟然是 GC 线程,瞬时 CPU 几乎打满了。



查看了 GC 监控,每分钟 5~6 次相比其他的正常节点要多很多,并且耗时很长。


问题节点GC Count



正常节点GC Count



应该是代码出问题了,继续求助 devops 将线上有问题的机器拉了一份 dump,使用 MAT 工具分析了下,打开 dump 就提示了两个风险点,两个都像是指标相关的对象。



查看详情发现两个可疑对象,一个是 60+M 的 byte[], 一个是 60+M 的 map,都是指标相关的对象,问题应该出在指标上。



初步去排查了下代码,看是否有自定义指标之类的,发现一个 job 会对指标进行操作,就把 job 停了一段时间,GC 少了很多,但是 open files 只减少了一点点, 很明显不是根本原因。




继续深入,将 byte[] 保存成字符串查看(确实文本也有60+M),发现全是 JMX 的指标数据,我们的系统使用了两种指标一种是Micrometer,一种是 prometheus-jmx-exporter,这个 byte[] 数组就是第二种指标的数据。



并且这些指标中发现有非常多的 kafka_producer 开头的指标。



为了验证是否属于 JMX 的指标数据,再次求助 devops 拉取线上有问题机器的 JMX 指标接口, 看返回的是否是 60M+ 的指标数据,发现根本拉不下来。



到此基本确认问题出在 JMX 指标上, 那这些指标谁注册的呢?


通过指标名称在源代码里搜索,发现是来自org.apache.kafka.common.network.Selector.SelectorMetrics,是 kafka-client注册的指标。


具体的创建顺序如下,每创建一个KafkaProducer,就会以 client id 为唯一标识创建一个SelectorMetrics, 而创建 KafkaProducer 会创建一个守护线程,并开启一个长连接定时去 Broker 拉取/更新 Metadata 信息,这个就是open files飙高的根本原因。


KafkaProducer -> Sender -> Selector -> SelectorMetrics



难道创建了很多 KafkaProducer???查看构造方法调用的地方,找到了真凶。。。



这段代码是为了支持延迟消息,业务服务每发一个延迟消息,就会执行一次这段逻辑, 就会创建一个 KafkaProducer,并且会随着收到的消息越来越多导致创建的 KafkaProducer 越来越多,直至系统无法承受。。。


庆幸的是我们延迟消息并不是太多,没有直接把系统给打挂掉


那为什么只有一个节点会有问题,其他节点没有问题呢?这个比较简单直接说结果了,就是这段消费逻辑消费的 topic 只有一个分区....


解决方案:


由于 Kafka 管理平台会连接多个 Broker,所以此处将创建的 KafkaProducer 根据 Cluster 缓存起来进行复用。


问题总结:



  1. KafkaProducer 本身是一个很重的对象,并且线程安全,创建的时候注意考虑场景

  2. 此次问题完全是凭运气提前发现了,证明监控系统也存在不够完善的地方, 我们使用 Prometheus 的标准差函数 (stddev() by()) 配置了资源严重倾斜的监控告警,防止出现类似的问题。

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

三分钟,趁同事上厕所的时间,我覆盖了公司的正式环境数据

大家好啊,又跟大家见面了,最近有个需求就是批量修改公司的数据报表,正式环境!! 而且要执行update!!update it_xtgnyhcebg I set taskStatus = XXX 而且是没有加where条件的,相当于全表更新,这可马虎不得,我们...
继续阅读 »

大家好啊,又跟大家见面了,最近有个需求就是批量修改公司的数据报表,正式环境!!
而且要执行update!!

update it_xtgnyhcebg I set taskStatus = XXX

而且是没有加where条件的,相当于全表更新,这可马虎不得,我们在任何操作正式数据库之前一定一定要对数据库备份!!不要问我怎么知道的,因为我就因为有一次把测试环境的数据覆盖到正式环境去了。。。


在这里插入图片描述


别到时候就后悔莫及,那是没有用的!


在这里插入图片描述
在这里插入图片描述


由于这个需求是需要在跨库操作的,所以我们在查询数据的时候需要带上库的名称,例如这样

SELECT
*
FROM
BPM00001.ACT_HI_PROCINST P
LEFT JOIN BPM00001.ACT_HI_VARINST V ON V.PROC_INST_ID_ = P.ID_
AND V.NAME_ = '__RESULE'


这样如果我们在任何一个库里面,只要在一个mysql服务里面都可以访问到这个数据
查出这个表之后
在这里插入图片描述
我们需要根据这里的内容显示出不同的东西
就例如说是“APPROVAL”我就显示“已通过”
这就类似与java中的Switch,其实sql也能实现这样的效果
如下:
在这里插入图片描述
这就是sql的case语句的使用
有了这些数据之后我们就可以更新数据表了,回到我们之前讨论过的,这是及其危险的操作
我们先把要set的值给拿出来
在这里插入图片描述


在这里插入图片描述
但是我们怎么知道这个里面的主键呢?
你如果直接这么加,肯定是不行的
在这里插入图片描述
所以我们需要在sql后面加入这样的一条语句
在这里插入图片描述
注意,这个语句一定要写在set语句的里面,这样sql就能依据里面判断的条件进行一一赋值
最后,将这个sql语句执行到生产库中


拓展:


作为查询语句的key绝对不能重复,否则会失败(找bug找了半天的人的善意提醒)
例如上面的语句中P.BUSINESS_KEY_必须要保证是唯一的!!


在这里插入图片描述
成功执行!!!
怎么样,这些sql的小妙招你学会了吗?


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

安卓知识点-应届生扫盲安卓WebView

作者 大家好,我叫Jack冯; 本人20年硕士毕业于广东工业大学,于2020年6月加入37手游安卓团队; 目前主要负责海外游戏发行安卓相关开发。 背景 最近在接触活动相关需求,其中涉及到一个安卓的WebView; 刚毕业的我,对安卓知识积累比较少,所以在这里对...
继续阅读 »

作者


大家好,我叫Jack冯;


本人20年硕士毕业于广东工业大学,于2020年6月加入37手游安卓团队;


目前主要负责海外游戏发行安卓相关开发。


背景


最近在接触活动相关需求,其中涉及到一个安卓的WebView;


刚毕业的我,对安卓知识积累比较少,所以在这里对Webview进行相关学习,希望自己可以在安卓方面逐步积累。


Webview介绍


1、关于MockView


( 1 ) 在targetSdkVersion 28/29的工程里面查看WebView继承关系

java.lang.Object
↳ android.view.View
↳ android.view.ViewGroup
↳ android.widget.FrameLayout
↳ android.layoutlib.bridge.MockView
↳ android.webkit.WebView


( 2 ) 使用26/27等低版本SDK,查看源码中的WebView 继承关系

java.lang.Object
↳ android.view.View
↳ android.view.ViewGroup
↳ android.widget.AbsoluteLayout
↳ android.webkit.WebView

( 3 )对比


两种方式对比,AbsoluteLayout和FrameLayout都是重写ViewGroup的方法,如与布局参数配置相关的 generateDefaultLayoutParams()、checkLayoutParams()等。两种方式明显不同的是多了一层MockView 。这里来看看MockView是什么:

public class MockView extends FrameLayout{
...
//创建方式
public MockView(Context context) {...}
public MockView(Context context,AttributeSet attrs) {...}
public MockView(Context context,AttributeSet attrs,int defStyleRes) {...}
//重写添加view方法
@Override
public void addView(View child){...}
@Override
public void addView(View child,int index){...}
@Override
public void addView(View child,int width,int height){...}
@Override
public void addView(View child,ViewGroup.LayoutParams params){...}
@Override
public void addView(View child,int index,ViewGroup.LayoutParams params){...}
public void setText(CharSequence text){...}
public void setGravity(int gravity){...}
}

MockView,译为"虚假的view"。


谷歌发布的Sdk其实只是为了提供App开发运行接口,实际运行时候替换为当前系统的Sdk。


具体说就是当谷歌在新的系统(Framework)版本上准备对WebView实现机制进行改动,同时又希望把新的sdk提前发出来,不影响用到WebView的App开发,于是谷歌提供给Android开发的sdk中让WebView继承自MockView,这个WebView只是暴露了接口,没有具体实现;这样当谷歌关于WebView新的实现做好,利用WebView,app也就做好了


2、基本使用


(1)创建


①一般方式:

WebView webView = findViewById(R.id.webview);

②建议方式:

LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,ViewGroup.LayoutParams.MATCH_PARENT);
mWebView = new WebView(getApplicationContext());
mWebView.setLayoutParams(params);

好处:构建不用依赖本地xml文件,自定义页面参数;手动销毁避免内存泄露;


③更多方式 : 继承Webview和主要API等进行拓展

public class BaseWebView extends WebView {...}
public class BaseWebClient extends WebClient {...}
public class BaseWebChromeClient extends WebChromeClient {...}

(2)加载


① 加载某个网页

webView.loadUrl("http://www.google.com/");

②新建assets目录,将html文件放到目录下,通过路径加载本地页面

 webView.loadUrl("file:///android_asset/loadFailed.html");

③使用evaluateJavascript(String script, ValueCallback resultCallback)方法加载,(Android4.4+)

mWebView.evaluateJavascript("file:///android_asset/javascript.html",new ValueCallback<String>() {
@Override
public void onReceiveValue(String value) {
Log.e("测试", "onReceiveValue:"+value );
}
});

3、WebViewClient


当URL即将加载到当前窗口,如果没有提供WebViewClient,默认情况下WebView将使用Activity管理器为URL选择适当的处理器。


如果提供了WebViewClient,按自定义配置要求来继续加载URL。


(1)常用方法

//加载过程对url的处理(webview加载、系统浏览器加载、其他操作等)
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
super.shouldOverrideUrlLoading(view, url);
}
//加载失败页面
@Override
public void onReceivedError(WebView view, int errorCode, String description, String failingUrl){
view.loadUrl("file:///android_asset/js_error.html");
}
//证书错误处理
@Override
public void onReceivedSslError(WebView view, final SslErrorHandler handler, SslError error) {
}
//开始加载页面(可自定义页面加载计时等)
@Override
public void onPageStarted(WebView view, String url, Bitmap favicon) {
super.onPageStarted(view, url, favicon);
Log.e(TAG, "onPageStarted:" + url);
}
//结束加载页面
@Override
public void onPageFinished(WebView view, String url) {
super.onPageFinished(view, url);
Log.e(TAG, "onPageFinished: " + url);
}

(2)关于shouldOverrideUrlLoading


如果在点击链接加载过程需要更多的控制,就可以在WebViewClient()中重写shouldOverrideUrlLoading()方法。


涉及shouldOverrideUrlLoading()的情形,大概分为三种:


(1)没有设定setWebViewClient(),点击链接使用默认浏览器打开;


(2)设定setWebViewClient(new WebViewClient()),默认shouldOverrideUrlLoading()返回false,点击链接在Webview加载;


(3)设定、重写shouldOverrideUrlLoading()


返回true:可由应用代码处理该 url,WebView 中止处理(若重写方法没加上view.loadUrl(url),不加载);


返回false:由 WebView 处理加载该 url。(即使没加上view.loadUrl(url),也会在当前Webview加载)


【一般应用】

@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
super.shouldOverrideUrlLoading(view, url);
if (url != null) {
if (!(url.startsWith("http") || url.startsWith("https"))) {
return true;
}
//重定向到别的页面
//view.loadUrl("file:///android_asset/javascript.html");
//区别不同链接加载
view.loadUrl(url);
}
return true;
}

(3)常见误区


【误区1】 : 需要重写 shouldOverrideUrlLoading 方法才能阻止浏览器打开页面。


解释:WebViewClient 源码中 shouldOverrideUrlLoading 方法已经返回 false,不设定setWebViewClient(),默认使用系统浏览器加载。如果重写该方法并返回true, 就可以实现在app页面中加载新链接而不去打开浏览器。


【误区2】 : 每一个url加载过程都会经过 shouldOverrideUrlLoading 方法。


Q1:加载一定会触发shouldOverrideUrlLoading?


Q2:触发时机一定在onPageStarted调用之前?


解释:关于shouldOverrideUrlLoading的触发


1)如果在点击页面链接时通过标签跳转,触发方法如下:


shouldOverrideUrlLoading() —> onPageStarted()—> onPageFinished()


2)如果使用loadUrl加载时,触发方法如下:


onPageStarted()—>onPageFinished()


3)如果使用loadUrl加载重定向地址时,触发方法如下:


shouldOverrideUrlLoadings—>onPageStarted —> onPageFinished


ps:多次重定向的过程,


onPage1Started


—>shouldOverrideUrlLoadings


—>onPage2Started —> xxx...


—> onPageNFinished


结论:shouldOverrideUrlLoading()方法不是每次加载都会调用,WebView的前进、后退等不会调用shouldOverrideUrlLoading方法;非loadUrl方式加载 或者 是重定向的,才会调用shouldOverrideUrlLoading方法。


【误区3 】: 重写 shouldOverrideUrlLoading 方法返回true比false的区别,多调用一次onPageStarted()和onPageFinished()。


解释:返回True:应用代码处理url;返回False,则由 WebView 处理加载 url。


ps:低版本系统(华为6.0),测试 False比True会多调用一次onPageStarted()和onPageFinished(),这点还在求证中。


4、WebChromeClient


对比WebviewClient , 添加了处理JavaScript对话框,图标,标题和进度等。


处理对象 : 影响浏览器的事件


(1)常用方法:

//alert弹出框
public boolean onJsAlert(WebView view, String url, String message,JsResult result){
return true;//true表示拦截
}

//confirm弹出框
public boolean onJsConfirm(WebView view, String url, String message,JsResult result){
return false;//false则允许弹出
}

public boolean onJsPrompt(WebView view, String url, String message,String defaultValue, JsPromptResult result)

//打印 console 信息。return true只显示log,不显示js控制台的输入;false则都显示出来
public boolean onConsoleMessage(ConsoleMessage consoleMessage){
Log.e("测试", "consoleMessage:"+consoleMessage.message());
}

//通知程序当前页面加载进度,结合ProgressBar显示
public void onProgressChanged(WebView view, int newProgress){
if (newProgress < 100) {
String progress = newProgress + "%";
Log.e("测试", "加载进度:"+progress);
webProgress.setProgress(newProgress);
}
}

(2)拦截示例:


JsResult.comfirm() --> 确定按钮的调用方法


JsResult.cancle() --> 取消按钮


示例:拦截H5的弹框,并显示自定义弹框,点击按钮后重定向页面到别的url

@Override
public boolean onJsConfirm(final WebView view, String url, String message, final JsResult result) {
Log.e("测试", "onJsConfirm:"+url+",message:"+message+",jsResult:"+result.toString());
new AlertDialog.Builder(chromeContext)
.setTitle("拦截JsConfirm显示!")
.setMessage(message)
.setPositiveButton(android.R.string.ok,
new AlertDialog.OnClickListener() {
public void onClick(DialogInterface dialog,int which) {
//重定向页面
view.loadUrl("file:///android_asset/javascript.html");
result.confirm();
}
}).setCancelable(false).create().show();
return true;
}

5、WebSettings


用于页面状态设置\插件支持等配置.


(1)常用方法

WebSettings webSettings = webView.getSettings();
/**
* 设置缓存模式、支持Js调用、缩放按钮、访问文件等
*/
webSettings.setCacheMode(WebSettings.LOAD_DEFAULT);
webSettings.setJavaScriptEnabled(true);
webSettings.setSupportZoom(true);
webSettings.setBuiltInZoomControls(true);
webSettings.setDisplayZoomControls(true);

//允许WebView使用File协议,访问本地私有目录的文件
webSettings.setAllowFileAccess(true);

//允许通过file url加载的JS页面读取本地文件
webSettings.setAllowFileAccessFromFileURLs(true);

//允许通过file url加载的JS页面可以访问其他来源内容,包括其他的文件和http,https等来源
webSettings.setAllowUniversalAccessFromFileURLs(true);
webSettings.setJavaScriptCanOpenWindowsAutomatically(true);
webSettings.setLoadsImagesAutomatically(true);
webSettings.setDefaultTextEncodingName("utf-8")

if (Build.VERSION.SDK_INT > Build.VERSION_CODES.LOLLIPOP) {
webSettings.setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW);
}

结束语


过程中有问题或者需要交流的同学,可以扫描二维码加好友,然后进群进行问题和技术的交流等;


作者:37手游移动客户端团队
链接:https://juejin.cn/post/7245084484756144186
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

分享Android App的几个核心概念

Application启动 点击桌面图标启动App(如下流程图) 针对以上流程图示: ActivityManagerService#startProcessLocked() Process#start() ActivityThread#main(),入口分...
继续阅读 »

Application启动


点击桌面图标启动App(如下流程图)


1240.jpg


针对以上流程图示:



  • ActivityManagerService#startProcessLocked()

  • Process#start()

  • ActivityThread#main(),入口分析的地方

  • ActivityThread#attach(),这个里面的逻辑很核心 ActivityManagerService#attachApplication(),通过Binder机制调用了ActivityManagerService的attachApplication

  • ActivityManagerService#attachApplicationLocked(),整个应用进程已经启动起来

  • ActivityManagerService#thread.bindApplication,具体回到ActivityThread

  • ActivityThread.ApplicationThread#bindApplication(),最后看到sendMessage处理bind逻辑

  • ActivityThread#handleBindApplication(),设置进程的pid,初始化进程信息

  • ActivityThread#mInstrumentation.callApplicationOnCreate,看到Application进入onCreate()方法中,这就是从最开始main()方法开始到最后的Application的onCreate()的创建过程


Window创建


如何创建Window


在创建Activity实例的同时,会调用Activity的内部方法attach方法完成window的初始化。Activity类中相关源码如下所示:

final void attach(Context context, ActivityThread aThread,
Instrumentation instr, IBinder token, int ident,
Application application, Intent intent, ActivityInfo info,
CharSequence title, Activity parent, String id,
NonConfigurationInstances lastNonConfigurationInstances,
Configuration config, String referrer, IVoiceInteractor voiceInteractor,
Window window, ActivityConfigCallback activityConfigCallback) {
//创建 PhoneWindow
mWindow = new PhoneWindow(this, window, activityConfigCallback);
}


  • Window是一个抽象类,具体实现是PhoneWindow。PhoneWindow中有个内部类DecorView,通过创建DecorView来加载Activity中设置的布局R.layout.activity_main

  • 创建Window需要通过WindowManager创建,通过WindowManager将DecorView加载其中,并将DecorView交给ViewRoot,进行视图绘制以及其他交互


Android组件设计


ActivityManagerService




  • 启动组件



    • 组件启动时,检查其所要运行在的进程是否已创建。如果已经创建,就直接通知它加载组件。否则,先将该进程创建起来,再通知它加载组件。




  • 关闭组件



    • 组件关闭时,其所运行在的进程无需关闭,这样就可以让组件重新打开时得到快速启动。




  • 维护组件状态



    • 维护组件在运行过程的状态,这样组件就可以在其所运行在的进程被回收的情况下仍然继续生存。




  • 进程管理




    • 在适当的时候主动回收空进程和后台进程,以及通知进程自己进行内存回收




    • 组件的UID和Process Name唯一决定了其所要运行在的进程。




    • 每次组件onStop时,都会将自己的状态传递给AMS维护。




    • AMS在以下四种情况下会调用trimApplications来主动回收进程:



      • A.activityStopped,停止Activity

      • B.setProcessLimit,设置进程数量限制

      • C.unregisterReceiver,注销Broadcast Receiver

      • D.finishReceiver,结束Broadcast Receiver






Binder




  • 为组件间通信提供支持



    • 进程间;进程内都可以




  • 高效的IPC机制



    • 进程间的组件通信时,通信数据只需一次拷贝

    • 进程内的组件通信时,跳过IPC进行直接的通信




说一说DecorView


DecorView是什么




  • DecorView是FrameLayout的子类,它是Android视图树的根节点视图



    • DecorView作为顶级View,一般情况下内部包含一个竖直方向的LinearLayout,在这个LinearLayout里面有上下三个部分,上面是个ViewStub,延迟加载的视图(设置ActionBar,根据Theme设置),中间的是标题栏(根据Theme设置,有的布局没有),下面的是内容栏。
    <LinearLayout >
    <ViewStub
    android:id="@+id/action_mode_bar_stub"/>
    <FrameLayout>
    <TextView
    android:id="@android:id/title"/>
    </FrameLayout>

    <FrameLayout
    android:id="@android:id/content"/>
    </LinearLayout>



  • 上面的id为content的FrameLayout中,在代码中可以通过content来得到对应加载的布局

    ViewGroup content = (ViewGroup)findViewById(android.R.id.content);
    ViewGroup rootView = (ViewGroup) content.getChildAt(0);



Activity 与 PhoneWindow 与 DecorView 关系


12401.jpg


一个 Activity 对应一个 PhoneWindow,一个 PhoneWindow 持有一个 DecorView 实例,DecorView 本身是一个 FrameLayout。


如何创建DecorView




  • 从Activity中的setContentView()开始



    • 在Activity中的attach()方法中,生成了PhoneWindow实例。既已有Window对象,那么就可以设置DecorView给Window对象了。

    • 从中获取mContentParent。获得到后,通过installDecor方法生成DecorView,源码中操作比较复杂,大概先从主题中获取样式,根据样式加载对应的布局到DecorView中,为mContentParent添加View,即Activity中的布局。

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

如果你的同事还不会配置commit提交规范,请把这篇文章甩给他

Git
前言 首先问问大家在日常工作中喜欢哪种commit提交?git commit -m "代码更新" git commit -m "解决公共样式问题" git commit -m "feat: 新增微信自定义分享" 如果你是第三种,那我觉得你肯定了解过com...
继续阅读 »

前言


首先问问大家在日常工作中喜欢哪种commit提交?

git commit -m "代码更新"

git commit -m "解决公共样式问题"

git commit -m "feat: 新增微信自定义分享"

如果你是第三种,那我觉得你肯定了解过commit提交规范,可能是刷到过同类文章也可能是在工作中受到的要求


我自己是在刚出来实习的一家公司了解到的,依稀记得“冒号要用英文的,冒号后面要接空格...”


虽然我一直保持这种习惯去提交代码,但是后面遇到的同事大部分都是放飞自我的提交,看的我很难受


因此这篇文章就教还不会配置的小伙伴如何配置被业界广泛认可的 Angular commit message 规范以及呼吁大家去使用。


先来了解下commit message的构成

<type>(<scope>): <subject>
<BLANK LINE>
<body>
<BLANK LINE>
<footer>

对应的commit记录如下图


微信截图_20230608114515.png




  • type: 必填 commit 类型,有业内常用的字段,也可以根据需要自己定义



    • feat 增加新功能

    • fix 修复问题/BUG

    • style 代码风格相关无影响运行结果的

    • perf 优化/性能提升

    • refactor 重构

    • revert 撤销修改

    • test 测试相关

    • docs 文档/注释

    • chore 依赖更新/脚手架配置修改等

    • workflow 工作流改进

    • ci 持续集成

    • types 类型定义文件更改

    • wip 开发中

    • undef 不确定的分类




  • scope: commit 影响的范围, 比如某某组件、某某页面




  • subject: 必填 简短的概述提交的代码,建议符合 50/72 formatting




  • body: commit 具体修改内容, 可以分为多行, 建议符合 50/72 formatting




  • footer: 其他备注, 包括 breaking changes 和 issues 两部分




git cz使用


只需要输入 git cz ,就能为我们生成规范代码的提交信息。


一、安装工具

npm install -g commitizen // 系统将弹出上述type、scope等来填写
npm install -g cz-conventional-changelog // 用来规范提交信息

ps:如果你是拉取别人已经配置好git cz的项目,记得也要在自己环境安装


然后将cz-conventional-changelog添加到package.json中

commitizen init cz-conventional-changelog --save --save-exact

微信截图_20230608155514.png


二、使用git cz提交


安装完第一步的工具后,就可以使用git cz命令提交代码了


微信图片_20230608092741.png


微信图片_20230608092732.png


如图,输入完git cz命令后,系统将会弹出提交所需信息,只需要依次填写就可以


commitlint使用


如果你不想使用git cz命令去提交代码,还是习惯git commit的方式去提交


那么接下来就教大家怎么在git commit命令或者vscode工具中同样规范的提交代码


一、安装工具

npm install --save-dev husky
npm install --save-dev @commitlint/cli
npm install --save-dev @commitlint/config-conventional

二、配置



  • 初始化husky
npx husky install


  • 添加hooks
npx husky add .husky/commit-msg 'npx --no -- commitlint --edit \$1'


  • 在项目根目录下创建commitlint.config.js,并配置如下
module.exports = {
extends: ['@commitlint/config-conventional'],
rules: {
'type-case': [2, 'always', ['lower-case', 'upper-case']],
'type-enum': [2, 'always',[
'feat', // 增加新功能
'fix', // 修复问题/BUG
'style', // 代码风格相关无影响运行结果的
'perf', // 优化/性能提升
'refactor', // 重构
'revert', // 撤销修改
'test', // 测试相关
'docs', // 文档/注释
'chore', // 依赖更新/脚手架配置修改等
'workflow', // 工作流改进
'ci', // 持续集成
'types', // 类型定义文件更改
'wip', // 开发中
'undef' // 不确定的分类
]
]
}
}

三、验证


没配置前能直接提交


微信图片_20230608092753.png


配置之后就会规范提交


微信图片_20230608092757.png


总结


以上两种方式都很简单,几个步骤下来就可以配置好,希望大家都能养成一个开发好习惯~


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

Handler真的难?看完这篇文章你就懂了!

在Android开发中,Handler是一个非常重要的组件,它可以用来实现线程之间的通信和任务调度。本篇文章将介绍Handler的使用方式和原理,帮助读者更好地理解Android开发中的线程处理。 什么是Handler? Handler是Android中的一个...
继续阅读 »

在Android开发中,Handler是一个非常重要的组件,它可以用来实现线程之间的通信和任务调度。本篇文章将介绍Handler的使用方式和原理,帮助读者更好地理解Android开发中的线程处理。


什么是Handler?


Handler是Android中的一个消息处理器,它可以接收并处理其他线程发来的消息。简单来说,Handler就是一个用来处理消息的工具类,它可以将消息发送给其他线程,也可以接收其他线程发送的消息进行处理。


Handler的使用方式


使用Handler的基本流程为:创建Handler对象 -> 发送消息 -> 处理消息。


在使用 Handler 之前,需要了解一些相关概念:



  • 线程:是独立运行的程序段,执行的代码是一个单独的任务。

  • 消息队列:是一种存储消息的数据结构,支持先进先出的队列操作。

  • Looper:可以让线程不停地从消息队列中取出消息并处理,是线程与消息队列交互的桥梁。

  • Message:是 Android 中处理消息的基本类,可以携带一些数据,用于在 Handler 中进行处理。


创建Handler对象


在使用Handler之前,需要先创建一个Handler对象。创建Handler对象的方式有两种:




  • 在主线程中创建Handler对象:


    在主线程中创建Handler对象非常简单,只需要在主线程中创建一个Handler对象即可:

    Handler handler = new Handler();



  • 在子线程中创建Handler对象:


    在子线程中创建Handler对象需要先获取到主线程的Looper对象,然后使用Looper对象来创建Handler对象:

    Handler handler = new Handler(Looper.getMainLooper());



发送消息


创建Handler对象之后,就可以使用它来发送消息了。发送消息的方式有两种:




  • 使用Handler的post()方法:


    使用Handler的post()方法可以将一个Runnable对象发送到Handler所在的消息队列中。Runnable对象中的代码会在Handler所在的线程中执行。

    handler.post(new Runnable() {
    @Override
    public void run() {
    // 在Handler所在的线程中执行的代码
    }
    });



  • 使用Handler的sendMessage()方法:


    使用Handler的sendMessage()方法可以将一个Message对象发送到Handler所在的消息队列中。Message对象中可以携带一些数据,用于在Handler中进行处理。

    Message message = new Message();
    message.what = 1;
    message.obj = "Hello World!";
    handler.sendMessage(message);



除了基本用法,Handler还有一些高级用法,下面列举了几个常用的:



  • 使用HandlerThread创建带有消息队列的线程,避免频繁地创建线程;

  • 使用Message.obtain()来获取Message对象,避免频繁地创建对象;

  • 使用Handler的sendEmptyMessage()方法来发送空消息。


处理消息


当其他线程发送消息到Handler所在的消息队列中时,Handler就会接收到这些消息并进行处理。处理消息的方式有两种:




  • 重写Handler的handleMessage()方法:


    重写Handler的handleMessage()方法可以处理其他线程发送的消息。handleMessage()方法中的代码会在Handler所在的线程中执行。

    Handler handler = new Handler() {
    @Override
    public void handleMessage(Message msg) {
    switch (msg.what) {
    case 1:
    String message = (String) msg.obj;
    // 处理消息的代码
    break;
    default:
    break;
    }
    }
    };



  • 实现Handler.Callback接口:


    实现Handler.Callback接口可以处理其他线程发送的消息。Callback接口中的方法会在Handler所在的线程中执行。

    Handler.Callback callback = new Handler.Callback() {
    @Override
    public boolean handleMessage(Message msg) {
    switch (msg.what) {
    case 1:
    String message = (String) msg.obj;
    // 处理消息的代码
    break;
    default:
    break;
    }
    return true;
    }
    };
    Handler handler = new Handler(callback);



Handler的原理


在Handler的背后,实际上是使用了消息队列和线程通信的机制。当其他线程发送消息时,消息会被加入到Handler所在的消息队列中。然后,Handler会从消息队列中取出消息进行处理。


消息队列和Looper


消息队列和Looper是 Handler 实现的基础。每个线程都有一个消息队列(Message Queue),消息队列中存储着队列的所有消息(Message)。线程通过一个 Looper 来管理它的消息队列,通过不断地从消息队列中读取消息,实现了线程的消息循环 (Message Loop) 的功能。

public static void loop() {
final Looper me = myLooper();
if (me == null) {
throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
}
final MessageQueue queue = me.mQueue;
for (;;) {
Message msg = queue.next(); // might block
// 一旦有消息,就会返回Message对象
msg.target.dispatchMessage(msg);
}
}

如上所示,一个线程中的消息无限循环直到队列里没有消息为止(MessageQueue.next())。消息通过 Message.target 属性来找到它想要执行的 Handler,从而被分配到正确的线程中并且得到执行。一旦有消息,就会调用 dispatchMessage(Message) 方法进行分发。


消息分发


消息分发是Handler 的核心部分,在它的内部逻辑中,也是最为关键的部分。


在 Handler 中,消息分发的流程如下:


1.1. 发送消息


由其他线程调用 Handler 的方法向消息队列中发送消息。

Handler handler = new Handler() ;
handler.post(new Runnable(){
@Override
public void run() {
// 在其他线程发送消息
}
});

1.2. 创建 Message 对象


将需要传输的数据封装成 Message 类型的对象,然后将该对象塞入消息队列中。

Message msg = new Message();
msg.obj = "消息内容";
handler.sendMessage(msg);

1.3. 将消息加入消息队列


Handler 将消息放入消息队列中。


在 Handler 内部,新构建的消息通过 enqueueMessage() 方法被加入到 MessageQueue 相应的内存块中,并且会在该内存块的标记 next 表示下一个内存块的索引号。

public void sendMessageAtTime(Message msg, long uptimeMillis) {
MessageQueue queue = mQueue;
if (queue == null) {
RuntimeException e = new RuntimeException(
this + " sendMessageAtTime() called with no mQueue");
Log.w("Looper", e.getMessage(), e);
return;
}
msg.target = this;
queue.enqueueMessage(msg, uptimeMillis);
}

enqueueMessage() 方法的核心逻辑,就是紧接着找到消息队列中最近的一个时间戳比当前时间小的消息,将新消息插入到这个消息之后。

boolean enqueueMessage(Message msg, long when) {
synchronized (this) {
if (msg.target == null) {
throw new IllegalArgumentException("Message must have a target.");
}

if (msg.isInUse()) {
throw new IllegalStateException(msg + " This message is already in use.");
}

boolean needWake;
if (mBlocked) {
// If the queue is blocked, then we don't need to wake
// any waiters since there can be no waiters.
msg.markInUse();
needWake = false;
} else {
msg.markInUse();
needWake = mMessagesForQueue.enqueueMessage(msg, when);
}

if (needWake) {
nativeWake(mPtr);
}
}

return true;
}

1.4. Looper 开启消息循环


Looper 不断轮询内部 MessageQueue 中的消息,获取消息后在 Handler 中进行分发处理。


在 Looper 类中,强制让当前线程创建一个 Looper 对象,并通过调用 QualityLooper 构造函数 create 方法捕获该对象(一般用于构建线程的消息循环)。接下来,通过调用 run 方法被延迟1秒钟来启动上下文中的消息循环。

public static void prepare() {
prepare(true);
}

public static void prepare(boolean quitAllowed) {
if (sThreadLocal.get() != null) {
throw new RuntimeException("Only one Looper may be created per thread");
}
sThreadLocal.set(new Looper(quitAllowed));
}

public static void loop() {
final Looper me = myLooper();
if (me == null) {
throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
}
final MessageQueue queue = me.mQueue;
for (;;) {
Message msg = queue.next(); // might block
if (msg == null) {
continue;
}
msg.target.dispatchMessage(msg);
}
}

这个方法是一个无限循环方法,在每个循环中,Looper 都会从自己的消息队列中获取一个消息,如果队列为空,则一直循环等待新的消息到来,直到被调用 quit() 方法,才终止循环。在获取到消息之后,调用 msg.target.dispatchMessage(msg) 进行消息的分发处理。


1.5. 查找目标 Handler


Looper 不断轮询消息队列,获取消息后,注意到 MessageQueue.next() 方法中有这样一行代码:

msg.target.dispatchMessage(msg);

1.6. 传递 Message 对象


从消息中获取到 target 属性,它就是当前这个Message对象所属的 Handler,并执行该Handler的 handleMessage(Message) 方法。


dispatchMessage(Message) 的核心代码是判断 Message.target 是否为 null,不为 null 则将消息传递给目标 Handler,如果为 null,则直接抛出异常。

void dispatchMessage(Message msg) {
if (msg.callback != null) {
// 消息带有回调方法,如果 callback 不为空,那么就直接执行
handleCallback(msg);
} else {
if (mCallback != null) {
// 尝试将消息抛给 mCallback
if (mCallback.handleMessage(msg)) {
return;
}
}
handleMessage(msg); // 如果消息中没有 callback,那就执行 handleMessage(msg)
}
}

// 处理具体的 Message
public void handleMessage(Message msg) {
switch (msg.what) {
// 根据消息类型分发处理
default:
break;
}
}

当 Handler 接收到消息时,它会回调自己的 handleMessage(Message) 方法处理消息。


handleMessage(Message) 方法中,我们可以编写各种不同的逻辑,并对当前情况下的消息进行处理。这通常包括对消息类型的检查以及消息携带的数据的解析和操作。


当我们在 handleMessage(Message) 方法中完成了所有处理后,我们就可以将数据发送回发送消息的线程,或将数据传递给其他线程进行进一步处理。


总结


本篇文章深入探讨了 Handler 的原理,主要包括了消息队列和 Looper 的相关概念,以及消息的发送和处理。除此之外,还讲了当不同线程的消息需要在 Handler 中处理时,需要用到 Looper、MessageQueue 和 Handler 这三个关键组件的协同工作。


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

如何深入掌握 Android 系统开发的拦路虎 Binder

0. 为什么要深入学习 Binder Binder 是整个 Android 的基石 所有的系统服务都是基于 Binder,比如 AMS WMS PMS SurfaceFlinger Audiofilinger 以及硬件操作服务等等 Android 四大组件...
继续阅读 »

0. 为什么要深入学习 Binder



  • Binder 是整个 Android 的基石

    • 所有的系统服务都是基于 Binder,比如 AMS WMS PMS SurfaceFlinger Audiofilinger 以及硬件操作服务等等

    • Android 四大组件的底层实现离不开 Binder



  • 做系统开发需要自定义一些系统服务,这些工作需要我们了解 Binder

  • Android O 以后的 Treble 计划,基于 Binder 魔改出了 HwBinder VndBinder。

  • ANR 冻屏 卡顿 卡死等偶现 BUG 可能与 Binder 相关


1. 学习 Binder 的四个阶段



  • 会用,能添加 Java Native 系统服务

  • 熟读应用层各种情景下的源码

  • 熟读内核里面的数据结构和流程

  • 能解决各种奇奇怪怪的 bug


2. 准备工作


下载编译好 AOSP + Kernel,能通过自定义内核的方式启动虚拟机。


这部分内容比较简单,可以参考:



3. 预备基础知识


预备基础知识快速过一遍,忘了再回头再看



4. Binder 基本原理


首先要明确一点 Binder 是一个 RPC(Remote Procedure Call) 框架,也就是说借助于 Binder,我们可以在 A 进程中访问 B 进程中的函数。


4.1 IPC 原理


RPC 一般基于 IPC 来实现的,IPC 就是跨进程数据传输,大白话就是在 A 进程可以访问到 B 进程中的数据,或者说 B 进程中的数据可以传递给 A 进程,都是一个意思。


在 Linux 中,每个进程都有自己的虚拟内存地址空间。虚拟内存地址空间又分为了用户地址空间和内核地址空间。



不同进程之间用户地址空间的变量和函数是不能相互访问的。


使得 A 进程能访问到 B 进程中数据的手段我们就称之为 IPC。


虽然用户地址空间是不能互相访问的,但是不同进程的内核地址空间是相同和共享的,我们可以借助内核地址空间作为中转站来实现进程间数据的传输。


具体的我们在 B 进程使用 copy_from_user 将用户态数据 int a 拷贝到内核态,这样就可以在 A 进程的内核态中访问到 int a



更进一步,可以在 A 进程中调用 copy_to_user 可以将 int a 从内核地址空间拷贝到用户地址空间。至此,我们的进程 A 用户态程序就可以访问到进程 B 中的用户地址空间数据 int a



为了访问 int a ,需要拷贝两次数据。能不能优化一下?我们可以通过 mmap 将进程 A 的用户地址空间与内核地址空间进行映射,让他们指向相同的物理地址空间:



完成映射后,B 进程只需调用一次 copy_from_user,A 进程的用户空间中就可以访问到 int a了。这里就优化到了一次拷贝。


4.2 RPC 原理


接着我们来看以下,Binder 的 RPC 是如何实现的:


一般来说,A 进程访问 B 进程函数,我们需要:



  • 在 A 进程中按照固定的规则打包数据,这些数据包含了:

    • 数据发给那个进程,Binder 中是一个整型变量 Handle

    • 要调用目标进程中的那个函数,Binder 中用一个整型变量 Code 表示

    • 目标函数的参数

    • 要执行具体什么操作,也就是 Binder 协议



  • 进程 B 收到数据,按照固定的格式解析出数据,调用函数,并使用相同的格式将函数的返回值传递给进程 A。



Binder 要实现的效果就是,整体上看过去,进程 A 执行进程 B 中的函数就和执行当前进程中的函数是一样的。


5. Binder 应用层工作流程


Binder 是一个 RPC(Remote Procedure Call) 框架,翻译成中文就是远程过程调用。也就是说通过 Binder:



  • 可以在 A 进程中访问 B 进程中定义的函数

  • 进程 B 中的这些等待着被远程调用的函数的集合,我们称其为 Binder 服务(Binder Service)

  • 进程 A 称之为 Binder 客户端(Binder Client),进程 B 称之为 Binder 服务端(Binder Server)

  • 通常,系统中的服务很多,我们需要一个管家来管理它们,服务管家(ServiceManager) 是 Android 系统启动时,启动的一个用于管理 Binder 服务(Binder Service) 的进程。通常,服务(Service) 需要事先注册到服务管家(ServiceManager),其他进程向服务管家(ServiceManager) 查询服务后才能使用服务。

  • Binder 的 RPC 能力通过 Binder 驱动实现


通常一个完整的 Binder 程序涉及 4 个流程:



  1. 在 Binder Server 端定义好服务

  2. 然后向 ServiceManager 注册服务

  3. 在 Binder Client 中向 ServiceManager 获取到服务

  4. 发起远程调用,调用 Binder Server 中定义好的服务


整个流程都是建立在 Binder 驱动提供的跨进程调用能力之上:



6. Android Binder 整体架构


从源码实现角度来说,Binder 整体架构实现如下:



有点复杂,我们一点点说:




  • VFS 是内核中的一个中间层,向上对应用层提供统一的系统调用函数,这些系统调用函数主要是 open mmap ioctl write read ioctl 等,向下封装不同的外设(字符设备,块设备),系统文件,文件系统的操作。Binder 是一个字符驱动,当应用层调用到 binder 的 open mmap ioctl release 系统调用时,经过 vfs 的一层包装后,就会调用到 Binder 驱动中的 binder_open bider_mmap binder_ioctl binder_release 函数。




  • 不同于一般的驱动,Binder 应用层的使用要复杂不少,如果直接使用 open mmap ioctl release 系统调用会使得应用程序非常复杂且难以复用相同功能的代码,刚开始 google 的工程师做了一套简单的封装,把常用的操作封装为一系列的函数,这些函数都在 binder.c 中,ServiceManger 的就是通过 binder.c 中封装的函数实现的(Android10及以前)。源码中还存在一个 bctest.c 的程序,这个是 binder.c 的一个测试程序。C 语言级别的封装虽然简单,但使用起来还是稍显麻烦,很多细节也没有考虑进去,所以 google 的工程师又封装了一个叫 libbinder 的库,我们 native 层的 binder 服务端与客户端都是基于这个库来实现的,Java 层的 binder 服务端与客户端都是通过 JNI 间接使用 libbinder 库实现的,从使用上来说 libbinder 更为简单,但是 libbinder 本身比 binder.c 复杂了不少。




7. C 层实现分析


很多博客教程会忽略这一层的分析,相比 libbinder 库的封装,binder.c 会简单不少 ,方便初学者理解 binder 应用层工作流程。


我们可以模仿 bctest.c 写一个完整的 Binder 应用层 demo。


这个工作已经有大佬完成了:


github.com/weidongshan…


但是也有一些问题,这个代码是基于 Android5 的,稍微有点老了,我在以上实现的基础上做了一些修改和适配工作,使得代码可以在 Android10 上跑起来:


github.com/yuandaimaah…


关于这个示例程序的分析,可以参考以下几篇文章:



8. 驱动分析


驱动分析这部分结合 C 层应用的实现来分析驱动的实现,主要搞清楚:



  • 三个情景的流程:注册,获取,使用

  • 三个情景下内核中各种数据结构的变化


这部分内容可以参考之前分享的:



9. C++ 层分析


首先我们要写一个基于 libbinder 库的 Demo,能跑起来就行:



接着分析三个情景下的执行过程与各个函数的功能:



当然还有两个特殊的场景也需要进行分析:



  • 死亡通知

  • 多线程


这部分内容会陆续在公众号和掘金平台推送。


10. Java 层分析


学习这部分的前提是了解 JNI 编程。这个可以参考系列文章:



我们先写一个 Demo,能跑起来就行:



接着我们分析三个情景下的执行过程与各个函数的功能:



当然还有一些其他高级特性也需要我们分析,这部分内容会在后续推送:



  • AIDL 中 in out inout oneway 的分析

  • Parcel 数据结构分析

  • Java 层死亡通知

  • Java 层多线程分析


11. 疑难问题


不论是应用开发还是系统开发我们都会遇到一些棘手的 bug,很多时候这些 bug 都和 binder 有关,总结起来,大概可以分为几类:



  • 死锁

  • 线程池满了

  • 代理对象内存泄露

  • 传输数据过大

  • 关键方法内发起 Binder 同步调用导致卡顿

  • Android O 异步远程调用无限阻塞冻屏 bug


这类 bug 很多都难以复现,很多时候都不了了之了,导致拥有这部分经验的同学很少。


这部分内容工作量巨大,我会在接下来的时间陆续在公众号和掘金推送相关的文章。


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

Gson与Kotlin的老生常谈的空安全问题

问题出现 偶然在一次debug中发现了一个按常理不该出现的NPE,用以下简化示例为例:Exception in thread "main" java.lang.NullPointerException: Cannot invoke "kotlin.Lazy.g...
继续阅读 »

问题出现


偶然在一次debug中发现了一个按常理不该出现的NPE,用以下简化示例为例:

Exception in thread "main" java.lang.NullPointerException: Cannot invoke "kotlin.Lazy.getValue()" because "<local1>" is null

对应的数据模型如下:

class Book(  
val id: Int,
val name: String?
) {
val summary by lazy { id.toString() + name }
}

发生在调用book.summary中。第一眼我是很疑惑了,怎么by lazy也能是null,因为summary本身就是一个委托属性,所以看看summary是怎么初始化的吧,反编译为java可知,在构造函数初始化,这完全没啥问题。

public final class Book {
@NotNull
private final Lazy summary$delegate;
private final int id;
@Nullable
private final String name;

@NotNull
public final String getSummary() {
Lazy var1 = this.summary$delegate;
Object var3 = null;
return (String)var1.getValue();
}

...略去其他

public Book(int id, @Nullable String name) {
this.id = id;
this.name = name;
this.summary$delegate = LazyKt.lazy((Function0)(new Function0() {
// $FF: synthetic method
// $FF: bridge method
public Object invoke() {
return this.invoke();
}

@NotNull
public final String invoke() {
return Book.this.getId() + Book.this.getName();
}
}));
}
}


所以唯一的可能性就是构造函数并未执行。而这块逻辑是存在json的解析的,而Gson与kotlin的空安全问题老生常谈了,便立马往这个方向排查。


追根溯源


直接找到Gson里的ReflectiveTypeAdapterFactory类,它是用于处理普通 Java 类的序列化和反序列化。作用是根据对象的类型和字段的反射信息,生成相应的 TypeAdapter 对象,以执行序列化和反序列化的操作。
然后再看到create方法,这也是TypeAdapterFactory的抽象方法

  @Override
public <T> TypeAdapter<T> create(Gson gson, final TypeToken<T> type) {
Class<? super T> raw = type.getRawType();

if (!Object.class.isAssignableFrom(raw)) {
return null; // it's a primitive!
}

FilterResult filterResult =
ReflectionAccessFilterHelper.getFilterResult(reflectionFilters, raw);
if (filterResult == FilterResult.BLOCK_ALL) {
throw new JsonIOException(
"ReflectionAccessFilter does not permit using reflection for " + raw
+ ". Register a TypeAdapter for this type or adjust the access filter.");
}
boolean blockInaccessible = filterResult == FilterResult.BLOCK_INACCESSIBLE;

// If the type is actually a Java Record, we need to use the RecordAdapter instead. This will always be false
// on JVMs that do not support records.
if (ReflectionHelper.isRecord(raw)) {
@SuppressWarnings("unchecked")
TypeAdapter<T> adapter = (TypeAdapter<T>) new RecordAdapter<>(raw,
getBoundFields(gson, type, raw, blockInaccessible, true), blockInaccessible);
return adapter;
}

ObjectConstructor<T> constructor = constructorConstructor.get(type);
return new FieldReflectionAdapter<>(constructor, getBoundFields(gson, type, raw, blockInaccessible, false));
}

最后到了ObjectConstructor<T> constructor = constructorConstructor.get(type);这一句,这很明显是一个类的构造器,继续走到里面的get方法

  public <T> ObjectConstructor<T> get(TypeToken<T> typeToken) {
final Type type = typeToken.getType();
final Class<? super T> rawType = typeToken.getRawType();

// ...省略其他部分逻辑

// First consider special constructors before checking for no-args constructors
// below to avoid matching internal no-args constructors which might be added in
// future JDK versions
ObjectConstructor<T> specialConstructor = newSpecialCollectionConstructor(type, rawType);
if (specialConstructor != null) {
return specialConstructor;
}

FilterResult filterResult = ReflectionAccessFilterHelper.getFilterResult(reflectionFilters, rawType);
ObjectConstructor<T> defaultConstructor = newDefaultConstructor(rawType, filterResult);
if (defaultConstructor != null) {
return defaultConstructor;
}

ObjectConstructor<T> defaultImplementation = newDefaultImplementationConstructor(type, rawType);
if (defaultImplementation != null) {
return defaultImplementation;
}

...

// Consider usage of Unsafe as reflection,
return newUnsafeAllocator(rawType);
}

先来看看前三个Constructor,



  • newSpecialCollectionConstructor

    • 注释说是提供给特殊的无参的集合类构造函数创建的构造器,里面的也只是判断了是否为EnumSet和EnumMap,未匹配上,跳过



  • newDefaultConstructor

    • 里面直接调用的Class.getDeclaredConstructor(),使用默认构造函数创建,很明显看最上面的结构是无法创建的,抛出NoSuchMethodException



  • newDefaultImplementationConstructor

    • 里面都是集合类的创建,如Collect和Map,也不是




最后,只能走到了newUnsafeAllocator()

  private <T> ObjectConstructor<T> newUnsafeAllocator(final Class<? super T> rawType) {
if (useJdkUnsafe) {
return new ObjectConstructor<T>() {
@Override public T construct() {
try {
@SuppressWarnings("unchecked")
T newInstance = (T) UnsafeAllocator.INSTANCE.newInstance(rawType);
return newInstance;
} catch (Exception e) {
throw new RuntimeException(("Unable to create instance of " + rawType + ". "
+ "Registering an InstanceCreator or a TypeAdapter for this type, or adding a no-args "
+ "constructor may fix this problem."), e);
}
}
};
} else {
final String exceptionMessage = "Unable to create instance of " + rawType + "; usage of JDK Unsafe "
+ "is disabled. Registering an InstanceCreator or a TypeAdapter for this type, adding a no-args "
+ "constructor, or enabling usage of JDK Unsafe may fix this problem.";
return new ObjectConstructor<T>() {
@Override public T construct() {
throw new JsonIOException(exceptionMessage);
}
};
}
}

缘由揭晓


方法内部调用了UnsafeAllocator.INSTANCE.newInstance(rawType);
我手动尝试了一下可以创建出对应的实例,而且和通常的构造函数创建出来的实例有所区别


image.png
很明显,summary的委托属性是null的,说明该方法是不走构造函数来创建的,里面的实现是通过Unsafe类的allocateInstance来直接创建对应ClassName的实例。


解决方案


看到这便已经知道缘由了,那如何解决这个问题?


方案一


回到上面的Book反编译后的java代码,可以看到只要调用了构造函数即可,所以添加一个默认的无参构造函数便是一个可行的方案。改动如下:

class Book(
val id: Int = 0,
val name: String? = null
) {
val summary by lazy { id.toString() + name }
}

或者手动加一个无参构造函数

class Book(
val id: Int,
val name: String?
) {
constructor() : this(0, null)

val summary by lazy { id.toString() + name }
}

而且要特别注意一定要提供默认的无参构造函数,不然通过newUnsafeAllocator创建的实例就导致kotlin的空安全机制就完全失效了


方案二


用moshi吧,用一个对kotlin支持比较好的json解析库即可。


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

两个Kotlin优化小技巧,你绝对用的上

数据对象data object的支持 @Repeatable注解的优化 接下来就带大家介绍下上面三个特性。 一. 数据对象data object的支持 该特性由kotlin1.7.20插件版本提供,并处于实验阶段。 这个特性主要是和原来的object声明的...
继续阅读 »
  1. 数据对象data object的支持

  2. @Repeatable注解的优化


接下来就带大家介绍下上面三个特性。


一. 数据对象data object的支持


该特性由kotlin1.7.20插件版本提供,并处于实验阶段。



这个特性主要是和原来的object声明的单例类的toString()方法输出有关,在了解这个特性之前,我们先看下下面一个例子:

object Single1

fun main() {
println(Single1)
}

输出:



这个输出本质上就是一个类名、@、地址的拼接,有时候你想要打印输出的仅仅是类名,就得需要重写下toString()方法:

object Single1 {

override fun toString(): String {
return "Single1"
}
}

然后再看一个密封类的例子:

sealed interface Response {

data class Success(val response: String): Response

data class Fail(val error: String): Response

object Loading : Response

}

fun main() {
println(Response.Success("{code: 200}"))
println(Response.Fail("no net"))
println(Response.Loading)
}

输出:



可以看到,大家都是密封子类,但就这个Loading类的输出比较"丑陋",没有上面两个兄弟类的输出简洁清爽。


接下来我们就要介绍下主人公数据对象data object了,这个东西其实使用起来和object一模一样,核心的区别就是前者的toString() 更加简洁。


接下来从一个例子一探究竟:

data object Single2

fun main() {
println(Single2)
}

看下输出:



输出是不是比上面的object Single1更加简单明了。最重要的是在密封类中使用效果更加,我们把上面密封类Loading声明为data object

    data object Loading : Response

看下最终的输出结果:



这下子输出结果是不是清爽更多!!


讲完了应用,我们再java的角度看下其背后的实现机制,相比较于objectdata object会多了下面这三个重写方法:

public final class Single2 {

@NotNull
public String toString() {
return "Single2";
}

public int hashCode() {
return -535782198;
}

public boolean equals(@Nullable Object var1) {
if (this != var1) {
if (!(var1 instanceof Single2)) {
return false;
}

Single2 var2 = (Single2)var1;
}

return true;
}
}

我们需要关心的toString()方法就是直接重写返回了当前的类名。


如果想要使用这个特性,我们只需要增加如下配置即可:

compileKotlin.kotlinOptions {
languageVersion = "1.9"
}

二. @Repeatable注解优化


该特性由kotlin1.6.0插件版本提供优化。



在了解这个特性之前,我们先回忆下@Repeatable这个注解在java中的使用:


如果一个注解在某个方法、类等等上面需要重复使用,那就需要@Repeatable帮助。



  • 首先定义需要重复使用的注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Repeatable(Fruits.class)
public @interface Fruit {
String name();
String color();
}


  • 然后定义注解容器,用来指定可重复使用的注解类型
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Fruits {
Fruit[] value();
}

然后就可以在代码中这样使用:

@Fruits({
@Fruit(name = "apple", color = "red"),
@Fruit(name = "origin", color = "yellow"),
})
public class Detail {
}

大家有没有发现,可重复注解定义起来还是由一丢丢的麻烦,接下来轮到我们kotlin重磅出击了。先看下面一个例子:

@Repeatable 
annotation class Animal(val name: String)

在kotlin中我们只要声明一个需要重复使用的注解即可,kotlin编译器会自动帮助我们生成注解容器@Animal.Container,然后我们就能在代码中这样使用:

@Animal(name = "dog")
@Animal(name = "horse")
public class Detail {
}

是不是非常简单便捷了。


如果你偏要显示指明一个包含注解,也可以,通过以下方式即可实现:

@JvmRepeatable(Animals::class)
annotation class Animal(val name: String)

annotation class Animals(val value: Array<Animal>)

然后除了上面的使用方式,你在kotlin中还可以这样使用:

@Animals([Animal(name = "dog"), Animal(name = "dog")])
class Detail {
}

请注意:



  1. 如果非要显示声明一个注解容器,其属性的名称一定要为value

  2. 其次,注解容器和可重复性直接不能同时声明在同一个元素上;


另外,其实这个特性kotlin早就支持了,只不过kotlin1.6.0插件版本之前,kotlin这个特性只只支持RetentionPolicy.SOURCE生命周期的注解,并且还和java的可重复注解不兼容。


总结


这两个小技巧相信在大家日常开发中还是比较实用的,希望本篇能对你有所帮助。


参考文章:


Improved string representations for singletons and sealed class hierarchies with data objects


Repeatable annotations with runtime retention for 1.8 JVM target


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

Compose + Fragment是一个不错的选择

Compose很好用,但是在真正应用到项目时,我们还需要解决一些问题。 我要开发一个这样的页面,外层用Bottom Navigation Activity,每个tab对应的一个fragment,页面内容我用Compose来填充,不使用xml来布局,因为Comp...
继续阅读 »

Compose很好用,但是在真正应用到项目时,我们还需要解决一些问题。


我要开发一个这样的页面,外层用Bottom Navigation Activity,每个tab对应的一个fragment,页面内容我用Compose来填充,不使用xml来布局,因为Compose太好用了,如果不是页面缓存的原因,我可能会选择全部使用Compose来写。


我曾尝试使用material3的Navigation Bar来处理tab,但是每次点击tab后,页面总会重新加载,虽然功能实现了,但是这不是我想要的结果,我希望重新点回页面时,页面的位置、状态还是之前的样子。


这是最终的效果


Screenshot_20221221_192042.png


创建 Bottom Navigation Activity


New Project时,选择Bottom Navigation Activity,系统就会帮你创建一个带有3个tab页面的应用,此时都是正常的,但是我的应用需要有4个tab,当我将第4个tab加上去之后,我发现tab item,只有选中的,才会展示文字标题,就像下面这个样子。


Screenshot_20221221_1941322.png


那为什么会这样呢,其实是因为Android有意而为之,关于Bottom Navigation Item的设计规范,可以参考这里Bottom Navigation,那要怎么样,才能让所有的tab item都能够展示文字标题呢,只需要在onCreate中设置下显示模式即可

val navView: BottomNavigationView = binding.navView
navView.labelVisibilityMode = NavigationBarView.LABEL_VISIBILITY_LABELED

添加 Compose 到 fragment 里


现在的代码,又臭又长,让我们先把Frament对应的xml布局文件删掉,不需要在xml中写布局了,另外把Fragment中onCreateView也清理一下,填充上简洁的代码片段,如下

override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
return ComposeView(requireContext()).apply {
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setContent {
Text("HomeFragment")
}
}
}

瞬间整个世界都清净了,值得一提的是,ComposeView是普通视图和Compose视图的桥梁,起着至关重要的作用。


添加这段代码后,会报错,因为没有添加对应Compose库,等待Android Studio自动补全需要的库文件,补全之后,错误会消失,并且会在build.gradle添加好依赖。


这个时候运行项目,我们发现一个奇怪的问题,页面顶部会出现一部分空白,有一块白色区域


Screenshot_20221221_2012272.png


我们需要找到activity_main.xml文件,删除其中的这行代码,这个高度为56dp的空白就可以消失了。

android:paddingTop="?attr/actionBarSize"

填充列表页面


不得不说Compose实在比xml布局好用多了,写起来更像是写SwiftUI和Flutter。列表页面填充完之后,我发现列表最后一项,并不能完全展示,被Bottom Nav View给挡住了


Screenshot_20221221_2025262.png


怎么解决呢,我们还是需要找到activity_main.xml文件,将fragment的高度,由match_parent改为默认0dp,不要过早的撑满容器,这样页面就能够显示正常了

android:layout_height="0dp"

替换布局文件中的fragment


解决掉上面的那么些问题之后,这里还有一个黄色的小提示,Android Studio建议我们把fragment替换成FragmentContainerView,当我们根据建议点击替换后,再运行项目,我们发现应用崩溃了


Caused by: java.lang.IllegalStateException: Activity xx.xx.xx.MainActivity@7b7a278 does not have a NavController set on 2131231026


此时我们需要找到MainActivity,将其中的一行代码

val navController = findNavController(R.id.nav_host_fragment_activity_main)

替换为

val navHostFragment = supportFragmentManager.findFragmentById(R.id.nav_host_fragment_activity_main) as NavHostFragment
val navController = navHostFragment.navController

此时就一切都正常了,可以进行下一步了。


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

Kotlin 函数接口与普通接口的区别

记一次编写Demo时SonarLint提示警告而关注到的kotlin1.4新增的接口声明方式.// SonarLint警告: Make this interface functional or replace it with a function type. ...
继续阅读 »

记一次编写Demo时SonarLint提示警告而关注到的kotlin1.4新增的接口声明方式.

// SonarLint警告: Make this interface functional or replace it with a function type.
interface GitHubService {

@GET("search/repositories?sort=stars&q=Android")
suspend fun searchRepos(@Query("page") page: Int, @Query("per_page") perPage: Int): GitHubResponse

}


// 声明为函数接口后修复警告
fun interface GitHubService {

@GET("search/repositories?sort=stars&q=Android")
suspend fun searchRepos(@Query("page") page: Int, @Query("per_page") perPage: Int): GitHubResponse

}

普通接口


当使用 interface 关键字定义接口时,可以声明抽象方法和默认方法。抽象方法是没有具体实现的方法,需要在实现接口的类中提供具体的实现。默认方法是在接口中提供了一个默认的实现,实现类可以选择重写或者直接使用默认实现。

interface GitHubService {
fun getUser(username: String): User // 抽象方法

fun getRepositories(username: String): List<Repository> { // 默认方法
val user = getUser(username)
// 通过用户获取仓库列表的具体实现
// ...
return repositories
}
}

函数接口


使用 fun interface 声明的接口只能包含一个抽象方法,并且不能包含默认方法。这种类型的接口通常用于函数式编程和 lambda 表达式的场景。实现这个接口的类可以通过 lambda 表达式或者函数引用来提供方法的具体实现。

fun interface GitHubService {
fun getUser(username: String): User
}

// 通过 lambda 表达式为 getUser 方法提供了具体的实现。
// lambda 表达式接收一个用户名参数,并返回对应的用户对象。
val service = GitHubService { username ->
// 通过用户名获取用户的具体实现
// ...
return user
}

常见使用场景


interface



  1. 定义回调接口:接口可以用作定义回调函数的契约。一个类可以实现接口并提供回调方法的具体实现,然后将实现类的实例传递给其他需要回调的组件。




  2. 实现多态行为:接口可以作为多态的手段,使得不同的类可以以不同的方式实现相同的接口。这种多态的特性允许在运行时根据对象的具体类型调用相应的方法。




  3. 定义服务接口:接口可以定义服务契约,描述系统的服务功能,并规定服务方法的签名。其他模块或组件可以实现接口,并提供具体的服务实现。




  4. 定义插件机制:接口可以用于定义插件的扩展点。主应用程序定义接口,并提供默认实现,而插件可以实现这个接口并提供自定义的行为。




  5. 实现策略模式:接口可以用于实现策略模式,其中不同的类实现相同的接口,并提供不同的算法或策略。




fun interface



  1. 定义函数式接口:函数式接口只包含一个抽象方法,通常用于表示某个操作或行为。这样的接口可以作为函数类型的参数或返回值,使得函数可以被传递、组合和使用。




  2. 使用 lambda 表达式:函数式接口可以通过 lambda 表达式提供方法的具体实现。这种方式使得代码更加简洁、易读,并支持函数式编程的风格。




  3. 支持函数引用:函数式接口可以与函数引用一起使用,允许直接引用已有的函数作为接口的实现。这样可以减少冗余的代码,并提高代码的可读性。




总而言之,interface 关键字适用于一般的接口定义和多态行为,而 fun interface 关键字则适用于函数式编程和 lambda 表达式的场景。
总结一下,interface 关键字用于定义常规的接口,可以包含抽象方法和默认方法。而 fun interface 关键字用于定义函数式接口,只能包含一个抽象方法,并且不能包含默认方法。


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

🎉学算法在业务开发中到底有没有用?

前言 作为一名大学多次打铁的前ACM-ICPC竞赛选手,对这个问题应该算多少有点话语权,首先先说一下结论:有用。抛开面试不谈,算法的收益可能没有学某项技术那么明显,它算是潜移默化的增强你的思维方式,拓展思路,分析业务逻辑的时候可能会更加迅速,处理起复杂业务相对...
继续阅读 »

前言


作为一名大学多次打铁的前ACM-ICPC竞赛选手,对这个问题应该算多少有点话语权,首先先说一下结论:有用。抛开面试不谈,算法的收益可能没有学某项技术那么明显,它算是潜移默化的增强你的思维方式,拓展思路,分析业务逻辑的时候可能会更加迅速,处理起复杂业务相对更加得心应手一些。


如果要说在业务开发中使用过哪些算法,那基本可以说是使用不上,使用上的也是一些相对基础的算法,像什么并查集、最小生成树、最短路径、图论等等当时学的时候抓耳挠腮的高级算法基本都用不到。


一些基础的算法还是能够经常遇到的,今天来盘一下业务开发中常见的算法。


桶排序


维基百科上的解释为,桶排序是一个排序算法,工作的原理是将数组分到有限数量的桶里。每个桶再个别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排序)。桶排序是鸽巢排序的一种归纳结果。当要被排序的数组内的数值是均匀分配的时候,桶排序使用线性时间O(n)


可以说是一堆废话了,根本看不懂,翻译成人话就是利用数组下标的有序性,通过空间换时间的思想,只要出现一次数就在数组对应的下标上+1,然后遍历数组中那些大于1即可。

public static void main(String[] args) {
int[] source = new int[]{8,3,7,12,5,6,9};
bucketSort(source);
}

public static void bucketSort(int[] array){
int[] reslut = new int[13];
for (int i = 0; i < array.length; i++) {
reslut[array[i]]++;
}

for (int i = 0; i < reslut.length; i++) {
for (int j = 1; j <= reslut[i]; j++) {
System.out.println(i);
}
}
}

这种思想在业务开发中经常会用到,可能不是在排序的场景,由于本身就很简单就不再举例子了。


DFS


Depth First Search深度优先搜索,简称DFS,通常会把数据抽象为一个树形结构,从根节点出发,按预定的顺序扩展到子节点,如果子节点还有子节点则继续递归这个过程,直到当前节点没有子节点。当到达最深的一个叶子结点后处理完当前节点逻辑,则需要返回上一个节点重新寻找一个新的扩展节点。如此搜索下去,直到找到目标节点,或者搜索完所有节点为止。


image.png


以这个树为例。假如要找的值为节点2,DFS会首先按一个路线走到不能再走,也就是0 -> 1 -> 3。因为节点3没有子节点了,DFS会回到上一级,也就是节点1的位置,然后按照另一条路走到黑。也就是0 -> 1 -> 3 -> 4。由于4没有子节点,DFS会回到节点1,然后节点1所有的子节点都已经去过了,于是乎再回到节点0,然后去到节点2,最终找到它,路线就是0 -> 1 -> 3 -> 4 -> 2。


我们可以用在一个真实的场景里,就拿文件夹与文件的结构,根据数据库三范式,我们简单定义一下文件夹与文件和文件夹关联关系

// 文件夹
public class Folder {
private String folderId;
private String parentId;
}

// 文件
public class File {
private String fileId;
private String fileName;
}

// 文件夹关联关系
public class FolderFielRel {
private String folderId;
private String fileId;
}


这样就定义出一个简单的文件夹结构实体,从结构上来看实现一个树形的逻辑还是很简单的。联表查询即可,用FolderFielRel关联出每个文件夹的文件,然后文件夹通过parentId组成树。


逻辑很清晰,但是如果产品老哥说每一层文件夹需要展示当前文件夹数量。很多同学肯定会脱口而出,让前端拿每一层的文件数量不完了吗。但如果老哥拿出不仅要当前文件夹的数量,还要知道本级文件夹及子级文件夹的文件数量,阁下应该怎么应对呢。


这时候就要DFS出手了,按照DFS的思路,如果要求文件夹本级及子级的文件数量,我们需要从以这个文件夹为根节点的子树的叶子结点开始处理。


我们假设最终VO为FolderTreeModel,并且已经组成了一个完整的树

public class FolderTreeModel {
private String folderId;
private String parentId;
private Integer fileCount;
private List<File> fileList;
private List<FolderTreeModel> subFolders;
}

dfs

private void dfsBuildFolderFileCount(Map<String, Integer> statBook,
FolderTreeModel rootTreeNode) {

List<Folder> sonTopicTrees = rootTreeNode.getSubFolders;
for (FolderTreeModel folderTreeModel : sonTopicTrees) {

this.dfsBuildFolderFileCount(statBook, folderTreeModel);
}

rootTreeNode.setFileCount(statBook.getOrDefault(rootTreeNode.getFolderId(), 0) + rootTreeNode.getFileCount());
statBook.put(rootTreeNode.getParentId(), statBook.getOrDefault(rootTreeNode.getParentId(), 0) + rootTreeNode.getFileCount());
}

statBook为记录每个文件夹的文件树,通过folder_id映射,当我们dfs到第一个叶子节点,我们把当前节点的文件数累加到当前节点的父级节点的id对应的文件数上。当整个dfs搜索结束之后每个节点的本级及子级的文件数都存在了statBook中。


BFS


bfs虽然好用,但他有个致命的弱点,时间复杂度高,按刚才的文件夹例子需要n*logn的复杂度。在一些性能要求较高的查询场景下基本上都不会用。


既然有深度优先搜索,那当然就得有广度优先搜索,广度优先搜索,顾名思义,跟深度的区别为处理数据以广度往外扩散,通常会借助队列先进先出的数据结构。


image.png


以上图为例也就是说,访问数据的过程为,1 -> 2,3 -> 4,5 -> 6。那么我们使用bfs来改写dfs中的统计文件夹树的代码。

private void buildFolderFileCount(Map<String, Integer> statBook,
FolderTreeModel rootTreeNode) {

LinkedBlockingQueue<FolderTreeModel> bfsQueue = new LinkedBlockingQueue<>(rootTreeNode.getSubFolders());

while (!bfsQueue.isEmpty()) {
FolderTreeModel firstObj = bfsQueue.poll();
statBook.put(firstObj.getParentId(), statBook.getOrDefault(firstObj.getParentId(), 0) + firstObj.getFileCount());
bfsQueue.addAll(firstObj.getSubFolders());
}
}

一波BFS下来,statBook的结果与DFS的结果一样。效率从n*logn直接飙升到n。除了这三个简单的算法之外实在想不到还有什么算法在日常的业务开发中使用的了。


算法的魅力还是很大的,大就大在学的时候难受的一比,用的时候拍案称奇,有时候在代码里露两手心里的成就感直接彪到Integer.MAX_VALUE


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

什么是序列化和反序列化?

1. 什么是序列化和反序列化? 序列化和反序列化是计算机科学中两个重要的概念,主要应用在数据存储和网络传输等场景。 序列化是将数据结构或对象状态转换为可以存储或传输的形式的过程。这种形式要求在重新构建原始对象时能够在其他环境或在程序运行的后续时间点使用。这个过...
继续阅读 »

1. 什么是序列化和反序列化?


序列化和反序列化是计算机科学中两个重要的概念,主要应用在数据存储和网络传输等场景。


序列化是将数据结构或对象状态转换为可以存储或传输的形式的过程。这种形式要求在重新构建原始对象时能够在其他环境或在程序运行的后续时间点使用。这个过程主要通过将对象的数据转化为字节流来实现,也可以将其转化为格式如 XML 或 JSON 的数据,以便在网络上进行传输或在磁盘上进行存储。


举个例子,假设你有一个复杂的数据结构,如一个包含多个字段和数组的对象。你不能直接将这个对象写入文件或通过网络发送。因此,你需要先将其转换为可以写入或发送的格式,这就是序列化。


反序列化是序列化的逆过程,也就是从一系列字节中提取出数据结构。在接收到序列化的数据(如从文件或网络)后,通过反序列化,可以将数据恢复为原始的对象或数据结构,从而可以在程序中使用。


以上述的例子,反序列化就是读取该文件或接收到的数据,并根据序列化时的格式将其恢复为原始的对象。


这两个过程在很多编程语言中都有内置的支持,例如在 Java 中,你可以使用 java.io.Serializable 接口来对对象进行序列化和反序列化;在 Python 中,你可以使用 pickle 模块进行序列化和反序列化;在 JavaScript 中,你可以使用 JSON 的 stringifyparse 方法进行序列化和反序列化等。


2. 在java中实现序列化和反序列化,为什么要实现Serializable接口?


Serializable 接口是一种标记接口,本身并没有定义任何方法,但是它向 JVM 提供了一个指示,表明实现该接口的类可以被序列化和反序列化。这意味着你可以将该类的对象转换为字节流(序列化),然后再将这个字节流转回为对象(反序列化)。


序列化的过程是 JVM 通过反射来完成的,它会查看对象的类是否实现了 Serializable 接口。如果没有实现,将会抛出一个 NotSerializableException 异常。


实现 Serializable 接口的主要原因如下:




  1. 允许 JVM 序列化对象:如上所述,JVM 只会序列化实现 Serializable 接口的对象。




  2. 表示类的实例可以被安全地序列化:实现 Serializable 接口的类表示它满足 JVM 对于序列化的要求。这不仅仅是类的实例可以被转换为字节流,还包括这个类的实例可以被反序列化,而且反序列化后的对象保持了原始对象的状态。




  3. 允许类的实例在 JVM 之间进行传输:序列化的一个重要应用是在网络应用或分布式系统中,允许对象在 JVM 之间进行传输。只有实现 Serializable 接口的对象才能通过网络进行传输。




  4. 持久化:序列化也被用于将对象的状态持久化,即将对象存储在数据库、文件或内存中,然后在需要的时候再进行恢复。实现 Serializable 接口的对象可以被持久化。




综上,实现 Serializable 接口是为了使对象可以被序列化和反序列化,以便在不同的环境或时间点恢复对象的状态,或在 JVM 之间传输对象,或将对象的状态持久化。


3. 案例


当然可以。下面这个简单的例子中,我们将创建一个实现了 Serializable 接口的类 Person,然后进行序列化和反序列化:


首先,我们创建一个实现了 Serializable 接口的 Person 类:

import java.io.Serializable;

public class Person implements Serializable {
private static final long serialVersionUID = 1L;

private String name;
private int age;

public Person(String name, int age) {
this.name = name;
this.age = age;
}

public String getName() {
return name;
}

public int getAge() {
return age;
}

@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}

接下来,我们创建一个序列化这个 Person 对象的类 SerializeDemo

import java.io.FileOutputStream;
import java.io.ObjectOutputStream;

public class SerializeDemo {
public static void main(String[] args) {
Person p1 = new Person("John Doe", 30);

try {
FileOutputStream fileOut = new FileOutputStream("/tmp/person.ser");
ObjectOutputStream out = new ObjectOutputStream(fileOut);
out.writeObject(p1);
out.close();
fileOut.close();
System.out.println("Serialized data is saved in /tmp/person.ser");
} catch (Exception e) {
e.printStackTrace();
}
}
}

现在我们来反序列化这个 Person 对象,创建一个类 DeserializeDemo

import java.io.FileInputStream;
import java.io.ObjectInputStream;

public class DeserializeDemo {
public static void main(String[] args) {
Person p = null;

try {
FileInputStream fileIn = new FileInputStream("/tmp/person.ser");
ObjectInputStream in = new ObjectInputStream(fileIn);
p = (Person) in.readObject();
in.close();
fileIn.close();
} catch (Exception e) {
e.printStackTrace();
}

System.out.println("Deserialized Person...");
System.out.println("Name: " + p.getName());
System.out.println("Age: " + p.getAge());
}
}

以上就是一个完整的 Java 序列化和反序列化的例子。首先我们创建了一个 Person 对象并序列化到一个文件中,然后我们从这个文件中读取数据并反序列化回 Person 对象。


作者:一只爱撸猫的程序猿
链接:https://juejin.cn/post/7247740398563000380
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

流量思维的觉醒,互联网原来是这么玩的

流量就是钱,这是一个很原始的认知。但最开始我并不清楚流量和钱之间是如何相互转化的。 微创业,认知很低 大学时期,不管是出于积累项目经验、还是折腾新技术的需要,我有做过一个相对完整的项目。 没记错的话,应该是在20年10月份启动的。当时在宿舍里买了一台激光打印机...
继续阅读 »

流量就是钱,这是一个很原始的认知。但最开始我并不清楚流量和钱之间是如何相互转化的。


微创业,认知很低


大学时期,不管是出于积累项目经验、还是折腾新技术的需要,我有做过一个相对完整的项目。


没记错的话,应该是在20年10月份启动的。当时在宿舍里买了一台激光打印机,做起了点小买卖。所以就发现如果我手动给同学处理订单会非常麻烦。他们把文件通过qq发给我,我这边打开,排版,确认格式没有问题之后算一个价格,然后打印。


所以根据痛点,我打算开发一个线上自助下单,商户自动打印的一整套系统。


百折不挠,项目终于上线


21年年中克服各种困难终于实现整套系统,提供了小程序端,商户客户端,web端。


用户在手机或网页上上传文件后会自动转换为pdf,还提供了在线预览,避免因为格式与用户本地不同的纠纷。可以自由调节单双面、打印范围、打印分数、色彩等参数。实时算出价格,自助下单。下单后服务器会通知商户客户端拉取新任务,拉取成功后将文件丢入打印队列中。打印完成后商户客户端发送信息,并由服务器转发,告知用户取件。


image.png


image.png


大三下学期,宿舍里通过线上平台,在期末考试最忙那段期间经过了“订单高峰”的考验,成交金额上千块钱。看着我商户端里面一个个跳动的文件,就像流入口袋里的💰,开心。


商业化的很失败


没想到,我自己就是我最大的客户。


期末考完,其实想拉上我的同学大干一场,让校里校外的所有的商户,都用上我们的软件,多好的东西啊。对于盈利模式的概念非常模糊,同时也有很强的竞品。我的同学并不看好我。


我对商业化的理解也源自美团模式,美团是外卖的流量入口,所以对商户抽佣很高。滴滴是打车的流量入口,对司机的抽佣也很高。所以我认为,假设我未来成为了自助打印的流量入口,那应该也可以试试抽佣模式。


而且就算我不能为商户引流,也能解放他们的双手。


当时的我,一个人做技术,做UI,还要做商业计划,去地推,真的搞得我精疲力尽。反正后面觉得短期内变现无望,就去腾讯实习了。


其实也推广了2个商户,但是他们因为各种原因不愿意用。一个是出于隐私合规风险的考虑,一个是订单量少,不需要。


所以基本这个自助打印只能框死在高校。大学生打印的文件私密性很低,但是单价低,量多,有自助打印的需求。还有一部分自助打印的场景是在行政办事大厅,这种估计没点门门道道是开不进去的。


看不懂的竞品玩法


商户通过我的平台走,我这边并不无本万利。


因为开通了微信支付、支付宝支付,做过的小伙伴应该都知道办这些手续也会花一些钱,公司还要每年花钱养。还有需要给用户的文档成转换成pdf,提供在线预览,这很消耗算力和带宽,如果用户的成交单价非常低,哪怕抽佣5%都是亏的。比如用户打印了100份1页的内容,和打印了1份100页的内容,对我来说成本差别很大,前者很低,后者很高。


当时学校里已经有一部分商户用上自助打印了。一共有3个竞品。


竞品A:不抽佣,但是每笔订单对用户收取固定的服务费,界面简陋,有广告。


竞品B:不抽佣,不收用户的服务费,界面清爽无广告。


竞品C:彻彻底底走无人模式,店铺内基本没有老板,店铺是自营或加盟的。


前期缺乏市场调研,后期缺乏商业认知


当时我在没有摸清自己商业模式,市场调研也没怎么做好的情况下。一心想的就是先把东西做出来再说,卖不成自己还能学到技术。毕竟技术这个玩意不在项目里历练,永远都是纸上谈兵。所以对于商业化的设想就是搞不成就不搞了。


我当时的想法就是要“轻”运营,就是最好我的利润是稳定的,不会亏损的。商户如果要用就得每笔订单都给我一笔钱。


后面为了补齐和竞品的功能差距,也耗费了大量心力。让我把项目从一个大学课程设计,变成了一个有商业化潜力的产品。


竞品玩法的底层逻辑


商业化的时候,就发现这个市场还是蛮卷的,不可能直接和商户收钱。竞品B不仅免费,还想着帮商户创造额外收入,做“增益”。那我确实是没有精力去对抗的。


我当时也没搞懂自己的定位,我究竟是tob还是toc。当时想着我精心设计的界面,怎么可以被广告侵蚀?那可是我的心血。所以一心想把产品体验做的比竞品好,就会有人用。但这个定位也很模糊,因为如果商户不用你的,用户怎么可能用你的下单呢。


其实应该to rmb。面向利润开发。美,是奢侈品,那是属于我内心的一种追求,但他很难具有说服力让商户使用。在国内的各种互联网产品,不盈利的产品最后都是越来越粗糙,越来越丑的,都要降本增效。而rmb是必需品,如果不能为各方创造价值,那就没有竞争力。


所以后续分析了一下各家的玩法:


竞品A:传统商业模式,依靠用户强制付费和广告,市占率一般,和第一差了10倍数量级。


竞品B:烧钱模式,免费给商户用,免费给用户用,自己想办法别的渠道做增益,还要补贴商户。市占率第一。先圈地,再养鱼,变现的事之后再说。


竞品C:不单单做打印软件,卖的是项目。一整套自助打印店的解决方案,不知道店铺能不能赚钱,但是可以先赚加盟商的钱。这个对商业运作的要求会很高,我一时半会做不了。


大佬指点了一下我


他说,你看现在什么自助贩卖机,其实就是一个流量入口。至于别的盈利不盈利再说,但是流量是值钱的。


我最近去查阿拉丁指数,了解到了买量和卖量的观念,重新认识了流量,因为知道价格了。


买量和卖量是什么?


买量说的就是你做了一个app,花钱让别人给你引流。


卖量就是你有一个日活很高的平台,可以为别人引流。


买量和卖量如何结算?


一般分为cpc和cpa两种计价方式。前者是只要用户点击了我的引流广告,广告主就得掏钱。后者是用户可能还需要注册并激活账号,完成一系列操作才掏钱。


一般价格在0.1-0.3元,每次引流。


后面我查了一下竞品B在卖量,每天可以提供10-30w的uv,单次引流报价0.1元。也就是理想情况下,每天可以有1-3w的广告费收入。


侧面说明了竞品B的市占率啊,在这个细分市场做到这个DAU……


关于流量,逆向思维的建立


流量是实现商业利益的工具。


工具类应用通过为别人引流将流量变现,内容类应用通过电商将流量变现的更贵。


依靠流量赚钱有两种姿势,主动迎合需求,和培养需求。前者就是你可以做一些大家必须要用的东西,来获得流量。比如自助打印小程序,只要商户接入了,那么他的所有顾客都会为这个小程序贡献流量。比如地铁乘车码,所有坐地铁的人都会用到,比如广州地铁就在卖量,每天有几百万的日活。


培养需求就是做自己看好的东西,但是当下不明朗,尝试发掘用户潜在的需求。


流量,如果不能利用好,那就是无效流量。所以正确的姿势是,发掘目标人群 -> 设计变现方案 -> 针对性的开发他们喜欢的内容或工具 -> 完成变现。而不是 自己发现有个东西不错 -> 开发出来 -> 测试一下市场反应 -> 期盼突然爆红,躺着收钱。


研究报告也蛮有意思,主打的就是一个研究如何将用户口袋里的钱转移到自己口袋里。做什么产品和个人喜好无关,和有没有市场前景相关。


互联网是基于实体的


互联网并不和实体脱钩,大部分平台依赖广告收入,但广告基本都是实体企业来掏钱。还有电商也是,消费不好,企业赚不到钱,就不愿意投更多推广费。


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

你可能一直在kt文件中写Java代码

关注 Kotlin 的大多数开发中可能都是 Android 开发者吧,大家基本也都是慢慢从 Java 逐步迁移到 Kotlin。 得益于 Kotlin 与 Java 之间良好的互通性,有的时候可能我们写代码还是比较随性的,尤其是依旧按照自己过去写 Java 的...
继续阅读 »

关注 Kotlin 的大多数开发中可能都是 Android 开发者吧,大家基本也都是慢慢从 Java 逐步迁移到 Kotlin。


得益于 Kotlin 与 Java 之间良好的互通性,有的时候可能我们写代码还是比较随性的,尤其是依旧按照自己过去写 Java 的编程习惯,书写 Kotlin 代码。


但实际上 Kotlin 与 Java 之间编码风格还是有很大的差异的,你的代码可能还是 Java 的咖啡味。


空判断


你大概早就听腻了 Kotlin 的空安全,可是你在代码里是否还在写if (xx != null) 这样满是咖啡味的代码呢?


现在把你的空判断代码都删除掉吧。使用 ?. 安全调用来操作你的对象。

// before
fun authWechat() {
if (api != null) {
if (!api.isWXAppInstalled) {
ToastUtils.showErrorToast("您还未安装微信客户端")
return
}
val req = SendAuth.Req()
req.scope = "snsapi_userinfo"
req.state = "none"
api.sendReq(req)
}
}

这段代码粗略看没什么问题吧,判断 IWXAPI 实例是否存在,存在的话判断是否安装了微信,未安装就 toast 提示


但是更符合 Kotlin 味道的代码应该是这样的

// after
fun authWechat() {
api?.takeIf { it.isWXAppInstalled }?.let {
it.sendReq(
SendAuth.Req().apply {
scope = "snsapi_userinfo"
state = "none"
}
)
} ?: api?.run { ToastUtils.showErrorToast("您还未安装微信客户端") }
}

使用?.安全调用配合 ?: Elvis 表达式,可以覆盖全部的空判断场景,再配合 takeIf 函数,可以让你的代码更加易读(字面意思上的)


上述代码用文字表达其实就是:


可空对象?.takeIf{是否满足条件}?.let{不为空&满足条件时执行的代码块} ?: run { 为空|不满足条件执行的代码块 }


这样是不是更加符合语义呢?


作用域


还是上面的例子,实例化一个req对象

val req = SendAuth.Req()
req.scope = "snsapi_userinfo"
req.state = "none"

更有 Kotlin 味道的代码应该是:

SendAuth.Req().apply {
scope = "snsapi_userinfo"
state = "none"
}

使用apply{} 函数可以帮我们轻松的初始化对象,或者配置参数,它更好的组织了代码结构,明确了这个闭包处于某个对象的作用域内,所有的操作都是针对这个对象的。


在 Kotlin 的顶层函数中,提供了数个作用域函数,包括上文中的 let 函数,他们大同小异,具体的使用其实更多看编码风格的取舍,例如在我司我们有如下约定:




  • apply{} 用于,修改、配置对象




  • with(obj){} 用于,读取对象的字段,用于赋值给其他变量


    with() 可以显式的切换作用域,我们常将它用于某个大的闭包内,实现局部的作用域切换,


    而且仅用作读时无需考虑作用域的入参命名问题 (多个嵌套的作用域函数往往会带来it的冲突)




  • let{} 用于配合?.用于非空安全调用,安全调用对象的函数




  • run{} 执行代码块、对象映射


    run 函数是有返回值的,其返回值是 block块的最后一行,所以它具备对象映射的能力,即将当前作用域映射为另外的对象




  • also{} 对象,另作他用




当出现超过两行的同一对象使用,无论是读、写,我们就应该考虑使用作用域函数,规范组织我们的代码,使之更具有可读性。


这几个函数其实作用效果可以互相转换,故而这只关乎编码风格,而无关对错之分。


?: Elvis 表达式

非空赋值



虽然说在 Kotlin 中可空对象,使用 ?. 可以轻松的安全调用,但是有的时候我们需要一个默认值,这种情况我们就需要用到 ?: Elvis 表达式。


例如:

val name: String = getName() ?: "default"

假如 getName() 返回的是一个 String? 可空对象,当他为空时,通过 ?: Elvis 表达式直接给予一个默认值。


配合 takeIf{} 实现特殊的三元表达式


总所周知,kotlin 中没有三元表达式 条件 ? 真值 : 假值,这一点其实比较遗憾,可能是因为 ? 被用作了空表达。


在kotlin 中我们如果需要一个三元表达该怎么做呢?if 条件 真值 else 假值,这样看起来也很简洁明了。


还有一种比较特殊的情况,就是我们判断逻辑,实际上是这个对象是否满足什么条件,也就是说既要空判断,又要条件判断,返回的真值呢又是对象本身。


这种情况代码可能会是这样的:

fun getUser(): User? = null
fun useUser(user: User) {}
// 从一个函数中获得了可空对象
val _userNullable = getUser()
// 判断非空+条件,返回对象或者构造不符合条件的值
val user =  if (_userNullable != null && _userNullable.user == "admin") {
   _userNullable
} else {
   User("guess")
}
//使用对象
useUser(user)

这个语句如果我们将if-else塞到 useUser() 函数中作为三元也不是不可以,但是看起来就比较乱了,而且我们也不得不使用一个临时变量_userNullable


如果我们使用 ?: Elvis 表达式 配合 takeIf{} 可以看起来更为优雅的表达

fun getUser(): User? = null
fun useUser(user: User) {}
// 使用`?:` Elvis 表达式简化的写法
useUser(getUser()?.takeIf { it.user == "admin" } ?: User("guest"))

这看起来就像是一个特殊的三元 真值.takeIf(条件) ?: 假值,在这种语义表达下,使用?: Elvis 表达式起到了简化代码,清晰语义的作用。


提前返回


当然 ?: Elvis 表达式还有很多其他用途,例如代码块的提前返回

fun View.onClickLike(user: String?, isGroup: Boolean = false) = this.setOnClickListener {
user?.takeUnless { it.isEmpty() } ?: return@setOnClickListener
StatisticsUtils.onClickLike(this.context, user, isGroup)
}

这里我们对入参进行了非空判断与字符长度判断,在?: Elvis 表达式后提前 return 避免了后续代码被执行,这很优雅也更符合语义。


这里不是说不能用 if 判断,那样虽然可以实现相同效果,但是额外增加了一层代码块嵌套,看起来不够整洁明了。


这些应用本质上都是利用了 ?: Elvis 表达式的特性,即前者为空时,执行后者。


使用函数对象


很多时候我们的函数会被复用,或者作为参数传递,例如在 Android 一个点击事件的函数可能会被多次复用:

// before
btnA.setOnClickListener { sendEndCommand() }
btnB.setOnClickListener { sendEndCommand() }
btnC.setOnClickListener { sendEndCommand() }

例如这是三个不同帧布局中的三个结束按钮,他们对于的点击事件是同一个,这样写其实也没什么问题,但是他不够 Kotlin 味,我们可以进一步改写

btnA.setOnClickListener(::sendEndCommand)
btnB.setOnClickListener(::sendEndCommand)
btnC.setOnClickListener(::sendEndCommand)

使用 :: 双冒号,将函数作为函数对象直接传递给一个接收函数参数的函数(高阶函数),这对于大量使用高阶函数的链式调用场合更加清晰明了,也更加函数式


ps:这里需要注意函数签名要对应,例如setOnClickListener 的函数签名是View->Unit,故而我们要修改函数与之一致

@JvmOverloads
fun sendEndCommand(@Suppress("UNUSED_PARAMETER") v: View? = null) {

}

使用 KDoc


你还在用 BugKotlinDocument 这样的插件帮你生成函数注释么?你的函数注释看起来是这样的么?

/**
* 获取全部题目的正确率,x:题目序号,y:正确率数值(float)
* @param format ((quesNum: Int) -> String)? 格式化X轴label文字
* @param denominator Int 计算正确率使用的分母
* @return BarData?
*/

这样的注释看起来没什么问题,也能正确的定位到代码中的参数,但实际上这是 JavaDoc ,并不是 KDoc,KDoc使用的是类似 Markdown 语法,我们可以改写成这样:

/**
* 获取全部题目的正确率的BarData,其中,x:题目序号,y:正确率数值(float)。
* [format] 默认值为null,用于格式化X轴label文字,
* [denominator] 除数,作为计算正确率使用的分母,
* 返回值是直接可以用在BarChart中的[BarData]。
*/

KDoc 非常强大,你可以使用 ``` 在注释块中写示例代码,或者JSON格式


例如:

/**
* 使用json填充视图的默认实现,必须遵循下面的数据格式
* ```json
* [
* {"index":0,"answer":["对"]},
* {"index":1,"answer":["错"]},
* {"index":2,"answer":["对"]},
* ]
* ```
* [result] 必须是一个JSONArray字符串
*/

在AS中他会被折叠成非常美观的注释块:


image.png






写在最后


文章最后我们看一段 ”Java“ 代码与 Kotlin 代码的对比吧:

// before
override fun onResponse(
   call: Call,
   response: Response
) {
   val avatarPathResult = response.body()
   if (avatarPathResult != null) {
       val status = avatarPathResult.status
       if (status == 200) {
           val data = avatarPathResult.data
           MMKVUtils.saveAvatarPath(data)
      } else {
           MMKVUtils.saveAvatarPath("")
      }
  } else {
       MMKVUtils.saveAvatarPath("")
  }
}

// after
override fun onResponse(
   call: Call,
   response: Response,
) {
   with(response.body()) {
       MMKVUtils.saveAvatarPath(this?.data?.takeIf { status == 200 } ?: "")
  }
}

鉴于有些同学对本文的观点有一些疑惑,这里我贴上 JetBrains 官方开发的 Ktor 项目中对各种语法糖使用的统计(基于 main 分支,23-6-9)


语句计数
if.*!= null331
if.*== null216
.let {}1210
?.let {}441
.apply {}469
?.apply {}11
run {}37
with\(.*\) \{219
.also{}119
?:1066
?.1239
.takeIf54
\.takeIf.*\?:13
.takeUnless2


这个项目可以说很能代表 JetBrains 官方对 Kotlin 语法的一些看法与标准了吧,前文我们也说了,如何取舍只关乎编码风格,而无关对错之分。


用 Java 风格是错的吗?那自然不是,只是显然空判断与安全调用两者相比,安全调用更符合 Kotlin 的风格。


重复的写对象名是错误的么?自然也不是,只是使用 apply 更优雅更 Kotlin。


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

收起阅读 »

编译优化跌落神坛

最近在一次技术分享中,有网友问我小公司可以考虑做哪些编译优化?我觉得这个课题也还是挺有必要展开下讲讲的。 编译优化方面其实我个人觉得并不一定是特别高大上的东西,除了一些特别深水区的地方,还是有些东西还是能从细微处进行展开的。今天我们就尝试下拉他下水。 组件化 ...
继续阅读 »

最近在一次技术分享中,有网友问我小公司可以考虑做哪些编译优化?我觉得这个课题也还是挺有必要展开下讲讲的。


编译优化方面其实我个人觉得并不一定是特别高大上的东西,除了一些特别深水区的地方,还是有些东西还是能从细微处进行展开的。今天我们就尝试下拉他下水。


组件化


组件化和编译优化有啥关系? 有些人甚至觉得可能会拖慢整个工程编译速度吧。


在我的认知中gradle是一个并行编译的模式,所以当我们的工程的taskGraph确定之后,很多没有依赖关系的模块是可以并行编译的。这样的情况下我们就可以充分的使用并行编译的能力。


而且从另外一个角度gradle build cache出发,我们可以拥有更细致的buildcache。如果当前模块没有变更,那么在增量过程中也可以变得更快。


巧用DI或者SPI


以前我其实挺反感DI(依赖注入)的,我认为会大大的增加工程的复杂度。毕竟掌握一门DI(依赖注入)还是挺麻烦的。


但是最近我突然想通了,我之前看GE(Gradle Enterprise)的时候,发现有些业务模块之间有依赖关系,导致了这些模块的编译顺序必须在相对靠后的一个状态,但是因为com.android.application会依赖所有业务模块,这个时候就会触发水桶理论,该模块的编译就是整个水桶最短的短板。也让整个工程的并行编译有一段时间不可用。


那么这种问题我们可以通过DI(依赖注入)或者SPI(服务发现)的形式进行解决。模块并不直接依赖业务的实现而是依赖于业务的一个抽象接口进行编程,这样可以优化既有工程模块间的依赖关系。


道理虽然简单,但是真的去抽象也会考验对应开发的代码能力。


关注AGP优化方案


这部分我觉得是很容易被开发遗忘的,比如我们最近在做的buildConfig,AIDL编译时默认关闭,还有去年到今年一直在进行的非传递R文件的改造。官方的Configuration Cache,还有后续的全局AGP通用属性,还有默认关闭Jetfied等等,这些跟随AGP迭代的属性。


结果上看关闭非必要模块的buildConfig AIDL可以让全量编译时间缩短大概2min,当然主要是我们模块多。而非传递R可以让我们的工程的R文件变更的增量缓存更好管理。


Kotlin技术栈更新


kt已经发布很长时间了,最近最让我期待的是kt2.0带来的K2的release版本。能大大的提升kotlin compiler编译速度。


另外还有kt之前发布的ksp,是一个非常牛逼的kapt的替代方案。而且官方也在逐步对ksp进行支持。比如room这个框架就已经适配好了ksp。我自己也写过好几个ksp插件。我个人认为还是非常酷的。


最后还有一些废弃东西的下架,比如KAE(kotlin-android-extensions)这种已经被明确说是后续不继续进行支持的框架。我们最近尝试的方案是通过Android Lint把所有声明KAE的进行报错处理。剩下的就是业务自行决定是改成findViewById还是viewBinding



KAEDetector



花点钱接个GE


如果这个老板不太差这么点钱,我真的觉得GE(Gradle Enterprise)是个非常好的选择。可以很直观的看出一些工程的编译问题,而且对接的gradle同学也都很专业。


不要轻易魔改Gradle


非必要的情况下,个人是不太建议同学们改这个的。非常容易破坏整个编译缓存系统!这里不仅仅只针对Transform,还有对任意编译产物进行修改的,比如xml,资源等等。当然如果可以的话,尽可能的使用最新AGP提供的一些新的api去进行修改吧。这个可以多参考下2BAB大神的一些文章。


另外比如很多非必要的Transform转化建议本地编译的时候就直接关闭了,虽然可能会出现一些本地行为不一致的情况,但是可以大大的优化一些编译速度。字节码不是炫技的工具,谨记!


总结


最近基本没有做一些特别适合分享的内容,文章也就没有更新了。各位大佬们体谅啊,抱拳了。


在下封于修,前来讨教。既分生死,也决高下。


image.png


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

匿名内部类为什么泄漏,Lambda为什么不泄漏

在Android开发中,内存泄露发生的场景其实主要就两点,一是数据过大的问题,而是调用与被调用生命周期不一致问题,对于对象生命周期不一致导致的泄漏问题占90%,最常见的也不好分析的当属匿名内部类的内存泄漏,在文章《# 内存泄漏大集结:安卓开发者不可错过的性能优...
继续阅读 »

在Android开发中,内存泄露发生的场景其实主要就两点,一是数据过大的问题,而是调用与被调用生命周期不一致问题,对于对象生命周期不一致导致的泄漏问题占90%,最常见的也不好分析的当属匿名内部类的内存泄漏,在文章《# 内存泄漏大集结:安卓开发者不可错过的性能优化技巧》 中我大概进行了总结,最近在开发时遇到了一个问题,就是LeakCannry 检测到的内存泄漏,LeakCannry检测的原理大概就是GC 可达性算法实现的,我们产品中最多的一个问题就是匿名内部类导致的。


案例不涉及持有外部类引用的状态下


匿名内部类如何导致内存泄漏


在Java体系中,内部类有多种,最常见的就是静态内部类、匿名内部类,一般情况下,都推荐使用静态内部类,那这是为什么呢,先看一个例子:

public class Test {
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {

}
}).start();
}
}


匿名内部类的泄漏原因:内部类持有外部类的引用,上述场景中,当外部类销毁时,匿名内部类Runnable 会导致内存泄漏,



验证这个结论


上述代码的class 文件通过Javap -c 查看后是这样的

Compiled from "Test.java"
public class Test {
public Test();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return

public static void main(java.lang.String[]);
Code:
0: new #2 // class java/lang/Thread
3: dup
4: new #3 // class Test$1
7: dup
8: invokespecial #4 // Method Test$1."<init>":()V
11: invokespecial #5 // Method java/lang/Thread."<init>":(Ljava/lang/Runnable;)V
14: invokevirtual #6 // Method java/lang/Thread.start:()V
17: return
}

我们直接看main 方法中的指令:

0: new #2 // 创建一个新的 Thread 对象 
3: dup // 复制栈顶的对象引用
4: new #3 // 创建一个匿名内部类 Test$1 的实例
7: dup // 复制栈顶的对象引用
8: invokespecial #4 // 调用匿名内部类 Test$1 的构造方法
11: invokespecial #5 // 调用 Thread 类的构造方法,传入匿名内部类对象
14: invokevirtual #6 // 调用 Thread 类的 start 方法,启动线程
17: return // 返回

我们可以看到,在第4步中 使用new 指令创建了一个Test$1的实例,并且在第8步中,通过invokespecial 指令调用匿名内部类的构造方法,这样一来生成的内部类就会持有外部类的引用,从而外部类不能回收,将导致内存泄漏。


Lambda为什么不泄漏


刚开始,我以为Lambda只是语法糖,不会有其他的作用,然而,哈哈 大家估计已经想到了,



匿名内部类使用Lambda 时不会造成内存泄漏。



看代码:

public class Test {
public static void main(String[] args) {
new Thread(() -> {

}).start();
}
}

将上面的代码改为Lambda 格式


class 文件:

Compiled from "Test.java"
public class Test {
public Test();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return

public static void main(java.lang.String[]);
Code:
0: new #2 // class java/lang/Thread
3: dup
4: invokedynamic #3, 0 // InvokeDynamic #0:run:()Ljava/lang/Runnable;
9: invokespecial #4 // Method java/lang/Thread."<init>":(Ljava/lang/Runnable;)V
12: invokevirtual #5 // Method java/lang/Thread.start:()V
15: return
}

第一眼看上去就已经知道了答案,在这份字节码中没有生成内部类,


在Lambda格式中,没有生成内部类,而是直接使用invokedynamic 指令动态调用run方法,生成一个Runnable对象。再调用调用Thread类的构造方法,将生成的Runnable对象传入。从而避免了持有外部类的引用,也就避免了内存泄漏的发生。


在开发中,了解字节码知识还是非常有必要的,在关键时刻,我们查看字节码,确实能帮助自己解答一些疑惑,下面是常见的一些字节码指令


常见的字节码指令


Java 字节码指令是一组在 Java 虚拟机中执行的操作码,用于执行特定的计算、加载、存储、控制流等操作。以下是 Java 字节码指令的一些常见指令及其功能:



  1. 加载和存储指令:



  • aload:从局部变量表中加载引用类型到操作数栈。

  • astore:将引用类型存储到局部变量表中。

  • iload:从局部变量表中加载 int 类型到操作数栈。

  • istore:将 int 类型存储到局部变量表中。

  • fload:从局部变量表中加载 float 类型到操作数栈。

  • fstore:将 float 类型存储到局部变量表中。



  1. 算术和逻辑指令:



  • iadd:将栈顶两个 int 类型数值相加。

  • isub:将栈顶两个 int 类型数值相减。

  • imul:将栈顶两个 int 类型数值相乘。

  • idiv:将栈顶两个 int 类型数值相除。

  • iand:将栈顶两个 int 类型数值进行按位与操作。

  • ior:将栈顶两个 int 类型数值进行按位或操作。



  1. 类型转换指令:



  • i2l:将 int 类型转换为 long 类型。

  • l2i:将 long 类型转换为 int 类型。

  • f2d:将 float 类型转换为 double 类型。

  • d2i:将 double 类型转换为 int 类型。



  1. 控制流指令:



  • if_icmpeq:如果两个 int 类型数值相等,则跳转到指定位置。

  • goto:无条件跳转到指定位置。

  • tableswitch:根据索引值跳转到不同位置的指令。



  1. 方法调用和返回指令:



  • invokevirtual:调用实例方法。

  • invokestatic:调用静态方法。

  • invokeinterface:调用接口方法。

  • ireturn:从方法中返回 int 类型值。

  • invokedynamic: 运行时动态解析并绑定方法调用


详细的字节码指令列表和说明可参考 Java 虚拟机规范(Java Virtual Machine Specification)


总结


为了解决问题而储备知识,是最快的学习方式。


在开发中,也不要刻意去设计invokedynamic的代码,但是Java开发的同学,Lambda是必选项哦


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

分享Android开发中常见的代码优化方案

前言 首先要做相关优化,就得先要大致清晰影响性能的相关因素,这样可以做针对性调优会比较有条理。 比较常见的性能调优因素有: 内存:Java 一般通过 JVM 对内存进行分配管理,主要是用 JVM 中堆内存来存储 Java 创建的对象。系统堆内存的读写速度非常...
继续阅读 »

前言


首先要做相关优化,就得先要大致清晰影响性能的相关因素,这样可以做针对性调优会比较有条理。


比较常见的性能调优因素有:



  • 内存:Java 一般通过 JVM 对内存进行分配管理,主要是用 JVM 中堆内存来存储 Java 创建的对象。系统堆内存的读写速度非常快,所以基本不存在读写性能瓶颈。但由于内存成本要比磁盘高,相比磁盘,内存的存储空间又非常有限。所以当内存空间被占满,对象无法回收时,就会导致内存溢出、内存泄露等问题。

  • 异常:抛出异常需要构建异常栈,对异常进行捕获和处理,这个过程非常消耗系统性能。

  • 网络:对于传输数据比较大,或者是并发量比较大的系统,网络就很容易成为性能瓶颈。

  • CPU: 复杂的计算,会长时间,频繁地占用cpu执行资源;例如:代码递归调用,JVM频繁GC以及多线程情况下切换资源都会导致CPU资源繁忙。


对以上这些因素可以在代码中做相关优化处理。


延迟加载(懒加载)优化


了解预加载



  • ViewPager控件有预加载机制,即默认情况下当前页面左右相邻页面会被加载,以便用户滑动切换到相邻界面时,更加顺畅的显示出来

  • 通过ViewPager的setOffscreenPageLimit(int limit)可设置预加载页面数量


介绍延迟加载


等页面UI展示给用户时,再加载该页面数据(从网络、数据库等),而不是依靠ViewPager预加载机制提前加载部分,甚至更多页面数据。可提高所属Activity的初始化速度,另一方面也可以为用户节省流量.而这种延迟加载方案已经被诸多APP所采用。


相关概括



  • 没有打开页面,就不预加载数据,当页面可见时,才加载所需数据。

  • 换句话说延迟加载就是可见时才去请求数据。

  • 实际应用开发中有哪些延迟加载案例:

    • ViewPager+Fragment 搭配使用延迟加载

    • H5网页使用延迟加载




ViewPager与Fragment延迟加载的场景



  • ViewPager中setOffscreenPageLimit(int limit)部分源码
//默认的缓存页面数量(常量)
private static final int DEFAULT_OFFSCREEN_PAGES = 1;

//缓存页面数量(变量)
private int mOffscreenPageLimit = DEFAULT_OFFSCREEN_PAGES;

public void setOffscreenPageLimit(int limit) {
//当我们手动设置的limit数小于默认值1时,limit值会自动被赋值为默认值1(即DEFAULT_OFFSCREEN_PAGES)
if (limit < DEFAULT_OFFSCREEN_PAGES) {
Log.w(TAG, "Requested offscreen page limit " + limit + " too small; defaulting to "+ DEFAULT_OFFSCREEN_PAGES);
limit = DEFAULT_OFFSCREEN_PAGES;
}

if (limit != mOffscreenPageLimit) {
//经过前面的拦截判断后,将limit的值设置给mOffscreenPageLimit,用于
mOffscreenPageLimit = limit;
populate();
}
}


  • 思路分析:Fragment中setUserVisibleHint(),此方法会在onCreateView()之前执行,当viewPager中fragment改变可见状态时也会调用,当fragment 从可见到不见,或者从不可见切换到可见,都会调用此方法,使用getUserVisibleHint() 可返回fragment是否可见状态。在onActivityCreated()及setUserVisibleHint()方法中都调一次lazyLoad() 方法。
public abstract class BaseMVPLazyFragment<T extends IBasePresenter> extends BaseMVPFragment<T> {
/**
* Fragment的View加载完毕的标记
*/
protected boolean isViewInitiated;
/**
* Fragment对用户可见的标记
*/
protected boolean isVisibleToUser;
/**
* 是否懒加载
*/
protected boolean isDataInitiated;

...

/**
* 第一步,改变isViewInitiated标记
* 当onViewCreated()方法执行时,表明View已经加载完毕,此时改变isViewInitiated标记为true,并调用lazyLoad()方法
*/
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
isViewInitiated = true;
//只有Fragment onCreateView好了,
//另外这里调用一次lazyLoad()
prepareFetchData();
//lazyLoad();
}

/**
* 第二步
* 此方法会在onCreateView()之前执行
* 当viewPager中fragment改变可见状态时也会调用
* 当fragment 从可见到不见,或者从不可见切换到可见,都会调用此方法
*/
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
this.isVisibleToUser = isVisibleToUser;
prepareFetchData();
}

/**
* 第四步:定义抽象方法fetchData(),具体加载数据的工作,交给子类去完成
*/
public abstract void fetchData();

/**
* 第三步:在lazyLoad()方法中进行双重标记判断,通过后即可进行数据加载
* 第一种方法
* 调用懒加载,getUserVisibleHint()会返回是否可见状态
* 这是fragment实现懒加载的关键,只有fragment 可见才会调用onLazyLoad() 加载数据
*/
private void lazyLoad() {
if (getUserVisibleHint() && isViewInitiated && !isDataInitiated) {
fetchData();
isDataInitiated = true;
}
}

/**
* 第二种方法
* 调用懒加载
*/
public void prepareFetchData() {
prepareFetchData(false);
}

/**
* 第三步:在lazyLoad()方法中进行双重标记判断,通过后即可进行数据加载
*/
public void prepareFetchData(boolean forceUpdate) {
if (isVisibleToUser && isViewInitiated && (!isDataInitiated || forceUpdate)) {
fetchData();
isDataInitiated = true;
}
}
}

多线程优化: 建议使用线程池


用线程池的好处


可重用线程池中的线程,避免频繁地创建和销毁线程带来的性能消耗;有效控制线程的最大并发数量,防止线程过大导致抢占资源造成阻塞;可对线程进行有效管理



  • RxJava,RxAndroid,底层对线程池的封装管理非常值得参考。

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

谈谈我的Google日历开发之路!

序言 介绍下自己,36岁,我是一名14年的Java服务器开发工程师。在小时候就有一个计算机的梦和愿景(小时候来在东莞,有一天家里人带在东莞长安步步高大道看步步高灯火通明,告诉我说那里面的人上班吹空调,工作只需要敲敲键盘就好了)-现在想来那时候他娘的就有996了...
继续阅读 »

序言


介绍下自己,36岁,我是一名14年的Java服务器开发工程师。在小时候就有一个计算机的梦和愿景(小时候来在东莞,有一天家里人带在东莞长安步步高大道看步步高灯火通明,告诉我说那里面的人上班吹空调,工作只需要敲敲键盘就好了)-现在想来那时候他娘的就有996了。从那时起这颗种子就埋下了。初中的时候还去报了电脑培训班,那里学会了DOS也学会了CS和热血传奇,同样也那以后自己也非常喜欢关注电脑,去新华书店看计算机相关的书。中间就不说了,说多了都是泪。终究如愿以偿自己还是走上了码农这条不归路-少壮不努力,老大搞IT。后面自己也是疯狂的一味的追求技术的宽度,深度。不管时C,C++,汇编,Js等语言(Java吃饭的家伙更不用说),还是Windows编程底层,网络编程,Linux核心翻了个底朝天(可是终究自己还是太年轻了,学的多懂得多与有没有工作之间差了可是一个NASA的哈勃空间望远镜的距离-没有好没学历,你说啥都没用)。转眼间自己已经是一个有着10多年经验的码农了,在这十多年期间,什么技术总监,经理,架构师咱都干过!但自己也是一直热衷于技术,从未放弃过,也知道自己喜欢一件事情想要放弃太困难,如果某一天谈到放弃这个词,心情会非常的艰难,甚至可能会哭的像个刚失恋的女孩一样!也曾有过自暴自弃,大部分的时间在撸游戏,放弃自己(因为在一个学历为王的时代,技术和能力显得那么的微不足道)。但奈何有一颗对技术钟爱的心,所以鞭抽着让自己一直在码代码这条路上一直走,不曾放下,也没有勇气去放下! 其实自己想过无数次,不走技术路还有其他路可走?多少个晚上都没有找到答案。但每每这样想后越发的发现自己更是无路可走,对未来的渺茫和害怕!也许正如《肖申克的救赎》所说-“体制化”真的很可怕,曾经尝试过问身边的人,现在的工作如果不干了会做什么,很多人的回答都是"去别的地方去干“,而事实上几乎不会再去想别的行业了,有的人甚至非常年轻。每个人都是自己的上帝,如果你自己都放弃自己了,还有谁会救你?每个人都在忙,有的忙着生,有的忙着死。忙着追逐名利的你,忙着柴米油盐的你,停下来想一秒:你的大脑,是不是已经被体制化了?你的上帝在哪里?


尝试着简单


很多时候也会三五好友聚在一起,在网吧开黑,在篮球场上畅快淋漓,他们之间有高中就闯荡江湖的,更有初中后就为生活奔波的。但在他们眼里看不到一丝的愁。在网吧肆意的笑声,球场上拼搏的样子,完全看不出一丝生活的不如意。或许是因为每个男人不得让别人看到自己的无助,也许总是要把好的一面留给朋友的每一次见面。生活谁都不易,可能是我要的太多了。但拿到篮球的那一刻是真的快乐了,发自内心的快乐。


的确该这样,我们应该追求自己初衷的东西。虽说爱好既不能饱腹,也不能裹体,更不能遮风挡雨,但每次我们在面对自己喜爱的事情时总是显得会更加自信。或许我该把自己的欲望都降低一点点,也许会变得更好。这里既没有肯定答案,更没有前人的经验来指导我。时间,人物,地点的变化都会让事物的变化都会让未来变得扑朔迷离。正如《复仇者联盟》 奇异博士预测未来和灭霸交手1400万次,只赢了一次那样。或者《萨利机长》 在计算机模拟成功降落的可能性,在避开所有的人为因素,时间因素,环境因素后。依旧模拟了17次后才成功的降落。与其每天活在幻想着如何成功,不如想清楚自己真的需要什么,也不必每天幻想能够去大厂挑战自我(或许这只是我想要更多的一个理由和借口,或许是最后的养老,至少大家都这么想的)但这估计比奇异博士打败灭霸1400万次的概率还要低吧。之后就是简单的过日子,柴米油盐酱醋茶,上班和下班,还有英雄联盟,绝地求生的陪伴。没有了远途的负担,生活和工作逐渐变得愈发的简单和平凡。


从10年到16年,这些年间一直在朋友的公司像青蛙跳荷叶游戏那样蹦来蹦去,估计整个职业生涯社保记录上的公司名称都能打全一张A4纸了。虽然很多时候自己也曾想过好好稳定,也有过稳定。也非常珍惜那一次稳定的机会。但奈何终究还是熬不过现实。或许大多数人和我一样,有些时候明明已经很努力了,工作也做得已经足够好了,可终究还是会留下很多的遗憾,却又无能为力。


突然间的醒悟


2016年,还是多少年来着。Google 公布了Plus关闭的消息。记得那时Google为了对抗Facebook ,Plus应运而生,他带着使命而来,可惜好景不长!很快就Google就不得已要关闭他了。而我却对Plus的钟爱有加,非常喜欢Plus的交互和体验,以及内容的呈现方式。对于Plus的关闭自己也很遗憾和惋惜,然后自己也萌生了一些想法-既然你关闭,我就开发一个出来自己玩! 这也许多年来第一次自己拿起了放下多年的自己了,以前忙着学技术,现在却想好好忙忙该如何做点东西了。


回到主题,话说plus关闭的日子越来越近,我也越来越迫不及待的要干一个plus出来。那年应该是16年吧,说干就干。虽说自己是服务器架构师,一直在Java这个世界里摸爬滚打,但一直相信技术是学来的,自己有这个学习能力去面对这些问题。而事实也是,我总能在工作中表现得游刃有余,任何一份工作都非常的顺利(所以几乎工作中没有过996,不管是研发还是管理)。在决定干plus,也用技术基础充分的证明了自己能做到!(刚好赶上16年从游戏公司离职)相信自己能做到、而后的日子就是每天像个屁股上长了钉子一样闷在房间里面研发Plus。时常也会玩游戏。花了接近1个半月的时间(平常还要玩游戏),做了3个版本,终于干出来了-这里为什么要做3个版本,因为每个版本做完后,发现体验和Google
plus体验相差悬殊,做完后总是不满意,所以为了保持一致,然后又重新开发。直到满意为止,最后却是让自己很满意!这种带来的成就感,结果却是让自己一发不可收拾,后面接连的把photos,mail也索性干了,sticky。曾经也有一段时间放在服务器上运行。终究发现这个东西水土不服,或者说就是我的一次心血来潮吧,运行了半年之后服务器不再续费,也随之不了了之!


2016年年中,回到了工作岗位中,这一次又是朋友的公司。在这里的2年,就再也没涉及过这事,安安静静的工作着。


日历转折点


2018年底呆了2年多的公司卖转手给了另一个老板。而接手的老板在接触几个月后发现志不同道不合不相为谋。因此决然选择了离开。 刚好赶上年底,大概还有1个月左右的时间过年,想着自己也能早点回家,年后也能好好休息下。在接下来没有上班的日历里,回首了这几年开发的Plus,mail,sticky,photos产品,总觉得自己像一个没有长大的小孩,总是对新鲜事物充满着好奇,总是在一次一次的尝试尝试的路上(这里倒不如换个词玩),而每次玩都没有一个真正的结果。功能和交互上都已经很完整了,但从细节和体验他们缺失的太多,同时这些似乎和用户本身的需求离的太远了,这些都是我自己异想天开的,再加上开发的东西觉得欠缺了太多的体验,似乎他们从一开始貌似就决定了他们的结束,我只是享受了这个作的过程-这不是典型的找罪受? 这是一个产品真正的痛。当然我能找一个让自己全身而退的理由-一个人前后端,一个人测试,一个人产品,一个人还要设计,我已经做的够多的了,自己不免会苦笑一下,承认自己的失败又如何。正如《绝望主妇》所说的:失败并不意味着你浪费了时间和生命,而是表明你有理由重新开始。而后再一次决定需要开发一个大家能真正意义上使用的产品,真的,真的,真的告诫自己,要做一件有头有尾的事情了-日程管理。之所以决定做日程管理,因为工作中发现自己每天都会用记事本记录自己的日程,工作任务。然后写完一本后,就会扔掉,总是觉得可惜和有一些遗憾,本想着用国内的一些平台,但VIP让我痛心疾首。于是更加坚定了自己开发一个日程平台来!之前也一直有用Google日历和mac上的苹果日历,也用过国内一些日程管理平台,思来想去还是日历是我的菜,所以索性决定自己干个日历出来!另一个理由是放眼看去这个国内市场和行业,一个日程管理软件,都是VIP,包月,包季,包年,甚至SVIP。免费的不好用,好用的不免费。


日历的开始之路


在仿苹果还是Google之间犯了难。后面索性选择一致好评度高和难度更高的Google。在别人的眼里的看来选择玩这个可能有点上头,终究会啪啪打脸!但自己总觉得这玩意没难度,是别人思想高度不够,还是自己高估自己了。带着疑问找了一些一直坚守前端朋友,结果答案一致的标准-有难度。也许是怕伤我自尊心吧!


心中的热血沸腾告诉自己是时候开始日历开发之旅了。第一次的尝试,先是在房间内把自己关了3天,没挪动过屁股。对整个架构进行了详细的分析和验证,证明了方法的可行性后,花了一个礼拜的时间做了第一个版本出来,只是些核心日程事件处理-当然目标也只是一些核心功能,日程渲染。动手做后发现了整个结构体系存在很多的问题,支撑不了日历的复杂交互以及在复杂交互中会产生很多事件的冲突带来极大体验麻烦。兵来将挡水来土掩,体系架构上的问题难不倒一个架构师(架构思想不管服务器还是视图页面都是一样的)。在重新花2天时间对其重新整理和分析后,新的方案随之出来了、心也更加的澎湃和坚定了! 接下来用15天证明了方案是没问题的,整个日历也是如期而至,功能上能满足日历的要求,但是在交互体验,动画效果,总是缺乏丝滑的效果。不过由于要回家过年了,也顾不上这些问题了。于是匆忙的部署在服务器上回家过年了。


年后(2019年)初又回到了找工作的路上。命还是幸运的,很快找到了一家硬件公司,智能穿戴行业,负责业务服务器这块。当时好几个offer,而这家硬件公司地方很偏,环境也不高大上,打动我的地方是因为上午面了2轮,在等和老板洽谈的时候到中午饭点了。而同仁却早已准备了午餐,水果和午睡床。告诉我说老板下午才有空,让我先等下!就这样被打动了,坚定了决心(上一次13年在华为面试架构师岗位也是这待遇,可惜学历这道硬伤)。本着对新工作的热情和做出一番业绩的渴望,身心很快投入到了工作中。同时日历变成了我的辅助,日程中的使用让我感觉到和Google的体验的差距。不过没关系,我已经不在乎日历了。 我更在乎的怎么做出更好的业绩出来。一番努力后在这里把一个人3个人的团队干到了30+,做出了非常多的业绩,很有成就感,公司氛围相当好!最满意的公司!可到后面大了之后政治斗争也越发严重,3年合同到期,被卸磨杀驴了!只能说无尽的不舍和不甘,但又无可奈何。 最后总结会发现当一个团队有一些没有能力干事的人混进来后就是要把干事的弄死!这就是俗话说解决不了问题就把提出问题的干掉道理一样吧!不过我走后这帮人陆续的在三个月的时间全部被干掉了,我只想说苍天饶过谁!


回到主题,既然肆业了,又想起了日历,其实在这3年的使用中发现了问题,但其实每次也都知道如何去解决。三年间它帮助我解决了非常多的问题,时间管理,日程管理。而后自己决定要开发在体验上能够极致的日历。决定之后,自己把自己锁在出租屋内,刚好又是疫情,一个月没出门对整个日历进行了重新架构设计,体验的交互,丝滑程度与架构有关。经过一个月在体验上的打磨!让它成为了一个真正可用的日历,从功能到到交互再到体验,终究算是了却了自己的一桩心事,算是这么多年唯一一次能交代自己了。 回头总结和看看过去有过的很多想法,很多次都能鼓起勇气尝试去做了,但事实上在有没有结果之间差着一个体验的距离,有体验即代表有结果。没有体验,即纯属到此一游,纵使无数次的开始和结束都将无济于事。


与大家分享下结果


日历导航
视图导航v1.gif


日程操作


日程编辑.gif


日历操作


日历编辑v2.gif


当然还有特别多的功能,修改创建日历,日历主题,订阅日历,日程协作共享,短信桌面通知提醒等。


由于录制屏幕的限制,很多体验无法一一说明,如有感兴趣的朋友希望能够帮助到您。本人还是将一如既往的凭一己之力尽可能提供更多的功能和体验。 后面自己将计划开发windows插件,用于嵌套在桌面上,更加简单灵活体验。


总结


做一件事路途可能很遥远,路途可能会迷路,甚至迷失自己,但坚持自己的初衷,我觉得总是会能达到的!生活中可能会有很多的欺骗和谎言,但始终我们的坚信自己!自己也会未来继续为免费这条路走的更宽和更远,同样提供极致体验。时间很短,也很长,我们可以一件事都不做,也可以做很多事。人生是一连串选择,都是一些常见选择题,最老套的选择就是当一个受害者-随波逐流; 或者选择反抗,也可以选择是忠诚,不论时局好坏。


一个人一台电脑,一个人设计,一个人撸代码,一个人测试,希望一个人能一直走下去。


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

Android全局的通知的弹窗

需求分析 如何创建一个全局通知的弹窗?如下图所示。 从手机顶部划入,短暂停留后,再从顶部划出。 首先需要明确的是: 1、这个弹窗的弹出逻辑不一定是当前界面编写的,比如用户上传文件,用户可能继续浏览其他页面的内容,但是监听文件是否上传完成还是在原来的Activ...
继续阅读 »

需求分析


如何创建一个全局通知的弹窗?如下图所示。


image.png


从手机顶部划入,短暂停留后,再从顶部划出。


首先需要明确的是:

1、这个弹窗的弹出逻辑不一定是当前界面编写的,比如用户上传文件,用户可能继续浏览其他页面的内容,但是监听文件是否上传完成还是在原来的Activity或Service,但是Dialog的弹出是需要当前页面的上下文Context的。


2、Dialog弹窗必须支持手势,用户在Dialog上向上滑时,Dialog需要退出,点击时可能需要处理点击事件。


一、Dialog的编写

/**
* 通知的自定义Dialog
*/
class NotificationDialog(context: Context, var title: String, var content: String) :
Dialog(context, R.style.dialog_notifacation_top) {

private var mListener: OnNotificationClick? = null
private var mStartY: Float = 0F
private var mView: View? = null
private var mHeight: Int? = 0

init {
mView = LayoutInflater.from(context).inflate(R.layout.common_layout_notifacation, null)
}


override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(mView!!)
window?.setGravity(Gravity.TOP)
val layoutParams = window?.attributes
layoutParams?.width = ViewGroup.LayoutParams.MATCH_PARENT
layoutParams?.height = ViewGroup.LayoutParams.WRAP_CONTENT
layoutParams?.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
window?.attributes = layoutParams
window?.setWindowAnimations(R.style.dialog_animation)
//按空白处不能取消
setCanceledOnTouchOutside(false)
//初始化界面数据
initData()
}

private fun initData() {
val tvTitle = findViewById<TextView>(R.id.tv_title)
val tvContent = findViewById<TextView>(R.id.tv_content)
if (title.isNotEmpty()) {
tvTitle.text = title
}

if (content.isNotEmpty()) {
tvContent.text = content
}
}


override fun onTouchEvent(event: MotionEvent): Boolean {
when (event.action) {
MotionEvent.ACTION_DOWN -> {
if (isOutOfBounds(event)) {
mStartY = event.y
}
}

MotionEvent.ACTION_UP -> {
if (mStartY > 0 && isOutOfBounds(event)) {
val moveY = event.y
if (abs(mStartY - moveY) >= 15) { //滑动超过20认定为滑动事件
//Dialog消失
} else { //认定为点击事件
//Dialog的点击事件
mListener?.onClick()
}
dismiss()
}
}
}
return false
}

/**
* 点击是否在范围外
*/
private fun isOutOfBounds(event: MotionEvent): Boolean {
val yValue = event.y
if (yValue > 0 && yValue <= (mHeight ?: (0 + 40))) {
return true
}
return false
}


private fun setDialogSize() {
mView?.addOnLayoutChangeListener { v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom ->
mHeight = v?.height
}
}

/**
* 显示Dialog但是不会自动退出
*/
fun showDialog() {
if (!isShowing) {
show()
setDialogSize()
}
}

/**
* 显示Dialog,3000毫秒后自动退出
*/
fun showDialogAutoDismiss() {
if (!isShowing) {
show()
setDialogSize()
//延迟3000毫秒后自动消失
Handler(Looper.getMainLooper()).postDelayed({
if (isShowing) {
dismiss()
}
}, 3000L)
}
}

//处理通知的点击事件
fun setOnNotificationClickListener(listener: OnNotificationClick) {
mListener = listener
}

interface OnNotificationClick {
fun onClick()
}
}

Dialog的主题

<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools">

<style name="dialog_notifacation_top">
<item name="android:windowIsTranslucent">true</item>
<!--设置背景透明-->
<item name="android:windowBackground">@android:color/transparent</item>
<!--设置dialog浮与activity上面-->
<item name="android:windowIsFloating">true</item>
<!--去掉背景模糊效果-->
<item name="android:backgroundDimEnabled">false</item>
<item name="android:windowNoTitle">true</item>
<!--去掉边框-->
<item name="android:windowFrame">@null</item>
</style>


<style name="dialog_animation" parent="@android:style/Animation.Dialog">
<!-- 进入时的动画 -->
<item name="android:windowEnterAnimation">@anim/dialog_enter</item>
<!-- 退出时的动画 -->
<item name="android:windowExitAnimation">@anim/dialog_exit</item>
</style>

</resources>

Dialog的动画

<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate
android:duration="600"
android:fromYDelta="-100%p"
android:toYDelta="0%p" />
</set>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate
android:duration="300"
android:fromYDelta="0%p"
android:toYDelta="-100%p" />
</set>

Dialog的布局,通CardView包裹一下就有立体阴影的效果

<androidx.cardview.widget.CardView
android:id="@+id/cd"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="@dimen/size_15dp"
app:cardCornerRadius="@dimen/size_15dp"
app:cardElevation="@dimen/size_15dp"
app:layout_constraintTop_toTopOf="parent">

<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/et_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="@dimen/size_15dp"
app:layout_constraintTop_toTopOf="parent">

<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/tv_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="#000000"
android:textSize="@dimen/font_14sp" android:textStyle="bold"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent" />

<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/tv_content"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/size_15dp"
android:textColor="#333"
android:textSize="@dimen/font_12sp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_title" />


</androidx.constraintlayout.widget.ConstraintLayout>

</androidx.cardview.widget.CardView>

二、获取当前显示的Activity的弱引用

/**
* 前台Activity管理类
*/
class ForegroundActivityManager {

private var currentActivityWeakRef: WeakReference<Activity>? = null
private var mIsActive:Boolean = false

companion object {
val TAG = "ForegroundActivityManager"
private val instance = ForegroundActivityManager()

@JvmStatic
fun getInstance(): ForegroundActivityManager {
return instance
}
}


fun getCurrentActivity(): Activity? {
var currentActivity: Activity? = null
if (currentActivityWeakRef != null) {
currentActivity = currentActivityWeakRef?.get()
}
return currentActivity
}


fun setCurrentActivity(activity: Activity) {
currentActivityWeakRef = WeakReference(activity)
}

fun setActive(isActive:Boolean){
mIsActive = isActive
}

fun getActive():Boolean=mIsActive


}

监听所有Activity的生命周期,并判断当前页面是否可以显示Dialog,参考LiveData的源码,判断当前是否是Active状态,如果是Activie状态则可以显示Dialog,如果非Active状态则等待下次Active时显示Dialog

class AppLifecycleCallback:Application.ActivityLifecycleCallbacks {

companion object{
val TAG = "AppLifecycleCallback"
}

override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
//获取Activity弱引用
ForegroundActivityManager.getInstance().setCurrentActivity(activity)
}

override fun onActivityStarted(activity: Activity) {
}

override fun onActivityResumed(activity: Activity) {
//获取Activity弱引用
ForegroundActivityManager.getInstance().setCurrentActivity(activity)
//设置当前Active状态为true
ForegroundActivityManager.getInstance().setActive(true)
}

override fun onActivityPaused(activity: Activity) {
//设置当前Active状态为false
ForegroundActivityManager.getInstance().setActive(false)
}

override fun onActivityStopped(activity: Activity) {
}

override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {
}

override fun onActivityDestroyed(activity: Activity) {
}
}

在Application中注册

//注册Activity生命周期
registerActivityLifecycleCallbacks(AppLifecycleCallback())

三、封装和使用

/**
* 通知的管理类
* example:
* //发系统通知
* NotificationControlManager.getInstance()?.notify("文件上传完成", "文件上传完成,请点击查看详情")
* //发应用内通知
* NotificationControlManager.getInstance()?.showNotificationDialog("文件上传完成","文件上传完成,请点击查看详情",
* object : NotificationControlManager.OnNotificationCallback {
* override fun onCallback() {
* Toast.makeText(this@MainActivity, "被点击了", Toast.LENGTH_SHORT).show()
* }
* })
*/

class NotificationControlManager {

private var autoIncreament = AtomicInteger(1001)
private var contentMap = mutableListOf<NotificationInfo>()
private var dialogList = mutableListOf<NotificationDialog>()

companion object {
const val channelId = "app"
const val description = "my application"

@Volatile
private var sInstance: NotificationControlManager? = null

@JvmStatic
fun getInstance(): NotificationControlManager {
if (sInstance == null) {
synchronized(NotificationControlManager::class.java) {
if (sInstance == null) {
sInstance = NotificationControlManager()
}
}
}
return sInstance!!
}
}


/**
* 是否打开通知
*/
fun isOpenNotification(): Boolean {
val notificationManager: NotificationManagerCompat =
NotificationManagerCompat.from(
ForegroundActivityManager.getInstance()?.getCurrentActivity()!!
)
return notificationManager.areNotificationsEnabled()
}


/**
* 跳转到系统设置页面去打开通知,注意在这之前应该有个Dialog提醒用户
*/
fun openNotificationInSys() {
val context = ForegroundActivityManager.getInstance()?.getCurrentActivity()!!
val intent: Intent = Intent()
try {
intent.action = Settings.ACTION_APP_NOTIFICATION_SETTINGS

//8.0及以后版本使用这两个extra. >=API 26
intent.putExtra(Settings.EXTRA_APP_PACKAGE, context.packageName)
intent.putExtra(Settings.EXTRA_CHANNEL_ID, context.applicationInfo.uid)

//5.0-7.1 使用这两个extra. <= API 25, >=API 21
intent.putExtra("app_package", context.packageName)
intent.putExtra("app_uid", context.applicationInfo.uid)

context.startActivity(intent)
} catch (e: Exception) {
e.printStackTrace()

//其他低版本或者异常情况,走该节点。进入APP设置界面
intent.action = Settings.ACTION_APPLICATION_DETAILS_SETTINGS
intent.putExtra("package", context.packageName)

//val uri = Uri.fromParts("package", packageName, null)
//intent.data = uri
context.startActivity(intent)
}
}

/**
* 发通知
* @param title 标题
* @param content 内容
* @param cls 通知点击后跳转的Activity,默认为null跳转到MainActivity
*/
fun notify(title: String, content: String, cls: Class<*>) {
val context = ForegroundActivityManager.getInstance()?.getCurrentActivity()!!
val notificationManager =
context.getSystemService(AppCompatActivity.NOTIFICATION_SERVICE) as NotificationManager
val builder: Notification.Builder
val intent = Intent(context, cls)
val pendingIntent: PendingIntent? = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_IMMUTABLE)
} else {
PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_ONE_SHOT)
}

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val notificationChannel =
NotificationChannel(channelId, description, NotificationManager.IMPORTANCE_HIGH)
notificationChannel.enableLights(true);
notificationChannel.lightColor = Color.RED;
notificationChannel.enableVibration(true);
notificationChannel.vibrationPattern =
longArrayOf(100, 200, 300, 400, 500, 400, 300, 200, 400)
notificationManager.createNotificationChannel(notificationChannel)
builder = Notification.Builder(context, channelId)
.setSmallIcon(R.drawable.jpush_notification_icon)
.setContentIntent(pendingIntent)
.setContentTitle(title)
.setContentText(content)
} else {
builder = Notification.Builder(context)
.setSmallIcon(R.drawable.jpush_notification_icon)
.setLargeIcon(
BitmapFactory.decodeResource(
context.resources,
R.drawable.jpush_notification_icon
)
)
.setContentIntent(pendingIntent)
.setContentTitle(title)
.setContentText(content)

}
notificationManager.notify(autoIncreament.incrementAndGet(), builder.build())
}


/**
* 显示应用内通知的Dialog,需要自己处理点击事件。listener默认为null,不处理也可以。dialog会在3000毫秒后自动消失
* @param title 标题
* @param content 内容
* @param filterActivityByClassNameList 过滤哪些类不显示Dialog
* @param listener 点击的回调
*/
fun showNotificationDialog(
title: String,
content: String,
filterActivityByClassNameList: MutableList<String>? = null,
listener: OnNotificationCallback? = null
) {
val currentActivity = ForegroundActivityManager.getInstance()?.getCurrentActivity()!!
//判断是否需要过滤页面不显示Dialog
filterActivityByClassNameList?.forEach {
val className = currentActivity.javaClass.simpleName
if (className == it) {
return
}
}

val isActive = ForegroundActivityManager.getInstance()?.getActive() ?: false
if (isActive) { //Active状态
val dialog = NotificationDialog(currentActivity, title, content)
dialogList.add(dialog)
if (Thread.currentThread() != Looper.getMainLooper().thread) { //子线程
currentActivity.runOnUiThread {
dialog.showDialogAutoDismiss()
setDialogClick(dialog, listener)
}
} else {
dialog.showDialogAutoDismiss()
setDialogClick(dialog, listener)
}
} else { //如果当前Activity非Active状态则把要显示的内容存储到集合中
//存到集合中
contentMap.add(NotificationInfo(title, content))
}
}


/**
* 显示应用内通知的Dialog,需要自己处理点击事件。listener默认为null,不处理也可以。dialog会在3000毫秒后自动消失
* @param title 标题
* @param content 内容
* @param activity 需要传入Activity(主要碰到多进程的问题)
* @param listener 点击的回调
*/
fun showNotificationDialog(
title: String,
content: String,
activity: AppCompatActivity,
listener: OnNotificationCallback? = null
) {
val dialog = NotificationDialog(activity, title, content)
dialogList.add(dialog)
if (Thread.currentThread() != Looper.getMainLooper().thread) { //子线程
activity.runOnUiThread {
dialog.showDialogAutoDismiss()
setDialogClick(dialog, listener)
}
} else {
dialog.showDialogAutoDismiss()
setDialogClick(dialog, listener)
}
}


/**
* 显示应用内通知的Dialog,需要自己处理点击事件。listener默认为null,不处理也可以。dialog会在3000毫秒后自动消失
* @param title 标题
* @param content 内容
* @param activity 需要传入Activity(主要碰到多进程的问题)
* @param listener 点击的回调
*/
fun showNotificationDialogWithNotLifecycle(
title: String,
content: String,
activity: AppCompatActivity,
listener: OnNotificationCallback? = null
) {
val dialog = NotificationDialog(activity, title, content)
dialogList.add(dialog)
if (Thread.currentThread() != Looper.getMainLooper().thread) { //子线程
activity.runOnUiThread {
dialog.showDialogAutoDismiss()
setDialogClick(dialog, listener)
}
} else {
dialog.showDialogAutoDismiss()
setDialogClick(dialog, listener)
}
}


/**
* set dialog click
*/
private fun setDialogClick(
dialog: NotificationDialog?,
listener: OnNotificationCallback?
) {
if (listener != null) {
dialog?.setOnNotificationClickListener(object :
NotificationDialog.OnNotificationClick {
override fun onClick() = listener.onCallback()
})
}
}

/**
* 显示没有显示过的Dialog
*/
fun showDialogNeverVisible() {
if (contentMap.isNotEmpty()) {
val iterator = contentMap.iterator()
while (iterator.hasNext()) {
val info = iterator.next()
val currentActivity =
ForegroundActivityManager.getInstance()?.getCurrentActivity()!!
val dialog =
NotificationDialog(currentActivity, info.title, info.content)
dialogList.add(dialog)
if (Thread.currentThread() != Looper.getMainLooper().thread) { //子线程
currentActivity.runOnUiThread {
dialog.showDialogAutoDismiss()
setDialogClick(dialog, null) //这里需要根据场景完善点击事件
iterator.remove()
}
} else {
dialog.showDialogAutoDismiss()
setDialogClick(dialog, null) //这里需要根据场景完善点击事件
iterator.remove()
}
}
}
}

/**
* dismiss Dialog
*/
fun dismissDialogWithLifecycle() {
if (dialogList.size > 0) {
val iterator = dialogList.iterator()
while (iterator.hasNext()) {
val dialog = iterator.next()
if (dialog != null && dialog.isShowing) {
dialog.dismiss()
}
iterator.remove()
}
}
}


interface OnNotificationCallback {
fun onCallback()
}

}
//根据需求封装
data class NotificationInfo(var title:String,var content:String)

需要注意的点是:

1、Activity处于转场时是不能显示Dialog的,此时会回调onPause方法,isActive处于false状态(这一点参考LiveData的源码),将需要显示的数据存储于集合中,待BaseActivity回调onResume时显示没有显示的Dialog。

override fun onResume() {
super.onResume()
NotificationControlManager.getInstance()?.showDialogNeverVisible()
}

2、因为dialog是延迟关闭的,可能用户立刻退出Activity,导致延迟时间到时dialog退出时报错,解决办法可以在BaseActivity的onPause方法中尝试关闭Dialog:

override fun onPause() {
super.onPause()
NotificationControlManager.getInstance()?.dismissDialogWithLifecycle()
}

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

Android 自定义View 之 饼状进度条

前言   前面写了圆环进度条,这次我们来写一个饼状进度条,首先看一下效果图: 正文   效果图感觉怎么样呢?下面我们来实现这个自定义View,依然是写在EasyView这个项目中,这是一个自定义View库,我会把自己写的自定义View都放在里面,文中如果代码...
继续阅读 »

前言


  前面写了圆环进度条,这次我们来写一个饼状进度条,首先看一下效果图:


在这里插入图片描述


正文


  效果图感觉怎么样呢?下面我们来实现这个自定义View,依然是写在EasyView这个项目中,这是一个自定义View库,我会把自己写的自定义View都放在里面,文中如果代码不是很全的话,你可以找到文章最后的源码去查看,话不多说,我们开始吧。


一、XML样式


  根据上面的效果图,我们首先来确定XML中的属性样式,在attrs.xml中添加如下代码:

	<!--饼状进度条-->
<declare-styleable name="PieProgressBar">
<!--半径-->
<attr name="radius" />
<!--最大进度-->
<attr name="maxProgress" />
<!--当前进度-->
<attr name="progress" />
<!--进度条进度颜色-->
<attr name="progressbarColor" />
<!--进度条描边宽度-->
<attr name="strokeWidth"/>
<!--进度是否渐变-->
<attr name="gradient" />
<!--渐变颜色数组-->
<attr name="gradientColorArray" />
<!--自定义开始角度 0 ,90,180,270-->
<attr name="customAngle">
<enum name="right" value="0" />
<enum name="bottom" value="90" />
<enum name="left" value="180" />
<enum name="top" value="270" />
</attr>
</declare-styleable>

  这里的公共属性我就抽离了出来,因为之前写过圆环进度条,有一些属性是可以通用的,并且我在饼状进度条中增加了开始的角度,之前是默认是从0°开始,现在可以根据属性设置开始的角度,并且我增加了渐变颜色。


二、构造方法


  现在属性样式已经有了,下一步就是写自定义View的构造方法了,在com.easy.view包下新建一个PieProgressBar 类,里面的代码如下所示:

public class PieProgressBar extends View {

/**
* 半径
*/
private int mRadius;
/**
* 进度条宽度
*/
private int mStrokeWidth;
/**
* 进度条进度颜色
*/
private int mProgressColor;
/**
* 开始角度
*/
private int mStartAngle = 0;

/**
* 当前角度
*/
private float mCurrentAngle = 0;
/**
* 结束角度
*/
private int mEndAngle = 360;
/**
* 最大进度
*/
private float mMaxProgress;
/**
* 当前进度
*/
private float mCurrentProgress;
/**
* 是否渐变
*/
private boolean isGradient;
/**
* 渐变颜色数组
*/
private int[] colorArray;
/**
* 动画的执行时长
*/
private long mDuration = 1000;
/**
* 是否执行动画
*/
private boolean isAnimation = false;

public PieProgressBar(Context context) {
this(context, null);
}

public PieProgressBar(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}

public PieProgressBar(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.PieProgressBar);
mRadius = array.getDimensionPixelSize(R.styleable.PieProgressBar_radius, 80);
mStrokeWidth = array.getDimensionPixelSize(R.styleable.PieProgressBar_strokeWidth, 8);
mProgressColor = array.getColor(R.styleable.PieProgressBar_progressbarColor, ContextCompat.getColor(context, R.color.tx_default_color));
mMaxProgress = array.getInt(R.styleable.PieProgressBar_maxProgress, 100);
mCurrentProgress = array.getInt(R.styleable.PieProgressBar_progress, 0);
//是否渐变
isGradient = array.getBoolean(R.styleable.PieProgressBar_gradient, false);
//渐变颜色数组
CharSequence[] textArray = array.getTextArray(R.styleable.PieProgressBar_gradientColorArray);
if (textArray != null) {
colorArray = new int[textArray.length];
for (int i = 0; i < textArray.length; i++) {
colorArray[i] = Color.parseColor((String) textArray[i]);
}
}
mStartAngle = array.getInt(R.styleable.PieProgressBar_customAngle, 0);
array.recycle();
}
}

  这里声明了一些变量,然后写了3个构造方法,在第三个构造方法中进行属性的赋值。


三、测量


  这里测量就比较简单了,和之前的圆环进度条差不多,代码如下所示:

    @Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int width = 0;
switch (MeasureSpec.getMode(widthMeasureSpec)) {
case MeasureSpec.UNSPECIFIED:
case MeasureSpec.AT_MOST: //wrap_content
width = mRadius * 2;
break;
case MeasureSpec.EXACTLY: //match_parent
width = MeasureSpec.getSize(widthMeasureSpec);
break;
}
//Set the measured width and height
setMeasuredDimension(width, width);
}

  因为不需要进行子控件处理,所以我们只要一个圆和描边就行了,下面看绘制的方法。


四、绘制


  绘制这里就是绘制描边和进度,绘制的代码如下所示:

    @Override
protected void onDraw(Canvas canvas) {
int centerX = getWidth() / 2;
@SuppressLint("DrawAllocation")
RectF rectF = new RectF(0,0,centerX * 2,centerX * 2);
//绘制描边
drawStroke(canvas, centerX);
//绘制进度
drawProgress(canvas, rectF);
}

  在绘制之前首先要确定中心点,因为我们是一个圆环,实际上也是一个圆,圆的宽高一样,所以中心点的x、y轴的位置就是一样的,然后是确定一个矩形的左上和右下两个位置的坐标点,通过这两个点就能绘制一个矩形,接下来就是绘制进度条背景。


① 绘制描边

    /**
* 绘制描边
*
* @param canvas 画布
* @param centerX 中心点
*/
private void drawStroke(Canvas canvas, int centerX) {
Paint paint = new Paint();
paint.setColor(mProgressColor);
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(mStrokeWidth);
paint.setAntiAlias(true);
canvas.drawCircle(centerX, centerX, mRadius - (mStrokeWidth / 2), paint);
}

  这里的要点就是我们需要设置画笔的类型为描边,然后设置描边宽度,这样我们就可以画一个空心圆,就成了描边,然后我们绘制进度。


① 绘制进度

    /**
* 绘制进度条背景
*/
private void drawProgress(Canvas canvas, RectF rectF) {
Paint paint = new Paint();
//画笔的填充样式,Paint.Style.STROKE 描边
paint.setStyle(Paint.Style.FILL);
//抗锯齿
paint.setAntiAlias(true);
//画笔的颜色
paint.setColor(mProgressColor);
//是否设置渐变
if (isGradient && colorArray != null) {
paint.setShader(new RadialGradient(rectF.centerX(), rectF.centerY(), mRadius, colorArray, null, Shader.TileMode.MIRROR));
}
if (!isAnimation) {
mCurrentAngle = 360 * (mCurrentProgress / mMaxProgress);
}
//开始画圆弧
canvas.drawArc(rectF, mStartAngle, mCurrentAngle, true, paint);
}

  因为背景是一个圆环,所以这里的画笔设置就比较注意一些,看一下就会了,这里最重要的是drawArc,用于绘制及角度圆,像下图这样,画了4/1的进度,同时增加是否渐变的设置,这里的开始角度是动态的。


在这里插入图片描述


五、API方法


  还需要提供一些方法在代码中调用,下面是这些方法的代码:

    /**
* 设置角度
* @param angle 角度
*/
public void setCustomAngle(int angle) {
if (angle >= 0 && angle < 90) {
mStartAngle = 0;
} else if (angle >= 90 && angle < 180) {
mStartAngle = 90;
} else if (angle >= 180 && angle < 270) {
mStartAngle = 180;
} else if (angle >= 270 && angle < 360) {
mStartAngle = 270;
} else if (angle >= 360) {
mStartAngle = 0;
}
invalidate();
}

/**
* 设置是否渐变
*/
public void setGradient(boolean gradient) {
isGradient = gradient;
invalidate();
}

/**
* 设置渐变的颜色
*/
public void setColorArray(int[] colorArr) {
if (colorArr == null) return;
colorArray = colorArr;
}

/**
* 设置当前进度
*/
public void setProgress(float progress) {
if (progress < 0) {
throw new IllegalArgumentException("Progress value can not be less than 0");
}
if (progress > mMaxProgress) {
progress = mMaxProgress;
}
mCurrentProgress = progress;
mCurrentAngle = 360 * (mCurrentProgress / mMaxProgress);
setAnimator(mStartAngle, mCurrentAngle);
}

/**
* 设置动画
*
* @param start 开始位置
* @param target 结束位置
*/
private void setAnimator(float start, float target) {
isAnimation = true;
ValueAnimator animator = ValueAnimator.ofFloat(start, target);
animator.setDuration(mDuration);
animator.setTarget(mCurrentAngle);
//动画更新监听
animator.addUpdateListener(valueAnimator -> {
mCurrentAngle = (float) valueAnimator.getAnimatedValue();
invalidate();
});
animator.start();
}

  那么到此为止这个自定义View就完成了,下面我们可以在PieProgressBarActivity中使用了。


六、使用


   关于使用,我在写这个文章的时候这个自定义View已经加入到仓库中了,可以通过引入依赖的方式,例如在app模块中使用,则打开app模块下的build.gradle,在dependencies{}闭包下添加即可,之后记得要Sync Now

dependencies {
implementation 'io.github.lilongweidev:easyview:1.0.4'
}

   或者你在自己的项目中完成了刚才上述的所有步骤,那么你就不用引入依赖了,直接调用就好了,不过要注意更改对应的包名,否则会爆红的。


  先修改activity_pie_progress_bar.xml的代码,如下所示:

<?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"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical"
tools:context=".used.PieProgressBarActivity">

<com.easy.view.PieProgressBar
android:id="@+id/progress"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:customAngle="right"
app:gradient="false"
app:gradientColorArray="@array/color"
app:maxProgress="100"
app:progress="5"
app:progressbarColor="@color/green"
app:radius="80dp" />

<CheckBox
android:id="@+id/cb_gradient"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:text="是否渐变" />

<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center_vertical">

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="开始角度:"
android:textColor="@color/black" />

<RadioGroup
android:id="@+id/rg"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal">

<RadioButton
android:id="@+id/rb_0"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:checked="true"
android:text="0%" />

<RadioButton
android:id="@+id/rb_90"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="90%" />

<RadioButton
android:id="@+id/rb_180"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="180%" />

<RadioButton
android:id="@+id/rb_270"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="270%" />
</RadioGroup>
</LinearLayout>


<Button
android:id="@+id/btn_set_progress"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:text="随机设置进度" />

<Button
android:id="@+id/btn_set_progress_0"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:text="设置0%进度" />

<Button
android:id="@+id/btn_set_progress_100"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:text="设置100%进度" />
</LinearLayout>

在strings.xml中增加渐变色,代码如下:

    <string-array name="color">
<item>#00FFF7</item>
<item>#FFDD00</item>
<item>#FF0000</item>
</string-array>

首先要注意看是否能够预览,我这里是可以预览的,如下图所示:


在这里插入图片描述


PieProgressBarActivity中使用,如下所示:

public class PieProgressBarActivity extends EasyActivity<ActivityPieProgressBarBinding> {

@SuppressLint("NonConstantResourceId")
@Override
protected void onCreate() {
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
//是否渐变
binding.cbGradient.setOnCheckedChangeListener((buttonView, isChecked) -> {
binding.cbGradient.setText(isChecked ? "渐变" : "不渐变");
binding.progress.setGradient(isChecked);
});
//开始角度
binding.rg.setOnCheckedChangeListener((group, checkedId) -> {
int angle = 0;
switch (checkedId) {
case R.id.rb_0:
angle = 0;
break;
case R.id.rb_90:
angle = 90;
break;
case R.id.rb_180:
angle = 180;
break;
case R.id.rb_270:
angle = 270;
break;
}
binding.progress.setCustomAngle(angle);
});
//设置随机进度值
binding.btnSetProgress.setOnClickListener(v -> {
int progress = Math.abs(new Random().nextInt() % 100);
Toast.makeText(this, "" + progress, Toast.LENGTH_SHORT).show();
binding.progress.setProgress(progress);
});
//设置0%进度值
binding.btnSetProgress0.setOnClickListener(v -> binding.progress.setProgress(0));
//设置100%进度值
binding.btnSetProgress100.setOnClickListener(v -> binding.progress.setProgress(100));
}
}

运行效果如下图所示:


在这里插入图片描述


七、源码


如果对你有所帮助的话,不妨 Star 或 Fork,山高水长,后会有期~


源码地址:EasyView


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

Android斩首行动——接口预请求

前言 开发同学应该都很熟悉我们页面的渲染过程一般是从Activity#onCreate开始,再发起网络请求,等请求回调回来后,再基于网络数据渲染页面。可以用下面这幅图来粗略描述这个过程: 可以看到,目标页面渲染完成前必须得等待网络请求,导致渲染速度并没有那么...
继续阅读 »

前言


开发同学应该都很熟悉我们页面的渲染过程一般是从Activity#onCreate开始,再发起网络请求,等请求回调回来后,再基于网络数据渲染页面。可以用下面这幅图来粗略描述这个过程:


image.png


可以看到,目标页面渲染完成前必须得等待网络请求,导致渲染速度并没有那么快。尤其是当网络并不好的时候感受会更加明显。并且,当目标页面是H5页面或者是Flutter页面的时候,因为涉及到H5容器与Flutter容器的创建,白屏时间会更长。


那么有没有可能提前发起请求,来缩短网络请求这一部分的等待时间呢?这就是我们今天要讲的部分,接口预请求。


目标


我们要达到的目标很简单,就是提前异步发起目标页面的网络请求,从而加快目标页面的渲染速度。改善后的过程可以用下图表示:


image.png


并且,我们的预请求能力需要尽量少地侵入业务,与业务解耦,并保证能力的通用性,适用于工程内的任意页面(Android页面、H5页面、Flutter页面)。


方案


整体链路


首先给大家看一下整体链路,具体的细节可以先不用去抠,下面会一一讲到。


image.png


预请求时机


预请求时机一般有三种选择:



  1. 由业务层自行选择时机进行异步预请求

  2. 点击控件时进行异步预请求

  3. 路由最终跳转前进行异步预请求


第1种选择,由业务层自行选择时机进行预请求,需要涉及到业务层的改造,以及对时机合理性的把握。一方面是存在改造成本,另一方面是无法保证业务侧调用时机的合理性。


第2种选择,点击控件时进行预请求。若点击时进行预请求,点击事件监听并不是业务域统一的,无法形成有效封装。并且,若后续路由拦截器修改了参数,或是终止了跳转,这次预请求就失去了意义。


因此这里我们选择第3种,基于统一路由框架,在路由最终跳转前进行预请求。既保证了良好的封装性,也实现了对业务的零侵入,同时也做到了懒请求,即用户必然要发起该请求时才会去预请求。这里需要注意的是必须是在最终跳转前进行预请求,可以理解为是路由的最后一个前置异步拦截器。


预请求规则配置


我们通过本地的json文件(当然,有需要也可以上云通过配置后台下发),对预请求的规则进行配置,并将这份配置在App启动阶段异步读入到内存。后续在路由过程中,只有命中了预请求规则,才能发起预请求。配置demo如下:

{
"routeConfig":{
"scheme://domain/path?param1=true&itemId=123":["prefetchKey"],
"route2":["prefetchKey2"],
"route3":["prefetchKey3","prefetchKey4"]
},
"prefetcher":{
"prefetchKey":{
"prefetchType":"network",
"prefetchInfo":{
"api":"network.api.name",
"apiVersion":"1.0",
"method":"post",
"needLogin":"false",
"showLoginUI":"false",
"params": {
"itemId":"$route.itemId",
"firstTime":"true"
},
"headers": {

},
"prefetchImgInResponse": [
{
"imgUrl":"$data.imgData.img",
"imgWidth":"$data.imgData.imgWidth",
"imgHeight":150
}
]
}
},
"prefetchKey2":{
"prefetchType":"network",
"prefetchInfo":{
"api":"network.api.name2",
"apiVersion":"1.0",
"method":"post",
"needLogin":"false",
"showLoginUI":"false",
"params": {
"itemId":"$route.productId",
"firstTime":"false"
},
"headers": {

}
},
"prefetchKey3":{
"prefetchType":"image",
"prefetchInfo":{
"imgUrl":"$route.imgUrl",
"imgWidth":"$route.imgWidth",
"imgHeight": 150
}
},
"prefetchKey4":{
"prefetchInfo":{}
}
}
}


规则解读




















































参数名描述备注
routeConfig路由配置配置路由到预请求的映射
prefetcher预请求配置记录所有的预请求
prefetchKey预请求的key
prefetchType预请求类型分为network类型与image类型,两种类型所需要的参数不同
prefetchInfo预请求所需要的信息其中value若为route.param格式,那么该值从路由中获取;若为route.param格式,那么该值从路由中获取;若为data.param格式,则从响应数据中获取。
paramsnetwork请求所需要的请求params
headersnetwork请求所需要的请求headers
prefetchImgFromResponse预请求的响应返回后,需要预加载的图片用于需要预加载图片时,无法确定图片url,图片url只能从预请求响应中获取的场景。

举例说明


网络预请求


例如跳转目标页面,它的路由是scheme://domain/path?param1=true&itemId=123


首先我们在跳转路由时,若跳转的路由是这个目标页面,我们就会尝试去发起预请求。根据上面的demo配置文件,它将匹配到prefetchKey这个预请求。


那么我们详细看prefetchKey这个预请求,预请求类型prefetchTypenetwork,是一个网络预请求,prefetchInfo中具备了请求的基本参数(如apiName、apiVersion、method、请求params与请求headers,不同工程不一样,大家可以根据自己的工程项目进行修改)。具体看params中,有一个参数为itemId:$route.itemId。以$route.开头的意思,就是这个value值要从路由中获取,即itemId=123,那么这个值就是123。


图片预请求


在做网络预请求的过程中,我忽然想到图片做预请求也是可以大大提升用户体验的,尤其是当大图片首次下载到内存中渲染需要的时间会比较长。图片预请求分为url已知url未知两种场景,下面各举两个例子。


图片url已知

什么是图片url已知呢?比如我们在首页跳转首页的二级页面时,如果二级页面需要预加载的图片跟首页的某张图是一样的(尺寸可能不同),那么首页跳转路由时我们是能够提前知道这个图片的url的,所以我们看到prefetchKey3中配置了prefetchTypeimage的预请求。image的信息来自于路由参数,需要在跳转时将图片url和宽高作为路由参数之一。


比如scheme://domain/path?imgUrl=${encodeUrl}&imgWidth=200,那么根据配置项,我们将提前将encodeUrl这个图片以宽200,高150的尺寸,加载到内存中去。当目标页面用到这个图片时,将能很快渲染出来。


图片url未知

相反,当跳转目标页面时,目标页面所要加载的图片url没法取到,就对应了图片url未知的场景。


例如闪屏页跳转首页时,如果需要预加载首页顶部的图片,此时闪屏页是无法获取到图片的url的,因为这个图片url是首页接口返回的。这种情况下,我们只能依赖首页的预请求进行。


在demo配置文件中,我们可以看到prefetchImgFromResponse字段。这个字段代表着,当这个预请求响应回来之后,我需要去预请求某张图片。其中,imgUrl$data.param格式,以$data.开头,代表着这份数据是来自于响应数据的。响应数据就是一串json串,可以凭此,索引到预请求响应中图片url的位置,就能实现图片的提前加载了。


至于图片怎么提前加载到内存中,以及真实图片的加载怎么匹配到内存中的图片,这一部分是通过glide已有的preload机制实现的,感兴趣的同学可以去看一下源码了解一下,这里就不展开了。后面讲的预请求的方案细节,都只限于网络请求。


预请求匹配


预请求匹配指的是实际的业务请求怎样与已经执行的预请求匹配上,从而节省请求的空中时间,直接返回预请求的结果。


首先网络预请求执行前先在内存中生成一份PrefetchRecord,代表着已经执行的预请求,其中的字段跟配置文件中差不多,主要就是记录预请求相关的信息:

class PrefetchRecord {
// 请求信息
String api;
String apiVersion;
String method;
String needLogin;
String showLoginUI;
JSONObject params;
JSONObject headers;

// 预请求状态
int status;
// 预请求结果
ResponseModel response;
// 生成的请求id
String requestId;

boolean isMatch(RealRequest realRequest) {
requestId.equals(realRequest.requestId)
}
}

每一个PrefetchRecord生成时,都会生成一个requestId,用于跟实际业务请求进行匹配。requestId的生成规则可以自行制定,比如将所有请求信息包一起做一下md5处理之类。


在实际业务请求发起之前,也会根据同样的规则生成requestId。若内存中存在相同requestId对应的PrefetchRecord,那么就相当于匹配成功了。匹配成功后,再根据预请求的状态进行进一步的处理。


预请求状态


预请求状态分为START、FINISH、ABORT,对应“正在发起预请求”、“已经获得预请求结果”、“预请求被抛弃”。ABORT状态下一节再讲。


为什么要记录这个状态呢?因为我们无法保证,预请求的响应一定在实际请求之前。用图来表示:


image.png


因为预请求是一个并发行为。当预请求的空中时间特别长,长到目标页面已经发出实际请求了,预请求的响应还没回来,即预请求状态为START,而非FINISH。那么此时该怎么办?我们就需要让实际请求在一旁等着(记录到内存中,RealRequestRecord),等预请求接收到响应了,再根据requestId去进行匹配,匹配到RealRequestRecord了,就触发RealRequestRecord中的回调,返回数据。


另外,在匹配过程中需要注意一点,因为每次路由跳转,如果发起预请求了,总会生成一个Record在内存中等待匹配。因此在匹配结束后,不管是匹配成功还是匹配失败,都要及时释放将Record从内存中释放掉。


超时重试机制


基于实际请求等待预请求响应的场景,我们再延伸一下。若预请求请求超时,迟迟拿不到响应,该怎么办?用图表示:


image.png


假设目前的网络请求,端上默认的超时时间是30s。那么在超时场景下,实际的业务请求在30s内若拿不到预请求的结果,就需要重新发起业务请求,抛弃预请求,并将预请求的状态置为ABORT,这样即使后面预请求响应回来了也不做任何处理。


image.png


忽然想到一个很贴切的场景来比喻这个预请求方案。


我们把跳转页面理解为去柜台取餐。


预请求代表着我们人还没到柜台,就先远程下单让柜员去准备食物。


如果柜员准备得比较快,那么我们到柜台后就能直接把食物拿走了,就能快点吃上了(代表着页面渲染速度变快)。


如果柜员准备得比较慢,那么我们到柜台后还是得等一会儿才能取餐,但总体上吃上食物的速度还是要比到柜台后再点餐来得快。


但如果这个柜员消极怠工准备得太慢了,我们到柜台等了很久都没拿到食物,那么我们就只能换个柜员重新点了(超时后发起实际的业务请求),同时还不忘投诉一把(预请求空中时间太慢了)。


总结


通过这篇文章,我们知道了什么是接口预请求,怎么实现接口预请求。我们通过配置文件+统一路由处理+预请求发起、匹配、回调,实现了与业务解耦的,可适用于任意页面的轻量级预请求方案,从而提升页面的渲染速度。


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

🐞 如何成为一名合格的“高级开发”

嗨,大家好!这里是道长王jj~ 🎩🧙‍♂️ 这几天疯狂在肝游戏,已经到了魔怔的地步,每天早上起床是想着我今天该怎么在地铁上杀爆,每天晚上躺下的时候想的是我的装备还能怎么配装…… 哈哈,今天我们继续分享怎么一步步做一个专业的开发者,还有工作中要注意什么事情。 如...
继续阅读 »

嗨,大家好!这里是道长王jj~ 🎩🧙‍♂️


这几天疯狂在肝游戏,已经到了魔怔的地步,每天早上起床是想着我今天该怎么在地铁上杀爆,每天晚上躺下的时候想的是我的装备还能怎么配装……


哈哈,今天我们继续分享怎么一步步做一个专业的开发者,还有工作中要注意什么事情。


如果你是第一次看这个系列,我强烈建议你回去看看我之前写的三篇文章,说不定能对你有帮助。




  1. 🎖️怎么知道我的能力处于什么水平?我该往哪里努力?

  2. 🚗我毕业/转行了,怎么适应我的第一份开发工作?

  3. 🐞 如何成为一名合格的“中级开发”



今天,我们继续聊一聊,看看到底是什么造就了“高级开发”,而我们应该怎么往这个方向冲刺呢?😎


❓ 什么是“高级开发”?


回顾一下我这个系列第一篇文章的定义,我这边罗列一下:



  1. 精通团队所使用的核心技术,对其应用得非常熟练。

  2. 能处理团队项目中的系统架构问题和设计🏢

  3. 有多年的编码经验(一定是真正在一线的真正写代码的时间,而不是通过经历硬凑的时间)

  4. 拥有构建“完整”解决方案的经验,能够考虑到项目的各个方面并提供全面的解决方案。🔍

  5. 在其他专业相关领域有一定经验,了解负载平衡、连接池等跨领域知识。🖥️

  6. 积极指导中级和初级开发工程师。👥


如果你能做到以上这些部分或者全部的内容,比如:



  • 你在公司中解决了很多一般开发解决不了的难题

  • 善于沟通,能够处理各方的关系和调解工作沟通

  • 在许多团队决策上能提供许多建设性的想法


等等……


就算短时间内不被授予领导者的角色,潜移默化地,你的同事都会帮你当成团队大佬和领导(更注重你的意见)


❓ “高级开发”比“中级开发”多了什么?


多得多的开发经历


现在国内公司普遍的一个潜规则是 5年以上开发经验是“高级开发”职务的基本条件


因此现在有很多的“中级开发”喜欢使用 编码经历(不是开发经历) 来判断自己是不是能胜任“高级开发”的职务。


例如:小张只有3年的工作经验,但是喜欢把实习1年和大学的编程作业1年这些时间加上,来给自己打上5年开发经验的标签。



可能因为内卷的原因,简历伪造基本上50%的概率都会遇到,大家都想把自己最好的一面展示出来,甚至不惜夸大一部分的事实。至少如果这份简历到了我这里看到,是一件非常危险的事情。



这样就导致了,很多时候我们真的没有招募到一个有高级开发实力的“高级开发”。


高级开发人员在构建解决方案、管理复杂性、处理令人困惑的业务需求、应用设计模式等方面积累了丰富的经验。因为他们做过很多次这些事情,一遍又一遍,他们可以“用心”解决许多常见问题。


“高级开发”应该要像一个成熟的成年人,很多方案的尝试不应该带有实验性,而是真正的“做过”


这种能力只能来自你从失败、成功、导师等中学到的真实经验,需要大量的练习,需要做很多次这些事情,以至于它们会印在你的大脑中!



这里提到的经验,不包括没有挑战性的工作,如果只是CRUD,你永远都不会成长



在这之前他们应该在开发经验上有很多很多时间的沉淀,研究过许多Demo,并为他们以后的解决方案奠定基础。


所以,“高级开发”需要的是比“中级开发”多得多得多的实战经验才能构建出一个属于自己的“解决方案”体系。


谦卑


“高级开发”因为在很多问题的已经有了解决方案。


而且他们已经把有效、有用、实用、简单这几个字贯彻到了实际的开发工作中。


因此由于他们的经验,“高级开发”虽然知道很多东西,对自己的能力很有信心,但是他不会再有“骄傲”的心理。


因为面对的事情太多,会开始知道其实自己不知道的事情太多了。


反而“高级开发”对如何让实现方案趋于完美有很高的追求。


❓ “高级开发”应该有怎么样的知识广度?


现在我们业内流行一个说法叫做 T型人才


这其实就要求“高级开发”要对许多其他专业领域要有基本的了解,而且要再本专业领域或多个本专业领域拥有深入的知识和技能。


例如:



  1. 我从来没有构建过分布式微服务系统,但是我知道这个系统能决什么问题,而且我大概了解构造他们的不同方法。

  2. 我从来没有在实际生产中应用前端监控平台,但是我知道他能解决什么样的问题。


就在几周前,我们公司进行数据治理的时候,我向我们应用服务团队推荐了一些使用“落地表”,“增加表索引”和“使用缓存”优化数据库性能的组合方案。我近两年没有再操作过数据库,甚至没建过索引,但是我知道它们组合起来能解决什么问题。


同样,这个也是在 什么是“高级开发” 中提到的一个“高级开发”的关键特征:在其他专业相关领域有一定经验🖥️


❓ “高级开发”应该有怎么样的知识深度?


同样,在 “T” 的垂直领域,“高级开发”应该在自己的专业领域有深入的研究,具备完整的知识和技能(这个是在开发领域的立足之本)。


也就是 什么是“高级开发” 提到的:精通团队所使用的核心技术,对其应用得非常熟练


无论如何,“高级开发”必须先是某个专业领域的专业人才为前提。


这些特定的领域可能是编程语言或框架:Vue、Angular、React、Three.js、Node、ava、Go等等


或者是一组特定的技术:系统架构、编程范式、专业解决方案、应用安全、网络安全等等


甚至是特定行业的针对性了解:医疗安全体系、金融安全体系等等


❓ 我是怎么成长过来的?


如果以我自己的职业生涯为案例。


我的第一份工作是在一家国内知名的PCB行业民企的IT部门,当时该企业内部的“OA”系统正在进行重构和维护,目标是想让OA系统以一个全新现代化的面貌展现给公司的全体职员。


可是这个系统很旧很烂,而且我需要不仅仅是在单个领域,而是在前端、后端、数据库等方面都要着手进行改造。😱


在这个过程中,因为老旧的OA系统的后端是使用VB语言开发的,而我实在是不愿意花大量的时间在其上学习这类老旧开发语言。


于是在任职的三年期间,我从0开始为公司搭建了一个使用Node的转发服务,并且基于这个Node服务,我构建了很多新的功能。👏


虽然这些功能看起来技术难度都不高,但是整个过程因为都经过自己的双手,确实让自己成长迅速,自己也学到了很多东西,包括很多不该学的(我甚至可以直接操作生产服务器和读取生产数据库)。


当然这些过程中也遇到了很多非常复杂的业务逻辑,这促使我寻找一些标准的代码实践。


为了解决这些问题,我花了很多时间(当然包括下班时间)学习了一些高级的编程知识,比如DDD,面向切面编程,设计模式等等。


然后我就会在工作中尝试使用这类代码实践,同事也会在这个时候问我这些东西怎么使用。


因为这样的环境,我不断地主导和帮助我的团队解决了很多代码组织和业务实践的难题。


而这些经验也让我在找下一份工作的时候更有优势。😎


📌 我想成为一名“高级开发”


看看自己是不是能做到下面这些事情:



  1. 你知道你真正想要深入的技术栈,并且真的在深入研究它们

  2. 每天都有学习的时间

  3. 不害怕承担难度高但是有价值的项目

  4. 真的奋斗在一线编程,而不是在管理岗位摸鱼

  5. 真的了解自己的“T”型技能树

  6. 如果你还不知道自己该学习什么,开始规划自己想拥有什么技能

  7. 了解你学的技能能解决什么问题,而不是因为热门才学习

  8. 了解设计模式(别以为设计模式不重要,它们是大多数领域的通用原则!)

  9. 如果你的工作在你的舒适区,建议你转向更有挑战的工作


🚩 避免成为“高级初学者”


现实中很多人可能并不拥有“高级开发”的职位,但是其他们已经拥有了高级开发的能力


不要对职位盲目崇拜,在国内许多地方,很多人都有高级开发工程师的头衔,甚至叫做“前端专家”


但是其实,他们可能:



  1. 他们已经在该岗位工作了好几年

  2. 他们面试很厉害,就是那个时候评了这个职级


这种情况在国内无处不在,当然也无法改变。我们只要意识到,他们可能并不具备高级开发的素养,不要盲目地模仿公司中地所谓的高级开发人员的代码,可能这些人在多年前就一直在这个舒适区待着,从来没有成长。


不要因为选错了导师而阻碍了自己成长。




🎉 你觉得怎么样?这篇文章可以给你带来帮助吗?当你处于这个阶段时,你发现什么对你帮助最大?如果你有任何疑问或者想进一步讨论相关话题,请随时发表评论分享您的想法,让其他人从中受益。🚀✨


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

【干货分享】安卓加固原理分享

App会面临的风险 我们首先了解一下为什么需要加固,尤其是安卓APP,下面是App目前会面临的各种风险: 而通过进行安卓加固,可以降低应用程序遭受各种恶意攻击的风险,保护用户数据和应用程序的安全性,增强用户对应用程序的信任度。 安卓加固的原理 安卓应用程序的...
继续阅读 »



App会面临的风险


我们首先了解一下为什么需要加固,尤其是安卓APP,下面是App目前会面临的各种风险:


image.png


而通过进行安卓加固,可以降低应用程序遭受各种恶意攻击的风险,保护用户数据和应用程序的安全性,增强用户对应用程序的信任度。


安卓加固的原理


安卓应用程序的加固涉及多个方面和技术。我列举了一些常见的安卓加固原理以及相关的示例代码:


1. 代码混淆(Code Obfuscation):


代码混淆通过对应用程序代码进行重命名、删除无用代码、添加虚假代码等操作,使代码难以阅读和理解,增加逆向工程的难度。常用的代码混淆工具包括ProGuard和DexGuard。


示例代码混淆配置(build.gradle):

android {
buildTypes {
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}

2. 反调试(Anti-debugging):


反调试技术可以检测应用程序是否在被调试,并采取相应的防护措施,例如中断应用程序的执行、隐藏关键信息等。


示例代码检测调试状态:

import android.os.Debug;

if (Debug.isDebuggerConnected()) {
// 应用程序正在被调试,采取相应的措施
}

3. 加密和密钥管理(Encryption and Key Management):


加密可以用于保护应用程序中的敏感数据。对于密钥管理,建议使用安全的存储方式,例如使用Android Keystore系统来保存和管理密钥。


示例代码使用AES加密算法对数据进行加密和解密:

import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;

public class EncryptionUtils {
private static final String AES_ALGORITHM = "AES";

public static byte[] encrypt(byte[] data, byte[] key) throws Exception {
SecretKey secretKey = new SecretKeySpec(key, AES_ALGORITHM);
Cipher cipher = Cipher.getInstance(AES_ALGORITHM);
cipher.init(Cipher.ENCRYPT_MODE, secretKey);
return cipher.doFinal(data);
}

public static byte[] decrypt(byte[] encryptedData, byte[] key) throws Exception {
SecretKey secretKey = new SecretKeySpec(key, AES_ALGORITHM);
Cipher cipher = Cipher.getInstance(AES_ALGORITHM);
cipher.init(Cipher.DECRYPT_MODE, secretKey);
return cipher.doFinal(encryptedData);
}
}

4. 动态加载和反射(Dynamic Loading and Reflection):


通过动态加载和反射技术,可以将应用程序的核心逻辑和敏感代码进行动态加载和执行,增加逆向工程的难度。


示例代码使用反射加载类和调用方法:

try {
Class<?> clazz = Class.forName("com.example.MyClass");
Object instance = clazz.newInstance();
Method method = clazz.getDeclaredMethod("myMethod");
method.invoke(instance);
} catch (Exception e) {
e.printStackTrace();
}

5. 安全存储(Secure Storage):


对于敏感数据(如密码、API密钥等),建议使用安全的存储方式,例如使用Android Keystore系统或将数据加密后存储在SharedPreferences或数据库中。


示例代码使用Android Keystore存储密钥:

import android.security.keystore.KeyGenParameterSpec;
import android.security.keystore.KeyProperties;
import java.security.KeyStore;

public class KeyStoreUtils {
private static final String KEY_ALIAS = "my_key_alias";

public static void generateKey() {
try {
KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");
keyStore.load(null);

if (!keyStore.containsAlias(KEY_ALIAS)) {
KeyGenParameterSpec spec = new KeyGenParameterSpec.Builder(KEY_ALIAS, KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
.setBlockModes(KeyProperties.BLOCK_MODE_CBC)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
.setRandomizedEncryptionRequired(false)
.build();
KeyGenerator keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore");
keyGenerator.init(spec);
keyGenerator.generateKey();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}

以上就是简单的代码示例。


目前市场上加固的方式


目前市面上加固的方式一般是一套纵深防御体系,分别从代码安全、资源文件安全、数据安全和运行时环境安全维度提供安全保护。同时针对每个维度又进行了不同层次的划分,加固策略可依据实际场景进行定制化调配,安全和性能达到平衡。


所以一般会从下面几个方面进行加固:


image.png


而不同的公司或者APP对于加固的要求又会不一样,所以具体的使用,其实还是要看具体的场景,等之后有机会再展开详细讲一下。


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

Android 开发还有必要深耕吗?现状怎么样?未来前景将会怎样?

截止到今天,Android的生态发生了不少变化 以前的鼎盛时期,堪称是个公司就做App,由于当时市场上缺乏Android开发,招聘往往是低要求、高薪资,只要你面试说得上四大组件,第二天马上拎包入职,线下的Android培训也是一抓一大把,吸引了一大批人涌入An...
继续阅读 »

截止到今天,Android的生态发生了不少变化


以前的鼎盛时期,堪称是个公司就做App,由于当时市场上缺乏Android开发,招聘往往是低要求、高薪资,只要你面试说得上四大组件,第二天马上拎包入职,线下的Android培训也是一抓一大把,吸引了一大批人涌入Android开发行业。Android招聘市场的需求逐渐被填充,招聘要求逐步提高……


随着“互联网寒冬”的到来,大批互联网公司纷纷倒闭,大厂也纷纷裁员节流,人才供给大幅增加、需求大幅降低,造成当时的市场迅速达到饱和。培训出来的初级Android开发找不到工作,大厂被裁员的Android开发放不下薪资要求,这批人找不到工作,再加上当时自媒体的大肆渲染,Android开发可不就“凉了”吗?


毫不夸张的说,早期说得上四大组件稍微能做一点点,拿个15-20k是比较轻松的,要是你还有过完整开发经验,30k真是一点都不过分,而在“寒冬”之后,当招聘市场供给过剩时,面试官有了充分的选择权,你会四大组件,那我就有完整App独立开发经验,另一人或许有过十万级App开发经验,你说面试官会收下谁呢?岗位招聘要求也是在这时迅速拔高,整个Android市场逐渐趋于平稳,大家感觉Android开发来到了内卷期……


再来说现在:


Android凉了吗?


其实并不是Android凉了,而是技术不过硬的Android凉了


被高薪晃晕了头脑放不下身段的假高工凉了


现在的Android市场,Android初级工程师早就已经严重饱和了,供远大于求。这就导致了很多Android开发会面临被优化、被毕业、找不到工作这种情况,然后这部分人又出来说Android凉了,如此循环之下,以致于很多人都觉得Android凉了……


其核心原因只是Android开发市场由鼎盛的疯狂逐渐趋于平稳


这里也给出Android开发薪资/年限图给大家参考:


互联网公司Android开发薪资、年限.jpg


也不缺少学历突出的、能力突出的、努力突出的,这三类都可以拿到比图中同级别更可观的薪资


当然,我们并不能以薪资作为职级的标准,决定一个Android工程师到底是初级、中级、高级还是资深的,永远都不会是开发年限!


只有技术才能客观的作为衡量标准!


不管是几年经验,如果能力与工作年限不匹配,都会有被毕业的风险,如果掌握的技术达不到对应职级的标准,那别想了,毕业警告……


在很多人觉得Android凉了的时候,也不乏有Android开发跳槽进大厂拿高薪,不少在闷头提升技术水平,迄今为止还没有听过哪个Android开发大牛说“Android凉了”,当大家达到一定的高度之后,就会得知谁谁谁跳槽美团,几百万;某某某又跳进了阿里、腾讯……


不管在任何行业,任何岗位,初级技术人才总是供大于求;不管任何行业、岗位,技术过硬的也都是非常吃香的!


在初级市场”凉了“的同时,高级市场几乎是在抢人!


很多高薪、急招岗位挂上了招聘网站,往往一整年都面试不了几场,自打挂上来,就没动过了……


image.png
所以说,Android开发求职,质量才是关键!


再说到转行问题


我一直都比较佩服有大勇气转行的朋友,因为转行需要我们抛弃现有的知识技能,重新起航


佩服归佩服,身边不少之前是Android开发的朋友转行Java、Python,但他们对于目前市场还是过于乐观了,Python很火,它竞争不大吗?部分转行从0开始的,甚至连应届生都比不过~


不要轻易转行,如果要转一定要尽早转


转行有两种我认为是正常的,一种是行业消失了、没落了,继续留在业内无法施展才华。另一种是兴趣压根就不在本行,因此选一个自己感兴趣的。而现在大部分转行都是为了跟风,为了那看得见但摸不着的”风口“,而忽略了长期的发展潜力。


image.png


不管是学习力也好,精力也好,大部分人在35岁之前都属于加速期,加速期的一些选择,决定了35岁之后到底是上升还是衰落。


以Android开发转Python来说,一个Android高级转行Python会变为Python初级,这时从事Python的人都在加速提高,要想赶超在你之前的拥有同样学习力的人是不可能办到的,这就导致在转行前期极为被动,还要保证在35岁前成为Python专家或者Leader才有可能在35岁后不进入衰落期,当然这时你的Android基本也就荒废了,不说很难成为专家,高级也成为了一个很大的门槛。


如果你还想要在对应的技术领域走的更远,就不要轻易选择转行,如果实在想要转,那么越早越好、越快越好,你的竞争者都在加速提升技术水平,职场上,没人会停下等你的……


转行大部分都产生不了质变


我们所说的质变可以理解为在一个技术领域的大幅提升,或者是不相关领域的跨界


比如由高级开发变为专家,或者是由高级开发升到Leader,再或者跨界开始做一些技术相关的博客、培训、演讲、出书等等而被人所熟知。


凡是能帮助你在职业生涯中后期进入上升期的都可以看做是一次质变,而转行很少是质变,更多的都是倒退回到原点重新出发,形象点来说,你只是换了个不同的砖头接着搬砖而已。因此我们更应该去追求质变,而不是平行或者倒退,一次倒退或许可以承受,多次倒退就很难在职业生涯中后期再进入上升期。


其实不少转行的人都没有起到积极作用,毕竟都是从0开始,精进到专家绝不是一朝一夕可以完成的


或许到时又会有同样的问题:



前端凉了?前景怎么样?


Java凉了?前景怎么样?


大数据凉了?前景怎么样?


人工智能凉了?前景怎么样?


……



而另一类人,其实不管在哪个行业都可以混的风生水起!


如果是这种,那么想必也不需要考虑转行了。


所以根本不用想着Android凉了或是说要转行,与其焦虑不安,不如努力提升技术水平,毕竟在这时代,有硬技术的人到哪都吃香。


我们想要往高级进阶,建立属于自己的系统化知识体系才是最重要的,高工所需要掌握的技术不是通过蹭热点和玩黑科技,而是需要真正深入到核心技术的本质,知晓原理,知其然知其所以然。


可能不少人会觉得Android技术深度不深,技术栈不庞大,Android职业发展有限,这就真是个天大的误解。


先说技术上,Android的技术栈随着时间的推移变得越来越庞大,细分领域也越来越多,主要有应用开发、逆向安全、音视频、车联网、物联网、手机开发和SDK开发等等,每个细分领域都有很多技术栈组成,深度都足够精深,就拿所有细分领域通用的Android系统底层源码来说,就会叫你学起来生不如死。


还有AI、大数据、边缘计算、VR/AR,很多新的技术浪潮也都可以结合进移动开发的技术范畴……


那么现在Android怎么学?学什么?


这几年Android新技术的迭代明显加速了,有来自外部跨平台新物种的冲击,有去Java化的商业考量,也有Jetpack等官方自建平台的加速等多种原因。


作为Android开发者,我们需要密切关注的同时也不要盲目跟随,还是要认清趋势,结合项目现状学习。


Kotlin


Kotlin已经成为Android开发的官方语言,Android的新的文档和Sample代码都开始转向 Kotlin,在未来Java将加速被 Kotlin替代。


刚推出时,很多人都不愿意学习,但现在在面试中已经是经常会出现了,很多大公司也都已经拥抱新技术了。现在Kotlin是一个很明显的趋势了,不少新技术都需要结合Kotlin来使用,未来在工作中、面试中所占的比重肯定会更大。


Jetpack+Compose


Jetpack的意义在于帮我们在SDK基础上提供了一系列中间件工具,让我们可以摆脱不断造轮子抄轮子的窘境。同类的解决方案首先考虑Jetpack其次考虑第三方实现,没毛病。


Jetpack本身也会不断吸收优秀的第三方解决方案进来。所以作为开发者实时关注其最新动态就可以了。


Compose是Google I/O 2019 发布的新的声明式的UI框架。其实Google内部自2017年便开始立项,目前API已稳定,构建,预览等开发体验已经趋于完整。


而且新的设计思想绝对是趋势,已经在react和flutter等前端领域中得到验证,ios开发中同期推出的swiftUI更是证明了业界对于这种声明式UI开发趋势的共识。这必将是日后Android app极为重要的编程方式。


开源框架底层原理


现在的面试从头到尾都是比较有深度的技术问题,虽然那些问题看上去在网上都能查到相关的资料,但面试官基本都是根据你的回答持续深入,如果没有真正对技术原理和底层逻辑有一定的了解是无法通过的。


很多看似无理甚至无用的问题,比如 “Okhttp请求复用有没有了解”,其实是面试官想借此看看你对网络优化和Socket协议的理解情况和掌握程度,类似问题都是面试官想借此看看你对相关原理的理解情况和掌握程度,甚至进而引伸到你对架构,设计模式的理解。只有在熟知原理的前提下,你才能够获得面试官的青睐。


Framework


Framework作为Android的框架层,为App提供了很多API调用,但很多机制都是Framework包装好后直接给App用的,如果不懂这些机制的原理,就很难在这基础上进行优化。


像启动监控、掉帧监控、函数插桩、慢函数检测、ANR监控,都需要比较深入的了解Framework,才能知道怎么去监控、利用什么机制监控、函数插桩插到哪里、反射调用该反射哪个类哪个方法哪个属性……


性能优化


性能优化是软件工程的深水区,也是衡量一个程序员能力高低的标准


想要搞清楚性能优化,必须对各种底层原理有着深度的了解,对各种 case非常丰富的经验;很多朋友经常遇到措手不及的问题,大多是因为对出现问题的情况和处理思路模糊不清,导致此原因就是因为没有彻底搞懂底层原理。


性能优化始终穿插在 App 整个研发生命周期中,不管是从 0 到 1 的建立阶段,还是从 1 到 N 打磨阶段,都离不开性能优化。


音视频


伴随着疫情的反复以及5G的普及,本就火爆的音视频技术是越来越热,很多大小厂在这几年也都纷纷入局。但音视频学习起来门槛比较高,没有比较系统的教程或者书籍,网上的博客文章也都是比较零散的。


招聘市场上,同级别的音视频开发要比应用开发薪资高出30%以上。


车载


在智能手机行业初兴起时,包括BAT在内许多传统互联网企业都曾布局手机产业,但是随着手机市场的基本定型,造车似乎又成了各大资本下一个追逐的方向。百度、小米先后宣布造车,阿里巴巴则与上汽集团共同投资创立了,面向汽车全行业提供智能汽车操作系统和智能网联汽车整体解决方案的斑马网络,一时间造车俨然成了资本市场的下一个风口。


而作为移动端操作系统的霸主Android,也以一种新的姿态高调侵入造车领域


关于学习


在学习的过程中,可能会选择看博客自学、看官方文档、看书、看大厂大牛整理的知识点文档、看视频,但要按学习效率来排序的话:报培训班>看视频>知识点>书籍>官方文档>博客


报班,可能很多朋友对于报班这个事情比较抵触,但不可否认,报一个培训班是可以学到很多深层次的、成体系的技术,像之前读书一样,都是捣碎了喂给你,并且培训班其实对于新技术、新趋势是相当敏锐的,可以第一时间接触,也会规避开自学的烦恼。


看视频,基本也是由别人捣碎知识点教会你,但较之培训班的话,视频的知识成体系吗?有没有过时?


大厂大牛整理的知识点文档,大厂大牛技术还是比较可靠的,这类型的知识点文档初版基本是可以放心享用,但如果只是少数人甚至是一个人进行维护的话,当整个文档的知识体系越来越广时,其中的部分知识点可能已经过时但一直没有时间更新


书籍,相比前者就更甚了,一个技术出来,先研究、再整理、修正……直到最后出版被你买到,中间经过的这段时间就是你落后于其他人的地方了,但其中的知识点基本可以肯定成体系、无重大错误。学习比较底层的,不会有很大改动的知识点还是相当不错的。


官方文档,这一块也是我思考了很久才排好,官方文档往往是第一手资源,对于有能力看懂的朋友来说,可以直接上手品尝。但其实很多开发拿到官方文档还是看的一知半解,再者说,自己看可能会有遗漏,还是没有别人一点一点将重点翻开来解读更好


博客,网络上的博客水平参差不齐,通常大家擅长的也不是同一个技术领域,往往是学习一块看A的,另一块看B的,而且网上很多博客都是抄来自己记录的,很多API已经过时了,甚至不少连代码都是完全错误的,这样的学习,可想而知……


最后


一些个人见解,也参考了不少大佬的观点,希望可以给大家带来一些帮助,如果大家有什么不同看法,也欢迎在评论区一起讨论交流


Android路漫漫,共勉!


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

Binder Java 层服务注册过程分析

1. Java 层整体框架 在分析之前,我们要明白,Java 只是一层方便 Java 程序使用的接口,Binder 的核心功能实现都是通过 JNI 调用到 Native 层来实现的,这里先给出 Java 层的整体框架图: 接下来几篇文章我们逐步分析,解密整张...
继续阅读 »

1. Java 层整体框架


在分析之前,我们要明白,Java 只是一层方便 Java 程序使用的接口,Binder 的核心功能实现都是通过 JNI 调用到 Native 层来实现的,这里先给出 Java 层的整体框架图:



接下来几篇文章我们逐步分析,解密整张框架图。


2. 服务注册


Binder 程序示例之 Java 篇 中介绍的示例程序中,Server 端我们使用如下代码注册我们定义的服务:

ServiceManager.addService("hello", new HelloService());

addService 是 frameworks/base/core/java/android/os/ServiceManager.java 中定义的静态方法:

@UnsupportedAppUsage
public static void addService(String name, IBinder service) {
addService(name, service, false, IServiceManager.DUMP_FLAG_PRIORITY_DEFAULT);
}

@UnsupportedAppUsage
public static void addService(String name, IBinder service, boolean allowIsolated) {
addService(name, service, allowIsolated, IServiceManager.DUMP_FLAG_PRIORITY_DEFAULT);
}

@UnsupportedAppUsage
public static void addService(String name, IBinder service, boolean allowIsolated,int dumpPriority) {
try {
getIServiceManager().addService(name, service, allowIsolated, dumpPriority);
} catch (RemoteException e) {
Log.e(TAG, "error in addService", e);
}
}

通过层层调用,调用到 getIServiceManager().addService(name, service, allowIsolated, dumpPriority); :


2.1 getIServiceManager()


我们先看看 getIServiceManager,该方法是定义在 ServiceManager 类中的静态方法:

//frameworks/base/core/java/android/os/ServiceManager.java
@UnsupportedAppUsage
private static IServiceManager getIServiceManager() {
if (sServiceManager != null) {
return sServiceManager;
}

// 等价于 new ServiceManagerProxy(new BinderProxy(0))
// 但是实际过程有点曲折
sServiceManager = ServiceManagerNative
.asInterface(Binder.allowBlocking(BinderInternal.getContextObject()));
return sServiceManager;
}

接着我们逐一分析三个方法调用:

BinderInternal.getContextObject()
Binder.allowBlocking
ServiceManagerNative.asInterface

2.1.1 BinderInternal.getContextObject

//frameworks/base/core/java/com/android/internal/os/BinderInternal.java
// 返回一个 BinderProxy 对象
@UnsupportedAppUsage
public static final native IBinder getContextObject();

getContextObject 是一个 native 方法,在之前的文章中我们提到 BinderInternal 在进程启动时注册了其 native 方法,其 native 实现在 frameworks/base/core/jni/android_util_Binder.cpp 中:

static jobject android_os_BinderInternal_getContextObject(JNIEnv* env, jobject clazz)
{
//此处返回的是 new BpBinder(0)
sp<IBinder> b = ProcessState::self()->getContextObject(NULL);
//此处返回的是 new BinderProxy()
return javaObjectForIBinder(env, b);
}

接着看 getContextObject 的实现:

sp<IBinder> ProcessState::getContextObject(const sp<IBinder>& /*caller*/)
{
return getStrongProxyForHandle(0);
}

sp<IBinder> ProcessState::getStrongProxyForHandle(int32_t handle)
{
sp<IBinder> result;

AutoMutex _l(mLock);

handle_entry* e = lookupHandleLocked(handle);

if (e != nullptr) {
IBinder* b = e->binder;
if (b == nullptr || !e->refs->attemptIncWeak(this)) {
if (handle == 0) {
Parcel data;
status_t status = IPCThreadState::self()->transact(
0, IBinder::PING_TRANSACTION, data, nullptr, 0);
if (status == DEAD_OBJECT)
return nullptr;
}

//走这里
b = BpBinder::create(handle);
e->binder = b;
if (b) e->refs = b->getWeakRefs();
result = b;
} else {
result.force_set(b);
e->refs->decWeak(this);
}
}

return result;
}

BpBinder* BpBinder::create(int32_t handle) {
int32_t trackedUid = -1;
if (sCountByUidEnabled) {
trackedUid = IPCThreadState::self()->getCallingUid();
AutoMutex _l(sTrackingLock);
uint32_t trackedValue = sTrackingMap[trackedUid];
if (CC_UNLIKELY(trackedValue & LIMIT_REACHED_MASK)) {
if (sBinderProxyThrottleCreate) {
return nullptr;
}
} else {
if ((trackedValue & COUNTING_VALUE_MASK) >= sBinderProxyCountHighWatermark) {
ALOGE("Too many binder proxy objects sent to uid %d from uid %d (%d proxies held)",
getuid(), trackedUid, trackedValue);
sTrackingMap[trackedUid] |= LIMIT_REACHED_MASK;
if (sLimitCallback) sLimitCallback(trackedUid);
if (sBinderProxyThrottleCreate) {
ALOGI("Throttling binder proxy creates from uid %d in uid %d until binder proxy"
" count drops below %d",
trackedUid, getuid(), sBinderProxyCountLowWatermark);
return nullptr;
}
}
}
sTrackingMap[trackedUid]++;
}
//走这里
return new BpBinder(handle, trackedUid);
}

代码看着很繁琐,实际流程其实很简单就是 new BpBinder(0)


接着看 javaObjectForIBinder 的实现:

//frameworks/base/core/jni/android_util_Binder.cpp

//当前情景下, val 的类型是 BpBinder
jobject javaObjectForIBinder(JNIEnv* env, const sp<IBinder>& val)
{
if (val == NULL) return NULL;

if (val->checkSubclass(&gBinderOffsets)) {
// It's a JavaBBinder created by ibinderForJavaObject. Already has Java object.
jobject object = static_cast<JavaBBinder*>(val.get())->object();
LOGDEATH("objectForBinder %p: it's our own %p!\n", val.get(), object);
return object;
}

//构造 BinderProxyNativeData 结构体
BinderProxyNativeData* nativeData = new BinderProxyNativeData();
nativeData->mOrgue = new DeathRecipientList;
nativeData->mObject = val;

//gBinderProxyOffsets 中保存了 BinderProxy 类相关的信息
//调用 Java 层 GetInstance 方法获得一个 BinderProxy 对象
jobject object = env->CallStaticObjectMethod(gBinderProxyOffsets.mClass,
gBinderProxyOffsets.mGetInstance, (jlong) nativeData, (jlong) val.get());
if (env->ExceptionCheck()) { //异常处理
// In the exception case, getInstance still took ownership of nativeData.
return NULL;
}
BinderProxyNativeData* actualNativeData = getBPNativeData(env, object);
if (actualNativeData == nativeData) {
// Created a new Proxy
uint32_t numProxies = gNumProxies.fetch_add(1, std::memory_order_relaxed);
uint32_t numLastWarned = gProxiesWarned.load(std::memory_order_relaxed);
if (numProxies >= numLastWarned + PROXY_WARN_INTERVAL) {
// Multiple threads can get here, make sure only one of them gets to
// update the warn counter.
if (gProxiesWarned.compare_exchange_strong(numLastWarned,
numLastWarned + PROXY_WARN_INTERVAL, std::memory_order_relaxed)) {
ALOGW("Unexpectedly many live BinderProxies: %d\n", numProxies);
}
}
} else {
delete nativeData;
}

//返回 BinderProxy
return object;
}

native 代码调用了 BinderProxy 的 getInstance 方法:

// frameworks/base/core/java/android/os/BinderProxy.java
private static BinderProxy getInstance(long nativeData, long iBinder) {
BinderProxy result;
synchronized (sProxyMap) {
try {
result = sProxyMap.get(iBinder);
if (result != null) {
return result;
}
result = new BinderProxy(nativeData);
} catch (Throwable e) {
// We're throwing an exception (probably OOME); don't drop nativeData.
NativeAllocationRegistry.applyFreeFunction(NoImagePreloadHolder.sNativeFinalizer,
nativeData);
throw e;
}
NoImagePreloadHolder.sRegistry.registerNativeAllocation(result, nativeData);
// The registry now owns nativeData, even if registration threw an exception.
sProxyMap.set(iBinder, result);
}
return result;
}

代码很繁琐,但是从结果上来说还是比较简单的:



  • getContextObject 函数 new 了一个 BpBinder(c++结构体),其内部的 handle 是 0

  • javaObjectForIBinder 函数 new 了一个 BinderProxy(Java 对象),其内部成员 mNativeData 是一个 native 层指针,指向一个 BinderProxyNativeData,BinderProxyNativeData 的成员 mObject 指向上述的 BpBinder。


整体结构用一个图表示如下:



2.1.2 Binder.allowBlocking

    //这里传入的是 BinderProxy 对象
public static IBinder allowBlocking(IBinder binder) {
try {
if (binder instanceof BinderProxy) { //走这里
((BinderProxy) binder).mWarnOnBlocking = false;
} else if (binder != null && binder.getInterfaceDescriptor() != null
&& binder.queryLocalInterface(binder.getInterfaceDescriptor()) == null) {
Log.w(TAG, "Unable to allow blocking on interface " + binder);
}
} catch (RemoteException ignored) {
}
return binder;
}

这个方法比较简单,主要是设置 binder 的成员变量 mWarnOnBlocking 为 false。从名字来看,作用是允许阻塞调用。


2.1.3 ServiceManagerNative.asInterface

    //frameworks/base/core/java/android/os/ServiceManagerNative.java
//传入的参数是 BinderProxy
@UnsupportedAppUsage
static public IServiceManager asInterface(IBinder obj)
{
if (obj == null) {
return null;
}

//返回 null
IServiceManager in =
(IServiceManager)obj.queryLocalInterface(descriptor);
if (in != null) {
return in;
}

//走这里,构建一个 ServiceManagerProxy
return new ServiceManagerProxy(obj);
}

//从名字来看,本来要做缓存的,但是没有做
// frameworks/base/core/java/android/os/BinderProxy.java
public IInterface queryLocalInterface(String descriptor) {
return null;
}


最终是构建一个 ServiceManagerProxy 结构体。其内部持有一个 BinderProxy 。


至此,getIServiceManager 的整体流程就分析完了。


2.2 addService

    // frameworks/base/core/java/android/os/ServiceManagerNative.java
public void addService(String name, IBinder service, boolean allowIsolated, int dumpPriority)
throws RemoteException {
Parcel data = Parcel.obtain();
Parcel reply = Parcel.obtain();
data.writeInterfaceToken(IServiceManager.descriptor);
data.writeString(name);
data.writeStrongBinder(service);
data.writeInt(allowIsolated ? 1 : 0);
data.writeInt(dumpPriority);
mRemote.transact(ADD_SERVICE_TRANSACTION, data, reply, 0);
reply.recycle();
data.recycle();
}

构造两个 Parcel 结构,然后调用 mRemote.transact 发起远程过程调用。


mRemote 就是 new ServiceManagerProxy 时传入的 BinderProxy:

 public ServiceManagerProxy(IBinder remote) {
mRemote = remote;
}

进入 frameworks/base/core/java/android/os/BinderProxy.java 查看:

public boolean transact(int code, Parcel data, Parcel reply, int flags) throws RemoteException {
Binder.checkParcel(this, code, data, "Unreasonably large binder buffer");

//......

try {
//关注这里
return transactNative(code, data, reply, flags);
} finally {
//......
}
}

//native 方法
public native boolean transactNative(int code, Parcel data, Parcel reply,int flags) throws RemoteException;


transact 会调用 transactNative 发起远程调用,transactNative 是一个 native 方法,具体实现在 frameworks/base/core/jni/android_util_Binder.cpp

// obj 对应类型为 BinderProxy
static jboolean android_os_BinderProxy_transact(JNIEnv* env, jobject obj,
jint code, jobject dataObj, jobject replyObj, jint flags) // throws RemoteException
{
if (dataObj == NULL) {
jniThrowNullPointerException(env, NULL);
return JNI_FALSE;
}

// Java 对象 转为 c++ 对象
Parcel* data = parcelForJavaObject(env, dataObj);
if (data == NULL) {
return JNI_FALSE;
}

// Java 对象 转为 c++ 对象
Parcel* reply = parcelForJavaObject(env, replyObj);
if (reply == NULL && replyObj != NULL) {
return JNI_FALSE;
}

//拿到 BinderProxyNativeData 成员的 mObject,实际是一个 BpBinder
IBinder* target = getBPNativeData(env, obj)->mObject.get();
if (target == NULL) {
jniThrowException(env, "java/lang/IllegalStateException", "Binder has been finalized!");
return JNI_FALSE;
}

ALOGV("Java code calling transact on %p in Java object %p with code %" PRId32 "\n",
target, obj, code);


bool time_binder_calls;
int64_t start_millis;
if (kEnableBinderSample) {
// Only log the binder call duration for things on the Java-level main thread.
// But if we don't
time_binder_calls = should_time_binder_calls();

if (time_binder_calls) {
start_millis = uptimeMillis();
}
}

//BpBinder 发起远程调用
status_t err = target->transact(code, *data, reply, flags);

if (kEnableBinderSample) {
if (time_binder_calls) {
conditionally_log_binder_call(start_millis, target, code);
}
}

if (err == NO_ERROR) {
return JNI_TRUE;
} else if (err == UNKNOWN_TRANSACTION) {
return JNI_FALSE;
}

signalExceptionForError(env, obj, err, true /*canThrowRemoteException*/, data->dataSize());
return JNI_FALSE;
}


可以看出,绕了一圈还是通过 native 层的 BpBinder 发起远程调用,native 层的调用过程可以参考之前的文章Binder 服务注册过程情景分析之 C++ 篇


关于


我叫阿豪,2015 年本科毕业于国防科技大学指挥自动化专业,毕业后,从事信息化装备的研发工作。主要研究方向为 Android Framework 与 Linux Kernel,2023年春节后开始做 Android Framework 相关的技术分享。


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

Spring Boot如何优雅实现结果统一封装和异常统一处理

1.概述 当下基于Spring Boot框架开发的系统几乎都是前后端分离的,也都是基于RESTFUL风格进行接口定义开发的,意味着前后端开发大部分数据的传输格式都是json,因此定义一个统一规范的数据格式返回有利于前后端的交互与UI的展示 Restful风格是...
继续阅读 »

1.概述


当下基于Spring Boot框架开发的系统几乎都是前后端分离的,也都是基于RESTFUL风格进行接口定义开发的,意味着前后端开发大部分数据的传输格式都是json,因此定义一个统一规范的数据格式返回有利于前后端的交互与UI的展示


Restful风格是什么?


RESTFUL(英文:Representational State Transfer,简称REST)可译为"表现层状态转化”,是一种网络应用程序的设计风格和开发方式,是资源定位和资源操作的一种风格。不是标准也不是协议。基于HTTP可以使用 XML 格式定义或 JSON 格式定义。最常用的数据格式是JSON。由于JSON能直接被JavaScript读取,所以,使用JSON格式的REST风格的API具有简单、易读、易用的特点。Restful风格最大的特点为:资源、统一接口、URI和无状态。


对于我们Web开发人员而言,restful风格简单来说就是使用一个url地址表示一个唯一的资源。然后把原来的请求参数加入到请求资源地址中。把原来请求的增,删,改,查操作路径标识,改为使用HTTP协议中请求方式GET、POST、PUT、DELETE表示。


传统的方式是:http://127.0.0.1:8080/shepherd/user/add 表示新增用户的接口,需要在路径上加以增删改查标识,如果我们要修改:那么路径是:http://127.0.0.1:8080/shepherd/user/update


但是我们基于restful风格就比较优雅:http://127.0.0.1:8080/shepherd/user,增删改查都可以用这个路径,使用请求方法来进行区别即可,如post代表新增,put代表修改等。



项目推荐:基于SpringBoot2.x、SpringCloud和SpringCloudAlibaba企业级系统架构底层框架封装,解决业务开发时常见的非功能性需求,防止重复造轮子,方便业务快速开发和企业技术栈框架统一管理。引入组件化的思想实现高内聚低耦合并且高度可配置化,做到可插拔。严格控制包依赖和统一版本管理,做到最少化依赖。注重代码规范和注释,非常适合个人学习和企业使用


Github地址github.com/plasticene/…


Gitee地址gitee.com/plasticene3…


微信公众号Shepherd进阶笔记



2.返回结果统一封装


定义一个统一的标准返回格式,有助于后端接口开发的规范性和通用性,同时也提高了前后端联调的效率,前端通过接收同一返回结构体进行相应映射处理,不用担心每个接口返回的格式都不一样而做一一适配了。


2.1 定义返回统一结构体

@Data
public class ResponseVO<T> implements Serializable {

   private Integer code;

   private String msg;

   private T data;

   public ResponseVO() {

  }

   public ResponseVO(Integer code, String msg) {
       this.code = code;
       this.msg = msg;
  }

   public ResponseVO(Integer code, T data) {
       this.code = code;
       this.data = data;
  }

   public ResponseVO(Integer code, String msg, T data) {
       this.code = code;
       this.msg = msg;
       this.data = data;
  }

   private ResponseVO(ResponseStatusEnum resultStatus, T data) {
       this.code = resultStatus.getCode();
       this.msg = resultStatus.getMsg();
       this.data = data;
  }

   /**
    * 业务成功返回业务代码和描述信息
    */
   public static ResponseVO<Void> success() {
       return new ResponseVO<Void>(ResponseStatusEnum.SUCCESS, null);
  }

   /**
    * 业务成功返回业务代码,描述和返回的参数
    */
   public static <T> ResponseVO<T> success(T data) {
       return new ResponseVO<T>(ResponseStatusEnum.SUCCESS, data);
  }

   /**
    * 业务成功返回业务代码,描述和返回的参数
    */
   public static <T> ResponseVO<T> success(ResponseStatusEnum resultStatus, T data) {
       if (resultStatus == null) {
           return success(data);
      }
       return new ResponseVO<T>(resultStatus, data);
  }

   /**
    * 业务异常返回业务代码和描述信息
    */
   public static <T> ResponseVO<T> failure() {
       return new ResponseVO<T>(ResponseStatusEnum.SYSTEM_ERROR, null);
  }

   /**
    * 业务异常返回业务代码,描述和返回的参数
    */
   public static <T> ResponseVO<T> failure(ResponseStatusEnum resultStatus) {
       return failure(resultStatus, null);
  }

   /**
    * 业务异常返回业务代码,描述和返回的参数
    */
   public static <T> ResponseVO<T> failure(ResponseStatusEnum resultStatus, T data) {
       if (resultStatus == null) {
           return new ResponseVO<T>(ResponseStatusEnum.SYSTEM_ERROR, null);
      }
       return new ResponseVO<T>(resultStatus, data);
  }

   public static <T> ResponseVO<T> failure(Integer code, String msg) {
       return new ResponseVO<T>(code, msg);
  }
}


这里包含了三个字段信息:



  1. code 状态值:由后端统一定义各种返回结果的状态码, 比如说code=200代表接口调用成功

  2. msg 描述:本次接口调用的结果描述,比如说后端抛出的业务异常信息就在这里体现

  3. data 数据:本次返回的数据,泛型类型意味着可以支持任意类型的返回数据


成功返回如下:

{
 "code": 200,
 "msg": "OK",
 "data": {
   "id": 123,
   "name": "shepherd"
}
}

业务异常返回如下:

{
 "code": 400,
 "msg": "当前用户不存在"
}

按照上面成功返回的示例我们接口定义如下:

    @GetMapping("/test/user")
   public ResponseVO<User> testUser() {
       User user = new User();
       user.setId(123l);
       user.setName("shepherd");
       return ResponseVO.success(user);
  }

可以看到接口方法返回类型为ResponseVO<User>,然后通过ResponseVO.success()对返回结果进行包装后返回给前端。这就意味着写一个接口都需要调用ResultData.success()这行代码对结果进行包装,有点重复劳动不够优雅的感觉。还有一种情况,有些项目服务前期为了赶时间开发时没有返回统一结构,等项目上线了有时间之后按照规范需要对后端接口返回结构进行统一,这时候如果复杂的系统已经有成百上千的接口了,如果一个个地像上面说的那样把接口返回类型改为ResponseVO<T>,再用ResponseVO.success()进行结果包装,工作量不小,也比较繁琐。


2.2 高级优雅实现统一结果封装


为了解决上面阐述的问题,我们借助于Spring Boot提供的ResponseBodyAdvice进行了高级实现。


ResponseBodyAdvice的作用:拦截Controller方法的返回值,统一处理返回值/响应体,一般用来统一返回格式,加解密,签名等等。 我们在分享 Spring Boot如何对接口参数进行加解密就有提到过这个类进行返回结果参数的加密。


先来看下ResponseBodyAdvice的源码

public interface ResponseBodyAdvice<T> {
 /**
 * 是否支持advice功能
 * true 支持,false 不支持
 */
   boolean supports(MethodParameter var1, Class<? extends HttpMessageConverter<?>> var2);

  /**
 * 对返回的数据进行处理
 */
   @Nullable
   T beforeBodyWrite(@Nullable T var1, MethodParameter var2, MediaType var3, Class<? extends HttpMessageConverter<?>> var4, ServerHttpRequest var5, ServerHttpResponse var6);
}

所以我们编写一个具体实现类即可:

@RestControllerAdvice
@Slf4j
public class ResponseResultBodyAdvice implements ResponseBodyAdvice<Object> {
   @Resource
   private ObjectMapper objectMapper;

   private static final Class<? extends Annotation> ANNOTATION_TYPE = ResponseResultBody.class;

   /**
    * 判断类或者方法是否使用了 @ResponseResultBody
    */
   @Override
   public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
       return AnnotatedElementUtils.hasAnnotation(returnType.getContainingClass(), ANNOTATION_TYPE) || returnType.hasMethodAnnotation(ANNOTATION_TYPE);
  }

   /**
    * 当类或者方法使用了 @ResponseResultBody 就会调用这个方法
    */
   @SneakyThrows
   @Override
   public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
       //如果返回类型是string,那么springmvc是直接返回的,此时需要手动转化为json
       // 当body都为null时,下面的if判断条件都不满足,如果接口返回类似为String,会报错com.shepherd.fast.global.ResponseVO cannot be cast to java.lang.String
       Class<?> returnClass = returnType.getMethod().getReturnType();
       if (body instanceof String || Objects.equals(returnClass, String.class)) {
           String value = objectMapper.writeValueAsString(ResponseVO.success(body));
           return value;
      }
       // 防止重复包裹的问题出现
       if (body instanceof ResponseVO) {
           return body;
      }
       return ResponseVO.success(body);
  }

}


这里使用到一个自定义注解@ResponseResultBody:

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
@Documented
@ResponseBody
public @interface ResponseResultBody {

}

注入bean

    @Bean
   public ResponseResultBodyAdvice responseResultBodyAdvice() {
       return new ResponseResultBodyAdvice();
  }

从上面我们自己定义实现类ResponseResultBodyAdvice#supports()可以看到,只要我们的Controller类或者方法上使用了ResponseResultBody注解,就会执行方法#beforeBodyWrite(),使用ResponseVO对结果进行包装统一返回。


实现类上使用了RestControllerAdvice注解,@RestControllerAdvice是一个组合注解,由@ControllerAdvice、@ResponseBody组成,而@ControllerAdvice继承了@Component,因此@RestControllerAdvice本质上是个Component,用于定义@ExceptionHandler,@InitBinder和@ModelAttribute方法,适用于所有使用@RequestMapping方法,该注解特点如下:


1.通过@ControllerAdvice注解可以将对于控制器的全局配置放在同一个位置。


2.注解了@RestControllerAdvice的类的方法可以使用@ExceptionHandler、@InitBinder、@ModelAttribute注解到方法上。


3.@RestControllerAdvice注解将作用在所有注解了@RequestMapping的控制器的方法上。


4.@ExceptionHandler:用于指定异常处理方法。当与@RestControllerAdvice配合使用时,用于全局处理控制器里的异常。


5.@InitBinder:用来设置WebDataBinder,用于自动绑定前台请求参数到Model中。


6.@ModelAttribute:本来作用是绑定键值对到Model中,当与@ControllerAdvice配合使用时,可以让全局的@RequestMapping都能获得在此处设置的键值对


实现类ResponseResultBodyAdvice使用该注解就是满足上面的第3种情况,将拦截作用在所有注解了@RequestMapping的控制器的方法进行判断是否使用了注解@ResponseResultBody,从而对接口结构进行统一ResponseVO包装。


3.全局异常统一处理


使用统一返回结果时,还有一种情况,就是程序由于运行时异常导致报错的结果,有些异常我们可能无法提前预知,不能正常走到我们return的ResponseVO对象返回,因此,我们需要定义一个统一的全局异常来捕获这些信息,并作为一种结果返回给控制层。


使用上面的@ControllerAdvice@ExceptionHandler进行全局异常统一处理:

@ControllerAdvice
@Slf4j
public class GlobalExceptionHandler {

   /**
    * 全局异常处理
    * @param e
    * @return
    */
   @ResponseBody
   @ResponseStatus(HttpStatus.BAD_REQUEST)
   @ExceptionHandler(Exception.class)
   public ResponseVO exceptionHandler(Exception e){
       // 处理业务异常
       if (e instanceof BizException) {
           BizException bizException = (BizException) e;
           if (bizException.getCode() == null) {
               bizException.setCode(ResponseStatusEnum.BAD_REQUEST.getCode());
          }
           return ResponseVO.failure(bizException.getCode(), bizException.getMessage());
      } else if (e instanceof MethodArgumentNotValidException) {
           // 参数检验异常
           MethodArgumentNotValidException methodArgumentNotValidException = (MethodArgumentNotValidException) e;
           Map<String, String> map = new HashMap<>();
           BindingResult result = methodArgumentNotValidException.getBindingResult();
           result.getFieldErrors().forEach((item)->{
               String message = item.getDefaultMessage();
               String field = item.getField();
               map.put(field, message);
          });
           log.error("数据校验出现错误:", e);
           return ResponseVO.failure(ResponseStatusEnum.BAD_REQUEST, map);
      } else if (e instanceof HttpRequestMethodNotSupportedException) {
           log.error("请求方法错误:", e);
           return ResponseVO.failure(ResponseStatusEnum.BAD_REQUEST.getCode(), "请求方法不正确");
      } else if (e instanceof MissingServletRequestParameterException) {
           log.error("请求参数缺失:", e);
           MissingServletRequestParameterException ex = (MissingServletRequestParameterException) e;
           return ResponseVO.failure(ResponseStatusEnum.BAD_REQUEST.getCode(), "请求参数缺少: " + ex.getParameterName());
      } else if (e instanceof MethodArgumentTypeMismatchException) {
           log.error("请求参数类型错误:", e);
           MethodArgumentTypeMismatchException ex = (MethodArgumentTypeMismatchException) e;
           return ResponseVO.failure(ResponseStatusEnum.BAD_REQUEST.getCode(), "请求参数类型不正确:" + ex.getName());
      } else if (e instanceof NoHandlerFoundException) {
           NoHandlerFoundException ex = (NoHandlerFoundException) e;
           log.error("请求地址不存在:", e);
           return ResponseVO.failure(ResponseStatusEnum.NOT_EXIST, ex.getRequestURL());
      } else {
           //如果是系统的异常,比如空指针这些异常
           log.error("【系统异常】", e);
           return ResponseVO.failure(ResponseStatusEnum.SYSTEM_ERROR.getCode(), ResponseStatusEnum.SYSTEM_ERROR.getMsg());
      }
  }

}


注入bean

    @Bean
  public GlobalExceptionHandler globalExceptionHandler() {
      return new GlobalExceptionHandler();
  }

通过以上步骤就可以对异常进行全局异常统一处理,这样做的好处不仅是可以对未知异常进行处理之后按照统一结构返回给前端,同时还能对异常处理之后进行error级别的日志输出,这样才能结合logback,log4j2等日志框架写入到日志文件中,以便后续查看异常错误日志排查追踪问题,否则异常信息不会被记录在error日志文件中。


4.总结


基于以上全部内容,我们讲述了如何优雅实现返回结果统一封装和全局异常统一处理,这样可以规范后端接口输出,同时也增强了项目服务的健壮性,可以说这两个统一处理是当下项目服务的必须要求,所以我们得了解一下哦。


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

mysql 到底是 join性能好,还是in一下更快呢

先总结: 数据量小的时候,用join更划算 数据量大的时候,join的成本更高,但相对来说join的速度会更快 数据量过大的时候,in的数据量过多,会有无法执行SQL的问题,待解决 事情是这样的,去年入职的新公司,之后在代码review的时候被提出说,不要...
继续阅读 »

先总结:



  1. 数据量小的时候,用join更划算

  2. 数据量大的时候,join的成本更高,但相对来说join的速度会更快

  3. 数据量过大的时候,in的数据量过多,会有无法执行SQL的问题,待解决


事情是这样的,去年入职的新公司,之后在代码review的时候被提出说,不要写join,join耗性能还是慢来着,当时也是真的没有多想,那就写in好了,最近发现in的数据量过大的时候会导致sql慢,甚至sql太长,直接报错了。这次来浅究一下,到底是in好还是join好,仅目前认知探寻,有不对之处欢迎指正


以下实验仅在本机电脑试验


一、表结构


1、用户表


image.png

 CREATE TABLE `user` (
`id` int NOT NULL AUTO_INCREMENT,
`name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '姓名',
`gender` smallint DEFAULT NULL COMMENT '性别',
`mobile` varchar(11) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '手机号',
`create_time` datetime NOT NULL COMMENT '创建时间',
PRIMARY KEY (`id`),
UNIQUE KEY `mobile` (`mobile`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1005 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci

2、订单表


image.png

CREATE TABLE `order` (
`id` int unsigned NOT NULL AUTO_INCREMENT,
`price` decimal(18,2) NOT NULL,
`user_id` int NOT NULL,
`product_id` int NOT NULL,
`status` smallint NOT NULL DEFAULT '0' COMMENT '订单状态',
PRIMARY KEY (`id`),
KEY `user_id` (`user_id`),
KEY `product_id` (`product_id`)
) ENGINE=InnoDB AUTO_INCREMENT=202 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci

二、先来试少量数据的情况


用户表插一千条随机生成的数据,订单表插一百条随机数据


查下所有的订单以及订单对应的用户


下面从三个维度来看



多表连接查询成本 = 一次驱动表成本 + 从驱动表查出的记录数 * 一次被驱动表的成本



1、join



JOIN: explain format=json select order.id, price, user.name from order join user on order.user_id = user.id;


子查询: select order.id,price,user.name from order,user where user_id=user.id;



image.png


2、分开查



select id,price,user_id from order;



image.png



select name from user where id in (8, 11, 20, 32, 49, 58, 64, 67, 97, 105, 113, 118, 129, 173, 179, 181, 210, 213, 215, 216, 224, 243, 244, 251, 280, 309, 319, 321, 336, 342, 344, 349, 353, 358, 363, 367, 374, 377, 380, 417, 418, 420, 435, 447, 449, 452, 454, 459, 461, 472, 480, 487, 498, 499, 515, 525, 525, 531, 564, 566, 580, 584, 586, 592, 595, 610, 633, 635, 640, 652, 658, 668, 674, 685, 687, 701, 718, 720, 733, 739, 745, 751, 758, 770, 771, 780, 806, 834, 841, 856, 856, 857, 858, 882, 934, 942, 983, 989, 994, 995); [in的是order查出来的所有用户id]



image.png


如此看来,分开查和join查的成本并没有相差许多


3、代码层面


主要用php原生写了脚本,用ab进行10个同时的请求,看下时间,进行比较



ab -n 100 -c 10



in
 $mysqli = new mysqli('127.0.0.1', 'root', 'root', 'test');
if ($mysqli->connect_error) {
die('Connect Error (' . $mysqli->connect_errno . ') ' . $mysqli->connect_error);
}

$result = $mysqli->query('select `id`,price,user_id from `order`');
$orders = $result->fetch_all(MYSQLI_ASSOC);

$userIds = implode(',', array_column($orders, 'user_id')); // 获取订单中的用户id
$result = $mysqli->query("select `id`,`name` from `user` where id in ({$userIds})");
$users = $result->fetch_all(MYSQLI_ASSOC);// 获取这些用户的姓名

// 将id做数组键
$userRes = [];
foreach ($users as $user) {
$userRes[$user['id']] = $user['name'];
}

$res = [];
// 整合数据
foreach ($orders as $order) {
$current = [];
$current['id'] = $order['id'];
$current['price'] = $order['price'];
$current['name'] = $userRes[$order['user_id']] ?: '';
$res[] = $current;
}
var_dump($res);

// 关闭mysql连接

$mysqli->close();

image.png


join
$mysqli = new mysqli('127.0.0.1', 'root', 'root', 'test');
if ($mysqli->connect_error) {
die('Connect Error (' . $mysqli->connect_errno . ') ' . $mysqli->connect_error);
}

$result = $mysqli->query('select order.id, price, user.`name` from `order` join user on order.user_id = user.id;');
$orders = $result->fetch_all(MYSQLI_ASSOC);

var_dump($orders);
$mysqli->close();

image.png
看时间的话,明显join更快一些


三、试下多一些数据的情况


user表现在10000条数据,order表10000条试下


1、join


image.png


2、分开


order
image.png


user


image.png


3、代码层面


in


image.png


join


image.png


三、试下多一些数据的情况


随机插入后user表十万条数据,order表一百万条试下


1、join


image.png


2、分开


order


image.png


user


order查出来的结果过长了,,,


3、代码层面


in


image.png


join


image.png


四、到底怎么才能更好


注:对于本机来说100000条数据不少了,更大的数据量害怕电脑卡死


总的来说,当数据量小时,可能一页数据就够放的时候,join的成本和速度都更好。数据量大的时候确实分开查的成本更低,但是由于数据量大,造成循环的成本更多,代码执行的时间也就越长。实验过程中发现,当in的数据量过大的时候,sql过长会无法执行,可能还要拆开多条sql进行查询,这样的查询成本和时间一定也会更长,而且如果有分页的需求的话,也无法满足。。。


感觉这两个方法都不是太好,各位小伙伴,有没有更好的方法呢?


终于在尾巴上赶上了这个活动


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

同事问我为什么电脑屏幕上会有那么多球在飘

记得以前用的Windows电脑里面,有一个屏保程序就是在屏幕上出现很多飘来飘去的球,当球碰到电脑边缘的时候,会反弹到相反的方向,然后最近就琢磨着能不能使用Compose DeskTop也实现一个这样的效果,那以后我的Mac屏幕上也能出现好多小球,那简直是泰裤辣...
继续阅读 »

记得以前用的Windows电脑里面,有一个屏保程序就是在屏幕上出现很多飘来飘去的球,当球碰到电脑边缘的时候,会反弹到相反的方向,然后最近就琢磨着能不能使用Compose DeskTop也实现一个这样的效果,那以后我的Mac屏幕上也能出现好多小球,那简直是泰裤辣~


设计思路


我们把整体动效拆分一下总共有五步,每一步都不是很难




  • 第一步:使用循环动画不断改变小球位移的x坐标与y坐标,x坐标的变化范围是0到窗口宽度的最大值,y坐标的变化范围是0到窗口高度的最大值

  • 第二步:判断当x,y坐标到达自己的最大值的边界值的时候,将各自的变化范围的初始值与最终值互相对换一下,达到往相反方向移动的效果

  • 第三步:通过改变tween函数的durationMilliseasing属性,来改变小球的位移速度与位移路线

  • 第四步:将小球的动画需要的属性作为函数的入参,达到可以在上层定制小球动画的效果

  • 第五步:将窗口的宽度与高度更改成屏幕的宽高,背景色改成透明



让球动起来


首先我们来把球的样式做出来,球本身就是个圆形,我们使用Surface组件就可以完成,里面再包一个Box组件,这样做的目的是因为Surface没有办法设置渐变的背景色,我们如果想要让圆形看起来立体一些,就需要让背景色带点渐变,所以渐变的工作就交给里面的Box组件来完成


image.png

然后就可以把这个球放到我们的窗口里面去了,在这之前我们先创建三个常量,分别是窗口的宽高最大值以及小球的大小


image.png

然后把这三个常量分别设置给Window组件以及ball组件,代码与效果就如下图所示


image.png

接下去就是让这个球动起来了,我们通过改变球的位移坐标来实现球体的移动,这里给位移坐标的x,y分别设置一个无限循环动画,动画的初始值是为0,目标值为窗口的宽高,动画时间设置为5秒,然后让这个动画过程线性改变,实现过程如下所示


image.png

我们给Surface组件添加了offset操作符,让它接收mainxmainy的变化值,我们这个球就动起来了


0602aa2.gif

改变位移方向


现在已经让球动起来了,接下来就是要考虑如何让球“碰壁”以后反弹,由于我们的初始位置在窗口左上角,所以我们可以先做碰到下面以后的反弹以及碰到右边以后的反弹,也就是当x坐标到达或者接近x轴位移的最大值,或者y坐标到达或者接近y轴的位移最大值以后,我们将mainxmainy的初始值与目标值对调一下,这样就能往相反方向移动了,注意这里说的是位移最大值,不是窗口的宽高,因为球位移坐标是从球的左上角开始计算的,当碰到窗口边界的时候,其实位移距离是窗口的宽高减去小球的直径大小,所以我们再加上两个常量作为位移的最大值,方便后面计算时使用


image.png

然后如果想要在无限循环动画里面改变初始值与目标值,我们就要使用Animatable来切换,所以这里再创建四个Animatable的变量,分别代表x,y轴的初始值与目标值


image.png

创建好了以后,就直接把mainxmainy的初始值与目标值替换成了新建的四个Animatable变量,这样当我们去切换它们的值以后,mainxmainy的变化范围也发生了改变,而Animatable的切换函数snapTo是一个挂起函数,所以还需要一个协程作用域,我们这里使用rememberCoroutineScope函数来创建,那么小球碰到窗口下边与右边的反弹代码就有了


image.png

这边判断到达边界的条件不是mainx.value.value == offsetx.value的原因是因为通过打印日志发现,mainx或者mainy的变化值不会一直刚好是offsetX.value或者offsetY.value,所以只能把判断当两个值接近的时候当作小球移动到边界的条件,我们运行下看看反弹效果


0602aa3.gif

动图上看不出来,实际效果其实达到边界时候有点细微的抖动,这也跟我们刚刚那个边界值的判断条件有关,不过也不影响功能,我们按照这个方式把碰到左边与上边的代码也加上,一个完整的球体移动动画就做好了


image.png
0602aa4.gif

现在已经能够实现小球碰到窗口四周反弹的效果了,但是实现方式还是比较繁琐的,又是协程又是切换又是看边界值的,我们其实还有更简单的办法,因为不管是x轴的值还是y轴的值,它的变化范围始终在两个值之间,差别就是每次起始位置不同,那么这就是一个反复的过程,而我们这个循环动画其实就可以设置反复模式,使用repeatMode属性,值取RepeatMode.Reverse就可以了,我们试一下


image.png

我们看到现在我们把那四个Animatable都去掉了,mainxmainy的初始值与目标值又回到了固定值,区别就是增加了repeatMode,现在我们在看下实现效果咋样


0602aa5.gif

看起来好像差别不大,但其实碰到边界后的效果比之前要好多了,因为不用去关心那一点误差,而且也可以随意设置动画时间,之前为了让动画的变化值不要变化的太大,所以动画时间我是最小只能设置成5秒,现在的动效看起来就舒服多了


改变速度与路线


动画速度的话我们刚刚其实已经实现了,通过改变动画时间durationMills来实现,但是由于我们的easing设置的是LinearEasing线性变化,所以小球的位移路线永远都是沿着一根直线移动的,我们可以通过改变easing的值,来改变小球的位移路线,比如现在我将easing改成FastOutSlowInEasing


image.png

得到的效果就是这样的


0602aa6.gif

这球一下子就变得好像“有智商”了一样,感觉要“撞了”就马上减速,然后换个方向继续飘


属性作为参数,让小球可定制


想要定制小球动画的话,首先要确定好哪些属性可以拿出来定制,通过上面的开发,我们这个小球动画可以被定制的属性有以下几个



  • ballSize:小球的大小

  • ballColor:小球的颜色

  • xTime:小球位移x轴上的动画时间

  • yTime:小球位移y轴上的动画时间

  • xAnimateEasing:小球x轴上动画的变化速度

  • yAnimateEasing:小球y轴上动画的变化速度


这样子的话我们ball函数的参数列表就如下所示


image.png

然后再将代码中的对应位置用参数来代替


image.png

我们这里把计算最大位移的步骤也移到函数里面了,这样就可以根据不同的小球大小来计算各自的位移距离,我们这个ball函数到这里算是完成了,现在我们就可以想弄几个小球就弄几个小球了,比如我这边就弄了这么几个小球


image.png

下面就是一堆小球的效果


0602aa7.gif

我们再改下小球的样式,将小球弄成背景有点透明的样子,让飘动的小球看起来像是气泡一样,改完以后的小球代码如下


image.png

然后再将调用ball函数的地方,ballColor的入参也改成带点透明值


image.png
0604aa1.gif

如效果图所示,是不是有那么点意思了呢,现在我们进行最后一步。


将窗口透明,宽高增大为全屏


想要将窗口弄成透明的话,可以使用Window组件的transparentundecorated属性,代码如下


image.png

然后把screenWidthscreenHeight大小设置成全屏大小就可以了,我们使用ToolKit来获取屏幕宽高


image.png

还差一步,因为到了这里就算screenWidthscreenHeight设置成全屏宽高了,但实际上所在的窗口并没有真正的全屏,它跟屏幕左边留有一点距离,然后右边延伸至屏幕外边了,所以我们需要让整个窗口居中显示,使用WindowPosition,这个是WindowState里面的一个参数,我们在WindowPosition中设置成居中对齐就可以了


image.png

最终我们得到的效果就是这样的


0604aa2small.gif

总结


整体效果实现起来还是蛮容易的,总共代码加一块也不到一百行,感觉把Window设置成透明以后,DeskTop开发变得好玩多了,大家有兴趣的也可以尝试下,所有元素都可以按照自己喜好来定制,去设计属于自己的屏保程序


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

天气太热,希望这个小风扇能给你带来一点凉意

最近气温多变,这几天又回到了三十多度的高温天气,在这样的天气里面如果办公室里面不开个空调或者电风扇的话,那么是很难集中精神工作的,空调的话可能每个办公室都有,但风扇的话估计要自己去准备了,如果还没来得及准备的话,那么可以先考虑下在桌面上画个风扇看着它吹,毕竟古...
继续阅读 »

最近气温多变,这几天又回到了三十多度的高温天气,在这样的天气里面如果办公室里面不开个空调或者电风扇的话,那么是很难集中精神工作的,空调的话可能每个办公室都有,但风扇的话估计要自己去准备了,如果还没来得及准备的话,那么可以先考虑下在桌面上画个风扇看着它吹,毕竟古人有望梅止渴,我们今天就来画风扇降温


源码地址


github.com/coffeetang/…


准备工作


首先考虑下这个风扇的结构,我们这个风扇总共有这几个部分组成,分别是底座,立柱,扇框,扇叶,底座上有总开关,有可以调节风速强度的开关,那么整体来看是个上下结构,底座在最下面,其他的在上面,除了底座其余的都绘制在一个Canvas里面,大致结构如下


image.png

扇叶


风扇的扇叶,其实可以看成是在画布上画扇形,而我们画扇形就要用到函数drawArc,这个函数的传参列表如下所示


image.png

参数我们都很熟悉了,这里绘制扇形所需要用到的参数有



  • brush:用来设置渐变色的

  • startAngle:表示起始角度

  • sweepAngle:表示扇形角度

  • userCenter:表示扇形两端是否与圆心相连

  • topLeft:表示绘制扇形范围的左上角坐标

  • size:表示扇形的绘制范围

  • style:默认值就是Fill填充的,所以我们可以不用去挂心


根据参数,我们需要创建几个变量,首先是圆心坐标,它是取的Canvas的中心坐标,所以无论窗口变大变小,我们的圆心坐标都会在整个画布的中心位置


image.png

其次是我们的半径,半径的大小决定了整个扇形绘制的范围大小


image.png

那么我们绘制一个扇形的代码就是下面这样的


image.png
image.png

一个扇形就这样画出来了,而一个风扇总共有三个扇叶,咱要画三个扇形,而且是圆周上等分的,该怎么画呢?这里使用这个方法,首先创建一个数组,这个数组里面是每个扇形要用到的渐变颜色


image.png

然后再创建一个数组,这个数组里面是每个扇形的startAngle


image.png

那么首先我们就可以通过遍历数组的方式,把六个扇形都画出来


image.png
image.png

我们看到这个时候界面上展示的就是一个由六个扇形组成的大圆形,然后我们把colorList里面每隔一组颜色就把颜色改成透明的,那么这个圆形就看起来就像是三个被等分的出来的扇形一样了


image.png
image.png

扇框与立柱


扇叶已经完成了,接下来就是绘制扇框与立柱的工作,这两个都比较容易,立柱就是从圆心位置向下绘制出一条直线


image.png
image.png

然后我们在扇形靠外一点的位置绘制一个圆形作为扇框的边框,这里用到了drawPath函数,drawPath的第一个参数是Path,所以我们先将Path做出来


image.png

然后再调用drawPath函数,将framePath传进去


image.png
image.png

然后就是风扇前面的网罩,网罩常见的有从中心向外延伸出去的一条条直线,也有的就是一个个井字格组成起来的样子,这边按照前者做个网罩样式出来,这种样式与扇叶的思想有点接近,都是按照角度在一个圆周上等分的绘制样式,所以我们首先需要确定好这些角度,也有一个list维护起来


image.png

这里就是有45根线,每过8度画一根,而我们知道绘制线条用到的函数drawLine需要知道一个start坐标和一个end坐标,start都知道是圆形坐标,而end的xy坐标就要根据角度与半径算出来了,计算的代码如下所示


image.png

第一个参数就是网罩的半径长度,第二个参数为圆形x坐标或者y坐标,第三个参数是角度,那么我们就可以使用这两个函数,遍历lineAngleList来绘制出网罩


image.png
image.png

绘制风扇部分就完成了,下面开始开发底座上的开关


总开关与强度开关


底座上的开关分两个区域,左边是调节强度的区域,右边是总开关区域,总开关设计成一个滑块的样式,滑块默认在左边为关闭状态,点击或者拖动滑块,滑块滑动到右边,状态变成开启,滑块高亮,首先建立一个变量用来记录当前开关状态,再定义两个常量分别代表关闭与打开


image.png

默认为关闭状态,滑块的实现我们需要用到swipeable操作符,参数列表如下


image.png

其中我们需要用到的参数有



  • state:滑块的状态,需要监听滑块的状态来更新开关的状态值

  • anchors:锚点,某个位置对应滑块的一个状态值

  • thresholds:阈值,判读一个鼠标拖动事件滑动到某个位置的时候,这个位置属于哪种状态,那么当鼠标停止拖动时候,滑动可以animate到对应状态位置

  • orientation:拖动方向


根据需要的参数我们来创建对应的变量,滑块的代码如下所示


image.png
image.png

这里滑块的背景颜色会根据开关状态来变化,而开关的状态我们就通过监听swipeState来更新


image.png
0606aa1.gif

我们拖动滑块改变开关状态的功能如上图所示完成了,而点击滑块边上区域来改变开关状态就需要在滑块父布局上添加点击事件,点击一次更新滑块状态,而更新操作需要用到SwipeableState里面的animateTo函数,这个函数是个挂起函数,所以还需要给它提供一个协程作用域,恰好我们更新滑块状态是根据点击来触发的,所以这里选择使用LaunchedEffect函数


image.png
0606aa2.gif

滑块部分就做完了,然后是左边区域的调节强度功能,这个区域准备由三个色块组成,每个色块都可以点击,每点击一个色块,强度设置成对应级别,符合该级别的色块颜色高亮,否则就变暗,所以这里也需要一个表示强度的变量值


image.png

然后添加上三个色块以及每个色块的逻辑代码和点击事件


image.png
0606aa3.gif

这里每个色块高亮的条件都不一样,但是有个共同条件就是必须是开关状态开启的情况下才能高亮,如果关闭的话,所有色块都会变暗,另外fanState对应的值有1000,600,200的原因我们接下去会说,这个跟风扇的转速有关


让风扇转起来


让风扇转起来从代码的角度就是让扇形每次绘制的位置不同就可以了,而从我们绘制扇形的函数drawArc里面的参数来看,要更改扇形绘制的位置那就是改变startAngle的值,也就是它的初始值加上一个时刻改变的值,这个改变的范围就是0f~360f,那么对于这个循环变化的过程,我们肯定第一个想到的就是使用循环动画


image.png

这边我们看到了,fanState的值其实就是循环动画的时间,fanState的值越小说明转速越快,另外动画的初始值与目标值也加上了fanState的值,这样做的目的是为了当fanState变化时候,需要让Composable函数animateFloat触发重组,这样才能重新生成新的InfiniteRepeatableSpec对象,改变转速,不然的话,animateFloat的三个参数都不发生变化也就不发生重组,wheelState依然还是最初的值,wheelState的值拿到以后,就可以把它加给drawArc函数里面的startAngle


image.png

这里还加了一个判断,当开关是开启状态下,drawArcstartAngle才加上变化值wheelState,当关闭状态下,则不加这个值,也就是风扇处于静止状态,我们再来看看效果


0606aa5.gif

总结


我们这个风扇也做完了,用到的知识点都是平时Compose开发中常用,像是循环动画,Canvas绘制以及手势操作之类的,就这样简简单单在电脑屏幕上画出来了一个风扇,不知道看完这篇文章的你能否感受到一丝凉风~


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

天黑了,开个灯继续看书

像素点这个词对于前端来讲可能与UI设计师们打交道的时候用的会比较多一些,比如两个控件之间距离多少个像素,这个文字离容器边距多少个像素,这里的像素通常是以一种度量单位而存在,那大家有没有试过将像素点作为一个个小的控件来使用呢?让每个像素点可以变大变小,颜色可以变...
继续阅读 »

像素点这个词对于前端来讲可能与UI设计师们打交道的时候用的会比较多一些,比如两个控件之间距离多少个像素,这个文字离容器边距多少个像素,这里的像素通常是以一种度量单位而存在,那大家有没有试过将像素点作为一个个小的控件来使用呢?让每个像素点可以变大变小,颜色可以变深变浅,那么今天这个文章我们继续在Compose DeskTop上来带大家看看像素点的另一种玩法。


画出像素点


首先我们在Canvas上画出所有像素点,下面是画像素点所需要的变量


image.png

  • screenW:画布宽度,在Canvas中通过Size实时更新

  • screenY:画布高度,在Canvas中通过Size实时更新

  • gridCount:宽度或者高度上需要分出的像素数量

  • xUnit:单个像素的宽度

  • yUnit:单个像素的高度

  • pRadius:需要绘制的小圆点半径

  • xList:所有绘制出来的小圆点的x坐标

  • yList:所有绘制出来的小圆点的y坐标


然后在Canvas里面遍历xListyList这两个集合,将所有小圆点都画出来,怎么画的大家都很熟悉了,使用drawCircle函数


image.png

前方密恐福利~


image.png

我们已经获得了一堆小黑点,现在我们来尝试下更改一些小黑点的透明值,比如从我点击某一个位置开始,该位置的透明值最小,然后逐个向外透明值变大,直到透明值变成1为止,代码如下


image.png

这里新增两个变量tapXtapY用来保存点击位置的x坐标与y坐标,在循环遍历的代码里面,新增了xdisydis两个变量表示透明值,并且从点击位置开始向外递增透明值逐个变大,到第十个黑点的时候透明值就变成1了,当屏幕点击以后,xdisydis同时都小于1的点在绘制的时候都设置透明值alpha,否则就不设置透明值,下面是效果图


0613aa1.gif

圆点的透明值已经从点击位置开始向外变大了,我们再用同样的逻辑,让圆点的半径逐个向外变大


image.png

这里就做了一个小改动,将半径去乘上刚才算出来的透明值,让那些变透明的圆点同时也能有一个大小上的变化,我们再看下效果图


0613aa2.gif

可以看到我们的小圆点已经呈现出向外扩散,并且在色值与大小上都有了一定的变化,但是如果说扩散的话这里还看着有点别扭,因为一般的扩散都是以一个圆形的形状扩散的,而这里是正方形,所以我们得想办法把这个正方形也弄成圆形,怎么弄呢?那就要改变一下计算透明值与半径大小的方式了,之前是按照向外扩散经过的圆点个数来逐渐改变圆点的透明值与半径大小的,关键代码是这俩句


image.png

那么这种计算方式肯定是斜着的方向扩散的距离要大一些,所以我们不能再限制个数了,而是限制一下扩散的距离,也就是将这个扩散的圆的半径得先确定好,比如变量circleSize就是这个扩散的半径


image.png

然后我们需要做的就是计算出两点之间的距离除上这个circleSize,得到的值如果小于1那么就是我们需要的透明值,大于等于1我们就正常处理,这里唯一需要琢磨的就是如何计算两点之间的距离,四个字,勾股定理


image.png

最后一步开根号kotlin里面有现成的函数sqrt,那么计算两个小圆点之间的距离以及透明值的代码如下所示


image.png

接下去只需要将画圆点的透明值设置成div以及半径去乘上div就好了


image.png
0614aa1.gif

我们看到效果图上扩散的区域已经变成了一个圆形了,到了这里我们像素点的主要玩法就讲完了,接下去就是利用上面讲到的知识点,来做一个开灯关灯的效果


关灯后的效果


关灯后一般都是漆黑一片,但隐约还能有点能见度,所以我们这里的黑也要带点透明度,然后圆点的个数也要增多,要让单个圆点变得不明显,所以gridCount首先增加到300


image.png

然后将非扩散区域的背景色调成有点透明的黑色,并且增大圆点半径值,目的是去除圆点之间的间隙,扩散的圆点的背景色也设置成带点透明,并且半径在乘上div的基础上再减小一点,目的是加强扩散区域的灯光朦胧感


image.png
0614aa3.gif

绘制电灯,确定扩散中心位置


到了这里,扩散区域的代码暂时先告一段落,我们将电灯绘制出来,后面电灯的灯泡就作为我们扩散的中心区域,绘制电灯都是些基本Canvas绘图技巧,不多做介绍,直接贴上电灯的代码


image.png

drawCircle函数用来画灯泡,灯泡的中心点就是我们扩散的中心坐标tapX,tapY,函数drawline是画的电线,函数drawArc是画的罩在灯泡外面的灯罩,另外tapX,tapY的具体值就从点击获取变成了一个固定值


image.png

整个电灯的代码就完成了,效果如下


image.png

调节电灯亮度


当我们在生活中调节灯泡亮度的时候,灯泡的亮度会越来越亮,颜色会越来越深,那么这边如果要实现这一点的话,就需要确定一个最亮值以及最暗值,然后通过函数animateColorAsState来创建个颜色过渡的动画过程


image.png

lightState是这个开关灯的状态,作为当前所在的函数的入参从上层传进来


image.png

我们在最外层Window函数里面建立个菜单项,添加两个选项开灯与关灯,用来控制lightState的值


image.png

有了切换状态的开关,就开启了颜色过渡的动画,灯泡的色值就用lightColor来取代


image.png
0614aa4.gif

调节灯光扩散区域大小


灯光亮度能够有个由弱到强的过程了,那么灯光的扩散范围也应该有所变化,而上面我们已经知道了,控制扩散区域大小的变量就是circleSize,所以我们只要通过改变circleSize就能达到改变扩散范围的目的了,这里同样也创建个circleSize的过渡动画


image.png

然后我们更改下绘制扩散区域的条件,之前是将div小于1作为绘制扩散区域条件,现在就不需要加这个限制了,因为灯光照射的范围肯定是整个窗口范围,所以最后扩散的区域一定是到窗口以外的地方,但是绘制的条件还是有的,那就是circleSize大于最小值的时候,所以最终的代码如下


image.png

至于为什么不将判断条件设置成判断开关的开启状态是因为当开关关闭的时候,窗口一下子就变黑了,这里也希望关闭时候也有一个过渡的效果,我们看下现在的效果


0614aa5.gif

开关灯的效果就做好了,现在我们可以找一张看书的图片,然后在最外层用Image组件展示出来,那么开灯看书的效果就做好了


image.png
0614aa6.gif

源码地址


总结


最近在各种琢磨怎么做点好玩的动画效果出来,感觉在Compose里面做动效比在Android View里面简单多了,比如像这篇文章里面说到的内容,是不是改一改,一个水波纹效果就出来了,再改一改,一个数据加载完以后的转场效果也出来了,大家也可以在自己项目里面动手试试看


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

RecyclerView优化实战指南

在 Android 开发中,RecyclerView 是一个非常常用的组件,用于展示大量数据。然而,如果不进行优化,RecyclerView 可能会导致 UI 卡顿、内存泄漏等问题。本文将介绍一些优化技巧,帮助你更好地使用 RecyclerView。 简介 R...
继续阅读 »

在 Android 开发中,RecyclerView 是一个非常常用的组件,用于展示大量数据。然而,如果不进行优化,RecyclerView 可能会导致 UI 卡顿、内存泄漏等问题。本文将介绍一些优化技巧,帮助你更好地使用 RecyclerView。


简介


RecyclerView 是 Android 的一个高级 UI 组件,用于展示大量数据。它可以自动回收不可见的视图,并且可以使用不同的布局管理器来实现不同的布局。RecyclerView 还提供了一些回调函数,允许你在视图复用时进行一些自定义操作。


RecyclerView 可以大大简化开发过程,但是如果不进行优化,它可能会导致一些性能问题。下面将介绍一些优化技巧,帮助你充分发挥 RecyclerView 的性能。


优化技巧


对于 RecyclerView,我们可以采用以下优化技巧:


1. 使用 DiffUtil


DiffUtil 是计算两个列表之间差异的工具类,可帮助 RecyclerView 局部刷新数据。使用 DiffUtil 可以提升性能,减少 UI 卡顿。在 Adapter 中重写 DiffUtil.Callback,创建新列表的 DiffResult 与旧列表进行比较,从而更新列表数据。


代码演示:

class MyAdapter : RecyclerView.Adapter<MyViewHolder>() {
// ...
fun updateData(newData: List<Data>) {
val diffResult = DiffUtil.calculateDiff(object : DiffUtil.Callback() {
override fun getOldListSize() = dataSet.size
override fun getNewListSize() = newData.size
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int) =
dataSet[oldItemPosition].id == newData[newItemPosition].id
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int) =
dataSet[oldItemPosition] == newData[newItemPosition]
})
diffResult.dispatchUpdatesTo(this)
dataSet = newData
}
}

2. 使用 ViewHolder


ViewHolder 是一种模式,用于缓存 RecyclerView 中的视图,减少内存开销,提高性能。使用 ViewHolder,可以在 Adapter 中重写 onCreateViewHolder 方法创建 ViewHolder,并在 onBindViewHolder 方法中获取 ViewHolder 显示的 view,并更新数据。


代码演示:

class MyViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val titleTextView: TextView = itemView.findViewById(R.id.title)
val subTitleTextView: TextView = itemView.findViewById(R.id.subtitle)
// ...
}

class MyAdapter : RecyclerView.Adapter<MyViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
val itemView = LayoutInflater.from(parent.context)
.inflate(R.layout.item_layout, parent, false)
return MyViewHolder(itemView)
}
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
holder.titleTextView.text = dataSet[position].title
holder.subTitleTextView.text = dataSet[position].subTitle
// ...
}
}

3. 使用异步加载


如果 RecyclerView 需要加载大量数据,可以考虑使用异步加载来避免 UI 卡顿。以下是异步加载的示例:在 onBindViewHolder 中使用线程池 executor 和 ImageLoader 下载图片,并在下载完成后将其设置到 ImageView 上。


代码演示:

class MyAdapter : RecyclerView.Adapter<MyViewHolder>() {
// ...
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
val itemView = LayoutInflater.from(parent.context)
.inflate(R.layout.item_layout, parent, false)
return MyViewHolder(itemView)
}
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
if (dataSet[position].imageURL != null) {
holder.imageView.setImageResource(R.drawable.placeholder)
holder.imageView.tag = dataSet[position].imageURL
executor.execute {
val bitmap = ImageLoader.fetchBitmapFromURL(dataSet[position].imageURL!!)
if (holder.imageView.tag == dataSet[position].imageURL) {
holder.imageView.post { holder.imageView.setImageBitmap(bitmap) }
}
}
} else {
holder.imageView.setImageBitmap(null)
}
// ...
}
}

object ImageLoader {
// ...
fun fetchBitmapFromURL(url: String): Bitmap? {
// ...
return bitmap
}
}

4. 合理使用布局管理器


RecyclerView 提供多种布局管理器,每种管理器都适用于不同的场景。我们应该根据具体需求选择适合的管理器。以下是布局管理器的示例:


代码演示:

val layoutManager = when (layoutType) {
LayoutType.LINEAR -> LinearLayoutManager(context)
LayoutType.GRID -> GridLayoutManager(context, spanCount)
LayoutType.STAGGERED_GRID -> StaggeredGridLayoutManager(spanCount, orientation)
}
recyclerView.layoutManager = layoutManager

5. 使用数据绑定


数据绑定是一种将数据直接绑定到视图上的技术,减少代码量,提高代码可读性。我们可以在 adapter_layout.xml 中使用 <layout> 标签,将数据绑定到视图的布局文件中,从而减少代码量。


代码演示:

<layout>
<data>
<variable name="data" type="com.example.Data" />
</data>
<LinearLayout ...>
<TextView android:text="@{data.title}" ... />
<TextView android:text="@{data.subtitle}" ... />
</LinearLayout>
</layout>

在 Adapter 中使用 DataBindingUtil.inflate 方法,将 layout 绑定到 Data 中并设置到 ViewHolder 上。


代码演示:

class MyAdapter : RecyclerView.Adapter<MyViewHolder>() {
// ...
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
val binding = ItemLayoutBinding.inflate(
LayoutInflater.from(parent.context), parent, false)
return MyViewHolder(binding.root)
}
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
holder.binding.data = dataSet[position]
// ...
}
// ...
}

6. 减少布局中嵌套层级


布局中的嵌套层级越多,性能就越低,所以需要尽可能减少嵌套层级。可以使用 ConstraintLayout 或者扁平布局来减少嵌套层级。


7. 设置 Recyclerview 的固定大小


在 Recyclerview 的布局中,设置 android:layout_heightandroid:layout_width 的值为具体数值,可以避免列表项的宽高随着内容的变化而变化,从而使布局横向和纵向的测量也相应变快。


8. 禁止自动滑动


当数据项发生变化,RecyclerView 默认会自动滚动到新位置。如果这种行为不是必需的,可以在 Adapter 中重写 onItemRangeChanged 方法,并在其中禁止滑动。


代码演示:

override fun onItemRangeChanged(positionStart: Int, itemCount: Int) {
if (itemCount == 1) {
notifyItemChanged(positionStart)
} else {
notifyDataSetChanged()
}
recyclerView.stopScroll()
}

9. 使用预加载


使用预加载技术可以使 RecyclerView 在滑动过程中提前加载更多数据,保证滑动的流畅性和用户体验。


这些技巧可以根据具体的应用情况来使用,针对不同的问题提供不同的解决方案,从而提升 RecyclerView 的性能。如果需要更高级的功能,可以考虑使用 RecyclerView 提供的其它高级接口。


结论


通过本文,我们介绍了一些优化 RecyclerView 的技巧,包括使用 DiffUtil、使用 ViewHolder、使用异步加载、合理使用布局管理器、使用数据绑定、减少布局中嵌套层级、设置 RecyclerView 的固定大小、禁止自动滑动、使用预加载等。我们可以根据实际需求选择合适的优化方案,提升 RecyclerView 的性能,使其更加流畅。


推荐


android_startup: 提供一种在应用启动时能够更加简单、高效的方式来初始化组件,优化启动速度。不仅支持Jetpack App Startup的全部功能,还提供额外的同步与异步等待、线程控制与多进程支持等功能。


AwesomeGithub: 基于Github的客户端,纯练习项目,支持组件化开发,支持账户密码与认证登陆。使用Kotlin语言进行开发,项目架构是基于JetPack&DataBinding的MVVM;项目中使用了Arouter、Retrofit、Coroutine、Glide、Dagger与Hilt等流行开源技术。


flutter_github: 基于Flutter的跨平台版本Github客户端,与AwesomeGithub相对应。


android-api-analysis: 结合详细的Demo来全面解析Android相关的知识点, 帮助读者能够更快的掌握与理解所阐述的要点。


daily_algorithm: 每日一算法,由浅入深,欢迎加入一起共勉。


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

安卓-入门kotlin协程

作者 大家好,我叫小琪; 本人16年毕业于中南林业科技大学软件工程专业,毕业后在教育行业做安卓开发,后来于19年10月加入37手游安卓团队; 目前主要负责国内发行安卓相关开发,同时兼顾内部几款App开发。 一些概念 在了解协程之前,我们先回顾一下线程、进程的概...
继续阅读 »

作者


大家好,我叫小琪;


本人16年毕业于中南林业科技大学软件工程专业,毕业后在教育行业做安卓开发,后来于19年10月加入37手游安卓团队;


目前主要负责国内发行安卓相关开发,同时兼顾内部几款App开发。


一些概念


在了解协程之前,我们先回顾一下线程、进程的概念


img

1.进程:拥有代码和打开的文件资源、数据资源、独立的内存空间,是资源分配的最小单位。


2.线程:从属于进程,是程序的实际执行者,一个进程至少包含一个线程,操作系统调度(CPU调度)执行的最小单位


3.协程:



  • 不是被操作系统内核所管理的,而是完全由程序所控制,也就是在用户态执行

  • 进程、线程是操作系统维度的,协程是语言维度的。


协程特点



  • 异步代码同步化


下面通过一个例子来体验kotlin中协程的这一特点


有这样一个场景,请求一个网络接口,用于获取用户信息而后更新UI,将用户信息展示,用kotlin的协程这样写:

GlobalScope.launch(Dispatchers.Main) {   // 在主线程开启协程
val user = api.getUser() // IO 线程执行网络请求
tvName.text = user.name // 主线程更新 UI
}

而通过 Java 实现以上逻辑,我们通常需要这样写:

api.getUser(new Callback<User>() {
@Override
public void success(User user) {
runOnUiThread(new Runnable() {
@Override
public void run() {
tvName.setText(user.name);
}
})
}

@Override
public void failure(Exception e) {
...
}
});

java中的这种异步回调打乱了正常代码顺序,虽说保证了逻辑上是顺序执行的,但使得阅读相当难受,如果并发的场景再多一些,将会出现“回调地狱”,而使用了 Kotlin 协程,多层网络请求只需要这么写:

GlobalScope.launch(Dispatchers.Main) {       // 开始协程:主线程
val token = api.getToken() // 网络请求:IO 线程
val user = api.getUser(token) // 网络请求:IO 线程
tvName.text = user.name // 更新 UI:主线程
}

可以看到,即便是比较复杂的并行网络请求,也能够通过协程写出结构清晰的代码


协程初体验


1.引入依赖

    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.0'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.0'

2.第一个协程程序


布局中添加一个button,并为它设置点击事件

btn.setOnClickListener {
Log.i("TAG","1.准备启动协程.... [当前线程为:${Thread.currentThread().name}]")
CoroutineScope(Dispatchers.Main).launch{
delay(1000) //延迟1000ms
Log.i("TAG","2.执行CoroutineScope.... [当前线程为:${Thread.currentThread().name}]")
}
Log.i("TAG","3.BtnClick.... [当前线程为:${Thread.currentThread().name}]")
}

执行结果如下:

1.准备启动协程....[当前线程为:main]
3.BtnClick.... [当前线程为:main]
2.执行CoroutineScope.... [当前线程为:main]

通过CoroutineScope.launch方法开启了一个协程,launch后面花括号内的代码就是运行在协程内的代码。协程启动后,协程体里的任务就会先挂起(suspend),让CoroutineScope.launch后面的代码继续执行,直到协程体内的方法执行完成再自动切回来


进入到launch方法看看它里面的参数,

public fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job {
}

对这些参数的说明:



  • context:协程上下文,可以指定协程限制在一个特定的线程执行。常用的有Dispatchers.Default、Dispatchers.Main、Dispatchers.IO等。Dispatchers.Main即Android 中的主线程;Dispatchers.IO:针对磁盘和网络 IO 进行了优化,适合 IO 密集型的任务,比如:读写文件,操作数据库以及网络请求

  • start: 协程的启动模式。默认的(也是最常用的)CoroutineStart.DEFAULT指协程立即执行,另外还有CoroutineStart.LAZY、CoroutineStart.ATOMIC、CoroutineStart.UNDISPATCHED

  • block:协程主体,即要在协程内部运行的代码,也就是上述例子花括号中的代码

  • 返回值Job:对当前创建的协程的引用。可以通过调用它的的join、cancel等方法来控制协程的启动和取消。


3.挂起函数


上面有提到”挂起“即suspend的概念,


回到上面的例子,有一个delay函数,进到这个函数看看它的定义:

public suspend fun delay(timeMillis: Long) {...}

发现多了个suspend关键字,也就是上文中提到的“挂起”,根据程序的输出结果看,首先输出了1,3,等待一秒后再输出了2,而且打印的线程显示的也是主线程,这说明,协程在遇到suspend关键字的时候,会被挂起,所谓的挂起,就是程序切了个线程,并且当这个挂起函数执行完毕后又会自动切回来,这个切回来的动作其实就是恢复,因此挂起、恢复也是协程的一个特点。所以说,协程的挂起可以理解为协程中的代码离开协程所在线程的过程,协程的恢复可以理解为协程中的代码重新进入协程所在线程的过程。协程就是通过这个挂起恢复机制进行线程的切换。


关于suspend函数也有个规定:挂起函数必须在协程或者其他挂起函数中被调用,换句话说就是挂起函数必须直接或者间接地在协程中执行。


4.创建协程的其他方式


上面介绍了通过launch方法创建协程,当遇到 suspend 函数的时候 ,该协程会自动逃离当前所在的线程执行任务,此时原来协程所在的线程就继续干自己的事,等到协程的suspend 函数执行完成后又自动切回来原来线程继续往下走。 但如果协程所在的线程已经运行结束了,协程还没执行完成就不会继续执行了 。为了避免这样的情况就需要结合 runBlocking 来暂时阻塞当前线程,保证代码的执行顺序。


下面我们通过runBlocking 来创建协程

btn.setOnClickListener {
Log.i("TAG", "1.准备启动协程.... [当前线程为:${Thread.currentThread().name}]")
runBlocking {
delay(1000) //延迟1000ms
Log.i("TAG", "2.执行CoroutineScope.... [当前线程为:${Thread.currentThread().name}]")
}
Log.i("TAG", "3.BtnClick.... [当前线程为:${Thread.currentThread().name}]")
}

执行结果如下:

1.准备启动协程.... [当前线程为:main]
2.执行CoroutineScope.... [当前线程为:main]
3.BtnClick.... [当前线程为:main]

可以看到运行结果顺序和上面的launch方式不同,这里的log先输出1、2,再输出3,程序会等待runBlocking中的代码块执行完后才会还执行后面的代码,因此launch是非阻塞的,而runBlocking是阻塞式的。


launch和runBlocking都是没有返回结果的,有时我们想知道协程的返回结果,拿到结果去做业务例如UI更新,这时withContext和async就派上用场了。


先看下withContext的使用场景:

 btn.setOnClickListener {
CoroutineScope(Dispatchers.Main).launch {
val startTime = System.currentTimeMillis()
val task1 = withContext(Dispatchers.IO) {
delay(2000)
Log.i("TAG", "1.执行task1.... [当前线程为:${Thread.currentThread().name}]")
1 //返回结果赋值给task1
}

val task2 = withContext(Dispatchers.IO) {
delay(1000)
Log.i("TAG", "2.执行task2.... [当前线程为:${Thread.currentThread().name}]")
2 //返回结果赋值给task2
}
Log.i(
"TAG",
"3.计算task1+task2 = ${task1+task2} , 耗时 ${System.currentTimeMillis() - startTime} ms [当前线程为:${Thread.currentThread().name}]"
)
}
}

输出结果为:

 1.执行task1.... [当前线程为:DefaultDispatcher-worker-3]
2.执行task2.... [当前线程为:DefaultDispatcher-worker-1]
3.计算 task1+task2 = 3 , 耗时 3032 ms [当前线程为:main]

从输出结果可以看出,通过withContext指定协程运行在一个io线程,延迟了两秒后返回结果1赋值给task1,之后程序向下执行,同样的,延迟了1s后返回结果2赋值给了task2,最后执行到步骤三,并且打印了耗时时间,可以看到,耗时是两个task的时间总和,也就是先执行完task1,在执行task到,说明withContext是串行执行的,这适用于在一个请求结果依赖另一个请求结果的场景。


如果同时处理多个耗时任务,且这几个任务都没有相互依赖时,可以使用 async ... await() 来处理,将上面的例子改为 async 来实现如下

btn.setOnClickListener {
CoroutineScope(Dispatchers.Main).launch {
val startTime = System.currentTimeMillis()
val task1 = async(Dispatchers.IO) {
delay(2000)
Log.i("TAG", "1.执行task1.... [当前线程为:${Thread.currentThread().name}]")
1 //返回结果赋值给task1
}

val task2 = async(Dispatchers.IO) {
delay(1000)
Log.i("TAG", "2.执行task2.... [当前线程为:${Thread.currentThread().name}]")
2 //返回结果赋值给task2
}

Log.i(
"TAG",
"3.计算 task1+task2 = ${task1.await()+task2.await()} , 耗时 ${System.currentTimeMillis() - startTime} ms [当前线程为:${Thread.currentThread().name}]"
)
}
}

输出结果:

2.执行task2.... [当前线程为:DefaultDispatcher-worker-4]
1.执行task1.... [当前线程为:DefaultDispatcher-worker-5]
3.计算 task1+task2 = 3 , 耗时 2010 ms [当前线程为:main]

可以看到,输出的总耗时明显比withContext更短,且task2优先task1执行完,说明async 是并行执行的。


总结


本文首先通过对进程、线程、协程的区别认清协程的概念,接着对协程的特点也就是优势进行了介绍,最后通过几个实例介绍了协程的几种启动方式,并分析了其各自特点和使用场景,本文更多是对协程的概念和使用进行了简单的介绍,而协程的内容远不止这些。


结束语


过程中有问题或者需要交流的同学,可以扫描二维码加好友,然后进群进行问题和技术的交流等;


作者:37手游移动客户端团队
链接:https://juejin.cn/post/7245096955966177338
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Redis性能瓶颈揭秘:如何优化大key问题?

1. 什么是Redis大key问题 Redis大key问题指的是某个key对应的value值所占的内存空间比较大,导致Redis的性能下降、内存不足、数据不均衡以及主从同步延迟等问题。 到底多大的数据量才算是大key? 没有固定的判别标准,通常认为字符串类型的...
继续阅读 »

1. 什么是Redis大key问题


Redis大key问题指的是某个key对应的value值所占的内存空间比较大,导致Redis的性能下降、内存不足、数据不均衡以及主从同步延迟等问题。


到底多大的数据量才算是大key?


没有固定的判别标准,通常认为字符串类型的key对应的value值占用空间大于1M,或者集合类型的k元素数量超过1万个,就算是大key。


Redis大key问题的定义及评判准则并非一成不变,而应根据Redis的实际运用以及业务需求来综合评估。例如,在高并发且低延迟的场景中,仅10kb可能就已构成大key;然而在低并发、高容量的环境下,大key的界限可能在100kb。因此,在设计与运用Redis时,要依据业务需求与性能指标来确立合理的大key阈值。


2. 大key带来的影响



  1. 内存占用过高。大Key占用过多的内存空间,可能导致可用内存不足,从而触发内存淘汰策略。在极端情况下,可能导致内存耗尽,Redis实例崩溃,影响系统的稳定性。

  2. 性能下降。大Key会占用大量内存空间,导致内存碎片增加,进而影响Redis的性能。对于大Key的操作,如读取、写入、删除等,都会消耗更多的CPU时间和内存资源,进一步降低系统性能。

  3. 阻塞其他操作。某些对大Key的操作可能会导致Redis实例阻塞。例如,使用DEL命令删除一个大Key时,可能会导致Redis实例在一段时间内无法响应其他客户端请求,从而影响系统的响应时间和吞吐量。

  4. 网络拥塞。每次获取大key产生的网络流量较大,可能造成机器或局域网的带宽被打满,同时波及其他服务。例如:一个大key占用空间是1MB,每秒访问1000次,就有1000MB的流量。

  5. 主从同步延迟。当Redis实例配置了主从同步时,大Key可能导致主从同步延迟。由于大Key占用较多内存,同步过程中需要传输大量数据,这会导致主从之间的网络传输延迟增加,进而影响数据一致性。

  6. 数据倾斜。在Redis集群模式中,某个数据分片的内存使用率远超其他数据分片,无法使数据分片的内存资源达到均衡。另外也可能造成Redis内存达到maxmemory参数定义的上限导致重要的key被逐出,甚至引发内存溢出。


3. 大key产生的原因



  1. 业务设计不合理。这是最常见的原因,不应该把大量数据存储在一个key中,而应该分散到多个key。例如:把全国数据按照省行政区拆分成34个key,或者按照城市拆分成300个key,可以进一步降低产生大key的概率。

  2. 没有预见value的动态增长问题。如果一直添加value数据,没有删除机制、过期机制或者限制数量,迟早出现大key。例如:微博明星的粉丝列表、热门评论等。

  3. 过期时间设置不当。如果没有给某个key设置过期时间,或者过期时间设置较长。随着时间推移,value数量快速累积,最终形成大key。

  4. 程序bug。某些异常情况导致某些key的生命周期超出预期,或者value数量异常增长 ,也会产生大key。


4. 怎样排查大key


4.1 SCAN命令


通过使用Redis的SCAN命令,我们可以逐步遍历数据库中的所有Key。结合其他命令(如STRLEN、LLEN、SCARD、HLEN等),我们可以识别出大Key。SCAN命令的优势在于它可以在不阻塞Redis实例的情况下进行遍历。


4.2 bigkeys参数


使用redis-cli命令客户端,连接Redis服务的时候,加上 —bigkeys 参数,可以扫描每种数据类型数量最大的key。



redis-cli -h 127.0.0.1 -p 6379 —bigkeys



4.3 Redis RDB Tools工具


使用开源工具Redis RDB Tools,分析RDB文件,扫描出Redis大key。


例如:输出占用内存大于1kb,排名前3的keys。



rdb —commond memory —bytes 1024 —largest 3 dump.rbd



5. 怎么解决大key



  1. 拆分成多个小key。这是最容易想到的办法,降低单key的大小,读取可以用mget批量读取。

  2. 数据压缩。使用String类型的时候,使用压缩算法减少value大小。或者是使用Hash类型存储,因为Hash类型底层使用了压缩列表数据结构。

  3. 设置合理的过期时间。为每个key设置过期时间,并设置合理的过期时间,以便在数据失效后自动清理,避免长时间累积的大Key问题。

  4. 启用内存淘汰策略。启用Redis的内存淘汰策略,例如LRU(Least Recently Used,最近最少使用),以便在内存不足时自动淘汰最近最少使用的数据,防止大Key长时间占用内存。

  5. 数据分片。例如使用Redis Cluster将数据分散到多个Redis实例,以减轻单个实例的负担,降低大Key问题的风险。

  6. 删除大key。使用UNLINK命令删除大key,UNLINK命令是DEL命令的异步版本,它可以在后台删除Key,避免阻塞Redis实例。


6. 总结


大Key问题是Redis中常见的问题之一,可能导致性能下降、内存占用过高、阻塞其他操作以及主从同步延迟等问题。本文详细介绍了大Key产生的原因、影响、检测方法和解决方案。通过优化数据结构设计、设定合理的数据过期策略、优化系统架构和配置,以及渐进式删除大Key等方法,我们可以有效地解决和预防大Key问题,从而提高Redis系统的稳定性和性能。



我是「一灯架构」,如果本文对你有帮助,欢迎各位小伙伴点赞、评论和关注,感谢各位老铁,我们下期见


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

微信扫码登录,做了又好像没做

这两天,和微信扫码登录这个功能杠上了。 为什么??? 大家都知道,个人网站我做了很多,而我的每个网站都有登录/注册相关的功能。无一例外我的网站都是通过账号和密码这种形式进行登录/注册,而前两天有人和我说这样太麻烦了,搞个扫码登录多方便啊! 你还别说,我以前是真...
继续阅读 »

这两天,和微信扫码登录这个功能杠上了。


为什么???


大家都知道,个人网站我做了很多,而我的每个网站都有登录/注册相关的功能。无一例外我的网站都是通过账号和密码这种形式进行登录/注册,而前两天有人和我说这样太麻烦了,搞个扫码登录多方便啊!


你还别说,我以前是真考虑过,但那会不是赶工期嘛,就没去过多的了解扫码登录这回事。而这次再次被提及,那就顺势这两天给他办了吧!


G8JkY.gif


考虑到现在的用户都喜欢使用微信,而微信扫码登录这个功能也是非常普遍的,所以一拍即合,我也整个微信扫码登录。


经过周末两天的摸索,现在个人开发者要给自己的网站加上微信扫码登录这个功能有三个途径:



  1. 利用微信扫码,关注公众号实现微信扫码登录

  2. 利用微信开放平台提供的网站应用提供的获取用户信息接口,实现扫码登录

  3. 利用小程序做中间过渡实现的微信扫码登录


然而,这里我要给你们泼一盆凉水了,这三个方案,我都试过,对于个人而言,无法实现,或者说无法满足我对扫码登录功能的期望



期望:微信扫码,用户授权,获取用户头像及昵称等信息



先来说说公众号,为啥不行。


首先,我有公众号,但是我的公众号仅仅是个人,而不是企业认证的公众号,这就表明我只能发信息,而没有获取和支付等这种高级的功能。


就比如获取用户信息:


Snipaste_2023-06-11_19-04-17.jpg


可能有人会说去认证一个不就行了,我只能说,你们可以去试试(认证费几千块 + 其他材料)。


再来说说微信开放平台,为啥不行。


其实这个平台的对接功能写的非常通俗易懂,而且也相对简单,但是,和上面的问题也是一样,个人想要获取用户相关的功能,也要认证。


那我再看一下他的认证条件吧:


Snipaste_2023-06-11_19-04-117.jpg


这个认证条件倒是简单明了,就 300 块钱,你认证了就给你获取用户的功能权限。


虽然 300 也不是什么大数目,但是我一想我做的那几个项目用户都还没几个,就把 300 花出去,不太划算,所以这个方向也 pass。


那就来看看最后的小程序方案吧!


查看了小程序的接口文档,发现还真有获取用户信息的 wx 接口,文档如下:


Snipaste_2023-06-11_19-47-46.jpg


有了这个接口就好办了,而且个人是可以开发小程序且不需要任何认证流程的,有微信号就 ok。这不就是我想要的方案嘛,有手就行了,扫码登录。


此时我还没意识到,这个有手就行和我想象的不太一样。


敲定好方案之后,我就在脑子里构思如何通借助小程序来实现微信扫码登录,大致思考了一会,我设计了如下登录方案:


Snipaste_2023-06-11_20-06-39.jpg


图中的主要步骤就两个:



  1. 获取小程序二维码,给用户扫码

  2. 微信扫码,获取用户信息,进行后续登录流程


之后通过编码,居然发现最重要的两个信息(头像、昵称)拿不到,我也是醉了。而我去翻看微信的官方文档,原来是腾讯把 wx.getUserProfile 接口给下线了,这给我整无语了。


Snipaste_2023-06-11_21-20-30.jpg


咋办?


小程序代码我都写好上线了,后端的相关接口我也写好了,就差网站页面联调了。你说这功能是联调,还是不联调呢!


算了,先这样吧!功能先保留,等以后要做新的项目了,再把这个功能放上去。现在整个的流程问题比较大的就是获取不到用户的头像和昵称,但是,扫码登录这个基本流程是已经走通的。等新项目对接这个功能的时候,再让用户通过网站提供的功能再来完善用户头像和昵称等信息吧!


Snipaste_2023-06-11_21-29-44.jpg


吐槽完了!!!


最后,感谢各位看官看到这里,如果大家有啥好的微信扫码登录方案,欢迎评论区留言。


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

Android-策略模式与状态模式:形似而行不似

引言: 在软件开发中,我们经常面临着需求的变化和复杂的业务逻辑。为了解决这些问题,设计模式应运而生。其中,策略模式和状态模式是两种常用的设计模式。本文将带你走进它们的世界,一起探索它们的魅力和应用场景。 一、策略模式 在生活中,我们经常会遇到需要根据不同情况采...
继续阅读 »

引言: 在软件开发中,我们经常面临着需求的变化和复杂的业务逻辑。为了解决这些问题,设计模式应运而生。其中,策略模式和状态模式是两种常用的设计模式。本文将带你走进它们的世界,一起探索它们的魅力和应用场景。


一、策略模式


在生活中,我们经常会遇到需要根据不同情况采取不同策略的情况。软件开发也存在类似的情况。策略模式就是为了解决这类问题而生的。它允许在运行时根据需要选择算法的行为。


应用场景: 想象一下,你是一位游戏开发者,你的游戏中有多种角色,每个角色都有不同的攻击方式。使用策略模式,你可以为每个角色创建一个独立的攻击策略,然后在运行时根据角色的选择来决定使用哪种策略。这样,你就能灵活地扩展和切换角色的攻击方式,而无需修改大量的代码。


代码示例: 让我们以一个游戏的例子来说明策略模式的使用。假设你正在开发一个角色扮演游戏,其中有两个角色:战士(Warrior)和法师(Mage)。每个角色都有自己的攻击方式。首先,我们需要定义一个策略接口,表示不同的攻击策略:

interface AttackStrategy {
fun attack()
}

接下来,我们创建两个具体的策略类,分别代表战士和法师的攻击方式:

class WarriorAttackStrategy : AttackStrategy {
override fun attack() {
println("战士使用剑攻击")
}
}

class MageAttackStrategy : AttackStrategy {
override fun attack() {
println("法师使用魔法攻击")
}
}

最后,我们创建一个角色类,其中包含一个攻击策略的引用:

class Character(private val attackStrategy: AttackStrategy) {
fun attack() {
attackStrategy.attack()
}
}

现在,我们可以创建战士和法师的实例,并分别调用它们的攻击方法:

val warrior = Character(WarriorAttackStrategy())
warrior.attack() // 输出:战士使用剑攻击

val mage = Character(MageAttackStrategy())
mage.attack() // 输出:法师使用魔法攻击

通过使用策略模式,我们可以轻松地为角色添加新的攻击方式,而无需修改角色类的代码。


策略模式的优势:



  • 灵活性:策略模式允许在运行时动态切换算法,使系统更具灵活性。

  • 可扩展性:通过添加新的策略类,我们可以轻松地扩展系统的功能。


缺点:



  • 增加了类的数量:引入策略模式会增加类的数量,特别是当策略类较多时。过度使用策略模式可能会导致类爆炸。


二、状态模式


在某些情况下,对象的行为取决于其内部状态的变化。状态模式可以帮助我们更好地管理对象的状态,并基于不同的状态执行相应的行为。


应用场景: 以电梯为例,电梯在不同的状态下有不同的行为。比如,当电梯在运行状态下,按下楼层按钮不会有任何反应;而当电梯在停止状态下,按下楼层按钮会触发电梯移动到相应楼层的行为。使用状态模式,我们可以更好地管理电梯的状态和行为,提高系统的可维护性和扩展性。


代码示例: 让我们以电梯的例子来说明状态模式的使用。首先,我们定义一个电梯状态接口:

interface ElevatorState {
fun pressFloorButton(floor: Int)
}

接下来,我们创建两个具体的状态类,分别代表电梯的运行状态和停止状态:

class RunningState : ElevatorState {
override fun pressFloorButton(floor: Int) {
// 运行状态下不响应按钮按下事件
}
}

class StoppedState : ElevatorState {
override fun pressFloorButton(floor: Int) {
println("电梯移动到楼层 $floor")
}
}

然后,我们创建一个电梯类,其中包含一个状态的引用:

class Elevator(private var state: ElevatorState) {
fun setState(state: ElevatorState) {
this.state = state
}

fun pressFloorButton(floor: Int) {
state.pressFloorButton(floor)
}
}

现在,我们可以创建一个电梯的实例,并模拟按下楼层按钮的事件:

val elevator = Elevator(StoppedState())
elevator.pressFloorButton(5) // 输出:电梯移动到楼层 5

elevator.setState(RunningState())
elevator.pressFloorButton(7) // 没有任何输出

通过使用状态模式,我们可以根据电梯的状态来决定是否响应按钮按下事件,并执行相应的行为。


状态模式的优势:



  • 易于扩展:通过添加新的状态类,我们可以轻松地扩展系统的行为。

  • 可维护性:状态模式使状态转换和行为与特定状态相关,使代码更具可读性和可维护性。


缺点:



  • 增加了类的数量:引入状态模式会增加类的数量,特别是当状态较多时。过度使用状态模式可能会导致类爆炸。


策略模式与状态模式的区别:



  • 目的不同:策略模式关注的是算法的选择和使用,用于根据不同的策略执行不同的行为;而状态模式关注的是对象内部状态的变化,用于根据不同的状态执行不同的行为。

  • 职责不同:策略模式将不同的算法封装成独立的策略类,客户端主动选择使用哪种策略;而状态模式将不同的状态封装成独立的状态类,并由环境类根据当前状态执行相应的行为。

  • 关注点不同:策略模式关注的是行为的选择和灵活性,用于动态地切换算法;而状态模式关注的是状态的变化和可维护性,用于根据状态执行相应的行为。


结语: 策略模式和状态模式是两种常用的设计模式,它们分别解决了根据不同策略和状态执行不同行为的问题。通过灵活地选择和管理策略或状态,我们可以提高系统的灵活性、可扩展性和可维护性。在实际开发中,我们可以根据具体的需求和场景选择适合的设计模式,以优化代码结构和提高系统的可维护性。希望本文能帮助你更好地理解和应用策略模式和状态模式,让你在设计中游刃有余,解决你的设计困扰!


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

天马行空使用适配器模式

1. 前言 因为最近没有什么比较好的技术想要分享,所以来水一下文章,啊不对,是分享一下一些思路,和大家交流一下想法,没准能产生一些新的想法。这篇文章不具备权威,不一定正确,简单来说全都是我瞎吹。 开发这么久,多多少少也有了解过适配器模式。我还是一如既往的建议学...
继续阅读 »

1. 前言


因为最近没有什么比较好的技术想要分享,所以来水一下文章,啊不对,是分享一下一些思路,和大家交流一下想法,没准能产生一些新的想法。这篇文章不具备权威,不一定正确,简单来说全都是我瞎吹。


开发这么久,多多少少也有了解过适配器模式。我还是一如既往的建议学习设计模式,不能光靠看别人的文章,得靠自己的积累和理解源码的做法来得更切实际。


2. 浅谈适配器


我们都知道,适配器模式是一种结构型模式,结构型模式我的理解都有一个特点,简单来说就是封装了一些操作,就是隐藏细节嘛,比如说代理就是隐藏了通过代理调用实体的细节。


我还是觉得我们要重头去思考它是怎样的一个思路。如果你去查“适配器模式”,相信你看到很多关于插头的说法,而且你会觉得这东西很简单,但好像看完之后又感觉学了个寂寞。


还是得从源码中去理解。先看看源码中最经典的适配器模式的使用地方,没错,RecyclerView的Adapter。但是它又不仅仅只使用Adapter模式,如果拿它的源码来做分析反而会有些绕。但是我们可以进行抽象。想想Adapter其实主要的流程就是输入数据然后输出View给RecyclerView


然后又可以大胆的去思考一下,如果不定义一个Adapter,没有ViewHolder,要怎么实现这个效果?写一个循环,然后在循环里面做一些逻辑判断,然后创建对应的子View,添加到父View中 ,大概会这样做吧。那其实Adapter就帮我们做了这一步,并且还做了很多比较经典的优化操作,其实大概就是这样。


然后从这样的模型中,我大概是能看出了一些东西,比如使用Adapter是为了输入一些东西,也可以说是为了让它封装一些逻辑,然后达到某些目的,如果用抽象的眼光去看,我不care你封装的什么东西,我只要你达到我的目的(输出我的东西),RecyclerView的adapter就是输出View。这是其一,另外,在这个模型中我有一个使用者去使用这个Adaper,这个Apdater在这里不是一个概念,而是一个对象,我这个使用者通过Adaper这个对象给它一些输入,以此来实现我的某些目标


ok,结合Adapter模式的结构图看看(随便从网上找一张图)


image.png


可以看到这个模型中有个Client,有个Tagrget,有个Adapter。拿RecyclerView的Adapter来说,Client是RecyclerView (当然具体的addView操作是在LayoutManager中,这里是抽象去看),Adapter就是RecyclerView.Adapter,而Tagrget抽象去看就是对view的操作。


3. 天马行空的使用


首先一般使用官方的RecyclerView啊这些,都会提供自己的Adapter,但是会让人会容易觉得Adapter就是在复用View的情况下使用。而我的理解是,RecyclerView的复用是ViewHolder的思想,不是Adapter的思想,比如早期的ListView也有Adapter啊,当时出RecyclerView之前也没有说用到ViewHolder的这种做法(这是很多年前的事),我说这个是想要表达不要把Adapter和RecyclerView绑一起,这样会让思维受到局限。


对我而言,Adapter在RecyclerView中的作用是 “Data To View” ,适配数据而产出对应的View。


那我可以把Apdater的作用理解成“Object To Object” ,对于我来说,Object它可以是Data,可以是View,甚至可以是业务逻辑。


所以当我跳出RecyclerView这种传统的 “Data To View” 的思维模式之后,Adapter适配器模式可以做到的场景就很多,正如上面我理解的,Adapter简单来说就是 “Data To View” ,那我可以用适配器模式去做 “Data To Data” ,可以去做 “View To Data” ,可以去做 “Business To Business” , “Business To View” 等等,实现多种效果。


假设我这里做个Data To Data的场景 (强行举的例子可能不是很好)


我请求后台拿到个人的基本信息数据

data class PeopleInfo(
var name: String?,
var sex: Int?,
......
)

然后通过个人的ID,再请求服务端另外一个接口拿到成绩数据

data class ScoreInfo(
var language : Int,
var math : Int,
......
)

然后我有个数据竞赛的报名表对象。

data class MathCompetition(
var math : Int,
var name : String
)

然后一般我们使用到的时候就会这样赋值,假设这段代码在一个Competition类中进行

val people = getPeopleInfo()
val score = getScoreInfo()
val mathTable = MathCompetition(
score.math?,
people.name?,
......
)

就是深拷贝的一种赋值,我相信很多人的代码里面肯定会有

两个对象 A 和 B
A.1 = B.1
A.2 = B.2
A.3 = B.3
......

这样的代码,然后对象的熟悉多的话这个代码就会写得很长。当然这不会造成什么大问题,但是你看着看着,总觉得缺少一些美感,但是好像这玩意没封装不起来这种感觉。


如果用适配器来做的话,首先明确要做的流程是从Competition这个类中,将所有数据整合成MathCompetition对象,目标就是输出MathCompetition对象。那从适配器模式的模型上去看,client就是Competition,是它的需求,Taget就是getMathCompetition,输出mathTable,然后我们可以写一个Adapter。

class McAdapter {

var mathCompetition : MathCompetition? = null

init {
// 给默认值
mathCompetition = MathCompetition(0, "name", ......)
}

fun setData(people : PeopleInfo? = null, score : ScoreInfo? = null){
people?.let {
mathCompetition?.name = it.name
......
}

score?.let {
mathCompetition?.math = it.math
......
}
}

fun getData() : MathCompetition?{
return mathCompetition
}

}

然后在Competition中就不需要直接引用MathCompetition,而是设置个setAdapter方法,然后需要拿数据时再调用adapter的getData()方法,这样就恰到好处,不会把这些深拷贝方式的赋值代码搞得到处都是。 这个Demo看着好像没什么,但是真碰到了N合1这样的数据场景的时候,使用Adapter显然会更安全。


我再简单举一个Business To View的例子吧。假设你的app中有很几套EmptyView,比如你有嵌入到页面的EmptyView,也有做弹窗类型的EmptyView,我们一般的做法就是对应的页面的xml文件中直接写EmptyView,那这些EmptyView的代码就会很分散是吧。OK,你也想整合起来,所以你会写个EmptyHelper,大概是这个意思,用单例写一个管理EmptyView的类,然后里面统一封装对EmptyView的操作,一般都会这样写。 其实如果你让我来做,我可能就会用适配器模式去实现。 ,当然也有其他办法能很好的管理,这具体的得看心情。


写一个Adapter,我这里写一段伪代码,应该比较容易能看懂

class EmptyAdapter() {
// 这样语法要伴生,懒得写了,看得懂就行
const val STATUS_NORMAL = 0
const val STATUS_LOADING = 1
const val STATUS_ERROR = 2

private var type: Int = 0
var parentView: ViewGroup? = null
private var mEmptyView: BaseEmptyView? = null
private var emptyStatus = 0 // 有个状态,0是不显示,1是显示加载中,2是加载失败......

init {
createEmptyView()
}

private fun createEmptyView() {
// 也可以判断是否有parentView决定使用哪个EmptyView等逻辑
mEmptyView = when (type) {
0 -> AEmptyView
1 -> BEmptyView
2 -> CEmptyView
else -> {
AEmptyView
}
}
}

fun setData(status: Int) {
when (status) {
0 -> parentView?.removeView(mEmptyView)
1 -> mEmptyView?.showLoading()
2 -> mEmptyView?.showError()
}
}

fun updateType(type: Int) {
setData(0)
this.type = type
createEmptyView()
}
}

然后在具体的Activity调用的时候,可以

val emptyAdapter = EmptyAdapter(getContentView())
// 然后在每次要loading的时候去设置adapter的的状态
emptyAdapter.setData(EmptyAdapter.STATUS_LOADING)
emptyAdapter.setData(EmptyAdapter.STATUS_NORMAL)
emptyAdapter.setData(EmptyAdapter.STATUS_ERROR)

可以看出这样做就有几个好处,其一就是不用每个xml都写EmptyView,然后也能做到统一的管理,和一些人写的Helper这种的效果类似,最后调用的方法也很简单,你只需要创建一个Adaper,然后调用它的setData就行,我们的RecyclerView也是这样的,在外层去创建然后调用Adapter就行。


4. 总结


写这篇文章主要是为了水,啊不对,是为了想说明几个问题:

(1)开发时要善于跳出一些限制去思考,比如RecyclerView你可能觉得适配器模式就和它绑定了,就和View绑定了,有View的地方才能使用适配器模式,至少我觉得不是这样。

(2)学习设计模式,只去看一些介绍是很难理解的,当然要知道它的一个大致的思想,然后要灵活运用到开发中,这样学它才有用。

(3)我对适配器模式的理解就是 Object To Object,我可以去写ViewAdapter,可以去写DataAdapter,也可以去写BusinessAadpter,可以用这个模式去适配不同的场景,利用这个思想来使代码更加合理。


当然最后还是要强调一下,我不敢保证我说的就是对的,我肯定不是权威的。但至少我使用这招之后的的新代码效果要比一些旧代码更容易维护,更容易扩展。


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

Kotlin特性实现高拓展性Logger

前言 在Android中记录日志一般是使用Android自带的Log工具, 但是实际项目中,我们经常需要统一控制项目的日志开关或过滤等级,有时还需要将日志写入到文件中。 目前除了自己实现外,基本都是通过第三方的Log日志库比如JakeWharton/timbe...
继续阅读 »

前言


在Android中记录日志一般是使用Android自带的Log工具,


但是实际项目中,我们经常需要统一控制项目的日志开关过滤等级,有时还需要将日志写入到文件中。


目前除了自己实现外,基本都是通过第三方的Log日志库比如JakeWharton/timberorhanobut/logger ,但是有些情况仍然实现起来困难。


比如:



  • 多模块或封装SDK的情况,子模块也用到日志框架,如何单独管理子模块的日志开关?

  • Java library中没有Android Log类引入日志模块报错,同理使用JUnit做单元测试的时候日志模块报错。


并且替换项目中的Log是痛苦的,因此就需要一个可拓展的日志框架尤为重要。


和大多数流行的Logger框架不同,本文实现的Logger不是对各种数据进行美化输出,本文充分利用Kotlin的拓展函数实现了一个灵活配置的轻量Logger,它具有以下特性:



  • 控制模块中的Logger配置

  • 输出到文件等多个目标,控制输出线程

  • 支持拦截器,实现过滤,格式化

  • 支持Jvm使用,支持JUnit


项目地址 Logger


原理和


核心只有3个类




  1. Logger 日志的操作入口:主要保证稳定简单易用,用作门面。




  2. LogPrinter 日志输出目标:配置日志输出。




  3. LogLevel 日志输出等级:(VERBOSE,DEBUG,INFO,WARNING,ERROR,WTF)




Logger.png


日志记录


通过Logger 可以获取全局实例。


输出日志有许多等级,和android的Log等级是一致的:

Logger.v("Test verbose")
Logger.d("Test debug")
Logger.i("Test info")
Logger.w("Test warning")
Logger.e("Test error")
Logger.wtf("Test wtf")

Logger.v { "Test verbose" }
Logger.d { "Test debug" }
Logger.i { "Test info" }
Logger.w { "Test warning" }
Logger.e { "Test error" }
Logger.wtf { "Test wtf" }


通过Logger["subTag"]可以生成子Logger
默认情况,子LoggerlevellogPrinter均继承自父Logger,tag"$父tag-$subTag"

Logger.tag = "App" //全局Logger的tag设置为“APP”
class XxActivity {
val logger = Logger["XxActivity"] //二级Logger的tag为“APP-XxActivity”
val logger = loggerForClass() //使用当前类名生成二级Logger的tag为“APP-XxActivity”

inner class XxFragment {
val fragmentLogger = logger["XxFragment"]//三级Logger的tag为“APP-XxActivity-XxFragment”
}
}


日志配置


Logger总共只有4个属性:



  • level 过滤等级(VERBOSE,DEBUG,INFO,WARNING,ERROR,WTF)

  • tag 日志TAG

  • logPrinter 日志输出目标,比如Android Logcat,文件,标注输出流,Socket等。

  • loggerFactory 生产子Logger的工厂,默认情况,子LoggerlevellogPrinter均继承自父Logger,tag"$父tag-$subTag"


如下示例配置了顶级Loggerleveltag,输出目标为AndroidLogcat,同时在子线程把WARNING等级的日志按一定格式输出到文件"warning.log"中,
同时"SubModule"Logger的输出等级设置为ERROR等级。

Logger.level = LogLevel.VERBOSE
Logger.tag = "AppName"
Logger.logPrinter = AndroidLogPrinter()
.logAlso(FileLogPrinter({ File("warning.log") })
.format { _, _, messageAny, _ -> "【${Thread.currentThread().name}】:$messageAny" }
.logAt(Executors.newSingleThreadExecutor())
.filterLevel(LogLevel.WARNING)
)
Logger.loggerFactory = { subTag ->
Logger("$tag-$it", level, logPrinter).also { child ->
if (child.tag == "SubModule") {
logger.level = LogLevel.ERROR
}
}
}


实现


Logger功能实现:


因为Logger是门面,所以提供便捷的方法来使用,而真正的写入日志代理给LogPrinter

open class Logger(
var tag: String = "LOG",
var level: LogLevel = LogLevel.VERBOSE,
var logPrinter: LogPrinter = createPlatformDefaultLogPrinter(),
) {
fun v(message: Any?) = log(LogLevel.VERBOSE, message, null)
fun log(level: LogLevel, message: Any?, throwable: Throwable? = null) {
if (this.level <= level) {
logPrinter.log(level, tag, message, throwable)
}
}
//省略其他等级...

var loggerFactory: (childTag: String) -> Logger = ::defaultLoggerFactory

/**
* 创建子Logger
* @param subTag 次级tag,一般为模块名
*/
operator fun get(subTag: String): Logger = loggerFactory(subTag)

companion object INSTANCE : Logger(tag = "Logger")

}
enum class LogLevel(val shortName: String) {
VERBOSE("V"),
DEBUG("D"),
INFO("I"),
WARNING("W"),
ERROR("E"),
WTF("WTF")
}
fun interface LogPrinter {
fun log(level: LogLevel, tag: String, messageAny: Any?, throwable: Throwable?)
}

LogPrinter拓展实现


首先实现对LogPrinter进行拦截,后续的功能都通过拦截器实现。


拦截器

/**
* 拦截器
* logPrinter 被拦截对象
*/
typealias LogPrinterInterceptor = (logPrinter: Logger.LogPrinter, level: Logger.LogLevel, tag: String, messageAny: Any?, throwable: Throwable?) -> Unit

inline fun Logger.LogPrinter.intercept(crossinline interceptor: LogPrinterInterceptor) =
Logger.LogPrinter { level, tag, messageAny, throwable ->
interceptor(this@intercept, level, tag, messageAny, throwable)
}

添加额外的LogPrinter


添加一个额外的LogPrinter,也可看作将2个LogPrinter合并成1个。想要添加多个输出目标时使用。

fun Logger.LogPrinter.logAlso(other: Logger.LogPrinter) =
intercept { logPrinter, level, tag, messageAny, throwable ->
logPrinter.log(level, tag, messageAny, throwable)
other.log(level, tag, messageAny, throwable)
}


设置日志记录线程


控制LogPrinter的输出线程

fun Logger.LogPrinter.logAt(executor: Executor) =
intercept { logPrinter, level, tag, messageAny, throwable ->
executor.execute {
logPrinter.log(level, tag, messageAny, throwable)
}
}

格式化


控制LogPrinter的输出的格式,比如csv格式,Json格式等。

typealias LogFormatter = (level: Logger.LogLevel, tag: String, messageAny: Any?, throwable: Throwable?) -> String

fun Logger.LogPrinter.format(formatter: LogFormatter) =
intercept { logPrinter, level, tag, messageAny, throwable ->
val formattedMessage = formatter(level, tag, messageAny, throwable)
logPrinter.log(level, tag, formattedMessage, throwable)
}

日志过滤


LogPrinter中输出的日志进行过滤,可以根据tagmessagelevelthrowable进行组合判断来过滤。

fun Logger.LogPrinter.filter(
predicate: (
level: Logger.LogLevel,
tag: String,
messageAny: Any?,
throwable: Throwable?
) -> Boolean
) =
intercept { logPrinter, level, tag, messageAny, throwable ->
if (predicate(level, tag, messageAny, throwable)) {
logPrinter.log(level, tag, messageAny, throwable)
}
}


Logger拷贝


Logger为原型拷贝一个新Logger,和生成子Logger不同,它并不是通过loggerFactory生成的,并且tag也是拷贝的。

/**
* 拷贝
*/
fun Logger.copy(
tag: String = this.tag,
level: Logger.LogLevel = this.level,
logPrinter: Logger.LogPrinter = this.logPrinter,
loggerFactory: (childTag: String) -> Logger = ::defaultLoggerFactory,
) = Logger(tag, level, logPrinter).also { it.loggerFactory = loggerFactory }

Json格式化


因为并没有引入任何Android类和Json序列化库,所以没有内置。在此提供Gson示例


方式1,使用LogPrinter拓展


适用于该Logger所有日志都需要转Json的情况

val gson = GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create()

fun Logger.jsonLogger() =
copy(logPrinter = logPrinter.format { _, _, messageAny, _ -> gson.toJson(messageAny) })

//使用
fun testLogJsonLogger() {
val logger = Logger.jsonLogger()
logger.d {
arrayOf("hello", "world")
}
logger.i {
mapOf(
"name" to "tom",
"age" to 19,
)
}
}

方式2,拓展Logger方法


通过拓展Logger方法实现,适用于Logger的部分数据需要输出为Json模式。

inline fun Logger.logJson(level: Logger.LogLevel = Logger.LogLevel.INFO, any: () -> Any) {
log(level, block = { gson.toJson(any()) })
}

//使用
fun testLogJsonExt() {
Logger.logJson {
mapOf(
"name" to "tom",
"age" to 19,
)
}
}


拓展使用


这些拓展方法可以连续调用,就像使用RxJava一样。

Logger.logPrinter = ConsoleLogPrinter()
.format { _, tag, messageAny, _ -> "$tag : $messageAny\n" }
.logAlso(ConsoleLogPrinter()
.format { _, tag, messageAny, _ ->//添加分割线 tag,时间,message转json,同时加上堆栈信息
"""---------------------------\n $tag ${currentTime()} ${Json.toJson(messageAny)} \n${Thread.currentThread().stackTrace.contentToString()}"""
}
.filterLevel(LogLevel.INFO))//仅记录level在INFO及以上的
.logAlso(ConsoleLogPrinter()
.format { _, tag, messageAny, _ -> "$tag :《$messageAny》\n" }
.filter { _, tag, _, _ -> tag.contains("CHILD") })//仅记录tag包含CHILD

混淆


如果通过混淆去除日志信息,可按如下配置。

-assumenosideeffects class me.lwb.logger.Logger {
public *** d(...);

public *** e(...);

public *** i(...);

public *** v(...);

public *** log(...);

public *** w(...);

public *** wtf(...);

}

总结


本文主要使用了Kotlin拓展和高阶函数实现了一个拓展性高的Logger库,通过拓展方法实现线程切换,多输出,格式化等,同时通过配置全局logFactory的方法可以在不修改子模块代码的情况下去控制子模块Logger的level等信息。


该库十分精简,加上拓展和默认实现总代码小于300行,不依赖Android库第三方库,可以在纯Jvm程序中使用,也可在Android程序中使用。


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

北美 2023 被裁员的感悟

不贩卖焦虑,就事论事,希望能帮助到有需要的朋友。 很多人觉得在裁员之前是没有任何迹象的,其实真的不是这样。 公司在裁员的过程中有很多要裁员的迹象,我会在另外一篇文章中对我遇到的一些裁员信号设置一些雷区和警告,当你遇到上面的这些信号的时候,直觉告诉你需要马上考虑...
继续阅读 »

不贩卖焦虑,就事论事,希望能帮助到有需要的朋友。


很多人觉得在裁员之前是没有任何迹象的,其实真的不是这样。


公司在裁员的过程中有很多要裁员的迹象,我会在另外一篇文章中对我遇到的一些裁员信号设置一些雷区和警告,当你遇到上面的这些信号的时候,直觉告诉你需要马上考虑寻找下一个替代方案了。


因为当这些信号的任何一个或者多个同时出现的时候就意味着裁员在进行中了,通常会在 3 到 6 个月左右发生。


WeChat Image_20230602102637


在公司的职位


在被裁公司的职位是 Tech Lead。


虽然这个职位并不意味着你对其他同事而言能够获得更多的有效信息,但是通常可能会让自己与上级有更好的沟通管道。


但是,非常不幸的是这家公司的沟通渠道非常有问题。


因为负责相关开发部分的副总是从 PHP 转 Java 的,对 Java 的很多情况都不非常明确,所以他非常依赖一个架构师。


但,公司大部分人都认为这个架构师的要求是错误的,并且是非常愚蠢的。


比如说要求代码必须要放在一行上面,导致代码上面有不少行甚至超过了 1000 个字符。


所有开发都对这个要求非常不理解,并且多次提出这种要求是无理并且愚蠢的,我们组是对这个要求反应最激烈,并且抵触最大的(也有可能是因为我的原因,我不希望在明显错误的地方让步;我可以让步,但是需要给一个能说服的理由)。


然而,这个所谓的架构师就利用 PR 合并的权力,不停的让我们的组员进行修改。


裁员之前


正是因为在公司的职位和上面说到的和架构师直接的冲突。


在 6 个月之前,我就已经和组里的同事说要准备进行下一步了,你们该面试的就面试了,不要拖延。


在这个中间过程中,我的领导还找我谈过一次。领导的意思就是他非常同意我们的有关代码 PR 的要求,也觉得这些要求都是狗屁。


但,负责开发的副总,认为我们组现在是所有组里面最差的。


可能当时没有认真理解这句话的意思,我们组从所有组里面最好的,变成最差的只用了 2 周(一个 Sprint)的时间。


在这次谈话后,我更加坚信让我的组员找下一家的信息了,对他们中途要面试其他公司我都放开一面。


非常不幸的,我自己那该死的拖延症,我是把我自己的简历准备好了,但是还没有来得及投就等来了真正裁员的这一天。


深刻的教训和学到的经验:


如果公司的运营或者管理让感觉到不舒服,并且已经有开始寻找下家的想法的时候,一定要尽快准备,马上实施,不要拖延


这就是我在上面标黑马上的原因。


裁员过程


裁员过程非常简单和迅速,并且在毫不知情的情况下进行。


在周四的时候,公司的高层提示所有的会议今天全部取消,并且把应该 11 点开的全公司会议提前到了 9 点。


因为很多人都没有准备,所以很多人也没有参加。


后来才知道,9 点就是宣布裁员的开始,事后知道裁员比率为 40%。


然后就是各个部门找自己的被裁的员工开会,这个会议通常首先是一个 Zoom 的 Group 会议,说了一堆屁话,就是什么这是不得已的决定呀,什么乱七八糟的东西。


当然,在这个时候你还需要或者期待公司给你什么合理的理由呢?


然后就是 P&C 人员说话,基本上就是每个人 15 分钟的时间单独 Zoom。


这个 15 分钟,主要就是读下文件了,至于 2 个会议上是不是开摄像头,随意。


你愿意开也行,不愿意开也行,反正上面的所有人都心不在焉。我是懒得开,因为和你谈话的人,你都根本不认识。


第二个会议就是 P&C,这个会议主要就是和你说说被裁员后可以有的一些福利和什么的,对我个人来说我更关注的是补偿。


至于 401K 和里面的资金都是可以自行转的,也没啥需要他们说的,了解到补偿就是 6 周工资,不算多也凑合能接受。


负责裁员的人说,还有什么需要问的,我简单的回答了下 All Set 然后 have a nice day 就好了。毕竟他们只是具体做事的人,问他们也问不出个所以然,这有啥的。


裁员之后


裁员之后,感觉所有认识的被裁的同事都是懵的。


开完 15 分钟的 P&C 会议后,基本上电脑和邮箱马上就不能用了。公司貌似说电脑可以自己留着,但是上面的数据会被远程清理掉。


留在公司里面的东西会有人收拾后寄到家里。


我在公司里的办公桌就属于离职型办公桌,简单的来说,上面只有一台不属于我的显示器,另外就是从其他地方拿过来的一盒消毒湿巾,公司里面压根没有我需要的东西。


很多人认为公司禁用账户有点太不讲人情,其实从技术层面来说根本没有什么,因为所有的管理都是 LDAP,直接在 LDAP 上禁用你账户就好了,没啥稀奇的。


中午的时候,被裁的同事都互相留下了手机号码,感觉大家因为我在裁员列表里面感觉有点扯。另外更扯的同事在这个公司工作了 7 年了,也在列表里面(所有 PHP 的基础架构都是他写的和建立的)。


虽然最开始和这个同事有过一些摩擦,但是这个印度的同事真的挺好的,我们都觉得他挺不错,也愿意和他一起共事。


很多人,包括我。都对这个同事感觉不值,也觉得这很扯。


奈何公司的选择就是一些阿谀奉承,天天扯淡的人,比如说那个奇葩的架构师。


没多久,被裁的同事建了一个群,然后把我给拉进去了,主要还是我们组里面的同事,大家希望能够分享一些面试经验和机会,偶尔吐槽下。


在晚上的时候,突然收到另外一个同事的 LinkedIn 好友邀请,他不在这次裁员内。


但是他也被降职了,他本来是 Sr 开发人员和小组长,后来被提拔成架构师了,现在连小组长都不是了。


他和我说,如果需要帮助的话,他会尽量帮忙,并且还给他的一些曾经的招聘专员账号推送给了我。


我也非常感谢他们,虽然经历过,但是也收获了一些朋友,虽然说在美国职场比较难收获朋友,但是也并不是完全这样的。


没有了利益的纠葛,更容易说点实话。


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

Android开发中的MVC_MVP_MVVM

前言 MVC、MVP和MVVM是Android开发中常见的架构模式,这三种架构模式都有其独特的优点和适用场景。它们可以帮助开发者更好地组织和管理代码,提高应用程序的可维护性和可测试性,同时也能够提升开发效率和团队协作能力。选择适合的架构模式取决于项目的需求和开...
继续阅读 »

前言


MVC、MVP和MVVM是Android开发中常见的架构模式,这三种架构模式都有其独特的优点和适用场景。它们可以帮助开发者更好地组织和管理代码,提高应用程序的可维护性和可测试性,同时也能够提升开发效率和团队协作能力。选择适合的架构模式取决于项目的需求和开发团队的经验,理解和掌握这些架构模式对于Android开发者来说是非常重要的。现通过一个案例需求分析三种框架


案例需求



查询用户账号信息:用户输入账号,点击按钮可进行查询账号信息,如果查询数据成功,则将数据展示在界面上;如果查询数据失败,则在界面上提示获取数据失败



MVC框架模型


为什么学习MVC?


如果不使用任何框架来实现这个案例需求,则需要实现以下功能:



  • 获取用户输入的信息

  • 展示获取信息成功界面

  • 展示获取信息失败界面

  • 查询用户数据

  • 业务逻辑


代码示例


Bean对象

//账号信息
public class Account {
private String name; //账号名称
private int level; //账号登记

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public int getLevel() {
return level;
}

public void setLevel(int level) {
this.level = level;
}
}

回调接口

public interface MCallback {
void onSuccess(Account account);
void onFailed();
}

整体业务逻辑

public class NormalActivity extends AppCompatActivity implements View.OnClickListener {

private EditText mEtAccount;
private TextView mTvResult;
private Button mBtGetAccount;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_normal);

initView();
}

private void initView() {
mEtAccount = findViewById(R.id.et_account);
mTvResult = findViewById(R.id.tv_result);
mBtGetAccount = findViewById(R.id.btn_getAccount);
mBtGetAccount.setOnClickListener(this);
}

@Override
public void onClick(View view) {
switch (view.getId()) {
case R.id.btn_getAccount:
String userInput = getUserInput();
getAccountData(userInput, new MCallback() {
@Override
public void onSuccess(Account account) {
showSuccessPage(account);
}

@Override
public void onFailed() {
showFailedPage();
}
});
}
}

//获取用户输入的信息
private String getUserInput() {
return mEtAccount.getText().toString();
}

//展示获取数据成功的界面
private void showSuccessPage(Account account) {
mTvResult.setText("用户账号:" + account.getName() + "|"
+ "用户等级:" + account.getLevel());
}

//展示获取数据失败的界面
private void showFailedPage() {
mTvResult.setText("获取数据失败");
}

//模拟查询账号数据
private void getAccountData(String accountName, MCallback callback) {
Random random = new Random();
boolean isSuccess = random.nextBoolean();
if (isSuccess) {
Account account = new Account();
account.setName(accountName);
account.setLevel(100);
callback.onSuccess(account);
} else {
callback.onFailed();
}
}
}

MVC模型简介


MVC是一种经典的架构模式,将应用程序分为三个主要组成部分:模型(Model)、视图(View)和控制器(Controller)。模型负责数据管理和业务逻辑,视图负责用户界面展示,控制器处理用户输入和协调模型与视图之间的交互。


截屏2023-06-07 15.27.41.png



  • Controller:Activity、Fragment

  • View:layout、View控件

  • Model:数据处理(网络请求、SQL等)


MVC代码演练


使用MVC框架实现案例需求的话,需要实现以下功能:



  • MVCActivity(C层):业务逻辑处理、获取用户输入、展示成功页面、展示失败页面

  • MVCModel层(M层):查询账号数据

  • View层(V层):layout



  1. 将数据的获取与界面的展示分离(将查询账号数据从Acitity中分离到Model中即可)

  2. 解决各层之间通信问题(Activity通知Model获取数据,Model通知Activity更新界面)


将查询账号数据抽离到MVCModel中:

public class MVCModel {
//模拟查询账号数据
public void getAccountData(String accountName, MCallback callback) {
Random random = new Random();
boolean isSuccess = random.nextBoolean();
if (isSuccess) {
Account account = new Account();
account.setName(accountName);
account.setLevel(100);
callback.onSuccess(account);
} else {
callback.onFailed();
}
}
}

MVC的优缺点



  • 优点:
    一定程度上实现了Model与View的分离,降低了代码的耦合度。

  • 缺点:
    Controller与View难以完全解耦,并且随着项目复杂度的提升,Controller将越来越臃肿。


MVP框架模型


MVP模型简介


MVP模式在MVC的基础上做了一些改进,将视图和模型之间的直接交互改为通过一个中间层——Presenter来完成。Presenter负责处理视图的逻辑和用户交互,并将数据获取和处理的任务委托给模型。
截屏2023-06-12 16.34.57.png



  1. Model与View不再直接进行通信,而是通过中间层Presenter来实现

  2. Activity的功能被简化,不再充当控制器,主要负责View层面的工作


MVP代码实战


使用MVP框架实现案例需求的话,需要实现以下功能:



  • MVPActivity(V层):获取用户输入、展示成功界面、展示失败界面

  • MVPPresenter(P层):业务逻辑处理

  • MVPModel(M层):查询账号数据



  1. MVPctivity负责提供View层面的功能(采用实现接口的方式)

  2. MVPModel负责提供数据方面的功能

  3. Model与View不再进行直接通信,通过Presenter来实现


IMVPView接口设计

public interface IMVPView {
String getUserInput();
void showSuccessPage(Account account);
void showFailedPage();
}

MVPModel设计

public class MVPModel {
//模拟查询账号数据
public void getAccountData(String accountName, MCallback callback) {
Random random = new Random();
boolean isSuccess = random.nextBoolean();
if (isSuccess) {
Account account = new Account();
account.setName(accountName);
account.setLevel(100);
callback.onSuccess(account);
} else {
callback.onFailed();
}
}
}

MVPPresenter设计

public class MVPPresenter {
private IMVPView imvpView;
private MVPModel mvpModel;

public MVPPresenter(IMVPView imvpView) {
this.imvpView = imvpView;
mvpModel = new MVPModel();
}

public void getData(String accountName) {
mvpModel.getAccountData(accountName, new MCallback() {
@Override
public void onSuccess(Account account) {
imvpView.showSuccessPage(account);
}

@Override
public void onFailed() {
imvpView.showFailedPage();
}
});
}
}

MVP的优缺点



  • 优点:解决了MVC中Controller与View过度耦合的缺点,职责划分明显,更加易于维护

  • 缺点:接口数量多,项目复杂度升高。随着项目复杂度的提升,Presenter层将越来越臃肿。


使用MVP的建议:



  1. 接口规范化(封装父类接口以减少接口的使用量)

  2. 使用第三方插件自动生成MVP代码

  3. 对于一些简单的界面,可以选择不使用框架

  4. 根据项目复杂度,部分模块可以选择不使用接口


MVVM框架模型


MVVM模型简介


MVVM模式进一步改进了MVP模式,引入了一个新的组件——ViewModel。ViewModel与视图进行双向绑定,负责处理视图的状态和逻辑,同时也能够监听模型的变化。这种双向绑定的机制使得视图与数据的同步更加方便,减少了手动更新视图的代码量。


截屏2023-06-13 11.27.10.png



  1. 减少了接口数量

  2. 告别繁琐findViewById操作


DataBinding学习


DataBinding是谷歌官方发布的一个实现数据绑定的框架(实现数据与视图的双向绑定),DataBinding可以帮助我们在安卓中更好的实现MVVM模式。


DataBinding使用步骤



  1. 启用DataBinding

  2. 修改布局文件为DataBinding布局

  3. 数据绑定


DataBinding实战


在 build.gradle(app)的android中启动DataBinding

dataBinding {
enabled = true
}

alt+enter或option+enter修改布局为DataBinding布局

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

<data>

<variable
name="account"
type="com.example.mdemo.bean.Account" />
<variable
name="activity"
type="com.example.mdemo.databinding.DemoActivity" />
</data>

<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical"
tools:context=".databinding.DemoActivity">

<TextView
android:id="@+id/tv_info"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="50dp"
android:gravity="center"
android:text="@{account.name+'|'+account.level}" />

<Button
android:id="@+id/btn_addLevel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="50dp"
android:onClick="@{activity.onclick}"
android:text="账号等级+1" />
</LinearLayout>
</layout>

数据绑定

public class DemoActivity extends AppCompatActivity {

private Account account;
private ActivityDemoBinding binding;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = DataBindingUtil.setContentView(this, R.layout.activity_demo);
account = new Account();
account.setName("TEST");
account.setLevel(100);
binding.setAccount(account);
binding.setActivity(this);
}

public void onclick(View view) {
Toast.makeText(this, "点击了", Toast.LENGTH_SHORT).show();
int level = account.getLevel();
account.setLevel(level + 1);
binding.setAccount(account);
}
}

代码优化


为减少binding.setAccount赋值实现数据更新的代码冗余操作,可借助BaseObservable接口、Bindable注解及notifyPropertyChanged实现数据自动更新操作

public class Account extends BaseObservable {
private String name; //账号名称
private int level; //账号登记

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

@Bindable
public int getLevel() {
return level;
}

public void setLevel(int level) {
this.level = level;
notifyPropertyChanged(BR.level);
}
}

MVVM代码实战


使用MVVM框架实现案例需求的话,需要实现以下功能:



  • layout(V层):获取用户输入、展示成功界面、展示失败界面

  • MVVMViewModel(VM层):业务逻辑处理、数据更新

  • MVVMModel层(M层):查询账号数据



  1. 提供View、ViewModel以及Model三层

  2. 将布局修改为DataBinding布局

  3. View与ViewModel之间通过DataBinding进行通信

  4. 获取数据并展示在界面上


MVVMDataBinding布局

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

<data>
<variable
name="viewModel"
type="com.example.mdemo.mvvm.MVVMViewModel" />

</data>

<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".mvvm.MVVMActivity">

<EditText
android:id="@+id/et_account"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="请输入要查询的账号"
android:layout_marginTop="30dp"
android:paddingLeft="5dp"
android:paddingRight="5dp"
android:text="@={viewModel.userInput}"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />

<Button
android:id="@+id/btn_getAccount"
android:text="查询用户信息"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="100dp"
android:onClick="@{viewModel.getData}"
app:layout_constraintTop_toTopOf="@id/et_account"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />

<TextView
android:id="@+id/tv_result"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{viewModel.result}"
app:layout_constraintTop_toTopOf="@+id/btn_getAccount"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
android:layout_marginTop="120dp" />

</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

MVVMViewModel设计

public class MVVMViewModel extends BaseObservable {
private MVVMModel mvvmModel;
private String userInput;
private ActivityMvvmactivityBinding binding;
private String result;

//一般需要传入Application对象,方便在ViewModel中使用application,
//比如sharedpreferences需要使用
public MVVMViewModel(Application application) {
mvvmModel = new MVVMModel();
}

public MVVMViewModel(Application application, ActivityMvvmactivityBinding binding) {
mvvmModel = new MVVMModel();
this.binding = binding;
}

@Bindable
public String getResult() {
return result;
}

public void setResult(String result) {
this.result = result;
notifyPropertyChanged(BR.result);
}

@Bindable
public String getUserInput() {
return userInput;
}

public void setUserInput(String userInput) {
this.userInput = userInput;
notifyPropertyChanged(BR.userInput);
}


public void getData(View view) {
// String userInput = binding.etAccount.getText().toString();
mvvmModel.getAccountData(userInput, new MCallback() {
@Override
public void onSuccess(Account account) {
String info = account.getName() + "|" + account.getLevel();
setResult(info);
}

@Override
public void onFailed() {
setResult("获取数据失败");
}
});
}
}

MVVMActivity

public class MVVMActivity extends AppCompatActivity {

private ActivityMvvmactivityBinding binding;
private MVVMViewModel mvvmViewModel;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = DataBindingUtil.setContentView(this,R.layout.activity_mvvmactivity);
mvvmViewModel = new MVVMViewModel(getApplication(),binding);
binding.setViewModel(mvvmViewModel);
}
}

LiveData+ViewModel



  • LiveData是一个可以被观察的数据持有者,它可以通过添加观察者的方式来让其他组件观察它的变更

  • LiveData遵从应用程序的生命周期(如果LiveData的观察者已经是销毁状态,LiveData就不会通知该观察者)


MVVM的优缺点



  • 优点:实现了数据和视图的双向绑定,极大的简化代码

  • 缺点:bug难以调试,并且dataBinding目前还存在一些编译问题


总结



  • MVC:学习简单但是解耦不够彻底

  • MVP:解耦更加彻底,学习相对简单,但是代码相对繁琐

  • MVVM:代码逻辑简介,但是学习成本较大

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

从Kotlin中return@forEach了个寂寞

今天在Review(copy)同事代码的时候,发现了一个问题,想到很久之前,自己也遇到过这个问题,那么就来看下吧。首先,我们抽取最小复现代码。(1..7).forEach { if (it == 3) { return@forEach ...
继续阅读 »

今天在Review(copy)同事代码的时候,发现了一个问题,想到很久之前,自己也遇到过这个问题,那么就来看下吧。首先,我们抽取最小复现代码。

(1..7).forEach {
if (it == 3) {
return@forEach
}
Log.d("xys", "Num: $it")
}

�很简单的代码,我相信很多人都这样写过,实际上就是遍历的过程中,满足条件后就退出遍历,那么上面的代码,能实现这样的需求吗?我们来看下执行结果。

Num: 1
Num: 2
Num: 4
Num: 5
Num: 6
Num: 7

很遗憾,即使等于3之后就return了,但是然并卵,遍历依然继续执行了。相信很多写Kotlin的开发者都遇到过这个问题,其原因,还是在于语法的思维定势,我们在Kotlin的文档上,可以找到非常明确的解释。
kotlinlang.org/docs/return…


我们先来看下Kotlin中forEach的源码。

/**
* Performs the given [action] on each element.
*/
@kotlin.internal.HidesMembers
public inline fun <T> Iterable<T>.forEach(action: (T) -> Unit): Unit {
for (element in this) action(element)
}

�我们来提取下关键信息:



  • 内联函数

  • 高阶函数


发现了吗,由于高阶函数的存在,当你在高阶函数的闭包内「return」时,是结束的整个函数,当你使用「return@forEach�」时,是结束当前的闭包,所以,如果你像这样写:

(1..7).forEach {
if (it == 3) {
return
}
Log.d("xys", "Num: $it")
}

那么等于3之后,整个函数就被return了,那么如果你像文章开头这样写,那么等效于continue,因为你结束了当前的闭包,而这个闭包只是其中的一次遍历过程。那么我们要如何实现我们最初的需求呢?看到这样,答案其实已经呼之欲出了,那就是要return整个遍历的闭包。所以,官方也给出了解决方案,那就是外面套一层闭包:

run loop@{
(1..7).forEach {
if (it == 3) {
return@loop
}
Log.d("xys", "Num: $it")
}
}

写起来确实是麻烦一点,但这却是必不可少的过程,是引入闭包所带来的一点副作用。



当然这里不仅限于run,任何闭包都是可以的。



欢迎大家关注我的公众号——【群英传】,专注于「Android」「Flutter」「Kotlin」
我的语雀知识库——http://www.yuque.com/xuyisheng


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

[Android] 弱网检测示例

使用ConnectivityManager类来检查当前的网络状态和连接类型 使用以下代码来检查当前是否有可用的网络连接:ConnectivityManager cm = (ConnectivityManager) getSystemService(Contex...
继续阅读 »

使用ConnectivityManager类来检查当前的网络状态和连接类型


使用以下代码来检查当前是否有可用的网络连接:

ConnectivityManager cm = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo activeNetwork = cm.getActiveNetworkInfo();
boolean isConnected = activeNetwork != null && activeNetwork.isConnectedOrConnecting();

您还可以使用ConnectivityManager的getNetworkCapabilities()方法来检查当前网络的能力。例如,您可以使用以下代码来检查当前网络是否支持较高的下载速度:

ConnectivityManager cm = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkCapabilities nc = cm.getNetworkCapabilities(cm.getActiveNetwork());
if (nc.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) {
// 这是一个蜂窝网络
if (nc.hasCapability(NetworkCapabilities.NET_CAPABILITY_LTE)) {
// 快
} else {
// 慢
}
}

使用Ping或者Traceroute命令来检测网络质量


然后,您可以使用Java的ProcessBuilder类来执行Ping命令。例如,您可以使用以下代码来Ping http://www.baidu.com

// ProcessBuilder pb = new ProcessBuilder("ping", "-c", "4", "www.baidu.com");
ProcessBuilder pb = new ProcessBuilder("traceroute", "-n", "www.baidu.com");
pb.redirectErrorStream(true);
Process process = pb.start();

// 读取命令的输出
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
String line;
while ((line = reader.readLine()) != null) {
// todo:处理输出行
}

请注意,需要在应用的主线程之外运行此代码,因为它会阻塞线程。您可以使用协程或其他方法来在后台线程上执行Ping/Traceroute命令。


执行Ping命令后,您可以从输出中提取有关网络质量的信息。例如,您可以查找"time="字符串来获取每个Ping请求的延迟(单位是毫秒)。您还可以查找"packet loss"字符串来获取丢包率。


执行Traceroute命令后,您可以从输出中提取有关网络路径的信息。例如,每行输出都包含一个路由器的IP地址和延迟(单位是毫秒)。您可以使用此信息来了解数据在网络中的传输路径,以及在路径上的延迟。


最后,请注意,使用Ping/Traceroute命令进行网络测量可能会对网络造成轻微负载,并且在某些情况下可能会被防火墙阻止。因此,您应该谨慎使用此方法。


为了在网络状况较差的情况下优化Android应用的性能,您可以考虑以下几点:



  1. 使用缓存:缓存常用的数据和资源可以帮助您的应用在没有网络连接的情况下也能工作。您可以使用Android提供的缓存API,或者使用第三方库来管理缓存。

  2. 使用离线模式:如果您的应用依赖于网络连接来提供内容,那么您可以考虑让用户在离线时也能使用应用。这可以通过在线时下载内容并在离线时使用来实现。

  3. 使用后台同步:如果您的应用需要定期与服务器同步数据,那么您可以使用Android的后台同步机制来实现。这样,当网络状况较差时,您的应用就可以在后台自动同步数据,而不会影响用户体验。

  4. 优化网络请求:对于使用网络加载数据的应用,可以考虑优化网络请求。这可以通过减少不必要的请求,使用压缩传输,以及使用较快的网络连接来实现。

  5. 使用较少的数据:尽量减少应用使用的数据量可以帮助您的应用在网络状况较差的情况下更好地运行。这可以通过优化图像大小,使用缓存和离线模式,以及减少不必要的数据传输来实现。

  6. 使用较快的网络连接:如果您的应用需要使用网络,那么使用较快的网络连接可以帮助您的应用在网络状况较差的情况下更好地运行。例如,您可以使用Wi-Fi而不是移动数据连接,或者在有多个可用网络连接时选择速度较快的连接。

  7. 使用网络状态监测:通过监测网络状态,您的应用可以根据当前的网络状态调整其行为。例如,您可以在网络状态较差时显示提示信息,或者在网络不可用时禁用某些功能。

  8. 尝试使用新的网络技术:最新的移动设备和操作系统通常都会提供更多的网络优化功能。例如,Android 11引入了网络请求优化工具,可以帮助您的应用在网络状况较差的情况下更好地运行

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

速度优化:重新认识速度优化

应用的速度优化是我们使用最频繁,也是应用最重要的优化之一,它包括启动速度优化,页面打开速度优化,功能或业务执行速度优化等等,能够直接提升应用的用户体验。因此,只要是 Android 开发者,肯定或多或少有过速度相关的优化经验。但是大部分人谈到速度优化,只能想到...
继续阅读 »

应用的速度优化是我们使用最频繁,也是应用最重要的优化之一,它包括启动速度优化,页面打开速度优化,功能或业务执行速度优化等等,能够直接提升应用的用户体验。因此,只要是 Android 开发者,肯定或多或少有过速度相关的优化经验。但是大部分人谈到速度优化,只能想到一些零碎的优化点,比如使用多线程、预加载等等。这对速度的提升肯定是不够的,想要做得更好,我们不妨来思考下面几个问题:




  • 我们的优化方案是全面且体系的吗?




  • 我们的方案为什么能提升速度呢?




  • 我们的方案效果怎样?




想要回答好这几个问题,我们就需要了解影响和决定应用速度的底层原理及本质。那从底层来看,CPU、缓存、任务调度才是决定应用速度最本质的因素。CPU 和缓存都属于硬件层,任务调度机制则属于操作系统层。


那这一节课,我们就一起深入硬件和操作系统层面去了解以上三个因素是如何决定应用速度的,重新认识应用的速度优化,由下而上地建立起速度优化的认知体系和方法。


如何从 CPU 层面进行速度优化?


我们知道,所有的程序最终会被编译成机器码指令,然后交给 CPU 执行,CPU 以流水线的形式一条一条执行程序的机器码指令。当我们想要提升某些场景(如启动、打开页面、滑动等)的速度时,本质上就是降低 CPU 执行完这些场景指令的时间,这个时间简称为 CPU 时间。想要降低 CPU 时间,我们需要先知道程序所消耗 CPU 时间的计算公式:CPU 时间=程序的指令数 x 时钟周期时间 x 每条指令的平均时钟周期数。下面一一解释一下这三项因子的含义。




  • 程序的指令数:这一项很好理解,就是程序编译成机器码指令后的指令数量。




  • 时钟周期时间:每一次时钟周期内,CPU 仅完成一次执行,所以时钟周期时间越短,CPU 执行得越快。或许你对时钟周期时间不熟悉,但是它的倒数也就是时钟周期频率,你肯定听说过。1 纳秒的时钟周期时间就是 1 GHZ 的时钟周期频率,厂商发布新手机或者我们购买新手机时,都或多或少会提到 CPU 的时钟频率,比如高通骁龙 888 这款 CPU 的时钟频率是 2.8 GHZ,这个指标也是衡量 CPU 性能最重要的一个指标




  • 每条指令的平均时间周期:是指令执行完毕所消耗的平均时间周期,指令不同所需的机器周期数也不同。对于一些简单的单字节指令,在取指令周期中,指令取出到指令寄存器后会立即译码执行,不再需要其它的机器周期。对于一些比较复杂的指令,例如转移指令、乘法指令,则需要两个或者两个以上的机器周期。




从 CPU 来看,当我们想要提升程序的速度时,优化这三项因子中的任何一项都可以达到目的。那基于这三项因子有哪些通用方案可以借鉴呢?


减少程序的指令数


通过减少程序的指令数来提升速度,是我们最常用也是优化方案最多的方式,比如下面这些方案都是通过减少指令数来提升速度的。




  1. 利用手机的多核:当我们将要提速的场景的程序指令交给多个 CPU 同时执行时,对于单个 CPU 来说,需要执行的指令数就变少了,那 CPU 时间自然就降低了,也就是并发的思想。但要注意的是,并发只有在多核下才能实现,如果只有一个 CPU,即使我们将场景的指令拆分成多份,对于这个 CPU 来说,程序的指令数依然没有变少。如何才能发挥机器的多核呢?使用多线程即可,如果我们的手机是 4 核的,就能同时并发的运行 4 个线程。




  2. 更简洁的代码逻辑和更优的算法:这一点很好理解,同样的功能用更简洁或更优的代码来实现,指令数也会减少,指令数少了程序的速度自然也就快了。具体落地这一类优化时,我们可以用抓 trace 或者在函数前后统计耗时的方式去分析耗时,将这些耗时久的方法用更优的方式实现。




  3. 减少 CPU 的闲置:通过在 CPU 闲置的时候,执行预创建 View,预准备数据等预加载逻辑,也是减少指令数的一种优化方案,我们需要加速场景的指令数量由于预加载执行了一部分而变少了,自然也就快了。




  4. 通过其他设备来减少当前设备程序的指令数:这一点也衍生很多优化方案,比如 Google 商店会把某些设备中程序的机器码上传,这样其他用户下载这个程序时,便不需要自己的设备再进行编译操作,因为提升了安装或者启动速度。再比如在打开一些 WebView 网页时,服务端会通过预渲染处理,将 IO 数据都处理完成,直接展示给用户一个静态页面,这样就能极大提高页面打开速度。




上面提到的这些方案都是我们最常用的方案,基于指令数这一基本原理,还能衍生出很多方案来提升速度,这里没法一一列全,大家也可以自己想一想还能扩展出哪些方案出来。


降低时钟周期时间


想要降低手机的时钟周期,一般只能通过升级 CPU 做到,每次新出一款 CPU,相比上一代,不仅在时钟周期时间上有优化,每个周期内可执行的指令也都会有优化。比如高通骁龙 888 这款 CPU 的大核时钟周期频率为 2.84GHz,而最新的 Gen 2 这款 CPU 则达到了 3.50GHz。


虽然我们没法降低设备的时钟周期,但是应该避免设备提高时钟周期时间,也就是降频现象,当手机发热发烫时,CPU 往往都会通过降频来减少设备的发热现象,具体的方式就是通过合理的线程使用或者代码逻辑优化,来减少程序长时间超负荷的使用 CPU。


降低每条指令的平均时间周期


在降低每条指令的平均时间周期上,我们能做的其实也不多,因为它和 CPU 的性能有很大的关系,但除了 CPU 的性能,以下几个方面也会影响到指令的时间周期。




  1. 编程语言:Java 翻译成机器码后有更多的简介调用,所以比 C++ 代码编译成的机器码指令的平均时间周期更长。




  2. 编译程序:一个好的编译程序可以通过优化指令来降低程序指令的平均时间周期。




  3. 降低 IO 等待:从严格意义来说,IO 等待的时间并不能算到指令执行的耗时中,因为 CPU 在等待 IO 时会休眠或者去执行其他任务。但是等待 IO 会使执行完指令的时间变长,所以这里依然把减少 IO 等待算入是降低每条指令的平均时间周期的优化方案之一。




如何从缓存层面进行速度优化?


程序的指令并不是直接就能被 CPU 执行的,而是要放在缓存中,CPU 从缓存中读取,而且一个程序也不可能全是 CPU 计算逻辑,必然也会涉及到 IO 的操作或等待,比如往磁盘或者内存中读写数据成功后才能继续执行后面的逻辑,所以缓存也是决定应用速度的关键因素之一。缓存对程序速度的影响主要体现在 2 个方面:




  1. 缓存的读写速度;




  2. 缓存的命中率。




下面就详细讲解一下这 2 方面对速度的影响。


缓存的读写速度


手机或电脑的存储设备都被组织成了一个存储器层次结构,在这个层次结构中,从上至下,设备的访问速度越来越慢,但容量也越来越大,并且每字节的造价也越来越便宜。寄存器文件在层次结构中位于最顶部,也就是第 0 级。下图展示的是三层高速缓存的存储结构。


img


高速缓存是属于 CPU 的组成部分,并且实际有几层高速缓存也是由 CPU 决定的。以下图高通骁龙 888 的芯片为例,它是 8 块核组成的 CPU,从架构图上可以看到,它的 L2 是 1M 大小(没有 L1 是因为这其实只是序号称呼上的不同而已,你也可以理解成 L1),L3 是 3M 大小,并且所有核共享。


img


不同层之间的读写速度差距是很大的,所以为了能提高场景的速度,我们需要将和核心场景相关的资源(代码、数据等)尽量存储在靠上层的存储器中。 基于这一原理,便能衍生出了非常多的优化方案,比如常用的加载图片的框架 Fresco,请求网络的框架 OkHttp 等等,都会想尽办法将数据缓存在内存中,其次是磁盘中,以此来提高速度。


缓存的命中率


将数据放在缓存中是一种非常入门的优化思想,也是非常容易办到的,即使是开发新手都能想到以此来提升速度。但是我们的缓存容量是有限的,越上层的缓存虽然访问越快,但是容量越少,价格也越贵,所以我们只能将有限的数据存放在缓存中,在这样的制约下,提升缓存的命中率往往是一件非常难的事情


一个好的编译器可以提升寄存器的命中率,好的操作系统可以提升高速缓存的命中率,对于我们应用来说,好的优化方案可以提升主存和硬盘的命中率,比如我们常用的 LruCache 等数据结构都是用来提升主存命中率的。除了提升应用的主存,应用也可以提升高速缓存的命中率,只是能做的事情不多,后面的章节中也会介绍如何通过 Dex 中 class 文件重排,来提升高速缓存读取类文件时的命中率。


想要提高缓存命中率,一般都是利用局部性原理(局部性原理指如果某数据被访问,则不久之后该数据可能再次被访问,或者程序访问了某个存储单元,则不久之后,其附近的存储单元也将被访问)或者通过行为预测,分析大概率事件等多种原理来提高缓存命中率。


如何从任务调度层面进行速度优化?


我们学过操作系统为了能同时运行多个程序,所以诞生了虚拟内存这个技术,但只有虚拟内存技术是不够的,还需要任务调度机制,所以任务调度也属于操作系统关键的组成之一。有了任务调度机制,我们的程序才能获得 CPU 的资源并正常跑起来,所以任务调度也是影响程序速度的本质因素之一


我们从两个方面来熟悉任务调度机制,一是调度机制的原理,二是任务的载体,即进程的生命周期。


在 Linux 系统中,任务调度的维度是进程,Java 线程也属于轻量级的进程,所以线程也是遵循 Linux 系统的任务调度规则的,那进程的调度规则又是怎样的呢?Linux 系统将进程分为了实时进程和普通进程这两类,实时进程需要响应技术的进程,比如 UI 交互进程,而普通进程对响应速度要求不是非常高,比如读写文件、下载等进程。两种类型的进程的调度规则也不一样,我们分别来说。


首先是实时进程的调度规则。Linux 系统对实时进程的调度策略有两种:先进先出(SCHED_FIFO)和循环(SCHED_RR)。Android 只使用了 SCHED_FIFO 这一策略,所以我们主要介绍 SCHED_FIFO 。当系统使用先进先出的策略来调度进程时,如果某个进程占有 CPU 时间片,此时没有更高优先级的实时进程抢占 CPU,或该进程主动让出,那么该进程就始终保持使用 CPU 的状态。这种策略会提高进程运行的持续时间,减少被打断或被切换的次数,所以响应更及时。Android 中的 AudIO、SurfaceFlinger、Zygote 等系统核心进程都是实时进程。


非实时进程也称为普通进程,针对普通进程,Linux 系统则采用了一种完全公平调度算法来实现对进程的切换调度,我们可以不需要知道这一算法的实现细节,但需要了解它的原理。在完全公平调度算法中,进程的优先级由 nice 值表示,nice 值越低代表优先级越大,但是调度器并不是直接根据 nice 值的大小作为优先级来进行任务调度的,当每次进程的时间片执行完后,调度器就会寻找所有进程中运行时间最少的进程来执行


既然调度器是根据进程的运行时间来进行任务调度,那进程优先级即 nice 值的作用又体现在哪呢?实际上,这里进程的运行时间并不是真实的物理运行时间,而是进行了加权计算的虚拟时间,这个权值系数就是 nice 值,所以同样的物理时间内,nice 值越低的进程所记录的运行时间实际越少,运行时间更少就更容易被调度器所选择,优先级也就这样表现出来了。在 Android 中,除了部分核心进程,其他大部分都是普通进程。


了解了进程的调度原理,我们再来了解一下进程的生命周期。


img


通过上图可以看到,进程可能有以下几种状态。并且运行、等待和睡眠这三种状态之间是可以互相转换的。




  • 运行:该进程此刻正在执行。




  • 等待:进程能够运行,但没有得到许可,因为 CPU 分配给另一个进程。调度器可以在下一次任务切换时选择该进程。




  • 睡眠:进程正在睡眠无法运行,因为它在等待一个外部事件。调度器无法在下一次任务切换时选择该进程。




  • 终止:进程终止。




知道了任务调度相关的原理后,怎样根据这些原理性知识来优化应用场景的速度呢?实际上,我们对进程的优先级做不了太大的改变,即使改变了也产生不了太大的作用,但是前面提到了线程实际是轻量级的进程,同样遵循上面的调度原理和规则,所以我们真正落地的场景在线程的优化上。基于任务调度的原理,我们可以衍生出这 2 类的优化思路:




  1. 提高线程的优先级:对于关键的线程,比如主线程,我们可以提高它的优先级,来帮助我们提升速度。除了直接提高线程的优先级,我们还可以将关键线程绑定 CPU 的大核这一种特殊的方式来提高该线程的执行效率。




  2. 减少线程创建或者状态切换的耗时:这一点可以通过在线程池中设置合理的常驻线程,线程保活时间等参数来减少线程频繁创建或者状态切换的耗时。因为线程池非常重要,我们后面会专门用一节课来详细讲解。




小结


在这一节中,我们详细介绍了影响程序速度的三个本质因素,并基于这三个因素,介绍了许多衍生而来优化思路,这其实就是一种自下而上的性能优化思路,也就是从底层原理出发去寻找方案,这样我们在进行优化时,才能更加全面和体系。


希望你通过这一节的学习,能对速度优化建立起一个体系的认知。当然,你可能会觉得我们这一节介绍的优化思路太过简洁,不必担心,在后面的章节中,我们会基于 CPU、缓存和任务调度这三个维度,挑选出一些优化效果较好的方案,进行更加深入的详细讲解。


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

程序员浪漫起来 做一个心形layout

序言 最近浪漫心饱满,于是写了个心形控件。可以将一切内容约束成心形,并且支持两种心形,使用起来很简单,就当成FrameLayout就行了。 让一切都浪漫起来 效果 下面依次展示的是: 包裹ScrollView ,形状为桃心 包裹ScrollView,形状为圆...
继续阅读 »

序言


最近浪漫心饱满,于是写了个心形控件。可以将一切内容约束成心形,并且支持两种心形,使用起来很简单,就当成FrameLayout就行了。
让一切都浪漫起来


效果


下面依次展示的是:



  1. 包裹ScrollView ,形状为桃心

  2. 包裹ScrollView,形状为圆心

  3. 包裹WebView,形状为圆心


在这里插入图片描述


代码


LoveLayout


主要代码就是下面的。

package com.example.myapplication;

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Path;
import android.util.AttributeSet;
import android.widget.FrameLayout;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

/**
* <pre>
* Created by zhuguohui
* Date: 2023/6/6
* Time: 10:26
* Desc:爱心Layout
* </pre>
*/
public class LoveLayout extends FrameLayout {

private Path path1;
private int heardType;

public LoveLayout(@NonNull Context context) {
super(context);
}

public LoveLayout(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.LoveLayout);
heardType = array.getInt(R.styleable.LoveLayout_HeardType,0);
array.recycle();

}

@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
if(heardType==0) {
getHeardPath();
}else{
getHeardPath2();
}
}

private void getHeardPath(){

int px = getMeasuredWidth() / 2;
int py = getMeasuredHeight() / 2;
path1=new Path();
path1.moveTo(px, py);
float rate=getMeasuredWidth()*1.0f/34;
// 根据心形函数画图
for (double i = 0; i < 2 * Math.PI; i += 0.001) {
float x = (float) (16 * Math.sin(i) * Math.sin(i) * Math.sin(i));
float y = (float) (13 * Math.cos(i) - 5 * Math.cos(2 * i) - 2 * Math.cos(3 * i) - Math.cos(4 * i));
x *= rate;
y *= rate;
x = px - x;
y = py - y;
path1.lineTo(x, y);
}
}

private void getHeardPath2(){
// f(x)=sqrt(1-(abs(x)-1)^2)
// h(x)=-2*sqrt(1-0.5*abs(x))
path1=getPathByMathFunction(x -> (float) Math.sqrt(1-Math.pow((Math.abs(x)-1),2)));
Path path2=getPathByMathFunction(x->(float) (-2* Math.sqrt(1-0.5*Math.abs(x))));
path1.moveTo(0,getMeasuredHeight()*1.0f/2);
path1.addPath(path2);
}

private interface MathFunction{
float call(float x);
}

private Path getPathByMathFunction(MathFunction function){
Path path=new Path();
path.moveTo(0,getMeasuredHeight()*1.0f/2);
int px = getMeasuredWidth() / 2;
int py = getMeasuredHeight() / 2;
float scale=getMeasuredWidth()*1.0f/4;
for(float i=-2;i<=2;i+=0.01){
float x=i;
float y= function.call(x);
x*=scale;
y*=scale;
x=px-x;
y=py-y;

path.lineTo(x,y);
}
return path;
}

@Override
protected void dispatchDraw(Canvas canvas) {
int save = canvas.save();
canvas.clipPath(path1);
super.dispatchDraw(canvas);
canvas.restoreToCount(save);
}
}


xml属性

<?xml version="1.0" encoding="utf-8"?>
<resources>


<declare-styleable name="LoveLayout">

<attr name="HeardType">
<!-- 桃心 -->
<enum name="PeachHeart" value="0" />
<!-- 圆一点的心 -->
<enum name="CircularHeart" value="1" />
</attr>
</declare-styleable>


</resources>

数学原理


圆心是按照以下公式实现的,桃心是网上找到的代码改的,没找到公司。
在这里插入图片描述


使用


很简单,当成FrameLayout包裹就行了。
在这里插入图片描述


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

Android稳定性方案浅析

定义: 这里的Android稳定性单指Crash 指标口径: 用户可感知崩溃率,以用户为纬度,即Crash设备数 / 总设备数 精细化单个bug的纬度: bug频次 爆炸半径 影响时长 从崩溃的发生解决整个链路出发: RD编码 --> 线下测试 --...
继续阅读 »

定义: 这里的Android稳定性单指Crash


指标口径: 用户可感知崩溃率,以用户为纬度,即Crash设备数 / 总设备数


精细化单个bug的纬度:



  • bug频次

  • 爆炸半径

  • 影响时长


从崩溃的发生解决整个链路出发: RD编码 --> 线下测试 --> 灰度发版 --> 崩溃发生&数据采集聚类 --> RD修复 --> 版本发布


整体思路


image.png


1、编码阶段


制定代码规范,加强代码Review,引入静态代码检查,完善测试流程等减少问题发生


a、静态代码检查

有时会出现某些低级错误如:divide by zero 导致的crash,使用Lintdetekt进行静态代码检查不失为一种好方案,可以放在代码合入的SA阶段


b、查看是否需要解决警告问题

在我们编码或者改动历史遗留的代码时,AS中存在一些警告⚠️,commit时要根据提示进行review,是否需要每次修复和改动还有待商榷


c、检测SQL质量

参考Matrix SQLite Lint: 按官方最佳实践自动化检测 SQLite 语句的使用质量;(看能否拓展到其他场景)


d、依赖库的检查 (可以跟组件化相关)

采用gradle脚本编译时检查依赖库是否相同,解决不同组件或者不同APP之间SDK版本不一致可能导致的崩溃,避免CI阶段打包失败或者上线后出现异常



  • 新增or改动SDK检测,CI diff 产出依赖树 全功能提测阶段


e、review规范


  • 提交reviewer的质量把控意识;关键代码需要两个人review,+1 +2通过才可以合入

  • 跟流水线CI自动识别核心类文件要求两人review的能力结合起来


2、测试&流水线


提高稳定性的意识,不管多微小的改动都要进行自测!!!


a.单元测试


  • 重点模块的单测能力


b.自动化测试


  • 结合QA补齐自动化测试的能力,覆盖核心场景

  • 跟devops平台能力结合,将LeakCanary等能力跟Monkey结合,自动创建卡片

  • 针对函数接口的异常数据排雷测试(参考juejin.cn/post/702812…)


c.CI/CD


  • 流水线打包效率提升


3、崩溃数据采集&分析


正如RD讲的那样,给我一个崩溃堆栈我就能定位到问题所在,能完整的还原"事故现场"成为重中之重


a.堆栈反混淆

结合各个公司APM平台上传mapping文件以及符号表,每个崩溃数据均展示混淆之前的定位


b.平台堆栈聚类


  • 接入iqiyi开源的XCrash库或者breakpad,上报java和native的崩溃堆栈到Server并聚类展示

  • 接入开源的Sentry库并搭建本地私有化服务,实现崩溃的上报和聚类


c.平台数据状态标识

已修复、Pending、下个版本修复等状态或者备注的标识,每次分析结果留下文字记录


d.分模块上传关键数据

会员模块上传会员信息、支付模块上传订单信息等


4、灰度阶段


a.测试轨道前置小版本提前暴露问题

b.三方SDK降级策略

firebase、广告库等三方SDK升级一定要观察崩溃率变化,并做好降级处理


c.crash率异常熔断机制


  • 灰度过程缺少中间对Crash率异常的评定标准,业务异常的评定标准

  • 整体&每个崩溃数据的量级(该崩溃人数/安装率)记录,设定阈值,每日将两个版本对比骤增或者骤降的数据输出并产出报告


c.上车策略

核心思路是代码改动最小化,预留todo下迭代改,避免造成新的线上crash


5、重点问题解决


I.源码分析

虽然 androidxref.comcs.android.com 都可以在线查阅源码,但这两处的Android版本并不全;android.googlesource.com 这里可以下载到几乎所有版本的源码,本地通过 Sublime 分析源码也十分方便(可以直接显示和跳转到方法的定义&引用位置)。


II.OOM问题

a.大图监控治理


  • 线下监控:通过插件在 mergeResources 任务后,遍历图片资源,搜集超过阈值的图片资源,输出列表

  • 参考NativeBitmap 把应用内存使用的大头(即 Bitmap 的像素占用的内存)转移到 Native 堆;但是可能会导致32位虚拟内存不足

  • 在接口层中将所有被创建出来的 Bitmap 加入一个 WeakHashMap,同时记录创建 Bitmap 的时间、堆栈等信息,然后在适当的时候查看这个 WeakHashMap 看看哪些 Bitmap 仍然存活来判断是否出现 Bitmap 滥用或泄漏。 微信 Android 终端内存优化实践


b.hook pthred

采用bhook,针对pthread_create创建的子线程中发生了信号(SIGSEGV)进行兜底操作;相当于catch native crash,发生crash时重新执行之前的逻辑


c.32位虚拟内存优化


  • Patrons 通过一系列技术手段实现运行期间动态调整Region Space预分配的地址空间

  • mSponge 优化了虚拟机对 LargeObjectSpace 的内存管理策略,间接增加其它内存空间使用上限 (未开源)

  • pthread hook 对 native 线程的默认栈大小进行减半


image.png


d.监控

接入KOOM,进行线下OOM发生时的采样上报


III.native crash


6、防裂化&基础建设


a.版本回顾

崩溃数据自动化采集,以月度或者季度为纬度爬取数据,形成总结


b.崩溃保护和安全模式机制


  • 通过ASM在编译时对四大组件生命周期等关键代码加上try-catch处理

  • 通过一定的策略,针对反复重启导致的崩溃问题,让用户选择继续初始化或者清除数据


c.日志回捞


  • 全埋点用户操作路径辅助分析

  • 参考Logan进行日志回捞系统的建设,方便针对某一用户发生问题后捞回日志分析


d.移动端性能中台

自建集崩溃监控、上报、分析、归因于一体(可以参考Matrix直接建立),可以轻松定位各种线上疑难杂症,更有超详细性能、卡顿、打点等全流程监控处理平台


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

安卓埋点策略+Retrofit上传埋点数据

安卓埋点 在企业级安卓项目中,埋点是一项重要的技术,用于收集用户行为数据以进行分析和改进产品。以下是一个常见的安卓企业级项目开发中使用的埋点方案: 定义埋点事件:首先,确定需要埋点的关键事件,如页面访问、按钮点击、数据提交等。为每个事件定义唯一的标识符或名...
继续阅读 »

安卓埋点


在企业级安卓项目中,埋点是一项重要的技术,用于收集用户行为数据以进行分析和改进产品。以下是一个常见的安卓企业级项目开发中使用的埋点方案:




  1. 定义埋点事件:首先,确定需要埋点的关键事件,如页面访问、按钮点击、数据提交等。为每个事件定义唯一的标识符或名称。




  2. 埋点代码插入:在关键事件的代码位置插入埋点代码,以便在事件发生时触发埋点记录。可以通过在代码中手动插入埋点代码或使用 AOP(面向切面编程)等技术自动插入埋点代码。




  3. 数据收集和存储:在埋点代码中,收集相关的事件数据,如事件类型、时间戳、页面名称、按钮名称等。将这些数据存储到本地数据库或发送到服务器进行存储。




  4. 数据上传和分析:定期将本地存储的埋点数据上传到服务器端进行分析。可以使用网络请求库发送数据到服务器,并在服务器端使用数据分析工具进行处理和分析。




  5. 数据展示和可视化:通过数据分析工具,将埋点数据进行可视化展示,生成报表、图表等形式的数据分析结果,以便开发团队或业务团队进行数据分析和决策。




  6. 隐私和合规性:在进行埋点时,要确保遵守隐私保护和数据合规性的相关法规和政策。确保用户数据的安全和保密,并进行必要的用户授权和通知。




  7. 埋点策略优化:根据实际业务需求和数据分析结果,优化埋点策略,增加或调整关键事件的埋点,提高数据的准确性和有用性。




需要注意的是,具体的埋点方案可能因项目需求、技术架构和团队实际情况而有所不同。因此,在实施埋点方案时,应根据项目的具体情况进行定制化开发,并考虑到性能、稳定性、安全性和用户体验等因素。


埋点数据和上传埋点数据代码示例


定义埋点事件的工具类,包含事件的标识符、名称、属性等信息

public class TrackEventUtils {
public static final String EVENT_PAGE_VIEW = "page_view";
public static final String EVENT_BUTTON_CLICK = "button_click";
// 其他事件定义...

// 获取页面访问事件
public static TrackEvent getPageViewEvent(String pageName) {
TrackEvent event = new TrackEvent(EVENT_PAGE_VIEW);
event.addProperty("page_name", pageName);
// 其他属性...
return event;
}

// 获取按钮点击事件
public static TrackEvent getButtonClickEvent(String buttonName) {
TrackEvent event = new TrackEvent(EVENT_BUTTON_CLICK);
event.addProperty("button_name", buttonName);
// 其他属性...
return event;
}

// 其他事件获取方法...
}


定义埋点事件的实体类,包含事件类型、属性等信息

public class TrackEvent {
private String eventType;
private Map<String, Object> properties;

public TrackEvent(String eventType) {
this.eventType = eventType;
this.properties = new HashMap<>();
}

public String getEventType() {
return eventType;
}

public void addProperty(String key, Object value) {
properties.put(key, value);
}

public Map<String, Object> getProperties() {
return properties;
}
}


使用Retrofit框架上传埋点数据到对应路径


1.添加 Retrofit 依赖到项目的 build.gradle 文件中:

implementation 'com.squareup.retrofit2:retrofit:2.x.x'
implementation 'com.squareup.retrofit2:converter-gson:2.x.x' // 如果要使用 Gson 解析器


2.创建 Retrofit 实例并定义 API 接口:

public interface TrackApiService {
@POST("/track")
Call<Void> sendTrackEvent(@Body TrackEvent event);
}


3.修改 TrackManager 类,使用 Retrofit 发送网络请求:

public class TrackManager {
private static final String API_ENDPOINT = "https://your-api-endpoint.com";
private static TrackManager instance;
private Context context;
private TrackApiService apiService;

private TrackManager(Context context) {
this.context = context.getApplicationContext();

// 创建 Retrofit 实例
Retrofit retrofit = new Retrofit.Builder()
.baseUrl(API_ENDPOINT)
.addConverterFactory(GsonConverterFactory.create()) // 使用 Gson 解析器
.build();

// 创建 API Service
apiService = retrofit.create(TrackApiService.class);
}

public static synchronized TrackManager getInstance(Context context) {
if (instance == null) {
instance = new TrackManager(context);
}
return instance;
}

public void trackEvent(TrackEvent event) {
// 发送网络请求
Call<Void> call = apiService.sendTrackEvent(event);
call.enqueue(new Callback<Void>() {
@Override
public void onResponse(Call<Void> call, Response<Void> response) {
// 处理服务器响应...
}

@Override
public void onFailure(Call<Void> call, Throwable t) {
// 处理请求失败...
}
});
}
}


4.解释下上面的代码中有关Retrofit中的注解和上面我们定义的接口TrackApiService
在 Retrofit 中,TrackApiService 是一个接口,用于定义网络请求的方法。@POST("/track") 是一个注解,表示发送 POST 请求到指定的路径 "/track"。


@Body TrackEvent event 是另一个注解,用于指定请求体的内容。它告诉 Retrofit 将 TrackEvent 对象作为请求体发送给服务器。


具体解释如下:




  • @POST("/track"):表示将使用 POST 方法发送请求到路径 "/track"。这个路径是你的 API 后端定义的接收埋点事件的路径。




  • Call<Void>:表示 Retrofit 将返回一个 Call 对象,用于异步执行网络请求并处理响应。Void 表示响应的主体内容为空。




  • sendTrackEvent(@Body TrackEvent event):这是一个方法定义,用于发送埋点事件。@Body 注解表示将 TrackEvent 对象作为请求体发送。TrackEvent 是你定义的类,包含了发送给服务器的埋点事件数据。




综合起来,TrackApiService 接口中的 sendTrackEvent 方法定义了一个发送埋点事件的请求,通过 POST 方法发送到指定路径,并将 TrackEvent 对象作为请求体发送给服务器。


你可以根据实际需求修改这个接口,添加其他请求方法和参数,以适应你的埋点需求。




在 Retrofit 中,@Body 注解用于将对象作为请求体发送给服务器。这意味着你可以将任何 Java 类的实例作为请求体发送出去,不限于特定的类或数据类型。


当你使用 @Body 注解时,Retrofit 将会自动将指定的对象序列化为请求体的格式,例如 JSON 或者其他格式。然后,它将使用适当的请求头信息将请求发送到服务器。


因此,你可以创建自己的 Java 类,用于表示需要发送的数据,并将其作为请求体发送给服务器。这样,你可以根据实际需求定义和发送不同类型的数据。


请确保在使用 @Body 注解时,服务器能够正确地解析和处理请求体的格式。通常,你需要在服务器端进行相应的处理和解析,以确保能够正确地接收和处理你发送的 Java 对象。


注:,Retrofit 会动态地创建接口的实现类,你无需手动编写实现类。当你使用 Retrofit 创建接口的实例时,它会在运行时生成一个代理类来处理实际的网络请求。因此,你不需要手动实现 TrackApiService 接口中的方法。


使用异步或者同步请求


使用 enqueue 方法是一种常见的异步执行网络请求的方式,它会在后台线程执行网络请求,并在请求完成后回调相应的方法。


Retrofit 支持同步和异步的网络请求方式。如果你希望使用同步请求,可以使用 execute 方法来执行请求,但需要注意的是,在 Android 主线程上执行网络请求会导致阻塞,可能会引起 ANR(Application Not Responding)错误,因此建议在后台线程中执行同步请求。


关于接口是异步还是同步的,一般情况下是由接口的定义和服务端的实现决定的。通常,网络请求都会以异步方式执行,以避免阻塞主线程。在 Retrofit 中,默认情况下,接口的方法会被当作异步请求进行处理,需要使用 enqueue 方法来执行异步请求。


如果你想要执行同步请求,可以在 Retrofit 创建时设置合适的执行器(Executor),以控制请求的执行方式。例如,可以使用 OkHttp 客户端来创建 Retrofit 实例,并设置自定义的执行器来执行同步请求。

// 创建 OkHttpClient 实例
OkHttpClient okHttpClient = new OkHttpClient.Builder()
.build();

// 创建 Retrofit 实例,并指定 OkHttp 客户端
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://api.example.com")
.addConverterFactory(GsonConverterFactory.create())
.client(okHttpClient) // 设置自定义的 OkHttpClient
.build();

// 创建 TrackApiService 实例
TrackApiService trackApiService = retrofit.create(TrackApiService.class);

// 创建同步请求执行器
ExecutorService executor = Executors.newSingleThreadExecutor();

// 执行同步请求
try {
Response<Void> response = executor.submit(() -> trackApiService.sendTrackEvent(event)).get();
// 请求成功的处理逻辑
} catch (ExecutionException | InterruptedException e) {
// 请求失败的处理逻辑
}

// 关闭执行器
executor.shutdown();


在这个示例中,我们使用 OkHttp 客户端创建了一个自定义的 OkHttpClient 实例,并将其传递给 Retrofit 的构建器。然后,我们创建了一个 ExecutorService 实例,并使用 submit 方法执行网络请求。通过调用 get 方法获取 Response 对象,我们可以同步地获取请求的结果。


需要注意的是,同步请求仍然需要在合适的线程中执行,以避免阻塞主线程。在这个示例中,我们使用了单线程的执行器来执行同步请求,并在请求完成后关闭执行器。


综上所述,Retrofit 提供了异步和同步两种方式来执行网络请求,具体使用哪种方式取决于你的需求和服务器端的实现。一般来说,推荐使用异步请求以避免阻塞主线程,除非你确切地知道需要执行同步请求,并且在合适的线程上执行它们。
如果使用的是同步请求,即使使用了execute方法,也要手动开启子线程来调用execute方法,若是异步请求,则使用Retrofit的enqueue方法即可,无需自己手动开启子线程。


服务器如何决定接口是异步请求或是同步请求


1.在服务端,决定接口是同步请求还是异步请求是由服务端的实现逻辑来决定的。
通常情况下,服务端会为每个接口定义好其执行方式,包括是同步还是异步。这通常是通过服务端框架或编程语言提供的特定机制来实现的。
例如,在某些服务器框架中,可以使用异步处理机制(如基于回调的异步编程、Future/Promise、协程等)来处理异步请求。而对于同步请求,则可能直接在请求处理方法中执行阻塞操作。
因此,具体接口是同步还是异步请求,你需要参考服务端接口文档或与服务端开发人员进行沟通,了解其设计和实现细节。根据服务端的要求,你可以相应地选择使用 Retrofit 的 enqueue 方法或 execute 方法来发送请求。


2.在服务端的代码中,决定接口是同步还是异步的方式取决于所使用的服务器框架和编程语言。以下是一些常见的示例代码,展示了如何在不同的环境中定义同步和异步接口:


a.Node.js(使用 Express 框架):

// 异步接口
app.get('/async', (req, res) => {
someAsyncOperation((data) => {
res.send(data);
});
});

// 同步接口
app.get('/sync', (req, res) => {
const result = someSyncOperation();
res.send(result);
});

b.Java(使用 Spring 框架):

// 异步接口
@GetMapping("/async")
public CompletableFuture<String> asyncEndpoint() {
return CompletableFuture.supplyAsync(() -> {
// 异步操作
return "Async response";
});
}

// 同步接口
@GetMapping("/sync")
public String syncEndpoint() {
// 同步操作
return "Sync response";
}

这些示例只是简单的展示了如何在不同环境中定义同步和异步接口。实际上,具体的实现方式取决于所使用的服务器框架和编程语言的特性和机制。因此,你需要根据你所使用的具体服务器框架和编程语言的文档,了解如何定义和处理同步和异步接口。


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

Android自定义一个车牌字母选择键盘

今天的内容大致如下: 1、最终实现效果及简单分析 2、设置属性,制定可扩展效果 3、部分源码剖析 4、开源地址及使用总结 一、最终实现效果及简单分析 以上就是本篇文章最终要实现的效果,和省份简称键盘不同的是,数据源上和边距有所差别之外,其他的实现方式均是一样...
继续阅读 »

今天的内容大致如下:


1、最终实现效果及简单分析


2、设置属性,制定可扩展效果


3、部分源码剖析


4、开源地址及使用总结


一、最终实现效果及简单分析



以上就是本篇文章最终要实现的效果,和省份简称键盘不同的是,数据源上和边距有所差别之外,其他的实现方式均是一样的,采用外部垂直LinearLayout,内部多个横向的LinearLayout的搭配方式。


需要注意的是,英文和数字键盘,默认状态下,顶部的数字是禁止的,也就是输入完地区代码之后,数字的禁止状态才会释放;由于距离左右的边距不同,其在数据源的判断上也会有不同,这个也是需要注意的。


二、设置属性,制定可扩展效果


其相关属性和上篇的省份键盘基本上没有太大的出入,主要就是动态化设置,设置一些,文字的背景,大小,颜色以及格子之间的编辑等,大概罗列了以下属性:

属性类型概述
ek_backgroundcolor整体的背景颜色
ek_rect_spacingdimension格子的边距
ek_rect_heightdimension格子的高度
ek_rect_margin_topdimension格子的距离上边
ek_margin_left_rightdimension左右距离
ek_margin_topdimension上边距离
ek_margin_bottomdimension下边距离
ek_rect_backgroundreference格子的背景
ek_rect_select_backgroundreference格子选择后的背景
ek_rect_text_sizedimension格子的文字大小
ek_rect_text_colorcolor格子的文字颜色
ek_rect_select_text_colorcolor格子的文字选中颜色
ek_is_show_completeboolean是否显示完成按钮
ek_complete_text_sizedimension完成按钮文字大小
ek_complete_text_colorcolor完成按钮文字颜色
ek_complete_textstring完成按钮文字内容
ek_complete_margin_topdimension完成按钮距离上边
ek_complete_margin_bottomdimension完成按钮距离下边
ek_complete_margin_rightdimension完成按钮距离右边
ek_other_lines_margindimension其他行边距
ek_is_num_prohibitboolean数字是否禁止
ek_text_prohibit_colorcolor数字禁止颜色
ek_text_click_effectboolean是否触发点击效果,true点击后背景消失,false不消失



设置回调函数



























方法概述
keyboardContent获取点击的省份简称简称信息
keyboardDelete删除省份简称简称信息
keyboardComplete键盘点击完成
openProhibit打开禁止(使领学港澳),使其可以点击

三、部分源码剖析


这里只贴出部分的关键性代码,整体的代码,大家滑到底部查看源码地址即可。


定义字母和数字数组

   private val mEnglishList = arrayListOf(
"1", "2", "3", "4", "5", "6", "7", "8", "9", "0",
"Q", "W", "E", "R", "T", "Y", "U", "O", "P",
"A", "S", "D", "F", "G", "H", "J", "K", "L",
"Z", "X", "C", "V", "B", "N", "M"
)

定义遍历数字和字母


由于在数据源上使用的是同一个,那么需要做截取分别进行遍历,便于控制左右的边距和本身的格子大小。

 //遍历数字
eachData(mEnglishList.subList(0, 10), mLength, true)
//遍历字母
eachData(mEnglishList.subList(10, mEnglishList.size), mLength - 1, false)
//追加最后一个删除按钮View,动态计算宽度
addEndView(mLineLayout)

遍历数据


遍历数据的逻辑和上篇保持一致,当和定义的长度取模为0时,就需要换行,换行就是重新创建一个水平的LinearLayout,添加至垂直的LinearLayout之中,需要做判断的是,左右的边距。

/**
* AUTHOR:AbnerMing
* INTRODUCE:遍历数据
*/
private fun eachData(
list: List,
len: Int,
isNumber: Boolean = false
) {
list.forEachIndexed { index, s ->
if (index % len == 0) {
//重新创建,并添加View
mLineLayout = createLinearLayout()
mLineLayout?.weightSum = len.toFloat()
addView(mLineLayout)
val params = mLineLayout?.layoutParams as LayoutParams
params.apply {
topMargin = mRectMarginTop.toInt()
height = mRectHeight.toInt()
if (isNumber) {
//是数字
leftMargin = mMarginLeftRight.toInt()
rightMargin = mMarginLeftRight.toInt() - mSpacing.toInt()
} else {
//是字母
leftMargin = mOtherLinesMargin.toInt()
rightMargin = mOtherLinesMargin.toInt() - mSpacing.toInt()
}
mLineLayout?.layoutParams = this
}
}

//创建文字视图
val textView = TextView(context).apply {
text = s
//设置文字的属性
textSize = px2sp(mRectTextSize)
//禁止
if (isNumber) {
//是数字
if (mNumProhibit) {
setTextColor(mRectTextColor)
} else {
setTextColor(mNumProhibitColor)
}
} else {
setTextColor(mRectTextColor)
}
setBackgroundResource(mRectBackGround)
gravity = Gravity.CENTER
setOnClickListener {
//每个格子的点击事件
if (isNumber && !mNumProhibit) {
//如果是数字,根据规则暂时不触发点击
return@setOnClickListener
}
changeTextViewState(this)
}
}
//是数字
if (isNumber) {
mTempTextViewList.add(textView)
}
addRectView(textView, mLineLayout, 1f)
}
}

添加视图


设置每个格子的宽高和权重。

 /**
* AUTHOR:AbnerMing
* INTRODUCE:追加视图
*/
private fun addRectView(view: View, layout: LinearLayout?, w: Float) {
layout?.addView(view)
val textParams = view.layoutParams as LayoutParams
textParams.apply {
weight = w
width = 0
height = LayoutParams.MATCH_PARENT
//每行的最后一个
rightMargin = mSpacing.toInt()
view.layoutParams = this
}

}

至于最后一个删除按钮,也需要动态的计算其本身的宽高,基本上和上篇一致,就不过多赘述了。


四、开源地址及使用总结


开源地址:github.com/AbnerMing88…


关于如何使用,有两种方式,一种是下载源码,直接把源码复制出来,二是可以使用以下的远程Maven依赖方式。


Maven具体调用


1、在你的根项目下的build.gradle文件下,引入maven。

allprojects {
repositories {
maven { url "https://gitee.com/AbnerAndroid/almighty/raw/master" }
}
}

2、在你需要使用的Module中build.gradle文件下,引入依赖。

dependencies {
implementation 'com.vip:board:1.0.0'
}


代码使用


android:layout_width="match_parent"
android:layout_height="wrap_content" />

总结


属性配置了有很多,可以实现多种自定义的相关效果,大家可以查找第二项中的属性介绍,进行自定义配置,还是那句话,本身的实现方式有很多种,本篇只是其中的一个简单的案例,仅供大家作为一个参考。


自定义英文和数字键盘,大家有没有发现了少了一个字母,为什么会没有这个字母呢?你知道原因吗?


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

安卓滴滴路由框架DRouter原理浅析

前言 最近的一个新项目使用了Clean Architecture+模块化+MVVM架构,将首页每个tab对应的功能都放到单独的模块且不相互依赖,这时就有了模块间页面跳转的问题,经过一番研究选择了滴滴的DRouter,因为其出色的性能、灵活的组件拆分,更重要的是...
继续阅读 »

前言


最近的一个新项目使用了Clean Architecture+模块化+MVVM架构,将首页每个tab对应的功能都放到单独的模块且不相互依赖,这时就有了模块间页面跳转的问题,经过一番研究选择了滴滴的DRouter,因为其出色的性能、灵活的组件拆分,更重要的是生成路由表时支持插件增量编译、多线程扫描,运行时异步加载路由表,支持回调式ActivityResult,比ARouter好太多。本着用一个新框架,只会用还不够的原则,我决定去了解一下框架的原理,并给自己制定了以下几个问题:


1、框架的设计分层是什么样的?

2、它是如何生成路由表的?

3、它是如何加载路由表的?

4、相比于ARouter如何提高了性能?


阅读官方文档


相比于直接一头扎进源码,先阅读官方的文档总是没错的,官方给了一篇介绍的文章,写得非常好,基本回答了我以上的所有问题。


滴滴开源DRouter:一款高效的Android路由框架


首先在介绍DRouter的亮点部分得到了问题2、3、4的答案。



路由表在编译期通过插件动态生成。插件会启动多线程同时异步处理所有的组件;增量扫描功能可以帮助开发者在第二次编译时,只对修改过的代码进行处理,极大地缩短路由表生成的时间。



在编译器使用gradle插件配合transform扫描所有的类,生成路由表,并且支持增量扫描,回答了问题2。



另外框架初始化的时候启动子线程去加载路由表,不阻塞主线程的执行,尽其所能提高效率。



回答了问题3。



加载路由表、实例化路由、以及跨进程命令到达服务端后的分发这些常规应该使用反射的场景,使用预占位或动态生成代码来替换成java的new创建和显式方式执行,最大限度的去避免反射执行,提高性能。



回答了问题4,通过减少使用反射提升了性能。


在原理和架构章节处给了一张架构的设计图:


架构设计



整体架构分三层,自下而上是数据流层、组件层、开放接口层。


数据流层是DRouter最重要的核心模块,这里承载着插件生成的路由表、路由元素、动态注册、以及跨进程功能相关的序列化数据流。所有的路由流转都会从这里取得对应的数据,进而流向正确的目标。



RouterPlugin和MetaLoader负责生成路由表,路由元素指的是RouterMeta,存放scheme/host/path等信息。



组件层,核心的路由分发、拦截器、生命周期、异步暂存和监控、ServiceLoader、多维过滤、Fragment路由,以及跨进程命令打包等。



开放接口层则是使用时接触到的一些类,API设计得也很简单易用,DRouter类和Request类分别只有75和121行代码。


问题1得到解答,到此处也对整个框架有了一个整体的认识。


阅读源码


1.初始化流程


调用DRouter.init(app)后的时序图如下:


截屏2023-06-02 16.54.46.png


默认是在子线程实现路由表加载,不影响主线程。

    public static void checkAndLoad(final String app, boolean async) {
if (!loadRecord.contains(app)) {
// 双重校验锁
synchronized (RouterStore.class) {
if (!loadRecord.contains(app)) {
loadRecord.add(app);
if (!async) {
Log.d(RouterLogger.CORE_TAG, "DRouter start load router table sync");
load(app);
} else {
new Thread("drouter-table-thread") {
@Override
public void run() {
Log.d(RouterLogger.CORE_TAG, "DRouter start load router table in drouter-table-thread");
load(app);
}
}.start();
}
}
}
}
}

最终走到了RouterLoader的load方法来加载路由表到一个map中,仔细看它的引入路径是com.didi.drouter.loader.host.RouterLoader,是不存在于源码中的,因为它是编译的时候生成的,位置位于app/build/intermediates/transforms/DRouter/dev/debug/../com/didi/drouter/loader/host/RouterLoader。

public class RouterLoader extends MetaLoader {
@Override
public void load(Map var1) {
var1.put("@@$$/browse/BrowseActivity", RouterMeta.build(RouterMeta.ACTIVITY).assembleRouter("", "", "/browse/BrowseActivity", "com.example.demo.browse.BrowseActivity", (IRouterProxy)null, (Class[])null, (String[])null, 0, 0, false));
}

public RouterLoader() {
}
}

public abstract class MetaLoader {

public abstract void load(Map<?, ?> data);

// for regex router
protected void put(String uri, RouterMeta meta, Map<String, Map<String, RouterMeta>> data) {
Map<String, RouterMeta> map = data.get(RouterStore.REGEX_ROUTER);
if (map == null) {
map = new ConcurrentHashMap<>();
data.put(RouterStore.REGEX_ROUTER, map);
}
map.put(uri, meta);
}

// for service
protected void put(Class<?> clz, RouterMeta meta, Map<Class<?>, Set<RouterMeta>> data) {
Set<RouterMeta> set = data.get(clz);
if (set == null) {
set = Collections.newSetFromMap(new ConcurrentHashMap<RouterMeta, Boolean>());
data.put(clz, set);
}
set.add(meta);
}
}

不难猜出其是在编译期加了一个transform,生成RouterLoader类时加入了load方法的具体实现,具体来说是javaassit API+Gradle Transform,所以去看看drouter-plugin在编译期做了什么。


2.编译期transform


直接看时序图。


截屏2023-06-02 17.56.36.png


创建了一个RouterPlugin,并且注册了一个Gradle Transform。

class RouterPlugin implements Plugin<Project> {

@Override
void apply(Project project) {
...
project.android.registerTransform(new TransformProxy(project))
}
}

class TransformProxy extends Transform {
@Override
void transform(TransformInvocation invocation) throws TransformException, InterruptedException, IOException {
String pluginVersion = ProxyUtil.getPluginVersion(invocation)
if (pluginVersion != null) {
...

if (pluginJar.exists()) {
URLClassLoader newLoader = new URLClassLoader([pluginJar.toURI().toURL()] as URL[], getClass().classLoader)
Class<?> transformClass = newLoader.loadClass("com.didi.drouter.plugin.RouterTransform")
ClassLoader threadLoader = Thread.currentThread().getContextClassLoader()
// 1.设置URLClassLoader
Thread.currentThread().setContextClassLoader(newLoader)
Constructor constructor = transformClass.getConstructor(Project.class)
// 2.反射创建一个RouterTransform
Transform transform = (Transform) constructor.newInstance(project)
transform.transform(invocation)
Thread.currentThread().setContextClassLoader(threadLoader)
return
} else {
ProxyUtil.Logger.e("Error: there is no drouter-plugin jar")
}
}
}
}

注释2处反射创建一个com.didi.drouter.plugin.RouterTransform对象,并执行其transform方法,此处真正处理transform逻辑,它的位置位于drouter-plugin模块。

class RouterTransform extends Transform {
@Override
void transform(TransformInvocation invocation) throws TransformException, InterruptedException, IOException {
...
// 1.创建一个DRouterTable目录
File dest = invocation.outputProvider.getContentLocation("DRouterTable", TransformManager.CONTENT_CLASS,
ImmutableSet.of(QualifiedContent.Scope.PROJECT), Format.DIRECTORY)
// 2.执行RouterTask
(new RouterTask(project, compilePath, cachePathSet, useCache, dest, tmpDir, setting, isWindow)).run()
FileUtils.writeLines(cacheFile, cachePathSet)
Logger.v("Link: https://github.com/didi/DRouter")
Logger.v("DRouterTask done, time used: " + (System.currentTimeMillis() - timeStart) / 1000f + "s")
}
}

注释2处new了一个RouterTask对象,并执行其run方法,之后的log输出就是平时编译能看到的信息,表示transform的耗时。

public class RouterTask {
void run() {
StoreUtil.clear();
JarUtils.printVersion(project, compileClassPath);
pool = new ClassPool();
// 1.创建ClassClassify
classClassify = new ClassClassify(pool, setting);
startExecute();
}

private void startExecute() {
try {
...
// 2.执行ClassClassify的generatorRouter
classClassify.generatorRouter(routerDir);
Logger.d("generator router table used: " + (System.currentTimeMillis() - timeStart) + "ms");
Logger.v("scan class size: " + count.get() + " | router class size: " + cachePathSet.size());
} catch (Exception e) {
JarUtils.check(e);
throw new GradleException("Could not generate d_router table\n" + e.getMessage(), e);
} finally {
executor.shutdown();
FileUtils.deleteQuietly(wTmpDir);
}
}
}

重点在于ClassClassify这个类,其generatorRouter方法便是最终处理生成路由表的逻辑。

public class ClassClassify {
private List<AbsRouterCollect> classifies = new ArrayList<>();

public ClassClassify(ClassPool pool, RouterSetting.Parse setting) {
classifies.add(new RouterCollect(pool, setting));
classifies.add(new ServiceCollect(pool, setting));
classifies.add(new InterceptorCollect(pool, setting));
}

public void generatorRouter(File routerDir) throws Exception {
for (int i = 0; i < classifies.size(); i++) {
AbsRouterCollect cf = classifies.get(i);
cf.generate(routerDir);
}
}
}

构造函数处添加了RouterCollect/ServiceCollect/InterceptorCollect,最终执行的是他们的generate方法,分别处理路由表、service、拦截器,我们只看路由表的。

class RouterCollect extends AbsRouterCollect {
@Override
public void generate(File routerDir) throws Exception {
// 1.创建RouterLoader类
CtClass ctClass = pool.makeClass(getPackageName() + ".RouterLoader");
CtClass superClass = pool.get("com.didi.drouter.store.MetaLoader");
ctClass.setSuperclass(superClass);

StringBuilder builder = new StringBuilder();
builder.append("public void load(java.util.Map data) {\n");
for (CtClass routerCc : routerClass.values()) {
try {
// 处理注解、class类型等逻辑
...
StringBuilder metaBuilder = new StringBuilder();
metaBuilder.append("com.didi.drouter.store.RouterMeta.build(");
metaBuilder.append(type);
metaBuilder.append(").assembleRouter(");
metaBuilder.append("\"").append(schemeValue).append("\"");
metaBuilder.append(",");
metaBuilder.append("\"").append(hostValue).append("\"");
metaBuilder.append(",");
metaBuilder.append("\"").append(pathValue).append("\"");
metaBuilder.append(",");
if ("com.didi.drouter.store.RouterMeta.ACTIVITY".equals(type)) {
if (!setting.isUseActivityRouterClass()) {
metaBuilder.append("\"").append(routerCc.getName()).append("\"");
} else {
metaBuilder.append(routerCc.getName()).append(".class");
}
} else {
metaBuilder.append(routerCc.getName()).append(".class");
}
metaBuilder.append(", ");
...
metaBuilder.append(proxyCc != null ? "new " + proxyCc.getName() + "()" : "null");
metaBuilder.append(", ");
metaBuilder.append(interceptorClass != null ? interceptorClass.toString() : "null");
metaBuilder.append(", ");
metaBuilder.append(interceptorName != null ? interceptorName.toString() : "null");
metaBuilder.append(", ");
metaBuilder.append(thread);
metaBuilder.append(", ");
metaBuilder.append(priority);
metaBuilder.append(", ");
metaBuilder.append(hold);
metaBuilder.append(")");
...
if (isAnyRegex) {
// 2. 插入路由表
items.add(" put(\"" + uri + "\", " + metaBuilder + ", data); \n");
//builder.append(" put(\"").append(uri).append("\", ").append(metaBuilder).append(", data); \n");
} else {
items.add(" data.put(\"" + uri + "\", " + metaBuilder + "); \n");
//builder.append(" data.put(\"").append(uri).append("\", ").append(metaBuilder).append("); \n");
}
} catch (Exception e) {
e.printStackTrace();
}
Collections.sort(items);
for (String item : items) {
builder.append(item);
}
builder.append("}");

Logger.d("\nclass RouterLoader" + "\n" + builder.toString());
// 3.生成代码
generatorClass(routerDir, ctClass, builder.toString());
}
}
}

此处逻辑比较多,但总体是清晰的,处理完注解和类型的判断,获取路由的信息,构造将要插入的代码,最后统一在父类AbsRouterCollect的generatorClass处理load方法的生成,此时编译器的工作就完成了。


ARouter也提供了arouter-register插件,同是在编译期生成路由表,不同的是在生成代码时,ARouter使用的是ASM,DRouter使用Javassist,查了一下资料,ASM性能比Javassist更好,但更难上手,需要懂字节码知识,Javassist在复杂的字节码级操作上提供了更高级别的抽象层,因此实现起来更容易、更快,只需要懂很少的字节码知识,它使用反射机制。


3.运行期加载路由表


重新贴一下加载路由表的load方法。

public class RouterLoader extends MetaLoader {
@Override
public void load(Map var1) {
var1.put("@@$$/browse/BrowseActivity", RouterMeta.build(RouterMeta.ACTIVITY).assembleRouter("", "", "/browse/BrowseActivity", "com.example.demo.browse.BrowseActivity", (IRouterProxy)null, (Class[])null, (String[])null, 0, 0, false));
}

public RouterLoader() {
}
}

看下RouteMeta的build方法。

public static RouterMeta build(int routerType) {
return new RouterMeta(routerType);
}

可见是直接new的一个路由类,这与ARouter直接通过反射创建路由类不同,性能更好。

private static void register(String className) {
if (!TextUtils.isEmpty(className)) {
try {
// 1.反射创建路由类
Class<?> clazz = Class.forName(className);
Object obj = clazz.getConstructor().newInstance();
if (obj instanceof IRouteRoot) {
registerRouteRoot((IRouteRoot) obj);
} else if (obj instanceof IProviderGroup) {
registerProvider((IProviderGroup) obj);
} else if (obj instanceof IInterceptorGroup) {
registerInterceptor((IInterceptorGroup) obj);
} else {
logger.info(TAG, "register failed, class name: " + className
+ " should implements one of IRouteRoot/IProviderGroup/IInterceptorGroup.");
}
} catch (Exception e) {
logger.error(TAG,"register class error:" + className, e);
}
}
}

4.总结


本文分析了DRouter路由部分的原理,其在编译器使用Gradle Transform和Javassist生成路由表,运行时new路由类,异步初始化加载路由表,实现了高性能。


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