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
张量可以理解为 多维数组:

0 维张量 → 一个标量(单个数字,例如 5)

1 维张量 → 一个向量(数组,例如 [1, 2, 3])

2 维张量 → 一个矩阵(表格,例如 [[1,2],[3,4]])

3 维张量 → 例如一张彩色图片 [高度, 宽度, 通道数]

4 维张量 → 一批彩色图片 [批大小, 高度, 宽度, 通道数]

张量的 形状 就是它的 维度大小,告诉我们张量里有多少数据。

例子:

import torch

x = torch.randn(1, 3, 224, 224) # 随机生成一个张量
print(x.shape)


输出:

torch.Size([1, 3, 224, 224])


解释:

1 → 批量大小(batch size),表示有 1 张图片

3 → 通道数(RGB 三个通道)

224, 224 → 图片的高度和宽度

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
1. Handler + Runnable(最常用,简单)
private Handler handler = new Handler(Looper.getMainLooper());
private Runnable batteryRunnable = new Runnable() {
@Override
public void run() {
updateBattery(context); // 查询电池
handler.postDelayed(this, 1000); // 每秒重复
}
};

public void start() {
handler.post(batteryRunnable);
}

public void stop() {
handler.removeCallbacks(batteryRunnable);
}
优点:简单、直接、在主线程可操作 UI。
缺点:任务在主线程执行,不适合耗时操作


2. ScheduledExecutorService(后台线程)
private ScheduledExecutorService scheduler;

public void start() {
scheduler = Executors.newSingleThreadScheduledExecutor();
scheduler.scheduleAtFixedRate(() -> updateBattery(context), 0, 1, TimeUnit.SECONDS);
}

public void stop() {
if (scheduler != null) {
scheduler.shutdownNow();
scheduler = null;
}
}

优点:在后台线程,不会阻塞主线程,可扩展。
缺点:操作 UI 需要 Handler 或 runOnUiThread()。

3. RxJava(如果项目已经有 Rx)

disposable = Observable.interval(0, 1, TimeUnit.SECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(tick -> updateBattery(context));

优点:易于管理订阅和取消,UI 操作简单。
缺点:依赖 RxJava。

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
1. build.gradle 依赖
确保你的 app 模块的 build.gradle(通常是 app/build.gradle)包含以下依赖:

// Room 依赖 分为java和kotlin
//java的
def room_version = "2.5.2"
implementation "androidx.room:room-runtime:$room_version"
annotationProcessor "androidx.room:room-compiler:$room_version"




//kotlin必须 kapt
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
id 'kotlin-kapt'
}

implementation "androidx.room:room-runtime:$room_version"
implementation "androidx.room:room-ktx:$room_version" // 可选,支持协程
kapt "androidx.room:room-compiler:$room_version" // Kotlin 必须用 kapt






import androidx.room.Entity;
import androidx.room.PrimaryKey;

@Entity
public class User {
@PrimaryKey(autoGenerate = true)
public int userId;

public String name;
public int age;

public User(String name, int age) {
this.name = name;
this.age = age;
}
}

import androidx.room.Dao;
import androidx.room.Insert;
import androidx.room.Query;
import java.util.List;

@Dao
public interface UserDao {

@Insert
void insertUser(User user);

@Query("SELECT * FROM User")
List<User> getAllUsers();
}

import androidx.room.Database;
import androidx.room.RoomDatabase;

@Database(entities = {User.class}, version = 1)
public abstract class AppDatabase extends RoomDatabase {
public abstract UserDao userDao();
}

// 在Activity或Application里初始化
AppDatabase db = Room.databaseBuilder(getApplicationContext(),
AppDatabase.class, "my-database").build();

// 插入数据(注意不能在主线程,建议放到子线程)
new Thread(() -> {
UserDao userDao = db.userDao();
BookDao bookDao = db.bookDao();

User user = new User("Alice", 30);
userDao.insertUser(user);

Book book = new Book("Android Development", 1); // 这里1为userId,需要先获取
bookDao.insertBook(book);

List<User> users = userDao.getAllUsers();
List<Book> books = bookDao.getBooksByUserId(1);

// 处理结果
}).start();



import android.app.Application;
import androidx.room.Room;

public class MyApp extends Application {

private static AppDatabase database;

@Override
public void onCreate() {
super.onCreate();
database = Room.databaseBuilder(getApplicationContext(),
AppDatabase.class, "my-database")
.build();
}

// 对外暴露获取数据库实例的方法
public static AppDatabase getDatabase() {
return database;
}
}

其他页面调用
AppDatabase db = MyApp.getDatabase();
UserDao userDao = db.userDao();

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
		  try (InputStream is = context.getAssets().open(filePath)) {
List<AirPortNoFlyEntity> points = parseExcelPoints2(is);
addMarkersToMap(baiduMap,points,RADIUS_KM);

} catch (Exception e) {
e.printStackTrace();
}


private List<AirPortNoFlyEntity> parseExcelPoints2(InputStream is) throws Exception {
try {
// 从 assets 读取 JSON 文件
byte[] buffer = new byte[is.available()];
is.read(buffer);
is.close();
String json = new String(buffer, StandardCharsets.UTF_8);

// 使用 Gson 解析为 List<Point>
return new Gson().fromJson(json, new TypeToken<List<AirPortNoFlyEntity>>() {}.getType());
} catch (Exception e) {
e.printStackTrace();
return null;
}
}

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

