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
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
深入理解 LinkedBlockingQueue 队列

在 Android 开发中,我们经常需要处理多线程任务,例如后台数据下载、消息分发、任务调度等。这时候 线程安全的队列 就显得非常重要,而 LinkedBlockingQueue 是 Java/Kotlin 提供的经典选择。

本文将从概念、用法、关键方法、实际应用和示例代码,全面讲解 LinkedBlockingQueue 的使用。

一、什么是 LinkedBlockingQueue?

LinkedBlockingQueue 是 Java 并发包 (java.util.concurrent) 下的阻塞队列实现。它的特点:

链表结构存储:内部通过链表存储元素,插入和删除效率高。

线程安全:支持多线程并发操作,无需额外加锁。

阻塞特性:

当队列满时,生产者线程会阻塞,直到有空间。

当队列空时,消费者线程会阻塞,直到有元素。

可选容量:

可指定最大容量,也可无限容量(默认是 Integer.MAX_VALUE)。

二、关键概念

生产者-消费者模式
LinkedBlockingQueue 非常适合生产者-消费者模式:

生产者线程负责向队列添加任务。

消费者线程负责从队列取出任务执行。

队列作为缓冲区,保证线程安全且无需手动同步。

阻塞 vs 非阻塞

阻塞方法:put() / take()

非阻塞方法:offer() / poll() / peek()

容量控制
队列容量可控,避免无限增长造成内存压力。

三、基本用法(Kotlin 示例)
1. 导入包
import java.util.concurrent.LinkedBlockingQueue

2. 定义队列
// 有容量限制的队列
val queue = LinkedBlockingQueue<String>(5)

// 无容量限制的队列
val unlimitedQueue = LinkedBlockingQueue<String>()

3. 入队操作
// 阻塞入队,如果队列满,会等待
queue.put("任务1")

// 非阻塞入队,队列满返回 false
val success = queue.offer("任务2")

4. 出队操作
// 阻塞出队,如果队列空,会等待
val task = queue.take()

// 非阻塞出队,队列空返回 null
val polledTask = queue.poll()

5. 查看队列元素
// 查看队头元素,但不移除
val head = queue.peek()

四、结合 Android 的多线程示例

假设我们要在安卓中做一个后台任务队列,生产者产生任务(如网络请求),消费者执行任务(如处理结果)。

class TaskQueueManager {

private val taskQueue = LinkedBlockingQueue<String>(10)

init {
// 启动消费者线程
Thread { consumeTasks() }.start()
}

// 生产任务
fun produceTask(task: String) {
Thread {
try {
taskQueue.put(task) // 队列满会阻塞
Log.d("TaskQueue", "任务入队: $task")
} catch (e: InterruptedException) {
e.printStackTrace()
}
}.start()
}

// 消费任务
private fun consumeTasks() {
while (true) {
try {
val task = taskQueue.take() // 队列空会阻塞
Log.d("TaskQueue", "处理任务: $task")
// 模拟任务处理
Thread.sleep(500)
} catch (e: InterruptedException) {
e.printStackTrace()
}
}
}
}


使用示例:

val manager = TaskQueueManager()
manager.produceTask("下载图片")
manager.produceTask("上传数据")
manager.produceTask("处理日志")


特点:

生产者和消费者完全解耦。

队列自动控制线程安全。

阻塞队列确保在高并发下不会丢任务。

五、常用方法总结
方法 功能 阻塞特性
put(E e) 入队,如果队列满,阻塞 阻塞
take() 出队,如果队列空,阻塞 阻塞
offer(E e) 入队,不阻塞,队列满返回 false 非阻塞
poll() 出队,不阻塞,队列空返回 null 非阻塞
peek() 查看队头元素,不移除,队列空返回 null 非阻塞
remainingCapacity() 查看剩余可用容量 非阻塞
六、应用场景

后台任务队列:图片下载、上传、文件处理。

消息分发系统:事件通知、日志分发。

生产者-消费者模型:游戏逻辑、AI Agent任务队列。

限流场景:通过指定容量控制并发量。

七、总结

LinkedBlockingQueue 是 Android 开发中非常实用的线程安全队列,尤其适合 生产者-消费者模式 和多线程任务调度。

通过 Kotlin 的简单封装和线程启动方式,可以快速在安卓中实现可靠的任务队列,避免手动同步、线程安全问题,并提供阻塞/非阻塞两种操作模式,非常适合 AI Agent 或后台任务管理。

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
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
Android 中实现稳定的 Socket 长连接:自动重连、心跳机制与前台服务保活

在实时通讯、物联网、无人机遥控、设备上传、IM(即时通讯)等场景中,Socket 长连接是最关键的通信方式之一。

但在 Android 上稳定维护 Socket 长连接并不是一件容易的事,尤其是:

App 切后台后连接断开

网络切换(4G/WiFi)导致连接失败

服务被系统回收

心跳不稳定导致服务端主动断开

需要自动重连、保活

本篇文章从实践角度讲解:
如何在 Android 中构建一个 稳定、可重连、可保活 的 Socket 通信模块。

1. 整体架构设计

推荐使用 前台服务 + 单独 Socket 线程 + RxJava 消息分发 的组合:

SocketService(前台服务)

├── SocketClient(核心连接模块)
│ ├── connect()
│ ├── disconnect()
│ ├── send()
│ ├── listenThread(接收消息)
│ ├── heartBeatThread(心跳包)
│ ├── autoReconnect()

└── DataBus(RxJava 发布订阅)


优点:

服务不容易被杀死 → 前台服务

Socket 独立线程运行 → 不阻塞 UI

RxJava → 方便上层 Activity 订阅数据

独立心跳和自动重连机制 → 稳定持续通信

2. 创建前台服务 SocketService
public class SocketService extends Service {

private SocketClient socketClient;

@Override
public void onCreate() {
super.onCreate();
startForegroundService();
socketClient = new SocketClient("192.168.1.100", 9000);
socketClient.connect();
}

@Override
public int onStartCommand(Intent intent, int flags, int startId) {
return START_STICKY; // 保活关键
}

@Nullable
@Override
public IBinder onBind(Intent intent) {
return null;
}

private void startForegroundService() {
NotificationChannel channel = new NotificationChannel(
"socket_channel", "Socket Service", NotificationManager.IMPORTANCE_LOW
);
NotificationManager manager = getSystemService(NotificationManager.class);
manager.createNotificationChannel(channel);

Notification notification = new Notification.Builder(this, "socket_channel")
.setContentTitle("Socket 正在运行")
.setSmallIcon(R.mipmap.ic_launcher)
.build();

startForeground(1, notification);
}
}


关键点:

START_STICKY 让服务被系统杀死后自动重启

前台服务可以在后台长期稳定运行

