源码学习_12Dialog

2018-05-02  本文已影响21人  冉桓彬

一、参考文章:

为什么Dialog不能用Application的Context
使用Dialog时需要注意的问题
创建Dialog所需的上下文为什么必须是Activity?

二、问题:

image.png

三、demo:

public class Activity {
    public void method() {
        DialogTest dialogTest = new DialogTest(getApplicationContext());
        dialogTest.show();
    }
}

public class DialogTest extends Dialog {

    DialogTest(@NonNull Context context) {
        super(context);
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.dialog_test);
    }
}
构造Dialog传入ApplicationContext会报错

四、源码分析:

4.1 Dialog构造函数:
public class Dialog {
    public Dialog(@NonNull Context context) {
        this(context, 0, true);
    }

    Dialog(@NonNull Context context, @StyleRes int themeResId, boolean createContextThemeWrapper) {
        if (createContextThemeWrapper) {
            mContext = new ContextThemeWrapper(context, themeResId);
        } else {...}
        /**
         * 1. WindowManager指向WindowManagerImpl;
         * 2. Context有两种情况: Context instanceof Application; Context instanceof ActivityContext,
         *    结合这两种模式构建WindowManagerImpl的方式可能不同<4.2>;
         * 3. 结合模块<4.2>可知, 如果Context instanceof Application, 构建一个Application_WML, 如果
         *    Context instanceof ActivityContext, 则返回Activity_WML;
         */
        mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);

        final Window w = new PhoneWindow(mContext);
        mWindow = w;
        /**
         * 1. ContextThemeWrapper持有我们外部传进来的Context, 可能指向ApplicationContext/ActivityContext;
         * 2. PhoneWindow只有ContextThemeWrapper的引用;
         */
        w.setWindowManager(mWindowManager, null, null);
    }
}

public class PhoneWindow {

    public Window(Context context) {
        mContext = context;
        ...
    }

    public void setWindowManager(WindowManager wm, IBinder appToken, String appName) {
        setWindowManager(wm, appToken, appName, false);
    }
    /**
     * 1. 结合上面传来的参数---> Dialog构造函数会触发PhoneWindow被初始化, 然后通过setWindowManager
     *    将PhoneWindow与WindowManager进行关联;
     * 2. 此时PhoneWindow内部的mAppToken默认为null;
     */
    public void setWindowManager(WindowManager wm, IBinder appToken, String appName, boolean hardwareAccelerated) {
        mAppToken = appToken;
        if (wm == null) {
            wm = (WindowManager)mContext.getSystemService(Context.WINDOW_SERVICE);
        }
        mWindowManager = ((WindowManagerImpl)wm).createLocalWindowManager(this);
    }
}

public final class WindowManagerImpl implements WindowManager {
    public WindowManagerImpl createLocalWindowManager(Window parentWindow) {
        return new WindowManagerImpl(mContext, parentWindow);
    }

    private WindowManagerImpl(Context context, Window parentWindow) {
        mContext = context;
        mParentWindow = parentWindow;
    }
}
4.2 WindowManagerImpl构建:
public class Application {
    @Override
    public Object getSystemService(String name) {
        return mBase.getSystemService(name);
    }
}

public class ContextImpl {
    @Override
    public Object getSystemService(String name) {
        return SystemServiceRegistry.getSystemService(this, name);
    }
}

public class Activity {
    @Override
    public Object getSystemService(@ServiceName @NonNull String name) {
        if (WINDOW_SERVICE.equals(name)) {
            return mWindowManager;
        } else if (SEARCH_SERVICE.equals(name)) {
            ...
        }
        return super.getSystemService(name);
    }
}
4.3 Dialog.show:
public void show() {
    /**
     * mShowing默认为false, 调用show()方法之后, mShowing = true, dismiss被触发时, mShowing = false;
     */
    if (mShowing) {
        return;
    }
    mCanceled = false;
    /**
     * 1. 默认为false, 触发dispatchOnCreate之后, mCreated = true;
     * 2. dispatchOnCreate内部会触发onCreate的执行;
     */
    if (!mCreated) {
        dispatchOnCreate(null);
    } else {
        final Configuration config = mContext.getResources().getConfiguration();
        mWindow.getDecorView().dispatchConfigurationChanged(config);
    }
    /**
     * 采用模板模式, 控制Dialog的生命周期;
     */
    onStart();
    /**
     * 这里采用的方式其实有些类似Activity, 我们在onCreate中执行setContentView方法, 然后
     * 该方法会构造一个DecorView出来, 这里的mWindow指向PhoneWindow, mDecor指向DecorView;
     */
    mDecor = mWindow.getDecorView();
    /**
     * 构建LayoutParams; 模块<4.4>
     * 结合模块<4.4>可知, Dialog窗口类型默认为应用级别;
     */
    WindowManager.LayoutParams l = mWindow.getAttributes();
    /**
     * 1. mWindowManager实际指向WindowManagerImpl;模块<4.5>
     * 2. onCreate内部通过setContentView构建DecorView树, 然后通过这里将DecorView与WindowManager
     *    进行绑定, 并绘制在界面上;
     */
    mWindowManager.addView(mDecor, l);
    mShowing = true;
    ...
} 
4.4 PhoneWindow.getAttributes:
public class PhoneWindow {
    private final WindowManager.LayoutParams mWindowAttributes = new WindowManager.LayoutParams();

