环信即时通讯云

环信即时通讯云

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

环信开发文档

环信客服云

环信客服云

无需下载,注册即用
声网开发者社区

声网开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

小程序原理 及 优化

小程序使用的是双线程 在这种架构中,视图层使用 WebView 作为渲染载体,而逻辑层是由独立的 JavascriptCore 作为运行环境 > 两者都是独立的模块,并不具备数据直接共享的通道。视图层和逻辑层的数据传输,要由 Native 的 JS...
继续阅读 »

小程序使用的是双线程



在这种架构中,视图层使用 WebView 作为渲染载体,而逻辑层是由独立的 JavascriptCore 作为运行环境 >




两者都是独立的模块,并不具备数据直接共享的通道。视图层和逻辑层的数据传输,要由 NativeJSBrigde 做中转



小程序的启动过程



  • 1、小程序初始化: 微信初始化小程序环境:包括 Js 引擎WebView 进行初始化,并注入公共基础库。 这步是微信做的,在用户打开小程序之前就已经准备好了,是小程序运行环境预加载。

  • 2、下载小程序代码包 对小程序业务代码包进行下载:下载的不是小程序的源代码,而是编译、压缩、打包之后的代码。

  • 3、加载小程序代码包 对下载完成对代码包进行注入执行。 此时,app.js、页面所在的 Js 文件和所有其他被require 的 Js 文件会被自动执行一次,小程序基础库会完成所有页面的注册。

  • 4、初始化小程序首页 拉取数据,从逻辑层传递到视图层,进行渲染


setData 的工作原理



  • 1、调用setData方法;

  • 2、逻辑层会执行一次 JSON.stringify 来去除掉 setData 数据中不可传输的部分,将待传输数据转换成字符串并拼接到特定的JS脚本, 并通过 evaluateJavascript 执行脚本将数据传输到渲染层。

  • 3、渲染层接收到后, WebView JS 线程会对脚本进行编译,得到待更新数据后进入渲染队列等待 WebView 线程空闲时进行页面渲染。

  • 4、WebView 线程开始执行渲染时,将 data setData 数据套用在WXML 片段上,得到一个新节点树。经过新虚拟节点树与当前节点树的 diff 对比,将差异部分更新到UI视图。最后,将 setData 数据合并到 data 中,并用新节点树替换旧节点树,用于下一次重渲染


小程序官方性能指标



  • 1、首屏时间不超过 5 秒

  • 2、渲染时间不超过 500ms

  • 3、每秒调用 setData 的次数不超过 20 次

  • 4、setData 的数据在 JSON.stringify 后不超过 256kb

  • 5、页面 WXML 节点少于 1000 个,节点树深度少于 30 层子节点数不大于 60 个

  • 6、所有网络请求都在 1 秒内返回结果;


小程序优化


1、分包并且使用


分包预加载(通过配置 preloadRule) 将访问率低的页面放入子包里,按需加载;启动时需要访问的页面及其依赖的资源文件应放在主包中。 不需要等到用户点击到子包页面后在下载子包,而是可以根据后期数据,做子包预加载,将用户在当先页可能点击的子包页面先加载,当用户点击后直接跳转;可以根据用户网络类型来判断的,当用户处于网络条件好时才预加载;


image.png


2、采用独立分包技术(感觉开普勒黄金流程源码可以独立分包)


主包+子包的方式,,如果要跳到子包里,还是会加载主包然后加载子包;采用独立分包技术,区别于子包,和主包之间是无关的,在功能比较独立的子包里,使用户只需下载分包资源;


3、异步请求可以在页面onLoad就加载


4、注意利用缓存


利用storage API, 对变动频率比较低的异步数据进行缓存,二次启动时,先利用缓存数据进行初始化渲染,然后后台进行异步数据的更新


5、及时反馈


及时对需要用户等待的交互操作进行反馈,避免用户以为小程序卡了 先反馈,再请求。比如说,点赞的按钮,可以先改变按钮的样式,再 发起异步请求。


6、可拆分的部分尽量使用自定义组件


自定义组件的更新并不会影响页面上其他元素的更新,各个组件具有独立的逻辑空间、数据、样式环境及 setData 调用


7、避免不当的使用onPageScroll


