內(nèi)存泄露和內(nèi)存優(yōu)化

內(nèi)存泄露和內(nèi)存優(yōu)化

對(duì)于Android來說,每一個(gè)APP的內(nèi)存是有限的。你過你的內(nèi)存出現(xiàn)問題:泄露,長期占用過高,就會(huì)導(dǎo)致app易于被殺掉。頻繁的gc導(dǎo)致app卡頓等現(xiàn)象。

常見情況

  • Activity的Context的使用

    • 界面的Context靜態(tài)化
    • 單例式將界面的Context作為初始化入?yún)?shù),并且在單例模式保存
    • 特殊的,在Android 6.0中,不能使用Activity的Context通過接口getSystemService()來獲取各種Manager,如下所示:
    AActivityManager activityManager =(ActivityManager)MainActivity.this.getSystemService(Context.ACTIVITY_SERVICE);  
    

如上所示,在Android 6.0 中就會(huì)造成內(nèi)存泄露

  • 非靜態(tài)內(nèi)部類持有外部類的引用

在Java中,非靜態(tài)內(nèi)部類(包括匿名內(nèi)部類)都會(huì)持有外部類(一般是指Activity等頁面)的引用,當(dāng)兩者的生命周期出現(xiàn)不一致的時(shí)候,很容易導(dǎo)致內(nèi)存泄露。
  如下所示,非常常見的幾種情況:
Hanlder

private Handler mHandler = new Handler() {
 @Override    
 public void handleMessage(Message msg)      
 {   
  super.handleMessage(msg);  
 }  
}; 

這里的Handler會(huì)引用Activity的引用,當(dāng)handler調(diào)用postDelay的時(shí)候,若Activity已經(jīng)finish掉了,因?yàn)檫@個(gè) handler 會(huì)在一段時(shí)間內(nèi)繼續(xù)被 main Looper 持有,導(dǎo)致引用仍然存在,在這段時(shí)間內(nèi),如果內(nèi)存吃緊至超出,是很危險(xiǎn)的。

Thread

public class ThreadActivity extends Activity {  
    public void onCreate(Bundle savedInstanceState) {  
        super.onCreate(savedInstanceState);  
        setContentView(R.layout.activity_main);  
        new MyThread().start();  
    }  
  
    private class MyThread extends Thread {  
        @Override  
        public void run() {  
            super.run();  
            dosomthing();  
        }  
    }  
    private void dosomthing(){  
      
    }  
}  

假設(shè)MyThread的run函數(shù)是一個(gè)很費(fèi)時(shí)的操作,當(dāng)我們開啟該線程后,將設(shè)備的橫屏變?yōu)榱素Q屏,一般情況下當(dāng)屏幕轉(zhuǎn)換時(shí)會(huì)重新創(chuàng)建Activity,按照我們的想法,老的Activity應(yīng)該會(huì)被銷毀才對(duì),然而事實(shí)上并非如此。由于我們的線程是Activity的內(nèi)部類,所以MyThread中保存了Activity的一個(gè)引用,當(dāng)MyThread的run函數(shù)沒有結(jié)束時(shí),MyThread是不會(huì)被銷毀的,因此它所引用的老的Activity也不會(huì)被銷毀,因此就出現(xiàn)了內(nèi)存泄露的問題。

Runnable

  public class MainActivity extends Activity {
    ...
    Runnable ref1 = new MyRunable();
    Runnable ref2 = new Runnable() {
        @Override
        public void run() {

        }
    };
       ...
    }

ref1和ref2的區(qū)別是,ref2使用了匿名內(nèi)部類,也就是說當(dāng)前的Activity會(huì)被ref2所應(yīng)用,如果將這個(gè)引用傳入到了一個(gè)異步線程,該線程的生命周期與Activity的生命周期不一致的時(shí)候,就會(huì)導(dǎo)致內(nèi)存泄露。

  • Static變量造成內(nèi)存泄露

1. 界面類的靜態(tài)化: 靜態(tài)Activity
  2. 界面中View的靜態(tài)化: 靜態(tài)View
  界面中View的靜態(tài)化一定會(huì)導(dǎo)致頁面內(nèi)存泄露。界面中的View都是持有界面引用的,靜態(tài)變量的生命周期與整個(gè)app的生命周期一致。
  3. 非靜態(tài)內(nèi)部類的靜態(tài)化
  具體的 如下所示:

public class MainActivity extends AppCompatActivity {  

    private static Drawable sDrawable;

    @Override protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        TextView lableView = new TextView(this);
    if(sDrawable == null) {
        sDrawable = getDrawable(R.drawable.icon);       
    }
    labelView.setBackgroundDrawable(sDrawable);
        setContentView(lableView);
    }
}  

View的setBackgroundDrawable()的源碼如下所示:

public void setBackgroundDrawable(Drawable background) {
        ...

        if (background != null) {
            ...

            background.setCallback(this);
            ...
        } else {
            ...
        }

        ...
}

其中有一個(gè)background.setCallback(this);,所以這就導(dǎo)致這個(gè)靜態(tài)變量指向的對(duì)象又持有了TextView這個(gè)對(duì)象的引用,TextView持有的確實(shí)整個(gè)Activity的引用。這樣就導(dǎo)致了內(nèi)存泄露。

我們?cè)賮砜匆粋€(gè)例子:

public class MainActivity extends AppCompatActivity {
    private static InnerClass sInnerClass;

    @Override protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_leak);
        sHello = new Hello();
    }
    public class InnerClass {}
}  

靜態(tài)的非靜態(tài)內(nèi)部類對(duì)象sInnerClass持有了外部Acitivity的引用,當(dāng)屏幕發(fā)生變化時(shí),不會(huì)被釋放。

  • <font size = 5>資源沒有關(guān)閉</font>
      1. Cursor游標(biāo)沒有關(guān)閉
      數(shù)據(jù)庫中才操作經(jīng)常碰到cursor。
      2. InputStream、OutputStream等沒有關(guān)閉
      文件讀寫、Socket讀寫等經(jīng)常碰到
      3. 注冊(cè)的廣播等沒有unRegister
      4. 一些CallBack的Listener沒有被清除,舉例:
void registerListener() {
    SensorManager sensorManager = (SensorManager)getSystemService(SENSOR_SERVICE);
    Sensor snedor = sensorManager.getDefaultSensor(Sensor.TYPE_ALL);
    sensorManager.registerListneer(this, sensor, SensorManager.SENSOR_DELAY_FASTEST);
}

getSystemService負(fù)責(zé)執(zhí)行某些后臺(tái)任務(wù),或?yàn)橛布峁┙涌冢绻鹀ontext對(duì)象想要在服務(wù)內(nèi)部的事件發(fā)生時(shí)被通知,需要注冊(cè)監(jiān)聽器。然而這讓服務(wù)持有了activity的引用,如果activity銷毀時(shí)沒有取消注冊(cè),那么你的activity就泄露了。

  • View添加到?jīng)]有刪除機(jī)制的容器中

  • 屬性動(dòng)畫導(dǎo)致的內(nèi)存泄露
      如果你設(shè)置你的動(dòng)畫為無限循環(huán),而且沒有在onDestroy中停止該動(dòng)畫,那么動(dòng)畫會(huì)一直播放下去,Activity的View會(huì)被動(dòng)畫吃持有,而View持有了Activiy。從而導(dǎo)致內(nèi)存泄露。

  • <font size = 5>過期引用</font>

當(dāng)一個(gè)數(shù)組擴(kuò)容后又被縮減,比如size從0->200->100(一個(gè)棧先增長,后收縮),那么元素的index>=100的那些元素(被Pop掉的)都算是過期的元素,那些引用就是過期的引用(永遠(yuǎn)不會(huì)再被接觸的應(yīng)用)-來自Effective Java

public Object pop(){
      if(size==0) throw new EmptyStackException();
      Object result = elements[--size];
     elements[size] = null; //消除過期引用
     return result;
}

由于過期引用的存在,GC并不會(huì)去回收他們,我們需要手動(dòng)的釋放他們。

內(nèi)存溢出和內(nèi)存的查看方法

  • 使用第三方開源庫

LeakCanary
在這里就不做具體的介紹了。網(wǎng)上的使用demo:
leakCanary Demo

  • adb shell命令

通過以下命令可以查看你APP的內(nèi)存使用情況已經(jīng)Activity和View等的個(gè)數(shù)情況,具體的

adb shell dumpsys meminfo packagename