3. SocketClient:核心连接模块
public class SocketClient {

private String host;
private int port;
private Socket socket;
private boolean isRunning = false;

public SocketClient(String host, int port) {
this.host = host;
this.port = port;
}

public void connect() {
new Thread(() -> {
try {
socket = new Socket();
socket.connect(new InetSocketAddress(host, port), 5000);
isRunning = true;

startReadThread();
startHeartBeat();

} catch (Exception e) {
autoReconnect();
}
}).start();
}

4. 自动重连机制
private void autoReconnect() {
new Thread(() -> {
while (!isRunning) {
try {
Thread.sleep(3000);
Log.d("Socket", "正在重连...");
connect();
} catch (InterruptedException ignored) {}
}
}).start();
}


特点:

服务端断开 / 网络切换时自动重连

防止频繁重连 → 每 3 秒尝试一次

5. 心跳包(Heartbeat)

为了防止服务器认为客户端掉线,需要定时发送心跳:

private void startHeartBeat() {
new Thread(() -> {
while (isRunning) {
try {
send("HEARTBEAT".getBytes());
Thread.sleep(5000);
} catch (Exception e) {
isRunning = false;
autoReconnect();
}
}
}).start();
}


你也可以使用协议规定的心跳帧,比如 0x55 AA 00 01 00.

6. 接收消息线程
private void startReadThread() {
new Thread(() -> {
try {
InputStream in = socket.getInputStream();
byte[] buffer = new byte[1024];

while (isRunning) {
int len = in.read(buffer);
if (len > 0) {
byte[] data = Arrays.copyOf(buffer, len);
DataBus.post(data);
}
}

} catch (Exception e) {
isRunning = false;
autoReconnect();
}
}).start();
}


收到的消息通过 DataBus 分发到 UI。

7. RxJava 消息总线
public class DataBus {
private static final PublishSubject<byte[]> bus = PublishSubject.create();

public static void post(byte[] data) {
bus.onNext(data);
}

public static Observable<byte[]> toObservable() {
return bus;
}
}


在 Activity 中监听:

DataBus.toObservable()
.subscribe(bytes -> {
Log.d("SocketData", ByteUtils.toHex(bytes));
});

8. 发送数据
public void send(byte[] data) {
new Thread(() -> {
try {
OutputStream out = socket.getOutputStream();
out.write(data);
out.flush();
} catch (Exception e) {
isRunning = false;
autoReconnect();
}
}).start();
}

9. Activity 中启动服务并通信
Intent intent = new Intent(this, SocketService.class);
startForegroundService(intent);

findViewById(R.id.btn_send).setOnClickListener(v -> {
socketClient.send("Hello".getBytes());
});

10. 总结:这是一套可上线的 Socket 长连接方案
功能 状态
前台服务保活 ✔
自动重连 ✔
网络切换重连 ✔
心跳保持在线 ✔
独立读写线程 ✔
RxJava 分发消息 ✔
UI 可随时订阅 ✔


以下是可用工具类:
package com.htnova.common.socket;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class SocketClient {

private String host;
private int port;
private volatile boolean isConnected = false;

private Socket socket;
private BufferedInputStream inputStream;
private BufferedOutputStream outputStream;

private ExecutorService socketThread = Executors.newSingleThreadExecutor();
private ExecutorService sendThread = Executors.newSingleThreadExecutor();

private SocketCallback callback;
private boolean isManualClose = false;

private int reconnectDelay = 3000; //自动重连间隔
private int heartbeatInterval = 5000; //心跳间隔(毫秒)
private byte[] heartbeatData = "HEARTBEAT".getBytes();

public interface SocketCallback {
void onConnected();
void onDisconnected();
void onMessage(byte[] msg);
void onError(Exception e);
}

public SocketClient(String host, int port, SocketCallback callback) {
this.host = host;
this.port = port;
this.callback = callback;
}

/**
* 开始连接
*/
public void connect() {
isManualClose = false;
socketThread.execute(() -> startConnect());
}

private void startConnect() {
try {
socket = new Socket();
socket.connect(new InetSocketAddress(host, port), 5000);
socket.setKeepAlive(true);

inputStream = new BufferedInputStream(socket.getInputStream());
outputStream = new BufferedOutputStream(socket.getOutputStream());
isConnected = true;

if (callback != null) callback.onConnected();

startReadLoop();
startHeartBeatLoop();

} catch (Exception e) {
isConnected = false;
if (callback != null) callback.onError(e);
autoReconnect();
}
}

/**
* 消息读取循环
*/
private void startReadLoop() {
socketThread.execute(() -> {
byte[] buffer = new byte[1024];
int len;
try {
while (isConnected && (len = inputStream.read(buffer)) != -1) {
byte[] data = new byte[len];
System.arraycopy(buffer, 0, data, 0, len);

if (callback != null) callback.onMessage(data);
}
} catch (Exception e) {
if (!isManualClose) {
if (callback != null) callback.onError(e);
autoReconnect();
}
} finally {
close();
}
});
}

/**
* 发送心跳包
*/
private void startHeartBeatLoop() {
socketThread.execute(() -> {
while (isConnected) {
try {
Thread.sleep(heartbeatInterval);
send(heartbeatData);
} catch (Exception ignored) { }
}
});
}

/**
* 自动重连
*/
private void autoReconnect() {
if (isManualClose) return;

isConnected = false;
try {
Thread.sleep(reconnectDelay);
} catch (InterruptedException ignored) {}

connect();
}

/**
* 发送数据
*/
public void send(byte[] data) {
sendThread.execute(() -> {
try {
if (isConnected && outputStream != null) {
outputStream.write(data);
outputStream.flush();
}
} catch (Exception e) {
if (callback != null) callback.onError(e);
}
});
}

/**
* 关闭连接
*/
public void close() {
isManualClose = true;
isConnected = false;

try {
if (socket != null) socket.close();
if (inputStream != null) inputStream.close();
if (outputStream != null) outputStream.close();
} catch (IOException ignored) {}

if (callback != null) callback.onDisconnected();
}

/** 设置心跳包 */
public void setHeartbeat(byte[] heartbeat, int intervalMs) {
this.heartbeatData = heartbeat;
this.heartbeatInterval = intervalMs;
}

/** 设置自动重连间隔 */
public void setReconnectDelay(int delayMs) {
this.reconnectDelay = delayMs;
}
}

如何使用?
1. 初始化
SocketClient socketClient = new SocketClient("192.168.1.100", 9000, new SocketClient.SocketCallback() {
@Override
public void onConnected() {
Log.d("Socket", "已连接");
}

@Override
public void onDisconnected() {
Log.d("Socket", "已断开");
}

@Override
public void onMessage(byte[] msg) {
Log.d("Socket", "收到消息:" + new String(msg));
}

@Override
public void onError(Exception e) {
Log.e("Socket", "错误:" + e.getMessage());
}
});

2. 连接服务器
socketClient.connect();

3. 发送数据
socketClient.send("Hello Server".getBytes());

4. 设置心跳包
socketClient.setHeartbeat("PING".getBytes(), 3000);

5. 关闭连接
socketClient.close();





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
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
📌 目录

自定义 View 为什么存在?

自定义控件的 4 大分类

方式一:继承 View(完全自绘)

方式二:继承 ViewGroup(自定义布局)

方式三:组合控件(XML + inflate)

方式四:继承已有控件扩展功能

View 绘制三大流程(Measure/Layout/Draw)

资源文件(attrs.xml,自定义属性)

四种方式对比总结(表格)

最佳实践与性能优化

⭐ 1. 自定义 View 为什么存在?

Android 原生控件有限,自定义 View 可以让我们做:

仪表盘

雷达图

折线图

特效控件

自定义动画

高级布局(FlowLayout、TagLayout)

底层都离不开:测量 + 绘制 + 布局。

⭐ 2. 自定义控件的 4 大分类

Android 自定义控件主要分为 4 类:

类型 继承 是否自己画 是否自布局 是否包含 XML 子控件 使用场景
① 自定义绘制 View View ✔ ❌ ❌ 图形、动画、仪表盘等
② 自定义布局 ViewGroup ViewGroup ❌ ✔ ✔ 自定义复杂布局流式布局
③ 组合控件(复合控件) FrameLayout/LinearLayout 等 部分 部分 ✔(inflate) 自定义输入框、Card、搜索框
④ 扩展已有控件 TextView/ImageView… 可选 ❌ ❌ 扩展行为,如跑马灯、可折叠文字
🟥 3. 方式一:继承 View(完全自绘控件)

适用场景:

绘制图形(仪表盘、波形图)

自定义动画

数据可视化控件

核心点:

重写 onMeasure() 手动测量尺寸

重写 onDraw() 绘制图像

使用 Canvas/Path/Paint

🔧 示例代码
class CircleView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null
) : View(context, attrs) {

private val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = Color.RED
}

