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

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


  • 首页

  • 归档

  • 搜索

你可能一直在kt文件中写Java代码

发表于 2023-06-08

我正在参加「掘金·启航计划」

关注 Kotlin 的大多数开发中可能都是 Android 开发者吧,大家基本也都是慢慢从 Java 逐步迁移到 Kotlin。

得益于 Kotlin 与 Java 之间良好的互通性,有的时候可能我们写代码还是比较随性的,尤其是依旧按照自己过去写 Java 的编程习惯,书写 Kotlin 代码。

但实际上 Kotlin 与 Java 之间编码风格还是有很大的差异的,你的代码可能还是 Java 的咖啡味。现在请你“暂时”忘记 Java 编码规范,放下成见,看一下 Kotlin 有哪些有趣之处。

空判断

你大概早就听腻了 Kotlin 的空安全,可是你在代码里是否还在写if (xx != null) 这样满是咖啡味的代码呢?

现在把你的空判断代码都删除掉吧。使用 ?. 安全调用来操作你的对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
kotlin复制代码// before
fun authWechat() {
if (api != null) {
if (!api.isWXAppInstalled) {
ToastUtils.showErrorToast("您还未安装微信客户端")
return
}
val req = SendAuth.Req()
req.scope = "snsapi_userinfo"
req.state = "none"
api.sendReq(req)
}
}

这段代码粗略看没什么问题吧,判断 IWXAPI 实例是否存在,存在的话判断是否安装了微信,未安装就 toast 提示

但是更符合 Kotlin 味道的代码可以是这样的

1
2
3
4
5
6
7
8
9
10
11
12
13
kotlin复制代码// after
fun authWechat(callbackContext: CallbackContext?) {
val api = DsApplication.getDsInstance().wxapi
api?.takeIf { it.isWXAppInstalled }?.let {
// do something else
it.sendReq(
SendAuth.Req().apply {
scope = "snsapi_userinfo"
state = "none"
}
)
} ?: api?.run { ToastUtils.showErrorToast("您还未安装微信客户端") }
}

使用?.安全调用配合 ?: Elvis 表达式,可以覆盖全部的空判断场景,再配合 takeIf 函数,可以让你的代码更加易读(字面意思上的)

上述代码用文字表达其实就是:

可空对象?.takeIf{是否满足条件}?.let{不为空&满足条件时执行的代码块} ?: run { 为空|不满足条件执行的代码块 }

这样是不是更加符合语义呢?

写在前面:关于代码风格的额外补充

鉴于这一段引起了一些讨论,特意将这段说明移到前面,你如果有任何不同的观点,请先阅读完下面的说明。

友好的讨论我是欢迎的,但是请不要在评论区输出情绪,不友好的评论我会直接删除。

1. 关于链式函数调用、函数式编程

#每天一个知识点# 之前我在 ⌈你可能一直在kt文件中写Java代码⌋ 中介绍过一些关于对对象非空判断然后调用的写法,有的jym表达了反对意见,认为这样写代码非常难以理解、是“屎山”代码,我不得不解释一下为什么要这样写。

kotlin是支持多编程范式的,而高阶函数、函数是一等公民这种思想,都是函数式编程的重要思想。在文中我介绍作用域时简单介绍的let、apply、run、with、also 这几个函数都是kotlin内置的高阶函数,使用它们本就是为了遵循函数式编程。函数式编程思想中有一个重要的理念,用我的理解表达的话:函数是数据变形的过程。如果你用过 RxJava,你应该能深刻理解这一点,在 RxJava 中有大量的中间操作符(链式函数调用),每一个中间操作符其实都是在对 Observable 进行数据上的变形、或产生副作用。

例如:val other = obj?.let{}?.run{}?.takeIf{} ,这样的代码并不是什么屎山代码,而是一个非常典型的数据(对象)的变形过程,一个可空对象obj,经过let函数进行自身数据处理返回值又被 run 函数接收并处理(之前说过run函数一般用于映射),最终产生的对象在经过takeIf条件判断,符合条件则采用,不符合条件则为null,而且中间的每个环节都是空安全的、都可以随时被空中断执行过程。

只是在沸点中三言两语可能无法让你彻底理解这种思想理念,我还是之前对那位jy的回复:如果你对多个高阶函数链式调用觉得很难阅读、理解,可能是你不适合这种编程范式,而非这种写法不好,大可不必强求自己接收这种编程范式。

2.关于使用?空判断

?.表示的是非空对象的传递,其传递路径可以被空中断,它与if-else的流控制并不冲突,如果你的代码涉及到非空对象传递就用?.。这一设计思想个人猜测来自 Haskell 中的包装类型,不同的是 Kotlin 没有使用一个具体的类型类来实现这一效果,而是直接从语法层进行了近似的功能实现,在kotlin中使用?是一种规范的写法。如果你写过JS前端项目,对这种写法就不会有这么多的疑惑。

另外在 Java 中虽然没有从语法层支持链式空判断,但是在 Java8 中引入的 Optional 类,就是用来实现这一效果的。其实 Optional 类非常像 Haskell 中的 Monad。

作用域

还是上面的例子,实例化一个req对象

1
2
3
kotlin复制代码val req = SendAuth.Req()
req.scope = "snsapi_userinfo"
req.state = "none"

更有 Kotlin 味道的代码应该是:

1
2
3
4
kotlin复制代码SendAuth.Req().apply {
scope = "snsapi_userinfo"
state = "none"
}

使用apply{} 函数可以帮我们轻松的初始化对象,或者配置参数,它更好的组织了代码结构,明确了这个闭包处于某个对象的作用域内,所有的操作都是针对这个对象的。

在 Kotlin 的顶层函数中,提供了数个作用域函数,包括上文中的 let 函数,他们大同小异,具体的使用其实更多看编码风格的取舍,例如在我司我们有如下约定:

  • apply{} 用于写,修改、配置对象
  • with(obj){} 用于读,读取对象的字段,用于赋值给其他变量

with() 可以显式的切换作用域,我们常将它用于某个大的闭包内,实现局部的作用域切换,

而且仅用作读时无需考虑作用域的入参命名问题 (多个嵌套的作用域函数往往会带来it的冲突)

  • let{} 用于配合?.用于非空安全调用,安全调用对象的函数
  • run{} 执行代码块、对象映射

run 函数是有返回值的,其返回值是 block块的最后一行,所以它具备对象映射的能力,即将当前作用域映射为另外的对象

  • also{} 读对象,另作他用

当出现超过两行的同一对象使用,无论是读、写,我们就应该考虑使用作用域函数,规范组织我们的代码,使之更具有可读性。

这几个函数其实作用效果可以互相转换,故而这只关乎编码风格,而无关对错之分。

?: Elvis 表达式

非空赋值

虽然说在 Kotlin 中可空对象,使用 ?. 可以轻松的安全调用,但是有的时候我们需要一个默认值,这种情况我们就需要用到 ?: Elvis 表达式。

例如:

1
kotlin复制代码val name: String = getName() ?: "default"

假如 getName() 返回的是一个 String? 可空对象,当他为空时,通过 ?: Elvis 表达式直接给予一个默认值。

配合 takeIf{} 实现特殊的三元表达式

总所周知,kotlin 中没有三元表达式 条件 ? 真值 : 假值,这一点其实比较遗憾,可能是因为 ? 被用作了空表达。

在kotlin 中我们如果需要一个三元表达该怎么做呢?if 条件 真值 else 假值,这样看起来也很简洁明了。

还有一种比较特殊的情况,就是我们判断逻辑,实际上是这个对象是否满足什么条件,也就是说既要空判断,又要条件判断,返回的真值呢又是对象本身。

这种情况代码可能会是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
kotlin复制代码fun getUser(): User? = null
fun useUser(user: User) {}
// 从一个函数中获得了可空对象
val _userNullable = getUser()
// 判断非空+条件,返回对象或者构造不符合条件的值
val user =  if (_userNullable != null && _userNullable.user == "admin") {
   _userNullable
} else {
   User("guess")
}
//使用对象
useUser(user)

这个语句如果我们将if-else塞到 useUser() 函数中作为三元也不是不可以,但是看起来就比较乱了,而且我们也不得不使用一个临时变量_userNullable。

如果我们使用 ?: Elvis 表达式 配合 takeIf{} 可以看起来更为优雅的表达

1
2
3
4
kotlin复制代码fun getUser(): User? = null
fun useUser(user: User) {}
// 使用`?:` Elvis 表达式简化的写法
useUser(getUser()?.takeIf { it.user == "admin" } ?: User("guest"))

这看起来就像是一个特殊的三元 真值.takeIf(条件) ?: 假值,在这种语义表达下,使用?: Elvis 表达式起到了简化代码,清晰语义的作用。

提前返回

当然 ?: Elvis 表达式还有很多其他用途,例如代码块的提前返回

1
2
3
4
kotlin复制代码fun View.onClickLike(user: String?, isGroup: Boolean = false) = this.setOnClickListener {
user?.takeUnless { it.isEmpty() } ?: return@setOnClickListener
StatisticsUtils.onClickLike(this.context, user, isGroup)
}

这里我们对入参进行了非空判断与字符长度判断,在?: Elvis 表达式后提前 return 避免了后续代码被执行,这很优雅也更符合语义。

这里不是说不能用 if 判断,那样虽然可以实现相同效果,但是额外增加了一层代码块嵌套,看起来不够整洁明了。

这些应用本质上都是利用了 ?: Elvis 表达式的特性,即前者为空时,执行后者。

使用函数对象

很多时候我们的函数会被复用,或者作为参数传递,例如在 Android 一个点击事件的函数可能会被多次复用:

1
2
3
4
kotlin复制代码// before
btnA.setOnClickListener { sendEndCommand() }
btnB.setOnClickListener { sendEndCommand() }
btnC.setOnClickListener { sendEndCommand() }

例如这是三个不同帧布局中的三个结束按钮,他们对于的点击事件是同一个,这样写其实也没什么问题,但是他不够 Kotlin 味,我们可以进一步改写

1
2
3
kotlin复制代码btnA.setOnClickListener(::sendEndCommand)
btnB.setOnClickListener(::sendEndCommand)
btnC.setOnClickListener(::sendEndCommand)

使用 :: 双冒号,将函数作为函数对象直接传递给一个接收函数参数的函数(高阶函数),这对于大量使用高阶函数的链式调用场合更加清晰明了,也更加函数式。

ps:这里需要注意函数签名要对应,例如setOnClickListener 的函数签名是View->Unit,故而我们要修改函数与之一致

1
2
3
4
kotlin复制代码@JvmOverloads
fun sendEndCommand(@Suppress("UNUSED_PARAMETER") v: View? = null) {

}

使用 KDoc

你还在用 BugKotlinDocument 这样的插件帮你生成函数注释么?你的函数注释看起来是这样的么?

1
2
3
4
5
6
kotlin复制代码/**
* 获取全部题目的正确率,x:题目序号,y:正确率数值(float)
* @param format ((quesNum: Int) -> String)? 格式化X轴label文字
* @param denominator Int 计算正确率使用的分母
* @return BarData?
*/

这样的注释看起来没什么问题,也能正确的定位到代码中的参数,但实际上这是 JavaDoc ,并不是 KDoc,KDoc使用的是类似 Markdown 语法,我们可以改写成这样:

1
2
3
4
5
6
kotlin复制代码/**
* 获取全部题目的正确率的BarData,其中,x:题目序号,y:正确率数值(float)。
* [format] 默认值为null,用于格式化X轴label文字,
* [denominator] 除数,作为计算正确率使用的分母,
* 返回值是直接可以用在BarChart中的[BarData]。
*/

KDoc 非常强大,你可以使用 **

在注释块中写示例代码,或者JSON格式
1
2
3


例如:

kotlin复制代码/**

  • 使用json填充视图的默认实现,必须遵循下面的数据格式

  • 1
    2
    3
    4
    5
    6
    *  [
    * {"index":0,"answer":["对"]},
    * {"index":1,"answer":["错"]},
    * {"index":2,"answer":["对"]},
    * ]
    *
  • [result] 必须是一个JSONArray字符串

  • /

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

在AS中他会被折叠成非常美观的注释块:


![image.png](https://gitee.com/songjianzaina/juejin_p3/raw/master/img/4728596a5d0539b2abe478df068579480806e4437c56deb6d3db88f0c6e98d50)




---




---


写在最后
----


文章最后我们看一段 ”Java“ 代码与 Kotlin 代码的对比吧:

kotlin复制代码// before
override fun onResponse(
call: Call<AvatarPathResult?>,
response: Response<AvatarPathResult?>
) {
val avatarPathResult = response.body()
if (avatarPathResult != null) {
val status = avatarPathResult.status
if (status == 200) {
val data = avatarPathResult.data
MMKVUtils.saveAvatarPath(data)
} else {
MMKVUtils.saveAvatarPath(“”)
}
} else {
MMKVUtils.saveAvatarPath(“”)
}
}

// after
override fun onResponse(
call: Call<AvatarPathResult?>,
response: Response<AvatarPathResult?>,
) {
with(response.body()) {
MMKVUtils.saveAvatarPath(this?.data?.takeIf { status == 200 } ?: “”)
}
}


鉴于有些同学对本文的观点有一些疑惑,这里我贴上 JetBrains 官方开发的 [Ktor](https://github.com/ktorio/ktor) 项目中对各种语法糖使用的统计(基于 main 分支,23-6-9)




| 语句 | 计数 | 备注 |
| --- | --- | --- |
| `if.*!= null` | 331 | 非空判断 |
| `if.*== null` | 216 | 空判断 |
| `.let {}` | **1210** | let作用域 |
| `?.let {}` | 441 | ?非空 |
| `.apply {}` | 469 | apply作用域 |
| `?.apply {}` | 11 | ?非空 |
| `run {}` | 37 | run作用域 |
| `with\(.*\) \{` | 219 | with作用域 |
| `.also{}` | 119 | also作用域 |
| `?:` | **1066** | Elvis |
| `?.` | **1239** | ?.非空调用 |
| `\?\..*\?\.` | **134** | ?.单行使用两次(非空传递) |
| `.takeIf` | 54 | 链式判断 |
| `\.takeIf.*\?:` | **13** | 链式判断配合Elvis |
| `.takeUnless` | 2 | 链式判断(很少用) |


这个项目可以说很能代表 JetBrains 官方对 Kotlin 语法的一些看法与标准了吧,前文我们也说了,如何取舍只关乎编码风格,而无关对错之分。


用 Java 风格是错的吗?那自然不是,只是显然空判断与安全调用两者相比,安全调用更符合 Kotlin 的风格。


重复的写对象名是错误的么?自然也不是,只是使用 `apply` 更优雅更 Kotlin。



**本文转载自:** [掘金](https://juejin.cn/post/7242198986261135421)

*[开发者博客 – 和开发相关的 这里全都有](https://dev.newban.cn/)*

【干货】使用Canal 解决数据同步场景中的疑难杂症!!!

发表于 2023-06-04

觉得不错请按下图操作,掘友们,哈哈哈!!!
image.png

一:Canal 官网介绍

canal :译意为水道/管道/沟渠,主要用途是基于 MySQL 数据库增量日志解析,提供增量数据订阅和消费

早期阿里巴巴因为杭州和美国双机房部署,存在跨机房同步的业务需求,实现方式主要是基于业务 trigger 获取增量变更。从 2010 年开始,业务逐步尝试数据库日志解析获取增量变更进行同步,由此衍生出了大量的数据库增量订阅和消费业务。

基于日志增量订阅和消费的业务包括

  • 数据库镜像
  • 数据库实时备份
  • 索引构建和实时维护(拆分异构索引、倒排索引等)
  • 业务 cache 刷新
  • 带业务逻辑的增量数据处理

当前的 canal 支持源端 MySQL 版本包括 5.1.x , 5.5.x , 5.6.x , 5.7.x , 8.0.x

二:工作原理

使用Canal 首先我们要了解MySql主从复制的工作原理。

2.1 MySQL主从复制原理

image.png

  • MySQL master 将数据变更写入二进制日志( binary log, 其中记录叫做二进制日志事件binary log events,可以通过 show binlog events 进行查看)
  • MySQL slave 将 master 的 binary log events 拷贝到它的中继日志(relay log)
  • MySQL slave 重放 relay log 中事件,将数据变更反映它自己的数据

2.2 canal 工作原理

image.png

  • canal 模拟 MySQL slave 的交互协议,伪装自己为 MySQL slave ,向 MySQL master 发送dump 协议
  • MySQL master 收到 dump 请求,开始推送 binary log 给 slave (即 canal )
  • canal 解析 binary log 对象(原始为 byte 流)

三:canal 安装使用

3.1 canal 简介

image.png

去官网下载页面进行下载:github.com/alibaba/can…

我这里下载的是1.1.6的版本:

canal 对应包的下载和装置的教程,都间接看 canal官网github,安装包目前有三兄弟:

  • canal deployer:又称 canal server,是真正监听 mysql 日志的服务端。
  • canal adapter:顾名思义“适配器”,搭配 canal server,目前能实现mysql 数据到 hbase、rdb、es的增量同步,妥妥的 ETL 工具。
  • canal admin:也是为 canal server 服务的,为canal提供整体配置管理、节点运维等面向运维的性能,提供绝对敌对的WebUI操作界面。如果 canal server 要搭建集群环境,必少不了 canal admin 这样业余的运维工具。

3.2 mysql 相关配置

1. MySql 相关配置

  • 安装canal前我们先开启MySql的 binlog,在MySQL配置文件my.cnf设置如下信息:
1
2
3
4
5
6
7
ini复制代码[mysqld] 
# 打开binlog
log-bin=mysql-bin
# 选择ROW(行)模式
binlog-format=ROW
# 配置MySQL replaction需要定义,不要和canal的slaveId重复
server_id=1
  • 改了配置文件之后,重启MySQL,使用命令查看是否打开binlog模式:

image.png

查看binlog日志文件列表:

image.png

查看当前正在写入的binlog文件:

image.png

MySQL服务器这边配置就完成。

  • 在mysql中给canal单独建一个用户,给全库全表的读,拷贝,复制的权限
1
2
3
4
5
6
sql复制代码-- 使用命令登录:
mysql -u root -p
-- 创建用户 用户名:canal 密码:Canal@123456
create user 'canal'@'%' identified by 'Canal@123456';
-- 授权 *.*表示所有库
grant SELECT, REPLICATION SLAVE, REPLICATION CLIENT on *.* to 'canal'@'%' identified by 'Canal@123456';

3.3 安装canal

3.3.1 canal.deployer

解压canal.deployer-1.1.6.tar.gz,我们可以看到里面有四个文件夹:

image.png

接着打开配置文件conf/example/instance.properties,配置信息如下:

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
ini复制代码## mysql serverId , v1.0.26+ will autoGen
## v1.0.26版本后会自动生成slaveId,所以可以不用配置
# canal.instance.mysql.slaveId=0

# 数据库地址
canal.instance.master.address=127.0.0.1:3306
# binlog日志名称
canal.instance.master.journal.name=mysql-bin.000001
# mysql主库链接时起始的binlog偏移量
canal.instance.master.position=154
# mysql主库链接时起始的binlog的时间戳
canal.instance.master.timestamp=
canal.instance.master.gtid=

# username/password
# 在MySQL服务器授权的账号密码
canal.instance.dbUsername=canal
canal.instance.dbPassword=Canal@123456
# 字符集
canal.instance.connectionCharset = UTF-8
# enable druid Decrypt database password
canal.instance.enableDruid=false

# table regex .*\..*表示监听所有表 也可以写具体的表名,用,隔开
canal.instance.filter.regex=.*\..*
# mysql 数据解析表的黑名单,多个表用,隔开
canal.instance.filter.black.regex=

我这里用的是家里的win10系统,公司的mac没遇到这个问题,所以在bin目录下找到startup.bat启动:

但是启动就报错,要踩坑了???

image.png

后来修改一下启动的脚本startup.bat解决了:

image.png

然后再启动脚本:

image.png

这就启动成功了。

3.3.2 canal adapter

作用:

  1. 对接上游消息,包括kafka、rocketmq、canal-server
  2. 实现mysql数据的增量同步
  3. 实现mysql数据的全量同步
  4. 下游写入支持mysql、es、hbase等

它既然是适配器,那么就得介绍“源头”和“指标”这两个部位数据的对接:

  • 源头:
  • (1)canal adapter 能够直连 canal server ,生产 instance的数据;
  • (2)也能够在让 canal server 将数据投递到 MQ,而后 cancal adapter 生产 MQ 中的数据。
  • 指标:对接上游消息,包括kafka、rocketmq、canal-server 实现mysql数据的增量同步 实现mysql数据的全量同步 下游写入支持mysql、es、hbase

目前 adapter 是支持动态配置的,也就是说修改配置文件后无需重启,任务会自动刷新配置!

(1) 修改application.yml
执行 vim conf/application.yml 修改consumerProperties、srcDataSources、canalAdapters的配置

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
yaml复制代码canal.conf:
mode: tcp # kafka rocketMQ # canal client的模式: tcp kafka rocketMQ
flatMessage: true # 扁平message开关, 是否以json字符串形式投递数据, 仅在kafka/rocketMQ模式下有效
syncBatchSize: 1000 # 每次同步的批数量
retries: 0 # 重试次数, -1为无限重试
timeout: # 同步超时时间, 单位毫秒
consumerProperties:
canal.tcp.server.host: # 对应单机模式下的canal
canal.tcp.zookeeper.hosts: 127.0.0.1:2181 # 对应集群模式下的zk地址, 如果配置了canal.tcp.server.host, 则以canal.tcp.server.host为准
canal.tcp.batch.size: 500 # tcp每次拉取消息的数量
srcDataSources: # 源数据库
defaultDS: # 自定义名称
url: jdbc:mysql://127.0.0.1:3306/mytest?useUnicode=true # jdbc url
username: root # jdbc 账号
password: 123 # jdbc 密码
canalAdapters: # 适配器列表
- instance: example # canal 实例名或者 MQ topic 名
groups: # 分组列表
- groupId: g1 # 分组id, 如果是MQ模式将用到该值
outerAdapters: # 分组内适配器列表
- name: es7 # es7适配器
mode: rest # transport or rest
hosts: 127.0.0.1:9200 # es地址
security.auth: test:123456 # 访问es的认证信息,如没有则不需要填
cluster.name: my-es # 集群名称,transport模式必需配置
......
  1. 一份数据可以被多个group同时消费, 多个group之间会是一个并行执行, 一个group内部是一个串行执行多个outerAdapters, 比如例子中logger和hbase
  2. 目前client adapter数据订阅的方式支持两种,直连canal server 或者 订阅kafka/RocketMQ的消息

(2) conf/es7目录下新增映射配置文件

adapter将会自动加载 conf/es7 下的所有 .yml 结尾的配置文件

新增表映射的配置文件,如 sys_user.yml 内容如下:

1
2
3
4
5
6
7
8
9
10
yaml复制代码dataSourceKey: defaultDS
destination: example
groupId: g1
esMapping:
_index: sys_goods
_id: id
upsert: true
sql: "select id, goodsname, price from sys_goods"
etlCondition: "where update_time>={}"
commitBatch: 3000
  • dataSourceKey 配置 application.yml 里 srcDataSources 的值
  • destination 配置 canal.deployer 的 Instance 名
  • groupId 配置 application.yml 里 canalAdapters.groups 的值
  • _index 配置索引名
  • _id 配置主键对应的字段
  • upsert 是否更新
  • sql 映射sql
  • etlCondition etl 的条件参数,全量同步时可以使用
  • commitBatch 提交批大小

sql映射支持多表关联自由组合, 但是有一定的限制:

  1. 主表不能为子查询语句
  2. 只能使用left outer join即最左表一定要是主表
  3. 关联从表如果是子查询不能有多张表
  4. 主sql中不能有where查询条件(从表子查询中可以有where条件但是不推荐, 可能会造成数据同步的不一致, 比如修改了where条件中的字段内容)
  5. 关联条件只允许主外键的’=’操作不能出现其他常量判断比如: on a.role_id=b.id and b.statues=1
  6. 关联条件必须要有一个字段出现在主查询语句中比如: on a.role_id=b.id 其中的 a.role_id 或者 b.id 必须出现在主select语句中

Elastic Search的mapping 属性与sql的查询值将一一对应(不支持 select *), 比如: select a.id as _id, a.name, a.email as _email from user, 其中name将映射到es mapping的name field, _email将 映射到mapping的_email field, 这里以别名(如果有别名)作为最终的映射字段. 这里的_id可以填写到配置文件的 _id: _id映射

四: 项目实战

4.1 项目使用场景

在现有演出业务中,我们有这样一个场景,管理员在后台管理系统中可以对先有演出项目,场次,售卖时间,票种等信息进行修改,比如原先我有一个演出项目 “周杰伦演唱会”,这时候用户在C端就可以看到名称就是“周杰伦演唱会”,但是我觉的这个项目的名字不够大气,这时候我在后台改了叫 “周杰伦嘉年华演唱会”,在后台更改后首先项目不会立即生效,而是要通过一个发布的过程C端才能看到更改后的。
那么由于C端查询量巨,特别是在这种大型演唱会的时候,面对这么大的流量我们怎么去设计呢???
所以这就是我们使用的场景。

4.2 设计对比

  • 流程上涉及的模型直接添加Redis 缓存,在业务侧组装查询逻辑
    • 这是我们原先的使用方案,但是对Redis 依赖很大,而且在高峰期的时候出现过缓存击穿的问题
    • 由于查询逻辑复杂,业务侧能实现查询业务的组装,但是在查询高峰期内存消耗极高,效率也不是很高。
  • 数据库数据更改后在业务层再去更新ES,ES提供对外的查询能力
    • 这就是上边方案的变种,ES有天然的查询优势,并且能应对复杂的查询条件,所以在上边很多业务组装的复杂查询此时都可以用ES来实现。但是数据同步ES的地方就比较分散,每个更改表的地方都要考虑,如果同步的表比较多就很难维护了。
  • 使用Canal 监听数据库binary log,同步到Kafka,专门的服务消费kafka消息,同步ES
    • 第三种方案,需要进入中间件,canal和kafka,但是这个方案只会在服务搭建的时候比较复杂一些,属于一劳永逸那种,canal 我们监听数据库 binary log ,然后将 binary log 同步到kafka中,新增消费服务读取kafka 中 binary log,处理ES逻辑。这样业务方可以提各种各样的查询需求,我们只需要在查询服务中利用ES进行组装即可。

4.3 应用:

在我们的项目中,数据流向包含:腾讯云订阅服务 -> canal adapter -> es 。

只使用canal的客户端功能也就是canal adapter,因为我们我们数据库是使用的腾讯云Mql,腾讯云提供了数据订阅功能。

image.png

image.png

image.png

在这里说明下,实际上腾讯云,阿里云都提供了这样的服务,他们对数据库指定的表监听binglog,然后写入到kakaka中,比如上边的截图。

4.4 问题以及解决方案

问题1:监听多张表怎么保证数据消息不乱?

image.png

就像这个截图,我们可以按照表名进行分区,也就是说,同一张表数据的变更只会在一个partion上,这样表变更的消息在一个partion 上就变得有序了。

问题2:如果数据库中数据和ES中的数据对不上怎么处理?

目前我们是提供一个job,每十分钟跑一次去对比数据库中有效数据的数量和ES中的数据数量,但是Job很慢,可以考虑,从数据库捞数据,分批跑(shard的方式,先取比如订单号是1的,后续一直累加到9),如果有多台机器就可以分布式跑,每台下放shardIndex ,数据库sql就捞 orderNum%10 = shardIndex的数据,这样也不会跑重,处理的时候再用多线程。

问题3: 怎么提高处理速度以及效率
在摸索中我们采用了一种方法:就是对消息批量处理,我们从消息中心拿消息的时候批量去拿,举个例子吧,goods 表对应的消息现在有1000条,orderId =100 的数据在占了100个,分布在不同的offset中,如果我每次都是拿一条消息去消费一次这样重复的逻辑我要跑100次,但是们可以批量拿200条消息,然后再对消息中的OrderId进行分组,得到最终要执行的OrderId这样是不是就可以少跑很多次。拿到要执行的OrderId后我们就可以找到对应的Sql得到最后的Select结果(这个地方有个点,我不关心你是什么操作【DML】,我只要操作的数据ID,然后执行我的Select sql 去查询最后的结果,然后再去同步到ES中),处理的过程中我们也可以使用多线程来加快处理速度,这都是一些优化的点。

【干货】常见库存设计方案-各种方案对比总有一个适合你

JYM来一篇消息队列-Kafka的干货吧!!!

设计模式:

JYM 设计模式系列- 单例模式,适配器模式,让你的代码更优雅!!!

JYM 设计模式系列- 责任链模式,装饰模式,让你的代码更优雅!!!

JYM 设计模式系列- 策略模式,模板方法模式,让你的代码更优雅!!!

JYM 设计模式系列-工厂模式,让你的代码更优雅!!!

Spring相关:

Spring源码解析-老生常谈Bean ⽣命周期 ,但这个你值得看!!!

Spring 源码解析-JYM你值得拥有,从源码角度看bean的循环依赖!!!

Spring源码解析-Spring 事务

本文正在参加「金石计划」

本文转载自: 掘金

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

JYM 设计模式系列- 单例模式,适配器模式,让你的代码更优

发表于 2023-05-31

觉得不错请按下图操作,掘友们,哈哈哈!!!
image.png

概述设计模式分类

总体来说设计模式分为三大类:

创建型模式,共五种:工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式。

结构型模式,共七种:适配器模式、装饰器模式、代理模式、外观模式、桥接模式、组合模式、享元模式。

行为型模式,共十一种:策略模式、模板方法模式、观察者模式、迭代子模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式。

一:单例模式

1.1 名词解释

简单来说单例模式是指在内存中只会创建且仅创建一次对象的设计模式。在程序中多次使用同一个对象且作用相同时,为了防止频繁地创建对象使得内存飙升,单例模式可以让程序仅在内存中创建一个对象,让所有需要调用的地方都共享这一单例对象。

1.2 优缺点

单例模式的优点:

  • 单例模式可以保证内存里只有一个实例,减少了内存的开销。
  • 可以避免对资源的多重占用。
  • 单例模式设置全局访问点,可以优化和共享资源的访问。

单例模式的缺点:

  • 单例模式一般没有接口,扩展困难。如果要扩展,则除了修改原来的代码,没有第二种途径,违背开闭原则。
  • 在并发测试中,单例模式不利于代码调试。在调试过程中,如果单例中的代码没有执行完,也不能模拟生成一个新的对象。
  • 单例模式的功能代码通常写在一个类中,如果功能设计不合理,则很容易违背单一职责原则。

1.3 适用场景

在 Java中,单例模式可以保证在一个 JVM 中只存在单一实例。单例模式的应用场景主要有以下几个方面。

  • 需要频繁创建的一些类,使用单例可以降低系统的内存压力,减少 GC。
  • 某类只要求生成一个对象的时候。
  • 某些类创建实例时占用资源较多,或实例化耗时较长,且经常使用。
  • 某类需要频繁实例化,而创建的对象又频繁被销毁的时候,如多线程的线程池、网络连接池等。
  • 频繁访问数据库或文件的对象。
  • 对于一些控制硬件级别的操作,或者从系统上来讲应当是单一控制逻辑的操作,如果有多个实例,则系统会完全乱套。
  • 当对象需要被共享的场合。由于单例模式只允许创建一个对象,共享该对象可以节省内存,并加快对象访问速度。如 Web 中的配置对象、数据库的连接池等。

1.4 实现方式

好了,既然我们已经知道什么是单例,以及单例模式的优缺点,那我们接下来继续讨论下怎么实现单例。

一般来说,我们可以把单例分为行为上的单例和管理上的单例。行为上的单例代表不管如何操作,在jvm层面上都只有一个类的实例,而管理上的单例则可以理解为:不管谁去使用这个类,都要守一定的规矩,比方说,我们使用某个类,只能从指定的地方’去拿‘,这样拿到就是同一个类了。

而对于管理上的单例,相信大家最为熟悉的就是spring了,spring将所有的类放到一个容器中,以后使用该类都从该容器去取,这样就保证了单例。

所以这里我们剩下的就是接着来谈谈如何实现行为上的单例了。一般来说,这种单例实现有两种思路,私有构造器,枚举。

单例模式有两种类型:

  • 懒汉式:在真正需要使用对象的时候才会去创建该对象;
  • 饿汉式:在类加载时创建好该单例对象,等待被程序使用。

1.5 上demo

image.png

饿汉模式:

IvoryTower :单例类。初始化的静态实例保证线程安全

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
arduino复制代码public final class IvoryTower {

/**
* 私有构造函数,因此没有人可以实例化该类.
*/
private IvoryTower() {
}

