Android面试题内存&性能篇,由本人整理汇总,后续将继续推出系列篇,如果喜欢请持续关注和推荐,更多内容可以关注微信公众号(Android高级编程):android-tech
系列文章目录:
- Android面试题View篇
- Android面试题进程篇
- Android面试题线程篇
- Android面试题网络篇
- Android面试题架构篇
- Android面试题系统原理篇
内存分配
RAM(random access memory)随机存取存储器。说白了就是内存。 一般Java在内存分配时会涉及到以下区域:
- 寄存器(Registers):速度最快的存储场所,因为寄存器位于处理器内部,我们在程序中无法控制
- 栈(Stack):存放基本类型的数据和对象的引用,但对象本身不存放在栈中,而是存放在堆中
- 堆(Heap):堆内存用来存放由new创建的对象和数组。在堆中分配的内存,由Java虚拟机的自动垃圾回收器(GC)来管理。
- 静态域(static field): 静态存储区域就是指在固定的位置存放应用程序运行时一直存在的数据,Java在内存中专门划分了一个静态存储区域来管理一些特殊的数据变量如静态的数据变量
- 常量池(constant pool):虚拟机必须为每个被装载的类型维护一个常量池。常量池就是该类型所用到常量的一个有序集和,包括直接常量(string,integer和floating point常量)和对其他类型,字段和方法的符号引用。
- 非RAM存储:硬盘等永久存储空间
堆栈存储特点对比:
- 栈:当定义一个变量时,Java就在栈中为这个变量分配内存空间,当该变量退出该作用域后,Java会自动释放掉为该变量所分配的内存空间,该内存空间可以立即被另作他用。
- 堆:当堆中的new产生数组和对象超出其作用域后,它们不会被释放,只有在没有引用变量指向它们的时候才变成垃圾,不能再被使用。即使这样,所占内存也不会立即释放,而是等待被垃圾回收器收走。这也是Java比较占内存的原因。
堆栈运行特点对比:
- 栈:存取速度比堆要快,仅次于寄存器。但缺点是,存在栈中的数据大小与生存期必须是确定的,缺乏灵活性。
- 堆:堆是一个运行时数据区,可以动态地分配内存大小,因此存取速度较慢。也正因为这个特点,堆的生存期不必事先告诉编译器,而且Java的垃圾收集器会自动收走这些不再使用的数据。
对象引用类型:
引用分为四种级别,这四种级别由高到低依次为:强引用>软引用>弱引用>虚引用。
- 强引用(strong reference)
如:Object object=new Object(),object就是一个强引用了。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题。
- 软引用(SoftReference)
只有内存不够时才回收,常用于缓存;当内存达到一个阀值,GC就会去回收它;
- 弱引用(WeakReference)
弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它 所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。
- 虚引用(PhantomReference)
"虚引用"顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收。
UI优化
- a.合理选择RelativeLayout、LinearLayout、FrameLayout,RelativeLayout会让子View调用2次onMeasure,而且布局相对复杂时,onMeasure相对比较复杂,效率比较低,LinearLayout在weight>0时也会让子View调用2次onMeasure。LinearLayout weight测量分配原则。
- b.使用标签
- c.减少布局层级,可以通过手机开发者选项>GPU过渡绘制查看,一般层级控制在4层以内,超过5层时需要考虑是否重新排版布局。
- d.自定义View时,重写onDraw()方法,不要在该方法中新建对象,否则容易触发GC,导致性能下降
- e.使用ListView时需要复用contentView,并使用Holder减少findViewById加载View。
- f.去除不必要背景,getWindow().setBackgroundDrawable(null)
- g.使用TextView的leftDrawabel/rightDrawable代替ImageView+TextView布局
内存优化
主要为了避免OOM和频繁触发到GC导致性能下降
- a.Bitmap.recycle(),Cursor.close,inputStream.close()
- b.大量加载Bitmap时,根据View大小加载Bitmap,合理选择inSampleSize,RGB_565编码方式;使用LruCache缓存
- c.使用 静态内部类+WeakReference 代替内部类,如Handler、线程、AsyncTask
- d.使用线程池管理线程,避免线程的新建
- e.使用单例持有Context,需要记得释放,或者使用全局上下文
- f.静态集合对象注意释放
- g.属性动画造成内存泄露
- h.使用webView,在Activity.onDestory需要移除和销毁,webView.removeAllViews()和webView.destory()
备:使用LeakCanary检测内存泄露
响应速度优化
Activity如果5秒之内无法响应屏幕触碰事件和键盘输入事件,就会出现ANR,而BroadcastReceiver如果10秒之内还未执行操作也会出现ANR,Serve20秒会出现ANR 为了避免ANR,可以开启子线程执行耗时操作,但是子线程不能更新UI,因此需要Handler消息机制、AsyncTask、IntentService进行线程通信。
备:出现ANR时,adb pull data/anr/tarces.txt 结合log分析
其他性能优化
- a.常量使用static final修饰
- b.使用SparseArray代替HashMap
- c.使用线程池管理线程
- d.ArrayList遍历使用常规for循环,LinkedList使用foreach
- e.不要过度使用枚举,枚举占用内存空间比整型大
- f.字符串的拼接优先考虑StringBuilder和StringBuffer
- g.数据库存储是采用批量插入+事务
Android内存泄露及管理
(1)内存溢出(OOM)和内存泄露(对象无法被回收)的区别。 (2)引起内存泄露的原因
(3)内存泄露检测工具---->LeakCanary
内存溢出 out of memory:是指程序在申请内存时,没有足够的内存空间供其使用,出现out of memory;比如申请了一个integer,但给它存了long才能存下的数,那就是内存溢出。内存溢出通俗的讲就是内存不够用。
内存泄露 memory leak:是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光
内存泄露原因:
- Handler 引起的内存泄漏。
解决:将Handler声明为静态内部类,就不会持有外部类SecondActivity的引用,其生命周期就和外部类无关,如果Handler里面需要context的话,可以通过弱引用方式引用外部类
- 单例模式引起的内存泄漏。
解决:Context是ApplicationContext,由于ApplicationContext的生命周期是和app一致的,不会导致内存泄漏
- 非静态内部类创建静态实例引起的内存泄漏。
解决:把内部类修改为静态的就可以避免内存泄漏了
- 非静态匿名内部类引起的内存泄漏。
解决:将匿名内部类设置为静态的。
- 注册/反注册未成对使用引起的内存泄漏。
注册广播接受器、EventBus等,记得解绑。
- 资源对象没有关闭引起的内存泄漏。
在这些资源不使用的时候,记得调用相应的类似close()、destroy()、recycler()、release()等方法释放。
- 集合对象没有及时清理引起的内存泄漏。
通常会把一些对象装入到集合中,当不使用的时候一定要记得及时清理集合,让相关对象不再被引用。
JAVA相关性能优化
1 不用new关键词创建类的实例
用new关键词创建类的实例时,构造函数链中的所有构造函数都会被自动调用。但如果一个对象实现了Cloneable接口,我们可以调用它的clone()方法。clone()方法不会调用任何类构造函数。
在使用设计模式(Design Pattern)的场合,如果用Factory模式创建对象,则改用clone()方法创建新的对象实例非常简单。例如,下面是Factory模式的一个典型实现:
public static Credit getNewCredit() {
return new Credit();
}
改进后的代码使用clone()方法,如下所示:
private static Credit BaseCredit = new Credit();
public static Credit getNewCredit() {
return (Credit) BaseCredit.clone();
}
上面的思路对于数组处理同样很有用。
2 使用非阻塞I/O
版本较低的JDK不支持非阻塞I/O API。为避免I/O阻塞,一些应用采用了创建大量线程的办法(在较好的情况下,会使用一个缓冲池)。这种技术可以在许多必须支持并发I/O流的应用中见 到,如Web服务器、报价和拍卖应用等。然而,创建Java线程需要相当可观的开销。 JDK 1.4引入了非阻塞的I/O库(java.nio)。如果应用要求使用版本较早的JDK,在这里有一个支持非阻塞I/O的软件包。
3 慎用异常
异 常对性能不利。抛出异常首先要创建一个新的对象。Throwable接口的构造函数调用名为fillInStackTrace()的本地(Native) 方法,fillInStackTrace()方法检查堆栈,收集调用跟踪信息。只要有异常被抛出,VM就必须调整调用堆栈,因为在处理过程中创建了一个新 的对象。 异常只能用于错误处理,不应该用来控制程序流程。
4 不要重复初始化变量
默认情况下,调用类的构造函数时, Java会把变量初始化成确定的值:所有的对象被设置成null,整数变量(byte、short、int、long)设置成0,float和 double变可柚贸?.0,逻辑值设置成false。当一个类从另一个类派生时,这一点尤其应该注意,因为用new关键词创建一个对象时,构造函数链中 的所有构造函数都会被自动调用。
5 尽量指定类的final修饰符
带有final修饰符的类是不可派生的。在Java核心API中,有许多应用final的例子,例如java.lang.String。为String类指定final防止了人们覆盖length()方法。 另外,如果指定一个类为final,则该类所有的方法都是final。Java编译器会寻找机会内联(inline)所有的final方法(这和具体的编译器实现有关)。此举能够使性能平均提高50%。
6 尽量使用局部变量
调用方法时传递的参数以及在调用中创建的临时变量都保存在栈(Stack)中,速度较快。其他变量,如静态变量、实例变量等,都在堆(Heap)中创建,速度较慢。另外,依赖于具体的编译器/JVM,局部变量还可能得到进一步优化。请参见《尽可能使用堆栈变量》。
7 乘法和除法
考虑下面的代码: for (val = 0; val < 100000; val +=5) { alterX = val * 8; myResult = val * 2; } 用移位操作替代乘法操作可以极大地提高性能。下面是修改后的代码: for (val = 0; val < 100000; val += 5) { alterX = val << 3; myResult = val << 1; }
8.字符串拼接
不要随意的使用stingA=StringB+StringC的写法,有大量拼接操作的地方用StringBuilder代替