Android面试题整理(一)

1.Android系统的架构

Android采用层次化系统架构,官方公布的标准架构如下图所示。Android由底层往上分为4个主要功能层,分别是linux内核层(Linux Kernel)系统运行时库层(Libraries和Android Runtime)应用程序架构层(Application Framework)应用程序层(Applications)

Android面试题整理(一)_第1张图片
Android系统架构图

1.Android系统架构之应用程序

Android会同一系列核心应用程序包一起发布,该应用程序包包括email客户端,SMS短消息程序,日历,地图,浏览器,联系人管理程序等。所有的应用程序都是使用JAVA语言编写的。

2.Android系统架构之应用程序框架

开发人员可以完全访问核心应用程序所使用的API框架(android.jar)。该应用程序的架构设计简化了组件的重用;任何一个应用程序都可以发布它的功能块并且任何其它的应用程序都可以使用其所发布的功能块。

3.Android系统架构之系统运行库

Android 包含一些C/C++库,这些库能被Android系统中不同的组件使用。它们通过 Android 应用程序框架为开发者提供服务。

4.Android系统架构之Linux 内核

Android 的核心系统服务依赖于 Linux 2.6 内核,如安全性,内存管理,进程管理, 网络协议栈和驱动模型。 Linux 内核也同时作为硬件和软件栈之间的抽象层。

2.Activity的生命周期

Android面试题整理(一)_第2张图片
Activity生命周期图

3.Activity的四种启动模式

基础知识

Activity任务栈(Task):

Activity任务栈(Task)是一个标准的栈结构,具有“First In Last Out”的特性,用于在ActivityManagerService侧管理所有的Activity(AMS通过TaskRecord标识一个任务栈,通过ActivityRecord标识一个Activity)。

每当我们打开一个Activity时,就会有一个Activity组件被添加到任务栈,每当我们通过“back”键退出一个Activity时,就会有一个Activity组件从任务栈出栈。任意时刻,只有位于栈顶的Activity才可以跟用户进行交互。

同一时刻,Android系统可以有多个任务栈;每个任务栈可能有一个或多个Activity,这些Activity可能来自于同一个应用程序,也可能来自于多个应用程序。另外,同一个Activity可能只有一个实例,也可能有多个实例,而且这些实例既可能位于同一个任务栈,也可能位于不同的任务栈。而这些行为都可以通过Activity启动模式进行控制。

在Android系统的多个任务栈中,只有一个处于前台,即前台任务栈,其它的都位于后台,即后台任务栈。后台任务栈中的Activity处于暂停状态,用户可以通过唤起后台任务栈中的任意Activity,将后台任务栈切换到前台。

android:taskAffinity属性

android:taskAffinity是Activity的一个属性,表示该Activity期望的任务栈的名称。默认情况下,一个应用程序中所有Activity的taskAffinity都是相同的,即应用程序的包名。当然,我们可以在配置文件中为每个Activity指定不同的taskAffinity(只有和已有包名不同,才有意义)。一般情况下,该属性主要和SingleTask启动模式或者android:allowTaskReparenting属性结合使用(下面会详细介绍),在其他情况下没有意义。

四种启动模式

1.Standard:

标准模式,也是系统的默认模式。该模式下,每次启动Activity,都会创建一个新实例,并且将其加入到启动该Activity的那个Activity所在的任务栈中,所以目标Activity的多个实例可以位于不同的任务栈。例如:ActivityA启动了标准模式的ActivityB,那么ActivityB就会在ActivityA所在的任务栈中。

启动顺序:A->B->C

回退顺序:C->B->A.


2.SingleTop

栈顶复用模式。该模式下,若目标Activity的实例已经存在,但是没有位于栈顶,那么仍然会创建新的实例,并添加到任务栈;若目标Activity的实例已经存在,且位于栈顶,那么就不会创建新的实例,而是复用已有实例,并依次调用目标Activity的onPause -> onNewIntent -> onResume方法。

例如A.B启动模式为Standard,C启动模式为SingleTop

启动顺序:A->B->C—>C

回退顺序:C->B->A.而不是C->C->B->A


3.SingleTask

栈内复用模式。该模式是对SingleTop的进一步加强,若Activity实例已经存在,则不管是不是在栈顶,都不会创建新的实例,而是复用已有Activity实例,即清除任务栈中目标Activity之上的所有Activity,使其位于栈顶,同时也会调用其onNewIntent方法;若Activity实例不存在,系统首先会确认是否有目标Activity期望的任务栈,如果没有,就首先创建目标Activity期望的任务栈,然后创建目标Activity实例并添加到期望的任务栈中;相反,若存在期望的任务栈,那么就直接创建目标Activity实例并将其添加到期望的任务栈。

而Activity期望的任务栈名称就是通过上面介绍的android:taskAffinity属性进行设置的。

例如A.C.D启动模式为Standard,B启动模式为SingleTask

启动顺序:A->B->C—>D—>B

回退顺序:B->A.而不是B—>D->C->B->A

以上例子未考虑android:taskAffinity属性

4.SingleInstance

应用场景:系统的发短信,打电话,来电,浏览器等。

单实例模式。该模式是SingleTask的强化,除了具有SingleTask的所有特性外,还强调任意时刻只允许存在唯一的Activity实例,且该Activity实例独自占有一个任务栈。即该任务栈只能容纳该Activity实例,不能再添加其他Activity实例到该任务栈,如果该Activity实例已经存在于某个任务栈,则直接跳转到该任务栈。

例如A.C启动模式为Standard,B启动模式为SingleInstance

启动顺序:A->B->C;注意:此时产生了两个任务栈,B产生了一个新的任务栈,并处于其他任务栈的下面。

回退顺序:C->A->B.而不是C->B->A

具体可参考:Activity启动模式一

图片来源:深入Activity,Activity启动模式LaunchMode完全解析

4.Fragment的生命周期

Android面试题整理(一)_第3张图片
Fragment生命周期图

Fragment与Activity生命周期对比:

Android面试题整理(一)_第4张图片
Fragment与Activity生命周期对比图

5.Activity与Fragment之间如何进行通信?

1.Activity中可以通过getFragmentManager().findFragmentById()拿到Fragment对象

2.通过接口

3.通过广播接收者

