Android开发 - Context 类上下文的使用解析

Context是什么

  • Context的中文翻译为:语境; 上下文; 背景; 环境,在开发中我们经常说称之为“上下文”,那么这个“上下文”到底是指什么意思呢?在语文中,我们可以理解为语境,在程序中,我们可以理解为当前对象在程序中所处的一个环境,一个与系统交互的过程。比如微信聊天,此时的“环境”是指聊天的界面以及相关的数据请求与传输,Context在加载资源、启动Activity、获取系统服务、创建View等操作都要参与

  • Context到底是什么呢?一个Activity就是一个Context,一个Service也是一个ContextAndroid程序员把“场景”抽象为Context类,他们认为用户和操作系统的每一次交互都是一个场景,比如打电话、发短信,这些都是一个有界面的场景,还有一些没有界面的场景,比如后台运行的服务(Service)。一个应用程序可以认为是一个工作环境,用户在这个环境中会切换到不同的场景,这就像一个前台秘书,她可能需要接待客人,可能要打印文件,还可能要接听客户电话,而这些就称之为不同的场景,前台秘书可以称之为一个应用程序

  • 源码中的注释是这么来解释Context的:Context提供了关于应用环境全局信息的接口。它是一个抽象类,它的执行被Android系统所提供。它允许获取以应用为特征的资源和类型,是一个统领一些资源(应用程序环境变量等)的上下文。就是说,它描述一个应用程序环境的信息(即上下文);是一个抽象类Android提供了该抽象类的具体实现类通过它我们可以获取应用程序的资源和类包括应用级别操作如启动Activity发广播接受Intent等)。既然上面Context是一个抽象类,那么肯定有他的实现类咯,我们在Context的源码中通过IDE可以查看到他的子类最终可以得到如下关系图:

image-20240720094349605

  • Context类本身是一个纯abstract类,它有两个具体的实现子类:ContextImplContextWrapper。其中ContextWrapper类,如其名所言,这只是一个包装而已,ContextWrapper构造函数中必须包含一个真正的Context引用,同时ContextWrapper中提供了attachBaseContext()用于给ContextWrapper对象中指定真正的Context对象,调用ContextWrapper的方法都会被转向其所包含的真正的Context对象。ContextThemeWrapper类,如其名所言,其内部包含了与主题(Theme)相关的接口,这里所说的主题就是指在AndroidManifest.xml中通过android:themeApplication元素或者Activity元素指定的主题。当然,只有Activity才需要主题Service是不需要主题的,因为Service是没有界面的后台场景,所以Service直接继承ContextWrapperApplication同理而ContextImpl类则真正实现了Context中的所有函数应用程序中所调用的各种Context类的方法其实现均来自于该类一句话总结Context的两个子类分工明确其中ContextImpl是Context的具体实现类ContextWrapper是Context的包装类ActivityApplicationService虽都继承自ContextWrapperActivity继承自ContextWrapper的子类ContextThemeWrapper),但它们初始化的过程中都会创建ContextImpl对象由ContextImpl实现Context中的方法

Context能干什么

  • Context到底可以实现哪些功能呢?这个就实在是太多了:弹出Toast启动Activity启动Service发送广播操作数据库等等都需要用到Context,部分实例如下:

    TextView tv = new TextView(getContext());  
      
    ListAdapter adapter = new SimpleCursorAdapter(getApplicationContext(), …);  
      
    AudioManager am = (AudioManager)getContext().getSystemService(Context.AUDIO_SERVICE);
    getApplicationContext().getSharedPreferences(name, mode);  
      
    getApplicationContext().getContentResolver().query(uri, …);  
      
    getContext().getResources().getDisplayMetrics().widthPixels * 5 / 8;  
      
    getContext().startActivity(intent);  
      
    getContext().startService(intent);  
      
    getContext().sendBroadcast(intent); 
    

Context作用域

  • 虽然Context神通广大,但并不是随便拿到一个Context实例就可以为所欲为,它的使用还是有一些规则限制的。由于Context的具体实例是由ContextImpl类去实现的,因此在绝大多数场景下,ActivityServiceApplication这三种类型的Context都是可以通用的。不过有几种场景比较特殊比如启动Activity还有弹出Dialog出于安全原因的考虑Android是不允许Activity或Dialog凭空出现的一个Activity的启动必须要建立在另一个Activity的基础之上也就是以此形成的返回栈。而Dialog则必须在一个Activity上面弹出(除非是System Alert类型的Dialog),因此在这种场景下,我们只能使用Activity类型的Context,否则将会出错

  • 其中Activity所持有的Context的作用域最广,无所不能。因为Activity继承自ContextThemeWrapper,而ApplicationService继承自ContextWrapper,很显然ContextThemeWrapperContextWrapper的基础上又做了一些操作使得Activity变得更强大。以下说明Application和Service所不推荐的两种使用情况

    1. 如果我们用ApplicationContext去启动一个LaunchModestandardActivity的时候会报错android.util.AndroidRuntimeException: Calling startActivity from outside of an Activity context requires the FLAG_ACTIVITY_NEW_TASK flag. Is this really what you want?这是因为非Activity类型的Context并没有所谓的任务栈,所以待启动的Activity就找不到栈了。解决这个问题的方法就是为待启动的Activity指定FLAG_ACTIVITY_NEW_TASK标记位,这样启动的时候就为它创建一个新的任务栈,而此时Activity是以singleTask模式启动的。所有这种用Application启动Activity的方式不推荐使用ServiceApplication

    2. ApplicationService中去layout inflate也是合法的,但是会使用系统默认的主题样式如果你自定义了某些样式可能不会被使用。所以这种方式也不推荐使用

    • 一句话总结:凡是跟UI相关的,都应该使用Activity做为Context来处理;其他的一些操作,ServiceActivityApplication等实例都可以,当然了,注意Context引用的持有防止内存泄漏

