0%

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
Step 1️⃣ 安装 Certbot(CentOS / Rocky / Alma)
sudo yum install -y epel-release
sudo yum install -y certbot python3-certbot-nginx

Step 2️⃣ 一条命令申请 HTTPS(⭐ 核心)
sudo certbot --nginx -d example.com -d www.example.com

Step 3️⃣ 立刻生效(不用重启)

Certbot 会自动:

申请证书

配置 Nginx

打开 443

加 SSL 配置

现在直接访问:

https://example.com


🔒 浏览器出现小锁 = 成功



如果是自己解压安装的nginx,建立软连接,指向安装目录,
sudo ln -s /usr/local/nginx/sbin/nginx /usr/sbin/nginx
sudo certbot --nginx --nginx-server-root /usr/local/nginx/conf -d codehelps.online -d www.codehelps.online




1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
repositories {
maven { url 'https://jitpack.io' }
}

dependencies {
implementation 'com.github.PhilJay:MPAndroidChart:v3.1.0'
}

布局引用
<com.github.mikephil.charting.charts.CombinedChart
android:id="@+id/combinedChart"
android:layout_width="350dp"
android:layout_height="270dp"
android:layout_margin="20dp"
app:layout_constraintTop_toBottomOf="@+id/toolbar"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
/>


private lateinit var lineDataSet: LineDataSet//折线数据集

/** 初始化 CombinedChart **/
private fun setupChart() {
val chart = binding.combinedChart
// 折线数据集 参数设置
lineDataSet = LineDataSet(mutableListOf(Entry(0f, 0f)), "实时曲线").apply {
color = ColorTemplate.rgb("1E88E5")
lineWidth = 2f
setDrawCircles(false)
setDrawValues(false)
setDrawFilled(true) // 曲线下方填充
mode = LineDataSet.Mode.CUBIC_BEZIER // 平滑曲线 LINEAR
axisDependency = YAxis.AxisDependency.LEFT
}
lineDataSet.setDrawFilled(true)
lineDataSet.fillColor = Color.parseColor("#82F689") // 填充颜色
lineDataSet.fillAlpha = 80 // 透明度 0~255



// 曲面折线
redDataSet = LineDataSet(mutableListOf(), "GC 谱图").apply {
setDrawHorizontalHighlightIndicator(false)
setDrawVerticalHighlightIndicator(false)
lineWidth = 0f // 安全
color = Color.TRANSPARENT // 即使透明也不影响
mode = LineDataSet.Mode.LINEAR // 不要贝塞尔

setCircleColor(Color.RED) // 圆点颜色为红色
setDrawCircleHole(false) // 圆点不留白
setDrawCircles(true) // 不画圆点
setDrawFilled(false) // 曲线下方填充
fillColor = Color.TRANSPARENT
}


val lineData = LineData(redDataSet,lineDataSet)

// 柱状数据集(初始化为空)
barDataSet = BarDataSet(mutableListOf(), "目标气体峰").apply {
color = Color.RED
}
val barData = BarData(barDataSet).apply { barWidth = 0.8f }

// CombinedData
val combinedData = CombinedData().apply {
setData(lineData)
setData(barData)
}
chart.data = combinedData

// 限制最大缩放比例
chart.viewPortHandler.setMaximumScaleX(Float.MAX_VALUE) // X轴无限放大
chart.viewPortHandler.setMinimumScaleX(0f) // 无限缩小(到很小)
chart.viewPortHandler.setMaximumScaleY(Float.MAX_VALUE) // Y轴无限放大
chart.viewPortHandler.setMinimumScaleY(0f) // 无限缩小

// X轴设置
chart.xAxis.apply {
position = XAxis.XAxisPosition.BOTTOM
granularity = 1f
axisMinimum = 0f
}

// Y轴设置
// chart.axisLeft.apply {
// axisMinimum = 0f
// valueFormatter = object : ValueFormatter() {
// override fun getFormattedValue(value: Float): String = "${value.toInt()} mV"
// }
// setAxisMinValue(0f)//强制y轴最小值
// }
chart.axisLeft.apply {
axisMinimum = 0f // 下限固定 0(PID 不会为负)
spaceTop = 80f // 上方预留 20%,防止贴顶
spaceBottom = 5f // 下方预留一点更美观
setDrawGridLines(true)
setDrawZeroLine(false)

// Y 轴单位
valueFormatter = object : ValueFormatter() {
override fun getFormattedValue(value: Float): String = "${value.toInt()} mV"
}
}

chart.axisRight.isEnabled = false

// 缩放 + 拖动 + 图表一次最多能看到多少 X 点(宽度限制)
chart.setScaleEnabled(true)
chart.setPinchZoom(true)
chart.setVisibleXRangeMaximum(maxVisiblePoints)

chart.description.isEnabled = false



//替换默认的renderer为自定义的SampleHighlightRenderer,画高亮区间
val highlightPaint = Paint().apply {
style = Paint.Style.FILL
color = Color.parseColor("#5533B5E5") // 半透明蓝色
}
val renderer = SampleHighlightRenderer(binding.combinedChart, binding.combinedChart.viewPortHandler, highlightPaint)
binding.combinedChart.renderer = renderer


//设置高亮的mark
// val marker = MyMarkerView(this)
// marker.chartView = binding.combinedChart
// binding.combinedChart.marker = marker
//
// chart.isHighlightPerTapEnabled = true
// chart.isHighlightPerDragEnabled = true
chart.invalidate()
}



private fun addGCPoint(value: Float) {
val chart = binding.combinedChart
val entry = Entry(xIndex, value)

xIndex += 1f

lineDataSet.addEntry(entry)
if(xIndex == 10f || xIndex == 20f){
redDataSet.addEntry(entry)
}


chart.data.lineData.notifyDataChanged()
chart.data.notifyDataChanged()
chart.notifyDataSetChanged()

// 固定显示最近 maxVisiblePoints 个点
if (lineDataSet.entryCount > maxVisiblePoints) {
chart.setVisibleXRangeMaximum(maxVisiblePoints)
chart.moveViewToX(lineDataSet.entryCount - maxVisiblePoints)
} else {
chart.moveViewToX(0f)
}

chart.invalidate()

}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
先进入/var/www  hexo对应的根目录

