ToastUtils任意线程吐司优化完成。

This commit is contained in:
ZhanGSKen
2025-11-13 04:21:56 +08:00
parent fbf5ceadc7
commit cb15fe5eb0
6 changed files with 249 additions and 73 deletions

View File

@@ -1,8 +1,8 @@
#Created by .winboll/winboll_app_build.gradle
#Tue Nov 11 13:06:53 GMT 2025
#Wed Nov 12 20:21:00 GMT 2025
stageCount=10
libraryProject=libappbase
baseVersion=15.10
publishVersion=15.10.9
buildCount=8
buildCount=21
baseBetaVersion=15.10.10

View File

@@ -5,8 +5,8 @@ package cc.winboll.studio.appbase;
* @Date 2025/01/05 09:54:42
* @Describe APPbase 应用类
*/
import android.content.IntentFilter;
import cc.winboll.studio.libappbase.GlobalApplication;
import cc.winboll.studio.libappbase.ToastUtils;
public class App extends GlobalApplication {
@@ -15,5 +15,13 @@ public class App extends GlobalApplication {
@Override
public void onCreate() {
super.onCreate();
ToastUtils.init(getApplicationContext());
}
@Override
public void onTerminate() {
super.onTerminate();
ToastUtils.release();
}
}

View File

@@ -59,9 +59,16 @@ public class MainActivity extends Activity {
public void onToastUtilsTest(View view) {
LogUtils.d(TAG, "onToastUtilsTest");
ToastUtils.init(getApplicationContext());
ToastUtils.show("Hello, WinBoLL!");
ToastUtils.release();
ToastUtils.show("Hello, WinBoLL!");
new Thread(new Runnable(){
@Override
public void run() {
try {
Thread.sleep(2000);
ToastUtils.show("Thread.sleep(2000);ToastUtils.show...");
} catch (InterruptedException e) {}
}
}).start();
}
/**

View File

@@ -1,8 +1,8 @@
#Created by .winboll/winboll_app_build.gradle
#Tue Nov 11 13:06:53 GMT 2025
#Wed Nov 12 20:21:00 GMT 2025
stageCount=10
libraryProject=libappbase
baseVersion=15.10
publishVersion=15.10.9
buildCount=8
buildCount=21
baseBetaVersion=15.10.10

View File

@@ -17,12 +17,23 @@ public class GlobalApplication extends Application {
/** 日志标签 */
public static final String TAG = "GlobalApplication";
/** 全局 Application 单例实例volatile 保证多线程可见性,避免指令重排) */
private static volatile GlobalApplication sInstance;
/**
* 应用调试模式标记volatile 保证多线程可见性)
* true调试模式开启日志、调试功能false正式模式关闭调试相关功能
*/
private static volatile boolean isDebugging = false;
/**
* 获取全局 Application 单例实例(外部可通过此方法获取上下文)
* @return GlobalApplication 单例(未初始化时返回 null需确保配置 AndroidManifest
*/
public static GlobalApplication getInstance() {
return sInstance;
}
/**
* 设置应用调试模式
* @param debugging 调试模式状态true/false
@@ -33,9 +44,13 @@ public class GlobalApplication extends Application {
/**
* 保存调试模式状态到本地文件(持久化存储,重启应用后生效)
* @param application 全局 Application 实例(通过 getInstance() 获取更规范,此处保留原有参数
* @param application 全局 Application 实例(通过 getInstance() 获取更规范)
*/
public static void saveDebugStatus(GlobalApplication application) {
if (application == null) {
LogUtils.e(TAG, "saveDebugStatus: Application 实例为空,保存失败");
return;
}
// 将调试状态封装为 APPModel 并保存到文件
APPModel.saveBeanToFile(
getAppModelFilePath(application),
@@ -68,11 +83,15 @@ public class GlobalApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
// 初始化单例实例(确保在所有初始化操作前完成)
sInstance = this;
// 初始化基础组件日志、崩溃处理、Toast
initCoreComponents();
// 恢复/初始化调试模式状态(从本地文件读取,无文件则默认关闭调试)
restoreDebugStatus();
LogUtils.d(TAG, "GlobalApplication 初始化完成,单例实例已创建");
}
/**
@@ -103,9 +122,11 @@ public class GlobalApplication extends Application {
// 配置文件不存在,默认关闭调试模式并创建文件
setIsDebugging(false);
saveDebugStatus(this);
LogUtils.d(TAG, "调试配置文件不存在,默认关闭调试模式并创建配置文件");
} else {
// 配置文件存在,使用保存的调试状态
setIsDebugging(appModel.isDebugging());
LogUtils.d(TAG, "从配置文件恢复调试模式:" + isDebugging);
}
}
@@ -115,6 +136,10 @@ public class GlobalApplication extends Application {
* @return 应用名称(读取失败返回 null
*/
public static String getAppName(Context context) {
if (context == null) {
LogUtils.w(TAG, "getAppName: 上下文为空,返回 null");
return null;
}
PackageManager packageManager = context.getPackageManager();
try {
// 获取应用信息(包含应用名称、图标等)
@@ -123,12 +148,27 @@ public class GlobalApplication extends Application {
0 // 额外标志0 表示默认获取基本信息)
);
// 从应用信息中获取应用名称(支持多语言)
return (String) packageManager.getApplicationLabel(applicationInfo);
String appName = (String) packageManager.getApplicationLabel(applicationInfo);
LogUtils.d(TAG, "获取应用名称成功:" + appName);
return appName;
} catch (NameNotFoundException e) {
// 包名不存在(理论上不会发生,捕获异常避免崩溃)
LogUtils.d(TAG, e, Thread.currentThread().getStackTrace());
//LogUtils.e(TAG, "获取应用名称失败:包名不存在", e);
e.printStackTrace();
}
return null;
}
/**
* 应用终止时调用(仅用于释放全局资源)
*/
@Override
public void onTerminate() {
super.onTerminate();
// 释放单例引用(可选,避免内存泄漏风险)
sInstance = null;
LogUtils.d(TAG, "GlobalApplication 终止,单例实例已释放");
}
}

View File

@@ -1,17 +1,17 @@
package cc.winboll.studio.libappbase;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2025/11/11 20:51
* @Describe 吐司工具类(单例模式)
* 简化 Android 吐司的创建与展示,通过 Handler 确保主线程显示,统一管理上下文,避免内存泄漏
*/
import android.content.Context;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.widget.Toast;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2025/11/11 20:51
* @Describe 吐司工具类(单例模式)
* 简化 Android 吐司的创建与展示,通过独立线程 + Handler 处理消息,最终切换到主线程显示吐司,避免内存泄漏
*/
public class ToastUtils {
/** 工具类日志 TAG用于调试输出 */
@@ -21,38 +21,81 @@ public class ToastUtils {
/** 单例实例volatile 保证多线程下可见性,避免指令重排) */
private static volatile ToastUtils sInstance;
/** 全局上下文(建议传入 Application 实例,避免内存泄漏 */
private Context mContext;
/** 线程 Handler用于接收并处理吐司显示消息,确保 UI 操作在主线程 */
private static Handler _mMainHandler;
/** 全局上下文(volatile 保证多线程可见性,避免空指针 */
private volatile Context mContext;
/** 独立线程 Handlervolatile 保证可见性 */
private volatile Handler mWorkerHandler;
/** 主线程 Handlervolatile 保证可见性) */
private volatile Handler mMainHandler;
/** 消息处理独立线程 */
private Thread mWorkerThread;
/** 资源释放标记volatile 避免多线程误操作) */
private volatile boolean isReleased = false;
/**
* 私有构造方法(禁止外部直接创建实例,确保单例)
* 初始化主线程 Handler,绑定主线程 Looper
* 1. 初始化主线程 Handler
* 2. 创建并启动独立消息处理线程。
*/
private ToastUtils() {
// 初始化 Handler,绑定主线程 Looper确保吐司在主线程显示
_mMainHandler = new Handler(Looper.getMainLooper()) {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
// 处理消息:显示吐司
if (msg.what == MSG_SHOW_SHORT_TOAST && msg.obj != null) {
String message = (String) msg.obj;
showToastInternal(message);
}
}
};
initMainHandler(); // 优先初始化主线程 Handler
startWorkerThread(); // 启动独立消息处理线程
}
/**
* 获取单例实例(双重检查锁定,高效且线程安全)
* 初始化主线程 Handler
*/
private void initMainHandler() {
if (Looper.getMainLooper() == null) {
LogUtils.e(TAG, "主线程 Looper 为空,无法初始化 mMainHandler");
throw new IllegalStateException("主线程 Looper 未初始化,无法创建 ToastUtils");
}
mMainHandler = new Handler(Looper.getMainLooper());
LogUtils.d(TAG, "主线程 Handler 初始化完成线程ID" + Looper.getMainLooper().getThread().getId());
}
/**
* 启动独立消息处理线程
*/
private void startWorkerThread() {
mWorkerThread = new Thread(new Runnable() {
@Override
public void run() {
LogUtils.d(TAG, "消息处理线程启动线程ID" + Thread.currentThread().getId());
Looper.prepare();
mWorkerHandler = new Handler(Looper.myLooper()) {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
// 若已释放,直接返回,不处理消息
if (isReleased) {
LogUtils.w(TAG, "资源已释放,忽略消息处理");
return;
}
LogUtils.d(TAG, "WorkerHandler 接收消息当前线程ID" + Thread.currentThread().getId());
if (msg.what == MSG_SHOW_SHORT_TOAST && msg.obj != null) {
String message = (String) msg.obj;
postToMainThreadShowToast(message);
}
}
};
Looper.loop();
LogUtils.d(TAG, "消息处理线程退出");
}
}, "ToastWorkerThread");
mWorkerThread.start();
}
/**
* 获取单例实例(双重检查锁定)
* @return ToastUtils 单例对象
*/
private static ToastUtils getInstance() {
if (sInstance == null) { // 第一次检查(无锁,提升效率)
synchronized (ToastUtils.class) { // 加锁,确保线程安全
if (sInstance == null) { // 第二次检查(避免多线程并发创建多个实例)
if (sInstance == null) {
synchronized (ToastUtils.class) {
if (sInstance == null) {
sInstance = new ToastUtils();
}
}
@@ -61,76 +104,154 @@ public class ToastUtils {
}
/**
* 初始化工具类(必须在 Application 或 Activity 启动时调用)
* 传入全局上下文,用于创建 Toast 实例
* @param context 全局上下文(推荐传入 getApplicationContext()
* 初始化工具类(必须在 Application 启动时调用)
* @param context 全局上下文(推荐 Application 上下文)
*/
public static void init(Context context) {
if (context == null) {
throw new IllegalArgumentException("初始化上下文不能为 null");
}
// 初始化全局上下文(使用 Application 上下文,避免内存泄漏)
getInstance().mContext = context.getApplicationContext();
ToastUtils instance = getInstance();
// 若已释放,重置释放标记
if (instance.isReleased) {
instance.isReleased = false;
instance.startWorkerThread(); // 重新启动线程
}
instance.mContext = context.getApplicationContext();
LogUtils.d(TAG, "ToastUtils 初始化完成,上下文已设置");
}
/**
* 外部接口:显示短时长吐司(默认显示 2 秒)
* 接收外部消息参数,通过 Handler 发送消息到主线程
* @param message 吐司展示的文本内容(非空)
* 外部接口:显示短时长吐司
* @param message 吐司内容
*/
public static void show(String message) {
LogUtils.d(TAG, "show()");
LogUtils.d(TAG, "外部调用 show()当前线程ID" + Thread.currentThread().getId());
if (message == null || message.isEmpty()) {
return; // 空消息直接返回,避免无效显示
return;
}
// 校验工具类是否初始化
if (getInstance().mContext == null) {
throw new IllegalStateException("ToastUtils 未初始化!请先调用 init(Context) 方法");
ToastUtils instance = getInstance();
// 校验资源是否已释放
if (instance.isReleased) {
LogUtils.w(TAG, "ToastUtils 已释放,无法显示吐司:" + message);
return;
}
// 发送消息到主线程 Handler
getInstance().sendToastMessage(message);
// 校验上下文是否初始化
if (instance.mContext == null) {
LogUtils.e(TAG, "ToastUtils 未初始化!请先调用 init(Context) 方法");
// 不抛出异常,避免崩溃,改为日志提示
return;
}
instance.sendToastMessage(message);
}
/**
* 内部私有方法:发送吐司消息(通过 Handler 传递)
* 使用实例初始化时的全局上下文确保消息发送有效性
* @param message 吐司文本内容
* 发送吐司消息到 WorkerHandler
* @param message 吐司内容
*/
private void sendToastMessage(String message) {
// 校验 Handler 和上下文是否有效
if (_mMainHandler == null || mContext == null) {
LogUtils.d(TAG, "发送消息到 WorkerHandler");
// 校验 WorkerHandler 是否就绪
if (mWorkerHandler == null) {
LogUtils.w(TAG, "WorkerHandler 未就绪,直接主线程显示");
postToMainThreadShowToast(message);
return;
}
// 创建消息对象,携带吐司内容
Message msg = _mMainHandler.obtainMessage(MSG_SHOW_SHORT_TOAST);
// 发送消息
Message msg = mWorkerHandler.obtainMessage(MSG_SHOW_SHORT_TOAST);
msg.obj = message;
// 发送消息(放入主线程消息队列)
_mMainHandler.sendMessage(msg);
mWorkerHandler.sendMessage(msg);
}
/**
* 内部私有方法:实际显示吐司(运行在主线程)
* @param message 吐司文本内容
* 切换到主线程显示吐司
* @param message 吐司内容
*/
private void showToastInternal(String message) {
// 校验上下文有效性
if (mContext == null) {
private void postToMainThreadShowToast(final String message) {
LogUtils.d(TAG, "切换到主线程显示吐司当前线程ID" + Thread.currentThread().getId());
// 校验资源是否已释放
if (isReleased) {
LogUtils.w(TAG, "资源已释放,取消显示吐司");
return;
}
// 校验并初始化 mMainHandler
if (mMainHandler == null) {
LogUtils.e(TAG, "mMainHandler 为空,尝试重新初始化");
initMainHandler();
if (mMainHandler == null) {
LogUtils.e(TAG, "mMainHandler 初始化失败,无法显示吐司:" + message);
return;
}
}
// 主线程显示
mMainHandler.post(new Runnable() {
@Override
public void run() {
if (isReleased) return; // 释放后取消执行
showToastInternal(message);
}
});
}
/**
* 实际显示吐司(主线程)
* @param message 吐司内容
*/
private void showToastInternal(String message) {
LogUtils.d(TAG, "执行 showToastInternal()");
// 最终校验上下文
if (mContext == null) {
LogUtils.w(TAG, "上下文为空,无法显示吐司:" + message);
// 尝试重新获取 Application 上下文(降级策略)
Context appContext = GlobalApplication.getInstance();
if (appContext != null) {
mContext = appContext;
Toast.makeText(mContext, message, Toast.LENGTH_SHORT).show();
LogUtils.d(TAG, "通过 GlobalApplication 获取上下文,成功显示吐司");
}
return;
}
// 显示短时长吐司
Toast.makeText(mContext, message, Toast.LENGTH_SHORT).show();
}
/**
* 可选:释放资源(在应用退出时调用)
* 移除 Handler 未处理的消息,避免内存泄漏
* 释放资源(在应用退出时调用)
*/
public static void release() {
if (getInstance()._mMainHandler != null) {
_mMainHandler.removeCallbacksAndMessages(null); // 移除所有未处理消息
_mMainHandler = null;
LogUtils.d(TAG, "开始释放 ToastUtils 资源");
ToastUtils instance = getInstance();
// 标记为已释放,阻止后续消息处理
instance.isReleased = true;
// 停止 Worker 线程
if (instance.mWorkerHandler != null && instance.mWorkerHandler.getLooper() != null) {
instance.mWorkerHandler.getLooper().quit();
instance.mWorkerHandler = null;
}
sInstance = null; // 销毁单例实例
// 清理主线程 Handler
if (instance.mMainHandler != null) {
instance.mMainHandler.removeCallbacksAndMessages(null);
instance.mMainHandler = null;
}
// 等待线程退出
if (instance.mWorkerThread != null && instance.mWorkerThread.isAlive()) {
try {
instance.mWorkerThread.join(1000);
} catch (InterruptedException e) {
LogUtils.d(TAG, e, Thread.currentThread().getStackTrace());
//LogUtils.e(TAG, "线程退出异常", e);
Thread.currentThread().interrupt();
}
instance.mWorkerThread = null;
}
// 清空上下文(避免内存泄漏)
instance.mContext = null;
LogUtils.d(TAG, "ToastUtils 资源释放完成");
}
}