Android-小知识
Android-小知识
Activity界面加载过程解析
Activity-->setContentView -> mWindow -> setContentView(window的setContentView方法)
mWindow初始化:
attach -> mWindow -> PolicyManager.makeNewWindow(获得window对象) --> policy.makeNewWindows{return PhoneWindow(context)}
PhoneWindow构造函数如下:
PhoneWindow(Context context){
super(context);
mLayoutInflater = LayoutInflater.from(context);
}
最终调用的方法是PhoneWindow中的setContentView方法
public void setContentView(int layoutResID) {
if (mContentParent == null) {
installDecor();
} else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
mContentParent.removeAllViews();
}
if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
getContext());
transitionTo(newScene);
} else {
mLayoutInflater.inflate(layoutResID, mContentParent);
}
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
cb.onContentChanged();
}
}
另外需要注意的是,invalidate()方法虽然最终会调用到performTraversals()方法中,但这时measure和layout流程是不会重新执行的,因为视图没有强制重新测量的标志位,而且大小也没有发生过变化,所以这时只有draw流程可以得到执行。而如果你希望视图的绘制流程可以完完整整地重新走一遍,就不能使用invalidate()方法,而应该调用 requestLayout()
了。这个方法中的流程比invalidate()
方法要简单一些,但中心思想是差不多的。
findViewById的流程
-> activity.findViewById
-> getWindow().findViewById(id)
-> window.findViewById
-> getDecorView().findViewById(id) -> installDecor() -> generateDecor() -> generateDecor() -> DecorView(context,-1){PhoneWindow的内部类 extends FrameLayout}
-> view.findViewById()
-> view.findViewTraversal()
/**
* findViewById的起点匹配View中的mID变量
* @param id the id of the view to be found
* @return the view of the specified id, null if cannot be found
*/
protected View findViewTraversal(int id) {
if (id == mID) {
return this;
}
return null;
}
View和ViewGroup
invalidate() 触发view重绘
在布局文件中设置大小属性失败原因
为什么我们在布局文件中直接改变一个按钮的宽度,如按钮的宽度改成300dp,高度改成80dp,但重新运行程序来观察效果,却发现并没有什么变化。
其实不管你将Button
的layout_width
和layout_height
的值修改成多少,都不会有任何效果的,因为这两个值现在已经完全失去了作用。平时我们经常使用layout_width
和layout_height
来设置View的大小,并且一直都能正常工作,就好像这两个属性确实是用于设置View的大小的。而实际上则不然,它们其实是用于设置View在布局中的大小的,也就是说,首先View必须存在于一个布局中,之后如果将layout_width
设置成match_parent
表示让View
的宽度填充满布局,如果设置成wrap_content
表示让View的宽度刚好可以包含其内容,如果设置成具体的数值则View的宽度会变成相应的数值。这也是为什么这两个属性叫作layout_width
和layout_height
,而不是width和height。
布局加载
inflate()的作用就是将一个用xml定义的布局文件加载出来,注意与findViewById()的区别,inflate是加载一个布局文件,而findViewById则是从布局文件中查找一个控件。
- 获取LayoutInflater对象有三种方法
LayoutInflater inflater=LayoutInflater.from(this);
LayoutInflater inflater=getLayoutInflater();
LayoutInflater inflater=(LayoutInflater)this.getSystemService(LAYOUT_INFLATER_SERVICE);
关于LayoutInflater类inflate(int resource, ViewGroup root, boolean attachToRoot)方法三个参数的含义:
- resource:需要加载布局文件的id,意思是需要将这个布局文件中加载到Activity中来操作。
- root:需要附加到resource资源文件的根控件,什么意思呢,就是inflate()会返回一个View对象,如果第三个参数attachToRoot为true,就将这个root作为根对象返回,否则仅仅将这个root对象的LayoutParams属性附加到resource对象的根布局对象上,就是返回resource对象实例化的组件。
- attachToRoot:是否将root附加到布局文件的根视图上(如果传true,系统会多创建一个多余的root来装载需要加载的组件,即传true返回root,传false返回resource加载的组件。)
UI小知识
如何监听EditText的文本变化
关键函数:public void addTextChangedListener(TextWatcher watcher)
在Activity启动的时候去获取某个View的dimension信息
因为View的measure过程和Activity的生命周期方法不是同步执行的,所以无法保证Activity执行了onCreate、onStart、onResume时某个View已经测量完毕。这个时候,三个周期方法拿到的dimension可能都是0。下面有四个方法可以解决这个问题:
- Activity/View#onWindowFocusChanged
onWindowFocusChanged
这个方法的含义是:View已经初始化完毕了,宽/高已经准备好。但有个问题,当前Activity得到或失去焦点时 都会导致onWindowFocusChanged
被调用,所以当Activity频繁切换的时候,这个方法会被频繁调用。 - view.post(runnable)
通过post可以将一个runnable投递到消息队列的尾部,然后等待Looper调用,但被调用到的时候,View已经初始化好了。 - ViewTreeObserver
使用ViewTreeObserver
的众多回调可以完成这个功能,比如使用OnGlobalLayoutListener
这个接口,当View - 在重写view.measure方法,获取View的宽度(PS:虽然测量的宽度并不一定是View最后的宽度,但一般都是最终的宽度)
Activity基础
activity是什么
Activity是用于与用户交互的组件,所以Activity会提供窗口来和用户进行交互。
Activity全都是用task来管理的,一个task里有多个activity,这些activity按照启动的顺序存入task中(后进先出)。Android默认会为每个应用创建一个task来管理该应用的activity,task的名字一般都是应用的包名packagename
。
我们可用通过AndroidManifest.xml
中activity的taskAffinity属性来单独定义管理该activity的task,但如果其他应用申明了相同的task,就有可能启动到该activity,就会带来安全问题(显而易见,这里Intent被其他应用获取到)。
Activity的生命周期
- 打开一个Activity实例的时候,系统会依次调用:
onCreate ==> onStart ==> onResume ==> 取得焦点进入交互状态
- 当其他activity启动或者锁屏的时候
activity依旧在前台运行,onPause被调用。
该方法执行activity暂停,通常用于提交未保存的更改到持久化数据,停止动画和其他的东西。但这个activity还是完全活着(它保持所有的状态和成员信息,并保持连接到窗口管理器)
- 接下来 这个activity有三种可能
用户返回到该activity就调用onResume()方法重新取得焦点
//
//
======用户回到桌面或是打开其他activity,调用onStop()进入停止状态(对用户不可见)
\\
\\
系统内存不足,拥有更高限权的应用需要内存,该activity的进程就可能会被系统回收。(回收onPause()和onStop()状态的activity进程)要想重新打开就必须重新创建一遍。
- 如果用户重新回到onStop()状态的activity(又显示在前台了),如下生命周期方法会被调用
onRestart ==> onStart ==> onResume
activity结束(主动调用finish ())或是被系统杀死之前会调用onDestroy()方法释放所有占用的资源。
activity生命周期中三个嵌套的循环
- activity的完整生存期会在 onCreate() 调用和 onDestroy() 调用之间发生。
- activity的可见生存期会在 onStart() 调用和 onStop() 调用之间发生。系统会在activity的整个生存期内多次调用 onStart() 和onStop(),因为activity可能会在显示和隐藏之间不断地来回切换。
- activity的前后台切换会在 onResume() 调用和 onPause() 之间发生。 因为这个状态可能会经常发生转换,为了避免切换迟缓引起的用户等待,不要添加耗时操作。
activity被回收的状态和信息保存和恢复过程
onSaveInstanceState方法
在activity可能会被回收前调用,用于保存相关状态和信息,以便于回收后现场重建时的数据恢复(在onRestoreInstanceState
或onCreate
中恢复)。但处于onPause
或onStop
状态的activity,被回收的时候不一定会被调用。
系统灵活的来决定调不调用该方法,但是如果要调用就一定发生在onStop方法之前,但并不保证发生在onPause的前面还是后面。
onRestoreInstanceState方法
这个方法在onStart
和 onPostCreate
之间调用,在onCreate
中也可以状态恢复,但有时候需要所有布局初始化完成后再恢复状态。
onPostCreate
:一般不实现这个方法,当程序的代码开始运行时,它调用系统做最后的初始化工作。
启动模式
启动模式的作用
决定activity和task之间的关系。具体一些作用:
- 让某个 activity 启动一个新的 task (而不是被放入当前 task )
- 让 activity 启动时只是调出已有的某个实例(而不是在 back stack 顶创建一个新的实例)
- 你想在用户离开 task 时只保留根 activity,而 back stack 中的其它 activity 都要清空
四种启动模式对应的作用
- standard:
这种模式下,Android总会为目标 Activity创建一个新的实例,并将该Activity添加到当前Task栈中。(不会启动新的栈) - singleTop:
该模式和standard模式基本一致,有一点不同:当将要被启动的Activity已经位于Task栈顶时,系统不会重新创建目标Activity实例,而是直接复用Task栈顶的Activity。 - singleTask:
保持task中只有一个实例,如果要启动的activity不存在,那么系统将会创建该实例,并将其加入Task栈顶;如果已存在,直接复用Task内的Activity;如果没有位于栈顶,那么系统会把位于该Activity上面的所有其他Activity全部移出Task,从而使得该目标Activity位于栈顶。 - singleInstance
用该模式启动的Activity,系统将会创建一个全新的Task栈来装载该实例(全局单例)。如果将要启动的Activity已存在,那么无论它位于哪个应用程序,哪个Task中;系统都会把该Activity所在的Task转到前台,从而使该Activity显示出来。
如何使用
通过 manifest 文件
通过manifest文件在activity声明时,添加launchMode 属性来设定 activity 与 task 的关系。这种方式只是设定启动模式,但任然能被启动activity Intent修改。
<activity
...
android:launchMode="standard">
...
</activity>
使用 Intent
在要启动 activity 时,你可以在传给 startActivity() 的 intent 中包含相应标志,以修改 activity 与 task 的默认关系。
Intent i = new Intent(this,NewActivity.class);
i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(i);
与启动模式对应:
- FLAG_ACTIVITY_NEW_TASK — singleTask
- FLAG_ACTIVITY_SINGLE_TOP — singleTop
- FLAG_ACTIVITY_CLEAR_TOP — 与singleTask类型
此种模式在launchMode中没有对应的属性值。如果要启动的 activity 已经在当前 task 中运行,则不再启动一个新的实例,且所有在其上面的 activity 将被销毁。