✅ TorchScript (.pt) → PyTorch Mobile
✅ ONNX (.onnx) → TensorFlow Lite
✅ TFLite (.tflite) → 通用方案
.pt文件是PyTorch保存模型的格式,通常包含模型的结构与参数。保存的文件可以分为两类: 仅保存模型参数:通常使用torch.save(model.state_dict(), 'model.pt')来保存。 保存整个模型:使用torch.save(model, 'model.pt')来保存。



.pth 文件是 PyTorch 训练后保存的模型权重文件,不能直接在 Android 中使用,因为 Android 原生不支持 PyTorch 的 Python 环境。但你可以通过以下方法将其转换为 Android 可用的格式并集成到项目中:

1. 转换 .pth 为 Android 兼容格式
方案 1:转换为 TorchScript(PyTorch Mobile 官方支持)
步骤:

将 .pth 转换为 TorchScript(.pt 文件)

python
import torch
from your_model import YourModel # 替换为你的模型类

# 加载模型权重
model = YourModel()
model.load_state_dict(torch.load("model.pth"))
model.eval() # 设置为推理模式

# 转换为 TorchScript
example_input = torch.rand(1, 3, 224, 224) # 替换为你的输入形状
traced_script = torch.jit.trace(model, example_input)
traced_script.save("model.pt") # 保存为 .pt 文件
在 Android 中集成 PyTorch Mobile

将 model.pt 放入 app/src/main/assets/。

添加依赖:

gradle
dependencies {
implementation 'org.pytorch:pytorch_android_lite:2.3.0' // 最新版本
}
加载模型并推理:

java
Module module = Module.load(assetFilePath(this, "model.pt"));
Tensor inputTensor = Tensor.fromBlob(inputData, new long[]{1, 3, 224, 224});
Tensor outputTensor = module.forward(IValue.from(inputTensor)).toTensor();

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

private Polyline realtimePolyline;
private Track finalizedTrack;
private List<LatLng> allPoints = new ArrayList<>();
private int[] allHeights = new int[0];

private int heightCounter = 1000;
private Marker aircraftMarker;
private Marker aircraftControllMarker;
private LocationClient locationClient;
/**
* 添加点 高度 进入地图
* @param newPoint
* @param height
*/
public void addPointCombined(LatLng newPoint, int height) {

// 添加新数据
allPoints.add(newPoint);

// 更新高度数组
int[] newHeights = Arrays.copyOf(allHeights, allHeights.length + 1);
newHeights[allHeights.length] = height;
allHeights = newHeights;

runOnUiThread(new Runnable() {
@Override
public void run() {
//更新点
if (aircraftMarker == null) {
// 第一次创建标记
MarkerOptions markerOptions = new MarkerOptions()
.position(newPoint)
.title("飞机位置")
.icon(BitmapDescriptorFactory.fromResource(R.mipmap.icon_plane)) // 你的飞机图标
.anchor(0.5f, 0.5f);
aircraftMarker = (Marker) baiduMap.addOverlay(markerOptions);
} else {
// 后续更新位置
aircraftMarker.setPosition(newPoint);
}

// 更新实时线段
if(allPoints.size()>=2){
if (realtimePolyline == null) {
PolylineOptions options = new PolylineOptions()
.points(allPoints)
.width(8)
.color(0xAA0000FF);
realtimePolyline = (Polyline) baiduMap.addOverlay(options);
} else {
realtimePolyline.setPoints(allPoints);
}
}


// 每20个点生成一次3D轨迹
if (allPoints.size() % 20 == 0) {
update3DTrack();
}

// 调整视角,移动到中心位置,测试先关闭
// MapStatusUpdate mapStatusUpdate = MapStatusUpdateFactory.newLatLng(newPoint);
// baiduMap.animateMapStatus(mapStatusUpdate);
}
});

}

/**
* 更新3D路线
*/
private void update3DTrack() {
if (finalizedTrack != null) {
finalizedTrack.remove();
}

// ... 自定义绘制颜色渐变到paletteBitmap ...
BitmapDescriptor paletteDesc = BitmapDescriptorFactory.fromResource(R.mipmap.ic_color);
// 方法1:从资源文件创建(推荐)
BitmapDescriptor projPalette = BitmapDescriptorFactory.fromResource(R.mipmap.ic_color);

BMTrackOptions options = new BMTrackOptions();
// options.setTrackType(BMTrackType.Surface);
options.setTrackType(BMTrackType.Default3D);
options.setPoints(allPoints);
options.setHeights(allHeights);
options.setPalette(createLineTexture(Color.parseColor("#4285F4"), 3));//一条显示,中间不显示
int[] gradientColors = {Color.RED, Color.YELLOW, Color.GREEN};
options.setOpacity(1f);
options.setPaletteOpacity(0.1f); // 透明度
options.setProjectionPalette(projPalette);
options.setWidth(10); // 轨迹宽度
options.setAnimationTime(100); // 动画时长(毫秒)
options.setTraceAnimationListener(mTraceAnimationListener);
// 其他设置...

finalizedTrack = (Track) baiduMap.addOverlay(options);

}

