Neil's blog

Let's start from here


  • 首页

  • 归档

  • 标签

  • 关于

Android性能优化(一)布局优化

发表于 2018-04-13 |

布局优化主要的思想就一点:减少布局的层级

常用的优化方式有三种:

一、<include>标签

<include>标签可以将制定的布局加载到当前布局中。可以多处引用,类似于工具类的公共方法。

举个例子:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <include layout="@layout/include_comm_new_topbar_header"/>

    <android.support.v7.widget.RecyclerView
        android:id="@+id/courier_recycle_change_record"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
</LinearLayout>

引用的部分,

<?xml version="1.0" encoding="utf-8"?>

<ComTopBarNew xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/toolBar"
    android:layout_width="match_parent"
    android:layout_height="@dimen/com_toolbar_height"
    android:background="@color/comm_color18"
    />

需要注意的是,include标签仅支持layout开头的属性;当在include的布局中指定了id,在当前的布局文件中也指定了id,那么是以include中指定的id为准的。

二、<merge>标签

<merge>标签,一般是和标签配合使用的,举个例子:

<?xml version="1.0" encoding="utf-8"?>
<merge>

    <View style="@style/Comm.Divider.Horizontal" />
</merge>

使用场景也很好理解,如果当前布局是LinearLayout,被包含的布局也是LinearLayout,那么就可以使用标签,这样做就减少了布局的层级了。

三、 <ViewStub>标签

<ViewStub>标签,ViewStub继承自View,标签最大的特点就是当你需要的时候才会加载,但并不会影响UI初始化的性能。各种不常用的布局文件如进度条、显示错误信息等可以使用标签以减少内存使用量,加快渲染速度。标签是一个不可见的,大小为0的View。所以不会参与任何布局和绘制。

在需要使用的时候再去加载显示,这样就提高了程序初始的性能了。

举个例子:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"  
    xmlns:tools="http://schemas.android.com/tools"  
    android:layout_width="match_parent"  
    android:layout_height="match_parent"  
    android:orientation="vertical"  
    tools:context=".MainActivity">  

    <Button  
        android:id="@+id/btn_show"  
        android:layout_width="wrap_content"  
        android:layout_height="wrap_content"  
        android:text="Show"/>  
    <Button  
        android:id="@+id/btn_gone"  
        android:layout_width="wrap_content"  
        android:layout_height="wrap_content"  
        android:text="GONE"/>  

    <LinearLayout  
        android:layout_width="wrap_content"  
        android:layout_height="wrap_content">  
    <ViewStub  
        android:id="@+id/viewstub"  
        android:layout_width="wrap_content"  
        android:layout_height="wrap_content"  
        android:layout="@layout/view"/>  
    </LinearLayout>  

</LinearLayout> 

在需要展示的时候,在设置显示出来,即按需加载。ViewStub是一个轻量级的View,它一个看不见的,不占布局位置,占用资源非常小的控件。可以为ViewStub指定一个布局,在Inflate布局的时候,只有ViewStub会被初始化,然后当ViewStub被设置为可见的时候,或是调用了ViewStub.inflate()的时候,ViewStub所向的布局就会被Inflate和实例化,然后ViewStub的布局属性都会传给它所指向的布局。这样,就可以使用ViewStub来方便的在运行时,要还是不要显示某个布局。

那么如何加载?

两种方式:

  • stub.setVisibility(View.VISIBLE);

  • stub.inflate();

Bitmap OOM解决方案

发表于 2018-04-11 |

