摘要:Android中內存泄漏的的分析。
Android的內存基礎知識
Android系統在安裝、加載一個apk文件時,會在系統內存中劃出一部分作爲該apk的運行內存。
這個運行內存的大小,目前隨着Android設備的進化,也已經適量增大。從早期默認的90M左右到現在200M、300M。當你在設定屬性android:largeheap = "true"時,內存大小基本還會翻倍。如果想要得到具體可用內存,可在代碼中獲取具體數值:
Runtime runtimeMemory=Runtime.getRuntime();
long maxMemory=runtimeMemory.maxMemory()/(1024*1024);
在apk可用內存增大的情況下,你仍然需要注意合理的分配內存,使用內存。雖然在大內存的情況下,可能將一些內存使用的隱患隱藏起來,沒有造成apk崩潰等,但如果apk中存在內存使用不善的情況,如內存泄漏,仍會影響apk的運行效率,嚴重的情況下,apk會發生內存溢出,導致崩潰。
如果將apk可用內存比喻成一隻水桶,apk運行時佔用的內存比喻成桶裏的水。那麼現在這個桶變大變高了,正常情況下桶裏的水是不會滿溢的,Android與Java一樣,會隱式的進行GC垃圾對象回收(你可以顯示調用GC方法回收)。但當apk中存在內存泄漏的情況下,每一次泄漏導致無法GC回收,桶裏的水位就會慢慢增長,直至滿溢,造成內存溢出。
內存溢出是日常代碼編寫中,因不易發現,會導致很多線上問題的產生。代碼編寫的規範與良好習慣,是避免這個問題的主要辦法。
Android中常見的內存泄漏情景
內存泄漏的產生過程:apk運行時,操作系統爲apk中的各種變量以及對象實例分配內存。假設程序運行後,產生了兩個對象:長生命週期對象A,短生命週期對象B,其中A持有了B的引用。在B生命週期結束後,理論上系統GC應該要釋放B佔用的內存,但因爲引用被A持有,導致內存無法釋放,這就造成了內存泄漏。
Android中我們常見的泄漏場景,列出代表性的如下:
1、靜態變量持有短生命週期的對象引用
示例如下:object是一個靜態變量,在onCreate中將當前activity的引用賦值給了object。因爲static變量賦值後,會將引用保存在整個app的方法區。生命週期是與整個app是一致的,會一直持有這個引用,導致當前activity即使onDestroy後,引用也無法被GC釋放,造成內存泄漏。
public class StaticViewTestActivity extends AppCompatActivity {
private static Object object;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_leak_test);
object = this;
}
}
解法:(1)儘量不要將生命週期短暫的對象賦值給static變量,謹慎使用 (2)如果業務有這個需求,以上述代碼爲例,在使用完object後,在onDestroy請將object置爲null。
謹慎使用靜態變量也要分清何時使用,不能總是擔心引發問題而不用。
靜態相關延伸-1
靜態方法是否會造成內存泄漏呢?看下面這段代碼:
public class StaticMethodTestActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_leak_test);
test(this);
}
private static void test(Activity activity){
Object object = activity;
}
}
先給出結論:這是不會造成內存泄漏的。原因在於java、android中,調用方法時(無論是靜態方法還是非靜態方法),JVM的虛機棧都會爲這個方法創建一個方法棧幀。你可以理解爲:一個線程中,有多個方法時,會產生多個棧幀,存於這個線程的棧幀隊列中。當方法被調用完畢後,該棧幀被彈出銷燬。方法中的局部變量等,也都將被釋放,因此不會存在內存泄漏情況。
靜態相關延伸-2
當一個類中,同時存在靜態變量、靜態方法、普通方法時,關係如下:
如圖中所述:一個類中靜態變量、靜態方法的生命週期與該類的實例化對象是沒有關係的。
public static void main(String[] args){
A a = new A();
a.d();
A.c();
A.b = 1;
}
執行以上這段代碼後,對象a的實例將會存在內存中的堆中,靜態方法c與靜態變量b將會存在內存的方法區。
靜態相關延伸-3
還有哪些常見場景是靜態變量持有短生命週期的引用,會引發泄漏?
(1)單例模式,持有短生命週期的context
public class Test {
private static Test INSTANCE;
private Context context;
private Test(Context context){
this.context = context;
}
public static Test getInstance(Context context) {
if (INSTANCE == null) {
synchronized (Test.class) {
if (INSTANCE == null) {
INSTANCE = new Test(context);
}
}
}
return INSTANCE;
}
}
單例模式下getInstance中如果傳入的Context對象引用是activity的引用,因爲單例模式內部INSTANCE是靜態對象,沒有賦值爲null前,都會長存於內存中,context作爲該對象的屬性,也不會釋放,引發內存泄漏。
解法:如果單例中必要傳入Context對象,使用Application的Context對象,因爲這個是與整個app生命週期同步的。
(2) 靜態集合類引發的內存泄漏
public class Test {
private static List<Object> ls;
void operationList(){
ls = new ArrayList<>();
for(int i=0;i<100;i++){
Object o = new Object();
ls.add(o);
o = null;
}
}
}
以上代碼雖然在循環中,每次在集合ls添加Object對象o後,都將o對象置爲null,但集合ls仍持有o的引用,不會釋放。
Object o = new Object(); 這個語句細分爲三個階段。
Object o:聲明引用變量o,並在內存中分配空間;
new Object():創建Object對象,並在內存的堆中分配空間存放它;
= :等號,是個指向,將引用變量o指向創建好的Object對象。
以此分析,上面的代碼在循環中,只是把引用變量本身的指向置爲null,在這之前,已經把引用存入到了靜態集合中,所以不會釋放。
解法:不使用這種寫法。
2、非靜態內部類持有短生命週期的對象引用
內部類概述:
內部類包含靜態內部類和非靜態內部類。
非靜態內部類包含匿名內部類以及內部類(有類名)。
Java與Android中不存在頂層的靜態類,所有的靜態類都是指靜態內部類。
內部類有以下幾種場景:
public class Test {
//局部變量
private int val = 1;
//一個成員內部類
class Inner{
public void testInInner(){
System.out.println("這是一個成員內部類的方法");
System.out.println("可以直接引用外部類Test的變量,val=" + val);
System.out.println("可以直接引用外部類Test的變量,該寫法是在內部類中有同名變量時使用,val=" + Test.this.val);
}
}
public void test1(){
//匿名內部類
//此處有匿名內部類, new Runnable
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("這是一個匿名內部類的方法");
System.out.println("可以直接引用外部類Test的變量,val=" + val);
System.out.println("可以直接引用外部類Test的變量,該寫法是在內部類中有同名變量時使用,val=" + Test.this.val);
}
}).start();
}
public void test2(){
class MethodInner{
public void testInMethodInner(){
System.out.println("這是一個方法內部類的方法");
System.out.println("可以直接引用外部類Test的變量,val=" + val);
System.out.println("可以直接引用外部類Test的變量,該寫法是在內部類中有同名變量時使用,val=" + Test.this.val);
}
}
}
static class StaticInner{
public void testInStatic(){
System.out.println("這是一個成員內部類的方法");
System.out.println("與外部類無關,不可以直接引用外部類Test的非靜態變量val");
}
}
}
(1)非靜態內部類爲何容易造成內存泄漏
主要原因在於:非靜態內部類隱性的持有外部類的引用,當內部類中進行耗時等操作時,外部類的引用會被一直持有,無法被釋放。
將上面的Test類做編譯操作(javac),能看到同級目錄下生成了5個字節碼文件:Test$1.class 、Test$1MethodInner.class 、Test$Inner.class、Test$StaticInner.class、Test.class;
以Test$1.class爲例,字節碼文件中默認生成的構造函數中,參數是外部類的對象引用。除靜態內部類外,其他的內部類也是類似的構造,因此說非靜態內部類隱性的持有外部類的引用。
class Test$Inner {
Test$Inner(Test var1) {
this.this$0 = var1;
}
public void testInInner() {
System.out.println("這是一個成員內部類的方法");
System.out.println("可以直接引用外部類Test的變量,val=" + Test.access$000(this.this$0));
System.out.println("可以直接引用外部類Test的變量,該寫法是在內部類中有同名變量時使用,val=" + Test.access$000(this.this$0));
}
}
其中access$000是編譯器默認給外部類生成的方法。
(2)內部類的泄漏,有哪些場景
- 如上面舉例的new Runnable(){...},如果再run中有耗時操作,在耗時操作未結束前,就退出頁面,因持有外部類引用並不釋放,就會造成內存泄漏。
public class ThreadTestActivity extends AppCompatActivity {
private static final String TAG = "ThreadTestActivity";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_leak_test);
Log.d(TAG, "onCreate-this:"+this.toString());
testThread();
}
private void testThread(){
new Thread(new Runnable() {
@Override
public void run() {
Log.d(TAG, "testThread-this:"+this.toString());
SystemClock.sleep(10*1000);
}
}).start();
}
}
解法:改寫方法,如必要這麼寫,定義靜態內部類實現Runnable接口。
- 如定時器 TimeTask
new Timer().schedule(new TimerTask() {
@Override
public void run() {
while (true);
}
},3 * 1000);
解法:在相對位置,對定時器做cancel,或者改爲靜態內部類實現。
- 如Handler。下方例子:匿名內部類new Handler(){...}持有外部類Test的引用。handlerOp方法執行後,主線程的消息隊列在60s內都會持有handler的引用。handler又持有了外部類Test的引用,導致Test對象無法回收,造成內存泄漏。
public class Test {
Handler handler = new Handler(){
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
}
};
public void handlerOp(){
handler.postDelayed(new Runnable() {
@Override
public void run() {
// doSomeThing
}
},60 * 1000);
}
}
解法:靜態內部類實現,或者handler改爲弱引用,或者在相對位置對handler做remove,語句(Handler.removeCallbacksAndMessages(null);)
- 其他場景,如AsyncTask等,類似處理。
3、資源對象未釋放
資源對象未釋放,也是出現內存泄漏的一個常見場景。
(1)如文件未關閉,導致分配給該文件引用的緩衝未及時釋放。如果多次未關閉,文件句柄太多沒有被關閉(Could not read input channel file descriptors from parcel)
(2)如數據庫操作的遊標 Cursor未關閉,頻繁操作後會導致(android.database.CursorWindowAllocationException: Cursor window allocation of 2048 kb failed)
如下示例,頻繁操作而out不做處理,可能就會導致句柄過多,內存泄漏直至溢出。
public class ResourceTestActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_leak_test);
test();
}
private void test(){
String filename = "app.txt";
File file = new File(getExternalCacheDir(),filename);
try{
file.createNewFile();
FileOutputStream out = new FileOutputStream(file);
} catch (FileNotFoundException e){
} catch (IOException e){
}
}
}
解法:針對以上等情況,在正常流程或異常流程中,對資源關閉做好處理,如finally中做好關閉操作。
總結的說,內存泄漏本質就是本該被回收的內存,沒有被回收,多關注生命週期。
內存泄漏不經意間就會被你寫出,時時輕拂拭,莫使惹塵埃。