override fun onMeasure(widthSpec: Int, heightSpec: Int) {
val defaultSize = 200
val w = resolveSize(defaultSize, widthSpec)
val h = resolveSize(defaultSize, heightSpec)
setMeasuredDimension(w, h)
}

override fun onDraw(canvas: Canvas) {
canvas.drawCircle(width / 2f, height / 2f, width / 2f, paint)
}
}

🟦 4. 方式二:继承 ViewGroup(自定义布局控件)

适用场景:

流式布局 FlowLayout

九宫格

复杂排序布局

自定义 Banner、卡片堆叠布局

关键点:

onMeasure():测量每个子 View

onLayout():摆放子 View 位置

不负责绘制(不重写 onDraw)

🔧 示例代码示例(FlowLayout)
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
var lineWidth = 0
var totalHeight = paddingTop + paddingBottom
val widthSize = MeasureSpec.getSize(widthMeasureSpec)

for (i in 0 until childCount) {
val child = getChildAt(i)
measureChild(child, widthMeasureSpec, heightMeasureSpec)

if (lineWidth + child.measuredWidth > widthSize) {
totalHeight += child.measuredHeight
lineWidth = 0
}
lineWidth += child.measuredWidth
}

setMeasuredDimension(widthSize, totalHeight)
}

override fun onLayout(p0: Boolean, l: Int, t: Int, r: Int, b: Int) {
var x = paddingLeft
var y = paddingTop
val width = r - l

for (i in 0 until childCount) {
val child = getChildAt(i)
if (x + child.measuredWidth > width) {
x = paddingLeft
y += child.measuredHeight
}
child.layout(x, y, x + child.measuredWidth, y + child.measuredHeight)
x += child.measuredWidth
}
}

🟩 5. 方式三:组合控件(XML + inflate)

最常见的自定义控件方式。

适用场景:

自定义 TitleBar

自定义输入框(带图标、清理按钮)

SearchView、自定义卡片控件

核心点:使用 LayoutInflater

🔧 示例代码
class SearchBar @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null
) : FrameLayout(context, attrs) {

init {
LayoutInflater.from(context)
.inflate(R.layout.view_search_bar, this)
}
}


view_search_bar.xml:

<LinearLayout ... >
<ImageView android:src="@drawable/ic_search"/>
<EditText android:hint="搜索"/>
</LinearLayout>


优点:

复用已有控件,效率高

易扩展、易维护

高度可定制化

🟨 6. 方式四:继承已有控件(行为扩展型)

适用场景:

扩展 EditText(限制输入)

扩展 TextView(自定义跑马灯)

扩展 ImageView(实现圆角图片)

示例:

class RoundImageView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null
) : AppCompatImageView(context, attrs) {

private val path = Path()

override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
path.reset()
path.addRoundRect(
0f, 0f, w.toFloat(), h.toFloat(),
20f, 20f,
Path.Direction.CW
)
}

override fun onDraw(canvas: Canvas) {
canvas.save()
canvas.clipPath(path)
super.onDraw(canvas)
canvas.restore()
}
}

🎨 7. View 绘制三大流程(适用于所有方式)

核心流程图:

Measure → Layout → Draw

① onMeasure:测量大小

决定 View 的宽高(wrap_content 逻辑写这里)

② onLayout:对子 View 摆放位置

只存在于 ViewGroup

③ onDraw:绘制内容

纯 View 重绘的核心

🧩 8. 自定义属性(attrs.xml)

几乎所有自定义控件都需要支持 XML 属性。

attrs.xml:

<declare-styleable name="CircleView">
<attr name="circleColor" format="color"/>
</declare-styleable>


使用:

<com.xxx.CircleView
app:circleColor="@color/red"/>


读取:

val typed = context.obtainStyledAttributes(attrs, R.styleable.CircleView)
paint.color = typed.getColor(R.styleable.CircleView_circleColor, Color.BLACK)
typed.recycle()

📊 9. 四种方式对比总结
类型 自绘 子 View 自布局 难度 性能
纯 View(绘制型) ✔ ❌ ❌ ⭐⭐⭐⭐ ⭐⭐⭐⭐⭐
ViewGroup(布局型) ❌ ✔ ✔ ⭐⭐⭐⭐⭐ ⭐⭐⭐
组合控件(XML inflate) 部分 ✔ 部分 ⭐⭐ ⭐⭐⭐⭐
扩展已有控件 部分 ❌ ❌ ⭐ ⭐⭐⭐⭐⭐
🧠 10. 性能优化建议

不要在 onDraw() 里创建对象

使用 postInvalidate() 更新 UI(子线程)

使用硬件加速(默认开启)

谨慎使用 saveLayer()(会创建离屏缓冲)

减少过深的 View 层级(组合控件时注意)

📌 最终总结

Android 自定义控件的核心是:

绘制(View) + 布局(ViewGroup) + 组合复用(inflate) + 控件行为扩展

四种方式对应不同场景,理解:

onMeasure

onLayout

onDraw

attrs.xml

即可轻松实现任意定制 UI。



📌 目录

View 绘制体系概述

自定义 View 的本质是什么?

Measure 测量流程解析

Layout 布局流程

Draw 绘制流程

Canvas & Paint 底层原理

自定义 View 的完整模板

常见问题与性能优化

总结

🔥 1. View 绘制体系概述(整体流程图)

Android UI 渲染是 从 ViewRootImpl → DecorView → 各层级 View 一层一层递归传递的。

下面是完整流程图(你可以作为博客插图):

┌───────────────────┐
│ ViewRootImpl │
└─────────┬─────────┘

┌─────────────▼──────────────┐
│ performTraversals() │
└─────────────┬──────────────┘
Measure → Layout → Draw(核心三步骤)

┌────────────┐ ┌────────────┐ ┌────────────┐
│ measure() │ │ layout() │ │ draw() │
└──────┬─────┘ └──────┬─────┘ └──────┬─────┘
│ │ │
▼ ▼ ▼
onMeasure() onLayout() onDraw()

⭐ 2. 自定义 View 的本质是什么?

一句话总结:

自定义 View = 手动实现 Measure + 绘制逻辑 + 事件逻辑。

Android 框架提供了一个基础绘制管线,开发者只需要实现以下部分:

测量:决定 View 的大小 (onMeasure)

绘制:决定 View 怎么画 (onDraw)

布局:决定子 View 的位置(自定义 ViewGroup 才需要)

🎯 3. Measure 测量流程
✔ 测量的目标:

计算 View 的:

measuredWidth

measuredHeight

这两个值由 测量模式 (MeasureSpec) 决定:

Mode 含义
EXACTLY 精确大小(match_parent 或固定值)
AT_MOST 最大不能超过父容器(wrap_content)
UNSPECIFIED 不限制(滚动容器会用)
✔ 必须重写 onMeasure(wrap_content 的关键)
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val widthMode = MeasureSpec.getMode(widthMeasureSpec)
val widthSize = MeasureSpec.getSize(widthMeasureSpec)

val heightMode = MeasureSpec.getMode(heightMeasureSpec)
val heightSize = MeasureSpec.getSize(heightMeasureSpec)

val desiredWidth = 200
val desiredHeight = 200

val width = when(widthMode) {
MeasureSpec.EXACTLY -> widthSize
MeasureSpec.AT_MOST -> desiredWidth.coerceAtMost(widthSize)
else -> desiredWidth
}

