Android面试整理
Activity中的四种启动模式
介绍前须知:
- 任务栈用来存放用户开启的Activity。
- 在应用程序创建之初,系统会默认分配给其一个任务栈(默认一个),并存储根Activity。
- 同一个Task Stack,只要不在栈顶,就是onStop状态。 任务栈的id自增长型,是Integer类型。
- 新创建Activity会被压入栈顶。点击back会将栈顶Activity弹出,并产生新的栈顶元素作为显示界面(onResume状态)。
5.当Task最后一个Activity被销毁时,对应的应用程序被关闭,清除Task栈,但是还会保留应用程序进程(狂点Back退出到Home界面后点击Menu会发现还有这个App的框框。个人理解应该是这个意思),再次点击进入应用会创建新的Task栈。
启动模式:
standard: 标准模式这个是android默认的Activity启动模式,每启动一个Activity都会被实例化一个Activity,并且新创建的Activity在堆栈中会在栈顶。
singleTop: 栈顶复用模式 如果当前要启动的Activity就是在栈顶的位置,那么此时就会复用该Activity,并且不会重走onCreate方法,会直接它的onNewIntent方法,如果不在栈顶,就跟standard一样的。如果当前activity已经在前台显示着,突然来了一条推送消息,此时不想让接收推送的消息的activity再次创建,那么此时正好可以用该启动模式,如果之前activity栈中是A–>B–>C如果点击了推动的消息还是A–>B–C,不过此时C是不会再次创建的,而是调用C的onNewIntent。而如果现在activity中栈是A–>C–>B,再次打开推送的消息,此时跟正常的启动C就没啥区别了,当前栈中就是A–>C–>B–>C了。
singleTask:栈内复用模式 该种情况下就比singleTop厉害了,不管在不在栈顶,在Activity的堆栈中永远保持一个。这种启动模式相对于singleTop而言是更加直接,比如之前activity栈中有A–>B–>C—D,再次打开了B的时候,在B上面的activity都会从activity栈中被移除。下面的acitivity还是不用管,所以此时栈中是A–>B,一般项目中主页面用到该启动模式
singleInstance: 全局唯一模式 该种情况就用得比较少了,主要是指在该activity永远只在一个单独的栈中。一旦该模式的activity的实例已经存在于某个栈中,任何应用在激活该activity时都会重用该栈中的实例,解决了多个task共享一个activity。其余的基本和上面的singleTask保持一致。在该模式下,我们会为目标Activity分配一个新的affinity,并创建一个新的Task栈,将目标Activity放入新的Task,并让目标Activity获得焦点。新的Task有且只有这一个Activity实例。 如果已经创建过目标Activity实例,则不会创建新的Task,而是将以前创建过的Activity唤醒(对应Task设为Foreground状态)
事件分发
拥有事件传递功能的类
- Activity:拥有dispatchTouchEvent和onTouchEvent方法
- View:拥有dispatchTouchEvent和onTouchEvent方法
- ViewGroup:拥有dispatchTouchEvent、onTouchEvent、onInterceptTouchEvent
触摸事件的类型
- ACTION_DOWN : 手指的按下操作
- ACTION_MOVE:手指按下后,松开手之前,轻微移动所触发的事件
- ACTION_UP:手指离开屏幕的操作
事件分发主要分三块:分发、拦截、消费;
事件分发:public boolean dispatchTouchEvent(MotionEvent ev)
Touch事件发生时Activity的dispatchTouchEvent(MotionEvent ev)方法会将事件传递给最外层View的dispatchTouchEvent(MotionEvent ev)方法,该方法对事件进行分发。分发逻辑如下:
-
如果return true,事件会由当前View的dispatchTouchEvent方法进行消费,同时事件会停止向下传递;
-
如果return false,事件分发分为两种情况:
如果当前 View 获取的事件直接来自 Activity,则会将事件返回给Activity的onTouchEvent进行消费;
如果当前 View 获取的事件来自外层父控件,则会将事件返回给父View的onTouchEvent进行消费。 -
如果return super.dispatchTouchEvent(ev),事件分发分为两种情况:
如果当前View是ViewGroup,则事件会分发给onInterceptTouchEvent方法进行处理;
事件拦截:public boolean onInterceptTouchEvent(MotionEvent ev)
方法只有ViewGroup才有, Activity与普通View没有。上面已经提到,如果当前ViewGroup的dispatchTouchEvent(事件分发)返回super.dispatchTouchEvent(ev), 那么事件会传递到传递到onInterceptTouchEvent方法, 该方法对事件进行拦截。拦截逻辑如下:
-
如果return true,则表示拦截该事件,并将事件交给当前View的onTouchEvent方法;
-
如果return
false,则表示不拦截该事件,并将该事件交由子View的dispatchTouchEvent方法进行事件分发,重复上述过程;如果return super.onInterceptTouchEvent(ev), 事件拦截分两种情况:
-
如果该View(ViewGroup)存在子View且点击到了该子View, 则不拦截, 继续分发给子View 处理,
此时相当于return false。
- 如果该View(ViewGroup)没有子View或者有子View但是没有点击中子View(此时ViewGroup相当于普通View),
则交由该View的onTouchEvent响应,此时相当于return true。
一般的LinearLayout、 RelativeLayout、FrameLayout等ViewGroup默认不拦截, 而ScrollView、ListView等ViewGroup则可能拦截,得看具体情况。
事件响应:public boolean onTouchEvent(MotionEvent ev)
上面已经提到,在dispatchTouchEvent(事件分发)返回super.dispatchTouchEvent(ev)并且onInterceptTouchEvent进行拦截(事件拦截返回true)的情况下,那么事件会传递到onTouchEvent方法,该方法对事件进行响应。响应逻辑如下:
-
如果return true,则表示响应并消费该事件;
-
如果return
fasle,则表示不响应事件,那么该事件将会不断向上层View的onTouchEvent方法传递,直到某个View的onTouchEvent方法返回true,如果到了最顶层View还是返回false,那么认为该事件不消耗,则在同一个事件系列中,当前View无法再次接收到事件,该事件会交由Activity的onTouchEvent进行处理;如果return super.dispatchTouchEvent(ev),事件处理分为两种情况:
- 如果该View是clickable或者longclickable的,则会返回true, 表示消费了该事件, 与返回true一样;
- 如果该View不是clickable或者longclickable的,则会返回false,
表示不消费该事件,将会向上传递,与返回false一样.
结论: 无论是View还是ViewGroup,不管他是DispatchTouchEvent还是onTouchEvent方法,方法返回true、返回false的处理逻辑都是一样的,只是调用父类的同名方法的时候处理的逻辑有所不同,View偏重消费、ViewGourp偏重分发
面试:
需要总结的小点:
1、Android 事件分发总是遵循 Activity => ViewGroup => View 的传递顺序;
2、onTouch() 执行总优先于 onClick()
事件分发的过程用到哪些方法
:有 dispatchTouchEvent 、onTouchEvent 、 onInterceptTouchEvent ;ViewGroup 在调用 dispatchTouchEvent 进行事件分发时,会适时调用 onInterceptTouchEvent ,来判断是否能拦截这个事件。相应如果不想 ViewGroup 拦截事件,可以调用 ViewGroup 的 requestDisallowInterceptTouchEvent方法,传 true 就是禁止拦截,false 你开心就拦吧;常用来解决一些嵌套 View 的事件冲突。
讲讲 Android 的事件分发机制?
答:本会遵从 Activity => ViewGroup => View 的顺序进行事件分发,然后通过调用 onTouchEvent() 方法进行事件的处理。我们在项目中一般会对 MotionEvent.ACTION_DOWN,MotionEvent.ACTION_UP,MotionEvent.ACTION_MOVE,MotionEvent.ACTION_CANCEL 分情况进行操作。
在一个列表中,同时对父 View 和子 View 设置点击方法,优先响应哪个?为什么会这样?
子view先响应
人为不干预的情况下,事件分发是从父view到子view 消费从子view;
分发从外到内,消费从里到外!
滑动冲突解决方案:
比如 RecyclerView 嵌套 RecyclerView,直接通过相关方法禁掉内部 RecyclerView 的滑动;ScrollView 嵌套 RecyclerView 直接把 ScrollView 替换为 NestedScrollView 等等
禁掉Recycler
true—可以滑动
false—禁止滑动
LinearLayoutManager manager = new LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL,false){
@Override
public boolean canScrollHorizontally() {
return false;
}
};
ScrollView 嵌套RecyclerView
ScrollView 替换为NestedScrollView
NestedScrollView 与 ScrollView 的区别就在于 NestedScrollView 支持 嵌套滑动,无论是作为父控件还是子控件,嵌套滑动都支持,且默认开启。
滑动卡顿解决方案
//使自身size不受adapt影响
recyclerView.setHasFixedSize(true);
recyclerView.setNestedScrollingEnabled(false);
setNestedScrollingEnabled调用如下
@Override
public void setNestedScrollingEnabled(boolean enabled) {
getScrollingChildHelper().setNestedScrollingEnabled(enabled);
}
自定义view解决滑动冲突
外部拦截法
所谓外部拦截法,顾名思义,就是直接在父容器中直接拦截掉我们的滑动事件,让其不能进入到子元素中,这似乎和我们 RecyclerView 嵌套 RecyclerView 时禁用内部 RecyclerView 滑动有那么一丝相似之处,就是内部不处理就完事儿了。但细细品来又完全不一样,这里的外部拦截法会让内部元素根本就收不到滑动事件。
这种方法明显非常适合我们上面讲的事件分发机制。我们在接收 ACTION_MOVE 事件的时候,直接通过使 onInterceptTouchEvent() 方法返回 true 来直接拦截掉事件就可以了,伪代码想必大家也知道了:
@Override
public boolean onInterceptTouchEvent(MotionEvent action) {
if (action == MotionEvent.ACTION_MOVE && 父容器需要点击事件) {
return true;
}
return super.onInterceptTouchEvent(action);
}
代码很简单,我们仅仅需要在事件 ACTION_MOVE 时去处理我们的逻辑就好了,当满足我们的逻辑的时候,就拦截掉 ACTION_MOVE 事件给自己处理。
内部拦截法
内部拦截法相对外部拦截法会复杂一些,所以我们通常来说,都更加推荐用外部拦截法进行处理。不过,内部拦截法依然有着它非常重要的地位,具体情况有可能会遇到。
内部拦截法的话,需要 requestDisallowInterceptTouchEvent() 方法的支持,这个方法是干什么的呢?顾名思义,请求是否不允许拦截事件,其接收一个 boolean 参数,表示是否不允许拦截。
我们直接重写子元素的 dispatchTouchEvent() 方法,得到伪代码如下:
//子view的代码·
public boolean dispatchTouchEvent(MotionEvent ev) {
int y= (int) ev.getY();
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
getParent().requestDisallowInterceptTouchEvent(true);
yDown=y;
break;
case MotionEvent.ACTION_MOVE:
yMove=y;
Log.e("mes",yMove+"!!!");
int scrollY = getScrollY();
if (scrollY == 0&&yMove-yDown>0) { //根据业务需求判断是否需要通知父viewgroup来拦截处理该事件
//允许父View进行事件拦截
Log.e("mes",yMove-yDown+"拦截");
getParent().requestDisallowInterceptTouchEvent(false);
}
break;
case MotionEvent.ACTION_UP:
break;
}
return super.dispatchTouchEvent(ev);
}
Handler
- 什么是Handler
handler是Android给我们提供用来更新UI的一套机制,也是一套消息处理机制,我们可以发消息,也可以通过它 处理消息。Android Framework 架构中的一个 基础组件,用于 子线程与主线程间的通讯,实现了一种 非堵塞的消息传递机制。 - 那为什么要用handler呢?我能不能不用?
肯定是不行的。因为android在设计的时候就封装了一套消息创建、传递、处理。如果不遵循就不能更新UI信息,就会报出异常。 - Android为什么要设计只能用handler机制更新UI呢?
最根本的目的就是为了解决多线程并发的问题!
打个比方,如果在一个activity中有多个线程,并且没有加锁,就会出现界面错乱的问题。但是如果对这些更新UI的操作都加锁处理,又会导致性能下降。
处于对性能的问题考虑,Android给我们提供这一套更新UI的机制我们只需要遵循这种机制就行了。不用再去关系多线程的问题,所有的更新UI的操作,都是在主线程的消息队列中去轮训的。
而我们平时在 子线程中更新UI 的错:
异常翻译:只有创建这个view的线程才能操作这个view;
引起原因:在子线程中更新了主线程创建的UI;
也就是说:子线程更新UI也行,但是只能更新自己创建的View;
换句话说:Android的UI更新(GUI)被设计成了单线程;
为啥不设计成多线程?
多个线程同时对同一个UI控件进行更新,容易发生不可控的错误!
那么怎么解决这种线程安全问题?
最简单的 加锁,不是加一个,是每层都要加锁(用户代码→GUI顶层→GUI底层…)这样也意味着更多的 耗时,UI更新效率降低;如果每层共用同一把锁的话,其实就是单线程。
必须在UI线程才能更新控件/界面吗
肯定不是的;
众所周知安卓不允许在非UI线程中去更新UI,每当我们对View状态做出改变的时候(如调用requestLayout()或invalidate()等方式时)都会去检查当前线程是否是主线程,而检查线程的判断是在ViewRootImpl的checkThread()方法中去执行的。也就是说在ViewRootImpl没有创建出来的时候(OnResume执行完后wm.addView(decor, l)后ViewRootImpl才创建出来的)checkThread()这一步检测是不会执行的,在这种情况下我们在子线程中是可以更新UI的。
ViewRootImpl.java
void checkThread() {
if (mThread != Thread.currentThread()) {
throw new CalledFromWrongThreadException(
"Only the original thread that created a view hierarchy can touch its views.");
}
}
如果我们在异步线程中去创建一个ViewRootImpl对象的话,同样可以在异步线程中去更新View,因为ViewRootImpl的checkThread()去检测线程时是通过比对当前ViewRootImpl所创建的线程和当前线程是否相同确定是否是主线程的,而ViewRootImpl默认是在主线程中创建的,所以是判断是否在主线程中去更新了View,如果在异步线程中创建了ViewRootImpl对象的话,就可以在异步线程更新UI了。
在异步线程创建ViewRootImpl时首先要绑定Looper,因为ViewRootImpl内部有一个handler,不绑定会出现找不到Looper的异常,且必须通过windowManager.addView的方式去创建ViewRootImpl 对象。ViewRootImpl被@hide注解标注,无法new出来。
thread{
Looper.prepare();
val button=new Button(this);
windowManager.addView(button,WindowManager.LayoutParams());
button.setOnClickListener(){
button.text="123";
}
Looper.loop();
}
android在子线程更新UI的最常见的五种方式 (这里不讲解AsyncTask(异步任务))
- runOnUiThread()方法
- handler.post()方法
- handler.sendMessage()方法
- view.post()方法。
- view postDelayed(Runnable,long)
自定义View
坐标:
View提供的获取的坐标以及距离的方法:
- getTop() 获取到的是view自身的顶边到其父布局顶边的距离
- getLeft() 获取到的是view自身的左边到其父布局左边的距离
- getRight() 获取到的是view自身的右边到其父布局左边的距离
- getBottom() 获取到的是view自身底边到其父布局顶边的距离
MotionEvent提供的方法:
-
getX() 获取点击事件距离控件左边的距离,即视图坐标
-
getY() 获取点击事件距离控件顶边的距离,即视图坐标
-
getRawX() 获取到的是点击事件距离整个屏幕左边的距离,即绝对坐标
-
getRawY() 获取到的是点击事件距离整个屏幕顶边的距离,即绝对坐标
invalidate方法和postInvalidate方法的区别
invalidate方法和postInvalidate方法都是用于进行View的刷新,invalidate方法应用在UI线程中,而postInvalidate方法应用在非UI线程中,用于将线程切换到UI线程,postInvalidate方法最后调用的也是invalidate方法。
invalidate方法和requestLayout方法的区别
requestLayout方法会导致View的onMeasure、onLayout、onDraw方法被调用;invalidate方法则只会导致View的onDraw方法被调用
RectF函数
public RectF (float left, float top, float right, float bottom)
注意: 根据指定坐标创建一个长方形,需要注意的是,此函数没有边界检查,所以输入要确保bottom<top,left<right。
参数:
- left 长方形左侧的x坐标
- top 长方形顶的Y坐标
- right 长方形右侧的X坐标
- bottom 长方形底的Y坐标
如图:
RectF rf1 = new RectF(100,100,300,200);
drawText
字体度量(FontMetrics)
字体的度量,是指对于指定字号的某种字体,在度量方面的各种属性
其描述参数包括:
- baseline:字符基线
- ascent:字符最高点到baseline的推荐距离
- top:字符最高点到baseline的最大距离
- descent:字符最低点到baseline的推荐距离
- bottom:字符最低点到baseline的最大距离
- leading:行间距,即前一行的descent与下一行的ascent之间的距离
参考图:
注意,以上获取到的属性值,是相对于baseline的坐标值,而不是距离值。
例如: 在自定义view中,需要在view中心点绘制view ,即要找到baseline
String defaultText = mProgress + "%";
// 获取字的宽度
float textWidth = textPaint.measureText(defaultText, 0, defaultText.length());
//文字绘制是从文字左下角开始
float dx = getWidth() / 2 - textWidth / 2;
// FontMetricsInt 字体度量
Paint.FontMetricsInt fontMetricsInt = textPaint.getFontMetricsInt();
/**
* 文字坐标为文字的左下角
* fontMetricsInt.top 为负,fontMetricsInt.bottom - fontMetricsInt.top等于文字高度
* (fontMetricsInt.bottom - fontMetricsInt.top) / 2 高度的中心也就是高的一半
* 高的一半包括bottom dy是中心线到baseline 的距离
*/
float dy = (fontMetricsInt.bottom - fontMetricsInt.top) / 2 - fontMetricsInt.bottom;
float baseLine = getHeight() / 2 + dy;
canvas.drawText(defaultText, dx, baseLine, textPaint);
drawText
/**
*text 绘制文本
* x 绘制坐标x
* y 绘制做标y
* paint 画笔
/
public void drawText(@NonNull String text, float x, float y, @NonNull Paint paint)
drawArc
public void drawArc(RectF oval, float startAngle, float sweepAngle, boolean useCenter, Paint paint)
oval :指定圆弧的外轮廓矩形区域。
startAngle: 圆弧起始角度,单位为度。
sweepAngle: 圆弧扫过的角度,顺时针方向,单位为度,从右中间开始为零度。
useCenter: 如果为True时,在绘制圆弧时将圆心包括在内,通常用来绘制扇形。关键是这个变量,下面将会详细介绍。
paint: 绘制圆弧的画板属性,如颜色,是否填充等。
设计模式:
- 单例模式
单例模式:使用时,单例的对象必须保证只有一个实例存在,不予许自由构造对象。
减少对象的创建,降低内存占用,提高代码的整洁性。举个例子,像有时候会创建一个数据管理类DataManager,里面会有缓存管理、线程池处理等一系列工具的实现,直接操作多类Data,这种一般就没有理由创建多个实例,也就是单例模式的使用场景。
如何“单例”?
1.构造函数不对外开放,一般为private 。
2.通过一个静态方法或者枚举返回单例类对象。
3.确保单例类的对象在反序列化时不会重新构建对象。(下文会讲)
4.确保单例类对象有且只有一个,尤其是多线程下。
单例模式又分类为 懒汉式,饿汉式
- 饿汉式
public class Singleton {
private final static Singleton INSTANCE = new Singleton();
private Singleton(){}
public static Singleton getInstance(){
return INSTANCE;
}
}
- 懒汉式 线程安全,同步方法)[不推荐用]
public class Singleton {
private static Singleton singleton;
private Singleton() {}
public static synchronized Singleton getInstance() {
if (singleton == null) {
singleton = new Singleton();
}
return singleton;
}
}
- 静态内部类[推荐用]
public class Singleton {
private Singleton() {}
private static class SingletonInstance {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return SingletonInstance.INSTANCE;
}
}
这种方式跟饿汉式方式采用的机制类似,但又有不同。两者都是采用了类装载的机制来保证初始化实例时只有一个线程。不同的地方在饿汉式方式是只要Singleton类被装载就会实例化,没有Lazy-Loading的作用,而静态内部类方式在Singleton类被装载时并不会立即实例化,而是在需要实例化时,调用getInstance方法,才会装载SingletonInstance类,从而完成Singleton的实例化。
类的静态属性只会在第一次加载类的时候初始化,所以在这里,JVM帮助我们保证了线程的安全性,在类进行初始化时,别的线程是无法进入的。
优点:避免了线程不安全,延迟加载,效率高。
双重检查[推荐用]
public class Singleton {
//ava提供了volatile关键字来保证可见性。当一个共享变量被volatile修饰时,
它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。
private static volatile Singleton singleton;
private Singleton() {}
public static Singleton getInstance() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
Double-Check概念对于多线程开发者来说不会陌生,如代码中所示,我们进行了两次if (singleton == null)检查,这样就可以保证线程安全了。这样,实例化代码只用执行一次,后面再次访问时,判断if (singleton == null),直接return实例化对象。
优点:线程安全;延迟加载;效率较高。
适用场合
- 需要频繁的进行创建和销毁的对象;
- 创建对象时耗时过多或耗费资源过多,但又经常用到的对象;
- 工具类对象;
- 频繁访问数据库或文件的对象。