Bitmap OOM常用解决方案


  1. 在Android 2.3.3以及之前,建议使用Bitmap.recycle()方法,及时释放资源。

  2. 在Android 3.0开始,可设置BitmapFactory.options.inBitmap值,(从缓存中获取)达到重用Bitmap的目的。如果设置,则inPreferredConfig属性值会被重用的Bitmap该属性值覆盖。

  3. 通过设置Options.inPreferredConfig值来降低内存消耗:
    默认为ARGB_8888: 每个像素4字节. 共32位。
    Alpha_8: 只保存透明度,共8位,1字节。
    ARGB_4444: 共16位,2字节。
    RGB_565:共16位,2字节。
    如果不需要透明度,可把默认值ARGB_8888改为RGB_565,节约一半内存。

  4. 通过设置Options.inSampleSize 对大图片进行压缩,可先设置Options.inJustDecodeBounds,获取Bitmap的外围数据,宽和高等。然后计算压缩比例,进行压缩。

  5. 设置Options.inPurgeable和inInputShareable:让系统能及时回收内存。

    • inPurgeable:

      • 设置为True,则使用BitmapFactory创建的Bitmap用于存储Pixel的内存空间,在系统内存不足时可以被回收,当应用需要再次访问该Bitmap的Pixel时,系统会再次调用BitmapFactory 的decode方法重新生成Bitmap的Pixel数组。
      • 设置为False时,表示不能被回收。
    • inInputShareable:设置是否深拷贝,与inPurgeable结合使用,inPurgeable为

      • false,该参数无意义;
      • True,share a reference to the input data(inputStream, array,etc) 。 False :a deep copy。
  6. 使用decodeStream代替其他decodeResource,setImageResource,setImageBitmap等方法来加载图片。

区别:
decodeStream直接读取图片字节码,调用nativeDecodeAsset/nativeDecodeStream来完成decode。无需使用Java空间的一些额外处理过程,节省dalvik内存。但是由于直接读取字节码,没有处理过程,因此不会根据机器的各种分辨率来自动适应,需要在hdpi,mdpi和ldpi中分别配置相应的图片资源,否则在不同分辨率机器上都是同样的大小(像素点数量),显示的实际大小不对。

decodeResource会在读取完图片数据后,根据机器的分辨率,进行图片的适配处理,导致增大了很多dalvik内存消耗。

decodeStream调用过程:
      decodeStream(InputStream,Rect,Options) -> nativeDecodeAsset/nativeDecodeStream
decodeResource调用过程:即finishDecode之后,调用额外的Java层的createBitmap方法,消耗更多dalvik内存。
      decodeResource(Resource,resId,Options)  -> decodeResourceStream (设置Options的inDensity和inTargetDensity参数)  -> decodeStream() (在完成Decode后,进行finishDecode操作)
      finishDecode() -> Bitmap.createScaleBitmap()(根据inDensity和inTargetDensity计算scale) -> Bitmap.createBitmap()

以上方法的组合使用,合理避免OOM错误。

IPC基础概念介绍

发表于 2018-03-22 |

IPC基础概念介绍

一、简介

  • IPC是 Inter-Process Communication的缩写,意为进程间通信或跨进程通信,是指两个进程之间进行数据交换的过程。

  • 线程是CPU调度的最小单元,同时线程是一种有限的系统资源。进程一般指一个执行单元,在PC和移动设备上指一个程序或者一个应用。一个进程可以包含多个线程,因此进程和线程是包含与被包含的关系。最简单的情况下,一个进程中只可以有一个线程,即主线程,在Android中也叫UI线程。

二、Android中的多进程模式

Android中开启多进程的方法:

<activity android:name=".BinderOneActivity"
           android:process="com.neil.binder.one"/>
<activity android:name=".BinderTwoActivity"
           android:process="com.neil.binder.two"/>
<activity android:name=".BinderThreeActivity"
           android:process="com.neil.binder.three"/>

在AndroidManifest.xml指定android:process属性,若没有指定process属性,默认的进程中,默认进程的进程名是包名。

日志输出:

u0_a60    4080  1207  1255464 42888 ffffffff b74c94f5 S com.example.liushihan.glidedemo
u0_a60    4113  1207  1256320 42124 ffffffff b74c94f5 S com.neil.binder.one
u0_a60    4144  1207  1254256 42088 ffffffff b74c94f5 S com.neil.binder.two
u0_a60    4170  1207  1254256 41744 ffffffff b74c94f5 S com.neil.binder.three

Android系统为每个应用分配一个唯一的UID,具有相同UID的应用才能共享数据。两个应用通过ShareUID跑在同一个进程中是有要求的,需要这两个应用有相同的ShareUID并且签名相同才可以。在这种情况下,它们可以互相访问对方的私有数据,比如data目录,组件信息等。

三、序列化和反序列化

  1. 序列化

