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

✅ 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();
}
}
掌握单例模式后,可以在这些场景中使用它:

全局配置管理

日志工具类

数据库访问

文件系统操作

网络请求客户端

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
一、工厂模式核心思想
解决的问题:
当代码中需要根据不同条件创建不同对象时,避免到处使用 new 关键字,而是将对象创建过程封装起来。

生活类比:

普通方式:自己买面粉、糖、鸡蛋...做蛋糕(相当于直接 new 对象)

工厂模式:去蛋糕店直接点"巧克力蛋糕"(工厂帮你处理复杂创建过程)

二、三种工厂模式的Java实现
1️⃣ 简单工厂模式(静态工厂)
场景:根据类型创建不同类型的对话框

java
// 1. 定义产品接口
interface Dialog {
void show();
}

// 2. 具体产品实现
class AlertDialog implements Dialog {
@Override
public void show() {
System.out.println("显示警告对话框");
}
}

class ProgressDialog implements Dialog {
@Override
public void show() {
System.out.println("显示进度条对话框");
}
}

// 3. 简单工厂
class DialogFactory {
public static Dialog createDialog(String type) {
if ("alert".equals(type)) {
return new AlertDialog();
} else if ("progress".equals(type)) {
return new ProgressDialog();
}
return null;
}
}

// 4. 使用示例
public class Main {
public static void main(String[] args) {
Dialog dialog = DialogFactory.createDialog("alert");
if (dialog != null) {
dialog.show(); // 输出: 显示警告对话框
}
}
}
特点:

通过静态方法创建对象

新增类型需要修改工厂类

2️⃣ 工厂方法模式
场景:跨平台按钮创建(Android/iOS)

java
// 1. 抽象产品
interface Button {
void render();
}

// 2. 具体产品
class AndroidButton implements Button {
@Override
public void render() {
System.out.println("渲染Android风格按钮");
}
}

class IOSButton implements Button {
@Override
public void render() {
System.out.println("渲染iOS风格按钮");
}
}

// 3. 抽象工厂
interface ButtonFactory {
Button createButton();
}

// 4. 具体工厂
class AndroidButtonFactory implements ButtonFactory {
@Override
public Button createButton() {
return new AndroidButton();
}
}

class IOSButtonFactory implements ButtonFactory {
@Override
public Button createButton() {
return new IOSButton();
}
}

// 5. 使用示例
public class Main {
public static void main(String[] args) {
ButtonFactory factory = new AndroidButtonFactory(); // 只需切换工厂
Button button = factory.createButton();
button.render(); // 输出: 渲染Android风格按钮
}
}
特点:

每个产品对应一个工厂类

符合开闭原则(扩展时不修改原有代码)

3️⃣ 抽象工厂模式
场景:创建整套UI控件(按钮+文本框)

java
// 1. 抽象产品族
interface TextBox {
void display();
}

interface Button {
void click();
}

// 2. Android实现
class AndroidTextBox implements TextBox {
@Override
public void display() {
System.out.println("Android文本框");
}
}

class AndroidButton implements Button {
@Override
public void click() {
System.out.println("Android按钮被点击");
}
}

// 3. iOS实现
class IOSTextBox implements TextBox {
@Override
public void display() {
System.out.println("iOS文本框");
}
}

class IOSButton implements Button {
@Override
public void click() {
System.out.println("iOS按钮被点击");
}
}

// 4. 抽象工厂
interface UIFactory {
TextBox createTextBox();
Button createButton();
}

// 5. 具体工厂
class AndroidUIFactory implements UIFactory {
@Override
public TextBox createTextBox() {
return new AndroidTextBox();
}

@Override
public Button createButton() {
return new AndroidButton();
}
}

class IOSUIFactory implements UIFactory {
@Override
public TextBox createTextBox() {
return new IOSTextBox();
}

@Override
public Button createButton() {
return new IOSButton();
}
}

// 6. 使用示例
public class Main {
public static void main(String[] args) {
UIFactory factory = new AndroidUIFactory(); // 切换为IOSUIFactory即可换整套UI

TextBox textBox = factory.createTextBox();
Button button = factory.createButton();

textBox.display(); // 输出: Android文本框
button.click(); // 输出: Android按钮被点击
}
}
特点:

生产相关联的产品族

保证产品兼容性

三、Android实际应用案例
案例1:BitmapFactory
java
// 使用工厂隐藏复杂的Bitmap解码过程
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.ic_launcher);
案例2:RecyclerView的ViewHolder创建
java
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
// 根据viewType返回不同的ViewHolder(工厂方法模式)
if (viewType == TYPE_HEADER) {
return new HeaderViewHolder(inflater.inflate(R.layout.header, parent, false));
} else {
return new ItemViewHolder(inflater.inflate(R.layout.item, parent, false));
}
}

LayoutInflater.from(context)

NotificationCompat.Builder

Retrofit.create()
也是用的工厂模式
四、工厂模式对比总结
模式 优点 缺点 适用场景
简单工厂 调用简单 违反开闭原则 对象类型较少且稳定
工厂方法 扩展性强 类数量增多 需要灵活扩展(如插件系统)
抽象工厂 保证产品兼容性 复杂度高 成套产品(如UI主题/跨平台)

···
AGP 版本 Gradle 版本 Kotlin 插件版本 说明
4.2.2 6.7.1 1.5.21(推荐) 最后一个支持 compile 的版本
7.0.2 7.0.2 1.5.31 Android Studio Arctic Fox
7.1.3 7.2 1.6.10 Android Studio Bumblebee
7.2.2 7.3 1.6.21 Android Studio Chipmunk
7.3.1 7.4 1.7.10 Android Studio Dolphin
7.4.2 7.5 1.8.10 Android Studio Electric Eel
8.0.2 8.0 1.8.21 Android Studio Flamingo
8.1.2 8.1 1.9.0 Android Studio Giraffe
8.2.2 8.2 1.9.10 Android Studio Hedgehog
8.3.1+ 8.4 2.0.0(兼容) Android Studio Iguana(K2 编译器)

✅ 示例配置(以 AGP 8.1.2 为例)
build.gradle(Project):

buildscript {
ext.kotlin_version = ‘1.9.0’
dependencies {
classpath ‘com.android.tools.build:gradle:8.1.2’
classpath “org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version”
}
}
gradle-wrapper.properties:

distributionUrl=https://services.gradle.org/distributions/gradle-8.1-bin.zip
distributionUrl=https://mirrors.cloud.tencent.com/gradle/gradle-8.11.1-bin.zip
✅ Kotlin DSL(build.gradle.kts)用户

plugins {
id(“com.android.application”) version “8.1.2” apply false
kotlin(“android”) version “1.9.0” apply false
}
⚠️ 注意事项
Kotlin 1.9+ 之后使用了新的 K2 编译器,速度提升但有兼容性变化。

从 AGP 8.0 开始必须使用命名空间(即 namespace = “xxx” 取代 manifest package)。

Kotlin 插件和 AGP 插件必须配套更新,否则构建会报错。

···

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
在 Android 开发中,内存泄漏是非常常见但又隐蔽的问题,严重时会导致 应用卡顿、OOM(内存溢出)或崩溃。 

✅ 一、LeakCanary(推荐,快速集成,自动分析)
🧪 LeakCanary 是什么?
LeakCanary 是 Square 出品的一个 Android 内存泄漏检测库,当你的 app 出现内存泄漏时,它会:

自动监听 Activity/Fragment 的销毁情况

如果内存泄漏,它会 弹出通知 + 详细引用链

🛠️ 集成方式(以 Java 项目为例)
在 build.gradle 中添加依赖:

dependencies {
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.12'
}
无需其他初始化,LeakCanary 会自动开始监控。

🧩 如何使用 LeakCanary 排查泄漏?
运行 App,进入可能内存泄漏的界面,然后退出该界面。

如果泄漏发生,会自动弹出如下通知:

Memory Leak Detected
com.xxx.MyActivity has leaked
点击通知,查看泄漏路径:

com.xxx.MyActivity → mContext → this$0 → mHandler → MessageQueue
按照引用链查看是哪个字段导致 Activity 没有被回收,例如:

静态变量持有 Activity

Handler 没有移除消息