避免在onPageScroll 中执行复杂的逻辑,避免在onPageScroll中频繁使用setData,避免在onPageScroll中 频繁查询几点信息(selectQuery


8、减少在代码包中直接嵌入的资源文件;图片放在cdn,使用适当的图片格式


9、setData 优化


(1)与界面渲染无关的数据最好不要设置在 data 中,可以考虑设置在 page 对象的其他字段下;


this.setData({ 
a: '与渲染有关的字符串',
b: '与渲染无关的字符串'
})
// 可以优化为
this.setData({
a: '与渲染有关的字符串'
})
this.b = '与渲染无关的字符串'

(2)不要过于频繁调用 setData,将多次 setData 合并成一次 setData 调用


(3)数据通信的性能与数据量正相关,因而如果有一些数据字段不在界面中展示数据结构比较复杂包含长字符串,则不应使用setData来设置这些数据


(4)列表局部更新 在更新列表的某一个数据时。不要用 setData 进行全部数据的刷新。查找对应 id 的那条数据的下标(index是不会改变的),用 setData 进行局部刷新


this.setData({ 
`list[${index}]` = newList[index]
})

(5)切勿在后台页面进行setData(就是不要再页面跳转后使用setData) 页面跳转后,代码逻辑还在执行,此时多个webview是共享一个js进程;后台的setData操作会抢占前台页面的渲染资源;


10、避免过多的页面节点数


页面初始渲染时,渲染树的构建、计算节点几何信息以及绘制节点到屏幕的时间开销都跟页面节点数量成正相关关系,页面节点数量越多,渲染耗时越长。


每次执行 setData 更新视图,WebView JS 线程都要遍历节点树计算新旧节点数差异部分。当页面节点数量越多,计算的时间开销越大,减少节点树节点数量可以有效降低重渲染的时间开销。


11、事件使用不当


(1)去掉不必要的事件绑定(WXML中的bindcatch),从而减少通信的数据量和次数;
(2)事件绑定时需要传输targetcurrentTargetdataset,因而不要在节点的data前缀属性中放置过大的数据


12、逻辑后移,精简业务逻辑


就比如咱们生成分享图片,再比如领取新人券的时候将是否是新人是否符合风控条件和最终领券封装为一个接口


13、数据预拉取(重要


小程序官方为开发者提供了一个在小程序冷启动时提前拉取第三方接口的能力 developers.weixin.qq.com/miniprogram… 预拉取能够在小程序冷启动的时候通过微信后台提前向第三方服务器拉取业务数据,当代码包加载完时可以更快地渲染页面,减少用户等待时间,从而提升小程序的打开速度


14、跳转时预拉取


可以在发起跳转前(如 wx.navigateTo 调用前),提前请求下一个页面的主接口并存储在全局 Promise 对象中,待下个页面加载完成后从 Promise 对象中读取数据即可


15、非关键渲染数据延迟请求


小程序会发起一个聚合接口请求来获取主体模块的数据,而非主体模块的数据则从另一个接口获取,通过拆分的手段来降低主接口的调用时延,同时减少响应体的数据量,缩减网络传输时间。


16、分屏渲染


在 主体模块 的基础上再度划分出 首屏模块 和 非首屏模块(比如京挑好货的猜你喜欢模块),在所有首屏模块都渲染完成后才会渲染非首屏模块和非主体模块,以此确保首屏内容以最快速度呈现


17、接口聚合,请求合并(主要解决小程序中针对 API 调用次数的限制)


在小程序中针对 API 调用次数的限制: wx.request (HTTP 连接)的最大并发限制是 10 个; wx.connectSocket (WebSocket 连接)的最大并发限制是 5 个;


18、事件总线,替代组件间数据绑定的通信方式


通过事件总线(EventBus),也就是发布/订阅模式,来完成由父向子的数据传递


19、大图裁剪为多块加载


20、长列表优化


(1)不要每次加载更多的时候 都用concat
每获取到新一页数据时,就把它们concatlist上去,这样就会导致每次setData时的list越来越长越来越长,渲染速度也就越来越慢
(2)分批setData,减少一次setData的数量。不要一次性setData list,而是把每一页一批一批地set Data到这个list中去


this.setData({ 
['feedList[' + (page - 1) + ']']: newVal,
})

(3)运用官方的 IntersectionObserver.relativeToViewport 将超出或者没进入可视区的 部分卸载掉(适用于一次加载很多的列表数据,超出了两屏高度所展示的内容)


image.png


this.extData.listItemContainer.relativeToViewport({ top: showNum * windowHeight, bottom: showNum * windowHeight }) 
.observe(`#list-item-${this.data.skeletonId}`, (res) => {
let { intersectionRatio } = res
if (intersectionRatio === 0) {
console.log('【卸载】', this.data.skeletonId, '超过预定范围,从页面卸载')
this.setData({
showSlot: false
})
} else {
console.log('【进入】', this.data.skeletonId, '达到预定范围,渲染进页面')
this.setData({
showSlot: true,
height: res.boundingClientRect.height
})
}
})

21、合理运用函数的防抖与节流,防止出现重复点击及重复请求出现 为避免频繁setData和渲染,做了防抖函数,时间是600ms


作者:甘草倚半夏
链接:https://juejin.cn/post/7023671521075806244

收起阅读 »

Vue 开发规范(下)

提供组件 API 文档 使用 Vue.js 组件的过程中会创建 Vue 组件实例,这个实例是通过自定义属性配置的。为了便于其他开发者使用该组件,对于这些自定义属性即组件API应该在 README.md 文件中进行说明。 为什么?良好的文档可以让开发者比较容易的...
继续阅读 »

提供组件 API 文档


使用 Vue.js 组件的过程中会创建 Vue 组件实例,这个实例是通过自定义属性配置的。为了便于其他开发者使用该组件,对于这些自定义属性即组件API应该在 README.md 文件中进行说明。


为什么?

良好的文档可以让开发者比较容易的对组件有一个整体的认识,而不用去阅读组件的源码,也更方便开发者使用。

组件配置属性即组件的 API,对于组件的用户来说他们更感兴趣的是 API 而不是实现原理。

正式的文档会告诉开发者组件 API 变更以及向后的兼容性情况

README.md 是标准的我们应该首先阅读的文档文件。代码托管网站(GitHub、Bitbucket、Gitlab 等)会默认在仓库中展示该文件作为仓库的介绍。


怎么做?


在模块目录中添加 README.md 文件:


range-slider/
├── range-slider.vue
├── range-slider.less
└── README.md
在 README 文件中说明模块的功能以及使用场景。对于 vue 组件来说,比较有用的描述是组件的自定义属性即 API 的描述介绍。


提供组件 demo


添加 index.html 文件作为组件的 demo 示例,并提供不同配置情况的效果,说明组件是如何使用的。


为什么?

demo 可以说明组件是独立可使用的。

demo 可以让开发者预览组件的功能效果。

demo 可以展示组件各种配置参数下的功能。


对组件文件进行代码校验


代码校验可以保持代码的统一性以及追踪语法错误。.vue 文件可以通过使用 eslint-plugin-html插件来校验代码。你可以通过 vue-cli 来开始你的项目,vue-cli 默认会开启代码校验功能。


为什么?

保证所有的开发者使用同样的编码规范。

更早的感知到语法错误。


怎么做?


为了校验工具能够校验 *.vue文件,你需要将代码编写在

ESLint
ESLint 需要通过 ESLint HTML 插件来抽取组件中的代码。


通过 .eslintrc 文件来配置 ESlint,这样 IDE 可以更好的理解校验配置项:


{
"extends": "eslint:recommended",
"plugins": ["html"],
"env": {
"browser": true
},
"globals": {
"opts": true,
"vue": true
}
}
运行 ESLint


eslint src/**/*.vue


JSHint
JSHint 可以解析 HTML(使用 --extra-ext命令参数)和抽取代码(使用 --extract=auto命令参数)。


通过 .jshintrc 文件来配置 ESlint,这样 IDE 可以更好的理解校验配置项。


{
"browser": true,
"predef": ["opts", "vue"]
}
运行 JSHint


jshint --config modules/.jshintrc --extra-ext=html --extract=auto modules/
注:JSHint 不接受 vue 扩展名的文件,只支持 html。


只在需要时创建组件


为什么?


Vue.js 是一个基于组件的框架。如果你不知道何时创建组件可能会导致以下问题:



  • 如果组件太大, 可能很难重用和维护;

  • 如果组件太小,你的项目就会(因为深层次的嵌套而)被淹没,也更难使组件间通信;


怎么做?



  • 始终记住为你的项目需求构建你的组件,但是你也应该尝试想到它们能够从中脱颖而出(独立于项目之外)。如果它们能够在你项目之外工作,就像一个库那样,就会使得它们更加健壮和一致。

  • 尽可能早地构建你的组件总是更好的,因为这样使得你可以在一个已经存在和稳定的组件上构建你的组件间通信(props & events)。


规则



  • 首先,尽可能早地尝试构建出诸如模态框、提示框、工具条、菜单、头部等这些明显的(通用型)组件。总之,你知道的这些组件以后一定会在当前页面或者是全局范围内需要。

  • 第二,在每一个新的开发项目中,对于一整个页面或者其中的一部分,在进行开发前先尝试思考一下。如果你认为它有一部分应该是一个组件,那么就创建它吧。

  • 最后,如果你不确定,那就不要。避免那些“以后可能会有用”的组件污染你的项目。它们可能会永远的只是(静静地)待在那里,这一点也不聪明。注意,一旦你意识到应该这么做,最好是就把它打破,以避免与项目的其他部分构成兼容性和复杂性。


Vue 组件规范


<!-- iview 等第三方公共组件,推荐大写开头 -->
<Button> from the top</Button>
<Row>
<Col span="24">
</Col>
</Row>


/** * 公共组件 项目内,自己开发的 推荐p开头 * import pLinkpage from 'public/module/linkage' */


<p-linkage v-model="form.pcarea"></p-linkage>


/** * 非公共组件 项目内,自己开发的推荐v开头 * import vSearch from './search' */
<v-search @search="params = $event"></v-search>

自闭合组件


在单文件组件、字符串模板和 JSX 中没有内容的组件应该是自闭合的——但在 DOM 模板里永远不要这样做。


自闭合组件表示它们不仅没有内容,而且刻意没有内容。其不同之处就好像书上的一页白纸对比贴有“本页有意留白”标签的白纸。而且没有了额外的闭合标签,你的代码也更简洁。


不幸的是,HTML 并不支持自闭合的自定义元素——只有官方的“空”元素。所以上述策略仅适用于进入 DOM 之前 Vue 的模板编译器能够触达的地方,然后再产出符合 DOM 规范的 HTML。


// 反例
<!-- 在单文件组件、字符串模板和 JSX 中 -->
<MyComponent></MyComponent>


<!-- 在 DOM 模板中 -->
<my-component/>
// 好例子
<!-- 在单文件组件、字符串模板和 JSX 中 -->
<MyComponent/>


<!-- 在 DOM 模板中 -->
<my-component></my-component>

作者:_Battle
链接:https://juejin.cn/post/7023549490372182052/

收起阅读 »

Vue 开发规范(中)

上一篇:https://www.imgeek.org/article/825358938将 this 赋值给 component 变量 在 Vue.js 组件上下文中,this指向了组件实例。因此当你切换到了不同的上下文时,要确保 this 指向一个可用的 c...
继续阅读 »

上一篇:https://www.imgeek.org/article/825358938

将 this 赋值给 component 变量


在 Vue.js 组件上下文中,this指向了组件实例。因此当你切换到了不同的上下文时,要确保 this 指向一个可用的 component 变量。


换句话说,如果你正在使用 ES6 的话,就不要再编写 var self = this; 这样的代码了,您可以安全地使用 Vue 组件。


为什么?



  • 使用 ES6,就不再需要将 this 保存到一个变量中了。

  • 一般来说,当你使用箭头函数时,会保留 this 的作用域。(译者注:箭头函数没有它自己的 this 值,箭头函数内的 this 值继承自外围作用域。)

  • 如果你没有使用 ES6,当然也就不会使用 箭头函数 啦,那你必须将 “this” 保存到到某个变量中。这是唯一的例外。


怎么做?


<script type="text/javascript"> 
export default { methods: { hello() { return 'hello'; }, printHello() { console.log(this.hello()); }, }, };
</script>
<!-- 避免 -->
<script type="text/javascript">
export default { methods: { hello() { return 'hello'; }, printHello() { const self = this; // 没有必要 console.log(self.hello()); }, }, };
</script>

组件结构化


按照一定的结构组织,使得组件便于理解。


为什么?



  • 导出一个清晰、组织有序的组件,使得代码易于阅读和理解。同时也便于标准化。

  • 按首字母排序 properties、data、computed、watches 和 methods 使得这些对象内的属性便于查找。

  • 合理组织,使得组件易于阅读。(name; extends; props, data 和 computed; components; watch 和 methods; lifecycle methods 等)。

  • 使用 name 属性。借助于 vue devtools 可以让你更方便的测试。

  • 合理的 CSS 结构,如 BEM 或 rscss - 详情?。

  • 使用单文件 .vue 文件格式来组件代码。


怎么做?


组件结构化


<template lang="html">
<div class="Ranger__Wrapper">
<!-- ... -->
</div>
</template>

<script type="text/javascript"> 
export default {
// 不要忘记了 name 属性 name: 'RangeSlider',
// 组合其它组件 extends: {},
// 组件属性、变量 props: { bar: {},
// 按字母顺序 foo: {}, fooBar: {}, },
// 变量 data() {}, computed: {},
// 使用其它组件 components: {},
// 方法 watch: {}, methods: {},
// 生命周期函数 beforeCreate() {}, mounted() {}, };
</script>

<style scoped> .Ranger__Wrapper { /* ... */ } </style>

组件事件命名


Vue.js 提供的处理函数和表达式都是绑定在 ViewModel 上的,组件的每一个事件都应该按照一个好的命名规范来,这样可以避免不少的开发问题,具体可见如下 为什么。


为什么?



  • 开发者可以随意给事件命名,即使是原生事件的名字,这样会带来迷惑性。

  • 过于宽松的事件命名可能与 DOM 模板不兼容。


怎么做?



  • 事件名也使用连字符命名。

  • 一个事件的名字对应组件外的一组意义操作,如:upload-success、upload-error 以及 dropzone-upload-success、dropzone-upload-error (如果需要前缀的话)。

  • 事件命名应该以动词(如 client-api-load) 或是 形容词(如 drive-upload-success)结尾。(出处)


避免 this.$parent


Vue.js 支持组件嵌套,并且子组件可访问父组件的上下文。访问组件之外的上下文违反了基于模块开发的第一原则。因此你应该尽量避免使用 this.$parent。


为什么?



  • 组件必须相互保持独立,Vue 组件也是。如果组件需要访问其父层的上下文就违反了该原则。

  • 如果一个组件需要访问其父组件的上下文,那么该组件将不能在其它上下文中复用。


怎么做?



  • 通过 props 将值传递给子组件。

  • 通过 props 传递回调函数给子组件来达到调用父组件方法的目的。

  • 通过在子组件触发事件来通知父组件。


谨慎使用 this.$refs


Vue.js 支持通过 ref 属性来访问其它组件和 HTML 元素。并通过 this.$refs 可以得到组件或 HTML 元素的上下文。在大多数情况下,通过 this.$refs来访问其它组件的上下文是可以避免的。在使用的的时候你需要注意避免调用了不恰当的组件 API,所以应该尽量避免使用 this.$refs


为什么?



  • 组件必须是保持独立的,如果一个组件的 API 不能够提供所需的功能,那么这个组件在设计、实现上是有问题的。

  • 组件的属性和事件必须足够的给大多数的组件使用。


怎么做?



  • 提供良好的组件 API

  • 总是关注于组件本身的目的。

  • 拒绝定制代码。如果你在一个通用的组件内部编写特定需求的代码,那么代表这个组件的 API 不够通用,或者你可能需要一个新的组件来应对该需求。

  • 检查所有的 props 是否有缺失的,如果有提一个 issue 或是完善这个组件。

  • 检查所有的事件。子组件向父组件通信一般是通过事件来实现的,但是大多数的开发者更多的关注于 props 从忽视了这点。

  • Props向下传递,事件向上传递!。以此为目标升级你的组件,提供良好的 API 和 独立性。

  • 当遇到 propsevents 难以实现的功能时,通过 this.$refs来实现。

  • 当需要操作 DOM 无法通过指令来做的时候可使用 this.$ref 而不是 JQuery、document.getElement*、document.queryElement

  • 基础使用准则是,能不用ParseError: KaTeX parse error: Expected 'EOF', got '就' at position 5: refs就̲尽量不用,如果用,尽量不要通过refs操作状态,可以通过$refs调用methods


<!-- 推荐,并未使用 this.$refs -->
<range :max="max" :min="min" @current-value="currentValue" :step="1"></range>

<!-- 使用 this.$refs 的适用情况-->
<modal ref="basicModal">
<h4>Basic Modal</h4>
<button class="primary" @click="$refs.basicModal.hide()">Close</button>
</modal>
<button @click="$refs.basicModal.open()">Open modal</button>

<!-- Modal component -->
<template>
<div v-show="active">
<!-- ... -->
</div>
</template>

<script> 
export default { // ... data() { return { active: false, }; }, methods: { open() { this.active = true; }, hide() { this.active = false; }, }, // ... };
</script>
<!-- 这里是应该避免的 -->
<!-- 如果可通过 emited 来做则避免通过 this.$refs 直接访问 -->
<template>
<range :max="max" :min="min" ref="range" :step="1"></range>
</template>

<script>
export default { // ... methods: { getRangeCurrentValue() { return this.$refs.range.currentValue; }, }, // ... };
</script>

使用组件名作为样式作用域空间


Vue.js 的组件是自定义元素,这非常适合用来作为样式的根作用域空间。可以将组件名作为 CSS 类的命名空间。


为什么?



  • 给样式加上作用域空间可以避免组件样式影响外部的样式。

  • 保持模块名、目录名、样式根作用域名一样,可以很好的将其关联起来,便于开发者理解。


怎么做?


使用组件名作为样式命名的前缀,可基于 BEM 或 OOCSS 范式。同时给 style 标签加上 scoped 属性。加上 scoped 属性编译后会给组件的 class 自动加上唯一的前缀从而避免样式的冲突。


<style scoped> /* 推荐 */ 
.MyExample { }
.MyExample li { }
.MyExample__item { }
/* 避免 */
.My-Example { }
/* 没有用组件名或模块名限制作用域, 不符合 BEM 规范 */
</style>


作者:_Battle
链接:https://juejin.cn/post/7023548108214648863

收起阅读 »

Vue 开发规范(上)

基于模块开发 始终基于模块的方式来构建你的 app,每一个子模块只做一件事情。 Vue.js 的设计初衷就是帮助开发者更好的开发界面模块。一个模块是应用程序中独立的一个部分。 怎么做? 每一个 Vue 组件(等同于模块)首先必须专注于解决一个单一的问题,独立的...
继续阅读 »

基于模块开发


始终基于模块的方式来构建你的 app,每一个子模块只做一件事情。


Vue.js 的设计初衷就是帮助开发者更好的开发界面模块。一个模块是应用程序中独立的一个部分。


怎么做?


每一个 Vue 组件(等同于模块)首先必须专注于解决一个单一的问题,独立的、可复用的、微小的 和 可测试的。


如果你的组件做了太多的事或是变得臃肿,请将其拆分成更小的组件并保持单一的原则。一般来说,尽量保证每一个文件的代码行数不要超过 100 行。也请保证组件可独立的运行。比较好的做法是增加一个单独的 demo 示例。


Vue 组件命名


组件的命名需遵从以下原则:



  • 有意义的: 不过于具体,也不过于抽象

  • 简短: 2 到 3 个单词

  • 具有可读性: 以便于沟通交流


同时还需要注意:




  • 必须符合自定义元素规范: 使用连字符分隔单词,切勿使用保留字。




  • app- 前缀作为命名空间: 如果非常通用的话可使用一个单词来命名,这样可以方便于其它项目里复用。




为什么?


组件是通过组件名来调用的。所以组件名必须简短、富有含义并且具有可读性。


如何做?


<!-- 推荐 -->
<app-header></app-header>
<user-list></user-list>
<range-slider></range-slider>

<!-- 避免 -->
<btn-group></btn-group> <!-- 虽然简短但是可读性差. 使用 `button-group` 替代 -->
<ui-slider></ui-slider> <!-- ui 前缀太过于宽泛,在这里意义不明确 -->
<slider></slider> <!-- 与自定义元素规范不兼容 -->

组件表达式简单化


Vue.js 的表达式是 100% 的 Javascript 表达式。这使得其功能性很强大,但也带来潜在的复杂性。因此,你应该尽量保持表达式的简单化。


为什么?




  • 复杂的行内表达式难以阅读。




  • 行内表达式是不能够通用的,这可能会导致重复编码的问题。




  • IDE 基本上不能识别行内表达式语法,所以使用行内表达式 IDE 不能提供自动补全和语法校验功能。




怎么做?


如果你发现写了太多复杂并难以阅读的行内表达式,那么可以使用 method 或是 computed 属性来替代其功能。


<!-- 推荐 -->
<template>
<h1>
{{ `${year}-${month}` }}
</h1>
</template>
<script type="text/javascript"> export default { computed: { month() { return this.twoDigits((new Date()).getUTCMonth() + 1); }, year() { return (new Date()).getUTCFullYear(); } }, methods: { twoDigits(num) { return ('0' + num).slice(-2); } }, }; </script>

<!-- 避免 -->
<template>
<h1>
{{ `${(new Date()).getUTCFullYear()}-${('0' + ((new Date()).getUTCMonth()+1)).slice(-2)}` }}
</h1>
</template>

组件 props 原子化


虽然 Vue.js 支持传递复杂的 JavaScript 对象通过 props 属性,但是你应该尽可能的使用原始类型的数据。尽量只使用 JavaScript 原始类型(字符串、数字、布尔值)和函数。尽量避免复杂的对象。


为什么?



  • 使得组件 API 清晰直观。

  • 只使用原始类型和函数作为 props 使得组件的 API 更接近于 HTML(5) 原生元素。

  • 其它开发者更好的理解每一个 prop 的含义、作用。

  • 传递过于复杂的对象使得我们不能够清楚的知道哪些属性或方法被自定义组件使用,这使得代码难以重构和维护。


怎么做?


组件的每一个属性单独使用一个 props,并且使用函数或是原始类型的值。







验证组件的 props


在 Vue.js 中,组件的 props 即 API,一个稳定并可预测的 API 会使得你的组件更容易被其他开发者使用。


组件 props 通过自定义标签的属性来传递。属性的值可以是 Vue.js 字符串(:attr="value" 或 v-bind:attr="value")或是不传。你需要保证组件的 props 能应对不同的情况。


为什么?


验证组件 props 可以保证你的组件永远是可用的(防御性编程)。即使其他开发者并未按照你预想的方法使用时也不会出错。


怎么做?



  • 提供默认值。

  • 使用 type 属性校验类型。

  • 使用 props 之前先检查该 prop 是否存在。


<template>
<input type="range" v-model="value" :max="max" :min="min">
</template>
<script type="text/javascript">
export default {
props: {
max: { type: Number, // 这里添加了数字类型的校验 default() { return 10; }, },
min: { type: Number, default() { return 0; }, },
value: { type: Number, default() { return 4; }, },
},
};
</script>

作者:_Battle
链接:https://juejin.cn/post/7023188232368029710

收起阅读 »

带你理解scoped、>>>、/deep/、::v-deep的原理

前言 平时开发项目我们在使用第三方插件时,必须使用element-ui的某些组件需要修改样式时,老是需要加上/deep/深度选择器,以前只是知道这样用,但是还不清楚他的原理。还有平时每个组件的样式都会加上scoped,但是也不知道他具体的原理。今天我就带大家理...
继续阅读 »

前言


平时开发项目我们在使用第三方插件时,必须使用element-ui的某些组件需要修改样式时,老是需要加上/deep/深度选择器,以前只是知道这样用,但是还不清楚他的原理。还有平时每个组件的样式都会加上scoped,但是也不知道他具体的原理。今天我就带大家理解理解理解


1. Scoped CSS的原理


1.1 区别


先带大家看一下无设置Scoped与设置Scoped的区别在哪


无设置Scoped


<div class="login">登录</div>
<style>
.login {
width: 100px;
height: 100px
}
</style>

打包之后的结果是跟我们的代码一摸一样的,没有区别。


设置Scoped


<div class="login">登录</div>
<style scoped>
.login {
width: 100px;
height: 100px
}
</style>

打包之后的结果是跟我们的代码就有所区别了。如下:


<div data-v-257dda99b class="login">登录</div>
<style scoped>
.login[data-v-257dda99b] {
width: 100px;
height: 100px
}
</style>


我们通过上面的例子,不难发现多了一个data-v-hash属性,也就是说加了scoped,PostCSS给一个组件中的所有dom添加了一个独一无二的动态属性,然后,给CSS选择器额外添加一个对应的属性选择器来选择该组件中dom,这种做法使得样式只作用于含有该属性的dom——组件内部dom,可以使得组件之间的样式不互相污染。



1.2 原理


Vue的作用域样式 Scoped CSS 的实现思路如下:



  1. 为每个组件实例(注意:是组件的实例,不是组件类)生成一个能唯一标识组件实例的标识符,我称它为组件实例标识,简称实例标识,记作 InstanceID;

  2. 给组件模板中的每一个标签对应的Dom元素(组件标签对应的Dom元素是该组件的根元素)添加一个标签属性,格式为 data-v-实例标识,示例:<div data-v-e0f690c0="" >

  3. 给组件的作用域样式 <style scoped> 的每一个选择器的最后一个选择器单元增加一个属性选择器 原选择器[data-v-实例标识] ,示例:假设原选择器为 .cls #id > div,则更改后的选择器为 .cls #id > div[data-v-e0f690c0]


1.3 特点




  1. 将组件的样式的作用范围限制在了组件自身的标签,即:组件内部,包含子组件的根标签,但不包含子组件的除根标签之外的其它标签;所以 组件的css选择器也不能选择到子组件及后代组件的中的元素(子组件的根元素除外);



    因为它给选择器的最后一个选择器单元增加了属性选择器 [data-v-实例标识] ,而该属性选择器只能选中当前组件模板中的标签;而对于子组件,只有根元素 即有 能代表子组件的标签属性 data-v-子实例标识,又有能代表当前组件(父组件)的 签属性 data-v-父实例标识,子组件的其它非根元素,仅有能代表子组件的标签属性 data-v-子实例标识





  2. 如果递归组件有后代选择器,则该选择器会打破特性1中所说的子组件限制,从而选中递归子组件的中元素;



    原因:假设递归组件A的作用域样式中有选择器有后代选择器 div p ,则在每次递归中都会为本次递归创建新的组件实例,同时也会为该实例生成对应的选择器 div p[data-v-当前递归组件实例的实例标识],对于递归组件的除了第一个递归实例之外的所有递归实例来说,虽然 div p[data-v-当前递归组件实例的实例标识] 不会选中子组件实例(递归子组件的实例)中的 p 元素(具体原因已在特性1中讲解),但是它会选中当前组件实例中所有的 p 元素,因为 父组件实例(递归父组件的实例)中有匹配的 div 元素;





2. >>>、/deep/、::v-deep深度选择器的原理


2.1 例子


实际开发中遇到的例子:当我们开发一个页面使用了子组件的时候,如果这时候需要改子组件的样式,但是又不影响其他页面使用这个子组件的样式的时候。比如:


父组件:Parent.vue


<template>
<div class="parent" id="app">
<h1>我是父组件</h1>
<div class="gby">
<p>我是一个段落</p>
</div>

<child></child>
</div>
</template>

<style scoped>
.parent {
background-color: green;
}

.gby p {
background-color: red;
}
// 把子组件的背景变成红色,原组件不变
.child .dyx p {
background-color: red;
}
</style>

子组件:Child.vue


<template>
<div class="child">
<h1>我是子组件</h1>
<div class="dyx">
<p>我是子组件的段落</p>
</div>
</div>
</template>

<style scoped>
.child .dyx p {
background-color: blue;
}
</style>

这时候我们就会发现没有效果。但是如果我们使用>>>/deep/::v-deep三个深度选择器其中一个就能实现了。看代码:


<template>
<div class="parent" id="app">
<h1>我是父组件</h1>
<div class="gby">
<p>我是一个段落</p>
</div>

<child></child>
</div>
</template>

<style scoped>
.parent {
background-color: green;
}

.gby p {
background-color: red;
}
// 把子组件的背景变成红色,原组件不变
::v-deep .child .dyx p {
background-color: red;
}
</style>

2.2 原理


如果你希望 scoped 样式中的一个选择器能够选择到子组 或 后代组件中的元素,我们可以使用 深度作用选择器,它有三种写法:



  • >>>,示例: .gby div >>> #dyx p

  • /deep/,示例: .gby div /deep/ #dyx p.gby div/deep/ #dyx p

  • ::v-deep,示例: .gby div::v-deep #dyx p.gby div::v-deep #dyx p


它的原理与 Scoped CSS 的原理基本一样,只是第3步有些不同(前2步一样),具体如下:



  1. 为每个组件实例(注意:是组件的实例,不是组件类)生成一个能唯一标识组件的标识符,我称它为实例标识,记作 InstanceID;

  2. 给组件模板中的每一个标签对应的Dom元素(组件标签对应的Dom元素是该组件的根元素)添加一个标签属性,格式为 data-v-实例标识,示例:<div data-v-e0f690c0="" >

  3. 给组件的作用域样式 <style scoped> 的每一个深度作用选择器前面的一个选择器单元增加一个属性选择器[data-v-实例标识] ,示例:假设原选择器为 .cls #id >>> div,则更改后的选择器为 .cls #id[data-v-e0f690c0] div


因为Vue不会为深度作用选择器后面的选择器单元增加 属性选择器[data-v-实例标识],所以,后面的选择器单元能够选择到子组件及后代组件中的元素;



收起阅读 »

Moshi踩坑之ArrayList

就是这个错 moshi让你写自定义Adapter呢,No JsonAdapter for class java.util.ArrayList, you should probably use List instead of ArrayList (Mo...
继续阅读 »

就是这个错 moshi让你写自定义Adapter呢,

No JsonAdapter for class java.util.ArrayList, you should probably use List instead of ArrayList (Moshi only supports the collection interfaces by default) or else register a custom JsonAdapter.
java.lang.IllegalArgumentException: No JsonAdapter for class java.util.ArrayList, you should probably use List instead of ArrayList (Moshi only supports the collection interfaces by default) or else register a custom JsonAdapter.

解决方法

代码如下自己看吧,这几天就因为这个moshi搞死人哦。

abstract class MoshiArrayListJsonAdapter<C : MutableCollection<T>?, T> private constructor(
private val elementAdapter: JsonAdapter<T>
) :
JsonAdapter<C>() {
abstract fun newCollection(): C

@Throws(IOException::class)
override fun fromJson(reader: JsonReader): C {
val result = newCollection()
reader.beginArray()
while (reader.hasNext()) {
result?.add(elementAdapter.fromJson(reader)!!)
}
reader.endArray()
return result
}

@Throws(IOException::class)
override fun toJson(writer: JsonWriter, value: C?) {
writer.beginArray()
for (element in value!!) {
elementAdapter.toJson(writer, element)
}
writer.endArray()
}

override fun toString(): String {
return "$elementAdapter.collection()"
}

companion object {
val FACTORY = Factory { type, annotations, moshi ->
val rawType = Types.getRawType(type)
if (annotations.isNotEmpty()) return@Factory null
if (rawType == ArrayList::class.java) {
return@Factory newArrayListAdapter<Any>(
type,
moshi
).nullSafe()
}
null
}

private fun <T> newArrayListAdapter(
type: Type,
moshi: Moshi
): JsonAdapter<MutableCollection<T>> {
val elementType =
Types.collectionElementType(
type,
MutableCollection::class.java
)

val elementAdapter: JsonAdapter<T> = moshi.adapter(elementType)

return object :
MoshiArrayListJsonAdapter<MutableCollection<T>, T>(elementAdapter) {
override fun newCollection(): MutableCollection<T> {
return ArrayList()
}
}
}
}
}

用法

本来不想写 但怕有人骂我代码不写全

    Moshi.Builder()
.add(MoshiArrayListJsonAdapter.FACTORY)
.build()


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

收起阅读 »

Android 序列化(Serializable和Parcelable)

什么是序列化 由于存在于内存中的对象都是暂时的,无法长期驻存,为了把对象的状态保持下来,这时需要把对象写入到磁盘或者其他介质中,这个过程就叫做序列化。 🔥 为什么序列化 永久的保存对象数据(将对象数据保存在文件当中、或者是磁盘中)。 对象在网络中传递。 对象...
继续阅读 »

什么是序列化


由于存在于内存中的对象都是暂时的,无法长期驻存,为了把对象的状态保持下来,这时需要把对象写入到磁盘或者其他介质中,这个过程就叫做序列化。


🔥 为什么序列化



  • 永久的保存对象数据(将对象数据保存在文件当中、或者是磁盘中)。

  • 对象在网络中传递。

  • 对象在IPC间传递。


🔥 实现序列化的方式



  • 实现Serializable接口

  • 实现Parcelable接口


🔥 Serializable 和 Parcelable 区别




  • Serializable 是Java本身就支持的接口。




  • Parcelable 是Android特有的接口,效率比实现Serializable接口高效(可用于Intent数据传递,也可以用于进程间通信(IPC))。




  • Serializable的实现,只需要implements Serializable即可。这只是给对象打了一个标记,系统会自动将其序列化。




  • Parcelabel的实现,不仅需要implements Parcelabel,还需要在类中添加一个静态成员变量CREATOR,这个变量需要实现 Parcelable.Creator接口。




  • Serializable 使用I/O读写存储在硬盘上,而Parcelable是直接在内存中读写。




  • Serializable 会使用反射,序列化和反序列化过程需要大量I/O操作,Parcelable 自己实现封送和解封(marshalled &unmarshalled)操作不需要用反射,数据也存放在Native内存中,效率要快很多




💥 实现Serializable


import java.io.Serializable;

public class UserSerializable implements Serializable {
public String name;
public int age;
}

然后你会发现没有serialVersionUID


Android Studio 是默认关闭 serialVersionUID 生成提示的,我们需要打开Setting,执行如下操作:



再次回到UserSerializable类,有个提示,就可以添加serialVersionUID了。



效果如下:


public class UserSerializable implements Serializable {
private static final long serialVersionUID = 1522126340746830861L;
public String name;
public int age = 0;

}

💥 实现Parcelable


Parcelabel的实现,不仅需要实现Parcelabel接口,还需要在类中添加一个静态成员变量CREATOR,这个变量需要实现 Parcelable.Creator 接口,并实现读写的抽象方法。如下:


 public class MyParcelable implements Parcelable {
private int mData;

public int describeContents() {
return 0;
}

public void writeToParcel(Parcel out, int flags) {
out.writeInt(mData);
}

public static final Parcelable.Creator<MyParcelable> CREATOR
= new Parcelable.Creator<MyParcelable>() {
public MyParcelable createFromParcel(Parcel in) {
return new MyParcelable(in);
}

public MyParcelable[] newArray(int size) {
return new MyParcelable[size];
}
};

private MyParcelable(Parcel in) {
mData = in.readInt();
}
}

此时Android Studio 给我们了一个插件可自动生成Parcelable 。


🔥 自动生成 Parcelable


public class User {
String name;
int age;

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}
}

想进行序列化,但是自己写太麻烦了,这里介绍个插件操作简单易上手。


💥先下载



💥使用





💥效果


public class User implements Parcelable {
String name;
int age;

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}

@Override
public int describeContents() {
return 0;
}

@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(this.name);
dest.writeInt(this.age);
}

public void readFromParcel(Parcel source) {
this.name = source.readString();
this.age = source.readInt();
}

public User() {
}

protected User(Parcel in) {
this.name = in.readString();
this.age = in.readInt();
}

public static final Parcelable.Creator<User> CREATOR = new Parcelable.Creator<User>() {
@Override
public User createFromParcel(Parcel source) {
return new User(source);
}

@Override
public User[] newArray(int size) {
return new User[size];
}
};
}

搞定。


写完了咱就运行走一波。


🔥 使用实例


💥 Serializable



MainActivity.class
Bundle bundle = new Bundle();
UserSerializable userSerializable=new UserSerializable("SCC",15);
bundle.putSerializable("user",userSerializable);
Intent intent = new Intent(MainActivity.this, BunderActivity.class);
intent.putExtra("user",bundle);
startActivity(intent);

BunderActivity.class
Bundle bundle = getIntent().getBundleExtra("user");
UserSerializable userSerializable= (UserSerializable) bundle.getSerializable("user");
MLog.e("Serializable:"+userSerializable.name+userSerializable.age);

日志:
2021-10-25 E/-SCC-: Serializable:SCC15

💥 Parcelable



MainActivity.class
Bundle bundle = new Bundle();
bundle.putParcelable("user",new UserParcelable("SCC",15));
Intent intent = new Intent(MainActivity.this, BunderActivity.class);
intent.putExtra("user",bundle);
startActivity(intent);

BunderActivity.class
Bundle bundle = getIntent().getBundleExtra("user");
UserParcelable userParcelable= (UserParcelable) bundle.getParcelable("user");
MLog.e("Parcelable:"+userParcelable.getName()+userParcelable.getAge());

日志:
2021-10-25 E/-SCC-: Parcelable:SCC15

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

看动画学算法之:队列queue

简介 队列Queue是一个非常常见的数据结构,所谓队列就是先进先出的序列结构。 想象一下我们日常的排队买票,只能向队尾插入数据,然后从队头取数据。在大型项目中常用的消息中间件就是一个队列的非常好的实现。 队列的实现 一个队列需要一个enQueue入队列操作和一...
继续阅读 »

简介


队列Queue是一个非常常见的数据结构,所谓队列就是先进先出的序列结构。


想象一下我们日常的排队买票,只能向队尾插入数据,然后从队头取数据。在大型项目中常用的消息中间件就是一个队列的非常好的实现。


队列的实现


一个队列需要一个enQueue入队列操作和一个DeQueue操作,当然还可以有一些辅助操作,比如isEmpty判断队列是否为空,isFull判断队列是否满员等等。



为了实现在队列头和队列尾进行方便的操作,我们需要保存队首和队尾的标记。


先看一下动画,直观的感受一下队列是怎么入队和出队的。


先看入队:



再看出队:



可以看到入队是从队尾入,而出队是从队首出。


队列的数组实现


和栈一样,队列也有很多种实现方式,最基本的可以使用数组或者链表来实现。


先考虑一下使用数组来存储数据的情况。


我们用head表示队首的index,使用rear表示队尾的index。


当队尾不断插入,队首不断取数据的情况下,很有可能出现下面的情况:



上面图中,head的index已经是2了,rear已经到了数组的最后面,再往数组里面插数据应该怎么插入呢?


如果再往rear后面插入数据,head前面的两个空间就浪费了。这时候需要我们使用循环数组。


循环数组怎么实现呢?只需要把数组的最后一个节点和数组的最前面的一个节点连接即可。



有同学又要问了。数组怎么变成循环数组呢?数组又不能像链表那样前后连接。


不急,我们先考虑一个余数的概念,假如我们知道了数组的capacity,当要想数组插入数据的时候,我们还是照常的将rear+1,但是最后除以数组的capacity, 队尾变到了队首,也就间接的实现了循环数组。


看下java代码是怎么实现的:


public class ArrayQueue {

//存储数据的数组
private int[] array;
//head索引
private int head;
//real索引
private int rear;
//数组容量
private int capacity;

public ArrayQueue (int capacity){
this.capacity=capacity;
this.head=-1;
this.rear =-1;
this.array= new int[capacity];
}

public boolean isEmpty(){
return head == -1;
}

public boolean isFull(){
return (rear +1)%capacity==head;
}

public int getQueueSize(){
if(head == -1){
return 0;
}
return (rear +1-head+capacity)%capacity;
}

//从尾部入队列
public void enQueue(int data){
if(isFull()){
System.out.println("Queue is full");
}else{
//从尾部插入
rear = (rear +1)%capacity;
array[rear]= data;
//如果插入之前队列为空,将head指向real
if(head == -1 ){
head = rear;
}
}
}

//从头部取数据
public int deQueue(){
int data;
if(isEmpty()){
System.out.println("Queue is empty");
return -1;
}else{
data= array[head];
//如果只有一个元素,则重置head和real
if(head == rear){
head= -1;
rear = -1;
}else{
head = (head+1)%capacity;
}
return data;
}
}
}

大家注意我们的enQueue和deQueue中使用的方法:


rear = (rear +1)%capacity
head = (head+1)%capacity

这两个就是循环数组的实现。


队列的动态数组实现


上面的实现其实有一个问题,数组的大小是写死的,不能够动态扩容。我们再实现一个能够动态扩容的动态数组实现。


    //因为是循环数组,这里不能做简单的数组拷贝
private void extendQueue(){
int newCapacity= capacity*2;
int[] newArray= new int[newCapacity];
//先全部拷贝
System.arraycopy(array,0,newArray,0,array.length);
//如果real<head,表示已经进行循环了,需要将0-head之间的数据置空,并将数据拷贝到新数组的相应位置
if(rear< head){
for(int i=0; i< head; i++){
//重置0-head的数据
newArray[i]= -1;
//拷贝到新的位置
newArray[i+capacity]=array[i];
}
//重置real的位置
rear= rear+capacity;
//重置capacity和array
capacity=newCapacity;
array=newArray;
}
}

需要注意的是,在进行数组扩展的时候,我们不能简单的进行拷贝,因为是循环数组,可能出现rear在head后面的情况。这个时候我们需要对数组进行特殊处理。


其他部分是和普通数组实现基本一样的。


队列的链表实现


除了使用数组,我们还可以使用链表来实现队列,只需要在头部删除和尾部添加即可。


看下java代码实现:


public class LinkedListQueue {
//head节点
private Node headNode;
//rear节点
private Node rearNode;

class Node {
int data;
Node next;
//Node的构造函数
Node(int d) {
data = d;
}
}

public boolean isEmpty(){
return headNode==null;
}

public void enQueue(int data){
Node newNode= new Node(data);
//将rearNode的next指向新插入的节点
if(rearNode !=null){
rearNode.next=newNode;
}
rearNode=newNode;
if(headNode == null){
headNode=newNode;
}
}

public int deQueue(){
int data;
if(isEmpty()){
System.out.println("Queue is empty");
return -1;
}else{
data=headNode.data;
headNode=headNode.next;
}
return data;
}
}

队列的时间复杂度


上面的3种实现的enQueue和deQueue方法,基本上都可以立马定位到要入队列或者出队列的位置,所以他们的时间复杂度是O(1)。


本文的代码地址:


learn-algorithm


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

来讨论下 Android 面试该问什么?

经历过一些面试,也面过一些同学。 被面试官问到头皮发麻,也把候选人问得面红耳赤。 曾怨恨问题刁钻刻薄,也曾怀疑提问跑题超纲。 经历过攻守的角色转换后,沉下心,回顾过往,不由得发出感叹。如果要将“面试”作类比的话,我愿意将其比作“相亲”。 之所以这样类比,是因为...
继续阅读 »

经历过一些面试,也面过一些同学。


被面试官问到头皮发麻,也把候选人问得面红耳赤。


曾怨恨问题刁钻刻薄,也曾怀疑提问跑题超纲。


经历过攻守的角色转换后,沉下心,回顾过往,不由得发出感叹。如果要将“面试”作类比的话,我愿意将其比作“相亲”。


之所以这样类比,是因为看似客观的技术面试,其实充斥了各种各样的主观判断。“候选人合不合面试官胃口”可能比“候选人有多优秀”更重要一点。


世界这么大,Android 知识体系这么庞杂,我也时不时地怀疑自己,特别是当 pass 一个候选人之后,这种情感愈发强烈。“是不是自己的知识有局限性?”、“我认为关键的问题,真的这么关键吗?”


带着这样的怀疑,我对自己的面试偏好做了一下总结,在此抛砖引玉,欢迎各路大神指点迷津。


ps:本篇仅关注 Android 应用层开发相关面试。


八股文式问题



  1. Activity 有几种 launch mode?每一种有什么特点?

  2. Service 有几种类型?各有什么应用场景?

  3. 广播有几种注册方式?有什么区别?

  4. Activity 有哪些生命周期回调?

  5. Kotlin 中的扩展函数是什么?

  6. JVM 内存模型是怎么样的?

  7. GC 回收算法?

  8. Java 中有几种引用类型?


这类问题的特点是“只需百度即可立马获得答案”。候选人若做过充足的准备,刷过题,就可以倒背如流。但这些问题也是有价值的,可以快速判断候选人是否了解 Android 的基本概念。


上面的第 6,7 问,我不太喜欢问。原因是“掌握了这个问题对应用层开发能起到什么可见的好处?”


计算机的复杂度高,分层是常用的降低复杂度的方法,层与层之间形成了壁垒,也提高了层内的效率。将单独一层的复杂度吃透,都可能要花去毕生的精力。并不是否定深挖底层的价值,学有余力当然可以打通好几层,但作为 Android 应用层的面试,重点还是要关注应用层的技术细节。(个人愚见,欢迎拍砖~)


但如果面试中全都是八股文式问题,则不太公平,太过偏袒死记硬背者,也可能因此 pass 掉能力很强,但基本问题准备不太充分的候选人。


原理性问题


这类问题旨在考察候选人的技术深度,在会用的技术上,知道为什么用它,及其背后的实现原理。比如:



  1. Android 消息机制是怎么实现的?

  2. Android 触摸事件如何传递?

  3. Android 视图是怎么被绘制出来的?

  4. Android 如何在不同组件间通信?(跨进程,跨线程)

  5. Activity 启动流程?

  6. AMS、PMS、WMS 创建过程?

  7. 手写消息入 MessageQueue 的算法。

  8. RecyclerView 缓存机制?


原理性问题也可以被百度出来,但可能得多看几篇博客再消化一番,最后用自己的语言组织一下,才能在面试中对答如流。


这类问题不同于八股文的地方不仅在于考察了技术深度,还顺带便考察了理解分析能力和总结表达能力。把原理性的东西用简单精炼的语言表达出来并让人听懂也是一种能力。


我不太喜欢问 5、6 这样的问题,还是之前提到的那个原因,即“回答出这样的问题对应用层开发能起到什么可见的好处?”。若是 Android 系统开发工程的面试,倒是很有必要问。


第 7 问将原理性和算法结合,不是让默写算法,而是在考察理解原理的基础上的算法实现能力。若死记硬背原理,通常都写不出。


项目经历类问题


这类问题旨在考察候选人项目经历是否真实,技术栈情况。也可就某一个使用过的技术栈追问背后的原理。


这类问题对面试官要求最高,若是没有一定的技术广度和深度,很难就候选人的技术栈问出好问题。


场景类问题


场景类问题是指设计一个“待解决的问题”,让候选人当场解决。


所有前面的问题,都可以提前准备,若准备足够充分,全部拿下不是问题。而场景题是无法提前准备的。



  1. 如图所示:按住View,移到 View 边界外后松手。这个过程中,哪些触摸事件会被传递,它们是如何传递的?


image.png



  1. 要做一个 1MB * 10 的帧动画,有什么办法优化内存?

  2. 如何防止搜索框过度频繁地发起请求?

  3. 如何实现弹幕?

  4. 如何设计直播间礼物队列?

  5. 设计图片异步加载组件需要注意哪些方面?


第 1 问将原理性问题场景化了,对死记硬背不友好。


这些问题都是应用层开发过程中可能遇到的技术问题,场景类问题是开放性的,没有唯一解,考察候选人的思路、技术积累及综合运用能力,甚至是抗压能力。


但场景类问题也有致命的缺点,受到面试官知识及经验的限制,面试官见过多少世面,就能问出多少问题。若面试官经验恰好和候选人有交集则两情相悦,不然则可能话不投机。所以这类问题也不是公平的,就好像相亲,甲之蜜糖乙之砒霜是有可能出现的。


需求拆解估时问题


即把一个真真切切的迭代需求给到面试者,要求把业务需求拆解成技术步骤,然后为每个步骤精确估时。


不要小看“需求拆解”,首先得深入领会需求,能否把产品想表达的理解到位?,能否意会产品想表达而为表达之意?在实际迭代过程中,产品和研发对需求理解的不一致是屡见不鲜的,候选人会不会和产品成为好朋友?


在深入领会需求的基础上,能否将业务故事拆解成技术步骤?考察候选人掌握的技术栈及其综合运用能力,技术选型及实现方案是否合理?是否将扩展性或性能优化考虑在内?


“估时”可以看出候选人对技术实现细节的熟练程度,假设“用 ViewPager + Fragment 实现分页框架”的估时是 1 天,那说明虽然了解改用什么技术,但并未实践过。但此时的估时是理想化的,因为没有将应用的代码现状考虑在内。


这些问题也是候选者入职之后,在每次迭代时真真切切遇到的问题。“拆解合理,估时准确”不是一件容易的事情,即需要深入领会需求、有丰富的技术栈实战经验,还需要对现有代码框架了然于胸,这是一个成熟研发的标志。


没有找到比需求拆解估时问题更务实的面试题了。若相亲的第一感觉不可靠,那就试着约会一次。


总结


我对面试的偏好是按罗列顺序递进的,但水平有限,经验局限,对 Android 应用层的面试也只能停留在这个阶段。还望掘金大神点拨~


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

Swift 方法

Swift 方法是与某些特定类型相关联的函数在 Objective-C 中,类是唯一能定义方法的类型。但在 Swift 中,你不仅能选择是否要定义一个类/结构体/枚举,还能灵活的在你创建的类型(类/结构体/枚举)上定义方法。实例方法在 Swift 语言中,实例...
继续阅读 »

Swift 方法是与某些特定类型相关联的函数

在 Objective-C 中,类是唯一能定义方法的类型。但在 Swift 中,你不仅能选择是否要定义一个类/结构体/枚举,还能灵活的在你创建的类型(类/结构体/枚举)上定义方法。


实例方法

在 Swift 语言中,实例方法是属于某个特定类、结构体或者枚举类型实例的方法。

实例方法提供以下方法:

  • 可以访问和修改实例属性

  • 提供与实例目的相关的功能

实例方法要写在它所属的类型的前后大括号({})之间。

实例方法能够隐式访问它所属类型的所有的其他实例方法和属性。

实例方法只能被它所属的类的某个特定实例调用。

实例方法不能脱离于现存的实例而被调用。

语法

func funcname(Parameters) -> returntype
{
Statement1
Statement2
……
Statement N
return parameters
}

实例

import Cocoa

class Counter {
var count = 0
func increment() {
count += 1
}
func incrementBy(amount: Int) {
count += amount
}
func reset() {
count = 0
}
}
// 初始计数值是0
let counter = Counter()

// 计数值现在是1
counter.increment()

// 计数值现在是6
counter.incrementBy(amount: 5)
print(counter.count)

// 计数值现在是0
counter.reset()
print(counter.count)

以上程序执行输出结果为:

6
0

Counter类定义了三个实例方法:

  • increment 让计数器按 1 递增;
  • incrementBy(amount: Int) 让计数器按一个指定的整数值递增;
  • reset 将计数器重置为0。

Counter 这个类还声明了一个可变属性 count,用它来保持对当前计数器值的追踪。


方法的局部参数名称和外部参数名称

Swift 函数参数可以同时有一个局部名称(在函数体内部使用)和一个外部名称(在调用函数时使用

Swift 中的方法和 Objective-C 中的方法极其相似。像在 Objective-C 中一样,Swift 中方法的名称通常用一个介词指向方法的第一个参数,比如:with,for,by等等。

Swift 默认仅给方法的第一个参数名称一个局部参数名称;默认同时给第二个和后续的参数名称为全局参数名称。

以下实例中 'no1' 在swift中声明为局部参数名称。'no2' 用于全局的声明并通过外部程序访问。

import Cocoa

class division {
var count: Int = 0
func incrementBy(no1: Int, no2: Int) {
count = no1 / no2
print(count)
}
}

let counter = division()
counter.incrementBy(no1: 1800, no2: 3)
counter.incrementBy(no1: 1600, no2: 5)
counter.incrementBy(no1: 11000, no2: 3)

以上程序执行输出结果为:

600
320
3666

是否提供外部名称设置

我们强制在第一个参数添加外部名称把这个局部名称当作外部名称使用(Swift 2.0前是使用 # 号)。

相反,我们呢也可以使用下划线(_)设置第二个及后续的参数不提供一个外部名称。

import Cocoa

class multiplication {
var count: Int = 0
func incrementBy(first no1: Int, no2: Int) {
count = no1 * no2
print(count)
}
}

let counter = multiplication()
counter.incrementBy(first: 800, no2: 3)
counter.incrementBy(first: 100, no2: 5)
counter.incrementBy(first: 15000, no2: 3)

以上程序执行输出结果为:

2400
500
45000

self 属性

类型的每一个实例都有一个隐含属性叫做self,self 完全等同于该实例本身。

你可以在一个实例的实例方法中使用这个隐含的self属性来引用当前实例。

import Cocoa

class calculations {
let a: Int
let b: Int
let res: Int

init(a: Int, b: Int) {
self.a = a
self.b = b
res = a + b
print("Self 内: \(res)")
}

func tot(c: Int) -> Int {
return res - c
}

func result() {
print("结果为: \(tot(c: 20))")
print("结果为: \(tot(c: 50))")
}
}

let pri = calculations(a: 600, b: 300)
let sum = calculations(a: 1200, b: 300)

pri.result()
sum.result()

以上程序执行输出结果为:

Self 内: 900
Self 内: 1500
结果为: 880
结果为: 850
结果为: 1480
结果为: 1450

在实例方法中修改值类型

Swift 语言中结构体和枚举是值类型。一般情况下,值类型的属性不能在它的实例方法中被修改。

但是,如果你确实需要在某个具体的方法中修改结构体或者枚举的属性,你可以选择变异(mutating)这个方法,然后方法就可以从方法内部改变它的属性;并且它做的任何改变在方法结束时还会保留在原始结构中。

方法还可以给它隐含的self属性赋值一个全新的实例,这个新实例在方法结束后将替换原来的实例。

import Cocoa

struct area {
var length = 1
var breadth = 1

func area() -> Int {
return length * breadth
}

mutating func scaleBy(res: Int) {
length *= res
breadth *= res

print(length)
print(breadth)
}
}

var val = area(length: 3, breadth: 5)
val.scaleBy(res: 3)
val.scaleBy(res: 30)
val.scaleBy(res: 300)

以上程序执行输出结果为:

9
15
270
450
81000
135000

在可变方法中给 self 赋值

可变方法能够赋给隐含属性 self 一个全新的实例。

import Cocoa

struct area {
var length = 1
var breadth = 1

func area() -> Int {
return length * breadth
}

mutating func scaleBy(res: Int) {
self.length *= res
self.breadth *= res
print(length)
print(breadth)
}
}
var val = area(length: 3, breadth: 5)
val.scaleBy(res: 13)

以上程序执行输出结果为:

39
65

类型方法

实例方法是被类型的某个实例调用的方法,你也可以定义类型本身调用的方法,这种方法就叫做类型方法。

声明结构体和枚举的类型方法,在方法的func关键字之前加上关键字static。类可能会用关键字class来允许子类重写父类的实现方法。

类型方法和实例方法一样用点号(.)语法调用。

import Cocoa

class Math
{
class func abs(number: Int) -> Int
{
if number < 0
{
return (-number)
}
else
{
return number
}
}
}

struct absno
{
static func abs(number: Int) -> Int
{
if number < 0
{
return (-number)
}
else
{
return number
}
}
}

let no = Math.abs(number: -35)
let num = absno.abs(number: -5)

print(no)
print(num)

以上程序执行输出结果为:

35
5
收起阅读 »

Swift 属性

Swift 属性将值跟特定的类、结构或枚举关联。属性可分为存储属性和计算属性:存储属性计算属性存储常量或变量作为实例的一部分计算(而不是存储)一个值用于类和结构体用于类、结构体和枚举存储属性和计算属性通常用于特定类型的实例。属性也可以直接用于类型本身,这种属性...
继续阅读 »

Swift 属性将值跟特定的类、结构或枚举关联。

属性可分为存储属性和计算属性:

存储属性计算属性
存储常量或变量作为实例的一部分计算(而不是存储)一个值
用于类和结构体用于类、结构体和枚举

存储属性和计算属性通常用于特定类型的实例。

属性也可以直接用于类型本身,这种属性称为类型属性。

另外,还可以定义属性观察器来监控属性值的变化,以此来触发一个自定义的操作。属性观察器可以添加到自己写的存储属性上,也可以添加到从父类继承的属性上。


存储属性

简单来说,一个存储属性就是存储在特定类或结构体的实例里的一个常量或变量。

存储属性可以是变量存储属性(用关键字var定义),也可以是常量存储属性(用关键字let定义)。

  • 可以在定义存储属性的时候指定默认值

  • 也可以在构造过程中设置或修改存储属性的值,甚至修改常量存储属性的值

import Cocoa

struct Number
{
var digits: Int
let pi = 3.1415
}

var n = Number(digits: 12345)
n.digits = 67

print("\(n.digits)")
print("\(n.pi)")

以上程序执行输出结果为:

67
3.1415

考虑以下代码:

let pi = 3.1415

代码中 pi 在定义存储属性的时候指定默认值(pi = 3.1415),所以不管你什么时候实例化结构体,它都不会改变。

如果你定义的是一个常量存储属性,如果尝试修改它就会报错,如下所示:

import Cocoa

struct Number
{
var digits: Int
let numbers = 3.1415
}

var n = Number(digits: 12345)
n.digits = 67

print("\(n.digits)")
print("\(n.numbers)")
n.numbers = 8.7

以上程序,执行会报错,错误如下所示:

error: cannot assign to property: 'numbers' is a 'let' constant
n.numbers = 8.7

意思为 'numbers' 是一个常量,你不能修改它。


延迟存储属性

延迟存储属性是指当第一次被调用的时候才会计算其初始值的属性。

在属性声明前使用 lazy 来标示一个延迟存储属性。

注意:
必须将延迟存储属性声明成变量(使用var关键字),因为属性的值在实例构造完成之前可能无法得到。而常量属性在构造过程完成之前必须要有初始值,因此无法声明成延迟属性。

延迟存储属性一般用于:

  • 延迟对象的创建。

  • 当属性的值依赖于其他未知类

import Cocoa

class sample {
lazy var no = number() // `var` 关键字是必须的
}

class number {
var name = "Runoob Swift 教程"
}

var firstsample = sample()
print(firstsample.no.name)

以上程序执行输出结果为:

Runoob Swift 教程

实例化变量

如果您有过 Objective-C 经验,应该知道Objective-C 为类实例存储值和引用提供两种方法。对于属性来说,也可以使用实例变量作为属性值的后端存储。

Swift 编程语言中把这些理论统一用属性来实现。Swift 中的属性没有对应的实例变量,属性的后端存储也无法直接访问。这就避免了不同场景下访问方式的困扰,同时也将属性的定义简化成一个语句。

一个类型中属性的全部信息——包括命名、类型和内存管理特征——都在唯一一个地方(类型定义中)定义。


计算属性

除存储属性外,类、结构体和枚举可以定义计算属性,计算属性不直接存储值,而是提供一个 getter 来获取值,一个可选的 setter 来间接设置其他属性或变量的值。

import Cocoa

class sample {
var no1 = 0.0, no2 = 0.0
var length = 300.0, breadth = 150.0

var middle: (Double, Double) {
get{
return (length / 2, breadth / 2)
}
set(axis){
no1 = axis.0 - (length / 2)
no2 = axis.1 - (breadth / 2)
}
}
}

var result = sample()
print(result.middle)
result.middle = (0.0, 10.0)

print(result.no1)
print(result.no2)

以上程序执行输出结果为:

(150.0, 75.0)
-150.0
-65.0

如果计算属性的 setter 没有定义表示新值的参数名,则可以使用默认名称 newValue。


只读计算属性

只有 getter 没有 setter 的计算属性就是只读计算属性。

只读计算属性总是返回一个值,可以通过点(.)运算符访问,但不能设置新的值。

import Cocoa

class film {
var head = ""
var duration = 0.0
var metaInfo: [String:String] {
return [
"head": self.head,
"duration":"\(self.duration)"
]
}
}

var movie = film()
movie.head = "Swift 属性"
movie.duration = 3.09

print(movie.metaInfo["head"]!)
print(movie.metaInfo["duration"]!)

以上程序执行输出结果为:

Swift 属性
3.09

注意:

必须使用var关键字定义计算属性,包括只读计算属性,因为它们的值不是固定的。let关键字只用来声明常量属性,表示初始化后再也无法修改的值。


属性观察器

属性观察器监控和响应属性值的变化,每次属性被设置值的时候都会调用属性观察器,甚至新的值和现在的值相同的时候也不例外。

可以为除了延迟存储属性之外的其他存储属性添加属性观察器,也可以通过重载属性的方式为继承的属性(包括存储属性和计算属性)添加属性观察器。

注意:
不需要为无法重载的计算属性添加属性观察器,因为可以通过 setter 直接监控和响应值的变化。

可以为属性添加如下的一个或全部观察器:

  • willSet在设置新的值之前调用
  • didSet在新的值被设置之后立即调用
  • willSet和didSet观察器在属性初始化过程中不会被调用
import Cocoa

class Samplepgm {
var counter: Int = 0{
willSet(newTotal){
print("计数器: \(newTotal)")
}
didSet{
if counter > oldValue {
print("新增数 \(counter - oldValue)")
}
}
}
}
let NewCounter = Samplepgm()
NewCounter.counter = 100
NewCounter.counter = 800

以上程序执行输出结果为:

计数器: 100
新增数 100
计数器: 800
新增数 700

全局变量和局部变量

计算属性和属性观察器所描述的模式也可以用于全局变量和局部变量。

局部变量全局变量
在函数、方法或闭包内部定义的变量。函数、方法、闭包或任何类型之外定义的变量。
用于存储和检索值。用于存储和检索值。
存储属性用于获取和设置值。存储属性用于获取和设置值。
也用于计算属性。也用于计算属性。

类型属性

类型属性是作为类型定义的一部分写在类型最外层的花括号({})内。

使用关键字 static 来定义值类型的类型属性,关键字 class 来为类定义类型属性。

struct Structname {
static var storedTypeProperty = " "
static var computedTypeProperty: Int {
// 这里返回一个 Int 值
}
}

enum Enumname {
static var storedTypeProperty = " "
static var computedTypeProperty: Int {
// 这里返回一个 Int 值
}
}

class Classname {
class var computedTypeProperty: Int {
// 这里返回一个 Int 值
}
}

注意:
例子中的计算型类型属性是只读的,但也可以定义可读可写的计算型类型属性,跟实例计算属性的语法类似。


获取和设置类型属性的值

类似于实例的属性,类型属性的访问也是通过点运算符(.)来进行。但是,类型属性是通过类型本身来获取和设置,而不是通过实例。实例如下:

import Cocoa

struct StudMarks {
static let markCount = 97
static var totalCount = 0
var InternalMarks: Int = 0 {
didSet {
if InternalMarks > StudMarks.markCount {
InternalMarks = StudMarks.markCount
}
if InternalMarks > StudMarks.totalCount {
StudMarks.totalCount = InternalMarks
}
}
}
}

var stud1Mark1 = StudMarks()
var stud1Mark2 = StudMarks()

stud1Mark1.InternalMarks = 98
print(stud1Mark1.InternalMarks)

stud1Mark2.InternalMarks = 87
print(stud1Mark2.InternalMarks)

以上程序执行输出结果为:

97
87
收起阅读 »

Swift 类

Swift 类是构建代码所用的一种通用且灵活的构造体。我们可以为类定义属性(常量、变量)和方法。与其他编程语言所不同的是,Swift 并不要求你为自定义类去创建独立的接口和实现文件。你所要做的是在一个单一文件中定义一个类,系统会自动生成面向其它代码的外部接口。...
继续阅读 »

Swift 类是构建代码所用的一种通用且灵活的构造体。

我们可以为类定义属性(常量、变量)和方法。

与其他编程语言所不同的是,Swift 并不要求你为自定义类去创建独立的接口和实现文件。你所要做的是在一个单一文件中定义一个类,系统会自动生成面向其它代码的外部接口。

类和结构体对比

Swift 中类和结构体有很多共同点。共同处在于:

  • 定义属性用于存储值
  • 定义方法用于提供功能
  • 定义附属脚本用于访问值
  • 定义构造器用于生成初始化值
  • 通过扩展以增加默认实现的功能
  • 符合协议以对某类提供标准功能

与结构体相比,类还有如下的附加功能:

  • 继承允许一个类继承另一个类的特征
  • 类型转换允许在运行时检查和解释一个类实例的类型
  • 解构器允许一个类实例释放任何其所被分配的资源
  • 引用计数允许对一个类的多次引用

语法:

class classname {
Definition 1
Definition 2
……
Definition N
}

类定义

class student{
var studname: String
var mark: Int
var mark2: Int
}

实例化类:

let studrecord = student()

实例

import Cocoa

class MarksStruct {
var mark: Int
init(mark: Int) {
self.mark = mark
}
}

class studentMarks {
var mark = 300
}
let marks = studentMarks()
print("成绩为 \(marks.mark)")

以上程序执行输出结果为:

成绩为 300

作为引用类型访问类属性

类的属性可以通过 . 来访问。格式为:实例化类名.属性名

import Cocoa

class MarksStruct {
var mark: Int
init(mark: Int) {
self.mark = mark
}
}

class studentMarks {
var mark1 = 300
var mark2 = 400
var mark3 = 900
}
let marks = studentMarks()
print("Mark1 is \(marks.mark1)")
print("Mark2 is \(marks.mark2)")
print("Mark3 is \(marks.mark3)")

以上程序执行输出结果为:

Mark1 is 300
Mark2 is 400
Mark3 is 900

恒等运算符

因为类是引用类型,有可能有多个常量和变量在后台同时引用某一个类实例。

为了能够判定两个常量或者变量是否引用同一个类实例,Swift 内建了两个恒等运算符:

恒等运算符不恒等运算符
运算符为:===运算符为:!==
如果两个常量或者变量引用同一个类实例则返回 true如果两个常量或者变量引用不同一个类实例则返回 true

实例

import Cocoa

class SampleClass: Equatable {
let myProperty: String
init(s: String) {
myProperty = s
}
}
func ==(lhs: SampleClass, rhs: SampleClass) -> Bool {
return lhs.myProperty == rhs.myProperty
}

let spClass1 = SampleClass(s: "Hello")
let spClass2 = SampleClass(s: "Hello")

if spClass1 === spClass2 {// false
print("引用相同的类实例 \(spClass1)")
}

if spClass1 !== spClass2 {// true
print("引用不相同的类实例 \(spClass2)")
}

以上程序执行输出结果为:

引用不相同的类实例 SampleClass
收起阅读 »

Swift 结构体

Swift 结构体是构建代码所用的一种通用且灵活的构造体。我们可以为结构体定义属性(常量、变量)和添加方法,从而扩展结构体的功能。与 C 和 Objective C 不同的是:结构体不需要包含实现文件和接口。结构体允许我们创建一个单一文件,且系统会自动生成面向...
继续阅读 »

Swift 结构体是构建代码所用的一种通用且灵活的构造体。

我们可以为结构体定义属性(常量、变量)和添加方法,从而扩展结构体的功能。

与 C 和 Objective C 不同的是:

  • 结构体不需要包含实现文件和接口。

  • 结构体允许我们创建一个单一文件,且系统会自动生成面向其它代码的外部接口。

结构体总是通过被复制的方式在代码中传递,因此它的值是不可修改的。

语法

我们通过关键字 struct 来定义结构体:

struct nameStruct { 
Definition 1
Definition 2
……
Definition N
}

实例

我们定义一个名为 MarkStruct 的结构体 ,结构体的属性为学生三个科目的分数,数据类型为 Int:

struct MarkStruct{
var mark1: Int
var mark2: Int
var mark3: Int
}

我们可以通过结构体名来访问结构体成员。

结构体实例化使用 let 关键字:

import Cocoa

struct studentMarks {
var mark1 = 100
var mark2 = 78
var mark3 = 98
}
let marks = studentMarks()
print("Mark1 是 \(marks.mark1)")
print("Mark2 是 \(marks.mark2)")
print("Mark3 是 \(marks.mark3)")

以上程序执行输出结果为:

Mark1  100
Mark2 78
Mark3 98

实例中,我们通过结构体名 'studentMarks' 访问学生的成绩。结构体成员初始化为mark1, mark2, mark3,数据类型为整型。

然后我们通过使用 let 关键字将结构体 studentMarks() 实例化并传递给 marks。

最后我们就通过 . 号来访问结构体成员的值。

以下实例化通过结构体实例化时传值并克隆一个结构体:

import Cocoa

struct MarksStruct {
var mark: Int

init
(mark: Int) {
self.mark = mark
}
}
var aStruct = MarksStruct(mark: 98)
var bStruct = aStruct // aStruct 和 bStruct 是使用相同值的结构体!
bStruct
.mark = 97
print(aStruct.mark) // 98
print(bStruct.mark) // 97

以上程序执行输出结果为:

98
97

结构体应用

在你的代码中,你可以使用结构体来定义你的自定义数据类型。

结构体实例总是通过值传递来定义你的自定义数据类型。

按照通用的准则,当符合一条或多条以下条件时,请考虑构建结构体:

  • 结构体的主要目的是用来封装少量相关简单数据值。
  • 有理由预计一个结构体实例在赋值或传递时,封装的数据将会被拷贝而不是被引用。
  • 任何在结构体中储存的值类型属性,也将会被拷贝,而不是被引用。
  • 结构体不需要去继承另一个已存在类型的属性或者行为。

举例来说,以下情境中适合使用结构体:

  • 几何形状的大小,封装一个width属性和height属性,两者均为Double类型。
  • 一定范围内的路径,封装一个start属性和length属性,两者均为Int类型。
  • 三维坐标系内一点,封装xyz属性,三者均为Double类型。

结构体实例是通过值传递而不是通过引用传递。

import Cocoa

struct markStruct{
var mark1: Int
var mark2: Int
var mark3: Int

init
(mark1: Int, mark2: Int, mark3: Int){
self.mark1 = mark1
self.mark2 = mark2
self.mark3 = mark3
}
}

print("优异成绩:")
var marks = markStruct(mark1: 98, mark2: 96, mark3:100)
print(marks.mark1)
print(marks.mark2)
print(marks.mark3)

print("糟糕成绩:")
var fail = markStruct(mark1: 34, mark2: 42, mark3: 13)
print(fail.mark1)
print(fail.mark2)
print(fail.mark3)

以上程序执行输出结果为:

优异成绩:
98
96
100
糟糕成绩:
34
42
13

以上实例中我们定义了结构体 markStruct,三个成员属性:mark1, mark2 和 mark3。结构体内使用成员属性使用 self 关键字。

从实例中我们可以很好的理解到结构体实例是通过值传递的。

收起阅读 »

动画曲线天天用,你能自己整一个吗?看完这篇你就会了!

前言最近在写动画相关的篇章,经常会用到 Curve 这个动画曲线类,那这个类到底怎么实现的?如果想自己来一个自定义的动画曲线该怎么弄?本篇我们就来一探究竟。Curve 类定义查看源码, Curve 类定义如下:abstr...
继续阅读 »

前言

最近在写动画相关的篇章,经常会用到 Curve 这个动画曲线类,那这个类到底怎么实现的?如果想自己来一个自定义的动画曲线该怎么弄?本篇我们就来一探究竟。

曲线

Curve 类定义

查看源码, Curve 类定义如下:

abstract class Curve extends ParametricCurve<double> {
const Curve();

@override
double transform(double t) {
if (t == 0.0 || t == 1.0) {
return t;
}
return super.transform(t);
}

Curve get flipped => FlippedCurve(this);
}

看上去好像没定义什么, 实际这里只是做了两个处理,一个是明确的数据类型为 double,另一个是对 transform 做了重载,也只是对参数 t 做了特殊处理,保证参数 t 的范围在0-1之间,且起点值0.0和终点值1.0不被转换函数转换。主要定义在上一层的ParametricCurve。文档是建议子类重载transformInternal方法,那我们就继续往上看ParametricCurve这个类的实现,代码如下:

abstract class ParametricCurve<T> {
const ParametricCurve();

T transform(double t) {
assert(t != null);
assert(t >= 0.0 && t <= 1.0, 'parametric value $t is outside of [0, 1] range.');
return transformInternal(t);
}

@protected
T transformInternal(double t) {
throw UnimplementedError();
}

@override
String toString() => objectRuntimeType(this, 'ParametricCurve');
}

可以看到,实际上 transform 方法除了做参数合法性验证以外,其实就是调用了transformInternal方法,因此子类必须要实现该方法,否则会抛出UnimplementedError异常。

实例解析

上面的源码可以看到,关键在于参数 t。这个参数 t 代表什么呢?注释里说的是:

Returns the value of the curve at point t. — 返回 t 点的曲线对应的值。

因此 t 可以认为是曲线的横坐标,而为了保证曲线的一致性,做了归一化处理,也就是t的取值都是在0-1之间。这么说可能有点抽象,我们来看2个例子来对比就明白了,先看最简单 Curves.linear 的实现。

class _Linear extends Curve {
const _Linear._();

@override
double transformInternal(double t) => t;
}

超级简单吧,直接返回 t,其实对应我们的数学的函数就是:

y = f(t) = t

对应的曲线就是一条斜线。也就是说在设定的动画时间内,会完成从0-1的线性转变,也就是变化是均匀的。 线性这个很好理解,我们再来看一个减速曲线decelerate的实现。

class _DecelerateCurve extends Curve {
const _DecelerateCurve._();

@override
double transformInternal(double t) {
t = 1.0 - t;
return 1.0 - t * t;
}
}

我们先看一下_DecelerateCurve 的计算表达式是什么。减速公式1

回忆一下我们高中物理学的匀减速运动,加速度为负(即减速)的距离计算公式:减速公式2

上面的减速曲线其实就可以看做是初始速度是2,加速度也是2的减速运动。为什么要是2这个值呢,这是因为 t 的取值范围是0-1,这样计算完的结果的取值范围还是0-1。你肯定会问,为什么要保证曲线的计算结果要是0-1? 我们来假设计算结果不为0-1会发生什么情况,比如我们要在屏幕上移动一个组件为60像素。假设动画曲线初始值不为0。那就意味着一开始的移动距离是跳变的。同样的,如果结束值不为1.0,意味着在最后一个点的距离值不是60.0,那么就意味着结束时需要从最后一个点跳到最终的60像素的位置(动画需要保证最终的移动距离是60像素)这样意味着动画会出现跳变的效果,绘制曲线的话会是下的样子(绿色是正常的,红线是异常的)。这样的动画体验是很糟糕的!因此,这是一个关键点,如果你的自定义曲线的 transformInternal 方法的返回值范围不是0-1,就意味着动画会出现跳变,导致动画缺帧的感觉。

image.png

有了这个基础,我们就可以解释动画曲线的基本机制了,实际上就是在给定的动画时间(Duration)范围内,完成组件的初始状态到结束状态的转变,这个转变是沿着设定的 Curve 类完成的,而其横坐标是0-1.0,曲线的初始值和结束值分别是0和1.0,而至于中间值是可以低于0或超过1的。我们可以想像是我们沿着设定的曲线运动,最终无论如何都会达到设定的目的地,而至于怎么走,拐多少道弯,速度怎么变化都是曲线控制的。但是,如果你的曲线初始值不为0或结束值不为1,就像是跳悬崖的那种感觉!

正弦动画曲线

我们来一个正弦曲线的动画验证一下上面的说法。

class SineCurve extends Curve {
final int count;
const SineCurve({this.count = 1}) : assert(count > 0);

@override
double transformInternal(double t) {
return sin(2 * count* pi * t);
}
}

count 参数用于控制周期,即达到目的地之前可以多来几个来回。这里我们发现,初始值是0,但是一个周期(2π)结束值也是0,这样在动画结束前会出现跳变的结果。来看一下示例代码,这个示例是让圆形向下移动60像素。

AnimatedContainer(
decoration: BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.circular(30.0),
),
transform: Matrix4.identity()..translate(0.0, up ? 60.0 : 0.0, 0.0),
duration: Duration(milliseconds: 3000),
curve: SineCurve(count: 1),
child: ClipOval(
child: Container(
width: 60.0,
height: 60.0,
color: Colors.blue,
),
),
)

运行效果如下,注意看最后一帧从0的位置直接跳到了60的位置。

跳动动画

这个怎么调呢,我们来看一下正弦曲线的样子。

正弦曲线

如果我们要满足0-1范围的要求,那么要往后再移动90度才能够达到。但是,这样还有个问题,这样破坏了周期性,比如设置 count=2的时候结果又不对了。我们来看一下规律,实际上只有第一个周期需要多移动90度(途中箭头指向的点),后面的都是按360度(即2π)为周期了。也就是角度其实是按2.5π,4.5π,6.5π……规律来的,对应的角度公式其实就是:调整后公式

所以调整后的正弦曲线代码为:

class SineCurve extends Curve {
final int count;
const SineCurve({this.count = 1}) : assert(count > 0);

@override
double transformInternal(double t) {
// 需要补偿pi/2个角度,使得起始值是0.终止值是1,避免出现最后突然回到0
return sin(2 * (count + 0.25) * pi * t);
}
}

再看调整后的效果,是不是丝滑般地过渡了?调整后动画

总结

本篇介绍了 Flutter 动画曲线类的原理和控制动画的机制,实际上 Curve 类就是在指定的时间内,沿曲线完成从起点到终点的过渡。但是为了保证动画平滑过渡,应该保证自定义曲线的transformInternal方法返回值的起始值和结束值分别是0和1。

收起阅读 »

Android协程(Coroutines)系列-深入理解suspend(挂起函数)关键字

Kotlin 协程把suspend 修饰符引入到了我们 Android 开发者的日常开发中。您是否好奇它的底层工作原理呢?编译器是如何转换我们的代码,使其能够挂起和恢复协程操作的呢?suspend挂起函数,是指把协程代码挂起不继续执行的函数,也叫协程被函数挂起...
继续阅读 »

Kotlin 协程把suspend 修饰符引入到了我们 Android 开发者的日常开发中。您是否好奇它的底层工作原理呢?编译器是如何转换我们的代码,使其能够挂起和恢复协程操作的呢?

suspend

挂起函数,是指把协程代码挂起不继续执行的函数,也叫协程被函数挂起了。协程中调用挂起函数时,协程所在的线程不会挂起也不会阻塞,但是协程被挂起了。也就是说,协程内挂起函数之后的代码停止执行了,直到挂起函数完成后恢复协程,协程才继续执行后续的代码。所有挂起函数都会通过suspend修饰符修饰。

suspend是协程的关键字,每一个被suspend修饰的方法都必须在另一个suspend函数或者Coroutine协程程序中进行调用。

挂起函数(由suspend关键字修饰)的目的是用来挂起协程的执行等待异步计算的结果,所以一个挂起函数通常有两个要点:挂起异步

这里涉及到一种机制俗称CPS(Continuation-Passing-Style:续体传递风格)。每一个suspend修饰的方法或者lambda表达式都会在代码调用的时候为其额外添加Continuation(续体)类型的参数。

Kotlin协程中使用了状态机,编译器会将协程体编译成一个匿名内部类,每一个挂起函数的调用位置对应一个挂起点。

挂起函数意义解释
join挂起当前协程,直到等待的子协程执行完毕通过当前协程返回的Job接口的join方法,可以单纯的挂起当前协程,等待子协程完成后再恢复继续执行
await挂起当前协程,直到等待的子协程返回结果和join的区别是,它属于Job接口的子接口Deferred的方法,可以等待子协程完成后,带着返回值恢复当前协程
delay挂起当前协程,直到指定时间后恢复当前协程单纯挂起当前协程,指定时长后恢复协程执行
withContext()挂起外部协程,直到自己内部协程全部返回后,才会恢复外部的协程。没有创建新的协程,在指定协程上运行挂起代码块,并挂起该协程直至代码块运行完成并返回结果。类似async.await的效果

协程挂起流程详解

协程实现异步的核心原理就是通过挂起函数实现协程体的挂起,还不阻塞协程体所在的线程。

fun testInMain() {
Log.d("["+Thread.currentThread().name+"]testInMain start")
var job = CoroutineScope(Dispatchers.Main).launch { //启动协程job
Log.d("[" + Thread.currentThread().name+"]job start")
var job1 = async(Dispatchers.IO) { //启动协程job1
Log.d("["+Thread.currentThread().name+"]job1 start")
delay(3000) //挂起job1协程 3秒
Log.d("["+Thread.currentThread().name+"]job1 end ")
"job1-Return"
} //job1协程 续体执行完毕

var job2 = async(Dispatchers.Default) {
Log.d("["+Thread.currentThread().name+"]job2 start" )
delay(1000) //挂起job2协程 1秒
Log.d("["+Thread.currentThread().name+"]job2 end")
"job2-Return"
} //job2协程 续体执行完毕

Log.d("["+Thread.currentThread().name+"]before job1 return")
Log.d("["+Thread.currentThread().name+"]job1 result = " + job1.await()) //挂起job协程,等待job1返回结果;如果已有结果,不挂起,直接返回

Log.d("["+Thread.currentThread().name+"]before job2 return")
Log.d("["+Thread.currentThread().name+"]job2 result = " + job2.await()) //挂起job协程,等待job2返回结果;如果已有结果,不挂起,直接返回

Log.d("["+Thread.currentThread().name+"]job end ")
} //job协程 续体执行完毕

Log.d("["+Thread.currentThread().name+"]testInMain end")
} //testInMain

示例代码的log输出如下,我们需要重点关注Log输出的次序,和时间间隔:

10:15:04.046 26079-26079/com.example.myapplication D/TC: [main]testInMain start
10:15:04.067 26079-26079/com.example.myapplication D/TC: [main]testInMain end
10:15:04.080 26079-26079/com.example.myapplication D/TC:
[main]job start
10:15:04.083 26079-26079/com.example.myapplication D/TC:
[main]before job1 return
10:15:04.086 26079-26683/com.example.myapplication D/TC: [DefaultDispatcher-worker-1]job1 start
10:15:04.087 26079-26684/com.example.myapplication D/TC: [DefaultDispatcher-worker-2]job2 start
10:15:05.090 26079-26683/com.example.myapplication D/TC: [DefaultDispatcher-worker-1]job2 end
10:15:05.095 26079-26079/com.example.myapplication D/TC:
[main]button-2 onclick now
10:15:07.090 26079-26685/com.example.myapplication D/TC: [DefaultDispatcher-worker-3]job1 end
10:15:07.091 26079-26079/com.example.myapplication D/TC:
[main]job1 result = job1-Return
10:15:07.091 26079-26079/com.example.myapplication D/TC:
[main]before job2 return
10:15:07.091 26079-26079/com.example.myapplication D/TC:
[main]job2 result = job2-Return
10:15:07.091 26079-26079/com.example.myapplication D/TC:
[main]job end
  • 步骤一:在主线程调用TestInMain,直接打印“[main]testInMain start”的log
  • 步骤二:TestInMain方法继续执行完毕,打印“[main]testInMain end”的log
  • 步骤三:job协程被主线程调度执行,打印“[main]job start”的log
  • 步骤四:job协程继续执行,打印“[main]before job1 return”的log
  • 步骤五:job协程被job1.await挂起函数中断执行,退出main线程,等待job1返回结果后再恢复执行
  • 步骤六:job1协程被异步调度到work-1子线程执行,打印“[DefaultDispatcher-worker-1]job1 start”的log,接着被delay挂起函数中断执行,退出work-1子线程,等待delay 3秒结束后再恢复执行
  • 步骤七:job2协程被异步调度到work-2子线程执行,打印“[DefaultDispatcher-worker-2]job2 start”的log,接着被delay挂起函数中断执行,退出work-2子线程,等待delay 1秒结束后再恢复执行
  • 步骤八:1秒钟后(从04秒-05秒),job2协程被delay挂起函数异步调度到[DefaultDispatcher-worker-1]子线程恢复执行,打印“[DefaultDispatcher-worker-1]job2 end”的log,job2续体结束执行,同时将结果存储到job2协程的result字段中。
  • 步骤九:main线程中button-2点击事件被处理,打印“[main]button-2 onclick now”的log
  • 步骤十:3秒钟后(从04秒-07秒),job1协程被delay挂起函数异步调度到[DefaultDispatcher-worker-3]子线程恢复执行,打印“[DefaultDispatcher-worker-3]job1 end”的log,job1续体结束执行,同时将结果存储到job1协程的result字段中。
  • 步骤十一:job1.await挂起函数得到结果,job协程被await挂起函数异步调度到main线程恢复执行,打印“[main]job1 result = job1-Return”的log
  • 步骤十二:job协程继续执行,打印“[main]before job2 return”的log
  • 步骤十三:job协程继续调用job2.await挂起函数,此时job2协程已经有result结果,所有它不会中断job协程的执行,而是直接返回结果,打印“[main]job2 result = job2-Return”的log
  • 步骤十四:job协程继续执行,打印“[main]job end”的log,job续体结束执行。

微信图片_20211025132142.jpg 从图中,我们可以清晰的得到几点结论:

  1. job协程内部,通过await 阻塞了后续代码的执行。job1和job2协程,通过delay阻塞了后续代码的执行。
  2. 协程job1,job2 启动后,保持并行执行。job2 并没有等待job1执行完才启动执行和恢复,而是在各自线程并行执行。
  3. job的后续代码被await 阻塞后,并没有阻塞main线程,main线程中其它模块的代码能同时被执行,并打印出"[main]button 2 onclick now"。
  4. job1 被delay阻塞后续代码执行时,并没有阻塞所在线程[DefaultDispatcher-worker-1],job2中的后续代码被恢复到此[DefaultDispatcher-worker-1]子线程中执行。
  5. job1 和 job2 协程在恢复执行时,并不能确保在原线程中执行后续代码。如log所示,job2在DefaultDispatcher-worker-2中启动和阻塞后,却在DefaultDispatcher-worker-1中恢复了后续的代码执行。

所以可以看出,协程的挂起,并不会阻塞协程所在的线程,而只是中断了协程后面的代码执行。然后等待挂起函数完成后,恢复协程的后续代码执行。这就是协程挂起最最基本的关键点。

协程挂起的实现原理

上节中的示例代码,经过反编译后的核心代码如下:

//TestCoroutin.decompiled.java
public final void testInMain() {
Log.d("cjf---", var10001.append("testInMain start").toString());

Job job = BuildersKt.launch$default( CoroutineScopeKt.CoroutineScope((CoroutineContext)Dispatchers.getMain()), (CoroutineContext)null, (CoroutineStart)null, (Function2)(new Function2((Continuation)null) {
@Nullable
public final Object invokeSuspend(@NotNull Object $result) {
//单独拆分到下面,需要详细讲解
}

public final Continuation create(@Nullable Object value, @NotNull Continuation completion) {/......./}

public final Object invoke(Object var1, Object var2){/......./}

}), 3, (Object)null);

Log.d("cjf---", var10001.append("testInMain end ").toString());
}

//job协程的SuspendLambda续体,其invokeSuspend方法代码
public final Object invokeSuspend(@NotNull Object $result) {
... ...
label17: {
Object var8 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
switch(this.label) {
case 0:
Log.d(var10001.append("job start ").toString());
Deferred job1 = BuildersKt.async$default(/......./);
job2 = BuildersKt.async$defaultdefault(/......./);
Log.d(var10001.append("before job1 return").toString());
var6 = var10001.append("job1 result =");
this.L$0 = job2;
this.L$1 = var5;
this.L$2 = var6;
this.label = 1;
var10000 = job1.await(this);
if (var10000 == var8) {
return var8;
}
break;
case 1:
var6 = (StringBuilder)this.L$2;
var5 = (String)this.L$1;
job2 = (Deferred)this.L$0;
ResultKt.throwOnFailure($result);
var10000 = $result;
break;
case 2:
var6 = (StringBuilder)this.L$1;
var5 = (String)this.L$0;
ResultKt.throwOnFailure($result);
var10000 = $result;
break label17;
default:
throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
}

var7 = var10000;
Log.d(var5, var6.append((String)var7).toString());
Log.d(var10001.append("before wait job2 return").toString());
var6 = var10001.append("job2 result = ");
this.L$0 = var5;
this.L$1 = var6;
this.L$2 = null;
this.label = 2;
var10000 = job2.await(this);
if (var10000 == var8) {
return var8;
}
} //end of label17

Log.d(var5, var6.append((String)var7).toString());
Log.d("cjf---", var10001.append("job end ").toString());
return Unit.INSTANCE;
} //end of invokeSuspend

反编译后的主要区别在job协程,其Lambda代码块转换成了Function2 实现。

我们借助APK反编译工具,可以看到执行代码中,Function2 实际上被SuspendLambda 类继承实现。

微信图片_20211025132929.jpg

SuspendLambda实现类的关键逻辑在invokeSuspend方法中,而invokeSuspend方法中采用了CPS(Continuation-Passing-Style) 续体传递风格

续体传递风格会将job协程的Lambda代码块,通过label标签和switch分割成多个代码块。代码块分割的点,就是协程中调用suspend挂起函数的地方。

分支代码调用到await挂起函数时,如果返回了COROUTINE_SUSPENDED,就退出invokeSuspend,进入挂起状态。

我们用流程图来描述上面示例代码,转换后的续体传递风格代码,如下:

微信图片_20211025132944.jpg

我们可以看到,整个示例代码,被分割成了5个代码块。其中case1 代码块主要负责为label17 代码块进行参数转换;case2 代码块主要负责为最外层代码块进行参数转换;所以相当于2个await挂起函数,将lambda代码块分割成了3个实际执行的代码块。

而且job1.await和job2.await会根据挂起函数的返回值进行不同处理,如果返回挂起,则进行协程挂起,当前协程退出执行;如果返回其它值,则协程继续后续代码块的执行。

编译器在编译期间,会对所有suspend修饰的函数调用处进行续体传递风格变换, Continuation可以称之为协程续体,它提供了协程恢复的基本方法:resumeWith。Continuation续体声明很简单:

public interface Continuation<in T> {
/**
* The context of the coroutine that corresponds to this continuation.
*/

public val context: CoroutineContext

/**
* Resumes the execution of the corresponding coroutine passing a successful or failed [result] as the
* return value of the last suspension point.
*/

public fun resumeWith(result: Result<T>)
}

其具体实现在SuspendLambda的父类BaseContinuationImpl中:

//class BaseContinuationImpl 中 fun resumeWith 内部核心代码
while (true) {
probeCoroutineResumed(current)
with(current) {
val completion = completion!!
val outcome: Result<Any?> = //协程返回了结果,说明协程执行完毕
try {
val outcome = invokeSuspend(param)//执行协程的续体代码块
if (outcome === COROUTINE_SUSPENDED) return //挂起函数返回挂起标志,退出后续代码执行
Result.success(outcome) //没有返回挂起标志,将返回值outcome封装为Result返给外层outcome
} catch (exception: Throwable) {
Result.failure(exception)//将异常Result返给外层outcome
}
releaseIntercepted() // 释放当前协程的拦截器
if (completion is BaseContinuationImpl) {//如果上一层续体是一个单纯的续体,则将结果作为上一层续体的恢复参数,进行上一层续体的恢复
current = completion
param = outcome
} else {//上一层续体是一个协程,则调用协程的恢复函数,进行上一层的协程恢复
completion.resumeWith(outcome)
return
}
}
}

如果invokeSuspend函数返回中断标志时,会直接从函数中返回,等待后续继续被恢复执行。

如果invokeSuspend函数返回的是结果,且上一层续体不是单纯的续体而是协程体,它会调用参数completion的resumeWith函数,恢复上一层协程的invokeSuspend代码的执行。

协程被resumeWith恢复后,会继续调用invokeSuspend函数,根据label值执行下一个case分支代码块。按照这个恢复流程,直到所有invokeSuspend代码执行完,返回非COROUTINE_SUSPENDED的结果,协程就执行结束。

我们继续看job续体在invokeSuspend中调用到job1.await函数时,await是怎么实现返回挂起标志,和后续恢复job协程的。核心代码可以在awaitSuspend中查看:

// JobSupport.kt中 awaitSuspend方法
private suspend fun awaitSuspend(): Any? = suspendCoroutineUninterceptedOrReturn { uCont ->
val cont = AwaitContinuation(uCont.intercepted(), this)
cont.disposeOnCancellation(invokeOnCompletion(
ResumeAwaitOnCompletion(this, cont).asHandler))
cont.getResult()
}

// JobSupport.kt中 invokeOnCompletion方法
public final override fun invokeOnCompletion(...):DisposableHandle {
var nodeCache: JobNode<*>? = null
loopOnState { state ->
when (state) {
is Empty -> { // 没有completion handlers,直接创建Node放入state
val node = nodeCache ?: makeNode(handler, onCancelling).also { nodeCache = it }
if (_state.compareAndSet(state, node)) return nod
}
is Incomplete -> {// 有completion handlers,加入到node list列表
val list = state.list
val node = nodeCache ?: makeNode(handler, onCancelling).also { nodeCache = it }
if (!addLastAtomic(state, list, node)) return@loopOnState /
}
else -> { // 已经完成,不需要加入结果监听Node
if (invokeImmediately) handler.invokeIt((state as? CompletedExceptionally)?.cause) return NonDisposableHandle
}
}
}
}

// AbstractCoroutine.kt 中 resumeWith方法
// 通知state node,进行恢复
public final override fun resumeWith(result: Result<T>) {
// makeCompletingOnce 大致实现是修改协程状态,如果需要的话还会将结果返回给调用者协程,并恢复调用者协程
makeCompletingOnce(result.toState(), defaultResumeMode)
}

可以看出,job1.await()首先会通过getResult()去获取job1的结果,如果有结果则直接返回结果,否则立即返回中断标志,这样就实现了await挂起点挂起job协程了。await()挂起函数恢复job协程的流程是,将job 协程封装为 ResumeAwaitOnCompletion,并将其再次封装成handler 节点,添加job1协程的 state.list。

等job1协程完成后,会通知 handler 节点调用job协程的 resumeWith(result) 方法,从而恢复 job协程await 挂起点之后的代码块的执行。

我们再次结合示例代码, 来梳理这个挂起和恢复流程:

微信图片_20211025145009.jpg

note:绿色底色,表示在主线程执行;红色字体,表示调用挂起函数;

可以看到整个过程:

  • job协程没有阻塞调用者TestInMain,job协程会被post到主线程执行;
  • 子协程job1,job2会同时调度到不同子线程中执行,会并行执行;
  • job协程通过job1,和job2 的 await挂起函数等待异步结果。等待异步结果的时候,job协程也没有阻塞主线程。

通过续体传递风格的invokeSuspend代码,和续体之间形成的resumewith恢复链,协程得以实现挂起和恢复的核心流程。


收起阅读 »

实现一个 Coroutine 版 DialogFragment

Android 对话框有多种实现方法,目前比较推荐的是 DialogFragment,先比较与直接使用 AlertDialog,可以避免屏幕旋转等配置变化造成消失。但是其 API 建立在回调的基础上使用起来并不友好。接入 Coroutine...
继续阅读 »

Android 对话框有多种实现方法,目前比较推荐的是 DialogFragment,先比较与直接使用 AlertDialog,可以避免屏幕旋转等配置变化造成消失。但是其 API 建立在回调的基础上使用起来并不友好。接入 Coroutine 我们可以对其进行一番改造。

1. 使用 Coroutine 进行改造

自定义 AlertDialogFragment 继承自 DialogFragment 如下

class AlertDialogFragment : DialogFragment() {

override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {

val listener = DialogInterface.OnClickListener { _: DialogInterface, which: Int ->
_cont.resume(which)
}
return AlertDialog.Builder(context)
.setTitle("Title")
.setMessage("Message")
.setPositiveButton("Ok", listener)
.setNegativeButton("Cancel", listener)
.create()
}

private lateinit var _cont : Continuation<Int>
suspend fun showAsSuspendable(fm: FragmentManager, tag: String? = null) = suspendCoroutine<Int> { cont ->
show(fm, tag)
_cont = cont
}
}

实现很简单,我们是使用 suspendCoroutine 将原本基于 listener 的回调转化为挂起函数。接下来我们可以用同步的方式获取 dialog 的返回值了:

button.setOnClickListener {
GlobalScope.launch {
val result = AlertDialogFragment().showAsSuspendable(supportFragmentManager)
Log.d("AlertDialogFragment", "$result Clicked")
}
}

2. 屏幕旋转时的崩溃

经过测试,发现上述代码存在问题。我们知道 DialogFragment 在屏幕旋转时可以保持不消失,但是此时如果点击 Dialog 的按钮,会出现崩溃:

kotlin.UninitializedPropertyAccessException: lateinit property _cont has not been initialized

如果了解 Fragment 和 Activity 销毁重建的过程就能轻松推理出发生问题的原因:

  1. 旋转屏幕时,Activity 将会重新创建。
  2. Activity 临终前会在 onSaveInstanceState() 中保存 DialogFragment 的状态 FragmentManagerState;
  3. 重建后的 Activity,在 onCreate() 中根据 savedInstanceState 所给予的 FragmentManagerState 自动重建 DialogFragment 并且 show() 出来

总结起来流程如下:

旋转屏幕 --> Activity.onSaveInstanceState() --> Activity.onCreate() --> DialogFragment.show()

重建后的 FragmentDialog 其成员变量 _cont 尚未初始化,此时对其访问自然发生 crash。

那么如果不使用 lateinit 就没问题了呢? 我们尝试引入 RxJava 对其进行改造


3. 二次改造: RxJava + Coroutine

通过 RxJava 的 Subject 避免了 lateinit 的出现,防止 crash :

//build.gradle
implementation "io.reactivex.rxjava2:rxjava:2.2.8"

新的 AlertDialogFragment 代码如下:

class AlertDialogFragment : DialogFragment() {

private val subject = SingleSubject.create<Int>()

override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val listener = DialogInterface.OnClickListener { _: DialogInterface, which: Int ->
subject.onSuccess(which)
}

return AlertDialog.Builder(requireContext())
.setTitle("Title")
.setMessage("Message")
.setPositiveButton("Ok", listener)
.setNegativeButton("Cancel", listener)
.create()
}

suspend fun showAsSuspendable(fm: FragmentManager, tag: String? = null) = suspendCoroutine<Int> { cont ->
show(fm, tag)
subject.subscribe { it -> cont.resume(it) }
}
}

显示 dialog 时,通过订阅 SingleSubject 响应 listener 的回调。

经过修改,旋转屏幕后点击 Dialog 按钮时没有再发生 crash 的现象,但是仍然存在问题:屏幕旋转后我们无法接收到 Dialog 的返回值,即没有按预期的那样显示下面的日志

Log.d("AlertDialogFragment", "$result Clicked")

当 DialogFragment 重建后, Subject 也跟随重建,但是丢失了之前的 Subscriber ,所以点击按钮后,Rx 的下游无法响应。

有没有办法让 Subject 重建时能够恢复之前的 Subscriber 呢? 此时想到了借助 onSaveInstanceState 。

想要 subject 作为 Fragment 的 arguments 保存到 savedInstanceState,必须是一个 Serializable 或者 Parcelable


4. 三次改造: SerializableSingleSubject

令人高兴的是,查阅 SingleSubject 源码后发现其成员变量全是 Serializable 的子类,也就是只要 SingleSubject 实现 Serializable 接口就可以存入 savedInstanceState 了, 但可惜它不是,而且它是一个 final 类,只好拷贝源码出来,自己实现一个 SerializableSingleSubject :

/**
* 实现 Serializable 接口并增加 serialVersionUID
*/

public final class SerializableSingleSubject<T> extends Single<T> implements SingleObserver<T>, Serializable {
private static final long serialVersionUID = 1L;

final AtomicReference<SerializableSingleSubject.SingleDisposable<T>[]> observers;

@SuppressWarnings("rawtypes")
static final SerializableSingleSubject.SingleDisposable[] EMPTY = new SerializableSingleSubject.SingleDisposable[0];

@SuppressWarnings("rawtypes")
static final SerializableSingleSubject.SingleDisposable[] TERMINATED = new SerializableSingleSubject.SingleDisposable[0];

final AtomicBoolean once;
T value;
Throwable error;

// 以下代码同 SingleSubject,省略

基于 SerializableSingleSubject 重写 AlertDialogFragment 如下:

class AlertDialogFragment : DialogFragment() {

private var subject = SerializableSingleSubject.create<Int>()

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
savedInstanceState?.let {
subject = it["subject"] as SerializableSingleSubject<Int>
}

}

override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val listener = DialogInterface.OnClickListener { _: DialogInterface, which: Int ->
subject.onSuccess(which)
}

return AlertDialog.Builder(requireContext())
.setTitle("Title")
.setMessage("Message")
.setPositiveButton("Ok", listener)
.setNegativeButton("Cancel", listener)
.create()
}


override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putSerializable("subject", subject);

}

suspend fun showAsSuspendable(fm: FragmentManager, tag: String? = null) = suspendCoroutine<Int> { cont ->
show(fm, tag)
subject.subscribe { it -> cont.resume(it) }
}
}

重建后通过 savedInstanceState 恢复之前的 Subscriber ,下游顺利收到消息,日志正常输出。

需要注意的是,此时仍然存在隐患:屏幕旋转后,点击 dialog 虽然可以正常获得返回值,但是此时协程恢复的上下文是前一次 launch { ... } 的闭包

    GlobalScope.launch {
val frag = AlertDialogFragment()
val result = frag.showAsSuspendable(supportFragmentManager)
Log.d("AlertDialogFragment", "$result Clicked on $frag")
}

如上,此时打印的 frag 是重建之前的 DialogFragment,如果 launch{...} 里引用了外部 Activity(获取成员) ,那也是旧的 Activity,此处需要特别注意避免类似操作。


5. 纯 RxJava 方式

既然引入了 RxJava,最后捎带介绍一下不使用 Coroutine 只依靠 RxJava 的版本:

fun showAsSingle(fm: FragmentManager, tag: String? = null): Single<Int> {
show(fm, tag)
return subject.hide()
}

使用时,由 subscribe() 替代挂起函数的使用。

button.setOnClickListener {
AlertDialogFragment().showAsSingle(supportFragmentManager).subscribe { result ->
Log.d("AlertDialogFragment", "$result Clicked")
}
}


收起阅读 »

LeetCode刷题-合并区间

一、题目描述 难度:中等~ 以数组 intervals 表示若干个区间的集合,其中单个区间为 intervals[i] = [starti, endi] 。请你合并所有重叠的区间,并返回一个不重叠的区间数组,该数组需恰好覆盖输入中的所有区间。 示例1: 输入:...
继续阅读 »

一、题目描述


难度:中等~

以数组 intervals 表示若干个区间的集合,其中单个区间为 intervals[i] = [starti, endi] 。请你合并所有重叠的区间,并返回一个不重叠的区间数组,该数组需恰好覆盖输入中的所有区间。


示例1:


输入:intervals = [[1,3],[2,6],[8,10],[15,18]]
输出:[[1,6],[8,10],[15,18]]
解释:区间 [1,3] 和 [2,6] 重叠, 将它们合并为 [1,6].

示例2:


输入:intervals = [[1,4],[4,5]]
输出:[[1,5]]
解释:区间 [1,4] 和 [4,5] 可被视为重叠区间。

提示:
  1 <= intervals.length <= 10^4
  intervals[i].length == 2
  0 <= starti <= endi <= 10^4


作者:力扣 (LeetCode)
链接:leetcode-cn.com/leetbook/re…
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。


二、题目解析


思路:
直接代码里注释!


三、代码


1.Python实现



初见的第一思路:
1.按左端点从小到大排序



2.有交集,更新右端点;无交集,则保存当前区间


class Solution:
def merge(self, intervals: List[List[int]]) -> List[List[int]]:
//将二维数组intervals按照其内每个子数组第一个元素从小到大排序
intervals.sort()
result = list()
for i in intervals:
//如果result中没有子数组或者当前两个数组无交集
//直接保存当前区间
if not result or result[-1][1] < i[0]:
result.append(i)
//否则有交集,取两个数组中第一个元素最大的值作为当前数组的第一个元素(即合并操作)
else:
result[-1][1] = max(result[-1][1], i[1])
return result

复杂度分析




  • 时间复杂度:O(n log n),其中 n 为区间的数量。除去排序的开销,我们只需要一次线性扫描,所以主要的时间开销是排序的 O(n log n)。




  • 空间复杂度:O(log n),其中 n 为区间的数量。这里计算的是存储答案之外,使用的额外空间。O(log n) 即为排序所需要的空间复杂度。




2.C实现


留空,等变再牛B点再来手写快排加合并!


3.C++实现


class Solution {
public:
vector<vector<int>> merge(vector<vector<int>>& intervals) {
if (intervals.size() == 0) {
return {};
}
sort(intervals.begin(), intervals.end());
vector<vector<int>> merge;
for (int i = 0; i < intervals.size(); ++i) {
int L = intervals[i][0], R = intervals[i][1];
if (!merge.size() || merge.back()[1] < L) {
merge.push_back({L, R});
}
else {
merge.back()[1] = max(merge.back()[1], R);
}
}
return merge;
}
};

🔆In The End!


请添加图片描述








从现在做起,坚持下去,一天进步一小点,不久的将来,你会感谢曾经努力的你!

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

Swift 枚举

枚举简单的说也是一种数据类型,只不过是这种数据类型只包含自定义的特定数据,它是一组有共同特性的数据的集合。Swift 的枚举类似于 Objective C 和 C 的结构,枚举的功能为:它声明在类中,可以通过实例化类来访问它的值。枚举也可以定义构造函数(ini...
继续阅读 »

枚举简单的说也是一种数据类型,只不过是这种数据类型只包含自定义的特定数据,它是一组有共同特性的数据的集合。

Swift 的枚举类似于 Objective C 和 C 的结构,枚举的功能为:

  • 它声明在类中,可以通过实例化类来访问它的值。

  • 枚举也可以定义构造函数(initializers)来提供一个初始成员值;可以在原始的实现基础上扩展它们的功能。

  • 可以遵守协议(protocols)来提供标准的功能。

语法

Swift 中使用 enum 关键词来创建枚举并且把它们的整个定义放在一对大括号内:

enum enumname {
// 枚举定义放在这里
}

例如我们定义以下表示星期的枚举:

import Cocoa

// 定义枚举
enum DaysofaWeek {
case Sunday
case Monday
case TUESDAY
case WEDNESDAY
case THURSDAY
case FRIDAY
case Saturday
}

var weekDay = DaysofaWeek.THURSDAY
weekDay = .THURSDAY
switch weekDay
{
case .Sunday:
print("星期天")
case .Monday:
print("星期一")
case .TUESDAY:
print("星期二")
case .WEDNESDAY:
print("星期三")
case .THURSDAY:
print("星期四")
case .FRIDAY:
print("星期五")
case .Saturday:
print("星期六")
}

以上程序执行输出结果为:

星期四

枚举中定义的值(如 SundayMonday……Saturday)是这个枚举的成员值(或成员)。case关键词表示一行新的成员值将被定义。

注意: 和 C 和 Objective-C 不同,Swift 的枚举成员在被创建时不会被赋予一个默认的整型值。在上面的DaysofaWeek例子中,SundayMonday……Saturday不会隐式地赋值为01……6。相反,这些枚举成员本身就有完备的值,这些值是已经明确定义好的DaysofaWeek类型。

var weekDay = DaysofaWeek.THURSDAY 

weekDay的类型可以在它被DaysofaWeek的一个可能值初始化时推断出来。一旦weekDay被声明为一个DaysofaWeek,你可以使用一个缩写语法(.)将其设置为另一个DaysofaWeek的值:

var weekDay = .THURSDAY 

weekDay的类型已知时,再次为其赋值可以省略枚举名。使用显式类型的枚举值可以让代码具有更好的可读性。

枚举可分为相关值与原始值。

相关值与原始值的区别

相关值原始值
不同数据类型相同数据类型
实例: enum {10,0.8,"Hello"}实例: enum {10,35,50}
值的创建基于常量或变量预先填充的值
相关值是当你在创建一个基于枚举成员的新常量或变量时才会被设置,并且每次当你这么做得时候,它的值可以是不同的。原始值始终是相同的

相关值

以下实例中我们定义一个名为 Student 的枚举类型,它可以是 Name 的一个字符串(String),或者是 Mark 的一个相关值(Int,Int,Int)。

import Cocoa

enum Student{
case Name(String)
case Mark(Int,Int,Int)
}
var studDetails = Student.Name("Runoob")
var studMarks = Student.Mark(98,97,95)
switch studMarks {
case .Name(let studName):
print("学生的名字是: \(studName)。")
case .Mark(let Mark1, let Mark2, let Mark3):
print("学生的成绩是: \(Mark1),\(Mark2),\(Mark3)。")
}

以上程序执行输出结果为:

学生的成绩是: 98,97,95。

原始值

原始值可以是字符串,字符,或者任何整型值或浮点型值。每个原始值在它的枚举声明中必须是唯一的。

在原始值为整数的枚举时,不需要显式的为每一个成员赋值,Swift会自动为你赋值。

例如,当使用整数作为原始值时,隐式赋值的值依次递增1。如果第一个值没有被赋初值,将会被自动置为0。

import Cocoa

enum Month: Int {
case January = 1, February, March, April, May, June, July, August, September, October, November, December
}

let yearMonth = Month.May.rawValue
print("数字月份为: \(yearMonth)。")

以上程序执行输出结果为:

数字月份为: 5。
收起阅读 »

Swift 闭包

闭包(Closures)是自包含的功能代码块,可以在代码中使用或者用来作为参数传值。Swift 中的闭包与 C 和 Objective-C 中的代码块(blocks)以及其他一些编程语言中的 匿名函数比较相似。全局函数和嵌套函数其实就是特殊的闭包。闭包的形式有...
继续阅读 »

闭包(Closures)是自包含的功能代码块,可以在代码中使用或者用来作为参数传值。

Swift 中的闭包与 C 和 Objective-C 中的代码块(blocks)以及其他一些编程语言中的 匿名函数比较相似。

全局函数和嵌套函数其实就是特殊的闭包。

闭包的形式有:

全局函数嵌套函数闭包表达式
有名字但不能捕获任何值。有名字,也能捕获封闭函数内的值。无名闭包,使用轻量级语法,可以根据上下文环境捕获值。

Swift中的闭包有很多优化的地方:

  1. 根据上下文推断参数和返回值类型
  2. 从单行表达式闭包中隐式返回(也就是闭包体只有一行代码,可以省略return)
  3. 可以使用简化参数名,如$0, $1(从0开始,表示第i个参数...)
  4. 提供了尾随闭包语法(Trailing closure syntax)
  5. 语法

    以下定义了一个接收参数并返回指定类型的闭包语法:

    {(parameters) -> return type in
    statements
    }

    实例

    import Cocoa

    let studname = { print("Swift 闭包实例。") }
    studname
    ()

    以上程序执行输出结果为:

    Swift 闭包实例。

    以下闭包形式接收两个参数并返回布尔值:

    {(Int, Int) -> Bool in
    Statement1
    Statement 2
    ---
    Statement n
    }

    实例

    import Cocoa

    let divide = {(val1: Int, val2: Int) -> Int in
    return val1 / val2
    }
    let result = divide(200, 20)
    print (result)

    以上程序执行输出结果为:

    10

    闭包表达式

    闭包表达式是一种利用简洁语法构建内联闭包的方式。 闭包表达式提供了一些语法优化,使得撰写闭包变得简单明了。


    sorted 方法

    Swift 标准库提供了名为 sorted(by:) 的方法,会根据您提供的用于排序的闭包函数将已知类型数组中的值进行排序。

    排序完成后,sorted(by:) 方法会返回一个与原数组大小相同,包含同类型元素且元素已正确排序的新数组。原数组不会被 sorted(by:) 方法修改。

    sorted(by:)方法需要传入两个参数:

    • 已知类型的数组
    • 闭包函数,该闭包函数需要传入与数组元素类型相同的两个值,并返回一个布尔类型值来表明当排序结束后传入的第一个参数排在第二个参数前面还是后面。如果第一个参数值出现在第二个参数值前面,排序闭包函数需要返回 true,反之返回 false

    实例

    import Cocoa

    let names = ["AT", "AE", "D", "S", "BE"]

    // 使用普通函数(或内嵌函数)提供排序功能,闭包函数类型需为(String, String) -> Bool。
    func backwards
    (s1: String, s2: String) -> Bool {
    return s1 > s2
    }
    var reversed = names.sorted(by: backwards)

    print(reversed)

    以上程序执行输出结果为:

    ["S", "D", "BE", "AT", "AE"]

    如果第一个字符串 (s1) 大于第二个字符串 (s2),backwards函数返回true,表示在新的数组中s1应该出现在s2前。 对于字符串中的字符来说,"大于" 表示 "按照字母顺序较晚出现"。 这意味着字母"B"大于字母"A",字符串"S"大于字符串"D"。 其将进行字母逆序排序,"AT"将会排在"AE"之前。


    参数名称缩写

    Swift 自动为内联函数提供了参数名称缩写功能,您可以直接通过$0,$1,$2来顺序调用闭包的参数。

    实例

    import Cocoa

    let names = ["AT", "AE", "D", "S", "BE"]

    var reversed = names.sorted( by: { $0 > $1 } )
    print(reversed)

    $0和$1表示闭包中第一个和第二个String类型的参数。

    以上程序执行输出结果为:

    ["S", "D", "BE", "AT", "AE"]

    如果你在闭包表达式中使用参数名称缩写, 您可以在闭包参数列表中省略对其定义, 并且对应参数名称缩写的类型会通过函数类型进行推断。in 关键字同样也可以被省略.


    运算符函数

    实际上还有一种更简短的方式来撰写上面例子中的闭包表达式。

    Swift 的String类型定义了关于大于号 (>) 的字符串实现,其作为一个函数接受两个String类型的参数并返回Bool类型的值。 而这正好与sort(_:)方法的第二个参数需要的函数类型相符合。 因此,您可以简单地传递一个大于号,Swift可以自动推断出您想使用大于号的字符串函数实现:

    import Cocoa

    let names = ["AT", "AE", "D", "S", "BE"]

    var reversed = names.sorted(by: >)
    print(reversed)

    以上程序执行输出结果为:

    ["S", "D", "BE", "AT", "AE"]

    尾随闭包

    尾随闭包是一个书写在函数括号之后的闭包表达式,函数支持将其作为最后一个参数调用。

    func someFunctionThatTakesAClosure(closure: () -> Void) {
    // 函数体部分
    }

    // 以下是不使用尾随闭包进行函数调用
    someFunctionThatTakesAClosure
    ({
    // 闭包主体部分
    })

    // 以下是使用尾随闭包进行函数调用
    someFunctionThatTakesAClosure
    () {
    // 闭包主体部分
    }

    实例

    import Cocoa

    let names = ["AT", "AE", "D", "S", "BE"]

    //尾随闭包
    var reversed = names.sorted() { $0 > $1 }
    print(reversed)

    sort() 后的 { $0 > $1} 为尾随闭包。

    以上程序执行输出结果为:

    ["S", "D", "BE", "AT", "AE"]

    注意: 如果函数只需要闭包表达式一个参数,当您使用尾随闭包时,您甚至可以把()省略掉。

    reversed = names.sorted { $0 > $1 }

    捕获值

    闭包可以在其定义的上下文中捕获常量或变量。

    即使定义这些常量和变量的原域已经不存在,闭包仍然可以在闭包函数体内引用和修改这些值。

    Swift最简单的闭包形式是嵌套函数,也就是定义在其他函数的函数体内的函数。

    嵌套函数可以捕获其外部函数所有的参数以及定义的常量和变量。

    看这个例子:

    func makeIncrementor(forIncrement amount: Int) -> () -> Int {
    var runningTotal = 0
    func incrementor
    () -> Int {
    runningTotal
    += amount
    return runningTotal
    }
    return incrementor
    }

    一个函数makeIncrementor ,它有一个Int型的参数amout, 并且它有一个外部参数名字forIncremet,意味着你调用的时候,必须使用这个外部名字。返回值是一个()-> Int的函数。

    函数体内,声明了变量 runningTotal 和一个函数 incrementor。

    incrementor函数并没有获取任何参数,但是在函数体内访问了runningTotal和amount变量。这是因为其通过捕获在包含它的函数体内已经存在的runningTotal和amount变量而实现。

    由于没有修改amount变量,incrementor实际上捕获并存储了该变量的一个副本,而该副本随着incrementor一同被存储。

    所以我们调用这个函数时会累加:

    import Cocoa

    func makeIncrementor
    (forIncrement amount: Int) -> () -> Int {
    var runningTotal = 0
    func incrementor
    () -> Int {
    runningTotal
    += amount
    return runningTotal
    }
    return incrementor
    }

    let incrementByTen = makeIncrementor(forIncrement: 10)

    // 返回的值为10
    print(incrementByTen())

    // 返回的值为20
    print(incrementByTen())

    // 返回的值为30
    print(incrementByTen())

    以上程序执行输出结果为:

    10
    20
    30

    闭包是引用类型

    上面的例子中,incrementByTen是常量,但是这些常量指向的闭包仍然可以增加其捕获的变量值。

    这是因为函数和闭包都是引用类型。

    无论您将函数/闭包赋值给一个常量还是变量,您实际上都是将常量/变量的值设置为对应函数/闭包的引用。 上面的例子中,incrementByTen指向闭包的引用是一个常量,而并非闭包内容本身。

    这也意味着如果您将闭包赋值给了两个不同的常量/变量,两个值都会指向同一个闭包:

    import Cocoa

    func makeIncrementor
    (forIncrement amount: Int) -> () -> Int {
    var runningTotal = 0
    func incrementor
    () -> Int {
    runningTotal
    += amount
    return runningTotal
    }
    return incrementor
    }

    let incrementByTen = makeIncrementor(forIncrement: 10)

    // 返回的值为10
    incrementByTen
    ()

    // 返回的值为20
    incrementByTen
    ()

    // 返回的值为30
    incrementByTen
    ()

    // 返回的值为40
    incrementByTen
    ()

    let alsoIncrementByTen = incrementByTen

    // 返回的值也为50
    print(alsoIncrementByTen())
收起阅读 »

Swift 函数

Swift 函数用来完成特定任务的独立的代码块。Swift使用一个统一的语法来表示简单的C语言风格的函数到复杂的Objective-C语言风格的方法。函数声明: 告诉编译器函数的名字,返回类型及参数。函数定义: 提供了函数的实体。Swift 函数包含了参数类型...
继续阅读 »

Swift 函数用来完成特定任务的独立的代码块。

Swift使用一个统一的语法来表示简单的C语言风格的函数到复杂的Objective-C语言风格的方法。

  • 函数声明: 告诉编译器函数的名字,返回类型及参数。

  • 函数定义: 提供了函数的实体。

Swift 函数包含了参数类型及返回值类型:


函数定义

Swift 定义函数使用关键字 func

定义函数的时候,可以指定一个或多个输入参数和一个返回值类型。

每个函数都有一个函数名来描述它的功能。通过函数名以及对应类型的参数值来调用这个函数。函数的参数传递的顺序必须与参数列表相同。

函数的实参传递的顺序必须与形参列表相同,-> 后定义函数的返回值类型。

语法

func funcname(形参) -> returntype
{
Statement1
Statement2
……
Statement N
return parameters
}

实例

以下我们定义了一个函数名为 runoob 的函数,形参的数据类型为 String,返回值也为 String:

import Cocoa

func runoob
(site: String) -> String {
return (site)
}
print(runoob(site: "www.runoob.com"))

以上程序执行输出结果为:

www.runoob.com

函数调用

我们可以通过函数名以及对应类型的参数值来调用函数,函数的参数传递的顺序必须与参数列表相同。

以下我们定义了一个函数名为 runoob 的函数,形参 site 的数据类型为 String,之后我们调用函数传递的实参也必须 String 类型,实参传入函数体后,将直接返回,返回的数据类型为 String。

import Cocoa

func runoob
(site: String) -> String {
return (site)
}
print(runoob(site: "www.runoob.com"))

以上程序执行输出结果为:

www.runoob.com

函数参数

函数可以接受一个或者多个参数,这些参数被包含在函数的括号之中,以逗号分隔。

以下实例向函数 runoob 传递站点名 name 和站点地址 site:

import Cocoa

func runoob
(name: String, site: String) -> String {
return name + site
}
print(runoob(name: "菜鸟教程:", site: "www.runoob.com"))
print(runoob(name: "Google:", site: "www.google.com"))

以上程序执行输出结果为:

菜鸟教程:www.runoob.com
Googlewww.google.com

不带参数函数

我们可以创建不带参数的函数。

语法:

func funcname() -> datatype {
return datatype
}

实例

import Cocoa

func sitename
() -> String {
return "菜鸟教程"
}
print(sitename())

以上程序执行输出结果为:

菜鸟教程

元组作为函数返回值

函数返回值类型可以是字符串,整型,浮点型等。

元组与数组类似,不同的是,元组中的元素可以是任意类型,使用的是圆括号。

你可以用元组(tuple)类型让多个值作为一个复合值从函数中返回。

下面的这个例子中,定义了一个名为minMax(_:)的函数,作用是在一个Int数组中找出最小值与最大值。

import Cocoa

func minMax
(array: [Int]) -> (min: Int, max: Int) {
var currentMin = array[0]
var currentMax = array[0]
for value in array[1..<array.count] {
if value < currentMin {
currentMin
= value
} else if value > currentMax {
currentMax
= value
}
}
return (currentMin, currentMax)
}

let bounds = minMax(array: [8, -6, 2, 109, 3, 71])
print("最小值为 \(bounds.min) ,最大值为 \(bounds.max)")

minMax(_:)函数返回一个包含两个Int值的元组,这些值被标记为min和max,以便查询函数的返回值时可以通过名字访问它们。

以上程序执行输出结果为:

最小值为 -6 ,最大值为 109

如果你不确定返回的元组一定不为nil,那么你可以返回一个可选的元组类型。

你可以通过在元组类型的右括号后放置一个问号来定义一个可选元组,例如(Int, Int)?或(String, Int, Bool)?

注意
可选元组类型如(Int, Int)?与元组包含可选类型如(Int?, Int?)是不同的.可选的元组类型,整个元组是可选的,而不只是元组中的每个元素值。

前面的minMax(_:)函数返回了一个包含两个Int值的元组。但是函数不会对传入的数组执行任何安全检查,如果array参数是一个空数组,如上定义的minMax(_:)在试图访问array[0]时会触发一个运行时错误。

为了安全地处理这个"空数组"问题,将minMax(_:)函数改写为使用可选元组返回类型,并且当数组为空时返回nil

import Cocoa

func minMax
(array: [Int]) -> (min: Int, max: Int)? {
if array.isEmpty { return nil }
var currentMin = array[0]
var currentMax = array[0]
for value in array[1..<array.count] {
if value < currentMin {
currentMin
= value
} else if value > currentMax {
currentMax
= value
}
}
return (currentMin, currentMax)
}
if let bounds = minMax(array: [8, -6, 2, 109, 3, 71]) {
print("最小值为 \(bounds.min),最大值为 \(bounds.max)")
}

以上程序执行输出结果为:

最小值为 -6,最大值为 109

没有返回值函数

下面是 runoob(_:) 函数的另一个版本,这个函数接收菜鸟教程官网网址参数,没有指定返回值类型,并直接输出 String 值,而不是返回它:

import Cocoa

func runoob
(site: String) {
print("菜鸟教程官网:\(site)")
}
runoob
(site: "http://www.runoob.com")

以上程序执行输出结果为:

菜鸟教程官网:http://www.runoob.com

函数参数名称

函数参数都有一个外部参数名和一个局部参数名。

局部参数名

局部参数名在函数的实现内部使用。

func sample(number: Int) {
println
(number)
}

以上实例中 number 为局部参数名,只能在函数体内使用。

import Cocoa

func sample
(number: Int) {
print(number)
}
sample
(number: 1)
sample
(number: 2)
sample
(number: 3)

以上程序执行输出结果为:

1
2
3

外部参数名

你可以在局部参数名前指定外部参数名,中间以空格分隔,外部参数名用于在函数调用时传递给函数的参数。

如下你可以定义以下两个函数参数名并调用它:

import Cocoa

func pow
(firstArg a: Int, secondArg b: Int) -> Int {
var res = a
for _ in 1..<b {
res
= res * a
}
print(res)
return res
}
pow
(firstArg:5, secondArg:3)

以上程序执行输出结果为:

125

注意
如果你提供了外部参数名,那么函数在被调用时,必须使用外部参数名。


可变参数

可变参数可以接受零个或多个值。函数调用时,你可以用可变参数来指定函数参数,其数量是不确定的。

可变参数通过在变量类型名后面加入(...)的方式来定义。

import Cocoa

func vari
<N>(members: N...){
for i in members {
print(i)
}
}
vari
(members: 4,3,5)
vari
(members: 4.5, 3.1, 5.6)
vari
(members: "Google", "Baidu", "Runoob")

以上程序执行输出结果为:

4
3
5
4.5
3.1
5.6
Google
Baidu
Runoob

常量,变量及 I/O 参数

一般默认在函数中定义的参数都是常量参数,也就是这个参数你只可以查询使用,不能改变它的值。

如果想要声明一个变量参数,可以在参数定义前加 inout 关键字,这样就可以改变这个参数的值了。

例如:

func  getName(_ name: inout String).........

此时这个 name 值可以在函数中改变。

一般默认的参数传递都是传值调用的,而不是传引用。所以传入的参数在函数内改变,并不影响原来的那个参数。传入的只是这个参数的副本。

当传入的参数作为输入输出参数时,需要在参数名前加 & 符,表示这个值可以被函数修改。

实例

import Cocoa

func swapTwoInts
(_ a: inout Int, _ b: inout Int) {
let temporaryA = a
a
= b
b
= temporaryA
}


var x = 1
var y = 5
swapTwoInts
(&x, &y)
print("x 现在的值 \(x), y 现在的值 \(y)")

swapTwoInts(_:_:) 函数简单地交换 a 与 b 的值。该函数先将 a 的值存到一个临时常量 temporaryA 中,然后将 b 的值赋给 a,最后将 temporaryA 赋值给 b。

需要注意的是,someInt 和 anotherInt 在传入 swapTwoInts(_:_:) 函数前,都加了 & 的前缀。

以上程序执行输出结果为:

x 现在的值 5, y 现在的值 1

函数类型及使用

每个函数都有种特定的函数类型,由函数的参数类型和返回类型组成。

func inputs(no1: Int, no2: Int) -> Int {
return no1/no2
}

inputs 函数类型有两个 Int 型的参数(no1、no2)并返回一个 Int 型的值。

实例如下:

import Cocoa

func inputs
(no1: Int, no2: Int) -> Int {
return no1/no2
}
print(inputs(no1: 20, no2: 10))
print(inputs(no1: 36, no2: 6))

以上程序执行输出结果为:

2
6

以上函数定义了两个 Int 参数类型,返回值也为 Int 类型。

接下来我们看下如下函数,函数定义了参数为 String 类型,返回值为 String 类型。

func inputstr(name: String) -> String {
return name
}

函数也可以定义一个没有参数,也没有返回值的函数,如下所示:

import Cocoa

func inputstr
() {
print("菜鸟教程")
print("www.runoob.com")
}
inputstr
()

以上程序执行输出结果为:

菜鸟教程
www
.runoob.com

使用函数类型

在 Swift 中,使用函数类型就像使用其他类型一样。例如,你可以定义一个类型为函数的常量或变量,并将适当的函数赋值给它:

var addition: (Int, Int) -> Int = sum

解析:

"定义一个叫做 addition 的变量,参数与返回值类型均是 Int ,并让这个新变量指向 sum 函数"。

sum 和 addition 有同样的类型,所以以上操作是合法的。

现在,你可以用 addition 来调用被赋值的函数了:

import Cocoa

func sum
(a: Int, b: Int) -> Int {
return a + b
}
var addition: (Int, Int) -> Int = sum
print("输出结果: \(addition(40, 89))")

以上程序执行输出结果为:

输出结果: 129

函数类型作为参数类型、函数类型作为返回类型

我们可以将函数作为参数传递给另外一个参数:

import Cocoa

func sum
(a: Int, b: Int) -> Int {
return a + b
}
var addition: (Int, Int) -> Int = sum
print("输出结果: \(addition(40, 89))")

func another
(addition: (Int, Int) -> Int, a: Int, b: Int) {
print("输出结果: \(addition(a, b))")
}
another
(addition: sum, a: 10, b: 20)

以上程序执行输出结果为:

输出结果: 129
输出结果: 30

函数嵌套

函数嵌套指的是函数内定义一个新的函数,外部的函数可以调用函数内定义的函数。

实例如下:

import Cocoa

func calcDecrement
(forDecrement total: Int) -> () -> Int {
var overallDecrement = 0
func decrementer
() -> Int {
overallDecrement
-= total
return overallDecrement
}
return decrementer
}
let decrem = calcDecrement(forDecrement: 30)
print(decrem())

以上程序执行输出结果为:

-30
收起阅读 »

Swift 字典

Swift 字典用来存储无序的相同类型数据的集合,Swift 字典会强制检测元素的类型,如果类型不同则会报错。Swift 字典每个值(value)都关联唯一的键(key),键作为字典中的这个值数据的标识符。和数组中的数据项不同,字典中的数据项并没有具体顺序。我...
继续阅读 »

Swift 字典用来存储无序的相同类型数据的集合,Swift 字典会强制检测元素的类型,如果类型不同则会报错。

Swift 字典每个值(value)都关联唯一的键(key),键作为字典中的这个值数据的标识符。

和数组中的数据项不同,字典中的数据项并没有具体顺序。我们在需要通过标识符(键)访问数据的时候使用字典,这种方法很大程度上和我们在现实世界中使用字典查字义的方法一样。

Swift 字典的key没有类型限制可以是整型或字符串,但必须是唯一的。

如果创建一个字典,并赋值给一个变量,则创建的字典就是可以修改的。这意味着在创建字典后,可以通过添加、删除、修改的方式改变字典里的项目。如果将一个字典赋值给常量,字典就不可修改,并且字典的大小和内容都不可以修改。


创建字典

我们可以使用以下语法来创建一个特定类型的空字典:

var someDict =  [KeyType: ValueType]()

以下是创建一个空字典,键的类型为 Int,值的类型为 String 的简单语法:

var someDict = [Int: String]()

以下为创建一个字典的实例:

var someDict:[Int:String] = [1:"One", 2:"Two", 3:"Three"]

访问字典

我们可以根据字典的索引来访问数组的元素,语法如下:

var someVar = someDict[key]

我们可以通过以下实例来学习如何创建,初始化,访问字典:

import Cocoa

var someDict:[Int:String] = [1:"One", 2:"Two", 3:"Three"]

var someVar = someDict[1]

print( "key = 1 的值为 \(someVar)" )
print( "key = 2 的值为 \(someDict[2])" )
print( "key = 3 的值为 \(someDict[3])" )

以上程序执行输出结果为:

key = 1 的值为 Optional("One")
key
= 2 的值为 Optional("Two")
key
= 3 的值为 Optional("Three")

修改字典

我们可以使用 updateValue(forKey:) 增加或更新字典的内容。如果 key 不存在,则添加值,如果存在则修改 key 对应的值。updateValue(_:forKey:)方法返回Optional值。实例如下:

import Cocoa

var someDict:[Int:String] = [1:"One", 2:"Two", 3:"Three"]

var oldVal = someDict.updateValue("One 新的值", forKey: 1)

var someVar = someDict[1]

print( "key = 1 旧的值 \(oldVal)" )
print( "key = 1 的值为 \(someVar)" )
print( "key = 2 的值为 \(someDict[2])" )
print( "key = 3 的值为 \(someDict[3])" )

以上程序执行输出结果为:

key = 1 旧的值 Optional("One")
key
= 1 的值为 Optional("One 新的值")
key
= 2 的值为 Optional("Two")
key
= 3 的值为 Optional("Three")

你也可以通过指定的 key 来修改字典的值,如下所示:

import Cocoa

var someDict:[Int:String] = [1:"One", 2:"Two", 3:"Three"]

var oldVal = someDict[1]
someDict
[1] = "One 新的值"
var someVar = someDict[1]

print( "key = 1 旧的值 \(oldVal)" )
print( "key = 1 的值为 \(someVar)" )
print( "key = 2 的值为 \(someDict[2])" )
print( "key = 3 的值为 \(someDict[3])" )

以上程序执行输出结果为:

key = 1 旧的值 Optional("One")
key
= 1 的值为 Optional("One 新的值")
key
= 2 的值为 Optional("Two")
key
= 3 的值为 Optional("Three")

移除 Key-Value 对

我们可以使用 removeValueForKey() 方法来移除字典 key-value 对。如果 key 存在该方法返回移除的值,如果不存在返回 nil 。实例如下:

import Cocoa

var someDict:[Int:String] = [1:"One", 2:"Two", 3:"Three"]

var removedValue = someDict.removeValue(forKey: 2)

print( "key = 1 的值为 \(someDict[1])" )
print( "key = 2 的值为 \(someDict[2])" )
print( "key = 3 的值为 \(someDict[3])" )

以上程序执行输出结果为:

key = 1 的值为 Optional("One")
key
= 2 的值为 nil
key
= 3 的值为 Optional("Three")

你也可以通过指定键的值为 nil 来移除 key-value(键-值)对。实例如下:

import Cocoa

var someDict:[Int:String] = [1:"One", 2:"Two", 3:"Three"]

someDict
[2] = nil

print( "key = 1 的值为 \(someDict[1])" )
print( "key = 2 的值为 \(someDict[2])" )
print( "key = 3 的值为 \(someDict[3])" )

以上程序执行输出结果为:

key = 1 的值为 Optional("One")
key
= 2 的值为 nil
key
= 3 的值为 Optional("Three")

遍历字典

我们可以使用 for-in 循环来遍历某个字典中的键值对。实例如下:

import Cocoa

var someDict:[Int:String] = [1:"One", 2:"Two", 3:"Three"]

for (key, value) in someDict {
print("字典 key \(key) - 字典 value \(value)")
}

以上程序执行输出结果为:

字典 key 2 -  字典 value Two
字典 key 3 - 字典 value Three
字典 key 1 - 字典 value One

我们也可以使用enumerate()方法来进行字典遍历,返回的是字典的索引及 (key, value) 对,实例如下:

import Cocoa

var someDict:[Int:String] = [1:"One", 2:"Two", 3:"Three"]

for (key, value) in someDict.enumerated() {
print("字典 key \(key) - 字典 (key, value) 对 \(value)")
}

以上程序执行输出结果为:

字典 key 0 -  字典 (key, value)  (2, "Two")
字典 key 1 - 字典 (key, value) (3, "Three")
字典 key 2 - 字典 (key, value) (1, "One")

字典转换为数组

你可以提取字典的键值(key-value)对,并转换为独立的数组。实例如下:

import Cocoa

var someDict:[Int:String] = [1:"One", 2:"Two", 3:"Three"]

let dictKeys = [Int](someDict.keys)
let dictValues = [String](someDict.values)

print("输出字典的键(key)")

for (key) in dictKeys {
print("\(key)")
}

print("输出字典的值(value)")

for (value) in dictValues {
print("\(value)")
}

以上程序执行输出结果为:

输出字典的键(key)
2
3
1
输出字典的值(value)
Two
Three
One

count 属性

我们可以使用只读的 count 属性来计算字典有多少个键值对:

import Cocoa

var someDict1:[Int:String] = [1:"One", 2:"Two", 3:"Three"]
var someDict2:[Int:String] = [4:"Four", 5:"Five"]

print("someDict1 含有 \(someDict1.count) 个键值对")
print("someDict2 含有 \(someDict2.count) 个键值对")

以上程序执行输出结果为:

someDict1 含有 3 个键值对
someDict2
含有 2 个键值对

isEmpty 属性

Y我们可以通过只读属性 isEmpty 来判断字典是否为空,返回布尔值:

import Cocoa

var someDict1:[Int:String] = [1:"One", 2:"Two", 3:"Three"]
var someDict2:[Int:String] = [4:"Four", 5:"Five"]
var someDict3:[Int:String] = [Int:String]()

print("someDict1 = \(someDict1.isEmpty)")
print("someDict2 = \(someDict2.isEmpty)")
print("someDict3 = \(someDict3.isEmpty)")

以上程序执行输出结果为:

someDict1 = false
someDict2
= false
someDict3
= true
收起阅读 »

使用 Kotlin Flow 优化你的网络请求框架,减少模板代码

一、以前封装的遗憾点 主要集中在如下2点上: Loading的处理 多余的LiveData 总而言之,就是需要写很多模板代码。 不必编写模版代码的一个最大好处就是: 写的代码越少,出错的概率越小. 1.1 Loading的处理 对于封装二,虽然...
继续阅读 »

一、以前封装的遗憾点


主要集中在如下2点上:




  • Loading的处理




  • 多余的LiveData




总而言之,就是需要写很多模板代码。



不必编写模版代码的一个最大好处就是: 写的代码越少,出错的概率越小.



1.1 Loading的处理


对于封装二,虽然解耦比封装一更彻底,但是关于Loading这里我觉得还是有遗憾。


试想一下:如果Activity中业务很多、逻辑复杂,存在很多个网络请求,在需要网络请求的地方都要手动去showLoading() ,然后在 observer() 中手动调用 stopLoading()


假如Activity中代码业务复杂,存在多个api接口,这样Activity中就存在很多个与loading有关的方法。


此外,如果一个网络请求的showLoading()方法和dismissLoading()方法相隔很远。会导致一个顺序流程的割裂。


请求开始前showLoading() ---> 请求网络 ---> 结束后stopLoading(),这是一个完整的流程,代码也应该尽量在一起,一目了然,不应该割裂存在。


如果代码量一多,以后维护起来,万一不小心删除了某个showLoading()或者stopLoading(),也容易导致问题。


还有就是每次都要手动调用这两个方法,麻烦。


1.2 重复的LiveData声明


个人认为常用的网络请求分为两大类:




  • 用完即丢




  • 需要监听数据变化




举个常见的例子,看下面这个页面:


image.png


用户一进入这个页面,绿色框里面内容基本不会变化,(不去纠结微信这个页面是不是webview之类的),这种ui其实是不需要设置一个LiveData去监听的,因为它几乎不会再更新了。


典型的还有:点击登录按钮,成功后就进去了下一个页面。


但是红色的框里面的ui不一样,需要实时刷新数据,也就用到LiveData监听,这种情况下观察者订阅者模式的好处才真正展示出来。并且从其他页面过来,LiveData也会把最新的数据自动更新。


对于用完即丢的网络请求,LoginViewModel会存在这种代码:


// LoginViewModel.kt
val loginLiveData = MutableLiveData<User?>()
val logoutLiveData = MutableLiveData<Any?>()
val forgetPasswordLiveData = MutableLiveData<User?>(

并且对应的Activity中也需要监听这3个LiveData。


这种模板代码让我写的很烦。


用了Flow优化后,完美的解决这2个痛点。



“Talk is cheap. Show me the code.”



二、集成Flow之后的用法


2.1 请求自带Loading&&不需要监听数据变化


需求:




  • 不需要监听数据变化,对应上面的用完即丢




  • 不需要在ViewModel中声明LiveData成员对象




  • 发起请求之前自动showLoading(),请求结束后自动stopLoading()




  • 类似于点击登录按钮,finish 当前页面,跳转到下一个页面




TestActivity 中示例代码:


// TestActivity.kt
private fun login() {
launchWithLoadingAndCollect({mViewModel.login("username", "password")}) {
onSuccess = { data->
showSuccessView(data)
}
onFailed = { errorCode, errorMsg ->
showFailedView(code, msg)
}
onError = {e ->
e.printStackTrace()
}
}
}

TestViewModel 中代码:


// TestViewModel中代码
suspend fun login(username: String, password: String): ApiResponse<User?> {
return repository.login(username, password)
}

2.2 请求不带Loading&&不需要声明LiveData


需求:




  • 不需要监听数据变化




  • 不需要在ViewModel中声明LiveData成员对象




  • 不需要Loading的展示




// TestActivity.kt
private fun getArticleDetail() {
launchAndCollect({ mViewModel.getArticleDetail() }) {
onSuccess = {
showSuccessView()
}
onFailed = { errorCode, errorMsg ->
showFailedView(code, msg)
}
onDataEmpty = {
showEmptyView()
}
}
}

TestViewModel 中代码和上面一样,这里就不写了。


是不是非常简单,一个方法搞定,将Loading的逻辑都隐藏了,再也不需要手动写 showLoading()stopLoading()


并且请求的结果直接在回调里面接收,直接处理,这样请求网络和结果的处理都在一起,看起来一目了然,再也不需要在 Activity 中到处找在哪监听的 LiveData


同样,它跟 LiveData 一样,也会监听 Activity 的生命周期,不会造成内存泄露。因为它是运行在ActivitylifecycleScope 协程作用域中的。


2.3 需要监听数据变化


需求:




  • 需要监听数据变化,要实时更新数据




  • 需要在 ViewModel 中声明 LiveData 成员对象




  • 例如实时获取最新的配置、最新的用户信息等




TestActivity 中示例代码:


// TestActivity.kt
class TestActivity : AppCompatActivity(R.layout.activity_api) {

private fun initObserver() {
mViewModel.wxArticleLiveData.observeState(this) {

onSuccess = { data: List<WxArticleBean>? ->
showSuccessView(data)
}

onDataEmpty = { showEmptyView() }

onFailed = { code, msg -> showFailedView(code, msg) }

onError = { showErrorView() }
}
}

private fun requestNet() {
// 需要Loading
launchWithLoading {
mViewModel.requestNet()
}
}
}

ViewModel 中示例代码:


class ApiViewModel : ViewModel() {

private val repository by lazy { WxArticleRepository() }

val wxArticleLiveData = StateMutableLiveData<List<WxArticleBean>>()

suspend fun requestNet() {
wxArticleLiveData.value = repository.fetchWxArticleFromNet()
}
}

本质上是通过FLow来调用LiveDatasetValue()方法,还是LiveData的使用。虽然可以完全用 Flow 来实现,但是我觉得这里用 Flow 的方式麻烦,不容易懂,还是怎么简单怎么来。


这种方式其实跟上篇文章中的封装二差不多,区别就是不需要手动调用Loading有关的方法。


三、拆封装


如果不抽取通用方法是这样写的:


// TestActivity.kt
private fun login() {
lifecycleScope.launch {
flow {
emit(mViewModel.login("username", "password"))
}.onStart {
showLoading()
}.onCompletion {
dismissLoading()
}.collect { response ->
when (response) {
is ApiSuccessResponse -> showSuccessView(response.data)
is ApiEmptyResponse -> showEmptyView()
is ApiFailedResponse -> showFailedView(response.errorCode, response.errorMsg)
is ApiErrorResponse -> showErrorView(response.error)
}
}
}
}

简单介绍下Flow


Flow类似于RxJava,操作符都跟Rxjava差不多,但是比Rxjava简单很多,kotlin通过flow来实现顺序流和链式编程。


flow关键字大括号里面的是方法的执行,结果通过emit发送给下游。


onStart表示最开始调用方法之前执行的操作,这里是展示一个 loading ui


onCompletion表示所有执行完成,不管有没有异常都会执行这个回调。


collect表示执行成功的结果回调,就是emit()方法发送的内容,flow必须执行collect才能有结果。因为是冷流,对应的还有热流。


更多的Flow知识点可以参考其他博客和官方文档。


这里可以看出,通过Flow完美的解决了loading的显示与隐藏。


我这里是在Activity中都调用flow的流程,这样我们扩展BaseActivity即可。


为什么扩展的是BaseActivity?


因为startLoading()stopLoading()BaseActivity中。😂


3.1 解决 flow 的 Loading 模板代码


fun <T> BaseActivity.launchWithLoadingGetFlow(block: suspend () -> ApiResponse<T>): Flow<ApiResponse<T>> {
return flow {
emit(block())
}.onStart {
showLoading()
}.onCompletion {
dismissLoading()
}
}

这样每次调用launchWithLoadingGetFlow方法,里面就实现了 Loading 的展示与隐藏,并且会返回一个 FLow 对象。


下一步就是处理 flow 结果collect里面的模板代码。


3.2 声明结果回调类


class ResultBuilder<T> {
var onSuccess: (data: T?) -> Unit = {}
var onDataEmpty: () -> Unit = {}
var onFailed: (errorCode: Int?, errorMsg: String?) -> Unit = { _, _ -> }
var onError: (e: Throwable) -> Unit = { e -> }
var onComplete: () -> Unit = {}
}

各种回调按照项目特性删减即可。


3.3 对ApiResponse对象进行解析


private fun <T> parseResultAndCallback(response: ApiResponse<T>, 
listenerBuilder: ResultBuilder<T>.() -> Unit) {
val listener = ResultBuilder<T>().also(listenerBuilder)
when (response) {
is ApiSuccessResponse -> listener.onSuccess(response.response)
is ApiEmptyResponse -> listener.onDataEmpty()
is ApiFailedResponse -> listener.onFailed(response.errorCode, response.errorMsg)
is ApiErrorResponse -> listener.onError(response.throwable)
}
listener.onComplete()
}

上篇文章这里的处理用的是继承LiveDataObserver,这里就不需要了,毕竟继承能少用就少用。


3.4 最终抽取方法


将上面的步骤连起来如下:


fun <T> BaseActivity.launchWithLoadingAndCollect(block: suspend () -> ApiResponse<T>, 
listenerBuilder: ResultBuilder<T>.() -> Unit) {
lifecycleScope.launch {
launchWithLoadingGetFlow(block).collect { response ->
parseResultAndCallback(response, listenerBuilder)
}
}
}

3.5 将Flow转换成LiveData对象


获取到的是Flow对象,如果想要变成LiveDataFlow原生就支持将Flow对象转换成不可变的LiveData对象。


val loginFlow: Flow<ApiResponse<User?>> =
launchAndGetFlow(requestBlock = { mViewModel.login("UserName", "Password") })
val loginLiveData: LiveData<ApiResponse<User?>> = loginFlow.asLiveData()

调用的是 Flow 的asLiveData()方法,原理也很简单,就是用了livedata的扩展函数:


@JvmOverloads
fun <T> Flow<T>.asLiveData(
context: CoroutineContext = EmptyCoroutineContext,
timeoutInMs: Long = DEFAULT_TIMEOUT
): LiveData<T> = liveData(context, timeoutInMs) {
collect {
emit(it)
}
}

这里返回的是LiveData<ApiResponse<User?>>对象,如果想要跟上篇文章一样用StateLiveData,在observe的回调里面监听不同状态的callback


以前的方式是继承,有如下缺点:



  • 必须要用StateLiveData,不能用原生的LiveData,侵入性很强

  • 不只是继承LiveData,还要继承Observer,麻烦

  • 为了实现这个,写了一堆的代码


这里用 Kotlin 扩展实现,直接扩展 LiveData


@MainThread
inline fun <T> LiveData<ApiResponse<T>>.observeState(
owner: LifecycleOwner,
listenerBuilder: ResultBuilder<T>.() -> Unit
) {
val listener = ResultBuilder<T>().also(listenerBuilder)
observe(owner) { apiResponse ->
when (apiResponse) {
is ApiSuccessResponse -> listener.onSuccess(apiResponse.response)
is ApiEmptyResponse -> listener.onDataEmpty()
is ApiFailedResponse -> listener.onFailed(apiResponse.errorCode, apiResponse.errorMsg)
is ApiErrorResponse -> listener.onError(apiResponse.throwable)
}
listener.onComplete()
}
}

感谢Flywith24开源库提供的思路,感觉自己有时候还是在用Java的思路在写Kotlin。


3.6 进一步完善


很多网络请求的相关并不是只有 loading 状态,还需要在请求前和结束后处理一些特定的逻辑。


这里的方式是:直接在封装方法的参数加 callback,默认用是 loading 的实现。


fun <T> BaseActivity.launchAndCollect(
requestBlock: suspend () -> ApiResponse<T>,
startCallback: () -> Unit = { showLoading() },
completeCallback: () -> Unit = { dismissLoading() },
listenerBuilder: ResultBuilder<T>.() -> Unit
)

四、针对多数据来源


虽然项目中大部分都是单一数据来源,但是也偶尔会出现多数据来源,多数据源结合Flow的操作符,也非常的方便。


示例


假如同一份数据可以从数据库获取,可以从网络请求获取,TestRepository的代码如下:


// TestRepository.kt
suspend fun fetchDataFromNet(): Flow<ApiResponse<List<WxArticleBean>>> {
val response = executeHttp { mService.getWxArticle() }
return flow { emit(response) }.flowOn(Dispatchers.IO)
}

suspend fun fetchDataFromDb(): Flow<ApiResponse<List<WxArticleBean>>> {
val response = getDataFromRoom()
return flow { emit(response) }.flowOn(Dispatchers.IO)


Repository中的返回不再直接返回实体类,而是返回flow包裹的实体类对象。


为什么要这么做?


为了用神奇的flow操作符来处理。


flow组合操作符



  • combine、combineTransform


combine操作符可以连接两个不同的Flow。



  • merge


merge操作符用于将多个流合并。



  • zip


zip操作符会分别从两个流中取值,当一个流中的数据取完,zip过程就完成了。


关于 Flow 的基础操作符,徐医生大神的这篇文章已经写的很棒了,这里就不多余的写了。


根据操作符的示例可以看出,就算返回的不是同一个对象,也可以用操作符进行处理。


几年前刚开始学RxJava时,好几次都是入门到放弃,操作符太多了,搞的也很懵逼,Flow 真的比它简单太多了。


五、flow的奇淫技巧


flowWithLifecycle


需求:
Activity 的 onSume() 方法中请求最新的地理位置信息。


以前的写法:


// TestActivity.kt
override fun onResume() {
super.onResume()
getLastLocation()
}

override fun onDestory() {
super.onDestory()
// 释放获取定位的代码,防止内存泄露
}

这种写法没问题,也很正常,但是用了 Flow 之后,有一种新的写法。


用了 flow 的写法:


// TestActivity.kt
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
getLastLocation()
}

@ExperimentalCoroutinesApi
@SuppressLint("MissingPermission")
private fun getLastLocation() {
if (LocationPermissionUtils.isLocationProviderEnabled() && LocationPermissionUtils.isLocationPermissionGranted()) {
lifecycleScope.launch {
SharedLocationManager(lifecycleScope)
.locationFlow()
.flowWithLifecycle(lifecycle, Lifecycle.State.RESUMED)
.collect { location ->
Log.i(TAG, "最新的位置是:$location")
}
}
}
}

onCreate中书写该函数,然后 flow 的链式调用中加入:


.flowWithLifecycle(lifecycle, Lifecycle.State.RESUMED)


flowWithLifecycle能监听 Activity 的生命周期,在 Activity 的onResume开始请求位置信息,onStop 时自动停止,不会导致内存泄露。



flowWithLifecycle 会在生命周期进入和离开目标状态时发送项目和取消内部的生产者。



这个api需要引入 androidx.lifecycle:lifecycle-runtime-ktx:2.4.0-rc01依赖库。


callbackFlow


有没有发现5.1中调用获取位置信息的代码很简单?


SharedLocationManager(lifecycleScope)
.locationFlow()
.flowWithLifecycle(lifecycle, Lifecycle.State.RESUMED)
.collect { location ->
Log.i(TAG, "最新的位置是:$location")
}

几行代码解决获取位置信息,并且任何地方都直接调用,不要写一堆代码。


这里就是用到callbackFlow,简而言之,callbackFlow就是将callback回调代码变成同步的方式来写。


这里直接上SharedLocationManager的代码,具体细节自行 Google,因为这就不是网络框架的内容。


这里附上主要的代码:


@ExperimentalCoroutinesApi
@SuppressLint("MissingPermission")
private val _locationUpdates: SharedFlow<Location> = callbackFlow<Location> {
val callback = object : LocationCallback() {
override fun onLocationResult(result: LocationResult?) {
result ?: return
Log.d(TAG, "New location: ${result.lastLocation}")
trySend(result.lastLocation)
}

}
Log.d(TAG, "Starting location updates")

fusedLocationClient.requestLocationUpdates(
locationRequest,callback,Looper.getMainLooper())
.addOnFailureListener { e ->close(e)}

awaitClose {
Log.d(TAG, "Stopping location updates")
fusedLocationClient.removeLocationUpdates(callback)
}
}.shareIn(
externalScope,
replay = 0,
started = SharingStarted.WhileSubscribed()
)

完整代码见:GitHub


总结


上一篇文章# 两种方式封装Retrofit+协程,实现优雅快速的网络请求


加上这篇的 flow 网络请求封装,一共是三种对Retrofit+协程的网络封装方式。


对比下三种封装方式:




  • 封装一 (对应分支oneWay) 传递ui引用,可按照项目进行深度ui定制,方便快速,但是耦合高




  • 封装二 (对应分支master) 耦合低,依赖的东西很少,但是写起来模板代码偏多




  • 封装三 (对应分支dev) 引入了新的flow流式编程(虽然出来很久,但是大部分人应该还没用到),链式调用,loading 和网络请求以及结果处理都在一起,很多时候甚至都不要声明 LiveData 对象。




第二种封装我在公司的商业项目App中用了很长时间了,涉及几十个接口,暂时没遇到什么问题。


第三种是我最近才折腾出来的,在公司的新项目中(还没上线)使用,也暂时没遇到什么问题。


如果某位大神看到这篇文章,有不同意见,或者发现封装三有漏洞,欢迎指出,不甚感谢!


项目地址


FastJetpack


项目持续更新...


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

MVVM 进阶版:MVI 架构了解一下~

MVI
前言 Android开发发展到今天已经相当成熟了,各种架构大家也都耳熟能详,如MVC,MVP,MVVM等,其中MVVM更是被官方推荐,成为Android开发中的显学。 不过软件开发中没有银弹,MVVM架构也不是尽善尽美的,在使用过程中也会有一些不太方便之处,而...
继续阅读 »

前言


Android开发发展到今天已经相当成熟了,各种架构大家也都耳熟能详,如MVC,MVP,MVVM等,其中MVVM更是被官方推荐,成为Android开发中的显学。

不过软件开发中没有银弹,MVVM架构也不是尽善尽美的,在使用过程中也会有一些不太方便之处,而MVI可以很好的解决一部分MVVM的痛点。

本文主要包括以下内容



  1. MVC,MVP,MVVM等经典架构介绍

  2. MVI架构到底是什么?

  3. MVI架构实战



需要重点指出的是,标题中说MVI架构是MVVM的进阶版是指MVIMVVM非常相似,并在其基础上做了一定的改良,并不是说MVI架构一定比MVVM适合你的项目

各位同学可以在分析比较各个架构后,选择合适项目场景的架构



经典架构介绍


MVC架构介绍


MVC是个古老的Android开发架构,随着MVPMVVM的流行已经逐渐退出历史舞台,我们在这里做一个简单的介绍,其架构图如下所示:



MVC架构主要分为以下几部分



  1. 视图层(View):对应于xml布局文件和java代码动态view部分

  2. 控制层(Controller):主要负责业务逻辑,在android中由Activity承担,同时因为XML视图功能太弱,所以Activity既要负责视图的显示又要加入控制逻辑,承担的功能过多。

  3. 模型层(Model):主要负责网络请求,数据库处理,I/O的操作,即页面的数据来源


由于androidxml布局的功能性太弱,Activity实际上负责了View层与Controller层两者的工作,所以在androidmvc更像是这种形式:



因此MVC架构在android平台上的主要存在以下问题:



  1. Activity同时负责ViewController层的工作,违背了单一职责原则

  2. Model层与View层存在耦合,存在互相依赖,违背了最小知识原则


MVP架构介绍


由于MVC架构在Android平台上的一些缺陷,MVP也就应运而生了,其架构图如下所示



MVP架构主要分为以下几个部分



  1. View层:对应于ActivityXML,只负责显示UI,只与Presenter层交互,与Model层没有耦合

  2. Presenter层: 主要负责处理业务逻辑,通过接口回调View

  3. Model层:主要负责网络请求,数据库处理等操作,这个没有什么变化


我们可以看到,MVP解决了MVC的两个问题,即Activity承担了两层职责与View层与Model层耦合的问题


MVP架构同样有自己的问题



  1. Presenter层通过接口与View通信,实际上持有了View的引用

  2. 但是随着业务逻辑的增加,一个页面可能会非常复杂,这样就会造成View的接口会很庞大。


MVVM架构介绍


MVVM 模式将 Presenter 改名为 ViewModel,基本上与 MVP 模式完全一致。

唯一的区别是,它采用双向数据绑定(data-binding):View的变动,自动反映在 ViewModel,反之亦然

MVVM架构图如下所示:



可以看出MVVMMVP的主要区别在于,你不用去主动去刷新UI了,只要Model数据变了,会自动反映到UI上。换句话说,MVVM更像是自动化的MVP


MVVM的双向数据绑定主要通过DataBinding实现,不过相信有很多人跟我一样,是不喜欢用DataBinding的,这样架构就变成了下面这样



  1. View观察ViewModle的数据变化并自我更新,这其实是单一数据源而不是双向数据绑定,所以其实MVVM的这一大特性我其实并没有用到

  2. View通过调用ViewModel提供的方法来与ViewMdoel交互


小结



  1. MVC架构的主要问题在于Activity承担了ViewController两层的职责,同时View层与Model层存在耦合

  2. MVP引入Presenter层解决了MVC架构的两个问题,View只能与Presenter层交互,业务逻辑放在Presenter

  3. MVP的问题在于随着业务逻辑的增加,View的接口会很庞大,MVVM架构通过双向数据绑定可以解决这个问题

  4. MVVMMVP的主要区别在于,你不用去主动去刷新UI了,只要Model数据变了,会自动反映到UI上。换句话说,MVVM更像是自动化的MVP

  5. MVVM的双向数据绑定主要通过DataBinding实现,但有很多人(比如我)不喜欢用DataBinding,而是View通过LiveData等观察ViewModle的数据变化并自我更新,这其实是单一数据源而不是双向数据绑定


MVI架构到底是什么?


MVVM架构有什么不足?


要了解MVI架构,我们首先来了解下MVVM架构有什么不足

相信使用MVVM架构的同学都有如下经验,为了保证数据流的单向流动,LiveData向外暴露时需要转化成immutable的,这需要添加不少模板代码并且容易遗忘,如下所示


class TestViewModel : ViewModel() {
//为保证对外暴露的LiveData不可变,增加一个状态就要添加两个LiveData变量
private val _pageState: MutableLiveData<PageState> = MutableLiveData()
val pageState: LiveData<PageState> = _pageState
private val _state1: MutableLiveData<String> = MutableLiveData()
val state1: LiveData<String> = _state1
private val _state2: MutableLiveData<String> = MutableLiveData()
val state2: LiveData<String> = _state2
//...
}

如上所示,如果页面逻辑比较复杂,ViewModel中将会有许多全局变量的LiveData,并且每个LiveData都必须定义两遍,一个可变的,一个不可变的。这其实就是我通过MVVM架构写比较复杂页面时最难受的点。

其次就是View层通过调用ViewModel层的方法来交互的,View层与ViewModel的交互比较分散,不成体系


小结一下,在我的使用中,MVVM架构主要有以下不足



  1. 为保证对外暴露的LiveData是不可变的,需要添加不少模板代码并且容易遗忘

  2. View层与ViewModel层的交互比较分散零乱,不成体系


MVI架构是什么?


MVIMVVM 很相似,其借鉴了前端框架的思想,更加强调数据的单向流动和唯一数据源,架构图如下所示



其主要分为以下几部分



  1. Model: 与MVVM中的Model不同的是,MVIModel主要指UI状态(State)。例如页面加载状态、控件位置等都是一种UI状态

  2. View: 与其他MVX中的View一致,可能是一个Activity或者任意UI承载单元。MVI中的View通过订阅Intent的变化实现界面刷新(注意:这里不是ActivityIntent

  3. Intent: 此Intent不是ActivityIntent,用户的任何操作都被包装成Intent后发送给Model层进行数据请求


单向数据流


MVI强调数据的单向流动,主要分为以下几步:



  1. 用户操作以Intent的形式通知Model

  2. Model基于Intent更新State

  3. View接收到State变化刷新UI。


数据永远在一个环形结构中单向流动,不能反向流动:


上面简单的介绍了下MVI架构,下面我们一起来看下具体是怎么使用MVI架构的


MVI架构实战


总体架构图




我们使用ViewModel来承载MVIModel层,总体结构也与MVVM类似,主要区别在于ModelView层交互的部分



  1. Model层承载UI状态,并暴露出ViewStateView订阅,ViewState是个data class,包含所有页面状态

  2. View层通过Action更新ViewState,替代MVVM通过调用ViewModel方法交互的方式


MVI实例介绍


添加ViewStateViewEvent


ViewState承载页面的所有状态,ViewEvent则是一次性事件,如Toast等,如下所示


data class MainViewState(val fetchStatus: FetchStatus, val newsList: List<NewsItem>)  

sealed class MainViewEvent {
data class ShowSnackbar(val message: String) : MainViewEvent()
data class ShowToast(val message: String) : MainViewEvent()
}


  1. 我们这里ViewState只定义了两个,一个是请求状态,一个是页面数据

  2. ViewEvent也很简单,一个简单的密封类,显示ToastSnackbar


ViewState更新


class MainViewModel : ViewModel() {
private val _viewStates: MutableLiveData<MainViewState> = MutableLiveData()
val viewStates = _viewStates.asLiveData()
private val _viewEvents: SingleLiveEvent<MainViewEvent> = SingleLiveEvent()
val viewEvents = _viewEvents.asLiveData()

init {
emit(MainViewState(fetchStatus = FetchStatus.NotFetched, newsList = emptyList()))
}

private fun fabClicked() {
count++
emit(MainViewEvent.ShowToast(message = "Fab clicked count $count"))
}

private fun emit(state: MainViewState?) {
_viewStates.value = state
}

private fun emit(event: MainViewEvent?) {
_viewEvents.value = event
}
}

如上所示



  1. 我们只需定义ViewStateViewEvent两个State,后续增加状态时在data class中添加即可,不需要再写模板代码

  2. ViewEvents是一次性的,通过SingleLiveEvent实现,当然你也可以用Channel当来实现

  3. 当状态更新时,通过emit来更新状态


View监听ViewState


    private fun initViewModel() {
viewModel.viewStates.observe(this) {
renderViewState(it)
}
viewModel.viewEvents.observe(this) {
renderViewEvent(it)
}
}

如上所示,MVI 使用 ViewStateState 集中管理,只需要订阅一个 ViewState 便可获取页面的所有状态,相对 MVVM 减少了不少模板代码。


View通过Action更新State


class MainActivity : AppCompatActivity() {
private fun initView() {
fabStar.setOnClickListener {
viewModel.dispatch(MainViewAction.FabClicked)
}
}
}
class MainViewModel : ViewModel() {
fun dispatch(action: MainViewAction) =
reduce(viewStates.value, action)

private fun reduce(state: MainViewState?, viewAction: MainViewAction) {
when (viewAction) {
is MainViewAction.NewsItemClicked -> newsItemClicked(viewAction.newsItem)
MainViewAction.FabClicked -> fabClicked()
MainViewAction.OnSwipeRefresh -> fetchNews(state)
MainViewAction.FetchNews -> fetchNews(state)
}
}
}

如上所示,View通过ActionViewModel交互,通过 Action 通信,有利于 ViewViewModel 之间的进一步解耦,同时所有调用以 Action 的形式汇总到一处,也有利于对行为的集中分析和监控


总结


本文主要介绍了MVC,MVP,MVVMMVI架构,目前MVVM是官方推荐的架构,但仍然有以下几个痛点



  1. MVVMMVP的主要区别在于双向数据绑定,但由于很多人(比如我)并不喜欢使用DataBindg,其实并没有使用MVVM双向绑定的特性,而是单一数据源

  2. 当页面复杂时,需要定义很多State,并且需要定义可变与不可变两种,状态会以双倍的速度膨胀,模板代码较多且容易遗忘

  3. ViewViewModel通过ViewModel暴露的方法交互,比较零乱难以维护


MVI可以比较好的解决以上痛点,它主要有以下优势



  1. 强调数据单向流动,很容易对状态变化进行跟踪和回溯

  2. 使用ViewStateState集中管理,只需要订阅一个 ViewState 便可获取页面的所有状态,相对 MVVM 减少了不少模板代码

  3. ViewModel通过ViewStateAction通信,通过浏览ViewStateAciton 定义就可以理清 ViewModel 的职责,可以直接拿来作为接口文档使用。


当然MVI也有一些缺点,比如



  1. 所有的操作最终都会转换成State,所以当复杂页面的State容易膨胀

  2. state是不变的,因此每当state需要更新时都要创建新对象替代老对象,这会带来一定内存开销


软件开发中没有银弹,所有架构都不是完美的,有自己的适用场景,读者可根据自己的需求选择使用。

但通过以上的分析与介绍,我相信使用MVI架构代替没有使用DataBindingMVVM是一个比较好的选择~


Sample代码


github.com/shenzhen201…


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

Android IPC 之 Messenger

绑定服务(Bound Services)概述 绑定服务是client-server接口中的服务器。它允许组件(例如活动)绑定到服务、发送请求、接收响应和执行进程间通信(IPC)。 绑定服务通常仅在它为另一个应用程序组件提供服务时才存在,并且不会无限期地在后台运...
继续阅读 »

绑定服务(Bound Services)概述


绑定服务是client-server接口中的服务器。它允许组件(例如活动)绑定到服务、发送请求、接收响应和执行进程间通信(IPC)。 绑定服务通常仅在它为另一个应用程序组件提供服务时才存在,并且不会无限期地在后台运行


💥 基础知识


绑定服务是 Service 类的实现,它允许其他应用程序绑定到它并与之交互。 要为服务提供绑定,你必须实现 onBind() 回调方法。 此方法返回一个 IBinder 对象,该对象定义了客户端可用于与服务交互的编程接口。


🔥 Messenger


💥 概述


一提到IPC 很多人的反应都是 AIDL,其实如果仅仅是多进程单线程,那么你可以使用 Messenger 为你的服务提供接口。


使用 Messenger 比使用 AIDL 更简单,因为 Messenger 会将所有对服务的调用排入队列


对于大多数应用程序,该服务不需要执行多线程,因此使用 Messenger 允许该服务一次处理一个调用。如果你的 服务多线程很重要,那你就要用到ALDL了。


💥 使用 Messenger 步骤




  • 1、该 Service 实现了一个 Handler,该 Handler 接收来自客户端的每次调用的回调。




  • 2、该服务使用 Handler 创建一个 Messenger 对象(它是对 Handler 的引用)。




  • 3、Messenger 创建一个 IBinder,该服务从 onBind() 返回给客户端。




  • 4、客户端使用 IBinder 来实例化 Messenger(引用服务的Handler),客户端使用 Handler 来向服务发送 Message 对象。




  • 5、服务在其 Handler 的 handleMessage() 中接收每个消息。




💥 实例(Client到Server数据传递)


🌀 MessengerService.java


public class MessengerService extends Service {
public static final int MSG_SAY_HELLO = 0;
//让客户端向IncomingHandler发送消息。
Messenger messenger = null;

//当绑定到服务时,我们向我们的Messenger返回一个接口,用于向服务发送消息。
public IBinder onBind(Intent intent) {
MLog.e("MessengerService:onBind");
//创建 Messenger 对象(对 Handler 的引用)
messenger = new Messenger(new IncomingHander(this));
//返回支持此Messenger的IBinder。
return messenger.getBinder();
}
//实现了一个 Handler
static class IncomingHander extends Handler {
private Context appliacationContext;
public IncomingHander(Context context) {
appliacationContext = context.getApplicationContext();
}

@Override
public void handleMessage(Message msg) {
switch (msg.what){
case MSG_SAY_HELLO:
Bundle bundle = msg.getData();
String string = bundle.getString("name");
//处理来自客户端的消息
MLog.e("handleMessage:来自Acitvity的"+string);
break;
case 1:

break;
default:
super.handleMessage(msg);
}
}
}
}

🌀 AndroidMainfest.xml


        <service android:name=".ipc.MessengerService"
android:process="com.scc.ipc.messengerservice"
android:exported="true"
android:enabled="true"/>

使用 android:process 属性 创建不同进程。


🌀 MainActivity.class


public class MainActivity extends ActivityBase implements View.OnClickListener {
Messenger mService = null;
Messenger messenger = null;
private boolean bound;
private ViewStub v_stud;

@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
...
}

ServiceConnection connection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
//从原始 IBinder 创建一个 Messenger,该 IBinder 之前已使用 getBinder 检索到。
mService = new Messenger(service);
bound = true;
}

@Override
public void onServiceDisconnected(ComponentName name) {
bound = false;
}
};

@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.btn_bind_service:
bindService(new Intent(MainActivity.this, MessengerService.class), connection, Context.BIND_AUTO_CREATE);
break;
case R.id.btn_send_msg:
Message message = Message.obtain(null, MessengerService.MSG_SAY_HELLO);
Bundle bundle = new Bundle();
bundle.putString("name","Scc");
message.setData(bundle);
try {
mService.send(message);
} catch (RemoteException e) {
e.printStackTrace();
}
break;

}
}
@Override
protected void onStop() {
super.onStop();
if (bound) {
unbindService(connection);
bound = false;
}
}
}

🌀 运行效果如下



两个进程也存在着,也完成了进程间的通信,并把数据传递过去了。


💥 实例(Server将数据传回Client)


我不仅想将消息传递给 Server ,还想让 Server 将数据处理后传会Client。


🌀 MessengerService.java


public class MessengerService extends Service {
/** 用于显示和隐藏我们的通知。 */
ArrayList<Messenger> mClients = new ArrayList<Messenger>();
/** 保存客户端设置的最后一个值。 */
int mValue = 0;

/**
* 数组中添加 Messenger (来自客户端)。
* Message 的 replyTo 字段必须是应该发送回调的客户端的 Messenger。
*/
public static final int MSG_REGISTER_CLIENT = 1;

/**
* 数组中删除 Messenger (来自客户端)。
* Message 的 replyTo 字段必须是之前用 MSG_REGISTER_CLIENT 给出的客户端的 Messenger。
*/
public static final int MSG_UNREGISTER_CLIENT = 2;
/**
* 用于设置新值。
* 这可以发送到服务以提供新值,并将由服务发送给具有新值的任何注册客户端。
*/
public static final int MSG_SET_VALUE = 3;
//让客户端向IncomingHandler发送消息。
Messenger messenger = null;

//当绑定到服务时,我们向我们的Messenger返回一个接口,用于向服务发送消息。
public IBinder onBind(Intent intent) {
MLog.e("MessengerService-onBind");
//创建 Messenger 对象(对 Handler 的引用)
messenger = new Messenger(new IncomingHander(this));
//返回支持此Messenger的IBinder。
return messenger.getBinder();
}
//实现了一个 Handler
class IncomingHander extends Handler {
private Context appliacationContext;
public IncomingHander(Context context) {
appliacationContext = context.getApplicationContext();
}

@Override
public void handleMessage(Message msg) {
switch (msg.what){
case MSG_REGISTER_CLIENT:
mClients.add(msg.replyTo);
break;
case MSG_UNREGISTER_CLIENT:
mClients.remove(msg.replyTo);
break;
case MSG_SET_VALUE:
mValue = msg.arg1;
for (int i=mClients.size()-1; i>=0; i--) {
try {
mClients.get(i).send(Message.obtain(null,
MSG_SET_VALUE, mValue, 0));
} catch (RemoteException e) {
// 客户端没了。 从列表中删除它;
//从后往前安全,从前往后遍历数组越界。
mClients.remove(i);
}
}
default:
super.handleMessage(msg);
}
}
}
}

🌀 MainActivity.java


public class MainActivity extends ActivityBase implements View.OnClickListener {
Messenger mService = null;
Messenger messenger = null;
private boolean bound;
private ViewStub v_stud;

@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
...
}

ServiceConnection connection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
//从原始 IBinder 创建一个 Messenger,该 IBinder 之前已使用 getBinder 检索到。
mService = new Messenger(service);
bound = true;
}

@Override
public void onServiceDisconnected(ComponentName name) {
bound = false;
}
};
static class ReturnHander extends Handler {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MessengerService.MSG_SET_VALUE:
//我要起飞:此处处理
MLog.e("Received from service: " + msg.arg1);
break;
default:
super.handleMessage(msg);
}
}
}
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.btn_bind_service:
bindService(new Intent(MainActivity.this, MessengerService.class), connection, Context.BIND_AUTO_CREATE);
break;
case R.id.btn_send_msg:
try {
mMessenger = new Messenger(new ReturnHander());
Message msg = Message.obtain(null,
MessengerService.MSG_REGISTER_CLIENT);
msg.replyTo = mMessenger;
//先发一则消息添加Messenger:msg.replyTo = mMessenger;
mService.send(msg);

// Give it some value as an example.
msg = Message.obtain(null,
MessengerService.MSG_SET_VALUE, this.hashCode(), 0);
//传入的arg1值:this.hashCode()
mService.send(msg);
} catch (RemoteException e) {
e.printStackTrace();
}
break;

}
}
@Override
protected void onStop() {
super.onStop();
if (bound) {
unbindService(connection);
bound = false;
}
}
}