由于存在于内存中的对象都是暂时的,无法长期驻存,为了把对象的状态保持下来,这时需要把对象写入到磁盘或者其他介质中,这个过程就叫做序列化。

指把堆内存中的 Java 对象数据,通过某种方式把对象存储到磁盘文件中或者传递给其他网络节点(在网络上传输)。这个过程称为序列化。通俗来说就是将数据结构或对象转换成二进制串的过程

  1. 反序列化

反序列化恰恰是序列化的反向操作,也就是说,把已存在在磁盘或者其他介质中的对象,反序列化(读取)到内存中,以便后续操作,而这个过程就叫做反序列化。

  概括性来说序列化是指将对象实例的状态存储到存储媒体(磁盘或者其他介质)的过程。在此过程中,先将对象的公共字段和私有字段以及类的名称(包括类所在的程序集)转换为字节流,然后再把字节流写入数据流。在随后对对象进行反序列化时,将创建出与原对象完全相同的副本。
  
  把磁盘文件中的对象数据或者把网络节点上的对象数据,恢复成Java对象模型的过程。也就是将在序列化过程中所生成的二进制串转换成数据结构或者对象的过程   

四、Serializable VS Parcelable

1. Serializable

Serializable是java提供的一个序列化接口,它是一个空接口,专门为对象提供标准的序列化和反序列化操作,使用Serializable实现类的序列化比较简单,只要在类声明中实现Serializable接口即可。

举个例子:

import java.io.Serializable;

public class Person implements Serializable {

    **
     * 生成序列号标识
     */
    private static final long serialVersionUID = -2083503801443301445L;

    private String firstName;
    private String lastName;
    private int age;


    public Person(String firstName, String lastName, int age) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.age = age;
    }


    public String getFirstName() {
        return firstName;
    }

    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }

    public String getLastName() {
        return lastName;
    }

    public void setLastName(String lastName) {
        this.lastName = lastName;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}

实现的Serializable接口并声明了序列化标识serialVersionUID。

那么serialVersionUID有什么作用呢?

实际上我们不声明serialVersionUID也是可以的,因为在序列化过程中会自动生成一个serialVersionUID来标识序列化对象。

那么既然自动生成我们是不是可以不写?

由于serialVersionUID是用来辅助序列化和反序列化过程的,原则上序列化后的对象中serialVersionUID只有和当前类的serialVersionUID相同才能够正常被反序列化,也就是说序列化与反序列化的serialVersionUID必须相同才能够使序列化操作成功。具体过程是这样的:序列化操作的时候系统会把当前类的serialVersionUID写入到序列化文件中,当反序列化时系统会去检测文件中的serialVersionUID,判断它是否与当前类的serialVersionUID一致,如果一致就说明序列化类的版本与当前类版本是一样的,可以反序列化成功,否则失败。不指定的话在反序列化会报错。

例子的实现很简单,当然也是有弊端的,如果在此过程中使用反射,可能会创建大量其他对象,这就可能会导致很多垃圾回收。造成的结果是性能差和电池耗尽。

2. Parcelable

Parcelable是Android SDK中接口。鉴于Serializable在内存序列化上开销比较大,而内存资源属于android系统中的稀有资源(android系统分配给每个应用的内存开销都是有限的),为此android中提供了Parcelable接口来实现序列化操作,Parcelable的性能比Serializable好,在内存开销方面较小,所以在内存间数据传输时推荐使用Parcelable,如通过Intent在activity间传输数据,而Parcelable的缺点就使用起来比较麻烦。

举个例子:

import android.os.Parcel;
import android.os.Parcelable;

public class Person implements Parcelable {

    private String firstName;
    private String lastName;
    private int age;


    public Person(String firstName, String lastName, int age) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.age = age;
    }


    public String getFirstName() {
        return firstName;
    }

    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }

    public String getLastName() {
        return lastName;
    }

    public void setLastName(String lastName) {
        this.lastName = lastName;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }


    @Override
    public int describeContents() {
        return 0;
    }

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeString(this.firstName);
        dest.writeString(this.lastName);
        dest.writeInt(this.age);
    }

    protected Person(Parcel in) {
            this.firstName = in.readString();
            this.lastName = in.readString();
            this.age = in.readInt();
        }

    public static final Parcelable.Creator<Person> CREATOR = new Parcelable.Creator<Person>() {
        @Override
        public Person createFromParcel(Parcel source) {
            return new Person(source);
        }

        @Override
        public Person[] newArray(int size) {
            return new Person[size];
        }
    };
}