6.Service的生命周期

Android面试题整理(一)_第5张图片

7.Service的启动方式

1.直接启动的方式

startService启动服务,stopService停止服务。

完整生命周期回调顺序为:onCreate -> onStartCommand -> onDestroy

有效生命周期为:onStartCommandonDestroy之间

生命周期方法介绍:

onCreate 创建服务时回调。onCreate只会调用一次

onStartCommand 启动服务时回调。一旦启动,服务即可在后台运行,即使启动服务的组件已被销毁也不受影响。每次调用startService()时都会回调,允许多次调用。 传参startId是请求的id唯一标识,其返回值描述系统应该如何在服务终止的情况下继续运行服务,是对系统的要求。

返回值取值为:

START_NOT_STICKY 如果系统在 onStartCommand() 返回后终止服务,则除非有挂起 Intent 要传递,否则系统不会重建服务。这是最安全的选项,可以避免在不必要时以及应用能够轻松重启所有未完成的作业时运行服务。

START_STICKY 如果系统在 onStartCommand() 返回后终止服务,则会重建服务并调用 onStartCommand(),但绝对不会重新传递最后一个 Intent。相反,除非有挂起 Intent 要启动服务(在这种情况下,将传递这些 Intent ),否则系统会通过空 Intent 调用 onStartCommand()。这适用于不执行命令、但无限期运行并等待作业的媒体播放器(或类似服务)。

START_REDELIVER_INTENT 如果系统在 onStartCommand() 返回后终止服务,则会重建服务,并通过传递给服务的最后一个 Intent 调用 onStartCommand()。任何挂起 Intent 均依次传递。这适用于主动执行应该立即恢复的作业(例如下载文件)的服务。

onDestroy 停止服务时回调。此方法用来清理所有资源,如线程、注册的侦听器、接收器等。需要调用stopSelf(int)或stopService()停止服务。当有多个请求时,stopSelf(int) 确保服务停止请求始终基于最近的启动请求id,服务才能停止,否则服务会继续运行。

2.绑定的方式

bindService绑定服务,unbindService解绑服务。

完整生命周期回调顺序为:onCreate -> onBind -> onUnbind -> onDestroy

有效生命周期为:onBindonUnbind之间

生命周期方法介绍:

onCreate 创建服务时回调。onCreate只会调用一次

onBind 绑定服务时回调。绑定服务提供了一个客户端-服务器接口,允许组件与服务进行交互、发送请求、获取结果,甚至是利用进程间通信 (IPC) 跨进程执行这些操作,如果不允许绑定,则应返回null。 仅当与另一个应用组件绑定时,绑定服务才会运行。 多个组件可以同时绑定到该服务,但全部取消绑定后,该服务即会被销毁。只有在第一个客户端绑定时,系统才会调用服务的 onBind() 方法来检索 IBinder,系统随后无需再次调用 onBind(),便可将同一 IBinder 传递至任何其他绑定的客户端。该方法不一定在UI线程。

onUnbind 解绑服务时回调。当所有客户端都与Service断开连接时调用。默认返回false,当返回值为true时,后续有新Client绑定时会回调onRebind()

onRebind 重新绑定时回调。onUnbind()返回true,且有新Client绑定时调用

onDestroy 停止服务时回调。此方法用来清理所有资源,如线程、注册的侦听器、接收器等。服务与所有客户端之间的绑定全部取消时,系统便会销毁它

绑定方式详细生命周期图:


Android面试题整理(一)_第6张图片

具体可参考:Service知识总结

8.# 6.Service 和 Activity 在同一个线程吗

默认情况下service与activity在同一个线程,都在main Thread,或者ui线程中。

如果在清单文件中指定service的process属性,那么service就在另一个进程中运行。

9.进程保活

1、开启一个像素的Activity

据说这个是手Q的进程保活方案,基本思想,系统一般是不会杀死前台进程的。所以要使得进程常驻,我们只需要在锁屏的时候在本进程开启一个Activity,为了欺骗用户,让这个Activity的大小是1像素,并且透明无切换动画,在开屏幕的时候,把这个Activity关闭掉,所以这个就需要监听系统锁屏广播.

2、前台服务

这种大部分人都了解,据说这个微信也用过的进程保活方案,移步微信Android客户端后台保活经验分享,这方案实际利用了Android前台service的漏洞。

3、相互唤醒

相互唤醒的意思就是,假如你手机里装了支付宝、淘宝、天猫、UC等阿里系的app,那么你打开任意一个阿里系的app后,有可能就顺便把其他阿里系的app给唤醒了。这个完全有可能的。此外,开机,网络切换、拍照、拍视频时候,利用系统产生的广播也能唤醒app,不过Android N已经将这三种广播取消了。

4、JobSheduler

JobSheduler是作为进程死后复活的一种手段,native进程方式最大缺点是费电, Native 进程费电的原因是感知主进程是否存活有两种实现方式,在 Native 进程中通过死循环或定时器,轮训判断主进程是否存活,当主进程不存活时进行拉活。其次5.0以上系统不支持。 但是JobSheduler可以替代在Android5.0以上native进程方式,这种方式即使用户强制关闭,也能被拉起来.

5、粘性服务&与系统服务捆绑

这个是系统自带的,onStartCommand方法必须具有一个整形的返回值,这个整形的返回值用来告诉系统在服务启动完毕后,如果被Kill,系统将如何操作,这种方案虽然可以,但是在某些情况or某些定制ROM上可能失效,我认为可以多做一种保保守方案。

具体可参考:Android进程保活的一般套路

10.BroadcastReceiver的注册方式

首先写一个类要继承BroadCastReceiver

第一种:在清单文件中声明,添加


    
        
    

第二种:使用代码进行注册如:

IntentFilter filter = new    IntentFilter("android.provider.Telephony.SMS_RECEIVED");
BroadCastReceiverDemo receiver = new BroadCastReceiver();
registerReceiver(receiver, filter);

两种注册类型的区别是:

a.第一种是常驻型广播,也就是说当应用程序关闭后,如果有信息广播来,程序也会被系统调用自动运行。

b.第二种不是常驻广播,也就是说广播跟随程序的生命周期。

11.ContentProvider

ContentProvider一般为存储和获取数据提供统一的接口,可以在不同的应用程序之间共享数据。

