1. 程式人生 > >Android 效能篇 -- 帶你領略Android記憶體洩漏的前世今生

Android 效能篇 -- 帶你領略Android記憶體洩漏的前世今生

基礎瞭解

什麼是記憶體洩漏?

記憶體洩漏是當程式不再使用到的記憶體時,釋放記憶體失敗而產生了無用的記憶體消耗。記憶體洩漏並不是指物理上的記憶體消失,這裡的記憶體洩漏是指由程式分配的記憶體但是由於程式邏輯錯誤而導致程式失去了對該記憶體的控制,使得記憶體浪費。

Java 記憶體分配策略

Java 程式執行時的記憶體分配策略有三種,分別是 靜態分配 棧式分配 堆式分配 ,對應的三種儲存策略使用的記憶體空間主要分別是 靜態儲存區(也稱方法區) 棧區 堆區

         ? 靜態儲存區(方法區):主要存放 靜態資料全域性 static 資料常量。這塊記憶體在程式編譯時就已經分配好,並且在程式整個執行期間都存在。

         ? 棧區:當方法被執行時,方法體內的 區域性變數 (其中包括基礎資料型別、物件的引用)都在棧上建立,並在方法執行結束時這些區域性變數所持有的記憶體將會自動被釋放。因為棧記憶體分配運算內置於處理器的指令集中,效率很高,但是分配的記憶體容量有限。

         ? 堆區: 又稱動態記憶體分配,通常就是指在程式執行時直接 new 出來的記憶體,也就是 物件的例項。這部分記憶體在不使用時將會由 Java 垃圾回收器(GC)來負責回收。

棧與堆的區別

在方法體內定義的(區域性變數)一些基本型別的變數和物件的引用變數都是在方法的棧記憶體中分配的。當在一段方法塊中定義一個變數時,Java 就會在棧中為該變數分配記憶體空間,當超過該變數的作用域後,該變數也就無效了,分配給它的記憶體空間也將被釋放掉,該記憶體空間可以被重新使用。

堆記憶體用來存放所有由 new 建立的物件(包括該物件其中的所有成員變數)和陣列。在堆中分配的記憶體,將由 Java 垃圾回收器來自動管理。在堆中產生了一個數組或者物件後,還可以在棧中定義一個特殊的變數,這個變數的取值等於陣列或者物件在堆記憶體中的首地址,這個特殊的變數就是我們上面說的引用變數。我們可以通過這個引用變數來訪問堆中的物件或者陣列。

舉例說明:

public class Sample {
    int s1 = 0;
    Sample mSample1 = new Sample();

    public void method() {
        int s2 = 1;
        Sample mSample2 = new Sample();     // Sample 類的區域性變數 s2 和引用變數 mSample2 都是存在於棧中,但 mSample2 指向的物件是存在於堆上
    }
}

Sample mSample3 = new Sample();             // mSample3 指向的物件實體存放在堆上,包括這個物件的所有成員變數 s1 和 mSample1,而它自己存在於棧中。

Java是如何管理記憶體

Java的記憶體管理就是物件的分配和釋放問題。在 Java 中,程式設計師需要通過關鍵字 new 為每個物件申請記憶體空間 (基本型別除外),所有的物件都在堆 (Heap)中分配空間。另外,物件的釋放是由 GC 決定和執行的。在 Java 中,記憶體的分配是由程式完成的,而記憶體的釋放是由 GC 完成的,這種收支兩條線的方法確實簡化了程式設計師的工作。但同時,它也加重了JVM的工作。這也是 Java 程式執行速度較慢的原因之一。因為,GC 為了能夠正確釋放物件,GC 必須監控每一個物件的執行狀態,包括物件的申請、引用、被引用、賦值等,GC 都需要進行監控。

監視物件狀態是為了更加準確地、及時地釋放物件,而釋放物件的根本原則就是該物件不再被引用。

Java中的記憶體洩漏