val height = when(heightMode) {
MeasureSpec.EXACTLY -> heightSize
MeasureSpec.AT_MOST -> desiredHeight.coerceAtMost(heightSize)
else -> desiredHeight
}

setMeasuredDimension(width, height)
}


⚠ 如果你不重写 onMeasure,wrap_content 会失效!

🎯 4. Layout 布局流程(仅 ViewGroup 需要)

作用:

确定子 View 的位置(left、top、right、bottom)

流程:

layout()
└── onLayout()


例子(自定义简单线性布局):

override fun onLayout(p0: Boolean, l: Int, t: Int, r: Int, b: Int) {
var childTop = paddingTop

for (i in 0 until childCount) {
val child = getChildAt(i)
val childHeight = child.measuredHeight
child.layout(paddingLeft, childTop, r - paddingRight, childTop + childHeight)
childTop += childHeight
}
}

🎯 5. Draw 绘制流程(核心)

draw() 的内部流程如下:

draw()
├── drawBackground()
├── onDraw() ← 开发者核心绘制逻辑
├── dispatchDraw() ← 绘制子 View(ViewGroup)
└── onDrawForeground()


你的自定义内容都写在 onDraw():

override fun onDraw(canvas: Canvas) {
paint.color = Color.RED
canvas.drawCircle(width / 2f, height / 2f, 100f, paint)
}

🎨 6. Canvas & Paint 底层绘制原理
✔ Canvas 是 绘图指令的集合

本质是向 Surface 发送 GPU 绘制指令,例如:

drawLine

drawCircle

drawPath

clipRect

rotate

Canvas 内部使用 GPU(Skia 图形库)。

✔ Paint 是画笔

Paint 决定线条风格:

color

strokeWidth

style (FILL / STROKE)

shader(渐变、BitmapShader)

antiAlias

例如:

val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = Color.BLUE
strokeWidth = 4f
style = Paint.Style.STROKE
}

🧩 7. 自定义 View 完整模板(可直接复制)
class CircleView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {

private val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = Color.RED
}

override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val defaultSize = 300
val width = resolveSize(defaultSize, widthMeasureSpec)
val height = resolveSize(defaultSize, heightMeasureSpec)
setMeasuredDimension(width, height)
}

override fun onDraw(canvas: Canvas) {
val radius = min(width, height) / 2f
canvas.drawCircle(width / 2f, height / 2f, radius, paint)
}
}


这是一份标准自定义 View 模板。

⚙️ 8. 性能优化 & 常见问题
1. 避免在 onDraw 创建对象

❌ 不要 new Paint / Path
✔ 在构造函数创建

2. 使用 invalidate() vs postInvalidate()

invalidate():UI 线程

postInvalidate():非 UI 线程

3. 避免使用过多的 saveLayer()

它会创建离屏缓冲,非常耗性能。

4. onMeasure 尽量使用 resolveSize()
5. 大量动画建议用:ValueAnimator + invalidate()

不要直接在 onDraw 做运算。

📌 9. 总结

自定义 View 是 Android UI 开发的核心能力,理解其底层流程是高级开发者必备技能。

View 绘制三大流程:Measure、Layout、Draw

测量模式 MeasureSpec

Canvas / Paint 底层原理

自定义 View 模板

性能优化策略

掌握这些,就可以绘制任何 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
Android 启动优化:从冷启动到首帧渲染

原理 → 工具 → 优化策略 → 实战代码 全流程解析 Android 启动优化

⭐ 目录

什么是应用启动?

冷启动 / 温启动 / 热启动的区别

Android 启动流程原理(Zygote → AMS → ActivityThread)

启动性能分析工具(Systrace / Perfetto / Start-up Profiler)

启动优化策略(从布局到异步化)

启动优化实战代码

启动耗时监控:生产环境落地方案

1. 什么是应用启动?

Android 官方定义应用启动包含:

Process Start:系统为你的 APP 创建新进程

Application.attach / onCreate

ActivityThread 创建 Activity

setContentView

首帧渲染完成(First Draw)

👉 从点击图标到看到主界面的整个过程,就是启动过程。

2. 冷启动 / 温启动 / 热启动
冷启动(Cold Start)

APP 进程完全不存在,需要:

fork Zygote 生成进程

初始化 Application

创建 Activity

⏱耗时最长,也是我们最要优化的。

温启动(Warm Start)

进程还在,但 Activity 被销毁,需要:

重新创建 Activity

热启动(Hot Start)

Activity 被放到后台并未销毁,仅需恢复界面。

3. Android 启动流程原理

完整启动链路如下:

Launcher 点击图标

AMS(ActivityManagerService) → 启动进程

Zygote fork 出应用进程

ActivityThread.main()

Application.attach() / onCreate()

创建 Activity → onCreate()

setContentView() → LayoutInflate

首帧渲染 Choreographer#doFrame()


看起来很长,但真正拖慢启动速度的主要集中在:

✔ Application.onCreate
✔ Activity.onCreate
✔ 布局过度复杂(setContentView)
✔ 主线程阻塞耗时任务

4. 启动性能分析工具

Google 提供了非常强大的工具来监控启动过程。

① Logcat Start-up Profiler(最简单)

Android Studio → Logcat → 使用过滤器 ActivityTaskManager
可以直接看到:

Displayed com.xxx/.MainActivity +500ms

② Android Studio Profiler - Startup

从 Android Studio 4.2 后有独立的 Startup Profiler。

可以看到:

Application 初始化耗时

Activity 初始化耗时

布局耗时

CPU 占用

③ Perfetto / Systrace(最专业)

可以分析每一段调用链耗时。
复杂,但分析非常精准,用于深度优化。

5. 启动优化策略(最核心)

Android 启动优化的核心结论只有一句话:

把所有不影响首屏显示的任务,全部延后。

(1)Application.onCreate 要“瘦身”

不要做:

复杂 SDK 初始化

大量 I/O 操作

大量反射

应该做:

只做必要初始化

其余延迟到 IdleHandler 或后台线程

(2)避免主线程阻塞

典型坑点:

读写文件

查询数据库

网络请求

SharedPreferences.apply() 卡顿

(3)首屏布局优化

减少嵌套(LinearLayout + ConstraintLayout)

使用 ViewStub 延迟加载

避免在 XML 中使用过度复杂的 Constraint

(4)使用 SplashActivity 容灾

在 Android 12 后必须使用 SplashScreen API,但仍可以:

在 Splash 阶段做预加载

减少真正 Activity 的压力

6. 启动优化实战代码

下面给你能直接复制使用的 Application 延迟初始化模板:

class App : Application() {

override fun onCreate() {
super.onCreate()

// 必要初始化(主线程)
initLogger()
initCrashHandler()

// 非必要初始化延迟执行
delayInit()
}

private fun delayInit() {
// 方式1:IdleHandler(UI 空闲时)
Looper.myQueue().addIdleHandler {
initBigSdk()
false
}

// 方式2:后台线程
Executors.newSingleThreadExecutor().execute {
initDatabase()
initAds()
initAnalytics()
}
}
}

启动耗时监控(线上生产可用)
object AppStartTrace {

private var appStartTime = 0L

fun start() {
appStartTime = System.currentTimeMillis()
}

fun end(activity: Activity) {
val cost = System.currentTimeMillis() - appStartTime
Log.d("AppStartTrace", "冷启动耗时:${cost}ms")
// 可上报后台
}
}


使用方式:

class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
AppStartTrace.end(this)
}
}

7. 总结
从 原理、工具、优化策略、实战代码、线上监控 全面介绍了 Android 启动优化。


