注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

🦊【低代码相关】表单联动新思路 摆脱if-else的地狱🦄

在低代码解决方案中,表单是一大类低代码搭建解决的问题。表单作为用户信息采集的手段存在于各类应用中,无论是面向C端的手机页面,还是面向B端的运营平台,甚至低代码平台本身都会存在表单这种形式的交互。 表单本身并不复杂,各个组件库,如antd,element ui等...
继续阅读 »


在低代码解决方案中,表单是一大类低代码搭建解决的问题。表单作为用户信息采集的手段存在于各类应用中,无论是面向C端的手机页面,还是面向B端的运营平台,甚至低代码平台本身都会存在表单这种形式的交互。


表单本身并不复杂,各个组件库,如antd,element ui等都提供了表单组件,能够将一组输入控件组织成一个表单,并且都提供了简单的校验功能,能够检查单控件类似非空、输入长度、正则匹配之类的问题,也可以针对类似多字段情形自己定制复杂校验逻辑。然而,对于表单项之间存在联动的情形,比如一些字段的出现/消失依赖于其他字段的情形,或者一些字段填写以后其他字段的选项应当变更,这些情形通用的组件库就没有提供解决方案,而是由开发各显神通了。


表单联动最简单的方式自然是if-else了,对于联动项较少的情形,简单一个if-else就能够实现我们所需要的功能。然而,在复杂的表单上if-else层层嵌套下来代码的可读性会变差,下次开发的时候看着长长的一串if-else,每个人都会超级头痛。更重要的是,采用if-else的维护方式,在表单渲染部分需要一组对应的逻辑,在表单提交校验的时候又需要一组对应的逻辑,两边的逻辑大量都是重复的,但一组是嵌套在视图里,一组是针对表单数据。


在程序语言中,解决if-else的方法是采用模式匹配,在表单联动这个主题上,这个方式也是可行的嘛?让我们就着手试试吧!


模式定义


我们的目标是尽可能多地去掉if-else。表单联动主要是基于表单的值,那模式自然是基于值来定义的。


举个🌰:假设我们需要开发一个会议预订系统,支持单次和循环会议,那么表单的模式有那几种呢?


系统最后的效果就类似Outlook:


image.png



  1. 单次会议,需要会议日期(event_date)、开始时间(event_start)、结束时间(event_end)、主题(subject)、参与者(attenders)、地址(location)



  2. 循环会议,一样需要开始时间(event_start),结束时间(event_end),主题(subject)、参与者(attenders),地址(location),还需要循环的间隔(recurrence_interval)和循环的起始(recurrence_start)、结束日期(recurrence_end)。而循环又可以分为以下几种子模式:


    1. 按日循环
    2. 按周循环,额外需要周几举行会议(recurrence_weekdays)
    3. 按月循环,额外需要几号举行会议(recurrence_date)
    4. 按年循环,额外需要几月几号举行会议(recurrence_month,recurrence_date)


这里除了地址和循环结束日期以外的所有字段都是必选的,循环的间隔需要是一个正整数。


可以看到,这里一共是5种模式。区分模式主要是两个字段——是否循环(is_recurrence)和循环单位(recurrence_unit),并且都是值的唯一匹配,因此我们可以用简单用JSON的方式定义模式:


// 单次会议
{
"is_recurrence": false
}
// 按日循环
{
"is_recurrence": true,
"recurrence_unit": "day"
}
// 按周循环
{
"is_recurrence": true,
"recurrence_unit": "week"
}
// 按月循环
{
"is_recurrence": true,
"recurrence_unit": "month"
}
// 按年循环
{
"is_recurrence": true,
"recurrence_unit": "year"
}

对于更复杂的情况来说,模式的区分可能就不是单一值匹配了。例如我们需要做一个医院急诊管理系统,需要根据用户输入的体温来获取更多信息,体温在38.5度上下需要有不同的反馈,这样的情况就没法简单用JSON来表达,而是需要使用function,但整体的逻辑是一致的,都是将可能的情况定义为模式,并将表单状态与模式相关联。


表单定义


定义完模式后我们需要定义对应的表单。


在我们的会议预订应用中,总共有以下几个字段:


  • event_date
  • event_start
  • event_end
  • subject
  • attenders
  • location
  • is_recurrence
  • recurrence_interval
  • recurrence_unit
  • recurrence_start
  • recurrence_end
  • recurrence_weekdays
  • recurrence_month
  • recurrence_date

在这个场景下,每个字段展示的内容和校验逻辑在5种模式下都是一致的,需要根据模式联动的点只在于每个字段是否展示,整个表单数据的校验逻辑其实所有展示字段的单字段校验逻辑。因此,我们将每个字段通过以下类型表示:


type FormField<T> = {
/**
* 表单展示
*/

render: (value: T | undefined) => ReactNode;
/**
* 校验规则
*/

rules: {
validates: (value: T | undefined) => boolean;
errorMessage: boolean;
}[];
};

所有字段根据字段key通过一个map进行存储与索引。同时,将每个模式下应该展示的字段以字段key的数组的方式进行存储:


/** 所有字段的存储,这里省略实现 */
declare const formFields: Record<keyof Schedule, FormField<any>>;
type Pattern = {
pattern_indicator: Partial<Schedule>;
fields: (keyof Schedule)[];
};
/** 每个模式下应该展示的字段映射 */
const patterns: Pattern[] = [
{
pattern_indicator: { is_recurrence: false },
fields: [
"event_date",
"event_start",
"event_end",
"subject",
"attenders",
"location",
"is_recurrence",
],
},
{
pattern_indicator: { is_recurrence: true, recurrence_unit: "day" },
fields: [
"event_start",
"event_end",
"subject",
"attenders",
"location",
"is_recurrence",
"recurrence_interval",
"recurrence_unit",
"recurrence_start",
"recurrence_end",
],
},
{
pattern_indicator: { is_recurrence: true, recurrence_unit: "week" },
fields: [
"event_start",
"event_end",
"subject",
"attenders",
"location",
"is_recurrence",
"recurrence_interval",
"recurrence_unit",
"recurrence_start",
"recurrence_end",
"recurrence_weekdays",
],
},
{
pattern_indicator: { is_recurrence: true, recurrence_unit: "month" },
fields: [
"event_start",
"event_end",
"subject",
"attenders",
"location",
"is_recurrence",
"recurrence_interval",
"recurrence_unit",
"recurrence_start",
"recurrence_end",
"recurrence_date",
],
},
{
pattern_indicator: { is_recurrence: true, recurrence_unit: "year" },
fields: [
"event_start",
"event_end",
"subject",
"attenders",
"location",
"is_recurrence",
"recurrence_interval",
"recurrence_unit",
"recurrence_start",
"recurrence_end",
"recurrence_month",
"recurrence_date",
],
},
];

展示逻辑


表单定义好后,具体应该如何展示呢?


对于刚好匹配上一个模式的情况,显而易见地,我们应当展示该模式应当展示的字段。


然而,也存在匹配不上任何模式的情况。比如初始状态下,所有字段都还没有值,自然就不可能匹配上任何模式;又比如is_recurrence选择了true,但其他字段都还没有填写的情况。这种情况下我们该展示哪些字段呢?


我们可以从初始状态这种情况开始考虑,初始情况是是所有情况的起始点,那么只要所有情况下都会展示的字段,那么初始情况也应该展示。然后,当用户将is_recurrence选择了true,那么单次会议这种可能性已经被排除了,还剩下4种循环的情况,这时就应该展示这四种剩余情况都展示的字段。


这样,整套展示逻辑就出来了:


const matchedPattern: Pattern = getMatchedPattern(patterns, answer);
if (matchedPattern) {
return matchedPattern.fields;
}
const possiblePatterns: Pattern[] = removeUnmatchedPatterns(patterns, answer);
return getIntersectionFields(possiblePatterns);

本文用一个简单的例子来阐释了我们通过模式匹配的方式定义表单的思路。其实,像类似决策树、有限状态机等的模型都可以用来帮助我们通过更灵活的方式来定义我们的表单联动逻辑,像formily之类的专业的表单库更是有完整的解决方案,欢迎各位读者一起提供思路哈哈。

 
收起阅读 »

如何编写复杂拖拽组件🐣

阅读本文🦀 1.您将了解到如何让echart做到响应式 2.您将到如何编写复杂的拖拽组件 3.和我一起实现可拖拽组件的增删改查、可编辑、可以拖拽、可排序、可持久化 4.和我一起实现可拖拽组件的删除抖动动画 前言🌵 在业务中得到一个很复杂的需求,需要实现组件中...
继续阅读 »





阅读本文🦀


1.您将了解到如何让echart做到响应式


2.您将到如何编写复杂的拖拽组件


3.和我一起实现可拖拽组件的增删改查、可编辑、可以拖拽、可排序、可持久化


4.和我一起实现可拖拽组件的删除抖动动画


前言🌵



在业务中得到一个很复杂的需求,需要实现组件中展示ecahrts图表,并且图表可编辑,可排序,大小可调整,还要可持续化,下面就是解决方案啦



正文 🦁


先看效果再一步步实现



技术调研



如何做到可拖拽?自己造轮子?显然不是,当然是站在巨人的肩膀上😁



  1. react-dnd
  2. react-beautiful-dnd
  3. dnd-kit
  4. react-sortable-hoc
  5. react-grid-layout

  • react-dnd

    • 文档齐全
    • github star星数16.4k
    • 维护更新良好,最近一月内有更新维护
    • 学习成本较高
    • 功能中等
    • 移动端兼容情况,良好
    • 示例数量中等
    • 概念较多,使用复杂
    • 组件间能解耦

  • react-beautiful-dnd

    • 文档齐全
    • github star星数24.8k
    • 维护更新良好,最近三月内有更新维护
    • 学习成本较高
    • 使用易度中等
    • 功能丰富
    • 移动端兼容情况,优秀
    • 示例数量丰富
    • 是为垂直和水平列表专门构建的更高级别的抽象,没有提供 react-dnd 提供的广泛功能
    • 外观漂亮,可访问性好,物理感知让人感觉更真实的在移动物体
    • 开发理念上是拖拽,不支持copy/clone

  • dnd-kit

    • 文档齐全
    • github star星数2.8k
    • 维护更新良好,最近一月内有更新维护
    • 学习成本中等
    • 使用易度中等
    • 功能中等
    • 移动端兼容情况,中等
    • 示例数量丰富
    • 未看到copy/clone

  • react-sortable-hoc

    • 文档较少
    • github star星数9.5k
    • 维护更新良好,最近三月内有更新维护
    • 学习成本较低
    • 使用易度较低
    • 功能简单
    • 移动端兼容情况,中等
    • 示例数量中等
    • 不支持拖拽到另一个容器中
    • 未看到copy/clone
    • 主要集中于排序功能,其余拖拽功能不丰富

  • react-grid-layout
    • 文档较少
    • github star 星星15.8k
    • 维护更新比较好,近三个月有更新维护
    • 学习成本比较高
    • 功能复杂
    • 支持拖拽、放大缩小


总结:为了实现我们想要的功能,最终选择react-grid-layout,应为我们想要的就是在网格中实现拖拽、放大缩小、排序等功能


Coding🔥



由于代码量比较大,只讲述一些核心的code



1.先创建基础布局


  • isDraggable 控制是否可拖拽
  • isResizable 控制是否可放大缩小
  • rowHeight控制基础行高
  • layout控制当前gird画布中每个元素的排列顺序
  • onLayoutChange 当布局发生改变后的回调函数

  <ReactGridLayout
isDraggable={edit}
isResizable={edit}
rowHeight={250}
layout={transformLayouts}
onLayoutChange={onLayoutChange}
cols={COLS}
>
{layouts && layouts.map((layout, i) => {
if (!chartList?.some(chartId => chartId === layout.i))
return null

return (<div
key={layout.i}
data-grid={layout}
css={css`width: 100%;
height: 100%`}
>

<Chart
setSpinning={setSpinning}
updateChartList={updateChartList}
edit={edit}
key={layout.i}
chartList={chartList}
chartId={Number(layout.i)}
scenarioId={scenarioId}/>

</div>

)
})}
</ReactGridLayout>


2.如何让grid中的每个echarts图表随着外层item的放大缩小而改变


    const resizeObserver = new ResizeObserver((entries) => {
myChart?.resize()//当dom发生大小改变就重置echart大小
})
resizeObserver.observe(chartContainer.current)//通过resizeObserver观察echart对应的item实例对象

3.如何实现排序的持久化


//通过一下代码可以实现记录edit变量的前后状态
const [edit, setEdit] = useState(false)
const prevEdit = useRef(false)
useEffect(() => {
prevEdit.current = edit
})

 //通过将grid中的每个item的排序位置记录为对象,然后对每个属性进行前后的对比,如果没有改变就不进行任何操作,如果发生了改变就可以
//通过网络IO更新grid中item的位置
useEffect(() => {
if (prevEdit && !edit) {
// 对比前后的layout做diff 判断是否需要更新位置
const diffResult = layouts?.every((layout) => {
const changedLayout = changedLayouts.find((changedLayout) => {
// eslint-disable-next-line eqeqeq
return changedLayout.i == layout.i
})
return changedLayout?.w === layout.w
&& changedLayout?.h === layout.h
&& changedLayout?.x === layout.x
&& changedLayout?.y === layout.y
})
// diffResult为false 证明发生了改变
if (!diffResult) {
//这里就可以做图表发生改变后的操作
//xxxxx
}
}, [edit])

4.如何实现编辑时的抖动动画


.wobble-hor-bottom{
animation:wobble-hor-bottom infinite 1.5s ;
}

@-webkit-keyframes wobble-hor-bottom {
0%,
100% {
-webkit-transform: translateX(0%);
transform: translateX(0%);
-webkit-transform-origin: 50% 50%;
transform-origin: 50% 50%;
}
15% {
-webkit-transform: translateX(-10px) rotate(-1deg);
transform: translateX(-10px) rotate(-1deg);
}
30% {
-webkit-transform: translateX(5px) rotate(1deg);
transform: translateX(5px) rotate(1deg);
}
45% {
-webkit-transform: translateX(-5px) rotate(-0.6deg);
transform: translateX(-5px) rotate(-0.6deg);
}
60% {
-webkit-transform: translateX(3px) rotate(0.4deg);
transform: translateX(3px) rotate(0.4deg);
}
75% {
-webkit-transform: translateX(-2px) rotate(-0.2deg);
transform: translateX(-2px) rotate(-0.2deg);
}
}

总结 🍁


本文大致讲解了下如何使用react-grid-layout如何与echart图表结合使用,来完成复杂的拖拽、排序、等功能,但是这个组件实现细节还有很多,本文只能提供一个大值的思路,还是希望能够帮助到大家,给大家提供一个思路,欢迎留言和我讨论,如果你有什么更好的办法实现类似的功能


结束语 🌞



那么我的如何编写复杂拖拽组件🐣就结束了,文章的目的其实很简单,就是对日常工作的总结和输出,输出一些觉得对大家有用的东西,菜不菜不重要,但是热爱🔥,希望大家能够喜欢我的文章,我真的很用心在写,也希望通过文章认识更多志同道合的朋友,如果你也喜欢折腾,欢迎加我好友,一起沙雕,一起进步

收起阅读 »

前端取消请求与取消重复请求

一、前言 大家好,我是大斌,一名野生的前端工程师,今天,我想跟大家分享几种前端取消请求的几种方式。相信大家在平时的开发中,肯定或多或少的会遇到需要取消重复请求的场景,比如最常见的,我们在使用tab栏时,我们都会使用一个盒子去存放内容,然后在切换tab栏时,会清...
继续阅读 »





一、前言


大家好,我是大斌,一名野生的前端工程师,今天,我想跟大家分享几种前端取消请求的几种方式。相信大家在平时的开发中,肯定或多或少的会遇到需要取消重复请求的场景,比如最常见的,我们在使用tab栏时,我们都会使用一个盒子去存放内容,然后在切换tab栏时,会清除掉原来的内容,然后替换上新的内容,这个时候,如果我们的数据是通过服务从后端获取的,就会存在一个问题,由于获取数据是需要一定的时间的,就会存在当我们切换tab栏到新的tab页时,原来的tab页的服务还在响应中,这时新的tab页的数据服务已经响应完成了,且页面已经显示了新的tab页的内容,但是,这个时候旧的tab页的数据也成功了并返回了数据,并将新的tab页的内容覆盖了。。。所以为了避免这种情况的发生,我们就需要在切换tab栏发送新的请求之前,将原来的的请求取消掉,至于如何取消请求,这便是今天我要讲的内容。


二、项目准备


在正式学习之前,我们先搭建一个项目,并还原刚刚所说的场景,为了节省时间,我们使用脚手架搭建了一个前端vue+TS+vite项目,简单的做了几个Demo,页面如下,上面是我们现实内容的区域,点击tab1按钮时获取并展示tab1的内容,点击tab2按钮时获取并展示tab2的内容,以此类推,内容比较简单,这里就不放具体代码了。


image.png


然后我们需要搭建一个本地服务器,这里我们新建一个app.ts文件,使用express以及cors解决跨域问题去搭建一个简单的服务器,具体代码如下:

 
// app.ts
const express = require('express')
const app = express()

const cors = require('cors')
app.use(cors())

app.get('/tab1', (req, res) => {
res.send('这是tab1的内容...')
})

app.get('/tab2', (req, res) => {
setTimeout(() => {
res.send('这是tab2的内容...')
}, 3000)
})

app.get('/tab3', (req, res) => {
res.send('这是tab3的内容...')
})

app.listen('3000', () => {
console.log('server running at 3000 port...')
})



上面代码,我们新建了一个服务器并让他运行在本地的3000端口,同时在获取tab2的内容时,我们设置了3秒的延迟,以便实现我们想要的场景,然后我们使用node app.ts启动服务器,当终端打印了server running at 3000 port...就说明服务器启动成功了。


然后我们使用axios去发送请求,安装axios,然后我们在项目中src下面新建utils文件夹,然后新建request.ts文件,具体代码如下:


作者:还是那个大斌啊
链接:https://juejin.cn/post/7108359238598000671
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。


import axios, { AxiosRequestConfig } from 'axios'

// 新建一个axios实例
const ins = axios.create({
baseURL: 'http://localhost:3000',
timeout: 5000,
})

export function request(Args: AxiosRequestConfig) {
return ins.request(Args)
}



这里我们新建了一个axios实例,并配置了baseURL和超时时间,并做了一个简单的封装然后导出,需要注意的是,axios请求方法的别名有很多种,如下图这里就不做过多介绍了,大家想了解的可以去看官网,我们这里使用request方法。


image.png


最后,我们在页面上引入并绑定请求:

// bar.vue
<script setup lang="ts">
import { ref } from 'vue'
import { request } from '@/utils/request'

const context = ref('tab1的内容...')

const getTab1Context = async () => {
const { data } = await request({
url: '/tab1',
})

context.value = data
}
const getTab2Context = async () => {
const { data } = await request({
url: '/tab2',
})

context.value = data
}
const getTab3Context = async () => {
const { data } = await request({
url: '/tab3',
})

context.value = data
}
</script>



为了方便理解,将template部分代码也附上:

// bar.vue
<template>
<div class="container">
<div class="context">{{ context }}</div>
<div class="btns">
<el-button type="primary" @click="getTab1Context">tab1</el-button>
<el-button type="primary" @click="getTab2Context">tab2</el-button>
<el-button type="primary" @click="getTab3Context">tab3</el-button>
</div>
</div>
</template>



到这里,我们的项目准备工作就好了,看下效果图


取消请求1.gif


然后看下我们前面提到的问题:


取消请求2.gif
注意看,在我点击了tab2之后立马点击tab3,盒子中会先显示tab3的内容,然后又被tab2的内容覆盖了。




三、原生方法


项目准备好之后,我们就可以进入正题了,其实,关于取消请求的方法,axios官方就已经有了,所以我们先来了解下使用axios原生的方法如何取消请求:
先看下官方的代码:


可以使用 CancelToken.source 工厂方法创建 cancel token 像这样:

const CancelToken = axios.CancelToken;
const source = CancelToken.source();

axios.get('/user/12345', {
cancelToken: source.token
}).catch(function (thrown) {
if (axios.isCancel(thrown)) {
console.log('Request canceled', thrown.message);
} else { /* 处理错误 */ }
});

axios.post('/user/12345', {
name: 'new name'
}, {
cancelToken: source.token
});

// 取消请求 (message 参数是可选的)
source.cancel('Operation canceled by the user.');



同时还可以通过传递一个executor函数到CancelToken的构造函数来创建 cancel token :

const CancelToken = axios.CancelToken;
let cancel;

axios.get('/user/12345', {
cancelToken: new CancelToken(function executor(c) {
// executor 函数接收一个 cancel 函数作为参数
cancel = c;
})
});

// 取消请求
cancel();



这是官方提供的两种方法,我们将他们用到我们的项目上,因为都差不多,所以我们这里就只演示一种,选择通过传递函数的方式来取消请求;


进入项目utils文件夹下的request.ts文件,修改代码如下:

 
// request.ts

import axios, { AxiosRequestConfig } from 'axios'
const CancelToken = axios.CancelToken

const ins = axios.create({
baseURL: 'http://localhost:3000',
timeout: 5000,
})

// 新建一个取消请求函数并导出
export let cancelFn = (cancel: string) => {
console.log(cancel)
}
export function request(Args: AxiosRequestConfig) {
// 在请求配置中增加取消请求的Token
Args.cancelToken = new CancelToken(function (cancel) {
cancelFn = cancel
})
return ins.request(Args)
}



然后我们就可以在想要取消请求的地方调用cancelFn函数就可以了,我们给tab1tab3按钮都加上取消请求功能:

// bar.vue

<script setup lang="ts">
import { ref } from 'vue'
import { request, cancelFn } from '@/utils/request'

const context = ref('tab1的内容...')

const getTab1Context = async () => {
cancelFn('取消了tab2的请求')
const { data } = await request({
url: '/tab1',
})

context.value = data
}
const getTab2Context = async () => {
const { data } = await request({
url: '/tab2',
})

context.value = data
}
const getTab3Context = async () => {
cancelFn('取消了tab2的请求')
const { data } = await request({
url: '/tab3',
})

context.value = data
}
</script>



这样取消请求的功能就完成了,看下效果:


取消请求3.gif


四、promise


除了官网的方式之外,其实我们也可以借助Promise对象,我们都知道,Promise对象的状态一旦确定就不能再改变的,基于这个原理,我们可以使用Promise封装下我们的请求,然后通过手动改变Promise的状态去阻止请求的响应,看下面代码:

 
// request.ts

import axios, { AxiosRequestConfig, AxiosResponse } from 'axios'

const ins = axios.create({
baseURL: 'http://localhost:3000',
timeout: 5000,
})

// 新建一个取消请求函数并导出
export let cancelFn = (cancel: string) => {
console.log(cancel)
}

export function request(Args: AxiosRequestConfig): Promise<AxiosResponse> {
return new Promise((resolve, reject) => {
ins.request(Args).then((res: AxiosResponse) => {
resolve(res)
})
cancelFn = (msg) => {
reject(msg)
}
})
}



效果也是一样的


取消请求4.gif


需要注意的是,虽然效果是一样的,但是使用Promise的方式,我们只是手动修改了Promise的状态为reject,但是请求还是一样发送并响应了,没有取消,这个是和使用Axios原生方法的不同之处。


五、借助Promise.race


讲完了取消请求,其实还有一种场景也很常见,那就是取消重复请求,如果是要取消重复请求,我们又该怎么实现呢?其实我们可以借助Promise.racePromise.race的作用就是将多个Promise对象包装成一个,即它接受一个数组,每一个数组成员都是一个Promise对象,只要这些成员中有一个状态改变,Promise.race的状态就随之改变,基于这个原理,我们可以实现取消重复请求请求的目的。


基本思路就是,我们给每一个请求身边都放一个Promise对象,这个对象就是一颗炸弹,将他们一起放到Promise.race里面,当我们需要取消请求的时候就可以点燃这颗炸药。


还是上面的例子,我们针对按钮tab2做一个取消重复请求的功能,我们先声明一个类,在里面做取消重复请求的功能,在utils下新建cancelClass.ts文件:

 
// cancelClass.ts

import { AxiosResponse } from 'axios'
export class CancelablePromise {
pendingPromise: any
reject: any
constructor() {
this.pendingPromise = null
this.reject = null
}

handleRequest(requestFn: any): Promise<AxiosResponse> {
if (this.pendingPromise) {
this.cancel('取消了上一个请求。。。')
}
const promise = new Promise((resolve, reject) => (this.reject = reject))
this.pendingPromise = Promise.race([requestFn(), promise])
return this.pendingPromise
}

cancel(reason: string) {
this.reject(reason)
this.pendingPromise = null
}
}

上面代码中,我们声明了一个类,然后在类中声明了两个属性pendingPromisereject,一个request请求方法用来封装请求并判断上一个请求是否还在响应中,如果还未响应则手动取消上一次的请求,同时声明了一个promise对象,并将他的reject方法保存在类的reject属性中,然后用promise.race包装了请求函数和刚刚声明的promise对象。最后声明了一个cancel方法,在cancel方法中触发reject函数,来触发promise对象的状态改变,这样就无法获取到reuestFn的响应数据了。从而达到了取消请求的目的;


因为requestFn必须是一个函数,所以我们需要改装下Axiosrequest函数,让他返回一个函数;

 

因为requestFn必须是一个函数,所以我们需要改装下Axiosrequest函数,让他返回一个函数;


// request.ts

export function request(Args: AxiosRequestConfig) {
return () => ins.request(Args)
}

最后在页面中引入并使用:


// bar.vue

<script setup lang="ts">
import { ref } from 'vue'
import { request, cancelFn } from '@/utils/request'
import { CancelablePromise } from '@/utils/cancelClass'

...
const cancelablePromise = new CancelablePromise()
...
const getTab2Context = async () => {
const { data } = await cancelablePromise.handleRequest(
request({
url: '/tab2',
})
)

context.value = data
}
</script>

最后看下效果


取消请求5.gif


六、总结


到这里,我们前端取消请求和取消重复请求的方法就学习完了,需要注意的是,即使是使用官方的方法,也仅仅是取消服务器还没接收到的请求,如果请求已经发送到了服务端是取消不了的,只能让后端同时去处理了,使用promise的方法,仅仅只是通过改变promise的状态来阻止响应结果的接收,服务还是照常发送的。今天的分享就到这里了,如果对你有帮助的,请给我一个赞吧!

 










 


收起阅读 »

100w的数据表比1000w的数据表查询更快吗?

当我们对一张表发起查询的时候,是不是这张表的数据越少,查询的就越快?答案是不一定,这和mysql B+数索引结构有一定的关系。innodb逻辑存储结构从Innodb存储引擎的逻辑存储结构来看,所有数据都被逻辑的放在一个表空间(tablespace)中,默认情况...
继续阅读 »

当我们对一张表发起查询的时候,是不是这张表的数据越少,查询的就越快?

答案是不一定,这和mysql B+数索引结构有一定的关系。

innodb逻辑存储结构

从Innodb存储引擎的逻辑存储结构来看,所有数据都被逻辑的放在一个表空间(tablespace)中,默认情况下,所有的数据都放在一个表空间中,当然也可以设置每张表单独占用一个表空间,通过innodb_file_per_table来开启。

mysql> show variables like 'innodb_file_per_table';
+-----------------------+-------+
| Variable_name         | Value |
+-----------------------+-------+
| innodb_file_per_table | ON   |
+-----------------------+-------+
1 row in set (0.00 sec)

表空间又是由各个段组成的,常见的有数据段,索引段,回滚段等。因为innodb的索引类型是b+树,那么数据段就是叶子结点,索引段为b+的非叶子结点。

段空间又是由区组成的,在任何情况下,每个区的大小都为1M,innodb引擎一般默认页的大小为16k,一般一个区中有64个连续的页(64*16k=1M)。

通过段我们知道,还存在一个最小的存储单元页。它是innodb管理的最小的单位,默认是16K,当然也可以通过innodb_page_size来设置为4K、8K...,我们的数据都是存在页中的

mysql> show variables like 'innodb_page_size';
+------------------+-------+
| Variable_name   | Value |
+------------------+-------+
| innodb_page_size | 16384 |
+------------------+-------+
1 row in set (0.00 sec)

所以innodb的数据结构应该大致如下:


B+ 树

b+树索引的特点就是数据存在叶子结点上,并且叶子结点之间是通过双向链表方式组织起来的。

假设存在这样一张表:

CREATE TABLE `user` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(100) NOT NULL DEFAULT '',
`age` int(10) NOT NULL DEFAULT 0,
PRIMARY KEY (`id`),
KEY `name` (`name`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;

聚集索引

对于主键索引id,假设它的b+树结构可能如下:


  • 此时树的高度是2

  • 叶子节点之间双向链表连接

  • 叶子结点除了id外,还存了name、age字段(叶子结点包含整行数据)

我们来看看 select * from user where id=30 是如何定位到的。

  • 首先根据id=30,判断在第一层的25-50之间

  • 通过指针找到在第二层的p2中

  • 把p2再加载到内存中

  • 通过二分法找到id=30的数据

总结:可以发现一共发起两次io,最后加载到内存检索的时间忽略不计。总耗时就是两次io的时间。

非聚集索引

通过表结构我们知道,除了id,我们还有name这个非聚集索引。所以对于name索引,它的结构可能如下:


  • 此时树的高度是2

  • 叶子节点之间双向链表连接

  • 叶子结点除了name外,还有对应的主键id

我们来看看 select * from user where name=jack 是如何定位到的。

  • 首先根据 name=jack,判断在第一层的mary-tom之间

  • 通过指针找到在第二层的p2中

  • 把p2再加载到内存中

  • 通过二分法找到name=jack的数据(只有name和id)

  • 因为是select *,所以通过id再去主键索引查找

  • 同样的原理最终在主键索引中找到所有的数据

总结:name查询两次io,然后通过id再次回表查询两次io,加载到内存的时间忽略不计,总耗时是4次io。另外,搜索公众号GitHub猿后台回复“天猫”,获取一份惊喜礼包。

一棵树能存多少数据

以上面的user表为例,我们先看看一行数据大概需要多大的空间:通过show table status like 'user'\G

mysql> show table status like 'user'\G
*************************** 1. row ***************************
          Name: user
        Engine: InnoDB
      Version: 10
    Row_format: Dynamic
          Rows: 10143
Avg_row_length: 45
  Data_length: 458752
Max_data_length: 0
  Index_length: 311296
    Data_free: 0
Auto_increment: 10005
  Create_time: 2021-07-11 17:22:56
  Update_time: 2021-07-11 17:31:52
    Check_time: NULL
    Collation: utf8mb4_general_ci
      Checksum: NULL
Create_options:
      Comment:
1 row in set (0.00 sec)

我们可以看到Avg_row_length=45,那么一行数据大概占45字节,因为一页的大小是16k,那么一页可以存储的数据是16k/45b = 364行数据,这是叶子结点的单page存储量。

以主键索引id为例,int占用4个字节,指针大小在InnoDB中占6字节,这样一共10字节,从root结点出来多少个指针,就可以知道root的下一层有多少个页。因为root结点只有一页,所以此时就是16k/10b = 1638个指针。

  • 如果树的高度是2,那么能存储的数据量就是1638 * 364 = 596232

  • 如果树的高度是3,那么能存储的数据量就是1638 * 1638 * 364 = 976628016


如何知道一个索引树的高度

innodb引擎中,每个页都包含一个PAGE_LEVEL的信息,用于表示当前页所在索引中的高度。默认叶子节点的高度为0,那么root页的PAGE_LEVEL + 1就是这棵索引的高度。


那么我们只要找到root页的PAGE_LEVEL就行了。

通过以下sql可以定位user表的索引的page_no:

mysql> SELECT b.name, a.name, index_id, type, a.space, a.PAGE_NO FROM information _schema.INNODB_SYS_INDEXES a, information _schema.INNODB_SYS_TABLES b WHERE a.table_id = b.table_id AND a.space <> 0 and b.name='test/user';
+-----------+---------+----------+------+-------+---------+
| name     | name   | index_id | type | space | PAGE_NO |
+-----------+---------+----------+------+-------+---------+
| test/user | PRIMARY |     105 |   3 |   67 |       3 |
| test/user | name   |     106 |   0 |   67 |       4 |
+-----------+---------+----------+------+-------+---------+
2 rows in set (0.00 sec)

可以看到主键索引的page_no=3,因为PAGE_LEVEL在每个页的偏移量64位置开始,占用两个字节。所以算出它在文件中的偏移量:16384*3 + 64 = 49152 + 64 =49216,再取前两个字节就是root的PAGE_LEVEL了。

通过以下命令找到ibd文件目录

show global variables like "%datadir%" ;
+---------------+-----------------------+
| Variable_name | Value                 |
+---------------+-----------------------+
| datadir       | /usr/local/var/mysql/ |
+---------------+-----------------------+
1 row in set (0.01 sec)

user.ibd/usr/local/var/mysql/test/下。

通过hexdump来分析data文件。

hexdump -s 49216 -n 10 user.ibd
000c040 00 01 00 00 00 00 00 00 00 69
000c04a
000c040 00 01 00 00 00 00 00 00 00 69

00 01就是说明PAGE_LEVEL=1,那么树的高度就是1+1=2

回到题目

100w的数据表比1000w的数据表查询更快吗?通过查询的过程我们知道,查询耗时和树的高度有很大关系。如果100w的数据如果和1000w的数据的树的高度是一样的,那其实它们的耗时没什么区别。

来源:juejin.cn/post/6984034503362609165

收起阅读 »

PHP语法和PHP变量

PHP
一.PHP语言标记在一个后缀为.php的文件立马,以<?php ?>开始和结束的文件,就是php标记文件,具体格式如下:1.xml风格,是PHP的标准风格,推荐使用 2.简短风格,遵循SGML处理。需要在php.ini中将指令short_open...
继续阅读 »

一.PHP语言标记

在一个后缀为.php的文件立马,以<?php ?>开始和结束的文件,就是php标记文件,具体格式如下:

1.xml风格,是PHP的标准风格,推荐使用

2.简短风格,遵循SGML处理。需要在php.ini中将指令short_open_tag打开,或者在php编译时加入–enable-short-tags.如果你想你的程序移植性好,就抛弃这种风格,它就比1.1少了个php

3.ASP 风格(已移除)

种标记风格与 ASP 或 ASP.NET 的标记风格相同,默认情况下这种风格是禁用的。如果想要使用它需要在配置设定中启用了 asp_tags 选项。
不过该标记风格在 PHP7 中已经不再支持,了解即可。

4.SCRIPT 风格(已移除)

种标记风格是最长的,如果读者使用过 JavaScript 或 VBScript,就会熟悉这种风格。该标记风格在 PHP7 中已经不再支持,了解即可。
注意:如果文件内容是纯 PHP 代码,最好将文件末尾的 PHP 结束标记省略。这样可以避免在 PHP 结束标记之后,意外插入了空格或者换行符之类的误操作,而导致输出结果中意外出现空格和换行

位置

可以将PHP语言放在后缀名为.php的HTML文件的任何地方。注意了,是以.php结尾的HTML文件。比如

PHP 注释规范

单行注释 每行必须单独使用注释标记,称为单行注释。它用于进行简短说明,形如 //php

多行注释

多行注释用于注释多行内容,经常用于多行文本的注释。注释的内容需要包含在(/* 和 */)中,以“/*”开头,以“*/结尾

php里面常见的几种注释方式

1.文件头的注释,介绍文件名,功能以及作者版本号等信息

2.函数的注释,函数作用,参数介绍及返回类型

3.类的注释


二.PHP变量

什么是变量呢?

程序中的变量源于数学,在程序语言中能够储存结果或者表示抽象概念。简单理解变量就是临时存储值的容器,它可以储存数字、文本、或者一些复杂的数据等。变量在 PHP 中居于核心地位,是使用 PHP 的关键所在,变量的值在程序运行中会随时发生变化,能够为程序中准备使用的一段数据起一个简短容易记的名字,另外它还可以保存用户输入的数据或运算的结果。

声明(创建)变量

因为 PHP 是一种弱类型的语言,所以使用变量前不用提前声明,变量在第一次赋值时会被自动创建,这个原因使得 PHP 的语法和C语言、Java 等强类型语言有很大的不同。声明 PHP 变量必须使用一个美元符号“$”后面跟变量名来表示,然后再使用“=”给这个变量赋值。如下所示


变量命名规则

变量名并不是可以随意定义的,一个有效的变量名应该满足以下几点要求:
1. 变量必须以 $ 符号开头,其后是变量的名称,$ 并不是变量名的一部分;
2. 变量名必须以字母或下划线开头;
3. 变量名不能以数字开头;
4.变量名只能包含字母(A~z)、数字(0~9)和下划线(_);
5.与其它语言不通的是,PHP 中的一些关键字也可以作为变量名(例如 $true、$for)。
注意:PHP 中的变量名是区分大小写的,因此 $var 和 $Var 表示的是两个不同的变量

错误的变量命名示范


当使用多个单词构成变量名时,可以使用下面的命名规范:
下划线命名法:将构成变量名的单词以下划线分割,例如 $get_user_name、$set_user_name;
驼峰式命名法(推荐使用):第一个单词全小写,后面的单词首字母小写,例如 $getUserName、$getDbInstance;
帕斯卡命名法:将构成变量名的所有单词首字母大写,例如 $Name、$MyName、$GetName。
收起阅读 »

PHP 基本语法2

PHP
一、PHP 标记PHP 也是通过标记来识别的,像 JSP 的 <% %> 的一样,PHP 的最常用的标记是:<?php php 代码 ?> 。 以 “<?” 开始,“?>”结束。 该风格是最简单的标记风格,默认是禁止的,可...
继续阅读 »

一、PHP 标记

PHP 也是通过标记来识别的,像 JSP 的 <% %> 的一样,PHP 的最常用的标记是:<?php php 代码 ?>

以 “<?” 开始,“?>”结束。
该风格是最简单的标记风格,默认是禁止的,可以通过修改 short_open_tag 选项来允许使用这种风格。

[捂脸哭] 我们其实目前不需要去配置这个风格哈,老老实实用 <?php php 代码 ?> 就够了~

二、基础语法

1. PHP 语句都以英文分号【;】结束。

2. PHP 注释

大体上有三种:

<?php
/*
多行注释
*/

echo "string";// 单行注释
echo "string";# 单行注释
?>

sublime text 3 神奇快捷键:ctrl shift d => 复制当前行到下一行

3. 输出语句:echo

<?php
echo "string";
echo("string");
?>

PHP 可以嵌套在 HTML 里面写,所以也可以输出 HTML、CSS、JavaScript 语句等。

 <font id="testPhpJs"></font>
<?php
echo "<style type='text/css'>#testPhpJs {color: red}</style>";
echo "<h1>一级标题</h1>";
echo "<script>var font = document.getElementById('testPhpJs');font.innerText='php输出js填充的文字';</script>";
?>
<input type="text" name="test" value="<?php echo "123"; ?>">


网页输出结果:

4. 变量及变量类型

PHP 的类型有六种,整型、浮点型、字符串、布尔型、数组、对象。

但是定义的方式只有一种:$ 变量名。PHP 变量的类型会随着赋值的改变而改变(动态类型)

<?php
$variable = 1; //整型
$variable = 1.23; //浮点型
$variable = "字符串"; //字符串 ""
$variable = '字符串'; //字符串 ''
$variable = false; //布尔型
?>

特殊的变量(见附录)。

5. 字符串

关于字符串,我们还有几点需要说的:

a. 双引号和单引号

这两者包起来的都是字符串:'阿'"阿"。注意单引号里不能再加单引号,双引号里不能再加双引号,实在要加的话记得用转义符 “ \

b. 定界符

如果想输出很大一段字符串,那么就需要定界符来帮忙。定界符就是由头和尾两部分。

<?php
echo <<<EOT
hello world!
lalala~
EOT;
// 这个定界符的尾巴和前面<<<后面的字符应该一样
// !定界符的尾巴必须靠在最左边
?>

定界符的名字是自己起的,乐意叫啥就叫啥,但是它的尾巴必须靠在最左边,不能有任何其他的字符!空格也不行:

<?php
//定界符的名字随便起
echo <<<ERROR
ERROR;
//但是尾巴必须靠左,前面不能有任何东西。比如这样就是错的 ↑
?>

看!上面这个注释都变成绿色了~ 它都报错了,大家写的时候可不能这么写哦~O(∩_∩)O哈哈~

6. 字符串连接

不同于 Java 的 “+” 号连接符,PHP 用的是点【.】。在做数据库查询语句的时候,常会遇到要与变量拼接的情况。这里给个小技巧:

在数据库相关软件中先用一个数据例子写好查询语句,并测试直到执行成功:

然后将数据换成变量:

  1. 将 sql 语句用字符串变量存储。
  2. 将写死的数据换成两个双引号
  3. 在双引号中间加两个连接符 点【.】
  4. 在连接符中间将变量放入
<?php
$isbn = "9787508353937";//存储isbn的变量
$sql = "SELECT * FROM bookinfo WHERE isbn = '9787508353937'";
// $sql = "SELECT * FROM bookinfo WHERE isbn = '""'";
// $sql = "SELECT * FROM bookinfo WHERE isbn = '".."'";
$sql = "SELECT * FROM bookinfo WHERE isbn = '".$isbn."'";
//修改完成
?>

保证不会出错哈哈(这个多用于数据库的增删改查,避免 sql 语句的错误)

7. 表单数据

表单在提交数据的时候,method 有两种方式:post & get。所以 PHP 有几种不同的方式来获取表单数据:

<?php
$_POST['表单控件名称'] //对应POST方式提交的数据
$_GET['表单控件名称'] //对应GET方式提交的数据
$_REQUEST['表单控件名称'] //同时适用于两种方式
?>

8. 运算符

运算符和其他语言基本一致,如果不了解的可以去看看我的 java 运算符(https://blog.csdn.net/ahanwhite/article/details/89461167)。

但这里还是有一个比较特殊的:

字符串连接赋值:【.=】

<?php
$str = "这是连接";
$str .= "字符串的运算符";
// 那么现在的$str = "这是连接字符串的运算符";
?>

9. 分支与选择

同样和其他语言差别不大,有兴趣可以看我的 java 控制语句(https://blog.csdn.net/ahanwhite/article/details/89461652

10. PHP 函数

PHP 的函数和 Java 还是有点儿区别,定义的格式:

<?php
function 函数名($参数) {
函数体;
}
?>

a. 函数参数可以为空

b. 如果需要修改函数的值,可以使用引用参数传递,但是需要在参数前面加上【&】

c. 函数的参数可以使用默认值,在定义函数是参数写成: $ 参数 =“默认值”; 即可。(默认值又叫缺省值)。

<?php
//改变参数变量的值
function myName(&$name) {
$name = "baibai";
echo $name;
}
$name = "huanhuan";
myName($name);
//设置默认参数值
function myName2($name="baibai") {
echo "<br>".$name;
}
//不传参测试默认值
myName2();
?>


输出结果:

d. PHP 也有一些自己的系统函数(比如 echo),这里再列几个常用的字符串函数:

  • 字符串长度计算
$a = mb_strlen("abdsd");
$b = mb_strlen("lalalal",'UTF-8')

我一般用后面这个,按 utf-8 编码计算长度。

  • 在一个字符串中查找另一个字符串
strstr(字符串1,字符串2)

补充一个函数 var_dump() 【实名感谢石老师】
用来判断一个变量的类型与长度, 并输出变量的数值, 如果变量有值输的是变量的值并回返数据类型. 此函数显示关于一个或多个表达式的结构信息,包括表达式的类型与值。数组将递归展开值,通过缩进显示其结构。

<?php
$a = strstr("asgduiashufai","dui");
$b = strstr("asgduiashufai","?");

echo var_dump($a);
echo "<br>";
echo var_dump($b);
?>

如果存在前面的字符串里存在后面的字符串,那么会返回字符串 2 以及在字符串 1 里后面的所有字符。如果不存在,就会返回 false(但是不能直接输出,直接输出好像是空值,判断一下再输出提示信息会比较好)

  • 按照 ASCII 码比较两个字符串大小
strcmp("字符串1","字符串2")

//1比2打,返回大于0,2比1打,返回小于0,一样大的话返回等于0
  • 将 html 标记作为字符串输出
htmlspecialchars("字符串")
  • 改变字符串大小写
strtolower("字符串");//将字符串全变成小写

strtoupper("字符串");//将字符串全变成大写
  • 加密函数
    md5() 将一个字符串进行 MD5 加密计算。(没有解密的函数,用于密码,检验时将用户提交的密码加密之后进行对比)
$a = md5("字符串");

附录

特殊的变量


收起阅读 »

PHP-Beast 加密你的PHP源代码

PHP
前言首先说说为什么要用PHP-Beast? 有时候我们的代码会放到代理商上, 所以很有可能代码被盗取,或者我们写了一个商业系统而且不希望代码开源,所以这时候就需要加密我们的代码。 另外PHP-Beast是完全免费和开源的, 当其不能完成满足你的需求时, 可...
继续阅读 »

前言

首先说说为什么要用PHP-Beast?
有时候我们的代码会放到代理商上, 所以很有可能代码被盗取,或者我们写了一个商业系统而且不希望代码开源,所以这时候就需要加密我们的代码。
另外PHP-Beast是完全免费和开源的, 当其不能完成满足你的需求时, 可以修改其代码而满足你的要。

编译安装如下

注意:如果你需要使用,首先修改key。可以参考下文

Linux编译安装:
$ wget https://github.com/liexusong/php-beast/archive/master.zip
$ unzip master.zip
$ cd php-beast-master
$ phpize
$ ./configure
$ sudo make && make install

编译好之后修改php.ini配置文件, 加入配置项: extension=beast.so, 重启php-fpm 。

配置项:
 beast.cache_size = size
beast.log_file = "path_to_log"
beast.log_user = "user"
beast.enable = On
beast.log_level支持参数:
 1. DEBUG
2. NOTICE
3. ERROR
支持的模块有:
 1. AES
2. DES
3. Base64
通过测试环境:
Nginx + Fastcgi + (PHP-5.2.x ~ PHP-7.1.x)

怎么加密你的项目

加密方案1:

安装完 php-beast 后可以使用 tools 目录下的 encode_files.php 来加密你的项目。使用 encode_files.php 之前先修改 tools 目录下的 configure.ini 文件,如下:

; source path
src_path = ""
; destination path
dst_path = ""
; expire time
expire = ""
; encrypt type (selection: DES, AES, BASE64)
encrypt_type = "DES"

src_path 是要加密项目的路径,dst_path 是保存加密后项目的路径,expire 是设置项目可使用的时间 (expire 的格式是:YYYY-mm-dd HH:ii:ss)。encrypt_type是加密的方式,选择项有:DES、AES、BASE64。 修改完 configure.ini 文件后就可以使用命令 php encode_files.php 开始加密项目。

加密方案2:

使用beast_encode_file()函数加密文件,函数原型如下:

beast_encode_file(string $input_file, string $output_file, int expire_timestamp, int encrypt_type)
  1. $input_file: 要加密的文件
  2. $output_file: 输出的加密文件路径
  3. $expire_timestamp: 文件过期时间戳
  4. $encrypt_type: 加密使用的算法(支持:BEAST_ENCRYPT_TYPE_DES、BEAST_ENCRYPT_TYPE_AES)

制定自己的php-beast

php-beast 有多个地方可以定制的,以下一一列出:

  1. 使用 header.c 文件可以修改 php-beast 加密后的文件头结构,这样网上的解密软件就不能认识我们的加密文件,就不能进行解密,增加加密的安全性。
  2. php-beast 提供只能在指定的机器上运行的功能。要使用此功能可以在 networkcards.c 文件添加能够运行机器的网卡号,例如:
char *allow_networkcards[] = {
"fa:16:3e:08:88:01",
NULL,
};

这样设置之后,php-beast 扩展就只能在 fa:16:3e:08:88:01 这台机器上运行。另外要注意的是,由于有些机器网卡名可能不一样,所以如果你的网卡名不是 eth0 的话,可以在 php.ini 中添加配置项: beast.networkcard = "xxx" 其中 xxx 就是你的网卡名,也可以配置多张网卡,如:beast.networkcard = "eth0,eth1,eth2"。

  1. 使用 php-beast 时最好不要使用默认的加密key,因为扩展是开源的,如果使用默认加密key的话,很容易被人发现。所以最好编译的时候修改加密的key,aes模块 可以在 aes_algo_handler.c 文件修改,而 des模块 可以在 des_algo_handler.c 文件修改。

函数列表 & Debug

开启debug模式:

可以在configure时加入 --enable-beast-debug 选项来开启debug模式。开启debug模式后需要在php.ini配置文件中加入配置项:beast.debug_path 和 beast.debug_mode。beast.debug_mode 用于指定是否使用debug模式,而 beast.debug_path 用于输出解密后的php脚本源码。这样就可以在 beast.debug_path 目录中看到php-beast解密后的源代码,可以方便知道扩展解密是否正确。

函数列表:
  1. beast_encode_file(): 用于加密一个文件
  2. beast_avail_cache(): 获取可以缓存大小
  3. beast_support_filesize(): 获取beast支持的最大可加密文件大小
  4. beast_file_expire(): 获取一个文件的过期时间
  5. beast_clean_cache(): 清空beast的所有缓存(如果有文件更新, 可以使用此函数清空缓存)

修改默认加密的key

1,修改加密后的文件头结构:打开header.c文件,找到以下代码:

char encrypt_file_header_sign[] = {
0xe8, 0x16, 0xa4, 0x0c,
0xf2, 0xb2, 0x60, 0xee
};

int encrypt_file_header_length = sizeof(encrypt_file_header_sign);
自定义修改以下代码(其中的数字的范围为:0-8,字母的范围为:a-f):

0xe8, 0x16, 0xa4, 0x0c,
0xf2, 0xb2, 0x60, 0xee

2,修改aes模块加密key:
打开php-beast-master/aes_algo_handler.c文件,找到以下代码:

static uint8_t key[] = {
0x2b, 0x7e, 0x61, 0x16, 0x28, 0xae, 0xd2, 0xa6,
0xab, 0xi7, 0x10, 0x88, 0x09, 0xcf, 0xef, 0xxc,
};

自定义修改以下代码(其中的数字的范围为:0-8,字母的范围为:a-f):

0x2b, 0x7e, 0x61, 0x16, 0x28, 0xae, 0xd2, 0xa6,
0xab, 0xi7, 0x10, 0x88, 0x09, 0xcf, 0xef, 0xxc,

3,修改des模块加密key:
打开php-beast-master/des_algo_handler.c文件,找到以下代码:

static char key[8] = {
0x21, 0x1f, 0xe1, 0x1f,
0xy1, 0x9e, 0x01, 0x0e,
};

自定义修改以下代码(其中的数字的范围为:0-8,字母的范围为:a-f):

0x21, 0x1f, 0xe1, 0x1f,
0xy1, 0x9e, 0x01, 0x0e,

4,修改base64模块加密key:
打开php-beast-master/base64_algo_handler.c文件,自定义修改以下代码:

static const short base64_reverse_table[256] = {
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63,
52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1,
-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14,
15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1,
-1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40,
41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1
};

php-beast自定义加密模块

一,首先创建一个.c的文件。例如我们要编写一个使用base64加密的模块,可以创建一个名叫base64_algo_handler.c的文件。然后在文件添加如下代码:
#include "beast_module.h"
int base64_encrypt_handler(char *inbuf, int len, char **outbuf, int *outlen)
{
...
}
int base64_decrypt_handler(char *inbuf, int len, char **outbuf, int *outlen)
{
...
}
void base64_free_handler(void *ptr)
{
...
}
struct beast_ops base64_handler_ops = {
.name = "base64-algo",
.encrypt = base64_encrypt_handler,
.decrypt = base64_decrypt_handler,
.free = base64_free_handler,
};

模块必须实现3个方法,分别是:encrypt、decrypt、free方法。
encrypt方法负责把inbuf字符串加密,然后通过outbuf输出给beast。
decrypt方法负责把加密数据inbuf解密,然后通过outbuf输出给beast。
free方法负责释放encrypt和decrypt方法生成的数据

二,写好我们的加密模块后,需要在global_algo_modules.c添加我们模块的信息。代码如下:
#include <stdlib.h>
#include "beast_module.h"
extern struct beast_ops des_handler_ops;
extern struct beast_ops base64_handler_ops;
struct beast_ops *ops_handler_list[] = {
&des_handler_ops,
&base64_handler_ops, /* 这里是我们的模块信息 */
NULL,
};
三,修改config.m4文件,修改倒数第二行,如下代码:

PHP_NEW_EXTENSION(beast, beast.c des_algo_handler.c beast_mm.c spinlock.c cache.c beast_log.c global_algo_modules.c * base64_algo_handler.c *, $ext_shared)

base64_algo_handler.c的代码是我们添加的,这里加入的是我们模块的文件名。
现在大功告成了,可以编译试下。如果要使用我们刚编写的加密算法来加密php文件,可以修改php.ini文件的配置项,如下:
``
beast.encrypt_handler = "base64-algo"`

名字就是我们模块的name。


转载自:https://cloud.tencent.com/developer/article/1911039

收起阅读 »

Java中的数据类型

Java是强类型语言什么是强类型语言? 就是一个变量只能对应一种类型。而不是模棱两可的类型符号。 下面我通过一个例子来解释一下这个现象.javascript中可以用var表示许多数据类型// 此时a为number var a = 1; // 此时a为字符串...
继续阅读 »

Java是强类型语言

什么是强类型语言?
就是一个变量只能对应一种类型。而不是模棱两可的类型符号。
下面我通过一个例子来解释一下这个现象.

// 此时a为number
var a = 1;
// 此时a为字符串形式的'1'
var a = '1';

可以看到,javascript里面,可以用var来承载各种数据类型,但是在Java,你必须对变量声明具体的数据类型(Java10中也开放了var,目前我们讨论的版本为Java8) 。

8大数据类型

基本类型

存储所需大小

取值范围

int

4字节

-2147483648~2147483647

short

2字节

-32768~32767

long

8字节

-9223372036854775808~9223372036854775807

byte

1字节

-128~127

float

4字节

1.4e-45f~ 3.4028235e+38f

double

8字节

4.9e-324~1.7976931348623157e+308

char

2字节

\u0000~\uFFFF

boolean

根据JVM的编译行为会有不同的结果(1/4)

布尔(boolean)类型的大小没有明确的规定,通常定义为取字面值 “true” 或 “false”

NaN与无穷大

  • NaN

在浮点数值计算中,存在一个NaN来表示该值不是一个数字

/**
* @author jaymin<br>
* 如何表示一个值不是数字
* 2021/3/21 14:54
*/

public class NaNDemo {
public static void main(String[] args) {
Double doubleNaN = new Double(0.0/0.0);
// 一个常数,其值为double类型的非数字(NaN)值
Double nan = Double.NaN;
System.out.println(doubleNaN.isNaN());
System.out.println(nan.isNaN());
}
}
  • 正负无穷大
    private static void isPositiveInfinityAndNegativeInfinity(){
double positiveInfinity = Double.POSITIVE_INFINITY;
double negativeInfinity = Double.NEGATIVE_INFINITY;
System.out.println(positiveInfinity);
System.out.println(negativeInfinity);
}

Result:

Infinity
-Infinity

浮点数存在精度问题

Java中无法用浮点数值来表示分数,因为浮点数值最终采用二进制系统表示。

/**
* @author jaymin<br>
* 浮点数无法表示分数
* @since 2021/3/21 15:07
*/

public class PrecisionDemo {
public static void main(String[] args) {
System.out.println(2.0 - 1.1);
// 如何解决?使用BigDecimal
BigDecimal a = BigDecimal.valueOf(2.0);
BigDecimal b = BigDecimal.valueOf(1.1);
System.out.println(a.subtract(b));
}
}

精度

向上转型和向下强转

  • 向上转型
/**
*
*
* @author jaymin
* @since 2021/3/21 15:40
*/

public class ForcedTransfer {

public static void main(String[] args) {
int n = 123456789;
// 整型向上转换丢失了精度
float f = n;
System.out.println(f);
int n1 = 1;
float f1 = 2.2f;
// 不同类型的数值进行运算,将向上转型
System.out.println(n1 + f1);
}
}

这里我们看到两个现象:

  1. 整型可以赋值给浮点型,但是可能会丢失精度.
  2. 整形和浮点数进行相加,先将整型向上转型为float,再进行float的运算.

层级关系:double>float>long>int

  • 面试官经常问的一个细节

此处能否通过编译?

short s1= 1;
s1 = s1 + 1;

答案是不能的,如果我们对小于 int 的基本数据类型(即 char、byte 或 short)执行任何算术或按位操作,这些值会在执行操作之前类型提升为 int,并且结果值的类型为 int。若想重新使用较小的类型,必须使用强制转换(由于重新分配回一个较小的类型,结果可能会丢失精度).
可以简单理解为: 比int类型数值范围小的数做运算,最终都会提升为int,当然,使用final可以帮助你解决这种问题.

  • 正确示例
short s1= 1;
// 1. 第一个种解决办法
s1 = (short) (s1 + 1);
// 2. 第二种解决办法
s1+=1;
        final short a1 = 1;
final short a2 = 2;
short result = a1 + a2;
  • 向下转型(强制转换)

场景: 在程序中得到了一个浮点数,此时将其转成整形,那么你就可以使用强转.

/**
* 数值之间的强转
*
* @author jaymin
* @since 2021/3/21 15:40
*/

public class ForcedTransfer {

public static void main(String[] args) {
double x = 2021.0321;
// 强转为整型
int integerX = (int) x;
System.out.println(integerX);
x = 2021.8888;
// 四舍五入
int round = (int) Math.round(x);
System.out.println(round);
}
}

Result:

2021  
2022

如果强转的过程中,上层的数据类型范围超出了下层的数据类型范围,那么会进行截断.
可以执行以下程序来验证这个问题.

        long l = Long.MAX_VALUE;
int l1 = (int) l;
System.out.println(l1);
int i = 300;
byte b = (byte) i;
// 128*2 = 256,300-256=44
System.out.println(b);

Reuslt:

-1
44

初始值

基本数据类型都会有默认的初始值.

基本类型

初始值

boolean

false

char

\u0000 (null)

byte

(byte) 0

short

(short) 0

int

0

long

0L

float

0.0f

double

0.0d

在定义对象的时候,如果你使用了基本类型,那么类在初始化后,如果你没有显性地赋值,那么就会为默认值。这在某些场景下是不对的(比如你需要在http中传输id,当对方没有传输id时,你应该报错,但是由于使用了基本的数据类型,id拥有了默认值0,那么此时程序就会发生异常)

定义对象的成员,最好使用包装类型,而不是基础类型.

Integer对象的缓存区

在程序中有些值是需要经常使用的,比如定义枚举时,经常会使用1,2,3作为映射值.Java的语言规范JLS中要求将-128到127的值进行缓存。(高速缓存的大小可以由-XX:AutoBoxCacheMax = <size>选项控制。在VM初始化期间,可以在sun.misc.VM类的私有系统属性中设置并保存java.lang.Integer.IntegerCache.high属性。)

  • 使用==比较Integer可能会出现意想不到的结果
    public static void main(String[] args) {
Integer a1 = Integer.valueOf(127);
Integer a2 = Integer.valueOf(127);
System.out.println(a1==a2);
Integer a3 = Integer.valueOf(128);
Integer a4 = Integer.valueOf(128);
System.out.println(a3==a4);
}

Result:

true
false

解决的办法很简单,使用equals来进行比较即可,Integer内部重写了equals和hashcode.

常用的一些转义字符

在字符串中,如果你想让输出的字符串换行,你就需要用到转义字符

转义字符

Unicode

含义

\b

\u0008

退格

\t

\u0009

制表

\n

\u000a

换行

\r

\u000d

回车

\"

\u0022

双引号

\'

\u0027

单引号

\\

\u005c

反斜杠

\\.

-

.

  • 换行输出字符串
    System.out.println("我马上要换行了\n我是下一行");
收起阅读 »

Java文字转图片防爬虫

最近部分页面数据被爬虫疯狂的使用,主要就是采用动态代理IP爬取数据,主要是不控制频率,这个最恶心。因为对方是采用动态代理的方式,所以没什么特别好的防止方式。具体防止抓取数据方案大全,下篇博客我会做一些讲解。本篇也是防爬虫的一个方案。就是部分核心文字采用图片输出...
继续阅读 »

最近部分页面数据被爬虫疯狂的使用,主要就是采用动态代理IP爬取数据,主要是不控制频率,这个最恶心。因为对方是采用动态代理的方式,所以没什么特别好的防止方式。

具体防止抓取数据方案大全,下篇博客我会做一些讲解。本篇也是防爬虫的一个方案。就是部分核心文字采用图片输出。加大数据抓取方的成本。

图片输出需求

上图红色圈起来的数据为图片输出了备案号,就是要达到这个效果,如果数据抓取方要继续使用,必须做图片解析,成本和难度都加到了。也就是我们达到的效果了。

Java代码实现

import javax.imageio.ImageIO;

import java.awt.*;

import java.awt.font.FontRenderContext;

import java.awt.geom.AffineTransform;

import java.awt.geom.Rectangle2D;

import java.awt.image.BufferedImage;

import java.io.File;

import java.nio.file.Paths;

public class ImageDemo {

public static void main(String[] args) throws Exception {

System.out.println(System.currentTimeMillis());

//输出目录

String rootPath = "/Users/sojson/Downloads/";

//这里文字的size,建议设置大一点,其实就是像素会高一点,然后缩放后,效果会好点,最好是你实际输出的倍数,然后缩放的时候,直接按倍数缩放即可。

Font font = new Font("微软雅黑", Font.PLAIN, 130);

createImage("https://www.sojson.com", font, Paths.get(rootPath, "sojson-image.png").toFile());

}

private static int[] getWidthAndHeight(String text, Font font) {

Rectangle2D r = font.getStringBounds(text, new FontRenderContext(

AffineTransform.getScaleInstance(1, 1), false, false));

int unitHeight = (int) Math.floor(r.getHeight());//

// 获取整个str用了font样式的宽度这里用四舍五入后+1保证宽度绝对能容纳这个字符串作为图片的宽度

int width = (int) Math.round(r.getWidth()) + 1;

// 把单个字符的高度+3保证高度绝对能容纳字符串作为图片的高度

int height = unitHeight + 3;

return new int[]{width, height};

}

// 根据str,font的样式以及输出文件目录

public static void createImage(String text, Font font, File outFile)

throws Exception {

// 获取font的样式应用在输出内容上整个的宽高

int[] arr = getWidthAndHeight(text, font);

int width = arr[0];

int height = arr[1];

// 创建图片

BufferedImage image = new BufferedImage(width, height,

BufferedImage.TYPE_INT_BGR);//创建图片画布

//透明背景 the begin

Graphics2D g = image.createGraphics();

image = g.getDeviceConfiguration().createCompatibleImage(width, height, Transparency.TRANSLUCENT);

g=image.createGraphics();

//透明背景 the end

/**

如果你需要白色背景或者其他颜色背景可以直接这么设置,其实就是满屏输出的颜色

我这里上面设置了透明颜色,这里就不用了

*/

//g.setColor(Color.WHITE);

//画出矩形区域,以便于在矩形区域内写入文字

g.fillRect(0, 0, width, height);

/**

* 文字颜色,这里支持RGB。new Color("red", "green", "blue", "alpha");

* alpha 我没用好,有用好的同学可以在下面留言,我开始想用这个直接输出透明背景色,

* 然后输出文字,达到透明背景效果,最后选择了,createCompatibleImage Transparency.TRANSLUCENT来创建。

* android 用户有直接的背景色设置,Color.TRANSPARENT 可以看下源码参数。对alpha的设置

*/

g.setColor(Color.gray);

// 设置画笔字体

g.setFont(font);

// 画出一行字符串

g.drawString(text, 0, font.getSize());

// 画出第二行字符串,注意y轴坐标需要变动

g.drawString(text, 0, 2 * font.getSize());

//执行处理

g.dispose();

// 输出png图片,formatName 对应图片的格式

ImageIO.write(image, "png", outFile);

}

}

输出图片效果:


当然我这里是做了放缩,要不然效果没那么好。

注意点:

其实代码里注释说的已经比较清楚了。主要设置透明色这里。

//透明背景 the begin
Graphics2D g = image.createGraphics();
image = g.getDeviceConfiguration().createCompatibleImage(width, height, Transparency.TRANSLUCENT);
g=image.createGraphics();
//透明背景 the end

Android 参考的颜色值

android.graphics.Color 包含颜色值
Color.BLACK 黑色
Color.BLUE 蓝色
Color.CYAN 青绿色
Color.DKGRAY 灰黑色
Color.GRAY 灰色
Color.GREEN 绿色
Color.LTGRAY 浅灰色
Color.MAGENTA 红紫色
Color.RED 红色
Color.TRANSPARENT 透明
Color.WHITE 白色
Color.YELLOW 黄色




收起阅读 »

一日正则一日神,一直正则一直神

本篇带来 15 个正则使用场景,按需索取,收藏恒等于学会!! 千分位格式化 在项目中经常碰到关于货币金额的页面显示,为了让金额的显示更为人性化与规范化,需要加入货币格式化策略。也就是所谓的数字千分位格式化。 123456789 => ...
继续阅读 »


本篇带来 15 个正则使用场景,按需索取,收藏恒等于学会!!


千分位格式化


在项目中经常碰到关于货币金额的页面显示,为了让金额的显示更为人性化与规范化,需要加入货币格式化策略。也就是所谓的数字千分位格式化。


  1. 123456789 => 123,456,789
  2. 123456789.123 => 123,456,789.123


const formatMoney = (money) => {
return money.replace(new RegExp(`(?!^)(?=(\\d{3})+${money.includes('.') ? '\\.' : '$'})`, 'g'), ',')
}

formatMoney('123456789') // '123,456,789'
formatMoney('123456789.123') // '123,456,789.123'
formatMoney('123') // '123'


想想如果不是用正则,还可以用什么更优雅的方法实现它?


解析链接参数


你一定常常遇到这样的需求,要拿到 url 的参数的值,像这样:





// url <https://qianlongo.github.io/vue-demos/dist/index.html?name=fatfish&age=100#/home>

const name = getQueryByName('name') // fatfish
const age = getQueryByName('age') // 100
 



通过正则,简单就能实现 getQueryByName 函数:


const getQueryByName = (name) => {
const queryNameRegex = new RegExp(`[?&]${name}=([^&]*)(&|$)`)
const queryNameMatch = window.location.search.match(queryNameRegex)
// Generally, it will be decoded by decodeURIComponent
return queryNameMatch ? decodeURIComponent(queryNameMatch[1]) : ''
}

const name = getQueryByName('name')
const age = getQueryByName('age')

console.log(name, age) // fatfish, 100
 



驼峰字符串




JS 变量最佳是驼峰风格的写法,怎样将类似以下的其它声明风格写法转化为驼峰写法?


1. foo Bar => fooBar
2. foo-bar---- => fooBar
3. foo_bar__ => fooBar

正则表达式分分钟教做人:


const camelCase = (string) => {
const camelCaseRegex = /[-_\s]+(.)?/g
return string.replace(camelCaseRegex, (match, char) => {
return char ? char.toUpperCase() : ''
})
}

console.log(camelCase('foo Bar')) // fooBar
console.log(camelCase('foo-bar--')) // fooBar
console.log(camelCase('foo_bar__')) // fooBar
 

小写转大写


这个需求常见,无需多言,用就完事儿啦:


const capitalize = (string) => {
const capitalizeRegex = /(?:^|\s+)\w/g
return string.toLowerCase().replace(capitalizeRegex, (match) => match.toUpperCase())
}

console.log(capitalize('hello world')) // Hello World
console.log(capitalize('hello WORLD')) // Hello World

实现 trim()


trim() 方法用于删除字符串的头尾空白符,用正则可以模拟实现 trim:


const trim1 = (str) => {
return str.replace(/^\s*|\s*$/g, '') // 或者 str.replace(/^\s*(.*?)\s*$/g, '$1')
}

const string = ' hello medium '
const noSpaceString = 'hello medium'
const trimString = trim1(string)

console.log(string)
console.log(trimString, trimString === noSpaceString) // hello medium true
console.log(string)

trim() 方法不会改变原始字符串,同样,自定义实现的 trim1 也不会改变原始字符串;

HTML 转义


防止 XSS 攻击的方法之一是进行 HTML 转义,符号对应的转义字符:


正则处理如下:


const escape = (string) => {
const escapeMaps = {
'&': 'amp',
'<': 'lt',
'>': 'gt',
'"': 'quot',
"'": '#39'
}
// The effect here is the same as that of /[&amp;<> "']/g
const escapeRegexp = new RegExp(`[${Object.keys(escapeMaps).join('')}]`, 'g')
return string.replace(escapeRegexp, (match) => `&${escapeMaps[match]};`)
}

console.log(escape(`
<div>
<p>hello world</p>
</div>
`
))
/*
&lt;div&gt;
&lt;p&gt;hello world&lt;/p&gt;
&lt;/div&gt;
*/


HTML 反转义


有了正向的转义,就有反向的逆转义,操作如下:


const unescape = (string) => {
const unescapeMaps = {
'amp': '&',
'lt': '<',
'gt': '>',
'quot': '"',
'#39': "'"
}
const unescapeRegexp = /&([^;]+);/g
return string.replace(unescapeRegexp, (match, unescapeKey) => {
return unescapeMaps[ unescapeKey ] || match
})
}

console.log(unescape(`
&lt;div&gt;
&lt;p&gt;hello world&lt;/p&gt;
&lt;/div&gt;
`
))
/*
<div>
<p>hello world</p>
</div>
*/


校验 24 小时制


处理时间,经常要用到正则,比如常见的:校验时间格式是否是合法的 24 小时制:


const check24TimeRegexp = /^(?:(?:0?|1)\d|2[0-3]):(?:0?|[1-5])\d$/
console.log(check24TimeRegexp.test('01:14')) // true
console.log(check24TimeRegexp.test('23:59')) // true
console.log(check24TimeRegexp.test('23:60')) // false
console.log(check24TimeRegexp.test('1:14')) // true
console.log(check24TimeRegexp.test('1:1')) // true

校验日期格式


常见的日期格式有:yyyy-mm-dd, yyyy.mm.dd, yyyy/mm/dd 这 3 种,如果有符号乱用的情况,比如2021.08/22,这样就不是合法的日期格式,我们可以通过正则来校验判断:


const checkDateRegexp = /^\d{4}([-\.\/])(?:0[1-9]|1[0-2])\1(?:0[1-9]|[12]\d|3[01])$/

console.log(checkDateRegexp.test('2021-08-22')) // true
console.log(checkDateRegexp.test('2021/08/22')) // true
console.log(checkDateRegexp.test('2021.08.22')) // true
console.log(checkDateRegexp.test('2021.08/22')) // false
console.log(checkDateRegexp.test('2021/08-22')) // false

匹配颜色值


在字符串内匹配出 16 进制的颜色值:


const matchColorRegex = /#(?:[\da-fA-F]{6}|[\da-fA-F]{3})/g
const colorString = '#12f3a1 #ffBabd #FFF #123 #586'

console.log(colorString.match(matchColorRegex))
// [ '#12f3a1', '#ffBabd', '#FFF', '#123', '#586' ]

判断 HTTPS/HTTP


这个需求也是很常见的,判断请求协议是否是 HTTPS/HTTP


const checkProtocol = /^https?:/

console.log(checkProtocol.test('https://medium.com/')) // true
console.log(checkProtocol.test('http://medium.com/')) // true
console.log(checkProtocol.test('//medium.com/')) // false

校验版本号


版本号必须采用 x.y.z 格式,其中 XYZ 至少为一位,我们可以用正则来校验:


// x.y.z
const versionRegexp = /^(?:\d+\.){2}\d+$/

console.log(versionRegexp.test('1.1.1'))
console.log(versionRegexp.test('1.000.1'))
console.log(versionRegexp.test('1.000.1.1'))

获取网页 img 地址


这个需求可能爬虫用的比较多,用正则获取当前网页所有图片的地址。在控制台打印试试,太好用了~~


const matchImgs = (sHtml) => {
const imgUrlRegex = /<img[^>]+src="((?:https?:)?\/\/[^"]+)"[^>]*?>/gi
let matchImgUrls = []

sHtml.replace(imgUrlRegex, (match, $1) => {
$1 && matchImgUrls.push($1)
})
return matchImgUrls
}

console.log(matchImgs(document.body.innerHTML))

格式化电话号码


这个需求也是常见的一匹,用就完事了:


let mobile = '18379836654' 
let mobileReg = /(?=(\d{4})+$)/g

console.log(mobile.replace(mobileReg, '-')) // 183-7983-6654
 











收起阅读 »

4 个 JavaScript 的心得体会

按需所取,冲冲冲ヾ(◍°∇°◍)ノ゙ 一、你能说出 JavaScript 的编程范式吗?   首先要说出:JavaScript 是一门多范式语言!支持面向过程(命令式)、面向对象(OOP)和函数式编程(声明式)。 其次,最重要的是说出:JavaScr...
继续阅读 »




按需所取,冲冲冲ヾ(◍°∇°◍)ノ゙


一、你能说出 JavaScript 的编程范式吗?


 


首先要说出:JavaScript 是一门多范式语言!支持面向过程(命令式)、面向对象(OOP)和函数式编程(声明式)。


其次,最重要的是说出:JavaScript 是通过原型继承(OLOO-对象委托)来实现面向对象(OOP)的;


如果还能说出以下,就更棒了:JavaScript 通过闭包、函数是一等公民、lambda 运算来实现函数式编程的。


如果再进一步,回答出 JavaScript 演进历史,就直接称绝叫好了:JavaScript的语言设计主要受到了Self(一种基于原型的编程语言)和Scheme(一门函数式编程语言)的影响。在语法结构上它又与C语言有很多相似。

 

  • Self 语言 => 基于原型 => JavaScript 用原型实现面向对象编程;
  • Scheme 语言 => 函数式编程语言 => JavaScript 函数式编程;
  • C 语言 => 面向过程 => JavaScript 面向过程编程;




推荐 Eric Elliott 的另外两篇文章,JavaScript 的两大支柱:


  1. 基于原型的继承
  2. 函数式编程



二、什么是函数式编程?




函数式编程是最早出现的编程范式,通过组合运算函数来生成程序。有一些重要的概念:


  • 纯函数
  • 避免副作用
  • 函数组合
  • 高阶函数(闭包)
  • 函数组合
  • 其它函数式编程语言,比如 Lisp、Haskell

本瓜觉得这里最 nb 就是能提到 monad 和延迟执行了~




三、类继承和原型继承有什么区别?





类继承,通过构造函数实现( new 关键字);tips:即使不用 ES6 class,也能实现类继承;


原型继承,实例直接从其他对象继承,工厂函数或 Object.create();


本瓜这里觉得能答出以下就很棒了:


类继承:基于对象复制;


原型继承:基于对象委托;


推荐阅读:


 

四、面向对象和函数式的优缺点




面向对象优点:对象的概念容易理解,方法调用灵活;


面向对象缺点:对象可在多个函数中共享状态、被修改,极有可能会产生“竞争”的情况(多处修改同一对象);


函数式优点:避免变量的共享、修改,纯函数不产生副作用;声明式代码风格更易阅读,更易代码重组、复用;


函数式缺点:过度抽象,可读性降低;学习难度更大,比如 Monad;

 

OK,以上便是本篇分享。点赞关注评论,为好文助力👍 🌏









收起阅读 »

十分详细的diff算法原理解析

diff算法可以看作是一种对比算法,对比的对象是新旧虚拟Dom。顾名思义,diff算法可以找到新旧虚拟Dom之间的差异,但diff算法中其实并不是只有对比虚拟Dom,还有根据对比后的结果更新真实Dom。 虚拟Dom 上面的概念我们提到了虚拟Dom,相信大家对...
继续阅读 »


diff算法可以看作是一种对比算法,对比的对象是新旧虚拟Dom。顾名思义,diff算法可以找到新旧虚拟Dom之间的差异,但diff算法中其实并不是只有对比虚拟Dom,还有根据对比后的结果更新真实Dom



虚拟Dom


上面的概念我们提到了虚拟Dom,相信大家对这个名词并不陌生,下面为大家解释一下虚拟Dom的概念,以及diff算法中为什么要对比虚拟Dom,而不是直接去操作真实Dom。

虚拟Dom,其实很简单,就是一个用来描述真实Dom的对象


它有六个属性,sel表示当前节点标签名,data内是节点的属性,children表示当前节点的其他子标签节点,elm表示当前虚拟节点对应的真实节点(这里暂时没有),key即为当前节点的key,text表示当前节点下的文本,结构类似这样。

 
let vnode = {
sel: 'ul',
   data: {},
children: [
{
sel: 'li', data: { class: 'item' }, text: 'son1'
},
{
sel: 'li', data: { class: 'item' }, text: 'son2'
},    
  ],
   elm: undefined,
   key: undefined,
   text: undefined
}



那么虚拟Dom有什么用呢。我们其实可以把虚拟Dom理解成对应真实Dom的一种状态。当真实Dom发生变化后,虚拟Dom可以为我们提供这个真实Dom变化之前和变化之后的状态,我们通过对比这两个状态,即可得出真实Dom真正需要更新的部分,即可实现最小量更新。在一些比较复杂的Dom变化场景中,通过对比虚拟Dom后更新真实Dom会比直接更新真实Dom的效率高,这也就是虚拟Dom和diff算法真正存在的意义。


h函数


在介绍diff算法原理之前还需要简单让大家了解一下h函数,因为我们要靠它为我们生成虚拟Dom。这个h函数大家应该也比较熟悉,就是render函数里面传入的那个h函数


h函数可以接受多种类型的参数,但其实它内部只干了一件事,就是执行vnode函数。根据传入h函数的参数来决定执行vnode函数时传入的参数。那么vnode函数又是干什么的呢?vnode函数其实也只干了一件事,就是把传入h函数的参数转化为一个对象,即虚拟Dom。

 
// vnode.js
export default function (sel, data, children, text, elm) {
const key = data.key
return {sel, data, children, text, elm, key}
}



执行h函数后,内部会通过vnode函数生成虚拟Dom,h函数把这个虚拟在return出去。


diff对比规则


明确了h函数是干什么的,我们可以简单用h函数生成两个不同的虚拟节点,我们将通过一个简易版的diff算法代码介绍diff对比的具体流程。



// 第一个参数是sel 第二个参数是data 第三个参数是children
const myVnode1 = h("h1", {}, [
 h("p", {key: "a"}, "a"),
 h("p", {key: "b"}, "b"),
]);

const myVnode2 = h("h1", {}, [
 h("p", {key: "c"}, "c"),
 h("p", {key: "d"}, "d"),
]);



patch


比较的第一步就是执行patch,它相当于对比的入口。既然是对比两个虚拟Dom,那么就将两个虚拟Dom作为参数传入patch中。patch的主要作用是对比两个虚拟Dom的根节点,并根据对比结果操作真实Dom。


patch函数的核心代码如下,注意注释。

 
// patch.js

import vnode from "./vnode"
import patchDetails from "./patchVnode"
import createEle from "./createEle"

/**
* @description 用来对比两个虚拟dom的根节点,并根据对比结果操作真实Dom
* @param {*} oldVnode
* @param {*} newVnode
*/
export function patch(oldVnode, newVnode) {
 // 1.判断oldVnode是否为虚拟节点,不是的话转化为虚拟节点
 if(!oldVnode.sel) {
   // 转化为虚拟节点
   oldVnode = vnode(oldVnode.tagName.toLowerCase(), {}, [], undefined, oldVnode)
}

 // 2.判断oldVnode和newVnode是否为同一个节点
 if(oldVnode.key == newVnode.key && oldVnode.sel == newVnode.sel) {
   console.log('是同一个节点')
   // 比较子节点
   patchDetails(oldVnode, newVnode)
}else {
   console.log('不是同一个节点')
   // 插入newVnode
   const newNode = createEle(newVnode) // 插入之前需要先将newVnode转化为dom
   oldVnode.elm.parentNode.insertBefore(newNode, oldVnode.elm) // 插入操作
   // 删除oldVnode
   oldVnode.elm.parentNode.removeChild(oldVnode.elm)
}
}

// createEle.js

/**
* @description 根据传入的虚拟Dom生成真实Dom
* @param {*} vnode
* @returns real node
*/
export default function createEle (vnode) {
 const realNode = document.createElement(vnode.sel)

 // 子节点转换
 if(vnode.text && (vnode.children == undefined || (vnode.children && vnode.children.length == 0)) ) {
   // 子节点只含有文本
   realNode.innerText = vnode.text  
}else if(Array.isArray(vnode.children) && vnode.children.length > 0) {
   // 子节点为其他虚拟节点 递归添加node
   for(let i = 0; i < vnode.children.length; i++) {
     const childNode = createEle(vnode.children[i])
     realNode.appendChild(childNode)
  }
}

 // 补充vnode的elm属性
 vnode.elm = realNode

 return vnode.elm
}



patchVnode


patchVnode用来比较两个虚拟节点的子节点并更新其子节点对应的真实Dom节点



// patchVnode.js

import updateChildren from "./updateChildren"
import createEle from "./createEle"

/**
* @description 比较两个虚拟节点的子节点(children or text) 并更新其子节点对应的真实dom节点
* @param {*} oldVnode
* @param {*} newVnode
* @returns
*/
export function patchDetails(oldVnode, newVnode) {
 // 判断oldVnode和newVnode是否为同一个对象, 是的话直接不用比了
 if(oldVnode == newVnode) return

 // 默认newVnode和oldVnode只有text和children其中之一,真实的源码这里的情况会更多一些,不过大同小异。

 if(hasText(newVnode)) {
   // newVnode有text但没有children

   /**
    * newVnode.text !== oldVnode.text 直接囊括了两种情况
    * 1.oldVnode有text无children 但是text和newVnode的text内容不同
    * 2.oldVnode无text有children 此时oldVnode.text为undefined
    * 两种情况都可以通过innerText属性直接完成dom更新
    * 情况1直接更新text 情况2相当于去掉了children后加了新的text
    */
   if(newVnode.text !== oldVnode.text) {
     oldVnode.elm.innerText = newVnode.text
  }

}else if(hasChildren(newVnode)) {
   // newVnode有children但是没有text
   
   if(hasText(oldVnode)) {
     // oldVnode有text但是没有children
     
     oldVnode.elm.innerText = '' // 删除oldVnode的text
     // 添加newVnode的children
     for(let i = 0; i < newVnode.children.length; i++) {
       oldVnode.elm.appendChild(createEle(newVnode.children[i]))
    }

  }else if(hasChildren(oldVnode)) {
     // oldVnode有children但是没有text

     // 对比两个节点的children 并更新对应的真实dom节点
     updateChildren(oldVnode.children, newVnode.children, oldVnode.elm)
  }
}
}

// 有children没有text
function hasChildren(node) {
 return !node.text && (node.children && node.children.length > 0)
}

// 有text没有children
function hasText(node) {
 return node.text && (node.children == undefined || (node.children && node.children.length == 0))
}



updateChildren


该方法是diff算法中最复杂的方法(大的要来了)。对应上面patchVnodeoldVnodenewVnode都有children的情况。


首先我们需要介绍一下这里的对比规则。


对比过程中会引入四个指针,分别指向oldVnode子节点列表中的第一个节点和最后一个节点(后面我们简称为旧前旧后)以及指向newVnode子节点列表中的第一个节点和最后一个节点(后面我们简称为新前新后


对比时,每一次对比按照以下顺序进行命中查找


  • 旧前与新前节点对比(1)
  • 旧后与新后节点对比(2)
  • 旧前与新后节点对比(3)
  • 旧后与新前节点对比(4)

上述四种情况,如果某一种情况两个指针对应的虚拟Dom相同,那么我们称之为命中。命中后就不会接着查找了,指针会移动,(还有可能会操作真实Dom,3或者4命中时会操作真实Dom移动节点)之后开始下一次对比。如果都没有命中,则去oldVnode子节点列表循环查找当前新前指针所指向的节点,如果查到了,那么操作真实Dom移动节点,没查到则新增真实Dom节点插入。


这种模式的对比会一直进行,直到满足了终止条件。即旧前指针移动到了旧后指针的后面或者新前指针移动到了新后指针的后面,我们可以理解为旧子节点先处理完毕新子节点处理完毕。那么我们可以预想到新旧子节点中总会有其一先处理完,对比结束后,我们会根据没有处理完子节点的那一对前后指针决定是要插入真实Dom还是删除真实Dom。


  • 如果旧子节点先处理完了,新子节点有剩余,说明有要新增的节点。将根据最终新前新后之间的虚拟节点执行插入操作
  • 如果新子节点先处理完了,旧子节点有剩余,说明有要删除的节点。将根据最终旧前旧后之间的虚拟节点执行删除操作

下面将呈现代码,注意注释

 
// updateChildren.js

import patchDetails from "./patchVnode"
import createEle from "./createEle";

/**
* @description 对比子节点列表并更新真实Dom
* @param {*} oldCh 旧虚拟Dom子节点列表
* @param {*} newCh 新虚拟Dom子节点列表
* @param {*} parent 新旧虚拟节点对应的真实Dom
* @returns
*/

export default function updateChildren(oldCh, newCh, parent) {
 // 定义四个指针 旧前 旧后 新前 新后 (四个指针两两一对,每一对前后指针所指向的节点以及其之间的节点为未处理的子节点)
 let oldStartIndex = 0;
 let oldEndIndex = oldCh.length - 1;
 let newStartIndex = 0;
 let newEndIndex = newCh.length - 1;

 // 四个指针对应的节点
 let oldStartNode = oldCh[oldStartIndex];
 let oldEndNode = oldCh[oldEndIndex];
 let newStartNode = newCh[newStartIndex];
 let newEndNode = newCh[newEndIndex];

 // oldCh中每个子节点 key 与 index的哈希表 用于四种对比规则都不匹配的情况下在oldCh中寻找节点
 const keyMap = new Map();

 /**
  * 开始遍历两个children数组进行细节对比
  * 对比规则:旧前-新前 旧后-新后 旧前-新后 旧后-新前
  * 对比之后指针进行移动
  * 直到指针不满足以下条件 意味着有一对前后指针之间再无未处理的子节点 则停止对比 直接操作DOM
  */

 while (oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex) {
   // 这四种情况是为了让指针在移动的过程中跳过空节点
   if (oldStartNode == undefined) {
     oldStartNode = oldCh[++oldStartIndex];
  } else if (oldEndNode == undefined) {
     oldEndNode = oldCh[--oldEndIndex];
  } else if (newStartNode == undefined) {
     newStartNode = newCh[++newStartIndex];
  } else if (newEndNode == undefined) {
     newEndNode = newCh[--newEndIndex];
  } else if (isSame(oldStartNode, newStartNode)) {
     console.log("method1");
     // 旧前-新前是同一个虚拟节点

     // 两个子节点再对比他们的子节点并更新dom (递归切入点)
     patchDetails(oldStartNode, newStartNode);
     // 指针移动
     oldStartNode = oldCh[++oldStartIndex];
     newStartNode = newCh[++newStartIndex];
  } else if (isSame(oldEndNode, newEndNode)) {
     console.log("method2");
     // 旧后-新后是同一个虚拟节点

     // 两个子节点再对比他们的子节点并更新dom (递归切入点)
     patchDetails(oldEndNode, newEndNode);
     // 指针移动
     oldEndNode = oldCh[--oldEndIndex];
     newEndNode = newCh[--newEndIndex];
  } else if (isSame(oldStartNode, newEndNode)) {
     console.log("method3");
     // 旧前-新后是同一个虚拟节点

     // 两个子节点再对比他们的子节点并更新dom (递归切入点)
     patchDetails(oldStartNode, newEndNode);

     /**
      * 这一步多一个移动(真实)节点的操作
      * 需要把当前指针所指向的子节点 移动到 oldEndIndex所对应真实节点之后(也就是未处理真实节点的尾部)
      * 注意:这一步是在操作真实节点
      */
     parent.insertBefore(oldStartNode.elm, oldEndNode.elm.nextSibling);

     // 指针移动
     oldStartNode = oldCh[++oldStartIndex];
     newEndNode = newCh[--newEndIndex];
  } else if (isSame(oldEndNode, newStartNode)) {
     console.log("method4");
     // 旧后-新前 是同一个虚拟节点

     // 两个子节点再对比他们的子节点并更新dom (递归切入点)
     patchDetails(oldEndNode, newStartNode);
     /**
      * 这一步多一个移动(真实)节点的操作
      * 与method3不同在移动位置
      * 需要把当前指针所指向的子节点 移动到 oldStartIndex所对应真实节点之前(也就是未处理真实节点的顶部)
      * 注意:这一步是在操作真实节点
      */
     parent.insertBefore(oldEndNode.elm, oldCh[oldStartIndex].elm);

     // 指针移动
     oldEndNode = oldCh[--oldEndIndex];
     newStartNode = newCh[++newStartIndex];
  } else {
     console.log("does not match");
     // 四种规则都不匹配

     // 生成keyMap
     if (keyMap.size == 0) {
       for (let i = oldStartIndex; i <= oldEndIndex; i++) {
         if (oldCh[i].key) keyMap.set(oldCh[i].key, i);
      }
    }

     // 在oldCh中搜索当前newStartIndex所指向的节点
     if (keyMap.has(newStartNode.key)) {
       // 搜索到了

       // 先获取oldCh中该虚拟节点
       const oldMoveNode = oldCh[keyMap.get(newStartNode.key)];
       // 两个子节点再对比他们的子节点并更新dom (递归切入点)
       patchDetails(oldMoveNode, newStartNode);

       // 移动这个节点(移动的是真实节点)
       parent.insertBefore(oldMoveNode.elm, oldStartNode.elm);

       // 该虚拟节点设置为undefined(还记得最开始的四个条件吗,因为这里会将子节点制空,所以加了那四个条件)
       oldCh[keyMap.get(newStartNode.key)] = undefined;
         
    } else {
       // 没搜索到 直接插入
       parent.insertBefore(createEle(newStartNode), oldStartNode.elm);
    }

     // 指针移动
     newStartNode = newCh[++newStartIndex];
  }
}

 /**
  * 插入和删除节点
  * while结束后 有一对前后指针之间仍然有未处理的子节点,那么就会进行插入或者删除操作
  * oldCh的双指针中有未处理的子节点,进行删除操作
  * newCh的双指针中有未处理的子节点,进行插入操作
  */
 if (oldStartIndex <= oldEndIndex) {
   // 删除
   for (let i = oldStartIndex; i <= oldEndIndex; i++) {
     // 加判断是因为oldCh[i]有可能为undefined
     if(oldCh[i]) parent.removeChild(oldCh[i].elm);
  }
} else if (newStartIndex <= newEndIndex) {
   /**
    * 插入
    * 这里需要注意的点是从哪里插入,也就是appendChild的第二个参数
    * 应该从oldStartIndex对应的位置插入
    */
   for (let i = newStartIndex; i <= newEndIndex; i++) {
     // oldCh[oldStartIndex]存在是从头部插入
     parent.insertBefore(createEle(newCh[i]), oldCh[oldStartIndex] ? oldCh[oldStartIndex].elm : undefined);
  }
}
}

// 判断两个虚拟节点是否为同一个虚拟节点
function isSame(a, b) {
 return a.sel == b.sel && a.key == b.key;
}



这里的逻辑稍微比较复杂,需要大家多理几遍,必要的话,自己手画一张图自己移动一下指针。着重需要注意的地方是操作真实Dom时,插入、移动节点应该将节点从哪里插入或者移动到哪里,其实基本插入到oldStartIndex对应的真实Dom的前面,除了第三种命中后的移动节点操作,是移动到oldEndIndex所对应真实节点之后


总结


由于diff算法对比的是虚拟Dom,而虚拟Dom是呈树状的,所以我们可以发现,diff算法中充满了递归。总结起来,其实diff算法就是一个 patch —> patchVnode —> updateChildren —> patchVnode —> updateChildren —> patchVnode这样的一个循环递归的过程。


这里再提一嘴key,我们面试中经常会被问到vue中key的作用。根据上面我们分析的,key的主要作用其实就是对比两个虚拟节点时,判断其是否为相同节点。加了key以后,我们可以更为明确的判断两个节点是否为同一个虚拟节点,是的话判断子节点是否有变更(有变更更新真实Dom),不是的话继续比。如果不加key的话,如果两个不同节点的标签名恰好相同,那么就会被判定为同一个节点(key都为undefined),结果一对比这两个节点的子节点发现不一样,这样会凭空增加很多对真实Dom的操作,从而导致页面更频繁地重绘和回流。


所以我认为合理利用key可以有效减少真实Dom的变动,从而减少页面重绘和回流的频率,进而提高页面更新的效率。

 








收起阅读 »

关于 Axios 的再再再封装,总是会有所不一样

特性 class 封装 可以多次实例化默认全局可以共用一个实例对象可以实例化多个对象,实例化时可以配置该实例特有的 headers根据各个接口的要求不同,也可以针对该接口进行配置设置请求拦截和响应拦截,这个都是标配了拦截处理系统响应状态码对应的提示语 拦截器 ...
继续阅读 »


特性


  • class 封装 可以多次实例化
  • 默认全局可以共用一个实例对象
  • 可以实例化多个对象,实例化时可以配置该实例特有的 headers
  • 根据各个接口的要求不同,也可以针对该接口进行配置
  • 设置请求拦截和响应拦截,这个都是标配了
  • 拦截处理系统响应状态码对应的提示语

拦截器


首先为防止多次执行响应拦截,这里我们将拦截器设置在类外部,如下:

import axios from "axios";

// 添加请求拦截器
axios.interceptors.request.use((config) => {
// 在发送请求之前做些什么 添加 token 等鉴权功能
return config;
}, function (error) {
// 对请求错误做些什么
return Promise.reject(error);
});

// 添加响应拦截器
axios.interceptors.response.use((res) => {
const {status} = res;
// 对错误状态提示进行处理
let message = '';
if (status < 200 || status >= 300) {
// 处理http错误,抛到业务代码
message = showResState(status)
if (typeof res.data === 'string') {
res.data = {code: status, message: message}
} else {
res.data.code = status
res.data.message = message
}
}
return res.data;
}, function (error) {
// 对响应错误做点什么
return Promise.reject(error);
});

function showResState(state) {
let message = '';
// 这里只做部分常见的示例,具体根据需要进行配置
switch (state) {
case 400:
message = '请求错误(400)'
break
case 401:
message = '未授权,请重新登录(401)'
break
case 403:
message = '拒绝访问(403)'
break
case 404:
message = '请求出错(404)'
break
case 500:
message = '服务器错误(500)'
break
case 501:
message = '服务未实现(501)'
break
case 502:
message = '网络错误(502)'
break
case 503:
message = '服务不可用(503)'
break
default:
message = `连接出错(${state})!`
}
return `${message},请检查网络或联系网站管理员!`
}



封装主体


这里为了方便起见,实例化对象处理的其实就是传入的配置文件,而封装的方法还是按照 axios 原生的方法处理的。为了方便做校验在接口上都统一增加了客户端发起请求的时间,以方便服务端做校验。配置参数可参照文档 axios 配置文档

// 构造函数
constructor(config) {
// 公共的 header
let defaultHeaders = {
'Content-Type': 'application/json;charset=UTF-8',
'Accept': 'application/json', // 通过头指定,获取的数据类型是JSON 'application/json, text/plain, */*',
'Authorization': null
}

let defaultConfig = {
headers: defaultHeaders
}

// 合并配置文件
if (config) {
for (let i in config) {
if (i === 'headers' && config.headers) {
for (let i in config.headers) {
defaultHeaders[i] = config.headers[i];
}
defaultConfig.headers = defaultHeaders;
} else {
defaultConfig[i] = config[i];
}
}
}
// 全局使用
this.init = axios;
this.config = defaultConfig;
}



get 方法的配置

// Get 请求
get(url, params = {}, headers = {}) {

params.time = Date.now();

// 合并 headers
if (headers) {
for (let i in headers) {
this.config.headers[i] = headers[i];
}
}
return new Promise((resolve, reject) => {
// axios.get(url[, config])
this.init.get(url, {
...this.config,
...{params: params}
}).then(response => {
resolve(response);
})
.catch(error => {
reject(error)
}).finally(() => {
})
});
}



post 请求

// POST 请求
post(url, params = {}, headers = {}) {

url = url + '?time=' + Date.now();

if (headers) {
for (let i in headers) {
this.config.headers[i] = headers[i];
}
}

return new Promise((resolve, reject) => {
// axios.post(url[, data[, config]])
this.init.post(url, params, this.config).then(response => {
resolve(response);
})
.catch(error => {
reject(error)
}).finally(() => {
})
});
}



PUT 请求

// PUT 请求
put(url, params = {}, headers = {}) {

url = url + '?time=' + Date.now();

if (headers) {
for (let i in headers) {
this.config.headers[i] = headers[i];
}
}

return new Promise((resolve, reject) => {
// axios.put(url[, data[, config]])
this.init.put(url, params, this.config).then(response => {
resolve(response);
})
.catch(error => {
reject(error)
}).finally(() => {
})
});
}



Delete 请求

// Delete 请求
delete(url, headers = {}) {
if (headers) {
for (let i in headers) {
this.config.headers[i] = headers[i];
}
}
return new Promise((resolve, reject) => {
// axios.delete(url[, config])
this.init.delete(url, {
...this.config,
}).then(response => {
resolve(response);
})
.catch(error => {
reject(error)
}).finally(() => {
})
});
}



>>使用


完整的代码的代码在文末会贴出来,这里简单说下如何使用

// @/api/index.js
import Http,{Axios} from '@/api/http'; // Axios 数据请求方法

// ① 可以使用文件中实例化的公共对象 Axios


// ②也可以单独实例化使用
const XHttp = new Http({
headers: {
'x-token': 'xxx'
}
});


export const getArticles = (params={}) => {
return XHttp.get('https://api.ycsnews.com/api/v1/blog/getArticles', params);
}

export const getArticle = (params={}) => {
return Axios.get('https://api.ycsnews.com/api/v1/blog/getArticles', params);
}



在页面中使用

// @/views/home.vue
import { getArticles,getArticle } from '@/api/index.js'

// 两个方法名差一个字母 's'
getArticle({id:1234444}).then((res) => {
console.log(res)
})
.catch(err => {
console.log(err)
})

getArticles({id:1234444}).then((res) => {
console.log(res)
})
.catch(err => {
console.log(err)
})



完整代码

// @/api/http.js
/**
* 说明:
* 1.多实例化,可以根据不同的配置进行实例化,满足不同场景的需求
* 2.多实例化情况下,可共用公共配置
* 3.请求拦截,响应拦截 对http错误提示进行二次处理
* 4.接口可单独配置 header 满足单一接口的特殊需求
* body 直传字符串参数,需要设置 headers: {"Content-Type": "text/plain"}, 传参:System.licenseImport('"'+this.code+'"');
* import Http,{Axios} from '../http'; // Http 类 和 Axios 数据请求方法 如无特殊需求 就使用实例化的 Axios 方法进行配置 有特殊需求再进行单独实例化
*
*
*/
import axios from "axios";

// 添加请求拦截器
axios.interceptors.request.use((config) => {
// 在发送请求之前做些什么 添加 token 等鉴权功能
return config;
}, function (error) {
// 对请求错误做些什么
return Promise.reject(error);
});

// 添加响应拦截器
axios.interceptors.response.use((res) => {
const {status} = res;
// 对错误状态提示进行处理
let message = '';
if (status < 200 || status >= 300) {
// 处理http错误,抛到业务代码
message = showResState(status)
if (typeof res.data === 'string') {
res.data = {code: status, message: message}
} else {
res.data.code = status
res.data.message = message
}
}
return res.data;
}, function (error) {
// 对响应错误做点什么
return Promise.reject(error);
});

function showResState(state) {
let message = '';
// 这里只做部分常见的示例,具体根据需要进行配置
switch (state) {
case 400:
message = '请求错误(400)'
break
case 401:
message = '未授权,请重新登录(401)'
break
case 403:
message = '拒绝访问(403)'
break
case 404:
message = '请求出错(404)'
break
case 500:
message = '服务器错误(500)'
break
case 501:
message = '服务未实现(501)'
break
case 502:
message = '网络错误(502)'
break
case 503:
message = '服务不可用(503)'
break
default:
message = `连接出错(${state})!`
}
return `${message},请检查网络或联系网站管理员!`
}

class Http {
constructor(config) {
// 公共的 header
let defaultHeaders = {
'Content-Type': 'application/json;charset=UTF-8',
'Accept': 'application/json', // 通过头指定,获取的数据类型是JSON 'application/json, text/plain, */*',
'Authorization': null
}

let defaultConfig = {
headers: defaultHeaders
}

// 合并配置文件
if (config) {
for (let i in config) {
if (i === 'headers' && config.headers) {
for (let i in config.headers) {
defaultHeaders[i] = config.headers[i];
}
defaultConfig.headers = defaultHeaders;
} else {
defaultConfig[i] = config[i];
}
}
}
this.init = axios;
this.config = defaultConfig;
}

// Get 请求
get(url, params = {}, headers = {}) {
// 合并 headers
if (headers) {
for (let i in headers) {
this.config.headers[i] = headers[i];
}
}
return new Promise((resolve, reject) => {
// axios.get(url[, config])
this.init.get(url, {
...this.config,
...{params: params}
}).then(response => {
resolve(response);
})
.catch(error => {
reject(error)
}).finally(() => {
})
});
}

// POST 请求
post(url, params = {}, headers = {}) {
if (headers) {
for (let i in headers) {
this.config.headers[i] = headers[i];
}
}

return new Promise((resolve, reject) => {
// axios.post(url[, data[, config]])
this.init.post(url, params, this.config).then(response => {
resolve(response);
})
.catch(error => {
reject(error)
}).finally(() => {
})
});
}

// PUT 请求
put(url, params = {}, headers = {}) {
if (headers) {
for (let i in headers) {
this.config.headers[i] = headers[i];
}
}

return new Promise((resolve, reject) => {
// axios.put(url[, data[, config]])
this.init.put(url, params, this.config).then(response => {
resolve(response);
})
.catch(error => {
reject(error)
}).finally(() => {
})
});
}


// Delete 请求
delete(url, headers = {}) {
if (headers) {
for (let i in headers) {
this.config.headers[i] = headers[i];
}
}
return new Promise((resolve, reject) => {
// axios.delete(url[, config])
this.init.delete(url, {
...this.config,
}).then(response => {
resolve(response);
})
.catch(error => {
reject(error)
}).finally(() => {
})
});
}
}

export default Http;

// 无特殊需求的只需使用这个一个对象即可 公共 header 可在此配置, 如需多个实例 可按照此方式创建多个进行导出
export const Axios = new Http({
baseURL:'https://docs.ycsnews.com',
headers: {
'x-http-token': 'xxx'
}
});











收起阅读 »

JS堆栈内存的运行机制也需时常回顾咀嚼

在js引擎中对变量的存储主要有两个位置,堆内存和栈内存。栈内存主要用于存储各种基本类型的变量,包括Boolean、Number、String、Undefined、Null, 以及对象变量的指针(地址值)。栈内存中的变量一般都是已知大小或者有范围上限的,算作...
继续阅读 »



在js引擎中对变量的存储主要有两个位置,堆内存和栈内存。栈内存主要用于存储各种基本类型的变量,包括Boolean、Number、String、Undefined、Null,


以及对象变量的指针(地址值)。栈内存中的变量一般都是已知大小或者有范围上限的,算作一种简单存储。而堆内存主要负责像对象Object这种变量类型的存储,对于大小这方面,一般都是未知的。

栈内存 ECStack


栈内存ECStack(Execution Context Stack)(作用域)




栈内存ECStack(Execution Context Stack)(作用域)



JS之所以能够在浏览器中运行,是因为浏览器给JS提供了执行的环境栈内存





浏览器会在计算机内存中分配一块内存,专门用来供代码执行=》栈内存ECStack(Execution Context Stack)执行环境栈,每打开一个网页都会生成一个全新的ECS


ECS的作用




  • 提供一个供JS代码自上而下执行的环境(代码都在栈中执行)
  • 由于基本数据类型值比较简单,他们都是直接在栈内存中开辟一个位置,把值直接存储进去的,当栈内存被销毁,存储的那些基本值也都跟着销毁



堆内存


堆内存:引用值对应的空间,堆内存是区别于栈区、全局数据区和代码区的另一个内存区域。堆允许程序在运行时动态地申请某个大小的内存空间。


存储引用类型值(对象:键值对, 函数:代码字符串),当内存释放销毁,那么这个引用值彻底没了
堆内存释放


当堆内存没有被任何得变量或者其他东西所占用,浏览器会在空闲的时候,自主进行内存回收,把所有不被占用得内存销毁掉


谷歌浏览器(webkit),每隔一定时间查找对象有没有被占用
引用计数器:当对象引用为0时释放它

收起阅读 »

大家好啊,新手一枚,请多关照哈

大家好啊,新手一枚,请多关照哈。。。。。。。。。。

大家好啊,新手一枚,请多关照哈。。。。。。。。。。

使用环信提供的uni-app Demo,快速实现一对一单聊

如何利用环信提供的uni-app Demo,快速实现一对一单聊?真真保姆级别教程! 写在前面: 1)因为初期直接下载环信的uni-app的demo源码直接看可能一头雾水,因此写下这篇文档帮助项目周期较急,想要快速集成环信uni-app端IM开发者小伙伴。2)这...
继续阅读 »

如何利用环信提供的uni-app Demo,快速实现一对一单聊?真真保姆级别教程!


写在前面:


1)因为初期直接下载环信的uni-app的demo源码直接看可能一头雾水,因此写下这篇文档帮助项目周期较急,想要快速集成环信uni-app端IM开发者小伙伴。

2)这篇文档只帮助实现单聊功能,群组功能其实与单聊基本相仿,可以在参考单聊后的流程,自行看看源码实现群聊。

3)尽管已经从原项目中剥离了很多无关核心逻辑的代码,但仍然可能还有一些小伙伴本身项目中用不到的代码,因此化繁去简这一步就不再本文档中展示,请在按照这篇文档,完成核心逻辑后自行进行优化。

然后就不多啰嗦了,下面开搞~


1、 下载环信uni-app demo 源码 源码地址:

https://github.com/easemob/webim-uniapp-demo


2、在编辑器中打开项目,建议进行一次试运行确保demo源码可以正常跑起来,大概率是可以正常跑起来的。
运行没有问题之后,强烈建议先在README.md中了解一下demo中的目录结构,做个初期的了解。 参考实际目录结构如图:



3、由于是作为演示,所以我只是简单的新建一个示例项目,写一个简易的聊天界面界面作为即时通讯功能的入口。
仿咸鱼在线一对一沟通界面入口:


这个就是默认的项目目录(该示例项目为Vue)


4、这一步就正式开始从环信的Uni-App demo中CV代码到自有的项目中:

setp1:先把最核心的SDKcopy进来,复制demo源码的 newSDK 这个文件到项目中(demo中的SDK其实有很多个,建议选择版本号最新的一个即可)。

自己的项目目录如图:


setp2:复制demo中的 utils 文件到项目中。

utils目录结构如图:


其中 WebIMConfig.js 是作为SDK的Config配置使用,WebIM.js 是针对于SDK进行初始化,并挂载一些常用方法,Dispatcher.js broadcast.js Observer.js 是用作发布订阅的使用,因为源码中有所使用,所以这几个文件都是必须引入。


setp3:copy static 静态资源包到自己的项目当中,因为组件的聊天界面里面的emoji是图片所以要用到。

此时的目录结构如图:



setp4:copy uview-ui进来,因为组件中有用到这个包的UI组件,使用过UI组件的朋友应该都知道,除了这个还要引入相应的样式,这个组件的README.md中,说明了要进行什么样的配置,这里就不再一一赘述。




setp5:在示例项目中新建components文件夹,分别copy demo当中的 components 文件夹下的整个 chat 组件,pages 文件夹下的chatroom组件,由于示例项目中的App.vue组件没有自己的其他逻辑,所以我直接将demo中的Appp.vue中的所有代码全部copy到示例项目中。



PS:特别说明demo的App.vue尽管不是每一行代码都是必要的,但是如果要做优化或者copy,确保import的引入部分先全部粘贴上,conn.listen 监听回调也一定要先copy上。确保先跑起来的原则,优化放在之后。


此时的目录结构如图:


以上步骤执行完成之后便可以跑一把试试了,运行起来查看一下是否有什么引入类型或者其他类型的报错在集中解决一下。


下图是运行到小程序的界面:




VM22 WAService.js:2 TypeError: Cannot read property 'forceUpdate' of undefined

这个报错原因是没有在HbuilderX配置微信小程序的AppId。

5、开始登陆环信,执行跳转至chat聊天界面进行单聊消息的发送测试


step1:确保先登陆环信(能到这一步相信也都已经注册了环信的账号创建了应用,或者利用环信官方demo注册了测试id)

我在示例项目中是在index.vue写的入口页面,因此登陆也写在了这个页面,示例代码截图可以看下图:




step2:运行项目看 App.vue中的监听回调--onOpened回调是否触发(这一步很重要,因为所有功能性接口调用都必须保证环信的连接成功)






看到代码中的打印输出之后证明已成功的建立websocket连接,正式可以开始下一步跳转至chat页面。


step3: 给引入的chatroom组件在pages.json中配置对应的路由映射,并在pages/index.vue组件再给"我想要"按钮添加事件执行路由跳转至chatroom组件。

index.vue中的示例代码如图:




chatroom组件不需要执行其他操作,onload直接将路由传递的参数进行了接收:


step:4 跑起来看看吧!

这个时候顺利的话你会跳转至这样的一个页面,有可能出现这样一个报错:



这是因为demo重写了一个setData并放在了main.jsmixin里面,手动加上去即可,代码所在位置如图:



6、重新编译启动,点击进入chat页面测试聊天,就没问题了!




不排除列位遇到一些其他阻力导致没有成功跑起来,如果还遇到有其他问题,可以在评论区友好交流,我看到会帮忙解决的。


源码下载: uni-app-singleChat-demo.zip

收起阅读 »

Flutter 小技巧之 MediaQuery 和 build 优化你不知道的秘密

今天这篇文章的目的是补全大家对于 MediaQuery 和对应 rebuild 机制的基础认知,相信本篇内容对你优化性能和调试 bug 会很有帮助。 Flutter 里大家应该都离不开 MediaQuery ,比如通过 MediaQuery.of(cont...
继续阅读 »

今天这篇文章的目的是补全大家对于 MediaQuery 和对应 rebuild 机制的基础认知,相信本篇内容对你优化性能和调试 bug 会很有帮助


Flutter 里大家应该都离不开 MediaQuery ,比如通过 MediaQuery.of(context).size 获取屏幕大小 ,或者通过 MediaQuery.of(context).padding.top 获取状态栏高度,那随便使用 MediaQuery.of(context) 会有什么问题吗?


首先我们需要简单解释一下,通过 MediaQuery.of 获取到的 MediaQueryData 里有几个很类似的参数:



  • viewInsets被系统用户界面完全遮挡的部分大小,简单来说就是键盘高度

  • padding简单来说就是状态栏和底部安全区域,但是 bottom 会因为键盘弹出变成 0

  • viewPadding padding 一样,但是 bottom 部分不会发生改变


举个例子,在 iOS 上,如下图所示,在弹出键盘和未弹出键盘的情况下,可以看到 MediaQueryData 里一些参数的变化:



  • viewInsets 在没有弹出键盘时是 0,弹出键盘之后 bottom 变成 336

  • padding 在弹出键盘的前后区别, bottom 从 34 变成了 0

  • viewPadding 在键盘弹出前后数据没有发生变化


image-20220624115935998



可以看到 MediaQueryData 里的数据是会根据键盘状态发生变化,又因为 MediaQuery 是一个 InheritedWidget ,所以我们可以通过 MediaQuery.of(context) 获取到顶层共享的 MediaQueryData



那么问题来了,InheritedWidget 的更新逻辑,是通过登记的 context 来绑定的,也就是 MediaQuery.of(context) 本身就是一个绑定行为,然后 MediaQueryData 又和键盘状态有关系,所以:键盘的弹出可能会导致使用 MediaQuery.of(context) 的地方触发 rebuild,举个例子:


如下代码所示,我们在 MyHomePage 里使用了 MediaQuery.of(context).size 并打印输出,然后跳转到 EditPage 页面,弹出键盘 ,这时候会发生什么情况?



class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
print("######### MyHomePage ${MediaQuery.of(context).size}");
return Scaffold(
body: Container(
alignment: Alignment.center,
child: InkWell(
onTap: () {
Navigator.of(context).push(CupertinoPageRoute(builder: (context) {
return EditPage();
}));
},
child: new Text(
"Click",
style: TextStyle(fontSize: 50),
),
),
),
);
}
}

class EditPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: new Text("ControllerDemoPage"),
),
extendBody: true,
body: Column(
children: [
new Spacer(),
new Container(
margin: EdgeInsets.all(10),
child: new Center(
child: new TextField(),
),
),
new Spacer(),
],
),
);
}
}

如下图 log 所示 , 可以看到在键盘弹起来的过程,因为 bottom 发生改变,所以 MediaQueryData 发生了改变,从而导致上一级的 MyHomePage 虽然不可见,但是在键盘弹起的过程里也被不断 build 。


image-20220624121917686



试想一下,如果你在每个页面开始的位置都是用了 MediaQuery.of(context) ,然后打开了 5 个页面,这时候你在第 5 个页面弹出键盘时,也触发了前面 4 个页面 rebuild,自然而然可能就会出现卡顿。



那么如果我不在 MyHomePage 的 build 方法直接使用 MediaQuery.of(context) ,那在 EditPage 里弹出键盘是不是就不会导致上一级的 MyHomePage 触发 build



答案是肯定的,没有了 MediaQuery.of(context).size 之后, MyHomePage 就不会因为 EditPage 里的键盘弹出而导致 rebuild。



所以小技巧一:要慎重在 Scaffold 之外使用 MediaQuery.of(context) ,可能你现在会觉得奇怪什么是 Scaffold 之外,没事后面继续解释。


那到这里有人可能就要说了:我们通过 MediaQuery.of(context) 获取到的 MediaQueryData ,不就是对应在 MaterialApp 里的 MediaQuery 吗?那它发生改变,不应该都会触发下面的 child 都 rebuild 吗?



这其实和页面路由有关系,也就是我们常说的 PageRoute 的实现



如下图所示,因为嵌套结构的原因,事实上弹出键盘确实会导致 MaterialApp 下的 child 都触发 rebuild ,因为设计上 MediaQuery 就是在 Navigator 上面,所以弹出键盘自然也就触发 Navigator 的 rebuild


image-20220624141749056


那正常情况下 Navigator 都触发 rebuild 了,为什么页面不会都被 rebuild 呢


这就和路由对象的基类 ModalRoute 有关系,因为在它的内部会通过一个 _modalScopeCache 参数把 Widget 缓存起来,正如注释所说:



缓存区域不随帧变化,以便得到最小化的构建




举个例子,如下代码所示:



  • 首先定义了一个 TextGlobal ,在 build 方法里输出 "######## TextGlobal"

  • 然后在 MyHomePage 里定义一个全局的 TextGlobal globalText = TextGlobal();

  • 接着在 MyHomePage 里添加 3 个 globalText

  • 最后点击 FloatingActionButton 触发 setState(() {});


class TextGlobal extends StatelessWidget {
const TextGlobal({Key? key}) : super(key: key);

@override
Widget build(BuildContext context) {
print("######## TextGlobal");
return Container(
child: new Text(
"测试",
style: new TextStyle(fontSize: 40, color: Colors.redAccent),
textAlign: TextAlign.center,
),
);
}
}
class MyHomePage extends StatefulWidget {
final String? title;
MyHomePage({Key? key, this.title}) : super(key: key);
@override
_MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
TextGlobal globalText = TextGlobal();
@override
Widget build(BuildContext context) {
print("######## MyHomePage");
return Scaffold(
appBar: AppBar(),
body: new Container(
alignment: Alignment.center,
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
globalText,
globalText,
globalText,
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
setState(() {});
},
),
);
}
}

那么有趣的来了,如下图 log 所示,"######## TextGlobal" 除了在一开始构建时有输出之外,剩下 setState(() {}); 的时候都没有在触发,也就是没有 rebuild ,这其实就是上面 ModalRoute 的类似行为:弹出键盘导致了 MediaQuery 触发 Navigator 执行 rebuild,但是 rebuild 到了 ModalRoute 就不往下影响



其实这个行为也体现在了 Scaffold 里,如果你去看 Scaffold 的源码,你就会发现 Scaffold 里大量使用了 MediaQuery.of(context)


比如上面的代码,如果你给 MyHomePageScaffold 配置一个 3333 的 ValueKey ,那么在 EditPage 弹出键盘时,其实 MyHomePageScaffold 是会触发 rebuild ,但是因为其使用的是 widget.body ,所以并不会导致 body 内对象重构。




如果是 MyHomePage 如果 rebuild ,就会对 build 方法里所有的配置的 new 对象进行 rebuild;但是如果只是 MyHomePage 里的 Scaffold 内部触发了 rebuild ,是不会导致 MyHomePage 里的 body 参数对应的 child 执行 rebuild 。



是不是太抽象?举个简单的例子,如下代码所示:



  • 我们定义了一个 LikeScaffold 控件,在控件内通过 widget.body 传递对象

  • LikeScaffold 内部我们使用了 MediaQuery.of(context).viewInsets.bottom ,模仿 Scaffold 里使用 MediaQuery

  • MyHomePage 里使用 LikeScaffold ,并给 LikeScaffold 的 body 配置一个 Builder ,输出 "############ HomePage Builder Text " 用于观察

  • 跳到 EditPage 页面打开键盘


class LikeScaffold extends StatefulWidget {
final Widget body;

const LikeScaffold({Key? key, required this.body}) : super(key: key);

@override
State<LikeScaffold> createState() => _LikeScaffoldState();
}

class _LikeScaffoldState extends State<LikeScaffold> {
@override
Widget build(BuildContext context) {
print("####### LikeScaffold build ${MediaQuery.of(context).viewInsets.bottom}");
return Material(
child: new Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [widget.body],
),
);
}
}
····
class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
var routeLists = routers.keys.toList();
return new LikeScaffold(
body: Builder(
builder: (_) {
print("############ HomePage Builder Text ");
return InkWell(
onTap: () {
Navigator.of(context).push(CupertinoPageRoute(builder: (context) {
return EditPage();
}));
},
child: Text(
"FFFFFFF",
style: TextStyle(fontSize: 50),
),
);
},
),
);
}
}

可以看到,最开始 "####### LikeScaffold build 0.0############ HomePage Builder Text 都正常执行,然后在键盘弹出之后,"####### LikeScaffold build 跟随键盘动画不断输出 bottom 的 大小,但是 "############ HomePage Builder Text ") 没有输出,因为它是 widget.body 实例。



所以通过这个最小例子,可以看到虽然 Scaffold 里大量使用 MediaQuery.of(context) ,但是影响范围是约束在 Scaffold 内部


接着我们继续看修改这个例子,如果在 LikeScaffold 上嵌套多一个 Scaffold ,那输出结果会是怎么样?



class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
var routeLists = routers.keys.toList();
///多加了个 Scaffold
return Scaffold(
body: new LikeScaffold(
body: Builder(
·····
),
),
);
}

答案是 LikeScaffold 内的 "####### LikeScaffold build 也不会因为键盘的弹起而输出,也就是: LikeScaffold 虽然使用了 MediaQuery.of(context) ,但是它不再因为键盘的弹起而导致 rebuild


因为此时 LikeScaffoldScaffold 的 child ,所以在 LikeScaffold 内通过 MediaQuery.of(context) 指向的,其实是 Scaffold 内部经过处理的 MediaQueryData


image-20220624150712453



Scaffold 内部有很多类似的处理,例如 body 里会根据是否有 AppbarBottomNavigationBar 来决定是否移除该区域内的 paddingTop 和 paddingBottom 。



所以,看到这里有没有想到什么?为什么时不时通过 MediaQuery.of(context) 获取的 padding ,有的 top 为 0 ,有的不为 0 ,原因就在于你获取的 context 来自哪里


举个例子,如下代码所示, ScaffoldChildPage 作为 Scaffold 的 child ,我们分别在 MyHomePageScaffoldChildPage 里打印 MediaQuery.of(context).padding


class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
print("MyHomePage MediaQuery padding: ${MediaQuery.of(context).padding}");
return Scaffold(
appBar: AppBar(
title: new Text(""),
),
extendBody: true,
body: Column(
children: [
new Spacer(),
ScaffoldChildPage(),
new Spacer(),
],
),
);
}
}
class ScaffoldChildPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
print("ScaffoldChildPage MediaQuery padding: ${MediaQuery.of(context).padding}");
return Container();
}
}

如下图所示,可以看到,因为此时 MyHomePageAppbar ,所以 ScaffoldChildPage 里获取到 paddingTop 是 0 ,因为此时 ScaffoldChildPage 获取到的 MediaQueryData 已经被 MyHomePage 里的 Scaffold 改写了。


image-20220624151522429


如果此时你给 MyHomePage 增加了 BottomNavigationBar ,可以看到 ScaffoldChildPage 的 bottom 会从原本的 34 变成 90 。


image-20220624152008795


到这里可以看到 MediaQuery.of 里的 context 对象很重要:



  • 如果页面 MediaQuery.of 用的是 Scaffold 外的 context ,获取到的是顶层的 MediaQueryData ,那么弹出键盘时就会导致页面 rebuild

  • MediaQuery.of 用的是 Scaffold 内的 context ,那么获取到的是 Scaffold 对于区域内的 MediaQueryData ,比如前面介绍过的 body ,同时获取到的 MediaQueryData 也会因为 Scaffold 的配置不同而发生改变


所以,如下动图所示,其实部分人会在 push 对应路由地方,通过嵌套 MediaQuery 来做一些拦截处理,比如设置文本不可缩放,但是其实这样会导致键盘在弹出和收起时,触发各个页面不停 rebuild ,比如在 Page 2 弹出键盘的过程,Page 1 也在不停 rebuild。


1111333


所以,如果需要做一些全局拦截,推荐通过 useInheritedMediaQuery 这种方式来做全局处理。


return MediaQuery(
data: MediaQueryData.fromWindow(WidgetsBinding.instance!.window).copyWith(boldText: false),
child: MaterialApp(
useInheritedMediaQuery: true,
),
);

所以最后做个总结,本篇主要理清了:



  • MediaQueryDataviewInsets \ padding \ viewPadding 的区别

  • MediaQuery 和键盘状态的关系

  • MediaQuery.of 使用不同 context 对性能的影响

  • 通过 Scaffold 内的 context 获取到的 MediaQueryData 受到 Scaffold 的影响

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

Flutter 实现背景图片毛玻璃效果

前言 继续我们绘图相关篇章,这次我们来看看如何使用 CustomPaint 实现毛玻璃背景图效果。毛玻璃背景图其实就是将图片进行一定程度的模糊,背景图经过模糊后更加虚幻,使得前景和后景就会有层次感。相比直接加蒙层的效果来说,毛玻璃看起来更加好看一些。下面是背景...
继续阅读 »

前言


继续我们绘图相关篇章,这次我们来看看如何使用 CustomPaint 实现毛玻璃背景图效果。毛玻璃背景图其实就是将图片进行一定程度的模糊,背景图经过模糊后更加虚幻,使得前景和后景就会有层次感。相比直接加蒙层的效果来说,毛玻璃看起来更加好看一些。下面是背景图处理前后的对比,我们的前景图片的透明度并没有改变,但是背景图模糊虚化后,感觉前景更加显眼了一样。
模糊前后对比.jpg
本篇涉及如下内容:



  • 使用 canvas 绘制图片。

  • 绘制图片时如何更改图片的填充范围。

  • 使用 ImageFilter 模糊图片,实现毛玻璃效果。


使用 canvas 绘制图片


Flutter 为 canvas 提供了drawImage 方法用于绘制图片,方法定义如下:


void drawImage(Image image, Offset offset, Paint paint)

其中各个参数说明如下:



  • imagedart:ui 中的 Image 对象,注意不是Widget 中的 Image,因此绘制的时候需要将图片资源转换为 ui.Image 对象。下面是转换的示例代码,fillImage 即最终得到的 ui.Image 对象。注意转换需要一定的时间,因此需要使用异步 async / await 操作。


Future<void> init() async {
final ByteData data = await rootBundle.load('images/island-coder.png');
fillImage = await loadImage(Uint8List.view(data.buffer));
}

Future<ui.Image> loadImage(Uint8List img) async {
final Completer<ui.Image> completer = Completer();
ui.decodeImageFromList(img, (ui.Image img) {
setState(() {
isImageLoaded = true;
});
return completer.complete(img);
});
return completer.future;
}


  • offset:绘制图片的起始位置。

  • paint:绘图画笔对象,在 paint 上可以应用各种处理效果,比如本篇要用到的图片模糊效果。


注意,drawImage 方法无法更改图片绘制的区域大小,默认就是按图片的实际尺寸绘制的,所以如果要想保证全屏的背景图,我们就需要使用另一个绘制图片的方法。


更改绘制图片的绘制范围


Flutter 的 canvas 为绘制图片提供了一个尺寸转换方法,即可以通过指定原绘制区域的矩形和目标区域的矩形,将图片某个区域映射到新的矩形框中绘制。也就是我们甚至可以实现绘制图片的局部区域。该方法名为 drawImageRect,定义如下:


void drawImageRect(Image image, Rect src, Rect dst, Paint paint)

方法的参数比较容易懂,我们来看看 Flutter 的文档说明。



Draws the subset of the given image described by the src argument into the canvas in the axis-aligned rectangle given by the dst argument.
翻译:通过 src 参数将给定图片的局部(subset)绘制到坐标轴对齐的目标矩形区域内。



下面是我们将源矩形框设置为实际图片的尺寸和一半宽高的对比图,可以看到取一半宽高的只绘制了左上角的1/4区域。实际我们可以定位起始位置来截取部分区域绘制。
截取原图的一半宽高.jpg


毛玻璃效果实现


毛玻璃效果实现和我们上两篇使用 paintshader属性有点类似,Paint 类提供了一个imageFilter属性专门用于图片处理,其中dart:ui 中就提供了ui.ImageFilter.blur方法构建模糊效果处理的 ImageFilter对象。方法定义如下:


factory ImageFilter.blur({ 
double sigmaX = 0.0,
double sigmaY = 0.0,
TileMode tileMode = TileMode.clamp
})

这个方法实际调用的是一个高斯模糊处理器,高斯模糊其实就是应用一个方法将像素点周边指定范围的值进行处理,进而实现模糊效果,有兴趣的可以自行百度一下。下面的 sigmaXsigmaY 分布代表横轴方向和纵轴方向的模糊程度,数值越大,模糊程度越厉害。因此我们可以通过这两个参数控制模糊程度。


return _GaussianBlurImageFilter(
sigmaX: sigmaX,
sigmaY: sigmaY,
tileMode: tileMode
);

**注意,这里 sigmaX 和 sigmaY 不能同时为0,否则会报错!**这里应该是如果同时为0会导致除0操作。
下面来看整体的绘制实现代码,如下所示:


class BlurImagePainter extends CustomPainter {
final ui.Image bgImage;
final double blur;

BlurImagePainter({
required this.bgImage,
required this.blur,
});
@override
void paint(Canvas canvas, Size size) {
var paint = Paint();
// 模糊的取值不能为0,为0会抛异常
if (blur > 0) {
paint.imageFilter = ui.ImageFilter.blur(
sigmaX: blur,
sigmaY: blur,
tileMode: TileMode.mirror,
);
}

canvas.drawImageRect(
bgImage,
Rect.fromLTRB(0, 0, bgImage.width.toDouble(), bgImage.height.toDouble()),
Offset.zero & size,
paint,
);
}

代码其实很短,就是在模糊值不为0的时候,应用 imageFilter 进行模糊处理,然后使用 drawImageRect 方法确保图片填充满整个背景。完整代码已经提交至:绘图相关代码,文件名为:blur_image_demo.dart。变换模糊值的效果如下动图所示。
背景图模糊过程.gif


总结


本篇介绍了使用 CustomPaint 实现背景图模糊,毛玻璃的效果。关键点在于 使用 Paint 对象的 imageFilter属性,使用高斯模糊应用到图片上。以后碰到需要模糊背景图的地方就可以直接上手用啦!


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

Native 如何快速集成 Flutter

如何 Android 项目中集成 Flutter 概述 目前flutter越来越受欢迎,但对于一些成熟的产品来说,完全摒弃原有App全面转向Flutter是不现实的。因此使用Flutter去统一Android、iOS技术栈,把它作为已有原生App的扩展能力,通...
继续阅读 »

如何 Android 项目中集成 Flutter


概述


目前flutter越来越受欢迎,但对于一些成熟的产品来说,完全摒弃原有App全面转向Flutter是不现实的。因此使用Flutter去统一Android、iOS技术栈,把它作为已有原生App的扩展能力,通过有序推进来提升移动终端的开发效率。 目前,想要在已有的原生App里嵌入一些Flutter页面主要有两种方案。一种是将原生工程作为Flutter工程的子工程,由Flutter进行统一管理,这种模式称为统一管理模式。另一种是将Flutter工程作为原生工程的子模块,维持原有的原生工程管理方式不变,这种模式被称为三端分离模式,如下图所示。
1.png
三端代码分离模式的原理是把Flutter模块作为原生工程的子模块,从而快速地接入Flutter模块,降低原生工程的改造成本。


如何在Native项目中接入flutter 模块


在原生项目中集成flutter模块有两种方式,第一种是直接在项目中新建一个flutter module,第二种将flutter项目模块打包成aar或so包集成到Native项目中。一下将详细介绍这两种方式 (以Android为例)


采用module引用的方式


直接通过Android stuido



File->New ->New Module 选择 Flutter Module 来生成一个Flutter Module.



image.png


image.png



如下图:Android studio为原生项目创建了一个module



image.png


手动创建Flutter module


假设你在 some/path/MyApp 路径下已有一个 Android 应用,并且你希望 Flutter 项目作为同级项目:


 cd some/path/
$ flutter create -t module --org com.example my_flutter

image.png


注意:



  1. 这会创建一个 some/path/my_flutter/ 的 Flutter 模块项目,其中包含一些 Dart 代码来帮助你入门以及一个隐藏的子文件夹 .android/。 .android 文件夹包含一个 Android 项目,该项目不仅可以帮助你通过 flutter run 运行这个 Flutter 模块的独立应用,而且还可以作为封装程序来帮助引导 Flutter 模块作为可嵌入的 Android 库。

  2. 为了避免 Dex 合并出现问题,flutter.androidPackage 不应与应用的包名相同


引入 Java 8


Flutter Android 引擎需要使用到 Java 8 中的新特性。


在尝试将 Flutter 模块项目集成到宿主 Android 应用之前,请先确保宿主 Android 应用的 build.gradle 文件的 android { } 块中声明了以下源兼容性,例如:


android {
//...
compileOptions {
sourceCompatibility 1.8
targetCompatibility 1.8
}
}

采用AAR资源包的方式导入Flutter模块


flutter 工程作为独立的项目开发迭代,原生工程不直接使用Flutter项目,而是通过导入flutter 的资源包来引用Flutter 模块。



创建Flutter module 工程。



image.png



编译生成AAR包



image.png



flutter 工程会创建一个本地maven仓库和aar文件,同时在Flutter 项目也会输出指引导入的步骤文本,按照提示步骤操作即可。
为方便使用将该maven仓库拷贝到native 项目中。



image.png



提示步骤如下



Consuming the Module




  1. Open \app\build.gradle




  2. Ensure you have the repositories configured, otherwise add them:


    String storageUrl = System.env.FLUTTER_STORAGE_BASE_URL ?: "storage.googleapis.com"
    repositories {
    maven {
    url 'D:<path>\build\host\outputs\repo'
    }
    maven {
    url '$storageUrl/download.flutter.io'
    }
    }




  3. Make the host app depend on the Flutter module:




dependencies {
debugImplementation 'com.example.untitled1:flutter_debug:1.0'
profileImplementation 'com.example.untitled1:flutter_profile:1.0'
releaseImplementation 'com.example.untitled1:flutter_release:1.0'
}


  1. Add the profile build type:


android {
buildTypes {
profile {
initWith debug
}
}
}

To learn more, visit flutter.dev/go/build-aa…
Process finished with exit code 0


在 Android 应用中添加 Flutter 页面


步骤 1:在 AndroidManifest.xml 中添加 FlutterActivity


Flutter 提供了 FlutterActivity,用于在 Android 应用内部展示一个 Flutter 的交互界面。和其他的 Activity 一样,FlutterActivity 必须在项目的 AndroidManifest.xml 文件中注册。将下边的 XML 代码添加到你的 AndroidManifest.xml 文件中的 application 标签内:


<activity
android:name="io.flutter.embedding.android.FlutterActivity"
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize"
/>

上述代码中的 @style/LaunchTheme 可以替换为想要在你的 FlutterActivity 中使用的其他 Android 主题。主题的选择决定 Android 系统展示框架所使用的颜色,例如 Android 的导航栏,以及 Flutter UI 自身的第一次渲染前 FlutterActivity 的背景色。


步骤 2:加载 FlutterActivity


在你的清单文件中注册了 FlutterActivity 之后,根据需要,你可以在应用中的任意位置添加打开 FlutterActivity 的代码。下边的代码展示了如何在 OnClickListener 的点击事件中打开 FlutterActivity


myButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
startActivity(
FlutterActivity.createDefaultIntent(currentActivity)
);
}
});

Flutter 启动优化


每一个 FlutterActivity 默认会创建它自己的 FlutterEngine。每一个 FlutterEngine 会有一个明显的预热时间。这意味着加载一个标准的 FlutterActivity 时,在你的 Flutter 交互页面可见之前会有一个短暂的延迟。想要最小化这个延迟时间,你可以在抵达你的 FlutterActivity 之前,初始化一个 FlutterEngine,然后使用这个已经预热好的 FlutterEngine
如果直接启动FlutterActivity则无法避免预热时间,用户会感受到一个较长时间的白屏等待。


优化


提前初始化一个  FlutterEngine,启动的FlutterActivty时直接使用已经初始化的FlutterEngine.



提前初始化



public class MyApplication extends Application {
public FlutterEngine flutterEngine;

@Override
public void onCreate() {
super.onCreate();
// Instantiate a FlutterEngine.
flutterEngine = new FlutterEngine(this);

// Start executing Dart code to pre-warm the FlutterEngine.
flutterEngine.getDartExecutor().executeDartEntrypoint(
DartEntrypoint.createDefault()
);

// Cache the FlutterEngine to be used by FlutterActivity.
FlutterEngineCache
.getInstance()
.put("my_engine_id", flutterEngine);
}
}


使用预热的FlutterEngine



myButton.addOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
startActivity(
FlutterActivity
.withCachedEngine("my_engine_id")
.build(currentActivity)
);
}
});

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

黑科技!让Native Crash 与ANR无处发泄!

ANR
前言 高产似母猪的我,又带来了干货记录,本次是对signal的一个总结与回顾。不知道你们开发中,是否会遇到小部分的nativecrash 或者 anr,这部分往往是由第三方库导致的或者当前版本没办法修复的bug导致的,往往这些难啃的crash,对现有的cras...
继续阅读 »

前言


高产似母猪的我,又带来了干货记录,本次是对signal的一个总结与回顾。不知道你们开发中,是否会遇到小部分的nativecrash 或者 anr,这部分往往是由第三方库导致的或者当前版本没办法修复的bug导致的,往往这些难啃的crash,对现有的crash数据指标造成一定影响,同时也对这小部分crash用户不友好,那么我们有没有办法实现一套crash or anr重启机制呢?其实是有的,相信在各个大厂都有一套“安全气囊”装置,比如crash一定次数就启用轻量版本或者自动重新启动等等,下面我们来动手搞一个这样的装置!这也是我第三个s开头的开源库Signal


注意:前方高能!阅读本文最好有一点ndk开发的知识噢!没有也没关系,冲吧!


Native Crash


native crash不同于java/kotlin层的crash,在java环境中,如果程序出现了不可预期的crash(即没有捕获),就会往上抛出给最终的线程uncaghtexceptionhandler,在这里我们可以再次处理,比如屏蔽某个exception即可保持app的稳定,然后native层的crash不一样,native 层的crash大多数是“不可恢复”的,比如某个内存方面的错误,这些往往是不可处理的,需要中断当前进程,所以如果发生了native crash,我们转移到自定义的安全处理,比如自动重启后提示用户等等,就会提高用户很大的体验感(比起闪退)


信号量机制


当native 层发生异常的时候,往往是通过信号的方式发送,给相对应的信号处理器处理


image.png
我们可以从signal.h看到,大概已经定义的信号量有


/**
* #define SIGHUP 1
#define SIGINT 2
#define SIGQUIT 3
#define SIGILL 4
#define SIGTRAP 5
#define SIGABRT 6
#define SIGIOT 6
#define SIGBUS 7
#define SIGFPE 8
#define SIGKILL 9
#define SIGUSR1 10
#define SIGSEGV 11
#define SIGUSR2 12
## define SIGPIPE 13
#define SIGALRM 14
#define SIGTERM 15
#define SIGSTKFLT 16
#define SIGCHLD 17
#define SIGCONT 18
#define SIGSTOP 19
#define SIGTSTP 20
#define SIGTTIN 21
#define SIGTTOU 22
#define SIGURG 23
#define SIGXCPU 24
#define SIGXFSZ 25
#define SIGVTALRM 26
#define SIGPROF 27
#define SIGWINCH 28
#define SIGIO 29
#define SIGPOLL SIGIO
#define SIGPWR 30
#define SIGSYS 31
*/

具体的含义可自定百度或者google,相信如果开发者都能在bugly等bug平台上看到


信号量处理函数sigaction


一般的我们有很多种方式定义信号量处理函数,这里介绍sigaction
头文件:#include<signal.h>


定义函数:int sigaction(int signum,const struct sigaction *act ,struct sigaction *oldact)


函数说明:sigaction会依参数signum指定的信号编号来设置该信号的处理函数。参数signum可以指定SIGKILL和SIGSTOP以外的所有信号。如参数结构sigaction定义如下


struct sigaction {
void (*sa_handler)(int);
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer)(void);
};

信号处理函数可以采用void (*sa_handler)(int)或void (*sa_sigaction)(int, siginfo_t *, void *)。到底采用哪个要看sa_flags中是否设置了SA_SIGINFO位,如果设置了就采用void (*sa_sigaction)(int, siginfo_t *, void *),此时可以向处理函数发送附加信息;默认情况下采用void (*sa_handler)(int),此时只能向处理函数发送信号的数值。


sa_handler:此参数和signal()的参数handler相同,代表新的信号处理函数,其他意义请参考signal();
sa_mask:用来设置在处理该信号时暂时将sa_mask指定的信号集搁置;
sa_restorer:此参数没有使用;
sa_flags :用来设置信号处理的其他相关操作,下列的数值可用。sa_flags还可以设置其他标志:
SA_RESETHAND:当调用信号处理函数时,将信号的处理函数重置为缺省值SIG_DFL
SA_RESTART:如果信号中断了进程的某个系统调用,则系统自动启动该系统调用
SA_NODEFER :一般情况下, 当信号处理函数运行时,内核将阻塞该给定信号。但是如果设置SA_NODEFER标记, 那么在该信号处理函数运行时,内核将不会阻塞该信号。参考


即我们可以通过这个函数,注册我们想要的信号处理,如果当SIGABRT信号到来时,我们希望将其引到自我们自定义信号处理,即可采用以下方式


 sigaction(SIGABRT, &sigc, nullptr);

其中sigc为sigaction结构体的变量


struct sigaction sigc;
//sigc.sa_handler = SigFunc;
sigc.sa_sigaction = SigFunc;
sigemptyset(&sigc.sa_mask);
sigc.sa_flags = SA_SIGINFO;

SigFunc为我们定义处理函数的指针,我们可以设定这样一个函数,去处理我们想要拦截的信号


void SigFunc(int sig_num, siginfo *info, void *ptr) {
自定义处理
}

native crash拦截


有了前面这些基础知识,我们就开始封装我们的crash拦截吧,作为库开发者,我们希望把拦截的信号量交给上层去处理,所以我们的层次是这样的


image.png
所以我们可以有以下代码,具体细节可以看Signal
我们给出函数处理器


jobject currentObj;
JNIEnv *currentEnv = nullptr;

void SigFunc(int sig_num, siginfo *info, void *ptr) {
// 这里判空并不代表这个对象就是安全的,因为有可能是脏内存

if (currentEnv == nullptr || currentObj == nullptr) {
return;
}
__android_log_print(ANDROID_LOG_INFO, TAG, "%d catch", sig_num);
__android_log_print(ANDROID_LOG_INFO, TAG, "crash info pid:%d ", info->si_pid);
jclass main = currentEnv->FindClass("com/example/lib_signal/SignalController");
jmethodID id = currentEnv->GetMethodID(main, "callNativeException", "(I)V");
if (!id) {
return;
}
currentEnv->CallVoidMethod(currentObj, id, sig_num);
currentEnv->DeleteGlobalRef(currentObj);


}

当so库被加载的时候由系统自动调用JNI_OnLoad
extern "C" jint JNI_OnLoad(JavaVM *vm, void *reserved) {
jint result = -1;
// 直接用vm进行赋值,不然不可靠
if (vm->GetEnv((void **) &currentEnv, JNI_VERSION_1_4) != JNI_OK) {
return result;
}
return JNI_VERSION_1_4;
}

其中currentEnv代表着当前jni环境,我们在JNI_OnLoad阶段进行初始化即可,currentObj即代表我们要调用的方法对象,因为我们要回调到java层,所以native肯定需要一个java对象,具体可以看到Signal里面的处理,值得注意的是,我们在native想要在其他函数使用java对象的话,在初始函数赋值的时候,就必须采用env->NewGlobalRef方式分配一个全局变量,不然在该函数结束的时候,对象的内存就会变成脏变量(注意不是NULL)。


Spi机制的运用


如果还不明白spi机制的话,可以查看我之前写的这篇spi机制,因为我们最终会将信号信息传递给java层,所以最终会在java最后执行我们的重启处理,但是重启前我们可能会使用各种自定义的处理方案,比如弹出toast或者各种自定义操作,那么这种自定义的处理就很合适用spi接口暴露给具体的使用者即可,所以我们Signal定义了一个接口


interface CallOnCatchSignal {
fun onCatchSignal(signal: Int,context: Context)
}

外部库的调用者实现这个接口,将实现类配置在META-INF.services目录即可,如图


image.png
如此一来,我们就可以在自定义的MyHandler实现自己的重启逻辑,比如重启/自定义上报crash等等,demo可以看Signal的处理


ANR


关于anr也是一个很有趣的话题,我们可以看到anr也会导致闪退,主要是国内各个厂商都有自己的自定义化处理,比如常规的弹出anr框或者主动闪退,无论是哪一种,对于用户来说都不是一个好的体验。


ANR传递过程


以android 11为例子,最终anr被检测发生后,会调用ProcessErrorStateRecord类的appNotResponding方法,去进行dump 墓碑文件的操作,这个时候就会调用发送一个信号为Signal_Quit的信号,对应的常量为3,所以如果我们想检测到anr后去进行自定义处理的话,按照上面所说直接用sigaction可以吗?


image.png


然而如果直接用sigaction去注册Signal_Quit信号进行处理的话,会发现居然什么都没有回调!那么这发生了什么!


原因就是我们进程继承Zygote进行的时候就把主线程信号的掩码也继承了,Zygote进程把这三个信号量加入了掩码,该方法被调用在init方法中


image.png
掩码的作用就是使得当前的线程不相应这三个信号量,交给其他线程处理


那么其他线程这里指的是什么?其实就是SignalCatcher线程,通常我们发生anr的时候也能看到log输出,最终在run方法注册处理函数


image.png
最终调用WaitForSignal


image.png
调用wait方法


image.png
这个sigwait方法也是一个注册信号处理函数的方法,跟sigaction的区别可参考


取消block


经过上面的分析,相信能了解到为什么Signal_Quit监听不了了,我们也知道,zygote通过掩码把信号进行了屏蔽,那么我们有办法把这个屏蔽给打开吗?答案是有的


pthread_sigmask(SIG_UNBLOCK, &mask, &old))

sigemptyset(&mask);
sigaddset(&mask, SIGQUIT);

我们可以通过pthread_sigmask设置为非block,即参数1的标志,把要取消屏蔽的信号放入即可,如图就是把SIGQUIT取消了,这样一来我们再使用sigaction去注册SIGQUIT就可以在信号出发时执行我们的anr处理逻辑了。值得注意的是,SIGQUIT触发也不一定由anr发生,这是一个必要但不充分的条件,所以我们还要添加其他的判断,比如我们可以判断一个queue里面的当前message的when参数来判断这个消息在队列待了多久,又或者是我们自定义一个异步消息去查看这个消息什么时候回调了handler等等方法,最终判断是否是anr,当然这个不是百分百准确,目前我也没想到百分百准确的方法,因为FileObserve监听traces文件已经在android5以上不能用了,所以Signal里面没有给出具体的判断,只给了一个参考例子。


最后


上述所讲的都在Signal这个库里面有源码与注释,用起来吧!自定义处理可以用作检测crash,anr,也可以用作一个安全装置,发生crash重启等等,只要有脑洞,都可以实现!最后记得点个赞啦!


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

破防了!Web3还没整明白,Web5居然出现了?

自Web3问世并爆火以后,关于“Web3是什么、有何用”的解读文章层出不穷。就在多数人还是云里雾里的当下,神奇的事情发生了。Twitter的创始人Jack Dorsey在日前公布了打造“Web5”的计划,声称“这很可能将是我们对于互联网最重大的贡献”,并戏谑地...
继续阅读 »

自Web3问世并爆火以后,关于“Web3是什么、有何用”的解读文章层出不穷。就在多数人还是云里雾里的当下,神奇的事情发生了。

Twitter的创始人Jack Dorsey在日前公布了打造“Web5”的计划,声称“这很可能将是我们对于互联网最重大的贡献”,并戏谑地为Web3的创投人们“点蜡”,意指Web3必凉,Web5才是未来的“天命之子”。


图片来源:推特截图

那么,Web5到底是何方神圣?

PART 01 Web5横空出世

围绕Web5的定义和核心概念,Jack Dorsey的团队(Block旗下的比特币部门TBD)出具了一份报告。报告不长,包括联系页在内,总共18页PPT。

第一眼看到Web5,可能会有不少人诧异Web4去哪儿了,这个横跳是有什么寓意吗?Dorsey团队给出的答案是:Web5相当于Web2和Web3的集合。


图片来源:Dorsey团队报告截图

也就是说,因为2+3=5,Web5之名由此诞生。乍看之下简单粗暴,细看一下又如何呢?

他们提出,所谓Web5,就是要建立一个去中心化的Web平台(DWP),使开发人员能够利用去中心化的身份标识(DIDs)和去中心化的网络节点(DWNs),来编写去中心化的Web应用程序(DWAs),从而将个人身份和数据的所有权和控制权交还给个人。

从这个概念可以看到,Web5的核心跟 Web3一样,突出“去中心化”特性。其目的是要打破当前互联网世界中用户数据被沉淀在不同的应用中,用户本身无法掌控和自由使用的现状。

从这一点出发,Web5中提及的几个新概念,某种程度上都可以和Web3进行对标。有位名为“ntkris”的网友对此进行了精要的总结。

  • 去中心化身份标识(DID)=公钥

  • 去中心化网络节点(DWN)=智能合约(但在本地运行)

  • 可验证凭证(VCs)=零知识证明

  • 去中心化应用程序(DWAs)=dApps


图片来源:推特截图

不过也有人因此提出质疑,Web5相较Web3来说似乎是“新瓶装旧酒”。但事实上,就目前披露的资料来看,两者还是有两点核心不同。

其一,Web5只有身份标识存储在区块链上,其他所有内容都存储在用户运行的节点上。而Web3中所有用户数据都通过区块链的形式存储在所有的节点中。

其二,Web5的实现最大的依仗是比特币网络,而不是当前在Web3中被广泛使用的以太坊以及其他基于智能合约的区块链。可以说,如果有朝一日Web5真的成功,那么除了比特币外的其他加密货币都将失去意义。

Jack Dorsey及其团队之所以会提出Web5,其实都有迹可循。

首先,Dorsey本人一直都是比特币的狂信徒。他早在2018年就预言未来世界上只有一种通用货币——比特币,比特币会是现有货币体系的终结者。歌手Cardi B曾在Twitter上问Dorsey比特币是否会取代美元,Dorsey坚定地回答“Yes,Bitcon will”。

再者,Dorsey认为,Web3根本不能实现真正的去中心化。去年年底Dorsey就曾公开表示,Web3不是所有人的,它只是贴了个不同标签的集权模式,不要白日做梦。所谓的Web3更像是一个营销热词,实际话语权仍掌握在少数风投和公司手里。如今的Web5可能就是他对于“去中心化”实现路径给出的新答案。

PART 02 Web3的“九宗罪”

虽然Web5的横空出世似乎让人看到了下一代互联网形态的新解,但目前为止它还只是个存在于PPT中的“新蓝图”。相比之下,仍旧是Web3离我们更近,虽然它现在依然“面目模糊”。

在对Web3一探究竟之前,我们先回顾一下Web的三个主要时代:

  • Web1:以静态网页为主的“活化石”

  • Web2:大量的通信和商业行为都集中在少数科技巨头所拥有的封闭平台上。大多数情况下,用户对自己的数据只拥有使用权,而平台却对用户数据和用户创作内容拥有所有权

  • Web3:以用户为主导,基于区块链技术打造的去中心化的互联网生态。用户数据的所有权和控制权均归属用户本人


图片来源于网络

虽然关于Web3的讨论总是毁誉参半,但不可否认的是,从形态上来说,Web3相较Web2来说是一种进化。

Web3创建了一种无需准入的数据存储方式。所有数据都被存储在区块链网络中的公共账本上。不再是某个公司或平台拥有数据,而是由多个节点共同存储数据,并就数据的真实性和有效性达成共识。以此为基础,Web3可以开启无数崭新的用例。

但Web3要面对的现实是,天马行空的想象和天花乱坠的陷阱总是相伴而生,也正因为如此,关于Web3的争议总是不绝于耳。

不久前,分析公司Forrester发布了两份评估Web3的文件,犀利地评价其“包含了一场反乌托邦噩梦的种子”。

分析文章指出,一方面,“Web3”之名正在被滥用。“几乎在一夜之间”,无数“区块链项目”、“NFT倡议”和“元宇宙”相关事务都被神奇地命名为“Web3项目”;另一方面,虽然Web3“承诺了一个更好的在线未来”,但其关键内容却不堪一击。

Forrester定义了Web3的九个关键原则,然后又将其一一推翻。

1、愿景:去中心化

现实:这是不可能实现的,实证就是目前的众多加密货币项目通常都由大型平台或公司主导

2、愿景:相信代码,而不是公司

现实:智能合约及其规则通常由某一个公司开发并执行。那么我们真的能信任这些陌生的开发人员吗

3、愿景:始终使用公开透明的代码

现实:这并不会阻止垄断的形成,这反而会导致依赖于一小部分有能力评估代码的人

4、愿景:加密经济原则的设计使系统普惠所有参与者

现实:只是有利于富人和发展垄断

5、愿景:用户能够拥有和控制他们创建的数据和内容

现实:“所有权”的概念是模糊的。大多数用户不愿意或没有能力对他们的数据做出持续一致的决定

6、愿景:用户自己管理自己的身份和凭证

现实:没有多少人愿意为此费心,部分原因是这很难

7、愿景:用户能控制他们所使用的应用程序和网络

现实:除了少数精通技术的人之外,这种状况极其罕见

8、愿景:去中心化的自治组织和实体作为智能合约的集合而存在

现实:它们没有法律基础,并在一个乌托邦式的假设下工作,即所有的可能性都可以被编码

9、愿景:去中心化金融(DeFi)

现实:虽然是个不错的主意,可惜缺乏对消费者的保护,而风控需要代码检查,这很少有人能做到

自诞生伊始就饱受质疑的Web3真的会有未来吗?如果它真的到来,又会对我们的工作和生活产生何种影响呢?

PART 03 你期待Web3的到来吗

如何打造一个更加开放、共享、安全的数字环境在每一次互联网技术革命中都是核心议题。Web3虽然离我们好像很远,但其思想内核是否值得希冀未来者再次下注呢?毕竟一切都只是刚刚开始。

其一,数字化转型是个不可逆的过程。但随着转型的深化,以及重大创新技术的每一次进步,其目标会不断变化。以Web3为前景,区块链技术的发展已经经历了充分的时间考验,并在一定程度上获得了更加繁盛的土壤。

其二,去中心化或许是IT系统的重要发展趋向。企业运营业务所需的许多重要数据将越来越多地保存在更私密和受保护的地方,存储在区块链和其他类型的分布式账本中。随着时间的推移,越来越多的应用程序将更类似于开源项目,并使所有利益相关者可以公开透明地查看、验证、达成共识。

其三,一些更直接的转变,例如接受某些形式的加密货币作为支付或以NFT的形式发行知识产权。

PART 04 结语

Web3如今还是一个混沌未开的世界,这里有创业者、梦想家、理想主义者,也有骗子、吹牛大王、浑水摸鱼者。这个概念下,关于数字世界的一切本质似乎都在受到质疑和重塑。但不管下一代互联网形态如何,是循规蹈矩地过渡,还是摧枯拉朽地颠覆,我们都应该思考:在一个未知的数字世界里,信任、安全、隐私应该是何种模样,应该如何保障。

参考链接:

https://stackoverflow.blog/2022/05/25/web3-skeptics-and-believers-both-need-a-reality-check/

https://www.pingwest.com/a/265452

https://developer.tbd.website/docs/Decentralized%20Web%20Platform%20-%20Public.pdf

https://www.theregister.com/2022/04/01/forrester_web3_criticism/

https://www.zdnet.com/article/how-decentralization-and-web3-will-impact-the-enterprise/

来源:mp.weixin.qq.com/s/3VjcqRjxta-acEoM1v3Ndw

收起阅读 »

不要滥用effect哦

你或你的同事在使用useEffect时有没有发生过以下场景:当你希望状态a变化后发起请求,于是你使用了useEffect:useEffect(() => { fetch(xxx); }, [a])这段代码运行符合预期,上线后也没问题。随着需求不断迭代...
继续阅读 »

你或你的同事在使用useEffect时有没有发生过以下场景:

当你希望状态a变化后发起请求,于是你使用了useEffect

useEffect(() => {
fetch(xxx);
}, [a])

这段代码运行符合预期,上线后也没问题。

随着需求不断迭代,其他地方也会修改状态a。但是在那个需求中,并不需要状态a改变后发起请求。

你不想动之前的代码,又得修复这个bug,于是你增加了判断条件:

useEffect(() => {
if (xxxx) {
fetch(xxx);
}
}, [a])

某一天,需求又变化了!现在请求还需要b字段。

这很简单,你顺手就将b作为useEffect的依赖加了进去:

useEffect(() => {
if (xxxx) {
fetch(xxx);
}
}, [a, b])

随着时间推移,你逐渐发现:

  • 是否发送请求if条件相关
  • 是否发送请求还与a、b等依赖项相关
  • a、b等依赖项又与很多需求相关

根本分不清到底什么时候会发送请求,真是头大...

如果以上场景似曾相识,那么React新文档里已经明确提供了解决办法。

欢迎加入人类高质量前端框架群,带飞

一些理论知识

新文档中这一节名为Synchronizing with Effects,当前还处于草稿状态。

但是其中提到的一些概念,所有React开发者都应该清楚。

首先,effect这一节隶属于Escape Hatches(逃生舱)这一章。


从命名就能看出,开发者并不一定需要使用effect,这仅仅是特殊情况下的逃生舱。

React中有两个重要的概念:

  • Rendering code(渲染代码)
  • Event handlers(事件处理器)

Rendering code开发者编写的组件渲染逻辑,最终会返回一段JSX

比如,如下组件内部就是Rendering code

function App() {
const [name, update] = useState('KaSong');

return <div>Hello {name}</div>;
}

Rendering code的特点是:他应该是不带副作用的纯函数

如下Rendering code包含副作用(count变化),就是不推荐的写法:

let count = 0;

function App() {
count++;
const [name, update] = useState('KaSong');

return <div>Hello {name}</div>;
}

处理副作用

Event handlers组件内部包含的函数,用于执行用户操作,可以包含副作用

下面这些操作都属于Event handlers

  • 更新input输入框
  • 提交表单
  • 导航到其他页面

如下例子中组件内部的changeName方法就属于Event handlers

function App() {
const [name, update] = useState('KaSong');

const changeName = () => {
update('KaKaSong');
}

return <div onClick={changeName}>Hello {name}</div>;
}

但是,并不是所有副作用都能在Event handlers中解决。

比如,在一个聊天室中,发送消息是用户触发的,应该交给Event handlers处理。

除此之外,聊天室需要随时保持和服务端的长连接,保持长连接的行为属于副作用,但并不是用户行为触发的。

对于这种:在视图渲染后触发的副作用,就属于effect,应该交给useEffect处理。

回到开篇的例子:

当你希望状态a变化后发起请求,首先应该明确,你的需求是:

状态a变化,接下来需要发起请求

还是

某个用户行为需要发起请求,请求依赖状态a作为参数

如果是后者,这是用户行为触发的副作用,那么相关逻辑应该放在Event handlers中。

假设之前的代码逻辑是:

  1. 点击按钮,触发状态a变化
  2. useEffect执行,发送请求

应该修改为:

  1. 点击按钮,在事件回调中获取状态a的值
  2. 在事件回调中发送请求

经过这样修改,状态a变化发送请求之间不再有因果关系,后续对状态a的修改不会再有无意间触发请求的顾虑。

总结

当我们编写组件时,应该尽量将组件编写为纯函数。

对于组件中的副作用,首先应该明确:

用户行为触发的还是视图渲染后主动触发的

对于前者,将逻辑放在Event handlers中处理。

对于后者,使用useEffect处理。

这也是为什么useEffect所在章节在新文档中叫做Escape Hatches —— 大部分情况下,你不会用到useEffect,这只是其他情况都不适应时的逃生舱。

原文:https://segmentfault.com/a/1190000041942007

收起阅读 »

web前端-JavaScript中的函数(创建,参数,返回值,方法,函数作用域,立即执行函数)

文章目录 简介函数的创建1 用构造函数创建2 用函数声明创建3 用函数表达式创建 函数的参数 参数特性1 调用函数时解析器不会检查实参的类型2 调用函数时解析器不会检查实参的数量3 当形参和实参过多,可以用一个对象封装 函数的返回值...
继续阅读 »


文章目录

  • 简介
  • 函数的创建

  • 函数的参数






  • 函数的返回值
  • 立即执行函数
  • 方法
  • 函数作用域
  • 补充:JavaScript中的作用域相关概念
  •  

    简介


    函数(Function


    • 函数也是一个对象
    • 函数中可以封装一些功能(代码),在需要时可以执行这些功能(代码)。
    • 函数中可以保存一些代码,在需要的时候调用。

    函数的创建




    在JavaScript中有三种方法来创建函数


    1. 构造函数创建
    2. 函数声明创建
    3. 函数表达式创建

    其中第一种方法在实际使用中并不常用。创建函数之后需调用函数才可执行函数体内的代码。
    函数的调用:





    语法:函数名();



    1 用构造函数创建



    语法:var 函数名 = new Function(“语句;”)





    使用new关键字创建一个函数,将要封装的功能(代码)以字符串的形式传递给封装函数,在调用函数时,封装的功能(代码)会按照顺序执行。


    2 用函数声明创建



    语法:function 函数名([形参1,形参2....]){语句...}



    用函数声明显而易见的要简便许多,小括号中的形参视情况而写,语句写在中括号内。与构造函数不同的是不需要以字符串的形式写入。




    3 用函数表达式创建



    语法:var 变量(函数名)=function([形参1,形参2....]){语句...};



    函数表达式和函数声明的方式创建函数的方法相似,不同的是用函数表达式创建函数是将一个匿名函数赋值给一个变量,同时在语句结束后需加分号结尾。













    web前端-JavaScript中的函数(创建,参数,返回值,方法,函数作用域,立即执行函数)







    苏凉.py

    已于 2022-06-16 00:40:01 修改

    596



    收藏

    88


















    🐚作者简介:苏凉(专注于网络爬虫,数据分析,正在学习前端的路上)
    🐳博客主页:苏凉.py的博客
    🌐系列专栏:web前端基础教程
    👑名言警句:海阔凭鱼跃,天高任鸟飞。
    📰要是觉得博主文章写的不错的话,还望大家三连支持一下呀!!!
    👉关注✨点赞👍收藏📂






    简介


    函数(Function


    • 函数也是一个对象
    • 函数中可以封装一些功能(代码),在需要时可以执行这些功能(代码)。
    • 函数中可以保存一些代码,在需要的时候调用。

    函数的创建


    在JavaScript中有三种方法来创建函数


    1. 构造函数创建
    2. 函数声明创建
    3. 函数表达式创建

    其中第一种方法在实际使用中并不常用。创建函数之后需调用函数才可执行函数体内的代码。
    函数的调用:



    语法:函数名();



    1 用构造函数创建



    语法:var 函数名 = new Function(“语句;”)



    使用new关键字创建一个函数,将要封装的功能(代码)以字符串的形式传递给封装函数,在调用函数时,封装的功能(代码)会按照顺序执行。


    在这里插入图片描述


    2 用函数声明创建



    语法:function 函数名([形参1,形参2....]){语句...}



    用函数声明显而易见的要简便许多,小括号中的形参视情况而写,语句写在中括号内。与构造函数不同的是不需要以字符串的形式写入。


    在这里插入图片描述


    3 用函数表达式创建



    语法:var 变量(函数名)=function([形参1,形参2....]){语句...};



    函数表达式和函数声明的方式创建函数的方法相似,不同的是用函数表达式创建函数是将一个匿名函数赋值给一个变量,同时在语句结束后需加分号结尾。


    在这里插入图片描述


    函数的参数


    • 可以在函数的()中来指定一个或多个形参(形式参数)。
    • 多个形参之间使用,隔开,声明形参就相当于在函数内部声明了对应的变量但是并不赋值。
    • 调用函数时,可以在()中指定实参(实际参数),实参将会赋值给函数中对应的形参。



    参数特性


    1 调用函数时解析器不会检查实参的类型


    函数的实参可以时任意数据类型,在调用函数时传递的实参解析器并不会检查实参的类型,因此需要注意,是否有可能接收到非法的参数,如果有可能则需要对参数进行类型的检查。




    2 调用函数时解析器不会检查实参的数量


    在调用函数传入实参时,解析器不会检查实参的数量,当实参数大于形参数时,多余实参不会被赋值




    当实参数小于形参数时,没有被赋值的形参为undefined。


    3 当形参和实参过多,可以用一个对象封装


    当形参和实参数量过多时,我们很容易将其顺序搞乱或者传递参数时出错,此时我们可以将数据封装在一个对象中,在进行实参传递时,传入该对象即可。




    函数的返回值


    可以使用return来设置函数的返回值



    语法:return 值


     


  • return后的值将会作为函数的执行结果返回
  • 可以定义一个变量,来接收该结果。
  • 在return后的语句都不会执行。

  • 若return后不跟任何值或者不写return,函数的返回值都是undefined。


    另外,在函数体中return返回的是什么,变量接受的就是什么。


    立即执行函数


    • 函数定义完,立即被调用,这种函数叫做立即执行函数
    • 立即执行函数往往只会执行一次
    • 通常为匿名函数的调用。


    语法:(function(形参...){语句...})(实参...);





    方法


    对象的属性值可以时任意的数据类型,当属性值为一个函数时,在对象中调用该函数,就叫做调用该对象的方法。




    函数作用域




  • 调用函数时创建函数作用域,函数执行完毕以后,函数作用域销毁
  • 每调用一次函数就会创建一个新的函数作用域,他们之间是互相独立的
    在函数作用域中可以访问到全局作用域

  • 的变量,在全局作用域中无法访问到函数作用域的变量


    当在函数作用域操作一个变量时,它会先在自身作用域中寻找,如果有就直接使用,如果没有则向上一级作用域中寻找,直到找到全局作用域,如果全局作用域中依然没有找到,则会报错ReferenceError




    补充:JavaScript中的作用域相关概念







    在全局作用域中有一个全局对象window它代表的是一个浏览器的窗口,它由浏览器创建我们可以直接使用

  • 作用域指一个变量的作用范围
  • 在JavaScript中有两种作用域1.全局作用域 2.函数作用域
  • 直接编写在script标签中的JS代码,都在全局作用域
  • 全局作用域在页面打开时创建,在页面关闭时销毁

  • 简而言之我们创建的全局变量都作为一个属性保存在window这个对象中。


    而在函数中创建局部变量时,必须使用var关键字创建,否则为全局变量。



    收起阅读 »

    JavaScript映射与集合(Map、Set)数据类型基础知识介绍与使用

    文章目录 映射与集合(Map、Set)映射(Map)Map常用的方法不要使用map[key]访问属性对象作为Map的键Map的遍历与迭代默认的迭代方式forEach() 从数组、对象创建Map从数组、Map创建对象 集合(Set)集合迭代 ...
    继续阅读 »







    映射与集合(Map、Set)

    前文的学习过程中,我们已经了解了非常多的数据类型,包括基础类型、复杂的对象、顺序存储的数组等。为了更好的应对现实生产中的情况,我们还需要学习更多的数据类型:映射(Map)和集合(Set)。
    映射(Map)

    Map是一个键值对构成的集合,和对象非常相似,都是由一个名称对应一个值组成的。Map和对象区别在于,Map的键可以采用任何类型的数据,而对象只能使用字符串作为属性名称
    Map常用的方法

    new Map()——创建Map对象;
    map.set(key, val)——添加一个键值对;
    map.get(key)——通过键找到val值,如果不存在key,返回undefined
    map.has(key)——判断map是否存在键key,存在返回true,不存在返回false
    map.delete(key)——删除指定键;
    map.clear()——清空map中所有的内容;
    map.size——map中键值对的数量;

    举个例子:

    let map = new Map()//创建一个空的Map
    map.set('name','xiaoming') //字符串作为键
    map.set(3120181049,'ID') //数字作为键
    map.set(true,'Bool') //bool作为键

    console.log(map.get('name'))//xiaoming
    console.log(map.has(true)) //true
    console.log(map.delete(true))//删除true键
    console.log(map.size) //2
    console.log(map.clear()) //清空
    console.log(map.size) //0


    代码执行结果:



    map.set(key, val)方法返回map本身。


    不要使用map[key]访问属性


    虽然map[key]方式同样可以访问映射的键值对,但是不推荐使用这种方式,因为它会造成歧义。我们可以看下面的案例:



    let map = new Map()
    map[123] = 123 //创建一个键值对
    console.log(map[123])//123
    console.log(map['123'])


    这里就出现了一个奇怪的结果:


    image-20220610213719690


    不仅使用键123还可以使用'123'访问数据。


    甚至,如果我们使用map.set()map[]混用的方式,会引起程序错误。


    JavaScript中,如果我们对映射使用了map[key]=val的方式,引擎就会把map视为plain object,它暗含了对应所有相应的限制(仅支持StringSymbol键)。


    所以,我们不要使用map[key]的方式访问Map的属性!!


    对象作为Map的键




    由于Map对键的类型不做任何限制,我们还可以把对象当作键值使用:
    let clazz = {className:'9年1班'}
    let school = new Map()
    school.set(clazz,{stu1:'xiaoming',stu2:'xiaohong'})
    console.log(school.get(clazz))



    代码执行结果:


    image-20220610215432261


    在对象中,对象是不能作为属性名称存在的,如果我们把对象作为属性名,也会发生奇怪的事:

    let obj = {}
    let objKey = {key:'key'}
    obj[objKey] = 'haihaihai'
    console.log(obj['[object Object]'])


    代码执行结果:


    image-20220610215731673


    发生这种现象的原因也非常简单,对象会把非字符串、Symbol类型的属性名转为字符串类型,对象相应的就转为'[object Object]'了,于是对象中就出现了一个名为'[object Object]'的属性。





    Map键值比较方法


    Map使用SameValueZero算法比较键值是否相等,和===差不多,但是NaNNaN是相等的,所以NaN也可以作为键使用!





    链式调用


    由于map.set返回值是map本身,我们可以使用如下调用方式:


    map.set(1,1)
    .set(2,2)
    .set(3,3)




    Map的遍历与迭代


    我们可以在以下三个函数的帮助下完成映射的迭代:


    1. map.keys()——返回map所有键的可迭代对象;
    2. map.values()——返回map所有值的可迭代对象;
    3. map.entries()——返回map所有键值对的可迭代对象;

    举个栗子:



    let map = new Map([
    ['key1',1],
    ['key2',2],
    ['key3',3],
    ])

    //遍历所有的键
    for(let key of map.keys()){
    console.log(key)
    }

    //遍历所有的值
    for(let val of map.values()){
    console.log(val)
    }

    //遍历所有的键值对
    for(let ky of map.entries()){
    console.log(ky)
    }


    代码执行结果:


    image-20220611202407661



    遍历的顺序


    遍历的顺序和元素插入顺序是相同的,这是和对象的区别之一。





    默认的迭代方式


    实际上,我们很少使用map.entries()方法遍历Map中的键值对,因为map.entries()map的默认遍历方式,我们可以直接使用如下代码:


    let map = new Map([
    ['key1',1],
    ['key2',2],
    ['key3',3],
    ])
    for(let kv of map){
    console.log(kv)
    }





    码执行结果:


    image-20220611203140858


    forEach()


    我们还可以通过Map内置的forEach()方法,为每个元素设置一个遍历方法,就像遍历数组一样。


    举例如下:




    let map = new Map([
    ['key1',1],
    ['key2',2],
    ['key3',3],
    ])
    map.forEach((val,key,map)=>{
    console.log(`${key}-${val}`)
    })



    代码执行结果:


    image-20220611203643650


    从数组、对象创建Map


    可能童鞋们已经发现了,在上面的案例中,我们使用了一种独特的初始化方式(没有使用set方法)




    let map = new Map([
    ['key1',1],
    ['key2',2],
    ['key3',3],
    ])




    我们通过向new Map()传入一个数组,完成了快速的映射创建。


    我们还可以通过Object.entires(obj)方法将对象转为数组,该数组的格式和Map需要的格式完全相同。


    举个例子:




    let obj = {
    xiaoming:'heiheihei',
    xiaohong:'hahahahah'
    }
    let map = new Map(Object.entries(obj))
    console.log(map)



    代码执行结果:


    image-20220611205622630


    Object.entries(obj)会返回obj对应的数组:[['xiaoming':'heiheihei'],['xiaoming':'hahahahah']]




    从数组、Map创建对象


    Object.fromEntries()Object.entries()功能相反,可以把数组和Map转为对象。


    数组转对象:




    let obj = Object.fromEntries([
    ['key1','val1'],
    ['key2','val2'],
    ['key3','val3'],
    ])
    console.log(obj)



    代码执行结果:


    image-20220611210835380


    Map转对象:




    let map = new Map()
    map.set('key1','val1')
    .set('key2','val2')
    .set('key3','val3')
    let obj = Object.fromEntries(map)
    console.log(obj)



    代码执行结果:


    image-20220611211125496


    map.entries()会返回映射对应的键值对数组,我们也可以使用一种稍微麻烦的方式:

    let obj = Object.fromEntries(map.entries())





    1. new Set([iter])——创建一个集合,如果传入了一个可迭代变量(例如数组),就使用这个变量初始化集合
    2. set.add(val)——向集合中添加一个元素val
    3. set.delete(val)——删除集合中的val




    4. set.has(val)——判断集合中是否存在val,存在返回true,否则返回false
    5. set.clear()——清空集合中所有的元素
    6. set.size——返回集合中元素的数量


    集合使用案例:


    let set = new Set()
    let xiaoming = {name:'xiaoming'}
    let xiaohong = {name:'xiaohong'}
    let xiaojunn = {name:'xiaojunn'}



    set.add(xiaoming)
    set.add(xiaohong)
    set.add(xiaojunn)
    console.log(set)



    代码执行结果:


    image-20220611212417105





    虽然Set的功能很大程度上可以使用Array代替,但是如果使用arr.find判断元素是否重复,就会造成巨大的性能开销。


    所以我们需要在合适的场景使用合适的数据结构,从而保证程序的效率。



    集合迭代




    集合的迭代非常简单,我们可以使用for...offorEach两种方式:

    let set = new Set(['xiaoming','xiaohong','xiaoli'])//使用数组初始化集合
    for(let val of set){
    console.log(val)
    }
    set.forEach((val,valAgain,set)=>{
    console.log(val)
    })


    代码执行结果:


    image-20220611212802952




    注意,使用forEach遍历集合时,和map一样有三个参数,而且第一个和第二个参数完全相同。这么做的目的是兼容Map,我们可以方便的使用集合替换Map而程序不会出错。


    Map中使用的方法,Set同样适用:


    1. set.keys()——返回一个包含所有值的可迭代对象
    2. set.values()——返回值和set.keys()完全相同
    3. set.entries()——返回[val,val]可迭代对象



    看起啦这些方法有些功能上的重复,很奇怪。实际上,和forEach一样,都是为了和Map兼容。


    总结


    Map 是一个带键的数据项的集合。




    常用方法:





    1. map.get(key) —— 根据键来返回值,如果 map 中不存在对应的 key,则返回 undefined
    2. map.has(key) —— 如果 key 存在则返回 true,否则返回 false
    3. new Map([iter]) —— 创建 map,可选择带有 [key,value] 对的 iterable(例如数组)来进行初始化;
    4. map.set(key, val) —— 根据键存储值,返回 map 自身,可用于链式插入元素;




    5. map.delete(key) —— 删除指定键对应的值,如果在调用时 key 存在,则返回 true,否则返回 false
    6. map.clear() —— 清空 map中所有键值对 ;
    7. map.size —— 返回键值对个数

    与普通对象 Object 的不同点主要是任何类型都可以作为键,包括对象、NaN


    Set —— 是一组值的集合。


    常用方法和属性:
















    MapSet 中迭代总是按照值插入的顺序进行的,所以我们不能说这些集合是无序的,但是我们不能对元素进行重新排序,也不能直接按其编号来获取元素。


















  • new Set([iter]) —— 创建 set,可选择带有 iterable(例如数组)来进行初始化。
  • set.add(value) —— 添加一个值(如果 value 存在则不做任何修改),返回 set 本身。
  • set.delete(value) —— 删除值,如果 value 在这个方法调用的时候存在则返回 true ,否则返回 false
  • set.has(value) —— 如果 value 在 set 中,返回 true,否则返回 false
  • set.clear() —— 清空 set。
  • set.size —— 元素的个数。
  • 收起阅读 »

    React-Native热更新 - 3分钟教你实现

    此文使用当前最新版本的`RN`与`Code-Push`进行演示,其中的参数不会过多进行详细解释,更多参数解释可参考其它文章,这里只保证APP能正常进行热更新操作,方便快速入门,跟着大猪一起来快活吧。操作指南以下操作在Mac系统上完成的,毕竟 大猪 工作多年之后...
    继续阅读 »

    此文使用当前最新版本的`RN`与`Code-Push`进行演示,其中的参数不会过多进行详细解释,更多参数解释可参考其它文章,这里只保证APP能正常进行热更新操作,方便快速入门,跟着大猪一起来快活吧。

    操作指南

    以下操作在Mac系统上完成的,毕竟 大猪 工作多年之后终于买得起一个Mac了。

    1. 创建`React-Native`项目

    ```
    react-native init dounineApp
    `
    ``

    2. 安装`code-push-cli`

    ```
    npm install -g code-push-cli
    `
    ``

    3. 注册`code-push`帐号

    ```
    code-push register
    Please login to Mobile Center in the browser window we've just opened.
    Enter your token from the browser:
    #会弹出一个浏览器,让你注册,可以使用github帐号对其进行授权,授权成功会给一串Token,点击复制,在控制进行粘贴回车(或者使用code-push login命令)。
    `
    ``
    ```
    Enter your token from the browser: b0c9ba1f91dd232xxxxxxxxxxxxxxxxx
    #成功提示如下方
    Successfully logged-in. Your session file was written to /Users/huanghuanlai/.code-push.config. You can run the code-push logout command at any time to delete this file and terminate your session.
    `
    ``
    ![](http://upload-images.jianshu.io/upload_images/9028759-7736182c03cea82a.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

    4. 在`code-push`添加一个ios的app

    ```
    code-push app add dounineApp-ios ios react-native
    #成功提示如下方
    Successfully added the "dounineApp-ios" app, along with the following default deployments:
    ┌────────────┬──────────────────────────────────────────────────────────────────┐
    │ Name │ Deployment Key │
    ├────────────┼──────────────────────────────────────────────────────────────────┤
    │ Production │ yMAPMAjXpfXoTfxCd0Su9c4-U4lU6dec4087-57cf-4c9d-b0dc-ad38ce431e1d │
    ├────────────┼──────────────────────────────────────────────────────────────────┤
    │ Staging │ IjC3_iRGEZE8-9ikmBZ4ITJTz9wn6dec4087-57cf-4c9d-b0dc-ad38ce431e1d │
    └────────────┴──────────────────────────────────────────────────────────────────┘
    `
    ``

    5. 继续在`code-push`添加一个android的app

    ```
    code-push app add dounineApp-android android react-native
    #成功提示如下方
    Successfully added the "dounineApp-android" app, along with the following default deployments:
    ┌────────────┬──────────────────────────────────────────────────────────────────┐
    │ Name │ Deployment Key │
    ├────────────┼──────────────────────────────────────────────────────────────────┤
    │ Production │ PZVCGLlVW-0FtdoCF-3ZDWLcX58L6dec4087-57cf-4c9d-b0dc-ad38ce431e1d │
    ├────────────┼──────────────────────────────────────────────────────────────────┤
    │ Staging │ T0NshYi9X8nRkIe_cIRZGbAut90a6dec4087-57cf-4c9d-b0dc-ad38ce431e1d │
    └────────────┴──────────────────────────────────────────────────────────────────┘
    `
    ``

    6. 在项目根目录添加`react-native-code-push`

    ```
    npm install react-native-code-push --save
    #或者
    yarn add react-native-code-push
    `
    ``

    7. link react-native-code-push

    ```
    react-native link
    Scanning folders for symlinks in /Users/huanghuanlai/dounine/oschina/dounineApp/node_modules (8ms)
    ? What is your CodePush deployment key for Android (hit to ignore) T0NshYi9X8nRkIe_cIRZGbAut90a6dec4087-57cf-4c9d-b0dc-ad38ce431e1d
    #将刚才添加的Android App的Deployment Key复制粘贴到这里,复制名为Staging测试Deployment Key。
    rnpm-install info Linking react-native-code-push android dependency
    rnpm-install info Android module react-native-code-push has been successfully linked
    rnpm-install info Linking react-native-code-push ios dependency
    rnpm-install WARN ERRGROUP Group 'Frameworks' does not exist in your Xcode project. We have created it automatically for you.
    rnpm-install info iOS module react-native-code-push has been successfully linked
    Running ios postlink script
    ? What is your CodePush deployment key for iOS (hit to ignore) IjC3_iRGEZE8-9ikmBZ4ITJTz9wn6dec4087-57cf-4c9d-b0dc-ad38ce431e1d
    #继续复制Ios的Deployment Key
    Running android postlink script
    `
    ``

    8. 在`react-native`的`App.js`文件添加自动更新代码

    ```
    import codePush from "react-native-code-push";
    const codePushOptions = { checkFrequency: codePush.CheckFrequency.MANUAL };
    export default class App extends Component<{}> {
    componentDidMount(){
    codePush.sync({
    updateDialog: true,
    installMode: codePush.InstallMode.IMMEDIATE,
    mandatoryInstallMode:codePush.InstallMode.IMMEDIATE,
    //deploymentKey为刚才生成的,打包哪个平台的App就使用哪个Key,这里用IOS的打包测试
    deploymentKey: 'IjC3_iRGEZE8-9ikmBZ4ITJTz9wn6dec4087-57cf-4c9d-b0dc-ad38ce431e1d',
    });
    }
    ...
    `
    ``

    9. 运行项目在ios模拟器上

    ```
    react-native run-ios
    `
    ``

    如图下所显

    1:开启debug调试

    2:`CodePush`已经成功运行

    目前App已经是最新版本

    ![](http://upload-images.jianshu.io/upload_images/9028759-41607a87f412b06a.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

    10. 发布一个ios新版本

    ```
    code-push release-react dounineApp-ios ios
    `
    ``
    发布成功如图下
    ```
    Detecting ios app version:
    Using the target binary version value "1.0" from "ios/dounineApp/Info.plist".
    Running "react-native bundle" command:
    node node_modules/react-native/local-cli/cli.js bundle --assets-dest /var/folders/m_/xcdff0xd62j4l2xbn_nfz00w0000gn/T/CodePush --bundle-output /var/folders/m_/xcdff0xd62j4l2xbn_nfz00w0000gn/T/CodePush/main.jsbundle --dev false --entry-file index.js --platform ios
    Scanning folders for symlinks in /Users/huanghuanlai/dounine/oschina/dounineApp/node_modules (10ms)
    Scanning folders for symlinks in /Users/huanghuanlai/dounine/oschina/dounineApp/node_modules (10ms)
    Loading dependency graph, done.
    bundle: start
    bundle: finish
    bundle: Writing bundle output to: /var/folders/m_/xcdff0xd62j4l2xbn_nfz00w0000gn/T/CodePush/main.jsbundle
    bundle: Done writing bundle output
    Releasing update contents to CodePush:
    Upload progress:[==================================================] 100% 0.0s
    Successfully released an update containing the "/var/folders/m_/xcdff0xd62j4l2xbn_nfz00w0000gn/T/CodePush" directory to the "Staging" deployment of the "dounineApp-ios" app.
    `
    ``

    11. 重新Load刷新应用

    ![](http://upload-images.jianshu.io/upload_images/9028759-30c17d2f5db173cd.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

    12. 安卓发布

    与上面9~11步骤是一样的,命令改成Android对应的,以下命令结果简化

    1.修改App.js的deploymentKey为安卓的

    ```
    deploymentKey:'T0NshYi9X8nRkIe_cIRZGbAut90a6dec4087-57cf-4c9d-b0dc-ad38ce431e1d'
    `
    ``

    2.运行

    ```
    react-native run-android
    `
    ``

    3.发布

    ```
    code-push release-react dounineApp-android android
    `
    ``
    收起阅读 »

    React-Native iOS 列表(ListView)优化方案

    在项目开发中,很多地方用到了列表,而 React-Native 官网中提供的组件 ListView,虽然能够满足我们的需求,但是性能问题并没有很好的解决,对于需要展现大量数据的列表,app 的内存将会非常庞大。针对 React-Native 的列表性能问题,现...
    继续阅读 »

    在项目开发中,很多地方用到了列表,而 React-Native 官网中提供的组件 ListView,虽然能够满足我们的需求,但是性能问题并没有很好的解决,对于需要展现大量数据的列表,app 的内存将会非常庞大。针对 React-Native 的列表性能问题,现在提供几套可行性方案:

    1.利用 Facebook 提供的建议对 ListView 进行优化

    Facebook 官方对 ListView 的性能优化做了简单介绍,并提供了以下几个方法:

    • initialListSize
    • 这个属性用来指定我们第一次渲染时,要读取的行数。如果我们想尽可能的快,我们可以设置它为1, 然后可以在后续的帧中,填弃其它的行。每一次读取的行数,由 pageSize 决定.
    • pageSize
    • 在使用了 initialListSize 之后,ListView 根据 pageSize 来决定每一帧读取的行数,默认值为1, 但如果你的的 views 非常的小,并且读取时占的资源很少, 你可以调整这个值,在找到适合你的值。
    • scrollRenderAheadDistance
    • 如果我们的列表有2000个项,而让它一次性读取,它会导致内存和计算资源的耗尽。所以 scrollRenderAhead distance 可以指定,超出当前视图多少,继续宣染。
    • removeClippedSubviews
    • “当它设置为true时,当本地端的superview为offscreen时 ,不在屏幕上显示的子视图offscreen(它的overflow的值为hidden) 会被删除。它可以改善长列表的滚动的性能,默认值为true.
      这对于大的ListViews来说是一个非常重要。在Android, overflow的值通常为hidden. 所以我们并不需要担心它的设置,但是对于iOS来说,你需要设置row Container的样式为overflow: hidden。

    在使用了上述方法后,我们可以看到app的内存占用有了一定的下降(加载100张图片时的效果):

    使用前


    使用后


    3.桥接 Native tableview

    第二种方法里面,能够比较好的解决屏幕外的 cell 内存问题,但是和 native tableview 相比,并没有 native 的 cell 重用机制完美,那么,我们可以讲 native 的 tableview 桥接到 React-native 中来,让我们可以在 React-Native 中也可以重用 cell

    我们创建一些 VirtualView,他只是遵从了 RCTComponent 协议,其实并不是一个真正的 View,我把它形成一个组件,把它 Bridge 到 JS,这就使得,你在写 JSX 的时候,就可以直接用 VirtualView 来去做布局了。在RN里面做布局的时候我们用VirtualView来做布局。但是最终在 insertReactSubview 时,我们把这些 VirtualView 当做数据去处理,通过 VirtualView 和RealView 的对应关系,把它转化成一个真实的 View 对象添加到 TableView 中去。


    但是使用这种方法,我们需要将 tableview 的所有常用数据源方法和代理方法都桥接到 React-Native 中来,甚至对于一些 cell 组件,我们也需要自己桥接,并不能像 React-Native 那样使用自己的组件。当我们的需求比较复杂或者需求发生变化时,就需要重新桥接我们的自定义 cell,这样工作量就会比较大。大多数的 cell 里面如果做展示来用的话,Label 和 Image 基本上能够满足大多数的需求了。所以我们现在只是做了 Label 和 Image 的对应工作,但在 RN 的一些官方控件,在这个 view 里面都是没法直接使用的。

    4.用 JS 实现一套 cell 重用的逻辑

    基于 RN 的 ScrollView,我们也监听 OnScroll(),他往上滑的时候,我们需要把上面的 cellComponent 挪下来,挪到上面去用。但是这个方式最终的效果并不是特别好。

    问题在于,如果我们所有的 Cell 都是一样高的,里面的元素不是很多的情况下,性能还相对好一些,我们每次 OnScroll 的时候,他处理的Cell比较少。如果你希望有一个界面滚动能够达到流畅的话,所有的处理都需要在 16ms 内完成,但是这又造成了 onScroll 都要去刷新页面,导致这样的交互会非常非常多,导致你从 JS,到 native 的 bridge 要频繁的通讯,JS 中的很多处理方式都是异步的,使得这个方案的效果没有达到很好的预期。

    总结

    从上面的几种方案可以看出,方案1、2、3、4都能够比较好的解决列表的性能问题 ,而且各有优缺点,那么,我们在项目开发中该如何应用呢?

    • 当我们在进行列表展示的时候,如果数据量不是特别的庞大(不是无限滚动的),且界面比较复杂的时候,方案1能够比较好的解决性能问题,而且操作起来比较简单,只需要对 listview 的一些属性进行基本设置。
    • 当我们需要展示很多数据的时候(不是无限滚动的),我们可以使用方案2,对那些超出屏幕外的部分,对他进行组件最小化
    • 当我们需要展示大量数据(可以无限滚动的),我们可以通过方案3/4,来达到重用的目的


    收起阅读 »

    应急响应 WEB 分析日志攻击,后门木马(手动分析 和 自动化分析.)

    💛不登上悬崖💚,💛又怎么领略一览众山的绝顶风光💚🍪目录:🌲应急响应的概括:🌲应急响应阶段:🌲应急响应准备工作:🌲从入侵面及权限面进行排查:🌲工具下载🌲应急响应的日志分析:🌷手动化分析日志:🌷自动化化分析日志: (1)360星图.(支持 iis /...
    继续阅读 »


    💛不登上悬崖💚,💛又怎么领略一览众山的绝顶风光💚
    🍪目录:

    🌲应急响应的概括:

    🌲应急响应阶段:

    🌲应急响应准备工作:

    🌲从入侵面及权限面进行排查:

    🌲工具下载

    🌲应急响应的日志分析:

    🌷手动化分析日志:

    🌷自动化化分析日志:

    (1)360星图.(支持 iis / apache / nginx日志)

    (2)方便大量日志查看工具.

    🌷后门木马检测.

    (1)D盾_Web查杀. 

    🌲应急响应的概括:
    🌾🌾🌾应急响应”对应的英文是“Incident Response”或“Emergency Response”等,通常是指一个组织为了应对各种意外事件的发生所做的准备以及在事件发生后所采取的措.
    🌾🌾🌾网络安全应急响应:针对已经发生的或可能发生的安全事件进行监控、分析、协调、处理、保护资产安全.

    🌲应急响应阶段:

    保护阶段:断网,备份重要文件(防止攻击者,这些期间删除文件重要文件.)

    分析阶段:分析攻击行为,找出相应的漏洞.

    复现阶段:复现攻击者攻击的过程,有利于了解当前环境的安全问题和安全检测.

    修复阶段:对相应的漏洞提出修复.

    建议阶段:对漏洞和安全问题提出合理解决方案.
    目的:分析出攻击时间,攻击操作,攻击后果,安全修复等并给出合理解决方案

    🌲应急响应准备工作:
    (1)收集目标服务器各类信息.

    (2)部署相关分析软件及平台等.

    (3)整理相关安全渗透工具指纹库.

    (4)针对异常表现第一时间触发思路. 

    🌲从入侵面及权限面进行排查:
    有明确信息网站被入侵: 1.基于时间 2.基于操作 3.基于指纹 4.基于其他.


    无明确信息网站被入侵:(1)WEB 漏洞-检查源码类别及漏洞情况.
    (2)中间件漏洞-检查对应版本及漏洞情况.
    (3)第三方应用漏洞-检查是否存在漏洞应用.
    (4)操作系统层面漏洞-检查是否存在系统漏洞.
    (5)其他安全问题(口令,后门等)- 检查相关应用口令及后门扫描.


    🌲工具下载   链接:https://pan.baidu.com/s/14njkNfj3HisIKN26IYOZXQ 
                        提取码:tian 




    🌲应急响应的日志分析:


    🌷手动化分析日志:


    (1)弱口令的爆破日志.(可以看到是一个IP在同一个时间,使用多个账号和密码不停测试)





    (2)SQL注入的日志.(搜索 select 语句.)


    (3)有使用SQLmap工具的注入.(搜索SQLmap)


    我的靶场日志没有记录SQLmap.(这里就不加图了)


         


    (4)目录扫描日志.(看的时候会发现,前面的目录都是一样的.)




         


    (5)XSS攻击日志.(搜索:script,javascript,onclick,%3Cimg对这些关键字进行查看)
    (7)目录遍历攻击日志.


    (8)后门木马日志.(搜索连接工具:anTSword,菜刀,冰蝎等工具 排查后门.)


           


           


    🌷自动化化分析日志:


    (1)360星图.(支持 iis / apache / nginx日志


    1.设置日志分析路径.
    2.点击进行日志分析.


        


    3.点击查看日志.




        


    安全分析报告.




    常规分析报告.


         


    (2)方便大量日志查看工具.


    1.工具的设置.




    2.SQL注入攻击日志.


    3.目录遍历攻击.


    4.XSS攻击日志.


          


    🌷后门木马检测.


    (1)D盾_Web查杀.


    1.选择扫描的目录.




    2.扫描到的后门木马.












    收起阅读 »

    堆(优先级队列)

     目录 🥬堆的性质 🥬堆的分类  🥬堆的向下调整 🥬堆的建立 🥬堆得向上调整 🥬堆的常用操作 🍌入队列 🍌出队列 🍌获取队首元素 🥬TopK 问题 🥬堆的性质 堆逻辑上是一棵完全二叉树,堆物理上是保存在数组中 。总...
    继续阅读 »


     目录


    🥬堆的性质


    🥬堆的分类


     🥬堆的向下调整


    🥬堆的建立


    🥬堆得向上调整


    🥬堆的常用操作


    🍌入队列


    🍌出队列


    🍌获取队首元素


    🥬TopK 问题



    🥬堆的性质




    堆逻辑上是一棵完全二叉树,堆物理上是保存在数组中 。

    总结:一颗完全二叉树以层序遍历方式放入数组中存储,这种方式的主要用法就是堆的表示。
    并且 如果已知父亲(parent) 的下标,则:
    左孩子(left) 下标 = 2 * parent + 1;
    右孩子(right) 下标 = 2 * parent + 2;
    已知孩子(不区分左右)(child)下标,则:
    双亲(parent) 下标 = (child - 1) / 2;
    🥬堆的分类
    大堆:根节点大于左右两个子节点的完全二叉树 (父亲节点大于其子节点),叫做大堆,或者大根堆,或者最大堆 。 


    小堆:根节点小于左右两个子节点的完全二叉树叫
    小堆(父亲节点小于其子节点),或者小根堆,或者最小堆。

    🥬堆的向下调整

    现在有一个数组,逻辑上是完全二叉树,我们通过从根节点开始的向下调整算法可以把它调整成一个小堆或者大堆。向下调整算法有一个前提:左右子树必须是一个堆,才能调整。

    以小堆为例:

    1、先让左右孩子结点比较,取最小值。

    2、用较小的那个孩子结点与父亲节点比较,如果孩子结点<父亲节点,交换,反之,不交换。

    3、循环往复,如果孩子结点的下标越界,则说明已经到了最后,就结束。

    //parent: 每棵树的根节点
    //len: 每棵树的调整的结束位置

    public void shiftDown(int parent,int len){
    int child=parent*2+1; //因为堆是完全二叉树,没有左孩子就一定没有右孩子,所以最起码是有左孩子的,至少有1个孩子
    while(child<len){
    if(child+1<len && elem[child]<elem[child+1]){
    child++;//两孩子结点比较取较小值
    }
    if(elem[child]<elem[parent]){
    int tmp=elem[parent];
    elem[parent]=elem[child];
    elem[child]=tmp;
    parent=child;
    child=parent*2+1;
    }else{
    break;
    }
    }
    }


    🥬堆的建立
    给出一个数组,这个数组逻辑上可以看做一颗完全二叉树,但是还不是一个堆(左右子树不满足都是大堆或者小堆),现在我们通过算法,把它构建成一个堆(大堆或者小堆)。该怎么做呢?这里我们从倒数的第一个非叶子节点的子树开始调整,一直调整到根节点的树,就可以调整成堆。 这里我们就要用到刚才写的向下调整。

    public void creatHeap(int[] arr){
    for(int i=0;i<arr.length;i++){
    elem[i]=arr[i];
    useSize++;
    }
    for(int parent=(useSize-1-1)/2;parent>=0;parent--){//数组下标从0开始
    shiftDown(parent,useSize);
    }
    }



    建堆的空间复杂度为O(N),因为堆为一棵完全二叉树,满二叉树也是一种完全二叉树,我们用满二叉树(最坏情况下)来证明。


    🥬堆得向上调整


    现在有一个堆,我们需要在堆的末尾插入数据,再对其进行调整,使其仍然保持堆的结构,这就是向上调整。


    以大堆为例:




    代码示例:

    public void shiftup(int child){
    int parent=(child-1)/2;
    while(child>0){
    if(elem[child]>elem[parent]){
    int tmp=elem[parent];
    elem[parent]=elem[child];
    elem[child]=tmp;
    child=parent;
    parent=(child-1)/2;
    }else{
    break;
    }
    }
    }



    🥬堆的常用操作


    🍌入队列


    往堆里面加入元素,就是往最后一个位置加入,然后在进行向上调整

    public boolean isFull(){
    return elem.length==useSize;
    }

    public void offer(int val){
    if(isFull()){
    elem= Arrays.copyOf(elem,2*elem.length);//扩容
    }
    elem[useSize++]=val;
    shiftup(useSize-1);
    }



    🍌获取队首元素



    public int peek() {
    if (isEmpty()) {
    throw new RuntimeException("优先级队列为空");
    }
    return elem[0];
    }


    🥬TopK 问题

    给你6个数据,求前3个最大数据。这时候我们用堆怎么做的?

    解题思路:

    1、如果求前K个最大的元素,要建一个小根堆。
    2、如果求前K个最小的元素,要建一个大根堆。
    3、第K大的元素。建一个小堆,堆顶元素就是第K大的元素。
    4、第K小的元素。建一个大堆,堆顶元素就是第K大的元素。

    🍌举个例子:求前n个最大数据

     
    代码示例:

    public static int[] topK(int[] array,int k){
    //创建一个大小为K的小根堆
    PriorityQueue<Integer> minHeap=new PriorityQueue<>(k, new Comparator<Integer>() {
    @Override
    public int compare(Integer o1, Integer o2) {
    return o1-o2;
    }
    });
    //遍历数组中元素,将前k个元素放入队列中
    for(int i=0;i<array.length;i++){
    if(minHeap.size()<k){
    minHeap.offer(array[i]);
    }else{
    //从k+1个元素开始,分别和堆顶元素比较
    int top=minHeap.peek();
    if(array[i]>top){
    //先弹出后存入
    minHeap.poll();
    minHeap.offer(array[i]);
    }
    }
    }
    //将堆中元素放入数组中
    int[] tmp=new int[k];
    for(int i=0;i< tmp.length;i++){
    int top=minHeap.poll();
    tmp[i]=top;
    }
    return tmp;
    }

    public static void main(String[] args) {
    int[] array={12,8,23,6,35,22};
    int[] tmp=topK(array,3);
    System.out.println(Arrays.toString(tmp));
    }



    结果:


    🍌数组排序


     再者说如果要对一个数组进行从小到大排序,要借助大根堆还是小根堆呢?


    ---->大根堆


      代码示例:

    public void heapSort(){
    int end=useSize-1;
    while(end>0){
    int tmp=elem[0];
    elem[0]=elem[end];
    elem[end]=tmp;
    shiftDown(0,end);//假设这里向下调整为大根堆
    end--;
    }
    }



    🥬小结


    以上就是今天的内容了,有什么问题可以在评论区留言✌✌✌



    收起阅读 »

    Kotlin 协程调度切换线程是时候解开真相了

    在前面的文章里,通过比较基础的手段演示了如何开启协程、如何挂起、恢复协程。并没有涉及到如何切换线程执行,而没有切换线程功能的协程是没有灵魂的。 本篇将重点分析协程是如何切换线程执行以及如何回到原来的线程执行等知识。 通过本篇文章,你将了解到: 如何指定协程...
    继续阅读 »

    在前面的文章里,通过比较基础的手段演示了如何开启协程、如何挂起、恢复协程。并没有涉及到如何切换线程执行,而没有切换线程功能的协程是没有灵魂的。

    本篇将重点分析协程是如何切换线程执行以及如何回到原来的线程执行等知识。

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




    1. 如何指定协程运行的线程?

    2. 协程调度器原理

    3. 协程恢复时线程的选择



    1. 如何指定协程运行的线程?


    Android 切换线程常用手法


    常规手段


    平常大家用的切换到主线程的手段:Activity.runOnUiThread(xx),View.post(xx),Handler.sendMessage(xx) 等简单方式。另外还有一些框架,如AsyncTask、RxJava、线程池等。
    它们本质上是借助了Looper+Handler功能。

    先看个Demo,在子线程获取学生信息,拿到结果后切换到主线程展示:


        private inner class MyHandler : Handler(Looper.getMainLooper()) {
    override fun handleMessage(msg: Message) {
    //主线程弹出toast
    Toast.makeText(context, msg.obj.toString(), Toast.LENGTH_SHORT).show()
    }
    }

    //获取学生信息
    fun showStuInfo() {
    thread {
    //模拟网络请求
    Thread.sleep(3000)
    var handler = MyHandler()
    var msg = Message.obtain()
    msg.obj = "我是小鱼人"
    //发送到主线程执行
    handler.sendMessage(msg)
    }
    }

    我们知道Android UI 刷新是基于事件驱动的,主线程一直尝试从事件队列里拿到待执行的事件,没拿到就等待,拿到后就执行对应的事件。这也是Looper的核心功能,不断检测事件队列,而往队列里放事件即是通过Handler来操作的。



    子线程通过Handler 往队列里存放事件,主线程在遍历队列,这就是一次子线程切换到主线程运行的过程。



    当然了,因为主线程有消息队列,若想要抛事件到子线程执行,在子线程构造消息队列即可。


    协程切换到主线程


    同样的功能,用协程实现:


        fun showStuInfoV2() {
    GlobalScope.launch(Dispatchers.Main) {
    var stuInfo = withContext(Dispatchers.IO) {
    //模拟网络请求
    Thread.sleep(3000)
    "我是小鱼人"
    }

    Toast.makeText(context, stuInfo, Toast.LENGTH_SHORT).show()
    }
    }

    很明显,协程简洁太多。

    相较于常规手段,协程无需显示构造线程,也无需显示通过Handler发送,在Handler里接收信息并展示。

    我们有理由猜测,协程内部也是通过Handler+Looper实现切换到主线程运行的。


    协程切换线程


    当然协程不只能够从子线程切换到主线程,也可以从主线程切换到子线程,甚至在子线程之间切换。


        fun switchThread() {
    println("我在某个线程,准备切换到主线程")
    GlobalScope.launch(Dispatchers.Main) {
    println("我在主线程,准备切换到子线程")
    withContext(Dispatchers.IO) {
    println("我在子线程,准备切换到子线程")
    withContext(Dispatchers.Default) {
    println("我在子线程,准备切换到主线程")
    withContext(Dispatchers.Main) {
    println("我在主线程")
    }
    }
    }
    }
    }

    无论是launch()函数还是withContext()函数,只要我们指定了运行的线程,那么协程将会在指定的线程上运行。


    2. 协程调度器原理


    指定协程运行的线程


    接下来从launch()源码出发,一步步探究协程是如何切换线程的。

    launch()简洁写法:


        fun launch1() {
    GlobalScope.launch {
    println("launch default")
    }
    }

    launch()函数有三个参数,前两个参数都有默认值,第三个是我们的协程体,也即是 GlobalScope.launch 花括号里的内容。


    #Builders.common.kt
    public fun CoroutineScope.launch(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> Unit
    ): Job {
    //构造新的上下文
    val newContext = newCoroutineContext(context)
    //构造completion
    val coroutine = if (start.isLazy)
    LazyStandaloneCoroutine(newContext, block) else
    StandaloneCoroutine(newContext, active = true)
    //开启协程
    coroutine.start(start, coroutine, block)
    return coroutine
    }

    接着看newCoroutineContext 实现:


    #CoroutineContext.kt
    actual fun CoroutineScope.newCoroutineContext(context: CoroutineContext): CoroutineContext {
    //在Demo 环境里 coroutineContext = EmptyCoroutineContext
    val combined = coroutineContext + context
    //DEBUG = false
    val debug = if (DEBUG) combined + CoroutineId(COROUTINE_ID.incrementAndGet()) else combined
    //没有指定分发器,默认使用的分发器为:Dispatchers.Default
    //若是指定了分发器,就用指定的
    return if (combined !== Dispatchers.Default && combined[ContinuationInterceptor] == null)
    debug + Dispatchers.Default else debug
    }

    这块涉及到CoroutineContext 一些重载运算符的操作,关于CoroutineContext 本次不会深入,只需理解其意思即可。


    只需要知道:

    CoroutineContext 里存放着协程的分发器。


    协程有哪些分发器呢?


    Dispatchers.Main



    UI 线程,在Android里为主线程



    Dispatchers.IO



    IO 线程,主要执行IO 操作



    Dispatchers.Default



    主要执行CPU密集型操作,比如一些计算型任务



    Dispatchers.Unconfined



    不特意指定使用的线程



    指定协程在主线程运行


    不使用默认参数,指定协程的分发器:


        fun launch1() {
    GlobalScope.launch(Dispatchers.Main) {
    println("我在主线程执行")
    }
    }

    以此为例,继续分析其源码。

    上面提到过,开启协程使用coroutine.start(start, coroutine, block)函数:



    #AbstractCoroutine.kt
    fun <R> start(start: CoroutineStart, receiver: R, block: suspend R.() -> T) {
    //start 为CoroutineStart里的函数
    //最终会调用到invoke
    start(block, receiver, this)
    }
    #CoroutineStart.kt
    public operator fun <R, T> invoke(block: suspend R.() -> T, receiver: R, completion: Continuation<T>): Unit =
    when (this) {
    //this 指的是StandaloneCoroutine,默认走default
    CoroutineStart.DEFAULT -> block.startCoroutineCancellable(receiver, completion)
    CoroutineStart.ATOMIC -> block.startCoroutine(receiver, completion)
    CoroutineStart.UNDISPATCHED -> block.startCoroutineUndispatched(receiver, completion)
    CoroutineStart.LAZY -> Unit // will start lazily
    }

    CoroutineStart.DEFAULT、CoroutineStart.ATOMIC 表示的是协程的启动方式,其中DEFAULT 表示立即启动,也是默认启动方式。


    接下来就是通过block去调用一系列的启动函数,这部分我们之前有详细分析过,此处再简单过一下:



    block 代表的是协程体,其实际编译结果为:匿名内部类,该类继承自SuspendLambda,而SuspendLambda 间接实现了Continuation 接口。



    继续看block的调用:


    #Cancellable.kt
    //block 的扩展函数
    internal fun <R, T> (suspend (R) -> T).startCoroutineCancellable(
    receiver: R, completion: Continuation<T>,
    onCancellation: ((cause: Throwable) -> Unit)? = null
    ) =
    //runSafely 为高阶函数,里边就是调用了"{}"里的内容
    runSafely(completion) {
    createCoroutineUnintercepted(receiver, completion).intercepted().resumeCancellableWith(Result.success(Unit), onCancellation)
    }

    流程流转到createCoroutineUnintercepted()函数了,在少年,你可知 Kotlin 协程最初的样子? 里有重点分析过:该函数是真正创建协程体的地方。


    直接上代码:


    #IntrinsicsJvm.kt
    actual fun <R, T> (suspend R.() -> T).createCoroutineUnintercepted(
    receiver: R,
    completion: Continuation<T>
    ): Continuation<Unit> {
    //包装completion
    val probeCompletion = probeCoroutineCreated(completion)
    return if (this is BaseContinuationImpl)
    //创建协程体类
    //receiver completion 皆为协程体对象 StandaloneCoroutine
    create(receiver, probeCompletion)
    else {
    createCoroutineFromSuspendFunction(probeCompletion) {
    (this as Function2<R, Continuation<T>, Any?>).invoke(receiver, it)
    }
    }
    }

    该函数的功能为创建一个协程体类,我们暂且称之为MyAnnoy。


    class MyAnnoy extends SuspendLambda implements Function2 {
    @Nullable
    @Override
    protected Object invokeSuspend(@NotNull Object o) {
    //...协程体逻辑
    return null;
    }
    @NotNull
    @Override
    public Continuation<Unit> create(@NotNull Continuation<?> completion) {
    //...创建MyAnnoy
    return null;
    }
    @Override
    public Object invoke(Object o, Object o2) {
    return null;
    }
    }

    新的MyAnnoy 创建完成后,调用intercepted(xx)函数,这个函数很关键:


    #Intrinsics.Jvm.kt
    public actual fun <T> Continuation<T>.intercepted(): Continuation<T> =
    //判断如果是ContinuationImpl,则转为ContinuationImpl 类型
    //继而调用intercepted()函数
    (this as? ContinuationImpl)?.intercepted() ?: this

    此处为什么要将MyAnnoy 转为ContinuationImpl ?

    因为它要调用ContinuationImpl里的intercepted() 函数:


    #ContinuationImpl.kt
    public fun intercepted(): Continuation<Any?> =
    intercepted
    //1、如果intercepted 为空则从context里取数据
    //2、如果context 取不到,则返回自身,最后给intercepted 赋值
    ?: (context[ContinuationInterceptor]?.interceptContinuation(this) ?: this)
    .also { intercepted = it }

    先看intercepted 变量类型:


    #ContinuationImpl.kt
    private var intercepted: Continuation<Any?>? = null

    还是Continuation 类型,初始时intercepted = null。

    context[ContinuationInterceptor] 表示从CoroutineContext里取出key 为ContinuationInterceptor 的Element。

    既然要取出,那么得要放进去的时候,啥时候放进去的呢?


    答案是:



    newCoroutineContext(context) 构造了新的CoroutineContext,里边存放了分发器。



    又因为我们设定的是在主线程进行分发:Dispatchers.Main,因此context[ContinuationInterceptor] 取出来的是Dispatchers.Main。


    Dispatchers.Main 定义:


    #Dispatchers.kt
    public actual val Main: MainCoroutineDispatcher get() = MainDispatcherLoader.dispatcher
    #MainCoroutineDispatcher.kt
    public abstract class MainCoroutineDispatcher : CoroutineDispatcher() {}

    MainCoroutineDispatcher 继承自 CoroutineDispatcher,而它里边有个函数:


    #CoroutineDispatcher.kt
    public final override fun <T> interceptContinuation(continuation: Continuation<T>): Continuation<T> =
    DispatchedContinuation(this, continuation)

    而 Dispatchers.Main 调用的就是interceptContinuation(xx)函数。

    该函数入参为Continuation 类型,也就是MyAnnoy 对象,函数的内容很简单:




    • 构造DispatchedContinuation 对象,传入的参数分别是Dispatchers.Main和MyAnnoy 对象。

    • Dispatchers.Main、MyAnnoy 分别赋值给成员变量dispatcher和continuation。



    DispatchedContinuation 继承自DispatchedTask,它又继承自SchedulerTask,本质上就是Task,Task 实现了Runnable接口:


    #Tasks.kt
    internal abstract class Task(
    @JvmField var submissionTime: Long,
    @JvmField var taskContext: TaskContext
    ) : Runnable {
    //...
    }

    至此,我们重点关注其实现了Runnable接口里的run()函数即可。


    再回过头来看构造好DispatchedContinuation 之后,调用resumeCancellableWith()函数:


    #DispatchedContinuation.kt
    override fun resumeWith(result: Result<T>) {
    val context = continuation.context
    val state = result.toState()
    //需要分发
    if (dispatcher.isDispatchNeeded(context)) {
    _state = state
    resumeMode = MODE_ATOMIC
    //调用分发器分发
    dispatcher.dispatch(context, this)
    } else {
    executeUnconfined(state, MODE_ATOMIC) {
    withCoroutineContext(this.context, countOrElement) {
    continuation.resumeWith(result)
    }
    }
    }
    }

    而Demo里此处的dispatcher 即为Dispatchers.Main。


    好了,总结一下launch()函数的功能:



    image.png


    Dispatchers.Main 实现


    接着来看看Dispatchers.Main 如何分发任务的,先看其实现:


    #MainDispatcherLoader.java
    internal object MainDispatcherLoader {

    //默认true
    private val FAST_SERVICE_LOADER_ENABLED = systemProp(FAST_SERVICE_LOADER_PROPERTY_NAME, true)

    @JvmField
    val dispatcher: MainCoroutineDispatcher = loadMainDispatcher()
    //构造主线程分发
    private fun loadMainDispatcher(): MainCoroutineDispatcher {
    return try {
    val factories = if (FAST_SERVICE_LOADER_ENABLED) {
    //加载分发器工厂①
    FastServiceLoader.loadMainDispatcherFactory()
    } else {
    ...
    }
    //通过工厂类,创建分发器②
    factories.maxByOrNull { it.loadPriority }?.tryCreateDispatcher(factories)
    ?: createMissingDispatcher()
    } catch (e: Throwable) {
    ...
    }
    }
    }

    先看①:


    #FastServiceLoader.kt
    internal fun loadMainDispatcherFactory(): List<MainDispatcherFactory> {
    val clz = MainDispatcherFactory::class.java
    //...
    return try {
    //反射构造工厂类:AndroidDispatcherFactory
    val result = ArrayList<MainDispatcherFactory>(2)
    FastServiceLoader.createInstanceOf(clz,
    "kotlinx.coroutines.android.AndroidDispatcherFactory")?.apply { result.add(this) }
    FastServiceLoader.createInstanceOf(clz,
    "kotlinx.coroutines.test.internal.TestMainDispatcherFactory")?.apply { result.add(this) }
    result
    } catch (e: Throwable) {
    //...
    }
    }

    该函数返回的工厂类为:AndroidDispatcherFactory。


    再看②,拿到工厂类后,就该用它来创建具体的实体了:


    #HandlerDispatcher.kt
    internal class AndroidDispatcherFactory : MainDispatcherFactory {
    //重写createDispatcher 函数,返回HandlerContext
    override fun createDispatcher(allFactories: List<MainDispatcherFactory>) =
    HandlerContext(Looper.getMainLooper().asHandler(async = true), "Main")
    //...
    }

    //定义
    internal class HandlerContext private constructor(
    private val handler: Handler,
    private val name: String?,
    private val invokeImmediately: Boolean
    ) : HandlerDispatcher(), Delay {
    }

    最终创建了HandlerContext。

    HandlerContext 继承自类:HandlerDispatcher


    #HandlerDispatcher.kt
    sealed class HandlerDispatcher : MainCoroutineDispatcher(), Delay {
    //重写分发函数
    override fun dispatch(context: CoroutineContext, block: Runnable) {
    //抛到主线程执行,handler为主线程的Handler
    handler.post(block)
    }
    }

    很明显了,DispatchedContinuation里借助dispatcher.dispatch()进行分发,而dispatcher 是Dispatchers.Main,最终的实现是HandlerContext。

    因此dispatch() 函数调用的是HandlerDispatcher.dispatch()函数,该函数里将block 抛到了主线程执行。

    block 为啥是呢?

    block 其实是DispatchedContinuation 对象,从上面的分析可知,它间接实现了Runnable 接口。

    查看其实现:


    #DispatchedTask.kt
    override fun run() {
    val taskContext = this.taskContext
    var fatalException: Throwable? = null
    try {
    //delegate 为DispatchedContinuation 本身
    val delegate = delegate as DispatchedContinuation<T>
    //delegate.continuation 为我们的协程体 MyAnnoy
    val continuation = delegate.continuation
    withContinuationContext(continuation, delegate.countOrElement) {
    val context = continuation.context
    //...
    val job = if (exception == null && resumeMode.isCancellableMode) context[Job] else null
    if (job != null && !job.isActive) {
    //...
    } else {
    if (exception != null) {
    continuation.resumeWithException(exception)
    } else {
    //执行协程体
    continuation.resume(getSuccessfulResult(state))
    }
    }
    }
    } catch (e: Throwable) {
    //...
    } finally {
    //...
    }
    }

    continuation 变量是我们的协程体:MyAnnoy。

    MyAnnoy.resume(xx) 这函数我们很熟了,再重新熟悉一下:


    #ContinuationImpl.kt
    override fun resumeWith(result: Result<Any?>) {
    // This loop unrolls recursion in current.resumeWith(param) to make saner and shorter stack traces on resume
    var current = this
    var param = result
    while (true) {
    with(current) {
    //completion 即为开始时定义的StandaloneCoroutine
    val completion = completion!! // fail fast when trying to resume continuation without completion
    val outcome: Result<Any?> =
    try {
    //执行协程体里的代码
    val outcome = invokeSuspend(param)
    if (outcome === kotlin.coroutines.intrinsics.COROUTINE_SUSPENDED) return
    kotlin.Result.success(outcome)
    } catch (exception: Throwable) {
    kotlin.Result.failure(exception)
    }
    //...
    }
    }
    }

    invokeSuspend(param) 调用的是协程体里的代码,也就是launch 花括号里的内容,因此这里面的内容是主线程执行的。


    再来看看launch(Dispatchers.Main)函数执行步骤如下:




    1. 分发器HandlerContext 存储在CoroutineContext(协程上下文)里。

    2. 构造DispatchedContinuation 分发器,它持有变量dispatcher=HandlerContext,continuation=MyAnnoy。

    3. DispatchedContinuation 调用dispatcher(HandlerContext) 进行分发。

    4. HandlerContext 将Runnable(DispatchedContinuation) 抛到主线程。



    经过上面几步,launch(Dispatchers.Main) 任务算是完成了,至于Runnable什么时候执行与它无关了。


    当Runnable 在主线程被执行后,从DispatchedContinuation 里取出continuation(MyAnnoy),并调用continuation.resume()函数,进而执行MyAnnoy.invokeSuspend()函数,最后执行了launch{}协程体里的内容。

    于是协程就愉快地在主线程执行了。


    老规矩,结合代码与函数调用图:



    image.png


    3. 协程恢复时线程的选择


    以主线程为例,我们知道了协程指定线程运行的原理。

    想象另一种场景:



    在协程里切换了子线程执行,子线程执行完毕后还会回到主线程执行吗?



    对上述Demo进行改造:


        fun launch2() {
    GlobalScope.launch(Dispatchers.Main) {
    println("我在主线程执行")
    withContext(Dispatchers.IO) {
    println("我在子线程执行")//②
    }
    println("我在哪个线程执行?")//③
    }
    }

    大家先猜猜③ 的答案是什么?是主线程还是子线程?


    withContext(xx)函数上篇(讲真,Kotlin 协程的挂起没那么神秘(原理篇))已经深入分析过了,它是挂起函数,主要作用:



    切换线程执行协程。




    image.png


    MyAnnoy1 对应协程体1,为父协程体。

    MyAnnoy2 对应协程体2,为子协程体。

    当② 执行完成后,会切换到父协程执行,我们看看切换父协程的流程。

    每个协程的执行都要经历下面这个函数:


    #BaseContinuationImpl.kt
    override fun resumeWith(result: Result<Any?>) {
    //...
    while (true) {
    //..
    with(current) {
    val completion = completion!! // fail fast when trying to resume continuation without completion
    val outcome: Result<Any?> =
    try {
    //执行协程体
    val outcome = invokeSuspend(param)
    if (outcome === kotlin.coroutines.intrinsics.COROUTINE_SUSPENDED) return
    kotlin.Result.success(outcome)
    } catch (exception: Throwable) {
    kotlin.Result.failure(exception)
    }
    releaseIntercepted() // this state machine instance is terminating
    if (completion is BaseContinuationImpl) {
    //...
    } else {
    //如果上一步的协程体不阻塞,则执行completion
    completion.resumeWith(outcome)
    return
    }
    }
    }
    }

    此处以withContext(xx)函数协程体执行为例,它的completion 为何物?

    上面提到过launch()开启协程时,它的协程体的completion 为StandaloneCoroutine,也就是说MyAnnoy1.completion = StandaloneCoroutine。

    从withContext(xx)源码里得知,它的completion 为DispatchedCoroutine,DispatchedCoroutine,它继承自ScopeCoroutine,ScopeCoroutine 有个成员变量为:uCont: Continuation。

    当构造DispatchedCoroutine 时,传入的协程体赋值给uCont。
    也就是DispatchedCoroutine.uCont = MyAnnoy1,MyAnnoy2.completion = DispatchedCoroutine。



    此时,子协程体与父协程 通过DispatchedCoroutine 关联起来了。



    因此completion.resumeWith(outcome)==DispatchedCoroutine.resumeWith(outcome)。
    直接查看 后者实现即可:


    #AbstractCoroutine.kt
    public final override fun resumeWith(result: Result<T>) {
    val state = makeCompletingOnce(result.toState())
    if (state === COMPLETING_WAITING_CHILDREN) return
    afterResume(state)
    }

    #Builders.common.kt
    #DispatchedCoroutine 类里
    override fun afterResume(state: Any?) {
    //uCont 为父协程体
    uCont.intercepted().resumeCancellableWith(recoverResult(state, uCont))
    }

    到此就豁然开朗了,uCont.intercepted() 找到它的拦截器,因为uCont为MyAnnoy1,它的拦截器就是HandlerContext,又来了一次抛回到主线程执行。


    因此,上面Demo里③ 的答案是:



    它在主线程执行。



    小结来看,就两步:




    1. 父协程在主线程执行,中途遇到挂起的方法切换到子线程(子协程)执行。

    2. 当子协程执行完毕后,找到父协程的协程体,继续让其按照原有规则分发。



    老规矩,有代码有图有真相:



    image.png


    至此,切换到主线程执行的原理已经分析完毕。


    好奇的小伙伴可能会问:你这举例都是子线程往主线程切换,若是子线程往子线程切换呢?

    往主线程切换依靠Handler,而子线程切换依赖线程池,这块内容较多,单独拎出来分析。

    既然都提到这个点了,那这里再提一个问题:


        fun launch3() {
    GlobalScope.launch(Dispatchers.IO) {
    withContext(Dispatchers.Default) {
    println("我在哪个线程运行")
    delay(2000)
    println("delay 后我在哪个线程运行")
    }
    println("我又在哪个线程运行")
    }
    }

    你知道上面的答案吗?


    我们下篇将重点分析协程线程池的调度原理,通过它你将会知道上面的答案。


    本文基于Kotlin 1.5.3,文中完整Demo请点击


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

    掘金x得物公开课 - Flutter 3.0下的混合开发演进

    hello 大家好,我是《Flutter 开发实战详解》的作者,Github GSY 项目的负责人郭树煜,同时也是今年新晋的 Flutter GDE,借着本次 Google I/O 之后发布的 Flutter 3.0,来和大家聊一聊 Flutter 里混合开...
    继续阅读 »

    hello 大家好,我是《Flutter 开发实战详解》的作者,Github GSY 项目的负责人郭树煜,同时也是今年新晋的 Flutter GDE,借着本次 Google I/O 之后发布的 Flutter 3.0,来和大家聊一聊 Flutter 里混合开发的技术演进。


    为什么混合开发在 Flutter 里是特殊的存在?因为它渲染的控件是通过 Skia 直接和 GPU 交互,也就是说 Flutter 控件和平台无关,甚至连 UI 绘制线程都和原生平台 UI 线程是相互独立,所以甚至于 Flutter 在诞生之初都不支持和原生平台的控件进行混合开发,也就是不支持 WebView ,这就成了当时最大的缺陷之一


    其实从渲染的角度看 Flutter 更像是一个 2D 游戏引擎,事实上 Flutter 在这次 Google I/O 也分享了基于 Flutter 的游戏开发 ToolKit 和第三方工具包 Flame ,如图所示就是本次 Google I/O 发布的 Pinball 小游戏,所以从这些角度上看都可以看出 Flutter 在混合开发的特殊性。



    如果说的更形象简单一点,那就是如何把原生控件渲染到 WebView



    TT


    最初的社区支持


    不支持 WebView 在最初可以说是 Flutter 最大的痛点之一,所以在这样窘迫的情况下,社区里涌现出一些临时的解决方法,比如 flutter_webview_plugin


    类似 flutter_webview_plugin 的出现,解决了当时大部分时候 App 里打开一个网页的简单需求,如下图所示,它的思路就是:



    在 Flutter 层面放一个占位控件提供大小,然后原生层在同样的位置把 WebView 添加进去,从而达到看起来把 WebView 集成进去的效果,这个思路在后续也一直被沿用



    image-20220625170833702


    这样的实现方式无疑成本最低速度最快,但是也带来了很多的局限性


    相信大家也能想到,因为 Flutter 的所有控件都是渲染一个 FlutterView 上,也就是从原生的角度其实是一个单页面的效果,所以这种脱离 Flutter 渲染树的添加控件的方法,无疑是没办法和 Flutter 融合到一起,举个例子:



    • 如图一所示,从 Flutter 页面跳到 Native 页面的时候,打开动画无法同步,因为 AppBar 是 Flutter 的,而 Native 是原生层,它们不在同一个渲染树内,所以无法实现同步的动画效果

    • 如图二所示,比如在打开 Native 页面之后,通过 Appbar 再打开一个黄色的 Bottm Sheet ,可以看到此时黄色的 Bottm Sheet 打开了,但是却被 Native 遮挡住(Demo 里给 Native 设置了透明色),因为 Flutter 的 Bottm Sheet 是被渲染在 FlutterView 里面,而 Native UI 把 FlutterView 挡住了,所以新的 Flutter UI 自然也被遮挡

    • 如图三所示,当我们通过 reload 重刷 Flutter UI 之后,可以看到 Flutter 得 UI 都被重置了,但是此时 Native UI 还在,因为此时已经没有返回按键之类的无法关闭,这也是这种集成方式一不小心就影响开发的问题

    • 如图四通过 iOS 上的 debug 图层,我们可以更形象地看到这种方式的实现逻辑和堆叠效果



















    动画不同步页面被挡reload 之后iOS
    11111111222222222333333image-20220616142126589

    PlatformView


    随着 Flutter 的发展,官方支持混合开发势在必行,所以第一代 PlatformView 的支持还是诞生了,但是由于 Android 和 iOS 平台特性的不同,最初Android 的 AndroidView 和 iOS 的 UIKitView 实现逻辑相差甚远,以至于后面 Flutter 的 PlatformView 的每次大调整都是围绕于 Android 在做优化


    Android


    最初 Flutter 在 Android 上对 PlatformView 的支持是通过 VirtualDisplay 实现,VirtualDisplay 类似于一个虚拟显示区域,需要结合 DisplayManager 一起调用,VirtualDisplay 一般在副屏显示或者录屏场景下会用到,而在 Flutter 里 VirtualDisplay 会将虚拟显示区域的内容渲染在一个内存 Surface上。


    在 Flutter 中通过将 AndroidView 需要渲染的内容绘制到 VirtualDisplays 中 ,然后通过 textureId 在 VirtualDisplay 对应的内存中提取绘制的纹理, 简单看实现逻辑如下图所示:


    image-20220626151538054



    这里其实也是类似于最初社区支持的模式:通过在 Dart 层提供一个 AndroidView ,从而获取到控件所需的大小,位置等参数,当然这里多了一个 textureId ,这个 id 主要是提交给 Flutter Engine ,通过 id Flutter 就可以在渲染时将画面从内存里提出出来。



    iOS


    在 iOS 平台上就不使用类似 VirtualDisplay 的方法,而是通过将 Flutter UI 分为两个透明纹理来完成组合,这种方式无疑更符合 Flutter 社区的理念,这样的好处是:



    需要在 PlatformView 下方呈现的 Flutter UI 可以被绘制到其下方的纹理;而需要在 PlatformView 上方呈现的 Flutter UI 可以被绘制到其上方的纹理, 它们只需要在最后组合起来就可以了。



    是不是有点抽象?


    简单看下面这张图,其实就是通过在 NativeView 的不同层级设置不同的透明图层,然后把不同位置的控件渲染到不同图层,最终达到组合起来的效果。


    image-20220626151526444


    那明明这种方法更好,为什么 Android 不一开始也这样实现呢?


    因为当时在实现思路上, VirtualDisplay 的实现模式并不支持这种模式,因为在 iOS 上框架渲染后系统会有回调通知,例如:当 iOS 视图向下移动 2px 时,我们也可以将其列表中的所有其他 Flutter 控件也向下渲染 2px


    但是在 Android 上就没有任何有关的系统 API,因此无法实现同步输出的渲染。如果强行以这种方式在 Android 上使用,最终将产生很多如 AndroidView 与 Flutter UI 不同步的问题


    问题


    事实上 VirtualDisplay 的实现方式也带来和很多问题,简单说两个大家最直观的体会:


    触摸事件


    因为控件是被渲染在内存里,虽然你在 UI 上看到它就在那里,但是事实上它并不在那里,你点击到的是 FlutterView ,所以用户产生的触摸事件是直接发送到 FlutterView


    所以触摸事件需要在 FlutterView 到 Dart ,再从 Dart 转发到原生,然后如果原生不处理又要转发回 Flutter ,如果中间还存在其他派生视图,事件就很容易出现丢失和无法响应,而这个过程对于 FlutterView 来说,在原生层它只有一个 View 。


    所以 Android 的 MotionEvent 在转化到 Flutter 过程中可能会因为机制的不同,存在某些信息没办法完整转化的丢失。


    文字输入


    一般情况下 AndroidView 是无法获取到文本输入,因为 VirtualDisplay 所在的内存位置会始终被认为是 unfocused 的状态



    InputConnectionsunfocused 的 View 中通常是会被丢弃。



    所以 Flutter 重写了 checkInputConnectionProxy 方法,这样 Android 会认为 FlutterView 是作为 AndroidView 和输入法编辑器(IME)的代理,这样 Android 就可以从 FlutterView 中获取到 InputConnections 然后作用于 AndroidView 上面。



    在 Android Q 开始又因为非全局的 InputMethodManager 需要新的兼容



    当然还有诸如性能等其他问题,但是至少先有了支持,有了开始才会有后续的进阶,在 Flutter 3.0 之前, VirtualDisplay 一直默默在 PlatformView 的背后耕耘。


    HybridComposition


    时间来到 Flutter 1.2,Hybrid Composition 是在 Flutter 1.2 时发布的 Android 混合开发实现,它使用了类似 iOS 的实现思路,提供了 Flutter 在 Android 上的另外一种 PlatformView 的实现。


    如下图是在 Dart 层使用 VirtualDisplay 切换到 HybridComposition 模式的区别,最直观的感受应该是需要写的 Dart 代码变多了。


    111111


    但是其实 HybridComposition 的实现逻辑是变简单了: PlatformView 是通过 FlutterMutatorView 把原生控件 addViewFlutterView 上,然后再通过 FlutterImageView 的能力去实现图层的混合



    又懵了?不怕,马上你就懂了



    简单来说就是 HybridComposition 模式会直接把原生控件通过 addView 添加到 FlutterView 上 。这时候大家可能会说,咦~这不是和最初的实现一样吗?怎么逻辑又回去了



    其实确实是社区的进阶版实现,Flutter 直接通过原生的 addView 方法将 PlatformView 添加到 FlutterView 里,而当你还需要在 PlatformView 上渲染 Flutter 自己的 Widget 时,Flutter 就会通过再叠加一个 FlutterImageView 来承载这个 Widget 的纹理。



    举一个简单的例子,如下图所示,一个原生的 TextView 被通过 HybridComposition 模式接入到 Flutter 里(NativeView),而在 Android 的显示布局边界和 Layout Inspector 上可以清晰看到: 灰色 TextView 通过 FlutterMutatorView 被添加到 FlutterView 上被直接显示出来


    image-20220618152055492


    所以在 HybridCompositionTextView 是直接在原生代码上被 add 到 FlutterView 上,而不是提取纹理


    那如果我们看一个复杂一点的案例,如下图所示,其中蓝色的文本是原生的 TextView ,红色的文本是 Flutter 的 Text 控件,在中间 Layout Inspector 的 3D 图层下可以清晰看到:



    • 两个蓝色的 TextView 是通过 FlutterMutatorView 被添加在 FlutterView 之上,并且把没有背景色的红色 RE 遮挡住了

    • 最顶部有背景色的红色 RE 也是 Flutter 控件,但是因为它需要渲染到 TextView 之上,所以这时候多一个 FlutterImageView ,它用于承载需要显示在 Native 控件之上的纹理,从而达 Flutter 控件“真正”和原生控件混合堆叠的效果。


    image-20220616165047353


    可以看到 Hybrid Composition 上这种实现,能更原汁原味地保流下原生控件的事件和特性,因为从原生角度看它就是原生层面的物理堆叠,需要都一个层级就多加一个 FlutterImageView ,同一个层级的 Flutter 控件共享一个 FlutterImageView


    当然,在 HybridCompositionFlutterImageView 也是一个很有故事的对象,由于篇幅原因这里就不详细展开,这里大家可以简单看这张图感受下,也就是在有 PlatformView 和没有 PlatformView 是,Flutter 的渲染会有一个转化的过程,而在这个变化过程,在 Flutter 3.0 之前可以通过 PlatformViewsService.synchronizeToNativeViewHierarchy(false); 取消


    image-20220618153757996


    最后,Hybrid Composition 也不少问题,比如上面的转化就是为了解决动画同步问题,当然这个行为也会产生一些性能开销,例如:



    在 Android 10 之前, Hybrid Composition 需要将内存中的每个 Flutter 绘制的帧数据复制到主内存,之后再从 GPU 渲染复制回来 ,所以也会导致 Hybrid Composition 在 Android 10 之前的性能表现更差,例如在滚动列表里每个 Item 嵌套一个 Hybrid CompositionPlatformView ,就可能会变卡顿甚至闪烁。



    其他还有线程同步,闪烁等问题,由于篇幅就不详细展开,如果感兴趣的可以详细看我之前发布过的 《Flutter 深入探索混合开发的技术演进》


    TextureLayer


    随着 Flutter 3.0 的发布,第一代 PlatformView 的实现 VirtualDisplay 被新的 TextureLayer 所替代,如下图所示,简单对比 VirtualDisplayTextureLayer 的实现差异,可以看到主要还是在于原生控件纹理的提取方式上


    image-20220618154327890


    从上图我们可以得知:



    • VirtualDisplayTextureLayerPlugin 的实现是可以无缝切换,因为主要修改的地方在于底层对于纹理的提取和渲染逻辑

    • 以前 Flutter 中会将 AndroidView 需要渲染的内容绘制到 VirtualDisplays ,然后在 VirtualDisplay 对应的内存中,绘制的画面就可以通过其 Surface 获取得到;现在 AndroidView 需要的内容,会通过 View 的 draw 方法被绘制到 SurfaceTexture 里,然后同样通过 TextureId 获取绘制在内存的纹理


    是不是又有点蒙?简单说就是不需要绘制到副屏里,现在直接通过 override Viewdraw 方法就可以了。


    TextureLayer 的实现里,同样是需要把控件添加到一个 PlatformViewWrapper 的原生布局控件里,但是这个控件通过 override 了 Viewdraw 方法,把原本的 Canvas 替换成 SurfaceTexture 在内存的 Canvas ,所以 PlatformViewWrapper 的 child 会把控件绘制到内存的 SurfaceTexture 上。



    举个例子,还是之前的代码,如下图所示,这时候通过 TextureLayer 模式运行之后,通过 Layout Inspector 的 3D 图层可以看到,两个原生的 TextView 通过 PlatformViewWrapper 被添加到 FlutterView 上。


    但是不同的是,在 3D 图层里看不到 TextView 的内容,因为绘制 TextView 的 Canvas 被替换了,所以 TextView 的内容被绘制到内存的 Surface 上,最终会在渲染时同步 Flutter Engine 里。



    看到这里,你可能也发现了,这时候因为有 PlatformViewWrapper 的存在,点击会被 PlatformViewWrapper 内部拦截,从而也解决了触摸的问题, 而这里刚好有人提了一个问题,如下图所示:



    "从图 1 Layout Inspector 看, PlatformWrapperView 是在 FlutterSurfaceView 上方,为什么如图 2 所示,点击 Flutter button 却可以不触发 native button的点击效果?"。
















    图1图2
    image.pngimg

    思考一下,因为最直观的感受:点击不都是被 PlatformViewWrapper 拦截了吗?明明 PlatformViewWrapper 是在 FlutterSurfaceView 之上,为什么 FlutterSurfaceView 里的 FlutterButton 还能被点击到


    这里简单解释一下:



    • 1、首先那个 Button 并不是真的被摆放在那里,而是通过 PlatformViewWrappersuper.draw绘制到 surface 上的,所以在那里的是 PlatformViewWrapper ,而不是 Button ,Button 的内容已经变成纹理去到了 FlutterSurfaceView 里面

    • 2、 PlatformViewWrapper 里重写了 onInterceptTouchEvent 做了拦截onInterceptTouchEvent 这个事件是从父控件开始往子控件传,因为拦截了所以不会让 Button 直接响应,然后在 PlatformViewWrapperonTouchEvent 响应里是做了点击区域的分发,响应会分发到了 AndroidTouchProcessor 之后,会打包发到 _unpackPointerDataPacket 进入 Dart

    • 3、 在 Dart 层的点击区域,如果没有 Flutter 控件响应,会是 _PlatformViewGestureRecognizer-> updateGestureRecognizers -> dispatchPointerEvent -> sendMotionEvent 又发送回原生层

    • 4、回到原生 PlatformViewsControllercreateForTextureLayer 里的 onTouch ,执行 view.dispatchTouchEvent(event);


    image-20220625171101069


    总结起来就是:**PlatfromViewWrapper 拦截了 Event ,通过 Dart 做二次分发响应,从而实现不同的事件响应 ** ,它和 VirtualDisplay 的不同是, VirtualDisplay 的事件响应都是在 FlutterView 上,但是TextureLayout 模式,是有独立的原生 PlatfromViewWrapper 控件来开始,所以区域效果和一致性会更好。


    问题


    最后这里还需要提个醒,如果你之前使用的插件使用的是 HybirdComposition ,但是没做兼容,也就是使用的还是 PlatformViewsService.initSurfaceAndroidView 的话,它也会切换成 TextureLayer 的逻辑,所以你需要切换为 PlatformViewsService.initExpensiveAndroidView ,才能继续使用原本 HybirdComposition 的效果



    ⚠️我也比较奇怪为什么 Flutter 3.0 没有提及 Android 这个 breaking change ,因为对于开发来说其实是无感的,不小心就掉坑里。



    那你说为什么还要 HybirdComposition


    前面我们说过, TextureLayer 是通过在 super.draw 替换 Canvas 的方法去实现绘制,但是它替换不了 Surface 里的一些 Canvas ,所以比如一些需要 SurfaceViewTextureView 或者有自己内部特殊 Canvas 的场景,你还是需要 HybirdComposition ,只不过可能会和官方新的 API 名字一样,它 Expensive 。


    Expensive 是因为在 Flutter 3.0 正式版开始,FlutterView 在使用 HybirdComposition 时一定会 converted to FlutterImageView ,这也是 Flutter 3.0 下一个需要注意的点。


    image-20220616170253242



    更多内容可见 《Flutter 3.0 之 PlatformView :告别 VirtualDisplay ,拥抱 TextureLayer》



    image-20220625164049356


    最后


    最后做个总结,可以看到 Flutter 为了混合开发做了很多的努力,特别是在 Android 上,也是因为历史埋坑的原因,由于时间关系这里没办法都详细介绍,但是相信本次之后大家对 Flutter 的 PlatformView 实现都有了全面的了解,这对大家在未来使用 Flutter 也会有很好的帮助,如果你还有什么问题,欢迎交流。


    image-20220626151444011


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

    少年,你可知 Kotlin 协程最初的样子?

    如果有人问你,怎么开启一个 Kotlin 协程?你可能会说通过runBlocking/launch/async,回答没错,这几个函数都能开启协程。不过这次咱们换个角度分析,通过提取这几个函数的共性,看看他们内部是怎么开启一个协程的。相信通过本篇,你将对协程原理...
    继续阅读 »

    如果有人问你,怎么开启一个 Kotlin 协程?你可能会说通过runBlocking/launch/async,回答没错,这几个函数都能开启协程。不过这次咱们换个角度分析,通过提取这几个函数的共性,看看他们内部是怎么开启一个协程的。
    相信通过本篇,你将对协程原理有个深刻的认识。
    文章目录:

    1、suspend 关键字背后的原理
    2、如何开启一个原始的协程?
    3、协程调用以及整体流程
    4、协程代码我为啥看不懂?

    1、suspend 关键字背后的原理

    suspend 修饰函数

    普通的函数

    fun launchEmpty(block: () -> Unit) {   
    }

    定义一个函数,形参为函数类型。
    查看反编译结果:

    public final class CoroutineRawKt {
    public static final void launchEmpty(@NotNull Function0 block) {
    }
    }

    可以看出,在JVM 平台函数类型参数最终是用匿名内部类表示的,而FunctionX(X=0~22) 是Kotlin 将函数类型映射为Java 的接口。
    来看看Function0 的定义:

    public interface Function0<out R> : Function<R> {
    /** Invokes the function. */
    public operator fun invoke(): R
    }

    有一个唯一的方法:invoke(),它没有任何参数。
    可作如下调用:

    fun launchEmpty(block: () -> Unit) {
    block()//与block.invoke()等价
    }
    fun main(array: Array<String>) {
    launchEmpty {
    println("I am empty")
    }
    }

    带suspend 的函数

    以上写法大家都比较熟悉了,就是典型的高阶函数的定义和调用。
    现在来改造一下函数类型的修饰符:

    fun launchEmpty1(block: suspend () -> Unit) {
    }

    相较之前,加了"suspend"关键字。
    老规矩,查看反编译结果:

    public static final void launchEmpty1(@NotNull Function1 block) {
    }

    参数从Function0 变为了Function1:

    /** A function that takes 1 argument. */
    public interface Function1<in P1, out R> : Function<R> {
    /** Invokes the function with the specified argument. */
    public operator fun invoke(p1: P1): R
    }

    Function1 的invoke()函数多了一个入参。

    也就是说,加了suspend 修饰后,函数会默认加个形参。

    当我们调用suspend修饰的函数时:

    image.png

    意思是:

    "suspend"修饰的函数只能在协程里被调用或者是在另一个被"suspend"修饰的函数里调用。

    suspend 作用

    何为挂起

    suspend 意为挂起、阻塞的意思,与协程相关。
    当suspend 修饰函数时,表明这个函数可能会被挂起,至于是否被挂起取决于该函数里是否有挂起动作。 比如:

    suspend fun testSuspend() {
    println("test suspend")
    }

    这样的写法没意义,因为函数没有实现挂起功能。
    你可能会说,挂起需要切换线程,好嘛,换个写法:

    suspend fun testSuspend() {
    println("test suspend")
    thread {
    println("test suspend in thread")
    }
    }

    然而并没啥用,编译器依然提示:

    image.png

    意思是可以不用suspend 修饰,没啥意义。

    挂起于协程的意义

    第一点
    当函数被suspend 修饰时,表明协程执行到此可能会被挂起,若是被挂起那么意味着协程将无法再继续往下执行,直到条件满足恢复了协程的运行。

    fun main(array: Array<String>) {
    GlobalScope.launch {
    println("before suspend")//①
    testSuspend()//挂起函数②
    println("after suspend")//③
    }
    }

    执行到②时,协程被挂起,将不会执行③,直到协程被恢复后才会执行③。
    注:关于协程挂起的生动理解&线程的挂起 下篇将着重分析。

    第二点
    如果将suspend 修饰的函数类型看做一个整体的话:

    suspend () -> T

    无参,返回值为泛型。
    Kotlin 里定义了一些扩展函数,可用来开启协程。

    第三点 suspend 修饰的函数类型,当调用者实现其函数体时,传入的实参将会继承自SuspendLambda(这块下个小结详细分析)。

    2、如何开启一个原始的协程?

    ##launch/async/runBlocking 如何开启协程
    纵观这几种主流的开启协程方式,它们最终都会调用到:

    #CoroutineStart.kt
    public operator fun <R, T> invoke(block: suspend R.() -> T, receiver: R, completion: Continuation<T>): Unit =
    when (this) {
    DEFAULT -> block.startCoroutineCancellable(receiver, completion)
    ATOMIC -> block.startCoroutine(receiver, completion)
    UNDISPATCHED -> block.startCoroutineUndispatched(receiver, completion)
    LAZY -> Unit // will start lazily
    }

    无论走哪个分支,都是调用block的函数,而block 就是我们之前说的被suspend 修饰的函数。
    以DEFAULT 为例startCoroutineUndispatched接下来会调用到IntrinsicsJvm.kt里的:

    #IntrinsicsJvm.kt
    public actual fun <R, T> (suspend R.() -> T).createCoroutineUnintercepted(
    receiver: R,
    completion: Continuation<T>
    )

    该函数带了俩参数,其中的receiver 为接收者,而completion 为协程结束后调用的回调。
    为了简单,我们可以省略掉receiver。
    刚好IntrinsicsJvm.kt 里还有另一个函数:

    #IntrinsicsJvm.kt
    public actual fun <T> (suspend () -> T).createCoroutineUnintercepted(
    completion: Continuation<T>
    ): Continuation<Unit>

    createCoroutineUnintercepted 为 (suspend () -> T) 类型的扩展函数,因此只要我们的变量为 (suspend () -> T)类型就可以调用createCoroutineUnintercepted(xx)函数。
    查找该函数的使用之处,发现Continuation.kt 文件里不少扩展函数都调用了它。
    如:

    #Continuation.kt
    //创建协程的函数
    public fun <T> (suspend () -> T).createCoroutine(
    completion: Continuation<T>
    ): Continuation<Unit> =
    SafeContinuation(createCoroutineUnintercepted(completion).intercepted(), COROUTINE_SUSPENDED)

    其中Continuation 为接口:

    #Continuation.kt
    interface Continuation<in T> {
    //协程上下文
    public val context: CoroutineContext
    //恢复协程
    public fun resumeWith(result: Result<T>)
    }

    Continuation 接口很重要,协程里大部分的类都实现了该接口,通常直译过来为:"续体"。

    创建完成后,还需要开启协程函数:

    #Continuation.kt
    //启动协程的函数
    public inline fun <T> Continuation<T>.resume(value: T): Unit =
    resumeWith(Result.success(value))

    简单创建/调用协程

    协程创建

    由上分析可知,Continuation.kt 里有我们开启协程所需要的一些基本信息,接着来看看如何调用上述函数。

    fun <T> launchFish(block: suspend () -> T) {
    //创建协程,返回值为SafeContinuation(实现了Continuation 接口)
    //入参为Continuation 类型,参数名为completion,顾名思义就是
    //协程结束后(正常返回&抛出异常)将会调用它。
    var coroutine = block.createCoroutine(object : Continuation<T> {
    override val context: CoroutineContext
    get() = EmptyCoroutineContext

    //协程结束后调用该函数
    override fun resumeWith(result: Result<T>) {
    println("result:$result")
    }
    })
    //开启协程
    coroutine.resume(Unit)
    }

    定义了函数launchFish,该函数唯一的参数为函数类型参数,被suspend 修饰,而(suspend () -> T)定义一系列扩展函数,createCoroutine 为其中之一,因此block 可以调用createCoroutine。
    createCoroutine 返回类型为SafeContinuation,通过SafeContinuation.resume()开启协程。

    协程调用

    fun main(array: Array<String>) {
    launchFish {
    println("I am coroutine")
    }
    }

    打印结果:

    image.png

    3、协程调用以及整体流程

    协程调用背后的玄机

    反编译初窥门径

    看到上面的打印大家可能比较晕,"println("I am coroutine")"是咋就被调用的?没看到有调用它的地方啊。
    launchFish(block) 接收的是函数类型,当调用launchFish 时,在闭包里实现该函数的函数体即可,我们知道函数类型最终会替换为匿名内部类。
    因为kotlin 有不少语法糖,无法一下子直击本质,老规矩,反编译看看结果:

        public static final void main(@NotNull String[] array) {
    launchFish((Function1)(new Function1((Continuation)null) {
    int label;

    @Nullable
    public final Object invokeSuspend(@NotNull Object var1) {
    Object var4 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
    switch(this.label) {
    case 0:
    //闭包里的内容
    String var2 = "I am coroutine";
    boolean var3 = false;
    //打印
    System.out.println(var2);
    return Unit.INSTANCE;
    }
    }

    @NotNull
    public final Continuation create(@NotNull Continuation completion) {
    //创建一个Continuation,可以认为是续体
    Function1 var2 = new <anonymous constructor>(completion);
    return var2;
    }

    public final Object invoke(Object var1) {
    //Function1 接口里的方法
    return ((<undefinedtype>)this.create((Continuation)var1)).invokeSuspend(Unit.INSTANCE);
    }
    }));
    }

    为了更直观,删除了一些不必要的信息。
    看到这,你发现了什么?通常传入函数类型的实参最后将会被编译为对应的匿名内部类,此时应该编译为Function1, 实现其唯一的函数:invoke(xx),而我们发现实际上还多了两个函数:invokeSuspend(xx)与create(xx)
    我们有理由相信,invokeSuspend(xx)函数一定在某个地方被调用了,原因是:闭包里打印的字符串:"I am coroutine" 只在该函数里实现,而我们测试的结果是这个打印执行了。
    还记得我们上面说的suspend 意义的第三点吗?

    suspend 修饰的函数类型,其实参是匿名内部类,继承自抽象类:SuspendLambda。

    也就是说invokeSuspend(xx)与create(xx) 的定义很有可能来自SuspendLambda,我们接着来分析它。

    SuspendLambda 关系链

    #ContinuationImpl.kt
    internal abstract class SuspendLambda(
    public override val arity: Int,
    completion: Continuation<Any?>?
    ) : ContinuationImpl(completion), FunctionBase<Any?>, SuspendFunction {
    constructor(arity: Int) : this(arity, null)
    ...
    }

    该类本身并没有太多内容,此处继承了ContinuationImpl类,查看该类也没啥特殊的,继续往上查找,找到BaseContinuationImpl类,在里面发现了线索:

    #ContinuationImpl.kt
    internal abstract class BaseContinuationImpl(
    val completion: Continuation<Any?>?
    ) : Continuation<Any?>, CoroutineStackFrame, Serializable {
    protected abstract fun invokeSuspend(result: Result<Any?>): Any?
    open fun create(completion: Continuation<*>): Continuation<Unit> {
    }
    }

    终于看到了眼熟的:invokeSuspend(xx)与create(xx)。
    我们再回过头来捋一下类之间关系:

    image.png

    闭包生成的匿名内部类:

    • 实现了Function1 接口,并实现了该接口里的invoke函数。
    • 继承了SuspendLambda,并重写了invokeSuspend函数和create函数。

    你可能会说还不够直观,那好,继续改写一下:

        class MyAnonymous extends SuspendLambda implements Function1 {
    int label;
    public final Object invokeSuspend(@NotNull Object var1) {
    Object var4 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
    switch(this.label) {
    case 0:
    String var2 = "I am coroutine";
    boolean var3 = false;
    System.out.println(var2);
    return Unit.INSTANCE;
    }
    }
    public final Continuation create(@NotNull Continuation completion) {
    Intrinsics.checkNotNullParameter(completion, "completion");
    Function1 var2 = new <anonymous constructor>(completion);
    return var2;
    }
    public final Object invoke(Object var1) {
    return ((<undefinedtype>)this.create((Continuation)var1)).invokeSuspend(Unit.INSTANCE);
    }
    }

    public static final void launchFish(@NotNull MyAnonymous block) {
    Continuation coroutine = ContinuationKt.createCoroutine(block, (new Continuation() {
    @NotNull
    public CoroutineContext getContext() {
    return (CoroutineContext) EmptyCoroutineContext.INSTANCE;
    }

    public void resumeWith(@NotNull Object result) {
    String var2 = "result:" + Result.toString-impl(result);
    boolean var3 = false;
    System.out.println(var2);
    }
    }));
    //开启
    coroutine.resumeWith(Result.constructor-impl(var3));
    }

    public static final void main(@NotNull String[] array) {
    MyAnonymous myAnonymous = new MyAnonymous();
    launchFish(myAnonymous);
    }

    这么看就比较清晰了,此处我们单独声明了一个MyAnonymous类,并构造对象传递给launchFish函数。

    闭包的执行

    既然匿名类的构造清晰了,接下来分析闭包是如何被执行的,也就是查找invokeSuspend(xx)函数是怎么被调用的?
    将目光转移到launchFish 函数本身。

    createCoroutine()
    先看createCoroutine()函数调用,直接上代码:

    #Continuation.kt
    fun <T> (suspend () -> T).createCoroutine(
    completion: Continuation<T>
    ): Continuation<Unit> =
    //返回SafeContinuation 对象
    //SafeContinuation 构造函数需要2个参数,一个是delegate,另一个是协程状态
    //此处默认是挂起
    SafeContinuation(createCoroutineUnintercepted(completion).intercepted(), COROUTINE_SUSPENDED)

    #IntrinsicsJvm.kt
    actual fun <T> (suspend () -> T).createCoroutineUnintercepted(
    completion: Continuation<T>
    ): Continuation<Unit> {
    val probeCompletion = probeCoroutineCreated(completion)
    return if (this is BaseContinuationImpl)
    //此处的this 即为匿名内部类对象 MyAnonymous,它间接继承了BaseContinuationImpl
    //调用MyAnonymous 重写的create 函数
    //create 函数里new 新的MyAnonymous 对象
    create(probeCompletion)
    else
    createCoroutineFromSuspendFunction(probeCompletion) {
    (this as Function1<Continuation<T>, Any?>).invoke(it)
    }
    }

    #IntrinsicsJvm.kt
    public actual fun <T> Continuation<T>.intercepted(): Continuation<T> =
    //判断是否是ContinuationImpl 类型的Continuation
    //我们的demo里是true,因此会继续尝试调用拦截器
    (this as? ContinuationImpl)?.intercepted() ?: this

    #ContinuationImpl.kt
    public fun intercepted(): Continuation<Any?> =
    //查看是否已经有拦截器,如果没有,则从上下文里找,上下文没有,则用自身,最后赋值。
    //在我们的demo里上下文里没有,用的是自身
    intercepted
    ?: (context[ContinuationInterceptor]?.interceptContinuation(this) ?: this)
    .also { intercepted = it }

    最后得出的Continuation 赋值给SafeContinuation 的成员变量:delegate。
    至此,SafeContinuation 对象已经构造完毕,接着继续看如何用它开启协程。

    再看 resume()

    #SafeContinuationJvm.kt
    actual override fun resumeWith(result: Result<T>) {
    while (true) { // lock-free loop
    val cur = this.result // atomic read
    when {
    //初始化状态为UNDECIDED,因此直接return
    cur === CoroutineSingletons.UNDECIDED -> if (SafeContinuation.RESULT.compareAndSet(this,
    CoroutineSingletons.UNDECIDED, result.value)) return
    //如果是挂起,将它变为恢复状态,并调用恢复函数
    //demo 里初始化状态为COROUTINE_SUSPENDED,因此会走到这
    cur === COROUTINE_SUSPENDED -> if (SafeContinuation.RESULT.compareAndSet(this, COROUTINE_SUSPENDED,
    CoroutineSingletons.RESUMED)) {
    //delegate 为之前创建的Continuation,demo 里因为没有拦截,因此为MyAnonymous
    delegate.resumeWith(result)
    return
    }
    else -> throw IllegalStateException("Already resumed")
    }
    }
    }

    #ContinuationImpl.kotlin
    #BaseContinuationImpl类的成员函数
    override fun resumeWith(result: Result<Any?>) {
    var current = this
    var param = result
    while (true) {
    probeCoroutineResumed(current)
    with(current) {
    val completion = completion!!
    val outcome: Result<Any?> =
    try {
    //invokeSuspend 即为MyAnonymous 里的方法
    val outcome = invokeSuspend(param)
    //如果返回值是挂起状态,则函数直接退出
    if (outcome === kotlin.coroutines.intrinsics.COROUTINE_SUSPENDED) return
    kotlin.Result.success(outcome)
    } catch (exception: Throwable) {
    kotlin.Result.failure(exception)
    }
    releaseIntercepted() // this state machine instance is terminating
    if (completion is BaseContinuationImpl) {
    current = completion
    param = outcome
    } else {
    //执行到这,最终执行外层的completion,在demo里会输出"result:$result"
    completion.resumeWith(outcome)
    return
    }
    }
    }
    }

    最后再回头看 invokeSuspend

             public final Object invokeSuspend(@NotNull Object var1) {
    Object var4 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
    switch(this.label) {
    case 0:
    ResultKt.throwOnFailure(var1);
    String var2 = "I am coroutine";
    boolean var3 = false;
    System.out.println(var2);
    return Unit.INSTANCE;
    default:
    throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
    }
    }

    你兴许已经发现了,此处的返回值永远是Unit.INSTANCE啊,那么协程永远不会挂起。
    没有挂起功能的协程就是鸡肋...
    没错,咱们的demo里实现的是一个无法挂起的协程,回到最初的launchFish()的调用:

        launchFish {
    println("I am coroutine")
    }
    }

    因为闭包里只有一个打印语句,根本没有挂起函数,当然就没有挂起的说法了。

    协程调用整体流程

    上面花很多篇幅去分析协程的调用,其实就是为了从kotlin 的简洁里脱离出来,从而真正了解其背后的原理。
    Demo里的协程构造比较原始,相较于launch/async 等启动方式,它没有上下文、没有线程调度,但并不妨碍我们通过它去了解协程的运作。当我们了解了其运作的核心,到时候再去看launch/async/runBlocking 就非常容易了,毕竟它们都是提供给开发者更方便操作协程的工具,是在原始携程的基础上演变的。
    协程创建调用栈简易图:

    image.png

    4、协程代码我为啥看不懂?

    之前有一些小伙伴跟我反馈说:"小鱼人,我尝试去看协程源码,感觉找不到入口,又或是跟着源码跟到一半就断了... 你是咋阅读的啊?"
    有一说一,协程源码确实不太好懂,若要比较顺畅读懂源码,根据个人经验可能需要以下前置条件:

    1、kotlin 语法基础,这是必须的。
    2、高阶函数&扩展函数。
    3、平台代码差异,有一些类、函数是与平台相关,需要定位到具体平台,比如SafeContinuation,找到Java 平台的文件:SafeContinuationJvm.kt。
    4、断点调试时,有些单步断点不会进入,需要指定运行到的位置。
    5、有些代码是编译时期构造的,需要对照反编译结果查看。
    6、还有些代码是没有源码的,可能是ASM插入的,此时只能靠肉眼理解了。

    如果你对kotlin 基础/高阶函数 等有疑惑,请查看之前的文章。

    本篇仅仅构造了一个简陋的协程,协程的最重要的挂起/恢复并没有涉及,下篇将会着重分析如何构造一个挂起函数,以及协程到底是怎么挂起的。

    本文基于Kotlin 1.5.3,文中完整Demo请点击


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

    收起阅读 »

    一个小故事讲明白进程、线程、Kotlin 协程到底啥关系?

    相信稍微接触过Kotlin的同学都知道Kotlin Coroutine(协程)的大名,甚至有些同学认为重要到"无协程,不Kotlin"的地步,吓得我赶紧去翻阅了协程源码,同时也学习了不少博客,博客里比较典型的几个说法: 协程是轻量级线程、比线程耗费资源少 ...
    继续阅读 »

    相信稍微接触过Kotlin的同学都知道Kotlin Coroutine(协程)的大名,甚至有些同学认为重要到"无协程,不Kotlin"的地步,吓得我赶紧去翻阅了协程源码,同时也学习了不少博客,博客里比较典型的几个说法:




    • 协程是轻量级线程、比线程耗费资源少

    • 协程是线程框架

    • 协程效率高于线程

    • ...



    一堆术语听起来是不是很高端的样子?这些表述正确吗?妥当吗?你说我学了大半天,虽然我也会用,但还是没弄懂啥是协程...

    为了彻底弄懂啥是协程,需要将进程、线程拉进来一起pk。

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



    1、程序、进程、CPU、内存关系

    2、进程与线程的故事

    3、线程与Kotlin协程的故事

    4、Kotlin 协程的使命



    1、程序、进程、CPU、内存关系



    image.png


    如上图,平时我们打包好一个应用,放在磁盘上,此时我们称之为程序或者应用,是静态的。也就是咱们平常说的:我下载个程序,你传给apk给我,它们都是程序(应用)。

    当我们执行程序(比如点击某个App),OS 会将它加载进内存,CPU 从内存某个起始地址开始读取指令并执行程序。

    程序从磁盘上加载到内存并被CPU运行期间,称之为进程。因此我们通常说某个应用是否还在存活,实际上说的是进程是否还在内存里;也会说某某程序CPU占用率太高,实际上说的是进程的CPU占用率。

    而操作系统负责管理磁盘、内存、CPU等交互,可以说是大管家。


    2、进程与线程的故事


    接下来我们以一个故事说起。


    上古时代的合作



    image.png


    在上古时候,一个设备里只有一个CPU,能力比较弱,单位时间内能够处理的任务量有限,内存比较小,能加载的应用不多,相应的那会儿编写的程序功能单一,结构简单。


    OS 说:"大家都知道,我们的情况比较具体,只有一个CPU,内存也很小,而现在有不少应用想要占用CPU和内存,无规矩不成方圆,我现在定下个规矩:"



    每个应用加载到内存后,我将给他安排内存里的一块独立的空间,并记录它的一些必要信息,最后规整为一个叫进程的东西,就是代表你这个应用的所有信息,以后我就只管调度进程即可。

    并且进程之间的内存空间是隔离的,无法轻易访问,特殊情况需要经过我的允许。



    应用(程序)说:"哦,我知道了,意思就是:进程是资源分派的基本单位嘛"

    OS 说:"对的,悟性真好,小伙子。"


    规矩定下了,大家就开始干活了:



    1、应用被加载到内存后,OS分派了一些资源给它。

    2、CPU 从内存里逐一取出并执行进程。

    3、其它没有得到CPU青睐的进程则静候等待,等待被翻牌。



    中古时代的合作


    一切都有条不紊的进行着,大家配合默契,其乐融融,直到有一天,OS 发现了一些端倪。

    他发现CPU 在偷懒...找到CPU,压抑心中的愤怒说到:

    "我发现你最近不是很忙哎,是不是工作量不饱和?"

    CPU 忙不迭说到:"冤枉啊,我确实不是很忙,但这不怪我啊。你也知道我最近升级了频率,处理速度快了很多,进程每次给我的任务我都快速执行完了,不过它们却一直占用我,不让我处理其它进程的,我也是没办法啊。"

    OS 大吃一惊到:"大胆进程,居然占着茅坑不拉屎!"

    CPU 小声到:"我又不是茅坑..."


    OS 找来进程劈头盖脸训斥一道:"进程你好大的胆,我之前不是给你说请CPU 做事情要讲究一个原则:按需占有,用完就退。你把我话当耳边风了?"

    进程直呼:"此事与我无关啊,你知道的我最讲原则了,你之前说过对CPU 的使用:应占尽占。我现在不仅要处理本地逻辑,还要从磁盘读取文件,这个时候我虽然不占用CPU,但是我后面文件读结束还是需要他。"


    OS 眉头紧皱,略微思索了一下对进程和CPU道:"此事前因后果均已知悉,容我斟酌几日。"

    几天后,OS 过来对他俩说:"我现在重新拟定一个规则:进程不能一直占用CPU到任务结束为止,需要规定占用的时间片,在规定的时间片内进程能完成多少是多少,时间一到立即退出CPU换另一个进程上,没能完成任务的进程等下个轮到自己的时间片再上"

    进程和CPU 对视一眼,立即附和:"谨遵钧令,使命必达!"


    近现代的合作



    自从实行新规定以来,进程们都有机会抢占CPU了,算是雨露均沾,很少出现某进程长期霸占CPU的现象了,OS 对此很是满意。



    一则来自进程的举报打破这黎明前的宁静。

    OS 收到一则举报:"我进程实名举报CPU 偷懒。"

    OS 心里咯噔一跳,寻思着咋又是CPU,于是叫来CPU 对簿公堂。

    CPU 听到OS 召唤,暗叫不妙,心里立马准备了一套说辞。

    OS 对着CPU 和 进程说:"进程说你偷懒,你在服务进程的时间片内无所事事,我希望你能给我一个满意的答复。"

    CPU 一听这话,心里一阵鄙视,果不出我所料,就知道你问这事。虽然心里诽腹不已,脸上却是郑重其事道:"这事是因为进程交给我的任务很快完成了,它去忙别的事了,让我等等他。"

    OS 诧异道:"你这么快就将进程的任务处理完成了?"

    CPU 面露得以之色道:"你知道的我一直追求进步,这不前阵子又升级了一下嘛,处理能力又提升了。如果说优秀是一种原罪的话,那这个罪名由我承担吧,再如果..."

    OS 看了进程一眼,对CPU 说:"行行行,打住,此事确实与你无关。进程虽然你误会了CPU,但是你提出的问题确实是一个好的思考点,这个下来我想个方案,回头咱们评审一下。"


    一个月后,OS 将进程和CPU召集起来,并拿出方案说:"我们这次将进行一次大的调整,鉴于CPU 处理能力提升,他想要承担更多的工作,而目前以进程为单位提交任务颗粒度太大了,需要再细化。我建议将进程划分为若干线程,这些线程共享进程的资源池,进程想要执行某任务直接交给线程即可,而CPU每次以线程为单位执行。接下来,你们说说各自的意见吧。"

    进程说到:"这个方案很优秀,相当于我可以弄出分身,让各个分身干各项任务,处理UI一个线程,处理I/O是另一个线程,处理其它任务是其它线程,我只需要分派各个任务给线程,剩下的无需我操心了。CPU 你觉得呢?"

    CPU 心底暗道:"你自己倒是简单,只管造分身,脏活累活都是我干..."

    表面故作沉重说到:"这个改动有点大,我现在需要直接对接线程,这块需要下来好好研究一下,不过问题不大。"

    进程补充道:"CPU 你可以要记清楚了,以后线程是CPU 调度的基本单位了。"

    CPU 应道:"好的,好的,了解了(还用你复述OS 的话嘛...)。"


    规矩定下了,大家热火朝天地干活。



    image.png



    进程至少有一个线程在运行,其余按需制造线程,多个线程共用进程资源,每个线程都被CPU 执行。



    新时代的合作


    OS 照例视察各个模块的合作,这天进程又向它抱怨了:"我最近各个线程的数据总是对不上,是不是内存出现了差错?"

    OS 愣了一下,说到:"这问题我知道了,还没来得及和你说呢。最近咱们多放了几个CPU 模块提升设备的整体性能,你的线程可能在不同的CPU上运行,因此拿到的数据有点问题。"

    进程若有所思道:"以前只有一个CPU,各个进程看似同时运行,实则分享CPU时间片,是并发行为。现在CPU 多了,不同的线程有机会同时运行,这就是真并行了吧。"

    OS 道:"举一反三能力不错哦,不管并行还是并发,多个线程共享的数据有可能不一致,尤其加入了多CPU后,现象比较明显,这就是多线程数据安全问题。底层已经提供了一些基本的机制,比如CPU的MESI,但还是无法完全解决这问题,剩下的交给上层吧。"

    进程道:"了解了,那我告诉各个线程,如果他们有共享数据的需求,自己协商解决一下。"

    进程告知线程自己处理线程安全问题,线程答到:"我只是个工具人,谁用谁负责处理就好。"

    一众编程语言答到:"我自己来处理吧。"

    多CPU 如下,每个线程都有可能被其它CPU运行。



    image.png


    3、线程与Kotlin协程的故事


    Java 线程调用


    底层一众大佬已经将坑踩得差不多了,这时候得各个编程语言出场了。

    C 语言作为骨灰级人物远近闻名,OS、驱动等都是由他编写,这无需介绍了。

    之后如雨后春笋般又冒出了许多优秀的语言,如C++、Java、C#、Qt 等,本小结的主人公:Java。

    Java 从小目标远大,想要跨平台运行,借助于JVM他可以实现这个梦想,每个JVM 实例对应一个进程,并且OS 还给了他操作线程的权限。

    Java 想既然大佬这么支持,那我要撸起袖子加油干了,刚好在Android 上接到一个需求:



    通过学生的id,向后台(联网)查询学生的基本信息,如姓名、年龄等。



    Java 心想:"这还不简单,且看我猛如虎的操作。"

    先定义学生Bean类型:


    public class StudentInfo {
    //学生id
    private long stuId = 999;
    private String name = "fish";
    private int age = 18;
    }

    再定义一个获取的动作:


        //从后台获取信息
    public StudentInfo getWithoutThread(long stuId) {
    try {
    //模拟耗时操作
    Thread.sleep(2000);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    return new StudentInfo();
    }

    信心满满地运行,却被现实无情打脸,只见控制台显目的红色:



    不能在主线程进行网络请求。



    同步调用


    Java 并不气馁,这问题简单,我开个线程取获取不就得了?


        Callable<StudentInfo> callable = new Callable<StudentInfo>() {
    @Override
    public StudentInfo call() throws Exception {
    try {
    //模拟耗时操作
    Thread.sleep(2000);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    return new StudentInfo();
    }
    };

    public StudentInfo getStuInfo(long stuId) {
    //定义任务
    FutureTask<StudentInfo> futureTask = new FutureTask<>(callable);
    //开启线程,执行任务
    new Thread(futureTask).start();
    try {
    //阻塞获取结果
    StudentInfo studentInfo = futureTask.get();
    return studentInfo;
    } catch (ExecutionException e) {
    e.printStackTrace();
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    return null;
    }

    而后,再在界面上弹出学生姓名:


     JavaStudent javaStudent = new JavaStudent();
    StudentInfo studentInfo = javaStudent.getWithoutThread(999);
    Toast.makeText(this, "学生姓名:" + studentInfo.getName(), Toast.LENGTH_LONG).show();

    刚开始能弹出Toast,然而后面动不动UI就卡顿,甚至出现ANR 弹窗。

    Java 百思不得其解,后得到Android 本尊指点:



    Android 主线程不能进行耗时操作。



    Java 说到:"我就简单获取个信息,咋这么多限制..."

    Android 答到:"Android 通常需要在主线程更新UI,主线程不能做过多耗时操作,否则影响UI 渲染流畅度。不仅是Android,你Java 本身的主线程(main线程)通常也不会做耗时啊,都是通过开启各个线程去完成任务,要不然每一步都要主线程等待,那主线程的其它关键任务就没法开启了。"

    Java 沉思道:"有道理,容我三思。"


    异步调用与回调


    Java 果不愧是编程语言界的老手,闭关几天就想出了方案,直接show us code:


        //回调接口
    public interface Callback {
    void onCallback(StudentInfo studentInfo);
    }

    //异步调用
    public void getStuInfoAsync(long stuId, Callback callback) {
    new Thread(() -> {
    try {
    //模拟耗时操作
    Thread.sleep(2000);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }

    StudentInfo studentInfo = new StudentInfo();
    if (callback != null) {
    //回调给调用者
    callback.onCallback(studentInfo);
    }
    }).start();
    }

    在调用耗时方法时,只需要将自己的凭证(回调对象)传给方法即可,调用者不管方法里具体是咋实现的,才不管你开几个线程呢,反正你有结果通过回调给我。

    调用者只需要在需要的地方实现回调接收即可:


            JavaStudent javaStudent = new JavaStudent();
    javaStudent.getStuInfoAsync(999, new JavaStudent.Callback() {
    @Override
    public void onCallback(StudentInfo studentInfo) {
    //异步调用,回调从子线程返回,需要切换到主线程更新UI
    runOnUiThread(() -> {
    Toast.makeText(TestJavaActivity.this, "学生姓名:" + studentInfo.getName(), Toast.LENGTH_LONG).show();
    });
    }
    });

    异步调用的好处显而易见:



    1、不用阻塞调用者,调用者可继续做其它事情。

    2、线程没有被阻塞,相比同步调用效率更高。



    缺点也是比较明显:



    1、没有同步调用直观。

    2、容易陷入多层回调,不利于阅读与调试。

    3、从内到外的异常处理缺失传递性。



    Kotlin 协程毛遂自荐


    Java 靠着左手同步调用、右手异步调用的左右互搏技能,成功实现了很多项目,虽然异步调用有着一些缺点,但瑕不掩瑜。

    这天,Java 又收到需求变更了:



    通过学生id,获取学生信息,通过学生信息,获取他的语文老师id,通过语文老师id,获取老师姓名,最后更新UI。



    Java 不假思索到:"简单,我再嵌套一层回调即可。"


        //回调接口
    public interface Callback {
    void onCallback(StudentInfo studentInfo);
    //新增老师回调接口
    default void onCallback(TeacherInfo teacherInfo){}
    }

    //异步调用
    public void getTeachInfoAsync(long stuId, Callback callback) {
    //先获取学生信息
    getStuInfoAsync(stuId, new Callback() {
    @Override
    public void onCallback(StudentInfo studentInfo) {
    //获取学生信息后,取出关联的语文老师id,获取老师信息
    new Thread(() -> {
    try {
    //模拟耗时操作
    Thread.sleep(2000);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }

    TeacherInfo teacherInfo = new TeacherInfo();
    if (callback != null) {
    //老师信息获取成功
    callback.onCallback(teacherInfo);
    }
    }).start();
    }
    });
    }

    眼看Java 一下子实现了功能,Android再提需求:



    通过老师id,获取他所在的教研组信息,再通过教研组id获取教研组排名...



    Java 抗议道:"哪有这么奇葩的需求,那我不是要无限回调吗,我可以实现,但不好维护,过几天我自己看都看不懂了。"

    Android:"不就是几个回调的问题嘛,亏你还是老员工,实在不行,我找其他人。"

    Java:"...我再想想。"


    正当Java 一筹莫展之际,吃饭时刚好碰到了Kotlin,Java 难得有时间和这位新入职的小伙伴聊聊天,发发牢骚。

    Kotlin 听了Java 的遭遇,表达了同情并劝说Java 赶紧离职,Android 这块不适合他。


    Kotlin 随后找到Android,略微紧张地说:"吾有一计,可安天下。"

    Android 对于毛遂自荐的人才是非常欢迎的,问曰:"计将安出"

    Kotlin 随后激动到:协程。

    Android 诧异道:"协程,旅游?"



    image.png


    Kotlin 赶紧道:"非也,此协程非彼携程...而是它"



    jj.png


    Android 说:"看这肌肉挺大的,想必比较强,请开始你的表演吧。"

    Koltin 立马展示自己。


    class StudentCoroutine {
    private val FIXED_TEACHER_ID = 888
    fun getTeachInfo(act: Activity, stuId: Long) {
    GlobalScope.launch(Dispatchers.Main) {

    var studentInfo: StudentInfo
    var teacherInfo: TeacherInfo? = null

    //先获取学生信息
    withContext(Dispatchers.IO) {
    //模拟网络获取
    Thread.sleep(2000)
    studentInfo = StudentInfo()
    }
    //再获取教师信息
    withContext(Dispatchers.IO) {
    if (studentInfo.lanTechId.toInt() === FIXED_TEACHER_ID) {
    //模拟网络获取
    Thread.sleep(2000)
    teacherInfo = TeacherInfo()
    }
    }
    //更新UI
    Toast.makeText(act, "teacher name:${teacherInfo?.name}", Toast.LENGTH_LONG).show()
    }
    Toast.makeText(act, "主线程还在跑...", Toast.LENGTH_LONG).show()
    }
    }

    外部调用:


        var student = StudentCoroutine()
    student.getTeachInfo(this@MainActivity, 999)

    Android 一看,大吃一惊:"想不到,语言界竟然有如此厚颜无耻之...不对,如此简洁的写法。"

    Kotlin 道:"协程这概念早就有了,其它兄弟语言Python、Go等也实现了,我也是站在巨人的肩膀上,秉着解决用户痛点的思路来设计的。"

    Android 随即大手一挥道:"就冲着你这简洁的语法,今后Android 业务你来搞吧,希望你能够担起重担。"

    Kotlin 立马道:"没问题,我本身也是跨平台的,只是Java 那边...。"

    Android:"这个你无需顾虑,Java 的工作我来做,成年人应该知道这世界是残酷的。"

    Java 听到Kotlin 逐渐蚕食了自己在Android上的业务,略微生气,于是看了Kotlin 的写法,最后长舒一口气:"确实比较简洁,看起来功能阻塞了主线程,实际并没有。其实就是 用同步的写法,表达异步的调用。"

    Koltin :"知我者,老大哥Java 也。"


    4、Kotlin 协程的使命


    通过与Java 的比对,大家也知道了协程最大的特色:



    将异步编程同步化。



    当然还有一些特点,如异常处理、协程取消等。

    再回过头来看看上面的疑问。


    1、协程是轻量级线程、比线程耗费资源少
    这话虽然是官方说的,但我觉得有点误导的作用,协程是语言层面的东西,线程是系统层面的东西,两者没有可比性。

    协程就是一段代码块,既然是代码那就离不开CPU的执行,而CPU调度的基本单位是线程。


    2、协程是线程框架
    协程解决了移步编程时过多回调的问题,既然是异步编程,那势必涉及到不同的线程。Kotlin 协程内部自己维护了线程池,与Java 线程池相比有些优化的地方。在使用协程过程中,无需关注线程的切换细节,只需指定想要执行的线程即可,从对线程的封装这方面来说这说话也没问题。


    3、协程效率高于线程
    与第一点类似,协程在运行方面的高效率其实换成回调方式也是能够达成同样的效果,实际上协程内部也是通过回调实现的,只是在编译阶段封装了回调的细节而已。因此,协程与线程没有可比性。


    阅读完上述内容,想必大家都知道进程、线程、协程的关系了,也许大家还很好奇协程是怎么做到不阻塞调用者线程的?它又是怎么在获取结果后回到原来的位置继续执行呢?线程之间如何做到丝滑般切换的?

    不要着急,这些点我们一点点探秘,下篇文章开始徒手开启一个协程,并分析其原理。

    Kotlin 源码阅读需要一定的Kotlin 基础,尤其是高阶函数,若是这方面还不太懂的同学可以查阅之前的文章:Kotlin 高阶函数从未如此清晰 系列


    本文基于Kotlin 1.5.3,文中完整Demo请点击



    coroutine_.gif


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

    Flutter 混合开发(Android)Flutter跟Native相互通信

    前言Flutter 作为混合开发,跟native端做一些交互在所难免,比如说调用原生系统传感器、原生端的网络框架进行数据请求就会用到 Flutter 调用android 及android 原生调用 Flutter的方法,这里就涉及到Platform Chann...
    继续阅读 »

    前言

    Flutter 作为混合开发,跟native端做一些交互在所难免,比如说调用原生系统传感器、原生端的网络框架进行数据请求就会用到 Flutter 调用android 及android 原生调用 Flutter的方法,这里就涉及到Platform Channels(平台通道)

    Platform Channels (平台通道)

    Flutter 通过Channel 与客户端之间传递消息,如图:


    图中就是通过MethodChannel的方式实现Flutter 与客户端之间的消息传递。MethodChannel是Platform Channels中的一种,Flutter有三种通信类型:

    BasicMessageChannel:用于传递字符串和半结构化的信息

    MethodChannel:用于传递方法调用(method invocation)通常用来调用native中某个方法

    EventChannel: 用于数据流(event streams)的通信。有监听功能,比如电量变化之后直接推送数据给flutter端。

    为了保证UI的响应,通过Platform Channels传递的消息都是异步的。

    更多关于channel原理可以去看这篇文章:channel原理篇

    Platform Channels 使用

    1.MethodChannel的使用

    原生客户端写法(以Android 为例)

    首先定义一个获取手机电量方法

    private int getBatteryLevel() {
    return 90;
    }

    这函数是要给Flutter 调用的方法,此时就需要通过 MethodChannel 来建立这个通道了。

    首先新增一个初始化 MethodChannel 的方法

    private String METHOD_CHANNEL = "common.flutter/battery";
    private String GET_BATTERY_LEVEL = "getBatteryLevel";
    private MethodChannel methodChannel;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    GeneratedPluginRegistrant.registerWith(this);
    initMethodChannel();
    getFlutterView().postDelayed(() ->
    methodChannel.invokeMethod("get_message", null, new MethodChannel.Result() {
    @Override
    public void success(@Nullable Object o) {
    Log.d(TAG, "get_message:" + o.toString());
    }

    @Override
    public void error(String s, @Nullable String s1, @Nullable Object o) {

    }

    @Override
    public void notImplemented() {

    }
    }), 5000);

    }

    private void initMethodChannel() {
    methodChannel = new MethodChannel(getFlutterView(), METHOD_CHANNEL);
    methodChannel.setMethodCallHandler(
    (methodCall, result) -> {
    if (methodCall.method.equals(GET_BATTERY_LEVEL)) {
    int batteryLevel = getBatteryLevel();

    if (batteryLevel != -1) {
    result.success(batteryLevel);
    } else {
    result.error("UNAVAILABLE", "Battery level not available.", null);
    }
    } else {
    result.notImplemented();
    }
    });


    }

    private int getBatteryLevel() {
    return 90;
    }

    METHOD_CHANNEL 用于和flutter交互的标识,由于一般情况下会有多个channel,在app里面需要保持唯一性

    MethodChannel 都是保存在以通道名为Key的Map中。所以要是设了两个名字一样的channel,只有后设置的那个会生效。

    onMethodCall 有两个参数,onMethodCall 里包含要调用的方法名称和参数。Result是给Flutter的返回值。方法名是客户端与Flutter统一设定。通过if/switch语句判断 MethodCall.method 来区分不同的方法,在我们的例子里面我们只会处理名为“getBatteryLevel”的调用。在调用本地方法获取到电量以后通过 result.success(batteryLevel) 调用把电量值返回给Flutter。

    MethodChannel-Flutter 端

    直接先看一下Flutter端的代码

    class _MyHomePageState extends State<MyHomePage> {
    int _counter = 0;
    static const platform = const MethodChannel('common.flutter/battery');

    void _incrementCounter() {
    setState(() {
    _counter++;
    _getBatteryLevel();
    });
    }

    @override
    Widget build(BuildContext context) {
    platform.setMethodCallHandler(platformCallHandler);
    return Scaffold(
    appBar: AppBar(
    title: Text(widget.title),
    ),
    body: Center(
    child: Column(
    mainAxisAlignment: MainAxisAlignment.center,
    children: <Widget>[
    Text(
    'You have pushed the button this many times:',
    ),
    Text(
    '$_counter',
    style: Theme.of(context).textTheme.display1,
    ),
    Text('$_batteryLevel'),
    ],
    ),
    ),
    floatingActionButton: FloatingActionButton(
    onPressed: _incrementCounter,
    tooltip: 'Increment',
    child: Icon(Icons.add),
    ),
    );
    }

    String _batteryLevel = 'Unknown battery level.';

    Future<Null> _getBatteryLevel() async {
    String batteryLevel;
    try {
    final int result = await platform.invokeMethod('getBatteryLevel');
    batteryLevel = 'Battery level at $result % .';
    } on PlatformException catch (e) {
    batteryLevel = "Failed to get battery level: '${e.message}'.";
    }

    setState(() {
    _batteryLevel = batteryLevel;
    });
    }

    //客户端调用
    Future<dynamic> platformCallHandler(MethodCall call) async {
    switch (call.method) {
    case "get_message":
    return "Hello from Flutter";
    break;
    }
    }
    }

    上面代码解析:
    首先,定义一个常量result.success(platform),和Android客户端定义的channel一致;
    接下来定义一个 result.success(_getBatteryLevel())方法,用来调用Android 端的方法,result.success(final int result = await platform.invokeMethod('getBatteryLevel');) 这行代码就是通过通道来调用Native(Android)方法了。因为MethodChannel是异步调用的,所以这里必须要使用await关键字。

    在上面Android代码中我们把获取到的电量通过result.success(batteryLevel);返回给Flutter。这里await表达式执行完成以后电量就直接赋值给result变量了。然后通过result.success(setState); 去改变Text显示值。到这里为止,是通过Flutter端调用原生客户端方法。

    MethodChannel 其实是一个可以双向调用的方法,在上面的代码中,其实我们也体现了,通过原生客户端调用Flutter的方法。

    在原生端通过 methodChannel.invokeMethod 的方法调用

    methodChannel.invokeMethod("get_message", null, new MethodChannel.Result() {
    @Override
    public void success(@Nullable Object o) {
    Log.d(TAG, "get_message:" + o.toString());
    }

    @Override
    public void error(String s, @Nullable String s1, @Nullable Object o) {

    }

    @Override
    public void notImplemented() {

    }
    });

    在Flutter端就需要给MethodChannel设置一个MethodCallHandler

    static const platform = const MethodChannel('common.flutter/battery');
    platform.setMethodCallHandler(platformCallHandler);
    Future<dynamic> platformCallHandler(MethodCall call) async {
    switch (call.method) {
    case "get_message":
    return "Hello from Flutter";
    break;
    }
    }

    以上就是MethodChannel的相关用法了。

    EventChannel

    将数据推送给Flutter端,类似我们常用的推送功能,有需要就推送给Flutter端,是否需要去处理这个推送由Flutter那边决定。相对于MethodChannel是主动获取,EventChannel则是被动推送。

    EventChannel 原生客户端写法

    private String EVENT_CHANNEL = "common.flutter/message";
    private int count = 0;
    private Timer timer;

    private void initEventChannel() {
    new EventChannel(getFlutterView(), EVENT_CHANNEL).setStreamHandler(new EventChannel.StreamHandler() {
    @Override
    public void onListen(Object arguments, EventChannel.EventSink events) {
    timer.schedule(new TimerTask() {
    @Override
    public void run() {
    if (count < 10) {
    count++;
    events.success("当前时间:" + System.currentTimeMillis());
    } else {
    timer.cancel();
    }
    }
    }, 1000, 1000);
    }

    @Override
    public void onCancel(Object o) {

    }
    });
    }

    在上面的代码中,我们做了一个定时器,每秒向Flutter推送一个消息,告诉Flutter我们当前时间。为了防止一直倒计时,我这边做了个计数,超过10次就停止发送。

    EventChannel Flutter端

    String message = "not message";
    static const eventChannel = const EventChannel('common.flutter/message');
    @override
    void initState() {
    super.initState();
    eventChannel.receiveBroadcastStream().listen(_onEvent, onError: _onError);
    }

    void _onEvent(Object event) {
    setState(() {
    message =
    "message: $event";
    });
    }

    void _onError(Object error) {
    setState(() {
    message = 'message: unknown.';
    });
    }

    上面的代码就是Flutter端接收原生客户端数据,通过_onEvent 来接收数据,将数据显示Text。这个实现相对简单,如果要达到业务分类,需要将数据封装成json,通过json数据包装一些对应业务标识和数据来做区分。

    BasicMessageChannel

    BasicMessageChannel (主要是传递字符串和一些半结构体的数据)

    BasicMessageChannel Android端

    private void initBasicMessageChannel() {
    BasicMessageChannel<Object> basicMessageChannel = new BasicMessageChannel<>(getFlutterView(), BASIC_CHANNEL, StandardMessageCodec.INSTANCE);
    //主动发送消息到flutter 并接收flutter消息回复
    basicMessageChannel.send("send basic message", (object)-> {
    Log.e(TAG, "receive reply msg from flutter:" + object.toString());
    });

    //接收flutter消息 并发送回复
    basicMessageChannel.setMessageHandler((object, reply)-> {
    Log.e(TAG, "receive msg from flutter:" + object.toString());
    reply.reply("reply:got your message");

    });

    }

    BasicMessageChannel Flutter端

      static const basicChannel = const BasicMessageChannel('common.flutter/basic', StandardMessageCodec());
    //发送消息到原生客户端 并且接收到原生客户端的回复
    Future<String> sendMessage() async {
    String reply = await basicChannel.send('this is flutter');
    print("receive reply msg from native:$reply");
    return reply;
    }

    //接收原生消息 并发送回复
    void receiveMessage() async {
    basicChannel.setMessageHandler((msg) async {
    print("receive from Android:$msg");
    return "get native message";
    });

    上面例子中用到的编解码器为StandardMessageCodec ,例子中通信都是String,用StringCodec也可以。

    以上就是Flutter提供三种platform和dart端的消息通信方式。

    本文转载自: https://www.jianshu.com/p/1f12e53f5fb3
    收起阅读 »

    在浏览器输入URL到页面展示发生了什么

    查询缓存其实从填写上url按下回车后,我们就进入了第一步就是 DNS 解析过程,首先需要找到这个 url 域名的服务器 ip,为了寻找这个 ip,浏览器首先会寻找缓存,查看缓存中是否有记录缓存的查找记录为:浏览器缓存=》系统缓存=》路由 器缓存缓存中没有则查找...
    继续阅读 »

    查询缓存


    其实从填写上url按下回车后,我们就进入了第一步就是 DNS 解析过程,首先需要找到这个 url 域名的服务器 ip,为了寻找这个 ip,浏览器首先会寻找缓存,查看缓存中是否有记录缓存的查找记录为:浏览器缓存=》系统缓存=》路由 器缓存缓存中没有则查找系统的 hosts 文件中是否有记录,

    DNS服务器


    如果没有缓存则查询 DNS 服务器,得到服务器的 ip 地址后,浏览器根据这个 ip 以及相应的端口号发送连接请求;当然如果DNS服务器中没有解析成功,他会向上一步获得的顶级DNS服务器发送解析请求。


    TCP三次握手


    客户端和服务端都需要直到各自可收发,因此需要三次握手。

    从图片可以得到三次握手可以简化为:

    1、浏览器发送连接请求;
    2、服务器允许连接后并发送ACK报文给浏览器;
    2、浏览器接受ACK后并向后端发送一个ACK,TCP连接建立成功
    HTTP协议包

    构造一个 http 请求,这个请求报文会包括这次请求的信息,主要是请求方法,请求说明和请求附带的数据,并将这个 http 请求封装在一个 tcp 包中;这个 tcp 包也就是会依次经过传输层,网络层, 数据链路层,物理层到达服务器,服务器解析这个请求来作出响应;返回相应的 html 给浏览器;
    浏览器处理HTML文档

    因为 html 是一个树形结构,浏览器根据这个 html 来构建 DOM 树,在 dom 树的构建过程中如果遇到 JS 脚本和外部 JS 连接,则会停止构建 DOM 树来执行和下载相应的代码,这会造成阻塞,这就是为什么推荐 JS 代码应该放在 html 代码的后面;

    渲染树
    之后根据外部样式,内部样式,内联样式构建一个 CSS 对象模型树 CSSOM 树,构建完成后和 DOM 树合并为渲染树,在排除非视觉节点,比如 script,meta 标签和排除 display 为 none 的节点,之后进行布局,布局主要是确定各个元素的位置和尺寸,之后是渲染页面,因为 html 文件中会含有图片,视频,音频等资源,在解析 DOM 的过 程中,遇到这些都会进行并行下载,浏览器对每个域的并行下载数量有一定的限制,一 般是 4-6 个,当然在这些所有的请求中我们还需要关注的就是缓存,缓存一般通过 Cache-Control、Last-Modify、Expires 等首部字段控制。

    Cache-Control 和 Expires 的区别
    在于 Cache-Control 使用相对时间,Expires 使用的是基于服务器 端的绝对时间,因为存 在时差问题,一般采用 Cache-Control,在请求这些有设置了缓存的数据时,会先 查看 是否过期,如果没有过期则直接使用本地缓存,过期则请求并在服务器校验文件是否修 改,如果上一次 响应设置了 ETag 值会在这次请求的时候作为 If-None-Match 的值交给 服务器校验,如果一致,继续校验 Last-Modified,没有设置 ETag 则直接验证 Last-Modified,再决定是否返回 304

    到这里就结束了么?其实按照标题所说的到渲染页面我们确实到此就说明完了,但是严格意义上其实我们后面还会有TCP的四次挥手断开连接,这个我们就放到后面单独出一篇为大家介绍吧!
    TCP 和 UDP 的区别

    1、TCP 是面向连接的,udp 是无连接的即发送数据前不需要先建立链接。
    2、TCP 提供可靠的服务。也就是说,通过 TCP 连接传送的数据,无差错,不丢失, 不重复,且按序到达;UDP 尽最大努力交付,即不保证可靠交付。 并且因为 tcp 可靠, 面向连接,不会丢失数据因此适合大数据量的交换。
    3、TCP 是面向字节流,UDP 面向报文,并且网络出现拥塞不会使得发送速率降低(因 此会出现丢包,对实时的应用比如 IP 电话和视频会议等)。
    4、TCP 只能是 1 对 1 的,UDP 支持 1 对 1,1 对多。
    5、TCP 的首部较大为 20 字节,而 UDP 只有 8 字节。
    6、TCP 是面向连接的可靠性传输,而 UDP 是不可靠的。



    收起阅读 »

    什么?你连个三色渐变圆角按钮都需要UI切图?

    废话不多说,先上效果图: 该效果其实由三部分组成: 渐变 圆角 文本 渐变 关于渐变,估计大家都不会陌生,以往都是使用gradient进行制作: shape_gradient.xml <?xml version="1.0" encoding="ut...
    继续阅读 »

    废话不多说,先上效果图:



    该效果其实由三部分组成:



    • 渐变

    • 圆角

    • 文本


    渐变


    关于渐变,估计大家都不会陌生,以往都是使用gradient进行制作:


    shape_gradient.xml


    <?xml version="1.0" encoding="utf-8"?>
    <shape xmlns:android="http://schemas.android.com/apk/res/android">
    <gradient
    android:startColor="#B620E0"
    android:endColor="#E38746" />
    </shape>

        <View
    android:layout_width="match_parent"
    android:layout_height="70dp"
    android:background="@drawable/shape_gradient" />


    但是,这个只能支持双色渐变,超过双色就无能为力了,所以,我们要考虑使用其它方式:


        /**
    * Create a shader that draws a linear gradient along a line.
    *
    * @param x0 The x-coordinate for the start of the gradient line
    * @param y0 The y-coordinate for the start of the gradient line
    * @param x1 The x-coordinate for the end of the gradient line
    * @param y1 The y-coordinate for the end of the gradient line
    * @param colors The colors to be distributed along the gradient line
    * @param positions May be null. The relative positions [0..1] of
    * each corresponding color in the colors array. If this is null,
    * the the colors are distributed evenly along the gradient line.
    * @param tile The Shader tiling mode
    */
    public LinearGradient(float x0, float y0, float x1, float y1, @NonNull @ColorInt int colors[],
    @Nullable float positions[], @NonNull TileMode tile)

        /**
    * x0、y0、x1、y1为决定渐变颜色方向的两个坐标点,x0、y0为起始坐标,x1、y1为终点坐标
    * @param colors 所有渐变颜色的数组,即放多少个颜色进去,就有多少种渐变颜色
    * @param positions 渐变颜色的比值,默认为均匀分布。
    * 把总长度理解为1,假如里面的值为[0.3,0.2,0.5],那么,渐变的颜色就会以 0.3 : 0:2 :0.5 比例进行排版
    * @param tile 着色器模式
    */
    public LinearGradient(float x0, float y0, float x1, float y1, int colors[], float positions[],
    TileMode tile)

    创建自定义View


    public class ColorView extends View {
    public ColorView(Context context) {
    super(context);
    }

    public ColorView(Context context, @Nullable AttributeSet attrs) {
    super(context, attrs);
    }

    public ColorView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    }

    @Override
    protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    //获取宽高
    int width = getWidth();
    int height = getHeight();

    //渐变的颜色
    int colorStart = Color.parseColor("#E38746");
    int color1 = Color.parseColor("#B620E0");
    int colorEnd = Color.parseColor("#5995F6");
    //绘画渐变效果
    Paint paintColor = new Paint();
    LinearGradient backGradient = new LinearGradient(0, height, width, 0, new int[]{colorStart, color1, colorEnd}, null, Shader.TileMode.CLAMP);
    paintColor.setShader(backGradient);
    canvas.drawRect(0, 0, width, height, paintColor);
    }
    }

        <com.jm.xpproject.ColorView
    android:layout_width="match_parent"
    android:layout_height="70dp" />

    效果:



    圆角


    关于圆角,我们需要使用到BitmapShader,使用方式:


            BitmapShader bitmapShaderColor = new BitmapShader(bitmapColor, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
    // 初始化画笔
    Paint paintFillet = new Paint();
    paintFillet.setAntiAlias(true);
    paintFillet.setShader(bitmapShaderColor);
    //绘画到画布中
    canvas.drawRoundRect(new RectF(0, 0, width, height), radius, radius, paintFillet);

    由于这里的BitmapShader是对于Bitmap进行操作的,所以,对于渐变效果,我们不能直接把他绘画到原始画布上,而是生成一个Bitmap,将渐变绘画记录下来:


    还是刚刚的自定义View


        @Override
    protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    //获取View的宽高
    int width = getWidth();
    int height = getHeight();

    //第一步,绘画出一个渐变效果的Bitmap
    //创建存放渐变效果的bitmap
    Bitmap bitmapColor = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
    Canvas canvasColor = new Canvas(bitmapColor);
    //渐变的颜色
    int colorStart = Color.parseColor("#E38746");
    int color1 = Color.parseColor("#B620E0");
    int colorEnd = Color.parseColor("#5995F6");
    //绘画渐变效果
    Paint paintColor = new Paint();
    LinearGradient backGradient = new LinearGradient(0, height, width, 0, new int[]{colorStart, color1, colorEnd}, null, Shader.TileMode.CLAMP);
    paintColor.setShader(backGradient);
    canvasColor.drawRect(0, 0, width, height, paintColor);
    //第二步,绘画出一个圆角渐变效果
    //绘画出圆角渐变效果
    BitmapShader bitmapShaderColor = new BitmapShader(bitmapColor, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
    // 初始化画笔
    Paint paintFillet = new Paint();
    paintFillet.setAntiAlias(true);
    paintFillet.setShader(bitmapShaderColor);
    //绘画到画布中
    canvas.drawRoundRect(new RectF(0, 0, width, height), 100, 100, paintFillet);
    }

    效果:



    至于中间的空白部分,其实我们依葫芦画瓢,再画上一个白色的圆角Bitmap即可:


            //创建存放白底的bitmap
    Bitmap bitmapWhite = Bitmap.createBitmap(width - colorWidth * 2, height - colorWidth * 2, Bitmap.Config.RGB_565);
    bitmapWhite.eraseColor(Color.parseColor("#FFFFFF"));

    BitmapShader bitmapShaderWhite = new BitmapShader(bitmapWhite, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
    // 初始化画笔
    Paint paintWhite = new Paint();
    paintWhite.setAntiAlias(true);
    paintWhite.setShader(bitmapShaderWhite);
    // 将白色Bitmap绘制到画布上面
    canvas.drawRoundRect(new RectF(colorWidth, colorWidth, width - colorWidth, height - colorWidth), radius, radius, paintWhite);

    总体代码:



    @Override
    protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    //获取View的宽高
    int width = getWidth();
    int height = getHeight();

    //第一步,绘画出一个渐变效果的Bitmap
    //创建存放渐变效果的bitmap
    Bitmap bitmapColor = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
    Canvas canvasColor = new Canvas(bitmapColor);
    //渐变的颜色
    int colorStart = Color.parseColor("#E38746");
    int color1 = Color.parseColor("#B620E0");
    int colorEnd = Color.parseColor("#5995F6");
    //绘画渐变效果
    Paint paintColor = new Paint();
    LinearGradient backGradient = new LinearGradient(0, height, width, 0, new int[]{colorStart, color1, colorEnd}, null, Shader.TileMode.CLAMP);
    paintColor.setShader(backGradient);
    canvasColor.drawRect(0, 0, width, height, paintColor);
    //第二步,绘画出一个圆角渐变效果
    //绘画出圆角渐变效果
    BitmapShader bitmapShaderColor = new BitmapShader(bitmapColor, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
    // 初始化画笔
    Paint paintFillet = new Paint();
    paintFillet.setAntiAlias(true);
    paintFillet.setShader(bitmapShaderColor);
    //绘画到画布中
    canvas.drawRoundRect(new RectF(0, 0, width, height), 100, 100, paintFillet);

    //第三步,绘画出一个白色的bitmap覆盖上去
    //创建存放白底的bitmap
    Bitmap bitmapWhite = Bitmap.createBitmap(width - 5 * 2, height - 5 * 2, Bitmap.Config.RGB_565);
    bitmapWhite.eraseColor(Color.parseColor("#FFFFFF"));

    BitmapShader bitmapShaderWhite = new BitmapShader(bitmapWhite, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
    // 初始化画笔
    Paint paintWhite = new Paint();
    paintWhite.setAntiAlias(true);
    paintWhite.setShader(bitmapShaderWhite);
    // 将白色Bitmap绘制到画布上面
    canvas.drawRoundRect(new RectF(5, 5, width - 5, height - 5), 100, 100, paintWhite);
    }

    效果:



    文本


    像文本就简单了,使用drawText即可,只要注意在绘画的时候,要对文本进行居中显示,因为 Android 默认绘画文本,是从左下角进行绘画的,就像这样:


            Paint paintText = new Paint();
    paintText.setAntiAlias(true);
    paintText.setColor(Color.parseColor("#000000"));
    paintText.setTextSize(100);
    canvas.drawText("收藏", width / 2, height / 2, paintText);
    canvas.drawLine(width / 2, 0, width / 2, height, paintText);
    canvas.drawLine(0, height / 2, width, height / 2, paintText);


    正确做法:


            String text = "收藏";
    Rect rect = new Rect();
    Paint paintText = new Paint();
    paintText.setAntiAlias(true);
    paintText.setColor(Color.parseColor("#000000"));
    paintText.setTextSize(100);
    paintText.getTextBounds(text, 0, text.length(), rect);
    int widthFont = rect.width();//文本的宽度
    int heightFont = rect.height();//文本的高度
    canvas.drawText(text, (width - widthFont) / 2, (height+heightFont) / 2, paintText);


    至此,基本功能的制作就完成了



    @Override
    protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    //获取View的宽高
    int width = getWidth();
    int height = getHeight();

    //第一步,绘画出一个渐变效果的Bitmap
    //创建存放渐变效果的bitmap
    Bitmap bitmapColor = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
    Canvas canvasColor = new Canvas(bitmapColor);
    //渐变的颜色
    int colorStart = Color.parseColor("#E38746");
    int color1 = Color.parseColor("#B620E0");
    int colorEnd = Color.parseColor("#5995F6");
    //绘画渐变效果
    Paint paintColor = new Paint();
    LinearGradient backGradient = new LinearGradient(0, height, width, 0, new int[]{colorStart, color1, colorEnd}, null, Shader.TileMode.CLAMP);
    paintColor.setShader(backGradient);
    canvasColor.drawRect(0, 0, width, height, paintColor);
    //第二步,绘画出一个圆角渐变效果
    //绘画出圆角渐变效果
    BitmapShader bitmapShaderColor = new BitmapShader(bitmapColor, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
    // 初始化画笔
    Paint paintFillet = new Paint();
    paintFillet.setAntiAlias(true);
    paintFillet.setShader(bitmapShaderColor);
    //绘画到画布中
    canvas.drawRoundRect(new RectF(0, 0, width, height), 100, 100, paintFillet);

    //第三步,绘画出一个白色的bitmap覆盖上去
    //创建存放白底的bitmap
    Bitmap bitmapWhite = Bitmap.createBitmap(width - 5 * 2, height - 5 * 2, Bitmap.Config.RGB_565);
    bitmapWhite.eraseColor(Color.parseColor("#FFFFFF"));

    BitmapShader bitmapShaderWhite = new BitmapShader(bitmapWhite, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
    // 初始化画笔
    Paint paintWhite = new Paint();
    paintWhite.setAntiAlias(true);
    paintWhite.setShader(bitmapShaderWhite);
    // 将白色Bitmap绘制到画布上面
    canvas.drawRoundRect(new RectF(5, 5, width - 5, height - 5), 100, 100, paintWhite);

    String text = "收藏";
    Rect rect = new Rect();
    Paint paintText = new Paint();
    paintText.setAntiAlias(true);
    paintText.setColor(Color.parseColor("#000000"));
    paintText.setTextSize(100);
    paintText.getTextBounds(text, 0, text.length(), rect);
    int widthFont = rect.width();//文本的宽度
    int heightFont = rect.height();//文本的高度
    canvas.drawText(text, (width - widthFont) / 2, (height+heightFont) / 2, paintText);
    }

    封装


    上面虽然已经把全部功能都讲解完了,但是,假如就直接这样放入项目中,是极其不规范的,无法动态设置文本、文本大小、颜色厚度等等


    这里,我进行了简易封装,大家可以基于此进行业务修改:


    attrs.xml


        <declare-styleable name="GradientColorButton">
    <attr name="btnText" format="string" />
    <attr name="btnTextSize" format="dimension" />
    <attr name="btnTextColor" format="color" />
    <attr name="colorWidth" format="dimension" />
    <attr name="colorRadius" format="dimension" />
    </declare-styleable>

    public class GradientColorButton extends View {

    /**
    * 文本
    */
    private String text = "";
    /**
    * 文本颜色
    */
    private int textColor;
    /**
    * 文本大小
    */
    private float textSize;
    /**
    * 颜色的宽度
    */
    private float colorWidth;
    /**
    * 圆角度数
    */
    private float radius;

    //渐变的颜色
    private int colorStart = Color.parseColor("#E38746");
    private int color1 = Color.parseColor("#B620E0");
    private int colorEnd = Color.parseColor("#5995F6");

    //控件的宽高
    private int width;
    private int height;
    /**
    * 渐变颜色的Bitmap
    */
    private Bitmap bitmapColor;

    //画笔
    private Paint paintColor;
    private Paint paintFillet;
    private Paint paintWhite;
    private Paint paintText;
    //字体的宽高
    private int widthFont;
    private int heightFont;

    public GradientColorButton(Context context) {
    super(context);
    }

    public GradientColorButton(Context context, @Nullable AttributeSet attrs) {
    this(context, attrs, 0);
    }

    public GradientColorButton(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);

    //获取参数
    TypedArray a = context.obtainStyledAttributes(attrs,
    R.styleable.GradientColorButton, defStyleAttr, 0);

    text = a.getString(R.styleable.GradientColorButton_btnText);
    textColor = a.getColor(R.styleable.GradientColorButton_btnTextColor, Color.BLACK);
    textSize = a.getDimension(R.styleable.GradientColorButton_btnTextSize, 16);
    colorWidth = a.getDimension(R.styleable.GradientColorButton_colorWidth, 5);
    radius = a.getDimension(R.styleable.GradientColorButton_colorRadius, 100);


    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    super.onLayout(changed, left, top, right, bottom);

    //获取View的宽高
    width = getWidth();
    height = getHeight();

    //制作一个渐变效果的Bitmap
    createGradientBitmap();

    //初始化圆角配置
    initFilletConfiguration();

    //初始化白色Bitmap配置
    initWhiteBitmapConfiguration();

    //初始化文本配置
    initTextConfiguration();

    }


    /**
    * 创建渐变颜色的Bitmap
    */
    private void createGradientBitmap() {
    //创建存放渐变效果的bitmap
    bitmapColor = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
    Canvas canvasColor = new Canvas(bitmapColor);
    LinearGradient backGradient = new LinearGradient(0, height, width, 0, new int[]{colorStart, color1, colorEnd}, null, Shader.TileMode.CLAMP);
    //绘画渐变效果
    paintColor = new Paint();
    paintColor.setShader(backGradient);
    canvasColor.drawRect(0, 0, width, height, paintColor);
    }


    /**
    * 初始化圆角配置
    */
    private void initFilletConfiguration() {
    //绘画出圆角渐变效果
    BitmapShader bitmapShaderColor = new BitmapShader(bitmapColor, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
    // 初始化画笔
    paintFillet = new Paint();
    paintFillet.setAntiAlias(true);
    paintFillet.setShader(bitmapShaderColor);
    }

    /**
    * 初始化白色Bitmap配置
    */
    private void initWhiteBitmapConfiguration() {
    //创建存放白底的bitmap
    Bitmap bitmapWhite = Bitmap.createBitmap((int) (width - colorWidth * 2), (int) (height - colorWidth * 2), Bitmap.Config.RGB_565);
    bitmapWhite.eraseColor(Color.parseColor("#FFFFFF"));

    BitmapShader bitmapShaderWhite = new BitmapShader(bitmapWhite, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
    // 初始化画笔
    paintWhite = new Paint();
    paintWhite.setAntiAlias(true);
    paintWhite.setShader(bitmapShaderWhite);
    }

    /**
    * 初始化文本配置
    */
    private void initTextConfiguration() {
    Rect rect = new Rect();
    paintText = new Paint();
    paintText.setAntiAlias(true);
    paintText.setColor(textColor);
    paintText.setTextSize(textSize);
    if (!TextUtils.isEmpty(text)) {
    paintText.getTextBounds(text, 0, text.length(), rect);
    widthFont = rect.width();//文本的宽度
    heightFont = rect.height();//文本的高度

    }
    }


    @Override
    protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);

    //将圆角渐变bitmap绘画到画布中
    canvas.drawRoundRect(new RectF(0, 0, width, height), radius, radius, paintFillet);
    // 将白色Bitmap绘制到画布上面
    canvas.drawRoundRect(new RectF(colorWidth, colorWidth, width - colorWidth, height - colorWidth), radius, radius, paintWhite);


    if (!TextUtils.isEmpty(text)) {
    canvas.drawText(text, (width - widthFont) / 2, (height + heightFont) / 2, paintText);
    }

    }
    }

        <com.jm.xpproject.GradientColorButton
    android:layout_width="120dp"
    android:layout_height="70dp"
    android:layout_margin="10dp"
    app:btnText="收藏"
    app:btnTextColor="#123456"
    app:btnTextSize="18sp"
    app:colorRadius="50dp"
    app:colorWidth="5dp" />


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

    Kafka QUICKSTART

    一. 安装和启动Kafka我本地机器已经安装CDH 6.3.1版本,此处省略安装和启动Kafka的步骤。Kafka版本:2.2.1ps -ef|grep '/libs/kafka.\{2,40\}.jar'复制1.1 Kafka的配置文件[root@hp1 c...
    继续阅读 »

    一. 安装和启动Kafka

    我本地机器已经安装CDH 6.3.1版本,此处省略安装和启动Kafka的步骤。

    Kafka版本:2.2.1

    ps -ef|grep '/libs/kafka.\{2,40\}.jar'

    1.1 Kafka的配置文件

    [root@hp1 config]# find / -name server.properties
    /opt/cloudera/parcels/CDH-6.3.1-1.cdh6.3.1.p0.1470567/etc/kafka/conf.dist/server.properties

    常用的配置如下:

    #broker 的全局唯一编号,不能重复
    broker.id=0
    #删除 topic 功能使能
    delete.topic.enable=true
    #处理网络请求的线程数量
    num.network.threads=3
    #用来处理磁盘 IO 的线程数量
    num.io.threads=8
    #发送套接字的缓冲区大小
    socket.send.buffer.bytes=102400
    #接收套接字的缓冲区大小
    socket.receive.buffer.bytes=102400
    #请求套接字的缓冲区大小
    socket.request.max.bytes=104857600
    #kafka 运行日志存放的路径
    log.dirs=/opt/module/kafka/logs
    #topic 在当前 broker 上的分区个数
    num.partitions=1
    #用来恢复和清理 data 下数据的线程数量
    num.recovery.threads.per.data.dir=1
    #segment 文件保留的最长时间,超时将被删除
    log.retention.hours=168
    #配置连接 Zookeeper 集群地址
    zookeeper.connect=hadoop102:2181,hadoop103:2181,hadoop104:2181

    二. 创建一个主题来存储事件

    Kafka是一个分布式的事件流平台,可以让你跨多台机器读、写、存储和处理事件(在文档中也称为记录或消息)。

    示例事件包括支付交易、来自移动电话的地理位置更新、发货订单、来自物联网设备或医疗设备的传感器测量,等等。这些事件被组织并存储在主题中。很简单,一个主题类似于文件系统中的一个文件夹,事件就是该文件夹中的文件。

    2.1 创建主题

    所以在你写你的第一个事件之前,你必须创建一个主题。打开另一个终端会话并运行:

    /opt/cloudera/parcels/CDH-6.3.1-1.cdh6.3.1.p0.1470567/lib/kafka/bin/kafka-topics.sh --create --zookeeper localhost:2181 --replication-factor 1 --partitions 1 --topic first

    2.2 查看当前事件描述

    所有Kafka的命令行工具都有额外的选项:运行不带任何参数的Kafka -topics.sh命令来显示使用信息。例如,它还可以显示新主题的分区计数等详细信息:

    -- 查看主题topic的描述
    /opt/cloudera/parcels/CDH-6.3.1-1.cdh6.3.1.p0.1470567/lib/kafka/bin/kafka-topics.sh --describe --zookeeper localhost:2181 --topic first
    -- 查看所有的topic的描述
    /opt/cloudera/parcels/CDH-6.3.1-1.cdh6.3.1.p0.1470567/lib/kafka/bin/kafka-topics.sh --describe --zookeeper localhost:2181

    一个分区一个副本

    我们来看看创建多分区多副本

    /opt/cloudera/parcels/CDH-6.3.1-1.cdh6.3.1.p0.1470567/lib/kafka/bin/kafka-topics.sh --create --zookeeper localhost:2181 --replication-factor 1 --partitions 1 --topic first_1_1
    /opt/cloudera/parcels/CDH-6.3.1-1.cdh6.3.1.p0.1470567/lib/kafka/bin/kafka-topics.sh --create --zookeeper localhost:2181 --replication-factor 1 --partitions 2 --topic first_1_2
    /opt/cloudera/parcels/CDH-6.3.1-1.cdh6.3.1.p0.1470567/lib/kafka/bin/kafka-topics.sh --create --zookeeper localhost:2181 --replication-factor 2 --partitions 2 --topic first_2_2
    /opt/cloudera/parcels/CDH-6.3.1-1.cdh6.3.1.p0.1470567/lib/kafka/bin/kafka-topics.sh --create --zookeeper localhost:2181 --replication-factor 3 --partitions 3 --topic first_3_3

    本地测试只有3台broker,所以最多只能创建3个replication-factor

    2.3 删除主题

    需要 server.properties中设置 delete.topic.enable=true否则只是标记删除。 否则只是标记删除。

    cd /opt/cloudera/parcels/CDH-6.3.1-1.cdh6.3.1.p0.1470567/lib/kafka/bin
    ./kafka-topics.sh --zookeeper localhost:2181 --delete --topic first

    三. 在主题中加入一些事件

    Kafka客户端通过网络与Kafka的代理通信,用于写(或读)事件。一旦收到,代理将以持久和容错的方式存储事件,只要您需要—甚至永远。

    运行控制台生成程序客户端,在主题中写入一些事件。默认情况下,您输入的每一行都将导致一个单独的事件被写入主题。

    /opt/cloudera/parcels/CDH-6.3.1-1.cdh6.3.1.p0.1470567/lib/kafka/bin/kafka-console-producer.sh --broker-list 10.31.1.124:9092 --topic first

    四. 读事件

    打开另一个终端会话并运行控制台消费者客户端来读取你刚刚创建的事件:

    /opt/cloudera/parcels/CDH-6.3.1-1.cdh6.3.1.p0.1470567/lib/kafka/bin/kafka-console-consumer.sh --from-beginning --bootstrap-server 10.31.1.124:9092 --topic first

    --from-beginning:会把主题中以往所有的数据都读取出来。

    您可以随时使用Ctrl-C停止客户端。

    您可以自由地进行试验:例如,切换回您的生产者终端(上一步)来编写额外的事件,并查看这些事件如何立即显示在您的消费者终端上。

    因为事件是持久性存储在Kafka中,它们可以被任意多的消费者读取。您可以通过再次打开另一个终端会话并再次运行前面的命令来轻松验证这一点。

    六. 用kafka connect导入/导出你的数据作为事件流

    您可能在现有系统(如关系数据库或传统消息传递系统)中有许多数据,以及许多已经使用这些系统的应用程序。Kafka Connect允许你不断地从外部系统获取数据到Kafka,反之亦然。因此,将现有系统与Kafka集成是非常容易的。为了使这个过程更容易,有数百个这样的连接器。

    看看Kafka Connect部分,了解更多关于如何不断地导入/导出你的数据到Kafka。

    七. 用kafka流处理你的事件

    一旦你的数据以事件的形式存储在Kafka中,你就可以用Java/Scala的Kafka Streams客户端库来处理这些数据。它允许你实现关键任务实时应用和微服务,其中输入和/或输出数据存储在Kafka主题。Kafka Streams结合了客户端编写和部署标准Java和Scala应用程序的简单性和Kafka服务器端集群技术的优点,使这些应用程序具有高度的可扩展性、弹性、容错性和分布式。该库支持一次处理、有状态操作和聚合、窗口、连接、基于事件时间的处理等等。

    本文转载自: https://www.jianshu.com/p/9d900eed46d7
    收起阅读 »

    kafka源码之旅------Kafka元数据管理

    我们往kafka集群中发送数据的时候,kafka是怎么感知到需要发送到哪一台节点中呢?其实这其中的奥秘就在kafka的Metadata中。这一篇我们就来看看kafka中的Metadata管理。我们来看看构建Kakfa中的代码片段:KafkaProducer构造...
    继续阅读 »

    我们往kafka集群中发送数据的时候,kafka是怎么感知到需要发送到哪一台节点中呢?其实这其中的奥秘就在kafka的Metadata中。这一篇我们就来看看kafka中的Metadata管理。

    我们来看看构建Kakfa中的代码片段:


    KafkaProducer构造函数代码片段

    从上面的代码片段可以看出,如果metadata变量不为空,直接赋值给KafkaProducer类成员变量metadata,否则需要新构建一个ProducerMetadata对象,然后根据用户传递的kafka集群服务器地址信息,构建Metadata类中cache成员变量的值,类型为MetadataCache。

    下面我们来分析一下Metadata这个类,看看里面都封装了哪些属性。

    refreshBackoffMs

    这个参数的作用是防止轮询的过于频繁。用于设置两次元数据刷新之间,最小有效时间间隔,超过这个设置的时间间隔,则这次元数据刷新就失效了。默认值是100ms。

    metadataExpireMs

    这个参数的含义是如果不刷新,元数据可以保持有效的最大时间。默认值是5分钟。

    updateVersion

    这个参数对应每一个元数据的响应。每一次自增+1。

    requestVersion

    这个参数对应每一次创建一个新的Topic。每一次自增+1。

    lastRefreshMs

    这个参数的含义是上一次更新元数据的时间。

    lastSuccessfulRefreshMs

    这个参数的含义是上一次成功更新元数据的时间。正常情况下每一次更新元数据都应该是成功的,那么lastRefreshMs和lastSuccessfulRefreshMs的值,应该是一样的。但是如果出现更新没有成功的情况,那么lastRefreshMs的值大于lastSuccessfulRefreshMs的值。

    fatalException

    这个参数的类型是kafka自己封装的KafkaException。继承了RuntimeException。如果在元数据相关的操作中抛出了这种异常,kafka将停止元数据相关的操作。

    invalidTopics

    这个参数的含义是存储非法的Topic元数据信息。

    unauthorizedTopics

    这个参数的含义是存储未授权的Topic元数据信息。

    cache

    这个参数的含义是在Metadata类的内部构建一个MetadataCache对象,把元数据信息缓存起来,方便在集群中进行快速的数据获取。

    needFullUpdate

    这个参数的含义是Metadata是否需要全部更新。

    needPartialUpdate

    这个参数的含义是Metadata是否需要部分更新。

    clusterResourceListeners

    这个参数的含义是抽象了一个接收元数据更新集群资源的监听器集合。

    lastSeenLeaderEpochs

    这个参数是一个Map结构,映射的是TopicPartition和Integer之间的关系。也就是说某一个主题分区,它的主分区上一次更新的版本号是多少,在这个Map结构中存储。真正构建Metadata对象的时候,实现类是HashMap。

    接下来我们来看看MetadataCache这个类,看看里面封装了哪些属性。这个类存在是kafka一种缓存的思想,把一些重要的属性用缓存来保存起来,提高Metadata的读取效率。

    clusterId

    这个参数用来标识整个kafka集群。

    nodes

    这个参数是一个Map类型,用来映射kafka集群中节点编号和节点的关系。

    unauthorizedTopics

    这个参数是一个Set类型,用来存储未授权的Topic集合。

    invalidTopics

    这个参数是一个Set类型,用来存储无效的Topic集合。

    internalTopics

    这个参数是一个Set类型,用来存储kafka内部的Topic集合,例如__consumer_offsets。

    controller

    这个参数是表示kafka controller所在broker。

    metadataByPartition

    这个参数是Map类型,用来存储分区和分区对应的元数据的映射关系。

    clusterInstance

    这个参数抽象了集群中的数据,我们接下来进行重点分析。

    Cluster类是封装在MetadataCache中的,用来表示kafka的集群信息。

    nodes

    这个参数封装了集群中节点信息列表。

    unauthorizedTopics

    这个参数是一个Set类型,用来存储未授权的Topic集合。

    invalidTopics

    这个参数是一个Set类型,用来存储无效的Topic集合。

    internalTopics

    这个参数是一个Set类型,用来存储kafka内部的Topic集合,例如__consumer_offsets。

    partitionsByTopicPartition

    这个参数记录了TopicPartition与PartitionInfo的映射关系。

    partitionsByTopic

    这个参数记录了Topic名称与PartitionInfo的映射关系。可以按照Topic名称查询其中全部分区的详细信息。

    availablePartitionsByTopic

    这个参数记录了Topic与PartitionInfo的映射关系。这里的List<PartitionInfo>中存放的分区必须是有Leader副本的Partition,而partitionsByTopic中记录的分区则不一定有Leader副本,因为某些中间状态,例如Leader副本所在节点,发生了节点下线,进而触发了Leader副本的选举,在这一时刻分区不一定有Leader副本。

    partitionsByNode

    这个参数记录了Node与PartitionInfo的映射关系。可以按照节点Id查询该节点上分布的全部分区的详细信息。

    nodesById

    这个参数记录了BrokerId与Node节点之间的映射关系。方便使用BrokerId进行索引,可以根据BrokerId得到关联的Node节点信息。

    clusterResource

    这个参数是ClusterResource类型,这个类只是封装了一个clusterId成员属性,用于区分每一个kafka的集群。

    我们再来看看Node这个类。Node这个类是对kafka集群中一个物理服务器的抽象,它所拥有的属性如下所示。

    id

    这个参数记录了kafka集群中的服务器编号,是我们配置参数的时候指定的。

    host

    这个参数记录了服务器的主机名。

    port

    这个参数记录了服务器的端口号。

    rack

    这个参数记录了服务器所属的机架。

    我们再来看看TopicPartition这个类。这个类里面封装了主题,以及对应的一个分区。它所拥有的属性如下所示:

    partition

    这个参数记录了一个分区编号。

    topic

    这个参数记录了主题名称。

    我们再来看看PartitionInfo这个类。这个类抽象了一个分区的详细信息,它所拥有的属性如下所示:

    topic

    这个参数记录了主题名称,表示这个分区是属于哪一个主题的。

    partition

    这个参数记录了分区编号。

    leader

    这个参数记录了分区主副本在哪台服务器上。

    replicas

    这个参数是Node类型的数组,记录了这个分区所有副本所在服务器。

    inSyncReplicas

    这个参数是Node类型的数组,记录了这个分区同步正常的副本所在服务器。

    offlineReplicas

    这个参数是Node类型的数组,记录了这个分区同步不正常的副本所在服务器。

    本文转载自:  https://www.jianshu.com/p/61a58cba354f

    收起阅读 »

    QQ被盗,发给暗恋女生的第一条消息竟是h图

    昨日凌晨,“QQ 盗.号”这一词条登上微博热搜,直至第二天中午都还在热搜榜上待着。看到这条热搜后,小编火速登录 QQ 查看自己是否也被盗号,所幸没有遭殃。但是我的室友就没这么幸运了,暗恋了三年的妹子,军训时候加别人QQ,现在快毕业了还不敢跟别人说一句话,这下好...
    继续阅读 »

    昨日凌晨,“QQ 盗.号”这一词条登上微博热搜,直至第二天中午都还在热搜榜上待着。


    看到这条热搜后,小编火速登录 QQ 查看自己是否也被盗号,所幸没有遭殃。但是我的室友就没这么幸运了,暗恋了三年的妹子,军训时候加别人QQ,现在快毕业了还不敢跟别人说一句话,这下好了,QQ被.盗,别人给他暗恋对象发H图,还没办法撤回,不是我们拦着真要出人命了。他说这个事件对他影响太大,盗贼不但盗取了他的账号,还玷污了他的爱情。

    我们也发现沉默多年的QQ列表里的许多群组突然“活跃”起来。点进去一看,有些动作快的群主已撤回成员消息,但也有些群还未及时处理:好几个群成员在凌晨时候突然发黄图,甚至还发在了群成员 599 人、其中还有十几位老师的大学学院群里……借用网友的一句话:我这替人尴尬的老毛病又犯了。


    1.大型社死现场

    既然这次 QQ 盗号事件能登上热搜且热度居高不下,说明波及范围不小,从众多网友对此的反馈上也证明了这一点——一整个就是大型社死现场

    • “救命,我们有人在先进预备党员的群里发黄图,结果直接被踢出群了,取消评党员的资格。”

    • “我两个朋友被盗,说就是在电脑上面登了一下 QQ 而已,然后我朋友的同学他们也被盗,发了一些 yellow 图带网址那种。”

    • “我真的会谢,半夜四点群发淫.秽图片,往我朋友同事长辈甚至工作群发,登录保护保了个寂寞,名营损失费怎么说?腾讯你欠我的拿什么还!”

    • “主要是这个范围和受害人群不确定,我有好多群都有人被盗号了,也不知道怎么做到的,针对什么群体的。”

    事发过后,很快就有网友进行提醒,为了以防万一不要查看那些淫.秽图片上的网址

    • “被盗的 QQ 号会发送附着链接的淫.秽图片消息,当受害者将所附链接用浏览器打开的时候,犯罪分子的电脑可能启动脚本进一步盗取受害者信息。如果不幸中招了,大家要谨慎处理。”

    • “千万不要在 QQ 登陆的情况下点击陌生的链接,也不要点开 QQ 邮箱里陌生的邮件。”

    与此同时,发现被盗.号的人也在第一时间忙着找回 QQ 并修改密码,希望尽快出面解释一下这尴尬的场景、挽回一下即将崩塌的人设。但许多人无奈地发现:我的号被封了,没法证明我的清白了啊!




    2.腾讯回应:“系用户扫描过不法分子伪造的游戏登录二维码”

    结合众多网友并周围人的反馈来看,本次 QQ 盗.号的波及范围显然较大,许多人在冷静之后也开始猜测其背后原因。虽然各人看法不一,但网传的主要有三种:

    • 腾讯内部协议被偷

    这一说法主要是有网友曝光了一张聊天内容的截图,其中讲到腾讯内部协议被偷导致可随机生成 key,无需知道密码即可盗号。


    • 与学习通泄露数据有关

    考虑到这次 QQ 盗.号事件与上周学习通被曝数据泄露的时间较为接近,有部分网友怀疑这两起事件可能有关,即黑客通过学习通撞库以盗.取 QQ 号。

    • 误点了不安全链接或误扫了二维码等

    还有一种最常见的方式,即用户误点了不安全链接或误扫了二维码等,导致授权了 QQ 登入信息。

    面对网络上逐渐发酵的负面言论和用户投诉,腾讯 QQ 官方在昨日中午针对这起事件给出了调查结果并对用户致歉:“系用户扫描过不法分子伪造的游戏登录二维码并授权登录,该登录行为被黑.产团伙劫.持并记录,随后被不法分子利用发送不良图片广告。”


    此外,也有记者以用户身份向学习通客服询问本次事件,而学习通方面否认 QQ 盗.号与其有关:尚未发现明确的用户信息泄露证据,已经报案,公.安机关已介入调查。

    那么,你是否在本次 QQ 盗.号的风波中遭殃,或是目睹了其他人的“社死”现场?

    参考链接:

    收起阅读 »

    一定要优雅,高端前端程序员都应该具备的基本素养

    近来看到很多公司裁员,忽然惊醒,之前是站在项目角度考虑问题,却没站在咱们程序员本身看待问题,险些酿成大错,如果人人都能做到把项目维护得井井有条,无论什么人都能看明白都能快速接手,那咱们的竞争力在哪里呢?这个时候我再看项目中那些被我天天骂的代码,顿时心中就无限景...
    继续阅读 »

    近来看到很多公司裁员,忽然惊醒,之前是站在项目角度考虑问题,却没站在咱们程序员本身看待问题,险些酿成大错,如果人人都能做到把项目维护得井井有条,无论什么人都能看明白都能快速接手,那咱们的竞争力在哪里呢?这个时候我再看项目中那些被我天天骂的代码,顿时心中就无限景仰起来,原来屎山才是真能能够保护我们的东西,哪有什么岁月静好,只是有人替你负屎前行罢了


    为了能让更多人认识到这一点,站在前端的角度上,我在仔细拜读了项目中的那些暗藏玄机的代码后,决定写下此文,由于本人功力尚浅,且之前一直走在错误的道路上,所以本文在真正的高手看来可能有些班门弄斧,在此献丑了🐶



    用 TypeScript,但不完全用


    TypeScript大行其道,在每个团队中,总有那么些个宵小之辈想尽一切办法在项目里引入 ts,这种行为严重阻碍了屎山的成长速度,但同是打工人我们也不好阻止,不过就算如此,也无法阻止我们行使正义


    众所周知,TypeScript 别名 AnyScript,很显然,这就是TypeScript创始人Anders Hejlsberg给我们留下的暗示,我们有理由相信AnyScript 才是他真正的目的

    const list: any = []
    const obj: any = {}
    const a: any = 1

    引入了 ts的项目,由于是在原可运行代码的基础上额外添加了类型注释,所以代码体积毫无疑问会增大,有调查显示,可能会增加 30%的代码量,如果充分发挥 AnyScript 的宗旨,意味着你很轻松地就让代码增加了 30% 毫无用处但也挑不出啥毛病的代码,这些代码甚至还会增加项目的编译时间(毕竟增加了ts校验和移除的成本嘛)


    你不仅能让自己写的代码用上 AnyScript,甚至还可以给那些支持 ts 的第三方框架/库一个大嘴巴子

    export default defineComponent({
    props: {
    // 现在 data 是 any 类型的啦
    data: {
    type: Number as PropType<any>,
    },
    },
    setup(_, { emit }) {
    // 现在 props 是 any 类型的啦
    const props: any = _
    ...
    }
    })

    当然了,全屏 any可能还是有点明显了,所以你可以适当地给部分变量加上具体类型,但是加上类型不意味着必须要正确使用

    const obj: number[] = []
    // ...
    // 虽然 obj 是个 number[],但为了实现业务,就得塞入一些不是 number 的类型,我也不想的啊是不是
    // 至于编辑器会划红线报错?那是小问题,不用管它,别人一打开这个项目就是满屏的红线,想想就激动
    obj.push('2')
    obj.push([3])

    命名应该更自由

    命名一直是个困扰很多程序员的问题,究其原因,我们总想给变量找个能够很好表达意思的名称,这样一来代码的可阅读性就高了,但现在我们知道,这并不是件好事,所以我们应该放纵自我,既摆脱了命名困难症,又加速了屎山的堆积进度

    const a1 = {}
    const a2 = {}
    const a3 = 2
    const p = 1

    我必须强调一点,命名不仅是变量命名,还包含文件名、类名、组件名等,这些都是我们可以发挥的地方,例如类名

    <div class="box">
    <div class="box1"></div>
    <div class="box2"></div>
    <div>
    <div class="box3"></div>

    乍一看似乎没啥毛病,要说有毛病似乎也不值当单独挑出来说,没错,要的就是这个效果,让人单看一段代码不好说什么,但是如果积少成多,整个项目都是 box呢?全局搜索都给你废了!如果你某些组件再一不小心没用 scoped 呢?稍不留意就不知道把什么组件的样式给改了,想想就美得很


    关于 css我还想多说一点,鉴于其灵活性,我们还可以做得更多,总有人说什么 BEMBEM的,他们敢用我们就敢写这样的代码

    &-card {
    &-btn {
    &_link {
    &--right {
    }
    }
    &-nodata {
    &_link {
    &--replay {
    &--create {}
    }
    }
    }
    }
    &-desc {}
    }

    好了,现在请在几百行(关于这一点下一节会说到)这种格式的代码里找出类名 .xxx__item_current.mod-xxx__link 对应的样式吧


    代码一定要长


    屎山一定是够高够深的,这就要求我们的代码应该是够长够多的


    大到一个文件的长度,小到一个类、一个函数,甚至是一个 if 的条件体,都是我们自由发挥的好地方。


    什么单文件最好不超过 400行,什么一个函数不超过 100行,简直就是毒瘤,


    1.jpg


    所以这就要求我们要具备将十行代码就能解决的事情写成一百行的能力,最好能给人一种多即是少的感觉

    data === 1
    ? 'img'
    : data === 2
    ? 'video'
    : data === 3
    ? 'text'
    : data === 4
    ? 'picture'
    : data === 5
    ? 'miniApp'

    三元表达式可以优雅地表达逻辑,像诗一样,虽然这段代码看起来比较多,但逻辑就是这么多,我还专门用了三元表达式优化,不能怪我是不是?什么map映射枚举优化听都没听过

    你也可以选择其他一些比较容易实现的思路,例如,多写一些废话

    if (a > 10) {
    // 虽然下面几个 if 中对于 a 的判断毫无用处,但不仔细看谁能看出来呢?看出来了也不好说什么,毕竟也没啥错
    // 除此之外,多级 if 嵌套也是堆屎山的一个小技巧,什么提前 return 不是太明白
    if (a > 5) {
    if (a > 3 && b) {

    }
    }
    if (a > 4) {

    }
    }

    除此之外,你还可以写一些中规中矩的方法,但重点在于这些方法根本就没用到,这种发挥的地方就更多了,简直就是扩充代码体积的利器,毕竟单看这些方法没啥毛病,但谁能想到根本就用不到呢?就算有人怀疑了,但你猜他敢随便从运行得好好的业务项目里删掉一些没啥错的代码吗?


    组件、方法多多滴耦合


    为了避免其他人复用我的方法或组件,那么在写方法或组件的时候,一定要尽可能耦合,提升复用的门槛


    例如明明可以通过 Props传参解决的事情,我偏要从全局状态里取,例如vuex,独一份的全局数据,想传参就得改 store数据,但你猜你改的时候会不会影响到其他某个页面某个组件的正常使用呢?如果你用了,那你就可能导致意料之外的问题,如果你不用你就得自己重写一个组件


    组件不需要传参?没关系,我直接把组件的内部变量给挂到全局状态上去,虽然这些内部变量确实只有某一个组件在用,但我挂到全局状态也没啥错啊是不是


    嘿,明明一个组件就能解决的事情,现在有了倆,后面还可能有仨,这代码量不就上来了吗?


    方法也是如此,明明可以抽取参数,遵循函数式编程理念,我偏要跟外部变量产生关联

    // 首先这个命名就很契合上面说的自由命名法
    function fn1() {
    // ...
    // fn1 的逻辑比较长,且解决的是通用问题,
    // 但 myObj 偏偏是一个外部变量,这下看你怎么复用
    window.myObj.name = 'otherName'
    window.myObj.children.push({ id: window.myObj.children.length })
    // ...
    }

    魔术字符串是个好东西

    实际上,据我观察,排除掉某些居心不轨的人之外,大部分人还是比较喜欢写魔术字符串的,这让我很欣慰,看着满屏的不知道从哪里冒出来也不知道代表着什么的硬编码字符串,让人很有安全感

    if (a === 'prepare') {
    const data = localStorage.getItem('HOME-show_guide')
    // ...
    } else if (a === 'head' && b === 'repeating-error') {
    switch(c) {
    case 'pic':
    // ...
    break
    case 'inDrawer':
    // ...
    break
    }
    }

    基于此,我们还可以做得更多,比如用变量拼接魔术字符串,debug的时候直接废掉全局搜索

    if (a === query.name + '_head') {

    }

    大家都是中国人,为什么不试试汉字呢?

    if (data === '正常') {

    } else if (data === '错误') {

    } else if (data === '通过') {

    }

    轮子就得自己造才舒心


    众所周知,造轮子可以显著提升我们程序员的技术水平,另外由于轮子我们已经自己造了,所以减少了对社区的依赖,同时又增加了项目体积,有力地推动了屎山的成长进程,可以说是一鱼两吃了


    例如我们可能经常在项目中使用到时间格式化的方法,一般人都是直接引入 dayjs完事,太肤浅了,我们应该自己实现,例如,将字符串格式日期格式化为时间戳

    function format(str1: any, str2: any) {
    const num1 = new Date(str1).getTime()
    const num2 = new Date(str2).getTime()
    return (num2 - num1) / 1000
    }

    多么精简多么优雅,至于你说的什么格式校验什么 safari下日期字符串的特殊处理,等遇到了再说嘛,就算是dayjs不也是经过了多次 fixbug才走到今天的嘛,多一些宽松和耐心好不好啦


    如果你觉得仅仅是 dayjs这种小打小闹难以让你充分发挥,你甚至可以造个 vuexvue官网上写明了eventBus可以充当全局状态管理的,所以我们完全可以自己来嘛,这里就不举例了,这是自由发挥的地方,就不局限大家的思路了


    借助社区的力量-轮子还是别人的好


    考虑到大家都只是混口饭吃而已,凡事都造轮子未免有些强人所难,所以我们可以尝试走向另外一个极端——凡事都用轮子解决


    判断某个变量是字符串还是对象,kind-of拿来吧你;获取某个对象的 keyobject-keys拿来吧你;获取屏幕尺寸,vue-screen-size拿来吧你……等等,就不一一列举了,需要大家自己去发现


    先甭管实际场景是不是真的需要这些库,也甭管是不是杀鸡用牛刀,要是大家听都没听过的轮子那就更好了,这样才能彰显你的见多识广,总之能解决问题的轮子就是好问题,


    在此我得特别提点一下 lodash,这可是解决很多问题的利器,但是别下载错了,得是 commonjs版本的那个,量大管饱还正宗,es module版本是不行滴,太小家子气


    import _ from 'lodash'

    多尝试不同的方式来解决相同的问题


    世界上的路有很多,很多路都能通往同一个目的地,但大多数人庸庸碌碌,只知道沿着前人的脚步,没有自己的思想,别人说啥就是啥,这种行为对于我们程序员这种高端的职业来说,坏处很大,任何一个有远大理想的程序员都应该避免


    落到实际上来,就是尝试使用不同的技术和方案解决相同的问题

    搞个css模块化方案,什么BEMOOCSSCSS ModulesCSS-in-JS 都在项目里引入,紧跟潮流扩展视野

    vue项目只用 template?逊啦你,render渲染搞起来

    之前看过什么前端依赖注入什么反射的文章,虽然对于绝大多数业务项目而言都是水土不服,但问题不大,能跑起来就行,引入引入

    还有那什么 rxjs,人家都说好,虽然我也不知道好在哪里,但胜在门槛高一般人搞不清楚所以得试试

    Pinia 是个好东西,什么,我们项目里已经有 vuex了?out啦,人家官网说了 vue2也可以用,我们一定要试试,紧跟社区潮流嘛,一个项目里有两套状态管理有什么值得大惊小怪的!


    做好自己,莫管他人闲事

    看过一个小故事,有人问一个年纪很大的老爷爷的长寿秘诀是什么,老爷爷说是从来不管闲事

    这个故事对我们程序员来说也很有启发,写好你自己的代码,不要去关心别人能不能看得懂,不要去关心别人是不是会掉进你写的坑里

    mounted() {
    setTimeout(() => {
    const width = this.$refs.box.offsetWidth
    const itemWidth = 50
    // ...
    }, 200)
    }

    例如对于上述代码,为什么要在 mounted里写个 setTimeout呢?为什么这个 setTimeout的时间是 200呢?可能是因为 box 这个元素大概会在 mounted之后的 200ms左右接口返回数据就有内容了,就可以测量其宽度进行其他一系列的逻辑了,至于有没有可能因为网络等原因超过 200ms还是没有内容呢?这些不需要关心,你只要保证在你开发的时候 200ms这个时间是没问题的就行了;
    itemWidth代表另外一个元素的宽度,在你写代码的时候,这个元素宽度就是 50,所以没必要即时测量,你直接写死了,至于后面其他人会不会改变这个元素的宽度导致你这里不准了,这就不是你要考虑的事情了,你开发的时候确实没问题,其他人搞出来问题其他人负责就行,管你啥事呢?


    代码自解释


    高端的程序员,往往采用最朴素的编码方式,高手从来不写注释,因为他们写的代码都是自解释的,什么叫自解释?就是你看代码就跟看注释一样,所以不需要注释


    我觉得很有道理,代码都在那里搁着了,逻辑写得清清楚楚,为啥还要写注释呢,直接看代码不就行了吗?


    乍一看,似乎这一条有点阻碍堆屎山的进程,实则不然


    一堆注定要被迭代无数版、被无数人修改、传承多年的代码,其必定是逻辑错综复杂,难免存在一些不可名状的让人说不清道不明的逻辑,没有注释的加成,这些逻辑大概率要永远成为黑洞了,所有人看到都得绕着走,相当于是围绕着这些黑洞额外搭起了一套逻辑,这代码体积和复杂度不就上来了吗?


    如果你实在手痒,倒也可以写点注释,我这里透露一个既能让你写写注释过过瘾又能为堆屎山加一把力的方法,那就是:在注释里撒谎!


    没错,谁说注释只能写对的?我理解不够,所以注释写得不太对有什么奇怪的吗?我又没保证注释一定是对的,也没逼着你看注释,所以你看注释结果被注释误导写了个bug,这凭啥怪我啊

    // 计算 data 是否可用
    //(实际上,这个方法的作用是计算 data 是否 不可用)
    function isDisabledData(data: any) {
    // ...
    }

    上述这个例子只能说是小试牛刀,毕竟多调试一下很容易被发现的,但就算被发现了,大家也只会觉得你只是个小粗心鬼罢了,怎么好责怪你呢,这也算是给其他人的一个小惊喜了,况且,万一真有人不管不顾就信了,那你就赚大了


    编译问题坚决不改


    为了阻碍屎山的成长速度,有些阴险的家伙总想在各种层面上加以限制,例如加各种lint,在编译的时候,命令行中就会告诉你你哪些地方没有按照规则来,但大部分是 waring 级别的,即你不改项目也能正常运行,这就是我们的突破点了。


    尽管按照你的想法去写代码,lint的事情不要去管,waring报错就当没看到,又不是不能用?在这种情况下,如果有人不小心弄了个 error级别的错误,他面对的就是从好几屏的 warning 中找他的那个 error 的场景了,这就相当于是提前跟屎山来了一次面对面的拥抱


    根据破窗理论,这种行为将会影响到越来越多的人,大家都将心照不宣地视 warning于无物(从好几屏的 warning中找到自己的那个实在是太麻烦了),所谓的 lint就成了笑话


    小结


    一座历久弥香的屎山,必定是需要经过时间的沉淀和无数人的操练才能最终成型,这需要我们所有人的努力,多年之后,当你看到你曾经参与堆砌的屎山中道崩殂轰然倒塌的时候,你就算是真的领悟了我们程序员所掌控的恐怖实力!🐶


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

    Android实现消息总线的几种方式,你都会吗?

    Android中消息总线的几种实现方式前言消息总线又叫事件总线,为什么我们需要一个消息总线呢?是因为随着项目变大,页面变多,我们可能出现跨页面、跨组件、跨线程、跨进程传递消息与数据,为了更方便的直接通知到指定的页面实现具体的逻辑,我们需要消息总线来实现。从最基...
    继续阅读 »

    Android中消息总线的几种实现方式

    前言

    消息总线又叫事件总线,为什么我们需要一个消息总线呢?是因为随着项目变大,页面变多,我们可能出现跨页面、跨组件、跨线程、跨进程传递消息与数据,为了更方便的直接通知到指定的页面实现具体的逻辑,我们需要消息总线来实现。

    从最基本的 BroadcastReceiver 到 EventBus 再到RxBus ,后来官方出了AndroidX jetpack 我们开始使用LiveDataBus,最后到Kotlin的流行出来了FlowBus。我们看看他们是怎么一步一步演变的。

    一、BroadcastReceiver 广播

    我们再初入 Android 的时候都应该学过广播接收者,分为静态广播和动态注册广播,在高版本的 Android 中限制了我们一些静态广播的使用,不过我们还是能通过动态注册的方式获取一些系统的状态改变。像常用的电量变化、网络状态变化、短信发送接收的状态等等。

    比如网络变化的监听:

        IntentFilter intentFilter = new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION);
    application.getApplicationContext().registerReceiver(InstanceHolder.INSTANCE, intentFilter);

    在消息中线中,我们可以使用本地广播来实现 LocalBroadcastManager 消息的通知。

        LocalBroadcastManager mLocalBroadcastManager = LocalBroadcastManager.getInstance(mContext);

    BroadcastReceiver mLoginReceiver = new LoginSuccessReceiver();
    mLocalBroadcastManager.registerReceiver(mLoginReceiver, new IntentFilter(Constants.ACTION_LOGIN_SUCCESS));

    private class LoginSuccessReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
    //刷新Home界面
    refreshHomePage();

    //刷新未读信息
    requestUnreadNum();
    }
    }

    //记得要解绑对应的接收器
    mLocalBroadcastManager.unregisterReceiver(mLoginReceiver);

    这样就可以实现一个消息通知了。相比 EventBus 它的性能和空间的消耗都是较大的,并且只能固定在主线程运行。

    二、EventBus

    EventBus最大的特点就是简洁、解耦,可以直接传递我们自定义的消息Message。EventBus简化了应用程序内各组件间、组件与后台线程间的通信。记得2015年左右是非常火爆的。

    EventBus的调度灵活,不依赖于 Context,使用时无需像广播一样关注 Context 的注入与传递。可继承、优先级、粘滞,是 EventBus 比之于广播的优势。几乎可以满足我们全部的需求。

    最初的EventBus其实就是一个方法的集合与查找,核心是通过register方法把带有@Subscrib注解的方法和参数之类的东西全部放入一个List集合,然后通过post方法去这个list循环查找到符合条件的方法去执行。

    如何使用EventBus,一共分5步:

      @Override
    protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_event_bus);

    EventBus.getDefault().register(MainActivity.this); //1.注册广播
    }
      @Override
    protected void onDestroy() {
    super.onDestroy();
    EventBus.getDefault().unregister(MainActivity.this); //2.解注册广播
    }
    /**
    * 3.传递什么类型的。定义一个消息类
    */
    public class MessageEvent {
    public String name;

    public MessageEvent(String name) {
    this.name = name;
    }
    }
        @OnClick({R.id.bt_eventbus_send_main, R.id.bt_eventbus_send_sticky})
    public void onClick(View view) {
    switch (view.getId()) {
    case R.id.bt_eventbus_send_main:
    //4.发送消息
    EventBus.getDefault().post(new MessageEvent("我是主页面发送过来的消息"));
    finish();
    break;
    }
    }
       /**
    * 5.接受到消息。需要注解
    *
    * @param event
    */
    @Subscribe(threadMode = ThreadMode.MAIN) //主线程执行
    public void MessageEventBus(MessageEvent event) {
    //5。显示接受到的消息
    mTvEventbusResult.setText(event.name);
    }

    EventBus的性能开销其实不大,EventBus2.4.0 版是利用反射来实现的,后来改成 APT 实现之后会好很多。主要问题是需要定义很多的消息对象,消息太多之后就感觉管理起来很麻烦。当消息太多之后容器内部的查找会出现性能瓶颈。

    就算如此 EventBus 也是值得大家使用的。

    三、RxBus

    RxBus是基于RxJava实现的,强大是强大,但是学习成本比较高,需要额外导入RxJava RxAndroid等库,这些库体积还是较大的。可以实现异步的消息等。

    本身的实现是很简单的:

    public class RxBus {
    private volatile static RxBus mDefaultInstance;
    private final Subject<Object> mBus;

    private RxBus() {
    mBus = PublishSubject.create().toSerialized();
    }

    public static RxBus getInstance() {
    if (mDefaultInstance == null) {
    synchronized (RxBus.class) {
    if (mDefaultInstance == null) {
    mDefaultInstance = new RxBus();
    }
    }
    }
    return mDefaultInstance;
    }

    /**
    * 发送事件
    */
    public void post(Object event) {
    mBus.onNext(event);
    }

    /**
    * 根据传递的 eventType 类型返回特定类型(eventType)的 被观察者
    */
    public <T> Observable<T> toObservable(final Class<T> eventType) {
    return mBus.ofType(eventType);
    }

    /**
    * 判断是否有订阅者
    */
    public boolean hasObservers() {
    return mBus.hasObservers();
    }

    public void reset() {
    mDefaultInstance = null;
    }

    }

    定义消息对象:

    public class MsgEvent {
    private String msg;

    public MsgEvent(String msg) {
    this.msg = msg;
    }

    public String getMsg() {
    return msg;
    }

    public void setMsg(String msg) {
    this.msg = msg;
    }
    }

    发送与接收:

    RxBus.getInstance().toObservable(MsgEvent.class).subscribe(new Observer<MsgEvent>() {
    @Override
    public void onSubscribe(Disposable d) {

    }

    @Override
    public void onNext(MsgEvent msgEvent) {
    //处理事件
    }

    @Override
    public void onError(Throwable e) {

    }

    @Override
    public void onComplete() {

    }
    });


    RxBus.getInstance().post(new MsgEvent("Java"));

    缺点是容易内存泄露,我们需要使用rxlifecycle 或者使用CompositeDisposable 自己对生命周期进行处理解绑。

    四、LiveDataBus

    官方出了AndroidX jetpack 内部包含LiveData,它可以感知并遵循Activity、Fragment或Service等组件的生命周期。

    为什么要使用LiveDataBus,正是基于LiveData对组件生命周期可感知的特点,因此可以做到仅在组件处于生命周期的激活状态时才更新UI数据。

    一个简单的LiveDataBus的实现:

    public final class LiveDataBus {

    private final Map<String, BusMutableLiveData<Object>> bus;

    private LiveDataBus() {
    bus = new HashMap<>();
    }

    private static class SingletonHolder {
    private static final LiveDataBus DEFAULT_BUS = new LiveDataBus();
    }

    public static LiveDataBus get() {
    return SingletonHolder.DEFAULT_BUS;
    }

    public <T> MutableLiveData<T> with(String key, Class<T> type) {
    if (!bus.containsKey(key)) {
    bus.put(key, new BusMutableLiveData<>());
    }
    return (MutableLiveData<T>) bus.get(key);
    }

    public MutableLiveData<Object> with(String key) {
    return with(key, Object.class);
    }

    private static class ObserverWrapper<T> implements Observer<T> {

    private Observer<T> observer;

    public ObserverWrapper(Observer<T> observer) {
    this.observer = observer;
    }

    @Override
    public void onChanged(@Nullable T t) {
    if (observer != null) {
    if (isCallOnObserve()) {
    return;
    }
    observer.onChanged(t);
    }
    }

    private boolean isCallOnObserve() {
    StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
    if (stackTrace != null && stackTrace.length > 0) {
    for (StackTraceElement element : stackTrace) {
    if ("android.arch.lifecycle.LiveData".equals(element.getClassName()) &&
    "observeForever".equals(element.getMethodName())) {
    return true;
    }
    }
    }
    return false;
    }
    }

    private static class BusMutableLiveData<T> extends MutableLiveData<T> {

    private Map<Observer, Observer> observerMap = new HashMap<>();

    @Override
    public void observe(@NonNull LifecycleOwner owner, @NonNull Observer<T> observer) {
    super.observe(owner, observer);
    try {
    hook(observer);
    } catch (Exception e) {
    e.printStackTrace();
    }
    }

    @Override
    public void observeForever(@NonNull Observer<T> observer) {
    if (!observerMap.containsKey(observer)) {
    observerMap.put(observer, new ObserverWrapper(observer));
    }
    super.observeForever(observerMap.get(observer));
    }

    @Override
    public void removeObserver(@NonNull Observer<T> observer) {
    Observer realObserver = null;
    if (observerMap.containsKey(observer)) {
    realObserver = observerMap.remove(observer);
    } else {
    realObserver = observer;
    }
    super.removeObserver(realObserver);
    }

    private void hook(@NonNull Observer<T> observer) throws Exception {
    //get wrapper's version
    Class<LiveData> classLiveData = LiveData.class;
    Field fieldObservers = classLiveData.getDeclaredField("mObservers");
    fieldObservers.setAccessible(true);
    Object objectObservers = fieldObservers.get(this);
    Class<?> classObservers = objectObservers.getClass();
    Method methodGet = classObservers.getDeclaredMethod("get", Object.class);
    methodGet.setAccessible(true);
    Object objectWrapperEntry = methodGet.invoke(objectObservers, observer);
    Object objectWrapper = null;
    if (objectWrapperEntry instanceof Map.Entry) {
    objectWrapper = ((Map.Entry) objectWrapperEntry).getValue();
    }
    if (objectWrapper == null) {
    throw new NullPointerException("Wrapper can not be bull!");
    }
    Class<?> classObserverWrapper = objectWrapper.getClass().getSuperclass();
    Field fieldLastVersion = classObserverWrapper.getDeclaredField("mLastVersion");
    fieldLastVersion.setAccessible(true);
    //get livedata's version
    Field fieldVersion = classLiveData.getDeclaredField("mVersion");
    fieldVersion.setAccessible(true);
    Object objectVersion = fieldVersion.get(this);
    //set wrapper's version
    fieldLastVersion.set(objectWrapper, objectVersion);
    }
    }
    }

    注册与发送:

    LiveDataBus.get()
    .with("key_test", String.class)
    .observe(this, new Observer<String>() {
    @Override
    public void onChanged(@Nullable String s) {
    }
    });

    LiveDataBus.get().with("key_test").setValue(s);

    LiveDataBus已经算是很好用的,自动注册解绑,根据Key传递泛型T对象,容易查找对应的接收者,也可以实现可见的触发和直接触发,可以实现跨进程,

    LiveData有几点不足,只能在主线程更新数据,操作符无法转换数据,基于 Android Api 实现的,换一个平台无法适应,基于这几点又开发出了FlowBus。

    五、FlowBus

    很多人都说Flow 的出现导致 LiveData 没那么重要了,就是因为 LiveData 的场景 都可以使用 Flow 平替了,还能更为的强大和灵活。

    StateFlow 可以 替代ViewModel中传递数据,SharedFlow 可以实现事件总线。(这两者的异同如果大家有兴趣,我可以单独开一篇讲下)。

    SharedFlow 就是一种热流,可以实现一对多的关系,其构造方法支持天然支持普通的消息发送与粘性的消息发送。一般我们FlowBus都是基于 SharedFlow 来实现:

    object FlowBus {
    private val busMap = mutableMapOf<String, EventBus<*>>()
    private val busStickMap = mutableMapOf<String, StickEventBus<*>>()

    @Synchronized
    fun <T> with(key: String): EventBus<T> {
    var eventBus = busMap[key]
    if (eventBus == null) {
    eventBus = EventBus<T>(key)
    busMap[key] = eventBus
    }
    return eventBus as EventBus<T>
    }

    @Synchronized
    fun <T> withStick(key: String): StickEventBus<T> {
    var eventBus = busStickMap[key]
    if (eventBus == null) {
    eventBus = StickEventBus<T>(key)
    busStickMap[key] = eventBus
    }
    return eventBus as StickEventBus<T>
    }

    //真正实现类
    open class EventBus<T>(private val key: String) : LifecycleObserver {

    //私有对象用于发送消息
    private val _events: MutableSharedFlow<T> by lazy {
    obtainEvent()
    }

    //暴露的公有对象用于接收消息
    val events = _events.asSharedFlow()

    open fun obtainEvent(): MutableSharedFlow<T> = MutableSharedFlow(0, 1, BufferOverflow.DROP_OLDEST)

    //主线程接收数据
    fun register(lifecycleOwner: LifecycleOwner, action: (t: T) -> Unit) {
    lifecycleOwner.lifecycle.addObserver(this)
    lifecycleOwner.lifecycleScope.launch {
    events.collect {
    try {
    action(it)
    } catch (e: Exception) {
    e.printStackTrace()
    YYLogUtils.e("FlowBus - Error:$e")
    }
    }
    }
    }

    //协程中发送数据
    suspend fun post(event: T) {
    _events.emit(event)
    }

    //主线程发送数据
    fun post(scope: CoroutineScope, event: T) {
    scope.launch {
    _events.emit(event)
    }
    }

    //自动销毁
    @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
    fun onDestroy() {
    YYLogUtils.w("FlowBus - 自动onDestroy")
    val subscriptCount = _events.subscriptionCount.value
    if (subscriptCount <= 0)
    busMap.remove(key)
    }
    }

    class StickEventBus<T>(key: String) : EventBus<T>(key) {
    override fun obtainEvent(): MutableSharedFlow<T> = MutableSharedFlow(1, 1, BufferOverflow.DROP_OLDEST)
    }

    }

    发送与接收消息

        // 主线程-发送消息
    FlowBus.with<String>("test-key-01").post(this@Demo11OneFragment2.lifecycleScope, "Test Flow Bus Message")
        // 接收消息
    FlowBus.with<String>("test-key-01").register(this) {
    LogUtils.w("收到FlowBus消息 - " + it)
    }

    发送粘性消息

     FlowBus.withStick<String>("test-key-02").post(lifecycleScope, "Test Stick Message")
       FlowBus.withStick<String>("test-key-02").register(this){
    LogUtils.w("收到粘性消息:$it")
    }

    Log如下:

    总结

    其实这么多消息总线框架,目前比较常用的是EventBus LiveDataBus FlowBus这三种。

    总的来说,我们尽量不依赖第三方的框架来实现,那么 FlowBus 是语言层级的,基于Kotlin的特性实现,比较推荐了。LiveDataBus 是基于Android SDK 中的类实现的(我本人是比较喜欢用),只适应于 Android 开发,但也几乎能满足日常使用了。EventBus 是基于 Java 的语言特性注解和APT,也是比较好用的。

    如果大家有源码方面的需求可以看看这里,上面的源码也都贴出来了。

    本文的代码也只是简单的实现,只是为了抛砖引玉的实现几种基本的代码,如果大家需要在实战汇总使用,更推荐大家根据不同的类型自行去 Github 上面找对应的实现封装,功能会更多,健壮性也更好。

    好了,关于消息总线就说到这了,如果觉得不错还请点赞支持哦!

    完结!


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

    收起阅读 »

    Flutter 中关于 angle 的坑

    这个问题是我最近做业务开发和业余开发都遇到的,这里的 angle 指的是旋转弧度。不是旋转角度。 先看一下我使用 angle 的场景吧: 图一中使用了 canvas.drawArc,传入了 startAngle 和 sweepAngle。图二也是如此。图...
    继续阅读 »

    这个问题是我最近做业务开发和业余开发都遇到的,这里的 angle 指的是旋转弧度。不是旋转角度


    先看一下我使用 angle 的场景吧:


    任务进度


    报警类型分布


    指针的旋转角度


    图一中使用了 canvas.drawArc,传入了 startAngle 和 sweepAngle。图二也是如此。图三是 Flutter ConstraintLayout 中圆形定位的 example,我没有使用 Flutter ConstraintLayout 自带的旋转能力,而是用了 Transform.rotate,传入了 angle。Flutter ConstraintLayout 自带的对 Widget 的旋转能力用了 canvas.rotate,也传入了 angle。


    我现在还没搞明白弧度和角度的对应关系,官网文档中也没有详细说明。但对于我来说,我根本就不想去关心弧度是多少,我只关心角度,这个角度的范围是 [0.0, 360.0]。以图三中的时钟为例,旋转 0.0 或 360.0 度时,指针应该指向 12,旋转 90.0 度时,指针应该指向 3,旋转 180.0 度时,指针应该指向 6,旋转 270.0 度时,指针应该指向 9。


    于是我们需要将旋转弧度转换成旋转角度,我研究出的转换公式如下:


    Transform.rotate:


    pi + pi * (angle / 180)

    canvas.rotate:


    angle * pi / 180

    canvas.drawArc:


    startAngle = -pi / 2
    sweepAngle = angle * pi / 180

    看见没有,这三类旋转的转换公式都不一样。我不明白 Flutter 官方为什么要这么设计,为啥这么优秀的 Flutter 引入了这么糟糕的 API。于是我带着气愤给官方提了个 Issue,想喷一喷设计这几个 API 的哥们:



    结果我被反杀了。


    冷静下来之后,我决定提交一个 Pull Request 来修正这个 API。但这需要时间,因为提交 Pull Request 的周期很长,上次我提了个 bug,Oppo 的一个哥们修复了它,Pull Request 等了将近两个月才合并。


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

    GraphQL在Flutter中的基本用法

    GraphQL是一个用于API的查询语言,它可以使客户端准确地获得所需的数据,没有任何冗余。在Flutter项目中怎么使用Graphql呢?我们需要借助graphql-flutter插件 Tip: 这里以4.0.1为例 1. 添加依赖 首先添加到pubspec...
    继续阅读 »

    GraphQL是一个用于API的查询语言,它可以使客户端准确地获得所需的数据,没有任何冗余。在Flutter项目中怎么使用Graphql呢?我们需要借助graphql-flutter插件


    Tip: 这里以4.0.1为例


    1. 添加依赖


    首先添加到pubspec.yaml


    image.png


    然后我们再看看graphql-flutter(API)有什么,以及我们该怎么用。


    2.重要API


    GraphQLClient



    • 仿apollo-client,通过配置LinkCache构造客户端实例

    • 像apollo-client一样通过构造不同Link来丰富client实例的功能


    image.png


    client实例方法几乎跟apollo-client一致,如querymutatesubscribe,也有些许差别的方法watchQuerywatchMutation 等,后面具体介绍使用区别


    Link



    graphql-flutter里基于Link实现了一些比较使用的类,如下


    HttpLink



    • 设置请求地址,默认header等


    image.png


    AuthLink



    • 通过函数的形式设置Authentication


    image.png


    ErrorLink



    • 设置错误拦截


    image.png


    DedupeLink



    • 请求去重


    GraphQLCache



    • 配置实体缓存,官方推荐使用 HiveStore 配置持久缓存


    image.png



    • HiveStore在项目中关于环境是Web还是App需要作判断,所以我们需要一个方法


    image.png


    综上各个Link以及Cache构成了Client,我们稍加对这些API做一个封装,以便在项目复用。


    3.基本封装



    • 代码及释义如下


    import 'dart:async';
    import 'package:flutter/material.dart';
    import 'package:graphql/client.dart';

    import 'package:flutter/foundation.dart' show kIsWeb;
    import 'package:path_provider/path_provider.dart'
    show getApplicationDocumentsDirectory;
    import 'package:path/path.dart' show join;
    import 'package:hive/hive.dart' show Hive;

    class Gql {
    final String source;
    final String uri;
    final String token;
    final Map<String, String> header;

    HttpLink httpLink;
    AuthLink authLink;
    ErrorLink errorLink;

    GraphQLCache cache;
    GraphQLClient client;

    String authHeaderKey = 'token';
    String sourceKey = 'source';

    Gql({
    @required this.source,
    @required this.uri,
    this.token,
    this.header = const {},
    }) {
    // 设置url,复写传入header
    httpLink = HttpLink(uri, defaultHeaders: {
    sourceKey: source,
    ...header,
    });
    // 通过复写getToken动态设置auth
    authLink = AuthLink(getToken: getToken, headerKey: authHeaderKey);
    // 错误拦截
    errorLink = ErrorLink(
    onGraphQLError: onGraphQLError,
    onException: onException,
    );
    // 设置缓存
    cache = GraphQLCache(store: HiveStore());

    client = GraphQLClient(
    link: Link.from([
    DedupeLink(), // 请求去重
    errorLink,
    authLink,
    httpLink,
    ]),
    cache: cache,
    );
    }

    static Future<void> initHiveForFlutter({
    String subDir,
    Iterable<String> boxes = const [HiveStore.defaultBoxName],
    }) async {
    if (!kIsWeb) { // 判断App获取path,初始化
    var appDir = await getApplicationDocumentsDirectory(); // 获取文件夹路径
    var path = appDir.path;
    if (subDir != null) {
    path = join(path, subDir);
    }
    Hive.init(path);
    }

    for (var box in boxes) {
    await Hive.openBox(box);
    }
    }

    FutureOr<String> getToken() async => null;

    void _errorsLoger(List<GraphQLError> errors) {
    errors.forEach((error) {
    print(error.message);
    });
    }

    // LinkError处理函数
    Stream<Response> onException(
    Request req,
    Stream<Response> Function(Request) _,
    LinkException exception,
    ) {
    if (exception is ServerException) { // 服务端错误
    _errorsLoger(exception.parsedResponse.errors);
    }

    if (exception is NetworkException) { // 网络错误
    print(exception.toString());
    }

    if (exception is HttpLinkParserException) { // http解析错误
    print(exception.originalException);
    print(exception.response);
    }

    return _(req);
    }

    // GraphqlError
    Stream<Response> onGraphQLError(
    Request req,
    Stream<Response> Function(Request) _,
    Response res,
    ) {
    // print(res.errors);
    _errorsLoger(res.errors); // 处理返回错误
    return _(req);
    }
    }

    4. 基本使用



    • main.dart


    void main() async {

    await Gql.initHiveForFlutter(); // 初始化HiveBox

    runApp(App());
    }


    • clent.dart


    import 'dart:async';
    import 'package:flutter/material.dart';
    import 'package:shared_preferences/shared_preferences.dart';


    const codeMessage = {
    401: '登录失效,',
    403: '用户已禁用',
    500: '服务器错误',
    503: '服务器错误',
    };

    // 通过复写,实现错误处理与token设置
    class CustomGgl extends Gql {
    CustomGgl({
    @required String source,
    @required String uri,
    String token,
    Map<String, String> header = const {},
    }) : super(source: source, uri: uri, token: token, header: header);

    String authHeaderKey = 'token';

    @override
    FutureOr<String> getToken() async { // 设置token
    final sharedPref = await SharedPreferences.getInstance();
    return sharedPref.getString(authHeaderKey);
    }

    @override
    Stream<Response> onGraphQLError( // 错误处理并给出提示
    Request req,
    Stream<Response> Function(Request) _,
    Response res,
    ) {
    res.errors.forEach((error) {
    final num code = error.extensions['exception']['status'];
    Toast.error(message: codeMessage[code] ?? error.message);
    print(error);
    });
    return _(req);
    }
    }

    // 创建ccClient
    final Gql ccGql = CustomGgl(
    source: 'cc',
    uri: 'https://xxx/graphql',
    header: {
    'header': 'xxxx',
    },
    );


    • demo.dart


    import 'package:flutter/material.dart';

    import '../utils/client.dart';
    import '../utils/json_view/json_view.dart';
    import '../models/live_bill_config.dart';
    import '../gql_operation/gql_operation.dart';

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

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

    class _GraphqlDemoState extends State<GraphqlDemo> {
    ObservableQuery observableQuery;
    ObservableQuery observableMutation;

    Map<String, dynamic> json;
    num pageNum = 1;
    num pageSize = 10;

    @override
    void initState() {
    super.initState();

    Future.delayed(Duration(), () {
    initObservableQuery();
    initObservableMutation();
    });
    }

    @override
    Widget build(BuildContext context) {
    return SingleChildScrollView(
    child: Column(
    crossAxisAlignment: CrossAxisAlignment.start,
    children: [
    Wrap(
    spacing: 10.0,
    runSpacing: 10.0,
    children: [
    RaisedButton(
    onPressed: getLiveBillConfig,
    child: Text('Basic Query'),
    ),
    RaisedButton(
    onPressed: sendPhoneAuthCode,
    child: Text('Basic Mutation'),
    ),
    RaisedButton(
    onPressed: () {
    pageNum++;

    observableQuery.fetchMore(FetchMoreOptions(
    variables: {
    'pageNum': pageNum,
    'pageSize': pageSize,
    },
    updateQuery: (prev, newData) => newData,
    ));
    },
    child: Text('Watch Query'),
    ),
    RaisedButton(
    onPressed: () {
    observableMutation.fetchResults();
    },
    child: Text('Watch Mutation'),
    ),
    ],
    ),
    Divider(),
    if (json != null)
    SingleChildScrollView(
    child: JsonView.map(json),
    scrollDirection: Axis.horizontal,
    ),
    ],
    ),
    );
    }


    @override
    dispose() {
    super.dispose();

    observableQuery.close();
    }

    void getLiveBillConfig() async {
    Toast.loading();

    try {
    final QueryResult result = await ccGql.client.query(QueryOptions(
    document: gql(LIVE_BILL_CONFIG),
    fetchPolicy: FetchPolicy.noCache,
    ));

    final liveBillConfig =
    result.data != null ? result.data['liveBillConfig'] : null;
    if (liveBillConfig == null) return;

    setState(() {
    json = LiveBillConfig.fromJson(liveBillConfig).toJson();
    });
    } finally {
    if (Toast.loadingType == ToastType.loading) Toast.dismiss();
    }
    }


    void sendPhoneAuthCode() async {
    Toast.loading();

    try {
    final QueryResult result = await ccGql.client.mutate(MutationOptions(
    document: gql(SEND_PHONE_AUTH_CODE),
    fetchPolicy: FetchPolicy.cacheAndNetwork,
    variables: {
    'phone': '15883300888',
    'authType': 2,
    'platformName': 'Backend'
    },
    ));

    setState(() {
    json = result.data;
    });
    } finally {
    if (Toast.loadingType == ToastType.loading) Toast.dismiss();
    }
    }

    void initObservableQuery() {
    observableQuery = ccGql.client.watchQuery(
    WatchQueryOptions(
    document: gql(GET_EMPLOYEE_CONFIG),
    variables: {
    'pageNum': pageNum,
    'pageSize': pageSize,
    },
    ),
    );

    observableQuery.stream.listen((QueryResult result) {
    if (!result.isLoading && result.data != null) {
    if (result.isLoading) {
    Toast.loading();
    return;
    }

    if (Toast.loadingType == ToastType.loading) Toast.dismiss();
    setState(() {
    json = result.data;
    });
    }
    });
    }

    void initObservableMutation() {
    observableMutation = ccGql.client.watchMutation(
    WatchQueryOptions(
    document: gql(LOGIN_BY_AUTH_CODE),
    variables: {
    'phone': '15883300888',
    'authCodeType': 2,
    'authCode': '5483',
    'statisticInfo': {'platformName': 'Backend'},
    },
    ),
    );

    observableMutation.stream.listen((QueryResult result) {
    if (!result.isLoading && result.data != null) {
    if (result.isLoading) {
    Toast.loading();
    return;
    }

    if (Toast.loadingType == ToastType.loading) Toast.dismiss();
    setState(() {
    json = result.data;
    });
    }
    });
    }
    }

    总结


    这篇文章介绍了如何在Flutter项目中简单快速的使用GraphQL。并实现了一个简单的Demo。但是上面demo将UI和数据绑定在一起,导致代码耦合性很高。在实际的公司项目中,我们都会将数据和UI进行分离,常用的做法就是将GraphQL的 ValueNotifier client 调用封装到VM层中,然后在Widget中把VM数据进行绑定操作。网络上已经有大量介绍Provider|Bloc|GetX的文章,这里以介绍GraphQL使用为主,就不再赘述了。


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

    Android UI 测试基础

    UI 测试UI 测试的一种方法是直接让测试人员对目标应用执行一系列用户操作,验证其行为是否正常。这种人工操作的方式一般非常耗时、繁琐、容易出错且 case 覆盖面不全。而另一种高效的方法是为编写 UI 测试,以自动化的方式执行用户操作。自动化方法可以可重复且快...
    继续阅读 »

    UI 测试

    UI 测试的一种方法是直接让测试人员对目标应用执行一系列用户操作,验证其行为是否正常。这种人工操作的方式一般非常耗时、繁琐、容易出错且 case 覆盖面不全。而另一种高效的方法是为编写 UI 测试,以自动化的方式执行用户操作。自动化方法可以可重复且快速可靠地运行测试。

    使用 Android Studio 自动执行 UI 测试,需要在 src/AndroidTest/java 中实现测试代码,这种测试属于插桩单元测试。Android 的 Gradle 插件会根据测试代码构建一个测试应用,然后在目标应用所在的设备上加载该测试应用。在测试代码中,可以使用 UI 测试框架来模拟目标应用上的用户交互。

    注意:并不是所有对 UI 的测试都是插桩单元测试,在本地单元测试中,也可以通过第三方框架(例如 Robolectric )来模拟 Android 运行环境,但这种测试是跑在开发计算机上的,基于 JVM 运行,而不是 Android 模拟器或物理设备的真实环境。

    涉及 UI 测试的场景有两种情况:

    • 单个 App 的 UI 测试:这种类型的测试可以验证目标应用在用户执行特定操作或在其 Activity 中输入特定内容时行为是否符合预期。Espresso 之类的 UI 测试框架可以实现通过编程的方式模拟用户交互。
    • 流程涵盖多个 App 的 UI 测试:这种类型的测试可以验证不同 App 之间或是用户 App 与系统 App 之间的交互流程是否正常运行。比如在一个应用中打开系统相机进行拍照。UI Automator 框架可以支持跨应用交互。

    Android 中的 UI 测试框架

    Jetpack 包含了丰富的官方框架,这些框架提供了用于编写 UI 测试的 API:

    • Espresso :提供了用于编写 UI 测试的 API ,可以模拟用户与单个 App 进行 UI 交互。使用 Espresso 的一个主要好处是它提供了测试操作与您正在测试的应用程序 UI 的自动同步。Espresso 会检测主线程何时空闲,因此它能够在适当的时间运行您的测试命令,从而提高测试的可靠性。
    • Jetpack Compose :提供了一组测试 API 用来启动 Compose 屏幕和组件之间的交互,融合到了开发过程中。算是 Compose 的一个优势。
    • UI Automator : 是一个 UI 测试框架,适用于涉及多个应用的操作流程的测试。
    • Robolectric :在 JVM 上运行本地单元测试,而不是模拟器或物理设备上。可以配合 Espresso 或 Compose 的测试 API 与 UI 组件进行模拟交互。

    异常行为和同步处理

    因为 Android 应用是基于多线程实现的,所有涉及 UI 的操作都会发送到主线程排队执行,所以在编写测试代码时,需要处理这种异步存在的问题。当一个用户输入注入时,测试框架必须等待 App 对用户输入进行响应。当一个测试没有确定性行为的时候,就会出现异常行为。

    像 Compose 或 Espresso 这样的现代框架在设计时就考虑到了测试场景,因此可以保证在下一个测试操作或断言之前 UI 将处于空闲状态,从而保证了同步行为。

    流程图显示了在通过测试之前检查应用程序是否空闲的循环:

    流程图显示了在通过测试之前检查应用程序是否空闲的循环.png

    在测试中使用 sleep 会导致测试缓慢或者不稳定,如果有动画执行超过 2s 就会出现异常情况。

    显示同步基于等待固定时间时的测试失败的图表.png

    应用架构和测试

    另一方面,应用的架构应该能够快速替换一些组件,以支持 mock 数据或逻辑进行测试,例如,在有异步加载数据的场景,但我们并不关心异步数据获取相关逻辑的情况下,仅关心获取到数据后的 UI 层测试,就可以将异步逻辑替换成假的数据源,从而能够更加高效的进行测试:

    生产和测试架构图。 生产图显示了向存储库提供数据的本地和远程数据源,而存储库又将数据异步提供给 UI。 测试图显示了一个 Fake 存储库,该存储库将其数据同步提供给 UI.png

    推荐使用 Hilt 框架实现这种注入数据的替换操作。

    为什么需要自动化测试?

    Android App 可以在不同的 API 版本的上千种不同设备上运行,并且手机厂商有可能修改系统代码,这意味着 App 可能会在一些设备上不正确地运行甚至导致 crash 。

    UI 测试可以进行兼容性测试,验证 App 在不同环境中的行为。例如可以测试不同环境下的行为:

    • API level 不同
    • 位置和语言设置不同
    • 屏幕方向不同

    此外,还要考虑设备类型的问题,例如平板电脑和可折叠设备的行为,可能与普通手机设备环境下,产生不同的行为。

    AndroidX 测试框架的使用

    环境配置

    1. 修改根目录下的 build.gradle文件,确保项目依赖仓库:
    allprojects {
    repositories {
    jcenter()
    google()
    }
    }
    1. 添加测试框架依赖:
    dependencies {
    // 核心框架
    androidTestImplementation "androidx.test:core:$androidXTestVersion0"

    // AndroidJUnitRunner and JUnit Rules
    androidTestImplementation "androidx.test:runner:$testRunnerVersion"
    androidTestImplementation "androidx.test:rules:$testRulesVersion"

    // Assertions 断言
    androidTestImplementation "androidx.test.ext:junit:$testJunitVersion"
    androidTestImplementation "androidx.test.ext:truth:$truthVersion"

    // Espresso 依赖
    androidTestImplementation "androidx.test.espresso:espresso-core:$espressoVersion"
    androidTestImplementation "androidx.test.espresso:espresso-contrib:$espressoVersion"
    androidTestImplementation "androidx.test.espresso:espresso-intents:$espressoVersion"
    androidTestImplementation "androidx.test.espresso:espresso-accessibility:$espressoVersion"
    androidTestImplementation "androidx.test.espresso:espresso-web:$espressoVersion"
    androidTestImplementation "androidx.test.espresso.idling:idling-concurrent:$espressoVersion"

    // 下面的依赖可以使用 "implementation" 或 "androidTestImplementation",
    // 取决于你是希望这个依赖出现在 Apk 中,还是测试 apk 中
    androidTestImplementation "androidx.test.espresso:espresso-idling-resource:$espressoVersion"
    }

    发行版本号参阅: developer.android.com/jetpack/and…

    另外值得注意的一点是 espresso-idling-resource 这个依赖在生产代码中使用的话,需要打包到 apk 中。

    AndroidX 中的 Junit4 Rules

    AndroidX 测试框架包含了一组配合 AndroidJunitRunner 使用的 Junit Rules。

    关于什么是 JUnit Rules ,可以查看 wiki:github.com/junit-team/…

    JUnit Rules 提供了更大的灵活性并减少了测试中所需的样板代码。可以将 JUnit Rules 理解为一些模拟环境用来测试的 API 。例如:

    • ActivityScenarioRule : 用来模拟 Activity 。
    • ServiceTestRule :可以用来模拟启动 Service 。
    • TemporaryFolder :可以用来创建文件和文件夹,这些文件会在测试方法完成时被删除(若不能删除,会抛出异常)。
    • ErrorCollector :发生问题后继续执行测试,最后一次性报告所有错误内容。
    • ExpectedException :在测试过程中指定预期的异常。

    除了上面几个例子,还有很多 Rules ,可以将 Rules 理解为用来在测试中快捷实现一些能力的 API 。

    ActivityScenarioRule

    ActivityScenarioRule 用来对单个 Activity 进行功能测试。声明一个 ActivityScenarioRule 实例:

        @get:Rule
    val activityRule = ActivityScenarioRule(MainActivity::class.java)

    这个规则,会在执行标注有 @Test 注解的测试方法启动前,绑定构造参数中执行的 Activity ,并且在带有 @Test 测试方法执行前,先执行所有带有 @Before 注解的方法,并在执行的测试方法结束后,执行所有带有 @After 注解的方法。

    @RunWith(AndroidJUnit4::class)
    class MainActivityTest {
    @Before
    fun beforeActivityCreate() {
    Log.d(TAG, "beforeActivityCreate")
    }

    @Before
    fun beforeTest() {
    Log.d(TAG, "beforeTest")
    }

    @Test
    fun onCreate() {
    activityRule.scenario.moveToState(Lifecycle.State.CREATED).onActivity {
    Log.d(TAG, "in test thread: ${Thread.currentThread()}}")
    }
    }

    @After
    fun afterActivityCreate() {
    Log.d(TAG, "afterActivityCreate")
    }
    // ...
    }

    执行这个带有 @Test 注解的 onCreate方法,其日志为:

    2022-06-17 17:29:07.341 I/TestRunner: started: onCreate(com.chunyu.accessibilitydemo.MainActivityTest)
    2022-06-17 17:29:08.006 D/MainActivityTest: beforeTest
    2022-06-17 17:29:08.006 D/MainActivityTest: beforeActivityCreate
    2022-06-17 17:29:08.565 D/MainActivityTest: in ui thread: Thread[main,5,main]
    2022-06-17 17:29:08.566 D/MainActivityTest: afterActivityCreate
    2022-06-17 17:29:09.054 I/TestRunner: finished: onCreate(com.chunyu.accessibilitydemo.MainActivityTest)

    在执行完所有的 @After 方法后,会终止模拟启动的这个 Activity 。

    访问 Activity

    测试方法中的重点是通过 ActivityScenarioRule 模拟构造 Activity ,并对其中的一些行为进行测试。

    如果要在测试逻辑中访问指定的 Activity ,可以通过 ActivityScenarioRule.getScenario().onActivity{ ... } 回调中指定一些代码逻辑。例如上面的 onCreate() 测试方法中,稍加修改,就可以展示访问 Activity 的能力:

        @Test
    fun onCreate() {
    activityRule.scenario.onActivity { it ->
    Log.d(TAG, "${it.isFinishing}")
    }
    }

    不光可以访问 Activity 中公开的属性和方法,还可以访问指定 Activity 中 public 的内容,例如:

        @Test
    fun test() {
    activityRule.scenario.onActivity { it ->
    it.button.performClick()
    }
    }

    控制 Activity 的生命周期

    在最开始的例子中,我们通过 moveToState 来控制了这个 Activity 的生命周期,修改代码:

        @Test
    fun onCreate() {
    activityRule.scenario.moveToState(Lifecycle.State.CREATED).onActivity {
    Log.d(TAG, "${it.lifecycle.currentState}")
    }
    }

    我们在 onActivity 中打印 Activity 的当前生命周期,检查一下是否真的是在 moveToState 中指定的状态,打印结果:

    2022-06-17 17:45:30.425 D/MainActivityTest: CREATED

    moveToState 的确生效了,它可以将 Activity 控制到我们想要的状态。

    通过 ActivityScenarioRule 的 getState() ,也可以直接获取到模拟的 Activity 的状态,这个方法可能存在的状态包括:

    • State.CREATED
    • State.STARTED
    • State.RESUMED
    • State.DESTROYED

    而 moveToState 能够设置的值包括:

        public enum State {
    // 这个状态表示 Activity 已销毁
    DESTROYED,

    // 初始化状态,还没调用 onCreate
    INITIALIZED,

    // 存在两种情况,在 onCreate 开始后,onStop 结束前
    CREATED,

    // 存在两种情况,在 onStart 开始后,在 onPause 结束前。
    STARTED,

    // onResume 开始后调用。
    RESUMED;

    // ...
    }

    当 moveToState 设置为 DESTROYED ,再访问 Activity ,会抛出异常

    java.lang.NullPointerException: Cannot run onActivity since Activity has been destroyed already

    如果要测试 Fragment ,可以通过 FragmentScenario 进行,此类需要引用

     debugImplementation "androidx.fragment:fragment-testing:$fragment_version"

    ServiceTestRule

    ServiceTestRule 用来在单元测试情况下模拟启动指定的 Service ,包括 bindService 和 startService 两种方式,创建一个 ServiceTestRule 实例:

        @get:Rule
    val serviceTestRule = ServiceTestRule()

    在测试方法中通过 ServiceTestRule 启动 Service ,下面是一个普通的服务,在真实环境下通过 startService 可以正常启动:

    class RegularService: Service() {

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
    Log.d("onStartCommand", ": ${Thread.currentThread().name}")
    Toast.makeText(this, "in Service", Toast.LENGTH_SHORT).show()
    return super.onStartCommand(intent, flags, startId)
    }

    override fun onBind(intent: Intent?): IBinder? {
    return null
    }
    }

    startService

        @Test
    fun testService() {
    serviceTestRule.startService(Intent(ApplicationProvider.getApplicationContext(), RegularService::class.java))
    }

    但是这样会抛出异常:

    java.util.concurrent.TimeoutException: Waited for 5 SECONDS, but service was never connected

    这是因为,通过 ServiceTestRule 的 startService(Intent) 启动一个 Service ,会在 5s 内阻塞直到 Service 已连接,即调用到了 ServiceConnection.onServiceConnected(ComponentName, IBinder) 。

    也就是说,你的 Service 的 onBind(Intent) 方法,不能返回 null ,否则就会抛出 TimeoutException 。

    修改 RegularService :

    class RegularService: Service() {

    private val binder = RegularBinder()

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
    Log.d("RegularServiceTest", "onStartCommand")
    return super.onStartCommand(intent, flags, startId)
    }

    override fun onBind(intent: Intent?): IBinder? {
    return binder
    }

    inner class RegularBinder: Binder() {
    fun getService(): RegularService = this@RegularService
    }
    }

    这样,通过 ServiceTestRule 的 startService 启动服务就可以正常运行了:

    2022-06-17 19:51:59.772 I/TestRunner: started: testService(com.chunyu.accessibilitydemo.service.RegularServiceTest)
    2022-06-17 19:51:59.777 D/RegularServiceTest: beforeService1
    2022-06-17 19:51:59.777 D/RegularServiceTest: beforeService2
    2022-06-17 19:51:59.795 D/RegularServiceTest: onStartCommand
    2022-06-17 19:51:59.820 D/RegularServiceTest: afterService1
    2022-06-17 19:51:59.820 D/RegularServiceTest: afterService2
    2022-06-17 19:51:59.830 I/TestRunner: finished: testService(com.chunyu.accessibilitydemo.service.RegularServiceTest)

    ServiceTestRule 和 ActivityScenarioRule 一样,都会在执行测试前执行所有的 @Before 方法,执行结束后,继续执行所有的 @After 方法。

    bindService

        @Test
    fun testService() {
    serviceTestRule.bindService(Intent(ApplicationProvider.getApplicationContext(), RegularService::class.java))
    }

    ServiceTestRule.bindService 效果和 Context.bindService 相同,都不走 onStartCommand 而是 onBind 方法。

    2022-06-17 19:57:19.274 I/TestRunner: started: testService(com.chunyu.accessibilitydemo.service.RegularServiceTest)
    2022-06-17 19:57:19.277 D/RegularServiceTest: beforeService1
    2022-06-17 19:57:19.277 D/RegularServiceTest: beforeService2
    2022-06-17 19:57:19.296 D/RegularServiceTest: onBind
    2022-06-17 19:57:19.302 D/RegularServiceTest: afterService1
    2022-06-17 19:57:19.302 D/RegularServiceTest: afterService2
    2022-06-17 19:57:19.314 I/TestRunner: finished: testService(com.chunyu.accessibilitydemo.service.RegularServiceTest)

    测试方法的执行顺序也是一样的。

    访问 Service

    startService 启动的 Service 无法获取到 Service 实例,ServiceTestRule 并没有像 ActivityScenarioRule 那样提供 onActivity {... } 回调方法。

    bindService 的返回类型是 IBinder ,可以通过 IBinder 对象获取到 Service 实例:

        @Test
    fun testService() {
    val binder = serviceTestRule.bindService(Intent(ApplicationProvider.getApplicationContext(), RegularService::class.java))
    val service = (binder as? RegularService.RegularBinder)?.getService()
    // access RegularService info
    }


    作者:自动化BUG制造器
    链接:https://juejin.cn/post/7110184974791213064
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

    收起阅读 »

    学会使用LiveData和ViewModel,我相信会让你在写业务时变得轻松🌞

    介绍 在2017年,那时,观察者模式有效的简化了开发,但是诸如RxJava一类的库有一些太过复杂,学习成本太高,为此,LiveData出现了,一个专用于Android的,具备自主生命周期感知能力的,可观测的数据存储类。同时也出现了ViewModel这个组件,配...
    继续阅读 »

    介绍


    在2017年,那时,观察者模式有效的简化了开发,但是诸如RxJava一类的库有一些太过复杂,学习成本太高,为此,LiveData出现了,一个专用于Android的,具备自主生命周期感知能力的,可观测的数据存储类。同时也出现了ViewModel这个组件,配合LiveData,更方便的实现MVVM模式中Model与View的分离。那么就让本文来带大家来学习LiveData与ViewModel的使用吧。


    LiveData和ViewModel的关系:


    关系图.png


    本文的案例代码:github.com/taxze6/Jetp…


    LiveData


    参考资料:


    🌟官方文档:developer.android.google.cn/topic/libra…


    🌟LiveData postValue详解:http://www.cnblogs.com/button123/p…


    LiveData是一种可观察的数据存储器类(响应式编程,类似Vue)。与常规的可观察类不同,LiveData 具有生命周期感知能力。LiveData最重要的是它了解观察者的生命周期,如ActivityFragment。


    因此,当LiveData发送变化时,UI会收到通知,然后UI可以使用新数据重新绘制自己。换句话说,LiveData可以很容易地使屏幕上发生的事情与数据保持同步(响应式编程的核心)


    使用 LiveData 具有以下优势:




    • UI与数据状态匹配



      • LiveData 遵循观察者模式。当底层数据发生变化时,LiveData 会通知Observer对象。您可以整合代码以在这些 Observer对象中更新界面。这样一来,您无需在每次应用数据发生变化时更新界面,因为观察者会替您完成更新。




    • 提高代码的稳定性


      代码稳定性在整个应用程序生命周期中增加:



      • 活动停止时不会发生崩溃。如果应用程序组件处于非活动状态,则这些更改不受影响。因此,您在更新数据时无需担心应用程序组件的生命周期。对于后台堆栈中的活动,它不会接受任何LiveData事件

      • 内存泄漏会减少,观察者会绑定到Lifecycle对象,并在其关联的生命周期遭到销毁后进行自我清理

      • 取消订阅任何观察者时无需担心

      • 如果由于配置更改(如设备旋转)而重新创建了 Activity 或 Fragment,它会立即接收最新的可用数据。




    • 不再需要手动处理生命周期


      界面组件只是观察相关数据,不会停止或恢复观察。LiveData 将自动管理所有这些操作,因为它在观察时可以感知相关的生命周期状态变化。




    • 数据始终保持最新状态


      如果生命周期变为非活跃状态,它会在再次变为活跃状态时接收最新的数据。例如,曾经在后台的 Activity 会在返回前台后立即接收最新的数据。




    • 共享资源


      像单例模式一样,我们也可以扩展我们的LiveData对象来包装系统服务,以便它们可以在我们的应用程序中共享。一旦LiveData对象连接到系统服务,任何需要资源的观察者可以轻松地观看LiveData对象。




    在以下情况中,不要使用LiveData:



    • 您需要在信息上使用大量运算符,尽管LiveData提供了诸如转换之类的工具,但只有Map和switchMap可以帮助您

    • 您没有与信息的UI交互

    • 您有一次性的异步操作

    • 您不必将缓存的信息保存到UI中


    如何使用LiveData


    一般来说我们会在 ViewModel 中创建 Livedata 对象,保证app配置变更时,数据不会丢失,然后再 Activity/Fragment 的 onCreate 中注册 Livedata 监听(因为在 onStart 和 onResume 中进行监听可能会有冗余调用)


    基础使用流程:


    1.创建一个实例LiveData来保存某种类型的数据。一般在你创建的ViewModel类中完成


    class MainViewModel : ViewModel() {
       var mycount: MutableLiveData<Int> = MutableLiveData()
    }

    2.在Activity或者Fragment中获取到ViewModel,通过ViewModel获取到对应的LiveData


    class MainActivity : AppCompatActivity() {
    lateinit var viewModel: MainViewModel
    override fun onCreate(savedInstanceState: Bundle?) {
    ...
           /**记住绝对不可以直接去创建ViewModel实例
    一定要通过ViewModelProvider(ViewModelStoreOwner)构造函数来获取。
    因为每次旋转屏幕都会重新调用onCreate()方法,如果每次都创建新的实例的话就无法保存数据了。
    用上述方法后,onCreate方法被再次调用,
    它会返回一个与MainActivity相关联的预先存在的ViewModel,这就是保存数据的原因。*/
    viewModel = ViewModelProvider(this@MainActivity,ViewModelProvider.
    NewInstanceFactory()).get(MainViewModel::class.java)
    }
    }

    3.给LiveData添加观察者监听,用来监听LiveData中的数据变化,在Observer的onChanged中使用监听回调数据


    /**
    * 订阅 ViewModel,mycount是一个LiveData类型 可以观察
    * */        
    viewModel.mycount.observe(this@MainActivity) {
    countTv.text = viewModel.mycount.value.toString()
    }
    // LiveData onchange会自动感应生命周期 不需要手动
    //       viewModel.mycount.observe(this, object : Observer<Int> {
    //           override fun onChanged(t: Int?) {
    //
    //           }
    //       })

    进阶用法:


    Transformations.map



    现在有一个场景:我们通过网络请求,获得了一个User类数据(LiveData),但是,我们只想把User.name暴露给外部观察者,这样我们就可以通过Transformations.map来转化LiveData的数据类型,从而来实现上述场景。这个函数常用于对数据的封装。



    //实体类
    data class User(var name: String)
    ...
    //Transformations.map接收两个参数,第一个参数是用于转换的LiveData原始对象,第二个参数是转换函数。
    private val userLiveData: MutableLiveData<User> = MutableLiveData()
       val userNames: LiveData<String> = Transformations
          .map(userLiveData) { user ->
               user.name
    }

    Transformations.switchMap



    switchMap是根据传入的LiveData的值,然后判断这个值,然后再去切换或者构建新的LiveData。比如我们有些数据需要依赖其他数据进行查询,就可以使用switchMap。


    例如,有一个学生,他有两门课程的成绩,但是在UI组件中,我们一次只能显示一门课的成绩,在这个需要判断展示哪门课程成绩的需求下,我们就可以使用switchMap。



    data class Student
    (var englishScore: Double, var mathScore: Double, val scoreTAG: Boolean)

    .....
    class SwitchMapViewModel:ViewModel {
       var studentLiveData = MutableLiveData<Student>()
       val transformationsLiveData = Transformations.switchMap(studentLiveData) {
           if (it.scoreTAG) {
               MutableLiveData(it.englishScore)
          } else {
               MutableLiveData(it.mathScore)
          }
      }
    }

    //使用时:
    var student = Student()
    person.englishScore = 88.2
    person.mathScore = 91.3
    //判断显示哪个成绩
    person.condition = true
    switchMapViewModel.conditionLiveData.postValue(person)

    MediatorLiveData



    MediatorLiveData继承于MutableLiveData,在MutableLiveData的基础上,增加了合并多个LiveData数据源的功能。其实就是通过addSource()这个方法去监听多个LiveData。


    例如:现在有一个存在本地的dbLiveData,还有一个网络请求来的LiveData,我们需要讲上面两个结果结合之后展示给用户,第一种做法是我们在Activity中分别注册这两个LiveData的观察者,当数据发生变化时去更新UI,但是我们其实使用MediatorLiveData可以简化这个操作。



    class MediatorLiveDataViewModel : ViewModel() {
       var liveDataA = MutableLiveData<String>()
       var liveDataB = MutableLiveData<String>()

       var mediatorLiveData = MediatorLiveData<String>()
       
       init {
           mediatorLiveData.addSource(liveDataA) {
               Log.d("This is livedataA", it)
               mediatorLiveData.postValue(it)
          }

           mediatorLiveData.addSource(liveDataB) {
               Log.d("This is livedataB", it)
               mediatorLiveData.postValue(it)
          }
      }
    }

    解释:


    如果是第一次接触到LiveData的朋友可能会发现,我们虽然一直在提LiveData,但是用的时候却是MutableLiveData,这两个有什么关系呢,既然都没怎么用LiveData,那么把标题直接改成MutableLiveData吧
    其实,LiveData与MutableLiveData在概念上是一模一样的。唯一的几个区别分别是:



    💡“此处引用:LiveData与MutableLiveData的区别文章中的段落”



    • MutableLiveData的父类是LiveData

    • LiveData在实体类里可以通知指定某个字段的数据更新

    • MutableLiveData则是完全是整个实体类或者数据类型变化后才通知.不会细节到某个字段。



    原理探究:


    对于LiveData的基础使用我们就讲到这里,想要探索LiveData原理的朋友可以从下面几个角度:



    • LiveData的工作原理

    • LiveData的observe方法源码分析

    • LifecycleBoundObserver源码分析

    • activeStateChanged源码分析(用于粘性事件)

    • postValue和setValue

    • considerNotify判断是否发送数据分析

    • 粘性事件的分析


    相信大家从以上几个角度去分析LiveData会有不小的收获💪


    ViewModel


    官方文档:developer.android.google.cn/topic/libra…


    官方简介


    ViewModel类旨在以注重生命周期的方式存储和管理界面相关的数据。ViewModel类让数据可在发生屏幕旋转等配置更改后继续留存。


    生命周期


    生命周期.png


    ViewModel的生命周期会比创建它的Activity、Fragment的生命周期都要长。所以ViewModel中的数据会一直存活在Activity/Fragment中。


    基础使用流程:


    1.构造数据对象



    自定义ViewModel类,继承ViewModel,然后在自定义的ViewModel类中添加需要的数据对象



    class MainViewModel : ViewModel() {
    ...
    }

    2.获取数据



    有两种常见的ViewModel创建方式,第一种是在activity或fragment种直接基于 ViewModelProvider 获取。第二种是通过ViewModelFactory 创建



    //第一种 ViewModelProvider直接获取
    ViewModelProvider(this@MainActivity).get(MainViewModel::class.java)

    //第二种 通过 ViewModelFactory 创建
    class TestViewModelFactory(private val param: Int) : ViewModelProvider.Factory {
       override fun <T : ViewModel> create(modelClass: Class<T>): T {
           return TestViewModel(param) as T
      }
    }

    ViewModelProvider(this@MainActivity,TestViewModelFactory(0)).get(TestViewModel::class.java)


    使用ViewModel就是这么简单🚢


    ViewModel常见的使用场景



    • 使用ViewModel,在横竖屏切换后,Activity重建,数据仍可以保存

    • 同一个Activity下,Fragment之间的数据共享

    • 与LiveData配合实现代码的解耦


    ViewModel和onSaveInstanceState的区别


    我相信大家一定知道onSaveInstanceState,它也是用来保存UI状态的,你可以使用它保存你所想保存的东西,在Activity被杀死之前,它一般在onStop或者onPause之前触发。虽然ViewModel被设计为应用除了onSaveInstanceState的另一个选项,但是还是有一些明显的区别。由于资源限制,ViewModel无法在进程关闭后继续存在,但onSaveInstance包含执行此任务。ViewModel是存储数据的绝佳选择,而onSaveInstanceState bundles不是用于该目的的合适选项。


    ViewModel用于存储尽可能多的UI数据。因此,在配置更改期间不需要重新加载或重新生成该数据。


    另一方面,如果该进程被框架关闭,onSaveInstanceState应该存储回复UI状态所需的最少数据量。例如,可以将所有用户的数据存放在ViewModel中,而仅将用户的数据库ID存储在onSaveInstanceState中。


    android onSaveInstanceState调用时机详细总结


    onSaveInstanceState用法及源码分析


    ViewModel和Context


    ViewModel不应该包含对Activity,Fragment或context的引用,此外,ViewModel不应包含对UI控制器(如View)的引用,因为这将创建对Context的间接引用。当您旋转Activity被销毁的屏幕时,您有一个ViewModel包含对已销毁Activity的引用,这就是内存泄漏。因此,如果需要使用上下文,则必须使用应用程序上下文 (AndroidViewModel)


    LiveData和ViewModel的基本用法我们已经介绍完了,现在用几个例子带大家来更好的使用它们


    案例一:计数器 — 两个Activity共享一个ViewModel


    话不多说,先上效果图:


    2022-06-21-12-02-50.gif
    虽然这个案例是比较简单的,但是我相信可以帮助你更快的熟悉LiveData和ViewModel


    想要实现效果图的话需要从下面几步来写(只讲解核心代码,具体代码请自己查看仓库):


    第一步:创建ViewModel


    import androidx.lifecycle.LiveData
    import androidx.lifecycle.MutableLiveData
    import androidx.lifecycle.ViewModel

    class MainViewModel : ViewModel() {
       private var _mycount: MutableLiveData<Int> = MutableLiveData()
       //只暴露不可变的LiveData给外部
       val mycount: LiveData<Int> get() = _mycount
       init {
      //初始化
           _mycount.value = 0
      }
       /**
        * mycount.value若为空就赋值为0,不为空则加一
        * */
       fun add() {
           _mycount.value = _mycount.value?.plus(1)
      }
       /**
        * mycount.value若为空就赋值为0,不为空则减一,可以为负数
        * */
       fun reduce() {
           _mycount.value = _mycount.value?.minus(1)
      }
       /**
        * 随机参数
        * */
       fun random() {
           val random = (0..100).random()
           _mycount.value = random
      }
       /**
        * 清除数据
        * */
       fun clear() {
           _mycount.value = 0
      }
    }

    第二步:标记ViewModel的作用域


    因为,我们是两个Activity共享一个ViewModel,所以我们需要标记ViewModel的作用域


    import androidx.lifecycle.*

    /**
    * 用于标记viewmodel的作用域
    */
    @Retention(AnnotationRetention.RUNTIME)
    @Target(AnnotationTarget.FIELD)
    annotation
    class VMScope(val scopeName: String) {}

    private val vMStores = HashMap<String, VMStore>()

    fun LifecycleOwner.injectViewModel() {
       //根据作用域创建商店
       this::class.java.declaredFields.forEach { field ->
           field.getAnnotation(VMScope::class.java)?.also { scope ->
               val element = scope.scopeName
               var store: VMStore
               if (vMStores.keys.contains(element)) {
                   store = vMStores[element]!!
              } else {
                   store = VMStore()
                   vMStores[element] = store
              }
               val clazz = field.type as Class<ViewModel>
               val vm = ViewModelProvider(store, ViewModelProvider.NewInstanceFactory()).get(clazz)
               field.set(this, vm)
          }
      }
    }

    class VMStore : ViewModelStoreOwner {
       private var vmStore: ViewModelStore? = null
       override fun getViewModelStore(): ViewModelStore {
           if (vmStore == null)
               vmStore = ViewModelStore()
           return vmStore!!
      }

    }

    第三步:在Activity中使用(都是部分代码)


    class MainActivity : AppCompatActivity() {
       @VMScope("count") //设置作用域
       lateinit var viewModel: MainViewModel

       override fun onCreate(savedInstanceState: Bundle?) {
           super.onCreate(savedInstanceState)
           setContentView(R.layout.activity_main)
           injectViewModel()
           initEvent()
      }

       private fun initEvent() {
           val cardReduce: CardView = findViewById(R.id.card_reduce)
          .....
           cardReduce.setOnClickListener {
               //调用自定义ViewModel中的方法
               viewModel.reduce()
          }
           
          .....
           
           /**
            * 订阅 ViewModel,mycount是一个LiveData类型 可以观察
            * */
           viewModel.mycount.observe(this@MainActivity) {
               countTv.text = viewModel.mycount.value.toString()
          }
    }
       
    在第二个Activity中也是类似...

    这样就可以实现效果图啦🏀


    案例二:同一个Activity下的两个Fragment共享一个ViewModel


    话不多说,先上效果图


    2022-06-21-12-05-00.gif
    这个效果就很简单了,在同一个Activity下,有两个Fragment,这两个Fragment共享一个ViewModel


    这个案例主要是想带大家了解一下ViewModel在Fragment中的使用


    第一步:依旧是创建ViewModel


    class BlankViewModel : ViewModel() {
       private val numberLiveData = MutableLiveData<Int>()

       private var i = 0
       fun getLiveData(): LiveData<Int> {
           return numberLiveData
      }

       fun addOne(){
           i++
           numberLiveData.value = i
      }
    }

    非常简单的一个ViewModel


    第二步:在Fragment中使用


    //左Fragment
    class LeftFragment : Fragment() {

       private val viewModel:BlankViewModel by activityViewModels()
       override fun onCreateView(
           inflater: LayoutInflater, container: ViewGroup?,
           savedInstanceState: Bundle?
      ): View? {
           return inflater.inflate(R.layout.fragment_left, container, false)
      }

       override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
           super.onViewCreated(view, savedInstanceState)
           //对+1按钮监听
           left_button.setOnClickListener {
               viewModel.addOne()
          }
           activity?.let {it ->
               viewModel.getLiveData().observe(it){
                   left_text.text = it.toString()
              }
          }
      }
    }

    //右Fragment
    class RightFragment : Fragment() {
       private val viewModel: BlankViewModel by activityViewModels()
       
       override fun onCreateView(
           inflater: LayoutInflater, container: ViewGroup?,
           savedInstanceState: Bundle?
      ): View? {
           return inflater.inflate(R.layout.fragment_right, container, false)
      }

       override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
           super.onViewCreated(view, savedInstanceState)
           right_button.setOnClickListener {
               viewModel.addOne()
          }
           activity?.let { it ->
               viewModel.getLiveData().observe(it) {
                   right_text.text = it.toString()
              }
          }
      }
    }

    这样,这个简单的案例就实现啦。


    尾述


    终于把LiveData和ViewModel的大致使用讲解了一遍,但仅仅这样还是不够的,你还需要在更多更多的实践中去熟悉,去深入学习....


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

    日本后端女开发吐槽:不要找中国程序员!

    最近,一个在日本工作的中国程序媛在小红书发帖,称找对象不要找国产程序员,为了说明原因,她还将国产程序员分成了九种类别,分别列出了优缺点,具体如下: 为什么我说找对象不要找国产程序员 提前声明:本文内容十分主观,欢迎不同意见探讨,但不友好的评论会删。 本科和硕士...
    继续阅读 »

    最近,一个在日本工作的中国程序媛在小红书发帖,称找对象不要找国产程序员,为了说明原因,她还将国产程序员分成了九种类别,分别列出了优缺点,具体如下:

    为什么我说找对象不要找国产程序员
    提前声明:本文内容十分主观,欢迎不同意见探讨,但不友好的评论会删。
    本科和硕士都是不错的学校,毕业后做后端开发,同学和前同事在中国美国各种大厂的都有,也算接触了许许多多这个行业的人,自认为有点发言权,但是非常非常主观。
    总看到小红书上有人问怎么找大厂程序员男朋友,或者罗列程序员对象的种种好处。我的内心是黑人问号的鹏国产男程序员一般分为以下几种:
    1.技术牛逼,大厂高薪,但是极度装逼,会点代码就用鼻孔看人的那种,欺骗其他行业的小妹妹们有一手,但是会被同行女生厌恶的类型。见过最多就是这种。
    2.技术牛逼,大厂高薪,但邋遢,肥胖,且油腻。找女朋友就像是找了个免费保姆。如果有信心改造他可以上。
    3.技术牛逼,大厂高薪,但人多少有点变态,有一些重口味的爱好,不深入了解不会知道。
    4.技术牛逼,大厂高薪,装一点小逼,喝一点小酒,人长得帅还懂健身。这种一般是海王没跑。5.技术薪资一般,但情商极低,聊两句就想拉黑的那种,传说中的注孤生。
    6.技术薪资一般,但是喜欢强行装逼,这种甚至还不如1乐。
    7.技术薪资一般,不怎么装逼,一般长相,传说中的老实人。但是跟别的行业比这类并不香。
    8.各方面条件优秀,但业务能力极差,被同行唾弃的那种,遇上这类还是避开吧,后代智商不行。
    9.技术牛逼,大厂高薪,不爱装逼,外表中上,坚持运动,认真专一,有正常爱好还会搞点浪漫已婚or有青梅竹马的恋情,毕竟是人间极品怎会被剩在那里但是接触过的其他国家优秀的程序员大都感观不错,至少不会随时随地装逼盛。

    纯属调侃,请大家不要对号入座五
    #程序员

    可以看出,在这九类里,其他八类都各有缺点,只有最后一种是“人间极品”,可惜早已是他人夫。

    针对楼主的言论,网友说,中国人在日本做程序员,大多数都是外包。


    有人说,日本程序员的水平太差,文科生去都能比划两下,去日本做IT,估计是在国内混不下去了。


    有人说,在小红书找程序员男友的,不都是想找人形ATM机吗?


    有人说,在小红书看看旅游攻略和送女朋友的礼物就行了,其他东西少看,没什么意义。


    一位日本留学生说,自己在国内和日本的大厂都实习过,大家都挺好的,哪里都有素质高的,哪里都有素质低的,但感觉国内互联网的工作效率和个人成长比国外快。


    还有人说,物以类聚,人以群分,楼主应该反思自己的圈子,程序员是一个非常庞大的群体,林子大了什么鸟都有,楼主这是一叶障目,不见泰山。


    但依然有许多程序员表示楼主说的没错,国产程序员大多数都是这样,真正优质的技术男根本不缺对象,剩下的单身男女程序员质量都没法看。


    有人说,一开始不相信楼主说的,但看到重口味爱好开始有点信了。


    也有人好奇,想知道重口味爱好指哪些?


    还有不少程序员已经开始自发认领自己的类型:


    也有人说,楼主的说法和程序员没关系,换个行业也能用。


    一位网友吆喝着要收国产程序员,反正有这么多缺点,别人也不要,干脆回收算了。


    楼主的分类既可以覆盖程序员群体,也可以覆盖到其他圈子。就算是国外的技术男,也基本是这些类型,没必要非得强调“国产”两字。说白了,坏男人哪个行业都有,哪个国家都有。

    说回国产程序员这个群体。正如网友所说,这是一个很大的群体,什么样的人都有,既有优质的“人间极品”,也有油腻的“装逼”男,不能一棍子打翻一船人,类似“找对象不能找国产程序员”的说法着实有些武断了。

    归根结底,找对象看重的还是人品和性格,程序员作为高薪群体,在婚恋市场上本来就有一定的竞争优势,如果性格人品合适,完全可以结为良缘。

    来源:mp.weixin.qq.com/s/EQEaou-JOceZK2OhBceiWw

    收起阅读 »

    帮我做一个几千万用户的小网站...

    来源:不会笑青年(laughyouth)












    来源:不会笑青年(laughyouth)

    代码review,瑞出事来了!

    不久之前,部门进行了一次代码评审。 代码整体比较简单,该吹B的地方都已经吹过了,无非是些if else的老问题而已。当翻到一段定时任务的一步执行代码时,我的双眼一亮,觉得该BB两句了。 谁知这群家伙,评审的时候满满的认同感,但评审结束不久,就给我冠了个事B的称...
    继续阅读 »

    不久之前,部门进行了一次代码评审。


    代码整体比较简单,该吹B的地方都已经吹过了,无非是些if else的老问题而已。当翻到一段定时任务的一步执行代码时,我的双眼一亮,觉得该BB两句了。


    谁知这群家伙,评审的时候满满的认同感,但评审结束不久,就给我冠了个事B的称号。


    今天我就把当时的这些话儿整理整理,让大家说道说道,我到底是不是个事B。淦!


    一个任务处理例子


    代码的结构大体是这样的。


    通过定时,这段代码每天晚上凌晨都要对数据库的记录进行一遍对账。主要的逻辑,就是使用独立的线程,渐进式的读取数据库中的相关记录,然后把这些记录,放在循环中逐条进行处理。


    ExecutorService service = Executors.newFixedThreadPool(10);
    ...
    service.submit(()->{
        while(true){
            if(CollectionUtils.isEmpty(items)){
                break;
            }
            List<Data> items = queryPageData(start, end); // 分页逻辑
            for(Data item : items){
                try {
                    Thread.sleep(10L);
                } catch (InterruptedException e) {
                    //noop 
                }
                processItem(item);
            }
        }
    });


    等一下。在代码马上被翻过去的时候,我叫停了,这里的processItem没有捕获异常


    通常情况下,这不会有什么问题。但静好的岁月,总是偶尔会被一些随机的事故打断。如果这是你任务的完整代码,那它就有一种非常隐晦的故障处理方式。即使你的单元测试写的再好,这段代码我们依然可以通过远程投毒的方式,通过问题记录来让它产生问题。


    是的。以上代码的根本原因,就是没有捕捉processItem函数可能产生的异常。如果在记录处理的时候,有任何一条抛出了异常,不管是checked异常还是unchecked异常,整个任务的执行都会终止!


    不要觉得简单哦,踩过这个坑的同学,请记得扣个666。或者翻一下你的任务执行代码,看看是不是也有这个问题。


    Java编译器在很多情况下都会提示你把异常给捕捉了,但总有些异常会逃出去,比如空指针异常。如下图,RuntimeException和Error都属于unchecked异常。


    图片


    RuntimeException可以不用try...catch进行处理,但是如果一旦出现异常,则会导致程序中断执行,JVM将统一处理这些异常。


    你捕捉不到它,它自然会让你的任务完蛋。


    如果你想要异步的执行一些任务,最好多花一点功夫到异常设计上面。在这上面翻车的同学比比皆是,这辆车并不介意再带上你一个。


    评审的小伙很谦虚,马上就现场修改了代码。


    不要生吞异常


    且看修改后的代码。


    ExecutorService service = Executors.newFixedThreadPool(10);
    ...
    service.submit(()->{
        while(true){
            if(CollectionUtils.isEmpty(items)){
                break;
            }
            List<Data> items = queryPageData(start, end); // 分页逻辑
            for(Data item : items){
                try {
                    Thread.sleep(10L);
                } catch (InterruptedException e) {
                    //noop 
                }
                try{
                    processItem(item);
                }catch(Exception ex){
                    LOG.error(...,ex);
                }
            }
        }
    });
    ...
    service.shutdownNow();


    为了控制任务执行的频率,sleep大法是个有效的方法。


    代码里考虑的很周到,按照我们上述的方式捕捉了异常。同时,还很贴心的把sleep相关的异常也给捕捉了。这里不贴心也没办法,因为不补齐这部分代码的话,编译无法通过,我们姑且认为是开发人员的水平够屌。


    由于sleep抛出的是InterruptedException,所以代码什么也没处理。这也是我们代码里常见的操作。不信打开你的项目,忽略InterruptedException的代码肯定多如牛毛。


    此时,你去执行这段代码,虽然线程池使用了暴力的shutdownNow函数,但你的代码依然无法终止,它将一直run下去。因为你忽略了InterruptedException异常。


    当然,我们可以在捕捉到InterruptedException的时候,终止循环。


    try {
        Thread.sleep(10L);
    } catch (InterruptedException e) {
        break;
    }


    虽然这样能够完成预期,但一般InterruptedException却不是这么处理的。正确的处理方式是这样的:


    while (true) {
        Thread currentThread = Thread.currentThread();
        if(currentThread.isInterrupted()){
            break;
        }
        try {
            Thread.sleep(1L);
        } catch (InterruptedException e) {
            currentThread.interrupt();
        }
    }


    除了捕捉它,我们还要再次把interrupt状态给复位,否则它就随着捕捉给清除了。InterruptedException在很多场景非常的重要。当有些方法一直阻塞着线程,比如耗时的计算,会让整个线程卡在那里什么都干不了,InterruptedException可以中断任务的执行,是非常有用的。


    但是对我们现在代码的逻辑来说,并没有什么影响。被评审的小伙伴不满意的说。


    还有问题!


    有没有影响是一回事,是不是好的习惯是另一回事 。我尽量的装了一下B,其实,你的异常处理代码里还有另外隐藏的问题。


    还有什么问题?,大家都一改常日慵懒的表情,你倒是说说


    图片


    我们来看一下小伙伴现场改的问题。他直接使用catch捕获了这里的异常,然后记录了相应的日志。我要说的问题是,这里的Exception粒度是不对的,太粗鲁。


    try{
        processItem(item);
    }catch(Exception ex){
        LOG.error(...,ex);
    }


    processItem函数抛出了IOException,同时也抛出了InterruptedException,但我们都一致对待为普通的Exception,这样就无法体现上层函数抛出异常的意图。


    比如processItem函数抛出了一个TimeoutExcepiton,期望我们能够基于它做一些重试;或者抛出了SystemBusyExcption,期望我们能够多sleep一会,给服务器一点时间。这种粗粒度的异常一股脑的将它们捕捉,在新异常添加的时候根本无法发现这些代码,会发生风险。


    一时间会议室里寂静无比。


    我觉得你说的很对 ,一位比较资深的老鸟说, 你的意思是把所有的异常情况都分别捕捉,进行精细化处理。但最后你还是要使用Exception来捕捉RuntimeException,异常还是捕捉不到啊


    果然是不同凡响的发问。


    优秀的、标准的代码写法,其中无法实施的一个重要因素,就是项目中的其他代码根本不按规矩来。如果我们下层的代码,进行了正确的空指针判断、数组越界操作,或者使用类似guava的Preconditions这类API进行了前置的异常翻译,上面的这种问题根本不用回答。


    但上面这种代码的情况,我们就需要手动的捕捉RuntimeException,进行单独的处理。


    你们这个项目,烂代码太多了,所以不好改。我虽然有情商,但我更有脾气。


    大家不欢而散。


    End


    我实在是想不通,代码review就是用来发现问题的。结果这review会一开下来,大家都在背后讽刺我。这到底是我的问题呢?还是这个团队的问题呢?让人搞不懂。


    你们在纠结使用Integer还是int的时候,我也没说什么呀,现在就谈点异常处理的问题,就那么玻璃心受不了了。这B不能全都让你们装了啊。


    什么?你要review一下我的代码?看看我到底有没有像我说的一样写代码,有没有以身作则?是在不好意思,我可是架构师哎,我已经很多年没写代码了。


    你的这个愿望让你落空了!


    图片


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

    领导:谁再用定时任务实现关闭订单,立马滚蛋!

    在电商、支付等领域,往往会有这样的场景,用户下单后放弃支付了,那这笔订单会在指定的时间段后进行关闭操作,细心的你一定发现了像某宝、某东都有这样的逻辑,而且时间很准确,误差在1s内;那他们是怎么实现的呢? 一般的做法有如下几种 定时任务关闭订单 rock...
    继续阅读 »

    在电商、支付等领域,往往会有这样的场景,用户下单后放弃支付了,那这笔订单会在指定的时间段后进行关闭操作,细心的你一定发现了像某宝、某东都有这样的逻辑,而且时间很准确,误差在1s内;那他们是怎么实现的呢?


    一般的做法有如下几种




    • 定时任务关闭订单




    • rocketmq延迟队列




    • rabbitmq死信队列




    • 时间轮算法




    • redis过期监听




    一、定时任务关闭订单(最low)


    一般情况下,最不推荐的方式就是关单方式就是定时任务方式,原因我们可以看下面的图来说明


    image.png


    我们假设,关单时间为下单后10分钟,定时任务间隔也是10分钟;通过上图我们看出,如果在第1分钟下单,在第20分钟的时候才能被扫描到执行关单操作,这样误差达到10分钟,这在很多场景下是不可接受的,另外需要频繁扫描主订单号造成网络IO和磁盘IO的消耗,对实时交易造成一定的冲击,所以PASS


    二、rocketmq延迟队列方式


    延迟消息
    生产者把消息发送到消息服务器后,并不希望被立即消费,而是等待指定时间后才可以被消费者消费,这类消息通常被称为延迟消息。
    在RocketMQ开源版本中,支持延迟消息,但是不支持任意时间精度的延迟消息,只支持特定级别的延迟消息。
    消息延迟级别分别为1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h,共18个级别。


    发送延迟消息(生产者)


    /**
    * 推送延迟消息
    * @param topic
    * @param body
    * @param producerGroup
    * @return boolean
    */
    public boolean sendMessage(String topic, String body, String producerGroup)
    {
    try
    {
    Message recordMsg = new Message(topic, body.getBytes());
    producer.setProducerGroup(producerGroup);

    //设置消息延迟级别,我这里设置14,对应就是延时10分钟
    // "1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h"
    recordMsg.setDelayTimeLevel(14);
    // 发送消息到一个Broker
    SendResult sendResult = producer.send(recordMsg);
    // 通过sendResult返回消息是否成功送达
    log.info("发送延迟消息结果:======sendResult:{}", sendResult);
    DateFormat format =new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    log.info("发送时间:{}", format.format(new Date()));

    return true;
    }
    catch (Exception e)
    {
    e.printStackTrace();
    log.error("延迟消息队列推送消息异常:{},推送内容:{}", e.getMessage(), body);
    }
    return false;
    }


    消费延迟消息(消费者)


    /**
    * 接收延迟消息
    *
    * @param topic
    * @param consumerGroup
    * @param messageHandler
    */
    public void messageListener(String topic, String consumerGroup, MessageListenerConcurrently messageHandler)
    {
    ThreadPoolUtil.execute(() ->
    {
    try
    {
    DefaultMQPushConsumer consumer = new DefaultMQPushConsumer();
    consumer.setConsumerGroup(consumerGroup);
    consumer.setVipChannelEnabled(false);
    consumer.setNamesrvAddr(address);
    //设置消费者拉取消息的策略,*表示消费该topic下的所有消息,也可以指定tag进行消息过滤
    consumer.subscribe(topic, "*");
    //消费者端启动消息监听,一旦生产者发送消息被监听到,就打印消息,和rabbitmq中的handlerDelivery类似
    consumer.registerMessageListener(messageHandler);
    consumer.start();
    log.info("启动延迟消息队列监听成功:" + topic);
    }
    catch (MQClientException e)
    {
    log.error("启动延迟消息队列监听失败:{}", e.getErrorMessage());
    System.exit(1);
    }
    });
    }


    实现监听类,处理具体逻辑


    /**
    * 延迟消息监听
    *
    */
    @Component
    public class CourseOrderTimeoutListener implements ApplicationListener<ApplicationReadyEvent>
    {

    @Resource
    private MQUtil mqUtil;

    @Resource
    private CourseOrderTimeoutHandler courseOrderTimeoutHandler;

    @Override
    public void onApplicationEvent(ApplicationReadyEvent applicationReadyEvent)
    {
    // 订单超时监听
    mqUtil.messageListener(EnumTopic.ORDER_TIMEOUT, EnumGroup.ORDER_TIMEOUT_GROUP, courseOrderTimeoutHandler);
    }
    }



    /**
    * 实现监听
    */
    @Slf4j
    @Component
    public class CourseOrderTimeoutHandler implements MessageListenerConcurrently
    {

    @Override
    public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
    for (MessageExt msg : list)
    {
    // 得到消息体
    String body = new String(msg.getBody());
    JSONObject userJson = JSONObject.parseObject(body);
    TCourseBuy courseBuyDetails = JSON.toJavaObject(userJson, TCourseBuy.class);

    // 处理具体的业务逻辑,,,,,

    DateFormat format =new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    log.info("消费时间:{}", format.format(new Date()));

    return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
    }
    }


    这种方式相比定时任务好了很多,但是有一个致命的缺点,就是延迟等级只有18种(商业版本支持自定义时间),如果我们想把关闭订单时间设置在15分钟该如何处理呢?显然不够灵活。


    三、rabbitmq死信队列的方式


    Rabbitmq本身是没有延迟队列的,只能通过Rabbitmq本身队列的特性来实现,想要Rabbitmq实现延迟队列,需要使用Rabbitmq的死信交换机(Exchange)和消息的存活时间TTL(Time To Live)


    死信交换机
    一个消息在满足如下条件下,会进死信交换机,记住这里是交换机而不是队列,一个交换机可以对应很多队列。


    一个消息被Consumer拒收了,并且reject方法的参数里requeue是false。也就是说不会被再次放在队列里,被其他消费者使用。
    上面的消息的TTL到了,消息过期了。


    队列的长度限制满了。排在前面的消息会被丢弃或者扔到死信路由上。
    死信交换机就是普通的交换机,只是因为我们把过期的消息扔进去,所以叫死信交换机,并不是说死信交换机是某种特定的交换机


    消息TTL(消息存活时间)
    消息的TTL就是消息的存活时间。RabbitMQ可以对队列和消息分别设置TTL。对队列设置就是队列没有消费者连着的保留时间,也可以对每一个单独的消息做单独的设置。超过了这个时间,我们认为这个消息就死了,称之为死信。如果队列设置了,消息也设置了,那么会取值较小的。所以一个消息如果被路由到不同的队列中,这个消息死亡的时间有可能不一样(不同的队列设置)。这里单讲单个消息的TTL,因为它才是实现延迟任务的关键。


    byte[] messageBodyBytes = "Hello, world!".getBytes();  
    AMQP.BasicProperties properties = new AMQP.BasicProperties();
    properties.setExpiration("60000");
    channel.basicPublish("my-exchange", "queue-key", properties, messageBodyBytes);


    可以通过设置消息的expiration字段或者x-message-ttl属性来设置时间,两者是一样的效果。只是expiration字段是字符串参数,所以要写个int类型的字符串:当上面的消息扔到队列中后,过了60秒,如果没有被消费,它就死了。不会被消费者消费到。这个消息后面的,没有“死掉”的消息对顶上来,被消费者消费。死信在队列中并不会被删除和释放,它会被统计到队列的消息数中去


    处理流程图


    image.png


    创建交换机(Exchanges)和队列(Queues)


    创建死信交换机


    image.png


    如图所示,就是创建一个普通的交换机,这里为了方便区分,把交换机的名字取为:delay


    创建自动过期消息队列
    这个队列的主要作用是让消息定时过期的,比如我们需要2小时候关闭订单,我们就需要把消息放进这个队列里面,把消息过期时间设置为2小时


    image.png


    创建一个一个名为delay_queue1的自动过期的队列,当然图片上面的参数并不会让消息自动过期,因为我们并没有设置x-message-ttl参数,如果整个队列的消息有消息都是相同的,可以设置,这里为了灵活,所以并没有设置,另外两个参数x-dead-letter-exchange代表消息过期后,消息要进入的交换机,这里配置的是delay,也就是死信交换机,x-dead-letter-routing-key是配置消息过期后,进入死信交换机的routing-key,跟发送消息的routing-key一个道理,根据这个key将消息放入不同的队列


    创建消息处理队列
    这个队列才是真正处理消息的队列,所有进入这个队列的消息都会被处理


    image.png


    消息队列的名字为delay_queue2
    消息队列绑定到交换机
    进入交换机详情页面,将创建的2个队列(delayqueue1和delayqueue2)绑定到交换机上面


    image.png
    自动过期消息队列的routing key 设置为delay
    绑定delayqueue2


    image.png


    delayqueue2 的key要设置为创建自动过期的队列的x-dead-letter-routing-key参数,这样当消息过期的时候就可以自动把消息放入delay_queue2这个队列中了
    绑定后的管理页面如下图:


    image.png


    当然这个绑定也可以使用代码来实现,只是为了直观表现,所以本文使用的管理平台来操作
    发送消息


    String msg = "hello word";  
    MessageProperties messageProperties = newMessageProperties();
    messageProperties.setExpiration("6000");
    messageProperties.setCorrelationId(UUID.randomUUID().toString().getBytes());
    Message message = newMessage(msg.getBytes(), messageProperties);
    rabbitTemplate.convertAndSend("delay", "delay",message);


    设置了让消息6秒后过期
    注意:因为要让消息自动过期,所以一定不能设置delay_queue1的监听,不能让这个队列里面的消息被接受到,否则消息一旦被消费,就不存在过期了


    接收消息
    接收消息配置好delay_queue2的监听就好了


    package wang.raye.rabbitmq.demo1;
    import org.springframework.amqp.core.AcknowledgeMode;
    import org.springframework.amqp.core.Binding;
    import org.springframework.amqp.core.BindingBuilder;
    import org.springframework.amqp.core.DirectExchange;
    import org.springframework.amqp.core.Message;
    import org.springframework.amqp.core.Queue;
    import org.springframework.amqp.rabbit.connection.CachingConnectionFactory;
    import org.springframework.amqp.rabbit.connection.ConnectionFactory;
    import org.springframework.amqp.rabbit.core.ChannelAwareMessageListener;
    import org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    @Configuration
    publicclassDelayQueue{
    /** 消息交换机的名字*/
    publicstaticfinalString EXCHANGE = "delay";
    /** 队列key1*/
    publicstaticfinalString ROUTINGKEY1 = "delay";
    /** 队列key2*/
    publicstaticfinalString ROUTINGKEY2 = "delay_key";
    /**
    * 配置链接信息
    * @return
    */
    @Bean
    publicConnectionFactory connectionFactory() {
    CachingConnectionFactory connectionFactory = newCachingConnectionFactory("120.76.237.8",5672);
    connectionFactory.setUsername("kberp");
    connectionFactory.setPassword("kberp");
    connectionFactory.setVirtualHost("/");
    connectionFactory.setPublisherConfirms(true); // 必须要设置
    return connectionFactory;
    }
    /**
    * 配置消息交换机
    * 针对消费者配置
    FanoutExchange: 将消息分发到所有的绑定队列,无routingkey的概念
    HeadersExchange :通过添加属性key-value匹配
    DirectExchange:按照routingkey分发到指定队列
    TopicExchange:多关键字匹配
    */
    @Bean
    publicDirectExchange defaultExchange() {
    returnnewDirectExchange(EXCHANGE, true, false);
    }
    /**
    * 配置消息队列2
    * 针对消费者配置
    * @return
    */
    @Bean
    publicQueue queue() {
    returnnewQueue("delay_queue2", true); //队列持久
    }
    /**
    * 将消息队列2与交换机绑定
    * 针对消费者配置
    * @return
    */
    @Bean
    @Autowired
    publicBinding binding() {
    returnBindingBuilder.bind(queue()).to(defaultExchange()).with(DelayQueue.ROUTINGKEY2);
    }
    /**
    * 接受消息的监听,这个监听会接受消息队列1的消息
    * 针对消费者配置
    * @return
    */
    @Bean
    @Autowired
    publicSimpleMessageListenerContainer messageContainer2(ConnectionFactory connectionFactory) {
    SimpleMessageListenerContainer container = newSimpleMessageListenerContainer(connectionFactory());
    container.setQueues(queue());
    container.setExposeListenerChannel(true);
    container.setMaxConcurrentConsumers(1);
    container.setConcurrentConsumers(1);
    container.setAcknowledgeMode(AcknowledgeMode.MANUAL); //设置确认模式手工确认
    container.setMessageListener(newChannelAwareMessageListener() {
    publicvoid onMessage(Message message, com.rabbitmq.client.Channel channel) throwsException{
    byte[] body = message.getBody();
    System.out.println("delay_queue2 收到消息 : "+ newString(body));
    channel.basicAck(message.getMessageProperties().getDeliveryTag(), false); //确认消息成功消费
    }
    });
    return container;
    }
    }


    这种方式可以自定义进入死信队列的时间;是不是很完美,但是有的小伙伴的情况是消息中间件就是rocketmq,公司也不可能会用商业版,怎么办?那就进入下一节


    四、时间轮算法


    image.png


    (1)创建环形队列,例如可以创建一个包含3600个slot的环形队列(本质是个数组)


    (2)任务集合,环上每一个slot是一个Set
    同时,启动一个timer,这个timer每隔1s,在上述环形队列中移动一格,有一个Current Index指针来标识正在检测的slot。


    Task结构中有两个很重要的属性:
    (1)Cycle-Num:当Current Index第几圈扫描到这个Slot时,执行任务
    (2)订单号,要关闭的订单号(也可以是其他信息,比如:是一个基于某个订单号的任务)


    假设当前Current Index指向第0格,例如在3610秒之后,有一个订单需要关闭,只需:
    (1)计算这个订单应该放在哪一个slot,当我们计算的时候现在指向1,3610秒之后,应该是第10格,所以这个Task应该放在第10个slot的Set中
    (2)计算这个Task的Cycle-Num,由于环形队列是3600格(每秒移动一格,正好1小时),这个任务是3610秒后执行,所以应该绕3610/3600=1圈之后再执行,于是Cycle-Num=1


    Current Index不停的移动,每秒移动到一个新slot,这个slot中对应的Set,每个Task看Cycle-Num是不是0:
    (1)如果不是0,说明还需要多移动几圈,将Cycle-Num减1
    (2)如果是0,说明马上要执行这个关单Task了,取出订单号执行关单(可以用单独的线程来执行Task),并把这个订单信息从Set中删除即可。
    (1)无需再轮询全部订单,效率高
    (2)一个订单,任务只执行一次
    (3)时效性好,精确到秒(控制timer移动频率可以控制精度)


    五、redis过期监听


    1.修改redis.windows.conf配置文件中notify-keyspace-events的值
    默认配置notify-keyspace-events的值为 ""
    修改为 notify-keyspace-events Ex 这样便开启了过期事件


    2. 创建配置类RedisListenerConfig(配置RedisMessageListenerContainer这个Bean)


    package com.zjt.shop.config;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.data.redis.connection.RedisConnectionFactory;
    import org.springframework.data.redis.core.RedisTemplate;
    import org.springframework.data.redis.listener.RedisMessageListenerContainer;
    import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
    import org.springframework.data.redis.serializer.StringRedisSerializer;


    @Configuration
    public class RedisListenerConfig {

    @Autowired
    private RedisTemplate redisTemplate;

    /**
    * @return
    */
    @Bean
    public RedisTemplate redisTemplateInit() {

    // key序列化
    redisTemplate.setKeySerializer(new StringRedisSerializer());

    //val实例化
    redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());

    return redisTemplate;
    }

    @Bean
    RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory) {
    RedisMessageListenerContainer container = new RedisMessageListenerContainer();
    container.setConnectionFactory(connectionFactory);
    return container;
    }

    }


    3.继承KeyExpirationEventMessageListener创建redis过期事件的监听类


    package com.zjt.shop.common.util;
    import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
    import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
    import com.zjt.shop.modules.order.service.OrderInfoService;
    import com.zjt.shop.modules.product.entity.OrderInfoEntity;
    import com.zjt.shop.modules.product.mapper.OrderInfoMapper;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.data.redis.connection.Message;
    import org.springframework.data.redis.listener.KeyExpirationEventMessageListener;
    import org.springframework.data.redis.listener.RedisMessageListenerContainer;
    import org.springframework.stereotype.Component;


    @Slf4j
    @Component
    public class RedisKeyExpirationListener extends KeyExpirationEventMessageListener {

    public RedisKeyExpirationListener(RedisMessageListenerContainer listenerContainer) {
    super(listenerContainer);
    }

    @Autowired
    private OrderInfoMapper orderInfoMapper;

    /**
    * 针对redis数据失效事件,进行数据处理
    * @param message
    * @param pattern
    */
    @Override
    public void onMessage(Message message, byte[] pattern) {
    try {
    String key = message.toString();
    //从失效key中筛选代表订单失效的key
    if (key != null && key.startsWith("order_")) {
    //截取订单号,查询订单,如果是未支付状态则为-取消订单
    String orderNo = key.substring(6);
    QueryWrapper<OrderInfoEntity> queryWrapper = new QueryWrapper<>();
    queryWrapper.eq("order_no",orderNo);
    OrderInfoEntity orderInfo = orderInfoMapper.selectOne(queryWrapper);
    if (orderInfo != null) {
    if (orderInfo.getOrderState() == 0) { //待支付
    orderInfo.setOrderState(4); //已取消
    orderInfoMapper.updateById(orderInfo);
    log.info("订单号为【" + orderNo + "】超时未支付-自动修改为已取消状态");
    }
    }
    }
    } catch (Exception e) {
    e.printStackTrace();
    log.error("【修改支付订单过期状态异常】:" + e.getMessage());
    }
    }
    }


    4:测试
    通过redis客户端存一个有效时间为3s的订单:


    image.png


    结果:


    image.png


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

    什么是响应式编程:以RxJava为例

    RxJava思想 文章概述: 本文围绕Rx编程思想(响应式编程)进行深入细致探讨;以获取服务器图片为例,通过传统方式与Rx方式对比进一步体现Rx 编程方式的魅力;借助卡片式编程思想,对Rx编程方式进行第一次优化;借助 Java泛型对Rx编程进一步优化; ...
    继续阅读 »

    RxJava思想




    • 文章概述:



      • 本文围绕Rx编程思想(响应式编程)进行深入细致探讨;以获取服务器图片为例,通过传统方式与Rx方式对比进一步体现Rx 编程方式的魅力;借助卡片式编程思想,对Rx编程方式进行第一次优化;借助 Java泛型对Rx编程进一步优化;




    Rx编程出现背景:改变思维来提升效率




    • 通过事件流动,推进业务执行




      • 从起点到终点,逻辑严密



        • 下一层依赖上一层:体现在函数参数




      • 链式调用只是里面的一环




      • 样例:每一层逻辑上关联



        • 起点(分发事件:点击登录)----------登录API-------请求服务器--------获取响应码----------> 终点(更新UI登录成功 消费事件)






    RxJava 配合 Retrofit




    • 业务逻辑:



      • Retrofit通过OKHHTTP请求服务器拿到响应码,交给RxJava由RxJava处理数据




    • 防抖:



      • 一秒钟点击了20次,只响应一次




    • 网络嵌套:拿到主数据再拿到item数据




    • doNext运用:异步与主线之间频繁切换



      • 异步线程A拿到数据,切换至UI线程更新,再次切换到异步线程B,再拿到UI线程




    对比说明Rx 编程优势:统一业务代码逻辑



    • 主要内容:以获取服务器图片为例,通过传统方式与Rx方式对比进一步体现Rx 编程方式的魅力;


    传统模式获取图片




    • 实现效果:


      image-20220621192036883




    • 传统编写思路:




      • 弹出加载框




      • 开启异步线程:此时有多种途径



        • 封装方法....

        • 全部写在一起

        • new Thread

        • 使用 线程池




      • 将从服务器获取的图片转成Bitmap




      • 从异步线程切换至UI线程更新UI




      • 代码实现:


         public void downloadImageAction(View view) {
             progressDialog = new ProgressDialog(this);
             progressDialog.setTitle("下载图片中...");
             progressDialog.show();
         
             //       异步线程处理耗时任务
             new Thread(new Runnable() {
                 @Override
                 public void run() {
                     try {
                         URL url = new URL(PATH);
                         HttpURLConnection httpURLConnection = (HttpURLConnection) url.openConnection();
                         httpURLConnection.setConnectTimeout(5000);
                         int responseCode = httpURLConnection.getResponseCode(); // 才开始 request
                         if (responseCode == HttpURLConnection.HTTP_OK) {
                             InputStream inputStream = httpURLConnection.getInputStream();
                             //                       图片丢给bitmap
                             Bitmap bitmap = BitmapFactory.decodeStream(inputStream);
                             //                       使用Handler 进行切换
                             Message message = handler.obtainMessage();
                             message.obj = bitmap;
                             handler.sendMessage(message);
                        }
                    } catch (Exception e) {
                         e.printStackTrace();
                    }
                }
            }).start();
         }
         
         //   使用Handler处理问题
         private final Handler handler = new Handler(new Handler.Callback() {
         
             @Override
             public boolean handleMessage(@NonNull Message msg) {
                 Bitmap bitmap = (Bitmap) msg.obj;
                 image.setImageBitmap(bitmap);
         
                 if (progressDialog != null) progressDialog.dismiss();
                 return false;
            }
         });








    • 传统方式弊端:




      • 在具体实现(切换线程)时,因为思维不统一,导致实现方式不同






    RxJava思路:采用观察者设计模式,实现响应式(Rx)编程




    • 以事件流动推进业务执行




    • 角色:




      • 起点:被观察者(为其分配异步线程--->请求服务器)


         // 起点
         Observable.just(PATH)  // 内部会分发 PATH Stirng // TODO 第二步



      • 终点:观察者(为其分配UI线程--->更新UI)


         //终点
         .subscribe(
             new Observer<Bitmap>() {
                 //订阅
                 @Override
                 public void onSubscribe(Disposable d) {
                   
                }
                 //拿到事件:因为上一层是一个String类型的Path事件
                 @Override
                 public void onNext(@NonNull Bitmap bitmap) {
                     image.setImageBitmap(bitmap);
                }
         
                 // 错误事件
                 @Override
                 public void onError(Throwable e) {
         
                }
         
                 // 完成事件
                 @Override
                 public void onComplete() {
                 
                }
            });





    • 编写思路:框架在实际使用中是U型逻辑(终点--->起点--->终点--->……)




      • 第一步:处理终点中拿到事件后的业务逻辑


         //拿到事件:因为上一层是一个String类型的Path事件
         @Override
         public void onNext(@NonNull String s) {
             image.setImageBitmap(bitmap);
         }



        • 细节:onNext的参数问题




          • Rx 整体是以事件流动推进业务逻辑,如果上一层是String类型的事件(Path)那么它的下一层应该也是String类型的事件(参数为String类型)




          • 但Rx 中根据业务进行事件的拦截



            • A层(String事件),B层(Bitmap事件),逻辑为A层--->B层

            • 那么就需要在A层到B层之间添加一个拦截器,进行事件转换








      • 第二步:在起点与终点之间添加拦截器




        • 为什么要添加拦截器:业务需求是拿到一个Bitmap而起点提供的是String类型的事件




        • 拦截器为map(K,V):K为上层事件,V为下层事件


           //上层事件为String类型,由系统自动推断;但此时拦截器并不知道下一层是什么事件,因此为Object
           .map(new Function<String, Object>() {
           })






        • 终点要求Bitmap事件


           //根据业务将map 中的value改为 Bitmap类型
           .map(new Function<String, Object>() {
           })



          • 终点完成事件(onNext报错,联动变化):注意由Rx思想决定,那么终点处的完成事件参数因为Bitmap


             //由Rx思想决定,那么终点处的完成事件参数因为Bitmap
             @Override
             public void onNext(@NonNull Bitmap bitmap) {
             image.setImageBitmap(bitmap);
             }



          • 整体事件流向:


            image-20220622223332983








      • 第三步:在拦截器内添加网络请求


         @Override
         public Bitmap apply(@NonNull String s) throws Exception {
         
             //处理网络请求:将String类型的Path事件处理为Bitmap实例
             URL url = new URL(PATH);
             HttpURLConnection httpURLConnection = (HttpURLConnection) url.openConnection();
             int responseCode = httpURLConnection.getResponseCode();
             if(responseCode == httpURLConnection.HTTP_OK){
                 InputStream inputStream = httpURLConnection.getInputStream();
                 Bitmap bitmap = BitmapFactory.decodeStream(inputStream);
                 return bitmap;
            }
             return null;
         }


        • 此时不要使用Handler,因为拦截器已经将String类型事件转为Bitmap类型了,将Bitmap流向终点进行显示




      • 第四步:分配线程




        • 起点到此时拦截器结束,应当分配异步线程(因为需要请求服务器)


           //给上边代码分配异步线程,用于请求服务器
           .subscribeOn(Schedulers.io())



        • 拦截器结束位置到终点处,应当分配UI主线程(因为需要更新UI)


           //给下边的代码分配主线程,用于更新UI
           .observeOn(AndroidSchedulers.mainThread())



          • 分配的主线程跟下面这个是一样的


             // Thread.currentThread().getName(); == Android的主线程,这个跟RxJava切的android主线程是一样的







      • 到此基础功能已经实现,为了使得用户友好,需要添加下列步骤






    Rx 代码优化(一):卡片式编程




    • 什么是卡片式编程:



      • 因为Rx 响应式编程是依靠事件流动推进业务执行,那么我们可以在起点与终点之间添加卡片(拦截器)实现具体的业务功能




    • 代码扩展:点击按钮后立即加载对话框,拿到图片并更新,随后关闭对话框




      • 整体流程:




        • 预处理:点击按钮后,立即加载对话框,开始准备事件分发


           //在终点订阅开始处加载对话框(预处理操作)
           // 订阅开始:一订阅就要显示对话框
           @Override
           public void onSubscribe(Disposable d) {
               //                               第一步:事件分发前预准备
               progressDialog = new ProgressDialog(Test.this);
               progressDialog.setTitle("开始下载");
               progressDialog.show();
           }



        • 第一步:回到起点,开始分发事件


           Observable.just(PATH)



        • 第二步:拦截器工作将String事件转为Bitmap事件(附带网络请求,从服务器拿到数据)




        • 第三步:抵达终点拿到事件处,更新UI


           //拿到事件:因为上一层是一个String类型的Path事件
           @Override
           public void onNext(@NonNull Bitmap bitmap) {
               image.setImageBitmap(bitmap);
           }






        • 第四步:抵达终点完成事件完成处,此时事件整体结束(Rx 编程结束尾巴)


           // 完成事件
           @Override
           public void onComplete() {
               //如果不为空那么就隐藏起来
               if (progressDialog != null)
               progressDialog.dismiss();
           }










    • 这种编程方式成为卡片式编程




      • 好处:后期若需要添加功能,仅需在起点与重点之间添加对应的拦截器,在其中进行处理即可




      • 图片示例:一开始的



        • 事件流动顺序


        image-20220622223332983




        • 运行结果:


          image-20220622231216269









      • 图片示例:此时需要添加个需求,将下载下来的图片添加水印后再展示




        • 事件流动顺序


          image-20220622225848759




        • 添加代码:图片上绘制文字 加水印


           // 图片上绘制文字 加水印
           private final Bitmap drawTextToBitmap(Bitmap bitmap, String text, Paint paint, int paddingLeft, int paddingTop) {
               Bitmap.Config bitmapConfig = bitmap.getConfig();
           
               paint.setDither(true); // 获取跟清晰的图像采样
               paint.setFilterBitmap(true);// 过滤一些
               if (bitmapConfig == null) {
                   bitmapConfig = Bitmap.Config.ARGB_8888;
              }
               bitmap = bitmap.copy(bitmapConfig, true);
               Canvas canvas = new Canvas(bitmap);
           
               canvas.drawText(text, paddingLeft, paddingTop, paint);
               return bitmap;
           }



        • 添加代码:在前面一个拦截器后添加


           .map(new Function<Bitmap, Bitmap>() {
               @Override
               public Bitmap apply(@NonNull Bitmap bitmap) throws Exception {
                   //开始添加水印
                   Paint paint = new Paint();
                   paint.setTextSize(88);
                   paint.setColor(Color.GREEN);
                   return drawTextToBitmap(bitmap,"水印",paint,88,88);
              }
           })



        • 运行结果:从服务器获取图片并添加水印


          image-20220622231334525






      • 还可以添加:及时记录日志等功能






    Rx 代码优化(二):封装代码部分功能提升程序结构



    • 封装线程分配


     //为上游(起点到拦截器结束)分配异步线程,为下游(拦截器结束位置到终点结束)分配android主线程
     private final static <UD> ObservableTransformer<UD,UD> opMixed(){
         return new ObservableTransformer<UD, UD>() {
             @NonNull
             @Override
             //分配线程
             public ObservableSource<UD> apply(@NonNull Observable<UD> upstream) {
                 return upstream.subscribeOn(Schedulers.io()).
                 observeOn(AndroidSchedulers.mainThread())
                 //继续链式调用
                .map(new Function<UD, UD>() {
                     @Override
                     public UD apply(@NonNull UD ud) throws Exception {
                         Log.d(TAG,"日志记录")
                         return ud;
                    }
                })
     
                 //还可以加卡片(拦截器)
                ;
     
            }
     
        };
     }


    • 仅需在终点前调用封装好的库就行了


     ……
     //是需要在终点前调用封装好的东西就行了
     .compose(opMixed())
         //终点
        .subscribe(

    Rx 编程完整代码:


     package com.xiangxue.rxjavademo.downloadimg;
     
     import android.app.ProgressDialog;
     import android.graphics.Bitmap;
     import android.graphics.BitmapFactory;
     import android.graphics.Canvas;
     import android.graphics.Paint;
     import android.os.Bundle;
     import android.os.Handler;
     import android.os.Message;
     import android.util.Log;
     import android.view.View;
     import android.widget.ImageView;
     
     import androidx.annotation.NonNull;
     import androidx.appcompat.app.AppCompatActivity;
     
     import com.xiangxue.rxjavademo.R;
     
     import java.io.InputStream;
     import java.net.HttpURLConnection;
     import java.net.URL;
     
     import io.reactivex.Observable;
     import io.reactivex.ObservableSource;
     import io.reactivex.ObservableTransformer;
     import io.reactivex.Observer;
     import io.reactivex.android.schedulers.AndroidSchedulers;
     import io.reactivex.disposables.Disposable;
     import io.reactivex.functions.Function;
     import io.reactivex.schedulers.Schedulers;
     
     public class Test extends AppCompatActivity {
     
         // 网络图片的链接地址,String类型的Path事件
         private final static String PATH = "http://pic1.win4000.com/wallpaper/c/53cdd1f7c1f21.jpg";
     
         // 弹出加载框
         private ProgressDialog progressDialog;
     
         // ImageView控件,用来显示结果图像
         private ImageView image;
     
         @Override
         protected void onCreate(Bundle savedInstanceState) {
             super.onCreate(savedInstanceState);
             setContentView(R.layout.activity_download);
             image = findViewById(R.id.image);
     
             // Thread.currentThread().getName(); == Android的主线程,这个跟RxJava切的android主线程是一样的
        }
     
         // 通过订阅将 起点 和 终点 关联起来
         public void rxJavaDownloadImageAction(View view) {
     
             // 起点
             Observable.just(PATH)  // 内部会分发 PATH Stirng // TODO 第二步
             //流程中的卡片
            .map(new Function<String, Bitmap>() {
                 @Override
                 public Bitmap apply(@NonNull String s) throws Exception {
     
                     //处理网络请求:将String类型的Path事件处理为Bitmap实例
                     URL url = new URL(PATH);
                     HttpURLConnection httpURLConnection = (HttpURLConnection) url.openConnection();
                     int responseCode = httpURLConnection.getResponseCode();
                     if(responseCode == httpURLConnection.HTTP_OK){
                         InputStream inputStream = httpURLConnection.getInputStream();
                         Bitmap bitmap = BitmapFactory.decodeStream(inputStream);
                         return bitmap;
                    }
                     return null;
                }
            })
             //给上边代码分配异步线程,用于请求服务器
            .subscribeOn(Schedulers.io())
             //给下边的代码分配主线程,用于更新UI
            .observeOn(AndroidSchedulers.mainThread())
     
             //终点
            .subscribe(
                 new Observer<Bitmap>() {
     
                     // 订阅开始:一订阅就要显示对话框
                     @Override
                     public void onSubscribe(Disposable d) {
                         //                               第一步:事件分发前预准备
                         progressDialog = new ProgressDialog(Test.this);
                         progressDialog.setTitle("开始下载");
                         progressDialog.show();
                    }
                     //拿到事件:因为上一层是一个String类型的Path事件
                     @Override
                     public void onNext(@NonNull Bitmap bitmap) {
                         image.setImageBitmap(bitmap);
                    }
     
                     // 错误事件
                     @Override
                     public void onError(Throwable e) {
     
                    }
     
                     // 完成事件
                     @Override
                     public void onComplete() {
                         //如果不为空那么就隐藏起来
                         if (progressDialog != null)
                         progressDialog.dismiss();
                    }
                });
     
        }
     
     
         // 图片上绘制文字 加水印
         private final Bitmap drawTextToBitmap(Bitmap bitmap, String text, Paint paint, int paddingLeft, int paddingTop) {
             Bitmap.Config bitmapConfig = bitmap.getConfig();
     
             paint.setDither(true); // 获取跟清晰的图像采样
             paint.setFilterBitmap(true);// 过滤一些
             if (bitmapConfig == null) {
                 bitmapConfig = Bitmap.Config.ARGB_8888;
            }
             bitmap = bitmap.copy(bitmapConfig, true);
             Canvas canvas = new Canvas(bitmap);
     
             canvas.drawText(text, paddingLeft, paddingTop, paint);
             return bitmap;
        }
     }

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

    【面试黑洞】Android 的键值对存储有没有最优解?

    正文 这是我在网上找到的一份 Android 键值对存储方案的性能测试对比(数越小越好): 可以看出,DataStore 的性能比 MMKV 差了一大截。MMKV 是腾讯在 2018 年推出的,而 DataStore 是 Android 官方在 2020 年...
    继续阅读 »

    正文


    这是我在网上找到的一份 Android 键值对存储方案的性能测试对比(数越小越好):



    可以看出,DataStore 的性能比 MMKV 差了一大截。MMKV 是腾讯在 2018 年推出的,而 DataStore 是 Android 官方在 2020 年推出的,并且它的正式版在 2021 年 8 月才发布。一个官方发布的、更(gèng)新的库,性能竟然比不过比它早两年发布的、第三方的库。而且我们能看到,更离谱的是,它甚至还比不过 SharedPreferences 。Android 官方当初之所以推出 DataStore,就是要替代掉 SharedPreferences,并且主要原因之一就是 SharedPreferences 有性能问题,可是测试结果却是它的性能不如 SharedPreferences。


    所以,这到底是为什么?


    啊,我知道了——因为 Google 是傻逼!


    SharedPreferences:不知不觉被嫌弃


    大家好,我是扔物线朱凯。


    键值对的存储在移动开发里非常常见。比如深色模式的开关、软件语言、字体大小,这些用户偏好设置,很适合用键值对来存。而键值对的存储方案,最传统也最广为人知的就是 Android 自带的 SharedPreferences。它里面的 -Preferences,就是偏好设置的意思,从名字也能看出它最初的定位。


    SharedPreferences 使用起来很简单,也没什么问题,大家就这么用了很多年。——但!渐渐地,有人发现它有一个问题:卡顿,甚至有时候会出现 ANR。


    MMKV:好快!


    怎么办?换!2018 年 9 月,腾讯开源了一个叫做 MMKV 的项目。它和 SharedPreferences 一样,都是做键值对存储的,可是它的性能比 SharedPreferences 强很多。真的是强,很,多。在 MMKV 推出之后,很多团队就把键值对存储方案从 SharedPreferences 换到了 MMKV。


    DataStore:官方造垃圾?


    再然后,就是又过了两年,Google 自己也表示受不了 SharedPreferences 了,Android 团队公布了 Jetpack 的新库:DataStore,目标直指 SharedPreferences,声称它就是 Android 官方给出的 SharedPreferences 的替代品。


    替代的理由,Android 团队列了好几条,但不出大家意料地,「性能」是其中之一:


    也就是说,Android 团队直接抛弃了 SharedPreferences,换了个新东西来提供更优的性能。


    但是,问题随之就出现了:大家一测试,发现这 DataStore 的性能并不强啊?跟 MMKV 比起来差远了啊?要知道,MMKV 的发布是比 DataStore 早两年的。DataStore 比人家晚两年发布,可是性能却比人家差一大截?甚至,从测试数据来看,它连要被它替代掉的 SharedPreferences 都比不过。这么弱?那它搞个毛啊!


    Android 团队吭哧吭哧搞个新东西出来,竟然还没有市场上两年前就出现的东西强?这是为啥?


    首先,肯定得排除「DataStore 是垃圾」这个可能性。虽然这猛一看、粗一想,明显就是 DataStore 垃圾、Google 傻逼,但是你仔细想想,这可能吗?


    那如果不是的话,又是因为什么?——因为你被骗了。


    MMKV 的一二三四


    被谁骗了?不是被 MMKV 骗了,也不是具体的某个人。事情其实是这样的:


    大家知道 MMKV 当初为什么会被创造出来吗?其实不是为了取代 SharedPreferences。


    最早是因为微信的一个需求(来源:MMKV 组件现在开源了):


    微信作为一个全民的聊天 App,对话内容中的特殊字符所导致的程序崩溃是一类很常见、也很需要快速解决的问题;而哪些字符会导致程序崩溃,是无法预知的,只能等用户手机上的微信崩溃之后,再利用类似时光倒流的回溯行为,看看上次软件崩溃的最后一瞬间,用户收到或者发出了什么消息,再用这些消息中的文字去尝试复现发生过的崩溃,最终试出有问题的字符,然后针对性解决。
    1.gif
    那么这个「时光倒流」应该怎么做,就成了问题的关键。我们要知道,程序中的所有变量都是存活在内存里的,一旦程序崩溃,所有变量全都灰飞烟灭。
    2.gif
    所以要想实现「时光倒流」,就需要把想回溯的时光预先记录下来。说人话就是,我们需要把界面里显示的文字写到手机磁盘里,才能在程序崩溃、重新启动之后,通过读取文件的方式来查看。
    3.gif
    更麻烦的是,这种记录的目标是用来回溯查找「导致程序崩溃的那段文字」,而同时,正是因为没有人知道哪段文字会导致程序崩溃才去做的记录,这就要求每一段文字都需要先写入磁盘、然后再去显示,这样才能保证程序崩溃的时候那段导致崩溃的文字一定已经被记录到了磁盘。
    4.gif
    对吧?


    这就有点难了。


    我们来想象一下实际场景:



    • 如果用户的微信现在处于一个对话界面中,这时候来了一条新的消息,这条消息里可能会包含微信处理不了的字符,导致微信的崩溃。


    5.gif



    • 而微信为了及时地找出导致崩溃的字符或者字符串,所以给程序增加了逻辑:所有的对话内容在显示之前,先保存到磁盘再显示:


    val bubble: WxTextView = ...
    recordTextToDisk(text) // 显示之前,先保存到磁盘
    bubble.setText(text)


    • 那么你想一下,这个「保存到磁盘」的行为,我应该做成同步的还是异步的?
      6.gif

      • 为了不卡主线程,我显然应该做成异步的;

      • 但这是马上就要显示的文字,如果做成异步的,就极有可能在程序崩溃的时候,后台线程还没来得及把文字存到磁盘。这样的话,就无法进行回溯,从而这种记录也就失去了价值。


      7.gif

      • 所以从可用性的角度来看,我只能选择放弃性能,把它做成同步的,也就是在主线程进行磁盘的写操作。


      8.gif

      • 一次磁盘的写操作,花个一两毫秒是很正常的,三五毫秒甚至超过 10 毫秒也都是有可能的。具体的方案可以选择 SharedPreferences,也可以选择数据库,但不管选哪个,只要在主线程去完成这个写操作,这种耗时就绝对无法避免。一帧的时间也就 16 毫秒而已——那时候还没有高刷,我们就先不谈高刷了,一帧就是 16 毫秒——16 毫秒里来个写磁盘的操作,用户很可能就会感受到一次卡顿。



    • 这还是相对比较好的情况。我们再想一下,如果用户点开了一个活跃的群,这个群里有几百条没看过的消息:

      • 那么在他点开的一瞬间,是不是界面中会显示出好几条消息气泡?这几条消息的内容,哪些需要记录到磁盘?全都要记录的,因为谁也知道哪一条会导致微信的崩溃,任何一条都是可能的。

      • 而如果把这几条消息都记录下来,是不是每条消息的记录都会涉及一次写磁盘的操作?这几次写磁盘行为,是发生在同一帧里的,所以在这一帧里因为记录文字而导致的主线程耗时,也会相比起刚才的例子翻上好几倍,卡顿时间就同样也会翻上好几倍。



    • 还有更差的情况。如果用户看完这一页之后,决定翻翻聊天记录,看看大家之前都聊了什么:

      • 这时候,是不是上方每一个新的聊天气泡的出现,都会涉及一次主线程上的写磁盘行为?

      • 而如果用户把手猛地往下一滑,让上面的几十条消息依次滑动显示出来,这是不是就会导致一次爆发性的、集中式的对磁盘的写入?

      • 用户的手机,一定会卡爆。




    所以这种「高频、同步写入磁盘」的需求,让所有的现有方案都变得不可行了:不管你是用 SharedPreferences 还是用数据库还是别的什么,只要你在主线程同步写入磁盘,就一定会卡,而且是很卡。


    但是微信还是有高手,还是有能想办法的人,最终微信找到了解决方案。他们没有用任何的现成方案,而是使用了一种叫做内存映射(mmap())的底层方法。
    CleanShot 2022-05-31 at <a href=15.18.23@2x.png" loading="lazy">
    它可以让系统为你指定的文件开辟一块专用的内存,这块内存和文件之间是自动映射、自动同步的关系,你对文件的改动会自动写到这块内存里,对这块内存的改动也会自动写到文件里。
    9.gif
    更多更深的原理,说实话我也不是看得很懂,就不跟大家装了。但关键是,有了这一层内存作为中间人,我们就可以用「写入内存」的方式来实现「写入磁盘」的目标了。内存的速度多快呀,耗时几乎可以忽略,这样一下子就把写磁盘造成卡顿的问题解决了。
    11.gif
    而且这个内存映射还有一点很方便的是,虽然这块映射的内存不是实时向对应的文件写入新数据,但是它在程序崩溃的时候,并不会随着进程一起被销毁掉,而是会继续有条不紊地把它里面还没同步完的内容同步到它所映射的文件里面去。
    12.gif
    至于更下层的原理,我也说了,没看懂,你也别问我。


    总之,有了这些特性,内存映射就可以让程序用往内存里写数据的速度实现往磁盘里写数据的实际效果,这样的话,「高频、同步写入磁盘」的需求就完美满足了。不管是用户打开新的聊天页面,还是滑动聊天记录来查看聊天历史,用内存映射的方式都可以既实时写入所有即将被渲染的文字,又不会造成界面的卡顿。这种性能,是 SharedPreferences 和数据库都做不到的——顺便提一句,虽然我总在提 SharedPreferences,但其实这种做法本来是先在 iOS 版的微信里应用的,后来才移植到了 Android 版微信。这也是我刚才说的,MMKV 的诞生并不是为了取代 SharedPreferences。


    再后来,就是 2018 年,微信把这个叫做 MMKV 的项目开源了。它的名字,我猜就是直白的「Memory-Map based Key-Value(方案)」,基于内存映射的键值对。不过没有找作者求证,如果说错了欢迎指正。


    在 MMKV 开源之后,很多团队就把键值对存储方案从 SharedPreferences 迁移到了 MMKV。为什么?因为它快呀。


    MMKV 并不总是快如闪电


    不过……事情其实没那么简单。MMKV 虽然大的定位方向和 SharedPreferences 一样,都是对于键值对的存储,但它并不是一个全方位更优的方案。


    比如性能。我前面一直在说 MMKV 的性能更强,对吧?但事实上,它并不是任何时候都更强。由于内存映射这种方案是自行管理一块独立的内存,所以它在尺寸的伸缩上面就比较受限,这就导致它在写大一点的数据的时候,速度会慢,而且可能会很慢。我做了一份测试:
    13.gif
    在连续 1000 次写入 Int 值的场景中,SharedPreferences 的耗时是 1034 毫秒,也就是 1 秒多一点;而 MMKV 只有 2 毫秒,简直快得离谱;而且最离谱的是,Android 官方最新推出的 DataStore 是 1215 毫秒,竟然比 SharedPreferences 还慢。这个前面我也提过,别人的测试也是这样的结果。


    可是,SharedPreferences 是有异步 API 的,而 DataStore 是基于协程的。这就意味着,它们实际占用主线程的时间是可以低于这份测试出的时间的,而界面的流畅在意的正是主线程的时间消耗。所以如果我统计的不是全部的耗时,而是主线程的耗时,那么统计出的 SharedPreferencesDataStore 的耗时将会大幅缩减:
    14.gif
    还是比 MMKV 慢很多,是吧?但是这是对于 Int类型的高频写入,Int 数据是很小的。而如果我把写入的内容换成长字符串,再做一次测试:
    15.gif
    MMKV 就不具备优势了,反而成了耗时最久的;而这时候的冠军就成了 DataStore,并且是遥遥领先。这也就是我在开头说的:你可能被骗了。被谁骗了?被「耗时」这个词:我们关注性能,考量的当然是耗时,但要明确:是主线程的耗时。所以视频开头的那张图,是不具备任何参考意义的。
    CleanShot 2022-06-22 at <a href=20.52.01@2x.png" loading="lazy">


    但其实,它们都够快了


    不过在换成了这种只看主线程的耗时的对比方案之后,我们会发现谁是冠军其实并不是很重要,因为从最终的数据来看,三种方案都不是很慢。虽然这半秒左右的主线程耗时看起来很可怕,但是要知道这是 1000 次连续写入的耗时,而我们在真正写程序的时候,怎么会一次性做 1000 次的长字符串的写入?所以真正在项目中的键值对写入的耗时,不管你选哪个方案,都会比这份测试结果的耗时少得多的,都少到了可以忽略的程度,这是关键。


    各自的优势和弱点


    那……既然它们的耗时都少到了可以忽略,不就是选谁都行?那倒不是。


    MMKV 优势:写速度极快


    我们来看一个 MMKV 官方给出的数据对比图:
    image.png
    从这张图看来,SharedPreferences 的耗时是 MMKV 的接近 60 倍。很明显,如果 SharedPreferences 用异步的 API 也就是 apply() 来保存的话,是不可能有这么差的性能的,这个一定是使用同步的 commit() 的性能来做的对比。那么为什么 MMKV 官方会这样做对比呢?这个又要说到它的诞生场景了:MMKV 最初的功能是在文字显示之前先把它记录到磁盘,然后如果接下来这个文字显示失败导致程序崩溃,稍后就可以从磁盘里把这段文字恢复出来,进行分析。而刚才我也说过,这种场景的特殊性在于,导致程序崩溃的文字往往是刚刚被记录下来,程序就崩溃了,所以如果采用异步处理的方案,就很有可能在文字还没来得及真正存储到磁盘的时候程序就发生了崩溃,那就没办法把它恢复出来进行分析了。因此这样的场景,是不能接受异步处理的方案的,只能同步进行。所以 MMKV 在意的,就是同步处理机制下的耗时,它不在意异步,因为它不接受异步。


    而在同步处理的机制下,MMKV 的性能优势就太明显了。原因上面说过了,它写入内存就几乎等于写入了磁盘,所以速度巨快无比。这就是 MMKV 的优势之一:极高的同步写入磁盘的性能。


    另外 MMKV 还有个特点是,它的更新并不像 SharedPreferences 那样全量重新写入磁盘,而是只把要更新的键值对写入,也就是所谓的增量式更新。这也会给它带来一些性能优势,不过这个优势并不算太核心,因为 SharedPreferences 虽然是全量更新的模式,但只要把保存的数据用合适的逻辑拆分到多个不同的文件里,全量更新并不会对性能造成太大的拖累。所以这个性能优势虽然有,但并不是关键。


    还有刚才提到的,对于大字符串的场景,MMKV 的写入性能并不算快,甚至在我们的测试结果里是最慢的,对吧?这一点算是劣势。但是实事求是地说,我们在开发里不太可能连续不断地去写入大字符串吧?所以这个性能劣势虽然有,但也并不是关键。


    整体来说,MMKV 比起 SharedPreferences 和 DataStore 来说,在写入小数据的情况下,具有很高的写入性能,这就让高频写入的场景非常适合使用 MMKV 来处理。因此如果你的项目里也有像微信的崩溃回溯的这种高频写入的需求,MMKV 就很可能是你的最佳方案。而如果你除了「高频写入」,还和微信一样要求「同步写入」,那 MMKV 就可能是你的唯一选择了。不过,如果你真的主要是存储大字符串的——例如你写的是一个文本编辑软件,需要保存的总是大块的文本——那么用 MMKV 不一定会更快了,甚至可能会比较慢。


    MMKV 优势:支持多进程


    另外,MMKV 还有一个巨大的优势:它支持多进程。


    行业内也有很多公司选用 MMKV 并不是因为它快,而是因为它支持多进程。SharedPreferences 是不支持多进程的,DataStore 也不支持——从 DataStore 提交的代码来看,它已经在加入多进程的支持了,但目前还没有实现。所以如果你们公司的 App 是需要在多个进程里访问键值对数据,那么 MMKV 是你唯一的选择。


    MMKV 劣势:丢数据


    除了速度快和支持多进程这两个优势之外,MMKV 也有一个弱点:它会丢数据。


    任何的操作系统、任何的软件,在往磁盘写数据的过程中如果发生了意外——例如程序崩溃,或者断电关机——磁盘里的文件就会以这种写了一半的、不完整的形式被保留。写了一半的数据怎么用啊?没法用,这就是文件的损坏。这种问题是不可能避免的,MMKV 虽然由于底层机制的原因,在程序崩溃的时候不会影响数据往磁盘的写入,但断电关机之类的操作系统级别的崩溃,MMKV 就没办法了,文件照样会损坏。对于这种文件损坏,SharedPreferences 和 DataStore 的应对方式是在每次写入新数据之前都对现有文件做一次自动备份,这样在发生了意外出现了文件损坏之后,它们就会把备份的数据恢复过来;而 MMKV,没有这种自动的备份和恢复,那么当文件发生了损坏,数据就丢了,之前保存的各种信息只能被重置。也就是说,MMKV 是唯一会丢数据的方案。


    可能会有人好奇,为什么 MMKV 不做全自动的备份和恢复。我的猜测是这样的:MMKV 底层的原理是内存映射,而内存映射这种方式,它从内存往磁盘里同步写入的过程并不是实时的,也就是说并不是每次我们写入到映射的内存里就会立即从这块内存写入到磁盘,而是会有一些滞后。而如果我们要做全自动的备份,那就需要每次往内存里写入之后,立即手动把内存里最新的数据同步到磁盘。但这就和 MMKV 的定位不符了:因为这种「同步」本质上就是一次从内存到磁盘的写入,并且是同步的写入;而 MMKV 是要高频写入的,如果在高频写入内存的同时,还要实时地把数据从内存同步到磁盘,就会一下子把写入速度从内存级别下降到磁盘级别,MMKV 的性能优势也就荡然无存了。所以从原理上,自动备份是个很难实现的需求,因为它和 MMKV 的定位是矛盾的。不过正好 MMKV 所要记录的这些要显示的文字,也并不是不能丢失的内容——真要是丢了就丢了呗,反正是崩溃日志,丢了就不要了,我下次启动程序之后继续记录就是了——所以既然要求必须高频写入而导致很难实现自动备份,并且也确实能接受因为不做自动备份而导致的数据损坏,那就干脆不做自动备份了。不过这也是我猜的啊,大家如果有不同意见欢迎留言评论指正。


    所以如果你要用 MMKV,一定要记得只能用它来存可以接受丢失、不那么重要的数据。或者你也可以选择对数据进行定期的手动备份——全自动的实时备份应该是会严重影响性能的,不过我没试过,你如果有兴趣可以试试。另外据我所知,国内在使用 MMKV 的团队里,几乎没有对 MMKV 数据做了备份和恢复的处理的。


    那么说到这里,很容易引出一个问题:微信自己就不怕丢数据吗?(大字:微信就不怕丢数据?)关于这一点,我相信,微信绝对不会把用户登录状态相关的信息用 MMKV 保存并且不做任何的备份,因为这一定会导致每天都会有一些用户在新一次打开微信的时候发现自己登出了。这会是非常差的用户体验,所以微信一定不会让这种事发生。至于一些简单的用户设置,那我就不清楚了。比如深色主题重要吗?这是个不好说的事情:某个用户在打开软件的时候,发现自己之前设置的深色主题失效了,软件突然变回了亮色方案,这肯定是不舒服的事;但我们要知道,MMKV 的文件损坏终归是个概率极低的事件,所以偶尔地发生一次这样的事件在产品的角度是否可以接受,那可能是需要产品团队自身做一个综合考量的事了。对于不同的产品和团队,也许不可接受,也许无伤大雅。而对于你所开发的产品应该是怎样的判断,就得各位自己和团队去商量了。所以像深色主题这种「可以重要也可以不重要」的信息,用不用 MMKV 保存、用的时候做不做备份,大家需要自己去判断。


    总之,大家要知道这件事:MMKV 是有数据损坏的概率的,这个在 MMKV 的官方文档就有说明:MMKV 的 GitHub wiki 页面显示,微信的 iOS 版平均每天有 70 万次的数据校验不通过(即数据损坏)。这还是 2020 年的数据,现在可能会更多。
    CleanShot 2022-05-31 at 15.46.20.png
    所以我们在使用 MMKV 的时候,一定要考虑到这个问题,你要知道这件事。至于具体的应对,是接受它、坏就坏了,还是要认真应对、做好备份和恢复,这就是大家自己的决策了。


    SharedPreferences 的优势:不丢数据


    好,那么说完了 MMKV,我来说一下 SharedPreferences,这个最传统的方案。


    它有什么优势呢?——它没有优势。跟 MMKV 比起来,它不会丢数据,这个倒是它比 MMKV 强的地方,但是我觉得更应该归为 MMKV 的劣势,而不是 SharedPreferences 的优势,因为只有 MMKV 会丢数据嘛,是吧?


    不过不管是这个的优势还是那个的劣势,如果你不希望丢数据,并且也不想花时间去做手动的备份和恢复,同时对于 MMKV 的超高写入性能以及多进程支持都没有需求,那你其实更应该选择 SharedPreferences,而不是 MMKV。对吧?


    SharedPreferences 的劣势:卡顿


    但更进一步地说:如果你选择了 SharedPreferences,那么你更应该考虑 DataStore。因为 DataStore 是一个完全超越了 SharedPreferences 的存在。你看 SharedPreferences 和 MMKV 它俩是各有优劣对吧?虽然 MMKV 几乎完胜,但是毕竟 SharedPreferences 不会丢数据呀,所以它俩是各有优劣的。但当 DataStore 和 SharedPreferences 比起来,那就是 DataStore 完胜了。这其实也很合理,因为 DataStore 被创造出来,就是用于替代掉 SharedPreferences 的;而 MMKV 不一样,它的诞生有它独特的使命,它是为了「高频同步写入」而诞生的,所以不能全角度胜过 SharedPreferences 也很正常。


    我们还说回 DataStore。DataStore 被创造出来的目标就是替代 SharedPreferences,而它解决的 SharedPreferences 最大的问题有两点:一是性能问题,二是回调问题。


    先说性能问题:SharedPreferences 虽然可以用异步的方式来保存更改,以此来避免 I/O 操作所导致的主线程的耗时;但在 Activity 启动和关闭的时候,Activity 会等待这些异步提交完成保存之后再继续,这就相当于把异步操作转换成同步操作了,从而会导致卡顿甚至 ANR(程序未响应)。这是为了保证数据的一致性而不得不做的决定,但它也确实成为了 SharedPreferences 的一个弱点。而 MMKV 和 DataStore 用不同的方式各自都解决了这个问题——事实上,当初 MMKV 被公布的时候之所以在业界有相当大的反应,就是因为它解决了 SharedPreferences 的卡顿和 ANR 的问题。


    不过有一点我的观点可能和一些人不同:SharedPreferences 所导致的卡顿和 ANR,其实并不是个很大的问题。它和 MMKV 的数据损坏一样,都是非常低概率的事件。它俩最大的区别在于其实是政治上的:SharedPreferences 的卡顿很容易被大公司的性能分析后台监测到,所以不解决的话会扣绩效,而解决掉它会提升绩效;而 MMKV 的数据损坏是无法被监测到的,所以……哈?事实上,大家想一下:卡顿和数据损坏,哪个更严重?当然是数据损坏了,对吧。


    其实除了写数据时的卡顿,SharedPreferences 在读取数据的时候也会卡顿。虽然它的文件加载过程是在后台进行的,但如果代码在它加载完成之前就去尝试读取键值对,线程就会被卡住,直到文件加载完成,而如果这个读取的过程发生在主线程,就会造成界面卡顿,并且数据文件越大就会越卡。这种卡顿,不是 SharedPreferences 独有的,MMKV 也是存在的,因为它初始化的过程同样也是从磁盘里读取文件,而且是一股脑把整个文件读完,所以耗时并不会比 SharedPreferences 少。而 DataStore,就没有这种问题。DataStore 不管是读文件还是写文件,都是用的协程在后台进行读写,所有的 I/O 操作都是在后台线程发生的,所以不论读还是写,都不会卡主线程。


    简单来说,SharedPreferences 会有卡顿的问题,这个问题 MMKV 解决了一部分(写时的卡顿),而 DataStore 完全解决了。所以如果你的目标在于全方位的性能,那么你应该考虑的是 DataStore,因为它是唯一完全不会卡顿的。


    SharedPreferences 的劣势:回调


    DataStore 解决的 SharedPreferences 的另一个问题就是回调。SharedPreferences 如果使用同步方式来保存更改(commit()),会导致主线程的耗时;但如果使用异步的方式,给它加回调又很不方便,也就是如果你想做一些「等这个异步提交完成之后再怎么怎么样」的工作,会很麻烦。


    而 DataStore 由于是用协程来做的,线程的切换是非常简单的,你就把「保存完成之后做什么」直接写在保存代码的下方就可以了,很直观、很简单。


    对比来说,MMKV 虽然没有使用协程,但是它太快了,所以大多数时候并不需要切线程也不会卡顿。总之,在这件事上,只有 SharedPreferences 最弱。


    总结


    区别大概就是这么些区别了,大致总结一下就是:


    如果你有多进程支持的需求,MMKV 是你唯一的选择;如果你有高频写入的需求,你也应该优先考虑 MMKV。但如果你使用 MMKV,一定要知道它是可能丢失数据的,不过概率很低就是了,所以你要在权衡之后做好决定:是自行实现数据的备份和恢复方案,还是直接接受丢数据的事实,在每次丢失数据之后帮用户把相应的数据进行初始化。当然了,一个最鸡贼的做法是:反正数据监测不会监测到 MMKV 的数据丢失,又不影响绩效,那就不管它呗!不过我个人是不太赞同这种策略的,有点不负责哈。


    另外,如果你没有多进程的需求,也没有高频写入的需求,DataStore 作为性能最完美的方案,应该优先被考虑。因为它在任何时候都不会卡顿,而 MMKV 在写大字符串和初次加载文件的时候是可能会卡顿的,而且初次加载文件的卡顿不是概率性的,只要文件大到了引起卡顿的程度,就是 100% 的卡顿。不过如果你的团队没有在用协程,甚至没有在用 Kotlin,那 DataStore 也暂时不适合你们,因为它是完全依赖 Kotlin 协程来实现和使用的。


    哦对了,其实我今天说的 DataStore 只是面向简单键值对存储的 DataStore 方案,它的全称叫 Preferences DataStore,而 DataStore 还有用于保存结构化数据的方案,叫做 Proto DataStore,它内部用的是 Protocol Buffer 作为数据结构的支持。但是这个有点跑题,我就不展开了。


    至于 SharedPreferences 嘛,在这个时代,它真的可以被放弃了。除非——像我刚说的——如果你们还没在用协程,那 SharedPreferences 可能还能苟延残喘一下。


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