/**
* 静态到类的类实例.
*/
private static final IvoryTower INSTANCE = new IvoryTower();

/**
* 被用户调用以获取类的实例.
*
* @return instance of the singleton.
*/
public static IvoryTower getInstance() {
return INSTANCE;
}
}

懒汉模式初始化单例:

ThreadSafeLazyLoadedIvoryTower:线程安全的单例类。实例被惰性初始化,因此需要同步机制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
csharp复制代码public final class ThreadSafeLazyLoadedIvoryTower {

private static volatile ThreadSafeLazyLoadedIvoryTower instance;

private ThreadSafeLazyLoadedIvoryTower() {
// 通过反射,防止实例化化
if (instance != null) {
throw new IllegalStateException("Already initialized.");
}
}

/**
* 在第一次调用方法之前不会创建实例
*/
public static synchronized ThreadSafeLazyLoadedIvoryTower getInstance() {
if (instance == null) {
instance = new ThreadSafeLazyLoadedIvoryTower();
}
return instance;
}
}

是按需初始化的单例实现。缺点是访问速度很慢,因为整个访问方法是同步的

通过枚举实现单例:

EnumIvoryTower: 此实现是线程安全的,但是添加任何其他方法及其线程安全是开发所做的

1
2
3
4
5
6
7
8
9
typescript复制代码public enum EnumIvoryTower {

INSTANCE;

@Override
public String toString() {
return getDeclaringClass().getCanonicalName() + "@" + hashCode();
}
}

双重检查锁定实现:

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
csharp复制代码public final class ThreadSafeDoubleCheckLocking {

private static volatile ThreadSafeDoubleCheckLocking instance;

/**
* 私有构造函数以防止客户端实例化.
*/
private ThreadSafeDoubleCheckLocking() {
// to prevent instantiating by Reflection call
if (instance != null) {
throw new IllegalStateException("Already initialized.");
}
}

/**
* 公共访问器
*
* @return an instance of the class.
*/
public static ThreadSafeDoubleCheckLocking getInstance() {
// 局部变量将性能提高 25%
// Joshua Bloch “Effective Java,第二版”,p. 283-284

var result = instance;
// 检查单例实例是否初始化.
// 如果它已初始化,那么我们可以返回实例。
if (result == null) {
// 它没有初始化,但我们不能确定,因为其他线程可能有
// 同时初始化它.
// 所以为了确保我们需要锁定一个对象以获得互斥.
synchronized (ThreadSafeDoubleCheckLocking.class) {
//再次将实例分配给局部变量以检查它是否由其他地方初始化
//当前线程被阻塞进入锁定区时其他线程
// 如果它已被初始化,那么我们可以返回之前创建的实例
// 就像之前的空检查一样。
result = instance;
if (result == null) {
// 该实例仍未初始化,因此我们可以安全地创建实例
// (没有其他线程可以进入这个区域)
// 创建一个实例并使其成为我们的单例实例.
instance = result = new ThreadSafeDoubleCheckLocking();
}
}
}
return result;
}
}

它比 ThreadSafeLazyLoadedIvoryTower 快一些,因为它不同步整个访问方法,而只同步特定条件下的方法内部。

按需初始化 holder:

可以找到另一种实现线程安全的延迟初始化单例的方法。但是,此实现至少需要 Java 8 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
arduino复制代码public final class InitializingOnDemandHolderIdiom {

/**
* 私有构造函数.
*/
private InitializingOnDemandHolderIdiom() {
}

/**
* 单例实例.
*
* @return Singleton instance
*/
public static InitializingOnDemandHolderIdiom getInstance() {
return HelperHolder.INSTANCE;
}

/**
* 提供延迟加载的 Singleton 实例.
*/
private static class HelperHolder {
private static final InitializingOnDemandHolderIdiom INSTANCE =
new InitializingOnDemandHolderIdiom();
}
}

Initialize-on-demand-holder 习惯用法是在 Java 中创建惰性初始化单例对象的安全方法;该技术尽可能惰性,适用于所有已知的 Java 版本。它利用 关于类初始化的语言保证,因此可以在所有兼容 Java 的编译器和虚拟机中正常工作;部类的引用时间不早于调用 getInstance() 的时间(因此类加载器加载时间也不早)。因此,此解决方案是线程安全的,不需要需要特殊的语言结构。

程序入口:

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
java复制代码@Slf4j
public class App {


public static void main(String[] args) {

// 饿汉模式
var ivoryTower1 = IvoryTower.getInstance();
var ivoryTower2 = IvoryTower.getInstance();
LOGGER.info("ivoryTower1={}", ivoryTower1);
LOGGER.info("ivoryTower2={}", ivoryTower2);

// 懒汉模式
var threadSafeIvoryTower1 = ThreadSafeLazyLoadedIvoryTower.getInstance();
var threadSafeIvoryTower2 = ThreadSafeLazyLoadedIvoryTower.getInstance();
LOGGER.info("threadSafeIvoryTower1={}", threadSafeIvoryTower1);
LOGGER.info("threadSafeIvoryTower2={}", threadSafeIvoryTower2);

// 枚举单例
var enumIvoryTower1 = EnumIvoryTower.INSTANCE;
var enumIvoryTower2 = EnumIvoryTower.INSTANCE;
LOGGER.info("enumIvoryTower1={}", enumIvoryTower1);
LOGGER.info("enumIvoryTower2={}", enumIvoryTower2);

// 双重检查锁定实现
var dcl1 = ThreadSafeDoubleCheckLocking.getInstance();
LOGGER.info(dcl1.toString());
var dcl2 = ThreadSafeDoubleCheckLocking.getInstance();
LOGGER.info(dcl2.toString());

// 按需初始化 holder
var demandHolderIdiom = InitializingOnDemandHolderIdiom.getInstance();
LOGGER.info(demandHolderIdiom.toString());
var demandHolderIdiom2 = InitializingOnDemandHolderIdiom.getInstance();
LOGGER.info(demandHolderIdiom2.toString());
}
}

二:适配器模式

2.1 名词解释

适配器模式属于“结构型模式”的一种。该模式的核心思想就是将类中原本不适合当前客户端使用的接口转换成适用的接口,从而大大提高程序的兼容性。并且从使用者的角度是看不到被适配的类地,也降低了代码之间的耦合性。适配器模式的工作原理就是:用户调用适配器转换出来的接口,适配器在调用被适配类的相关接口,从而完成适配。

2.2 优缺点

优点:

  • 客户端通过适配器可以透明地调用目标接口。
  • 复用了现存的类,程序员不需要修改原有代码而重用现有的适配者类。
  • 将目标类和适配者类解耦,解决了目标类和适配者类接口不一致的问题。
  • 在很多业务场景中符合开闭原则。

缺点:

  • 适配器编写过程需要结合业务场景全面考虑,可能会增加系统的复杂性。
  • 增加代码阅读难度,降低代码可读性,过多使用适配器会使系统代码变得凌乱

2.3 适用场景

  • 封装有缺陷的接口设计: 假设我们依赖的外部系统在接口设计方面有缺陷(比如包含大量静态方法),引入之后会影响到我们自身代码的可测试性。为了隔离设计上的缺陷,我们希望对外部系统提供的接口进行二次封装,抽象出更好的接口设计,这个时候就可以使用适配器模式了。
  • 统一多个类的接口设计: 某个功能的实现依赖多个外部系统(或者说类)。通过适配器模式,将它们的接口适配为统一的接口定义,然后我们就可以使用多态的特性来复用代码逻辑。
  • 替换依赖的外部系统: 当我们把项目中依赖的一个外部系统替换为另一个外部系统的时候,利用适配器模式,可以减少对代码的改动。
  • 兼容老版本接口: 在做版本升级的时候,对于一些要废弃的接口,我们不直接将其删除,而是暂时保留,并且标注为 deprecated,并将内部实现逻辑委托为新的接口实现。这样做的好处是,让使用它的项目有个过渡期,而不是强制进行代码修改。这也可以粗略地看作适配器模式的一个应用场景。
  • 适配不同格式的数据: 适配器模式主要用于接口的适配,实际上,它还可以用在不同格式的数据之间的适配。比如,把从不同征信系统拉取的不同格式的征信数据,统一为相同的格式,以方便存储和使用。再比如,Java 中的 Arrays.asList() 也可以看作一种数据适配器,将数组类型的数据转化为集合容器类型。

2.4 实现方式

通过不同的实现方式,我们可以将其分成三类:
类适配器模式,对象适配器模式,接口适配器模式。

适配器模式(Adapter)包含以下主要角色。

  • 目标(Target)接口:当前系统业务所期待的接口,它可以是抽象类或接口。
  • 适配者(Adaptee)类:它是被访问和适配的现存组件库中的组件接口。
  • 适配器(Adapter)类:它是一个转换器,通过继承或引用适配者的对象,把适配者接口转换成目标接口,让客户按目标接口的格式访问适配者。

2.5 上demo

这个故事是这样的。 海盗来了!我们需要 RowingBoat(划艇) 来逃跑!我们有一艘 FishingBoat(渔船)和我们的船长。我们没有时间去补新船!我们需要重用这个FishingBoat(渔船)。船长需要一艘他可以操作的划艇。规范在 RowingBoat(划艇) 中。我们将使用适配器模式来重用它。

image.png

RowingBoat:客户所期望的接口,一艘可以开的划艇

1
2
3
4
5
csharp复制代码public interface RowingBoat {

void row();

}

Captain:船长使用 RowingBoat 航行。 这是在这个模式中的客户

1
2
3
4
5
6
7
8
9
10
11
12
less复制代码@Setter
@NoArgsConstructor
@AllArgsConstructor
public final class Captain {

private RowingBoat rowingBoat;

void row() {
rowingBoat.row();
}

}

FishingBoat:设备类(模式中的适配器)。我们想重用这个类用渔船航行。

1
2
3
4
5
6
7
8
java复制代码@Slf4j
final class FishingBoat {

void sail() {
LOGGER.info("The fishing boat is sailing");
}

}

FishingBoatAdapter: 适配器类。将设备接口 FishingBoat 调整为 RowingBoat 客户所期望的接口

1
2
3
4
5
6
7
8
java复制代码public class FishingBoatAdapter implements RowingBoat {

private final FishingBoat boat = new FishingBoat();

public final void row() {
boat.sail();
}
}

App:程序入口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
arduino复制代码public final class App {

private App() {
}

/**
* 程序入口.
*
* @param args command line args
*/
public static void main(final String[] args) {
// 船长只能操作划艇,但通过适配器他能够
// 也使用渔船
var captain = new Captain(new FishingBoatAdapter());
captain.row();
}
}

本文正在参加「金石计划」

本文转载自: 掘金

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

JYM来一篇消息队列-Kafka的干货吧!!! 一:先来了解

发表于 2023-05-30

image.png

一:先来了解下概念

Kafka 是一种高吞吐量、分布式、基于发布/订阅的消息系统,最初由 LinkedIn 公司开发,使用 Scala 语言编写,目前是 Apache 的开源项目,也是目前较为主流的消息列队。作用吗和其他消息队列相识,主要用于 【异步】,【解耦】,【削峰】。

二:Kafka组成

  • Producer: 消息生产者,向 Kafka Broker 发消息的客户端。
  • Consumer: 消息消费者,从 Kafka Broker 取消息的客户端。
  • Consumer Group: 消费者组(CG),消费者组内每个消费者负责消费不同分区的数据,提高消费能力。一个分区只能由组内一个消费者消费,消费者组之间互不影响。所有的消费者都属于某个消费者组,即消费者组是逻辑上的一个订阅者。
  • Broker: 一台 Kafka 机器就是一个 broker。一个集群由多个 broker 组成。一个 broker 可以容纳多个 topic。
  • Topic: 可以理解为一个队列,topic 将消息分类,生产者和消费者面向的是同一个 topic。
  • Partition: 为了实现扩展性,提高并发能力,一个非常大的 topic 可以分布到多个 broker (即服务器)上,一个 topic 可以分为多个 partition,每个 partition 是一个 有序的队列。
  • Replica: 副本,为实现备份的功能,保证集群中的某个节点发生故障时,该节点上的 partition 数据不丢失,且 Kafka 仍然能够继续工作,Kafka 提供了副本机制,一个 topic 的每个分区都有若干个副本,一个 leader 和若干个 follower。
  • Leader: 每个分区多个副本的“主”副本,生产者发送数据的对象,以及消费者消费数据的对象,都是 leader。
  • Follower: 每个分区多个副本的“从”副本,实时从 leader 中同步数据,保持和 leader 数据的同步。leader 发生故障时,某个 follower 还会成为新的 leader。
  • offset: 消费者消费的位置信息,监控数据消费到什么位置,当消费者挂掉再重新恢复的时候,可以从消费位置继续消费。
  • Zookeeper: Kafka 集群能够正常工作,需要依赖于 zookeeper,zookeeper 帮助 Kafka 存储和管理集群信息

kafka集群1.png

三:Kafka 数据存储设计

3.1. partition 的数据文件(offset,MessageSize,data)

partition 中的每条 Message 包含了以下三个属性:offset,MessageSize,data,其中 offset 表 示 Message 在这个 partition 中的偏移量,offset 不是该 Message 在 partition 数据文件中的实 13/04/2018 Page 176 of 283 际存储位置,而是逻辑上一个值,它唯一确定了 partition 中的一条 Message,可以认为 offset 是 partition 中 Message 的 id;MessageSize 表示消息内容 data 的大小;data 为 Message 的具 体内容。

3.2 数据文件分段 segment(顺序读写、分段命令、二分查找)

partition 物理上由多个 segment 文件组成,每个 segment 大小相等,顺序读写。每个 segment 数据文件以该段中最小的 offset 命名,文件扩展名为.log。这样在查找指定 offset 的 Message 的 时候,用二分查找就可以定位到该 Message 在哪个 segment 数据文件中。

3.3 数据文件索引(分段索引、稀疏存储)

Kafka 为每个分段后的数据文件建立了索引文件,文件名与数据文件的名字是一样的,只是文件扩 展名为.index。index 文件中并没有为数据文件中的每条 Message 建立索引,而是采用了稀疏存 储的方式,每隔一定字节的数据建立一条索引。这样避免了索引文件占用过多的空间,从而可以 将索引文件保留在内存中。

image.png

四:生产者设计

4.1 负载均衡(partition 会均衡分布到不同 broker 上)

由于消息 topic 由多个 partition 组成,且 partition 会均衡分布到不同 broker 上,因此,为了有 效利用 broker 集群的性能,提高消息的吞吐量,producer 可以通过随机或者 hash 等方式,将消 息平均发送到多个 partition 上,以实现负载均衡。

image.png

4.2 批量发送

是提高消息吞吐量重要的方式,Producer 端可以在内存中合并多条消息后,以一次请求的方式发 送了批量的消息给 broker,从而大大减少 broker 存储消息的 IO 操作次数。但也一定程度上影响 了消息的实时性,相当于以时延代价,换取更好的吞吐量。

4.3 压缩(GZIP 或 Snappy)

Producer 端可以通过 GZIP 或 Snappy 格式对消息集合进行压缩。Producer 端进行压缩之后,在 Consumer 端需进行解压。压缩的好处就是减少传输的数据量,减轻对网络传输的压力,在对大 数据处理上,瓶颈往往体现在网络上而不是 CPU(压缩和解压会耗掉部分 CPU 资源)。

五:消费者设计

image.png

5.1 Consumer Group

同一 Consumer Group 中的多个 Consumer 实例,不同时消费同一个 partition,等效于队列模 式。partition 内消息是有序的,Consumer 通过 pull 方式消费消息。Kafka 不删除已消费的消息 对于 partition,顺序读写磁盘数据,以时间复杂度 O(1)方式提供消息持久化能力。

六:数据可靠性

6.1 ack机制

为保证producer 发送的数据不丢失,broker 接收到数据后都需要对producer发送ack(确认接收) ,如果producer 未收到ack则会重新发送该条消息。producer 的 ack 策略又分为三种:

  • ack=0 producer不等待broker同步完成的确认,继续发送下一条(批)信息
  • ack=1 producer要等待leader成功收到数据并得到确认,才发送下一条message。
  • ack=-1 producer得到follwer确认(全副本同步完成),才发送下一条数据

6.2 ISR (同步副本表)

采用全副本同步完成再ack会有一个问题:

当leader 接收完数据,所有的follower开始同步数据,但一旦有一个follower不能与leader进行同步,那leader会一直等下去,这样会非常的浪费时间。

为此kafka引入了 isr 机制——leader会维护一个动态的 isr(in-sync replica set)列表,这个列表维护了和leader保持同步的集合。当ISR中的follower完成数据的同步之后,leader就会发送ack。如果follower 长时间未向leader同步数据,则该follower将会被踢出 isr,当其他满足条件的follower也会被加入到isr。这个同步最大时间配置项为replica.lag.time.max.ms 参数设置。如果leader故障了,也会从isr的follower中选举新的leader。

6.3 数据一致性解决

因为副本的消息数在各个之间是存在差异的,可能leader10条,而follower只同步了8条;当leader挂掉,数据就有可能会发生丢失,通过一种机制来保证消费者消费数据的一致性就很有必要了。kafka的数据一致性通过 LEO(每个副本的最后一条offset)和HW(所有的LEO中最小的那个)来保证。示意图:

image.png

消费者只能看到offset<=HW 的消息。

七:消费机制

