全面总结Android面试知识要点:程序性能优化与数据持久化面试
请点赞,你的点赞对我意义重大,满足下我的虚荣心。
🔥常在河边走,哪有不湿鞋。或许面试过程中你遇到的问题就在这呢?
🔥关注我个人简介,面试不迷路~
一、一张图片100x100在内存中的大小?
这道题想考察什么?
在 Android 开发中,经常需要对图片进行优化,因为图片占用内存比较大,很容易耗尽内存。那么,就需要知道,一张图片的大小是如何计算的,当加载进内存中时,占用的空间又是多少?
考察的知识点
内存优化,图片内存占用如何计算
考生如何回答
我们有一张分辨率是100x100的图片,而我们在电脑上看到的这张 png 图片大小仅有 2.31KB,那么问题来了:

我们看到的一张大小为 2.31KB 的 png 图片,它在内存中占有的大小也是 2.31KB 吗?
理清这点蛮重要的,因为碰到过有人说,我一张图片就几 KB,虽然界面上显示了上百张,但为什么内存占用却这么高?
所以,我们需要搞清楚一个概念:我们在电脑上看到的 png 格式或者 jpg 格式的图片,png(jpg) 只是这张图片的容器,它们是经过相对应的压缩算法将原图每个像素点信息转换用另一种数据格式表示,以此达到压缩目的,减少图片文件大小。
而当我们通过代码,将这张图片加载进内存时,会先解析图片文件本身的数据格式,然后还原为位图,也就是 Bitmap 对象,Bitmap 的大小取决于像素点的数据格式以及分辨率两者了。所以,一张的图片文件的大小与文件格式(png 或者 jpg 格式),跟这张图片加载进内存所占用的大小完全是两回事。
图片内存大小
一般的,计算一张图片占用的内存大小公式:分辨率 * 每个像素点的大小。
每个像素点的大小:在Android中一般我们会以RGB_565或者ARGB_8888格式加载位图,其中RGB_565表示:R占用5位数据,G使用6位表示,B也是5位,则一个像素点为:16位两字节。(ARGB_8888同理)
比如100x100分辨的图片,以RGB_565加载进入内存,则其内存大小为:100x100x2 = 20000 字节。
但是需要注意的是:使用 Android BitmapFactory 加载 Bitmap时,如果加载res目录下的图片,图片被加载进内存时的分辨率会经过一层转换,所以,虽然最终图片大小的计算公式仍旧是分辨率*像素点大小,但此时的分辨率已不是图片本身的分辨率了,系统会根据设备当前的 dpi 值以及资源目录所对应的 dpi 值,做一次分辨率转换,规则如下:
- 转换后高度 = 原图高度 * (设备的 dpi / 目录对应的 dpi )
- 转换后宽度 = 原图宽度 * (设备的 dpi / 目录对应的 dpi )
其中目标表示drawable-hdpi,drawable-xhdpi:

如:设备240dpi,100x100的图片放在drawable-hdpi,使用BitmapFactory.decodeResource
加载的内存为:
转换后分辨率:100 (240/240) 100 * (240*240) ,则此图片内存占用为:100 * 100 * 像素大小。
而如果将图片放入 mdpi 中,则内存占用为:100 * (240 / 160) * 100 * (240 / 160) * 像素大小 ,图片内存会比hdpi更大。
如果图片不是通过BitmapFactory.decodeResource
加载,则不会出现上述被转换的情况。
二、内存优化,内存抖动和内存泄漏。(东方头条)
这道题想考察什么?
内存抖动与内存泄漏是什么,会对程序造成什么影响?为什么会产生这些影响?
考察的知识点
内存优化、JVM GC
考生如何回答
什么是内存抖动?
在Java中,每创建一个对象,就会申请一块内存,存储对象信息;每分配一块内存,程序的可用内存也就少一块;当程序被占用的内存达到一定临界程度,GC 也就是垃圾回收器(Garbage Collector)就会出动,来释放掉一部分不再被使用的内存。 这本身没有问题,但是当频繁创建对象就会造成内存不断地攀升,在回收了之后又迅速涨起来,接着又一次的回收。在短时间内反复地发生内存增长和回收,这就是内存抖动(Memory Churn)。
我们可以通过 Android Studio 的 Memory Profiler 来直观地观察到这种现象:

内存抖动的问题
内存抖动可能导致程序卡顿甚至OOM内存溢出。
卡顿
内存的回收在Java当中采用的是GC机制,无论是何种方式实现的GC在执行的时候都不可避免的需要 STW(Stop The World) 。STW意味着我们所有的工作线程都将会被暂停,虽然这个时间很短,但终究是有时间成本的。一两次内存回收不容易被用户察觉,但多次内存回收行为集中在短时间内爆发,这就造成了比较大的界面卡顿的风险。 例如当用户点击某个按钮,或者在界面中进行滑动时,此时虚拟机在运行GC线程,进行内存回收,那响应用户点击事件的线程就被GC暂停了,只能在恢复后才能响应,因此给到用户最直观的感受就是程序卡了。
OOM
内存抖动除了可能造成卡顿之外,也可能会造成内存溢出(OOM)。这是因为如果垃圾回收的实现采用的是标记-清除算法,那么此算法可能导致大量的内存碎片。

当我们程序频繁的创建与回收对象(内存抖动),那么可能就会导致程序中连续内存不足。比如上图中,我们需要创建一个占用10个格子大小内存的字节数组对象,此时就会出现OOM。因为虽然在内存回收后,拥有不止10个格子大小的可用内存,但是没有10个连续的白色格子(可用内存)。这就是内存碎片,空闲的连续空间比要申请的空间小,导致这些小内存块不能被利用。
Android 在官方文档和 Android Studio 里都建议我们尽量避免在 View的onDraw() 里创建对象,就是因为onDraw方法可能会被频繁的调用。因此我们应该避免在可能会频繁被执行的、循环体内创建一个新对象。
什么是内存泄露
内存泄漏(Memory Leak)是指程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。 在Java中,就是该释放的对象无法被释放,那这些对象将一直内存,最终导致程序可用内存越来越少,直至无内存可用(OOM) 。
为什么会出现这种情况?这就需要了解GC机制是如何判断一个对象是否可被回收的。垃圾对象检测主要有两种算法:引用计数法和可达性分析法
引用计数法
所谓的引用计数法就是给每个对象一个引用计数器,每当有一个地方引用它时,计数器就会加1;当引用失效时,计数器的值就会减1;任何时刻计数器的值为0的对象就是不可能再被使用的;但是当两个对象互相引用会导致无法回收。
这种方法没有被Java使用,Java中采用的是可达性分析法.
可达性分析法
通过一系列称为“GC Roots”的对象作为起始点,从这些节点向下搜索,搜索所有的引用链,当一个对象到GC Roots没有任何引用链(即GC Roots到对象不可达)时,则证明此对象是不可用的。