之所以使用ContentProvider,主要有以下几个理由:

1,ContentProvider提供了对底层数据存储方式的抽象。比如下图中,底层使用了SQLite数据库,在用了ContentProvider封装后,即使你把数据库换成MongoDB,也不会对上层数据使用层代码产生影响

2,Android框架中的一些类需要ContentProvider类型数据。如果你想让你的数据可以使用在如SyncAdapter, Loader, CursorAdapter等类上,那么你就需要为你的数据做一层ContentProvider封装。

3,第三个原因也是最主要的原因,是ContentProvider为应用间的数据交互提供了一个安全的环境。它准许你把自己的应用数据根据需求开放给其他应用进行增、删、改、查,而不用担心直接开放数据库权限而带来的安全问题。

我们知道了ContentProvider是对数据层的封装后,那么大家可能会问我们要如何对ContentProvider进行增,删,改,查的操作呢?下面我们来介绍一个新的类ContentResolver,我们可以通过它,来对不同的ContentProvider进行操作。

具体实现:

首先我们创建一个自己的TestProvider继承ContentProvider。默认该Provider需要实现如下六个方法,onCreate(), query(Uri, String[], String, String[], String),insert(Uri, ContentValues), update(Uri, ContentValues, String, String[]), delete(Uri, String, String[]), getType(Uri),方法的具体介绍可以参考
http://developer.android.com/reference/android/content/ContentProvider.html

因为ContentProvider作为四大组件之一,所以还需要在AndroidManifest.xml中注册一下。

然后你就可以使用getContentResolver()方法来对该ContentProvider进行操作了,ContentResolver对应ContentProvider也有insert,query,delete等方法,详情请参考:
http://developer.android.com/reference/android/content/ContentResolver.html

具体可参考:ContentProvider从入门到精通

12.Handler的消息机制

在整个Handler机制中所有使用到的类,主要包括Message,MessageQueue,Looper以及Handler。

Handler是Android中引入的一种让开发者参与处理线程中消息循环的机制,Handler直接继承自Object,如果要使用Handler必须先调用Looper.prepare();方法,然后再初始化Handler,之后再调用Looper.loop();方法,每个Handler都关联了一个线程,每个线程内部都维护了一个消息队列MessageQueue,这样Handler实际上也就关联了一个消息队列。这样就可以通过Handler将Message和Runnable对象发送到该Handler所关联线程的MessageQueue(消息队列)中,然后该消息队列通过Looper一直在循环拿出一个Message,对其进行处理,处理完之后拿出下一个Message,继续处理.

Handler可以用来在多线程之间进行通信,在另一个线程中去更新UI线程中的UI控件只是Handler使用中的一种典型案例,除此之外,Handler还可以做其他很多的事情,Handler是Thread的代言人,是多线程之间通信的桥梁,通过Handler,我们可以在一个线程中控制另一个线程去做某些事.

具体可参考:Handler 原理梳理

13.事件分发机制

主要涉及到以下三个方法:

public boolean dispatchTouchEvent(MotionEvent ev); 这个方法用来进行事件的分发

public boolean onInterceptTouchEvent(MotionEvent ev); 这个方法用来判断是否拦截事件

onTouchEvent(MotionEvent ev); 这个方法用来处理点击事件。

点击事件的传递规则:

对于一个根ViewGroup,点击事件产生后,首先会传递给他,这时候就会调用他的onDispatchTouchEvent方法,如果Viewgroup的onInterceptTouchEvent方法返回true表示他要拦截事件,接下来事件就会交给ViewGroup处理,调用ViewGroup的onTouchEvent方法;如果ViewGroup的onInteceptTouchEvent方法返回值为false,表示ViewGroup不拦截该事件,这时事件就传递给他的子View,接下来子View的dispatchTouchEvent方法,如此反复直到事件被最终处理。

当一个View需要处理事件时,如果它设置了OnTouchListener,那么onTouch方法会被调用,如果onTouch返回false,则当前View的onTouchEvent方法会被调用,返回true则不会被调用,同时,在onTouchEvent方法中如果设置了OnClickListener,那么他的onClick方法会被调用。由此可见处理事件时的优先级关系: onTouchListener > onTouchEvent > onClickListener.

事件传递的机制,一些结论:

1.一个事件系列以down事件开始,中间包含数量不定的move事件,最终以up事件结束.

2.正常情况下,一个事件序列只能由一个View拦截并消耗。

3.某个View拦截了事件后,该事件序列只能由它去处理,并且它的onInterceptTouchEvent不会再被调用.

4.某个View一旦开始处理事件,如果它不消耗ACTION_DOWN事件(onTouchEvnet返回false),那么同一事件序列中的其他事件都不会交给他处理,并且事件将重新交由他的父元素去处理,即父元素的onTouchEvent被调用.

5.如果View不消耗ACTION_DOWN以外的其他事件,那么这个事件将会消失,此时父元素的onTouchEvent并不会被调用,并且当前View可以持续收到后续的事件,最终消失的点击事件会传递给Activity去处理.

6.ViewGroup默认不拦截任何事件.

7.View没有onInterceptTouchEvent方法,一旦事件传递给它,它的onTouchEvent方法会被调用.

8.View的onTouchEvent默认消耗事件,除非他是不可点击的(clickable和longClickable同时为false).

9.onClick会发生的前提是当前View是可点击的,并且收到了down和up事件.

10.事件传递过程总是由外向内的,即事件总是先传递给父元素,然后由父元素分发给子View,通过requestDisallowInterceptTouchEvent方法可以在子元素中干预父元素的分发过程,但是ACTION_DOWN事件除外.

具体可参考:View的事件体系(四)View 的事件分发机制

14.动画

1、View动画
View动画定义了渐变Alpha、旋转Rotate、缩放Scale、平移Translate四种基本动画.

使用:

a.xml+java代码:

公有属性:

android:duration     动画持续时间
android:fillAfter    为true动画结束时,View将保持动画结束时的状态
android:fillBefore   为true动画结束时,View将还原到开始开始时的状态
android:repeatCount  动画重复执行的次数
android:repeatMode   动画重复模式 ,重复播放时restart重头开始,reverse重复播放时倒叙回放,该属性需要和android:repeatCount一起使用
android:interpolator 插值器,相当于变速器,改变动画的不同阶段的执行速度

