内存处理机制

因为Android内存比较少,16M,有的是24M,虽然有着GC帮我们管理,但是我们还是应该对内存有简单的了解。
内存简介
看图

内存处理机制
文章图片

我们了解一下我们代码申请的数据都放在哪里。

  • 对象实例数据
    实际上是保存对象实例的属性,属性的类型和对象本身的类型标记等,但是不保存实例的方法。实例的方法是属于数据指令,是保存在Stack里面,也就是上面表格里面的类方法。
    对象实例在Heap中分配好以后,会在Stack中保存一个4字节的Heap内存地址,用来查找对象的实例。因为在Stack里面会用到Heap的实例,特别是调用实例的时候需要传入一个this指针。
  • 方法内部变量
    类方法的内部变量分为两种情况:简单类型保存在Stack中;对象类型在Stack中保存地址,在Heap 中保存值。
  • 非静态方法和静态方法
    非静态方法有一个隐含的传入参数,这个参数是dalvik虚拟机传进去的,这个隐含参数就是对象实例在Stack中的地址指针。因此非静态方法(在Stack中的指令代码)总是可以找到自己的专用数据(在Heap 中的对象属性值)。当然非静态方法也必须获得该隐含参数,因此非静态方法在调用前,必须先new一个对象实例,获得Stack中的地址指针,否则dalvik虚拟机将无法将隐含参数传给非静态方法。
    静态方法没有隐含参数,因此也不需要new对象,只要class文件被ClassLoader load进入JVM的Stack,该静态方法即可被调用。所以我们可以直接使用类名调用类的方法。当然此时静态方法是存取不到Heap 中的对象属性的。
  • 静态属性和动态属性
    静态属性是保存在Stack中的,而不同于动态属性保存在Heap 中。正因为都是在Stack中,而Stack中指令和数据都是定长的,因此很容易算出偏移量,所以类方法(静态和非静态)都可以访问到类的静态属性。也正因为静态属性被保存在Stack中,所以具有了全局属性。
内存区别
  • 栈内存
    栈的优势是,存取速度比堆要快,仅次于寄存器,栈数据可以共享。但缺点是,存在栈中的数据大小与生存期必须是确定的,缺乏灵活性。栈中主要存放一些基本类型的变量(int, short, long, byte, float, double, boolean, char)和对象句柄
  • 堆内存
    堆的优势是可以动态地分配内存大小,生存期也不必事先告诉编译器,因为它是在运行时动态分配内存的,Java的垃圾收集器会自动收走这些不再使用的数据。但缺点是,由于要在运行时动态分配内存,存取速度较慢。
内存管理 上面简单介绍了一下Android内存基本知识,下面我们简单介绍一下内存管理机制。
GC帮我们管理者内存,只有被废弃的对象才会被回收。那哪些才是被废弃的?简单的说,没有被引用的对象就是被废弃的对象。Android和Java内存管理相似,采用了有向图的原理。Java将引用关系考虑为图的有向边,有向边从引用者指向引用对象。线程对象可以作为有向图的起始顶点,该图就是从起始顶点开始的一棵树,根顶点可以到达的对象都是有效对象,GC不会回收这些对象。如果某个对象 (连通子图)与这个根顶点不可达(注意,该图为有向图),那么我们认为这个(这些)对象不再被引用,可以被GC回收。
管理内存是为了防止内存溢出,毕竟Android内存比较少。
发生内存溢出的原因:
  • 由于我们程序的失误,长期保持某些资源(如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也没有得到释放,发生了内存泄露。
    如何才能有效的避免这种引用的发生呢?
    1.应该尽量避免static成员变量引用资源耗费过多的实例,比如Context。
    2.Context尽量使用Application Context,因为Application的Context的生命周期比较长,引用它不会出现内存泄露的问题。
    ** 3.使用WeakReference代替强引用。比如可以使用WeakReference mContextRef; **
  • 注意线程
    线程也是造成内存泄露的一个重要的源头。线程产生内存泄露的主要原因在于线程生命周期的不可控。我们来考虑下面一段代码。
    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 WeakAsyncTask extends
    AsyncTask {
    protected WeakReference mTarget;
    public WeakAsyncTask(WeakTarget target) {
    mTarget = new WeakReference(target);
    }
    /** {@inheritDoc} /
    @Override
    protected final void onPreExecute() {
    final WeakTarget target = mTarget.get();
    if (target != null) {
    this.onPreExecute(target);
    }
    }
    /
    * {@inheritDoc} /
    @Override
    protected final Result doInBackground='#'" /span>
    final WeakTarget target = mTarget.get();
    if (target != null) {
    return this.doInBackground='#'" /span>
    } else {
    return null;
    }
    }
    /
    * {@inheritDoc} */
    @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
    }
    }
    事实上,线程的问题并不仅仅在于内存泄露,还会带来一些灾难性的问题。由于本文讨论的是内存问题,所以在此不做讨论。
  • Bitmap
    可以说出现OutOfMemory问题的绝大多数人,都是因为Bitmap的问题。因为Bitmap占用的内存实在是太多了,它是一个“超级大胖子”,特别是分辨率大的图片,如果要显示多张那问题就更显著了。
    如何解决Bitmap带给我们的内存问题?
    1.及时的销毁。
    虽然,系统能够确认Bitmap分配的内存最终会被销毁,但是由于它占用的内存过多,所以很可能会超过java堆的限制。因此,在用完Bitmap时,要及时的recycle掉。recycle并不能确定立即就会将Bitmap释放掉,但是会给虚拟机一个暗示:“该图片可以释放了”。
    2.设置一定的采样率。
    有时候,我们要显示的区域很小,没有必要将整个图片都加载出来,而只需要记载一个缩小过的图片,这时候可以设置一定的采样率,那么就可以大大减小占用的内存。如下面的代码:
    private ImageView preview;
    BitmapFactory.Options options = new BitmapFactory.Options();
    options.inSampleSize = 2; //图片宽高都为原来的二分之一,即图片为原来的四分之一
    Bitmap bitmap = BitmapFactory.decodeStream(cr.openInputStream(uri), null, options);
    preview.setImageBitmap(bitmap);
    3.巧妙的运用软引用(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;
    }
    }
说明 【内存处理机制】以上只是描述一些常用的场景,其实,要减小内存的使用,其实还有很多方法和要求。比如不要使用整张整张的图,尽量使用9path图片。Adapter要使用convertView等等,好多细节都可以节省内存。这些都需要我们去挖掘。

    推荐阅读