然后下载主题:
git clone -b master https://github.com/jerryc127/hexo-theme-butterfly.git themes/butterfly


⚙️ 主题配置
启用主题:修改您的 Hexo 配置文件 _config.yml:
theme: butterfly
安装依赖项:如果您尚未安装 pug 和 stylus 渲染器,请运行:
npm install hexo-renderer-pug hexo-renderer-stylus --save

重启:hexo clean && hexo g && hexo server

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
使用子域名是多网站部署里最常见的方式。你可以给每个网站分配一个子域名,比如:

www.example.com → 主站

shop.example.com → 电商站

blog.example.com → 博客

1️⃣ DNS 配置

在你的域名解析后台(阿里云、Cloudflare、Namecheap 等)添加子域名 A 记录,指向服务器 IP:

主机记录 类型 记录值 TTL
www A 123.123.123.123 10分钟
shop A 123.123.123.123 10分钟
blog A 123.123.123.123 10分钟

如果使用泛域名 *.example.com,也可以所有子域名都指向同一 IP。

2️⃣ Nginx 配置示例(子域名方式)
# 主站 www
server {
listen 80;
server_name www.example.com;

root /var/www/main;
index index.html;
}

# 电商站 shop
server {
listen 80;
server_name shop.example.com;

root /var/www/shop;
index index.html;
}

# 博客 blog
server {
listen 80;
server_name blog.example.com;

root /var/www/blog;
index index.html;
}

3️⃣ 测试 & 重载
sudo nginx -t
sudo systemctl reload nginx


然后访问:

http://www.example.com → 主站

http://shop.example.com → 电商站

http://blog.example.com → 博客

Nginx 会根据请求的 Host 自动匹配对应的 server_name,返回对应网页。

✅ 优点

不占用额外端口,所有网站用标准 80/443 端口

DNS 可灵活管理,支持分发到不同服务器

子域名 SSL 证书可独立或用泛域名证书

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
服务器与域名解析详解(国内 & 海外)
1. 服务器概述

服务器是提供数据和服务的计算机或软件系统,分为 物理服务器 和 云服务器,按地理位置又可分为 国内服务器 和 海外服务器。

1.1 国内服务器

提供商:阿里云、腾讯云、华为云等

特点:

访问速度快(对国内用户友好)

受中国法律监管,需要 ICP备案

通常带有完善的云安全和监控系统

适用场景:

面向国内用户的企业网站、电商、应用服务等

1.2 海外服务器

提供商:AWS、DigitalOcean、Linode、Vultr、Hetzner 等

特点:

国际访问速度较快

不需要国内备案

价格相对灵活

适用场景:

面向海外用户的网站或服务

全球访问加速需求的应用

2. 域名与DNS概述

域名是网站在互联网上的“门牌号”,DNS(Domain Name System)是将域名转换为服务器 IP 地址的系统。

2.1 域名的组成

顶级域名(TLD):如 .com, .cn, .org

二级域名:如 example.com

子域名:如 www.example.com

2.2 DNS解析流程

用户在浏览器输入域名 www.example.com

浏览器查询本地缓存,若无记录则请求 操作系统 DNS

DNS 递归解析服务器(如 8.8.8.8 / 阿里 DNS)查找域名对应的 IP

返回 IP 后,浏览器向服务器发起 HTTP/HTTPS 请求

2.3 常用DNS记录类型
类型 描述 示例
A 域名映射到 IPv4 地址 www.example.com -> 115.190.154.26
AAAA 域名映射到 IPv6 地址 www.example.com -> 2400:xxxx:xxxx
CNAME 别名记录,指向另一个域名 blog.example.com -> www.example.com
MX 邮件交换记录 mail.example.com
TXT 文本记录,用于验证或配置 SPF/DKIM google-site-verification=xxx
NS 指定域名服务器 example.com -> dns1.example.com
3. 域名解析流程实例(国内&海外)
3.1 国内域名解析

在 阿里云/腾讯云/华为云注册域名

登录域名管理控制台,设置 A记录/CNAME记录,指向国内服务器 IP

若访问国内用户,建议使用 云解析DNS,提高解析速度

对于网站访问,需完成 ICP备案(法律要求)

示例:

A记录:
@ 115.190.154.26
www 115.190.154.26


注意:国内备案后,域名才能在国内用户正常访问。

3.2 海外域名解析

在 Namecheap / GoDaddy / Cloudflare 等注册域名

设置 A 记录指向海外服务器 IP

无需国内备案,但访问国内用户速度可能受限

可结合 CDN(如 Cloudflare) 加速访问

示例:

A记录:
@ 139.59.12.34
www 139.59.12.34

4. 服务器与域名的配置步骤
4.1 国内服务器配置

服务器部署:

云服务器(ECS/云主机)系统:Linux/Windows

安装 Web 服务:Nginx/Apache

域名解析:

登录域名控制台,添加 A/CNAME 记录

备案流程:

提交个人/企业信息

等待 ICP 审核通过(一般 1~20 个工作日)

Nginx 配置示例:

server {
listen 80;
server_name codehelps.online www.codehelps.online;
root /var/www/html;
index index.html index.htm;
}


生效命令:

nginx -t # 检查配置是否正确
systemctl reload nginx # 重新加载配置

4.2 海外服务器配置

基本步骤与国内类似,但无需备案

可以选择配置 HTTPS(通过 Let’s Encrypt 免费证书)

sudo certbot --nginx -d codehelps.online -d www.codehelps.online

