开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

说一说http的发展史 前言 HTTP/09 HTTP/1

发表于 2024-04-10

前言

HTTP (Hypertext Transfer Protocol) 是一种用于传输超文本的应用层协议。它是万维网的基础,用于在客户端和服务器之间传输资源,如 HTML 页面、图像、视频等。

HTTP的发展史可以追溯到互联网的早期阶段, 那个时候的数据是写死的,只能传输纯文本数据。从最早的HTTP/0.9到如今的HTTP/3,HTTP协议发生了许多的变化,今天我们就来聊聊HTTP各个版本叭。

HTTP/0.9

HTTP/0.9是HTTP协议的最早版本,也是最简单的版本,于1991年由蒂姆·伯纳斯-李(Tim Berners-Lee)创建。它的设计目标是简单、易于实现,用于传输超文本。

  1. 客户端发送GET请求,请求一个 xxxx/index.html
  2. 服务器接受到请求后,读取对应的html文件,以ASCII的字符流返回给客户端

特征

  1. 只支持GET请求:
* HTTP/0.9只支持一种请求方法,即GET方法。这意味着客户端只能请求服务器上的资源,并不能向服务器发送任何数据。例如,通过GET请求可以请求一个HTML页面,但不能通过HTTP/0.9协议向服务器提交表单数据。
  1. 只支持纯文本:
* HTTP/0.9只能传输纯文本数据,不能传输二进制数据或其他类型的数据。因此,它主要用于传输HTML页面和超文本链接。
  1. 只有请求行,没有请求头和请求体
  2. 没有响应头
  3. 传输的内容是以ASCII的字符流

HTTP/1.0

HTTP/1.0是HTTP协议的一个重要版本,它于1996年发布。HTTP/1.0在HTTP/0.9的基础上引入了许多新的功能和改进,使得它更加灵活和功能强大。

  1. 相比于HTTP/0.9, HTTP/1.0可以支持多种类型文件的传输, 且不限于ASCII编码方式
  2. 因为Http/1.0支持多种类型文件的传输,那么就需要告诉浏览器需要以哪种方式去加载这些文件,通过引入请求头和响应头来让客户端和服务端更加深入的交流,key-value形式。

特征

  1. 支持多种请求方法:
* HTTP/1.0引入了多种请求方法,包括GET、POST、HEAD、PUT、DELETE等。这些请求方法允许客户端向服务器发送不同类型的请求,如获取资源、提交数据、获取资源头部信息等。
  1. 响应状态码:
* HTTP/1.0引入了响应状态码,用于指示服务器对请求的处理结果。常见的状态码包括200(OK,请求成功)、404(Not Found,资源未找到)、500(Internal Server Error,服务器内部错误)等。

HTTP/1.1

HTTP/1.1是HTTP协议的一个重要版本,于1999年发布。它在HTTP/1.0的基础上引入了许多改进和新特性,提高了性能、效率和安全性。

  1. 持久连接:
* HTTP/1.1默认支持持久连接,即在单个TCP连接上可以发送和接收多个HTTP请求和响应,而不必为每个请求建立新的连接。这减少了连接建立和关闭的开销,提高了性能和效率。但是持久连接会带来`队头阻塞`的问题。

队头阻塞:如果某个请求因为某种原因被阻塞,那么后续的请求也会被阻塞,从而导致页面加载速度下降。

页面中的所有不可能一次性的请求回来,所以可能会存在多条TCP持久连接,浏览器默认支持 6个TCP持久连接

  1. Chunk transfer 机制:
    • 将数据分割成若干个任意大小的数据块,每个数据块标记好长度,最后发送一个长度为0的数据块为标志发送完毕

在http/1.0中,需要在响应头中设置数据的大小,例如content-Length: 1024,但是后端并不清楚自己所发的数据的大小,数据有可能是动态的。所以http/1.1中推出Chunk transfer去解决这个问题。

  1. 虚拟主机的默认支持:
* HTTP/1.1将虚拟主机的支持作为默认功能,不再需要通过特殊的头部信息指定主机。这使得托管多个域名的服务器更容易配置和管理。
  1. Host头部:
* HTTP/1.1引入了Host头部,用于指定请求的目标主机。这使得在同一台服务器上托管多个域名的虚拟主机更加灵活和简单。

HTTP/2.0

  • 1.1 的问题: 带宽用不满
    1. TCP的慢启动: 拥塞控制导致一定会慢启动,慢启动导致页面关键资源加载时间推后
    2. 多条TCP连接竞争带宽导致每条TCP连接中能被分配的带宽大大降低
    3. 队头阻塞,前一个请求延迟,后一个请求便无法发送,导致带宽浪费

HTTP/2.0通过引入多路复用来优化

多路复用

  1. 一个域名只使用一个TCP长连接
  2. 将每一个请求分成一帧一帧的数据进行传输并打上标记,同时发送给服务端,且可以在重要资源
    请求中编辑为加急,服务端接受到带有各种标号的数据帧后,可以区分哪个数据帧加急,优先整理
    和响应该请求的数据帧 (通过引入了二进制分帧层实现多路复用)

HTTP/3.0

2.0的缺陷:

  1. TCP的队头阻塞: 当单个数据包丢失时,TCP会暂停后续的包的传输,先重传丢失的包数据
  2. TCP的慢启动

基于 QUIC 协议:

  • HTTP/3 使用 QUIC(Quick UDP Internet Connections)作为传输协议,取代了 TCP。QUIC 是基于 UDP 的协议,结合了传输层和应用层协议的功能,提供了更快的连接建立、更灵活的拥塞控制和更强的安全性。QUIC 还支持多路复用和头部压缩等功能,有助于提高性能和效率。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

开发经理:谁在项目里面用Stream paraller()

发表于 2024-04-09

大家好,我是小玺,今天给大家分享一下项目中关于Stream.parallel() 碰到的坑。

Stream.parallel() 是Java 8及以上版本引入的Stream API中的一个方法,它用于将一个串行流转换为并行流。并行流可以在多个处理器上同时执行操作,从而显著提高对大量数据进行处理的性能。

踩坑日记

某个大型项目,晚上十一点多有个用户对小部分数据进行某项批量操作后,接口大半天没有反应最后返回超时报错,但是过了一段时间后,出现了部分数据被修改成功,部分数据则没有反应。用户立马跳起来,打电话投诉到公司领导层,于是乎领导层对上至开发经理和PM,下至小开发进行会议批斗,要求马上排查并解决问题,毕竟项目这么大,当初也是要求测试做过压测的,怎么出现这么大的生产事故。

1712648893920.png

于是乎开发和实施运维分头行事,开发人员排查问题,实施人员先把问题数据维护好,不能应该用户使用。一群开发也是很疑惑,开发和测试环境都没法复现出问题,简单过一下代码也没看出个所以然,由于时间问题,不得不呼叫一手开发经理帮忙看看,开发经理后台接口看完Stream.parallel()进行的操作代码立马就炸了,git看了下提交人【会笑】,把这个开发从头到脚喷了一遍。

在对会笑单独进行了长达半小时的“耐心教育”后(ps:问题安排另一名开发同事修复),开发经理给团队的所有后端开发人员又都教育了一遍。原来会笑在用并行流的时候,没有考虑线程池配置和事务问题,把一堆数据进行了批量更新,Stream.parallel()并行流默认使用的是ForkJoinPool.commonPool()作为线程池,该线程池默认最大线程数就是CPU核数。

1712648957687.png

雀食对于一些初中级开发来说,开发过程中往往喜欢用一些比较新颖的写法来实现但是对新语法又是一知半解的,Stream.parallel()作为Java的新特性,也就成了其中一个反面教材。如果操作数据量不大的情况,其实没有必要用到Stream.parallel(),效率反而会变差。

注意事项

  1. 线程安全:并行流并不能保证线程安全性,因此,如果流中的元素是共享资源或操作本身不是线程安全的,你需要确保正确同步或使用线程安全的数据结构。
  2. 数据分区:Java的并行流机制会自动对数据进行分区,但在某些情况下,数据分区的开销可能大于并行带来的收益,特别是对于小规模数据集。
  3. 效率考量:并非所有的流操作都能从并行化中受益,有些操作(如短流操作或依赖于顺序的操作)并行执行反而可能导致性能下降。而且,过多的上下文切换也可能抵消并行带来的优势。
  4. 资源消耗:并行流默认使用的线程池大小可能与机器的实际物理核心数相适应,但也可能与其他并发任务争夺系统资源。
  5. 结果一致性:并行流并不保证执行的顺序性,也就是说,如果流操作的结果依赖于元素的处理顺序,则不应该使用并行流。
  6. 事务处理:在涉及到事务操作时,通常需要避免在并行流中直接处理,如上述例子所示,应当将事务边界放在单独的服务方法内,确保每个线程内的事务独立完成。

Tips:线程数可以通JVM启动参数-Djava.util.concurrent.ForkJoinPool.common.parallelism=20进行修改

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

JWT vs Session:到底哪个才是你的菜? 1、JW

发表于 2024-04-09
大家好,我是石头~


今天咱们来聊聊程序员圈子里一个老生常谈的话题——JWT与Session。

u=3804734326,4270128854&fm=253&fmt=auto&app=138&f=JPEG.webp

这俩家伙在身份验证的世界里可是大名鼎鼎。每当新项目启动,或者旧系统升级,关于“JWT好还是Session好”的争论就如约而至。


那它们究竟有何神通,又各自适合怎样的场景呢?


别急,接下来咱就把它们掰扯清楚,帮你找到最适合自家项目的“真命天子”!

1、JWT:轻装上阵的无状态小能手

下载 (3).jfif

●JWT是个啥?

