美文网首页
Android内存和性能

Android内存和性能

作者: FreedApe | 来源:发表于2019-01-07 09:42 被阅读0次

关于内存的几个理论知识

1.Generational Heap Memory模型

Android系统里的Generational Heap Memory的模型是一个三级Generation的内存模型,它包括Young Generation,Old Generation,Permanent Generation三个部分。最新分配的对象会存放在Young Generation区域,当这个对象在这个区域停留的时间超过某个值的时候,会被移动到Old Generation,最后到Permanent Generation区域。三个区域对象创建速度和GC执行的速度是不一样的Young Generation最快,Old Generation次之,Permanent Generation最慢。整个结构如下图所示:

Generational Heap Memory.png

三个区域的存储空间都有一个固定的大小,当这些对象总的大小快达到阀值时,会触发GC的操作,以腾出空间来存放其他新的对象。

2. GC 的工作机制

当虚拟机执行GC操作的时候,所有线程的任何操作都会需要暂停,等待GC操作完成之后,其他操作才能够继续运行,如果GC操作过多,就会导致系统性能严重下降。

Android 开发过程中的常见内存异常:

在Android 开发过程中主要的内存异常有内存抖动内存泄露内存溢出(OOM)等几种。

1. 内存抖动
导致GC频繁发生的原因是由于短时间内大量对象被创建,瞬间产生的大量对象会占用大量的Young Generation的内存区域很有可能导致该区域的内存达到阈值,从而触发GC。典型地,在 View 控件的 onDraw 方法里分配大量内存,又释放大量内存,这种做法极易引起内存抖动,从而导致性能下降。因为 onDraw 里的大量内存分配和释放会给系统堆空间造成压力,触发 GC 工作去释放更多可用内存,而 GC 工作起来时,又会吃掉宝贵的帧时间 (帧时间是 16ms) ,最终导致性能问题。

Memory Churn.png

发生内存抖动的常见原因:

  • 在循环内部创建新的对象
  • 在自定义View的onDraw方法中创建新的对象
    我们知道onDraw方法是执行在UI线程的,在UI线程中创建对象本身是允许的,并且创建对象本身不会花费太多的系统资源,但是要考虑到一点就是设备存在刷新频率,每次刷新就有可能调用onDraw方法,如果太多频繁得调用onDraw方法极有可能照成内存抖动问题。

解决方法:

  • 别在循环里分配内存 (创建新对象)
  • 尽量别在 View 的 onDraw 函数里分配内存
  • 实在无法避免在这些场景里分配内存时,考虑使用对象池 (Object Pool)
    例如:
public class User {
  public String id;
  public String name;
  private static final SynchronizedPool<User> sPool = new SynchronizedPool<User>(10);
  public static User obtain() {
      User instance = sPool.acquire();
      return (instance != null) ? instance : new User();
  }
  public void recycle() {
      sPool.release(this);
  }
}

我们在申请实例化时调用

//从对象池中获取,第一次对象池没有,会直接new一个,如果有会复用
User user = User.obtain();
对象使用完释放时调用

//使用完毕务必要将对象归还到对象池
user.recycle();



2. 内存泄漏
内存泄漏表示的是不再用到的对象原本需要被回收的但是由于被错误引用(还有对象依旧持有这个对象)而无法被正常回收。内存泄漏会导致可用内存越来越少,从而导致频繁触发 GC 回收内存,进而导致性能下降。

内存泄漏的特征:

从 Memory Monitor 来看,内存占用越来越大


memory_tracker

常见的内存泄露:

  • 1.在异步任务回调中引用View
void onClick(final View view){
    webservice.fetchPosts(new Callback(){
        public void onResult(Response response){
            //The View may not be valid and the Activity may be gone
            view.yoPerfSoSlow();
        }
    });
}

在异步回调中引用到了View。假如在异步任务执行完成之前,点击返回键后退,Activity需要Destory,但是由于异步任务的引用导致Activity无法被杀死。更糟糕的是,Activity只有在异步任务被释放后才能被杀死。如果反复进入后退进入后退,那么内存就会越来越小,导致OOM。
我们可以利用Android的LocalBroadcastManager发送本地广播让Activity更新View.

  • 2.在静态对象中引用View

静态对象的生命周期和App一样长。直到应用退出才会被回收。被静态对象引用的View在整个应用生命周期中都不会被销毁,其引用的Activity也不会被销毁,直到应用退出。

  • 3.避免把View添加到没有清除机制的容器里
    假如把view添加到WeakHashMap,如果没有执行清除操作,很可能会导致泄漏。

  • 3.避免使用static对象:
    因为static的生命周期过长,使用不当很可能导致内存泄露。

  • 4.Activity的泄漏

  • 匿名内部类/非静态内部类导致内存泄露

 public class MainActivity extends AppCompatActivity {
    
    private static TestNoneStaticClass mTestClass = null;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        if(mTestClass == null){
            mTestClass = new TestNoneStaticClass();
        }
        //...
    }
    
    class TestNoneStaticClass {
        //...
    }
}

mTestClass 是一个静态变量它的生命周期是整个应用周期,但是它是一个非静态内部类,所以会持有外部类的引用,换句话说MainActivity被一个生命周期为整个应用周期的对象所持有,所以它退出后就不能被GC回收。从而导致了内存泄露。


  • Handler使用不当导致内存泄露

将Handler声明为非静态的内部类,这样的话它就持有外部Activity的引用,如果使用这种Handler向Looper发送消息后如果在Activity退出后仍然没有被处理,那么Message将会保留在Looper内,由于上面所说Message持有Handler的引用,Handler由于是非静态内部类所以也会持有Activity的引用,那么这样就导致了Activity退出后,内存不能被回收,也就是内存泄露了。