/**
* 动画监听
*/
private TraceAnimationListener mTraceAnimationListener = new TraceAnimationListener() {

@Override
public void onTraceAnimationUpdate(float v) {
// LogUtils.d("onTraceAnimationUpdate:"+v);
}

@Override
public void onTraceUpdatePosition(com.baidu.mapapi.model.LatLng latLng) {
// LogUtils.d("onTraceUpdatePosition:"+latLng.toString());
}

@Override
public void onTraceAnimationFinish() {
// LogUtils.d("onTraceAnimationFinish:");
}
};


//轨迹颜色start

private BitmapDescriptor createLineTexture(int color, int width) {
// 创建1xN像素的Bitmap(N=线宽)
// 创建一个1xwidth像素的纹理图
Bitmap bitmap = Bitmap.createBitmap(width, 1, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
Paint paint = new Paint();
paint.setColor(color);
paint.setStrokeWidth(width);
canvas.drawLine(0, 0, width, 0, paint);

return BitmapDescriptorFactory.fromBitmap(bitmap);
}



//轨迹颜色end

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Android 存储目录访问与写入能力总表

公共媒体目录
(DCIM, Pictures等) Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES) ✅ 但Android 10+需通过MediaStore写入 Android 9-:WRITE_EXTERNAL_STORAGE
Android 10+:MediaStore无权限要求 ✔️ 自动显示 ❌ 保留 用户可见的照片、视频、音乐

应用私有目录

(外部存储) context.getExternalFilesDir(Environment.DIRECTORY_PICTURES) ✅ 始终可写 ❌ 无需权限 ❌ 需手动扫描 ✔️ 删除 应用专属文件,无需共享

应用缓存目录 context.getExternalCacheDir() ✅ 可写但空间有限 ❌ 无需权限 ❌ 不扫描 ✔️ 删除(可能被系统提前清理) 临时文件、缓存

物理SD卡目录 ContextCompat.getExternalFilesDirs(context, null)[1] ⚠️ 需用户手动授权(部分厂商特殊权限) 依赖厂商实现 ❌ 需手动扫描 ❌ 保留 大容量文件(如无人机拍摄原片)

MediaStore专属目录
(Android 10+) MediaStore.Images.Media.getContentUri() + RELATIVE_PATH ✅ 通过ContentResolver写入 ❌ 无需权限(但需声明READ_EXTERNAL_STORAGE读取现有文件) ✔️ 自动显示 ❌ 保留 兼容Android 10+的媒体文件存储


旧版SD卡根目录
(不推荐) Environment.getExternalStorageDirectory() ⚠️ Android 10+只读(需MANAGE_EXTERNAL_STORAGE特殊权限) Android 9-:WRITE_EXTERNAL_STORAGE
Android 10+:需特殊申请 ✔️ 但可能被系统限制 ❌ 保留 仅旧版兼容时使用


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
//百度地图
implementation 'com.baidu.lbsyun:BaiduMapSDK_Map:7.6.4'
implementation 'com.baidu.lbsyun:BaiduMapSDK_Util:7.6.4' // 工具库(部分功能需要)
implementation 'com.baidu.lbsyun:BaiduMapSDK_Location_All:9.6.4' // 高精度定位

//百度地图:在使用SDK各组件之前初始化context信息,传入ApplicationContext
SDKInitializer.setAgreePrivacy(getApplicationContext(),true);
SDKInitializer.initialize(getApplicationContext());
//自4.3.0起,百度地图SDK所有接口均支持百度坐标和国测局坐标,用此方法设置您使用的坐标类型.
//包括BD09LL和GCJ02两种坐标,默认是BD09LL坐标。
SDKInitializer.setCoordType(CoordType.BD09LL);