自定义线程、定时器、RxJava 没有释放

🔍 常见内存泄漏原因
场景 原因
Handler 未 removeCallbacksAndMessages(null)
静态变量 持有 Context/Activity
单例 单例中持有非 Application 的 Context
资源未释放 Bitmap、Camera、MediaPlayer
自定义 View 监听器未解绑
RxJava 未正确 dispose()


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
<com.amap.api.maps.MapView
android:id="@+id/mapView"
android:layout_width="match_parent"
android:layout_height="match_parent"
/>


//地图显示start
private MapView mapView;
private AMap aMap;
private MyLocationStyle myLocationStyle;
private long lastSaveTime = 0;
private static final long SAVE_INTERVAL = 3000; // 每3秒最多写一次

private List<MapPoint> recordedPoints = new ArrayList<>();
private List<Marker> markers = new ArrayList<>();
private Polyline currentPolyline;

//地图显示end
// 初始化地图
mapView = findViewById(R.id.mapView);
mapView.onCreate(savedInstanceState);
if (aMap == null) {
aMap = mapView.getMap();
aMap.moveCamera(CameraUpdateFactory.zoomTo(15));
// 添加禁飞 区
// addNoFlyZones();
}
setupMap();

private void setupMap(){
myLocationStyle = new MyLocationStyle();//初始化定位蓝点样式类myLocationStyle.myLocationType(MyLocationStyle.LOCATION_TYPE_LOCATION_ROTATE);//连续定位、且将视角移动到地图中心点,定位点依照设备方向旋转,并且会跟随设备移动。(1秒1次定位)如果不设置myLocationType,默认也会执行此种模式。
myLocationStyle.interval(20000); //设置连续定位模式下的定位间隔,只在连续定位模式下生效,单次定位模式下不会生效。单位为毫秒。
BitmapDescriptor myIcon = BitmapDescriptorFactory.fromResource(R.mipmap.ic_fly_control);
myLocationStyle.myLocationIcon(myIcon);
aMap.setMyLocationStyle(myLocationStyle);//设置定位蓝点的Style
//aMap.getUiSettings().setMyLocationButtonEnabled(true);设置默认定位按钮是否显示,非必需设置。
aMap.setMyLocationEnabled(true);// 设置为true表示启动显示定位蓝点,false表示隐藏定位蓝点并不进行定位,默认是false。
// myLocationStyle.myLocationType(MyLocationStyle.LOCATION_TYPE_SHOW);//只定位一次。
// myLocationStyle.myLocationType(MyLocationStyle.LOCATION_TYPE_LOCATE) ;//定位一次,且将视角移动到地图中心点。
// myLocationStyle.myLocationType(MyLocationStyle.LOCATION_TYPE_FOLLOW) ;//连续定位、且将视角移动到地图中心点,定位蓝点跟随设备移动。(1秒1次定位)
// myLocationStyle.myLocationType(MyLocationStyle.LOCATION_TYPE_MAP_ROTATE);//连续定位、且将视角移动到地图中心点,地图依照设备方向旋转,定位点会跟随设备移动。(1秒1次定位)
// myLocationStyle.myLocationType(MyLocationStyle.LOCATION_TYPE_LOCATION_ROTATE);//连续定位、且将视角移动到地图中心点,定位点依照设备方向旋转,并且会跟随设备移动。(1秒1次定位)默认执行此种模式。
////以下三种模式从5.1.0版本开始提供
// myLocationStyle.myLocationType(MyLocationStyle.LOCATION_TYPE_LOCATION_ROTATE_NO_CENTER);//连续定位、蓝点不会移动到地图中心点,定位点依照设备方向旋转,并且蓝点会跟随设备移动。
// myLocationStyle.myLocationType(MyLocationStyle.LOCATION_TYPE_FOLLOW_NO_CENTER);//连续定位、蓝点不会移动到地图中心点,并且蓝点会跟随设备移动。
// myLocationStyle.myLocationType(MyLocationStyle.LOCATION_TYPE_MAP_ROTATE_NO_CENTER);//连续定位、蓝点不会移动到地图中心点,地图依照设备方向旋转,并且蓝点会跟随设备移动。




// 初始化定位客户端 也可以获取定位信息
// AMapLocationClient mLocationClient = null;
// try {
// mLocationClient = new AMapLocationClient(getApplicationContext());
// } catch (Exception e) {
// throw new RuntimeException(e);
// }
// AMapLocationClientOption mLocationOption = new AMapLocationClientOption();
// mLocationOption.setLocationMode(AMapLocationClientOption.AMapLocationMode.Hight_Accuracy);
// mLocationOption.setInterval(2000); // 定位间隔2秒
// mLocationClient.setLocationOption(mLocationOption);
//
// mLocationClient.setLocationListener(location -> {
// if (location != null) {
// if (location.getErrorCode() == 0) {
// // 定位成功,获取经纬度
// double latitude = location.getLatitude();
// double longitude = location.getLongitude();
// Log.d("定位", "定位成功,纬度:" + latitude + ", 经度:" + longitude);
//
// // 可更新地图中心点或UI
// runOnUiThread(() -> {
// // 比如移动摄像头到当前定位点
// aMap.animateCamera(CameraUpdateFactory.newLatLngZoom(
// new LatLng(latitude, longitude), 15));
// });
// } else {
// Log.e("定位", "定位失败,错误码:" + location.getErrorCode() +
// " 错误信息:" + location.getErrorInfo());
// }
// }
// });
// mLocationClient.startLocation();




aMap.setOnMyLocationChangeListener(new AMap.OnMyLocationChangeListener() {
@Override
public void onMyLocationChange(Location location) {
if (location != null) {
double latitude = location.getLatitude();
double longitude = location.getLongitude();

long now = System.currentTimeMillis();

// 3秒内只保存一次
if (now - lastSaveTime > SAVE_INTERVAL) {
MapPoint mapPoint = new MapPoint(latitude, longitude, "");
SPUtils.putObject(SPKeysConstance.MAP_POINT, mapPoint); // 建议内部用 apply()
lastSaveTime = now;
Log.d("定位变化", "存储纬度: " + latitude + ", 经度: " + longitude);
}

Log.d("定位变化", "纬度: " + latitude + ", 经度: " + longitude);

// 例如:更新 UI 或定位点
}
}
});

// 获取传递过来的 Bundle
Bundle bundle = getIntent().getExtras();
if (bundle != null) {
fullView.setVisibility(GONE);
FrameLayout.LayoutParams fullParams = new FrameLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
);
smallView.setLayoutParams(fullParams);

smallView.animate()
.scaleX(1f)
.scaleY(1f)
.translationX(0f)
.translationY(0f)
.setDuration(300)
.withStartAction(() -> {
fullView.setElevation(1f);
fullView.bringToFront();
})
.start();
String type = bundle.getString("type");
aMap.setOnMapClickListener(new AMap.OnMapClickListener() {
@Override
public void onMapClick(LatLng latLng) {
mapNewCavns(latLng);
}
});

}else{
aMap.setOnMapClickListener(new AMap.OnMapClickListener() {
@Override
public void onMapClick(LatLng latLng) {
addMarker(latLng);
}
});

}