7.1 消费策略

kafka 对消息消费的处理有三种方式:

  • (at least once)至少一次
  • (at most once)至多一次
  • (exactly once) 有且只有一次

因为ack机制的存在,producer 向kafka发送消息时如果 ack=0,由于producer不等确认消息是否投递成功就不管了 ,可能丢失数据,此时消费者最多消费一次消息;如果ack=1,当producer未收到消息确认投递成功时会再次投递,这个时候可能消息被投递了多次,可能会存在重复消费的情况。当kafka开启数据幂等性且ack=1的时候,此时重复的消息会被去重,因此不会产生重复消费的情况。

启用幂等性的方式是将producer中的参数 enable.idompotence 设置为true。

7.2 消费者相关特性

和rabbitMQ一样,可以指定消费者消费消息是推模式还是拉模式。在消费者组中,有多个消费者,一个topic中有多个partition。那么消息的分配是怎么样的呢,首先一个消费者组中的消费者不能同时消费同一个partition,这是基本原则。 然后partiotion的分配机制有两种,一种是range(范围) 一种是 RoundRobin(轮询),range示 意图:

image.png

RoundRobin 示意图:

image.png

由于consumer也可能会宕机挂掉的风险,当consumer恢复的时候必须要能够从上一次消费的地方重新开始消费。所以consumer需要实时记录自己消费到了哪一个offset,以便能够恢复到宕机前状态。

八:一些疑问

8.1 kafka高效读写保证

kafka的producer生产数据,要以追加的形式写入到log文件中,这个写磁盘的过程是顺序写,相对于磁盘的随机写来说,这个效率要高出很多,这个是kafka高效读写的保证之一。而另外的一个保证高效读写的技术是零拷贝,用过netty的同学应该知道这个,中间少了两次用户态的切换。

8.2 Kafka无丢失消息解决方案

实现Kafka无丢失消息的解决方案如下:

  • 必须使用producer.send(msg, callback)接口发送消息。
  • Producer端设置acks参数值为all。acks参数值为all表示ISR中所有Broker副本都接收到消息,消息才算已提交。
  • 设置Producer端retries参数值为一个较大值,表示Producer自动重试次数。当出现网络瞬时抖动时,消息发送可能会失败,此时Producer能够自动重试消息发送,避免消息丢失。
  • 设置Broker端unclean.leader.election.enable = false,unclean.leader.election.enable参数用于控制有资格竞选分区Leader的Broker。如果一个Broker落后原Leader太多,那么成为新Leader必然会造成消息丢失。因此,要将unclean.leader.election.enable参数设置成false。
  • 设置Broker端参数replication.factor >= 3,将消息保存多份副本。
  • 设置Broker参数min.insync.replicas > 1,保证ISR中Broker副本的最少个数,在acks=-1时才生效。设置成大于1可以提升消息持久性,生产环境中不能使用默认值 1。
  • 必须确保replication.factor > min.insync.replicas,如果两者相等,那么只要有一个副本挂机,整个分区无法正常工作。推荐设置成replication.factor = min.insync.replicas + 1。
  • 确保消息消费完成再提交。设置Consumer端参数enable.auto.commit为false,并采用手动提交位移的方式。

但是上边的一些操作很大一部分限制了kafka 本身的吞吐量。

8.3 Kafka消息重复消费问题

8.3.1 消费者消费过程解析

生产者将消息发送到Topic中,消费者即可对其进行消费,其消费过程如下:

  1. Consumer向Broker提交连接请求,其所连接上的Broker都会向其发送Broker Controller的通信URL,即配置文件中的listeners地址;
  2. 当Consumer指定了要消费的Topic后,会向Broker Controller发送消费请求;
  3. Broker Controller会为Consumer分配一个或几个Partition Leader,并将Partition的当前offset发送给Consumer;
  4. Consumer会按照Broker Controller分配的Partition对其中的消息进行消费;
  5. 当Consumer消费完消息后,Consumer会向Broker发送一个消息已经被消费反馈,即消息的offset;
  6. 在Broker接收到Consumer的offset后,会更新相应的consumer_offset中;
  7. Consumer可以重置offset,从而可以灵活消费存储在Broker上的消息。

8.3.2 重复消费解决方案

1.0 同一个Consumer重复消费

当Consumer由于消费能力低而引发了消费超时,则可能会形成重复消费。
在某数据刚好消费完毕,但正准备提交offset时,消费时间超时,则Broker认为消息未消费成功,产生重复消费问题。

2.0 其解决方案:延长offset提交时间。

不同的Consumer重复消费
当Consumer消费了消息,但还没有提交offset时宕机,则已经被消费过的消息会被重复消费。
其解决方案:将自动提交改为手动提交

从架构设计上解决Kafka重复消费的问题

  • 保存并查询
    • 给每个消息都设置一个唯一的UUID,在消费消息时,首先去持久化系统中查询,查看消息是否被消费过,如果没有消费过,再进行消费;如果已经消费过,直接丢弃。
  • 利用幂等性
    • 幂等性操作的特点是任意多次执行所产生的影响均与一次执行的影响相同。
      如果将系统消费消息的业务逻辑设计为幂等性操作,就不用担心Kafka消息的重复消费问题,因此可以将消费的业务逻辑设计成具备幂等性的操作。利用数据库的唯一约束可以实现幂等性,如在数据库中建一张表,将表的两个或多个字段联合起来创建一个唯一约束,因此只能存在一条记录。
  • 设置前提条件
    • 实现幂等性的另一种方式是给数据变更设置一个前置条件。如果满足条件就更新数据,否则拒绝更新数据,在更新数据的时候,同时变更前置条件中需要判断的数据。

8.3.3 kafka为什么这么快

  • 利用 Partition 实现并行处理 :我们都知道 Kafka 是一个 Pub-Sub 的消息系统,无论是发布还是订阅,都要指定 Topic。Topic 只是一个逻辑的概念。每个 Topic 都包含一个或多个 Partition,不同 Partition 可位于不同节点。
  • 顺序读写:因为硬盘是机械结构,每次读写都会寻址->写入,其中寻址是一个“机械动作”,它是最耗时的。所以硬盘最“讨厌”随机I/O,最喜欢顺序I/O。为了提高读写硬盘的速度,Kafka就是使用顺序I/O。
  • 充分利用 Page Cache:引入 Cache 层的目的是为了提高 Linux 操作系统对磁盘访问的性能。Cache 层在内存中缓存了磁盘上的部分数据。当数据的请求到达时,如果在 Cache 中存在该数据且是最新的,则直接将数据传递给用户程序,免除了对底层磁盘的操作,提高了性能。Cache 层也正是磁盘 IOPS 为什么能突破 200 的主要原因之一。在 Linux 的实现中,文件 Cache 分为两个层面,一是 Page Cache,另一个 Buffer Cache,每一个 Page Cache 包含若干 Buffer Cache。Page Cache 主要用来作为文件系统上的文件数据的缓存来用,尤其是针对当进程对文件有 read/write 操作的时候。Buffer Cache 则主要是设计用来在系统对块设备进行读写的时候,对块进行数据缓存的系统来使用。
  • 零拷贝技术:
    • 零拷贝(Zero-copy)技术指在计算机执行操作时,CPU 不需要先将数据从一个内存区域复制到另一个内存区域,从而可以减少上下文切换以及 CPU 的拷贝时间。它的作用是在数据报从网络设备到用户程序空间传递的过程中,减少数据拷贝次数,减少系统调用,实现 CPU 的零参与,彻底消除 CPU 在这方面的负载。
    • mmap:Memory Mapped Files:简称 mmap,也有叫 MMFile 的,使用 mmap 的目的是将内核中读缓冲区(read buffer)的地址与用户空间的缓冲区(user buffer)进行映射。从而实现内核缓冲区与应用程序内存的共享,省去了将数据从内核读缓冲区(read buffer)拷贝到用户缓冲区(user buffer)的过程。它的工作原理是直接利用操作系统的 Page 来实现文件到物理内存的直接映射。完成映射之后你对物理内存的操作会被同步到硬盘上。使用这种方式可以获取很大的 I/O 提升,省去了用户空间到内核空间复制的开销。
    • 批处理:在很多情况下,系统的瓶颈不是 CPU 或磁盘,而是网络IO。因此,除了操作系统提供的低级批处理之外,Kafka 的客户端和 broker 还会在通过网络发送数据之前,在一个批处理中累积多条记录 (包括读和写)。记录的批处理分摊了网络往返的开销,使用了更大的数据包从而提高了带宽利用率。
    • 数据压缩:Producer 可将数据压缩后发送给 broker,从而减少网络传输代价,目前支持的压缩算法有:Snappy、Gzip、LZ4。数据压缩一般都是和批处理配套使用来作为优化手段的。

总结:

  • 1.0 partition 并行处理
  • 2.0 顺序写磁盘,充分利用磁盘特性
  • 3.0 利用了现代操作系统分页存储 Page Cache 来利用内存提高 I/O 效率
  • 4.0 采用了零拷贝技术
  • 5.0 Producer 生产的数据持久化到 broker,采用 mmap 文件映射,实现顺序的快速写入
  • 6.0 Customer 从 broker 读取数据,采用 sendfile,将磁盘文件读到 OS 内核缓冲区后,转到 NIO buffer进行网络发送,减少 CPU 消耗

8.3.4 Kafka中的HW、LEO、LSO、LW等分别代表什么?

  • HW是High Watermak的缩写,俗称高水位,它表示了一个特定消息的偏移量(offset),消费之只能拉取到这个offset之前的消息。
  • LEO是Log End Offset的缩写,它表示了当前日志文件中下一条待写入消息的offset。
  • LSO特指LastStableOffset。它具体与kafka的事物有关。消费端参数——isolation.level,这个参数用来配置消费者事务的隔离级别。字符串类型,“read_uncommitted”和“read_committed”。
  • LW是Low Watermark的缩写,俗称“低水位”,代表AR集合中最小的logStartOffset值,副本的拉取请求(FetchRequest)和删除请求(DeleteRecordRequest)都可能促使LW的增长。

8.3.5 Kafka中是怎么体现消息顺序性的

  1. 一个 topic,一个 partition,一个 consumer,内部单线程消费,单线程吞吐量太低,一般不会用这个。
  2. 写 N 个内存 queue,具有相同 key 的数据都到同一个内存 queue;然后对于 N 个线程,每个线程分别消费一个内存 queue 即可,这样就能保证顺序性。

8.3.6 Kafka中的ISR、AR又代表什么?ISR的伸缩又指什么?

  1. 分区中的所有副本统称为AR(Assigned Repllicas)。所有与leader副本保持一定程度同步的副本(包括Leader)组成ISR(In-Sync Replicas),ISR集合是AR集合中的一个子集。与leader副本同步滞后过多的副本(不包括leader)副本,组成OSR(Out-Sync Relipcas),由此可见:AR=ISR+OSR。
  2. ISR集合的副本必须满足:
    副本所在节点必须维持着与zookeeper的连接;副本最后一条消息的offset与leader副本最后一条消息的offset之间的差值不能超出指定的阈值
  3. 每个分区的leader副本都会维护此分区的ISR集合,写请求首先由leader副本处理,之后follower副本会从leader副本上拉取写入的消息,这个过程会有一定的延迟,导致follower副本中保存的消息略少于leader副本,只要未超出阈值都是可以容忍的
  4. ISR的伸缩指的是Kafka在启动的时候会开启两个与ISR相关的定时任务,名称分别为“isr-expiration”和”isr-change-propagation”.。isr-expiration任务会周期性的检测每个分区是否需要缩减其ISR集合。

8.3.6 如果我指定了一个offset,Kafka怎么查找到对应的消息?

  1. 通过文件名前缀数字x找到该绝对offset 对应消息所在文件。
  2. offset-x为在文件中的相对偏移。
  3. 通过index文件中记录的索引找到最近的消息的位置。
  4. 从最近位置开始逐条寻找。

8.3.7 Kafka中的延迟队列怎么实现?

Kafka中存在大量的延迟操作,比如延迟生产、延迟拉取以及延迟删除等。Kafka并没有使用JDK自带的Timer或者DelayQueue来实现延迟的功能,而是基于时间轮自定义了一个用于实现延迟功能的定时器(SystemTimer)。JDK的Timer和DelayQueue插入和删除操作的平均时间复杂度为O(nlog(n)),并不能满足Kafka的高性能要求,而基于时间轮可以将插入和删除操作的时间复杂度都降为O(1)。Kafka中的时间轮(TimingWheel)是一个存储定时任务的环形队列,底层采用数组实现,数组中的每个元素可以存放一个定时任务列表(TimerTaskList)。TimerTaskList是一个环形的双向链表,链表中的每一项表示的都是定时任务项(TimerTaskEntry),其中封装了真正的定时任务TimerTask。时间轮由多个时间格组成,每个时间格代表当前时间轮的基本时间跨度(tickMs)。时间轮的时间格个数是固定的,可用wheelSize来表示,那么整个时间轮的总体时间跨度(interval)可以通过公式 tickMs × wheelSize计算得出。

8.3.8 消费者提交消费位移时提交的是当前消费到的最新消息的offset还是offset+1?

offset+1

8.3.9 优先副本是什么?它有什么特殊的作用?

优先副本 会是默认的leader副本 发生leader变化时重选举会优先选择优先副本作为leader

本文正在参加「金石计划」

本文转载自: 掘金

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

JYM 设计模式系列- 责任链模式,装饰模式,让你的代码更优

发表于 2023-05-27

觉得不错请按下图操作,掘友们,哈哈哈!!!
image.png

概述设计模式分类

总体来说设计模式分为三大类:

  • 创建型模式,共五种:工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式。
  • 结构型模式,共七种:适配器模式、装饰器模式、代理模式、外观模式、桥接模式、组合模式、享元模式。
  • 行为型模式,共十一种:策略模式、模板方法模式、观察者模式、迭代子模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式。

一:责任链模式

1.1 名词解释:

责任链模式是一种行为设计模式,允许你将请求沿着处理者链进行发送。收到请求后,每个处理者均可对请求进行处理,或将其传递给链上的下个处理者。责任链模式的核心是解决一组服务中的先后执行处理关系。

责任链模式可以让各个服务模块更加清晰,而每一个模块可以通过next的方式进行获取。而每一个next是由继承的统一抽象类实现的,最终所有类的职责可以动态的进行编排使用,编排的过程可以做成可配置化。

在使用责任链时,如果场景比较固定,可写死到代码中进行初始化。但如果业务场景经常变化可以做成xml配置的方式进行处理,也可以保存到数据库中进行初始化操作。

实际的业务中在使用责任链时会进行一系列的包装,通过把不同的责任节点进行组装,构成一条完整业务的责任链。

1.2 优点:

责任链模式很好的处理单一职责和开闭原则,简单耦合也使对象关系更加清晰,而且外部的调用方并不需要关系责任链是如何处理的。

1.3责任链模式结构

  • 处理者

声明了所有具体处理者的通用接口,该接口通常仅包含单个方法用于请求处理,但有时其还会包含一个设置链上下处理者的方法。

  • 基础处理者 是一个可选的类,你可以将所有处理者共用的样本代码放置在其中。(通常情况下,该类定义了一个保存对于下个处理者引用的成员变量。客户端可通过将处理者的构造函数或设定方法来创建链。该类还可以实现默认的处理行为,确定下个处理者存在后再将请求传递给它。)
  • 具体处理者 包含处理请求的实际代码。每个处理者接收到请求后,都必须决定是否进行处理,或者说是否沿着链传递请求。
  • 客户端

可根据程序逻辑一次性或者动态的生成链。

1.4 适用场景

  • 当程序需要使用不同方式处理不同种类请求,而且请求类型和顺序预先未知时。
  • 业务逻辑必须按顺序执行多个处理者时。
  • 处理者及其顺序必须在运行时进行改变,可以使用责任链模式。

1.5 实现方式

  • 声明处理者接口并描述请求处理方法的签名
  • 可以根据处理者接口创建抽象处理者基类(需要一个成员变量来存储指向链上下个处理者的引用)
  • 创建具体的处理者子类并实现其处理方法。(每个处理者在接收到请求后都必须做两个决定:1、是否自行处理请求;2、是否将该请求沿着链进行传递。)
  • 客户端可自行组装链,或者从其他对象处获得预先组装好的链。
  • 客户端可触发链中的任意处理者,而不仅仅是第一个。请求将通过链进行传递,直至某个处理者拒绝继续传递或者请求到达链尾。

1.6 上demo

image.png

RequestHandler :请求处理器

1
2
3
4
5
6
7
8
9
10
csharp复制代码public interface RequestHandler {

boolean canHandleRequest(Request req);

int getPriority();

void handle(Request req);

String name();
}

Request:

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
arduino复制代码public class Request {

/**
* 此请求的类型,由链中的每个项目使用以查看它们是否应该或可以处理
* 这个特殊要求
*/
private final RequestType requestType;

/**
* 请求的描述
*/
private final String requestDescription;

/**
* 指示请求是否已处理。请求只能将状态从未处理切换到
* 已处理,无法“取消处理”请求
*/
private boolean handled;

/**
* 创建给定类型和随附描述的新请求。
*
* @param requestType The type of request
* @param requestDescription The description of the request
*/
public Request(final RequestType requestType, final String requestDescription) {
this.requestType = Objects.requireNonNull(requestType);
this.requestDescription = Objects.requireNonNull(requestDescription);
}

/**
* 获取请求的描述。
*
* @返回请求的人可读描述
*/
public String getRequestDescription() {
return requestDescription;
}

/**
* G获取此请求的类型,由命令链中的每个人使用以查看他们是否应该
* 或者可以处理这个特定的请求
*
* @return The request type
*/
public RequestType getRequestType() {
return requestType;
}

/**
* 将请求标记为已处理
*/
public void markHandled() {
this.handled = true;
}

/**
* 指示是否处理此请求
*
* @return <tt>true</tt> when the request is handled, <tt>false</tt> if not
*/
public boolean isHandled() {
return this.handled;
}

@Override
public String toString() {
return getRequestDescription();
}

}

RequestType:请求枚举类

1
2
3
4
5
6
7
arduino复制代码public enum RequestType {

DEFEND_CASTLE, //防御城堡
TORTURE_PRISONER,//酷刑囚犯
COLLECT_TAX //收税

}

OrcCommander:兽人指挥官

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
typescript复制代码@Slf4j
public class OrcCommander implements RequestHandler {
@Override
public boolean canHandleRequest(Request req) {
return req.getRequestType() == RequestType.DEFEND_CASTLE;
}

@Override
public int getPriority() {
return 2;
}

@Override
public void handle(Request req) {
req.markHandled();
LOGGER.info("{} handling request "{}"", name(), req);
}

@Override
public String name() {
return "Orc commander";
}
}

OrcKing: 发出由链处理的请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
scss复制代码public class OrcKing {

private List<RequestHandler> handlers;

public OrcKing() {
buildChain();
}

private void buildChain() {
handlers = Arrays.asList(new OrcCommander(), new OrcOfficer(), new OrcSoldier());
}

/**
* Handle request by the chain.
*/
public void makeRequest(Request req) {
handlers
.stream()
.sorted(Comparator.comparing(RequestHandler::getPriority))
.filter(handler -> handler.canHandleRequest(req))
.findFirst()
.ifPresent(handler -> handler.handle(req));
}
}

OrcOfficer:兽人军官

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
typescript复制代码@Slf4j
public class OrcOfficer implements RequestHandler {
@Override
public boolean canHandleRequest(Request req) {
return req.getRequestType() == RequestType.TORTURE_PRISONER;
}

@Override
public int getPriority() {
return 3;
}

@Override
public void handle(Request req) {
req.markHandled();
LOGGER.info("{} handling request "{}"", name(), req);
}

@Override
public String name() {
return "Orc officer";
}
}

OrcSoldier:兽人士兵

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
typescript复制代码@Slf4j
public class OrcSoldier implements RequestHandler {
@Override
public boolean canHandleRequest(Request req) {
return req.getRequestType() == RequestType.COLLECT_TAX;
}

@Override
public int getPriority() {
return 1;
}

@Override
public void handle(Request req) {
req.markHandled();
LOGGER.info("{} handling request "{}"", name(), req);
}

@Override
public String name() {
return "Orc soldier";
}
}

程序入口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
typescript复制代码public class App {

/**
* Program entry point.
*
* @param args command line args
*/
public static void main(String[] args) {

var king = new OrcKing();
king.makeRequest(new Request(RequestType.DEFEND_CASTLE, "defend castle 保卫城堡"));
king.makeRequest(new Request(RequestType.TORTURE_PRISONER, "torture prisoner 酷刑囚犯"));
king.makeRequest(new Request(RequestType.COLLECT_TAX, "collect tax 征税"));
}
}

在这个例子中,我们将请求处理程序 RequestHandler组织成一个链,其中 每个处理程序都有机会轮流处理请求。这里国王 OrcKing发出请求,军事兽人 OrcCommander, OrcOfficer, OrcSoldier 形成处理程序链。

二:装饰模式

2.1 什么是装饰模式解释:

装饰模式又名包装模式(Wrapper)。装饰模式是以对客户端透明的方式扩展对象的功能,是继承关系的一种替代方案。

  • 装饰模式以对客户透明的方式动态的给对象附加更多的责任,换言之客户并不会觉得对象在装饰前和装饰后有什么区别。
  • 装饰模式可以在不增加子类的情况下,将对象的功能扩展。
  • 装饰模式把客户端的调用委派到被装饰类,装饰模式的关键在于这种功能的扩展是透明的。
  • 装饰模式是在不必改变原类文件和使用继承的情况下,动态的扩展一个对象的功能,它是通过创建一个包装对象,也就是装饰来包裹真是的对象。

2.2 装饰模式的组成

  • 抽象构件角色(Component):给出一个抽象接口,已规范准备接受附加责任的对象。
  • 具体构件角色(Concrete Component):定义一个将要接收附加责任的类。
  • 装饰角色(Decrator):持有一个构建角色对象的引用,并定义一个与抽象构建角色一致的接口。
  • 具体装饰角色(Concrete Decrator):负责给构建对象贴上附加的责任。

2.3 装饰模式的优点

装饰模式的优点:

  • 装饰模式与继承关系的目的都是要扩展对象的功能,但是装饰模式可以提供比继承更多的灵活性。
  • 可以通过一种动态的方式来扩展一个对象的功能,通过配置文件可以在运行时选择不同的装饰器,从而实现不同的行为。
  • 通过使用不同的具体装饰类以及这些装饰类的排列组合,可以创造出很多不同行为的组合。可以使用多个具体装饰类来装饰同一对象,得到功能更为强大的对象。
  • 具体构件类与具体装饰类可以独立变化,用户可以根据需要增加新的具体构件类和具体装饰类,在使用时再对其进行组合,原有代码无须改变,符合“开闭原则”

装饰模式的缺点:

  • 使用装饰模式进行系统设计时将产生很多小对象,这些对象的区别在于它们之间相互连接的方式有所不同,而不是它们的类或者属性值有所不同,同时还将产生很多具体装饰类。这些装饰类和小对象的产生将增加系统的复杂度,加大学习与理解的难度。
  • 这种比继承更加灵活机动的特性,也同时意味着装饰模式比继承更加易于出错,排错也很困难,对于多次装饰的对象,调试时寻找错误可能需要逐级排查,较为烦琐。

2.4装饰模式的适用环境

  • 在不影响其他对象的情况下,以动态、透明的方式给单个对象添加职责。
  • 需要动态地给一个对象增加功能,这些功能也可以动态地被撤销。
  • 当不能采用继承的方式对系统进行扩充或者采用继承不利于系统扩展和维护时。不能采用继承的情况主要有两类:第一类是系统中存在大量独立的扩展,为支持每一种组合将产生大量的子类,使得子类数目呈爆炸性增长;第二类是因为类定义不能继承(如final类).

2.5 上demo

image.png

Troll:巨魔接口

1
2
3
4
5
6
7
8
9
csharp复制代码public interface Troll {

void attack(); //攻击

int getAttackPower(); // 获取攻击力

void fleeBattle(); //逃离战斗

}

棒子巨魔实现巨魔接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
typescript复制代码@Slf4j
@RequiredArgsConstructor
public class ClubbedTroll implements Troll {

private final Troll decorated;

@Override
public void attack() {
decorated.attack();
LOGGER.info("The troll swings at you with a club!");
}

@Override
public int getAttackPower() {
return decorated.getAttackPower() + 10;
}

@Override
public void fleeBattle() {
decorated.fleeBattle();
}
}

一般的巨魔:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
typescript复制代码@Slf4j
public class SimpleTroll implements Troll {

@Override
public void attack() {
LOGGER.info("The troll tries to grab you!");
}

@Override
public int getAttackPower() {
return 10;
}

@Override
public void fleeBattle() {
LOGGER.info("The troll shrieks in horror and runs away!");
}
}