5. 注意事项
场景 注意事项
国内服务器 必须备案,否则访问国内会被屏蔽
海外服务器 国内访问可能慢,可用 CDN 或加速节点
域名到期 域名过期会导致网站无法访问,备案信息不需主动撤销
DNS生效 DNS修改后通常 5~30 分钟生效,最长 48 小时
6. 小结

国内域名+国内服务器:速度快,必须备案

海外域名+海外服务器:无需备案,全球访问方便

混合方案:国内用户走国内服务器,海外用户走海外服务器,结合 CDN 提高访问速度

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
在安卓开发中,内存管理是应用稳定性和性能的关键。了解安卓内存结构、垃圾回收机制和内存优化技巧,可以帮助开发者写出高效、低耗的应用,避免 ANR(应用无响应)和 OOM(内存溢出)问题。

1️⃣ 安卓内存结构概览

安卓应用运行在 Android Runtime (ART) 上,每个应用进程独立,内存主要分为以下几部分:

内存区域 描述
Dalvik/ART 堆(Heap) 存储应用对象,受垃圾回收管理
栈(Stack) 存储方法调用、局部变量,随线程创建和销毁
Native 内存 C/C++ 层分配的内存,如 Bitmap、OpenGL 资源
共享库/内存映射 加载系统库和 APK 资源
内存映射文件(Mapped Files) APK、dex、so 文件映射内存

💡 小技巧:使用 Android Studio Profiler → Memory 可以实时查看各区域内存占用情况。

2️⃣ 垃圾回收机制(GC)

安卓内存管理的核心是 ART 的垃圾回收:

2.1 垃圾回收策略

ART 主要使用 分代 + 并发 GC:

年轻代(Young Generation):

存储短生命周期对象

使用 Copying GC,清理效率高

老年代(Old Generation):

存储长生命周期对象

使用 Mark-Sweep-Compact GC,减少碎片

大对象(如大 Bitmap):

直接分配在老年代或 Native 内存

2.2 GC 类型
类型 特点 适用场景
Serial GC 单线程,简单 小内存设备
Parallel GC 多线程 多核设备
Concurrent GC 并发执行,减少停顿 UI 主线程敏感应用
3️⃣ 内存泄漏与分析
3.1 内存泄漏常见场景

Activity/Fragment 持有 Context 导致无法回收

Handler/Thread 持有外部引用

静态集合存储对象

Bitmap 或 Drawable 未回收

3.2 内存泄漏工具

LeakCanary:实时检测内存泄漏

Memory Profiler:可视化对象分配、堆快照

MAT (Memory Analyzer Tool):分析复杂堆内存

示例使用 LeakCanary:

class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
if (LeakCanary.isInAnalyzerProcess(this)) return
LeakCanary.install(this)
}
}


当 Activity 或对象未被回收时,LeakCanary 会弹出警告。

4️⃣ 内存优化实战
4.1 避免内存泄漏

使用 弱引用(WeakReference) 或 软引用(SoftReference)

避免在静态变量中持有 Context

Activity/Fragment 使用 ViewBinding 时注意解绑

4.2 控制对象创建

重复使用对象池(如 BitmapPool)

减少临时对象创建,尤其在 RecyclerView/循环里

4.3 图片/资源优化

使用 BitmapFactory.Options.inSampleSize 降低内存占用

Glide 或 Coil 自动处理缓存和复用

4.4 内存监控
val runtime = Runtime.getRuntime()
Log.d("Memory", "Max: ${runtime.maxMemory()/1024/1024}MB")
Log.d("Memory", "Total: ${runtime.totalMemory()/1024/1024}MB")
Log.d("Memory", "Free: ${runtime.freeMemory()/1024/1024}MB")


maxMemory:应用可用最大堆

totalMemory:已申请堆

freeMemory:堆中可用空间

5️⃣ 案例:Bitmap 内存优化
// 原始加载大图,容易 OOM
val bitmap = BitmapFactory.decodeResource(resources, R.drawable.large_image)

// 优化:采样缩放
val options = BitmapFactory.Options().apply {
inJustDecodeBounds = true
}
BitmapFactory.decodeResource(resources, R.drawable.large_image, options)
options.inSampleSize = 4 // 缩小 4 倍
options.inJustDecodeBounds = false
val optimizedBitmap = BitmapFactory.decodeResource(resources, R.drawable.large_image, options)

6️⃣ 总结

安卓内存管理核心点:

了解内存结构:Heap、Stack、Native 内存

掌握 GC 原理:年轻代、老年代、并发回收

避免内存泄漏:Context、Handler、静态引用注意

优化对象创建:复用、对象池、图片压缩

实时监控:Profiler、LeakCanary

内存优化不仅提升性能,还能提升用户体验,降低崩溃率。掌握这些原理,你的应用会更稳定、更流畅。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
Kotlin 协程与 Room 数据库异步操作:suspend 与 Flow 全解析

在 Android 开发中,Room 数据库操作不能在主线程执行耗时操作,否则会抛出 android.os.NetworkOnMainThreadException 或 ANR。Kotlin 协程提供了非常方便的方式来处理异步操作,suspend 与 Flow 是其中最常用的两种模式。

1. suspend:异步函数(一次性操作)

suspend 表示挂起函数,它本身不会创建新线程,但可以挂起当前协程,让协程切换到其他线程去执行耗时操作,然后恢复。

用法示例
@Dao
interface GasProjectDao {

@Insert
suspend fun insertGasProject(gasProject: GasProject)

@Query("SELECT * FROM GasProject WHERE id = :id")
suspend fun getGasProjectById(id: Int): GasProject?
}

调用方式
lifecycleScope.launch {
val project = GasProject(name = "测试项目")
gasProjectDao.insertGasProject(project) // 挂起函数在协程中调用

val savedProject = gasProjectDao.getGasProjectById(project.id)
textView.text = "项目名称: ${savedProject?.name}"
}


✅ 特点:

执行一次性操作(插入、查询单条数据)