🌀 运行效果如下



我们在MainActivity 的 Handler.sendMessger()中接收到了来自 MesengerService 的消息 。


本次 Messenger 进程间通信齐活,这只是个简单的Demo。最后咱们看一波源码。


🔥 Messenger 源码


Messenger.java


public final class Messenger implements Parcelable {
private final IMessenger mTarget;
public Messenger(Handler target) {
mTarget = target.getIMessenger();
}
public void send(Message message) throws RemoteException {
mTarget.send(message);
}
public IBinder getBinder() {
return mTarget.asBinder();
}
...
public Messenger(IBinder target) {
mTarget = IMessenger.Stub.asInterface(target);
}
}

然后你会发现 只要代码还是在 IMessenger 里面,咱们去找找。


IMessenger.aidl


package android.os;

import android.os.Message;

/** @hide */
oneway interface IMessenger {
void send(in Message msg);
}

new Messenger(Handler handelr)


这里其实是用Handler 调用 getIMessenger() 。咱们去Handler.class里面转转。


    @UnsupportedAppUsage
final IMessenger getIMessenger() {
synchronized (mQueue) {
if (mMessenger != null) {
return mMessenger;
}
mMessenger = new MessengerImpl();
return mMessenger;
}
}
//创建了Messenger实现类
private final class MessengerImpl extends IMessenger.Stub {
public void send(Message msg) {
msg.sendingUid = Binder.getCallingUid();
//Messenger调用send()方法,通过Handler发送消息。
//然后在服务端通过Handler的handleMessge(msg)接收这个消息。
Handler.this.sendMessage(msg);
}
}