View动画都要放在anim目录下。

渐变view_anim_alpha.xml:




旋转view_anim_rotate.xml:




缩放view_anim_scale.xml:




平移view_anim_translate.xml:




代码:

public void clickToAlpha(View view) {
    Animation alphaAnim = AnimationUtils.loadAnimation(getApplicationContext(), R.anim.view_anim_alpha);
    mTargetView.startAnimation(alphaAnim);
}

public void clickToRotate(View view) {
    Animation rotateAnim = AnimationUtils.loadAnimation(getApplicationContext(), R.anim.view_anim_rotate);
    mTargetView.startAnimation(rotateAnim);
}

public void clickToScale(View view) {
    Animation scaleAnim = AnimationUtils.loadAnimation(getApplicationContext(), R.anim.view_anim_scale);
    mTargetView.startAnimation(scaleAnim);
}

public void clickToTranslate(View view) {
    Animation translateAnim = AnimationUtils.loadAnimation(getApplicationContext(), R.anim.view_anim_translate);
    mTargetView.startAnimation(translateAnim);
}

public void clickToSet(View view) {
    Animation setAnim = AnimationUtils.loadAnimation(getApplicationContext(), R.anim.view_anim_set);
    mTargetView.startAnimation(setAnim);
}

b.java代码:

在平常的业务逻辑中也可以直接用Java代码来实现Veiw动画,Android系统给我们提供了AlphaAnimation、RotateAnimation、ScaleAnimation、TranslateAnimation四个动画类分别来实现View的渐变、旋转、缩放、平移动画。

渐变:

public void clickToAlpha(View view) {
    AlphaAnimation alphaAnimation = new AlphaAnimation(1, 0);
    alphaAnimation.setDuration(2000);
    mTargetView.startAnimation(alphaAnimation);
}

旋转:

public void clickToRotate(View view) {
    RotateAnimation rotateAnimation = new RotateAnimation(
        0, 360,
        Animation.RELATIVE_TO_SELF, 0.5f,
        Animation.RELATIVE_TO_SELF, 0.5f);
    rotateAnimation.setDuration(2000);
    mTargetView.startAnimation(rotateAnimation);
}

缩放:

public void clickToScale(View view) {
    ScaleAnimation scaleAnimation = new ScaleAnimation(
        1, 0.5f,
        1, 0.5f,
        Animation.RELATIVE_TO_SELF, 0.5f,
        Animation.RELATIVE_TO_SELF, 0.5f);
    scaleAnimation.setDuration(2000);
    mTargetView.startAnimation(scaleAnimation);
}

平移:

public void clickToTranslate(View view) {
    TranslateAnimation translateAnimation = new TranslateAnimation(
        Animation.RELATIVE_TO_SELF, 0,
        Animation.RELATIVE_TO_SELF, 1,
        Animation.RELATIVE_TO_SELF, 0,
        Animation.RELATIVE_TO_SELF, 1);
    translateAnimation.setDuration(2000);
    mTargetView.startAnimation(translateAnimation);
}

组合:

public void clickToSet(View view) {
    AlphaAnimation alphaAnimation = new AlphaAnimation(1, 0);
    alphaAnimation.setDuration(2000);

    RotateAnimation rotateAnimation = new RotateAnimation(
        0, 360,
        Animation.RELATIVE_TO_SELF, 0.5f,
        Animation.RELATIVE_TO_SELF, 0.5f);
    rotateAnimation.setDuration(2000);

    ScaleAnimation scaleAnimation = new ScaleAnimation(
        1, 0.5f,
        1, 0.5f,
        Animation.RELATIVE_TO_SELF, 0.5f,
        Animation.RELATIVE_TO_SELF, 0.5f);
    scaleAnimation.setDuration(2000);

    TranslateAnimation translateAnimation = new TranslateAnimation(
        Animation.RELATIVE_TO_SELF, 0,
        Animation.RELATIVE_TO_SELF, 1,
        Animation.RELATIVE_TO_SELF, 0,
        Animation.RELATIVE_TO_SELF, 1);
    translateAnimation.setDuration(2000);

    AnimationSet animationSet = new AnimationSet(true);
    animationSet.addAnimation(alphaAnimation);
    animationSet.addAnimation(rotateAnimation);
    animationSet.addAnimation(scaleAnimation);
    animationSet.addAnimation(translateAnimation);

    mTargetView.startAnimation(animationSet);
}

2、属性动画

所谓属性动画,就是改变对象Object的属性来实现动画过程。属性动画是对View的动画的扩展,通过它可以实现更多漂亮的动画效果。同时属性动画的作用对象不仅仅是View,任何对象都可以。
属性动画的作用效果就是:在一个指定的时间段内将对象的一个属性的属性值动态地变化到另一个属性值。

3、帧动画
帧动画需要开发者制定好动画每一帧,系统一帧一帧的播放图片。

使用:

a.java代码:

private void start() {
    AnimationDrawable ad = new AnimationDrawable();
    for (int i = 0; i < 7; i++) {
        Drawable drawable = getResources().getDrawable(getResources().getIdentifier("ic_fingerprint_" + i, "drawable", getPackageName()));
        ad.addFrame(drawable, 100);
    }
    ad.setOneShot(false);
    mImageView.setImageDrawable(ad);
    ad.start();
}

b.xml+java代码使用:

直接在工程drawable目录新建animation-list标签:



    
    
    
    
    
    
    

代码中:

private void start() {
     mImageView.setImageResource(R.drawable.frame_anim);
AnimationDrawable animationDrawable = (AnimationDrawable) mImageView.getDrawable();
      animationDrawable.start();

}

具体可参考:Android动画总结——View动画、属性动画、帧动画

15.ListView和RecyclerView

ListView和RecycleView的缓存原理大致相同,如下图:

Android面试题整理(一)_第7张图片

都是在内部维护一个缓存池,回收划出列表的item,添加给将要进入列表的item。只不过ListView内部是两级缓存,分别是mActiveViews和mScrapViews.而RecycleView内部有四级缓存。

ListView相比RecyclerView,有一些优点:

addHeaderView(), addFooterView()添加头视图和尾视图。
通过”android:divider”设置自定义分割线。