✔ 启动流程(Zygote → AMS → Activity)
✔ 如何判断启动慢在哪里
✔ 如何用工具分析
✔ 如何分阶段初始化
✔ 如何优化布局
✔ 如何写可落地的启动监控

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
📘 Android Binder 机制通俗原理全解析(流程图 + AIDL 示例)

Binder 是 Android 系统最核心的机制之一。所有系统服务(AMS、WMS、PackageManager、MediaService…)背后几乎都通过 Binder 完成通信,因此如果想真正理解 Android 系统原理,Binder 是绕不过去的基础。

本文通过通俗解释 + 配图 + 代码,快速理解 Binder 的本质与工作机制。

⭐ 1. 为什么 Android 一定要用 Binder?

一句话解释:

Android 是多进程系统,进程之间必须能通信,Binder 是它选中的 IPC 方案。

🔍 为什么要多进程?

每个 App 独立进程 → 防止互相崩溃

系统服务独立进程(system_server)

UI 服务 (SurfaceFlinger)

媒体服务(MediaService)

所以:App 想启动 Activity、申请权限、绘制 UI…全部要通过跨进程调用。

传统 IPC(Socket/管道/共享内存)都不够好,Binder 胜出。

⭐ 2. Binder 有多强?(对比传统 IPC)
IPC 方式 拷贝次数 性能 安全性 使用复杂度
Socket 2 中 低 高
管道 pipe 2 中 低 中
共享内存 1 高 低 高(需同步)
❗ Binder 1 高 高(自动携带UID/PID) 低(系统封装)

Binder 为什么快?

Binder 只进行一次“用户态 → 内核态”的数据拷贝
其他 IPC 都需要“两次拷贝”。

⭐ 3. Binder 机制大图(核心流程)

以下是最常见的 “App 启动 Activity” 的 Binder 调用流程:

+-----------------+ +----------------+
| Client | | Server |
| (App 进程) | | (AMS等服务) |
+--------+--------+ +--------+-------+
| ^
| Proxy 调用 |
v |
+--------+--------+ +--------+-------+
| Binder 驱动 | <----> | Stub(服务端)|
| (内核 Kernel) | | 业务逻辑 |
+-----------------+ +----------------+


再看更完整的带 ServiceManager 的图:

获取服务 注册服务
┌──────────────┐ ┌───────────────┐
│ Client │ --查询服务--> │ ServiceManager │
└──────────────┘ └───────────────┘
│ │
│ 调用服务 │
v v
┌──────────────┐ Binder ┌──────────────┐
│ ClientProxy │ <-------------> │ ServerStub │
└──────────────┘ └──────────────┘


✔ Proxy:客户端代理
✔ Stub:服务端接收者
✔ ServiceManager:系统服务目录
✔ Binder Driver:内核通信枢纽

⭐ 4. Binder 的四大组件结构
角色 作用
Binder 驱动(内核) 负责数据传输、线程管理、内存映射
ServiceManager 类似“系统服务注册中心”
Server(Stub) 服务端对象,处理真正的业务逻辑
Client(Proxy) 客户端代理,负责发起 Binder 调用

你写 AIDL 时生成的就是:

Stub.java(服务端)

Proxy.java(客户端)

⭐ 5. AIDL 示例:最小可运行 Binder Demo

下面以一个简单的 AIDL 文件演示:

📄 5.1 AIDL 文件(IRemoteService.aidl)
package com.demo.ipc;

// AIDL 接口
interface IRemoteService {
int add(int a, int b);
}


编译后自动生成:

IRemoteService.Stub(服务端)

IRemoteService.Stub.Proxy(客户端)

📄 5.2 服务端实现(Stub)
public class RemoteService extends Service {

private final IRemoteService.Stub binder = new IRemoteService.Stub() {
@Override
public int add(int a, int b) {
return a + b;
}
};

@Override
public IBinder onBind(Intent intent) {
return binder;
}
}

📄 5.3 客户端调用(Proxy)
private IRemoteService remoteService;

private ServiceConnection connection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
remoteService = IRemoteService.Stub.asInterface(service);
try {
int result = remoteService.add(3, 5);
Log.d("IPC", "结果:" + result);
} catch (RemoteException e) {
e.printStackTrace();
}
}

@Override
public void onServiceDisconnected(ComponentName name) {
remoteService = null;
}
};


绑定:

Intent intent = new Intent();
intent.setClassName("com.demo.server",
"com.demo.server.RemoteService");
bindService(intent, connection, BIND_AUTO_CREATE);


✔ 完整的 Binder Demo 至此全部跑通。

⭐ 6. Binder 为什么安全?

每次 Binder 调用都自带调用者 UID/PID(由内核提供),因此 AMS/WMS 能做:

权限校验

角色校验

进程身份确认

不可伪造 UID → 无法伪造系统身份

⭐ 7. Binder 为什么快?

关键原因:

一次拷贝 + 内核共享内存

驱动调度(队列 + 线程池)

零上下文切换同步(不用共享锁)

这是“低耗、高吞吐”的本质。

⭐ 8. 总结

Binder = 高性能 IPC + 安全校验 + RPC 封装 + 系统服务基础设施
Android 系统的运行效率、安全性和架构清晰度,几乎都依赖 Binder。

如果你想真正理解 Android:

✔ AMS 怎么启动 Activity
✔ WMS 怎么管理窗口
✔ MediaServer 为什么这么快
✔ PackageManagerService 如何校验 APK

全部要从 Binder 入门。

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
1. Android 5.0 Lollipop (API 21 / 2014)
主要更新

引入 Material Design 视觉风格

ART 替代 Dalvik 作为默认运行时,提升性能

多用户支持,尤其平板

通知栏可展开,带操作按钮

适配点

新 UI 样式需适配 Material Design

RecyclerView 替代 ListView 提升性能

ART 可能导致一些老旧 Dalvik 字节码异常

2. Android 6.0 Marshmallow (API 23 / 2015)
主要更新

运行时权限机制

Doze / App Standby 节电模式

指纹识别 API

适配点

所有敏感权限需在运行时请求

后台服务 / 定时任务需适配 Doze 节电策略

如果用指纹解锁,需要调用 FingerprintManager / BiometricPrompt

3. Android 7.0 Nougat (API 24 / 2016)
主要更新

多窗口 / 分屏模式

FileProvider 替代直接文件 URI

通知增强:直接回复消息

适配点

UI 需考虑多窗口布局

文件共享需使用 FileProvider,避免 FileUriExposedException

通知交互要支持远程输入

4. Android 8.0 Oreo (API 26 / 2017)
主要更新

后台限制:后台服务受限

通知渠道(Notification Channel)

Adaptive Icon(自适应图标)

Picture-in-Picture 支持

适配点

后台服务需改为 前台服务 + 通知

所有通知需设置 渠道 ID,否则不显示

App 图标需提供适配 圆形 / 方形 的资源

5. Android 9.0 Pie (API 28 / 2018)
主要更新

安全与隐私增强:限制后台访问摄像头、麦克风

App Actions / Slices

Gesture Navigation

适配点

后台访问摄像头或麦克风会报错

手势导航影响全屏布局和 UI 交互

网络安全配置(Network Security Config)可限制明文 HTTP

6. Android 10 (API 29 / 2019)
主要更新

Scoped Storage,限制外部存储访问

暗黑模式

位置权限细化:前台/后台分开

适配点

文件读写要适配 MediaStore / SAF / App-specific storage

UI 适配暗黑模式

权限请求要区分 前台/后台定位

7. Android 11 (API 30 / 2020)
主要更新

