V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
heitao1
V2EX  ›  Android

Android 内存优化,如何避免 OOM

  •  1
     
  •   heitao1 · 2016-09-21 10:17:41 +08:00 · 8839 次点击
    这是一个创建于 2991 天前的主题,其中的信息可能已经有所发展或是发生改变。
    一、 Android 的内存机制
    二、 Android 的内存溢出
    三、万恶的 static
    四、都是线程惹的祸
    五、超级大胖子 Bitmap
    六、行踪诡异的 Cursor
    七、其它要说的。
    一、 Android 的内存机制
    Android 的程序由 Java 语言编写,所以 Android 的内存管理与 Java 的内存管理相似。程序员通过 new 为对象分配内存,所有对象在 java 堆内分配空间;然而对象的释放是由垃圾回收器来完成的。 C / C++中的内存机制是“谁污染,谁治理”, java 的就比较人性化了,给我们请了一个专门的清 洁工( GC )。

    那么 GC 怎么能够确认某一个对象是不是已经被废弃了呢? Java 采用了有向图的原理。 Java 将引用关系考虑为图的有向边,有向边从引用者指向引用对象。 线程对象可以作为有向图的起始顶点,该图就是从起始顶点开始的一棵树,根顶点可以到达的对象都是有效对象, GC 不会回收这些对象。如果某个对象 (连通子图)与这个根顶点不可达(注意,该图为有向图),那么我们认为这个(这些)对象不再被引用,可以被 GC 回收。

    二、 Android 的内存溢出
    Android 的内存溢出是如何发生的?

    Android 的虚拟机是基于寄存器的 Dalvik ,它的最大堆大小一般是 16M ,有的机器为 24M 。因此我们所能利用的内存空间是有限的。如果我们的内存占用超过了一定的水平就会出现 OutOfMemory 的错误。

    为什么会出现内存不够用的情况呢?我想原因主要有两个:

    由于我们程序的失误,长期保持某些资源(如 Context )的引用,造成内存泄露,资源造成得不到释放。
    保存了多个耗用内存过大的对象(如 Bitmap ),造成内存超出限制。

    三、万恶的 static
    static 是 Java 中的一个关键字,当用它来修饰成员变量时,那么该变量就属于该类,而不是该类的实例。所以用 static 修饰的变量,它的生命周期是很长的,如果用它来引用一些资源耗费过多的实例( Context 的情况最多),这时就要谨慎对待了。

    public class ClassName {
    private static Context mContext;
    //省略
    }
    以上的代码是很危险的,如果将 Activity 赋值到么 mContext 的话。那么即使该 Activity 已经 onDestroy ,但是由于仍有对象保存它的引用,因此该 Activity 依然不会被释放。

    我们举 Android 文档中的一个例子。

    private static Drawable sBackground;

    @Override
    protected void onCreate(Bundle state) {
    super.onCreate(state);

    TextView label = new TextView(this);
    label.setText(“ Leaks are bad ”);

    if (sBackground == null) {
    sBackground = getDrawable(R.drawable.large_bitmap);
    }
    label.setBackgroundDrawable(sBackground);

    setContentView(label);
    }
    sBackground, 是一个静态的变量,但是我们发现,我们并没有显式的保存 Contex 的引用,但是,当 Drawable 与 View 连接之后, Drawable 就将 View 设置为一个回调,由于 View 中是包含 Context 的引用的,所以,实际上我们依然保存了 Context 的引用。这个引用链如下:

    Drawable->TextView->Context
    所以,最终该 Context 也没有得到释放,发生了内存泄露。

    如何才能有效的避免这种引用的发生呢?

    第一,应该尽量避免 static 成员变量引用资源耗费过多的实例,比如 Context 。

    第二、 Context 尽量使用 Application Context ,因为 Application 的 Context 的生命周期比较长,引用它不会出现内存泄露的问题。

    第三、使用 WeakReference 代替强引用。比如可以使用 WeakReference mContextRef;

    该部分的详细内容也可以参考 Android 文档中 Article 部分。

    四、都是线程惹的祸
    线程也是造成内存泄露的一个重要的源头。线程产生内存泄露的主要原因在于线程生命周期的不可控。我们来考虑下面一段代码。

    public class MyActivity extends Activity {
    @Override
    public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);
    new MyThread().start();
    }

    private class MyThread extends Thread{
    @Override
    public void run() {
    super.run();
    //do somthing
    }
    }
    }
    这段代码很平常也很简单,是我们经常使用的形式。我们思考一个问题:假设 MyThread 的 run 函数是一个很费时的操作,当我们开启该线程后,将设备的 横屏变为了竖屏,一般情况下当屏幕转换时会重新创建 Activity ,按照我们的想法,老的 Activity 应该会被销毁才对,然而事实上并非如此。

    由于我们的线程是 Activity 的内部类,所以 MyThread 中保存了 Activity 的一个引用,当 MyThread 的 run 函数没有结束时, MyThread 是不会被销毁的,因此它所引用的老的 Activity 也不会被销毁,因此就出现了内存泄露的问题。

    有些人喜欢用 Android 提供的 AsyncTask ,但事实上 AsyncTask 的问题更加严重, Thread 只有在 run 函数不结束时才出现这种内存 泄露问题,然而 AsyncTask 内部的实现机制是运用了 ThreadPoolExcutor,该类产生的 Thread 对象的生命周期是不确定的,是应用 程序无法控制的,因此如果 AsyncTask 作为 Activity 的内部类,就更容易出现内存泄露的问题。

    这种线程导致的内存泄露问题应该如何解决呢?

    第一、将线程的内部类,改为静态内部类。

    第二、在线程内部采用弱引用保存 Context 引用。

    解决的模型如下:

    public abstract class WeakAsyncTaskProgress, Result, WeakTarget> extends
    AsyncTaskProgress, Result> {
    protected WeakReference mTarget;

    public WeakAsyncTask(WeakTarget target) {
    mTarget = new WeakReference(target);
    }

    @Override
    protected final void onPreExecute() {
    final WeakTarget target = mTarget.get();
    if (target != null) {
    this.onPreExecute(target);
    }
    }

    @Override
    protected final Result doInBackground(Params … params) {
    final WeakTarget target = mTarget.get();
    if (target != null) {
    return this.doInBackground(target, params);
    } else {
    return null;
    }
    }

    @Override
    protected final void onPostExecute(Result result) {
    final WeakTarget target = mTarget.get();
    if (target != null) {
    this.onPostExecute(target, result);
    }
    }

    protected void onPreExecute(WeakTarget target) {
    // No default action
    }

    protected abstract Result doInBackground(WeakTarget target, Params … params);

    protected void onPostExecute(WeakTarget target, Result result) {
    // No default action
    }
    }
    事实上,线程的问题并不仅仅在于内存泄露,还会带来一些灾难性的问题。由于本文讨论的是内存问题,所以在此不做讨论。

    由于 51cto 不让我一次传完,说我的字数太多了,所以分开传了。

    五、超级大胖子 Bitmap
    可以说出现 OutOfMemory 问题的绝大多数人,都是因为 Bitmap 的问题。因为 Bitmap 占用的内存实在是太多了,它是一个“超级大胖子”,特别是分辨率大的图片,如果要显示多张那问题就更显著了。

    如何解决 Bitmap 带给我们的内存问题?

    第一、及时的销毁。

    虽然,系统能够确认 Bitmap 分配的内存最终会被销毁,但是由于它占用的内存过多,所以很可能会超过 java 堆的限制。因此,在用完 Bitmap 时,要 及时的 recycle 掉。 recycle 并不能确定立即就会将 Bitmap 释放掉,但是会给虚拟机一个暗示:“该图片可以释放了”。

    第二、设置一定的采样率。

    有时候,我们要显示的区域很小,没有必要将整个图片都加载出来,而只需要记载一个缩小过的图片,这时候可以设置一定的采样率,那么就可以大大减小占用的内存。如下面的代码:

    private ImageView preview;
    BitmapFactory.Options options = new BitmapFactory.Options();
    options.inSampleSize = 2; //图片宽高都为原来的二分之一,即图片为原来的四分之一
    Bitmap bitmap = BitmapFactory.decodeStream(cr.openInputStream(uri), null, options);
    preview.setImageBitmap(bitmap);
    第三、巧妙的运用软引用( SoftRefrence )

    有些时候,我们使用 Bitmap 后没有保留对它的引用,因此就无法调用 Recycle 函数。这时候巧妙的运用软引用,可以使 Bitmap 在内存快不足时得到有效的释放。如下例:

    private class MyAdapter extends BaseAdapter {

    private ArrayList> mBitmapRefs = new ArrayList>();
    private ArrayList mValues;
    private Context mContext;
    private LayoutInflater mInflater;

    MyAdapter(Context context, ArrayList values) {
    mContext = context;
    mValues = values;
    mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    }
    public int getCount() {
    return mValues.size();
    }

    public Object getItem(int i) {
    return mValues.get(i);
    }

    public long getItemId(int i) {
    return i;
    }

    public View getView(int i, View view, ViewGroup viewGroup) {
    View newView = null;
    if(view != null) {
    newView = view;
    } else {
    newView =(View)mInflater.inflate(R.layout.image_view, false);
    }

    Bitmap bitmap = BitmapFactory.decodeFile(mValues.get(i).fileName);
    mBitmapRefs.add(new SoftReference(bitmap)); //此处加入 ArrayList
    ((ImageView)newView).setImageBitmap(bitmap);

    return newView;
    }
    }
    六、行踪诡异的 Cursor
    Cursor 是 Android 查询数据后得到的一个管理数据集合的类,正常情况下,如果查询得到的数据量较小时不会有内存问题,而且虚拟机能够保证 Cusor 最终会被释放掉。

    然而如果 Cursor 的数据量特表大,特别是如果里面有 Blob 信息时,应该保证 Cursor 占用的内存被及时的释放掉,而不是等待 GC 来处理。并且 Android 明显是倾向于编程者手动的将 Cursor close 掉,因为在源代码中我们发现,如果等到垃圾回收器来回收时,会给用户以错误提示。

    所以我们使用 Cursor 的方式一般如下:

    Cursor cursor = null;
    try {
    cursor = mContext.getContentResolver().query(uri,null, null,null,null);
    if(cursor != null) {
    cursor.moveToFirst();
    //do something
    }
    } catch (Exception e) {
    e.printStackTrace();
    } finally {
    if (cursor != null) {
    cursor.close();
    }
    }
    有一种情况下,我们不能直接将 Cursor 关闭掉,这就是在 CursorAdapter 中应用的情况,但是注意, CursorAdapter 在 Acivity 结束时并没有自动的将 Cursor 关闭掉,因此,你需要在 onDestroy 函数中,手动关闭。

    @Override
    protected void onDestroy() {
    if (mAdapter != null && mAdapter.getCurosr() != null) {
    mAdapter.getCursor().close();
    }
    super.onDestroy();
    }
    CursorAdapter 中的 changeCursor 函数,会将原来的 Cursor 释放掉,并替换为新的 Cursor ,所以你不用担心原来的 Cursor 没有被关闭。
    你可能会想到使用 Activity 的 managedQuery 来生成 Cursor ,这样 Cursor 就会与 Acitivity 的生命周期一致了,多么完美的解决方法!然而事实上 managedQuery 也有很大的局限性。
    managedQuery 生成的 Cursor 必须确保不会被替换,因为可能很多程序事实上查询条件都是不确定的,因此我们经常会用新查询的 Cursor 来替换掉原先的 Cursor 。因此这种方法适用范围也是很小。

    七、其它要说的。
    其实,要减小内存的使用,其实还有很多方法和要求。比如不要使用整张整张的图,尽量使用 9path 图片。 Adapter 要使用 convertView 等等,好多细节都可以节省内存。这些都需要我们去挖掘,谁叫 Android 的内存不给力来着。
    9 条回复    2016-09-21 20:51:52 +08:00
    iCodex
        1
    iCodex  
       2016-09-21 10:22:45 +08:00
    不排版
    字太多
    不缩进

    差评。
    micookie
        2
    micookie  
       2016-09-21 10:49:23 +08:00
    由于 51cto 不让我一次传完,说我的字数太多了,所以分开传了。


    这句话是什么鬼~
    asdwfwqd
        3
    asdwfwqd  
       2016-09-21 11:04:01 +08:00
    名字叫做 Override 那位躺枪了
    同理,还有 param 等等
    iFlicker
        4
    iFlicker  
       2016-09-21 11:13:24 +08:00
    v2ex 不让全文转载吧,除非作者
    (这不是阿里校招(移动端)大题第一题么
    lzjamao
        5
    lzjamao  
       2016-09-21 14:37:52 +08:00
    不错
    XhstormR
        6
    XhstormR  
       2016-09-21 14:38:59 +08:00
    如果某个对象 (连通子图)与这个根顶点不可达(注意,该图为有向图),

    NM ,图呢?
    leewangyang
        7
    leewangyang  
       2016-09-21 14:40:58 +08:00 via Android
    @asdwfwqd 点开 Override 的主页,签名很应景啊
    mason961125
        8
    mason961125  
       2016-09-21 14:43:10 +08:00
    现在讲 Dalvik 相关,是否有些过时?
    kitalphaj
        9
    kitalphaj  
       2016-09-21 20:51:52 +08:00
    线程那个确实如此
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   3748 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 21ms · UTC 04:16 · PVG 12:16 · LAX 20:16 · JFK 23:16
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.