Android性能优化《Android开发艺术探索》笔记


部分图片来自Android性能优化:关于 内存泄露 的知识都在这里了!

Android性能优化

普通优化

1.布局优化

  • 尽量避免嵌套,可以使用RelativeLayout来取代LinearLayout的多层嵌套,但在使用Relativelayout和LinearLayout都可以的时候,使用LinearLayout。
  • 使用include来复用xml文件
<include layout = "@layout/titlebar">
  • 使用merge来减少布局的层级
  • 使用ViewStub可以让布局在使用时再被加载,提高初始化时的性能。其中id是当前ViewStub的id,而layout则是ViewStub所包含的layout文件,ll_include则是当前layout/include_main_test布局所对应的根元素的id。
    <ViewStub
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout="@layout/include_main_test"
        android:inflatedId="@id/ll_include"
        android:id="@+id/stud_import"
        />

2.绘制优化

  1. 在onDraw中不要创建局部变量,因为onDraw会多次调用会占用内存,同时导致多次GC。
  2. 在onDraw中不要进行耗时操作,也不能进行成千上万次的循环,这样会导致View的绘制不流畅。

3.内存泄漏优化

内存泄漏:程序申请的内存,在使用后无法释放,是导致OOM的主要原因。

内存泄漏的实质无意识地持有对象引用,使得 持有引用者的生命周期 > 被引用者的生命周期

内存溢出:程序在申请内存时,没有足够的内存够其使用

Android内存的管理

在这里插入图片描述
进程的内存管理:(内存分配与内存回收)
通过AMS对进程进行内存分配由Framework决定回收的进程类型,当内存紧缺时,会按照优先级的顺序对进程进行回收Linux 内核真正回收具体进程。

对象的内存管理与Java虚拟机类似。

存储包括

方法区中存放已被加载的类信息,以及常量静态变量
栈帧存放方法执行时的局部变量,包括数据类型对象引用
而对象的实例存于堆中。实例包括对象以及对象所在类的成员变量等等

释放是对堆中进行GC操作

内存泄漏的实例

1.静态变量

被 Static 关键字修饰的成员变量的生命周期 = 应用程序的生命周期

当前情况下context(成员变量)的生命周期为应用程序的生命周期大于Activity(引用实例)的生命周期,因此Activity在回收时,由于Context对Activity的持有,因此Activity无法回收。

public class MainActivity extends AppCompatActivity {

    private static final String TAG = "MainActivity";
    //内存泄漏
    private static Context mContext;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mContext = this;
    }
}

解决方法
尽量避免静态成员变量引用资源耗费过多的实例(如 Context)如果需要,可以使用Applaction的Context
context.getApplicationContext()

单例模式的使用

我们需要通过context来创建一个单例的对象(静态对象,因为是单例模式),但我们不能直接使用context,而应该通过context.getApplicationContext()来获取。(不能让单例的对象一直引用Activity,这样当Activity准备销毁时,也会因为被单例对象引用而无法销毁,导致内存泄漏)

public class SingleInstanceClass {    
    private static SingleInstanceClass instance;    
    private Context mContext;    
    private SingleInstanceClass(Context context) {        
        this.mContext = context.getApplicationContext(); // 传递的是Application 的context
    }    

    public static SingleInstanceClass getInstance(Context context) {        
        if (instance == null) {
            instance = new SingleInstanceClass(context);
        }        
        return instance;
    }
}

2.集合类

集合类添加元素后,会对 对象具有引用,即使已经将对象置为null,也无法回收。

// 通过 循环申请Object 对象 & 将申请的对象逐个放入到集合List
List<Object> objectList = new ArrayList<>();        
       for (int i = 0; i < 10; i++) {
            Object o = new Object();
            objectList.add(o);
            o = null;
        }
// 虽释放了集合元素引用的本身:o=null)
// 但集合List 仍然引用该对象,故垃圾回收器GC 依然不可回收该对象