在Java中,記憶體洩漏就是存在一些被分配的物件,這些物件有下面兩個特點,首先,這些物件是可達的,即在有向圖中,存在通路可以與其相連;其次,這些物件是無用的,即程式以後不會再使用這些物件。如果物件滿足這兩個條件,這些物件就可以判定為Java中的記憶體洩漏,這些物件不會被GC所回收,然而它卻佔用記憶體。

在C++中,記憶體洩漏的範圍更大一些。有些物件被分配了記憶體空間,然後卻不可達,由於C++中沒有GC,這些記憶體將永遠收不回來。在Java中,這些不可達的物件都由GC負責回收,因此程式設計師不需要考慮這部分的記憶體洩露。

通過分析,我們得知,對於C++,程式設計師需要自己管理邊和頂點,而對於Java程式設計師只需要管理邊就可以了(不需要管理頂點的釋放)。通過這種方式,Java提高了程式設計的效率。

因此,通過以上分析,我們知道在Java中也有記憶體洩漏,但範圍比C++要小一些。因為Java從語言上保證,任何物件都是可達的,所有的不可達物件都由GC管理。

對於程式設計師來說,GC基本是透明的,不可見的。雖然,我們只有幾個函式可以訪問GC,例如執行GC的函式System.gc(),但是根據Java語言規範定義, 該函式不保證JVM的垃圾收集器一定會執行。因為,不同的JVM實現者可能使用不同的演算法管理GC。通常,GC的執行緒的優先級別較低。JVM呼叫GC的策略也有很多種,有的是記憶體使用到達一定程度時,GC才開始工作,也有定時執行的,有的是平緩執行GC,有的是中斷式執行GC。但通常來說,我們不需要關心這些。除非在一些特定的場合,GC的執行影響應用程式的效能,例如對於基於Web的實時系統,如網路遊戲等,使用者不希望GC突然中斷應用程式執行而進行垃圾回收,那麼我們需要調整GC的引數,讓GC能夠通過平緩的方式釋放記憶體,例如將垃圾回收分解為一系列的小步驟執行,Sun提供的HotSpot JVM就支援這一特性。

以下給出一個 Java 記憶體洩漏的典型例子:

Vector v = new Vector(10);
for (int i = 1; i < 100; i++) {
    Object o = new Object();
    v.add(o);
    o = null;   
}

在這個例子中,我們迴圈申請Object物件,並將所申請的物件放入一個 Vector 中,如果我們僅僅釋放引用本身,那麼 Vector 仍然引用該物件,所以這個物件對 GC 來說是不可回收的。因此,如果物件加入到Vector 後,還必須從 Vector 中刪除,最簡單的方法就是將 Vector 物件設定為 null。

常見記憶體洩漏

永遠的單例

單例的使用在我們的程式中隨處可見,因為使用它可以完美的解決我們在程式中重複建立物件的問題,不過可別小瞧它。由於 單例的靜態特性使得其生命週期跟應用的生命週期一樣長 ,所以一旦使用有誤,小心無限制的持有Activity的引用而導致記憶體洩漏。

我們看個例子:

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;
    }
}

這是一個普通的單例模式,當建立這個單例的時候,由於需要傳入一個Context,所以這個Context的生命週期的長短至關重要! (實際常見)

1、如果此時傳入的是 Application 的 Context ,因為 Application 的生命週期就是整個應用的生命週期,所以這將沒有任何問題。

2、如果此時傳入的是 Activity 的 Context ,當這個 Context 所對應的 Activity 退出時,由於該 Context 的引用被單例物件所持有,其生命週期等於整個應用程式的生命週期,所以當前 Activity 退出時它的記憶體並不會被回收,這就造成洩漏了。

正確的方式(寫法一):

public class AppManager {

    private static AppManager instance;
    private Context context;
    
    private AppManager(Context context) {
        this.context = context.getApplicationContext(); // 使用 Application 的 context
    }
    
    public static AppManager getInstance(Context context) {
        if (instance == null) {
            instance = new AppManager(context);
        }
        return instance;
    }
}