new Messenger(IBinder target)


package android.os;
/** @hide */
public interface IMessenger extends android.os.IInterface
{
/** Default implementation for IMessenger. */
public static class Default implements android.os.IMessenger
{
@Override public void send(android.os.Message msg) throws android.os.RemoteException
{
}
@Override
public android.os.IBinder asBinder() {
return null;
}
}
/** Local-side IPC implementation stub class. */
public static abstract class Stub extends android.os.Binder implements android.os.IMessenger
{
/** Construct the stub at attach it to the interface. */
public Stub()
{
this.attachInterface(this, DESCRIPTOR);
}
/**
* Cast an IBinder object into an android.os.IMessenger interface,
* generating a proxy if needed.
*/
public static android.os.IMessenger asInterface(android.os.IBinder obj)
{
if ((obj==null)) {
return null;
}
android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
//判断是否在同一进程。
if (((iin!=null)&&(iin instanceof android.os.IMessenger))) {
//同一进程
return ((android.os.IMessenger)iin);
}
//代理对象
return new android.os.IMessenger.Stub.Proxy(obj);
}
@Override public android.os.IBinder asBinder()
{
return this;
}
...
}
public void send(android.os.Message msg) throws android.os.RemoteException;
}

看了上面代码你会发现这不就是个aidl吗? 什么是aidl,咱们下一篇继续讲到。


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

