注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

iOS性能优化 — 三、安装包瘦身

瘦身指导原则 总体指导原则为:压缩资源、删除无用/重复资源、删除无用代码、通过编译选项进行优化。 常规瘦身方案 压缩资源项目中资源包括图片、字符串、音视频等资源。由于项目中图片比较多,所以资源压缩一般会从图片入手。在把图片加入到项目中时候需要采用tinypng...
继续阅读 »

瘦身指导原则


总体指导原则为:压缩资源、删除无用/重复资源、删除无用代码、通过编译选项进行优化。


常规瘦身方案


压缩资源
项目中资源包括图片、字符串、音视频等资源。由于项目中图片比较多,所以资源压缩一般会从图片入手。在把图片加入到项目中时候需要采用tinypng或者ImageOptim对图片进行压缩;另外,可以通知设计,对切图进行压缩处理再上传;不需要内嵌到项目中的图片可以改为动态下载。


  • png,jpg,gif可以替换成webp


  • 动画图片可替换为lotties、APNG


  • 小图或表情图可替换为iconFont


  • 大图可替换为svg



删除无用/重复资源
删除无用的资源。项目中主要以删除图片为主:


  • 图片用2x和3x图就可以,不要用1x图。


  • 可以用LSUnusedResources搜索出未使用的图片然后删除之。注意:该软件搜索出来的图片有可能项目中还在用,删除之前需要在工程中先搜索下图片是否有使用再确认是否可以删除。



删除无用代码
删除无用类和库:可以用WBBladesForMac来分析,注意:通过字符串调用的类也会检测为无用类。

非常规瘦身方案
1、Strip :去除不必要的符号信息。
-Strip Linked Product 和 Strip Swift Symbols 设置为 YES,Deployment Postprocessing 设置为 NO,发布代码的时候也需要勾选 Strip Swift Symbols。


  • Strip Debug Symbols During Copy 和 Symbols Hidden by Default 在release下设为YES


  • Dead Code Stripping 设置为 YES


  • 对于动态库,可用strip -x [动态库路径] 去除不必要的符号信息



2、Make Strings Read-Only设为YES。
3、Link-Time Optimization(LTO)release下设为 Incremental。WWDC2016介绍编译时会移除没有被调用的方法和代码,优化程序运行效率。
4、开启BitCode
5、去除异常支持。不能使用@try @catch,包只缩小0.1M,效果不显著。
Enable C++ Exceptions和Enable Objective-C Exceptions设为NO,Other C Flags添加-fno-exceptions
6、不生成debug symbols:不能生成dSYM,效果非常显著。
Generate debug symbols选项 release 设置为NO

脑图借鉴

转自:https://www.jianshu.com/p/369c909c1067

收起阅读 »

关于webpack面试题总结

最近在读《webpack深入浅出》,总结一下webpack关于面试常见的问题,分享出来,希望可以帮助更多小伙伴在找到心爱的工作和期待的薪水。一.常见的构建工具有哪些?他们各自优缺点?为什么选择了webpack?Grunt、Gulp、Fis3、Rollup、Np...
继续阅读 »

最近在读《webpack深入浅出》,总结一下webpack关于面试常见的问题,分享出来,希望可以帮助更多小伙伴在找到心爱的工作和期待的薪水。

一.常见的构建工具有哪些?他们各自优缺点?为什么选择了webpack?

Grunt、Gulp、Fis3、Rollup、Npm Script、webpack

<1>Grunt的优点是:

• 灵活,它只负责执行我们定义的任务;

• 大量的可复用插件封装好了常见的构建任务。

Grunt的缺点是:

集成度不高,要写很多配置后才可以用,无法做到开箱即用。Grunt相当于进化版的NpmScript,它的诞生其实是为了弥补NpmScript的不足。

<2>Gulp的优点是: 好用又不失灵活,既可以单独完成构建,也可以和其他工具搭配使用。

其缺点: 和Grunt类似,集成度不高,要写很多配置后才可以用,无法做到开箱即用。

<3> Fis3的优点是:集成了各种Web开发所需的构建功能,配置简单、开箱即用。

其缺点是 目前官方己经不再更新和维护,不支持最新版本的T、fode

<4>Webpack的优点是:• 专注于处理模块化的项目,能做到开箱即用、一步到位:

• 可通过Plugin扩展,完整好用又不失灵活;

• 使用场景不局限于Web开发

• 社区庞大活跃,经常引入紧跟时代发展的新特性,能为大多数场景找到已有的开源扩展:

• 良好的开发体验。

Webpack的缺点是:只能用于采用模块化开发的项目。

<5> Rollup是在Webpack流行后出现的替代品,讲述差别::
• Rollup生态链还不完善,体验不如Webpack;

• Rollup的功能不如Webpack完善,但其配置和使用更简单:

• Rollup不支持CodeSpliting,但好处是在打包出来的代码中没有Webpack那段模块的加载、执行和缓存的代码。

Roll up在用于打包JavaScript库时比Webpack更有优势,因为其打包出来的代码更小、

深入浅出Webpack更快。

缺点:但它的功能不够完善,在很多场景下都找不到现成的解决方案

<6>Npm Script的优点 是内置,无须安装其他依赖。
其缺点 是功能太简单,虽然提供了pre和post两个钩子,但不能方便地管理多个任务之间的依赖

为啥选择webpack?
大多数团队在开发新项目时会采用紧跟时代的技术,这些技术几乎都会采用“模块化+新语言+新框架”,Webpack可以为这些新项目提供一站式的解决方案:
• Webpack有良好的生态链和维护团队,能提供良好的开发体验并保证质量:

• Webpack被全世界大量的Web开发者使用和验证,能找到各个层面所需的教程和经验分享。

二.有哪些常见的Loader?你用过哪些Loader?

1. 加载文件
• raw-loader :将文本文件的内容加载到代码中

• file-loader :将文件输出到一个文件夹中,在代码中通过相对 URL 去引用输出的文件

• url-loader :和 file-loader 类似,但是能在文件很小的情况下以 base64 方式将文件的内容注入代码中

• source-map-loader :加载额外的 SourceMap 文件,以方便断点调试

• svg-inline-loader :将压缩后的SVG 内容注入代码中

• node-loader :加载 Node.js 原生模块的 .node 文件

• image-loader :加载并且压缩图片文件

• json-loader:加载 JSON 文件

• yaml-loader:加载 YAML 文件

2. 编译模版
• pug-loader :将 Pug 模版转换成 JavaScript 函数井返回。

• handlebars-loader:将 Handlebars模版编译成函数并返回

• s-loader :将 EJS 模版编译成函数井返回

• haml-loader:将 HAML 代码转换成 HTML

• markdown-loader 将 Markdown 文件转换成 HTML

3.转换脚本语言
• babel-loader :将 ES6 转换成 ES5

• ts-loader :将 TypeScript 转换成 JavaScript,

• awesome-typescript-loader: Type Script 转换成 JavaScript ,性能要比 ts-loader好

• coffee-loader 将 CoffeeScript换成 JavaScript

4.转换样式文件
• css-loader :加载 css ,支持模块化、压缩、文件导入等特性。

• style-loader :将 css 代码 注入JavaScript 中,通过 DOM 操作去加载 css

• sass-loader :将 SCSS SASS 代码转换成 css

• postcss-loader : 扩展 css 语法,使用css

• less-loader : Less 代码转换成 css代码

• stylus-loader :将 Stylu 代码转换成 css 码。

5. 检查代码
• eslint-loader :通过 ESLint 检查 JavaScript
代码

• tslint-loader :通过 TSLint peScript
代码

• mocha-loader :加载 Mocha 测试
用例的代码

• coverjs-loader : 计算测试的覆盖率。

6.其他 Loader
• vue-loader :加载 Vue. 单文件组件

• i18n-loader:加载多语言版本,支持国际化

• ignore-loader :忽略部分文件

• ui-component-loader:按需加载
UI 组件库,例如在使用 antdUI 组件库时,不会因为只用到了 Button 组件而打包进所有的组件

3.有哪些常见的Plugin?你用过哪些Plugin?

1.用于修改行为
• define-plugin :定义环境变量

• context-replacement-plugin : 修改 require 语句在寻找文件时的默认行为

• ignore-plugin :用 于忽略部分文件

2.用于优化
• commons-chunk-plugin :提取公共代码。

• extract-text-webpack-plugin :提取 JavaScript 中的 css 代码到单独的文件中

• prepack-webpack-plugin :通过Facebook Prepack 优化输出的 JavaScript 代码的性能

• uglifyjs-webpack-plugin :通过 UglifyES 压缩 S6 代码

• webpack-parallel-uglify-plugin :多进程执行 glifyJS 代码压缩,提升构建的速度

• imagemin-webpack-plugin : 压缩图片文件。

• webpack-spritesmith :用插件制作碧图

• ModuleConcatenationPlugin : 开启 WebpackScopeHoisting 功能

• dll-plugin :借鉴 DDL 的思想大幅度提升构建速度

• hot-module-replacem nt-plugin 开启模块热替换功能。

3. 其他 Plugin
• serviceworker-webpack-plugin :为网页应用增加离钱缓存功能

• stylelint-webpack-plugin : stylelint集成到项目中,

• i18n-webpack-plugin : 使网页支持国际化。

• provide-plugin : 从环境中提供的全局变量中加载模块,而不用导入对应的文件。

• web-webpack-plugin : 可方便地为单页应用输出 HTML ,比 html-webpack-plugin 好用

4.那你再说一说Loader和Plugin的区别

Loader :模块转换器,用于将模块的原内容按照需求转换成新内容。
Plugin :扩展插件,在 Webpack 构建流程中的特定时机注入扩展逻辑,来改变构建结
果或做我们想要的事情。

5.Webpack构建流程简单说一下

初始化参数 从配置文件和 Shell 语句中读取与合并参数,得出最终的参数
• 开始编译:用上 步得到的参数初始 Co er 对象,加载所有配置的插件,通
过执行对象的 run 方法开始执行编译
• 确定入口 根据配置中的 ntry 找出所有入口文件
• 编译模块:从入口文件出发,调用所有配置的 Loader 对模块进行翻译,再找出
模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理
• 完成模块编译 在经过第 步使用 Loader 翻译完所有模块后, 得到了每个模块被
翻译后的最终内容及它们之间的依赖关系。
• 输出资源:根据入口和模块之间的依赖关系,组装成 个个包含多个模块的 Chunk,
再将每个 Chunk 转换成 个单独的文件加入输出列表中,这是可以修改输出内容
的最后机会
• 输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,将文件的内
容写入文件系统中。

6.使用webpack开发时,你用过哪些可以提高效率的插件

webpack-dashboard:可以更友好的展示相关打包信息。
webpack-merge:提取公共配置,减少重复配置代码
speed-measure-webpack-plugin:简称 SMP,分析出 Webpack 打包过程中 Loader 和 Plugin 的耗时,有助于找到构建过程中的性能瓶颈。
size-plugin:监控资源体积变化,尽早发现问题
HotModuleReplacementPlugin:模块热替换

7.模块打包原理知道吗?


8.什么 是模块热更新?


devServer.hot 配置是否启用 ,开启模块热替换功能后,将在不刷新整个页面的情况下通过用新模块替换老模块来做到实时预览


9.如何提高webpack的构建速度?


10.文件监听原理呢?


11.source map是什么?生产环境怎么用?


12.如何对bundle体积进行监控和分析?


13.文件指纹是什么?怎么用?


14.在实际工程中,配置文件上百行乃是常事,如何保证各个loader按照预想方式工作?


15.如何优化 Webpack 的构建速度?


16.你刚才也提到了代码分割,那代码分割的本质是什么?有什么意义呢?


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


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


19.聊一聊Babel原理吧?


20.什么是Tree-shaking?
Tree Shaking 可以用来剔除 JavaScript 中用 不上的死代码。


21.如何实现 按需加载?


``import(/* webpackChunkName : ” show " */ ’. / show ’>


Webpack 内置了对 import *)语句的支持,当 Wepack 遇到了类似的语句时会这样


处理:
• 以./ show.j 为入口重新生成一个 Chunk;
• 代码执行到 import 所在的语句时才去加载由 Chunk 对应生成的文件:
• import 返回一个 Promise ,当文件加载成功时可以在 Promise then 方法中获取
show.j 导出的内容。``


22.如何配置单页应用?如何配置多页应用?


23.如何利用webpack来优化前端性能?(提高性能和体验)


24.npm打包时需要注意哪些?如何利用webpack来更好的构建


25.什么是模块化,都有哪些?


模块化是指一个复杂的系统分解为多个模块以方便编码。


js模块化:


mommon.js:核型思想,通过require方法来同步加载依赖的其他模块,通过module.exports导出需要暴露的接口。


优点
1.代码可复用于node环境并运行,例如同构应用
2.通过npm发布的很多第三方模块都采用了mommonJS规范


缺点:1.无法直接运行在浏览器环境下,必需通过工具转换成标准的es5


AMD:异步方式去加载依赖的模块,主要用来解决针对浏览器环境的模块化问题,最具代表的实现是require.js


优点
1.可在不转换代码的情况下,直接在浏览器中运行
2.可异步加载依赖
3.可并行加载多个依赖
4.代码可运行在浏览器和node环境下


缺点 :1.js运行环境没有原生支持AMD,需要先导入实现了AMD的库后才能正常使用。


es6模块化:

import { readFile} from 'fs';
import react from 'react';

// 导出
export function hello(){};
export default{...}


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

收起阅读 »

NodeJs中的stream(流)- 基础篇

一、什么是Stream(流) 流(stream)在 Node.js 中是处理流数据的抽象接口(abstract interface)。 stream 模块提供了基础的 API 。使用这些 API 可以很容易地来构建实现流接口的对象。 流是可读的、可写的,或...
继续阅读 »

一、什么是Stream(流)



流(stream)在 Node.js 中是处理流数据的抽象接口(abstract interface)。 stream 模块提供了基础的 API 。使用这些 API 可以很容易地来构建实现流接口的对象。



流是可读的、可写的,或是可读写的。


二、NodeJs中的Stream的几种类型


Node.js 中有四种基本的流类型:



  • Readable - 可读的流(fs.createReadStream())

  • Writable - 可写的流(fs.createWriteStream())

  • Duplex - 可读写的流(net.Socket)

  • Transform - 在读写过程中可以修改和变换数据的 Duplex 流 (例如 zlib.createDeflate())


NodeJs中关于流的操作被封装到了Stream模块中,这个模块也被多个核心模块所引用。

const stream = require('stream');

在 NodeJS 中对文件的处理多数使用流来完成



  • 普通文件

  • 设备文件(stdin、stdout)

  • 网络文件(http、net)


注:在NodeJs中所有的Stream(流)都是EventEmitter的实例


Example:


1.将1.txt的文件内容读取为流数据

const fs = require('fs');

// 创建一个可读流(生产者)
let rs = fs.createReadStream('./1.txt');

通过fs模块提供的createReadStream()可以轻松创建一个可读的文件流。但我们并有直接使用Stream模块,因为fs模块内部已经引用了Stream模块并做了封装。所以说 流(stream)在 Node.js 中是处理流数据的抽象接口,提供了基础Api来构建实现流接口的对象。

var rs = fs.createReadStream(path,[options]);

1.path 读取文件的路径


2.options



  • flags打开文件的操作, 默认为'r'

  • mode 权限位 0o666

  • encoding默认为null

  • start开始读取的索引位置

  • end结束读取的索引位置(包括结束位置)

  • highWaterMark读取缓存区默认的大小64kb


Node.js 提供了多种流对象。 例如:



  • HTTP 请求 (request response)

  • process.stdout 就都是流的实例。


2.创建可写流(消费者)处理可读流


将1.txt的可读流 写入到2.txt文件中 这时我们需要一个可写流

const fs = require('fs');
// 创建一个可写流
let ws = fs.createWriteStream('./2.txt');
// 通过pipe让可读流流入到可写流 写入文件
rs.pipe(ws);
var ws = fs.createWriteStream(path,[options]);

1.path 读取文件的路径


2.options



  • flags打开文件的操作, 默认为'w'

  • mode 权限位 0o666

  • encoding默认为utf8

  • autoClose:true是否自动关闭文件

  • highWaterMark读取缓存区默认的大小16kb


pipe 它是Readable流的方法,相当于一个"管道",数据必须从上游 pipe 到下游,也就是从一个 readable 流 pipe 到 writable 流。

后续将深入将介绍pipe。




如上图,我们把文件比作装水的桶,而水就是文件里的内容,我们用一根管子(pipe)连接两个桶使得水从一个桶流入另一个桶,这样就慢慢的实现了大文件的传输过程。

三、为什么应该使用 Stream


当有用户在线看视频,假定我们通过HTTP请求返回给用户视频内容

const http = require('http');
const fs = require('fs');

http.createServer((req, res) => {
fs.readFile(videoPath, (err, data) => {
res.end(data);
});
}).listen(8080);

但这样有两个明显的问题


1.视频文件需要全部读取完,才能返回给用户,这样等待时间会很长

2.视频文件一次全放入内存中,内存吃不消


用流可以将视频文件一点一点读到内存中,再一点一点返回给用户,读一部分,写一部分。(利用了 HTTP 协议的 Transfer-Encoding: chunked 分段传输特性),用户体验得到优化,同时对内存的开销明显下降

const http = require('http');
const fs = require('fs');

http.createServer((req, res) => {
fs.createReadStream(videoPath).pipe(res);
}).listen(8080);

四、可读流(Readable Stream)



可读流(Readable streams)是对提供数据的源头(source)的抽象。



例如:



  • HTTP responses, on the client

  • HTTP requests, on the server

  • fs read streams

  • TCP sockets

  • process.stdin


所有的 Readable 都实现了 stream.Readable 类定义的接口。


可读流的两种模式(flowing 和 paused)


1.在 flowing 模式下, 可读流自动从系统底层读取数据,并通过 EventEmitter 接口的事件尽快将数据提供给应用。


2.在 paused 模式下,必须显式调用 stream.read()方法来从流中读取数据片段。


所有初始工作模式为paused的Readable流,可以通过下面三种途径切换为flowing模式:



  • 监听'data'事件

  • 调用stream.resume()方法

  • 调用stream.pipe()方法将数据发送到Writable


流动模式flowing


流切换到流动模式 监听data事件

const rs = fs.createReadStream('./1.txt');
const ws = fs.createWriteStream('./2.txt');
rs.on('data', chunk => {
ws.write(chunk);
});
ws.on('end', () => {
ws.end();
});

如果写入的速度跟不上读取的速度,有可能导致数据丢失。正常的情况应该是,写完一段,再读取下一段,如果没有写完的话,就让读取流先暂停,等写完再继续。

var fs = require('fs');
// 读取highWaterMark(3字节)数据,读完之后填充缓存区,然后触发data事件
var rs = fs.createReadStream(sourcePath, {
highWaterMark: 3
});
var ws = fs.createWriteStream(destPath, {
highWaterMark: 3
});

rs.on('data', function(chunk) { // 当有数据流出时,写入数据
if (ws.write(chunk) === false) { // 如果没有写完,暂停读取流
rs.pause();
}
});

ws.on('drain', function() { // 缓冲区清空触发drain事件 这时再继续读取
rs.resume();
});

rs.on('end', function() { // 当没有数据时,关闭数据流
ws.end();
});

或者使用更直接的pipe

fs.createReadStream(sourcePath).pipe(fs.createWriteStream(destPath));

暂停模式paused


1.在流没有 pipe() 时,调用 pause() 方法可以将流暂停

2.pipe() 时,需要移除所有 data 事件的监听,再调用 unpipe() 方法


read(size)

流在暂停模式下需要程序显式调用 read() 方法才能得到数据。read() 方法会从内部缓冲区中拉取并返回若干数据,当没有更多可用数据时,会返回null。read()不会触发'data'事件。


使用 read() 方法读取数据时,如果传入了 size 参数,那么它会返回指定字节的数据;当指定的size字节不可用时,则返回null。如果没有指定size参数,那么会返回内部缓冲区中的所有数据。

NodeJS 为我们提供了一个 readable 的事件,事件在可读流准备好数据的时候触发,也就是先监听这个事件,收到通知又数据了我们再去读取就好了:

const fs = require('fs');
rs = fs.createReadStream(sourcePath);

// 当你监听 readable事件的时候,会进入暂停模式
rs.on('readable', () => {
console.log(rs._readableState.length);
// read如果不加参数表示读取整个缓存区数据
// 读取一个字段,如果可读流发现你要读的字节小于等于缓存字节大小,则直接返回
let ch = rs.read(1);
});

暂停模式 缓存区的数据以链表的形式保存在BufferList中


五、可写流(Writable Stream)



可写流是对数据流向设备的抽象,用来消费上游流过来的数据,通过可写流程序可以把数据写入设备,常见的是本地磁盘文件或者 TCP、HTTP 等网络响应。



Writable 的例子包括了:



  • HTTP requests, on the client

  • HTTP responses, on the server

  • fs write streams

  • zlib streams

  • crypto streams

  • TCP sockets

  • child process stdin

  • process.stdout, process.stderr


所有 Writable 流都实现了 stream.Writable 类定义的接口。

process.stdin.pipe(process.stdout);

process.stdout 是一个可写流,程序把可读流 process.stdin 传过来的数据写入的标准输出设备。在了解了可读流的基础上理解可写流非常简单,流就是有方向的数据,其中可读流是数据源,可写流是目的地,中间的管道环节是双向流。


可写流使用


调用可写流实例的 write() 方法就可以把数据写入可写流

const fs = require('fs');
const rs = fs.createReadStream(sourcePath);
const ws = fs.createWriteStream(destPath);

rs.setEncoding('utf-8'); // 设置编码格式
rs.on('data', chunk => {
ws.write(chunk); // 写入数据
});

监听了可读流的 data 事件就会使可读流进入流动模式,我们在回调事件里调用了可写流的 write() 方法,这样数据就被写入了可写流抽象的设备destPath中。


write() 方法有三个参数



  • chunk {String| Buffer},表示要写入的数据

  • encoding 当写入的数据是字符串的时候可以设置编码

  • callback 数据被写入之后的回调函数


'drain'事件


如果调用 stream.write(chunk) 方法返回 false,表示当前缓存区已满,流将在适当的时机(缓存区清空后)触发 'drain

const fs = require('fs');
const rs = fs.createReadStream(sourcePath);
const ws = fs.createWriteStream(destPath);

rs.setEncoding('utf-8'); // 设置编码格式
rs.on('data', chunk => {
let flag = ws.write(chunk); // 写入数据
if (!flag) { // 如果缓存区已满暂停读取
rs.pause();
}
});

ws.on('drain', () => {
rs.resume(); // 缓存区已清空 继续读取写入
});

六、总结


stream(流)分为可读流(flowing mode 和 paused mode)、可写流、可读写流,Node.js 提供了多种流对象。 例如, HTTP 请求 和 process.stdout 就都是流的实例。stream 模块提供了基础的 API 。使用这些 API 可以很容易地来构建实现流接口的对象。它们底层都调用了stream模块并进行封装。



后续我们将继续对stream深入解析以及Readable Writable pipe的实现

作者:Brolly
链接:https://www.jianshu.com/p/1d36648fb87e
来源:简书

收起阅读 »

线程切换哪家强?RxJava与Flow的操作符对比

Flow作为Coroutine版的RxJava,同RxJava一样可以方便地进行线程切换。 本文针对两者在多线程场景中的使用区别进行一个简单对比。 1. RxJava 我们先来回顾一下RxJava中的线程切换 如上,RxJava使用subscriberOn...
继续阅读 »

Flow作为Coroutine版的RxJava,同RxJava一样可以方便地进行线程切换。
本文针对两者在多线程场景中的使用区别进行一个简单对比。


1. RxJava




我们先来回顾一下RxJava中的线程切换
在这里插入图片描述


如上,RxJava使用subscriberOnobserveOn进行线程切换


subscribeOn


subscribeOn用来决定在哪个线程进行订阅,对于Cold流来说即决定了数据的发射线程。使用中有两点注意:



  1. 当调用链上只有一个subscribeOn时,可以出现在任意位置


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


上面两种写法效果是一样的:都是在io线程订阅后发射数据



  1. 当调用链上有多个subscribeOn时,只有第一个生效:


在这里插入图片描述


上面第二个subscribeOn没有意义


observeOn


observeOn用来决定在哪个线程上响应



  1. observeOn决定调用链上下游操作符执行的线程


在这里插入图片描述


上面绿线部分的代码将会运行在主线程



  1. subscribeOn不同,调用链上允许存在多个observeOn且每个都有效


在这里插入图片描述


上面蓝色绿色部分因为observeOn的存在分别切换到了不同线程执行


just


RxJava的初学者经常会犯的一个错误是在Observable.just(...)里做耗时任务。 just并不是接受lambda,所以是立即执行的,不受subscribeOn的影响


在这里插入图片描述


如上,loadDataSync()不会在io执行,


想要在io执行,需要使用Observable.deffer{}


在这里插入图片描述


flatMap


结合上面介绍的RxJava的线程切换,看下面这段代码


在这里插入图片描述


如果我们希望loadData(id)并发执行,那么上面的写法是错误的。


subscribe(io())意味着其上游的数据在单一线程中串行发射。因此虽然flatMap{}返回多个Observable, 都是都在单一线程中订阅,多个loadData始终运行在同一线程。


代码经过一下修改后,可以达到并发执行的效果:


在这里插入图片描述


当订阅flatMap返回的Observable时,通过subscribeOn分别指定订阅线程。


其他类似flatMap这种涉及多个Observable订阅的操作符(例如mergezip等),需要留意各自的subscribeOn的线程,以防不符合预期的行为出现。


2. Flow




接下来看一下 Flow的线程切换 。


Flow是基于CoroutineContext进行线程切换,所以这部分内容需要你对Croutine事先有基本的了解。


在这里插入图片描述
flowOn类似于RxJava的subscribeOn,Flow中没有对应observeOn的操作符,因为collect是一个suspend函数,必须在CoroutineScope中执行,所以响应线程是由CoroutineContext决定的。例如你在main中执行collect,那么响应线程就是Dispatcher.Main


flowOn


flowOn类似于subscribeOn,因为它们都可以用来决定上游线程
在这里插入图片描述
上面代码中,flowOn前面代码将会在IO执行。


subscribeOn不同的是,flowOn允许出现多次,每个都会影响其前面的操作
在这里插入图片描述
上面代码,根据颜色可以看出来flowOn影响的范围


launchIn


collect是suspend函数,所以后续代码因为协程挂起不会继续执行
在这里插入图片描述
所以上面代码可能会不符合预期,因为第一个collect不走完第二个走不到。


正确的写法是为每个collect单独起一个协程
在这里插入图片描述
或者使用launchIn,写法更加优雅
在这里插入图片描述
launchIn不会挂起协程,所以与RxJava的subscribe更加接近。


通过名字可以感觉出来launchIn只不过是之前例子中launch的一个链式调用的语法糖。


flowOf


flowOf类似于Observable.just(),需要注意flowOf内的内容是立即执行的,不受flowOn影响
在这里插入图片描述


希望calculate()运行在IO,可以使用flow{ }


在这里插入图片描述


flatMapMerge


flatMapMerge类似RxJava的flatMap
在这里插入图片描述
如上,2个item各自flatMap成2个item,即一共发射了4条数据,日志输出如下:


inner: pool-2-thread-2 @coroutine#4
inner: pool-2-thread-3 @coroutine#5
inner: pool-2-thread-3 @coroutine#5
inner: pool-2-thread-2 @coroutine#4
collect: pool-1-thread-2 @coroutine#2
collect: pool-1-thread-2 @coroutine#2
collect: pool-1-thread-2 @coroutine#2
collect: pool-1-thread-2 @coroutine#2
复制代码

通过日志我们发现flowOn虽然写在flatMapMerge外面,inner的日志却可以打印在多个线程上(都来自pool2线程池),这与flatMap是不同的,同样场景下flatMap只能运行在线程池的固定线程上。


如果将flowOn写在flatMapMerge内部


在这里插入图片描述


结果如下:


inner: pool-2-thread-2 @coroutine#6
inner: pool-2-thread-1 @coroutine#7
inner: pool-2-thread-2 @coroutine#6
inner: pool-2-thread-1 @coroutine#7
collect: pool-1-thread-3 @coroutine#2
collect: pool-1-thread-3 @coroutine#2
collect: pool-1-thread-3 @coroutine#2
collect: pool-1-thread-3 @coroutine#2
复制代码

inner仍然打印在多个线程,flowOn无论写在flatMapMerge内部还是外部,对flatMapMerge内的处理没有区别。


但是flatMapMerge之外还是有区别的,看下面两段代码
在这里插入图片描述
在这里插入图片描述


通过颜色可以知道flowOn影响的范围,向上追溯到flowOf为止


3. Summary




RxJava的Observable与Coroutine的Flow都支持线程切换,相关API的对比如下:































线程池调度线程操作符数据源同步创建异步创建并发执行
RxJavaSchedulers (io(), computation(), mainThread())subscribeOn, observeOnjustdeffer{}flatMap(inner subscribeOn)
FlowDispatchers (IO, Default, Main)flowOnflowOfflow{}flatMapMerge(inner or outer flowOn)

最后通过一个例子看一下如何将代码从RxJava迁移到Flow


RxJava


RxJava代码如下:


在这里插入图片描述


使用到的Schedulers定义如下:
在这里插入图片描述


代码执行结果:


1: pool-1-thread-1
1: pool-1-thread-1
1: pool-1-thread-1
2: pool-3-thread-1
2: pool-3-thread-1
2: pool-3-thread-1
inner 1: pool-4-thread-1
inner 1: pool-4-thread-2
inner 1: pool-4-thread-1
inner 1: pool-4-thread-1
inner 1: pool-4-thread-2
inner 1: pool-4-thread-2
inner 1: pool-4-thread-3
inner 2: pool-5-thread-1
inner 2: pool-5-thread-2
3: pool-5-thread-1
inner 2: pool-5-thread-2
inner 1: pool-4-thread-3
inner 2: pool-5-thread-2
inner 2: pool-5-thread-3
3: pool-5-thread-1
3: pool-5-thread-1
3: pool-5-thread-1
end: pool-6-thread-1
end: pool-6-thread-1
inner 1: pool-4-thread-3
end: pool-6-thread-1
3: pool-5-thread-1
inner 2: pool-5-thread-1
3: pool-5-thread-1
inner 2: pool-5-thread-3
inner 2: pool-5-thread-1
end: pool-6-thread-1
3: pool-5-thread-3
3: pool-5-thread-3
end: pool-6-thread-1
inner 2: pool-5-thread-3
3: pool-5-thread-3
end: pool-6-thread-1
end: pool-6-thread-1
end: pool-6-thread-1
end: pool-6-thread-1
复制代码

代码较长,通过颜色标记法帮我们理清线程关系


在这里插入图片描述


上色后一目了然了,需要特别注意的是由于flatMap中切换了数据源的同时切换了线程,所以打印 3的线程不是s2 而是 s4


Flow


首相创建对应的Dispatcher


在这里插入图片描述


然后将代码换成Flow的写法,主要遵循下列原则



  • RxJava通过observeOn切换后续代码的线程

  • Flow通过flowOn切换前置代码的线程


在这里插入图片描述


打印结果如下:


1: pool-1-thread-1 @coroutine#6
1: pool-1-thread-1 @coroutine#6
1: pool-1-thread-1 @coroutine#6
2: pool-2-thread-2 @coroutine#5
2: pool-2-thread-2 @coroutine#5
2: pool-2-thread-2 @coroutine#5
inner 1: pool-3-thread-1 @coroutine#10
inner 1: pool-3-thread-2 @coroutine#11
inner 1: pool-3-thread-3 @coroutine#12
inner 1: pool-3-thread-2 @coroutine#11
inner 1: pool-3-thread-3 @coroutine#12
inner 2: pool-4-thread-3 @coroutine#9
inner 1: pool-3-thread-1 @coroutine#10
inner 1: pool-3-thread-3 @coroutine#12
inner 1: pool-3-thread-2 @coroutine#11
inner 2: pool-4-thread-1 @coroutine#7
inner 2: pool-4-thread-2 @coroutine#8
inner 2: pool-4-thread-1 @coroutine#7
inner 2: pool-4-thread-3 @coroutine#9
inner 1: pool-3-thread-1 @coroutine#10
3: pool-4-thread-1 @coroutine#3
inner 2: pool-4-thread-3 @coroutine#9
inner 2: pool-4-thread-2 @coroutine#8
end: pool-5-thread-1 @coroutine#2
3: pool-4-thread-1 @coroutine#3
inner 2: pool-4-thread-2 @coroutine#8
3: pool-4-thread-1 @coroutine#3
end: pool-5-thread-1 @coroutine#2
3: pool-4-thread-1 @coroutine#3
end: pool-5-thread-1 @coroutine#2
end: pool-5-thread-1 @coroutine#2
3: pool-4-thread-1 @coroutine#3
3: pool-4-thread-1 @coroutine#3
end: pool-5-thread-1 @coroutine#2
end: pool-5-thread-1 @coroutine#2
3: pool-4-thread-1 @coroutine#3
3: pool-4-thread-1 @coroutine#3
end: pool-5-thread-1 @coroutine#2
end: pool-5-thread-1 @coroutine#2
inner 2: pool-4-thread-1 @coroutine#7
3: pool-4-thread-1 @coroutine#3
end: pool-5-thread-1 @coroutine#2
复制代码

从日志可以看到,123的时序性以及inner1inner2的并发性与RxJava的一致。


4. FIN




Flow在线程切换方面可以完全取代RxJava的能力,而且将subscribeOnobserveOn两个操作符合二为一成flowOn,学习成本更低。随着flow的操作符种类日趋完善,未来在Android/Kotlin开发中可以跟RxJava说再见了👋🏻


image.png


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

收起阅读 »

用Jetpack Compose制作出可爱的天气动画

1. 背景介绍 最近参加了Compose挑战赛的终极挑战,使用Compose完成了一个天气app。之前几轮挑战我也都有参与,每次都学到不少新东西,希望在这最后一轮挑战中,活用这段时间的积累做出更加成熟的作品。 项目挑战 因为没有美工协助,所以我考虑通过代码实...
继续阅读 »

1. 背景介绍




最近参加了Compose挑战赛的终极挑战,使用Compose完成了一个天气app。之前几轮挑战我也都有参与,每次都学到不少新东西,希望在这最后一轮挑战中,活用这段时间的积累做出更加成熟的作品。


项目挑战


因为没有美工协助,所以我考虑通过代码实现app中的所有UI元素例如各种icon等,这样的UI在任何分辨率下都不会失真,而且可以更灵活地完成各种动画效果。


为了降低实现成本,我将app中的UI元素定义成偏卡通的风格,更利于代码实现:
在这里插入图片描述



上面的动画没有使用giflottie等三方资源,所有效果都基于Compose代码绘制。



MyApp:CuteWeather




App界面比较简洁,采用单页面呈现(这也是挑战赛要求),可以查看近一周的天气信息和温度走势等。


项目地址: github.com/vitaviva/co…


在这里插入图片描述


其中,卡通风格的天气动画算是这个app相对于同类应用的特色,本文将围绕这些天气动画介绍一下如何使用Compose绘制自定义图形、并基于这些图形实现动画。




2. Compose自定义绘制




像常规的Android开发一样,除了各种默认的Composable控件以外,Compose也提供了Canvas用来绘制自定义图形。


Canvas相关的API在各个平台都大同小异,但在Compose上具有以下特点:



  • 用声明式的方式创建和使用Canvas

  • 通过DrawScope提供必要的state及各种APIs

  • API更简单易用


声明式地创建和使用Canvas


Compose中,Canvas作为Composable可以声明式地添加到其他Composable中,并通过Modifier进行配置


Canvas(modifier = Modifier.fillMaxSize()){ // this: DrawScope 
//内部进行自定义绘制
}
复制代码

传统方式需要获取Canvas句柄命令式地进行绘制,而Canvas{...}通过状态驱动的方式执行block内的绘制逻辑,从而刷新UI。


强大的DrawScope


Canvas{...}通过DrawScope提供了一些当前绘制所需的state,例如经常使用到的size;DrawScope还提了各种常用的绘制API,例如drawLine


Canvas(modifier = Modifier.fillMaxSize()){
//通过size获取当前canvas的width和height
val canvasWidth = size.width
val canvasHeight = size.height

//绘制直线
drawLine(
start = Offset(x=canvasWidth, y = 0f),
end = Offset(x = 0f, y = canvasHeight),
color = Color.Blue,
strokeWidth = 5F //设置直线宽度
)
}
复制代码

上面代码绘制效果如下:


在这里插入图片描述


简单易用的API


传统的Canvas API需要进行Paint的配置,而DrawScope的API则更简单、使用更友好。


例如绘制一个圆,传统的API是这样:


public void drawCircle(float cx, float cy, float radius, @NonNull Paint paint) {
//...
}
复制代码

DrawScope提供的API:


fun drawCircle(
color: Color,
radius: Float = size.minDimension / 2.0f,
center: Offset = this.center,
alpha: Float = 1.0f,
style: DrawStyle = Fill,
colorFilter: ColorFilter? = null,
blendMode: BlendMode = DefaultBlendMode
) {...}
复制代码

虽然看起来参数变多了,但是其实已经通过size等设置了合适的默认值,同时省去了Paint的创建和配置,使用起来更方便。


使用原生Canvas


目前DrawScope提供的API还不及原生Canvas丰富(比如不支持drawText等),当不满足使用需求时,也可以直接使用原生Canvas对象进行绘制


   drawIntoCanvas { canvas ->
//nativeCanvas是原生canvas对象,android平台即android.graphics.Canvas
val nativeCanvas = canvas.nativeCanvas

}
复制代码

上面对Compose中的Canvas做了简单介绍,下面结合app中的具体示例看一下实际使用效果


首先,看一下雨水的绘制过程。




3. 雨天效果




雨天天气的关键是如何绘制不断下落的雨水


在这里插入图片描述


雨滴的绘制


我们先绘制构成雨水的基本单元:雨滴


在这里插入图片描述


经拆解后,雨水效果可由三组雨滴构成,每一组雨滴分成上下两段,这样在运动时可以形成接连不断的效果。


我们使用drawLine绘制每一段黑线,设置适当的stokeWidth,并通过cap设置端点的圆形效果:


@Composable
fun rainDrop() {

Canvas(modifier) {

val x: Float = size.width / 2 //x坐标: 1/2的位置

drawLine(
Color.Black,
Offset(x, line1y1), //line1 的起点
Offset(x, line1y2), //line1 的终点
strokeWidth = width, //设置宽度
cap = StrokeCap.Round//头部圆形
)

// line2同上
drawLine(
Color.Black,
Offset(x, line2y1),
Offset(x, line2y2),
strokeWidth = width,
cap = StrokeCap.Round
)
}
}
复制代码

雨滴下落动画


完成雨滴的基本图形绘制后,接下来为两线段增加位移动画,形成流动的效果。


在这里插入图片描述


以两线段中间空隙为动画的锚点,根据animationState变动其y轴位置,从canvas的顶端移动到低端(0 ~ size.hight),然后restart这个动画。


然后以锚点为基准绘制上下两线段,就行成接连不断的动画效果了


在这里插入图片描述


代码如下:


@Composable
fun rainDrop() {
//循环播放的动画 ( 0f ~ 1f)
val animateTween by rememberInfiniteTransition().animateFloat(
initialValue = 0f,
targetValue = 1f,
animationSpec = infiniteRepeatable(
tween(durationMillis, easing = LinearEasing),
RepeatMode.Restart //start动画
)
)

Canvas(modifier) {

// scope : 绘制区域
val width = size.width
val x: Float = size.width / 2

// width/2是strokCap的宽度,scopeHeight处预留strokCap宽度,让雨滴移出时保持正圆,提高视觉效果
val scopeHeight = size.height - width / 2

// space : 两线段的间隙
val space = size.height / 2.2f + width / 2 //间隙size
val spacePos = scopeHeight * animateTween //锚点位置随animationState变化
val sy1 = spacePos - space / 2
val sy2 = spacePos + space / 2

// line length
val lineHeight = scopeHeight - space

// line1
val line1y1 = max(0f, sy1 - lineHeight)
val line1y2 = max(line1y1, sy1)

// line2
val line2y1 = min(sy2, scopeHeight)
val line2y2 = min(line2y1 + lineHeight, scopeHeight)

// draw
drawLine(
Color.Black,
Offset(x, line1y1),
Offset(x, line1y2),
strokeWidth = width,
colorFilter = ColorFilter.tint(
Color.Black
),
cap = StrokeCap.Round
)

drawLine(
Color.Black,
Offset(x, line2y1),
Offset(x, line2y2),
strokeWidth = width,
colorFilter = ColorFilter.tint(
Color.Black
),
cap = StrokeCap.Round
)
}
}

复制代码

Compose自定义布局


完成了单个雨滴的动画,接下来我们使用三个雨滴组成雨水的效果。


首先可以使用Row+Space的方式进行组装,但是这种方式缺少灵活性,仅通过Modifier很难准确布局三雨滴的相对位,因此考虑借助Compose的自定义布局,以提高灵活性和准确性:


Layout(
modifier = modifier.rotate(30f), //雨滴旋转角度
content = { // 定义子Composable
Raindrop(modifier.fillMaxSize())
Raindrop(modifier.fillMaxSize())
Raindrop(modifier.fillMaxSize())
}
) { measurables, constraints ->
// List of measured children
val placeables = measurables.mapIndexed { index, measurable ->
// Measure each children
val height = when (index) { //让三个雨滴的height不同,增加错落感
0 -> constraints.maxHeight * 0.8f
1 -> constraints.maxHeight * 0.9f
2 -> constraints.maxHeight * 0.6f
else -> 0f
}
measurable.measure(
constraints.copy(
minWidth = 0,
minHeight = 0,
maxWidth = constraints.maxWidth / 10, // raindrop width
maxHeight = height.toInt(),
)
)
}

// Set the size of the layout as big as it can
layout(constraints.maxWidth, constraints.maxHeight) {
var xPosition = constraints.maxWidth / ((placeables.size + 1) * 2)

// Place children in the parent layout
placeables.forEachIndexed { index, placeable ->
// Position item on the screen
placeable.place(x = xPosition, y = 0)

// Record the y co-ord placed up to
xPosition += (constraints.maxWidth / ((placeables.size + 1) * 0.8f)).roundToInt()
}
}
}
复制代码

Compose中可以使用Layout{...}对Composable进行自定义布局,content{...}中定义参与布局的子Composable。


跟传统Android视图一样,自定义布局需要先后经历measurelayout两步。



  • measruemeasurables返回所有待测量的子Composable,constraints类似于MeasureSpec,封装父容器对子元素的布局约束。measurable.measure()中对子元素进行测量

  • layoutplaceables返回测量后的子元素,依次调用placeable.place()对雨滴进行布局,通过xPosition预留雨滴在x轴的间隔


经过layout之后,通过 modifier.rotate(30f) 对Composable进行旋转,完成最终效果:


在这里插入图片描述




4. 雪天效果




雪天效果的关键在于雪花的飘落。


在这里插入图片描述


雪花的绘制


雪花的绘制非常简单,用一个圆圈代表一个雪花


Canvas(modifier) {

val radius = size / 2

drawCircle( //白色填充
color = Color.White,
radius = radius,
style = FILL
)

drawCircle(// 黑色边框
color = Color.Black,
radius = radius,
style = Stroke(width = radius * 0.5f)
)
}
复制代码

雪花飘落动画


雪花飘落的过程相对于雨滴坠落要复杂一些,由三个动画组成:



  1. 下落:改变y轴坐标:0f ~ 2.5f

  2. 左右飘移:改变x轴的offset:-1f ~ 1f

  3. 逐渐消失:改变alpha:1f ~ 0f


借助InfiniteTransition同步控制多个动画,代码如下:


@Composable
private fun Snowdrop(
modifier: Modifier = Modifier,
durationMillis: Int = 1000 // 雪花飘落动画的druation
) {

//循环播放的Transition
val transition = rememberInfiniteTransition()

//1. 下降动画:restart动画
val animateY by transition.animateFloat(
initialValue = 0f,
targetValue = 2.5f,
animationSpec = infiniteRepeatable(
tween(durationMillis, easing = LinearEasing),
RepeatMode.Restart
)
)

//2. 左右飘移:reverse动画
val animateX by transition.animateFloat(
initialValue = -1f,
targetValue = 1f,
animationSpec = infiniteRepeatable(
tween(durationMillis / 3, easing = LinearEasing),
RepeatMode.Reverse
)
)

//3. alpha值:restart动画,以0f结束
val animateAlpha by transition.animateFloat(
initialValue = 1f,
targetValue = 0f,
animationSpec = infiniteRepeatable(
tween(durationMillis, easing = FastOutSlowInEasing),
)
)

Canvas(modifier) {

val radius = size.width / 2

// 圆心位置随AnimationState改变,实现雪花飘落的效果
val _center = center.copy(
x = center.x + center.x * animateX,
y = center.y + center.y * animateY
)

drawCircle(
color = Color.White.copy(alpha = animateAlpha),//alpha值的变化实现雪花消失效果
center = _center,
radius = radius,
)

drawCircle(
color = Color.Black.copy(alpha = animateAlpha),
center = _center,
radius = radius,
style = Stroke(width = radius * 0.5f)
)
}
}
复制代码

animateYtargetValue设为2.5f是为了让雪花的运动轨迹更长,看起来更加真实


雪花的自定义布局


像雨滴一样,对雪花也使用Layout自定义布局


@Composable
fun Snow(
modifier: Modifier = Modifier,
animate: Boolean = false,
) {

Layout(
modifier = modifier,
content = {
//摆放三个雪花,分别设置不同duration,增加随机性
Snowdrop( modifier.fillMaxSize(), 2200)
Snowdrop( modifier.fillMaxSize(), 1600)
Snowdrop( modifier.fillMaxSize(), 1800)
}
) { measurables, constraints ->
val placeables = measurables.mapIndexed { index, measurable ->
val height = when (index) {
// 雪花的height不同,也是为了增加随机性
0 -> constraints.maxHeight * 0.6f
1 -> constraints.maxHeight * 1.0f
2 -> constraints.maxHeight * 0.7f
else -> 0f
}
measurable.measure(
constraints.copy(
minWidth = 0,
minHeight = 0,
maxWidth = constraints.maxWidth / 5, // snowdrop width
maxHeight = height.roundToInt(),
)
)
}

layout(constraints.maxWidth, constraints.maxHeight) {
var xPosition = constraints.maxWidth / ((placeables.size + 1))

placeables.forEachIndexed { index, placeable ->
placeable.place(x = xPosition, y = -(constraints.maxHeight * 0.2).roundToInt())

xPosition += (constraints.maxWidth / ((placeables.size + 1) * 0.9f)).roundToInt()
}
}
}
}
复制代码

最终效果如下:


在这里插入图片描述




5. 晴天效果




通过一个旋转的太阳代表晴天效果


在这里插入图片描述


太阳的绘制


太阳的图形由中心圆形和围绕圆环的等分线段组成。


@Composable
fun Sun(modifier: Modifier = Modifier) {

Canvas(modifier) {

val radius = size.width / 6
val stroke = size.width / 20

// draw circle
drawCircle(
color = Color.Black,
radius = radius + stroke / 2,
style = Stroke(width = stroke),
)
drawCircle(
color = Color.White,
radius = radius,
style = Fill,
)

// draw line

val lineLength = radius * 0.2f
val lineOffset = radius * 1.8f
(0..7).forEach { i ->

val radians = Math.toRadians(i * 45.0)

val offsetX = lineOffset * cos(radians).toFloat()
val offsetY = lineOffset * sin(radians).toFloat()

val x1 = size.width / 2 + offsetX
val x2 = x1 + lineLength * cos(radians).toFloat()

val y1 = size.height / 2 + offsetY
val y2 = y1 + lineLength * sin(radians).toFloat()

drawLine(
color = Color.Black,
start = Offset(x1, y1),
end = Offset(x2, y2),
strokeWidth = stroke,
cap = StrokeCap.Round
)
}
}
}
复制代码

均分360度,每间隔45度画一条线段,cos计算x轴坐标,sin计算y轴坐标。


太阳的旋转


太阳的旋转动画很简单,通过Modifier.rotate不断转动Canvas即可。


@Composable
fun Sun(modifier: Modifier = Modifier) {

//循环动画
val animateTween by rememberInfiniteTransition().animateFloat(
initialValue = 0f,
targetValue = 360f,
animationSpec = infiniteRepeatable(tween(5000), RepeatMode.Restart)
)

Canvas(modifier.rotate(animateTween)) {// 旋转动画

val radius = size.width / 6
val stroke = size.width / 20
val centerOffset = Offset(size.width / 30, size.width / 30) //圆心偏移量

// draw circle
drawCircle(
color = Color.Black,
radius = radius + stroke / 2,
style = Stroke(width = stroke),
center = center + centerOffset //圆心偏移
)

//...略
}
}
复制代码

此外,DrawScope提供了rotate的API,也可以实现旋转效果。


最后我们给太阳的圆心增加一个偏移量,让转动更加活泼:


在这里插入图片描述




6. 动画的组合、切换




在实现了RainSnowSun等图形后,就可以使用这些图形组合成各种天气效果了。


将图形组合成天气


Compose的声明式语法非常有利于UI的组合:


比如,多云转阵雨,我们摆放SunCloudRain等元素后,通过Modifier调整各自位置即可:


@Composable
fun CloudyRain(modifier: Modifier) {
Box(modifier.size(200.dp)){
Sun(Modifier.size(120.dp).offset(140.dp, 40.dp))
Rain(Modifier.size(80.dp).offset(80.dp, 60.dp))
Cloud(Modifier.align(Aligment.Center))
}
}
复制代码

让动画切换更加自然


在这里插入图片描述


当在多个天气动画之间进行切换时,我们希望能实现更自然的过渡。实现思路是将组成天气动画的各元素的Modifier配置变量化,然后通过Animation不断改变


假设所有的天气都由CloudSunRain组成,无非就是offsetsizealpha值的不同:


ComposeInfo


data class IconInfo(
val size: Float = 1f,
val offset: Offset = Offset(0f, 0f),
val alpha: Float = 1f,
)
复制代码

//天气组合信息,即Sun、Cloud、Rain的位置信息
data class ComposeInfo(
val sun: IconInfo,
val cloud: IconInfo,
val rains: IconInfo,

) {
operator fun times(float: Float): ComposeInfo =
copy(
sun = sun * float,
cloud = cloud * float,
rains = rains * float
)

operator fun minus(composeInfo: ComposeInfo): ComposeInfo =
copy(
sun = sun - composeInfo.sun,
cloud = cloud - composeInfo.cloud,
rains = rains - composeInfo.rains,
)

operator fun plus(composeInfo: ComposeInfo): ComposeInfo =
copy(
sun = sun + composeInfo.sun,
cloud = cloud + composeInfo.cloud,
rains = rains + composeInfo.rains,
)
}

复制代码

如上,ComposeInfo中持有各种元素的位置信息,运算符重载用于跟随Animation计算当前最新值。


定义不同天气的ComposeInfo如下:


//晴天
val SunnyComposeInfo = ComposeInfo(
sun = IconInfo(1f),
cloud = IconInfo(0.8f, Offset(-0.1f, 0.1f), 0f),
rains = IconInfo(0.4f, Offset(0.225f, 0.3f), 0f),
)

//多云
val CloudyComposeInfo = ComposeInfo(
sun = IconInfo(0.1f, Offset(0.75f, 0.2f), alpha = 0f),
cloud = IconInfo(0.8f, Offset(0.1f, 0.1f)),
rains = IconInfo(0.4f, Offset(0.225f, 0.3f), alpha = 0f),
)

//雨天
val RainComposeInfo = ComposeInfo(
sun = IconInfo(0.1f, Offset(0.75f, 0.2f), alpha = 0f),
cloud = IconInfo(0.8f, Offset(0.1f, 0.1f)),
rains = IconInfo(0.4f, Offset(0.225f, 0.3f), alpha = 1f),
)

复制代码

ComposedIcon


接着,定义ComposedIcon,消费ComposeInfo绘制天气组合的UI


@Composable
fun ComposedIcon(modifier: Modifier = Modifier, composeInfo: ComposeInfo) {

//各元素的ComposeInfo
val (sun, cloud, rains) = composeInfo

Box(modifier) {

//应用ComposeInfo到Modifier
val _modifier = remember(Unit) {
{ icon: IconInfo ->
Modifier
.offset( icon.size * icon.offset.x, icon.size * icon.offset.y )
.size(icon.size)
.alpha(icon.alpha)
}
}

Sun(_modifier(sun))
Rains(_modifier(rains))
AnimatableCloud(_modifier(cloud))
}
}
复制代码

ComposedWeather


最后,定义ComposedWeather,通过动画更新当前的ComposedIcon


@Composable
fun ComposedWeather(modifier: Modifier, composedIcon: ComposedIcon) {

val (cur, setCur) = remember { mutableStateOf(composedIcon) }
var trigger by remember { mutableStateOf(0f) }

DisposableEffect(composedIcon) {
trigger = 1f
onDispose { }
}

//创建动画(0f ~ 1f),用于更新ComposeInfo
val animateFloat by animateFloatAsState(
targetValue = trigger,
animationSpec = tween(1000)
) {
//当动画结束时,更新ComposeWeather到最新state
setCur(composedIcon)
trigger = 0f
}

//根据AnimationState计算当前ComposeInfo
val composeInfo = remember(animateFloat) {
cur.composedIcon + (weatherIcon.composedIcon - cur.composedIcon) * animateFloat
}

//使用最新的ComposeInfo显示Icon
ComposedIcon(
modifier,
composeInfo
)
}
复制代码

到此,我们就实现了天气动画的自然过度了。



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

为Fragment换装ViewPager2

1. 开启ViewPager2之旅距离ViewPager2正式版的发布已经一年多了,目前ViewPager早已停止更新,官方鼓励使用ViewPager2替代。 ViewPager2底层基于RecyclerView实现,因此可以获得RecyclerView带来的...
继续阅读 »

为Fragment换装ViewPager2

1. 开启ViewPager2之旅

image.png

距离ViewPager2正式版的发布已经一年多了,目前ViewPager早已停止更新,官方鼓励使用ViewPager2替代。 ViewPager2底层基于RecyclerView实现,因此可以获得RecyclerView带来的诸多收益:

  • 抛弃传统的PagerAdapter,统一了Adapter的API
  • 通过LinearLayoutManager可以实现类似抖音的纵向滑动
  • 支持DiffUitl,可以实现局部刷新
  • 支持RTL(right-to-left),对于一些有出海需求的APP非常有用
  • 支持ItemDecorator

2. ViewPager2 + Fragment

跟ViewPager一样,除了View以外,ViewPager2更多的是配合Fragment使用,这需要借助于FragmentStateAdapter

image.png

接下来,本文简单介绍一下FragmentStateAdapter的使用及实现原理:

首先在gradle中引入ViewPager2:

 implementation 'androidx.viewpager2:viewpager2:1.1.0'
复制代码

然后在xml中布局:

<androidx.viewpager2.widget.ViewPager2
android:id="@+id/doppelgangerViewPager"
android:layout_width="match_parent"
android:layout_height="match_parent" />

复制代码

FragmentStateAdapter

import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
import androidx.viewpager2.adapter.FragmentStateAdapter

class DoppelgangerAdapter(activity: AppCompatActivity, val doppelgangerList: List<DoppelgangerItem>) :
FragmentStateAdapter(activity) {

override fun getItemCount(): Int {
return doppelgangerList.size
}

override fun createFragment(position: Int): Fragment {
return DoppelgangerFragment.getInstance(doppelgangerList[position])
}
}
复制代码

FragmentStateAdapter的API跟旧的Adapter很相似:

  • getItemCount:返回Item的数量
  • createFragment:用来根据position创建fragment
  • DoppelgangerFragment:创建的具体Fragment类型

MainActivity

在Activity中为ViewPager2设置Adapter:

val doppelgangerAdapter = DoppelgangerAdapter(this, doppelgangerList) 
doppelgangerViewPager.adapter = doppelgangerAdapter
复制代码

在这里插入图片描述


3. 揭秘FragmentStateAdapter的实现

因为ViewPager2继承自RecyclerView,因此可以推断出FragmentStateAdapter继承自RecyclerView.Adapter

public abstract class FragmentStateAdapter extends 
RecyclerView.Adapter<FragmentViewHolder> implements StatefulAdapter {
}
复制代码

虽说是继承关系,但两者的API却不一致,RecyclerView.Adapter关注的是ViewHolder的复用,而在FragmentStateAdapter中Framgent是不会复用的,即有多少个item就应该创建多少个Fragment,那么这其中是如何转换的呢?

onCreateViewHolder

通过FragmentStateAdapter声明中的泛型可以知道,ViewPager2之所以能够在RecyclerView的基础上对外屏蔽对ViewHolder的使用,其内部是借助FragmentViewHolder实现的。

onCreateViewHolder中会创建一个FragmentViewHolder

@NonNull
@Override
public final FragmentViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
return FragmentViewHolder.create(parent);
}
复制代码

FragmentViewHolder的主要作用是通过FrameLayout为Fragment提供用作容器的container:

@NonNull static FragmentViewHolder create(@NonNull ViewGroup parent) {
FrameLayout container = new FrameLayout(parent.getContext());
container.setLayoutParams(
new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT));
container.setId(ViewCompat.generateViewId());
container.setSaveEnabled(false);
return new FragmentViewHolder(container);
}
复制代码

onBindViewHolder

@Override
public final void onBindViewHolder(final @NonNull FragmentViewHolder holder, int position) {
...
ensureFragment(position);
...
gcFragments();
}
复制代码

ensureFragment(position),其内部会最终回调用createFragment创建当前Fragment

   private void ensureFragment(int position) {
long itemId = getItemId(position);
if (!mFragments.containsKey(itemId)) {
// TODO(133419201): check if a Fragment provided here is a new Fragment
Fragment newFragment = createFragment(position);
newFragment.setInitialSavedState(mSavedStates.get(itemId));
mFragments.put(itemId, newFragment);
}
}
复制代码

mFragments缓存创建的Fragment,供后面placeFramentInViewholder使用; gcFragments回收已经不再使用的的Fragment(对应的item已经删除),节省内存开销。

placeFragmentInViewHolder

  @Override
public final void onViewAttachedToWindow(@NonNull final FragmentViewHolder holder) {
placeFragmentInViewHolder(holder);
gcFragments();
}
复制代码

onViewAttachToWindow的时候调用placeFragmentInViewHolder,将FragmentViewHolder的container与当前Fragment绑定

    void placeFragmentInViewHolder(@NonNull final FragmentViewHolder holder) {
Fragment fragment = mFragments.get(holder.getItemId());
if (fragment == null) {
throw new IllegalStateException("Design assumption violated.");
}
FrameLayout container = holder.getContainer();
View view = fragment.getView();

...
if (fragment.isAdded() && view.getParent() != null) {
if (view.getParent() != container) {
addViewToContainer(view, container);
}
return;
}
...
}
复制代码

void addViewToContainer(@NonNull View v, @NonNull FrameLayout container) {
...

if (container.getChildCount() > 0) {
container.removeAllViews();
}

if (v.getParent() != null) {
((ViewGroup) v.getParent()).removeView(v);
}

container.addView(v);
}
复制代码

通过上面源码分析可以知道,虽然Fragment没有被复用,但是通过复用了ViewHolder的container实现了Framgent的交替显示


4. 滑动监听

监听页面滑动是一个常见需求,ViewPager2的API也发生了变化,使用OnPageChangeCallback

image.png

使用效果如下:

var doppelgangerPageChangeCallback = object : ViewPager2.OnPageChangeCallback() {
override fun onPageSelected(position: Int) {
Toast.makeText(this@MainActivity, "Selected position: ${position}",
Toast.LENGTH_SHORT).show()
}
}
复制代码

OnPageChangeCallback同样也有三个方法:

  • onPageScrolled: 当前页面开始滑动时
  • onPageSelected: 当页面被选中时
  • onPageScrollStateChanged: 当前页面滑动状态变动时

在这里插入图片描述


5. 纵向滑动

设置纵向滑动很简单,一行代码搞定

doppelgangerViewPager.orientation = ViewPager2.ORIENTATION_VERTICAL
复制代码

在这里插入图片描述

源码也很简单

/**
* Sets the orientation of the ViewPager2.
*
* @param orientation {@link #ORIENTATION_HORIZONTAL} or {@link #ORIENTATION_VERTICAL}
*/

public void setOrientation(@Orientation int orientation) {
mLayoutManager.setOrientation(orientation);
mAccessibilityProvider.onSetOrientation();
}
复制代码

6. TabLayout


配合TabLayout的使用也是一个常见需求,TabLayout需要引入material

implementation 'com.google.android.material:material:1.2.0-alpha04'
复制代码

然后在xml中声明

<com.google.android.material.tabs.TabLayout
android:id="@+id/tabLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/colorPrimary"
app:tabMode="scrollable"
app:tabTextColor="@android:color/white" />

复制代码

TabsLayoutMediator

要关联TabLayout和ViewPager2需要借助TabLayoutMediator

public TabLayoutMediator(
@NonNull TabLayout tabLayout,
@NonNull ViewPager2 viewPager,
@NonNull TabConfigurationStrategy tabConfigurationStrategy)
{
this(tabLayout, viewPager, true, tabConfigurationStrategy);
}
复制代码

其中,TabConfigurationStrategy定义如下:根据position配置当前tab

/**
* A callback interface that must be implemented to set the text and styling of newly created
* tabs.
*/

public interface TabConfigurationStrategy {
/**
* Called to configure the tab for the page at the specified position. Typically calls {@link
* TabLayout.Tab#setText(CharSequence)}, but any form of styling can be applied.
*
* @param tab The Tab which should be configured to represent the title of the item at the given
* position in the data set.
* @param position The position of the item within the adapter's data set.
*/

void onConfigureTab(@NonNull TabLayout.Tab tab, int position);
}
复制代码

在MainActivity中具体使用如下:

TabLayoutMediator(tabLayout, doppelgangerViewPager) { tab, position ->
//To get the first name of doppelganger celebrities
tab.text = doppelgangerList[position].title
}.attach()
复制代码

attach方法很关键,经过前面一系列配置后最终需要通过它关联两个组件。

加入TabLayout后的最终效果如下:

在这里插入图片描述


7. DiffUtil 局部更新

RecyclerView基于DiffUtil可以实现局部更新,如今,FragmentStateAdapter也可以对Fragment实现局部更新。

首先定义DiffUtil.Callback

class PagerDiffUtil(private val oldList: List<DoppelgangerItem>, private val newList: List<DoppelgangerItem>) : DiffUtil.Callback() {

enum class PayloadKey {
VALUE
}

override fun getOldListSize() = oldList.size

override fun getNewListSize() = newList.size

override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
return oldList[oldItemPosition].id == newList[newItemPosition].id
}

override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
return oldList[oldItemPosition].value == newList[newItemPosition].value
}

override fun getChangePayload(oldItemPosition: Int, newItemPosition: Int): Any? {
return listOf(PayloadKey.VALUE)
}
}
复制代码

然后在Adapter中使用DiffUtil更新数据


class DoppelgangerAdapter(private val activity: FragmentActivity) : FragmentStateAdapter(activity) {

private val items: ArrayList<DoppelgangerItem> = arrayListOf()


override fun createFragment(position: Int): Fragment {
return DoppelgangerFragment.getInstance(doppelgangerList[position])
}

override fun getItemCount() = items.size

override fun getItemId(position: Int): Long {
return items[position].id.toLong()
}

override fun containsItem(itemId: Long): Boolean {
return items.any { it.id.toLong() == itemId }
}

fun setItems(newItems: List<PagerItem>) {
//不借助DiffUtil更新数据
//items.clear()
//items.addAll(newItems)
//notifyDataSetChanged()

//使用DiffUtil更新数据
val callback = PagerDiffUtil(items, newItems)
val diff = DiffUtil.calculateDiff(callback)
items.clear()
items.addAll(newItems)
diff.dispatchUpdatesTo(this)
}
}
复制代码

8. 总结

本文主要介绍了ViewPager2配合Fragment的使用方法以及FragmentStateAdapter的实现原理,顺带介绍了TabLayoutOnPageChangeCallbackDiffUtil等常见功能的用法。ViewPager2的使用非常简单,在性能以及使用体验等各方面都要优于传统的ViewPager,没尝试的小伙伴抓紧用起来吧~

收起阅读 »

一道面试题:ViewModel为什么横竖屏切换时不销毁?

又到金三银四 往年面试中有关Jetpack的考察可以算是加分项,随着官方对Modern Android development (MAD) 的大力推广,今年基本上都是必选题了。 很多候选人对Jetpack各组件的功能及用法如数家珍,但一问及到原理往往卡壳。原理...
继续阅读 »

又到金三银四


往年面试中有关Jetpack的考察可以算是加分项,随着官方对Modern Android development (MAD) 的大力推广,今年基本上都是必选题了。


很多候选人对Jetpack各组件的功能及用法如数家珍,但一问及到原理往往卡壳。原理不清虽不影响API的使用,但也正因为如此,如果能对源码有一定了解,也许可以脱颖而出得到加分。


本文分享一个入门级的源码分析,也是在面试中经常被问到的问题




# ViewModel


ViewModel是Android Jetpack中的重要组件,其优势是具有下图这样的生命周期、不会因为屏幕旋转等Activity配置变化而销毁,是实现MVVM架构中UI状态管理的重要基础。
在这里插入图片描述


class MyActivity : AppCompatActivity {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Log.d(TAG, "onCreate")

val activity: FragmentActivity = this
val factory: ViewModelProvider.Factory = ViewModelProvider.NewInstanceFactory()

// Activity由于横竖品切换销毁重建,此处的viewModel 仍然是重建前的实例
val viewModel = ViewModelProvider(activity, factory).get(MyViewModel::class.java)
// 如果直接new实例则会创建新的ViewModel实例
// val viewModel = MyViewModel()

Log.d(TAG, " - Activity :${this.hashCode()}")
Log.d(TAG, " - ViewModel:${viewModel.hashCode()}")
}
}
复制代码

上面代码在横竖屏切换时的log如下:


#Activity初次启动
onCreate
- Activity :132818886
- ViewModel:249530701
onStart
onResume

#屏幕旋转
onPause
onStop
onRetainNonConfigurationInstance
onDestroy
onCreate
- Activity :103312713 #Activity实例不同
- ViewModel:249530701 #ViewModel实例相同
onStart
onResume
复制代码

下面代码是保证屏幕切换时ViewModel不销毁的关键,我们依次为入口看一下源码


val viewModel = ViewModelProvider(activity, factory).get(MyViewModel::class.java)
复制代码



# ViewModelProvider


ViewModelProvider源码很简单,分别持有一个ViewModelProvider.FactoryViewModelStore实例


package androidx.lifecycle;

public class ViewModelProvider {

public interface Factory {
@NonNull
<T extends ViewModel> T create(@NonNull Class<T> modelClass);
}

private final Factory mFactory;
private final ViewModelStore mViewModelStore;

public ViewModelProvider(@NonNull ViewModelStoreOwner owner, @NonNull Factory factory) {
this(owner.getViewModelStore(), factory);
}

public ViewModelProvider(@NonNull ViewModelStore store, @NonNull Factory factory) {
mFactory = factory;
this.mViewModelStore = store;
}

...
}
复制代码

get()返回ViewModel实例


package androidx.lifecycle;

public class ViewModelProvider {
...

public <T extends ViewModel> T get(@NonNull Class<T> modelClass) {
String canonicalName = modelClass.getCanonicalName();

...

return get(DEFAULT_KEY + ":" + canonicalName, modelClass);
}

public <T extends ViewModel> T get(@NonNull String key, @NonNull Class<T> modelClass) {
ViewModel viewModel = mViewModelStore.get(key);

if (modelClass.isInstance(viewModel)) {
//noinspection unchecked
return (T) viewModel;
} else {
//noinspection StatementWithEmptyBody
if (viewModel != null) {
// TODO: log a warning.
}
}

viewModel = mFactory.create(modelClass);
mViewModelStore.put(key, viewModel);
//noinspection unchecked
return (T) viewModel;
}

...
}
复制代码

逻辑非常清晰:



  1. ViewModelProvider通过ViewModelStore获取ViewModel

  2. 若获取失败,则通过ViewModelProvider.Factory创建ViewModel




# ViewModelStore


package androidx.lifecycle;

public class ViewModelStore {

private final HashMap<String, ViewModel> mMap = new HashMap<>();

final void put(String key, ViewModel viewModel) {
ViewModel oldViewModel = mMap.put(key, viewModel);
if (oldViewModel != null) {
oldViewModel.onCleared();
}
}

final ViewModel get(String key) {
return mMap.get(key);
}

public final void clear() {
for (ViewModel vm : mMap.values()) {
vm.onCleared();
}
mMap.clear();
}
}
复制代码

可见,ViewModelStore就是一个对Map的封装。


val viewModel = ViewModelProvider(activity, factory).get(FooViewModel::class.java)
复制代码

上面代码ViewModelProvider()构造参数1中传入的FragmentActivity(基类是ComponentActivity)实际上是ViewModelStoreOwner的一个实现。


package androidx.lifecycle;

public interface ViewModelStoreOwner {
@NonNull
ViewModelStore getViewModelStore();
}
复制代码

ViewModelProvider中的ViewModelStore正是来自ViewModelStoreOwner。


public class ViewModelProvider {

private final ViewModelStore mViewModelStore;

public ViewModelProvider(@NonNull ViewModelStoreOwner owner, @NonNull Factory factory) {
this(owner.getViewModelStore(), factory);
}

public ViewModelProvider(@NonNull ViewModelStore store, @NonNull Factory factory) {
this.mViewModelStore = store;
}
复制代码

Activity在onDestroy会尝试对ViewModelStore清空。如果是由于ConfigurationChanged带来的Destroy则不进行清空,避免横竖屏切换等造成ViewModel销毁。


//ComponentActivity.java
getLifecycle().addObserver(new LifecycleEventObserver() {
@Override
public void onStateChanged(@NonNull LifecycleOwner source,
@NonNull Lifecycle.Event event) {
if (event == Lifecycle.Event.ON_DESTROY) {
// Clear out the available context
mContextAwareHelper.clearAvailableContext();
// And clear the ViewModelStore
if (!getLifecycle().addObserver(new LifecycleEventObserver() {
@Override
public void onStateChanged(@NonNull LifecycleOwner source,
@NonNull Lifecycle.Event event) {
if (event == Lifecycle.Event.ON_DESTROY) {
// Clear out the available context
mContextAwareHelper.clearAvailableContext();
// And clear the ViewModelStore
if (!isChangingConfigurations()) {
getViewModelStore().clear();
}
}
}
});()) {
getViewModelStore().clear();
}
}
}
});
复制代码



# FragmentActivity#getViewModelStore()


FragmentActivity实现了ViewModelStoreOwnergetViewModelStore方法


package androidx.fragment.app;

public class FragmentActivity extends ComponentActivity implements ViewModelStoreOwner ... {

private ViewModelStore mViewModelStore;

@NonNull
@Override
public ViewModelStore getViewModelStore() {
...

if (mViewModelStore == null) {
NonConfigurationInstances nc =
(NonConfigurationInstances) getLastNonConfigurationInstance();
if (nc != null) {
// Restore the ViewModelStore from NonConfigurationInstances
mViewModelStore = nc.viewModelStore;
}
if (mViewModelStore == null) {
mViewModelStore = new ViewModelStore();
}
}
return mViewModelStore;
}

static final class NonConfigurationInstances {
Object custom;
ViewModelStore viewModelStore;
FragmentManagerNonConfig fragments;
}

...
}
复制代码

通过getLastNonConfigurationInstance() 获取 NonConfigurationInstances 实例,从而得到真正的viewModelStoregetLastNonConfigurationInstance()又是什么?


# Activity#getLastNonConfigurationInstance()


package android.app;

public class Activity extends ContextThemeWrapper implements ... {

/* package */ NonConfigurationInstances mLastNonConfigurationInstances;

@Nullable
public Object getLastNonConfigurationInstance() {
return mLastNonConfigurationInstances != null
? mLastNonConfigurationInstances.activity : null;
}
复制代码


Retrieve the non-configuration instance data that was previously returned by onRetainNonConfigurationInstance(). This will be available from the initial onCreate(Bundle) and onStart() calls to the new instance, allowing you to extract any useful dynamic state from the previous instance.



通过官方文档我们知道,屏幕旋转前通过onRetainNonConfigurationInstance()返回的Activity实例,屏幕旋转后可以通过getLastNonConfigurationInstance()获取,因此屏幕旋转前后不销毁的关键就在onRetainNonConfigurationInstance




# Activity#onRetainNonConfigurationInstance()


#Activity初次启动
onCreate
- Activity :132818886
- ViewModel:249530701
onStart
onResume

#屏幕旋转
onPause
onStop
onRetainNonConfigurationInstance
onDestroy
onCreate
- Activity :103312713 #Activity实例不同
- ViewModel:249530701 #ViewModel实例相同
onStart
onResume
复制代码

屏幕旋转时,onRetainNonConfigurationInstance()onStoponDestroy之间调用


package android.app;

public class Activity extends ContextThemeWrapper implements ... {

public Object onRetainNonConfigurationInstance() {
return null;
}

...
}
复制代码

onRetainNonConfigurationInstance在Activity中只有空实现,在FragmentActivity中被重写


package androidx.fragment.app;

public class FragmentActivity extends ComponentActivity implements ViewModelStoreOwner, ... {

@Override
public final Object onRetainNonConfigurationInstance() {
Object custom = onRetainCustomNonConfigurationInstance();

FragmentManagerNonConfig fragments = mFragments.retainNestedNonConfig();

if (fragments == null && mViewModelStore == null && custom == null) {
return null;
}

NonConfigurationInstances nci = new NonConfigurationInstances();
nci.custom = custom;
nci.viewModelStore = mViewModelStore;
nci.fragments = fragments;
return nci;
}

static final class NonConfigurationInstances {
Object custom;
ViewModelStore viewModelStore;
FragmentManagerNonConfig fragments;
}

...
}
复制代码

FragmentActivity 通过 onRetainNonConfigurationInstance() 返回 了存放ViewModelStore的NonConfigurationInstances 实例。
值得一提的是onRetainNonConfigurationInstance提供了一个hook时机:onRetainCustomNonConfigurationInstance,允许我们像ViewModel一样使得自定义对象不被销毁


NonConfigurationInstances会在attach中由系统传递给新重建的Activity:


final void attach(Context context, ActivityThread aThread,
Instrumentation instr, IBinder token, int ident,
Application application, Intent intent, ActivityInfo info,
CharSequence title, Activity parent, String id,
NonConfigurationInstances lastNonConfigurationInstances,
Configuration config, String referrer, IVoiceInteractor voiceInteractor,
Window window, ActivityConfigCallback activityConfigCallback, IBinder assistToken)

复制代码

然后在onCreate中,通过getLastNonConfigurationInstance()获取NonConfigurationInstances中的ViewModelStore


package androidx.fragment.app;

public class FragmentActivity extends ComponentActivity implements ViewModelStoreOwner ... {

private ViewModelStore mViewModelStore;

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

super.onCreate(savedInstanceState);

NonConfigurationInstances nc =
(NonConfigurationInstances) getLastNonConfigurationInstance();
if (nc != null && nc.viewModelStore != null && mViewModelStore == null) {
mViewModelStore = nc.viewModelStore;
}
...
}
}
复制代码



# 总结


Activity首次启动



  • FragmentActivity#onCreate()被调用

    • 此时 FragmentActivity 的 mViewModelStore 尚为 null



  • HogeActivity的onCreate() 被调用

    • ViewModelProvider 实例创建

    • FragmentActivity#getViewModelStore() 被调用,mViewModelStore被创建并赋值




发生屏幕旋转



  • FragmentActivity#onRetainNonConfigurationInstance() 被调用

    • 持有mViewModelStore 的NonConfigurationInstances 实例被返回




Activity重建



  • FragmentActivity#onCreate() 被调用

    • 从Activity#getLastNonConfigurationInstance() 获取 NonConfigurationInstances 实例

    • NonConfigurationInstances 中保存了屏幕旋转前的 FragmentActivity 的 mViewModelStore,将其赋值给重建后的FragmentActivity 的 mViewModelStore



  • HogeActivity#onCreate() 被调用

    • 通过ViewModelProvider#get() 获取 ViewModel 实例




收起阅读 »

iOS内存管理-深入解析自动释放池

主要内容:AutoreleasePool简介AutoreleasePool底层原理Autorelease与NSThread、NSRunLoop的关系AutoreleasePool在主线程上的释放时机AutoreleasePool在子线程上的释放时机Autore...
继续阅读 »

主要内容:

  • AutoreleasePool简介
  • AutoreleasePool底层原理
  • Autorelease与NSThread、NSRunLoop的关系
  • AutoreleasePool在主线程上的释放时机
  • AutoreleasePool在子线程上的释放时机
  • AutoreleasePool需要手动添加的情况
  • 一、Autorelease简介

    iOS开发中的Autorelease机制是为了延时释放对象。自动释放的概念看上去很像ARC,但实际上这更类似于C语言中自动变量的特性。

    自动变量:在超出变量作用域后将被废弃;
    自动释放池:在超出释放池生命周期后,向其管理的对象实例的发送release消息。

    1.1 MRC下使用自动释放池
    在MRC环境中使用自动释放池需要用到NSAutoreleasePool对象,其生命周期就相当于C语言变量的作用域。对于所有调用过autorelease方法的对象,在废弃NSAutoreleasePool对象时,都将调用release实例方法。用源代码表示如下:
    //MRC环境下的测试:
    //第一步:生成并持有释放池NSAutoreleasePool对象;
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];

    //第二步:调用对象的autorelease实例方法;
    id obj = [[NSObject alloc] init];
    [obj autorelease];

    //第三步:废弃NSAutoreleasePool对象;
    [pool drain]; //向pool管理的所有对象发送消息,相当于[obj release]

    //obi已经释放,再次调用会崩溃(Thread 1: EXC_BAD_ACCESS (code=EXC_I386_GPFLT))
    NSLog(@"打印obj:%@", obj);

    理解NSAutoreleasePool对象的生命周期,如下图所示:


    1.2 ARC下使用自动释放池
    ARC环境不能使用NSAutoreleasePool类也不能调用autorelease方法,代替它们实现对象自动释放的是@autoreleasepool块和__autoreleasing修饰符。比较两种环境下的代码差异如下图:

    如图所示,@autoreleasepool块替换了NSAutoreleasePoool类对象的生成、持有及废弃这一过程。而附有__autoreleasing修饰符的变量替代了autorelease方法,将对象注册到了Autoreleasepool;由于ARC的优化,__autorelease是可以被省略的,所以简化后的ARC代码如下:
    //ARC环境下的测试:
    @autoreleasepool {
    id obj = [[NSObject alloc] init];
    NSLog(@"打印obj:%@", obj);
    }

    显式使用__autoreleasing修饰符的情况非常少见,这是因为ARC的很多情况下,即使是不显式的使用__autoreleasing,也能实现对象被注册到释放池中。主要包括以下几种情况:

  • 编译器会进行优化,检查方法名是否以alloc/new/copy/mutableCopy开始,如果不是则自动将返回对象注册到Autoreleasepool;
  • 访问附有__weak修饰符的变量时,实际上必定要访问注册到Autoreleasepool的对象,即会自动加入Autoreleasepool;
  • id的指针或对象的指针(id*,NSError **),在没有显式地指定修饰符时候,会被默认附加上__autoreleasing修饰符,加入Autoreleasepool

  • 注意:如果编译器版本为LLVM.3.0以上,即使ARC无效@autoreleasepool块也能够使用;如下源码所示:

    //MRC环境下的测试:
    @autoreleasepool{
    id obj = [[NSObject alloc] init];
    [obj autorelease];
    }


    二、AutoRelease原理

    2.1 使用@autoreleasepool{}

    我们在main函数中写入自动释放池相关的测试代码如下:

    int main(int argc, const char * argv[]) {
    @autoreleasepool {
    NSLog(@"Hello, World!");
    }
    return 0;
    }

    为了探究释放池的底层实现,我们在终端使用clang -rewrite-objc + 文件名命令将上述OC代码转化为C++源码:

    int main(int argc, const char * argv[]) {
    /* @autoreleasepool */
    {
    __AtAutoreleasePool __autoreleasepool;
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_3f_crl5bnj956d806cp7d3ctqhm0000gn_T_main_d37e0d_mi_0);
    }//大括号对应释放池的作用域

    return 0;
    }

    在经过编译器clang命令转化后,我们看到的所谓的@autoreleasePool块,其实对应着__AtAutoreleasePool的结构体。

    2.2 分析结构体__AtAutoreleasePool的具体实现

    在源码中找到__AtAutoreleasePool结构体的实现代码,具体如下:

    extern "C" __declspec(dllimport) void * objc_autoreleasePoolPush(void);
    extern "C" __declspec(dllimport) void objc_autoreleasePoolPop(void *);

    struct __AtAutoreleasePool {
    __AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}
    ~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}
    void * atautoreleasepoolobj;
    };

    __AtAutoreleasePool结构体包含了:构造函数、析构函数和一个边界对象;
    构造函数内部调用:objc_autoreleasePoolPush()方法,返回边界对象atautoreleasepoolobj
    析构函数内部调用:objc_autoreleasePoolPop()方法,传入边界对象atautoreleasepoolobj

    分析main函数中__autoreleasepool结构体实例的生命周期是这样的:
    __autoreleasepool是一个自动变量,其构造函数是在程序执行到声明这个对象的位置时调用的,而其析构函数则是在程序执行到离开这个对象的作用域时调用。所以,我们可以将上面main函数的代码简化如下:

    int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ {
    void *atautoreleasepoolobj = objc_autoreleasePoolPush();
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_kb_06b822gn59df4d1zt99361xw0000gn_T_main_d39a79_mi_0);
    objc_autoreleasePoolPop(atautoreleasepoolobj);
    }
    return 0;
    }
    2.3 objc_autoreleasePoolPush与objc_autoreleasePoolPop

    进一步观察自动释放池构造函数与析构函数的实现,其实它们都只是对AutoreleasePoolPage对应静态方法pushpop的封装

    void *objc_autoreleasePoolPush(void) {
    return AutoreleasePoolPage::push();
    }

    void objc_autoreleasePoolPop(void *ctxt) {
    AutoreleasePoolPage::pop(ctxt);
    }

    2.4 理解AutoreleasePoolPage
    AutoreleasePoolPage是一个C++中的类,打开Runtime的源码工程,在NSObject.mm文件中可以找到它的定义,摘取其中的关键代码如下:
    //大致在641行代码开始
    class AutoreleasePoolPage {
    # define EMPTY_POOL_PLACEHOLDER ((id*)1) //空池占位
    # define POOL_BOUNDARY nil //边界对象(即哨兵对象)
    static pthread_key_t const key = AUTORELEASE_POOL_KEY;
    static uint8_t const SCRIBBLE = 0xA3; // 0xA3A3A3A3 after releasing
    static size_t const SIZE =
    #if PROTECT_AUTORELEASEPOOL
    PAGE_MAX_SIZE; // must be multiple of vm page size
    #else
    PAGE_MAX_SIZE; // size and alignment, power of 2
    #endif
    static size_t const COUNT = SIZE / sizeof(id);
    magic_t const magic; //校验AutoreleasePagePoolPage结构是否完整
    id *next; //指向新加入的autorelease对象的下一个位置,初始化时指向begin()
    pthread_t const thread; //当前所在线程,AutoreleasePool是和线程一一对应的
    AutoreleasePoolPage * const parent; //指向父节点page,第一个结点的parent值为nil
    AutoreleasePoolPage *child; //指向子节点page,最后一个结点的child值为nil
    uint32_t const depth; //链表深度,节点个数
    uint32_t hiwat; //数据容纳的一个上限
    //......
    };

    其实,每个自动释放池都是是由若干个AutoreleasePoolPage组成的双向链表结构,如下图所示:


    AutoreleasePoolPage中拥有parentchild指针,分别指向上一个和下一个page;当前一个page的空间被占满(每个AutorelePoolPage的大小为4096字节)时,就会新建一个AutorelePoolPage对象并连接到链表中,后来的 Autorelease对象也会添加到新的page中;

    另外,当next== begin()时,表示AutoreleasePoolPage为空;当next == end(),表示AutoreleasePoolPage已满。

    2.5 理解哨兵对象/边界对象(POOL_BOUNDARY)的作用

    AutoreleasePoolPage的源码中,我们很容易找到边界对象(哨兵对象)的定义:

    #define POOL_BOUNDARY nil

    边界对象其实就是nil的别名,而它的作用事实上也就是为了起到一个标识的作用。

    每当自动释放池初始化调用objc_autoreleasePoolPush方法时,总会通过AutoreleasePoolPagepush方法,将POOL_BOUNDARY放到当前page的栈顶,并且返回这个边界对象;

    而在自动释放池释放调用objc_autoreleasePoolPop方法时,又会将边界对象以参数传入,这样自动释放池就会向释放池中对象发送release消息,直至找到第一个边界对象为止。

    2.6 理解objc_autoreleasePoolPush方法
    经过前面的分析,objc_autoreleasePoolPush最终调用的是  AutoreleasePoolPagepush方法,该方法的具体实现如下:
    static inline void *push() {
    return autoreleaseFast(POOL_BOUNDARY);
    }

    static inline id *autoreleaseFast(id obj)
    {
    AutoreleasePoolPage *page = hotPage();
    if (page && !page->full()) {
    return page->add(obj);
    } else if (page) {
    return autoreleaseFullPage(obj, page);
    } else {
    1. return autoreleaseNoPage(obj);
    }
    }

    //压栈操作:将对象加入AutoreleaseNoPage并移动栈顶的指针
    id *add(id obj) {
    id *ret = next;
    *next = obj;
    next++;
    return ret;
    }

    //当前hotPage已满时调用
    static id *autoreleaseFullPage(id obj, AutoreleasePoolPage *page) {
    do {
    if (page->child) page = page->child;
    else page = new AutoreleasePoolPage(page);
    } while (page->full());

    setHotPage(page);
    return page->add(obj);
    }

    //当前hotpage不存在时调用
    static id *autoreleaseNoPage(id obj) {
    AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
    setHotPage(page);

    if (obj != POOL_SENTINEL) {
    page->add(POOL_SENTINEL);
    }

    return page->add(obj);
    }

    观察上述代码,每次调用push其实就是创建一个新的AutoreleasePool,在对应的AutoreleasePoolPage中插入一个POOL_BOUNDARY ,并且返回插入的POOL_BOUNDARY 的内存地址。push方法内部调用的是autoreleaseFast方法,并传入边界对象(POOL_BOUNDARY)。hotPage可以理解为当前正在使用的AutoreleasePoolPage

    自动释放池最终都会通过page->add(obj)方法将边界对象添加到释放池中,而这一过程在autoreleaseFast方法中被分为三种情况:

  • 当前page存在且不满,调用page->add(obj)方法将对象添加至page的栈中,即next指向的位置
  • 当前page存在但是已满,调用autoreleaseFullPage初始化一个新的page,调用page->add(obj)方法将对象添加至page的栈中
  • 当前page不存在时,调用autoreleaseNoPage创建一个hotPage,再调用page->add(obj) 方法将对象添加至page的栈中

  • 2.7 objc_autoreleasePoolPop方法

    AutoreleasePool的释放调用的是objc_autoreleasePoolPop方法,此时需要传入边界对象作为参数。这个边界对象正是每次执行objc_autoreleasePoolPush方法返回的对象atautoreleasepoolobj

    同理,我们找到objc_autoreleasePoolPop最终调用的方法,即AutoreleasePoolPagepop方法,该方法的具体实现如下:

    static inline void pop(void *token)   //POOL_BOUNDARY的地址
    {
    AutoreleasePoolPage *page;
    id *stop;

    page = pageForPointer(token); //通过POOL_BOUNDARY找到对应的page
    stop = (id *)token;
    if (DebugPoolAllocation && *stop != POOL_SENTINEL) {
    // This check is not valid with DebugPoolAllocation off
    // after an autorelease with a pool page but no pool in place.
    _objc_fatal("invalid or prematurely-freed autorelease pool %p; ",
    token);
    }

    if (PrintPoolHiwat) printHiwat(); // 记录最高水位标记

    page->releaseUntil(stop); //向栈中的对象发送release消息,直到遇到第一个哨兵对象

    // memory: delete empty children
    // 删除空掉的节点
    if (DebugPoolAllocation && page->empty()) {
    // special case: delete everything during page-per-pool debugging
    AutoreleasePoolPage *parent = page->parent;
    page->kill();
    setHotPage(parent);
    } else if (DebugMissingPools && page->empty() && !page->parent) {
    // special case: delete everything for pop(top)
    // when debugging missing autorelease pools
    page->kill();
    setHotPage(nil);
    }
    else if (page->child) {
    // hysteresis: keep one empty child if page is more than half full
    if (page->lessThanHalfFull()) {
    page->child->kill();
    }
    else if (page->child->child) {
    page->child->child->kill();
    }
    }
    }

    上述代码中,首先根据传入的边界对象地址找到边界对象所处的page;然后选择当前page中最新加入的对象一直向前清理,可以向前跨越若干个page,直到边界所在的位置;清理的方式是向这些对象发送一次release消息,使其引用计数减一;

    另外,清空page对象还会遵循一些原则:

    1.如果当前的page中存放的对象少于一半,则子page全部删除;

    2.如果当前当前的page存放的多余一半(意味着马上将要满),则保留一个子page,节省创建新page的开销;

    2.8 autorelease方法

    上述是对自动释放池整个生命周期的分析,现在我们来理解延时释放对象autorelease方法的实现,首先查看该方法的调用栈:

    - [NSObject autorelease]
    └── id objc_object::rootAutorelease()
    └── id objc_object::rootAutorelease2()
    └── static id AutoreleasePoolPage::autorelease(id obj)
    └── static id AutoreleasePoolPage::autoreleaseFast(id obj)
    ├── id *add(id obj)
    ├── static id *autoreleaseFullPage(id obj, AutoreleasePoolPage *page)
    │ ├── AutoreleasePoolPage(AutoreleasePoolPage *newParent)
    │ └── id *add(id obj)
    └── static id *autoreleaseNoPage(id obj)
    ├── AutoreleasePoolPage(AutoreleasePoolPage *newParent)
    └── id *add(id obj)

    如上所示,autorelease方法最终也会调用上面提到的 autoreleaseFast方法,将当前对象加到AutoreleasePoolPage中。关于autoreleaseFast的分析这里不再累述,我们主要来考虑一下两次调用的区别:

    autorelease函数和push函数一样,关键代码都是调用autoreleaseFast函数向自动释放池的链表栈中添加一个对象,不过push函数入栈的是一个边界对象,而autorelease函数入栈的是一个具体的Autorelease的对象。

    三、AutoreleasePool与NSThread、NSRunLoop的关系

    由于AppKitUIKit框架的优化,我们很少需要显式的创建一个自动释放池块。这其中就涉及到AutoreleasePoolNSThreadNSRunLoop的关系。

    3.1 RunLoop和NSThread的关系
    RunLoop是用于控制线程生命周期并接收事件进行处理的机制,其实质是一个do-While循环。在苹果文档找到关于NSRunLoop的介绍如下:
    Your application neither creates or explicitly manages NSRunLoop objects. Each NSThread object—including the application’s main thread—has an NSRunLoop object automatically created for it as needed. If you need to access the current thread’s run loop, you do so with the class method currentRunLoop.

    总结RunLoopNSThread(线程)之间的关系如下:
  • RunLoop与线程是一一对应关系,每个线程(包括主线程)都有一个对应的RunLoop对象;其对应关系保存在一个全局的Dictionary里;
  • 主线程的RunLoop默认由系统自动创建并启动;而其他线程在创建时并没有RunLoop,若该线程一直不主动获取,就一直不会有RunLoop
  • 苹果不提供直接创建RunLoop的方法;所谓其他线程Runloop的创建其实是发生在第一次获取的时候,系统判断当前线程没有RunLoop就会自动创建;
  • 当前线程结束时,其对应的Runloop也被销毁;
  • 3.2 RunLoop和AutoreleasePool的关系

    苹果文档中找到两者关系的介绍如下:

    The Application Kit creates an autorelease pool on the main thread at the beginning of every cycle of the event loop, and drains it at the end, thereby releasing any autoreleased objects generated while processing an event.

    如上所述,主线程的NSRunLoop在监测到事件响应开启每一次event loop之前,会自动创建一个autorelease pool,并且会在event loop结束的时候执行drain操作,释放其中的对象。

    3.3 Thread和AutoreleasePool的关系
    苹果文档中找到两者关系的介绍如下:

    Each thread (including the main thread) maintains its own stack of NSAutoreleasePool objects (see Threads). As new pools are created, they get added to the top of the stack. When pools are deallocated, they are removed from the stack. Autoreleased objects are placed into the top autorelease pool for the current thread. When a thread terminates, it automatically drains all of the autorelease pools associated with itself.

    如上所述, 包括主线程在内的所有线程都维护有它自己的自动释放池的堆栈结构。新的自动释放池被创建的时候,它们会被添加到栈的顶部,而当池子销毁的时候,会从栈移除。对于当前线程来说,Autoreleased对象会被放到栈顶的自动释放池中。当一个线程线程停止,它会自动释放掉与其关联的所有自动释放池。

    四、AutoreleasePool在主线程上的释放时机

    4.1 理解主线程上的自动释放过程
    分析主线程RunLoop管理自动释放池并释放对象的详细过程,我们在如下Demo中的主线程中设置断点,并执行lldb命令:po [NSRunLoop currentRunLoop],具体效果如下:

    我们看到主线程RunLoop中有两个与自动释放池相关的Observer,它们的 activities分别为0x10xa0这两个十六进制的数,转为二进制分别为110100000,对应CFRunLoopActivity的类型如下:
    /* Run Loop Observer Activities */
    typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry = (1UL << 0), //0x1,启动Runloop循环
    kCFRunLoopBeforeTimers = (1UL << 1),
    kCFRunLoopBeforeSources = (1UL << 2),
    kCFRunLoopBeforeWaiting = (1UL << 5), //0xa0,即将进入休眠
    kCFRunLoopAfterWaiting = (1UL << 6),
    kCFRunLoopExit = (1UL << 7), //0xa0,退出RunLoop循环
    kCFRunLoopAllActivities = 0x0FFFFFFFU
    };

    结合RunLoop监听的事件类型,分析主线程上自动释放池的使用过程如下:

  • App启动后,苹果在主线程RunLoop里注册了两个Observer,其回调都是 _wrapRunLoopWithAutoreleasePoolHandler();
  • 第一个Observer监视的事件是Entry(即将进入Loop),其回调内会调用 _objc_autoreleasePoolPush()创建自动释放池。order = -2147483647(即32位整数最小值)表示其优先级最高,可以保证创建释放池发生在其他所有回调之前;
  • 第二个Observer监视了两个事件BeforeWaiting(准备进入休眠)时调用_objc_autoreleasePoolPop()_objc_autoreleasePoolPush()释放旧的池并创建新池;Exit(即将退出Loop) 时调用 _objc_autoreleasePoolPop()来释放自动释放池。order = 2147483647(即32位整数的最大值)表示其优先级最低,保证其释放池子发生在其他所有回调之后;
  • 在主线程执行的代码,通常是写在诸如事件回调、Timer回调内的。这些回调会被 RunLoop创建好的AutoreleasePool环绕着,所以不会出现内存泄漏,开发者也不必显示创建AutoreleasePool了;
  • 最后,也可以结合图示理解主线程上自动释放对象的具体流程:


  • 程序启动到加载完成后,主线程对应的RunLoop会停下来等待用户交互
  • 用户的每一次交互都会启动一次运行循环,来处理用户所有的点击事件、触摸事件。
  • RunLoop检测到事件后,就会创建自动释放池;
  • 所有的延迟释放对象都会被添加到这个池子中;
  • 在一次完整的运行循环结束之前,会向池中所有对象发送release消息,然后自动释放池被销毁;

  • 4.2 测试主线程上的对象自动释放过程
    下面的代码创建了一个Autorelease对象string,并且通过weakString进行弱引用(不增加引用计数,所以不会影响对象的生命周期),具体如下:
    @interface TestMemoryVC ()
    @property (nonatomic,weak)NSString *weakString;
    @end

    @implementation TestMemoryVC
    - (void)viewDidLoad {
    [super viewDidLoad];
    NSString *string = [NSString stringWithFormat:@"%@",@"WUYUBEICHEN"];
    self.weakString = string;
    }

    - (void)viewWillAppear:(BOOL)animated {
    [super viewWillAppear:animated];
    NSLog(@"viewWillAppear:%@", self.weakString);
    }

    - (void)viewDidAppear:(BOOL)animated {
    [super viewDidAppear:animated];
    NSLog(@"viewDidAppear:%@", self.weakString);
    }

    @end

    //打印结果:
    //viewWillAppear:WUYUBEICHEN
    //viewDidAppear:(null)
    代码分析:自动变量的string在离开viewDidLoad的作用域后,会依靠当前主线程上的RunLoop迭代自动释放。最终string对象在viewDidAppear方法执行前被释放(RunLoop完成此次迭代)。

    五、AutoreleasePool子线程上的释放时机

    子线程默认不开启RunLoop,那么其中的延时对象该如何释放呢?其实这依然要从ThreadAutoreleasePool的关系来考虑:
    Each thread (including the main thread) maintains its own stack of NSAutoreleasePool objects.
    也就是说,每一个线程都会维护自己的 Autoreleasepool栈,所以子线程虽然默认没有开启RunLoop,但是依然存在AutoreleasePool,在子线程退出的时候会去释放autorelease对象。

    前面讲到过,ARC会根据一些情况进行优化,添加__autoreleasing修饰符,其实这就相当于对需要延时释放的对象调用了autorelease方法。从源码分析的角度来看,如果子线程中没有创建AutoreleasePool ,而一旦产生了Autorelease对象,就会调用autoreleaseNoPage方法自动创建hotpage,并将对象加入到其栈中。所以,一般情况下,子线程中即使我们不手动添加自动释放池,也不会产生内存泄漏。

    六、AutoreleasePool需要手动添加的情况

    尽管ARC已经做了诸多优化,但是有些情况我们必须手动创建AutoreleasePool,而其中的延时对象将在当前释放池的作用域结束时释放。苹果文档中说明了三种情况,我们可能会需要手动添加自动释放池:
    1. 编写的不是基于UI框架的程序,例如命令行工具;
    2. 通过循环方式创建大量临时对象;
    3. 使用非Cocoa程序创建的子线程;

    而在ARC环境下的实际开发中,我们最常遇到的也是第二种情况,以下面的代码为例:

    - (void)viewDidLoad {
    [super viewDidLoad];
    for (int i = 0; i < 1000000; i++) {
    NSObject *obj = [[NSObject alloc] init];
    NSLog(@"打印obj:%@", obj);
    }
    }
    上述代码中,obj因为离开作用域所以会被加入最近一次创建的自动释放池中,而这个释放池就是主线程上的RunLoop管理的;因为for循环在当前线程没有执行完毕,Runloop也就没有完成当前这一次的迭代,所以导致大量对象被延时释放。释放池中的对象将会在viewDidAppear方法执行前就被销毁。在此情况下,我们就有必要通过手动干预的方式及时释放不需要的对象,减少内存消耗;优化的代码如下:
    - (void)viewDidLoad {
    [super viewDidLoad];
    for (int i = 0; i < 1000000; i++) {
    @autoreleasepool{
    NSObject *obj = [[NSObject alloc] init];
    NSLog(@"打印obj:%@", obj);
    }
    }
    }


    摘自作者:梧雨北辰
    原贴链接:https://www.jianshu.com/p/7bd2f85f03dc

    收起阅读 »

    android 逆向工程-工具篇 jadx(九)

    Android逆向工程篇:android 逆向工程-工具篇 drozer(一)android 逆向工程-工具篇 apktool(二)android 逆向工程-语言篇 Smali(三)android 逆向工程-分析篇 漏洞与风险(四)android 逆向工程-工...
    继续阅读 »

    Android逆向工程篇:


    Jadx Github

    下载地址:https://github.com/skylot/jadx


    使用 jadx

    双击 jadx-gui运行起来,直接打开  apk、dex、jar、zip、class、aar 文件。

    搜索功能

    点击 Navigation -> Text Search 或 Navigation -> Class Search 

    Class、Method、Field、Code四种类型搜索


    搜索引用的代码


    deobfuscation

    Tools -> deobfusation 方便我们识别和搜索,以免被混淆后的代码绕晕。

    一键导出 Gradle 

    主要是为了借助 AS 强大的 IDE 功能,例如方法跳转、引用搜索等等,阅读更方便。

    收起阅读 »

    android 逆向工程-技术篇 Android studio动态调试(八)

    Android逆向工程篇:android 逆向工程-工具篇 drozer(一)android 逆向工程-工具篇 apktool(二)android 逆向工程-语言篇 Smali(三)android 逆向工程-分析篇 漏洞与风险(四)android 逆向工程-工...
    继续阅读 »

    Android逆向工程篇:


    一、下载插件smalidea

    地址: https://bitbucket.org/JesusFreke/smali/downloads    下载smalidea-0.03.zip


    二、反编译APK

    java -jar apktool.jar d -f F:\apktools\demo.apk -o F:\apktools\demo


    三、添加DUBUG属性

    在AndroidManifest.xml的application添加属性:android:debuggable="true"


    四、安装修改后的应用


    1、安装上面重新签名得到的apk应用

    2、创建目录Smali/src,并且把smali反编译出的文件放到该目录下


    五、调试启动应用

    adb shell am start -D -n app.mm.demo/.demoActivity
    adb shell  ps | grep demo 查看应用pid  24551
    然后进行端口转发:
    adb forward tcp:8700 jdwp:24551
    注意:如果不允许建立则输入netstat -ano查看进程
    kill了8700进程就好了


    六、用Android studio打开smali文件

    配置如下


    end

    Run->Debug,开始动态调试

    收起阅读 »

    android 逆向工程-工具篇 IDA pro入门(七)

    Android逆向工程篇:android 逆向工程-工具篇 drozer(一)android 逆向工程-工具篇 apktool(二)android 逆向工程-语言篇 Smali(三)android 逆向工程-分析篇 漏洞与风险(四)android 逆向工程-工...
    继续阅读 »

    Android逆向工程篇:


    注:自行网上下载IDA pro

    我安装好的IDA 包含64和32两个版本,经测试 32位支持伪代码,可以F5对照C语言等进行更方便的分析。

    用IDA pro打开一个so文件


    展示如下(F5查看伪代码)


    • IDA View-A是反汇编窗口
    • HexView-A是十六进制格式显示的窗口
    • Imports是导入表(程序中调用到的外面的函数)
    • Functions是函数表(这个程序中的函数)
    • Structures是结构
    • Enums是枚举

    IDA View-A

    这里会有流程图(按回车进行切换),判断是执行绿色,判断否执行红色,蓝色为一个执行块。

    分析

    先展示些ARM汇编的基础

    寄存器

    • R0-R3:用于函数参数及返回值的传递
    • R4-R6, R8,R10-R11:没有特殊规定,就是普通的通用寄存器
    • R7:栈帧指针(Frame Pointer).指向前一个保存的栈帧(stack frame)和链接寄存器(link register, lr)在栈上的地址。
    • R9:操作系统保留
    • R12:又叫IP(intra-procedure scratch)
    • R13:又叫SP(stack pointer),是栈顶指针
    • R14:又叫LR(link register),存放函数的返回地址。
    • R15:又叫PC(program counter),指向当前指令地址。
    • CPSR:当前程序状态寄存器(Current Program State Register),在用户状态下存放像condition标志中断禁用等标志的。
    • VFP:(向量浮点运算)相关的寄存器

    基本的指令

    • add 加指令
    • sub 减指令
    • str 把寄存器内容存到栈上去
    • ldr 把栈上内容载入一寄存器中
    • .w是一个可选的指令宽度说明符。它不会影响为此指令的行为,它只是确保生成 32 位指令。
    • bl 执行函数调用,并把使lr指向调用者(caller)的下一条指令,即函数的返回地址
    • blx 同上,但是在ARM和thumb指令集间切换。
    • bx bx lr返回调用函数(caller)。
    • bne 数据跳转指令,标志寄存器中Z标志位不等于零时, 跳转到BNE后标签处。
    • CMP 比较命令
    • B 无条件跳转
    收起阅读 »

    android 逆向工程-开发篇 apk加固(六)

    Android逆向工程篇:android 逆向工程-工具篇 drozer(一)android 逆向工程-工具篇 apktool(二)android 逆向工程-语言篇 Smali(三)android 逆向工程-分析篇 漏洞与风险(四)android 逆向工程-工...
    继续阅读 »

    Android逆向工程篇:

    注:参考 https://blog.csdn.net/jiangwei0910410003/article/details/48415225

    加密工具:https://github.com/dileber/DexShellTools/tree/master

    壳程序Apk:https://github.com/dileber/ReforceApk/tree/master

    加固原理

    一句话:通过修改壳apk中的dex文件,把需要加壳的apk通过二进制形式,来加密到壳apk中,运行时进行解密操作。

    加壳重点(其余的参考注释的文章):

    加壳时需要了解dex文件头部

    加壳后的dex文件需要替换壳的dex文件

    加壳后的apk需要对其重新签名:

    jarsigner -verbose -keystore 签名文件 -storepass 密码  -keypass alias的密码 -sigfile CERT -digestalg SHA1 -sigalg MD5withRSA  签名后的文件 签名前的apk alias名称

    eg:
    jarsigner -verbose -keystore forceapk -storepass 123456 -keypass 123456 -sigfile CERT -digestalg SHA1 -sigalg MD5withRSA -signedjar ReforceApk_des.apk ReforceApk_src.apk jiangwei

    签名文件的密码:123456
    alais的密码:123456


    收起阅读 »

    android 逆向工程-工具篇 dex2jar jd-gui(五)

    Android逆向工程篇: android 逆向工程-工具篇 drozer(一) android 逆向工程-工具篇 apktool(二) android 逆向工程-语言篇 Smali(三) android 逆向工程-分析篇 漏洞与风险(四) android ...
    继续阅读 »

    Android逆向工程篇:



    获取classes.dex文件:


    使用压缩软件打开apk,把目录下的classes.dex文件解压出来


    下载dex2jar,并解压到目录下。


    在cmd中运行




    d2j-dex2jar.bat classes.dex


    于是在dex2jar目录下产生了一个classes.jar


    下载 jd-gui 官网地址


    直接下载win版打开 jar文件


    收起阅读 »

    iOS性能优化 — 四、内存泄露检测

    上篇文章为大家讲解了安装包瘦身,这篇文章继续为大家讲解下内存泄露检测。造成内存泄漏原因常见循环引用及解决方案怎么检测循环引用造成内存泄漏原因在用C/C++时,创建对象后未销毁,比如调用malloc后不free、调用new后不delete;调用CoreFound...
    继续阅读 »

    上篇文章为大家讲解了安装包瘦身,这篇文章继续为大家讲解下内存泄露检测。

    • 造成内存泄漏原因

    • 常见循环引用及解决方案

    • 怎么检测循环引用

    造成内存泄漏原因

    • 在用C/C++时,创建对象后未销毁,比如调用malloc后不free、调用new后不delete;

    • 调用CoreFoundation里面的C方法后创建对对象后不释放。比如调用CGImageCreate不调用CGImageRelease;

    • 循环引用。当对象A和对象B互相持有的时候,就会产生循环引用。常见产生循环引用的场景有在VC的cellForRowAtIndexPath方法中cell block引用self。

    常见循环引用及解决方案

    1) 在VC的cellForRowAtIndexPath方法中cell的block直接引用self或者直接以_形式引用属性造成循环引用。

    cell.clickBlock = ^{
    self.name = @"akon";
    };

    cell.clickBlock = ^{
    _name = @"akon";
    };

    解决方案:把self改成weakSelf;

    __weak typeof(self)weakSelf = self;
    cell.clickBlock = ^{
    weakSelf.name = @"akon";
    };

    2)在cell的block中直接引用VC的成员变量造成循环引用。

    //假设 _age为VC的成员变量
    @interface TestVC(){

    int _age;

    }
    cell.clickBlock = ^{
    _age = 18;
    };

    解决方案有两种:

    • 用weak-strong dance

    __weak typeof(self)weakSelf = self;
    cell.clickBlock = ^{
    __strong typeof(weakSelf) strongSelf = weakSelf;
    strongSelf->age = 18;
    };
    • 把成员变量改成属性

    //假设 _age为VC的成员变量
    @interface TestVC()

    @property(nonatomic, assign)int age;

    @end

    __weak typeof(self)weakSelf = self;
    cell.clickBlock = ^{
    weakSelf.age = 18;
    };

    3)delegate属性声明为strong,造成循环引用。

    @interface TestView : UIView

    @property(nonatomic, strong)id<TestViewDelegate> delegate;

    @end

    @interface TestVC()<TestViewDelegate>

    @property (nonatomic, strong)TestView* testView;

    @end

    testView.delegate = self; //造成循环引用

    解决方案:delegate声明为weak

    @interface TestView : UIView

    @property(nonatomic, weak)id<TestViewDelegate> delegate;

    @end

    4)在block里面调用super,造成循环引用。

    cell.clickBlock = ^{
    [super goback]; //造成循环应用
    };

    解决方案,封装goback调用

    __weak typeof(self)weakSelf = self;
    cell.clickBlock = ^{
    [weakSelf _callSuperBack];
    };

    - (void) _callSuperBack{
    [self goback];
    }

    5)block声明为strong
    解决方案:声明为copy
    6)NSTimer使用后不invalidate造成循环引用。
    解决方案:

    • NSTimer用完后invalidate;

    • NSTimer分类封装

    *   (NSTimer *)ak_scheduledTimerWithTimeInterval:(NSTimeInterval)interval
    block:(void(^)(void))block
    repeats:(BOOL)repeats{

    return [self scheduledTimerWithTimeInterval:interval
    target:self
    selector:@selector(ak_blockInvoke:)
    userInfo:[block copy]
    repeats:repeats];
    }

    * (void)ak_blockInvoke:(NSTimer*)timer{

    void (^block)(void) = timer.userInfo;
    if (block) {
    block();
    }
    }

    怎么检测循环引用

    • 静态代码分析。 通过Xcode->Product->Anaylze分析结果来处理;

    • 动态分析。用MLeaksFinder或者Instrument进行检测。

    转自:https://www.jianshu.com/p/f06f14800cf7

    收起阅读 »

    Xcode12适配The linked library is missing one or more architectures required by this target问题

    问题升级到Xcode12后,运行Release模式后,会提示以下信息: The linked library 'xxxx.a/Framework' is missing one or more architectures required by this ta...
    继续阅读 »

    问题
    升级到Xcode12后,运行Release模式后,会提示以下信息:

    The linked library 'xxxx.a/Framework' is missing one or more architectures required by this target: armv7.

    又或者


    xxx/Pods/Target Support Files/Pods-xxx/Pods-xxx-frameworks.sh: line 128: ARCHS[@]: unbound variable
    Command PhaseScriptExecution failed with a nonzero exit code

    以上涉及架构问题

    解决方案

    在Target-Build Settings-Excluded Architectures中添加以下代码

    EXCLUDED_ARCHS__EFFECTIVE_PLATFORM_SUFFIX_simulator__NATIVE_ARCH_64_BIT_x86_64=arm64 arm64e armv7 armv7s armv6 armv8 EXCLUDED_ARCHS=$(inherited) $(EXCLUDED_ARCHS__EFFECTIVE_PLATFORM_SUFFIX_$(EFFECTIVE_PLATFORM_SUFFIX)__NATIVE_ARCH_64_BIT_$(NATIVE_ARCH_64_BIT))


    转自:https://www.jianshu.com/p/81741aed39f7


    收起阅读 »

    iOS 使用NSSetUncaughtExceptionHandler收集Crash

    在iOS程序崩溃时,一般我们是用Bugtags、Bugly、友盟等第三方收集崩溃,其实官方提供的NSUncaughtExceptionHandler来收集crash信息。实现方式如下:自定义一个UncaughtExceptionHandler类,在.h中: @...
    继续阅读 »

    在iOS程序崩溃时,一般我们是用Bugtags、Bugly、友盟等第三方收集崩溃,其实官方提供的NSUncaughtExceptionHandler来收集crash信息。实现方式如下:
    自定义一个UncaughtExceptionHandler类,在.h中:

    @interface CustomUncaughtExceptionHandler : NSObject
    + (void)setDefaultHandler;
    + (NSUncaughtExceptionHandler *)getHandler;
    @end

    复制代码
    在.m中实现:

    #import "CustomUncaughtExceptionHandler.h"

    // 沙盒的地址
    NSString * applicationDocumentsDirectory() {
    return [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];
    }


    // 崩溃时的回调函数
    void UncaughtExceptionHandler(NSException * exception) {
    NSArray * arr = [exception callStackSymbols];
    NSString * reason = [exception reason]; // // 崩溃的原因 可以有崩溃的原因(数组越界,字典nil,调用未知方法...) 崩溃的控制器以及方法
    NSString * name = [exception name];
    NSString * url = [NSString stringWithFormat:@"crash报告\nname:%@\nreason:\n%@\ncallStackSymbols:\n%@",name,reason,[arr componentsJoinedByString:@"\n"]];
    NSString * path = [applicationDocumentsDirectory() stringByAppendingPathComponent:@"crash.txt"];
    // 将一个txt文件写入沙盒
    [url writeToFile:path atomically:YES encoding:NSUTF8StringEncoding error:nil];
    }

    @implementation CustomUncaughtExceptionHandler

    + (void)setDefaultHandler {
    NSSetUncaughtExceptionHandler(&UncaughtExceptionHandler);
    }

    + (NSUncaughtExceptionHandler *)getHandler {
    return NSGetUncaughtExceptionHandler();
    }

    @end

    复制代码
    这样我们就实现好了一个自定义UncaughtExceptionHandler类,接下来只需要在合适的地方获取crash文件以及传到服务器上去即可,如下所示:

    - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    // Override point for customization after application launch.

    //崩溃日志
    [CustomUncaughtExceptionHandler setDefaultHandler];
    //获取崩溃日志,然后发送
    NSString *path = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];
    NSString *dataPath = [path stringByAppendingPathComponent:@"crash.txt"];
    NSData *data = [NSData dataWithContentsOfFile:dataPath];
    if (data != nil) {
    //发送崩溃日志
    NSLog(@"crash了:%@",data);
    }
    }

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


    收起阅读 »

    Bootstrap Table

    前端1.BootStrap Table1.1.1 HTML<div> <div class="panel-body table-responsive"> <table id="productTable" class="tab...
    继续阅读 »

    前端

    1.BootStrap Table

    1.1.1 HTML

    <div>
    <div class="panel-body table-responsive">
    <table id="productTable" class="table">
    </table>
    </div>
    </div>

    1.1.2 js初始化(开发常用方法)

    $('#productTable').bootstrapTable('refreshOptions',{pageNumber:1,pageSize:10});
    var tableObject= $.find("#productTable");
    $(tableObject).bootstrapTable({
    locale: 'zn-CN',
    pageSize: 10,
    pageNumber: 1,
    pageList: [10, 25, 50,100],
    clickToSelect: true,
    striped: true,
    ajax: function(ajaxParams) {
    json.NEXT_KEY = (ajaxParams.data.offset /ajaxParams.data.limit + 1) + "";
    json.PAGE_SIZE = ajaxParams.data.limit + "";
    //json.SORT_NAME = ajaxParams.data.sort;
    //json.SORT_ORDER = ajaxParams.data.order;
    YT.ajaxData({
    url:dataUrl,
    params: json,
    success: function (msg) {
    var resultData = {total: msg.TOTAL_NUM||0,rows: msg.LIST|| []};
    ajaxParams.success(resultData);
    }
    });
    },
    pagination: true,
    sidePagination: 'server',
    //sortName: '表格头排序字段',
    //sortOrder: 'desc',
    formatNoMatches: function() {
    return "暂无数据";
    },
    columns: [
    {
    checkbox: true,
    singleSelect : true,
    align: 'center'
    },
    {
    field: '',
    title: '操作',
    formatter: removeHtml,
    align: 'center'
    }]
    });
    // 自定义table列
    function removeHtml(value,row,index){
    var data = $("#productTable").bootstrapTable('getData');
    var params= data[index];
    return [
    '<a class="btn btn-xs btn-primary" >自定义一些方法</a>'
    ].join('')
    }
    // 常用方法
    1.获取当前table初始化数据
    var data = $("#productTable").bootstrapTable('getData');
    data-index:该属性是bootstrap table 下角标
    2.获取多选选中行的数据
    var data = $("#productTable").bootstrapTable('getSelections');
    3.清楚多选框全选
    $("#prodTable input[type='checkbox']:checked").prop("checked",false);
    4.获取每页显示的数量
    var pageSize = $('#prodTable').bootstrapTable('getOptions').pageSize;
    5.获取当前是第几页
    var pageNumber = $('#prodTable').bootstrapTable('getOptions').pageNumber;
    6.隐藏列、显示列(可用于初始化table之后的列的动添显示与隐藏,执行该时间之后数据会回滚到初始化table时的数据)
    $("#prodTable").bootstrapTable("hideColumn","GROUP_LEADER_PRICE")
    $("#prodTable").bootstrapTable("showColumn","GROUP_LEADER_PRICE")

    1.1.3 总计

    function statisticsTableInit() {
    var columns = [
    {
    field: 'column1',
    title: '表头1',
    align: 'center'
    },
    {
    field: 'column2',
    title: '表头2',
    align: 'center'
    },
    {
    field: 'column3',
    title: '表头3',
    align: 'center'
    }
    ];
    pageList.find("#prodTable").bootstrapTable({
    locale: 'zn-CN',
    columns: columns
    });
    }
    function statisticsAjax(json) {
    YT.ajaxData({
    url:YT.dataUrl,
    params: json,
    success: function (msg) {
    if(msg && msg.LIST){
    pageList.find("#prodTable").bootstrapTable('load',(msg.LIST));
    }
    }
    });
    }


    收起阅读 »

    JavaScript重构技巧 — 函数和类

    JavaScript 是一种易于学习的编程语言,编写运行并执行某些操作的程序很容易。然而,要编写一段干净的JavaScript 代码是很困难的。在本文中,我们将介绍一些与清理 JavaScript 函数和类有关的重构思想。不要直接对参数赋值在使用参数之前,我们...
    继续阅读 »

    JavaScript 是一种易于学习的编程语言,编写运行并执行某些操作的程序很容易。然而,要编写一段干净的JavaScript 代码是很困难的。

    在本文中,我们将介绍一些与清理 JavaScript 函数和类有关的重构思想。

    不要直接对参数赋值

    在使用参数之前,我们应该删除对参数的赋值,并将参数值赋给变量。

    例如,我们可能会写这样的代码:

    const discount = (subtotal) => {
    if (subtotal > 50) {
    subtotal *= 0.8;
    }
    }

    对比上面的代码,我们可以这样写:

    const discount = (subtotal) => {
    let _subtotal = subtotal;
    if (_subtotal > 50) {
    _subtotal *= 0.8;
    }
    }

    因为参数有可能是通过值或者引用传递的,如果是引用传递的,直接负值操作,有些结果会让感到困惑。

    本例是通过值传递的,但为了清晰起见,我们还是将参数赋值给变量了。

    用函数替换方法

    我们可以将一个方法变成自己的函数,以便所有类都可以访问它。

    例如,我们可能会写这样的代码:

    const hello = () => {
    console.log('hello');
    }
    class Foo {
    hello() {
    console.log('hello');
    }
    //...
    }
    class Bar {
    hello() {
    console.log('hello');
    }
    //...
    }

    我们可以将hello方法提取到函数中,如下所示:

    const hello = () => {
    console.log('hello');
    }
    class Foo {
    //...
    }
    class Bar {
    //...
    }

    由于hello方法不依赖于this,并且在两个类中都重复,因此我们应将其移至其自己的函数中以避免重复。

    替代算法

    相对流程式的写法,我们想用一个更清晰的算法来代替,例如,我们可能会写这样的代码:

    const doubleAll = (arr) => {
    const results = []
    for (const a of arr) {
    results.push(a * 2);
    }
    return results;
    }

    对比上面的代码,我们可以这样写:

    const doubleAll = (arr) => {
    return arr.map(a => a * 2);
    }

    通过数组方法替换循环,这样doubleAll函数就会更加简洁。

    如果有一种更简单的方法来解决我们的需求,那么我们就应该使用它。

    移动方法

    在两个类之间,我们可以把其中一个类的方法移动到另一个类中,例如,我们可能会写这样的代码:

    class Foo {
    method() {}
    }
    class Bar {
    }

    假如,我们在 Bar 类使用 method 的次数更多,那么应该把 method 方法移动到 Bar 类中, Foo 如果需要在直接调用 Bar 类的中方法即可。

    class Foo {
    }
    class Bar {
    method() {}
    }

    移动字段

    除了移动方法外,我们还可以移动字段。例如,我们可能会写这样的代码:

    class Foo {
    constructor(foo) {
    this.foo = foo;
    }
    }
    class Bar {
    }

    跟移动方法的原因类似,我们有时这么改代码:

    class Foo {
    }
    class Bar {
    constructor(foo) {
    this.foo = foo;
    }
    }

    我们可以将字段移至最需要的地方

    提取类

    如果我们的类很复杂并且有多个方法,那么我们可以将额外的方法移到新类中。

    例如,我们可能会写这样的代码:

    class Person {
    constructor(name, phoneNumber) {
    this.name = name;
    this.phoneNumber = phoneNumber;
    }
    addAreaCode(areaCode) {
    return `${areaCode}-${this.phoneNumber}`
    }
    }

    我们可以这样重构:

    class PhoneNumber {
    constructor(phoneNumber) {
    this.phoneNumber = phoneNumber;
    }
    addAreaCode(areaCode) {
    return `${areaCode}-${this.phoneNumber}`
    }
    }
    class Person {
    constructor(name, phoneNumber) {
    this.name = name;
    this.phoneNumber = new PhoneNumber(phoneNumber);
    }
    }

    上面我们将Person类不太相关的方法addAreaCode 移动了自己该处理的类中。

    通过这样做,两个类只做一件事,而不是让一个类做多件事。

    总结

    我们可以从复杂的类中提取代码,这些复杂的类可以将多种功能添加到自己的类中。

    此外,我们可以将方法和字段移动到最常用的地方。

    将值分配给参数值会造成混淆,因此我们应该在使用它们之前将其分配给变量。


    代码部署后可能存在的BUG没法实时知道,事后为了解决这些BUG,花了大量的时间进行log 调试,这边顺便给大家推荐一个好用的BUG监控工具 Fundebug

    原文:https://levelup.gitconnected....

    收起阅读 »

    我是如何在 Vue 项目中做代码分割的

    通常为了开发效率,我们会使用 vue-cli 创建项目,这样创建的项目默认情况下编译是会对代码进行分割的。但是如果是自行配置的 webpack 环境的话,还是很有必要熟悉代码分割的相关知识的。为什么要做代码分割在配置 webpack 的过程...
    继续阅读 »

    通常为了开发效率,我们会使用 vue-cli 创建项目,这样创建的项目默认情况下编译是会对代码进行分割的。但是如果是自行配置的 webpack 环境的话,还是很有必要熟悉代码分割的相关知识的。

    为什么要做代码分割

    在配置 webpack 的过程中,很多时候我们的 webpack 入口只写了一个 entry: '${sourceDir}/index.js’,默认情况下只会生成一个 bundle 文件,包含了第三方库、公共代码及不同页面所用到的业务逻辑,这必然会造成该 bundle 文件体积过大,影响页面首次的加载速度,因此我们需要对代码进行分割,加快首次进入页面的速度。

    代码分割思路

    首先把第三方库、公共代码抽离出来,因为这些代码变动的频率小,可以打包成一个文件,这样每次上线文件都不发生变化,可以充分利用网络缓存加快文件下载速度,分割的细的话就是,第三方库为一个 js 文件, 公共代码为一个 js 文件。

    然后,按照路由(页面)进行代码分割,每个页面生成一个 js 文件,这样每次首次进入就只加载公共代码和本页面用的的 js 文件, 而不用加载其它页面无关的代码。

    最后,再进行精细分割的话,就是根据组件使用情况进行分割,来实现组件的懒加载,比如:页面中的不同 tab,可以根据 tab 的展示情况进行分割,把需要点击或者用户主动操作才能呈现的组件进行懒加载,这样就在页面级又进行了更细粒度的代码分割。

    代码分割实战

    第三方库及公共代码分割

    第一步我们进行第三方库的分割,比如 vue、vue-router、vuex、axios 等三方库,把它们放到 vender.js 中,然后 utils、common 文件等放在 common.js 中。这些通过 webpack 的 entry 及 splitChunk 配置即可实现。

    修改 entry 配置:

    {
    // ...
    entry: {
    // 把公共代码放到 common 里
    common: [`${sourceDir}/utils/index.js`],
    main: `${sourceDir}/index.js`,
    },
    // ...
    }

    splitChunk 配置:

    {
    optimization: {
    // splitChunks 配置
    splitChunks: {
    cacheGroups: {
    default: {
    name: 'vendor',
    // 把第三方库放到 vendor 里,包括 vue, vue-router, vuex 等
    // 因为他们都是从 node_modules 里加载的,这里直接正则匹配
    test: /[\\/]node_modules[\\/]/,
    chunks: 'initial',
    // 调整优先级,优先处理
    priority: 10,
    },
    common: {
    chunks: 'all',
    name: 'common',
    // 匹配 entry 里的 common 配置
    test: 'common',
    },
    },
    },
    // runtime 代码放在 runtime 文件中
    runtimeChunk: {
    name: 'runtime',
    },
    }
    }

    另外就是 output 配置了,[name] 表示让 chunk 名称作为文件名, [chunkhash:8] 表示加上 hash,上线后不走缓存加载最新的代码。

    {
    output: {
    path: path.join(__dirname, './dist'),
    filename: 'static/[name].[chunkhash:8].bundle.js',
    chunkFilename: 'static/[name].[chunkhash:8].bundle.js',
    },
    }

    做完第三方库及公共代码分割,打包后生成的文件如下:

    assets by path static/*.js 138 KiB
    asset static/vendor.4391b08b.bundle.js 133 KiB [emitted] [immutable] [minimized] (name: vendor) (id hint: default)
    asset static/main.0d6dab3a.bundle.js 3.9 KiB [emitted] [immutable] [minimized] (name: main)
    asset static/runtime.bdaa3432.bundle.js 1.1 KiB [emitted] [immutable] [minimized] (name: runtime)
    asset static/common.3f62940b.bundle.js 204 bytes [emitted] [immutable] [minimized] (name: common)
    asset index.html 537 bytes [emitted]
    asset static/main.acdc2841.bundle.css 127 bytes [emitted] [immutable] [minimized] (name: main)

    我们可以看到代码分割到了不同的文件中,vender.js 包含了所有的第三方库,main.js 包含了我们各个页面的业务逻辑,公共代码在 common 中,runtime 包含了运行时代码,这样代码就分散到了不同的文件中,各司其职,且有利于同时进行加载。

    但是 main.js 还是包含了多个页面的代码,如果只是进入首页的话,其它页面的代码就是多余的,接下来再进行优化。

    按路由分割

    这一个比较容易处理,只需改变下路由配置即可,以 () => import(path) 的方式加载页面组件:

    const routes = [
    {
    path: '/',
    // component: Home,
    component: () => import('./pages/Home'),
    },
    {
    path: '/todos',
    // component: Todos,
    component: () => import('./pages/Todos'),
    },
    {
    path: '/about',
    // component: About,
    component: () => import('./pages/About'),
    },
    {
    path: '/404',
    // component: NotFound,
    component: () => import('./pages/NotFound'),
    },
    {
    path: '*',
    redirect: '/404',
    },
    ];

    此时打包会看到多了很多文件,这是把不同页面的代码分割到了不同的 JS 文件中,只有访问对应的页面才会加载相关的代码。

    assets by path static/*.js 142 KiB
    asset static/vendor.4391b08b.bundle.js 133 KiB [emitted] [immutable] [minimized] (name: vendor) (id hint: default)
    asset static/runtime.07c35c52.bundle.js 3.99 KiB [emitted] [immutable] [minimized] (name: runtime)
    asset static/821.7ba5112d.bundle.js 1.89 KiB [emitted] [immutable] [minimized]
    asset static/main.1697fd27.bundle.js 1.68 KiB [emitted] [immutable] [minimized] (name: main)
    asset static/820.de28fd7b.bundle.js 562 bytes [emitted] [immutable] [minimized]
    asset static/646.a902d0eb.bundle.js 406 bytes [emitted] [immutable] [minimized]
    asset static/114.26876aa2.bundle.js 402 bytes [emitted] [immutable] [minimized]
    asset static/common.3f62940b.bundle.js 204 bytes [emitted] [immutable] [minimized] (name: common)
    assets by path static/*.css 127 bytes
    asset static/main.beb1183a.bundle.css 75 bytes [emitted] [immutable] [minimized] (name: main)
    asset static/821.cd9a22a5.bundle.css 52 bytes [emitted] [immutable] [minimized]
    asset index.html 537 bytes [emitted]

    当然,这个地方可能会有争议,争议的地方就是:「页面进入时就把所有页面的代码都下载下来,再进入其它页面不是更快吗?」。这就取决于项目情况了,看是着重于页面秒开,还是着重于页面切换体验。如果着重于秒开的话,配合 SSR 处理效果会更好。

    更细粒度的分割

    如果对于页面打开速度或性能有更高的要求,还可以做更细粒度的代码分割,比如页面中功能模块的懒加载。

    这里以一个点击按钮时加载相应的组件为例,进行代码演示:

    这里有一个 Load Lazy Demo 按钮,点击时才加载 LazyComponent 组件,LazyComponent 组件并没有什么特别之处,写法跟普通组件一样。

    <template>
    <button @click="loadLazyDemo">Load Lazy Demo</button>
    <template v-if="showLazyComponent">
    <lazy-component />
    </template>
    </template>

    这里通过一个 showLazyComponent 控制组件的显示,当点击按钮时,把 showLazyComponent 置为 true,然后就加载 LazyComponent 对应的代码了。其实关键还是通过 () => import(path) 的方式引入组件。

    <script>
    export default {
    data() {
    return {
    showLazyComponent: false,
    };
    },
    methods: {
    loadLazyDemo() {
    this.showLazyComponent = true;
    },
    },
    components: {
    'lazy-component': () => import('../components/LazyComponent'),
    },
    };
    </script>

    K,以上就是我在 Vue 项目中做的代码分割的相关内容。

    原文链接:https://segmentfault.com/a/1190000039859930

    收起阅读 »

    高质量代码的原则

    简单性原则What:追求简单自始至终都以最简单的逻辑编写代码,让编程初学者一眼就能看懂。在编程时我们要重视的是局部的完整性,而不是复杂的整体关联性。Why:Bug 喜欢出现在复杂的地方软件故障常集中在某一个区域,而这些区域都有一个共同的特点,那就是复杂。编写代...
    继续阅读 »

    简单性原则

    What:追求简单

    自始至终都以最简单的逻辑编写代码,让编程初学者一眼就能看懂。在编程时我们要重视的是局部的完整性,而不是复杂的整体关联性。

    Why:Bug 喜欢出现在复杂的地方

    软件故障常集中在某一个区域,而这些区域都有一个共同的特点,那就是复杂。编写代码时如果追求简单易懂,代码就很难出现问题。不过,简单易懂的代码往往给人一种不够专业的感觉。这也是经验老到的程序员喜欢写老练高深的代码的原因。所以我们要有足够的定力来抵挡这种诱惑。

    Do:编写自然的代码

    放下高超的技巧,坚持用简单的逻辑编写代码。既然故障集中在代码复杂的区域,那我们只要让代码简单到让故障无处可藏即可。不要盲目地让代码复杂化、臃肿化,要保证代码简洁。

    同构原则

    What:力求规范

    同等对待相同的东西,坚持不搞特殊。同等对待,举例来说就是同一个模块管理的数值全部采用同一单位、公有函数的参数个数统一等。

    Why:不同的东西会更显眼

    相同的东西用相同的形式表现能够使不同的东西更加突出。不同的东西往往容易产生 bug。遵循同构原则能让我们更容易嗅出代码的异样,从而找出问题所在。
    统一的代码颇具美感,而美的东西一般更容易让人接受,因此统一的代码有较高的可读性。

    Do:编写符合规范的代码

    可靠与简单是代码不可或缺的性质,在编写代码时,务必克制住自己的表现欲,以规范为先。

    对称原则

    What:讲究形式上的对称

    在思考一个处理时,也要想到与之成对的处理。比如有给标志位置 1 的处理,就要有给标志位置 0 的处理。

    Why:帮助读代码的人推测后面的代码

    具有对称性的代码能够帮助读代码的人推测后面的代码,提高其理解代码的速度。同时,对称性会给代码带来美感,这同样有助于他人理解代码。
    此外,设计代码时将对称性纳入考虑的范围能防止我们在思考问题时出现遗漏。如果说代码的条件分支是故障的温床,那么对称性就是思考的框架,能有效阻止条件遗漏。

    Do:编写有对称性的代码

    在出现“条件”的时候,我们要注意它的“反条件”。每个控制条件都存在与之成对的反条件(与指示条件相反的条件)。要注意条件与反条件的统一,保证控制条件具有统一性。
    我们还要考虑到例外情况并极力避免其发生。例外情况的特殊性会破坏对称性,成为故障的温床。特殊情况过多意味着需求没有得到整理。此时应重新审视需求,尽量从代码中剔除例外情况。
    命名也要讲究对称性。命名时建议使用 set/getstart/stopbegin/ end 和 push/pop 等成对的词语。

    层次原则

    What:讲究层次

    注意事物的主从关系、前后关系和本末关系等层次关系,整理事物的关联性。
    不同层次各司其职,同种处理不跨越多个层次,这一点非常重要。比如执行了获取资源的处理,那么释放资源的处理就要在相同的层次进行。又比如互斥控制的标志位置 1 和置 0 的处理要在同一层次进行。

    Why:层次结构有助于提高代码的可读性

    有明确层次结构的代码能帮助读代码的人抽象理解代码的整体结构。读代码的人可以根据自身需要阅读下一层次的代码,掌握更加详细的信息。
    这样可以提高代码的可读性,帮助程序员表达编码意图,降低 bug 发生的概率。

    Do:编写有抽象层次结构的代码

    在编写代码时设计各部分的抽象程度,构建层次结构。保证同一个层次中的所有代码抽象程度相同。另外,高层次的代码要通过外部视角描述低层次的代码。这样做能让调用低层次代码的高层次代码更加简单易懂。

    线性原则

    What:处理流程尽量走直线

    一个功能如果可以通过多个功能的线性结合来实现,那它的结构就会非常简单。
    反过来,用条件分支控制代码、毫无章法地增加状态数等行为会让代码变得难以理解。我们要避免做出这些行为,提高代码的可读性。

    Why:直线处理可提高代码的可读性

    复杂的处理流程是故障的温床。故障多出现在复杂的条件语句和循环语句中。另外,goto 等让流程出现跳跃的语句也是故障的多发地。
    如果能让处理由高层次流向低层次,一气呵成,代码的可读性就会大幅提高。与此同时,可维护性也将提高,添加功能等改良工作将变得更加容易。
    一般来说,自上而下的处理流程简单明快,易于理解。我们应避开复杂反复的处理流程。

    Do:尽量不在代码中使用条件分支

    尽量减少条件分支的数量,编写能让代码阅读者线性地看完整个处理流程的代码。
    为此,我们需要把一些特殊的处理拿到主处理之外。保证处理的统一性,注意处理的流程。记得时不时俯瞰代码整体,检查代码是否存在过于复杂的部分。
    另外,对于经过长期维护而变得过于复杂的部分,我们可以考虑对其进行重构。明确且可靠的设计不仅对我们自身有益,还可以给负责维护的人带来方便。

    清晰原则

    What:注意逻辑的清晰性

    逻辑具有清晰性就代表逻辑能清楚证明自身的正确性。也就是说,我们编写的代码要让人一眼就能判断出没有问题。任何不明确的部分都要附有说明。

    Why:消除不确定性

    代码免不了被人一遍又一遍地阅读,所以代码必须保持较高的可读性。编写代码时如果追求高可读性,我们就不会采用取巧的方式编写代码,编写出的代码会非常自然。代码是给人看的,也是由人来修改的,所以我们必须以人为对象来编写代码。消除代码的不确定性是对自己的作品负责,这么做也可以为后续负责维护的人提供方便。

    Do:编写逻辑清晰的代码

    我们应选用直观易懂的逻辑。会给读代码的人带来疑问的部分要么消除,要么加以注释。另外,我们应使用任何人都能立刻理解且不存在歧义的术语。要特别注意变量名等一定不能没有意义。

    安全原则

    What:注意安全性

    就是在编写代码时刻意将不可能的条件考虑进去。比如即便某个 if 语句一定成立,我们也要考虑 else 语句的情况;即便某个 case 语句一定成立,我们也要考虑 default 语句的情况;即便某个变量不可能为空,我们也要检查该变量是否为 null

    Why:防止故障发展成重大事故

    硬件提供的服务必须保证安全,软件也一样。硬件方面,比如取暖器,为防止倾倒起火,取暖器一般会配有倾倒自动断电装置。同样,设计软件时也需要考虑各种情况,保证软件在各种情况下都能安全地运行。这一做法在持续运营服务和防止数据损坏等方面有着积极的意义。

    Do:编写安全的代码

    选择相对安全的方法对具有不确定性的部分进行设计。列出所有可能的运行情况,确保软件在每种情况下都能安全运行。理解需求和功能,将各种情况正确分解到代码中,这样能有效提高软件安全运行的概率。
    为此,我们也要将不可能的条件视为考察对象,对其进行设计和编程。不过,为了统一标准,我们在编写代码前最好规定哪些条件需要写,哪些条件不需要写。


    原文链接:https://segmentfault.com/a/1190000039864589

    收起阅读 »

    TS实用工具类型

    Partial<Type>构造类型Type,并将它所有的属性设置为可选的。它的返回类型表示输入类型的所有子类型。例子interface Todo { title: string; description: string; } fu...
    继续阅读 »

    Partial<Type>

    构造类型Type,并将它所有的属性设置为可选的。它的返回类型表示输入类型的所有子类型。

    例子

    interface Todo {
    title: string;
    description: string;
    }

    function updateTodo(todo: Todo, fieldsToUpdate: Partial<Todo>) {
    return { ...todo, ...fieldsToUpdate };
    }

    const todo1 = {
    title: 'organize desk',
    description: 'clear clutter',
    };

    const todo2 = updateTodo(todo1, {
    description: 'throw out trash',
    });

    Readonly<Type>

    构造类型Type,并将它所有的属性设置为readonly,也就是说构造出的类型的属性不能被再次赋值。

    例子

    interface Todo {
    title: string;
    }

    const todo: Readonly<Todo> = {
    title: 'Delete inactive users',
    };

    todo.title = 'Hello'; // Error: cannot reassign a readonly property

    这个工具可用来表示在运行时会失败的赋值表达式(比如,当尝试给冻结对象的属性再次赋值时)。

    Object.freeze

    function freeze<T>(obj: T): Readonly<T>;

    Record<Keys, Type>

    构造一个类型,其属性名的类型为K,属性值的类型为T。这个工具可用来将某个类型的属性映射到另一个类型上。

    例子

    interface PageInfo {
    title: string;
    }

    type Page = 'home' | 'about' | 'contact';

    const x: Record<Page, PageInfo> = {
    about: { title: 'about' },
    contact: { title: 'contact' },
    home: { title: 'home' },
    };

    Pick<Type, Keys>

    从类型Type中挑选部分属性Keys来构造类型。

    例子

    interface Todo {
    title: string;
    description: string;
    completed: boolean;
    }

    type TodoPreview = Pick<Todo, 'title' | 'completed'>;

    const todo: TodoPreview = {
    title: 'Clean room',
    completed: false,
    };

    Omit<Type, Keys>

    从类型Type中获取所有属性,然后从中剔除Keys属性后构造一个类型。

    例子

    interface Todo {
    title: string;
    description: string;
    completed: boolean;
    }

    type TodoPreview = Omit<Todo, 'description'>;

    const todo: TodoPreview = {
    title: 'Clean room',
    completed: false,
    };

    Exclude<Type, ExcludedUnion>

    从类型Type中剔除所有可以赋值给ExcludedUnion的属性,然后构造一个类型。

    例子

    type T0 = Exclude<'a' | 'b' | 'c', 'a'>; // "b" | "c"
    type T1 = Exclude<'a' | 'b' | 'c', 'a' | 'b'>; // "c"
    type T2 = Exclude<string | number | (() => void), Function>; // string | number

    Extract<Type, Union>

    从类型Type中提取所有可以赋值给Union的类型,然后构造一个类型。

    例子

    type T0 = Extract<'a' | 'b' | 'c', 'a' | 'f'>; // "a"
    type T1 = Extract<string | number | (() => void), Function>; // () => void

    NonNullable<Type>

    从类型Type中剔除nullundefined,然后构造一个类型。

    例子

    type T0 = NonNullable<string | number | undefined>; // string | number
    type T1 = NonNullable<string[] | null | undefined>; // string[]

    Parameters<Type>

    由函数类型Type的参数类型来构建出一个元组类型。

    例子

    declare function f1(arg: { a: number; b: string }): void;

    type T0 = Parameters<() => string>;
    // []
    type T1 = Parameters<(s: string) => void>;
    // [s: string]
    type T2 = Parameters<<T>(arg: T) => T>;
    // [arg: unknown]
    type T3 = Parameters<typeof f1>;
    // [arg: { a: number; b: string; }]
    type T4 = Parameters<any>;
    // unknown[]
    type T5 = Parameters<never>;
    // never
    type T6 = Parameters<string>;
    // never
    // Type 'string' does not satisfy the constraint '(...args: any) => any'.
    type T7 = Parameters<Function>;
    // never
    // Type 'Function' does not satisfy the constraint '(...args: any) => any'.

    ConstructorParameters<Type>

    由构造函数类型来构建出一个元组类型或数组类型。
    由构造函数类型Type的参数类型来构建出一个元组类型。(若Type不是构造函数类型,则返回never)。

    例子

    type T0 = ConstructorParameters<ErrorConstructor>;
    // [message?: string | undefined]
    type T1 = ConstructorParameters<FunctionConstructor>;
    // string[]
    type T2 = ConstructorParameters<RegExpConstructor>;
    // [pattern: string | RegExp, flags?: string | undefined]
    type T3 = ConstructorParameters<any>;
    // unknown[]

    type T4 = ConstructorParameters<Function>;
    // never
    // Type 'Function' does not satisfy the constraint 'new (...args: any) => any'.

    ReturnType<Type>

    由函数类型Type的返回值类型构建一个新类型。

    例子

    type T0 = ReturnType<() => string>;  // string
    type T1 = ReturnType<(s: string) => void>; // void
    type T2 = ReturnType<(<T>() => T)>; // {}
    type T3 = ReturnType<(<T extends U, U extends number[]>() => T)>; // number[]
    type T4 = ReturnType<typeof f1>; // { a: number, b: string }
    type T5 = ReturnType<any>; // any
    type T6 = ReturnType<never>; // any
    type T7 = ReturnType<string>; // Error
    type T8 = ReturnType<Function>; // Error

    InstanceType<Type>

    由构造函数类型Type的实例类型来构建一个新类型。

    例子

    class C {
    x = 0;
    y = 0;
    }

    type T0 = InstanceType<typeof C>; // C
    type T1 = InstanceType<any>; // any
    type T2 = InstanceType<never>; // any
    type T3 = InstanceType<string>; // Error
    type T4 = InstanceType<Function>; // Error

    Required<Type>

    构建一个类型,使类型Type的所有属性为required
    与此相反的是Partial

    例子

    interface Props {
    a?: number;
    b?: string;
    }

    const obj: Props = { a: 5 }; // OK

    const obj2: Required<Props> = { a: 5 }; // Error: property 'b' missing

    ThisParameterType<Type>

    从函数类型中提取 this 参数的类型。
    若函数类型不包含 this 参数,则返回 unknown 类型。

    例子

    function toHex(this: Number) {
    return this.toString(16);
    }

    function numberToString(n: ThisParameterType<typeof toHex>) {
    return toHex.apply(n);
    }

    OmitThisParameter<Type>

    Type类型中剔除 this 参数。
    若未声明 this 参数,则结果类型为 Type 。
    否则,由Type类型来构建一个不带this参数的类型。
    泛型会被忽略,并且只有最后的重载签名会被采用。

    例子

    function toHex(this: Number) {
    return this.toString(16);
    }

    const fiveToHex: OmitThisParameter<typeof toHex> = toHex.bind(5);

    console.log(fiveToHex());

    ThisType<Type>

    这个工具不会返回一个转换后的类型。
    它作为上下文的this类型的一个标记。
    注意,若想使用此类型,必须启用--noImplicitThis

    例子

    // Compile with --noImplicitThis

    type ObjectDescriptor<D, M> = {
    data?: D;
    methods?: M & ThisType<D & M>; // Type of 'this' in methods is D & M
    };

    function makeObject<D, M>(desc: ObjectDescriptor<D, M>): D & M {
    let data: object = desc.data || {};
    let methods: object = desc.methods || {};
    return { ...data, ...methods } as D & M;
    }

    let obj = makeObject({
    data: { x: 0, y: 0 },
    methods: {
    moveBy(dx: number, dy: number) {
    this.x += dx; // Strongly typed this
    this.y += dy; // Strongly typed this
    },
    },
    });

    obj.x = 10;
    obj.y = 20;
    obj.moveBy(5, 5);

    上面例子中,makeObject参数里的methods对象具有一个上下文类型ThisType<D & M>,因此methods对象的方法里this的类型为{ x: number, y: number } & { moveBy(dx: number, dy: number): number }

    lib.d.ts里,ThisType<T>标识接口是个简单的空接口声明。除了在被识别为对象字面量的上下文类型之外,这个接口与一般的空接口没有什么不同。

    原文链接:https://segmentfault.com/a/1190000039868550

    收起阅读 »

    怎么获取到环信老版本的SDK和Demo

    来到环信官网的下载页面:下载-即时通讯云-环信 找到想要下载的sdk,以iOS端为例,右键“SDK+Demo源码”,拷贝链接,然后修改链接里的版本号即可例如:https://download-sdk.oss-cn-beijing.aliyuncs.com/do...
    继续阅读 »

    来到环信官网的下载页面:下载-即时通讯云-环信

    找到想要下载的sdk,以iOS端为例,右键“SDK+Demo源码”,拷贝链接,然后修改链接里的版本号即可
    例如:https://download-sdk.oss-cn-beijing.aliyuncs.com/downloads/iOS_IM_SDK_V3.7.4.zip

    收起阅读 »

    (IM)iOS端离线推送收不到怎么办?

    离线推送收不到,按照下面步骤一步一步进行排查: 0、如果你的app之前可以收到离线推送,突然收不到了,那么先移步苹果开发者中心查看推送证书是否过期。如果过期了,需要重新制作证书,然后到环信管理后台(Console)将旧的删掉再上传新的。过期的一般会被封禁,需要...
    继续阅读 »

    离线推送收不到,按照下面步骤一步一步进行排查:


    0、如果你的app之前可以收到离线推送,突然收不到了,那么先移步苹果开发者中心查看推送证书是否过期。如果过期了,需要重新制作证书,然后到环信管理后台(Console)将旧的删掉再上传新的。过期的一般会被封禁,需要联系环信进行解封操作。


    1、首先已经按照环信的文档集成了离线推送:APNs离线推送


    2、如果是iOS13及以上的系统,那么需要将IM SDK更新到3.6.4或以上版本。
    如果更新后还不行那么退出登录、重启app、再登录试下。
    初始化sdk成功之后打印版本号:
    NSString *ver = [EMClient sharedClient].version;

    3、测试APNs推送的时候,接收方的APP需要是杀死状态,需要用户长连接断开才会发APNs推送;
    **所以直接上划杀死APP测试。**


    4、要确保导出p12时使用的Mac和创建CertificateSigningRequest.certSigningRequest文件的Mac是同一台;导出证书的时候要直接点击导出,不要点击秘钥的内容导出;确认 APP ID 是否带有推送功能;


    5、环信管理后台(Console)上传证书时填写的Bundle ID须与工程中的Bundle ID、推送证书的 APP ID 相同;选择的证书类型须与推送证书的环境一致;导出.p12文件需要设置密码,并在上传管理后台时传入;


    6、工程中初始化SDK那里填的证书名与环信管理后台上传的证书名称必须是相同的;


    7、测试环境测试,需要使用development环境的推送证书,Xcode直接真机运行;
    正式环境测试,需要使用production环境的推送证书,而且要打包,打包时选择Ad Hoc,导出IPA安装到手机上。

    8、APP杀死后可调用“获取单个用户”的rest接口,确认证书名称是否有绑定(正常情况下,登录成功后会绑定上推送证书,绑定后会显示推送证书名称);还需要确认绑定的证书名称和管理后台上传的证书名称是否一致。


    接口文档:获取单个用户
    获取用户信息
    如果没绑定上,那么退出登录、重启app、重新登录再试下。

    如果证书名称不一致,改正过来后重新登录试下。


    9、如果以上都确认无误,可以联系环信排查。需提供以下信息(请勿遗漏,以免反复询问耽误时间):
    appkey、devicetoken、bundle id、证书的.p12文件、证书名称、证书密码、收不到推送的环信id、测试的环境(development or production)、消息id、消息的内容和发送时间

    消息id要在消息发送成功后获取,如图:
    获取消息id
    收起阅读 »

    线上直播 | iOS Runtime 项目实际应用与面试对刚!

    直播主题:iOS  Runtime 项目实际应用与面试对刚!直播时间:4月28日 19:00 嘉宾介绍:Zuyu    环信生态开发者kol直播亮点:1. 如何使用runtime ...
    继续阅读 »

    直播主题:

    iOS  Runtime 项目实际应用与面试对刚!

    直播时间:

    4月28日 19:00 

    嘉宾介绍:

    Zuyu    环信生态开发者kol

    直播亮点:

    1. 如何使用runtime 动态创建类

    2. 如何使用runtime 进行hook

    3. Method Swizzling 误区详解 ,让你面试或开发如虎添翼


    欢迎大家进iOS开发交流群~~群里各个都是人才,说话还好听。

    直播过程中还会抽礼物~


    海报428.jpg



    收起阅读 »

    java设计模式:状态模式

    定义对有状态的对象,把复杂的“判断逻辑”提取到不同的状态对象中,允许状态对象在其内部状态发生改变时改变其行为。优点结构清晰,状态模式将与特定状态相关的行为局部化到一个状态中,并且将不同状态的行为分割开来,满足“单一职责原则”。将状态转换显示化,减少对象间的相互...
    继续阅读 »

    定义

    对有状态的对象,把复杂的“判断逻辑”提取到不同的状态对象中,允许状态对象在其内部状态发生改变时改变其行为。

    优点

    1. 结构清晰,状态模式将与特定状态相关的行为局部化到一个状态中,并且将不同状态的行为分割开来,满足“单一职责原则”。
    2. 将状态转换显示化,减少对象间的相互依赖。将不同的状态引入独立的对象中会使得状态转换变得更加明确,且减少对象间的相互依赖。
    3. 状态类职责明确,有利于程序的扩展。通过定义新的子类很容易地增加新的状态和转换。

    缺点

    1. 状态模式的使用必然会增加系统的类与对象的个数。
    2. 状态模式的结构与实现都较为复杂,如果使用不当会导致程序结构和代码的混乱。
    3. 状态模式对开闭原则的支持并不太好,对于可以切换状态的状态模式,增加新的状态类需要修改那些负责状态转换的源码,否则无法切换到新增状态,而且修改某个状态类的行为也需要修改对应类的源码。

    代码实现

    状态模式包含以下主要角色。

    1. 环境类:也称为上下文,它定义了客户端需要的接口,内部维护一个当前状态,并负责具体状态的切换。
    2. 抽象状态:定义一个接口,用以封装环境对象中的特定状态所对应的行为,可以有一个或多个行为。
    3. 具体状态:实现抽象状态所对应的行为,并且在需要的情况下进行状态切换。
    状态模式中,行为是由状态来决定的,不同状态下有不同行为。

    举个例子把,比如电视,电视有2个状态,一个是开机,一个是关机,开机时可以切换频道,关机时切换频道不做任何响应。
    public interface TvState{
    public void nextChannerl();
    public void prevChannerl();
    public void turnUp();
    public void turnDown();
    }

    public class PowerOffState implements TvState{
    public void nextChannel(){}
    public void prevChannel(){}
    public void turnUp(){}
    public void turnDown(){}

    }


    public class PowerOnState implements TvState{
    public void nextChannel(){
    System.out.println("下一频道");
    }
    public void prevChannel(){
    System.out.println("上一频道");
    }
    public void turnUp(){
    System.out.println("调高音量");
    }
    public void turnDown(){
    System.out.println("调低音量");
    }

    }

    public interface PowerController{
    public void powerOn();
    public void powerOff();
    }

    public class TvController implements PowerController{
    TvState mTvState;
    public void setTvState(TvStete tvState){
    mTvState=tvState;
    }
    public void powerOn(){
    setTvState(new PowerOnState());
    System.out.println("开机啦");
    }
    public void powerOff(){
    setTvState(new PowerOffState());
    System.out.println("关机啦");
    }
    public void nextChannel(){
    mTvState.nextChannel();
    }
    public void prevChannel(){
    mTvState.prevChannel();
    }
    public void turnUp(){
    mTvState.turnUp();
    }
    public void turnDown(){
    mTvState.turnDown();
    }

    }


    public class Client{
    public static void main(String[] args){
    TvController tvController=new TvController();
    tvController.powerOn();
    tvController.nextChannel();
    tvController.turnUp();

    tvController.powerOff();
    //调高音量,此时不会生效
    tvController.turnUp();
    }


    }

    应用场景

    通常在以下情况下可以考虑使用状态模式。
    • 当一个对象的行为取决于它的状态,并且它必须在运行时根据状态改变它的行为时,就可以考虑使用状态模式。
    • 一个操作中含有庞大的分支结构,并且这些分支决定于对象的状态时。

    例子:

    应用在已登录状态,点击评论,会正常弹出评论框,而未登录状态下,则是要跳转到登录界面登录后,再正常评论。

    所以已登录和未登录状态下的评论行为是不同的,这个就可以用状态模式设计。

    收起阅读 »

    android 逆向工程-分析篇 漏洞与风险(四)

    Android逆向工程篇:android 逆向工程-工具篇 drozer(一)android 逆向工程-工具篇 apktool(二)android 逆向工程-语言篇 Smali(三)android 逆向工程-分析篇 漏洞与风险(四)android 逆向工程-工...
    继续阅读 »

    Android逆向工程篇:


    风险与漏洞:

    1.静态破解获取源码
    通过各种工具可以对未加密应用反编译出源码

    2.二次打包
    将反编译出的源码,进行修改,再次打包,签名

    3.输入监听
    通过adb shell getevent/sendevent 对用户数据进行窃取,存储,编辑

    4.页面截图
    通过截图来获取用户隐私信息

    5.获取本地存储的数据库,缓存文件
    对本地应用储存的数据进行窃取、编辑、存储。

    6.网络数据抓取
    通过数据抓包来获取网络中的数据,对数据进行截获,重放,编辑,存储。
    收起阅读 »

    android 逆向工程-语言篇 Smali(三)

    Android逆向工程篇:android 逆向工程-工具篇 drozer(一)android 逆向工程-工具篇 apktool(二)android 逆向工程-语言篇 Smali(三)android 逆向工程-分析篇 漏洞与风险(四)android 逆向工程-工...
    继续阅读 »

    Android逆向工程篇:


    数据类型

    • B---byte
    • C---char
    • D---double
    • F---float
    • I---int
    • J---long
    • S---short
    • V---void
    • Z---boolean
    • [XXX---array
    • Lxxx/yyy---object

    基本语法

    .field private isFlag:z定义变量
    .method方法
    .parameter方法参数
    .prologue方法开始
    .line 12此方法位于第12行
    invoke-super调用父函数
    const/high16  v0, 0x7fo3把0x7fo3赋值给v0
    invoke-direct调用函数
    return-void函数返回void
    .end method函数结束
    new-instance创建实例
    iput-object对象赋值
    iget-object调用对象
    invoke-static调用静态函数

    条件跳转分支:

    if-eq vA, vB, :cond_**如果vA等于vB则跳转到:cond_**
    if-ne vA, vB, :cond_**如果vA不等于vB则跳转到:cond_**
    if-lt vA, vB, :cond_**如果vA小于vB则跳转到:cond_**
    if-ge vA, vB, :cond_**如果vA大于等于vB则跳转到:cond_**
    if-gt vA, vB, :cond_**如果vA大于vB则跳转到:cond_**
    if-le vA, vB, :cond_**如果vA小于等于vB则跳转到:cond_**
    if-eqz vA, :cond_**如果vA等于0则跳转到:cond_**
    if-nez vA, :cond_**如果vA不等于0则跳转到:cond_**
    if-ltz vA, :cond_**如果vA小于0则跳转到:cond_**
    if-gez vA, :cond_**如果vA大于等于0则跳转到:cond_**
    if-gtz vA, :cond_**如果vA大于0则跳转到:cond_**
    if-lez vA, :cond_**如果vA小于等于0则跳转到:cond_**

    smali文件格式

    .class < 访问权限> [ 修饰关键字] < 类名>  
    .
    super < 父类名>
    .
    source <源文件名>

    MainActivity.smali展示

    .class public Lcom/droider/crackme0502/MainActivity;     //指令指定了当前类的类名。  
    .super Landroid/app/Activity; //指令指定了当前类的父类。
    .source "MainActivity.java" //指令指定了当前类的源文件名。

    smali文件中字段的声明使用“.field”指令。字段有静态字段与实例字段两种。静态字段的声明格式如下:

    .field < 访问权限> static [ 修饰关键字] < 字段名>:< 字段类型>  

    实例字段的声明与静态字段类似,只是少了static关键字,它的格式如下:

    .field < 访问权限> [ 修饰关键字] < 字段名>:< 字段类型>  

    比如以下的实例字段声明。  

    .field private btnAnno:Landroid/widget/Button;  //私有字段  

    smali 文件中方法的声明使用“.method ”指令,方法有直接方法与虚方法两种。

    .method <访问权限> [ 修饰关键字] < 方法原型>  
    <.locals> //指定了使用的局部变量的个数
    [.parameter] //指定了方法的参数
    [.prologue] //指定了代码的开始处,混淆过的代码可能去掉了该指令
    [.line] //指定了该处指令在源代码中的行号
    <代码体>
    .end method

    虚方法的声明与直接方法相同,只是起始处的注释为“virtual methods”,如果一个类实现了接口,会在smali 文件中使用“.implements ”指令指出,相应的格式声明如下:

    .implements < 接口名>        //接口关键字  

    如果一个类使用了注解,会在 smali 文件中使用“.annotation ”指令指出,注解的格式声明如下:  

    .annotation [ 注解属性] < 注解类名>  
    [ 注解字段 = 值]
    .end annotation

    注解的作用范围可以是类、方法或字段。如果注解的作用范围是类,“.annotation ”指令会直接定义在smali 文件中,如果是方法或字段,“.annotation ”指令则会包含在方法或字段定义中。例如:

    .field public sayWhat:Ljava/lang/String;            //String 类型 它使用了 com.droider.anno.MyAnnoField 注解,注解字段info 值为“Hello my friend”  
    .annotation runtime Lcom/droider/anno/MyAnnoField;
    info = "Hello my friend"
    .end annotation
    .end field


    收起阅读 »

    android 逆向工程-工具篇 apktool(二)

    Android逆向工程篇:android 逆向工程-工具篇 drozer(一)android 逆向工程-工具篇 apktool(二)android 逆向工程-语言篇 Smali(三)android 逆向工程-分析篇 漏洞与风险(四)android 逆向工程-工...
    继续阅读 »

    Android逆向工程篇:


    注:java环境自行安装

    apktool 下载

    Apktool

    运行

    apk文件 F:\apktools\demo.apk

    导出目录 F:\apktools\demo

    解包执行

    java -jar apktool.jar d -f F:\apktools\demo.apk -o F:\apktools\demo

    结果

    assets(未被编译) assets文件
    res(未被编译) res文件
    smali(被编译) smali格式文件

    二次打包执行

     java -jar  apktool.jar b F:\apktools\demo

    重新打包后的apk在要打包的文件夹里的dist目录下

    这样打完的apk是没有签名的

    jarsigner 签名apk

    jarsigner -verbose -keystore signapk.keystore -signedjar signapk_new.apk signapk.apk keyAlias

    注意:

    signapk.keystore 自己的签名证书
    signapk_new.apk  签名成功之后输出的apk名称
    signapk.apk 输入的待签名的apk
    keyAlias keyAlias名称

    收起阅读 »

    android 逆向工程-工具篇 drozer(一)

    Android逆向工程篇:android 逆向工程-工具篇 drozer(一)android 逆向工程-工具篇 apktool(二)android 逆向工程-语言篇 Smali(三)android 逆向工程-分析篇 漏洞与风险(四)android 逆向工程-工...
    继续阅读 »

    注:adb,java环境请自行配制好

    drozer 官方网站:https://labs.mwrinfosecurity.com/tools/drozer/

    下载安装以下两个文件:


    android 端:安装好 drozer.apk后,点击开启服务

    pc端:

    使用 forward 端口映射 将31415端口进行映射

    adb forward tcp:31415 tcp:31415
     

    drozer目录下cmd 运行 drozer console connect


    特殊(出现java环境没有找到):

    按照提示在 用户目录下添加 .drozer_config文件 内容如下:

    [executables]
    java=D:\Java\jdk1.7.0_65\bin\java.exe
    javac=D:\Java\jdk1.7.0_65\bin\javac.exe

    命令:

    list或ls 查看可用模块 

    run app.package.list  查看程序安装包  

    run app.package.info -a com.example.myapp  查看程序包信息  

    run app.package.attacksurface com.example.myapp  查看项目对外exported的组件 

    run app.activity.info -a com.example.myapp  查看对外activity的信息  

    run app.service.info -a com.example.myapp  查看对外service的信息  

    run app.broadcast.info -a com.example.myapp  查看对外broadcast的信息  

    run app.provider.info -a com.example.myapp  查看对外provider的信息  

    run app.activity.start --component com.example.myapp com.example.MainActivity 尝试启动MainActivity  

    run scanner.provider.finduris -a com.example.myapp 探测出可以查询的URI  

    run app.provider.query content://***/ -vertical  获取Uri的数据  

    run app.provider.insert URI对应数据表中的字段    对数据库表进行插入操作

    run app.provider.delete URI-selection “条件” 对数据库表进行删除操作

    Content Providers(SQL注入)
    run app.provider.query content://***/ --projection "'"
    run app.provider.query content://***/ --selection "'"

    报错则说明存在SQL注入。
    run app.provider.query content://***/ --projection "* FROM SQLITE_MASTER WHERE type='table';--"
    获取某个表(如Key)中的数据:
    run app.provider.query content://***/ --projection "* FROM Key;--"
    同时检测SQL注入和目录遍历
    run scanner.provider.injection -a com.example.myapp
    run scanner.provider.traversal -a com.example.myapp
     
    收起阅读 »

    java设计模式:原型模式

    定义用一个已经创建的实例作为原型,通过复制该原型对象来创建一个和原型相同或相似的新对象。在这里,原型实例指定了要创建的对象的种类。用这种方式创建对象非常高效,根本无须知道对象创建的细节。 优点 Java 自带的原型模式基于内存二进制流的复制,在性能上比直接 n...
    继续阅读 »

    定义

    用一个已经创建的实例作为原型,通过复制该原型对象来创建一个和原型相同或相似的新对象。在这里,原型实例指定了要创建的对象的种类。用这种方式创建对象非常高效,根本无须知道对象创建的细节。


    优点


    • Java 自带的原型模式基于内存二进制流的复制,在性能上比直接 new 一个对象更加优良。
    • 可以使用深克隆方式保存对象的状态,使用原型模式将对象复制一份,并将其状态保存起来,简化了创建对象的过程,以便在需要的时候使用(例如恢复到历史某一状态),可辅助实现撤销操作。

      缺点

    • 需要为每一个类都配置一个 clone 方法
    • clone 方法位于类的内部,当对已有类进行改造的时候,需要修改代码,违背了开闭原则。
    • 当实现深克隆时,需要编写较为复杂的代码,而且当对象之间存在多重嵌套引用时,为了实现深克隆,每一层对象对应的类都必须支持深克隆,实现起来会比较麻烦。因此,深克隆、浅克隆需要运用得当。

      代码实现

      原型模式包含以下主要角色。


    1. 抽象原型类:规定了具体原型对象必须实现的接口。
    2. 具体原型类:实现抽象原型类的 clone() 方法,它是可被复制的对象。
    3. 访问类:使用具体原型类中的 clone() 方法来复制新的对象。

    在这里插入图片描述

    浅克隆与深克隆


    • 浅克隆:创建一个新对象,新对象的属性和原来对象完全相同,对于非基本类型属性,仍指向原有属性所指向的对象的内存地址。
    • 深克隆:创建一个新对象,属性中引用的其他对象也会被克隆,不再指向原有对象地址。

    //具体原型类
    class Realizetype implements Cloneable {
    Realizetype() {
    System.out.println("具体原型创建成功!");
    }
    public Object clone() throws CloneNotSupportedException {
    System.out.println("具体原型复制成功!");
    return (Realizetype) super.clone();
    }
    }
    //原型模式的测试类
    public class PrototypeTest {
    public static void main(String[] args) throws CloneNotSupportedException {
    Realizetype obj1 = new Realizetype();
    Realizetype obj2 = (Realizetype) obj1.clone();
    System.out.println("obj1==obj2?" + (obj1 == obj2));
    }
    }

    输出


    具体原型创建成功!
    具体原型复制成功!
    obj1==obj2?false

    应用场景


    • 对象之间相同或相似,即只是个别的几个属性不同的时候。
    • 创建对象成本较大,例如初始化时间长,占用CPU太多,或者占用网络资源太多等,需要优化资源。
    • 创建一个对象需要繁琐的数据准备或访问权限等,需要提高性能或者提高安全性。
    • 系统中大量使用该类对象,且各个调用者都需要给它的属性重新赋值。

    收起阅读 »

    扫盲:Kotlin 的泛型(2)

    Kotlin 的 out 和 in 和 Java 泛型一样,Kolin 中的泛型本身也是不可变的。 不过换了一种表现形式: 使用关键字 out 来支持协变,等同于 Java 中的上界通配符 ? extends。 使用关键字 in 来支持逆变,等同于 Java...
    继续阅读 »

    Kotlin 的 out 和 in


    和 Java 泛型一样,Kolin 中的泛型本身也是不可变的。


    不过换了一种表现形式:



    • 使用关键字 out 来支持协变,等同于 Java 中的上界通配符 ? extends

    • 使用关键字 in 来支持逆变,等同于 Java 中的下界通配符 ? super


    val appleShop: Shop<out Fruit>
    val fruitShop: Shop<in Apple>
    复制代码

    它们完全等价于:


    Shop<? extends Fruit> appleShop;
    Shop<? super Apple> fruitShop;
    复制代码

    换了个写法,但作用是完全一样的。out 表示,我这个变量或者参数只用来输出,不用来输入,你只能读我不能写我;in 就反过来,表示它只用来输入,不用来输出,你只能写我不能读我。


    泛型的上下界约束


    上面讲的都是在使用的时候再对泛型进行限制,我们称之为「上界通配符」和「下界通配符」。那我们可以在函数设计的时候,就设置这个限制么?


    可以的可以的。


    比如:


    open class Animal
    class PetShop<T : Animal?>(val t: T)
    复制代码

    等同于 Java 的:


    class PetShop<T extends Animal> {
    private T t;

    PetShop(T t) {
    this.t = t;
    }
    }
    复制代码

    这样,我们在设计宠物店类 PetShop 就给支持的泛型设置了上界约束,支持的泛型类型必须是 Animal 的子类。所以我们使用的话:


    class Cat : Animal()

    val catShop = PetShop(Cat())
    val appleShop = PetShop(Apple())
    // 👆 报错:Type mismatch. Required: Animal? Found: Apple
    复制代码

    很明显,Apple 并不是 Animal 的子类,当然不满足 PetShop 泛型类型的上界约束。


    那....可以设置多个上界约束么?


    当然可以,在 Java 中,给一个泛型参数声明多个约束的方式是,使用 &


    class PetShop<T extends Animal & Serializable> {
    // 👆 通过 & 实现了两个上界,必须是 Animal 和 Serializable 的子类或实现类
    private T t;

    PetShop(T t) {
    this.t = t;
    }
    }
    复制代码

    而在 Kotlin 中舍弃了 & 这种方式,而是增加了 where 关键字:


    open class Animal
    class PetShop<T>(val t: T) where T : Animal?, T : Serializable
    复制代码

    通过上面的方式,就实现了多个上界的约束。


    Kotlin 的通配符 *


    前面我们说的泛型类型都是在我们需要知道参数类型是什么类型的,那如果我们对泛型参数的类型不感兴趣,有没有一种方式处理这个情况呢?


    有的有的。


    在 Kotlin 中,可以用通配符 * 来替代泛型参数。比如:


    val list: MutableList<*> = mutableListOf(1, "nanchen2251")
    list.add("nanchen2251")
    // 👆 报错:Type mismatch. Required: Nothing Found: String
    复制代码

    这个报错确实让人匪夷所思,上面用通配符代表了 MutableList 的泛型参数类型。初始化里面也加入了 String 类型,但在新 add 字符串的时候,却发生了编译错误。


    而如果是这样的代码:


    val list: MutableList<Any> = mutableListOf(1, "nanchen2251")
    list.add("nanchen2251")
    // 👆 不再报错
    复制代码

    看来,所谓的通配符作为泛型参数并不等价于 Any 作为泛型参数。MutableList<*>MutableList<Any> 并不是同一种列表,后者的类型是确定的,而前者的类型并不确定,编译器并不能知道这是一种什么类型。所以它不被允许添加元素,因为会导致类型不安全。


    不过细心的同学肯定发现了,这个和前面泛型的协变非常类似。其实通配符 * 不过是一种语法糖,背后也是用协变来实现的。所以:MutableList<*> 等价于 MutableList<out Any?>,使用通配符与协变有着一样的特性。


    在 Java 中,也有一样意义的通配符,不过使用的是 ? 作为通配。


    List<?> list = new ArrayList<Apple>(); 
    复制代码

    Java 中的通配符 ? 也等价于 ? extends Object


    多个泛型参数声明


    那可以声明多个泛型么?


    可以的可以的。


    HashMap 不就是一个典型的例子么?


    class HashMap<K,V>
    复制代码

    多个泛型,可以通过 , 进行分割,多个声明,上面是两个,实际上多个都是可以的。


    class HashMap<K: Animal, V, T, M, Z : Serializable>
    复制代码

    泛型方法


    上面讲的都是都是在类上声明泛型类型,那可以声明在方法上么?


    可以的可以的。


    如果你是一名 Android 开发,ViewfindViewById 不就是最好的例子么?


    public final <T extends View> T findViewById(@IdRes int id) {
    if (id == NO_ID) {
    return null;
    }
    return findViewTraversal(id);
    }
    复制代码

    很明显,View 是没有泛型参数类型的,但其 findViewById 就是典型的泛型方法,泛型声明就在方法上。


    上述写法改写成 Kotlin 也非常简单:


    fun <T : View?> findViewById(@IdRes id: Int): T? {
    return if (id == View.NO_ID) {
    null
    } else findViewTraversal(id)
    }
    复制代码

    Kotlin 的 reified


    前面有说到,由于 Java 中的泛型存在类型擦除的情况,任何在运行时需要知道泛型确切类型信息的操作都没法用了。比如你不能检查一个对象是否为泛型类型 T 的实例:


    <T> void printIfTypeMatch(Object item) {
    if (item instanceof T) { // 👈 IDE 会提示错误,illegal generic type for instanceof

    }
    }
    复制代码

    Kotlin 里同样也不行:


    fun <T> printIfTypeMatch(item: Any) {
    if (item is T) { // 👈 IDE 会提示错误,Cannot check for instance of erased type: T
    println(item)
    }
    }
    复制代码

    这个问题,在 Java 中的解决方案通常是额外传递一个 Class<T> 类型的参数,然后通过 Class#isInstance 方法来检查:


                                   👇
    <T> void check(Object item, Class<T> type) {
    if (type.isInstance(item)) {
    👆
    }
    }
    复制代码

    Kotlin 中同样可以这么解决,不过还有一个更方便的做法:使用关键字 reified 配合 inline 来解决:


      👇          👇
    inline fun <reified T> printIfTypeMatch(item: Any) {
    if (item is T) { // 👈 这里就不会在提示错误了

    }
    }
    复制代码

    上面的 Gson 解析的时候用的非常广泛,比如咱们项目里就有这样的扩展方法:


    inline fun <reified T> String?.toObject(type: Type? = null): T? {
    return if (type != null) {
    GsonFactory.GSON.fromJson(this, type)
    } else {
    GsonFactory.GSON.fromJson(this, T::class.java)
    }
    }
    复制代码

    总结


    本文花了非常大的篇幅来讲 Kotlin 的泛型和 Java 的泛型,现在再回过头去回答文首的几个问题,同学你有谱了吗?如果还是感觉一知半解,不妨多看几遍。


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

    扫盲:Kotlin 的泛型(1)

    引子南尘很久没有写文章啦,其实一直在思考挺多问题,如何才能写出对大家有价值的文字。思来想去,参看了好几位新继网红博主的风格,我觉得我时候开启新篇章了。没错,这就是扫盲。今天,就带来扫盲的第一篇:Kotlin 的泛型。相信总是有很多同学,总是在抱怨泛型无论怎么学...
    继续阅读 »

    扫盲:Kotlin 的泛型

    引子

    南尘很久没有写文章啦,其实一直在思考挺多问题,如何才能写出对大家有价值的文字。思来想去,参看了好几位新继网红博主的风格,我觉得我时候开启新篇章了。没错,这就是扫盲。

    今天,就带来扫盲的第一篇:Kotlin 的泛型。

    相信总是有很多同学,总是在抱怨泛型无论怎么学习,都只是停留在一个简单使用的水平,所以一直为此而备受苦恼。

    Kotlin 作为一门能和 Java 相互调用的语言,自然也支持泛型,不过 Kotlin 的新关键字 in  out 却总能绕晕一部分人,归根结底,还是因为 Java 的泛型基本功没有足够扎实。

    很多同学总是会产生这些疑问:

    • Kotlin 泛型和 Java 泛型到底有何区别?
    • Java 泛型存在的意义到底是什么?
    • Java 的类型擦除到底是指什么?
    • Java 泛型的上界、下界、通配符到底有何区别?它们可以实现多重限制么?
    • Java 的 <? extends T><? super T><?> 到底对应了什么?有哪些使用场景?
    • Kotlin 的 inout*where 到底有何魔力?
    • 泛型方法又是什么?

    今天,就用一篇文章为大家解除上述疑惑。

    泛型:类型安全的利刃

    总所周知,Java 在 1.5 之前,是没有泛型这个概念的。那时候的 List 还只是一个可以装下一切的集合。所以我们难免会写上这样的代码:

    List list = new ArrayList();
    list.add(1);
    list.add("nanchen2251");
    String str = (String) list.get(0);
    复制代码

    上面的代码编译并没有任何问题,但运行的时候一定会出现常见的 ClassCastException 异常:

    Exception in thread "main" java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
    复制代码

    这个体验非常糟糕,我们真正需要的是在代码编译的时候就能发现错误,而不是让错误的代码发布到生产环境中。

    而如果上述代码我们增加上泛型,就会在编译期就能看到明显的错误啦。

    List<String> list = new ArrayList<>();
    list.add(1);
    // 👆 报错 Required type:String but Provided:int
    list.add("nanchen2251");
    String str = list.get(0);
    复制代码

    很明显,泛型的出现,让类型更加安全,使我们在使用 ListMap 等不再需要去专门编写 StringListStringMap 了,只需要在声明 List 的同时指定参数类型即可。

    总的来说,泛型具备以下优势:

    • 类型检查,能在编译时就帮开发检查出错误;
    • 更加语义化,比如我们声明一个 LIst<String>,我们可以很直接知道里面存储的是 String 对象;
    • 能自动进行类型转换,获取数据的时候不需要再做强转操作;
    • 能写出更加通用化的代码。

    类型擦除

    可能有些同学思考过这样一个问题,既然泛型是和类型相关的,那么是不是也能使用类型的多态呢?

    我们知道,一个子类型是可以赋值给父类型的,比如:

    Object obj = "nanchen2251";
    // 👆 这是多态
    复制代码

    Object 作为 String 的父类,自然可以接受 String 对象的赋值,这样的代码我们早已司空见惯,并没有什么问题。

    但当我们写下这串代码:

    List<String> list = new ArrayList<String>();
    List<Object> objects = list;
    // 👆 多态用在这里会报错 Required type:List<Object> Provided: List<String>
    复制代码

    上面发生了赋值错误,这是因为 Java 的泛型本身具有「不可变性 Invariance」,Java 里面认为 List<String>  List<Object> 类型并不一致,也就是说,子类的泛型 List<String> 不属于泛型 List<Object> 的子类。

    由于 Java 的泛型本身是一种 「伪泛型」,Java 为了兼容 1.5 以前的版本,不得以在泛型底层实现上使用 Object 引用,所以我们声明的泛型在编译时会发生「类型擦除」,泛型类型会被 Object 类型取代。比如:

    class Demo<T> {
    void func(T t){
    // ...
    }
    }
    复制代码

    会被编译成:

    class Demo {
    void func(Object t){
    // ...
    }
    }
    复制代码

    可能你会好奇,在编译时发生类型擦除后,我们的泛型都被更换成了 Object,那为什么我们在使用的时候,却不需要强转操作呢?比如:

    List<String> list = new ArrayList<>();
    list.add("nanchen2251");
    String str = list.get(0);
    // 👆 这里并没有要求我们把 list.get(0) 强转为 String
    复制代码

    这是因为编译器会根据我们声明的泛型类型进行提前的类型检查,然后再进行类型擦除,擦除为 Object,但在字节码中其实还存储了我们的泛型的类型信息,在使用到泛型类型的时候会把擦除后的 Object 自动做类型强转操作。所以上面的 list.get(0) 本身就是一个经过强转的 String 对象了。

    这个技术看起来还蛮好的,但却有一个弊端。就是既然擦成 Object 了,那么在运行的时候,你根本不能确定这个对象到底是什么类型,虽然你可以通过编译器帮你插入的 checkcast 来获得此对象的类型。但是你并不能把 T 真正的当作一个类型使用:比如这条语句在 Java 中是非法的。

    T a = new T();
    // 👆 报错:Type parameter 'T' cannot be instantiated directly
    复制代码

    同理,因为都被擦成了 Object,你就不能根据类型来做某种区分。

    比如 instanceof

    if("nanchen2251" instanceof T.class){
    // 👆 报错:Identifier expected Unexpected token
    }
    复制代码

    比如重载:

    void func(T t){
    // 👆 报错:'func(T)' clashes with 'func(E)'; both methods have same erasure
    }
    void func(E e){
    }
    复制代码

    同样,因为基本数据类型不属于 oop,所以也不能被擦除为 Object,所以 Java 的泛型也不能用于基本类型:

    List<int> list;
    // 👆 报错:Type argument cannot be of primitive type
    复制代码

    oop:面向对象的程序设计(Object Oriented Programming)

    到这里,是不是可以回答上面的第 3 个问题了:Java 的类型擦除到底是指什么?

    首先你要明白一点,一个对象的类型永远不会被擦出的,比如你用一个 Object 去引用一个 Apple 对象,你还是可以获得到它的类型的。比如用 RTTI。

    RTTI:运行时类型信息,运行时类型识别 (Run Time Type Identification)

    Object object = new Apple();
    System.out.println(object.getClass().getName());
    // 👆 will print Apple
    复制代码

    哪怕它是放到泛型里的。

    class FruitShop<T>{
    private T t;

    public void set(T t){
    this.t = t;
    }

    public void showFruitName(){
    System.out.println(t.getClass().getName());
    }
    }
    FruitShop<Apple> appleShop = new FruitShop<Apple>();
    appleShop.set(new Apple());
    appleShop.showFruitName();
    // 👆 will print Apple too
    复制代码

    为啥?因为引用就是一个用来访问对象的标签而已,对象一直在堆上放着呢。

    所以不要断章取义认为类型擦除就是把容器内对象的类型擦掉了,所谓的类型擦除,是指容器类FruitShop<Apple>,对于 Apple 的类型声明在编译期的类型检查之后被擦掉,变为和 FruitShop<Object> 等同效果,也可以说是 FruitShop<Apple>  FruitShop<Banana> 被擦为和 FruitShop<Object> 等价,而不是指里面的对象本身的类型被擦掉!

    那,Kotlin 中有类型擦除么?

    C# 和 Java 在一开始都是不支持泛型的。Java 在 1.5 开始才加入了泛型。为了让一个不支持泛型的语言支持泛型,只有两条路可以走:

    • 以前的非泛型容器保持不变,然后平行的增加一套泛型化的类型。
    • 直接把已有的非泛型容器扩展为泛型,不添加任何新的泛型版本。

    Java 由于 1.5 之前市面上一句有大量的代码,所以不得以选择了第 2 种方式,而 C# 比较机智就选择了第一种。

    而 Kotlin 本身就是基于 Java 1.6 编写的,一开始就有泛型,不存在兼容老版本代码的问题,那 Kotlin 实现的泛型还具备类型擦除么?

    当然具备。上面其实已经说的很清楚了,Kotlin 本身就是基于 Java 1.6 编写的,而且 Kotlin 和 Java 有极强的互调能力,当然也存在类型擦除。

    不过...

    你还是会发现有意思的点:

    val list = ArrayList()
    // 👆 报错:Not enough information to infer type variable E
    复制代码

    在 Java 中,不指定泛型类型是没问题的,但 Kotlin 这样不好使了。想来也简单,毕竟在 Java 1.5 之前是肯定不存在上述类似代码的,而泛型的设计初衷就不是用来装默认的 Kotlin Any 的。

    泛型的上界通配符

    前面说到:因为 Java 的泛型本身具有「不可变性 Invariance」,所以即使 Fruit 类是 Apple 类的父类,但 Java 里面认为 List<Fruit>  List<Apple> 类型并不一致,也就是说,子类的泛型 List<Apple> 不属于泛型 List<Fruit> 的子类。

    所以这样的代码并不被运行。

    List<Apple> apples = new ArrayList<Apple>();
    List<Fruit> fruits = apples;
    // 👆 多态用在这里会报错 Required type:List<Fruit> Provided: List<Apple>
    复制代码

    那假如我们想突破这层限制,怎么办?使用上界通配符 ? extends

    List<Apple> apples = new ArrayList<Apple>();
    List<? extends Fruit> fruits = apples;
    // 👆使用上界通配符后,编译不再报错
    复制代码

    「上界通配符」,可以使 Java 泛型具有「协变性 Covariance」,协变就是允许上面的赋值是合法的。

    在继承关系树中,子类继承自父类,可以认为父类在上,子类在下。extends 限制了泛型类型的父类型,所以叫上界。

    它有两层意思:

    • 其中 ? 是个通配符,表示这个 List 的泛型类型是一个未知类型。
    • extends 限制了这个未知类型的上界,也就是泛型类型必须满足这个 extends 的限制条件,这里和定义 class  extends 关键字有点不一样:
      • 它的范围不仅是所有直接和间接子类,还包括上界定义的父类本身,也就是 Fruit
      • 它还有 implements 的意思,即这里的上界也可以是 interface

    这个突破限制有意义么?

    有的有的。

    假如我们有一个接口 Fruit

    interface Fruit {
    float getWeight();
    }
    复制代码

    有两个水果类实现了 Fruit 接口:

    class Banana implements Fruit {
    @Override
    public float getWeight() {
    return 0.5f;
    }
    }

    class Apple implements Fruit {
    @Override
    public float getWeight() {
    return 1f;
    }
    }
    复制代码

    假设我们有个需求是需要给水果称重:

    List<Apple> apples = new ArrayList<>();
    apples.add(new Apple());
    float totalWeight = getTotalWeight(apples);
    // 👆 报错:Required type: List<Fruit> Provided: List<Apple>

    private float getTotalWeight(List<Fruit> fruitList) {
    float totalWeight = 0;
    for (Fruit fruit : fruitList) {
    totalWeight += fruit.getWeight();
    }
    return totalWeight;
    }
    复制代码

    想来这也是一个非常正常的需求,秤可以称各种水果的重量,但也可以只称苹果。你不能因为我只买苹果就不给我称重吧。所以把上面的代码加上上界通配符就可以啦。

    List<Apple> apples = new ArrayList<>();
    apples.add(new Apple());
    float totalWeight = getTotalWeight(apples);
    // 👆 不再报错
    // 👇 增加了上界通配符 ? extends
    private float getTotalWeight(List<? extends Fruit> fruitList) {
    float totalWeight = 0;
    for (Fruit fruit : fruitList) {
    totalWeight += fruit.getWeight();
    }
    return totalWeight;
    }
    复制代码

    不过,上面使用 ? extends 上界通配符突破了一层限制,却被施加了另一层限制:只可输出不可输入

    什么意思呢?

    比如:

    List<Apple> apples = new ArrayList<Apple>();
    List<? extends Fruit> fruits = apples;
    Fruit fruit = fruits.get(0);
    fruits.add(new Apple());
    // 👆 报错:Required type: capture of ? extends Fruit Provided: Apple
    复制代码

    声明了上界通配符泛型的集合,不再允许 add 新的对象,Apple 不行,Fruit 也不行。拓展开来说:不止是集合,自己编写一个泛型做输入也不行

    interface Shop<T> {
    void showFruitName(T t);
    T getFruit();
    }

    Shop<? extends Fruit> apples = new Shop<Apple>(){
    @Override
    public void showFruitName(Apple apple) { }

    @Override
    public Apple getFruit() {
    return null;
    }
    };
    apples.getFruit();
    apples.showFruitName(new Apple());
    // 👆 报错:Required type: capture of ? extends Fruit Provided: Apple
    复制代码

    泛型的下界通配符

    泛型有上界通配符,那有没有下界通配符呢?

    有的有的。

    与上界通配符 ? extends 对应的就是下界通配符 ? super

    下界通配符 ? super 所有情况和 ? extends 上界通配符刚刚相反:

    • 通配符 ? 表示 List 的泛型类型是一个 未知类型
    • super 限制了这个未知类型的下界,也就是泛型类型必须满足这个 super 的限制条件
      • 它的范围不仅是所有直接和间接子父类,还包括下界定义的子类本身。
      • super 同样支持 interface

    它被施加的新限制是:只可输入不可输出

    Shop<? super Apple> apples = new Shop<Fruit>(){
    @Override
    public void showFruitName(Fruit apple) { }

    @Override
    public Fruit getFruit() {
    return null;
    }
    };
    apples.showFruitName(new Apple());
    Apple apple = apples.getFruit();
    // 👆 报错:Required type: Apple Provided: capture of ? super Apple
    复制代码

    解释下,首先 ? 表示未知类型,编译器是不确定它的类型的。

    虽然不知道它的具体类型,不过在 Java 里任何对象都是 Object 的子类,所以这里只能把apples.getFruit() 获取出来的对象赋值给 Object。由于类型未知,所以直接赋值给一个 Apple 对象肯定是不负责任的,需要我们做一层强制转换,不过强制转换本身可能发生错误。

     Apple 对象一定是这个未知类型的子类型,根据多态的特性,这里通过 showFruitName 输入 Button 对象是合法的。

    小结下,Java 的泛型本身是不支持协变和逆变的:

    • 可以使用泛型通配符 ? extends 来使泛型支持协变,但是「只能读取不能修改」,这里的修改仅指对泛型集合添加元素,如果是 remove(int index) 以及 clear 当然是可以的。
    • 可以使用泛型通配符 ? super 来使泛型支持逆变,但是「只能修改不能读取」,这里说的不能读取是指不能按照泛型类型读取,你如果按照 Object 读出来再强转当然也是可以的。

    理解了 Java 的泛型之后,再理解 Kotlin 中的泛型,就比较容易了。

    收起阅读 »

    Kotlin写一个解释器(1)---词法分析

    为什么学最近对编译器很感兴趣,为什么要学习编译原理,于我而言是因为最近需要写一个DSL,需要一个解释器,而对于大部分程序员来说,学习编译器可能有一下三个方面:(1)学习编译器设计,可以帮助更好的理解程序以及计算机是怎么运行的,同时编写编译器或者解释器需要大量的...
    继续阅读 »

    Kotlin写一个解释器(1)---词法分析

    为什么学

    最近对编译器很感兴趣,为什么要学习编译原理,于我而言是因为最近需要写一个DSL,需要一个解释器,而对于大部分程序员来说,学习编译器可能有一下三个方面:

    (1)学习编译器设计,可以帮助更好的理解程序以及计算机是怎么运行的,同时编写编译器或者解释器需要大量的计算机技巧,对技术 也是一个提升。

    (2)面试需要,所谓“工作拧螺丝,面试造火箭”。学习编译器设计有助于加强计算机基础能力,提高编码素养,更好的应对面试,毕竟你不知道你的面试官是不是对这个也感兴趣。

    (3)工作需要,有的时候你可能需要创造一些领域特定语言或者发明一种新语言(这个需要不太多),这个时候你就需要写一个编译器或者解释器来满足你的需要了。

    编译器与解释器

    编译器将以某种语言编写的程序作为输入,产生一个等价的程序作为输出,通常这个输入语言可能就是C活着C++,等价的目标程序通常是某种处理器的指令集,然后可以直接运行该目标程序。解释器与编译器的不同就在于它直接执行由编程语言或者脚本编写的代码,并不会把源代码预编译成指令集,它会实时的返回结果。如下图所示

    compiler.jpg

    还有一种编译器,它将一种高级语言翻译成另一种高级语言,比如将php转为C++,这种编译器一般称为源到源的转换器。

    目标

    这个博客系列的目标其实很小,就是利用Kotlin去实现一个小型的解释器来进行正数的四则运算,因为本人目前也是学习阶段,所以只能先弄一个这样的小功能,等技术提高了再开新番。该系列只会涉及到词法分析、语法分析、抽象语法树等编译器前端相关的概念,至于编译器后端相关的代码优化以及代码生成不会涉及。

    词法分析

    词法分析的任务是将字符流变换为输入语言的单词流。每个单词都必须归类到某个语法范畴中,也叫词类。词法分析器是编译器中唯一会接触到程序中每个字符的一趟处理。这里有几个概念要说明,比如词类,列比我们平时的语言,我们会说“走”是动词,“跑”也是动词,“漂亮”是形容词,“美丽”也是形容词,这里动词和形容词,就是词类,代表着单词的分类,还有一个就是词素,前面说的“走”,“跑”,"漂亮“等其实就是词素,也就是具体的单词内容,实际的文本。词法分析的作用就是将源代码中的文件按字符读入,根据读入的字符识别单词组成一个词法单元,每个词法单元由词法单元名和词素组成,列如<形容词:漂亮>,其中形容词就是词法单元名,可以看做和词类相同,漂亮就是词素,而对于我们要实现的计算器来说可能就是<PLUS,+>,<NUMBER,9>这样的。

    代码

    定义词法单元

    前面说过,词法单元由词法单元名和词素组成,所以我们定义一个类Token来代表词法单元,TokenType这个枚举类来代表词法单元名

    data class Token(val tokenType: TokenType, val value: String) {
    override fun toString(): String {
    return "Token(tokenType=${this.tokenType.value}, value= ${this.value})"
    }
    }


    enum class TokenType(val value: String) {
    NUMBER("NUMBEER"), PLUS("+"), MIN("-"), MUL("*"), DIV("/"), LBRACKETS("("), RBRACKETS(")"), EOF("EOF")
    }
    复制代码

    可以看到我有8个TokenType,8个TokenType代表着8个词法单元名,也就是8个词类,其中EOF代表END OF FILE及源文件扫描结束。

    定义词法分析器

    import java.lang.RuntimeException

    class Lexer(private val text: String) {

    private var nextPos = 0

    private val tokenMap = mutableMapOf<String, TokenType>()

    private var nextChar: Char? = null

    init {
    TokenType.values().forEach {
    if(it!=TokenType.NUMBER) {
    tokenMap[it.value] = it
    }
    }
    nextChar = text.getOrNull(nextPos)
    }

    fun getNextToken(): Token {
    loop@ while (nextChar != null) {
    when (nextChar) {
    in '0'..'9' -> {
    return Token(TokenType.NUMBER, getNumber())
    }
    ' ' -> {
    skipWhiteSpace()
    continue@loop
    }
    }
    if (tokenMap.containsKey(nextChar.toString())) {
    val tokenType = tokenMap[nextChar.toString()]
    if (tokenType != null) {
    val token = Token(tokenType, tokenType.value)
    advance()
    return token
    }
    }
    throw RuntimeException("Error parsing input")
    }
    return Token(TokenType.EOF, TokenType.EOF.value)
    }

    private fun getNumber(): String {
    var item = ""
    while (nextChar != null && nextChar!! in '0'..'9') {
    item += nextChar
    advance()
    if ('.' == nextChar) {
    item += nextChar
    advance()
    while (nextChar != null && nextChar!! in '0'..'9') {
    item += nextChar
    advance()
    }
    }
    }
    return item
    }

    private fun skipWhiteSpace() {
    var nextChar = text.getOrNull(nextPos)
    while (nextChar != null && ' ' == nextChar) {
    advance()
    nextChar = text.getOrNull(nextPos)
    }
    }

    private fun advance() {
    nextPos++
    nextChar = if (nextPos > text.length - 1) {
    null
    } else {
    text.getOrNull(nextPos)
    }
    }

    }
    复制代码

    先看成员变量,其中text代表的是源文件内容,nextPos代表着源代码中下一个字符的位置,nextChar代表着源代码中下一个字符,tokenMap代表着词素和词法单元是唯一对应的Map,其中key代表的是词素,TokenType代表的词法单元,可以看到在我们的定义中除了TokenType.NUMBER其他都是存在唯一对应的,因为对于数字来说,9对应的是词法单元是NUMBER,10对应的词法单元也是NUMBER,不存在唯一对应,所以需要我们单独识别。

    构造方法很简单,为tokenMap和nextChar计算赋值

    再看剩下的方法,其实这个词法解析器对外暴露的只有getNextToken方法用以返回Token,其他方法都是private的用以辅助返回Token,在getNextToken方法中,有一个while循环判断nextChar是否为null,如果为null代表着源文件已经处理完毕,返回Token(TokenType.EOF, TokenType.EOF.value),否则的话就判断当前字符是不是数字,如果是数字,就返回Token(TokenType.NUMBER, getNumber()),其中getNumber方法用以获取数字内容直到nextChar不再为数字为止。如果nextChar为空格,就跳过空格skipWhiteSpce,然后继续当前循环,如果既不是数字也不是空格,那么就根据tokenMap获取相应的TokenType,看看是不是运算符号或者括号,如果也不存在,说明输入中存在非数字和四则运算符号以及括号的字符,抛出错误"Error parsing input”。

    测试

    import java.util.*

    fun main() {
    while(true) {
    val scanner = Scanner(System.`in`)
    val text = scanner.nextLine()
    val lexer = Lexer(text)
    var nextToken = lexer.getNextToken()
    while (TokenType.EOF != nextToken.tokenType) {
    println(nextToken.toString())
    nextToken = lexer.getNextToken()
    }
    }
    }
    复制代码

    编写测试代码,打印相应的token,运行代码,在代码中输入数据并回车,查看结果

    截屏2021-04-20 上午10.37.01.png

    至此词法分析器基本完成,其实词法分析器还有很多概念可说,比如说正则表达式,然后由正则表达式生成NFA,NFA应用最小子集法生成DFA,再由DFA生成最小DFA,其中有很多概念,但是个人认为太多的概念会让人迷茫,不如先由简单的例子开始,然后逐步加深理解,熟悉概念

    收起阅读 »

    Android从源码分析RecyclerView四级缓存复用机制二(复用ViewHolder)

    上一篇文章说了RecyclerView的四级缓存中的缓存ViewHolder,文章链接在这里:Android从源码分析RecyclerView四级缓存复用机制一(缓存ViewHolder) 列表视图在原生开发中一直占用重要地位,不管是之前的ListV...
    继续阅读 »

    Android从源码分析RecyclerView四级缓存复用机制二(复用ViewHolder)

    上一篇文章说了RecyclerView的四级缓存中的缓存ViewHolder,文章链接在这里:Android从源码分析RecyclerView四级缓存复用机制一(缓存ViewHolder) 列表视图在原生开发中一直占用重要地位,不管是之前的ListView还是现在RecyclerView,无论实在性能上还是使用功能上都有着巨大的优势,其中最重要的其实还是对于视图的复用机制。从ListView的RecycleBin到RecyclerView的Recycler,Google对于列表视图的缓存的设计一直非常考究值得我们学习和研究。而网页的H5以及RN对于复杂的列表视图的渲染性能不好从这里面其实也可以寻找到一些原因。 本文就来说一下ViewHolder的复用(回收) ,缓存的ViewHolder必须要复用才能够体现缓存的意义。

    1.总流程图

    放上一张Bugly的一篇博客对RecyclerView的缓存的流程图吧(自己绘制也差不多,直接拿来用了...若侵立删) 在这里插入图片描述

    2 .调用方法跟踪

    recyclerView在什么时候复用被缓存的ViewHolder,毫无疑问肯定是在recyclerView滚动的时候。recyclerView在滚动的时候触发复用。

    // 1.RecyclerView的scrollBy()方法
    @Override
    public void scrollBy(int x, int y) {
    //...
    final boolean canScrollHorizontal = mLayout.canScrollHorizontally();
    final boolean canScrollVertical = mLayout.canScrollVertically();
    //横向滑动和纵向滑动都可以进入if
    if (canScrollHorizontal || canScrollVertical) {
    scrollByInternal(canScrollHorizontal ? x : 0, canScrollVertical ? y : 0, null);
    }
    }

    // 2.RecyclerView的scrollByInternal()方法
    boolean scrollByInternal(int x, int y, MotionEvent ev) {
    int unconsumedX = 0, unconsumedY = 0;
    int consumedX = 0, consumedY = 0;

    consumePendingUpdateOperations();
    if (mAdapter != null) {
    eatRequestLayout();
    onEnterLayoutOrScroll();
    TraceCompat.beginSection(TRACE_SCROLL_TAG);
    if (x != 0) {
    //横向滑动调用LayoutManager的scrollHorizontallyBy
    consumedX = mLayout.scrollHorizontallyBy(x, mRecycler, mState);
    unconsumedX = x - consumedX;
    }
    if (y != 0) {
    //纵向滑动调用LayoutManager的scrollVerticallyBy
    consumedY = mLayout.scrollVerticallyBy(y, mRecycler, mState);
    unconsumedY = y - consumedY;
    }
    //...
    }
    //...
    }

    // 3.RecyclerView.LayoutManager的scrollVerticallyBy()方法 本文主要分析纵向滑动其实都一样
    public int scrollVerticallyBy(int dy, Recycler recycler, State state) {
    return 0;
    }

    // 4.LinearLayoutManager重写了LayoutManager的scrollVerticallyBy()方法
    @Override
    public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler,RecyclerView.State state) {
    if (mOrientation == HORIZONTAL) {
    return 0;
    }
    return scrollBy(dy, recycler, state);
    }

    // 5.LinearLayoutManager的scrollBy()
    int scrollBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
    //...
    //这里调用fill方法
    final int consumed = mLayoutState.mScrollingOffset
    + fill(recycler, mLayoutState, state, false);
    //...
    final int scrolled = absDy > consumed ? layoutDirection * consumed : dy;
    mOrientationHelper.offsetChildren(-scrolled);
    //...
    mLayoutState.mLastScrollDelta = scrolled;
    return scrolled;
    }
    // 6.LinearLayoutManager的fill()
    int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
    RecyclerView.State state, boolean stopOnFocusable)
    {
    //...
    while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
    //...
    //根据当前的布局方向调用适当的回收方法
    layoutChunk(recycler, state, layoutState, layoutChunkResult);
    //...
    }
    //...
    }
    // 7.LinearLayoutManager的layoutChunk()
    void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,LayoutState layoutState, LayoutChunkResult result) {
    //传入recycler获取到相应的view
    View view = layoutState.next(recycler);
    //...
    if (layoutState.mScrapList == null) {
    if (mShouldReverseLayout == (layoutState.mLayoutDirection== LayoutState.LAYOUT_START)) {
    //将获取到的view重新添加到recyclerview中
    addView(view);
    } else {
    addView(view, 0);
    }
    }
    //...
    }
    // 8.LinearLayoutManager的next()
    View next(RecyclerView.Recycler recycler) {
    if (mScrapList != null) {
    return nextViewFromScrapList();
    }
    //根据位置获取view
    final View view = recycler.getViewForPosition(mCurrentPosition);
    mCurrentPosition += mItemDirection;
    return view;
    }
    // 9.RecyclerView.Recycler的getViewForPosition()
    View getViewForPosition(int position, boolean dryRun) {
    return tryGetViewHolderForPositionByDeadline(position, dryRun, FOREVER_NS).itemView;
    }
    复制代码

    回顾一下方法的调用

    入口:滑动 Move 事件 --> scrollByInternal --> scrollStep --> mLayout.scrollVerticallyBy 
    --> scrollBy --> fill --> layoutChunk --> layoutState.next --> addView(view);

    layoutState.next--> getViewForPosition --> tryGetViewHolderForPositionByDeadline
    复制代码

    经过一系列的方法调用,我们调用到了RecyclerView.Recycler的tryGetViewHolderForPositionByDeadline()方法中拿到四级缓存中的ViewHolder然后拿到view,然后在layoutChunk()中的addView(view)方法中添加到recyclerView中。

    下面通过源码来验证一下1.总流程图

    3.核心源码分析

    ViewHolder tryGetViewHolderForPositionByDeadline(int position,
    boolean dryRun, long deadlineNs)
    {
    //...各种判断
    boolean fromScrapOrHiddenOrCache = false;
    ViewHolder holder = null;
    if (mState.isPreLayout()) {
    // 1.对应的是一级缓存中的 -- mChangeScrap 与动画相关的ViewHolder
    holder = getChangedScrapViewForPosition(position);
    fromScrapOrHiddenOrCache = holder != null;
    }
    //如果一级缓存没有对应位置的holder
    if (holder == null) {
    //2.对应的是二级缓存中-- mAttachedScrap 、mCachedViews 存储屏幕外的ViewHolder
    holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
    if (holder != null) {
    if (!validateViewHolderForOffsetPosition(holder)) {
    if (!dryRun) {
    //...一堆判断
    //回收方法
    recycleViewHolderInternal(holder);
    }
    holder = null;
    } else {
    fromScrapOrHiddenOrCache = true;
    }
    }
    }
    if (holder == null) {
    //...

    final int type = mAdapter.getItemViewType(offsetPosition);
    if (mAdapter.hasStableIds()) {
    // 3.对应的是二级缓存中-- mAttachedScrap 、mCachedViews 根据(ViewType,itemid) 获取的ViewHolder
    holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
    type, dryRun);
    if (holder != null) {
    holder.mPosition = offsetPosition;
    fromScrapOrHiddenOrCache = true;
    }
    }
    if (holder == null && mViewCacheExtension != null) {
    //4.对应的是三级缓存 -- 自定义缓存 -- (使用情况:局部刷新??)的ViewHolder
    final View view = mViewCacheExtension
    .getViewForPositionAndType(this, position, type);
    if (view != null) {
    holder = getChildViewHolder(view);
    //...
    }
    }
    if (holder == null) { // fallback to pool
    //...
    //5.对应的是四级缓存-- 从缓冲池里面获取 ViewHolder
    holder = getRecycledViewPool().getRecycledView(type);
    if (holder != null) {
    holder.resetInternal();
    if (FORCE_INVALIDATE_DISPLAY_LIST) {
    invalidateDisplayListInt(holder);
    }
    }
    }
    if (holder == null) {
    long start = getNanoTime();
    //...
    // 6.四级缓存都找不到 ViewHolder则走createViewHolder()创建ViewHolder
    holder = mAdapter.createViewHolder(RecyclerView.this, type);
    //...
    }
    //...
    return holder;
    }
    复制代码

    至此文章中的大体的源码就分析完了,如果想进一步了解更多的recyclerView回收复用的更多的细节的话,可以自己阅读一下源码。

    收起阅读 »

    Android与JS相互通信

    Android调用js的方法实现是引入一个webview用webview打开一个页面调取的JS函数。private void InitWebView() { mWebView = (WebView) findViewById(R.id.webview)...
    继续阅读 »

    Android与JS相互通信

    Android调用js的方法实现是引入一个webview用webview打开一个页面调取的JS函数。

    private void InitWebView() {
    mWebView = (WebView) findViewById(R.id.webview);

    WebSettings webSettings = mWebView.getSettings();

    // 设置与Js交互的权限
    webSettings.setJavaScriptEnabled(true);
    // 设置允许JS弹窗
    webSettings.setJavaScriptCanOpenWindowsAutomatically(true);

    //把js弹窗转化成安卓弹窗
    SetJavaScriptAlertEnable();

    // 先载入html代码
    // 格式规定为:file:///android_asset/文件名.html
    // 只需要将第一种方法的loadUrl()换成下面该方法即可
    // Android版本变量
    //mWebView.addJavascriptInterface(new AndroidtoJs(), "test");//AndroidtoJS类对象映射到js的test对象
    mWebView.loadUrl("file:///android_asset/javascript.html");
    //SetWebChromeClient()//处理JS调用Android函数的方法在第二部分解释

    }
    复制代码

    首先先初始化webview

    第二步对webview基本初始化

    第三部设置JS交互权限

    其余有明确注释

    loadurl是加载html网页端的

    我们以button点击开始执行js事件为例

    首先app/src/main下创建assets文件

    下面是javascript.html文件我们在引入文件时需要将文件放到所在的Android项目的/project_home/app/src/main/assets/javascript.html 下就可以使用了。

    <!DOCTYPE html>
    <html>

    <head>
    <meta charset="utf-8">
    <title>Carson_Ho</title>
    <h1 id="ip">
    你好这里已经调用过文件了
    </h1>
    <button onclick="callJS()">调用弹窗</button>
    <button type="button" id="button1" onclick="callAndroid()">点击调用Android代码</button>
    <button type="button" id="button2" onclick="clickprompt()">点击调用Androidtest代码</button>
    <script>
    function callJS() {
    alert("Android调用了JS的callJS方法");
    }

    function change() {
    document.getElementById('ip').innerText = "真的难啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊a";
    }
    function clickprompt(){
    var result=prompt("js://demo?arg1=111&arg2=222");
    alert("demo " + result);
    }
    function callAndroid(){
    // 由于对象映射,所以调用test对象等于调用Android映射的对象
    test.hello("js调用了android中的hello方法");
    }

    </script>

    // JS代码
    <script src="./1/roslib.min.js"></script>
    <script src="./1/RoboX.js"></script>

    </head>

    </html>
    复制代码

    将JS弹窗改为Android弹窗方法SetJavaScriptAlertEnable();

    private void SetJavaScriptAlertEnable() {
    // 由于设置了弹窗检验调用结果,所以需要支持js对话框
    // webview只是载体,内容的渲染需要使用webviewChromClient类去实现
    // 通过设置WebChromeClient对象处理JavaScript的对话框
    //设置响应js 的Alert()函数
    mWebView.setWebChromeClient(new WebChromeClient() {
    @Override
    public boolean onJsAlert(WebView view, String url, String message, final JsResult result) {
    //设置弹窗
    AlertDialog.Builder b = new AlertDialog.Builder(MainActivity.this);
    b.setTitle("Alert");
    b.setMessage(message);
    b.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
    @Override
    public void onClick(DialogInterface dialog, int which) {
    result.confirm();
    }
    });
    b.setCancelable(true);
    b.create().show();
    return true;
    }
    });
    }
    复制代码

    初始化界面用button测试程序是否能通信。

    private void InitView() {

    button = (Button) findViewById(R.id.button);

    button.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
    // 通过Handler发送消息
    mWebView.post(new Runnable() {
    @Override
    public void run() {

    // 注意调用的JS方法名要对应上
    // 调用javascript的callJS()方法
    final int version = Build.VERSION.SDK_INT;//获取系统版本
    //我们需要判断当前系统版本。为了尽可能减少错误我们使用了两种方式来实现调用JS方法。
    if (version < 18) {
    mWebView.loadUrl("javascript:callJS()");
    } else {

    // Toast.makeText(getApplicationContext(), "单击完成", Toast.LENGTH_SHORT).show();


    //Toast.makeText(getApplicationContext(), ans.toString(), Toast.LENGTH_SHORT).show();
    mWebView.evaluateJavascript("javascript:callJS()", new ValueCallback<String>() {
    @Override
    public void onReceiveValue(String value) {
    //此处为 js 返回的结果
    }
    });
    mWebView.evaluateJavascript("javascript:change()", new ValueCallback<String>() {
    @Override
    public void onReceiveValue(String value) {
    //此处为 js 返回的结果
    }
    });
    }
    }
    });

    }
    });
    }
    复制代码

    下面是JS同Android的通信,JS调用Android的函数方法。

    将上边webview初始化中取消注释SetWebChromeClient()同时取消注mWebView.addJavascriptInterface(new AndroidtoJs(), "test");重新创建一个类继承于Object同时创建的test对象在js中同样如此像HTML文件中的

    function callAndroid(){
    // 由于对象映射,所以调用test对象等于调用Android映射的对象
    test.hello("js调用了android中的hello方法");
    }
    复制代码

    下面是创建的类AndroidtoJS

    import android.webkit.JavascriptInterface;

    public class AndroidtoJs extends Object {
    @JavascriptInterface
    public void hello(String msg) {
    System.out.println("JS调用了Android的hello方法woc");
    }
    }

    复制代码

    SetWebChromeClient()是处理JS和Android之间通信的在下边有详细解释

    private void SetWebChromeClient() {
    mWebView.setWebChromeClient(new WebChromeClient() {
    // 拦截输入框(原理同方式2)
    // 参数message:代表promt()的内容(不是url)
    // 参数result:代表输入框的返回值
    @RequiresApi(api = Build.VERSION_CODES.HONEYCOMB)
    @Override
    public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {
    // 根据协议的参数,判断是否是所需要的url(原理同方式2)
    // 一般根据scheme(协议格式) & authority(协议名)判断(前两个参数)
    //假定传入进来的 url = "js://demo?arg1=111&arg2=222"(同时也是约定好的需要拦截的)

    Uri uri = Uri.parse(message);
    // 如果url的协议 = 预先约定的 js 协议
    // 就解析往下解析参数
    if (uri.getScheme().equals("js")) {

    // 如果 authority = 预先约定协议里的 webview,即代表都符合约定的协议
    // 所以拦截url,下面JS开始调用Android需要的方法
    if (uri.getAuthority().equals("demo")) {

    //
    // 执行JS所需要调用的逻辑
    System.out.println("js调用了Android的方法");
    // 可以在协议上带有参数并传递到Android上
    HashMap<String, String> params = new HashMap<>();
    Set<String> collection = uri.getQueryParameterNames();

    //参数result:代表消息框的返回值(输入值)
    result.confirm("js调用了Android的方法成功啦");
    }
    return true;
    }
    return super.onJsPrompt(view, url, message, defaultValue, result);
    }

    // 通过alert()和confirm()拦截的原理相同,此处不作过多讲述
    // 拦截JS的警告框
    @Override
    public boolean onJsAlert(WebView view, String url, String message, JsResult result) {
    return super.onJsAlert(view, url, message, result);
    }

    //拦截JS的确认框
    @Override
    public boolean onJsConfirm(WebView view, String url, String message, JsResult result) {
    return super.onJsConfirm(view, url, message, result);
    }
    }
    );
    }

    复制代码

    我们不是采用直接调用的方法而是采用拦截的方法

    第一次接触Android项目有的地方写的可能有问题有的地方说的也可不能不准确,大家下方积极留言,第一次写博客排版也没搞好大家见谅吧哈哈哈哈。

    收起阅读 »

    算法与数据结构之数组

    个人说明:拿过国内某算法大赛全国三等。。。(渣渣) 概念它用一组连续的内存空间,来存储一组具有相同类型的数据。优点:查找速度快,可以快速随机访问缺点:删除,插入效率低,大小固定,不支持动态扩展,要求内存空间必须连续数组是一种线性表结构 基本操作(以下图都是盗的...
    继续阅读 »

    个人说明:拿过国内某算法大赛全国三等。。。(渣渣)


    概念

    它用一组连续的内存空间,来存储一组具有相同类型的数据。
    优点:查找速度快,可以快速随机访问
    缺点:删除,插入效率低,大小固定,不支持动态扩展,要求内存空间必须连续
    数组是一种线性表结构

    基本操作(以下图都是盗的,有侵的话私聊我)

    插入:

    如果要想在任意位置插入元素,那么必须要将这个位置后的其他元素后移。
    那么其最好的时间复杂度O(1) 最差的时间复杂度为O(n)
    在这里插入图片描述

    删除

    同样的删除数据需要前移


    注意事项:
    如果插入的数据多的情况下,那么数组的容量是固定的,那么就存在扩容的操作。
    同样的,如果删除数据多的情况下,也会出现缩容的操作
    例:java的arraylist
    首先,先看看ArrayList的初始化,源码如下:

    public ArrayList(int initialCapacity) {
    if (initialCapacity > 0) {
    //更根据初始值大小创建数组
    this.elementData = new Object[initialCapacity];
    } else if (initialCapacity == 0) {
    //默认无规定初始值大小时,会创建一个空数组
    //private static final Object[] EMPTY_ELEMENTDATA = {};
    //待通过add方法时创建初始容量为10的数组
    this.elementData = EMPTY_ELEMENTDATA;
    } else {
    throw new IllegalArgumentException("Illegal Capacity: "+
    initialCapacity);
    }
    }

    add(E e)方法的源码解析


    public boolean add(E e) {
    //检查是否需要扩容
    ensureCapacityInternal(size + 1); // Increments modCount!!
    //添加新元素
    elementData[size++] = e;
    return true;
    }

    //传入数组最小所需要的长度
    private void ensureCapacityInternal(int minCapacity) {
    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
    }
    //检查原先数组是否为空数组,返回数组所需最小长度
    private static int calculateCapacity(Object[] elementData, int minCapacity) {
    //判断数组是否为空。
    //private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
    //private static final int DEFAULT_CAPACITY = 10;
    //数组为空时,返回DEFAULT_CAPACITY与minCapacity中大的数,减少扩容次数
    return Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    //直接返回数组所需的最小长度size+1
    return minCapacity;
    }

    //判断当前数组长度是否超过添加元素所需的最小长度
    private void ensureExplicitCapacity(int minCapacity) {
    modCount++;

    // overflow-conscious code
    //如果所需最小长度大于当前数组长度
    if (minCapacity - elementData.length > 0)
    //进行扩容
    grow(minCapacity);
    //否则,不做任何处理
    }


    private void grow(int minCapacity) {
    // overflow-conscious code
    int oldCapacity = elementData.length;
    //新数组长度等于旧长度+1/2旧长度
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    //计算newCapacity的大小
    if (newCapacity - minCapacity < 0)
    newCapacity = minCapacity;
    //private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
    if (newCapacity - MAX_ARRAY_SIZE > 0)
    newCapacity = hugeCapacity(minCapacity);
    // minCapacity is usually close to size, so this is a win:
    //拷贝到新数组,数组长度为newCapacity
    elementData = Arrays.copyOf(elementData, newCapacity);
    }

    private static int hugeCapacity(int minCapacity) {
    if (minCapacity < 0) // overflow
    throw new OutOfMemoryError();
    return (minCapacity > MAX_ARRAY_SIZE) ?
    Integer.MAX_VALUE :
    MAX_ARRAY_SIZE;
    }

    ArrayList缩容
    ArrayList没有自动缩容机制。无论是remove方法还是clear方法,它们都不会改变现有数组elementData的长度。但是它们都会把相应位置的元素设置为null,以便垃圾收集器回收掉不使用的元素,节省内存。ArrayList的缩容,需要我们自己手动去调用trimToSize()方法,达到缩容的目的。

    /**
    * Trims the capacity of this <tt>ArrayList</tt> instance to be the
    * list's current size. An application can use this operation to minimize
    * the storage of an <tt>ArrayList</tt> instance.
    */
    public void trimToSize() {
    modCount++;
    //判断当前容量与数组长度的大小关系
    if (size < elementData.length) {
    //如果size小于elementData.length,则将数组拷贝到长度为size的数组中,如果size==0,则将elementData 置为空数组,{}
    elementData = (size == 0)
    ? EMPTY_ELEMENTDATA
    : Arrays.copyOf(elementData, size);
    }
    }

    有关数组的算法

    以下算法只给思路(代码写一写就知道了)


    数组算法注意事项

    并没什么可注意的,哈哈哈哈
    在这里插入图片描述
    有一点就是:数组根据下标进行访问时,时间复杂度为O(1),进行插入和删除操作时,时间复杂度为O(n)(主要就是结构可以让他这么屌)

    算法一(一个未排序的整数数组,找出最长连续序列的长度。)

    leetcode等级困难
    时间要求O(n)
    示例:

    输入: [100, 4, 200, 1, 3, 2]
    输出: 4
    解释: 最长连续序列是 [1, 2, 3, 4]。它的长度为 4。

    思路:
    java中Hashset数组是个没有重复元素的集合,那么可以将所有数据存入到set中,之后进行顺序删除,把数组进行遍历,把每个遍历的元素进行删除,删除的最大长度则为数组最大序列长度。

    public int longestConsecutive(int[] nums) {
    if(nums.length<=0){
    return 0;
    }
    Set<Integer> set = new HashSet<>();
    for(int n : nums){
    set.add(n);
    }
    int maxv = 1;
    for(int n : nums){
    if(!set.remove(n)){
    continue;
    }
    int vb = 1;
    int va = 1;
    while (set.remove(n-vb)){ vb++; }
    while (set.remove(n+va)){ va++; }
    maxv = Math.max(maxv,vb + va -1);
    }
    return maxv;
    }

    时间复杂度为O(n)空间复杂度为O(n) remove 的次数最多为n


    算法二(奇偶数排序)

    给定一个整数数组,请调整数组中数的顺序,使得所有奇数位于数组的前半部分,所有偶数位于数组的后半部分。
    要求时间复杂度为O(n)
    思路:
    双指针法,可以两个指针分别从前往后,进行交换。

    算法三(两个有序整数数组成为一个有序数组)

    给你两个有序整数数组 nums1 和 nums2,请你将 nums2 合并到 nums1 中,使 nums1 成为一个有序数组。


    示例 1:
    输入:nums1 = [1,2,3,0,0,0], m = 3, nums2 = [2,5,6], n = 3
    输出:[1,2,2,3,5,6]

    思路:
    双指针法
    收起阅读 »

    算法与数据结构之算法复杂度

    情景回顾平时在聊天谈论算法时候,发现很多人并不清楚算法的时间复杂度怎么计算,一些稍微复杂的算法时间复杂度问题,就无法算出时间复杂度。那么我在今天的文章里去解答这些问题 时间复杂度与空间复杂度时间复杂度执行这个算法所需要的计算工作量 随着输入数据的规模,时间也...
    继续阅读 »

    情景回顾

    平时在聊天谈论算法时候,发现很多人并不清楚算法的时间复杂度怎么计算,一些稍微复杂的算法时间复杂度问题,就无法算出时间复杂度。那么我在今天的文章里去解答这些问题


    时间复杂度与空间复杂度

    时间复杂度

    执行这个算法所需要的计算工作量



    1. 随着输入数据的规模,时间也不断增长,那么时间复杂度就是一个算法性能可观的数据。


    2. 由于实际算法的时间会根据机器的性能,软件的环境导致不同的结果,那么通常都是预估这个时间。


    3. 算法花费时间与执行语句次数成正比,执行次数称为语句频度或时间频度。记为T(n)


    4. 在时间频度中,n称为问题的规模,当n不断变化时,它所呈现出来的规律,称之为时间复杂度



    说明:



    • 若算法中的语句执行次数为一个常数,则时间复杂度为o(1)
    • 不同算法的时间频度不一样,但他们的时间复杂度却可能是一样的。

    例如:T(n)=2n+4+n^2
    T(n)=n+8 + 4n^2
    很明显时间频度不同,但是他们时间复杂度是相同的 都为O(n^2)

    计算时间复杂度步骤:


    1. 找出算法中的基本语句(执行次数最多的那条语句就是基本语句,通常是最内层循环的循环体。)
    2. 计算基本语句的执行次数的数量级(只要保证基本语句执行次数的函数中的最高次幂正确即可,可以忽略所有低次幂和最高次幂的系数。这样能够简化算法分析,并且使注意力集中在最重要的一点上:增长率。)
    3. 用大Ο记号表示算法的时间性能。

    原则:



    1. 如果运行时间是常数量级,用常数1表示
    2. 只保留时间函数中的最高阶项
    3. 如果最高阶项存在,则省去最高阶项前面的系数

    例1:


            for(i in 0..n){
    a++
    }
    for(i in 0..n){
    for(i in 0..n){
    a++
    }
    }

    在看这个算法时候,很明显时间 复杂度为O(n+n^2) 但要留下最高阶 也就是O(n^2)
    例2:

            for(i in 0..n){
    a++
    b++
    c++
    }

    在看这个算法时候,很明显时间 复杂度为O(3n) 但要抹掉常数3 也就是O(n)


    例3:


            var i = 1
    while (i < n) {
    a++
    b++
    c++
    i *= 2
    }

    在看这个算法时候,很明显时间 复杂度为O(3logn) 但要抹掉常数3 也就是O(logn)


    例4:


               a++
    b++
    c++

    在看这个算法时候,很明显时间 复杂度为O(3) 常数级别的算法都为O(1)


    平衡二叉搜索树的时间复杂度是怎么计算出来的呢(重点)

    为啥我要加个重点标识呢?因为很多人都不晓得平衡二叉搜索树的O(logn)是咋计算出来的,今天我给大家掰扯掰扯


    假设生成高度树h的节点数是n(h) ,高度为h-1的节点数为n(h-1)
    他们之间的关系为 n(h) = n(h-1)+n(h-1)+1
    n(h) = 2n(h-1)+1
    n(h) 约等于 2n(h-1)
    注意的一点 n(h-1) 几乎是一半的节点数目
    基准点 h(0) = 1 h(1) = 3
    依次类推
    n(h) 约等于 h^2
    那么
    n(h) = h^2
    那么
    h = log2n
    则平衡二叉搜索树的时间复杂度 O(logn)

    时间复杂度比较

    常见的时问复杂度如表所
    在这里插入图片描述
    常用的时间复杂度所耗费的时间从小到大依次是:
    在这里插入图片描述
    复杂度在1s内,能处理的数据量大小
    在这里插入图片描述

    复杂度分析的4个概念


    1. 最坏情况时间复杂度:代码在最坏情况下执行的时间复杂度。
    2. 最好情况时间复杂度:代码在最理想情况下执行的时间复杂度。
    3. 平均时间复杂度:代码在所有情况下执行的次数的加权平均值。
    4. 均摊时间复杂度:在代码执行的所有复杂度情况中绝大部分是低级别的复杂度,个别情况是高级别复杂度且发生具有时序关系时,可以将个别高级别复杂度均摊到低级别复杂度上。基本上均摊结果就等于低级别复杂度。

    为什么要引入这4个概念?



    1. 同一段代码在不同情况下时间复杂度会出现量级差异,为了更全面,更准确的描述代码的时间复杂度,所以引入这4个概念。
    2. 代码复杂度在不同情况下出现量级差别时才需要区别这四种复杂度。大多数情况下,是不需要区别分析它们的。

    如何分析平均、均摊时间复杂度?



    1. 平均时间复杂度
      代码在不同情况下复杂度出现量级差别,则用代码所有可能情况下执行次数的加权平均值表示。
    2. 均摊时间复杂度
      两个条件满足时使用:
      1)代码在绝大多数情况下是低级别复杂度,只有极少数情况是高级别复杂度;
      2)低级别和高级别复杂度出现具有时序规律。均摊结果一般都等于低级别复杂度。

    最坏情况与平均情况

    我们查找一个有n 个随机数字数组中的某个数字,最好的情况是第一个数字就是,那么算法的时间复杂度为O(1),但也有可能这个数字就在最后一个位置上待着,那么算法的时间复杂度就是O(n),这是最坏的一种情况了。
    最坏情况运行时间是一种保证,那就是运行时间将不会再坏了。 在应用中,这是一种最重要的需求, 通常, 除非特别指定, 我们提到的运行时间都是最坏情况的运行时间。
    而平均运行时间也就是从概率的角度看, 这个数字在每一个位置的可能性是相同的,所以平均的查找时间为n/2次后发现这个目标元素。平均运行时间是所有情况中最有意义的,因为它是期望的运行时间。也就是说,我们运行一段程序代码时,是希望看到平均运行时间的。可现实中,平均运行时间很难通过分析得到,一般都是通过运行一定数量的实验数据后估算出来的。一般在没有特殊说明的情况下,都是指最坏时间复杂度。

    空间复杂度

    执行这个算法所需要的内存空间



    1. 空间复杂度是对一个算法在运行过程中临时占用存储空间大小的量度
    2. 在算法计算中,其中的辅助变量与空间复杂度相关
    3. 一般的,只要算法不涉及到动态分配的空间,以及递归、栈所需的空间,空间复杂度通常为O(1)
    4. 时间复杂度和空间复杂度往往是相互影响,空间复杂度越好相对的时间会耗费的较多,反之亦然。


            var m = arrayOfNulls<Int>(n)
    for(i in 0..n){
    a++
    m[i] = a
    }

    空间复杂度为 O(n)


    常用算法的时间复杂度和空间复杂度

    在这里插入图片描述
    收起阅读 »

    算法与数据结构之链表

    个人说明:拿过国内某算法大赛全国三等。。。(渣渣) 概念链表是计算机数据结构中比较重要的一个,也是最基础之一。在开发过程中,有些时候会采用这种结构。链表可以说是一种动态的数据结构。链表是一种物理存储上非连续的存储结构,数据的顺序与关联通过链表中的节点指针来实现...
    继续阅读 »

    个人说明:拿过国内某算法大赛全国三等。。。(渣渣)


    概念

    链表是计算机数据结构中比较重要的一个,也是最基础之一。在开发过程中,有些时候会采用这种结构。链表可以说是一种动态的数据结构。
    链表是一种物理存储上非连续的存储结构,数据的顺序与关联通过链表中的节点指针来实现。结点可以动态变更,那也就说明链表这种结构可快速添加数据。
    链表是一种线性表结构

    链表分类(以下图都是盗的,有侵的话私聊我)

    单链表

    链表中的元素的指向只能指向链表中的下一个元素或者为空,元素之间不能相互指向,是一种线性链表
    在这里插入图片描述

    循环链表

    在单向链表和双向链表的基础上,将两种链表的最后一个结点指向第一个结点从而实现循环
    在这里插入图片描述

    双向链表

    每个节点既有指向下一个元素的指针,又有指向前一个元素的指针,其中每个结点都有两种指针
    在这里插入图片描述

    基本单位

    节点
    包含下一节点(上一节点)指针与数据

    基本操作

    就拿单链表作为示例(其他链表操作没太大区别)


    删除节点

    在这里插入图片描述
    删除头节点:
    直接让root节点指向头节点的下一个节点
    删除中间节点:
    直接让中间节点的前一个节点指向当前节点的下一个节点
    删除尾节点:
    直接让当前尾节点的上一个节点指向null

    添加节点

    在这里插入图片描述
    增加的新节点会指向当前节点的下一个,那么当前节点的下一个又会指向新的节点。很快就增加了一个新节点。
    添加节点如果在头部的话,root指到新节点,新节点的下一个节点指向旧root
    添加尾节点的话,就让尾节点的下一个节点指向新节点
    例:java的LinkedList(本文针对的是1.7的源码)
    首先,先看看LinkedList的初始化,源码如下:
    LinkedList包含3个全局参数,
    size存放当前链表有多少个节点。
    first为指向链表的第一个节点的引用。
    last为指向链表的最后一个节点的引用。

    // 什么都没做,是一个空实现
    public LinkedList() {
    }

    public LinkedList(Collection<? extends E> c) {
    this();
    addAll(c);
    }

    public boolean addAll(Collection<? extends E> c) {
    return addAll(size, c);
    }

    public boolean addAll(int index, Collection<? extends E> c) {
    // 检查传入的索引值是否在合理范围内
    checkPositionIndex(index);
    // 将给定的Collection对象转为Object数组
    Object[] a = c.toArray();
    int numNew = a.length;
    // 数组为空的话,直接返回false
    if (numNew == 0)
    return false;
    // 数组不为空
    Node<E> pred, succ;
    if (index == size) {
    // 构造方法调用的时候,index = size = 0,进入这个条件。
    succ = null;
    pred = last;
    } else {
    // 链表非空时调用,node方法返回给定索引位置的节点对象
    succ = node(index);
    pred = succ.prev;
    }
    // 遍历数组,将数组的对象插入到节点中
    for (Object o : a) {
    @SuppressWarnings("unchecked") E e = (E) o;
    Node<E> newNode = new Node<>(pred, e, null);
    if (pred == null)
    first = newNode;
    else
    pred.next = newNode;
    pred = newNode;
    }

    if (succ == null) {
    last = pred; // 将当前链表最后一个节点赋值给last
    } else {
    // 链表非空时,将断开的部分连接上
    pred.next = succ;
    succ.prev = pred;
    }
    // 记录当前节点个数
    size += numNew;
    modCount++;
    return true;
    }

    Node节点类:


        private static class Node<E> {
    E item;
    Node<E> next;
    Node<E> prev;

    Node(Node<E> prev, E element, Node<E> next) {
    this.item = element;
    this.next = next;
    this.prev = prev;
    }
    }

    addFirst/addLast


    public void addFirst(E e) {
    linkFirst(e);
    }

    private void linkFirst(E e) {
    final Node<E> f = first;
    final Node<E> newNode = new Node<>(null, e, f); // 创建新的节点,新节点的后继指向原来的头节点,即将原头节点向后移一位,新节点代替头结点的位置。
    first = newNode;
    if (f == null)
    last = newNode;
    else
    f.prev = newNode;
    size++;
    modCount++;
    }

    getFirst/getLast


    public E getFirst() {
    final Node<E> f = first;
    if (f == null)
    throw new NoSuchElementException();
    return f.item;
    }

    public E getLast() {
    final Node<E> l = last;
    if (l == null)
    throw new NoSuchElementException();
    return l.item;
    }

    get方法,在操作时候会判断索引位置,来从后或从前找,提升查找效率


    public E get(int index) {
    // 校验给定的索引值是否在合理范围内
    checkElementIndex(index);
    return node(index).item;
    }

    Node<E> node(int index) {
    //如果索引小于size的一半则从前往后找,如果大于则从后往前找
    if (index < (size >> 1)) {
    Node<E> x = first;
    for (int i = 0; i < index; i++)
    x = x.next;
    return x;
    } else {
    Node<E> x = last;
    for (int i = size - 1; i > index; i--)
    x = x.prev;
    return x;
    }
    }

    removeFirst/removeLast


    public E removeFirst() {
    final Node<E> f = first;
    if (f == null)
    throw new NoSuchElementException();
    return unlinkFirst(f);
    }

    private E unlinkFirst(Node<E> f) {
    // assert f == first && f != null;
    final E element = f.item;
    final Node<E> next = f.next;
    f.item = null;
    f.next = null; // help GC
    //摘掉头结点,将原来的第二个节点变为头结点,改变frist的指向
    first = next;
    if (next == null)
    last = null;
    else
    next.prev = null;
    size--;
    modCount++;
    return element;
    }

    有关链表的算法

    以下算法只给思路(代码写一写就知道了)


    链表算法注意事项

    并没什么可注意的,哈哈哈哈
    在这里插入图片描述
    有一点就是:链表的插入和删除操作O(1)的复杂度(主要就是结构可以让他这么屌)

    算法一(单链表的倒数第K个结点)

    这是个比较简单的算法
    思路:由于单链表的结构无法从链表尾节点来倒序查找,那么你可以两个节点相差k-1来计算倒数k个节点,如果节点1的next是空,那么节点2就是你要找的。

    算法二(判断一个链表有环)

    首先创建两个指针1和2,同时指向这个链表的头节点。让指针1每次向下移动一个节点,让指针2每次向下移动两个节点,然后比较两个指针指向的节点是否相同。如果相同,则判断出链表有环,如果不同,则继续下一次循环。


    算法三(两个链表的第一个公共结点)

    将两个链表的节点分别存在两个栈中,


    生活中的算法(排排坐,赶走第三者)

    题目:
    有100人排排坐,从第一个人开始,喊号(1,2,3)。第一个人喊1,第二个人喊2,第三个人喊3,这时候第三个人就出队,第四个人开始喊1,如此循环,最后剩下哪个人?
    思路:
    循环链表,把100个人放到循环链表里,循环每次到三时候,把三这个删除节点,继续循环,直到一个节点的next指针指到自己,结束循环。
    收起阅读 »

    Jetpack之Lifecycle

    1.什么是Lifecycle Lifecycle是具备宿主生命周期感知能力的组件。它能持有组件(如Activity或Fragment)生命周期状态的信息,并且允许其他观察者监听宿主的状态。它也是Jetpack组件库的核心基础,包括我们就会讲到的LiveData...
    继续阅读 »

    1.什么是Lifecycle


    Lifecycle是具备宿主生命周期感知能力的组件。它能持有组件(如Activity或Fragment)生命周期状态的信息,并且允许其他观察者监听宿主的状态。它也是Jetpack组件库的核心基础,包括我们就会讲到的LiveData,ViewModel组件等也都是基于它来实现的。



    再也不用手动分发宿主生命周期,再也不用手动反注册了



    2.Lifecycle的使用方法


    使用Lifecycle前需要先添加依赖


    //通常情况下,只需要添加appcompat就可以了
    api 'androidx.appcompat:appcompat:1.1.0'

    //如果想单独使用,可引入下面这个依赖
    api 'androidx.lifecycle:lifecycle-common:2.1.0'
    复制代码

    被观察者


    通过实现LifecycleOwner接口


    androidx中的ComponentActivity和Fragment自生都已经实现了LifecycleOwner。


    public class ComponentActivity extends androidx.core.app.ComponentActivity implements
    LifecycleOwner,
    ViewModelStoreOwner,
    HasDefaultViewModelProviderFactory,
    SavedStateRegistryOwner,
    OnBackPressedDispatcherOwner {}

    //androidx.fragment.app.Fragment
    public class Fragment implements ComponentCallbacks, OnCreateContextMenuListener, LifecycleOwner,
    ViewModelStoreOwner, HasDefaultViewModelProviderFactory, SavedStateRegistryOwner {}
    复制代码

    如果要自己实现可以参考androidx的Activity和Fragment的实现。


    观察者


    方式一:LifecycleObserver配合注解


    //1. 自定义的LifecycleObserver观察者,用注解声明每一个观察者的宿主状态
    class LocationObserver implements LifecycleObserver{
    //宿主执行了onstart时,会分发该事件
    @OnLifecycleEvent(Lifecycle.Event.ON_START)
    void onStart(@NotNull LifecycleOwner owner){
    //开启定位
    }

    //宿主执行了onstop时 会分发该事件
    @OnLifecycleEvent(Lifecycle.Event.ON_STOP)
    void onStop(@NotNull LifecycleOwner owner){
    //停止定位
    }
    }

    //2. 注册观察者,观察宿主生命周期状态变化
    class MyFragment extends Fragment{
    public void onCreate(Bundle bundle){
    LocationObserver observer =new LocationObserver()
    getLifecycle().addObserver(observer);
    }
    }
    复制代码

    方式二:实现FullLifecyclerObserver接口


    class LocationObserver implements FullLifecycleObserver{

    void onCreate(LifecycleOwner owner);

    void onStart(LifecycleOwner owner);

    void onResume(LifecycleOwner owner);

    void onPause(LifecycleOwner owner);

    void onStop(LifecycleOwner owner);

    void onDestroy(LifecycleOwner owner);

    }
    复制代码

    方式三:LifecycleEventObserver宿主生命周期事件封装成Lifecycle.Event


    //1.源码
    public interface LifecycleEventObserver extends LifecycleObserver {
    void onStateChanged(LifecycleOwner source, Lifecycle.Event event);
    }
    //2.用法
    class LocationObserver extends LifecycleEventObserver{
    @override
    void onStateChanged(LifecycleOwner source, Lifecycle.Event event){
    //需要自行判断life-event是onstart, 还是onstop
    }
    }
    复制代码

    上面的这两种Lifecycle写法比较推荐第二种和第三种,因为第一种你虽然用注解很爽,但是如果没有添加lifecycle-compiler这个注解处理器的话,运行时会使用反射的形式回调到对应的方法上。


    订阅关系


    //被观察者.addObserver(观察者)
    getLifecycle().addObserver(presenter);
    复制代码

    3.实现原理


    引入的库


    dependencies {
    implementation "androidx.lifecycle:lifecycle-runtime:2.0.0"
    implementation "androidx.lifecycle:lifecycle-extensions:2.0.0"
    implementation "androidx.lifecycle:lifecycle-common-java8:2.0.0"
    annotationProcessor "androidx.lifecycle:lifecycle-compiler:2.0.0"
    }
    复制代码

    核心类


    事件在分发宿主生命期事件的流程中涉及到三个类:



    • LifecycleOwner :Activity和Fragment都实现了该接口,用以生命周期观察,它是一个能够提供生命周期事件的宿主 。同时必须复写getLifecycle()方法提供一个Lifecycle对象;可以将其理解为观察者模式中的Observable。

    • Lifecycle :是一个抽象类 ,里面定义了两个枚举State宿主的状态,Event需要分发的事件的类型;

    • LifecycleRegistry :是Lifecycle的唯一实现类 ,主要用来负责注册Observer ,以及分发宿主状态事件给它们。LifecycleRegistr聚合多个LifecycleObserver,生命周期改变时通知LifecycleObserver进行相应的方法调用。


    img


    具体的类关系请看下面的Fragment的基本实现。


    宿主生命周期监听


    Fragment是如何实现Lifecycle的?


    使用Fragment实现Lifecycle需要在各个生命周期方法内里用LifecycleRegistry分发相应的事件给每个观察者,以实现生命周期观察的能力:


    public class Fragment implements LifecycleOwner {

    LifecycleRegistry mLifecycleRegistry = new LifecycleRegistry(this);

    @Override
    public Lifecycle getLifecycle() {
    //复写自LifecycleOwner,所以必须new LifecycleRegistry对象返回
    return mLifecycleRegistry;
    }

    void performCreate(){
    mLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE);
    }

    void performStart(){
    mLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START);
    }
    .....
    void performResume(){
    mLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_RESUME);
    }

    }
    复制代码

    Activity是如何实现Lifecycle的?


    Activity实现Lifecycle需要借助于ReportFragment往Activity上添加一个fragment用以报告生命周期的变化。目的是为了兼容不是集成自
    AppCompactActivity的场景,同时也支持我们自定义LifecycleOwener的场景。


    public class ComponentActivity extends Activity implements LifecycleOwner{

    private LifecycleRegistry mLifecycleRegistry = new LifecycleRegistry(this);
    @NonNull
    @Override
    public Lifecycle getLifecycle() {
    return mLifecycleRegistry;
    }

    protected void onCreate(Bundle bundle) {
    super.onCreate(savedInstanceState);
    //往Activity上添加一个fragment,用以报告生命周期的变化
    //目的是为了兼顾不是继承自AppCompactActivity的场景.
    ReportFragment.injectIfNeededIn(this);
    }
    复制代码

    注意:ComponentActivity的部分生命周期是自己实现的,并不是全部都是ReportFragment实现的。


    ReportFragment核心源码


    这里的实现其实跟Fragment中的源码是一样的,在各个生命周期方法内利用LifecycleRegistry派发相应的Lifecycle.Event事件给每个观察者。


    注意:在具体的生命周期的调用上有所差异:


    SDK>=29:用LifecycleCallbacks进行回调


    SDK<29:用ReportFragment的生命周期中调用


    public class ReportFragment extends Fragment{


    public static void injectIfNeededIn(Activity activity) {

    //注意:在29以后对Activity注册了一个LifecycleCallbacks,这里的dispatch的调用时机有所差异
    if (Build.VERSION.SDK_INT >= 29) {
    activity.registerActivityLifecycleCallbacks(
    new LifecycleCallbacks());
    }


    android.app.FragmentManager manager = activity.getFragmentManager();
    if (manager.findFragmentByTag(REPORT_FRAGMENT_TAG) == null) {
    manager.beginTransaction().add(new ReportFragment(), REPORT_FRAGMENT_TAG).commit();
    manager.executePendingTransactions();
    }
    }


    @Override
    public void onStart() {
    super.onStart();
    dispatch(Lifecycle.Event.ON_START);
    }
    @Override
    public void onResume() {
    super.onResume();
    dispatch(Lifecycle.Event.ON_RESUME);
    }
    @Override
    public void onPause() {
    super.onPause();
    dispatch(Lifecycle.Event.ON_PAUSE);
    }
    @Override
    public void onDestroy() {
    super.onDestroy();
    dispatch(Lifecycle.Event.ON_DESTROY);
    }

    //只要<29才借助的ReportFragment的生命周期
    private void dispatch(@NonNull Lifecycle.Event event) {
    if (Build.VERSION.SDK_INT < 29) {
    dispatch(getActivity(), event);
    }
    //29之后的另外处理
    }

    //最终收拢在这里处理
    static void dispatch(@NonNull Activity activity, @NonNull Lifecycle.Event event) {
    //兼容Activity自己实现的LifecycleRegistryOwner
    if (activity instanceof LifecycleRegistryOwner) {
    ((LifecycleRegistryOwner) activity).getLifecycle().handleLifecycleEvent(event);
    return;
    }

    if (activity instanceof LifecycleOwner) {
    Lifecycle lifecycle = ((LifecycleOwner) activity).getLifecycle();
    if (lifecycle instanceof LifecycleRegistry) {
    ((LifecycleRegistry) lifecycle).handleLifecycleEvent(event);
    }
    }
    }
    }
    复制代码

    SDK29以后的实现调用


    //ReportFragment的静态内部类
    static class LifecycleCallbacks implements Application.ActivityLifecycleCallbacks {
    @Override
    public void onActivityCreated(@NonNull Activity activity,
    @Nullable Bundle bundle) {
    }

    @Override
    public void onActivityPostCreated(@NonNull Activity activity,
    @Nullable Bundle savedInstanceState) {
    //调用的是静态方法
    dispatch(activity, Lifecycle.Event.ON_CREATE);
    }

    @Override
    public void onActivityStarted(@NonNull Activity activity) {
    }

    @Override
    public void onActivityPostStarted(@NonNull Activity activity) {
    dispatch(activity, Lifecycle.Event.ON_START);
    }

    @Override
    public void onActivityResumed(@NonNull Activity activity) {
    }

    @Override
    public void onActivityPostResumed(@NonNull Activity activity) {
    dispatch(activity, Lifecycle.Event.ON_RESUME);
    }

    @Override
    public void onActivityPrePaused(@NonNull Activity activity) {
    dispatch(activity, Lifecycle.Event.ON_PAUSE);
    }

    @Override
    public void onActivityPaused(@NonNull Activity activity) {
    }

    @Override
    public void onActivityPreStopped(@NonNull Activity activity) {
    dispatch(activity, Lifecycle.Event.ON_STOP);
    }

    @Override
    public void onActivityStopped(@NonNull Activity activity) {
    }

    @Override
    public void onActivitySaveInstanceState(@NonNull Activity activity,
    @NonNull Bundle bundle) {
    }

    @Override
    public void onActivityPreDestroyed(@NonNull Activity activity) {
    dispatch(activity, Lifecycle.Event.ON_DESTROY);
    }

    @Override
    public void onActivityDestroyed(@NonNull Activity activity) {
    }
    }
    复制代码

    在Activity中的生命周期对进行回调


        //Activity
    private void dispatchActivityCreated(@Nullable Bundle savedInstanceState) {
    getApplication().dispatchActivityCreated(this, savedInstanceState);
    Object[] callbacks = collectActivityLifecycleCallbacks();
    if (callbacks != null) {
    for (int i = 0; i < callbacks.length; i++) {
    ((Application.ActivityLifecycleCallbacks) callbacks[i]).onActivityCreated(this,
    savedInstanceState);
    }
    }
    }

    private void dispatchActivityPostCreated(@Nullable Bundle savedInstanceState) {
    Object[] callbacks = collectActivityLifecycleCallbacks();
    if (callbacks != null) {
    for (int i = 0; i < callbacks.length; i++) {
    //回调监听
    ((Application.ActivityLifecycleCallbacks) callbacks[i]).onActivityPostCreated(this,
    savedInstanceState);
    }
    }
    getApplication().dispatchActivityPostCreated(this, savedInstanceState);
    }
    复制代码

    而dispatch()方法则会判断Activity是否实现了LifecycleOwner接口,如果实现了该接口就调用LifecycleRegister的handleLifecycleEvent()
    这样生命周期的状态就会借由LifecycleRegistry通知给各个LifecycleObserver从而调用其中对应Lifecycle.Event的方法。这种通过Fragment来感知Activity生命周期的方法其实在Glide的中也是有体现的。


    宿主生命周期与宿主状态模型图


    LifecycleRegistry在分发事件的时候会涉及到两个概念:



    • 宿主生命周期 :就是我们烂熟于心的onCreate,onStart,onResume,onPause,onStop...;

    • 宿主的状态 :这个不是很好理解,这个意思是指宿主执行了上述方法后,它处于对应周期的生命状态。


    从下面这张图不难看出宿主生命周期与宿主状态的对应关系分裂为onCreate-Created、onStart-Started、onResume-Resumed、onPause-Started、onStop-Created、onDestroy-Destroyed,这里不用全部记住有个印象即可。



    添加observer时,完整的生命周期事件分发


    基于Lifecycle的特性我们在任意生命周期方法内注册观察者都能接受到完整的生命周期事件,比如在onResume中注册一个观察者,它会依次收到:


    LifecycleEvent.onCreate -> LifecycleEvent.onStart -> LifecycleEvent.onResume
    复制代码

    image-20201224082259033


    分发同步生命周期


    添加Observer时完整的生命周期事件分发


    这一点需要掌握,面试中是肯定会考察的。但是如果没有看过源码是回答不上来的:


    public void addObserver(@NonNull LifecycleObserver observer) {
    //1.初始状态。添加新的Observer时,会首先根据宿主的状态计算出它的初始状态,只要不是在onDestroy中注册的,它的初始状态都是INITIALIZED
    State initialState = mState == DESTROYED ? DESTROYED : INITIALIZED;

    //2.将观察者和状态封装。
    //会把observer包装成ObserverWithState,这个类主要是包含了观察者及其状态。每个事件都会经由这个对象类转发,这个类后面会来分析
    ObserverWithState statefulObserver = new ObserverWithState(observer, initialState);
    //3.添加到集合,如果之前已经添加过了,则return
    ObserverWithState previous = mObserverMap.putIfAbsent(observer, statefulObserver);
    if (previous != null) {
    return;
    }

    State targetState = calculateTargetState(observer);
    //4.同步状态
    //这里的while循环,是实现上图状态同步与事件分发的主要逻辑
    //拿观察者的状态和宿主当前状态做比较,如果小于0,说明两者状态还没有对齐。
    while ((statefulObserver.mState.compareTo(targetState) < 0
    && mObserverMap.contains(observer))) {
    pushParentState(statefulObserver.mState);
    //接着就会分发一次相应的事件,于此同时statefulObserver的mState对象也会被升级
    //假设是在宿主的onresume方法内注册的该观察者
    //第一次:分发on_Create事件,观察者状态INIT->CREATED
    //第二次:分发on_Start事件,观察者状态CREATED->STARTED
    //第三次:分发on_Resume事件,观察者状态STARTED->RESUMED
    statefulObserver.dispatchEvent(lifecycleOwner, upEvent(statefulObserver.mState));
    //再一次计算观察者应该到达的状态,在下一轮循环中和宿主状态在做比较,知道两者状态对齐,退出循环。
    targetState = calculateTargetState(observer);
    }
    }
    复制代码

    宿主生命周期变化后相应事件的分发


    这一点了解即可,面试中也不会考这一部分的内容:


    //宿主生命周期变化时触发
    public void handleLifecycleEvent(@NonNull Lifecycle.Event event){
    //宿主的每个生命周期的变化都会分发一个对应的Lifecycle.Event,走到这里
    //此时会根据需要分发的事件反推出宿主当前的状态
    State next = getStateAfter(event);
    // moveToState方法只是将传入的宿主新的state和前持有宿主状态作比对,然后保存一下。
    moveToState(next);
    }

    private void moveToState(State next) {
    if (mState == next) {
    return;
    }
    mState = next;
    if (mHandlingEvent || mAddingObserverCounter != 0) {
    mNewEventOccurred = true;
    // we will figure out what to do on upper level.
    return;
    }
    mHandlingEvent = true;
    sync();
    mHandlingEvent = false;
    }

    //如果宿主状态有变动,则调用sync方法来完成事件的分发和观察者状态的同步
    private void sync() {
    while (!isSynced()) {
    //如果宿主当前转态 小于 mObserverMap集合中最先添加的那个观察者的状态
    //则说明宿主可能发生了状态回退,比如当前是RESUMED状态,执行了onPause则回退到STARTED状态
    //此时调用backwardPass把集合中的每个一观察者分发一个on_pause事件,并同步它的状态。
    if (mState.compareTo(mObserverMap.eldest().getValue().mState) < 0) {
    backwardPass(lifecycleOwner);
    }
    //如果宿主当前转态 大于 mObserverMap集合中最先添加的那个观察者的状态
    //则说明宿主可能发生了状态前进,比如当前是STARTED状态,执行了onResume则前进到RESUMED状态
    //此时调用forwardPass把集合中的每个一观察者分发一个on_resume事件,并同步它的状态。
    Entry<LifecycleObserver, ObserverWithState> newest = mObserverMap.newest();
    if (!mNewEventOccurred && newest != null
    && mState.compareTo(newest.getValue().mState) > 0) {
    forwardPass(lifecycleOwner);
    }
    }
    }
    复制代码

    同步状态


    ObserverWithState:持有观察者及其状态的内部类


    把传入的LifecycleObserver适配成LifecycleEventObserver,目的是为了统一事件的分发形式。


    持有观察者的状态,方便与宿主状态做比对同步:


    static class ObserverWithState {
    State mState;
    LifecycleEventObserver mLifecycleObserver;
    ObserverWithState(LifecycleObserver observer, State initialState) {
    //把传入的LifecycleObserver适配成LifecycleEventObserver,目的是为了统一事件的分发形式
    //因为我们前面提到观察者有三种类型,每种类型接收事件的形式并不一样,如果在分发的时候不统一事件分发的形式,将会变得很麻烦
    //至于是如何适配转换的,由于不是本文重点,所以不再详细展开
    //但核心思想这里说明一下,同学们自行看下就能明白
    //它会判断传入的observer是前面提到的那一种类型,进而转换成对应的适配器类,适配器类会对onStateChanged方法进行适配,并以相应的方式(反射、中转、)把事件转发到我们的observer上
    mLifecycleObserver = Lifecycling.lifecycleEventObserver(observer);
    mState = initialState;
    }

    void dispatchEvent(LifecycleOwner owner, Event event) {
    //再一次根据需要分发的事件类型反推出该观察者的状态,这样的好处是事件 & 状态 一一对应,不会出现跳跃。但阅读上可能会稍微有点绕
    State newState = getStateAfter(event);
    mState = min(mState, newState);
    //把事件分发给被包装的对象,完成本次流程。
    mLifecycleObserver.onStateChanged(owner, event);
    mState = newState;
    }
    }
    复制代码

    4.总结


    本篇从三种用法+分发原理+面试考点 三个维度展开对Lifecycle组件的介绍,现在相信同学们已经掌握了Lifecycle的核心了。Lifecycle组件是Jetpack组件库的核心,一旦跟宿主生命周期挂钩,那可以做很多文章,后面讲到的LiveData、ViewModel都是基于它来实现的。


    5.拓展


    基于Lifecycle实现APP前后台切换事件观察的能力。


    参考:使用 ProcessLifecycle 优雅地监听应用前后台切换


    class AppLifecycleOwner implements LifecycleOwner{
    LifecycleRegistry registry = new LifecycleRegistry(this)
    @override
    Lifecycle getLifecycle(){
    return registry
    }

    void init(Application application){
    //利用application的 ActivityLifecycleCallbacks 去监听每一个 Activity的onstart,onStop事件。
    //计算出可见的Activity数量,从而计算出当前处于前台还是后台。然后分发给每个观察者
    }
    }

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

    java设计模式:抽象工厂模式

    定义是一种为访问类提供一个创建一组相关或相互依赖对象的接口,且访问类无须指定所要产品的具体类就能得到同族的不同等级的产品的模式结构。 抽象工厂模式是工厂方法模式的升级版本,工厂方法模式只生产一个等级的产品,而抽象工厂模式可生产多个等级的产品。 使用抽象工厂模式...
    继续阅读 »

    定义

    是一种为访问类提供一个创建一组相关或相互依赖对象的接口,且访问类无须指定所要产品的具体类就能得到同族的不同等级的产品的模式结构。


    抽象工厂模式是工厂方法模式的升级版本,工厂方法模式只生产一个等级的产品,而抽象工厂模式可生产多个等级的产品。


    使用抽象工厂模式一般要满足以下条件。



    • 系统中有多个产品族,每个具体工厂创建同一族但属于不同等级结构的产品。
    • 系统一次只可能消费其中某一族产品,即同族的产品一起使用。

    优点

    抽象工厂模式除了具有工厂方法模式的优点外,其他主要优点如下。



    • 可以在类的内部对产品族中相关联的多等级产品共同管理,而不必专门引入多个新的类来进行管理。
    • 当需要产品族时,抽象工厂可以保证客户端始终只使用同一个产品的产品组。
    • 抽象工厂增强了程序的可扩展性,当增加一个新的产品族时,不需要修改原代码,满足开闭原则。

    缺点

    当产品族中需要增加一个新的产品时,所有的工厂类都需要进行修改。增加了系统的抽象性和理解难度。


    代码实现

    抽象工厂模式的主要角色如下。



    • 抽象工厂:提供了创建产品的接口,它包含多个创建产品的方法 newProduct(),可以创建多个不同等级的产品。
    • 具体工厂:主要是实现抽象工厂中的多个抽象方法,完成具体产品的创建。
    • 抽象产品:定义了产品的规范,描述了产品的主要特性和功能,抽象工厂模式有多个抽象产品。
    • 具体产品:实现了抽象产品角色所定义的接口,由具体工厂来创建,它同具体工厂之间是多对一的关系。

    在这里插入图片描述

    interface IProduct{
    fun setPingPai(string: String)
    fun showName() :String
    }

    interface ITools{
    fun setPingPai(string: String)
    fun showName() :String
    }

    class Dog : IProduct{
    var pinPai:String? = null
    override fun setPingPai(string: String) {
    this.pinPai = string
    }

    override fun showName() = "dog"

    }

    class DogTools : ITools{
    var pinPai:String? = null
    override fun setPingPai(string: String) {
    this.pinPai = string
    }

    override fun showName() = "DogTools"

    }

    class Cat : IProduct{
    var pinPai:String? = null
    override fun setPingPai(string: String) {
    this.pinPai = string
    }
    override fun showName() = "cat"
    }

    class CatTools : ITools{
    var pinPai:String? = null
    override fun setPingPai(string: String) {
    this.pinPai = string
    }
    override fun showName() = "CatTools"
    }

    interface IFactory{
    fun getPinPai():String
    fun createProduct(type:Int):IProduct
    fun createProductTools(type:Int):ITools
    }

    class ABCFactory():IFactory{
    override fun getPinPai() = "ABC"

    override fun createProduct(type: Int): IProduct {
    return when(type){
    1-> Dog().apply { setPingPai(getPinPai()) }
    2-> Cat().apply { setPingPai(getPinPai()) }
    else -> throw NullPointerException()
    }
    }

    override fun createProductTools(type: Int): ITools {
    return when(type){
    1-> DogTools().apply { setPingPai(getPinPai()) }
    2-> CatTools().apply { setPingPai(getPinPai()) }
    else -> throw NullPointerException()
    }
    }

    }

    class CBDFactory():IFactory{
    override fun getPinPai() = "CBD"
    override fun createProduct(type: Int): IProduct {
    return when(type){
    1-> Dog().apply { setPingPai(getPinPai()) }
    2-> Cat().apply { setPingPai(getPinPai()) }
    else -> throw NullPointerException()
    }
    }

    override fun createProductTools(type: Int): ITools {
    return when(type){
    1-> DogTools().apply { setPingPai(getPinPai()) }
    2-> CatTools().apply { setPingPai(getPinPai()) }
    else -> throw NullPointerException()
    }
    }

    }

    收起阅读 »

    iOS 唤起APP之Universal Link(通用链接)

    iOS 9之前,一直使用的是URL Schemes技术来从外部对App进行跳转,但是iOS系统中进行URL Schemes跳转的时候如果没有安装App,会提示Cannot open Page的提示,而且当注册有多个scheme相同的时候,目前没有办法区分,但是...
    继续阅读 »

    iOS 9之前,一直使用的是URL Schemes技术来从外部对App进行跳转,但是iOS系统中进行URL Schemes跳转的时候如果没有安装App,会提示Cannot open Page的提示,而且当注册有多个scheme相同的时候,目前没有办法区分,但是从iOS 9起可以使用Universal Links技术进行跳转页面,这是一种体验更加完美的解决方案


    什么是Universal Link(通用链接)
    Universal Link是Apple在iOS 9推出的一种能够方便的通过传统HTTPS链接来启动APP的功能。如果你的应用支持Universal Link,当用户点击一个链接时可以跳转到你的网站并获得无缝重定向到对应的APP,且不需要通过Safari浏览器。如果你的应用不支持的话,则会在Safari中打开该链接

    支持Universal Link(通用链接)
    先决条件:必须有一个支持HTTPS的域名,并且拥有该域名下上传到根目录的权限(为了上传Apple指定文件)

    集成步骤

    1、开发者中心配置
    找到对应的App ID,在Application Services列表里有Associated Domains一条,把它变为Enabled就可以了

    2、工程配置
    targets->Capabilites->Associated Domains,在其中的Domains中填入你想支持的域名,必须以applinks:为前缀,如:applinks:domain

    3、配置指定文件
    创建一个内容为json格式的文件,苹果将会在合适的时候,从我们在项目中填入的域名请求这个文件。这个文件名必须为apple-app-site-association,切记没有后缀名,文件内容大概是这样子:

    {
    “applinks”: {
    “apps”: [],
    “details”: [
    {
    “appID”: “9JA89QQLNQ.com.apple.wwdc”,
    “paths”: [ “/wwdc/news/“, “/videos/wwdc/2015/“]
    },
    {
    “appID”: “ABCD1234.com.apple.wwdc”,
    “paths”: [ ““ ]
    }
    ]
    }
    }
    复制代码appID:组成方式是TeamID.BundleID。如上面的9JA89QQLNQ就是teamId。登陆开发者中心,在Account -> Membership里面可以找到Team ID
    paths:设定你的app支持的路径列表,只有这些指定路径的链接,才能被app所处理。*的写法代表了可识别域名下所有链接

    4、上传该文件
    上传该文件到你的域名所对应的根目录或者.well-known目录下,这是为了苹果能获取到你上传的文件。上传完后,先访问一下,看看是否能够获取到,当你在浏览器中输入这个文件链接后,应该是直接下载apple-app-site-association文件

    5、代码中的相关支持
    当点击某个链接,可以直接进我们的app,但是我们的目的是要能够获取到用户进来的链接,根据链接来展示给用户相应的内容,我们需要在工程里实现AppDelegate对应的方法:


    • (BOOL)application:(UIApplication )application continueUserActivity:(NSUserActivity )userActivity restorationHandler:(void (^)(NSArray * _Nullable))restorationHandler {
      // NSUserActivityTypeBrowsingWeb 由Universal Links唤醒的APP
      if ([userActivity.activityType isEqualToString:NSUserActivityTypeBrowsingWeb]){
        NSURL *webpageURL = userActivity.webpageURL;
      NSString *host = webpageURL.host;
      if ([host isEqualToString:@"api.r2games.com.cn"]){
      //进行我们的处理
      NSLog(@"TODO....");
      }else{
      NSLog(@"openurl");
      [[UIApplication sharedApplication] openURL:webpageURL options:nil completionHandler:nil];
      // [[UIApplication sharedApplication] openURL:webpageURL];
      }
      }
      return YES;
      }
      复制代码苹果为了方便开发者,提供了一个网页验证我们编写的这个apple-app-site-association是否合法有效

    Universal Link(通用链接)注意点


    Universal Link跨域
    Universal Link有跨域问题,Universal Link必须要求跨域,如果不跨域,就不会跳转(iOS 9.2之后的改动)
    假如当前网页的域名是A,当前网页发起跳转的域名是B,必须要求B和A是不同域名才会触发Universal Link,如果B和A是相同域名,只会继续在当前WebView里面进行跳转,哪怕你的Universal Link一切正常,根本不会打开App
    Universal Link请求apple-app-site-association时机

    当我们的App在设备上第一次运行时,如果支持Associated Domains功能,那么iOS会自动去GET定义的Domain下的apple-app-site-association文件


    iOS会先请求https://domain.com/.well-known/apple-app-site-association,如果此文件请求不到,再去请求https://domain.com/apple-app-site-association,所以如果想要避免服务器接收过多GET请求,可以直接把apple-app-site-association放在./well-known目录下


    服务器上apple-app-site-association的更新不会让iOS本地的apple-app-site-association同步更新,即iOS只会在App第一次启动时请求一次,以后除非App更新或重新安装,否则不会在每次打开时请求apple-app-site-association


    Universal Link的好处


    之前的Custom URL scheme是自定义的协议,因此在没有安装该app的情况下是无法直接打开的。而Universal Links本身就是一个能够指向web页面或者app内容页的标准web link,因此能够很好的兼容其他情况
    Universal links是从服务器上查询是哪个app需要被打开,因此不存在Custom URL scheme那样名字被抢占、冲突的情况
    Universal links支持从其他app中的UIWebView中跳转到目标app
    提供Universal link给别的app进行app间的交流时,对方并不能够用这个方法去检测你的app是否被安装(之前的custom scheme URL的canOpenURL方法可以)

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

    kotlin json解析库moshi

    Moshi 是Square公司在2015年6月开源的一个 Json 解析库,相对于Gson,FastJson等老牌解析库而言,Moshi不仅支持对Kotlin的解析,并且提供了Reflection跟Annotion两种解析Kotl...
    继续阅读 »

    Moshi 是Square公司在2015年6月开源的一个 Json 解析库,相对于Gson,FastJson等老牌解析库而言,Moshi不仅支持对Kotlin的解析,并且提供了Reflection跟Annotion两种解析Kotlin的方法,除此之外,Moshi最大的改变在于支持自定义JsonAdapter,能够将Json的Value转换成任意你需要的类型。

    基本用法之Java

    Dependency

    implementation 'com.squareup.moshi:moshi:1.7.0'
    复制代码

    Bean

    String json = ...;
    Moshi moshi = new Moshi.Builder().build();
    JsonAdapter<Bean> jsonAdapter = moshi.adapter(Bean.class);
    //Deserialize
    Bean bean = jsonAdapter.fromJson(json);
    //Serialize
    String json = jsonAdapter.toJson(bean);
    复制代码

    List

    Moshi moshi = new Moshi.Builder().build();
    Type listOfCardsType = Types.newParameterizedType(List.class, Bean.class);
    JsonAdapter<List<Bean>> jsonAdapter = moshi.adapter(listOfCardsType);
    //Deserialize
    List<Bean> beans = jsonAdapter.fromJson(json);
    //Serialize
    String json = jsonAdapter.fromJson(json);
    复制代码

    Map

    Moshi moshi = new Moshi.Builder().build();
    ParameterizedType newMapType = Types.newParameterizedType(Map.class, String.class, Integer.class);
    JsonAdapter<Map<String,Integer>> jsonAdapter = moshi.adapter(newMapType);
    //Deserialize
    Map<String,Integer> beans = jsonAdapter.fromJson(json);
    //Serialize
    String json = jsonAdapter.fromJson(json);
    复制代码

    Others

    • @json:Key转换
    • transitent:跳过该字段不解析
    public final class Bean {
    @Json(name = "lucky number") int luckyNumber;
    @Json(name = "objec") int data;
    @Json(name = "toatl_price") String totolPrice;
    private transient int total;//jump the field
    }
    复制代码

    基本用法之Kotlin

    相对于 Java 只能通过反射进行解析,针对Kotlin,Moshi提供了两种解析方式,一种是通过 Reflection ,一种是通过 Annotation ,你可以采用其中的一种,也可以两种都使用,下面分别介绍下这两种解析方式

    Dependency

    implementation 'com.squareup.moshi:moshi-kotlin:1.7.0'
    复制代码

    Reflection

    Data类

    data class ConfigBean(
    var isGood: Boolean = false,
    var title: String = "",
    var type: CustomType = CustomType.DEFAULT
    )
    复制代码

    开始解析

    val moshi = Moshi.Builder()
    .add(KotlinJsonAdapterFactory())
    .build()
    复制代码

    这种方式会引入Kotlin-Reflect的Jar包,大概有2.5M。

    Annotation

    上面提到了Reflection,会导致APK体积增大,所以Moshi还提供了另外一种解析方式,就是注解,Moshi的官方叫法叫做Codegen,因为是采用注解生成的,所以除了添加Moshi的Kotlin依赖之外,还需要加上kapt

    kapt 'com.squareup.moshi:moshi-kotlin-codegen:1.7.0'
    复制代码

    改造Data类

    给我们的数据类增加JsonClass注解

    @JsonClass(generateAdapter = true)
    data
    class ConfigBean(
    var isGood: Boolean = false,
    var title: String = "",
    var type: CustomType = CustomType.DEFAULT
    )
    复制代码

    这样的话,Moshi会在编译期生成我们需要的JsonAdapter,然后通过JsonReader遍历的方式去解析Json数据,这种方式不仅仅不依赖于反射,而且速度快于Kotlin。

    高级用法(JsonAdapter)

    JsonAdapter是Moshi有别于Gson,FastJson的最大特点,顾名思义,这是一个Json的转换器,他的主要作用在于将拿到的Json数据转换成任意你想要的类型,Moshi内置了很多JsonAdapter,有如下这些:

    Built-in Type Adapters

    • Map :MapJsonAdapter
    • Enums :EnumJsonAdapter
    • Arrays :ArrayJsonAdapter
    • Object :ObjectJsonAdapter
    • String :位于StandardJsonAdapters,采用匿名内部类实现
    • Primitives (int, float, char,boolean) :基本数据类型的Adapter都在StandardJsonAdapters里面,采用匿名内部类实现

    Custom Type Adapters

    对于一些比较简单规范的数据,使用Moshi内置的JsonAdapter已经完全能够Cover住,但是由于Json只支持基本数据类型传输,所以很多时候不能满足业务上需要,举个例子:

    {
    "type": 2,
    "isGood": 1
    "title": "TW9zaGkgaXMgZmxleGlibGU="
    }
    复制代码

    这是一个很普通的Json,包含了5个字段,我们如果按照服务端返回的字段来定义解析的Bean,显然是可以完全解析的,但是我们在实际调用的时候,这些数据并不是很干净,我们还需要处理一下:

    • type :Int类型,我需要Enum,我得定义一个Enum的转换类,去将Int转换成Enum
    • isGood :Int类型,我需要Boolean,所以我用的时候还得将Int转成Boolean
    • title :String类型,这个字段是加密过的,可能是通过AES或者RSA加密,这里我们为了方便测试,只是用Base64对 Moshi is flexible 对进行encode。

    对于客户端的同学来说,好像没毛病,以前都是这么干的,如果这种 不干净 的Json少点还好,多了之后就很头疼,每个在用的时候都需要转一遍,很多时候我这么干的时候都觉得浪费时间,而今天有了Moshi之后,我们只需要针对需要转换的类型定义对应的JsonAdapter,达到 一次定义,一劳永逸 的效果,Moshi针对常见的数据类型已经定义了Adapter,但是内置的Adapter现在已经不能满足我们的需求了,所以我们需要自定义JsonAdapter。

    实体定义

    class ConfigBean {
    public CustomType type;
    public Boolean isGood;
    public String title;
    }
    复制代码

    此处我们定义的数据类型不是根据 服务器 返回的Json数据,而是定义的我们业务需要的格式,那么最终是通过JsonAdapter转换器来完成这个转换,下面开始自定义JsonAdapter。

    Int->Enum

    CustomType

    enum CustomType {
    DEFAULT
    (0, "DEFAULT"), BAD(1, "BAD"), NORMAL(2, "NORMAL"), GOOD(3, "NORMAL");
    public int type;
    public String content;
    CustomType(int type, String content) {
    this.type = type;
    this.content = content;
    }
    }
    复制代码

    TypeAdapter

    定义一个TypeAdapter继承自JsonAdapter,传入对应的泛型,会自动帮我们复写fromJson跟toJson两个方法

    public class TypeAdapter  {
    @FromJson
    public CustomType fromJson(int value) throws IOException {
    CustomType type = CustomType.DEFAULT;
    switch (value) {
    case 1:
    type
    = CustomType.BAD;
    break;
    case 2:
    type
    = CustomType.NORMAL;
    break;
    case 3:
    type
    = CustomType.GOOD;
    break;
    }
    return type;
    }
    @ToJson
    public Integer toJson(CustomType value) {
    return value != null ? value.type : 0;
    }
    }

    复制代码

    至此已经完成Type的转换,接下来我们再以title举个例子,别的基本上都是照葫芦画瓢,没什么难度

    StringDecode

    TitleAdapter

    public class TitleAdapter {
    @FromJson
    public String fromJson(String value) {
    byte[] decode = Base64.getDecoder().decode(value);
    return new String(decode);
    }
    @ToJson
    public String toJson(String value) {
    return new String(Base64.getEncoder().encode(value.getBytes()));
    }
    }
    复制代码

    Int->Boolean

    BooleanAdapter

    public class BooleanAdapter {
    @FromJson
    public Boolean fromJson(int value) {
    return value == 1;
    }
    @ToJson
    public Integer toJson(Boolean value) {
    return value ? 1 : 0;
    }
    }

    复制代码

    Adapter测试

    下面我们来测试一下

    String json = "{\n" + "\"type\": 2,\n" + "\"isGood\": 1,\n"
    + "\"title\": \"TW9zaGkgaXMgZmxleGlibGU=\"\n"+ "}";
    Moshi moshi = new Moshi.Builder()
    .add(new TypeAdapter())
    .add(new TitleAdapter())
    .add(new BooleanAdapter())
    .build();
    JsonAdapter<ConfigBean> jsonAdapter = moshi.adapter(ConfigBean.class);
    ConfigBean cofig = jsonAdapter.fromJson(json);
    System.out.println("=========Deserialize ========");
    System.out.println(cofig);
    String cofigJson = jsonAdapter.toJson(cofig);
    System.out.println("=========serialize ========");
    System.out.println(cofigJson);
    复制代码

    打印Log

    =========Deserialize ========
    ConfigBean{type=CustomType{type=2, content='NORMAL'}, isGood=true, title='Moshi is flexible'}
    =========serialize ========
    {"isGood":1,"title":"TW9zaGkgaXMgZmxleGlibGU=","type":2}
    复制代码

    符合我们预期的结果,并且我们在开发的时候,只需要将Moshi设置成单例的,一次性将所有的Adapter全部add进去,就可以一劳永逸,然后愉快地进行开发了。

    源码解析

    Moshi底层采用了 Okio 进行优化,但是上层的JsonReader,JsonWriter等代码是直接从Gson借鉴过来的,所以不再过多分析,主要是就Moshi的两大创新点 JsonAdapter 以及Kotlin的 Codegen 解析重点分析一下。

    Builder

    Moshi moshi = new Moshi.Builder().add(new BooleanAdapter()).build();
    复制代码

    Moshi是通过Builder模式进行构建的,支持添加多个JsonAdapter,下面先看看Builder源码

    public static final class Builder {
    //存储所有Adapter的创建方式,如果没有添加自定义Adapter,则为空
    final List<JsonAdapter.Factory> factories = new ArrayList<>();
    //添加自定义Adapter,并返回自身
    public Builder add(Object adapter) {
    return add(AdapterMethodsFactory.get(adapter));
    }
    //添加JsonAdapter的创建方法到factories里,并返回自身
    public Builder add(JsonAdapter.Factory factory) {
    factories
    .add(factory);
    return this;
    }
    //添加JsonAdapter的创建方法集合到factories里,并返回自身
    public Builder addAll(List<JsonAdapter.Factory> factories) {
    this.factories.addAll(factories);
    return this;
    }
    //通过Type添加Adapter的创建方法,并返回自身
    public <T> Builder add(final Type type, final JsonAdapter<T> jsonAdapter) {
    return add(new JsonAdapter.Factory() {
    @Override
    public @Nullable JsonAdapter<?> create(
    Type targetType, Set<? extends Annotation> annotations, Moshi moshi) { return annotations.isEmpty() && Util.typesMatch(type, targetType) ? jsonAdapter : null;
    }
    });
    }
    //创建一个Moshi的实例
    public Moshi build() {
    return new Moshi(this);
    }
    }
    复制代码

    通过源码发现Builder保存了所有自定义Adapter的创建方式,然后调用Builder的build方式创建了一个Moshi的实例,下面看一下Moshi的源码。

    Moshi

    构造方法

    Moshi(Builder builder) {
    List<JsonAdapter.Factory> factories = new ArrayList<>(
    builder
    .factories.size() + BUILT_IN_FACTORIES.size());
    factories
    .addAll(builder.factories);
    factories
    .addAll(BUILT_IN_FACTORIES);
    this.factories = Collections.unmodifiableList(factories);
    }
    复制代码

    构造方法里面创建了factories,然后加入了Builder中的factories,然后又增加了一个BUILT_IN_FACTORIES,我们应该也能猜到这个就是Moshi内置的JsonAdapter,点进去看一下

    BUILT_IN_FACTORIES

    static final List<JsonAdapter.Factory> BUILT_IN_FACTORIES = new ArrayList<>(5);
    static {
    BUILT_IN_FACTORIES
    .add(StandardJsonAdapters.FACTORY);
    BUILT_IN_FACTORIES
    .add(CollectionJsonAdapter.FACTORY);
    BUILT_IN_FACTORIES
    .add(MapJsonAdapter.FACTORY);
    BUILT_IN_FACTORIES
    .add(ArrayJsonAdapter.FACTORY);
    BUILT_IN_FACTORIES
    .add(ClassJsonAdapter.FACTORY);
    }
    复制代码

    BUILT_IN_FACTORIES这里面提前用一个静态代码块加入了所有内置的JsonAdapter

    JsonAdapter

    JsonAdapter<ConfigBean> jsonAdapter = moshi.adapter(ConfigBean.class);
    复制代码

    不管是我们自定义的JsonAdapter还是Moshi内置的JsonAdapter,最终都是为我们的解析服务的,所以最终所有的JsonAdapter最终汇聚成JsonAdapter,我们看看是怎么生成的,跟一下Moshi的adapter方法,发现最终调用的是下面的方法

    public <T> JsonAdapter<T> adapter(Type type, Set<? extends Annotation> annotations,
    @Nullable String fieldName) {
    type
    = canonicalize(type);
    // 如果有对应的缓存,那么直接返回缓存
    Object cacheKey = cacheKey(type, annotations);
    synchronized (adapterCache) {
    JsonAdapter<?> result = adapterCache.get(cacheKey);
    if (result != null) return (JsonAdapter<T>) result;
    }

    boolean success = false;
    JsonAdapter<T> adapterFromCall = lookupChain.push(type, fieldName, cacheKey);
    try {
    if (adapterFromCall != null)
    return adapterFromCall;
    // 遍历Factories,直到命中泛型T的Adapter
    for (int i = 0, size = factories.size(); i < size; i++) {
    JsonAdapter<T> result = (JsonAdapter<T>) factories.get(i).create(type, annotations, this);
    if (result == null) continue;
    lookupChain
    .adapterFound(result);
    success
    = true;
    return result;
    }
    }
    }
    复制代码

    最开始看到这里,我比较奇怪,不太确定我的Config命中了哪一个JsonAdapter,最终通过断点追踪,发现了是命中了 ClassJsonAdapter ,既然命中了他,那么我们就看一下他的具体实现

    ClassJsonAdapter

    构造方法

    final class ClassJsonAdapter<T> extends JsonAdapter<T> {
    public static final JsonAdapter.Factory FACTORY = new JsonAdapter.Factory() {
    @Override public @Nullable JsonAdapter<?> create(
    Type type, Set<? extends Annotation> annotations, Moshi moshi) {
    //省略了很多异常判断代码
    Class<?> rawType = Types.getRawType(type);
    //获取Class的所有类型
    ClassFactory<Object> classFactory = ClassFactory.get(rawType);
    Map<String, FieldBinding<?>> fields = new TreeMap<>();
    for (Type t = type; t != Object.class; t = Types.getGenericSuperclass(t)) {
    //创建Moshi跟Filed的绑定关系,便于解析后赋值
    createFieldBindings
    (moshi, t, fields);
    }
    return new ClassJsonAdapter<>(classFactory, fields).nullSafe();
    }
    }
    复制代码

    当我们拿到一个JsonAdapter的时候,基本上所有的构建都已经完成,此时可以进行Deserialize 或者Serialize 操作,先看下Deserialize 也就是fromjson方法

    JsonReader&JsonWriter

    对于Java的解析,Moshi并没有在传输效率上进行显著的提升,只是底层的IO操作采用的是Okio,Moshi的创新在于灵活性上面,也就是JsonAdapter,而且Moshi的官方文档上面也提到了

    Moshi uses the same streaming and binding mechanisms as Gson . If you’re a Gson user you’ll find Moshi works similarly. If you try Moshi and don’t love it, you can even migrate to Gson without much violence!

    所以这里的JsonReader跟JsonWriter说白了都是从Gson那里直接借鉴过来的,就是这么坦诚。

    fromjson

    ConfigBean cofig = jsonAdapter.fromJson(json);
    复制代码

    这个方法先是调用了父类JsonAdapter的fromJson方法

    public abstract  T fromJson(JsonReader reader) throws IOException;
    public final T fromJson(BufferedSource source) throws IOException {
    return fromJson(JsonReader.of(source));
    }
    public final T fromJson(String string) throws IOException {
    JsonReader reader = JsonReader.of(new Buffer().writeUtf8(string));
    T result
    = fromJson(reader);
    return result;

    复制代码

    我们发现fromJson是个重载方法,既可以传String也可以传BufferedSource,不过最终调用的都是fromJson(JsonReader reader)这个方法,BufferedSource是Okio的一个类,因为Moshi底层的IO采用的是Okio,但是我们发现参数为JsonReader的这个方法是抽象方法,所以具体的实现是是在ClassJsonAdapter里面,。

    @Override public T fromJson(JsonReader reader) throws IOException {
    T result
    = classFactory.newInstance();
    try {
    reader
    .beginObject();
    while (reader.hasNext()) {
    int index = reader.selectName(options);
    //如果不是Key,直接跳过
    if (index == -1) {
    reader
    .skipName();
    reader
    .skipValue();
    continue;
    }
    //解析赋值
    fieldsArray
    [index].read(reader, result);
    }
    reader
    .endObject();
    return result;
    } catch (IllegalAccessException e) {
    throw new AssertionError();
    }
    }

    //递归调用,直到最后
    void read(JsonReader reader, Object value) throws IOException, IllegalAccessException {
    T fieldValue
    = adapter.fromJson(reader);
    field
    .set(value, fieldValue);
    }
    复制代码

    toJson

    String cofigJson = jsonAdapter.toJson(cofig);
    复制代码

    跟fromJson一样,先是调用的JsonAdapter的toJson方法

    public abstract void toJson(JsonWriter writer,  T value) throws IOException;
    public final void toJson(BufferedSink sink, T value) throws IOException {
    JsonWriter writer = JsonWriter.of(sink);
    toJson
    (writer, value);
    }
    public final String toJson( T value) {
    Buffer buffer = new Buffer();
    try {
    toJson
    (buffer, value);
    } catch (IOException e) {
    throw new AssertionError(e); // No I/O writing to a Buffer.
    }
    return buffer.readUtf8();
    }
    复制代码

    不管传入的是泛型T还是BufferedSink,最终调用的toJson(JsonWriter writer),然后返回了buffer.readUtf8()。我们继续看一下子类的具体实现

    @Override public void toJson(JsonWriter writer, T value) throws IOException {
    try {
    writer
    .beginObject();
    for (FieldBinding<?> fieldBinding : fieldsArray) {
    writer
    .name(fieldBinding.name);
    //将fieldsArray的值依次写入writer里面
    fieldBinding
    .write(writer, value);
    }
    writer
    .endObject();
    } catch (IllegalAccessException e) {
    throw new AssertionError();
    }
    }
    复制代码

    Codegen

    Moshis Kotlin codegen support is an annotation processor. It generates a small and fast adapter for each of your Kotlin classes at compile time. Enable it by annotating each class that you want to encode as JSON:

    所谓Codegen,也就是我们上文提到的Annotation,在编译期间生成对应的JsonAdapter,我们看一下先加一下注解,看看Kotlin帮我们自动生成的注解跟我们自定义的注解有什么区别,rebuild一下项目:

    CustomType

    @JsonClass(generateAdapter = true)
    data
    class CustomType(var type: Int, var content: String)
    复制代码

    我们来看一下对应生成的JsonAdapter

    CustomTypeJsonAdapter

    这个类方法很多,我们重点看一下formJson跟toJson

    override fun fromJson(reader: JsonReader): CustomType {
    var type: Int? = null
    var content: String? = null
    reader
    .beginObject()
    while (reader.hasNext()) {
    when (reader.selectName(options)) {
    //按照变量的定义顺序依次赋值
    0 -> type = intAdapter.fromJson(reader)
    1 -> content = stringAdapter.fromJson(reader)
    -1 -> {
    reader
    .skipName()
    reader
    .skipValue()
    }
    }
    }
    reader
    .endObject()
    //不通过反射,直接创建对象,传入解析的Value
    var result = CustomType(type = type ,content = content )
    return result
    }

    override fun toJson(writer: JsonWriter, value: CustomType?) {
    writer
    .beginObject()
    writer
    .name("type")//写入type
    intAdapter
    .toJson(writer, value.type)
    writer
    .name("content")//写入content
    stringAdapter
    .toJson(writer, value.content)
    writer
    .endObject()
    }
    复制代码

    ConfigBean

    @JsonClass(generateAdapter = true)
    data
    class ConfigBean(var isGood: Boolean ,var title: String ,var type: CustomType)
    复制代码

    ConfigBeanJsonAdapter

    override fun fromJson(reader: JsonReader): ConfigBean {
    var isGood: Boolean? = null
    var title: String? = null
    var type: CustomType? = null
    reader
    .beginObject()
    while (reader.hasNext()) {
    when (reader.selectName(options)) {
    0 -> isGood = booleanAdapter.fromJson(reader)
    1 -> title = stringAdapter.fromJson(reader)
    2 -> type = customTypeAdapter.fromJson(reader)
    -1 -> {
    reader
    .skipName()
    reader
    .skipValue()
    }
    }
    }
    reader
    .endObject()
    var result = ConfigBean(isGood = isGood ,title = title ,type = type
    return result
    }

    override fun toJson(writer: JsonWriter, value: ConfigBean?) {
    writer
    .beginObject()
    writer
    .name("isGood")
    booleanAdapter
    .toJson(writer, value.isGood)
    writer
    .name("title")
    stringAdapter
    .toJson(writer, value.title)
    writer
    .name("type")
    customTypeAdapter
    .toJson(writer, value.type)
    writer
    .endObject()
    }
    复制代码

    通过查看生成的CustomTypeJsonAdapter以及ConfigBeanJsonAdapter,我们发现通过Codegen生成也就是注解的方式,跟反射对比一下,会发现有如下优点:

    • 效率高:直接创建对象,无需反射
    • APK体积小:无需引入Kotlin-reflect的Jar包

    注意事项

    在进行kotlin解析的时候不管是采用Reflect还是Codegen,都必须保证类型一致,也就是父类跟子类必须是Java或者kotlin,因为两种解析方式,最终都是通过ClassType来进行解析的,同时在使用Codegen解析的时候必须保证Koltin的类型是 internal 或者 public 的。

    总结

    Moshi整个用法跟源码看下来,其实并不是很复杂,但是针对Java跟Kotlin的解析增加了JsonAdapter的转换,以及针对Kotlin的Data类的解析提供了Codegen这种方式,真的让人耳目一新,以前遇到这种业务调用的时候需要二次转换的时候,都是去写 工具 类或者用的时候直接转换。不过Moshi也有些缺点,对于Kotlin的Null类型的支持并不友好,这样会在Kotlin解析的时候如果对于一个不可空的字段变成了Null就会直接抛异常,感觉不太友好,应该给个默认值比较好一些,还有就是对默认值的支持,如果Json出现了Null类型,那么解析到对应的字段依然会被赋值成Null,跟之前的Gson一样,希望以后可以进行完善,毕竟瑕不掩瑜。



    本文来源:码农网
    本文链接:https://www.codercto.com/a/36525.html

    收起阅读 »

    聊聊 Activity 的启动模式

    Activity 的启动模式本身是一个挺难理解的知识点,大多数开发者对这个概念的了解可能只限于四种 launchMode 属性值,但启动模式其实还需要受 Intent flag 的影响。而且 Activity 启动模式并不只是单纯地用来启动一个 Activit...
    继续阅读 »

    Activity 的启动模式本身是一个挺难理解的知识点,大多数开发者对这个概念的了解可能只限于四种 launchMode 属性值,但启动模式其实还需要受 Intent flag 的影响。而且 Activity 启动模式并不只是单纯地用来启动一个 Activity,实际上还会直接影响到用户的直观感受和使用体验,因为启动模式直接就决定了应用的任务栈和返回栈,这都是用户能直接接触到的


    本篇文章就来简单介绍下 Activity 的启动模式,希望对你有所帮助 😇😇


    1、任务栈


    任务栈是指用户在执行某项工作时与之互动的一系列 Activity 的集合,这些 Activity 按照打开的顺序排列在一个后进先出的栈中。例如,电子邮件应用包含一个 Activity 来显示邮件列表,当用户选择一封邮件时就会打开一个新的 Activity 来显示邮件详情,这个新的 Activity 就会添加到任务栈中,被推送到栈顶部并获得焦点,从而获得了与用户进行交互的机会。当用户按返回键时,邮件详情 Activity 就会从任务栈中退出并被销毁,邮件列表 Activity 就会成为新的栈顶并重新获得焦点


    任务栈代表的是一个整体,本身包含了多个 Activity,当任务栈中的所有 Activity 都被弹出后,任务栈也就随之就被回收了。就像下图所示,三个 Activity 通过相继启动组成了一个任务栈,Activity 1 是整个任务栈的根 Activity,当用户不断按返回键,Activity 就会依次被弹出



    2、返回栈


    返回栈是从用户使用的角度来进行定义的,返回栈中包含一个或多个任务栈,但同时只会有一个任务栈够处于前台,只有处于前台任务栈的 Activity 才能与用户交互


    例如,用户先启动了应用 A,先后打开了 Activity 1 和 Activity 2,此时 Task A 是前台任务栈。之后用户又点击 Home 键回到了桌面,启动了应用 B,又先后打开了 的 Activity 3 和 Activity 4,此时 Task B 就成为了前台任务栈,Task A 成了后台任务栈。用户点击返回键的过程中依次展现的页面就会是 Activity 4 -> Activity 3 -> 桌面


    而如果用户在打开应用 B 时并没有回到桌面,而是直接通过应用 A 启动了应用 B 的话,用户点击返回键的过程中依次展现的页面就会是 Activity 4 -> Activity 3 -> Activity 2 -> Activity 1 -> 桌面


    返回栈所表示的就是当用户不断回退页面时所能看到的一系列 Activity 的集合,而这些页面可能是处于多个不同的任务栈中。在第一种情况中,返回栈只包含 Task B 一个任务栈,所以当 Task B 被清空后就会直接回到桌面。在第二种情况中,返回栈中包含 Task A 和 Task B 两个任务栈,所以当 Task B 被清空后也会先切回到 Task A,等到 Task A 也被清空后才会回到桌面


    需要注意的是,返回栈中包含的多个任务栈之间并没有强制的先后顺序,多个任务栈之间的叠加关系可以随时发现变化。例如,当应用 A 启动了应用 B 后,Task B 是处于 Task A 之上,但之后如果应用 B 又反向启动了应用 A 的话,Task A 就会重新成为前台 Task 并覆盖在 Task B 之上


    3、taskAffinity


    返回栈这个概念对应的就是 taskAffinity,是 Activity 在 AndroidManifest 文件中声明的一个属性值。taskAffinity 翻译为“亲和性”,用于表示特定 Activity 倾向于将自身存放在哪个任务栈中


    在默认情况下,同一应用中的所有 Activity 会具有相同的亲和性,所有 Activity 默认会以当前应用的 applicationId 作为自己的 taskAffinity 属性值。我们可以手动为应用内的部分 Activity 指定特定的 taskAffinity,从而将这部分 Activity 进行分组


            <activity
    android:name=".StandardActivity"
    android:launchMode="standard"
    android:taskAffinity="task.test1" />
    <activity
    android:name=".SingleTopActivity"
    android:launchMode="singleTop"
    android:taskAffinity="task.test2" />
    <activity
    android:name=".SingleTaskActivity"
    android:launchMode="singleTask"
    android:taskAffinity="task.test3" />
    <activity
    android:name=".SingleInstanceActivity"
    android:launchMode="singleInstance"
    android:taskAffinity="task.test4" />
    复制代码

    从概念上讲,具有相同 taskAffinity 的 Activity 归属于同一任务栈(实际上并不一定)。从用户的角度来看则是归属于同一“应用”,因为每种 taskAffinity 在最近任务列表中会各自独占一个列表项,看起来就像一个个单独的应用,而实际上这些列表项可能是来自于同个应用


    4、启动模式


    Activity 的启动模式是一个很复杂的难点,其决定了要启动的 Activity 和任务栈还有返回栈之间的关联关系,直接影响到了用户的直观感受


    启动模式就由 launchMode 和 Intent flag 这两者来共同决定,我们可以通过两种方式来进行定义:



    • 在 AndroidManifest 文件中为 Activity 定义 launchMode 属性值,一共包含四种类型的属性值

    • 当通过 startActivity(Intent) 启动 Activity 时,向 Intent 添加或设置 flag 标记位,通过该 flag 来定义启动模式


    如果只看四个 launchMode 的话其实并不难理解,可是再考虑多应用交互还有 Intent flag 的话,情况就会变得复杂很多,其复杂性和难点主要就在于:单个任务栈包含的 Activity 可以是来自于不同的应用、单个应用也可以包含多个任务栈、返回栈包含的多个任务栈之间也可以进行顺序切换、甚至任务栈中的 Activity 也可以被迁移到另外一个任务栈、Intent flag 可以多个组合使用


    有些启动模式可通过 launchMode 来定义,但不能通过 Intent flag 定义,同样,有些启动模式可通过 Intent flag 定义,却不能在 launchMode 中定义。两者互相补充,但不能完全互相替代,且 Intent flag 的优先级会更高一些


    5、launchMode


    launchMode 一共包含以下四种属性值:



    • standard。默认模式。系统会在启动该 Activity 的任务栈中创建一个目标 Activity 的新实例,使该目标 Activity 成为任务栈的栈顶。该模式下允许先后启动多个相同的目标 Activity,一个任务栈可以拥有多个目标 Activity 实例,且不同 Activity 实例可以属于不同的任务栈

    • singleTop。如果当前任务栈的顶部已存在目标 Activity 的实例,则系统会通过调用其 onNewIntent() 方法来将 Intent 转送给该实例并进行复用,否则会创建一个目标 Activity 的新实例。目标 Activity 可以多次实例化,不同实例可以属于不同的任务栈,一个任务栈可以拥有多个实例(此时多个实例不会连续叠放在一起)

    • singleTask。如果系统当前不包含目标 Activity 的目标任务栈,那么系统就会先创建出目标任务栈,然后实例化目标 Activity 使之成为任务栈的根 Activity。如果系统当前包含目标任务栈,且该任务栈中已存在该目标 Activity 的实例,则系统会通过调用其 onNewIntent() 方法将 Intent 转送给该现有实例,而不会创建新实例,并同时弹出该目标 Activity 之上的所有其它实例,使目标 Activity 成为栈顶。如果系统当前包含目标任务栈,但该任务栈不包含目标 Activity 实例,则会实例化目标 Activity 并将其入栈。因此,系统全局一次只能有一个目标 Activity 实例存在

    • singleInstance。与 singleTask 相似,唯一不同的是通过 singleInstance 启动的 Activity 会独占一个任务栈,系统不会将其和其它 Activity 放置到同个任务栈中,由该 Activity 启动的任何 Activity 都会在其它的任务栈中打开


    四种 launchMode 还是很好理解的,当中比较特殊的应该属 singleTask,使用 singleTask 标记的 Activity 会有将自己存放在特定任务栈的倾向。如果目标任务栈和目标 Activity 都已经存在,则会进行复用,否则才会创建目标任务栈和目标 Activity。singleInstance 则是在 singleTask 的基础上多了一个“独占任务栈”的特性


    采用 singleTask 启动的 Activity 添加到返回栈的过程就如下图所示。一开始返回栈中只包含 Activity 1 和 Activity 2 组成的任务栈,当 Activity 2 启动了处于后台的 Activity Y 时,Activity Y 和 Activity X 组成的任务栈就会被转到前台,覆盖住当前任务栈。最终返回栈中就变成了四个 Activity



    再来写个 Demo 来验证下这四种 launchMode 的效果


    声明四种不同 launchMode 的 Activity,每个 Activity 均声明了不同的 taskAffinity


            <activity
    android:name=".StandardActivity"
    android:launchMode="standard"
    android:taskAffinity="task.a" />
    <activity
    android:name=".SingleTopActivity"
    android:launchMode="singleTop"
    android:taskAffinity="task.b" />
    <activity
    android:name=".SingleTaskActivity"
    android:launchMode="singleTask"
    android:taskAffinity="task.c" />
    <activity
    android:name=".SingleInstanceActivity"
    android:launchMode="singleInstance"
    android:taskAffinity="task.d" />
    复制代码

    通过打印 Activity 的 hashCode() 方法返回值来判断 Activity 的实例是否被复用了,再通过 getTaskId() 方法来判断 Activity 处于哪个任务栈中


    /**
    * @Author: leavesC
    * @Date: 2021/4/16 16:38
    * @Desc:
    * @Github:https://github.com/leavesC
    */
    abstract class BaseLaunchModeActivity : BaseActivity() {

    override val bind by getBind<ActivityBaseLaunchModeBinding>()

    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    bind.tvTips.text =
    getTip() + "\n" + "hashCode: " + hashCode() + "\n" + "taskId: " + taskId
    bind.btnStartStandardActivity.setOnClickListener {
    startActivity(StandardActivity::class.java)
    }
    bind.btnStartSingleTopActivity.setOnClickListener {
    startActivity(SingleTopActivity::class.java)
    }
    bind.btnStartSingleTaskActivity.setOnClickListener {
    startActivity(SingleTaskActivity::class.java)
    }
    bind.btnStartSingleInstanceActivity.setOnClickListener {
    startActivity(SingleInstanceActivity::class.java)
    }
    log("onCreate")
    }

    override fun onNewIntent(intent: Intent?) {
    super.onNewIntent(intent)
    log("onNewIntent")
    }

    override fun onDestroy() {
    super.onDestroy()
    log("onDestroy")
    }

    abstract fun getTip(): String

    private fun log(log: String) {
    Log.e(getTip(), log + " " + "hashCode: " + hashCode() + " " + "taskId: " + taskId)
    }

    }

    class StandardActivity : BaseLaunchModeActivity() {

    override fun getTip(): String {
    return "StandardActivity"
    }

    }
    复制代码

    四个 Activity 相继互相启动,查看输出的日志,可以看出 SingleTaskActivity 和 SingleInstanceActivity 均处于独立的任务栈中,而 StandardActivity 和 SingleTopActivity 处于同个任务栈中。说明 taskAffinity 对于 standard 和 singleTop 这两种模式不起作用


    E/StandardActivity: onCreate hashCode: 31933912 taskId: 37
    E/SingleTopActivity: onCreate hashCode: 95410735 taskId: 37
    E/SingleTaskActivity: onCreate hashCode: 255733510 taskId: 38
    E/SingleInstanceActivity: onCreate hashCode: 20352185 taskId: 39
    复制代码

    再依次启动 SingleTaskActivity 和 SingleTopActivity。可以看到 SingleTaskActivity 被复用了,且在 38 这个任务栈上启动了一个新的 SingleTopActivity 实例。之所以没有复用 SingleTopActivity,是因为之前的 SingleTopActivity 是在 37 任务栈中,并非当前任务栈


    E/SingleTaskActivity: onNewIntent hashCode: 255733510 taskId: 38
    E/SingleTopActivity: onCreate hashCode: 20652250 taskId: 38
    复制代码

    再启动一次 SingleTopActivity,两次 StandardActivity。可以看到 SingleTopActivity 的确在当前任务栈中被复用了,并均创建了两个新的 StandardActivity 实例。说明 singleTop 想要被复用需要当前任务栈的栈顶就是目标 Activity,而 standard 模式每次均会创建新实例


    E/SingleTopActivity: onNewIntent hashCode: 20652250 taskId: 38
    E/StandardActivity: onCreate hashCode: 252563788 taskId: 38
    E/StandardActivity: onCreate hashCode: 25716630 taskId: 38
    复制代码

    再依次启动 SingleTaskActivity 和 SingleInstanceActivity。可以看到 SingleTaskActivity 和 SingleInstanceActivity 均被复用了,且 SingleTaskActivity 之上的三个 Activity 均从任务栈中被弹出销毁了,SingleTaskActivity 成为了 task 38 新的栈顶 Activity


    E/StandardActivity: onDestroy hashCode: 252563788 taskId: 38
    E/SingleTopActivity: onDestroy hashCode: 20652250 taskId: 38
    E/SingleTaskActivity: onNewIntent hashCode: 255733510 taskId: 38
    E/StandardActivity: onDestroy hashCode: 25716630 taskId: 38
    E/SingleInstanceActivity: onNewIntent hashCode: 20352185 taskId: 39
    复制代码

    再依次启动 StandardActivity 和 SingleTopActivity。可以看到创建了一个新的任务栈,且启动的是两个新的 Activity 实例。由于 SingleInstanceActivity 所在的任务栈只会由其自身所独占,所以 StandardActivity 启动时就需要创建一个新的任务栈用来容纳自身


    E/StandardActivity: onCreate hashCode: 89641200 taskId: 40
    E/SingleTopActivity: onCreate hashCode: 254021317 taskId: 40
    复制代码

    可以做个总结:



    • standard 和 singleTop 这两种模式下 taskAffinity 属性均不会生效,这两种模式启动的 Activity 总会尝试加入到启动者所在的任务栈中,如果启动者是 singleInstance 的话则会创建一个新的任务栈

    • standard 模式的 Activity 每次启动都会创建一个新的实例,不会考虑任何复用

    • singleTop 模式的 Activity 想要被复用,需要启动者所在的任务栈的栈顶就是该 Activity 实例

    • singleTask 模式的 Activity 事实上是系统全局单例,只要实例没有被回收就会一直被复用。singleTask 可以通过声明 taskAffinity 从而在一个特定的任务栈中被启动,且允许其它 Activity 一起共享同一个任务栈。如果不声明 taskAffinity 的话就会尝试寻找或者主动创建 taskAffinity 为 applicationId 的任务栈,然后在该任务栈中创建或复用 Activity

    • singleInstance 可以看做是 singleTask 的加强版,singleInstance 在任何时候都会独占一个任务栈,不管是否声明了 taskAffinity。在 singleInstance 任务栈中启动的其它 Activity 都会加入到其它任务栈中


    需要注意的是,以上结论只适用于没有主动添加 Intent flag 的情况,如果同时添加了 Intent flag 的话就会出现很多奇奇怪怪的现象了


    6、Intent flag


    在启动 Activity 时,我们可以通过在传送给 startActivity(Intent) 方法的 Intent 中设置多个相应的 flag 来修改 Activity 与其任务栈的默认关联,即 Intent flag 的优先级会比 launchMode 高


    Intent 提供的设置 flag 的方法有以下两个,一个是覆盖设置,一个是增量添加


        private int mFlags;

    public @NonNull Intent setFlags(@Flags int flags) {
    mFlags = flags;
    return this;
    }

    public @NonNull Intent addFlags(@Flags int flags) {
    mFlags |= flags;
    return this;
    }
    复制代码

    通过如下方式来添加 flag 并启动 Activity


            val intent = Intent(this, StandardActivity::class.java)
    intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP)
    startActivity(intent)
    复制代码

    如果 Activity 的启动模式只由 launchMode 定义的话,那么在运行时 Activity 的启动模式就再也无法改变了,相当于被写死了,所以 launchMode 适合于那些具有固定情景的业务。而 Intent flag 存在的意义就是为了改变或者补充 launchMode,适合于那些大部分情况下固定,少数情况下需要动态进行变化的场景,例如在某些情况下不希望 singleInstance 模式的 Activity 被重用,此时就可以通过 Intent flag 来动态实现


    而这也造成了 Intent flag 很难理清楚逻辑,因为 Intent flag 往往需要组合使用,且还需要考虑和 launchMode 的各种组合配置,两者并不是简单的进行替换


    Intent flag 有很多个,比较常见的有四个,这里就简单介绍下这几种 Intent flag



    • FLAG_ACTIVITY_NEW_TASK

    • FLAG_ACTIVITY_SINGLE_TOP

    • FLAG_ACTIVITY_CLEAR_TOP

    • FLAG_ACTIVITY_CLEAR_TASK


    FLAG_ACTIVITY_NEW_TASK


    FLAG_ACTIVITY_NEW_TASK 应该是大多数开发者最熟悉的一个 flag,比较常用的一个场景就是用于在非 ActivityContext 环境下启动 Activity。Android 系统默认情况下是会将待启动的 Activity 加入到启动者所在的任务栈,而如果启动 Activity 的是 ServiceContext 的话,此时系统就不确定该如何存放目标 Activity 了,此时就会抛出一个 RuntimeException


    java.lang.RuntimeException: Unable to start service github.leavesc.launchmode.MyService@e3183b7 with Intent { cmp=github.leavesc.demo/github.leavesc.launchmode.MyService }: android.util.AndroidRuntimeException: Calling startActivity() from outside of an Activity  context requires the FLAG_ACTIVITY_NEW_TASK flag. Is this really what you want?
    复制代码

    从异常信息可以看出此时 Intent 需要添加一个 FLAG_ACTIVITY_NEW_TASK 才行,添加后 Activity 就可以正常启动了


            val intent = Intent(this, StandardActivity::class.java)
    intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
    startActivity(intent)
    复制代码

    FLAG_ACTIVITY_NEW_TASK 也有一个隐含的知识点,上文有讲到 standard 和 singleTop 这两种模式下 taskAffinity 属性均不会生效,但这个结论也只适用于没有主动添加 Intent flag 的情况


    FLAG_ACTIVITY_NEW_TASK 和 standard 模式的组合情况可以总结为:



    • standard 没有设置 taskAffinity。此时系统就会去复用或者创建一个默认任务栈,然后直接在该任务栈上新建一个 Activity 实例入栈

    • standard 有设置 taskAffinity。此时又可以分为当前系统是否存在 taskAffinity 关联的任务栈两种情况

      • 不存在目标任务栈。此时系统就会创建目标任务栈,然后直接在该任务栈上新建一个 Activity 实例入栈

      • 存在目标任务栈。此时系统会判断任务栈中是否已经存在目标 Activity 的实例,如果不存在的话则新建一个 Activity 实例入栈。如果存在目标实例的话,则只是将该任务栈转到前台而已,既不会新建 Activity 实例,也不会回调 onNewIntent方法,甚至也不管该 Activity 实例是否处于栈顶,总之只要存在相同实例就不做任何响应。。。。。。。




    可以看到,FLAG_ACTIVITY_NEW_TASK 的语义还是有点费解的,该标记位可以使得 taskAffinity 生效,创建或者复用任务栈并将其转到前台,但并不要求必须创建一个新的 Activity 实例,而是只要 Activity 实例有存在即可,而且也无需该 Activity 实例就在栈顶


    FLAG_ACTIVITY_SINGLE_TOP


    FLAG_ACTIVITY_SINGLE_TOP 这个 flag 看名字就很容易和 singleTop 联系在一起,实际上该 flag 也的确起到了和 singleTop 相同的作用


    只要待启动的 Activity 添加了该标记位,且当前任务栈的栈顶就是目标 Activity,那么该 Activity 实例就会被复用,并且回调其onNewIntent方法,即使该 Activity 声明了 standard 模式,这相当于将 Activity 的 launchMode 覆盖为了 singleTop


    FLAG_ACTIVITY_CLEAR_TOP


    FLAG_ACTIVITY_CLEAR_TOP 这个 flag 则是起到了清除目标 Activity 之上所有 Activity 的作用。例如,假设当前要启动的 Activity 已经在目标任务栈中了,那么设置该 flag 后系统就会清除目标 Activity 之上的所有其它 Activity,但系统最终并不一定会复用现有的目标 Activity 实例,有可能是销毁后再次创建一个新的实例


    看个例子。先以不携带 flag 的方式启动 StandardActivity 和 SingleTopActivity,此时日志信息如下


    E/StandardActivity: onCreate hashCode: 76763823 taskId: 39
    E/SingleTopActivity: onCreate hashCode: 217068130 taskId: 39
    复制代码

    再启动一次 StandardActivity,此时就带上 FLAG_ACTIVITY_CLEAR_TOP。此时就会看到最开始启动的两个 Activity 都会销毁了,并且再次新建了一个 StandardActivity 实例入栈


    E/StandardActivity: onDestroy hashCode: 76763823 taskId: 39
    E/StandardActivity: onCreate hashCode: 51163106 taskId: 39
    E/SingleTopActivity: onDestroy hashCode: 217068130 taskId: 39
    复制代码

    而如果同时加上 FLAG_ACTIVITY_SINGLE_TOP 和 FLAG_ACTIVITY_CLEAR_TOP 两个 flag 的话,那么 SingleTopActivity 就会被弹出,StandardActivity 会被复用,并且回调其onNewIntent方法,两个 flag 相当于组合出了 singleTask 的效果。这一个效果读者可以自行验证


    FLAG_ACTIVITY_CLEAR_TASK


    FLAG_ACTIVITY_CLEAR_TASK 的源码注释标明了该 flag 必须和 FLAG_ACTIVITY_NEW_TASK 组合使用,它起到的作用就是将目标任务栈中的所有 Activity 情空,然后新建一个目标 Activity 实例入栈,该 flag 的优先级很高,即使是 singleInstance 类型的 Activity 也会被销毁


    看个例子。先启动一个 SingleInstanceActivity,然后以添加了 NEW_TASK 和 CLEAR_TASK 两个 flag 的方式再次启动 SingleInstanceActivity,可以看到旧的 Activity 实例被销毁了,并重建了一个新实例入栈,但比较奇怪的一点就是:旧的 Activity 实例的 onNewIntent 方法同时也被调用了


    E/SingleInstanceActivity: onCreate hashCode: 144724929 taskId: 47
    E/SingleInstanceActivity: onNewIntent hashCode: 144724929 taskId: 47
    E/SingleInstanceActivity: onCreate hashCode: 106721743 taskId: 47
    E/SingleInstanceActivity: onDestroy hashCode: 144724929 taskId: 47
    复制代码

    7、总结


    关于 Activity 的启动模式的讲解到这里就结束了,最后再强调一遍,launchMode 和 Intent flag 的各种组合效果还是有点过于难理解了,使得我很难全面地进行描述,再加上似乎还存在版本兼容性问题,使用起来就更加麻烦了,所以我觉得开发者只需要有个大致的印象即可,当真正要使用的时候再来亲自测试验证效果就好,不必强行记忆


    以上各个示例 Demo 点这里:AndroidOpenSourceDemo


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

    Okio-很棒的文章

    前言 本文不去阐述 Okio 产生原因,也不去对比 Okio 与 Java 原生 IO 的优劣性,单纯分析 Okio 的实现,对每个关键点分析透彻,并配上精美图解。 本文分析点目录 Okio 类框架图 Source 与 Sink Buffer Segment...
    继续阅读 »

    前言


    本文不去阐述 Okio 产生原因,也不去对比 Okio 与 Java 原生 IO 的优劣性,单纯分析 Okio 的实现,对每个关键点分析透彻,并配上精美图解。


    本文分析点目录



    • Okio 类框架图

    • Source 与 Sink

    • Buffer

    • Segment

    • 具体分析 Buffer 的 write 方法,看数据流转过程


    Okio 类框架图


    Okio 整个框架的代码量并不大,体现了高内聚的设计,类框架大概如下:



    图中体现了框架内部类之间的关系,整个框架做的事情大致就是:


    Source 与 Sink


    Source 与 Sink 是 Okio 中所有输入输出流的基类接口,类似 Java IO 中 InputStream,OutputStream。


    Source,源的意思,也就是说我是数据源,你们要的数据都从我这来。


    Sink,往下沉,往外运输的意思,从 Source 中读到数据后,通过我传出去。


    分别定义了数据流的读写接口:


    public interface Source extends Closeable {
    /**
    * Removes at least 1, and up to {@code byteCount} bytes from this and appends
    * them to {@code sink}. Returns the number of bytes read, or -1 if this
    * source is exhausted.
    */
    long read(Buffer sink, long byteCount) throws IOException;

    /** Returns the timeout for this source. */
    Timeout timeout();

    /**
    * Closes this source and releases the resources held by this source. It is an
    * error to read a closed source. It is safe to close a source more than once.
    */
    @Override void close() throws IOException;
    }
    复制代码

    public interface Sink extends Closeable, Flushable {
    /** Removes {@code byteCount} bytes from {@code source} and appends them to this. */
    void write(Buffer source, long byteCount) throws IOException;

    /** Pushes all buffered bytes to their final destination. */
    @Override void flush() throws IOException;

    /** Returns the timeout for this sink. */
    Timeout timeout();

    /**
    * Pushes all buffered bytes to their final destination and releases the
    * resources held by this sink. It is an error to write a closed sink. It is
    * safe to close a sink more than once.
    */
    @Override void close() throws IOException;
    }
    复制代码

    在 Okio 中 Source,Sink 也有几个实现类,我们只看 RealBufferedSource,RealBufferedSink。


    RealBufferedSource


    从名字中看出,带有缓冲的数据源(既然是源,那么你们想要的数据都从我这拿),但是它不是真正的数据源,真正的数据源是成员变量 source,它只是包装装饰了一下,
    当 read 开头的方法被调用时,都是从成员变量 source 中读取,读到数据后,先存入成员变量 buffer 中,然后再从 buffer 中读数据。


    来看 read 方法:


    --> RealBufferedSource.java


    final class RealBufferedSource implements BufferedSource {
    // 缓冲,数据先放到 buffer 中,别人来读的时候先从 buffer 中读,没有的话再从 source 中读
    public final Buffer buffer = new Buffer();
    // 真正数据源,也是此 RealBufferedSource 的上游数据源,当 buffer 中没数据时,从 source 中读
    public final Source source;
    boolean closed;
    ...

    @Override public long read(Buffer sink, long byteCount) throws IOException {
    // 该方法作用:从当前 Source 中 读取 byteCount 个字节存到 sink 中
    if (sink == null) throw new IllegalArgumentException("sink == null");
    if (byteCount < 0) throw new IllegalArgumentException("byteCount < 0: " + byteCount);
    if (closed) throw new IllegalStateException("closed");

    // 若当前 buffer 中没数据,就从成员变量 source 读,读到了再存入 buffer 中
    // 若当前 source 中也没数据,则返回-1
    // 若当前 buffer 中有数据,则跳过这里,直接从 buffer 中读
    if (buffer.size == 0) {
    long read = source.read(buffer, Segment.SIZE);
    if (read == -1) return -1;
    }

    // 到这里 buffer 中肯定是有数据的,取小者,因为 buffer 中的数据量可能不足 byteCount 个,谁小就用谁
    long toRead = Math.min(byteCount, buffer.size);
    // 将 buffer 中数据读到 sink 中
    return buffer.read(sink, toRead);
    }

    }
    复制代码

    RealBufferedSink
    从名字中看出,带有缓冲的输出流(既然是输出流,那么你们想要往外写的数据都通过我来写),但它不是真正的输出流,真正的输出流是 成员变量 sink,它只是包装装饰了一下,当 write 开头的方法被调用时,都是先将数据写到成员变量 buffer 中,然后再通过成员变量 sink 往外写。


    来看 write 方法:


    final class RealBufferedSink implements BufferedSink {
    // 缓冲,当需要通过我进行数据输出时,数据会先存到 buffer 中,再通过 sink 输出
    public final Buffer buffer = new Buffer();
    // 真正输出流,也是此 RealBufferedSink 的下游,当需要进行数据输出时,通过 sink 输出
    public final Sink sink;
    boolean closed;
    ...
    @Override public void write(Buffer source, long byteCount)
    throws IOException {
    if (closed) throw new IllegalStateException("closed");
    // 将 source 中的数据写到 buffer 中
    buffer.write(source, byteCount);
    // 将 buffer 中的数据通过 sink 写出去,内部具体如何写的,在后续 Buffer 章节中详细分析
    emitCompleteSegments();
    }
    复制代码

    整个输入流到输出流可用如下图表示:



    Buffer


    缓冲的意思,Buffer 是 Okio 中的核心类,整个数据中转靠的就是它。


    那么请问,我们为什么要 Buffer 这个东西?我们之前用 Java IO 时不用带 Buffer 的 InputStream 不也照样可以读么?


    我们举个通俗的例子解答这个问题,假如,我们在果园里有一颗苹果树,想吃的时候,去摘一个,什么时候再想吃了,再去树上摘一个,那么,这样跑来跑去的不累么?每次还得跑到园子里。那我们何不先摘个十个八个的,放到箩筐里面带回家,想吃的时候,直接从箩筐里拿,就不必跑那么远到树上去摘了。


    Buffer 就是扮演的上面的箩筐的角色,所以 Buffer 的存在是非常关键的,可以做到省时省力。


    看下 Okio 中 Buffer:


    --> Buffer.java


    public final class Buffer implements BufferedSource, BufferedSink, Cloneable, ByteChannel {
    // Buffer 实现了 BufferedSource,BufferedSink,也就是说,Buffer 既可以
    // 作为 Source 的缓冲,也可以作为 Sink 的缓冲,
    private static final byte[] DIGITS =
    { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' };
    static final int REPLACEMENT_CHARACTER = '\ufffd';

    // 关键点,存储数据链表头
    @Nullable Segment head;
    // 当前 buffer 中的字节数量
    long size;
    ...

    public Buffer() {
    }
    }

    复制代码

    Buffer 是用来缓冲数据的,要缓冲数据,就需要容器,那么它内部靠什么来存储数据?是 Segment。Buffer 中主要就是依靠 Segment 来存储数据,将 Segment 组成循环链表的结构,可用下图表示:



    那我们就来看看 Segment 是什么,注意,Buffer 还没说完,等 Segment 分析完后,会再通过分析具体方法,来看看 Buffer 与 Segment 是如何配合完成工作的。


    Segment


    Segment 片段的意思,先看下类源码:


    --> Segment.java


    final class Segment {
    // 每个 Segment 最大容量,也就是最多容纳 8192 个字节
    static final int SIZE = 8192;

    // 在 Segment 分割的场景(后面讲),如果要分割出去的字节数大于 1024 这个临界值,
    // 那么直接共享 data,不要去做数组拷贝了
    static final int SHARE_MINIMUM = 1024;

    // 真正存放数据的地方
    final byte[] data;

    // data 中第一个可读位置,比如当前 data 中有 1024 个字节,pos = 0,被读取了一个字节后
    // pos = 1,下次再读的话,就要从 1 开始读了
    int pos;

    // data 中第一个可写的位置,比如当前 data 中有 1024 个字节,那么第一个可写的位置是 1025
    int limit;

    // 此 Segment 是否在与别的 Segment 或者 ByteString 共享 data
    boolean shared;

    // 该 Segment 是否独享 data,独享时,可向 data 写入数据
    boolean owner;

    // 循环链表下一个节点
    Segment next;

    // 循环链表前一个节点
    Segment prev;

    Segment() {
    this.data = new byte[SIZE];
    this.owner = true;
    this.shared = false;
    }

    Segment(byte[] data, int pos, int limit, boolean shared, boolean owner) {
    this.data = data;
    this.pos = pos;
    this.limit = limit;
    this.shared = shared;
    this.owner = owner;
    }
    ...
    }
    复制代码

    看到 next,prev,就知道 Segment 会组成双向链表结构,只不过 Okio 中是双向循环链表,Segment 可用如下图表示:


    Segment 内部定义了操作数据、操作链表的方法,我们着重分析以下几个方法:



    • push

    • pop

    • split

    • compact

    • writeTo


    push


    // 往当前链表中添加一个节点,放在被调用 Segment 之后
    public final Segment push(Segment segment) {
    segment.prev = this;
    segment.next = next;
    next.prev = segment;
    next = segment;
    return segment;
    }
    复制代码

    pop


    // 从当前链表中删除该节点
    public final @Nullable Segment pop() {
    Segment result = next != this ? next : null;
    prev.next = next;
    next.prev = prev;
    next = null;
    prev = null;
    return result;
    }
    复制代码

    上面两个方法是简单的链表增删操作,不需要注释,只需要注意以下几点:


    push:


    1、先将被push进来的节点的 prev、next 安顿好


    2、再将与新节点相邻的前一个节点的 next 安顿好


    3、最后将与新节点相邻的后一个节点的 prev 安顿好


    pop:


    1、先安顿前邻居的 next


    2、再安顿后邻居的 prev


    3、最后安顿自己的 prev、next


    总之,在一个双向链表中增删操作,无非就是关心三个节点:自己、前邻居、后邻居,自己要解决prev next,前邻居解决next,后邻居解决 prev


    split



    public final Segment split(int byteCount) {
    if (byteCount <= 0 || byteCount > limit - pos) throw new IllegalArgumentException();
    Segment prefix;

    // We have two competing performance goals:
    // 我们有两个竞争的性能目标
    // - Avoid copying data. We accomplish this by sharing segments.
    // - 避免数据的拷贝。我们通过共享 Segment 来达成这一目标
    // - Avoid short shared segments. These are bad for performance because they are readonly and
    // may lead to long chains of short segments.
    // - 避免短的共享 Segment,这样会导致性能差,因为共享后,Segment 只能读不能写,并且
    // 可能导致链表中短 Segment 链变的很长
    // To balance these goals we only share segments when the copy will be large.
    // 为了平衡这些目标,我们只在需要拷贝的数据量比较大时,才会采用共享 Segment 的方式
    if (byteCount >= SHARE_MINIMUM) {
    // 当要分割出去的数据大于SHARE_MINIMUM(1024)时,会采用共享 Segment 的方式创建新的Segment,
    // 注意,共享的是 Segment 里面的 data,还是会创建一个新的 Segment,只不过 data 是同一个
    prefix = sharedCopy();
    } else {
    // 当要分割的数据小于SHARE_MINIMUM(1024)时,那么直接 copy 吧,反正顶天不会超过 1024 个字节
    prefix = SegmentPool.take();
    System.arraycopy(data, pos, prefix.data, 0, byteCount);
    }

    // 到这里,我们分割出来的 Segment 就创建好了,但是我们要始终注意的是:分割的是
    // Segment 里的 data,data 被一刀两断了,那么两个 Segment 是不是要把 pos,limit 都调整下

    // 调整新创建的 Segment 的 limit 为 pos + byteCount
    prefix.limit = prefix.pos + byteCount;
    // 调整原 Segment 的 pos 为 pos + byteCount
    pos += byteCount;
    // 加到该 Segment 的前节点后面,其实在 Okio 中,这个 prev 就是 tail,
    // 你可能会问,这个方法哪个 Segment 对象都可以调用,这个 prev 是调用这个方法
    // 的 Segment 的前节点,为啥会是 tail 节点?其实在 Okio 框架内部只有一个地方
    // 调用了,就是在 Buffer 的 write(Buffer source, long byteCount) 里,
    // 里面是用 head 去调用的,而 head 的 prev 是 tail
    // 你可能又有疑问了,这方法是 public 的,外部都可以访问啊,但请看 Segment ,这家伙
    // 不是 public 的,包内访问限制,外部无法使用,只能在 Okio 内使用,so...
    prev.push(prefix);
    return prefix;
    }
    复制代码

    split 方法的过程可用下图表示:



    split 过程完成后,Buffer 中 链表变化如下图:



    那么这个 split 方法有何用呢? 上面注释中提到过,split 方法在 Okio 中唯一调用处在 Buffer 的 write(Buffer source, long byteCount),这个方法中的最后一段注释这样写道:


    /**
    * Occasionally we write only part of a source buffer to a sink buffer. For
    * example, given a sink [51%, 91%], we may want to write the first 30% of
    * a source [92%, 82%] to it. To simplify, we first transform the source to
    * an equivalent buffer [30%, 62%, 82%] and then move the head segment,
    * yielding sink [51%, 91%, 30%] and source [62%, 82%].
    */
    复制代码

    意思是:在某些场景下,我们只需要从 Source 中 写一部分数据 到 Sink 中,例如我们现在有一个 Sink,未读数据占比是[51%, 91%](注意,代表这个 Buffer 有两个 Segment,未读数据占比),我们可能想从一个[92%, 82%]的 Source 中往 Sink 中写30%的数据。为了简便,我们首先把 Source 转化为一个同等的 Buffer [30%, 62%, 82%],然后把头 Segment 移动到 Sink 中去(注意是移动,不是拷贝,这样就是一个指针操作),那么此时 Source 中只剩[62%, 82%]。


    不难理解,无非就是把 Source 的两个 Segment 拆成三个,然后把其中一个移到 Sink 中,这样就避免了比较大的数据量的拷贝,只是移动了指针,在 split 中,当需要写的数据小于1024,才会有拷贝操作,大于1024时,直接共享数据,所以这里是一个性能提升的地方。


    writeTo


    // 把当前 Segment 数据写到 sink 中去
    public final void writeTo(Segment sink, int byteCount) {
    // 当 sink 不独享数据时,直接抛异常,因为不独享时,只可读,不可写
    if (!sink.owner) throw new IllegalArgumentException();
    // 若 sink 的可写空间不足了
    if (sink.limit + byteCount > SIZE) {
    // We can't fit byteCount bytes at the sink's current position. Shift sink first.
    // 若 sink 与别的 Segment 共享数据,只读不写,直接抛异常
    if (sink.shared) throw new IllegalArgumentException();
    // 若将已读空间重复利用,sink空间还不够,抛异常
    if (sink.limit + byteCount - sink.pos > SIZE) throw new IllegalArgumentException();
    System.arraycopy(sink.data, sink.pos, sink.data, 0, sink.limit - sink.pos);
    sink.limit -= sink.pos;
    sink.pos = 0;
    }

    System.arraycopy(data, pos, sink.data, sink.limit, byteCount);
    sink.limit += byteCount;
    pos += byteCount;
    }
    }
    复制代码

    writeTo 过程可用如下图表示:



    compact


    // 压缩,将 tail 中数据往 prev 中转移
    public final void compact() {
    // 若前节点就是自己,那么此时链表中只有一个节点
    if (prev == this) throw new IllegalStateException();
    // 若前节点非独享,也就是不可写,直接return
    if (!prev.owner) return; // Cannot compact: prev isn't writable.
    // 当前节点的未读数据大小
    int byteCount = limit - pos;
    // 前节点的最大可写空间,记得我们上面的 writeTo方法吗,最大可写空间=可写空间+已读空间
    int availableByteCount = SIZE - prev.limit + (prev.shared ? 0 : prev.pos);
    // 若前节点的最大可写空间容不下即将要写入的数据,直接return
    if (byteCount > availableByteCount) return; // Cannot compact: not enough writable space.
    // 到这里肯定是可以写入的,可不还是调用 writeTo 么
    writeTo(prev, byteCount);
    // 当前节点从链表中断开
    pop();
    // 从链表中断开了别扔了,回收,下次利用
    SegmentPool.recycle(this);
    }
    复制代码

    compact 是压缩的意思,在有的情况下,链表中的 Segment 利用率不高(可写的空间还有很多),这个时候我们能不能把后面一个节点的数据往这个节点里面压一压呢?以提高利用率,同时可以回收后一个节点,减小链表长度,一举两得。要注意的是,这个方法是 tail 节点调用。


    compact过程可由下图表示:

    Segment 提供了这些原子方法,让他人去调用吧。


    Buffer 的 write 和 read 方法


    之前在 Buffer 小节,还未说完,我们现在通过 Buffer 的 write 和 read 方法来具体分析从一个缓存到另一个缓存的读写过程。


    write


    // 从 source 中移动 byteCount 个字节到当前Buffer(去掉许多注释)
    @Override public void write(Buffer source, long byteCount) {
    // Move bytes from the head of the source buffer to the tail of this buffer
    // 从 source 的 head 移动数据到当前Buffer的 tail中

    if (source == null) throw new IllegalArgumentException("source == null");
    if (source == this) throw new IllegalArgumentException("source == this");
    // 检查偏移和移动数量,防止越界
    checkOffsetAndCount(source.size, 0, byteCount);
    // 为什么是while循环,因为byteCount可能很大,而Segment移动数据有限
    while (byteCount > 0) {
    // Is a prefix of the source's head segment all that we need to move?
    // 如果source的头里面的未读数据就比byteCount大
    if (byteCount < (source.head.limit - source.head.pos)) {
    // 因为写都是往尾部写入,这里先找到当前Buffer的tail节点
    Segment tail = head != null ? head.prev : null;
    if (tail != null && tail.owner
    && (byteCount + tail.limit - (tail.shared ? 0 : tail.pos) <= Segment.SIZE)) {
    // Our existing segments are sufficient. Move bytes from source's head to our tail.
    // 当前Buffer的tail节点的可写空间就够了,直接将数据写入tail就行,这里使用的是拷贝
    source.head.writeTo(tail, (int) byteCount);
    source.size -= byteCount;
    size += byteCount;
    return;
    } else {
    // We're going to need another segment. Split the source's head
    // segment in two, then move the first of those two to this buffer.
    // 将source的head一分为二,因为当前Buffer的tail容不下byteCount个字节了
    // 当然,你可能会问,如果tail为null或者不满足上面的if里的任何一个条件都会走
    // 这里的else,别忘了,这个else始终被外层的if条件约束着
    source.head = source.head.split((int) byteCount);
    }
    }

    // Remove the source's head segment and append it to our tail.
    // 如过走了上面的if,那么这里的source.head已经是分割过的,如果没走if,什么都没干
    Segment segmentToMove = source.head;
    long movedByteCount = segmentToMove.limit - segmentToMove.pos;
    // 将segmentToMove从source中移除,因为它将要加入到当前的Buffer中
    source.head = segmentToMove.pop();
    if (head == null) {
    head = segmentToMove;
    head.next = head.prev = head;
    } else {
    Segment tail = head.prev;
    // 加入到当前Buffer中
    tail = tail.push(segmentToMove);
    // 再压缩一下,尽量让segment满载,提高利用率
    tail.compact();
    }

    // 数据移动完了,设置source与当前Buffer的变量,source数据少了,这边多了
    source.size -= movedByteCount;
    size += movedByteCount;
    byteCount -= movedByteCount;

    //如果一趟没将byteCount个字节的数据移动完,再进行下一次循环
    }
    }
    复制代码

    read


    public long read(Buffer sink, long byteCount) {
    if (sink == null) throw new IllegalArgumentException("sink == null");
    if (byteCount < 0) throw new IllegalArgumentException("byteCount < 0: " + byteCount);
    if (size == 0) return -1L;
    if (byteCount > size) byteCount = size;
    // 其实就是将当前Buffer中的数据写到sink中,这里好像与我们看到这个方法的第一反应
    // 有点不同,我们可能想的是:既然是read,就是要从外头读数据,但是这里是往外头读
    // 我们还是要理解source与sink的概念,从source读,往sink中写
    sink.write(this, byteCount);
    return byteCount;
    }

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

    java 设计模式:责任链模式与Android事件传递

    1、概念是一个请求有多个对象来处理,这些对象是一条链,但具体由哪个对象来处理,根据条件判断来确定,如果不能处理会传递给该链中的下一个对象,直到有对象处理它为止。2、使用场景有多个对象可以处理同一个请求,具体哪个对象处理该请求待运行时刻再确定在不明确指定接收者的...
    继续阅读 »

    1、概念

    是一个请求有多个对象来处理,这些对象是一条链,但具体由哪个对象来处理,根据条件判断来确定,如果不能处理会传递给该链中的下一个对象,直到有对象处理它为止。

    2、使用场景

    1. 有多个对象可以处理同一个请求,具体哪个对象处理该请求待运行时刻再确定
    2. 在不明确指定接收者的情况下,向多个对象中的一个提交一个请求。
    3. 可动态制定一组对象处理请求,客户端可以动态创建职责链来处理请求。

    3、uml结构分析


    4、实际代码分析


    /**
    * 抽象请假审批类
    */
    public abstract class Handler {
    public int maxDay;
    private Handler nextHandler;

    public Handler(int maxDay) {
    this.maxDay = maxDay;
    }

    public void setNextHandler(Handler nextHandler) {
    this.nextHandler = nextHandler;
    }

    public void handlerRequest(int day){
    if(day <= maxDay){
    reply(day);
    } else{
    if(nextHandler != null){
    nextHandler.handlerRequest(day);
    }else {
    System.out.println("没有跟高的领导审批了");
    }
    }
    }

    protected abstract void reply(int day);
    }
    /**
    * 技术主管审批类
    */
    public class Handler1 extends Handler {
    public Handler1() {
    super(10);
    }

    @Override
    protected void reply(int day) {
    System.out.print(day+"天请假,技术主管直接通过");
    }
    }

    * 项目经理审批类
    */
    public class Handler2 extends Handler {
    public Handler2() {
    super(5);
    }

    @Override
    protected void reply(int day) {
    System.out.print(day+"天请假,项目经历直接通过");
    }
    }
    //代码运行
    Handler2 handler2 = new Handler2();
    handler2.setNextHandler(new Handler1());
    handler2.handlerRequest(3);

    在java中的实际运用,try-cache语句

    在android 中发布有序广播 ordered broadcast,viewGroup/view 事件传递

    优点与缺点

    责任链模式是一种对象行为型模式,其主要优点如下。

    1. 降低了对象之间的耦合度。该模式使得一个对象无须知道到底是哪一个对象处理其请求以及链的结构,发送者和接收者也无须拥有对方的明确信息。
    2. 增强了系统的可扩展性。可以根据需要增加新的请求处理类,满足开闭原则。
    3. 增强了给对象指派职责的灵活性。当工作流程发生变化,可以动态地改变链内的成员或者调动它们的次序,也可动态地新增或者删除责任。
    4. 责任链简化了对象之间的连接。每个对象只需保持一个指向其后继者的引用,不需保持其他所有处理者的引用,这避免了使用众多的 if 或者 if···else 语句。
    5. 责任分担。每个类只需要处理自己该处理的工作,不该处理的传递给下一个对象完成,明确各类的责任范围,符合类的单一职责原则。


    其主要缺点如下。

    1. 不能保证每个请求一定被处理。由于一个请求没有明确的接收者,所以不能保证它一定会被处理,该请求可能一直传到链的末端都得不到处理。
    2. 对比较长的职责链,请求的处理可能涉及多个处理对象,系统性能将受到一定影响。
    3. 职责链建立的合理性要靠客户端来保证,增加了客户端的复杂性,可能会由于职责链的错误设置而导致系统出错,如可能会造成循环调用。

    viewGroup/view 事件传递

    1)主要概念

    MotionEvent 事件分发的基本操作,所有事件都是MotionEvent,对MotionEvent操作。

    主要有三种操作 ACTION_DOWN/ACTION_MOVE/ACTION_UP

    三个方法

    dispatchTouchEvent 当有事件传递给view的时候,view就会调用该方法。这个返回值为boolean,返回告诉系统是否消耗了该事件。true为消耗,不会继续向下分发

    onIntereptTouchEvent 拦截事件,只存在于viewgroup,view(存在于view树的最底层,没必要拦截了)中没有拦截事件。如果当前view 成功拦截这个事件,则要返回true,默认为false

    onTouchEvent 具体处理整个事件逻辑的,对于他的返回结果就是是否消耗当前事件,如果不消耗当前事件的话,对于同一个事件,当前view就不会再接到这个事件。

    总结:如果当前view可以处理就拦截处理,如果不可以,就交给子view

    事件处理顺序

    Activity -> PhoneWindow ->RootView ->ViewGroup ->View

    2)viewgroup的事件分发

    流程:

    1. 判断自身是否需要
    2. 自身不需要或者不确定,询问childview
    3. 如果子childview 不需要则调用自身的onTouchEvent

    3)view的事件分发

    view为啥会有dispatchTouchEvent方法?view可以注册很多事件监听器,可以进行事件分发

    1. 单机事件(onClickListener)
    2. 长按事件(onLongClickListener)
    3. 触摸事件(onTouchListener)
    4. View自身处理(onTouchEvent)

    事件调用顺序:onTouchListener>onTouchEvent>onLongClickListener>onClickListener

    收起阅读 »

    java 设计模式:观察者

    1、概念在对象之间定义了一对多的依赖,使得么当一个对象状态发生改变,其相关依赖对象会收到通知并自动更新。2、场景一个抽象模型有两个方面,其中一个方面依赖于另一个方面一个对象的改变将导致一个或多个其他对象也发生改变需要在系统中创建一个触发链3、UML结构图分析抽...
    继续阅读 »

    1、概念

    在对象之间定义了一对多的依赖,使得么当一个对象状态发生改变,其相关依赖对象会收到通知并自动更新。

    2、场景

    1. 一个抽象模型有两个方面,其中一个方面依赖于另一个方面
    2. 一个对象的改变将导致一个或多个其他对象也发生改变
    3. 需要在系统中创建一个触发链

    3、UML结构图分析

    • 抽象被观察者角色:也就是一个抽象主题,它把所有对观察者对象的引用保存在一个集合中,每个主题都可以有任意数量的观察者。抽象主题提供一个接口,可以增加和删除观察者角色。一般用一个抽象类和接口来实现。
    • 抽象观察者角色:为所有的具体观察者定义一个接口,在得到主题通知时更新自己。
    • 具体被观察者角色:也就是一个具体的主题,在集体主题的内部状态改变时,所有登记过的观察者发出通知。
    • 具体观察者角色:实现抽象观察者角色所需要的更新接口,一边使本身的状态与制图的状态相协调。

    4、实际代码分析

    实现观察者代码:


    /**
    *
    * 创建观察者抽象类
    */
    public interface Observer {

    //更新方法
    void update(String newStatus);

    }

    /**
    * 创建观察者实现类
    */
    public class ConcreteObserver implements Observer {

    /**
    * 观察者状态
    */
    private String observerState;

    @Override
    public void update(String newStatus) {
    observerState = newStatus;
    System.out.println(newStatus);
    }
    }

    /**
    * 创建抽象目标者
    * Created by shidawei on 2019/5/23.
    */
    public abstract class Subject {

    private List<Observer> mObservers = new ArrayList<>();

    /**
    * 注册观察
    * @param observer
    */
    public void attach(Observer observer){
    mObservers.add(observer);
    System.out.println("注册观察");
    }

    /**
    * 移除观察者
    * @param observer
    */
    public void detach(Observer observer){
    mObservers.remove(observer);
    }

    /**
    * 通知观察者
    * @param newStatus
    */
    public void notifyObsercers(String newStatus){
    for(Observer observer:mObservers){
    observer.update(newStatus);
    }
    }
    }

    /**
    * 实现被观察者
    */
    public class ConcreteSubject extends Subject {
    private String state;

    public String getState() {
    return state;
    }
    public void change(String newState){
    state = newState;
    System.out.println(newState);
    notifyObsercers(newState);
    }
    }

    //测试代码
    ConcreteSubject concreteSubject = new ConcreteSubject();
    concreteSubject.attach(new ConcreteObsercer());
    concreteSubject.attach(new ConcreteObsercer());
    concreteSubject.attach(new ConcreteObsercer());
    concreteSubject.change("123");

    java内部的接口实现:

    /**
    * Observable 是被观察者对象接口,是对被观察者的实现
    */
    public class TargetObervable extends Observable {

    private String message;

    public String getMessage() {
    return message;
    }

    public void setMessage(String message) {
    this.message = message;
    //被观察者数据发生改变的时候通过如下两行代码通知所有观察者
    this.setChanged();
    this.notifyObservers();
    }
    }
    /**
    * Observer 对象是观察者,实现Observer的对象就是实现观察者对象
    */
    public class TargetOberver implements Observer {


    private String name;

    public String getName() {
    return name;
    }

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

    @Override
    public void update(Observable o, Object arg) {
    System.out.println(name + "收到数据变更" + ((TargetObervable) o).getMessage());
    }
    }

    //测试代码

    TargetObervable targetObervable = new TargetObervable();
    targetObervable.addObserver(new TargetOberver());
    targetObervable.addObserver(new TargetOberver());
    targetObervable.addObserver(new TargetOberver());
    targetObervable.setMessage("1234");

    两种观察者对比:

    通过读源码Observable.class我们得知对Observer的集合为Vector

    Vector为线程安全的,会保证线程安全,但是性能差。可以采用CopyOnWriteArrayList来代替Vector。

    观察者设计模式在Android中的实际运用

    回调模式:一对一的模式

    实现了抽象类/接口的实例实现了负累的提供的抽象方法,然后将该方法还给父类来处理。

    Fragment与activity通信的代码实例:

    /**
    *回调接口,与activity通信
    **/

    public interface ISwitchCaoZuoRecordFragment {
    void toSwitch(CaiZuoRecordFragFragment fragment, CaiZuoRecordFragPresenter presenterDecorator);
    }
    /**
    * activity实现该接口
    **/
    public class CaoZuoRecordActivity extends BaseActivity<CaoZuoRecordView, CaoZuoRecordPresenter> implements CaoZuoRecordView ,CaiZuoRecordFragFragment.ISwitchCaoZuoRecordFragment{
    @Override
    public void toSwitch(CaiZuoRecordFragFragment fragment, CaiZuoRecordFragPresenter presenterDecorator) {
    mPresenterDecorator = presenterDecorator;
    if(presenterDecorator.studentName!=null&&!presenterDecorator.studentName.equals("")){
    searchListTitleBar.getSearch().setText(presenterDecorator.studentName);
    searchListTitleBar.getClear().setVisibility(View.VISIBLE);
    searchListTitleBar.getSearch_layout().setVisibility(View.VISIBLE);
    }else{
    searchListTitleBar.getSearch().setText("");
    searchListTitleBar.getClear().setVisibility(View.GONE);
    searchListTitleBar.getSearch_layout().setVisibility(View.GONE);
    }

    }

    /**
    *fragment注册和取消
    */
    @Override
    public void onDetach() {
    super.onDetach();
    iSwitchCaoZuoRecordFragment = null;

    }

    @Override
    public void onAttach(Context context) {
    super.onAttach(context);
    if (context instanceof ISwitchCaoZuoRecordFragment) {
    iSwitchCaoZuoRecordFragment = (ISwitchCaoZuoRecordFragment) context;
    }else{
    throw new RuntimeException(context.toString()
    + " 必须实现 ISwitchCaoZuoRecordFragment");
    }
    }


    收起阅读 »

    java 设计模式:模版方法

    模板方法模式  定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。 模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。通俗的说的就是有很多相同的步骤的,在某一些地方可能有一些差别适合于这种模式,如大话设计模式中说到的考试场景中,每个人...
    继续阅读 »

    模板方法模式

      定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。 模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。通俗的说的就是有很多相同的步骤的,在某一些地方可能有一些差别适合于这种模式,如大话设计模式中说到的考试场景中,每个人的试卷都是一样的,只有答案不一样。这种场景就适合于模板方法模式。

    模板方法模式适用的业务场景

    1. 算法或者操作遵循相似的逻辑
    2. 重构时(把相同的代码抽取到父类中)
    3. 重要、复杂的算法,核心算法设计为模板方法

    代码实现

    现在我有一个需求,就是要建立一套上课业务规则

    • 上课
    • 考情
    • 下课

    /**
    * 班级规则
    */
    public abstract class ClassShangke {

    public void shangke(){

    }

    public abstract void kaoqing();

    public void xiake(){

    }

    //注意模版规则 final 不可被重写
    public final void guize(){
    shangke();
    kaoqing();
    xiake();
    }

    }

    /**
    * A班级上课
    */
    public class AClassKaoQing extends ClassShangke {
    @Override
    public void kaoqing() {
    System.out.println("AClassKaoQing kaoqing");
    }
    }


    /**
    * B班级上课
    */
    public class BClassKaoQing extends ClassShangke {
    @Override
    public void kaoqing() {
    System.out.println("BClassKaoQing kaoqing");
    }
    }

    //运行

    ClassShangke classShangkea = new AClassKaoQing();
    classShangkea.guize();
    ClassShangke classShangkeb = new BClassKaoQing();
    classShangkeb.guize();

    我在项目中对网络请求返回数据进行模版方法定义

    public abstract class BaseRxNetworkResponseObserver<T extends SModel> extends BaseRxNetworkObserver<T> {

    @Override
    public final void onNext(T t) {
    onBeforeResponseOperation();
    try{
    onResponse(t);
    }catch (Exception e){
    ULog.e(e,e.getMessage());
    ULog.e(e);
    TipToast.shortTip(e.getMessage());
    onResponseFail(new CustomarilyException(e.getMessage()));
    }finally {
    onNextFinally();
    }
    }

    protected void onNextFinally() {
    }


    /**
    * 返回值
    * @param t
    */
    public abstract void onResponse(T t);

    /**
    * 错误
    * @param e
    */
    public abstract void onResponseFail(Exception e);

    /**
    * 执行一些起始操作
    */
    protected abstract void onBeforeResponseOperation();
    }

    注意模版方法的final 为了制定一些规则必须不可以被重写

    模板方法模式的优点

    • 封装性好
    • 复用性好 
    • 屏蔽细节
    • 便于维护
    收起阅读 »

    java 设计模式:策略模式

    概念:策略模式定义了一系列的算法,并将每一个算法封装起来,而且使它们还可以相互替换,策略模式让算法独立于使用它的客户而独立变化。策略模式使这些算法在客户端调用它们的时候能够互不影响地变化。使用场景:一个类定义了多种行为,并且这个行为在这个类的方法中以多个条件语...
    继续阅读 »

    概念:

    策略模式定义了一系列的算法,并将每一个算法封装起来,而且使它们还可以相互替换,策略模式让算法独立于使用它的客户而独立变化。

    策略模式使这些算法在客户端调用它们的时候能够互不影响地变化。

    使用场景:

    一个类定义了多种行为,并且这个行为在这个类的方法中以多个条件语句形式出现,那么可以使用策略模式避免在类中使用大量的条件语句。

    UML:

    代码展示:

    为了更清晰的展示出策略模式的优点,我在此写一套不用策略模式实现的代码。如下:

    /**
    * 有如下几个超市价格规则 1:五月一日 价格统统8折 2:十月一日 价格统统7折 3:十一月一日 价格统统半价
    * Created by on 2019/4/30.
    */
    public class Price {

    public double jisuanPrice(double price,String s){
    double p = price;
    System.out.println(s);
    switch (s){
    case "五月一日":
    p = price*0.8;
    break;
    case "十月一日":
    p = price*0.7;
    break;
    case "十一月一日":
    p = price*0.5;
    break;
    }
    return p;
    }

    }
    //运行
    Price p = new Price();
    System.out.println(p.jisuanPrice(10),"五月一日");

    你会发现,也很清晰表示了判断不同的日期来计算不同的价格,不同日期用switch来判断,但是呢,如果日期很多,而且规则又有了对特定商品的价格规则,那么这个类的负担是否有些复杂了呢?这个类不是单一职责。还根据swith来计算规则。

    于是我写下了如下的策略方法:

    /**
    * 半价计算策略
    */
    public class Banjia implements IPrice {
    @Override
    public double jisuanPrice(double price) {
    return price*0.5;
    }
    }
    /**
    * 8折计算策略
    */
    public class BaZhe implements IPrice {
    @Override
    public double jisuanPrice(double price) {
    return price*0.8;
    }
    }
    /**
    * 正常计算策略
    */
    public class NorPrice implements IPrice {
    @Override
    public double jisuanPrice(double price) {
    return price;
    }
    }

    /**
    * 7折计算
    */
    public class Qizhe implements IPrice {
    @Override
    public double jisuanPrice(double price) {
    return price*0.7;
    }
    }
    //作为价格管理器一定要持有IPrice的引用
    public class Price{

    private IPrice iPrice;

    public void setiPrice(IPrice iPrice) {
    this.iPrice = iPrice;
    }

    public double jisuanPrice(double price) {
    return iPrice.jisuanPrice(price);
    }
    }
    //运行
    //1.创建具体测策略实现
    IPrice iprice = new BaZhe();
    //2.在创建策略上下文的同时,将具体的策略实现对象注入到策略上下文当中
    Price p = new Price();
    p.setiPrice(iprice);
    //3.调用上下文对象的方法来完成对具体策略实现的回调
    System.out.println(p.jisuanPrice(10));

    这个能够明显是对价格做了分类,假如说十月一日要进行半价打折,那你是不是很容易就改变了策略呢?不需要动其他代码。

    策略模式优点

    1、上下文Context 和具体策略器(oncreteStrategy)是松耦合关系。

    2、满足开-闭原则。增加新的具体策略不需要修改context。

    开闭原则:

    1. 对于扩展是开放的(Open for extension)。这意味着模块的行为是可以扩展的。当应用的需求改变时,我们可以对模块进行扩展,使其具有满足那些改变的新行为。也就是说,我们可以改变模块的功能。
    2. 对于修改是关闭的(Closed for modification)。对模块行为进行扩展时,不必改动模块的源代码或者二进制代码。

    策略和上下文的关系:

    • 在策略模式中,一般情况下都是上下文持有策略的引用,以进行对具体策略的调用。但具体的策略对象也可以从上下文中获取所需数据,可以将上下文当做参数传入到具体策略中,具体策略通过回调上下文中的方法来获取其所需要的数据。

    如下例子来自 https://www.cnblogs.com/lewis0077/p/5133812.html 非常好的一篇文章

    **下面我们演示这种情况:**

      在跨国公司中,一般都会在各个国家和地区设置分支机构,聘用当地人为员工,这样就有这样一个需要:每月发工资的时候,中国国籍的员工要发人民币,美国国籍的员工要发美元,英国国籍的要发英镑。
    public interface PayStrategy {
    //在支付策略接口的支付方法中含有支付上下文作为参数,以便在具体的支付策略中回调上下文中的方法获取数据
    public void pay(PayContext ctx);
    }

    //人民币支付策略
    public class RMBPay implements PayStrategy {
    @Override
    public void pay(PayContext ctx) {
    System.out.println("现在给:"+ctx.getUsername()+" 人民币支付 "+ctx.getMoney()+"元!");
    }
    }

    //美金支付策略
    public class DollarPay implements PayStrategy {
    @Override
    public void pay(PayContext ctx) {
    System.out.println("现在给:"+ctx.getUsername()+" 美金支付 "+ctx.getMoney()+"dollar !");
    }
    }

    //支付上下文,含有多个算法的公有数据
    public class PayContext {
    //员工姓名
    private String username;
    //员工的工资
    private double money;
    //支付策略
    private PayStrategy payStrategy;

    public void pay(){
    //调用具体的支付策略来进行支付
    payStrategy.pay(this);
    }

    public PayContext(String username, double money, PayStrategy payStrategy) {
    this.username = username;
    this.money = money;
    this.payStrategy = payStrategy;
    }

    public String getUsername() {
    return username;
    }

    public double getMoney() {
    return money;
    }
    }

    //外部客户端
    public class Client {
    public static void main(String[] args) {
    //创建具体的支付策略
    PayStrategy rmbStrategy = new RMBPay();
    PayStrategy dollarStrategy = new DollarPay();
    //准备小王的支付上下文
    PayContext ctx = new PayContext("小王",30000,rmbStrategy);
    //向小王支付工资
    ctx.pay();

    //准备Jack的支付上下文
    ctx = new PayContext("jack",10000,dollarStrategy);
    //向Jack支付工资
    ctx.pay();
    }
    }
    控制台输出:

    现在给:小王 人民币支付 30000.0元!
    现在给:jack 美金支付 10000.0dollar !

    那现在我们要新增一个银行账户的支付策略,该怎么办呢?

      显然我们应该新增一个支付找银行账户的策略实现,由于需要从上下文中获取数据,为了不修改已有的上下文,我们可以通过继承已有的上下文来扩展一个新的带有银行账户的上下文,然后再客户端中使用新的策略实现和带有银行账户的上下文,这样之前已有的实现完全不需要改动,遵守了开闭原则。

    //银行账户支付
    public class AccountPay implements PayStrategy {
    @Override
    public void pay(PayContext ctx) {
    PayContextWithAccount ctxAccount = (PayContextWithAccount) ctx;
    System.out.println("现在给:"+ctxAccount.getUsername()+"的账户:"+ctxAccount.getAccount()+" 支付工资:"+ctxAccount.getMoney()+" 元!");
    }
    }

    //带银行账户的支付上下文
    public class PayContextWithAccount extends PayContext {
    //银行账户
    private String account;
    public PayContextWithAccount(String username, double money, PayStrategy payStrategy,String account) {
    super(username, money, payStrategy);
    this.account = account;
    }

    public String getAccount() {
    return account;
    }
    }

    //外部客户端
    public class Client {
    public static void main(String[] args) {
    //创建具体的支付策略
    PayStrategy rmbStrategy = new RMBPay();
    PayStrategy dollarStrategy = new DollarPay();
    //准备小王的支付上下文
    PayContext ctx = new PayContext("小王",30000,rmbStrategy);
    //向小王支付工资
    ctx.pay();
    //准备Jack的支付上下文
    ctx = new PayContext("jack",10000,dollarStrategy);
    //向Jack支付工资
    ctx.pay();
    //创建支付到银行账户的支付策略
    PayStrategy accountStrategy = new AccountPay();
    //准备带有银行账户的上下文
    ctx = new PayContextWithAccount("小张",40000,accountStrategy,"1234567890");
    //向小张的账户支付
    ctx.pay();
    }
    }

    控制台输出:

    现在给:小王 人民币支付 30000.0元!
    现在给:jack 美金支付 10000.0dollar !
    现在给:小张的账户:1234567890 支付工资:40000.0 元!

    除了上面的方法,还有其他的实现方式吗?

      当然有了,上面的实现方式是策略实现所需要的数据都是从上下文中获取,因此扩展了上下文;现在我们可以不扩展上下文,直接从策略实现内部来获取数据,看下面的实现:

    //支付到银行账户的策略
    public class AccountPay2 implements PayStrategy {
    //银行账户
    private String account;
    public AccountPay2(String account) {
    this.account = account;
    }
    @Override
    public void pay(PayContext ctx) {
    System.out.println("现在给:"+ctx.getUsername()+"的账户:"+getAccount()+" 支付工资:"+ctx.getMoney()+" 元!");
    }
    public String getAccount() {
    return account;
    }
    public void setAccount(String account) {
    this.account = account;
    }
    }

    //外部客户端
    public class Client {
    public static void main(String[] args) {
    //创建具体的支付策略
    PayStrategy rmbStrategy = new RMBPay();
    PayStrategy dollarStrategy = new DollarPay();
    //准备小王的支付上下文
    PayContext ctx = new PayContext("小王",30000,rmbStrategy);
    //向小王支付工资
    ctx.pay();
    //准备Jack的支付上下文
    ctx = new PayContext("jack",10000,dollarStrategy);
    //向Jack支付工资
    ctx.pay();
    //创建支付到银行账户的支付策略
    PayStrategy accountStrategy = new AccountPay2("1234567890");
    //准备上下文
    ctx = new PayContext("小张",40000,accountStrategy);
    //向小张的账户支付
    ctx.pay();
    }
    }

    控制台输出:

    现在给:小王 人民币支付 30000.0元!
    现在给:jack 美金支付 10000.0dollar !
    现在给:小张的账户:1234567890 支付工资:40000.0 元!

    那我们来比较一下上面两种实现方式:

      扩展上下文的实现:

        优点:具体的策略实现风格很是统一,策略实现所需要的数据都是从上下文中获取的,在上下文中添加的数据,可以视为公共的数据,其他的策略实现也可以使用。

        缺点:很明显如果某些数据只是特定的策略实现需要,大部分的策略实现不需要,那这些数据有“浪费”之嫌,另外如果每次添加算法数据都扩展上下文,很容易导致上下文的层级很是复杂。

      在具体的策略实现上添加所需要的数据的实现:

        优点:容易想到,实现简单

        缺点:与其他的策略实现风格不一致,其他的策略实现所需数据都是来自上下文,而这个策略实现一部分数据来自于自身,一部分数据来自于上下文;外部在使用这个策略实现的时候也和其他的策略实现不一致了,难以以一个统一的方式动态的切换策略实现。


    收起阅读 »

    iOS Instruments使用

    一、Instruments介绍Instruments 一个很灵活的、强大的工具,是性能分析、动态跟踪 和分析OS X以及iOS代码的测试工具,用它可以极为方便收集关于一个或多个系统进程的性能和行为的数据,并能及时随着时间跟踪而产生的数据,并检查所收集的数据,还...
    继续阅读 »

    一、Instruments介绍

    Instruments 一个很灵活的、强大的工具,是性能分析、动态跟踪 和分析OS X以及iOS代码的测试工具,用它可以极为方便收集关于一个或多个系统进程的性能和行为的数据,并能及时随着时间跟踪而产生的数据,并检查所收集的数据,还可以广泛收集不同类型的数据.也可以追踪程序运行的过程,这样instrument就可以帮助我们了解用户的应用程序和操作系统的行为。

    总结一下instrument能做的事情:

    1. Instruments是用于动态调追踪和分析OS X和iOS的代码的性能分析和测试工具;
    2.Instruments支持多线程的调试;
    3.可以用Instruments去录制和回放,图形用户界面的操作过程
    4.可将录制的图形界面操作和Instruments保存为模板,供以后访问使用。
    instrument还可以:

    1.追踪代码中的(甚至是那些难以复制的)问题;
    2.分析程序的性能;
    3.实现程序的自动化测试;
    4.部分实现程序的压力测试;
    5.执行系统级别的通用问题追踪调试;
    6.使你对程序的内部运行过程更加了解。

     打开方式:
    Xcode -> Open Developer Tool -> Instruments


    其中比较常用的有四种:

    1.Allocations:用来检查内存分配,跟踪过程的匿名虚拟内存和堆的对象提供类名和可选保留/释放历史

    2.Leaks:一般的查看内存使用情况,检查泄漏的内存,并提供了所有活动的分配和泄漏模块的类对象分配统计信息以及内存地址历史记录

    3.Time Profiler:分析代码的执行时间,执行对系统的CPU上运行的进程低负载时间为基础采样

    4.Zombies:检查是否访问了僵尸对象

    其他的:

    Blank:创建一个空的模板,可以从Library库中添加其他模板

    Activity Monitor:显示器处理的CPU、内存和网络使用情况统计

    Automation:用JavaScript语言编写,主要用于分析应用的性能和用户行为,模仿/击发被请求的事件,利用它可以完成对被测应用的简单的UI测试及相关功能测试

    Cocoa Layout:观察约束变化,找出布局代码的问题所在。

    Core Animation:用来检测Core Animation性能的,给我们提供了周期性的FPS,并且考虑到了发生在程序之外的动画,界面滑动FPS可以进行测试

    Core Data:监测读取、缓存未命中、保存等操作,能直观显示是否保存次数远超实际需要

    Energy Diagnostic :用于Xcode下的Instruments来分析手机电量消耗的。(必须是真机才有电量)

    GPU Driver :可以测量GPU的利用率,同样也是一个很好的来判断和GPU相关动画性能的指示器。它同样也提供了类似Core Animtaion那样显示FPS的工具。

    Network:分析应用程序如何使用TCP / IP和UDP / IP连接使用连接仪器。就是检查手机网速的。(这个最好是真机)

    二、Allocations(分配)

    1.内存分类:

    Leaked memory:泄漏的内存,如为对象A申请了内存空间,之后再也没用到A,也没有释放A导致内存泄漏(野指针。。。)

    Abandoned memory:被遗弃的内存,如循环引用,递归不断申请内存而导致的内存泄漏

    Cached memory:缓存的内存

    2.Abandoned memory

    其中内存泄漏我们可以用Leaks,野指针可以用Zombies(僵尸对象),而在这里我们就可以用Allocations来检测Abandoned memory的内存。


    即我们采用Generational Analysis的方法来分析,反复进入退出某一场景,查看内存的分配与释放情况,以定位哪些对象是属于Abandoned Memory的范畴。

    在Allocations工具中,有专门的Generational Analysis设置,如下:


    我们可以在程序运行时,在进入某个模块前标记一个Generation,这样会生成一个快照。然后进入、退出,再标记一个Generation,如下图:


    在详情面板中我们可以看到两个Generation间内存的增长情况,其中就可能存在潜在的被遗弃的对象,如下图:


    其中growth就是我们增长的内存,GenerationA是程序启动到进入该场景增长的内存,GenerationB就是第二次进入该场景所增长的内存,查看子类可以发现有两个管理类造成了Abandoned memory

    3.设置Generations

    使用instrument测试内存泄露 工具 Allocations 测试是否内存泄露 使用标记,可以更省事省力的测试页面是否有内存泄露
    1)设置Generations


    2)选择mark generation


    3)使用方法 在进入测试页面之前,mark一下----->进入页面----->退出----->mark------>进入------->退出------->mark------>进入如此往复5、6次,就可以看到如下结果


    这种情况下是内存有泄露,看到每次的增量都是好几百K或者上M的,都是属于内存有泄露的,这时候就需要检测下代码一般情况

    100K以下都属于正常范围,growth表示距离你上次mark的增量

    三、Leaks(泄漏)

    1.内存溢出和内存泄漏的区别

    内存溢出 out of memory,是指程序在申请内存时,没有足够的内存空间供其使用,出现out of memory;比如申请了一个integer,但给它存了long才能存下的数,那就是内存溢出

    内存泄露 memory leak,是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光。

    memory leak会最终会导致out of memory!

    在前面的ALLcations里面我们提到过内存泄漏就是应该释放而没有释放的内存。而内存泄漏分为两种:Leaked Memory 和 Abandoned Memory。前面我们讲到了如何找到Abandoned Memory被遗忘的内存,现在我们研究的就是Leaked Memory

    发生的方式来分类,内存泄漏可以分为4类:

    常发性内存泄漏。发生内存泄漏的代码会被多次执行到,每次被执行的时候都会导致一块内存泄漏。
    偶发性内存泄漏。发生内存泄漏的代码只有在某些特定环境或操作过程下才会发生。常发性和偶发性是相对的。对于特定的环境,偶发性的也许就变成了常发性的。所以测试环境和测试方法对检测内存泄漏至关重要。
    一次性内存泄漏。发生内存泄漏的代码只会被执行一次,或者由于算法上的缺陷,导致总会有一块仅且一块内存发生泄漏。比如,在类的构造函数中分配内存,在析构函数中却没有释放该内存,所以内存泄漏只会发生一次。
    隐式内存泄漏。程序在运行过程中不停的分配内存,但是直到结束的时候才释放内存。严格的说这里并没有发生内存泄漏,因为最终程序释放了所有申请的内存。但是对于一个服务器程序,需要运行几天,几周甚至几个月,不及时释放内存也可能导致最终耗尽系统的所有内存。所以,我们称这类内存泄漏为隐式内存泄漏。

    影响:从用户使用程序的角度来看,内存泄漏本身不会产生什么危害,作为一般的用户,根本感觉不到内存泄漏的存在。真正有危害的是内存泄漏的堆积,这会最终消耗尽系统所有的内存。从这个角度来说,一次性内存泄漏并没有什么危害,因为它不会堆积,而隐式内存泄漏危害性则非常大,因为较之于常发性和偶发性内存泄漏它更难被检测到。

    下边我们介绍Instruments里面的Leaked的用法,首先打开Leaked,跑起工程来,点击要测试的页面,如果有内存泄漏,会出现下图中的红色的❌。然后按照后边的步骤进行修复即可


    上面的旧版的样式,下面的是新版的样式,基本操作差不多



    在详情面板选中显示的若干条中的一条,双击,会自动跳到内存泄露代码处,然后点击右上角 Xcode 图标进行修改。

    下图是对Leaked页面进一步的理解:


    内存泄漏动态分析技巧:

    1.在 Display Settings 界面建议把 Snapshot Interval (snapʃɒt, 数据快照)间隔时间设置为10秒,勾选Automatic Snapshotting,Leaks 会自动进行内存捕捉分析。(新版本直接在底部修改)

    2.熟练使用 Leaks 后会对内存泄漏判断更准确,在可能导致泄漏的操作里,在你怀疑有内存泄漏的操作前和操作后,可以点击 Snapshot Now 进行手动捕捉。

    3.开始时如果设备性能较好,可以把自动捕捉间隔设置为 5 秒钟。

    4.使用ARC的项目,一般内存泄漏都是 malloc、自定义结构、资源引起的,多注意这些地方进行分析。

    5.开启ARC后,内存泄漏的原因,开启了ARC并不是就不会存在内存问题,苹果有句名言:ARC is only for NSObject。

    注:如果你的项目使用了ARC,随着你的操作,不断开启或关闭视图,内存可能持续上升,但这不一定表示存在内存泄漏,ARC释放的时机是不固定的

    这里对 Display Settings中 的 Call tree 选项做一下说明 [官方user guide翻译]:

    Separate By Thread:线程分离,只有这样才能在调用路径中能够清晰看到占用CPU最大的线程.每个线程应该分开考虑。只有这样你才能揪出那些大量占用CPU的"重"线程,按线程分开做分析,这样更容易揪出那些吃资源的问题线程。特别是对于主线程,它要处理和渲染所有的接口数据,一旦受到阻塞,程序必然卡顿或停止响应。

    Invert Call Tree:从上到下跟踪堆栈信息.这个选项可以快捷的看到方法调用路径最深方法占用CPU耗时(这意味着你看到的表中的方法,将已从第0帧开始取样,这通常你是想要的,只有这样你才能看到CPU中花费时间最深的方法),比如FuncA{FunB{FunC}},勾选后堆栈以C->B->A把调用层级最深的C显示最外面.反向输出调用树。把调用层级最深的方法显示在最上面,更容易找到最耗时的操作。

    Hide Missing Symbols:如果dSYM无法找到你的APP或者调用系统框架的话,那么表中将看到调用方法名只能看到16进制的数值,勾选这个选项则可以隐藏这些符号,便于简化分析数据.

    Hide System Libraries:表示隐藏系统的函数,调用这个就更有用了,勾选后耗时调用路径只会显示app耗时的代码,性能分析普遍我们都比较关系自己代码的耗时而不是系统的.基本是必选项.注意有些代码耗时也会纳入系统层级,可以进行勾选前后前后对执行路径进行比对会非常有用.因为通常你只关心cpu花在自己代码上的时间不是系统上的,隐藏系统库文件。过滤掉各种系统调用,只显示自己的代码调用。隐藏缺失符号。如果 dSYM 文件或其他系统架构缺失,列表中会出现很多奇怪的十六进制的数值,用此选项把这些干扰元素屏蔽掉,让列表回归清爽。

    Show Obj-C Only:只显示oc代码 ,如果你的程序是像OpenGl这样的程序,不要勾选侧向因为他有可能是C++的

    Flatten Recursion:递归函数, 每个堆栈跟踪一个条目,拼合递归。将同一递归函数产生的多条堆栈(因为递归函数会调用自己)合并为一条。

    Top Functions:找到最耗时的函数或方法。 一个函数花费的时间直接在该函数中的总和,以及在函数调用该函数所花费的时间的总时间。因此,如果函数A调用B,那么A的时间报告在A花费的时间加上B.花费的时间,这非常真有用,因为它可以让你每次下到调用堆栈时挑最大的时间数字,归零在你最耗时的方法。

    四、Time Profiler(时间分析器)

    用来检测app中每个方法所用的时间,并且可以排序,并查找出哪些函数占用了大量时间。

    使用Time Profile前有两点需要注意的地方:

    1、一定要使用真机调试

    在开始进行应用程序性能分析的时候,一定要使用真机。因为模拟器运行在Mac上,然而Mac上的CPU往往比iOS设备要快。相反,Mac上的GPU和iOS设备的完全不一样,模拟器不得已要在软件层面(CPU)模拟设备的GPU,这意味着GPU相关的操作在模拟器上运行的更慢,尤其是使用CAEAGLLayer来写一些OpenGL的代码时候,这就导致模拟器性能数据和用户真机使用性能数据相去甚远

    2、应用程序一定要使用发布配置

    在发布环境打包的时候,编译器会引入一系列提高性能的优化,例如去掉调试符号或者移除并重新组织代码。另iOS引入一种"Watch Dog"[看门狗]机制,不同的场景下,“看门狗”会监测应用的性能,如果超出了该场景所规定的运行时间,“看门狗”就会强制终结这个应用的进程。开发者可以crashlog看到对应的日志,但Xcode在调试配置下会禁用"Watch Dog"

    1)界面详情:


    2)详细面板


    主要是看Call Tree和Sample List这两种视图:

    3)调用树


    Running Time:函数运行的时间,这个时间是累积时间

    Self:在栈顶次数

    Symbol Name:被调用函数的符号信息

    4)详情面板更多的信息选项


    5)样本列表


    五、Zombies(僵尸)

    1.概念

    翻译英文:专注于检测过度释放的“僵尸”对象。还提供了数据对象分配的类以及所有活动分配内存地址的历史。

    这里我们可以看到一个词语叫“over-release”,过度释放。我们在项目中见到最多的就是“EXC_BAD_ACCESS”或者是这样的:Thread 1: Program received signal:"EXC_BAD_ACCESS",这就是访问了被释放的内存地址造成的

    过度释放,是对同一个对象释放了过多的次数,其实当引用计数降到0时,对象占用的内存已经被释放掉,此时指向原对象的指针就成了“悬垂指针”,如若再对其进行任何方法的调用,(原则上)都会直接crash(然而由于某些特殊的情况,不会马上crash)。过度释放简单的说就是对release的对象再release,就是过度释放

    我们需要知道这几个概念:

    1、内存泄漏:对象使用完没有释放,导致内存浪费。
    2、僵尸对象:已经被销毁的对象(不能再使用的对象)
    3、野指针:指向僵尸对象(不可用内存)的指针。给野指针发消息会报EXC_BAD_ACCECC错误
    4、空指针:没有指向储存空间的指针(里面存的是nil,也就是0)。在oc中使用空指针调中方法不会报错。

    注意:为了避免野指针错误的常见方法:在对象被销毁之后,将指向对象的指针变为空指针。

    对于过度释放的问题,可以直接使用Zombie,当过度释放发生时会立即停在发生问题的位置,同时结合内存分配释放历史和调用栈,可以发现问题。至于上文提到的不会crash的原因,其实有很多,比如:

    对象内存释放时,所用内存并没有完全被擦除,仍有旧对象部分数据可用
    原内存位置被写入同类或同样结构的数据

    2.原理

    我们将僵尸对象“复活”的目的:僵尸对象就是让已经释放了的对象重新复活,便于调试;是为了让已经释放了的对象在被再次访问时能够输出一些错误信息。其实这里的“复活”并不是真的复活,而是强行不死:这么说吧 相当于 他的RC=0的时候 系统再强行让他RC=1,顺便打上一个标记 zoom,等到你去掉那个沟以后 系统会把带有标记zoom的对象RC=0。

    3.用法

    下边是Instruments里面的Zombies的用法:

    在Launch Configuration中勾选Record reference counts和Enable NSZombie detection。其中Recordreference counts是显示引用计数,Enable NSZombie detection是能够检测僵尸对象。


    这样在程序运行的时候,如果发现僵尸对象它就会弹出一个对话框,点击其中“→”按钮,在屏幕的下方会显示僵尸对象的详细信息,下图可以看到僵尸对象的引用计数变化情况。


    注意:Zombies模版在使用的时候会导致内存的飙升,这是因为所有被释放的对象被僵尸对象取代,并未真的释放掉,在结束Zombies时会释放,这是预知行为,这就意味着instrument里的其它工具和Zombies是不能同时使用的,Zombies会导致其它的数据不准。包括leaks,你也不应该把它加到Zombies模版中,即使这么做了结果也没什么意义。对于iOS应用来说,在用Zombies模版时使用iOS模拟器比真机要好

    另外XCode也提供了手动设置NSZombieEnabled环境变量的方法,不过设置NSZombieEnabled为True后,会导致内存占用的增长,同时会影响Leaks工具的调试,这是因为设置NSZombieEnabled会用僵尸对象来代替已释放对象

    点击Product菜单Edit Scheme打开该页面,然后勾选Enable Zombie Objects复选框:


    最后提醒的是NSZombieEnabled只能在调试的时候使用,千万不要忘记在产品发布的时候去掉,因为NSZombieEnabled不会真正去释放dealloc对象的内存,一直开启的话,该死去的对象会一直存在,后果可想而知,自重!

    六、扩展

    野指针

    C语言: 当我们声明1个指针变量,没有为这个指针变量赋初始值.这个指针变量的值是1个垃圾指 指向1块随机的内存空间。

    OC语言: 指针指向的对象已经被回收掉了.这个指针就叫做野指针.

    僵尸对象

    内存回收的本质.

    申请1块空间,实际上是向系统申请1块别人不再使用的空间.
    释放1块空间,指的是占用的空间不再使用,这个时候系统可以分配给别人去使用.
    在这个个空间分配给别人之前 数据还是存在的.
    OC对象释放以后,表示OC对象占用的空间可以分配给别人.
    但是再分配给别人之前 这个空间仍然存在 对象的数据仍然存在.

    僵尸对象: 1个已经被释放的对象 就叫做僵尸对象.

    使用野指针访问僵尸对象.有的时候会出问题,有的时候不会出问题.

    当野指针指向的僵尸对象所占用的空间还没有分配给别人的时候, - 这个时候其实是可以访问的.
    因为对象的数据还在.
    当野指针指向的对象所占用的空间分配给了别人的时候 这个时候访问就会出问题.
    所以,你不要通过1个野指针去访问1个僵尸对象.
    虽然可以通过野指针去访问已经被释放的对象,但是我们不允许这么做.

    僵尸对象检测.

    默认情况下. Xcode不会去检测指针指向的对象是否为1个僵尸对象. 能访问就访问 不能访问就报错.

    可以开启Xcode的僵尸对象检测.

    那么就会在通过指针访问对象的时候,检测这个对象是否为1个僵尸对象 如果是僵尸对象 就会报错.

    为什么不默认开启僵尸对象检测呢?

    因为一旦开启,每次通过指针访问对象的时候.都会去检查指针指向的对象是否为僵尸对象.
    那么这样的话 就影响效率了.

    如何避免僵尸对象报错.

    当1个指针变为野指针以后. 就把这个指针的值设置为nil

    僵尸对象无法复活.

    当1个对象的引用计数器变为0以后 这个对象就被释放了.
    就无法取操作这个僵尸对象了. 所有对这个对象的操作都是无效的.
    因为一旦对象被回收 对象就是1个僵尸对象 而访问1个僵尸对象 是没有意义.

    摘自:https://blog.csdn.net/weixin_41963895/article/details/107231347

    收起阅读 »

    iOS-事件传递&&响应机制(二)

    如果想让某个view不能处理事件(或者说,事件传递到某个view那里就断了),那么可以通过刚才提到的三种方式。比如,设置其userInteractionEnabled = NO;那么传递下来的事件就会由该view的父控件处理。例如,不想让蓝色的view接收事件...
    继续阅读 »


    如果想让某个view不能处理事件(或者说,事件传递到某个view那里就断了),那么可以通过刚才提到的三种方式。比如,设置其userInteractionEnabled = NO;那么传递下来的事件就会由该view的父控件处理。
    例如,不想让蓝色的view接收事件,那么可以设置蓝色的view的userInteractionEnabled = NO;那么点击黄色的view或者蓝色的view所产生的事件,最终会由橙色的view处理,橙色的view就会成为最合适的view。
    所以,不管视图能不能处理事件,只要点击了视图就都会产生事件,关键在于该事件最终是由谁来处理!也就是说,如果蓝色视图不能处理事件,点击蓝色视图产生的触摸事件不会由被点击的视图(蓝色视图)处理!

    注意:如果设置父控件的透明度或者hidden,会直接影响到子控件的透明度和hidden。如果父控件的透明度为0或者hidden = YES,那么子控件也是不可见的!

    3.3.如何寻找最合适的view

    应用如何找到最合适的控件来处理事件?
    1.首先判断主窗口(keyWindow)自己是否能接受触摸事件

    2.触摸点是否在自己身上

    3.从后往前遍历子控件,重复前面的两个步骤(首先查找数组中最后一个元素)

    4.如果没有符合条件的子控件,那么就认为自己最合适处理

    详述:

    1.主窗口接收到应用程序传递过来的事件后,首先判断自己能否接手触摸事件。如果能,那么在判断触摸点在不在窗口自己身上
    2.如果触摸点也在窗口身上,那么窗口会从后往前遍历自己的子控件(遍历自己的子控件只是为了寻找出来最合适的view)
    3.遍历到每一个子控件后,又会重复上面的两个步骤(传递事件给子控件,1.判断子控件能否接受事件,2.点在不在子控件上)
    4.如此循环遍历子控件,直到找到最合适的view,如果没有更合适的子控件,那么自己就成为最合适的view。
    找到最合适的view后,就会调用该view的touches方法处理具体的事件。所以,只有找到最合适的view,把事件传递给最合适的view后,才会调用touches方法进行接下来的事件处理。找不到最合适的view,就不会调用touches方法进行事件处理。

    注意:之所以会采取从后往前遍历子控件的方式寻找最合适的view只是为了做一些循环优化。因为相比较之下,后添加的view在上面,降低循环次数。

    3.3.1.寻找最合适的view底层剖析

    两个重要的方法:
    hitTest:withEvent:方法
    pointInside方法

    3.3.1.1.hitTest:withEvent:方法

    什么时候调用?

    只要事件一传递给一个控件,这个控件就会调用他自己的hitTest:withEvent:方法

    作用

    寻找并返回最合适的view(能够响应事件的那个最合适的view)

    注 意:不管这个控件能不能处理事件,也不管触摸点在不在这个控件上,事件都会先传递给这个控件,随后再调用hitTest:withEvent:方法

    拦截事件的处理

    1.正因为hitTest:withEvent:方法可以返回最合适的view,所以可以通过重写hitTest:withEvent:方法,返回指定的view作为最合适的view。

    2.不管点击哪里,最合适的view都是hitTest:withEvent:方法中返回的那个view。

    3.通过重写hitTest:withEvent:,就可以拦截事件的传递过程,想让谁处理事件谁就处理事件。

    事件传递给谁,就会调用谁的hitTest:withEvent:方法。
    注 意:如果hitTest:withEvent:方法中返回nil,那么调用该方法的控件本身和其子控件都不是最合适的view,也就是在自己身上没有找到更合适的view。那么最合适的view就是该控件的父控件。

    所以事件的传递顺序是这样的:

     产生触摸事件->UIApplication事件队列->[UIWindow hitTest:withEvent:]->返回更合适的view->[子控件 hitTest:withEvent:]->返回最合适的view

    事件传递给窗口或控件的后,就调用hitTest:withEvent:方法寻找更合适的view。所以是,先传递事件,再根据事件在自己身上找更合适的view。

         不管子控件是不是最合适的view,系统默认都要先把事件传递给子控件,经过子控件调用子控件自己的hitTest:withEvent:方法验证后才知道有没有更合适的view。即便父控件是最合适的view了,子控件的hitTest:withEvent:方法还是会调用,不然怎么知道有没有更合适的!即,如果确定最终父控件是最合适的view,那么该父控件的子控件的hitTest:withEvent:方法也是会被调用的。

    技巧:想让谁成为最合适的view就重写谁自己的父控件的hitTest:withEvent:方法返回指定的子控件,或者重写自己的hitTest:withEvent:方法 return self。但是,建议在父控件的hitTest:withEvent:中返回子控件作为最合适的view!

    原因在于在自己的hitTest:withEvent:方法中返回自己有时候会出现问题。因为会存在这么一种情况:当遍历子控件时,如果触摸点不在子控件A自己身上而是在子控件B身上,还要要求返回子控件A作为最合适的view,采用返回自己的方法可能会导致还没有来得及遍历A自己,就有可能已经遍历了点真正所在的view,也就是B。这就导致了返回的不是自己而是触摸点真正所在的view。所以还是建议在父控件的hitTest:withEvent:中返回子控件作为最合适的view!

    例如:whiteView有redView和greenView两个子控件。redView先添加,greenView后添加。如果要求无论点击那里都要让redView作为最合适的view(把事件交给redView来处理)那么只能在whiteView的hitTest:withEvent:方法中return self.subViews[0];这种情况下在redView的hitTest:withEvent:方法中return self;是不好使的!

    // 这里redView是whiteView的第0个子控件
    #import "redView.h"

    @implementation redView
    - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
    return self;
    }
    - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
    NSLog(@"red-touch");
    }@end
    // 或者
    #import "whiteView.h"

    @implementation whiteView
    - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
    return self.subviews[0];
    }
    - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
    NSLog(@"white-touch");
    }
    @end

    特殊情况:

    谁都不能处理事件,窗口也不能处理。

    重写window的hitTest:withEvent:方法return nil

    只能有窗口处理事件。

    控制器的view的hitTest:withEvent:方法return nil或者window的hitTest:withEvent:方法return self

    return nil的含义:

    hitTest:withEvent:中return nil的意思是调用当前hitTest:withEvent:方法的view不是合适的view,子控件也不是合适的view。如果同级的兄弟控件也没有合适的view,那么最合适的view就是父控件。

    寻找最合适的view底层剖析之hitTest:withEvent:方法底层做法

    /********************************* hitTest:withEvent:方法底层实现********************************/

    #import "WYWindow.h"
    @implementation WYWindow
    // 什么时候调用:只要事件一传递给一个控件,那么这个控件就会调用自己的这个方法
    // 作用:寻找并返回最合适的view
    // UIApplication -> [UIWindow hitTest:withEvent:]寻找最合适的view告诉系统
    // point:当前手指触摸的点
    // point:是方法调用者坐标系上的点
    - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
    // 1.判断下窗口能否接收事件
    if (self.userInteractionEnabled == NO || self.hidden == YES || self.alpha <= 0.01) return nil;
    // 2.判断下点在不在窗口上
    // 不在窗口上
    if ([self pointInside:point withEvent:event] == NO) return nil;
    // 3.从后往前遍历子控件数组
    int count = (int)self.subviews.count;
    for (int i = count - 1; i >= 0; i--) {
    // 获取子控件
    UIView *childView = self.subviews[i];
    // 坐标系的转换,把窗口上的点转换为子控件上的点
    // 把自己控件上的点转换成子控件上的点
    CGPoint childP = [self convertPoint:point toView:childView];
    UIView *fitView = [childView hitTest:childP withEvent:event];
    if (fitView) {
    // 如果能找到最合适的view
    return fitView;
    }
    }
    // 4.没有找到更合适的view,也就是没有比自己更合适的view
    return self;
    }
    // 作用:判断下传入过来的点在不在方法调用者的坐标系上
    // point:是方法调用者坐标系上的点
    //- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
    //{
    // return NO;
    //}
    - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
    NSLog(@"%s",__func__);
    }
    @end

    hit:withEvent:方法底层会调用pointInside:withEvent:方法判断点在不在方法调用者的坐标系上。

    3.3.1.2.pointInside:withEvent:方法

    pointInside:withEvent:方法判断点在不在当前view上(方法调用者的坐标系上)如果返回YES,代表点在方法调用者的坐标系上;返回NO代表点不在方法调用者的坐标系上,那么方法调用者也就不能处理事件。

    3.3.2.练习

    屏幕上现在有一个viewA,viewA有一个subView叫做viewB,要求触摸viewB时,viewB会响应事件,而触摸viewA本身,不会响应该事件。如何实现?

    - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
    UIView *view = [super hitTest:point withEvent:event];
    if (view == self) {
    return nil;
    }
    return view;
    }

    (四)事件的响应

    4.1.触摸事件处理的整体过程

    1>用户点击屏幕后产生的一个触摸事件,经过一系列的传递过程后,会找到最合适的视图控件来处理这个事件

    2>找到最合适的视图控件后,就会调用控件的touches方法来作具体的事件处理touchesBegan…touchesMoved…touchedEnded…3>这些touches方法的默认做法是将事件顺着响应者链条向上传递(也就是touch方法默认不处理事件,只传递事件),将事件交给上一个响应者进行处理

    4.2.响应者链条示意图

    响应者链条:

    在iOS程序中无论是最后面的UIWindow还是最前面的某个按钮,它们的摆放是有前后关系的,一个控件可以放到另一个控件上面或下面,那么用户点击某个控件时是触发上面的控件还是下面的控件呢,这种先后关系构成一个链条就叫“响应者链”。也可以说,响应者链是由多个响应者对象连接起来的链条。在iOS中响应者链的关系可以用下图表示


    响应者对象:能处理事件的对象,也就是继承自UIResponder的对象
    作用:能很清楚的看见每个响应者之间的联系,并且可以让一个事件多个对象处理。

    如何判断上一个响应者

    1> 如果当前这个view是控制器的view,那么控制器就是上一个响应者

    2> 如果当前这个view不是控制器的view,那么父控件就是上一个响应者

    响应者链的事件传递过程:

    1>如果当前view是控制器的view,那么控制器就是上一个响应者,事件就传递给控制器;如果当前view不是控制器的view,那么父视图就是当前view的上一个响应者,事件就传递给它的父视图

    2>在视图层次结构的最顶级视图,如果也不能处理收到的事件或消息,则其将事件或消息传递给window对象进行处理

    3>如果window对象也不处理,则其将事件或消息传递给UIApplication对象

    4>如果UIApplication也不能处理该事件或消息,则将其丢弃

    事件处理的整个流程总结:

    1.触摸屏幕产生触摸事件后,触摸事件会被添加到由UIApplication管理的事件队列中(即,首先接收到事件的是UIApplication)。

    2.UIApplication会从事件队列中取出最前面的事件,把事件传递给应用程序的主窗口(keyWindow)。

    3.主窗口会在视图层次结构中找到一个最合适的视图来处理触摸事件。(至此,第一步已完成)

    4.最合适的view会调用自己的touches方法处理事件

    5.touches默认做法是把事件顺着响应者链条向上抛。

    touches的默认做法:#import "WYView.h"

    @implementation WYView 
    //只要点击控件,就会调用touchBegin,如果没有重写这个方法,自己处理不了触摸事件
    // 上一个响应者可能是父控件
    - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
    // 默认会把事件传递给上一个响应者,上一个响应者是父控件,交给父控件处理
    [super touchesBegan:touches withEvent:event];
    // 注意不是调用父控件的touches方法,而是调用父类的touches方法
    // super是父类 superview是父控件
    }
    @end

    事件的传递与响应:

    1、当一个事件发生后,事件会从父控件传给子控件,也就是说由UIApplication -> UIWindow -> UIView -> initial view,以上就是事件的传递,也就是寻找最合适的view的过程。

    2、接下来是事件的响应。首先看initial view能否处理这个事件,如果不能则会将事件传递给其上级视图(inital view的superView);如果上级视图仍然无法处理则会继续往上传递;一直传递到视图控制器view controller,首先判断视图控制器的根视图view是否能处理此事件;如果不能则接着判断该视图控制器能否处理此事件,如果还是不能则继续向上传 递;(对于第二个图视图控制器本身还在另一个视图控制器中,则继续交给父视图控制器的根视图,如果根视图不能处理则交给父视图控制器处理);一直到 window,如果window还是不能处理此事件则继续交给application处理,如果最后application还是不能处理此事件则将其丢弃

    3、在事件的响应中,如果某个控件实现了touches...方法,则这个事件将由该控件来接受,如果调用了[super touches….];就会将事件顺着响应者链条往上传递,传递给上一个响应者;接着就会调用上一个响应者的touches….方法

    如何做到一个事件多个对象处理:

    因为系统默认做法是把事件上抛给父控件,所以可以通过重写自己的touches方法和父控件的touches方法来达到一个事件多个对象处理的目的。

    - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{ 
    // 1.自己先处理事件...
    NSLog(@"do somthing...");
    // 2.再调用系统的默认做法,再把事件交给上一个响应者处理
    [super touchesBegan:touches withEvent:event];
    }

    事件处理的整个流程总结:

    1.触摸屏幕产生触摸事件后,触摸事件会被添加到由UIApplication管理的事件队列中(即,首先接收到事件的是UIApplication)。

    2.UIApplication会从事件队列中取出最前面的事件,把事件传递给应用程序的主窗口(keyWindow)。

    3.主窗口会在视图层次结构中找到一个最合适的视图来处理触摸事件。(至此,第一步已完成)

    4.最合适的view会调用自己的touches方法处理事件

    5.touches默认做法是把事件顺着响应者链条向上抛。



    收起阅读 »