Android 面试知识点整理

只整理 android 面试中涉及到的知识点。不断补充中。。。

1.Android 事件传递机制

下面内容摘选自《Android 开发艺术探索》

  • (1) 当一个点击事件发生之后,传递过程遵循如下顺序:Activity -> Window -> View
    如果一个 view 的 onTouchEvent 方法返回 false,那么它的父容器的 onTouchEvent 方法将会被调用,依此类推,如果所有的元素都不处理这个事件,那么这个事件将会最终传递给 Activity 处理 (调用 Activity 的 onTouchEvent 方法)。
  • (2) 正常情况下,一个事件序列只能被一个 view 拦截并消耗,因为一旦某个元素拦截了某个事件,那么同一个事件序列内的所有事件都会直接交给它处理,并且该元素的 onInterceptTouchEvent 方法不会再被调用了。
  • (3) 某个 view 一旦开始处理事件,如果它不消耗 ACTION_DOWN 事件,那么同一事件序列的其他事件都不会再交给它来处理,并且事件将重新交给它的父容器去处理 (调用父容器的 onTouchEvent 方法);如果它消耗 ACTION_DOWN 事件,但是不消耗其他类型事件,那么这个点击事件会消失,父容器的 onTouchEvent 方法不会被调用,当前 view 依然可以收到后续的事件,但是这些事件最后都会传递给 Activity 处理。
  • (4) ViewGroup 默认不拦截任何事件,因为它的 onInterceptTouchEvent 方法默认返回 false。view 没有 onInterceptTouchEvent 方法,一旦有点击事件传递给它,那么它的 onTouchEvent 方法就会被调用。
  • (5) View 的 onTouchEvent 默认都会消耗事件 (返回 true),除非它是不可点击的 (clickable 和 longClickable 都为 false)。view 的 longClickable 默认是 false 的,clickable 则不一定,Button 默认是 true,而 TextView 默认是 false。
  • (6) View 的 enable 属性不影响 onTouchEvent 的默认返回值。哪怕一个 view 是 disable 状态,只要它的 clickable 或者 longClickable 有一个是 true,那么它的 onTouchEvent 就会返回 true。
  • (7) 事件传递过程总是先传递给父元素,然后再由父元素分发给子 view,通过 requestDisallowInterceptTouchEvent 方法可以在子元素中干预父元素的事件分发过程,但是 ACTION_DOWN 事件除外,即当面对 ACTION_DOWN 事件时,ViewGroup 总是会调用自己的 onInterceptTouchEvent 方法来询问自己是否要拦截事件。
    ViewGroup 的 dispatchTouchEvent 方法中有一个标志位 FLAG_DISALLOW_INTERCEPT,这个标志位就是通过子 view 调用 requestDisallowInterceptTouchEvent 方法来设置的,一旦设置为 true,那么 ViewGroup 不会拦截该事件。
  • (8) 以上结论均可以在书中的源码解析部分得到解释。Window 的实现类为 PhoneWindow,获取 Activity 的 contentView 的方法

dispatchTouchEvent() 返回 true,后续事件(ACTION_MOVE、ACTION_UP)会再传递,如果返回 false,dispatchTouchEvent() 就接收不到 ACTION_UP、ACTION_MOVE。

((ViewGroup)getWindow().getDecorView().findViewById(android.R.id.content)).getChildAt(0);

2.OnTouchListener、onTouchEvent、OnClickListener 优先级顺序

如果给一个 view 设置了 OnTouchListener,那么 OnTouchListener 中的 onTouch 方法会被回调。这时事件如何处理还要看 onTouch 的返回值,如果返回 false,那么当前 view 的 onTouchEvent 方法会被调用;如果返回 true,那么 onTouchEvent 方法将不会被调用。
在 onTouchEvent 方法中,如果当前 view 设置了 OnClickListener,那么它的 onClick 方法会被调用,所以 OnClickListener 的优先级最低。

自定义 View 事件拦截

1. 重写 onInterceptTouchEvent()

这种是常规写法

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN: // 为了子view能够响应事件,当前view ACTION_DOWN一般不拦截
break;
// .. 满足条件拦截其他事件
}
}

// 配合 onTouchEvent
@Override
public boolean onTouchEvent(MotionEvent e) {
case MotionEvent.ACTION_MOVE:
// 当前view 拦截了ACTION_MOVE,并作出响应处理
break;
return true;
}

2. 重写 dispatchTouchEvent

