前言
通過Android Studio的Memory Monitor工具,對各種數據類型,如:boolean,int,float,long,SparseArray,HashMap等在內存的佔用情況進行分析。對一些特定場景下的代碼編寫,如:String拼接,OnClickListener等所消耗的內存情況進行分析。通過分析,更好的瞭解了不同情況下堆內存是如何分配的,也確切驗證了以往諸多的代碼經驗,爲高效合理的利用內存奠定基礎。
Memory Monitor的基本使用
- 新建MainActivity,啓動APP
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
}
在 Android Monitor -> Monitors -> Memory 中,點擊”initiate GC”,先手動GC一次,把沒用的內存進行回收。
點擊”Dump Java Heap”,生成.hprof(hprof文件爲特定時間點,Java進程的內存快照)
以下是根據.hprof文件生成的內存分析表,本文主要關注Shallow Size和Retained Size,其他column含義可以參考官方-HPROF Viewer and Analyzer
Shallow Size和Retained Size
Shallow Size:該對象本身佔有內存大小
Retained Size:釋放該對象後,節省的內存大小
Dominating Size:管轄的內存大小,大部分情況和Retained一致
因爲可以通過GC Roots直接訪問,所以左圖的obj3不是藍色節點;而右圖卻是藍色,因爲它已經被包含在 Retained size 中。
Shallow Size | Retained Size(左) | Retained Size(右) |
---|---|---|
obj1 | obj1 | obj1+obj2+obj4 |
obj2 | obj2 | obj2+obj4 |
案例分析
如圖heap_nothing.png,在MainActivity在新建的時候,初始佔用內存1776(以下案例分析基於紅米note3機型)。
- case 1:空對象TestModel+未初始化。
public class TestModel {
}
public class MainActivity extends AppCompatActivity {
private TestModel mModel;
...onCreate()
}
只定義TestModel成員變量的情況下,內存佔用1780=初始內存+引用類型(4)。所以在項目發版前,要把一些沒有使用到的變量都清理一遍,積少成多,免得造成內存浪費。
- case 2:空對象TestModel+初始化。
public class MainActivity extends AppCompatActivity {
private TestModel mModel = new TestModel();
...onCreate()
}
內存佔用1788=case1+類信息(8),說明調用new時,即使是空對象,也需要8字節左右的堆空間用於描述該對象的類信息。基於Java是在new的時候纔去申請堆空間的特性,在開發中,可以考慮對象的延遲初始化,養成個好習慣,在使用到的時候纔去new。
- case3:TestModel以局部變量的方式進行定義。
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
TestModel mModel = new TestModel();
}
}
內存佔用未變化,還是初始值1776,說明局部變量生命週期只存在於方法內部,方法結束後,即可被gc回收。除非必須,能使用局部變量的情況,就避免定義成員變量。
- case4:boolean基礎類型。
public class MainActivity extends AppCompatActivity {
private boolean mBoolean;
...onCreate()
}
內存佔用1777=初始狀態+1,說明基礎類型boolean的引用類型佔用1字節。
- case5:Boolean封裝類型。
public class MainActivity extends AppCompatActivity {
private Boolean mBoolean;
...onCreate()
}
內存佔用1780=初始狀態+4,裝箱類型Boolean本質上也是一個對象,由case1可以推導出引用類型佔用4字節。
- case6:Boolean封裝類型+初始化。
public class MainActivity extends AppCompatActivity {
private Boolean mBoolean = new Boolean(true);
...onCreate()
}
內存佔用1789=case5+9,如圖,Boolean的源碼中有個boolean基礎類型的字段value,當調用”new Boolean(true)”的時候,根據case2可以推導,類描述信息8字節,根據case4可以推導,value基礎類型佔用1字節,所以總共增加9字節。
同理,可以推導出以下表格:
boolean/byte | short/char | int/float/String/引用類型/數組引用 | long/double/類信息 |
---|---|---|---|
內存佔用 | 1 | 2 | 4 |
- case7:TestModel內部類。
public class MainActivity extends AppCompatActivity {
private TestModel mModel = new TestModel();
...onCreate()
public class TestModel {
}
}
佔用內存1792=case1(1780)+類信息(8)+this引用(4)。
- case8:TestModel靜態內部類。
public class MainActivity extends AppCompatActivity {
private TestModel mModel = new TestModel();
...onCreate()
public static class TestModel {
}
}
佔用內存1788=case1(1780)+類信息(8),靜態內部類由於沒有外部類的匿名this引用,少佔用4字節。
- case9:HashMap和SparseArray的對比。
public class MainActivity extends AppCompatActivity {
private Map<Integer, Integer> mMap = new HashMap<>();
private SparseArray<Integer> mSparseArray = new SparseArray();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
for (int i = 0; i < 1000; i++) {
mMap.put(i, i);
mSparseArray.put(i, i);
}
}
}
各添加1000條數據,HashMap佔用53168,SparseArray佔用18653,說明使用SparseArray替代HashMap更節省內存。
- case10:OnClickListener三種寫法的對比。從節省內存的角度考慮,通過方式3接口回調設置OnClickListener爲最優。
寫法1:匿名類
public class MainActivity extends AppCompatActivity {
private Button mButton;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mButton = (Button) findViewById(R.id.button);
mButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(MainActivity.this, "hello", Toast.LENGTH_SHORT).show();
}
});
}
}
內存佔用=MainActivity(1780)+MainActivity$1(12)=1792。
寫法2:成員變量類
public class MainActivity extends AppCompatActivity {
private Button mButton;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mButton = (Button) findViewById(R.id.button);
mButton.setOnClickListener(mOnClickListener);
}
private View.OnClickListener mOnClickListener = new View.OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(MainActivity.this, "hello", Toast.LENGTH_SHORT).show();
}
};
}
內存佔用=MainActivity(1784,包含4字節的成員變量)+MainActivity$1(12)=1796。
寫法3:接口回調
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
private Button mButton;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mButton = (Button) findViewById(R.id.button);
mButton.setOnClickListener(this);
}
@Override
public void onClick(View v) {
Toast.makeText(MainActivity.this, "hello", Toast.LENGTH_SHORT).show();
}
}
內存佔用=1780(減少1個成員變量,避免通過new創建新的對象,內存佔用最少)。
- case11:String的初始化。
case11_1:
public class MainActivity extends AppCompatActivity {
private String mStr = "aaaaa";
...onCreate()
}
case11_2:
public class MainActivity extends AppCompatActivity {
private String mStr = new String("aaaaa");
...onCreate()
}
- “aaaaa”這個String爲何佔用26字節?按以上方式分析,至少佔用內存30=類信息(8)+count(4)+hashCode(4)+char[]引用(4)+char[]數組(10),爲何少了4字節?
- 直接賦值的方式會將”aaaaa”加入到字符串常量池,不佔用堆空間;而case11_2的內存佔用爲 1806=case11_1+26,說明通過new String方式創建的字符串會在堆內存開闢空間。
- case12:String的拼接。
case12_1:基於case11_1,作字符串”+”拼接。
public class MainActivity extends AppCompatActivity {
private String mStr = "aaaaa";
protected void onCreate(Bundle savedInstanceState) {
...
mStr += "c";
}
}
可以發現,拼接後內存佔用1808=case11_1(1780)+28,而這28的空間正好是”aaaaac”的內存大小,也就是說在”+”拼接的時候,產生了一個臨時的變量用於存儲”aaaaac”的結果,並賦值給mStr。印證了《Effective in Java》的第51條中所說”由於字符串不可變,當2個字符串被連接在一起時,他們的內容都要被拷貝”。同時在淺談StringBuilder這篇文章中也講到了”+”拼接的時候,會轉化爲StringBuilder,再通過toString創建一個新的String對象。
case12_2:用StringBuilder進行字符串拼接。
case12_2_1:初始化1個空的StringBuilder
public class MainActivity extends AppCompatActivity {
private StringBuilder mStringBuilder = new StringBuilder();
...onCreate()
}
一個空的StringBuilder就佔49字節,類信息(8)+count(4)+shared(1)+value引用(4)+value[]數組(32)=49。value這個字符數組佔用了32字節,而我們最多也就添加”aaaaac”6個字符,所以這裏可以通過new StringBuilder(6)初始化字符數組的大小,避免浪費。
case12_2_2:使用StringBuilder進行”aaaaa”+”c”的字符串拼接。
public class MainActivity extends AppCompatActivity {
private StringBuilder mStringBuilder = new StringBuilder(6);
@Override
protected void onCreate(Bundle savedInstanceState) {
...
mStringBuilder.append("aaaaa");
mStringBuilder.append("c");
}
}
首先在StringBuilder初始化的時候設置了字符數組大小爲6,所以StringBuilder的初始內存佔用就變小了,而在完成append(“aaaaa”),append(“c”)之後,只要當前字符數組的容量夠用,就不會繼續擴容,避免了String拼接時,內存浪費的問題。當然前提是控制好StringBuilder的char[]初始容量,不然擴容後也會空餘一些閒置內存。
總結
1.謹慎創建成員變量:不管有用沒用,非基礎類型的成員變量只要定義了,至少需要4字節,基礎類型成員變量佔用大小各不一樣。儘量使用局部變量,縮短變量生命週期,促使GC更快回收。
2.謹慎new:如case2的TestModel,不管該對象是否爲空,至少8字節的類信息佔用。如case10的Listener,儘量避免不必要的new。考慮對象的延遲初始化,只有真正使用的時候才new。
3.除非必要,否則儘量使用基礎類型,避免使用裝箱類型。
4.少用內部類:內部類如果不需要訪問到外部類的成員時,可以抽取成獨立外部類,或加static,減少一個this引用(4字節),也可以避免內存泄漏。
5.使用google推薦的數據集合類型SparseArray,ArrayMap替代HashMap。
6.從節省內存的角度考慮,通過接口回調的方式設置OnClickListener爲最優。
7.通過StringBuilder替代String進行字符串拼接,最好預先設置好StringBuilder的容量。