正確的方式(寫法二):

// 在你的 Application 中新增一個靜態方法,getContext() 返回 Application 的 context

...

context = getApplicationContext();

...
   /**
     * 獲取全域性的context
     * @return 返回全域性context物件
     */
    public static Context getContext(){
        return context;
    }

public class AppManager {

    private static AppManager instance;
    private Context context;
    
    private AppManager() {
        this.context = MyApplication.getContext(); // 使用Application 的context
    }
    
    public static AppManager getInstance() {
        if (instance == null) {
            instance = new AppManager();
        }
        return instance;
    }
}

靜態Activity

我們看一段程式碼:

public class MainActivity extends AppCompatActivity {
    private static MainActivity activity;         // 這邊設定了靜態Activity,發生了記憶體洩漏
    TextView saButton;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        saButton = (TextView) findViewById(R.id.text);
        saButton.setOnClickListener(new View.OnClickListener() {
            @Override public void onClick(View v) {
                setStaticActivity();
                nextActivity();
            }
        });
    }
    void setStaticActivity() {
        activity = this;
    }

    void nextActivity(){
        startActivity(new Intent(this,RegisterActivity.class));
        SystemClock.sleep(1000);
        finish();
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
    }
}

在上面程式碼中,我們聲明瞭一個靜態的 Activity 變數並且在 TextView 的 OnClick 事件裡引用了當前正在執行的 Activity 例項,所以如果在 activity 的生命週期結束之前沒有清除這個引用,則會引起記憶體洩漏。因為宣告的 activity 是靜態的,會常駐記憶體,如果該物件不清除,則垃圾回收器無法回收變數。

我們可以這樣解決:

    protected void onDestroy() {
        super.onDestroy();
        activity = null;       // 在onDestory方法中將靜態變數activity置空,這樣垃圾回收器就可以將靜態變量回收
    }

靜態View

其實和靜態Activity頗為相似,我們看下程式碼:

    ...
    private static View view;               // 定義靜態View
    TextView saButton;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        saButton = (TextView) findViewById(R.id.text);
        saButton.setOnClickListener(new View.OnClickListener() {
            @Override public void onClick(View v) {
                setStaticView();
                nextActivity();
            }
        });
    }
    void setStaticView() {
        view = findViewById(R.id.sv_view);
    }
    ...

View一旦被載入到介面中將會持有一個Context物件的引用,在這個例子中,這個context物件是我們的Activity,宣告一個靜態變數引用這個View,也就引用了activity,所以當activity生命週期結束了,靜態View沒有清除掉,還持有activity的引用,因此記憶體洩漏了。

我們可以這樣解決:

protected void onDestroy() {
    super.onDestroy();
    view = null;         // 在onDestroy方法裡將靜態變數置空
} 

匿名類/AsyncTask

我們看下面的例子:

public class MainActivity extends AppCompatActivity {
    void startAsyncTask() {
        new AsyncTask<Void, Void, Void>() {
            @Override protected Void doInBackground(Void... params) {
                while(true);
            }
        }.execute();
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        View aicButton = findViewById(R.id.at_button);
        aicButton.setOnClickListener(new View.OnClickListener() {
            @Override 
            public void onClick(View v) {
                startAsyncTask();
            }
        });
    }
}

上面程式碼在activity中建立了一個匿名類 AsyncTask,匿名類和非靜態內部類相同,會持有外部類物件,這裡也就是activity,因此如果你在 Activity 裡宣告且例項化一個匿名的AsyncTask物件,則可能會發生記憶體洩漏,如果這個執行緒在Activity銷燬後還一直在後臺執行,那這個執行緒會繼續持有這個Activity的引用從而不會被GC回收,直到執行緒執行完成。

我們可以這樣解決:

自定義靜態 AsyncTask 類,並且讓 AsyncTask 的週期和 Activity 週期保持一致,也就是在 Activity 生命週期結束時要將 AsyncTask cancel 掉。

