0%

import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;

import androidx.annotation.Nullable;
import androidx.core.content.FileProvider;
import androidx.fragment.app.DialogFragment;

import com.htnova.fly.R;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;

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
public class HtUpdateDialogFragment extends DialogFragment {

private ProgressBar progressBar;
private TextView tvProgress;
private Button btnUpdate, btnSkip, btnCancel;

private volatile boolean isCancelled = false;
private final String apkUrl = "http://115.190.154.26/app-release.apk";
private final int serverVersionCode = 2;
private final int currentVersionCode;

public HtUpdateDialogFragment(Context context) {
currentVersionCode = getVersionCode(context);
}

@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.dialog_update, container, false);
initView(view);
return view;
}

private void initView(View view) {
progressBar = view.findViewById(R.id.progress_bar);
tvProgress = view.findViewById(R.id.tv_progress);
btnUpdate = view.findViewById(R.id.btn_update);
btnSkip = view.findViewById(R.id.btn_skip);
btnCancel = view.findViewById(R.id.btn_cancel);

TextView tvInfo = view.findViewById(R.id.tv_update_info);
tvInfo.setText("当前版本:" + currentVersionCode + "\n服务器版本:" + serverVersionCode +
"\n\n更新内容:\n- 修复问题\n- 优化性能");

btnUpdate.setOnClickListener(v -> {
btnUpdate.setVisibility(View.GONE);
btnSkip.setVisibility(View.GONE);
btnCancel.setVisibility(View.VISIBLE);
progressBar.setVisibility(View.VISIBLE);
tvProgress.setVisibility(View.VISIBLE);
startDownload();
});

btnSkip.setOnClickListener(v -> dismiss());

btnCancel.setOnClickListener(v -> {
isCancelled = true;
Toast.makeText(getContext(), "已取消下载", Toast.LENGTH_SHORT).show();
dismiss();
});
}

private void startDownload() {
new Thread(() -> {
HttpURLConnection conn = null;
InputStream is = null;
BufferedOutputStream bos = null;

try {
URL url = new URL(apkUrl);
conn = (HttpURLConnection) url.openConnection();
conn.setConnectTimeout(10000);
conn.setReadTimeout(10000);
conn.connect();

int length = conn.getContentLength();
if (length <= 0) {
showToast("服务器未返回文件大小");
return;
}

is = new BufferedInputStream(conn.getInputStream());
File apkFile = new File(requireContext().getExternalFilesDir(null), "update.apk");
bos = new BufferedOutputStream(new FileOutputStream(apkFile));

byte[] buffer = new byte[8192]; // 8KB缓冲区
int count = 0;
int bytesRead;
int lastProgress = 0;

while ((bytesRead = is.read(buffer)) != -1 && !isCancelled) {
bos.write(buffer, 0, bytesRead);
count += bytesRead;
int progress = (int) (count * 100L / length);

if (progress != lastProgress) {
int finalProgress = progress;
requireActivity().runOnUiThread(() -> {
progressBar.setProgress(finalProgress);
tvProgress.setText("下载进度:" + finalProgress + "%");
});
lastProgress = progress;
}
}

bos.flush();

if (!isCancelled) {
requireActivity().runOnUiThread(() -> {
Toast.makeText(getContext(), "下载完成,准备安装", Toast.LENGTH_SHORT).show();
installApk(apkFile);
dismiss();
});
}

} catch (Exception e) {
Log.e("HtUpdateDialog", "下载出错:" + e.getMessage(), e);
showToast("下载失败:" + e.getMessage());
} finally {
try {
if (is != null) is.close();
if (bos != null) bos.close();
if (conn != null) conn.disconnect();
} catch (Exception ignored) {
}
}
}).start();
}

private void installApk(File apkFile) {
Context context = requireContext();
Uri apkUri;
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
apkUri = FileProvider.getUriForFile(context, context.getPackageName() + ".fileprovider", apkFile);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
} else {
apkUri = Uri.fromFile(apkFile);
}

intent.setDataAndType(apkUri, "application/vnd.android.package-archive");
context.startActivity(intent);
}

private int getVersionCode(Context context) {
try {
return context.getPackageManager().getPackageInfo(context.getPackageName(), 0).versionCode;
} catch (PackageManager.NameNotFoundException e) {
return 0;
}
}

private void showToast(String msg) {
requireActivity().runOnUiThread(() ->
Toast.makeText(getContext(), msg, Toast.LENGTH_LONG).show());
}
}

R.layout.dialog_update

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
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:padding="20dp"
android:layout_width="match_parent"
android:layout_height="wrap_content">

<TextView
android:id="@+id/tv_update_info"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="发现新版本..."
android:textSize="16sp" />

<ProgressBar
android:id="@+id/progress_bar"
style="@android:style/Widget.ProgressBar.Horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:max="100"
android:visibility="gone" />

<TextView
android:id="@+id/tv_progress"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="下载进度:0%"
android:visibility="gone"
android:paddingTop="10dp" />

<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="20dp">

<Button
android:id="@+id/btn_update"
android:layout_weight="1"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="立即更新" />

<Button
android:id="@+id/btn_skip"
android:layout_weight="1"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="跳过" />

<Button
android:id="@+id/btn_cancel"
android:layout_weight="1"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="取消下载"
android:visibility="gone"/>
</LinearLayout>
</LinearLayout>

application中添加

1
2
3
4
5
6
7
8
9
10
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>

xml文件夹下新建file_paths.xml

1
2
3
4
5
6
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<external-files-path
name="update_apk"
path="." />
</paths>

使用new HtUpdateDialogFragment(this).show(getSupportFragmentManager(), “update_dialog”);

