Android 面试题集 包含答案

1、Activity如何与Service通信?

可以通过bindService的方式,先在Activity里实现一个ServiceConnection接口,并将该接口传递给bindService()方法,在ServiceConnection接口的onServiceConnected()方法里执行相关操作。

2、Service的生命周期与启动方法有什么区别?

startService():开启Service,调用者退出后Service仍然存在。

bindService():开启Service,调用者退出后Service也随即退出。

3、Service生命周期:

用startService()启动服务:onCreate() -> onStartCommand() -> onDestory

用bindService()绑定服务:onCreate() -> onBind() -> onUnBind() -> onDestory

同时使用startService()启动服务与bindService()绑定服务:onCreate() -> onStartCommnad() -> onBind() -> onUnBind() -> onDestory

4、广播分为哪几种,应用场景是什么?

普通广播:调用sendBroadcast()发送,最常用的广播。

有序广播:调用sendOrderedBroadcast(),发出去的广播会被广播接受者按照顺序接收,广播接收者按照Priority属性值从大-小排序,Priority属性相同者,动态注册的广播优先,广播接收者还可以选择对广播进行截断和修改。

5、广播的两种注册方式有什么区别?

静态注册:常驻系统,不受组件生命周期影响,即便应用退出,广播还是可以被接收,耗电、占内存。

动态注册:非常驻,跟随组件的生命变化,组件结束,广播结束。在组件结束前,需要先移除广播,否则容易造成内存泄漏。

6、广播发送和接收的原理了解吗?

1继承BroadcastReceiver,重写onReceive()方法。 

2通过Binder机制向ActivityManagerService注册广播。

3通过Binder机制向ActivityMangerService发送广播。

4ActivityManagerService查找符合相应条件的广播(IntentFilter/Permission)的BroadcastReceiver,将广播发送到BroadcastReceiver所在的消息队列中。

5BroadcastReceiver所在消息队列拿到此广播后,回调它的onReceive()方法。

7、ContentProvider、ContentResolver与ContentObserver之间的关系是什么?

ContentProvider:管理数据,提供数据的增删改查操作,数据源可以是数据库、文件、XML、网络等,ContentProvider为这些数据的访问提供了统一的接口,可以用来做进程间数据共享。

ContentResolver:ContentResolver可以不同URI操作不同的ContentProvider中的数据,外部进程可以通过ContentResolver与ContentProvider进行交互。ContentObserver:观察ContentProvider中的数据变化,并将变化通知给外界。

8、遇到过哪些关于Fragment的问题,如何处理的?

getActivity()空指针:这种情况一般发生在在异步任务里调用getActivity(),而Fragment已经onDetach(),此时就会有空指针,解决方案是在Fragment里使用一个全局变量mActivity,在onAttach()方法里赋值,这样可能会引起内存泄漏,但是异步任务没有停止的情况下本身就已经可能内存泄漏,相比直接crash,这种方式显得更妥当一些。

Fragment视图重叠:在类onCreate()的方法加载Fragment,并且没有判断saveInstanceState==null或if(findFragmentByTag(mFragmentTag) == null),导致重复加载了同一个Fragment导致重叠。(PS:replace情况下,如果没有加入回退栈,则不判断也不会造成重叠,但建议还是统一判断下)

9、Android里的Intent传递的数据有大小限制吗,如何解决?

Intent传递数据大小的限制大概在1M左右,超过这个限制就会静默崩溃。处理方式如下:

进程内:EventBus,文件缓存、磁盘缓存。

10、描述一下Android的事件分发机制?

1Android事件分发机制的本质:事件从哪个对象发出,经过哪些对象,最终由哪个对象处理了该事件。此处对象指的是Activity、Window与View。

2Android事件的分发顺序:Activity(Window) -> ViewGroup -> View

11、描述一下View的绘制原理?

View的绘制流程主要分为三步:

1onMeasure:测量视图的大小,从顶层父View到子View递归调用measure()方法,measure()调用onMeasure()方法,onMeasure()方法完成绘制工作。

