注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

环信FAQ

环信FAQ

集成常见问题及答案
RTE开发者社区

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

字节跳动面试官:请你实现一个大文件上传和断点续传(上)

前言事实上我在面试的时候确实被问到了这个问题,而且是一道在线 coding 的编程题,当时虽然思路正确,可惜最终也并不算完全答对本文将从零搭建前端和服务端,实现一个大文件上传和断点续传的 demo服务端:nodejs文章有误解的地方,欢迎指出,将在第一时间改正...
继续阅读 »



前言

这段时间面试官都挺忙的,频频出现在博客文章标题,虽然我不是特别想蹭热度,但是实在想不到好的标题了-。-,蹭蹭就蹭蹭 :)

事实上我在面试的时候确实被问到了这个问题,而且是一道在线 coding 的编程题,当时虽然思路正确,可惜最终也并不算完全答对

结束后花了一段时间整理了下思路,那么究竟该如何实现一个大文件上传,以及在上传中如何实现断点续传的功能呢?

本文将从零搭建前端和服务端,实现一个大文件上传和断点续传的 demo

前端:vue element-ui

服务端:nodejs

文章有误解的地方,欢迎指出,将在第一时间改正,有更好的实现方式希望留下你的评论

大文件上传

整体思路

前端

前端大文件上传网上的大部分文章已经给出了解决方案,核心是利用 Blob.prototype.slice 方法,和数组的 slice 方法相似,调用的 slice 方法可以返回原文件的某个切片

这样我们就可以根据预先设置好的切片最大数量将文件切分为一个个切片,然后借助 http 的可并发性,同时上传多个切片,这样从原本传一个大文件,变成了同时传多个小的文件切片,可以大大减少上传时间

另外由于是并发,传输到服务端的顺序可能会发生变化,所以我们还需要给每个切片记录顺序

服务端

服务端需要负责接受这些切片,并在接收到所有切片后合并切片

这里又引伸出两个问题

  1. 何时合并切片,即切片什么时候传输完成

  2. 如何合并切片

第一个问题需要前端进行配合,前端在每个切片中都携带切片最大数量的信息,当服务端接受到这个数量的切片时自动合并,也可以额外发一个请求主动通知服务端进行切片的合并

第二个问题,具体如何合并切片呢?这里可以使用 nodejs 的 读写流(readStream/writeStream),将所有切片的流传输到最终文件的流里

talk is cheap,show me the code,接着我们用代码实现上面的思路

前端部分

前端使用 Vue 作为开发框架,对界面没有太大要求,原生也可以,考虑到美观使用 element-ui 作为 UI 框架

上传控件

首先创建选择文件的控件,监听 change 事件以及上传按钮




复制代码

请求逻辑

考虑到通用性,这里没有用第三方的请求库,而是用原生 XMLHttpRequest 做一层简单的封装来发请求

request({
    url,
    method = "post",
    data,
    headers = {},
    requestList
  }) {
    return new Promise(resolve => {
      const xhr = new XMLHttpRequest();
      xhr.open(method, url);
      Object.keys(headers).forEach(key =>
        xhr.setRequestHeader(key, headers[key])
      );
      xhr.send(data);
      xhr.onload = e => {
        resolve({
          data: e.target.response
        });
      };
    });
  }
复制代码

上传切片

接着实现比较重要的上传功能,上传需要做两件事

  • 对文件进行切片

  • 将切片传输给服务端




复制代码

当点击上传按钮时,调用 createFileChunk 将文件切片,切片数量通过文件大小控制,这里设置 10MB,也就是说 100 MB 的文件会被分成 10 个切片

createFileChunk 内使用 while 循环和 slice 方法将切片放入 fileChunkList 数组中返回

在生成文件切片时,需要给每个切片一个标识作为 hash,这里暂时使用文件名 + 下标,这样后端可以知道当前切片是第几个切片,用于之后的合并切片

随后调用 uploadChunks 上传所有的文件切片,将文件切片,切片 hash,以及文件名放入 FormData 中,再调用上一步的 request 函数返回一个 proimise,最后调用 Promise.all 并发上传所有的切片

发送合并请求

这里使用整体思路中提到的第二种合并切片的方式,即前端主动通知服务端进行合并,所以前端还需要额外发请求,服务端接受到这个请求时主动合并切片




复制代码

服务端部分

简单使用 http 模块搭建服务端

const http = require("http");
const server = http.createServer();

server.on("request", async (req, res) => {
res.setHeader("Access-Control-Allow-Origin", "*");
res.setHeader("Access-Control-Allow-Headers", "*");
if (req.method === "OPTIONS") {
  res.status = 200;
  res.end();
  return;
}
});

server.listen(3000, () => console.log("正在监听 3000 端口"));
复制代码

接受切片

使用 multiparty 包处理前端传来的 FormData

在 multiparty.parse 的回调中,files 参数保存了 FormData 中文件,fields 参数保存了 FormData 中非文件的字段

const http = require("http");
const path = require("path");
const fse = require("fs-extra");
const multiparty = require("multiparty");

const server = http.createServer();
+ const UPLOAD_DIR = path.resolve(__dirname, "..", "target"); // 大文件存储目录

server.on("request", async (req, res) => {
res.setHeader("Access-Control-Allow-Origin", "*");
res.setHeader("Access-Control-Allow-Headers", "*");
if (req.method === "OPTIONS") {
  res.status = 200;
  res.end();
  return;
}

+ const multipart = new multiparty.Form();

+ multipart.parse(req, async (err, fields, files) => {
+   if (err) {
+     return;
+   }
+   const [chunk] = files.chunk;
+   const [hash] = fields.hash;
+   const [filename] = fields.filename;
+   const chunkDir = path.resolve(UPLOAD_DIR, filename);

+   // 切片目录不存在,创建切片目录
+   if (!fse.existsSync(chunkDir)) {
+     await fse.mkdirs(chunkDir);
+   }

+     // fs-extra 专用方法,类似 fs.rename 并且跨平台
+     // fs-extra 的 rename 方法 windows 平台会有权限问题
+     // https://github.com/meteor/meteor/issues/7852#issuecomment-255767835
+     await fse.move(chunk.path, `${chunkDir}/${hash}`);
+   res.end("received file chunk");
+ });
});

server.listen(3000, () => console.log("正在监听 3000 端口"));
复制代码

image-20200110215559194

查看 multiparty 处理后的 chunk 对象,path 是存储临时文件的路径,size 是临时文件大小,在 multiparty 文档中提到可以使用 fs.rename(由于我用的是 fs-extra,它的 rename 方法 windows 平台权限问题,所以换成了 fse.move) 移动临时文件,即移动文件切片

在接受文件切片时,需要先创建存储切片的文件夹,由于前端在发送每个切片时额外携带了唯一值 hash,所以以 hash 作为文件名,将切片从临时路径移动切片文件夹中,最后的结果如下

img

合并切片

在接收到前端发送的合并请求后,服务端将文件夹下的所有切片进行合并

const http = require("http");
const path = require("path");
const fse = require("fs-extra");

const server = http.createServer();
const UPLOAD_DIR = path.resolve(__dirname, "..", "target"); // 大文件存储目录

+ const resolvePost = req =>
+   new Promise(resolve => {
+     let chunk = "";
+     req.on("data", data => {
+       chunk += data;
+     });
+     req.on("end", () => {
+       resolve(JSON.parse(chunk));
+     });
+   });

+ const pipeStream = (path, writeStream) =>
+ new Promise(resolve => {
+   const readStream = fse.createReadStream(path);
+   readStream.on("end", () => {
+     fse.unlinkSync(path);
+     resolve();
+   });
+   readStream.pipe(writeStream);
+ });

// 合并切片
+ const mergeFileChunk = async (filePath, filename, size) => {
+ const chunkDir = path.resolve(UPLOAD_DIR, filename);
+ const chunkPaths = await fse.readdir(chunkDir);
+ // 根据切片下标进行排序
+ // 否则直接读取目录的获得的顺序可能会错乱
+ chunkPaths.sort((a, b) => a.split("-")[1] - b.split("-")[1]);
+ await Promise.all(
+   chunkPaths.map((chunkPath, index) =>
+     pipeStream(
+       path.resolve(chunkDir, chunkPath),
+       // 指定位置创建可写流
+       fse.createWriteStream(filePath, {
+         start: index * size,
+         end: (index + 1) * size
+       })
+     )
+   )
+ );
+ fse.rmdirSync(chunkDir); // 合并后删除保存切片的目录
+};

server.on("request", async (req, res) => {
res.setHeader("Access-Control-Allow-Origin", "*");
res.setHeader("Access-Control-Allow-Headers", "*");
if (req.method === "OPTIONS") {
  res.status = 200;
  res.end();
  return;
}

+   if (req.url === "/merge") {
+     const data = await resolvePost(req);
+     const { filename,size } = data;
+     const filePath = path.resolve(UPLOAD_DIR, `${filename}`);
+     await mergeFileChunk(filePath, filename);
+     res.end(
+       JSON.stringify({
+         code: 0,
+         message: "file merged success"
+       })
+     );
+   }

});

server.listen(3000, () => console.log("正在监听 3000 端口"));
复制代码

由于前端在发送合并请求时会携带文件名,服务端根据文件名可以找到上一步创建的切片文件夹

接着使用 fs.createWriteStream 创建一个可写流,可写流文件名就是切片文件夹名 + 后缀名组合而成

随后遍历整个切片文件夹,将切片通过 fs.createReadStream 创建可读流,传输合并到目标文件中

值得注意的是每次可读流都会传输到可写流的指定位置,这是通过 createWriteStream 的第二个参数 start/end 控制的,目的是能够并发合并多个可读流到可写流中,这样即使流的顺序不同也能传输到正确的位置,所以这里还需要让前端在请求的时候多提供一个 size 参数

   async mergeRequest() {
    await this.request({
      url: "http://localhost:3000/merge",
      headers: {
        "content-type": "application/json"
      },
      data: JSON.stringify({
+         size: SIZE,
        filename: this.container.file.name
      })
    });
  },
复制代码

img

其实也可以等上一个切片合并完后再合并下个切片,这样就不需要指定位置,但传输速度会降低,所以使用了并发合并的手段,接着只要保证每次合并完成后删除这个切片,等所有切片都合并完毕后最后删除切片文件夹即可

img

至此一个简单的大文件上传就完成了,接下来我们再此基础上扩展一些额外的功能

显示上传进度条

上传进度分两种,一个是每个切片的上传进度,另一个是整个文件的上传进度,而整个文件的上传进度是基于每个切片上传进度计算而来,所以我们先实现切片的上传进度

切片进度条

XMLHttpRequest 原生支持上传进度的监听,只需要监听 upload.onprogress 即可,我们在原来的 request 基础上传入 onProgress 参数,给 XMLHttpRequest 注册监听事件

 // xhr
  request({
    url,
    method = "post",
    data,
    headers = {},
+     onProgress = e => e,
    requestList
  }) {
    return new Promise(resolve => {
      const xhr = new XMLHttpRequest();
+       xhr.upload.onprogress = onProgress;
      xhr.open(method, url);
      Object.keys(headers).forEach(key =>
        xhr.setRequestHeader(key, headers[key])
      );
      xhr.send(data);
      xhr.onload = e => {
        resolve({
          data: e.target.response
        });
      };
    });
  }
复制代码

由于每个切片都需要触发独立的监听事件,所以还需要一个工厂函数,根据传入的切片返回不同的监听函数

在原先的前端上传逻辑中新增监听函数部分

    // 上传切片,同时过滤已上传的切片
  async uploadChunks(uploadedList = []) {
    const requestList = this.data
+       .map(({ chunk,hash,index }) => {
        const formData = new FormData();
        formData.append("chunk", chunk);
        formData.append("hash", hash);
        formData.append("filename", this.container.file.name);
+         return { formData,index };
      })
+       .map(async ({ formData,index }) =>
        this.request({
          url: "http://localhost:3000",
          data: formData,
+           onProgress: this.createProgressHandler(this.data[index]),
        })
      );
    await Promise.all(requestList);
      // 合并切片
    await this.mergeRequest();
  },
  async handleUpload() {
    if (!this.container.file) return;
    const fileChunkList = this.createFileChunk(this.container.file);
    this.data = fileChunkList.map(({ file },index) => ({
      chunk: file,
+       index,
      hash: this.container.file.name + "-" + index
+       percentage:0
    }));
    await this.uploadChunks();
  }    
+   createProgressHandler(item) {
+     return e => {
+       item.percentage = parseInt(String((e.loaded / e.total) * 100));
+     };
+   }
复制代码

每个切片在上传时都会通过监听函数更新 data 数组对应元素的 percentage 属性,之后把将 data 数组放到视图中展示即可

文件进度条

将每个切片已上传的部分累加,除以整个文件的大小,就能得出当前文件的上传进度,所以这里使用 Vue 计算属性

  computed: {
      uploadPercentage() {
        if (!this.container.file || !this.data.length) return 0;
        const loaded = this.data
          .map(item => item.size * item.percentage)
          .reduce((acc, cur) => acc + cur);
        return parseInt((loaded / this.container.file.size).toFixed(2));
      }
}
复制代码

最终视图如下

img

字节跳动面试官:请你实现一个大文件上传和断点续传(下)

作者:yeyan1996
来源:https://juejin.cn/post/6844904046436843527

收起阅读 »

腾讯三面:40亿个QQ号码如何去重?

大家好,我是道哥。今天,我们来聊一道常见的考题,也出现在腾讯面试的三面环节,非常有意思。具体的题目如下:文件中有40亿个QQ号码,请设计算法对QQ号码去重,相同的QQ号码仅保留一个,内存限制1G. 这个题目的意思应该很清楚了,比较直白。为了便于大家理解,我来画...
继续阅读 »

大家好,我是道哥。

今天,我们来聊一道常见的考题,也出现在腾讯面试的三面环节,非常有意思。具体的题目如下:

文件中有40亿个QQ号码,请设计算法对QQ号码去重,相同的QQ号码仅保留一个,内存限制1G.

这个题目的意思应该很清楚了,比较直白。为了便于大家理解,我来画个动图玩玩,希望大家喜欢。

能否做对这道题目,很大程度上就决定了能否拿下腾讯的offer,有一定的技巧性,一起来看下吧。

在原题中,实际有40亿个QQ号码,为了方便起见,在图解和叙述时,仅以4个QQ为例来说明。

方法一:排序

很自然地,最简单的方式是对所有的QQ号码进行排序,重复的QQ号码必然相邻,保留第一个,去掉后面重复的就行。

原始的QQ号为:

排序后的QQ号为:

去重就简单了:

可是,面试官要问你,去重一定要排序吗?显然,排序的时间复杂度太高了,无法通过腾讯面试。

方法二:hashmap

既然直接排序的时间复杂度太高,那就用hashmap吧,具体思路是把QQ号码记录到hashmap中:

mapFlag[123] = true
mapFlag[567] = true
mapFlag[123] = true
mapFlag[890] = true

由于hashmap的去重性质,可知实际自动变成了:

mapFlag[123] = true
mapFlag[567] = true
mapFlag[890] = true

很显然,只有123,567,890存在,所以这也就是去重后的结果。

可是,面试官又要问你了:实际要存40亿QQ号码,1G的内存够分配这么多空间吗?显然不行,无法通过腾讯面试。

方法三:文件切割

显然,这是海量数据问题。看过很多面经的求职者,自然想到文件切割的方式,避免内存过大。

可是,绞尽脑汁思考,要么使用文件间的归并排序,要么使用桶排序,反正最终是能排序的。

既然排序好了,那就能实现去重了,貌似就万事大吉了。我只能坦白地说,高兴得有点早哦。

接着,面试官又要问你:这么多的文件操作,效率自然不高啊。显然,无法通过腾讯面试。

方法四:bitmap

来看绝招!我们可以对hashmap进行优化,采用bitmap这种数据结构,可以顺利地同时解决时间问题和空间问题。

在很多实际项目中,bitmap经常用到。我看了不少组件的源码,发现很多地方都有bitmap实现,bitmap图解如下:

这是一个unsigned char类型,可以看到,共有8位,取值范围是[0, 255],如上这个unsigned char的值是255,它能标识0~7这些数字都存在。

同理,如下这个unsigned char类型的值是254,它对应的含义是:1~7这些数字存在,而数字0不存在:

由此可见,一个unsigned char类型的数据,可以标识0~7这8个整数的存在与否。以此类推:

  • 一个unsigned int类型数据可以标识0~31这32个整数的存在与否。

  • 两个unsigned int类型数据可以标识0~63这64个整数的存在与否。

显然,可以推导出来:512MB大小足够标识所有QQ号码的存在与否,请注意:QQ号码的理论最大值为2^32 - 1,大概是43亿左右。

接下来的问题就很简单了:用512MB的unsigned int数组来记录文件中QQ号码的存在与否,形成一个bitmap,比如:

bitmapFlag[123] = 1
bitmapFlag[567] = 1
bitmapFlag[123] = 1
bitmapFlag[890] = 1

实际上就是:

bitmapFlag[123] = 1
bitmapFlag[567] = 1
bitmapFlag[890] = 1

然后从小到大遍历所有正整数(4字节),当bitmapFlag值为1时,就表明该数是存在的。 而且,从上面的过程可以看到,自动实现了去重。显然,这种方式 可以通过腾讯的面试 。

扩展练习一

文件中有40亿个互不相同的QQ号码,请设计算法对QQ号码进行排序,内存限制1G.

很显然,直接用bitmap, 标记这40亿个QQ号码的存在性,然后从小到大遍历正整数,当bitmapFlag的值为1时,就输出该值,输出后的正整数序列就是排序后的结果。

请注意,这里必须限制40亿个QQ号码互不相同。通过bitmap记录,客观上就自动完成了排序功能。

扩展练习二

文件中有40亿个互不相同的QQ号码,求这些QQ号码的中位数,内存限制1G.

我知道,一些刷题经验丰富的人,最开始想到的肯定是用堆或者文件切割,这明显是犯了本本主义错误。直接用bitmap排序,当场搞定中位数。

扩展练习三

文件中有40亿个互不相同的QQ号码,求这些QQ号码的top-K,内存限制1G.

我知道,很多人背诵过top-K问题,信心满满,想到用小顶堆或者文件切割,这明显又是犯了本本主义错误。直接用bitmap排序,当场搞定top-K问题。

扩展练习四

文件中有80亿个QQ号码,试判断其中是否存在相同的QQ号码,内存限制1G.

我知道,一些吸取了经验教训的人肯定说,直接bitmap啊。然而,又一次错了。根据容斥原理可知:

因为QQ号码的个数是43亿左右(理论值2^32 - 1),所以80亿个QQ号码必然存在相同的QQ号码。

海量数据的问题,要具体问题具体分析,不要眉毛胡子一把抓。有些人完全不刷题,肯定不行。有些人刷题后不加思考,不会变通,也是不行的。好了,先说这么多。我们也会一步一个脚印,争取每篇文章讲清讲透一件事,也希望大家阅读后有所收获,心情愉快。

作者:爱码有道
来源:https://mp.weixin.qq.com/s/YlLYDzncB6tqbffrg__13w

收起阅读 »

看完这篇文章保你面试稳操胜券——React篇

✨欢迎各位小伙伴:\textcolor{blue}{欢迎各位小伙伴:}欢迎各位小伙伴: ✨ 进大厂收藏这一系列就够了,全方位搜集总结,为大家归纳出这篇面试宝典,面试途中祝你一臂之力!,共分为四个系列 ✨包含Vue40道经典面试题\textcolor{g...
继续阅读 »



✨欢迎各位小伙伴:\textcolor{blue}{欢迎各位小伙伴:}欢迎各位小伙伴:
✨ 进大厂收藏这一系列就够了,全方位搜集总结,为大家归纳出这篇面试宝典,面试途中祝你一臂之力!,共分为四个系列
✨包含Vue40道经典面试题\textcolor{green}{包含Vue40道经典面试题}包含Vue40道经典面试题
✨包含react12道高并发面试题\textcolor{green}{包含react12道高并发面试题}包含react12道高并发面试题
✨包含微信小程序34道必问面试题\textcolor{green}{包含微信小程序34道必问面试题}包含微信小程序34道必问面试题
✨包含javaScript80道扩展面试题\textcolor{green}{包含javaScript80道扩展面试题}包含javaScript80道扩展面试题
✨包含APP10道装逼面试题\textcolor{green}{包含APP10道装逼面试题}包含APP10道装逼面试题
✨包含HTML/CSS30道基础面试题\textcolor{green}{包含HTML/CSS30道基础面试题}包含HTML/CSS30道基础面试题
✨还包含Git、前端优化、ES6、Axios面试题\textcolor{green}{还包含Git、前端优化、ES6、Axios面试题}还包含Git、前端优化、ES6、Axios面试题
✨接下来让我们饱享这顿美味吧。一起来学习吧!!!\textcolor{pink}{接下来让我们饱享这顿美味吧。一起来学习吧!!!}接下来让我们饱享这顿美味吧。一起来学习吧!!!
✨本篇为《看完这篇文章保你面试稳操胜券》第五篇(react、app、git)\textcolor{pink}{本篇为《看完这篇文章保你面试稳操胜券》第五篇(react、app、git)}本篇为《看完这篇文章保你面试稳操胜券》第五篇(react、app、git)

react

React 中 keys 的作用是什么?

Keys是React用于追踪哪些列表中元素被修改、被添加或者被移除的辅助标识 在开发过程中,我们需要保证某个元素的 key 在其同级元素中具有唯一性。 在 React Diff 算法中React 会借助元素的 Key 值来判断该元素是新近创建的还是被移动而来的元素, 从而减少不必要的元素重渲染。此外,React 还需要借助 Key 值来判断元素与本地状态的关联关系, 因此我们绝不可忽视转换函数中 Key 的重要性

传入 setState 函数的第二个参数的作用是什么?

该函数会在 setState 函数调用完成并且组件开始重渲染的时候被调用,我们可以用该函数来监听渲染是否完成

React 中 refs 的作用是什么

Refs 是 React 提供给我们的安全访问 DOM元素或者某个组件实例的句柄 可以为元素添加ref属性然后在回调函数中接受该元素在 DOM 树中的句柄,该值会作为回调函数的第一个参数返回

在生命周期中的哪一步你应该发起 AJAX 请求

我们应当将AJAX 请求放到 componentDidMount 函数中执行,主要原因有下

React 下一代调和算法 Fiber 会通过开始或停止渲染的方式优化应用性能,其会影响到 componentWillMount 的触发次数。对于 componentWillMount 这个生命周期函数的调用次数会变得不确定,React 可能会多次频繁调用 componentWillMount。如果我们将 AJAX 请求放到 componentWillMount 函数中,那么显而易见其会被触发多次,自然也就不是好的选择。 如果我们将AJAX 请求放置在生命周期的其他函数中,我们并不能保证请求仅在组件挂载完毕后才会要求响应。如果我们的数据请求在组件挂载之前就完成,并且调用了setState函数将数据添加到组件状态中,对于未挂载的组件则会报错。而在 componentDidMount 函数中进行 AJAX 请求则能有效避免这个问题

shouldComponentUpdate 的作用

shouldComponentUpdate 允许我们手动地判断是否要进行组件更新,根据组件的应用场景设置函数的合理返回值能够帮我们避免不必要的更新

如何告诉 React 它应该编译生产环境版

通常情况下我们会使用 Webpack 的 DefinePlugin 方法来将 NODE_ENV 变量值设置为 production。 编译版本中 React会忽略 propType 验证以及其他的告警信息,同时还会降低代码库的大小, React 使用了 Uglify 插件来移除生产环境下不必要的注释等信息

概述下 React 中的事件处理逻辑

为了解决跨浏览器兼容性问题,React 会将浏览器原生事件(Browser Native Event)封装为合成事件(SyntheticEvent)传入设置的事件处理器中。 这里的合成事件提供了与原生事件相同的接口,不过它们屏蔽了底层浏览器的细节差异,保证了行为的一致性。 另外有意思的是,React 并没有直接将事件附着到子元素上,而是以单一事件监听器的方式将所有的事件发送到顶层进行处理。 这样 React 在更新 DOM 的时候就不需要考虑如何去处理附着在 DOM 上的事件监听器,最终达到优化性能的目的

createElement 与 cloneElement 的区别是什么

createElement 函数是 JSX 编译之后使用的创建 React Element 的函数,而 cloneElement 则是用于复制某个元素并传入新的 Props

redux中间件

中间件提供第三方插件的模式,自定义拦截 action -> reducer 的过程。变为 action -> middlewares -> reducer。 这种机制可以让我们改变数据流,实现如异步action ,action 过滤,日志输出,异常报告等功能 redux-logger:提供日志输出 redux-thunk:处理异步操作 redux-promise:处理异步操作,actionCreator的返回值是promise

react组件的划分业务组件技术组件?

根据组件的职责通常把组件分为UI组件和容器组件。 UI 组件负责 UI 的呈现,容器组件负责管理数据和逻辑。 两者通过React-Redux 提供connect方法联系起来

react旧版生命周期函数

初始化阶段

getDefaultProps:获取实例的默认属性 getInitialState:获取每个实例的初始化状态 componentWillMount:组件即将被装载、渲染到页面上 render:组件在这里生成虚拟的DOM节点 componentDidMount:组件真正在被装载之后 运行中状态

componentWillReceiveProps:组件将要接收到属性的时候调用 shouldComponentUpdate:组件接受到新属性或者新状态的时候(可以返回false,接收数据后不更新,阻止render调用,后面的函数不会被继续执行了) componentWillUpdate:组件即将更新不能修改属性和状态 render:组件重新描绘 componentDidUpdate:组件已经更新 销毁阶段

componentWillUnmount:组件即将销毁

新版生命周期

在新版本中,React 官方对生命周期有了新的 变动建议:

使用getDerivedStateFromProps替换componentWillMount; 使用getSnapshotBeforeUpdate替换componentWillUpdate; 避免使用componentWillReceiveProps; 其实该变动的原因,正是由于上述提到的 Fiber。首先,从上面我们知道 React 可以分成 reconciliation 与 commit两个阶段,对应的生命周期如下:

reconciliation

componentWillMount componentWillReceiveProps shouldComponentUpdate componentWillUpdate commit

componentDidMount componentDidUpdate componentWillUnmount 在 Fiber 中,reconciliation 阶段进行了任务分割,涉及到 暂停 和 重启,因此可能会导致 reconciliation 中的生命周期函数在一次更新渲染循环中被 多次调用 的情况,产生一些意外错误

Git相关面试题

git代码冲突处理

先将本地修改存储起来 git stash 暂存了本地修改之后,就可以pull了。 git pull 还原暂存的内容 git stash pop stash@{0}

避免重复的合并冲突

正如每个开发人员都知道的那样,修复合并冲突相当繁琐,但重复解决完全相同的冲突(例如,在长时间运行的功能分支中)更让人心烦。解决方案是:

git config --global rerere.enabled true 或者你可以通过手动创建目录在每个项目的基础上启用.git/rr-cache。

使用其他设备从GitHub中导出远程分支项目,无法成功。

其原因在于本地中根本没有其分支。解决命令如下: git fetch -- 获取所有分支的更新 git branch -a -- 查看本地和远程分支列表,remotes开头的均为远程分支 -- 导出其远程分支,并通过-b设定本地分支跟踪远程分支 git checkout remotes/branch_name -b branch_name

APP相关面试题

你平常会看日志吗, 一般会出现哪些异常(Exception)?

这个主要是面试官考察你会不会看日志,是不是看得懂java里面抛出的异常,Exception

一般面试中java Exception(runtimeException )是必会被问到的问题 app崩溃的常见原因应该也是这些了。常见的异常列出四五种,是基本要求。

常见的几种如下:

NullPointerException - 空指针引用异常 ClassCastException - 类型强制转换异常。 IllegalArgumentException - 传递非法参数异常。 ArithmeticException - 算术运算异常 ArrayStoreException - 向数组中存放与声明类型不兼容对象异常 IndexOutOfBoundsException - 下标越界异常 NegativeArraySizeException - 创建一个大小为负数的数组错误异常 NumberFormatException - 数字格式异常 SecurityException - 安全异常 UnsupportedOperationException - 不支持的操作异常

app的日志如何抓取?

app本身的日志,可以用logcat抓取,参考这篇:http://www.cnblogs.com/yoyoketang/…

adb logcat | find “com.sankuai.meituan” >d:\hello.txt

也可以用ddms抓取,手机连上电脑,打开ddms工具,或者在Android Studio开发工具中,打开DDMS

app对于不稳定偶然出现anr和crash时候你是怎么处理的?

app偶然出现anr和crash是比较头疼的问题,由于偶然出现无法复现步骤,这也是一个测试人员必备的技能,需要抓日志。查看日志主要有3个方法:

方法一:app开发保存错误日志到本地 一般app开发在debug版本,出现anr和crash的时候会自动把日志保存到本地实际的sd卡上,去对应的app目录取出来就可以了

方法二:实时抓取 当出现偶然的crash时候,这时候可以把手机拉到你们app开发那,手机连上他的开发代码的环境,有ddms会抓日志,这时候出现crash就会记录下来日志。 尽量重复操作让bug复现就可以了

也可以自己开着logcat,保存日志到电脑本地,参考这篇:http://www.cnblogs.com/yoyoketang/…

adb logcat | find “com.sankuai.meituan” >d:\hello.txt

方法三:第三方sdk统计工具

一般接入了第三方统计sdk,比如友盟统计,在友盟的后台会抓到报错的日志

App出现crash原因有哪些?

为什么App会出现崩溃呢?百度了一下,查到和App崩溃相关的几个因素:内存管理错误,程序逻辑错误,设备兼容,网络因素等,如下: 1.内存管理错误:可能是可用内存过低,app所需的内存超过设备的限制,app跑不起来导致App crash。 或是内存泄露,程序运行的时间越长,所占用的内存越大,最终用尽全部内存,导致整个系统崩溃。 亦或非授权的内存位置的使用也可能会导致App crash。 2.程序逻辑错误:数组越界、堆栈溢出、并发操作、逻辑错误。 e.g. app新添加一个未经测试的新功能,调用了一个已释放的指针,运行的时候就会crash。 3.设备兼容:由于设备多样性,app在不同的设备上可能会有不同的表现。 4.网络因素:可能是网速欠佳,无法达到app所需的快速响应时间,导致app crash。或者是不同网络的切换也可能会影响app的稳定性。

app出现ANR,是什么原因导致的?

那么导致ANR的根本原因是什么呢?简单的总结有以下两点:

1.主线程执行了耗时操作,比如数据库操作或网络编程 2.其他进程(就是其他程序)占用CPU导致本进程得不到CPU时间片,比如其他进程的频繁读写操作可能会导致这个问题。

细分的话,导致ANR的原因有如下几点: 1.耗时的网络访问 2.大量的数据读写 3.数据库操作 4.硬件操作(比如camera) 5.调用thread的join()方法、sleep()方法、wait()方法或者等待线程锁的时候 6.service binder的数量达到上限 7.system server中发生WatchDog ANR 8.service忙导致超时无响应 9.其他线程持有锁,导致主线程等待超时 10.其它线程终止或崩溃导致主线程一直等待。

android和ios测试区别?

App测试中ios和Android有哪些区别呢? 1.Android长按home键呼出应用列表和切换应用,然后右滑则终止应用; 2.多分辨率测试,Android端20多种,ios较少; 3.手机操作系统,Android较多,ios较少且不能降级,只能单向升级;新的ios系统中的资源库不能完全兼容低版本中的ios系统中的应用,低版本ios系统中的应用调用了新的资源库,会直接导致闪退(Crash); 4.操作习惯:Android,Back键是否被重写,测试点击Back键后的反馈是否正确;应用数据从内存移动到SD卡后能否正常运行等; 5.push测试:Android:点击home键,程序后台运行时,此时接收到push,点击后唤醒应用,此时是否可以正确跳转;ios,点击home键关闭程序和屏幕锁屏的情况(红点的显示); 6.安装卸载测试:Android的下载和安装的平台和工具和渠道比较多,ios主要有app store,iTunes和testflight下载; 7.升级测试:可以被升级的必要条件:新旧版本具有相同的签名;新旧版本具有相同的包名;有一个标示符区分新旧版本(如版本号), 对于Android若有内置的应用需检查升级之后内置文件是否匹配(如内置的输入法)

另外:对于测试还需要注意一下几点: 1.并发(中断)测试:闹铃弹出框提示,另一个应用的启动、视频音频的播放,来电、用户正在输入等,语音、录音等的播放时强制其他正在播放的要暂停; 2.数据来源的测试:输入,选择、复制、语音输入,安装不同输入法输入等; 3.push(推送)测试:在开关机、待机状态下执行推送,消息先死及其推送跳转的正确性; 应用在开发、未打开状态、应用启动且在后台运行的情况下是push显示和跳转否正确; 推送消息阅读前后数字的变化是否正确; 多条推送的合集的显示和跳转是否正确;

4.分享跳转:分享后的文案是否正确;分享后跳转是否正确,显示的消息来源是否正确;

5.触屏测试:同时触摸不同的位置或者同时进行不同操作,查看客户端的处理情况,是否会crash等

app测试和web测试有什么区别?

WEB测试和App测试从流程上来说,没有区别。 都需要经历测试计划方案,用例设计,测试执行,缺陷管理,测试报告等相关活动。 从技术上来说,WEB测试和APP测试其测试类型也基本相似,都需要进行功能测试、性能测试、安全性测试、GUI测试等测试类型。

他们的主要区别在于具体测试的细节和方法有区别,比如:性能测试,在WEB测试只需要测试响应时间这个要素,在App测试中还需要考虑流量测试和耗电量测试。

兼容性测试:在WEB端是兼容浏览器,在App端兼容的是手机设备。而且相对应的兼容性测试工具也不相同,WEB因为是测试兼容浏览器,所以需要使用不同的浏览器进行兼容性测试(常见的是兼容IE6,IE8,chrome,firefox)如果是手机端,那么就需要兼容不同品牌,不同分辨率,不同android版本甚至不同操作系统的兼容。(常见的兼容方式是兼容市场占用率前N位的手机即可),有时候也可以使用到兼容性测试工具,但WEB兼容性工具多用IETester等工具,而App兼容性测试会使用Testin这样的商业工具也可以做测试。

安装测试:WEB测试基本上没有客户端层面的安装测试,但是App测试是存在客户端层面的安装测试,那么就具备相关的测试点。

还有,App测试基于手机设备,还有一些手机设备的专项测试。如交叉事件测试,操作类型测试,网络测试(弱网测试,网络切换)

交叉事件测试:就是在操作某个软件的时候,来电话、来短信,电量不足提示等外部事件。

操作类型测试:如横屏测试,手势测试

网络测试:包含弱网和网络切换测试。需要测试弱网所造成的用户体验,重点要考虑回退和刷新是否会造成二次提交。弱网络的模拟,据说可以用360wifi实现设置。

从系统架构的层面,WEB测试只要更新了服务器端,客户端就会同步会更新。而且客户端是可以保证每一个用户的客户端完全一致的。但是APP端是不能够保证完全一致的,除非用户更新客户端。如果是APP下修改了服务器端,意味着客户端用户所使用的核心版本都需要进行回归测试一遍。

还有升级测试:升级测试的提醒机制,升级取消是否会影响原有功能的使用,升级后用户数据是否被清除了。

Android四大组件

Android四大基本组件:Activity、BroadcastReceiver广播接收器、ContentProvider内容提供者、Service服务。

Activity:

应用程序中,一个Activity就相当于手机屏幕,它是一种可以包含用户界面的组件,主要用于和用户进行交互。一个应用程序可以包含许多活动,比如事件的点击,一般都会触发一个新的Activity。

BroadcastReceiver广播接收器:

应用可以使用它对外部事件进行过滤只对感兴趣的外部事件(如当电话呼入时,或者数据网络可用时)进行接收并做出响应。广播接收器没有用户界面。然而,它们可以启动一个activity或serice 来响应它们收到的信息,或者用NotificationManager来通知用户。通知可以用很多种方式来吸引用户的注意力──闪动背灯、震动、播放声音等。一般来说是在状态栏上放一个持久的图标,用户可以打开它并获取消息。

ContentProvider内容提供者:

内容提供者主要用于在不同应用程序之间实现数据共享的功能,它提供了一套完整的机制,允许一个程序访问另一个程序中的数据,同时还能保证被访问数据的安全性。只有需要在多个应用程序间共享数据时才需要内容提供者。例如:通讯录数据被多个应用程序使用,且必须存储在一个内容提供者中。它的好处:统一数据访问方式。

Service服务:

是Android中实现程序后台运行的解决方案,它非常适合去执行那些不需要和用户交互而且还要长期运行的任务(一边打电话,后台挂着QQ)。服务的运行不依赖于任何用户界面,即使程序被切换到后台,或者用户打开了另一个应用程序,服务扔然能够保持正常运行,不过服务并不是运行在一个独立的进程当中,而是依赖于创建服务时所在的应用程序进程。当某个应用程序进程被杀掉后,所有依赖于该进程的服务也会停止运行(正在听音乐,然后把音乐程序退出)。

Activity生命周期?

周期即活动从开始到结束所经历的各种状态。生命周期即活动从开始到结束所经历的各个状态。从一个状态到另一个状态的转变,从无到有再到无,这样一个过程中所经历的状态就叫做生命周期。

Activity本质上有四种状态:

1.运行(Active/Running):Activity处于活动状态,此时Activity处于栈顶,是可见状态,可以与用户进行交互

2.暂停(Paused):当Activity失去焦点时,或被一个新的非全面屏的Activity,或被一个透明的Activity放置在栈顶时,Activity就转化为Paused状态。此刻并不会被销毁,只是失去了与用户交互的能力,其所有的状态信息及其成员变量都还在,只有在系统内存紧张的情况下,才有可能被系统回收掉

3.停止(Stopped):当Activity被系统完全覆盖时,被覆盖的Activity就会进入Stopped状态,此时已不在可见,但是资源还是没有被收回

4.系统回收(Killed):当Activity被系统回收掉,Activity就处于Killed状态

如果一个活动在处于停止或者暂停的状态下,系统内存缺乏时会将其结束(finish)或者杀死(kill)。这种非正常情况下,系统在杀死或者结束之前会调用onSaveInstance()方法来保存信息,同时,当Activity被移动到前台时,重新启动该Activity并调用onRestoreInstance()方法加载保留的信息,以保持原有的状态。

在上面的四中常有的状态之间,还有着其他的生命周期来作为不同状态之间的过度,用于在不同的状态之间进行转换,生命周期的具体说明见下。

什么是activity

什么是activity,这个前两年出去面试APP测试岗位,估计问的最多了,特别是一些大厂,先问你是不是做过APP测试,那好,你说说什么是activity? 如果没看过android的开发原理,估计这个很难回答,要是第一个问题就被难住了,面试的信心也会失去一半了,士气大减。

Activity是Android的四大组件之一,也是平时我们用到最多的一个组件,可以用来显示View。 官方的说法是Activity一个应用程序的组件,它提供一个屏幕来与用户交互,以便做一些诸如打电话、发邮件和看地图之类的事情,原话如下: An Activity is an application component that provides a screen with which users can interact in order to do something, such as dial the phone, take a photo, send an email, or view a map.

Activity是一个Android的应用组件,它提供屏幕进行交互。每个Activity都会获得一个用于绘制其用户界面的窗口,窗口可以充满哦屏幕也可以小于屏幕并浮动在其他窗口之上。 一个应用通常是由多个彼此松散联系的Activity组成,一般会指定应用中的某个Activity为主活动,也就是说首次启动应用时给用户呈现的Activity。将Activity设为主活动的方法 当然Activity之间可以进行互相跳转,以便执行不同的操作。每当新Activity启动时,旧的Activity便会停止,但是系统会在堆栈也就是返回栈中保留该Activity。 当新Activity启动时,系统也会将其推送到返回栈上,并取得用在这里插入图片描述 户的操作焦点。当用户完成当前Activity并按返回按钮是,系统就会从堆栈将其弹出销毁,然后回复前一Activity 当一个Activity因某个新Activity启动而停止时,系统会通过该Activity的生命周期回调方法通知其这一状态的变化。 Activity因状态变化每个变化可能有若干种,每一种回调都会提供执行与该状态相应的特定操作的机会

语音通话功能

WebRTC实时通讯的核心 WebRTC 建立连接步骤 1.为连接的两端创建一个 RTCPeerConnection 对象,并且给 RTCPeerConnection 对象添加本地流。

2.获取本地媒体描述信息(SDP),并与对端进行交换。

3.获取网络信息(Candidate,IP 地址和端口),并与远端进行交换。

装逼神器

一般通过面试的短短一个小时时间,面试官需要对你的技术底子进行磨盘,如果你看完下面这些材料,相信你一定能够让他心里直呼牛逼(下面所有链接文章均是小编自己总结的)

关于scoped样式穿透问题

blog.csdn.net/JHXL_/artic…

Vue2和Vue3的区别

blog.csdn.net/JHXL_/artic…

项目中的登录流程

blog.csdn.net/JHXL_/artic…

构造函数、原型、继承

blog.csdn.net/JHXL_/artic…

项目中遇到的难点

写在最后

✨原创不易,还希望各位大佬支持一下\textcolor{blue}{原创不易,还希望各位大佬支持一下}原创不易,还希望各位大佬支持一下
👍 点赞,你的认可是我创作的动力!\textcolor{green}{点赞,你的认可是我创作的动力!}点赞,你的认可是我创作的动力!
⭐️ 收藏,你的青睐是我努力的方向!\textcolor{green}{收藏,你的青睐是我努力的方向!}收藏,你的青睐是我努力的方向!
✏️ 评论,你的意见是我进步的财富!\textcolor{green}{评论,你的意见是我进步的财富!}评论,你的意见是我进步的财富!

作者:几何心凉
来源:https://juejin.cn/post/7039640038509903909

收起阅读 »

撸一个 webpack 插件,希望对大家有所帮助

最近,陆陆续续搞 了一个 UniUsingComponentsWebpackPlugin 插件(下面介绍),这是自己第三个开源项目,希望大家一起来维护,一起 star 呀,其它两个:vue-okr-tree基于 Vue 2的组织架构树组件地址:github....
继续阅读 »

最近,陆陆续续搞 了一个 UniUsingComponentsWebpackPlugin 插件(下面介绍),这是自己第三个开源项目,希望大家一起来维护,一起 star 呀,其它两个:

  • vue-okr-tree

    基于 Vue 2的组织架构树组件

    地址:github.com/qq449245884…

  • ztjy-cli

    团队的一个简易模板初始化脚手架

    地址:github.com/qq449245884…

  • UniUsingComponentsWebpackPlugin

    地址:github.com/qq449245884…

    配合UniApp,用于集成小程序原生组件

    • 配置第三方库后可以自动引入其下的原生组件,而无需手动配置

    • 生产构建时可以自动剔除没有使用到的原生组件

背景

第一个痛点

用 uniapp开发小程序的小伙伴应该知道,我们在 uniapp 中要使用第三方 UI 库(vant-weappiView-weapp)的时候 ,想要在全局中使用,需要在 src/pages.json 中的 usingComponents 添加对应的组件声明,如:

// src/pages.json
"usingComponents": {
   "van-button": "/wxcomponents/@vant/weapp/button/index",
}

但在开发过程中,我们不太清楚需要哪些组件,所以我们可能会全部声明一遍(PS:这在做公共库的时候更常见),所以我们得一个个的写,做为程序员,我们绝不允许使用这种笨方法。这是第一个痛点

第二个痛点

使用第三方组件,除了在 src/pages.json 还需要在对应的生产目录下建立 wxcomponents,并将第三方的库拷贝至该文件下,这个是 uniapp 自定义的,详细就见:uniapp.dcloud.io/frame?id=%e…

这是第二个痛点

第三个痛点

第二痛点,我们将整个UI库拷贝至 wxcomponents,但最终发布的时候,我们不太可能全都用到了里面的全局组件,所以就将不必要的组件也发布上去,增加代码的体积。

有的小伙伴就会想到,那你将第三方的库拷贝至 wxcomponents时候,可以只拷使用到的就行啦。是这理没错,但组件里面可能还会使用到其它组件,我们还得一个个去看,然后一个个引入,这又回到了第一个痛点了

有了这三个痛点,必须得有个插件来做这些傻事,处理这三个痛点。于是就有 UniUsingComponentsWebpackPlugin 插件,这个webpack 插件主要解决下面几个问题:

  • 配置第三方库后可以自动引入其下的原生组件,而无需手动配置

  • 生产构建时可以自动剔除没有使用到的原生组件

webpack 插件

webpack 的插件体系是一种基于 Tapable 实现的强耦合架构,它在特定时机触发钩子时会附带上足够的上下文信息,插件定义的钩子回调中,能也只能与这些上下文背后的数据结构、接口交互产生 side effect,进而影响到编译状态和后续流程。

从形态上看,插件通常是一个带有 apply函数的类:

class SomePlugin {
   apply(compiler) {
  }
}

Webpack 会在启动后按照注册的顺序逐次调用插件对象的 apply 函数,同时传入编译器对象 compiler ,插件开发者可以以此为起点触达到 webpack 内部定义的任意钩子,例如:

class SomePlugin {
   apply(compiler) {
       compiler.hooks.thisCompilation.tap('SomePlugin', (compilation) => {
      })
  }
}

注意观察核心语句 compiler.hooks.thisCompilation.tap,其中 thisCompilation 为 tapable 仓库提供的钩子对象;tap 为订阅函数,用于注册回调。

Webpack 的插件体系基于tapable 提供的各类钩子展开,所以有必要先熟悉一下 tapable 提供的钩子类型及各自的特点。

到这里,就不做继续介绍了,关于插件的更多 详情可以去官网了解。

这里推荐 Tecvan 大佬写的 《Webpack 插件架构深度讲解》mp.weixin.qq.com/s/tXkGx6Ckt…

实现思路

UniUsingComponentsWebpackPlugin 插件主要用到了三个 compiler 钩子。

第一个钩子是 environment:

compiler.hooks.environment.tap(
    'UniUsingComponentsWebpackPlugin',
    async () => {
      // todo someing
    }
  );

这个钩子主要用来自动引入其下的原生组件,这样就无需手动配置。解决第一个痛点

第二个钩子 thisCompilation,这个钩子可以获得 compilation,能对最终打包的产物进行操作:

compiler.hooks.thisCompilation.tap(
    'UniUsingComponentsWebpackPlugin',
    (compilation) => {
      // 添加资源 hooks
      compilation.hooks.additionalAssets.tapAsync(
        'UniUsingComponentsWebpackPlugin',
        async (cb) => {
          await this.copyUsingComponents(compiler, compilation);
          cb();
        }
      );
    }
  );

所以这个勾子用来将 node_modules 下的第三库拷贝到我们生产 dist 目录里面的 wxcomponents解决第二个痛点

ps:这里也可直接用现有的 copy-webpack-plugin 插件来实现。

第三个钩子 done,表示 compilation 执行完成:

    if (process.env.NODE_ENV === 'production') {
    compiler.hooks.done.tapAsync(
      'UniUsingComponentsWebpackPlugin',
      (stats, callback) => {
        this.deleteNoUseComponents();
        callback();
      }
    );
  }

执行完成后,表示我们已经生成 dist 目录了,可以读取文件内容,分析,获取哪些组件被使用了,然后删除没有使用到组件对应的文件。这样就可以解决我们第三个痛点了

PS:这里我判断只有在生产环境下才会 剔除,开发环境没有,也没太必要。

使用

安装

npm install uni-using-components-webpack-plugin --save-dev

然后将插件添加到 WebPack Config 中。例如:

const UniUsingComponentsWebpackPlugin = require("uni-using-components-webpack-plugin");

module.exports = {
 plugins: [
new UniUsingComponentsWebpackPlugin({
  patterns: [
  {
  prefix: 'van',
  module: '@vant/weapp',
  },
  {
  prefix: 'i',
  module: 'iview-weapp',
  },
  ],
  })
],
};

注意:uni-using-components-webpack-plugin 只适用在 UniApp 开发的小程序。

参数

NameTypeDescription
patterns{Array}为插件指定相关

Patterns

moduleprefix
模块名组件前缀

module 是指 package.json 里面的 name,如使用是 Vant 对应的 module@vant/weapp,如果使用是 iview,刚对应的 moduleiview-weapp,具体可看它们各自的 package.json

prefix 是指组件的前缀,如 Vant 使用是 van 开头的前缀,iview 使用是 i 开头的前缀,具体可看它们各自的官方文档。

PS: 这里得吐曹一下 vant,叫别人使用 van 的前缀,然后自己组件里面声明子组件时,却没有使用 van 前缀,如 picker 组件,它里面的 JSON 文件是这么写的:

{
"component": true,
"usingComponents": {
"picker-column": "../picker-column/index",
"loading": "../loading/index"
}
}

picker-columnloading 都没有带 van 前缀,因为这个问题,在做 自动剔除 功能中,我是根据 前缀来判断使用哪些组件的,由于这里的 loadingpicker-column 没有加前缀,所以就被会删除,导致最终的 picker 用不了。为了解决这个问题,增加了不少工作量。

希望 Vant 官方后面的版本能优化一下。

总结

本文通用自定义 Webpack 插件来实现日常一些技术优化需求。主要为大家介绍了 Webpack 插件的基本组成和简单架构,通过三个痛点,引出了 uni-using-components-webpack-plugin 插件,并介绍了使用方式,实现思路。

最后,关于 Webpack 插件开发,还有更多知识可以学习,建议多看看官方文档《Writing a Plugin》进行学习。

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

作者:前端小智
来源:https://juejin.cn/post/7039855875967696904

收起阅读 »

膜拜!用最少的代码却实现了最牛逼的滚动动画!

今天老鱼带领大家学习如何使用最少的代码创建令人叹为观止的滚动动画~ 在聊ScrollTrigger插件之前我们先简单了解下GSAP。 GreenSock 动画平台 (GSAP) 可为 JavaScript 可以操作的任何内容(CSS 属性、SVG、Reac...
继续阅读 »

今天老鱼带领大家学习如何使用最少的代码创建令人叹为观止的滚动动画~



在聊ScrollTrigger插件之前我们先简单了解下GSAP



GreenSock 动画平台 (GSAP) 可为 JavaScript 可以操作的任何内容(CSS 属性、SVG、React、画布、通用对象等)动画化,并解决不同浏览器上存在的兼容问题,而且比 jQuery快 20 倍。大约1000万个网站和许多主要品牌都在使用GSAP。



接下来老鱼带领大家一起学习ScrollTrigger插件的使用。


插件简介


ScrollTrigger是基于GSAP实现的一款高性能页面滚动触发HTML元素动画的插件。


通过ScrollTrigger使用最少的代码创建令人叹为观止的滚动动画。我们需要知道ScrollTrigger是基于GSAP实现的插件,ScrollTrigger是处理滚动事件的,而真正处理动画是GSAP,二者组合使用才能实现滚动动画~


插件特点



  • 将任何动画链接到特定元素,以便它仅在视图中显示该元素时才执行该动画。

  • 可以在进入/离开定义的区域或将其直接链接到滚动栏时在动画上执行操作(播放、暂停、恢复、重新启动、反转、完成、重置)。

  • 延迟动画和滚动条之间的同步。

  • 根据速度捕捉动画中的进度值。

  • 嵌入滚动直接触发到任何 GSAP 动画(包括时间线)或创建独立实例,并利用丰富的回调系统做任何您想做的事。

  • 高级固定功能可以在某些滚动位置之间锁定一个元素。

  • 灵活定义滚动位置。

  • 支持垂直或水平滚动。

  • 丰富的回调系统。

  • 当窗口调整大小时,自动重新计算位置。

  • 在开发过程中启用视觉标记,以准确查看开始/结束/触发点的位置。

  • 在滚动记录器处于活动状态时,如将active类添加到触发元素中:toggleClass: "active"

  • 使用 matchMedia() 标准媒体查询为各种屏幕尺寸创建不同的设置。

  • 自定义滚动触发器容器,可以定义一个 div 而不一定是浏览器视口。

  • 高度优化以实现最大性能。

  • 插件大约只有6.5kb大小。


安装/引用


CDN


<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.8.0/gsap.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.8.0/ScrollTrigger.min.js"></script>

ES Modules


import { gsap } from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";

gsap.registerPlugin(ScrollTrigger);

UMD/CommonJS


import { gsap } from "gsap/dist/gsap";
import { ScrollTrigger } from "gsap/dist/ScrollTrigger";

gsap.registerPlugin(ScrollTrigger);


简单示例


gsap.to(".box", {
scrollTrigger: ".box", // start the animation when ".box" enters the viewport (once)
x: 500
});

高级示例


let tl = gsap.timeline({
  // 添加到整个时间线
  scrollTrigger: {
    trigger: ".container",
    pin: true,   // 在执行时固定触发器元素
    start: "top top", // 当触发器的顶部碰到视口的顶部时
    end: "+=500", // 在滚动 500 px后结束
    scrub: 1, // 触发器1秒后跟上滚动条
    snap: {
      snapTo: "labels", // 捕捉时间线中最近的标签
      duration: {min: 0.2, max: 3}, // 捕捉动画应至少为 0.2 秒,但不超过 3 秒(由速度决定)
      delay: 0.2, // 从上次滚动事件开始等待 0.2 秒,然后再进行捕捉
      ease: "power1.inOut" // 捕捉动画的过度时间(默认为“power3”)
    }
  }
});

// 向时间线添加动画和标签
tl.addLabel("start")
.from(".box p", {scale: 0.3, rotation:45, autoAlpha: 0})
.addLabel("color")
.from(".box", {backgroundColor: "#28a92b"})
.addLabel("spin")
.to(".box", {rotation: 360})
.addLabel("end");

自定义示例


ScrollTrigger.create({
trigger: "#id",
start: "top top",
endTrigger: "#otherID",
end: "bottom 50%+=100px",
onToggle: self => console.log("toggled, isActive:", self.isActive),
onUpdate: self => {
  console.log("progress:", self.progress.toFixed(3), "direction:", self.direction, "velocity", self.getVelocity());
}
});

接下来,我们一起来看使用ScrollTrigger可以实现怎样的效果吧。


利用ScrollTrigger可以实现很多炫酷的效果,还有更多示例及源代码,快去公众号后台回复aaa滚动获取学习吧!也欢迎同学们和老鱼讨论哦~


作者:大前端实验室
链接:https://juejin.cn/post/7038378577028448293

收起阅读 »

领导:小伙子,咱们这个页面出来太慢了!赶紧给我优化一下。

性能优化 这样一个词应该已经是老生常谈了,不仅在面试中面试官会以此和你掰头,而且在工作中领导也会因为网页加载速度慢来敲打你学(打)习(工),那么前端性能优化,如果判断到底需不需要做,如果需要做又怎么去做或者说怎么去找到优化的切入点? 接下来让我们一起来探索前端...
继续阅读 »

性能优化


这样一个词应该已经是老生常谈了,不仅在面试中面试官会以此和你掰头,而且在工作中领导也会因为网页加载速度慢来敲打你学(打)习(工),那么前端性能优化,如果判断到底需不需要做,如果需要做又怎么去做或者说怎么去找到优化的切入点?


接下来让我们一起来探索前端性能优化(emo~


如何量化网站是否需要做性能优化?


首先现在工具碎片化的时代,各种工具满天飞,如何找到一个方便又能直击痛点的工具,是重中之重的首要任务。



下面使用的就是Chrome自带的插件工具进行分析



可以使用chrome自带的lightHouse工具进行分析。得出的分数会列举出三个档次。然后再根据提出不同建议进行优化。


例如:打开掘金的页面,然后点开开发者工具中的Lighthouse插件


1.png


我们可以看到几项指标:



  • First Contentful Paint 首屏加载时间(FCP)

  • Time to interactive 可互动的时间(TTI) 衡量一个页面多长时间才能完全交互

  • Speed Index 内容明显填充的速度(SI) 分数越低越好

  • Total Blocking Time 总阻塞时间(TBT) 主线程运行超过50ms的任务叫做Long Task,Total Blocking Time (TBT) 是 Long Tasks(所有超过 50ms 的任务)阻塞主线程并影响页面可用性的时间量,比如异步任务过长就会导致阻塞主线程渲染,这时就需要处理这部分任务

  • Largest Contentful Paint 最大视觉元素加载的时间(LCP) 对于SEO来说最重要的指标,用户如果打开页面很久都不能看清楚完整页面,那么SEO就会很低。(对于Google来说)

  • Cumulative Layout Shift 累计布局偏移(CLS) 衡量页面点击某些内容位置发生偏移后对页面对影响 eg:当图片宽高不确定时会时该指标更高,还比如异步或者dom动态加载到现有内容上的情况也会造成CLS升高


以上的6个指标就能很好的量化我们网页的性能。得出类似以下结论,并采取措施。



下面的图片是分析自己的项目得出的图表



2.png


3.png



  • 比如打包体积 (webpack优化,tree-sharking和按需加载插件,以及css合并)

  • 图片加载大小优化(使用可压缩图片,搭配上懒加载和预加载)

  • http1.0替换为http2.0后可使用二进制标头和多路复用。(某些图片使用cdn请求时使用了http1.0)

  • 图片没有加上width和heigth(或者说没有父容器限制),当页面重绘重排时容易造成页面排版混乱的情况

  • 避免巨大的网络负载,比如图片的同时请求和减少同时请求的数量

  • 静态资源缓存

  • 减少未使用的 JavaScript 并推迟加载脚本(defer和async)



千遍万遍,不如自己行动一遍。dev your project!然后再对比服用,效果更好哦!



如何做性能优化


Vue-cli已经做了的优化:



  • 使用cache-loader默认为Vue/Babel/TypeScript编译开启,文件会缓存在node_modules/.cache里

  • 图片小于4k的会转为base64储存在js文件中

  • 生产环境会将css提取成单独的文件

  • 提取公共代码

  • 代码压缩

  • 给所有的js文件和css文件加上preload


我们需要做的优化:(下面做出的优化都是根据分析工具得出后,对应自己的项目进行细化而来)

  1. 首先代码层面:

    1. 多图片的页面需要做图片懒加载+预加载+cdn请求以及压缩。后期会推出一篇关于图片优化的文章...
    2. 组件按需加载
    3. 对于迫不得已的dom操作,尽量一次性操作。避免多次操作dom造成页面重绘重排
    4. 公共组件的提取
    5. ajax的请求尽量能够减少多个,如果ajax请求比较慢,但是又必须得请求。那么可以考虑使用 Web Worker
  2. 打包项目。

    1. 使用webpack插件 例如 tree-sharking进行剔除无关的依赖加载。使用terser进行代码压缩,给执行时间长的loader加 cache-loader,可以使得下次打包就会使用 node_modules/.cache 里的
    2. 静态资源使用缓存或者cdn加载,部分动态文件设置缓存过期时间

作者:Tzyito
链接:https://juejin.cn/post/7008422231403397134

收起阅读 »

知道这个,再也不用写一堆el-table-column了

前言 最近在写一个练手项目,接触到了一个问题,就是el-table中的项太多了,我写了一堆el-table-column,导致代码太长了,看起来特别费劲,后来发现了一个让人眼前一亮的方法,瞬间挽救了我的眼睛。 下面就来分享一下! 进入正题 上面就是table...
继续阅读 »

前言


最近在写一个练手项目,接触到了一个问题,就是el-table中的项太多了,我写了一堆el-table-column,导致代码太长了,看起来特别费劲,后来发现了一个让人眼前一亮的方法,瞬间挽救了我的眼睛。


下面就来分享一下!


进入正题


image.png
上面就是table中的全部项,去除第一个复选框,最后一个操作的插槽,一共七项,也就是说el-table-column一共要写9对。这简直不能忍!


image.png



这个图只作举一个例子用,跟上面不产生对应关系。



其中就有5个el-form-item,就这么一大堆。


所以,我当时就想,可不可以用v-for去渲染el-table-column这个标签呢?保留复选框和最后的操作插槽,我们只需要渲染中间的那几项就行。


经过我的实验,确实是可以实现的。



这么写之后就开始质疑之前的我为什么没有这个想法? 要不就能少写一堆💩啦



实现代码如下(标签部分):



            v-for="item in columns"
:key="item.prop"
:prop="item.prop"
:label="item.label"
:formatter="item.formatter"
:width="item.width">



思路是这样,把标签需要显示的定义在一个数组中,遍历数组来达到我们想要的效果,formatter是我们完成提交的数据和页面显示数据的一个转换所用到的。具体写法在下面js部分有写。


定义数组的写法是vue3 composition api的写法,这个思路的话,用Vue2的写法也能实现的,重要的毕竟是思想(啊,我之前还是想不到这种思路)。



再吐槽一下下,这种写法每写一个函数或者变量就要return回去,也挺麻烦的感觉,hhhhh



实现代码如下(JS部分):


const columns = reactive([
{
label:'用户ID',
prop:'userId'
},
{
label:'用户名',
prop:'userName'
},
{
label:'用户邮箱',
prop:'userEmail'
},
{
label:'用户角色',
prop:'role',
formatter(row,column,value){
return {
0:"管理员",
1:"普通用户"
}[value]
}
},
{
label:'用户状态',
prop:'state',
formatter(row,column,value){
return {
1:"在职",
2:"离职",
3:"试用期"
}[value]
}
},
{
label:'注册时间',
prop:'createTime'
},
{
label:'最后登陆时间',
prop:'lastLoginTime'
}
])

作者:Ned
链接:https://juejin.cn/post/7025921628684943396

收起阅读 »

浏览器为什么能唤起App的页面

疑问的开端 大家有没有想过一个问题:在浏览器里打开某个网页,网页上有一个按钮点击可以唤起App。 这样的效果是怎么实现的呢?浏览器是一个app;为什么一个app可以调起其他app的页面? 说到跨app的页面调用,大家是不是能够想到一个机制:Activity的...
继续阅读 »

疑问的开端


大家有没有想过一个问题:在浏览器里打开某个网页,网页上有一个按钮点击可以唤起App。


image.png


这样的效果是怎么实现的呢?浏览器是一个app;为什么一个app可以调起其他app的页面?


说到跨app的页面调用,大家是不是能够想到一个机制:Activity的隐式调用?


一、隐式启动原理


当我们有需要调起其他app的页面时,使用的API就是隐式调用。


比如我们有一个app声明了这样的Activity:


<activity android:name=".OtherActivity"
android:screenOrientation="portrait">
<intent-filter>
<action android:name="mdove"/>
<category android:name="android.intent.category.DEFAULT"/>
</intent-filter>
</activity>

其他App想启动上边这个Activity如下的调用就好:


val intent = Intent()
intent.action = "mdove"
startActivity(intent)

我们没有主动声明Activity的class,那么系统是怎么为我们找到对应的Activity的呢?其实这里和正常的Activity启动流程是一样的,无非是if / else的实现不同而已。


接下来咱们就回顾一下Activity的启动流程,为了避免陷入细节,这里只展开和大家相对“耳熟能详”的类和调用栈,以串流程为主。


1.1、跨进程


首先我们必须明确一点:无论是隐式启动还是显示启动;无论是启动App内Activity还是启动App外的Activity都是跨进程的。比如我们上述的例子,一个App想要启动另一个App的页面。



注意没有root的手机,是看不到系统孵化出来的进程的。也就是我们常见的为什么有些代码打不上断点。



image.png


追过startActivity()的同学,应该很熟悉下边这个调用流程,跟进几个方法之后就发现进到了一个叫做ActivityTread的类里边。



ActivityTread这个类有什么特点?有main函数,就是我们的主线程。



很快我们能看到一个比较常见类的调用:Instrumentation


// Activity.java
public void startActivityForResult(@RequiresPermission Intent intent, int requestCode, @Nullable Bundle options) {
mInstrumentation.execStartActivity(this, mMainThread.getApplicationThread(), mToken, this, intent, requestCode, options);
// 省略
}

注意mInstrumentation#execStartActivity()有一个标黄的入参,它是ActivityThread中的内部类ApplicationThread



ApplicationThread这个类有什么特点,它实现了IApplicationThread.Stub,也就是aidl的“跨进程调用的客户端回调”。



此外mInstrumentation#execStartActivity()中又会看到一个大名鼎鼎的调用:


public ActivityResult execStartActivity(Context who, IBinder contextThread, IBinder token, Activity target, Intent intent, int requestCode, Bundle options) {
// 省略...
ActivityManager.getService()
.startActivity(whoThread, who.getBasePackageName(), intent,
intent.resolveTypeIfNeeded(who.getContentResolver()),
token, target != null ? target.mEmbeddedID : null,
requestCode, 0, null, options);
return null;
}

我们点击去getService()会看到一个标红的IActivityManager的类。



它并不是一个.java文件,而是aidl文件。



所以ActivityManager.``getService``()本质返回的是“进程的服务端”接口实例,也就是:


1.2、ActivityManagerService



public class ActivityManagerService extends IActivityManager.Stub



所以执行到这就转到了系统进程(system_process进程)。省略一下代码细节,看一下调用栈:


image.png


从过上述debug截图,看一看到此时已经拿到了我们的目标Activitiy的相关信息。


这里简化一些获取目标类的源码,直接引入结论:


1.3、PackageManagerService


这里类相当于解析手机内的所有apk,将其信息构造到内存之中,比如下图这样:



image.png



小tips:手机目录中/data/system/packages.xml,可以看到所有apk的path、进程名、权限等信息。



1.4、启动新进程


打开目标Activity的前提是:目标Activity的进程启动了。所以第一次想要打开目标Activity,就意味着要启动进程。


启动进程的代码就在启动Activity的方法中:


resumeTopActivityInnerLocked->startProcessLocked


image.png


这里便引入了另一个另一个大名鼎鼎的类:ZygoteInit。这里简单来说会通过ZygoteInit来进行App进程启动的。


1.5、ApplicationThread


进程启动后,继续回到目标Activity的启动流程。这里依旧是一系列的system_process进行的转来转去,然后IApplicationThread进入目标进程。



注意看,在这里再次通过IApplicationThread回调到ActivityThread


class H extends Handler {
// 省略
public void handleMessage(Message msg) {
switch (msg.what) {
case EXECUTE_TRANSACTION:
final ClientTransaction transaction = (ClientTransaction) msg.obj;
mTransactionExecutor.execute(transaction);
// 省略
break;
case RELAUNCH_ACTIVITY:
handleRelaunchActivityLocally((IBinder) msg.obj);
break;
}
// 省略...
}
}

// 执行Callback
public void execute(ClientTransaction transaction) {
final IBinder token = transaction.getActivityToken();
executeCallbacks(transaction);
}

这里所谓的CallBack的实现是LaunchActivityItem#execute(),对应的实现:


public void execute(ClientTransactionHandler client, IBinder token,
PendingTransactionActions pendingActions) {
ActivityClientRecord r = new ActivityClientRecord(token, mIntent, mIdent, mInfo,
mOverrideConfig, mCompatInfo, mReferrer, mVoiceInteractor, mState, mPersistentState,
mPendingResults, mPendingNewIntents, mIsForward,
mProfilerInfo, client);
client.handleLaunchActivity(r, pendingActions, null);
}

此时就转到了ActivityThread#handleLaunchActivity(),也就转到了咱们日常的生命周期里边,调用栈如下:



上述截图的调用链中暗含了Activity实例化的过程(反射):


public @NonNull Activity instantiateActivity(@NonNull ClassLoader cl, @NonNull String className, @Nullable Intent intent) throws InstantiationException, IllegalAccessException, ClassNotFoundException {

return (Activity) cl.loadClass(className).newInstance();

}
复制代码

二、浏览器启动原理


Helo站内的回流页就是一个标准的,浏览器唤起另一个App的实例。


2.1、交互流程


html标签有一个属性href,比如:<a href="...">


我们常见的一种用法:<a href="``https://www.baidu.com``">。也就是点击之后跳转到百度。


因为这个是前端的标签,依托于浏览器及其内核的实现,跳转到一个网页似乎很“顺其自然”(不然叫什么浏览器)。


当然这里和android交互的流程基本一致:用隐式调用的方式,声明需要启动的Activity;然后<a href="">传入对应的协议(scheme)即可。比如:


前端页面:


<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>
<a href="mdove1://haha"> 启动OtherActivity </a>
</body>

android声明:


<activity
android:name=".OtherActivity"
android:screenOrientation="portrait">
<intent-filter>
<data
android:host="haha"
android:scheme="mdove1" />
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.BROWSABLE" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>

2.2、推理实现


浏览器能够加载scheme,可以理解为是浏览器内核做了封装。那么想要让android也能支持对scheme的解析,难道是由浏览器内核做处理吗?


很明显不可能,做了一套移动端的操作系统,然后让浏览器过来实现,是不是有点杀人诛心。


所以大概率能猜测出来,应该是手机中的浏览器app做的处理。我们就基于这个猜想去看一看浏览器.apk的实现。


2.3、浏览器实现


基于上边说的/data/system/packages.xml文件,我们可以pull出来浏览器的.apk。



然后jadx反编译一下Browser.apk中WebView相关的源码:




我们可以发现对href的处理来自于隐式跳转,所以一切就和上边的流程串了起来。


作者:咸鱼正翻身
链接:https://juejin.cn/post/7033751175551942692

收起阅读 »

实现穿梭栈帧的魔法--协程

1. 协程-穿梭栈帧的魔法 协程的特性是代码中调用一次,实际会执行2次,第一次如果不满足条件先return一个状态,当满足条件的时候线程池会回调该方法执行第二次,而且还具有单例特性(指该函数第一次和第二次执行期间共享同一个上下文),妥妥的操作栈帧的魔法师。 2...
继续阅读 »

1. 协程-穿梭栈帧的魔法


协程的特性是代码中调用一次,实际会执行2次,第一次如果不满足条件先return一个状态,当满足条件的时候线程池会回调该方法执行第二次,而且还具有单例特性(指该函数第一次和第二次执行期间共享同一个上下文),妥妥的操作栈帧的魔法师。


2. 如何实现协程


前提:本文仅探讨kotlin协程实现


其实在反编译suspend函数反编译后就能知道协程的实现原理(以下)


github.com/yujinyan/ko…


//协程代码
//suspend fun foo() :Any{
// delay(3000L)
// val value =getCurrentTime()
// Log.e("TAG", "result is $value")
//}
//等价代码
@suspend fun foo() {
foo(object : Continuation<Any> {
override fun resumeWith(result: Result<Any>) {
val value = result.getOrThrow()
Log.e("TAG", "result is $value")
}
})
}

@suspend fun foo(continuation: Continuation<Any>): Any {
class FooContinuation : Continuation<Any> {
var label: Int = 0

override fun resumeWith(result: Result<Any>) {
val outcome = invokeSuspend()
if (outcome === COROUTINE_SUSPENDED) return
continuation.resume(result.getOrThrow())
}

fun invokeSuspend(): Any {
return foo(this)
}
}

val cont = (continuation as? FooContinuation) ?: FooContinuation()
return when (cont.label) {
0 -> {
cont.label++
//异步延时任务
AppExecutors.newInstance().otherIO.execute {
Thread.sleep(3000L)
val value = getCurrentTime()
cont.resume(value)
}
COROUTINE_SUSPENDED
}
1 -> 1 // return 1
else -> error("shouldn't happen")
}
}

核心就是函数内匿名内部类的巧用,真的很妙



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

构建Java IO框架体系

IO框架 Java IO的学习是一件非常艰巨的任务。 它的挑战是来自于要覆盖所有的可能性。不仅存在各种I/O源端还有想要和他通信的接收端(文件/控制台/网络链接),而且还需要以不同的方式与他们进行通信(顺序/随机存取/缓冲/二进制/字符/行/字 等等)这...
继续阅读 »

IO框架


	Java IO的学习是一件非常艰巨的任务。

它的挑战是来自于要覆盖所有的可能性。不仅存在各种I/O源端还有想要和他通信的接收端(文件/控制台/网络链接),而且还需要以不同的方式与他们进行通信(顺序/随机存取/缓冲/二进制/字符/行/字 等等)这些情况综合起来就给我们带来了大量的学习任务,大量的类需要学习。


我们要学会所有的这些java 的IO是很难的,因为我们没有构建一个关于IO的体系,要构建这个体系又需要深入理解IO库的演进过程,所以,我们如果缺乏历史的眼光,很快我们会对什么时候应该使用IO中的哪些类,以及什么时候不该使用它们而困惑。


所以,在开发者的眼中,IO很乱,很多类,很多方法,很迷茫。


IO简介


数据流是一组有序,有起点和终点的字节的数据序列。包括输入流和输出流。


流序列中的数据既可以是未经加工的原始二进制数据,也可以是经一定编码处理后符合某种格式规定的特定数据。因此Java中的流分为两种: **1) 字节流:**数据流中最小的数据单元是字节 **2) 字符流:**数据流中最小的数据单元是字符, Java中的字符是Unicode编码,一个字符占用两个字节。


Java.io包中最重要的就是5个类和一个接口。5个类指的是File、OutputStream、InputStream、Writer、Reader;一个接口指的是Serializable。掌握了这些就掌握了Java I/O的精髓了。


Java I/O主要包括如下3层次:


  1. 流式部分——最主要的部分。如:OutputStream、InputStream、Writer、Reader等

  2. 非流式部分——如:File类、RandomAccessFile类和FileDescriptor等类

  3. 其他——文件读取部分的与安全相关的类,如:SerializablePermission类,以及与本地操作系统相关的文件系统的类,如:FileSystem类和Win32FileSystem类和WinNTFileSystem类。


[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uFYWs0jZ-1638951173815)(F:\001_优秀课题\29_Java IO\IO图谱.png)]


IO详细介绍


在Android 平台,从应用的角度出发,我们最需要关注和研究的就是 字节流(Stream)字符流(Reader/Writer)和 File/ RandomAccessFile。当我们需要的时候再深入研究也未尝不是一件好事。关于字符和字节,例如文本文件,XML这些都是用字符流来读取和写入。而如RAR,EXE文件,图片等非文本,则用字节流来读取和写入。面对如此复杂的类关系,有一个点是我们必须要首先掌握的,那就是设计模式中的修饰模式,学会并理解修饰模式是搞懂流必备的前提条件哦。


字节流的学习


在具体的学习流之前,我们必须要学的一个设计模式是装饰模式。因为从流的整个发展历史,出现的各种类之间的关系看,都是沿用了修饰模式,都是一个类的功能可以用来修饰其他类,然后组合成为一个比较复杂的流。比如说:


     	DataOutputStream out = new DataOutputStream(
new BufferedOutputStream(
new FileOutputStream(file)));

从上面的代码块中大家不难看出这些类的关系:为了向文件中写入数据,首先需要创建一个FileOutputStream,然后为了提升访问的效率,所以将它发送给具备缓存功能的BufferedOutput-Stream,而为了实现与机器类型无关的java基本类型数据的输出,所以,我们将缓存的流传递给了DataOutputStream。从上面的关系,我们可以看到,其根本目的都是为outputSteam添加额外的功能。而这种额外功能的添加就是采用了装饰模式来构建的代码。因此,学习流,必须要学好装饰模式。


下面的图是一个关于字节流的图谱,这张图谱比较全面的概况了我们字节流中间的各个类以及他们之间的关系。


输入输出流.jpg


字节流的学习过程


为什么要按照一个学习路线来呢?原因是他们的功能决定的。


OutputStream -> FileOutputStream/FilterOutputStream ->DataOutputStream->bufferedOutputStream


相应的学习InputStream方法就好了。


从学习的角度来,我们应该先掌握FilterOutputStream, 以及FileOutputStream,这两个类是基本的类,从继承关系可以不难发现他们都是对 abstract 类 OutputStream的拓展,是它的子类。然而,伴随着 对 Stream流的功能的拓展,所以就出现了 DataOutputStream,(将java中的基础数据类型写入数据字节输出流中、保存在存储介质中、然后可以用DataOutputStream从存储介质中读取到程序中还原成java基础类型)。这里多提一句、DataOutputStream、FilterOutputStream三个类的关系的这种设计既使用了装饰器模式 避免了类的爆炸式增长。


为了提升Stream的执行效率,所以出现了bufferedOutputStream。bufferedOutputStream就是将本地添加了一个缓存的数组。在使用bufferedOutputStream之前每次从磁盘读入数据的时候都是需要访问多少byte数据就向磁盘中读多少个byte的数据,而出现bufferedOutputSteam之后,策略就改了,会先读取整个缓存空间相应大小的数据,这样就是从磁盘读取了一块比较大的数据,然后缓存起来,从而减少了对磁盘的访问的次数以达到提升性能的目的。


另外一方面,我们知道了outputStream(输出流)的发展历史后,我们便可以知道如何使用outpuSteam了,同样的方法,我们可以运用到inputStream中来,这样对称的解释就出现到了inputStream相关的中来了,于是,我们对整个字节流就有了全方位的理解,所以这样子我们就不会感觉到流的复杂了。这个时候对于其他的一些字节流的使用(byteArrayOutputStream/PipeOutputStream/ObjectOutputStream)的学习就自需要在使用的时候看看API即可。


字符流的学习


下图则是一个关于字符流的图谱,这张图谱比较全面的概况了我们字符流中间的各个类以及他们之间的关系。


字符输入输出流.jpg


字符流的学习和字节流的学习是一样的,它和字节流有着同样的发展过程,只是,字节流面向的是我们未知或者即使知道了他们的编码格式也意义不大的文件(png,exe, zip)的时候是采用字节,而面对一些我们知道文件构造我们就能够搞懂它的意义的文件(json,xml)等文件的时候我们还是需要以字符的形式来读取,所以就出现了字符流。reader 和 Stream最大的区别我认为是它包含了一个readline()接口,这个接口标明了,一行数据的意义,这也是可以理解的,因为自有字符才具备行的概念,相反字节流中的行也就是一个字节符号。


字符流的学习历程:


Writer- >FilterWriter->BufferedWriter->OutputStreamWriter->FileWriter->其他


同时类比着学习Reader相关的类。


FilterWriter/FilterReader

字符过滤输出流、与FilterOutputStream功能一样、只是简单重写了父类的方法、目的是为所有装饰类提供标准和基本的方法、要求子类必须实现核心方法、和拥有自己的特色。这里FilterWriter没有子类、可能其意义只是提供一个接口、留着以后的扩展。。。本身是一个抽象类。


BufferedWriter/BufferedReader

BufferedWriter是 Writer类的一个子类。他的功能是为传入的底层字符输出流提供缓存功能、同样当使用底层字符输出流向目的地中写入字符或者字符数组时、每写入一次就要打开一次到目的地的连接、这样频繁的访问不断效率底下、也有可能会对存储介质造成一定的破坏、比如当我们向磁盘中不断的写入字节时、夸张一点、将一个非常大单位是G的字节数据写入到磁盘的指定文件中的、没写入一个字节就要打开一次到这个磁盘的通道、这个结果无疑是恐怖的、而当我们使用BufferedWriter将底层字符输出流、比如FileReader包装一下之后、我们可以在程序中先将要写入到文件中的字符写入到BufferedWriter的内置缓存空间中、然后当达到一定数量时、一次性写入FileReader流中、此时、FileReader就可以打开一次通道、将这个数据块写入到文件中、这样做虽然不可能达到一次访问就将所有数据写入磁盘中的效果、但也大大提高了效率和减少了磁盘的访问量!


OutputStreamWriter/InputStreamReader

输入字符转换流、是输入字节流转向输入字符流的桥梁、用于将输入字节流转换成输入字符流、通过指定的或者默认的编码将从底层读取的字节转换成字符返回到程序中、与OutputStreamWriter一样、本质也是使用其内部的一个类来完成所有工作:StreamDecoder、使用默认或者指定的编码将字节转换成字符;OutputStreamWriter/ InputStreamReader只是对StreamDecoder进行了封装、isr内部所有方法核心都是调用StreamDecoder来完成的、InputStreamReader只是对StreamDecoder进行了封装、使得我们可以直接使用读取方法、而不用关心内部实现。


	OutputStreamWriter、InputStreamReader分别为InputStream、OutputStream的低级输入输出流提供将字节转换成字符的桥梁、他们只是外边的一个门面、真正的核心:

OutputStreamWriter中的StreamEncoder:


         1、使用指定的或者默认的编码集将字符转码为字节        

2、调用StreamEncoder自身实现的写入方法将转码后的字节写入到底层字节输出流中。

InputStreamReader中的StreamDecoder:


        1、使用指定的或者默认的编码集将字节解码为字符         

2、调用StreamDecoder自身实现的读取方法将解码后的字符读取到程序中。

在理解这两个流的时候要注意:java——io中只有将字节转换成字符的类、没有将字符转换成字节的类、原因很简单——字符流的存在本来就像对字节流进行了装饰、加工处理以便更方便的去使用、在使用这两个流的时候要注意:由于这两个流要频繁的对读取或者写入的字节或者字符进行转码、解码和与底层流的源和目的地进行交互、所以使用的时候要使用BufferedWriter、BufferedReader进行包装、以达到最高效率、和保护存储介质。


FileReader/FileWriter

FileReader和FileWriter 继承于InputStreamReader/OutputStreamWriter。


从源码可以发现FileWriter 文件字符输出流、主要用于将字符写入到指定的打开的文件中、其本质是通过传入的文件名、文件、或者文件描述符来创建FileOutputStream、然后使用OutputStreamWriter使用默认编码将FileOutputStream转换成Writer(这个Writer就是FileWriter)。如果使用这个类的话、最好使用BufferedWriter包装一下、高端大气上档次、低调奢华有内涵!


FileReader 文件字符输入流、用于将文件内容以字符形式读取出来、一般用于读取字符形式的文件内容、也可以读取字节形式、但是因为FileReader内部也是通过传入的参数构造InputStreamReader、并且只能使用默认编码、所以我们无法控制编码问题、这样的话就很容易造成乱码。所以读取字节形式的文件还是使用字节流来操作的好、同样在使用此流的时候用BufferedReader包装一下、就算冲着BufferedReader的readLine()方法去的也要使用这个包装类、不说他还能提高效率、保护存储介质。


字节流与字符流的关系


那么字节输入流和字符输入流之间的关系是怎样的呢?请看下图


字节与字符输入流.jpg


同样的字节与字符输出流字节的关系也如下图所示


字节与字符输出流.jpg


字节流与字符流的区别


字节流和字符流使用是非常相似的,那么除了操作代码的不同之外,还有哪些不同呢?


  字节流在操作的时候本身是不会用到缓冲区(内存)的,是与文件本身直接操作的,而字符流在操作的时候是使用到缓冲区的字节流在操作文件时,即使不关闭资源(close方法),文件也能输出,但是如果字符流不使用close方法的话,则不会输出任何内容,说明字符流用的是缓冲区,并且可以使用flush方法强制进行刷新缓冲区,这时才能在不close的情况下输出内容


  那开发中究竟用字节流好还是用字符流好呢?

  在所有的硬盘上保存文件或进行传输的时候都是以字节的方法进行的,包括图片也是按字节完成,而字符是只有在内存中才会形成的,所以使用字节的操作是最多的。


  如果要java程序实现一个拷贝功能,应该选用字节流进行操作(可能拷贝的是图片),并且采用边读边写的方式(节省内存)。


字节流与字符流的转换


虽然Java支持字节流和字符流,但有时需要在字节流和字符流两者之间转换。InputStreamReader和OutputStreamWriter,这两个为类是字节流和字符流之间相互转换的类。


  InputSreamReader用于将一个字节流中的字节解码成字符:

  有两个构造方法: 


   InputStreamReader(InputStream in);

  功能:用默认字符集创建一个InputStreamReader对象


   InputStreamReader(InputStream in,String CharsetName);

  功能:接收已指定字符集名的字符串,并用该字符创建对象


  OutputStream用于将写入的字符编码成字节后写入一个字节流。

  同样有两个构造方法


  OutputStreamWriter(OutputStream out);

  功能:用默认字符集创建一个OutputStreamWriter对象;   


  OutputStreamWriter(OutputStream out,String  CharSetName);

  功能:接收已指定字符集名的字符串,并用该字符集创建OutputStreamWrite对象


为了避免频繁的转换字节流和字符流,对以上两个类进行了封装。


  BufferedWriter类封装了OutputStreamWriter类;


  BufferedReader类封装了InputStreamReader类;


  封装格式


  BufferedWriter out=new BufferedWriter(new OutputStreamWriter(System.out));
BufferedReader in= new BufferedReader(new InputStreamReader(System.in);

  利用下面的语句,可以从控制台读取一行字符串:


  BufferedReader in=new BufferedReader(new InputStreamReader(System.in));
String line=in.readLine();


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

kotlin 与java 互操作

简介 大多数情况下,你不需要关注这个问题。但是,如果你的代码中包含了部分Java代码,了解这些可能帮你解决一些棘手的问题,同时让你设计的Api更加可靠 互操作性与可空性 Java世界里所有对象都可能是null,当一个kotlin函数返回string类型值,你不...
继续阅读 »

简介


大多数情况下,你不需要关注这个问题。但是,如果你的代码中包含了部分Java代码,了解这些可能帮你解决一些棘手的问题,同时让你设计的Api更加可靠


互操作性与可空性


Java世界里所有对象都可能是null,当一个kotlin函数返回string类型值,你不能想当然地认为它的返回值就能符合kotlin关于空值的规定


kotlin


fun main() {
val my = MyClass()
val value = my.getCanNullValue()
println(value?.capitalize())
}

java


public class MyClass {
public String value;

public String getCanNullValue(){
return value;
}
}

类型映射


代码运行时,所有的映射类型都会重新映射回对应的java类型


fun main() {
val my = MyClass()
my.value = "a123"
val value = my.getCanNullValue()
println(value.javaClass)
}

结果为:class java.lang.String


属性访问


不需要调用相关setter方法,你可以使用赋值语法来设置一个java字段值了


val my = MyClass()
my.value = "a123"

@JvmName


这个注解可以改变字节码中生成的类名或方法名称,如果作用在顶级作用域(文件中),则会改变生成对应Java类的名称。如果作用在方法上,则会改变生成对应Java方法的名称。


kotlin


@file:JvmName("FooKt")
@JvmName("foo1")
fun foo() {
println("Hello, Jvm...")
}

java


// 相当于下面的Java代码
public final class FooKt {
public static final void foo1() {
String var0 = "Hello, Jvm...";
System.out.println(var0);
}
}

第一个注解@file:JvmName("FooKt")的作用是使生成的类名变为FooKt,第二个注解的作用是使生成的方法名称变为foo1


@JvmField


Kotlin编译器默认会将类中声明的成员变量编译成私有变量,Java语言要访问该变量必须通过其生成的getter方法。而使用上面的注解可以向Java暴露该变量,即使其访问变为公开(修饰符变为public)。


Kotlin


class JavaToKotlin {
@JvmField
val info = "Hello"
}

@JvmOverloads


由于Kotlin语言支持方法参数默认值,而实现类似功能Java需要使用方法重载来实现,这个注解就是为解决这个问题而生的,添加这个注解会自动生成重载方法


Kotlin


@JvmOverloads
fun prinltInfo(name: String, age: Int = 1) {
println("$name $age")
}

java


 public static void main(String[] args) {
MyKotlin.prinltInfo("arrom");
MyKotlin.prinltInfo("arrom", 20);
}

@JvmStatic


@JvmStatic注解的作用类似于@JvmField,可以直接调用伴生对象里的函数


class JavaToKotlin {
@JvmField
val info = "Hello"

companion object {
@JvmField
val max: Int = 200

@JvmStatic
fun loadConfig(): String {
return "loading config"
}
}
}

@Throws


由于Kotlin语言不支持CE(Checked Exception),所谓CE,即方法可能抛出的异常是已知的。Java语言通过throws关键字在方法上声明CE。为了兼容这种写法,Kotlin语言新增了@Throws注解,该注解的接收一个可变参数,参数类型是多个异常的KClass实例。Kotlin编译器通过读取注解参数,在生成的字节码中自动添加CE声明。


Kotlin


@Throws(IllegalArgumentException::class)
fun div(x: Int, y: Int): Float {
return x.toFloat() / y
}

Java


// 生成的代码相当于下面这段Java代码
public static final float div(int x, int y) throws IllegalArgumentException {
return (float)x / (float)y;
}

添加了@Throws(IllegalArgumentException::class)注解后,在生成的方法签名上自动添加了可能抛出的异常声明(throws IllegalArgumentException),即CE。


@Synchronized


用于产生同步方法。Kotlin语言不支持synchronized关键字,处理类似Java语言的并发问题,Kotlin语言建议使用同步方法进行处理


Kotlin


@Synchronized
fun start() {
println("Start do something...")
}

java


// 生成的代码相当于下面这段Java代码
public static final synchronized void start() {
String var0 = "Start do something...";
System.out.println(var0);
}

函数类型操作


Java中没有函数类型,所以,在Java里,kotlin函数类型使用FunctionN这样的名字的接口来表示,N代表入参的个数,一共有24个这样的接口,从Function0到Function23,每个接口都包含一个invoke函数,调用匿名函数需要调用invoke


kotlin:


val funcp:(String) -> String = {
it.capitalize()
}

java:


Function1 funcp = ArromKt.getFuncp();
funcp.invoke("arrom");

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

LiveData学习记

LiveData 使用 var liveData: MutableLiveData<String>? = null //初始化 liveData = MutableLiveData() // 设置 observe liveData?.observe...
继续阅读 »

LiveData 使用


var liveData: MutableLiveData<String>? = null
//初始化
liveData = MutableLiveData()
// 设置 observe
liveData?.observe(this, {
Log.e("Main2", "2界面接收数据 = $it")
Toast.makeText(this, "2界面接收数据 = $it", Toast.LENGTH_LONG).show()
})
// 发送值
liveData?.value = "2界面发送数据 $indexValue"

LiveData 是针对同一个界面数据相互传递, 配合 MVVM 使用


如果想跨界面使用 比如 Activity1 想传值 给 Activity2 可以把LiveData 下沉(二次封装)


package com.one_hour.test_livedata
import androidx.lifecycle.MutableLiveData
object LiveDataBusBeta{
//创建一个Map 管理 LiveData
private val liveDataMap: MutableMap<String, MutableLiveData<Any>> = HashMap()
// 设置一个 key
fun <T> getLiveData(key: String) : MutableLiveData<T>? {
if (!liveDataMap.containsKey(key)) {
liveDataMap.put(key, MutableLiveData<Any>())
}
return liveDataMap[key] as MutableLiveData<T>
}
fun removeMapLiveData(key : String) {
liveDataMap.remove(key)
}
}

像这样下沉后会出现 Bug, 如场景:当界面Activity1 向未创建的Activity2 发送消息时,会在Activity2 创建时 出现从界面1传过来的数据,这是我们不需要的。(现象出现叫 消息粘性)


什么是粘性事件

即发射的事件如果早于注册,那么注册之后依然可以接收到的事件称为粘性事件


消息粘性 咋个形成的 ?
先创建 new MutableLiveData -> setValue -> observe(绑定监听)


LiveData 绑定(observe)源码

    @MainThread
public void observe(@NonNull LifecycleOwner owner, @NonNull Observer<? super T> observer) {
assertMainThread("observe");
if (owner.getLifecycle().getCurrentState() == DESTROYED) {
// ignore
return;
}
LifecycleBoundObserver wrapper = new LifecycleBoundObserver(owner, observer);
ObserverWrapper existing = mObservers.putIfAbsent(observer, wrapper);
if (existing != null && !existing.isAttachedTo(owner)) {
throw new IllegalArgumentException("Cannot add the same observer"
+ " with different lifecycles");
}
if (existing != null) {
return;
}
owner.getLifecycle().addObserver(wrapper);
}


1.


owner.getLifecycle() 获取的是 Lifecycle 监听Activity 生命周期变化的流程
androidx.appcompat.app.AppCompatActivity (继承)-> androidx.fragment.app.FragmentActivity (继承)-> androidx.activity.ComponentActivity (继承)->androidx.core.app.ComponentActivity( 实现 LifecycleOwner) -> 现在 实例化 private LifecycleRegistry mLifecycleRegistry = new LifecycleRegistry(this);


androidx.core.app.ComponentActivity( 实现 LifecycleOwner)


@CallSuper
@Override
protected void onSaveInstanceState(@NonNull Bundle outState) {
//添加一个 mLifecycleRegistry 状态管理
mLifecycleRegistry.markState(Lifecycle.State.CREATED);
super.onSaveInstanceState(outState);
}

androidx.activity.ComponentActivity( 实现 LifecycleOwner)


    @CallSuper
@Override
protected void onSaveInstanceState(@NonNull Bundle outState) {
Lifecycle lifecycle = getLifecycle();
//设置 lifecycle 当前状态
if (lifecycle instanceof LifecycleRegistry) {
((LifecycleRegistry) lifecycle).setCurrentState(Lifecycle.State.CREATED);
}
super.onSaveInstanceState(outState);
mSavedStateRegistryController.performSave(outState);
}

androidx.fragment.app.FragmentActivity


final LifecycleRegistry mFragmentLifecycleRegistry = new LifecycleRegistry(this);
//开始绑定什么周期 调用 handleLifecycleEvent 绑定状态
mFragmentLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.xxx);

2.


** owner.getLifecycle().addObserver(wrapper); 中 addObserver 调用了 androidx.lifecycle.LifecycleRegistry的 addObserver,而LifecycleRegistry是在FragmentActivity类中实例化获取**


    @Override
public void addObserver(@NonNull LifecycleObserver observer) {
State initialState = mState == DESTROYED ? DESTROYED : INITIALIZED;
ObserverWithState statefulObserver = new ObserverWithState(observer, initialState);
ObserverWithState previous = mObserverMap.putIfAbsent(observer, statefulObserver);

if (previous != null) {
return;
}
LifecycleOwner lifecycleOwner = mLifecycleOwner.get();
if (lifecycleOwner == null) {
// it is null we should be destroyed. Fallback quickly
return;
}

boolean isReentrance = mAddingObserverCounter != 0 || mHandlingEvent;
State targetState = calculateTargetState(observer);
mAddingObserverCounter++;
while ((statefulObserver.mState.compareTo(targetState) < 0
&& mObserverMap.contains(observer))) {
pushParentState(statefulObserver.mState);
statefulObserver.dispatchEvent(lifecycleOwner, upEvent(statefulObserver.mState));
popParentState();
// mState / subling may have been changed recalculate
targetState = calculateTargetState(observer);
}
、、、、、省略代码
}

statefulObserver.dispatchEvent(lifecycleOwner, upEvent(statefulObserver.mState)); 在循环中一直调用


    static class ObserverWithState {
State mState;
LifecycleEventObserver mLifecycleObserver;

ObserverWithState(LifecycleObserver observer, State initialState) {
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;
}
}

LifecycleBoundObserver wrapper = new LifecycleBoundObserver(owner, observer); 监听状态改变 并且
在ObserverWithState 中调用了 mLifecycleObserver.onStateChanged(owner, event); -》mLifecycleObserver 指的就是 LifecycleBoundObserver


class LifecycleBoundObserver extends ObserverWrapper implements GenericLifecycleObserver


        @Override
public void onStateChanged(LifecycleOwner source, Lifecycle.Event event) {
if (mOwner.getLifecycle().getCurrentState() == DESTROYED) {
removeObserver(mObserver);
return;
}
// 这里是如果状态 是可见的 那么就发送消息
// 就调用 class LifecycleBoundObserver extends ObserverWrapper 父类 ObserverWrapper 的方法
//shouldBeActive() 获取 mOwner.getLifecycle().getCurrentState().isAtLeast(STARTED); 状态 是否可见
activeStateChanged(shouldBeActive());
}

class LifecycleBoundObserver extends ObserverWrapper 父类 ObserverWrapper 的方法 并分发 dispatchingValue 值


        void activeStateChanged(boolean newActive) {
if (newActive == mActive) {
return;
}
// immediately set active state, so we'd never dispatch anything to inactive
// owner
mActive = newActive;
boolean wasInactive = LiveData.this.mActiveCount == 0;
LiveData.this.mActiveCount += mActive ? 1 : -1;
if (wasInactive && mActive) {
onActive();
}
if (LiveData.this.mActiveCount == 0 && !mActive) {
onInactive();
}
if (mActive) {
// 调用 dispatchingValue 回到 abstract class LiveData<T> 类里面的 dispatchingValue 方法
dispatchingValue(this);
}
}
}

dispatchingValue 都调用了相同的函数 considerNotify


    void dispatchingValue(@Nullable ObserverWrapper initiator) {
if (mDispatchingValue) {
mDispatchInvalidated = true;
return;
}
mDispatchingValue = true;
do {
mDispatchInvalidated = false;
if (initiator != null) {
considerNotify(initiator);
initiator = null;
} else {
for (Iterator<Map.Entry<Observer<? super T>, ObserverWrapper>> iterator =
mObservers.iteratorWithAdditions(); iterator.hasNext(); ) {
considerNotify(iterator.next().getValue());
if (mDispatchInvalidated) {
break;
}
}
}
} while (mDispatchInvalidated);
mDispatchingValue = false;
}

considerNotify 中 observer.mObserver.onChanged 回调数据


    private void considerNotify(ObserverWrapper observer) {
if (!observer.mActive) {
return;
}
// Check latest state b4 dispatch. Maybe it changed state but we didn't get the event yet.
//
// we still first check observer.active to keep it as the entrance for events. So even if
// the observer moved to an active state, if we've not received that event, we better not
// notify for a more predictable notification order.
if (!observer.shouldBeActive()) {
observer.activeStateChanged(false);
return;
}
if (observer.mLastVersion >= mVersion) {
return;
}
observer.mLastVersion = mVersion;
//noinspection unchecked
observer.mObserver.onChanged((T) mData);
}

解决粘性代码


  • 方法1


import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Observer


class BaseLiveData<T> : MutableLiveData<T>() {
private var isSticky: Boolean = false
private var mStickyData: T? = null
private var mVersion = 0

override fun observe(owner: LifecycleOwner, observer: Observer<in T>) {
if (isSticky) {
super.observe(owner, observer)
} else {
super.observe(owner, CustomObserver<T>(this, observer))
}

}

/**
* 发送非粘性数据
*/
override fun setValue(value: T) {
mVersion++
isSticky = false
super.setValue(value)
}

override fun postValue(value: T) {
mVersion++
isSticky = false
super.postValue(value)
}

/**
* 发送粘性数据
*/
fun setStickyData(data: T?) {
mStickyData = data
isSticky = true
setValue(data!!)
}

fun postStickyData(mStickyData: T?) {
this.mStickyData = mStickyData
isSticky = true
super.postValue(mStickyData!!)
}

inner class CustomObserver<T>(val mLiveData: BaseLiveData<T>, var mObserver: Observer<in T>?,
var isSticky: Boolean = false) : Observer<T> {

private var mLastVersion = mLiveData.mVersion

override fun onChanged(t: T) {
if (mLastVersion >= mLiveData.mVersion) {
if (isSticky && mLiveData.mStickyData != null) {
mObserver?.onChanged(mLiveData.mStickyData)
}
return
}
mLastVersion = mLiveData.mVersion
mObserver?.onChanged(t)

}

}
}


  • 方法2


利用反射 修改 observer.mLastVersion 值
observer.mLastVersion 的 获取值的调用链 :
observer.mLastVersion -》considerNotify (iterator.next().getValue()) -> mObservers (SafeIterableMap<Observer<? super T>, ObserverWrapper> mObservers = new SafeIterableMap<>()) -> ObserverWrapper(int mLastVersion = START_VERSION;) (子类LifecycleBoundObserver, 但是只有父类 ObserverWrapper 才有 mLastVersion, 所以获取父类的 mLastVersion 进行修改)


import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Observer
import java.lang.reflect.Field
import java.lang.reflect.Method


class BaseUnStickyLiveData<T> : MutableLiveData<T>() {

private var isSticky = false

override fun observe(owner: LifecycleOwner, observer: Observer<in T>) {
super.observe(owner, observer)
if (!isSticky) {
hookClass(observer)
}
}

override fun setValue(value: T) {
isSticky = false
super.setValue(value)
}

override fun postValue(value: T) {
isSticky = false
super.postValue(value)
}

fun setStickyValue(value: T) {
isSticky = true
super.setValue(value)
}

fun setStickyPostValue(value: T) {
isSticky = true
super.postValue(value)
}

private fun hookClass(observer: Observer<in T>) {
val liveDataClass = LiveData::class.java
try {
//获取field private SafeIterableMap<Observer<T>, ObserverWrapper> mObservers
val mObservers: Field = liveDataClass.getDeclaredField("mObservers")
mObservers.setAccessible(true)

//获取SafeIterableMap集合mObservers
val observers: Any = mObservers.get(this)

//获取SafeIterableMap的get(Object obj)方法
val observersClass: Class<*> = observers.javaClass
val methodGet: Method = observersClass.getDeclaredMethod("get", Any::class.java)
methodGet.setAccessible(true)

//获取到observer在集合中对应的ObserverWrapper对象
val objectWrapperEntry: Any = methodGet.invoke(observers, observer)
var objectWrapper: Any? = null
if (objectWrapperEntry is Map.Entry<*, *>) {
objectWrapper = objectWrapperEntry.value
}
if (objectWrapper == null) {
//throw NullPointerException("ObserverWrapper can not be null")
return
}

// 获取ListData的mVersion
val mVersion: Field = liveDataClass.getDeclaredField("mVersion")
mVersion.setAccessible(true)
val mVersionValue: Any = mVersion.get(this)

//获取ObserverWrapper的Class对象 LifecycleBoundObserver extends ObserverWrapper
val wrapperClass: Class<*> = objectWrapper.javaClass.superclass

//获取ObserverWrapper的field mLastVersion
val mLastVersion: Field = wrapperClass.getDeclaredField("mLastVersion")
mLastVersion.setAccessible(true)

//把当前ListData的mVersion赋值给 ObserverWrapper的field mLastVersion
mLastVersion.set(objectWrapper, mVersionValue)
} catch (e: Exception) {
if (BuildConfig.DEBUG) {
throw RuntimeException(e)
} else {
e.printStackTrace()
}
}
}
}

配合二次 封装的 LiveDataBusBeta 使用


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

看一遍就理解:动态规划详解

前言 我们刷leetcode的时候,经常会遇到动态规划类型题目。动态规划问题非常非常经典,也很有技巧性,一般大厂都非常喜欢问。今天跟大家一起来学习动态规划的套路,文章如果有不正确的地方,欢迎大家指出哈,感谢感谢~ 什么是动态规划? 动态规划的核心思想 一个例...
继续阅读 »

前言


我们刷leetcode的时候,经常会遇到动态规划类型题目。动态规划问题非常非常经典,也很有技巧性,一般大厂都非常喜欢问。今天跟大家一起来学习动态规划的套路,文章如果有不正确的地方,欢迎大家指出哈,感谢感谢~



  • 什么是动态规划?

  • 动态规划的核心思想

  • 一个例子走进动态规划

  • 动态规划的解题套路

  • leetcode案例分析



公众号:捡田螺的小男孩


什么是动态规划?


动态规划(英语:Dynamic programming,简称 DP),是一种在数学、管理科学、计算机科学、经济学和生物信息学中使用的,通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法。动态规划常常适用于有重叠子问题和最优子结构性质的问题。



dynamic programming is a method for solving a complex problem by breaking it down into a collection of simpler subproblems.



以上定义来自维基百科,看定义感觉还是有点抽象。简单来说,动态规划其实就是,给定一个问题,我们把它拆成一个个子问题,直到子问题可以直接解决。然后呢,把子问题答案保存起来,以减少重复计算。再根据子问题答案反推,得出原问题解的一种方法。



一般这些子问题很相似,可以通过函数关系式递推出来。然后呢,动态规划就致力于解决每个子问题一次,减少重复计算,比如斐波那契数列就可以看做入门级的经典动态规划问题。



动态规划核心思想


动态规划最核心的思想,就在于拆分子问题,记住过往,减少重复计算


动态规划在于记住过往


我们来看下,网上比较流行的一个例子:




  • A : "1+1+1+1+1+1+1+1 =?"

  • A : "上面等式的值是多少"

  • B : 计算 "8"

  • A : 在上面等式的左边写上 "1+" 呢?

  • A : "此时等式的值为多少"

  • B : 很快得出答案 "9"

  • A : "你怎么这么快就知道答案了"

  • A : "只要在8的基础上加1就行了"

  • A : "所以你不用重新计算,因为你记住了第一个等式的值为8!动态规划算法也可以说是 '记住求过的解来节省时间'"



一个例子带你走进动态规划 -- 青蛙跳阶问题


暴力递归



leetcode原题:一只青蛙一次可以跳上1级台阶,也可以跳上2级台阶。求该青蛙跳上一个 10 级的台阶总共有多少种跳法。



有些小伙伴第一次见这个题的时候,可能会有点蒙圈,不知道怎么解决。其实可以试想:




  • 要想跳到第10级台阶,要么是先跳到第9级,然后再跳1级台阶上去;要么是先跳到第8级,然后一次迈2级台阶上去。

  • 同理,要想跳到第9级台阶,要么是先跳到第8级,然后再跳1级台阶上去;要么是先跳到第7级,然后一次迈2级台阶上去。

  • 要想跳到第8级台阶,要么是先跳到第7级,然后再跳1级台阶上去;要么是先跳到第6级,然后一次迈2级台阶上去。



假设跳到第n级台阶的跳数我们定义为f(n),很显然就可以得出以下公式:


f(10) = f(9)+f(8)
f (9) = f(8) + f(7)
f (8) = f(7) + f(6)
...
f(3) = f(2) + f(1)

即通用公式为: f(n) = f(n-1) + f(n-2)

那f(2) 或者 f(1) 等于多少呢?



  • 当只有2级台阶时,有两种跳法,第一种是直接跳两级,第二种是先跳一级,然后再跳一级。即f(2) = 2;

  • 当只有1级台阶时,只有一种跳法,即f(1)= 1;


因此可以用递归去解决这个问题:


class Solution {
public int numWays(int n) {
if(n == 1){
return 1;
}
if(n == 2){
return 2;
}
return numWays(n-1) + numWays(n-2);
}
}

去leetcode提交一下,发现有问题,超出时间限制了



为什么超时了呢?递归耗时在哪里呢?先画出递归树看看:




  • 要计算原问题 f(10),就需要先计算出子问题 f(9) 和 f(8)

  • 然后要计算 f(9),又要先算出子问题 f(8) 和 f(7),以此类推。

  • 一直到 f(2) 和 f(1),递归树才终止。


我们先来看看这个递归的时间复杂度吧:


递归时间复杂度 = 解决一个子问题时间*子问题个数


  • 一个子问题时间 = f(n-1)+f(n-2),也就是一个加法的操作,所以复杂度是 O(1);

  • 问题个数 = 递归树节点的总数,递归树的总节点 = 2^n-1,所以是复杂度O(2^n)。


因此,青蛙跳阶,递归解法的时间复杂度 = O(1) * O(2^n) = O(2^n),就是指数级别的,爆炸增长的,如果n比较大的话,超时很正常的了。


回过头来,你仔细观察这颗递归树,你会发现存在大量重复计算,比如f(8)被计算了两次,f(7)被重复计算了3次...所以这个递归算法低效的原因,就是存在大量的重复计算


既然存在大量重复计算,那么我们可以先把计算好的答案存下来,即造一个备忘录,等到下次需要的话,先去备忘录查一下,如果有,就直接取就好了,备忘录没有才开始计算,那就可以省去重新重复计算的耗时啦!这就是带备忘录的解法。


带备忘录的递归解法(自顶向下)


一般使用一个数组或者一个哈希map充当这个备忘录



  • 第一步,f(10)= f(9) + f(8),f(9) 和f(8)都需要计算出来,然后再加到备忘录中,如下:




  • 第二步, f(9) = f(8)+ f(7),f(8)= f(7)+ f(6), 因为 f(8) 已经在备忘录中啦,所以可以省掉,f(7),f(6)都需要计算出来,加到备忘录中~



第三步, f(8) = f(7)+ f(6),发现f(8),f(7),f(6)全部都在备忘录上了,所以都可以剪掉。



所以呢,用了备忘录递归算法,递归树变成光秃秃的树干咯,如下:



备忘录的递归算法,子问题个数=树节点数=n,解决一个子问题还是O(1),所以带备忘录的递归算法的时间复杂度是O(n)。接下来呢,我们用带备忘录的递归算法去撸代码,解决这个青蛙跳阶问题的超时问题咯~,代码如下:


public class Solution {
//使用哈希map,充当备忘录的作用
Map<Integer, Integer> tempMap = new HashMap();
public int numWays(int n) {
// n = 0 也算1种
if (n == 0) {
return 1;
}
if (n <= 2) {
return n;
}
//先判断有没计算过,即看看备忘录有没有
if (tempMap.containsKey(n)) {
//备忘录有,即计算过,直接返回
return tempMap.get(n);
} else {
// 备忘录没有,即没有计算过,执行递归计算,并且把结果保存到备忘录map中,对1000000007取余(这个是leetcode题目规定的)
tempMap.put(n, (numWays(n - 1) + numWays(n - 2)) % 1000000007);
return tempMap.get(n);
}
}
}

去leetcode提交一下,如图,稳了:



其实,还可以用动态规划解决这道题。


自底向上的动态规划


动态规划跟带备忘录的递归解法基本思想是一致的,都是减少重复计算,时间复杂度也都是差不多。但是呢:



  • 带备忘录的递归,是从f(10)往f(1)方向延伸求解的,所以也称为自顶向下的解法。

  • 动态规划从较小问题的解,由交叠性质,逐步决策出较大问题的解,它是从f(1)往f(10)方向,往上推求解,所以称为自底向上的解法。


动态规划有几个典型特征,最优子结构、状态转移方程、边界、重叠子问题。在青蛙跳阶问题中:



  • f(n-1)和f(n-2) 称为 f(n) 的最优子结构

  • f(n)= f(n-1)+f(n-2)就称为状态转移方程

  • f(1) = 1, f(2) = 2 就是边界啦

  • 比如f(10)= f(9)+f(8),f(9) = f(8) + f(7) ,f(8)就是重叠子问题。


我们来看下自底向上的解法,从f(1)往f(10)方向,想想是不是直接一个for循环就可以解决啦,如下:



带备忘录的递归解法,空间复杂度是O(n),但是呢,仔细观察上图,可以发现,f(n)只依赖前面两个数,所以只需要两个变量a和b来存储,就可以满足需求了,因此空间复杂度是O(1)就可以啦



动态规划实现代码如下:


public class Solution {
public int numWays(int n) {
if (n<= 1) {
return 1;
}
if (n == 2) {
return 2;
}
int a = 1;
int b = 2;
int temp = 0;
for (int i = 3; i <= n; i++) {
temp = (a + b)% 1000000007;
a = b;
b = temp;
}
return temp;
}
}

动态规划的解题套路


什么样的问题可以考虑使用动态规划解决呢?



如果一个问题,可以把所有可能的答案穷举出来,并且穷举出来后,发现存在重叠子问题,就可以考虑使用动态规划。



比如一些求最值的场景,如最长递增子序列、最小编辑距离、背包问题、凑零钱问题等等,都是动态规划的经典应用场景。


动态规划的解题思路


动态规划的核心思想就是拆分子问题,记住过往,减少重复计算。 并且动态规划一般都是自底向上的,因此到这里,基于青蛙跳阶问题,我总结了一下我做动态规划的思路:



  • 穷举分析

  • 确定边界

  • 找出规律,确定最优子结构

  • 写出状态转移方程


1. 穷举分析



  • 当台阶数是1的时候,有一种跳法,f(1) =1

  • 当只有2级台阶时,有两种跳法,第一种是直接跳两级,第二种是先跳一级,然后再跳一级。即f(2) = 2;

  • 当台阶是3级时,想跳到第3级台阶,要么是先跳到第2级,然后再跳1级台阶上去,要么是先跳到第 1级,然后一次迈 2 级台阶上去。所以f(3) = f(2) + f(1) =3

  • 当台阶是4级时,想跳到第3级台阶,要么是先跳到第3级,然后再跳1级台阶上去,要么是先跳到第 2级,然后一次迈 2 级台阶上去。所以f(4) = f(3) + f(2) =5

  • 当台阶是5级时......


自底向上的动态规划


2. 确定边界


通过穷举分析,我们发现,当台阶数是1的时候或者2的时候,可以明确知道青蛙跳法。f(1) =1,f(2) = 2,当台阶n>=3时,已经呈现出规律f(3) = f(2) + f(1) =3,因此f(1) =1,f(2) = 2就是青蛙跳阶的边界。


3. 找规律,确定最优子结构


n>=3时,已经呈现出规律 f(n) = f(n-1) + f(n-2) ,因此,f(n-1)和f(n-2) 称为 f(n) 的最优子结构。什么是最优子结构?有这么一个解释:



一道动态规划问题,其实就是一个递推问题。假设当前决策结果是f(n),则最优子结构就是要让 f(n-k) 最优,最优子结构性质就是能让转移到n的状态是最优的,并且与后面的决策没有关系,即让后面的决策安心地使用前面的局部最优解的一种性质



4, 写出状态转移方程


通过前面3步,穷举分析,确定边界,最优子结构,我们就可以得出状态转移方程啦:



5. 代码实现


我们实现代码的时候,一般注意从底往上遍历哈,然后关注下边界情况,空间复杂度,也就差不多啦。动态规划有个框架的,大家实现的时候,可以考虑适当参考一下:


dp[0][0][...] = 边界值
for(状态1 :所有状态1的值){
for(状态2 :所有状态2的值){
for(...){
//状态转移方程
dp[状态1][状态2][...] = 求最值
}
}
}

leetcode案例分析


我们一起来分析一道经典leetcode题目吧



给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。



示例 1:


输入:nums = [10,9,2,5,3,7,101,18]
输出:4
解释:最长递增子序列是 [2,3,7,101],因此长度为 4 。

示例 2:


输入:nums = [0,1,0,3,2,3]
输出:4

我们按照以上动态规划的解题思路,



  • 穷举分析

  • 确定边界

  • 找规律,确定最优子结构

  • 状态转移方程


1.穷举分析


因为动态规划,核心思想包括拆分子问题,记住过往,减少重复计算。 所以我们在思考原问题:数组num[i]的最长递增子序列长度时,可以思考下相关子问题,比如原问题是否跟子问题num[i-1]的最长递增子序列长度有关呢?


自顶向上的穷举

这里观察规律,显然是有关系的,我们还是遵循动态规划自底向上的原则,基于示例1的数据,从数组只有一个元素开始分析。



  • 当nums只有一个元素10时,最长递增子序列是[10],长度是1.

  • 当nums需要加入一个元素9时,最长递增子序列是[10]或者[9],长度是1。

  • 当nums再加入一个元素2时,最长递增子序列是[10]或者[9]或者[2],长度是1。

  • 当nums再加入一个元素5时,最长递增子序列是[2,5],长度是2。

  • 当nums再加入一个元素3时,最长递增子序列是[2,5]或者[2,3],长度是2。

  • 当nums再加入一个元素7时,,最长递增子序列是[2,5,7]或者[2,3,7],长度是3。

  • 当nums再加入一个元素101时,最长递增子序列是[2,5,7,101]或者[2,3,7,101],长度是4。

  • 当nums再加入一个元素18时,最长递增子序列是[2,5,7,101]或者[2,3,7,101]或者[2,5,7,18]或者[2,3,7,18],长度是4。

  • 当nums再加入一个元素7时,最长递增子序列是[2,5,7,101]或者[2,3,7,101]或者[2,5,7,18]或者[2,3,7,18],长度是4.


分析找规律,拆分子问题

通过上面分析,我们可以发现一个规律


如果新加入一个元素nums[i], 最长递增子序列要么是以nums[i]结尾的递增子序列,要么就是nums[i-1]的最长递增子序列。看到这个,是不是很开心,nums[i]的最长递增子序列已经跟子问题 nums[i-1]的最长递增子序列有关联了。


原问题数组nums[i]的最长递增子序列 = 子问题数组nums[i-1]的最长递增子序列/nums[i]结尾的最长递增子序列

是不是感觉成功了一半呢?但是如何把nums[i]结尾的递增子序列也转化为对应的子问题呢?要是nums[i]结尾的递增子序列也跟nums[i-1]的最长递增子序列有关就好了。又或者nums[i]结尾的最长递增子序列,跟前面子问题num[j](0=<j<i)结尾的最长递增子序列有关就好了,带着这个想法,我们又回头看看穷举的过程:



nums[i]的最长递增子序列,不就是从以数组num[i]每个元素结尾的最长子序列集合,取元素最多(也就是长度最长)那个嘛,所以原问题,我们转化成求出以数组nums每个元素结尾的最长子序列集合,再取最大值嘛。哈哈,想到这,我们就可以用dp[i]表示以num[i]这个数结尾的最长递增子序列的长度啦,然后再来看看其中的规律:



其实,nums[i]结尾的自增子序列,只要找到比nums[i]小的子序列,加上nums[i] 就可以啦。显然,可能形成多种新的子序列,我们选最长那个,就是dp[i]的值啦




  • nums[3]=5,以5结尾的最长子序列就是[2,5],因为从数组下标0到3遍历,只找到了子序列[2]5小,所以就是[2]+[5]啦,即dp[4]=2

  • nums[4]=3,以3结尾的最长子序列就是[2,3],因为从数组下标0到4遍历,只找到了子序列[2]3小,所以就是[2]+[3]啦,即dp[4]=2

  • nums[5]=7,以7结尾的最长子序列就是[2,5,7][2,3,7],因为从数组下标0到5遍历,找到2,5和3都比7小,所以就有[2,7],[5,7],[3,7],[2,5,7]和[2,3,7]这些子序列,最长子序列就是[2,5,7]和[2,3,7],它俩不就是以5结尾和3结尾的最长递增子序列+[7]来的嘛!所以,dp[5]=3 =dp[3]+1=dp[4]+1



很显然有这个规律:一个以nums[i]结尾的数组nums



  • 如果存在j属于区间[0,i-1],并且num[i]>num[j]的话,则有,dp(i) =max(dp(j))+1,


最简单的边界情况


当nums数组只有一个元素时,最长递增子序列的长度dp(1)=1,当nums数组有两个元素时,dp(2) =2或者1,
因此边界就是dp(1)=1。


确定最优子结构


从穷举分析,我们可以得出,以下的最优结构:


dp(i) =max(dp(j))+1,存在j属于区间[0,i-1],并且num[i]>num[j]。

max(dp(j)) 就是最优子结构。


状态转移方程


通过前面分析,我们就可以得出状态转移方程啦:



所以数组num[i]的最长递增子序列就是:


最长递增子序列 =max(dp[i])

代码实现


class Solution {
public int lengthOfLIS(int[] nums) {
if (nums.length == 0) {
return 0;
}
int[] dp = new int[nums.length];
//初始化就是边界情况
dp[0] = 1;
int maxans = 1;
//自底向上遍历
for (int i = 1; i < nums.length; i++) {
dp[i] = 1;
//从下标0到i遍历
for (int j = 0; j < i; j++) {
//找到前面比nums[i]小的数nums[j],即有dp[i]= dp[j]+1
if (nums[j] < nums[i]) {
//因为会有多个小于nums[i]的数,也就是会存在多种组合了嘛,我们就取最大放到dp[i]
dp[i] = Math.max(dp[i], dp[j] + 1);
}
}
//求出dp[i]后,dp最大那个就是nums的最长递增子序列啦
maxans = Math.max(maxans, dp[i]);
}
return maxans;
}
}

参考与感谢



  • leetcode官网

  • 《labuladong算法小抄》

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

如何进一步提高flutter内存表现

前言 性能稳定性是App的生命,Flutter带了很多创新与机遇,然而团队在享受Flutter带来的收益同时也迎接了很多新事物带来的挑战。 本文就内存优化过程中一些实践经验跟大家做一个分享。 Flutter 上线之后 闲鱼使用一套混合栈管理的方案将Flutte...
继续阅读 »

前言


性能稳定性是App的生命,Flutter带了很多创新与机遇,然而团队在享受Flutter带来的收益同时也迎接了很多新事物带来的挑战。


本文就内存优化过程中一些实践经验跟大家做一个分享。


Flutter 上线之后


闲鱼使用一套混合栈管理的方案将Flutter嵌入到现有的App中。在产品体验上我们取得了优于Native的体验。主要得益于Flutter的在跨平台渲染方面的优势,部分原因则是因为我们用Dart语言重新实现的页面抛弃了很多历史的包袱轻装上阵。


上线之后各方面技术指标,都达到甚至超出了部分预期。而我们最为担心的一些稳定性指标,比如crash也在稳定的范围之内。但是在一段时间后我们发现由于内存过高而被系统杀死的abort率数据有比较明显的异常。性能稳定性问题是非常关键的,于是我们火速开展了问题排查。


问题定位与排查


显然问题出在了过大的内存消耗上。内存消耗在App中构成比较复杂,如何在复杂的业务中去定位到罪魁祸首呢?稍加观察,我们确定Flutter问题相对比价明显。工欲善其事必先利其器,需要更好地定位内存的问题,善用已经的工具是非常有帮助的。好在我们在Native层和Dart层都有足够多的性能分析工具进行使用。


工具分析


这里简单介绍我们如何使用的工具去观察手机数据以便于分析问题。需要注意的是,本文的重点不是工具的使用方法介绍,所以只是简单列举部分使用到的常见工具。


Xcode Instruments


Instruments是iOS内存排查的利器,可以比较便捷地观察实时内存使用情况,自然不必多说。


Xcode MemGraph + VMMap


XCode 8之后推出的MEMGraph是Xcode的内存调试利器,可以看到实时的可视化的内存。更为方便的是,你可以将MemGraph导出,配合命令行工具更好的得到结构化的信息。


Dart Observatory


这是Dart语言官方的调试工具,里面也包含了类似于Xcode的Instruments的工具。在Debug模式下Dart VM启动以后会在特定的端口接受调试请求。官方文档


观察结果


在整个过程中我进行了大量的观察,这里分享一部分典型的数据表现。


通过Xcode Instruments排查的话,我们观察到CG Raster Data这个数据有些高。这个Raster Data呢其实是图片光栅化的时候的内存消耗。


我们将App内存异常的场景的MemGraph导出来,对其执行VMMap指令得出的结果:


vmmap --summary Runner[40957].memgraph

vmmap Runner[40957].memgraph | grep 'IOKit'

vmmap Summary


vmmap address


我们主要关注resident和dirty的内存。发现IOKit占用了大量的内存。


结合Xcode Raster Data还有IOKit的大量内存消耗,我们开始怀疑问题是图内存泄漏导致的。经过进一步通过Dart Observatory观察Dart Image对象的内存情况。

Dart image instance

观察结果显示,在内存较高的场景下在Dart层的确同时存在了较多Image(如图中270)的对象。现在基本可以确定内存问题跟Dart层的图片有很大的关系。


这个结果,我估计很多人都已经想到了,App有明显的内存问题很有可能就是跟多媒体资源有关系。通过工具得出的准确数据线索,我们得到一个大致的方向去深入研究。


诡异的Dart图片数量爆炸


图片对象泄漏?


前面我们用工具观察到Dart层的Image对象数量过多直接导致了非常大的内存压力,我们起初怀疑存在图片的内存泄漏。但是我们在经过进一步确认以后发现图片其实并没有真正的泄漏。


Dart语言采用垃圾回收机制(Garbage Collection 下面开始简称GC)来管理分配的内存,VM层面的垃圾回收应该大多数情况下是可信的。但是从实际观察来看,图片数量的爆炸造成的较大的内存峰值直观感觉上GC来得有些不及时。在Debug模式下我们使用Dart Observatory手动触发GC,最终这些图片对象在没有引用的情况下最终还是会被回收。


至此,我们基本可以确认,图片对象不存在泄漏。那是什么导致了GC的反应迟钝呢,难道是Dart语言本身的问题吗?


Garbage Collection 不及时?


为此我需要了解一下Dart内存管理机制垃圾回收的实现,关于详细的内存问题我团队的 @匠修 同学已经发过一篇相关文章可以参考:内存文章


我这里不详细讨论Dart垃圾回收实现细节,只聊一聊Flutter与Dart相关的一些内容。


关于Flutter我需要首先明确几个概念:




  1. Framework(Dart)(跟iOS平台连接的库Flutter.framework要区别开)特指由Dart编写的Flutter相关代码。




  2. Dart VM执行Dart代码的Dart语言相关库,它是以C实现的Dart SDk形式提供的。对外主要暴露了C接口Dart Api。里面主要包含了Dart的编译器,运行时等等。




  3. FLutter Engine C++实现的Flutter驱动引擎。他主要负责跨平台的绘制实现,包含Skia渲染引擎的接入;Dart语言的集成;以及跟Native层的适配和Embeder相关的一些代码。简单理解,iOS平台上面Flutter.framework, Android平台上的Flutter.jar便是引擎代码构建后的产物。




在Dart代码里面对于GC是没有感知的。


对于Dart SDK也就是Dart语言我们可以做的很有限,因为Dart语言本身是一种标准,如果Dart真的有问题我们需要和Dart维护团队协作推进问题的解决。Dart语言设计的时候初衷也是希望GC对于使用者是透明的,我们不应该依赖GC实现的具体算法和策略。不过我们还是需要通过Dart SDK的源码去理解GC的大致情况。


既然我们前面已经确认并非内存泄漏,所以我们在对GC延迟的问题的调查主要放在Flutter Engine以及Dart CG入口上。


Flutter与Dart Garbage Collection


既然感觉GC不及时,先撇开消耗,我们至少可以尝试多触发几次GC来减轻内存峰值压力。但是我在仔细查阅dart_api.h(/src/third_party/dart/runtime/include/dart_api.h )接口文件后,但是并没有找到显式提供触发GC的接口。


但是找到了如下这个方法Dart_NotifyIdle


/**
* Notifies the VM that the embedder expects to be idle until |deadline|. The VM
* may use this time to perform garbage collection or other tasks to avoid
* delays during execution of Dart code in the future.
*
* |deadline| is measured in microseconds against the system's monotonic time.
* This clock can be accessed via Dart_TimelineGetMicros().
*
* Requires there to be a current isolate.
*/

DART_EXPORT void Dart_NotifyIdle(int64_t deadline);

这个接口意思是我们可以在空闲的时候显式地通知Dart,你接下来可以利用这些时间(dealine之前)去做GC。注意,这里的GC不保证会马上执行,可以理解我们请求Dart去做GC,具体做不做还是取决于Dart本身的策略。


另外,我还找到一个方法叫做Dart_NotifyLowMemory:


/**
* Notifies the VM that the system is running low on memory.
*
* Does not require a current isolate. Only valid after calling Dart_Initialize.
*/

DART_EXPORT void Dart_NotifyLowMemory();

不过这个Dart_NotifyLowMemory方法其实跟GC没有太大关系,它其实是在低内存的情况下把多余的isolate去终止掉。你可以简单理解,把一些不是必须的线程给清理掉。


在研究Flutter Engine代码后你会发现,Flutter Engine其实就是通过Dart_NotifyIdle去跟Dart层进行GC方面的协作的。我们可以在Flutter Engine源码animator.cc看到以下代码:


  
//Animator负责刷新和通知帧的绘制
if (!frame_scheduled_) {
// We don't have another frame pending, so we're waiting on user input
// or I/O. Allow the Dart VM 100 ms.
delegate_.OnAnimatorNotifyIdle(*this, dart_frame_deadline_ + 100000);
}


//delegate 最终会调用到这里
bool RuntimeController::NotifyIdle(int64_t deadline) {
if (!root_isolate_) {
return false;
}

tonic::DartState::Scope scope(root_isolate_.get());
//Dart api接口
Dart_NotifyIdle(deadline);
return true;
}

这里的逻辑比较直观:如果当前没有帧渲染的任务时候就通过NotifyIdle告诉Dart层可以进行GC操作了。注意,这里并不是说只有在这种情况下Dart才回去做GC,Flutter只是通过这种方式尽可能利用空闲去做GC,配合Dart以更合理的时间去做GC。


看到这里,我们有足够的理由去尝试一下这个接口,于是我们在一些内存压力比较大的场景进行了手动请求GC的操作。线上的Abort虽然有明显好转,但是内存峰值并没有因此得到改善。我们需要进一步找到根本原因。


图片数量爆炸的真相


为了确定图片大量囤积释放不及时的问题,我们需要跟踪Flutter图片从初始化到销毁的整个流程。


我们从Dart层开始去追寻Image对象的生命周期,我们可以看到Flutter里面所以的图片都是经过ImageProvider来获取的,ImageProvider在获取图片的时候会调用一个Resolve接口,而这个接口会首先查询ImageCache去读取图片,如果不存在缓存就new Image的实例出来。


关键代码:


  ImageStream resolve(ImageConfiguration configuration) {
assert(configuration != null);
final ImageStream stream = new ImageStream();
T obtainedKey;
obtainKey(configuration).then((T key) {
obtainedKey = key;
stream.setCompleter(PaintingBinding.instance.imageCache.putIfAbsent(key, () => load(key)));
}).catchError(
(dynamic exception, StackTrace stack) async {
FlutterError.reportError(new FlutterErrorDetails(
exception: exception,
stack: stack,
library: 'services library',
context: 'while resolving an image',
silent: true, // could be a network error or whatnot
informationCollector: (StringBuffer information) {
information.writeln('Image provider: $this');
information.writeln('Image configuration: $configuration');
if (obtainedKey != null)
information.writeln('Image key: $obtainedKey');
}
));
return null;
}
);
return stream;
}

大致的逻辑



  1. Resolve 请求获取图片.

  2. 查询是否存在于ImageCache.Yes->3 NO->4

  3. 返回已经存在的图片对象

  4. 生成新的Image对象并开始加载
    看起来没有特别复杂的逻辑,不过这里我要提一下Flutter ImageCache的实现。


Flutter ImageCache


Flutter ImageCache最初的版本其实非常简单,用Map实现的基于LRU算法缓存。这个算法和实现没有什么问题,但是要注意的是ImageCache缓存的是ImageStream对象,也就是缓存的是一个异步加载的图片的对象。而且缓存没有对占用内存总量做限制,而是采用默认最大限制1000个对象(Flutter在0.5.6 beta中加入了对内存大小限制的逻辑)。缓存异步加载对象的一个问题是,在图片加载解码完成之前,无法知道到底将要消耗多少内存,至少在Flutter这个Cache实现中没有处理这个问题。具体的实现感兴趣的朋友可以阅读ImageCache.dart源码。


其实Flutter本身提供了定制化Cache的能力,所以优化ImageCache的第一步就是要根据机型的物理内存去做缓存大小的适配,设置ImageCache的合理限制。关于ImageCache的问题,可以参考官方文档和这个issue,我这里不展开去聊了。


Flutter Image生命周期


回到我们的Image对象跟踪,很明显,在缓存没有命中的情况下会有新的Image产生。继续深入代码会发现Image对象是由这段代码产生的:



Future instantiateImageCodec(Uint8List list) {
return _futurize(
(_Callback callback) => _instantiateImageCodec(list, callback, null)
);
}

String _instantiateImageCodec(Uint8List list, _Callback callback, _ImageInfo imageInfo)
native 'instantiateImageCodec';

这里有个native关键字,这是Dart调用C代码的能力,我们查看具体的源码可以发现这个最终初始化的是一个C++的codec对象。具体的代码在Flutter Engine codec.cc。它大致的过程就是先在IO线程中启动了一个解码任务,在IO完成之后再把最终的图片对象发回UI线程。关于Flutter线程的详细介绍,我在另外一篇文章中已经有介绍,这里附上链接给有兴趣的朋友。深入理解Flutter Engine线程模型。经过来这些代码和线程分析,我们得到大致的流程图:


图片爆炸流程图


也就是说,解码任务在IO线程进行,IO任务队列里面都是C++ lambda表达式,持有了实际的解码对象,也就持有了内存资源。当IO线程任务过多的时候,会有很多IO任务在等待执行,这些内存资源也被闭包所持有而等待释放。这就是为什么直观上会有内存释放不及时而造成内存峰值的问题。这也解释了为什么之前拿到的vmmap虚拟内存数据里面IOKit是大头。


这样我们找到了关键的线索,在缓存不命中的情况下,大量初始化Image对象,导致IO线程任务繁重,而IO又持有大量的图片解码所用的内存资源。带这个推论,我在Flutter Engine的Task Runner加入了任务数量和C++ image对象的监控代码,证实了的确存在IO任务线程过载的情况,峰值在极端情况下瞬时达到了100+IO操作。


IO Runner监控


到这里问题似乎越来越明了了,但是为什么会有这么IO任务触发呢?上述逻辑虽然可能会有IO线程过载的情况下占用大量内存的情况。上层要求生成新的图片对象,这种请求是没有错误的,设计就是如此。就好比主线程阻塞大量的任务,必然会导致界面卡顿,但者却不是主线程本身的问题。我们需要从源头找到导致新对象创建暴涨真正导致IO线程过载的原因。


大量请求的根源


在前面的线索之下,我们继续寻找问题的根源。我们在实际App操作的过程当中发现,页面Push的越多,图片生成的速度越来越快。也就是说页面越多请求越快,看起来没有什么大问题。但是可见的图片其实总是在一定数量范围之内的,不应该随着页面增多而加快对象创建的频率。我们下意识的开始怀疑是否存在不可见的Image Widget也在不断请求图片的情况。最终导致了Cache无法命中而大量生成新的图片的场景。


我开始调查每个页面的图片加载请求,我们知道Flutter里面万物皆Widget,页面都是是Widget,由Navigator管理。我在Widget的生命周期方法(详细见Flutter官方文档)中加入监控代码,如我所料,在Navigator栈底下不可见的页面也还在不停的Resolve Image,直接导致了image对象暴涨而导致IO线程过载,导致了内存峰值。


看起来,我们终于找到了根本原因。解决方案并不难。在页面不可见的时候没必要发出多余的图片加载请求,峰值也就随之降下来了。再经过一番代码优化和测试以后问题得到了根本上的解决。优化上线以后,我们看到了数据发生了质的好转。
有朋友可能想问,为什么不可见的Widget也会被调用到相关的生命周期方法。这里我推荐阅读Flutter官方文档关于Widget相关的介绍,篇幅有限我这里不展开介绍了。widgets


至此,我们已经解决了一个较为严重的内存问题。内存优化情况复杂,可以点也比较多,接下来我继续简要分享在其它一些方面的优化方案。


截图缓存优化


文件缓存+预加载策略


我们是采用嵌入式Flutter并使用一套混合栈模式管理Native和Flutter页面相互跳转的逻辑。由于FlutterView在App中是单例形式存在的,我们为了更好的用户体验,在页面切换的过程中使用的截图的方式来进行过渡。


大家都知道,图片是非常占用内存的对象,我们如何在不降低用户体验的同时获得最小的内存消耗呢?假如我们每push一个页面都保存一张截图,那么内存是以线性复杂度增长的,这显然不够好。


内存和空间在大多数情况下是一个互相转换的关系,优化很多时候其实是找一个合理的折中点。
最终我采用了预加载+缓存的策略,在页面最多只在内存中同时存在两个截图,其它的存文件,在需要的时候提前进行预加载。
简要流程图:


简要流程图


这样的话就做到了不影响用户体验的前提下,将空间复杂度从O(n)降低到了O(1)。
这个优化进一步节省了不必要的内存开销。


截图额外的优化



  • 针对当前设备的内存情况,自适应调整截图的分辨率,争取最小的内存消耗。

  • 在极端的内存情况下,把所有截图都从内存中移除存(存文件可恢复),采用PlaceHolder的形式。极端情况下避免被杀,保证可用性的体验降级策略。


页面兜底策略


对于电商类App存在一个普遍的问题,用户会不断的push页面到栈里面,我们不能阻止用户这种行为。我们当然可以把老页面干掉,每次回退的时候重新加载,但是这种用户体验跟Web页一样,是用户不可接受的。我们要维持页面的状态以保证用户体验。这必然会导致内存的线性增长,最终肯定难免要被杀。我们优化的目的是提高用户能够push的极限页面数量。


对于Flutter页面优化,除了在优化每一个页面消耗的内存之外,我们做了降级兜底策略去保证App的可用性:在极端情况下将老页面进行销毁,在需要的时候重新创建。这的确降低了用户体验,在极端情况下,降级体验还是比Crash要好一些。



FlutterViewController 单例析构


另外我想讨论的一个话题是关于FlutterViewController的。目前Flutter的设计是按照单例模式去运行的,这对于完全用Flutterc重新开发的App没有太大的问题。但是对于混合型App,多出来的常驻内存确实是一个问题。


实际上,Flutter Engine底层实现是考虑到了析构这个问题,有相关的接口。但是在Embeder这一层(具体FlutterViewController Message Channels这一层),在实现过程中存在一些循环引用,导致在Native层就算没有引用FlutterViewController的时候也无法释放.


FlutterViewController引用图


我在经过一段时间的尝试后,算是把循环引用解除了。这些循环引用主要集中在FlutterChannel这一块。在解除之后我顺利的释放了FlutterViewController,可以明显看到常驻内存得到了释放。但是我发现释放FlutterViewController的时候会导致一部分Skia Image对象泄漏,因为Skia Objects必须在它创建的线程进行释放(详情请参考skia_gpu_object.cc源码),线程同步的问题。关于这个问题我在GitHub上面有一个issue大家可以参考。FlutterViewController释放issue


目前,这个优化我们已经反馈给Flutter团队,期待他们官方支持。希望大家可以一起探索研究。


进一步探讨


除此之外,Flutter内存方面其实还有比较多方面可以去研究。我这里列举几个目前观察到的问题。




  1. 我在内存分析的时候发现Flutter底层使用的boring ssl库有可以确定的内存泄漏。虽然这个泄漏比较缓慢,但是对于App长期运行还是有影响的。我在GitHub上面提了个issue跟进,目前已有相关的人员进行跟进。SSL leak issue




  2. 关于图片渲染,目前Flutter还是有优化空间的,特别是图片的按需剪裁。大多数情况下是没有不要将整一个bitmap解压到内存中的,我们可以针对显示的区域大小和屏幕的分辨率对图片进行合理的缩放以取得最好的性能消耗。




  3. 在分析Flutter内存的MemGraph的时候,我发现Skia引擎当中对于TextLayout消耗了大量的内存.目前我没有找到具体的原因,可能存在优化的空间。




结语


在这篇文章里,我简要的聊了一下目前团队在Flutter应用内存方面做出的尝试和探索。短短一篇文章无法包含所有内容,只能推出了几个典型的案例来作分析,希望可以跟大家一起探讨研究。欢迎感兴趣的朋友一起研究,如有更好的想法方案,我非常乐意看到你的分享。


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

Flutter动画实现粒子漂浮效果

要问2019年最火的移动端框架,肯定非Google的Flutter莫属。 本着学习的态度,基本的Dart语法(个人感觉语法风格接近Java+JS)过完之后,开始撸代码练手。 效果图 (这里为了方便录制gif,动画设置的较快;如果将动画的Duration设...
继续阅读 »

要问2019年最火的移动端框架,肯定非Google的Flutter莫属。

image

本着学习的态度,基本的Dart语法(个人感觉语法风格接近Java+JS)过完之后,开始撸代码练手。




效果图


image


(这里为了方便录制gif,动画设置的较快;如果将动画的Duration设置成20s,看起来就是浮动的效果了)
粒子碰撞的效果参考了张风捷特列 大佬的Flutter动画之粒子精讲


1. Flutter的动画原理



在任何系统的UI框架中,动画实现的原理都是相同的,即:在一段时间内,快速地多次改变UI外观;由于人眼会产生视觉暂留,所以最终看到的就是一个“连续”的动画,这和电影的原理是一样的。我们将UI的一次改变称为一个动画帧,对应一次屏幕刷新,而决定动画流畅度的一个重要指标就是帧率FPS(Frame Per Second),即每秒的动画帧数。



简而言之,就是逐帧绘制,只要屏幕刷新的足够快,我们就会觉得这是个连续的动画。
设想一个小球从屏幕顶端移动到底端的动画,为了完成这个动画,我们需要哪些数据呢?



  • 小球的运动轨迹,即起始点s、终点e和中间任意一点p

  • 动画持续时长t


只有这两个参数够吗?明显是不够的,因为小球按照给定的轨迹运动,可能是匀速、先快后慢、先慢后快、甚至是一会儿快一会慢的交替地运动,只要在时间t内完成,都是可能的。所以我们应该再指定一个参数c来控制动画的速度。


1.1 vsync探究


废话不多说,我们看看Flutter中是动画部分的代码:


AnimationController controller = AnimationController(
vsync: this,
duration: Duration(seconds: 2),
)..addListener(() {
//_renderBezier();
print(controllerG.value);
print('这是第${++count}次回调');
});
复制代码

简要分析一下,AnimationController,顾名思义,控制器,用来控制动画的播放。传入的参数中,duration我们知道是前面提到过的动画持续时长t,那这个vsync是啥参数呢?打过游戏的同学可能对这个单词有印象,vsync 就是 垂直同步 。那什么是垂直同步呢?



垂直同步又称场同步(Vertical Hold),从CRT显示器的显示原理来看,单个像素组成了水平扫描线,水平扫描线在垂直方向的堆积形成了完整的画面。显示器的刷新率受显卡DAC控制,显卡DAC完成一帧的扫描后就会产生一个垂直同步信号。我们平时所说的打开垂直同步指的是将该信号送入显卡3D图形处理部分,从而让显卡在生成3D图形时受垂直同步信号的制约。



简而言之就是,显卡在完成渲染后,将画面数据送往显存中,而显示器从显存中一行一行从上到下取出画面数据,进行显示。但是屏幕的刷新率和显卡渲染数据的速度很多时候是不匹配的,试想一下,显示器刚扫描显示完屏幕上半部分的画面,正准备从显存取下面的画面数据时,显卡送来了下一帧的图像数据覆盖了原来的显存,这个时候显示器取出的下面部分的图像就和上面的不匹配,造成画面撕裂。


为了避免这种情况,我们引入垂直同步信号,只有在显示器完整的扫描显示完一帧的画面后,显卡收到垂直同步信号才能刷新显存。
可是这个物理信号跟我们flutter动画有啥关系呢?vsync对应的参数是this,我们继续分析一下this对应的下面的类。


class _RunBallState extends State<RunBall> with TickerProviderStateMixin 
复制代码

with关键字是使用该类的方法而不继承该类,Mixin是类似于Java的接口,区别在于Mixin中的方法不是抽象方法而是已经实现了的方法。



这个TickerProviderStateMixin到底是干啥的呢???经过哥们儿Google的帮助,在网上找到了



关于动画的驱动,在此简单的说一下,Ticker是被SchedulerBinding所驱动。SchedulerBinding则是监听着Window.onBeginFrame回调。
Window.onBeginFrame的作用是什么呢,是告诉应用该提供一个scene了,它是被硬件的VSync信号所驱动的。



于是我们终于发现了,绕了一圈,归根到底还是真正的硬件产生的垂直同步信号在驱动着Flutter的动画的进行。


..addListener(() {
//_renderBezier();
print(controllerG.value);
print('这是第${++count}次回调');
});

复制代码

注意到之前的代码中存在一个动画控制器的监听器,动画在执行时间内,函数回调controller.value会生成一个从0到1的double类型的数值。我们在控制台打印出结果如下:

image


image


经过观察,两次试验,在2s的动画执行时间内,该回调函数分别被执行了50次,53次,并不是一个固定值。也就是说硬件(模拟器)的屏幕刷新率大概维持在(25~26.5帧/s)。


结论:硬件决定动画刷新率


1.2 动画动起来


搞懂了动画的原理之后,我们接下来就是逐帧的绘制了。关于Flutter的自定义View,跟android原生比较像。


image


继承CustomPainter类,重写paint和shouldRepaint方法,具体实现可以看代码.


class Ball {
double aX;
double aY;
double vX;
double vY;
double x;
double y;
double r;
Color color;}

复制代码

小球Ball具有圆心坐标、半径、颜色、速度、加速度等属性,通过数学表达式计算速度和加速度的关系,就可以实现匀加速的效果。


//运动学公式,看起来少了个时间t;实际上这些函数在动画过程中逐帧回调,把每帧刷新周期当成单位时间,相当于t=1
_ball.x += _ball.vX;//位移=速度*时间
_ball.y += _ball.vY;
_ball.vX += _ball.aX;//速度=加速度*时间
_ball.vY += _ball.aY;

复制代码

控制器使得函数不断回调,在回调函数函数里改变小球的相关参数,并且调用setState()函数,使得UI重新绘制;小球的轨迹坐标不断地变化,逐帧绘制的小球看起来就在运动了。你甚至可以在添加效果使得小球在撞到边界时变色或者半径变小(参考文章开头的粒子碰撞效果图)。


2. 小球随机浮动的思考


问题来了,我想要一个漂浮的效果呢?最好是随机的轨迹,就像气泡在空中飘乎不定,于是引起了我的思考;匀速然后方向随机?感觉不够优雅,于是去网上搜了一下,发现了思路!



首先随机生成一条贝塞尔曲线作为轨迹,等小球运动到终点,再生成新的贝塞尔曲线轨迹



生成二阶贝塞尔曲线的公式如下:


//二次贝塞尔曲线轨迹坐标,根据参数t返回坐标;起始点p0,控制点p1,终点p2
Offset quadBezierTrack(double t, Offset p0, Offset p1, Offset p2) {
var bx = (1 - t) * (1 - t) * p0.dx + 2 * t * (1 - t) * p1.dx + t * t * p2.dx;
var by = (1 - t) * (1 - t) * p0.dy + 2 * t * (1 - t) * p1.dy + t * t * p2.dy;

return Offset(bx, by);
}
复制代码

很巧的是,这里需要传入一个0~1之间double类型的参数t,恰好前面我们提过,animationController会在给定的时间内,生成一个0~1的value;这太巧了。


起始点的坐标不用说,接下来就剩解决控制点p1和p2,当然是随机生成这两点,但是如果同时有多个小球呢?比如5个小球同时进行漂浮,每个小球都对应一组三个坐标的信息,给小球Ball添加三个坐标的属性?不,这个时候,我们可以巧妙地利用带种子参数的随机数。



我们知道随机数在生成的时候,如果种子相同的话,每次生成的随机数也是相同的。



每个小球对象在创建的时候自增地赋予一个整形的id,作为随机种子;比如5个小球,我们起始的id为:2,4,6,8,10;


    Offset p0 = ball.p0;//起点坐标
Offset p1 = _randPosition(ball.id);
Offset p2 = _randPosition(ball.id + 1);
复制代码

rand(2),rand(2+1)为第一个小球的p1和p2坐标;当所有小球到达终点时,此时原来的终点p2为新一轮贝塞尔曲线的起点;此时相应的id也应增加,为了防止重复,id应增加小球数量5 *2,即第二轮运动开始时,5个小球的id为:12,14,16,18,20。
这样就保证了每轮贝塞尔曲线运动的时候,对于每个小球而言,p0,p1,p2是确定的;新一轮的运动所需要的随机的三个坐标点,只需要改变id的值就好了。


Path quadBezierPath(Offset p0, Offset p1, Offset p2) {
Path path = new Path();
path.moveTo(p0.dx, p0.dy);
path.quadraticBezierTo(p1.dx, p1.dy, p2.dx, p2.dy);
return path;
}
复制代码

这个时候,我们还可以利用Flutter自带的api画出二次贝塞尔曲线的轨迹,看看小球的运动是否落在轨迹上。


image


2.1 一些细节


animation = CurvedAnimation(parent: controllerG, curve: Curves.bounceOut);
复制代码

这里的Curve就是前面提到的,控制动画过程的参数,flutter自带了挺多效果,我最喜欢这个bounceOut(弹出效果)

image


 animation.addStatusListener((status) {
switch (status) {
case AnimationStatus.dismissed:
// TODO: Handle this case.
break;
case AnimationStatus.forward:
// TODO: Handle this case.
break;
case AnimationStatus.reverse:
// TODO: Handle this case.
break;
case AnimationStatus.completed:
// TODO: Handle this case.
controllerG.reset();
controllerG.forward();
break;
}
});

复制代码

监听动画过程的状态,当一轮动画结束时,status状态为AnimationStatus.completed;此时,我们将控制器reset重置,再forward重新启动,此时就会开始新一轮的动画效果;如果我们选的是reverse,则动画会反向播放。




GestureDetector(
child: Container(
width: double.infinity,
height: 200,
child: CustomPaint(
painter: FloatBallView(_ballsF, _areaF),
),
),
onTap: () {
controllerG.forward();
},
onDoubleTap: () {
controllerG.stop();
},
),
复制代码

为了方便控制,我还加了个手势监听器,单击控制动画运行,双击暂停动画。


3 完结


水平有限,文中如有错误还请各位指出,我是梦龙Dragon


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

浅探Google V8引擎

探析它之前,我们先抛出以下几个疑问:为什么需要 V8 引擎呢?V8 引擎到底是个啥?它可以做些什么呢?了解它能有什么收获呢?接下来就针对以上几个问题进行详细描述。由来我们都知道,JS 是一种解释型语言,支持动态类型(明显不同于 Java 等这类静态语言就是在命...
继续阅读 »

探析它之前,我们先抛出以下几个疑问:

  • 为什么需要 V8 引擎呢?

  • V8 引擎到底是个啥?

  • 它可以做些什么呢?

  • 了解它能有什么收获呢?

接下来就针对以上几个问题进行详细描述。

由来

我们都知道,JS 是一种解释型语言,支持动态类型(明显不同于 Java 等这类静态语言就是在命名一个变量时不需要声明变量的类型)、弱类型、基于原型的语言,内置支持类型。而一般 JS 都是在前端执行(直接影响界面),需要能够快速响应用户,那么就要求语言本身可以被快速地解析和执行,JS 引擎就为此而问世。

这里提到了解释型语言和静态语言(编译型语言),先简单介绍一下二者:

  • 解释型语言(JS)

    • 每次运行时需要解释器按句依次解释源代码执行,将它们翻译成机器认识的机器代码再执行

  • 编译型语言(Java)

    • 运行时可经过编译器翻译成可执行文件,再由机器运行该可执行文件即可

从上面的描述中可以看到 JS 运行时每次都要根据源文件进行解释然后执行,而编译型的只需要编译一次,下次可直接运行其可执行文件,但是这样就会导致跨平台的兼容性很差,因此各有优劣。

而众多 JS 引擎(V8、JavaScriptCore、SpiderMonkey、Chakra等)中 V8 是最为出色的,加上它也是应用于当前最流行的谷歌浏览器,所以我们非常有必要去认识和了解一下,这样对于开发者也就更清楚 JS 在浏览器中到底是如何运行的了。

认识

定义

  • 使用 C++ 开发

  • 谷歌开源

  • 编译成原生机器码(支持IA-32, x86-64, ARM, or MIPS CPUs)

  • 使用了如内联缓存(inline caching)等方法来提高性能

  • 运行速度快,可媲美二进制程序

  • 支持众多操作系统,如 windows、linux、android 等

  • 支持其他硬件架构,如 IA32,X64,ARM 等

  • 具有很好的可移植和跨平台特性

运行

先来一张官方流程图:

img

准备

JS 文件加载(不归 V8 管):可能来自于网络请求、本地的cache或者是也可以是来自service worker,这是 V8 运行的前提(有源文件才有要解释执行的)。 3种加载方式 & V8的优化

  • Cold load: 首次加载脚本文件时,没有任何数据缓存

  • Warm load:V8 分析到如果使用了相同的脚本文件,会将编译后的代码与脚本文件一起缓存到磁盘缓存中

  • Hot load: 当第三次加载相同的脚本文件时,V8 可以从磁盘缓存中载入脚本,并且还能拿到上次加载时编译后的代码,这样可以避免完全从头开始解析和编译脚本

而在 V8 6.6 版本的时候进一步改进代码缓存策略,简单讲就是从缓存代码依赖编译过程的模式,改变成两个过程解耦,并增加了可缓存的代码量,从而提升了解析和编译的时间,大大提升了性能,具体细节见V8 6.6 进一步改进缓存性能

分析

此过程是将上面环节得到的 JS 代码转换为 AST(抽象语法树)。

词法分析

从左往右逐个字符地扫描源代码,通过分析,产生一个不同的标记,这里的标记称为 token,代表着源代码的最小单位,通俗讲就是将一段代码拆分成最小的不可再拆分的单元,这个过程称为词法标记,该过程的产物供下面的语法分析环节使用。

这里罗列一下词法分析器常用的 token 标记种类:

  • 常数(整数、小数、字符、字符串等)

  • 操作符(算术操作符、比较操作符、逻辑操作符)

  • 分隔符(逗号、分号、括号等)

  • 保留字

  • 标识符(变量名、函数名、类名等)

TOKEN-TYPE TOKEN-VALUE\
-----------------------------------------------\
T_IF                 if\
T_WHILE              while\
T_ASSIGN             =\
T_GREATTHAN          >\
T_GREATEQUAL         >=\
T_IDENTIFIER name    / numTickets / ...\
T_INTEGERCONSTANT    100 / 1 / 12 / ....\
T_STRINGCONSTANT     "This is a string" / "hello" / ...

上面提到会逐个从左至右扫描代码然后分析,那么很明显就会想到两种方案,扫描完再分析(非流式处理)和边扫描边分析(流式处理),简单画一下他们的时序图就能发现流式处理效率要高得多,同时分析完也会释放分析过程中占用的内存,也能大大提高内存使用效率,可见该优化的细节处理。

语法分析

语法分析是指根据某种给定的形式文法对由单词序列构成的输入文本(例如上个阶段的词法分析产物-tokens stream),进行分析并确定其语法结构的过程,最后产出其 AST(抽象语法树)。

V8 会将语法分析的过程分为两个阶段来执行:

  • Pre-parser

    • 跳过还未使用的代码

    • 不会生成对应的 AST,会产生不带有变量的引用和声明的 scopes 信息

    • 解析速度会是 Full-parser 的 2 倍

    • 根据 JS 的语法规则仅抛出一些特定的错误信息

  • Full-parser

    • 解析那些使用的代码

    • 生成对应的 AST

    • 产生具体的 scopes 信息,带有变量引用和声明等信息

    • 抛出所有的 JS 语法错误

为什么要做两次解析?

如果仅有一次,那只能是 Full-parser,但这样的话,大量未使用的代码会消耗非常多的解析时间,结合实例来看下:通过 Coverage 录制的方式可以分析页面哪些代码没有用到,如下图可以看到最高有 75% 的没有被执行。

img

但是预解析并不是万能的,得失是并存的,很明显的一个场景:该文件中的代码全都执行了,那其实就是没必要的,当然这种情况其实还是占比远不如上面的例子,所以这里其实也是一种权衡,需要照顾大多数来达到综合性能的提升。

下面给出一个示例:

function add(x, y) {
   if (typeof x === "number") {
       return x + y;
  } else {
       return x + 'tadm';
  }
}

复制上面的代码到 web1web2 可以很直观的看到他们的 tokens 和 AST 结构(也可自行写一些代码体验)。

img

  • tokens

[
  {
       "type": "Keyword",
       "value": "function"
  },
  {
       "type": "Identifier",
       "value": "add"
  },
  {
       "type": "Punctuator",
       "value": "("
  },
  {
       "type": "Identifier",
       "value": "x"
  },
  {
       "type": "Punctuator",
       "value": ","
  },
  {
       "type": "Identifier",
       "value": "y"
  },
  {
       "type": "Punctuator",
       "value": ")"
  },
  {
       "type": "Punctuator",
       "value": "{"
  },
  {
       "type": "Keyword",
       "value": "if"
  },
  {
       "type": "Punctuator",
       "value": "("
  },
  {
       "type": "Keyword",
       "value": "typeof"
  },
  {
       "type": "Identifier",
       "value": "x"
  },
  {
       "type": "Punctuator",
       "value": "==="
  },
  {
       "type": "String",
       "value": "\"number\""
  },
  {
       "type": "Punctuator",
       "value": ")"
  },
  {
       "type": "Punctuator",
       "value": "{"
  },
  {
       "type": "Keyword",
       "value": "return"
  },
  {
       "type": "Identifier",
       "value": "x"
  },
  {
       "type": "Punctuator",
       "value": "+"
  },
  {
       "type": "Identifier",
       "value": "y"
  },
  {
       "type": "Punctuator",
       "value": ";"
  },
  {
       "type": "Punctuator",
       "value": "}"
  },
  {
       "type": "Keyword",
       "value": "else"
  },
  {
       "type": "Punctuator",
       "value": "{"
  },
  {
       "type": "Keyword",
       "value": "return"
  },
  {
       "type": "Identifier",
       "value": "x"
  },
  {
       "type": "Punctuator",
       "value": "+"
  },
  {
       "type": "String",
       "value": "'tadm'"
  },
  {
       "type": "Punctuator",
       "value": ";"
  },
  {
       "type": "Punctuator",
       "value": "}"
  },
  {
       "type": "Punctuator",
       "value": "}"
  }
]
  • AST

{
 "type": "Program",
 "body": [
  {
     "type": "FunctionDeclaration",
     "id": {
       "type": "Identifier",
       "name": "add"
    },
     "params": [
      {
         "type": "Identifier",
         "name": "x"
      },
      {
         "type": "Identifier",
         "name": "y"
      }
    ],
     "body": {
       "type": "BlockStatement",
       "body": [
        {
           "type": "IfStatement",
           "test": {
             "type": "BinaryExpression",
             "operator": "===",
             "left": {
               "type": "UnaryExpression",
               "operator": "typeof",
               "argument": {
                 "type": "Identifier",
                 "name": "x"
              },
               "prefix": true
            },
             "right": {
               "type": "Literal",
               "value": "number",
               "raw": "\"number\""
            }
          },
           "consequent": {
             "type": "BlockStatement",
             "body": [
              {
                 "type": "ReturnStatement",
                 "argument": {
                   "type": "BinaryExpression",
                   "operator": "+",
                   "left": {
                     "type": "Identifier",
                     "name": "x"
                  },
                   "right": {
                     "type": "Identifier",
                     "name": "y"
                  }
                }
              }
            ]
          },
           "alternate": {
             "type": "BlockStatement",
             "body": [
              {
                 "type": "ReturnStatement",
                 "argument": {
                   "type": "BinaryExpression",
                   "operator": "+",
                   "left": {
                     "type": "Identifier",
                     "name": "x"
                  },
                   "right": {
                     "type": "Literal",
                     "value": "tadm",
                     "raw": "'tadm'"
                  }
                }
              }
            ]
          }
        }
      ]
    },
     "generator": false,
     "expression": false,
     "async": false
  }
],
 "sourceType": "script"
}

解释

该阶段就是将上面产生的 AST 转换成字节码。

这里增加字节码(中间产物)的好处是,并不是将 AST 直接翻译成机器码,因为对应的 cpu 系统会不一致,翻译成机器码时要结合每种 cpu 底层的指令集,这样实现起来代码复杂度会非常高;还有个就是内存占用的问题,因为机器码会存储在内存中,而退出进程后又会存储在磁盘上,加上转换后的机器码多出来很多信息,会比源文件大很多,导致了严重的内存占用问题。

V8 在执行字节码的过程中,使用到了通用寄存器累加寄存器,函数参数和局部变量保存在通用寄存器里面,累加器中保存中间计算结果,在执行指令的过程中,如果直接由 cpu 从内存中读取数据的话,比较影响程序执行的性能,使用寄存器存储中间数据的设计,可以大大提升 cpu 执行的速度。

编译

这个过程主要是 V8 的 TurboFan编译器 将字节码翻译成机器码的过程。

字节码配合解释器和编译器这一技术设计,可以称为JIT(即时编译技术),Java 虚拟机也是类似的技术,解释器在解释执行字节码时,会收集代码信息,标记一些热点代码(就是一段代码被重复执行多次),TurboFan 会将热点代码直接编译成机器码,缓存起来,下次调用直接运行对应的二进制的机器码,加快执行速度。

在 TurboFan 将字节码编译成机器码的过程中,还进行了简化处理:常量合并、强制折减、代数重新组合。

比如:3 + 4 --> 7,x + 1 + 2 --> x + 3 ......

执行

到这里我们就开始执行上一阶段产出的机器码。

而在 JS 的执行过程中,经常遇到的就是对象属性的访问。作为一种动态的语言,一个简单的属性访问可能包含着复杂的语义,比如Object.xxx的形式,可能是属性的直接访问,也可能去调用的对象的Getter方法,还有可能是要通过原型链往上层对象中查找。这种不确定性而且动态判断的情况,会浪费很多查找时间,所以 V8 会把第一次分析的结果放在缓存中,当再次访问相同的属性时,会优先从缓存中去取,调用 GetProperty(Object, "xxx", feedback_cache) 的方法获取缓存,如果有缓存结果,就会跳过查找过程,又大大提升了运行性能。

除了上面针对读取对象属性的结果缓存的优化,V8 还引入了 Object Shapes(隐藏类)的概念,这里面会记录一些对象的基本信息(比如对象拥有的所有属性、每个属性对于这个对象的偏移量等),这样我们去访问属性时就可以直接通过属性名和偏移量直接定位到他的内存地址,读取即可,大大提升访问效率。

既然 V8 提出了隐藏类(两个形状相同的对象会去复用同一个隐藏类,何为形状相同的对象?两个对象满足有相同个数的相同属性名称和相同的属性顺序),那么我们开发者也可以很好的去利用它:

  • 尽量创建形状相同的对象

  • 创建完对象后尽量不要再去操作属性,即不增加或者删除属性,也就不会破环对象的形状

完成

到此 V8 已经完成了一份 JS 代码的读取、分析、解释、编译、执行。

总结

以上就是从 JS 代码下载到最终在 V8 引擎执行的过程分析,可以发现 V8 其实有很多实现的技术点,有着很巧妙的设计思想,比如流式处理、缓存中间产物、垃圾回收等,这里面又会涉及到很多细节,很值得继续深入研究。

作者:Tadm
来源:https://juejin.cn/post/7032278688192430117

收起阅读 »

手写清除console的loader

前言删除console方式介绍通过编辑器查找所有console,或者eslint编译时的报错提示定位语句,然后清除,就是有点费手,不够优雅 因此下面需要介绍几种优雅的清除方式该插件可用于压缩我们的js代码,同时可以通过配置去掉console语句,安装后配置在...
继续阅读 »




前言

作为一个前端,对于console.log的调试可谓是相当熟悉,话不多说就是好用!帮助我们解决了很多bug^_^
但是!有因必有果(虽然不知道为什么说这句但是很顺口),如果把console发到生产环境也是很头疼的,尤其是如果打印的信息很私密的话,可能要凉凉TT

删除console方式介绍

对于在生产环境必须要清除的console语句,如果手动一个个删除,听上去就很辛苦,因此这篇文章本着看到了就要学,学到了就要用的精神我打算介绍一下手写loader的方式清除代码中的console语句,在此之前也介绍一下其他可以清除console语句的方式吧哈哈

1. 方式一:暴力清除

通过编辑器查找所有console,或者eslint编译时的报错提示定位语句,然后清除,就是有点费手,不够优雅
因此下面需要介绍几种优雅的清除方式

2. 方式二 :uglifyjs-webpack-plugin

该插件可用于压缩我们的js代码,同时可以通过配置去掉console语句,安装后配置在webpack的optimization下,即可使用,需要注意的是:此配置只在production环境下生效

安装
npm i uglifyjs-webpack-plugin

其中drop_console和pure_funcs的区别是:

  • drop_console的配置值为boolean,也就是说如果为true,那么代码中所有带console前缀的调试方式都会被清除,包括console.log,console.warn等

  • pure_funcs的配置值是一个数组,也就是可以配置清除那些带console前缀的语句,截图中配的是['console.log'],因此生产环境上只会清除console.log,如果代码中包含其他带console的前缀,如console.warn则保留

但是需要注意的是,该方法只对ES5语法有效,如果你的代码中涉及ES6就会报错

3. 方式三:terser-webpack-plugin

webpack v5 开箱即带有最新版本的 terser-webpack-plugin。如果你使用的是 webpack v5 或更高版本,同时希望自定义配置,那么仍需要安装 terser-webpack-plugin。如果使用 webpack v4,则必须安装 terser-webpack-plugin v4 的版本。

安装
npm i terser-webpack-plugin@4

terser-webpack-plugin对于清楚console的配置可谓是跟uglifyjs-webpack-plugin一点没差,但是他们最大的差别就是TerserWebpackPlugin支持ES6的语法

4. 方式四:手写loader删除console

终于进入了主题了,朋友们

  1. 什么是loader

众所周知,webpack只能理解js,json等文件,那么除了js,json之外的文件就需要通过loader去顺利加载,因此loader在其中担任的就是翻译工作。loader可以看作一个node模块,实际上就是一个函数,但他不能是一个箭头函数,因为它需要继承webpack的this,可以在loader中使用webpack的方法。

  • 单一原则,一个loader只做一件事

  • 调用方式,loader是从右向左调用,遵循链式调用

  • 统一原则,输入输出都是字符串或者二进制数据

根据第三点,下面的代码就会报错,因为输出的是数字而不是字符串或二进制数据

module.exports = function(source) {
  return 111
}

  1. 新建清除console语句的loader

首先新建一个dropConsole.js文件

// source:表示当前要处理的内容
const reg = /(console.log\()(.*)(\))/g;
module.exports = function(source) {
  // 通过正则表达式将当前处理内容中的console替换为空字符串
  source = source.replace(reg, "")
  // 再把处理好的内容return出去,坚守输入输出都是字符串的原则,并可达到链式调用的目的供下一个loader处理
  return source
}
  1. 在webpack的配置文件中引入

module: {
  rules:[
      {
          test: /\.js/,
          use: [
              {
              loader: path.resolve(__dirname, "./dropConsole.js"),
              options: {
                name: "前端"
              }
              }
          ]
      },
    {
  ]
}

在webpack的配置中,loader的导入需要绝对路径,否则导入失效,如果想要像第三方loader一样引入,就需要配置resolveLoader 中的modules属性,告诉webpack,当node_modules中找不到时,去别的目录下找

module: {
  rules:[
      {
          test: /\.js/,
          use: [
              {
              loader: 'dropConsole',
              options: {
                name: "前端"
              }
              }
          ]
      },
    {
  ]
}
resolveLoader:{
  modules:["./node_modules","./build"] //此时我的loader写在build目录下
},

正常运行后,调试台将不会打印console信息

  1. 最后介绍几种在loader中常用的webpack api

  • this.query:返回webpack的参数即options的对象

  • this.callback:同步模式,可以把自定义处理好的数据传递给webpack

const reg = /(console.log\()(.*)(\))/g;
module.exports = function(source) {
  source = source.replace(reg, "");
  this.callback(null,source);
  // return的作用是让webpack知道loader返回的结果应该在this.callback当中,而不是return中
  return    
}
  • this.async():异步模式,可以大致的认为是this.callback的异步版本,因为最终返回的也是this.callback

const  path = require('path')
const util = require('util')
const babel = require('@babel/core')


const transform = util.promisify(babel.transform)

module.exports = function(source,map,meta) {
var callback = this.async();

transform(source).then(({code,map})=> {
    callback(null, code,map)
}).catch(err=> {
    callback(err)
})
};

最后的最后,webpack博大精深,值得我们好好学习,深入研究!

作者:我也想一夜暴富
来源:https://juejin.cn/post/7038413043084034062

收起阅读 »

uniapp热更新

热更新主要是针对app上线之后页面出现bug,修改之后又得打包,上线,每次用户都得在应用市场去下载很影响用户体验,如果用户不愿意更新,一直提示都不愿意更新,这个bug就会一直存在。 可能你一不小心写错了代码,整个团队的努力都会付之东流,苦不苦,冤不冤,想想都苦...
继续阅读 »



为什么要热更新

热更新主要是针对app上线之后页面出现bug,修改之后又得打包,上线,每次用户都得在应用市场去下载很影响用户体验,如果用户不愿意更新,一直提示都不愿意更新,这个bug就会一直存在。 可能你一不小心写错了代码,整个团队的努力都会付之东流,苦不苦,冤不冤,想想都苦,所以这个时候热更新就显得很重要了。

首先你需要在manifest.json 中修改版本号

如果之前是1.0.0那么修改之后比如是1.0.1或者1.1.0这样

然后你需要在HBuilderX中打一个wgt包

在顶部>发行>原生App-制作移动App资源升级包

包的位置会在控制台里面输出

你需要和后端约定一下接口,传递参数

然后你就可以在app.vue的onLaunch里面编写热更新的代码了,如果你有其他需求,你可以在其他页面的onLoad里面编写。

// #ifdef APP-PLUS  //APP上面才会执行
plus.runtime.getProperty(plus.runtime.appid,
function(widgetInfo) {
uni.request({
url: '请求url写你自己的',
method: "POST",
data: {
version: widgetInfo.version,
//app版本号
name: widgetInfo.name //app名称
},
success: (result) = >{
console.log(result) //请求成功的数据
var data = result.data.data
if (data.update && data.wgtUrl) {
var uploadTask = uni.downloadFile({ //下载
url: data.wgtUrl,
//后端传的wgt文件
success: (downloadResult) = >{ //下载成功执行
if (downloadResult.statusCode === 200) {
plus.runtime.install(downloadResult.tempFilePath, {
force: flase
},
function() {
plus.runtime.restart();
},
function(e) {});
}
},
}) uploadTask.onProgressUpdate((res) = >{
// 测试条件,取消上传任务。
if (res.progress == 100) { //res.progress 上传进度
uploadTask.abort();
}
});
}
}
});
});
// #endif

不支持的情况

  • SDK 部分有调整,比如新增了 Maps 模块等,不可通过此方式升级,必须通过整包的方式升级。

  • 原生插件的增改,同样不能使用此方式。
    对于老的非自定义组件编译模式,这种模式已经被淘汰下线。但以防万一也需要说明下,老的非自定义组件编译模式,如果之前工程没有 nvue 文件,但更新中新增了 nvue 文件,不能使用此方式。因为非自定义组件编译模式如果没有nvue文件是不会打包weex引擎进去的,原生引擎无法动态添加。自定义组件模式默认就含着weex引擎,不管工程下有没有nvue文件。

注意事项

  • 条件编译,仅在 App 平台执行此升级逻辑。

  • appid 以及版本信息等,在 HBuilderX 真机运行开发期间,均为 HBuilder 这个应用的信息,因此需要打包自定义基座或正式包测试升级功能。

  • plus.runtime.version 或者 uni.getSystemInfo() 读取到的是 apk/ipa 包的版本号,而非 manifest.json 资源中的版本信息,所以这里用 plus.runtime.getProperty() 来获取相关信息。

  • 安装 wgt 资源包成功后,必须执行 plus.runtime.restart(),否则新的内容并不会生效。

  • 如果App的原生引擎不升级,只升级wgt包时需要注意测试wgt资源和原生基座的兼容性。平台默认会对不匹配的版本进行提醒,如果自测没问题,可以在manifest中配置忽略提示,详见ask.dcloud.net.cn/article/356…

  • http://www.example.com 是一个仅用做示例说明的地址,实际应用中应该是真实的 IP 或有效域名,请勿直接复制粘贴使用。

关于热更新是否影响应用上架

应用市场为了防止开发者不经市场审核许可,给用户提供违法内容,对热更新大多持排斥态度。

但实际上热更新使用非常普遍,不管是原生开发中还是跨平台开发。

Apple曾经禁止过jspatch,但没有打击其他的热更新方案,包括cordovar、react native、DCloud。封杀jspatch其实是因为jspatch有严重安全漏洞,可以被黑客利用,造成三方黑客可篡改其他App的数据。

使用热更新需要注意:

  • 上架审核期间不要弹出热更新提示

  • 热更新内容使用https下载,避免被三方网络劫持

  • 不要更新违法内容、不要通过热更新破坏应用市场的利益,比如iOS的虚拟支付要老老实实给Apple分钱

如果你的应用没有犯这些错误,应用市场是不会管的。

作者:是一个秃头
来源:https://juejin.cn/post/7039273141901721608

收起阅读 »

GC回收机制与分代回收策略

GC回收机制一、前言垃圾回收:Garbage Collection,简写 GC。JVM 中的垃圾回收器会自动回收无用的对象。但是 GC 自动回收的代价是:当这种自动化机制出错,我们就需要深入理解 GC 回收机制,甚至需要对这些 自动化 的技术实施必要的监控与...
继续阅读 »



GC回收机制

一、前言

垃圾回收Garbage Collection,简写 GCJVM 中的垃圾回收器会自动回收无用的对象。

但是 GC 自动回收的代价是:当这种自动化机制出错,我们就需要深入理解 GC 回收机制,甚至需要对这些 自动化 的技术实施必要的监控与调节。

在虚拟机中,程序计数器、虚拟机栈、本地方法栈 3 个区域随线程而生,随线程而灭,栈中的栈帧随着方法的进入和退出而执行着出栈和入栈操作。所以这几个区域不需要考虑回收的问题。

而在 堆和方法区 中,一个接口中的多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存也可能不一样。这部分的只有在程序运行期间才会知道需要创建哪些对象,这部分的内存的创建和回收是动态的,也是垃圾回收器重点关注的地方。

二、什么是垃圾

垃圾 就是 内存中已经没有用的对象。既然是 垃圾回收,就必须知道哪些对象是垃圾。

Java 虚拟机中使用了一种叫做 可达性分析 的算法 来决定对象是否可以被回收

GCRoot示意图

上图中 A、B、C、D、E 与 GCRoot 直接或间接产生引用链,所以 GC 扫描到这些对象时,并不会执行回收操作;J、K、M虽然之间有引用链,但是并没有与 GCRoot 存在引用链,所以当 GC 扫描到他们时会将他们回收。

注意的是,上图中所有的对象,包括 GCRoot,都是内存中的引用。

作为 GCRoot 的几种对象
  1. Java虚拟机栈(局部变量表)中的引用的对象;

  2. 方法区中静态引用指向的对象;

  3. 仍处于存活状态中的线程对象;

  4. Native方法中 JNI 引用的对象;

三、什么时候回收

不同的虚拟机实现有着不同的 GC 实现机制,但一般情况下都会存在下面两种情况:

  1. Allocation Failure:在堆内存分配中,如果因为可用剩余空间不足导致对象内存分配失败,这时系统会触发一次 GC。

  2. System.gc():在应用层,Java开发工程师可以主动调用此API来请求一次 GC。

四、验证GCRoot的几种情况

在验证之前,先了解Java命令时的参数。

-Xms:初始分配 JVM 运行时的内存大小,如果不指定则默认为物理内存的 1/64

举个小例子

// 表示从物理内存中分配出 200M 空间给 JVM 内存
java -Xms200m HelloWorld
1.验证虚拟机栈(栈帧中的局部变量)中引用的对象作为 GCRoot
// 验证代码
public class GCRootLocalVariable {

  private int _10MB = 10 * 1024 * 1024;
  private byte[] memory = new byte[8 * _10MB];

  public static void main(String[] args) {
      System.out.println("开始时:");
      printMemory();
      method();
      System.gc();
      System.out.println("第二次GC完成");
      printMemory();
  }

  public static void method() {
      GCRootLocalVariable gc = new GCRootLocalVariable();
      System.gc();
      System.out.println("第一次GC完成");
      printMemory();
  }

  // 打印出当前JVM剩余空间和总的空间大小
  public static void printMemory() {
      long freeMemory = Runtime.getRuntime().freeMemory();
      long totalMemory = Runtime.getRuntime().totalMemory();
      System.out.println("剩余空间:" + freeMemory / 1024 / 1024 + "M");
      System.out.println("总共空间:" + totalMemory / 1024 / 1024 + "M");
  }
}
// 打印日志:
开始时:
剩余空间:119M
总共空间:123M
第一次GC完成
剩余空间:40M
总共空间:123M
第二次GC完成
剩余空间:120M
总共空间:123M

从上述代码中可以看到:

第一次打印内存信息,分别为 119M 和 123M;

第二次打印内存信息,分别为 40M 和 123M;剩余空间小了 80M,是因为在 method() 方法中创建了局部变量 gc(位于栈帧中的局部变量),并且这个 gc 对象会被作为 GCRoot。虽然创建的对象未被使用并且调用了 System.gc(),但是因为该方法未结束,所以创建的对象不能被回收。

第三次打印内存信息,分别为 120M 和 123M;method() 方法已经结束,创建的对象 gc 也随方法消失,不再有引用类型指向该 80M 对象。

【值得注意的是】

private int _10MB = 10 * 1024 * 1024;
private byte[] memory = new byte[8 * _10MB];

上面 2 行代码是必须的,如果去掉,那么 3 次打印结果将会一致,idea 也会出现Instantiation of utility class 警告信息,说这个类只存在静态方法,没必要创建这个对象。

这也就是说为什么创建 GCRootLocalVariable() 会需要 80M 的大小,是因为 GCRootLocalVariable 在创建时就会为其内部变量 memory 确定 80M 的大小。

2.验证方法区中的静态变量引用的对象作为 GCRoot
public class GCRootStaticVariable {
  private static int _10M = 10 * 1024 * 1024;
  private byte[] memory;
  private static GCRootStaticVariable staticVariable;

  public GCRootStaticVariable(int size) {
      memory = new byte[size];
  }

  public static void main(String[] args) {
      System.out.println("程序开始:");
      printMemory();
      GCRootStaticVariable g = new GCRootStaticVariable(2 * _10M);
      g.staticVariable = new GCRootStaticVariable(4 * _10M);
      // 将g设置为null,调用GC时可以回收此对象内存
      g = null;
      System.gc();
      System.out.println("GC完成");
      printMemory();
  }

  // 打印JVM剩余空间和总空间
  private static void printMemory() {
      long freeMemory = Runtime.getRuntime().freeMemory();
      long totalMemory = Runtime.getRuntime().totalMemory();
      System.out.println("剩余空间" + freeMemory/1024/1024 + "M");
      System.out.println("总共空间" + totalMemory/1024/1024 + "M");
  }
}

打印结果:
程序开始:
剩余空间119M
总共空间123M
GC完成
剩余空间81M
总共空间123M

通过上述打印结果可知:

  1. 程序刚开始时打印结果为 119M;

  2. 当创建 g 对象时分配 20M 内存,又为静态变量 staticVariable 分配 40M 内存;

  3. 当调用 gc 回收时,非静态变量 memory 分配的 20M 内存被回收;

  4. 但是作为 GCRoot 的静态变量 staticVariable 不会被回收,所以最终打印结果少了 40M 内存。

3.验证活跃线程作为GCRoot
public class GCRootThread {

  private int _10M = 10 * 1024 * 1024;
  private byte[] memory = new byte[8 * _10M];

  public static void main(String[] args) throws InterruptedException {
      System.out.println("程序开始:");
      printMemory();
      AsyncTask asyncTask = new AsyncTask(new GCRootThread());
      Thread thread = new Thread(asyncTask);
      thread.start();
      System.gc();
      System.out.println("main方法执行完成,执行gc");
      printMemory();
      thread.join();
      asyncTask = null;
      System.gc();
      System.out.println("线程代码执行完成,执行gc");
      printMemory();
  }

  private static void printMemory() {
      long freeMemory = Runtime.getRuntime().freeMemory();
      long totalMemory = Runtime.getRuntime().totalMemory();
      System.out.println("剩余内存:" + freeMemory / 1024 / 1024 + "M");
      System.out.println("总共内存:" + totalMemory / 1024 / 1024 + "M");
  }

  private static class AsyncTask implements Runnable {

      private GCRootThread gcRootThread;

      public AsyncTask(GCRootThread gcRootThread) {
          this.gcRootThread = gcRootThread;
      }

      @Override
      public void run() {
          try {
              Thread.sleep(500);
          } catch (InterruptedException e) {
              e.printStackTrace();
          }
      }
  }
}
打印结果:
程序开始:
剩余内存:119M
总共内存:123M
main方法执行完成,执行gc
剩余内存:41M
总共内存:123M
线程代码执行完成,执行gc
剩余内存:120M
总共内存:123M

通过上述打印结果可知:

  1. 程序刚开始时可用内存为 119M;

  2. 第一次调用 gc 时,线程并没有执行结束,并且它作为 GCRoot ,所以它所引用的 80M 内存不会被 GC 回收掉;

  3. thread.join() 保证线程结束后再调用后续代码,所以当第二次调用 GC 时,线程已经执行完毕并被置为 null;

  4. 这时线程已经销毁,所以该线程所引用的 80M 内存被 GC 回收掉。

4.测试成员变量是否可作为GCRoot
public class GCRootClassVariable {
  private static int _10M = 10 * 1024 * 1024;
  private byte[] memory;
  private GCRootClassVariable gcRootClassVariable;

  public GCRootClassVariable(int size) {
      memory = new byte[size];
  }

  public static void main(String[] args) {
      System.out.println("程序开始:");
      printMemory();
      GCRootClassVariable g = new GCRootClassVariable(2 * _10M);
      g.gcRootClassVariable = new GCRootClassVariable(4 * _10M);
      g = null;
      System.gc();
      System.out.println("GC完成");
      printMemory();
  }

  private static void printMemory() {
      long freeMemory = Runtime.getRuntime().freeMemory();
      long totalMemory = Runtime.getRuntime().totalMemory();
      System.out.println("剩余内存:" + freeMemory / 1024 / 1024 + "M");
      System.out.println("总共内存:" + totalMemory / 1024 / 1024 + "M");
  }
}
打印结果:
程序开始:
剩余内存:119M
总共内存:123M
GC完成
剩余内存:121M
总共内存:123M

上述打印结果可知:

  1. 第一次打印结果与第二次打印结果一致:全局变量 gcRootClassVariable 随着 g=null 后被销毁。

  2. 所以全局变量并不能作为 GCRoot。

五、如何回收垃圾(常见的几种垃圾回收算法)

1.标记清除算法(Mark and Sweep GC)

从 “GCRoots” 集合开始,将内存整个遍历一次,保留所有可以被 GCRoots 直接或间接引用到的对象,而剩下的对象都当做垃圾对待并回收。

上述整个过程分为两步:

  1. Mark标记阶段:找到内存中的所有 GC Root 对象,只要是和 GC Root 对象直接或间接相连则标记为灰色(存活对象),否则标记为黑色(垃圾对象)。

  2. Sweep清楚阶段:当遍历完所有的 GC Root 之后,则将标记为垃圾的对象直接清楚。

标记清除算法示意图

标记清除算法优缺点

【优点】

实现简单,不需要将对象进行移动。

【缺点】

需要中断进程内其他组件的执行,并且可能产生内存碎片,提高了垃圾回收的频率。

2.复制算法(Copying)
  1. 将现有的内存空间分为两块,每次只使用其中一块;

  2. 在垃圾回收时将正在使用的内存中的存活对象复制到未被使用的内存块中;

  3. 之后清除正在使用的内存块中的所有对象;

  4. 交换两个内存的角色,完成垃圾回收(目前使用A,B是空闲,算法完成后A为空闲,设置B为使用状态)。

复制算法复制前示意图

复制算法复制后示意图

复制算法优缺点

【优点】

按顺序分配内存即可;实现简单、运行高效,不用考虑内存碎片问题。

【缺点】

可用的内存大小缩小为原来的一半,对象存活率高时会频繁进行复制。

3.标记压缩算法(Mark-Compact)
  1. 需要先从根节点开始对所有可达对象做一次标记;

  2. 之后并不简单地清理未标记的对象,而是将所有的存活对象压缩到内存的一端;

  3. 最后清理边界外所有的空间。

所有,标记压缩也分为两步完成:

  1. Mark标记阶段:找到内存中的所有 GC Root 对象,只要和 GC Root 对象直接或间接相连则标记为灰色(存活对象),否则标记为黑色(垃圾对象)

  2. Compact压缩阶段:将剩余存活对象按顺序压缩到内存的某一端

标记压缩算法示意图

标记压缩算法优缺点

【优点】

避免了碎片产生,又不需要两块相同的内存空间,性价比较高。

【缺点】

所谓压缩操作,仍需要进行局部对象移动,一定程度上还是降低了效率。

分代回收策略

Java 虚拟机根据对象存活的周期不同,把堆内存划分为 新生代老年代,这就是 JVM 的内存分代策略。

注意:在 HotSpot 中除了 新生代老年代,还有 永久代

分代回收的中心思想:对于新创建的对象会在新生代中分配内存,此区域的对象生命周期一般较短,如果经过多次回收仍然存活下来,则将它们转移到老年代中。

一、年轻代

新生成的对象优先存放在新生代中,存活率很低

新生代中,常规应用进行一次垃圾收集一般可以回收 70% ~ 95% 的空间,回收效率很高。所以一般采用的 GC 回收算法是 复制算法

新生代也可细分3部分:Eden、Survivor0(简称 S0)、Survivor1(简称 S1),这 3 部分按照 8:1:1 的比例来划分新生代。

新生代老年代示意图

新生成的对象会存放在 Eden 区。

新生代老年代示意图

当 Eden 区满时,会触发垃圾回收,回收掉垃圾之后,将剩下存活的对象存放到 S0 区。当下一次 Eden 区满时,再次触发垃圾回收,这时会将 Eden 区 和 S0 区存活的对象全部复制到 S1 区,并清空 Eden 区和 S0 区。

新生代老年代示意图

上述步骤重复 15 次之后,依然存活下来的对象存放到 老年区

二、老年代

一个对象如果在新生代存活了足够长的时间而没有被清理掉,则会被复制到老年代。

老年代的内存大小一般比新生代大,能存放更多的对象。如果对象比较大(比如长字符串或大数组),并且新生代的剩余空间不足,则这个大对象会直接被分配到老年代上。

可以使用 -XX:PretenureSizeThreshold 来控制直接升入老年代的对象大小。

因为老年代对象的生命周期较长,不需要过多的复制操作,所以一般采用标记压缩的回收算法。

【注意的是】

有这么一种情况,老年代中的对象会引用新生代中的对象,这时如果要执行新生代的 GC,则可能要查询整个老年代引用新生代的情况,这种效率是极低的。所以老年代中维护了一个 512byte 的 table,所有老年代对象引用新生代对象的引用都记录在这里。这样新生代 GC 时只需要查询这个表即可。

三、GC log分析

为了让上层应用开发人员更加方便调试 Java 程序,JVM 提供了相应的 GC 日志,在 GC 执行垃圾回收事件中,会有各种相应的 log 被打印出来。

新生代和老年代打印的日志是有区别的:

【新生代GC:轻GC】这一区域的 GC 叫做 Minor GC。因为 Java 对象大多都具备朝生夕灭的特性,所以 Minor GC 非常频繁,一般回收速度比较快。

【老年代GC:重GC】发生在这一区域的 GC 叫做 Major GC 或者 Full GC,当出现 Major GC,经常会伴随至少一次 Minor GC

Major GCFull GC 在有些虚拟机中还是有区别的:前者是仅回收老年代中的垃圾对象,后者是回收整个堆中的垃圾对象。

常用的 GC 命令参数
命令参数功能描述
-verbose:gc显示 GC 的操作内容
-Xms20M初始化堆大小为 20M
-Xmx20M设置堆最大分配内存 20M
-Xmn10M设置新生代的内存大小为 10M
-XX:+PrintGCDetails打印GC的详细log日志
-XX:SurvivorRatio=8新生代中 Eden 区域与 Survivor 区域的大小比值为 8:1:1

添加 VM Options 参数:分配堆内存 20M,10M给新生代,10M给老年代

// VM args: -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8
public class MinorGCTest {

  private static final int _1M = 1024 * 1024;

  public static void main(String[] args) {
      byte[] a, b, c, d;
      a = new byte[2 * _1M];
      b = new byte[2 * _1M];
      c = new byte[2 * _1M];
      d = new byte[_1M];
  }
}
打印结果:(这里测试是第二次修改后的运行效果)
[GC (Allocation Failure) [PSYoungGen: 7820K->840K(9216K)] 7820K->6992K(19456K), 0.0072302 secs] [Times: user=0.02 sys=0.00, real=0.01 secs]
[Full GC (Ergonomics) [PSYoungGen: 840K->0K(9216K)] [ParOldGen: 6152K->6759K(10240K)] 6992K->6759K(19456K), [Metaspace: 3198K->3198K(1056768K)], 0.0087734 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
Heap
PSYoungGen     total 9216K, used 1190K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
eden space 8192K, 14% used [0x00000000ff600000,0x00000000ff7298d8,0x00000000ffe00000)
from space 1024K, 0% used [0x00000000ffe00000,0x00000000ffe00000,0x00000000fff00000)
to   space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000)
ParOldGen       total 10240K, used 6759K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
object space 10240K, 66% used [0x00000000fec00000,0x00000000ff299cd0,0x00000000ff600000)
Metaspace       used 3205K, capacity 4496K, committed 4864K, reserved 1056768K
class space   used 351K, capacity 388K, committed 512K, reserved 1048576K

上述字段意思代表如下:

字段代表含义
PSYoungGen新生代
eden新生代中的 Eden 区
from新生代中的 S0 区
to新生代中的 S1 区
ParOldGen老年代
  1. 第一次运行效果后,因为 Eden 区 8M,S0 和 S1 各 1M。所以 a、b、c、d 共有 7M 空间都会在 Eden 区。

  2. 修改 d = new byte[2 * _1M],再次运行;

  3. JVM 会将 a/b/c 存放到 Eden 区,Eden 占有 6M 空间,无法再分配 2M 空间给 d;

  4. 因此会执行一次轻 GC,并尝试将 a/b/c 复制到 S1 区;

  5. 但是因为 S1 区只有 1M 空间,所以没办法存储 a/b/c 三者任一对象。

  6. 这种情况下,JVM 将 a/b/c 转移到老年代,将 d 保存在 Eden 区。

【最终结果】

Eden区 占用 2M 空间(d),老年代占用 6M 空间(a,b,c)

四、引用

通过 GC Roots 的引用可达性来判断对象是否存活,JVM 中的引入关系有以下四种:

引用英文名GC回收机制使用示例
强引用Strong Reference如果一个对象具有强引用,那么垃圾回收期绝不会回收它Object obj = new Object();
软引用Soft Reference在内存实在不足时,会对软引用进行回收SoftReference softObj = new SoftReference();
弱引用Weak Reference第一次GC回收时,如果垃圾回收器遍历到此弱引用,则将其回收WeakReference weakObj = new WeakReference();
虚引用Phantom Reference一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来获取一个对象的实例不会使用
软引用的用法
public class SoftReferenceNormal {

  static class SoftObject {
      byte[] data = new byte[120 * 1024 * 1024]; // 120M
  }

  public static void main(String[] args) {
      SoftReference<SoftObject> softObj = new SoftReference<>(new SoftObject());
      System.out.println("第一次GC前,软引用:" + softObj.get());
      System.gc();
      System.out.println("第一次GC后,软引用:" + softObj.get());
      SoftObject obj = new SoftObject();
      System.out.println("分配100M强引用,软引用:" + softObj.get());
  }
}

添加 VM Option 参数:-Xmx200M 给堆内存分配最大200M内存

第一次 GC 前,软引用:SoftReferenceNormal$SoftObject@1b6d3586
第一次 GC 后,软引用:SoftReferenceNormal$SoftObject@1b6d3586
分配 100M 强引用,软引用:null
  1. 添加参数后位堆内存分配最大 200M 空间,分配给 softObj 对象 120M。

  2. 第一次 GC 后,因为剩余内存任然够,所以软引用并没有被回收。

  3. 当分配 100M 强引用后,堆内存空间不够,会触发GC回收,回收掉软引用。

软引用隐藏的问题

【注意】

被软引用对象关联的对象会自动被垃圾回收器回收,但是软引用对象本身也是一个对象,这些创建的软引用并不会自动被垃圾回收器回收掉。

public class SoftReferenceTest {

  static class SoftObject {
      byte[] data = new byte[1024]; // 占用1k空间
  }

  private static final int _100K = 100 * 1024;
  // 静态集合保存软引用,会导致这些软引用对象本身无法被垃圾回收器回收
  private static Set<SoftReference<SoftObject>> cache = new HashSet<>(_100K);

  public static void main(String[] args) {
      for (int i = 0; i < _100K; i++) {
          SoftObject obj = new SoftObject();
          cache.add(new SoftReference(obj));
          if (i * 10000 == 0) {
              System.out.println("cache size is " + cache.size());
          }
      }
      System.out.println("END");
  }
}

添加 VM Option 参数:-Xms4m -Xmx4m -Xmn2m

// 打印结果:
cache size is 1
Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded
at SoftReferenceTest$SoftObject.<init>(SoftReferenceTest.java:8)
at SoftReferenceTest.main(SoftReferenceTest.java:17)

程序崩溃,崩溃的原因并不是堆内存溢出,而是超出了 GC 开销限制。

这里错误的原因是:JVM 不停的回收软引用中的对象,回收次数过快,回收内存较小,占用资源过高了。

【解决方案】注册一个引用队列,将这个对象从 Set 中移除掉。

public class SoftReferenceTest {

  static class SoftObject {
      byte[] data = new byte[1024]; // 占用1k空间
  }

  private static final int _100K = 100 * 1024;
  // 静态集合保存软引用,会导致这些软引用对象本身无法被垃圾回收器回收
  private static Set<SoftReference<SoftObject>> cache = new HashSet<>(_100K);
  // 解决方案:注册一个引用队列,将要移除的对象从中删除
  private static ReferenceQueue<SoftObject> queue = new ReferenceQueue<>();
  // 记录清空次数
  private static int removeReferenceIndex = 0;

  public static void main(String[] args) {
      for (int i = 0; i < _100K; i++) {
          SoftObject obj = new SoftObject();
          cache.add(new SoftReference(obj, queue));
          // 清除掉软引用
          removeSoft();
          if (i * 10000 == 0) {
              System.out.println("cache size is " + cache.size());
          }
      }
      System.out.println("END removeReferenceIndex: " + removeReferenceIndex);
  }

  private static void removeSoft() {
      Reference<? extends SoftObject> poll = queue.poll();
      while (poll != null) {
          if (cache.remove(poll)) {
              removeReferenceIndex++;
          }
          poll = queue.poll();
      }
  }
}
// 打印结果:
cache size is 1
END removeReferenceIndex: 101745

作者:沅兮
来源:https://juejin.cn/post/7037330678731505672


收起阅读 »

swift 键盘收起

iOS
直接调用就能收起键盘,无需调用其他方法        UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), t...
继续阅读 »







直接调用就能收起键盘,无需调用其他方法    

    UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)

收起阅读 »

iOS 底层原理探索 之 结构体内存对齐

iOS
写在前面: iOS底层原理探究是本人在平时的开发和学习中不断积累的一段进阶之 路的。 记录我的不断探索之旅,希望能有帮助到各位读者朋友。 目录如下:iOS 底层原理探索之 alloc以上内容的总结专栏iOS 底层原理探索 之 阶段总结准备Objective-C...
继续阅读 »


写在前面: iOS底层原理探究是本人在平时的开发和学习中不断积累的一段进阶之
路的。 记录我的不断探索之旅,希望能有帮助到各位读者朋友。

目录如下:

  1. iOS 底层原理探索之 alloc

以上内容的总结专栏


准备

Objective-C ,通常写作ObjC或OC,是扩充C的面向对象编程语言。它主要适用于Mac OS X 和 GNUstep者两个使用OpenStep标准的系统,而在NeXTSTEP和OpenStep中它更是基本语言。

GCC和Clang含Objective-C的编译器,Objective-C可以在GCC以及Clang运作的系统上编译。

我们平时开发用的Objective-C语言,编译后最终会转化成C/C++语言。

为什么要研究结构体的内存对齐呢? 因为作为一名iOS开发人员,随着对于底层的不断深入探究,我们都知道,所有的对象在底层中都是一个结构体。那么结构体的内存空间又会被系统分配多少空间,这个问题,值得我们一探究竟。

首先,从大神Cooci那里盗取了一张各数据类型占用的空间大小图片,作为今天探究结构体内存对齐原理的依据。

image.png

当我们创建一个对象的时候,我们并不需要过多的在意属性的顺序,因为系统会帮我们做优化处理。但是,在创建结构体的时候,就需要我们去分析了,因为这个时候系统并不会帮助我们做优化。

接下来,我们看下面两个结构体:

struct Struct1 {    
double a;
char b;
int c;
short d;
char e;
}struct1;

struct Struct2 {
double a;
int b;
char c;
short d;
char e;
}struct2;


两个结构体拥有的数据类型是相同的,按照图片中double 是8字节, char 是1字节, int 是4字节,short 是2字节, 那么 两个结构体应该是占 16字节的内存空间,也就是分配16字节空间即可,然而,我们看下面的结果:

    printf("%lu--%lu", sizeof(struct1), sizeof(struct2));
------------
24--16

那么,这就是有问题的了,两个拥有相同数据类型的结构体,被系统分配到的内存空间是不一样的,这是为什么呢?今天的重点就是这里,结构体的

内存对齐原则:

1:数据成员对⻬规则:结构(struct)(或联合(union))的数据成员,第
一个数据成员放在offset为0的地方,以后每个数据成员存储的起始位置
要从该成员大小或者成员的子成员大小(只要该成员有子成员,比如说是
数组,结构体等)的整数倍开始(比如int为4字节,则要从4的整数倍地址
开始存储。

2:结构体作为成员:如果一个结构里有某些结构体成员,则结构体成员要从
其内部最大元素大小的整数倍地址开始存储。(struct a里存有struct b,
b里有char,int ,double等元素,那b应该从8的整数倍开始存储)

3:收尾工作:结构体的总大小,也就是sizeof的结果,必须是其内部最大成员
的整数倍,不足的要补⻬。

那么,我们按照以上内存对齐原则再来分析下 struct1 和 struct2 :


struct Struct1 { /// 18 --> 24
double a; //8 [0 1 2 3 4 5 6 7]
char b; //1 [8 ]
int c; //4 [9 [12 13 14 15]
short d; //2 [16 17]
char e; //1 [18]
}struct1;


struct Struct2 { /// 16 --> 16
double a; //8 [0 1 2 3 4 5 6 7]
int b; //4 [8 9 10 11]
char c; //1 [ 12 ]
short d; //2 [14 15]
char e; // 1 [16]
}struct2;


接着,我们看下下面的结构体

struct Struct3 {    
double a;
int b;
char c;
short d;
int e;
struct Struct1 str;
}struct3;


打印输出结果为 48 ,分析如下:

    double a;           //8 [0 1 2 3 4 5 6 7]
int b; //4 [8 9 10 11]
char c; //1 [12]
short d; //2 [ 14 15 ]
int e; //4 [ 16 17 18 19]
struct Struct1 str; //24 [24 ... 47]

所以,struct3 大小为48。


猜想:内存对齐的收尾工作中的内部最大成员指的是什么的大小呢?

接下来我们来一一验证一下

struct LGStruct4 {          /// 40 --> 48 
double a; //8 [0 1 2 3 4 5 6 7]
int b; //4 [8 9 10 11]
char c; //1 [12]
short d; //2 [14 15]
int e; //4 [16 17 18 19]
struct Struct2 str; //16 [24 ... 39]
}struct4;

按照我对于内存对齐原则中收尾工作的理解, 最终的大小 应该是 Struct2 的 大小 16 的整数倍 也就是 48 才对。然而, 结果却是:

    NSLog(@"%lu", sizeof(struct4));
--------
SMObjcBuild[8076:213800] 40

对,是40你没有看错,这样的话,很显然,我理解的就是错误的, 结构体内部最大成员应该指的是这里的 double,那么我们接下来验证一下: 1、

struct Struct2 {    ///16
double a; //8 [0 1 2 3 4 5 6 7]
int b; //4 [8 9 10 11]
char c; //1 [ 12 ]
short d; //2 [14 15]
}struct2;

struct LGStruct4 { /// 24

short d; //2 [0 1]

struct Struct2 str; // 16 [8 ... 23]

}struct4;

结果是 :24


因为,结构体内部最大成员是 double也就是8;并不是按照 LGStruct4中的str长度为16的整数倍来计算,所以最后的结果是24。

总结

结构体内部最大成员指的是结构体内部的数据类型,所以,结构体内包含结构体的时候,并不是按照内部的结构体长度的整数倍来计算的哦。


收起阅读 »

iOS 底层原理探索 之 alloc

iOS
iOS 底层原理探索 之 alloc写在前面: iOS底层原理探究是本人在平时的开发和学习中不断积累的一段进阶之 路的。 记录我的不断探索之旅,希望能有帮助到各位读者朋友。 内容的总结专栏iOS 底层原理探索 之 阶段总结序作为一名iOS开发人员,在平时开发工...
继续阅读 »

iOS 底层原理探索 之 alloc

写在前面: iOS底层原理探究是本人在平时的开发和学习中不断积累的一段进阶之
路的。 记录我的不断探索之旅,希望能有帮助到各位读者朋友。


内容的总结专栏


作为一名iOS开发人员,在平时开发工作中,所有的对象我们使用最多的是alloc来创建。那么alloc底层做了哪些操作呢?接下来我会一步一步探究alloc方法的底层实现。

初探

我们先来看下面的代码

    SMPerson *p1 = [SMPerson alloc];
SMPerson *p2 = [p1 init];
SMPerson *p3 = [p1 init];

NSLog(@"%@-%p-%p", p1, p1, &p1);
NSLog(@"%@-%p-%p", p2, p2, &p2);
NSLog(@"%@-%p-%p", p3, p3, &p3);

打印内容:

    <SMPerson: 0x600000710400>-0x600000710400-0x7ffee6f15088
<SMPerson: 0x600000710400>-0x600000710400-0x7ffee6f15080
<SMPerson: 0x600000710400>-0x600000710400-0x7ffee6f15078

可见,在 SMPerson 使用 alloc 方法从系统中申请开辟内存空间后 init方法并没有对内存空间做任何的处理,地址指针的创建来自于 alloc方法。如下所示:

地址.001.jpeg

注:细心的你一定注意到了,p1、p2、p3都是相差了8个字节。 这是因为,指针占内存空间大小为8字节,p1、p2、p3 都是从栈内存空间上申请的,且栈内存空间是连续的。同时,他们都指向了同一个内存地址。

那么, alloc 是如何开辟内存空间的呢?

首先,第一反应是,我们要Jump to Definition,

2241622899100_.pic_hd.jpg

结果,Xcode中并不能直接跳转后显示其底层实现,所以 并不是我们想要的。

2251622899278_.pic_hd.jpg

WX20210605-214250@2x.png

中探

接下来,我们通过三种方法来一探究竟:

方法1

既然不可以直接跳转到API文档来查看alloc的内部实现,那么我们还可以通过下 符号断点 来探寻 其实现原理。

WX20210605-212725@2x.png

接下来我们就来到此处

WX20210605-213213@2x.png

一个名为 libobjc.A.dylib 的库,至此,我们就应该要去找苹果开源的库,以寻找我们想要的答案。

点击查看苹果开源源码汇总

方法2

我们也可以直接在alloc那一行打一个断点,代码运行到此处后,按住control键 点击 step into, 接下来,就来到里这里

WX20210605-214413@2x.png 我们可以看到一个 objc_alloc 的函数方法到调用,此时,我们再下一个符号断点,同样的,我们还是找到了 libobjc.A.dylib 这个库。

WX20210605-215027@2x.png

方法3

此外,我们还是可以通过汇编来调试和查找相应的实现内容,断点依然是在alloc那一行。

Debug > Debug Workflow > Always Show Disassembly

WX20210605-215336@2x.png

找到 callq 方法调用那一行, WX20210605-215715@2x.png

接着, step into 进去, 我们找到了 objc_alloc 的调用, 之后的操作和 方法2的后续步骤一样,最终,可以找到 libobjc.A.dylib 这个库。 WX20210605-215732@2x.png

深探

下载源码 objc4-818.2

接下来对源码进行分析,

alloc方法会调用到此处

WX20210605-231454@2x.png

接着是 调用 _objc_rootAlloc

WX20210605-231517@2x.png

之后调用 到 callAlloc

WX20210605-231545@2x.png

跟着断点会来到 _objc_rootAllocWithZone

WX20210605-231647@2x.png

之后是 _class_createInstanceFromZone

此方法是重点

WX20210605-231758@2x.png

_class_createInstanceFromZone 方法中,该方法就是一个类初始化所走的流程,重点的地方有三处

第一处是:
    // 计算出开辟内存空间大小
size = cls->instanceSize(extraBytes);

内部实现如下: WX20210605-231838@2x.png 其中在计算内存空间大小时,会调用 cache.fastInstanceSize(extraBytes) 方法,

最终会调用 align16(size + extra - FAST_CACHE_ALLOC_DELTA16) 方法。 align16 的实现如下:

static inline size_t align16(size_t x) {
return (x + size_t(15)) & ~size_t(15);
}

可见, 系统会进行 16字节 的对齐操作,也就是说,一个对象所占用的内存大小至少是16字节。

在这里 我们举个例子: size_t x = 8; 那么 align16操作后的大小计算过程如下:

    (8 + 15) & ~15;

0000 0000 0000 1000 8
0000 0000 0000 1111 15

= 0000 0000 0001 0111 23
1111 1111 1111 0000 ~15

= 0000 0000 0001 0000 16


第二处是:
    ///向系统申请开辟内存空间,返回地址指针;
obj = (id)calloc(1, size);

第三处是:
    /// 将类和指针做绑定
obj->initInstanceIsa(cls, hasCxxDtor);

总结:

所以,最后我们总结一下, alloc的底层调用流程如下:

alloc流程.001.jpeg

就是这样一个流程,系统就帮我们创建出来一个类对象。

补充

image.png

  • lldb 如何打印实力对象中成员为 double 类型的数值: e -f f -- <值>
收起阅读 »

String还有长度限制?是多少?

前言 话说Java中String是有长度限制的,听到这里很多人不禁要问,String还有长度限制?是的有,而且在JVM编译中还有规范,而且有的家人们在面试的时候也遇到了。 String 首先要知道String的长度限制我们就需要知道String是怎么存储字符串...
继续阅读 »

前言


话说Java中String是有长度限制的,听到这里很多人不禁要问,String还有长度限制?是的有,而且在JVM编译中还有规范,而且有的家人们在面试的时候也遇到了。


String


首先要知道String的长度限制我们就需要知道String是怎么存储字符串的,String其实是使用的一个char类型的数组来存储字符串中的字符的。



那么String既然是数组存储那数组会有长度的限制吗?是的有限制,但是是在有先提条件下的,我们看看String中返回length的方法。



由此我们看到返回值类型是int类型,Java中定义数组是可以给数组指定长度的,当然不指定的话默认会根据数组元素来指定:


int[] arr1 = new int[10]; // 定义一个长度为10的数组
int[] arr2 = {1,2,3,4,5}; // 那么此时数组的长度为5
复制代码

整数在java中是有限制的,我们通过源码来看看int类型对应的包装类Integer可以看到,其长度最大限制为2^31 -1,那么说明了数组的长度是0~2^31-1,那么计算一下就是(2^31-1 = 2147483647 = 4GB)



看到这我们尝试通过编码来验证一下上述观点。



以上是我通过定义字面量的形式构造的10万个字符的字符串,编译之后虚拟机提示报错,说我们的字符串长度过长,不是说好了可以存21亿个吗?为什么才10万个就报错了呢?


其实这里涉及到了JVM编译规范的限制了,其实JVM在编译时,如果我们将字符串定义成了字面量的形式,编译时JVM是会将其存放在常量池中,这时候JVM对这个常量池存储String类型做出了限制,接下来我们先看下手册是如何说的。



常量池中,每个 cp_info 项的格式必须相同,它们都以一个表示 cp_info 类型的单字节 “tag”项开头。后面 info[]项的内容 由tag 的类型所决定。



我们可以看到 String类型的表示是 CONSTANT_String ,我们来看下CONSTANT_String具体是如何定义的。



这里定义的 u2 string_index 表示的是常量池的有效索引,其类型是CONSTANT_Utf8_info 结构体表示的,这里我们需要注意的是其中定义的length我们看下面这张图。



在class文件中u2表示的是无符号数占2个字节单位,我们知道1个字节占8位,2个字节就是16位 ,那么2个字节能表示的范围就是2^16- 1 = 65535 。范中class文件格式对u1、u2的定义的解释做了一下摘要:


#这里对java虚拟机规摘要部分


##1、class文件中文件内容类型解释


定义一组私有数据类型来表示 Class 文件的内容,它们包括 u1,u2 和 u4,分别代 表了 1、2 和 4 个字节的无符号数。


每个 Class 文件都是由 8 字节为单位的字节流组成,所有的 16 位、32 位和 64 位长度的数 据将被构造成 2 个、4 个和 8 个 8 字节单位来表示。


##2、程序异常处理的有效范围解释


start_pc 和 end_pc 两项的值表明了异常处理器在 code[]数组中的有效范围。


start_pc 必须是对当前 code[]数组中某一指令的操作码的有效索引,end_pc 要 么是对当前 code[]数组中某一指令的操作码的有效索引,要么等于 code_length 的值,即当前 code[]数组的长度。start_pc 的值必须比 end_pc 小。


当程序计数器在范围[start_pc, end_pc)内时,异常处理器就将生效。即设 x 为 异常句柄的有效范围内的值,x 满足:start_pc ≤ x < end_pc


实际上,end_pc 值本身不属于异常处理器的有效范围这点属于 Java 虚拟机历史上 的一个设计缺陷:如果 Java 虚拟机中的一个方法的 code 属性的长度刚好是 65535 个字节,并且以一个 1 个字节长度的指令结束,那么这条指令将不能被异常处理器 所处理。


不过编译器可以通过限制任何方法、实例初始化方法或类初始化方法的code[]数组最大长度为 65534,这样可以间接弥补这个 BUG。



注意:这里对个人认为比较重要的点做了标记,首先第一个加粗说白了就是说数组有效范围就是【0-65565】但是第二个加粗的地方又解释了,因为虚拟机还需要1个字节的指令作为结束,所以其实真正的有效范围是【0-65564】,这里要注意这里的范围仅限编译时期,如果你是运行时拼接的字符串是可以超出这个范围的。



接下来我们通过一个小实验来测试一下我们构建一个长度为65534的字符串,看看是否就能编译通过。0期阶段汇总


首先通过一个for循环构建65534长度的字符串,在控制台打印后,我们通过自己度娘的一个在线字符统计工具计算了一下确实是65534个字符,如下:




然后我们将字符复制后以定义字面量的形式赋值给字符串,可以看到我们选择这些字符右下角显示的确实是65534,于是乎运行了一波,果然成功了。




#看到这里我们来总结一下:


##字符串有长度限制吗?是多少?


首先字符串的内容是由一个字符数组 char[] 来存储的,由于数组的长度及索引是整数,且String类中返回字符串长度的方法length() 的返回值也是int ,所以通过查看java源码中的类Integer我们可以看到Integer的最大范围是2^31 -1,由于数组是从0开始的,所以数组的最大长度可以使【0~2^31】通过计算是大概4GB。


但是通过翻阅java虚拟机手册对class文件格式的定义以及常量池中对String类型的结构体定义我们可以知道对于索引定义了u2,就是无符号占2个字节,2个字节可以表示的最大范围是2^16 -1 = 65535。


其实是65535,但是由于JVM需要1个字节表示结束指令,所以这个范围就为65534了。超出这个范围在编译时期是会报错的,但是运行时拼接或者赋值的话范围是在整形的最大范围。


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

ASM字节码插桩

ASM字节码插桩 一、什么是插桩 QQ空间曾经发布的热修复解决方案中利用Javaassist库实现向类的构造函数中插入一段代码解决CLASS_ISPREVERIFIED 问题。包括了Instant Run的实现以及参照Instant Run实现的热修复美团Ro...
继续阅读 »

ASM字节码插桩


一、什么是插桩


QQ空间曾经发布的热修复解决方案中利用Javaassist库实现向类的构造函数中插入一段代码解决CLASS_ISPREVERIFIED 问题。包括了Instant Run的实现以及参照Instant Run实现的热修复美团Robus等都利用到了插桩技术。


插桩就是将一段代码插入或者替换原本的代码。字节码插桩顾名思义就是在我们编写的源码编译成字节码(Class)后,在Android下生成dex之前修改Class文件,修改或者增强原有代码逻辑的操作。


插桩前.png


插桩后.png


我们需要查看方法执行耗时,如果每一个方法都需要自己手动去加入这些内容,当不需要时也需要一个个删去相应的代码。1个、两个方法还好,如果有10个、20个得多麻烦!所以可以利用注解来标记需要插桩的方法,结合编译后操作字节码来帮助我们自动插入,当不需要时关掉插桩即可。这种AOP思想让我们只需要关注插桩代码本身。


二、字节码操作框架


上面我们提到QQ空间使用了Javaassist来进行字节码插桩,除了Javaassist之外还有一个应用更为广泛的ASM框架同样也是字节码操作框架,Instant Run包括AspectJ就是借助ASM来实现各自的功能。


我们非常熟悉的JSON格式数据是基于文本的,我们只需要知道它的规则就能够轻松的生成、修改JSON数据。同样的Class字节码也有其自己的规则(格式)。操作JSON可以借助GSON来非常方便的生成、修改JSON数据。而字节码Class,同样可以借助Javassist/ASM来实现对其修改。



字节码操作框架的作用在于生成或者修改Class文件,因此在Android中字节码框架本身是不需要打包进入APK的,只有其生成/修改之后的Class才需要打包进入APK中。它的工作时机在上图Android打包流程中的生成Class之后,打包dex之前。


三、ASM的使用


由于ASM具有相对于Javassist更好的性能以及更高的灵活行,我们这篇文章以使用ASM为主。在真正利用到Android中之前,我们可以先在Java程序中完成对字节码的修改测试。


3.1、在AS中引入ASM


ASM可以直接从jcenter()仓库中引入,所以我们可以进入:bintray.com/进行搜索



点击图中标注的工件进入,可以看到最新的正式版本为:7.1。



因此,我们可以在AS中加入:


引入ASM.png


同时,需要注意的是:我们使用testImplementation引入,这表示我们只能在Java的单元测试中使用这个框架,对我们Android中的依赖关系没有任何影响。



AS中使用gradle的Android工程会自动创建Java单元测试与Android单元测试。测试代码分别在test与androidTest。



3.2、准备待插桩Class


test/java下面创建一个Java类:


public class InjectTest {

public static void main(String[] args) {

}
}
</pre>

由于我们操作的是字节码插桩,所以可以进入test/java下面使用javac对这个类进行编译生成对应的class文件。


javac InjectTest.java

3.3、执行插桩


因为main方法中没有任何输出代码,我们输入命令:java InjectTest执行这个Class不会有任何输出。那么我们接下来利用ASM,向main方法中插入一开始图中的记录函数执行时间的日志输出。


在单元测试中写入测试方法


<pre spellcheck="false" lang="java" cid="n37" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;"> /**
* 1、准备待分析的class
*/
FileInputStream fis = new FileInputStream
("xxxxx/test/java/InjectTest.class");

/**
* 2、执行分析与插桩
*/
//class字节码的读取与分析引擎
ClassReader cr = new ClassReader(fis);
// 写出器 COMPUTE_FRAMES 自动计算所有的内容,后续操作更简单
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
//分析,处理结果写入cw EXPAND_FRAMES:栈图以扩展格式进行访问
cr.accept(new ClassAdapterVisitor(cw), ClassReader.EXPAND_FRAMES);


/**
* 3、获得结果并输出
*/
byte[] newClassBytes = cw.toByteArray();
File file = new File("xxx/test/java2/");
file.mkdirs();

FileOutputStream fos = new FileOutputStream
("xxx/test/java2/InjectTest.class");
fos.write(newClassBytes);

fos.close();</pre>

关于ASM框架本身的设计,我们这里先不讨论。上面的代码会获取上一步生成的class,然后由ASM执行完插桩之后,将结果输出到test/java2目录下。其中关键点就在于第2步中,如何进行插桩。


把class数据交给ClassReader,然后进行分析,类似于XML解析,分析结果会以事件驱动的形式告知给accept的第一个参数ClassAdapterVisitor


<pre spellcheck="false" lang="java" cid="n41" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">public class ClassAdapterVisitor extends ClassVisitor {

public ClassAdapterVisitor(ClassVisitor cv) {
super(Opcodes.ASM7, cv);
}

@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature,
String[] exceptions) {
System.out.println("方法:" + name + " 签名:" + desc);

MethodVisitor mv = super.visitMethod(access, name, desc, signature,
exceptions);
return new MethodAdapterVisitor(api,mv, access, name, desc);
}
}</pre>

分析结果通过ClassAdapterVisitor获得,一个类中会存在方法、注解、属性等,因此ClassReader会将调用ClassAdapterVisitor中对应的visitMethodvisitAnnotationvisitField这些visitXX方法。


我们的目的是进行函数插桩,因此重写visitMethod方法,在这个方法中我们返回一个MethodVisitor方法分析器对象。一个方法的参数、注解以及方法体需要在MethodVisitor中进行分析与处理。


<pre spellcheck="false" lang="java" cid="n45" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">package com.enjoy.asminject.example;

import com.enjoy.asminject.ASMTest;

import org.objectweb.asm.AnnotationVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Type;
import org.objectweb.asm.commons.AdviceAdapter;
import org.objectweb.asm.commons.Method;

/**
* AdviceAdapter: 子类
* 对methodVisitor进行了扩展, 能让我们更加轻松的进行方法分析
*/
public class MethodAdapterVisitor extends AdviceAdapter {

private boolean inject;

protected MethodAdapterVisitor(int api, MethodVisitor methodVisitor, int access, String name, String descriptor) {
super(api, methodVisitor, access, name, descriptor);
}


/**
* 分析方法上面的注解
* 在这里干嘛???
* <p>
* 判断当前这个方法是不是使用了injecttime,如果使用了,我们就需要对这个方法插桩
* 没使用,就不管了。
*
* @param desc
* @param visible
* @return
*/
@Override
public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
if (Type.getDescriptor(ASMTest.class).equals(desc)) {
System.out.println(desc);
inject = true;
}
return super.visitAnnotation(desc, visible);
}

private int start;

@Override
protected void onMethodEnter() {
super.onMethodEnter();
if (inject) {
//执行完了怎么办? 记录到本地变量中
invokeStatic(Type.getType("Ljava/lang/System;"),
new Method("currentTimeMillis", "()J"));

start = newLocal(Type.LONG_TYPE); //创建本地 LONG类型变量
//记录 方法执行结果给创建的本地变量
storeLocal(start);
}
}

@Override
protected void onMethodExit(int opcode) {
super.onMethodExit(opcode);
if (inject){
invokeStatic(Type.getType("Ljava/lang/System;"),
new Method("currentTimeMillis", "()J"));
int end = newLocal(Type.LONG_TYPE);
storeLocal(end);

getStatic(Type.getType("Ljava/lang/System;"),"out",Type.getType("Ljava/io" +
"/PrintStream;"));

//分配内存 并dup压入栈顶让下面的INVOKESPECIAL 知道执行谁的构造方法创建StringBuilder
newInstance(Type.getType("Ljava/lang/StringBuilder;"));
dup();
invokeConstructor(Type.getType("Ljava/lang/StringBuilder;"),new Method("<init>","()V"));


visitLdcInsn("execute:");
invokeVirtual(Type.getType("Ljava/lang/StringBuilder;"),new Method("append","(Ljava/lang/String;)Ljava/lang/StringBuilder;"));

//减法
loadLocal(end);
loadLocal(start);
math(SUB,Type.LONG_TYPE);


invokeVirtual(Type.getType("Ljava/lang/StringBuilder;"),new Method("append","(J)Ljava/lang/StringBuilder;"));
invokeVirtual(Type.getType("Ljava/lang/StringBuilder;"),new Method("toString","()Ljava/lang/String;"));
invokeVirtual(Type.getType("Ljava/io/PrintStream;"),new Method("println","(Ljava/lang/String;)V"));

}
}
}</pre>

MethodAdapterVisitor继承自AdviceAdapter,其实就是MethodVisitor 的子类,AdviceAdapter封装了指令插入方法,更为直观与简单。


上述代码中onMethodEnter进入一个方法时候回调,因此在这个方法中插入指令就是在整个方法最开始加入一些代码。我们需要在这个方法中插入long s = System.currentTimeMillis();。在onMethodExit中即方法最后插入输出代码。


<pre spellcheck="false" lang="java" cid="n48" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">@Override
protected void onMethodEnter() {
super.onMethodEnter();
if (inject) {
//执行完了怎么办? 记录到本地变量中
invokeStatic(Type.getType("Ljava/lang/System;"),
new Method("currentTimeMillis", "()J"));

start = newLocal(Type.LONG_TYPE); //创建本地 LONG类型变量
//记录 方法执行结果给创建的本地变量
storeLocal(start);
}
}</pre>

这里面的代码怎么写?其实就是long s = System.currentTimeMillis();这句代码的相对的指令。我们可以先写一份代码


<pre spellcheck="false" lang="java" cid="n50" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">void test(){
//插入的代码
long s = System.currentTimeMillis();
/**
* 方法实现代码....
*/
//插入的代码
long e = System.currentTimeMillis();
System.out.println("execute:"+(e-s)+" ms.");
}</pre>

然后使用javac编译成Class再使用javap -c查看字节码指令。也可以借助插件来查看,就不需要我们手动执行各种命令。


插件安装.png


安装完成之后,可以在需要插桩的类源码中点击右键:


查看字节码.png


点击ASM Bytecode Viewer之后会弹出


字节码.png


所以第20行代码:long s = System.currentTimeMillis();会包含两个指令:INVOKESTATICLSTORE


再回到onMethodEnter方法中


<pre spellcheck="false" lang="java" cid="n59" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">@Override
protected void onMethodEnter() {
super.onMethodEnter();
if (inject) {
//invokeStatic指令,调用静态方法
invokeStatic(Type.getType("Ljava/lang/System;"),
new Method("currentTimeMillis", "()J"));
//创建本地 LONG类型变量
start = newLocal(Type.LONG_TYPE);
//store指令 将方法执行结果从操作数栈存储到局部变量
storeLocal(start);
}
}</pre>

而`onMethodExit`也同样根据指令去编写代码即可。最终执行完插桩之后,我们就可以获得修改后的class数据。

四、Android中的实现


在Android中实现,我们需要考虑的第一个问题是如何获得所有的Class文件来判断是否需要插桩。Transform就是干这件事情的。


相关视频


Android项目实战 微信Matrix卡顿监控方案,函数自动埋点实践


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

Flutter 单例的实现

和谐学习!不急不躁!!我是你们的老朋友小青龙~ 前言 回顾iOS,单例的写法如下: static JXWaitingView *shared; +(JXWaitingView*)sharedInstance{ static dispatch_once_t...
继续阅读 »

和谐学习!不急不躁!!我是你们的老朋友小青龙~


前言


回顾iOS,单例的写法如下:


static JXWaitingView *shared;

+(JXWaitingView*)sharedInstance{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
shared=[[JXWaitingView alloc]initWithTitle:nil];
});
return shared;
}

其目的是通过dispatch_once来控制【初始化方法】只会执行一次,然后用static修饰的对象来接收并返回它。所以核心是只会执行一次初始化


创建单例


创建单例的案例


class Student {
String? name;
int? age;
//构造方法
Student({this.name, this.age});

// 单例方法
static Student? _dioInstance;
static Student instanceSingleStudent() {
if (_dioInstance == null) {
_dioInstance = Student();
}
return _dioInstance!;
}
}

测试单例效果


测试一


import 'package:flutter_async_programming/Student.dart';

void main() {
Student studentA = Student.instanceSingleStudent();
studentA.name = "张三";
Student studentB = Student.instanceSingleStudent();
print('studentA姓名是${studentA.name}');
print('studentB姓名是${studentB.name}');
}

运行效果


image.png


测试二


import 'package:flutter_async_programming/Student.dart';

void main() {
Student studentA = Student.instanceSingleStudent();
studentA.name = "张三";
Student studentB = Student.instanceSingleStudent();
studentB.name = "李四";
print('studentA姓名是${studentA.name}');
print('studentB姓名是${studentB.name}');
}

运行效果


image.png


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

拒绝编译等待 - 动态研发模式 ARK

iOS
背景 pod install 时间长:编译优化绝大部分任务放在了 CocoaPods 上,CocoaPods 承担了更多工作,执行时间因此变长。编译时间长:虽然现阶段绝大部分工程已经从源码编译转型成二进制编译,但编译耗时依旧在十分钟左右,且现有工程基础上已无更...
继续阅读 »



背景

iOS 业界研发模式多为 CocoaPods + Xcode + Git 的多仓组件化开发模型。为追求极致的研发体验、提升研发效率,对该研发模式进行了大量优化,但目前遇到了以下瓶颈,亟需突破:

  • pod install 时间长:编译优化绝大部分任务放在了 CocoaPods 上,CocoaPods 承担了更多工作,执行时间因此变长。

  • 编译时间长:虽然现阶段绝大部分工程已经从源码编译转型成二进制编译,但编译耗时依旧在十分钟左右,且现有工程基础上已无更好优化手段。

  • 超大型工程通病:Xcode Index 慢、爆内存、甚至卡死,链接时间长。

如何处理这些问题?

究其本质,产生这些问题的原因在于工程规模庞大。据此我们停下了对传统模式各节点的优化工作,以"缩小工程规模"为切入点,探索新型研发模式——动态研发模式 ARK。

ARK[1] 是全链路覆盖的动态研发模式,旨在保证工程体验的前提下缩小工程规模:通过基线构建的方式,提供线下研发所需物料;同时通过实时的动态库转化技术,保证本地研发仅需下载和编译开发仓库。

Show Case

动态研发模式本地研发流程图如下。接下来就以抖音产品为例,阐述如何使用 ARK 做一次本地开发。
演示基于字节跳动本地研发工具 MBox[2]

  1. 仓库下载

ARK 研发模式下,本地研发不再拉取主仓代码,取而代之的是 ARK 仓库。ARK 仓库含有与主仓对应的所有配置,一次适配接入后期不需要持续维护。

相较传统 APP 仓库动辄几个 GB 的大小,ARK 仓库贯彻了缩减代码规模这一概念。仓库仅有应用配置信息,不包含任何组件代码。ARK 仓库大小仅 2 MB,在 1 s 内可以完成仓库下载 。

在 MBox 中的使用仅需几步点击操作。首先选择要开发的产品,然后勾选 ark 模式,选择开发分支,最后点击 Create 便可以数秒完成仓库下载。

  1. 开发组件

CocoaPods 下进行组件开发一般是将组件仓库下载到本地,修改 Podfile 对应组件 A 为本地引用 pod A, :path =>'./A' ,之后进行本地开发。而在 MBox 和 ARK 的研发流程中,仅需选择要开发的组件点击 Add 便可进行本地开发。

动态研发模式 ARK 通过解析 Podfile.lock 支持了 Checkout From Commit 功能,该功能根据宿主的组件依赖信息自动拉取相应的组件版本到本地,带来便捷性的同时也保证了编译成功率。

  1. pod install

传统研发模式下 pod install 必须要经历 解析 Podfile 依赖、下载依赖、创建 Pods.xcodeproj 工程、集成 workspace 四个步骤,其中依赖解析和下载依赖两个步骤尤为耗时。

ARK 研发模式下 Podfile 中没有组件,因此依赖解析、下载依赖这两个环节耗时几乎为零。其次由于工程中仅需开发组件步骤中添加的组件,在创建 Pods 工程、集成工程这两个环节中代码规模的降低,对提升集成速度的效果非常显著。

没有依赖信息,编译、链接阶段显然不能成功。ARK 解决方案通过自研 cocoapods-ark 及配套工具链来保证编译、链接、运行的成功,其原理后续会在系列文章中介绍。

  1. 开发组件编译&调试

和传统模式一样通过 Xcode 打开工程的 xcworkspace ,即可正常开发、调试完整的应用。

工程中仅保留开发组件,但是依然有变量、函数、头文件跳转能力;参与 Index、编译的规模变小,Xcode 几乎不存在 loading 状态,大型工程也可以秒开;编译速度大幅提升。在整个动态研发流程中,通过工具链将组件从静态库转化成动态库,链接时间明显缩短。

  1. 查看全源码

ARK 工程下默认只有开发组件的源码,查看全源码是开发中的刚需。动态研发流程提供了 pod doc 异步命令实现该能力,此命令可以在开发时执行,命令执行完成后重启工程即可通过 Document Target 查看工程中其他组件源码。

pod doc 优点:

  • 支持异步和同步,执行过程中不影响本地开发。

  • 执行指令时跳过依赖解析环节,从服务端获取依赖信息,下载源码。

  • 通过 xcodegen 异步生成 Document 工程,大幅降低 pod install 时间。

  • 仅复用 pod installer 中的资源下载、缓存模块。

  • 支持仓库统一鉴权,自动跳过无权限组件仓库。

收益

体验上: 与传统模式开发流程一致,零成本切换动态研发模式。

工具上: 站在巨人的肩膀上,CocoaPods 工具链相关优化在 ARK 同样生效。

时间上: 传统研发模式中,历经各项优化后虽然能将全链路开发时间控制在 20 分钟左右,但这样的研发体验依旧不够友好。开发非常容易在这个时间间隔内被其他事情打断,良好的研发体验应该是连贯的。结合本地开发体验我们认为,一次连贯的开发体验应该将工程集成时间控制在分钟级,当前研发模式成功做到了这一点,将全链路开发时间控制在 5 分钟以内。

成功率: 成功率一直是研发效率中容易被忽视的一个指标。据不完全数据统计,集团内应用的本地全量编译成功率不足五成。一半的同学会在首次编译后执行重试。显然,对于工程新手来说就是噩梦,这意味着很长时间将在这一环节中浪费。而 ARK 从平台基线到本地工具链贯彻 Sandbox 的理念,整体上提高编译成功率。

写在最后

ARK 当前已经在字节跳动内部多业务落地使用。从初期技术方案的探索到实际落地应用,遇到了很多技术难点,也对研发模式有了新的思考。

相关技术文章将陆续分享,敬请期待。

扩展阅读

[1] ARK: https://github.com/kuperxu/KwaiTechnologyCommunication/blob/master/5.WWDC-ARK.pdf
[2] MBox: https://mp.weixin.qq.com/s/5_IlQPWnCug_f3SDrnImCw

作者:字节跳动终端技术——徐纪光
来源:https://blog.csdn.net/YZcoder/article/details/121374743


收起阅读 »

手把手带你,优化一个滚动时流畅的TableView

iOS
手把手带你,优化一个滚动时流畅的TableView这是我参与8月更文挑战的第7天,活动详情查看:8月更文挑战我的专栏iOS 底层原理探索iOS 底层原理探索 之 阶段总结意识到我的问题平时使用手机的时间不算少,每天阅读新闻的时候会感觉到新闻类的app优化的还是...
继续阅读 »

手把手带你,优化一个滚动时流畅的TableView

这是我参与8月更文挑战的第7天,活动详情查看:8月更文挑战


我的专栏

  1. iOS 底层原理探索
  2. iOS 底层原理探索 之 阶段总结

意识到我的问题

平时使用手机的时间不算少,每天阅读新闻的时候会感觉到新闻类的app优化的还是很好的,TableView的Cell滚动的时候不会去加载显示图片内容,当一次滑动结束之后,Cell上的新闻图片便开始逐个的加载显示出来,所以整个滑动的过程是很流畅的。这中体验也是相当nice的。

我最开始的做法

开发中TableView的使用是非常值频繁的,当TableViewCell上需要加载图片的时候,是一件比较头疼的事。因为,用户一边滑动TableView,TableView需要一边从网络获取图片。之前的操作都是放在 cellForRowAtIndexPath 中来处理,这就导致用户在滑动TableView的时候,会特别的卡(尤其是滑动特别快时),而且,手机的CPU使用率也会飙的非常的高。对于用户来说,这显然是一个十分糟糕的体验。

糟糕的图片显示 代码

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {

ImageTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell"];
cell.index = indexPath;

NSMutableDictionary *info = [self.dataSource objectAtIndex:cell.index.row];

NSString *url = [info objectForKey: @"img" ];
NSData *iData = [NSData dataWithContentsOfURL:[NSURL URLWithString: url ]];
cell.img.image = [UIImage imageWithData:iData];
cell.typeL.text = [NSString stringWithFormat:@"%ld-%ld", cell.index.section, cell.index.row];

return cell;
}

糟糕的手机CPU飙升率

未命名.gif

糟糕的用户滑动体验

未命名1.gif

不只是用户,对于开发这来讲,这也是不可以接受的体验。

平时接触并使用的app也非常的多,发现他们多处理方式就是,当用户滑动列表的时候,不再加载图片,等用户的滑动结束之后,会开始逐一的加载图片。这是非常好的优化思路,减轻了CPU的负担,也不会基本不会让用户感觉到页面滚动时候的卡顿。这也就是最开始我描述的我看新闻app的使用体验。

收到这个思路的启发,我们开始着手将上面糟糕的体验作一下优化吧。

总结思路开启优化之路

那么,带着这个优化思路,我开始了对于这个TableView 的优化。

  • 首先,我们只加载当前用户可以看到的cell上的图片。
  • 其次,我们一次只加载一张图片。

要完成以上两点,图片的加载显示就不能在cellForRowAtIndexPath中完成,我们要定义并实现一个图片的加载显示方法,以便在合适的时机,调用刷新内容显示。

loadSeeImage 加载图片的优化

#pragma mark load Images
- (void)loadSeeImage {

//记录本次加载的几张图片
NSInteger loadC = 0;

// 用户可以看见的cells
NSArray *cells = [self.imageTableView visibleCells];

// 调度组
dispatch_group_t group = dispatch_group_create();

for (int i = 0; i < cells.count; i++) {

ImageTableViewCell *cell = [cells objectAtIndex:i];

NSMutableDictionary *info = [self.dataSource objectAtIndex:cell.index.row];
NSString *url = [info objectForKey: @"img" ];

NSString *data = [info objectForKey:@"data"];

if ([data isKindOfClass:[NSData class]]) {


}else {

// 添加调度则到我们的串行队列中去
dispatch_group_async(group, self.loadQueue, ^{

NSData *iData = [NSData dataWithContentsOfURL:[NSURL URLWithString: url ]];
NSLog(@" load image %ld-%ld ", cell.index.section, cell.index.row);
if (iData) {
// 缓存
[info setValue:@"1" forKey:@"isload"];
[info setValue:iData forKey:@"data"];
}
NSString *isload = [info objectForKey:@"isload"];

if ([isload isEqualToString:@"0"]) {

dispatch_async(dispatch_get_main_queue(), ^{

cell.img.image = [UIImage imageNamed:@""];
}); }else {

if (iData) {

dispatch_async(dispatch_get_main_queue(), ^{
//显示加载后的图片
cell.img.image = [UIImage imageWithData:iData];
});
}
}

});

if (i == cells.count - 1) {

dispatch_group_notify(group, dispatch_get_main_queue(), ^{
// 全部加载完毕的通知
NSLog(@"load finished");
});
}

loadC += 1;
}
}

NSLog(@"本次加载了 %ld 张图片", loadC);
}

其次就是 loadSeeImage 调用时机的处理,我们要做到用户在滑动列表之后加载,就是在下面两处加载:

- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView   {  

[self loadSeeImage];
}

- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate {

if (scrollView.isDragging || scrollView.isDecelerating || scrollView.isTracking) {
return;
}
[self loadSeeImage];
}

当然,首次进入页面,列表数据加载完毕后,我们也要加载一次图片的哦。 好的下面看下优化后的结果:

优化xcode.gif

优化phone.gif

CPU占用率比之前最高的时候降低了一半多,app在滑动的时候也没有明显卡顿的地方。 完美。

收起阅读 »

面向 JavaScript 开发人员的 5 大物联网库

最近元宇宙的概念火遍互联网,自 Facebook 改名 Meta 以来,国内外越来越多的企业纷纷加入到布局元宇宙的行列。元宇宙之所以强势进入人们视野,与其底层技术的进步有着莫大的关系,包括AR/VR、云计算、物联网、5G、人工智能、数字孪生等等。其中,5G作为...
继续阅读 »


最近元宇宙的概念火遍互联网,自 Facebook 改名 Meta 以来,国内外越来越多的企业纷纷加入到布局元宇宙的行列。元宇宙之所以强势进入人们视野,与其底层技术的进步有着莫大的关系,包括AR/VR、云计算、物联网、5G、人工智能、数字孪生等等。其中,5G作为重要的连接基础,是元宇宙场景得以实现的关键。元宇宙将汇集游戏引擎、AR可穿戴设备、VR、现实世界数据集和不断发展的物联网。

物联网(英语:InternetofThings,简称 IoT)是一种计算设备、机器、数码机器之间相互联系的系统,它拥有一种统一的统一识别代码(UID),并且能够在网络上传送数据,不需要人与人、或人与设备之间的交互。

作为一个前端工程师(JavaScript工程师),似乎觉得这一切有点模式,其实不然,现代 JavaScript 的可以使用的场景越来越多,包括物联网,在本文中,将介绍可以在 JavaScript 代码中用于连接设备的 5 个脚本库。

1. Cylon.js

官方网站: https://cylonjs.com/

Cylon.js 是用于机器人、物理计算和物联网 (IoT) 的流行 JavaScript 框架之一。不仅仅是一个“物联网”库,它还是一个完整的机器人框架,支持超过 43 个不同的平台,这是与机器连接的 43 种不同的地方或方式,目前支持的机器人和物理计算系统及软件平台有Arduino、Beaglebone Black、BLE、Disispark、Intel Galileo and Edison、Intel IoT Analytics、OpenCV、Octobl、Raspberry Pi、Salesforce等。

可以使用 Cylon.js 连接到关键字并侦听它或 Arduino 板发送的事件,或者提供一个 HTTP API 接口并通过那里获取数据(它们也支持 socket.ioMQTT)。想通过 JavaScript 控制无人机吗?这并非不可以,首先需要安装:

npm install cylon cylon-firmata cylon-gpio cylon-i2c

然后运行一个这样的小脚本, 参考文章:

npm install cylon cylon-ardrone

然后运行脚本:

const Cylon = require("cylon");

Cylon.robot({
  connections: {
      ardrone: { adaptor: "ardrone", port: "192.168.1.1" },
  },

  devices: {
      drone: { driver: "ardrone" },
  },

  work: function (my) {
      my.drone.takeoff();
      after((10).seconds(), function () {
          my.drone.land();
      });
      after((15).seconds(), function () {
          my.drone.stop();
      });
  },
}).start();

如果有设备可以试试。 Cylon.js 的工作方式是允许其他人通过插件的方式提供连接器,这意味着这个库提供的功能没有限制。最重要的是,文档本身非常详细,写得很好,完整的代码示例。

2. IoT.js

官方网站: https://iotjs.net/

IoT.js 是一个用 JavaScript 编写的物联网 (IoT) 框架。它旨在基于网络技术在物联网世界中提供一个可互操作的服务平台。

如果希望在一个连接的设备中执行一些物联网(而不是在一个强大的、充满资源的服务器中的接收端),那么可能需要针对该环境进行优化。这个 IoT 框架运行在 JerryScript 引擎之上, JerryScript 引擎是一个针对小型设备优化的 JavaScript 运行时。这意味着,虽然无法使用最先进的 JS 的全部功能,但确实可以使用:

  • 完全支持 ECMAScript 5.1 语法。

  • 低内存消耗优化

  • 能够将 JS 代码预编译为字节码

但是,兼容平台的数量没有 Cylon.js 多,而 IoT.js 只兼容:

关于他们的文档,这应该是衡量一个库有多好的标准之一。他们有一些基本的例子和入门指南。但可能就是这样了。考虑到 IoT.js 是一个底层的硬件接口,现在看起来它希望开发人员已经有使用其他产品的经验,而不是针对JS开发人员寻求进入物联网。

3. Johnny-Five

官方网站: http://johnny-five.io/

Johnny Five 是流行的 JavaScript 机器人和物联网平台之一。由 Bocoup 于 2012 年开发的 Johnny Five 一个开源的、基于 Firmata 协议的物联网和机器人编程框架,是 JavaScript 开发人员可用的最古老的机器人和物联网平台之一,从那时起,它的功能和兼容性都在不断增长。

Johnny Five 支持 Arduino(所有型号)、Electric Imp、Beagle Bone、Intel Galileo & Edison、Raspberry Pi 等。该平台可轻松与流行的应用程序库(如 Express.js 和 Socket.io)以及物联网框架(如 Octoblu)结合使用。

他们的文档非常详细,充满了关于硬件连接的示例和图表,这是一个很好的学习资源。

4. NodeRed

官方网站: https://nodered.org/

NodeRed 是建立在 Node.js 之上,是一个基于流的编程工具,最初由 IBM 的新兴技术服务团队开发,现在是 JS 基金会的一部分。该平台允许在部署之前从浏览器以图形方式设置数据流和工作流。在理想的情况下,不需要编写任何代码,也许设置一些平台凭据。 NodeRed 还充当和其他人共享他们创建的流程的中心化平台,这是防止每次都重新创建轮子的好方法,即使没有真正编写代码。

5. Zetta

官方网站: https://www.zettajs.org/

ZettaJS 是一个基于 Node.js 构建的开源平台,用于创建跨地理分布式计算机和云运行的物联网服务器。是另一种通过 JavaScript 与远程设备交互的方式。这里的主要区别在于 ZettaJS 的目标是将每个设备都变成一个 API,这是将 IoT 泛化为一个通用概念的好方法。如今,设备及其接口的数量正在爆炸增长,但没有对其进行规范控制。 ZettaJS 正试图在这方面进行改进,通过非常直观的编码方式,可以简单地为设备安装驱动程序,并在其中启用公共接口,并通过代码与它们交互。

6. 总结

通过上面介绍,JavaScript 不仅限于浏览器,甚至不限于基于 API 的后端开发,还可以随心所欲地从设备中提取数据或从设备中提取数据,并使用几乎完全相同的语言来控制它。

作者:天行无忌
来源:https://blog.51cto.com/devpoint/4762760

收起阅读 »

给团队做个分享,用30张图带你快速了解TypeScript

正文30张脑图常见的基本类型我们知道TS是JS的超集,那我们先从几种JS中常见的数据类型说起,当然这些类型在TS中都有相应的,如下:特殊类型除了一些在JS中常见的类型,也还有一些TS所特有的类型类型断言和类型守卫如何在运行时需要保证和检测来自其他地方的数据也符...
继续阅读 »

正文

30张脑图

常见的基本类型

我们知道TSJS的超集,那我们先从几种JS中常见的数据类型说起,当然这些类型在TS中都有相应的,如下:

1常见的基本类型.png

特殊类型

除了一些在JS中常见的类型,也还有一些TS所特有的类型

2特殊类型.png

类型断言和类型守卫

如何在运行时需要保证和检测来自其他地方的数据也符合我们的要求,这就需要用到断言,而断言需要类型守卫

3类型断言.png

接口

接口本身只是一种规范,里头定义了一些必须有的属性或者方法,接口可以用于规范functionclass或者constructor,只是规则有点区别

4TS中的接口.png

类和修饰符

JS一样,类class出现的目的,其实就是把一些相关的东西放在一起,方便管理

TS主要也是通过class关键字来定义一个类,并且它还提供了3个修饰符

5类和修饰符.png

类的继承和抽象类

TS中的继承ES6中的类的继承极其相识,子类可以通过extends关键字继承一个类

但是它还有抽象类的概念,而且抽象类作为基类,不能new

6.0类的继承和抽象类.png

泛型

将泛型理解为宽泛的类型,它通常用于类和函数

但不管是用于类还是用于函数,核心思想都是:把类型当一种特殊的参数传入进去

7泛型.png

类型推断

TS中是有类型推论的,即在有些没有明确指出类型的地方,类型推论会帮助提供类型

8类型推断.png

函数类型

为了让我们更容易使用,TS为函数添加了类型等

9函数.png

数字枚举和字符串枚举

枚举的好处是,我们可以定义一些带名字的常量,而且可以清晰地表达意图或创建一组有区别的用例

TS支持数字的和基于字符串的枚举

10枚举.png

类型兼容性

TS里的类型兼容性是基于结构子类型的 11类型兼容性.png

联合类型和交叉类型

补充两个TS的类型:联合类型和交叉类型

12联合类型和交叉类型.png

for..of和for..in

TS也支持for..offor..in,但你知道他们两个主要的区别吗

13forin和forof.png

模块

TS的模块化沿用了JS模块的概念,模块是在自身的作用域中执行,在一个模块里的变量,函数,类等等在模块外部是不可见的,除非你明确地使用export形式之一导出它们

14模块.png

命名空间的使用

使用命名空间的方式,其实非常简单,格式如下: namespace X {}

15命名空间的使用.png

解决单个命名空间过大的问题

16解决单个命名空间过大的问题.png

简化命名空间

要简化命名空间,核心就是给常用的对象起一个短的名字

TS中使用import为指定的符号创建一个别名,格式大概是:import q = x.y.z

17简化命名空间.png

规避2个TS中命名空间和模块的陷阱

18陷阱.png

模块解析流程

模块解析是指编译器在查找导入模块内容时所遵循的流程

流程大致如下:

image.png

相对和非相对模块导入

相对和非相对模块导入主要有以下两点不同

image.png

Classic模块解析策略

TS的模块解析策略,其中的一种就叫Classic

21Classic模块解析策略.png

Node.js模块解析过程

为什么要说Node.js模块解析过程,其实是为了讲TS的另一种模块解析策略做铺垫---Node模块解析策略。

因为Node模块解析策略就是一种试图在运行时模仿Node.js模块解析的策略

22Node.js的模块解析过程.png

Node模块解析策略

Node模块解析策略模仿Node.js运行时的解析策略来在编译阶段定位模块定义文件的模块解析的策略,但是跟Node.js会有点区别

23Node模块解析策略.png

声明合并之接口合并

声明合并指的就是编译器会针对同名的声明合并为一个声明

声明合并包括接口合并,接口的合并需要区分接口里面的成员有函数成员和非函数成员,两者有差异

24接口合并.png

合并命名空间

命名空间的合并需要分两种情况:一是同名的命名空间之间的合并,二是命名空间和其他类型的合并

25合并命名空间.png

JSX模式

TS具有三种JSX模式:preservereactreact-native

26JSX.png

三斜线指令

三斜线指令其实上面有讲过,像/// <reference>

它的格式就是三条斜线后面跟一个标签

27三斜线指令.png


作者:LBJ
链接:https://juejin.cn/post/7036266588227502093

收起阅读 »

我去!爬虫遇到字体反爬,哭了

今天准备爬取某某点评店铺信息时,遇到了『字体』反爬。比如这样的: 还有这样的: 可以看到这些字体已经被加密(反爬) 竟然遇到这种情况,那辰哥就带大家如何去解决这类反爬(字体反爬类) 01 网页分析在开始分析反爬之前,先简单的介绍一下背景(爬取的网页) 辰...
继续阅读 »

今天准备爬取某某点评店铺信息时,遇到了『字体』反爬。比如这样的:


img

还有这样的:


img

可以看到这些字体已经被加密反爬


竟然遇到这种情况,那辰哥就带大家如何去解决这类反爬(字体反爬类


01 网页分析

在开始分析反爬之前,先简单的介绍一下背景(爬取的网页)


img

辰哥爬取的某某点评的店铺信息。一开始查看网页源码是这样的


img

这种什么也看不到,咱们换另一种方式:通过程序直接把整个网页源代码保存下来


img

获取到的网页源码如下:


img

比如这里看到评论数(4位数)都有对应着一个编号(相同的数字编号相同),应该是对应着网站的字体库


下一步,我们需要找到这个网站的字体库。


02 获取字体库

这里的字体库建议在目标网站里面去获取,因为不同的网站的字体库是不一样,导致解码还原的字体也会不一样。


1、抓包获取字体库


img

在浏览器network里面可以看到一共有三种字体库。(三种字体库各有不同的妙用,后面会有解释


img

把字体库链接复制在浏览器里面打开,就可以把字体库下载到本地。


2、查看字体库


这里使用FontCreator的工具查看字体库。


下载地址:


https://www.high-logic.com/font-editor/fontcreator/download

这里需要注册,邮箱验证才能下载,不过辰哥已经下载了,可以在公众号回复:FC,获取安装包。


安装之后,把刚刚下载的字体库在FontCreator中打开


img

可以看到字体的内容以及对应的编号


比如数字7对应F399数字8对应F572 ,咱们在原网页和源码对比,是否如此???


img

可以看到,真是一模一样对应着解码就可以还原字体。


3、为什么会有三个字体库


img

在查看加密字体的CSS样式时,方式有css内容是这样的


img

字体库1:d35c3812.woff 对应解码class为 shopNum


字体库2:084c9fff.woff 对应解码class为 reviewTag和address


字体库3:73f5e6f3.woff 对应解码class为 tagName


也就是说,字体所属的不同class标签,对应的解密字体库是不一样的,辰哥这里不得不说一句:太鸡贼了


img

咱们这里获取的评论数,clas为shopNum,需要用到字体库d35c3812.woff


03 代码实现解密

1、加载字体库


既然我们已经知道了字体反爬的原理,那么我们就可以开始编程实现解密还原。


加载字体库的Python库包是:fontTools ,安装命令如下:


pip install fontTools

img

将字体库的内容对应关系保存为xml格式


img

code和name是一一对应关系


img

img

可以看到网页源码中的编号后四位对应着字体库的编号。


因此我们可以建立应该字体对应集合


img

建立好映射关系好,到网页源码中去进行替换


img

img

这样我们就成功的将字体反爬处理完毕。后面提取内容大家基本都没问题。


2、完整代码


img

输出结果:


img

可以看到加密的数字全部都还原了。


04 小结

辰哥在本文中主要讲解了如此处理字体反爬问题,并以某某点评为例去实战演示分析。辰哥在文中处理的数字类型,大家可以尝试去试试中文如何解决。


作者:Python研究者
来源:https://juejin.cn/post/6970933428145356831

收起阅读 »

js实现放大镜

借助宽高等比例放大的两张图片,结合js中鼠标偏移量、元素偏移量、元素自身宽高等属性完成;左侧遮罩移动Xpx,右侧大图移动X*倍数px;其余部分就是用小学数学算一下就OK了。JS // 获取小图和遮罩、大图、大盒子    var small ...
继续阅读 »



先看效果图

实现原理

借助宽高等比例放大的两张图片,结合js中鼠标偏移量、元素偏移量、元素自身宽高等属性完成;左侧遮罩移动Xpx,右侧大图移动X*倍数px;其余部分就是用小学数学算一下就OK了。

HTML和CSS

 <div class="wrap">
   
   <div id="small">
     <img src="img/1.jpg" alt="" >
     <div id="mark">div>
   div>
   
   <div id="big">
     <img src="img/2.jpg" alt="" id="bigimg">
   div>
 div>
* {
    margin: 0;
    padding: 0;
  }
  .wrap {
    width: 1500px;
    margin: 100px auto;
  }

  #small {
    width: 432px;
    height: 768px;
    float: left;
    position: relative;
  }

  #big {
    /* background-color: seagreen; */
    width: 768px;
    height: 768px;
    float: left;
    /* 超出取景框的部分隐藏 */
    overflow: hidden;
    margin-left: 20px;
    position: relative;
    display: none;
  }

  #bigimg {
    /* width: 864px; */
    position: absolute;
    left: 0;
    top: 0;
  }

  #mark {
    width: 220px;
    height: 220px;
    background-color: #fff;
    opacity: .5;
    position: absolute;
    left: 0;
    top: 0;
    /* 鼠标箭头样式 */
    cursor: move;
    display: none;
  }

JS

 // 获取小图和遮罩、大图、大盒子
   var small = document.getElementById("small")
   var mark = document.getElementById("mark")
   var big = document.getElementById("big")
   var bigimg = document.getElementById("bigimg")
   // 在小图区域内获取鼠标移动事件;遮罩跟随鼠标移动
   small.onmousemove = function (e) {
     // 得到遮罩相对于小图的偏移量(鼠标所在坐标-小图相对于body的偏移-遮罩本身宽度或高度的一半)
     var s_left = e.pageX - mark.offsetWidth / 2 - small.offsetLeft
     var s_top = e.pageY - mark.offsetHeight / 2 - small.offsetTop
     // 遮罩仅可以在小图内移动,所以需要计算遮罩偏移量的临界值(相对于小图的值)
     var max_left = small.offsetWidth - mark.offsetWidth;
     var max_top = small.offsetHeight - mark.offsetHeight;
     // 遮罩移动右侧大图也跟随移动(遮罩每移动1px,图片需要向相反对的方向移动n倍的距离)
     var n = big.offsetWidth / mark.offsetWidth
     // 遮罩跟随鼠标移动前判断:遮罩相对于小图的偏移量不能超出范围,超出范围要重新赋值(临界值在上边已经计算完成:max_left和max_top)
     // 判断水平边界
     if (s_left < 0) {
       s_left = 0
    } else if (s_left > max_left) {
       s_left = max_left
    }
     //判断垂直边界
     if (s_top < 0) {
       s_top = 0
    } else if (s_top > max_top) {
       s_top = max_top
    }
     // 给遮罩left和top赋值(动态的?因为e.pageX和e.pageY为变化的量),动起来!
     mark.style.left = s_left + "px";
     mark.style.top = s_top + "px";
     // 计算大图移动的距离
     var levelx = -n * s_left;
     var verticaly = -n * s_top;
     // 让图片动起来
     bigimg.style.left = levelx + "px";
     bigimg.style.top = verticaly + "px";
  }
   // 鼠标移入小图内才会显示遮罩和跟随移动样式,移出小图后消失
   small.onmouseenter = function () {
     mark.style.display = "block"
     big.style.display= "block"
  }
   small.onmouseleave = function () {
     mark.style.display = "none"
     big.style.display= "none"
  }

总结

  • 鼠标焦点一旦动起来,它的偏移量就是动态的;父元素和子元素加上定位后,通过动态改变某个元素的lefttop值来实现“动”的效果。

  • 大图/小图=放大镜(遮罩)/取景框

  • 两张图片一定要等比例缩放

作者:Onion韩
来源:https://juejin.cn/post/7030963292818374670

收起阅读 »

从谷歌一行代码学到的姿势

网上很流行的一行代码,据说是谷歌工程师写的,它的作用是给页面所有元素增加一个随机颜色的外边框。[].forEach.call($$("*"),function(a){a.style.outline="1px solid #"+(~~(Math.random()...
继续阅读 »

网上很流行的一行代码,据说是谷歌工程师写的,它的作用是给页面所有元素增加一个随机颜色的外边框

[].forEach.call($$("*"),function(a){a.style.outline="1px solid #"+(~~(Math.random()*(1<<24))).toString(16)})

运行效果如下图:

这个代码虽然只有一行,但是包含的知识点不少,网上有很多解析。我也说下自己的理解,然后最后推荐在实务中使用TreeWalker对象进行遍历。

我的理解其中主要包含如下4个知识点:

1. [].forEach.call
2. $$("*")
3. a.style.outline
4. (~~(Math.random()*(1<<24))).toString(16)

1 [].forEach.call

1.1 [].forEach

forEach是数组遍历的一个方法,接收一个函数参数用来处理每一个遍历的元素,常规的使用姿势是:

let arr = [3, 5, 8];
arr.forEach((item) => {
console.log(item);
})
// 控制台输出:
// 3
// 5
// 8

那么下面的写法:

[].forEach

只是为了得到 forEach 这个方法,这个方法是定义都在Array.prototype上的方法,[] 表示空数组,可以访问到数组原型对象上的方法。

得到 forEach 这个方法后,就可以通过 call 发起调用。

1.2 call

call函数用来调用一个函数,和普通调用不同,call调用可以修改函数内this指向。

常规调用函数的姿势:

let object1 = {
id: 1,
printId() {
console.log(this.id)
}
}
object1.printId();
// 控制台输出:
// 1

因为是正常调用,方法内的this指向object1对象,所以上例输出1。

使用call调用printId方法,并传入另外一个对象object2:

let object2 = {
id: 2
}
object1.printId.call(object2);
// 控制台输出:
// 2

这里使用call调用object1.printId函数,传入了object2对象,那么printId函数内的this就是指向object2这个对象,所以结果输出2。

1.3 综合分析

综合来看:

[].forEach.call( $$("*"), function(a){} )

这行代码的意思就是遍历如下对象:

$$("*") 

然后用如下方法处理每个元素:

function(a){}

其中,a就是遍历的的每一个元素。

那么

$$("*") 

指什么呢?我们接着往后看。

2 $$("*")

这个写法用来获取页面所有元素,相当于

document.querySelectorAll('*')

只是

$$("*") 

只能在浏览器开发控制台内使用,这个是浏览器开发控制台提供出来的预定义API,至于为什么,大家可以参考底部的参考文章。

3 a.style.outline

设置元素边框,估计很多人都知道,但是设置外边框就比较少人了解了,外边框的效果和边框类似,唯一不同的点是外边框盒子模型的算式,仅仅做装饰使用。

<style type="text/css">
#swiper {
width: 100px;
height: 100px;
outline: 10px solid;
}
style>

<div id="swiper">div>

运行效果:

div元素实际的宽高还是100 * 100,如果把outline改成border,那么div元素的实际宽高就是120 * 120,因为要加上border的宽度。

外边框设置的最大作用就是:

可以设置元素边框效果,但是不影响页面布局。

4 (~~(Math.random()*(1<<24))).toString(16)

这个代码从结果是得到一个16进制的颜色值,但是为什么能得到呢?

16进制的颜色值:81f262

4.1 Math.random()

这个容易理解,就是随机 [0, 1) 的小数。

4.2 1<<24

这个表示1左移24位,二进制表示如下所示:

1 0000 0000 0000 0000 0000 0000  

十进制就是表示:

2^24

那么

Math.random() * (1<<24)

就会得到如下范围的一个随机浮点数:

[0, 2^24) 

4.3 两次按位取反

因为Math.random()得到是一个小数,所以两次按位取反就是为了过滤掉小数部分,最后得到整数。

所以

(~~(Math.random()*(1<<24)))

就会得到如下范围的一个随机整数:

[0, 2^24) 

4.4 转成字符串toString(16)

最后就是把上面得到的数字转成16进制,我们知道toString()是用来把相关的对象转成字符串的,它可以接收一个进制参数,转成不同的进制,默认是转成10进制。

对象.toString(2); // 转成2进制
对象.toString(8); // 转成8进制
对象.toString(10); // 转成10进制
对象.toString(16); // 转成16进制

上面的得到的随机整数用二进制表示就是:

0000 0000 0000 0000 0000 0000  

1111 1111 1111 1111 1111 1111

那么2进制转成16进制,是不是就是每4位转一个?

最终是不是就得到一个6个长度的16进制数了?

这个字符串加上#是不是就是16进制的颜色值了?

形如:

#ac83ce
#b74384
等等...

实务应用

虽然上面的代码简短,并且知识含量也很高,但是在实务中如果要遍历元素,我并不建议使用这样的方式。

主要原因是两个:

1. $$("*") 只在开发控制台可以用,正常项目代码中不能用。
2. 选中所有元素再遍历,性能低。

如果实务中要遍历元素,建议是用 TreeWalker。querySelectorAll是一次性获取所有元素然后遍历,TreeWalker是迭代器的方式,性能上 TreeWalker 更优,另外 TreeWalker 还支持各种过滤。

参考如下示例:

// 实例化 TreeWalker 对象
let walker = document.createTreeWalker(
document.documentElement,
NodeFilter.SHOW_ELEMENT
);
// 遍历
let node = walker.nextNode();
while (node !== null) {
node.style.outline = "1px solid #" + (~~(Math.random() * (1 << 24))).toString(16);
node = walker.nextNode();
}

虽然代码更多,当时性能更好,并且支持各种过滤等,功能也更加强大。

如果大家有学到新姿势,麻烦帮忙点个赞,谢谢。欢迎大家留言讨论。

参考资料

JavaScript中的$$(*)代表什么和$选择器的由来:ourjs.com/detail/54ab…

querySelectorAll vs NodeIterator vs TreeWalker:stackoverflow.com/questions/6…

作者:晴空闲云
来源:https://juejin.cn/post/7034777643014684703

收起阅读 »

现在实现倒计时都这么卷了吗?

但是在校准时间的过程中,为了快速追赶落后的时间,时间跳动太快了,导致体验不太好,体感上感觉这时间不准呀,因此我再在那基础上再优化了一版 为求实现一版超准确!超平稳!性能极好!体验极佳的倒计时 旧版的功能实现代码 const totalDuration = 10...
继续阅读 »

但是在校准时间的过程中,为了快速追赶落后的时间,时间跳动太快了,导致体验不太好,体感上感觉这时间不准呀,因此我再在那基础上再优化了一版


为求实现一版超准确!超平稳!性能极好!体验极佳的倒计时


旧版的功能实现代码


const totalDuration = 10 * 1000;
let requestRef = null;
let startTime;
let prevEndTime;
let prevTime;
let currentCount = totalDuration;
let endTime;
let timeDifferance = 0; // 每1s倒计时偏差值,单位ms
let interval = 1000;
let nextTime = interval;

setInterval(() => {
let n = 0;
while (n++ < 1000000000);
}, 0);

const animate = (timestamp) => {
if (prevTime !== undefined) {
const deltaTime = timestamp - prevTime;
if (deltaTime >= nextTime) {
prevTime = timestamp;
prevEndTime = endTime;
endTime = new Date().getTime();
currentCount = currentCount - 1000;
console.log("currentCount: ", currentCount / 1000);
timeDifferance = endTime - startTime - (totalDuration - currentCount);
console.log(timeDifferance);
nextTime = interval - timeDifferance;
// 慢太多了,就立刻执行下一个循环
if (nextTime < 0) {
nextTime = 0;
}
console.log(`执行下一次渲染的时间是:${nextTime}ms`);
if (currentCount <= 0) {
currentCount = 0;
cancelAnimationFrame(requestRef);
console.log(`累计偏差值: ${endTime - startTime - totalDuration}ms`);
return;
}
}
} else {
startTime = new Date().getTime();
prevTime = timestamp;
endTime = new Date().getTime();
}
requestRef = requestAnimationFrame(animate);
};

requestRef = requestAnimationFrame(animate);


然后有个细小的问题在于这段代码


// 慢太多了,就立刻执行下一个循环
if (nextTime < 0) {
nextTime = 0;
}

问题在于,假如遇到线程阻塞的情况,出现了倒计时落后情况严重,假设3s,我这里设置下一个循环是0s,然后现在倒计时当前15s,就会看到快速倒计时到12s,产品同学说你这倒计时还怎么加速了呀


这倒计时加速像极了职业生涯结束在加速倒计时一样,瑟瑟发抖的我立刻赶紧修复一下


其实很简单,就是把这个临近值0设置接近每次循环的时间数即可,那么其实是看不出来每次是有在稍微加速/减速的,这里每次循环的时间数是1s,那么我们可以将上面这段代码修改下,把以前立刻就追赶描述的操作,放缓一下追赶的脚步,以此优化用户体验


例如以前追赶2s3s~4s内立刻追赶上,那么波动是很明显的,但是如果把2s的落后秒数,平躺到接下来要倒计时的1min里,每次大概追赶30ms,那是看不出来滴


// 慢到一定临界点,比正常循环的时间数稍微慢点,再执行下一个循环
if (nextTime < 900) {
nextTime = 900;
}

这里我设置落后太多时,每秒追赶100ms,假如落后2s20s后就能追赶回来啦,而且看不出明显波动,时间又是被校验准确的,得到了产品同学的好评!


虽然修改很小,但是也是反复思考得到的~如果对时间要求比较严格,而且倒计时时间范围比较小,来不及把差距平摊到这么大的时间段,可建议让后端同学定时推送最新的倒计时给前端来校验时间准确性,这就万无一失啦


结语


以上是我使用requestAnimationFrame实现倒计时功能反复雕琢的心得,希望能对大家有帮助~如果能获得一个小小的赞作为鼓励会十分感激!!


作者:一只凤梨
链接:https://juejin.cn/post/7026735190634414087

收起阅读 »

中高级前端不一定了解的setTimeout | 网易实践小总结

setTimeout的创建和执行 我们知道setTimeout是一个延时器,它会在规定的时间后延迟执行回调函数,这篇文章就来说说setTimeout它是怎么执行的。 首先我们知道消息队列是用来存储宏任务的,并且主线程会按照顺序取出队列里的任务依次执行,所以为了...
继续阅读 »

setTimeout的创建和执行


我们知道setTimeout是一个延时器,它会在规定的时间后延迟执行回调函数,这篇文章就来说说setTimeout它是怎么执行的。


首先我们知道消息队列是用来存储宏任务的,并且主线程会按照顺序取出队列里的任务依次执行,所以为了保证setTimeout能够在规定的时间内执行,setTimeout创建的任务不会被添加到消息队列里,与此同时,浏览器还维护了另外一个队列叫做延迟消息队列,该队列就是用来存放延迟任务,setTimeout创建的任务会被存放于此,同时它会被记住创建时间,延迟执行时间。


然后我们看下具体例子:


setTimeout(function showName() { console.log('showName') }, 1000)
setTimeout(function showName() { console.log('showName1') }, 1000)
console.log('martincai')

以上例子执行是这样:



  • 1.从消息队列中取出宏任务进行执行(首次任务直接执行)

  • 2.执行setTimeout,此时会创建一个延迟任务,延迟任务的回调函数是showName,发起时间是当前的时间,延迟时间是第二个参数1000ms,然后该延迟任务会被推入到延迟任务队列

  • 3.执行console.log('martincai')代码

  • 4.从延迟队列里去筛选所有已过期任务(当前时间 >= 发起时间 + 延迟时间),然后依次执行


所以我们可以看到showName和showName1同时执行,原因是两个延迟任务都已经过期


循环源码:


void MainTherad(){
for(;;){
// 执行消息队列中的任务
Task task = task_queue.takeTask();
ProcessTask(task);

// 执行延迟队列中的任务
ProcessDelayTask()

if(!keep_running) // 如果设置了退出标志,那么直接退出线程循环
break;
}
}

删除延迟任务


clearTimeout是windows下提供的原生方法,用于删除特定的setTimeout的延迟任务,我们在定义setTimeout的时候返回值就是当前任务的唯一id值,那么clearTimeout就会拿着id在延迟消息队列里查找对应的任务,将其踢出队列即可


setTimeout的几个注意点:



  1. setTimeout会受到消息队列里的宏任务的执行时间影响,上面我们可以看到延迟消息队列的任务会在消息队列的弹出的当前任务执行完之后再执行,所以当前任务的执行时间会阻碍到setTimeout的延迟任务的执行时间


  function showName() {
setTimeout(function show() {
console.log('show')
}, 0)
for (let i = 0; i <= 5000; i++) {}
}
showName()

这里去执行一遍可以发现setTimeout并不是在0ms左右执行,中间会有明显的延迟,因为setTimeout在执行的时候首先会将任务放入到延迟消息队列里,等到showName执行完之后,才会去延迟队列里去查找已过期的任务,这里setTimeout任务会被showName耽误



  1. setTimeout嵌套下会有4ms的延迟


Chrome会把嵌套5层以上的setTimeout后当作阻塞方法,在第6次调用setTimeout的时候会自动将延时器更改为至少4ms的延迟时间



  1. 未激活的页面的setTimeout更改为至少1000ms


当前tab页面不在active状态的时候,setTimeout的延迟至少会被更改1000ms,这样做是为了减少性能消耗和电量消耗



  1. 延迟时间有最大值


目前Chrome、Firefox等主流浏览器都是用32bit去存储延时时间,所以最大值是2的31次方 - 1


  setTimeout(() => {
console.log(1)
}, 2 ** 31)

以上代码会立即执行


作者:我在曾经眺望彼岸
链接:https://juejin.cn/post/7032091028609990692

收起阅读 »

Android 图形处理 —— Matrix 原理剖析

Matrix 简介 Android 图形库中的 android.graphics.Matrix 是一个 3×3 的 float 矩阵,其主要作用是坐标变换 它的结构大概是这样的 其中每个位置的数值作用和其名称所代表的的含义是一一对应的 MSCALE_X、M...
继续阅读 »

Matrix 简介


Android 图形库中的 android.graphics.Matrix 是一个 3×3 的 float 矩阵,其主要作用是坐标变换


它的结构大概是这样的


matrix


其中每个位置的数值作用和其名称所代表的的含义是一一对应的



  • MSCALE_X、MSCALE_Y:控制缩放

  • MTRANS_X、MTRANS_Y:控制平移

  • MSKEW_X、MSKEW_X:控制错切

  • MSCALE_X、MSCALE_Y、MSKEW_X、MSKEW_X:控制旋转

  • MPERSP_0、MPERSP_1、MPERSP_2:控制透视


matrix_1


在 Android 中,我们直接实例化一个 Matrix,内部的矩阵长这样:


matrix_3


是一个左上到右下为 1,其余为 0 的矩阵,也叫单位矩阵,一般数学上表示为 I


Matrix 坐标变换原理


前面说到 Matirx 主要的作用就是处理坐标的变换,而坐标的基本变换有:平移、缩放、旋转和错切



这里所说的基本变换,也称仿射变换 ,透视不属于仿射变化,关于透视相关的内容不在本文的范围内



当矩阵的最后一行是 0,0,1 代表该矩阵是仿射矩阵,下文中所有的矩阵默认都是仿射矩阵


线性代数中的矩阵乘法


在正式介绍 Matrix 是如何控制坐标变换的原理之前,我们先简单复习一下线性代数中的矩阵乘法,详细的讲解可参见维基百科或者翻翻大学的《线性代数》,这里只做最简单的介绍




  • 两个矩阵相乘,前提是第一个矩阵的列数等于第二个矩阵的行数




  • 若 A 为 m × n 的矩阵,B 为 n × p 的矩阵,则他们的乘积 AB 会是一个 m × p 的矩阵,表达可以写为





  • 由定义计算,AB 中任意一点(a,b)的值为 A 中第 a 行的数和 B 中第 b 列的数的乘积的和







了解矩阵乘法的基本方法之后,我们还需要记住几个性质,对后续的分析有用



  • 满足结合律,即 A(BC)=(AB)C

  • 满足分配律,即 A(B + C) = AB + AC (A + B)C = AC + BC

  • 不满足交换律,即 AB != BA

  • 单位矩阵 I 与任意矩阵相乘,等于矩阵本身,即 IA = ABI = B


缩放(Scale)


我们先想想,让我们实现把一个点 (x0, y0) 的 x 轴和 y 轴分别缩放 k1 和 k2 倍,我们会怎么做,很简单


val x = k1 * x0
val y = k2 * y0

那如果用矩阵怎么实现呢,前面我们讲到 Matrix 中 MSCALE_XMSCALE_Y 是用来控制缩放的,我们在这里填分别设置为 k1 和 k2,看起来是这样的


image-20211109103257621

而点 (x0, y0) 用矩阵表示是这样的


image-20211109103824496

有些人会疑问,最后一行这里不是还有一个 1 吗,这是使用了齐次坐标系的缘故,在数学中我们的点和向量都是这样表示的 (x, y),两者看起来一样,计算机无法区分,为了让计算机也可以区分它们,增加了一个标志位,即


(x, y, 1) -> 点
(x, y, 0) -> 向量

现在 Matrix 和点都可以用矩阵表示了,接下来我们看看怎么通过这两个矩阵得到一个缩放之后的点 (x, y). 前面我们已经介绍过矩阵的乘法,让我们看看把上面两个矩阵相乘会得到什么结果


image-20211109104922576

可以看到,矩阵相乘得到了一个(k1x0, k2y0,1)的矩阵,上面说过,计算机中,这个矩阵就代表点 (k1x0, k2y0), 而这个点刚好就是我们要的缩放之后的点


以上所有过程用代码来实现,看起来就是像下面这样


val xy = FloatArray(x0, y0)
Matrix().apply {
setScale(k1, k2)
mapPoints(xy)
}

平移(Translate)


平移和缩放也是类似的,实现平移,我们一般可写为


val x = x0 + deltaX
val y = y0 + deltaY

而用矩阵来实现则是


val xy = FloatArray(x0, y0)
Matrix().apply {
setTranslate(k1, k2)
mapPoints(xy)
}

换成数学表示


translate


根据矩阵乘法


x = 1 × x0 + 0 × y0 + deltaX × 1 = x0 + deltaX
y = 0 × x0 + 1 × y0 + deltaY × 1 = y0 + deltaY

可得和一开始的实现也是效果一致的


错切(Skew)


错切相对于平移和缩放,可能大部分人对这个名词比较陌生,直接看三张图大家可能会比较直观


水平错切


x = x0 + ky0
y = y0

矩阵表示



水平错切


垂直错切


x = x0
y = kx0 + y0

矩阵表示




复合错切


x = x0 + k1y0
y = k2x0 + y0

矩阵表示




旋转(Rotate)


旋转相对以上三种变化又有一点复杂,这里涉及一些三角函数的计算,忘记的可以去维基百科 先复习下



image-20211108215739508

同样我们先自己实现一下旋转,假设一个点 A(x0, y0), 距离原点的距离为 r,与水平夹角为 α,现绕原点顺时针旋转 θ 度,旋转之后的点为 B(x, y)



用矩阵表示




Matrix 复合操作原理


前面介绍了四种基本变换,如果我们需要同时应用上多种变化,比如先绕原点顺时针旋转 90° 再 x 轴平移 100,y 轴平移 100, 最后 x、y 轴缩放0.5 倍,那么就需要用到复合操作


还是先用自己的实现来实现一下


x = ((x0 · cosθ - y0 · sinθ) + 100) · 0.5
y = ((y0 · cosθ + x0 · sinθ) + 100) · 0.5

矩阵表示


image-20211206155715836


按照前面的方式逐个推导,最终也能得到和上述一样的结果


到此,我们可以对 Matrix 做出一个基本的认识:Matrix 基于矩阵计算的原理,解决了计算机中坐标映射和变化的问题


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

Glide线程池

hello:大家好我是 小小小小小鹿,一枚菜鸡Android程序猿。最近正在阅读Glide源码,今天我们要研究的部分是Glide 线程池的配置。 本次代码阅读主要有两个目标 弄清楚Glide是如何做线程池配置的 Glide如何进行优先级加载 Glide用来...
继续阅读 »

hello:大家好我是 小小小小小鹿,一枚菜鸡Android程序猿。最近正在阅读Glide源码,今天我们要研究的部分是Glide 线程池的配置。 本次代码阅读主要有两个目标



  1. 弄清楚Glide是如何做线程池配置的

  2. Glide如何进行优先级加载


Glide用来进行图片加载,我们知道当页面暂停的时候,glide可以根据页面的生命周期,来暂停当前页面的请求,但是如果当前页面通过滑动加载大量图片,那么Glide是怎么进行图片加载的呢?是先调用的加载在前还是后调用的加载在前面呢?如果某个页面的部分图片需要优先被加载,那么Glide又该如何处理呢?


Glide线程池的使用


Glide DecodeJob 的工作过程我们知道Glide在进行一次完成的数据加载会经历 ResourceCacheGenerator --> DataCacheGenerator --> SourceGenerator 的三个过程变化。而在这个过程变化中会涉及到两个线程池的使用。




  1. EngineJob#start开始本次请求


    public synchronized void start(DecodeJob<R> decodeJob) {
     this.decodeJob = decodeJob;
       //如果是从 缓存中获取图片使用 diskCacheExecutor
     GlideExecutor executor = decodeJob.willDecodeFromCache()
         ? diskCacheExecutor
        : getActiveSourceExecutor();
     executor.execute(decodeJob);
    }

    private GlideExecutor getActiveSourceExecutor() {
        //如果useUnlimitedSourceGeneratorPool 为true 使用无限制的线程池
        //如果useAnimationPool 为true且如果useUnlimitedSourceGeneratorPool为false 使用动画线程池 否则使用sourceExecutor
       return useUnlimitedSourceGeneratorPool
           ? sourceUnlimitedExecutor : (useAnimationPool ? animationExecutor : sourceExecutor);
    }



  2. EngineJob#reschedule重新进行调度


    @Override
    public void reschedule(DecodeJob<?> job) {
     //此时线程池的使用逻辑和EngineJob#start不在文件中加载数据一致
     getActiveSourceExecutor().execute(job);
    }



Glide线程池的配置


Glide Excutor参数初始化来自于GlideBuilder#build 而这些在不额外设置的情况下都来自于GlideExecutor。而GlideExecutor的所有线程池都是通过配置ThreadPoolExecutor来完成的。


初识ThreadPoolExecutor


ExecutorService是最初的线程池接口,ThreadPoolExecutor类是对线程池的具体实现,它通过构造方法来配置线程池的参数。


public ThreadPoolExecutor(int corePoolSize,
                             int maximumPoolSize,
                             long keepAliveTime,
                             TimeUnit unit,
                             BlockingQueue<Runnable> workQueue,
                             ThreadFactory threadFactory,
                             RejectedExecutionHandler handler) {
       if (corePoolSize < 0 ||
           maximumPoolSize <= 0 ||
           maximumPoolSize < corePoolSize ||
           keepAliveTime < 0)
           throw new IllegalArgumentException();
       if (workQueue == null || threadFactory == null || handler == null)
           throw new NullPointerException();
       this.corePoolSize = corePoolSize;
       this.maximumPoolSize = maximumPoolSize;
       this.workQueue = workQueue;
       this.keepAliveTime = unit.toNanos(keepAliveTime);
       this.threadFactory = threadFactory;
       this.handler = handler;
  }

参数解释:


corePoolSize,线程池中核心线程的数量,默认情况下,即使核心线程没有任务在执行它也存在的,我们固定一定数量的核心线程且它一直存活这样就避免了一般情况下CPU创建和销毁线程带来的开销。我们如果将ThreadPoolExecutor的allowCoreThreadTimeOut属性设置为true,那么闲置的核心线程就会有超时策略,这个时间由keepAliveTime来设定,即keepAliveTime时间内如果核心线程没有回应则该线程就会被终止。allowCoreThreadTimeOut默认为false,核心线程没有超时时间。 maximumPoolSize,线程池中的最大线程数,当任务数量超过最大线程数时其它任务可能就会被阻塞。最大线程数=核心线程+非核心线程。非核心线程只有当核心线程不够用且线程池有空余时才会被创建,执行完任务后非核心线程会被销毁。 keepAliveTime,非核心线程的超时时长,当闲置时间超过这个时间时,非核心线程就会被回收。当allowCoreThreadTimeOut设置为true时,此属性也作用在核心线程上。 unit,枚举时间单位,TimeUnit。 workQueue,线程池中的任务队列,我们提交给线程池的runnable会被存储在这个对象上。 线程池的分配遵循这样的规则:


当线程池中的核心线程数量未达到最大线程数时,启动一个核心线程去执行任务; 如果线程池中的核心线程数量达到最大线程数时,那么任务会被插入到任务队列中排队等待执行; 如果在上一步骤中任务队列已满但是线程池中线程数量未达到限定线程总数,那么启动一个非核心线程来处理任务; 如果上一步骤中线程数量达到了限定线程总量,那么线程池则拒绝执行该任务,且ThreadPoolExecutor会调用RejectedtionHandler的rejectedExecution方法来通知调用者。


threadFactory:线程工厂,为线程池提供创建新线程的能力。


DiskCacheExecutor的配置过程


GlideExecutor提供了三个创建DiskCacheExecutor的方法,最终都会调用到有三个参数那个


public static GlideExecutor newDiskCacheExecutor(
   int threadCount, String name, UncaughtThrowableStrategy uncaughtThrowableStrategy) {
 return new GlideExecutor(
     new ThreadPoolExecutor(
         threadCount /* corePoolSize */,
         threadCount /* maximumPoolSize */,
         0 /* keepAliveTime */,
         TimeUnit.MILLISECONDS,
         new PriorityBlockingQueue<Runnable>(),
         new DefaultThreadFactory(name, uncaughtThrowableStrategy, true)));
}

在默认创建的时候,调用的是无参数的那个,threadCount 值为1 即DiskCacheExecutor是一个核心线程数为1,没有非核心线程的线程池,所有任务在线程池中串行执行,Runnable的存储对象是PriorityBlockingQueue。


SourceExecutor的配置过程


public static GlideExecutor newSourceExecutor() {
 return newSourceExecutor(
     calculateBestThreadCount(),
     DEFAULT_SOURCE_EXECUTOR_NAME,
     UncaughtThrowableStrategy.DEFAULT);
}

public static GlideExecutor newSourceExecutor(
     int threadCount, String name, UncaughtThrowableStrategy uncaughtThrowableStrategy) {
   return new GlideExecutor(
       new ThreadPoolExecutor(
           threadCount /* corePoolSize */,
           threadCount /* maximumPoolSize */,
           0 /* keepAliveTime */,
           TimeUnit.MILLISECONDS,
           new PriorityBlockingQueue<Runnable>(),
           new DefaultThreadFactory(name, uncaughtThrowableStrategy, false)));
}

可以看到SourceExecutor的构建过程和基本一致,不同的地方在于核心线程的数量是通过calculateBestThreadCount来动态计算的。


if (bestThreadCount == 0) {
   //如果cpu核心数超过4则核心线程数为4 如果Cpu核心数小于4那么使用Cpu核心数作为核心线程数量
 bestThreadCount =
     Math.min(MAXIMUM_AUTOMATIC_THREAD_COUNT, RuntimeCompat.availableProcessors());
}
return bestThreadCount;

UnlimitedSourceExecutor无限制的线程池


public static GlideExecutor newUnlimitedSourceExecutor() {
 return new GlideExecutor(new ThreadPoolExecutor(
     0,
     Integer.MAX_VALUE,
     KEEP_ALIVE_TIME_MS,
     TimeUnit.MILLISECONDS,
     new SynchronousQueue<Runnable>(),
     new DefaultThreadFactory(
         SOURCE_UNLIMITED_EXECUTOR_NAME,
         UncaughtThrowableStrategy.DEFAULT,
         false)));
}

UnlimitedSourceExecutor没有核心线程,非核心线程数量无限大。


AnimationExecutor


public static GlideExecutor newAnimationExecutor() {
 int bestThreadCount = calculateBestThreadCount();
 int maximumPoolSize = bestThreadCount >= 4 ? 2 : 1;
 return newAnimationExecutor(maximumPoolSize, UncaughtThrowableStrategy.DEFAULT);
}

public static GlideExecutor newAnimationExecutor(
     int threadCount, UncaughtThrowableStrategy uncaughtThrowableStrategy) {
    return new GlideExecutor(
       new ThreadPoolExecutor(
           0 /* corePoolSize */,
           threadCount,
           KEEP_ALIVE_TIME_MS,
           TimeUnit.MILLISECONDS,
           new PriorityBlockingQueue<Runnable>(),
           new DefaultThreadFactory(
               ANIMATION_EXECUTOR_NAME,
               uncaughtThrowableStrategy,
               true)));
}

AnimationExecutor没有核心线程,非核心线程数量根据Cpu核心数来决定,当Cpu核心数大于等4时 非核心线程数为2,否则为1。


Glide线程池总结


DiskCacheExecutor和SourceExecutor 采用固定核心线程数固定,适用于处理CPU密集型的任务,但是没有非核心线程。确保CPU在长期被工作线程使用的情况下,尽可能的少的分配线程,即适用执行长期的任务。


UnlimitedSourceExecutor采用无核心线程,非核心线程无限大适用于并发执行大量短期的小任务。在空闲的时候消耗资源非常少。


AnimationExecutor没有核心线程,非核心线程有限,同UnlimitedSourceExecutor的区别就是核心线程数量和工作队列不一致。第一次看到这么用。


Glide如何实现加载优先级


除了UnlimitedSourceExecutor其余的都是使用的PriorityBlockingQueue。PriorityBlockingQueue是一个具有优先级的无界阻塞队列。也就是说优先级越高越先执行。


我们知道图片的加载是在线程池中执行的DecodeJob,DecodeJob实现了Runnable和Comparable接口。当DecodeJob被提交到线程池的时候,如果需要加入工作队列会通过compareTo比较Decodejob优先级


@Override
public int compareTo(@NonNull DecodeJob<?> other) {
 //先比较 Priority  
 int result = getPriority() - other.getPriority();
 //如果 Priority优先级一致 ,比较order order是一个自增的int 每一次初始化DecodeJob 都会执行++ 因此后初始化的DecodeJob比先初始化的优先级高。
 if (result == 0) {
   result = order - other.order;
}
 return result;
}
作者:小小小小小鹿
链接:https://juejin.cn/post/7038795986482757669
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Kotlin开发中的一些Tips

作用域函数选择 目前有let、run、with、apply 和 also五个作用域函数。 官方文档有张表来说明它们之间的区别:   总结一下有几点区别: 1、apply和also返回上下文对象。 2、let、run 和with返回lambda 结果。 3、l...
继续阅读 »

作用域函数选择


目前有letrunwithapply 和 also五个作用域函数。


官方文档有张表来说明它们之间的区别: 



 总结一下有几点区别:


1、applyalso返回上下文对象。


2、letrun 和with返回lambda 结果。


3、letalso引用对象是it ,其余是this


1.letrun是我日常使用最多的两个,它们之间很类似。


private var textView: TextView? = null

textView?.let {
it.text = "Kotlin"
it.textSize = 14f
}

textView?.run {
text = "Kotlin"
textSize = 14f
}

相比较来说使用run显得比较简洁,但let的优势在于可以将it重命名,提高代码的可读性,也可以避免作用域函数嵌套时导致混淆上下文对象的情况。


2.对于可空对象,使用let比较方便。对于非空对象可以使用with


3.applyalso也非常相似,文档给出的建议是如果是对象配置操作使用apply,额外的处理使用also。例如:


val numberList = mutableListOf()
numberList.also { println("Populating the list") }
.apply {
add(2.71)
add(3.14)
add(1.0)
}
.also { println("Sorting the list") }
.sort()

简单说就是符合单词的含义使用,提高代码可读性。


总的来说,这几种函数有许多重叠的部分,因此可以根据开发中的具体情况来使用。以上仅做参考。


Sequence


我们经常会使用到kotlin的集合操作符,比如 map 和 filter 等。


list.map {
it * 2
}.filter {
it % 3 == 0
}

老规矩,看一下反编译后的代码: 



就干了这么点事情,创建了两个集合,循环了两遍。效率太低,这还不如自己写个for循环,一个循环就处理完了。看一下map的源码:


public inline fun  Iterable.map(transform: (T) -> R): List {
return mapTo(ArrayList(collectionSizeOrDefault(10)), transform)
}

public inline fun > Iterable.mapTo(destination: C, transform: (T) -> R): C {
for (item in this)
destination.add(transform(item))
return destination
}

内部实现确实如此,难道这些操作符不香了?


其实这时就可以使用Sequences(序列),用法很简单,只需要在集合后添加一个asSeqence() 方法。


list.asSequence().map {
it * 2
}.filter {
it % 3 == 0
}

反编译:


SequencesKt.filter(SequencesKt.map(CollectionsKt.asSequence((Iterable)list), (Function1)null.INSTANCE), (Function1)null.INSTANCE);

有两个Function1,其实就是lambda表达式,这是因为Sequence没有使用内联导致的。我们先看看SequencesKt.map源码:


public fun  Sequence.map(transform: (T) -> R): Sequence {
return TransformingSequence(this, transform)
}

internal class TransformingSequence
constructor(private val sequence: Sequence, private val transformer: (T) -> R) : Sequence {
override fun iterator(): Iterator = object : Iterator {
val iterator = sequence.iterator()
override fun next(): R {
return transformer(iterator.next())
}

override fun hasNext(): Boolean {
return iterator.hasNext()
}
}

internal fun flatten(iterator: (R) -> Iterator): Sequence {
return FlatteningSequence(sequence, transformer, iterator)
}
}

可以看到没有创建中间集合去循环,只是创建了一个Sequence对象,里面实现了迭代器。SequencesKt.filter方法也是类似。细心的话你会发现,这都只是创建Sequence对象,所以要想真正拿到处理后的集合,需要添加toList()这种末端操作。


map 和 filter 这类属于中间操作,返回的是一个新Sequence,里面有数据迭代时的实际处理。而 toList和first这类属于末端操作用来返回结果。


所以Sequence是延迟执行的,这也就是它为何不会出现我们一开始提到的问题,一次循环就处理完成了。


总结一下Sequence的使用场景:


1、有多个集合操作符时,建议使用Sequence。


2、数据量大的时候,这样可以避免重复创建中间集合。这个数据量大,怎么也是万以上的级别了。


所以对于一般Android开发中来说,不使用Sequence其实差别不大。。。哈哈。。


协程


有些人会错误理解kotlin的协程,觉得它的性能更高,是一种“轻量级”的线程,类似go语言的协程。但是如果你细想一下,这是不太可能的,最终它都是要在JVM上运行,java都没有的东西,你就实现了,你这不是打java的脸嘛。


所以对于JVM平台,kotlin的协程只能是对Thread API的封装,和我们用的Executor类似。所以对于协程的性能,我个人也认为差别不大。只能说kotlin借助语言简洁的优势,让操作线程变的更加简单。


之所以上面说JVM,是因为kotlin还有js和native平台。对于它们来说,或许可以实现真正的协程。


推荐扔物线大佬关于协程的文章,帮你更好的理解kotlin的协程:到底什么是「非阻塞式」挂起?协程真的更轻量级吗?


Checked Exception


这对熟悉Java的同学并不陌生,Checked Exception 是处理异常的一种机制,如果你的方法中声明了它可能会抛出的异常,编译器就会强制开发者对异常进行处理,否则编译不会通过。我们需要使用 try catch 捕获异常或者使用 throws 抛出异常处理它。


但是Kotlin中并不支持这个机制,也就是说不会强制你去处理抛出的异常。至于Checked Exception 好不好,争议也不少。这里就不讨论各自的优缺点了。


既然Kotlin中没有这个机制已经是既成事实,那么我们在使用中就需要考虑它带来的影响。比如我们开发中在调用一些方法时,要注意看一下源码中是否有指定异常抛出,然后做相应处理,避免不必要的崩溃。


例如常用的json解析:


private fun test() {
val jsonObject = JSONObject("{...}")
jsonObject.getString("id")
...
}

在java中我们需要处理JSONException,kotlin中因为没有Checked Exception,如果我们像上面这样直接使用,虽然程序可以运行,可是一但解析出现异常,程序就会崩溃。


Intrinsics检查


如果你经常观察反编译后的java代码,会发现有许多类似Intrinsics.checkXXX这样的代码。


fun test(str: String) {
println(str)
}

反编译: 



 比如图中的checkParameterIsNotNull就是用了检查参数是否为空。虽然我们的参数是不可控的,但是考虑到方法会被Java调用,Kotlin会默认的增加checkParameterIsNotNull校验。如果kotlin方法是私有的,也就不会有此行检查。


checkParameterIsNotNull并不会有性能问题,相反这种提前判断参数是否正确,可以避免程序向后执行导致不必要的资源消耗。


当然如果你想去除它,可以添加下面的配置到你的gradle文件,这样就会在编译时去除它。


kotlinOptions {
freeCompilerArgs = [
'-Xno-param-assertions',
'-Xno-call-assertions',
'-Xno-receiver-assertions'
]
}

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

一图秒懂CDN原理

CDN
前些天,线上灰度了一个功能,下午接到一些业务上报国外用户访问时图片无法显示,但是国内访问都是正常,所以怀疑是国外CDN问题导致。 先了说明下现状: 图片保存在阿里OSS中 国内使用了阿里云CDN 国外使用Akamai(全球CDN厂商) 按理说,CDN都有...
继续阅读 »

前些天,线上灰度了一个功能,下午接到一些业务上报国外用户访问时图片无法显示,但是国内访问都是正常,所以怀疑是国外CDN问题导致。


先了说明下现状:



  1. 图片保存在阿里OSS中

  2. 国内使用了阿里云CDN

  3. 国外使用Akamai(全球CDN厂商)



按理说,CDN都有,图片不应该访问不到。于是,在脑子中根据CDN的原理,先思考下可能的问题



CDN原理


CDN全称是Content Delivery Network,即内容分发网络,也称为内容传送网络。CDN是构建在现有网络基础之上的智能虚拟网络,依靠部署在各地的边缘服务器,使用户就近获取所需内容,降低网络拥塞,提高用户访问响应速度和命中率。


cdn.jpg


如上图CDN的逻辑主要分为两步:DNS解析请求边缘节点



用dig看下DNS解析结果:



$ dig juejin.cn

; <<>> DiG 9.10.6 <<>> juejin.cn
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 63296
;; flags: qr rd ra; QUERY: 1, ANSWER: 9, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 4096
;; QUESTION SECTION:
;juejin.cn. IN A

;; ANSWER SECTION:
juejin.cn. 412 IN CNAME juejin.cn.w.cdngslb.com.
juejin.cn.w.cdngslb.com. 60 IN A 112.85.251.229
juejin.cn.w.cdngslb.com. 60 IN A 112.85.251.227
juejin.cn.w.cdngslb.com. 60 IN A 112.85.251.231
juejin.cn.w.cdngslb.com. 60 IN A 112.85.251.224
juejin.cn.w.cdngslb.com. 60 IN A 112.85.251.225
juejin.cn.w.cdngslb.com. 60 IN A 112.85.251.230
juejin.cn.w.cdngslb.com. 60 IN A 112.85.251.226
juejin.cn.w.cdngslb.com. 60 IN A 112.85.251.228

;; Query time: 9 msec
;; SERVER: 192.168.3.1#53(192.168.3.1)
;; WHEN: Sat May 15 14:26:26 CST 2021
;; MSG SIZE rcvd: 203

在ANSWER SECTION列表可以看出



  1. juejin.cn为cname记录指向juejin.cn.w.cdngslb.com

  2. juejin.cn.w.cdngslb.com返回了7条A记录,这7个ip 信息是江苏 徐州 联通,我所在地是上海,联通,可以看出返回的都是就近节点。实际上CDN是有非常多的边缘节点。



用tcpdump来监控下DNS的UDP数据包




  1. 在一个窗口输入sudo tcpdump -n -s 1500 udp and port 53

  2. 在另一个窗口输入ping juejin.cn


监控到的UDP数据包如下:


21:49:13.960212 IP 192.168.3.201.52647 > 192.168.3.1.53: 37581+ A? juejin.cn. (27)
21:49:13.975290 IP 192.168.3.1.53 > 192.168.3.201.52647: 37581 9/0/0 CNAME juejin.cn.w.cdngslb.com., A 112.85.251.229, A 112.85.251.230, A 112.85.251.226, A 112.85.251.228, A 112.85.251.224, A 112.85.251.231, A 112.85.251.225, A 112.85.251.227 (192)

其中,192.168.3.1为路由器IP。也就是本机向路由器询问DNS解析,如果路由器已经缓存了,就会直接返回。


复现问题


我们回到问题中,如果CDN返回的边缘节点如果不出问题,图片应该是可以很快访问到的,CDN厂商不至于出现这个问题。那么问题在那里呢?


在公司环境无法复现问题,就要找一个最接近客户场景的环境来测试,于是想办法搞到一台香港window系统的测试机,远程上去一看,还果真有问题。



图片在界面中不显示,但是直接在浏览器访问是正常的,开发者模式下发现访问图片时出现跨域错误



一张正常显示的图片请求返回的http头是这样的:


Response Headers:
accept-ranges: bytes
access-control-allow-origin: *
etag: "A31F477F3232DA431D3B77543C3EBF92"
last-modified: Thu, 25 Jul 2019 01:37:03 GMT
...省略

1. access-control-allow-origin


通配符 * 表示允许被任何网站引用。如果想让资源只被指定域名访问,只需把*改为域名就行了,如下:


access-control-allow-origin: `https://juejin.cn`

2. etag


etag是http协议缓存逻辑中的一个属性。CDN的目的就是减少网络访问,因为缓存是必须要用的功能。


而无法显示的图片,返回的请求头是这样的:


Response Headers:
accept-ranges: bytes
etag: "A31F477F3232DA431D3B77543C3EBF92"
last-modified: Thu, 25 Jul 2019 01:37:03 GMT
...省略

没有access-control-allow-origin这一项,导致页面中无法加载。


浏览器边缘节点请求图片命中缓存,返回图片响应头中没有CORS属性抛出CORS异常,图片不渲染浏览器边缘节点


解决办法很简单,在CDN后台配置返回access-control-allow-origin信息即可


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

Flutter: 完成一个图片APP

自从 Flutter 推出之后, 一直是备受关注, 有看好的也有不看好的, 作为移动开发人员自然是要尝试一下的(但是它的嵌套写法真的难受), 本着学一个东西, 就一定要动手的态度, 平时又喜欢看一些猫狗的图片, 就想着做一个加载猫狗图片你的 APP, 界面图如...
继续阅读 »

自从 Flutter 推出之后, 一直是备受关注, 有看好的也有不看好的, 作为移动开发人员自然是要尝试一下的(但是它的嵌套写法真的难受), 本着学一个东西, 就一定要动手的态度, 平时又喜欢看一些猫狗的图片, 就想着做一个加载猫狗图片你的 APP, 界面图如下(界面不是很好看).






主要模块



NetWork

api.dart文件中, 分别定义了DogApi, CatApi两个类, 一个用于处理获取猫的图片的类, 一个用于处理狗的图片的类.


http_request.dart文件封装了Http请求, 用于发送和接收数据.


url.dart文件封装了需要用到的Api接口, 主要是为了方便和统一管理而编写.


Models文件夹下分别定义不同API接口返回数据的模型.


图片页

瀑布流使用的flutter_staggered_grid_view库, 作者自定义了Delegate计算布局, 使用起来非常简单.


Widget scene = new StaggeredGridView.countBuilder(
physics: BouncingScrollPhysics(),
itemCount: this.breedImgs != null ? this.breedImgs.urls.length : 0,
mainAxisSpacing: 4.0,
crossAxisSpacing: 4.0,
crossAxisCount: 3,
itemBuilder: (context, index) {
return new GestureDetector(
onTapUp: (TapUpDetails detail) {
// 展示该品种的相关信息
dynamic breed = this.breeds[this.selectedIdx].description;
// TODO: 取出当前点击的然后所有往后的
List<String> unreadImgs = new List<String>();
for (int i = index; i < this.breedImgs.urls.length; i++) {
unreadImgs.add(this.breedImgs.urls[i]);
}
AnimalImagesPage photoPage = new AnimalImagesPage(
listImages: unreadImgs,
breed: this.breeds[this.selectedIdx].name,
imgType: "Cat",
petInfo: this.breeds[this.selectedIdx],
);
Navigator.of(context)
.push(new MaterialPageRoute(builder: (context) {
return photoPage;
}));
},
child: new Container(
width: 100,
height: 100,
color: Color(0xFF2FC77D), //Colors.blueAccent,
child: new CachedNetworkImage(
imageUrl: this.breedImgs.urls[index],
fit: BoxFit.fill,
placeholder: (context, index) {
return new Center(child: new CupertinoActivityIndicator());
},
),
),
);
},
// 该属性可以控制当前 Cell 占用的空间大小, 用来实现瀑布的感觉
staggeredTileBuilder: (int index) =>
new StaggeredTile.count(1, index.isEven ? 1.5 : 1),
);

  • 组装PickerView


系统默认的 PickerView 在每一次切换都会回调, 而且没有确定和取消事件,
如果直接使用会造成频繁的网络请求, 内存消耗也太快, 所以组装了一下, 增加确定和取消才去执行网络请求, 这样就解决了这个问题.


    Widget column = Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
new Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
new Container(
width: MediaQuery.of(context).size.width,
height: 40,
child: new Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
new Padding(
padding: EdgeInsets.only(left: 10.0),
child: new GestureDetector(
onTapUp: (detail) {
// 点击了确定按钮, 退出当前页面
Navigator.of(context).pop();
// 回调操作
this.submit(this.selectedIndex);
},
child: new Text(
"确定",
style: TextStyle(
decoration: TextDecoration.none,
color: Colors.white,
fontSize: 18),
),
),
),
new Padding(
padding: EdgeInsets.only(right: 10.0),
child: new GestureDetector(
onTapUp: (detail) {
// 点击了确定按钮, 退出当前页面
Navigator.of(context).pop();
},
child: new Text(
"取消",
style: TextStyle(
decoration: TextDecoration.none,
color: Colors.white,
fontSize: 18),
),
),
)
],
),
),
],
),
new Container(
height: 1,
color: Colors.white,
),
// Picker
new Expanded(
child: new CupertinoPicker.builder(
backgroundColor: Colors.transparent,
itemExtent: 44,
childCount: this.names.length,
onSelectedItemChanged: (int selected) {
this.selectedIndex = selected;
this.onSelected(selected);
},
itemBuilder: (context, index) {
return new Container(
width: 160,
height: 44,
alignment: Alignment.center,
child: new Text(
this.names[index],
textAlign: TextAlign.right,
style: new TextStyle(
color: Colors.white,
fontSize: 16,
decoration: TextDecoration.none),
),
);
}),
)
],
);
详情页


  • Column 包含 ListView


详情页中, 上方是一个图片, 下方是关于品种的相关信息, 下方是通过 API获取到的属性进行一个展示, 需要注意一点是, 如果Column封装了MainAxis相同方向的滚动控件, 必须设置Width/Height, 同理, Row也是需要注意这一点的.


我在这里的做法是通过一个Container包裹 ListView.


new Container(
margin: EdgeInsets.only(bottom: 10, top: 10),
height: MediaQuery.of(context).size.height - MediaQuery.of(context).size.width / 1.2 - 80,
width: MediaQuery.of(context).size.width,
child: listView,
),

  • 图片动画


这一部分稍微复杂一些, 首先需要监听滑动的距离, 来对图片进行变换, 最后根据是否达到阈值来进行切换动画, 这里我没有实现在最后一张和第一张图片进行切换以至于可以无限循环滚动, 我在边界阈值上只是阻止了下一步动画.


动画我都是通过Matrix4来设置不同位置的属性, 它也能模拟出 3D 效果,


动画的变换都是Tween来管理.


  void _initAnimation() {
// 透明度动画
this.opacityAnimation = new Tween(begin: 1.0, end: 0.0).animate(
new CurvedAnimation(
parent: this._nextAnimationController, curve: Curves.decelerate))
..addListener(() {
this.setState(() {
// 通知 Fluter Engine 重绘
});
});
// 翻转动画
// 第三个值是角度
var startTrans = Matrix4.identity()..setEntry(3, 2, 0.006);
var endTrans = Matrix4.identity()
..setEntry(3, 2, 0.006)
..rotateX(3.1415927);
this.transformAnimation = new Tween(begin: startTrans, end: endTrans)
.animate(new CurvedAnimation(
parent: this._nextAnimationController, curve: Curves.easeIn))
..addListener(() {
this.setState(() {});
});
// 缩放
var saveStartTrans = Matrix4.identity()..setEntry(3, 2, 0.006);
// 平移且缩放
var saveEndTrans = Matrix4.identity()
..setEntry(3, 2, 0.006)
..scale(0.1, 0.1)
..translate(-20.0, 20.0); // MediaQuery.of(context).size.height
this.saveToPhotos = new Tween(begin: saveStartTrans, end: saveEndTrans)
.animate(new CurvedAnimation(
parent: this._saveAnimationController, curve: Curves.easeIn))
..addListener(() {
this.setState(() {});
});
}

Widget引用这个属性来执行动画.


Widget pet = new GestureDetector(
onVerticalDragUpdate: nextUpdate,
onVerticalDragStart: nextStart,
onVerticalDragEnd: next,
child: new Transform(
transform: this.dragUpdateTransform,
child: Container(
child: new Transform(
alignment: Alignment.bottomLeft,
transform: transform,
child: new Opacity(
opacity: opacity,
child: Container(
width: MediaQuery.of(context).size.width / 1.2,
height: MediaQuery.of(context).size.width / 1.5 - 30,
child: new Padding(
padding: EdgeInsets.all(0),
child: new CachedNetworkImage(
imageUrl: this.widget.listImages[item],
fit: BoxFit.fill,
placeholder: (context, content) {
return new Container(
width: MediaQuery.of(context).size.width / 2.0 - 40,
height: MediaQuery.of(context).size.width / 2.0 - 60,
color: Color(0xFF2FC77D),
child: new Center(
child: new CupertinoActivityIndicator(),
),
);
},
),
),
),
),
),
),
),
);
Firebase_admob

注意: 这里需要去 firebase 官网注册 APP, 然后分别下载 iOS, Android 的配置文件放到指定的位置, 否则程序启动的时候会闪退.


iOS info.plist: GADApplicationIdentifier也需要配置, 虽然在 Dart 中会启动的时候就注册ID, 但是这里也别忘了配置.


Android Manifst.xml 也需要配置


<meta-data
android:name="com.google.android.gms.ads.APPLICATION_ID"
android:value=""/>

这里说一下我因为个人编码导致的问题, 我尝试自己来控制广告展示, 加了一个读秒跳过按钮(想强制观看一段时间), 点击跳过设置setState, 但是在 build 方法中又请求了广告, 导致了一个死循环, 最后由于请求次数过多还没有设置自己的设备为测试设备也不是使用的测试ID, 账号被暂停了, 所以大家使用的时候要避免这个问题, 尽量还是将自己的设备添加到测试设备中.


使用的话比较简单(官方的演示代码直接复制也可以用).


class AdPage {
MobileAdTargetingInfo targetingInfo;

InterstitialAd interstitial;

BannerAd banner;

void initAttributes() {
if (this.targetingInfo == null) {
this.targetingInfo = MobileAdTargetingInfo(
keywords: ["some keyword for your app"],
// 防止被Google 认为是无效点击和展示.
testDevices: ["Your Phone", "Simulator"]);

bool android = Platform.isAndroid;

this.interstitial = InterstitialAd(
adUnitId: InterstitialAd.testAdUnitId,
targetingInfo: this.targetingInfo,
listener: (MobileAdEvent event) {
if (event == MobileAdEvent.closed) {
// 点击关闭
print("InterstitialAd Closed");
this.interstitial.dispose();
this.interstitial = null;
} else if (event == MobileAdEvent.clicked) {
// 关闭
print("InterstitialAd Clicked");
this.interstitial.dispose();
this.interstitial = null;
} else if (event == MobileAdEvent.loaded) {
// 加载
print("InterstitialAd Loaded");
}
print("InterstitialAd event is $event");
},
);

// this.banner = BannerAd(
// targetingInfo: this.targetingInfo,
// size: AdSize.smartBanner,
// listener: (MobileAdEvent event) {
// if (event == MobileAdEvent.closed) {
// // 点击关闭
// print("InterstitialAd Closed");
// this.interstitial.dispose();
// this.interstitial = null;
// } else if (event == MobileAdEvent.clicked) {
// // 关闭
// print("InterstitialAd Clicked");
// this.interstitial.dispose();
// this.interstitial = null;
// } else if (event == MobileAdEvent.loaded) {
// // 加载
// print("InterstitialAd Loaded");
// }
// print("InterstitialAd event is $event");
// });
}
}

@override
void show() {
// 初始化数据
this.initAttributes();
// 然后控制跳转
if (this.interstitial != null) {
this.interstitial.load();
this.interstitial.show(
anchorType: AnchorType.bottom,
anchorOffset: 0.0,
);
}
}
}

项目比较简单, 但是编写的过程中也遇到了许多问题, 慢慢解决的过程也学到了挺多.


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

Flutter页面传值的几种方式

今天来聊聊Flutter页面传值的几种方式: InheritWidget Notification Eventbus (当前Flutter版本:2.0.4) InheritWidget 如果看过Provider的源码的同学都知道,Provider跨组件传值...
继续阅读 »

今天来聊聊Flutter页面传值的几种方式:



  1. InheritWidget

  2. Notification

  3. Eventbus


(当前Flutter版本:2.0.4)


InheritWidget


如果看过Provider的源码的同学都知道,Provider跨组件传值的原理就是根据系统提供的InheritWidget实现的,让我们来看一下这个组件。
InheritWidget是一个抽象类,我们写一个保存用户信息的类UserInfoInheritWidget继承于InheritWidget:


class UserInfoInheritWidget extends InheritedWidget {

UserInfoBean userInfoBean;
UserInfoInheritWidget({Key key, this.userInfoBean, Widget child}) : super (child: child);

static UserInfoWidget of(BuildContext context){
return context.dependOnInheritedWidgetOfExactType<UserInfoWidget>();
}

@override
bool updateShouldNotify(UserInfoInheritWidget oldWidget) {
return oldWidget.userInfoBean != userInfoBean;
}
}

我们在这里面定义了一个静态方法:of,并且传入了一个context,根据context获取当前类,拿到当前类中的UserInfoBean,其实获取主题数据也是根据InheritWidget这种方式获取Theme.of(context),关于of方法后面重点讲一下,updateShouldNotify是刷新机制,什么时候刷新数据


还有一个用户信息的实体:


class UserInfoBean {
String name;
String address;
UserInfoBean({this.name, this.address});
}

我们做两个页面,第一个页面显示用户信息,还有一个按钮,点击按钮跳转到第二个页面,同样也是显示用户信息:


class Page19PassByValue extends StatefulWidget {
@override
_Page19PassByValueState createState() => _Page19PassByValueState();
}

class _Page19PassByValueState extends State<Page19PassByValue> {

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('PassByValue'),
),
body: DefaultTextStyle(
style: TextStyle(fontSize: 30, color: Colors.black),
child: Column(
children: [
Text(UserInfoWidget.of(context)!.userInfoBean.name),
Text(UserInfoWidget.of(context)!.userInfoBean.address),
SizedBox(height: 40),
TextButton(
child: Text('点击跳转'),
onPressed: (){
Navigator.of(context).push(CupertinoPageRoute(builder: (context){
return DetailPage();
}));
},
)
],
),
),
);
}
}

class DetailPage extends StatefulWidget {
@override
_DetailPageState createState() => _DetailPageState();
}

class _DetailPageState extends State<DetailPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Detail'),
),
body: DefaultTextStyle(
style: TextStyle(fontSize: 30, color: Colors.black),
child: Center(
child: Column(
children: [
Text(UserInfoWidget.of(context).userInfoBean.name),
Text(UserInfoWidget.of(context).userInfoBean.address),
TextButton(
onPressed: () {
setState(() {
UserInfoWidget.of(context)!.updateBean('wf123','address123');
});
},
child: Text('点击修改'))
],
),
),
)
);
}
}

由于我们这里是跨组件传值,需要把UserInfoWidget放在MaterialApp的上层,并给UserInfoBean一个初始值:


class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return UserInfoWidget(
userInfoBean: UserInfoBean(name: 'wf', address: 'address'),
child: MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
),
);
}
}

这样就实现了一个跨组件传值,但是还有个问题,我们给UserInfoWidget赋值的时候是在最顶层,在真实业务场景中,如果我们把UserInfo的赋值放在MaterialApp上面,这时候我们还没拿到用户数据呢,所以就要有一个可以更新UserInfo的方法,并且修改后立即刷新,我们可以借助setState,把我们上面定义的UserInfoWidget改个名字然后封装在StatefulWidget 中:


class _UserInfoInheritWidget extends InheritedWidget {

UserInfoBean userInfoBean;
Function update;
_UserInfoInheritWidget({Key key, this.userInfoBean, this.update, Widget child}) : super (child: child);

updateBean(String name, String address){
update(name, address);
}

@override
bool updateShouldNotify(_UserInfoInheritWidget oldWidget) {
return oldWidget.userInfoBean != userInfoBean;
}
}

class UserInfoWidget extends StatefulWidget {
UserInfoBean userInfoBean;
Widget child;
UserInfoWidget({Key key, this.userInfoBean, this.child}) : super (key: key);

static _UserInfoInheritWidget of(BuildContext context){
return context.dependOnInheritedWidgetOfExactType<_UserInfoInheritWidget>();
}
@override
State<StatefulWidget> createState() => _UserInfoState();
}

class _UserInfoState extends State <UserInfoWidget> {

_update(String name, String address){
UserInfoBean bean = UserInfoBean(name: name, address: address);
widget.userInfoBean = bean;
setState(() {});
}
@override
Widget build(BuildContext context) {
return _UserInfoInheritWidget(
child: widget.child,
userInfoBean: widget.userInfoBean,
update: _update,
);
}
}

上面把继承自InheritWidget的类改了一个名字:_UserInfoInheritWidget,对外只暴露用StatefulWidget封装过的UserInfoWidget,向_UserInfoInheritWidget传入了包含setState的更新数据方法,更新数据的时候通过UserInfoWidget.of(context)获取到继承于InheritWidget_UserInfoInheritWidget类,调用updateBean方法实际上就调用了包含setState的方法,所以做到了数据更新和页面刷新


1.gif


下面重点说一下UserInfoWidget.of(context)是如何获取到继承于InheritWidget类的对象的,通过查看类似的方法:Theme.of(context)发现是根据dependOnInheritedWidgetOfExactType,于是我们也照着它的样子获取到了_UserInfoInheritWidget,点到dependOnInheritedWidgetOfExactType源码中看一下,发现跳转到了BuildContext中定义了这个方法:


  T? dependOnInheritedWidgetOfExactType<T extends InheritedWidget>({ Object? aspect });
复制代码

了解WidgetElementRenderObject三只之间关系的同学都知道,其实contextElement的一个实例,BuildContext的注释也提到了这一点:


image.png
我们可以在Element中找到这个方法的实现:


@override
T? dependOnInheritedWidgetOfExactType<T extends InheritedWidget>({Object? aspect}) {
assert(_debugCheckStateIsActiveForAncestorLookup());
final InheritedElement? ancestor = _inheritedWidgets == null ? null : _inheritedWidgets![T];
if (ancestor != null) {
assert(ancestor is InheritedElement);
return dependOnInheritedElement(ancestor, aspect: aspect) as T;
}
_hadUnsatisfiedDependencies = true;
return null;
}

_inheritedWidgets是从哪来的,我们搜索一下在Element中发现


void _updateInheritance() {
assert(_lifecycleState == _ElementLifecycle.active);
_inheritedWidgets = _parent?._inheritedWidgets;
}

再看一下_updateInheritance方法是什么时候调用的


@mustCallSuper
void mount(Element? parent, dynamic newSlot) {
...
...省略无关代码
_parent = parent;
_slot = newSlot;
_lifecycleState = _ElementLifecycle.active;
_depth = _parent != null ? _parent!.depth + 1 : 1;
if (parent != null) // Only assign ownership if the parent is non-null
_owner = parent.owner;
final Key? key = widget.key;
if (key is GlobalKey) {
key._register(this);
}
_updateInheritance();//这里调用了一次
}

还有:


@mustCallSuper
void activate() {
...
...已省略无关代码
final bool hadDependencies = (_dependencies != null && _dependencies!.isNotEmpty) || _hadUnsatisfiedDependencies;
_lifecycleState = _ElementLifecycle.active;
_dependencies?.clear();
_hadUnsatisfiedDependencies = false;
_updateInheritance();//这里又调用了一次
if (_dirty)
owner!.scheduleBuildFor(this);
if (hadDependencies)
didChangeDependencies();
}

从上面代码我们可以看到每个页面的Element都会通过_parent向下级传递父级信息,而我们的UserInfoWidget就保存在_parent中的_inheritedWidgets集合中:
Map<Type, InheritedElement>? _inheritedWidgets;,当_inheritedWidgets在页面树中向下传递的时候,如果当前WidgetInheritWidget,在当前Widget对应的Element中先看_parent传过来的_inheritedWidgets是否为空,如果为空就新建一个集合,把自己存到这个集合中,以当前的类型作为key(这也是为什么调用of方法中的context.dependOnInheritedWidgetOfExactType方法为什么要传当前类型的原因),从_inheritedWidgets集合中去取值;如果不为空直接把自己存进去,这就是of的原理了。


Notification


上面讲的InheritWidget一般是根部组建向子级组件传值,Notification是从子级组件向父级组件传值,下面我们来看一下它的用法


class Page19PassByValue extends StatefulWidget {
@override
_Page19PassByValueState createState() => _Page19PassByValueState();
}

class _Page19PassByValueState extends State<Page19PassByValue> {
UserInfoBean userInfoBean = UserInfoBean(name: 'wf', address: 'address');

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('PassByValue'),
),
body: Center(
child: NotificationListener<MyNotification>(
onNotification: (MyNotification data) {
userInfoBean = data.userInfoBean;
setState(() {});
///这里需要返回一个bool值,true表示阻止事件继续向上传递,false表示事件可以继续向上传递到父级组件
return true;
},
child: Builder(
///这里用了一个Builder包装了一下,为的是能取到
///NotificationListener的context
builder: (context) {
return Column(
children: [
Text(userInfoBean.name),
Text(userInfoBean.address),
Container(
child: FlatButton(
child: Text('点击传值'),
onPressed: () {
MyNotification(userInfoBean: UserInfoBean(name: 'wf123', address: 'address123')).dispatch(context);
},
),
)
],
);
},
),
),
),
);
}
}

///Notification是一个抽象类,
///使用Notification需要自定义一个class继承Notification
class MyNotification extends Notification {
UserInfoBean userInfoBean;
MyNotification({this.userInfoBean}) : super();
}

我们到源码中看一下这个dispatch方法:


void dispatch(BuildContext target) {
// The `target` may be null if the subtree the notification is supposed to be
// dispatched in is in the process of being disposed.
target?.visitAncestorElements(visitAncestor);
}

target就是我们传进来的context,也就是调用了BuildContextvisitAncestorElements方法,并且把visitAncestor方法作为一个参数传过去,visitAncestor方法返回一个bool值:


  @protected
@mustCallSuper
bool visitAncestor(Element element) {
if (element is StatelessElement) {
final StatelessWidget widget = element.widget;
if (widget is NotificationListener<Notification>) {
if (widget._dispatch(this, element)) // that function checks the type dynamically
return false;
}
}
return true;
}

我们进入Element内部看一下visitAncestorElements方法的实现:


@override
void visitAncestorElements(bool visitor(Element element)) {
assert(_debugCheckStateIsActiveForAncestorLookup());
Element? ancestor = _parent;
while (ancestor != null && visitor(ancestor))
ancestor = ancestor._parent;
}

当有父级节点,并且visitor方法返回true的时候执行while循环,visitorNotification类传进来的方法,回过头再看visitor方法的实现,当Elementvisitor方法传递的ancestorNotificationListener类的情况下,再判断widget._dispatch方法,而widget._dispatch方法:


final NotificationListenerCallback<T>? onNotification;

bool _dispatch(Notification notification, Element element) {
if (onNotification != null && notification is T) {
final bool result = onNotification!(notification);
return result == true; // so that null and false have the same effect
}
return false;
}

就是我们在外面写的onNotification方法的实现,我们在外面实现的onNotification方法返回true(即阻止事件继续向上传递),上面的while循环主要是为了执行我们onNotification里面的方法.


总结一下:MyNotification执行dispatch方法,传递context,根据当前context向父级查找对应NotificationListener,并且执行NotificationListener里面的onNotification方法,返回true,则事件不再向上级传递,如果返回false则事件继续向上一个NotificationListener传递,并执行里面对应的方法。Notification主要用在同一个页面中,子级向父级传值,比较轻量级,不过如果我们用了Provider可能就就直接借助Provider传值了。


Eventbus


Eventbus用于两个不同的页面,可以跨多级页面传值,用法也比较简单,我创建了一个EventBusUtil来创建一个单例


import 'package:event_bus/event_bus.dart';
class EventBusUtil {
static EventBus ? _instance;
static EventBus getInstance(){
if (_instance == null) {
_instance = EventBus();
}
return _instance!;
}
}

在第一个页面监听:


class Page19PassByValue extends StatefulWidget {
@override
_Page19PassByValueState createState() => _Page19PassByValueState();
}

class _Page19PassByValueState extends State<Page19PassByValue> {
UserInfoBean userInfoBean = UserInfoBean(name: 'wf', address: 'address');
@override
void initState() {
super.initState();
EventBusUtil.getInstance().on<UserInfoBean>().listen((event) {
setState(() {
userInfoBean = event;
});
});
}

@override
void dispose() {
super.dispose();
//不用的时候记得关闭
EventBusUtil.getInstance().destroy();
}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('PassByValue'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(userInfoBean.name),
Text(userInfoBean.address),
TextButton(onPressed: (){
Navigator.of(context).push(CupertinoPageRoute(builder: (_){
return EventBusDetailPage();
}));
}, child: Text('点击跳转'))

],
),
),
);
}
}

在第二个页面发送事件:


class EventBusDetailPage extends StatefulWidget {
@override
_EventBusDetailPageState createState() => _EventBusDetailPageState();
}

class _EventBusDetailPageState extends State<EventBusDetailPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('EventBusDetail'),
),
body: Center(
child: TextButton(onPressed: (){
EventBusUtil.getInstance().fire(UserInfoBean(name: 'name EventBus', address: 'address EventBus'));
}, child: Text('点击传值')),
),
);
}
}

我们看一下EventBus的源码,发现只有几十行代码,他的内部是创建了一个StreamController,通过StreamController来实现跨组件传值,我们也可以直接使用一下这个StreamController实现页面传值:


class Page19PassByValue extends StatefulWidget {
@override
_Page19PassByValueState createState() => _Page19PassByValueState();
}

StreamController controller = StreamController();

class _Page19PassByValueState extends State<Page19PassByValue> {

//设置一个初始值
UserInfoBean userInfoBean = UserInfoBean(name: 'wf', address: 'address');
@override
void initState() {
super.initState();
controller.stream.listen((event) {
setState(() {
userInfoBean = event;
});
});
}

@override
void dispose() {
super.dispose();
//页面销毁的时候记得关闭
controller.close();
}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('PassByValue'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(userInfoBean.name),
Text(userInfoBean.address),
TextButton(onPressed: (){
Navigator.of(context).push(CupertinoPageRoute(builder: (_){
return MyStreamControllerDetail();
}));
}, child: Text('点击跳转'))
],
),
)
);
}
}

class MyStreamControllerDetail extends StatefulWidget {
@override
State<StatefulWidget> createState() {
return _MyStreamControllerDetailState();
}
}
class _MyStreamControllerDetailState extends State <MyStreamControllerDetail> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('StreamController'),
),
body: Center(
child: TextButton(onPressed: (){
//返回上个页面,会发现页面的数据已经变了
controller.sink.add(UserInfoBean(name: 'StreamController pass name: 123', address: 'StreamController pass address 123'));
}, child: Text('点击传值'),),
),
);
}
}

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

Swift 指针的应用

iOS
Swift与指针由于Swift本身是一门较为现代的语言,支持很多高级特性,所以对于程序员来说,大部分时候不需要用到指针这种更“底层”的特性。而Swift语言的设计者也在尽可能希望开发者能尽量少的使用指针。但是,“慎用”不代表“不能用”,更不代表“没用”。相反,...
继续阅读 »

Swift与指针

由于Swift本身是一门较为现代的语言,支持很多高级特性,所以对于程序员来说,大部分时候不需要用到指针这种更“底层”的特性。而Swift语言的设计者也在尽可能希望开发者能尽量少的使用指针。

但是,“慎用”不代表“不能用”,更不代表“没用”。相反,指针非常有用,在某些场景下还是必不可少的特性。尤其是开发工作和系统底层特性、内存处理、高性能需求息息相关时。

所以,Swift通过在施加某种限制的前提下为开发者暴露了指针的使用接口,本篇文章重点介绍Swift使用指针的相关类型、函数,以及在实践应用中灵活使用指针解决问题的技巧。

类型限定的指针 UnsafePointer

Swift通过UnsafePointer<T>来指向一个类型为T的指针,该指针的内容是 只读 的,对于一个UnsafePointer<T>变量来说,通过pointee成员即可获得T的值。

func call(_ p: UnsafePointer<Int>) {
print("\(p.pointee)")
}
var a = 1234
call(&a) // 打印:1234

以上例子中函数call接收一个UnsafePointer<Int>类型作为参数,变量a通过在变量名前面加上&将其地址传给call。函数call直接打印指针的pointee成员,该成员就是a的值,所以最终打印结果为1234

注1:&aswift提供的语法特性,用于传递指针,但它有严格的适用场景限制。

注2:注意示例中对于变量a使用了var声明,而事实上UnsafePointer是“常量指针”,并不会修改a的内容,即使是这样a还是必须用var声明,如果用let会报错Cannot pass immutable value as inout argument: 'a' is a 'let' constant。这是因为swift规定UnsafePointer作为参数只能接收inout修饰的类型,而inout修饰的类型必然是可写的,所以使用var在所难免。

内容可写的类型限定指针 UnsafeMutablePointer

既然有 内容只读 指针,必须也得有 内容可读写 指针搭配才行,在Swift中,内容可读写的类型限定指针为UnsafeMutablePointer<T>类型,就和名字描述的那样,它和UnsafePointer最大的区别就是它指向的内容是可更改的,并且更改后指向的“数据源”也会被改动。

func modify(_ p: UnsafeMutablePointer<Int>) {
p.pointee = 5678
}
var a = 1234
modify(&a)
print("\(a)") // 打印:5678

在以上的例子中,指针p指向的值被重新赋值为5678,这也使得指针的“源”,即变量a的值发生变化,最终打印a的结果可以看出a被修改为5678

指针的辅助函数 withUnsafePointer

通过函数withUnsafePointer,获得指定类型的对应指针。该函数原型如下:

func withUnsafePointer<T, Result>(to value: inout T, _ body: (UnsafePointer<T>) throws -> Result) rethrows -> Result

这个函数原型看似很复杂很长,其实只要理解它所需要的信息只有两个:

  1. 指针指向类型是什么。
  2. 想要返回的指针地址是什么。
var a = 1234
let p = withUnsafePointer(to: &a) { $0 }
print("\(p.pointee)") // 打印:1234

以上例子是withUnsafePointer最精简的调用例子,我们定义了一个整形a,而p就是指向a的整形指针,事实上它的类型会被自动转换为UnsafePointer<Int>,第二个参数被简化为了{ $0 },它传入了一个代码块,代码块接收一个UnsafePointer<Int>参数,该参数即是a的地址,直接通过$0将它返回,即得到了a的指针,最终它被传给了p

对于第二个参数,或许有人会产生疑问,它似乎是没有意义的参数,大部分时候我们不是直接返回a的地址吗,为什么要多此一举通过代码块返回一次?这个疑问是合理的,绝大多数时候确实第二个参数显得有些多余,当然了,有时候可以通过第二个参数提供指针偏移的灵活性,如下例子可以提供一个案例。

var a = [1234, 5678]
let p = withUnsafePointer(to: &a[0]) { $0 + 1 }
print("\(p.pointee)") // 打印:5678

以上例子中,通过在第二个参数中对地址施加偏移,可以原来指向数组首个元素的地址偏移到第二个地址中。

另外,由于withUnsafePointer带着两个泛型参数,这意味着第二个参数可以是不同的类型。

var a = 1234
let p = withUnsafePointer(to: &a) { $0.debugDescription }
print("\(p)")

以上例子中,withUnsafePointer返回的并不是UnsafePointer<Int>类型,甚至不是指针,而是一个字符串,字符串保存着a对应指针的debug信息。

注1:同样的,和withUnsafePointer相对应的,还有withUnsafeMutablePointer,一样是只读和可读写的区别。读者可以自行测试用法。 注2:基本上Swift指针操作的with系列函数都提供了第二个参数用来灵活的提供函数的返回类型。

获取指针并进行字节级操作 withUnsafeBytes

有时候,我们需要对某块内存进行字节级编程。比如我们用一个32位整形来表示一个32位的内存块,对内存中的每个字节进行读写操作。

通过withUnsafeBytes,可以得到某个类型的数据的字节指针,从而可以对它们进行字节级编程。

var a: UInt32 = 0x12345678
let p = withUnsafeBytes(of: &a) { $0 }
var log = ""
for item in p {
let hex = NSString(format: "%x", item)
log += "\(hex)"
}
print("\(p.count)") // 打印:4
print("\(log)") // 对于小端机器会打印:78563412

在以上例子中,withUnsafeBytes返回了一个类型UnsafeRawBufferPointer,该类型代表着一个字节级的内存块,并提供了等价于数组操作,所以你可以通过下标索引、for循环的方式来处理返回的对象。

例子中的a是一个32位整形,所以p指针的count返回的是4,单位为字节。 在本例中,对内存块p从低到高逐字节的打印每个字节的16进制值。 具体打印出来的结果因运行的机器而异,在大端机器上,打印的结果是12345678,而在小端机器上打印结果则是78563412

注:大端和小端决定了一个基础数据单元在内存中是如何按序存放的,例如小端机器会将基本数据单元的低位放在内存的低位,由低到高排列,而大端机器则相反。具体相关知识可查阅维基百科。大部分情况下,同一台机器采用的字节序列是一致的,某些CPU可以配置大小端的切换。

指向连续内存的指针 UnsafeBufferPointer

Swift的数组提供了函数withUnsafeBufferPointer,通过它我们可以方便的用指针来处理数组。如下例子:

let a: [Int32] = [1, 2, -1, -2, 5, 6]
let p = a.withUnsafeBufferPointer { $0 }
print("\(p.count)") // 打印:6
print("\(p[3])") // 打印:-2

在该例子中,通过withUnsafeBufferPointer,可以获得变量pp的类型为UnsafeBufferPointer<Int32>,它代表着一整块的连续内存,我们可以像看待数组一样看待它,并且它也支持大部分数组操作。

指针的类型转换

介绍了那么多Swift中的指针类型,每一种都有各自的用途,但是在实际开发中,很可能我们需要将一个指针类型转换为特定的指针类型。

以下例子提供了几个类型指针之间的转换

let a: [Int32] = [1, 2, -1, -2, 5, 6]
// 类型 p: UnsafeBufferPointer<Int32>
let p = a.withUnsafeBufferPointer { $0 }
// 类型 p2: UnsafePointer<UInt32>
let p2 = p.baseAddress!.withMemoryRebound(to: UInt32.self, capacity: p.count) { $0 }
// 类型 p3: UnsafeBufferPointer<UInt32>
let p3 = UnsafeBufferPointer(start: p2, count: p.count)
print("\(p3.count)") // 打印:6
print("\(p3[3])") // 打印:4294967294

以上例子中,我们获得了以下三个指针类型

  1. UnsafeBufferPointer<Int32>类型的指针p
  2. UnsafePointer<UInt32>类型的指针p2
  3. UnsafeBufferPointer<UInt32>类型的指针p3

该例子有部分细节必须讲明,首先是baseAddress,通过该成员得到UnsafeBufferPointer基地址,获得的数据类型是UnsafePointer<>

由于a指向的元数据类型是Int32,所以其baseAddress类型即是UnsafePointer<Int32>

在本例中,我们将元数据类型由Int32改为UInt32,这里用到了UnsafePointer的成员函数withMemoryRebound,通过它将UnsafePointer<Int32>转换为UnsafePointer<UInt32>

最后一部分,我们创建了一个新的指针UnsafeBufferPointer,通过其构造函数,我们让该指针的起始位置设定为p2,元素个数设定为p的元素个数,这样就成功得到了一个UnsafeBufferPointer<UInt32>类型。

接下来的打印语句,我们可以看到p3类型的count成员依然是6,而p3[3]打印的结果却是4294967294,而不是数组a对应元素的-2,这是因为从p3的角度来看,它是用UInt32类型来“看待”原先的Int32数据元素。

回调函数的实用性

前面讨论withUnsafePointer时我曾经提过第二个回调参数似乎略显鸡肋,事实上它非常有用,通过回调函数,我们可以对上一段代码进行“优化”。

let a: [Int32] = [1, 2, -1, -2, 5, 6]
// 类型 p: UnsafeBufferPointer<Int32>
let p = a.withUnsafeBufferPointer { $0 }
// 类型 p3: UnsafeBufferPointer<UInt32>
let p3 = p.baseAddress!.withMemoryRebound(to: UInt32.self, capacity: p.count) {
UnsafeBufferPointer(start: $0, count: p.count)
}
print("\(p3.count)") // 打印:6
print("\(p3[3])") // 打印:4294967294

可以看到利用回调函数,我们把原先的p2p3代码合并了,这样可以让withMemoryRebound立刻返回UnsafeBufferPointer<UInt32>类型。

注:事实上该回调还可以不断“套娃”,也就是说可以直接把p3部分的代码和p也进行合并,但是出于可读性考虑,开发者应自己根据需要选择性进行嵌套。

Swift中的空指针:UnsafeRawPointer

就像C语言有void*(即空指针)一样,Swift也有自己的空指针,它通过类型UnsafeRawPointer来获得,我们知道,空指针没有指向特定的类型,又“可以”指向任何类型,灵活性极高,也需要程序员自己能够理解和处理好对应的细节。

同样是将UnsafeBufferPointer<Int32>转换为UnsafeBufferPointer<UInt32>,以下代码通过UnsafeRawPointer来实现。

let a: [Int32] = [1, 2, -1, -2, 5, 6]
let p = a.withUnsafeBufferPointer { $0 }
let p2 = UnsafeRawPointer(p.baseAddress!).assumingMemoryBound(to: UInt32.self)
let p3 = UnsafeBufferPointer(start: p2, count: p.count)
print("\(p3.count)") // 打印:6
print("\(p3[3])") // 打印:4294967294

在该例子中我们通过空指针完成了如下操作:

  1. UnsafeRawPointer通过构造函数接收了p的“基地址”构造了一个空指针类型。
  2. 由于构造的是空指针类型,我们需要对它进行类型转换,通过assumingMemoryBound把它转换成新的数据类型UnsafePointer<UInt32>
  3. 通过UnsafeBufferPointer构造函数重新构造了一个新的指针UnsafeBufferPointer<UInt32>

通过指针动态创建、销毁内存

有时候我们需要动态开辟和管理一块内存,最后释放它,Swift提供了UnsafeMutablePointer的成员函数allocate来处理该工作。

let p = UnsafeMutablePointer<Int32>.allocate(capacity: 1)
p.initialize(to: 0) // 初始化
p.pointee = 32
print("\(p.pointee)") // 打印:32
p.deinitialize(count: 1) // 反初始化
p.deallocate()

以上例子中我们提供了一个存放32位整形的内存块,容量为1(即其容量为1个32位整形,实际就是 4 个字节)。 接下来的代码示例较为简单,即开辟内存,初始化、赋值、反初始化,释放内存的流程。

Swift指针类型和C指针类型的对应关系

Swift的指针类型看似繁多,事实上只是对C指针类型进行了封装和类别整理,并增加了一定程度上的安全性。

下表提供了SwiftC部分指针类型和函数的大致等价关系。

SwiftC描述
UnsafeMutableRawPointervoid*空指针
UnsafeMutablePointerT*类型指针
UnsafeRawPointerconst void*常量空指针
UnsafePointerconst T*常量类型指针
UnsafeMutablePointer.allocate(int32_t*)malloc分配内存

可以看出Swift的指针并不神秘,它只是映射了C语言指针的对应操作(只是乍看一下更复杂)。

进阶实践:C标准库函数的映射调用

Swift提供了大量的C标准库的桥接调用,也就是说,我们可以像调用C语言库函数一样调用Swift函数。这其中包括很多有用的函数,如memcpystrcpy等。

下面通过一段示例程序来展现这类函数的调用。

var n = 10086
// malloc
let p = malloc(MemoryLayout<Int32>.size)!
// memcpy
memcpy(p, &n, MemoryLayout<Int32>.size)
let p2 = p.assumingMemoryBound(to: Int32.self)
print("\(p2.pointee)") // 打印:10086
// strcpy
let str = "abc".cString(using: .ascii)!
if str.count != MemoryLayout<Int32>.size {
return
}
let pstr = p.assumingMemoryBound(to: CChar.self)
strcpy(pstr, str)
print("\(String(cString: pstr))") // 打印:abc
// strlen
print("\(strlen(pstr))") // 打印: 3
// memset
memset(p, 0, MemoryLayout<Int32>.size)
print("\(p2.pointee)") // 打印:0
// strcat
strcat(pstr, "h".cString(using: .ascii)!)
strcat(pstr, "i".cString(using: .ascii)!)
print("\(String(cString: pstr))") // 打印:hi
// strstr
let s = strstr(pstr, "i")!
print("\(String(cString: s))") // 打印:i
// strcmp
print("\(strcmp(pstr, "hi".cString(using: .ascii)!))") // 打印:0
// free
free(p)

以上demo提供了如memsetstrcpyC库函数原型的调用方式。通过该例子可以看出指针操作的灵活性,对于开辟的一块4个字节的内存,我们既可以把它看做一个32位整形,又可以把它看做4个ascii字符,当把它看做4个字符时,我们可以用它存放abc三个字符,并在最后一个字节用\0作为终止符。

总结

指针可以让我们用更底层的视角来看待程序和数据,在某些场景下,通过指针我们有机会开发出更高性能的代码。但同时指针的使用有时也是极复杂易出错的。如何使用好这把双刃剑,全看开发者自身的能力和态度。本文仅仅是抛砖引玉的提供了Swift指针的基本框架和使用技巧,大量细节因为篇幅原因并未提及,还需要读者自行不断研究和学习。

本文的样例代码已上传至我的github,请参见地址:github.com/FengHaiTong… 。


作者:风海铜锣
链接:https://juejin.cn/post/7030789069915291661
来源:稀土掘金

收起阅读 »

Swift热更新(1)- 免费版接入

iOS
SOT学习和使用的成本主要集中在前期,主要涉及编译流程的修改。之前介绍了纯OC项目如何接入「 OC接入例子 」。本文介绍如何给纯Swift项目接入SOT,包括免费版和网站版。本文以开源的「 SwiftMessages 」Demo为例,该工程全部用Swift语言...
继续阅读 »

SOT学习和使用的成本主要集中在前期,主要涉及编译流程的修改。之前介绍了纯OC项目如何接入「 OC接入例子 」。本文介绍如何给纯Swift项目接入SOT,包括免费版和网站版。

本文以开源的「 SwiftMessages 」Demo为例,该工程全部用Swift语言开发。这里把修改好的版本上传到了git上,分支为 「 sotdemo 」,Debug模式下接入了免费版,Release模式接入了网站版,读者也可以直接用该分支测试。

现在开始从头讲解,clone原本的工程后,命令行cd进入根目录,使用版本切换命令:git checkout 1e49de7b3780b699(因本文档制作于21年10月21号,以当日版本为准)。首先进入Demo目录,打开Demo.xcodeproj工程,scheme默认就已经选中了Demo:...

我使用的是Xcode12.4,可以直接编译成功,启动APP能看到画面(模拟器):

......

点击最上面的MESSAGE VIEW控件,会弹出一个错误提示窗口,今天我们就来用SOT热更的方式修改错误提示的文案:...

Step1: 配置编译环境

参考「 免费版 」的step1到step3,step3拷贝的sotconfig.sh放到项目的Demo的目录下:...

用文本编辑器打开sotconfig.sh,修改EnableSot=1:...

Step2: 修改编译选项

添加热更需要的编译选项,添加SOT虚拟机静态库等,步骤如下:

  1. 选中Demo工程,然后选择Demo这个Target,再选择Build Settings:...

  2. Other Linker Flags添加-sotmodule $(PRODUCT_NAME) /Users/sotsdk-1.0/libs/libsot_free.a -sotsaved $(SRCROOT)/sotsaved/$(CONFIGURATION)/$(CURRENT_ARCH) -sotconfig $(SRCROOT)/sotconfig.sh,每个选项的意义如下:

    • -sotmodule是module的名字,可以直接用$(PRODUCT_NAME),也可以自定义名字;
    • -sotsaved是编译中间产物保存的目录,补丁自动化生成需要对比前后编译的产物来生成补丁;
    • -sotconfig指定了项目sotconfig.sh的路径,该脚本控制sot编译器的工作,用$(SRCROOT)引用到
    • /Users/sotsdk-1.0/libs/libsot_free.a是SOT虚拟机静态库的路径,链接的是免费版的虚拟机
  3. Other C Flags以及Other Swift Flags添加-sotmodule $(PRODUCT_NAME) -sotconfig $(SRCROOT)/sotconfig.sh,意义跟上一步是一样的,需要保持一致。经过上面两步,相关的编译配置结果如下图:...

  4. 因为SOT SDK库文件编译时不带Bitcode,所以也需要把Target的Enable Bitcode设为No...


Step3: 增加拷贝补丁脚本

SDK里提供了一个便利脚本,路径在sdk目录的project-script/sot_package.sh,它会把生成的补丁拷贝到Bundle文件夹下,在每次项目编译成功时调用该脚本,添加步骤如下:

...

脚本内容为:sh /Users/sotsdk-1.0/project-script/sot_package.sh "$SOURCE_ROOT/sotconfig.sh" "$SOURCE_ROOT/sotsaved/$CONFIGURATION" Demo...

把Based on dependency analysis的勾去掉


Step4: 链接C++库

SOT需要压缩库和c++标准库的支持,还是在这个页面下,打开Link Binary With Libraries页...

点击加号,分别加入这两,libz.tbdlibc++.tbd...


Step5: 调用SDK API

需要用Swift代码调用OC代码,已经提供了一个样例代码在SDK的swift-call-objc目录中,可以直接添加到Demo工程中。点击Xcode软件的File按钮,接着点击Add Files to "Demo",如下图所示:...

选择到SDK目录swift-call-objc中,同时选中callsot.h和callsot.m两个文件,勾选下面的Copy items if needed,勾选Add to targets:中的Demo target,如下图所示:...

点击Add按钮,添加后会弹出询问:是否创建桥接文件。点击按钮Create Bridging Header:...

然后可以看到项目中多了3个文件,分别是callsot.h,callsot.m和Demo-Bridging-Header.h:...

打开Demo-Bridging-Header.h,加入一行代码#import "callsot.h"...

打开AppDelegate.swift,加入两行代码let sot = CallSot()sot.initSot()...


测试热更

Step1: 热更注入

按上面配置完之后,确保sotconfig.sh的配置是,EnableSot=1以及GenerateSotShip=0,先Clean Build Folder一下,然后再Build:...

然后看编译日志的输出,Link日志可以看到run sot link等输出,会告诉你每个文件里哪些函数可以被热更等信息:......

项目编译成功了,该APP可以正常启动。同时它具备了热更能力,可以加载补丁改变程序的代码逻辑,下面介绍如何生成补丁来测试它。


Step2: 生成补丁

上一步进行了热更注入的编译,当时的代码保存到了Demo/sotsaved这个文件夹下,用来和新代码比较生成补丁。生成补丁步骤如下:

  1. 首先启动SOT生成补丁模式,修改sotconfig.sh为EnableSot=1GenerateSotShip=1
  2. ...
  3. 接下来直接在Xcode里修改源代码,把ViewController.swift文件的”Something is horribly wrong!“改成了”SOT is great“:......
  4. 生成补丁跟OC项目不一样,每次都需要先Clean项目,再Build项目。然后查看编译日志输出,可以看到生成了补丁并且被脚本拷贝到了Bundle目录下,然后再展开Link Demo(x86_64)的编译日志:...可以看到此时的Link是用来生成补丁的,日志里也显示了函数demoBasics被修改了:...
  5. 生成出来的补丁原始文件保存到了Demo/sotsaved/Debug/x86_64/ship/ship.sot,还记得之前加了一个script到Build Phase中吗?它会每次编译结束时,会把这个补丁拷贝到了Bundle目录中,并且添加CPU架构到文件名中。可以在Bundle中看到这个补丁,至此完毕。

Step3: 加载补丁

启动APP,API会判断Bundle内是否有补丁,有则加载,加载成功的日志大概如下,提示有一个模块加载了热更补丁:...之后点击最上面的MESSAGE VIEW控件,发现弹出的文案变成了SOT is great:...

如果去Xcode断点调试demoBasics,会发现无法断住了,因为实际执行补丁代码的是SOT虚拟机。

顺便提一嘴,GenerateSotShip=1时,编译APP用的是保存在sotsaved目录下的代码,所以无论怎么修改Xcode里的代码,如果没有把补丁拷贝到Bundle目录里,那么APP都是最后一次GenerateSotShip=0热更注入时的样子。

如果怀疑,可以把拷贝补丁的Script脚本从Build Phases删除,可以发现怎么改代码都不会生效了。


作者:忒修斯科技
链接:https://juejin.cn/post/7026197659006287903
来源:稀土掘金

收起阅读 »

Swift开发规范

iOS
Swift开发规范前言开发规范的目的是保证统一项目成员的编码风格,并使代码美观,每个公司对于代码的规范也不尽相同,希望该份规范能给大家起到借鉴作用。本文为原创,如需转载请说明原文地址链接。命名规约代码中的命名严禁使用拼音及英文混合的方式,更不允许直接出现中文的...
继续阅读 »

Swift开发规范

前言

开发规范的目的是保证统一项目成员的编码风格,并使代码美观,每个公司对于代码的规范也不尽相同,希望该份规范能给大家起到借鉴作用。本文为原创,如需转载请说明原文地址链接。

命名规约

  • 代码中的命名严禁使用拼音及英文混合的方式,更不允许直接出现中文的方式,最好也不要使用下划线或者美元符号开头;
  • 文件名、class、struct、enum、protocol 命名统一使用 UpperCamelCase 风格;
  • 方法名、参数名、成员变量、局部变量、枚举成员统一使用 lowerCamelCase 风格
  • 全局常量命名使用 k 前缀 + UpperCamelCase 命名;
  • 扩展文件,用“原始类型名+扩展名”作为扩展文件名,其中原始类型名及扩展名也使用 UpperCamelCase 风格,如UIView+Frame.swift
  • 工程中文件夹或者 Group 统一使用 UpperCamelCase 风格,一律使用单数形式;
  • 命名中出现缩略词时,缩略词要么全部大写,要么全部小写,以首字母大小写为准,通用缩略词包括 JSON、URL 等;如class IDUtil {}func idToString() { }
  • 不要使用不规范的缩写,如 AbstractClass“缩写”命名成 AbsClass 等,不怕名称长,就怕名称不明确。
  • 文件名如果有复数含义,文件名应使用复数形式,如一些工具类;

修饰规约

  • 能用 let 修饰的时候,不要使用 var;
  • 修饰符顺序按照 注解、访问限制、static、final 顺序;
  • 尽可能利用访问限制修饰符控制类、方法等的访问限制;
  • 写方法时,要考虑这个方法是否会被重载。如果不会,标记为 final,final 会缩短编译时间;
  • 在编写库的时候需要注意修饰符的选用,遵循开闭原则;

格式规约

  • 类、函数左大括号不另起一行,与名称之间留有空格
  • 禁止使用无用分号
  • 代码中的空格出现地点
    • 注释符号与注释内容之间有空格
    • 类继承, 参数名和类型之间等, 冒号前面不加空格, 但后面跟空格
    • 任何运算符前后有空格
    • 表示返回值的 -> 两边
    • 参数列表、数组、tuple、字典里的逗号后面有一个空格
  • 方法之间空一行
  • 重载的声明放在一起,按照参数的多少从少到多向下排列
  • 每一行只声明一个变量
  • 如果是一个很长的数字时,建议使用下划线按照语言习惯三位或者四位一组分割连接。
  • 表示单例的静态属性,一般命名为 shared 或者 default
  • 如果是空的 block,直接声明{ },括号之间不需换行
  • 解包时推荐使用原有名字,前提是解包后的名字与解包前的名字在作用域上不会形成冲突
  • if 后面的 else\else if, 跟着上一个 if\else if 的右括号
  • switch 中, case 跟 switch 左对齐
  • 每行代码长度应小于 100 个字符,或者阅读时候不应该需要滚动屏幕,在正常范围内可以看到完整代码
  • 实现每个协议时, 在单独的 extension 里来实现

简略规约

  • Swift 会被结构体按照自身的成员自动生成一个非 public 的初始化方法,如果这个初始化方法刚好适合,不要自己再声明
  • 类及结构体初始化方法不要直接调用.init,直接直接省略,使用()
  • 如果只有一个 get 的计算属性,忽略 get
  • 数据定义时,尽量使用字面量形式进行自动推断,如果上下文不足以推断字面量类型时,需要声明赋值类型
  • 省略默认的访问权限(internal)
  • 过滤, 转换等, 优先使用 filter, map 等高阶函数简化代码,并尽量使用最简写
  • 使用闭包时,尽量使用最简写
  • 使用枚举属性时尽量使用自动推断,进行缩写
  • 无用的代码及时删除
  • 尽量使用各种语法糖
  • 访问实例成员或方法时尽量不要使用 self.,特殊场景除外,如构造函数时
  • 当方法无返回值时,不需添加 void

注释规约

  • 文档注释使用单行注释,即///,不使用多行注释,即/***/。 多行注释用于对某一代码段或者设计进行描述
  • 对于公开的类、方法以及属性等必须加上文档注释,方法需要加上对应的Parameter(s)ReturnsThrows 标签,强烈建议使用⌥ ⌘ /自动生成文档模板
  • 在代码中灵活的使用一些地标注释,如MARKFIXMETODO,当同一文件中存在多种类型定义或者多种逻辑时,可以使用Mark进行分组注释
  • 尽量将注释另起一行,而不是放在代码后

其他

  • 不要使用魔法值(即未经定义的常量);
  • 函数参数最多不得超过 8 个;寄存器数目问题,超过 8 个会影响效率;
  • 图形化的字面量,#colorLiteral(...)#imageLiteral(...)只能用在 playground 当做自我练习使用,禁止在项目工程中使用
  • 避免强制解包以及强制类型映射,尽量使用if let 或 guard let进行解包,禁止try!形式处理异常,避免使用隐式解包
  • 避免判断语句嵌套层次太深,使用 guard 提前返回
  • 如果 for 循环在函数体中只有一个 if 判断,使用 for where 进行替换
  • 实现每个协议时, 尽量在单独的 extension 里来实现;但需要考虑到协议的方法是否有 override 的可能,定义在 extension 的方法无法被 override,除非加上@objc 方法修改其派发方式
  • 优先创建函数而不是自定义操作符
  • 尽可能少的使用全局命名空间,如常量、变量、方法等
  • 赋值数组、字典时每个元素分别占用一行时,最后一个选项后面也添加逗号;这样未来如果有元素加入会更加方便
  • 布尔类型属性使用 is 作为属性名前缀,返回值为布尔型类型的方法名使用 is 作为方法名作为前缀
  • 类似注解的修饰词单独占一行,如@objc,@discardableResult 等
  • extension 上不用加任何修饰符,修饰符加在 extension 内的变量或方法上
  • 使用 guard 来提前结束条件,避免形成判断嵌套;
  • 善用字典去减少判断,可将条件与结果分别当做 key 及 value 存入字典中;
  • 封装时善用 assert,方便问题排查;
  • 在闭包中使用 self 时使用捕获列表[weak self]避免循环引用,闭包开始判断 self 的有效性
  • 使用委托和协议时,避免循环引用,定义属性的时候使用 weak 修饰

工具

SwiftLint 工具 提示格式错误

SwiftFormat 工具 提示并修复格式错误

两者大部分格式规范都是一致的,少许规范不一致,两个工具之间使用不冲突,可以在项目中共存。我们通过配置文件可以控制启用或者关闭相应的规则,具体使用规则参照对应仓库的 REAMME.md 文件。

相关规范

Swift 官方 API 设计指南

google 发布的 Swift 编码规范


有一个技术的圈子与一群同道众人非常重要,来我的技术公众号及博客,这里只聊技术干货。


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

? 我的独立开发的故事

iOS
🐻 我的独立开发的故事我是独立开发者熊大,最近一年尝试了独立开发的滋味,也想和大家聊一聊独立开发的心历路程。 如果你也有开发一款app的想法,那你可以看一看我的独立开发的故事。我做过直播、相机、社交类APP。个人独立app 《imi》《今日计划》2020年,我...
继续阅读 »

🐻 我的独立开发的故事

我是独立开发者熊大,最近一年尝试了独立开发的滋味,也想和大家聊一聊独立开发的心历路程。 如果你也有开发一款app的想法,那你可以看一看我的独立开发的故事。

  • 我做过直播、相机、社交类APP。
  • 个人独立app 《imi》《今日计划》
  • 2020年,我想要尝试一下独立开发的方向。

第一款app的开发周期

做第一款软件《今日计划》时,周一到周六工作,大小周,晚上会有一些开发时间。

总体如下:

  • 每天1小时写app代码 * 60 = 60小时
  • 每周周日有4个小时 * 8 = 32小时
  • 清明节三天 (按照8小时/天tian计算):3*8 = 24小时

一共约120个小时:完成了设计到上线。

我也买了阿里云的ECS,用vapor搭建了后台,维护成本有点高,果断放弃了。

当我开心的把它分享给朋友时,朋友们都说他很丑,于是被贴上一系列标签『丑』、『直男审美』、『搭配有问题』、『太简单了吧』····,总而言之,没什么好的形容词。

(PS:T M D 我自己都感觉有点坑)

 报着期望,又紧急改版一次,更换了icon,改了一些设计。也就是现在的这一版。我在圈子里又推广了一波,登顶效率榜Top20(其实是各位兄弟给面子)。

后来陆陆续续也有一些下载,但由于工作紧张,没能持续更新迭代。

离职风波

《水印相机》这款app目前,摄影榜Top20,很荣幸是我从零带到百万日活的,深知好产品的指数爆发增长。我内心真的想去外边看看,想见识更多优秀的、有趣的人,于是世界那么大,我想出去看看,真的成为了我离职的最主要理由。

从上家公司收获的最大的便是经验,一份让我受用很多年的经验。

离职后,并不缺少内推的机会,但我还没想好该怎么走接下来的路,我在思考,是去大厂深造,还是开启自由职业呢?自己一直是个骄傲的人,毕业时我的薪资就是 xx k,不能为五斗米而折腰,干脆做个自由职业好了。于是把想法讲给周围的人,最后还是找了份工作,公司就在我家的旁边,上下班5分钟。

于是从7月份开始,我就几乎每天晚上有两个小时的时间为开启我的自由职业之路做准备,只要副业收入过万,就开始全职独立开发。

新app上线

2020.08 一个小伙伴,会飞的猪,加入了开发阵营。

2020.10 小满 加了开发阵营。

(由于特殊原因,名字保密)

2021年1月上线了新的免费App《imi-成就最好的自己》,这次的app,至少在UI上取得了程序员的好评,我们还没有正式推广,只是在小圈子里发了一下动态试试水。

我们小团队也开了个新的公·众·号:《独立开发者基地》,感兴趣额可以关注。

惭愧的是,由于新公司较忙,进行了几次通宵加班后,我严重的拖累了小团队的开发进度,本来应该是2020年底就应该上线的。

《imi》

这是一款风格可爱简单的规划、计划类软件,致敬自己,致敬青春。

imi寓意:我就是我,我们一定是不完美的,也许不成功,也许不漂亮,但这就是我,与众不同。

给张图看看:

这个idea是我想的,简单说就是一个计划类软件,里边有

  • 人生节点
  • 座右铭
  • 成就
  • 笔记
  • 喜欢的人
  • 倒计时
  • 指纹解锁
  • 云同步。

设计这款软件希望能让大家觉得有用,不知道软件的初衷是不是个伪命题。让时间见证吧。

独立开发者应该都知道霸榜很久的《时间规划局》,这次《imi》就是冲着它去的,她将作为我们的竞品之一,我想我们这么有情怀的app对标这样的工具类软件,是有点希望的(怕怕)。

希望大家下载: imi-成就 给予我们支持 ^_^

给独立开发者的福利

这个应该算是福利吧,我们小团队,整理出了app的加速库,《今日计划》《imi-成就》两款app都是基于这个加速库开发的。接下来的其他app也会基于这个加速库开发,意味着我们会持续完善、维护这个加速库。里边有很多实用的功能,欢迎star🌟。

加速库SpeedySwift仓库:https://github.com/Tliens/SpeedySwift

imi 中用到的第三方库:

  # Pods for App1125
pod 'HWPanModal', '~> 0.8.1'
pod 'RealmSwift', '~> 10.5.0'
pod 'ZLPhotoBrowser', '~> 4.1.2'
pod 'SwiftDate', '~> 6.3.1'
pod 'IceCream',:path =>'Dev-pods/IceCream' # 数据同步icloud
# pod 'FSPagerView' # 轮播图
# pod 'SwiftyStoreKit' # 内购组件
pod 'Schedule', '~> 2.1.0'
pod 'Hero', '~> 1.5.0'
pod 'BiometricAuthentication'
#依赖库
pod 'UMCCommon', '~> 2.1.4'
#统计 SDK
pod 'UMCAnalytics', '~> 6.1.0'


回顾2020

get的技能:

  • 有幸能主导组件化开发
  • 函数响应式编程
  • go服务端

展望2021

希望大家健康、开心

我们会继续维护,维护今日计划、imi。也会有新的app出现。

最后

天行健君子以自强不息,地势坤君子以厚德载物。

虽大部分努力都没有收获,但热爱诞生创造的婴孩。

与君共勉!!!

写于 2021.01.13 北京·安贞门
链接:https://juejin.cn/post/6917058456184684557
收起阅读 »

python协程(超详细)

1、迭代1.1 迭代的概念使用for循环遍历取值的过程叫做迭代,比如:使用for循环遍历列表获取值的过程# Python 中的迭代for value in [2, 3, 4]:    print(value)1.2 可迭代对象标准概念:在类...
继续阅读 »



1、迭代

1.1 迭代的概念

使用for循环遍历取值的过程叫做迭代,比如:使用for循环遍历列表获取值的过程

# Python 中的迭代
for value in [2, 3, 4]:
   print(value)

1.2 可迭代对象

标准概念:在类里面定义__iter__方法,并使用该类创建的对象就是可迭代对象

简单记忆:使用for循环遍历取值的对象叫做可迭代对象, 比如:列表、元组、字典、集合、range、字符串

1.3 判断对象是否是可迭代对象

# 元组,列表,字典,字符串,集合,range都是可迭代对象
from collections import Iterable
# 如果解释器提示警告,就是用下面的导入方式
# from collections.abc import Iterable

# 判断对象是否是指定类型
result = isinstance((3, 5), Iterable)
print("元组是否是可迭代对象:", result)

result = isinstance([3, 5], Iterable)
print("列表是否是可迭代对象:", result)

result = isinstance({"name": "张三"}, Iterable)
print("字典是否是可迭代对象:", result)

result = isinstance("hello", Iterable)
print("字符串是否是可迭代对象:", result)

result = isinstance({3, 5}, Iterable)
print("集合是否是可迭代对象:", result)

result = isinstance(range(5), Iterable)
print("range是否是可迭代对象:", result)

result = isinstance(5, Iterable)
print("整数是否是可迭代对象:", result)

# 提示: 以后还根据对象判断是否是其它类型,比如以后可以判断函数里面的参数是否是自己想要的类型
result = isinstance(5, int)
print("整数是否是int类型对象:", result)

class Student(object):
   pass

stu = Student()
result = isinstance(stu, Iterable)

print("stu是否是可迭代对象:", result)

result = isinstance(stu, Student)

print("stu是否是Student类型的对象:", result)

1.4 自定义可迭代对象

在类中实现__iter__方法

自定义可迭代类型代码

from collections import Iterable
# 如果解释器提示警告,就是用下面的导入方式
# from collections.abc import Iterable

# 自定义可迭代对象: 在类里面定义__iter__方法创建的对象就是可迭代对象
class MyList(object):

   def __init__(self):
       self.my_list = list()

   # 添加指定元素
   def append_item(self, item):
       self.my_list.append(item)

   def __iter__(self):
       # 可迭代对象的本质:遍历可迭代对象的时候其实获取的是可迭代对象的迭代器, 然后通过迭代器获取对象中的数据
       pass

my_list = MyList()
my_list.append_item(1)
my_list.append_item(2)
result = isinstance(my_list, Iterable)

print(result)

for value in my_list:
   print(value)

执行结果:

Traceback (most recent call last):
True
 File "/Users/hbin/Desktop/untitled/aa.py", line 24, in <module>
   for value in my_list:
TypeError: iter() returned non-iterator of type 'NoneType'

通过执行结果可以看出来,遍历可迭代对象依次获取数据需要迭代器

总结

在类里面提供一个__iter__创建的对象是可迭代对象,可迭代对象是需要迭代器完成数据迭代的

2、迭代器

2.1 自定义迭代器对象

自定义迭代器对象: 在类里面定义__iter____next__方法创建的对象就是迭代器对象

from collections import Iterable
from collections import Iterator

# 自定义可迭代对象: 在类里面定义__iter__方法创建的对象就是可迭代对象
class MyList(object):

   def __init__(self):
       self.my_list = list()

   # 添加指定元素
   def append_item(self, item):
       self.my_list.append(item)

   def __iter__(self):
       # 可迭代对象的本质:遍历可迭代对象的时候其实获取的是可迭代对象的迭代器, 然后通过迭代器获取对象中的数据
       my_iterator = MyIterator(self.my_list)
       return my_iterator


# 自定义迭代器对象: 在类里面定义__iter__和__next__方法创建的对象就是迭代器对象
class MyIterator(object):

   def __init__(self, my_list):
       self.my_list = my_list

       # 记录当前获取数据的下标
       self.current_index = 0

       # 判断当前对象是否是迭代器
       result = isinstance(self, Iterator)
       print("MyIterator创建的对象是否是迭代器:", result)

   def __iter__(self):
       return self

   # 获取迭代器中下一个值
   def __next__(self):
       if self.current_index < len(self.my_list):
           self.current_index += 1
           return self.my_list[self.current_index - 1]
       else:
           # 数据取完了,需要抛出一个停止迭代的异常
           raise StopIteration


my_list = MyList()
my_list.append_item(1)
my_list.append_item(2)
result = isinstance(my_list, Iterable)

print(result)

for value in my_list:
   print(value)

运行结果:

True
MyIterator创建的对象是否是迭代器: True
1
2

2.2 iter()函数与next()函数

  1. iter函数: 获取可迭代对象的迭代器,会调用可迭代对象身上的__iter__方法

  2. next函数: 获取迭代器中下一个值,会调用迭代器对象身上的__next__方法

# 自定义可迭代对象: 在类里面定义__iter__方法创建的对象就是可迭代对象
class MyList(object):

   def __init__(self):
       self.my_list = list()

   # 添加指定元素
   def append_item(self, item):
       self.my_list.append(item)

   def __iter__(self):
       # 可迭代对象的本质:遍历可迭代对象的时候其实获取的是可迭代对象的迭代器, 然后通过迭代器获取对象中的数据
       my_iterator = MyIterator(self.my_list)
       return my_iterator


# 自定义迭代器对象: 在类里面定义__iter__和__next__方法创建的对象就是迭代器对象
# 迭代器是记录当前数据的位置以便获取下一个位置的值
class MyIterator(object):

   def __init__(self, my_list):
       self.my_list = my_list

       # 记录当前获取数据的下标
       self.current_index = 0

   def __iter__(self):
       return self

   # 获取迭代器中下一个值
   def __next__(self):
       if self.current_index < len(self.my_list):
           self.current_index += 1
           return self.my_list[self.current_index - 1]
       else:
           # 数据取完了,需要抛出一个停止迭代的异常
           raise StopIteration

# 创建了一个自定义的可迭代对象
my_list = MyList()
my_list.append_item(1)
my_list.append_item(2)

# 获取可迭代对象的迭代器
my_iterator = iter(my_list)
print(my_iterator)
# 获取迭代器中下一个值
# value = next(my_iterator)
# print(value)

# 循环通过迭代器获取数据
while True:
   try:
       value = next(my_iterator)
       print(value)
   except StopIteration as e:
       break

2.3 for循环的本质

遍历的是可迭代对象

  • for item in Iterable 循环的本质就是先通过iter()函数获取可迭代对象Iterable的迭代器,然后对获取到的迭代器不断调用next()方法来获取下一个值并将其赋值给item,当遇到StopIteration的异常后循环结束。

遍历的是迭代器

  • for item in Iterator 循环的迭代器,不断调用next()方法来获取下一个值并将其赋值给item,当遇到StopIteration的异常后循环结束。

2.4 迭代器的应用场景

我们发现迭代器最核心的功能就是可以通过next()函数的调用来返回下一个数据值。如果每次返回的数据值不是在一个已有的数据集合中读取的,而是通过程序按照一定的规律计算生成的,那么也就意味着可以不用再依赖一个已有的数据集合,也就是说不用再将所有要迭代的数据都一次性缓存下来供后续依次读取,这样可以节省大量的存储(内存)空间。

举个例子,比如,数学中有个著名的斐波拉契数列(Fibonacci),数列中第一个数为0,第二个数为1,其后的每一个数都可由前两个数相加得到:

0, 1, 1, 2, 3, 5, 8, 13, 21, 34, ...

现在我们想要通过for...in...循环来遍历迭代斐波那契数列中的前n个数。那么这个斐波那契数列我们就可以用迭代器来实现,每次迭代都通过数学计算来生成下一个数。

class Fibonacci(object):

   def __init__(self, num):
       # num:表示生成多少fibonacci数字
       self.num = num
       # 记录fibonacci前两个值
       self.a = 0
       self.b = 1
       # 记录当前生成数字的索引
       self.current_index = 0

   def __iter__(self):
       return self

   def __next__(self):
       if self.current_index < self.num:
           result = self.a
           self.a, self.b = self.b, self.a + self.b
           self.current_index += 1
           return result
       else:
           raise StopIteration


fib = Fibonacci(5)
# value = next(fib)
# print(value)

for value in fib:
   print(value)

执行结果:

0
1
1
2
3

小结

迭代器的作用就是是记录当前数据的位置以便获取下一个位置的值

3、生成器

3.1 生成器的概念

生成器是一类特殊的迭代器,它不需要再像上面的类一样写__iter__()和__next__()方法了, 使用更加方便,它依然可以使用next函数和for循环取值

3.2 创建生成器方法1

  • 第一种方法很简单,只要把一个列表生成式的 [ ] 改成 ( )

my_list = [i * 2 for i in range(5)]
print(my_list)

# 创建生成器
my_generator = (i * 2 for i in range(5))
print(my_generator)

# next获取生成器下一个值
# value = next(my_generator)
#
# print(value)
for value in my_generator:
   print(value)

执行结果:

[0, 2, 4, 6, 8]
<generator object <genexpr> at 0x101367048>
0
2
4
6
8

3.3 创建生成器方法2

在def函数里面看到有yield关键字那么就是生成器

def fibonacci(num):
   a = 0
   b = 1
   # 记录生成fibonacci数字的下标
   current_index = 0
   print("--11---")
   while current_index < num:
       result = a
       a, b = b, a + b
       current_index += 1
       print("--22---")
       # 代码执行到yield会暂停,然后把结果返回出去,下次启动生成器会在暂停的位置继续往下执行
       yield result
       print("--33---")


fib = fibonacci(5)
value = next(fib)
print(value)
value = next(fib)
print(value)

value = next(fib)
print(value)

# for value in fib:
#     print(value)

在使用生成器实现的方式中,我们将原本在迭代器__next__方法中实现的基本逻辑放到一个函数中来实现,但是将每次迭代返回数值的return换成了yield,此时新定义的函数便不再是函数,而是一个生成器了。

简单来说:只要在def中有yield关键字的 就称为 生成器

3.4 生成器使用return关键字

def fibonacci(num):
a = 0
b = 1
# 记录生成fibonacci数字的下标
current_index = 0
print("--11---")
while current_index < num:
result = a
a, b = b, a + b
current_index += 1
print("--22---")
# 代码执行到yield会暂停,然后把结果返回出去,下次启动生成器会在暂停的位置继续往下执行
yield result
print("--33---")
return "嘻嘻"

fib = fibonacci(5)
value = next(fib)
print(value)
# 提示: 生成器里面使用return关键字语法上没有问题,但是代码执行到return语句会停止迭代,抛出停止迭代异常

# return 和 yield的区别
# yield: 每次启动生成器都会返回一个值,多次启动可以返回多个值,也就是yield可以返回多个值
# return: 只能返回一次值,代码执行到return语句就停止迭代

try:
value = next(fib)
print(value)
except StopIteration as e:
# 获取return的返回值
print(e.value)

提示:

  • 生成器里面使用return关键字语法上没有问题,但是代码执行到return语句会停止迭代,抛出停止迭代异常

3.5 yield和return的对比

  • 使用了yield关键字的函数不再是函数,而是生成器。(使用了yield的函数就是生成器)

  • 代码执行到yield会暂停,然后把结果返回出去,下次启动生成器会在暂停的位置继续往下执行

  • 每次启动生成器都会返回一个值,多次启动可以返回多个值,也就是yield可以返回多个值

  • return只能返回一次值,代码执行到return语句就停止迭代,抛出停止迭代异常

3.6 使用send方法启动生成器并传参

send方法启动生成器的时候可以传参数

def gen():
   i = 0
   while i<5:
       temp = yield i
       print(temp)
       i+=1

执行结果:

In [43]: f = gen()

In [44]: next(f)
Out[44]: 0

In [45]: f.send('haha')
haha
Out[45]: 1

In [46]: next(f)
None
Out[46]: 2

In [47]: f.send('haha')
haha
Out[47]: 3

In [48]:

**注意:如果第一次启动生成器使用send方法,那么参数只能传入None,一般第一次启动生成器使用next函数

小结

  • 生成器创建有两种方式,一般都使用yield关键字方法创建生成器

  • yield特点是代码执行到yield会暂停,把结果返回出去,再次启动生成器在暂停的位置继续往下执行

4、协程

4.1 协程的概念

协程,又称微线程,纤程,也称为用户级线程,在不开辟线程的基础上完成多任务,也就是在单线程的情况下完成多任务,多个任务按照一定顺序交替执行 通俗理解只要在def里面只看到一个yield关键字表示就是协程

协程是也是实现多任务的一种方式

协程yield的代码实现

简单实现协程

import time

def work1():
   while True:
       print("----work1---")
       yield
       time.sleep(0.5)

def work2():
   while True:
       print("----work2---")
       yield
       time.sleep(0.5)

def main():
   w1 = work1()
   w2 = work2()
   while True:
       next(w1)
       next(w2)

if __name__ == "__main__":
   main()

运行结果:

----work1---
----work2---
----work1---
----work2---
----work1---
----work2---
----work1---
----work2---
----work1---
----work2---
----work1---
----work2---
...省略...

小结

协程之间执行任务按照一定顺序交替执行

5、greenlet

5.1 greentlet的介绍

为了更好使用协程来完成多任务,python中的greenlet模块对其封装,从而使得切换任务变的更加简单

使用如下命令安装greenlet模块:

pip3 install greenlet

使用协程完成多任务

import time
import greenlet


# 任务1
def work1():
for i in range(5):
print("work1...")
time.sleep(0.2)
# 切换到协程2里面执行对应的任务
g2.switch()


# 任务2
def work2():
for i in range(5):
print("work2...")
time.sleep(0.2)
# 切换到第一个协程执行对应的任务
g1.switch()


if __name__ == '__main__':
# 创建协程指定对应的任务
g1 = greenlet.greenlet(work1)
g2 = greenlet.greenlet(work2)

# 切换到第一个协程执行对应的任务
g1.switch()

运行效果

work1...
work2...
work1...
work2...
work1...
work2...
work1...
work2...
work1...
work2...

6、gevent

6.1 gevent的介绍

greenlet已经实现了协程,但是这个还要人工切换,这里介绍一个比greenlet更强大而且能够自动切换任务的第三方库,那就是gevent。

gevent内部封装的greenlet,其原理是当一个greenlet遇到IO(指的是input output 输入输出,比如网络、文件操作等)操作时,比如访问网络,就自动切换到其他的greenlet,等到IO操作完成,再在适当的时候切换回来继续执行。

由于IO操作非常耗时,经常使程序处于等待状态,有了gevent为我们自动切换协程,就保证总有greenlet在运行,而不是等待IO

安装

pip3 install gevent

6.2 gevent的使用

import gevent

def work(n):
   for i in range(n):
       # 获取当前协程
       print(gevent.getcurrent(), i)

g1 = gevent.spawn(work, 5)
g2 = gevent.spawn(work, 5)
g3 = gevent.spawn(work, 5)
g1.join()
g2.join()
g3.join()

运行结果

<Greenlet "Greenlet-0" at 0x26d8c970488: work(5)> 0
<Greenlet "Greenlet-1" at 0x26d8c970598: work(5)> 0
<Greenlet "Greenlet-2" at 0x26d8c9706a8: work(5)> 0
<Greenlet "Greenlet-0" at 0x26d8c970488: work(5)> 1
<Greenlet "Greenlet-1" at 0x26d8c970598: work(5)> 1
<Greenlet "Greenlet-2" at 0x26d8c9706a8: work(5)> 1
<Greenlet "Greenlet-0" at 0x26d8c970488: work(5)> 2
<Greenlet "Greenlet-1" at 0x26d8c970598: work(5)> 2
<Greenlet "Greenlet-2" at 0x26d8c9706a8: work(5)> 2
<Greenlet "Greenlet-0" at 0x26d8c970488: work(5)> 3
<Greenlet "Greenlet-1" at 0x26d8c970598: work(5)> 3
<Greenlet "Greenlet-2" at 0x26d8c9706a8: work(5)> 3
<Greenlet "Greenlet-0" at 0x26d8c970488: work(5)> 4
<Greenlet "Greenlet-1" at 0x26d8c970598: work(5)> 4
<Greenlet "Greenlet-2" at 0x26d8c9706a8: work(5)> 4

可以看到,3个greenlet是依次运行而不是交替运行

6.3 gevent切换执行

import gevent

def work(n):
   for i in range(n):
       # 获取当前协程
       print(gevent.getcurrent(), i)
       #用来模拟一个耗时操作,注意不是time模块中的sleep
       gevent.sleep(1)

g1 = gevent.spawn(work, 5)
g2 = gevent.spawn(work, 5)
g3 = gevent.spawn(work, 5)
g1.join()
g2.join()
g3.join()

运行结果

<Greenlet at 0x7fa70ffa1c30: f(5)> 0
<Greenlet at 0x7fa70ffa1870: f(5)> 0
<Greenlet at 0x7fa70ffa1eb0: f(5)> 0
<Greenlet at 0x7fa70ffa1c30: f(5)> 1
<Greenlet at 0x7fa70ffa1870: f(5)> 1
<Greenlet at 0x7fa70ffa1eb0: f(5)> 1
<Greenlet at 0x7fa70ffa1c30: f(5)> 2
<Greenlet at 0x7fa70ffa1870: f(5)> 2
<Greenlet at 0x7fa70ffa1eb0: f(5)> 2
<Greenlet at 0x7fa70ffa1c30: f(5)> 3
<Greenlet at 0x7fa70ffa1870: f(5)> 3
<Greenlet at 0x7fa70ffa1eb0: f(5)> 3
<Greenlet at 0x7fa70ffa1c30: f(5)> 4
<Greenlet at 0x7fa70ffa1870: f(5)> 4
<Greenlet at 0x7fa70ffa1eb0: f(5)> 4

6.4 给程序打补丁

import gevent
import time
from gevent import monkey

# 打补丁,让gevent框架识别耗时操作,比如:time.sleep,网络请求延时
monkey.patch_all()


# 任务1
def work1(num):
   for i in range(num):
       print("work1....")
       time.sleep(0.2)
       # gevent.sleep(0.2)

# 任务1
def work2(num):
   for i in range(num):
       print("work2....")
       time.sleep(0.2)
       # gevent.sleep(0.2)



if __name__ == '__main__':
   # 创建协程指定对应的任务
   g1 = gevent.spawn(work1, 3)
   g2 = gevent.spawn(work2, 3)

   # 主线程等待协程执行完成以后程序再退出
   g1.join()
   g2.join()

运行结果

work1....
work2....
work1....
work2....
work1....
work2....

6.5 注意

  • 当前程序是一个死循环并且还能有耗时操作,就不需要加上join方法了,因为程序需要一直运行不会退出

示例代码

import gevent
import time
from gevent import monkey

# 打补丁,让gevent框架识别耗时操作,比如:time.sleep,网络请求延时
monkey.patch_all()


# 任务1
def work1(num):
   for i in range(num):
       print("work1....")
       time.sleep(0.2)
       # gevent.sleep(0.2)

# 任务1
def work2(num):
   for i in range(num):
       print("work2....")
       time.sleep(0.2)
       # gevent.sleep(0.2)



if __name__ == '__main__':
   # 创建协程指定对应的任务
   g1 = gevent.spawn(work1, 3)
   g2 = gevent.spawn(work2, 3)

   while True:
       print("主线程中执行")
       time.sleep(0.5)

执行结果:

主线程中执行work1....work2....work1....work2....work1....work2....主线程中执行主线程中执行主线程中执行..省略..
  • 如果使用的协程过多,如果想启动它们就需要一个一个的去使用join()方法去阻塞主线程,这样代码会过于冗余,可以使用gevent.joinall()方法启动需要使用的协程

    实例代码

 import time
import gevent

def work1():
   for i in range(5):
       print("work1工作了{}".format(i))
       gevent.sleep(1)

def work2():
   for i in range(5):
       print("work2工作了{}".format(i))
       gevent.sleep(1)


if __name__ == '__main__':
   w1 = gevent.spawn(work1)
   w2 = gevent.spawn(work2)
   gevent.joinall([w1, w2])  # 参数可以为list,set或者tuple

7、进程、线程、协程对比

7.1 进程、线程、协程之间的关系

  • 一个进程至少有一个线程,进程里面可以有多个线程

  • 一个线程里面可以有多个协程

关系图.png

7.2 进程、线程、线程的对比

  1. 进程是资源分配的单位

  2. 线程是操作系统调度的单位

  3. 进程切换需要的资源最大,效率很低

  4. 线程切换需要的资源一般,效率一般(当然了在不考虑GIL的情况下)

  5. 协程切换任务资源很小,效率高

  6. 多进程、多线程根据cpu核数不一样可能是并行的,但是协程是在一个线程中 所以是并发

小结

  • 进程、线程、协程都是可以完成多任务的,可以根据自己实际开发的需要选择使用

  • 由于线程、协程需要的资源很少,所以使用线程和协程的几率最大

  • 开辟协程需要的资源最少

作者:y大壮
来源:https://juejin.cn/post/6971037591952949256

收起阅读 »

android媲美微信扫码库

之前使用的是zxing封装的库,但是识别率和识别速度没法和微信比较,现在使用的Google开源识别库完全可以和微信媲美github:github.com/DyncKathlin…强烈推荐MIKit Barcode Scanning识别速度超快,基本上camer...
继续阅读 »



之前使用的是zxing封装的库,但是识别率和识别速度没法和微信比较,现在使用的Google开源识别库完全可以和微信媲美

github:github.com/DyncKathlin…

强烈推荐MIKit Barcode Scanning

识别速度超快,基本上camera抓取到二维码就能识别到其内容(这是重点)。
基于MIKit Barcode Scanning的识别库进行封装,操作简单。
支持识别多个二维码,条形码。
支持任意比例展示,可以1:2,1.5:2等,不会发生像拉伸变形。
使用camera,不是cameraX哦。

效果图


第一个是Google开源的,第二个是zxing开源的

使用方式

build.gradle引用

implementation 'com.github.dynckathline:barcode:2.5'

初始化和监听结果回调

        //构造出扫描管理器
      configViewFinderView(viewfinderView);
      mlKit = new MLKit(this, preview, graphicOverlay);
      //是否扫描成功后播放提示音和震动
      mlKit.setPlayBeepAndVibrate(true, true);
      //仅识别二维码
      BarcodeScannerOptions options =
              new BarcodeScannerOptions.Builder()
                      .setBarcodeFormats(
                              Barcode.FORMAT_QR_CODE,
                              Barcode.FORMAT_AZTEC)
                      .build();
      mlKit.setBarcodeFormats(null);
      mlKit.setOnScanListener(new MLKit.OnScanListener() {
          @Override
          public void onSuccess(List<Barcode> barcodes, @NonNull GraphicOverlay graphicOverlay, InputImage image) {
              showScanResult(barcodes, graphicOverlay, image);
          }

          @Override
          public void onFail(int code, Exception e) {

          }
      });

展示结果

private void showScanResult(List<Barcode> barcodes, @NonNull GraphicOverlay graphicOverlay, InputImage image) {
      if (barcodes.isEmpty()) {
          return;
      }

      mlKit.setAnalyze(false);
      CustomDialog.Builder builder = new CustomDialog.Builder(context);
      CustomDialog dialog = builder
              .setContentView(R.layout.barcode_result_dialog)
              .setLayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)
              .setOnInitListener(new CustomDialog.Builder.OnInitListener() {
                  @Override
                  public void init(CustomDialog customDialog) {
                      Button btnDialogCancel = customDialog.findViewById(R.id.btnDialogCancel);
                      Button btnDialogOK = customDialog.findViewById(R.id.btnDialogOK);
                      TextView tvDialogContent = customDialog.findViewById(R.id.tvDialogContent);
                      ImageView ivDialogContent = customDialog.findViewById(R.id.ivDialogContent);

                      Bitmap bitmap = null;
                      ByteBuffer byteBuffer = image.getByteBuffer();
                      if (byteBuffer != null) {
                          FrameMetadata.Builder builder = new FrameMetadata.Builder();
                          builder.setWidth(image.getWidth())
                                  .setHeight(image.getHeight())
                                  .setRotation(image.getRotationDegrees());
                          bitmap = BitmapUtils.getBitmap(byteBuffer, builder.build());
                      } else {
                          bitmap = image.getBitmapInternal();
                      }
                      if (bitmap != null) {
                          graphicOverlay.add(new CameraImageGraphic(graphicOverlay, bitmap));
                      } else {
                          ivDialogContent.setVisibility(View.GONE);
                      }
                      SpanUtils spanUtils = SpanUtils.with(tvDialogContent);
                      for (int i = 0; i < barcodes.size(); ++i) {
                          Barcode barcode = barcodes.get(i);
                          BarcodeGraphic graphic = new BarcodeGraphic(graphicOverlay, barcode);
                          graphicOverlay.add(graphic);
                          Rect boundingBox = barcode.getBoundingBox();
                          spanUtils.append(String.format("(%d,%d)", boundingBox.left, boundingBox.top))
                                  .append(barcode.getRawValue())
                                  .setClickSpan(i % 2 == 0 ? getResources().getColor(R.color.colorPrimary) : getResources().getColor(R.color.colorAccent), false, new View.OnClickListener() {
                              @Override
                              public void onClick(View v) {
                                  Toast.makeText(getApplicationContext(), barcode.getRawValue(), Toast.LENGTH_SHORT).show();
                              }
                          })
                                  .setBackgroundColor(i % 2 == 0 ? getResources().getColor(R.color.colorAccent) : getResources().getColor(R.color.colorPrimary))
                                  .appendLine()
                                  .appendLine();
                      }
                      spanUtils.create();
                      Bitmap bitmapFromView = loadBitmapFromView(graphicOverlay);
                      ivDialogContent.setImageBitmap(bitmapFromView);

                      btnDialogCancel.setOnClickListener(new View.OnClickListener() {
                          @Override
                          public void onClick(View v) {
                              customDialog.dismiss();
                              finish();
                          }
                      });
                      btnDialogOK.setOnClickListener(new View.OnClickListener() {
                          @Override
                          public void onClick(View v) {
                              customDialog.dismiss();
                              mlKit.setAnalyze(true);
                          }
                      });
                  }
              })
              .build();
  }

作者:KathLine
来源:https://juejin.cn/post/6972476138203381790

收起阅读 »

Android:这是一个让你心动的日期&时间选择组件

预览引入添加 JitPack repositoryallprojects { repositories { ... maven { url "https://jitpack.io" } }}添加 Gradle依赖depe...
继续阅读 »



预览




imgimgimg

引入

添加 JitPack repository

allprojects {
repositories {
...
maven { url "https://jitpack.io" }
}
}

添加 Gradle依赖

dependencies {
  ...
  implementation 'com.google.android.material:material:1.1.0' //为了防止不必要的依赖冲突,0.0.3开始需要自行依赖google material库
  implementation 'com.github.loperSeven:DateTimePicker:0.3.0'//此处不保证最新版本,最新版需前往文末github查看
}

开始使用

内置弹窗CardDatePickerDialog

最简单的使用方式

//kotlin
    CardDatePickerDialog.builder(this)
              .setTitle("SET MAX DATE")
              .setOnChoose {millisecond->
                 
              }.build().show()
//java
new CardDatePickerDialog.Builder(this)
              .setTitle("SET MAX DATE")
              .setOnChoose("确定", aLong -> {
                   //aLong = millisecond
                   return null;
              }).build().show();

所有可配置属性

  CardDatePickerDialog.builder(context)
              .setTitle("CARD DATE PICKER DIALOG")
              .setDisplayType(displayList)
              .setBackGroundModel(model)
              .showBackNow(true)
              .setPickerLayout(layout)
              .setDefaultTime(defaultDate)
              .setMaxTime(maxDate)
              .setMinTime(minDate)
              .setWrapSelectorWheel(false)
              .setThemeColor(color)
              .showDateLabel(true)
              .showFocusDateInfo(true)
              .setLabelText("年","月","日","时","分")
              .setOnChoose("选择"){millisecond->}
              .setOnCancel("关闭") {}
              .build().show()

可配置属性说明

  • 设置标题

fun setTitle(value: String)
  • 是否显示回到当前按钮

fun showBackNow(b: Boolean)
  • 是否显示选中日期信息

fun showFocusDateInfo(b: Boolean)
  • 设置自定义选择器

//自定义选择器Layout注意事详见 【定制 DateTimePicker】
fun setPickerLayout(@NotNull layoutResId: Int)
  • 显示模式

// model 分为:CardDatePickerDialog.CARD//卡片,CardDatePickerDialog.CUBE//方形,CardDatePickerDialog.STACK//顶部圆角
// model 允许直接传入drawable资源文件id作为弹窗的背景,如示例内custom
fun setBackGroundModel(model: Int)
  • 设置主题颜色

fun setThemeColor(@ColorInt themeColor: Int)
  • 设置显示值

fun setDisplayType(vararg types: Int)
fun setDisplayType(types: MutableList<Int>)
  • 设置默认时间

fun setDefaultTime(millisecond: Long)
  • 设置范围最小值

fun setMinTime(millisecond: Long)
  • 设置范围最大值

fun setMaxTime(millisecond: Long)
  • 是否显示单位标签

fun showDateLabel(b: Boolean)
  • 设置标签文字

/**
*示例
*setLabelText("年","月","日","时","分")
*setLabelText("年","月","日","时")
*setLabelText(month="月",hour="时")
*/
fun setLabelText(year:String=yearLabel,month:String=monthLabel,day:String=dayLabel,hour:String=hourLabel,min:String=minLabel)
  • 设置是否循环滚动

/**
*示例(默认为true)
*setWrapSelectorWheel(false)
*setWrapSelectorWheel(DateTimeConfig.YEAR,DateTimeConfig.MONTH,wrapSelector = false)
*setWrapSelectorWheel(arrayListOf(DateTimeConfig.YEAR,DateTimeConfig.MONTH),false)
*/
fun setWrapSelectorWheel()
  • 绑定选择监听

/**
*示例
*setOnChoose("确定")
*setOnChoose{millisecond->}
*setOnChoose("确定"){millisecond->}
*/
fun setOnChoose(text: String = "确定", listener: ((Long) -> Unit)? = null)
  • 绑定取消监听

/**
*示例
*setOnCancel("取消")
*setOnCancel{}
*setOnCancel("取消"){}
*/
fun setOnCancel(text: String = "取消", listener: (() -> Unit)? = null)

选择器 DateTimePicker

xml中

app:layout 为自定义选择器布局 可参考 定制 DateTimePicker

        <com.loper7.date_time_picker.DateTimePicker
           android:id="@+id/dateTimePicker"
           android:layout_width="match_parent"
           android:layout_height="wrap_content"
           app:layout="@layout/layout_date_picker_segmentation"
           app:showLabel="true"
           app:textSize="16sp"
           app:themeColor="#FF8080" />

代码中

  • 设置监听

    dateTimePicker.setOnDateTimeChangedListener { millisecond ->  }

更多设置

  • 设置自定义选择器布局(注意:需要在dateTimePicker其他方法之前调用,否则其他方法将会失效)

 dateTimePicker.setLayout(R.layout.layout_date_picker_segmentation)//自定义layout resId
  • 设置显示状态

DateTimePicker支持显示 年月日时分 五个选项的任意组合,显示顺序以此为年、月、日、时、分,setDisplayType中可无序设置。

     dateTimePicker.setDisplayType(intArrayOf(
           DateTimeConfig.YEAR,//显示年
           DateTimeConfig.MONTH,//显示月
           DateTimeConfig.DAY,//显示日
           DateTimeConfig.HOUR,//显示时
           DateTimeConfig.MIN))//显示分
  • 设置默认选中时间

 dateTimePicker.setDefaultMillisecond(defaultMillisecond)//defaultMillisecond 为毫秒时间戳
  • 设置允许选择的最小时间

  dateTimePicker.setMinMillisecond(minMillisecond)
  • 设置允许选择的最大时间

  dateTimePicker.setMaxMillisecond(maxMillisecond)
  • 是否显示label标签(选中栏 年月日时分汉字)

  dateTimePicker.showLabel(true)
  • 设置主题颜色

  dateTimePicker.setThemeColor(ContextCompat.getColor(context,R.color.colorPrimary))
  • 设置字体大小

设置的字体大小为选中栏的字体大小,预览字体会根据字体大小等比缩放

  dateTimePicker.setTextSize(15)//单位为sp
  • 设置标签文字

  //全部
 dateTimePicker.setLabelText(" Y"," M"," D"," Hr"," Min")
 //指定
 dateTimePicker.setLabelText(min = "M")

定制 DateTimePicker

说明

DateTimePicker 主要由至多6个 NumberPicker 组成,所以在自定义布局时,根据自己所需的样式摆放 NumberPicker 即可。以下为注意事项

开始定制

  • DateTimePicker 至多支持6个 NumberPicker ,你可以在xml中按需摆放1-6个 NumberPicker

  • 为了让 DateTimePicker 找到 NumberPicker ,需要在xml中为 NumberPicker 指定 idtag,规则如下

/**
* year:np_datetime_year
* month:np_datetime_month
* day:np_datetime_day
* hour:np_datetime_hour
* minute:np_datetime_minute
* second:np_datetime_second
*/
android:id="@+id/np_datetime_year"  or  android:tag="np_datetime_year"
  • 使用定制UI

CardDatePickerDialog 中使用

fun setPickerLayout(@NotNull layoutResId: Int)

DateTimePicker 中使用

<com.loper7.date_time_picker.DateTimePicker
           android:id="@+id/dateTimePicker"
           android:layout_width="match_parent"
           android:layout_height="wrap_content"
           app:layout="@layout/layout_date_picker_segmentation"
           />

或者

 dateTimePicker.setLayout(R.layout.layout_date_picker_segmentation)//自定义layout resId

XML示例

示例图片

imgimg

更高的拓展性

如果以上自定义并不能满足你的需求,你还可以定制你自己的 DateTimePicker , 可参照 DateTimePicker.kt 定义你想要属性以及在代码内编写你的UI逻辑。选择器的各种逻辑约束抽离在 DateTimeController.kt ,你的 DateTimePicker 只需让 DateTimeController.kt 绑定 NumberPicker 即可。比如:

DateTimeController().bindPicker(YEAR, mYearSpinner)
          .bindPicker(MONTH, mMonthSpinner)
          .bindPicker(DAY, mDaySpinner).bindPicker(HOUR, mHourSpinner)
          .bindPicker(MIN, mMinuteSpinner).bindPicker(SECOND, mSecondSpinner).build()

作者:LOPER7
来源:https://juejin.cn/post/6917909994985750535

收起阅读 »

你可以永远相信debugger,但是不能永远相信console.log

总结放前面:console.log在打印引用数据类型的时候表现和我们的预期不相符合是因为console.log打印的是引用数据类型的一个快照,因为浏览器或者我们异步代码的原因在快照之后修改了对应的内存空间的值,所以等我们展开打印浏览器通过指针重新访问内存空间的...
继续阅读 »

总结放前面:

console.log在打印引用数据类型的时候表现和我们的预期不相符合是因为console.log打印的是引用数据类型的一个快照,因为浏览器或者我们异步代码的原因在快照之后修改了对应的内存空间的值,所以等我们展开打印浏览器通过指针重新访问内存空间的时候会获得最新的值导致展开和不展开的表现不一致。

不知道各位大佬有没有遇到过这样的情况,我在代码里面console.log()了一个数组,然后打开浏览器控制台,看着是空的就像这样[],结果我点展开它里面又有值了,但是在代码打印的位置使用length或者获取数组里面的值都是不行的,🤯 就像下面这样:

let arr = [];
const setFun = () => {
   return new Promise((reslove) => {
       let arr1 = [1, 2, 3];
       setTimeout(() => {
           reslove(arr1);
      }, 2000)
  });
}
const getFun = async () => {
   let result = await setFun();
   result.forEach((item) => {
       arr.push(item);
  })
}
getFun();
console.log(arr);


或者说我在某处代码console.log()一个对象,明明控制台打印对象的某一个key是1,但是我展开这个对象里面的key居然是2,我在代码里面获取的也是2,就像下面这样:
不知道各位大佬遇到这样的情况是怎么个想法,反正我第一次遇到的时候我还以为是我的谷歌浏览器出问题了,擦💦我甚至都想卸载重装一波。后来动了动🧠,觉得可能是代码执行顺序的原因,所以我就在代码里面打了断点看了一下,在执行console.log()的时候arr的确是一个空的对象,对arr数组的操作是在console.log()执行之后才进行的。

所以说这到底是为什么呐?
其实这个还是和js的引用数据类型还有console.log()的设计有关系。我们都知道引用数据类型大体上可以说是由两部分组成:指针和内容,指针保存的内容就是一个内存地址的指向,指针一般都是基本数据类型保存在栈内存,内容就包含着这个引用数据类型的实际值一般保存在堆内存。😍 而console.log呐打印的时候只是打印了这个引用数据类型的一个快照,快照中的指针和内容都是照相的时候的内容,在console.log()之后,修改了这个引用数据类型,或者说在这之前修改的操作在一个异步的内容里面,当我们去看打印的时候,这个引用数据类型的内容可能就被修改了,但是因为快照的原因我们看到的还是以前的值。
然后当我们展开的时候,浏览器会利用指针去内存重新读取内容,因为快找的指针是没有发生变化的,所以就看到了改变之后内存,这就是为什么我们展开和不展开看到的结果是不一样的原因了。当然造成这样的原因不一定都是因为我们代码在异步里面操作这个引用数据类型。
还有就是浏览器在进行I/O的时候异步会提升性能,所有这就是为什么有时候我们写的同步代码依然会出现不一致的情况,就像我第二个图一样。
下面就验证一下我上面的想法,当我把上面的代码修改一下,直接替换:

let arr = [];
const setFun = () => {
   return new Promise((reslove) => {
       let arr1 = [1, 2, 3];
       setTimeout(() => {
           reslove(arr1);
      }, 2000)
  });
}
const getFun = async () => {
   let result = await setFun();
   arr = result; // 修改部分
}
getFun();
console.log(arr);

那么我们看到的结果就和上面不一样了,这个展开的表现是和不展开是一样的。
相信各位大佬也知道是啥原因了,因为这次直接替换,修改的是指针的指向并没有修改之前引用数据饿类型的内存空间,所以当我们展开的时候快照中指针保存的地址还是空的,这样我们看到的和看之前的想法就对应上了。
注:该问题只存在于打印引用数据类型,基本数据类型不会出现。

作者:江湖不渡i
来源:https://juejin.cn/post/7032504319584780325

收起阅读 »

别被你的框架框住了

我短暂的职业生涯被 React 充斥着。还没毕业前我从 Vue 2.x 入手开始学习框架,在一个我当时觉得还行现在回看完全不行的状态进了公司。然后开启了跟 React 死磕的状态,从 class 组件到函数式组件,从 Redux 到 Recoil,从 Antd...
继续阅读 »

我短暂的职业生涯被 React 充斥着。

还没毕业前我从 Vue 2.x 入手开始学习框架,在一个我当时觉得还行现在回看完全不行的状态进了公司。然后开启了跟 React 死磕的状态,从 class 组件到函数式组件,从 Redux 到 Recoil,从 Antd 到 MUI...

不久前一个呆了2年多的项目成功结束,接下来要去一个新项目,新项目要用 Angular,于是我开始告别从毕业就开始用的 React,开始学习这个大家少有提及的框架。

回顾这几年,要说 React 带给我最多的是什么,我觉得可能是思想,是一种编程范式。为了理解 React 新的函数式组件,我去学习 FP,但我并不是一个原教旨主义者,所以我当然也不认同你想学 FP 就得去学 Lisp 的说法。

在这期间我发现小黄书的作者 Kyle Simpson 也写了一本专门为 JSer 介绍 FP 的,书中前言部分我深以为然:

The way I see it, functional programming is at its heart about using patterns in your code that are well-known, understandable, and proven to keep away the mistakes that make code harder to understand.

是的,编程范式的作用是为了让人们更好地组织和理解代码,编程范式应该去服务写代码的人,而不是人去事无巨细地遵循编程范式的每一个规则,理解每一个晦涩难懂的概念。

I believe that programming is fundamentally about humans, not about code. I believe that code is first and foremost a means of human communication, and only as a side effect (hear my self-referential chuckle) does it instruct the computer.

敏捷需要以人为本,写代码其实也一样。我们要做的应该是理解编程范式本身以及它背后的作用,或许在未来的某天你会突然发现,原来我用了这么久的这个玩意儿有一个这么有意思的名字,亦或者你可能永远也解释不清楚那个概念到底是什么:

A monad is just a monoid in the category of endofunctors.

一个单子不过是自函子范畴上的幺半群

那是不是搞不懂我就不能玩 FP 了?然后我就得站在鄙视链底端,被 Haskell、Lisp 玩家们指着鼻子嘲笑:你们看那家伙,其实啥也不懂,他那也叫 FP?

这个问题我没有答案,或许可以留给大家来讨论。但是到这里我至少明白了 React Hooks 为什么要叫 "hook";为什么有一个 hook 叫 "useEffect";我也理解了为什么大家都说不要用 hook 去实现 class 组件的生命周期。

除了写好 React 本身,我也尝试了纯函数、偏函数、柯里化、组合和 Point-free 风格的代码,确实得到了一些好处,也确实带来了一些不便。

可能这些思想就是学习 React 带给我最大的 side effect 吧(笑。

与 React 准备 all in FP 相反的是,与 Angular 短暂接触的我发现它全面拥抱 OOP。与当时 React 从 class 组件切换到函数式组件一样,首先你得把编程范式思想完全转变过来才能很好地理解 Angular。这又促使我不得不去复习许多被我丢弃很久的 OOP 思想。

到这我不禁想起一次公司内 TDD 训练营,作业完成后去找 coach 讲解,讲解过程中 coach 讲到了抽象能力、隔离层、防腐层。那时我才发现自己 OO 的抽象能力和一起的后端小伙伴一比实在是差到不行,只有大学时候的能力。反思过后像是被 React 给“惯”坏了,几乎已经丢掉了这部分能力。

老实说我接触 React class 组件时间并不长,第一个项目只有短短几个月。后面两个项目虽然去写 Java 了,但是第一个都是一些修修补补的工作,更像是在做 DevOps,后来的项目去写 Java BFF,毫无抽象可言,全是数据 mapping。然后又进到了一个将“追求技术卓越”贯彻执行的项目,成了那批最早吃函数式组件螃蟹的人。

于是我接触 class 组件的时间就只有作为毕业生的那短短几个月而已。

然后当我看到 Angular 文档中的依赖注入时,我脑子只能零星蹦出一些概念:SOLID、解耦。别说细节,我甚至不知道我蹦出来的这些东西是不是对的。于是我又只能凭着自己的记忆去邮件里搜相关的博客大赛的文章。

我好像已经丢掉了 OOP 了。

种下一棵树最好的时间是十年前,其次是现在。

跳出all in FP 的 React 我发现世界不是非黑即白的。说是全面拥抱 OOP,但其实你可以很轻易的在 Angular 中发现 FP 的影子 -- 用 pipe 来处理数据,用 Rx 来处理请求。

既然是以人为本,编程范式本就不应该对立,它们明明可以互补,在自己擅长的领域处理自己擅长的事情,哪怕是同一个项目。看惯了两个阵营吵架的场景,好像这样的场景才是我想要的。

于是我又回忆起某天在项目上和大家讨论的项目分包问题,最后的结论是 OOP 的以对象和 domain 分包的策略在大多数时候要优于单纯的 FP 的方式。它能让功能更集中,让大家更容易找到自己想要找的东西。

但是回过头来静静思考,我虽然会好好学习 OOP,但是我目前大概率不会去深入学习相关的建模方法。因为在目前我的工作环境下,我没看到有前端同学需要深刻理解建模方法的场景,大多数情况浅尝辄止即可。

以我自身的经历来看,DDD 我看过也参加过培训,也跟着项目后端小伙伴在搭建项目时从零到一实践过。但是在实践不多的情况下,整个过程逃脱不了学了忘忘了学的魔咒。大概唯一的用处就是当我被抓到后端去干活能看懂他们为什么要这么组织代码,至于建模的那个过程,被抓去干活的我是大概率不会参与的。(当然如果你有相关的经历还请喷醒我,比如你作为偏前端的小伙伴就是要熟练掌握建模方法,不然工作就做不下去了)

不要被技术栈限制住了自己,其实以前一直对这句话一知半解,虽然可能现在的理解也没有很强。可是当你从一个框里跳出来以后,去思考画框这个人的想法,你可能能够得到一些不一样的思考。对于 Thoughtworker 来说学习一个新框架,一门新语言可能不是什么问题,那我们是不是可以更进一步,想想那些看起来“虚无缥缈”的东西呢。

别被你的框架框住你了。


作者:Teobler
来源:https://juejin.cn/post/7032467133611294733

收起阅读 »

12 个救命的 CSS 技巧

✨12 个救命的 CSS 技巧✨ 1. 使用 Shape-outside 在浮动图像周围弯曲文本它是一个允许设置形状的 CSS 属性。它还有助于定义文本流动的区域。css代码:.any-shape {  width: 300px...
继续阅读 »



✨12 个救命的 CSS 技巧✨

1. 使用 Shape-outside 在浮动图像周围弯曲文本

它是一个允许设置形状的 CSS 属性。它还有助于定义文本流动的区域。css代码:

.any-shape {
 width: 300px;
 float: left;
 shape-outside: circle(50%);
}

2. 魔法组合

这个小组合实际上可以防止你在 HTML 中遇到的大多数布局错误的问题。我们确实不希望水平滑块或绝对定位的项目做他们想做的事情,也不希望到处都是随机的边距和填充。所以这是你们的魔法组合。

* {
padding: 0;
margin: 0;
max-width: 100%;
overflow-x: hidden;
position: relative;
display: block;
}

有时“display:block”没有用,但在大多数情况下,你会将 <a><span> 视为与其他块一样的块。所以,在大多数情况下,它实际上会帮助你!

3. 拆分 HTML 和 CSS

这更像是一种“工作流程”类型的技巧。我建议你在开发时创建不同的 CSS 文件,最后才合并它们。例如,一个用于桌面,一个用于移动等。最后,你必须合并它们,因为这将有助于最大限度地减少您网站的 HTTP 请求数量。

同样的原则也适用于 HTML。如果你不是在 Gatsby 等 SPA 环境中进行开发,那么 PHP 可用于包含 HTML 代码片段。例如,你希望在单独的文件中保留一个“/modules”文件夹,该文件夹将包含导航栏、页脚等。因此,如果需要进行任何更改,你不必在每个页面上都对其进行编辑。模块化越多,结果就越好。

4. ::首字母

它将样式应用于块级元素的第一个字母。因此,我们可以从印刷或纸质杂志中引入我们熟悉的效果。如果没有这个伪元素,我们将不得不创建许多跨度来实现这种效果。例如:

这是如何做到的?代码如下:

p.intro:first-letter {
 font-size: 100px;
 display: block;
 float: left;
 line-height: .5;
 margin: 15px 15px 10px 0 ;
}

5. 四大核心属性

CSS 动画提供了一种相对简单的方法来在大量属性之间平滑过渡。良好的动画界面依赖于流畅流畅的体验。为了在我们的动画时间线中保持良好的性能,我们必须将我们的动画属性限制为以下四个核心:

  • 缩放 - transform:scale(2)

  • 旋转 - transform:rotate(180deg)

  • 位置 – transform:translateX(50rem)

  • 不透明度 - opacity: 0.5

边框半径、高度/宽度或边距等动画属性会影响浏览器布局方法,而背景、颜色或框阴影的动画会影响浏览器绘制方法。所有这些都会大大降低您的 FPS (FramesPerSecond)。您可以使用这些属性来产生一些有趣的效果,但应谨慎使用它们以保持良好的性能。

6. 使用变量保持一致

保持一致性的一个好方法是使用 CSS 变量或预处理器变量来预定义动画时间。

:root{ timing-base: 1000;}

在不定义单元的情况下设置基线动画或过渡持续时间为我们提供了在 calc() 函数中调用此持续时间的灵活性。此持续时间可能与我们的基本 CSS 变量不同,但它始终是对该数字的简单修改,并将始终保持一致的体验。

7. 圆锥梯度

有没有想过是否可以只使用 CSS 创建饼图?好消息是,您实际上可以!这可以使用 conic-gradient 函数来完成。此函数创建一个由渐变组成的图像,其中设置的颜色过渡围绕中心点旋转。您可以使用以下代码行执行此操作:

.piechart {
 background: conic-gradient(rgb(255, 132, 45) 0% 25%, rgb(166, 195, 209) 25% 56%, #ffb50d  56% 100%);
 border-radius: 50%;
 width: 300px;
 height: 300px;
}

8. 更改文本选择颜色

要更改文本选择颜色,我们使用 ::selection。它是一个伪元素,在浏览器级别覆盖以使用您选择的颜色替换文本突出显示颜色。使用光标选择内容后即可看到效果。

::selection {
    background-color: #f3b70f;
}

9. 悬停效果

悬停效果通常用于按钮、文本链接、站点的块部分、图标等。如果您想在有人将鼠标悬停在其上时更改颜色,只需使用相同的 CSS,但要添加 :hover到它并更改样式。这是您的方法;

.m h2{ 
   font-size:36px;
   color:#000;
   font-weight:800;
}
.m h2:hover{
   color:#f00;
}

当有人将鼠标悬停在 h2 标签上时,这会将您的 h2 标签的颜色从黑色更改为红色。它非常有用,因为如果您不想更改它,则不必再次声明字体大小或粗细。它只会更改您指定的任何属性。

10.投影

添加此属性可为透明图像带来更好的阴影效果。您可以使用给定的代码行执行此操作。

.img-wrapper img{
         width: 100% ;
         height: 100% ;
         object-fit: cover ;
         filter: drop-shadow(30px 10px 4px #757575);
}

11. 使用放置项居中 Div

居中 div 元素是我们必须执行的最可怕的任务之一。但不要害怕我的朋友,你可以用几行 CSS 将任何 div 居中。只是不要忘记设置display:grid; 对于父元素,然后使用如下所示的 place-items 属性。

main{
width: 100% ;
height: 80vh ;
display: grid ;
place-items: center center;
}

12. 使用 Flexbox 居中 Div

我们已经使用地点项目将项目居中。但是现在我们解决了一个经典问题,使用 flexbox 将 div 居中。为此,让我们看一下下面的示例:

<div>
<div></div>
</div>
.center {
display: flex;
align-items: center;
justify-content: center;
}

.center div {
width: 100px;
height: 100px;
border-radius: 50%;
background: #b8b7cd;
}

首先,我们需要确保父容器持有圆,即 flex-container。在它里面,我们有一个简单的 div 来制作我们的圆圈。我们需要使用以下与 flexbox 相关的重要属性:

  • display: flex; 这确保父容器具有 flexbox 布局。

  • align-items: center; 这可确保 flex 子项与横轴的中心对齐。

  • justify-content: center; 这确保 flex 子项与主轴的中心对齐。

之后,我们就有了常用的圆形 CSS 代码。现在这个圆是垂直和水平居中的,试试吧!

作者:海拥
来源:https://juejin.cn/post/7024372412632268813

收起阅读 »