LiveData 是 ViewModel 用来“安全地通知 UI 层更新”的方式。

  1. ViewModel 中定义与更新 LiveData

public class MyViewModel extends ViewModel {
private final MutableLiveData messageLiveData = new MutableLiveData<>();

public LiveData<String> getMessageLiveData() {
    return messageLiveData;
}

public void updateMessage(String msg) {
    messageLiveData.setValue(msg);  // 主线程使用 setValue()
    // messageLiveData.postValue(msg); // 子线程也可以用 postValue()
}

}
2. Activity/Fragment 中观察 LiveData

MyViewModel viewModel = new ViewModelProvider(this).get(MyViewModel.class);

viewModel.getMessageLiveData().observe(this, message -> {
textView.setText(message); // UI 自动刷新
});
3. LiveData 常用操作
方法 说明
setValue() 主线程更新数据
postValue() 子线程更新数据
observe() 观察数据变化(生命周期感知)
observeForever() 永久观察(需手动移除,慎用)

🔁 和 RxJava 比较
特性 LiveData RxJava
生命周期感知 ✅ 内建 ❌ 需手动配合如 RxLifecycle
学习成本 ✅ 简单 ❌ 复杂,操作符多
功能丰富 ❌ 仅支持数据通知 ✅ 支持变换、合并、背压等复杂操作
UI 通知 ✅ 主线程安全 ✅ 手动指定线程

推荐组合用法:业务逻辑用 RxJava,最终结果放入 LiveData 中供 UI 层观察。

🧱 高阶用法

  1. MediatorLiveData
    观察多个 LiveData 源,合并逻辑:

MediatorLiveData result = new MediatorLiveData<>();
result.addSource(liveData1, value -> {
result.setValue(“来自 liveData1: “ + value);
});
result.addSource(liveData2, value -> {
result.setValue(“来自 liveData2: “ + value);
});
2. SingleLiveEvent(防止重复触发)
防止如“跳转页面”这种事件被重复触发,常自定义如下:

public class SingleLiveEvent extends MutableLiveData {
private final AtomicBoolean pending = new AtomicBoolean(false);

@Override
public void observe(@NonNull LifecycleOwner owner, @NonNull Observer<? super T> observer) {
    super.observe(owner, t -> {
        if (pending.compareAndSet(true, false)) {
            observer.onChanged(t);
        }
    });
}

@MainThread
public void setValue(T t) {
    pending.set(true);
    super.setValue(t);
}

}
🧭 总结:LiveData 用在哪里?
场景 是否推荐用 LiveData
后台网络请求结果通知 UI ✅ 推荐
定时更新状态(配合 RxJava) ✅ 推荐
复杂数据链式变换 ❌ 用 RxJava 更合适
临时事件(弹窗、跳转) ✅ 用 SingleLiveEvent

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
支持对象存储(自动 JSON 序列化和反序列化)的 Android SPUtils 工具类,使用 Gson 实现对象与字符串的互转。
✅ Gson 依赖(如果尚未添加)
在 build.gradle 中加入:

implementation 'com.google.code.gson:gson:2.10.1'
✅ 支持对象的 SPUtils 工具类

package com.htnova.fly.util;

import android.content.Context;
import android.content.SharedPreferences;

import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;

import java.lang.reflect.Type;

public class SPUtils {
private static final String SP_NAME = "AppCache";
private static SharedPreferences sp;
private static Gson gson = new Gson();

private SPUtils() {}

public static void init(Context context) {
if (sp == null) {
sp = context.getApplicationContext().getSharedPreferences(SP_NAME, Context.MODE_PRIVATE);
}
}

// 基础类型
public static void putString(String key, String value) {
sp.edit().putString(key, value).apply();
}

public static String getString(String key, String defValue) {
return sp.getString(key, defValue);
}

public static void putBoolean(String key, boolean value) {
sp.edit().putBoolean(key, value).apply();
}

public static boolean getBoolean(String key, boolean defValue) {
return sp.getBoolean(key, defValue);
}

public static void putInt(String key, int value) {
sp.edit().putInt(key, value).apply();
}

public static int getInt(String key, int defValue) {
return sp.getInt(key, defValue);
}

public static void putLong(String key, long value) {
sp.edit().putLong(key, value).apply();
}

public static long getLong(String key, long defValue) {
return sp.getLong(key, defValue);
}

public static void putFloat(String key, float value) {
sp.edit().putFloat(key, value).apply();
}

public static float getFloat(String key, float defValue) {
return sp.getFloat(key, defValue);
}

// 存储任意对象
public static <T> void putObject(String key, T obj) {
String json = gson.toJson(obj);
sp.edit().putString(key, json).apply();
}

// 获取对象
public static <T> T getObject(String key, Class<T> clazz) {
String json = sp.getString(key, null);
return json != null ? gson.fromJson(json, clazz) : null;
}

// 获取集合或泛型对象(如 List<User>)
public static <T> T getObject(String key, Type typeOfT) {
String json = sp.getString(key, null);
return json != null ? gson.fromJson(json, typeOfT) : null;
}

public static void remove(String key) {
sp.edit().remove(key).apply();
}

public static void clear() {
sp.edit().clear().apply();
}

public static boolean contains(String key) {
return sp.contains(key);
}
}
✅ 示例用法
1. 存储/读取单个对象:

User user = new User("Tom", 25);
SPUtils.putObject("user", user);

User savedUser = SPUtils.getObject("user", User.class);
2. 存储/读取 List 对象:

List<User> userList = new ArrayList<>();
userList.add(new User("Tom", 25));
userList.add(new User("Jerry", 26));