其中,packagename就是你程序的報(bào)名,具體的示例,如下圖所示:

dumpsysmeminfo.png

如上圖所示:
  上面部分顯示的是你的app所占用的內(nèi)存總數(shù)(主要是看TOTAL,內(nèi)存所實(shí)際占用的值)
  下面的部分可以看到你的一些對(duì)象的個(gè)數(shù):如Views、Activities等。
當(dāng)你進(jìn)入一個(gè)acitivity的時(shí)候,activity的個(gè)數(shù)會(huì)增加,退出后會(huì)減少,如果只增加、不減少,就說明出現(xiàn)了內(nèi)存泄露的問題。 (經(jīng)過實(shí)際的測(cè)試,這個(gè)對(duì)有些手機(jī),好不管用,就算我寫個(gè)demo:只有一個(gè)Activity,什么也沒有做,進(jìn)來、退出、進(jìn)來、activities個(gè)數(shù)會(huì)變大,不會(huì)立即變小,需要等一段時(shí)間才會(huì)變小)

  • DDMS

DDMS是Android開發(fā)環(huán)境中的Dalvik虛擬機(jī)(andoid4.4之前,4.4及其之后引入了ART虛擬機(jī))調(diào)試監(jiān)控服務(wù)。

1. update heap

ddms_update_heap.png

對(duì)一個(gè)activity進(jìn)入退出反復(fù)多次看data object是否穩(wěn)定在一個(gè)范圍
  2. MAT(Memory Analyzer Tool)

ddms_hprof.png

dump hprof file : 點(diǎn)擊后等待一會(huì),會(huì)生成一個(gè)hprof文件。插件版本的MAT可以直接打開該文件,否則需要進(jìn)行一步轉(zhuǎn)換操作。 提供了這個(gè)工具 hprof-conv (位于 sdk/tools下), 轉(zhuǎn)換命令如下所示:

./hprof-conv xxx-a.hprof xxx-b.hprof 

最后通過DDMS-File-open,打開的hprof文件即可進(jìn)行分析內(nèi)存泄露相關(guān)。

內(nèi)存優(yōu)化建議

  • 了解你機(jī)器的內(nèi)存情況

通過以下代碼可以查看每個(gè)進(jìn)程可用的最大內(nèi)存,即heapgrowthlimit值

ActivityManager actManager = getApplicationContext.getSystemService(Context.ACTIVITY_SERVICE);s int memClass = actManager.getMemeoryClass(); //以M為單位

通過以下代碼可以獲取 應(yīng)用程序的最大可用內(nèi)存

long maxMemory = Runtime.getRuntime().maxMemeory(); //以字節(jié)為單位

兩者的區(qū)別:
  單位不一致 前者以M為單位,后者以字節(jié)為單位。
  具體的以lenovo的一款手機(jī)(S850T, Android版本為4.4.2)為例: 經(jīng)過測(cè)試兩者得到的值一致均是128M。

使用場景
  當(dāng)你進(jìn)行圖片加載的時(shí)候,都會(huì)使用到LRUCache,初始化的時(shí)候設(shè)置緩存的大小。一般來說都設(shè)置為當(dāng)前最大內(nèi)存的1/8,如果你就是一個(gè)圖片應(yīng)用你直接1/4也可以。

long cacheSize = Runtime.getRuntime().maxMemeory();
mLruCache = new LruCache<String, Bitmap>(cacheSize)
        {
            @Override
            protected int sizeOf(String key, Bitmap value)
            {
                return value.getRowBytes() * value.getHeight();
            };
        };
  • 當(dāng)界面不可見、內(nèi)存緊張的時(shí)候釋放內(nèi)存

android4.0(包含4.0)之后引入了onTrimMemory(int level)(4.0之前為onLowMemory) ,系統(tǒng)會(huì)根據(jù)不同的內(nèi)存狀態(tài)來毀掉,參數(shù) level 代表了你app的不同狀態(tài),Application、Activity、Fragment、Service、ContentProvider均可以響應(yīng)。具體如下:

TRIM_MEMORY_UI_HIDDEN: 應(yīng)用程序被隱藏了,如按了Home或者Back導(dǎo)致UI不可見,這個(gè)時(shí)候,我們應(yīng)該釋放一些內(nèi)存。

