Skip to content

Commit 251dc77

Browse files
authored
更新 ExposureLogManager.kt
1 parent e4655fa commit 251dc77

1 file changed

Lines changed: 81 additions & 58 deletions

File tree

Lines changed: 81 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,47 @@
11
package com.netmonitor.app.util
22

33
import android.content.Context
4+
import com.netmonitor.app.Constants
45
import com.netmonitor.app.model.ConnectionInfo
56
import com.netmonitor.app.model.ExposureRecord
7+
import kotlinx.coroutines.*
68
import org.json.JSONArray
79
import java.io.File
810
import java.text.SimpleDateFormat
911
import java.util.Date
1012
import java.util.Locale
13+
import java.util.concurrent.ConcurrentHashMap
1114
import java.util.concurrent.CopyOnWriteArrayList
1215

1316
/**
1417
* 暴露记录管理器
15-
* - JSON 文件持久化存储到 App 内部存储
16-
* - 会话级去重,同一连接不会重复记录
17-
* - 最多保留 MAX_RECORDS 条记录
18+
*
19+
* 改进点:
20+
* - seenKeys 改用 ConcurrentHashMap.newKeySet(),解决多线程竞争
21+
* - save() 改为延迟批量写入(防抖),避免每条记录都触发全量磁盘 I/O
22+
* - 常量统一引用 Constants
1823
*/
1924
object ExposureLogManager {
2025

21-
private const val FILE_NAME = "exposure_log.json"
22-
private const val MAX_RECORDS = 500
26+
private const val TAG = "ExposureLog"
2327

2428
private val records = CopyOnWriteArrayList<ExposureRecord>()
25-
private val seenKeys = mutableSetOf<String>()
29+
30+
/** 线程安全的去重集合 —— 修复原来的 mutableSetOf 并发问题 */
31+
private val seenKeys: MutableSet<String> = ConcurrentHashMap.newKeySet()
32+
2633
private var initialized = false
2734

28-
/**
29-
* 初始化:从文件加载历史记录
30-
*/
35+
// ── 延迟写入防抖 ──
36+
private var saveJob: Job? = null
37+
private val saveScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
38+
39+
// ── 初始化 ──
40+
3141
@Synchronized
3242
fun init(context: Context) {
3343
if (initialized) return
34-
val file = File(context.filesDir, FILE_NAME)
44+
val file = File(context.filesDir, Constants.EXPOSURE_LOG_FILE)
3545
if (file.exists()) {
3646
try {
3747
val jsonArray = JSONArray(file.readText())
@@ -41,29 +51,29 @@ object ExposureLogManager {
4151
records.add(record)
4252
}
4353
}
44-
AppLogger.i("ExposureLog", "\u52a0\u8f7d\u4e86 ${records.size} \u6761\u66b4\u9732\u8bb0\u5f55")
54+
AppLogger.i(TAG, "加载了 ${records.size} 条暴露记录")
4555
} catch (e: Exception) {
46-
AppLogger.e("ExposureLog", "\u52a0\u8f7d\u66b4\u9732\u8bb0\u5f55\u5931\u8d25: ${e.message}")
56+
AppLogger.e(TAG, "加载暴露记录失败: ${e.message}")
4757
}
4858
}
4959
initialized = true
5060
}
5161

52-
/**
53-
* 重置会话去重(监控启动时调用)
54-
*/
62+
// ── 会话管理 ──
63+
5564
fun resetSession() {
5665
seenKeys.clear()
57-
AppLogger.i("ExposureLog", "\u4f1a\u8bdd\u53bb\u91cd\u5df2\u91cd\u7f6e")
66+
AppLogger.i(TAG, "会话去重已重置")
5867
}
5968

69+
// ── 扫描与记录 ──
70+
6071
/**
61-
* 扫描连接列表,自动记录新的暴露
72+
* 扫描连接列表,自动记录新暴露
6273
* @return 本次新增的记录数
6374
*/
6475
fun scanAndLog(context: Context, connections: List<ConnectionInfo>): Int {
6576
if (!initialized) init(context)
66-
6777
var newCount = 0
6878

6979
for (conn in connections) {
@@ -82,73 +92,96 @@ object ExposureLogManager {
8292
}
8393
}
8494
}
85-
8695
return newCount
8796
}
8897