SPUtils.putObject("user_list", userList);

// 获取时需要指定 Type
Type type = new TypeToken<List<User>>() {}.getType();
List<User> savedList = SPUtils.getObject("user_list", type);
✅ 示例数据类(User)

public class User {
private String name;
private int age;

public User() {}

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

// Getter / Setter 省略
}

Retrofit:网络请求框架

OkHttp3:Retrofit 底层网络请求库

RxJava2:响应式编程,处理异步流

MVVM 架构:解耦视图和数据逻辑

ViewModel + LiveData:生命周期感知组件

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

🔧 一、依赖配置(build.gradle)

// Retrofit 核心
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'

// OkHttp3 + 日志拦截器
implementation 'com.squareup.okhttp3:okhttp:4.12.0'
implementation 'com.squareup.okhttp3:logging-interceptor:4.12.0'

// RxJava2 + Retrofit 支持
implementation 'io.reactivex.rxjava2:rxjava:2.2.21'
implementation 'io.reactivex.rxjava2:rxandroid:2.1.1'
implementation 'com.squareup.retrofit2:adapter-rxjava2:2.9.0'

// MVVM 架构核心
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
implementation 'androidx.lifecycle:lifecycle-viewmodel:2.6.2'
🧱 二、Retrofit 初始化封装(ApiClient.java)


import android.util.Log;

import okhttp3.OkHttpClient;
import okhttp3.logging.HttpLoggingInterceptor;
import retrofit2.Retrofit;
import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory;
import retrofit2.converter.gson.GsonConverterFactory;

/**
* @author xqm
* @date 2025/7/12 16:20
* @description https://jsonplaceholder.typicode.com/
* retrofit+okhttp
*/
public class ApiClient {
private static final String BASE_URL = "https://xxxxx/";
private static Retrofit retrofit;

public static Retrofit getInstance() {
if (retrofit == null) {
// 添加日志拦截器
HttpLoggingInterceptor logging = new HttpLoggingInterceptor();
// HttpLoggingInterceptor logging = new HttpLoggingInterceptor(
// message -> Log.e("xqm", message) // 你可以自定义日志输出位置
// );
logging.setLevel(HttpLoggingInterceptor.Level.BODY);

logging.setLevel(HttpLoggingInterceptor.Level.BODY); // 打印完整请求和响应

OkHttpClient client = new OkHttpClient.Builder()
.addInterceptor(new HeaderInterceptor())
.addInterceptor(logging)
.build();

retrofit = new Retrofit.Builder()
.baseUrl(BASE_URL)
.client(client)
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJava2CallAdapterFactory.create()) // Rx 支持
.build();
}
return retrofit;
}
}

🔐 三、自定义请求头拦截器(HeaderInterceptor.java)


import java.io.IOException;

import okhttp3.Interceptor;
import okhttp3.Request;
import okhttp3.Response;

/**
* @author xqm
* @date 2025/7/12 16:21
* @description 用于自动在所有请求中附加 token 认证信息,可在登录成功后设置。
*/
public class HeaderInterceptor implements Interceptor {
private static String token = "";

public static void setToken(String t) {
token = t;
}

@Override
public Response intercept(Interceptor.Chain chain) throws IOException {
Request request = chain.request();
Request.Builder builder = request.newBuilder()
.addHeader("Content-Type", "application/json")
.addHeader("Authorization", token.isEmpty() ? "" : "Bearer " + token);

return chain.proceed(builder.build());
}
}



🔌 四、API 接口定义(ApiService.java)





import io.reactivex.Observable;
import retrofit2.http.Body;
import retrofit2.http.GET;
import retrofit2.http.POST;
import retrofit2.http.Query;

/**
* @author xqm
* @date 2025/7/12 16:20
* @description ApiService 接口定义
* 支持 GET 和 POST 请求
* 支持 @Query 参数和 @Body 参数
* 使用 RxJava 返回 Observable<T>
*/
public interface ApiService {

@GET("todos/1")
Observable<UserModel> getUserInfo();

@GET("access/auth/login")
Observable<UserModel> getlogin(@Query("username") String username, @Query("password") String password);

@POST("user/login")
Observable<LoginResponse> login(@Body LoginRequest request);
}



📦 五、数据访问封装(UserRepository.java)


import io.reactivex.Observable;

/**
* @author xqm
* @date 2025/7/12 16:21
* @description 将数据访问封装在 Repository 层,如网络请求,本地数据库访问等
*/
public class UserRepository {

private final ApiService api;

public UserRepository() {
api = ApiClient.getInstance().create(ApiService.class);
}

public Observable<LoginResponse> login(String username, String password) {
return api.login(new LoginRequest(username, password));
}

public Observable<UserModel> getUserInfo() {
return api.getUserInfo();
}

public Observable<UserModel> getLogin(String userName,String pwd) {
return api.getlogin(userName,pwd);
}
}



🧠 六、ViewModel 逻辑层(UserViewModel.java)



import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable;
import io.reactivex.schedulers.Schedulers;


/**
* @author xqm
* @date 2025/7/12 16:22
* @description 调用 Repository 提供的数据接口,处理线程调度和生命周期管理。
*/
public class UserViewModel extends BaseViewModel {
private final UserRepository repository = new UserRepository();

public void login(String username, String password, RxObserver<LoginResponse> observer) {
Disposable disposable = repository.login(username, password)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribeWith(observer);
addDisposable(disposable);
}

public void getUserInfo(RxObserver<UserModel> observer) {
Disposable disposable = repository.getUserInfo()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribeWith(observer);
addDisposable(disposable);
}

public void getLogin(String userName,String pwd,RxObserver<UserModel> observer) {
Disposable disposable = repository.getLogin(userName,pwd)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribeWith(observer);
addDisposable(disposable);
}
}