一天高中的女同桌突然问我是不是程序猿

背景 昨天一个我高中的女同桌突然发微信问我“你是不是程序猿 我有问题求助”, 先是激动后是茫然再是冷静,毕业多年不见联系,突然发个信息求助,感觉大脑有点反应不过来... 再说我一个搞Android的也不咋会python啊(不是说Java不能实现,大家懂的,人...
继续阅读 »

背景


昨天一个我高中的女同桌突然发微信问我“你是不是程序猿 我有问题求助”,


image-20211015101843733.png


先是激动后是茫然再是冷静,毕业多年不见联系,突然发个信息求助,感觉大脑有点反应不过来... 再说我一个搞Android的也不咋会python啊(不是说Java不能实现,大家懂的,人生苦短,我用python),即使如此,
为了大家的面子,为了程序猿们的脸,不就简单的小Python嘛,必须答应!


梳理需求


现有excel表格记录着 有效图片的名字,如:


image-20211015103418631.png


要从一个文件夹里把excel表格里记录名字的图片筛选出来;


需求也不是很难,代码思路就有了:



  1. 读取Excel表格第一列的信息并放入A集合

  2. 遍历文件夹下所有的文件,判断文件名字是否存在A集合

  3. 存在A集合则拷贝到目标文件夹


实现(Python 2.7)


读取Excel表格

加载Excel表格的方法有很多种,例如pandasxlrdopenpyxl,我这里选择openpyxl库,
先安装库



pip install openpyxl



代码如下:


from openpyxl import load_workbook

def handler_excel(filename=r'C:/Users/xxx/Desktop/haha.xlsx'):
   # 根据文件路径加载一个excel表格,这里包含所有的sheet
   excel = load_workbook(filename)
   # 根据sheet名称加载对应的table
   table = excel.get_sheet_by_name('Sheet1')
   imgnames = []
   # 读取所有列
   for column in table.columns:
       for cell in column:
           imgnames.append(cell.value+".png")
# 选择图片
   pickImg(imgnames)

遍历文件夹读取文件名,找到target并拷贝

使用os.listdir 方法遍历文件,这里注意windows环境下拿到的unicode编码,需要GBK重新解码


def pickImg(pickImageNames):
   # 遍历所有图片集的文件名
   for image in os.listdir(
           r"C:\Users\xxx\Desktop\work\img"):
       # 使用gbk解码,不然中文乱码
       u_file = image.decode('gbk')
       print(u_file)
       if u_file in pickImageNames:
           oldname = r"C:\Users\xxx\Desktop\work\img/" + image
           newname = r"C:\Users\xxx\Desktop\work\target/" + image
           # 文件拷贝
           shutil.copyfile(oldname, newname)

简单搞定!没有砸程序猿的招牌,豪横的把成果发给女同桌,结果:


image-20211015112550343.png


换来有机会请你吃饭,微信都不带回的,哎 ,xdm,小丑竟是我自己!
小丑竟是我自己什么梗-小丑竟是我自己是什么意思出自什么-55手游网


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

美团面试官问我一个字符的String.length()是多少,我说是1,面试官说你回去好好学一下吧

public class testT { public static void main(String [] args){ String A = "hi你是乔戈里"; System.out.println(A.lengt...
继续阅读 »


public class testT {
public static void main(String [] args){
String A = "hi你是乔戈里";
System.out.println(A.length());
}
}复制代码


以上结果输出为7。







小萌边说边在IDEA中的win环境下选中String.length()函数,使用ctrl+B快捷键进入到String.length()的定义。


    /**
* Returns the length of this string.
* The length is equal to the number of <a href="Character.html#unicode">Unicode
* code units</a> in the string.
*
* @return the length of the sequence of characters represented by this
* object.
*/
public int length() {
return value.length;
}复制代码


接着使用google翻译对这段英文进行了翻译,得到了大体意思:返回字符串的长度,这一长度等于字符串中的 Unicode 代码单元的数目。


小萌:乔戈里,那这又是啥意思呢?乔哥:前几天我写的一篇文章:面试官问你编码相关的面试题,把这篇甩给他就完事!)里面对于Java的字符使用的编码有介绍:


Java中 有内码和外码这一区分简单来说



  • 内码:char或String在内存里使用的编码方式。
  • 外码:除了内码都可以认为是“外码”。(包括class文件的编码)



而java内码:unicode(utf-16)中使用的是utf-16.所以上面的那句话再进一步解释就是:返回字符串的长度,这一长度等于字符串中的UTF-16的代码单元的数目。




代码单元指一种转换格式(UTF)中最小的一个分隔,称为一个代码单元(Code Unit),因此,一种转换格式只会包含整数个单元。UTF-X 中的数字 X 就是各自代码单元的位数。


UTF-16 的 16 指的就是最小为 16 位一个单元,也即两字节为一个单元,UTF-16 可以包含一个单元和两个单元,对应即是两个字节和四个字节。我们操作 UTF-16 时就是以它的一个单元为基本单位的。


你还记得你前几天被面试官说菜的时候学到的Unicode知识吗,在面试官让我讲讲Unicode,我讲了3秒说没了,面试官说你可真菜这里面提到,UTF-16编码一个字符对于U+0000-U+FFFF范围内的字符采用2字节进行编码,而对于字符的码点大于U+FFFF的字符采用四字节进行编码,前者是两字节也就是一个代码单元,后者一个字符是四字节也就是两个代码单元!


而上面我的例子中的那个字符的Unicode值就是“U+1D11E”,这个Unicode的值明显大于U+FFFF,所以对于这个字符UTF-16需要使用四个字节进行编码,也就是使用两个代码单元!


所以你才看到我的上面那个示例结果表示一个字符的String.length()长度是2!



来看个例子!


public class testStringLength {
public static void main(String [] args){
String B = "𝄞"; // 这个就是那个音符字符,只不过由于当前的网页没支持这种编码,所以没显示。
String C = "\uD834\uDD1E";// 这个就是音符字符的UTF-16编码
System.out.println(C);
System.out.println(B.length());
System.out.println(B.codePointCount(0,B.length()));
// 想获取这个Java文件自己进行演示的,可以在我的公众号【程序员乔戈里】后台回复 6666 获取
}
}复制代码



可以看到通过codePointCount()函数得知这个音乐字符是一个字符!




几个问题:0.codePointCount是什么意思呢?1.之前不是说音符字符是“U+1D11E”,为什么UTF-16是"uD834uDD1E",这俩之间如何转换?2.前面说了UTF-16的代码单元,UTF-32和UTF-8的代码单元是多少呢?



一个一个解答:


第0个问题:


codePointCount其实就是代码点数的意思,也就是一个字符就对应一个代码点数。


比如刚才音符字符(没办法打出来),它的代码点是U+1D11E,但它的代理单元是U+D834和U+DD1E,如果令字符串str = "u1D11E",机器识别的不是音符字符,而是一个代码点”/u1D11“和字符”E“,所以会得到它的代码点数是2,代码单元数也是2。


但如果令字符str = "uD834uDD1E",那么机器会识别它是2个代码单元代理,但是是1个代码点(那个音符字符),故而,length的结果是代码单元数量2,而codePointCount()的结果是代码点数量1.


第1个问题




上图是对应的转换规则:



  • 首先 U+1D11E-U+10000 = U+0D11E
  • 接着将U+0D11E转换为二进制:0000 1101 0001 0001 1110,前10位是0000 1101 00 后10位是01 0001 1110
  • 接着套用模板:110110yyyyyyyyyy 110111xxxxxxxxxx
  • U+0D11E的二进制依次从左到右填入进模板:110110 0000 1101 00 110111 01 0001 1110
  • 然后将得到的二进制转换为16进制:d834dd1e,也就是你看到的utf-16编码了



第2个问题




  • 同理,UTF-32 以 32 位一个单元,它只包含这一种单元就够了,它的一单元自然也就是四字节了。
  • UTF-8 的 8 指的就是最小为 8 位一个单元,也即一字节为一个单元,UTF-8 可以包含一个单元,二个单元,三个单元及四个单元,对应即是一,二,三及四字节。






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

领导:谁再用定时任务实现关闭订单,立马滚蛋!

在电商、支付等领域,往往会有这样的场景,用户下单后放弃支付了,那这笔订单会在指定的时间段后进行关闭操作,细心的你一定发现了像某宝、某东都有这样的逻辑,而且时间很准确,误差在1s内;那他们是怎么实现的呢? 一般的做法有如下几种定时任务关闭订单rocketmq延迟...
继续阅读 »

在电商、支付等领域,往往会有这样的场景,用户下单后放弃支付了,那这笔订单会在指定的时间段后进行关闭操作,细心的你一定发现了像某宝、某东都有这样的逻辑,而且时间很准确,误差在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
{

@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 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 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
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

搜索历史记录的实现-Android

前言最近一个客户想要实现搜索中搜索历史的功能,其实这个功能听起来很简单,实际上里面有很多逻辑在里面,一开始写的时候脑子蒙蒙的,最后提给客户的时候一堆毛病,这一次来详细梳理一下,也分享一下我的思路主要逻辑搜索后保存当前内容将最新的搜索记录在最前面搜索历史记录可以...
继续阅读 »

前言

最近一个客户想要实现搜索中搜索历史的功能,其实这个功能听起来很简单,实际上里面有很多逻辑在里面,一开始写的时候脑子蒙蒙的,最后提给客户的时候一堆毛病,这一次来详细梳理一下,也分享一下我的思路

主要逻辑

  1. 搜索后保存当前内容
  2. 将最新的搜索记录在最前面
  3. 搜索历史记录可以点击并执行搜索功能,并将其提到最前面

我里面使用了ObjectBox作为数据存储,因为实际项目用的Java所以没用Room,而且Room好像第一次搜索至少要200ms,不过可以在某个activity随便搜索热启动一下.GreenDao使用有点麻烦,查询条件没有什么太大需求,直接用ObjectBox了,而且使用超级简单

Code

ObjectBox的工具类

public class ObjectBoxUtils {
public static BoxStore init() {
BoxStore boxStore = null;
try {
boxStore = MyApplication.getBoxStore();
if (boxStore == null) {
boxStore = MyObjectBox.builder().androidContext(MyApplication.applicationContext).build();
MyApplication.setBoxStore(boxStore);
}
} catch (Exception e) {
}
return boxStore;
}


public static <T> List<T> getAllData(Class clazz) {
try {
BoxStore boxStore = init();
if (boxStore != null && !boxStore.isClosed()) {
Box<T> box = boxStore.boxFor(clazz);

return box.getAll();
}
} catch (Exception e) {
}
return new ArrayList<>();
}


/**
* 添加数据
*/
public static <T> long addData(T o, Class c) {
try {
BoxStore boxStore = init();
if (boxStore != null && !boxStore.isClosed()) {
return boxStore.boxFor(c).put(o);
}
} catch (Throwable e) {
}
return 0;
}


public static HistoryBean getHistroyBean(String name) {
try {
BoxStore boxStore = init();
if (boxStore != null && !boxStore.isClosed()) {
Box<HistoryBean> box = boxStore.boxFor(HistoryBean.class);
HistoryBean first = box.query().equal(HistoryBean_.name, name).build().findFirst();
return first;
}
} catch (Exception e) {
}
return null;
}
}

其实我在Application就初始化了ObjectBox,但是实际项目中有时候会初始化失败,导致直接空指针,所有每次调用我都会判断一下是否初始化了,没有的话就进行相应操作

Activity

class HistoryActivity : AppCompatActivity() {
private var list: MutableList<HistoryBean>? = null
private var inflate: ActivityHistoryBinding? = null
private var historyAdapter: HistoryAdapter? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
inflate = ActivityHistoryBinding.inflate(layoutInflater)
setContentView(inflate?.root)
list = ObjectBoxUtils.getAllData(HistoryBean::class.java)
list?.sort()
inflate!!.rv.layoutManager = LinearLayoutManager(this, RecyclerView.HORIZONTAL, false)
historyAdapter = HistoryAdapter(this, list)
inflate!!.rv.adapter = historyAdapter


inflate!!.et.setOnEditorActionListener(object : TextView.OnEditorActionListener {
override fun onEditorAction(v: TextView?, actionId: Int, event: KeyEvent?): Boolean {
saveHistory(inflate!!.et.text.toString())
return true
}

})
}

/**
* 保存搜索历史
*
*/
fun saveHistory(keyWord: String) {
//查询本地是否有name为参数中的数据
var histroyBean: HistoryBean? = ObjectBoxUtils.getHistroyBean(keyWord)
val currentTimeMillis = System.currentTimeMillis()
//没有就新创建一个
if (histroyBean == null) {
histroyBean = HistoryBean(currentTimeMillis, keyWord, currentTimeMillis)
} else {
//有的话就更新时间,也就说明了两种情况,第一 重复搜索了,搜索肯定要排重嘛,第二就是我们点击历史记录了,因此更新下时间
histroyBean.setTime(currentTimeMillis)
}
//把新/旧数据保存到本地
ObjectBoxUtils.addData(histroyBean, HistoryBean::class.java)
//每一次操作都从数据库拿取数据,性能消耗很低,就这么一个小模块没必要上纲上线
list?.clear()
list?.addAll(ObjectBoxUtils.getAllData(HistoryBean::class.java))
//实体Bean重写了Comparable,排序一下
list?.sort()
historyAdapter?.notifyDataSetChanged()
}
}

相应注释都在代码里,说实话kotlin用的好难受啊,还是自己语法学的不行,一个小东西卡我好久,导致我Application里面直接删除用Java重写了

实体类

@Entity
public class HistoryBean implements Comparable<HistoryBean> {
@Id(assignable = true)
public long id;

public HistoryBean(long id, String name, long time) {
this.id = id;
this.name = name;
this.time = time;
}

public String name;

public long time;

public long getId() {
return id;
}

public void setId(long id) {
this.id = id;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public long getTime() {
return time;
}

public void setTime(long time) {
this.time = time;
}


@Override
public int compareTo(HistoryBean o) {
return (int) (o.time-time);
}
}

实体类重写了CompareTo,因为集合的sort实际上也是调用了ComparteTo,我们直接重写相应逻辑就简化业务层很多代码

效果

历史记录.gif

嗯,效果还不错,继续学习令人脑壳痛的自定义View去了


收起阅读 »

动态代理的使用-功能增强

背景接手某项目时碰到切换主线程的逻辑, 原项目代码流程如下:xxPresenter 会创建observer直接用于二方库的 SDKService (通常在子线程中回调),记为 innerObserverxxActivit...
继续阅读 »

背景

接手某项目时碰到切换主线程的逻辑, 原项目代码流程如下:

切换主线程时序图.png

  1. xxPresenter 会创建observer直接用于二方库的 SDKService (通常在子线程中回调),记为 innerObserver
  2. xxActivity 也需要创建observer用于主线程回调, 记为 uiObserver
  3. xxPresenter 在收到 innerObserver 的回调后通过主线程handler进行线程切换, 最终触发 uiObserver 的对应方法
  4. 业务需求回调方法都在 xxActivity 主线程中执行后续操作, innerObserver 几乎仅用于线程切换而已

存在的问题

  1. 如图第2/3步, 对于同一类型的observer, 需要在 activity , presenter中各实现一次, presenter中会产生大量模板代码
  2. 如图第6步, 收到 SDKService 回调后, presenter需要构建Message, 设置各回调实参, 这完全依赖开发人员手动配置, 效率低下且易发生错误, 灵活度低
  3. 对应的, 第11步通过handler线程切换时, 又需要从 message 中依次还原各实参, 这一步同样依赖开发人员手动处理
  4. observer变化时(如形参列表顺序/类型发生变更), 均需要同步更新 prenter 和 handler
  5. 我司项目最多时, 某个SDKService有将近100个observer需要设置, 部分observer的方法数甚至超过45个, 导致单纯在 Presenter 中创建observer的空白匿名内部类时, 代码就超过100行, 模板代码过多
  6. ...

改造思路

根据已知条件:

  1. 各observer均为接口 interface 类型
  2. presenter 中实现的 innerObserver 仅用于进行线程切换,最终触发UI层创建的observer而已 --> 即:有统一的功能增强逻辑

自然联想到 代理模式 中的动态代理:

代理模式-图侵删,来源于C语言中文网

  1. 创建一个 ThreadSwitcher 辅助类, 可根据传入的 observer 的类型Class,自动生成动态代理类对象,即之前的 innerObserver, 然后作用于sdk中 --> 此步骤可节省prsetner中因 new observer(){} 产生的大量模板代码, 且在observer接口发生变更时, 也不需要修改代码,自动完成适配, 伪代码如下:
    Observer innerOb = ThreadSwitcher.generateInnerObserver(Observer.class)

  2. ThreadSwitcher 类同时透出接口供UI层传入用于主线程的observer, 缓存在 Map<Class,IObserver> 中, 供后续切换主线程时使用

  3. 当下层sdk回调动态代理对象时, 最终都会触发 InvocationHandler#invoke 方法, 其方法签名如下, 我们只需要在其方法体中构造runnable, 按需post到主线程中即可:

// package java.lang.reflect.InvocationHandler.java
/**
* @param method 接口中被触发的回调方法
* @param args 方法实参列表
*/

public Object invoke(Object proxy, Method method, Object[] args);
  1. 构造的runnable时, 需查找UI层注入的observer,并触发对应的方法, 而由于 InvocationHandler中已告知我们方法 method 及其实参 args , 因此可直接通过 method.invoke(uiObserver,args) 来触发 uiObserver 的对应方法, 具体代码见下一节

动态代理的使用

import java.lang.reflect.InvocationHandler
import java.lang.reflect.Method
import java.lang.reflect.Proxy

object ThreadSwitcher {
// ui层注入的observer, 会在主线程中回调
val uiObserverMap = mutableMapOf<Class<*>, Any>()
val targetHandler: Handler = Handler(Looper.mainLooper())

private fun runOnUIThread(runnable: Runnable) {
// 此处省略切换主线程代码,创建一个mainLooper的handler, post Runnable即可
}

// 生成代理类
fun <O> generateInnerObserver(clz: Class<O>): O? {
// 固定写法, 传入classLoader 和 待实现的接口列表, 以及核心的 InvocationHandler 的实现, 在其内部进行功能增强
return Proxy.newProxyInstance(clz.classLoader, arrayOf(clz), object : InvocationHandler {
override fun invoke(proxy: Any?, method: Method?, args: Array<out Any>?): Any? {

// 1. 构造runnable, 用于主线程切换
val runnable = Runnable {

// 3. 查找 uiObserver, 若存在则触发
uiObserverMap[clz]?.let { uiObserver ->
val result = method?.invoke(uiObserver, args)
result
}
}

// 2. 将runnable抛主线程
runOnUIThread(runnable)

// 4. 触发method方法得到的返回值, 根据实际类型构造, void时返回null, 此处仅做示意
return null
}
}) as O // 按需强转为实现的接口类型
}
}

具体封装实现可参考如下链接:

改造后的流程如下:

改造后的时序图.png

源码分析

动态代理的实现很简单, 两三行代码就可以搞定, 系统肯定做了很多封装, 把脏活累活给做了, 我们简单看下

从入口方法开始: java.lang.reflect.Proxy#newProxyInstance

// package java.lang.reflect.Proxy.java  基于api 29
private static final Class<?>[] constructorParams = { InvocationHandler.class };

public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h){
final Class<?>[] intfs = interfaces.clone();

// 从缓存中查找已生成过的class类型,若不存在则进行生成
Class<?> cl = getProxyClass0(loader, intfs);

// 反射调用构造方法 Proxy(InvocationHandler), 创建并返回实例
final Constructor<?> cons = cl.getConstructor(constructorParams);
final InvocationHandler ih = h;
if (!Modifier.isPublic(cl.getModifiers())) {
cons.setAccessible(true);
}
return cons.newInstance(new Object[]{h});
}

private static final WeakCache<ClassLoader, Class<?>[], Class<?>>
proxyClassCache = new WeakCache<>(new KeyFactory(), new ProxyClassFactory());

/**
* 创建代理类class
*/

private static Class<?> getProxyClass0(ClassLoader loader,Class<?>... interfaces) {
// 接口方法数限制
if (interfaces.length > 65535) { throw new IllegalArgumentException("interface limit exceeded"); }

// 优先从缓存中获取已创建过的代理类, 若不存在, 则创建
return proxyClassCache.get(loader, interfaces);
}

关键的 proxyClassCache 是个二级缓存类(WeakCache), 通过调用其 get 方法得到最终的实现类, 其构造方法签名如下:

// java.lang.reflect.WeakCache.java

/**
* Construct an instance of {@code WeakCache}
*
* @param subKeyFactory a function mapping a pair of
* {@code (key, parameter) -> sub-key}
* @param valueFactory a function mapping a pair of
* {@code (key, parameter) -> value}
* @throws NullPointerException if {@code subKeyFactory} or
* {@code valueFactory} is null.
*/

public WeakCache(BiFunction<K, P, ?> subKeyFactory, BiFunction<K, P, V> valueFactory) {

通过参数名也可以猜到最终是通过 valueFactory 生成的, 我们回到 Proxy 类看下:

// java.lang.reflect.Proxy.java

private static final WeakCache<ClassLoader, Class<?>[], Class<?>>
proxyClassCache = new WeakCache<>(new KeyFactory(), new ProxyClassFactory());

/**
* A factory function that generates, defines and returns the proxy class given
* the ClassLoader and array of interfaces.
*/

private static final class ProxyClassFactory
implements BiFunction<ClassLoader, Class<?>[], Class<?>>
{
// 所有动态代理类名的前缀
private static final String proxyClassNamePrefix = "$Proxy";

// 每一个动态代理类类名中唯一的数字,可猜测最终是分层的代理类名就是: $Proxy+数字
private static final AtomicLong nextUniqueNumber = new AtomicLong();

@Override
public Class<?> apply(ClassLoader loader, Class<?>[] interfaces) {
// 省略部分代码: 对传入的接口数组进行一些校验

String proxyPkg = null; // 最终实现类所在的包路径
int accessFlags = Modifier.PUBLIC | Modifier.FINAL; // 生成的代理类默认访问权限是: public final

// 对接口数组校验: 若待实现的接口是非public的, 则最终实现的代理类也是非public的,并且非public的接口需要在同一个包下
for (Class<?> intf : interfaces) {
int flags = intf.getModifiers();
if (!Modifier.isPublic(flags)) {
accessFlags = Modifier.FINAL;
String name = intf.getName();
int n = name.lastIndexOf('.');
String pkg = ((n == -1) ? "" : name.substring(0, n + 1));
if (proxyPkg == null) {
proxyPkg = pkg;
} else if (!pkg.equals(proxyPkg)) {
throw new IllegalArgumentException(
"non-public interfaces from different packages");
}
}
}

// 若待实现的接口均为 public, 则使用默认的包路径
if (proxyPkg == null) { proxyPkg = ""; }

{
List<Method> methods = getMethods(interfaces); // 递归获取所有接口(包括其父接口)的方法,并手动添加了 equals/hashCode/toString 三个方法
Collections.sort(methods, ORDER_BY_SIGNATURE_AND_SUBTYPE); // 对所有接口方法排序
validateReturnTypes(methods); // 校验接口方法: 确保同名方法得返回类型一致
List<Class<?>[]> exceptions = deduplicateAndGetExceptions(methods); // 去除重复的方法,并获取每个方法对应的异常值信息

Method[] methodsArray = methods.toArray(new Method[methods.size()]);
Class<?>[][] exceptionsArray = exceptions.toArray(new Class<?>[exceptions.size()][]);

long num = nextUniqueNumber.getAndIncrement(); // 生成当前代理实现类的数字信息
String proxyName = proxyPkg + proxyClassNamePrefix + num; // 拼接生成代理类名,默认为: $Proxy+数字

return generateProxy(proxyName, interfaces, loader, methodsArray, exceptionsArray); // 通过native方法生成代理类Class
}
}

@FastNative
private static native Class<?> generateProxy(String name, Class<?>[] interfaces, ClassLoader loader, Method[] methods, Class<?>[][] exceptions);

/**
* 根据传入的接口class信息,获取所有的接口方法,并额外添加 equals/hashCode/toString 三个方法
*/

private static List<Method> getMethods(Class<?>[] interfaces) {
List<Method> result = new ArrayList<Method>();
try {
result.add(Object.class.getMethod("equals", Object.class));
result.add(Object.class.getMethod("hashCode", EmptyArray.CLASS));
result.add(Object.class.getMethod("toString", EmptyArray.CLASS));
} catch (NoSuchMethodException e) {
throw new AssertionError();
}

getMethodsRecursive(interfaces, result); // 通过递归反射的方式一次获取接口所有的方法
return result;
}
}

动态代理生成的类长啥样?

上面我们简单分析了下动态代理的源码, 我们可以知道/推测得到以下信息:

  1. 生成的代理类叫做 $ProxyN 其中 N 是一个数字,随代理类的增加而递增
  2. $ProxyN 实现了所有接口方法,并自动添加了 equals/hashCode/toString 三个方法,因此: --> a. 动态代理生成类应可以强转为任何传入的接口类型 --> b. 额外增加的三个方法通常会影响对象的比较,需要手动赋值区分
  3. 触发动态代理类的方法最终都会回调 InvocationHandler#invoke 方法,而 InvocationHandler 是通过 Proxy#newProxyInstance 传入的,因此: --> 猜测生成 $ProxyN 类应是继承自 Proxy 类

猜测归猜测, 最好能导出生成的 $ProxyN 看下实际代码:

  1. 网上查到的通常是使用 JVM 提供的 sun.misc.ProxyGenerator 类, 但这个类在android中不存在,手动拷贝对应jar包到android中使用也有问题
  2. 尝试使用字节码操作库或者 Class#getResourceAsStream 等方式也失败了, 终究是JVM上的工具, 在android虚拟机上无法直接使用
  3. 最终退而求其次, 先通过反射获取 $ProxyN 的类结构, 至于方法的调用则通过 InvocationHandler#invoke 方法中打印堆栈来查看
// 1. 自定义一个接口如下
package org.lynxz.utils.observer
interface ICallback {
fun onCallback(a: Int, b: Boolean, c: String?)
}

// 2. 通过反射获取类结构
package org.lynxz.utils.reflect.ReflectUtilTest
@Test
fun oriProxyTest() {
val proxyObj = Proxy.newProxyInstance(
javaClass.classLoader,
arrayOf(ICallback::class.java)
) { proxy, method, args -> // InvocationHandler#invoke 方法体
RuntimeException("===> 调用堆栈:${method?.name}").printStackTrace() // 3. 打印调用堆栈信息
args?.forEachIndexed { index, any -> // 4. 打印方法得实参
LoggerUtil.w(TAG, "===> 方法参数: $index - $any")
}
ReflectUtil.generateDefaultTypeValue(method!!.returnType) // 根据方法返回类型生成对应数据
}

// ProxyGeneratorImpl 是自定义的通过反射获取类结构的实现类, 具体代码请查看上面给出的github仓库
LoggerUtil.w(TAG, "===>类结构:\n${ProxyGeneratorImpl(proxyObj.javaClass).generate()}")
if (proxyObj is ICallback) { // 强转生成的动态代理类为自定义的接口
proxyObj.onCallback(1, true, "hello") // 触发接口方法,以便触发 InvocationHandler#invoke 方法, 进而打印堆栈
}
}

最终得到日志如下, 验证了之前的猜测:

// ===>类结构:
public final class $Proxy6 extends java.lang.reflect.Proxy implements ICallback{
public static final Class[] NFC;
public static final Class[][] NFD;
public $Proxy6(Class){...}
public final boolean equals(Object){...} // 方法体的内容不可知, 此处用省略号替代
public final int hashCode(){...}
public final String toString(){...}
public final void onCallback(int,boolean,String){...}
}

// 调用堆栈:
===> 调用堆栈:onCallback
at org.lynxz.utils.reflect.ReflectUtilTest$oriProxyTest$proxyObj$1.invok(ReflectUtilTest.kt:86) // 对应上方代码: RuntimeException("===> 调用堆栈:${method?.name}").printStackTrace()
at java.lang.reflect.Proxy.invoke(Proxy.java:913) // 触发 Proxy#invoke 方法, 其内部直接触发 InvocationHandler#invoke 方法
at $Proxy6.onCallback(Unknown Source) // 对应上方代码: proxyObj.onCallback(1, true, "hello")

// 打印方法实参数据, 序号 - 值, 与我们传入的相同
===> 方法参数: 0 - 1
===> 方法参数: 1 - true
===> 方法参数: 2 - hello

Proxy#invoke 源码, 就是简单的触发 InvocationHandler#invoke 而已

// java.lang.reflect.Proxy.java
protected InvocationHandler h;
protected Proxy(InvocationHandler h) {
Objects.requireNonNull(h);
this.h = h;
}

// 直接触发 invocationHandler 方法
// 而 InvocationHandler 是通过 Proxy#newProxyInstance 传入的, 最终传到 $Proxy6 的构造方法
private static Object invoke(Proxy proxy, Method method, Object[] args) throws Throwable {
InvocationHandler h = proxy.h; // 此处的proxy就是上面动态代理生成 `$Proxy6` 类
return h.invoke(proxy, method, args);
}

收起阅读 »

smali语言之locals和registers的区别

介绍对于dalviks字节码寄存器都是32位的,它能够表示任何类型,2个寄存器用于表示64位的类型(Long and Double)。作用声明于方法内部(必须).method public getName()V .registers 6 retu...
继续阅读 »

介绍

对于dalviks字节码寄存器都是32位的,它能够表示任何类型,2个寄存器用于表示64位的类型(Long and Double)。

作用

声明于方法内部(必须)

.method public getName()V
.registers 6

return-void
.end method

.registers和locals基本区别

在一个方法(method)中有两中方式指定有多少个可用的寄存器。指令.registers指令指定了在这个方法中有多少个可用的寄存器,

指令.locals指明了在这个方法中非参(non-parameter)寄存器的数量。然而寄存器的总数也包括保存方法参数的寄存器。

参数是如何传递的?

1.如果是非静态方法

例如,你写了一个非静态方法LMyObject;->callMe(II)V。这个方法有2个int参数,但在这两个整型参数前面还有一个隐藏的参数LMyObject;也就是当前对象的引用,所以这个方法总共有3个参数。 假如在一个方法中包含了五个寄存器(V0-V4),如下:

.method public callMe(II)V
const-string v0,"1"
const-string v1,"1"

return-void
.end method

那么只需用.register指令指定5个,或者使用.locals指令指定2个(2个local寄存器+3个参数寄存器)。如下:

.method public callMe(II)V
.registers 5
const-string v0,"1"
const-string v1,"1"
v3==>p0
V4==>P1
V5==>P2

return-void
.end method

或者
.method public callMe(II)V
.locals 2
const-string v0,"1"
const-string v1,"1"
return-void
.end method

该方法被调用的时候,调用方法的对象(即this引用)会保存在V2中,第一个参数在V3中,第二个参数在v4中。

2.如果是静态方法

那么参数少了对象引用,除此之外和非静态原理相同,registers为4 locals依然是2

关于寄存器命名规则

v命名法

上面的例子中我们使用的是v命名法,也就是在本地寄存器后面依次添加参数寄存器,

但是这种命名方式存在一种问题:假如我后期想要修改方法体的内容,涉及到增加或者删除寄存器,由于v命名法需要排序的局限性,那么会造成大量代码的改动,有没有一种办法让我们只改动registers或者locals的值就可以了呢, 答案是:有的

v命名法之外,还有一种命名法叫做p命名法

p命名法

p命名法只能给方法参数命名,不能给本地变量命名

假如有一个非静态方法如下:

.method public print(Ljava/lang/String;Ljava/lang/String;I)V

以下是p命名法参数对应表:

p0this
p1第一个参数Ljava/lang/String;
p2第二个参数Ljava/lang/String;
p3第三个参数I

如前面提到的,long和double类型都是64位,需要2个寄存器。当你引用参数的时候一定要记住,例如:你有一个非静态方法

LMyObject;->MyMethod(IJZ)V

方法的参数为int、long、bool。所以这个方法的所有参数需要5个寄存器。

p0this
p1I
p2, p3J
p4Z

另外当你调用方法后,你必须在寄存器列表,调用指令中指明,两个寄存器保存了double-wide宽度的参数。

注意:在默认的baksmali中,参数寄存器将使用P命名方式,如果出于某种原因你要禁用P命名方式,而要强制使用V命名方式,应当使用-p/--no-parameter-registers选项。

总结

  • locals和registers都可以表示寄存器数量,locals指定本地局部变量寄存器个数,registers是locals和参数寄存器数量的总数,两者使用任选其一
  • 同时,寄存器命名一共分两种,一种是v命名法,另一种是p命名法
v0the first local register
v1the second local register
v2p0the first parameter register
v3p1the second parameter register
v4p2the third parameter register

收起阅读 »

Swift 数组

iOS
Swift 数组Swift 数组使用有序列表存储同一类型的多个值。相同的值可以多次出现在一个数组的不同位置中。Swift 数组会强制检测元素的类型,如果类型不同则会报错,Swift 数组应该遵循像Array<Element>这样的形式,其中Elem...
继续阅读 »

Swift 数组

Swift 数组使用有序列表存储同一类型的多个值。相同的值可以多次出现在一个数组的不同位置中。

Swift 数组会强制检测元素的类型,如果类型不同则会报错,Swift 数组应该遵循像Array<Element>这样的形式,其中Element是这个数组中唯一允许存在的数据类型。

如果创建一个数组,并赋值给一个变量,则创建的集合就是可以修改的。这意味着在创建数组后,可以通过添加、删除、修改的方式改变数组里的项目。如果将一个数组赋值给常量,数组就不可更改,并且数组的大小和内容都不可以修改。


创建数组

我们可以使用构造语法来创建一个由特定数据类型构成的空数组:

var someArray = [SomeType]()

以下是创建一个初始化大小数组的语法:

var someArray = [SomeType](repeating: InitialValue, count: NumbeOfElements)

以下实例创建了一个类型为 Int ,数量为 3,初始值为 0 的空数组:

var someInts = [Int](repeating: 0, count: 3)

以下实例创建了含有三个元素的数组:

var someInts:[Int] = [10, 20, 30]

访问数组

我们可以根据数组的索引来访问数组的元素,语法如下:

var someVar = someArray[index]

index 索引从 0 开始,即索引 0 对应第一个元素,索引 1 对应第二个元素,以此类推。

我们可以通过以下实例来学习如何创建,初始化,访问数组:

import Cocoa

var someInts = [Int](repeating: 10, count: 3)

var someVar = someInts[0]

print( "第一个元素的值 \(someVar)" )
print( "第二个元素的值 \(someInts[1])" )
print( "第三个元素的值 \(someInts[2])" )

以上程序执行输出结果为:

第一个元素的值 10
第二个元素的值 10
第三个元素的值 10

修改数组

你可以使用 append() 方法或者赋值运算符 += 在数组末尾添加元素,如下所示,我们初始化一个数组,并向其添加元素:

import Cocoa

var someInts = [Int]()

someInts
.append(20)
someInts
.append(30)
someInts
+= [40]

var someVar = someInts[0]

print( "第一个元素的值 \(someVar)" )
print( "第二个元素的值 \(someInts[1])" )
print( "第三个元素的值 \(someInts[2])" )

以上程序执行输出结果为:

第一个元素的值 20
第二个元素的值 30
第三个元素的值 40

我们也可以通过索引修改数组元素的值:

import Cocoa

var someInts = [Int]()

someInts
.append(20)
someInts
.append(30)
someInts
+= [40]

// 修改最后一个元素
someInts
[2] = 50

var someVar = someInts[0]

print( "第一个元素的值 \(someVar)" )
print( "第二个元素的值 \(someInts[1])" )
print( "第三个元素的值 \(someInts[2])" )

以上程序执行输出结果为:

第一个元素的值 20
第二个元素的值 30
第三个元素的值 50

遍历数组

我们可以使用for-in循环来遍历所有数组中的数据项:

import Cocoa

var someStrs = [String]()

someStrs
.append("Apple")
someStrs
.append("Amazon")
someStrs
.append("Runoob")
someStrs
+= ["Google"]

for item in someStrs {
print(item)
}

以上程序执行输出结果为:

Apple
Amazon
Runoob
Google

如果我们同时需要每个数据项的值和索引值,可以使用 String 的 enumerate() 方法来进行数组遍历。实例如下:

import Cocoa

var someStrs = [String]()

someStrs
.append("Apple")
someStrs
.append("Amazon")
someStrs
.append("Runoob")
someStrs
+= ["Google"]

for (index, item) in someStrs.enumerated() {
print("在 index = \(index) 位置上的值为 \(item)")
}

以上