程序入口:

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
scss复制代码@Slf4j
public class App {

/**
* Program entry point.
*
* @param args command line args
*/
public static void main(String[] args) {

// simple troll
LOGGER.info("A simple looking troll approaches.");
var troll = new SimpleTroll();
troll.attack();
troll.fleeBattle();
LOGGER.info("Simple troll power: {}.\n", troll.getAttackPower());

// 通过添加装饰器改变简单巨魔的行为
LOGGER.info("A troll with huge club surprises you.");
var clubbedTroll = new ClubbedTroll(troll);
clubbedTroll.attack();
clubbedTroll.fleeBattle();
LOGGER.info("Clubbed troll power: {}.\n", clubbedTroll.getAttackPower());
}
}

在这个例子中,我们展示了简单的 SimpleTroll 如何首先攻击然后逃离 战斗。然后我们用 ClubbedTroll装饰 SimpleTroll 并再次执行攻击。可以看到装饰后行为发生了怎样的变化。

装饰器模式是子类化的更灵活的替代方案。 Decorator 类实现与目标相同的接口,并使用组合来“装饰”对目标的调用。使用装饰器模式可以在运行时改变类的行为。

本文转载自: 掘金

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

这道面试题真的很变态吗?😱

发表于 2023-05-25

最近帮公司招聘,主要负责一面,所以基本上问的基础多一点。但是我在问这样一道面试题的时候,很少有人答对。不少人觉得我问这道题多少有点过分了😭,当然了面试还是奔着相互沟通相互学习的目的,并不是说这道题不会就被刷掉,单纯的觉得这道题有意思。话不多说,我们直接上题

题目

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
js复制代码var foo = function () {
console.log("foo1")
}
foo()

var foo = function () {
console.log("foo2")
}
foo()


function foo() {
console.log("foo1")
}
foo()

function foo() {
console.log("foo2")
}
foo()

这里我会要求面试者从上到下依次说出执行结果。普遍多的面试者给出的答案是:foo1、foo2、foo1、foo2。虽然在我看来这是一道简单的面试题,但是也不至于这么简单吧😱~

当然面试本来就是一个相互讨论的过程,那就和面试者沟通下这道题我的理解,万一我理解错了呢😂

解答

拆分函数表达式

首先我会让面试者先看前面两个函数

1
2
3
4
5
6
7
8
9
js复制代码var foo = function () {
console.log("foo1")
}
foo()

var foo = function () {
console.log("foo2")
}
foo()

这时候大部分人基本上都可以答对了,是:foo1、foo2。再有很少数的人答不对那就只能”施主,出门右转“😌。接着根据我当时的心情可能会稍作追问(美女除外🙂):

1
2
3
4
js复制代码foo()
var foo = function () {
console.log("foo1")
}

这时候又有一部分的人答不上来了。这毫无疑问是肯定会报错的啊

image.png

我们都知道用var定义的变量会变量提升,所以声明会被拿到函数或全局作用域的顶部,并且输出undefined。所以当执行foo()的时候,foo还是undefined,所以会报错。由于js从按照顺序从上往下执行,所以当执行foo = function(){}的时候,才对foo进行赋值为一个函数。我们重新看拆分之后的代码

1
2
3
4
5
6
7
8
9
js复制代码var foo = function () {
console.log("foo1")
}
foo()

var foo = function () {
console.log("foo2")
}
foo()

foo首先会变量提升,然后进行赋值为function。所以当执行第一个foo的时候,此时foo就是我们赋值的这个函数。接着执行第二个foo的赋值操作,由于函数作用域的特性,后面定义的函数将覆盖前面定义的函数。
由于在调用函数之前就进行了函数的重新定义,所以在调用函数时,实际执行的是最后定义的那个函数。所以上面的代码会打印:foo1、foo2。

这种定义函数的方式,我们称为函数表达式。函数表达式是将函数作为一个值赋给一个变量或属性

函数表达式我们拆分完了,下面就看看函数声明吧。

拆分函数声明

1
2
3
4
5
6
7
8
9
js复制代码function foo() {
console.log("foo1")
}
foo()

function foo() {
console.log("foo2")
}
foo()

大部分人其实都卡在了这里。函数声明会在任何代码执行之前先被读取并添加到执行上下文,也就是函数声明提升。说到这里其实大多数人就已经明白了。这里使用了函数声明定义了两个foo函数,由于函数声明提升,第二个foo会覆盖第一个foo,所以当调用第一个foo的时候,其实已经被第二个foo覆盖了,所以这两个打印的都是foo2。

当两段代码结合

当开始解析的时候,函数声明就已经提升了,第四个foo会覆盖第三个foo。然后js开始从上往下执行,第一个赋值操作之后执行foo()后,打印了”foo1“,第二个赋值之后执行foo(),打印了”foo2”。下面两个foo的执行其实是第二个赋值了的foo,因为函数声明开始从刚开始就被提升了,而下面的赋值会覆盖foo。

总结

我们整体分析代码的执行过程

  1. 通过函数表达式定义变量foo并赋值为一个匿名函数,该函数在被调用时打印”foo1”。
  2. 接着,通过函数表达式重新定义变量foo,赋值为另一个匿名函数,该函数在被调用时打印”foo2”。
  3. 使用函数声明定义了两个名为foo的函数。函数声明会在作用域中进行提升。后面的会覆盖前面的,由于声明从一开始就提升了,而又执行了两个赋值操作,所以此时foo是第二个赋值的函数。
  4. 然后调用foo(),输出”foo2”。
  5. 再调用foo(),也输出”foo2”。

其实就一个点: 函数表达式相对于函数声明的一个重要区别是函数声明在代码解析阶段就会被提升(函数声明提升),而函数表达式则需要在赋值语句执行到达时才会创建函数对象

小伙伴们,以上是我的理解,欢迎在评论区留言,大家相互讨论相互学习。

之前的描述确实有点不妥,所以做了改动,望大家谅解,还是本着相互学习的态度

本文转载自: 掘金

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

JYM 设计模式系列- 策略模式,模板方法模式,让你的代码更

发表于 2023-05-25

觉得不错请按下图操作,掘友们,哈哈哈!!!
image.png

概述设计模式分类

总体来说设计模式分为三大类:

  • 创建型模式,共五种:工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式。
  • 结构型模式,共七种:适配器模式、装饰器模式、代理模式、外观模式、桥接模式、组合模式、享元模式。
  • 行为型模式,共十一种:策略模式、模板方法模式、观察者模式、迭代子模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式。

一:策略模式

Strategy 模式(也称为策略模式)是一种软件设计模式,允许在运行时选择算法的行为。在 Java 8 之前,Strategies 需要是单独的类,迫使开发人员编写大量样板代码。使用现代 Java,很容易通过方法引用和 lambda 传递行为,从而使代码更短、更易读。

在这个例子中 DragonSlayingStrategy 封装了一个算法。包含对象 DragonSlayer(屠龙者) 可以通过改变它的策略来改变它的行为。

1.1 使用场景

  • 针对同一类型的问题多种处理
  • 需要安全的封装多种同一类型的操作,对客户隐藏具体实现。
  • 出现同一抽象类有多个子类,而又需要使用 if-else 或者switch-case 来选择具体子类时。

image.png

1.2 优缺点

优点

  • 结构清晰明了,使用简单直观
  • 耦合度相对较低,扩展方便
  • 操作封装更彻底,数据更加安全

缺点

  • 随着策略的增加,类 会变得越来越多,容易造成类爆炸
  • 使用者必须知道所有策略类,并自行解决使用哪个策略

1.3 直接上demo

策略接口:

1
2
3
4
5
6
csharp复制代码@FunctionalInterface
public interface DragonSlayingStrategy {

void execute();

}

DragonSlayer:屠龙者使用不同的策略来屠龙

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
csharp复制代码public class DragonSlayer {

private DragonSlayingStrategy strategy;

public DragonSlayer(DragonSlayingStrategy strategy) {
this.strategy = strategy;
}

public void changeStrategy(DragonSlayingStrategy strategy) {
this.strategy = strategy;
}

public void goToBattle() {
strategy.execute();
}
}

枚举策略模式的 Lambda 实现:

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
scss复制代码@Slf4j
public class LambdaStrategy {

/**
* Enum to demonstrate strategy pattern.
*/
public enum Strategy implements DragonSlayingStrategy {
MeleeStrategy(() -> LOGGER.info(
"With your Excalibur you severe the dragon's head!")),
ProjectileStrategy(() -> LOGGER.info(
"You shoot the dragon with the magical crossbow and it falls dead on the ground!")),
SpellStrategy(() -> LOGGER.info(
"You cast the spell of disintegration and the dragon vaporizes in a pile of dust!"));

private final DragonSlayingStrategy dragonSlayingStrategy;

Strategy(DragonSlayingStrategy dragonSlayingStrategy) {
this.dragonSlayingStrategy = dragonSlayingStrategy;
}

@Override
public void execute() {
dragonSlayingStrategy.execute();
}
}
}

近战策略:

1
2
3
4
5
6
7
8
typescript复制代码@Slf4j
public class MeleeStrategy implements DragonSlayingStrategy {

@Override
public void execute() {
LOGGER.info("With your Excalibur you sever the dragon's head!");
}
}

弹丸策略:

1
2
3
4
5
6
7
8
typescript复制代码@Slf4j
public class ProjectileStrategy implements DragonSlayingStrategy {

@Override
public void execute() {
LOGGER.info("You shoot the dragon with the magical crossbow and it falls dead on the ground!");
}
}

法术策略:

1
2
3
4
5
6
7
8
9
typescript复制代码@Slf4j
public class SpellStrategy implements DragonSlayingStrategy {

@Override
public void execute() {
LOGGER.info("You cast the spell of disintegration and the dragon vaporizes in a pile of dust!");
}

}

程序入口:

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
ini复制代码@Slf4j
public class App {

private static final String RED_DRAGON_EMERGES = "Red dragon emerges.";
private static final String GREEN_DRAGON_SPOTTED = "Green dragon spotted ahead!";
private static final String BLACK_DRAGON_LANDS = "Black dragon lands before you.";

/**
* Program entry point.
*
* @param args command line args
*/
public static void main(String[] args) {
//GoF 策略模式
LOGGER.info(GREEN_DRAGON_SPOTTED);
var dragonSlayer = new DragonSlayer(new MeleeStrategy());
dragonSlayer.goToBattle();
LOGGER.info(RED_DRAGON_EMERGES);
dragonSlayer.changeStrategy(new ProjectileStrategy());
dragonSlayer.goToBattle();
LOGGER.info(BLACK_DRAGON_LANDS);
dragonSlayer.changeStrategy(new SpellStrategy());
dragonSlayer.goToBattle();

// Java 8 函数式实现策略模式
LOGGER.info(GREEN_DRAGON_SPOTTED);
dragonSlayer = new DragonSlayer(
() -> LOGGER.info("With your Excalibur you severe the dragon's head!"));
dragonSlayer.goToBattle();
LOGGER.info(RED_DRAGON_EMERGES);
dragonSlayer.changeStrategy(() -> LOGGER.info(
"You shoot the dragon with the magical crossbow and it falls dead on the ground!"));
dragonSlayer.goToBattle();
LOGGER.info(BLACK_DRAGON_LANDS);
dragonSlayer.changeStrategy(() -> LOGGER.info(
"You cast the spell of disintegration and the dragon vaporizes in a pile of dust!"));
dragonSlayer.goToBattle();

// 具有枚举策略模式的 Java 8 lambda 实现
LOGGER.info(GREEN_DRAGON_SPOTTED);
dragonSlayer.changeStrategy(LambdaStrategy.Strategy.MeleeStrategy);
dragonSlayer.goToBattle();
LOGGER.info(RED_DRAGON_EMERGES);
dragonSlayer.changeStrategy(LambdaStrategy.Strategy.ProjectileStrategy);
dragonSlayer.goToBattle();
LOGGER.info(BLACK_DRAGON_LANDS);
dragonSlayer.changeStrategy(LambdaStrategy.Strategy.SpellStrategy);
dragonSlayer.goToBattle();
}
}

策略模式主要是分离算法,减少代码的耦合度,而且这个模式很好遵循了开闭原则。在书写代码中,多多使用设计模式,可以让代码更加优美。

二:模板方法模式

2.1 引言

模板方法模式是结构最简单的行为型设计模型,在其结构中只存在父类与之类之间的继承关系,通过使用模板方法模式,可以将一些复杂流程的实现步骤封装在一系列基本方法中,在抽象父类提供一个称之为模板方法的方法来定义这些基本方法的执行次序,而通过其子类来覆盖某些步骤,从而使得相同的算法框架可以有不同的执行结果。模板方法提供了一个模板方法来定义算法框架,而某些具体步骤的实现可以在其子类中完成。

2.2 定义

模板方法模式:定义一个操作中算法的框架,而将一些步骤延迟到子类中,模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些步骤。模板方法模式是一种类行为型模式。模板方法定义算法的框架。算法子类为空白部分提供实现。

2.3 优缺点

该模式的主要优点:

  • 它封装了不变部分,扩展可变部分。它把认为是不变部分的算法封装到父类中实现,而把可变部分算法由子类继承实现,便于子类继续扩展。
  • 它在父类中提取了公共的部分代码,便于代码复用。
  • 部分方法是由子类实现的,因此子类可以通过扩展方式增加相应的功能,符合开闭原则。

该模式的主要缺点:

  • 对每个不同的实现都需要定义一个子类,这会导致类的个数增加,系统更加庞大,设计也更加抽象,间接地增加了系统实现的复杂度。
  • 父类中的抽象方法由子类实现,子类执行的结果会影响父类的结果,这导致一种反向的控制结构,它提高了代码阅读的难度。
  • 由于继承关系自身的缺点,如果父类添加新的抽象方法,则所有子类都要改一遍。

2.4 适用场景:

  • 对一些复杂算法进行分割,将其算法中固定不变的部分设计为模板方法和父类方法,而一些改变的细节由子类实现,也就是一次性实现算法中不变部分,并将可变部分交由子类实现
  • 各子类中公共的行为应被提取出来并集中到一个公共父类中以避免代码重复
  • 需要通过子类决定父类算法中某个步骤是否执行,实现子类对父类的反向控制

2.5 直接上demo:

在此示例中, HalflingThief(蒙面小偷) 包含可以更改的 StealingMethod(窃取方法)。 小偷首先使用 HitAndRunMethod(打了就跑) 进行攻击,然后使用 SubtleMethod(微妙方法)。

image.png

StealingMethod 定义算法的框架:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
csharp复制代码@Slf4j
public abstract class StealingMethod {

protected abstract String pickTarget();

protected abstract void confuseTarget(String target);

protected abstract void stealTheItem(String target);

/**
* Steal.
*/
public final void steal() {
var target = pickTarget();
LOGGER.info("The target has been chosen as {}.", target);
confuseTarget(target);
stealTheItem(target);
}
}

蒙面小偷:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
csharp复制代码public class HalflingThief {

private StealingMethod method;

public HalflingThief(StealingMethod method) {
this.method = method;
}

public void steal() {
method.steal();
}

public void changeMethod(StealingMethod method) {
this.method = method;
}
}

打了就跑方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
typescript复制代码@Slf4j
public class HitAndRunMethod extends StealingMethod {

@Override
protected String pickTarget() {
return "old goblin woman";
}

@Override
protected void confuseTarget(String target) {
LOGGER.info("Approach the {} from behind.", target);
}

@Override
protected void stealTheItem(String target) {
LOGGER.info("Grab the handbag and run away fast!");
}
}

微妙的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
typescript复制代码@Slf4j
public class SubtleMethod extends StealingMethod {

@Override
protected String pickTarget() {
return "shop keeper";
}

@Override
protected void confuseTarget(String target) {
LOGGER.info("Approach the {} with tears running and hug him!", target);
}

@Override
protected void stealTheItem(String target) {
LOGGER.info("While in close contact grab the {}'s wallet.", target);
}
}

程序入口:

1
2
3
4
5
6
7
8
9
10
typescript复制代码public class App {


public static void main(String[] args) {
var thief = new HalflingThief(new HitAndRunMethod());
thief.steal();
thief.changeMethod(new SubtleMethod());
thief.steal();
}
}

本文转载自: 掘金

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

如何理解Native Crash问题 常见的Native C

发表于 2023-05-25

常见的Native Crash类型

类型 子类 原因
SIGSEGV SEGV_MAPERR 地址不在 /proc/self/maps 映射中
SEGV_ACCERR 没有访问权限
SEGV_MTESERR MTE特有类型
SIGABRT 程序主动退出,常见调用函数abort(),raise()等
SIGILL ILL_ILLOPC 非法操作码(opcode)
ILL_ILLOPN 非法操作数(operand)
ILL_ILLADR 非法寻址
ILL_ILLTRP 非法trap,如_builtintrap()主动崩溃
ILL_PRVOPC 非法特权操作码(privileged opcode)
ILL_PRVREG 非法特权寄存器(privileged register)
ILL_COPROC 协处理器错误
ILL_BADSTK 内部堆栈错误
SIGBUS BUS_ADRALN 访问地址未对齐
BUS_ADRERR 访问不存在的物理地址,常见访问的文件截断
BUS_OBJERR 特定对象的硬件错误
SIGFPE FPE_INTDIV 整数除以0
FPE_INTOVF 整数溢出
FPE_FLTDIV 浮点数除以0
FPE_FLTOVF 浮点数上溢(overflow)
FPE_FLTUND 浮点数下溢(underflow)
FPE_FLTRES 浮点数结果不精确
FPE_FLTINV 无效的浮点运算
FPE_FLTSUB 越界

Android日志

  当程序发生了 Native Crash 错误,Android 的日志会输出到 log crash buffer 上,因此我们通过adb logcat -b crash 抓取到相应的错误报告,而日志本身能提供的信息是有限的,仅仅是错误堆栈,与当前线程的寄存器信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
yaml复制代码--------- beginning of crash
06-07 01:53:32.465 12027 12027 F DEBUG : *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
06-07 01:53:32.465 12027 12027 F DEBUG : Revision: '0'
06-07 01:53:32.466 12027 12027 F DEBUG : ABI: 'arm64'
06-07 01:53:32.466 12027 12027 F DEBUG : Timestamp: 2022-06-07 01:53:32.033409857+0800
06-07 01:53:32.466 12027 12027 F DEBUG : Process uptime: 0s
06-07 01:53:32.466 12027 12027 F DEBUG : Cmdline: mediaserver64
06-07 01:53:32.466 12027 12027 F DEBUG : pid: 1139, tid: 11981, name: NPDecoder >>> mediaserver64 <<<
06-07 01:53:32.466 12027 12027 F DEBUG : uid: 1013
06-07 01:53:32.466 12027 12027 F DEBUG : tagged_addr_ctrl: 0000000000000001
06-07 01:53:32.466 12027 12027 F DEBUG : signal 11 (SIGSEGV), code 2 (SEGV_ACCERR), fault addr 0x7c02d886f0
06-07 01:53:32.466 12027 12027 F DEBUG : x0 79748c5e568e2ddc x1 0000007ca13c3618 x2 0000000000000000 x3 0000007ca1291000
06-07 01:53:32.466 12027 12027 F DEBUG : x4 0000000001909705 x5 0000000000000000 x6 0000007c02d88808 x7 b60625655bf0252f
06-07 01:53:32.467 12027 12027 F DEBUG : x8 0000000000000080 x9 0000007ca126fed7 x10 0000000000000006 x11 0000007bfd0a81fc
06-07 01:53:32.467 12027 12027 F DEBUG : x12 9ef8a95ca9649dbe x13 e44782d5ac38720e x14 0000007bfd0a8030 x15 0000001e56307b5c
06-07 01:53:32.467 12027 12027 F DEBUG : x16 0000007c95dfdb70 x17 0000007c9844f118 x18 0000007bfaa28000 x19 b400007c13c246d0
06-07 01:53:32.467 12027 12027 F DEBUG : x20 0000007c02d88730 x21 b400007c13c67c00 x22 0000000000000415 x23 0000007c02d89000
06-07 01:53:32.467 12027 12027 F DEBUG : x24 0000000000000002 x25 b400007c13c246d0 x26 b400007c13c67c00 x27 0000007c02d89000
06-07 01:53:32.467 12027 12027 F DEBUG : x28 0000007ca13c2c28 x29 0000007c02d886f0
06-07 01:53:32.467 12027 12027 F DEBUG : lr 0000007c02d886f0 sp 0000007c02d886d0 pc 0000007c02d886f0 pst 0000000080001000
06-07 01:53:32.467 12027 12027 F DEBUG : backtrace:
06-07 01:53:32.467 12027 12027 F DEBUG : #00 pc 00000000000f86f0 [anon:stack_and_tls:11981]

  当只有日志堆栈不能进行更详细的分析时,我们还需要程序的部分内存信息以及寄存器信息,而Android 的错误机制会相应的会生成一份 tombstone 文件保存到 /data/tombstones/tombstone_xx ,对于没有 Root 权限的机器则可以通过 adb bugreport 抓取出 tombstone 文件。

Tombstone

  tombstone 文件保存的信息有错误程序的体系结构,通俗的说 arm、arm64 等,发生时间点,程序名,错误类型,错误程序的进程号、线程号,错误现场寄存器,堆栈和部分寄存器地址附近的内存信息,程序内存映射表 /proc/self/maps ,FD 信息以及发生错误时该程序输出的日志。

1
2
3
4
5
6
yaml复制代码ABI: 'arm64' 【 arm64的程序 】
Timestamp: 2022-06-07 01:53:32.033409857+0800 【 发生错误的时间戳 】
Process uptime: 0s
Cmdline: mediaserver64 【 程序名 】
pid: 1139, tid: 11981, name: NPDecoder >>> mediaserver64 <<< 【 进程号、线程号 】
uid: 1013

错误类型

1
2
3
4
ini复制代码signal 11 (SIGSEGV), code 2 (SEGV_ACCERR), fault addr 0x7c02d886f0
【 错误类型是 SIGSEGV,子类是 SEGV_ACCERR,错误地址0x7c02d886f0 】
SIGSEGV 也是我们最常见的 Native Crash 类型,大部分时候我们称其为段错误,
而错误意思是在 PC=0x7c02d886f0 上发生拒绝访问的段错误。

寄存器信息

1
2
3
4
5
6
7
8
9
复制代码x0 79748c5e568e2ddc x1 0000007ca13c3618 x2 0000000000000000 x3 0000007ca1291000
x4 0000000001909705 x5 0000000000000000 x6 0000007c02d88808 x7 b60625655bf0252f
x8 0000000000000080 x9 0000007ca126fed7 x10 0000000000000006 x11 0000007bfd0a81fc
x12 9ef8a95ca9649dbe x13 e44782d5ac38720e x14 0000007bfd0a8030 x15 0000001e56307b5c
x16 0000007c95dfdb70 x17 0000007c9844f118 x18 0000007bfaa28000 x19 b400007c13c246d0
x20 0000007c02d88730 x21 b400007c13c67c00 x22 0000000000000415 x23 0000007c02d89000
x24 0000000000000002 x25 b400007c13c246d0 x26 b400007c13c67c00 x27 0000007c02d89000
x28 0000007ca13c2c28 x29 0000007c02d886f0
lr 0000007c02d886f0 sp 0000007c02d886d0 pc 0000007c02d886f0 pst 0000000080001000

堆栈信息

1
2
3
4
makefile复制代码backtrace:
#00 pc 00000000000f86f0 [anon:stack_and_tls:11981] 【PC刚好落在线程栈地址上】
这种情况很少见,虽然它只有的一条堆栈,并不代表程序是从这里开始运行,出现的这种情况仅仅 unwind 无法正确的回溯。
我们是可以通过栈地址空间内存进行恢复调用栈,用户态主线程栈(红色部分)结构如下:

UML diagram.jpg

1
2
3
ruby复制代码而线程栈位于 mmap segmemt上,我们可以在 /proc/self/maps 上找到该线程栈的地址空间范围。
0000007c'02c90000-0000007c'02d8bfff rw- 0 fc000 [anon:stack_and_tls:11981]
大多数 arm64 的 Linux Android 程序,它的线程调用栈结构样例如下:

UML diagram (12).jpg

内存信息

  tombstone 会记录当前有效地址的寄存器附近内存信息,大小为0x100,这个可以修改文件
system/core/debuggerd/libdebuggerd/utility.cpp 中的宏定义 MEMORY_BYTES_TO_DUMP 参数
像这种一条堆栈的情况下,栈的内存信息配合下边的映射表可以帮助到我们对栈进行恢复。

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
erlang复制代码memory near x1 (/system/lib64/libstagefright.so):
0000007ca13c35f0 0000000000000000 0000000000000000 ................
0000007ca13c3600 0000000000000000 0000000000000000 ................
0000007ca13c3610 0000000000000000 0000007ca132326c ........l22.|...
0000007ca13c3620 0000007ca1324008 0000007ca10552e4 .@2.|....R..|...
0000007ca13c3630 0000007ca10552e8 0000007ca10552ec .R..|....R..|...
0000007ca13c3640 0000007ca10552f4 0000007ca132402c .R..|...,@2.|...
0000007ca13c3650 0000000000000000 0000000000000000 ................
0000007ca13c3660 0000000000000000 0000000000000000 ................
0000007ca13c3670 0000000000000000 0000007ca134ea84 ..........4.|...
0000007ca13c3680 0000007ca134ecec 0000007ca10552e4 ..4.|....R..|...
0000007ca13c3690 0000007ca10552e8 0000007ca10552ec .R..|....R..|...
0000007ca13c36a0 0000007ca10552f4 0000007ca134ed10 .R..|.....4.|...
0000007ca13c36b0 0000000000000000 0000000000000000 ................
0000007ca13c36c0 0000000000000000 0000000000000000 ................
0000007ca13c36d0 0000000000000000 0000007ca135d02c ........,.5.|...
0000007ca13c36e0 0000007ca135d4b8 0000007ca10552e4 ..5.|....R..|...

