Android 面试之 Android 篇三
本文出自 Eddy Wiki ,转载请注明出处:http://eddy.wiki/interview-android.html
本文收集整理了 Android 面试中会遇到与 Android 知识相关的简述题。
内存相关
什么情况会导致内存泄漏
内存泄漏,简单点说就是该被释放的对象没有释放,一直被某个或某些实例所持有却不再被使用导致 GC 不能回收。
内存泄漏是指无用对象(不再使用的对象)持续占有内存或无用对象的内存得不到及时释放,从而造成内存空间的浪费称为内存泄漏。
- 资源对象没关闭造成的内存泄漏。对于使用了BraodcastReceiver,ContentObserver,File,游标 Cursor,Stream,Bitmap等资源的使用,应该在Activity销毁时及时关闭或者注销,否则这些资源将不会被回收,造成内存泄漏。
- 构造Adapter时,没有使用缓存的convertView。
- Bitmap对象不在使用时没有调用recycle()释放内存。
- 长生命周期持有短生命周期对象的引用造成的内存泄漏。试着使用关于application的context来替代和activity相关的context。保持对对象生命周期的敏感,特别注意单例、静态对象、全局性集合等的生命周期。
- 注册没取消造成的内存泄漏。
- 集合中对象没清理造成的内存泄漏。集合类如果仅仅有添加元素的方法,而没有相应的删除机制,导致内存被占用。如果这个集合类是全局性的变量 (比如类中的静态属性,全局性的 map 等即有静态引用或 final 一直指向它),那么没有相应的删除机制,很可能导致集合所占用的内存只增不减。
- 单例造成的内存泄漏。由于单例的静态特性使得其生命周期跟应用的生命周期一样长,所以如果使用不恰当的话,很容易造成内存泄漏。
- 匿名内部类和非静态内部类持有外部类的引用造成的内存泄漏。
- Handler 造成的内存泄漏。
参考:
什么情况会导致 OOM
- 加载对象过大,例如:图片、文件等。
- 相应资源过多,没有来不及释放。
- 内存泄漏。
如何避免 OOM:
- 减少对象的内存占用
- 使用更加轻量的数据结构。例如,我们可以考虑使用 ArrayMap 或 SparseArray 而不是 HashMap 等传统数据结构。
- 避免在 Android 里面使用 Enum。
- 减小 Bitmap 对象的内存占用。缩放比例,解码格式。
- 使用更小的图片。
- 内存对象的重复利用
- 复用系统自带的资源。但需要留意不同系统版本的差异性。
- 在 ListView 和 GridView 等大量出现重复子组件的视图里对 ConvertView 复用。
- Bitmap 对象的复用。在 ListView 和 GridView 等显示大量图片的控件里,需要使用 LRU 的机制来缓存处理好的 Bitmap。
- 避免在 onDraw 方法里面执行对象的创建。
- 使用大量字符串拼接操作是,考虑使用 StringBuffer 代替 “+”。
- 避免对象的内存泄漏
- 注意 Activity 的泄漏。1)内部类引用导致 Activity 的泄漏。典型的是 Handler 导致的泄漏。为了解决这个问题,可以在UI退出之前,执行remove Handler消息队列中的消息与runnable对象。或者是使用Static + WeakReference的方式来达到断开Handler与Activity之间存在引用关系的目的。2)Activity Context被传递到其他实例中,这可能导致自身被引用而发生泄漏。
- 考虑使用 Application Context 而不是 Activity Context。对于大部分非必须使用Activity Context的情况(Dialog的Context就必须是Activity Context),我们都可以考虑使用Application Context而不是Activity的Context,这样可以避免不经意的Activity泄露。
- 注意临时Bitmap对象的及时回收。
- 注意监听器的注销。
- 注意缓存容器中的对象泄漏。
- 注意 WebView 的泄漏。
- 注意 Cursor 对象是否及时关闭。
参考:
Android 为每个应用程序分配的内存大小是多少
Android 程序内存一般限制在16M,也有的是24M
查看每个应用程序最高可用内存
int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
Log.d("TAG", "Max memory is " + maxMemory + "KB");
ActivityManager manager = (ActivityManager)getSystemService(Context.ACTIVITY_SERVICE);
//可用堆内存,单个应用可以使用的最大内存,如果应用内存使用超过这个值,就报OOM
int heapgrowthlimit = manager.getMemoryClass();
//进程内存空间分配的最大值,表示的是单个虚拟机可用的最大内存
int heapsize = manager.getLargeMemoryClass();
L.d("heapgrowthlimit = "+heapgrowthlimit+"m"+", heapsize = "+heapsize+"");
//应用程序最大可用内存
int maxMemory = ((int) Runtime.getRuntime().maxMemory())/1024/1024;
//应用程序已获得内存
long totalMemory = ((int) Runtime.getRuntime().totalMemory())/1024/1024;
//应用程序已获得内存中未使用内存
long freeMemory = ((int) Runtime.getRuntime().freeMemory())/1024/1024;
System.out.println("---> maxMemory="+maxMemory+"M,totalMemory="+totalMemory+"M,freeMemory="+freeMemory+"M");
Android中弱引用与软引用的应用场景。
预防内存泄漏!擅用WeakReference!
所有从类外部传来的对象(特别对于Context,View,Fragmet,Activity对象),如果要将其放进类内部的容器对象或者静态类中引用,请一直用WeakReference包装!比如在TabLayout的源码中,容器对象或者静态类中引用,用WeakReference包装。在TabLayoutOnPageChangeListener中,就为TabLayout做了WeakReference wrap。
ANR
ANR 定位和修正
如果开发机器上出现问题,我们可以通过查看/data/anr/traces.txt即可,最新的ANR信息在最开始部分。
出现 ANR 原因:
-
应用在5秒内未响应用户的输入事件(如按键或者触摸)。
-
BroadcastReceiver在10秒内未完成相关的处理。
-
Service在 20秒内无法处理完成。
-
主线程被IO操作(从4.0之后网络IO不允许在主线程中)阻塞。
-
主线程中存在耗时的计算。
-
主线程中错误的操作,比如Thread.wait或者Thread.sleep等。
修正方法:
-
使用AsyncTask处理耗时IO操作。
-
使用Thread或者HandlerThread时,调用Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND)设置优先级,否则仍然会降低程序响应,因为默认Thread的优先级和主线程相同。
-
使用Handler处理工作线程结果,而不是使用Thread.wait()或者Thread.sleep()来阻塞主线程。
-
Activity的onCreate和onResume回调中尽量避免耗时的代码。
-
BroadcastReceiver中onReceive代码也要尽量减少耗时,建议使用IntentService处理
如何避免:
- UI线程尽量只做跟UI相关的工作
- 耗时的操作(比如数据库操作,I/O,连接网络或者别的有可能阻塞UI线程的操作)把它放在单独的线程处理
- 尽量用Handler来处理UIThread和别的Thread之间的交互
- 使用 AsyncTask 时:在doInBackground()方法中执行耗时操作;在onPostExecuted()更新UI 。
- 使用Handler实现异步任务时:在子线程中处理耗时操作,处理完成之后,通过handler.sendMessage()传递处理结果;在handler的handleMessage()方法中更新 UI 或者使用handler.post()方法将消息放到Looper中。
什么是ANR,如何避免ANR。
什么是FC?如何避免FC的发生,另外FC发生时如何捕获相应的uncaught exception?
性能优化
怎么对 Android APP 进行性能优化
合理管理内存
- 节制的使用Service
- 当界面不可见时释放内存
- 当内存紧张时释放内存
- 避免在Bitmap上浪费内存
- 使用优化过的数据集合
- 知晓内存的开支情况
- 谨慎使用抽象编程
- 尽量避免使用依赖注入框架
- 使用多个进程
- 避免内存泄漏
高性能编码优化
- 避免创建不必要的对象
- 静态优于抽象
- 对常量使用static final修饰符
- 使用增强型for循环语法
- 多使用系统封装好的API
- 避免在内部调用Getters/Setters方法
布局优化技巧
- 重用布局文件
- 仅在需要时才加载布局
参考:
Android APP 内存分析工具有哪些
Square 开源库 LeakCanary
参考:
利用 LeakCanary 来检查 Android 内存泄漏
屏幕适配经验
参考:
数据存储
数据存储,数据持久化的方式有哪些
- 网络
- SharedPreference: 除SQLite数据库外,另一种常用的数据存储方式,其本质就是一个xml文件,常用于存储较简单的参数设置。
- SQLite:SQLite是一个轻量级的数据库,支持基本的SQL语法,是常被采用的一种数据存储方式。Android为此数据库提供了一个名为SQLiteDatabase的类,封装了一些操作数据库的api
- File: 即常说的文件(I/O)存储方法,常用语存储大数量的数据,但是缺点是更新数据将是一件困难的事情。
- ContentProvider: Android系统中能实现所有应用程序共享的一种数据存储方式,由于数据通常在各应用间的是互相私密的,所以此存储方式较少使用,但是其又是必不可少的一种存储方式。例如音频,视频,图片和通讯录,一般都可以采用此种方式进行存储。每个Content Provider都会对外提供一个公共的URI(包装成Uri对象),如果应用程序有数据需要共享时,就需要使用Content Provider为这些数据定义一个URI,然后其他的应用程序就通过Content Provider传入这个URI来对数据进行操作。
文件和数据库哪个效率高
- 数据量非常少,可以使用文件。
- 如果涉及到查询等操作,并且数据量大,则应该使用数据库。
SharedPreference 实现
如何导入外部数据库
把数据库存放到 res/raw 目录下,然后在第一次安装启动应用的时间把该数据库拷贝到应用的内部存储空间即 android 系统下的 /data/data/packagename/ 目录下。
谈谈 SQLite
SQLite 数据库升级更新如何保留原来数据
在使用数据库之前,基本上会自定义一个类继承自SQLiteOpenHelper。该类的其中一个构造函数形式是这样的(另一个多出来一个DatabaseErrorHandler):
public SQLiteOpenHelper(Context context, String name,
CursorFactory factory, int version) {
this(context, name, factory, version, null);
}
这个构造函数里面的version参数即是我们设定的版本号。第一次使用数据库时传递的这个版本将被系统记录,并调用SQLiteOpenHelper#onCreate()方法进行建表操作。后续传入的版本如果比这个高,则会调用SQLiteOpenHelper#onUpgrade()方法进行升级。
跨越版本的升级
处理好了单个版本的升级,还有一个更加棘手的问题:如果应用程序发布了多个版本,以致出现了三个以上数据库版本, 如何确保所有的用户升级应用后数据库都能用呢?有两种方式:
- 确定 相邻版本 的差别,从版本1开始依次迭代更新,先执行v1到v2,再v2到v3……
- 为 每个版本 确定与现在数据库的差别,为每个case撰写专门的升级代码。
方式一的优点是每次更新数据库的时候只需要在onUpgrade方法的末尾加一段从上个版本升级到新版本的代码,易于理解和维护,缺点是当版本变多之后,多次迭代升级可能需要花费不少时间,增加用户等待;
方式二的优点则是可以保证每个版本的用户都可以在消耗最少的时间升级到最新的数据库而无需做无用的数据多次转存,缺点是强迫开发者记忆所有版本数据库的完整结构,且每次升级时onUpgrade方法都必须全部重写。
以上简单分析了两种方案的优缺点,它们可以说在花费时间上是刚好相反的,至于如何取舍,可能还需要结合具体情况分析。
参考:
SQLite 性能优化
参考:
网络通讯
描述一次网络请求的流程
- 建立TCP连接。在HTTP工作开始之前,Web浏览器首先要通过网络与Web服务器建立连接,该连接是通过TCP来完成的,该协议与IP协议共同构建Internet,即著名的TCP/IP协议族,因此Internet又被称作是TCP/IP网络。HTTP是比TCP更高层次的应用层协议,根据规则,只有低层协议建立之后才能进行更高层协议的连接,因此,首先要建立TCP连接,一般TCP连接的端口号是80。
- Web浏览器向服务器发送请求命令。一旦建立了TCP连接,Web浏览器就会向Web服务器发送请求命令。例如:GET/sample/hello.jsp HTTP/1.1。
- Web浏览器发送请求头信息。浏览器发送其请求命令之后,还要以头信息的形式向Web服务器发送一些别的信息,之后浏览器发送了一空白行来通知服务器,它已经结束了该头信息的发送。
- Web服务器应答。 客户机向服务器发出请求后,服务器会客户机回送应答, HTTP/1.1 200 OK ,应答的第一部分是协议的版本号和应答状态码。
- Web服务器发送应答头信息。 正如客户端会随同请求发送关于自身的信息一样,服务器也会随同应答向用户发送关于它自己的数据及被请求的文档。
- Web服务器向浏览器发送数据。Web服务器向浏览器发送头信息后,它会发送一个空白行来表示头信息的发送到此为结束,接着,它就以Content-Type应答头信息所描述的格式发送用户所请求的实际数据。
- Web服务器关闭TCP连接。一般情况下,一旦Web服务器向浏览器发送了请求数据,它就要关闭TCP连接,然后如果浏览器或者服务器在其头信息加入了这行代码:Connection:keep-alive
TCP连接在发送后将仍然保持打开状态,于是,浏览器可以继续通过相同的连接发送请求。保持连接节省了为每个请求建立新连接所需的时间,还节约了网络带宽。
推送心跳包是TCP包还是UDP包或者HTTP包
TCP
参考:
Android 代码中实现 WAP 方式联网
- 通过 APN 列表,获取代码服务器和端口号,如果未设置,则设置成对应运营商的配置。
- 实现 HtppClient 代理。
CMWAP, CMNET有何区别,网络通讯时是否要特殊处理?如何切换接入点?
断点续传
首先说多线程,我们要多线程下载一个大文件,就有开启多个线程,多个connection,既然是一个文件分开几个线程来下载,那肯定就是一个线程下载一个部分,不能重复。那么我们这么确定一个线程下载一部分呢,就需要我们在请求的header里面设置。
conn.setRequestProperty("Range", "bytes="+startPos+"-"+endPos);
这里startPos是指从数据端的哪里开始,endPos是指数据端的结束.根据这样我们就知道,只要多个线程,按顺序指定好开始跟结束,就可以不冲突的下载了。那么我们写文件的时候又该怎么写呢。
byte[] buffer = new byte[1024];
int offset = 0;
print("Thread "+this.threadId+" starts to download from position "+startPos);
RandomAccessFile threadFile = new RandomAccessFile(this.saveFile,"rwd");
threadFile.seek(startPos);
// ...
threadFile.write(buffer,0,offset);
这样就可以保证数据的完整性,也不会重复写入了。
那么我们接着说断点续传,断点续传其实也很简单,原理就是使用数据库保存上次每个线程下载的位置和长度。例如我开了两个线程T1,T2来下载一个文件,设文件总大小为1024M,那么就是每个线程下载512M。可是我的下载中断了,那么我下次启动线程的时候(继续下载),是不是应该要知道,我原来下载了多少呢。所以是这样的,我没下载一点,就更新数据库的数据,例如T1,下载了100M,就要实时更新数据库,记录下100M,并且记录下这个线程开始下载位置(startPos),还有线程负责的长度(512M)。那么我继续下载的时候,就可以像服务器请求startPos+1000M开始的数据了,然后在文件里面也是seek(startPos+1000M)的位置继续下载,就可以实现断点续传了。
参考:
网络的优化
参考:
HttpClient
动态加载
插件化,动态加载
参考:
Android中ClassLoader和java中有什么关系和区别?
插件化的原理实际是 Java ClassLoader 的原理,看其他资料前请先看:Java ClassLoader基础
Android 也有自己的 ClassLoader,分为 dalvik.system.DexClassLoader 和 dalvik.system.PathClassLoader,区别在于 PathClassLoader 不能直接从 zip 包中得到 dex,因此只支持直接操作 dex 文件或者已经安装过的 apk(因为安装过的 apk 在 cache 中存在缓存的 dex 文件)。而 DexClassLoader 可以加载外部的 apk、jar 或 dex文件,并且会在指定的 outpath 路径存放其 dex 文件。
参考:
注解
什么是注解
参考:
Java Annotation 及几个常用开源项目注解原理简析
使用注解是否会影响性能
有些注解是用反射实现的所以影响性能。这类注解库对程序的性能影响并没有想象中的那么夸张,而且类似dagger2这类编译时注解的框架是没有性能影响的。
JNI
JNI开发流程
WebView
WebView和JS
React Native
jar
65k限制 做内部库设计时,最重要的考虑是jar的成本,方法数、体积。
Android 傻瓜式分包插件
GitHub:https://github.com/TangXiaoLv/Android-Easy-MultiDex
这是一个可自定义哪些类放在 MainDex 中的插件。ReadMe 中详细介绍了在使用 MultiDex 时,为了解决 MainDex 方法数超标的问题,碰到的一个个坑及如何解决,并列出了详细的参考资料,一篇很不错的文章。
参考:
U8SDK——支持自动拆分成多个dex文件(MultiDex支持)
其他
APP启动过程
如何判断应用被强杀
在Applicatio中定义一个static常量,赋值为-1,在欢迎界面改为0,如果被强杀,application重新初始化,在父类Activity判断该常量的值。
参考:
http://blog.csdn.net/Small_Lee/article/details/51886746
应用被强杀如何解决
如果在每一个Activity的onCreate里判断是否被强杀,冗余了,封装到Activity的父类中,如果被强杀,跳转回主界面,如果没有被强杀,执行Activity的初始化操作,给主界面传递intent参数,主界面会调用onNewIntent方法,在onNewIntent跳转到欢迎页面,重新来一遍流程。
简述静默安装的原理,如何在无需root权限的情况下实现静默安装?
参考:
Serializable和Parcelable的区别
- 都能实现序列化且可用于Intent间的数据传递
- Serializable是Java中的序列化接口,使用简单但开销大,序列化和反序列化过程需要大量I/O操作。
- Parcelable更适合Android平台,使用麻烦但效率高,主要用在内存序列化上。
Debug和Release状态的不同
Toolbar的使用
低版本 SDK 如何实现高版本 API
自己实现或@TargetApi annotation
在低版本的 SDK 使用高版本的 API 会报错。解决方法是:在高版本 SDK 中使用高版本 API,低版本 SDK 中自己实现。
- 在使用了高版本 API 的方法前面加一个 @TargetApi(API版本号)。
- 在代码中判断版本号来控制不同的版本使用不同的代码。
@TargetApi(11)
public void text() {
if(Build.VERSION.SDK_INT >= 11){
// 使用 API 11 的方法
} else {
// 使用自己实现的方法
}
实现一个单例
public class Singleton{
private volatile static Singleton mSingleton;
private Singleton(){
}
public static Singleton getInstance(){
if(mSingleton == null){\\A
synchronized(Singleton.class){\\C
if(mSingleton == null)
mSingleton = new Singleton();\\B
}
}
return mSingleton;
}
}
如 View 的事件分发,屏幕适配经验,性能优化的经验、Java 线程几种用法等
如 AIDL、插件化, 如网络的优化, 如缓存的处理, 如插件化, 如 Service 保活
函数调用Trace 怎么玩
AlarmManager以及Wakelock的使用
算法理解
什么是二分算法
设计模式
Android 中主要用到的几种设计模式
Android设计模式
参考:
http://blog.csdn.net/bboyfeiyu/article/details/44563871
架构设计
mvc mvp mvvm
参考:
http://www.tianmaying.com/tutorial/AndroidMVC