如何获取Context

获取Context对象

  • 主要提供以下2种方法

    1. getView().getContext():返回当前View对象的Context对象,通常是当前正在展示的Activity对象

    2. getActivity().getApplicationContext():获取当前Activity所在的(应用)进程的Context对象,通常我们使用Context对象时,要优先考虑这个全局的进程Context

getApplication()getApplicationContext()的区别

  • Application本身就是一个Context,所以这里获取getApplicationContext()得到的结果就是Application本身的实例。那么问题来了,既然这两个方法得到的结果都是相同的,那么Android为什么要提供两个功能重复的方法呢?实际上这两个方法在作用域上有比较大的区别getApplication()方法的语义性非常强,一看就知道是用来获取Application实例的,但是这个方法只有在ActivityService中才能调用的到。那么也许在绝大多数情况下我们都是在Activity或者Service中使用Application的,但是如果在一些其它的场景,比如BroadcastReceiver中也想获得Application的实例,这时就可以借助getApplicationContext()方法了,下面提供两种获取方法:

    public classMyReceiver extends BroadcastReceiver{  
        @Override  
        public void onReceive(Context context,Intent intent){  
            //第一种方法
            Application myApp = (Application)context.getApplicationContext();	
            ---
            //第二种方法
            context = getActivity().getApplicationContext();
            Application myApp = (Application)context; 
        }  
    } 
    

Context引起的内存泄露

  • 但Context并不能随便乱用,用的不好有可能会引起内存泄露的问题,下面就示例两种错误的引用方式

    1. 错误的单例模式

      public class Singleton {      
          private static Singleton instance;      
          private Context mContext;    
              
          private Singleton(Context context) {          
              this.mContext = context;  
          }      
                
          public static Singleton getInstance(Context context) {         
              if (instance == null) {  
                   instance = new Singleton(context);  
              }         
              return instance;  
          }  
      }  
      
      • 这是一个非线程安全的单例模式instance作为静态对象,其生命周期要长于普通的对象,其中也包含Activity,假如Activity AgetInstance获得instance对象,传入this,常驻内存的Singleton保存了你传入的Activity A对象,并一直持有,即使Activity被销毁掉,但因为它的引用还存在于一个Singleton中,就不可能被GC(咔擦)掉,这样就导致了内存泄漏
    2. 错误的View持有Activity引用

      public class MainActivity extends Activity {  
          private static Drawable mDrawable;   
           
          @Override  
          protected void onCreate(Bundle saveInstanceState) {          
              super.onCreate(saveInstanceState);  
              setContentView(R.layout.activity_main);          
              ImageView iv = new ImageView(this);  
              mDrawable = getResources().getDrawable(R.drawable.ic_launcher);  
              iv.setImageDrawable(mDrawable);
          }  
      }  
      
      • 有一个静态的Drawable对象当ImageView设置这个Drawable时,ImageView保存了mDrawable的引用,而ImageView传入的thisMainActivitymContext,因为被static修饰的mDrawable是常驻内存的,MainActivity是它的间接引用,MainActivity被销毁时,也不能被GC(咔擦)掉,所以造成内存泄漏

正确使用Context

  • 一般Context造成的内存泄漏,几乎都是当Context销毁的时候,却因为被引用导致销毁失败,而ApplicationContext对象可以理解为随着进程存在的,所以我们总结出使用Context的正确姿势:
    1. ApplicationContext能搞定的情况下,并且生命周期长的对象,优先使用ApplicationContext
    2. 不要让生命周期长于Activity的对象持有到Activity的引用
    3. 尽量不要在Activity中使用非静态内部类因为非静态内部类会隐式持有外部类实例的引用,如果使用静态内部类,外部实例将引用作为弱引用持有

总结

  • Context在Android系统中的地位很重要,它几乎无所不能,但它也不是你想用就能随便用的,谨防使用不当引起的内存问题
posted @ 2024-07-23 08:58  阿俊学JAVA  阅读(9)  评论(0编辑  收藏  举报