挂起函数必须在协程中调用

可以和 Dispatchers.IO 配合,实现真正的后台线程执行

2. Flow:异步版可观察列表(持续更新)

Flow 是 Kotlin 协程提供的响应式流,用于表示数据的异步流,类似 RxJava 的 Observable。Room 与 Flow 结合,可以实现数据库表数据变化自动推送到 UI。

用法示例
@Dao
interface GasProjectDao {

@Query("SELECT * FROM GasProject")
fun getAllGasProject(): Flow<List<GasProject>>
}

调用方式
lifecycleScope.launch {
gasProjectDao.getAllGasProject()
.collect { projectList ->
// 每次数据库数据变化,这里都会收到最新列表
recyclerViewAdapter.submitList(projectList)
}
}


✅ 特点:

可观察数据库表的变化(自动推送 UI)

持续订阅,多次更新

可以和协程调度器一起使用,比如切换到 Dispatchers.Main 更新 UI

3. suspend 与 Flow 的区别
特性 suspend Flow
调用频率 一次操作 持续订阅,数据变化多次
线程 可挂起在协程中切换线程 也可挂起,支持调度线程切换
用途 插入、单条查询、一次性更新 列表查询、UI 自动刷新、响应式数据
返回值 具体类型 Flow<T> 流对象,需要 collect
示例 suspend fun insert(...) fun getAll(): Flow<List<...>>
4. 使用技巧与注意事项

主线程安全

suspend 或 Flow 都不能阻塞主线程

UI 更新需要 withContext(Dispatchers.Main) 或 collect 在主线程

一次性 vs 持续更新

插入、更新单条记录 → suspend

观察数据库表变化 → Flow

与 LiveData 结合
Room 支持直接返回 Flow 或 LiveData,可以配合 lifecycleScope 或 viewLifecycleOwner.lifecycleScope

gasProjectDao.getAllGasProject()
.asLiveData()
.observe(this) { list ->
adapter.submitList(list)
}


异常处理
协程可以使用 try/catch 捕获异常,也可以在 Flow 中使用 catch

lifecycleScope.launch {
gasProjectDao.getAllGasProject()
.catch { e -> Log.e("DB", "查询失败", e) }
.collect { list -> adapter.submitList(list) }
}

5. 总结

suspend = 一次异步操作,适合插入、更新、单条查询

Flow = 可观察数据流,适合列表查询、UI 自动刷新

配合 lifecycleScope 可以轻松实现协程 + Room 的异步、响应式编程

💡 技巧:如果你想同时处理一次性操作和持续观察数据,可以 suspend + Flow 结合使用,保持代码简洁、响应快速。

科普:协程使用情况

1️⃣ 默认情况:lifecycleScope.launch { ... }

默认线程:主线程(Dispatchers.Main)。

适合做的操作:

更新 UI(修改 View、TextView、RecyclerView 等)

处理用户交互事件(点击、滑动等)

调用不耗时的本地操作(小量内存计算、轻量对象操作)

示例:

lifecycleScope.launch {
textView.text = "Hello World" // UI操作,主线程
}

2️⃣ 切换到 IO 线程:lifecycleScope.launch(Dispatchers.IO) { ... }

适合操作:

数据库读写(Room 的非 suspend 方法或者耗时操作)

文件操作(读写本地文件)

网络请求(HTTP、Socket)

任何耗时、可能阻塞线程的操作

示例:

lifecycleScope.launch(Dispatchers.IO) {
val list = database.userDao().getAllUsers() // Room suspend 查询,会在IO线程执行
withContext(Dispatchers.Main) {
recyclerView.adapter = UserAdapter(list) // 更新UI要切回主线程
}
}

3️⃣ 混合使用

可以在 同一个协程里 通过 withContext 切换线程:

lifecycleScope.launch {
val list = withContext(Dispatchers.IO) {
database.userDao().getAllUsers() // 后台线程
}
recyclerView.adapter = UserAdapter(list) // 主线程
}


✅ 总结:


UI操作 主线程
Room suspend/数据库操作 IO线程
网络请求 IO线程
轻量计算/内存操作 主线程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
Android 视频与音频播放全解析:自带方案 + 第三方库实战
1️⃣ 技术背景

在移动端开发中,音视频播放是非常常见的功能,例如短视频、直播、音乐播放器等。Android 提供了内置的播放方案,但在复杂业务场景下,通常需要第三方播放器来实现更多功能,比如多格式支持、缓存、后台播放、字幕和流媒体播放。

全面理解 Android 视频音频播放,从基础方案到第三方库使用。

2️⃣ 内置播放方案
2.1 VideoView / MediaPlayer

VideoView 是 Android 提供的高层封装控件,快速上手。

MediaPlayer 是更底层的音视频播放类,支持更多自定义操作。

示例代码:VideoView 播放本地和网络视频
val videoView: VideoView = findViewById(R.id.videoView)
val uri = Uri.parse("http://example.com/video.mp4")
videoView.setVideoURI(uri)
videoView.setMediaController(MediaController(this))
videoView.start()

MediaPlayer 播放音频
val mediaPlayer = MediaPlayer()
mediaPlayer.setDataSource("http://example.com/audio.mp3")
mediaPlayer.prepareAsync()
mediaPlayer.setOnPreparedListener {
it.start()
}


优点:简单、快速、适合本地或单个视频
缺点:功能有限,不支持多码率、缓存或复杂格式

2.2 ExoPlayer

特点:由 Google 官方提供,功能丰富

支持 HLS、DASH、SmoothStreaming、RTMP 等流媒体

支持缓存、后台播放、字幕、速度调节

集成依赖
implementation "com.google.android.exoplayer:exoplayer:2.18.6"
implementation "com.google.android.exoplayer:exoplayer-ui:2.18.6"

示例代码
val player = ExoPlayer.Builder(this).build()
val playerView: PlayerView = findViewById(R.id.playerView)
playerView.player = player