setOnItemClickListener()和setOnItemLongClickListener()设置点击事件和长按事件。

这些功能在RecyclerView中都没有直接的接口,要自己实现(虽然实现起来很简单),因此如果只是实现简单的显示功能,ListView无疑更简单。

RecyclerView相比ListView,有一些明显的优点:

默认已经实现了View的复用,不需要类似if(convertView == null)的实现,而且回收机制更加完善。

默认支持局部刷新。

容易实现添加item、删除item的动画效果。

容易实现拖拽、侧滑删除等功能。

DiffUtil可用于高效进行RecyclerView的数据更新。

RecyclerView是一个插件式的实现,对各个功能进行解耦,从而扩展性比较好.

具体可参考以下几篇文章:

Android ListView与RecyclerView对比浅析--缓存机制

RecyclerView 必知必会

使用DiffUtil高效更新RecyclerView

16.6.0权限

鉴于6.0之前的版本权限管理相对不那么安全,所以Android 6.0 采用新的权限模型,只有在需要权限的时候,才告知用户是否授权,是在runtime时候授权,而不是在原来安装的时候 ,同时默认情况下每次在运行时打开页面时候,需要先检查是否有所需要的权限申请。这样的用户的自主性提高很多,比如用户可以给APP赋予摄像的权限,也可以使用权限。

适配方法:

  1. targetSdkVersion低于23
  2. 动态权限管理

例子:

 // 首先检查权限
if(ContextCompat.checkSelfPermission(thisActivity,Manifest.permission.READ_CONTACTS)
    != PackageManager.PERMISSION_GRANTED) {
// 检查用户是否拒绝了这个权限
if (ActivityCompat.shouldShowRequestPermissionRationale(thisActivity,
    Manifest.permission.READ_CONTACTS)) {
     // 给出一个提示,告诉用户为什么需要这个权限

} else {
     // 用户没有拒绝,直接申请权限
    ActivityCompat.requestPermissions(thisActivity,
      new String[]{Manifest.permission.READ_CONTACTS},
      MY_PERMISSIONS_REQUEST_READ_CONTACTS);
    //用户授权的结果会回调到FragmentActivity的onRequestPermissionsResult
    }
}else {
    //已经拥有授权
    //TODO: 正常业务逻辑
}


public void onRequestPermissionsResult(int requestCode,
  String permissions[], int[] grantResults) {
      switch (requestCode) {
          case MY_PERMISSIONS_REQUEST_READ_CONTACTS: {
          if (grantResults.length > 0
          && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
              readContacts();
           } else {
             // 权限拒绝了。
           }
           return;
           }
      }
}

17.大图片加载的处理

1.压缩

BitmapFactory这个类提供了多个解析方法(decodeByteArray, decodeFile, decodeResource等)用于创建Bitmap对象,我们应该根据图片的来源选择合适的方法。比如SD卡中的图片可以使用decodeFile方法,网络上的图片可以使用decodeStream方法,资源文件中的图片可以使用decodeResource方法。这些方法会尝试为已经构建的bitmap分配内存,这时就会很容易导致OOM出现。为此每一种解析方法都提供了一个可选的BitmapFactory.Options参数,将这个参数的inJustDecodeBounds属性设置为true就可以让解析方法禁止为bitmap分配内存,返回值也不再是一个Bitmap对象,而是null。虽然Bitmap是null了,但是BitmapFactory.Options的outWidth、outHeight和outMimeType属性都会被赋值。这个技巧让我们可以在加载图片之前就获取到图片的长宽值和MIME类型,从而根据情况对图片进行压缩。如下代码所示:

BitmapFactory.Options options = new BitmapFactory.Options();  
options.inJustDecodeBounds = true;  
BitmapFactory.decodeResource(getResources(), R.id.myimage, options);  
int imageHeight = options.outHeight;  
int imageWidth = options.outWidth;  
String imageType = options.outMimeType; 

现在图片的大小已经知道了,我们就可以决定是把整张图片加载到内存中还是加载一个压缩版的图片到内存中。以下几个因素是我们需要考虑的:

a.预估一下加载整张图片所需占用的内存。

b.为了加载这一张图片你所愿意提供多少内存。

c.用于展示这张图片的控件的实际大小。

d.当前设备的屏幕尺寸和分辨率。

通过设置BitmapFactory.Options中inSampleSize的值就可以实现对图片进行压缩.下面的方法可以根据传入的宽和高,计算出合适的inSampleSize值:

public static int calculateInSampleSize(BitmapFactory.Options options,  
    int reqWidth, int reqHeight) {  
    // 源图片的高度和宽度  
    final int height = options.outHeight;  
    final int width = options.outWidth;  
    int inSampleSize = 1;  
    if (height > reqHeight || width > reqWidth) {  
        // 计算出实际宽高和目标宽高的比率  
        final int heightRatio = Math.round((float) height / (float) reqHeight);  
        final int widthRatio = Math.round((float) width / (float) reqWidth);  
        // 选择宽和高中最小的比率作为inSampleSize的值,这样可以保证最终图片的宽和高  
        // 一定都会大于等于目标的宽和高。  
        inSampleSize = heightRatio < widthRatio ? heightRatio : widthRatio;  
    }  
    return inSampleSize;  
}  

使用这个方法,首先你要将BitmapFactory.Options的inJustDecodeBounds属性设置为true,解析一次图片。然后将BitmapFactory.Options连同期望的宽度和高度一起传递到到calculateInSampleSize方法中,就可以得到合适的inSampleSize值了。之后再解析一次图片,使用新获取到的inSampleSize值,并把inJustDecodeBounds设置为false,就可以得到压缩后的图片了。

public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId,  
    int reqWidth, int reqHeight) {
      
    // 第一次解析将inJustDecodeBounds设置为true,来获取图片大小  
    final BitmapFactory.Options options = new BitmapFactory.Options();  
    options.inJustDecodeBounds = true;  
    BitmapFactory.decodeResource(res, resId, options);  
    // 调用上面定义的方法计算inSampleSize值  
    options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);  
    // 使用获取到的inSampleSize值再次解析图片  
    options.inJustDecodeBounds = false;  
    return BitmapFactory.decodeResource(res, resId, options);  
}  

2.缓存