非靜態內部類

有的時候我們可能會在啟動頻繁的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();
        }
        //...
    }

    class TestResource {
        //...
    }
}

上面這段程式碼在Activity內部建立了一個非靜態內部類的單例(mManager),每次啟動Activity時都會使用該單例的資料,這樣雖然避免了資源的重複建立,不過這種寫法卻會造成記憶體洩漏。

因為非靜態內部類預設會持有外部類的引用,而該非靜態內部類又建立了一個靜態的例項,該例項的生命週期和應用的一樣長,這就導致了該靜態例項一直會持有該Activity的引用,導致Activity的記憶體資源不能正常回收。

正確的做法為:

將該內部類設為靜態內部類或將該內部類抽取出來封裝成一個單例,如果需要使用Context,請按照上面推薦的使用Application 的 Context。當然,Application 的 context 不是萬能的,所以也不能隨便亂用,對於有些地方則必須使用 Activity 的 Context。

Handler

Handler 的使用造成的記憶體洩漏問題應該說是 最為常見 了,很多時候我們為了避免 ANR 而不在主執行緒進行耗時操作,在處理網路任務或者封裝一些請求回撥等api都藉助Handler來處理,但 Handler 不是萬能的,對於 Handler 的使用程式碼編寫不規範即有可能造成記憶體洩漏。另外,我們知道 Handler、Message 和 MessageQueue 都是相互關聯在一起的,萬一 Handler 傳送的 Message 尚未被處理,則該 Message 及傳送它的 Handler 物件將被執行緒 MessageQueue 一直持有。

由於 Handler 屬於 TLS(Thread Local Storage) 變數, 生命週期和 Activity 是不一致的。因此這種實現方式一般很難保證跟 View 或者 Activity 的生命週期保持一致,故很容易導致無法正確釋放。

public class SampleActivity extends Activity {

    private final Handler mLeakyHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            // ...
        }
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // Post a message and delay its execution for 10 minutes.
        mLeakyHandler.postDelayed(new Runnable() {
            @Override
            public void run() { /* ... */ }
        }, 1000 * 60 * 10);

        // Go back to the previous Activity.
        finish();
    }
}

在該 SampleActivity 中聲明瞭一個延遲 10分鐘 執行的訊息 Message,mLeakyHandler 將其 push 進了訊息佇列 MessageQueue 裡。當該 Activity 被 finish() 掉時,延遲執行任務的 Message 還會繼續存在於主執行緒中,它持有該 Activity 的 Handler 引用,所以此時 finish() 掉的 Activity 就不會被回收了從而造成記憶體洩漏(因 Handler 為非靜態內部類,它會持有外部類的引用,在這裡就是指 SampleActivity)。

正確的做法為:

在 Activity 中避免使用非靜態內部類,比如上面我們將 Handler 宣告為靜態的,則其存活期跟 Activity 的生命週期就無關了。同時通過弱引用的方式引入 Activity,避免直接將 Activity 作為 context 傳進去,見下面程式碼:

public class SampleActivity extends Activity {

    private static class MyHandler extends Handler {
        private final WeakReference<SampleActivity> mActivity;

        public MyHandler(SampleActivity activity) {
            mActivity = new WeakReference<SampleActivity>(activity);
        }

        @Override
        public void handleMessage(Message msg) {
            SampleActivity activity = mActivity.get();
            if (activity != null) {                              // 每次使用前注意判空
                // ...
            }
        }
    }

    private final MyHandler mHandler = new MyHandler(this);

    private static final Runnable sRunnable = new Runnable() {
        @Override
        public void run() { /* ... */ }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // Post a message and delay its execution for 10 minutes.
        mHandler.postDelayed(sRunnable, 1000 * 60 * 10);

        // Go back to the previous Activity.
        finish();
    }
}

從上面的程式碼中我們可以看出如何避免Handler記憶體洩漏,推薦使用 "靜態內部類 + WeakReference" 這種方式,每次使用前注意判空。