以下三個(gè)是我們的應(yīng)用程序真正運(yùn)行時(shí)的回調(diào):
  TRIM_MEMORY_RUNNING_MODERATE: 程序正常運(yùn)行,并不會(huì)被殺掉,但是手機(jī)的內(nèi)存有點(diǎn)低了,系統(tǒng)可能開始根據(jù)LRU規(guī)則來殺死進(jìn)程了。
  TRIM_MEMORY_RUNNING_LOW: 程序正常運(yùn)行,并不會(huì)被殺掉,但是手機(jī)內(nèi)存非常的低了,應(yīng)該釋放一些資源了,否則影響性能。
  TRIM_MEMORY_RUNNING_CRITICAL: 程序正在運(yùn)行,但是系統(tǒng)已經(jīng)根據(jù)LRU殺死了大部分緩存的進(jìn)程了,此時(shí)我們需要釋放內(nèi)存,否則系統(tǒng)可能會(huì)干掉你。

以下三個(gè)是當(dāng)應(yīng)用程序是緩存時(shí)候的回調(diào):
  TRIM_MEMORY_BACKGROUND: 內(nèi)存不足,并且該進(jìn)程是后臺(tái)進(jìn)程。
  TRIM_MEMORY_MODERATE: 內(nèi)存不足,并且該進(jìn)程在后臺(tái)進(jìn)程列表的中部。
  TRIM_MEMORY_COMPLETE:內(nèi)存不足,并且該進(jìn)程在后臺(tái)進(jìn)程列表的最后一個(gè),馬上就要被清理了,這個(gè)時(shí)候應(yīng)該把一切盡可能釋放的都釋放掉。

通常在我們開始進(jìn)行架構(gòu)設(shè)計(jì)的時(shí)候,就要考慮到哪些東西是要常駐的,哪些東西是緩存后要被清理, 一般情況下,以下資源都要被清理:
緩存:包括文件緩存、圖片的緩存、比如第三方圖片緩存庫。
一些動(dòng)態(tài)生成的View: 比如一般應(yīng)用的圖片輪播View,在你的應(yīng)用隱藏后,根本不需要輪播。

案例分析:
  1. LRUCache緩存的清理方式:trimToSize()接口可以重新設(shè)置緩存的大小。evictAll()接口可以清楚所有的LRUCache緩存內(nèi)容。
  2. 暴力清理界面中的View

  • 圖片資源的壓縮
      1. res中資源到壓縮: 使用有損壓縮工具,比如:tinyPng,壓縮后的圖片肉眼根本看不出來,壓縮率可以達(dá)到50%以上。
      2. BitmapFactory的壓縮。
      通過BitmapFactory的Options設(shè)置,降低采樣率,壓縮圖片到適合的大小,同時(shí)注意使用若引用和緩存機(jī)制。
      Bitmap.Config設(shè)置圖片的格式為RGB565,這個(gè)設(shè)置肉眼是看不出色彩的丟失,而且比RGB8888占存小的多。
      使用BitmapFactory.Options.inBitmap字段。如果這個(gè)選項(xiàng)被設(shè)置,那么使用該Options 的decode方法將會(huì)嘗試復(fù)用一個(gè)已經(jīng)存在的bitmap來加載新的bitmap。這意味著bitmap的內(nèi)存將被復(fù)用,避免分配和釋放內(nèi)存來提升性能。然后,使用inBitmap有一些限制。特別是在Android4.4(API level19)之前,只有尺寸相同的bitmap才能使用該特性。具體的見使用示例
      3. 將圖片資源放在合適的drawable目錄下。
  • 使用Android優(yōu)化過的類和集合
      1. SparseArrry<T>來替代HashMap<int, T>
      2. LongSparseArray<T>, key為long,替代HashMap<long, T>
      3. SimpleArrayMap<K, T>和ArrayMap<K,T>替代HashMap<K, T>, ArrayMap是通過時(shí)間來換取效率,在數(shù)千之內(nèi)建議使用ArrayMap。
  • 避免創(chuàng)建不必要的對(duì)象

