文章目錄
部分圖片來自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.繪製優化
- 在onDraw中不要創建局部變量,因爲onDraw會多次調用會佔用內存,同時導致多次GC。
- 在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的優化:
- convertView參數用於將之前加載好的佈局進行緩存。
- 當convertView爲null的時候,首先加載子item佈局,之後創建一個ViewHolder對象,將控件通過findById加載進來,並將控件實例存入ViewHolder對象中。然後調用convertView對象的setTag()方法,將ViewHolder對象存在View中。
- 當convertView不爲null時,就調用convertView對象的getTag()方法,將ViewHolder對象重新取出。這樣所有控件的實例都緩存在了這個ViewHolder中,就不用每次都用findViewById()方法來獲取控件實例。
- 在Activity中爲adapter設置listener,就不需要在每次加載getView時重新創建。
- 爲控件設置監聽器,同時要向控件傳遞位置(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的高效加載
- 將BitmapFactory.Options.inJustDecodeBounds參數設爲true並加載圖片。
- 從BitmapFactory.Options中取出圖片的原始寬高信息
decodeResource(res,resId,options)
,對應outWidth和outHeight參數。 - 根據採樣率的規則並結合目標View的所需大小計算出採樣率inSampleSize。
- 將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.其他建議
- 避免創建過多對象
- 不要過多使用枚舉,枚舉的佔用的內存空間比整形大
- 常量要使用static final來修飾
- 使用一些Android自帶的數據結構,比如Pair,他們具有更好的性能,Pair可以理解爲一個帶有xy座標的Point類。
List<Pair<Integer,Integer>>list = new ArrayList<>();
- 適當使用軟引用與弱引用(弱引用可以防止內存泄漏)。
- 採用內存緩存與磁盤緩存
- 儘量使用靜態內部類,防止非靜態內部類具有外部類的引用而導致內存泄漏。