相对Serializable稍显复杂。

  • Parcelable.Creator

    • 必须实现的接口。
  • T createFromParcel (Parcel source)

    • 创建序列化的实例,实现从Parcel容器中读取传递数据值,封装成Parcelable对象返回逻辑层
  • T[] newArray (int size)

    • 创建一个类型为T,长度为size的数组,供外部类反序列化本类数组使用。
  • void writeToParcel (Parcel dest,

    int flags)
    
* 将当前对象写入序列化结构中

简单概括下:

序列化过程中,需要实现序列化和反序列化以及内容描述。

writeToParcel实现序列化,通过CREATOR内部对象来实现反序列化,其内部通过createFromParcel方法来创建序列化对象并通过newArray方法创建数组,最终利用Parcel的一系列read方法完成反序列化,最后由describeContents完成内容描述功能,该方法一般返回0,仅当对象中存在文件描述符时返回1。

一句话概括,就是通过writeToParcel将我们的对象映射成Parcel对象,再通过createFromParcel将Parcel对象映射成我们的对象。

3. Serializable 和 Parcelable 异同
  • 都能序列化,都可用于Intent传递数据

  • Serializable是Java中的序列化接口,使用简单但是内存开销大,序列化和反序列化需要大量的I/O操作。

  • Parcelable是Android SDK中的序列化接口,更适用Android平台,使用起来稍显复杂,但是效率很高。内存开销较小。

  • Parcelable主要用于内存序列化上,因此在序列化到存储设备或者网络传输方面还是尽量选择Serializable接口

HashMap的实现原理

发表于 2018-03-20 |

HashMap实现原理

HashMap简介

HashMap 是一个散列表,允许使用空值和空键。存储的内容是键值对(key-value)映射

HashMap 继承于AbstractMap,实现了Map、Cloneable、java.io.Serializable接口。
HashMap 的实现不是同步的,这意味着它不是线程安全的。它的key、value都可以为null。此外,HashMap中的映射不是有序的。

HashMap 的实例有两个参数影响其性能:“初始容量” 和 “加载因子”。容量 是哈希表中桶的数量,初始容量 只是哈希表在创建时的容量。加载因子 是哈希表在其容量自动增加之前可以达到多满的一种尺度。当哈希表中的条目数超出了加载因子与当前容量的乘积时,则要对该哈希表进行 rehash 操作(即重建内部数据结构),从而哈希表将具有大约两倍的桶数。
通常,默认加载因子是 0.75, 这是在时间和空间成本上寻求一种折衷。加载因子过高虽然减少了空间开销,但同时也增加了查询成本(在大多数 HashMap 类的操作中,包括 get 和 put 操作,都反映了这一点)。在设置初始容量时应该考虑到映射中所需的条目数及其加载因子,以便最大限度地减少 rehash 操作次数。如果初始容量大于最大条目数除以加载因子,则不会发生 rehash 操作。

HashMap的数据结构

在 Java 编程语言中,最基本的结构就是两种,一个是数组,另外一个是指针(引用),HashMap 就是通过这两个数据结构进行实现。HashMap实际上是一个“链表散列”的数据结构,即数组和链表的结合体。

HashMap 底层就是一个数组结构,数组中的每一项又是一个链表。当新建一个 HashMap 的时候,就会初始化一个数组。

继承关系:

java.lang.Object
   ↳     java.util.AbstractMap<K, V>
         ↳     java.util.HashMap<K, V>

public class HashMap<K,V>
    extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable { }

HashMap的构造函数

// 默认构造函数。
HashMap()

// 指定“容量大小”的构造函数
HashMap(int capacity)

// 指定“容量大小”和“加载因子”的构造函数
HashMap(int capacity, float loadFactor)

// 包含“子Map”的构造函数
HashMap(Map<? extends K, ? extends V> map)

HashMap的整体结构

HashMap数据结构

Android常见内存泄漏及解决办法

发表于 2018-03-13 |

Why?