解决方法:在不使用时,必须使用clear方法将所有对象删除,并将集合类对象置为null。

 // 释放objectList
        objectList.clear();
        objectList=null;

3.非静态内部类/匿名类

3.1创建非静态内部类的静态对象

非静态内部类默认持有外部类的引用,而静态内部类则不会。

而造成内存泄漏的原因是创建了一个非静态内部类的静态对象。则该对象的生命周期=应用的生命周期,因此会在外部类对象销毁的时候,仍然保留着对外部类的引用,则外部类的实例无法被GC。

public class TestActivity extends AppCompatActivity {  
   
    // 非静态内部类的实例的引用
    // 注:设置为静态  
    public static InnerClass innerClass = null; 
   
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {        
        super.onCreate(savedInstanceState);   

        // 保证非静态内部类的实例只有1个
        if (innerClass == null)
            innerClass = new InnerClass();
    }

    // 非静态内部类的定义    
    private class InnerClass {        
        //...
    }
}

解决方法:将内部类改成静态内部类,这样就不会有外部类的引用

3.2 匿名类持有外部类的引用

以Thread为例,我们需要创建一个匿名内部类Thread并实现Runnable接口

工作线程正在处理任务且外部类需销毁时, 由于工作线程的实例持有外部类引用,将使得外部类无法被垃圾回收器(GC)回收,从而造成内存泄露。

public class MainActivity extends AppCompatActivity {
 @Override
    protected void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);
        ...
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(1000);
                }catch (InterruptedException e)
                {
                    e.printStackTrace();
                }
            }
        }).start();

        group.setVisibility(View.GONE);
    }
}

解决方法是可以使用静态内部类(Thread)

 protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        new MyThread().start();
    }
 private static class MyThread extends Thread{
        @Override
        public void run() {
            try {
                Thread.sleep(1000);
            }catch (InterruptedException e)
            {
                e.printStackTrace();
            }
        }
    }

或者在外部类的回收时,强制关闭线程

 @Override
    protected void onDestroy() {
        super.onDestroy();
        Thread.stop();
        // 外部类Activity生命周期结束时,强制结束线程
    }
3.3 Handler

在匿名内部类时,会默认持有外部的引用,也就是说Handler会持有外部类Activity的引用。

  • 当Handler消息队列 还有未处理的消息 / 正在处理消息时,存在引用关系: “未被处理 / 正处理的消息 -> Handler实例 -> 外部类”(Looper的生命周期与Application相同)

  • 若出现 Handler的生命周期 > 外部类的生命周期 时(即 Handler消息队列 还有未处理的消息 / 正在处理消息 而 外部类需销毁时),将使得外部类无法被垃圾回收器(GC)回收,从而造成 内存泄露。

解决方法:静态内部类+弱引用
静态内部类不持有外部类的引用,从而使得 “未被处理 / 正处理的消息 -> Handler实例 -> 外部类” 的引用关系 的引用关系 不复存在。而弱引用不论内存是否够用,在GC时都会被回收

实例如下:

 // 分析1:自定义Handler子类
    // 设置为:静态内部类
    private static class FHandler extends Handler{

        // 定义 弱引用实例
        private WeakReference<Activity> reference;

        // 在构造方法中传入需持有的Activity实例
        public FHandler(Activity activity) {
            // 使用WeakReference弱引用持有Activity实例
            reference = new WeakReference<Activity>(activity); }

        @Override
        public void handleMessage(Message msg) {
           ...
            }
        }
    }

3.4 资源使用未关闭

未及时注销资源导致内存泄漏,如BraodcastReceiver、File、Cursor、Stream、Bitmap等。

解决办法:在Activity销毁的时候要及时关闭或者注销

BraodcastReceiver:调用unregisterReceiver()注销;
Cursor,Stream、File:调用close()关闭;
Bitmap:调用recycle()释放内存(2.3版本后无需手动)
属性动画:需要在onDestory中调用Animator.cancel方法来停止无限循环动画。