这种写法相对少见

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
// 在super之前处理某些事件,可以阻止事件传递到当前view的onTouchEvent
if (statified) {
// 比如原有onTouchEvent逻辑复杂,又加了一个单独的view,可以优先让这个view触发事件
return true;
}
return super.dispatchTouchEvent(ev);
}

3.AsyncTask 的方法介绍

    1. onPreExecute()
      这个方法会在后台任务开始执行之间调用,用于进行一些界面上的初始化操作,比如显示一个进度条对话框等。
    1. doInBackground(Params…)
      这个方法中的所有代码都会在子线程中运行,我们应该在这里去处理所有的耗时任务。任务一旦完成就可以通过 return 语句来将任务的执行结果进行返回,如果 AsyncTask 的第三个泛型参数指定的是 Void,就可以不返回任务执行结果。注意,在这个方法中是不可以进行 UI 操作的,如果需要更新 UI 元素,比如说反馈当前任务的执行进度,可以调用 publishProgress(Progress…) 方法来完成。
    1. onProgressUpdate(Progress…)
      当在后台任务中调用了 publishProgress(Progress…) 方法后,这个方法就很快会被调用,方法中携带的参数就是在后台任务中传递过来的。在这个方法中可以对 UI 进行操作,利用参数中的数值就可以对界面元素进行相应的更新。
    1. onPostExecute(Result)
      当后台任务执行完毕并通过 return 语句进行返回时,这个方法就很快会被调用。返回的数据会作为参数传递到此方法中,可以利用返回的数据来进行一些 UI 操作,比如说提醒任务执行的结果,以及关闭掉进度条对话框等。

4. 项目中 Handler 怎么使用?

5. 项目中图片的适配问题怎么解决?

  • dpi
    每英寸点数,全称 dots per inch。用来表示屏幕密度,即屏幕物理区域中的像素量。高密度屏幕比低密度屏幕在给定物理区域的像素要多。
  • dp
    即 dip,全称 device independent pixel。设备独立像素,是一种虚拟像素单位,用于以密度无关方式表示布局维度或位置,以确保在不同密度的屏幕上正常显示 UI。在 160dpi 的设备上,1dp=1px。
  • density
    设备的逻辑密度,是 dip 的缩放因子。以 160dpi 的屏幕为基线,density=dpi/160。

    getResources().getDisplayMetrics().density
  • sp
    缩放独立像素,全称 scale independent pixel。类似于 dp,一般用于设置字体大小,可以根据用户设置的字体大小偏好来缩放。

总结一下图片查找过程:优先匹配最适合的图片→查找密度高的目录(升序)→查找密度低的目录(降序)

也可以使用. 9 图片

6.Android 存储敏感信息的方式有?

使用 SharedPreferences, getSharedPreferences 指定为 MODE_PRIVATE
运用 SQLite 数据库
保存到 SDCard: FileOutputStream fos = this.openFileOutput(“oauth_1.out”,Context.MODE_WORLD_READABLE);
Keystore 也可以保存密钥
写入 SO 文件
结合 ContentProvider 来保存信息
上传到服务器由服务器保存
采用多进程,放在单独的进程中保存

7. 自定义广播

  • 继承自 BroadcastReceiver
  • 重写 onReceive(Context,Intent)
    Intent.getAction
    
  • 动态注册(Context.registerReceiver()),静态注册(使用 IntentFilter 指定 action)

8. 加分项

  • JNI 开发
  • 性能优化
  • 优秀作品

9.Sqlite 数据库更新并保留升级前的数据

我们知道在 SQLiteOpenHelper 的构造方法:

super(Context context, String name, SQLiteDatabase.CursorFactory factory, int version)

中最后一个参数表示数据库的版本号. 当新的版本号大于当前的 version 时会调用方法:

onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion)

所以我们的重点是在该方法中实现 SQLite 数据库版本升级的管理

对于保留升级前的数据,有两种解决办法:

SQLite 提供了 ALTER TABLE 命令,允许用户重命名或添加新的字段到已有表中,但是不能从表中删除字段。并且只能在表的末尾添加字段,比如,为 Subscription 添加两个字段:

  • ALTER TABLE Subscription ADD COLUMN Activation BLOB;
  • ALTER TABLE Subscription ADD COLUMN Key BLOB;

注释:Sqlite 支持 BLOB(二进制大对象) 数据类型

保留数据删除原表创建新表,具体思路是:

  • 1:将表 A 重新命名:例如重新命名为:temp_A
  • 2:创建新表 A
  • 3:将 temp_A 中的数据【也就是更新前的数据】插入到新表 A

