问题
接入第三方广告SDK后,我们不免需要展示一些插屏广告。但由于第三方的水平良莠不齐,部分SDK不支持插屏广告的自动消失,这种行为可能与自身产品相悖,所以我们需要将插屏隐藏掉。
这里有一点我们需要注意,一般的插屏广告,SDK提供的都是模版样式:广告的展示由SDK控制。这样就会出现一个问题:插屏的展示形式是多种多样的,没有进行统一,也就不容易进行抽象。
思路
在碰到上面的问题时,我先想到遍历ViewTree,找到关闭按钮位置,然后模拟点击该关闭按钮。但是,很快我就发现这样做有一些问题:
- SDK不一定提供关闭按钮;
- 提供的关闭按钮样式各种各样;
- 模拟点击不一定生效(SDK内部有可能在上层拦截掉事件);
一般情况下,模版插屏使用 Dialog 实现,那么我们是不是可以从 Dialog 的展示处入手那?不可避免的,我们需要去查看Dialog的源码。
源码分析
Dialog.java
private final WindowManager mWindowManager;
final Window mWindow;
// 构造方法
Dialog() {
....
mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
// Dialog 拥有自己的Window
final Window w = new PhoneWindow(mContext);
// 赋值
mWindow = w;
w.setCallback(this);
w.setOnWindowDismissedCallback(this);
w.setWindowManager(mWindowManager, null, null);
// Dialog默认都是内容居中展示的
w.setGravity(Gravity.CENTER);
}
// 展示
public void show() {
....
// 拿到Window的DecorView
mDecor = mWindow.getDecorView();
WindowManager.LayoutParams l = mWindow.getAttributes();
// 最终调用 WindowManagerGlobal的 addView 添加DecorView
mWindowManager.addView(mDecor, l);
mShowing = true;
sendShowMessage();
}
WindowManagerGlobal.java
public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow) {
if (view == null) {
throw new IllegalArgumentException("view must not be null");
}
if (display == null) {
throw new IllegalArgumentException("display must not be null");
}
if (!(params instanceof WindowManager.LayoutParams)) {
throw new IllegalArgumentException("Params must be WindowManager.LayoutParams");
}
....
ViewRootImpl root;
View panelParentView = null;
synchronized (mLock) {
// ?
int index = findViewLocked(view, false);
if (index >= 0) {
if (mDyingViews.contains(view)) {
// Don't wait for MSG_DIE to make it's way through root's queue.
mRoots.get(index).doDie();
} else {
throw new IllegalStateException("View " + view
+ " has already been added to the window manager.");
}
// The previous removeView() had not completed executing. Now it has.
}
// If this is a panel window, then find the window it is being
// attached to for future reference.
if (wparams.type >= WindowManager.LayoutParams.FIRST_SUB_WINDOW &&
wparams.type <= WindowManager.LayoutParams.LAST_SUB_WINDOW) {
final int count = mViews.size();
for (int i = 0; i < count; i++) {
if (mRoots.get(i).mWindow.asBinder() == wparams.token) {
panelParentView = mViews.get(i);
}
}
}
// 新建一个ViewRootImpl
root = new ViewRootImpl(view.getContext(), display);
view.setLayoutParams(wparams);
// 缓存DecorView
mViews.add(view);
// 缓存ViewRootImpl
mRoots.add(root);
// 缓存窗口的配置
mParams.add(wparams);
try {
// 将DecorView和ViewRootImpl进行绑定
root.setView(view, wparams, panelParentView);
} catch (RuntimeException e) {
// BadTokenException or InvalidDisplayException, clean up.
if (index >= 0) {
removeViewLocked(index, true);
}
throw e;
}
}
public static IWindowSession getWindowSession() {
synchronized (WindowManagerGlobal.class) {
if (sWindowSession == null) { // 类变量,作为hook点,动态代理
try {
// Emulate the legacy behavior. The global instance of InputMethodManager
// was instantiated here.
// TODO(b/116157766): Remove this hack after cleaning up @UnsupportedAppUsage
InputMethodManager.ensureDefaultInstanceForDefaultDisplayIfNecessary();
IWindowManager windowManager = getWindowManagerService();
sWindowSession = windowManager.openSession(
new IWindowSessionCallback.Stub() {
@Override
public void onAnimatorScaleChanged(float scale) {
ValueAnimator.setDurationScale(scale);
}
});
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
return sWindowSession;
}
}
}
ViewRootImpl.java
// 整个进程只有一个IWindowSession实例,所以适合作为hook点
final IWindowSession mWindowSession;
public ViewRootImpl(Context context, Display display) {
mWindowSession = WindowManagerGlobal.getWindowSession();
}
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
synchronized (this) {
if (mView == null) {
// 每个ViewRootImpl只有一个DecorView
mView = view;
....
// 添加成功
mAdded = true;
int res; /* = WindowManagerImpl.ADD_OKAY; */
....
// 执行绘制逻辑
requestLayout();
....
try {
// 将Window添加进行显示器上
res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
getHostVisibility(), mDisplay.getDisplayId(),
mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
mAttachInfo.mOutsets, mInputChannel);
} catch (RemoteException e) {
// 失败了
mAdded = false;
throw new RuntimeException("Adding window failed", e);
} finally {
if (restore) {
attrs.restore();
}
}
....
}
}
}
从上面简易的源码流程,我们不难发现,将window添加进屏幕上是最后一步,也就是 mWindowSession.addToDisplay 做的事情。如果我们hook 住 mWindowSession,是不是就可以通过动态代理拦截住 addToDisplay 的调用。
代码实现
public static void hookWindowSession() {
try {
final Class windowGlobalClz = Class.forName("android.view.WindowManagerGlobal");
Field instanceFiled = windowGlobalClz.getDeclaredField("sDefaultWindowManager");
instanceFiled.setAccessible(true);
final Object instance = instanceFiled.get(null);
Field sWindowSession = windowGlobalClz.getDeclaredField("sWindowSession");
sWindowSession.setAccessible(true);
final Object session = sWindowSession.get(null);
Class iWindowSessionClz = Class.forName("android.view.IWindowSession");
final Object proxyWindowSession = Proxy.newProxyInstance(iWindowSessionClz.getClassLoader(), new Class[]{iWindowSessionClz}, new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, final Object[] args) throws Throwable {
Log.e("hookWindow", "hook method is " + method.getName());
try {
if ("addToDisplay".equals(method.getName())) {
// 3秒后将window移除掉
new Handler().postDelayed(() -> {
try {
Method remove = iWindowSessionClz.getDeclaredMethod("remove", new Class[]{Class.forName("android.view.IWindow")});
remove.invoke(session, args[0]);
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
sWindowSession.set(instance, session);
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
},3000);
}
} catch (Exception exp) {
// exp.printStackTrace();
} finally {
return method.invoke(session, args);
}
}
});
sWindowSession.set(instance, proxyWindowSession);
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
上面的代码可以实现移除一个window。不过,有这样一个场景:假如在很短的时间内,会有多个 window 被添加到屏幕上,如何从这些window里面,找到我们想要删除的window那?
上面的问题,我想到的方法是抛出一个异常,从异常的堆栈里面过滤字符串,如果有命中我们匹配的规则(特殊字符串),则将对应的window移除掉。
public class WindowSessionException extends RuntimeException {
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
StackTraceElement[] stackTrace = getStackTrace();
for (int i = 0; i < stackTrace.length; i++) {
sb.append(stackTrace[i].toString() +"\n");
}
return sb.toString();
}
}
至于这个异常在哪个位置抛出,且制定命中规则的问题,我就不赘述了。
总结
阅读到这里,相信你已经知道了一个Dialog是如何在屏幕上展示,已经我们通过阅读源码,也知道怎样把这个window移除掉。不过,这种方式却存在一些问题,例如:1. 版本的兼容性;2. hook系统API,强制移除window,对整个系统的稳定性有什么影响;3. 如果规则不清晰,删错了window,对用户的影响不可逆;4. 如果是Dialog,强制移除window,不会触发onDismiss回调,周期不完整。
有一个稍微比较麻烦但相对安全的方法。我们拿到WindowManagerGlobal中的 mViews,然后倒序遍历这个数组,接着找到对应的ViewThree,从树中找到关闭按钮,模拟点击关闭按钮。
相对来说,第二个方法更安全一些,第一个方法稍微简单易懂一些,各有所长。
如果有更好的方法,请在评论区告诉小编一下吧。