/**
* 设置地图定位
*/
public void setupMap(){
Log.e("location", "百度隐私合规状态: " + SDKInitializer.getAgreePrivacy());
// 1. 显式设置定位SDK隐私合规
com.baidu.location.LocationClient.setAgreePrivacy(true);
//定位服务的客户端。宿主程序在客户端声明此类,并调用,目前只支持在主线程中启动

try {
locationClient = new LocationClient(mContext);
} catch (Exception e) {
Log.e("location","初始化失败"+e);
return;
}
//声明LocationClient类实例并配置定位参数
LocationClientOption locationOption = new LocationClientOption();
MyLocationListener myLocationListener = new MyLocationListener();
//注册监听函数
locationClient.registerLocationListener(myLocationListener);
//可选,默认高精度,设置定位模式,高精度,低功耗,仅设备
locationOption.setLocationMode(LocationClientOption.LocationMode.Hight_Accuracy);
//可选,默认gcj02,设置返回的定位结果坐标系,如果配合百度地图使用,建议设置为bd09ll;
// locationOption.setCoorType("gcj02");
//可选,默认0,即仅定位一次,设置发起连续定位请求的间隔需要大于等于1000ms才是有效的
locationOption.setScanSpan(1000);
//可选,设置是否需要地址信息,默认不需要
locationOption.setIsNeedAddress(true);
//可选,设置是否需要地址描述
locationOption.setIsNeedLocationDescribe(true);
//可选,设置是否需要设备方向结果
locationOption.setNeedDeviceDirect(false);
//可选,默认false,设置是否当卫星定位有效时按照1S1次频率输出卫星定位结果
locationOption.setLocationNotify(true);
//可选,默认true,定位SDK内部是一个SERVICE,并放到了独立进程,设置是否在stop的时候杀死这个进程,默认不杀死
locationOption.setIgnoreKillProcess(true);
//可选,默认false,设置是否需要位置语义化结果,可以在BDLocation.getLocationDescribe里得到,结果类似于“在北京天安门附近”
locationOption.setIsNeedLocationDescribe(true);
//可选,默认false,设置是否需要POI结果,可以在BDLocation.getPoiList里得到
locationOption.setIsNeedLocationPoiList(true);
//可选,默认false,设置是否收集CRASH信息,默认收集
locationOption.SetIgnoreCacheException(false);
//可选,默认false,设置是否开启卫星定位
locationOption.setOpenGnss(true);
//可选,默认false,设置定位时是否需要海拔信息,默认不需要,除基础定位版本都可用
locationOption.setIsNeedAltitude(false);
//设置打开自动回调位置模式,该开关打开后,期间只要定位SDK检测到位置变化就会主动回调给开发者,该模式下开发者无需再关心定位间隔是多少,定位SDK本身发现位置变化就会及时回调给开发者
locationOption.setOpenAutoNotifyMode();
//设置打开自动回调位置模式,该开关打开后,期间只要定位SDK检测到位置变化就会主动回调给开发者
locationOption.setOpenAutoNotifyMode(3000,1, LocationClientOption.LOC_SENSITIVITY_HIGHT);
//需将配置好的LocationClientOption对象,通过setLocOption方法传递给LocationClient对象使用
locationClient.setLocOption(locationOption);
//开始定位
locationClient.start();
}

private class MyLocationListener extends BDAbstractLocationListener {
@Override
public void onReceiveLocation(BDLocation location){
//此处的BDLocation为定位结果信息类,通过它的各种get方法可获取定位相关的全部结果
//以下只列举部分获取经纬度相关(常用)的结果信息
//更多结果信息获取说明,请参照类参考中BDLocation类中的说明
LogUtils.d("坐标类型:"+location.getCoorType());

//获取纬度信息
double latitude = location.getLatitude();
//获取经度信息
double longitude = location.getLongitude();
//获取定位精度,默认值为0.0f
float radius = location.getRadius();
//获取经纬度坐标类型,以LocationClientOption中设置过的坐标类型为准
String coorType = location.getCoorType();
//获取定位类型、定位错误返回码,具体信息可参照类参考中BDLocation类中的说明
int errorCode = location.getLocType();
LatLng baiduPoint = BaiduCoordinateTransformUtils.convertToBaiduCoord(location);
addControllMarker(baiduPoint);
MapPoint mapPoint = new MapPoint(latitude, longitude, "");
SPUtils.putObject(SPKeysConstance.MAP_POINT, mapPoint); // 建议内部用 apply()

}
}




以下是自己封装的工具类,可供外部调用:


import android.content.Context;
import android.graphics.Color;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;

import com.baidu.location.BDAbstractLocationListener;
import com.baidu.location.BDLocation;
import com.baidu.location.LocationClient;
import com.baidu.location.LocationClientOption;
import com.baidu.mapapi.SDKInitializer;
import com.baidu.mapapi.map.BMTrackOptions;
import com.baidu.mapapi.map.BMTrackType;
import com.baidu.mapapi.map.BaiduMap;
import com.baidu.mapapi.map.BitmapDescriptor;
import com.baidu.mapapi.map.BitmapDescriptorFactory;
import com.baidu.mapapi.map.CircleOptions;
import com.baidu.mapapi.map.MapStatusUpdate;
import com.baidu.mapapi.map.MapStatusUpdateFactory;
import com.baidu.mapapi.map.Marker;
import com.baidu.mapapi.map.MarkerOptions;
import com.baidu.mapapi.map.MyLocationConfiguration;
import com.baidu.mapapi.map.Polyline;
import com.baidu.mapapi.map.PolylineOptions;
import com.baidu.mapapi.map.Stroke;
import com.baidu.mapapi.map.Track;
import com.baidu.mapapi.map.track.TraceAnimationListener;
import com.baidu.mapapi.model.LatLng;
import com.baidu.mapapi.utils.CoordinateConverter;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import com.htnova.fly.R;
import com.htnova.fly.activity.HtFlightBaiduMapActivity;
import com.htnova.fly.entity.AirPortNoFlyEntity;
import com.htnova.fly.entity.MapPoint;
import com.htnova.fly.entity.SPKeysConstance;