val mediaItem = MediaItem.fromUri("http://example.com/video.mp4")
player.setMediaItem(mediaItem)
player.prepare()
player.play()

3️⃣ 第三方播放器推荐
3.1 IjkPlayer

特点:基于 FFmpeg,支持几乎所有音视频格式

适合:直播、RTMP、复杂视频格式

implementation 'tv.danmaku.ijk.media:ijkplayer-java:0.8.8'
implementation 'tv.danmaku.ijk.media:ijkplayer-armv7a:0.8.8'
implementation 'tv.danmaku.ijk.media:ijkplayer-arm64:0.8.8'

val ijkVideoView: IjkVideoView = findViewById(R.id.ijkVideoView)
ijkVideoView.setVideoPath("http://example.com/live/stream.m3u8")
ijkVideoView.start()

3.2 Vitamio

特点:兼容性好,支持多种视频格式、网络流和字幕

适合:旧项目或需要快速支持多格式

implementation 'io.vov.vitamio:vitamio:5.0.2'

val videoView: io.vov.vitamio.widget.VideoView = findViewById(R.id.videoView)
videoView.setVideoURI(Uri.parse("http://example.com/video.mp4"))
videoView.start()

3.3 ExoPlayer / Media3(音频播放)

特点:Google 官方支持,适合音频流应用

implementation 'androidx.media3:media3-exoplayer:1.1.1'
implementation 'androidx.media3:media3-ui:1.1.1'

val player = ExoPlayer.Builder(this).build()
val playerView: PlayerView = findViewById(R.id.playerView)
playerView.player = player

val mediaItem = MediaItem.fromUri("https://example.com/audio.mp3")
player.setMediaItem(mediaItem)
player.prepare()
player.play()

4️⃣ 播放优化建议
优化点 说明
后台播放 使用 ForegroundService + MediaSession,保证音乐/视频在后台继续播放
缓存与预加载 ExoPlayer 提供 CacheDataSource 支持本地缓存和预加载
音频焦点 管理 AudioFocus 避免与电话或其他音频冲突
UI 不卡顿 解码在后台线程,渲染在 SurfaceView / TextureView
5️⃣ 实战技巧

播放列表:结合 RecyclerView + ExoPlayer 管理多视频

字幕和弹幕:Media3 或自定义 OverlayView 实现

多码率流:使用 ExoPlayer TrackSelector 自动切换清晰度

快进/慢放:ExoPlayer PlaybackParameters 可以控制播放速度

6️⃣ 播放流程图(示意)
[UI交互] -> [Player控件] -> [解码器/ExoPlayer] -> [渲染到SurfaceView/TextureView]

[缓存模块] -> [网络/本地数据源]

[后台服务 / MediaSession]

7️⃣ 总结

自带方案(VideoView / MediaPlayer)简单快速,适合本地播放或基础网络播放

ExoPlayer 功能丰富,适合流媒体播放、缓存、字幕和复杂播放需求

IjkPlayer / Vitamio 在直播、多格式和高兼容性项目中不可替代

关键优化点:后台播放、缓存管理、音频焦点、UI不卡顿

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
目录

协程与调度器总览(Main / IO / Default / Unconfined)—— 为什么需要“切线程”

必备第三方库(Gradle 依赖)及作用说明

在协程中切换线程:withContext、launch 的调度器参数、flowOn 等详解 + 代码示例

常见集成示例:Retrofit(suspend)、Room(suspend / Flow)、OkHttp、Paging3、Lifecycle/Ktx 等

ViewModel + Repository + Retrofit + Room(完整代码片段)

异常处理、取消、Supervisor、结构化并发、性能优化与常见坑



1. 协程与调度器总览(为什么需要切线程)

在 Android 上,有两个核心概念要理解:

主线程(Main / UI 线程):负责绘制 UI、处理用户事件。任何长时间运行或阻塞的操作(网络、数据库、文件、复杂计算)放在主线程会导致 ANR(应用无响应)或界面卡顿。

后台线程(IO / Default):用于耗时操作。协程通过调度器(Dispatcher)将协程切换到合适线程池执行,从而避免卡 UI。

常用调度器:

Dispatchers.Main:运行在主线程(UI)。用于更新 UI、与 Android 框架交互。

Dispatchers.IO:用于网络、文件、数据库等 IO 密集型任务。基于一个可扩展线程池(适合等待型、阻塞型 IO)。

Dispatchers.Default:用于 CPU 密集型任务(复杂计算、排序、图像处理等)。

Dispatchers.Unconfined:常用于某些测试或非常特殊的案例——不是常规选择,可能在调度上下文上行为不稳定。

“切线程” 的常见方式:在协程内部使用 withContext(...) { ... } 来切换执行上下文(线程池)。这是协程推荐的显式切换方式,语义清晰、安全。

2. 必备第三方库(Gradle 依赖)及作用说明

下面是一个常见 Android 项目会用到的协程相关依赖(Kotlin DSL 或 Gradle Groovy 均可):

// Kotlin Coroutines
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3" // 提供 Dispatchers.Main

// AndroidX lifecycle + ktx
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2" // viewModelScope
implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.6.2" // lifecycleScope

// Retrofit + Coroutine (suspend support)
implementation "com.squareup.retrofit2:retrofit:2.9.0"
implementation "com.squareup.retrofit2:converter-moshi:2.9.0" // 或 converter-gson
implementation "com.squareup.okhttp3:okhttp:4.11.0"

// Room
implementation "androidx.room:room-runtime:2.5.2"
kapt "androidx.room:room-compiler:2.5.2"
implementation "androidx.room:room-ktx:2.5.2" // support suspend & Flow

// Kotlin Flow + StateFlow 是 Kotlin stdlib coroutines 的一部分,无额外依赖

// Paging 3(可选)
implementation "androidx.paging:paging-runtime-ktx:3.2.0"


说明:

kotlinx-coroutines-android 提供 Dispatchers.Main 的实现(基于 Android Looper)。

lifecycle-viewmodel-ktx 提供 viewModelScope(自动随 ViewModel 清理)。

retrofit + suspend:Retrofit 本身会把 suspend 接口函数当成挂起点(配合 OkHttp)。你可以直接在 Retrofit 的 service 定义 suspend fun getUsers(): List<User>。

room-ktx 提供 suspend DAO 方法 + 返回 Flow<T> 的集成,便于结合协程使用。

3. 在协程中切换线程:withContext、launch 的调度器参数、flowOn 等详解
3.1 launch 本身接收 Dispatcher
viewModelScope.launch(Dispatchers.IO) {
// 协程的默认执行上下文是 IO(适合在这里直接做网络/DB)
val data = repository.loadFromNetwork()
withContext(Dispatchers.Main) {
// 回到主线程更新 UI
_uiState.value = data
}
}

3.2 withContext:显式切换并返回结果(推荐)

withContext 会挂起当前协程并把代码块切到目标调度器执行,执行完再切回调用者上下文(如果需要)。

viewModelScope.launch {
// 默认上下文为 Main(viewModelScope)
val data = withContext(Dispatchers.IO) {
api.getUsers() // 在 IO 池运行
}
// 回到 Main 继续执行
textView.text = data[0].name
}


推荐模式:在 ViewModel 中用 viewModelScope.launch { val r = withContext(Dispatchers.IO) { ... } ... }
这样将 IO 逻辑隔离在 withContext(Dispatchers.IO) 块中,方便单元测试与可读性。

3.3 async + await 与并发

当需要并发请求时使用 async(注意 async 默认是懒或立即启动取决于参数):

viewModelScope.launch {
val deferred1 = async(Dispatchers.IO) { api.loadA() }
val deferred2 = async(Dispatchers.IO) { api.loadB() }
val a = deferred1.await()
val b = deferred2.await()
// 合并结果
}


或更安全的 coroutineScope 包裹并发任务实现结构化并发:

viewModelScope.launch {
coroutineScope {
val a = async(Dispatchers.IO) { api.loadA() }
val b = async(Dispatchers.IO) { api.loadB() }
// 如果一个失败,coroutineScope 会取消其它协程
}
}

3.4 Flow 的 flowOn 与 collect 在 Main/IO 的区别

flow {} 定义的数据生产(emit)部分默认在调用者的上下文执行,flowOn() 用来改变上游执行调度器:

val dataFlow = flow {
val data = db.query() // 这是生产端,应该在 IO
emit(data)
}.flowOn(Dispatchers.IO) // 指定上游在 IO 执行

lifecycleScope.launch {
dataFlow.collect { data ->
// collect 在 Main(当前作用域)执行,可直接更新 UI
}
}


注意:flowOn 只影响上游发射;collect 的代码(下游)仍在调用 collect 时的上下文运行。

3.5 Dispatchers.Main.immediate

Main.immediate 会尝试在当前主线程立即执行,如果当前已经在主线程则不会重新调度。适用于需要避免重复切换但要保证在主线程执行的场景(较少用)。

4. 常见集成示例(更具体)
4.1 Retrofit + suspend

定义接口:

interface ApiService {
@GET("users")
suspend fun getUsers(): List<User>
}


直接在协程中调用 api.getUsers() —— Retrofit 会在 OkHttp 的线程池执行网络请求并恢复协程。

注意:不要在 suspend 中又使用回调(反模式),把回调包装成 suspend 才行。

4.2 Room + suspend / Flow

DAO 示例:

@Dao
interface UserDao {
@Query("SELECT * FROM user")
fun observeAll(): Flow<List<User>> // 推荐:Flow 自动发射 DB 更新

@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertAll(users: List<User>) // suspend 插入
}

4.3 Retrofit + OkHttp 拦截器(logging、token 等)

OkHttp 是在网络层处理的,与协程无直接关系,但在协程中使用 suspend 的 Retrofit 时,请确保 OkHttp 的拦截器不会阻塞主线程(拦截器本身运行在 OkHttp 的线程池)。

4.4 Paging3(与协程整合)

Paging 3 有 PagingSource 返回 PagingData,结合 Pager 和 flow 使用,推荐与 lifecycleScope 一起收集。

5. 实战:ViewModel → Repository → Retrofit + Room(完整示例)

下面给出一个合理的分层示例(简化版),展示如何在协程中切 IO/Main、如何处理异常与缓存到 Room。

Gradle(关键依赖)

(见第2节)

Repository(数据层)
class UserRepository(
private val api: ApiService,
private val userDao: UserDao
) {
// 1) 直接暴露本地 Flow(Room)
val usersFlow: Flow<List<User>> = userDao.observeAll()
.flowOn(Dispatchers.IO) // 上游读取 DB 在 IO

// 2) 同步刷新:从网络拉取并存本地
suspend fun refreshFromNetwork(): Result<Unit> {
return try {
val remote = withContext(Dispatchers.IO) {
api.getUsers() // network in IO
}
withContext(Dispatchers.IO) {
userDao.insertAll(remote) // DB write in IO
}
Result.success(Unit)
} catch (e: Exception) {
Result.failure(e)
}
}

// 3) 安全封装模板(可复用)
suspend fun <T> safeApiCall(block: suspend () -> T): Result<T> {
return try {
val res = withContext(Dispatchers.IO) { block() }
Result.success(res)
} catch (e: Exception) {
Result.failure(e)
}
}
}