具体操作如下:

//重命名原来的数据表
public static final String TEMP_SQL_CREATE_TABLE_SUBSCRIBE = "alter table "
+ A + " rename to temp_A";

//然后把备份表temp_A中的数据copy到新创建的数据库表A中,这个表A没发生结构上的变化
public static final String INSERT_SUBSCRIBE = "select 'insert into A (code,name,username,tablename)
values ("code","name","cnki","tablename")' as insertSQL from temp_A";

//删除备份表
public static final String DELETE_TEMP_SUBSCRIBE = "delete from temp_A ";
public static final String DROP_TEMP_SUBSCRIBE = "drop table if exists temp_A";

@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {

for (int j = oldVersion; j <= newVersion; j++) {
switch (j) {
case 2:
         //创建临时表
db.execSQL(TEMP_SQL_CREATE_TABLE_SUBSCRIBE);
         //执行OnCreate方法,这个方法中放的是表的初始化操作工作,比如创建新表之类的
onCreate(db);
         //删除之前的表里面的默认数据
for (int i = 0; i < arrWhereAct.length; i++) {
db.execSQL(DELETE_TEMP_SUBSCRIBE + arrWhereAct[i]);
}

//将临时表中的数据放入表A
         Cursor cursor = db.rawQuery(INSERT_SUBSCRIBE, null);
if (cursor.moveToFirst()) {
do {
db.execSQL(cursor.getString(cursor
.getColumnIndex("insertSQL")));
} while (cursor.moveToNext());
}

cursor.close();
         //将临时表删除掉
db.execSQL(DROP_TEMP_SUBSCRIBE);

break;

default:
break;
}
}
}

注意,为什么要在方法里写 for 循环,主要是考虑到跨版本升级,比如有的用户一直不升级版本,数据库版本号一直是 1,而客户端最新版本其实对应的数据库版本已经是 4 了,那么我中途可能对数据库做了很多修改,通过这个 for 循环,可以迭代升级,不会发生错误。

10.AIDL 支持的数据类型

其实 AIDL 这门语言非常的简单,基本上它的语法和 Java 是一样的,只是在一些细微处有些许差别——毕竟它只是被创造出来简化 Android 程序员工作的,太复杂不好——所以在这里我就着重的说一下它和 Java 不一样的地方。主要有下面这些点:

文件类型:用 AIDL 书写的文件的后缀是 .aidl,而不是 .java。

数据类型:AIDL 默认支持一些数据类型,在使用这些数据类型的时候是不需要导包的,但是除了这些类型之外的数据类型,在使用之前必须导包,就算目标文件与当前正在编写的 .aidl 文件在同一个包下——在 Java 中,这种情况是不需要导包的。比如,现在我们编写了两个文件,一个叫做 Book.java ,另一个叫做 BookManager.aidl,它们都在 com.lypeer.aidldemo 包下 ,现在我们需要在 .aidl 文件里使用 Book 对象,那么我们就必须在 .aidl 文件里面写上 import com.lypeer.aidldemo.Book; 哪怕 .java 文件和 .aidl 文件就在一个包下。

默认支持的数据类型包括:

  • Java 中的八种基本数据类型,包括 byte,short,int,long,float,double,boolean,char。
  • String 类型。
  • CharSequence 类型。
  • List 类型:List 中的所有元素必须是 AIDL 支持的类型之一,或者是一个其他 AIDL 生成的接口,或者是定义的 parcelable(下文关于这个会有详解)。List 可以使用泛型。
  • Map 类型:Map 中的所有元素必须是 AIDL 支持的类型之一,或者是一个其他 AIDL 生成的接口,或者是定义的 parcelable。Map 是不支持泛型的。

定向 tag:这是一个极易被忽略的点——这里的 “被忽略” 指的不是大家都不知道,而是很少人会正确的使用它。在我的理解里,定向 tag 是这样的:AIDL 中的定向 tag 表示了在跨进程通信中数据的流向,其中 in 表示数据只能由客户端流向服务端out 表示数据只能由服务端流向客户端而 inout 则表示数据可在服务端与客户端之间双向流通。其中,数据流向是针对在客户端中的那个传入方法的对象而言的。in 为定向 tag 的话表现为服务端将会接收到一个那个对象的完整数据,但是客户端的那个对象不会因为服务端对传参的修改而发生变动;out 的话表现为服务端将会接收到那个对象的的空对象,但是在服务端对接收到的空对象有任何修改之后客户端将会同步变动;inout 为定向 tag 的情况下,服务端将会接收到客户端传来对象的完整信息,并且客户端将会同步服务端对该对象的任何变动。