JWT,全称JSON Web Tokens,就像一张特殊的电子身份证,里面包含了用户的认证信息。这张身份证被精心设计成三段式:头(说清楚这是个JWT)、身子(装着用户数据)、尾(盖个戳确保没人乱改)。(了解JWT详细信息可进入《[JWT:你真的了解它吗?](https://dev.newban.cn/7354308608044072996)》)


然后,它会被编码成一串字符串,随HTTP请求在客户端和服务端之间飞来飞去。

●JWT的独门秘籍

5981fe09d54ef_610.jpg

  1. 无状态神功:服务器不记仇(不对,是不记用户会话信息),全靠客户端带着JWT自己证明身份。这样既减轻了服务器压力,又能轻松应对分布式部署,简直是微服务架构的福音。
  2. 跨界高手:因为JWT藏在HTTP头里,天生支持跨域访问,API接口调用那叫一个爽快。
  3. 安全护盾:签名机制确保了JWT内容不会被偷偷篡改,让你用得安心。

●JWT也有软肋

u=3372138622,870835507&fm=253&fmt=auto&app=138&f=JPEG.webp

  1. 过期管理不易:JWT的有效期得拿捏好,太短吧,用户可能刚喝口水回来就得重新登录;太长吧,万一令牌丢了,风险就大了。
  2. 想撤回?没那么简单:发出去的JWT就像泼出去的水,需要增加缓存机制才能撤回,否则需要等它自然过期,或者记它一笔在黑名单,没有其他办法主动撤销。
  3. 个头儿有点大:要是JWT里塞太多东西,长度就会变长,可能影响网络传输速度。

2、Session:稳扎稳打的传统守护者

1568864348_958982.jpg

●Session是何方神圣?

Session嘛,就是服务器给每位登录用户发的一张VIP卡(Session ID)。用户每次来访,出示这张卡,服务器就能认出他,给他相应的服务。这张VIP卡一般藏在Cookie里,由浏览器贴心保管。


Session的认证流程如下图:

1909759-20201101223209422-1062085422.png

●Session的看家本领

fa580eacd69b424174af39e9acd99bd9bfc45963.jpg

  1. 一手掌握用户会话:服务器亲自管着所有用户的Session信息,想创建、更新、销毁或者设置有效期,都是一句话的事儿,权限控制灵活得很。
  2. 令牌召回无压力:只要从服务器的Session池子里撤掉某张VIP卡,对应的用户会话立马失效,安全性妥妥的。
  3. 肚量大,能装事儿:服务器端存储会话信息,不像JWT有长度限制,哪怕业务再复杂也不怕。

●Session的阿喀琉斯之踵

7dd98d1001e93901ecee1a4f76ec54e737d196d7.png

  1. 服务器压力山大:用户越多,服务器要记的VIP卡越多,内存消耗那是哗哗的。
  2. 跨域是个麻烦事:Session依赖Cookie,跨域时得多费点心思,比如设置 CORS 或者用 Token 传 Session ID。
  3. 分布式架构,头疼:多台服务器怎么共享Session信息?要么“粘性会话”让用户老找同一位服务器,要么搞个Session复制,总之复杂度上去了。

3、应用场景大比拼

436888piCskT.jpg

  1. 微服务、API Gateway等分布式架构:这种环境下,JWT无状态、易扩展的特性简直是量身定制,绝对是主力选手。
  2. 单体应用、内部系统等非分布式场景:如果没啥跨域、分布式的问题,对用户会话管理要求又高,Session就是你的贴心小棉袄。
  3. 移动应用、SPA等前端技术栈:现代前端应用跟JWT更搭哦,跨域友好、容易集成,还能帮网络请求减负。

4、结语:萝卜青菜,各有所爱

u=3501297655,2295734413&fm=253&fmt=auto&app=138&f=JPEG.webp

JWT和Session,一个是轻装上阵的无状态小能手,一个是稳扎稳打的传统守护者。选谁,关键看你的项目需求、技术架构和团队偏好。没有绝对的优劣,只有适不适合。所以,别再问“JWT和Session哪个好”了,先理清自家情况,答案自然浮出水面。

**MORE | 更多精彩文章**

  • H5推送,为什么都用WebSocket?
  • 别再这么写POST请求了~
  • 揭秘布谷鸟过滤器:一场数据过滤中的“鸠占鹊巢”大戏!
  • JWT:你真的了解它吗?

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

不是Jenkins玩不起,而是脚本更有性价比,在1Panel

发表于 2024-04-09

前言

公司测试环境的运维管理面板是1Panel,由于近期有新项目的开发,部署功能并不完善,每次版本的发布需要开发人员在自己电脑上build并通过压缩包手动进行操作发布,这么做既降低了效率,还会导致操作的不一致性,并且难以扩展和维护。于是我计划在面板中搭建一套流水线来维护前端包的自动构建与代码发布。

需求调研

在原先的文章中我对Jenkins+Gitea的前端自动化实现有了一定的认识,并且使用pipeline实现了一套部署方案,使开发部署有了一定的效率提升,于是准备着手将这套机制运行在服务器中,然鹅不出意外的出意外了,使用这套解决方案为我带来以下问题:

  1. 服务器的资源占用较高,我新建容器时将内存资源限制在512MB,但是启动后直接拉满了,于是我重建了容器,将内存资源控制在1G,这才有了54%的占用量,也就是说空闲状态下的Jenkins都要占用600M左右的资源
  2. 第二点是核心的问题,使用Jenkins无法获取到容器外的实例,或者说无法操控IO调度,比如:node,npm,pnpm等等,需要单独在Jenkins所在的容器中再搭建一套
  3. Pipeline语法需要重新维护,与之前在window服务器中的语法不同
  4. Jenkins的环境,插件等也需要占用一定资源

综合下来,在轻量级服务器中使用Jenkins或许会大材小用,我决定另辟蹊径,使用一套脚本或许就可以实现部署诉求,下面我将分享一下脚本实现,搭建,使用过程以及遇到的问题

准备工作

  • Linux服务器,最好有公网IP,若没有则需要保证git仓库可以访问到该服务器(在局域网内)
  • 1Panel管理面板,下文简称面板
  • Git系列仓库(gitee,gitea,gitlab,github等)及源码,下文以gitea为例

环境搭建

使用面板创建Node脚本文件夹

接着我们在本地使用npm init -y或者pnpm init初始化一个Node项目文件夹,在文件夹中新建index.js文件,在里面随便输入点输出,然后在package中script新建start启动脚本

将文件夹打包成zip,或者直接拖到服务器文件夹中

此外,如果服务器本身就安装了pnpm或者npm,也可以通过终断直接在Node脚本文件夹中执行上一步操作

创建Node运行环境

等待应用创建完成后可能会显示异常,可能是由于没有启动node服务导致的,我们可以在index.js代码中实现一个最简单的server服务

1
2
3
4
5
6
7
8
javascript复制代码const http = require("http");

http
.createServer((req, res) => {
console.log(req.headers);
res.end("Hello World!");
})
.listen(2048, () => console.log("server start"));

最后,我们在浏览器输入服务器IP加端口就可以看到脚本的运行效果

如果无法出现访问,可以在容器中看看映射地址是否正确

将映射IP改成0.0.0.0就可以了

Webhook配置

webhook可以参考这篇文章,也可以参照下图的步骤

我这里使用的触发条件是当代码推送时触发钩子

最后可以发送请求测试一下

可以访问到就说明hook已经连通

来看看服务器端的日志

可以看到日志也是没有问题的

注意:除此之外,在点击推送消息后可能会出现以下抛错

这是由于webhook需要开白,具体操作参考Config Cheat Sheet | Gitea Documentation

在app.ini文件中的webhook那一栏中增加需要访问的ip白名单:webhook.ALLOWED_HOST_LIST,即 在app.ini添加:

**[webhook]

ALLOWED_HOST_LIST = external, 192.168.1.85**

就可以解决上述问题

脚本实现

来到核心部分,脚本的实现

我们借助express框架实现webhook接口,并使用bodyParser模块解析Post请求的body参数,然后通过子线程模块child_process进行git或者其他命令的输入,最终实现以下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
javascript复制代码import express from "express";
import bodyParser from "body-parser";
import { exec } from "child_process";
import path from "path";
import fs from "fs/promises";
import "./env.js";
const app = express();
const port = 1024; // 监听的端口号
let step = 1; // 记录步骤顺序的变量
app.use(bodyParser.json());
const projectPath = process.env.PROJECT_PATH ?? ""; // 项目路径
const destinationPath = process.env.DESTINATION_PATH ?? ""; // 目标路径,一般是nginx下的项目路径
const buildPath = process.env.BUILD_OUTPUT ?? ""; // 项目打包后输出路径,如dist,build等
const _log = (...args) => {
console.log(new Date().toLocaleString(), ...args);
};
// 执行命令的辅助函数,返回Promise以处理异步执行
const executeCommand = (command) => {
return new Promise((resolve, reject) => {
_log(`步骤 ${step}:执行命令 "${command}"`);
exec(command, (error, stdout, stderr) => {
if (error) {
console.error(`步骤 ${step}:执行命令出错:${error}`);
console.error(stderr);
reject(error);
} else {
_log(`步骤 ${step}:命令执行成功`);
step++; // 执行成功,递增步骤数
resolve(stdout);
}
});
});
};

// 检查目标文件夹是否存在,如果不存在则创建,存在则清空
const ensureProjectFolder = async (projectPath) => {
try {
await fs.access(projectPath);
const files = await fs.readdir(projectPath);
for (const file of files) {
const filePath = path.join(projectPath, file);
const stat = await fs.stat(filePath);
if (stat.isDirectory()) {
await fs.rm(filePath, { recursive: true });
} else {
await fs.unlink(filePath);
}
}
} catch (error) {
// 如果文件夹不存在则创建
await fs.mkdir(projectPath, { recursive: true });
}
};


// 递归复制文件夹的函数
async function copyFolder(src, dest) {
try {
_log(`复制文件夹 "${src}" 到 "${dest}"`);
await fs.mkdir(dest, { recursive: true });
const files = await fs.readdir(src);
for (const file of files) {
const srcPath = path.join(src, file);
const destPath = path.join(dest, file);
const stats = await fs.stat(srcPath);
if (stats.isDirectory()) {
await copyFolder(srcPath, destPath);
} else {
await fs.copyFile(srcPath, destPath);
_log(`文件 "${srcPath}" 复制到 "${destPath}"`);
}
}
_log(`文件夹 "${src}" 成功复制到 "${dest}"`);
} catch (error) {
console.error("复制文件夹时出错:", error);
}
}

app.post("/webhook", async (req, res) => {
const payload = req.body;
const head = req.headers;
if (payload?.ref_type && head["x-gitea-event-type"] === "create") {
_log("从 Gitea 接收到Tag事件:", payload);
try {
await ensureProjectFolder(projectPath);
const agreement = "http";
const gitCommand = `cd ${projectPath} && git clone ${agreement}://${
process.env.GIT_USER_NAME
}:${process.env.GIT_PASS_WORD}@${payload.repository.clone_url.replace(
`${agreement}://`,
""
)} ./`;
await executeCommand(gitCommand);
await executeCommand(`cd ${projectPath} && git checkout ${payload.ref}`);
const pnpmInstalled = await executeCommand("pnpm -v")
.then(() => true)
.catch(() => false);
if (!pnpmInstalled) {
await executeCommand("npm install -g pnpm");
}
await executeCommand(`cd ${projectPath} && pnpm install`);
await executeCommand(`cd ${projectPath} && pnpm build`);
await ensureProjectFolder(destinationPath);
await copyFolder(path.join(projectPath, buildPath), destinationPath);
_log("部署成功");
res.status(200).send("部署成功");
} catch (error) {
console.error("部署过程中出错:", error);
res.status(500).send("部署失败");
} finally {
await ensureProjectFolder(projectPath);
_log(`文件夹 "${projectPath}" 内容删除成功`);
}
} else {
_log("从 Gitea 接收到不可识别的事件");
res.status(400).send("不可识别的事件");
}
});

app.listen(port, () => {
_log(`服务器正在端口 ${port} 上运行`);
});

脚本部署

将代码部署到node容器中

注意:需要特别注意,由于node容器中无法访问到Nginx容器或者外部容器的文件夹,从而导致我们build完成之后无法部署到指定文件夹下,所以我们需要借助1Panel面板中容器的挂载功能,将Nginx中的项目文件夹挂载到当前node环境可访问的目录下,比如

脚本使用

在gitea中,我们将webhook配置修改一下,在请求路径最后加上我们使用脚本写好的接口,将请求方式改成Post,触发条件改成tag创建时触发(或者使用自己想构建的方式,我这里只是分享我当前的构建步骤)

在我们的代码项目中使用以下两种方式触发自动化构建

使用GIT

git tag “版本号” 发布标签

git push origin –tags 上传标签

使用工具

工具的文档参考:为了方便项目打包,我用Node写了个git-tag工具_编写tag脚本-CSDN博客

npm i git-tag-sh -g

修改版本号并运行git push后

执行git-tag-sh 发布上传标签

效果展示

在需要构建的项目中输入命令后显示以下内容

在面板的日志中显示以下内容

项目也成功部署在Nginx目录下

至此,需求全部实现完成

总结

本文主要分享了使用node实现前端部署的全过程以及注意事项,其中node脚本虽然占用资源相对较小,但是也有一定的缺点,比如可维护性,需要了解一定的JS语法,局限性,只能实现文件读写和容器中的api调用。但是总的来说使用node结合合适的优化和设计,还是可以最大程度地发挥其优势。

以上就是文章全部内容了,感谢你看到了最后,如果觉得文章不错的话,还望三连支持一下,谢谢!

相关源码:

自动化脚本:CICDNodeAutoTask: 使用node脚本实现的前端自动化构建工具,提供一个接口供git系列仓库的webhook调用 (gitee.com)

Git标签工具:git-tag-sh: 针对当前项目执行 git 打包操作 (gitee.com)

相关代码同样集成在总仓库的模块下:myCode: 基于js的一些小案例或者项目 (gitee.com)

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

心态崩了,约了半个月,就只有3个面试! 先来唠唠 北京-度小

发表于 2024-04-09

声明:本文首发在同名公众号:王中阳Go,未经授权禁止转载。

先来唠唠

今儿咱们聊聊这位面试的哥们儿,最近半个月他只约到了3次面试,心里那个急啊,总怕错过了找工作的黄金期。

我跟他说:“淡定点,现在找工作的机会还多着呢。不要和别人比,把握好自己的节奏。花也不是一下子全开的,找工作也得慢慢来,求职面试也是学习的过程,别急,每个人都要有自己的节奏。”

每次面试都是向前迈了一步,哪怕有时候面试题目答不上来,那也是在给你的以后成功上岸做准备。

继续最新面经分享系列文章,再多跟大家分享分享面试真题,肯定会对你有帮助,欢迎关注我,收藏文章,方便以后复习。

北京-度小满

自我介绍

1、介绍项目具体业务

2、项目碰到的技术难点

微服务第一次接触没有底,go-zero、looklook学习,looklook架构用到项目上,问题:浪费资源、替代方案

3、日志收集用来干什么?

微服务链路跟踪

4、链路最长有多少个服务?

3个

5、日志收集的意义在哪里,服务很少,没有必要做trace这些东西呀?

项目后面需要引入AI,还有加入其他功能服务,提前准备好

6、redis持久化机制

AOF和RDB;两个持久化的特点介绍

7、重新介绍一下AOF写入的三种方式

8、了解过redis集群吗?

没有,运维人员做的工作个人觉得没必要看

9、redis从客户端执行命令到最终命令的响应,这中间经历了那些过程,能大概描述一下吗

10、redis速度为什么那么快?

基于内存:极高的读写速度,特别对于简单的存取操作,执行时间非常短,主要耗时在于网络IO

单线程:必要上下文切换,锁竞争

IO多路服用:如epoll,能够在一个线程中高效地处理多个客户端连接 >
高效的数据结构:全局哈希、压缩表、跳跃表

11、使用过那些redis的数据类型?

redis的常用数据类型,主要使用string

12、redis的过期清理策略

惰性删除和定期删除:详细介绍… 加强

13、说一下innodb数据存储的结构?

B+树,B+树的特性

14、MySQL怎么做异常恢复的,MySQL挂了,重新启动的时候怎么做异常恢复?

redo-log日志做恢复,redo-log主要是记录写操作,通过里面的写操作记录恢复 两次提交的状态 加强

15、聚簇索引和非聚簇索引的区别?

16、建索引的时候有哪些需要注意的点?

17、你常用Linux命令有哪些?
mv、cd、ls、vim、ps

18、平时使用kafka的时候,出现消息堆积一般是怎么处理的?

19、TCP和UDP的区别?

20、TCP怎么处理拥塞控制的?

21、算法:删除链表倒数第N个节点

广州-没有提供公司名称

主要问Linux运维,排查问题的多
介绍一下Linux,IO多路复用
Linux的文件描述符
常用的Linux指令、vim指令

  1. MySQL的数据结构?

B+树,回答它的特点

  1. B+树和B树的区别?
  2. TCP和UDP的区别
  3. 三次握手和四次挥手的过程

杭州-玩心不止玩网络科技有限公司

项目问答

  1. 介绍一下锁?
  2. CAS是什么?
  3. 自旋的意义是什么?
  4. golang怎么判断对象是分配到堆上还是栈上?
  5. 发生内存泄漏怎么排查?
  6. 介绍一下GMP模型?
  7. 介绍一下GC?
  8. 如果A对象和B对象相互引用,会被GC吗?为什么?
  9. 假设需要请求第三方接口,而第三方接口不太稳定,你会怎么设计?
  10. MySQL的数据结构是什么?
  11. B+树和B树的区别?
  12. Redis的IO复用

欢迎交流讨论

上面提到的面试题,欢迎大家在评论区交流讨论。

尤其是你不知道怎么回答好的,可以交流一下,每天我都会抽时间回复评论的。

早日上岸!

我们搞了一个免费的面试真题共享群,互通有无,一起刷题进步。

没准能让你能刷到自己意向公司的最新面试题呢。

感兴趣的朋友们可以加我微信:wangzhongyang1993,备注:面试群。

本文首发在我的同名公众号:王中阳Go,未经授权禁止转载。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

JSBridge原理 - 前端H5与客户端Native交互

发表于 2024-04-08
  1. 概述:

在混合应用开发中,一种常见且成熟的技术方案是将原生应用与 WebView 结合,使得复杂的业务逻辑可以通过网页技术实现。实现这种类型的混合应用时,就需要解决H5与Native之间的双向通信。JSBridge 是一种在混合应用中实现 Web 和原生代码之间通信的重要机制。

1.1. 混和开发:

混合开发(Hybrid)是一种开发模式,指使用多种开发模型开发App,通常会涉及到两大类技术:

原生 Native、Web H5

  • 原生技术主要指iOS、Android,原生开发效率较低,开发完成需要重新打包整个App,发布依赖用户的更新,性能较高功能覆盖率更高
  • Web H5可以更好的实现发布更新,跨平台也更加优秀,但性能较低,特性也受限

混合开发的意义就在于吸取两者的优点,而且随着手机硬件的升级迭代、系统(Android 5.0+、ISO 9.0+)对于Web特性的较好支持,H5的劣势被逐渐缩小。

1.2. JSBridge 的概念和作用:

  1. 通信桥梁: JSBridge 充当了 Web 应用和原生应用之间的通信桥梁。通过 JSBridge,我们可以在 web 和原生代码之间进行双向通信,使这两者能够互相调用和传递数据。
  2. 原生功能调用: 使用 JSBridge,我们可以在 JavaScript 中调用原生应用中的功能。我们可以通过 web 来触发原生应用中的特定操作,如打开相机、发送通知、调用硬件设备等。
  3. 数据传递: JSBridge 使得 JavaScript 和原生代码之间可以方便地传递数据。意味着我们可以在 web 和原生代码之间传递复杂的数据结构,如对象、数组等,以满足应用的功能需求。
  4. 回调机制: JSBridge 支持回调机制,使得在原生代码执行完某些操作后可以通知 JavaScript,并传递相应的结果。

1.3. 为什么在混合应用开发中 JSBridge 如此重要:

  1. 跨平台开发: JSBridge 允许我们在混合应用中使用一套代码同时运行在不同的平台上。这意味着我们可以使用 Web 技术来开发应用的核心逻辑,并在需要时通过 JSBridge 调用原生功能,从而实现跨平台开发,提高开发效率。
  2. 原生功能扩展: 使用 JSBridge,我们可以充分利用原生平台提供的功能和能力,例如访问硬件设备、调用系统 API 等。这使得我们可以为应用添加更多丰富的功能,提升用户体验。
  3. 灵活性和扩展性: JSBridge 提供了一种灵活和可扩展的方式来实现 Web 和原生代码之间的通信。开发人员可以根据应用的需求随时添加新的原生功能,并通过 JSBridge 在 JavaScript 中调用这些功能,从而实现应用的功能扩展和升级。
  1. JSBridge 做了什么?

在Hybrid模式下,H5会需要使用Native的功能,比如打开二维码扫描、调用原生页面、获取用户信息等,同时Native也需要向Web端发送推送、更新状态等,而JavaScript是运行在单独的 JS Context 中(Webview容器)与原生有运行环境的隔离,所以需要有一种机制实现Native端和Web端的 双向通信 ,这就是JSBridge:以JavaScript引擎或Webview容器作为媒介,通过协定协议进行通信,实现Native端和Web端双向通信的一种机制。

通过JSBridge,Web端可以调用Native端的Java接口,同样Native端也可以通过JSBridge调用Web端的JavaScript接口,实现彼此的双向调用。

  1. JSBridge 实现原理:

把 Web 端和 Native 端的通信比作 Client/Server 模式。JSBridge 充当了类似于 HTTP 协议的角色,实现了 Web 端和 Native 端之间的通信。

将 Native 端原生接口封装成 JavaScript 接口:在 Native 端将需要被调用的原生功能封装成 JavaScript 接口,让 JavaScript 代码可以调用。 JavaScript 接口会被注册到全局对象中,以供 JavaScript 代码调用。

将 Web 端 JavaScript 接口封装成原生接口: 这一步是在 Web 端将需要被调用的 JavaScript 功能封装成原生接口。这些原生接口会通过 WebView 的某些机制暴露给原生代码,以供原生代码调用。

3.1. Native -> Web

Native端调用Web端,JavaScript作为解释性语言,最大的一个特性就是可以随时随地地通过解释器执行一段JS代码,所以可以将拼接的JavaScript代码字符串,传入JS解析器执行就可以,JS解析器在这里就是webView。

3.1.1. Android:

Android 提供了 evaluateJavascript 来执行JS代码,并且可以获取返回值执行回调:

1
2
3
4
5
6
7
java复制代码String jsCode = String.format("window.showWebDialog('%s')", text);
webView.evaluateJavascript(jsCode, new ValueCallback<String>() {
@Override
public void onReceiveValue(String value) {

}
});

3.1.2. IOS:

IOS的 WKWebView 使用 evaluateJavaScript:

1
2
3
4
java复制代码[webView evaluateJavaScript:@"执行的JS代码" 
completionHandler:^(id _Nullable response, NSError * _Nullable error) {
//
}];

3.2. Web -> Native

Web调用Native端主要有两种方式

3.2.1. URL Schema

URL Schema是类URL的一种请求格式,格式如下:

1
2
3
4
java复制代码<protocol>://<host>/<path>?<qeury>#fragment

// 我们可以自定义JSBridge通信的URL Schema,比如:
hellobike://showToast?text=hello

Native加载WebView之后,Web发送的所有请求都会经过WebView组件,所以Native可以重写WebView里的方法,从来拦截Web发起的请求,我们对请求的格式进行判断:

  • 符合我们自定义的URL Schema,对URL进行解析,拿到相关操作、操作,进而调用原生Native的方法
  • 不符合我们自定义的URL Schema,我们直接转发,请求真正的服务

例如:

1
2
3
4
5
6
7
8
9
typescript复制代码  get existOrderRedirect() {
let url: string;
if (this.env.isHelloBikeApp) {
url = 'hellobike://hellobike.com/xxxxx_xxx?from_type=xxxx&selected_tab=xxxxx';
} else if (this.env.isSFCApp) {
url = 'hellohitch://hellohitch.com/xxx/xxxx?bottomTab=xxxx';
}
return url;
}

这种方式从早期就存在,兼容性很好,但是由于是基于URL的方式,长度受到限制而且不太直观,数据格式有限制,而且建立请求有时间耗时。

3.2.2. 在Webview中注入JS API

通过webView提供的接口,App将Native的相关接口注入到JS的Context(window)的对象中

Web端就可以直接在全局 window 下使用这个暴露的全局JS对象,进而调用原生端的方法。

Android注入方法:

  • 4.2 前,Android 注入 JavaScript 对象的接口是 ****addJavascriptInterface ****但是这个接口有漏洞
  • 4.2 之后,Android引入新的接口 @JavascriptInterface 以解决安全问题,所以 Android 注入对对象的方式是有兼容性问题的。

IOS注入方法:

  • iOS的UIWebView:JavaSciptCore 支持 iOS 7.0 及以上系统
  • iOS的WKWebView:WKScriptMessageHandler 支持 iOS 8.0 及以上系统

例如:

  1. 注入全局对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码// 注入全局JS对象
webView.addJavascriptInterface(new NativeBridge(this), "NativeBridge");

class NativeBridge {
private Context ctx;
NativeBridge(Context ctx) {
this.ctx = ctx;
}

// 绑定方法
@JavascriptInterface
public void showNativeDialog(String text) {
new AlertDialog.Builder(ctx).setMessage(text).create().show();
}
}
  1. Web调用方法:
1
2
java复制代码// 调用nativeBridge的方法
window.NativeBridge.showNativeDialog('hello');
  1. H5具体实现:

将功能抽象为一个 AppBridge 类,封装两个方法,处理交互和回调

具体步骤:

  1. 首先需要定义一个 JavaScript 类或者对象来封装 JSBridge 方法。
  2. 在 JavaScript 类或对象的构造函数中,初始化桥接回调的方法。这个方法负责接收来自原生应用的回调数据,并根据回调数据中的信息执行相应的操作。
  3. 调用原生方法: 定义一个方法,用于在 JavaScript 中调用原生方法。这个方法需要接收原生类的映射、要调用的原生方法名以及传递给原生方法的参数,并将这些信息传递给原生应用。
  4. 处理原生回调: 在初始化桥接回调的方法中,需要定义处理原生回调的逻辑。当收到原生应用的回调数据时,根据回调数据中的信息执行相应的操作,比如调用 JavaScript 中注册的回调函数,并传递执行结果或错误信息等。

具体实现代码:

  1. 调用原生方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
typescript复制代码// 定义一个名为 callNative 的方法,用于在 JavaScript 中调用原生方法
callNative<P, R>(classMap: string, method: string, params: P): Promise<R> {
return new Promise<R>((resolve, reject) => {
// 生成一个唯一的回调 ID
const id = v4();
// 将当前的回调函数保存到 __callbacks 对象中,以 callbackId 作为键
this.__callbacks[id] = { resolve, reject, method: `${classMap} - ${method}` };
// 构造通信数据,包括原生类映射、要调用的方法、参数和 callbackId
const data = {
classMap,
method,
params: params === null ? '' : JSON.stringify(params),
callbackId: id,
};
const dataStr = JSON.stringify(data);
// 根据当前环境判断是 iOS 还是 Android,并调用相应平台的原生方法
if (this.env.isIOS && isFunction(window?.webkit?.messageHandlers?.callNative?.postMessage)) {
// 如果是 iOS 平台,则调用 iOS 的原生方法
window.webkit.messageHandlers.callNative.postMessage(dataStr);
} else if (this.env.isAndroid && isFunction(window?.AppFunctions?.callNative)) {
// 如果是 Android 平台,则调用 Android 的原生方法
window.AppFunctions.callNative(dataStr);
}
});
}
  1. 回调处理:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
typescript复制代码// 初始化桥接回调函数,该参数在 constructor 中调用
private initBridgeCallback() {
// 保存旧的回调函数到 oldCallback 变量中
const oldCallback = window.callBack;
// 重新定义 window.callBack 方法,用于处理原生应用的回调数据
window.callBack = (data) => {
// 如果存在旧的回调函数,则调用旧的回调函数
if (isFunction(oldCallback)) {
oldCallback(data);
}
// 获取原生应用的回调信息,包括数据和回调 ID
console.info('native callback', data, data.callbackId);
// 从回调数据中获取回调 ID
const { callbackId } = data;
// 根据回调 ID 查找对应的回调函数
const callback = this.__callbacks[callbackId];
// 如果找到了对应的回调函数
if (callback) {
// 如果回调数据中的 code 为 0,则表示执行成功,调用 resolve 方法处理成功的结果
if (data.code === 0) {
callback.resolve(data.data);
} else {
// 否则,表示执行失败,构造一个错误对象并调用 reject 方法处理错误信息
const error = new Error(data.msg) as Error & {response:unknown};
error.response = data;
callback.reject(error);
}
// 删除已经处理过的回调函数
delete this.__callbacks[callbackId];
}
};
}
  1. 使用:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
typescript复制代码// 调用原生方法的封装函数
callNative<P, R>(classMap: string, method: string, params: P) {
// 从容器中解析出 AppBridge 实例
const bridge = container.resolve<AppBridge>(AppBridge);
// 使用 bind 方法将 AppBridge 实例中的 callNative 方法绑定到 bridge 对象上,并保存到 func 变量中
const func = bridge.callNative.bind(bridge);
// 调用 func 方法,并传入 classMap、method 和 params 参数,实现调用原生方法的功能
return func<P, R>(classMap, method, params);
}


// 打开 webview
// 调用 callNative 方法,传入参数 url,classMap 为 'xxxxx/hitch',method 为 'openWebview'
openWebView(url: string): Promise<void> {
return this.callNative<{url:string}, void>('xxxxx/hitch', 'openWebview', { url });
}


// 获取驾驶证 OCR 信息
getDriverLicenseOcrInfo(
params: HBNative.getDriverLicenseOcrInfo.Params,
): Promise<HBNative.getDriverLicenseOcrInfo.Result> {
// 调用 callNative 方法,传入参数 params,classMap 为 'xxxxx/hitch',method 为 'getOcrInfo'
// 返回一个 Promise 对象,该 Promise 对象用于处理异步结果
return this.callNative<
HBNative.getDriverLicenseOcrInfo.Params,
HBNative.getDriverLicenseOcrInfo.Result>(
'xxxxx/hitch', 'getOcrInfo', params,
);
}

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Jetpack Compose -> mutableStat

发表于 2024-04-08

前言


上一章我们讲解了 Jetpack Compose 的无状态、状态提升、单向数据流 本章我们讲解下状态机制的背后秘密

List


前面我们讲过,通过 by mutableStateOf() 就可以被 Compose 自动订阅了;我们前面是通过 String 类型进行的自动订阅,那么换成其他类型是可以的吗?答案是可以的,只要被 mutableStateOf 包裹之后,它就会被一个 MutableState 包裹,这个 MutableState 就是一个代理对象,状态的订阅和更新会被代理到它上面,所以 我们使用其他类型也是可以的,我们可以来看一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
kotlin复制代码private var number by mutableStateOf(1)

@OptIn(ExperimentalMaterial3Api::class)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
Android_VMTheme {
Surface {
Text(text = "当前数值是: $number", modifier = Modifier.clickable {
number ++
})
}
}
}
}

我们执行 Text 的点击事件,让 number++ 来看下 number 的变化是不是可以及时的更新到结果,运行看下:

SVID_20240408_122244_1.gif

可以看到,是可以实时的更新的,所以说,换成其他类型,也是可以的;

这个时候,可能会有人有疑问了,那么换成非基本数据类型可以吗?比如换成 List,好,我们来试一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
kotlin复制代码private var nums by mutableStateOf(mutableListOf(1, 2, 3))

@OptIn(ExperimentalMaterial3Api::class)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
Android_VMTheme {
Surface {
Column {
for (num in nums) {
Text(text = "当前是第 $num 的文字")
}
}
}
}
}
}

我们来运行看下是否是我们想要的效果:

image.png

达到了我们期望的效果,但是,接下来,我们对这个 List 进行一下修改看下界面是否还会跟着改变,怎么修改呢?我们可以继续使用点击监听的逻辑;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
kotlin复制代码private var nums by mutableStateOf(mutableListOf(1, 2, 3))

@OptIn(ExperimentalMaterial3Api::class)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
Android_VMTheme {
Surface {
Button(onClick = {
nums.add(nums.last() + 1)
}) {
Text(text = "添加内容")
}
Column {
for (num in nums) {
Text(text = "当前是第 $num 的文字")
}
}
}
}
}
}

我们额外增加了一个 Button,点击的时候,每点击一次,就给 List 增加一项内容,每次取最后一个值进行加1的操作,我们来运行看下效果:

SVID_20240408_123643_1.gif

可以看到,并没有达到我们期望的效果,界面内容并没有随着 List 内容的改变而改变,那么这又是为什么呢?我们来一探究竟;

我们先来想一下,这个被 by mutableStateOf 初始化的对象为什么可以被监听?因为它的get 和 set 函数被加了钩子,它的赋值和取值操作被代理了,所以它能够被监听,也就是 nums 的赋值取值被 mutableStateOf 代理了,所以它能够被监听。这个 nums 读的地方在 for 循环中被读取,那么『写』的地方是在哪里呢?是在 Button 的点击监听中更新了,这种写法看起来是没有问题的呀?那么它到底是哪里不对呢?

其实,就是在 nums 更新的地方不对!

nums 的 set 被加了钩子,是针对的 nums 的 set 方法,而不是 add 方法,所以这个改动是不生效的!也就是说 add 逻辑不会触发 setValue 的调用,所以这个改动不生效,也就不会触发自动更新的操作了;也就是说 如果我们强行增加一个 ReCompose 的过程,它的结果是会更新的;

我们来看一个 ReCompose 的过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
kotlin复制代码private var number by mutableStateOf(1)
private var nums by mutableStateOf(mutableListOf(1, 2, 3))

@OptIn(ExperimentalMaterial3Api::class)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
Android_VMTheme {
Surface {
Column {
Text(text = "当前数值是: $number", modifier = Modifier.clickable {
number ++
})
Button(onClick = {
nums.add(nums.last() + 1)
}) {
Text(text = "添加内容")
}
Column {
for (num in nums) {
Text(text = "当前是第 $num 的文字")
}
}
}
}
}
}
}

我们在按钮的上面增加了一个 文字,给这个文字增加了点击监听,同时更改这个 number 的值,因为这些整体是被一个 Column 包裹,那么当 number 改变的时候,整个的区域会被 ReCompose,我们运行看下效果:

SVID_20240408_125301_1.gif

可以看到,当 number 改变的时候,List 的更新也呈现了出来;

所以,Compose 的监听更新是对 『赋值』操作的监听更新,像这种『nums.add(nums.last() + 1)』修改内部状态的是不会触发更新的,从而不会触发界面的刷新;

重新赋值

问题定位了,那么我们怎么来实现界面的刷新呢?首先大家想到的肯定是重新赋值,怎么赋值呢?

1
2
3
4
5
6
7
ini复制代码Button(onClick = {
nums = nums.toMutableList().apply {
add(nums.last() + 1)
}
}) {
Text(text = "添加内容")
}

通过 nums.toMutableList() 转换成一个新的 list 之后赋值给 nums,这样就是执行了一个『赋值』操作,我们运行看下效果:

SVID_20240408_130329_1.gif

我们通过点击,直接实现了界面的刷新操作~

但是,写到这里的时候,好多人会提出疑问了,这种写法会不会带来性能问题,以及这种写法是不是太笨重了,有没有更优雅的写法呢?答案是,不会带来性能问题,有的~

mustableStateListOf

Compose 针对 List 给我们提供另一个 API,叫作 mutableStateListOf,它内部也会创建一个 MutableStateList,并且它内部的变化也会被 Compose 观测到;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
kotlin复制代码    private var nums = mutableStateListOf(1, 2, 3)

@OptIn(ExperimentalMaterial3Api::class)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
Android_VMTheme {
Surface {
Column {
Text(text = "当前数值是: $number", modifier = Modifier.clickable {
number ++
})
Button(onClick = {
nums.add(nums.last() + 1)
}) {
Text(text = "添加内容")
}
Column {
for (num in nums) {
Text(text = "当前是第 $num 的文字")
}
}
}
}
}
}
}

我们直接使用 mutableStateListOf 来监听内部的变化,我们运行看下效果;

SVID_20240408_131312_1.gif

完美的实现了状态变化的界面刷新,并且比起前一种写法要好了很多;

mustableStateMapOf

跟 mutableStateListOf 比较相似的是 mutableStateMapOf 它是创建的一个 Map,并且监听这个 Map 的内部变化;

总结


Compose 里面用 mutableStateOf 创造出的 MutableState 是很简单的判断『是否重新赋值』 所以其无法监听普通的 List 和 Map,包括普通的 mutableListOf 和 mutableMapOf, 只能使用 mutableStateListOf 和 mutableStateMapOf 来解决;

好了,Compose 的课程今天就讲到这里吧~~

下一章预告


重组的性能风险和优化

欢迎三连


来都来了,点个关注,点个赞吧,你的支持是我最大的动力~

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

基于Redission布隆过滤器原理,优缺点及工具类和使用示

发表于 2024-04-08

布隆过滤器原理:布隆过滤器(Bloom Filter)是一种空间效率很高的概率型数据结构,用于判断一个元素是否在一个集合中。它允许有一定的误判率,换取了存储空间的极大节省。这种数据结构在空间效率和查询速度上具有明显优势,尤其适用于大规模数据去重和快速查找的场景。

布隆过滤器的工作原理如下:

布隆过滤器的核心是一个m位的位数组(Bit Array)和k个哈希函数。

  1. 初始化时,布隆过滤器创建一个 m 位的位数组(Bit Array),所有位都设为 0。
  2. 选取 k 个不同的哈希函数,每个函数都能将任意元素映射到位数组的 m 位中的一个位置。
  3. 添加元素时,将元素通过所有 k 个哈希函数进行哈希,得到 k 个位置,并将这些位置的位设为 1。
  4. 检查元素是否存在时,同样通过 k 个哈希函数计算出 k 个位置。如果所有这些位置的位都是 1,则认为元素可能存在;如果任何一个位是 0,则元素一定不存在。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
yaml复制代码开始
|
v
初始化位数组为0,选择k个哈希函数
|
v
添加元素?
|
是 v 否
| |
| 添加元素 返回结果
| |
v |
检查所有哈希位置 |
| v
所有位置都为1? |
| 是 v 否
| 元素可能存在 元素一定不存在
| v
结束

布隆过滤器的优点:

  1. 空间效率:相比于其他数据结构,布隆过滤器使用很少的空间来存储大量数据的存在性信息。
  2. 时间效率:添加和查询元素的时间复杂度都是常数 O(k),与数据量大小无关。
  3. 易于合并:两个布隆过滤器可以通过位数组的 OR 操作来合并。

布隆过滤器的缺点:

  1. 误判率:布隆过滤器可能会错误地判断某个不存在的元素为存在(False Positive),但不会将存在的元素判断为不存在(False Negative)。
  2. 无法删除:传统的布隆过滤器不支持删除操作,因为删除一个元素需要将对应的位设置为 0,这可能会影响其他元素的判断结果。有一种变体叫计数布隆过滤器(Counting Bloom Filter)可以支持删除操作。

Redisson 中的布隆过滤器:

Redisson 提供了基于 Redis 的布隆过滤器实现,它使用 Redis 的数据结构来存储位数组和计算哈希值。

工具类和使用示例:

首先,确保你已经添加了 Redisson 的依赖到你的项目中。

工具类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
arduino复制代码import org.redisson.api.RBloomFilter;
import org.redisson.api.RedissonClient;

public class RedissonBloomFilterUtil<T> {

private RBloomFilter<T> bloomFilter;

public RedissonBloomFilterUtil(RedissonClient redissonClient, String name) {
bloomFilter = redissonClient.getBloomFilter(name);
}

public void createFilter(long expectedInsertions, double falseProbability) {
bloomFilter.tryInit(expectedInsertions, falseProbability);
}

public boolean add(T element) {
return bloomFilter.add(element);
}

public boolean contains(T element) {
return bloomFilter.contains(element);
}
}

使用示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
arduino复制代码import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;

public class BloomFilterExample {

public static void main(String[] args) {
// 配置 Redisson 客户端
Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379");
RedissonClient redissonClient = Redisson.create(config);

// 创建布隆过滤器实例
RedissonBloomFilterUtil<String> bloomFilterUtil = new RedissonBloomFilterUtil<>(redissonClient, "sampleBloomFilter");

// 初始化布隆过滤器:预计插入数量为 1000,误判率为 0.03
bloomFilterUtil.createFilter(1000, 0.03);

// 添加元素
bloomFilterUtil.add("element1");
bloomFilterUtil.add("element2");

// 检查元素是否存在
System.out.println("Does bloom filter contain 'element1'? " + bloomFilterUtil.contains("element1")); // true
System.out.println("Does bloom filter contain 'element3'? " + bloomFilterUtil.contains("element3")); // false (可能)

// 关闭 Redisson 客户端
redissonClient.shutdown();
}
}

在这个示例中,我们首先配置了 Redisson 客户端,并连接到本地的 Redis 服务器。然后,我们创建了一个名为 “sampleBloomFilter” 的布隆过滤器,并初始化它以支持预计的插入数量和可接受的误判率。

接着,我们添加了一些元素,并检查它们是否存在于布隆过滤器中。最后,我们关闭了 Redisson 客户端。

在实际应用中,布隆过滤器可以用于解决缓存穿透问题、邮件过滤、爬虫爬过的网站过滤等场景。例如,在处理用户注册时,可以使用布隆过滤器来检查用户名是否已存在,从而避免插入重复数据。

以上就是基于Redisson布隆过滤器的原理、优缺点以及工具类和使用示例的详细介绍。布隆过滤器是一种非常实用的数据结构,尤其适合在大数据环境下进行高效的数据去重和快速查询操作。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Android Camera各个API录像实践 Androi

发表于 2024-04-08

Android Camera各个API录像实践

前言

上次刚写了《Android相机各个API拍照实践》,实际上用Camera1、Camera2、CameraX三个API的录像功能,我这也写好了,和拍照类似,但是比拍照坑更多,花了我挺多时间调试的,下面记录下。

目标

还是和上一篇文章一样,先明确下目标:

  1. 能够使用Android三种API预览、录像、播放结果
  2. 能够在录像过程中对视频进行放大,类似微信录小视频
  3. 三种API能够自由切换,互不干扰
  4. 能够拿到拍照结果,并保存到系统录像位置

上一篇文章的代码里面还加上了系统拍照、系统选取、系统分享、保存相册等,这里就不重复了,只要保存到DCIM就行了。

效果图

这里还是可以先搞张效果图看下的,我觉得最后弄的还凑合:

ezgif-4-2e54ba82c4.gif
接口封装


本来想把拍照和录像写到同一个接口的,后面发现预览还是有一些差别的,就另外建了个接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
kotlin复制代码import androidx.activity.ComponentActivity
import androidx.core.util.Consumer

interface ICameraVideoHelper<in T> {

/**
* 使用相机API开始预览
*
* @param activity 带lifecycle的activity,提供context,并且便于使用协程
* @param view 根据不同的API可能传入不同的预览页面: SurfaceView、TextureView、PreviewView
*/
fun startPreview(
activity: ComponentActivity,
view: T
)

/**
* 使用相机API 拍视频
*
* @param activity 带lifecycle的activity,提供context,并且便于使用协程
* @param view 根据不同的API可能传入不同的预览页面: SurfaceView、TextureView、PreviewView
* @param callback 结果回调
*/
fun startRecord(
activity: ComponentActivity,
view: T,
callback: Consumer<String>
)

/**
* 缩放
*
* @param activity 带lifecycle的activity,提供context,并且便于使用协程
* @param zoom 缩放倍数
*/
fun zoom(
activity: ComponentActivity,
zoom: Float
)

/**
* 结束视频
*
* @param activity 带lifecycle的activity,提供context,并且便于使用协程
* @param callback 结果回调
*/
fun stopRecord(
activity: ComponentActivity,
callback: Consumer<String>
)

/**
* 释放资源
*/
fun release()
}

startPreview和release还是一样,不过takePhoto换成了另外三个方法,startRecord开始录像,stopRecord停止录像并获取结果,zoom能够在录像过程中放大视频。

自定义相关View

录制按钮

要想录像,首先还是要有个像样的按钮,我瞄了一样微信录像的按钮,自己也搞了个,主要有下面点功能:

  1. 能够触发开始录像、放大、停止录像三种事件
  2. 开始录像时有过场动画
  3. 按钮可以移动,移动距离和放大倍数相关联,页面显示放大倍数
  4. 能够记录录像时间,在外圈更新进度
  5. 有最大录制时长,到达该值时停止录制

然后,我就写了下面一个自定义的按钮,用来录像,效果如上面的Gif图:

RecordButton

因为本篇文章的重点不上这个按钮,这里就不详细介绍了,不是很复杂,代码里面注释也挺清楚。

类似ViewPager的带缩放的容器

为了能够在一个页面能够显示三个录像功能,我这又把RecordButton放到了一个能缩放的容器里面,也是根据我之前自定义View事件的控件改造的:

《自定义view实战(7):大小自动变换的类ViewPager》

稍微对这个控件改造了下,去掉了很多东西,只保留这个缩放功能,控件源码如下:

ScrollViewLayout

当然这个控件也是锦上添花罢了,我们这篇文章的重点是用三种API去录像。

使用Camera1 API

搞定录像按钮的问题,我们就能来编写三种API的代码了。

Camera1预览

首先是Camera1的预览:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
kotlin复制代码/**
* 使用Camera API进行预览
*
* @param activity 带lifecycle的activity,提供context,并且便于使用协程
* @param view Camera API使用的 SurfaceView
*/
override fun startPreview(
activity: ComponentActivity,
view: SurfaceView
) {
// IO协程中执行,
activity.lifecycleScope.launch(Dispatchers.IO) {

// 1、获取后置摄像头ID, 默认 Camera.CameraInfo.CAMERA_FACING_BACK
val cameraId = getCameraId(mFacingType)

// 2、获取相机实例
if (mCamera == null) {
mCamera = Camera.open(cameraId)
}

// 3、设置和屏幕方向一致
setCameraDisplayOrientation(activity, mCamera!!, cameraId)

// 4、设置相机参数
setCameraParameters(mCamera!!)

// 5、在startPreview前设置holder(有前提: surfaceCreated已完成)
// 不要在surfaceCreated设置,不然有问题,使用工具类没法收到surfaceCreated回调
mCamera!!.setPreviewDisplay(view.holder)

// 6、设置SurfaceHolder回调
view.holder.addCallback(mSurfaceCallback)

// 7、开始预览
mCamera!!.startPreview()
}
}

和拍照一模一样,按顺序执行这七步就行,具体代码后面给出。同样,因为是工具类mSurfaceCallback的surfaceCreated收不到,默认已经created了,可以直接open Camera。

Camera1录像

Camera1的录像需要用到MediaRecorder,MediaRecorder使用前注意要先预览,下面看下预览代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
kotlin复制代码/**
* 使用相机API拍视频
*
* @param activity 带lifecycle的activity,提供context,并且便于使用协程
* @param view 使用 SurfaceView 拍视频
* @param callback 结果回调
*/
override fun startRecord(
activity: ComponentActivity,
view: SurfaceView,
callback: Consumer<String>
){
// 创建一个 MediaRecorder 对象,或者重置
if (mMediaRecorder == null) {
mMediaRecorder = MediaRecorder()
}

// 释放相机资源,给mMediaRecorder使用
mCamera?.unlock()

// 设定参数
mMediaRecorder!!.apply {

// 绑定相机
setCamera(mCamera)

// 设置预览画面
setPreviewDisplay(view.holder.surface)

// 设置方向
setOrientationHint(
getCameraDisplayOrientation(activity, getCameraId(mFacingType)))

// 设置视频参数
setAudioSource(MediaRecorder.AudioSource.CAMCORDER)
setVideoSource(MediaRecorder.VideoSource.CAMERA)

// 设置尺寸,注意两者顺序
setOutputFormat(MediaRecorder.OutputFormat.MPEG_4)
setVideoSize(mPreviewSize.width, mPreviewSize.height)

setVideoEncoder(MediaRecorder.VideoEncoder.DEFAULT)
setAudioEncoder(MediaRecorder.AudioEncoder.DEFAULT)

// 获取临时视频路径
mTempPath = getTempVideoPath(activity).absolutePath

// 设置输出文件路径
setOutputFile(mTempPath)

// 准备 MediaRecorder
prepare()

// 开始录制
start()

// 传出路径
callback.accept(mTempPath)
}
}

实际就是创建一个mMediaRecorder对象,设置好相关参数,调用start进行录制就可以。

这里代码看起来简单,实际有好多坑,下面一个一个讲。

首先是mCamera的unlock一定要调用,而且在stopRecord的时候还要调用lock方法,目的就是释放相机资源,给mMediaRecorder使用。

其次就是这里的参数设置是有顺序的,不要乱改顺序,当然读者也可以改下顺序,看下哪些会有问题。

在一个就是setVideoSize方法一定要传入合适的尺寸,不然会出问题,可能就是prepare抛出异常,造成闪退,我这没try-catch prepare方法,因为prepare失败了也没法用啊。

Camera1缩放

Camera1的缩放比较简单,拿到mCamera的params进行修改就行了,重新设置即生效。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
kotlin复制代码override fun zoom(
activity: ComponentActivity,
zoom: Float
){
val params = mCamera!!.parameters

// 检查设备是否支持变焦
if (!params.isZoomSupported) {
// 处理不支持的情况,例如提示用户或忽略请求
return
}

// 确保缩放级别在允许的范围内
val maxZoom = params.maxZoom
val zoomLevel = kotlin.math.min(maxZoom, 1 + (zoom * (maxZoom - 1)).toInt())

// 设置新的缩放级别
params.zoom = zoomLevel

// 应用新的参数到相机
mCamera!!.parameters = params
}

Camera1停止录像

停止录像调用mMediaRecorder的stop方法即可,不过这里还要reset一下,不然无法继续拍照。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
kotlin复制代码override fun stopRecord(activity: ComponentActivity, callback: Consumer<String>) {
mMediaRecorder?.let {
var isTooShort = false
try {
// 时间过短无法stop
it.stop()
}catch (e: Exception) {
isTooShort = true
e.printStackTrace()
}

it.reset()
mCamera?.lock()
continuePreview()
callback.accept(if (isTooShort) "" else mTempPath)
}
}

mCamera的lock上面有提到,Camera1拍完照之后不会再预览了,需要手动调用下。

这里有个坑就是,录制时间过短的话stop会失败,造成闪退,找了很久也没找到这个最短时间,我就不如catch这个异常,直接传出去算了。

Camera1释放资源

加了一个mMediaRecorder,需要注意它的释放。

1
2
3
4
5
6
7
8
9
10
kotlin复制代码/**
* 释放资源
*/
override fun release() {
mCamera?.stopPreview()
mCamera?.release()
mMediaRecorder?.release()
mCamera = null
mMediaRecorder = null
}

完整代码

Camera1VideoHelper

使用Camera2 API

使用Camera1进行录像相对来说还是比较简单的,只不过就是加了一个MediaRecorder,到了Camera2感觉就头疼了,下面看下吧。

这里先说一下啊,我这用的Camera2 API录像可能不是最佳选择,下面我把预览和拍照分成了两个独立的Session,实际是可以写成一个的,只不过会把预览和录像搞在一起,看需要吧。

Camera2预览

前面说了,我把预览和拍照分成了两个独立的Session,所以这里Camera2预览就仅仅需要预览罢了,下面看代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
kotlin复制代码/**
* 使用Camera2 API进行预览
*
* @param activity 带lifecycle的activity,提供context,并且便于使用协程
* @param view Camera API2使用的 TextureView(当然也能用SurfaceView)
*/
override fun startPreview(
activity: ComponentActivity,
view: TextureView
) {
// 持有TextureView的弱引用,便于释放资源
mTextureViewRef = WeakReference(view)

// IO协程中执行,
activity.lifecycleScope.launch(Dispatchers.IO) {

// 1、获取CameraManager
val cameraManager = ContextCompat.getSystemService(activity, CameraManager::class.java)
?: throw IllegalStateException("get cameraManager fail")

// 2、获取摄像头mCameraId、摄像头信息mCameraCharacteristics
chooseCameraIdByFacing(mFacingType, cameraManager)

// 3. 获取预览和录像的尺寸(!!!N多错误都是尺寸造成的)
getSizes()

// 4、开启相机
mCameraDevice = openCamera(cameraManager)

// 5.创建预览Session
val surface = getSurface(view)
mPreviewSession = startCaptureSession(mutableListOf(
// 注意一定要传入使用到的surface,不然会闪退
surface
), mCameraDevice!!)

// 6.设置textureView回调,destroy时释放资源
view.surfaceTextureListener = mTextureViewCallback

// 7.开始预览,预览和拍照都用request实现
preview(surface)
}
}

和拍照的预览相对比,去掉了一个ImageReader的配置,然后就是着重写了下尺寸的获取:

1
2
3
4
5
6
7
8
9
10
11
12
kotlin复制代码private fun getSizes() {
// 获取尺寸
mCameraCharacteristics?.let { info ->
val map = info.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)!!

// !!!再次注意width比height更大,不然选不到对的size
mVideoSize = getOptimalPreviewSize(
map.getOutputSizes(MediaRecorder::class.java).toList(), 1920, 1080)
mPreviewSize = getOptimalPreviewSize(
map.getOutputSizes(MediaRecorder::class.java).toList(), 1920, 1080)
}
}

这个很重要,这里一定要选对尺寸,不然后面MediaRecorder的prepare就是过不去。其他很好理解,使用可以看下完整代码。

Camera2录像

Camera2录像和Camera1的录像比起来就复杂了,先看下代码,我尽量把要注意的细节说一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
kotlin复制代码/**
* 使用相机API拍视频
*
* @param activity 带lifecycle的activity,提供context,并且便于使用协程
* @param view 使用 TextureView 拍视频
* @param callback 结果回调
*/
override fun startRecord(
activity: ComponentActivity,
view: TextureView,
callback: Consumer<String>
){
// IO协程中执行,
activity.lifecycleScope.launch(Dispatchers.Main) {

// 关闭预览对话
closePreviewSession()

// 设置MediaRecorder
setUpMediaRecorder(activity, callback)

// 创建
mRecordRequestBuilder = mCameraDevice!!.createCaptureRequest(CameraDevice.TEMPLATE_RECORD)

// 设置预览输出
val previewSurface = getSurface(view)
mRecordRequestBuilder!!.addTarget(previewSurface)

// 设置录像输出
val recorderSurface = mMediaRecorder!!.surface
mRecordRequestBuilder!!.addTarget(recorderSurface)

// 创建新的预览对话,能将视频输出到录像surface
mPreviewSession = startCaptureSession(mutableListOf(
previewSurface, recorderSurface
), mCameraDevice!!)

// 录像请求
mRecordRequestBuilder!!.set(CaptureRequest.CONTROL_AF_MODE,
CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_VIDEO)
mPreviewSession!!.setRepeatingRequest(mRecordRequestBuilder!!.build(), null, mHandler)

// 启动录制
mMediaRecorder!!.start()
}
}

预览Session切换

首先,这里要先把预览的session给关了,创建一个带预览和录像的session进行录像,我这都用mPreviewSession去保存,但要注意下这里有两个session:

1
2
3
4
kotlin复制代码private fun closePreviewSession() {
mPreviewSession?.close()
mPreviewSession = null
}

下面是创建新的Session:

1
2
3
4
kotlin复制代码// 创建新的预览对话,能将视频输出到录像surface
mPreviewSession = startCaptureSession(mutableListOf(
previewSurface, recorderSurface
), mCameraDevice!!)

MediaRecorder配置

其次,MediaRecorder的配置也比较容易出错:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
kotlin复制代码private fun setUpMediaRecorder(activity: ComponentActivity, callback: Consumer<String>) {
// 创建一个 MediaRecorder 对象,或者重置
if (mMediaRecorder == null) {
mMediaRecorder = MediaRecorder()
}

// 设定参数
mMediaRecorder!!.apply {

// 设置视频参数(!!!注意视频不是从Camera来了)
setAudioSource(MediaRecorder.AudioSource.MIC)
setVideoSource(MediaRecorder.VideoSource.SURFACE)

// 设置输出文件路径
setOutputFormat(MediaRecorder.OutputFormat.MPEG_4)
mTempPath = getTempVideoPath(activity).absolutePath
setOutputFile(mTempPath)

// 设置比特率和帧率
setVideoEncodingBitRate(100000000)
setVideoFrameRate(30)

// 设置尺寸,注意两者顺序
setVideoSize(mVideoSize!!.width, mVideoSize!!.height)

setAudioEncoder(MediaRecorder.AudioEncoder.AAC)
setVideoEncoder(MediaRecorder.VideoEncoder.H264)

// !!!不能掉了,不然prepare不会成功
setOrientationHint(90)

// 准备 MediaRecorder
prepare()

// 传出路径
callback.accept(mTempPath)
}
}

这里MediaRecorder设置setVideoSource为MediaRecorder.VideoSource.SURFACE后,它自己就带了一个surface,我们要通过MediaRecorder的getSurface,向里面传递数据。

这里的参数顺序也要注意下,最最最坑的就是这个尺寸了,这里不能和拍照一样使用最大尺寸,使用的话就闪退,后面我重写了getSizes方法,才让prepare生效,实际就是(width=1920, height=1080),不过还是要根据机型决定:

1
2
3
4
5
6
7
8
9
10
11
12
kotlin复制代码private fun getSizes() {
// 获取尺寸
mCameraCharacteristics?.let { info ->
val map = info.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)!!

// !!!再次注意width比height更大,不然选不到对的size
mVideoSize = getOptimalPreviewSize(
map.getOutputSizes(MediaRecorder::class.java).toList(), 1920, 1080)
mPreviewSize = getOptimalPreviewSize(
map.getOutputSizes(MediaRecorder::class.java).toList(), 1920, 1080)
}
}

如果发生各种异常,代码又觉得没错,那估计就是你的尺寸出错了,很坑。

录像请求

上面切换Session后,要注意把预览和录像的surface传进去,这里是两个surface了:

1
2
3
4
5
6
7
kotlin复制代码// 设置预览输出
val previewSurface = getSurface(view)
mRecordRequestBuilder!!.addTarget(previewSurface)

// 设置录像输出
val recorderSurface = mMediaRecorder!!.surface
mRecordRequestBuilder!!.addTarget(recorderSurface)

切换session并发送请求后,就可以路线了:

1
2
3
4
5
6
7
kotlin复制代码// 录像请求
mRecordRequestBuilder!!.set(CaptureRequest.CONTROL_AF_MODE,
CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_VIDEO)
mPreviewSession!!.setRepeatingRequest(mRecordRequestBuilder!!.build(), null, mHandler)

// 启动录制
mMediaRecorder!!.start()

总而言之,Camera2的录像比较复杂。

Camera2缩放

看完Camera2的录像是不是觉得头痛,只可惜Camera2缩放也是让人头痛,下面看代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
kotlin复制代码override fun zoom(
activity: ComponentActivity,
zoom: Float
){
// 缩放实际是通过修改Rect实现的
val zoomRect = calculateZoomRect(zoom)
// 这两行代码只是为了防止重复请求
mPreviewSession?.stopRepeating()
// 创建请求修改
mRecordRequestBuilder!!.set(CaptureRequest.SCALER_CROP_REGION, zoomRect)
mPreviewSession?.setRepeatingRequest(mRecordRequestBuilder!!.build(), null, mHandler)

}

private fun calculateZoomRect(zoomLevel: Float): Rect {
val sensorRect = mCameraCharacteristics!!.get(
CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE)!!

val minZoom = 1.0f
val maxZoom = mCameraCharacteristics!!.get(
CameraCharacteristics.SCALER_AVAILABLE_MAX_DIGITAL_ZOOM)!!
val currentZoom = minZoom + (maxZoom - minZoom) * zoomLevel

val centerX = sensorRect.width() / 2
val centerY = sensorRect.height() / 2
val deltaX = (0.5f * sensorRect.width() / currentZoom).toInt()
val deltaY = (0.5f * sensorRect.height() / currentZoom).toInt()

val zoomRect = Rect()
zoomRect.left = centerX - deltaX
zoomRect.right = centerX + deltaX
zoomRect.top = centerY - deltaY
zoomRect.bottom = centerY + deltaY

return zoomRect
}

真不知道谁设计的这功能,缩放居然是通过修改Rect实现的,这里需要我们计算缩放的Rect,好在我用GPT帮我写的。

这里我通过持有mRecordRequestBuilder,并修改了参数,再次对mPreviewSession发起请求,算是有用了。

Camera2停止录像

Camera2停止录像和拍照的差不多,只不过拍照后需要重新预览,这里不做处理,在使用的地方调用吧。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
kotlin复制代码override fun stopRecord(activity: ComponentActivity, callback: Consumer<String>) {
mMediaRecorder?.let {
var isTooShort = false
try {
// 时间过短无法stop
it.stop()
}catch (e: Exception) {
isTooShort = true
e.printStackTrace()
}

it.reset()

// 重新预览(外部去操作吧,这里不动了)
// startPreview(activity, mTextureViewRef!!.get()!!)

callback.accept(if (isTooShort) "" else mTempPath)
}
}

Camera2释放资源

记得把mMediaRecorder和持有的mRecordRequestBuilder释放了。

1
2
3
4
5
6
7
8
9
10
11
kotlin复制代码override fun release() {
// 从 SurfaceTexture 中移除 SurfaceTextureListener
mTextureViewRef?.get()?.surfaceTextureListener = null
// 需要关闭这三个
mCameraDevice?.close()
mPreviewSession?.close()
mMediaRecorder?.release()
mMediaRecorder = null
mRecordRequestBuilder = null
mHandler.removeCallbacksAndMessages(null)
}

完整代码

Camera2VideoHelper

使用CameraX API

引引入CameraX库

这里和拍照类似,暂且列一下吧,这用的version catalog管理依赖,实际都差不多:

1
2
3
4
5
6
7
8
9
10
toml复制代码# cameraX
camerax = "1.1.0-beta01"

# cameraX
camerax = { module = "androidx.camera:camera-core", version.ref = "camerax"}
camerax_camera2 = { module = "androidx.camera:camera-camera2", version.ref = "camerax"}
camerax_video = { module = "androidx.camera:camera-video", version.ref = "camerax"}
camerax_lifecycler = { module = "androidx.camera:camera-lifecycle", version.ref = "camerax"}
camerax_view = { module = "androidx.camera:camera-view", version.ref = "camerax"}
camerax_extensions = { module = "androidx.camera:camera-extensions", version.ref = "camerax"}

在dependence里面添加上依赖,就能开始写代码了。

1
2
3
4
5
6
7
8
9
10
11
kotlin复制代码dependencies {
//。。。

// CameraX 相关依赖
implementation(libs.camerax)
implementation(libs.camerax.camera2)
implementation(libs.camerax.lifecycler)
implementation(libs.camerax.video)
implementation(libs.camerax.view)
implementation(libs.camerax.extensions)
}

CameraX预览

需要注意下,录像和前面不一样,CameraX提供了录像功能,只要使用videoCapture便可以,下面是代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
kotlin复制代码/**
* 使用CameraX API进行预览
*
* @param activity 带lifecycle的activity,提供context,并且便于使用协程
* @param view Camera API使用的 PreviewView
*/
@SuppressLint("RestrictedApi")
override fun startPreview(
activity: ComponentActivity,
view: PreviewView
) {
val cameraProviderFuture = ProcessCameraProvider.getInstance(activity)
cameraProviderFuture.addListener({
// 用于将相机的生命周期绑定到生命周期所有者
// 消除了打开和关闭相机的任务,因为 CameraX 具有生命周期感知能力
mCameraProvider = cameraProviderFuture.get()

// 预览
val preview = Preview.Builder()
.build()
.also {
it.setSurfaceProvider(view.surfaceProvider)
}

// 录像的使用场景
val recorder = Recorder.Builder()
.setQualitySelector(QualitySelector.from(Quality.HIGHEST))
.build()
videoCapture = VideoCapture.withOutput(recorder)

// 选择摄像头,省去了去判断摄像头ID
val cameraSelector = mSelector

try {
// Unbind use cases before rebinding
mCameraProvider!!.unbindAll()

// 将相机绑定到 lifecycleOwner,就不用手动关闭了
mCamera = mCameraProvider!!.bindToLifecycle(
activity, cameraSelector, preview, videoCapture)

} catch(exc: Exception) {
Log.e("TAG", "Use case binding failed", exc)
}

// 回调代码在主线程处理
}, ContextCompat.getMainExecutor(activity))
}

需要注意的是videoCapture别和ImageCapture一样用Builder创建,会提示报错,如果强行使用的话会很卡很卡!按网上说的,这里会把预览和录像的surface叠加,导致卡顿,虽然很多博客都是通过Builder创建的,实际上用法是错的。。。

我们需要通过recorder去创建videoCapture,来实现录像功能。

CameraX录像

CameraX录像稍微复杂一些,网上大部分博文都是通过videoCapture去startRecord,其实不对,正确用法应该如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
kotlin复制代码/**
* 使用相机API拍视频
*
* @param activity 带lifecycle的activity,提供context,并且便于使用协程
* @param view 使用 PreviewView 拍视频
* @param callback 结果回调
*/
@SuppressLint("MissingPermission")
override fun startRecord(
activity: ComponentActivity,
view: PreviewView,
callback: Consumer<String>
){
// 视频文件
val videoFile = getTempVideoPath(activity)
mTempPath = videoFile.absolutePath
val outputFileOptions = FileOutputOptions.Builder(videoFile).build()

// 录像(直接用videoCapture的写法预览会卡顿)
mRecording = videoCapture!!.output
.prepareRecording(activity, outputFileOptions)
.apply {
withAudioEnabled()
}
.start(ContextCompat.getMainExecutor(activity)) { recordEvent ->
when(recordEvent) {
is VideoRecordEvent.Start -> {}
is VideoRecordEvent.Finalize -> {
if (!recordEvent.hasError()) {
mRecordEndCallback?.accept(mTempPath)
} else {
// 录制失败
mRecording?.close()
mRecording = null
mRecordEndCallback?.accept("")
}
}
}
}

// 传出地址
callback.accept(mTempPath)
}

这里有个坑,因为我们需要在stopRecord中拿到回调的视频路径,但是这个视频完成录制是异步的,只能在startRecord里面的代码监听,所以我这加了个mRecordEndCallback来传递结果,具体代码要结合后面stopRecord来看。

CameraX缩放

CameraX的缩放需要通过mCamera的cameraControl去设置:

1
2
3
4
5
6
7
8
9
10
11
12
13
kotlin复制代码override fun zoom(
activity: ComponentActivity,
zoom: Float
){
mCamera?.let {
// 先获取最大缩放级别
val zoomState = it.cameraInfo.zoomState
val maxZoomRatio = zoomState.value?.maxZoomRatio ?: 1.0f
// 设置缩放级别
val zoomLevel = 1 + (zoom * (maxZoomRatio - 1))
it.cameraControl.setZoomRatio(zoomLevel)
}
}

注意下,这个mCamera是在bindToLifecycle时的返回值,我们在拍照的时候并未用到:

1
2
3
4
5
6
7
8
9
10
11
kotlin复制代码try {
// Unbind use cases before rebinding
mCameraProvider!!.unbindAll()

// 将相机绑定到 lifecycleOwner,就不用手动关闭了
mCamera = mCameraProvider!!.bindToLifecycle(
activity, cameraSelector, preview, videoCapture)

} catch(exc: Exception) {
Log.e("TAG", "Use case binding failed", exc)
}

CameraX停止录像

CameraX停止录像只需要通过mRecording执行stop就行,只是stop是异步的,所以这里需要先保存下callback,再stop:

1
2
3
4
5
kotlin复制代码override fun stopRecord(activity: ComponentActivity, callback: Consumer<String>) {
// 注意stopRecording是个异步方法,先保存下,在startRecording的callback里面触发
mRecordEndCallback = callback
mRecording!!.stop()
}

最后在startRecord代码中的回调中执行,通过“?.”操作符就能在录像完成时触发回调了:

1
2
3
4
5
6
7
8
9
10
11
12
13
kotlin复制代码when(recordEvent) {
is VideoRecordEvent.Start -> {}
is VideoRecordEvent.Finalize -> {
if (!recordEvent.hasError()) {
mRecordEndCallback?.accept(mTempPath)
} else {
// 录制失败
mRecording?.close()
mRecording = null
mRecordEndCallback?.accept("")
}
}
}

CameraX释放资源

这里多了一个mCamera需要释放,内部设置的回调mRecordEndCallback也清除下:

1
2
3
4
5
6
7
8
9
10
kotlin复制代码/**
* 释放资源
*/
override fun release() {
// 取消绑定生命周期观察者
mCameraProvider?.unbindAll()
mCameraProvider = null
mRecordEndCallback = null
mCamera = null
}

完整代码

CameraXVideoHelper

使用Demo

使用的demo就是上面gif显示的内容,代码如下:

TakeVideoFragment

小结

这篇文章把Android相机中Camera1、Camera2、CameraX三个API的录像功能实践了下,涉及不深,主要就是使用,记录学习的过程吧。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Android Camera各个API拍照实践 Androi

发表于 2024-04-07

Android Camera各个API拍照实践

前言

好久没更新博客了,过了年基本就没怎么动了,不过还是做了一些东西,最近有时间觉得还是得写一写,不然过段时间就忘了,不划算。

最近把Android Camera的三种API一一试了下,实现了拍照和录像,和图片、bitmap相关的功能也练习了下,比如获取、保存、删除图片等。

ps. 录像的文章也写好了,有兴趣可以继续看下:

Android Camera各个API录像实践

下面就来记录下Android三种API的拍照实践。

目标

先来明确下目标,即要做到的效果:

  1. 能够使用Android三种API预览、拍照、显示结果: Camera1、Camera2、CameraX
  2. 三种API能够自由切换,互不干扰
  3. 能够拿到拍照结果,并对图片进行一些操作,如保存、删除等

还是比较简单的,下面就开干。

使用Camera1 API

接口封装

在一段摸索后,我先抽象了一个接口,用来统一三种API的行为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
kotlin复制代码import android.graphics.Bitmap
import androidx.activity.ComponentActivity
import androidx.core.util.Consumer

interface ICameraCaptureHelper<in T> {

/**
* 使用相机API开始预览
*
* @param activity 带lifecycle的activity,提供context,并且便于使用协程
* @param view 根据不同的API可能传入不同的预览页面: SurfaceView、TextureView、PreviewView
*/
fun startPreview(
activity: ComponentActivity,
view: T
)

/**
* 使用相机API拍照
*
* @param activity 带lifecycle的activity,提供context,并且便于使用协程
* @param view 根据不同的API可能传入不同的预览页面: SurfaceView、TextureView、PreviewView
* @param callback 结果回调
*/
fun takePhoto(
activity: ComponentActivity,
view: T,
callback: Consumer<Bitmap>
)

/**
* 释放资源
*/
fun release()
}

其实就三个方法,预览、拍照、释放资源,这里我传入了ComponentActivity来获取context,也方便用它的lifecycleScope来使用协程。因为拍照预览可以用SurfaceView、TextureView、PreviewView这几个,我这直接设置成了泛型,看情况用吧。

封装好接口,我们就一步一步实现功能了。

Camera1预览

使用Camera1 API,必须先预览才能拍照,其他API倒没有要求。我这用了SurfaceView来预览,TextureView也可以,下面看下startPreview的写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
kotlin复制代码/**
* 使用Camera API进行预览
*
* @param activity 带lifecycle的activity,提供context,并且便于使用协程
* @param view Camera API使用的 SurfaceView
*/
override fun startPreview(
activity: ComponentActivity,
view: SurfaceView
) {
// IO协程中执行,
activity.lifecycleScope.launch(Dispatchers.IO) {

// 1、获取后置摄像头ID: 默认 Camera.CameraInfo.CAMERA_FACING_BACK
val cameraId = getCameraId(mFacingType)

// 2、获取相机实例
if (mCamera == null) {
mCamera = Camera.open(cameraId)
}

// 3、设置和屏幕方向一致
setCameraDisplayOrientation(activity, mCamera!!, cameraId)

// 4、设置相机参数
setCameraParameters(mCamera!!)

// 5、在startPreview前设置holder(有前提: surfaceCreated已完成)
// 不要在surfaceCreated设置,不然有问题,使用工具类没法收到surfaceCreated回调
mCamera!!.setPreviewDisplay(view.holder)

// 6、设置SurfaceHolder回调
view.holder.addCallback(mSurfaceCallback)

// 7、开始预览
mCamera!!.startPreview()
}
}

主要就是这七步,首先要根据摄像头类型获得cameraId,再根据cameraId去打开摄像头,这时候摄像头的方向默认是横着的,还得改下摄像头方向,详细的代码后面会完整提供,现在大致看下流程,

参数设置特别要注意下,这里的width要比height更大,而且要选对尺寸,不然会造成各种意想不到的问题,后面几个API也是一样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
kotlin复制代码private fun setCameraParameters(camera: Camera) {
val params = camera.parameters

// 设置图像格式
params.previewFormat = ImageFormat.NV21

// 设置预览尺寸(注意相机方向是width>height)
val previewSize = getOptimalPreviewSize(params.supportedPreviewSizes, 1920, 1080)
params.setPreviewSize(previewSize.width, previewSize.height)

// 设置图片尺寸
params.setPictureSize(previewSize.width, previewSize.height)

// 设置对焦模式
params.focusMode = Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE

// 设置闪光灯模式
params.flashMode = Camera.Parameters.FLASH_MODE_AUTO

// 设置场景模式
params.sceneMode = Camera.Parameters.SCENE_MODE_AUTO

// 应用参数设置
camera.parameters = params
}

private fun getOptimalPreviewSize(sizes: List<Camera.Size>, w: Int, h: Int): Camera.Size {
val targetRatio = w.toDouble() / h
return sizes.minByOrNull { abs(it.width.toDouble() / it.height - targetRatio) } ?: sizes[0]
}

另外一个需要注意的就是mSurfaceCallback,正常使用的话,应该在mSurfaceCallback的surfaceCreated里面去open Camera的,不过我这写成了工具类,调用的时候收不到surfaceCreated,而是已经created了,所以不处理mSurfaceCallback,直接open,当然这里也要根据实际情况去处理。

再一个就是在startPreview前,一定要调用setPreviewDisplay,传入SurfaceView的holder,再通过mCamera去startPreview就可以预览了,预览的时候确保下SurfaceView处于VISIBLE状态,预览就完成了。

Camera1拍照

搞定预览后,拍照功能其实就完成的差不多了,通过mCamera去takePicture就行了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
kotlin复制代码/**
* 使用相机API拍照
*
* @param activity 带lifecycle的activity,提供context,并且便于使用协程
* @param view SurfaceView
* @param callback 结果回调
*/
override fun takePhoto(
activity: ComponentActivity,
view: SurfaceView,
callback: Consumer<Bitmap>
) {
// camera1 API需要先预览才能拍照
if (mCamera == null) {
throw IllegalStateException("camera not prepared!!!")
}

// IO协程中执行,
activity.lifecycleScope.launch(Dispatchers.IO) {
mCamera!!.takePicture(null, null) { data, _ ->

// 处理拍照结果
val bitmap = BitmapFactory.decodeByteArray(data, 0, data.size)
val cameraInfo = Camera.CameraInfo()
Camera.getCameraInfo(Camera.CameraInfo.CAMERA_FACING_BACK, cameraInfo)
val rotation = cameraInfo.orientation

// 将结果投递到UI线程
activity.lifecycleScope.launch(Dispatchers.Main) {
callback.accept(rotateBitmap(bitmap, rotation))
}
}
}
}

private fun rotateBitmap(bitmap: Bitmap, degrees: Int): Bitmap {
val matrix = Matrix()
matrix.postRotate(degrees.toFloat())
return Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true)
}

这里传出的bitmap,并且调整了下方向,没什么好说的。

Camera1释放资源

只会写拍照,没什么好说的,要能正确释放资源,才是一个好程序员,下面看下释放资源:

1
2
3
4
5
6
7
8
kotlin复制代码/**
* 释放资源
*/
override fun release() {
mCamera?.stopPreview()
mCamera?.release()
mCamera = null
}

完整代码

上面讲了个大概,下面提供完整代码,加了个takePhotoNoFeeling无感拍照和continuePreview,使用Camera1拍照后,需要调用startPreview继续预览才能再拍照,需要注意下。
(代码有点长,还是去GitHub看吧)

Camera1CaptureHelper

使用Camera2 API

Camera1被标记过时了,Google推荐使用CameraX,CameraX其实用的也是Camera2,只不过Camera2比较难用,但是学习嘛,总得试试,就仿照上面Camera1的写法,下面看下Camera2的使用。

Camera2预览

Camera1中我们用了SurfaceView,这里就来用下TextureView,关于两者区别可以查下资料,这里不详叙,下面看预览代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
kotlin复制代码/**
* 使用Camera2 API进行预览
*
* @param activity 带lifecycle的activity,提供context,并且便于使用协程
* @param view Camera API2使用的 TextureView(当然也能用SurfaceView)
*/
override fun startPreview(
activity: ComponentActivity,
view: TextureView
) {
// 持有TextureView的弱引用,便于释放资源
mTextureViewRef = WeakReference(view)

// IO协程中执行,
activity.lifecycleScope.launch(Dispatchers.IO) {

// 1、获取CameraManager
val cameraManager = ContextCompat.getSystemService(activity, CameraManager::class.java)
?: throw IllegalStateException("get cameraManager fail")

// 2、获取摄像头mCameraId、摄像头信息mCameraCharacteristics
// 默认 CameraCharacteristics.LENS_FACING_BACK
chooseCameraIdByFacing(mFacingType, cameraManager)

// 3、开启相机
mCameraDevice = openCamera(cameraManager)

// 4、设置如何读取图片的ImageReader
mImageReader = getImageReader()

// 5.创建Capture Session
val surface = getSurface(view)
mSession = startCaptureSession(mutableListOf(
// 注意一定要传入使用到的surface,不然会闪退
surface,
mImageReader!!.surface
), mCameraDevice!!)

// 6.设置textureView回调
view.surfaceTextureListener = mTextureViewCallback

// 7.开始预览,预览和拍照都用request实现
preview(surface)
}
}

其实吧,和Camera1类似,都要选择相机得到mCameraId,再开启相机,只不过Camera2预览的时候,要要创建Session对话,还要把要输出的surface全部传进去,而且图片要通过ImageReader去读取,这么一搞真就复杂多了。

这里要注意下创建的session,要把预览及ImageReader的surface都传进去,不然就出错了。

1
2
3
4
5
6
7
kotlin复制代码// 5.创建Capture Session
val surface = getSurface(view)
mSession = startCaptureSession(mutableListOf(
// 注意一定要传入使用到的surface,不然会闪退
surface,
mImageReader!!.surface
), mCameraDevice!!)

另外,这两者的尺寸也要匹配下,我这两个都是用的最大尺寸:

1
2
3
4
kotlin复制代码val map = info.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)
?: throw IllegalStateException("Cannot get available preview/video sizes")
val largest = map.getOutputSizes(ImageFormat.JPEG).maxByOrNull { it.width * it.height }
mLargestSize = largest ?: throw IllegalStateException("Cannot get largest preview size")

和mSurfaceCallback类似,Camera2的openCamera也应该在mTextureViewCallback的onSurfaceTextureAvailable中调用,只不过我这写成工具类,收不到这个回调,直接就用了。

因为这里有很多回调,我这用了协程和suspend方法,不是必须,只不过能让代码结构更清晰。

Camera2的preview是通过发送重复的请求实现的,其实可以持有previewRequestBuilder,在过程中修改配置,达到想要的效果:

1
2
3
4
5
6
7
8
9
10
11
12
13
kotlin复制代码private fun preview(surface: Surface) {
// 通过模板创建RequestBuilder
// CaptureRequest还可以配置很多其他信息,例如图像格式、图像分辨率、传感器控制、闪光灯控制、
// 3A(自动对焦-AF、自动曝光-AE和自动白平衡-AWB)控制等
val previewRequestBuilder =
mCameraDevice!!.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW)

// 设置预览画面
previewRequestBuilder.addTarget(surface)

mPreviewRequest = previewRequestBuilder.build()
mSession!!.setRepeatingRequest(mPreviewRequest!!, null, mHandler)
}

Camera2拍照

看到上面Camera2的预览就挺麻烦了,结果Camera2的拍照也比Camera1来的麻烦:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
kotlin复制代码/**
* 使用相机API拍照
*
* @param activity 带lifecycle的activity,提供context,并且便于使用协程
* @param view Camera2 API使用的 TextureView(当然也能用SurfaceView)
* @param callback 结果回调
*/
override fun takePhoto (
activity: ComponentActivity,
view: TextureView,
callback: Consumer<Bitmap>
) {
// IO协程中执行,
activity.lifecycleScope.launch(Dispatchers.IO) {

// 1、创建拍照的请求
val captureRequestBuilder =
mCameraDevice!!.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE)

// 2、设置参数
captureRequestBuilder.addTarget(mImageReader!!.surface)
captureRequestBuilder.set(CaptureRequest.CONTROL_MODE, CameraMetadata.CONTROL_MODE_AUTO)

// 3、设置拍照方向
val rotation = activity.windowManager.defaultDisplay.rotation
captureRequestBuilder.set(CaptureRequest.JPEG_ORIENTATION,
getJpegOrientation(mCameraCharacteristics!!, rotation))

mCaptureRequest = captureRequestBuilder.build()

// 4、拍照
// mSession?.stopRepeating() // 这行代码只是为了防止重复请求
// mSession?.abortCaptures() // 这行代码只是为了防止重复请求
mSession!!.capture(mCaptureRequest!!, object : CameraCaptureSession.CaptureCallback() {
override fun onCaptureCompleted(
session: CameraCaptureSession,
request: CaptureRequest,
result: TotalCaptureResult
) {
// 图片已捕获
// 可选步骤,根据需要进行处理
}
}, mHandler)

// 5、设置图片回调,拿到结果
setImageReaderCallback(callback)
}
}

private fun setImageReaderCallback(callback: Consumer<Bitmap>) {
mImageReader?.setOnImageAvailableListener({
val image = mImageReader!!.acquireNextImage()
image?.use {
val planes = it.planes
if (planes.isNotEmpty()) {
val buffer = planes[0].buffer
val data = ByteArray(buffer.remaining())
buffer.get(data)

// 转成bitmap
val bitmap = BitmapFactory.decodeByteArray(data, 0, data.size)

// 传递结果,mHandler应该是在UI线程了
callback.accept(bitmap)
}
}
}, mHandler)
}

这里需要用captureRequestBuilder创建拍照请求,设置好参数,最后通过mSession去拍照,这里能拿到TotalCaptureResult,里面有很多数据,只不过我只想拿bitmap,所以要去mImageReader获取。

Camera2释放资源

Camera2涉及的东西更多,释放资源也更复杂些,需要注意下。

1
2
3
4
5
6
7
8
9
kotlin复制代码override fun release() {
// 从 SurfaceTexture 中移除 SurfaceTextureListener
mTextureViewRef?.get()?.surfaceTextureListener = null
// 需要关闭这三个
mCameraDevice?.close()
mSession?.close()
mImageReader?.close()
mHandler.removeCallbacksAndMessages(null)
}

完整代码

需要注意Android 5.0以后版本才能使用Camera2 API。
Camera2CaptureHelper

使用CameraX API

Camera1 API简单但是过时了,Camera2 API功能强大,使用起来却十分复杂,还好Google在JetPack中提供了CameraX,方便我们使用Camera的相关功能,下面就俩看看吧。

引入CameraX库

CameraX作为JetPack的库,还是需要我们引入库的,我这用的version catalog管理依赖,实际都差不多,就下面几个库(算是把拍照也引入了):

1
2
3
4
5
6
7
8
9
10
toml复制代码# cameraX
camerax = "1.1.0-beta01"

# cameraX
camerax = { module = "androidx.camera:camera-core", version.ref = "camerax"}
camerax_camera2 = { module = "androidx.camera:camera-camera2", version.ref = "camerax"}
camerax_video = { module = "androidx.camera:camera-video", version.ref = "camerax"}
camerax_lifecycler = { module = "androidx.camera:camera-lifecycle", version.ref = "camerax"}
camerax_view = { module = "androidx.camera:camera-view", version.ref = "camerax"}
camerax_extensions = { module = "androidx.camera:camera-extensions", version.ref = "camerax"}

这里的cameraX版本并没有用最新的,我试了1.2和1.3版本,需要升级比较高的gradle版本,想想还是算了。

在dependence里面添加上依赖,就能开始写代码了。

1
2
3
4
5
6
7
8
9
10
11
kotlin复制代码dependencies {
//。。。

// CameraX 相关依赖
implementation(libs.camerax)
implementation(libs.camerax.camera2)
implementation(libs.camerax.lifecycler)
implementation(libs.camerax.video)
implementation(libs.camerax.view)
implementation(libs.camerax.extensions)
}

CameraX预览

前面我们分别使用了SurfaceView和TextureView进行预览,而在CameraX里面提供了更好的PreviewView来预览,下面看下CameraX如何预览:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
kotlin复制代码/**
* 使用CameraX API进行预览
*
* @param activity 带lifecycle的activity,提供context,并且便于使用协程
* @param view Camera API使用的 PreviewView
*/
override fun startPreview(
activity: ComponentActivity,
view: PreviewView
) {
val cameraProviderFuture = ProcessCameraProvider.getInstance(activity)
cameraProviderFuture.addListener({
// 用于将相机的生命周期绑定到生命周期所有者
// 消除了打开和关闭相机的任务,因为 CameraX 具有生命周期感知能力
mCameraProvider = cameraProviderFuture.get()

// 预览
val preview = Preview.Builder()
.build()
.also {
it.setSurfaceProvider(view.surfaceProvider)
}

// 拍照的使用场景
imageCapture = ImageCapture.Builder()
.build()

// 选择摄像头,省去了去判断摄像头ID
// 默认 CameraSelector.DEFAULT_BACK_CAMERA
val cameraSelector = mSelector

try {
// Unbind use cases before rebinding
mCameraProvider!!.unbindAll()

// 将相机绑定到 lifecycleOwner,就不用手动关闭了
mCameraProvider!!.bindToLifecycle(
activity, cameraSelector, preview, imageCapture)

} catch(exc: Exception) {
Log.e("TAG", "Use case binding failed", exc)
}

// 回调代码在主线程处理
}, ContextCompat.getMainExecutor(activity))
}

果然看起来就舒服多了,和Camera2比起来,简单太多了,就是获取了一个mCameraProvider,设置下preview,然后绑定到activity的生命周期就能预览了,根本不需要怎么解释。

如果要拍照,创建个imageCapture,在bindToLifecycle最后加上就行了,这里还帮我们搞定了异步线程问题。

CameraX拍照

CameraX的预览很简单,拍照就更简单了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
kotlin复制代码/**
* 使用相机API拍照
*
* @param activity 带lifecycle的activity,提供context,并且便于使用协程
* @param view Camerax API使用的 PreviewView
* @param callback 结果回调
*/
override fun takePhoto (
activity: ComponentActivity,
view: PreviewView,
callback: Consumer<Bitmap>
) {
// Get a stable reference of the modifiable image capture use case
val imageCapture = imageCapture ?: return

// 直接拍照拿bitmap,存文件可以用 OutputFileOptions
imageCapture.takePicture(
ContextCompat.getMainExecutor(activity),
object : ImageCapture.OnImageCapturedCallback() {
override fun onCaptureSuccess(image: ImageProxy) {
// 转换为 Bitmap,并传递结果
callback.accept(imageProxyToBitmap(image))
image.close()
}

override fun onError(exc: ImageCaptureException) {
// 处理拍摄过程中的异常
Log.e("TAG", "Photo capture failed: ${exc.message}", exc)
}
}
)
}

private fun imageProxyToBitmap(image: ImageProxy): Bitmap {
val planeProxy = image.planes[0]
val buffer = planeProxy.buffer

val bytes = ByteArray(buffer.remaining())
buffer.get(bytes)

return BitmapFactory.decodeByteArray(bytes, 0, bytes.size)
}

直接通过imageCapture的takePicture拍照,拿到image对象就能获取bitmap了,so easy!

CameraX释放资源

CameraX不用了直接解除生命周期的绑定就行了。

1
2
3
4
5
6
7
kotlin复制代码/**
* 释放资源
*/
override fun release() {
// 取消绑定生命周期观察者
mCameraProvider?.unbindAll()
}

完整代码

CameraXCaptureHelper

使用Demo

我这写了个例子,用了这三个API,还加上了系统拍照、系统选取、系统分享、保存相册等,有兴趣可以参考下:

image.png

Demo地址:

TakePhotoFragment

小结

花了点时间,把Android相机中Camera1、Camera2、CameraX三个API的拍照功能实践了下,并编写成工具类方便使用。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…414243…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%