注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

当面试官问Webpack的时候他想知道什么

前言 在前端工程化日趋复杂的今天,模块打包工具在我们的开发中起到了越来越重要的作用,其中webpack就是最热门的打包工具之一。 说到webpack,可能很多小伙伴会觉得既熟悉又陌生,熟悉是因为几乎在每一个项目中我们都会用上它,又因为webpack复杂的配置和...
继续阅读 »

前言


在前端工程化日趋复杂的今天,模块打包工具在我们的开发中起到了越来越重要的作用,其中webpack就是最热门的打包工具之一。


说到webpack,可能很多小伙伴会觉得既熟悉又陌生,熟悉是因为几乎在每一个项目中我们都会用上它,又因为webpack复杂的配置和五花八门的功能感到陌生。尤其当我们使用诸如umi.js之类的应用框架还帮我们把webpack配置再封装一层的时候,webpack的本质似乎离我们更加遥远和深不可测了。


当面试官问你是否了解webpack的时候,或许你可以说出一串耳熟能详的webpack loaderplugin的名字,甚至还能说出插件和一系列配置做按需加载和打包优化,那你是否了解他的运行机制以及实现原理呢,那我们今天就一起探索webpack的能力边界,尝试了解webpack的一些实现流程和原理,拒做API工程师。


CgqCHl6pSFmAC5UzAAEwx63IBwE024.png


你知道webpack的作用是什么吗?


从官网上的描述我们其实不难理解,webpack的作用其实有以下几点:




  • 模块打包。可以将不同模块的文件打包整合在一起,并且保证它们之间的引用正确,执行有序。利用打包我们就可以在开发的时候根据我们自己的业务自由划分文件模块,保证项目结构的清晰和可读性。




  • 编译兼容。在前端的“上古时期”,手写一堆浏览器兼容代码一直是令前端工程师头皮发麻的事情,而在今天这个问题被大大的弱化了,通过webpackLoader机制,不仅仅可以帮助我们对代码做polyfill,还可以编译转换诸如.less, .vue, .jsx这类在浏览器无法识别的格式文件,让我们在开发的时候可以使用新特性和新语法做开发,提高开发效率。




  • 能力扩展。通过webpackPlugin机制,我们在实现模块化打包和编译兼容的基础上,可以进一步实现诸如按需加载,代码压缩等一系列功能,帮助我们进一步提高自动化程度,工程效率以及打包输出的质量。




说一下模块打包运行原理?


如果面试官问你Webpack是如何把这些模块合并到一起,并且保证其正常工作的,你是否了解呢?


首先我们应该简单了解一下webpack的整个打包流程:



  • 1、读取webpack的配置参数;

  • 2、启动webpack,创建Compiler对象并开始解析项目;

  • 3、从入口文件(entry)开始解析,并且找到其导入的依赖模块,递归遍历分析,形成依赖关系树;

  • 4、对不同文件类型的依赖模块文件使用对应的Loader进行编译,最终转为Javascript文件;

  • 5、整个过程中webpack会通过发布订阅模式,向外抛出一些hooks,而webpack的插件即可通过监听这些关键的事件节点,执行插件任务进而达到干预输出结果的目的。


其中文件的解析与构建是一个比较复杂的过程,在webpack源码中主要依赖于compilercompilation两个核心对象实现。


compiler对象是一个全局单例,他负责把控整个webpack打包的构建流程。
compilation对象是每一次构建的上下文对象,它包含了当次构建所需要的所有信息,每次热更新和重新构建,compiler都会重新生成一个新的compilation对象,负责此次更新的构建过程。


而每个模块间的依赖关系,则依赖于AST语法树。每个模块文件在通过Loader解析完成之后,会通过acorn库生成模块代码的AST语法树,通过语法树就可以分析这个模块是否还有依赖的模块,进而继续循环执行下一个模块的编译解析。


最终Webpack打包出来的bundle文件是一个IIFE的执行函数。


// webpack 5 打包的bundle文件内容

(() => { // webpackBootstrap
var __webpack_modules__ = ({
'file-A-path': ((modules) => { // ... })
'index-file-path': ((__unused_webpack_module, __unused_webpack_exports, __webpack_require__) => { // ... })
})

// The module cache
var __webpack_module_cache__ = {};

// The require function
function __webpack_require__(moduleId) {
// Check if module is in cache
var cachedModule = __webpack_module_cache__[moduleId];
if (cachedModule !== undefined) {
return cachedModule.exports;
}
// Create a new module (and put it into the cache)
var module = __webpack_module_cache__[moduleId] = {
// no module.id needed
// no module.loaded needed
exports: {}
};

// Execute the module function
__webpack_modules__[moduleId](module, module.exports, __webpack_require__);

// Return the exports of the module
return module.exports;
}

// startup
// Load entry module and return exports
// This entry module can't be inlined because the eval devtool is used.
var __webpack_exports__ = __webpack_require__("./src/index.js");
})

webpack4相比,webpack5打包出来的bundle做了相当的精简。在上面的打包demo中,整个立即执行函数里边只有三个变量和一个函数方法,__webpack_modules__存放了编译后的各个文件模块的JS内容,__webpack_module_cache__ 用来做模块缓存,__webpack_require__Webpack内部实现的一套依赖引入函数。最后一句则是代码运行的起点,从入口文件开始,启动整个项目。


其中值得一提的是__webpack_require__模块引入函数,我们在模块化开发的时候,通常会使用ES Module或者CommonJS规范导出/引入依赖模块,webpack打包编译的时候,会统一替换成自己的__webpack_require__来实现模块的引入和导出,从而实现模块缓存机制,以及抹平不同模块规范之间的一些差异性。


你知道sourceMap是什么吗?


提到sourceMap,很多小伙伴可能会立刻想到Webpack配置里边的devtool参数,以及对应的evaleval-cheap-source-map等等可选值以及它们的含义。除了知道不同参数之间的区别以及性能上的差异外,我们也可以一起了解一下sourceMap的实现方式。


sourceMap是一项将编译、打包、压缩后的代码映射回源代码的技术,由于打包压缩后的代码并没有阅读性可言,一旦在开发中报错或者遇到问题,直接在混淆代码中debug问题会带来非常糟糕的体验,sourceMap可以帮助我们快速定位到源代码的位置,提高我们的开发效率。sourceMap其实并不是Webpack特有的功能,而是Webpack支持sourceMap,像JQuery也支持souceMap


既然是一种源码的映射,那必然就需要有一份映射的文件,来标记混淆代码里对应的源码的位置,通常这份映射文件以.map结尾,里边的数据结构大概长这样:


{
"version" : 3, // Source Map版本
"file": "out.js", // 输出文件(可选)
"sourceRoot": "", // 源文件根目录(可选)
"sources": ["foo.js", "bar.js"], // 源文件列表
"sourcesContent": [null, null], // 源内容列表(可选,和源文件列表顺序一致)
"names": ["src", "maps", "are", "fun"], // mappings使用的符号名称列表
"mappings": "A,AAAB;;ABCDE;" // 带有编码映射数据的字符串
}

其中mappings数据有如下规则:



  • 生成文件中的一行的每个组用“;”分隔;

  • 每一段用“,”分隔;

  • 每个段由1、4或5个可变长度字段组成;


有了这份映射文件,我们只需要在我们的压缩代码的最末端加上这句注释,即可让sourceMap生效:


//# sourceURL=/path/to/file.js.map

有了这段注释后,浏览器就会通过sourceURL去获取这份映射文件,通过解释器解析后,实现源码和混淆代码之间的映射。因此sourceMap其实也是一项需要浏览器支持的技术。


如果我们仔细查看webpack打包出来的bundle文件,就可以发现在默认的development开发模式下,每个_webpack_modules__文件模块的代码最末端,都会加上//# sourceURL=webpack://file-path?,从而实现对sourceMap的支持。


sourceMap映射表的生成有一套较为复杂的规则,有兴趣的小伙伴可以看看以下文章,帮助理解soucrMap的原理实现:


Source Map的原理探究


Source Maps under the hood – VLQ, Base64 and Yoda


是否写过Loader?简单描述一下编写loader的思路?


从上面的打包代码我们其实可以知道,Webpack最后打包出来的成果是一份Javascript代码,实际上在Webpack内部默认也只能够处理JS模块代码,在打包过程中,会默认把所有遇到的文件都当作 JavaScript代码进行解析,因此当项目存在非JS类型文件时,我们需要先对其进行必要的转换,才能继续执行打包任务,这也是Loader机制存在的意义。


Loader的配置使用我们应该已经非常的熟悉:


// webpack.config.js
module.exports = {
// ...other config
module: {
rules: [
{
test: /^your-regExp$/,
use: [
{
loader: 'loader-name-A',
},
{
loader: 'loader-name-B',
}
]
},
]
}
}

通过配置可以看出,针对每个文件类型,loader是支持以数组的形式配置多个的,因此当Webpack在转换该文件类型的时候,会按顺序链式调用每一个loader,前一个loader返回的内容会作为下一个loader的入参。因此loader的开发需要遵循一些规范,比如返回值必须是标准的JS代码字符串,以保证下一个loader能够正常工作,同时在开发上需要严格遵循“单一职责”,只关心loader的输出以及对应的输出。


loader函数中的this上下文由webpack提供,可以通过this对象提供的相关属性,获取当前loader需要的各种信息数据,事实上,这个this指向了一个叫loaderContextloader-runner特有对象。有兴趣的小伙伴可以自行阅读源码。


module.exports = function(source) {
const content = doSomeThing2JsString(source);

// 如果 loader 配置了 options 对象,那么this.query将指向 options
const options = this.query;

// 可以用作解析其他模块路径的上下文
console.log('this.context');

/*
* this.callback 参数:
* error:Error | null,当 loader 出错时向外抛出一个 error
* content:String | Buffer,经过 loader 编译后需要导出的内容
* sourceMap:为方便调试生成的编译后内容的 source map
* ast:本次编译生成的 AST 静态语法树,之后执行的 loader 可以直接使用这个 AST,进而省去重复生成 AST 的过程
*/
this.callback(null, content);
// or return content;
}

更详细的开发文档可以直接查看官网的 Loader API


是否写过Plugin?简单描述一下编写plugin的思路?


如果说Loader负责文件转换,那么Plugin便是负责功能扩展。LoaderPlugin作为Webpack的两个重要组成部分,承担着两部分不同的职责。


上文已经说过,webpack基于发布订阅模式,在运行的生命周期中会广播出许多事件,插件通过监听这些事件,就可以在特定的阶段执行自己的插件任务,从而实现自己想要的功能。


既然基于发布订阅模式,那么知道Webpack到底提供了哪些事件钩子供插件开发者使用是非常重要的,上文提到过compilercompilationWebpack两个非常核心的对象,其中compiler暴露了和 Webpack整个生命周期相关的钩子(compiler-hooks),而compilation则暴露了与模块和依赖有关的粒度更小的事件钩子(Compilation Hooks)。


Webpack的事件机制基于webpack自己实现的一套Tapable事件流方案(github


// Tapable的简单使用
const { SyncHook } = require("tapable");

class Car {
constructor() {
// 在this.hooks中定义所有的钩子事件
this.hooks = {
accelerate: new SyncHook(["newSpeed"]),
brake: new SyncHook(),
calculateRoutes: new AsyncParallelHook(["source", "target", "routesList"])
};
}

/* ... */
}


const myCar = new Car();
// 通过调用tap方法即可增加一个消费者,订阅对应的钩子事件了
myCar.hooks.brake.tap("WarningLampPlugin", () => warningLamp.on());

Plugin的开发和开发Loader一样,需要遵循一些开发上的规范和原则:



  • 插件必须是一个函数或者是一个包含 apply 方法的对象,这样才能访问compiler实例;

  • 传给每个插件的 compilercompilation 对象都是同一个引用,若在一个插件中修改了它们身上的属性,会影响后面的插件;

  • 异步的事件需要在插件处理完任务时调用回调函数通知 Webpack 进入下一个流程,不然会卡住;


了解了以上这些内容,想要开发一个 Webpack Plugin,其实也并不困难。


class MyPlugin {
apply (compiler) {
// 找到合适的事件钩子,实现自己的插件功能
compiler.hooks.emit.tap('MyPlugin', compilation => {
// compilation: 当前打包构建流程的上下文
console.log(compilation);

// do something...
})
}
}

更详细的开发文档可以直接查看官网的 Plugin API


最后


本文也是结合一些优秀的文章和webpack本身的源码,大概地说了几个相对重要的概念和流程,其中的实现细节和设计思路还需要结合源码去阅读和慢慢理解。


Webpack作为一款优秀的打包工具,它改变了传统前端的开发模式,是现代化前端开发的基石。这样一个优秀的开源项目有许多优秀的设计思想和理念可以借鉴,我们自然也不应该仅仅停留在API的使用层面,尝试带着问题阅读源码,理解实现的流程和原理,也能让我们学到更多知识,理解得更加深刻,在项目中才能游刃有余的应用。




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

收起阅读 »

面试官,我实现了一个 Chrome Devtools

网页会加载资源、运行 JS、渲染界面、存储数据等,我们开发时怎么看到执行的状态呢? 用调试工具 chrome devtools。它支持 dom 调试、JS debugger、本地存储的展示、运行时间的 profile 等。 Node.js 也是同样,不过它只支...
继续阅读 »

网页会加载资源、运行 JS、渲染界面、存储数据等,我们开发时怎么看到执行的状态呢? 用调试工具 chrome devtools。它支持 dom 调试、JS debugger、本地存储的展示、运行时间的 profile 等。


Node.js 也是同样,不过它只支持 JS debugger 和 profile。我们可以通过 chrome devtools 或者 vscode debugger 等来调试。


这些工具都是远程 attach 到运行的程序上来调试的,之间怎么交互数据呢? 通过 webSocket。而且还制定了 chrome devtools protocol 的协议,规定了有什么能力,如何通信。


这种基于 websocket 的调试协议叫做 chrome devtools protocol
因为功能比较多,所以分了多个域(一般复杂的东西都会分域),包括 DOM、Debugger、Network、Page 等等,分别放不同的调试协议。chrome devtools 就是通过这个协议实现的调试。


新版 chrome(金丝雀版)可以打开设置中的实验特性的 Protocol Monitor 面板。



就可以看到传输的 CDP 数据:



这就是 chrome devtools 的原理。


理解了这个原理有什么用呢?


我们可以重新实现服务端,只要对接了调试协议,那么就能够用 chrome devtools 来调试。


比如 kraken(把 css 渲染到 flutter)是怎么做到用 chrome devtools 调试 dom 和样式的?就是对接了这个协议。


我们可以重新实现客户端,只要对接了这个协议,那就可以用任何工具调试网页/Node.js。


大家用 chrome devtools 可以调试 Node.js 和网页,用 vscode debugger 也可以,用 webstorm debugger 也可以。为什么呢?因为它们都对接了这个协议。


那我们是不是可以对接这个协议实现一个类似 chrome devtools 的调试工具呢?


我们来实验下:


我们启动 chrome,通过 --remote-debugging-port 指定调试端口:


/Applications/Google\ Chrome\ Canary.app/Contents/MacOS/Google\ Chrome\ Canary --remote-debugging-port=9222

然后连上它。


这里我们不用直接对接协议,chrome 提供了各种语言的 sdk,调用 api 就行:



我们先连接上 chrome:


const CDP = require('chrome-remote-interface');

async function test() {
let client;
try {
client = await CDP();
const { Page, DOM, Debugger } = client;
//...
} catch(err) {
console.error(err);
}
}
test();

然后打开 baidu.com,等 2s,做个截图:


const CDP = require('chrome-remote-interface');
const fs = require('fs');

async function test() {
let client;
try {
client = await CDP();
const { Page, DOM, Debugger } = client;

await Page.enable();
await Page.navigate({url: 'https://baidu.com'});

await new Promise(resolve => setTimeout(resolve, 2000));

const res = await Page.captureScreenshot();
fs.writeFileSync('./screenshot.jpg', res.data, {
encoding: 'base64'
});
} catch(err) {
console.error(err);
}
}
test();

查看下效果:



这样,我们就跑通了 CDP 的第一段代码。


其余的功能,包括 Network、Debugger、DOM 等等也能实现,我们简单试一下:


await DOM.enable();

const { root } = await DOM.getDocument({
depth: -1
});

depth 为深度,设置为 -1 就是返回整个 dom:



有了这些数据我们是不是可以做 DOM 的浏览呢?


还有 DOM.setAttributeValue 可以设置属性、DOM.getBoxModel 拿到盒模型大小等。


基于这些,我们做个 dom 编辑器没问题吧。


还有网络部分:


await Network.enable();
Network.on('responseReceived', async evt => {
const res = await Network.getResponseBody({
requestId: evt.requestId
});

console.log(evt.response.url);
console.log(res.body);
});

我们通过 responseReceived 事件监听每一个响应,然后再通过 Network.getResponseBody 拿到响应的内容:




基于这些,我们实现 Network 面板的功能没问题吧。


还可以对接 profiler:


await Profiler.start();
await new Promise(resolve => setTimeout(resolve,2000));
const { profile } = await Profiler.stop();


有这些数据,我们就可以通过 canvas 画出个火焰图出来。


理论上来说,chrome devtools 的所有功能我们都能实现。而且,一个网页同时用多个调试工具调试是可以的,因为 websocket 本来就可以有多个客户端。



可能你会说自己实现 chrome devtools 有什么意义?


大家自己做开源前端项目的时候,一般都是写个网易云音乐客户端,因为有现成的数据可以用。那为什么不做个 chrome devtools 呢?也有现成的数据啊,启动浏览器就行,而且这个逼格多高啊。


我们也不用实现完整的 chrome devtools,可以单把网络部分、单把 DOM 部分、单把 debugger 部分实现了,可以做不同的 UI,可以做 chrome devtools 没有的功能和交互。


比如你面试可视化岗位,你说你对接了 chrome devtools protocol 的 profiler 部分,用 canvas 画了个火焰图,会加分很多的。


总结


Chrome 的调试是通过 WebSocket 和调试客户端通信,制定了 Chrome Devtools Protocol 的协议,Node.js 也是,不过协议叫做 V8 debugger protocol。我们可以通过 protocol monitor 的面板看到所有的 CDP 协议请求响应。


可以实现 CDP 服务端,来对接 chrome devtools 的调试功能,调试不同的目标,比如 kraken 渲染引擎。


可以实现 CDP 客户端,来用不同的工具调试,比如 vscode debugger、webstorm debugger 等。


我们也可以通过 sdk 的 api 来和 CDP 服务端对接,拿到数据,实现调试的功能。比如单独实现 DOM 编辑器、Network 查看器、JS Debugger、 Profiler 和火焰图都可以,而且可以做到比 chrome devtools 更强的功能,更好的交互。


当大家想做开源项目没有数据的时候,不妨考虑下做个 CDP 客户端,这不比云音乐项目香么?


作者:zxg_神说要有光
链接:https://juejin.cn/post/7012853971362512933
收起阅读 »

10分钟教你自动化部署前端项目

背景 前几年,作为小白的我,只需要安安心心写前端代码就行,前端代码部署的事情直接交给运维人员去部署,部署到哪台服务器,这些都不需要我们关心。 突然,运维人员离职了,没办法,业务需要又起了一个项目,没有运维人员的我们只能每次本地编译完毕,然后手动同步到服务器目录...
继续阅读 »

背景


前几年,作为小白的我,只需要安安心心写前端代码就行,前端代码部署的事情直接交给运维人员去部署,部署到哪台服务器,这些都不需要我们关心。


突然,运维人员离职了,没办法,业务需要又起了一个项目,没有运维人员的我们只能每次本地编译完毕,然后手动同步到服务器目录,累成个狗了!累就要想办法解决。


GitLab CI


我们的项目中经常有一个.yml文件,有很多人不知道这个文件是干什么用的。


GitLab CI/CD(后简称 GitLab CI)是一套基于 GitLab 的 CI/CD 系统,可以让开发人员通过 .gitlab-ci.yml 在项目中配置 CI/CD 流程,在提交后,系统可以自动/手动地执行任务,完成 CI/CD 操作。


说的通俗一点,就是这个yml文件的作用是我们提交代码到GitLab以后,GitLab CI会自动读取yml文件中的内容,进行对应的操作。gitlab只是代码仓库,想要实现CI/CD,我们需要安装gitlab-runner。gitlab-runner相当于任务执行器。


下载安装


例如我们在测试服务器(9.138)上安装git-runner


首先我们下载安装git-runner


# 下载 
sudo curl -L --output /usr/local/bin/gitlab-runner https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-linux-amd64
# 给予执行权限
sudo chmod +x /usr/local/bin/gitlab-runner
# 创建一个CI用户
sudo useradd --comment 'GitLab Runner' --create-home gitlab-runner --shell /bin/bash
# 安装并且运行服务
sudo gitlab-runner install --user=gitlab-runner --working-directory=/home/gitlab-runner
sudo gitlab-runner start

注册runner


image.png


gitLab查看runner


image.png


使用注册好的runner


编写.gitlab-ci.yml文件,先了解一些基本概念


1. Pipeline


一次 Pipeline 其实相当于一次构建任务,里面可以包含多个流程,如安装依赖、运行测试、编译、部署测试服务器、部署生产服务器等流程。 任何提交或者 Merge Request 的合并都可以触发 Pipeline,如下图所示:


image.png


2. Stages


Stages 表示构建阶段,说白了就是上面提到的流程。 我们可以在一次 Pipeline 中定义多个 Stages,这些 Stages 会有以下特点:



  • 所有 Stages 会按照顺序运行,即当一个 Stage 完成后,下一个 Stage 才会开始

  • 只有当所有 Stages 完成后,该构建任务 (Pipeline) 才会成功

  • 如果任何一个 Stage 失败,那么后面的 Stages 不会执行,该构建任务 (Pipeline) 失败


因此,Stages 和 Pipeline 的关系就是:


image.png


2. Jobs


Jobs 表示构建工作,表示某个 Stage 里面执行的工作。 我们可以在 Stages 里面定义多个 Jobs,这些 Jobs 会有以下特点:



  • 相同 Stage 中的 Jobs 会并行执行

  • 相同 Stage 中的 Jobs 都执行成功时,该 Stage 才会成功

  • 如果任何一个 Job 失败,那么该 Stage 失败,即该构建任务 (Pipeline) 失败


所以,Jobs 和 Stage 的关系图就是:


image.png


下面我们来写一个简单的.gitlab-ci.yml


image.png


注意


安装git-runner的服务器需要安装git,不然你可能看到一下错误:


image.png


后语


欢迎大家多提意见。项目模板在不断优化,一赞一回!欢迎评论。


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

收起阅读 »

让小脚本帮你做哪些枯燥无味的git提交吧

睡前小故事 在某次开会的时候,组长说每次做完一个小需求和修改一个bug都要提交一个commit,下班前要将本地分支推送到远程仓库。没commit的,没push的都扣当月的绩效...,虽然觉得这样很烦躁,但是谁让自己是打工崽呢,又不敢说什么,只能照着做了,还能...
继续阅读 »

睡前小故事



在某次开会的时候,组长说每次做完一个小需求和修改一个bug都要提交一个commit,下班前要将本地分支推送到远程仓库。没commit的,没push的都扣当月的绩效...,虽然觉得这样很烦躁,但是谁让自己是打工崽呢,又不敢说什么,只能照着做了,还能怎么办。之后就是每天不停的git status, git add . ,git commit -m 'xxxxx'...有时bug多的时候,一天输入几十次这样的命令,终于有一天我受不了,实在太没劲了(也许是自己懒吧),git,git,git的,代码还写不写啊。然后就想能不能输一个命令自动完成这几个步骤,接着就诞生了这个小脚本。



脚本的实现



脚本用shell写的



判断有没有安装git


if(!shell.which('git')) {
shell.echo('你还没安装git,请先安装git')
shell.exit(1)
}

判断有没有文件变动


if(shell.exec('git status').stdout.indexOf('working tree clean') !== -1) {
shell.echo('没有变动文件')
shell.exit(1)
}

查看哪些文件变动了


if(shell.exec('git status').code !== 0) {
shell.echo('git status执行出错')
shell.exit(1)
}

添加文件的追踪


if(shell.exec('git add .').code !== 0) {
shell.echo('git add执行出错')
shell.exit(1)
}

提交本地仓库


let intro = process.argv[2]

if(!intro) {
shell.echo('请填写提交信息,格式为feat(xxxx):xxxxx')
shell.exit(1)
}
if(shell.exec(`git commit -m ${intro}`).code !== 0) {
shell.echo('git commit执行出错')
shell.exit(1)
}

推送远程仓库


let BranchName = shell.exec('git rev-parse --abbrev-ref HEAD')
// 因为我7点下班,七点后commit的都会推送到远程
if(new Date().getHours() >= 19) {
if(shell.exec(`git push origin ${BranchName}`).code !== 0) {
shell.echo('推送远程失败')
shell.exit(1)
}
shell.echo('推送远程完成')
}

完整代码


let shell = require('shelljs')

if(!shell.which('git')) {
shell.echo('你还没安装git,请先安装git')
shell.exit(1)
}

shell.echo('查看哪些文件变动')

if(shell.exec('git status').stdout.indexOf('working tree clean') !== -1) {
shell.echo('没有变动文件')
shell.exit(1)
}

if(shell.exec('git status').code !== 0) {
shell.echo('git status执行出错')
shell.exit(1)
}

shell.echo('开始添加新文件追踪')

if(shell.exec('git add .').code !== 0) {
shell.echo('git add执行出错')
shell.exit(1)
}

let intro = process.argv[2]

if(!intro) {
shell.echo('请填写提交信息,格式为feat(xxxx):xxxxx')
shell.exit(1)
}

if(shell.exec(`git commit -m ${intro}`).code !== 0) {
shell.echo('git commit执行出错')
shell.exit(1)
}

let BranchName = shell.exec('git rev-parse --abbrev-ref HEAD')

shell.echo(`代码提交到本地完成,当前分支是${BranchName}`)

if(new Date().getHours() >= 19) {
if(shell.exec(`git push origin ${BranchName}`).code !== 0) {
shell.echo('推送远程失败')
shell.exit(1)
}
shell.echo('推送远程完成')
}

使用


 node git-commit.js(写上你脚本的文件) 'feat(xxx):xxxxx'(你要commit的描述)

效果


没变动


image.png


有变动


image.png


总结


程序猿懒就对。


作者:EasyMoment23
链接:https://juejin.cn/post/7012518849543487495

收起阅读 »

关于compute你不知道的骚操作

compute的的用法 首先你要知道computed的用法,在官网中只是简单的介绍了它的缓存作用,根据双向绑定的数据通过计算返回值,当被依赖的值发生改变的时候就会触发重新计算 很多人 就只知道这个只能用来计算值并不能作为其他的用法,其实根据它依赖双向绑定的数...
继续阅读 »

compute的的用法


首先你要知道computed的用法,在官网中只是简单的介绍了它的缓存作用,根据双向绑定的数据通过计算返回值,当被依赖的值发生改变的时候就会触发重新计算


image.png


很多人 就只知道这个只能用来计算值并不能作为其他的用法,其实根据它依赖双向绑定的数据可以监听多个数据,这句话怎么理解呢?就是我们都知道watch只能监听一个数据对象,但是有时候我们要监听多个对象的时候就要写很多个watch方法,而且会触发很多次,很多时候我们要监听的数据并不是1个,而且我们监听的是异步的,这个时候就可以用computed去解决。如果你看到这里还不明白什么意思,你可以看看下面的例子。


image.png


image.png


这里我监听了2个双向绑定的数据 一个是testData,一个是test3,然而test3 会在挂载之后的下一次更新发生改变,当这个改变发生的时候 会再次触发computed的方法,这个时候就代表我监听到了这个变化了,反过来想我是不是监听到了2个双向绑定的数据,那么结果就是 监听单个数据的变化 可以用watch,监听多个数据变化可以考虑computed,有人觉得我另一个数据没有改变,这也是只是监听了一个数据,我改了下代码。让另一个数据也发生改变。


image.png


image.png
可以看到这里触发了3次,为啥有3次 我解释下,computed本身是处于created 跟mounted的生命周期之间会自己去调用的,这是第一次,mounted里面的$nextTick方法里面test3改变的时候触发了一次,setTimeout里面testData改变的时候 就是第三次了。也就是说它监听的双向绑定数据改变的时候都会触发 ,这样子就可以达到监听多个数据的目的了。
适用的场景有 表单校验的时候,所有数据填完按钮点亮 这个时候就可以监听多个,还有就是多个异步返回的数据同时满足条件的时候可以监听。


有这么一个场景 ,一个列表是个数组会增加,高亮的地方(也就是下标会改变),只要一个改变就执行操作,你们会用什么去监听呢?


最后抛个问题


image.png


作者:Try to do
链接:https://juejin.cn/post/7012543084923715591

收起阅读 »

别再用generator模拟async啦,它还有很酷的用法

前一阵在某个技术群里发现有人在讨论JavaScript的generator,不少人一提generator就会把它跟异步联系在一起,我认为这是一个很深的误解。 generator 究竟跟异步是什么关系?以co为代表的一批早期框架用它来模拟async/await,...
继续阅读 »

前一阵在某个技术群里发现有人在讨论JavaScript的generator,不少人一提generator就会把它跟异步联系在一起,我认为这是一个很深的误解。


generator 究竟跟异步是什么关系?以co为代表的一批早期框架用它来模拟async/await,但是这是否能说明 generator 与异步相关呢?我认为答案是否定的,co中用到的语言特性很多,if、函数、变量……而 generator 只是其中之一罢了。


generator 的确是实现模拟async/await的一个关键语言特性,但是,正确的因果关系是 generator 和 async/await 共用了一个JS的底层设施:函数暂停执行并保留当时执行环境。


在我的观念中,generator 的应用前景远远比 async/await 更为广阔。generator 代表了一种"无穷序列"的抽象,或者说不定长序列的抽象,这个抽象可以为我们带来编程思路上的突破。


在非常前卫的函数式语言Haskell的官网首页,有这样一段代码:


primes = filterPrime [2..]
where filterPrime (p:xs) =
p : filterPrime [x | x <- xs, x `mod` p /= 0]

这是一段 Haskell 程序员引以为傲的代码,它的作用是生成一个质数序列,在多数语言中,都很难复刻这个逻辑结构。其中,最关键的一点就是它应用了延迟计算和无穷列表的概念。


我们试着分析这段 Haskell 代码的逻辑,[2..]表示一个从2开始到无穷的整数序列, filterPrime是一个函数,对这个整数序列做了过滤,函数具体内容则由后面的where指定。所以,能够把整数序列变成质数序列的关键代码就是 filterPrime。那么,它究竟做了什么呢?


这段代码简短得不可思议,首先我们来看参数部分,p:xs 是解构赋值的形参,它表示,把输入中的列表第一个元素赋值为p,剩余部分,赋值为xs。


第一次调用 filterPrime 实参为[2..],此时,p的值就是2,而xs则是无尽列表[3..]


那么,filterPrime 是如何将 p 和 xs 过滤成质数列表的呢?我们来看这段代码:


[x | x <- xs, x `mod\` p /= 0]`

这段大概的意思,可以用一段适合JS程序员理解的伪代码来解释:


xs.filter(x => x % p !== 0)

就是从列表 xs 中,过滤 p 的倍数。当然了,xs 并不是 JavaScript 原生数组,所以它并没有方便的filter方法。


那么,接下来,这个过滤好的数组传递给 filterPrime 递归就很有意思了,此时 xs 中已经被过滤掉了 p 的倍数,剩下的第一个数就必定是质数了,我们继续用 filterPrime 递归过滤其第一个元素的倍数,就可以继续找到下一个质数。


最后,代码p : 表示将 p 拼接到列表的第一个。


那么,在 JavaScript 中,是否能复刻这样的编程思想呢?


答案当然是可以,其关键正是 generator。


首先我们要解决的问题就是[2..],这是一个无尽列表,JavaScript中不支持无尽列表,但是我们可以用 generator 来表示,其代码如下:


function *integerRange(from, to){
for(let i = from; i < to; i++){
yield i;
}
}

接下来,数组的filter并不能够很好地作用于无尽列表,所以我们需要一个针对无尽列表的filter函数,其代码如下:



function *filter(iter, condition) {
for(let v of iter) {
if(condition(v)) {
yield v;
}
}
}

最后是我们的重头戏 filterPrime 啦,只要读懂了Haskell,这算不上困难,实现代码如下:


function* filterPrime(iter) {
let p = iter.next().value;
let rest = iter;

yield p;
for(let v of filterPrime(filter(iter, x => x % p != 0)))
yield v;
}

代码写好了,我们可以用JavaScript中独有的异步能力,来输出这个质数序列看看:


function sleep(d){
return new Promise(resolve => setTimeout(resolve, d));
}
void async function(){
for(let v of filterPrime(integerRange(2, Infinity))){
await sleep(1000);
console.log(v);
}
}();

好啦,虽然语法噪声稍微有点多,但是到此为止我们就实现了跟 Haskell 一样的质数序列算法。


除了无尽列表,generator 也很适合包装一些API,表达“不定项的列表”这样的概念。比如,我们可以对正则表达式的exec做一些包装,使它变成一个 generator。


function* execRegExp(regexp, string) {
let r = null;
while(r = regexp.exec(string)) {
yield r;
}
}

使用的时候,我们就可以用 for...of 结构了。下面代码展示了一个简单的词法分析写法。


let tokens = execRegExp(/let|var|\s+|[a-zA-Z$][0-9a-zA-Z$]*|[1-9][0-9]*|\+|-|\*|\/|;|=/g, "let a = 1 + 2;")
for(let s of tokens){
console.log(s);
}

这样的API设计,是不是比原来更简洁优美呢?


你看 generator 是一个潜力如此之大的语言特性,它为 JavaScripter 们打开了通往"无尽"数学概念的大门。所以,别再想着拿它模拟异步啦,希望看过本文,你能获得一点灵感,把 generator 用到开源项目或者生产中的 API 设计,谢谢观赏。


作者:winter
链接:https://juejin.cn/post/7012596693271052325

收起阅读 »

【环信IM集成指南】Web端常见问题整理

1. 掉线之后,会有回调或通知吗?sdk有提供连接状态的回调监听https://docs-im.easemob.com/im/web/intro/basic#%E6%B7%BB%E5%8A%A0%E5%9B%9E%E8%B0%83%E5%87%BD%E6%95...
继续阅读 »

1. 掉线之后,会有回调或通知吗?

sdk有提供连接状态的回调监听

https://docs-im.easemob.com/im/web/intro/basic#%E6%B7%BB%E5%8A%A0%E5%9B%9E%E8%B0%83%E5%87%BD%E6%95%B0


2. 怎么转发多条聊天记录?

挑选你要转发的消息内容然后以自定义消息 或者ext扩展字段携带过去实现


3. 客户端创建聊天室提示you have no permission to do this.

客户端需要赋予超级管理员后才能创建聊天室,否则只能通过rest服务端创建


4. 消息发出多长时间可以撤回?

消息撤回时限默认为2分钟,可根据开发者需求以AppKey为单位进行单独设置


5. 漫游消息的拉取除了按条数,能否支持按时间段?

目前漫游都是分页获取,无法按照时间段来获取


6. 历史图片消息为什么看不到了?

历史附件类的消息,在环信服务器默认只保存7天,后续就会自动清理,拉取失败。如果有历史功能需求,建议将记录保存到自己服务器,


7. web有删除会话的api吗?




8.会话列表是默认已经排序好的 还是需要我这边来排序?

已经排好序的,约新的会话 数组的下标约靠前。


9. 发出去的图片还是会有?em-redirect=true

可以直接在 WebIMConfig 中设置useOwnUploadFun: true,

然后初始化时引入下,就不会在返回 em-redirect=true


10. 如何监听好友在线、离线状态?

这个有api 和实时回调,可以查询和 监听用户状态

https://docs-im.easemob.com/im/server/ready/user#在线与离线
https://docs-im.easemob.com/im/extensions/value/presencecallback#用户在线状态回调


11.小程序 真机调试可以正常聊天,但是发到体验版,要打开调试工具才能正常聊天

看一下小程序后台有没有配置域名

文档:

https://docs-im.easemob.com/im/applet/wechat#微信小程序集成介绍


12. web im是否支持发送订单消息?

支持的 需要通过自定义消息或通过ext扩展消息发送


13.为什么调用rest api接口获取不到历史记录?


服务器聊天历史记录默认只保存3天,确认下获取记录的时间是否是三天之内,如果时间没有问题确认下获取的这个时间是否有记录产生,rest接口获取历史记录还有一个小时的延迟


14.token的过期时间是多久?

这个token有效期没有具体的时间 接口返回的有效期是60天 这个最终以请求接口返回的为准 正常情况下token没有那么快就过期


15. 创建群组或者添加群组提示 user xxxx as joined too many groups!

单个用户ID只能加入500个群,可以调用接口获取用户已添加的群组数,如果超出不能继续添加,也可以联系商务上调。


16. 调用rest 上传文件为什么没有返回share-secret?

header里需要传restrict-access:true


17. web im发送文本消息报错,type 为503

可以检查一下发送消息的body 体,看一下是否有参数为 undefined


18.web im发送消息604 inter-app communication is not allowed

是需要在初始化的地方加上: appKey:WebIM.config.appkey


19. 撤回消息的时候,id 和mid 和撤回回调的不一样,怎么才能知道我撤回的是哪一条信息呢?

发消息本地生成的id 只是本地的 消息发出之后 在onReceivedMessage 这个回调里会回调出这条消息的服务端 mid 撤回的时候用这个mid 这个和接收端的消息id 是一样的


20. 小程序音视频 如果多次调用推流的方法,SDK这边是会把旧的流移除,重新订阅新流吗,这块儿的实现机制是什么样的

小程序调推流返回的是一个推流地址,地址是一个就只存在于一个流地址剩下的就是小程序本身的机制,多次调用也不存在重新给你创建新的流。


21. 小程序视频会议,为什么看不到自己和对方的图像呢?

需要在小程序后台申请livr-pusher 和 live-player 权限,并且是真机调试


22.web小程序小程序加入音视频会议不显示图像

原因一:后端返回的域名变化,没有在小程序白名单注册的域名是不可用的。
解决办法:让客户把新域名加入小程序白名单:mprtc.easecdn.com

原因二:小程序后台并未开通live-push live-player等标签权限。
解决方法:走小程序平台流程进行开通。

23.WEBIM能否查询到(群组/聊天室)当前在线人数?
web的 可以通过调rest 服务端接口 获取群组人数以及 群成员的id
然后调rest 服务端接口 查看群成员id 是否在线

24.群组聊天室的区别:
聊天室: 随进随出 在线可以收到聊天室消息,如果离线了,那么两分钟后自动退出聊天室,不会再接收消息。
群组: 有owner群主 和管理员 可分为私有群 公有群 加入后只要不被踢出去 就会一直在群里 在线情况下正常接收消息,离线之后会产生离线消息,在线之后接收到离线时的离线消息推送。

25.web端下载日志的方法是:WebIM.logger.download()

26.环信日活统计说明:

通过IM长连接登录、退出以及重连,通过IM长连接收发消息以及请求,均会产生日活计数,环信会按照用户ID进行排重(一个用户无论当日产生多少日活行为,排重后只统计为1)

27.关于环信console后台的强制推送与图片缩略图设置的作用:



①强制推送就是设置了离线推送免打扰也会接收到离线推送,
②缩略图就是设置APP端接收到图片消息自动下载的缩略图宽高

28.Rest请求注册用户报错405:
解决方案:是检查请求时的method,例如post的请求写成的是put。通常405是method错误。



29.关闭SDK log的方式:
WebIM.logger.disableAll()



30.WebSDK不支持IE9以下的原因

是因为用到了protobuf,并且IE9以下不支持websocket如图:




31.Windows 环境下cmd启动HTTPS=true npm start 启动方式:
因为此命令为Linux命令 用git bash 启动。


32.头像昵称的处理方式:
头像昵称则需要使用用户属性接口将自定义的昵称、头像URL地址、电话等一些列自己定义的参数set传入,后续在需要调用的时候通过获取用户属性接口拿到。


33.如何查看消息投递的状态?

进入console管理后台查看即时通讯->服务质量监控 查看IM消息投递查询。




34.如何将获取token这一步放在服务端,前端只负责token登录?
首先服务端则可以通过拿到该用户的环信id以及密码,调用http://a1.easemob.com/easemob-demo/easeim/token接口并且请求体内传入

{
"grant_type": "password",
"password": "1",
"username": "hfp"
}

获取拿到token,并且返回给前端即可。



35.漫游消息拉取接口返回过慢:
原因:漫游消息接口默认会返回一些非展示类型的消息,比如reacdack,channelack等,非展示在页面上,例如拉取十条展示消息,但会有一些非展示消息返回导致接口需要调用多次拉取展示消息,因此会返回过慢。
解决方式:可以配置漫游过滤,不漫游非展示类型的消息。


36、删除消息
没有删除消息的接口,可以去开通消息撤回,去撤回消息



37、服务器为什么不能删聊天记录
删了环信id,聊天记录是不会删除的,这么设计的逻辑是因为每个客户的业务场景不同,如果客户误删了环信id,需要重新注册回来,并且需要看到历史聊天记录。如果你这边的业务,是不希望这种场景,你可以去定义注册环信IM的id规则,你用户注册你自己应用的username时,按你定义的规则去注册IM的id,也就是说你这边的username和环信的id不是同一个,环信这边是根据环信id保存历史记录的



38、会话列表
您好,会话列表默认是保存7天的,可以延长至跟消息漫游一致的时间(3个月或者6个月),具体时间可以参考消息漫游时间,消息漫游服务联系建议您联系商务经理咨询。
默认拉取10条,最多拉取100条
获取会话列表有两种方式,
1.您可以将会话存到本地,然后去展示
1.您可以从环信服务器去拉取会话列表,会话列表功能需要联系您的商务经理去开通,可参考文档https://docs-im.easemob.com/im/web/basics/message#会话列表



39、聊天室和群组的区别
群组:无论用户有没有打开群都会接收到在线消息和离线消息
聊天室:离开后不再接收消息



40、关闭sdk自动打印日志
这个日志是sdk自动打印的,没有影响的,打印日志功能是可以关闭的,如果您的sdk版本在3.6.0及以上可以在页面加载完成时通过WebIM.logger.disableAll()关闭;sdk版本在3.6.0以下时,在webim.config.js文件中将isDebug改成false就可以了。



41、console后台client获取失败
清除缓存重新登录,若还是获取失败,则需要查看该appkey是不是被禁用了



42、发送消息报503
一般是消息体的问题,排查一下消息体的参数有没有undefined或者不是字符串类型



43、关联账号
没有这种关联效果
不过您可以开通实时回调, 消息到达环信服务器后,会同步回调到您服务器
到时您可以自己实现给其他的个人身份账号通知



44、注册的时候填了昵称,为什么还要设置用户属性
在注册的时候填写的昵称只是用于apns推送的,并不是用户属性,要先设置用户属性,才可以去获取用户属性的。
设置用户属性可参考文档:https://docs-im.easemob.com/im/web/basics/profile#设置用户属性(客户端)
https://docs-im.easemob.com/im/server/ready/usermetadata#设置用户属性(服务端)
获取用户属性可参考文档:https://docs-im.easemob.com/im/web/basics/profile#获取用户属性(客户端)
https://docs-im.easemob.com/im/server/ready/usermetadata#获取用户属性(服务端)


45、为什么appkey、用户id、用户密码都正确,还会报跨域问题、type:16、websocket断链
检查是否是本地启动项目,需要线上启动

46、声网报错 device not found

没有开摄像头或者麦克风

47、建群404
检查群成员的id要是环信id,不能为汉字

48、websocket断链
导致websocket断链有以下两种原因
1.断网,如果是因为断网导致的websocket断链,环信这边会自动重连,不用您做任何操作,
2.刷新页面或者其他操作导致退出使websocket断链,则需要重新登录
在您进行收发消息及调用环信sdk时,请您确保您登陆成功且onOpened回调已经触发,只有onOpened回调触发才代表与环信链接成功,才可以进行后续的操作。

49、uniapp写的同一套代码,在安卓机运行正常,在iOS发送消息报this.isZero is not a function

在官方demo中并没有复现这个问题,让客户提供了可以复现的demo,发现客户在配置文件和初始化sdk时将isDebug写成了true,将isDebug改成false之后问题解决

50、如何在第一次登录成功之后,刷新页面重新登录成功
您好,您可以在第一次登录成功之后,将用户名和密码及登录成功的token保存下来,之后在页面加载完成前使用token登录的方式实现登录。token登录的参考文档:https://docs-im.easemob.com/im/web/intro/basic#使用_token_登录 您可以参考一下这个方法去实现。或者您也可以参考一下我们demo的处理。

51、web端发消息报错503是什么问题?
A:检查下消息体,消息的body体里面包含undefined。


52、web端消息已读未读如何处理?
web端支持消息回执,接收方接收到消息,发送已读回执给对方,对方监听到onreadmassage回调后,将消息置为已读,ui展示已读状态。


53、web端报错type28是什么原因?
A:type 28通常就是没有登陆就调用了功能性接口导致的问题,需要检查登陆状态,在onOpened 触发之后再调用功能性接口。


54、web登陆成功,能发送消息,但是接收不到消息?
A:打印下msg.body,看下发送方、接收方是不是string 类型。


55、添加回调规则添加失败。
A:检查下回调规则名称是不是用的汉字,回调规则只能是数字、字母,不能用汉字。


56、对方离线了之后,发送的消息,上线后如何获取?
A:对方离线,消息会进入离线队列,如果没有集成第三方厂商离线推送,用户上线后,服务器下发给客户端。


57、调用SDK 方法报错: Cannot read property 'lookup' of undefined?
A:因为未登陆成功就调用了SDK 的api,需要在onOpened 链接成功回调执行后再去调用SDK 的api。


58、聊天室如何获取历史消息?
A:两种方式:1、环信服务器端主动推,需要联系商务开通服务,默认10条,数量可以调整。2、通过消息漫游接口自己去拉取历史消息,各端都有提供拉取漫游消息接口。


59、拉取消息漫游,conversationId是怎么获取的?
A:单聊的话,conversationId 就是对方用户的环信id。
群聊或聊天室的话,conversationId 就是groupid 或者chatroomid。


60、如何实现只有好友才可以发消息?
A:可以使用环信的发送前回调服务,消息先回调给配置的回调服务器,然后去判断收发双方是否是好友关系,如果是好友关系,那么下发消息,如果是非好友关系,则不下发消息,客户端ui可以根据不下发返回的code做提示。


61、调rest接口报401是什么原因?
A:调环信rest接口,需要管理员权限的token,确认下请求是否有token,且是在有效期,token的有效期以请求时服务器返回的时间为准。


62、调修改群信息报错如下
System.Net.WebException:“远程服务器返回错误: (400) 错误的请求。
A:检查下请求体,看下参数格式是否正确,比如"membersonly",,"allowinvites" 这两个参数的值为布尔值。


63、注册用户username是纯数字可以吗。

调restapi是可以的,serversdk的话,为了让用户使用更规范的名字,命名规则更严格一些,要求首位是字母。


64. web端怎么关掉日志?
3.6.0以上的 设置WebIM.logger.disableAll();
3.6.0之前的版本 配置isDebug false

65. 注册用户名的时候是大写,为什么这里用户名会转成了小写,导致两位聊天列表里同一个人出来了两条记录。
登录的用户环信这边会默认使用小写登录的,这里您可以都用小写,来避免这个问题

66.web聊天提示用户未登录?
所有的操作必须都在登录的情况下,刷新页面,跳转页面,都是需要重新登陆的。

67. 提示Cannot read property '_msgHash' of undefind
检查下是否登录成功,或者检查下接口调用次序,看看有么有在onOpened执行之前就有一些接口调用的情况。

68. 在demo测试群组聊天时,群成员消息上显示的是用户名,可以显示昵称吗?
可以在拿到这个ID的时候,去调获取用户属性的api,拿到用户对应的昵称等信息,再去展示。

69. A用户向B用户发送消息,为什么收不到呢?
1). 检查A用户是否发送成功
2). 如果发送成功,看下B用户是否有多设备登录,有可能在其它设备登录接收了这条消息,所以该设备不会再接收
3). 消息如果发送成功 看下监听回调onTextMessage是否收到消息 如果收到消息,看下ui层面是否有渲染

70. 已经删除好友,在环信管理后台已经相互看不到两个人的好友关系了,但还是能互相收到消息
环信默认是支持陌生人通讯的,只要知道对方ID 即可
demo中是做了好友验证,做了不是好友消息不渲染,但是能收到,会执行监听

71. 文字内容消息的长度上限是多少?
sdk目前没有限制 rest api限制 了40k

72. 聊天记录发送的url怎么变成可跳转的?
可以通过a标签,发送消息的时候可以判断是否是http连接,然后用a标签包裹上。

73. web端和移动端发送消息需要通过服务端集成,才能互相发送消息吗?
这个不要通过服务端集成,使用同一个appkey就可以互相发送消息。

74.uniapp和vue的demo,收到消息时,有提示声音吗?
这个demo中没有实现,收到消息有对应的监听回调,可以在回调触发时,实现自己的业务逻辑。

75. web端这边发送消息时没有自带时间戳吗?
目前在发送的时候没有自动传入发送的时间戳,可以在发消息的ext自行传入时间戳即可。

76. 如何设置在线消息免打扰?
单个会话的免打扰模式您可以自己去实现,要自己去维护一个免打扰list集合,当监听到有消息时,去判断下是否是免打扰用户,如果是免打扰用户,就不去提醒。
群组免打扰:可以使用rest去设置。("notification_ignore_群组id": true)

77.群组和聊天室区别
聊天室: 随进随出 在线可以收到聊天室消息,如果离线了,那么两分钟后自动退出聊天室,不会在接收消息。
群组:有owner群主 和管理员 可分为私有群 公有群 加入后只要不被踢出去 就就会一直在群里 在线情况下正常接收消息,离线之后会产生离线消息,在线之后接收到离线时的离线消息推送。

78. 设置全局禁言后,移除单个用户禁言为什么不生效?
全局禁言 和 把id单独添加到禁言列表 这虽然是同一个功能 但是是2种不同的实现方式 全局禁言是后台开关控制 所以开启全员禁言后 再去单独把某个id移除禁言列表 实际上是不生效的 在开启全员禁言时只有添加到白名单列表中才会生效

79、查到用户在群里,为什么发消息提示602 not in group or chatroom
需要查询该用户加入的群组中是否有这个群,可能会是脏数据导致的假象。需要用户重新加入群组

80、为什么设置了敏感词还能发送成功
需要排查是否将敏感词通过ext扩展发出,ext里面的内容不过敏感词

81、发送消息报错604 blocked by mod_antispam
反垃圾导致的,需要排查是否多次发送同一条消息,有没有收到反垃圾邮件

82、环信音视频报错conn.sendMSync is not a function
sdk4.0导致的问题,回退到3.6.3可正常使用

83、rest发送的消息不走发送前回调

84、为什么发送的消息,拉取会话列表拉取不到
需要排查是客户端发送的消息还是rest发送的消息,rest发消息写会话列表需要单独开通

85、在群组中,只有最后一个进入群的人可以接收到群里面其余人发送的信息,在这时如果有另一个人加入群,那么这个加入群的人可以接收其他人的信息,剩下的人接收不到,
sdk版本的问题,替换为最新的sdk即可解决

86、上传到环信的视频无法在safari浏览器播放的问题
url?xxx=true&origin-file=true 拼接这个后缀




87、vue项目打包上线,报跨域问题
检查项目是否是线上启动,有可能只启动了本地文件

88、设置某个群组推送免打扰
curl -X PUT {{url}}/{{org}}/{{app}}/users/{{username}}' \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-H 'Authorization: {{Authorization}}' \
--data-raw '{
"notification_ignore_{{groupId}}": true,
"notification_ignore_{{groupId2}}": false
}'


89、web端如何变更群主?
web需要调rest方法,web客户端去调客户自己服务器,然后客户服务器再去调环信服务器,调这个转让群组的方法。

https://docs-im.easemob.com/im/server/basics/group#%E8%BD%AC%E8%AE%A9%E7%BE%A4%E7%BB%84


90、添加群管理员,返回结果是404,是什么情况呢?
首先需要确认下群组是否还是存在的,另外确认下用户是否加入了群组,添加管理员权限的成员一定要是该群的成员。

收起阅读 »

Android 系统启动流程Init、Zygote、SystemService、ServiceManager

Android系统启动流程操作系统本身也是一个程序,只是这个程序是用来管理我们 App 应用程序的。 从系统的角度上来讲,Android系统的启动过程可以分为 bootloader 引导,装载和启动 linux内核,启动Android系统。Android 系统...
继续阅读 »

Android系统启动流程

操作系统本身也是一个程序,只是这个程序是用来管理我们 App 应用程序的。 从系统的角度上来讲,Android系统的启动过程可以分为 bootloader 引导,装载和启动 linux内核,启动Android系统。
Android 系统虽然也是基于 Linux 系统的,但是由于 Android 属于移动设备,并没有像 PC 那样的 BIOS 程序, 取而代之的是 BootLoader (系统启动加载器)。 Bootloader 相当于电脑上的Bios 他的主要作用就是初始化基本的硬件设备,建立内存空间映射,为装载linux内核准备好运行环境,当linux内核加载完毕之后,bootloder就会从内存中清除。在 Android 里没有硬盘,而是 ROM,它类似于硬盘存放操作系统,用户程序等。 ROM 跟硬盘一样也会划分为不同的区域,用于放置不同的程序。当 Linux 内核启动后会初始化各种软硬件环境,加载驱动程序,挂载根文件系统,Linux 内核加载的准备完毕后就开始加载一些特定的程序(进程)了。
具体流程,可以参考下流程图:

android_start.png
对于纯Android应用层开发来讲,了解一些Android的启动流程的知识并不会直接提高自己的代码质量。但是作为整个Android系统的开端,这部分的流程时刻影响着应用层的方方面面。这些知识也是作为Android开发进阶必须要了解的一部分。对于前面的bootloader引导也好,装载启动linux文件都是很底层的东西,感兴趣的可以自行了解一下,我们从启动androng系统开始分析,第一个加载的就是 init 进程。

一:init进程

我们应该都知道不管是 Java 还是 C/C++ 去运行某一个程序(进程)都是 XXX.xxx 的 main 方法作为入口,相信有很多大佬都跟我一样,App 开发做久了渐渐就忘记了还有个 main 方法。因此我们找到 /system/core/init/Init.cpp 的 main() 方法:

int main(int argc,char ** argv){

...
if(is_first_stage){
//创建和挂在启动所需要的文件目录
mount("tmpfs","/dev","tmpfs",MS_NOSUID,"mode=0755");
mkdir("/dev/pts",0755);
//创建和挂在很多...
...
}

...
//对属性服务进行初始化
property_init();

...
//用于设置子进程信号处理函数(如Zygote),如果子进程异常退出,init进程会调用该函数中设定的信号处理函数来处理
signal_handler_init();

...

//启动属性服务
start_property_service();

...

//解析init.rc配置文件
parser.ParseConfig("/init.rc");

}

main 方法里面有 148 行代码(不包括子函数代码)具体分为四个步骤:1.创建目录,挂载分区,2.解析启动脚本,3.启动解析的服务,4.守护解析的服务。init.rc 文件是 Android 系统的重要配置文件,位于 /system/core/rootdir/init.rc

import /init.environ.rc
import /init.usb.rc
// 当前硬件版本的脚本
import /init.${ro.hardware}.rc
import /init.${ro.zygote}.rc
import /init.trace.rc

on early-init
...
on init
...
// 服务 服务名称 执行文件路径 执行参数
// 有几个重要的服务
service zygote /system/bin/app_process64 -Xzygote /system/bin --zygote --start-system-server
service servicemanager /system/bin/servicemanager
service surfaceflinger /system/bin/surfaceflinger
service media /system/bin/mediaserver
service installd /system/bin/installd

在处理了繁多的任务后,init进程会进行最关键的一部操作: 启动Zygote
至此 init 进程已全部分析完毕,有四个步骤:1. 创建目录,挂载分区,2. 解析启动脚本,3. 启动解析的服务,4. 守护解析的服务。最需要注意的是 init 创建了 zygote(创建 App 应用的服务)、servicemanager (client 与 service 通信管理的服务)、surfaceflinger(显示渲染服务) 和 media(多媒体服务) 等 service 进程。

二:Zygote

Zygote 进程是由 init 进程通过解析 init.rc 文件而创建的。

service zygote /system/bin/app_process -Xzygote /system/bin --zygote --start-system-server
socket zygote stream 666
onrestart write /sys/android_power/request_state wake
onrestart write /sys/power/state on
onrestart restart media
onrestart restart netd

对应找到 /frameworks/base/cmds/app_process/app_main.cpp 源码文件中的 main 方法

int main(int argc, char* const argv[])
{
// AppRuntime 继承 AndoirdRuntime
AppRuntime runtime(argv[0], computeArgBlockSize(argc, argv));
// 过滤第一个参数
argc--;
argv++;
...
bool zygote = false;
bool startSystemServer = false;
bool application = false;
String8 niceName;
String8 className;

++i; // Skip unused "parent dir" argument.
// 解析参数
while (i < argc) {
const char* arg = argv[i++];
if (strcmp(arg, "--zygote") == 0) {
zygote = true;
niceName = ZYGOTE_NICE_NAME;
} else if (strcmp(arg, "--start-system-server") == 0) {
startSystemServer = true;
} else if (strcmp(arg, "--application") == 0) {
application = true;
} else if (strncmp(arg, "--nice-name=", 12) == 0) {
niceName.setTo(arg + 12);
} else if (strncmp(arg, "--", 2) != 0) {
className.setTo(arg);
break;
} else {
--i;
break;
}
}

...
//设置进程名
if (!niceName.isEmpty()) {
runtime.setArgv0(niceName.string());
set_process_name(niceName.string());
}
// 如果 zygote ,AndroidRuntime 执行 com.android.internal.os.ZygoteInit
// 看上面解析的脚本参数执行的是这里。
if (zygote) {
runtime.start("com.android.internal.os.ZygoteInit", args, zygote);
} else if (className) {
runtime.start("com.android.internal.os.RuntimeInit", args, zygote);
} else {
fprintf(stderr, "Error: no class name or --zygote supplied.\n");
app_usage();
LOG_ALWAYS_FATAL("app_process: no class name or --zygote supplied.");
return 10;
}
}

上面首先是解析参数,然后来到 /frameworks/base/core/jni/AndroidRuntime.cpp 中的 start 方法:

/*
* Start the Android runtime. This involves starting the virtual machi

相信很多人都跟我一样,刚开始看这样的代码有一定的难度,毕竟都是大学时期学的,不涉及底层开发的,看起来有些吃力,但是仔细阅读之后,或者通过命名,都可以大致看得懂流程。

Zygote 进程是由 init 进程解析 init.rc 脚本创建的,其具体的执行源码是在 App_main.main 方法,首先会创建一个虚拟机实例,然后注册 JNI 方法,最后通过 JNI 调用进入 Java 世界来到 ZygoteInit.main 方法。在 Java 世界中我们会为 Zygote 注册 socket 用于进程间通信,预加载一些通用的类和资源,启动 system_server 进程,循环等待孵化创建新的进程。总结一下ZygoteInit的main方法都做了哪些事情:

1.创建了一个Server端的Socket

2.预加载类和资源

3.启动了SystemServer进程

4.等待AMS请求创建新的应用程序进程

Zygote进程启动后,总共做了哪几件事:

1.创建AndroidRuntime并调用其start方法,启动Zygote进程。

2.创建Java虚拟机并为Java虚拟机注册JNI方法。

3.通过JNI调用ZygoteInit的main函数进入Zygote的java框架层。

4.通过registerZygoteSocket方法创建服务端Socket,并通过runSelectLoop方法等待AMS的请求来创建新的应用程序进程。

5.启动SystemServer。

三:SystemService

Zygote 进程的启动过程中会调用 startSystemServer 方法来启动 SystemServer 进程:

private static boolean startSystemServer(String abiList, String socketName) throws MethodAndArgsCaller, RuntimeException {
...
// 设置一些参数
String args[] = {
"--setuid=1000",
"--setgid=1000",
"--setgroups=1001,1002,1003,1004,1005,1006,1007,1008,1009,1010,1018,1021,1032,3001,3002,3003,3006,3007",
"--capabilities=" + capabilities + "," + capabilities,
"--nice-name=system_server",
"--runtime-args",
"com.android.server.SystemServer",
};

ZygoteConnection.Arguments parsedArgs = null;
int pid;
try {
...
// fork 创建 system_server 进程,后面会具体分析
pid = Zygote.forkSystemServer(
parsedArgs.uid, parsedArgs.gid,
parsedArgs.gids,
parsedArgs.debugFlags,
null,
parsedArgs.permittedCapabilities,
parsedArgs.effectiveCapabilities);
} catch (IllegalArgumentException ex) {
throw new RuntimeException(ex);
}

// pid == 0 代表子进程,也就是 system_server 进程
if (pid == 0) {
// 执行初始化 system_server 进程
handleSystemServerProcess(parsedArgs);
}
return true;
}

1. 启动 SystemServer

public static int forkSystemServer(int uid, int gid, int[] gids, int debugFlags,
int[][] rlimits, long permittedCapabilities, long effectiveCapabilities) {
VM_HOOKS.preFork();
int pid = nativeForkSystemServer(uid, gid, gids, debugFlags, rlimits, permittedCapabilities, effectiveCapabilities);
// Enable tracing as soon as we enter the system_server.
if (pid == 0) {
Trace.setTracingEnabled(true);
}
VM_HOOKS.postForkCommon();
return pid;
}

// 调用的 native 方法去创建的,nativeForkSystemServer() 方法在 AndroidRuntime.cpp 中注册的,调用 com_android_internal_os_Zygote.cpp 中的 com_android_internal_os_Zygote_nativeForkSystemServer() 方法
native private static int nativeForkSystemServer(int uid, int gid, int[] gids, int debugFlags,int[][] rlimits, long permittedCapabilities, long effectiveCapabilities);

static jint com_android_internal_os_Zygote_nativeForkSystemServer(
JNIEnv* env, jclass, uid_t uid, gid_t gid, jintArray gids,
jint debug_flags, jobjectArray rlimits, jlong permittedCapabilities,
jlong effectiveCapabilities) {
// fork 创建 systemserver 进程
pid_t pid = ForkAndSpecializeCommon(env, uid, gid, gids,
debug_flags, rlimits, permittedCapabilities, effectiveCapabilities,
MOUNT_EXTERNAL_DEFAULT, NULL, NULL, true, NULL,NULL, NULL);
// pid > 0 是父进程执行的逻辑
if (pid > 0) {
// waitpid 等待 SystemServer 进程的退出,如果退出了重启 zygote 进程
if (waitpid(pid, &status, WNOHANG) == pid) {
RuntimeAbort(env);
}
}
return pid;
}

static pid_t ForkAndSpecializeCommon(JNIEnv* env, uid_t uid, gid_t gid, jintArray javaGids, jint debug_flags, jobjectArray javaRlimits, jlong permittedCapabilities, jlong effectiveCapabilities, jint mount_external, jstring java_se_info, jstring java_se_name, bool is_system_server, jintArray fdsToClose, jstring instructionSet, jstring dataDir) {
//设置子进程的 signal 信号处理函数
SetSigChldHandler();
// fork 子进程(SystemServer)
pid_t pid = fork();
if (pid == 0) {
// 进入子进程
...
// gZygoteClass = com/android/internal/os/Zygote
// gCallPostForkChildHooks = GetStaticMethodIDOrDie(env, gZygoteClass, "callPostForkChildHooks", "(ILjava/lang/String;)V");
// 等价于调用 Zygote.callPostForkChildHooks()
env->CallStaticVoidMethod(gZygoteClass, gCallPostForkChildHooks, debug_flags,is_system_server ? NULL : instructionSet);
...
} else if (pid > 0) {
// the parent process
}
return pid;
}

private static void handleSystemServerProcess( ZygoteConnection.Arguments parsedArgs) throws ZygoteInit.MethodAndArgsCaller {
...
if (parsedArgs.invokeWith != null) {
...
} else {
ClassLoader cl = null;
if (systemServerClasspath != null) {
// 创建类加载器,并赋予当前线程
cl = new PathClassLoader(systemServerClasspath, ClassLoader.getSystemClassLoader());
Thread.currentThread().setContextClassLoader(cl);
}
// RuntimeInit.zygoteInit
RuntimeInit.zygoteInit(parsedArgs.targetSdkVersion, parsedArgs.remainingArgs, cl);
}
}

public static final void zygoteInit(int targetSdkVersion, String[] argv, ClassLoader classLoader) throws ZygoteInit.MethodAndArgsCaller {
...
// 通用的一些初始化
commonInit();
// 这个方法是 native 方法,主要是打开 binder 驱动,启动 binder 线程,后面分析 binder 驱动的时候再详解。
nativeZygoteInit();
// 应用初始化
applicationInit(targetSdkVersion, argv, classLoader);
}

private static void applicationInit(int targetSdkVersion, String[] argv, ClassLoader classLoader) throws ZygoteInit.MethodAndArgsCaller {
...
final Arguments args;
try {
// 解析参数 Arguments
args = new Arguments(argv);
} catch (IllegalArgumentException ex) {
return;
}
...
invokeStaticMain(args.startClass, args.startArgs, classLoader);
}

private static void invokeStaticMain(String className, String[] argv, ClassLoader classLoader) throws ZygoteInit.MethodAndArgsCaller {
Class<?> cl = Class.forName(className, true, classLoader);
...

Method m;
try {
m = cl.getMethod("main", new Class[] { String[].class });
} catch (NoSuchMethodException ex) {
...
} catch (SecurityException ex) {
...
}
...
// 通过抛出异常,回到了 ZygoteInit.main()
// try{} catch (MethodAndArgsCaller caller) {caller.run();}
throw new ZygoteInit.MethodAndArgsCaller(m, argv);
}

绕了一大圈我们发现是通过抛异常回到了 ZygoteInit.main() 方法中的 try…catch(){MethodAndArgsCaller.run() }

2. 创建 SystemServer

public static class MethodAndArgsCaller extends Exception implements Runnable {
...
public void run() {
try {
// 根据传递过来的参数可知,此处通过反射机制调用的是 SystemServer.main() 方法
mMethod.invoke(null, new Object[] { mArgs });
} catch (IllegalAccessException ex) {
...
}
}

public final class SystemServer {
...
public static void main(String[] args) {
new SystemServer().run();
}

private void run() {
// 主线程 looper
Looper.prepareMainLooper();

// 初始化系统上下文
createSystemContext();

// 创建系统服务管理
mSystemServiceManager = new SystemServiceManager(mSystemContext);
// 将 mSystemServiceManager 添加到本地服务的成员 sLocalServiceObjects,sLocalServiceObjects 里面是一个静态的 map 集合
LocalServices.addService(SystemServiceManager.class, mSystemServiceManager);

//启动各种系统服务
try {
// 启动引导服务
startBootstrapServices();
// 启动核心服务
startCoreServices();
// 启动其他服务
startOtherServices();
} catch (Throwable ex) {
Slog.e("System", "************ Failure starting system services", ex);
throw ex;
}

// 一直循环执行
Looper.loop();
throw new RuntimeException("Main thread loop unexpectedly exited");
}

private void createSystemContext() {
// 创建系统进程的上下文信息,这个在进程启动再详解
ActivityThread activityThread = ActivityThread.systemMain();
mSystemContext = activityThread.getSystemContext();
...
}

private void startBootstrapServices() {
// 阻塞等待与 installd 建立 socket 通道
Installer installer = mSystemServiceManager.startService(Installer.class);

// 启动服务 ActivityManagerService
mActivityManagerService = mSystemServiceManager.startService(ActivityManagerService.Lifecycle.class).getService();
mActivityManagerService.setSystemServiceManager(mSystemServiceManager);

// 启动服务 PackageManagerService
mPackageManagerService = PackageManagerService.main(mSystemContext, installer, mFactoryTestMode != FactoryTest.FACTORY_TEST_OFF, mOnlyCore);
mPackageManager = mSystemContext.getPackageManager();

// 设置 AMS , 把自己交给 ServiceManager. addService 去管理
mActivityManagerService.setSystemProcess();

...
}

private void startCoreServices() {
...
}

private void startOtherServices() {
// 启动闹钟服务
mSystemServiceManager.startService(AlarmManagerService.class);
// 初始化 Watchdog
final Watchdog watchdog = Watchdog.getInstance();
watchdog.init(context, mActivityManagerService);
// 输入管理的 service
inputManager = new InputManagerService(context);
// WindowManagerService
wm = WindowManagerService.main(...);
// InputManagerService 和 WindowManagerService 都交给 ServiceManager 管理
ServiceManager.addService(Context.WINDOW_SERVICE, wm);
ServiceManager.addService(Context.INPUT_SERVICE, inputManager);
// 启动input
inputManager.start();
// 显示启动界面
ActivityManagerNative.getDefault().showBootMessage(...);
// 状态栏管理
statusBar = new StatusBarManagerService(context, wm);
// JobSchedulerService
mSystemServiceManager.startService(JobSchedulerService.class);
...
// 准备好了 wms, pms, ams 服务
wm.systemReady();
mPackageManagerService.systemReady();
mActivityManagerService.systemReady();
}

...
}

我们可以看到SystemServer在启动后,陆续启动了各项服务,包括ActivityManagerService,PowerManagerService,PackageManagerService等等,而这些服务的父类都是SystemService。

最后总结一下SystemServer进程:

1.启动Binder线程池

2.创建了SystemServiceManager(用于对系统服务进行创建、启动和生命周期管理)

3.启动了各种服务

3. 管理 SystemServer

系统服务启动后都会交给 ServiceManager 来管理,无论是 mSystemServiceManager.startService 还是 ServiceManager.addService 都是走的 ServiceManager.addService() 方法:

public static void addService(String name, IBinder service) {
try {
getIServiceManager().addService(name, service, false);
} catch (RemoteException e) {
Log.e(TAG, "error in addService", e);
}
}

private static IServiceManager getIServiceManager() {
if (sServiceManager != null) {
return sServiceManager;
}
sServiceManager = ServiceManagerNative.asInterface(BinderInternal.getContextObject());
return sServiceManager;
}

public abstract class ServiceManagerNative extends Binder implements IServiceManager {
static public IServiceManager asInterface(IBinder obj) {
if (obj == null) {
return null;
}
IServiceManager in =(IServiceManager)obj.queryLocalInterface(descriptor);
if (in != null) {
return in;
}
// 创建 ServiceManagerProxy 对象
return new ServiceManagerProxy(obj);
}
}

class ServiceManagerProxy implements IServiceManager {
private IBinder mRemote;

public ServiceManagerProxy(IBinder remote) {
mRemote = remote;
}
...
// IPC binder 驱动
public void addService(String name, IBinder service, boolean allowIsolated)
throws RemoteException {
Parcel data = Parcel.obtain();
Parcel reply = Parcel.obtain();
data.writeInterfaceToken(IServiceManager.descriptor);
data.writeString(name);
data.writeStrongBinder(service);
data.writeInt(allowIsolated ? 1 : 0);
// mRemote 是 IBinder 对象
mRemote.transact(ADD_SERVICE_TRANSACTION, data, reply, 0);
reply.recycle();
data.recycle();
}
}

最后我们再来总结一下:SystemServer 进程是由 Zygote 进程 fork 创建的,SystemServer 进程创建后会创建启动引导服务、核心服务和其他服务,并且将所创建的服务,通过跨进程通信交给 ServiceManager 进程来管理。

哈哈,看完这么多,是不是感觉有点了解,然后又不是特别了解,有一定的了解,就会想去深入了解,花点时间,慢慢去消化,主体流程了解,具体的底层实现,有兴趣可以多了解。


收起阅读 »

国庆渐变头像

国庆五星红旗渐变色头像五星红旗半透明头像教程国旗渐变头像国庆头像 国旗渐变 制作设置教程此生不悔入华夏 祝祖国繁荣昌盛!效果展示缘起群聊的时候, 有人说这个国旗渐变的效果, 我看了一下, 有点帅呢, 就研究了一下环境雷电模拟器: 4.0.63 Android版...
继续阅读 »

国庆五星红旗渐变色头像

五星红旗半透明头像教程

国旗渐变头像

国庆头像 国旗渐变 制作设置教程

此生不悔入华夏 祝祖国繁荣昌盛!

效果展示

01.jpg02.jpg03.jpg04.jpg05.jpg06.jpg07.jpg08.jpg09.jpg10.jpg11.jpg12.jpg13.jpg14.jpg15.jpg

缘起

群聊的时候, 有人说这个国旗渐变的效果, 我看了一下, 有点帅呢, 就研究了一下

环境

雷电模拟器: 4.0.63 Android版本: 7.1.2 Autojs版本: 8.8.20

思路

  1. 准备国旗和头像
  2. 国旗修改透明渐变
  3. 合并两张图片

你将学到以下知识点

  • 判断图片类型, 如果用户传入了jpg, 就把他变成png
  • 修改图片大小, 把国旗和头像宽高改为一致
  • 修改图片为透明渐变
  • 保存mat为文件
  • byte数组转为16进制字符串
  • mat转bitmap
  • 打印mat的属性
  • 合并图片

代码讲解

1. 国旗渐变我做成了模块, 方便调用
let formatImg = require("./formatImg");

let dir = files.path("./img");
var arr = files.listDir(dir);
let filePathList = arr.map((item) => {
return files.join(dir, item);
});

filePathList.map((filePath) => {
formatImg(filePath);
});
2. 导入类
runtime.images.initOpenCvIfNeeded();
importClass(org.opencv.core.MatOfByte);
importClass(org.opencv.core.Scalar);
importClass(org.opencv.core.Point);
importClass(org.opencv.core.CvType);
importClass(java.util.List);
importClass(java.util.ArrayList);
importClass(java.util.LinkedList);
importClass(org.opencv.imgproc.Imgproc);
importClass(org.opencv.imgcodecs.Imgcodecs);
importClass(org.opencv.core.Core);
importClass(org.opencv.core.Mat);
importClass(org.opencv.core.MatOfDMatch);
importClass(org.opencv.core.MatOfKeyPoint);
importClass(org.opencv.core.MatOfRect);
importClass(org.opencv.core.Size);
importClass(org.opencv.features2d.DescriptorMatcher);
importClass(org.opencv.features2d.Features2d);
importClass(org.opencv.core.MatOfPoint2f);
importClass(org.opencv.android.Utils);
importClass(android.graphics.Bitmap);
importClass(java.lang.StringBuilder);
importClass(java.io.FileInputStream);
importClass(java.io.File);
3. 定义图片类型, 用于判断图片类型
const TYPE_JPG = "jpg";
const TYPE_GIF = "gif";
const TYPE_PNG = "png";
const TYPE_BMP = "bmp";
const TYPE_UNKNOWN = "unknown";
4. 归一化图片, 都改为png格式
function formatImg(imgPath) {
let type = getPicType(new FileInputStream(new File(files.path(imgPath))));
if (type === "png") {
return imgPath;
} else if (type === "jpg") {
var img = images.read(imgPath);
images.save(img, "/sdcard/tempImg001.png");
return "/sdcard/tempImg001.png";
} else {
toastLog("只支持jpg或者png");
return false;
}
}
5. 读取图片
var img = images.read(imgPath);
var img2 = images.read(imgPath2);
6. 修改图片大小
function normalize(img, img2) {
let imgWidth = img.getWidth(); // 200
let imgHeight = img.getHeight(); // 200
return images.resize(img2, [imgWidth, imgHeight]);
}
7. 修改图片透明渐变
function transparentGradient(mat) {
let width = mat.width();
let height = mat.height();
let unit = 256 / width;
let wLimit = (width / 5) * 3;
for (var i = 0; i < height; i++) {
for (var j = 0; j < width; j++) {
let item = mat.get(i, j);
if (j > wLimit) {
item[3] = 0;
} else {
item[3] = 180 - unit * j;
}
mat.put(i, j, item);
}
}
return mat;
}
8. 合并图片
let img4 = merge(mat, mat3);
9. 预览效果
let tempDir = files.join("/sdcard/Pictures/img");
let tempFilePath2 = files.join(tempDir, files.getName(oImgPath2));
files.createWithDirs(tempFilePath2);
images.save(img4, tempFilePath2);
app.viewFile("/sdcard/1.png");
10. 释放资源
img.recycle();
img2.recycle();
img3.recycle();
img4.recycle();
mat.release();
mat3.release();

名人名言

思路是最重要的, 其他的百度, bing, stackoverflow, 安卓文档, autojs文档, 最后才是群里问问 --- 牙叔教程


收起阅读 »

Kotlin系列八:静态方法、infix函数、高阶函数的常见应用举例

一 静态方法 java中定义静态方法只需要在方法前添加static即可; kotlin中有四种方式:object的单例类模式、companion object(可以局部写静态方法)、JvmStatic注解模式、顶层函数模式。 1.1 object 用objec...
继续阅读 »

一 静态方法


java中定义静态方法只需要在方法前添加static即可;


kotlin中有四种方式:object的单例类模式、companion object(可以局部写静态方法)、JvmStatic注解模式、顶层函数模式。


1.1 object


用object修饰的类,实际上是单例类,在Kotlin中调用时是类名加方法直接使用。


object Util {
fun doAction(){
Log.v("TAG","doAction")
}
}

//kotlin中调用
Util.doAction()

//java中调用 INSTANCE是Util的单例类
Util.INSTANCE.doAction();

1.2 companion object


用companion object修饰的方法也能通过类名加.直接调用,但是这时通过伴生对象实现的。它的原理是在原有类中生成一个伴生类,Kotlin会保证这个伴生类只有一个对象。


class Util{
//此处是单例类可以调用的地方
companion object {
fun doAction(){
Log.v("TAG","doAction")
}
}
//此处是普通方法
fun doAction2(){
Log.v("TAG","doAction")
}
}

//kotlin调用
Util.doAction()

//java调用,Companion是单例类
Util.Companion.doAction();

1.3 @JvmStatic注解


给单例类(object)和伴生对象的方法加@JvmStatic注解,这时编译器会将这些方法编译成真正的静态方法。


注意:JvmStatic只能注释在单例类或companion object中的方法上。


class Util{
//此处是单例类可以调用的地方
companion object {
@JvmStatic
fun doAction(){
Log.v("TAG","doAction")
}
}

//此处是普通方法
fun doAction2(){
Log.v("TAG","doAction")
}

}

//kotlin中的调用
Util.doAction()

//java中的调用,成为了真正的单例类
Util.doAction();

1.4 顶层方法


顶层函数是指那些没有定义在任何类中的函数,写在任何类的外层即可,kotlin会将所有顶层函数编译成静态方法,可以在任何位置被直接调用。


//在类的外部
fun doAction(){
}
class Util {
}

//kotlin调用方式
doAction()

//java调用方式,真正的静态方法
UtilKt.doAction();

二 infix函数


infix函数作用:将函数调用的语法修改了一下。


比如:A to B 等于 A.to(B)。


实现方式:在函数前面加上infix即可。


限制条件:1.不能是顶层函数;2.参数只能有一个。


例子:


infix fun String.beginsWith(p:String) = startsWith(p)

三 利用高阶函数简化常用API


apply函数简化intent.putExtra():


 fun SharedPreferences.open(block: SharedPreferences.Editor.() -> Unit) {
val editor = edit()
editor.block()
editor.apply()
}


使用:
getSharedPreferences("user", Context.MODE_PRIVATE).open {
putString("username", "Lucas")
putBoolean("graduated", false)
}

简化SharedPreferences:


 fun SharedPreferences.open(block: SharedPreferences.Editor.() -> Unit) {
val editor = edit()
editor.block()
editor.apply()
}


使用:
getSharedPreferences("user", Context.MODE_PRIVATE).open {
putString("username", "Lucas")
putBoolean("graduated", false)
}

简化ContentValues:


fun cvOf(vararg pairs: Pair<String, Any?>) =  ContentValues().apply {
for(pair in pairs){
val key = pair.first
val value = pair.second
when(value){
is Int -> put(key, value)
is Long -> put(key, value)
is Short -> put(key, value)
is Float -> put(key, value)
is Double -> put(key, value)
is Boolean -> put(key, value)
is String -> put(key, value)
is Byte -> put(key, value)
is ByteArray -> put(key, value)
null -> putNull(key)
}
}
}

懒加载技术实现lazy():


  fun <T> later(block: () -> T) = Later(block)

class Later<T>(val block: () -> T){
var value: Any? = null

operator fun getValue(any: Any?, prop: KProperty<*>): T{
if (value == null){
value = block
}
return value as T
}
}

使用:
val haha:String by later {
"hhaa"
}

泛型实化简化startActivity()并附带intent传值:


    inline fun <reified T> startActivity(context: Context, block: Intent.() -> Unit){
val intent = Intent(context, T::class.java)
intent.block()
context.startActivity(intent)
}
使用:
startActivity<MainActivity>(this){
putExtra("haha","1111")
putExtra("hahaaaa","1111")
}

简化N个数的最大值最小值:


   fun <T : Comparator<T>> MyMax(vararg nums: T): T{
if (nums.isEmpty()) throw RuntimeException("params can not be empty")
var maxNum = nums[0]
for (num in nums){
if (num > maxNum){
maxNum = num
}
}
return maxNum
}

使用:
val a = 1
val b = 3
val c = 2
val largest = MyMax(a,b,c)

简化Toast:


    fun String.showToast(context: Context, duration: Int = Toast.LENGTH_SHORT){
Toast.makeText(context, this, duration).show()
}
fun Int.showToast(context: Context, duration: Int = Toast.LENGTH_SHORT){
Toast.makeText(context, this, duration).show()
}

用法:
"ahah".showToast(this, Toast.LENGTH_LONG)

简化Snackbar:


fun View.showSnackbar(text: String, actionText: String?=null, duration: Int = Snackbar.LENGTH_SHORT, block: (() -> Unit)? =null){
val snackbar = Snackbar.make(this, text, duration)
if (actionText != null && block != null){
snackbar.setAction(actionText){
block()
}
}
snackbar.show()
}

fun View.showSnackbar(text: String, actionResId: Int?=null, duration: Int = Snackbar.LENGTH_SHORT, block: (() -> Unit)? =null){
val snackbar = Snackbar.make(this, text, duration)
if (actionResId != null && block != null){
snackbar.setAction(actionResId){
block()
}
}
snackbar.show()
}

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

反射解决FragmentDialog内存泄露??‍♂️

怎么引发内存泄露的 这个DialogFragment的内存泄露几年前我就遇到了,但当时也稀里糊涂的,在网上搜索各种办法,看的我也是云里雾里,迷迷糊糊。在查阅大量资料之后,终于明白为什么会导致内存泄露了。 归根到底就是DialogFragment在给Dialog...
继续阅读 »

怎么引发内存泄露的


这个DialogFragment的内存泄露几年前我就遇到了,但当时也稀里糊涂的,在网上搜索各种办法,看的我也是云里雾里,迷迷糊糊。在查阅大量资料之后,终于明白为什么会导致内存泄露了。


归根到底就是DialogFragment在给Dialog设置setOnCancelListenersetOnDismissListener的时候将当前的DialogFragment引用传给了Message。在一些复杂项目中,各种各样的第三方库都有自己的消息处理,是根HandleThread有关系,这玩意一多就容易有问题。(最后一句话我搬的,其实我也不清楚🤣)


Looper.loop()中用MessageQueue.next()去取消息,如果之后没有消息,next()会处于一个挂起状态,MessageQueue会一直检测最后一条消息链是否有next消息被添加,于是最后的消息会被一直索引,直到下一条Message出现。


我就不展示这些源码了,因为可能看不懂,所以我根据自己的理解写了个简单的差不多的测试:


我先创建一个自己的Looper->MyLooper,模拟Looper的运作


object MyLooper {
//处理消息队列的类
val myQueue = MyMessageQueue()
///添加一条消息
fun addMessage(msg: Message) {
println("添加消息: ${msg.obj}")
myQueue.addMessage(msg)
}
//开始吧
fun lopper() {
while (true) {
val next = myQueue.next()
println("处理消息---->${next?.obj}")
if (next == null) {
return
}
}
}
}

创建消息Message和队列MessageQueue,我不写那么复杂了,差不多一个意思,一个是消息载体,一个是处理消息队列的。



class Message(var obj: Any? = null, var next: Message? = null)

class MyMessageQueue {
//初始消息
private var message: Message = Message("线程启动")
//将新来的消息添加到当前消息的屁股后面
fun addMessage(msg: Message?) {
//我的下一个消息就是你
message.next = msg
}
//检索下一个Message,如果没有下一个message,我就等下一条消息出现。
fun next(): Message {
while (true) {
if (message.next == null) {
println("重新检查消息 当前被卡住的消息-${message.obj}")
Thread.sleep(100)
continue
}
val next = message.next
message = next!!
return message
}
}
}

写一个测试类试试


    @Test
fun test() {
println("消息测试开始")
Thread {
MyLooper.lopper()
}.start()
Thread.sleep(100)
MyLooper.addMessage(Message("One Message"))//发送第一个消息
Thread.sleep(100)
MyLooper.addMessage(Message("Two Message"))//发送第二个消息
Thread.sleep(100)
while (true) {
continue
}
}

运行结果也不负众望,最后一条消息一直被索引。


myLooper.png


这差不多就是我理解的意思。


如何处理


DialogFragment要通过消息机制来通知自己关闭了,这个逻辑没办法更改。我们只能通过弱引用当前的DialogFragment让系统GG的时候帮我们回收掉,我的最终解决是通过反射替换父类的变量。


重写DialogFragment设置的两个监听器

    private DialogInterface.OnCancelListener mOnCancelListener =
new DialogInterface.OnCancelListener() {
@SuppressLint("SyntheticAccessor")
@Override
public void onCancel(@Nullable DialogInterface dialog) {
if (mDialog != null) {
DialogFragment.this.onCancel(mDialog);
}
}
};

private DialogInterface.OnDismissListener mOnDismissListener =
new DialogInterface.OnDismissListener() {
@SuppressLint("SyntheticAccessor")
@Override
public void onDismiss(@Nullable DialogInterface dialog) {
if (mDialog != null) {
DialogFragment.this.onDismiss(mDialog);
}
}
};

上面两个是DialogFragment源码的两个监听器,不管他怎么写,最后都是要把当前的this放进去。


所以我们重写两个监听器。


因为两个监听器的操作流程差不多一样,我就写了个接口,等会你就明白了。


interface IDialogFragmentReferenceClear {
//弱引用对象
val fragmentWeakReference: WeakReference<DialogFragment>
//清理弱引用
fun clear()
}

重写取消监听器:


class OnCancelListenerImp(dialogFragment: DialogFragment) :
DialogInterface.OnCancelListener, IDialogFragmentReferenceClear {

override val fragmentWeakReference: WeakReference<DialogFragment> =
WeakReference(dialogFragment)

override fun onCancel(dialog: DialogInterface) {
fragmentWeakReference.get()?.onCancel(dialog)
}

override fun clear() {
fragmentWeakReference.clear()
}
}

重写关闭监听器:


class OnDismissListenerImp(dialogFragment: DialogFragment) :
DialogInterface.OnDismissListener, IDialogFragmentReferenceClear {

override val fragmentWeakReference: WeakReference<DialogFragment> =
WeakReference(dialogFragment)

override fun onDismiss(dialog: DialogInterface) {
fragmentWeakReference.get()?.onDismiss(dialog)
}

override fun clear() {
fragmentWeakReference.clear()
}
}

很简单是吧。


然后就是替换了。


替换父类的监听器

我这里的替换是直接替换的DialogFragment这两个变量。


我们在替换父类的监听器的时候,一定要在父类使用这两个监听器之前替换。因为在我测试过程中,在之后替换,还是有极小的概率造成内存泄露,很无语,但我也不知道为什么。


我们先捋一下Dialog的创建流程:


onCreateDialog(@Nullable Bundle savedInstanceState)出发,会依次找到这几个方法。



  1. public LayoutInflater onGetLayoutInflater

  2. private void prepareDialog

  3. public Dialog onCreateDialog


上面是按1.2.3顺序执行的。触发Dialog设置监听器是在onGetLayoutInflater,所以我们重写这个方法。在父类执行之前进行替换,使用反射替换~


    override fun onGetLayoutInflater(savedInstanceState: Bundle?): LayoutInflater {
//先尝试反射替换
val isReplaceSuccess = replaceCallBackByReflexSuper()
//现在可以执行父类的操作了
val layoutInflater = super.onGetLayoutInflater(savedInstanceState)
if (!isReplaceSuccess) {
Log.d("Dboy", "反射设置DialogFragment 失败!尝试设置Dialog监听")
replaceDialogCallBack()
} else {
Log.d("Dboy", "反射设置DialogFragment 成功!")
}

return layoutInflater
}

这里是核心的替换操作。我们找到要替换的类和字段,然后反射修改它的值。


    private fun replaceCallBackByReflexSuper(): Boolean {
try {
val superclass: Class<*> =
findSuperclass(javaClass, DialogFragment::class.java) ?: return false
//重新给取消接口赋值
val mOnCancelListener = superclass.getDeclaredField("mOnCancelListener")
mOnCancelListener.isAccessible = true
mOnCancelListener.set(this, OnCancelListenerImp(this))
//重新给关闭接口赋值
val mOnDismissListener = superclass.getDeclaredField("mOnDismissListener")
mOnDismissListener.isAccessible = true
mOnDismissListener.set(this, OnDismissListenerImp(this))
return true
} catch (e: NoSuchFieldException) {
Log.e("Dboy", "dialog 反射替换失败:未找到变量")
} catch (e: IllegalAccessException) {
Log.e("Dboy", "dialog 反射替换失败:不允许访问")
}
return false
}

我们在反射获取失败之后,在手动进行一次设置,看上面的调用时机。


    private fun replaceDialogCallBack() {
if (mOnCancelListenerImp == null) {
mOnCancelListenerImp = OnCancelListenerImp(this)
}
dialog?.setOnCancelListener(mOnCancelListenerImp)
if (mOnDismissListenerImp == null) {
mOnDismissListenerImp = OnDismissListenerImp(this)
}
dialog?.setOnDismissListener(mOnDismissListenerImp)
}

replaceDialogCallBack替换回调接口,可以减少内存泄露,但不能完全解决内存泄露。在没有特殊情况下,反射都是会成功的,只要反射替换成功,给内存泄露说拜拜。


然后再onDestroyView清空一下我们的弱引用。


    override fun onDestroyView() {
super.onDestroyView()
//手动清理一下弱引用
mOnCancelListenerImp?.clear()
mOnCancelListenerImp = null

mOnDismissListenerImp?.clear()
mOnDismissListenerImp = null
}

为什么你的解决方法不管用


我刚接触DialogFragment的时候,这个内存泄露就一直伴随着我。


我当时菜鸟,在网上找各种解决方法,有的说重写onCreateDialog替换一个自己的Dialog,重写两个监听器设置方法,然后不让DialogFragment设置这两个监听器就解决了...我去,我现在想想感觉这个是最弱智的解决办法了,完全是为了解决而解决,直接掐断源头。


之后还有一个比较靠谱的方法,和我这个一样,也是重写这两个接口弱引用对象,不过那个方法是在onActivityCreated中对Dialog的这两个接口进行的重新赋值。这个方法是可行了。但是后来,我发现又不行了。就是因为是在父类先设置一次监听器之后还是有机会造成内存泄露。


还有就是说,等你去翻阅自己AndroidStudio的DialogFragment源码之后你会发现你根本没有看到父类有这两个变量mOnCancelListenermOnDismissListener。其实我也发现了。


这是为什么?


DialogFragment的源码包是依赖在appcompat中的,它的版本有好几个.


appcompat_versions.png


当你引用低于1.3.0的版本是不适用于我这个解决办法的。当你高于1.3.0版本是可以使用的,当然你也可以单独引Fragment的依赖只要高于1.3.0就行。


appcompat:1.2.0 的源码

Snipaste_2021-09-27_18-04-50.png


在1.2.0,只能在onActivityCreated中重新设置两个监听器来减少内存泄露出现的概率


 override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
if (isLowVersion) {
Log.d("Dboy", "低版本中重新替换覆盖")
if (mOnCancelListenerImp == null) {
mOnCancelListenerImp = OnCancelListenerImp(this)
}
dialog?.setOnCancelListener(mOnCancelListenerImp)

if (mOnDismissListenerImp == null) {
mOnDismissListenerImp = OnDismissListenerImp(this)
}
dialog?.setOnDismissListener(mOnDismissListenerImp)
}
}

appcompat:1.3.0 的源码:

dialogFragment_1.3.4_listener.png
dialogFragment_1.3.4_listener_set.png


这两个版本的差异还是比较大的。所以你直接搜的解决办法,放到你的项目里,可能因为版本不对,导致没有效果。不过我也做了替代方案。当反射失败提示找不到变量的时候,做一下标记,认为是低版本,然后再到onActivityCreated中进行一次设置。


当你引用的第三方库或者其他模块中存在不同appcompat版本的时候,打包时会使用你项目里最高版本的,所以要多注意检查是否存在依赖冲突,版本内容差异过大会直接报错的。


加一下混淆

差点忘了最重要的,既然是反射,当然少不了混淆文件了。我们只需要保证在混淆编译的时候,DialogFragment中这两个变量mOnCancelListenermOnCancelListener不被混淆就可以了。


在你项目的proguard-rules.pro中加入这个规则:


-keepnames class androidx.fragment.app.DialogFragment{
private ** mOnCancelListener;
private ** mOnDismissListener;
}

后言


在我解决这个内存泄露的时候,当时真的是烦死我了,在网上搜索的帖子,不是复制粘贴别人的就是复制粘贴别人的。我看到某个帖子不错之后就会去找原文,我找到一篇使用弱引用解决内存泄露的文章DialogFragment引起的内存泄露 来自隔壁的。我看这位老哥最早发布的,不知道老哥是不是原创作者,如果是还是很厉害的。我也是从中学习到了。虽然我的解决办法是从他那里学到的,但是我不会复制粘贴别人的文章,不能做技术的盗窃者。我也不会使用别人的代码,我喜欢自己动手写,这样能在写代码中学到更多东西。


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

图解 ArrayDeque 比 LinkedList 快

接口 Deque 的子类 ArrayDeque ,作为栈使用时比 Stack 快,因为原来的 Java 的 Stack 继承自 Vector,而 Vector 在每个方法中都加了锁,而 Deque 的子类 ArrayDeque 并没有锁的开销。 接口 Dequ...
继续阅读 »

接口 Deque 的子类 ArrayDeque ,作为栈使用时比 Stack 快,因为原来的 Java 的 Stack 继承自 Vector,而 Vector 在每个方法中都加了锁,而 Deque 的子类 ArrayDeque 并没有锁的开销。


接口 Deque 还有另外一个子类 LinkedListLinkedList 基于双向链表实现的双端队列,ArrayDeque 作为队列使用时可能比 LinkedList 快。


而这篇文章主要来分析,为什么 ArrayDequeLinkedList 快。在开始分析之前,我们需要简单的了解一下它们的数据结构的特点。


接口 Deque


接口 Deque 继承自 Queue 即队列, 在 Java 中队列有两种形式,单向队列( AbstractQueue ) 和 双端队列( Deque ),单向队列效果如下所示,只能从一端进入,另外一端出去。



而今天主要介绍双端队列( Deque ), Deque 是双端队列的线性数据结构, 可以在两端进行插入和删除操作,效果如下所示。



双端队列( Deque )的子类分别是 ArrayDequeLinkedListArrayDeque 基于数组实现的双端队列,而 LinkedList 基于双向链表实现的双端队列,它们的继承关系如下图所示。



接口 DequeQueue 提供了两套 API ,存在两种形式,分别为抛出异常,和不抛出异常,返回一个特殊值 null 或者布尔值 ( true | false )。



























操作类型抛出异常返回特殊值
插入addXXX(e)offerXXX(e)
移除removeXXX()pollXXX()
查找element()peekXXX()

ArrayDeque


ArrayDeque 是基于(循环)数组的方式实现双端队列,数组初始化容量为 16(JDK 8),结构图如下所示。



ArrayDeque 具有以下特点:



  • 因为双端队列只能在头部和尾部插入或者删除元素,所以时间复杂度为 O(1),但是在扩容的时候需要批量移动元素,其时间复杂度为 O(n)

  • 扩容的时候,将数组长度扩容为原来的 2 倍,即 n << 1

  • 数组采用连续的内存地址空间,所以查询的时候,时间复杂度为 O(1)

  • 它是非线程安全的集合


LinkedList


LinkedList 基于双向链表实现的双端队列,它的结构图如下所示。



LinkedList 具有以下特点:



  • LinkedList 是基于双向链表的结构来存储元素,所以长度没有限制,因此不存在扩容机制

  • 由于链表的内存地址是非连续的,所以只能从头部或者尾部查找元素,查询的时间复杂为 O(n),但是 JDK 对 LinkedList 做了查找优化,当我们查找某个元素时,若 index < (size / 2),则从 head 往后查找,否则从 tail 开始往前查找 , 但是我们在计算时间复杂度的时候,常数项可以省略,故时间复杂度 O(n)


Node<E> node(int index) {
// size >> 1 等价于 size / 2
if (index < (size >> 1)) {
// form head to tail
} else {
// form tail to head
}
}


  • 链表通过指针去访问各个元素,所以插入、删除元素只需要更改指针指向即可,因此插入、删除的时间复杂度 O(1)

  • 它是非线程安全的集合


最后汇总一下 ArrayDequeLinkedList 的特点如下所示:































集合类型数据结构初始化及扩容插入/删除时间复杂度查询时间复杂度是否是线程安全
ArrqyDeque循环数组初始化:16
扩容:2 倍
0(n)0(1)
LinkedList双向链表0(1)0(n)

为什么 ArrayDeque 比 LinkedList 快


了解完数据结构特点之后,接下来我们从两个方面分析为什么 ArrayDeque 作为队列使用时可能比 LinkedList 快。




  • 从速度的角度:ArrayDeque 基于数组实现双端队列,而 LinkedList 基于双向链表实现双端队列,数组采用连续的内存地址空间,通过下标索引访问,链表是非连续的内存地址空间,通过指针访问,所以在寻址方面数组的效率高于链表。




  • 从内存的角度:虽然 LinkedList 没有扩容的问题,但是插入元素的时候,需要创建一个 Node 对象, 换句话说每次都要执行 new 操作,当执行 new 操作的时候,其过程是非常慢的,会经历两个过程:类加载过程 、对象创建过程。




    • 类加载过程



      • 会先判断这个类是否已经初始化,如果没有初始化,会执行类的加载过程

      • 类的加载过程:加载、验证、准备、解析、初始化等等阶段,之后会执行 <clinit>() 方法,初始化静态变量,执行静态代码块等等




    • 对象创建过程



      • 如果类已经初始化了,直接执行对象的创建过程

      • 对象的创建过程:在堆内存中开辟一块空间,给开辟空间分配一个地址,之后执行初始化,会执行 <init>() 方法,初始化普通变量,调用普通代码块






接下来我们通过 算法动画图解 | 被 "废弃" 的 Java 栈,为什么还在用 文章中 LeetCode 算法题:有效的括号,来验证它们的执行速度,以及在内存方面的开销,代码如下所示:


class Solution {
public boolean isValid(String s) {

// LinkedList VS ArrayDeque

// Deque<Character> stack = new LinkedList<Character>();
Deque<Character> stack = new ArrayDeque<Character>();

// 开始遍历字符串
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
// 遇到左括号,则将其对应的右括号压入栈中
if (c == '(') {
stack.push(')');
} else if (c == '[') {
stack.push(']');
} else if (c == '{') {
stack.push('}');
} else {
// 遇到右括号,判断当前元素是否和栈顶元素相等,不相等提前返回,结束循环
if (stack.isEmpty() || stack.poll() != c) {
return false;
}
}
}
// 通过判断栈是否为空,来检查是否是有效的括号
return stack.isEmpty();
}
}

正如你所看到的,核心算法都是一样的,通过接口 Deque 来访问,只是初始化接口 Deque 代码不一样。


// 通过 LinkedList 初始化     
Deque<Character> stack = new LinkedList<Character>();

// 通过 ArrayDeque 初始化
Deque<Character> stack = new ArrayDeque<Character>();


结果如上所示,无论是在执行速度、还是在内存开销上 ArrayDeque 的性能都比 LinkedList 要好。



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

iOS RXSwift 5.9

iOS
materialize将序列产生的事件,转换成元素通常,一个有限的 Observable 将产生零个或者多个 onNext 事件,然后产生一个 onCompleted 或者 onError&...
继续阅读 »

materialize

将序列产生的事件,转换成元素

通常,一个有限的 Observable 将产生零个或者多个 onNext 事件,然后产生一个 onCompleted 或者 onError 事件。

materialize 操作符将 Observable 产生的这些事件全部转换成元素,然后发送出来。

merge

将多个 Observables 合并成一个

通过使用 merge 操作符你可以将多个 Observables 合并成一个,当某一个 Observable 发出一个元素时,他就将这个元素发出。

如果,某一个 Observable 发出一个 onError 事件,那么被合并的 Observable 也会将它发出,并且立即终止序列。


演示

let disposeBag = DisposeBag()

let subject1 = PublishSubject<String>()
let subject2 = PublishSubject<String>()

Observable.of(subject1, subject2)
.merge()
.subscribe(onNext: { print($0) })
.disposed(by: disposeBag)

subject1.onNext("🅰️")

subject1.onNext("🅱️")

subject2.onNext("①")

subject2.onNext("②")

subject1.onNext("🆎")

subject2.onNext("③")

输出结果:

🅰️
🅱️


🆎

map

通过一个转换函数,将 Observable 的每个元素转换一遍

map 操作符将源 Observable 的每个元素应用你提供的转换方法,然后返回含有转换结果的 Observable


演示

let disposeBag = DisposeBag()
Observable.of(1, 2, 3)
.map { $0 * 10 }
.subscribe(onNext: { print($0) })
.disposed(by: disposeBag)

输出结果:

10
20
30

参考

just

创建 Observable 发出唯一的一个元素

just 操作符将某一个元素转换为 Observable


演示

一个序列只有唯一的元素 0

let id = Observable.just(0)

它相当于:

let id = Observable<Int>.create { observer in
observer.onNext(0)
observer.onCompleted()
return Disposables.create()
}

interval

创建一个 Observable 每隔一段时间,发出一个索引数

interval 操作符将创建一个 Observable,它每隔一段设定的时间,发出一个索引数的元素。它将发出无数个元素。

ignoreElements

忽略掉所有的元素,只发出 error 或 completed 事件

ignoreElements 操作符将阻止 Observable 发出 next 事件,但是允许他发出 error 或 completed 事件。

如果你并不关心 Observable 的任何元素,你只想知道 Observable 在什么时候终止,那就可以使用 ignoreElements 操作符。

收起阅读 »

iOS RXSwift 5.8

iOS
groupBy将源 Observable 分解为多个子 Observable,并且每个子 Observable 将源 Observable 中“相似”的元素发送出来groupBy ...
继续阅读 »

groupBy

将源 Observable 分解为多个子 Observable,并且每个子 Observable 将源 Observable 中“相似”的元素发送出来

groupBy 操作符将源 Observable 分解为多个子 Observable,然后将这些子 Observable 发送出来。

它会将元素通过某个键进行分组,然后将分组后的元素序列以 Observable 的形态发送出来。

from

将其他类型或者数据结构转换为 Observable

当你在使用 Observable 时,如果能够直接将其他类型转换为 Observable,这将是非常省事的。from 操作符就提供了这种功能。


演示

将一个数组转换为 Observable

let numbers = Observable.from([0, 1, 2])

它相当于:

let numbers = Observable<Int>.create { observer in
observer.onNext(0)
observer.onNext(1)
observer.onNext(2)
observer.onCompleted()
return Disposables.create()
}

将一个可选值转换为 Observable

let optional: Int? = 1
let value = Observable.from(optional: optional)

它相当于:

let optional: Int? = 1
let value = Observable<Int>.create { observer in
if let element = optional {
observer.onNext(element)
}
observer.onCompleted()
return Disposables.create()
}

flatMapLatest

将 Observable 的元素转换成其他的 Observable,然后取这些 Observables 中最新的一个

flatMapLatest 操作符将源 Observable 的每一个元素应用一个转换方法,将他们转换成 Observables。一旦转换出一个新的 Observable,就只发出它的元素,旧的 Observables 的元素将被忽略掉。


演示

tips:与 flatMap 比较更容易理解

let disposeBag = DisposeBag()
let first = BehaviorSubject(value: "👦🏻")
let second = BehaviorSubject(value: "🅰️")
let variable = Variable(first)

variable.asObservable()
.flatMapLatest { $0 }
.subscribe(onNext: { print($0) })
.disposed(by: disposeBag)

first.onNext("🐱")
variable.value = second
second.onNext("🅱️")
first.onNext("🐶")

输出结果:

👦🏻
🐱
🅰️
🅱️

flatMap

将 Observable 的元素转换成其他的 Observable,然后将这些 Observables 合并

flatMap 操作符将源 Observable 的每一个元素应用一个转换方法,将他们转换成 Observables。 然后将这些 Observables 的元素合并之后再发送出来。

这个操作符是非常有用的,例如,当 Observable 的元素本身拥有其他的 Observable 时,你可以将所有 Observables 的元素发送出来。


演示

let disposeBag = DisposeBag()
let first = BehaviorSubject(value: "👦🏻")
let second = BehaviorSubject(value: "🅰️")
let variable = Variable(first)

variable.asObservable()
.flatMap { $0 }
.subscribe(onNext: { print($0) })
.disposed(by: disposeBag)

first.onNext("🐱")
variable.value = second
second.onNext("🅱️")
first.onNext("🐶")

输出结果:

👦🏻
🐱
🅰️
🅱️
🐶
收起阅读 »

iOS RXSwift 5.7

iOS
filter仅仅发出 Observable 中通过判定的元素filter 操作符将通过你提供的判定方法过滤一个 Observable。演示let disposeBag = DisposeBag() Observable...
继续阅读 »

filter

仅仅发出 Observable 中通过判定的元素

filter 操作符将通过你提供的判定方法过滤一个 Observable


演示

let disposeBag = DisposeBag()

Observable.of(2, 30, 22, 5, 60, 1)
.filter { $0 > 10 }
.subscribe(onNext: { print($0) })
.disposed(by: disposeBag)

输出结果:

30
22
60

error

创建一个只有 error 事件的 Observable

error 操作符将创建一个 Observable,这个 Observable 只会产生一个 error 事件。


演示

创建一个只有 error 事件的 Observable

let error: Error = ...
let id = Observable<Int>.error(error)

它相当于:

let error: Error = ...
let id = Observable<Int>.create { observer in
observer.onError(error)
return Disposables.create()
}

elementAt

只发出 Observable 中的第 n 个元素

elementAt 操作符将拉取 Observable 序列中指定索引数的元素,然后将它作为唯一的元素发出。


演示

let disposeBag = DisposeBag()

Observable.of("🐱", "🐰", "🐶", "🐸", "🐷", "🐵")
.elementAt(3)
.subscribe(onNext: { print($0) })
.disposed(by: disposeBag)

输出结果:

🐸


empty

创建一个空 Observable

empty 操作符将创建一个 Observable,这个 Observable 只有一个完成事件。


演示

创建一个空 Observable

let id = Observable<Int>.empty()

它相当于:

let id = Observable<Int>.create { observer in
observer.onCompleted()
return Disposables.create()
}
收起阅读 »

iOS RXSwift 5.6

iOS
delay将 Observable 的每一个元素拖延一段时间后发出delay 操作符将修改一个 Observable,它会将 Observable 的所有元素都拖延一段设定好的时间, 然后才将它们发送...
继续阅读 »

delay

将 Observable 的每一个元素拖延一段时间后发出

delay 操作符将修改一个 Observable,它会将 Observable 的所有元素都拖延一段设定好的时间, 然后才将它们发送出来。


delaySubscription

进行延时订阅

delaySubscription 操作符将在经过所设定的时间后,才对 Observable 进行订阅操作。


dematerialize

dematerialize 操作符将 materialize 转换后的元素还原


distinctUntilChanged

阻止 Observable 发出相同的元素

distinctUntilChanged 操作符将阻止 Observable 发出相同的元素。如果后一个元素和前一个元素是相同的,那么这个元素将不会被发出来。如果后一个元素和前一个元素不相同,那么这个元素才会被发出来。


演示

let disposeBag = DisposeBag()

Observable.of("🐱", "🐷", "🐱", "🐱", "🐱", "🐵", "🐱")
.distinctUntilChanged()
.subscribe(onNext: { print($0) })
.disposed(by: disposeBag)

输出结果:

🐱
🐷
🐱
🐵
🐱



do

当 Observable 产生某些事件时,执行某个操作

当 Observable 的某些事件产生时,你可以使用 do 操作符来注册一些回调操作。这些回调会被单独调用,它们会和 Observable 原本的回调分离。

收起阅读 »

Android -activity的布局加载流程

Activity 布局加载的流程首先在onCreate通过setContentView设置布局protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstan...
继续阅读 »

Activity 布局加载的流程

首先在onCreate通过setContentView设置布局

protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}

进入Activity中

public void setContentView(@LayoutRes int layoutResID) {
//实际调用的是PhoneWindow.setContentView , PhoneWindow是window的唯一实现类
getWindow().setContentView(layoutResID);
initWindowDecorActionBar();
}
public Window getWindow() {
return mWindow;
}

image.png

找到PhoneWindow中的setContentView方法

public void setContentView(int layoutResID) {
//因为我们当前是窗体初始化,所以mContentParent肯定为空
if (mContentParent == null) {
//初始化顶层布局
installDecor();
} else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
mContentParent.removeAllViews();
}

if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
getContext());
transitionTo(newScene);
} else {
//去加载我们自定义的layout
mLayoutInflater.inflate(layoutResID, mContentParent);
}
mContentParent.requestApplyInsets();
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
cb.onContentChanged();
}
mContentParentExplicitlySet = true;
}

PhoneWindow里面有两个比较重要的参数:

DecorView:
这是窗口的顶层试图,它可以包含所有的窗口装饰
ViewGroup:
布局容器。放置窗口内容的视图。要么放置DecorView本生,要么防止内容所在的
DecorView的子级

初始化顶层布局:

private void installDecor() {
mForceDecorInstall = false;
if (mDecor == null) {
//初始化 mDecor
mDecor = generateDecor(-1);
mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
mDecor.setIsRootNamespace(true);
if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
}
} else {
mDecor.setWindow(this);
}
if (mContentParent == null) {
//初始化mContentParent
mContentParent = generateLayout(mDecor);
....
}
}

进入generateDecor方法其实啥事都没有干只是new了一个DecorView出来


protected DecorView generateDecor(int featureId) {
// System process doesn't have application context and in that case we need to directly use
// the context we have. Otherwise we want the application context, so we don't cling to the
// activity.
Context context;
if (mUseDecorContext) {
Context applicationContext = getContext().getApplicationContext();
if (applicationContext == null) {
context = getContext();
} else {
context = new DecorContext(applicationContext, getContext());
if (mTheme != -1) {
context.setTheme(mTheme);
}
}
} else {
context = getContext();
}
return new DecorView(context, featureId, this, getAttributes());
}

进入generateLayout方法

protected ViewGroup generateLayout(DecorView decor) {
//做一些窗体样式的判断

//给窗体进行装饰
int layoutResource;
int features = getLocalFeatures();
// System.out.println("Features: 0x" + Integer.toHexString(features));
//加载系统布局 判断到底是加载那个布局
if ((features & (1 << FEATURE_SWIPE_TO_DISMISS)) != 0) {
layoutResource = R.layout.screen_swipe_dismiss;
setCloseOnSwipeEnabled(true);
}

mDecor.startChanging();
//将加载到的基础布局添加到mDecor中
mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);
//通过系统的content的资源ID去进行实例化这个控件
ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
if (contentParent == null) {
throw new RuntimeException("Window couldn't find content container view");
}


}

找一个比较简单的布局screen_simple.xml看一下

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
android:orientation="vertical">
<ViewStub android:id="@+id/action_mode_bar_stub"
android:inflatedId="@+id/action_mode_bar"
android:layout="@layout/action_mode_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="?attr/actionBarTheme" />
<FrameLayout
android:id="@android:id/content"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:foregroundInsidePadding="false"
android:foregroundGravity="fill_horizontal|top"
android:foreground="?android:attr/windowContentOverlay" />
</LinearLayout>

进入到DecorView里面

void onResourcesLoaded(LayoutInflater inflater, int layoutResource) {
if (mBackdropFrameRenderer != null) {
loadBackgroundDrawablesIfNeeded();
mBackdropFrameRenderer.onResourcesLoaded(
this, mResizingBackgroundDrawable, mCaptionBackgroundDrawable,
mUserCaptionBackgroundDrawable, getCurrentColor(mStatusColorViewState),
getCurrentColor(mNavigationColorViewState));
}

mDecorCaptionView = createDecorCaptionView(inflater);
//把传进来的layoutResource布局进行解析并渲染
final View root = inflater.inflate(layoutResource, null);
if (mDecorCaptionView != null) {
if (mDecorCaptionView.getParent() == null) {
addView(mDecorCaptionView,
new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
}
mDecorCaptionView.addView(root,
new ViewGroup.MarginLayoutParams(MATCH_PARENT, MATCH_PARENT));
} else {

// Put it below the color views.
addView(root, 0, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
}
mContentRoot = (ViewGroup) root;
initializeElevation();
}

加载用户的资源文件


if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
getContext());
transitionTo(newScene);
} else {
//加载用户的xml文件
mLayoutInflater.inflate(layoutResID, mContentParent);
}

进入LayoutInflater类中的inflate方法

public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) {
return inflate(resource, root, root != null);
}

调用inflate方法

public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
final Resources res = getContext().getResources();
if (DEBUG) {
Log.d(TAG, "INFLATING from resource: "" + res.getResourceName(resource) + "" ("
+ Integer.toHexString(resource) + ")");
}
//解析xml文件
final XmlResourceParser parser = res.getLayout(resource);
try {
return inflate(parser, root, attachToRoot);
} finally {
parser.close();
}
}
if (root != null && attachToRoot) {
//把自定义的布局解析完之后添加到mContentParent中
root.addView(temp, params);
}

至此Activity加载布局的大致流程已经分析完成。 最后上一下流程图:

Android-activity的UI绘制流程.jpg

收起阅读 »

设计模式-代理模式(Proxy Pattern)

定义为其他对象提供一种代理以控制对这个对象的访问按照代理的创建时期,代理类可以分为两种: 静态代理:由程序员创建代理类或特定工具自动生成源代码再对其编译。在程序运行前代理类的.class文件就已经存在了。动态代理:在程序运行时运用反射机制动态创建而成...
继续阅读 »

定义

为其他对象提供一种代理以控制对这个对象的访问

按照代理的创建时期,代理类可以分为两种: 

  • 静态代理:由程序员创建代理类或特定工具自动生成源代码再对其编译。在程序运行前代理类的.class文件就已经存在了。

  • 动态代理:在程序运行时运用反射机制动态创建而成。

使用场景

主要作用:控制对象访问

  • 扩展目标对象的功能:例如演员(目标对象),有演戏的功能,找一个经纪人(代理),会额外提供收费的功能,实际上是代理的功能,而不是演员的功能。
  • 限制目标对象的功能:例如经纪人对收费不满意,只让演员演一场戏,对演员的功能进行了部分限制。

类图

  • Subject:抽象主题角色,主要是声明代理类和被代理类共同的接口方法
  • RealSubject:具体主题角色(被代理角色),执行具体的业务逻辑
  • Proxy:代理类,持有一个被代理对象的引用,负责在被代理对象方法调用的前后做一些额外操作

4、优点

  • 职责清晰,被代理角色只实现实际的业务逻辑,代理对象实现附加的处理逻辑
  • 扩展性高,可以更换不同的代理类,实现不同的代理逻辑

静态代理

编译时期就已经存在,一般首先需要定义接口,而被代理的对象和代理对象一起实现相同的接口。

1、接口定义:

public interface Play {
//唱歌
void sing(int count);
//演出
void show();
}

2、演员(被代理对象):

public class Actor implements Play {
@Override
public void sing(int count) {
System.out.print("唱了" + count + "首歌");
}

@Override
public void show() {
System.out.print("进行演出");
}
}

被代理对象提供了几个具体方法实现

3、经纪人(代理对象):

public class Agent implements Play {
//被代理对象
private Play player;
private long money;

public void setMoney(long money){
this.money = money;
}

/**
* @param player
* @param money 收费
*/

public Agent(Play player, long money) {
this.player = player;
this.money = money;
}

@Override
public void sing(int count) {
player.sing(count);
}
//控制了被代理对象的访问
@Override
public void show() {
if (money > 100) {
player.show();
} else {
System.out.println("baibai...");
}
}
}

4、使用

public class PlayTest {
public static void main(String[] args){
Actor actor = new Actor();
Agent agent = new Agent(actor, 50);
agent.sing(2);
agent.show();
agent.setMoney(200);
agent.show();
}
}

代理对象通过自身的逻辑处理对目标对象的功能进行控制。

动态代理

动态一般指的是在运行时的状态,是相对编译时的静态来区分,就是在运行时生成一个代理对象帮我们做一些逻辑处理。主要使用反射技术获得类的加载器并且创建实例。
动态代理可以在运行时动态创建一个类,实现一个或多个接口,可以在不修改原有类的基础上动态为通过该类获取的对象添加方法、修改行为。

1、生成动态代理类:

InvocationHandler是动态代理接口,动态代理类需要实现该接口,并在invoke方法中对代理类的方法进行处理

public interface InvocationHandler {
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable;
}

参数说明:

  • Object proxy:被代理的对象
  • Object[] args:要调用的方法
  • Object[] args:方法调用所需要的参数

2、创建动态代理类

Proxy类可以通过newProxyInstance创建一个代理对象

public static Object newProxyInstance(ClassLoader loader,
Class<?>[] interfaces,
InvocationHandler h)

throws IllegalArgumentException {
if (h == null) {
throw new NullPointerException();
}
Class<?> cl = getProxyClass0(loader, interfaces);
try {
//通过反射完成了代理对象的创建
final Constructor<?> cons = cl.getConstructor(constructorParams);
return newInstance(cons, h);
} catch (NoSuchMethodException e) {
throw new InternalError(e.toString());
}
}

参数说明:

  • ClassLoader loader:类加载器
  • Class<?>[] interfaces:所有的接口
  • InvocationHandler h:实现InvocationHandler接口的子类

3、动态代理demo:

(1)定义动态代理类

public class ActorProxy implements InvocationHandler {
private Play player;

public ActorProxy(Play player) {
this.player = player;
}

/**
* 获取动态代理对象
*/

public Object getDynamicProxy() {
return Proxy.newProxyInstance(player.getClass().getClassLoader(), player.getClass().getInterfaces(), this);
}

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//处理被代理对象的方法实现
if ("show".equals(method.getName())) {
System.out.println("代理处理show....");
return method.invoke(player, null);
} else if ("sing".equals(method.getName())) {
System.out.println("代理处理sing....");
return method.invoke(player, 2);
}
return null;
}
}

代理类实现InvocationHandler接口,在invoke方法中对player(被代理对象)做相应的逻辑处理。

(2)使用

public class ProxyTest {

public static void main(String[] args) {

ActorProxy actorProxy = new ActorProxy(new Actor());
//通过调用Proxy.newProxyInstance方法生成代理对象
Play proxy = (Play) actorProxy.getDynamicProxy();
//调用代理类相关方法
proxy.show();
proxy.sing(3);
}
}

四、Android中的代理模式

Retrofit代理模式

(1)Retrofit使用: 定义接口

public interface MyService {
@GET("users/{user}/list")
Call<String> getMyList(@Path("user") String user);
}

新建retrofit对象,然后产生一个接口对象,然后调用具体方法去完成请求。

Retrofit retrofit = new Retrofit.Builder()
.baseUrl("http://xxx.com")
.build();
MyService myService = retrofit.create(MyService.class);
Call<String> myList = myService.getMyList("my");

retrofit.create方法就是通过动态代理的方式传入一个接口,返回了一个对象

(2)动态代理分析:

public <T> T create(final Class<T> service) {
//判断是否为接口
Utils.validateServiceInterface(service);
if (validateEagerly) {
eagerlyValidateMethods(service);
}
//创建请求接口的动态代理对象
return (T) Proxy.newProxyInstance(service.getClassLoader(), new Class<?>[] { service },
new InvocationHandler() {
private final Platform platform = Platform.get();
private final Object[] emptyArgs = new Object[0];

@Override public Object invoke(Object proxy, Method method, @Nullable Object[] args)
throws Throwable {
// If the method is a method from Object then defer to normal invocation.
if (method.getDeclaringClass() == Object.class) {
return method.invoke(this, args);
}
if (platform.isDefaultMethod(method)) {
return platform.invokeDefaultMethod(method, service, proxy, args);
}
//将接口中方法传入返回了ServiceMethod
return loadServiceMethod(method).invoke(args != null ? args : emptyArgs);
}
});
}

通过Proxy.newProxyInstance,该动态代理对象可以拿到请求接口实例上所有注解,然后通过代理对象进行网络请求。

收起阅读 »

iOS RXSwift 5.5

iOS
deferred直到订阅发生,才创建 Observable,并且为每位订阅者创建全新的 Observabledeferred 操作符将等待观察者订阅它,才创建一个 Observable,它会通过一个构建函数为每一位订阅者创建新的 Observable。看上去每...
继续阅读 »

deferred

直到订阅发生,才创建 Observable,并且为每位订阅者创建全新的 Observable

deferred 操作符将等待观察者订阅它,才创建一个 Observable,它会通过一个构建函数为每一位订阅者创建新的 Observable。看上去每位订阅者都是对同一个 Observable 产生订阅,实际上它们都获得了独立的序列。

在一些情况下,直到订阅时才创建 Observable 是可以保证拿到的数据都是最新的。

debug

打印所有的订阅,事件以及销毁信息


演示

let disposeBag = DisposeBag()

let sequence = Observable<String>.create { observer in
observer.onNext("🍎")
observer.onNext("🍐")
observer.onCompleted()
return Disposables.create()
}

sequence
.debug("Fruit")
.subscribe()
.disposed(by: disposeBag)

输出结果:

2017-11-06 20:49:43.187: Fruit -> subscribed
2017-11-06 20:49:43.188: Fruit -> Event next(🍎)
2017-11-06 20:49:43.188: Fruit -> Event next(🍐)
2017-11-06 20:49:43.188: Fruit -> Event completed
2017-11-06 20:49:43.189: Fruit -> isDisposed

debounce

过滤掉高频产生的元素

debounce 操作符将发出这种元素,在 Observable 产生这种元素后,一段时间内没有新元素产生。

收起阅读 »

iOS RXSwift 5.4

iOS
connect通知 ConnectableObservable 可以开始发出元素了ConnectableObservable 和普通的 Observable 十分相似,不过在被订阅后不会发出元素,直到 ...
继续阅读 »


connect

通知 ConnectableObservable 可以开始发出元素了

ConnectableObservable 和普通的 Observable 十分相似,不过在被订阅后不会发出元素,直到 connect 操作符被应用为止。这样一来你可以等所有观察者全部订阅完成后,才发出元素。


演示

let intSequence = Observable<Int>.interval(1, scheduler: MainScheduler.instance)
.publish()

_ = intSequence
.subscribe(onNext: { print("Subscription 1:, Event: \($0)") })

DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
_ = intSequence.connect()
}

DispatchQueue.main.asyncAfter(deadline: .now() + 4) {
_ = intSequence
.subscribe(onNext: { print("Subscription 2:, Event: \($0)") })
}

DispatchQueue.main.asyncAfter(deadline: .now() + 6) {
_ = intSequence
.subscribe(onNext: { print("Subscription 3:, Event: \($0)") })
}

输出结果:

Subscription 1:, Event: 0
Subscription 1:, Event: 1
Subscription 2:, Event: 1
Subscription 1:, Event: 2
Subscription 2:, Event: 2
Subscription 1:, Event: 3
Subscription 2:, Event: 3
Subscription 3:, Event: 3
Subscription 1:, Event: 4
Subscription 2:, Event: 4
Subscription 3:, Event: 4
Subscription 1:, Event: 5
Subscription 2:, Event: 5
Subscription 3:, Event: 5
Subscription 1:, Event: 6
Subscription 2:, Event: 6
Subscription 3:, Event: 6
...

create

通过一个构建函数完整的创建一个 Observable

create 操作符将创建一个 Observable,你需要提供一个构建函数,在构建函数里面描述事件(nexterrorcompleted)的产生过程。

通常情况下一个有限的序列,只会调用一次观察者的 onCompleted 或者 onError 方法。并且在调用它们后,不会再去调用观察者的其他方法。


演示

创建一个 [0, 1, ... 8, 9] 的序列:

let id = Observable<Int>.create { observer in
observer.onNext(0)
observer.onNext(1)
observer.onNext(2)
observer.onNext(3)
observer.onNext(4)
observer.onNext(5)
observer.onNext(6)
observer.onNext(7)
observer.onNext(8)
observer.onNext(9)
observer.onCompleted()
return Disposables.create()
}
收起阅读 »

iOS RXSwift 5.3

iOS
concat让两个或多个 Observables 按顺序串连起来concat 操作符将多个 Observables 按顺序串联起来,当前一个 Observable 元素发送完毕后,后一个&n...
继续阅读 »

concat

让两个或多个 Observables 按顺序串连起来

concat 操作符将多个 Observables 按顺序串联起来,当前一个 Observable 元素发送完毕后,后一个 Observable 才可以开始发出元素。

concat 将等待前一个 Observable 产生完成事件后,才对后一个 Observable 进行订阅。如果后一个是“热” Observable ,在它前一个 Observable 产生完成事件前,所产生的元素将不会被发送出来。

startWith 和它十分相似。但是startWith不是在后面添加元素,而是在前面插入元素。

merge 和它也是十分相似。merge并不是将多个 Observables 按顺序串联起来,而是将他们合并到一起,不需要 Observables 按先后顺序发出元素。


演示

let disposeBag = DisposeBag()

let subject1 = BehaviorSubject(value: "🍎")
let subject2 = BehaviorSubject(value: "🐶")

let variable = Variable(subject1)

variable.asObservable()
.concat()
.subscribe { print($0) }
.disposed(by: disposeBag)

subject1.onNext("🍐")
subject1.onNext("🍊")

variable.value = subject2

subject2.onNext("I would be ignored")
subject2.onNext("🐱")

subject1.onCompleted()

subject2.onNext("🐭")

输出结果:

next(🍎)
next(🍐)
next(🍊)
next(🐱)
next(🐭)


concatMap

将 Observable 的元素转换成其他的 Observable,然后将这些 Observables 串连起来

concatMap 操作符将源 Observable 的每一个元素应用一个转换方法,将他们转换成 Observables。然后让这些 Observables 按顺序的发出元素,当前一个 Observable 元素发送完毕后,后一个 Observable 才可以开始发出元素。等待前一个 Observable 产生完成事件后,才对后一个 Observable 进行订阅。


演示

let disposeBag = DisposeBag()

let subject1 = BehaviorSubject(value: "🍎")
let subject2 = BehaviorSubject(value: "🐶")

let variable = Variable(subject1)

variable.asObservable()
.concatMap { $0 }
.subscribe { print($0) }
.disposed(by: disposeBag)

subject1.onNext("🍐")
subject1.onNext("🍊")

variable.value = subject2

subject2.onNext("I would be ignored")
subject2.onNext("🐱")

subject1.onCompleted()

subject2.onNext("🐭")

输出结果:

next(🍎)
next(🍐)
next(🍊)
next(🐱)
next(🐭)
收起阅读 »

iOS RXSwift 5.2

iOS
buffer缓存元素,然后将缓存的元素集合,周期性的发出来buffer 操作符将缓存 Observable 中发出的新元素,当元素达到某个数量,或者经过了特定的时间,它就会将这个元素集合发送出来。catchError从一个错误事件...
继续阅读 »

buffer

缓存元素,然后将缓存的元素集合,周期性的发出来

buffer 操作符将缓存 Observable 中发出的新元素,当元素达到某个数量,或者经过了特定的时间,它就会将这个元素集合发送出来。

catchError

从一个错误事件中恢复,将错误事件替换成一个备选序列

catchError 操作符将会拦截一个 error 事件,将它替换成其他的元素或者一组元素,然后传递给观察者。这样可以使得 Observable 正常结束,或者根本都不需要结束。

这里存在其他版本的 catchError 操作符。


演示

let disposeBag = DisposeBag()

let sequenceThatFails = PublishSubject<String>()
let recoverySequence = PublishSubject<String>()

sequenceThatFails
.catchError {
print("Error:", $0)
return recoverySequence
}
.subscribe { print($0) }
.disposed(by: disposeBag)

sequenceThatFails.onNext("😬")
sequenceThatFails.onNext("😨")
sequenceThatFails.onNext("😡")
sequenceThatFails.onNext("🔴")
sequenceThatFails.onError(TestError.test)

recoverySequence.onNext("😊")

输出结果:

next(😬)
next(😨)
next(😡)
next(🔴)
Error: test
next(😊)

catchErrorJustReturn

catchErrorJustReturn 操作符会将error 事件替换成其他的一个元素,然后结束该序列。


演示

let disposeBag = DisposeBag()
let sequenceThatFails = PublishSubject<String>()

sequenceThatFails
.catchErrorJustReturn("😊")
.subscribe { print($0) }
.disposed(by: disposeBag)

sequenceThatFails.onNext("😬")
sequenceThatFails.onNext("😨")
sequenceThatFails.onNext("😡")
sequenceThatFails.onNext("🔴")
sequenceThatFails.onError(TestError.test)

输出结果:

next(😬)
next(😨)
next(😡)
next(🔴)
next(😊)
completed



combineLatest

当多个 Observables 中任何一个发出一个元素,就发出一个元素。这个元素是由这些 Observables 中最新的元素,通过一个函数组合起来的

combineLatest 操作符将多个 Observables 中最新的元素通过一个函数组合起来,然后将这个组合的结果发出来。这些源 Observables 中任何一个发出一个元素,他都会发出一个元素(前提是,这些 Observables 曾经发出过元素)。


演示

tips: 可与 zip 比较学习

let disposeBag = DisposeBag()

let first = PublishSubject<String>()
let second = PublishSubject<String>()

Observable.combineLatest(first, second) { $0 + $1 }
.subscribe(onNext: { print($0) })
.disposed(by: disposeBag)

first.onNext("1")
second.onNext("A")
first.onNext("2")
second.onNext("B")
second.onNext("C")
second.onNext("D")
first.onNext("3")
first.onNext("4")

输出结果:

1A
2A
2B
2C
2D
3D
4D
收起阅读 »

Compose 仅50行代码轻松定制下滑刷新

目前有一个正在进行的 Jetpack Compose中文手册 项目,旨在帮助开发者更好的理解和掌握Compose框架,目前仍还在开荒中,欢迎大家进行关注与加入! 这篇文章由本人撰写,目前文章已经发布到该手册中,欢迎进行查阅。 下滑刷新效果展...
继续阅读 »

目前有一个正在进行的 Jetpack Compose中文手册 项目,旨在帮助开发者更好的理解和掌握Compose框架,目前仍还在开荒中,欢迎大家进行关注与加入! 这篇文章由本人撰写,目前文章已经发布到该手册中,欢迎进行查阅。


下滑刷新效果展示


像下滑刷新这样涉及到嵌套滑动的手势行为就需要使用 nestedScroll 修饰符来完成。接下来,就让我们先来介绍一下 nestedScroll 修饰符是什么,并且该怎么用。





nestedScroll 修饰符


nestedScroll 修饰符主要用于处理嵌套滑动的场景,为父布局劫持消费子布局滑动手势提供了可能。


使用 nestedScroll 参数列表中有一个必选参数 connection 和一个可选参数 dispatcher


connection: 嵌套滑动手势处理的核心逻辑,内部回调可以在子布局获得滑动事件前预先消费掉部分或全部手势偏移量,也可以获取子布局消费后剩下的手势偏移量。


dispatcher:调度器,内部包含用于父布局的 NestedScrollConnection , 可以调用 dispatch* 方法来通知父布局发生滑动


fun Modifier.nestedScroll(
connection: NestedScrollConnection,
dispatcher: NestedScrollDispatcher? = null
)

NestedScrollConnection


NestedScrollConnection 提供了四个回调方法。


interface NestedScrollConnection {
fun onPreScroll(available: Offset, source: NestedScrollSource): Offset = Offset.Zero

fun onPostScroll(
consumed: Offset,
available: Offset,
source: NestedScrollSource
): Offset = Offset.Zero

suspend fun onPreFling(available: Velocity): Velocity = Velocity.Zero

suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
return Velocity.Zero
}
}

onPreScroll


方法描述:预先劫持滑动事件,消费后再交由子布局。


参数列表:



  • available:当前可用的滑动事件偏移量

  • source:滑动事件的类型


返回值:当前组件消费的滑动事件偏移量,如果不想消费可返回Offset.Zero




onPostScroll


方法描述:获取子布局处理后的滑动事件


参数列表:



  • consumed:之前消费的所有滑动事件偏移量

  • available:当前剩下还可用的滑动事件偏移量

  • source:滑动事件的类型


返回值:当前组件消费的滑动事件偏移量,如果不想消费可返回 Offset.Zero ,则剩下偏移量会继续交由当前布局的父布局进行处理




onPreFling


方法描述:获取 Fling 开始时的速度。


参数列表:



  • available:Fling 开始时的速度


返回值:当前组件消费的速度,如果不想消费可返回 Velocity.Zero




onPostFling


方法描述:获取 Fling 结束时的速度信息。


参数列表:




  • consumed:之前消费的所有速度




  • available:当前剩下还可用的速度




返回值:当前组件消费的速度,如果不想消费可返回Velocity.Zero,剩下速度会继续交由当前布局的父布局进行处理。



Fling含义:当我们手指在滑动列表时,如果是快速滑动并抬起,则列表会根据惯性继续飘一段距离后停下,这个行为就是 Fling onPreFling 在你手指刚抬起时便会回调,而 onPostFling 会在飘一段距离停下后回调。



实现下滑刷新


像下滑刷新这样涉及到嵌套滑动的手势行为就可以使用 nestedScroll 修饰符来完成。


示例介绍


在这个示例中存在着加载动画和列表数据。当我们手指向下滑时,此时如果列表顶部没有数据则会逐渐出现加载动画。与之相反,当我们手指向上滑时,此时如果加载动画还在,则加载动画逐渐向上消失,直到加载动画完全消失后,列表才会被向下滑动。


设计实现方案


为实现这个滑动刷新的需求,我们可以设计如下方案。我们首先需要将加载动画和列表数据放到一个父布局中统一管理。




  1. 当我们手指向下滑时,我们希望滑动手势首先交给子布局中的列表进行处理,如果列表已经滑到顶部说明此时滑动手势事件没有被消费,此时再交由父布局进行消费。父布局可以消费列表消费剩下的滑动手势事件(为加载动画增加偏移)。




  2. 当我们手指向上滑时,我们希望滑动手势首先被父布局消费(为加载动画减小偏移),如果加载动画本身仍未出现时,则不进行消费。然后将剩下的滑动手势交给子布局列表进行消费。




NestedScrollConnection 实现


使用 nestedScroll 修饰符最重要的就是根据自己的业务场景来定制 NestedScrollConnection 的实现,接下来我们就逐个分析 NestedScrollConnection 重的借口该如何进行实现。


实现 onPostScroll


向我们之前设计的实现方案一样,当我们手指向下滑时,我们希望滑动手势首先交给子布局中的列表进行处理,如果列表已经滑到顶部说明此时滑动手势事件没有被消费,此时再交由父布局进行消费。 onPostScroll 回调时机是符合我们的需求的。


我们首先需要判断该滑动事件是不是拖动事件,通过 available.y > 0 判断是否是下滑手势,如果都没问题时,通知加载动画增加偏移量。返回值 Offset(x = 0f, y = available.y) 意味着将剩下的所有偏移量全部消费调,不再向外层父布局继续传播了。


override fun onPostScroll(
consumed: Offset,
available: Offset,
source: NestedScrollSource
): Offset {
if (source == NestedScrollSource.Drag && available.y > 0) {
state.updateOffsetDelta(available.y)
return Offset(x = 0f, y = available.y)
} else {
return Offset.Zero
}
}

实现 onPreScroll


与上面相反,此时我们希望下滑收回加载动画,当我们手指向上滑时,我们希望滑动手势首先被父布局消费(为加载动画减小偏移),如果加载动画本身仍未出现时,则不进行消费。然后将剩下的滑动手势交给子布局列表进行消费。onPreScroll 回调时机是符合这个需求的。


我们首先需要判断该滑动事件是不是拖动事件,通过 available.y < 0 判断是否是上滑手势。此时可能加载动画本身未出现,所以需要额外进行判断。如果未出现则返回 Offset.Zero 不消费,如果出现了则返回 Offset(x = 0f, y = available.y) 进行消费。


override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
if (source == NestedScrollSource.Drag && available.y < 0) {
state.updateOffsetDelta(available.y)
return if (state.isSwipeInProgress) Offset(x = 0f, y = available.y) else Offset.Zero
} else {
return Offset.Zero
}
}

实现 onPreFling


接下来,我们需要一个松手时的吸附效果。如果拉过加载动画高度的一般则进行加载,否则就收缩回初始状态。前问我提到了 onPreFling 在松手时回调,即符合我们当前这个的场景。



即使松手时速度很慢或静止,onPreFlingonPostFling都会回调,只是速度数值很小。



这里我们只需要吸引效果,并不希望消费速度,所以返回 Velocity.Zero 即可


override suspend fun onPreFling(available: Velocity): Velocity {
if (state.indicatorOffset > height / 2) {
state.animateToOffset(height)
state.isRefreshing = true
} else {
state.animateToOffset(0.dp)
}
return Velocity.Zero
}

实现 onPreFling


由于我们的下滑刷新手势处理不涉及 onPreFling 回调时机,所以不进行额外的实现。


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

Flutter ListView懒加载(滑动不加载,停止滑动加载)

前言:为了更好的减小网络的带宽,使得列表更加流畅,我们需要了解懒加载,也称延迟加载。 面试真题:flutter如何实现懒加载? 关于上一章的登录界面,各位属实难为我了,我也在求ui小姐姐,各位点点赞给我点动力吧~ 懒加载也叫延迟加载,指的是在长网页中延迟...
继续阅读 »

前言:为了更好的减小网络的带宽,使得列表更加流畅,我们需要了解懒加载,也称延迟加载。 面试真题:flutter如何实现懒加载?


关于上一章的登录界面,各位属实难为我了,我也在求ui小姐姐,各位点点赞给我点动力吧~


5e3c9f11dc5c8d53c46907cf16e2b5e.jpg
ca.png


image.png


懒加载也叫延迟加载,指的是在长网页中延迟加载图像,是一种很好优化网页性能的方式。用户滚动到它们之前,可视区域外的图像不会加载。这与图像预加载相反,在长网页上使用延迟加载将使网页加载更快。在某些情况下,它还可以帮助减少服务器负载。常适用图片很多,页面很长的电商网站场景中。


对ListView优化就那么几点:(后面都会写出来):


1.Flutter ListView加载图片优化(懒加载)


2.Flutter ListView加载时使用图片使用缩略图,对图片进行缓存


3.Flutter 减少build()的耗时


本章,我们会实现wechat朋友圈的优化功能,即当页面在滑动时不加载图片,在界面停止滑动时加载图片。

效果图:

tt0.top-288153.gif


1.了解widget通知监听:NotificationListener


NotificationListener属性:




  • child:widget



  • onNotification:NotificationListenerCallback<Notification>

    返回值true表示消费掉当前通知不再向上一级NotificationListener传递通知,false则会再向上一级NotificationListener传递通知;这里需要注意的是通知是由下而上去传递的,所以才会称作冒泡通知!




2.需要一个bool来控制是否加载


///加载图片的标识
bool isLoadingImage = true;

3.编写传递通知的方法,使其作用于NotificationListener


bool notificationFunction(Notification notification) {
 ///通知类型
 switch (notification.runtimeType) {
   case ScrollStartNotification:
     print("开始滚动");

     ///在这里更新标识 刷新页面 不加载图片
     isLoadingImage = false;
     break;
   case ScrollUpdateNotification:
     print("正在滚动");
     break;
   case ScrollEndNotification:
     print("滚动停止");

     ///在这里更新标识 刷新页面 加载图片
     setState(() {
       isLoadingImage = true;
    });
     break;
   case OverscrollNotification:
     print("滚动到边界");
     break;
}
 return true;
}

4.根据bool值加载不同的组件


ListView buildListView() {
 return ListView.separated(
   itemCount: 1000, //子条目个数
   ///构建每个条目
   itemBuilder: (BuildContext context, int index) {
     if (isLoadingImage) {
       ///这时将子条目单独封装在了一个StatefulWidget中
       return Image.network(
         netImageUrl,
         width: 100,
         height: 100,
         fit: BoxFit.fitHeight,
      );
    } else {
       return Container(
         height: 100,
         width: 100,
         child: Text("加载中..."),
      ); //占位
    }
  },

   ///构建每个子Item之间的间隔Widget
   separatorBuilder: (BuildContext context, int index) {
     return new Divider();
  },
);
}

完整代码:


class ScrollHomePageState extends State {
 ///加载图片的标识
 bool isLoadingImage = true;

 ///网络图片地址
 String netImageUrl =
     "https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0a4ce25d48b8405cbf5444b6195928d4~tplv-k3u1fbpfcp-no-mark:0:0:0:0.awebp";

 @override
 Widget build(BuildContext context) {
   return Scaffold(
     appBar: new AppBar(
       title: Text("详情"),
    ),
     ///列表
     body: NotificationListener(
       ///子Widget中的滚动组件滑动时就会分发滚动通知
       child: buildListView(),
       ///每当有滑动通知时就会回调此方法
       onNotification: notificationFunction,
    ),
  );
}

 bool notificationFunction(Notification notification) {
   ///通知类型
   switch (notification.runtimeType) {
     case ScrollStartNotification:
       print("开始滚动");

       ///在这里更新标识 刷新页面 不加载图片
       isLoadingImage = false;
       break;
     case ScrollUpdateNotification:
       print("正在滚动");
       break;
     case ScrollEndNotification:
       print("滚动停止");

       ///在这里更新标识 刷新页面 加载图片
       setState(() {
         isLoadingImage = true;
      });
       break;
     case OverscrollNotification:
       print("滚动到边界");
       break;
  }
   return true;
}

 ListView buildListView() {
   return ListView.separated(
     itemCount: 1000, //子条目个数
     ///构建每个条目
     itemBuilder: (BuildContext context, int index) {
       if (isLoadingImage) {
         ///这时将子条目单独封装在了一个StatefulWidget中
         return Image.network(
           netImageUrl,
           width: 100,
           height: 100,
           fit: BoxFit.fitHeight,
        );
      } else {
         return Container(
           height: 100,
           width: 100,
           child: Text("加载中..."),
        ); //占位
      }
    },

     ///构建每个子Item之间的间隔Widget
     separatorBuilder: (BuildContext context, int index) {
       return new Divider();
    },
  );
}
}

是不是很简单,但是懒加载确实是面试真题,你了解了吗?


ad.png


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

为什么 Compose 没有布局嵌套问题?

前言 做过布局性能优化的同学都知道,为了优化界面加载速度,要尽可能的减少布局的层级。这主要是因为布局层级的增加,可能会导致测量时间呈指数级增长。 而Compose却没有这个问题,它从根本上解决了布局层级对布局性能的影响: Compose界面只允许一次测量。这意...
继续阅读 »

前言


做过布局性能优化的同学都知道,为了优化界面加载速度,要尽可能的减少布局的层级。这主要是因为布局层级的增加,可能会导致测量时间呈指数级增长。

Compose却没有这个问题,它从根本上解决了布局层级对布局性能的影响: Compose界面只允许一次测量。这意味着随着布局层级的加深,测量时间也只是线性增长的.

下面我们就一起来看看Compose到底是怎么只测量一次就把活给干了的,本文主要包括以下内容:



  1. 布局层级过深为什么影响性能?

  2. Compose为什么没有布局嵌套问题?

  3. Compose测量过程源码分析


1. 布局层级过深为什么影响性能?


我们总说布局层级过深会影响性能,那么到底是怎么影响的呢?主要是因为在某些情况下ViewGroup会对子View进行多次测量

举个例子


<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:orientation="vertical">

<View
android:layout_width="match_parent"
android:layout_height="100dp"
android:background="@android:color/holo_red_dark" />

<View
android:layout_width="100dp"
android:layout_height="100dp"
android:background="@android:color/black" />
</LinearLayout>


  1. LinearLayout宽度为wrap_content,因此它将选择子View的最大宽度为其最后的宽度

  2. 但是有个子View的宽度为match_parent,意思它将以LinearLayout的宽度为宽度,这就陷入死循环了

  3. 因此这时候, LinearLayout 就会先以0为强制宽度测量一下子View,并正常地测量剩下的其他子View,然后再用其他子View里最宽的那个的宽度,二次测量这个match_parent的子 View,最终得出它的尺寸,并把这个宽度作为自己最终的宽度。

  4. 这是对单个子View的二次测量,如果有多个子View写了match_parent ,那就需要对它们每一个都进行二次测量。

  5. 除此之外,如果在LinearLayout中使用了weight会导致测量3次甚至更多,重复测量在Android中是很常见的


上面介绍了为什么会出现重复测量,那么会有什么影响呢?不过是多测量了几次,会对性能有什么大的影响吗?

之所以需要避免布局层级过深是因为它对性能的影响是指数级的



  1. 如果我们的布局有两层,其中父View会对每个子View做二次测量,那它的每个子View一共需要被测量 2 次

  2. 如果增加到三层,并且每个父View依然都做二次测量,这时候最下面的子View被测量的次数就直接翻倍了,变成 4 次

  3. 同理,增加到 4 层的话会再次翻倍,子 View 需要被测量 8 次



也就是说,对于会做二次测量的系统,层级加深对测量时间的影响是指数级的,这就是Android官方文档建议我们减少布局层级的原因


2. Compose为什么没有布局嵌套问题?


我们知道,Compose只允许测量一次,不允许重复测量。

如果每个父组件对每个子组件只测量一次,那就直接意味着界面中的每个组件只会被测量一次



这样即使布局层级加深,测量时间却没有增加,把组件加载的时间复杂度从O(2ⁿ) 降到了 O(n)


那么问题就来了,上面我们已经知道,多次测量有时是必要的,但是为什么Compose不需要呢?

Compose中引入了固有特性测量(Intrinsic Measurement)


固有特性测量即Compose允许父组件在对子组件进行测量之前,先测量一下子组件的「固有尺寸」

我们上面说的,ViewGroup的二次测量,也是先进行这种「粗略测量」再进行最终的「正式测量」,使用固有特性测量可以产生同样的效果


而使用固有特性测量之所以有性能优势,主要是因为其不会随着层级的加深而加倍,固有特性测量也只进行一次

Compose会先对整个组件树进行一次Intrinsic测量,然后再对整体进行正式的测量。这样开辟两个平行的测量过程,就可以避免因为层级增加而对同一个子组件反复测量所导致的测量时间的不断加倍了。



总结成一句话就是,在Compose里疯狂嵌套地写界面,和把所有组件全都写进同一层里面,性能是一样的!所以Compose没有布局嵌套问题


2.1 固有特性测量使用


假设我们需要创建一个可组合项,该可组合项在屏幕上显示两个用分隔线隔开的文本,如下所示:


p14.png


为了实现分隔线与最高的文本一样高,我们可以怎么做呢?


@Composable
fun TwoTexts(
text1: String,
text2: String,
modifier: Modifier = Modifier
) {
Row(modifier = modifier.height(IntrinsicSize.Min)) {
Text(
modifier = Modifier
.weight(1f)
.padding(start = 4.dp)
.wrapContentWidth(Alignment.Start),
text = text1
)
Divider(
color = Color.Black,
modifier = Modifier
.fillMaxHeight()
.width(1.dp)
)
Text(
modifier = Modifier
.weight(1f)
.padding(end = 4.dp)
.wrapContentWidth(Alignment.End),
text = text2
)
}
}

注意,这里给Rowheight设置为了IntrinsicSize.Min,IntrinsicSize.Min会递归查询它子项的最小高度,其中两个Text的最小高度即文本的宽度,而Divider的最小高度为0
因此最后Row的高度即为最长的文本的高度,而Divider的高度为fillMaxHeight,也就跟最高的文本一样高了

如果我们这里不设置高度为IntrinsicSize.Min的话,Divider的高度是占满屏幕的,如下所示


p13.png


3. Compose测量过程源码分析


上面我们介绍了固有特性测量是什么,及固有特性测量的使用,下面我们来看看Compose的测量究竟是怎么实现的


3.1 测量入口


我们知道,在Compose中自定义Layout是通过Layout方法实现的


@Composable inline fun Layout(
content: @Composable () -> Unit,
modifier: Modifier = Modifier,
measurePolicy: MeasurePolicy
)

主要传入3个参数



  1. content:自定义布局的子项,我们后续需要对它们测量和定位

  2. modifier: 对Layout添加的一些修饰modifier

  3. measurePolicy: 即测量规则,这个是我们主要需要处理的地方


measurePolicy中主要有五个接口


fun interface MeasurePolicy {
fun MeasureScope.measure(measurables: List<Measurable>,constraints: Constraints): MeasureResult

fun IntrinsicMeasureScope.minIntrinsicWidth(measurables: List<IntrinsicMeasurable>,height: Int): Int

fun IntrinsicMeasureScope.minIntrinsicHeight(measurables: List<IntrinsicMeasurable>,width: Int): Int

fun IntrinsicMeasureScope.maxIntrinsicWidth(measurables: List<IntrinsicMeasurable>,height: Int): Int

fun IntrinsicMeasureScope.maxIntrinsicHeight(measurables: List<IntrinsicMeasurable>,width: Int): Int
}

可以看出:



  1. 使用固有特性测量的时候,会调用对应的IntrinsicMeasureScope方法,如使用Modifier.height(IntrinsicSize.Min),就会调用minIntrinsicHeight方法

  2. 父项测量子项时,就是在MeasureScope.measure方法中调用measure.meausre(constraints),但是具体是怎么实现的呢?我们来看个例子


@Composable
fun MeasureTest() {
Row() {
Layout(content = { }, measurePolicy = { measurables, constraints ->
measurables.forEach {
it.measure(constraints)
}
layout(100, 100) {

}
})
}
}

一个简单的例子,我们在measure方法中打个断点,如下图所示:



  1. 如下图所示,是由RowMeasurePolicy中开始测量子项,rowColumnMeasurePolicy我们定义为ParentPolicy

  2. 然后调用到LayoutNode,OuterMeasurablePlaceable,InnerPlaceablemeasure方法

  3. 最后再由InnerPlaceable中调用到子项的MeasurePolicy,即我们自定义Layout实现的部分,我们定义它为ChildPolicy

  4. 子项中也可能会测量它的子项,在这种情况下它就变成了一个ParentPolicy,然后继续后续的测量



综上所述,父项在测量子项时,子项的测量入口就是LayoutNode.measure,然后经过一系列调用到子项自己的MeasurePolicy,也就是我们自定义Layout中自定义的部分


3.2 LayoutNodeWrapper链构建


上面我们说了,测量入口是LayoutNode,后续还要经过OuterMeasurablePlaceable,InnerPlaceablemeasure方法,那么问题来了,这些东西是怎么来的呢?

首先给出结论



  1. 子项都是以LayoutNode的形式,存在于Parentchildren中的

  2. Layout的设置的modifier会以LayoutNodeWrapper链的形式存储在LayoutNode中,然后后续做相应变换


由于篇幅原因,关于第一点就不在这里详述了,有兴趣的同学可以参考:Jetpack Compose 测量流程源码分析

我们这里主要看下LayoutNodeWrapper链是怎么构建的


  internal val innerLayoutNodeWrapper: LayoutNodeWrapper = InnerPlaceable(this)
private val outerMeasurablePlaceable = OuterMeasurablePlaceable(this, innerLayoutNodeWrapper)
override fun measure(constraints: Constraints) = outerMeasurablePlaceable.measure(constraints)
override var modifier: Modifier = Modifier
set(value) {
// …… code
field = value
// …… code


// 创建新的 LayoutNodeWrappers 链
// foldOut 相当于遍历 modifier
val outerWrapper = modifier.foldOut(innerLayoutNodeWrapper) { mod /*📍 modifier*/ , toWrap ->
var wrapper = toWrap
if (mod is OnGloballyPositionedModifier) {
onPositionedCallbacks += mod
}
if (mod is RemeasurementModifier) {
mod.onRemeasurementAvailable(this)
}

val delegate = reuseLayoutNodeWrapper(mod, toWrap)
if (delegate != null) {
wrapper = delegate
} else {
// …… 省略了一些 Modifier判断
if (mod is KeyInputModifier) {
wrapper = ModifiedKeyInputNode(wrapper, mod).assignChained(toWrap)
}
if (mod is PointerInputModifier) {
wrapper = PointerInputDelegatingWrapper(wrapper, mod).assignChained(toWrap)
}
if (mod is NestedScrollModifier) {
wrapper = NestedScrollDelegatingWrapper(wrapper, mod).assignChained(toWrap)
}
// 布局相关的 Modifier
if (mod is LayoutModifier) {
wrapper = ModifiedLayoutNode(wrapper, mod).assignChained(toWrap)
}
if (mod is ParentDataModifier) {
wrapper = ModifiedParentDataNode(wrapper, mod).assignChained(toWrap)
}

}
wrapper
}

outerWrapper.wrappedBy = parent?.innerLayoutNodeWrapper
outerMeasurablePlaceable.outerWrapper = outerWrapper

……
}

如上所示:



  1. 默认的LayoutNodeWrapper链即由LayoutNode , OuterMeasurablePlaceable, InnerPlaceable 组成

  2. 当添加了modifier时,LayoutNodeWrapper链会更新,modifier会作为一个结点插入到其中


举个例子,如果我们给Layout设置一些modifier:


Modifier.size(100.dp).padding(10.dp).background(Color.Blue)

那么对应的LayoutNodeWrapper链如下图所示



这样一个接一个链式调用下一个的measure,直到最后一个结点InnerPlaceable

那么InnerPlaceable又会调用到哪儿呢?



InnerPlaceable最终调用到了我们自定义Layout时写的measure方法


3.3 固有特性测量是怎样实现的?


上面我们介绍了固有特性测量的使用,也介绍了LayoutNodeWrapper链的构建,那么固有特性测量是怎么实现的呢?

其实固有特性测量就是往LayoutNodeWrapper链中插入了一个Modifier


@Stable
fun Modifier.height(intrinsicSize: IntrinsicSize) = when (intrinsicSize) {
IntrinsicSize.Min -> this.then(MinIntrinsicHeightModifier)
IntrinsicSize.Max -> this.then(MaxIntrinsicHeightModifier)
}

private object MinIntrinsicHeightModifier : IntrinsicSizeModifier {
override fun MeasureScope.measure(
measurable: Measurable,
constraints: Constraints
): MeasureResult {
//正式测量前先根据固有特性测量获得一个约束
val contentConstraints = calculateContentConstraints(measurable, constraints)
//正式测量
val placeable = measurable.measure(
if (enforceIncoming) constraints.constrain(contentConstraints) else contentConstraints
)
return layout(placeable.width, placeable.height) {
placeable.placeRelative(IntOffset.Zero)
}
}

override fun MeasureScope.calculateContentConstraints(
measurable: Measurable,
constraints: Constraints
): Constraints {
val height = measurable.minIntrinsicHeight(constraints.maxWidth)
return Constraints.fixedHeight(height)
}

override fun IntrinsicMeasureScope.maxIntrinsicHeight(
measurable: IntrinsicMeasurable,
width: Int
) = measurable.minIntrinsicHeight(width)
}

如上所示:



  1. IntrinsicSize.Min其实也是个Modifier

  2. MinIntrinsicHeightModifier会在测量之间,先调用calculateContentConstraints计算约束

  3. calculateContentConstraints中则会递归地调用子项的minIntrinsicHeight,并找出最大值,这样父项的高度就确定了

  4. 固有特性测量完成后,再调用measurable.measure,开始真正的递归测量


3.4 测量过程小结


准备阶段

子项在声明时,会生成LayoutNode添加到父项的chindredn中,同时子项的modifier也将构建成LayoutNodeWrapper链,保存在LayoutNode

值得注意的是,如果使用了固有特性测量,将会添加一个IntrinsicSizeModifierLayoutNodeWrapper链中


测量阶段

父容器在其测量策略MeasurePolicymeasure函数中会执行childmeasure函数。

childmeasure方法按照构建好的LayoutNodeWrapper链一步步的执行各个节点的measure函数,最终走到InnerPlaceablemeasure函数,在这里又会继续它的children进行测量,此时它的children 就会和它一样进行执行上述流程,一直到所有children测量完成。


用下面这张图总结一下上述流程。


总结


本文主要介绍了以下内容



  1. Android中布局层级过深为什么会对性能有影响?

  2. Compose中为什么没有布局嵌套问题?

  3. 什么是固有特性测量及固有特性测量的使用

  4. Compose测量过程源码分析及固有特性测量到底是怎样实现的?


如果本文对你有所帮助,欢迎点赞收藏~


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

JavaScript 中有了Object 为什么还需要 Map 呢

众所周知,Map 是用于存储键值对的,而 JavaScript 中对象也是由键值对组成的,那么 Map 存在的意义是什么呢? 别把对象当 Map 1、可能通过原型链访问到未定义的属性 假设现有场景,开发一个网站,需要提供日语、汉语、韩语三种语言,我们可以定义一...
继续阅读 »

众所周知,Map 是用于存储键值对的,而 JavaScript 中对象也是由键值对组成的,那么 Map 存在的意义是什么呢?


别把对象当 Map


1、可能通过原型链访问到未定义的属性


假设现有场景,开发一个网站,需要提供日语、汉语、韩语三种语言,我们可以定义一个字典去管理。


const dictionary = {
'ja': {
'Ninjas for hire': '忍者を雇う',
},
'zh': {
'Ninjas for hire': '忍者出租',
},
'ko': {
'Ninjas for hire': '고용 닌자',
}
}

console.log(dictionary.ja['Ninjas for hire']) // 忍者を雇う
console.log(dictionary.zh['Ninjas for hire']) // 忍者出租
console.log(dictionary.ko['Ninjas for hire']) // 고용 닌자

这样我们就把不同语言的字典管理起来了。但是,当我们试图访问 constroctor 属性,问题就出现了。


console.log(dictionary.ko['constructor']) // ƒ Object() { [native code] }

对于不存在的属性,我们期望得到 undefined,结果却通过原型链访问到了未定义的属性,原型对象的 constructor 属性,指向构造函数。


此处有一个解决办法是把原型设置为 null


Object.setPrototypeOf(dictionary.ko, null)
console.log(dictionary.ko['constructor']) // undefined

2、对象的 Key 只能是字符串


假设需要将对象的 key 映射为 html 节点。我们写如下代码:


/* html部分
<div id="firstElement"></div>
<div id="secondElement"></div>
*/

const firstElement = document.getElementById('firstElement')
const secondElement = document.getElementById('secondElement')

const map = {}

map[firstElement] = {
data: 'firstElement'
}
map[secondElement] = {
data: 'secondElement'
}

console.log(map[firstElement].data) // secondElement
console.log(map[secondElement].data) // secondElement


第一个元素的数据被覆盖了,原因是对象中的 key 只能是字符串类型,当我们没有使用字符串类型时,它会隐式调用 toString() 函数进行转换。于是两个 html 元素都被转为字符串 [object HTMLDivElement]


使用 Map


1、Map 常用操作


Map 可以使用任何 JavaScript 数据类型作为键


function People(name) {
this.name = name
}
const zhangsan = new People('zhangsan')
const xiaoming = new People('xiaoming')
const lihua = new People('lihua')
// 创建 Map
const map = new Map()
// 创建 Map 并进行初始化
const map1 = new Map([
['key1', 'val1'],
['key2', 'val2'],
])
// 设置键值映射关系
map.set(zhangsan, {
region: 'HB'
})
map.set(xiaoming, {
region: 'HN'
})
// 根据 key 获取对应值
console.log(map.get(zhangsan)) // { region: 'HB' }
console.log(map.get(xiaoming)) // { region: 'HN' }
// 获取不存在的 key 得到 undefined
console.log(map.get(lihua)) // undefined
// 通过 has 函数判断指定 key 是否存在
console.log(map.has(lihua)) // false
console.log(map.has(xiaoming)) // true
// map存储映射个数
console.log(map.size) // 2
// delete 删除 key
map.delete(xiaoming)
console.log(map.has(xiaoming)) // false
console.log(map.size) // 1
// clear 清空 map
map.clear()
console.log(map.size) // 0

2、遍历 Map


Map 可以确保遍历的顺序和插入的顺序一致


const zhangsan = { name: 'zhangsan' }
const xiaoming = { name: 'xiaoming' }
const map = new Map()
map.set(zhangsan, { region: 'HB' })
map.set(xiaoming, { region: 'HN' })

for (let item of map) { // = for (let item of map.entries()) {
console.log(item)
}
// 每个键值对返回的是 [key, value] 的数组
// [ { name: 'zhangsan' }, { region: 'HB' } ]
// [ { name: 'xiaoming' }, { region: 'HN' } ]
for (let key of map.keys()) {
console.log(key)
}
// 遍历 key
// { name: 'zhangsan' }
// { name: 'xiaoming' }
for (let key of map.values()) {
console.log(key)
}
// 遍历 value
// { region: 'HB' }
// { region: 'HN' }

3、Map 中判断 key 相等


Map 内部使用 SameValueZero 比较操作。


关于SameValue 和 SameValueZero


SameValue (Object.is()) 和严格相等(===)相比,对于 NaN+0-0 的处理不同


Object.is(NaN, NaN) // true
Object.is(0, -0) // false

SameValueZero 与 SameValue 的区别主要在于 0-0 是否相等。


map.set(NaN, 0)
map.set(0, 0)
console.log(map.has(NaN)) // true
console.log(map.has(-0)) // true

4、Map 序列化


感谢 @一条鱼的心事 的提醒。


Map 无法被序列化,如果试图用 JSON.stringify 获得 MapJSON 的话,只会得到 "{}"


由于 Map 的键可以是任意数据类型,而 JSON 仅允许将字符串作为键,所以一般情况下无法将 Map 转为 JSON


不过可以通过下面的方式去尝试序列化一个 Map


// 初始化 Map(1) {"key1" => "val1"}
const originMap = new Map([['key1', 'val1']])
// 序列化 "[[\"key1\",\"val1\"]]"
const mapStr = JSON.stringify(Array.from(originMap.entries()))
// 反序列化 Map(1) {"key1" => "val1"}
const cloneMap = new Map(JSON.parse(mapStr))

Map 和 Object 的性能差异



  1. 内存占用


不同浏览器的情况不同,但给定固定大小的内存,Map 大约可以比 Object 多存储 50% 的键/值对。



  1. 插入性能


Map 略快,如果涉及大量操作,建议使用 Map



  1. 查找速度


性能差异极小,但如果只包含少量键/值对,则 Object 有时候速度更快。Object 作为数组使用时浏览器会进行优化。如果涉及大量查找操作,选择 Object 会更好一些。



  1. 删除性能


如果代码涉及大量的删除操作,建议选择 Map



作者:我不吃饼干呀
链接:https://juejin.cn/post/7012036506994868255

收起阅读 »

CSS实现瀑布流的两种方式

瀑布流又称瀑布流式布局,是比较流行的一种网站页面布局方式。在手机端进行多图片展示时会经常用到。即多行等宽元素排列,后面的元素依次添加到其后,等宽不等高,根据图片原比例缩放直至宽度达到我们的要求,依次按照规则放入指定位置。 那么瀑布流式布局有哪些实现方式呢? c...
继续阅读 »

瀑布流又称瀑布流式布局,是比较流行的一种网站页面布局方式。在手机端进行多图片展示时会经常用到。即多行等宽元素排列,后面的元素依次添加到其后,等宽不等高,根据图片原比例缩放直至宽度达到我们的要求,依次按照规则放入指定位置。


那么瀑布流式布局有哪些实现方式呢?


column 多行布局实现瀑布流



column 实现瀑布流主要依赖两个属性。


column-count 属性,是控制屏幕分为多少列。


column-gap 属性,是控制列与列之间的距离。



<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>瀑布流布局-column</title>
<style>
.box {
margin: 10px;
column-count: 3;
column-gap: 10px;
}
.item {
margin-bottom: 10px;
}
.item img{
width: 100%;
height:100%;
}
</style>
</head>
<body>
<div class="box">
<div class="item">
<img src="./imgs/1.jpg" alt="1" />
</div>
<div class="item">
<img src="./imgs/2.jpg" alt="2" />
</div>
<div class="item">
<img src="./imgs/3.jpg" alt="3" />
</div>
<div class="item">
<img src="./imgs/1.jpg" alt="1" />
</div>
<div class="item">
<img src="./imgs/2.jpg" alt="2" />
</div>
<div class="item">
<img src="./imgs/3.jpg" alt="3" />
</div>
<div class="item">
<img src="./imgs/1.jpg" alt="1" />
</div>
<div class="item">
<img src="./imgs/2.jpg" alt="2" />
</div>
<div class="item">
<img src="./imgs/3.jpg" alt="3" />
</div>
<div class="item">
<img src="./imgs/1.jpg" alt="1" />
</div>
<div class="item">
<img src="./imgs/2.jpg" alt="2" />
</div>
<div class="item">
<img src="./imgs/3.jpg" alt="3" />
</div>
<div class="item">
<img src="./imgs/1.jpg" alt="1" />
</div>
<div class="item">
<img src="./imgs/2.jpg" alt="2" />
</div>
<div class="item">
<img src="./imgs/3.jpg" alt="3" />
</div>
<div class="item">
<img src="./imgs/1.jpg" alt="1" />
</div>
<div class="item">
<img src="./imgs/2.jpg" alt="2" />
</div>
<div class="item">
<img src="./imgs/3.jpg" alt="3" />
</div>
<div class="item">
<img src="./imgs/1.jpg" alt="1" />
</div>
<div class="item">
<img src="./imgs/2.jpg" alt="2" />
</div>
<div class="item">
<img src="./imgs/3.jpg" alt="3" />
</div>
<div class="item">
<img src="./imgs/1.jpg" alt="1" />
</div>
<div class="item">
<img src="./imgs/2.jpg" alt="2" />
</div>
<div class="item">
<img src="./imgs/3.jpg" alt="3" />
</div>
</div>
</body>
</html>

展示效果如下


column.png


flex 弹性布局实现瀑布流



flex 实现瀑布流需要将最外层元素设置为 display: flex,使用弹性布局


flex-flow:column wrap 使其纵向排列并且换行换行


设置 height: 100vh 填充屏幕的高度,也可以设置为单位为 px 的高度,来容纳子元素。


每一列的宽度可用 calc 函数来设置,即 width: calc(100%/3 - 20px)。分成等宽的 3 列减掉左右两遍的 margin 距离。



<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>瀑布流布局-flex</title>
<style>
.box {
display: flex;
flex-flow: column wrap;
height: 100vh;
}
.item {
margin: 10px;
width: calc(100%/3 - 20px);
}
.item img{
width: 100%;
height:100%;
}
</style>
</head>
<body>
<div class="box">
<div class="item">
<img src="./imgs/1.jpg" alt="1" />
</div>
<div class="item">
<img src="./imgs/2.jpg" alt="2" />
</div>
<div class="item">
<img src="./imgs/3.jpg" alt="3" />
</div>
<div class="item">
<img src="./imgs/1.jpg" alt="1" />
</div>
<div class="item">
<img src="./imgs/2.jpg" alt="2" />
</div>
<div class="item">
<img src="./imgs/3.jpg" alt="3" />
</div>
<div class="item">
<img src="./imgs/1.jpg" alt="1" />
</div>
<div class="item">
<img src="./imgs/2.jpg" alt="2" />
</div>
<div class="item">
<img src="./imgs/3.jpg" alt="3" />
</div>
<div class="item">
<img src="./imgs/1.jpg" alt="1" />
</div>
<div class="item">
<img src="./imgs/2.jpg" alt="2" />
</div>
<div class="item">
<img src="./imgs/3.jpg" alt="3" />
</div>
<div class="item">
<img src="./imgs/1.jpg" alt="1" />
</div>
</div>
</body>
</html>

展示效果如下


flex.png



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

收起阅读 »

学透CSS-:focus-within 仿掘金登录小人动画

兼容性 作为:focus的好兄弟,在兼容性上也还是不错的。主流的浏览器基本都已经支持这个属性。 :focus-within 和 :focus 的区 :focus-within 表示一个元素自身获取焦点,以及子元素获取焦点后的效果。 :focus 表...
继续阅读 »

兼容性


作为:focus的好兄弟,在兼容性上也还是不错的。主流的浏览器基本都已经支持这个属性。
image.png


:focus-within 和 :focus 的区


:focus-within 表示一个元素自身获取焦点,以及子元素获取焦点后的效果。


:focus 表示元素自身获取到焦点后的效果。


示例


定义一个form表单,背景颜色是green。


form{          
padding: 50px;
background-color:green ;
}
<form action="">
<input type="text">
</form>

image.png


定义获取焦点后的效果


form:focus-within{
background-color: aqua;
}
input:focus{
background-color: red;
}

当input标签获取到焦点后,背景颜色变成了red,同时form的背景颜色变成aqea
image.png


应用场景- form表单输入(掘金登录页面)


掘金在登录输入密码的时候,这个小人会挡住自己的眼睛,有很多作者用各种方法实现这个效果,:focu-within有同样可以实现这个效果。
image.png


首先实现登陆前的画面(比较丑)


<div class="login">
<form action="">
<div class="panfish"></div>
<div><label for=""> 账号</label> <input type="text" /></div>
<div><label for=""> 密码</label> <input type="text" /></div>
</form>
</div>

.login {
position: relative;
padding: 2rem;
width: 20rem;
font-size: 1.167rem;
background-color: #fff;
border-radius: 2px;
box-sizing: border-box;
}
.panfish {
background: url(https://lf3-cdn-tos.bytescm.com/obj/static/xitu_juejin_web/ad7fa76844a2df5c03151ead0ce65ea6.svg);
z-index: 1;
padding-top: 50px;
width: 20rem;
height:50px;
position: absolute;
background-repeat: no-repeat;

top: -60px;
}
input:focus {
background-color: red;
}

image.png


使用:fous-within


form:focus-within > .panfish {
background: url(https://lf3-cdn-tos.bytescm.com/obj/static/xitu_juejin_web/4f6f6f316cde4398d201cd67e44ddea3.svg);
background-repeat: no-repeat;

}

获取焦点后的效果


image.png


GIF


focuswithin.gif


作者:前端picker
链接:https://juejin.cn/post/7012171045155110942

收起阅读 »

给女友写的,每日自动推送暖心消息

起因是因为刷到一则给女友发的每日提醒消息的沸点,每天自动定时发送消息,感觉很有趣,刚好最近在学习egg,里面有用到定时任务,于是决定尝试一把 egg 实现 环境准备 操作系统:支持 macOS,Linux,Windows 运行环境:建议选择 node LTS ...
继续阅读 »

起因是因为刷到一则给女友发的每日提醒消息的沸点,每天自动定时发送消息,感觉很有趣,刚好最近在学习egg,里面有用到定时任务,于是决定尝试一把 egg 实现


环境准备


操作系统:支持 macOS,Linux,Windows


运行环境:建议选择 node LTS 版本,最低要求 8.x。


创建egg项目和目录结构介绍


快速入门


目录结构


运行


本地开发


$ npm i
$ npm run dev
$ open http://localhost:7001/

部署生产


$ npm start
$ npm stop

控制器


class HomeController extends Controller {
async send() {
const { ctx, app } = this;
ctx.body = app.config;
const result = await ctx.service.sendmsg.sendOut();
ctx.logger.info('主动触发,发送模板消息 结果: %j', result);
ctx.body = result;
ctx.set('Content-Type', 'application/json');
}
}

service服务层


 // 时间处理
const moment = require('moment');
class sendmsg extends Service {
// 发送模板消息给媳妇儿
async sendOut() {
const { ctx, app } = this;
const token = await this.getToken();
const data = await this.getTemplateData();
ctx.logger.info('获取token 结果: %j', token);
// 模板消息接口文档
const users = app.config.weChat.users;
const promise = users.map(id => {
ctx.logger.info('--------------开始发送每日提醒-----------------------------------------------: %j', id);
data.touser = id;
return this.toWechart(token, data);
});
const results = await Promise.all(promise);
ctx.logger.info('--------------结束发送每日提醒->结果-----------------------------------------------: %j', results);
return results;
}
// 通知微信接口
async toWechart(token, data) {
...
}
// 获取token
async getToken() {
...
}
// 组装模板消息数据
async getTemplateData() {
...
}
// 获取天气
async getWeather(city = '深泽') {
...
}
// 获取 下次发工资 还有多少天
getWageDay() {
...
}
// 获取距离 下次结婚纪念日还有多少天
getMarryDay() {
...
}
// 获取 距离 下次生日还有多少天
getbirthday() {
...
}
// 获取 相恋天数
getLoveDay() {
...
}
// 获取 相恋几年了
getLoveYear() {
...
}
// 获取是第几个生日
getBirthYear() {
...
}
// 获取是第几个结婚纪念日
getMarryYear() {
...
}
// 获取 每日一句
async getOneSentence() {
...
}
// 获取时间日期
getDatetime() {
...
}
}

发送模板消息


  async toWechart(token, data) {
// 模板消息接口文档
const url = 'https://api.weixin.qq.com/cgi-bin/message/template/send?access_token=' + token;
const result = await this.ctx.curl(url, {
method: 'POST',
data,
dataType: 'json',
headers: {
'Content-Type': 'application/json',
},
});
return result;
}

获取Access token


  async getToken() {
const { app } = this;
const url = 'https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=' + app.config.weChat.appld + '&secret=' + app.config.weChat.secret;
const result = await this.ctx.curl(url, {
method: 'get',
dataType: 'json',
});
if (result.status === 200) {
return result.data.access_token;
}
}

组装模板消息数据


  async getTemplateData() {
const { app } = this;
// 判断所需 模板
// 发工资模板 getWageDay == 0 wageDay
// 结婚纪念日模板 getMarryDay == 0 marry
// 生日 模板 getbirthday == 0 birthday
// 正常模板 daily

const wageDay = this.getWageDay();
const marry = this.getMarryDay();
const birthday = this.getbirthday();
const data = {
topcolor: '#FF0000',
data: {},
};
// 发工资模板
if (!wageDay) {
data.template_id = app.config.weChat.wageDay;
data.data = {
dateTime: {
value: this.getDatetime(),
color: '#cc33cc',
},
};
} else if (!marry) {
// 结婚纪念日模板
data.template_id = app.config.weChat.marry;
data.data = {
dateTime: {
value: this.getDatetime(),
color: '#cc33cc',
},
anniversary: {
value: this.getMarryYear(),
color: '#ff3399',
},
year: {
value: this.getLoveYear(),
color: '#ff3399',
},
};
} else if (!birthday) {
// 生日模板
data.template_id = app.config.weChat.birthday;
data.data = {
dateTime: {
value: this.getDatetime(),
color: '#cc33cc',
},
individual: {
value: this.getBirthYear(),
color: '#ff3399',
},
};
} else {
// 正常模板
data.template_id = app.config.weChat.daily;
// 获取天气
const getWeather = await this.getWeather();
// 获取每日一句
const message = await this.getOneSentence();
data.data = {
dateTime: {
value: this.getDatetime(),
color: '#cc33cc',
},
love: {
value: this.getLoveDay(),
color: '#ff3399',
},
wage: {
value: wageDay,
color: '#66ff00',
},
birthday: {
value: birthday,
color: '#ff0033',
},
marry: {
value: marry,
color: '#ff0033',
},
wea: {
value: getWeather.wea,
color: '#33ff33',
},
tem: {
value: getWeather.tem,
color: '#0066ff',
},
airLevel: {
value: getWeather.air_level,
color: '#ff0033',
},
tem1: {
value: getWeather.tem1,
color: '#ff0000',
},
tem2: {
value: getWeather.tem2,
color: '#33ff33',
},
win: {
value: getWeather.win,
color: '#3399ff',
},
message: {
value: message,
color: '#8C8C8C',
},
};
}
return data;
}

获取天气


  async getWeather(city = '石家庄') {
const { app } = this;
const url = 'https://www.tianqiapi.com/api?unescape=1&version=v6&appid=' + app.config.weather.appid + '&appsecret=' + app.config.weather.appsecret + '&city=' + city;
const result = await this.ctx.curl(url, {
method: 'get',
dataType: 'json',
});
console.log(result.status);
// "wea": "多云",
// "tem": "27", 实时温度
// "tem1": "27", 高温
// "tem2": "17", 低温
// "win": "西风",
// "air_level": "优",
if (result && result.status === 200) {
return result.data;
}
return {
city,
wea: '未知',
tem: '未知',
tem1: '未知',
tem2: '未知',
win: '未知',
win_speed: '未知',
air_level: '未知',
};
}

获取 下次发工资 还有多少天


  getWageDay() {
const { app } = this;
const wage = app.config.time.wageDay;
// 获取日期 day
// 如果在 wage号之前或等于wage时 那么就用 wage-day
// 如果在 wage号之后 那么就用 wage +(当前月总天数-day)
// 当日 日期day
const day = moment().date();
// 当月总天数
const nowDayTotal = moment().daysInMonth();
// // 下个月总天数
// const nextDayTotal = moment().month(moment().month() + 1).daysInMonth();
let resultDay = 0;
if (day <= wage) {
resultDay = wage - day;
} else {
resultDay = wage + (nowDayTotal - day);
}
return resultDay;
}

获取距离 下次结婚纪念日还有多少天


  getMarryDay() {
const { app } = this;
const marry = app.config.time.marry;
// 获取当前时间戳
const now = moment(moment().format('YYYY-MM-DD')).valueOf();
// 获取纪念日 月-日
const mmdd = moment(marry).format('-MM-DD');
// 获取当年
const y = moment().year();
// 获取今年结婚纪念日时间戳
const nowTimeNumber = moment(y + mmdd).valueOf();
// 判断 今天的结婚纪念日 有没有过,如果已经过去(now>nowTimeNumber),resultMarry日期为明年的结婚纪念日
// 如果还没到,则 结束日期为今年的结婚纪念日
let resultMarry = nowTimeNumber;
if (now > nowTimeNumber) {
// 获取明年纪念日
resultMarry = moment((y + 1) + mmdd).valueOf();
}
return moment(moment(resultMarry).format()).diff(moment(now).format(), 'day');
}

获取 距离 下次生日还有多少天



getbirthday() {
const { app } = this;
const birthday = app.config.time.birthday[moment().year()];
// 获取当前时间戳
const now = moment(moment().format('YYYY-MM-DD')).valueOf();
// 获取纪念日 月-日
const mmdd = moment(birthday).format('-MM-DD');
// 获取当年
const y = moment().year();
// 获取今年生日 时间戳
const nowTimeNumber = moment(y + mmdd).valueOf();
// 判断 生日 有没有过,如果已经过去(now>nowTimeNumber),resultBirthday日期为明年的生日 日期
// 如果还没到,则 结束日期为今年的目标日期
let resultBirthday = nowTimeNumber;
if (now > nowTimeNumber) {
// 获取明年目标日期
resultBirthday = moment(app.config.time.birthday[y + 1]).valueOf();
}
return moment(moment(resultBirthday).format()).diff(moment(now).format(), 'day');
}

获取 相恋天数


  getLoveDay() {
const { app } = this;
const loveDay = app.config.time.love;
return moment(moment().format('YYYY-MM-DD')).diff(loveDay, 'day');
}

获取 相恋几年了


  getLoveYear() {
const { app } = this;
const loveDay = app.config.time.love;
return moment().year() - moment(loveDay).year();
}

获取是第几个生日


  getBirthYear() {
const { app } = this;
const birthYear = app.config.time.birthYear;
return moment().year() - birthYear;
}

获取是第几个结婚纪念日


  getMarryYear() {
const { app } = this;
const marry = app.config.time.marry;
return moment().year() - moment(marry).year();
}

获取 每日一句


  async getOneSentence() {
const url = 'https://v1.hitokoto.cn/';
const result = await this.ctx.curl(url, {
method: 'get',
dataType: 'json',
});
if (result && result.status === 200) {
return result.data.hitokoto;
}
return '今日只有我爱你!';
}

获取时间日期


  getDatetime() {
console.log('moment().weekday()', moment().weekday());
const week = {
1: '星期一',
2: '星期二',
3: '星期三',
4: '星期四',
5: '星期五',
6: '星期六',
0: '星期日',
};
return moment().format('YYYY年MM月DD日 ') + week[moment().weekday()];
}


定时任务和主动触发


定时任务


设置规则 请参考文档


└── app
└── schedule
└── update_cache.js

class UpdateCache extends Subscription {
// 通过 schedule 属性来设置定时任务的执行间隔等配置
static get schedule() {
return {
cron: '0 30 7 * * *', // 每天的7点30分0秒执行
// interval: '1m', // 1 分钟间隔
type: 'all', // 指定所有的 worker 都需要执行
};
}

// subscribe 是真正定时任务执行时被运行的函数
async subscribe() {
const { ctx } = this;
const result = await ctx.service.sendmsg.send();
ctx.logger.info('定时任务执行消息提醒 结果: %j', result);
}
}

日志中 可以查看 定时任务的 执行记录


└── logs
└── serves
└── serves-web

主动发送


请求或浏览器访问 http://localhost:7001/send


image


配置文件说明


└── logs
└── config.default.js

天气秘钥


注册地址


// 天气接口配置
config.weather = {
appid: '*******',
appsecret: '*******',
};

特殊 时间点设置


下方是 birthday生日,因老家都是过阴历生日,不好处理,暂时写死的


// 时间
config.time = {
wageDay: 15, // 工资日
love: '2017-06-09', // 相爱日期
marry: '2021-11-27', // 结婚纪念日
birthday: {
2021: '2021-04-17',
2022: '2022-04-06',
2023: '2023-04-25',
2024: '2024-04-14',
2025: '2025-04-03',
2026: '2026-04-22',
}, // 每年生日 阳历
birthYear: '1995-03-06',
};

微信公众号 配置


因个人只能申请订阅号,而订阅号不支持发送模板消息,所以在此使用的测试的微信公众号,有微信号都可以申请,免注册,扫码登录


无需公众帐号、快速申请接口测试号


直接体验和测试公众平台所有高级接口


申请地址


// 测试 微信公众号
config.weChat = {
appld: '**********',
secret: '**********',
// 用户的openid
users: [
'**********************',
'**********************',
'**********************',
'**********************'
],
daily: '************', // 普通模板
marry: ''************',', // 结婚纪念日模板
wageDay: ''************',', // 工资日模板
birthday: ''************',', // 生日模板
};

微信消息模板


这个需要在 上文提到的 微信公众平台测试账号 单独设置


以下是 我用的模板


正常模板


{{dateTime.DATA}}
今天是 我们相恋的第{{love.DATA}}天
距离上交工资还有{{wage.DATA}}天
距离你的生日还有{{birthday.DATA}}天
距离我们结婚纪念日还有{{marry.DATA}}天
今日天气 {{wea.DATA}}
当前温度 {{tem.DATA}}度
最高温度 {{tem1.DATA}}度
最低温度 {{tem2.DATA}}度
空气质量 {{airLevel.DATA}}
风向 {{win.DATA}}
每日一句
{{message.DATA}}

发工资模板


{{dateTime.DATA}}
老婆大人,今天要发工资了,预计晚九点前会准时上交,记得查收!

生日 模板


{{dateTime.DATA}}
听说今天是你人生当中第 {{individual.DATA}} 个生日?天呐,
我差点忘记!因为岁月没有在你脸上留下任何痕迹。
尽管,日历告诉我:你又涨了一岁,但你还是那个天真可爱的小妖女,生日快乐!

结婚纪念日


{{dateTime.DATA}}
今天是结婚{{anniversary.DATA}}周年纪念日,在一起{{year.DATA}}年了,
经历了风风雨雨,最终依然走在一起,很幸运,很幸福!我们的小家庭要一直幸福下去。

展示效果


d086745c18c811ef17488c004f64cb0.jpg


81c079890d8acd86cf02aeb22c1ff4b.jpg


0fb7faaa548c212ae6c31bf5d9ce816.jpg


作者:iwhao
链接:https://juejin.cn/post/7012171027790692388

收起阅读 »

如何小程序上绘制树状图

前言 现有的移动端图可视化技术有Antv旗下的F2、F6。F2主要专注于数据分析的统计图,而F6专注与各种场景的关系图。两者各有侧重。F6 是一个简单、易用、完备的移动端图可视化引擎,它在高定制能力的基础上,提供了一系列设计优雅、便于使用的图可视化解决方案。能...
继续阅读 »

前言


现有的移动端图可视化技术有Antv旗下的F2、F6。F2主要专注于数据分析的统计图,而F6专注与各种场景的关系图。两者各有侧重。F6 是一个简单、易用、完备的移动端图可视化引擎,它在高定制能力的基础上,提供了一系列设计优雅、便于使用的图可视化解决方案。能帮助开发者搭建属于自己的图可视化、图分析、或图编辑器应用。如果您希望将内容通过流程图、知识图谱、思维导图等形式进行输出,并希望可以方便的实现对图的操控,那么建议您一定要尝试一下F6。



欢迎star和提交issue


github.com/antvis/F6



什么是树图?


树图,表现形式如下,


具体原理可以参考 emr.cs.iit.edu/~reingold/t… ,由根节点不断的派生,形成一个树状结构,是一种可以很好表达层级关系的可视化方法。
举个例子:
Kapture 2021-09-22 at 23.40.34.gif


什么场景下使用































































名称应用
分解树在人口调查中,将人口样本分解为人口统计信息
关键质量特性树将顾客的需求转化为产品的可测量参数和过程特性
决策树或逻辑图绘制出思维过程以便于决策
树干图在产品的设计和开发阶段,用于识别产品的特性
故障树分析识别故障的潜在原因
装配图在制造过程中,描绘产品零部件的装配
方法-方法图解决问题
工作或任务分析识别一项工作或任务的要求
组织图识别管理和汇报间的关联水平
过程决策程序图确定潜在的问题和复杂计划中的对策
需求测量树确定顾客、需求以及对测量产品或服务的测量
原因一原因图或five whys识别问题的根本原因
生产分类结构(WBS)识别项目的所有方面,分解成具体工作包水平

可见树图在实际场景中有很多应用,不论是在日常生活中,还是在生产中都有多种用途。我们最熟悉的脑图(mind map)也是树图的一种形式,


F6中如何绘制


演示示例可以参考f6.antv.vision/zh/docs/exa…
本节代码已经开源,感兴趣可以查看



支付宝中


首先安装


npm install @antv/f6 @antv/f6-alipay -S


index.json


{
"defaultTitle": "紧凑树",
"usingComponents": {
"f6-canvas": "@antv/f6-alipay/es/container/container"
}
}


index.js


import F6 from '@antv/f6';
import TreeGraph from '@antv/f6/dist/extends/graph/treeGraph';

import data from './data.js';

/**
* 紧凑树
*/

Page({
canvas: null,
ctx: null, // 延迟获取的2d context
renderer: '', // mini、mini-native等,F6需要,标记环境
isCanvasInit: false, // canvas是否准备好了
graph: null,

data: {
width: 375,
height: 600,
pixelRatio: 2,
forceMini: false,
},

onLoad() {
// 注册自定义树,节点等
F6.registerGraph('TreeGraph', TreeGraph);

// 同步获取window的宽高
const { windowWidth, windowHeight, pixelRatio } = my.getSystemInfoSync();

this.setData({
width: windowWidth,
height: windowHeight,
pixelRatio,
});
},

/**
* 初始化canvas回调,缓存获得的context
* @param {*} ctx 绘图context
* @param {*} rect 宽高信息
* @param {*} canvas canvas对象,在render为mini时为null
* @param {*} renderer 使用canvas 1.0还是canvas 2.0,mini | mini-native
*/
handleInit(ctx, rect, canvas, renderer) {
this.isCanvasInit = true;
this.ctx = ctx;
this.renderer = renderer;
this.canvas = canvas;
this.updateChart();
},

/**
* canvas派发的事件,转派给graph实例
*/
handleTouch(e) {
this.graph && this.graph.emitEvent(e);
},

updateChart() {
const { width, height, pixelRatio } = this.data;

// 创建F6实例
this.graph = new F6.TreeGraph({
context: this.ctx,
renderer: this.renderer,
width,
height,
pixelRatio,
fitView: true,
modes: {
default: [
{
type: 'collapse-expand',
onChange: function onChange(item, collapsed) {
const model = item.getModel();
model.collapsed = collapsed;
return true;
},
},
'drag-canvas',
'zoom-canvas',
],
},
defaultNode: {
size: 26,
anchorPoints: [
[0, 0.5],
[1, 0.5],
],
},
defaultEdge: {
type: 'cubic-horizontal',
},
layout: {
type: 'compactBox',
direction: 'LR',
getId: function getId(d) {
return d.id;
},
getHeight: function getHeight() {
return 16;
},
getWidth: function getWidth() {
return 16;
},
getVGap: function getVGap() {
return 10;
},
getHGap: function getHGap() {
return 100;
},
},
});

this.graph.node(function(node) {
return {
label: node.id,
labelCfg: {
offset: 10,
position: node.children && node.children.length > 0 ? 'left' : 'right',
},
};
});

this.graph.data(data);
this.graph.render();
this.graph.fitView();
},
});


index.axml


<f6-canvas
width="{{width}}"
height="{{height}}"
forceMini="{{forceMini}}"
pixelRatio="{{pixelRatio}}"
onTouchEvent="handleTouch"
onInit="handleInit"
></f6-canvas>

微信中


首先安装


npm install @antv/f6-wx -S

@antv/f6-wx 由于微信对npm包不是很友好,所以我们封装了 @antv/f6-wx 帮助用户简化操作。


index.json


{
"defaultTitle": "紧凑树",
"usingComponents": {
"f6-canvas": "@antv/f6-wx/canvas/canvas"
}
}


index.wxml


<f6-canvas
width="{{width}}"
height="{{height}}"
forceMini="{{forceMini}}"
pixelRatio="{{pixelRatio}}"
bind:onTouchEvent="handleTouch"
bind:onInit="handleInit"
></f6-canvas>


index.js


import F6 from '@antv/f6-wx';
import TreeGraph from '@antv/f6-wx/extends/graph/treeGraph';

import data from './data.js';
Page({
canvas: null,
ctx: null, // 延迟获取的2d context
renderer: '', // mini、mini-native等,F6需要,标记环境
isCanvasInit: false, // canvas是否准备好了
graph: null,

data: {
width: 375,
height: 600,
pixelRatio: 2,
forceMini: false,
},

onLoad() {
// 注册自定义树,节点等
F6.registerGraph('TreeGraph', TreeGraph);
// 同步获取window的宽高
const { windowWidth, windowHeight, pixelRatio } = wx.getSystemInfoSync();

this.setData({
width: windowWidth * pixelRatio,
height: windowHeight * pixelRatio,
pixelRatio,
});
},

/**
* 初始化canvas回调,缓存获得的context
* @param {*} ctx 绘图context
* @param {*} rect 宽高信息
* @param {*} canvas canvas对象,在render为mini时为null
* @param {*} renderer 使用canvas 1.0还是canvas 2.0,mini | mini-native
*/
handleInit(event) {
const { ctx, rect, canvas, renderer } = event.detail;
this.isCanvasInit = true;
this.ctx = ctx;
this.renderer = renderer;
this.canvas = canvas;
this.updateChart();
},

/**
* canvas派发的事件,转派给graph实例
*/
handleTouch(e) {
this.graph && this.graph.emitEvent(e.detail);
},

updateChart() {
const { width, height, pixelRatio } = this.data;

// 创建F6实例
this.graph = new F6.TreeGraph({
context: this.ctx,
renderer: this.renderer,
width,
height,
pixelRatio,
fitView: true,
modes: {
default: [
{
type: 'collapse-expand', // 点击后展开/收缩
onChange: function onChange(item, collapsed) {
const model = item.getModel();
model.collapsed = collapsed;
return true;
},
},
'drag-canvas',
'zoom-canvas',
],
},
defaultNode: {
size: 26,
anchorPoints: [
[0, 0.5],
[1, 0.5],
],
},
defaultEdge: {
type: 'cubic-horizontal',
},
layout: {
type: 'compactBox',
direction: 'LR',
getId: function getId(d) {
return d.id;
},
getHeight: function getHeight() {
return 16;
},
getWidth: function getWidth() {
return 16;
},
getVGap: function getVGap() {
return 10;
},
getHGap: function getHGap() {
return 100;
},
},
});

this.graph.node(function(node) {
return {
label: node.id,
labelCfg: {
offset: 10,
position: node.children && node.children.length > 0 ? 'left' : 'right',
},
};
});

this.graph.data(data);
this.graph.render();
this.graph.fitView();
},
});



作者:AntCredit
链接:https://juejin.cn/post/7011374414394556452

收起阅读 »

Android-activity的启动流程

需要结合Application的启动流程。 juejin.cn/post/701209…//查看栈顶可见activity是否正等待 if (normalMode) { try { if (mStackSupervisor.at...
继续阅读 »


需要结合Application的启动流程。 juejin.cn/post/701209…

//查看栈顶可见activity是否正等待
if (normalMode) {
try {
if (mStackSupervisor.attachApplicationLocked(app)) {
didSomething = true;
}
} catch (Exception e) {
Slog.wtf(TAG, "Exception thrown launching activities in " + app, e);
badApp = true;
}
}

进入ActivityStackSupervisor中的

boolean attachApplicationLocked(ProcessRecord app) throws RemoteException {
final String processName = app.processName;
boolean didSomething = false;
for (int displayNdx = mActivityDisplays.size() - 1; displayNdx >= 0; --displayNdx) {
final ActivityDisplay display = mActivityDisplays.valueAt(displayNdx);
for (int stackNdx = display.getChildCount() - 1; stackNdx >= 0; --stackNdx) {
final ActivityStack stack = display.getChildAt(stackNdx);
if (!isFocusedStack(stack)) {
continue;
}
//从activityStack(Activity栈)把所有的activity添加给mTmpActivityList
stack.getAllRunningVisibleActivitiesLocked(mTmpActivityList);
//返回当前应用最顶端的activity
final ActivityRecord top = stack.topRunningActivityLocked();
final int size = mTmpActivityList.size();
//遍历所有的activity
for (int i = 0; i < size; i++) {
final ActivityRecord activity = mTmpActivityList.get(i);
f (activity.app == null && app.uid == activity.info.applicationInfo.uid
&& processName.equals(activity.processName)) {
try {
if (realStartActivityLocked(activity, app,
top == activity /* andResume */, true /* checkConfig */)) {
didSomething = true;
}
} catch (RemoteException e) {

throw e;
}
}
}
}
}
if (!didSomething) {
ensureActivitiesVisibleLocked(null, 0, !PRESERVE_WINDOWS);
}
return didSomething;
}

进入realStartActivityLocked方法

final boolean realStartActivityLocked(ActivityRecord r, ProcessRecord app,
boolean andResume, boolean checkConfig) throws RemoteException {
...
//创建activity启动事务
final ClientTransaction clientTransaction = ClientTransaction.obtain(app.thread,
r.appToken);
//添加回调
clientTransaction.addCallback(LaunchActivityItem.obtain(new Intent(r.intent),
System.identityHashCode(r), r.info,
mergedConfiguration.getGlobalConfiguration(),
mergedConfiguration.getOverrideConfiguration(), r.compat,
r.launchedFromPackage, task.voiceInteractor, app.repProcState, r.icicle,
r.persistentState, results, newIntents, mService.isNextTransitionForward(),
profilerInfo));

final ActivityLifecycleItem lifecycleItem;
if (andResume) {
lifecycleItem = ResumeActivityItem.obtain(mService.isNextTransitionForward());
} else {
lifecycleItem = PauseActivityItem.obtain();
}
clientTransaction.setLifecycleStateRequest(lifecycleItem);
//提交事务
mService.getLifecycleManager().scheduleTransaction(clientTransaction);
...
}

进入ClientLifecycleManager


void scheduleTransaction(ClientTransaction transaction) throws RemoteException {
final IApplicationThread client = transaction.getClient();
transaction.schedule();
if (!(client instanceof Binder)) {
// If client is not an instance of Binder - it's a remote call and at this point it is
// safe to recycle the object. All objects used for local calls will be recycled after
// the transaction is executed on client in ActivityThread.
transaction.recycle();
}
}

进入ClientTransaction这个类的schedule方法

public void schedule() throws RemoteException {
mClient.scheduleTransaction(this);
}

mClient其实就是IApplicationThread

public static ClientTransaction obtain(IApplicationThread client, IBinder activityToken) {
ClientTransaction instance = ObjectPool.obtain(ClientTransaction.class);
if (instance == null) {
instance = new ClientTransaction();
}
instance.mClient = client;
instance.mActivityToken = activityToken;

return instance;
}

回到ActivityThread中的scheduleTransaction方法中

public void scheduleTransaction(ClientTransaction transaction) throws RemoteException {
ActivityThread.this.scheduleTransaction(transaction);
}

进入activitythread父类ClientTransactionHandler中

void scheduleTransaction(ClientTransaction transaction) {
transaction.preExecute(this);
sendMessage(ActivityThread.H.EXECUTE_TRANSACTION, transaction);
}

回到activitythread中handlerMessage中处理

case EXECUTE_TRANSACTION:
final ClientTransaction transaction = (ClientTransaction) msg.obj;
mTransactionExecutor.execute(transaction);
if (isSystem()) {
// Client transactions inside system process are recycled on the client side
// instead of ClientLifecycleManager to avoid being cleared before this
// message is handled.
transaction.recycle();
}
// TODO(lifecycler): Recycle locally scheduled transactions.
break;

进入TransactionExecutor类中执行execute方法

public void execute(ClientTransaction transaction) {
final IBinder token = transaction.getActivityToken();
log("Start resolving transaction for client: " + mTransactionHandler + ", token: " + token);

executeCallbacks(transaction);

executeLifecycleState(transaction);
mPendingActions.clear();
log("End resolving transaction");
}

到executeCallbacks方法中

public void executeCallbacks(ClientTransaction transaction) {
final List<ClientTransactionItem> callbacks = transaction.getCallbacks();
if (callbacks == null) {
// No callbacks to execute, return early.
return;
}
log("Resolving callbacks");

final IBinder token = transaction.getActivityToken();
ActivityClientRecord r = mTransactionHandler.getActivityClient(token);

// In case when post-execution state of the last callback matches the final state requested
// for the activity in this transaction, we won't do the last transition here and do it when
// moving to final state instead (because it may contain additional parameters from server).
final ActivityLifecycleItem finalStateRequest = transaction.getLifecycleStateRequest();
final int finalState = finalStateRequest != null ? finalStateRequest.getTargetState()
: UNDEFINED;
// Index of the last callback that requests some post-execution state.
final int lastCallbackRequestingState = lastCallbackRequestingState(transaction);
//遍历事务管理器中的所有窗体请求对象
final int size = callbacks.size();
for (int i = 0; i < size; ++i) {
//获得的是LaunchActivityItem
final ClientTransactionItem item = callbacks.get(i);
log("Resolving callback: " + item);
final int postExecutionState = item.getPostExecutionState();
final int closestPreExecutionState = mHelper.getClosestPreExecutionState(r,
item.getPostExecutionState());
if (closestPreExecutionState != UNDEFINED) {
cycleToPath(r, closestPreExecutionState);
}
//进行窗体创建请求
item.execute(mTransactionHandler, token, mPendingActions);
item.postExecute(mTransactionHandler, token, mPendingActions);
if (r == null) {
// Launch activity request will create an activity record.
r = mTransactionHandler.getActivityClient(token);
}

if (postExecutionState != UNDEFINED && r != null) {
// Skip the very last transition and perform it by explicit state request instead.
final boolean shouldExcludeLastTransition =
i == lastCallbackRequestingState && finalState == postExecutionState;
cycleToPath(r, postExecutionState, shouldExcludeLastTransition);
}
}
}

进入到LaunchActivityItem中的execute方法

@Override
public void execute(ClientTransactionHandler client, IBinder token,
PendingTransactionActions pendingActions) {
Trace.traceBegin(TRACE_TAG_ACTIVITY_MANAGER, "activityStart");
//创建一个ActivityClientRecord对象,用于activity的实例化
ActivityClientRecord r = new ActivityClientRecord(token, mIntent, mIdent, mInfo,
mOverrideConfig, mCompatInfo, mReferrer, mVoiceInteractor, mState, mPersistentState,
mPendingResults, mPendingNewIntents, mIsForward,
mProfilerInfo, client);
//回调给activityThread
client.handleLaunchActivity(r, pendingActions, null /* customIntent */);
Trace.traceEnd(TRACE_TAG_ACTIVITY_MANAGER);
}

回到activityThread类中的handleLaunchActivity方法

public Activity handleLaunchActivity(ActivityClientRecord r,
PendingTransactionActions pendingActions, Intent customIntent) {
...
//根据传递过来的activityClientRecord创建一个activity
final Activity a = performLaunchActivity(r, customIntent);
...
}

进入performLaunchActivity方法

private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
...
try {
//通过反射创建activity对象
java.lang.ClassLoader cl = appContext.getClassLoader();
activity = mInstrumentation.newActivity(
cl, component.getClassName(), r.intent);
StrictMode.incrementExpectedActivityCount(activity.getClass());
r.intent.setExtrasClassLoader(cl);
r.intent.prepareToEnterProcess();
if (r.state != null) {
r.state.setClassLoader(cl);
}
} catch (Exception e) {
if (!mInstrumentation.onException(activity, e)) {
throw new RuntimeException(
"Unable to instantiate activity " + component
+ ": " + e.toString(), e);
}
}
...
}

进入Instrumentation类中的newActivity方法

public Activity newActivity(ClassLoader cl, String className,
Intent intent)
throws InstantiationException, IllegalAccessException,
ClassNotFoundException {
String pkg = intent != null && intent.getComponent() != null
? intent.getComponent().getPackageName() : null;
return getFactory(pkg).instantiateActivity(cl, className, intent);
}

进入AppComponentFactory类中

public @NonNull Activity instantiateActivity(@NonNull ClassLoader cl, @NonNull String className,
@Nullable Intent intent)
throws InstantiationException, IllegalAccessException, ClassNotFoundException {
return (Activity) cl.loadClass(className).newInstance();
}

到此activity就被创建出来了。 继续回到activityThread类中

activity.mCalled = false;
if (r.isPersistable()) {
//通过mInstrumentation调用activity的生命周期方法
mInstrumentation.callActivityOnCreate(activity, r.state, r.persistentState);
} else {
mInstrumentation.callActivityOnCreate(activity, r.state);
}

进入Instrumentation类中的callActivityOnCreate方法

public void callActivityOnCreate(Activity activity, Bundle icicle,
PersistableBundle persistentState) {
prePerformCreate(activity);
activity.performCreate(icicle, persistentState);
postPerformCreate(activity);
}

进入activity中的performCreate方法中

final void performCreate(Bundle icicle, PersistableBundle persistentState) {
mCanEnterPictureInPicture = true;
restoreHasCurrentPermissionRequest(icicle);
if (persistentState != null) {
onCreate(icicle, persistentState);
} else {
onCreate(icicle);
}
writeEventLog(LOG_AM_ON_CREATE_CALLED, "performCreate");
mActivityTransitionState.readState(icicle);

mVisibleFromClient = !mWindow.getWindowStyle().getBoolean(
com.android.internal.R.styleable.Window_windowNoDisplay, false);
mFragments.dispatchActivityCreated();
mActivityTransitionState.setEnterActivityOptions(this, getActivityOptions());


收起阅读 »

Kotlin系列三:空指针检查

Android系统上崩溃率最高的异常类型就是空指针异常(NullPointerException)。public void doStudy(Study study) { if (study != null) { study.readBo...
继续阅读 »

Android系统上崩溃率最高的异常类型就是空指针异常(NullPointerException)。

public void doStudy(Study study) {
if (study != null) {
study.readBooks();
study.doHomework();
}
}

这种java里常见的判空检查容易陷入判空地狱的灾难。Kotlin提供了很好的解决思路。

1 可空类型(?)

Kotlin在编译时就进行判空检查,这会导致代码变得相对难写些,因为你得实时考虑到对象的为空与否。

一个判空举例:

fun doStudy(study: Study) {
study.readBooks()
study.doHomework()
}

如果你尝试向doStudy()函数传入一个null参数,则会提示错误:

那么可为空的类型系统是什么样的呢?很简单,就是在类名的后面加上一个问号:

为什么会出现红色报错:由于我们将参数改成了可为空的Study?类型,此时调用参数的readBooks()和doHomework()方法都可能造成空指针异常过。如何解决呢:

fun doStudy(study: Study?) {
if (study != null) {
study.readBooks()
study.doHomework()
}
}

2 判空辅助工具

2.1 ?.操作符

?.操作符:当对象不为空时正常调用相应的方法,当对象为空时则什么都不做(相当于外部包裹了 !=null 的一个判断了):

fun doStudy(study: Study?) {
study?.readBooks()
study?.doHomework()
}

2.1 ?:操作符

?:操作符:操作符的左右两边都接收一个表达式,如果左边表达式的结果不为空就返回左边表达式的结果,否则就返回右边表达式的结果。

val c = if (a ! = null) {
a
} else {
b
}

这段代码的逻辑使用?:操作符就可以简化成:

val c = a ?: b

比如现在我们要编写一个函数用来获得一段文本的长度,使用传统的写法就可以这样写:

fun getTextLength(text: String?): Int {
if (text != null) {
return text.length
}
return 0
}

改进:

fun getTextLength(text: String?) = text?.length ?: 0

2.2 !!操作符

不过Kotlin的空指针检查机制也并非总是那么智能,有的时候我们可能从逻辑上已经将空指针异常处理了,但是Kotlin的编译器并不知道,这个时候它还是会编译失败。

观察如下的代码示例:

var content: String? = "hello"

fun main() {
if (content != null) {
printUpperCase()
}
}

fun printUpperCase() {
val upperCase = content.toUpperCase()
println(upperCase)
}

看上去好像逻辑没什么问题,但这段代码一定是无法运行的。因为printUpperCase()函数并不知道外部已经对content变量进行了非空检查,在调用toUpperCase()方法时,还认为这里存在空指针风险,从而无法编译通过。在这种情况下,如果我们想要强行通过编译,可以使用非空断言工具,写法是在对象的后面加上!!,如下所示:

fun printUpperCase() {
val upperCase = content!!.toUpperCase()
println(upperCase)
}

这种写法意在告诉Kotlin,我非常确信这里的对象不会为空,所以不用你来帮我做空指针检查了,如果出现问题,你可以直接抛出空指针异常,后果由我自己承担。

2.3 let函数

let函数属于Kotlin中的标准函数,这个函数提供了函数式API的编程接口,并将原始调用对象作为参数传递到Lambda表达式中。示例代码如下:

obj.let { obj2 ->
// 编写具体的业务逻辑
}
结合doStudy()函数:

fun doStudy(study: Study?) {
study?.readBooks()
study?.doHomework()
}

虽然这段代码我们通过?.操作符优化之后可以正常编译通过,但其实这种表达方式是有点啰嗦的,如果将这段代码准确翻译成使用if判断语句的写法,对应的代码如下:

fun doStudy(study: Study?) {
if (study != null) {
study.readBooks()
}
if (study != null) {
study.doHomework()
}
}

也就是说,本来我们进行一次if判断就能随意调用study对象的任何方法,但受制于?.操作符的限制,现在变成了每次调用study对象的方法时都要进行一次if判断。

这个时候就可以结合使用?.操作符和let函数来对代码进行优化了,如下所示:

fun doStudy(study: Study?) {
study?.let { stu ->
stu.readBooks()
stu.doHomework()
}
}

我来简单解释一下上述代码,?.操作符表示对象为空时什么都不做,对象不为空时就调用let函数,而let函数会将study对象本身作为参数传递到Lambda表达式中,此时的study对象肯定不为空了,我们就能放心地调用它的任意方法了。

另外还记得Lambda表达式的语法特性吗?当Lambda表达式的参数列表中只有一个参数时,可以不用声明参数名,直接使用it关键字来代替即可,那么代码就可以进一步简化成:

fun doStudy(study: Study?) {
study?.let {
it.readBooks()
it.doHomework()
}
}
收起阅读 »

iOS RXSwift 5.1

iOS
如何选择操作符?下面这个决策树可以帮助你找到需要的操作符。决策树我想要创建一个 Observable产生特定的一个元素:just经过一段延时:timer从一个序列拉取元素:from重复的产生某一个元素:repeatElement存在自定义逻辑:cre...
继续阅读 »

如何选择操作符?

下面这个决策树可以帮助你找到需要的操作符。


决策树

我想要创建一个 Observable

  • 产生特定的一个元素:just
    • 经过一段延时:timer
  • 从一个序列拉取元素:from
  • 重复的产生某一个元素:repeatElement
  • 存在自定义逻辑:create
  • 每次订阅时产生:deferred
  • 每隔一段时间,发出一个元素:interval
    • 在一段延时后:timer
  • 一个空序列,只有一个完成事件:empty
  • 一个任何事件都没有产生的序列:never

我想要创建一个 Observable 通过组合其他的 Observables

  • 任意一个 Observable 产生了元素,就发出这个元素:merge
  • 让这些 Observables 一个接一个的发出元素,当上一个 Observable 元素发送完毕后,下一个 Observable 才能开始发出元素:concat
  • 组合多个 Observables 的元素
    • 当每一个 Observable 都发出一个新的元素:zip
    • 当任意一个 Observable 发出一个新的元素:combineLatest

我想要转换 Observable 的元素后,再将它们发出来

  • 对每个元素直接转换:map
  • 转换到另一个 ObservableflatMap
    • 只接收最新的元素转换的 Observable 所产生的元素:flatMapLatest
    • 每一个元素转换的 Observable 按顺序产生元素:concatMap
  • 基于所有遍历过的元素: scan

我想要将产生的每一个元素,拖延一段时间后再发出:delay

我想要将产生的事件封装成元素发送出来

我想要忽略掉所有的 next 事件,只接收 completed 和 error 事件:ignoreElements

我想创建一个新的 Observable 在原有的序列前面加入一些元素:startWith

我想从 Observable 中收集元素,缓存这些元素之后在发出:buffer

我想将 Observable 拆分成多个 Observableswindow

  • 基于元素的共同特征:groupBy

我想只接收 Observable 中特定的元素

  • 发出唯一的元素:single

我想重新从 Observable 中发出某些元素

  • 通过判定条件过滤出一些元素:filter
  • 仅仅发出头几个元素:take
  • 仅仅发出尾部的几个元素:takeLast
  • 仅仅发出第 n 个元素:elementAt
  • 跳过头几个元素
    • 跳过头 n 个元素:skip
    • 跳过头几个满足判定的元素:skipWhileskipWhileWithIndex
    • 跳过某段时间内产生的头几个元素:skip
    • 跳过头几个元素直到另一个 Observable 发出一个元素:skipUntil
  • 只取头几个元素
    • 只取头几个满足判定的元素:takeWhiletakeWhileWithIndex
    • 只取某段时间内产生的头几个元素:take
    • 只取头几个元素直到另一个 Observable 发出一个元素:takeUntil
  • 周期性的对 Observable 抽样:sample
  • 发出那些元素,这些元素产生后的特定的时间内,没有新的元素产生:debounce
  • 直到元素的值发生变化,才发出新的元素:distinctUntilChanged
  • 在开始发出元素时,延时后进行订阅:delaySubscription

我想要从一些 Observables 中,只取第一个产生元素的 Observableamb

我想评估 Observable 的全部元素

  • 并且对每个元素应用聚合方法,待所有元素都应用聚合方法后,发出结果:reduce
  • 并且对每个元素应用聚合方法,每次应用聚合方法后,发出结果:scan

我想把 Observable 转换为其他的数据结构:as...

我想在某个 Scheduler 应用操作符:subscribeOn

我想要 Observable 发生某个事件时, 采取某个行动:do

我想要 Observable 发出一个 error 事件:error

  • 如果规定时间内没有产生元素:timeout

我想要 Observable 发生错误时,优雅的恢复

  • 如果规定时间内没有产生元素,就切换到备选 Observable :timeout
  • 如果产生错误,将错误替换成某个元素 :catchErrorJustReturn
  • 如果产生错误,就切换到备选 Observable :catchError
  • 如果产生错误,就重试 :retry

我创建一个 Disposable 资源,使它与 Observable 具有相同的寿命:using

我创建一个 Observable,直到我通知它可以产生元素后,才能产生元素:publish

  • 并且,就算是在产生元素后订阅,也要发出全部元素:replay
  • 并且,一旦所有观察者取消观察,他就被释放掉:refCount
  • 通知它可以产生元素了:connect


amb

在多个源 Observables 中, 取第一个发出元素或产生事件的 Observable,然后只发出它的元素

当你传入多个 Observables 到 amb 操作符时,它将取其中一个 Observable:第一个产生事件的那个 Observable,可以是一个 nexterror 或者 completed 事件。 amb 将忽略掉其他的 Observables

收起阅读 »

iOS RXSwift 4.9

iOS
Schedulers - 调度器Schedulers 是 Rx 实现多线程的核心模块,它主要用于控制任务在哪个线程或队列运行。如果你曾经使用过 GCD, 那你对以下代码应该不会陌生:// 后台取得数据,主线程处理结果 D...
继续阅读 »

Schedulers - 调度器

Schedulers 是 Rx 实现多线程的核心模块,它主要用于控制任务在哪个线程或队列运行。

如果你曾经使用过 GCD, 那你对以下代码应该不会陌生:

// 后台取得数据,主线程处理结果
DispatchQueue.global(qos: .userInitiated).async {
let data = try? Data(contentsOf: url)
DispatchQueue.main.async {
self.data = data
}
}

如果用 RxSwift 来实现,大致是这样的:

let rxData: Observable<Data> = ...

rxData
.subscribeOn(ConcurrentDispatchQueueScheduler(qos: .userInitiated))
.observeOn(MainScheduler.instance)
.subscribe(onNext: { [weak self] data in
self?.data = data
})
.disposed(by: disposeBag)

使用 subscribeOn

我们用 subscribeOn 来决定数据序列的构建函数在哪个 Scheduler 上运行。以上例子中,由于获取 Data 需要花很长的时间,所以用 subscribeOn 切换到 后台 Scheduler 来获取 Data。这样可以避免主线程被阻塞。

使用 observeOn

我们用 observeOn 来决定在哪个 Scheduler 监听这个数据序列。以上例子中,通过使用 observeOn 方法切换到主线程来监听并且处理结果。

一个比较典型的例子就是,在后台发起网络请求,然后解析数据,最后在主线程刷新页面。你就可以先用 subscribeOn 切到后台去发送请求并解析数据,最后用 observeOn 切换到主线程更新页面。


MainScheduler

MainScheduler 代表主线程。如果你需要执行一些和 UI 相关的任务,就需要切换到该 Scheduler 运行。

SerialDispatchQueueScheduler

SerialDispatchQueueScheduler 抽象了串行 DispatchQueue。如果你需要执行一些串行任务,可以切换到这个 Scheduler 运行。

ConcurrentDispatchQueueScheduler

ConcurrentDispatchQueueScheduler 抽象了并行 DispatchQueue。如果你需要执行一些并发任务,可以切换到这个 Scheduler 运行。

OperationQueueScheduler

OperationQueueScheduler 抽象了 NSOperationQueue

它具备 NSOperationQueue 的一些特点,例如,你可以通过设置 maxConcurrentOperationCount,来控制同时执行并发任务的最大数量。

Error Handling - 错误处理

一旦序列里面产出了一个 error 事件,整个序列将被终止。RxSwift 主要有两种错误处理机制:

  • retry - 重试
  • catch - 恢复

retry - 重试

retry 可以让序列在发生错误后重试:

// 请求 JSON 失败时,立即重试,
// 重试 3 次后仍然失败,就将错误抛出

let rxJson: Observable<JSON> = ...

rxJson
.retry(3)
.subscribe(onNext: { json in
print("取得 JSON 成功: \(json)")
}, onError: { error in
print("取得 JSON 失败: \(error)")
})
.disposed(by: disposeBag)

以上的代码非常直接 retry(3) 就是当发生错误时,就进行重试操作,并且最多重试 3 次。

retryWhen

如果我们需要在发生错误时,经过一段延时后重试,那可以这样实现:

// 请求 JSON 失败时,等待 5 秒后重试,

let retryDelay: Double = 5 // 重试延时 5 秒

rxJson
.retryWhen { (rxError: Observable<Error>) -> Observable<Int> in
return Observable.timer(retryDelay, scheduler: MainScheduler.instance)
}
.subscribe(...)
.disposed(by: disposeBag)

这里我们需要用到 retryWhen 操作符,这个操作符主要描述应该在何时重试,并且通过闭包里面返回的 Observable 来控制重试的时机:

.retryWhen { (rxError: Observable<Error>) -> Observable<Int> in
...
}

闭包里面的参数是 Observable<Error> 也就是所产生错误的序列,然后返回值是一个 Observable。当这个返回的 Observable 发出一个元素时,就进行重试操作。当它发出一个 error 或者 completed 事件时,就不会重试,并且将这个事件传递给到后面的观察者。

如果需要加上一个最大重试次数的限制:

// 请求 JSON 失败时,等待 5 秒后重试,
// 重试 4 次后仍然失败,就将错误抛出

let maxRetryCount = 4 // 最多重试 4 次
let retryDelay: Double = 5 // 重试延时 5 秒

rxJson
.retryWhen { (rxError: Observable<Error>) -> Observable<Int> in
return rxError.flatMapWithIndex { (error, index) -> Observable<Int> in
guard index < maxRetryCount else {
return Observable.error(error)
}
return Observable<Int>.timer(retryDelay, scheduler: MainScheduler.instance)
}
}
.subscribe(...)
.disposed(by: disposeBag)

我们这里要实现的是,如果重试超过 4 次,就将错误抛出。如果错误在 4 次以内时,就等待 5 秒后重试:

...
rxError.flatMapWithIndex { (error, index) -> Observable<Int> in
guard index < maxRetryCount else {
return Observable.error(error)
}
return Observable<Int>.timer(retryDelay, scheduler: MainScheduler.instance)
}
...

我们用 flatMapWithIndex 这个操作符,因为它可以给我们提供错误的索引数 index。然后用这个索引数判断是否超过最大重试数,如果超过了,就将错误抛出。如果没有超过,就等待 5 秒后重试。


catchError - 恢复

catchError 可以在错误产生时,用一个备用元素或者一组备用元素将错误替换掉:

searchBar.rx.text.orEmpty
...
.flatMapLatest { query -> Observable<[Repository]> in
...
return searchGitHub(query)
.catchErrorJustReturn([])
}
...
.bind(to: ...)
.disposed(by: disposeBag)

我们开头的 Github 搜索就用到了catchErrorJustReturn。当错误产生时,就返回一个空数组,于是就会显示一个空列表页。

你也可以使用 catchError,当错误产生时,将错误事件替换成一个备选序列:

// 先从网络获取数据,如果获取失败了,就从本地缓存获取数据

let rxData: Observable<Data> = ... // 网络请求的数据
let cahcedData: Observable<Data> = ... // 之前本地缓存的数据

rxData
.catchError { _ in cahcedData }
.subscribe(onNext: { date in
print("获取数据成功: \(date.count)")
})
.disposed(by: disposeBag)

Result

如果我们只是想给用户错误提示,那要如何操作呢?

以下提供一个最为直接的方案,不过这个方案存在一些问题:

// 当用户点击更新按钮时,
// 就立即取出修改后的用户信息。
// 然后发起网络请求,进行更新操作,
// 一旦操作失败就提示用户失败原因

updateUserInfoButton.rx.tap
.withLatestFrom(rxUserInfo)
.flatMapLatest { userInfo -> Observable<Void> in
return update(userInfo)
}
.observeOn(MainScheduler.instance)
.subscribe(onNext: {
print("用户信息更新成功")
}, onError: { error in
print("用户信息更新失败: \(error.localizedDescription)")
})
.disposed(by: disposeBag)

这样实现是非常直接的。但是一旦网络请求操作失败了,序列就会终止。整个订阅将被取消。如果用户再次点击更新按钮,就无法再次发起网络请求进行更新操作了。

为了解决这个问题,我们需要选择合适的方案来进行错误处理。例如,使用系统自带的枚举 Result

public enum Result<Success, Failure> where Failure : Error {
case success(Success)
case failure(Failure)
}

然后之前的代码需要修改成:

updateUserInfoButton.rx.tap
.withLatestFrom(rxUserInfo)
.flatMapLatest { userInfo -> Observable<Result<Void, Error>> in
return update(userInfo)
.map(Result.success) // 转换成 Result
.catchError { error in Observable.just(Result.failure(error)) }
}
.observeOn(MainScheduler.instance)
.subscribe(onNext: { result in
switch result { // 处理 Result
case .success:
print("用户信息更新成功")
case .failure(let error):
print("用户信息更新失败: \(error.localizedDescription)")
}
})
.disposed(by: disposeBag)

这样我们的错误事件被包装成了 Result.failure(Error) 元素,就不会终止整个序列。即便网络请求失败了,整个订阅依然存在。如果用户再次点击更新按钮,也是能够发起网络请求进行更新操作的。

另外你也可以使用 materialize 操作符来进行错误处理。这里就不详细介绍了,如你想了解如何使用 materialize 可以参考这篇文章 How to handle errors in RxSwift!

收起阅读 »

iOS RXSwift 4.9

iOS
Disposable - 可被清除的资源通常来说,一个序列如果发出了 error 或者 completed 事件,那么所有内部资源都会被释放。如果你需要提前释放这些资源或取消订阅的话,那么你可以对返回的 可被清...
继续阅读 »

Disposable - 可被清除的资源

通常来说,一个序列如果发出了 error 或者 completed 事件,那么所有内部资源都会被释放。如果你需要提前释放这些资源或取消订阅的话,那么你可以对返回的 可被清除的资源(Disposable) 调用 dispose 方法:

var disposable: Disposable?

override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)

self.disposable = textField.rx.text.orEmpty
.subscribe(onNext: { text in print(text) })
}

override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)

self.disposable?.dispose()
}

调用 dispose 方法后,订阅将被取消,并且内部资源都会被释放。通常情况下,你是不需要手动调用 dispose 方法的,这里只是做个演示而已。我们推荐使用 清除包(DisposeBag) 或者 takeUntil 操作符 来管理订阅的生命周期。

DisposeBag - 清除包

因为我们用的是 Swift ,所以我们更习惯于使用 ARC 来管理内存。那么我们能不能用 ARC 来管理订阅的生命周期了。答案是肯定了,你可以用 清除包(DisposeBag) 来实现这种订阅管理机制:

var disposeBag = DisposeBag()

override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)

textField.rx.text.orEmpty
.subscribe(onNext: { text in print(text) })
.disposed(by: self.disposeBag)
}

override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)

self.disposeBag = DisposeBag()
}

当 清除包 被释放的时候,清除包 内部所有 可被清除的资源(Disposable) 都将被清除。在输入验证中我们也多次看到 清除包 的身影:

var disposeBag = DisposeBag() // 来自父类 ViewController

override func viewDidLoad() {
super.viewDidLoad()

...

usernameValid
.bind(to: passwordOutlet.rx.isEnabled)
.disposed(by: disposeBag)

usernameValid
.bind(to: usernameValidOutlet.rx.isHidden)
.disposed(by: disposeBag)

passwordValid
.bind(to: passwordValidOutlet.rx.isHidden)
.disposed(by: disposeBag)

everythingValid
.bind(to: doSomethingOutlet.rx.isEnabled)
.disposed(by: disposeBag)

doSomethingOutlet.rx.tap
.subscribe(onNext: { [weak self] in self?.showAlert() })
.disposed(by: disposeBag)
}

这个例子中 disposeBag 和 ViewController 具有相同的生命周期。当退出页面时, ViewController 就被释放,disposeBag 也跟着被释放了,那么这里的 5 次绑定(订阅)也就被取消了。这正是我们所需要的。

takeUntil

另外一种实现自动取消订阅的方法就是使用 takeUntil 操作符,上面那个输入验证的演示代码也可以通过使用 takeUntil 来实现:

override func viewDidLoad() {
super.viewDidLoad()

...

_ = usernameValid
.takeUntil(self.rx.deallocated)
.bind(to: passwordOutlet.rx.isEnabled)

_ = usernameValid
.takeUntil(self.rx.deallocated)
.bind(to: usernameValidOutlet.rx.isHidden)

_ = passwordValid
.takeUntil(self.rx.deallocated)
.bind(to: passwordValidOutlet.rx.isHidden)

_ = everythingValid
.takeUntil(self.rx.deallocated)
.bind(to: doSomethingOutlet.rx.isEnabled)

_ = doSomethingOutlet.rx.tap
.takeUntil(self.rx.deallocated)
.subscribe(onNext: { [weak self] in self?.showAlert() })
}

这将使得订阅一直持续到控制器的 dealloc 事件产生为止。

注意⚠️:这里配图中所使用的 Observable 都是“热” Observable,它可以帮助我们理解订阅的生命周期。如果你想要了解 “冷热” Observable 之间的区别,可以参考官方文档 Hot and Cold Observables

收起阅读 »

iOS RXSwift 4.8

iOS
Operator - 操作符操作符可以帮助大家创建新的序列,或者变化组合原有的序列,从而生成一个新的序列。我们之前在输入验证例子中就多次运用到操作符。例如,通过 map 方法将输入的用户名,转换为用户名是否有效。然后用这个转化后来的序列来控...
继续阅读 »

Operator - 操作符

操作符可以帮助大家创建新的序列,或者变化组合原有的序列,从而生成一个新的序列。

我们之前在输入验证例子中就多次运用到操作符。例如,通过 map 方法将输入的用户名,转换为用户名是否有效。然后用这个转化后来的序列来控制红色提示语是否隐藏。我们还通过 combineLatest 方法,将用户名是否有效密码是否有效合并成两者是否同时有效。然后用这个合成后来的序列来控制按钮是否可点击。

这里 map 和 combineLatest 都是操作符,它们可以帮助我们构建所需要的序列。现在,我们再来看几个例子:

filter - 过滤

你可以用 filter 创建一个新的序列。这个序列只发出温度大于 33 度的元素。

map - 转换

你可以用 map 创建一个新的序列。这个序列将原有的 JSON 转换成 Model 。这种转换实际上就是解析 JSON 。

zip - 配对

你可以用 zip 来合成一个新的序列。这个序列将汉堡序列的元素和薯条序列的元素配对后,生成一个新的套餐序列。

如何使用操作符

使用操作符是非常容易的。你可以直接调用实例方法,或者静态方法:

  • 温度过滤

    // 温度
    let rxTemperature: Observable<Double> = ...

    // filter 操作符
    rxTemperature.filter { temperature in temperature > 33 }
    .subscribe(onNext: { temperature in
    print("高温:\(temperature)度")
    })
    .disposed(by: disposeBag)
  • 解析 JSON

    // JSON
    let json: Observable<JSON> = ...

    // map 操作符
    json.map(Model.init)
    .subscribe(onNext: { model in
    print("取得 Model: \(model)")
    })
    .disposed(by: disposeBag)
  • 合成套餐

    // 汉堡
    let rxHamburg: Observable<Hamburg> = ...
    // 薯条
    let rxFrenchFries: Observable<FrenchFries> = ...

    // zip 操作符
    Observable.zip(rxHamburg, rxFrenchFries)
    .subscribe(onNext: { (hamburg, frenchFries) in
    print("取得汉堡: \(hamburg) 和薯条:\(frenchFries)")
    })
    .disposed(by: disposeBag)

决策树

Rx 提供了充分的操作符来帮我们创建序列。当然如果内置操作符无法满足你的需求时,你还可以创建自定义的操作符。

如果你不确定该如何选择操作符,可以参考 决策树。它会引导你找出合适的操作符。

操作符列表

26个英文字母我都认识,可是连成一个句子我就不怎么认得了...

这里提供一个操作符列表,它们就好比是26个英文字母。你如果要将它们的作用全部都发挥出来,是需要学习如何将它们连成一个句子的:

收起阅读 »

Flutter 入门与实战(八十):使用GetX构建更优雅的页面结构

前言 App 的大部分页面都会涉及到数据加载、错误、无数据和正常几个状态,在一开始的时候我们可能数据获取的状态枚举用 if...else 或者 switch 来显示不同的 Widget,这种方式会显得代码很丑陋,譬如下面这样的代码: if (PersonalC...
继续阅读 »

前言


App 的大部分页面都会涉及到数据加载、错误、无数据和正常几个状态,在一开始的时候我们可能数据获取的状态枚举用 if...else 或者 switch 来显示不同的 Widget,这种方式会显得代码很丑陋,譬如下面这样的代码:


if (PersonalController.to.loadingStatus == LoadingStatus.loading) {
return Center(
child: Text('加载中...'),
);
}
if (PersonalController.to.loadingStatus == LoadingStatus.failed) {
return Center(
child: Text('请求失败'),
);
}
// 正常状态
PersonalEntity personalProfile = PersonalController.to.personalProfile;
return Stack(
...
);

这种情况实在是不够优雅,在 GetX 中提供了一种 StateMixin 的方式来解决这个问题。


StateMixin


StateMixin 是 GetX 定义的一个 mixin,可以在状态数据中混入页面数据加载状态,包括了如下状态:



  • RxStatus.loading():加载中;

  • RxStatus.success():加载成功;

  • RxStatus.error([String? message]):加载失败,可以携带一个错误信息 message

  • RxStatus.empty():无数据。


StateMixin 的用法如下:


class XXXController extends GetxController
with StateMixin<T> {
}

其中 T 为实际的状态类,比如我们之前一篇 PersonalEntity,可以定义为:


class PersonalMixinController extends GetxController
with StateMixin<PersonalEntity> {
}

然后StateMixin 提供了一个 change 方法用于传递状态数据和状态给页面。


void change(T? newState, {RxStatus? status})

其中 newState 是新的状态数据,status 就是上面我们说的4种状态。这个方法会通知 Widget 刷新。


GetView


GetX 提供了一个快捷的 Widget 用来访问容器中的 controller,即 GetViewGetView是一个继承 StatelessWidget的抽象类,实现很简单,只是定义了一个获取 controllerget 属性。


abstract class GetView<T> extends StatelessWidget {
const GetView({Key? key}) : super(key: key);

final String? tag = null;

T get controller => GetInstance().find<T>(tag: tag)!;

@override
Widget build(BuildContext context);
}

通过继承 GetView,就可以直接使用controller.obx构建界面,而 controller.obx 最大的特点是针对 RxStatus 的4个状态分别定义了四个属性:


Widget obx(
NotifierBuilder<T?> widget, {
Widget Function(String? error)? onError,
Widget? onLoading,
Widget? onEmpty,
})


  • NotifierBuilder<T?> widget:实际就是一个携带状态变量,返回正常状态界面的函数,NotifierBuilder<T?>的定义如下。通过这个方法可以使用状态变量构建正常界面。


typedef NotifierBuilder<T> = Widget Function(T state);


  • onError:错误时对应的 Widget构建函数,可以使用错误信息 error

  • onLoading:加载时对应的 Widget

  • onEmpty:数据为空时的 Widget



通过这种方式可以自动根据 change方法指定的 RxStatus 来构建不同状态的 UI 界面,从而避免了丑陋的 if...elseswitch 语句。例如我们的个人主页,可以按下面的方式来写,是不是感觉更清晰和清爽了?


class PersonalHomePageMixin extends GetView<PersonalMixinController> {
PersonalHomePageMixin({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return controller.obx(
(personalEntity) => _PersonalHomePage(personalProfile: personalEntity!),
onLoading: Center(
child: CircularProgressIndicator(),
),
onError: (error) => Center(
child: Text(error!),
),
onEmpty: Center(
child: Text('暂无数据'),
),
);
}
}

对应的PersonalMixinController的代码如下:


class PersonalMixinController extends GetxController
with StateMixin<PersonalEntity> {
final String userId;
PersonalMixinController({required this.userId});

@override
void onReady() {
getPersonalProfile(userId);
super.onReady();
}

void getPersonalProfile(String userId) async {
change(null, status: RxStatus.loading());
var personalProfile = await JuejinService().getPersonalProfile(userId);
if (personalProfile != null) {
change(personalProfile, status: RxStatus.success());
} else {
change(null, status: RxStatus.error('获取个人信息失败'));
}
}
}

Controller 的构建


从 GetView 的源码可以看到,Controller 是从容器中获取的,这就需要使用 GetX 的容器,在使用 Controller 前注册到 GetX 容器中。


Get.lazyPut<PersonalMixinController>(
() => PersonalMixinController(userId: '70787819648695'),
);

总结


本篇介绍了使用GetXStateMixin方式构建更优雅的页面结构,通过controller.obx 的参数配置不同状态对应不同的组件。可以根据 RxStatus 状态自动切换组件,而无需写丑陋的 if...elseswitch 语句。当然,使用这种方式的前提是需要在 GetX 的容器中构建 controller 对象,本篇源码已上传至:GetX 状态管理源码。实际上使用容器能够带来其他的好处,典型的应用就是依赖注入(Dependency Injection,简称DI),接下来我们会使用两篇来介绍依赖注入的概念和具体应用。


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

落地西瓜视频埋点方案,埋点从未如此简单

前言 目前,几乎每个商用应用都有数据埋点的需求。你的 App 是怎么做埋点的呢,有遇到让你 “难顶” 的问题吗? 在这篇文章里,我将带你建立数据埋点的基本认识,还会介绍西瓜视频团队的前端埋点方案,最后为你带来我的落地实现 EasyTrack。如果能帮上忙,请...
继续阅读 »

前言



  • 目前,几乎每个商用应用都有数据埋点的需求。你的 App 是怎么做埋点的呢,有遇到让你 “难顶” 的问题吗?

  • 在这篇文章里,我将带你建立数据埋点的基本认识,还会介绍西瓜视频团队的前端埋点方案,最后为你带来我的落地实现 EasyTrack。如果能帮上忙,请务必点赞加关注,这真的对我非常重要。




目录





1. 数据埋点概述


1.1 为什么要埋点?


“除了上帝,任何人都必须用数据说话”,在数据时代,使用数据驱动产品迭代已经称为行业共识。在分析应用数据之前,首先需要获得数据,这就需要前端或服务端进行数据埋点。


1.2 数据需求的工作流程


首先,你需要了解数据需求的工作流程,需求是如何产生,又是如何流转的,主要分为以下几个环节:



  • 1、需求产生: 产品需求引起产品形态变化,产生新的数据需求;

  • 2、事件设计: 数据产品设计埋点事件并更新数据字典文档,提出埋点评审;

  • 3、埋点开发: 开发进行数据埋点开发;

  • 4、埋点测试: 测试进行数据埋点测试,确保数据质量;

  • 5、数据消费: 数据分析师进行数据分析,推荐系统工程师进行模型训练,赋能产品运营决策。



1.3 数据消费的经典场景



























消费场景需求描述技术需求
渗透率分析统计 DAU/PV/UV/VV 等准确的上报时机
归因分析分析前因后果准确上报上下文 (如场景、会话、来源页面)
1. A / B 测试
2. 个性化推荐
分析用户特征、产品特征等准确上报事件属性

可以看到,在归因分析中,除了需要上报事件本身的属性之外,还需要上报事件产生时的上下文信息,例如当前页面、来源页面、会话等。


1.4 埋点数据采集的基本模型


数据采集是指在前端或服务端收集需要上报的事件属性的过程。为了满足复杂、高效的数据消费需求,需要科学合理地设计端侧的数据采集逻辑,基本可以总结为 “4W + 1H” 模型:





































模型描述举例
1、WHAT什么行为事件名
2、WHEN行为产生的时间时间戳
3、WHO行为产生的对象对象唯一标识 (例如用户 ID、设备 ID)
4、WHERE行为产生的环境设备所处的环境 (例如 IP、操作系统、网络)
5、HOW行为的特征上下文信息 (例如当前页面、来源页面、会话)



2. 如何实现数据埋点?


2.1 埋点方案总结


目前,业界已经存在多种埋点方案,主要分为全埋点、前端代码埋点和服务端代码埋点三种,优缺点和适用场景总结如下:































全埋点前端埋点服务端埋点
优势开发成本低完整采集上下文信息不依赖于前端版本
劣势数据量大,无法获取上下文数据,数据质量低前端开发成本较高服务端开发成本较高、获取上下文信息依赖于接口传值
适用场景通用基础事件(如启动/退出、浏览、点击)核心业务流程(如登录、注册、收藏、购买)核心业务结果事件(如支付成功)



  • 1、全埋点: 指通过编译时插桩、运行时动态代理等 AOP 手段实现自动埋点和上报,无须开发者手动进行埋点,因此也称为 “无埋点”;




  • 2、前端埋点: 指前端 (包括客户端) 开发者手动编码实现埋点,虽然可以通过埋点工具或者脚本简化埋点开发工作,但总体上还是需要手动操作;




  • 3、服务端埋点: 指服务端手动编码实现埋点,缺点是需要客户端需要侵入接口来保留上下文参数。




2.2 全埋点方案的局限性


表面上看,全埋点方案的优势很明显:客户端和服务端只需要一次开发,就能实现所有页面、所有路径的曝光和点击事件埋点,节省了研发人力,也不用担心埋点逻辑会侵入正常业务逻辑。然而,不可能存在完美的解决方案,全埋点方案还是存在一些局限性:




  • 1、资源消耗较大: 全场景上报会产生大量无用数据,网络传输、数据存储和数据计算需要消耗大量资源;




  • 2、页面稳定性要求较高: 需要保持页面视图结构相对稳定,一旦页面视图结果变化,历史录入的埋点数据就会失效;




  • 3、无法采集上下文信息: 无法采集事件产生时的上下文信息,也就无法满足复杂的数据消费需求。




2.3 埋点设计的整体方案


考虑的不同方案都存在优缺点,单纯采用一种埋点方案是不切实际的,需要根据不同业务场景和不同数据消费需要而采用不同的埋点方案:




  • 1、全埋点: 作为全局兜底方案,可以满足粗粒度的统计需求;




  • 2、前端埋点: 作为全埋点的补充方案,可以自定义埋点参数,主要处理核心业务流程事件,例如(如登录、注册、收藏、购买);




  • 3、服务端埋点: 核心业务结果事件,例如订单支付成功。






3. 前端埋点中的困难


3.1 一个简单的埋点场景


现在,我们通过一个具体的埋点场景,试着发现在做埋点需求时会遇到的困难或痛点。我直接使用西瓜视频中的一个埋点场景:



—— 图片引用自西瓜视频技术博客


这个产品场景很简单,左边是西瓜视频的推荐流列表,点击 “电影卡片” 会进入右边的 “电影详情页” 。两个页面中都有 “收藏按钮”,现在的数据需求是采集不同页面中 “收藏按钮” 的点击事件,以便分析用户收藏影片的行为,优化影片的推荐模型。



  • 1、在推荐列表页中上报点击事件:


“event_name" : "click_favorite", // 事件名
"cur_page" : "feed", // 当前页面
"video_id" : "123", // 影片 ID
"video_name" : "影片名", // 影片名
"video_type" : "1", // 影片类型
"$user_id" : "10000", // 用户 ID
"$device_id" : "abc" // 设备 ID
... // 其他预置属性


  • 2、在电影详情页中上报点击事件:


“event_name" : "click_favorite", // 事件名
"from_page" : "feed"
"cur_page" : "video_detail", // 当前页面
"video_id" : "123", // 影片 ID
"video_name" : "影片名", // 影片名
"video_type" : "1", // 影片类型
"$user_id" : "10000", // 用户 ID
"$device_id" : "abc" // 设备 ID
... // 其他预置属性

3.2 现状分析


理解了这个埋点场景之后,我们先梳理出目前遇到的困难:




  • 1、埋点参数分散: 需要上报的埋点参数位于不同 UI 容器或不同业务模块,代码跨度很大(例如:Activity、Fragment、ViewHolder、自定义 View);




  • 2、组件复用: 组件抽象复用后在多个页面使用(例如通用的 ViewHolder 或自定义 View);




  • 3、数据模型不一致: 不同场景 / 页面下描述状态的数据模型不一致,需要额外的转换适配过程(例如有的模型用 video_type 表示影片类型,另一些模型用 videoType 表示影片类型)。




3.3 评估标准


理解了问题和现状,现在我们开始尝试找到解决方案。为此,我们需要想清楚理想中的解决方案,应该满足什么标准:



  • 1、准确性: 这是核心目标,能够在保证不同场景 / 页面下准确收集埋点数据;

  • 2、简洁性: 使用方法尽可能简单,收敛模板代码;

  • 3、可用性: 尽可能高效稳定,不容易出错,性能开销小。


3.4 常规解决方案


1、逐级传递 —— 通过面向对象的关系逐级传递埋点参数:


通过 Android 框架支持的 Activity / Fragment 参数传递方式和面向对象程序设计,逐级将埋点参数传递到最深层的收藏按钮。例如:




  • 列表页: Activity -> ViewModel -> FeedFragment (推荐) -> Adapter -> ViewHolder (电影卡片) -> CollectButton (收藏按钮)




  • 详情页: Activity -> ViewModel -> DetailBottomFragment(底部功能区) -> CollectButton (收藏按钮)




缺点 (参数传递困难) :传递数据需要编写大量重复模板代码,工程代码膨胀,增大维护难度。再叠加上组件复用的情况,逐级传递会让代码复杂度非常高,很明显不是一个合理的解决方案。


2、Bean 传递 —— 在 Java Bean 中增加字段来收集埋点参数:


缺点 (违背单一职责原则):Java Bean 中侵入了与业务无关的埋点参数,同时会造成 Java Bean 数据冗余,增大维护难度。


3、全局单例 —— 通过全局单例对象来收集埋点参数:


这个方案与 “Bean 传递 ” 类似,区别在于埋点参数从 Java Bean 中移动到全局单例中,但缺点还是很明显:


缺点 (写入和清理时机):单例会被多个位置写入,一旦被覆盖就无法被恢复,容易导致上报错误;另外清理的时机也难以把握,清理过早会导致埋点参数丢失,清理过晚会污染后面的埋点事件。




4. 西瓜视频方案


理解了数据埋点开发中的困难,有没有什么方案可以简化埋点过程中的复杂度呢?我们来讨论下西瓜视频团队分享的一个思路:基于视图树收集埋点参数。




—— 图片引用自西瓜视频技术博客


通过分析数据与视图节点的关系可以发现,事件的埋点数据正好分布在视图树的不同节点中。当 “收藏按钮” 触发事件时,只需要沿着视图树逐级向上查找 (通过 View#getParent()) 就可以收集到所有数据。


并且,树的分支天然地支持为参数设置不同的值。例如 “推荐 Fragment” 需要上报 “channel : recomment”,而 “电影 Fragment” 需要上报 “channel : film”。因为 Fragment 的根布局对应有视图树中的不同节点,所以在不同 Fragment 中触发的事件最终收集到的 “channel” 参数值也就不同了。Nice~




5. EasyTrack 埋点框架


思路 Get 到了,现在我们来讨论如何应用这个思路来解决问题。贴心的我已经帮你实现为一个框架 EasyTrack。源码地址:github.com/pengxurui/E…


5.1 添加依赖



  • 1、依赖 JitPack 仓库


在项目级 build.gradle 声明远程仓库:


allprojects {
repositories {
google()
mavenCentral()
// JitPack 仓库
maven { url "https://jitpack.io" }
}
}


  • 2、依赖 EasyTrack 框架


在模块级 build.gradle 中依赖类库:


dependencies {
...
// 依赖 EasyTrack 框架
implementation 'com.github.pengxurui:EasyTrack:v1.0.1'
// 依赖 Kotlin 工具(非必须)
implementation 'com.github.pengxurui:KotlinUtil:1.0.1'
}

5.2 依附埋点参数到视图树


ITrackModel接口定义了一个数据填充能力,你可以创建它的实现类来定义一个数据节点,并在 fillTrackParams() 方法中声明参数。例如:MyGoodsViewHolder 实现了 ITrackMode 接口,在 fillTrackParams() 方法中声明参数(goods_id / goods_name)。


随后,通过 View 的扩展函数View.trackModel()将其依附到视图节点上。扩展函数 View.trackModel() 内部基于 View#setTag() 实现。


MyGoodsViewHolder.kt


class MyGoodsViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView), ITrackModel {

private var mItem: GoodsItem? = null

init {
// Java:EasyTrackUtilsKt.setTrackModel(itemView, this);
itemView.trackModel = this
}

override fun fillTrackParams(params: TrackParams) {
mItem?.let {
params.setIfNull("goods_id", it.id)
params.setIfNull("goods_name", it.goods_name)
}
}
}

EasyTrackUtils.kt


/**
* Attach track model on the view.
*/
var View.trackModel: ITrackModel?
get() = this.getTag(R.id.tag_id_track_model) as? ITrackModel
set(value) {
this.setTag(R.id.tag_id_track_model, value)
}

ITrackModel.kt


/**
* 定义数据填充能力
*/
interface ITrackModel : Serializable {
/**
* 数据填充
*/
fun fillTrackParams(params: TrackParams)
}

5.3 触发事件埋点


在需要埋点的地方,直接通过定义在 View 上的扩展函数 trackEvent(事件名)触发埋点事件,它会以该扩展函数的接收者对象为起点,逐级向上层视图节点收集参数。另外,它还有多个定义在 Activity、Fragment、ViewHolder 上的扩展函数,但最终都会调用到 View.trackEvent。


class MyGoodsViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
fun bind(item: GoodsItem) {
...
trackEvent(GOODS_EXPOSE)
}
}

EasyTrackUtils.kt


@JvmOverloads
fun Activity?.trackEvent(eventName: String, params: TrackParams? = null) =
findRootView(this)?.doTrackEvent(eventName, params)

@JvmOverloads
fun Fragment?.trackEvent(eventName: String, params: TrackParams? = null) =
this?.requireView()?.doTrackEvent(eventName, params)

@JvmOverloads
fun RecyclerView.ViewHolder?.trackEvent(eventName: String, params: TrackParams? = null) {
this?.itemView?.let {
if (null == it.parent) {
it.post { it.doTrackEvent(eventName, params) }
} else {
it.doTrackEvent(eventName, params)
}
}
}

@JvmOverloads
fun View?.trackEvent(eventName: String, params: TrackParams? = null): TrackParams? =
this?.doTrackEvent(eventName, params)

查看 logcat 日志,可以看到以下日志,显示埋点并没有生效。这是因为没有为 EasyTrack 配置埋点数据上报和统计分析的能力。


logcat 日志


EasyTrackLib: Try track event goods_expose, but the providers is Empty.

5.4 实现 ITrackProvider 接口


EasyTrack 的职责在于收集分散的埋点数据,本身没有提供埋点数据上报和统计分析的能力。因此,你需要实现 ITrackProvider 接口进行依赖注入。例如,这里模拟实现友盟数据埋点提供器,在 onInit() 方法中进行初始化,在 onEvent() 方法中调用友盟 SDK 事件上报方法。


MockUmengProvider.kt


/**
* 模拟友盟数据上报
*/
class MockUmengProvider : ITrackProvider() {

companion object {
const val TAG = "Umeng"
}

/**
* 是否启用
*/
override var enabled = true

/**
* 名称
*/
override var name = TAG

/**
* 初始化
*/
override fun onInit() {
Log.d(TAG, "Init Umeng provider.")
}

/**
* 执行事件上报
*/
override fun onEvent(eventName: String, params: TrackParams) {
Log.d(TAG, params.toString())
}
}

5.5 配置 EasyTrack


在应用初始化时,进行 EasyTrack 的初始化配置。我们可以将相关的初始化代码单独封装起来,例如:


StatisticsUtils.kt


// 模拟友盟数据统计提供器
val umengProvider by lazy {
MockUmengProvider()
}

// 模拟神策数据统计提供器
val sensorProvider by lazy {
MockSensorProvider()
}

/**
* 初始化 EasyTrack,在 Application 初始化时调用
*/
fun init(context: Context) {
configStatistics(context)
registerProviders(context)
}

/**
* 配置
*/
private fun configStatistics(context: Context) {
// 调试开关
EasyTrack.debug = BuildConfig.DEBUG
// 页面间参数映射
EasyTrack.referrerKeyMap = mapOf(
CUR_PAGE to FROM_PAGE,
CUR_TAB to FROM_TAB
)
}

/**
* 注册提供器
*/
private fun registerProviders(context: Context) {
EasyTrack.registerProvider(umengProvider)
EasyTrack.registerProvider(sensorProvider)
}

EventConstants.java


public static final String FROM_PAGE = "from_page";
public static final String CUR_PAGE = "cur_page";
public static final String FROM_TAB = "from_tab";
public static final String CUR_TAB = "cur_tab";


























配置类型描述
debugBoolean调试开关
referrerKeyMapMap<String,String>全局页面间参数映射
registerProvider()ITrackProvider底层数据埋点能力

以上步骤是 EasyTrack 的必选步骤,完成后重新执行 trackEvent() 后可以看到以下日志:


logcat 日志


/EasyTrackLib:  
onEvent:goods_expose
goods_id= 10000
goods_name = 商品名
Try track event goods_expose with provider Umeng.
Try track event goods_expose with provider Sensor.
------------------------------------------------------

5.6 页面间参数映射


上一节中有一个referrerKeyMap配置项,定义了全局的页面间参数映射。 举个例子,在分析不同入口的转化率时,不仅仅需要上报当前页面的数据,还需要上报来源页面的信息。这样我们才能分析用户经过怎样的路径来到当前页面,并最终触发了某个行为。


需要注意的是,来源页面的参数往往不能直接添加到当前页面的埋点参数中,这里一般会有一定的转换规则 / 映射关系。例如:来源页面的 cur_page 参数,在当前页面应该映射为 from_page 参数。 在这个例子里,我们配置的映射关系是:



  • 来源页面的 cur_page 映射为当前页面的 from_page;

  • 来源页面的 cur_tab 映射为当前页面的 from_tab。


因此,假设来源页面传递给当前页面的参数是 A,则当前页面在触发事件时的收集参数是 B:


A (来源页面):
{
"cur_page" : "list"
...
}

B (当前页面):
{
"cur_page" : "detail",
"from_page" : "list",
...
}

BaseTrackActivity 实现了页面间参数映射,你可以创建 BaseActivity 类并继承于 BaseTrackActivity,或者将其内部的逻辑迁移到你的 BaseActivity 中。这一步是可选的,如果你不使用页面间参数映射的特性,你那大可不必使用 BaseTrackActivity。



















操作描述
定义映射关系1、EasyTrack.referrerKeyMap 配置项
2、重写 BaseTrackActivity #referrerKeyMap() 方法
传递页面间参数Intent.referrerSnapshot(TrackParams) 扩展函数

MyGoodsDetailActivity.java


public class MyGoodsDetailActivity extends MyBaseActivity {

private static final String EXTRA_GOODS = "extra_goods";

public static void start(Context context, GoodsItem item, TrackParams params) {
Intent intent = new Intent(context, GoodsDetailActivity.class);
intent.putExtra(EXTRA_GOODS, item);
EasyTrackUtilsKt.setReferrerSnapshot(intent, params);
context.startActivity(intent);
}

@Nullable
@Override
protected String getCurPage() {
return GOODS_DETAIL_NAME;
}

@Nullable
@Override
public Map<String, String> referrerKeyMap() {
Map<String, String> map = new HashMap<>();
map.put(STORE_ID, STORE_ID);
map.put(STORE_NAME, STORE_NAME);
return map;
}
}

需要注意的是,BaseTrackActivity 不会将来源页面的全部参数都添加到当前页面的参数中,只有在全局 referrerKeyMap 配置项或 referrerKeyMap() 方法中定义了映射关系的参数,才会添加到当前页面。 例如:MyGoodsDetailActivity 继承于 BaseActivity,并重写 referrerKeyMap() 定义了感兴趣的参数(STORE_ID、STORE_NAME)。最终触发埋点时的日志如下:


logcat 日志


/EasyTrackLib:  
onEvent:goods_detail_expose
goods_id= 10000
goods_name = 商品名
store_id = 10000
store_name = 商店名
from_page = Recommend
cur_page = goods_detail
Try track event goods_expose with provider Umeng.
Try track event goods_expose with provider Sensor.
------------------------------------------------------

在一般的埋点模型中,每个 Activity (页面) 都有对应一个唯一的 page_id,因此你可以重写 fillTrackParams() 方法追加这些固定的参数。例如:MyBaseActivity 定义了 getCurPage() 方法,子类可以通过重写 getCurPage() 来设置 page_id。


MyBaseActivity.java


abstract class MyBaseActivity : BaseTrackActivity() {

@CallSuper
override fun fillTrackParams(params: TrackParams) {
super.fillTrackParams(params)
// 填充页面统一参数
getCurPage()?.also {
params.setIfNull(CUR_PAGE, it)
}
}

protected open fun getCurPage(): String? = null
}

5.7 TrackParams 参数容器


TrackParams 是 EasyTrack 收集参数的中间容器,最终会分发给 ITrackProvider 使用。



























方法描述
set(key: String, value: Any?)设置参数,无论无何都覆盖
setIfNull(key: String, value: Any?)设置参数,如果已经存在该参数则丢弃
get(key: String): String?获取参数值,参数不存在则返回 null
get(key: String, default: String?)获取参数值,参数不存在则返回默认值 default

5.8 使用 Kotlin 委托依附参数


如果你觉得每次定义 ITrackModel 数据节点后都需要调用 View.trackModel,你可以使用我定义的 Kotlin 委托 “跳过” 这个步骤,例如:


MyFragment.kt


private val trackNode by track()

EasyTrackUtils.kt


fun <F : Fragment> F.track(): TrackNodeProperty<F> = FragmentTrackNodeProperty()

fun RecyclerView.ViewHolder.track(): TrackNodeProperty<RecyclerView.ViewHolder> =
LazyTrackNodeProperty() viewFactory@{
return@viewFactory itemView
}

fun View.track(): TrackNodeProperty<View> = LazyTrackNodeProperty() viewFactory@{
return@viewFactory it
}

如果你还不了解委托属性,可以看下我之前写过的一篇文章,这里不解释其原理了:Android | ViewBinding 与 Kotlin 委托双剑合璧




6. EasyTrack 核心源码


这一节,我简单介绍下 EasyTrack 的核心源码,最核心的部分在入口类 EasyTrack 中:


6.1 doTrackEvent()


doTrackEvent() 是触发埋点的主方法,主要流程是调用 fillTrackParams() 收集埋点参数,再将参数分发给有效的 ITrackProvider。


internal fun Any.doTrackEvent(eventName: String, otherParams: TrackParams? = null): TrackParams? {
1. 检查是否有有效的 ITrackProvider
2. 基于视图树递归收集埋点参数(fillTrackParams)
3. 日志
4. 将收集到的埋点参数分发给有效的 ITrackProvider
}

6.2 fillTrackParams()


-> 基于视图树递归收集埋点参数
internal fun fillTrackParams(node: Any?, params: TrackParams? = null): TrackParams {
val result = params ?: TrackParams()
var curNode = node
while (null != curNode) {
when (curNode) {
is View -> {
// 1. 视图节点
if (android.R.id.content == curNode.id) {
// 1.1 Activity 节点
val activity = getActivityFromView(curNode)
if (activity is IPageTrackNode) {
// 1.1.1 IPageTrackNode节点(处理页面间参数映射)
activity.fillTrackParams(result)
curNode = activity.referrerSnapshot()
} else {
// 1.1.2 终止
curNode = null
}
} else {
// 1.2 Activity 视图子节点
curNode.trackModel?.fillTrackParams(result)
curNode = curNode.parent
}
}
is ITrackNode -> {
// 2. 非视图节点
curNode.fillTrackParams(result)
curNode = curNode.parent
}
else -> {
// 3. 终止
curNode = null
}
}
}
return result
}

主要逻辑:从入参 node 为起点,循环获取依附在视图节点上的 ITrackModel 数据节点并调用 fillTrackParams() 方法收集参数,并将循环指针指向 parent。




7. 总结


EasyTrack 框架的源码我已经放在 Github 上了,源码地址:github.com/pengxurui/E… 我也写了一个简单的 Sample Demo,你可以直接运行体验下。欢迎批评,欢迎 Issue~


说说目前遇到的问题,在处理页面间参数传递时,我们需要依赖 Intent extras 参数。这就导致我们需要在大量创建 Intent 的地方都加入来源页面的埋点参数(注意:即使你不使用 EasyTrack,你也要这么做)。目前我还没有想到比较好的方法,你觉得呢?说说你的看法吧。


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

【Flutter 状态管理】第一论: 对状态管理的看法与理解

前言 由 编程技术交流圣地[-Flutter群-] 发起的 状态管理研究小组,将就 状态管理 相关相关话题进行为期 两个月 的讨论。小组将于两个月后解散,并发布相关讨论成果。 目前只有内定的 5 个人参与讨论,如果你对状态管理有什么独特的见解,或想参与其中。...
继续阅读 »
前言

编程技术交流圣地[-Flutter群-] 发起的 状态管理研究小组,将就 状态管理 相关相关话题进行为期 两个月 的讨论。小组将于两个月后解散,并发布相关讨论成果。



目前只有内定的 5 个人参与讨论,如果你对状态管理有什么独特的见解,或想参与其中。可以发表一篇自己对状态管理的认知文章,作为入群的“门票”,欢迎和我们共同交流。




前两周进行第一个话题的探讨 :


你对状态管理的看法与理解



状态管理,状态管理。顾名思义是状态+管理,那问题来了,到底什么是状态?为什么要管理呢?


一、何谓状态


1. 对状态概念的思考

其实要说明一个东西是什么,是非常困难的。这并不像数学中能给出具体的定义,比如


平行四边形: 是在同一个二维平面内,由两组平行线段组成的闭合图形
三角形: 是由同一平面内不在同一直线上的三条线段首尾顺次连接所组成的封闭图形

如果具有明确定义的概念,我们可以很容易理解它的特性和作用。但对于 状态 这种含义比较笼统的词汇,那就仁者见仁,智者见智 了。我查了一下,对于状态而言有如下解释:


状态是人或事物表现出来的形态。是指现实(或虚拟)事物处于生成、生存、发展、消亡时期
或各转化临界点时的形态或事物态势。

如果影射到编程上,状态就是界面各个时期的表现,状态的改变,通过刷新后会导致界面的变化。那 界面状态 有什么区别和联系呢?


比如说一颗种子发芽、长大、开花、结果、枯萎,这是外在的表征,是外界所看到的形态变化。但从根本上来说,这些变化是种子与外界的资源交换,导致的内部数据变化,而产生的结果。也就是一个是 面子 ,一个是 里子


看花人并不会在意种子的内部的变化逻辑,他们只需满足看花的需求就行了。 也就是说 界面是表现 ,是用来给用户看的;状态是本质 ,是需要编程者去维护的。如果一个开发者只能看到 面子 ,而忽略我们本身就是那颗种子,还谈什么状态,想什么管理?。




2.状态、交互与界面

对一个应用而言,最根本的目的在于: 用户 通过操作界面, 可以进行正确的逻辑处理,并得到一定的响应反馈





从用户的角度来看,应用内部运作机制是个 黑盒,用户不需要、也没必要了解细节。但这个黑盒内部逻辑处理需要编程者进行实现,我们是无法逃避的。



拿我们最熟悉的计数器而言,点击按钮,修改状态信息,重新构建后,实现界面上数字变化的效果。





二、为什么需要管理


说到 管理 一词,你觉得什么情况下需要管理?是 复杂,只有 复杂 才有管理的必要。那管理有什么好处?


比如张三开了一家餐馆,雇了四个人,他们各干各的,都要同时进行招乎食客、烧菜、送快递、清洁等任务,那效率将非常低下。如果菜里吃出了不明生物 (bug),也不容易定位问题根源。这很像什么东西都塞在一个 XXXState 里去完成,其中不仅需要处理组件构建逻辑,还掺杂着大量的业务逻辑


如果将复杂的事务,分层次地交由不同人进行处理,各司其职,要比四个人各干各的要高效。而管理的目的就是分层级提高地 处理任务。




1.状态的作用范围

首先来思考一个问题:是不是所有的状态都需要管理?比如说下面的 FloatingActionButton ,在点击时会有水波纹的效果,界面的变化就意味着存在着状态的变化



FloatingActionButton 组件继承自 StatelessWidget,也就是说它并没有改变自身状态的能力。那点击时,为什么状态会发生变化呢?因为它在 build 中使用了 RawMaterialButton 组件,RawMaterialButton 中使用了 InkWell ,而 InkWell 继承自 InkResponseInkResponsebuild 中使用了_InkResponseStateWidget ,这个组件中维护了水波纹在手势中的状态变化逻辑。


class FloatingActionButton extends StatelessWidget{

---->[FloatingActionButton#build]----
Widget result = RawMaterialButton(
onPressed: onPressed,
mouseCursor: mouseCursor,
elevation: elevation,
focusElevation: focusElevation,
hoverElevation: hoverElevation,
highlightElevation: highlightElevation,
disabledElevation: disabledElevation,
constraints: sizeConstraints,
materialTapTargetSize: materialTapTargetSize,
fillColor: backgroundColor,
focusColor: focusColor,
hoverColor: hoverColor,
splashColor: splashColor,
textStyle: extendedTextStyle,
shape: shape,
clipBehavior: clipBehavior,
focusNode: focusNode,
autofocus: autofocus,
enableFeedback: enableFeedback,
child: resolvedChild,
);



也就是说:点击时,水波纹的变化效果,被封装在 _InkResponseStateWidget 组件状态中。像这种私有的状态,我们并不需要进行管理,因为它能够独立完成自己任务,而且外界并不需要了解这些状态。比如水波纹的圆心半径等会变化的状态信息,在外界是不关心的。

Flutter 中的 State 本身就是一种状态管理的手段。因为:


1. State 具有根据状态信息,构建组件的能力
2. State 具有重新构建组件的能力

所有的 StatefulWidget 都是这样,变化逻辑及状态量都会被封装在对应的 XXXState 类中。是局部的,私有的,外界无需了解内部状态的信息变化,也没有可以直接访问的途径。这一般用于对组件的封装,将复杂且相对独立的状态变化,封装起来,简化用户使用。




2.状态的共享及修改同步

上面说的 State 管理状态虽然非常小巧,方便。但同时也会存在不足之处,因为状态量被维护在 XXXState 内部,外界很难访问修改。比如下面 page1 中,C 是数字信息,跳转到 page2 时,也要显示这个数值,且按下 R 按钮能要让 page1page2 的数字都重置为 0。这就存在着状态存在共享及修改同步更新,该如何实现呢?





我们先来写个如下的设置界面:



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

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('设置界面'),),
body: Container(
height: 54,
color: Colors.white,
child: Row(
children: [
const SizedBox(width: 10,),
Text('当前计数为:'),
Spacer(),
ElevatedButton(child: Text('重置'),onPressed: (){} ),
const SizedBox(width: 10,)
],
),
),
);
}
}

那如何知道当前的数值,以及如何将 重置 操作点击时,影响 page1 的数字状态呢?其实 构造入参回调函数 可以解决一切的数据共享和修改同步问题。




3.代码实现 - setState 版:源码位置

在点击重置时 ,由于 page2 的计数也要清空,这就说明其状态量需要变化,要用 StatefulWidget 维护状态。在构造时,通过构造方法传入 initialCounter ,让 page2 的数字可以与 page1 一致。通过 onReset 回调函数来监听重置按钮的触发,以此来重置 page1 的数字状态,让 page1 的数字可以与 page2 一致。这就是让两个界面的同一状态量保持一致。如下图:


class SettingPage extends StatefulWidget {
final int initialCounter;
final VoidCallback onReset;

const SettingPage({
Key? key,
required this.initialCounter,
required this.onReset,
}) : super(key: key);

@override
State<SettingPage> createState() => _SettingPageState();
}














跳转到设置页设置页重置

class _SettingPageState extends State<SettingPage> {
int _counter = 0;

@override
void initState() {
super.initState();
_counter = widget.initialCounter;
}

//构建同上, 略...

void _onReset() {
widget.onReset();
setState(() {
_counter = 0;
});
}

_SettingPageState 中维护 _counter 状态量,在点击 重置 时执行 _onReset 方法,触发 onReset 回调。在 界面1 中监听 onReset ,来重置 界面1 的数字状态。这样通过 构造入参回调函数 ,就能保证两个界面 数字状态信息 的同步。


---->[界面1 跳转代码]-----
Navigator.push(context,
MaterialPageRoute(builder: (context) => SettingPage(
initialCounter: _counter,
onReset: (){
setState(() {
_counter=0;
});
},
)));

但这样,确定也很明显,数据传来传去,调来调去,非常麻烦,乱就容易出错。如果再多几个需要共享的信息,或者在其他界面里还需要共享这个状态,那代码里将会更加混乱。




4.代码实现 - ValueListenableBuilder 版:源码位置

上面的 setState 版实现 数据共享和修改同步,除了代码混乱之外,还有一些其他的缺点。首先,在 SettingPage 中我们又维护了一个状态信息,两个界面的信息虽然相同,却是两份一样的。如果状态信息是比较大的对象,这未免会造成不必要的内存浪费。





其次,就是深为大家诟病的 setState 重构范围。State#setState 执行后,会触发 build 方法重新构建组件。比如在 page1 中,_MyHomePageState#build 构建的是 Scaffold ,当状态变化时触发 setState ,其下的所有组件都会被构建一遍,重新构建的范围过大。

大家可以想一下,这里为什么不把 Scaffold 提到外面去?原因是:FloatingActionButton 组件需要修改状态量 _counter 并执行重新构建,所以不得不扩大构建的范围,来包含住 FloatingActionButton





其实 Flutter 中有个组件可以解决上面两个问题,那就是 ValueListenableBuilder 。使用方式很简单,先创建一个 ValueNotifier 的可监听对象 _counter


class _MyHomePageState extends State<MyHomePage> {

final ValueNotifier<int> _counter = ValueNotifier(0);

@override
void dispose() {
super.dispose();
_counter.dispose();
}

void _incrementCounter() {
_counter.value++;
}

如下使用 ValueListenableBuilder 组件,监听 _counter 对象,当该可监听对象的数值变化时,会可以通知监听者,重新构建 builder 方法里的组件。这样最大的好处在于:不需要 通过 _MyHomePageState#setState 对内部整体进行构建,仅对需要改变的局部 进行重新构建。


ValueListenableBuilder(
valueListenable: _counter,
builder: (ctx, int value, __) => Text(
'$value',
style: Theme.of(context).textTheme.headline4,
),
),



可以将 对于_counter 可见听对象传入 page2 中,同样通过 ValueListenableBuilder 监听 counter。这就相当于观察者模式中,两个订阅者 同时监听一个发布者 。在 page2 中让发布者信息变化,也会通知两个订阅者,比如执行 counter.value =0 ,两处的 ValueListenableBuilder 都会触发局部重建。



这样就能达到和 setState 版 一样的效果,通过 ValueListenableBuilder 简化了入参和回调通知,并具有局部重构组件的能力。可以说 State状态的共享及修改同步 方面是被 ValueListenableBuilder 完胜的。但话说回来, State 本来就不是做这种事的,它更注重于私有状态的处理。比如ValueListenableBuilder 的本质,就是一个通过 State 实现的私有状态封装 ,所以没有什么好不好,只有适合或不适合。





三、使用状态管理工具


1. 状态管理工具的必要性

其实前面的 ValueListenableBuilder 的效果以及不错了,但是在某些场合仍存在不足。因为 _counter 需要通过构造方法进行传递,如果状态量过多,或共享场合变多、传递层级过深,也会使代码处理比较复杂。最致命的一点是:业务逻辑处理界面组件都耦合在 _MyHomePageState 中,这对于拓展维护而言并不是件好事。所以 管理 对于 复杂逻辑性下的状态的共享及修改同步 是有必要的。





2.通过 flutter_bloc 实现状态管理: 源码位置

我们前面说过,状态管理的目的在于:让状态可以共享及在更新状态时可以同步更新相关组件显示,且将状态变化逻辑界面构建进行分离。flutter_bloc 是实现状态管理的工具之一,它的核心是:通过 BlocEvent 操作转化成 State;同时通过 BlocBuilder 监听状态的变化,进行局部组件构建。


通过这种方式,编程者可以将 状态变化逻辑 集中在 Bloc 中处理。当事件触发时,通过发送 Event 指令,让 Bloc 驱动 State 进行变化。就这个小案例而言,主要有两个事件: 自加重置 。像这样不需要参数的 Event , 通过枚举进行区分即可,比如定义事件:


enum CountEvent {
add, // 自加
reset, // 重置
}



状态,就是界面构建需要依赖的信息。这里定义 CountState ,持有 value 数值。


class CountState {
final int value;
const CountState({this.value = 0});
}



最后是 Bloc ,新版的 flutter_bloc 通过 on 监听事件,通过 emit 产出新状态。如下在构造中通过 on 来监听 CountEvent 事件,通过 _onCountEvent 方法进行处理,进行 CountState 的变化。当 event == CountEvent.add 时,会产出一个原状态 +1 的新 CountState 对象。


class CountBloc extends Bloc<CountEvent, CountState> {
CountBloc() : super(const CountState()){
on<CountEvent>(_onCountEvent);
}

void _onCountEvent(CountEvent event, Emitter<CountState> emit) {
if (event == CountEvent.add) {
emit(CountState(value: state.value + 1));
}

if (event == CountEvent.reset) {
emit (const CountState(value: 0));
}
}
}

画一个简单的示意图,如下:点击 _incrementCounter 时,只需要触发 CountEvent.add 指令即可。核心的状态处理逻辑会在 CountBloc 中进行,并生成新的状态,且通过 BlocBuilder 组件 触发局部更新 。这样,状态变化的逻辑界面构建的逻辑就能够很好地分离。



// 发送自加事件指定
void _incrementCounter() {
BlocProvider.of<CountBloc>(context).add(CountEvent.add);
}

//构建数字 Text 处使用 BlocBuilder 局部更新:
BlocBuilder<CountBloc, CountState>(
builder: _buildCounterByState,
),

Widget _buildCounterByState(BuildContext context, CountState state) {
return Text(
'${state.value}',
style: Theme.of(context).textTheme.headline4,
);
}



这样,设置界面的 重置 按钮也是类似,只需要发出 CountEvent.reset 指令即可,核心的状态处理逻辑会在 CountBloc 中进行,并生成新的状态,且通过 BlocBuilder 组件 触发局部更新





由于 BlocProvider.of<CountBloc>(context) 获取 Bloc 对象,需要上级的上下文存在该 BlocProvider ,可以在最顶层进行提供。这样在任何界面中都可以获取该 Bloc 及对其状态进行共享。



这是个比较小的案例,可能无法体现 Bloc 的精髓,但作为一个入门级的体验还是挺不错的。你需要自己体会一下:


[1]. 状态的 [共享] 及 [修改状态] 时同步更新。
[2]. [状态变化逻辑] 和 [界面构建逻辑] 的分离。

个人认为,这两点是状态管理的核心。也许每个人都会有各自的认识,但至少你不能在不知道自己要管理什么的情况下,做着表面上认为是状态管理的事。最后总结一下我的观点:状态就是界面构建需要依赖的信息;而管理,就是通过分工,让这些状态信息可以更容易维护更便于共享更好同步变化更'高效'地运转flutter_bloc 只是 状态管理 的工具之一,而其他的工具,也不会脱离这个核心。




四、官方案例 - github_search 解读


1. 案例介绍:源码位置

为了让大家对 flutter_bloc 在逻辑分层上有更深的认识,这里选取了 flutter_bloc 官方的一个案例进行解读。下面先简单看一下界面效果:


[1] 输入字符进行搜索,界面显示 github 项目
[2] 在不同的状态下显示不同的界面,如未输入、搜索中、搜索成功、无数据。
[3] 输入时防抖 debounce。避免每输入一个字符都请求接口。

注: debounce : 当调用动作 n 毫秒后,才会执行该动作,若在这 n 毫秒内又调用此动作则将重新计算执行时间。















搜索状态变化无数据时状态显示

项目结构


├── bloc         # 处理状态变化逻辑
├── view # 处理视图构建
├── repository # 处理数据获取逻辑
└── main.dart # 程序入口



2.仓储层 repository

我们先来看一下仓储层 repository ,这是将数据获取逻辑单独抽离出来,其中包含model 包下相关数据实体类 ,和 api 包下数据获取操作。



有人可能会问,业务逻辑都放在 Bloc 里处理不就行了吗,为什么非要搞个 repository 层。其实很任意理解,Bloc 核心是处理状态的变化,如果接口请求代码都放在 Bloc 里就显得非常臃肿。更重要的有点是: repository 层是相对独立的,你完全可以单独对进行测试,保证数据获取逻辑的正确性。


这样能带来另一个好处,当数据模型确定后。repository 层和界面层完全可以同步进行开发,最后通过 Bloc 层将 repository界面 进行整合。分层是进行管理的一种手段,就像不同部门来处理不同的事务,一旦出错,就很容易定位是哪个环节出了问题。当一个部门的进行拓展升级,也能尽可能不波及其他部门。



repository 层也是通用的,不管是 Bloc 也好、Provider 也好,都只是管理的一种手段。repository 层作为数据的获取方式是完全独立的,比如 todo 的案例,Bloc 版和 Provider 可以共用一个 repository 层,因为即使框架的使用方式有差异,但数据的获取方式是不变的。




下面来简单看一下repository 层的逻辑,GithubRepository 依赖两个对象,只有一个 search 方法。其中 GithubCache 类型 cache 对象用于记录缓存,在查询时首先从缓存中查看,如果已存在,则返回缓存数据。否则使用 GithubClient 类型的 client 对象进行搜索。





GithubClient 主要通过 http 获取网络数据。





GithubClient 就是通过一个 Map 维护搜索字符搜索结果的映射。这了处理的比较简单,完全可以基于此进行拓展:比如设置一个缓存数量上限,不然随着搜索缓存会一直加入;或将缓存加入数据库,支持离线缓存。将 repository 层独立出来后,这些功能的拓展就能和界面层解耦。因为界面只关心数据本身,并不关心数据如何缓存、如何获取。





3. bloc 层

首先来看事件,整个搜索功能只有一个事件:文字输入时的TextChanged,事件触发时需要附带搜索的信息字符串。


abstract class GithubSearchEvent extends Equatable {
const GithubSearchEvent();
}

class TextChanged extends GithubSearchEvent {
const TextChanged({required this.text});

final String text;

@override
List<Object> get props => [text];

@override
String toString() => 'TextChanged { text: $text }';
}



至于状态,整个过程中有四类状态:



  • [1]. SearchStateEmpty : 输入字符为空时的状态,无维护数据。

  • [2]. SearchStateLoading : 从请求开始到响应中的等待状态,无维护数据。

  • [3]. SearchStateSuccess: 请求成功的状态,维护 SearchResultItem 条目列表。

  • [4]. SearchStateError:失败状态,维护错误信息字符串。





最后是 Bloc,用于整合状态变化的逻辑。在 构造方法 中通过 onTextChanged 事件进行监听,触发 _onTextChanged 产出状态。比如 searchTerm.isEmpty 说明无字符输入,产出 SearchStateEmpty 状态。在 githubRepository.search 获取数据前,产出 SearchStateLoading 表示等待状态。请求成功则产出 SearchStateSuccess 状态,且内含结果数据,失败则产出 SearchStateError 状态。


class GithubSearchBloc extends Bloc<GithubSearchEvent, GithubSearchState> {
GithubSearchBloc({required this.githubRepository})
: super(SearchStateEmpty()) {
on<TextChanged>(_onTextChanged);
}

final GithubRepository githubRepository;

void _onTextChanged(
TextChanged event,
Emitter<GithubSearchState> emit,
) async {
final searchTerm = event.text;

if (searchTerm.isEmpty) return emit(SearchStateEmpty());

emit(SearchStateLoading());

try {
final results = await githubRepository.search(searchTerm);
emit(SearchStateSuccess(results.items));
} catch (error) {
emit(error is SearchResultError
? SearchStateError(error.message)
: const SearchStateError('something went wrong'));
}
}
}

到这里,整个业务逻辑就完成了,不同时刻的状态变化也已经完成,接下来只需要通过 BlocBuilder 监听状态变化,构建组件即可。另外说明一下 debounce 的作用:如果不进行防抖处理,每次输入字符都会触发请求获取数据,这样会造成请求非常频繁,而且过程中的输入大多数是无用的。这种情况,就可以使用 debounce 进行处理,比如,输入 300 ms 后才进行请求操作,如果在此期间有新的输入,就重新计时。
其本质是对流的转换操作,在 stream_transform 插件中有相关处理,在 pubspec.yaml 中添加依赖


stream_transform: ^2.0.0



on<TextChanged>transformer 参数中可以指定事件流转换器,这样就能完成防抖效果:


const Duration _duration = Duration(milliseconds: 300);

EventTransformer<Event> debounce<Event>(Duration duration) {
return (events, mapper) => events.debounce(duration).switchMap(mapper);
}

class GithubSearchBloc extends Bloc<GithubSearchEvent, GithubSearchState> {
GithubSearchBloc({required this.githubRepository})
: super(SearchStateEmpty()) {
// 使用 debounce 进行转换
on<TextChanged>(_onTextChanged, transformer: debounce(_duration));
}



4.界面层

界面层的处理非常简单,通过 BlocBuilder 监听状态变化,根据不同的状态构建不同的界面元素即可。





事件的触发,是在文字输入时。输入框被单独封装成 SearchBar 组件,在 TextFieldonChanged 方法中,触发 _githubSearchBlocTextChanged 方法,这样驱动点,让整个状态变化的“齿轮组”运转了起来。


---->[search_bar.dart]----
@override
void initState() {
super.initState();
_githubSearchBloc = context.read<GithubSearchBloc>();
}

return TextField(
//....
onChanged: (text) {
_githubSearchBloc.add(TextChanged(text: text));
},

这样一个简单的搜索需求就完成了,flutter_bloc 还通过了非常多的实例、文档,有兴趣的可以自己多研究研究。




五、小结


这里小结一下我对状态管理的理解:


[1]. [状态] 是界面构建需要依赖的信息。
[2]. [管理] 是对复杂场景的分层处理,使[状态变化逻辑]独立于[视图构建逻辑]。

再回到那个最初的问题,是所有的状态都需要管理吗?如何区分哪些状态需要管理?就像前端 redux 状态管理,在 You Might Not Need Redux (可自行百度译文) 中说到:人们常常在正真需要 Redux 之前,就选择使用它 。对于状态管理,其实都是这样,往往初学者 "趋之若鹜" ,不明白为什么要状态管理,为什么一个很简单的功能,非要弯弯绕绕一大圈来实现。就是看到别用了,使用我也要用,这是不理智的。


我们在使用前应该明白:


[1]. 状态是否需要被共享和修改同步。如果否,也许通过 [State] 封装为内部状态是更好的选择。
[2]. [业务逻辑] 和[界面状态变化] 是否复杂到有分层的必要。如果不是非常复杂,
FutureBuilder、ValueListenableBuilder 这种小巧的局部构建组件也许是更好的选择。

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

[JS基础回顾] 闭包 又双叒叕来~~~

闭包是基于词法作用域书写代码时所产生的自然结果,你甚至不需要为了利用它们而有意识地创建闭包 MDN的解释闭包是函数和声明该函数的词法环境的组合。 Tips: 词法作用域和词法环境 1,此时函数还没被执行,所以使用的是词法作用域即静态作用域.2, 此时函...
继续阅读 »

闭包是基于词法作用域书写代码时所产生的自然结果,你甚至不需要为了利用它们而有意识地创建闭包




MDN的解释闭包是函数声明该函数的词法环境的组合。




Tips: 词法作用域词法环境 1,此时函数还没被执行,所以使用的是词法作用域即静态作用域.2, 此时函数被执行,此时词法作用域就会变成词法环境(包含静态作用域与动态作用域)



以上的解释 个人感觉还是不够清晰
我这样理解

  1. 闭包就是突破了函数作用域
  2. 闭包就是函数嵌套函数子函数可以访问父函数的变量(也就是所谓的自由变量), 所以,此变量不会被回收.


闭包暴露``函数作用域3种方式:


1) 通过外部函数的参数进行暴露



闭包内 调用外部函数 通过外部函数的参数 暴露 闭包内 自由变量.



function fn() { 
var a = 2;
function innerFn() {
outerFn(a) //通过外部函数的参数进行暴露
}
innerFn();
};
function outerFn(val) {
console.log(val); // 2
}
fn(); // 2

2) 通过外部作用域的变量进行暴露



其中val为全局变量



function fn() { 
var a = 1;
function innerFn() {
val = a; //通过外部作用域的变量进行暴露
}
innerFn();
};

fn();
console.log(val); // 1


3) 通过return直接将整个函数进行暴露


function fn() { 
var a = 1;
function innerFn() {
console.log(a);
}
return innerFn; //通过return直接将整个函数进行暴露
};

let a = fn();
a(); // 1

关于闭包的内存泄露



首先必须声明一点:使用闭包并不一定会造成内存泄露,只有使用闭包不当才可能会造成内存泄露.




为什么闭包可能会造成内存泄露呢?原因就是上面提到的,因为它一般会暴露自身的作用域给外部使用.如果使用不当,就可能导致该内存一直被占用,无法被JS的垃圾回收机制回收.就造成了内存泄露.




注意: 即使闭包里面什么都没有,闭包仍然会隐式地引用它所在作用域里的所用变量. 正因为这个隐藏的特点,闭包经常会发生不易发现的内存泄漏问题.



常见哪些情况使用闭包会造成内存泄露:





    1. 使用定时器未及时清除.因为计时器只有先停止才会被回收.所以决办法很简单,将定时器及时清除,并将造成内存的变量赋值为null(变成空指针)





    1. 相互循环引用.这是经常容易犯的错误,并且也不容易发现.





    1. 闭包引用到全局变量上.因为全局变量是只有当页面被关闭的时候才会被回收.




四 循环和闭包


1) 同步循环打印 正确的值


for (var i=1; i<5; i++) { 
console.log( i );
}
// 1 2 3 4

2) 同步中嵌套异步任务(中的宏任务)循环打印 错误的值



当执行 console 时, 循环已经完成, 同步任务执行完成后,执行宏任务,此时 i 已经是 5.所以打印5个5.



for (var i=1; i<5; i++) { 
setTimeout( function timer() {
console.log( i );
}, i*1000 );
}
// 打印出 5 个 5

3) 创造5个独立的函数作用域,但是 i 也全都是对外部作用域的引用 错误的值



它的最终值仍然是5个5.为什么?我们来分析下,它用了一个匿名函数包裹了定时器,并立即执行.在进行for循环时,会创造5个独立的函数作用域(由匿名函数创建的,因为它是闭包函数).但是这5个独立的函数作用域里的i也全都是对外部作用域的引用.即它们访问的都是i的最终值5.这并不是我们想要的,我们要的是5个独立的作用域,并且每个作用域都保存一个"当时"i的值.



for (var i=1; i<5; i++) { 
(function() {
setTimeout( function timer() {
console.log( i );
}, i*1000 );
})();
}
// 打印出 5 个 5

4) 通过匿名函数创建独立的函数作用域,并且通过 变量 保存独立的 i 值


for (var i=1; i<5; i++) { 
(function () {
var x=i;
console.log(x*1000); // 1000 2000 3000 4000
setTimeout( function timer() {
console.log( x );
}, x*1000 );
})();
}

// 1 2 3 4

5) 通过匿名函数创建独立的函数作用域,并且通过 参数 保存独立的 i 值


for (var i=1; i<5; i++) { 
(function (x) {
console.log(x*1000); // 1000 2000 3000 4000
setTimeout( function timer() {
console.log( x );
}, x*1000 );
})(i);
}

// 1 2 3 4

注意

  • 使用定时器未及时清除.因为计时器只有先停止才会被回收.所以决办法很简单,将定时器及时清除,并将造成内存的变量赋值为null(变成空指针)
  • 闭包引用到全局变量上.因为全局变量是只有当页面被关闭的时候才会被回收.
  • 闭包就是函数嵌套函数子函数可以访问父函数的变量(也就是所谓的自由变量), 所以,此变量不会被回收.


  • 作者:无限循环无限
    链接:https://juejin.cn/post/7011805931201642533

    收起阅读 »

    JS箭头函数 什么时候用 ,什么时候不能用,我总结出了4点

    箭头函数的定义 箭头函数定义包括一个参数列表(零个或多个参数,如果参数个数不是一个的话要用 ( .. ) 包围起来),然后是标识 =>,函数体放在最后。 箭头函数与普通函数的区别 箭头函数 let arrowSum = (a, b) => { ...
    继续阅读 »

    箭头函数的定义



    箭头函数定义包括一个参数列表(零个或多个参数,如果参数个数不是一个的话要用 ( .. ) 包围起来),然后是标识 =>,函数体放在最后。



    箭头函数与普通函数的区别


    箭头函数


    let arrowSum = (a, b) => { 
    return a + b
    }

    普通函数


    let zz = function(a, b){
    return a + b
    }

    箭头函数的用法


    我们打印fn函数的原型,我们会发现箭头函数本身没有this;


    var fn = (a, b) => {
    console.log(this, fn.prototype);
    //window, undefined
    var fn2 = () => {
    console.log(this, '测试');
    // window
    };
    fn2();
    }
    fn()

    箭头函数的arguments
    我们会发现这样写会报语法错误


    var fn = (a) => {
    console.log(a.arguments)
    }
    fn();
    // TypeError:Cannot read property 'arguments' of undefined

    我们换一种情况,我们看代码会发现箭头函数argemnets指向了上一个函数



    箭头函数不会创建自己的this,它只会从自己的作用域链的上一层继承this。




    var z = function(a){
    console.log(arguments);
    bb();
    function bb() {
    console.log(arguments);
    let ac = () => {
    console.log(arguments);
    //arguments 指向第二层函数
    };
    ac();
    }
    }
    z()

    什么时候不能用箭头函数


    1. 通过构造函数调用


    let Foo = () =>  {

    }
    let result = new Foo();
    //TypeError: Foo is not a constructor

    2. 需要使用prototype


    let foo = () =>  {

    }
    console.log(foo.prototype)
    //underfind

    3. 没有super



    连原型都没有,自然也不能通过 super 来访问原型的属性,所以箭头函数也是没有 super 的,不过跟 this、arguments、new.target 一样,这些值由外围最近一层非箭头函数决定



    总结




    • 如果你有一个简单语句的在线函数表达式,其中唯一的语句是return某个计算出的值,而且这个函数内部没有this引用,且没有自身引用(比如递归,事件绑定/解绑定),且不会要求函数执行这些,那么我们可以安全的把它重构为=>箭头函数




    • 如果你的内层函数表达式依赖于它的函数中调用 let self= this 或者.bind(this)来确保适当的this绑定,那么内层函数表达式可以转换为=>箭头函数




    • 如果你的内函数表达式依赖于封装函数像 let args = Array.prototype.slice.call
      (arguments)的词法复制,那么这个内层函数表达式应该可以安全的转换=>箭头函数




    • 所有的其他情况——函数声明,较长的多函数表达式,需要词法名称标识符(比如递归 , 构造函数)的函数,以及任何不符合以上几点特征的函数一般都应该避免=>箭头函数





    关于this arguments 和 super 的词法绑定。这是利用es6的特性来修正一些常见的问题,而不是bug或者错误。


    作者:zz
    链接:https://juejin.cn/post/7011270097721360421
    收起阅读 »

    ?Map和Set巧解力扣算法问题

    问题一:什么是Map和Set? ES6以前,在JavaScript中实现“键/值”式存储可以使用Object来方便高效的完成,也就是使用对象属性作为键,再使用属性来引用值,像下面这样 let student = { name: '啊呜', se...
    继续阅读 »

    问题一:什么是Map和Set?


    ES6以前,在JavaScript中实现“键/值”式存储可以使用Object来方便高效的完成,也就是使用对象属性作为键,再使用属性来引用值,像下面这样


    let student = {
    name: '啊呜',
    sex: 'male',
    age: 18
    }

    但是这种实现并非没有问题,这里的键只能是对象的属性,于是就出现了Map这一新的集合类型,为JavaScript带来了真正的键/值存储机制,我们可以这样初始化映射:


    const map = new Map([
    ['key1','value1'],
    ['key2','value2'],
    ['key3','value3'],
    ])

    ES6还新增了Set这一种新的集合类型,Set在很多方面都像是加强的Map,这是因为它们的大多数API和行为都是共有的。Set集合类型的特点是不能存储重复元素,成员值都是唯一且没有重复的值


    问题二:Map和Set的基本API怎么用?


    Map的API:




    • get() :返回键值对




    • set() :添加键值对,返回实例




    • delete() :删除键值对,返回布尔




    • has() :检查键值对,返回布尔




    • clear() :清除所有成员




    • keys() :返回以键为遍历器的对象




    • values() :返回以值为遍历器的对象




    • entries() :返回以键和值为遍历器的对象




    • forEach() :使用回调函数遍历每个成员




    Set的API:




    • add() :添加值,返回实例




    • delete() :删除值,返回布尔




    • has() :检查值,返回布尔




    • clear() :清除所有成员




    • keys() :返回以属性值为遍历器的对象




    • values() :返回以属性值为遍历器的对象




    • entries() :返回以属性值和属性值为遍历器的对象




    • forEach() :使用回调函数遍历每个成员





    好啦,到这,相信你对JS中的Map和Set有了一定的了解,我们现在尝试使用这两种集合类型,在LeetCode中大显身手~



    LeetCode20:有效的括号



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



    示例1:



    输入: s = "()"

    输出: true



    示例2:



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

    输出: true



    示例 3:



    输入: s = "(]"

    输出: false



    这题的思路是使用栈+Map来解决,直接上代码:


    carbon (2).png


    LeetCode141:环形链表



    给定一个链表,判断链表中是否有环。



    示例:


    circularlinkedlist.png



    输入: head = [3,2,0,-4], pos = 1

    输出: true



    这题我的思路是使用Set来解决,当然还有一种方法,用快慢指针来解决,但是比较难想到,而且比较反人类,我们这里只介绍Set,清晰易懂~


    carbon (3).png



    作者:_啊呜
    链接:https://juejin.cn/post/7011710641807294477
    收起阅读 »

    深入理解 redux 数据流和异步过程管理

    前端框架的数据流 前端框架实现了数据驱动视图变化的功能,我们用 template 或者 jsx 描述好了数据和视图的绑定关系,然后就只需要关心数据的管理了。 数据在组件和组件之间、组件和全局 store 之间传递,叫做前端框架的数据流。 一般来说,除了某部分状...
    继续阅读 »

    前端框架的数据流


    前端框架实现了数据驱动视图变化的功能,我们用 template 或者 jsx 描述好了数据和视图的绑定关系,然后就只需要关心数据的管理了。


    数据在组件和组件之间、组件和全局 store 之间传递,叫做前端框架的数据流。


    一般来说,除了某部分状态数据是只有某个组件关心的,我们会把状态数据放在组件内以外,业务数据、多个组件关心的状态数据都会放在 store 里面。组件从 store 中取数据,当交互的时候去通知 store 改变对应的数据。


    这个 store 不一定是 redux、mobox 这些第三方库,其实 react 内置的 context 也可以作为 store。但是 context 做为 store 有一个问题,任何组件都能从 context 中取出数据来修改,那么当排查问题的时候就特别困难,因为并不知道是哪个组件把数据改坏的,也就是数据流不清晰。


    正是因为这个原因,我们几乎见不到用 context 作为 store,基本都是搭配一个 redux。


    所以为什么 redux 好呢?第一个原因就是数据流清晰,改变数据有统一的入口。



    组件里都是通过 dispatch 一个 action 来触发 store 的修改,而且修改的逻辑都是在 reducer 里面,组件再监听 store 的数据变化,从中取出最新的数据。


    这样数据流动是单向的,清晰的,很容易管理。


    这就像为什么我们在公司里想要什么权限都要走审批流,而不是直接找某人,一样的道理。集中管理流程比较清晰,而且还可以追溯。


    异步过程的管理


    很多情况下改变 store 数据都是一个异步的过程,比如等待网络请求返回数据、定时改变数据、等待某个事件来改变数据等,那这些异步过程的代码放在哪里呢?


    组件?


    放在组件里是可以,但是异步过程怎么跨组件复用?多个异步过程之间怎么做串行、并行等控制?


    所以当异步过程比较多,而且异步过程与异步过程之间也不独立,有串行、并行、甚至更复杂的关系的时候,直接把异步逻辑放组件内不行。


    不放组件内,那放哪呢?


    redux 提供的中间件机制是不是可以用来放这些异步过程呢?


    redux 中间件


    先看下什么是 redux 中间件:


    redux 的流程很简单,就是 dispatch 一个 action 到 store, reducer 来处理 action。那么如果想在到达 store 之前多做一些处理呢?在哪里加?


    改造 dispatch!中间件的原理就是层层包装 dispatch。


    下面是 applyMiddleware 的源码,可以看到 applyMiddleware 就是对 store.dispatch 做了层层包装,最后返回修改了 dispatch 之后的 store。


    function applyMiddleware(middlewares) {
    let dispatch = store.dispatch
    middlewares.forEach(middleware =>
    dispatch = middleware(store)(dispatch)
    )
    return { ...store, dispatch}
    }

    所以说中间件最终返回的函数就是处理 action 的 dispatch:


    function middlewareXxx(store) {
    return function (next) {
    return function (action) {
    // xx
    };
    };
    };
    }

    中间件会包装 dispatch,而 dispatch 就是把 action 传给 store 的,所以中间件自然可以拿到 action、拿到 store,还有被包装的 dispatch,也就是 next。


    比如 redux-thunk 中间件的实现:


    function createThunkMiddleware(extraArgument) {
    return ({ dispatch, getState }) => next => action => {
    if (typeof action === 'function') {
    return action(dispatch, getState, extraArgument);
    }

    return next(action);
    };
    }

    const thunk = createThunkMiddleware();

    它判断了如果 action 是一个函数,就执行该函数,并且把 store.dispath 和 store.getState 传进去,否则传给内层的 dispatch。


    通过 redux-thunk 中间件,我们可以把异步过程通过函数的形式放在 dispatch 的参数里:


    const login = (userName) => (dispatch) => {
    dispatch({ type: 'loginStart' })
    request.post('/api/login', { data: userName }, () => {
    dispatch({ type: 'loginSuccess', payload: userName })
    })
    }
    store.dispatch(login('guang'))

    但是这样解决了组件里的异步过程不好复用、多个异步过程之间不好做并行、串行等控制的问题了么?


    没有,这段逻辑依然是在组件里写,只不过移到了 dispatch 里,也没有提供多个异步过程的管理机制。


    解决这个问题,需要用 redux-saga 或 redux-observable 中间件。


    redux-saga


    redux-saga 并没有改变 action,它会把 action 透传给 store,只是多加了一条异步过程的处理。



    redux-saga 中间件是这样启用的:


    import { createStore, applyMiddleware } from 'redux'
    import createSagaMiddleware from 'redux-saga'
    import rootReducer from './reducer'
    import rootSaga from './sagas'

    const sagaMiddleware = createSagaMiddleware()
    const store = createStore(rootReducer, {}, applyMiddleware(sagaMiddleware))
    sagaMiddleware.run(rootSaga)

    要调用 run 把 saga 的 watcher saga 跑起来:


    watcher saga 里面监听了一些 action,然后调用 worker saga 来处理:


    import { all, takeLatest } from 'redux-saga/effects'

    function* rootSaga() {
    yield all([
    takeLatest('login', login),
    takeLatest('logout', logout)
    ])
    }
    export default rootSaga

    redux-saga 会先把 action 透传给 store,然后判断下该 action 是否是被 taker 监听的:


    function sagaMiddleware({ getState, dispatch }) {
    return function (next) {
    return function (action) {
    const result = next(action);// 把 action 透传给 store

    channel.put(action); //触发 saga 的 action 监听流程

    return result;
    }
    }
    }

    当发现该 action 是被监听的,那么就执行相应的 taker,调用 worker saga 来处理:


    function* login(action) {
    try {
    const loginInfo = yield call(loginService, action.account)
    yield put({ type: 'loginSuccess', loginInfo })
    } catch (error) {
    yield put({ type: 'loginError', error })
    }
    }

    function* logout() {
    yield put({ type: 'logoutSuccess'})
    }

    比如 login 和 logout 会有不同的 worker saga。


    login 会请求 login 接口,然后触发 loginSuccess 或者 loginError 的 action。


    logout 会触发 logoutSuccess 的 action。


    redux saga 的异步过程管理就是这样的:先把 action 透传给 store,然后判断 action 是否是被 taker 监听的,如果是,则调用对应的 worker saga 进行处理。


    redux saga 在 redux 的 action 流程之外,加了一条监听 action 的异步处理的流程。


    其实整个流程还是比较容易理解的。理解成本高一点的就是 generator 的写法了:


    比如下面这段代码:


    function* xxxSaga() {
    while(true) {
    yield take('xxx_action');
    //...
    }
    }

    它就是对每一个监听到的 xxx_action 做同样的处理的意思,相当于 takeEvery:


    function* xxxSaga() {
    yield takeEvery('xxx_action');
    //...
    }

    但是因为有一个 while(true),很多同学就不理解了,这不是死循环了么?


    不是的。generator 执行后返回的是一个 iterator,需要另外一个程序调用 next 方法才会继续执行。所以怎么执行、是否继续执行都是由另一个程序控制的。


    在 redux-saga 里面,控制 worker saga 执行的程序叫做 task。worker saga 只是告诉了 task 应该做什么处理,通过 call、fork、put 这些命令(这些命令叫做 effect)。


    然后 task 会调用不同的实现函数来执行该 worker saga。


    为什么要这样设计呢?直接执行不就行了,为啥要拆成 worker saga 和 task 两部分,这样理解成本不就高了么?


    确实,设计成 generator 的形式会增加理解成本,但是换来的是可测试性。因为各种副作用,比如网络请求、dispatch action 到 store 等等,都变成了 call、put 等 effect,由 task 部分控制执行。那么具体怎么执行的就可以随意的切换了,这样测试的时候只需要模拟传入对应的数据,就可以测试 worker saga 了。


    redux saga 设计成 generator 的形式是一种学习成本和可测试性的权衡。


    还记得 redux-thunk 有啥问题么?多个异步过程之间的并行、串行的复杂关系没法处理。那 redux-saga 是怎么解决的呢?


    redux-saga 提供了 all、race、takeEvery、takeLatest 等 effect 来指定多个异步过程的关系:


    比如 takeEvery 会对多个 action 的每一个做同样的处理,takeLatest 会对多个 action 的最后一个做处理,race 会只返回最快的那个异步过程的结果,等等。


    这些控制多个异步过程之间关系的 effect 正是 redux-thunk 所没有的,也是复杂异步过程的管理必不可少的部分。


    所以 redux-saga 可以做复杂异步过程的管理,而且具有很好的可测试性。


    其实异步过程的管理,最出名的是 rxjs,而 redux-observable 就是基于 rxjs 实现的,它也是一种复杂异步过程管理的方案。


    redux-observable


    redux-observable 用起来和 redux-saga 特别像,比如启用插件的部分:


    const epicMiddleware = createEpicMiddleware();

    const store = createStore(
    rootReducer,
    applyMiddleware(epicMiddleware)
    );

    epicMiddleware.run(rootEpic);

    和 redux saga 的启动流程是一样的,只是不叫 saga 而叫 epic。


    但是对异步过程的处理,redux saga 是自己提供了一些 effect,而 redux-observable 是利用了 rxjs 的 operator:


    import { ajax } from 'rxjs/ajax';

    const fetchUserEpic = (action$, state$) => action$.pipe(
    ofType('FETCH_USER'),
    mergeMap(({ payload }) => ajax.getJSON(`/api/users/${payload}`).pipe(
    map(response => ({
    type: 'FETCH_USER_FULFILLED',
    payload: response
    }))
    )
    );

    通过 ofType 来指定监听的 action,处理结束返回 action 传递给 store。


    相比 redux-saga 来说,redux-observable 支持的异步过程的处理更丰富,直接对接了 operator 的生态,是开放的,而 redux-saga 则只是提供了内置的几个 effect 来处理。


    所以做特别复杂的异步流程处理的时候,redux-observable 能够利用 rxjs 的操作符的优势会更明显。


    但是 redux-saga 的优点还有基于 generator 的良好的可测试性,而且大多数场景下,redux-saga 提供的异步过程的处理能力就足够了,所以相对来说,redux-saga 用的更多一些。


    总结


    前端框架实现了数据到视图的绑定,我们只需要关心数据流就可以了。


    相比 context 的混乱的数据流,redux 的 view -> action -> store -> view 的单向数据流更清晰且容易管理。


    前端代码中有很多异步过程,这些异步过程之间可能有串行、并行甚至更复杂的关系,放在组件里并不好管理,可以放在 redux 的中间件里。


    redux 的中间件就是对 dispatch 的层层包装,比如 redux-thunk 就是判断了下 action 是 function 就执行下,否则就是继续 dispatch。


    redux-thunk 并没有提供多个异步过程管理的机制,复杂异步过程的管理还是得用 redux-saga 或者 redux-observable。


    redux-saga 透传了 action 到 store,并且监听 action 执行相应的异步过程。异步过程的描述使用 generator 的形式,好处是可测试性。比如通过 take、takeEvery、takeLatest 来监听 action,然后执行 worker saga。worker saga 可以用 put、call、fork 等 effect 来描述不同的副作用,由 task 负责执行。


    redux-observable 同样监听了 action 执行相应的异步过程,但是是基于 rxjs 的 operator,相比 saga 来说,异步过程的管理功能更强大。


    不管是 redux-saga 通过 generator 来组织异步过程,通过内置 effect 来处理多个异步过程之间的关系,还是 redux-observable 通过 rxjs 的 operator 来组织异步过程和多个异步过程之间的关系。它们都解决了复杂异步过程的处理的问题,可以根据场景的复杂度灵活选用。


    作者:zxg_神说要有光
    链接:https://juejin.cn/post/7011835078594527263

    收起阅读 »

    【JavaScript】async await 更优雅的错误处理

    背景 团队来了新的小伙伴,发现我们的团队代码规范中,要给 async await 添加 try...catch。他感觉很疑惑,假如有很多个(不集中),那不是要加很多个地方?那不是很不优雅? 为什么要错误处理 JavaScript 是一个单线程的语言,假如不加...
    继续阅读 »

    背景


    团队来了新的小伙伴,发现我们的团队代码规范中,要给 async await 添加 try...catch。他感觉很疑惑,假如有很多个(不集中),那不是要加很多个地方?那不是很不优雅?


    为什么要错误处理


    JavaScript 是一个单线程的语言,假如不加 try ...catch ,会导致直接报错无法继续执行。当然不意味着你代码中一定要用 try...catch 包住,使用 try...catch 意味着你知道这个位置代码很可能出现报错,所以你使用了 try...catch 进行捕获处理,并让程序继续执行。


    我理解我们一般在执行 async await 的时候,一般运行在异步的场景下,这种场景一般不应该阻塞流程的进行,所以推荐使用了 try...catch 的处理。


    async await 更优雅的错误处理


    但确实如那位同事所说,加 try...catch 并不是一个很优雅的行为。所以我 Google 了一下,发现 How to write async await without try-catch blocks in Javascript 这篇文章中提到了一种更优雅的方法处理,并封装成了一个库——await-to-js。这个库只有一个 function,我们完全可以将这个函数运用到我们的业务中,如下所示:


    /**
    * @param { Promise } promise
    * @param { Object= } errorExt - Additional Information you can pass to the err object
    * @return { Promise }
    */
    export function to<T, U = Error> (
    promise: Promise<T>,
    errorExt?: object
    ): Promise<[U, undefined] | [null, T]> {
    return promise
    .then<[null, T]>((data: T) => [null, data]) // 执行成功,返回数组第一项为 null。第二个是结果。
    .catch<[U, undefined]>((err: U) => {
    if (errorExt) {
    Object.assign(err, errorExt);
    }

    return [err, undefined]; // 执行失败,返回数组第一项为错误信息,第二项为 undefined
    });
    }

    export default to;

    这里需要有一个前置的知识点:await 是在等待一个 Promise 的返回值


    正常情况下,await 命令后面是一个 Promise 对象,返回该对象的结果。如果不是 Promise 对象,就直接返回对应的值。


    所以我们只需要利用 Promise 的特性,分别在 promise.thenpromise.catch 中返回不同的数组,其中 fulfilled 的时候返回数组第一项为 null,第二个是结果。rejected 的时候,返回数组第一项为错误信息,第二项为 undefined。使用的时候,判断第一项是否为空,即可知道是否有错误,具体使用如下:


    import to from 'await-to-js';
    // If you use CommonJS (i.e NodeJS environment), it should be:
    // const to = require('await-to-js').default;

    async function asyncTaskWithCb(cb) {
    let err, user, savedTask, notification;

    [ err, user ] = await to(UserModel.findById(1));
    if(!user) return cb('No user found');

    [ err, savedTask ] = await to(TaskModel({userId: user.id, name: 'Demo Task'}));
    if(err) return cb('Error occurred while saving task');

    if(user.notificationsEnabled) {
    [ err ] = await to(NotificationService.sendNotification(user.id, 'Task Created'));
    if(err) return cb('Error while sending notification');
    }

    if(savedTask.assignedUser.id !== user.id) {
    [ err, notification ] = await to(NotificationService.sendNotification(savedTask.assignedUser.id, 'Task was created for you'));
    if(err) return cb('Error while sending notification');
    }

    cb(null, savedTask);
    }

    小结


    async await 中添加错误处理个人认为是有必要的,但方案不仅仅只有 try...catch。利用 async awaitPromise 的特性,我们可以更加优雅的处理 async await 的错误。


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

    收起阅读 »

    国内知名Wchat团队荣誉出品顶级IM通讯聊天系统

    iOS
    国内知名Wchat团队荣誉出品顶级IM通讯聊天系统团队言语在先:想低价购买者勿扰(团队是在国内首屈一指的通信公司离职后组建,低价购买者/代码代码贩子者/同行勿扰/)。想购买劣质低等产品者勿扰(行业鱼龙混杂,想购买类似低能协议xmpp者勿扰)。想购买由类似ope...
    继续阅读 »



    国内知名Wchat团队荣誉出品顶级IM通讯聊天系统



    团队言语在先:

    想低价购买者勿扰(团队是在国内首屈一指的通信公司离职后组建,低价购买者/代码代码贩子者/同行勿扰/)

    。想购买劣质低等产品者勿扰(行业鱼龙混杂,想购买类似低能协议xmpp者勿扰)

    。想购买由类似openfire第三方开源改造而来的所谓第三方通信server者勿扰

    。想购买没有做任何安全加密场景者勿扰(随便一句api 一个接口就构成了红包收发/转账/密码设置等没有任何安全系数可言的低质产品)

    。想购买非运营级别通信系统勿扰(到处呼喊:最稳定/真正可靠/大并发/真正安全!所有一切都需要实际架构支撑以及理论数值测验)

    。想购买无保障/无支撑者勿扰(1W/4W/10W低质产品不可谓没有,必须做到:大并发支持合同保障/合作支持运维保障/在线人数支持架构保障)

    。想购买消息丢包者勿扰(满天飞的所谓消息确认机制,最简单的测验既是前端支持消息收发demo测试环境,低质产品一秒收发百条消息必丢必崩,

    别提秒发千条/万条,更低质产品可测验:同时发九张图片/根据数字12345678910发送出去,必丢!android vs ios)

    。想购买大容量群uer者勿扰(随便宣传既是万人大群/几千大群/群组无限,小团队产品群组上线用户超过4000群消息体量不用很大手机前端必卡)

    。最重要一点:口口声声说要运营很大的系统 却想出十几个money的人群勿扰,买产品做系统一要稳定二要长久用三要抛开运维烦恼,预算有限那就干脆

    别买,买了几万的系统你一样后面用不起来会烂掉!

    。产品体系包括:android ios server adminweb maintenance httpapi h5 webpc (支持server压测/前端消息收发压测/httpapi压测)

    。。支持源码,但需要您拿去做一个伟大的系统出来!

    。。团队产品目前国内没有同质化,客户集中在国外,有求高质量产品的个人或团队可通过以下方式联系到我们(低价者勿扰!)

    。。。球球:383189941 q 513275129

    。。。。产品不多介绍直接加我 测试产品更直接

    。。。。。创新从未停止 更新不会终止 大陆唯一一家支持大并发保障/支持合同费用包含运维支撑的团队 

    收起阅读 »

    Android 高级UI5 画笔Paint的基本用法

    1.setStyle(Paint.Style style)设置画笔样式,取值有Paint.Style.FILL :填充内部Paint.Style.FILL_AND_STROKE :填充内部和描边Paint.Style.STROKE :仅描边代码实例:publi...
    继续阅读 »

    1.setStyle(Paint.Style style)

    设置画笔样式,取值有
    Paint.Style.FILL :填充内部
    Paint.Style.FILL_AND_STROKE :填充内部和描边
    Paint.Style.STROKE :仅描边

    代码实例:


    public class PaintViewBasic extends View {
    private Paint mPaint;

    public PaintViewBasic(Context context) {
    super(context);
    mPaint = new Paint();
    }

    public PaintViewBasic(Context context, @Nullable AttributeSet attrs) {
    super(context, attrs);
    mPaint = new Paint();
    }

    @Override
    protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    drawStyle(canvas);
    }

    private void drawStyle( Canvas canvas ) {

    mPaint.setColor(Color.RED);//设置画笔的颜色
    mPaint.setTextSize(60);//设置文字大小
    mPaint.setStrokeWidth(5);//设置画笔的宽度
    mPaint.setAntiAlias(true);//设置抗锯齿功能 true表示抗锯齿 false则表示不需要这功能

    mPaint.setStyle(Paint.Style.STROKE);
    canvas.drawCircle(200,200,160,mPaint);

    mPaint.setStyle(Paint.Style.FILL);
    canvas.drawCircle(200,600,160,mPaint);

    mPaint.setStyle(Paint.Style.FILL_AND_STROKE);
    canvas.drawCircle(200,1000,160,mPaint);

    }

    }

    2.setStrokeCap(Paint.Cap cap)

    设置线冒样式,取值有
    Paint.Cap.BUTT(无线冒)
    Paint.Cap.ROUND(圆形线冒)
    Paint.Cap.SQUARE(方形线冒)
    注意:冒多出来的那块区域就是线帽!就相当于给原来的直线加上一个帽子一样,所以叫线帽


        private void drawStrokeCap(Canvas canvas) {
    Paint paint = new Paint();

    paint.setAntiAlias(true);
    paint.setStrokeWidth(200);
    paint.setColor(Color.parseColor("#00ff00"));
    paint.setStrokeCap(Paint.Cap.BUTT); // 线帽,即画的线条两端是否带有圆角,butt,无圆角
    canvas.drawLine(200, 200, 500, 200, paint);

    paint.setColor(Color.parseColor("#ff0000"));
    paint.setStrokeCap(Paint.Cap.ROUND); // 线帽,即画的线条两端是否带有圆角,ROUND,圆角
    canvas.drawLine(200, 500, 500, 500, paint);

    paint.setColor(Color.parseColor("#0000ff"));
    paint.setStrokeCap(Paint.Cap.SQUARE); // 线帽,即画的线条两端是否带有圆角,SQUARE,矩形
    canvas.drawLine(200, 800, 500, 800, paint);
    }

    3.setStrokeJoin(Paint.Join join)

    设置线段连接处样式,取值有:
    Paint.Join.MITER(结合处为锐角)
    Paint.Join.Round (结合处为圆弧)
    Paint.Join.BEVEL (结合处为直线)


      private void drawStrokeJoin( Canvas canvas ) {
    Paint paint = new Paint();

    paint.setAntiAlias( true );
    paint.setStrokeWidth( 80 );
    paint.setStyle(Paint.Style.STROKE ); // 默认是填充 Paint.Style.FILL
    paint.setColor( Color.parseColor("#0000ff") );

    Path path = new Path();
    path.moveTo(100, 100);
    path.lineTo(400, 100);
    path.lineTo(100, 300);
    paint.setStrokeJoin(Paint.Join.MITER);
    canvas.drawPath(path, paint);

    path.moveTo(100, 500);
    path.lineTo(400, 500);
    path.lineTo(100, 700);
    paint.setStrokeJoin(Paint.Join.ROUND);
    canvas.drawPath(path, paint);

    path.moveTo(100, 900);
    path.lineTo(400, 900);
    path.lineTo(100, 1100);
    paint.setStrokeJoin(Paint.Join.BEVEL);
    canvas.drawPath(path, paint);
    }

    }

    4.setPathEffect(PathEffect effect)

    设置绘制路径的效果,如点画线等

    CornerPathEffect:

    这个类的作用就是将Path的各个连接线段之间的夹角用一种更平滑的方式连接,类似于圆弧与切线的效果。
    一般的,通过CornerPathEffect(float radius)指定一个具体的圆弧半径来实例化一个CornerPathEffect。

    DashPathEffect:

    这个类的作用就是将Path的线段虚线化。
    构造函数为DashPathEffect(float[] intervals, float offset),其中intervals为虚线的ON和OFF数组,该数组的length必须大于等于2,phase为绘制时的偏移量。

    DiscretePathEffect:

    这个类的作用是打散Path的线段,使得在原来路径的基础上发生打散效果。
    一般的,通过构造DiscretePathEffect(float segmentLength,float deviation)来构造一个实例,其中,segmentLength指定最大的段长,deviation指定偏离量。

    PathDashPathEffect:

    这个类的作用是使用Path图形来填充当前的路径,其构造函数为PathDashPathEffect (Path shape, float advance, float phase,PathDashPathEffect.Stylestyle)。
    shape则是指填充图形,advance指每个图形间的间距,phase为绘制时的偏移量,style为该类自由的枚举值,有三种情况:Style.ROTATE、Style.MORPH和
    Style.TRANSLATE。其中ROTATE的情况下,线段连接处的图形转换以旋转到与下一段移动方向相一致的角度进行转转,MORPH时图形会以发生拉伸或压缩等变形的情况与下一段相连接,TRANSLATE时,图形会以位置平移的方式与下一段相连接。

    ComposePathEffect:

    组合效果,这个类需要两个PathEffect参数来构造一个实例,ComposePathEffect (PathEffect outerpe,PathEffect innerpe),表现时,会首先将innerpe表现出来,然后再在innerpe的基础上去增加outerpe的效果。

    SumPathEffect:

    叠加效果,这个类也需要两个PathEffect作为参数SumPathEffect(PathEffect first,PathEffect second),但与ComposePathEffect不同的是,在表现时,会分别对两个参数的效果各自独立进行表现,然后将两个效果简单的重叠在一起显示出来。

    关于参数phase

    在存在phase参数的两个类里,如果phase参数的值不停发生改变,那么所绘制的图形也会随着偏移量而不断的发生变动,这个时候,看起来这条线就像动起来了一样。


    private float phase;
    private PathEffect[] effects;
    private int[] colors;

    private void drawPathEffect(Canvas canvas) {
    mPaint.setStyle(Paint.Style.STROKE);
    mPaint.setStrokeWidth(4);
    // 创建,并初始化Path
    Path path = new Path();
    path.moveTo(0, 0);
    for (int i = 1; i <= 35; i++) {
    // 生成15个点,随机生成它们的坐标,并将它们连成一条Path
    path.lineTo(i * 20, (float) Math.random() * 60);
    }
    // 初始化七个颜色
    colors = new int[]{Color.BLACK, Color.BLUE, Color.CYAN, Color.GREEN, Color.MAGENTA, Color.RED,
    Color.GRAY};


    // 将背景填充成白色
    canvas.drawColor(Color.WHITE);
    effects = new PathEffect[7];
    // -------下面开始初始化7中路径的效果
    // 使用路径效果
    effects[0] = null;
    // 使用CornerPathEffect路径效果
    effects[1] = new CornerPathEffect(10);
    // 初始化DiscretePathEffect
    effects[2] = new DiscretePathEffect(3.0f, 5.0f);
    // 初始化DashPathEffect
    effects[3] = new DashPathEffect(new float[]{20, 10, 5, 10}, phase);
    // 初始化PathDashPathEffect
    Path p = new Path();
    p.addRect(0, 0, 8, 8, Path.Direction.CCW);
    effects[4] = new PathDashPathEffect(p, 12, phase, PathDashPathEffect.Style.ROTATE);
    // 初始化PathDashPathEffect
    effects[5] = new ComposePathEffect(effects[2], effects[4]);
    effects[6] = new SumPathEffect(effects[4], effects[3]);
    // 将画布移到8,8处开始绘制
    canvas.translate(8, 8);
    // 依次使用7中不同路径效果,7种不同的颜色来绘制路径
    for (int i = 0; i < effects.length; i++) {
    mPaint.setPathEffect(effects[i]);
    mPaint.setColor(colors[i]);
    canvas.drawPath(path, mPaint);
    canvas.translate(0, 200);
    }
    // 改变phase值,形成动画效果
    phase += 1;
    invalidate();
    }

    5.setShadowLayer(float radius, float dx, float dy, int shadowColor)

    阴影制作:包括各种形状(矩形,圆形等等),以及文字等等都能设置阴影。


        private void drawShadowLayer(Canvas canvas) {
    // 建立Paint 物件
    Paint paint1 = new Paint();
    paint1.setTextSize(100);
    // 设定颜色
    paint1.setColor(Color.BLACK);
    // 设定阴影(柔边, X 轴位移, Y 轴位移, 阴影颜色)
    paint1.setShadowLayer(10, 5, 5, Color.GRAY);
    // 实心矩形& 其阴影
    canvas.drawText("我爱你", 20,100,paint1);
    Paint paint2 = new Paint();
    paint2.setTextSize(100);
    paint2.setColor(Color.GREEN);
    paint2.setShadowLayer(10, 6, 6, Color.GRAY);
    canvas.drawText("你真傻", 20,200,paint2);

    //cx和cy为圆点的坐标
    int radius = 80;
    int offest = 40;
    int startX = radius + offest;
    int startY = radius + offest + 200;

    Paint paint3 = new Paint();
    //如果不关闭硬件加速,setShadowLayer无效
    setLayerType(LAYER_TYPE_SOFTWARE, null);
    paint3.setShadowLayer(20, -20, 10, Color.DKGRAY);
    canvas.drawCircle(startX, startY, radius, paint3);
    paint3.setStyle(Paint.Style.STROKE);
    paint3.setStrokeWidth(5);
    canvas.drawCircle(startX + radius * 2 + offest, startY, radius, paint3);
    }

    6.setXfermode(Xfermode xfermode)

    Xfermode国外有大神称之为过渡模式,这种翻译比较贴切但恐怕不易理解,大家也可以直接称之为图像混合模式,因为所谓的“过渡”其实就是图像混合的一种,这个方法跟我们上面讲到的setColorFilter蛮相似的。查看API文档发现其果然有三个子类:AvoidXfermode, PixelXorXfermode和PorterDuffXfermode,这三个子类实现的功能要比setColorFilter的三个子类复杂得多。

    由于AvoidXfermode, PixelXorXfermode都已经被标注为过时了,所以这次主要研究的是仍然在使用的PorterDuffXfermode:

    PorterDuffXfermode

    该类同样有且只有一个含参的构造方法PorterDuffXfermode(PorterDuff.Mode mode),虽说构造方法的签名列表里只有一个PorterDuff.Mode的参数,但是它可以实现很多酷毙的图形效果!!而PorterDuffXfermode就是图形混合模式的意思,其概念最早来自于SIGGRAPH的Tomas Proter和Tom Duff,混合图形的概念极大地推动了图形图像学的发展,延伸到计算机图形图像学像Adobe和AutoDesk公司著名的多款设计软件都可以说一定程度上受到影响,而我们PorterDuffXfermode的名字也来源于这俩人的人名组合PorterDuff,那PorterDuffXfermode能做些什么呢?我们先来看一张API DEMO里的图片:

    这张图片从一定程度上形象地说明了图形混合的作用,两个图形一圆一方通过一定的计算产生不同的组合效果,在API中Android为我们提供了18种(比上图多了两种ADD和OVERLAY)模式:

    ADD:饱和相加,对图像饱和度进行相加,不常用

    CLEAR:清除图像

    DARKEN:变暗,较深的颜色覆盖较浅的颜色,若两者深浅程度相同则混合

    DST:只显示目标图像

    DST_ATOP:在源图像和目标图像相交的地方绘制【目标图像】,在不相交的地方绘制【源图像】,相交处的效果受到源图像和目标图像alpha的影响

    DST_IN:只在源图像和目标图像相交的地方绘制【目标图像】,绘制效果受到源图像对应地方透明度影响

    DST_OUT:只在源图像和目标图像不相交的地方绘制【目标图像】,在相交的地方根据源图像的alpha进行过滤,源图像完全不透明则完全过滤,完全透明则不过滤

    DST_OVER:将目标图像放在源图像上方

    LIGHTEN:变亮,与DARKEN相反,DARKEN和LIGHTEN生成的图像结果与Android对颜色值深浅的定义有关

    MULTIPLY:正片叠底,源图像素颜色值乘以目标图像素颜色值除以255得到混合后图像像素颜色值

    OVERLAY:叠加

    SCREEN:滤色,色调均和,保留两个图层中较白的部分,较暗的部分被遮盖

    SRC:只显示源图像

    SRC_ATOP:在源图像和目标图像相交的地方绘制【源图像】,在不相交的地方绘制【目标图像】,相交处的效果受到源图像和目标图像alpha的影响

    SRC_IN:只在源图像和目标图像相交的地方绘制【源图像】

    SRC_OUT:只在源图像和目标图像不相交的地方绘制【源图像】,相交的地方根据目标图像的对应地方的alpha进行过滤,目标图像完全不透明则完全过滤,完全透明则不过滤

    SRC_OVER:将源图像放在目标图像上方

    XOR:在源图像和目标图像相交的地方之外绘制它们,在相交的地方受到对应alpha和色值影响,如果完全不透明则相交处完全不绘制


    public class PorterDuffView extends View {

    Paint mPaint;
    Context mContext;
    int BlueColor;
    int PinkColor;
    int mWith;
    int mHeight;
    public PorterDuffView(Context context) {
    super(context);
    init(context);
    }
    public PorterDuffView(Context context, AttributeSet attrs) {
    super(context, attrs);
    init(context);
    }

    public PorterDuffView(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    init(context);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    mHeight = getMeasuredHeight();
    mWith = getMeasuredWidth();
    }

    private void init(Context context) {
    mContext = context;
    BlueColor = ContextCompat.getColor(mContext, R.color.colorPrimary);
    PinkColor = ContextCompat.getColor(mContext, R.color.colorAccent);
    mPaint = new Paint();
    mPaint.setStyle(Paint.Style.FILL);
    mPaint.setAntiAlias(true);
    }
    private Bitmap drawRectBm(){
    Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
    paint.setColor(BlueColor);
    paint.setStyle(Paint.Style.FILL);
    paint.setAntiAlias(true);
    Bitmap bm = Bitmap.createBitmap(200,200, Bitmap.Config.ARGB_8888);
    Canvas cavas = new Canvas(bm);
    cavas.drawRect(new RectF(0,0,70,70),paint);
    return bm;
    }
    private Bitmap drawCircleBm(){
    Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
    paint.setColor(PinkColor);
    paint.setStyle(Paint.Style.FILL);
    paint.setAntiAlias(true);
    Bitmap bm = Bitmap.createBitmap(200,200, Bitmap.Config.ARGB_8888);
    Canvas cavas = new Canvas(bm);
    cavas.drawCircle(70,70,35,paint);
    return bm;
    }
    @Override
    protected void onDraw(Canvas canvas) {
    mPaint.setFilterBitmap(false);
    mPaint.setStyle(Paint.Style.FILL);
    mPaint.setTextSize(20);
    RectF recf = new RectF(20,20,60,60);
    mPaint.setColor(BlueColor);
    canvas.drawRect(recf,mPaint);
    mPaint.setColor(PinkColor);
    canvas.drawCircle(100,40,20,mPaint);
    @SuppressLint("WrongConstant") int sc = canvas.saveLayer(0, 0,mWith,mHeight, null, Canvas.MATRIX_SAVE_FLAG |
    Canvas.CLIP_SAVE_FLAG |
    Canvas.HAS_ALPHA_LAYER_SAVE_FLAG |
    Canvas.FULL_COLOR_LAYER_SAVE_FLAG |
    Canvas.CLIP_TO_LAYER_SAVE_FLAG);
    int y = 180;
    int x = 50;
    for(PorterDuff.Mode mode : PorterDuff.Mode.values()){
    if(y >= 900){
    y = 180;
    x += 200;
    }
    mPaint.setXfermode(null);
    canvas.drawText(mode.name(),x + 100,y,mPaint);
    canvas.drawBitmap(drawRectBm(),x,y,mPaint);
    mPaint.setXfermode(new PorterDuffXfermode(mode));
    canvas.drawBitmap(drawCircleBm(),x,y,mPaint);
    y += 120;
    }
    mPaint.setXfermode(null);
    // 还原画布
    canvas.restoreToCount(sc);
    }
    }

    收起阅读 »

    android音视频基础

    一、编码目的编码的目的:压缩,各种音视频的编码方式就是为了让视频体积更小,有利于存储和传输。编码的核心四想就是去除冗余信息。二、编码思路1.空间冗余图像内部相邻元素之间存在较强的相关性,造成信息的冗余。(一块区域颜色一样)2.时间冗余相邻视频帧具有较大的相关性...
    继续阅读 »

    一、编码目的

    编码的目的:压缩,各种音视频的编码方式就是为了让视频体积更小,有利于存储和传输。编码的核心四想就是去除冗余信息。

    二、编码思路

    1.空间冗余

    图像内部相邻元素之间存在较强的相关性,造成信息的冗余。(一块区域颜色一样)

    2.时间冗余

    相邻视频帧具有较大的相关性,造成信息的冗余。(第一帧和第二帧绝大多数数据一样)

    3. 视觉冗余

    人类不敏感的信息可以去除。(红色偏点橘色)

    4.信息熵冗余 == 熵编码-哈夫曼算法

    也称编码冗余,人们用于表达某一信息所需要的比特数总比理论上表示该信息所需要的最少比特数要大,它们之间的差距就是信息熵冗余,或称编码冗余。

    5.知识冗余 == 人类(头 身体 腿),汽车,房子 不需要记录

    是指在有些图像中还包含与某些验证知识有关的信息。

    6.I帧、P帧、B帧压缩思路

    I帧:帧内编码帧,关键帧,I帧可以看作一个图像经过压缩之后的产物,可以单独解码出一个完整的图像;(压缩率最低)

    P帧:前向预测/参考 编码帧,记录了本帧跟之前的一个关键帧(或P帧)的差别,解码时需要用之前缓存的画面叠加上本帧定义的差别,生成最终画面。 (压缩率比I帧高,比B帧低 属于 适中情况)

    B帧:双向预测/参考 编码帧,记录了本帧与前后帧的差别,解码需要参考前面一个I帧或者P帧,同时也需要后面的P帧才能解码一张完整的图像。 (参考前后的预测得到的,压缩率是最高,但是耗时)

    image.png

    三、编码标准

    1.组织

    • 国际电信联盟:H.264、H.265
    • MPEG系列标准:MPEG1、MPEG2、MPEG4、AVC

    AVC == H.264

    HEVC == H.265

    2.视频编码概念

    通过指定的压缩技术,把某一种视频格式文件,转换成另一种视频文件格式文件的方式。

    3. H.264分层结构(VCL和NAL)

    • VCL

      VCL(viedo coding layer,视频编码层):负责高效的视频内容展示。

      VCL数据:编码处理的输出,被压缩编码后的视频数据序列。

    • NAL

      NAL(Network Abstraction Layer,网络提取层):以网络所要求的恰当方式对数据进行打包传送,是传输层。不管是网络还是本地都需要通过这一层来传输。

    NAL = 一个字节的片头 + 若干的片数据

    image.png

    传输的是NAL

    4. H.264的输出结构

    H.264编码器默认的输出为:起始码+NALU。

    起始码:0x00000001和0x000001

    0x00000001:NALU里有狠多片

    0x000001:NALU里只有一片。

    5.举例分析H.264文件格式。

    image.png

    SPS 序列参数集(记录有多少I帧,多少B帧,多少P帧,帧是如何排列) == 7
    00 00 00 01 670x67 ---> 2进制01100111 ---> 取低五位 00000111 ---> 十六进制 0x07


    PPS 图像参数集(图像宽高信息等) == 8
    00 00 00 01 68, 0x68 ---> 2进制01101000---> 取低五位 00001000 ---> 十六进制 0x08


    SEI补充信息单元(可以记录坐标信息,人员信息, 后面解码的时候,可以通过代码获取此信息)https://blog.csdn.net/y601500359/article/details/80943990
    00 00 01 06 , 0x06 ---> 2进制00000110---> 取低五位00000110 ---> 十六进制 0x06

    I帧
    00 00 00 65, 0x65 ---> 2进制01100101---> 取低五位00000101 ---> 十六进制 0x05
    最终是 5 I帧完整画面出来

    P帧
    61 -->0x01 重要P帧
    41 -->0x01 非重要P帧

    B帧
    01 -->0x01 B帧

    image.png

    6.PTS和DTS

    DTS:解码时间戳,在什么时候解码这一帧的数据。

    PTS:显示时间戳,在什么时候显示这一帧数据。

    在没有B帧的时候,DTS和PTS是一样的顺序。

    因为B帧的解码需要靠前一帧和后一帧,只要有B帧DTS和PTS就一定会乱。

    image.png

    GOP:I帧+ 下一个I帧之前的所有B帧和P帧。

    i帧=GOP是什么理解的?
    SPS PPS I P B P B P B P B I 一组   SPS PPS I P B P B P B P B I 二组
    收起阅读 »

    Android 是怎么捕捉 java 异常的

    val default = Thread.getDefaultUncaughtExceptionHandler() Thread.setDefaultUncaughtExceptionHandler { t, e ->    //...
    继续阅读 »
     val default = Thread.getDefaultUncaughtExceptionHandler()

    Thread.setDefaultUncaughtExceptionHandler { t, e ->
       // 处理异常
       Log.e("Uncaught", "exception message : "+ e.message)
       // 将异常回执给原注册的 handler
       default.uncaughtException(t, e)
    }

    以上是很简单的一段代码,经常被用于 java 异常全局捕捉,但我的疑问是,他是怎么实现全局捕捉的,带着这样的疑问,我们来扒一下代码看看。


    顺藤摸瓜,我们看看静态方法 getDefaultUncaughtExceptionHandler 是被谁调用的,看了下所有的类调用的类,唯有 ThreadGroup 最靠谱:


    image.png


    在 parent 为空的情况下,就会调用 getDefaultUncaughtExceptionHandler 来回调异常,然后继续顺藤摸瓜,看看 ThreadGroup 的 uncaughtException 是被谁触发的,搜了一个圈,没有一个靠谱的。在我踌躇时,顺带瞄了一眼注释,奇迹发现:


    -   Called by the Java Virtual Machine when a thread in this
    - thread group stops because of an uncaught exception, and the thread
    - does not have a specific {[@link ](/link%20)Thread.UncaughtExceptionHandler}
    - installed.

    意思是:当一个未捕获的异常导致线程组中的线程停止时,JVM 会调用该方法。那我们就去搜搜 jvm 的源码,看看是怎么触发这个方法的。


    在 Hotspot 虚拟机源码的 thread.cpp 中的 JavaThread::exit 方法发现了这样的一段代码,并且还给出了注释:


    image.png


    在线程调用 exit 退出时,如果有未捕获的异常,则会调用 Thread.dispatchUncaughtException 方法,然后我们继续跟踪该方法:


    image.png


    然后调用当前线程的 uncaughtException 分发异常:


    image.png


    有意思的来了,如果我们没有给当前线程设置 UncaughtExceptionHandler ,则会将这个异常交给当前线程的 ThreadGroup 处理。如果我们给当前线程设置了 UncaughtExceptionHandler,则当前线程发生了异常,永远也不会抛给 getDefaultUncaughtExceptionHandler,该功能适合捕捉当前线程异常来用。


    终于回到了我们起初看到的 ThreadGroup.UncaughtExceptionHandler 方法,贴回原来的图继续分析:


    image.png


    这个地方会继续判断 parent 是否为空,parent 是个 ThreadGroup,ThreadGroup 实现了 Thread.UncaughtExceptionHandler 接口。这里我就直接说答案了,后面再说 ThreadGroup 和 Thread 的关系,最终会走到 system 的 ThreadGroup,system 的 parent 是个空,这时候走 else 分支,获取 Thread 中的 getDefaultUncaughtExceptionHandler 静态变量,触发 uncaughtException 方法,由于我们在 Activity 中设置了这个静态变量,所以,我们收到了这个异常通知。


    小知识


    1、如何捕获异常不退出


    val default = Thread.getDefaultUncaughtExceptionHandler()

    Log.e("Uncaught", "Uncaught handler: "+ default)
    // Uncaught handler: com.android.internal.os.RuntimeInit$KillApplicationHandler@21f02a3

    Thread.setDefaultUncaughtExceptionHandler { t, e ->
       // 将异常回执给原注册的 handler
       // default.uncaughtException(t, e)
    }

    捕获异常后,什么都不处理。但这样做显得非常不地道,这样会导致其他框架无法通过之前设置的静态变量捕获到异常上报。我打印了一下 default 是 RuntimeInit,该类在捕获到异常后,会做 killProcess。


    2、如何捕获指定线程异常:


    val thread = Thread {
         val a = 1/0
    }
    thread.setUncaughtExceptionHandler { t, e ->
           Log.e("Uncaught", "Uncaught trace: "+ e.message)
    }
    thread.start()

    3、ThreadGroup 和 Thread 的关系结构


    image.png



    • Thread 的 parent 是在 new Thread 的时候指定的,构造可传自定义的 ThreadGroup,默认是使用创建当前线程的 ThreadGroup

    • Thread 添加进 ThreadGroup 的 Thread[] 数组时机是在调用 start 启动线程的时候做的

    • ThreadGroup 的 parent 是在 new ThreadGroup 的时候指定的,构造可传自定义的 ThreadGroup,默认是使用当前线程的 ThreadGroup

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

    iOS RXSwift 4.7

    iOS
    ReplaySubjectReplaySubject 将对观察者发送全部的元素,无论观察者是何时进行订阅的。这里存在多个版本的 ReplaySubject,有的只会将最新的 n 个元素发送给观察者,有的只会将限制时间段内最新的元素发送给观察...
    继续阅读 »

    ReplaySubject

    ReplaySubject 将对观察者发送全部的元素,无论观察者是何时进行订阅的。

    这里存在多个版本的 ReplaySubject,有的只会将最新的 n 个元素发送给观察者,有的只会将限制时间段内最新的元素发送给观察者。

    如果把 ReplaySubject 当作观察者来使用,注意不要在多个线程调用 onNextonError 或 onCompleted。这样会导致无序调用,将造成意想不到的结果。


    演示

    let disposeBag = DisposeBag()
    let subject = ReplaySubject<String>.create(bufferSize: 1)

    subject
    .subscribe { print("Subscription: 1 Event:", $0) }
    .disposed(by: disposeBag)

    subject.onNext("🐶")
    subject.onNext("🐱")

    subject
    .subscribe { print("Subscription: 2 Event:", $0) }
    .disposed(by: disposeBag)

    subject.onNext("🅰️")
    subject.onNext("🅱️")

    输出结果:

    Subscription: 1 Event: next(🐶)
    Subscription: 1 Event: next(🐱)
    Subscription: 2 Event: next(🐱)
    Subscription: 1 Event: next(🅰️)
    Subscription: 2 Event: next(🅰️)
    Subscription: 1 Event: next(🅱️)
    Subscription: 2 Event: next(🅱️)

    BehaviorSubject

    当观察者对 BehaviorSubject 进行订阅时,它会将源 Observable 中最新的元素发送出来(如果不存在最新的元素,就发出默认元素)。然后将随后产生的元素发送出来。

    如果源 Observable 因为产生了一个 error 事件而中止, BehaviorSubject 就不会发出任何元素,而是将这个 error 事件发送出来。


    演示

    let disposeBag = DisposeBag()
    let subject = BehaviorSubject(value: "🔴")

    subject
    .subscribe { print("Subscription: 1 Event:", $0) }
    .disposed(by: disposeBag)

    subject.onNext("🐶")
    subject.onNext("🐱")

    subject
    .subscribe { print("Subscription: 2 Event:", $0) }
    .disposed(by: disposeBag)

    subject.onNext("🅰️")
    subject.onNext("🅱️")

    subject
    .subscribe { print("Subscription: 3 Event:", $0) }
    .disposed(by: disposeBag)

    subject.onNext("🍐")
    subject.onNext("🍊")

    输出结果:

    Subscription: 1 Event: next(🔴)
    Subscription: 1 Event: next(🐶)
    Subscription: 1 Event: next(🐱)
    Subscription: 2 Event: next(🐱)
    Subscription: 1 Event: next(🅰️)
    Subscription: 2 Event: next(🅰️)
    Subscription: 1 Event: next(🅱️)
    Subscription: 2 Event: next(🅱️)
    Subscription: 3 Event: next(🅱️)
    Subscription: 1 Event: next(🍐)
    Subscription: 2 Event: next(🍐)
    Subscription: 3 Event: next(🍐)
    Subscription: 1 Event: next(🍊)
    Subscription: 2 Event: next(🍊)
    Subscription: 3 Event: next(🍊)

    Variable (已弃用)

    Variable 是早期添加到 RxSwift 的概念,通过 “setting” 和 “getting”, 他可以帮助我们从原先命令式的思维方式,过渡到响应式的思维方式

    但这只是我们一厢情愿的想法。许多开发者滥用 Variable,来构建 重度命令式 系统,而不是 Rx 的 声明式 系统。这对于新手很常见,并且他们无法意识到,这是代码的坏味道。所以在 RxSwift 4.x 中 Variable 被轻度弃用,仅仅给出一个运行时警告。

    在 RxSwift 5.x 中,他被官方的正式的弃用了,并且在需要时,推荐使用 BehaviorRelay 或者 BehaviorSubject


    ControlProperty

    ControlProperty 专门用于描述 UI 控件属性的,它具有以下特征:

    • 不会产生 error 事件
    • 一定在 MainScheduler 订阅(主线程订阅)
    • 一定在 MainScheduler 监听(主线程监听)
    • 共享附加作用
    收起阅读 »

    Kotlin协程实现原理概述

    协程的顶层实现-CPS 现有如下代码: fun test(a: Int, b: Int) { // 求和 var result = a + b // 乘以2 result = result shl 1 // 加2 ...
    继续阅读 »

    协程的顶层实现-CPS


    现有如下代码:


    fun test(a: Int, b: Int) {
    // 求和
    var result = a + b
    // 乘以2
    result = result shl 1
    // 加2
    result += 2
    // 打印结果
    println(result)
    }

    我们来将代码SRP一下(单一职责):


    // 加法
    fun sum(a: Int,b: Int) = a + b
    // x乘以2
    fun double(x: Int) = x shl 1
    // x加2
    fun add2(x: Int) = x + 2

    // 最终的test
    fun test(a: Int, b: Int) {
    // 从内层依次调用,最终打印
    println(add2(double(sum(a,b))))
    }

    可以看到,我们将原来一坨的方法,抽离成了好几个方法,每个方法干一件事,虽然提高了可读性和可维护性,但是代码复杂了,我们来让它更复杂一点。


    上述代码是 让内层方法的返回值 作为参数 传递给外层方法,现在我们 把外层方法作为接口回调 传递给 内层方法:


    // 加法,next是加法做完的回调,会传入相加的结果
    fun sum(a: Int, b: Int, next: (Int) -> Unit) = a + b
    // x乘以2
    fun double(x: Int, next: (Int) -> Unit) = x shl 1
    // x加2
    fun add2(x: Int, next: (Int) -> Unit) = x + 2

    // 最终的test
    fun test2(a: Int, b: Int) {
    // 执行加法
    sum(a, b) { sum ->
    // 加完执行乘法
    double(sum) { double ->
    // 乘完就加2
    add2(double) { result ->
    // 最后打印
    println(result)
    }
    }
    }
    }

    这就是CPS的代码风格:通过接口回调的方式来实现的


    假设: 我们上述的几个方法: sum()/double()/add2()都是挂起函数,那么最终也会编译为CPS风格的回调函数方式,也就是:原来看起来同步的代码,经过编译器的"修改",变成了异步的方法,也就是:CPS化了,这就是kotlin协程的顶层实现逻辑。


    现在,让我们来验证一下,我们定义一个suspend函数,反编译看下是否真的CPS化了。


    // 定义挂起函数
    suspend fun test(id: String): String = "hello"

    反编译结果如下:


    // 参数添加了一个Continuation参数
    public final Object test(@NotNull String id, @NotNull Continuation $completion) {
    return "hello";
    }

    可以看到,多了个Continuation参数,这是个接口,是在本次函数执行完毕后执行的回调,内容如下:


    public interface Continuation<in T> {
    // 保存上下文(比如变量状态)
    public val context: CoroutineContext

    // 方法执行结束的回调,参数是个范型,用来传递方法执行的结果
    public fun resumeWith(result: Result<T>)
    }

    好,现在我们知道了suspend函数 是通过添加Continuation来实现的,我们来看个具体的业务:


    // 根据id获取token
    suspend fun getToken(id: String): String = "token"

    // 根据token获取info
    suspend fun getInfo(token: String): String = "info"

    // 测试
    suspend fun test() {
    // 先获取token,这是耗时请求
    val token = getToken("123")
    // 再根据token获取info,这也是个耗时请求
    val info = getInfo(token)
    // 打印
    println(info)
    }

    上述的业务代码很简单,但是前两步都是耗时操作,线程会卡在那里wait吗?显然不会,既然是suspend函数,那么就可以CPS化,等价的CPS代码如下:


    // 跟上述相同,传递了Continuation回调
    fun getToken(id: String, callback: Continuation<String>): String = "token"

    // 跟上述相同,传递了Continuation回调
    fun getInfo(token: String, callback: Continuation<String>): String = "info"

    // 测试(只写了主线代码)
    fun test() {
    // 先获取token,传入回调
    getToken("123", object : Continuation<String> {
    override fun resumeWith(result: Result<String>) {
    // 用token获取info,传入回调
    val token = result.getOrNull()
    getInfo(token!!, object : Continuation<String> {
    override fun resumeWith(result: Result<String>) {
    // 打印结果
    val info = result.getOrNull()
    println(info)
    }
    })
    }
    })
    }

    上述就是无suspend的CPS风格代码,通过传入接口回调来实现协程的同步代码风格。


    接下来我们来反编译suspend风格代码,看下它里面是怎么调度的。


    协程的底层实现-状态机


    我们先来简单修改下suspend test函数:


    // 没变化
    suspend fun getToken(id: String): String = "token"
    // 没变化
    suspend fun getInfo(token: String): String = "info"

    // 添加了局部变量a,看下suspend怎么保存a这个变量
    suspend fun test() {
    val token = getToken("123") // 挂起点1
    var a = 10 // 这里是10
    val info = getInfo(token) // 挂起点2,需要将前面的数据保存(比如a),在挂起点之后恢复
    println(info)
    println(a
    }

    每个suspend函数调用点,都会生成一个挂起点,在挂起点我们要保存当前的运行状态,比如局部变量等。


    反编译后的代码大致如下:


    public final Object getToken(String id, Continuation completion) {
    return "token";
    }

    public final Object getInfo(String token, Continuation completion) {
    return "info";
    }

    // 重点函数(伪代码)
    public final Object test(Continuation<String>: continuation) {
    Continuation cont = new ContinuationImpl(continuation) {
    int label; // 保存状态
    Object result; // 保存中间结果,还记得那个Result<T>吗,是个泛型,因为泛型擦除,所以为Object,用到就强转
    int tempA; // 保存上下文a的值,这个是根据具体代码产生的
    };
    switch(cont.label) {
    case 0 : {
    cont.label = 1; //更新label

    getToken("123",cont) // 执行对应的操作,注意cont,就是传入的回调
    break;
    }

    case 1 : {
    cont.label = 2; // 更新label

    // 这是一个挂起点,我们要保存上下文数据,这里就保存a的值
    int a = 10;
    cont.tempA = a; // 保存a的值

    // 获取上一步的结果,因为泛型擦除,需要强转
    String token = (Object)cont.result;
    getInfo(token, cont); // 执行对应的操作
    break;
    }

    case 2 : {
    String info = (Object)cont.result; // 获取上一步的结果
    println(info); // 执行对应的操作

    // 在挂起点之后,恢复a的值
    int a = cont.tempA;
    println(a);

    return;
    }
    }
    }

    我们可以将每个case理解为一个状态,每个case分支对应的语句,理解为一个Continuation实现。


    上述伪代码大致描述了协程的调度流程:



    • 1 调用test函数时,需要传入一个Continuation接口,我们会对它进行二次装饰。

    • 2 装饰就是根据函数具体逻辑,在内部添加额外的上下文数据和状态信息(也就是label)。

    • 3 每个状态对应一个Continuation接口,里面会执行对应的业务逻辑。

    • 4 每个状态都会: 保存上下文信息 -> 获取上一个状态的结果 -> 执行本状态业务逻辑 -> 恢复上下文信息。

    • 5 直到最后一个状态对应的逻辑执行完毕。


    总结


    综上,我们可以归纳以下几点:



    • 1 Kotlin协程没有很"频繁"的切换线程,它是在顶层通过调度方式实现的,所以效率是比较高的。

    • 2 Kotlin中,每个suspend方法,都需要一个Continuation接口实现,用来执行下一个状态的操作;并且,每个suspend方法的调用点都会产生一个挂起点。

    • 3 每个挂起点,都会产生一个label,对应于状态机的一个状态,不同的状态之间,通过Continuation来切换。

    • 4 Kotlin协程会在每个挂起点保存当前的上下文数据,并且在挂起点之后进行恢复。这样,每个状态之间就是相互独立的,可以独立调度。

    • 5 协程的切换,只不过是从一种状态切换到另一种状态,因为不同状态是相互独立的,所以在合适的时机,再切换回来也不会对结果造成影响。

    作者:奔波儿灞取经
    链接:https://juejin.cn/post/7011011123814072327
    来源:掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »