Java 内部类原理解析

本文深入探讨Java内部类的四种类型:静态、成员、局部和匿名内部类,详细解析它们的工作原理,尤其是静态内部类和成员内部类如何可能导致内存泄漏。通过实例分析,揭示Handler和网络请求回调中内部类造成内存泄漏的情况,并提供避免内存泄漏的建议,如使用静态内部类和弱引用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

       Java 内部类在代码中是非常常见的,但是在Android系统里,常常会因为内部类的使用导致内存泄漏的问题,所以本文对内部类从原理上做一个比较详细的解析以及如何在使用内部类时该如何避免内存泄漏

内部类的种类


Java的内部类分为四种:静态内部类、成员内部类、局部内部类和匿名内部类;

静态内部类

静态内部类,顾名思义是一个静态的类,代码如下

public class MainActivity extends AppCompatActivity {
    //...省略部分代码...
   //静态内部类
    static class StaticInnerClass implements View.OnClickListener{

        Activity mActivity;
        StaticInnerClass(Activity activity) {
            mActivity = activity;
        }

        @Override
        public void onClick(View view) {
            Toast.makeText(mActivity, "这是静态内部类", Toast.LENGTH_SHORT).show();
        }
    }

    private void setValue(int value) {
        this.value = value;
    }
}

如上面的代码,静态内部类不能够直接使用外部类,因此需要用到外部类的时候,需要将外部类注入进来,如上面的代码是通过构造函数注入了MainActivity,但是上面的代码会造成内存泄漏,因为持有了MainActivity,导致MainActivity在需要被回收的时候因为被引用而无法被回收。

成员内部类

成员内部类平时用的较多,但其往往是造成内存泄漏的原因之一

public class MainActivity extends AppCompatActivity {
  //成员内部类
    class MemberInnerClass implements View.OnClickListener{

        @Override
        public void onClick(View view) {
            Toast.makeText(MainActivity.this, "这是成员内部类"+ value, Toast.LENGTH_SHORT).show();
            setValue(value ++);
        }
    }

    private void setValue(int value) {
        this.value = value;
    }
}

上面的代码跟静态内部类的代码区别在于没有在构造函数中注入外部类MAinActivity,而能直接调用外部类的函数setValue()。但是由于内部类没有显示地依赖注入外部类,往往让人忽略成员内部类会瘾式地依赖外部类MainActivity,使成员内部类成为了内存泄漏的常见原因之一。至于成员内部类是如何依赖于外部类的,接下来会详细介绍。

局部内部类

    private void innerClass() {
        //局部内部类
        View.OnClickListener innerClass = new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Toast.makeText(MainActivity.this, "这是局部内部类" + value, Toast.LENGTH_SHORT).show();
                setValue(value ++);
            }
        };

        textView1.setOnClickListener(innerClass);

    }

局部内部类,在该定义的函数内部可以访问,这也是我们实现view的click事件的常用方式,但是有可能也会造成内存泄漏,至于原因接下来也会分析。

匿名内部类

匿名内部类是我们设置回调函数最常用的方式,如view的click事件以及其他的callback等等一系列回调,也许你会说这不会造成内存泄漏了吧,因为我们都这么用,也没见造成内存泄漏。但是我会告诉你,它也是会造成内存泄漏的。

    private void innerClass() {
        //匿名内部类
        textView2.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Toast.makeText(MainActivity.this, "这是匿名内部类"  + value, Toast.LENGTH_SHORT).show();
                setValue(value ++);
            }
        });
 }

这就是我们实现view事件监听的常用方式,确实,上面的代码不会造成内存泄漏,但是这是一个网络回调或者是耗时操作的回调,在回调之前,MainActivity是不能被回收到,同样会造成内存泄漏,因为它也会瘾式地依赖外部类MainActivity.

内部类原理解析


接下来分析内部类的原理,先贴出Java代码,以便前后做对比

public class MainActivity extends AppCompatActivity {
    /...省略若干.../
    @Override
    protected void onCreate(Bundle savedInstanceState) {
   /...省略若干.../
        innerClass();
    }
    private void innerClass() {
        //局部内部类
        View.OnClickListener innerClass = new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Toast.makeText(MainActivity.this, "这是局部内部类" + value, Toast.LENGTH_SHORT).show();
                setValue(value ++);
            }
        };
        textView1.setOnClickListener(innerClass);
        //匿名内部类
        textView2.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Toast.makeText(MainActivity.this, "这是匿名内部类"  + value, Toast.LENGTH_SHORT).show();
                setValue(value ++);
            }
        });
        textView3.setOnClickListener(new MemberInnerClass());
        textView4.setOnClickListener(new StaticInnerClass(this));
    }

    private void setValue(int value) {
        this.value = value;
    }

    //成员内部类
    class MemberInnerClass implements View.OnClickListener{

        @Override
        public void onClick(View view) {
            Toast.makeText(MainActivity.this, "这是成员内部类"+ value, Toast.LENGTH_SHORT).show();
            setValue(value ++);
        }
    }

    //静态内部类
    static class StaticInnerClass implements View.OnClickListener{

        Activity mActivity;

        StaticInnerClass(Activity activity) {
            mActivity = activity;
        }

        @Override
        public void onClick(View view) {
            Toast.makeText(mActivity, "这是静态内部类", Toast.LENGTH_SHORT).show();
        }
    }
}

分析内部类的原理,还是老套路,看看编译后的smali是怎么样的呢?

反编译apk后,里面包含有如下文件

这里写图片描述

其中有四个smali文件是以MainActivity开头命名的,没错,也许你猜对了,这是个文件smali文件分别对应上面的四个内部类,其中MainActivity$MemberInnerClass对应的是MemberInnerClass,MainActivity$StaticInnerClass对应的是StaticInnerClass,另外MainActivity$1MainActivity$2 分别对应的是局部内部类与匿名内部类。
所有的内部类都被编译成了单独的smali,也就是说此时内部类变成了顶级类,和上面的外部类是一个级别的,那么此时内部类是怎么与外部类通信的。来分析具体的smali代码

静态内部类 MainActivity$StaticInnerClass

.class Lcom/example/forone/innerclassdemo/MainActivity$StaticInnerClass;
.super Ljava/lang/Object;
.source "MainActivity.java"

# interfaces
.implements Landroid/view/View$OnClickListener;


# annotations
.annotation system Ldalvik/annotation/EnclosingClass;
    value = Lcom/example/forone/innerclassdemo/MainActivity;
.end annotation

.annotation system Ldalvik/annotation/InnerClass;
    accessFlags = 0x8
    name = "StaticInnerClass"
.end annotation


# instance fields
.field mActivity:Landroid/app/Activity;


# direct methods
.method constructor <init>(Landroid/app/Activity;)V
    .locals 0
    .param p1, "activity"    # Landroid/app/Activity;

    .prologue
    .line 72
    invoke-direct {p0}, Ljava/lang/Object;-><init>()V

    .line 73
    iput-object p1, p0, Lcom/example/forone/innerclassdemo/MainActivity$StaticInnerClass;->mActivity:Landroid/app/Activity;

    .line 74
    return-void
.end method


# virtual methods
.method public onClick(Landroid/view/View;)V
    .locals 3
    .param p1, "view"    # Landroid/view/View;

    .prologue
    .line 78
    iget-object v0, p0, Lcom/example/forone/innerclassdemo/MainActivity$StaticInnerClass;->mActivity:Landroid/app/Activity;

    const-string v1, "\u8fd9\u662f\u9759\u6001\u5185\u90e8\u7c7b"

    const/4 v2, 0x0

    invoke-static {v0, v1, v2}, Landroid/widget/Toast;->makeText(Landroid/content/Context;Ljava/lang/CharSequence;I)Landroid/widget/Toast;

    move-result-object v0

    invoke-virtual {v0}, Landroid/widget/Toast;->show()V

    .line 79
    return-void
.end method

因为StaticInnerActivity的构造函数显式注入了MainActivity,所以通过引用MainActivity直接调用即可

# direct methods
.method constructor <init>(Landroid/app/Activity;)V
    .locals 0
    .param p1, "activity"    # Landroid/app/Activity;

    .prologue
    .line 72
    invoke-direct {p0}, Ljava/lang/Object;-><init>()V

    .line 73
    iput-object p1, p0, Lcom/example/forone/innerclassdemo/MainActivity$StaticInnerClass;->mActivity:Landroid/app/Activity;

    .line 74
    return-void
.end method


成员内部类 MainActivity$MemberInnerActivity

静态内部类显式地注入了MainActivity,但是成员内部类没有显式地注入内部类,那么成员内部类是如何调用外部类的私有函数setValue()的呢

.class Lcom/example/forone/innerclassdemo/MainActivity$MemberInnerClass;
.super Ljava/lang/Object;
.source "MainActivity.java"

# interfaces
.implements Landroid/view/View$OnClickListener;


# annotations
.annotation system Ldalvik/annotation/EnclosingClass;
    value = Lcom/example/forone/innerclassdemo/MainActivity;
.end annotation

.annotation system Ldalvik/annotation/InnerClass;
    accessFlags = 0x0
    name = "MemberInnerClass"
.end annotation


# instance fields
.field final synthetic this$0:Lcom/example/forone/innerclassdemo/MainActivity;


# direct methods
.method constructor <init>(Lcom/example/forone/innerclassdemo/MainActivity;)V
    .locals 0
    .param p1, "this$0"    # Lcom/example/forone/innerclassdemo/MainActivity;

    .prologue
    .line 58
    iput-object p1, p0, Lcom/example/forone/innerclassdemo/MainActivity$MemberInnerClass;->this$0:Lcom/example/forone/innerclassdemo/MainActivity;

    invoke-direct {p0}, Ljava/lang/Object;-><init>()V

    return-void
.end method


# virtual methods
.method public onClick(Landroid/view/View;)V
    .locals 3
    .param p1, "view"    # Landroid/view/View;

    .prologue
    .line 62
    iget-object v0, p0, Lcom/example/forone/innerclassdemo/MainActivity$MemberInnerClass;->this$0:Lcom/example/forone/innerclassdemo/MainActivity;

    new-instance v1, Ljava/lang/StringBuilder;

    invoke-direct {v1}, Ljava/lang/StringBuilder;-><init>()V

    const-string v2, "\u8fd9\u662f\u6210\u5458\u5185\u90e8\u7c7b"

    invoke-virtual {v1, v2}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;

    move-result-object v1

    iget-object v2, p0, Lcom/example/forone/innerclassdemo/MainActivity$MemberInnerClass;->this$0:Lcom/example/forone/innerclassdemo/MainActivity;

    # getter for: Lcom/example/forone/innerclassdemo/MainActivity;->value:I
    invoke-static {v2}, Lcom/example/forone/innerclassdemo/MainActivity;->access$000(Lcom/example/forone/innerclassdemo/MainActivity;)I

    move-result v2

    invoke-virtual {v1, v2}, Ljava/lang/StringBuilder;->append(I)Ljava/lang/StringBuilder;

    move-result-object v1

    invoke-virtual {v1}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;

    move-result-object v1

    const/4 v2, 0x0

    invoke-static {v0, v1, v2}, Landroid/widget/Toast;->makeText(Landroid/content/Context;Ljava/lang/CharSequence;I)Landroid/widget/Toast;

    move-result-object v0

    invoke-virtual {v0}, Landroid/widget/Toast;->show()V

    .line 63
    iget-object v0, p0, Lcom/example/forone/innerclassdemo/MainActivity$MemberInnerClass;->this$0:Lcom/example/forone/innerclassdemo/MainActivity;

    iget-object v1, p0, Lcom/example/forone/innerclassdemo/MainActivity$MemberInnerClass;->this$0:Lcom/example/forone/innerclassdemo/MainActivity;

    # operator++ for: Lcom/example/forone/innerclassdemo/MainActivity;->value:I
    invoke-static {v1}, Lcom/example/forone/innerclassdemo/MainActivity;->access$008(Lcom/example/forone/innerclassdemo/MainActivity;)I

    move-result v1

    # invokes: Lcom/example/forone/innerclassdemo/MainActivity;->setValue(I)V
    invoke-static {v0, v1}, Lcom/example/forone/innerclassdemo/MainActivity;->access$100(Lcom/example/forone/innerclassdemo/MainActivity;I)V

    .line 64
    return-void
.end method

smali代码太长,挑重点看

# direct methods
.method constructor <init>(Lcom/example/forone/innerclassdemo/MainActivity;)V
    .locals 0
    .param p1, "this$0"    # Lcom/example/forone/innerclassdemo/MainActivity;

    .prologue
    .line 58
    iput-object p1, p0, Lcom/example/forone/innerclassdemo/MainActivity$MemberInnerClass;->this$0:Lcom/example/forone/innerclassdemo/MainActivity;

    invoke-direct {p0}, Ljava/lang/Object;-><init>()V
    return-void
.end method

成员内部类的构造函数同样注入了外部类MainActivity,但是MainActivity中的setValue()函数是private类型,value也是private,即使是有外部类MainActivity的引用,也应该无法访问private函数和字段,那成员内部类是怎么做到的呢?继续看代码

# virtual methods
.method public onClick(Landroid/view/View;)V
    .locals 3
    .param p1, "view"    # Landroid/view/View;

    .prologue
    .line 62
    iget-object v0, p0, Lcom/example/forone/innerclassdemo/MainActivity$MemberInnerClass;->this$0:Lcom/example/forone/innerclassdemo/MainActivity;

    new-instance v1, Ljava/lang/StringBuilder;

    invoke-direct {v1}, Ljava/lang/StringBuilder;-><init>()V

    const-string v2, "\u8fd9\u662f\u6210\u5458\u5185\u90e8\u7c7b"

    invoke-virtual {v1, v2}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;

    move-result-object v1

    iget-object v2, p0, Lcom/example/forone/innerclassdemo/MainActivity$MemberInnerClass;->this$0:Lcom/example/forone/innerclassdemo/MainActivity;

    # getter for: Lcom/example/forone/innerclassdemo/MainActivity;->value:I
    invoke-static {v2}, Lcom/example/forone/innerclassdemo/MainActivity;->access$000(Lcom/example/forone/innerclassdemo/MainActivity;)I

    move-result v2

    invoke-virtual {v1, v2}, Ljava/lang/StringBuilder;->append(I)Ljava/lang/StringBuilder;

    move-result-object v1

    invoke-virtual {v1}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;

    move-result-object v1

    const/4 v2, 0x0

    invoke-static {v0, v1, v2}, Landroid/widget/Toast;->makeText(Landroid/content/Context;Ljava/lang/CharSequence;I)Landroid/widget/Toast;

    move-result-object v0

    invoke-virtual {v0}, Landroid/widget/Toast;->show()V

    .line 63
    iget-object v0, p0, Lcom/example/forone/innerclassdemo/MainActivity$MemberInnerClass;->this$0:Lcom/example/forone/innerclassdemo/MainActivity;

    iget-object v1, p0, Lcom/example/forone/innerclassdemo/MainActivity$MemberInnerClass;->this$0:Lcom/example/forone/innerclassdemo/MainActivity;

    # operator++ for: Lcom/example/forone/innerclassdemo/MainActivity;->value:I
    invoke-static {v1}, Lcom/example/forone/innerclassdemo/MainActivity;->access$008(Lcom/example/forone/innerclassdemo/MainActivity;)I

    move-result v1
    # invokes: Lcom/example/forone/innerclassdemo/MainActivity;->setValue(I)V
    invoke-static {v0, v1}, Lcom/example/forone/innerclassdemo/MainActivity;->access$100(Lcom/example/forone/innerclassdemo/MainActivity;I)V

    .line 64
    return-void
.end method

看上面的代码,并没有直接调用外部类MainActivity的setVaule()函数,而是调用了MainActivity中的一个静态方法access$008(),外部类MainActivity中没有定义这个方法,那么这个静态方法哪里来的呢?看看MainActivity的代码,确实有一个access$008()的函数,同样访问字段value,也调用一个access$000() 的函数

.method static synthetic access$000(Lcom/example/forone/innerclassdemo/MainActivity;)I
    .locals 1
    .param p0, "x0"    # Lcom/example/forone/innerclassdemo/MainActivity;

    .prologue
    .line 10
    iget v0, p0, Lcom/example/forone/innerclassdemo/MainActivity;->value:I

    return v0
.end method

.method static synthetic access$008(Lcom/example/forone/innerclassdemo/MainActivity;)I
    .locals 2
    .param p0, "x0"    # Lcom/example/forone/innerclassdemo/MainActivity;

    .prologue
    .line 10
    iget v0, p0, Lcom/example/forone/innerclassdemo/MainActivity;->value:I

    add-int/lit8 v1, v0, 0x1

    iput v1, p0, Lcom/example/forone/innerclassdemo/MainActivity;->value:I

    return v0
.end method

access$008() 中,调用了setValue()函数,可以得知原来内部类不是直接调用外部类的函数,而是在编译时,生成了access$008() ,内部类通过它简间接调用外部类的私有函数。而访问private字段value也是间接调用access$000()进行的。

局部内部类与匿名内部类 MainActivity$数字

通过比较MainActivity$1MainActivity$2 的代码,基本上是一致的,编译后没有区别,那么局部内部类是不是也跟成员内部类一样,是隐式地在构造函数中注入了外部类MainActivity呢?

# direct methods
.method constructor <init>(Lcom/example/forone/innerclassdemo/MainActivity;)V
    .locals 0
    .param p1, "this$0"    # Lcom/example/forone/innerclassdemo/MainActivity;

    .prologue
    .line 32
    iput-object p1, p0, Lcom/example/forone/innerclassdemo/MainActivity$1;->this$0:Lcom/example/forone/innerclassdemo/MainActivity;

    invoke-direct {p0}, Ljava/lang/Object;-><init>()V

    return-void
.end method

看代码,确实,实现方式跟成员内部类一样,在构造函数中隐式地注入了外部类MainActivity,那么访问外部类MainActivity的私有函数和私有字段是不是也跟成员内部类一样呢?

      # getter for: Lcom/example/forone/innerclassdemo/MainActivity;->value:I
    invoke-static {v2}, Lcom/example/forone/innerclassdemo/MainActivity;->access$000(Lcom/example/forone/innerclassdemo/MainActivity;)I
    move-result v2

    /.../

  # operator++ for: Lcom/example/forone/innerclassdemo/MainActivity;->value:I
    invoke-static {v1}, Lcom/example/forone/innerclassdemo/MainActivity;->access$008(Lcom/example/forone/innerclassdemo/MainActivity;)I
    move-result v1

从上面的代码发现,确确实实也是跟成员内部类一样,是通过access$000()access$008()函数间接地访问外部类MainActivity的私有函数setValue()和私有字段value的。

同理,外部类访问成员内部类的私有方法和私有字段,也是通过在成员内部类中生成对应的access$XXX()的静态方法,外部类通过调用access$XXX()间接访问内部类的私有方法和私有字段。

使用内部类的常用情况分析


Handler的使用

在Android里面,使用比较多的内部类应该要数Handler的使用了,了解handler详情点击Handler解析(二):消息post与sendMessage机制,下面看一个Handler具体的例子

 private Handler handler = new Handler(){

        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            //逻辑处理
        }
    };

    private void sendMessage() {
        Message message = Message.obtain();
        message.what = 1;
        message.arg1 = 1;
        handler.sendMessage(message);
    }