内存缓存技术对那些大量占用应用程序宝贵内存的图片提供了快速访问的方法。其中最核心的类是LruCache (此类在android-support-v4的包中提供) 。这个类非常适合用来缓存图片,它的主要算法原理是把最近使用的对象用强引用存储在 LinkedHashMap 中,并且把最近最少使用的对象在缓存值达到预设定值之前从内存中移除。

下面是一个使用 LruCache 来缓存图片的例子:

private LruCache mMemoryCache;  

@Override  
protected void onCreate(Bundle savedInstanceState) {  
    // 获取到可用内存的最大值,使用内存超出这个值会引起OutOfMemory异常。  
    // LruCache通过构造函数传入缓存值,以KB为单位。  
    int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);  
    // 使用最大可用内存值的1/8作为缓存的大小。  
    int cacheSize = maxMemory / 8;  
    mMemoryCache = new LruCache(cacheSize) {  
        @Override  
        protected int sizeOf(String key, Bitmap bitmap) {  
            // 重写此方法来衡量每张图片的大小,默认返回图片数量。  
            return bitmap.getByteCount() / 1024;  
        }  
    };  
}  

public void addBitmapToMemoryCache(String key, Bitmap bitmap) {  
    if (getBitmapFromMemCache(key) == null) {  
        mMemoryCache.put(key, bitmap);  
    }  
}  

public Bitmap getBitmapFromMemCache(String key) {  
    return mMemoryCache.get(key);  
} 

当向 ImageView 中加载一张图片时,首先会在 LruCache 的缓存中进行检查。如果找到了相应的键值,则会立刻更新ImageView ,否则开启一个后台线程来加载这张图片。

public void loadBitmap(int resId, ImageView imageView) {  
    final String imageKey = String.valueOf(resId);  
    final Bitmap bitmap = getBitmapFromMemCache(imageKey);  
    if (bitmap != null) {  
        imageView.setImageBitmap(bitmap);  
    } else {  
         imageView.setImageResource(R.drawable.image_placeholder);  
         BitmapWorkerTask task = new BitmapWorkerTask(imageView);  
         task.execute(resId);  
    }  
} 

BitmapWorkerTask 还要把新加载的图片的键值对放到缓存中。

class BitmapWorkerTask extends AsyncTask {  
    // 在后台加载图片。  
    @Override  
    protected Bitmap doInBackground(Integer... params) {  
        final Bitmap bitmap = decodeSampledBitmapFromResource(  
            getResources(), params[0], 100, 100);  
        addBitmapToMemoryCache(String.valueOf(params[0]), bitmap);  
        return bitmap;  
    }  
}  

具体可参考: Android高效加载大图、多图解决方案,有效避免程序OOM

18.断点续传的实现原理

其实断点续传的原理很简单,从字面上理解,所谓断点续传就是从停止的地方重新下载。

断点:线程停止的位置。

续传:从停止的位置重新下载。

用代码解析就是:

断点 : 当前线程已经下载完成的数据长度。

续传 : 向服务器请求上次线程停止位置之后的数据。

原理知道了,功能实现起来也简单。每当线程停止时就把已下载的数据长度写入记录文件,当重新下载时,从记录文件读取已经下载了的长度。而这个长度就是所需要的断点。

续传的实现也简单,可以通过设置网络请求参数,请求服务器从指定的位置开始读取数据。

而要实现这两个功能只需要使用到httpURLconnection里面的setRequestProperty方法便可以实现.

public void setRequestProperty(String field, String newValue)

使用:

conn.setRequestProperty("Range", "bytes=" + startIndex + "-" + endIndex);

具体使用:

public class MutilDownloader {
    // 开启的线程的个数
    public static final int THREAD_COUNT = 3;
    public static int runningThread = 3;// 记录正在运行的下载文件的线程数
    public static void main(String[] args) throws Exception {
        String path = "文件下载地址";
        // 1、连接服务器,获取一个文件,获取文件的长度,在本地创建一个大小跟服务器文件大小一样的临时文件
        URL url = new URL(path);
        HttpURLConnection conn = (HttpURLConnection) url.openConnection();
        conn.setConnectTimeout(5000);
        conn.setRequestMethod("GET");
        int code = conn.getResponseCode();
        if (code == 200) {
            // 服务器返回的数据的长度,实际就是文件的长度
            int length = conn.getContentLength();
            System.out.println("----文件总长度----" + length);
            // 在客户端本地创建出来一个大小跟服务器端文件一样大小的临时文件
            RandomAccessFile raf = new RandomAccessFile("temp.apk", "rwd");
            // 指定创建的这个文件的长度
            raf.setLength(length);
            // 关闭raf
            raf.close();
            // 假设是3个线程去下载资源
            // 平均每一个线程下载的文件的大小
            int blockSize = length / THREAD_COUNT;
            for (int threadId = 1; threadId <= THREAD_COUNT; threadId++) {
                // 第一个线程开始下载的位置
                int startIndex = (threadId - 1) * blockSize;
                int endIndex = threadId * blockSize - 1;
                if (threadId == THREAD_COUNT) {
                    endIndex = length;
                }
                System.out.println("----threadId---" + "--startIndex--"
                    + startIndex + "--endIndex--" + endIndex);
                new DownloadThread(path, threadId, startIndex, endIndex).start();
            }
        }
    }
    /**
     * 下载文件的子线程,每一个线程下载对应位置的文件
     * 
     * @author loonggg
     * 
     */
    public static class DownloadThread extends Thread {
        private int threadId;
        private int startIndex;
        private int endIndex;
        private String path;
        /**
         * @param path
         *            下载文件在服务器上的路径
         * @param threadId
         *            线程id
         * @param startIndex
         *            线程下载的开始位置
         * @param endIndex
         *            线程下载的结束位置
         */
        public DownloadThread(String path, int threadId, int startIndex,int endIndex) {
            this.path = path;
            this.threadId = threadId;
            this.startIndex = startIndex;
            this.endIndex = endIndex;
        }
        