import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
* @author xqm
* @date 2025/8/1 9:42
* @description BaiduMapHelper 将 BaiduMap 绑定到工具类实例中,避免重复传递,同时保持生命周期安全。
*/
public class BaiduMapHelper {
private final BaiduMap baiduMap;
private final Handler mainHandler; // 主线程Handler
private final Context mContext; // 添加Context成员变量
private static final double RADIUS_KM = 5; // 5公里半径
private static final int CIRCLE_COLOR = 0x22FF0000; // 半透明红色
private static final int STROKE_COLOR = 0xFFFF0000; // 红色边框
private static final int STROKE_WIDTH = 2; // 边框宽度

private Polyline realtimePolyline;
private Track finalizedTrack;
private List<LatLng> allPoints = new ArrayList<>();
private int[] allHeights = new int[0];

private int heightCounter = 1000;
private Marker aircraftMarker;//
private Marker aircraftControllMarker; //
private LocationClient locationClient;


// 初始化时绑定 BaiduMap
public BaiduMapHelper(Context context,BaiduMap baiduMap) {
this.mContext = context.getApplicationContext();
this.baiduMap = baiduMap;
this.mainHandler = new Handler(Looper.getMainLooper()); // 绑定主线程Looper
}

/**
* 在主线程执行任务
*/
private void runOnUiThread(Runnable task) {
mainHandler.post(task);
}

/**
* 读取assets目录下的json文件并添加在地图上
* @param context
* @param baiduMap
* @param filePath
*/
public void drawCirclesFromFile(Context context, BaiduMap baiduMap,
String filePath) {
new Thread(() -> {
try (InputStream is = context.getAssets().open(filePath)) {
List<AirPortNoFlyEntity> points = parseExcelPoints2(is);
addMarkersToMap(baiduMap,points,RADIUS_KM);

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

private List<AirPortNoFlyEntity> parseExcelPoints2(InputStream is) throws Exception {
try {
// 从 assets 读取 JSON 文件
byte[] buffer = new byte[is.available()];
is.read(buffer);
is.close();
String json = new String(buffer, StandardCharsets.UTF_8);

// 使用 Gson 解析为 List<Point>
return new Gson().fromJson(json, new TypeToken<List<AirPortNoFlyEntity>>() {}.getType());
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
private void addMarkersToMap(BaiduMap baiduMap,List<AirPortNoFlyEntity> points,double radiusKm) {

if (points == null || points.isEmpty()) return;

// 分批次添加(每批50个)
int batchSize = 50;
for (int i = 0; i < points.size(); i += batchSize) {
int end = Math.min(i + batchSize, points.size());
List<AirPortNoFlyEntity> batch = points.subList(i, end);

// 延迟分批提交,减轻主线程压力
new Handler(Looper.getMainLooper()).postDelayed(() -> {
for (AirPortNoFlyEntity point : batch) {
LatLng corrected = BaiduCoordinateTransformUtils.wgsToBd(new LatLng(point.getLatitude(), point.getLongitude()));

CircleOptions options = new CircleOptions()
.center(corrected)
.radius((int) (radiusKm * 1000))
.fillColor(CIRCLE_COLOR)
.stroke(new Stroke(STROKE_WIDTH, STROKE_COLOR));
baiduMap.addOverlay(options);
}
}, i * 10L); // 间隔时间(毫秒)
}
}




// 在地图上添加圆形覆盖物
private void addCircleToMap(BaiduMap baiduMap, LatLng center, double radiusKm) {
runOnUiThread(new Runnable() {
@Override
public void run() {
CircleOptions options = new CircleOptions()
.center(center)
.radius((int) (radiusKm * 1000)) // 转换为米
.fillColor(CIRCLE_COLOR)
.stroke(new Stroke(STROKE_WIDTH, STROKE_COLOR));

baiduMap.addOverlay(options);
}
});

}

public void addControllMarker(LatLng latLng){
runOnUiThread(new Runnable() {
@Override
public void run() {
if(aircraftControllMarker == null){
MarkerOptions markerOptions = new MarkerOptions()
.position(latLng)
.title("飞机位置")
.icon(BitmapDescriptorFactory.fromResource(R.mipmap.ic_fly_control)) // 你的飞机图标
.anchor(0.5f, 0.5f);
aircraftControllMarker = (Marker) baiduMap.addOverlay(markerOptions);
}else{
// 更新已有Marker位置
aircraftControllMarker.setPosition(latLng);
}
}
});



}

/**
* 设置地图定位
*/
public void setupMap(){
Log.e("location", "百度隐私合规状态: " + SDKInitializer.getAgreePrivacy());
// 1. 显式设置定位SDK隐私合规
com.baidu.location.LocationClient.setAgreePrivacy(true);
//定位服务的客户端。宿主程序在客户端声明此类,并调用,目前只支持在主线程中启动

try {
locationClient = new LocationClient(mContext);
} catch (Exception e) {
Log.e("location","初始化失败"+e);
return;
}
//声明LocationClient类实例并配置定位参数
LocationClientOption locationOption = new LocationClientOption();
MyLocationListener myLocationListener = new MyLocationListener();
//注册监听函数
locationClient.registerLocationListener(myLocationListener);
//可选,默认高精度,设置定位模式,高精度,低功耗,仅设备
locationOption.setLocationMode(LocationClientOption.LocationMode.Hight_Accuracy);
//可选,默认gcj02,设置返回的定位结果坐标系,如果配合百度地图使用,建议设置为bd09ll;
// locationOption.setCoorType("gcj02");
//可选,默认0,即仅定位一次,设置发起连续定位请求的间隔需要大于等于1000ms才是有效的
locationOption.setScanSpan(1000);
//可选,设置是否需要地址信息,默认不需要
locationOption.setIsNeedAddress(true);
//可选,设置是否需要地址描述
locationOption.setIsNeedLocationDescribe(true);
//可选,设置是否需要设备方向结果
locationOption.setNeedDeviceDirect(false);
//可选,默认false,设置是否当卫星定位有效时按照1S1次频率输出卫星定位结果
locationOption.setLocationNotify(true);
//可选,默认true,定位SDK内部是一个SERVICE,并放到了独立进程,设置是否在stop的时候杀死这个进程,默认不杀死
locationOption.setIgnoreKillProcess(true);
//可选,默认false,设置是否需要位置语义化结果,可以在BDLocation.getLocationDescribe里得到,结果类似于“在北京天安门附近”
locationOption.setIsNeedLocationDescribe(true);
//可选,默认false,设置是否需要POI结果,可以在BDLocation.getPoiList里得到
locationOption.setIsNeedLocationPoiList(true);
//可选,默认false,设置是否收集CRASH信息,默认收集
locationOption.SetIgnoreCacheException(false);
//可选,默认false,设置是否开启卫星定位
locationOption.setOpenGnss(true);
//可选,默认false,设置定位时是否需要海拔信息,默认不需要,除基础定位版本都可用
locationOption.setIsNeedAltitude(false);
//设置打开自动回调位置模式,该开关打开后,期间只要定位SDK检测到位置变化就会主动回调给开发者,该模式下开发者无需再关心定位间隔是多少,定位SDK本身发现位置变化就会及时回调给开发者
locationOption.setOpenAutoNotifyMode();
//设置打开自动回调位置模式,该开关打开后,期间只要定位SDK检测到位置变化就会主动回调给开发者
locationOption.setOpenAutoNotifyMode(3000,1, LocationClientOption.LOC_SENSITIVITY_HIGHT);
//需将配置好的LocationClientOption对象,通过setLocOption方法传递给LocationClient对象使用
locationClient.setLocOption(locationOption);
//开始定位
locationClient.start();
}

private class MyLocationListener extends BDAbstractLocationListener {
@Override
public void onReceiveLocation(BDLocation location){
//此处的BDLocation为定位结果信息类,通过它的各种get方法可获取定位相关的全部结果
//以下只列举部分获取经纬度相关(常用)的结果信息
//更多结果信息获取说明,请参照类参考中BDLocation类中的说明
LogUtils.d("坐标类型:"+location.getCoorType());

//获取纬度信息
double latitude = location.getLatitude();
//获取经度信息
double longitude = location.getLongitude();
//获取定位精度,默认值为0.0f
float radius = location.getRadius();
//获取经纬度坐标类型,以LocationClientOption中设置过的坐标类型为准
String coorType = location.getCoorType();
//获取定位类型、定位错误返回码,具体信息可参照类参考中BDLocation类中的说明
int errorCode = location.getLocType();
LatLng baiduPoint = BaiduCoordinateTransformUtils.convertToBaiduCoord(location);
addControllMarker(baiduPoint);
MapPoint mapPoint = new MapPoint(latitude, longitude, "");
SPUtils.putObject(SPKeysConstance.MAP_POINT, mapPoint); // 建议内部用 apply()

}
}

public void stopLocation() {
if (locationClient != null) {
locationClient.stop();
}
}

// 添加资源释放方法
public void release() {
if (locationClient != null) {
locationClient.stop();
locationClient = null;
}
// 清理其他资源...
// 释放地图资源
if (baiduMap != null) {
baiduMap.setMyLocationEnabled(false);
baiduMap.clear();
}
}

/**
* 添加点 高度 进入地图
* @param newPoint
* @param height
*/
public void addPointCombined(LatLng newPoint, int height) {

// 添加新数据
allPoints.add(newPoint);

// 更新高度数组
int[] newHeights = Arrays.copyOf(allHeights, allHeights.length + 1);
newHeights[allHeights.length] = height;
allHeights = newHeights;

runOnUiThread(new Runnable() {
@Override
public void run() {
//更新点
if (aircraftMarker == null) {
// 第一次创建标记
MarkerOptions markerOptions = new MarkerOptions()
.position(newPoint)
.title("飞机位置")
.icon(BitmapDescriptorFactory.fromResource(R.mipmap.icon_plane)) // 你的飞机图标
.anchor(0.5f, 0.5f);
aircraftMarker = (Marker) baiduMap.addOverlay(markerOptions);
} else {
// 后续更新位置
aircraftMarker.setPosition(newPoint);
}

// 更新实时线段
if(allPoints.size()>=2){
if (realtimePolyline == null) {
PolylineOptions options = new PolylineOptions()
.points(allPoints)
.width(8)
.color(0xAA0000FF);
realtimePolyline = (Polyline) baiduMap.addOverlay(options);
} else {
realtimePolyline.setPoints(allPoints);
}
}


// 每20个点生成一次3D轨迹
if (allPoints.size() % 20 == 0) {
update3DTrack();
}

// 调整视角
MapStatusUpdate mapStatusUpdate = MapStatusUpdateFactory.newLatLng(newPoint);
baiduMap.animateMapStatus(mapStatusUpdate);
}
});

}

/**
* 更新3D路线
*/
private void update3DTrack() {
if (finalizedTrack != null) {
finalizedTrack.remove();
}

// ... 自定义绘制颜色渐变到paletteBitmap ...
BitmapDescriptor paletteDesc = BitmapDescriptorFactory.fromResource(R.mipmap.ic_color);
// 方法1:从资源文件创建(推荐)
BitmapDescriptor projPalette = BitmapDescriptorFactory.fromResource(R.mipmap.ic_color);


BMTrackOptions options = new BMTrackOptions();
options.setTrackType(BMTrackType.Surface);
options.setPoints(allPoints);
options.setHeights(allHeights);

options.setPalette(paletteDesc); // 颜色渐变
options.setPaletteOpacity(0.8f); // 透明度
options.setProjectionPalette(projPalette);
options.setWidth(10); // 轨迹宽度
options.setAnimationTime(1000); // 动画时长(毫秒)
options.setTraceAnimationListener(mTraceAnimationListener);
// 其他设置...

finalizedTrack = (Track) baiduMap.addOverlay(options);

}

/**
* 动画监听
*/
private TraceAnimationListener mTraceAnimationListener = new TraceAnimationListener() {

@Override
public void onTraceAnimationUpdate(float v) {
// LogUtils.d("onTraceAnimationUpdate:"+v);
}

@Override
public void onTraceUpdatePosition(com.baidu.mapapi.model.LatLng latLng) {
// LogUtils.d("onTraceUpdatePosition:"+latLng.toString());
}

@Override
public void onTraceAnimationFinish() {
// LogUtils.d("onTraceAnimationFinish:");
}
};


// 模拟实时接收GPS数据并调用方法
public void simulateRealTimeGPS() {
// 模拟调用飞机飞行
// 起点,比如某地经纬度
final double startLat = 39.90923;
final double startLng = 116.397428;

final Handler handler = new Handler(Looper.getMainLooper());
final int[] step = {0};

Runnable runnable = new Runnable() {
@Override
public void run() {
// 模拟飞机沿直线慢慢向北移动,每次移动0.0001度纬度
double newLat = startLat + 0.0001 * step[0];
double newLng = startLng;

LatLng newPosition = new LatLng(newLat, newLng);
addPointCombined(newPosition,heightCounter+=300);

step[0]++;
if (step[0] < 100) { // 模拟100次更新
handler.postDelayed(this, 1000); // 1秒更新一次
}
}
};
handler.post(runnable);

}

}

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


import android.content.Context;
import android.util.Log;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;

public class LogUtils {
private static final String DEFAULT_TAG = "AppLog";
private static boolean isDebug = true;
private static boolean writeToFile = true;

private static final String LOG_FILE_PREFIX = "log_"; // 文件名前缀
private static final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd", Locale.CHINA);
private static final SimpleDateFormat timeFormat = new SimpleDateFormat("HH:mm:ss.SSS", Locale.CHINA);

// 记录应用上下文,初始化时调用
private static Context sContext;

public static void init(Context context) {
sContext = context.getApplicationContext();
}

public static void setDebug(boolean debug) {
isDebug = debug;
}

public static void setWriteToFile(boolean write) {
writeToFile = write;
}

public static void d(String msg) {
d(DEFAULT_TAG, msg);
}

public static void d(String tag, String msg) {
if (isDebug && msg != null) {
Log.d(tag, msg);
writeLogToFile("D", tag, msg);
}
}

public static void i(String msg) {
i(DEFAULT_TAG, msg);
}

public static void i(String tag, String msg) {
if (isDebug && msg != null) {
Log.i(tag, msg);
writeLogToFile("I", tag, msg);
}
}

public static void w(String msg) {
w(DEFAULT_TAG, msg);
}

public static void w(String tag, String msg) {
if (isDebug && msg != null) {
Log.w(tag, msg);
writeLogToFile("W", tag, msg);
}
}

public static void e(String msg) {
e(DEFAULT_TAG, msg);
}

public static void e(String tag, String msg) {
if (isDebug && msg != null) {
Log.e(tag, msg);
writeLogToFile("E", tag, msg);
}
}

public static void e(String tag, String msg, Throwable tr) {
if (isDebug && msg != null) {
Log.e(tag, msg, tr);
writeLogToFile("E", tag, msg + "\n" + Log.getStackTraceString(tr));
}
}

private static void writeLogToFile(String level, String tag, String message) {
if (!writeToFile || sContext == null) return;

try {
String today = dateFormat.format(new Date());
String time = timeFormat.format(new Date());

// 使用应用私有外部存储目录: /Android/data/你的包名/files/AppLogs/
File logDir = new File(sContext.getExternalFilesDir(null), "AppLogs");
if (!logDir.exists()) {
logDir.mkdirs();
}

File logFile = new File(logDir, LOG_FILE_PREFIX + today + ".txt");
FileWriter writer = new FileWriter(logFile, true);
writer.write(String.format("[%s][%s][%s]: %s\n", time, level, tag, message));
writer.flush();
writer.close();
} catch (IOException e) {
Log.e("LogUtils", "writeLogToFile failed: " + e.getMessage());
}
}
}

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
一、单例模式的核心思想
定义:
确保一个类 只有一个实例,并提供一个全局访问点。

生活类比:

公司只有一个CEO(所有人找CEO办事都找同一个人)

电脑只有一个任务管理器(无论打开多少次都是同一个窗口)

二、单例模式的5种Java实现
我们从基础到高级逐步优化:

1️⃣ 饿汉式(线程安全)
java
public class CEO {
// 类加载时就初始化实例
private static final CEO instance = new CEO();

// 私有构造方法
private CEO() {}

// 全局访问点
public static CEO getInstance() {
return instance;
}

public void manageCompany() {
System.out.println("CEO在管理公司");
}
}

// 使用
CEO ceo1 = CEO.getInstance();
CEO ceo2 = CEO.getInstance();
System.out.println(ceo1 == ceo2); // 输出true
特点:

线程安全(因为类加载时初始化)

可能造成资源浪费(如果从未使用过这个实例)

2️⃣ 懒汉式(非线程安全)
java
public class TaskManager {
private static TaskManager instance;

private TaskManager() {}

// 需要时再创建实例(懒加载)
public static TaskManager getInstance() {
if (instance == null) {
instance = new TaskManager();
}
return instance;
}

public void showProcesses() {
System.out.println("显示运行中的进程");
}
}
问题:多线程环境下可能创建多个实例(❌ 不安全)

3️⃣ 懒汉式(线程安全版)
java
public synchronized static TaskManager getInstance() {
if (instance == null) {
instance = new TaskManager();
}
return instance;
}
缺点:每次获取实例都要同步,性能差

4️⃣ 双重检查锁(DCL)
java
public class Database {
private static volatile Database instance;

private Database() {}

public static Database getInstance() {
if (instance == null) { // 第一次检查
synchronized (Database.class) { // 加锁
if (instance == null) { // 第二次检查
instance = new Database();
}
}
}
return instance;
}
}
关键点:

volatile 防止指令重排序

两次判空确保线程安全

只在第一次创建时同步

这是Android中最常用的单例实现方式

5️⃣ 静态内部类(推荐)
java
public class SettingsManager {
private SettingsManager() {}

private static class Holder {
static final SettingsManager INSTANCE = new SettingsManager();
}

public static SettingsManager getInstance() {
return Holder.INSTANCE;
}
}
优势:

线程安全(JVM保证类加载的线程安全)

懒加载(只有调用getInstance()时才会加载Holder类)

代码简洁

三、Android中的实际应用
案例1:全局工具类
java
// 网络工具单例
public class NetworkUtils {
private static volatile NetworkUtils instance;

public static NetworkUtils getInstance() {
if (instance == null) {
synchronized (NetworkUtils.class) {
if (instance == null) {
instance = new NetworkUtils();
}
}
}
return instance;
}

private NetworkUtils() {
// 初始化网络相关配置
}

public boolean isNetworkAvailable() {
// 实现网络检查
}
}
案例2:SharedPreferences管理
java
public class PrefsManager {
private static PrefsManager instance;
private SharedPreferences prefs;

private PrefsManager(Context context) {
prefs = context.getSharedPreferences("my_prefs", Context.MODE_PRIVATE);
}

public static synchronized PrefsManager getInstance(Context context) {
if (instance == null) {
instance = new PrefsManager(context.getApplicationContext());
}
return instance;
}

public void saveString(String key, String value) {
prefs.edit().putString(key, value).apply();
}
}
四、单例模式的注意事项
内存泄漏风险:

在Android中避免持有Activity/Fragment的引用

推荐使用Application Context

单元测试困难:

单例难以被Mock或替换

解决方案:考虑依赖注入(如Dagger)

多进程问题:

每个进程会有自己的单例实例

解决方案:使用文件锁或ContentProvider

五、常见问题
Q1:为什么要用双重检查锁?
👉 既保证线程安全,又避免每次获取实例都同步,提升性能。

Q2:volatile关键字的作用?
👉 1. 保证可见性 2. 防止指令重排序(避免返回未初始化完成的对象)

Q3:单例模式违反单一职责原则吗?
👉 确实可能同时承担"创建对象"和"业务逻辑"两个职责,这是它的缺点之一。

Q4:如何破坏单例?如何防御?
👉 破坏方式:

反射:通过反射调用私有构造方法
防御:在构造方法中加判断

java
private Singleton() {
if (instance != null) {
throw new RuntimeException("请使用getInstance()方法");
}
}
反序列化:序列化后再反序列化会创建新对象
防御:实现readResolve()方法

java
protected Object readResolve() {
return getInstance();
}
六、代码模板(直接可用)
java
public class AppManager {
private static volatile AppManager instance;
private Context appContext;

private AppManager(Context context) {
this.appContext = context.getApplicationContext();
}

public static AppManager getInstance(Context context) {
if (instance == null) {
synchronized (AppManager.class) {
if (instance == null) {
instance = new AppManager(context);
}
}
}
return instance;
}

// 示例方法
public void doSomething() {
Toast.makeText(appContext, "单例方法被调用", Toast.LENGTH_SHORT).show();
}
}
掌握单例模式后,可以在这些场景中使用它:

全局配置管理

日志工具类

数据库访问

文件系统操作

网络请求客户端