ViewModel(逻辑层)
class UserViewModel(
private val repo: UserRepository
) : ViewModel() {

// StateFlow 封装 UI 状态
private val _uiState = MutableStateFlow<List<User>>(emptyList())
val uiState: StateFlow<List<User>> = _uiState.asStateFlow()

init {
// 收集本地 DB 的 Flow 并更新 UI(收集在 Main)
viewModelScope.launch {
repo.usersFlow.collect { users ->
_uiState.value = users
}
}
}

// 手动刷新网络
fun refresh() {
viewModelScope.launch {
// 如果不想阻塞主线程,可在 launch(Dispatchers.IO) 直接做
val res = repo.safeApiCall { api.getUsers() }
res.onSuccess {
// 如果 safeApiCall 已在 IO 写 DB, 不需要再切线程
// UI 更新通过 usersFlow 自动触发
}.onFailure {
// 在 Main 处理错误提示
}
}
}
}

Activity / Fragment(UI 层)
class UserFragment : Fragment(R.layout.fragment_user) {
private val vm: UserViewModel by viewModels()

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
lifecycleScope.launchWhenStarted {
vm.uiState.collect { users ->
// 这里运行在 Main,可以直接更新 RecyclerView
adapter.submitList(users)
}
}

refreshButton.setOnClickListener {
vm.refresh()
}
}
}


几点说明:

usersFlow.flowOn(Dispatchers.IO) 把 DB 读取放到 IO。

collect 在 lifecycleScope.launchWhenStarted {} 的上下文(Main)运行,因此无需显式切 Main 来更新 UI。

所有网络/DB 写操作都放到 IO(withContext(Dispatchers.IO) 或 launch 参数)。

6. 异常处理、取消、Supervisor、结构化并发、性能优化与常见坑
6.1 异常处理

父协程遇到未捕获异常会取消其子协程(结构化并发)。

使用 CoroutineExceptionHandler 只能捕获协程的顶层异常(launch 启动的协程),对 async 的异常需在 await() 时处理。

示例:

val handler = CoroutineExceptionHandler { _, e ->
Log.e("TAG", "Caught $e")
}
viewModelScope.launch(handler) {
// any uncaught exception here will be handled
}


对于 async:

val deferred = viewModelScope.async { throw RuntimeException("err") }
try {
deferred.await()
} catch (e: Exception) {
// 处理
}

6.2 取消(Cancellation)

协程是协作式取消:suspend 函数需检查取消点(例如 delay, withContext, suspendCancellableCoroutine 等会响应取消)。

若在 while(true) 中做工作,需手动检查 isActive:

import kotlinx.coroutines.isActive

viewModelScope.launch {
while (isActive) {
// do work
}
}

6.3 SupervisorJob(隔离子失败)

默认 Job:子协程失败会取消父协程并传播取消。若希望子失败不影响其他子协程使用 SupervisorJob()。

val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main)

6.4 性能优化

避免创建大量短生命周期 CoroutineScope。尽量复用 viewModelScope、lifecycleScope 或自定义长寿命 scope。

IO 操作尽量用 Dispatchers.IO,计算密集用 Default。

避免在生产端(flow 的 emit)做太重的工作:把重计算放到 map + flowOn(Dispatchers.Default)。

对并发量控制:对于大量并发请求,限制同时并发的数量(通过 Semaphore 或 Dispatcher 限制线程池)。

6.5 常见坑与纠正

不要在 suspend 函数里混用回调(未转换为 suspend),否则可能造成线程混乱。用 suspendCancellableCoroutine 把回调桥接为 suspend。

不要把所有逻辑都放在 Main scope:在 Main 中包含大量 withContext(Dispatchers.IO) 可以,但更清晰的写法是 launch(Dispatchers.IO) 来执行后台任务并在结束时切到 Main 更新 UI。

不要滥用 GlobalScope:会造成泄漏与生命周期管理困难。

async 未 await:可能会导致异常丢失(未触发时不会抛出)。

7. 最佳实践清单

使用结构化并发(viewModelScope/lifecycleScope),不要用 GlobalScope。

明确职责分层:UI(Fragment/Activity)只做展示,ViewModel 管理 UI 状态,Repository 负责数据源(网络/DB)。

IO 放 Dispatchers.IO;计算放 Default;UI 放 Main。

用 withContext 做显式切线程,更可读、易测试。

网络使用 suspend 的 Retrofit 接口;DB 使用 Room 的 suspend/Flow。

用 Flow + StateFlow 管理状态流(单向数据流)。

使用 safeApi 封装统一的错误处理与超时策略(withTimeout)。

对并发操作使用 async + await,但在发生失败时确保正确捕获异常。

使用 CoroutineExceptionHandler 处理顶层异常;对于 async 的异常在 await 时处理。

在需要子协程相互隔离失败时使用 SupervisorJob。

避免阻塞线程(Thread.sleep 等),使用 delay 与挂起函数。

测试时用 TestCoroutineDispatcher / runBlockingTest(或新版的 TestDispatcher)。

附:快速参考代码片段(最精简版)
// Retrofit service
interface ApiService {
@GET("users")
suspend fun getUsers(): List<User>
}

// Repository
class Repo(val api: ApiService, val dao: UserDao) {
val usersFlow = dao.observeAll().flowOn(Dispatchers.IO)
suspend fun refresh() = withContext(Dispatchers.IO) {
val list = api.getUsers()
dao.insertAll(list)
}
}

// ViewModel
class VM(val repo: Repo): ViewModel() {
val ui = repo.usersFlow.stateIn(viewModelScope, SharingStarted.Lazily, emptyList())
fun refresh() = viewModelScope.launch { repo.refresh() }
}