memory near x29 ([anon:stack_and_tls:11981]):
0000007c02d886d0 b400007c13c246d0 0000000001909705 .F..|……….. 【SP = 0x0000007c02d886d0】
0000007c02d886e0 0000007c02d88700 6f2ab3b40fa2f8ef ….|………*o*
0000007c02d886f0 0000007c02d88750 0000007ca133f8e0 P…|…..3.|…*【x29 = 0x0000007c02d886f0】
0000007c02d88700 0000000000000002 0000000000000000 …………….*
0000007c02d88710 0000000000000415 0000000001909705 …………….*
0000007c02d88720 0000000000000000 0000007c02d88808 …………|…*
0000007c02d88730 b400007c13c67c00 0000000000000000 .|..|………..*
0000007c02d88740 0000007c02d89000 6f2ab3b40fa2f8ef ….|………*o
0000007c02d88750 0000007c02d88830 0000007ca796ee7c 0…|…|…|…
0000007c02d88760 0000007ca79f3dd8 0000007ca79edb80 .=..|…….|…
0000007c02d88770 0000007c02d89000 b400007c13c04680 ….|….F..|…
0000007c02d88780 0000000000000000 0000000000000002 …………….
0000007c02d88790 b400007c13c67c00 0000000000000000 .|..|………..
0000007c02d887a0 0000000000000000 b400007c13c7c100 …………|…
0000007c02d887b0 0000007c02d88830 0000007ca796d420 0…|… …|…
0000007c02d887c0 0000007c02d88830 0000007ca796d460 0…|…`…|…

memory near lr ([anon:stack_and_tls:11981]):
0000007c02d886d0 b400007c13c246d0 0000000001909705 .F..|………..
0000007c02d886e0 0000007c02d88700 6f2ab3b40fa2f8ef ….|………*o*
0000007c02d886f0 0000007c02d88750 0000007ca133f8e0 P…|…..3.|…*
0000007c02d88700 0000000000000002 0000000000000000 …………….*
0000007c02d88710 0000000000000415 0000000001909705 …………….*
0000007c02d88720 0000000000000000 0000007c02d88808 …………|…*
0000007c02d88730 b400007c13c67c00 0000000000000000 .|..|………..*
0000007c02d88740 0000007c02d89000 6f2ab3b40fa2f8ef ….|………*o
0000007c02d88750 0000007c02d88830 0000007ca796ee7c 0…|…|…|…
0000007c02d88760 0000007ca79f3dd8 0000007ca79edb80 .=..|…….|…
0000007c02d88770 0000007c02d89000 b400007c13c04680 ….|….F..|…
0000007c02d88780 0000000000000000 0000000000000002 …………….
0000007c02d88790 b400007c13c67c00 0000000000000000 .|..|………..
0000007c02d887a0 0000000000000000 b400007c13c7c100 …………|…
0000007c02d887b0 0000007c02d88830 0000007ca796d420 0…|… …|…
0000007c02d887c0 0000007c02d88830 0000007ca796d460 0…|…`…|…

memory near sp ([anon:stack_and_tls:11981]):
0000007c02d886b0 0000000000000018 0000007caae98c58 ……..X…|…
0000007c02d886c0 0000007c02d886f0 0000007c95de6754 ….|…Tg..|…
0000007c02d886d0 b400007c13c246d0 0000000001909705 .F..|………..
0000007c02d886e0 0000007c02d88700 6f2ab3b40fa2f8ef ….|………*o*
0000007c02d886f0 0000007c02d88750 0000007ca133f8e0 P…|…..3.|…*
0000007c02d88700 0000000000000002 0000000000000000 …………….*
0000007c02d88710 0000000000000415 0000000001909705 …………….*
0000007c02d88720 0000000000000000 0000007c02d88808 …………|…*
0000007c02d88730 b400007c13c67c00 0000000000000000 .|..|………..*
0000007c02d88740 0000007c02d89000 6f2ab3b40fa2f8ef ….|………*o
0000007c02d88750 0000007c02d88830 0000007ca796ee7c 0…|…|…|…
0000007c02d88760 0000007ca79f3dd8 0000007ca79edb80 .=..|…….|…
0000007c02d88770 0000007c02d89000 b400007c13c04680 ….|….F..|…
0000007c02d88780 0000000000000000 0000000000000002 …………….
0000007c02d88790 b400007c13c67c00 0000000000000000 .|..|………..
0000007c02d887a0 0000000000000000 b400007c13c7c100 …………|…

memory near pc ([anon:stack_and_tls:11981]):
0000007c02d886d0 b400007c13c246d0 0000000001909705 .F..|………..
0000007c02d886e0 0000007c02d88700 6f2ab3b40fa2f8ef ….|………*o*
0000007c02d886f0 0000007c02d88750 0000007ca133f8e0 P…|…..3.|…*
0000007c02d88700 0000000000000002 0000000000000000 …………….*
0000007c02d88710 0000000000000415 0000000001909705 …………….*
0000007c02d88720 0000000000000000 0000007c02d88808 …………|…*
0000007c02d88730 b400007c13c67c00 0000000000000000 .|..|………..*
0000007c02d88740 0000007c02d89000 6f2ab3b40fa2f8ef ….|………*o
0000007c02d88750 0000007c02d88830 0000007ca796ee7c 0…|…|…|…
0000007c02d88760 0000007ca79f3dd8 0000007ca79edb80 .=..|…….|…
0000007c02d88770 0000007c02d89000 b400007c13c04680 ….|….F..|…
0000007c02d88780 0000000000000000 0000000000000002 …………….
0000007c02d88790 b400007c13c67c00 0000000000000000 .|..|………..
0000007c02d887a0 0000000000000000 b400007c13c7c100 …………|…
0000007c02d887b0 0000007c02d88830 0000007ca796d420 0…|… …|…
0000007c02d887c0 0000007c02d88830 0000007ca796d460 0…|…`…|…

内存映射表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
bash复制代码memory map (1146 entries):
0000005f'fabc7000-0000005f'fabc7fff r-- 0 1000 /system/bin/mediaserver64
0000005f'fabc8000-0000005f'fabc9fff r-x 1000 2000 /system/bin/mediaserver64
0000005f'fabca000-0000005f'fabcafff r-- 3000 1000 /system/bin/mediaserver64
0000007b'e79a3000-0000007b'e7d93fff --- 0 3f1000

0000007c'a120e000-0000007c'a128efff r-- 0 81000 /system/lib64/libstagefright.so
0000007c'a128f000-0000007c'a13c0fff r-x 81000 132000 /system/lib64/libstagefright.so
0000007c'a13c1000-0000007c'a13cffff r-- 1b3000 f000 /system/lib64/libstagefright.so
0000007c'a13d0000-0000007c'a13d1fff rw- 1c1000 2000 /system/lib64/libstagefright.so

0000007c'a787c000-0000007c'a78f4fff r-- 0 79000 /system/lib64/libmediaplayerservice.so
0000007c'a78f5000-0000007c'a79ecfff r-x 79000 f8000 /system/lib64/libmediaplayerservice.so
0000007c'a79ed000-0000007c'a79f8fff r-- 171000 c000 /system/lib64/libmediaplayerservice.so
0000007c'a79f9000-0000007c'a79f9fff rw- 17c000 1000 /system/lib64/libmediaplayerservice.so

FD信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
javascript复制代码open files:
fd 0: /dev/null (unowned)
fd 1: /dev/null (unowned)
fd 2: /dev/null (unowned)
fd 3: socket:[62562] (unowned)
fd 4: /dev/binderfs/binder (unowned)
fd 5: /dev/binderfs/hwbinder (unowned)
fd 6: /sys/kernel/tracing/trace_marker (unowned)
fd 7: /dev/ashmem4945d9b6-db30-413c-88c5-e50674f154c7 (unowned)
fd 8: /dmabuf: (unowned)
fd 9: /dev/ashmem4945d9b6-db30-413c-88c5-e50674f154c7 (unowned)
fd 10: /storage/emulated/0/zapya/folder/华语音乐/IN-K&王忻辰&苏星婕 - 落日与晚风.mp3 (owned by unique_fd 0x7c13c7a498)
fd 11: /dev/ashmem4945d9b6-db30-413c-88c5-e50674f154c7 (unowned)
...

Coredump

  前面 tombstone 文件内容,可以了解到它的信息很有限,当我们需要更多的内存信息时,它无法满足我们,这时 coredump 就尤为重要了,它可以按我们配置抓取相应的内存信息,关于 core 的介绍,见:
man7.org/linux/man-p…

AOSP方法

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
bash复制代码# build/envsetup.sh
# coredump_setup - enable core dumps globally for any process
# that has the core-file-size limit set correctly
#
# NOTE: You must call also coredump_enable for a specific process
# if its core-file-size limit is not set already.
# NOTE: Core dumps are written to ramdisk; they will not survive a reboot!

function coredump_setup()
{
echo "Getting root...";
adb root;
adb wait-for-device;

echo "Remounting root partition read-write...";
adb shell mount -w -o remount -t rootfs rootfs;
sleep 1;
adb wait-for-device;
adb shell mkdir -p /cores;
adb shell mount -t tmpfs tmpfs /cores;
adb shell chmod 0777 /cores;

echo "Granting SELinux permission to dump in /cores...";
adb shell restorecon -R /cores;

echo "Set core pattern.";
adb shell 'echo /cores/core.%p > /proc/sys/kernel/core_pattern';

echo "Done."
}

# coredump_enable - enable core dumps for the specified process
# $1 = PID of process (e.g., $(pid mediaserver))
#
# NOTE: coredump_setup must have been called as well for a core
# dump to actually be generated.

function coredump_enable()
{
local PID=$1;
if [ -z "$PID" ]; then
printf "Expecting a PID!\n";
return;
fi;
echo "Setting core limit for $PID to infinite...";
adb shell /system/bin/ulimit -P $PID -c unlimited
}

常用方法

  给 system_server 配置 coredump 参数,由于目标进程的 coredump 生成的目录受 selinux 权限限制,因此这种方法配置抓 coredump 的方法要注意目标进程对哪些目录文件有读写的 selinux 权限,再配置相应的目录。

1
2
3
4
5
6
7
8
9
bash复制代码adb wait-for-device
adb root
adb shell mkdir /data/cores
adb shell chmod 777 /data/cores
#adb shell setenforce 0
adb shell restorecon -R /data/cores
adb shell 'echo /data/cores/core.%e.%p > /proc/sys/kernel/core_pattern'
adb shell 'system/bin/ulimit -P `pidof system_server` -c unlimited'
#adb shell 'echo 2 > /proc/sys/fs/suid_dumpable'

  注意:确定问题与selinux权限无关,可以通过adb shell setenforce 0关闭selinux权限

  给 com.android.settings 配置抓取 coredump 的参数,由于前面的配置中 /data/cores 目录恢复 selinux 权限后如下:

  drwxrwxrwx 2 root root u:object_r:system_data_file:s0 3452 2022-07-04 15:08 cores

  我们知道 app 一定有权限对自身的 /data/data/$PACKAGE/ 目录下的文件具有读写权限,于是可以配置成如下参数:

1
2
3
4
5
6
7
8
kotlin复制代码adb wait-for-device
adb root
adb shell mkdir /data/data/com.android.settings/cores
adb shell chmod 777 /data/data/com.android.settings/cores
adb shell restorecon -R /data/data/com.android.settings/cores
adb shell 'echo /data/data/com.android.settings/cores/core.%e.%p > /proc/sys/kernel/core_pattern'
adb shell 'system/bin/ulimit -P `pidof com.android.settings` -c unlimited'
#adb shell 'echo 2 > /proc/sys/fs/suid_dumpable'
1
2
3
shell复制代码当我们在机器上验证对这个app进行kill -11模拟时
$ kill -11 `pidof com.android.settings`
$ ls /data/data/com.android.settings/cores/core.ndroid.settings.27946

参数说明

  coredump_filter 进程默认值是0x23,只抓取:私有匿名/共享匿名/私有大尺寸页,需要抓全部内存信息则 adb shell ‘echo 0x27 > /proc/$PID/coredump_filter’ 即可。

节点 参数
/proc/$PID/coredump_filter bit0: 私有匿名
bit1: 共享匿名
bit2: 有底层文件的私有映射
bit3: 有底层文件共享映射
bit4: ELF头
bit5: 私有大尺寸页
bit6: 共享大尺寸页

  core_pattern 控制生成 core 的文件名,以及输出的 core 的位置。例如:

  adb shell ‘echo /data/cores/core.%p > /proc/sys/kernel/core_pattern’

节点 参数
/proc/sys/kernel/core_pattern %p: 添加pid
%u: 添加当前uid
%g: 添加当前gid
%s: 添加导致产生core的信号
%t: 添加core文件生成时的unix时间
%h: 添加主机名
%e: 添加命令名
%E: 可执行文件的路径名,用斜杠(’/’)替换为感叹号(’!’)。

  当程序调用了 seteuid()/setegid() 改变进程的有效用户或组,则在默认情况下系统不会为这些进程生成 core,此时你可能需要调整 suid_dumpable 参数进入调试模式或安全模式下进行。

节点 参数
/proc/sys/fs/suid_dumpable 0:默认模式
1:调试模式
2:安全模式

文件格式

  core 文件也是ELF文件的一种,因此它的主体格式组成部分与 ELF 文件相同,以案例讲解的 core 文件为例,它主要组成部分为 /proc/self/maps 下的VMA以及各个线程寄存器。其中寄存器信息存放在PT_NOTE,各VMA存放在 PT_LOAD,当被过滤掉的 VMA,它只有 Program Header 描述,没有对应的 segment,也就是对应的段的 p_filesz = 0x0。

UML diagram (16).jpg

UML diagram (5).jpg

离线调试

  注:MTK 平台的 MINIDUMP 也是 coredump 的一种,它所保存的内存信息有限,core 的分析可以使用 GDB、lldb 等调试工具,如何使用这些调试工具,这里就不一一介绍。

1
2
3
4
5
6
7
bash复制代码$ ~/work/debug/gdb_arm64/gdb-12.1/output/bin/aarch64-linux-gdb
当我们没有符号表时,仅加载 core-file 也是可以使用的。
(gdb) core-file PROCESS_MINIDUMP
当我们有相应的符号表时,即可加载符号表目录
(gdb) set solib-search-path symbols/
或者
(gdb) set sysroot symbols/
命令 用途
(gdb) info sharedlibrary 显示所有共享库的地址范围
(gdb) info registers 显示当前线程的当前帧寄存器信息
(gdb) info locals 显示当前帧的局部变量
(gdb) info thread 显示有哪些线程
(gdb) thread 2 切换到2号线程上
(gdb) bt 显示当前线程的堆栈
(gdb) thread apply all [command]例如打印所有线程堆栈(gdb) thread apply all bt 让所有线程做同样的命令
(gdb) frame 显示当前帧信息
(gdb) frame 3 切换到#3帧
(gdb) print 或 (gdb) p 打印变量
(gdb) ptype ‘android::AHandler’ 查看某个class或struct的数据结构
(gdb) ptype /o ‘android::AHandler’ 查看数据类型占多少字节
(gdb) set print pretty on 格式化输出
(gdb) set log on 保存gdb输出的结果
(gdb) x /gx 0x7c02d886f0 读取地址0x7c02d886f0内存内容,其中输出格式如下:o(octal), x(hex), d(decimal),u(unsigned decimal), t(binary),f(float), a(address), i(instruction),c(char), s(string),z(hex, zero padded on the left).
(gdb) disassemble 0x0000007c95de6708或(gdb) disassemble ‘android::AMessage::setTarget’ 显示函数汇编信息

内存检测机制

ASAN

  在 Android 11 之后的 AOSP master 中,弃用了 arm64 上的平台开发 ASAN,改为使用 HWASAN,AddressSanitizer (ASAN) 是一种基于编译器的快速检测工具,用于检测内存错误。

类型 意义
Stack and heap buffer overflow/underflow 堆栈和堆缓冲区上溢/下溢
Heap use after free 使用已释放的内存
Stack use outside scope 超出栈范围
Double free/wild free 多次释放内存/错误释放

HWASAN

  HWASan 仅适用于 Android 10 及更高版本,且只能用于 AArch64 硬件,具备 ASAN 同样的检测能力,在 Linux-4.14 版本以上支持 tagged-pointers才能使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
makefile复制代码编译Android版本时带入环境变量如下:
$ export SANITIZE_TARGET=hwaddress
跳过某个模块,在相应模块下的Android.bp文件下添加内容如下:
sanitize: {
hwaddress: false,
address: false,
},

Android.mk则添加内容如下:
LOCAL_NOSANITIZE := hwaddress

APP构建支持HWASAN则在Application.mk下添加内容如下:
APP_STL := c++_shared
APP_CFLAGS := -fsanitize=hwaddress -fno-omit-frame-pointer
APP_LDFLAGS := -fsanitize=hwaddress

MTE

  最新 Android S 上引入的 ARM Memory Tagging Extension (MTE),MTE 的原理和 HWASan 类似,最大的区别在于 HWASan 需要重新编译,在所有内存访问前插桩相应的检测函数来实现,而 MTE 则在读写指令内部完成检测,完全由硬件支持。
更多内容转至掘金大佬-芦半山:

  juejin.cn/post/684490…

  juejin.cn/post/701359…

野指针的危害

  当所指向的对象被释放或者收回,但是对该指针没有作任何的修改,以至于该指针仍旧指向已经回收的内存地址,此情况下该指针便称 Wild pointer (野指针),如果这个野指针所指向的内存被分配给其它指针,而这个野指针仍在使用,程序将难以预测。

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
arduino复制代码#include <stdio.h>
class A {
public:
virtual ~A() = default;
virtual void foo() {
printf("A:%ld\n", a);
}

long a;
};

class B {
public:
virtual ~B() = default;
virtual void foo() {
printf("B:%ld\n", b);
}

long b;
};

int main(int /*argc*/, char** /*argv[]*/) {
A *a = new A();
A *a_bak = a;
a->a = 1000L;
printf("A ptr = %p\n", a);
delete a; // 此时a指针已经被free掉,那么指针a_bak是个野指针
B *b = new B();
printf("B ptr = %p\n", b);
b->b = 2000L;
b->foo();
a_bak->foo(); // 此处程序会运行怎么结果
delete b;
return 0;
}

  以上程序会怎么输出,由于 B 与 A 的数据结构大小一致,运行在同一个线程,极大可能会分配刚释放的指针地址,因此这个程序最后指针 b 与指针 a_bak 是同一个。

1
2
3
4
5
ini复制代码# ./data/Tester64
A ptr = 0xb400007690205010
B ptr = 0xb400007690205010
B:2000
B:2000

  像以上这个结果,那程序如下改写,那么可怕的事情就会出现,下面这个程序会报什么错。

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复制代码#include <stdio.h>
class A {
public:
long bad;
long a;
};

class B {
public:
virtual ~B() = default;
virtual void foo() {
b = 2000L;
printf("B:%ld\n", b);
}

long b;
};

int main(int /*argc*/, char** /*argv[]*/) {
A *a = new A();
A *a_bak = a;
printf("A ptr = %p\n", a);
delete a;
B *b = new B();
printf("B ptr = %p\n", b);
a_bak->bad = 0x20L;
b->foo();
delete b;
return 0;
}

  上面这个程序在27行处,bad 会将 B 的虚函数表破坏,导致28和29行在虚函数表上寻找 foo 函数与析构函数地址时发段错误。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
yaml复制代码Timestamp: 2022-07-06 14:47:50.925654058+0800
Process uptime: 0s
Cmdline: ./data/Tester64
pid: 12652, tid: 12652, name: Tester64 >>> ./data/Tester64 <<<
uid: 0
tagged_addr_ctrl: 0000000000000001
signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x28
Cause: null pointer dereference
x0 b40000762f005010 x1 b40000762f01b000 x2 0000000000000007 x3 ffffffffffffffff
x4 ffffffffffffffff x5 0000000040100401 x6 b40000762f01b006 x7 3637303030303462
x8 0000000000000020 x9 a454ef76eb4317d3 x10 0000000000004001 x11 0000000000000000
x12 0000000000000000 x13 0000000000000002 x14 0000000000000010 x15 0000000000000010
x16 000000762f5a0c58 x17 000000762f5910d4 x18 000000763745c000 x19 b40000762f005010
x20 b40000762f005010 x21 0000007fe173d378 x22 0000000000000001 x23 0000000000000000
x24 0000000000000000 x25 0000000000000000 x26 0000000000000000 x27 0000000000000000
x28 0000000000000000 x29 0000007fe173d2e0
lr 0000005ac14600c8 sp 0000007fe173d2e0 pc 0000005ac14600d0 pst 0000000060001000

backtrace:
#00 pc 00000000000010d0 /data/Tester64 (main+124)
#01 pc 000000000008436c /apex/com.android.runtime/lib64/bionic/libc.so (__libc_init+100)

  程序会出错还好,如果程序不会出错,仍继续运行,那么这个程序将变得很可怕,因为你不知道程序将会怎么运行,像下面这个程序,刻意改写控制程序往其它方向运行。

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
arduino复制代码#include <stdio.h>
class A {
public:
void *bad;
long a;
};

class B {
public:
virtual ~B() {
printf("delete B\n");
}

virtual void foo() {
b = 2000L;
printf("B:%ld\n", b);
}

long b;
};

void func1() {
printf("Hello !!\n");
}

void func2() {
printf("GoGoGo !!\n");
}

int main(int /*argc*/, char** /*argv[]*/) {
A *a = new A();
A *a_bak = a;
printf("A ptr = %p\n", a);
delete a;
B *b = new B();
printf("B ptr = %p\n", b);

long *data = new long[4] {0x0L, (long)func2, (long)func1, 0x0L};
a_bak->bad = data;

printf("Test .. \n");
b->foo();
delete b;
printf("Done.\n");
return 0;
}
1
2
3
4
5
6
7
ini复制代码# ./data/Tester64
A ptr = 0xb400007ce9605010
B ptr = 0xb400007ce9605010
Test ..
Hello !!
GoGoGo !!
Done.

数组越界的危害

  比起前面的野指针,数组越界大多数情况都能被 HWASAN 等内存检测发现,当然前面的野指针情况也是能被检测发现的,数组越界往往会出现某个对象的内存前半部分被污染,而后半部分数据正常的情况。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
arduino复制代码#include <stdio.h>
class A {
public:
long a = 0x55AA;
long b = 0xDEAD;
};

int main(int /*argc*/, char** /*argv[]*/) {
long *b = new long[2] {0x0L, 0x1L};
A *a = new A();
printf("A:%p\n", a);
printf("B:%p\n", b);
b[2] = 0xDEAD;
printf("B2:%p\n", &b[2]);
printf("0x%lx-0x%lx\n", a->a, a->b);
return 0;
}

  因为 b 的数据大小与对象 a 的数据大小相同,因此在这个程序刚启动分配指针地址时,基本会连在一块,因此 b[2] 越界行为将对象 a 的内容破坏,往往程序运行了很久,内存碎片化增加,就不知道 b[2] 越界操作会破坏哪个对象的内存。

1
2
3
4
5
makefile复制代码# ./data/Tester64
A:0xb400007fa7c05020
B:0xb400007fa7c05010
B2:0xb400007fa7c05020
0xdead-0xdead

机器码翻译

  本文案例中的 tombstone 文件 PC 跑飞,没有落在 text 段地址上,这里换一份 tombstone 方便说明,我们可以通过编译的方法生成对应的 ELF 文件,即可用 objdump 得到对应的汇编。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
bash复制代码signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x40
Cause: null pointer dereference
x0 0000000000000020 x1 b400007bf95f0e00 x2 63692f6863726165 x3 0000000000000008
x4 b400007bf95f0e74 x5 b400007bf8ea36f4 x6 656863732f676e69 x7 732f7269645f616d
x8 db4cf552ad46f717 x9 0000000000000001 x10 00000000000349e0 x11 0000007bc0000000
x12 0000000000000000 x13 0000000000034960 x14 b400007b58d7c4c0 x15 00000000374d2457
x16 0000007ca3543d50 x17 0000007ca35338ec x18 0000007b38c74000 x19 0000000000000020
x20 0000000000000000 x21 0000007b548e9000 x22 0000007b548e9000 x23 b400007bf946ffd0
x24 0000007b548e76c0 x25 0000007b548e9000 x26 0000007b548e7b30 x27 0000007b548e7b18
x28 0000007b548e7a10 x29 0000007b548e7410
lr 0000007a8e4e7980 sp 0000007b548e73e0 pc 0000007a8e4d0e00 pst 0000000060001000

memory near pc (/apex/com.android.appsearch/lib64/libicing.so):
0000007a8e4d0de0 a90557f6f90023f7 9100c3fda9064ff4 .#…W…O……
0000007a8e4d0df0 f94016c8d53bd056 f81f83a8aa0003f3 V.;…@………
0000007a8e4d0e00 350001c839408008 91181c42b0fffde2 ..@9…5….B…
0000007a8e4d0e10 2a1f03e1910023e0 910023f452808ee3 .#…..*…R.#..*
0000007a8e4d0e20 910022809400d695 912b742190fffde1 ….."……!t+.*

  编写汇编文件 code.S,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
复制代码.inst 0xf90023f7
.inst 0xa90557f6
.inst 0xa9064ff4
.inst 0x9100c3fd
.inst 0xd53bd056
.inst 0xf94016c8
.inst 0xaa0003f3
.inst 0xf81f83a8
.inst 0x39408008
.inst 0x350001c8
.inst 0xb0fffde2
.inst 0x91181c42
.inst 0x910023e0
.inst 0x2a1f03e1
.inst 0x52808ee3
.inst 0x910023f4
.inst 0x9400d695
.inst 0x91002280
.inst 0x90fffde1
.inst 0x912b7421
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
yaml复制代码编译:aarch64-linux-android-as code.S -o code.o
反汇编:aarch64-linux-android-objdump -d code.o

code.o: file format elf64-littleaarch64
Disassembly of section .text:
0000000000000000 <.text>:
0: f90023f7 str x23, [sp, #64]
4: a90557f6 stp x22, x21, [sp, #80]
8: a9064ff4 stp x20, x19, [sp, #96]
c: 9100c3fd add x29, sp, #0x30
10: d53bd056 mrs x22, tpidr_el0
14: f94016c8 ldr x8, [x22, #40]
18: aa0003f3 mov x19, x0
1c: f81f83a8 stur x8, [x29, #-8]
20: 39408008 ldrb w8, [x0, #32]
24: 350001c8 cbnz w8, 5c <.text+0x5c>
28: b0fffde2 adrp x2, fffffffffffbd000 <.text+0xfffffffffffbd000>
2c: 91181c42 add x2, x2, #0x607
30: 910023e0 add x0, sp, #0x8
34: 2a1f03e1 mov w1, wzr
38: 52808ee3 mov w3, #0x477 // #1143
3c: 910023f4 add x20, sp, #0x8
40: 9400d695 bl 35a94 <.text+0x35a94>
44: 91002280 add x0, x20, #0x8
48: 90fffde1 adrp x1, fffffffffffbc000 <.text+0xfffffffffffbc000>
4c: 912b7421 add x1, x1, #0xadd

由于x0=0x0000000000000020,因此在指令0x39408008( ldrb w8, [x0, #32]),会发生读取内存0x40的段错误。

汇编翻译

  有些时候我们可能需要手动修改 ELF 文件的机器码达到一些调试的目的,这时候我们就需要知道汇编对应的机器码,去查手册也比较麻烦,这时我们直接编写汇编文件即可。

编写汇编文件 code_asm.S,内容如下:

1
2
python复制代码str x30, [sp, #-16]!
str x29, [sp, #-16]!
1
2
3
4
5
6
7
8
python复制代码编译:aarch64-linux-android-as code_asm.S -o code_asm.o
反汇编:aarch64-linux-android-objdump -d code_asm.o

code_asm.o: file format elf64-littleaarch64
Disassembly of section .text:
0000000000000000 <.text>:
0: f81f0ffe str x30, [sp, #-16]!
4: f81f0ffd str x29, [sp, #-16]!

案例讲解

错误日志

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
yaml复制代码Timestamp: 2022-06-07 01:53:32.033409857+0800
Process uptime: 0s
Cmdline: mediaserver64
pid: 1139, tid: 11981, name: NPDecoder >>> mediaserver64 <<<
uid: 1013
tagged_addr_ctrl: 0000000000000001
signal 11 (SIGSEGV), code 2 (SEGV_ACCERR), fault addr 0x7c02d886f0
x0 79748c5e568e2ddc x1 0000007ca13c3618 x2 0000000000000000 x3 0000007ca1291000
x4 0000000001909705 x5 0000000000000000 x6 0000007c02d88808 x7 b60625655bf0252f
x8 0000000000000080 x9 0000007ca126fed7 x10 0000000000000006 x11 0000007bfd0a81fc
x12 9ef8a95ca9649dbe x13 e44782d5ac38720e x14 0000007bfd0a8030 x15 0000001e56307b5c
x16 0000007c95dfdb70 x17 0000007c9844f118 x18 0000007bfaa28000 x19 b400007c13c246d0
x20 0000007c02d88730 x21 b400007c13c67c00 x22 0000000000000415 x23 0000007c02d89000
x24 0000000000000002 x25 b400007c13c246d0 x26 b400007c13c67c00 x27 0000007c02d89000
x28 0000007ca13c2c28 x29 0000007c02d886f0
lr 0000007c02d886f0 sp 0000007c02d886d0 pc 0000007c02d886f0 pst 0000000080001000

backtrace:
#00 pc 00000000000f86f0 [anon:stack_and_tls:11981]

分析

直接原因
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
less复制代码0000007c'02c90000-0000007c'02d8bfff rw- 0 fc000 [anon:stack_and_tls:11981]
memory near pc ([anon:stack_and_tls:11981]):
0000007c02d886d0 b400007c13c246d0 0000000001909705 .F..|………..
0000007c02d886e0 0000007c02d88700 6f2ab3b40fa2f8ef ….|………*o*
0000007c02d886f0 0000007c02d88750 0000007ca133f8e0 P…|…..3.|…*
0000007c02d88700 0000000000000002 0000000000000000 …………….*
0000007c02d88710 0000000000000415 0000000001909705 …………….*

code.o: file format elf64-littleaarch64
Disassembly of section .text:
0000000000000000 <.text>:

0: 02d886f0 .inst 0x02d886f0 ; undefined
4: 0000007c udf #124

从 tombstone 的 backtrace 信息上看,很明显是PC跑飞了,跑到线程栈地址空间下,
栈内存地址不可运行,因此被拒绝访问而出错,如果有x的权限那么也是错误,会发生 SIGILL 指令错误
栈回溯

  从 tombstone 中的 x29 和 SP 寄存器附近的内存中进行栈回溯,由于大小受限,往往无法回溯到栈底,无法完全证明程序就是经过这几个函数。

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
arduino复制代码memory near x29 ([anon:stack_and_tls:11981]):
0000007c02d886d0 b400007c13c246d0 0000000001909705 .F..|………..【SP = 0x0000007c02d886d0】
0000007c02d886e0 0000007c02d88700 6f2ab3b40fa2f8ef ….|………*o*
0000007c02d886f0 0000007c02d88750 0000007ca133f8e0 P…|…..3.|…【x29 = 0x0000007c02d886f0】
0000007c02d88700 0000000000000002 0000000000000000 …………….*
0000007c02d88710 0000000000000415 0000000001909705 …………….*
0000007c02d88720 0000000000000000 0000007c02d88808 …………|…*
0000007c02d88730 b400007c13c67c00 0000000000000000 .|..|………..*
0000007c02d88740 0000007c02d89000 6f2ab3b40fa2f8ef ….|………*o
0000007c02d88750 0000007c02d88830 0000007ca796ee7c 0…|…|…|…
0000007c02d88760 0000007ca79f3dd8 0000007ca79edb80 .=..|…….|…
0000007c02d88770 0000007c02d89000 b400007c13c04680 ….|….F..|…
0000007c02d88780 0000000000000000 0000000000000002 …………….
0000007c02d88790 b400007c13c67c00 0000000000000000 .|..|………..
0000007c02d887a0 0000000000000000 b400007c13c7c100 …………|…
0000007c02d887b0 0000007c02d88830 0000007ca796d420 0…|… …|…
0000007c02d887c0 0000007c02d88830 0000007ca796d460 0…|…`…|…

