我们知道view是安卓中视图的呈现方式,但是view不能单独存在,他必须依附在window这个抽象概念上,因此有视图的地方就有window。
我们都清楚安卓提供视图的地方有Activity,Dialog,Toast,还有一些依托window而实现的视图如popUpWindow等。其实这些就对应三种window类型,应用window、子系统、系统window。接下来便总结下Activity,Dialog,Toast的对应window创建流程。
要分析Activity中的Window创建过程,必须了解activity的启动过程,这里我们大概了解下即可。
Activty的启动最终会在ActivtyThread的performLaunchActivty()中完成(参看下面源码)
private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
// .....
ContextImpl appContext = createBaseContextForActivity(r);
Activity activity = null;
try {
// 获取类加载器
java.lang.ClassLoader cl = appContext.getClassLoader();
// 反射获取一个Activty实例
activity = mInstrumentation.newActivity(
cl, component.getClassName(), r.intent);
// ......
try {
// ......
if (activity != null) {
// ......
// 调用attach()给activty关联变量
activity.attach(appContext, this, getInstrumentation(), r.token,
r.ident, app, r.intent, r.activityInfo, title, r.parent,
r.embeddedID, r.lastNonConfigurationInstances, config,
r.referrer, r.voiceInteractor, window, r.configCallback);
// ......
return activity;
}
通过源码我们知道:
1、通过获得类加载器来获得activity的实例
2、调用activity的attach方法为其关联运行过程中依赖的上下文变量
继续深入attach()源码:
final void attach(/*一系列的参数,太长了......*/) {
// ......
// 创建Window对象 ,PolicyManager为策略类,其实现类Policy 的makeNewWindow内部创建了window对象
mWindow = PolicyManager.makeNewWindow(this)
mWindow.setCallback(this);
mWindow.setOnWindowDismissedCallback(this);
// ......初始化Window中的部分内容
// ......赋值各种参数
;
}
Policy 的makeNewWindow:
public window makeNewWindow(Context context){
return new PhoneWindow(context);
}
通过attach源码我们可以知道:
1、 PolicyManager.makeNewWindow(this)主要功能创建window对象
2、PolicyManager为策略类,其实现类Policy 的makeNewWindow内部创建了window(PhoneWindow)对象
ps:PhoneWindow为window的唯一实现类
window被创建成功后就是activity被加载到window的过程了,activity的视图从setContentView开始
public void setContentView(@LayoutRes int layoutResID) {
getWindow().setContentView(layoutResID);
initWindowDecorActionBar();
}
....
public Window getWindow() {
return mWindow;// 这个window就是PhoneWindow对象
}
...
mWindow = new PhoneWindow(this, window, activityConfigCallback);
可以知道具体的实现类是由PhoneWindow来实现的,所以看setContentView源码:
@Override
public void setContentView(int layoutResID) {
// 初始化DecorView
if (mContentParent == null) {
installDecor();
} else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
// 如果已经创建过DecorView结构,已经有了content,就移除
mContentParent.removeAllViews();
}
if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
// ......
} else {
// mContentParent中加载传入的布局资源
mLayoutInflater.inflate(layoutResID, mContentParent);
}
// ......
if (cb != null && !isDestroyed()) {
// 回调通知Activity视图已经改变
cb.onContentChanged();
}
// ......
}
installDecor的源码:
private void installDecor() {
mForceDecorInstall = false;
if (mDecor == null) {
// 如果还没有DecorView,调用generateDecor()创建
mDecor = generateDecor(-1);
// ......
} else {
// 如果有DecorView对象了,就关联此Window
mDecor.setWindow(this);
}
if (mContentParent == null) {
// 赋值mContentParent
mContentParent = generateLayout(mDecor);
// 初始化布局......
}
}
generateLayout 源码:
protected ViewGroup generateLayout(DecorView decor) {
// 根据Window的R.styleable等参数设置Window的flag、params、layoutResource......
mDecor.startChanging();
// DecorView加载布局
mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);
// ID_ANDROID_CONTENT = com.android.internal.R.id.content
// setContentView()布局加载的ViewGroup
ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
// ......
mDecor.finishChanging();
return contentParent;
}
1、通过installDecor来创建DecorView对象(内部调用generateDecor方法完成)
2、phoneWindow通过generateLayout加载具体的布局到DecorView(generateLayout代码)
3、ID_ANDROID_CONTENT这个id就是contentParent容器的id
4、decorview创建好后就是将view添加到decorview的contentParent容器中(phoneWindow的setContentView源码:mLayoutInflater.inflate(layoutResID, mContentParent);)
5、回调activity的onContentChanged通知activity视图改变(phoneWindow的setContentView源码中)
补充:
DecorView是一个FrameLayout,activity中的顶级view(参考下图理解)
DecorView包括标题栏和内容栏,标题栏受安卓系统和主题不同而不同,内容栏就是我们setContentView设置view添加之处。且内容栏是个容器,有固定的id:R.android.id.content
经过上面的几步后decorview已经被创建完毕,activity的布局文件也被加载到Decorview的contentParent容器中。但是这个时候decorview还没被 WindowManager正式添加到window中。
虽然前文中说到 在activity的attach方法中 window被创建。这时候Decorview还没被WindowManager识别。
等activityThread的handleResumeActivty方法中进行处理后用户才可以被用户看到view(参看源码)
activityThread的handleResumeActivty方法
final void handleResumeActivity(IBinder token,
boolean clearHide, boolean isForward, boolean reallyResume, int seq, String reason) {
// 调用Activity的makeVisible()
r.activity.makeVisible();
// ......
}
void makeVisible() {
if (!mWindowAdded) {
ViewManager wm = getWindowManager();
wm.addView(mDecor, getWindow().getAttributes());
mWindowAdded = true;
}
mDecor.setVisibility(View.VISIBLE);
}
1、添加到window
2、设置view可见
首先看Dialog构造
...
mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
final Window w = new PhoneWindow(mContext);
mWindow = w;
w.setCallback(this);
w.setOnWindowDismissedCallback(this);
w.setOnWindowSwipeDismissedCallback(() -> {
if (mCancelable) {
cancel();
}
});
w.setWindowManager(mWindowManager, null, null);
w.setGravity(Gravity.CENTER);
mListenersHandler = new ListenersHandler(this);
public void setContentView(@LayoutRes int layoutResID) {
mWindow.setContentView(layoutResID);
}
public void show() {
// ......
// 监听通知
onStart();
mDecor = mWindow.getDecorView();
// ......
WindowManager.LayoutParams l = mWindow.getAttributes();
// ......
// 添加Window
mWindowManager.addView(mDecor, l);
mShowing = true;
sendShowMessage();
}
@Override
public void dismiss() {
if (Looper.myLooper() == mHandler.getLooper()) {
dismissDialog();
} else {
mHandler.post(mDismissAction);
}
}
void dismissDialog() {
if (mDecor == null || !mShowing) {
return;
}
if (mWindow.isDestroyed()) {
Log.e(TAG, "Tried to dismissDialog() but the Dialog's window was already destroyed!");
return;
}
try {
// 移除Window
mWindowManager.removeViewImmediate(mDecor);
} finally {
if (mActionMode != null) {
mActionMode.finish();
}
// 置空回收
mDecor = null;
mWindow.closeAllPanels();
onStop();
mShowing = false;
sendDismissMessage();
}
}
上面的前三步思路是不是和activity的window创建过程很像。这里注意:
普通的Dialog有个特殊指出就是context必须是Activity的context如果采用application的context就会报错。原因是没有应用token所导致的。而应用token一般只有activity拥有,所以只需要使用activity来作为context显示对话框即可。
另外系统window比较特殊,不需要token,所以我们为对话框指定window类型为系统window类型也可以正常显示对话框。但是要加权限哦。如下:
// 代码中:
dialog.getWindow().setType(LayoutParams.TYPE_SYSTEM_ERROR);
// 清单文件:
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
1、Toast属于系统Window
2、Toast有定是取消的功能,所以使用了Handler
3、内部会有两类IPC过程:
- Toast访问NotificationManagerService(简称NMS)的过程
- NotificationManagerService回调Toast里的TN接口
我们平时创建吐司十分简单
Toast.makeText(this, "xxx", Toast.LENGTH_SHORT).show();
public void show() {
if (mNextView == null) {
throw new RuntimeException("setView must have been called");
}
INotificationManager service = getService();
String pkg = mContext.getOpPackageName();
TN tn = mTN;
tn.mNextView = mNextView;
try {
// 调用NMS中的enqueueToast 参数 包名,TN回调,显示时长
service.enqueueToast(pkg, tn, mDuration);
} catch (RemoteException e) {
// Empty
}
}
public void cancel() {
mTN.cancel();
}
1、从上面的代码看显示隐藏都需要NMS来实现,由于NMS运行在系统进程中所以只能通过远程调用的方式显示隐藏吐司。
2、注意TN这个类他是一个Binder类,在Toast和NMS进行IPC过程中当NMS处理Toast显示或者隐藏的请求时会跨进程回调TN中的方法。这时由于TN运行在Binder线程池中,所以需要通过Handler将其切换到当前线程(当前线程:发送Toast请求所在线程)
3、由于使用了Handler所以意味着Toast无法在没有Looper的线程中弹出(Handler使用Looper才能完成线程切换)
@Override
public void enqueueToast(String pkg, ITransientNotification callback, int duration) {
// ......
synchronized (mToastQueue) {
// ......
try {
// Toast的封装对象
ToastRecord record;
int index;
if (!isSystemToast) {
// 如果不是系统应用的Toast,就遍历ToastRecord集合。
// 看是否存在来自于同一个包的Toast。如果存在就返回对应下标,没有就返回-1。
index = indexOfToastPackageLocked(pkg);
} else {
// 如果是系统Toast,除了对比包名还要对比是不是同一个callback,返回下标。
index = indexOfToastLocked(pkg, callback);
}
if (index >= 0) {
// 如果找到了,就直接跟新内容和时间。
record = mToastQueue.get(index);
record.update(duration);
record.update(callback);
} else {
// 创建一个token,显式加入WMS,显式加入就需要显式移除,在后面会看到。
Binder token = new Binder();
mWindowManagerInternal.addWindowToken(token, TYPE_TOAST, DEFAULT_DISPLAY);
// 如果不存在,就新创一个ToastRecord,并加入集合。
record = new ToastRecord(callingPid, pkg, callback, duration, token);
mToastQueue.add(record);
index = mToastQueue.size() - 1;
}
keepProcessAliveIfNeededLocked(callingPid);
// 如果添加前还没有ToastRecord,就开始显示
if (index == 0) {
showNextToastLocked();
}
} finally {
Binder.restoreCallingIdentity(callingId);
}
}
}
1、首先把吐司对象封装为ToastRecord 对象
2、ToastRecord 添加到mToastQueue队列
3、NMS通过showNextToastLocked显示吐司(源码如下)
void showNextToastLocked() {
// 第一个Toast
ToastRecord record = mToastQueue.get(0);
while (record != null) {
if (DBG) Slog.d(TAG, "Show pkg=" + record.pkg + " callback=" + record.callback);
try {
// 调用TN接口的show(),在应用的Binder线程中执行
record.callback.show(record.token);
// 发送一个延时消息,取决于Toast显示时长
scheduleTimeoutLocked(record);
return;
} catch (RemoteException e) {
// ......
// 处理IPC调用失败,显示失败的情况
int index = mToastQueue.indexOf(record);
if (index >= 0) {
// 如果显示失败的Record还在队列中就移除
mToastQueue.remove(index);
}
keepProcessAliveIfNeededLocked(record.pid);
// 如果队列中还有Record,就继续处理下一个
if (mToastQueue.size() > 0) {
record = mToastQueue.get(0);
} else {
record = null;
}
}
}
}
1、toast的显示是由ToastRecord的callbak来完成的,调用TN接口的show来完成的
callback其实就是Toast中TN对象的远程binder
2、NMS通过scheduleTimeoutLocked发送延时消息(具体时长取决于Toast时长)
系统时长为3.5秒和2秒两种
3、延时相应的时间后NMS会通过cancelToastLocked来隐藏Toast并把他从队列中移除
void cancelToastLocked(int index) {
ToastRecord record = mToastQueue.get(index);
try {
// 远程调用TN的hide()
record.callback.hide();
} catch (RemoteException e) {
// ......
}
// 将这个record从Toast队列中移除
ToastRecord lastToast = mToastQueue.remove(index);
// 调用WMS显式移除Window的token,这个token是在enqueueToast()中添加的
mWindowManagerInternal.removeWindowToken(lastToast.token, true, DEFAULT_DISPLAY);
keepProcessAliveIfNeededLocked(record.pid);
// 如果Toast队列中还有Record,就调用继续执行下一个
if (mToastQueue.size() > 0) {
showNextToastLocked();
}
}
record.callback.hide();也是record的callback来完成,远程调用TN的hide()。
通过磕磕绊绊的总结也明白了一些东西,还有一些东西也不太明白,总体来说收获不少,以后慢慢消化。。。。溜。
参考文章:https://www.jianshu.com/p/1e62a6412db2
本文来自<安卓开发艺术探索>笔记总结