UiSettings uiSettings = aMap.getUiSettings();
uiSettings.setZoomControlsEnabled(false); // 隐藏缩放按钮

coordinateConverter = new CoordinateConverter(this);
coordinateConverter.from(CoordinateConverter.CoordType.GPS);

}


// 在指定坐标添加标记点
private Marker addMarkerNew(AMap aMap, LatLng latLng, String title) {
// 数字 Marker(模拟 InfoWindow)
Bitmap bitmap = createNumberMarker(recordedPoints.size() + 1);
BitmapDescriptor numberIcon = BitmapDescriptorFactory.fromBitmap(bitmap);
aMap.addMarker(new MarkerOptions()
.position(new LatLng(latLng.latitude + 0.00008, latLng.longitude)) // 上方偏移一点
.icon(numberIcon)
.anchor(0.5f, 1f)
.zIndex(10)); // 保证浮在上层

return aMap.addMarker(new MarkerOptions()
.position(latLng)
.title(title)
.icon(BitmapDescriptorFactory.defaultMarker(BitmapDescriptorFactory.HUE_RED))
.draggable(true));// 确保标记点是可拖动的

}
// 连接多个点形成折线
private Polyline addPolylineNew(AMap aMap, List<LatLng> points) {
return aMap.addPolyline(new PolylineOptions()
.addAll(points)
.width(15) // 线宽
.color(Color.RED) // 醒目颜色
.zIndex(10)); // 置于上层
}
private void updatePolylineNew() {
// 如果有旧折线,先移除
if (currentPolyline != null) {
currentPolyline.remove();
}

// 如果有至少2个点,绘制连线
if (recordedPoints.size() >= 2) {
List<LatLng> points = new ArrayList<>();
for (MapPoint point : recordedPoints) {
points.add(new LatLng(point.getLatitude(), point.getLongitude()));
}
currentPolyline = addPolylineNew(aMap, points);
}
}