这里我们通过 FP(x29) 回溯,如果这个 x29 是有效的,那么 x29 存放的是上一个 Caller 的 FP 寄存器,x29+0x8 的地址存放上一个 Caller 的 LR 寄存器。

0x0000007ca133f8e0 地址落在 /system/lib64/libstagefright.so 的 TEXT 段上。
0000007c'a120e000-0000007c'a128efff r-- 0 81000 /system/lib64/libstagefright.so
0000007c'a128f000-0000007c'a13c0fff r-x 81000 132000 /system/lib64/libstagefright.so
0000007c'a13c1000-0000007c'a13cffff r-- 1b3000 f000 /system/lib64/libstagefright.so
0000007c'a13d0000-0000007c'a13d1fff rw- 1c1000 2000 /system/lib64/libstagefright.so

[14] .text PROGBITS 0000000000081000 00081000
000000000012bed8 0000000000000000 AX 0 0 4096

Funcation_Offset = PC - TEXT_LOAD + .text_offset

(gdb) p /x 0x0000007ca133f8e0-0x7ca128f000+0x81000
$1 = 0x1318e0

(gdb) x /2i 0x1318e0-0x4
0x1318dc <_ZN7android10MediaCodec16queueInputBufferEmmmljPNS_7AStringE+200>: bl 0x1ad390 <_ZN7android8AMessageC1EjRKNS_2spIKNS_8AHandlerEEE@plt>
0x1318e0 <_ZN7android10MediaCodec16queueInputBufferEmmmljPNS_7AStringE+204>: eor x8, x29, x25

(gdb) p _ZN7android10MediaCodec16queueInputBufferEmmmljPNS_7AStringE+200
$2 = (android::status_t (*)(struct android::MediaCodec * const, size_t, size_t, size_t, int64_t, uint32_t,
struct android::AString *)) 0x1318dc <android::MediaCodec::queueInputBuffer(unsigned long, unsigned long, unsigned long, long, unsigned int, android::AString*)+200>

因此0x0000007ca133f8e0所在的函数是android::MediaCodec::queueInputBuffer。
一般我们直接 PC 减去库文件的起始地址即可得到函数的偏移地址。

即: 0x0000007ca133f8e0 - 0x0000007ca120e000

  最后这份 tombstone 只能通过 FP 推导出这两个函数,是否正确待取证,这也体现了 tombstone 的局限性,此时我们希望能完整的推导,并取证栈是完整性,需要使用到 coredump 文件进行。

FP LR 函数名
0x0000007c02d886f0 0x0000007c02d88750 0x0000007ca133f8e0 android::MediaCodec::queueInputBuffer
0x0000007c02d88750 0x0000007c02d88830 0x0000007ca796ee7c android::NuPlayer::Decoder::onInputBufferFetched
1
2
3
4
5
scss复制代码MTK 的 aee 会将保存一份 minidump ,将 minidump 装载到gdb上回溯。
core-file PROCESS_MINIDUMP
(gdb) bt
#0 0x0000007c02d886f0 in ?? ()
由此可见 GDB 也无法正确回溯这个栈,我们重复前面的步骤在完整的栈上进行计算,可以得到
当前FP Caller FP Caller LR Caller 函数名
0x7c02d886f0 0x0000007c02d88750 0x0000007ca133f8e0 android::MediaCodec::queueInputBuffer
0x7c02d88750 0x0000007c02d88830 0x0000007ca796ee7c android::NuPlayer::Decoder::onInputBufferFetched
0x7c02d88830 0x0000007c02d888a0 0x0000007ca796c984 android::NuPlayer::Decoder::doRequestBuffers
0x7c02d888a0 0x0000007c02d88920 0x0000007ca7963d48 android::NuPlayer::DecoderBase::onRequestInputBuffers
0x7c02d88920 0x0000007c02d88980 0x0000007ca79717bc android::NuPlayer::Decoder::handleAnInputBuffer
0x7c02d88980 0x0000007c02d88a20 0x0000007ca7969328 android::NuPlayer::Decoder::onMessageReceived
0x7c02d88a20 0x0000007c02d88a70 0x0000007c95de2874 android::AHandler::deliverMessage
0x7c02d88a70 0x0000007c02d88ad0 0x0000007c95de8608 android::AMessage::deliver
0x7c02d88ad0 0x0000007c02d88b30 0x0000007c95de3bbc android::ALooper::loop
0x7c02d88b30 0x0000007c02d88ba0 0x0000007ca105907c android::Thread::_threadLoop
0x7c02d88ba0 0x0000007c02d88c10 0x0000007ca105886c thread_data_t::trampoline
0x7c02d88c10 0x0000007c02d88c50 0x0000007caaf3be74 __pthread_start
0x7c02d88c50 0x0000007c02d88c80 0x0000007caaedb830 __start_thread

  这个线程栈内存能被回溯到 __start_thread 函数,基本可以确定这个栈是可信的,并且最后的 FP 地址存放了 Caller FP 与 Caller LR,并且从汇编的指令流程上是吻合的,可以确定最后的 FP 应该是某个函数的栈顶,记这个函数为 A 函数。

A 函数是谁?
1
2
3
4
5
6
7
8
9
10
11
12
makefile复制代码(gdb) x /12gx 0x7c02d886d0
0x7c02d886d0: 0xb400007c13c246d0 0x0000000001909705 // SP
0x7c02d886e0: 0x0000007c02d88700 0x6f2ab3b40fa2f8ef
此处是 A 函数的栈。
0x7c02d886f0: 0x0000007c02d88750 0x0000007ca133f8e0 // FP
0x7c02d88700: 0x0000000000000002 0x0000000000000000
0x7c02d88710: 0x0000000000000415 0x0000000001909705
0x7c02d88720: 0x0000000000000000 0x0000007c02d88808

当 A 函数运行完成后返回 LR 地址 0x0000007ca133f8e0 (*android* *::MediaCodec::queueInputBuffer)*