当一个对象已经不需要再使用了,本该被回收时,而有另外一个正在使用的对象持有它的引用从而导致它不能被回收,这导致本该被回收的对象不能被回收而停留在堆内存中,这就产生了内存泄漏。

内存泄漏是造成应用程序OOM的主要原因之一!我们知道Android系统为每个应用程序分配的内存有限,而当一个应用中产生的内存泄漏比较多时,这就难免会导致应用所需要的内存超过这个系统分配的内存限额,这就造成了内存溢出而导致应用Crash。

How?

主要几点:

  • Context
  • 内部类(handler等)
  • Cursor
  • Adapter
  • Bitmap
1. 单例造成的内存泄漏

单例的静态特性使得单例的生命周期和应用的生命周期一样长,这就说明了如果一个对象已经不需要使用了,而单例对象还持有该对象的引用,那么这个对象将不能被正常回收,这就导致了内存泄漏。

例如:

public class AppManager {
    private static AppManager instance;
    private Context context;
    private AppManager(Context context) {
        this.context = context;
    }
    public static AppManager getInstance(Context context) {
        if (instance != null) {
            instance = new AppManager(context);
        }
        return instance;
    }
}
  • 传入的是Application的Context:这将没有任何问题,因为单例的生命周期和Application的一样长
  • 传入的是Activity的Context:当这个Context所对应的Activity退出时,由于该Context和Activity的生命周期一样长(Activity间接继承于Context),所以当前Activity退出时它的内存并不会被回收,因为单例对象持有该Activity的引用。

正确的单例:

 public class AppManager {
    private static AppManager instance;
    private Context context;
    private AppManager(Context context) {
        this.context = context.getApplicationContext();
    }
    public static AppManager getInstance(Context context) {
        if (instance != null) {
            instance = new AppManager(context);
        }
        return instance;
    }
}
2.非静态内部类创建静态实例造成的内存泄漏

在实际的项目开发中,有时候我们需要频繁的启动某个页面(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(mManager == null){
            mManager = new TestResource();
        }
        //...
    }
    private class TestResource {
        //...
    }
}

我们创建了一个静态的资源对象mResouce,每次Activity启动都会使用该资源的数据,避免了重复创建。但是这样会造成内存泄漏

  • 非静态内部类默认会持有外部类的引用
  • 又使用了该非静态内部类创建了一个静态的实例
  • 该静态实例的生命周期和应用的一样长,这就导致了该静态实例一直会持有该Activity的引用,导致Activity的内存资源不能正常回收。

解决办法:

  • 将内部类testResource改成静态内部类
  • 将testResource抽取出来,封装成一个单例
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(mManager == null){
            mManager = new TestResource();
        }
        //...
    }
    private static class TestResource {
        //...
    }
}
3. handler导致内存泄漏

举例如下

public class MainActivity extends AppCompatActivity {
    private Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            //...
        }
    };
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        loadData();
    }
    private void loadData(){
        //...request
        Message message = Message.obtain();
        mHandler.sendMessage(message);
    }
}

由于mHandler是Handler的非静态匿名内部类的实例,所以它持有外部类Activity的引用,我们知道消息队列是在一个Looper线程中不断轮询处理消息,那么当这个Activity退出时消息队列中还有未处理的消息或者正在处理消息,而消息队列中的Message持有mHandler实例的引用,mHandler又持有Activity的引用,所以导致该Activity的内存资源无法及时回收,引发内存泄漏

解决方案:

  • 在Activity onDestroy的时候,也应该对消息队列中的消息移除。
 @Override
public void onDestroy() {
   //三种均可
    mHandler.removeMessages(message);
    mHandler.removeCallbacks(Runnable);
    mHandler.removeCallbacksAndMessages(null);
}
4.资源未关闭导致内存泄漏

BraodcastReceiver,ContentObserver,File,Cursor,Stream,Bitmap等资源的使用,应该在Activity销毁时及时关闭或者注销,否则这些资源将不会被回收,造成内存泄漏。

1…567…14
Neil Liu

Neil Liu

优秀不够,你是否无可替代

68 日志
25 标签
GitHub
© 2019 Neil Liu
由 Hexo 强力驱动
主题 - NexT.Muse