一次性权限(临时访问摄像头/位置/麦克风)

包可见性限制:限制查询其他应用

前台服务限制更严格

适配点

权限申请增加“一次性”选项

访问其他应用需在 AndroidManifest.xml 中声明 <queries>

前台服务启动需考虑 延迟 / 弹窗 提示

8. Android 12 (API 31 / 2021)
主要更新

隐私仪表盘 / 麦克风、摄像头指示器

Splash Screen 官方支持

通知动画 / 圆角按钮

适配点

摄像头、麦克风访问时必须用户感知

SplashScreen API 替代自定义启动页

Notification UI 需适配 Material You 动画

9. Android 13 (API 33 / 2022)
主要更新

蓝牙权限细化:BLUETOOTH_CONNECT / BLUETOOTH_SCAN

通知权限必须显式请求

多语言 / 可变主题支持

适配点

蓝牙扫描、连接需申请新权限

发送通知需请求 POST_NOTIFICATIONS 权限

App 需要支持动态语言切换

10. Android 14 (API 34 / 2023)
主要更新

蓝牙 MTU 协商行为更严格

应用后台启动限制更严

隐私限制增强

适配点

BLE MTU 超过外围设备能力会导致数据收发异常,需要控制 MTU 大小或拆分包

后台服务、JobScheduler、WorkManager 适配新的限制

访问敏感信息、剪贴板、文件等操作需注意权限

11.Android 15 (API 级别 35)
✅ 重要新特性

引入或增强隐私、健康、安全相关功能,如 Health Connect 新数据类型支持。

强化大屏/折叠屏/平板设备支持,多设备体验增强。

新的性能/热管理 API:如热头空间 (thermal headroom) 预测、GPU/CPU hint 会话。

媒体与摄像头能力升级:低光增强、闪光强度精调、虚拟 MIDI 2.0 支持。

⚠ 适配要点

如果你的 App 涉及健康数据、营养、传感器等,检查是否使用了 Health Connect 或新 API。

对于大屏/折叠屏设备,UI 要支持可变化布局、分屏、多窗口。

性能密集型应用(游戏、图像处理)应考虑新的热管理 API,检查是否引入性能 hint。

媒体/摄像头应用要测试低光模式、闪光强度是否受影响。

12. Android 16 (API 级别 36)
✅ 重要新特性

引入或重构运行时 (ART) 性能和新 Java 特性支持。

支持更广泛的设备类型和用途,比如外接显示器、桌面模式增强。

强化隐私沙箱 (Privacy Sandbox)、健康记录 (FHIR 格式) 等新 API。

新 UI 设计趋势:如 Material 3 Expressive、强制自动主题图标、暗模式/图标适配(虽部分为后续 QPR 更新)


⚠ 适配要点

若 App 用到了蓝牙/媒体/外接显示器,则需测试在大屏幕、外接显示器环境下的交互、窗口适配。

若使用 Java / Kotlin 新特性或依赖 ART 行为,应测试在 Android 16 上的兼容性。

UI 元素(图标、主题、暗模式)应准备好适配系统自动图标主题、暗模式强制应用的场景。

健康、隐私相关功能(如医疗记录、用户敏感数据)需确认权限和用户同意流程是否变更。

💡 总结适配建议

权限变化是最大坑:运行时权限 / 后台权限 / 特定功能权限

后台限制:Oreo 后后台服务受限,必须用前台服务或 WorkManager

存储变化:Scoped Storage 后要改 MediaStore / SAF

蓝牙/网络:Android 12+ 权限更严格,Android 14+ BLE MTU 注意

UI:Material Design / 多窗口 / 手势导航 / 暗黑模式 / SplashScreen

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



//百度->gps
public static LatLng bdToWgs(LatLng bdLatLng) {
if (bdLatLng == null) return null;

// 百度坐标转高德坐标
double x = bdLatLng.longitude - 0.0065;
double y = bdLatLng.latitude - 0.006;
double z = Math.sqrt(x * x + y * y) - 0.00002 * Math.sin(y * Math.PI);
double theta = Math.atan2(y, x) - 0.000003 * Math.cos(x * Math.PI);
double gcjLon = z * Math.cos(theta);
double gcjLat = z * Math.sin(theta);

// 高德坐标转 GPS
return gcjToWgs(new LatLng(gcjLat, gcjLon));
}

// GCJ-02 → WGS84
public static LatLng gcjToWgs(LatLng gcjLatLng) {
if (gcjLatLng == null) return null;
double dLat = transformLat(gcjLatLng.longitude - 105.0, gcjLatLng.latitude - 35.0);
double dLon = transformLon(gcjLatLng.longitude - 105.0, gcjLatLng.latitude - 35.0);
double radLat = gcjLatLng.latitude / 180.0 * Math.PI;
double magic = Math.sin(radLat);
magic = 1 - 0.00669342162296594323 * magic * magic;
double sqrtMagic = Math.sqrt(magic);
dLat = (dLat * 180.0) / ((6378245.0 * (1 - 0.00669342162296594323)) / (magic * sqrtMagic) * Math.PI);
dLon = (dLon * 180.0) / (6378245.0 / sqrtMagic * Math.cos(radLat) * Math.PI);
double mgLat = gcjLatLng.latitude + dLat;
double mgLon = gcjLatLng.longitude + dLon;
return new LatLng(gcjLatLng.latitude * 2 - mgLat, gcjLatLng.longitude * 2 - mgLon);
}

private static double transformLat(double x, double y) {
double ret = -100.0 + 2.0 * x + 3.0 * y + 0.2 * y * y + 0.1 * x * y + 0.2 * Math.sqrt(Math.abs(x));
ret += (20.0 * Math.sin(6.0 * x * Math.PI) + 20.0 * Math.sin(2.0 * x * Math.PI)) * 2.0 / 3.0;
ret += (20.0 * Math.sin(y * Math.PI) + 40.0 * Math.sin(y / 3.0 * Math.PI)) * 2.0 / 3.0;
ret += (160.0 * Math.sin(y / 12.0 * Math.PI) + 320 * Math.sin(y * Math.PI / 30.0)) * 2.0 / 3.0;
return ret;
}

private static double transformLon(double x, double y) {
double ret = 300.0 + x + 2.0 * y + 0.1 * x * x + 0.1 * x * y + 0.1 * Math.sqrt(Math.abs(x));
ret += (20.0 * Math.sin(6.0 * x * Math.PI) + 20.0 * Math.sin(2.0 * x * Math.PI)) * 2.0 / 3.0;
ret += (20.0 * Math.sin(x * Math.PI) + 40.0 * Math.sin(x / 3.0 * Math.PI)) * 2.0 / 3.0;
ret += (150.0 * Math.sin(x / 12.0 * Math.PI) + 300.0 * Math.sin(x / 30.0 * Math.PI)) * 2.0 / 3.0;
return ret;
}


//GPS-百度

public static LatLng wgsToBd(LatLng sourceLatLng) {
if (sourceLatLng == null) {
return null;
}

CoordinateConverter converter = new CoordinateConverter();
converter.from(CoordinateConverter.CoordType.GPS);
converter.coord(sourceLatLng);

return converter.convert();
}




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
从 Linux 内核到应用层

引言

Android 不仅仅是一个应用开发平台,它是一个完整的操作系统,基于 Linux 内核,拥有丰富的系统服务和运行环境。理解 Android 系统架构对开发高性能应用、优化系统资源以及调试底层问题非常重要。

1. Android 系统架构概览

Android 系统可以分为 五层结构:

应用层(Applications)
应用框架层(Application Framework)
系统运行库(Libraries & Android Runtime)
硬件抽象层(HAL)
Linux 内核(Linux Kernel)