注:以下“_ZN7android10MediaCodec16queueInputBufferEmmmljPNS_7AStringE” 用 "android::MediaCodec::queueInputBuffer" 替代
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
ruby复制代码(gdb) disassemble 0x0000007ca133f8e0-0x20,+0x30
Dump of assembler code from 0x7ca133f8c0 to 0x7ca133f8f0:
0x0000007ca133f8c0 <android::MediaCodec::queueInputBuffer+172>: mov x1, sp
0x0000007ca133f8c4 <android::MediaCodec::queueInputBuffer+176>: mov x0, x26
0x0000007ca133f8c8 <android::MediaCodec::queueInputBuffer+180>: bl 0x7ca13bb130 <_ZNK7android7RefBase9incStrongEPKv@plt>
0x0000007ca133f8cc <android::MediaCodec::queueInputBuffer+184>: mov x2, sp
0x0000007ca133f8d0 <android::MediaCodec::queueInputBuffer+188>: mov x0, x25
0x0000007ca133f8d4 <android::MediaCodec::queueInputBuffer+192>: mov w1, #0x6549 // #25929
0x0000007ca133f8d8 <android::MediaCodec::queueInputBuffer+196>: movk w1, #0x7175, lsl #16
0x0000007ca133f8dc <android::MediaCodec::queueInputBuffer+200>: bl 0x7ca13bb390 <_ZN7android8AMessageC1EjRKNS_2spIKNS_8AHandlerEEE@plt>
0x0000007ca133f8e0 <android::MediaCodec::queueInputBuffer+204>: eor x8, x29, x25
0x0000007ca133f8e4 <android::MediaCodec::queueInputBuffer+208>: cmp x8, #0xfff
0x0000007ca133f8e8 <android::MediaCodec::queueInputBuffer+212>: str x25, [sp, #8]
0x0000007ca133f8ec <android::MediaCodec::queueInputBuffer+216>: b.hi 0x7ca133f8f4 <_ZN7android10MediaCodec16queueInputBufferEmmmljPNS_7AStringE+224> // b.pmore
End of assembler dump.

(gdb) p _ZN7android10MediaCodec16queueInputBufferEmmmljPNS_7AStringE+180
$59 = (android::status_t (*)(android::MediaCodec * const, size_t, size_t, size_t, int64_t, uint32_t,
android::AString *)) 0x7ca133f8c8 <* *android::MediaCodec::queueInputBuffer(unsigned long, unsigned long, unsigned long, long, unsigned int, android::AString*)+180>

这个 A 函数是 android::AMessage::AMessage(unsigned int, android::sp<android::AHandler const> const&)?

  A 函数会是 AMessage 的构造方法吗?

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
less复制代码(gdb) disassemble 0x7ca13bb390
Dump of assembler code for function _ZN7android8AMessageC1EjRKNS_2spIKNS_8AHandlerEEE@plt:
0x0000007ca13bb390 <+0>: adrp x16, 0x7ca13cd000
0x0000007ca13bb394 <+4>: ldr x17, [x16, #992]
0x0000007ca13bb398 <+8>: add x16, x16, #0x3e0
0x0000007ca13bb39c <+12>: br x17
End of assembler dump.

(gdb) x /gx 0x7ca13cd000+0x3e0
0x7ca13cd3e0 <_ZN7android8AMessageC1EjRKNS_2spIKNS_8AHandlerEEE@got.plt>: 0x0000007c95de66a8

(gdb) disassemble 0x0000007c95de66a8
Dump of assembler code for function _ZN7android8AMessageC2EjRKNS_2spIKNS_8AHandlerEEE:
0x0000007c95de66a8 <+0>: stp x29, x30, [sp, #-48]!
0x0000007c95de66ac <+4>: str x21, [sp, #16]
0x0000007c95de66b0 <+8>: stp x20, x19, [sp, #32]
0x0000007c95de66b4 <+12>: mov x29, sp
0x0000007c95de66b8 <+16>: mov x19, x2
0x0000007c95de66bc <+20>: mov w20, w1
0x0000007c95de66c0 <+24>: mov x21, x0
0x0000007c95de66c4 <+28>: bl 0x7c95dfb490 <_ZN7android7RefBaseC2Ev@plt>
0x0000007c95de66c8 <+32>: movi v0.2d, #0x0
0x0000007c95de66cc <+36>: adrp x8, 0x7c95dfd000 <__dso_handle_const>
0x0000007c95de66d0 <+40>: ldr x8, [x8, #2744]
0x0000007c95de66d4 <+44>: str w20, [x21, #16]
0x0000007c95de66d8 <+48>: stur q0, [x21, #24]
0x0000007c95de66dc <+52>: add x8, x8, #0x10
0x0000007c95de66e0 <+56>: stur q0, [x21, #40]
0x0000007c95de66e4 <+60>: mov x0, x21
0x0000007c95de66e8 <+64>: mov x1, x19
0x0000007c95de66ec <+68>: stur q0, [x21, #56]
0x0000007c95de66f0 <+72>: str x8, [x21]
0x0000007c95de66f4 <+76>: str xzr, [x21, #72]
0x0000007c95de66f8 <+80>: ldp x20, x19, [sp, #32]
0x0000007c95de66fc <+84>: ldr x21, [sp, #16]
0x0000007c95de6700 <+88>: ldp x29, x30, [sp], #48
0x0000007c95de6704 <+92>: b 0x7c95dfb950 <_ZN7android8AMessage9setTargetERKNS_2spIKNS_8AHandlerEEE@plt>
End of assembler dump.

  这个函数的栈大小为0x30,并且 FP=SP,如果A函数是 AMessage 的构造函数,那么在该函数未退栈时 FP=SP 不成立。

继续往前看栈上内存信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
makefile复制代码0x7c02d88600: 0xb400007c13c24bd0 0x6f2ab3b40fa2f8ef
0x7c02d88610: 0x0000007c02d88660 0x0000007c95de6510
0x7c02d88620: 0x0000007c02d89000 0x6f2ab3b40fa2f8ef
0x7c02d88630: 0x0000007c02d88670 0x0000007caae98c58
0x7c02d88640: 0x0000007c02d886b0 0x0000007caae9ce40
0x7c02d88650: 0x0000000000000005 0x6f2ab3b40fa2f8ef
0x7c02d88660: 0x0000007c02d886a0 0x0000007caae98c58
0x7c02d88670: 0x0000000000000002 0x0000000000000000
0x7c02d88680: 0x0000000000000415 0xb400007c13c246d0
0x7c02d88690: 0x0000000071756549 0x0000000000000018
0x7c02d886a0: 0x0000007c02d886c0 0x0000007c9dccfb74 operator new(unsigned long)
0x7c02d886b0: 0x0000000000000018 0x0000007caae98c58
0x7c02d886c0: 0x0000007c02d886f0 0x0000007c95de6754
1
2
3
4
5
6
7
8
9
10
11
12
makefile复制代码0x7c02d886d0: 0xb400007c13c246d0 0x0000000001909705 /SP
0x7c02d886e0: 0x0000007c02d88700 0x6f2ab3b40fa2f8ef
此处是 A 函数的栈。
0x7c02d886f0: 0x0000007c02d88750 0x0000007ca133f8e0 // FP
0x7c02d88700: 0x0000000000000002 0x0000000000000000
0x7c02d88710: 0x0000000000000415 0x0000000001909705
0x7c02d88720: 0x0000000000000000 0x0000007c02d88808

地址 0x7c02d886c0 存放的内存正好指向 A 函数 FP 地址,因此这里很可能是 A 函数的下一个函数的栈
函数栈{x29,x30} = {0x0000007c02d886f0,0x0000007c95de6754}

注:以下将字符串"_ZN7android8AMessage9setTargetERKNS_2spIKNS_8AHandlerEEE"更替为"android::AMessage::setTarget"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
ruby复制代码(gdb) disassemble 0x0000007c95de6754-0x20,+0x40
Dump of assembler code from 0x7c95de6734 to 0x7c95de6774:
0x0000007c95de6734 <android::AMessage::setTarget+44>: cbz x21, 0x7c95de6880 <android::AMessage::setTarget+376>
0x0000007c95de6738 <android::AMessage::setTarget+48>: mov x20, x1
0x0000007c95de673c <android::AMessage::setTarget+52>: ldr x1, [x21]
0x0000007c95de6740 <android::AMessage::setTarget+56>: mov x0, #0x2ddc // #11740
0x0000007c95de6744 <android::AMessage::setTarget+60>: movk x0, #0x568e, lsl #16
0x0000007c95de6748 <android::AMessage::setTarget+64>: movk x0, #0x8c5e, lsl #32
0x0000007c95de674c <android::AMessage::setTarget+68>: movk x0, #0x7974, lsl #48
0x0000007c95de6750 <android::AMessage::setTarget+72>: bl 0x7c95dfb370 <__cfi_slowpath@plt>
0x0000007c95de6754 <android::AMessage::setTarget+76>: ldr w8, [x21, #16]
0x0000007c95de6758 <android::AMessage::setTarget+80>: str w8, [x19, #20]
0x0000007c95de675c <android::AMessage::setTarget+84>: ldr x21, [x20]
0x0000007c95de6760 <android::AMessage::setTarget+88>: ldr x1, [x21]
0x0000007c95de6764 <android::AMessage::setTarget+92>: mov x0, #0x2ddc // #11740
0x0000007c95de6768 <android::AMessage::setTarget+96>: movk x0, #0x568e, lsl #16
0x0000007c95de676c <android::AMessage::setTarget+100>: movk x0, #0x8c5e, lsl #32
0x0000007c95de6770 <android::AMessage::setTarget+104>: movk x0, #0x7974, lsl #48
End of assembler dump.

(gdb) p _ZN7android8AMessage9setTargetERKNS_2spIKNS_8AHandlerEEE+72
$64 = (void (*)(android::AMessage * const, const android::sp<android::AHandler const> &)) 0x7c95de6750 <android::AMessage::setTarget(android::sp<android::AHandler const> const&)+72>

  A函数会是android::AMessage::setTarget?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
ruby复制代码(gdb) disassemble 0x0000007c95de6708,+0x30
Dump of assembler code from 0x7c95de6708 to 0x7c95de6738:
0x0000007c95de6708 <android::AMessage::setTarget+0>: sub sp, sp, #0x60
0x0000007c95de670c <android::AMessage::setTarget+4>: stp x29, x30, [sp, #32]
0x0000007c95de6710 <android::AMessage::setTarget+8>: stp x24, x23, [sp, #48]
0x0000007c95de6714 <android::AMessage::setTarget+12>: stp x22, x21, [sp, #64]
0x0000007c95de6718 <android::AMessage::setTarget+16>: stp x20, x19, [sp, #80]
0x0000007c95de671c <android::AMessage::setTarget+20>: add x29, sp, #0x20
0x0000007c95de6720 <android::AMessage::setTarget+24>: mrs x23, tpidr_el0
0x0000007c95de6724 <android::AMessage::setTarget+28>: ldr x8, [x23, #40]
0x0000007c95de6728 <android::AMessage::setTarget+32>: stur x8, [x29, #-8]
0x0000007c95de672c <android::AMessage::setTarget+36>: ldr x21, [x1]
0x0000007c95de6730 <android::AMessage::setTarget+40>: mov x19, x0
0x0000007c95de6734 <android::AMessage::setTarget+44>: cbz x21, 0x7c95de6880 <android::AMessage::setTarget+376>

  从汇编看函数栈正是0x60与A函数的栈相等。
回去再看看 AMessage::AMessage 的汇编,可以发现它是退栈后跳转到 android::AMessage::setTarget。

1
2
less复制代码0x0000007c95de6700 <+88>: ldp x29, x30, [sp], #48
0x0000007c95de6704 <+92>: b 0x7c95dfb950 <_ZN7android8AMessage9setTargetERKNS_2spIKNS_8AHandlerEEE@plt>
1
2
3
4
5
6
7
8
9
复制代码x0 79748c5e568e2ddc x1 0000007ca13c3618 x2 0000000000000000 x3 0000007ca1291000
x4 0000000001909705 x5 0000000000000000 x6 0000007c02d88808 x7 b60625655bf0252f
x8 0000000000000080 x9 0000007ca126fed7 x10 0000000000000006 x11 0000007bfd0a81fc
x12 9ef8a95ca9649dbe x13 e44782d5ac38720e x14 0000007bfd0a8030 x15 0000001e56307b5c
x16 0000007c95dfdb70 x17 0000007c9844f118 x18 0000007bfaa28000 x19 b400007c13c246d0
x20 0000007c02d88730 x21 b400007c13c67c00 x22 0000000000000415 x23 0000007c02d89000
x24 0000000000000002 x25 b400007c13c246d0 x26 b400007c13c67c00 x27 0000007c02d89000
x28 0000007ca13c2c28 x29 0000007c02d886f0
lr 0000007c02d886f0 sp 0000007c02d886d0 pc 0000007c02d886f0 pst 0000000080001000

  再回看 setTarget 汇编刚好 x0=0x79748c5e568e2ddc,

1
2
3
4
5
ruby复制代码0x0000007c95de6740 <android::AMessage::setTarget+56>: mov x0, #0x2ddc // #11740
0x0000007c95de6744 <android::AMessage::setTarget+60>: movk x0, #0x568e, lsl #16
0x0000007c95de6748 <android::AMessage::setTarget+64>: movk x0, #0x8c5e, lsl #32
0x0000007c95de674c <android::AMessage::setTarget+68>: movk x0, #0x7974, lsl #48
0x0000007c95de6750 <android::AMessage::setTarget+72>: bl 0x7c95dfb370 <__cfi_slowpath@plt>

  x16 = 0x0000007c95dfdb70与x17= 0x0000007c9844f118 也满足tombstone信息。

1
2
3
4
5
6
7
8
9
10
sql复制代码(gdb) disassemble 0x7c95dfb370
Dump of assembler code for function __cfi_slowpath@plt:
0x0000007c95dfb370 <+0>: adrp x16, 0x7c95dfd000 <__dso_handle_const>
0x0000007c95dfb374 <+4>: ldr x17, [x16, #2928]
0x0000007c95dfb378 <+8>: add x16, x16, #0xb70
0x0000007c95dfb37c <+12>: br x17
End of assembler dump.

(gdb) x /gx 0x7c95dfd000+0xb70
0x7c95dfdb70 <__cfi_slowpath@got.plt>: 0x0000007c9844f118
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
less复制代码(gdb) disassemble 0x0000007c9844f118
Dump of assembler code for function __cfi_slowpath(uint64_t, void *):*
0x0000007c9844f118 <+0>: stp x29, x30, [sp, #-16]!*
0x0000007c9844f11c <+4>: mov x29, sp*
0x0000007c9844f120 <+8>: lsr x8, x1, #17*
0x0000007c9844f124 <+12>: and x8, x8, #0x7ffffffffe*
0x0000007c9844f128 <+16>: mov w9, #0x80000000 // #-2147483648*
0x0000007c9844f12c <+20>: cmp x8, x9*
0x0000007c9844f130 <+24>: b.hi 0x7c9844f14c <__cfi_slowpath(uint64_t, void*)+52> // b.pmore
0x0000007c9844f134 <+28>: adrp x9, 0x7c98452000 <_ZL19shadow_base_storage>
0x0000007c9844f138 <+32>: ldr x9, [x9]
0x0000007c9844f13c <+36>: ldrh w8, [x9, x8]
0x0000007c9844f140 <+40>: cmp w8, #0x1
0x0000007c9844f144 <+44>: b.eq 0x7c9844f15c <__cfi_slowpath(uint64_t, void *)+68> // b.none*
0x0000007c9844f148 <+48>: cbnz w8, 0x7c9844f164 <__cfi_slowpath(uint64_t, void*)+76>
0x0000007c9844f14c <+52>: xpaclri
0x0000007c9844f150 <+56>: mov x2, xzr
0x0000007c9844f154 <+60>: mov x3, x30
0x0000007c9844f158 <+64>: bl 0x7c9844f2a0 <__loader_cfi_fail@plt>
0x0000007c9844f15c <+68>: ldp x29, x30, [sp], #16
0x0000007c9844f160 <+72>: ret
0x0000007c9844f164 <+76>: and x9, x1, #0xfffffffffffc0000
0x0000007c9844f168 <+80>: sub x8, x9, x8, lsl #12
0x0000007c9844f16c <+84>: add x3, x8, #0x42, lsl #12
0x0000007c9844f170 <+88>: mov x2, xzr
0x0000007c9844f174 <+92>: ldp x29, x30, [sp], #16
0x0000007c9844f178 <+96>: br x3
End of assembler dump.

因此 A 函数的栈是 android::AMessage::setTarget(android::sp<android::AHandler const> const&)
计算汇编走向

  根据 tombstone 最后的栈地址 SP=0x0000007c02d886d0 说明函数 __cfi_slowpath(uint64_t, void ) 已经退栈。

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
makefile复制代码0x7c02d88620: 0x0000007c02d89000 0x6f2ab3b40fa2f8ef
0x7c02d88630: 0x0000007c02d88670 0x0000007caae98c58
0x7c02d88640: 0x0000007c02d886b0 0x0000007caae9ce40
0x7c02d88650: 0x0000000000000005 0x6f2ab3b40fa2f8ef
je_malloc
0x7c02d88660: 0x0000007c02d886a0 0x0000007caae98c58
0x7c02d88670: 0x0000000000000002 0x0000000000000000
0x7c02d88680: 0x0000000000000415 0xb400007c13c246d0
0x7c02d88690: 0x0000000071756549 0x0000000000000018

malloc(size_t)
0x7c02d886a0: 0x0000007c02d886c0 0x0000007c9dccfb74 operator new(unsigned long)
0x7c02d886b0: 0x0000000000000018 0x0000007caae98c58

__cfi_slowpath(uint64_t, void*)
0x7c02d886c0: 0x0000007c02d886f0 0x0000007c95de6754
0x7c02d886d0: 0xb400007c13c246d0 0x0000000001909705
0x7c02d886e0: 0x0000007c02d88700 0x6f2ab3b40fa2f8ef

android::AMessage::setTarget(android::sp<android::AHandler const> const&)
0x7c02d886f0: 0x0000007c02d88750 0x0000007ca133f8e0
0x7c02d88700: 0x0000000000000002 0x0000000000000000
0x7c02d88710: 0x0000000000000415 0x0000000001909705
0x7c02d88720: 0x0000000000000000 0x0000007c02d88808
0x7c02d88730: 0xb400007c13c67c00 0x0000000000000000
0x7c02d88740: 0x0000007c02d89000 0x6f2ab3b40fa2f8ef

android::MediaCodec::queueInputBuffer(unsigned long, unsigned long, unsigned long, long, unsigned int, android::AString*)
0x7c02d88750: 0x0000007c02d88830 0x0000007ca796ee7c
0x7c02d88760: 0x0000007ca79f3dd8 0x0000007ca79edb80
0x7c02d88770: 0x0000007c02d89000 0xb400007c13c04680
0x7c02d88780: 0x0000000000000000 0x0000000000000002
0x7c02d88790: 0xb400007c13c67c00 0x0000000000000000
0x7c02d887a0: 0x0000000000000000 0xb400007c13c7c100

  其中恢复其调用栈如下:

1
2
3
4
5
arduino复制代码backtrace:
unknown?
android::AMessage::setTarget(android::sp<android::AHandler const> const&)
android::MediaCodec::queueInputBuffer(unsigned long, unsigned long, unsigned long, long, unsigned int, android::AString*)
...

  __cfi_slowpath函数有两处退栈相关。

1
2
3
4
5
6
7
ini复制代码第一部分:
0x0000007c9844f14c <+52>: xpaclri
0x0000007c9844f150 <+56>: mov x2, xzr
0x0000007c9844f154 <+60>: mov x3, x30
0x0000007c9844f158 <+64>: bl 0x7c9844f2a0 <__loader_cfi_fail@plt>
0x0000007c9844f15c <+68>: ldp x29, x30, [sp], #16
0x0000007c9844f160 <+72>: ret
1
2
3
4
5
6
7
less复制代码第二部分:
0x0000007c9844f164 <+76>: and x9, x1, #0xfffffffffffc0000
0x0000007c9844f168 <+80>: sub x8, x9, x8, lsl #12
0x0000007c9844f16c <+84>: add x3, x8, #0x42, lsl #12
0x0000007c9844f170 <+88>: mov x2, xzr
0x0000007c9844f174 <+92>: ldp x29, x30, [sp], #16
0x0000007c9844f178 <+96>: br x3

  如果走的是第一部分退栈,那么 x16 和 x17 将与 tombstone 不符合,因此可以确定走的是第二部分。
而这里会跳转至 x3,可以看下当前 tombstone 里 x3=0x0000007ca1291000 是否为一个有效的函数地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
less复制代码(gdb) disassemble 0x0000007ca1291000,+0x40
Dump of assembler code from 0x7ca1291000 to 0x7ca1291040:
0x0000007ca1291000 <__cfi_check+0>: str x30, [sp, #-16]!
0x0000007ca1291004 <__cfi_check+4>: mov x8, #0x48f1 // #18673
0x0000007ca1291008 <__cfi_check+8>: movk x8, #0xd799, lsl #16
0x0000007ca129100c <__cfi_check+12>: movk x8, #0xcf94, lsl #32
0x0000007ca1291010 <__cfi_check+16>: movk x8, #0xfc57, lsl #48
0x0000007ca1291014 <__cfi_check+20>: cmp x0, x8
0x0000007ca1291018 <__cfi_check+24>: b.le 0x7ca1291100 <__cfi_check+256>
0x0000007ca129101c <__cfi_check+28>: mov x8, #0xd5c9 // #54729
0x0000007ca1291020 <__cfi_check+32>: movk x8, #0x5fef, lsl #16
0x0000007ca1291024 <__cfi_check+36>: movk x8, #0x22f2, lsl #32
0x0000007ca1291028 <__cfi_check+40>: movk x8, #0x4e31, lsl #48
0x0000007ca129102c <__cfi_check+44>: cmp x0, x8
0x0000007ca1291030 <__cfi_check+48>: b.gt 0x7ca12911f4 <__cfi_check+500>
0x0000007ca1291034 <__cfi_check+52>: mov x8, #0x90ef // #37103
0x0000007ca1291038 <__cfi_check+56>: movk x8, #0x3c9, lsl #16
0x0000007ca129103c <__cfi_check+60>: movk x8, #0x8d30, lsl #32

  这个函数非常的大,但有规律,很显然先将 x8与x0 作比较,并且数值与 tombstone 中
x0=0x79748c5e568e2ddc 类似,那么我们可以搜索部分关键字找到相应的 case。

1
2
3
4
5
6
less复制代码0x0000007ca12920b4 <+4276>: mov x8, #0x2ddc // #11740
0x0000007ca12920b8 <+4280>: movk x8, #0x568e, lsl #16
0x0000007ca12920bc <+4284>: movk x8, #0x8c5e, lsl #32
0x0000007ca12920c0 <+4288>: movk x8, #0x7974, lsl #48
0x0000007ca12920c4 <+4292>: cmp x0, x8
0x0000007ca12920c8 <+4296>: b.eq 0x7ca129576c <__cfi_check+18284> // b.none
1
2
3
4
5
6
7
csharp复制代码0x0000007ca129576c <+18284>: adrp x8, 0x7ca13c2000 <_ZTVN7android17BufferChannelBaseE+24>
0x0000007ca1295770 <+18288>: add x8, x8, #0xc28
0x0000007ca1295774 <+18292>: sub x8, x1, x8
0x0000007ca1295778 <+18296>: sub x8, x8, #0x630
0x0000007ca129577c <+18300>: ror x8, x8, #4
0x0000007ca1295780 <+18304>: cmp x8, #0x5a
0x0000007ca1295784 <+18308>: b.hi 0x7ca1296680 <__cfi_check+22144> // b.pmore

  tombstone 中 x9=0x0000007ca126fed7

1
2
3
csharp复制代码0x0000007ca1295788 <+18312>: adrp x9, 0x7ca126f000 <_ZL5__txt+844>
0x0000007ca129578c <+18316>: add x9, x9, #0xed7
0x0000007ca1295790 <+18320>: b 0x7ca12963a4 <__cfi_check+21412>

  tombstone 中x8=0x0000000000000080

1
2
3
less复制代码0x0000007ca12963a4 <+21412>: ldrb w8, [x9, x8]
0x0000007ca12963a8 <+21416>: tbnz w8, #7, 0x7ca1296644 <__cfi_check+22084>
0x0000007ca12963ac <+21420>: b 0x7ca1296680 <__cfi_check+22144>
1
2
ini复制代码0x0000007ca1296644 <+22084>: ldr x30, [sp], #16
0x0000007ca1296648 <+22088>: ret

  最后在此处退栈回到 android::AMessage::setTarget ,然后 PC=LR=0x0000007c02d886f0 发生段错误。

结论

  不难发现一个细节 0x7c02d886c0 地址上存储的内容是有问题的。

1
2
3
4
5
6
7
8
9
10
11
makefile复制代码__cfi_slowpath(uint64_t, void*)
0x7c02d886c0: 0x0000007c02d886f0 0x0000007c95de6754

0x7c02d886d0: 0xb400007c13c246d0 0x0000000001909705
0x7c02d886e0: 0x0000007c02d88700 0x6f2ab3b40fa2f8ef

android::AMessage::setTarget(android::sp<android::AHandler const> const&)
0x7c02d886f0: 0x0000007c02d88750 0x0000007ca133f8e0
0x7c02d88700: 0x0000000000000002 0x0000000000000000
0x7c02d88710: 0x0000000000000415 0x0000000001909705
0x7c02d88720: 0x0000000000000000 0x0000007c02d88808
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
ini复制代码0x0000007c9844f118 < __cfi_slowpath +0>: stp x29, x30, [sp, #-16]!
压栈应该是 0x7c02d886c0: 0x0000007c02d886f0 0x0000007c95de6754
0x0000007c9844f11c < __cfi_slowpath +4>: mov x29, sp
...
0x0000007c9844f174 < __cfi_slowpath +92>: ldp x29, x30, [sp], #16
0x0000007c9844f178 < __cfi_slowpath +96>: br x3
0x0000007ca1291000 <__cfi_check+0>: str x30, [sp, #-16]!
压栈应该是 0x7c02d886c0: 0x0000007c95de6754 0x0000007c95de6754
...
0x0000007ca1296644 < __cfi_check +22084>: ldr x30, [sp], #16
0x0000007ca1296648 < __cfi_check +22088>: ret

(gdb) p /t 0xf81f0ffe 【str x30, [sp, #-16]!】
$5 = 11111000000111110000111111111110
(gdb) p /t 0xf81f0ffd 【str x29, [sp, #-16]!】
$6 = 11111000000111110000111111111101

因此能符合 tombstone 信息,只有此处指令异常导致。

错误模拟

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
yaml复制代码# ps -ef | grep "medias"
media 1145 1 0 01:35:04 ? 00:00:06 mediaserver64
mediacodec 1223 1 0 01:35:04 ? 00:00:01 media.swcodec oid.media.swcodec/bin/mediaswcodec

# cat /proc/1145/maps | grep "libstagefright.so"
7abb428000-7abb4a9000 r--p 00000000 fe:05 84399283 /system/lib64/libstagefright.so
7abb4a9000-7abb5db000 r-xp 00081000 fe:05 84399283 /system/lib64/libstagefright.so
7abb5db000-7abb5ea000 r--p 001b3000 fe:05 84399283 /system/lib64/libstagefright.so
7abb5ea000-7abb5ec000 rw-p 001c1000 fe:05 84399283 /system/lib64/libstagefright.so

PC=0x7abb428000+0x83000
修改机器码模拟报错场景。
# ./data/memory_get64 1145 0x7abb4ab000
pid 1145, target 0x7abb4ab000
0xd2891e28f81f0ffe

# ./data/memory_set64 1145 0x7abb4ab000 0xd2891e28 0xf81f0ffd
pid 1145, target 0x7abb4ab000, val 0xd2891e28f81f0ffd

06-27 23:10:07.311 1145 1145 F libc : Fatal signal 11 (SIGSEGV), code 2 (SEGV_ACCERR), fault addr 0x7fdc95f510 in tid 1145 (mediaserver64), pid 1145 (mediaserver64)
06-27 23:10:07.579 27515 27515 F DEBUG : *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
06-27 23:10:07.579 27515 27515 F DEBUG : Revision: '0'
06-27 23:10:07.579 27515 27515 F DEBUG : ABI: 'arm64'
06-27 23:10:07.579 27515 27515 F DEBUG : Timestamp: 2022-06-27 23:10:07.361670464+0800
06-27 23:10:07.579 27515 27515 F DEBUG : Process uptime: 0s
06-27 23:10:07.580 27515 27515 F DEBUG : Cmdline: mediaserver64
06-27 23:10:07.580 27515 27515 F DEBUG : pid: 1145, tid: 1145, name: mediaserver64 >>> mediaserver64 <<<
06-27 23:10:07.580 27515 27515 F DEBUG : uid: 1013
06-27 23:10:07.580 27515 27515 F DEBUG : tagged_addr_ctrl: 0000000000000001
06-27 23:10:07.580 27515 27515 F DEBUG : signal 11 (SIGSEGV), code 2 (SEGV_ACCERR), fault addr 0x7fdc95f510
06-27 23:10:07.580 27515 27515 F DEBUG : x0 bf2d51af2cdcf7a8 x1 0000007abb5df510 x2 0000000000000000 x3 0000007abb4ab000
06-27 23:10:07.580 27515 27515 F DEBUG : x4 0065006300690076 x5 7600690063006500 x6 0065006300690076 x7 b400007ac2afd3c8
06-27 23:10:07.580 27515 27515 F DEBUG : x8 0000000000000003 x9 0000007abb489b40 x10 0000000000000002 x11 0000000000000001
06-27 23:10:07.580 27515 27515 F DEBUG : x12 0000000000000000 x13 0000007aac820058 x14 0000000000000010 x15 0000000000000010
06-27 23:10:07.580 27515 27515 F DEBUG : x16 0000007ab0cbd538 x17 0000007ac1210118 x18 0000007ac3726000 x19 0000007fdc95f5d0
06-27 23:10:07.580 27515 27515 F DEBUG : x20 b400007a26e03048 x21 0000007ac13b1b98 x22 0000007ab0cadc50 x23 0000007ac2d13000
06-27 23:10:07.580 27515 27515 F DEBUG : x24 0000007fdc95f470 x25 b400007ac2aea1c4 x26 0000000000000000 x27 0000000000000479
06-27 23:10:07.580 27515 27515 F DEBUG : x28 0000000000000000 x29 0000007fdc95f510
06-27 23:10:07.580 27515 27515 F DEBUG : lr 0000007fdc95f510 sp 0000007fdc95f440 pc 0000007fdc95f510 pst 0000000080001000
06-27 23:10:07.580 27515 27515 F DEBUG : backtrace:
06-27 23:10:07.580 27515 27515 F DEBUG : #00 pc 000000000001f510 [stack]

Coredump寄存器恢复栈

  通过readelf读取MINIDUMP文件,保存寄存器上下文在ELF文件的第一个节中。

1
2
3
4
5
6
7
python复制代码Program Headers:
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
NOTE 0x000000000000fb28 0x0000000000000000 0x0000000000000000
0x00000000000032cc 0x0000000000000000 0x0
LOAD 0x0000000000013000 0x0000005ffabc7000 0x0000000000000000
0x0000000000001000 0x0000000000001000 R 0x0

  用hexedit工具打开MINIDUMP文件找到寄存器地址。

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
erlang复制代码0000FB20 00 00 00 00 00 00 00 00 05 00 00 00 88 01 00 00 ................
0000FB30 01 00 00 00 43 4F 52 45 00 00 00 00 00 00 00 00 ....CORE........
0000FB40 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0000FB50 00 00 00 00 00 00 00 00 00 00 00 00 CD 2E 00 00 ................
0000FB60 01 00 00 00 01 00 00 00 00 00 00 00 00 00 00 00 ................
0000FB70 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0000FB80 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0000FB90 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0000FBA0 00 00 00 00 00 00 00 00 00 00 00 00 DC 2D 8E 56 .............-.V
0000FBB0 5E 8C 74 79 18 36 3C A1 7C 00 00 00 00 00 00 00 ^.ty.6<.|.......
0000FBC0 00 00 00 00 00 10 29 A1 7C 00 00 00 05 97 90 01 ......).|.......
0000FBD0 00 00 00 00 00 00 00 00 00 00 00 00 08 88 D8 02 ................
0000FBE0 7C 00 00 00 2F 25 F0 5B 65 25 06 B6 80 00 00 00 |.../%.[e%......
0000FBF0 00 00 00 00 D7 FE 26 A1 7C 00 00 00 06 00 00 00 ......&.|.......
0000FC00 00 00 00 00 FC 81 0A FD 7B 00 00 00 BE 9D 64 A9 ........{.....d.
0000FC10 5C A9 F8 9E 0E 72 38 AC D5 82 47 E4 30 80 0A FD \....r8...G.0...
0000FC20 7B 00 00 00 5C 7B 30 56 1E 00 00 00 70 DB DF 95 {...\{0V....p...
0000FC30 7C 00 00 00 18 F1 44 98 7C 00 00 00 00 80 A2 FA |.....D.|.......
0000FC40 7B 00 00 00 D0 46 C2 13 7C 00 00 B4 30 87 D8 02 {....F..|...0...
0000FC50 7C 00 00 00 00 7C C6 13 7C 00 00 B4 15 04 00 00 |....|..|.......
0000FC60 00 00 00 00 00 90 D8 02 7C 00 00 00 02 00 00 00 ........|.......
0000FC70 00 00 00 00 D0 46 C2 13 7C 00 00 B4 00 7C C6 13 .....F..|....|..
0000FC80 7C 00 00 B4 00 90 D8 02 7C 00 00 00 28 2C 3C A1 |.......|...(,<.
0000FC90 7C 00 00 00 F0 86 D8 02 7C 00 00 00 F0 86 D8 02 |.......|...Tg..
0000FCA0 7C 00 00 00 D0 86 D8 02 7C 00 00 00 F0 86 D8 02 |.......|...Tg..
0000FCB0 7C 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |...............
0000FCC0 00 00 00 00 05 00 00 00 88 00 00 00 03 00 00 00 ................

  上图绿色为LR和PC,将它们修改为正确的返回地址0x0000007c95de6754

1
2
3
erlang复制代码0000FC90 7C 00 00 00 F0 86 D8 02 7C 00 00 00 54 67 DE 95 |.......|...Tg..
0000FCA0 7C 00 00 00 D0 86 D8 02 7C 00 00 00 54 67 DE 95 |.......|...Tg..
0000FCB0 7C 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |...............

  重新将恢复后的MINIDUMP装载到gdb上即可恢复callstack。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
arduino复制代码(gdb) bt
#0 android::AHandler::id (this=0xb400007c13c67c00)
#1 android::AMessage::setTarget (this=0xb400007c13c246d0, handler=...)
#2 0x0000007ca133f8e0 in android::MediaCodec::queueInputBuffer (this=0xb400007c13c67c00, index=2, offset=0, size=1045, presentationTimeUs=26253061, flags=0, errorDetailMsg=0x7c02d88808)
#3 0x0000007ca796ee7c in android::NuPlayer::Decoder::onInputBufferFetched (this=0xb400007c13c7c100, msg=...)
#4 0x0000007ca796c984 in android::NuPlayer::Decoder::doRequestBuffers (this=0xb400007c13c7c100)
#5 0x0000007ca7963d48 in android::NuPlayer::DecoderBase::onRequestInputBuffers (this=0xb400007c13c7c100)
#6 0x0000007ca79717bc in android::NuPlayer::Decoder::handleAnInputBuffer (this=0xb400007c13c7c100, index=2)
#7 0x0000007ca7969328 in android::NuPlayer::Decoder::onMessageReceived (this=0xb400007c13c7c100, msg=...)
#8 0x0000007c95de2874 in android::AHandler::deliverMessage (this=0xb400007c13c7c100, msg=...)
#9 0x0000007c95de8608 in android::AMessage::deliver (this=0xb400007c13c24c70)
#10 0x0000007c95de3bbc in android::ALooper::loop (this=<optimized out>)
#11 0x0000007ca105907c in android::Thread::_threadLoop (user=user@entry=0xb400007cadca8fc0)
#12 0x0000007ca105886c in thread_data_t::trampoline (t=<optimized out>)
#13 0x0000007caaf3be74 in __pthread_start (arg=0x7c02d88cb0)
#14 0x0000007caaedb830 in __start_thread (fn=0x7caaf3bda4 <__pthread_start(void*)>, arg=0x7c02d88cb0)

微信公众号链接:mp.weixin.qq.com/s/YDhqP_ZkS…

本文转载自: 掘金

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

JYM 设计模式系列-工厂模式,让你的代码更优雅!!! 概述

发表于 2023-05-25

觉得不错请按下图操作,掘友们,哈哈哈!!!
image.png

概述设计模式分类

总体来说设计模式分为三大类:

  • 创建型模式,共五种:工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式。
  • 结构型模式,共七种:适配器模式、装饰器模式、代理模式、外观模式、桥接模式、组合模式、享元模式。
  • 行为型模式,共十一种:策略模式、模板方法模式、观察者模式、迭代子模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式。

一:创建模式(5种)

工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式。

1. 工厂模式

1.1 简单工厂模式

Factory是用于创建其他对象的对象。它提供了一个静态方法来创建并返回不同类的对象,以便隐藏实现逻辑并使客户端代码专注于使用而不是对象初始化和管理。

二话不说上一个demo:在这个例子中,我们来通过制造硬币业务讲解。CoinFactory是工厂类,它提供了一个静态方法来创建不同类型的硬币。

image.png

Coin 接口:

1
2
3
4
5
csharp复制代码public interface Coin {

String getDescription();

}

Coin工厂:

1
2
3
4
5
6
7
8
9
typescript复制代码public class CoinFactory {

/**
* 工厂方法将硬币类型作为参数,并调用相应的类
*/
public static Coin getCoin(CoinType type) {
return type.getConstructor().get();
}
}

不同类型硬币的枚举:

1
2
3
4
5
6
7
8
9
less复制代码@RequiredArgsConstructor
@Getter
public enum CoinType {

COPPER(CopperCoin::new),
GOLD(GoldCoin::new);

private final Supplier<Coin> constructor;
}

铜硬币的实现:

1
2
3
4
5
6
7
8
9
typescript复制代码public class CopperCoin implements Coin {

static final String DESCRIPTION = "This is a copper coin.";

@Override
public String getDescription() {
return DESCRIPTION;
}
}

金硬币的实现:

1
2
3
4
5
6
7
8
9
typescript复制代码public class GoldCoin implements Coin {

static final String DESCRIPTION = "This is a gold coin.";

@Override
public String getDescription() {
return DESCRIPTION;
}
}

主类:Factory用于创建其他对象的对象。它提供了一个静态方法来
创建并返回不同类的对象,以便隐藏实现逻辑
并使客户端代码专注于使用而不是对象初始化和管理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
typescript复制代码@Slf4j
public class App {

/**
* Program main entry point.
*/
public static void main(String[] args) {
LOGGER.info("The alchemist begins his work.");
var coin1 = CoinFactory.getCoin(CoinType.COPPER);
var coin2 = CoinFactory.getCoin(CoinType.GOLD);
LOGGER.info(coin1.getDescription());
LOGGER.info(coin2.getDescription());
}
}

在这个例子中,炼金术士制造硬币。CoinFactory是工厂类提供一个静态方法来创建不同类型的硬币。

1.2 工厂方法

工厂方法模式(FACTORY METHOD)是一种常用的类创建型设计模式,此模式的核心精神是封装类中变化的部分,提取其中个性化善变的部分为独立类,通过依赖注入以达到解耦、复用和方便后期维护拓展的目的。它的核心结构有四个角色,分别是抽象工厂;具体工厂;抽象产品;具体产品。

适用场景

如果无法预知对象确切类别及其依赖关系时,可使用工厂方法。
工厂方法将创建产品的代码与实际使用产品的代码分离, 从而能在不影响其他代码的情况下扩展产品创建部分代码。
例如, 如果需要向应用中添加一种新产品, 你只需要开发新的创建者子类, 然后重写其工厂方法即可。

实现方式

  1. 让所有产品都遵循同一接口。该接口必须声明对所有产品都有意义的方法。
  2. 在创建类中添加一个空的工厂方法。该方法的返回类型必须遵循通用的产品接口。

以不同种类的铁匠铸造武器为demo:

image.png

Blacksmith 铁匠 包含用于生成对象的方法的接口:

1
2
3
4
5
csharp复制代码public interface Blacksmith {

Weapon manufactureWeapon(WeaponType weaponType);

}

WeaponType:武器类型枚举

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
typescript复制代码/**
* WeaponType enumeration.
*/
@RequiredArgsConstructor
public enum WeaponType {

SHORT_SWORD("short sword"),
SPEAR("spear"),
AXE("axe"),
UNDEFINED("");

private final String title;

@Override
public String toString() {
return title;
}
}

Weapon 武器类型接口:

1
2
3
4
5
6
7
8
csharp复制代码/**
* Weapon interface.
*/
public interface Weapon {

WeaponType getWeaponType();

}

精灵铁匠:用于创建新对象的具体子类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
typescript复制代码public class ElfBlacksmith implements Blacksmith {

private static final Map<WeaponType, ElfWeapon> ELFARSENAL;

static {
ELFARSENAL = new EnumMap<>(WeaponType.class);
Arrays.stream(WeaponType.values()).forEach(type -> ELFARSENAL.put(type, new ElfWeapon(type)));
}

@Override
public Weapon manufactureWeapon(WeaponType weaponType) {
return ELFARSENAL.get(weaponType);
}

@Override
public String toString() {
return "The elf blacksmith";
}
}

Elf Weapon 武器:

1
2
3
4
5
6
7
8
9
10
11
typescript复制代码@RequiredArgsConstructor
@Getter
public class ElfWeapon implements Weapon {

private final WeaponType weaponType;

@Override
public String toString() {
return "an elven " + weaponType;
}
}

兽人铁匠:用于创建新对象的具体子类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
typescript复制代码public class OrcBlacksmith implements Blacksmith {

private static final Map<WeaponType, OrcWeapon> ORCARSENAL;

static {
ORCARSENAL = new EnumMap<>(WeaponType.class);
Arrays.stream(WeaponType.values()).forEach(type -> ORCARSENAL.put(type, new OrcWeapon(type)));
}

@Override
public Weapon manufactureWeapon(WeaponType weaponType) {
return ORCARSENAL.get(weaponType);
}

@Override
public String toString() {
return "The orc blacksmith";
}
}

OrcWeapon 武器:

1
2
3
4
5
6
7
8
9
10
11
typescript复制代码@RequiredArgsConstructor
@Getter
public class OrcWeapon implements Weapon {

private final WeaponType weaponType;

@Override
public String toString() {
return "an orcish " + weaponType;
}
}

主类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
ini复制代码@Slf4j
public class App {

private static final String MANUFACTURED = "{} manufactured {}";

/**
* Program entry point.
* @param args command line args
*/
public static void main(String[] args) {

Blacksmith blacksmith = new OrcBlacksmith();
Weapon weapon = blacksmith.manufactureWeapon(WeaponType.SPEAR);
LOGGER.info(MANUFACTURED, blacksmith, weapon);
weapon = blacksmith.manufactureWeapon(WeaponType.AXE);
LOGGER.info(MANUFACTURED, blacksmith, weapon);

blacksmith = new ElfBlacksmith();
weapon = blacksmith.manufactureWeapon(WeaponType.SPEAR);
LOGGER.info(MANUFACTURED, blacksmith, weapon);
weapon = blacksmith.manufactureWeapon(WeaponType.AXE);
LOGGER.info(MANUFACTURED, blacksmith, weapon);
}
}

工厂方法是一种创造性的设计模式,它使用工厂方法来处理
创建对象时没有指定要创建的对象的确切类的问题。
这是通过调用在接口中指定的工厂方法来创建对象来完成的
并由子类实现,或在基类中实现并可选地被覆盖
派生类——而不是通过调用构造函数。

在这个工厂方法示例中,我们有一个接口 Blacksmith 和一个方法
创建对象(manufactureWeapon)。 具体的子类(
OrcBlacksmith, ElfBlacksmith) 然后覆盖该方法以生成他们喜欢的对象

1.3 抽象工厂

抽象工厂(AbstractFactory)模式提供了一种封装一组独立工厂的方法有一个共同的目标,而不指定它们的具体类。正常使用情况下,客户端软件创建抽象工厂的具体实现,然后使用泛型接口,用于创建作为主题一部分的具体对象。客户端不知道(或不关心)它从每个内部工厂获得哪些具体对象,因为它只使用他们产品的通用接口。这种模式将细节分开一组对象的实现从它们的一般用法和依赖于对象组合,因为对象创建是在工厂接口中公开的方法中实现的。

抽象工厂模式的本质是一个工厂接口 (KingdomFactory)及其实现ElfKingdomFactory,OrcKingdomFactory。该示例使用创建国王、城堡和军队的具体实现。

image.png

King 国王接口:

1
2
3
4
csharp复制代码public interface King {

String getDescription();
}

Army 军队接口:

1
2
3
4
csharp复制代码public interface Army {

String getDescription();
}

Castle:城堡接口

1
2
3
4
csharp复制代码public interface Castle {

String getDescription();
}

KingdomFactory:王国工厂

1
2
3
4
5
6
7
8
9
csharp复制代码public interface KingdomFactory {

Castle createCastle();

King createKing();

Army createArmy();

}

帮助KingdomFactory 生产 bean的类

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复制代码@Getter
@Setter
public class Kingdom {

private King king;
private Castle castle;
private Army army;

/**
* The factory of kingdom factories.
*/
public static class FactoryMaker {

/**
* Enumeration for the different types of Kingdoms.
*/
public enum KingdomType {
ELF, ORC
}

/**
* The factory method to create KingdomFactory concrete objects.
*/
public static KingdomFactory makeFactory(KingdomType type) {
return switch (type) {
case ELF -> new ElfKingdomFactory();
case ORC -> new OrcKingdomFactory();
default -> throw new IllegalArgumentException("KingdomType not supported.");
};
}
}
}

ElfArmy :精灵的军队

1
2
3
4
5
6
7
8
9
typescript复制代码public class ElfArmy implements Army {

static final String DESCRIPTION = "This is the elven army!";

@Override
public String getDescription() {
return DESCRIPTION;
}
}

ElfCastle:精灵城堡

1
2
3
4
5
6
7
8
9
typescript复制代码public class ElfCastle implements Castle {

static final String DESCRIPTION = "This is the elven castle!";

@Override
public String getDescription() {
return DESCRIPTION;
}
}

ElfKing 精灵国王:

1
2
3
4
5
6
7
8
9
typescript复制代码public class ElfKing implements King {

static final String DESCRIPTION = "This is the elven king!";

@Override
public String getDescription() {
return DESCRIPTION;
}
}

精灵国工厂:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
typescript复制代码public class ElfKingdomFactory implements KingdomFactory {

@Override
public Castle createCastle() {
return new ElfCastle();
}

@Override
public King createKing() {
return new ElfKing();
}

@Override
public Army createArmy() {
return new ElfArmy();
}

}

兽人军队:

1
2
3
4
5
6
7
8
9
typescript复制代码public class OrcArmy implements Army {

static final String DESCRIPTION = "This is the orc army!";

@Override
public String getDescription() {
return DESCRIPTION;
}
}

兽人城堡:

1
2
3
4
5
6
7
8
9
typescript复制代码public class OrcCastle implements Castle {

static final String DESCRIPTION = "This is the orc castle!";

@Override
public String getDescription() {
return DESCRIPTION;
}
}

兽人国王:

1
2
3
4
5
6
7
8
9
typescript复制代码public class OrcKing implements King {

static final String DESCRIPTION = "This is the orc king!";

@Override
public String getDescription() {
return DESCRIPTION;
}
}

兽人王国工厂:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
typescript复制代码public class OrcKingdomFactory implements KingdomFactory {

@Override
public Castle createCastle() {
return new OrcCastle();
}

@Override
public King createKing() {
return new OrcKing();
}

@Override
public Army createArmy() {
return new OrcArmy();
}
}

主类:

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
scss复制代码@Slf4j
public class App implements Runnable {

private final Kingdom kingdom = new Kingdom();

public Kingdom getKingdom() {
return kingdom;
}

/**
* Program entry point.
*
* @param args command line args
*/
public static void main(String[] args) {
var app = new App();
app.run();
}

@Override
public void run() {
LOGGER.info("elf kingdom");
createKingdom(Kingdom.FactoryMaker.KingdomType.ELF);
LOGGER.info(kingdom.getArmy().getDescription());
LOGGER.info(kingdom.getCastle().getDescription());
LOGGER.info(kingdom.getKing().getDescription());

LOGGER.info("orc kingdom");
createKingdom(Kingdom.FactoryMaker.KingdomType.ORC);
LOGGER.info(kingdom.getArmy().getDescription());
LOGGER.info(kingdom.getCastle().getDescription());
LOGGER.info(kingdom.getKing().getDescription());
}

/**
* Creates kingdom.
* @param kingdomType type of Kingdom
*/
public void createKingdom(final Kingdom.FactoryMaker.KingdomType kingdomType) {
final KingdomFactory kingdomFactory = Kingdom.FactoryMaker.makeFactory(kingdomType);
kingdom.setKing(kingdomFactory.createKing());
kingdom.setCastle(kingdomFactory.createCastle());
kingdom.setArmy(kingdomFactory.createArmy());
}
}

抽象工厂模式提供了一种方法来封装一组具有共同主题的独立工厂,而无需指定它们的具体类。在正常使用中,客户端软件创建抽象工厂的具体实现,然后使用工厂的通用接口创建作为主题一部分的具体对象。客户不知道(或不关心)它从这些内部工厂中的每一个获得了哪些具体对象, 因为它只使用它们产品的通用接口。这种模式将一组对象的实现细节与它们的一般用法分开,并依赖于对象组合,因为对象创建是在工厂接口中公开的方法中实现的。

这个例子中抽象工厂模式的本质是工厂接口({@link KingdomFactory})及其实现({@link ElfKingdomFactory}、{@link OrcKingdomFactory})。该示例使用两个具体实现来创建国王、城堡和军队。

1.4 Factory kit 模式

Factory kit 是一种创建模式,它定义了一个不可变内容的工厂,具有分离的builder 和 factory 接口,以处理直接在 factory kit 实例中创建指定对象之一的问题。

image.png

具体demo

定义一个武器接口:

1
2
kotlin复制代码public interface Weapon {
}

功能接口,工厂套件设计模式的一个例子。 本地创建的实例提供了一个机会来严格定义工厂实例将能够创建哪些对象类型。Factory 是 Builder 的占位符使用 WeaponFactory#create(WeaponType) 方法初始化新对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
php复制代码public interface WeaponFactory {

/**
* Creates an instance of the given type.
*
* @param name representing enum of an object type to be created.
* @return new instance of a requested class implementing {@link Weapon} interface.
*/
Weapon create(WeaponType name);

/**
* Creates factory - placeholder for specified {@link Builder}s.
*
* @param consumer for the new builder to the factory.
* @return factory with specified {@link Builder}s
*/
static WeaponFactory factory(Consumer<Builder> consumer) {
var map = new HashMap<WeaponType, Supplier<Weapon>>();
consumer.accept(map::put);
return name -> map.get(name).get();
}
}

Builder:允许将具有名称的构建器添加到工厂的功能接口

1
2
3
csharp复制代码public interface Builder {
void add(WeaponType name, Supplier<Weapon> supplier);
}

代表 斧头 的类:

1
2
3
4
5
6
typescript复制代码public class Axe implements Weapon {
@Override
public String toString() {
return "Axe";
}
}

代表弓的类:

1
2
3
4
5
6
typescript复制代码public class Bow implements Weapon {
@Override
public String toString() {
return "Bow";
}
}

代表矛的类:

1
2
3
4
5
6
typescript复制代码public class Spear implements Weapon {
@Override
public String toString() {
return "Spear";
}
}

代表剑的类:

1
2
3
4
5
6
typescript复制代码public class Sword implements Weapon {
@Override
public String toString() {
return "Sword";
}
}

武器类型的枚举:

1
2
3
4
5
6
arduino复制代码public enum WeaponType {
SWORD,
AXE,
BOW,
SPEAR
}

主类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
csharp复制代码@Slf4j
public class App {

/**
* Program entry point.
*
* @param args command line args
*/
public static void main(String[] args) {
var factory = WeaponFactory.factory(builder -> {
builder.add(WeaponType.SWORD, Sword::new);
builder.add(WeaponType.AXE, Axe::new);
builder.add(WeaponType.SPEAR, Spear::new);
builder.add(WeaponType.BOW, Bow::new);
});
var list = new ArrayList<Weapon>();
list.add(factory.create(WeaponType.AXE));
list.add(factory.create(WeaponType.SPEAR));
list.add(factory.create(WeaponType.SWORD));
list.add(factory.create(WeaponType.BOW));
list.forEach(weapon -> LOGGER.info("{}", weapon.toString()));
}
}

在给定的示例中,WeaponFactory表示工厂套件,其中包含四个 Builder,用于创建实现 Weapon接口的类的新对象。

它们中的每一个都可以使用 WeaponFactory#create(WeaponType) 方法调用,其中 一个输入表示 WeaponType 的一个实例,需要在工厂实例中显式映射所需的类类型。

本文转载自: 掘金

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

Flutter 小技巧之 310 适配之单例 Window

发表于 2023-05-17

Flutter 3.10 发布之后,大家可能注意到,在它的 release note 里提了一句: Window singleton 相关将被弃用,并且这个改动是为了支持未来多窗口的相关实现。

所以这是一个为了支持多窗口的相关改进,多窗口更多是在 PC 场景下更常见,但是又需要兼容 Mobile 场景,故而有此次改动作为提前铺垫。

如下图所示,如果具体到对应的 API 场景,主要就是涉及 WidgetsBinding.instance.window 和 MediaQueryData.fromWindow 等接口的适配,因为 WidgetsBinding.instance.window 即将被弃用。

你可以不适配,还能跑,只是升级的技术债务往后累计而已。

那首先可能就有一个疑问,为什么会有需要直接使用 WidgetsBinding.instance.window 的使用场景?简单来说,具体可以总结为:

  • 没有 BuildContext ,不想引入 BuildContext
  • 不希望获取到的 MediaQueryData 受到所在 BuildContext 的影响,例如键盘弹起导致 padding 变化重构和受到 Scaffold 下的参数影响等

这部分详细可见:《MediaQuery 和 build 优化你不知道的秘密》 。

那么从 3.10 开始,针对 WidgetsBinding.instance.window 可以通过新的 API 方式进行兼容:

  • 如果存在 BuildContex , 可以通过 View.of 获取 FlutterView,这是官方最推荐的替代方式
  • 如果没有 BuildContex 可以通过 PlatformDispatcher 的 views 对象去获取

这里注意到没有,现在用的是 View.of ,获取的是 FlutterView ,对象都称呼为 View 而不是 「Window」,对应的 MediaQueryData.fromWindow API 也被弃用,修改为 MediaQueryData.fromView ,这个修改的依据在于:

起初 Flutter 假定了它只支持一个 Window 的场景,所以会有 SingletonFlutterWindow 这样的 instance window 对象存在,同时 window 属性又提供了许多和窗口本身无关的功能,在多窗口逻辑下会显得很另类。

那么接下来就让我们用「长篇大论」来简单介绍下这两个场景的特别之处。

存在 BuildContext

回归到本次的调整,首先是存在 BuildContext 的场景,如下代码所示,对于存在 BuildContex 的场景, View.of 相关的调整为:

1
2
3
4
5
6
7
8
9
10
11
12
ini复制代码/// 3.10 之前
double dpr = WidgetsBinding.instance.window.devicePixelRatio;
Locale locale = WidgetsBinding.instance.window.locale;
double width =
   MediaQueryData.fromWindow(WidgetsBinding.instance.window).size.width;


/// 3.10 之后
double dpr = View.of(context).devicePixelRatio;
Locale locale = View.of(context).platformDispatcher.locale;
double width =
   MediaQueryData.fromView(View.of(context)).size.width;

可以看到,这里的 View 内部实现肯定是有一个 InheritedWidget ,它将 FlutterView 通过 BuildContext 往下共享,从而提供类似 「window」 的参数能力,而通过 View.of 获取的参数:

  • 当 FlutterView 本身的属性值发生变化时,是不会通知绑定的 context 更新,这个行为类似于之前的 WidgetsBinding.instance.window
  • 只有当 FlutterView 本身发生变化时,比如 context 绘制到不同的 FlutterView 时,才会触发对应绑定的 context 更新

可以看到 View.of 这个行为考虑的是「多 FlutterView」 下的更新场景,如果是需要绑定到具体对应参数的变动更新,如 size 等,还是要通过以前的 MediaQuery.of / MediaQuery.maybeOf 来实现。

而对于 View 来说,每个 FlutterView 都必须是独立且唯一的,在一个 Widget Tree 里,一个 FlutterView 只能和一个 View 相关联,这个主要体现在 FlutterView 标识 GlobalObjectKey 的实现上。

简单总结一下:在存在 BuildContex 的场景,可以简单将 WidgetsBinding.instance.window 替换为 View.of(context) ,不用担心绑定了 context 导致重构,因为 View.of 只对 FlutterView 切换的场景生效。

不存在 BuildContext

对于不存在或者不方便使用 BuildContext 的场景,官方提供了 PlatformDispatcher.views API 来进行支持,不过因为 get views 对应的是 Map 的 values ,它是一个 Iterable 对象,那么对于 3.10 我们需要如何使用 PlatformDispatcher.views 来适配没有 BuildContext 的 WidgetsBinding.instance.window 场面?

PlatformDispatcher 内部的views 维护了中所有可用 FlutterView 的列表,用于提供在没有 BuildContext 的情况下访问视图的支持。

你说什么情况下会有没有 BuildContext ?比如 Flutter 里 的 runApp ,如下图所示,3.10 目前在 runApp 时会通过 platformDispatcher.implicitView 来塞进去一个默认的 FlutterView 。

implicitView 又是什么?其实 implicitView 就是 PlatformDispatcher._views 里 id 为 0 的 FlutterView ,默认也是 views 这个 Iterable 里的 first 对象。

也就是在没有 BuildContext 的场景, 可以通过 platformDispatcher.views.first 的实现迁移对应的 instance.window 实现。

1
2
3
4
scss复制代码/// 3.10 之前
MediaQueryData.fromWindow(WidgetsBinding.instance.window)
/// 3.10 之后
MediaQueryData.fromView(WidgetsBinding.instance.platformDispatcher.views.first)

为什么不直接使用 implicitView 对象? 因为 implicitView 目前是一个过渡性方案,官方希望在多视图的场景下不应该始终存在 implicit view 的概念,而是应用自己应该主动请求创建一个窗口,去提供一个视图进行绘制。

所以对于 implicitView 目前官方提供了 _implicitViewEnabled 函数,后续可以通过可配置位来控制引擎是否支持 implicitView ,也就是 implicitView 在后续更新随时可能为 null ,这也是我们不应该在外部去使用它的理由,同时它是在 runApp 时配置的,所以它在应用启动运行后永远不会改变,如果它在启动时为空,则它永远都会是 null。

PlatformDispatcher.instance.views[0] 在之前的单视图场景中,无论是否有窗口存在,类似的 implicitView 会始终存在;而在多窗口场景下,PlatformDispatcher.instance.views 将会跟随窗口变化。

另外我们是通过 WidgetsBinding.instance.platformDispatcher.views 去访问 views ,而不是直接通过 PlatformDispatcher.instance.views ,因为通常官方更建议在 Binding 的依赖关系下去访问 PlatformDispatcher 。

除了需要在 runApp() 或者 ensureInitialized() 之前访问 PlatformDispatcher 的场景。

另外,如下图所示,通过 Engine 里对于 window 部分代码的实现,可以看到我们所需的默认FlutterView 是 id 为 0 的相关依据,所以这也是我们通过 WidgetsBinding.instance.platformDispatcher.views 去兼容支持的逻辑所在。

最后

最后总结一下,说了那么多,其实不外乎就是将 WidgetsBinding.instance.window 替换为 View.of(context) ,如果还有一些骚操作场景,可以使用 WidgetsBinding.instance.platformDispatcher.views ,如果不怕后续又坑,甚至可以直接使用 WidgetsBinding.instance.platformDispatcher.implicitView 。

整体上解释那么多,主要还是给大家对这次变动有一个背景认知,同时也对未来多窗口实现进展有进一步的了解,相信下一个版本多窗口应该就可以和大家见面了。

更多讨论可见:

  • github.com/flutter/flu…
  • github.com/flutter/eng…
  • github.com/flutter/flu…
  • github.com/flutter/flu…
  • github.com/flutter/eng…

本文转载自: 掘金

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

1…757677…956

开发者博客

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