    public final WindowManager.LayoutParams getAttributes() {
        return mWindowAttributes;
    }
}

public class WindowManager.LayoutParams {
    public LayoutParams() {
        /**
         * 创建Dialog会走这个方法, 也就是说Dialog的窗口类型默认为应用级别;
         */
        type = TYPE_APPLICATION;
    }
}
4.5 WindowManagerImpl.addView:
@Override
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
    /**
     * 对token进行赋值操作; 模块<4.6>
     */
    applyDefaultToken(params);
    mGlobal.addView(view, params, mDisplay, mParentWindow);
}

public void addView(View view, ViewGroup.LayoutParams params, Display display, Window parentWindow) {
    final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams) params;
    ...
    ViewRootImpl root;
    View panelParentView = null;
    synchronized (mLock) {
        ...          
        root = new ViewRootImpl(view.getContext(), display);
        ...
    }
    /**
     * 1. 将DecorView与ViewRootImpl进行绑定, 然后显示在WindowManager中; 
     * 2. 如果构建Dialog时传入Application会抛出上文所述异常, 而异常的原因就出在setView里面;模块<4.7>
     */
    root.setView(view, wparams, panelParentView);
}
4.6 WindowManagerImpl.applyDefaultToken:
private void applyDefaultToken(@NonNull ViewGroup.LayoutParams params) {
    /**
     * 结合模块<4.1> ~ <4.2>可知, 针对传入的Context不同, WMI有两种情况:
     * 1. 如果传入的Context属于Application, 则直接跳过<//1--->>处的if语句;  
     * 2. 如果传入的Context属于Activity, 则根据Activity启动流程可知, if内语句被赋值, 所以
     *    wparams.token不为空;
     */
    if (mDefaultToken != null && mParentWindow == null) {
        final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams) params;
//1--->
        if (wparams.token == null) {
            wparams.token = mDefaultToken;
        }
    }
}
4.7 ViewRootImpl.setView:
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
    synchronized (this) {
        mView = view;
        attrs = mWindowAttributes;
        /**
         * 1. 通过查看ViewRootImpl构造函数可知, mWindowSession实际指向Session;模块<4.8>
         * 2. 结合模块<4.8> 可知mWindowSession.addToDisplay最终调用WMS的addWindow方法;模块<4.9>
         */
        res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
                                getHostVisibility(), mDisplay.getDisplayId(),
                                mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
                                mAttachInfo.mOutsets, mInputChannel);
            ...
        if (res < WindowManagerGlobal.ADD_OKAY) {
            ...
            switch (res) {
                case WindowManagerGlobal.ADD_BAD_APP_TOKEN:
                case WindowManagerGlobal.ADD_BAD_SUBWINDOW_TOKEN:
                    throw new WindowManager.BadTokenException(
                                    "Unable to add window -- token " + attrs.token
                                    + " is not valid; is your activity running?");
                case WindowManagerGlobal.ADD_NOT_APP_TOKEN:
                    throw new WindowManager.BadTokenException(
                                    "Unable to add window -- token " + attrs.token
                                    + " is not for an application");
            }
        }
    }
}
4.8 Session.addToDisplay:
@Override
public int addToDisplay(IWindow window, int seq, WindowManager.LayoutParams attrs,
            int viewVisibility, int displayId, Rect outContentInsets, Rect outStableInsets,
            Rect outOutsets, InputChannel outInputChannel) {
    /**
     * mService指向的是WMS; 模块<4.9>
     */
    return mService.addWindow(this, window, seq, attrs, viewVisibility, displayId,
                outContentInsets, outStableInsets, outOutsets, outInputChannel);
}
4.9 WMS.addWindow:
public int addWindow(Session session, IWindow client, int seq,
            WindowManager.LayoutParams attrs, int viewVisibility, int displayId,
            Rect outContentInsets, Rect outStableInsets, Rect outOutsets,
            InputChannel outInputChannel) {
    int[] appOp = new int[1];
    /**
     * 1. 校验机制太长了, checkAddPermission触发窗口类型和权限机制的校验; 模块<4.10>;
     * 2. 结合模块<4.10>可知, res有以下几种类型:
     *    (1) WindowManagerGlobal.ADD_OKAY: 如果窗口类型为应用级别, 或者系统级别且对应权限已经
     *        进行了申请(包括动态申请/在清单文件中申请);
     *    (2) WindowManagerGlobal.ADD_PERMISSION_DENIED: 窗口级别为需要申请权限的系统级别, 但
     *        是没有进行权限申请时;
     * 3. 如果type为WindowManagerGlobal.ADD_PERMISSION_DENIED, 则直接返回该值, 反之在<//1>处
     *    继续进行具体的判断;
     */
    int res = mPolicy.checkAddPermission(attrs, appOp);
    if (res != WindowManagerGlobal.ADD_OKAY) {
        return res;
    }
//1--->
    /**
     * 结合模块<7>可知, type类型从大方面来说有三种: 系统级别、应用级别, 子窗口级别;
     */
    final int type = attrs.type;

    synchronized(mWindowMap) {
        ...
        boolean addToken = false;
        /**
         * token默认来自何处? 这里在回到Dialog的构造函数中;从Dialog构造函数<4.10>中了解到构造Dialog时, 
         * token默认被置为null, 然后如果Context instanceof ActivityContext时, taken会被赋值是因为
         * Dialog使用的是Activity的WML, 持有的token实际为Activity的token;
         */
        WindowToken token = mTokenMap.get(attrs.token);
        /**
         * 1. 能执行到这里, 说明res == WindowManagerGlobal.ADD_OKAY成立, 而该等式成立分三种情况:
         *   (1) 窗口类型为应用级别;
         *   (2) 窗口类型为系统级别, 且该系统级别不需要申请权限;
         *   (3) 窗口类型为系统级别, 且该系统级别需要申请权限, 而且确实已经申请了权限;
         * 2. 如果context属于Activity, 则token!=null, 如果context属于Application,
         *    则taken == null;
         */
        if (token == null) {
            /**
             * 1. 窗口类型为应用级别, 且token == null, 此时抛出该异常, 响应模块<4.7>
             */
            if (type >= FIRST_APPLICATION_WINDOW && type <= LAST_APPLICATION_WINDOW) {
                return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
            }
            ...
            /**
             * 1. 执行到这里说明: token == null, 且窗口级别为系统级别;
             * 2. 然后初始化token;
             */
            token = new WindowToken(this, attrs.token, -1, false);
            addToken = true;
        } 
        else if (type >= FIRST_APPLICATION_WINDOW && type <= LAST_APPLICATION_WINDOW) {...} 
        else if (type == TYPE_INPUT_METHOD) {...} 
        else if (type == TYPE_VOICE_INTERACTION) {...} 
        else if (type == TYPE_WALLPAPER) {...} 
        else if (type == TYPE_DREAM) {...} 
        else if (type == TYPE_ACCESSIBILITY_OVERLAY) {...} 
        else if (token.appWindowToken != null) {...}
        ...
        /**
         * 再次对type进行校验; 模块<4.11>
         */
        res = mPolicy.prepareAddWindowLw(win, attrs);
        if (res != WindowManagerGlobal.ADD_OKAY) {
            return res;
        }
        res = WindowManagerGlobal.ADD_OKAY;
    }
    return res;
}
4.10 PhoneWindowManager.checkAddPermission:
@Override
public int checkAddPermission(WindowManager.LayoutParams attrs, int[] outAppOp) {
    /**
     * 1. 结合上文---> attrs = PhoneWindow.getAttributes();模块<4.7>
     * 2. 结合模块<4.7>可知, Dialog窗口类型默认为应用级别, 即TYPE_APPLICATION, 直接在<//1--->>
     *    返回WindowManagerGlobal.ADD_OKAY;
     */
    int type = attrs.type;
//1--->
    /**
     * 如果这里if()内部执行语句为false, 也就是说当前窗口类型为系统级别或者是子窗口级别, 跳转至<//2--->>
     */
    if (type < FIRST_SYSTEM_WINDOW || type > LAST_SYSTEM_WINDOW) {
        return WindowManagerGlobal.ADD_OKAY;
    }
//2--->
    String permission = null;
    /**
     * 1. 如果窗口为系统级别, 在switch内部判断是否对具体系统级别申请了对应的权限; 
     * 2. 如果权限申请成功, 则返回type为WindowManagerGlobal.ADD_OKAY;
     * 3. 如果权限申请失败, 则返回type为WindowManagerGlobal.ADD_PERMISSION_DENIED;
     */
    switch (type) {
        case TYPE_TOAST:
            outAppOp[0] = AppOpsManager.OP_TOAST_WINDOW;
            break;
        case TYPE_DREAM:
        case TYPE_INPUT_METHOD:
        case TYPE_WALLPAPER:
        case TYPE_PRIVATE_PRESENTATION:
        case TYPE_VOICE_INTERACTION:
        case TYPE_ACCESSIBILITY_OVERLAY:
            break;
        case TYPE_PHONE:
        case TYPE_PRIORITY_PHONE:
        case TYPE_SYSTEM_ALERT:
        case TYPE_SYSTEM_ERROR:
        case TYPE_SYSTEM_OVERLAY:
            permission = android.Manifest.permission.SYSTEM_ALERT_WINDOW;
            outAppOp[0] = AppOpsManager.OP_SYSTEM_ALERT_WINDOW;
            break;
        default:
            permission = android.Manifest.permission.INTERNAL_SYSTEM_WINDOW;
    }
    if (permission != null) {
        if (permission == android.Manifest.permission.SYSTEM_ALERT_WINDOW) {
            final int callingUid = Binder.getCallingUid();
            if (callingUid == Process.SYSTEM_UID) {
                return WindowManagerGlobal.ADD_OKAY;
            }
            final int mode = mAppOpsManager.checkOp(outAppOp[0], callingUid, attrs.packageName);
            switch (mode) {
                case AppOpsManager.MODE_ALLOWED:
                case AppOpsManager.MODE_IGNORED:
                    return WindowManagerGlobal.ADD_OKAY;
                case AppOpsManager.MODE_ERRORED:
                    return WindowManagerGlobal.ADD_PERMISSION_DENIED;
                default:
                    if (mContext.checkCallingPermission(permission) != PackageManager.PERMISSION_GRANTED) {
                        return WindowManagerGlobal.ADD_PERMISSION_DENIED;
                    } else {
                        return WindowManagerGlobal.ADD_OKAY;
                    }
            }
        }

        if (mContext.checkCallingOrSelfPermission(permission) != PackageManager.PERMISSION_GRANTED) {
            return WindowManagerGlobal.ADD_PERMISSION_DENIED;
        }
    }
    return WindowManagerGlobal.ADD_OKAY;
}
4.11 PhoneWindowManager.prepareAddWindowLw:
@Override
public int prepareAddWindowLw(WindowState win, WindowManager.LayoutParams attrs) {
    switch (attrs.type) {
        case TYPE_STATUS_BAR:...break;
        case TYPE_NAVIGATION_BAR:...break;
        case TYPE_NAVIGATION_BAR_PANEL:
        case TYPE_STATUS_BAR_PANEL:
        case TYPE_STATUS_BAR_SUB_PANEL:
        case TYPE_VOICE_INTERACTION_STARTING:...break;
        case TYPE_KEYGUARD_SCRIM:...break;
    }
    return WindowManagerGlobal.ADD_OKAY;
}

五、如何避免在Dialog中使用ApplicationContext抛异常的问题:

5.1 改变Dialog窗口的级别类型:
在show方法执行之前;
public class Window {
    public void setType(int type) {
        final WindowManager.LayoutParams attrs = getAttributes();
        attrs.type = type;
        dispatchWindowAttributesChanged(attrs);
    }
}

DialogTest(@NonNull Context context) {
    super(context);
    getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_ALERT);
}
5.2 申请权限:
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>

或者直接将type类型改为Toast类型;

ublic class DialogTest extends Dialog {
    DialogTest(@NonNull Context context) {
        super(context);
        getWindow().setType(WindowManager.LayoutParams.TYPE_TOAST);
    }
}
上一篇下一篇

猜你喜欢

热点阅读