Java對引用的分類有Strong reference、SoftReference、WeakReference、PhatomReference四種。

級別 回收機制 用途 生存時間
從來不會 物件的一般狀態 JVM停止執行時終止
在記憶體不足時 聯合ReferenceQueue構造有效期短/佔記憶體打/生命週期長的物件的二級高速緩衝器(記憶體不足時才情況) 記憶體不足時終止
在垃圾回收時 聯合ReferenceQueue構造有效期短/佔記憶體打/生命週期長的物件的一級高速緩衝器(系統發生gc時清空) gc執行後終止
在垃圾回收時 聯合ReferenceQueue來跟蹤物件被垃圾回收期回收的活動 gc執行後終止

在Android應用的開發中,為了防止記憶體溢位,在處理一些佔用記憶體大而且宣告週期較長的物件時候,可以儘量應用軟引用和弱引用技術。

軟/弱引用可以和一個引用佇列(ReferenceQueue)聯合使用,如果軟引用所引用的物件被垃圾回收器回收,Java虛擬機器就會把這個軟引用加入到與之關聯的引用佇列中。利用這個佇列可以得知被回收的軟/弱引用的物件列表,從而為緩衝器清除已失效的軟/弱引用。

Thread

看個範例:

public class SampleActivity extends Activity {
    void spawnThread() {
        new Thread() {
            @Override public void run() {
                while(true);
            }
        }.start();
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        View tButton = findViewById(R.id.t_button);
        tButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                spawnThread();
            }
        });
    }
}

其實這邊發生的記憶體洩漏原因跟AsyncTask是一樣的。

正確的做法為:

我們自定義Thread並宣告成static這樣可以嗎?其實這樣的做法並不推薦,因為Thread位於GC根部,DVM會和所有的活動執行緒保持hard references關係,所以執行中的Thread絕不會被GC無端回收了,所以正確的解決辦法是在自定義靜態內部類的基礎上給執行緒加上取消機制,因此我們可以在Activity的onDestroy方法中將thread關閉掉。

Timer Tasks

看個範例:

public class SampleActivity extends Activity {
    void scheduleTimer() {
        new Timer().schedule(new TimerTask() {
            @Override
            public void run() {
                while(true);
            }
        },1000);
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        View ttButton = findViewById(R.id.tt_button);
        ttButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
            scheduleTimer();
            }
        });
    }
}

這裡記憶體洩漏在於Timer和TimerTask沒有進行Cancel,從而導致Timer和TimerTask一直引用外部類Activity。

正確的做法為:

在適當的時機進行Cancel。

Sensor Manager

看個範例:

public class SampleActivity extends Activity {
    void registerListener() {
           SensorManager sensorManager = (SensorManager) getSystemService(SENSOR_SERVICE);
           Sensor sensor = sensorManager.getDefaultSensor(Sensor.TYPE_ALL);
           sensorManager.registerListener(this, sensor, SensorManager.SENSOR_DELAY_FASTEST);
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        View smButton = findViewById(R.id.sm_button);
        smButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                registerListener();
            }
        });
    }
}

通過Context呼叫getSystemService獲取系統服務,這些服務執行在他們自己的程序執行一系列後臺工作或者提供和硬體互動的介面,如果Context物件需要在一個Service內部事件發生時隨時收到通知,則需要把自己作為一個監聽器註冊進去,這樣服務就會持有一個Activity,如果開發者忘記了在Activity被銷燬前登出這個監聽器,這樣就導致記憶體洩漏。

正確的做法為:

在onDestroy方法裡登出監聽器。

儘量避免使用 static 成員變數

如果成員變數被宣告為 static,那我們都知道其生命週期將與整個app程序生命週期一樣。