        @Override
        public void run() {
            try {
                // 检查是否存在记录下载长度的文件,如果存在读取这个文件的数据
                File tempFile = new File(threadId + ".txt");
                if (tempFile.exists() && tempFile.length() > 0) {
                    FileInputStream fis = new FileInputStream(tempFile);
                    byte[] temp = new byte[1024 * 10];
                    int leng = fis.read(temp);
                    // 已经下载的长度
                    String downloadLen = new String(temp, 0, leng);
                    int downloadInt = Integer.parseInt(downloadLen);
                    startIndex = downloadInt;
                    fis.close();
                }
                URL url = new URL(path);
                HttpURLConnection conn = (HttpURLConnection) url.openConnection();
                conn.setRequestMethod("GET");
                // 重要:请求服务器下载部分的文件 指定文件的位置
                conn.setRequestProperty("Range", "bytes=" + startIndex + "-" + endIndex);
                conn.setConnectTimeout(5000);
                // 从服务器请求全部资源的状态码200 ok 如果从服务器请求部分资源的状态码206 ok
                int code = conn.getResponseCode();
                System.out.println("---code---" + code);
                InputStream is = conn.getInputStream();// 已经设置了请求的位置,返回的是当前位置对应的文件的输入流
                RandomAccessFile raf = new RandomAccessFile("temp.apk", "rwd");
                // 随机写文件的时候从哪个位置开始写
                raf.seek(startIndex);// 定位文件
                int len = 0;
                byte[] buffer = new byte[1024];
                int total = 0;// 记录已经下载的数据的长度
                while ((len = is.read(buffer)) != -1) {
                    RandomAccessFile recordFile = new RandomAccessFile(threadId+ ".txt", "rwd");// 记录每个线程的下载进度,为断点续传做标记
                    raf.write(buffer, 0, len);
                    total += len;
                    recordFile.write(String.valueOf(startIndex + total)
                        .getBytes());
                    recordFile.close();
                }
                is.close();
                raf.close();
                System.out.println("线程:" + threadId + "下载完毕了!");
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                runningThread--;
                if (runningThread == 0) {// 所有的线程已经执行完毕
                    for (int i = 1; i <= THREAD_COUNT; i++) {
                        File file = new File(i + ".txt");
                        file.delete();
                    }
                }
            }
        }
    }
}

具体可参考:多线程系列之多线程下载之断点续传(2)

19.自定义View

可分为三类:

自定义View,——继承 View,然后自绘视图内容

自定义ViewGroup,——继承ViewGroup,然后对子类视图进行重新布局。

自定义已有View,——继承已有的View,比如继承ImageView

这里介绍下自定义视图的主要步骤:

  • 自定义属性
  • 继承View重写构造方法
  • 获取自定义属性
  • 重写测量控件的宽高
  • 绘制控件显示
  • 提供自定义事件

1.自定义属性

自定义属性一共有10中定义类型,String,boolean等,具体的类型
和使用对应如下代码:



    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
        
        
    
    
        
        
        
    

2.创建自定义View继承View(重写构造方法)

在创建View的时候,需要重写构造方法,一般重写前三个构造方法就可以了,但是如果我们的自定控件是通过布局文件的形式加载,则第二个构造必须重写,不然会报错。

public MyCuntomView(Context context) {
    this(context, null);
}

public MyCuntomView(Context context, AttributeSet attrs) {
    this(context, attrs, 0);
}

public MyCuntomView(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);

    //获取自定义属性
    initViewAtrr(context, attrs, defStyleAttr);
}

3.获取自定属性的值

在获取自定义属性值的时候,我们通过循环的方式来获取值,这样获取到属性值,就是我们xml文件中使用到的,没有使用到的就获取不到。而并获取我们所有自定义的属性。

private void initViewAtrr(Context context, AttributeSet attrs, int defStyleAttr) {

    TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.customView, defStyleAttr, 0);

    //获取有几个自定义属相
    final int count = a.getIndexCount();
    Log.e("TAG", "=====" + count);

    for (int i = 0; i < count; i++) {
        int type = a.getIndex(i);
        switch (type) {
            case R.styleable.customView_text:

                text = a.getString(type);
                if (TextUtils.isEmpty(text)) {
                    text = "我是文本";
                }
                break;

            case R.styleable.customView_mcolor:

                corlor = a.getColor(type, Color.RED);

                break;

            case R.styleable.customView_msize:

                msize = a.getDimensionPixelSize(type, 15);
                break;


        }

    }

    a.recycle();

    paint = new Paint();
    //抗锯齿
    paint.setAntiAlias(true);


}

4.测量控件的大小(重写onMeasure方法)

测量之前先了解MeasureSpec的specMode,mode共有三种情况,取值分别为MeasureSpec.UNSPECIFIED, MeasureSpec.EXACTLY, MeasureSpec.AT_MOST。

MeasureSpec.EXACTLY是精确尺寸,当我们将控件的layout_width或layout_height指定为具体数值时如andorid:layout_width=”50dp”,或者为FILL_PARENT是,都是控件大小已经确定的情况,都是精确尺寸。

MeasureSpec.AT_MOST是最大尺寸,当控件的layout_width或layout_height指定为WRAP_CONTENT时,控件大小一般随着控件的子空间或内容进行变化,此时控件尺寸只要不超过父控件允许的最大尺寸即可。因此,此时的mode是AT_MOST,size给出了父控件允许的最大尺寸。

MeasureSpec.UNSPECIFIED是未指定尺寸,这种情况不多,一般都是父控件是AdapterView,通过measure方法传入的模式。

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

    int mode = MeasureSpec.getMode(widthMeasureSpec);
    int size = MeasureSpec.getSize(widthMeasureSpec);
    bounds = new Rect();
    if (mode == MeasureSpec.EXACTLY) {
        mwidth = size;
    } else {

        paint.setTextSize(msize);

        paint.getTextBounds(text, 0, text.length(), bounds);
        mwidth = getPaddingLeft() + getPaddingRight() + bounds.width();

    }


    mode = MeasureSpec.getMode(heightMeasureSpec);
    size = MeasureSpec.getSize(heightMeasureSpec);
    if (mode == MeasureSpec.EXACTLY) {

        mheight = size;
    } else {

        paint.getTextBounds(text, 0, text.length(), bounds);
        mheight = getPaddingBottom() + getPaddingTop() + bounds.height();
    }

  r=Math.max(mwidth,mheight);
    setMeasuredDimension(r, r);

}

5.绘制控件显示(重写onDraw方法)