// Fragment
lifecycleScope.launchWhenStarted {
vm.ui.collect { adapter.submitList(it) }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
安卓多种通知 UI 的实现方式

在安卓开发中,我们经常需要在数据发生变化时通知 UI 更新,例如列表数据变化、状态改变、网络请求结果等。不同的项目架构、技术栈和团队习惯会导致选择不同的通知机制。本文整理了安卓常用的通知 UI 的方式,并分析其原理、优缺点和使用场景。

1. LiveData(Jetpack 组件)
特点

LiveData 是 Jetpack 架构组件的一部分。

具有生命周期感知(Lifecycle-aware),只在 Activity 或 Fragment 活跃时才更新 UI。

支持单向数据流,数据源更新,观察者自动收到通知。

用法示例
// ViewModel
val userName = MutableLiveData<String>()

// Activity/Fragment
viewModel.userName.observe(this) { name ->
textView.text = name
}

优点

生命周期感知,避免内存泄漏。

与 MVVM 架构自然结合。

简单易用,官方推荐。

缺点

只能在主线程更新(虽然可以用 postValue 跨线程,但有延迟)。

单向数据流,复杂事件管理可能不方便。

2. StateFlow / SharedFlow(Kotlin Coroutines)
特点

Kotlin 协程的 Flow 变体,用于响应式数据流。

StateFlow 持有最新状态,SharedFlow 用于事件流(类似 RxJava 的 PublishSubject)。

支持冷流、热流,灵活结合协程和生命周期。

用法示例
// ViewModel
private val _count = MutableStateFlow(0)
val count: StateFlow<Int> = _count

fun increment() {
_count.value += 1
}

// Fragment
lifecycleScope.launch {
viewModel.count.collect { value ->
textView.text = value.toString()
}
}

优点

与协程天然结合,异步友好。

可以处理复杂事件和状态流。

支持缓存最新值(StateFlow)。

缺点

使用前需要熟悉协程。

Lifecycle-aware 需要手动配合 lifecycleScope。

3. RxJava / RxKotlin(响应式编程)
特点

响应式流库,可实现复杂的事件组合、转换和过滤。

支持多线程切换、链式操作。

用法示例
val subject = PublishSubject.create<String>()

// 订阅
subject.observeOn(AndroidSchedulers.mainThread())
.subscribe { value -> textView.text = value }

// 发布
subject.onNext("Hello RxJava")

优点

功能强大,适合复杂事件流处理。

可实现跨组件通知。

支持背压、线程调度等高级特性。

缺点

学习成本较高。

容易产生内存泄漏,需要 CompositeDisposable 管理。

项目依赖大,现代 Android 官方更推荐使用 Flow/LiveData。

4. EventBus / Otto(事件总线)
特点

基于发布-订阅模式,实现跨组件消息传递。

组件之间无需直接引用。

用法示例(GreenRobot EventBus)
// 定义事件
data class MessageEvent(val message: String)

// 注册和接收
@Subscribe(threadMode = ThreadMode.MAIN)
fun onMessageEvent(event: MessageEvent) {
textView.text = event.message
}

// 发布事件
EventBus.getDefault().post(MessageEvent("Hello EventBus"))

优点

跨组件通信方便。

简化复杂 UI 回调链。

支持线程切换。

缺点

容易滥用,变成“全局状态管理”。

难以追踪调用链,调试困难。

生命周期管理需要手动处理注册/注销。

5. 接口回调(Callback / Listener)
特点

最传统的方式,通过接口实现组件间通信。

常用于 Adapter -> Activity / Fragment 更新 UI。

用法示例
interface OnItemClickListener {
fun onClick(position: Int)
}

adapter.setOnItemClickListener(object : OnItemClickListener {
override fun onClick(position: Int) {
textView.text = "Clicked $position"
}
})

优点

简单直接,无需依赖库。

适合局部通信。

缺点

不适合复杂跨组件通信。

易产生内存泄漏(尤其是匿名内部类持有 Activity)。

6. Handler / Message / Looper(线程间通信)
特点

Android 原生机制,用于线程间消息通知。

UI 线程通过 Handler 接收子线程消息更新 UI。

用法示例
val handler = Handler(Looper.getMainLooper()) { msg ->
textView.text = msg.obj as String
true
}

// 子线程发送消息
Thread {
val msg = Message.obtain()
msg.obj = "Hello Handler"
handler.sendMessage(msg)
}.start()

优点

精确控制线程。

官方原生,无第三方依赖。

缺点

代码繁琐,不适合复杂数据流。

与现代架构(MVVM / Jetpack)结合较差。

7. ContentProvider / LiveData + Room(数据库驱动 UI 更新)
特点

数据库变化触发 UI 更新。

Room + LiveData 可自动通知观察者。

用法示例
@Dao
interface UserDao {
@Query("SELECT * FROM user")
fun getAllUsers(): LiveData<List<User>>
}

优点

数据和 UI 双向绑定。

可实现持久化与实时更新结合。

缺点

适合持久化数据,不适合临时事件。

依赖 Room 和 LiveData。

8. BroadcastReceiver(系统广播或自定义广播)
特点

通过广播机制通知多个组件。

可以是系统广播或自定义广播。

用法示例
// 注册广播
val filter = IntentFilter("com.example.ACTION_UPDATE")
registerReceiver(receiver, filter)

// 发送广播
val intent = Intent("com.example.ACTION_UPDATE")
sendBroadcast(intent)

优点

可跨 App 或跨组件通知。

系统级事件监听。

缺点

性能开销大,不适合高频事件。

生命周期管理需要注意。

9. 总结对比表
| 方式 | 生命周期感知 | 跨组件 | 异步支持 | 使用场景 |
|--------------------------|-------------------|----------------|---------------|---------------------------|
| LiveData | ✅ | ❌ | 主线程 | MVVM UI 更新 |
| StateFlow / SharedFlow | ⚠️ 手动管理 | ✅ | ✅ 协程 | 复杂状态流 |
| RxJava | ❌ | ✅ | ✅ | 复杂事件流 / 跨组件 |
| EventBus | ❌ | ✅ | ✅ | 简单跨组件事件 |
| Callback / Listener | ❌ | ❌ | ⚠️ | 局部回调 |
| Handler / Message | ❌ | ❌ | ✅ | 子线程 -> UI |
| Room + LiveData | ✅ | ⚠️ | ✅ | 数据库驱动 UI |
| BroadcastReceiver | ❌ | ✅ | ⚠️ | 系统或跨组件事件 |