2onLayout:确定视图的位置,从顶层父View到子View递归调用layout()方法,父View将上一步measure()方法得到的子View的布局大小和布局参数,将子View放在合适的位置上。

3onDraw:绘制最终的视图,首先ViewRoot创建一个Canvas对象,然后调用onDraw()方法进行绘制。onDraw()方法的绘制流程为:① 绘制视图背景。② 绘制画布的图层。 ③ 绘制View内容。④ 绘制子视图,如果有的话。⑤ 还原图层。⑥ 绘制滚动条。

12、requestLayout()、invalidate()与postInvalidate()有什么区别?

requestLayout():该方法会递归调用父窗口的requestLayout()方法,直到触发ViewRootImpl的performTraversals()方法,此时mLayoutRequestede为true,会触发onMesaure()与onLayout()方法,不一定会触发onDraw()方法。

invalidate():该方法递归调用父View的invalidateChildInParent()方法,直到调用ViewRootImpl的invalidateChildInParent()方法,最终触发ViewRootImpl的performTraversals()方法,此时mLayoutRequestede为false,不会触发onMesaure()与onLayout()方法,当时会触发onDraw()方法。

postInvalidate():该方法功能和invalidate()一样,只是它可以在非UI线程中调用。

一般说来需要重新布局就调用requestLayout()方法,需要重新绘制就调用invalidate()方法。

13、了解APK的打包流程吗,描述一下?

Android的包文件APK分为两个部分:代码和资源,所以打包方面也分为资源打包和代码打包两个方面,这篇文章就来分析资源和代码的编译打包原理

APK整体的的打包流程如下图所示: 

具体说来: 

(1)通过AAPT工具进行资源文件(包括AndroidManifest.xml、布局文件、各种xml资源等)的打包,生成R.java文件。

(2)通过AIDL工具处理AIDL文件,生成相应的Java文件。

(3)通过Javac工具编译项目源码,生成Class文件。

(4)通过DX工具将所有的Class文件转换成DEX文件,该过程主要完成Java字节码转换成Dalvik字节码,压缩常量池以及清除冗余信息等工作。

(5)通过ApkBuilder工具将资源文件、DEX文件打包生成APK文件。

(6)利用KeyStore对生成的APK文件进行签名。

(7)如果是正式版的APK,还会利用ZipAlign工具进行对齐处理,对齐的过程就是将APK文件中所有的资源文件举例文件的起始距离都偏移4字节的整数倍,这样通过内存映射访问APK文件的速度会更快。

14、了解APK的安装流程吗,描述一下?


复制APK到/data/app目录下,解压并扫描安装包。

(1)资源管理器解析APK里的资源文件。

(2) 解析AndroidManifest文件,并在/data/data/目录下创建对应的应用数据目录。

(3) 然后对dex文件进行优化,并保存在dalvik-cache目录下。

(4) 将AndroidManifest文件解析出的四大组件信息注册到PackageManagerService中。

(5) 安装完成后,发送广播。

14、当点击一个应用图标以后,都发生了什么,描述一下这个过程?

点击应用图标后会去启动应用的LauncherActivity,如果LancerActivity所在的进程没有创建,还会创建新进程,整体的流程就是一个Activity的启动流程。

15、BroadcastReceiver与LocalBroadcastReceiver有什么区别?

BroadcastReceiver 是跨应用广播,利用Binder机制实现。

LocalBroadcastReceiver 是应用内广播,利用Handler实现,利用了IntentFilter的match功能,提供消息的发布与接收功能,实现应用内通信,效率比较高。

16、Android Handler机制是做什么的,原理了解吗?


主要涉及的角色如下所示: 

Message:消息,分为硬件产生的消息(例如:按钮、触摸)和软件产生的消息。

MessageQueue:消息队列,主要用来向消息池添加消息和取走消息。

Looper:消息循环器,主要用来把消息分发给相应的处理者。

Handler:消息处理器,主要向消息队列发送各种消息以及处理各种消息。

整个消息的循环流程还是比较清晰的,具体说来:

(1)Handler通过sendMessage()发送消息Message到消息队列MessageQueue。

(2)Looper通过loop()不断提取触发条件的Message,并将Message交给对应的target handler来处理。

(3)target handler调用自身的handleMessage()方法来处理Message。

事实上,在整个消息循环的流程中,并不只有Java层参与,很多重要的工作都是在C++层来完成的。我们来看下这些类的调用关系。

17、Android Binder机制是做什么的,为什么选用Binder,原理了解吗?

Android Binder是用来做进程通信的,Android的各个应用以及系统服务都运行在独立的进程中,它们的通信都依赖于Binder。

为什么选用Binder,在讨论这个问题之前,我们知道Android也是基于Linux内核,Linux现有的进程通信手段有以下几种:

管道:在创建时分配一个page大小的内存,缓存区大小比较有限;

消息队列:信息复制两次,额外的CPU消耗;不合适频繁或信息量大的通信;

共享内存:无须复制,共享缓冲区直接付附加到进程虚拟地址空间,速度快;但进程间的同步问题操作系统无法实现,必须各进程利用同步工具解决;

套接字:作为更通用的接口,传输效率低,主要用于不通机器或跨网络的通信;

信号量:常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。

6. 信号: 不适用于信息交换,更适用于进程中断控制,比如非法内存访问,杀死某个进程等;

既然有现有的IPC方式,为什么重新设计一套Binder机制呢。主要是出于以上三个方面的考量:

高性能:从数据拷贝次数来看Binder只需要进行一次内存拷贝,而管道、消息队列、Socket都需要两次,共享内存不需要拷贝,Binder的性能仅次于共享内存。

稳定性:上面说到共享内存的性能优于Binder,那为什么不适用共享内存呢,因为共享内存需要处理并发同步问题,控制负责,容易出现死锁和资源竞争,稳定性较差。而Binder基于C/S架构,客户端与服务端彼此独立,稳定性较好。

安全性:我们知道Android为每个应用分配了UID,用来作为鉴别进程的重要标志,Android内部也依赖这个UID进行权限管理,包括6.0以前的固定权限和6.0以后的动态权限,传统IPC只能由用户在数据包里填入UID/PID,这个标记完全是在用户空间控制的,没有放在内核空间,因此有被恶意篡改的可能,因此Binder的安全性更高。

18、描述一下Activity的生命周期,这些生命周期是如何管理的?

 19、Activity的通信方式有哪些? 

(1)startActivityForResult

(2)EventBus

(3)LocalBroadcastReceiver

20、Android应用里有几种Context对象?

实际的功能,Activity、Service与Application都直接或者间接的继承ContextWrapper。

21、描述一下进程和Application的生命周期? 

22、Android哪些情况会导致内存泄漏,如何分析内存泄漏?

(1)持有静态的Context(Activity)引用。

(2)持有静态的View引用,

(3)内部类&匿名内部类实例无法释放(有延迟时间等等),而内部类又持有外部类的强引用,导致外部类无法释放,这种匿名内部类常见于监听器、Handler、Thread、TimerTask

(4)资源使用完成后没有关闭,例如:BraodcastReceiver,ContentObserver,File,Cursor,Stream,Bitmap。

(5)不正确的单例模式,比如单例持有Activity。

(6)集合类内存泄漏,如果一个集合类是静态的(缓存HashMap),只有添加方法,没有对应的删除方法,会导致引用无法被释放,引发内存泄漏。

(7)错误的覆写了finalize()方法,finalize()方法执行不确定,可能会导致引用无法被释放。

查找内存泄漏可以使用Android Profiler工具或者利用LeakCanary工具。

23、Android有哪几种进程,是如何管理的?

Android的进程主要分为以下几种:

(1)前台进程