🧼 七、BaseViewModel(统一管理 Disposable)


/**
* @author xqm
* @date 2025/7/12 16:22
* @description 避免内存泄漏,销毁 ViewModel 时自动清理订阅。
*/
public abstract class BaseViewModel extends ViewModel {
protected CompositeDisposable compositeDisposable = new CompositeDisposable();

protected void addDisposable(Disposable d) {
compositeDisposable.add(d);
}

@Override
protected void onCleared() {
compositeDisposable.clear();
super.onCleared();
}
}


🧪 八、自定义 Observer 示例(RxObserver)


/**
* @author xqm
* @date 2025/7/12 16:43
* @description 可自我管理订阅生命周期的 Observer,既能收数据,又能自己取消订阅
*/
import io.reactivex.observers.DisposableObserver;

public abstract class RxObserver<T> extends DisposableObserver<T> {
@Override
public void onNext(T t) {
onSuccess(t);
}

@Override
public void onError(Throwable e) {
onFailure(e);
}

@Override
public void onComplete() {}

public void onStart() {}

public abstract void onSuccess(T t);
public abstract void onFailure(Throwable e);
}


可以在回调里更新 UI,比如:
viewModel = new ViewModelProvider(this).get(UserViewModel.class);
viewModel.login(user, pwd, new RxObserver<LoginResponse>() {
@Override
public void onNext(LoginResponse response) {
if (response.getCode() == 200) {
// 登录成功
HeaderInterceptor.setToken(response.getToken());
Toast.makeText(context, "登录成功", Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(context, "登录失败:" + response.getMsg(), Toast.LENGTH_SHORT).show();
}
}

@Override
public void onError(Throwable e) {
Toast.makeText(context, "请求出错: " + e.getMessage(), Toast.LENGTH_SHORT).show();
}
});

✅ 总结
层级 类名 作用
网络配置 ApiClient Retrofit 初始化,添加拦截器
拦截器 HeaderInterceptor 动态添加统一请求头
API 接口 ApiService 定义 Retrofit 请求
数据层 UserRepository 请求封装、解耦逻辑
ViewModel UserViewModel 调用 Repository 并分发到界面
Observer RxObserver 自定义网络回调处理逻辑
📁 项目目录结构

├── activity/ # UI 层 Activity 或 Fragment
├── model/ # 数据模型类(Bean)
├── network/ # 网络核心配置(Retrofit + 拦截器)
│ ├── ApiClient.java
│ ├── ApiService.java
│ ├── HeaderInterceptor.java
│ └── RxObserver.java
├── repository/ # 数据仓库层
│ └── UserRepository.java
├── viewmodel/ # ViewModel 层
│ ├── BaseViewModel.java
│ └── UserViewModel.java

在 Android 中使用 MQTT(Message Queuing Telemetry Transport) 与服务器交互,是实现物联网、实时通信等场景的常用方式

一、准备工作
在 build.gradle(:app) 中添加:
implementation ‘org.eclipse.paho:org.eclipse.paho.client.mqttv3:1.2.5’
implementation ‘org.eclipse.paho:org.eclipse.paho.android.service:1.1.1’

二、配置权限
在 AndroidManifest.xml 添加:

三、初始化 MQTT 客户端

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

import org.eclipse.paho.android.service.MqttAndroidClient;
import org.eclipse.paho.client.mqttv3.IMqttActionListener;
import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken;
import org.eclipse.paho.client.mqttv3.IMqttToken;
import org.eclipse.paho.client.mqttv3.MqttCallback;
import org.eclipse.paho.client.mqttv3.MqttConnectOptions;
import org.eclipse.paho.client.mqttv3.MqttException;
import org.eclipse.paho.client.mqttv3.MqttMessage;

import java.nio.charset.StandardCharsets;

/**

  • @author xqm

  • @date 2025/7/18 17:01

  • @description MqttManager 类功能说明
    */
    public class MqttManager {
    private static final String TAG = “MqttManager”;
    private static MqttManager instance;
    private final MqttAndroidClient mqttAndroidClient;
    private final MqttConnectOptions connectOptions;

    private String serverUri = “tcp://broker.hivemq.com:1883”; // 可替换为你自己的服务器
    private final String clientId = “AndroidClient_” + System.currentTimeMillis();
    private final Context context;

    private OnMessageReceiveListener onMessageReceiveListener;

    public MqttManager(Context context,String serverUriAdd,String clientIdAdd,String userName,String pwd) {
    this.context = context.getApplicationContext();
    mqttAndroidClient = new MqttAndroidClient(context, serverUriAdd, clientIdAdd);
    connectOptions = new MqttConnectOptions();
    connectOptions.setUserName(userName);
    connectOptions.setPassword(pwd.toCharArray());
    connectOptions.setCleanSession(true);
    connectOptions.setAutomaticReconnect(false);

    initCallback();
    }

    // 单例获取方法
    public static synchronized MqttManager getInstance(Context context, String serverUriAdd, String clientIdAdd, String userName, String pwd) {
    if (instance == null) {
    instance = new MqttManager(context, serverUriAdd, clientIdAdd, userName, pwd);
    }
    return instance;
    }

    // 可选:无参数获取(需提前初始化)
    public static synchronized MqttManager getInstance() {
    if (instance == null) {
    throw new IllegalStateException(“MqttManager 尚未初始化!”);
    }
    return instance;
    }

    public boolean isConnected() {
    return mqttAndroidClient != null && mqttAndroidClient.isConnected();
    }

    private void initCallback() {
    mqttAndroidClient.setCallback(new MqttCallback() {
    @Override
    public void connectionLost(Throwable cause) {
    Log.e(TAG, “连接丢失,准备重连”, cause);
    reconnectWithDelay();
    }

    @Override
    public void messageArrived(String topic, MqttMessage message) {
    String payload = new String(message.getPayload(), StandardCharsets.UTF_8);
    Log.d(TAG, “收到消息: topic=” + topic + “, message=” + payload);
    if (onMessageReceiveListener != null) {
    onMessageReceiveListener.onMessage(topic, payload);
    }
    }

    @Override
    public void deliveryComplete(IMqttDeliveryToken token) {
    Log.d(TAG, “消息发送完成”);
    }
    });
    }

    public void connect() {
    try {
    mqttAndroidClient.connect(connectOptions, null, new IMqttActionListener() {
    @Override
    public void onSuccess(IMqttToken asyncActionToken) {
    Log.d(TAG, “连接成功”);
    }

    @Override
    public void onFailure(IMqttToken asyncActionToken, Throwable exception) {
    Log.e(TAG, “连接失败”, exception);
    reconnectWithDelay();
    }
    });
    } catch (MqttException e) {
    Log.e(TAG, “连接异常”, e);
    }
    }

    private void reconnectWithDelay() {
    new Handler(Looper.getMainLooper()).postDelayed(this::connect, 3000);
    }

    public void subscribe(String topic) {
    try {
    mqttAndroidClient.subscribe(topic, 1, null, new IMqttActionListener() {
    @Override
    public void onSuccess(IMqttToken asyncActionToken) {
    Log.d(TAG, “订阅成功: “ + topic);
    }

    @Override
    public void onFailure(IMqttToken asyncActionToken, Throwable exception) {
    Log.e(TAG, “订阅失败: “ + topic, exception);
    }
    });
    } catch (MqttException e) {
    Log.e(TAG, “订阅异常”, e);
    }
    }

    public void publish(String topic, String message) {
    try {
    MqttMessage mqttMessage = new MqttMessage();
    mqttMessage.setPayload(message.getBytes(StandardCharsets.UTF_8));
    mqttAndroidClient.publish(topic, mqttMessage);
    Log.d(TAG, “发送消息: topic=” + topic + “, message=” + message);
    } catch (MqttException e) {
    Log.e(TAG, “发送失败”, e);
    }
    }

    public void disconnect() {
    try {
    mqttAndroidClient.disconnect();
    Log.d(TAG, “断开连接”);
    } catch (MqttException e) {
    Log.e(TAG, “断开异常”, e);
    }
    }

    public void setOnMessageReceiveListener(OnMessageReceiveListener listener) {
    this.onMessageReceiveListener = listener;
    }

    public interface OnMessageReceiveListener {
    void onMessage(String topic, String message);
    }

}

四、在 Activity 中使用
mqttManager = MqttManager.getInstance(getApplicationContext(), mqttUrl, clientId, userName, pwd);
mqttManager.setOnMessageReceiveListener((topic, message) -> {
runOnUiThread(() -> {
Toast.makeText(this, “收到: “ + message, Toast.LENGTH_SHORT).show();
});
});
mqttManager.connect();
mqttManager.subscribe(“your/topic”);

订阅多个 Topic(推荐做法)
mqttManager.subscribe(“drone/telemetry”); // 无人机遥测数据
mqttManager.subscribe(“drone/status”); // 状态变化
mqttManager.subscribe(“drone/camera”); // 摄像头控制消息
mqttManager.subscribe(“drone/command”); // 控制指令

也可以封装成数组遍历订阅:
String[] topics = {
“drone/telemetry”,
“drone/status”,
“drone/camera”,
“drone/command”
};

for (String topic : topics) {
mqttManager.subscribe(topic);
}
messageArrived 回调中区分 Topic 内容
mqttManager.setOnMessageReceiveListener((topic, message) -> {
switch (topic) {
case “drone/telemetry”:
// 解析遥测 JSON
break;
case “drone/status”:
// 更新状态 UI
break;
case “drone/command”:
// 响应控制指令
break;
default:
Log.w(“MQTT”, “未知 topic:” + topic);
break;
}
});

扩展建议
如你有“Topic + 消息体 JSON”的组合格式,可以创建实体类并使用 Gson 解析:
TelemetryData data = new Gson().fromJson(message, TelemetryData.class);

五、服务端推荐 MQTT Broker
免费测试:
HiveMQ: tcp://broker.hivemq.com:1883
Eclipse: tcp://iot.eclipse.org:1883
自建服务器推荐:
EMQX
Mosquitto

六、注意事项
保持后台连接:可以考虑 MQTT 放入前台服务中,保证连接不断。
网络断开重连处理:建议实现 connectionLost() 中的自动重连机制。
安全连接:生产环境使用 ssl:// + 用户认证更安全。

🧱 第一步:创建一个公共数据类(单例)

public class GlobalData extends ViewModel {
private static GlobalData instance;

private final MutableLiveData<String> flightStatus = new MutableLiveData<>();
private final MutableLiveData<Boolean> isConnected = new MutableLiveData<>();

public static GlobalData getInstance() {
    if (instance == null) {
        instance = new GlobalData();
    }
    return instance;
}

public MutableLiveData<String> getFlightStatus() {
    return flightStatus;
}

public MutableLiveData<Boolean> getIsConnected() {
    return isConnected;
}

}
🧱 第二步:页面中监听数据变化(Activity 或 Fragment)
在你的 Activity 中:

GlobalData globalData = GlobalData.getInstance();

globalData.getFlightStatus().observe(this, status -> {
flightStatusTextView.setText(status);
});

globalData.getIsConnected().observe(this, isConnected -> {
connectIcon.setVisibility(isConnected ? View.VISIBLE : View.GONE);
});
✅ 第三步:在任何地方修改数据,全局生效

GlobalData.getInstance().getFlightStatus().postValue(“起飞中”);
GlobalData.getInstance().getIsConnected().postValue(true);

🔄 如果希望多个 Activity 共享 ViewModel 数据?
建议用 ViewModelProvider 配合 Application:

public class MyApp extends Application {
private GlobalData globalData = new GlobalData();

public GlobalData getGlobalData() {
    return globalData;
}

}
然后在任意页面中用:

GlobalData globalData = ((MyApp) getApplication()).getGlobalData();

安卓打包时包含多个 ABI(CPU 架构) 的确会显著增大 APK 或 AAB 包体积。常见 ABI 有:

ABI 说明
armeabi-v7a 主流 32 位 ARM 设备(绝大多数 Android 手机)
arm64-v8a 主流 64 位 ARM 设备(目前主流)
x86 模拟器用(或极少量 Intel 手机)
x86_64 64 位模拟器(或极少量 Intel 平板)

✅ 原因:每个 ABI 会编译一份 .so 文件(Native 库),多架构直接打包 → 体积翻倍甚至更多。

只打主流架构(推荐 armeabi-v7a 和 arm64-v8a)
✅ 修改 build.gradle(模块级):
gradle
android {

defaultConfig {
    ...
    ndk {
        abiFilters 'armeabi-v7a', 'arm64-v8a'  // ← 只保留主流架构
    }
}

}

手动拆包(生成多个 APK)
gradle
android {

splits {
    abi {
        enable true
        reset()
        include 'armeabi-v7a', 'arm64-v8a'
        universalApk false // 是否生成包含所有 ABI 的 APK(默认 false)
    }
}

}
然后执行:

./gradlew assembleRelease
输出路径:app/build/outputs/apk/,每个架构生成一个独立 APK。

发布目标 推荐 ABI 设置
面向国内市场 armeabi-v7a, arm64-v8a
面向全球(含模拟器测试) 加上 x86, x86_64,但只用于测试或调试包
仅供线上发布(小体积) arm64-v8a(极致精简)
上传 Google Play 使用 .aab,让 Play 控制分发

手动安装最新版 apktool
下载最新版 apktool.jar 和 apktool 脚本:

https://github.com/iBotPeaches/Apktool/releases

假设你下载的是 apktool_2.9.3.jar,执行:

mv ~/Downloads/apktool_2.9.3.jar /usr/local/bin/apktool.jar
wget https://raw.githubusercontent.com/iBotPeaches/Apktool/master/scripts/osx/apktool
chmod +x apktool
sudo mv apktool /usr/local/bin/
确认版本:

apktool –version
✅ 再次执行你的命令:

apktool d /Users/xueqiaoming/allutils/utils/安卓反编译/apks/flight_2.6.14.apk -o ~/Desktop/ddd

你的系统没有安装 wget 工具,可以使用 curl 代替,或者先安装 wget。

✅ 方法一:用 curl 下载 apktool 脚本
在终端执行以下命令:

curl -o apktool https://raw.githubusercontent.com/iBotPeaches/Apktool/master/scripts/osx/apktool
chmod +x apktool
✅ 方法二:安装 wget 再执行原命令
如果你更喜欢使用 wget,先安装它:

brew install wget
然后再执行:

wget https://raw.githubusercontent.com/iBotPeaches/Apktool/master/scripts/osx/apktool
chmod +x apktool
安装完后,建议将 apktool 移动到全局目录:

sudo mv apktool /usr/local/bin/
之后就可以在任何目录下使用 apktool 命令了。

下载:

jadx-1.5.2.zip

🔗 官方下载地址(GitHub):
https://github.com/skylot/jadx/releases/tag/v1.5.2

  1. 编辑你的 shell 配置文件(macOS 默认 zsh)

nano ~/.zshrc
2. 在文件末尾添加这一行:

export PATH=”/Users/xueqiaoming/Downloads/jadx-1.5.2/bin:$PATH”
(这里假设 jadx 可执行文件在 bin 目录下)

  1. 保存并退出(Ctrl+O 回车保存,Ctrl+X 退出)
  2. 立即生效:

source ~/.zshrc
5. 测试

jadx-gui
或者

如果弹出 GUI 或显示帮助信息,说明配置成功!

如果你的 jadx 可执行文件不在 bin 文件夹,直接写可执行文件所在目录即可,比如:
编辑
export PATH=”/Users/xueqiaoming/Downloads/jadx-1.5.2:$PATH”

如果打不开
jadx-gui本质上是一个脚本,它最终会调用java来运行一个.jar文件。
windows 上 直接点击 jadx-gui.bat
mac上 进入bin目录,运行./jadx-gui

先创建本地仓库,再添加代码到仓库,然后提交上传到远程仓库。 http or ssh
https://blog.csdn.net/cy123cy456cy/article/details/134376003
https://blog.csdn.net/yzwfeng/article/details/123752599
1 前言
以前项目版本管理工具一直用的都是SVN,现在换成Git,并且项目托管平台使用的是GitLab。虽然经常用GitHub,但是却很少使用到分支,因为都是自己在写,也不用开发版与发布版、打Tag那些,所以这篇文章主要讲下GitLab与Git的使用。这篇文章还是会按我以前的写作风格,写的通俗易懂,并且全程自己操作一遍,目的是为了初学者也能看懂。

2 简介
Git:Git是一个开源的分布式版本控制系统,可以有效、高速的处理从很小到非常大的项目版本管理。简单说,它就是一个管理项目版本的工具。
GitLab:GitLab 是一个用于仓库管理系统的开源项目,使用Git作为代码管理工具,并在此基础上搭建起来的web服务。简单说,GitLab的性质是和GitHub一样的,都是用来存储项目的仓库。
3 准备Git
3.1 安装Git
下载地址:Git - Downloads
安装步骤:双击安装,按默认选项一路下去即可。
安装完成后,在开始菜单里找到“Git”–>“Git Bash”,出现如下图,就说明Git安装成功!

3.2 配置信息
在上图命令行中输入你的用户名与邮箱

$ git config –global user.name “Your Name”
$ git config –global user.email “email@example.com
AI写代码
命令中的 –global 参数表示你这台机器上所有的Git仓库都会使用这个配置。

3.3 Android Studio中配置Git
在Android Studio中点击Settings–>Version Control –> Git,然后在Path To Git executable上输入你刚刚安装Git的位置,最后点击Test按钮出现Git executed successfully说明配置成功,如下图:

4 GitLab使用
4.1 注册账号
这里说下注册账号的时候需要注意的事项。
如果注册账号的时候提示如下错误

there was an error with the reCAPTCHA,please solve the reCAPTCHA again
AI写代码
那是因为注册时需要验证码验证,就是reCAPTCHA,是一个图形验证码。但是这个验证码需要翻Q才能看得到,否则你一直都注册不了。如图:

4.2 GitLab账户配置SSH Keys
4.2.1 首先需要检查你电脑是否已经有 SSH Keys

在Git Bash 客户端,输入如下代码:

$ cd ~/.ssh
$ ls
AI写代码
如下说明已经存在,存在就可以忽略步骤4.2.2,直接进入步骤4.2.3

4.2.2 创建一个 SSH Keys

在Git Bash 客户端,输入如下代码:

$ ssh-keygen -t rsa -C “your_email”
AI写代码
然后根据提示进行相应操作,如下图所示:
第一个Enter表示Keys存储的位置,默认按回车键即可。第二个和第三个Enter表示Push文件的时候要输入的密码,不需要密码就默认按回车键即可,然后出现如图底部信息就说明创建成功了!

4.2.3 GitLab配置SSH Keys

进入上图所示存储 id_rsa.pub 的文件夹拷贝该文件中的内容,然后登录你的GitLab账号,点击网页右上角的头像–>Settings,点击左边菜单栏的 SSH Keys 进入页面添加 SSH key。如下操作:

4.3 创建项目
在GitLab网站点击导航条上的 “+” 即可进入创建项目的页面,然后根据提示填写相应信息,如下图:

Project path:项目路径
Project name:项目名称
Project description (optional):项目描述(可选项)
Visibility Level:可见登记,Private表示私有的,只有你自己或者组内的成员能访问,Internal表示所有登录的用户,Public表示公开的,所有人都可以访问。
创建完成后是这样的:

5 Android Studio中使用Git
5.1 将项目上传到GitLab
本地已创建好的项目为WildmaGit

5.1.1 配置忽略文件

Android Studio创建项目后会自动生成.gitignore文件,这个文件可以用来忽略那些不用加入到仓库的文件。项目根目录下与module目录下都会生成该文件,如下:

一般情况下我们只需要将项目根目录下的/.idea/workspace.xml与/.idea/libraries改成.idea即可,其他没有特殊要求就用默认的。如下:

修改前:

修改后:

5.1.2 初始化本地Git仓库

点击Android Studio顶部菜单栏上的VCS–>Import into Version Control–>Create Git Repository,如图:

然后选择需要初始化的项目根目录,如图:

初始化之后发现文件名由原来的白色变成了红色,这表示文件已经被git跟踪了,但是并没有添加到仓库中。如图:

5.1.3 将本地Git仓库已与远程仓库进行关联

打开项目文件夹,在该文件夹中打开Git Bash。输入如下命令后就代表本地Git仓库已与远程仓库进行关联了。

git remote add origin git@gitlab.com:wildma/WildmaGit.git
AI写代码
具体步骤见下图:

其中 git@gitlab.com:wildma/WildmaGit.git 为我们项目的远程地址,可以到我们前面创建项目中复制,如下:

image.png

5.1.4 添加文件

选中项目的根目录,点击Android Studio顶部菜单栏上的VCS–>Git–>Add…,如下图:

添加文件后发现文件名由原来的红色变成了绿色,说明已经将文件添加进仓库了。如下图:

5.1.5 提交文件

每次提交前需要先更新项目,原因是如果是多人合作的项目,需要先将远程仓库中他人提交的最新代码更新到本地仓库。如图:

然后选中Merge,点击OK,如果本地代码与远程代码有冲突就会弹出冲突提示,根据需求Merge即可,如图:

Update后就可以提交文件了,选中项目的根目录,点击Android Studio顶部菜单栏上的VCS–>Git–>Commit Directory…,如图:

然后会弹出如下提交框,这里可以选择你要提交的文件和填写提交信息,如图:

提交文件后发现文件名由原来的绿色又变成了最开始的白色,说明已经将文件提交上仓库了。如图:

5.1.6 将提交的文件推送到远程仓库

选中项目的根目录,点击Android Studio顶部菜单栏上的VCS–>Git–>Push…,如下图:

然后会弹出如下推送框

点击Push后,就可以在GitLab上看到刚刚推送的内容了。如下图:

image.png

5.2 从GitLab上clone项目
首先到GitLab上复制项目的地址,如图:

然后点击Android Studio顶部菜单栏上的VCS–>Checkout from Version Control–>Git ,如图:

最后填写相应的信息即可,如下图:

Git Repository URL:填写刚刚复制的项目地址
Parent Directory:项目路径
Directory Name:项目文件夹名称

点击clone后,项目就clone下来了!

5.3 分支管理
5.3.1 分支策略

在实际开发中,我们会用到很多分支。这里说下各分支的作用。

master分支:最稳定的分支,保存需要发布的版本,不在该分支上做任何开发。
dev分支:开发分支,保存最新的代码,平时开发就在该分支上。当某个版本开发完成后就合并到master分支,然后在master分支进行版本发布。
bug分支:用来修复bug的分支,一般是线上版出现bug的时候,从master分支创建一个新的bug分支进行bug修复,修复完成合并到master分支和dev分支(保证master分支与dev分支同步),然后删除该bug分支。
实际开发中我主要用到这三个分支,当然每个人都不同,有些人还会细分到功能分支,预发分支。其中master分支与dev分支都需要推送到远程,为了其他成员能共同开发,bug分支则放在本地即可,你自己修复完bug删掉即可。

5.3.2 分支操作

创建分支

点击Android Studio顶部菜单栏上的VCS–>Git–>Branches…,如图:

然后选择New Branch,输入分支的名字,例如“dev”,如图:

点击OK,分支dev就创建成功了,并且默认切换到该分支。重复上面的步骤可以看到当前分支显示的是dev,如图:

2.推送分支到远程

这时候我们在dev分支上修改内容,然后按照前面说的提交文件(步骤5.1.5),将提交的文件推送到远程仓库(步骤5.1.6)即可。如图:

点击push,然后就可以到Gitlab上看到我们刚刚推送的dev分支了,如图:

切换分支

dev分支上版本V1.0开发完了,需要将代码合并到master上进行发布。因为当前在dev分支,所以需要先切换到master分支,点击Android Studio顶部菜单栏上的VCS–>Git–>Branches…,打开Git Branches窗口,然后选择本地分支栏下的master->origin/master,然后点击Checkout。如图:

合并分支

第三步已经将分支切换到master了,现在进行分支合并。点击Android Studio顶部菜单栏上的VCS–>Git–>Branches…,打开Git Branches窗口,然后选择远程分支栏下的origin/dev,然后点击Merge。如图:

这个时候到GitLab上却发现master分支上并没有合并到dev分支修改的内容,那是因为刚刚只是将远程dev分支的内容合并到本地master分支,本地master分支还没有push到远程,push一下就可以啦~ 如图:

这个时候发现GitLab上master分支与dev分支是保持一致的了。然后将最新的master分支打包发布即可!

删除分支
删除本地分支:

点击Android Studio顶部菜单栏上的VCS–>Git–>Branches…,打开Git Branches窗口,选中本地dev分支,选择Delete即可。如图:

删除远程分支:

点击Android Studio顶部菜单栏上的VCS–>Git–>Branches…,打开Git Branches窗口,选中远程dev分支,选择Delete即可。如图:

5.4 标签(Tag)管理
标签一般是用于标记某个发布的版本, 例如你发布了版本v1.0,这个时候会打一个v1.0的Tag,主要是方便以后查看和管理某个版本的代码。

创建标签

点击Android Studio顶部菜单栏上的VCS–>Git–>Tag…,如图:

然后填写Tag名称与Tag信息,点击Create Tag即可创建本地标签。其中Commit可填写以前某次提交记录的id,表示在该次提交上打Tag。如果不填表示Tag打在最新提交的commit上。如图:

推送标签到远程
点击Android Studio顶部菜单栏上的VCS–>Git–>Push…,如图:

然后弹出Push框,选中Push Tags,点击Push即可推送到远程。如图:

最后到GitLab就可以看到刚刚创建的Tag了,如图:

checkout某个标签的代码

点击Android Studio顶部菜单栏上的VCS–>Git–>Branches…,如图:

然后选择Checkout Tag or Revision…,最后在弹出的Checkout框中填入Tag的名称即可。如图:

删除标签
在Android Studio中没找到删除Tag的操作,所以这里用Git命令。(有发现的小伙伴可以告诉我)
查看本地标签:

git tag
AI写代码
删除本地标签:

git tag -d tagName
AI写代码
删除远程标签:

git push origin :refs/tags/tagName
AI写代码
以上命令需要在Android Studio自带的命令行中输入,如图:

git remote set -url origin git@……git 修改远程仓地址
ls ~/.ssh 查看已有秘钥
ssh-keygen -t ed25519 -C “935607317@qq.com“ 生成秘钥 连续按3次回车
cat ~/.ssh/id_ed25519.pub 复制 添加到gitlab ssh

配置好签名文件,
./gradlew signingReport 可直接获取debug和release的sha1
signingConfigs {
release {
keyAlias ‘key0’
keyPassword ‘htnova1003’
storeFile file(‘cotton.jks’)
storePassword ‘htnova1003’
}
}

buildTypes {
    release {
        minifyEnabled true          // 开启代码混淆和压缩
        shrinkResources true        // 去掉未使用的资源
        proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        signingConfig signingConfigs.release
    }
    debug {
        minifyEnabled false         // debug 不压缩,方便调试
        shrinkResources false
    }
}

keytool -v -list -keystore /Users/xueqiaoming/Desktop/htnova.jks
keytool -list -keystore debug.keystore 去掉-v可以显示sha256

或者导出证书
keytool -exportcert -alias htnova -keystore /Users/xueqiaoming/Desktop/htnova.jks -rfc -file cert.pem
查看 cat cert.pem
openssl x509 -in cert.pem -noout -fingerprint -sha1