在短時(shí)間內(nèi)創(chuàng)建了大量的對(duì)象,然后有釋放,這樣就引起了內(nèi)存抖動(dòng)。頻繁的引起GC操作,會(huì)導(dǎo)致內(nèi)存的卡頓。
  1. 字符串的拼接:StringBuffer(非線程安全)和StringBuilder(線程安全)的使用
  2. 自定義View中不要在onDraw中定義畫筆等對(duì)象
  3. 在循環(huán)函數(shù)內(nèi)避免創(chuàng)建重復(fù)的對(duì)象,將多個(gè)函數(shù)都經(jīng)常用到的不可變對(duì)象拿出來統(tǒng)一進(jìn)行初始化,在一開始寫的時(shí)候就要特別的注意,否則后邊修改起來很是麻煩(主要是再找到他很麻煩)
  4. 在循環(huán)的內(nèi)部不要使用try catch操作,將其拿到外面來。
  5. 不要在循環(huán)中進(jìn)行文件的操作:比如判斷文件是否存在,這相對(duì)是一個(gè)很耗時(shí)的操作

案例說明
  SimpleDateFromat是用來時(shí)間轉(zhuǎn)換的,一般的,開發(fā)者都會(huì)定義個(gè)專門用于時(shí)間轉(zhuǎn)化的static的函數(shù):

public static String paserTimeToYM(long time)
    {
        SimpleDateFormat format = new SimpleDateFormat("yyyy年MM月dd日", Locale.getDefault());
        return format.format(new Date(time));
    } 

假如你在for循環(huán)中調(diào)用此函數(shù)。就不停的重復(fù)創(chuàng)建SimpleDateFromat對(duì)象。你應(yīng)該將對(duì)象創(chuàng)建拿出來,放在類中,或者是重新定義一個(gè)時(shí)間轉(zhuǎn)換函數(shù),入慘為已經(jīng)創(chuàng)建好的SimpleDateFormat對(duì)象。
  還需要注意的是:假如你的循環(huán)量很大,不建議在for循環(huán)中進(jìn)行時(shí)間轉(zhuǎn)換,而是在你用到的時(shí)候才進(jìn)行轉(zhuǎn)換,比如顯示出來。

  • 不要擴(kuò)大變量的作用域
classs A
{
    private B mB;
    public A(B b) {
        this.mB = b;
        //就在構(gòu)造函數(shù)中進(jìn)行了對(duì)mB進(jìn)行了一些操作
    }
    //后續(xù)再也沒有用到過mB
}

class B
{
    public B() {
    }
    public static void main(String[] args)
    {
    }
}

如上所示的簡單代碼:類A的構(gòu)造函數(shù)中,傳入了類B的對(duì)象,并且類A中定義了成員變量mB,但是mB就在構(gòu)造函數(shù)中用了一下,后續(xù)再也沒有用,在類A中mB的生命周期和A一致。本來mB的作用域就在構(gòu)造函數(shù),結(jié)果擴(kuò)大為整個(gè)類。

  • 不要讓生命周期比Activity長的對(duì)象持有Activity的引用

這樣的錯(cuò)誤很多,比如:將Activity的Context傳給單例模式,毫不知情的將Activity的Context傳給非靜態(tài)內(nèi)部類或者是匿名內(nèi)部類。

  • 盡量的使用Application的Context

Application的生命周期是整個(gè)app,他會(huì)一直在。
  1. 在界面類中直接使用getApplicationContext。
  2. 在其他地方使用MyApplication(extends Application)的getInstance操作。如下所示:

public class MyApplication extends Application
{
    private static Context sContext;
 
    @Override
    public void onCreate()
    {
        Log.d(tag, "onCreate");
        sContext = this;
    }

    public static Context getAppContext()
    {
        return sContext;
    }
}

總之一句話:能使用Application的Context,就不要使用Activity的。

  • 移除回調(diào)
      1. handler的removeCallbacksAndMessages(null)
      2. setXXXCallback(null)、 setXXXListener(null),需要注意的是,要進(jìn)行callback調(diào)用的地方就需要進(jìn)行判斷了
  • 常量的使用

關(guān)于enum和static。Android強(qiáng)烈建議不要使用enum,他會(huì)使得內(nèi)存消耗變大為原來的2倍以上。

  • 使用代碼混淆剔除不需要的代碼

  • 請(qǐng)使用靜態(tài)內(nèi)部類+WeakReference的方式

非靜態(tài)內(nèi)部類和匿名內(nèi)部類會(huì)持有頁面的應(yīng)用,請(qǐng)使用靜態(tài)內(nèi)部類,并將頁面的引用通過WeakReference的方式傳遞過去。

  • 合理的使用多進(jìn)程