2. Linux 内核(Linux Kernel)

作用:

驱动管理:管理硬件设备,如摄像头、显示屏、传感器

进程管理:提供多任务调度

内存管理:虚拟内存与物理内存管理

安全与网络:权限控制、网络通信

特点:

Android 使用的通常是 定制化 Linux 内核

提供 Binder IPC 机制,支撑应用层与系统服务通信

3. 硬件抽象层(HAL)

作用:

将硬件接口统一抽象

各种硬件模块通过 HAL 与 Android 框架交互,例如摄像头 HAL、音频 HAL

特点:

每个硬件模块都有对应的 HAL 模块(Camera HAL、GPS HAL 等)

开发者无需直接访问底层驱动,系统调用 HAL 接口即可

4. 系统运行库(Libraries & Android Runtime)

组成:

C/C++ 系统库

如 libc、SurfaceFlinger、OpenGL ES 等

Android Runtime(ART)

Java/Kotlin 应用运行环境

负责将 APK 中的字节码转换为机器码(JIT/AOT 编译)

提供垃圾回收机制和多线程管理

特点:

ART 替代了早期的 Dalvik 虚拟机

运行效率更高,内存管理更智能

5. 应用框架层(Application Framework)

作用:

为应用提供丰富的 API 和系统服务

核心组件:

Activity Manager:管理应用生命周期

Package Manager:应用安装和权限管理

Window Manager:界面窗口管理

Content Providers:应用数据共享

Location Manager:位置服务

Notification Manager:通知服务

特点:

应用通过调用框架 API 获取系统服务

框架层负责调度底层 HAL 和内核资源

6. 应用层(Applications)

作用:

直接运行用户应用,包括系统应用和第三方应用

应用通过 Java/Kotlin + Android SDK 调用框架层功能

特点:

每个应用运行在独立的 进程和虚拟机实例 中

利用 Linux 内核的权限机制进行隔离和安全管理

7. 总结

Android 系统是一套复杂的多层架构,从内核到应用层形成完整生态

开发者理解系统架构可以帮助:

性能优化

系统服务调用理解

调试底层问题

核心概念:内核提供基础,HAL封装硬件,运行库执行应用,框架层提供服务,应用层直接面向用户


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
一、前言

在物联网设备开发中,蓝牙通信(Bluetooth Low Energy, BLE) 是最常见的无线数据传输方式之一。
很多 Android 项目(如健康手环、传感模块等)都需要实现 BLE 的扫描、连接、收发数据、断线重连等功能。

本文将分享一个我在项目中实际使用的 BLE 工具类 —— BluetoothBleUtil,
它将 Android 原生的蓝牙 API 封装成简单易用的接口,让开发者可以几行代码就完成 BLE 通信。

二、功能概述

本工具类实现了以下 BLE 功能模块:

功能 说明
蓝牙初始化 初始化 BluetoothAdapter、BluetoothManager
扫描设备 支持超时自动停止扫描
自动连接目标设备 根据设备名过滤并自动连接
读写 GATT 特征 支持发送指令与接收通知
断开与释放 断线自动清理,防止内存泄漏
重连功能 自动重连上次已连接设备
状态监听 支持 LiveData 与回调接口
三、核心架构设计

BluetoothBleUtil 使用 Kotlin 单例模式 (object) 设计,保证 BLE 连接全局唯一。
核心流程如下:

startScan() → onScanResult() → connect() → onServicesDiscovered()
↓ ↓
stopScan() enableNotify()
↓ ↓
sendBytes() ←→ onCharacteristicChanged()

四、代码实现详解

import android.annotation.SuppressLint
import android.bluetooth.*
import android.bluetooth.le.*
import android.content.Context
import android.os.Build
import android.os.Handler
import android.os.Looper
import android.util.Log
import androidx.lifecycle.MutableLiveData
import com.comm.library.utils.LogUtils
import com.comm.library.utils.SPUtils
import com.htnova.gasdetection.model.SpConstance
import java.util.*

/**
* 低功耗蓝牙
*/
@SuppressLint("MissingPermission")
object BluetoothBleUtil {

private const val TAG = "gasdetection"

// ------------------------- BLE参数配置 -------------------------
private const val DEVICE_NAME = "GCPID"
private val SERVICE_UUID = UUID.fromString("0000a002-0000-1000-8000-00805f9b34fb")
private val WRITE_UUID = UUID.fromString("0000c303-0000-1000-8000-00805f9b34fb")
private val NOTIFY_UUID = UUID.fromString("0000c305-0000-1000-8000-00805f9b34fb")

// ------------------------- 回调接口 -------------------------
interface Callback {
fun onScanStarted() {}
fun onDeviceFound(device: BluetoothDevice) {}
fun onScanFinished() {}
fun onConnected(device: BluetoothDevice) {}
fun onDisconnected(device: BluetoothDevice) {}
fun onMessageReceived(device: BluetoothDevice, data: ByteArray) {}
fun onError(msg: String) {}
}

// ------------------------- 内部状态 -------------------------
private var callback: Callback? = null
private lateinit var appContext: Context
private var adapter: BluetoothAdapter? = null
private var scanner: BluetoothLeScanner? = null
private var bluetoothGatt: BluetoothGatt? = null
private var writeCharacteristic: BluetoothGattCharacteristic? = null
private var notifyCharacteristic: BluetoothGattCharacteristic? = null
private val handler = Handler(Looper.getMainLooper())

private var isScanning = false
private var isConnected = false

val connectedDeviceLiveData = MutableLiveData<BluetoothDevice?>()

// ------------------------- 初始化 -------------------------
fun init(context: Context, cb: Callback) {
appContext = context.applicationContext
callback = cb
adapter = (context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager).adapter
scanner = adapter?.bluetoothLeScanner
}

// ------------------------- 扫描 -------------------------
fun startScan(timeoutMs: Long = 8000) {
if (isScanning) return
if (adapter == null || !adapter!!.isEnabled) {
callback?.onError("蓝牙未开启")
return
}

// val filters = listOf(ScanFilter.Builder().setDeviceName(DEVICE_NAME).build())
val settings = ScanSettings.Builder().setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY).build()

isScanning = true
callback?.onScanStarted()
scanner?.startScan(null, settings, scanCallback)

handler.postDelayed({
stopScan()
}, timeoutMs)
}

fun stopScan() {
if (!isScanning) return
isScanning = false
try { scanner?.stopScan(scanCallback) } catch (_: Exception) {}
callback?.onScanFinished()
}

private val scanCallback = object : ScanCallback() {
override fun onScanResult(callbackType: Int, result: ScanResult) {
val device = result.device
callback?.onDeviceFound(device)
if (device.name == DEVICE_NAME) {
callback?.onDeviceFound(device)
stopScan()
connect(device)
}
}

override fun onScanFailed(errorCode: Int) {
callback?.onError("扫描失败: $errorCode")
isScanning = false
}
}

// ------------------------- 连接 -------------------------
fun connect(device: BluetoothDevice) {
Log.d(TAG, "正在连接: ${device.name} ${device.address}")
SPUtils.putString(SpConstance.BLUE_ADDRESS,device.address)
bluetoothGatt = device.connectGatt(appContext, false, gattCallback)
}

fun disconnect() {
isConnected = false
try { bluetoothGatt?.disconnect() } catch (_: Exception) {}
try { bluetoothGatt?.close() } catch (_: Exception) {}
bluetoothGatt = null
connectedDeviceLiveData.postValue(null)
}