比如:当我们某个Activity在finish退出之后,我们希望这个Activity对象能及时被回收掉,但是因为此Activity对象被一个单例(GC Root)引用着,那就导致Activity无法被回收,出现内存泄露。
public class Manager {
//GC ROOT
private static final Manager ourInstance = new Manager();
private Context mContext; //mContext是Activity则会导致此Activity被GC Root持有引用
public static Manager getInstance() {
return ourInstance;
}
private Manager() {
}
public void init(Context context){
mContext = context;
}
}
而要修改上面的代码,可以在允许传递Application的情况下,尽量传递Application,或者直接使用context.getApplicationContext()
避免传递Activity。也可以采用非强引用的方式(见Java中有几种引用关系,它们的区别是什么?)
三、什么时候会发生内存泄漏?举几个例子(美团)
这道题想考察什么?
- 是否了解内存泄漏的真实场景使用,是否熟悉内存泄漏引发的场景?
考察的知识点
- 内存泄漏在项目中使用与基本知识
考生应该如何回答
1.内存泄漏是什么?
答:
说白了以大白话来说,就是指一个对象已经不再使用了,本应该被回收,但由于某些原因导致对象无法回收,仍然占用着内存,这种长时间占用着内存,意味着内存无法被释放,当内存达到一定值时,就会有存在内存泄漏风险。
2.为什么会产生内存泄漏,内存泄漏会导致什么问题?
第一点需知:Java语言相比C++语音是需要手动去管理对象的创建和回收,Java有着自己的一套垃圾回收机制,它能够自动回收内存,但是它往往会因为某些原因而变得“不靠谱”。
第二点需知:在Android开发中,一些不好的编码习惯就很可能会导致内存泄漏,而这些内存泄漏会导致应用内存越占越大,使得应用变得卡顿,甚至造成OOM(Out Of Memory)内存溢出问题,同时也使应用变得极其不稳定,因为当内存不足的时候,系统会优先回收那些“内存占比”大的应用。
第三点需知:首先我们先来了解下Java的内存分配机制,Java 程序运行时的内存分配策略有三种,分别是静态分配,栈式分配,和堆式分配,对应的,三种存储策略使用的内存空间主要分别是静态存储区(也称方法区)、栈区和堆区。
第四点需知:静态存储区(方法区):主要存放静态数据、全局 static 数据和常量。这块内存在程序编译时就已经分配好,并且在程序整个运行期间都存在。
第五点需知:栈区 :当方法被执行时,方法体内的局部变量(其中包括基础数据类型、对象的引用)都在栈上创建,并在方法执行结束时这些局部变量所持有的内存将会自动被释放。因为栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。
第六点需知:堆区 : 又称动态内存分配,通常就是指在程序运行时直接 new 出来的内存,也就是对象的实例。这部分内存在不使用时将会由 Java 垃圾回收器来负责回收。
总结:为了更好理解 GC 的工作原理,我们可以将对象考虑为有向图的顶点,将引用关系考虑为图的有向边,有向边从引用者指向被引对象。另外,每个线程对象可以作为一个图的起始顶点,例如大多程序从 main 进程开始执行,那么该图就是以 main 进程顶点开始的一棵根树。在这个有向图中,根顶点可达的对象都是有效对象,GC将不回收这些对象。如果某个对象 (连通子图)与这个根顶点不可达(注意,该图为有向图),那么我们认为这个(这些)对象不再被引用,可以被 GC 回收。
3.请你说说你在工作中,哪些情况会引发内存泄漏?
答:
情况1会引发单例引起的内存泄漏 由于单例的静态特性导致它的生命周期和整个应用的生命周期一样长,如果有对象已经不再使用了,但又却被单例持有引用,那么就会导致这个对象就没办法被回收,从而导致内存泄漏。
// 使用了单例模式
public class AppManagerXiangxue {
private static AppManagerXiangxue instance;
private Context context;
private AppManagerXiangxue(Context context) {
this.context = context;
}
public static AppManagerXiangxue getInstance(Context context) {
if (instance != null) {
instance = new AppManagerXiangxue(context);
}
return instance;
}
}
我们来分析问题所在: 从上面的代码我们可以看出,在创建单例对象的时候,引入了一个Context上下文对象,如果我们把Activity注入进来,会导致这个Activity一直被单例对象持有引用,当这个Activity销毁的时候,对象也是没有办法被回收的。
才有的解决方案: 在这里我们只需要让这个上下文对象指向应用的上下文即可(this.context=context.getApplicationContext()
),因为应用的上下文对象的生命周期和整个应用一样长。
情况2会引发非静态内部类创建静态实例引起的内存泄漏
由于非静态内部类会默认持有外部类的引用,如果我们在外部类中去创建这个内部类对象,当频繁打开关闭Activity,会导致重复创建对象,造成资源的浪费,为了避免这个问题我们一般会把这个实例设置为静态,这样虽然解决了重复创建实例,但是会引发出另一个问题,就是静态成员变量它的生命周期是和应用的生命周期一样长的,然而这个静态成员变量又持有该Activity的引用,所以导致这个Activity销毁的时候,对象也是无法被回收的。
public class MainActivity extends AppCompatActivity {
private static TestResource mResource = null;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
if(mResource == null){
mResource = new TestResource();
}
//...
}
class TestResource {
//...
}
}
我们来分析问题所在: 其实这个和上面单例对象的内容泄漏问题是一样的,由于静态对象持有Activity的引用,导致Activity没办法被回收。
采用的解决方案: 在这里我们只需要把非静态内部类改成静态内部类即可(static class TestResource
)。
情况3会引发Handler引起的内存泄漏
记得我们刚学习Handler的时候,网上资料甚至学校教材“教科书”式的写法都是这样的
Handler mHandler=new Handler(){
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
//to do something..
switch (msg.what){
case 0:
//to do something..
break;
}
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
new Thread(new Runnable() {
@Override
public void run() {
//to do something..
mHandler.sendEmptyMessage(0);
}
}).start();
}
我们来分析问题所在: 别看上面短短几行代码,其实涉及到了很多问题,首先我们知道程序启动时在主线程中会创建一个Looper对象,这个Looper里维护着一个MessageQueue消息队列,这个消息队列里会按时间顺序存放着Message,然后上面的Handler是通过内部类来创建的,内部类会持有外部类的引用,也就是Handler持有Activity的引用,而消息队列中的消息target是指向Handler的,也就等同消息持有Handler的引用,也就是说当消息队列中的消息如果还没有处理完,这些未处理的消息(也可以理解成延迟操作)是持有Activity的引用的,此时如果关闭Activity,是没办法回收的,从而就会导致内存泄露。
解决方案: 和上文一样,我们需要先把非静态内部类改成静态内部类(如果是Runnable类也需要改成静态),然后在Activity的onDestroy中移除对应的消息,再来需要在Handler内部用弱引用持有Activity,因为让内部类不再持有外部类的引用时,程序也就不允许Handler操作Activity对象了。
MyHandler myHandler = new MyHandler(this);
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
new Thread(new Runnable() {
@Override
public void run() {
myHandler.sendMessage(Message.obtain());
}
}).start();
}
@Override
protected void onDestroy() {
super.onDestroy();
//移除对应的Runnable或者是Message
//mHandler.removeCallbacks(runnable);
//mHandler.removeMessages(what);
mHandler.removeCallbacksAndMessages(null);
}
private static class MyHandler extends Handler {
private WeakReference<Activity> mActivity;
public MyHandler(Activity activity) {
mActivity = new WeakReference<Activity>(activity);
}
@Override
public void handleMessage(Message msg) {
if (mActivity.get() == null) {
return;
}
//to do something..
}
};
情况4会引发WebView引起的内存泄露 关于WebView的内存泄漏,这是个绝对的大大大大大坑!不同版本都存在着不同版本的问题,这里我只能给出我平时的处理方法,可能不同机型上存在的差异,只能靠积累了。 方法一: 首先不要在xml去定义<WebView/>,定义一个ViewGroup就行,然后动态在代码中new WebView(Context context)
(传入的Context采取弱引用),再通过addView添加到ViewGroup中,最后在页面销毁执行onDestroy()的时候把WebView移除。 方法二: 简单粗暴,直接为WebView新开辟一个进程,在结束操作的时候直接System.exit(0)
结束掉进程,这里需要注意进程间的通讯,可以采取Aidl,Messager,Content Provider,Broadcast等方式。
情况5会引发Asynctask引起的内存泄露 这部分和Handler比较像,其实也是因为内部类持有外部类引用,一样的改成静态内部类,然后在onDestory方法中取消任务即可。
情况6会引发资源对象未关闭引起的内存泄露 这块就比较简单了,比如我们经常使用的广播接收者,数据库的游标,多媒体,文档,套接字等。
情况7会引发其他一些 还有一些需要注意的,比如注册了EventBus没注销,添加Activity到栈中,销毁的时候没移除等。
Bitmap压缩,质量100%与90%的区别?(东方头条)
这道题想考察什么?
- 是否熟悉Bitmap质量压缩
- 是否熟悉Bitmap的压缩机理
考察的知识点
- Bitmap质量压缩compress的原理
- Bitmap的压缩机理
考生应该如何回答
1、图片常用的压缩格式:
其中字母代表的意思我们大概都可以理解,接下来我们来算算它们单个像素点的字节数:
- ALPHA_8:表示8位Alpha位图,即透明度占8个位,一个像素点占用1个字节,它没有颜色,只有透明度。
- ARGB_4444:表示16位ARGB位图,即A=4,R=4,G=4,B=4,一个像素点占4+4+4+4=16位,2个字节。
- ARGB_8888:表示32位ARGB位图,即A=8,R=8,G=8,B=8,一个像素点占8+8+8+8=32位,4个字节。
- RGB_565 :表示16位RGB位图,即R=5,G=6,B=5,它没有透明度,一个像素点占5+6+5=16位,2个字节
我们在做压缩处理的时候,可以先通过改变Bitmap的图片格式,来达到压缩的效果,其实压缩最主要就是要么改变其宽高,要么就通过减少其单个像素占用的内存。
2、质量压缩:
private void compressQuality() {
Bitmap bm = BitmapFactory.decodeResource(getResources(), R.drawable.test);
mSrcSize = bm.getByteCount() + "byte";
ByteArrayOutputStream bos = new ByteArrayOutputStream();
bm.compress(Bitmap.CompressFormat.JPEG, 100, bos);
byte[] bytes = bos.toByteArray();
mSrcBitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length);
}
质量压缩不会减少图片的像素,它是在保持像素的前提下改变图片的位深及透明度,来达到压缩图片的目的,图片的长,宽,像素都不会改变,那么bitmap所占内存大小是不会变的。我们可以看到有个参数:quality,可以调节你压缩的比例,但是还要注意一点就是,质量压缩堆png格式这种图片没有作用,因为png是无损压缩。
/**
* Write a compressed version of the bitmap to the specified outputstream.
* If this returns true, the bitmap can be reconstructed by passing a
* corresponding inputstream to BitmapFactory.decodeStream(). Note: not
* all Formats support all bitmap configs directly, so it is possible that
* the returned bitmap from BitmapFactory could be in a different bitdepth,
* and/or may have lost per-pixel alpha (e.g. JPEG only supports opaque
* pixels).
*
* @param format The format of the compressed image
* @param quality Hint to the compressor, 0-100. 0 meaning compress for
* small size, 100 meaning compress for max quality. Some
* formats, like PNG which is lossless, will ignore the
* quality setting
* @param stream The outputstream to write the compressed data.
* @return true if successfully compressed to the specified stream.
*/
public boolean compress(CompressFormat format, int quality, OutputStream stream) {
checkRecycled("Can't compress a recycled bitmap");
// do explicit check before calling the native method
if (stream == null) {
throw new NullPointerException();
}
if (quality < 0 || quality > 100) {
throw new IllegalArgumentException("quality must be 0..100");
}
return nativeCompress(mNativeBitmap, format.nativeInt, quality,
stream, new byte[WORKING_COMPRESS_STORAGE]);
}
int count = image.getWidth() * image.getHeight() / 1024;
Log.d("bitmap:compress", "压缩前:" + count);
ByteArrayOutputStream bout = new ByteArrayOutputStream();
// 质量压缩方法,这里100表示不压缩,把压缩后的数据存放到baos中
image.compress(Bitmap.CompressFormat.JPEG, 50, bout); //修改第二个参数
count = image.getWidth() * image.getHeight() / 1024;
Log.d("bitmap:compress", "压缩后:" + count);
Log.d("bitmap:compress", "压缩后:" + bout.toByteArray().length / 1024);
可以发现bitmap基本上没有被压缩吧。 压缩后的数据被写到 bout里面去了。
今天的面试分享到此结束拉~下期在见