濫用Static封裝Dialog導致某些情況下Dialog show失敗問題

2020-10-27 11:00:47

由於靜態的欄位或方法在記憶體中只存在一份,所以利用這個特性可以做一些物件記憶體的優化,典型的就是單例的使用。然而static特性雖好,如果對全域性的知識點掌握不夠充分,還是會出現一些不易發現的bug的。最近使用公司專案中Dialog封裝庫就採坑了,,,

一、Dialog庫以及引起的鍋

1、DialogUtils

如下是一個簡化版的封裝庫,但是前同事的這鍋已經基本模擬出來了。

public class DialogUtils {

    private static final String TAG = DialogUtils.class.getSimpleName();

    private static AlertDialog sslErrorDialog = null;

    private static AlertDialog.Builder dialogBuilder(Activity context, String title, String message) {
        AlertDialog.Builder builder = new AlertDialog.Builder(context);
        if (!TextUtils.isEmpty(message)) {
            builder.setMessage(message);
        }
        if (!TextUtils.isEmpty(title)) {
            builder.setTitle(title);
        }
        builder.setCancelable(false);
        return builder;
    }


    /**
     * SslError 彈窗
     * @param context activity
     * @param message title
     */
    public static void showSSLErrorDialog(final Activity context, String message) {
        if (context == null || context.isFinishing()) {
            return;
        }
        if (sslErrorDialog == null) { // if 語句導致,潛在bug。
            AlertDialog.Builder builder = dialogBuilder(context, null, message);
            builder.setNegativeButton("back",
                    new DialogInterface.OnClickListener() {
                        @Override
                        public void onClick(DialogInterface dialog, int which) {
                            if (sslErrorDialog != null) {
                                sslErrorDialog.dismiss();
                                sslErrorDialog = null;
                            }
                        }
                    });

            builder.setPositiveButton("sure",
                    new DialogInterface.OnClickListener() {
                        @Override
                        public void onClick(DialogInterface dialog, int which) {
                            if (sslErrorDialog != null) {
                                sslErrorDialog.dismiss();
                                sslErrorDialog = null;
                            }
                        }
                    });

            sslErrorDialog = builder.create();
        }
        sslErrorDialog.show();
        Button button = sslErrorDialog.getButton(DialogInterface.BUTTON_NEGATIVE);
        button.requestFocus();
    }
}
2、bug的觸發

在這裡插入圖片描述

如上圖有個簡單的App A ,activity 為預設啟動模式,在activity的onCreate中通過上述工具庫呼叫dialog。這時進行如下步驟:
1、點選桌面app A icon 進入app A
2、嗯home鍵->點選 app iconB->點選B內按鈕
這時你會發現1會彈出dialog2不會彈出dialog。所以bug就出來了。預設情況下2也需要彈窗的。

二、問題排查

熟悉Android Dialog 的同學都知道AlertDialog 是基於Build模式封裝的Dialog類,AlertDialog.Builder類的作用就是建立AlertDialog 物件,為AlertDialog物件提供一些「原料」。這些原料中有一個最重要的欄位Context。這裡就是引起上述bug的關鍵所在。

1、大話Dialog與Activity的關係

(1)說到Dialog 與Activity 的關係這裡就不得不扯一下安卓的Window的作用

其實我們在手機上能夠看到的介面都是View,View最終被新增到Window顯示出來我們才能看到的。這時有人可能會問不對啊,我們看到的介面不是Activity嗎?這時我只能說你瞭解的知識還不夠全面(啊哈哈裝逼了輕拍。。。)其實Activity 內部的setContentView最終還是被載入到Window上的。

(2)Window 的分類

  • 應用Window(對應activity)
  • 子Window(對應dialog,依附於activity)
  • 系統window(對應Toast等)

其實Window就是View的載體:
如某一個Activity頁面就是一個Window表單(activity作為媒介吧View載入到對應的Window上)。
在比如我們通過WindowManager可以像Window上新增VIew。

(3)子window被add到window上的限制

dialog最終是被載入到window上的,但是被載入的過程需要activity的token。也就是當你在某個activity上show dialog時傳遞這個activity範例即可。

(4)原始碼略讀
在這裡插入圖片描述

public void addView(View view, ViewGroup.LayoutParams params,
        Display display, Window parentWindow) {
...
...
 if (parentWindow != null) {
       // 核心就在此處。具體實現在Window類中adjustLayoutParamsForSubWindow方法
       // 內部處理了三種win型別。
        parentWindow.adjustLayoutParamsForSubWindow(wparams);
    }
....
....
}

這裡我們只需知道普通的dialog是需要依附於activity而存在的。也就是需要activity的token,所以你在傳遞引數時要傳activity的context即可。在哪個activity彈窗,就傳哪個activity的範例即可。

2、回顧問題

分析DialogUtils 工具類結合我們的實踐步驟我們可以知道,當第二次進入activity時又建立了新的activity範例,雖然showSSLErrorDialog(final Activity context, String message)傳遞了第二次進入的範例,但是方法內部的sslErrorDialog !=null直接把第二次傳入的context範例掐死了,AlertDialog.Build 沒有重新構建AlertDialog,這時context範例還是第一次傳入的。所以就算彈窗也是彈在App A的activity上。

三、栗子驗證

接下來以一個小栗子驗證下 指定activity上彈窗

public class MyApplication extends Application {

    public static List<Activity>mList = new ArrayList<>();

    @Override
    public void onCreate() {
        super.onCreate();
        registerActivityLifecycleCallbacks(new ActivityLifecycleCallbacks() {
            @Override
            public void onActivityCreated(@NonNull Activity activity, @Nullable Bundle savedInstanceState) {
                mList.add(activity);
            }

            @Override
            public void onActivityStarted(@NonNull Activity activity) {

            }

            @Override
            public void onActivityResumed(@NonNull Activity activity) {

            }

            @Override
            public void onActivityPaused(@NonNull Activity activity) {

            }

            @Override
            public void onActivityStopped(@NonNull Activity activity) {

            }

            @Override
            public void onActivitySaveInstanceState(@NonNull Activity activity, @NonNull Bundle outState) {

            }

            @Override
            public void onActivityDestroyed(@NonNull Activity activity) {

            }
        });
    }
}


public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        startActivity(new Intent(this,Main2Activity.class));
    }
}

public class Main2Activity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main2);

        Log.d("Main2Activity",""+this);
        Log.d("MainActivity",""+MyApplication.mList.get(0));
        Log.d("Main2Activity",""+MyApplication.mList.get(1));

        DialogUtils.showSSLErrorDialog(MyApplication.mList.get(0),"簡單的dialog模擬");// 此時dialog會彈在MainActivity上
    }
}

觀察發現dialog會彈在MainActivity上

三、總結

至此採坑完畢。。。其實上述強調過普通dialog,其實就是依附於activity的dialog,其實我們還可以通過api設定dialog所屬Window的型別讓其不依附activity,這些都可以在原始碼中找到答案這裡就不在扯了,有興趣的小夥伴可以自行探討。