3.5 ListView的Adapter导致的内存泄漏

不使用缓存,而一直在getView中重新实例化Item。

解决方法:使用ContertView和ViewHolder,防止多次实例化Item与Item中的子控件。

4ListView优化

主要体现在getView的优化

  1. convertView参数用于将之前加载好的布局进行缓存
    • 当convertView为null的时候,首先加载子item布局,之后创建一个ViewHolder对象,将控件通过findById加载进来,并将控件实例存入ViewHolder对象中。然后调用convertView对象的setTag()方法,将ViewHolder对象存在View中
    • 当convertView不为null时,就调用convertView对象的getTag()方法,将ViewHolder对象重新取出。这样所有控件的实例都缓存在了这个ViewHolder中,就不用每次都用findViewById()方法来获取控件实例。
  2. 在Activity中为adapter设置listener,就不需要在每次加载getView时重新创建。
  3. 为控件设置监听器,同时要向控件传递位置(setTag(position)),以便触发了点击事件后,可以通过Integer) v.getTag()知道当前点击的是哪个子项。
   @Override
    public View getView(int position, View convertView, ViewGroup parent){
        ViewHolder viewHolder;
        if (convertView==null) {
            //convertView=View.inflate(mContext, R.layout.series_of_courses_item,null);
            convertView= LayoutInflater.from(mContext).inflate(R.layout.series_of_courses_item,parent,false);
            ViewHolder holder=new ViewHolder();
            holder.course_title=convertView.findViewById(R.id.soc_course_title);
            holder.course_time=convertView.findViewById(R.id.soc_course_time);
            holder.enter_class=convertView.findViewById(R.id.enter_class);
            holder.btn_son_course_delete = convertView.findViewById(R.id.btn_son_course_delete);
            convertView.setTag(holder);
        }
        viewHolder=(ViewHolder)convertView.getTag();
        final SeriesCourseModel seriesCourses=data.get(position);
        //Log.d("SeriesCoursesAdapter", "getView: " + courses.getCourseName());
        viewHolder.course_title.setText(seriesCourses.getName());
        //int minutes= (int) LangUtils.parseLong(seriesCourses.getCourseTime(),0)/60;
        viewHolder.course_time.setText(seriesCourses.getCourseTime());
        //注册Item控件点击事件,首先在Activity中要设置监听器
        if (onClickListener != null){
            viewHolder.enter_class.setOnClickListener(onClickListener);
            viewHolder.enter_class.setTag(position);
            viewHolder.btn_son_course_delete.setOnClickListener(onClickListener);
            viewHolder.btn_son_course_delete.setTag(position);
        }
        //type=1就显示那个button
        if (type==1) {
            viewHolder.enter_class.setVisibility(View.VISIBLE);
        }
        else viewHolder.enter_class.setVisibility(View.INVISIBLE);
        return convertView;
    }
    class ViewHolder{
        TextView course_title;
        TextView course_time;
        Button enter_class;
        ImageButton btn_son_course_delete;
    }
     //向XListView添加数据项
    public void addItems(SeriesCourseModel seriesCourseModel){
        this.data.add(seriesCourseModel);
        notifyDataSetChanged();
    }
    //向XListView删除数据项 注意添加了headView
    public void deleteItems(int position){
        this.data.remove(position);
        notifyDataSetChanged();
    }

5.BitMap优化

Bitmap的高效加载

  1. 将BitmapFactory.Options.inJustDecodeBounds参数设为true并加载图片。
  2. 从BitmapFactory.Options中取出图片的原始宽高信息decodeResource(res,resId,options),对应outWidth和outHeight参数。
  3. 根据采样率的规则并结合目标View的所需大小计算出采样率inSampleSize
  4. 将BitmapFactory.Options.inJustDecodeBounds参数设为false,然后重新加载图片decodeResource(res,resId,options)
  /**
     * 对一个Resources的资源文件进行指定长宽来加载进内存, 并把这个bitmap对象返回
     *
     * @param res   资源文件对象
     * @param resId 要操作的图片id
     * @param reqWidth 最终想要得到bitmap的宽度
     * @param reqHeight 最终想要得到bitmap的高度
     * @return 返回采样之后的bitmap对象
     */