private void clearAllMarkers() {
// 清除所有标记
for (Marker marker : markers) {
marker.remove();
}
markers.clear();

// 清除折线
if (currentPolyline != null) {
currentPolyline.remove();
currentPolyline = null;
}

// 清空记录
recordedPoints.clear();
}



@Override
protected void onResume() {
super.onResume();

if(mapView != null){
mapView.onResume();
}

}

@Override
protected void onPause() {
super.onPause();

mapView.onPause();
}

@Override
protected void onDestroy() {
super.onDestroy();

mapView.onDestroy();
}


高德地图画矩形:
center:正方形中心点,类型是 LatLng(经纬度);

size:正方形边长(单位:米);
private void createSquare(LatLng center, double size) {
double dLat = GeometryUtils.meterToLat(size / 2);
double dLng = GeometryUtils.meterToLng(size / 2, center.latitude);

LatLng p1 = new LatLng(center.latitude + dLat, center.longitude - dLng);
LatLng p2 = new LatLng(center.latitude + dLat, center.longitude + dLng);
LatLng p3 = new LatLng(center.latitude - dLat, center.longitude + dLng);
LatLng p4 = new LatLng(center.latitude - dLat, center.longitude - dLng);

List<LatLng> points = Arrays.asList(p1, p2, p3, p4);
square = aMap.addPolygon(new PolygonOptions()
.addAll(points)
.strokeColor(Color.BLUE)
.fillColor(0x220000FF)
.strokeWidth(4));
}
获取两点间的距离:
public class GeometryUtils {
private static final double R = 6378137; // 地球半径
/**
* 将“米”转换为“纬度差”(1纬度 ≈ 111km)
* @param meter 距离(米)
* @return 纬度差值
*/
public static double meterToLat(double meter) {
return (meter / R) * (180 / PI);
}
/**
* 将“米”转换为“经度差”(因纬度不同,1经度代表的实际米数不同)
* @param meter 距离(米)
* @param lat 当前纬度
* @return 经度差值
*/
public static double meterToLng(double meter, double lat) {
return (meter / (R * cos(toRadians(lat)))) * (180 / PI);
}
/**
* 计算一组“航线段”的总长度
* 假设传入的点序列是成对出现的,每两个点组成一条线段:p0→p1、p2→p3、...
* @param points 航线点列表(经纬度)
* @return 所有线段的总长度(单位:米)
*/
public static double calculateLength(java.util.List<LatLng> points) {
double sum = 0;
for (int i = 1; i < points.size(); i += 2) {
sum += distance(points.get(i - 1), points.get(i));
}
return sum;
}
/**
* 计算两个坐标点之间的球面距离(单位:米)
* 使用 Haversine 公式,适合短距离精确计算
* @param p1 起点(纬度经度)
* @param p2 终点(纬度经度)
* @return 球面两点距离(米)
*/
public static double distance(LatLng p1, LatLng p2) {
double dLat = toRadians(p2.latitude - p1.latitude);
double dLng = toRadians(p2.longitude - p1.longitude);
double a = sin(dLat / 2) * sin(dLat / 2) + cos(toRadians(p1.latitude)) * cos(toRadians(p2.latitude)) * sin(dLng / 2) * sin(dLng / 2);
double c = 2 * atan2(sqrt(a), sqrt(1 - a));
return R * c;
}
}