android對(duì)單個(gè)進(jìn)程都有一個(gè)內(nèi)存允許的最大內(nèi)存限制。加入你在你的app中又啟動(dòng)一個(gè)進(jìn)程,這樣你的內(nèi)存限制就變?yōu)榱嗽瓉淼?倍。
  啟動(dòng)多進(jìn)程的方法很簡單,只需要在AndroidManifest.xml聲明的四大組件的標(biāo)簽中增加"android:process"屬性即可。
  進(jìn)程分為兩種:私有進(jìn)程和全局進(jìn)程。私有進(jìn)程在名稱簽名添加冒號(hào)即可。
  但是多進(jìn)程有一些需要注意的地方:
  1. Application的onCreate會(huì)被調(diào)用多次。一般程序會(huì)將程序的一些初始化的操作放在這里,這點(diǎn)需要注意。
  2. 多進(jìn)程之間的通訊必須使用AIDL接口,需要注意的一點(diǎn)是:AIDL之間傳遞大量數(shù)據(jù)是有一個(gè)限制的。 傳遞內(nèi)容過大會(huì)出現(xiàn):TransactionToolLargeException。官方文檔說明:最大的限制為1M。
  3. 多進(jìn)程導(dǎo)致 靜態(tài)成員、單例模式和SharedPreference 都變的不可靠。
  4. 多進(jìn)程之間傳遞數(shù)據(jù)的效率:有些手機(jī)在傳遞大量數(shù)據(jù)的時(shí)候,效率很差。
  5. 多進(jìn)程傳遞對(duì)象需要實(shí)現(xiàn)序列化操作。
  6. AIDL支持的數(shù)據(jù)類型:基本數(shù)據(jù)類型;String和CharSequence;List僅僅支持ArrayList,里面的每一個(gè)對(duì)象都必須支持序列化,Map只支持HashMap,里面的key和value都必須支持序列化(必須被AIDL支持)。
  7. AIDL服務(wù)端可以使用CopyOnWriteArrayList和ConcurrentHashMap來進(jìn)行自動(dòng)線程同步,客戶端拿到的依然是ArrayList和HashMap。
  8.AIDL服務(wù)端和客戶端之間做監(jiān)聽器,服務(wù)端需要使用RemoteCallbackList,否則客戶端的監(jiān)聽器無法收到通知(因?yàn)榉?wù)端實(shí)質(zhì)還是一份新的序列化后的監(jiān)聽器實(shí)例,并不是客戶端那份)。
  9.客戶端調(diào)用遠(yuǎn)程服務(wù)方法時(shí),因?yàn)檫h(yuǎn)程方法運(yùn)行在服務(wù)端的binder線程池中,同時(shí)客戶端線程會(huì)被掛起,所以如果該方法過于耗時(shí),而客戶端又是UI線程,會(huì)導(dǎo)致ANR,所以當(dāng)確認(rèn)該遠(yuǎn)程方法是耗時(shí)操作時(shí),應(yīng)避免客戶端在UI線程中調(diào)用該方法。同理,當(dāng)服務(wù)器調(diào)用客戶端的listener方法時(shí),該方法也運(yùn)行在客戶端的binder線程池中,所以如果該方法也是耗時(shí)操作,請(qǐng)確認(rèn)運(yùn)行在服務(wù)端的非UI線程中。另外,因?yàn)榭蛻舳说幕卣{(diào)listener運(yùn)行在binder線程池中,所以更新UI需要用到handler。

我們將在進(jìn)程常駐中進(jìn)行簡單的示例分析,實(shí)現(xiàn)多進(jìn)程的相互喚醒操作。

  • 請(qǐng)不要使用注解框架

程序注解框架極大的方便了程序開發(fā)者,不需要開發(fā)者大量的寫findViewById(), setOnclickListener()等方法,但是程序注解框架是將類中的所有相關(guān)方法都緩存在內(nèi)容中不會(huì)釋放,這些內(nèi)存就會(huì)越來越大,從而得不到釋放。而且一般程序注解方法都是用到了Java的反射機(jī)制。這個(gè)是不建議使用的(雖然有時(shí)候反射不得不使用)。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容