用户当前操作所必需的进程。如果一个进程满足以下任一条件,即视为前台进程:

托管用户正在交互的 Activity(已调用 Activity 的 onResume() 方法)

托管某个 Service,后者绑定到用户正在交互的 Activity

托管正在“前台”运行的 Service(服务已调用 startForeground())

托管正执行一个生命周期回调的 Service(onCreate()、onStart() 或 onDestroy())

托管正执行其 onReceive() 方法的 BroadcastReceiver

通常,在任意给定时间前台进程都为数不多。只有在内存不足以支持它们同时继续运行这一万不得已的情况下,系统才会终止它们。 此时,设备往往已达到内存分页状态,因此需要终止一些前台进程来确保用户界面正常响应。

(2)可见进程

没有任何前台组件、但仍会影响用户在屏幕上所见内容的进程。 如果一个进程满足以下任一条件,即视为可见进程:

托管不在前台、但仍对用户可见的 Activity(已调用其 onPause() 方法)。例如,如果前台 Activity 启动了一个对话框,允许在其后显示上一 Activity,则有可能会发生这种情况。

托管绑定到可见(或前台)Activity 的 Service。

可见进程被视为是极其重要的进程,除非为了维持所有前台进程同时运行而必须终止,否则系统不会终止这些进程。

(3)服务进程

正在运行已使用 startService() 方法启动的服务且不属于上述两个更高类别进程的进程。尽管服务进程与用户所见内容没有直接关联,但是它们通常在执行一些用户关心的操作(例如,在后台播放音乐或从网络下载数据)。因此,除非内存不足以维持所有前台进程和可见进程同时运行,否则系统会让服务进程保持运行状态。

(4)后台进程

包含目前对用户不可见的 Activity 的进程(已调用 Activity 的 onStop() 方法)。这些进程对用户体验没有直接影响,系统可能随时终止它们,以回收内存供前台进程、可见进程或服务进程使用。 通常会有很多后台进程在运行,因此它们会保存在 LRU (最近最少使用)列表中,以确保包含用户最近查看的 Activity 的进程最后一个被终止。如果某个 Activity 正确实现了生命周期方法,并保存了其当前状态,则终止其进程不会对用户体验产生明显影响,因为当用户导航回该 Activity 时,Activity 会恢复其所有可见状态。

(5)空进程

不含任何活动应用组件的进程。保留这种进程的的唯一目的是用作缓存,以缩短下次在其中运行组件所需的启动时间。 为使总体系统资源在进程缓存和底层内核缓存之间保持平衡,系统往往会终止这些进程。

24、SharePreference性能优化,可以做进程同步吗? 

在Android中, SharePreferences是一个轻量级的存储类,特别适合用于保存软件配置参数。使用SharedPreferences保存数据,其背后是用xml文件存放数据,文件存放在/data/data/ < package name > /shared_prefs目录下.

之所以说SharedPreference是一种轻量级的存储方式,是因为它在创建的时候会把整个文件全部加载进内存,如果SharedPreference文件比较大,会带来以下问题:

第一次从sp中获取值的时候,有可能阻塞主线程,使界面卡顿、掉帧。

解析sp的时候会产生大量的临时对象,导致频繁GC,引起界面卡顿。

这些key和value会永远存在于内存之中,占用大量内存。

优化建议

不要存放大的key和value,会引起界面卡、频繁GC、占用内存等等。

毫不相关的配置项就不要放在在一起,文件越大读取越慢。

读取频繁的key和不易变动的key尽量不要放在一起,影响速度,如果整个文件很小,那么忽略吧,为了这点性能添加维护成本得不偿失。

不要乱edit和apply,尽量批量修改一次提交,多次apply会阻塞主线程。

尽量不要存放JSON和HTML,这种场景请直接使用JSON。

SharedPreference无法进行跨进程通信,MODE_MULTI_PROCESS只是保证了在API 11以前的系统上,如果sp已经被读取进内存,再次获取这个SharedPreference的时候,如果有这个flag,会重新读一遍文件,仅此而已。