// ------------------------- GATT回调 -------------------------
private val gattCallback = object : BluetoothGattCallback() {

override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) {
when (newState) {
BluetoothProfile.STATE_CONNECTED -> {
isConnected = true
connectedDeviceLiveData.postValue(gatt.device)
callback?.onConnected(gatt.device)
Log.d(TAG, "已连接,开始发现服务")
gatt.discoverServices()
}
BluetoothProfile.STATE_DISCONNECTED -> {
isConnected = false
connectedDeviceLiveData.postValue(null)
callback?.onDisconnected(gatt.device)
Log.d(TAG, "已断开: ${gatt.device.name}")
// try { bluetoothGatt?.close() } catch (_: Exception) {}
// bluetoothGatt = null
// connectedDeviceLiveData.postValue(null)
}
}
}

override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) {
if (status == BluetoothGatt.GATT_SUCCESS) {
val service = gatt.getService(SERVICE_UUID)
if (service != null) {
writeCharacteristic = service.getCharacteristic(WRITE_UUID)
notifyCharacteristic = service.getCharacteristic(NOTIFY_UUID)

// 启用通知
//注意:BLE 特征通知的描述符(Descriptor)一般是 Client Characteristic Configuration Descriptor (CCCD),UUID 固定为:
//00002902-0000-1000-8000-00805f9b34fb
//这是 BLE 规范规定的,用来启用特征通知或指示。
if (notifyCharacteristic != null) {
gatt.setCharacteristicNotification(notifyCharacteristic, true)
val descriptor = notifyCharacteristic!!.getDescriptor(
UUID.fromString("00002902-0000-1000-8000-00805f9b34fb")
)
descriptor?.let {
it.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
gatt.writeDescriptor(it)
}
}
} else {
callback?.onError("未发现指定服务UUID")
}
}
}

override fun onCharacteristicChanged(
gatt: BluetoothGatt,
characteristic: BluetoothGattCharacteristic
) {
if (characteristic.uuid == NOTIFY_UUID) {
val data = characteristic.value
callback?.onMessageReceived(gatt.device, data)
}
}

override fun onCharacteristicWrite(
gatt: BluetoothGatt,
characteristic: BluetoothGattCharacteristic,
status: Int
) {
if (status != BluetoothGatt.GATT_SUCCESS) {
callback?.onError("写入失败: $status")
}
}
}

// ------------------------- 写数据 -------------------------
fun sendBytes(data: ByteArray) {
if (!isConnected || writeCharacteristic == null || bluetoothGatt == null) {
callback?.onError("未连接设备")
return
}
writeCharacteristic!!.value = data
val result = bluetoothGatt!!.writeCharacteristic(writeCharacteristic)
if (!result) {
callback?.onError("发送失败: writeCharacteristic返回false")
}
}

fun sendMessage(msg: String) = sendBytes(msg.toByteArray())

// ------------------------- 工具 -------------------------
fun isBluetoothEnabled(): Boolean = adapter?.isEnabled == true
fun enableBluetooth() = adapter?.enable()
fun disableBluetooth() = adapter?.disable()

fun release() {
stopScan()
disconnect()
callback = null
adapter = null
scanner = null
}

fun getConnectedBleDevices(): List<BluetoothDevice> {
val manager = appContext.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
return manager.getConnectedDevices(BluetoothProfile.GATT)
}

//连接上次连接的
fun tryReconnectLastDevice() {
val address = SPUtils.getString(SpConstance.BLUE_ADDRESS,"") ?: return
val device = adapter?.getRemoteDevice(address)
if (device != null) {
Log.d(TAG, "尝试重连上次设备: ${device.address}")
// connect(device)
bluetoothGatt?.connect()
}
}
}

五、权限配置

Android 12+ 必须在 AndroidManifest.xml 中声明以下权限:

<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />


同时,在运行时动态申请:

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
requestPermissions(arrayOf(
Manifest.permission.BLUETOOTH_CONNECT,
Manifest.permission.BLUETOOTH_SCAN
), 100)
}

🚀 Android 性能优化全攻略:从卡顿到丝滑的系统性思考

一、前言

性能优化,是一个 Android 开发者从“能写”到“写好”的分水岭。
在业务迭代和功能膨胀的背景下,App 卡顿、耗电、内存暴涨、启动慢等问题屡见不鲜。
本文结合实际项目经验,总结 Android 性能优化的核心思路与落地方案,帮助开发者构建一套系统化的优化思维。


二、性能优化的核心目标

优化的最终目的,不是“让分数变高”,而是提升用户体验。

在有限资源下,让应用运行得更快、更稳、更省。

主要指标包括:

  • 启动速度(冷启动、热启动)
  • 页面流畅度(帧率 / 掉帧率 / 渲染时间)
  • 内存占用(内存泄漏、对象复用、Bitmap管理)
  • CPU & 电量(过度计算、线程滥用)
  • 网络效率(请求合并、缓存策略、压缩传输)

三、启动优化:用户等待的第一印象

1️⃣ 延迟非关键初始化

很多 App 启动慢,根本原因在于 Application#onCreate 里做了太多事。

优化思路:

  • 把非关键初始化延迟到首屏渲染后(如延迟 2s 执行)
  • 例如:日志系统、推送、统计、广告 SDK 都可懒加载
new Handler(Looper.getMainLooper()).postDelayed(() -> {
    initPush();
    initAnalytics();
}, 2000);

2️⃣ 启动页轻量化

SplashActivity 只负责检查必要权限与路由逻辑,禁止做业务初始化

图片资源压缩至 WebP

避免复杂动画

尽量复用已加载资源

四、渲染优化:从掉帧到丝滑
1️⃣ 分析工具

使用 adb shell dumpsys gfxinfo 查看帧渲染耗时
每个帧耗时 <16ms 才能保持 60fps。

2️⃣ 优化建议

减少主线程阻塞(IO、网络、复杂运算应放后台)

使用 ViewStub、include、merge 优化布局层级

大量 RecyclerView Item 使用 DiffUtil 替代 notifyDataSetChanged()

动画尽量用 Lottie 或硬件加速的 PropertyAnimator

五、内存优化:让 App 更持久
1️⃣ 常见问题

静态持有 Context

Bitmap 未回收

Handler / Thread 未移除消息

监听器泄漏(RxJava / LiveData)

2️⃣ 调优工具

Android Profiler

LeakCanary(自动检测内存泄漏)

3️⃣ 示例
@Override
protected void onDestroy() {
    super.onDestroy();
    handler.removeCallbacksAndMessages(null);
    binding = null;
}

六、线程与异步优化

原则:CPU 要忙得刚刚好

避免线程池滥用,每个模块不要新建线程池

建议统一封装 ThreadPoolExecutor:

ExecutorService executor = new ThreadPoolExecutor(
        2, 4, 60, TimeUnit.SECONDS,
        new LinkedBlockingQueue<>(),
        new ThreadFactory() {
            @Override
            public Thread newThread(Runnable r) {
                Thread t = new Thread(r);
                t.setName("AppThreadPool-" + t.getId());
                return t;
            }
        });

七、网络优化

开启 HTTP 压缩(GZIP)

使用 OKHttp 缓存机制

合并请求、批量上传

图片加载用 Glide / Coil 并开启缓存策略:

Glide.with(context)
    .load(url)
    .diskCacheStrategy(DiskCacheStrategy.ALL)
    .into(imageView);

八、监控与持续优化

优化不是一次性行为,而是持续工程。

建议在项目中引入以下监控指标:

启动耗时监控

卡顿帧率监控(BlockCanary)

内存泄漏监控(LeakCanary)

崩溃收集(Firebase / Bugly)

持续监控 + 自动上报 + 定期分析,
才能让优化成为开发流程的一部分。