以下代码是绘制一个圆并绘制文字:

@Override
protected void onDraw(Canvas canvas) {
    paint.setColor(corlor);
    canvas.drawCircle(r/2,r/2,r/2,paint);
    paint.setColor(Color.BLACK);
    canvas.drawText(text,r/2-bounds.width()/2,r/2+bounds.height()/2,paint);

}

6.定义事件

一些根据手势操作的代码可以写在此处

@Override
public boolean onTouchEvent(MotionEvent event) {

    //手势操作相关代码
         ...
         ...
    return super.onTouchEvent(event);
}

7.编写自定义控件,使用自定义属性

编写自定义控件,使用自定义属性,在跟布局添加如下代码:

 xmlns:app="http://schemas.android.com/apk/res-auto"

并以以下方式引用控件:

<包名.MyCuntomView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    app:xxx="xxx"
    app:xxx="xxx"
    app:xxx="xxx" />

完成以上步骤后,Activity和Fragment就可以使用相应的控件来实现相应的交互.

具体可参考:Android 自定义View(基础)

20.SQLite数据库

SQLite是一个轻量级数据库,支持SQL语言、事务处理等功能。SQLite没有服务器进程,它通过文件保存数据,该文件是跨平台的,可以放在其他平台中使用。

保存位置:

/data/data/应用包名/databases/xxx.db
数据库在创建的时候默认会创建一张表(metadata.db)来保存系统语言环境

使用:

1.继承SQLiteOpenHelper类,并实现其中的方法

/**
 * SQLiteOpenHelper
 * 1.提供了onCreate() onUpgrade()等创建数据库更新数据库的方法
 * 2.提供了获取数据库对象的函数
 */
public class MySqliteHple extends SQLiteOpenHelper{

    public MySqliteHple(Context context) {
        super(context, Constant.DATABASE_NAME, null, Constant.DATABASE_VERSION);
    }

    /**
     * 构造函数
     * @param context 上下文对象
     * @param name 表示创建数据库的名称
     * @param factory 游标工厂
     * @param version 表示创建数据库的版本 >=1
     */
     public MySqliteHple(Context context, String name, SQLiteDatabase.CursorFactory factory, int version) {
         super(context, name, factory, version);
     }

    /**
     * 当数据库创建时回调的函数
     * @param db 数据库对象
     */
    @Override
    public void onCreate(SQLiteDatabase db) {
        Log.i("tag","------onCreate-------");
        String sql="create table student(_id Integer primary key,name varchar(10),age Integer not null)";

        Log.i("tag","sql:"+sql);

        db.execSQL(sql);//执行sql语句
    }

    /**
     * 当数据库版本更新时回调的函数
     * @param db 数据库对象
     * @param oldVersion 数据库旧版本
     * @param newVersion 数据库新版本
     */
    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        Log.i("tag","------onUpgrade-------");
    }

    /**
     * 当数据库打开时回调的函数
     * @param db 数据库对象
     */
    @Override
    public void onOpen(SQLiteDatabase db) {
        super.onOpen(db);
        Log.i("tag","------onOpen-------");
    }
}

2.通过SQLiteOpenHelper获取数据库SQLiteDatabase对象

//getReadableDatabase()和getWritableDatabase() 创建或打开数据库,如果数据库不存在则创建数据库,如果数据库存在则直接打开数据库。默认情况下两个函数都表示打开或者创建可读可写的数据库对象,如果磁盘已满或者数据库本身权限等情况下getReadableDatabase()打开的是只读数据库
SQLiteDatabase db=mHple.getWritableDatabase();

3.增删改查

  • 通过sql语句
  • 通过api(底层也是调用了sql语句)

经常使用的sql语句:

创建表的语句

create table 表名(字段名称 数据类型 约束,字段名称 数据类型 约束......)

删除表的语句

drop table 表名

插入数据

insert into 表名[字段,字段] values(值1,值2......)

修改数据

update 表名 set 字段=新值 where 修改条

删除数据

delete from 表名 where 删除的条件

查询数据

select 字段名 from 表名 where 查询条件 group by 分组的字段 having 筛选条件 order by 排序字段

使用Api进行操作:

插入数据

/**
* insert(String table, String nullColumnHack, ContentValues values)
* String table 表示插入数据表的名字
* String nullColumnHack SQL要求插入的数据不能全为null,但有些字段可以为null。一般这个参数我们直接给null
* ContentValues values 键为String类型的HashMap集合
* 返回值为long类型  表示插入数据的列数 如果值为-1则表示插入失败
*/
insert(String table, String nullColumnHack, ContentValues values)

更新数据

/**
 * update(String table, ContentValues values, String whereClause, String[] whereArgs)
 * String table 表示修改数据表的名字
 * ContentValues values 键为String类型的HashMap集合
 * String whereClause 表示修改条件
 * String[] whereArgs 表示修改条件的占位符
 */
update(String table, ContentValues values, String whereClause, String[] whereArgs)

删除数据

/**
 * delete(String table, String whereClause, String[] whereArgs)
 * String table 表示删除数据表的名字
 * String whereClause 表示删除条件
 * String[] whereArgs 表示删除条件的占位符
 */
delete(String table, String whereClause, String[] whereArgs)

查询数据

/**
 * query(String table, String[] columns, String selection,
 * String[] selectionArgs, String groupBy, String having,
 * String orderBy)
 * String table 表示查询的表名
 * String[] columns 表示查询的表中的字段名字 null查询所有
 * String selection 表示查询条件 where子句
 * String[] selectionArgs 表示查询条件占位符的取值
 * String groupBy 表示分组条件 group by子句
 * String having 表示筛选条件 having子句
 * String orderBy 表示排序条件 order by子句
 */
query(String table, String[] columns, String selection,String[] selectionArgs, String groupBy, String having,String orderBy)

SQLite事务的使用(一般是在批量操作的时候使用)

1.数据库显式开启事务
db.beginTransaction();

2.提交当前事务
db.setTransactionSuccessful();

3.关闭事务
db.endTransaction();

SQLite数据库分页

//主要使用以下sql语句
select * from student limit ?,?

由于篇幅有限,其它知识点在以下文章中:
Android面试题整理(二)

你可能感兴趣的:(Android面试题整理(一))