public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId, int reqWidth, int reqHeight){
        BitmapFactory.Options options = new BitmapFactory.Options();
        //1.设置inJustDecodeBounds=true获取图片尺寸
        options.inJustDecodeBounds = true;
        BitmapFactory.decodeResource(res,resId,options);
        //3.计算缩放比
        options.inSampleSize = calculateInSampleSize(options,reqHeight,reqWidth);
        //4.再设为false,重新从资源文件中加载图片
        options.inJustDecodeBounds =false;
        return BitmapFactory.decodeResource(res,resId,options);
    }

   /**
     *  一个计算工具类的方法, 传入图片的属性对象和想要实现的目标宽高. 通过计算得到采样值
     * @param options 要操作的原始图片属性
     * @param reqWidth 最终想要得到bitmap的宽度
     * @param reqHeight 最终想要得到bitmap的高度
     * @return 返回采样率
     */
    private static int calculateInSampleSize(BitmapFactory.Options options, int reqHeight, int reqWidth) {
        //2.height、width为图片的原始宽高
        int height = options.outHeight;
        int width = options.outWidth;
        int inSampleSize = 1;
        if(height>reqHeight||width>reqWidth){
            int halfHeight = height/2;
            int halfWidth = width/2;
            //计算缩放比,是2的指数
            while((halfHeight/inSampleSize)>=reqHeight&&(halfWidth/inSampleSize)>=reqWidth){
                inSampleSize*=2;
            }
        }    
        return inSampleSize;
    }

Bitmap的缓存策略

当缓存满时, 会优先淘汰那些近期最少使用的缓存对象。

LruCache类是一个线程安全的泛型类:内部采用一个LinkedHashMap以强引用的方式存储外界的缓存对象,并提供get和put方法来完成缓存的获取和添加操作,当缓存满时会移除较早使用的缓存对象,再添加新的缓存对象。

常用属性accessOrder:决定LinkedHashMap的链表顺序。值为true:以访问顺序维护链表。值为false:以插入的顺序维护链表

而LruCache利用是accessOrder=true 时的LinkedHashMap实现LRU算法,使得最近访问的数据会在链表尾部,在容量溢出时,将链表头部的数据移除
在这里插入图片描述
使用方法
计算当前可用的内存大小;
分配LruCache缓存容量;
创建LruCache对象并传入最大缓存大小的参数、重写sizeOf()用于计算每个缓存对象的大小;
通过put()、get()和remove()实现数据的添加、获取和删除

 lruCache = new LruCache<String, Bitmap>(maxSize)
        {
            @Override
            protected int sizeOf(String key, Bitmap value) {
                // TODO Auto-generated method stub
                return value.getWidth() * value.getHeight() / 1024;
            }
        };

6.线程优化

使用线程池,避免创建大量的Thread。Android线程与线程池《开发艺术》探索

7.响应速度优化

避免在主线程中做耗时操作。Activity规定如果在5s之内无法响应点击事件或键盘输入事件就会ANR,如果10s内BroadCastReciver还没有完成执行逻辑就会ANR。当发生ANR后可以通过traces文件定位。

8.其他建议

  1. 避免创建过多对象
  2. 不要过多使用枚举,枚举的占用的内存空间比整形大
  3. 常量要使用static final来修饰
  4. 使用一些Android自带的数据结构,比如Pair,他们具有更好的性能,Pair可以理解为一个带有xy座标的Point类。List<Pair<Integer,Integer>>list = new ArrayList<>();
  5. 适当使用软引用与弱引用(弱引用可以防止内存泄漏)。
  6. 采用内存缓存与磁盘缓存
  7. 尽量使用静态内部类,防止非静态内部类具有外部类的引用而导致内存泄漏
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章