25、如何做SQLite升级?

数据库升级增加表和删除表都不涉及数据迁移,但是修改表涉及到对原有数据进行迁移。升级的方法如下所示:

(1)将现有表命名为临时表。

(2)创建新表。

(3)将临时表的数据导入新表。

(4)删除临时表。

如果是跨版本数据库升级,可以由两种方式,如下所示:

(1)逐级升级,确定相邻版本与现在版本的差别,V1升级到V2,V2升级到V3,依次类推。

(2)跨级升级,确定每个版本与现在数据库的差别,为每个case编写专门升级大代码。

26、进程保护如何做,如何唤醒其他进程?

进程保活主要有两个思路:

(1)提升进程的优先级,降低进程被杀死的概率。 

(2)拉活已经被杀死的进程。

如何提升优先级,如下所示:

监控手机锁屏事件,在屏幕锁屏时启动一个像素的Activity,在用户解锁时将Activity销毁掉,前台Activity可以将进程变成前台进程,优先级升级到最高。

如果拉活

利用广播拉活Activity。

27、理解序列化吗,Android为什么引入Parcelable?

所谓序列化就是将对象变成二进制流,便于存储和传输。

Serializable是java实现的一套序列化方式,可能会触发频繁的IO操作,效率比较低,适合将对象存储到磁盘上的情况。

Parcelable是Android提供一套序列化机制,它将序列化后的字节流写入到一个共性内存中,其他对象可以从这块共享内存中读出字节流,并反序列化成对象。因此效率比较高,适合在对象间或者进程间传递信息。

28、如何计算一个Bitmap占用内存的大小,怎么保证加载Bitmap不产生内存溢出?

Bitamp 占用内存大小 = 宽度像素 x (inTargetDensity / inDensity) x 高度像素 x (inTargetDensity / inDensity)x 一个像素所占的内存.

在Bitmap里有两个获取内存占用大小的方法。

getByteCount():API12 加入,代表存储 Bitmap 的像素需要的最少内存。

getAllocationByteCount():API19 加入,代表在内存中为 Bitmap 分配的内存大小,代替了 getByteCount() 方法。

在不复用 Bitmap 时,getByteCount() 和 getAllocationByteCount 返回的结果是一样的。在通过复用 Bitmap 来解码图片时,那么 getByteCount() 表示新解码图片占用内存的大小,getAllocationByteCount() 表示被复用 Bitmap真实占用的内存大小(即 mBuffer 的长度)。

为了保证在加载Bitmap的时候不产生内存溢出,可以受用BitmapFactory进行图片压缩,主要有以下几个参数:

BitmapFactory.Options.inPreferredConfig:将ARGB_8888改为RGB_565,改变编码方式,节约内存。

BitmapFactory.Options.inSampleSize:缩放比例,可以参考Luban那个库,根据图片宽高计算出合适的缩放比例。

BitmapFactory.Options.inPurgeable:让系统可以内存不足时回收内存。

Android如何在不压缩的情况下加载高清大图?

使用BitmapRegionDecoder进行布局加载。

29、Android里的内存缓存和磁盘缓存是怎么实现的?

内存缓存基于LruCache实现,磁盘缓存基于DiskLruCache实现。这两个类都基于Lru算法和LinkedHashMap来实现。

LRU算法可以用一句话来描述,如下所示:

LRU是Least Recently Used的缩写,最近最久未使用算法,从它的名字就可以看出,它的核心原则是如果一个数据在最近一段时间没有使用到,那么它在将来被访问到的可能性也很小,则这类数据项会被优先淘汰掉。

LruCache的原理是利用LinkedHashMap持有对象的强引用,按照Lru算法进行对象淘汰。具体说来假设我们从表尾访问数据,在表头删除数据,当访问的数据项在链表中存在时,则将该数据项移动到表尾,否则在表尾新建一个数据项。当链表容量超过一定阈值,则移除表头的数据。