8998
private fun addRecord(context: Context, record: ExposureRecord) {
9099
records.add(0, record)
91100

92101
// 超过上限,删除最旧的
93-
while (records.size > MAX_RECORDS) {
102+
while (records.size > Constants.MAX_EXPOSURE_RECORDS) {
94103
records.removeAt(records.lastIndex)
95104
}
96105

97-
save(context)
98-
AppLogger.i("ExposureLog",
99-
"\u8bb0\u5f55\u66b4\u9732: ${record.displayType} | ${record.appName} | ${record.localIp}:${record.localPort}")
106+
// 延迟批量写入(防抖),避免每条记录都触发全量 I/O
107+
scheduleSave(context)
108+
109+
AppLogger.i(TAG, "记录暴露: ${record.displayType} | ${record.appName} | " +
110+
"${record.localIp}:${record.localPort}")
100111
}
101112

102113
/**
103-
* 获取所有记录
114+
* 防抖写入:在最后一次 addRecord 后延迟 EXPOSURE_DEBOUNCE_SAVE_MS 再执行写入,
115+
* 连续添加多条记录只会触发一次磁盘写入
104116
*/
117+
private fun scheduleSave(context: Context) {
118+
saveJob?.cancel()
119+
saveJob = saveScope.launch {
120+
delay(Constants.EXPOSURE_DEBOUNCE_SAVE_MS)
121+
saveNow(context)
122+
}
123+
}
124+
125+
/** 立即写入(用于 clear / App 退出等场景) */
126+
fun forceSave(context: Context) {
127+
saveJob?.cancel()
128+
saveNow(context)
129+
}
130+
131+
private fun saveNow(context: Context) {
132+
try {
133+
val jsonArray = JSONArray()
134+
for (record in records) {
135+
jsonArray.put(record.toJson())
136+
}
137+
val file = File(context.filesDir, Constants.EXPOSURE_LOG_FILE)
138+
file.writeText(jsonArray.toString())
139+
} catch (e: Exception) {
140+
AppLogger.e(TAG, "保存暴露记录失败: ${e.message}")
141+
}
142+
}
143+
144+
// ── 查询 ──
145+
105146
fun getRecords(): List<ExposureRecord> = records.toList()
106147

107-
/**
108-
* 按类型筛选记录
109-
*/
110148
fun getRecordsByType(type: String): List<ExposureRecord> {
111149
return records.filter { it.type == type }
112150
}
113151

114-
/**
115-
* 获取记录数量
116-
*/
117152
fun getCount(): Int = records.size
118153

119-
/**
120-
* 清空所有记录
121-
*/
154+
// ── 清空 ──
155+
122156
fun clear(context: Context) {
123157
records.clear()
124158
seenKeys.clear()
125-
save(context)
126-
AppLogger.i("ExposureLog", "\u66b4\u9732\u8bb0\u5f55\u5df2\u6e05\u7a7a")
159+
forceSave(context)
160+
AppLogger.i(TAG, "暴露记录已清空")
127161
}
128162

129-
/**
130-
* 导出为文本
131-
*/
163+
// ── 导出 ──
164+
132165
fun exportText(): String {
133-
if (records.isEmpty()) return "\u65e0\u66b4\u9732\u8bb0\u5f55"
166+
if (records.isEmpty()) return "无暴露记录"
134167

135168
val sb = StringBuilder()
136-
sb.appendLine("===== NetMonitor \u66b4\u9732\u8bb0\u5f55 =====")
137-
sb.appendLine("\u5bfc\u51fa\u65f6\u95f4: " + SimpleDateFormat(
169+
sb.appendLine("===== NetMonitor 暴露记录 =====")
170+
sb.appendLine("导出时间: " + SimpleDateFormat(
138171
"yyyy-MM-dd HH:mm:ss", Locale.getDefault()
139172
).format(Date()))
140-
sb.appendLine("\u603b\u8bb0\u5f55\u6570: ${records.size}")
173+
sb.appendLine("总记录数: ${records.size}")
141174
sb.appendLine()
142175

143176
val ipLeaks = records.filter { it.type == "ip_leak" }
144177
val portExposed = records.filter { it.type == "port_exposed" }
145178

146-
sb.appendLine("--- \u771f\u5b9eIP\u66b4\u9732: ${ipLeaks.size} \u6761 ---")
179+
sb.appendLine("--- 真实IP暴露: ${ipLeaks.size} ---")
147180
for (r in ipLeaks) {
148181
sb.appendLine(r.format())
149182
}
150183

151-
sb.appendLine("--- \u7aef\u53e3\u66b4\u9732: ${portExposed.size} \u6761 ---")
184+
sb.appendLine("--- 端口暴露: ${portExposed.size} ---")
152185
for (r in portExposed) {
153186
sb.appendLine(r.format())
154187
}
@@ -157,19 +190,9 @@ object ExposureLogManager {
157190
return sb.toString()
158191
}
159192

160-
/**
161-
* 保存到文件
162-
*/
163-
private fun save(context: Context) {
164-
try {
165-
val jsonArray = JSONArray()
166-
for (record in records) {
167-
jsonArray.put(record.toJson())
168-
}
169-
val file = File(context.filesDir, FILE_NAME)
170-
file.writeText(jsonArray.toString())
171-
} catch (e: Exception) {
172-
AppLogger.e("ExposureLog", "\u4fdd\u5b58\u66b4\u9732\u8bb0\u5f55\u5931\u8d25: ${e.message}")
173-
}
193+
/** 释放资源(App 退出时调用) */
194+
fun destroy() {
195+
saveJob?.cancel()
196+
saveScope.cancel()
174197
}
175198
}

0 commit comments

Comments
 (0)