From cb15fe5eb05505e38098bffc8960596118f88f02 Mon Sep 17 00:00:00 2001 From: ZhanGSKen Date: Thu, 13 Nov 2025 04:21:56 +0800 Subject: [PATCH] =?UTF-8?q?ToastUtils=E4=BB=BB=E6=84=8F=E7=BA=BF=E7=A8=8B?= =?UTF-8?q?=E5=90=90=E5=8F=B8=E4=BC=98=E5=8C=96=E5=AE=8C=E6=88=90=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- appbase/build.properties | 4 +- .../java/cc/winboll/studio/appbase/App.java | 10 +- .../winboll/studio/appbase/MainActivity.java | 13 +- libappbase/build.properties | 4 +- .../studio/libappbase/GlobalApplication.java | 44 +++- .../winboll/studio/libappbase/ToastUtils.java | 247 +++++++++++++----- 6 files changed, 249 insertions(+), 73 deletions(-) diff --git a/appbase/build.properties b/appbase/build.properties index a7e41487..c359ff42 100644 --- a/appbase/build.properties +++ b/appbase/build.properties @@ -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 diff --git a/appbase/src/main/java/cc/winboll/studio/appbase/App.java b/appbase/src/main/java/cc/winboll/studio/appbase/App.java index 271747c0..025983f7 100644 --- a/appbase/src/main/java/cc/winboll/studio/appbase/App.java +++ b/appbase/src/main/java/cc/winboll/studio/appbase/App.java @@ -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(); + } } diff --git a/appbase/src/main/java/cc/winboll/studio/appbase/MainActivity.java b/appbase/src/main/java/cc/winboll/studio/appbase/MainActivity.java index b3ec737a..45884918 100644 --- a/appbase/src/main/java/cc/winboll/studio/appbase/MainActivity.java +++ b/appbase/src/main/java/cc/winboll/studio/appbase/MainActivity.java @@ -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(); } /** diff --git a/libappbase/build.properties b/libappbase/build.properties index a7e41487..c359ff42 100644 --- a/libappbase/build.properties +++ b/libappbase/build.properties @@ -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 diff --git a/libappbase/src/main/java/cc/winboll/studio/libappbase/GlobalApplication.java b/libappbase/src/main/java/cc/winboll/studio/libappbase/GlobalApplication.java index eba41715..b0018c88 100644 --- a/libappbase/src/main/java/cc/winboll/studio/libappbase/GlobalApplication.java +++ b/libappbase/src/main/java/cc/winboll/studio/libappbase/GlobalApplication.java @@ -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 终止,单例实例已释放"); + } } diff --git a/libappbase/src/main/java/cc/winboll/studio/libappbase/ToastUtils.java b/libappbase/src/main/java/cc/winboll/studio/libappbase/ToastUtils.java index 7380aa58..f4586ff2 100644 --- a/libappbase/src/main/java/cc/winboll/studio/libappbase/ToastUtils.java +++ b/libappbase/src/main/java/cc/winboll/studio/libappbase/ToastUtils.java @@ -1,17 +1,17 @@ package cc.winboll.studio.libappbase; -/** - * @Author ZhanGSKen&豆包大模型 - * @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&豆包大模型 + * @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; + /** 独立线程的 Handler(volatile 保证可见性) */ + private volatile Handler mWorkerHandler; + /** 主线程 Handler(volatile 保证可见性) */ + 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 资源释放完成"); } }