为什么会选择LinkedHashMap呢?

这跟LinkedHashMap的特性有关,LinkedHashMap的构造函数里有个布尔参数accessOrder,当它为true时,LinkedHashMap会以访问顺序为序排列元素,否则以插入顺序为序排序元素。

30、PathClassLoader与DexClassLoader有什么区别?

PathClassLoader:只能加载已经安装到Android系统的APK文件,即/data/app目录,Android默认的类加载器。

DexClassLoader:可以加载任意目录下的dex、jar、apk、zip文件。

 31、WebView优化了解吗,如何提高WebView的加载速度?

为什么WebView加载会慢呢?

这是因为在客户端中,加载H5页面之前,需要先初始化WebView,在WebView完全初始化完成之前,后续的界面加载过程都是被阻塞的。

优化手段围绕着以下两个点进行:

预加载WebView。

加载WebView的同时,请求H5页面数据。

因此常见的方法是:

全局WebView。

客户端代理页面请求。WebView初始化完成后向客户端请求数据。

asset存放离线包。

除此之外还有一些其他的优化手段:

脚本执行慢,可以让脚本最后运行,不阻塞页面解析。

DNS与链接慢,可以让客户端复用使用的域名与链接。

React框架代码执行慢,可以将这部分代码拆分出来,提前进行解析。

32、Java和JS的相互调用怎么实现,有做过什么优化吗? 

jockeyjs:https://github.com/tcoulter/jockeyjs

对协议进行统一的封装和处理。

33、JNI了解吗,Java与C++如何相互调用?

Java调用C++

在Java中声明Native方法(即需要调用的本地方法)

编译上述 Java源文件javac(得到 .class文件)

 通过 javah 命令导出JNI的头文件(.h文件)

使用 Java需要交互的本地代码 实现在 Java中声明的Native方法

编译.so库文件

通过Java命令执行 Java程序,最终实现Java调用本地代码

C++调用Java

从classpath路径下搜索ClassMethod这个类,并返回该类的Class对象。

获取类的默认构造方法ID。

查找实例方法的ID。

创建该类的实例。

调用对象的实例方法。

JNIEXPORT void JNICALL Java_com_study_jnilearn_AccessMethod_callJavaInstaceMethod   (JNIEnv *env, jclass cls)   {     

 jclass clazz = NULL;      

jobject jobj = NULL;      

jmethodID mid_construct = NULL;      

jmethodID mid_instance = NULL;      

jstring str_arg = NULL;      

// 1、从classpath路径下搜索ClassMethod这个类,并返回该类的Class对象      

clazz = (*env)->FindClass(env, "com/study/jnilearn/ClassMethod");      

if (clazz == NULL) {          

printf("找不到'com.study.jnilearn.ClassMethod'这个类");          

return;      

}     

 // 2、获取类的默认构造方法ID      

mid_construct = (*env)->GetMethodID(env,clazz, "","()V");      

if (mid_construct == NULL) {          

printf("找不到默认的构造方法");          

return;      

}      

// 3、查找实例方法的ID     

 mid_instance = (*env)->GetMethodID(env, clazz, "callInstanceMethod", "(Ljava/lang/String;I)V");      

if (mid_instance == NULL) { 

         return;      

}      

// 4、创建该类的实例      

jobj = (*env)->NewObject(env,clazz,mid_construct);      

if (jobj == NULL) {          

printf("在com.study.jnilearn.ClassMethod类中找不到callInstanceMethod方法");          return;      

}      

// 5、调用对象的实例方法      

str_arg = (*env)->NewStringUTF(env,"我是实例方法");      

(*env)->CallVoidMethod(env,jobj,mid_instance,str_arg,200);      

// 删除局部引用      

(*env)->DeleteLocalRef(env,clazz);      

(*env)->DeleteLocalRef(env,jobj);      

(*env)->DeleteLocalRef(env,str_arg);   

}  



























推荐阅读更多精彩内容