這會導致一系列問題,如果你的app程序設計上是長駐記憶體的,那即使app切到後臺,這部分記憶體也不會被釋放。按照現在手機app記憶體管理機制,佔記憶體較大的後臺程序將優先回收,如果此app做過程序互保保活,那會造成app在後臺頻繁重啟。當手機安裝了你參與開發的app以後一夜時間手機被消耗空了電量、流量,你的app不得不被使用者解除安裝或者靜默。

這裡修復的方法是:

不要在類初始時初始化靜態成員。可以考慮lazy初始化(使用時初始化)。架構設計上要思考是否真的有必要這樣做,儘量避免。如果架構需要這麼設計,那麼此物件的生命週期你有責任管理起來。

避免 override finalize()

1、finalize 方法被執行的時間不確定,不能依賴與它來釋放緊缺的資源。時間不確定的原因是:虛擬機器呼叫GC的時間不確定Finalize daemon執行緒被排程到的時間不確定

2、finalize 方法只會被執行一次,即使物件被複活,如果已經執行過了 finalize 方法,再次被 GC 時也不會再執行了,原因是:

含有 finalize 方法的 object 是在 new 的時候由虛擬機器生成了一個 finalize reference 在來引用到該Object的,而在 finalize 方法執行的時候,該 object 所對應的 finalize Reference 會被釋放掉,即使在這個時候把該 object 復活(即用強引用引用住該 object ),再第二次被 GC 的時候由於沒有了 finalize reference 與之對應,所以 finalize 方法不會再執行。

3、含有Finalize方法的object需要至少經過兩輪GC才有可能被釋放。

集合物件及時清除

我們通常會把一些物件的引用加入到集合容器(比如ArrayList)中,當我們不再需要該物件時,並沒有把它的引用從集合中清理掉,這樣這個集合就會越來越大。如果這個集合是static的話,那情況就更嚴重了。

所以在退出程式之前,將集合裡面的東西clear,然後置為null,再退出程式,如下:

private List<String> nameList;
private List<Fragment> list;

@Override
public void onDestroy() {
    super.onDestroy();
    if (nameList != null){
        nameList.clear();
        nameList = null;
    }
    if (list != null){
        list.clear();
        list = null;
    }
}

webView

當我們不再需要使用webView的時候,應該呼叫它的destory()方法來銷燬它,並釋放其佔用的記憶體,否則其佔用的記憶體長期也不能回收,從而造成記憶體洩漏。

正確的做法為:

為webView開啟另外一個程序,通過AIDL與主執行緒進行通訊,webView所在的程序可以根據業務的需要選擇合適的時機進行銷燬,從而達到記憶體的完整釋放。

資源未關閉

對於使用了BraodcastReceiver,ContentObserver,File,遊標 Cursor,Stream,Bitmap等資源的使用,應該在Activity銷燬時及時關閉或者登出,否則這些資源將不會被回收,造成記憶體洩漏。

拓展 -- 相關知識點

static 關鍵字

使用static宣告屬性

如果在程式中使用static宣告屬性,則此屬性稱為全域性屬性(也稱靜態屬性),那麼宣告成全域性屬性有什麼用?我們看下程式碼:

class Person {
    String name;
    int age;
    static String country = "A城";
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
    public void info() {
        System.out.println("姓名:" + this.name + ",年齡:" + this.age + ",城市:" + country);
    }
};

public class Demo {
    public static void main(String agrs[]) {
        Person p1 = new Person("張三", 30);
        Person p1 = new Person("李四", 31);
        Person p1 = new Person("王五", 32);
        Person.country = "B城";
        p1.info();
        p2.info();
        p3.info();
    }
}

以上程式很清晰的說明了static宣告屬性的好處,需要注意一點的是,類的公共屬性應該由類進行修改是最合適的(當然也可以p1.country = ...),有時也就把使用static宣告的屬性稱為類屬性。

使用static宣告方法

直接看下程式碼就清楚了:

class Person {
    private String name;
    private int age;
    private static String country = "A城";
    public static void setCountry(String C) {
        country = c;
    }
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
    public void info() {
        System.out.println("姓名:" + this.name + ",年齡:" + this.age + ",城市:" + country);
    }
    public static String getCountry() {
        return country;
    }
};