public class SampleActivity extends Activity {

    private final Handler mLeakyHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            // ...
        }
    }

    @Override   
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // Post a message and delay its execution for 10 minutes.
        mLeakyHandler.postDelayed(new Runnable() {
        @Override
        public void run() { /* ... */ }
        
        }, 1000 * 60 * 10);

        // Go back to the previous Activity.
        finish();
    }
}

Handler当中使用Activity的引用,解决方法

static class MyHandler extends Handler {
        WeakReference<Activity > mActivityReference;
        MyHandler(Activity activity) {
            mActivityReference= new WeakReference<Activity>(activity);
        }
    
        @Override
        public void handleMessage(Message msg) {
            final Activity activity = mActivityReference.get();
            if (activity != null) {
                mImageView.setImageBitmap(mBitmap);
            }
        }
    }

除了上面的例子外在Aactivity退出之前,需要注意执行remove Handler消息队列中的Message与Runnable对象从而达到彻底的退出。

除此之外还有:

  • Android 5.1 之后,带webview的界面,退出后,这个activity都不会被释放,activity的实例会被持有,由于我们项目中经常会用到浏览web页面的地方,可能引起内存积压,导致内存溢出的现象,所以这个问题还是比较严重的。
 @Override
    public void onDestroy() {
        if (mWebView != null) {
         ViewParent parent = mWebView.getParent();
            if (parent != null) {
                ((ViewGroup) parent).removeView(mWebView);
            }
        mWebView.stopLoading();
        mWebView.getSettings().setBuiltInZoomControls(false);
        mWebView.setCustomWebViewClient(null);
        mWebView.setCustomWebChromeClient(null);
        mWebView.clearCache(true);
        mWebView.clearSslPreferences();
        mWebView.clearFormData();
        mWebView.clearView();
        mWebView.destroyDrawingCache();
        mWebView.destroy();
        }
        super.onDestroy();
    }
  • Activity Context被传递到其他实例中,这可能导致自身被引用而发生泄漏。
    内部类引起的泄漏不仅仅会发生在Activity上,其他任何内部类出现的地方,都需要特别留意。我们可以考虑尽量使用static类型的内部类,同时使用WeakReference的机制来避免因为互相引用而出现的泄露。
  • 考虑使用Application Context而不是Activity Context
    在开发的时候除了必须使用Activity Context的情况,尽量使用Application Context而不是Activity Context,由于Application Context的生命周期较长所以使用它可以有效避免Activity 被持有而导致内存泄露的可能。
  • 对于临时Bitmap对象要及时回收
    临时创建的某个相对比较大的 bitmap对象,在经过变换得到新的bitmap对象之后,应该尽快回收原始的bitmap
    在退出Activity的时候不要忘记注销监听器
    在Android程序里面存在很多需要register与unregister的监听器,我们需要确保在合适的时候及时unregister那些监听器。并且要注意添加和注销必须对应起来。比如在onCreate上监听的,需要在onDestroy中注销监听。
  • 缓存对象没有及时清理导致的泄露问题
    为了提高对象的复用性我们会将某些对象放置到缓存容器而不是直接销毁,这样可以在下次使用的时候直接使用。但是这有可能导致一个问题就是如果存储在缓存里面的对象没有及时从容器中清除,并且缓存容器中存储的对象持有View或者Activity的引用极有可能导致Activity发生泄漏。
  • Cursor对象没有及时关闭导致的泄漏
    在程序中有的时候会发现我们查询完数据库后,返回结果的Cursor使用结束后没有及时关闭的情况,这时候就会导致Cursor的泄漏。这个是比较初级的泄漏,但也是最常见的泄漏。

** 3.内存溢出**

在Android系统中每个app所能够使用的堆大小是有限的,不同RAM大小的设备堆大小是不同的,一旦我们的app在超过了这个限制的情况下继续分配内存的话会引起OOM.如果你想要查询当前设备的堆大小显示可以调用getMemoryClass()来查询。它会返回一个用于表示当前应用堆大小限制的数据。如果你确定你的应用需要耗费较大的堆空间,可以通过在manifest的application标签下添加largeHeap=true的属性。但是这样做有个不好的地方是,它会使得每次GC的运行时间更长,在任务切换时,系统的性能会变得大打折扣,所以在遇到内存溢出的时候不应该只借助申请大堆栈的方式来解决,而应该从根本上节省内存的消耗。
因此防止内存溢出的问题也可以转换为如何减少内存的消耗。

  • 避免在Android里面使用Enum
    Enums的内存消耗通常是static constants的2倍。应该尽量避免在Android上使用enums。
  • 谨慎使用第三方库:
    不要为了少量的功能而导入整library。如果没有一个合适的库满足你的需求的情况下, 我们应该考虑自己去实现或者对一个相近库进行移植,而不是直接导入一整个库方案。
  • 使用ProGuard来剔除不需要的代码
    Android为我们提供了Proguard的工具来帮助应用程序对代码进行瘦身,优化,混淆的处理。它能够通过移除不需要的代码,重命名类,域与方法等方对代码进行压缩,优化与混淆。使用ProGuard可以使得你的代码更加紧凑,这样能够使用更少mapped代码所需要的RAM。使用Proguard只需要在build.gradle文件中配置minifEnable为true即可。
  • 删除无效资源:
    上面介绍的是如何去除无效的代码,接下来要介绍的将是如何自动删除无效的资源,对于那些没有被引用到的资源,会在编译阶段被排除在APK安装包之外,要实现这个目的,只需要在build.gradle文件中配置shrinkResource为true即可。

以上。

相关文章

网友评论

      本文标题:Android内存和性能

      本文链接:https://www.haomeiwen.com/subject/aepyrqtx.html