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 #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 stageCount=10
libraryProject=libappbase libraryProject=libappbase
baseVersion=15.10 baseVersion=15.10
publishVersion=15.10.9 publishVersion=15.10.9
buildCount=8 buildCount=21
baseBetaVersion=15.10.10 baseBetaVersion=15.10.10

View File

@@ -5,8 +5,8 @@ package cc.winboll.studio.appbase;
* @Date 2025/01/05 09:54:42 * @Date 2025/01/05 09:54:42
* @Describe APPbase 应用类 * @Describe APPbase 应用类
*/ */
import android.content.IntentFilter;
import cc.winboll.studio.libappbase.GlobalApplication; import cc.winboll.studio.libappbase.GlobalApplication;
import cc.winboll.studio.libappbase.ToastUtils;
public class App extends GlobalApplication { public class App extends GlobalApplication {
@@ -15,5 +15,13 @@ public class App extends GlobalApplication {
@Override @Override
public void onCreate() { public void onCreate() {
super.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) { public void onToastUtilsTest(View view) {
LogUtils.d(TAG, "onToastUtilsTest"); LogUtils.d(TAG, "onToastUtilsTest");
ToastUtils.init(getApplicationContext()); ToastUtils.show("Hello, WinBoLL!");
ToastUtils.show("Hello, WinBoLL!"); new Thread(new Runnable(){
ToastUtils.release(); @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 #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 stageCount=10
libraryProject=libappbase libraryProject=libappbase
baseVersion=15.10 baseVersion=15.10
publishVersion=15.10.9 publishVersion=15.10.9
buildCount=8 buildCount=21
baseBetaVersion=15.10.10 baseBetaVersion=15.10.10

View File

@@ -17,12 +17,23 @@ public class GlobalApplication extends Application {
/** 日志标签 */ /** 日志标签 */
public static final String TAG = "GlobalApplication"; public static final String TAG = "GlobalApplication";
/** 全局 Application 单例实例volatile 保证多线程可见性,避免指令重排) */
private static volatile GlobalApplication sInstance;
/** /**
* 应用调试模式标记volatile 保证多线程可见性) * 应用调试模式标记volatile 保证多线程可见性)
* true调试模式开启日志、调试功能false正式模式关闭调试相关功能 * true调试模式开启日志、调试功能false正式模式关闭调试相关功能
*/ */
private static volatile boolean isDebugging = false; private static volatile boolean isDebugging = false;
/**
* 获取全局 Application 单例实例(外部可通过此方法获取上下文)
* @return GlobalApplication 单例(未初始化时返回 null需确保配置 AndroidManifest
*/
public static GlobalApplication getInstance() {
return sInstance;
}
/** /**
* 设置应用调试模式 * 设置应用调试模式
* @param debugging 调试模式状态true/false * @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) { public static void saveDebugStatus(GlobalApplication application) {
if (application == null) {
LogUtils.e(TAG, "saveDebugStatus: Application 实例为空,保存失败");
return;
}
// 将调试状态封装为 APPModel 并保存到文件 // 将调试状态封装为 APPModel 并保存到文件
APPModel.saveBeanToFile( APPModel.saveBeanToFile(
getAppModelFilePath(application), getAppModelFilePath(application),
@@ -68,11 +83,15 @@ public class GlobalApplication extends Application {
@Override @Override
public void onCreate() { public void onCreate() {
super.onCreate(); super.onCreate();
// 初始化单例实例(确保在所有初始化操作前完成)
sInstance = this;
// 初始化基础组件日志、崩溃处理、Toast // 初始化基础组件日志、崩溃处理、Toast
initCoreComponents(); initCoreComponents();
// 恢复/初始化调试模式状态(从本地文件读取,无文件则默认关闭调试) // 恢复/初始化调试模式状态(从本地文件读取,无文件则默认关闭调试)
restoreDebugStatus(); restoreDebugStatus();
LogUtils.d(TAG, "GlobalApplication 初始化完成,单例实例已创建");
} }
/** /**
@@ -103,9 +122,11 @@ public class GlobalApplication extends Application {
// 配置文件不存在,默认关闭调试模式并创建文件 // 配置文件不存在,默认关闭调试模式并创建文件
setIsDebugging(false); setIsDebugging(false);
saveDebugStatus(this); saveDebugStatus(this);
LogUtils.d(TAG, "调试配置文件不存在,默认关闭调试模式并创建配置文件");
} else { } else {
// 配置文件存在,使用保存的调试状态 // 配置文件存在,使用保存的调试状态
setIsDebugging(appModel.isDebugging()); setIsDebugging(appModel.isDebugging());
LogUtils.d(TAG, "从配置文件恢复调试模式:" + isDebugging);
} }
} }
@@ -115,6 +136,10 @@ public class GlobalApplication extends Application {
* @return 应用名称(读取失败返回 null * @return 应用名称(读取失败返回 null
*/ */
public static String getAppName(Context context) { public static String getAppName(Context context) {
if (context == null) {
LogUtils.w(TAG, "getAppName: 上下文为空,返回 null");
return null;
}
PackageManager packageManager = context.getPackageManager(); PackageManager packageManager = context.getPackageManager();
try { try {
// 获取应用信息(包含应用名称、图标等) // 获取应用信息(包含应用名称、图标等)
@@ -123,12 +148,27 @@ public class GlobalApplication extends Application {
0 // 额外标志0 表示默认获取基本信息) 0 // 额外标志0 表示默认获取基本信息)
); );
// 从应用信息中获取应用名称(支持多语言) // 从应用信息中获取应用名称(支持多语言)
return (String) packageManager.getApplicationLabel(applicationInfo); String appName = (String) packageManager.getApplicationLabel(applicationInfo);
LogUtils.d(TAG, "获取应用名称成功:" + appName);
return appName;
} catch (NameNotFoundException e) { } catch (NameNotFoundException e) {
// 包名不存在(理论上不会发生,捕获异常避免崩溃) // 包名不存在(理论上不会发生,捕获异常避免崩溃)
LogUtils.d(TAG, e, Thread.currentThread().getStackTrace());
//LogUtils.e(TAG, "获取应用名称失败:包名不存在", e);
e.printStackTrace(); e.printStackTrace();
} }
return null; 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; 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.content.Context;
import android.os.Handler; import android.os.Handler;
import android.os.Looper; import android.os.Looper;
import android.os.Message; import android.os.Message;
import android.widget.Toast; import android.widget.Toast;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2025/11/11 20:51
* @Describe 吐司工具类(单例模式)
* 简化 Android 吐司的创建与展示,通过独立线程 + Handler 处理消息,最终切换到主线程显示吐司,避免内存泄漏
*/
public class ToastUtils { public class ToastUtils {
/** 工具类日志 TAG用于调试输出 */ /** 工具类日志 TAG用于调试输出 */
@@ -21,38 +21,81 @@ public class ToastUtils {
/** 单例实例volatile 保证多线程下可见性,避免指令重排) */ /** 单例实例volatile 保证多线程下可见性,避免指令重排) */
private static volatile ToastUtils sInstance; private static volatile ToastUtils sInstance;
/** 全局上下文(建议传入 Application 实例,避免内存泄漏 */ /** 全局上下文(volatile 保证多线程可见性,避免空指针 */
private Context mContext; private volatile Context mContext;
/** 线程 Handler用于接收并处理吐司显示消息,确保 UI 操作在主线程 */ /** 独立线程 Handlervolatile 保证可见性 */
private static Handler _mMainHandler; 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() { private ToastUtils() {
// 初始化 Handler,绑定主线程 Looper确保吐司在主线程显示 initMainHandler(); // 优先初始化主线程 Handler
_mMainHandler = new Handler(Looper.getMainLooper()) { startWorkerThread(); // 启动独立消息处理线程
@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);
}
}
};
} }
/** /**
* 获取单例实例(双重检查锁定,高效且线程安全) * 初始化主线程 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 单例对象 * @return ToastUtils 单例对象
*/ */
private static ToastUtils getInstance() { private static ToastUtils getInstance() {
if (sInstance == null) { // 第一次检查(无锁,提升效率) if (sInstance == null) {
synchronized (ToastUtils.class) { // 加锁,确保线程安全 synchronized (ToastUtils.class) {
if (sInstance == null) { // 第二次检查(避免多线程并发创建多个实例) if (sInstance == null) {
sInstance = new ToastUtils(); sInstance = new ToastUtils();
} }
} }
@@ -61,76 +104,154 @@ public class ToastUtils {
} }
/** /**
* 初始化工具类(必须在 Application 或 Activity 启动时调用) * 初始化工具类(必须在 Application 启动时调用)
* 传入全局上下文,用于创建 Toast 实例 * @param context 全局上下文(推荐 Application 上下文)
* @param context 全局上下文(推荐传入 getApplicationContext()
*/ */
public static void init(Context context) { public static void init(Context context) {
if (context == null) { if (context == null) {
throw new IllegalArgumentException("初始化上下文不能为 null"); throw new IllegalArgumentException("初始化上下文不能为 null");
} }
// 初始化全局上下文(使用 Application 上下文,避免内存泄漏) ToastUtils instance = getInstance();
getInstance().mContext = context.getApplicationContext(); // 若已释放,重置释放标记
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) { public static void show(String message) {
LogUtils.d(TAG, "show()"); LogUtils.d(TAG, "外部调用 show()当前线程ID" + Thread.currentThread().getId());
if (message == null || message.isEmpty()) { if (message == null || message.isEmpty()) {
return; // 空消息直接返回,避免无效显示 return;
} }
// 校验工具类是否初始化
if (getInstance().mContext == null) { ToastUtils instance = getInstance();
throw new IllegalStateException("ToastUtils 未初始化!请先调用 init(Context) 方法"); // 校验资源是否已释放
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 传递) * 发送吐司消息到 WorkerHandler
* 使用实例初始化时的全局上下文确保消息发送有效性 * @param message 吐司内容
* @param message 吐司文本内容
*/ */
private void sendToastMessage(String message) { private void sendToastMessage(String message) {
// 校验 Handler 和上下文是否有效 LogUtils.d(TAG, "发送消息到 WorkerHandler");
if (_mMainHandler == null || mContext == null) { // 校验 WorkerHandler 是否就绪
if (mWorkerHandler == null) {
LogUtils.w(TAG, "WorkerHandler 未就绪,直接主线程显示");
postToMainThreadShowToast(message);
return; return;
} }
// 创建消息对象,携带吐司内容 // 发送消息
Message msg = _mMainHandler.obtainMessage(MSG_SHOW_SHORT_TOAST); Message msg = mWorkerHandler.obtainMessage(MSG_SHOW_SHORT_TOAST);
msg.obj = message; msg.obj = message;
// 发送消息(放入主线程消息队列) mWorkerHandler.sendMessage(msg);
_mMainHandler.sendMessage(msg);
} }
/** /**
* 内部私有方法:实际显示吐司(运行在主线程) * 切换到主线程显示吐司
* @param message 吐司文本内容 * @param message 吐司内容
*/ */
private void showToastInternal(String message) { private void postToMainThreadShowToast(final String message) {
// 校验上下文有效性 LogUtils.d(TAG, "切换到主线程显示吐司当前线程ID" + Thread.currentThread().getId());
if (mContext == null) { // 校验资源是否已释放
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; return;
} }
// 显示短时长吐司
Toast.makeText(mContext, message, Toast.LENGTH_SHORT).show(); Toast.makeText(mContext, message, Toast.LENGTH_SHORT).show();
} }
/** /**
* 可选:释放资源(在应用退出时调用) * 释放资源(在应用退出时调用)
* 移除 Handler 未处理的消息,避免内存泄漏
*/ */
public static void release() { public static void release() {
if (getInstance()._mMainHandler != null) { LogUtils.d(TAG, "开始释放 ToastUtils 资源");
_mMainHandler.removeCallbacksAndMessages(null); // 移除所有未处理消息 ToastUtils instance = getInstance();
_mMainHandler = null; // 标记为已释放,阻止后续消息处理
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 资源释放完成");
} }
} }