另外,Java 中的基本类型和 String,CharSequence 的定向 tag 默认且只能是 in。还有,请注意,请不要滥用定向 tag,而是要根据需要选取合适的——要是不管三七二十一,全都一上来就用 inout,等工程大了系统的开销就会大很多——因为排列整理参数的开销是很昂贵的。

两种 AIDL 文件:在我的理解里,所有的 AIDL 文件大致可以分为两类。一类是用来定义 parcelable 对象,以供其他 AIDL 文件使用 AIDL 中非默认支持的数据类型的。一类是用来定义方法接口,以供系统使用来完成跨进程通信的。可以看到,两类文件都是在 “定义” 些什么,而不涉及具体的实现,这就是为什么它叫做“Android 接口定义语言”。
注:所有的非默认支持数据类型必须通过第一类 AIDL 文件定义才能被使用。

下面是两个例子,对于常见的 AIDL 文件都有所涉及:

// Book.aidl
//第一类AIDL文件的例子
//这个文件的作用是引入了一个序列化对象 Book 供其他的AIDL文件使用
//注意:Book.aidl与Book.java的包名应当是一样的
package com.lypeer.ipcclient;

//注意parcelable是小写
parcelable Book;

// BookManager.aidl
//第二类AIDL文件的例子
package com.lypeer.ipcclient;
//导入所需要使用的非默认支持数据类型的包
import com.lypeer.ipcclient.Book;

interface BookManager {

//所有的返回值前都不需要加任何东西,不管是什么数据类型
List<Book> getBooks();
Book getBook();
int getBookCount();

//传参时除了Java基本类型以及String,CharSequence之外的类型
//都需要在前面加上定向tag,具体加什么量需而定
void setBookPrice(in Book book , int price)
void setBookName(in Book book , String name)
void addBookIn(in Book book);
void addBookOut(out Book book);
void addBookInout(inout Book book);
}

11.Android 布局的优化方案

  • 层级观察器 (Hierarchy Viewer):
  • 使用 layoutopt 工具输出
  • 重用布局文件:
  • 使用 <merge /> 标签减少布局的嵌套层次;
  • 仅在需要时才加载布局,ViewStub

12.Android 性能调优

  • UI 卡顿
  • ANR 异常
  • 内存性能优化
  • Android API 使用:
    StringBuffer/String、HashMap/ArrayMap/SparseArray
    

性能调优参考这篇博客 Andoid 应用开发性能优化完全分析

使用 DialogFragment 创建对话框

使用 DialogFragment 至少需要实现 onCreateView 或者 onCreateDIalog 方法。onCreateView 即使用定义的 xml 布局文件展示 Dialog。onCreateDialog 即利用 AlertDialog 或者 Dialog 创建出 Dialog。

注:官方不推荐直接使用 Dialog 创建对话框。

没有布局的 Fragment 的作用
没有布局文件 Fragment 实际上是为了保存,当 Activity 重启时,保存大量数据准备的

在运行时配置发生变化时,在 Fragment 中保存有状态的对象
a) 继承 Fragment,声明引用指向你的有状态的对象
b) 当 Fragment 创建时调用 setRetainInstance(boolean)
c) 把 Fragment 实例添加到 Activity 中
d) 当 Activity 重新启动后,使用 FragmentManager 对 Fragment 进行恢复

import android.app.Fragment;
import android.os.Bundle;

/**
* 保存对象的Fragment
*
* @author zhy
*
*/
public class OtherRetainedFragment extends Fragment
{

// data object we want to retain
// 保存一个异步的任务
private MyAsyncTask data;

// this method is only called once for this fragment
@Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
// retain this fragment
setRetainInstance(true);
}

public void setData(MyAsyncTask data)
{
this.data = data;
}

public MyAsyncTask getData()
{
return data;
}
}

具体参考:Android 屏幕旋转 处理 AsyncTask 和 ProgressDialog 的最佳方案



本文采用知识共享署名 2.5 中国大陆许可协议进行许可,欢迎转载,但转载请注明来自 Agehua’s Blog,并保持转载后文章内容的完整。本人保留所有版权相关权利。

本文链接:http://agehua.github.io/2016/09/28/Android-interview-questions/

Share Comments
Loading comments...