public class Demo {
    public static void main(String agrs[]) {
        Person p1 = new Person("張三", 30);
        Person p1 = new Person("李四", 31);
        Person p1 = new Person("王五", 32);
        Person.setCountry("B城");
        p1.info();
        p2.info();
        p3.info();
    }
}

【特殊說明】

      ? 非static宣告的方法可以呼叫static宣告的屬性或方法      ? static宣告的方法不能呼叫非static宣告的屬性或方法

比如以下程式碼就會出錯:

class Person {
    private static String country = "A城";
    private String name = "Hello";
    public static void sFun(String C) {
        System.out.println("name = " + name);       // 錯誤,不能呼叫非static屬性
        fun();                                      // 錯誤,不能呼叫非static方法
    }
    public void fun() {
        System.out.println("World!!!");
    }
};

內部類

基本定義

我們都知道,在類內部可以定義成員變數與方法,同樣,在類內部也可以定義另一個類。如果在類Outer的內部定義一個類Inner,此時類Inner就稱為內部類,而類Outer則稱為外部類。

內部類可宣告成 public 或 private。當內部類宣告成 public 或 private時,對其訪問的限制與成員變數和成員方法完全相同。

內部類的定義格式

識別符號 class 外部類的名稱 {
    // 外部類的成員
    識別符號 class 內部類的名稱 {
        // 內部類的成員
    }
}

內部類的好處

可以方便地訪問外部類中的私有屬性!

靜態內部類

使用static可以宣告屬性或方法,而使用static也可以宣告內部類,用static宣告的內部類就變成了外部類,但是用static宣告的內部類不能訪問非static的外部類屬性。

比如如下例子:

class Outer {
    private static String info = "Hello World!!!";    // 如果此時info不是static屬性,則程式執行報錯
    static class Inner {
        public void print() {
            System.out.println(info);
        }
    };
};

public class InnerClassDemo {
    public static void main(String args[]) {
        new Outer.Inner().print();
    }
}

執行結果:

Hello World!!!

在外部訪問內部類

一個內部類除了可以通過外部類訪問,也可以直接在其他類中進行呼叫。

【在外部訪問內部類的格式】

外部類.內部類 內部類物件 = 外部類例項.new 內部類();
class Outer {
    private String info = "Hello World!!!";  
    class Inner {
        public void print() {
            System.out.println(info);
        }
    };
};

public class InnerClassDemo {
    public static void main(String args[]) {
        Outer out = new Out();              // 例項化外部類物件
        Outer.Inner in = out.new Inner();   // 例項化內部類物件
        in.print();                         // 呼叫內部類方法
    }
}

在方法中定義內部類

除了在外部類中定義內部類,我們也可以在方法中定義內部類。但是需要注意的是,在方法中定義的內部類不能直接訪問方法中的引數,如果方法中的引數想要被內部類訪問,則引數前必須加上final關鍵字。

class Outer {
    private String info = "Hello World!!!";
    public void fun(final int temp) {      // 引數要被訪問必須用final宣告
        class Inner {
            public void print() {
                System.out.println("類中的屬性:" + info);
                System.out.println("方法中的引數:" + temp);
            }
        };
        new Inner().print();
    }
};

public class InnerClassDemo {
    public static void main(String args[]) {
        new Outer().fun(30);               // 呼叫外部類方法              
    }
}

總結

在開發中,記憶體洩漏最壞的情況是app耗盡記憶體導致崩潰,但是往往真實情況不是這樣的,相反它只會耗盡大量記憶體但不至於閃退,可分配的記憶體少了,GC便會更多的工作釋放記憶體,GC是非常耗時的操作,因此會使得頁面卡頓。我們在開發中一定要注意當在Activity裡實例化一個物件時看看是否有潛在的記憶體洩漏,一定要經常對記憶體洩漏進行檢測。

參考