根据上面的分析,Handler为一个内部类,handler会隐式地依赖外部类MainActivity,当调用sendMessage的时候,生成message的target为引用handler,当message处于MessageQueue中没有被及时处理时,message引用handler,handler引用了MainActivity的,导致MainActivity无法被回收,导致内存泄漏。上面发送的即时处理的消息可能还不明显,若发送一个delay 20分钟的消息时,MainActivity在这20分钟都没法被回收!

 Message message = Message.obtain();
 message.what = 1;
 message.arg1 = 1;
 handler.sendMessageDelayed(message,20 * 60 * 1000)


网络请求Callback

下面是一段常见的网络请求代码,网络请求,通过callback回调结果。

public class MainActivity extends AppCompatActivity {
     RequestParams params = new RequestParams();
     params.setFullUrl(url);
     HttpClientProxy client = new HttpClientProxy(params);
     client.setCallback(new Callback() {
          @Override
          public void onError(Call call, Exception e) {
          }

          @Override
          public void onResponse(Object response, int id) {
               mTextView.setText(response.string());
          }
      });
  }

上面通过网络请求返回结果,设置mTextView,Callback为内部类,因此会隐式引用外部类MainActivity,在网络请求过程中,因为Callback会被网络请求模块引用,而Callback引用了MainActivity,导致在网络请求没有结束时,即Callback被网络请求模块引用的过程中,MainActivity没法被回收,导致内存泄漏。

还有例如磁盘文件的读取写入、数据库的读取写入等一些耗时的操作,使用内部类时,都可能因为内部类引用了外部类,导致外部类无法被及时回收,导致内存泄漏。

内部类避免内存泄漏的用法


通过上面各种内部类的分析,成员内部类、局部内部类、匿名内部类都会隐式地注入外部类,而静态内部类不会隐式地注入外部类,当需要使用外部类的属性以及函数时,是我们显式地注入外部类。因此使用内部类的时候,推荐使用静态内部类来避免内部类带来的内存泄漏。


当内部类不需要依赖外部类的时候,直接使用静态内部类即可

    //静态内部类
    static class StaticInnerClass {
        StaticInnerClass() {
        }
        public void doSomeThing() {
        }
    }

当内部类需要依赖外部类的时候,不可避免的需要注入外部类,但是我们可以通过弱引用来避免内存泄漏

 //静态内部类
    static class StaticInnerClass implements View.OnClickListener{

        WeakReference<Activity> mActivityRef;

        StaticInnerClass(Activity activity) {
            mActivityRef = new WeakReference<>(activity);
        }

        @Override
        public void onClick(View view) {
            Activity activity = mActivityRef.get();
            if(activity != null) {//判断activity是否被回收
            Toast.makeText(mActivityRef.get(), "这是静态内部类", Toast.LENGTH_SHORT).show();
            }
        }
    }

当在静态内部类的生命周期中,如外部类需要被回收时,因为静态内部类此时使用的是弱引用,因此能够被回收,所以在静态内部类使用外部类的时候,一定要判断外部类是否被回收,做空判断,否则引发NullPointerException,导致崩溃那就得不偿失了。

总结


  1. 内部类分为静态内部类、成员内部类、局部内部类、匿名内部类;

  2. 静态内部类不会隐式注入外部类,而成员内部类、局部内部类、匿名内部类会隐式注入外部类;

  3. 内部类都会被编译成单独的顶级类,与外部类一样;静态内部类与成员内部类编译后的命名为 外部类名$内部类名 ,而局部内部类与匿名内部类会被命名为 外部类$累计数字

  4. 静态内部类无法引用外部类的私有函数与私有属性,但成员内部类、局部内部类、匿名内部类可以使用外部类的私有函数与私有属性,每个被内部类使用的私有函数与私有属性会在编译时通过在外部类生成对应的access$XXX()(XXX为数字)的静态函数,内部类通过access$XXX()调用外部类的私有函数与私有属性;外部类调用内部类的私有方法和私有属性也是同样生成access$XXX() 来间接调用实现的;

  5. 在网络请求、IO读取、Handler等耗时或延时操作的使用时,内部类易造成内存泄漏,可以通过静态内部类和弱引用来解决。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值