美文网首页四大组件Android学习
Android Context 熟悉还是陌生?

Android Context 熟悉还是陌生?

作者: 叛逆的青春不回头 | 来源:发表于2017-04-06 19:10 被阅读0次

    一、什么是Context
    二、Context的创建时机和获取
      1. Context的创建时机
      2. Context的获取
    三、Application使用相关问题
      1. 什么时候初始化全局变量
      2. 自定义Application?
    四、Context引起的内存泄露


    Android应用都是使用Java语言来编写的,本质上也是一个对象,那么Activity可以new吗?一个Android程序和一个Java程序,他们最大的区别在哪里?划分界限又是什么呢?其实简单点分析,Android程序不像Java程序一样,随便创建一个类,写个main()方法就能跑了,Android应用模型是基于Activity、Service、BroadcastReceiver等组件的应用设计模式,组件的运行要有一个完整的Android工程环境,在这个环境下,这些组件并不是像一个普通的Java对象new一下就能创建实例的了,而是要有它们各自的上下文环境Context。可以这样讲,Context是维持Android程序中各组件能够正常工作的一个核心功能类。

    什么是Context

    一个Activity是一个Context,一个Service也是一个Context。在程序中,我们把可以把Context理解为当前对象在程序中所处的一个环境,一个与系统交互的过程。用户和操作系统的每一次交互都是一个场景,比如微信聊天,此时的“环境”是指聊天的界面以及相关的数据请求与传输,Context在加载资源、启动Activity、获取系统服务、创建View等操作都要参与。打电话、发短信,这些都是一个有界面的场景,还有一些没有界面的场景,比如后台运行的服务(Service)。一个应用程序可以认为是一个工作环境,用户在这个环境中会切换到不同的场景,这就像一个前台秘书,她可能需要接待客人,可能要打印文件,还可能要接听客户电话,而这些就称之为不同的场景,前台秘书可以称之为一个应用程序。下面我们来看一下Context的继承结构:


    Context类,一个纯Abstract类,有ContextImpl和ContextWrapper两个实现类:

    • ContextWrapper包装类
      其构造函数中必须包含一个真正的Context引用。ContextWrapper中提供了attachBaseContext()(由系统调用)方法,用于给ContextWrapper对象中指定真正的Context对象,即ContextImpl对象,调用ContextWrapper的方法都会被转向ContextImpl的方法。
    • ContextImpl类
      上下文功能的实现类。
    • ContextThemeWrapper类
      一个带主题的封装类,其内部包含了与Theme相关的接口,这里所说的主题是指在AndroidManifest.xml中通过android:theme为Application元素或者Activity元素指定的主题。当然,只有Activity才需要主题,Service是不需要主题的,因为Service是没有界面的后台场景,所以Service直接继承于ContextWrapper,Application同理。

    总结:Context的两个子类分工明确,其中ContextImpl是Context的具体实现类,ContextWrapper是Context的包装类。Activity,Application,Service虽都继承自ContextWrapper,但它们初始化的过程中都会创建ContextImpl对象,由ContextImpl实现Context中的方法。
      那么,Context到底可以实现哪些功能呢?这个就实在是太多了,弹出Toast、启动Activity、启动Service、发送广播、操作数据库等等都需要用到Context。由于Context的具体能力是由ContextImpl类去实现的,因此在绝大多数场景下,Activity、Service和Application这三种类型的Context都是可以通用的,但在使用场景上是有一些规则,以下表格中列出了各Context的使用场景:

    以上表格中NO上添加了一些数字,其实这些从能力上来说是YES,但是为什么说是NO呢?下面一个一个解释:

    • NO^1:启动Activity在这些类中是可以的,但是需要创建一个新的task。不推荐。
      如果我们用Application Context或Service Context去启动一个LaunchMode为standard的Activity的时候会报错,这是因为非Activity类型的Context并没有所谓的任务栈,所以待启动的Activity就找不到栈了。解决这个问题的方法就是为待启动的Activity指定FLAG_ACTIVITY_NEW_TASK标记位,这样启动的时候就为它创建一个新的任务栈,而此时Activity是以singleTask模式启动的。
    • NO^2:在这些类中去layout inflate是合法的,但是会使用系统默认的主题样式,如果你自定义了某些样式可能不会被使用。不推荐。

    所以:

    • 凡是跟UI相关的,都应使用Activity做为Context来处理;其他的一些操作,Service,Activity,Application等都可以,当然得注意Context引用的持有,防止内存泄漏。
      比如启动Activity,还有弹出Dialog。出于安全原因的考虑,Android是不允许Activity或Dialog凭空出现的,一个Activity的启动必须要建立在另一个Activity的基础之上,也就是以此形成的返回栈。而Dialog则必须在一个Activity上面弹出(除非是System Alert类型的Dialog),因此在这种场景下,我们只能使用Activity类型的Context,否则将会出错。

    了解了Context,那在一个应用程序中,Context的数量又是多少呢?由以上的介绍可以知道:**Context数量 = Activity数量 + Service数量 + 1 **

    Context的创建时机和获取

    1.Context的创建时机

    (1)创建Application对象的时机
      每个应用程序在第一次启动时,都会首先创建Application对象。在应用程序启动一个Activity(startActivity)的流程中,创建Application的时机是创建handleBindApplication()方法中,该函数位于 ActivityThread.java类中,如下:

    //创建Application时同时创建的ContextIml实例
      private final void handleBindApplication(AppBindData data){
          …
          ///创建Application对象
          Application app = data.info.makeApplication(data.restrictedBackupMode, null);
          …
      }
      public Application makeApplication(boolean forceDefaultAppClass, Instrumentation instrumentation) {
          …
         try {
             java.lang.ClassLoader cl = getClassLoader();
             ContextImpl appContext = new ContextImpl();    //创建一个ContextImpl对象实例
             appContext.init(this, null, mActivityThread);  //初始化该ContextIml实例的相关属性
             ///新建一个Application对象
             app = mActivityThread.mInstrumentation.newApplication(
                     cl, appClass, appContext);
            appContext.setOuterContext(app);  //将该Application实例传递给该ContextImpl实例
         }
         …
     }
    

    (2)创建Activity对象的时机
      通过startActivity()或startActivityForResult()请求启动一个Activity时,如果系统检测需要新建一个Activity对象时,就会回调handleLaunchActivity()方法,该方法继而调用performLaunchActivity()方法,去创建一个Activity实例,并且回调onCreate(),onStart()方法等, 函数都位于 ActivityThread.java类 ,如下:

    //创建一个Activity实例时同时创建ContextIml实例
    private final void handleLaunchActivity(ActivityRecord r, Intent customIntent) {
        …
        Activity a = performLaunchActivity(r, customIntent);  //启动一个Activity
    }
    private final Activity performLaunchActivity(ActivityRecord r, Intent customIntent) {
        …
        Activity activity = null;
        try {
            //创建一个Activity对象实例
            java.lang.ClassLoader cl = r.packageInfo.getClassLoader();
            activity = mInstrumentation.newActivity(cl, component.getClassName(), r.intent);
        }
        if (activity != null) {
            ContextImpl appContext = new ContextImpl();      //创建一个Activity实例
            appContext.init(r.packageInfo, r.token, this);   //初始化该ContextIml实例的相关属性
            appContext.setOuterContext(activity);            //将该Activity信息传递给该ContextImpl实例
            …
        }
        …
    }
    

    (3)创建Service对象的时机
      通过startService或者bindService时,如果系统检测到需要新创建一个Service实例,就会回调handleCreateService()方法,完成相关数据操作。handleCreateService()函数位于 ActivityThread.java类,如下:

    //创建一个Service实例时同时创建ContextIml实例
    private final void handleCreateService(CreateServiceData data){
        …
        //创建一个Service实例
        Service service = null;
        try {
            java.lang.ClassLoader cl = packageInfo.getClassLoader();
            service = (Service) cl.loadClass(data.info.name).newInstance();
        } catch (Exception e) {
        }
        …
        ContextImpl context = new ContextImpl(); //创建一个ContextImpl对象实例
        context.init(packageInfo, null, this);   //初始化该ContextIml实例的相关属性
        //获得我们之前创建的Application对象信息
        Application app = packageInfo.makeApplication(false, mInstrumentation);
        //将该Service信息传递给该ContextImpl实例
        context.setOuterContext(service);
        …
    }
    

    另外,通过对ContextImp的分析可知,其方法的大多数操作都是直接调用其属性mPackageInfo(该属性类型为PackageInfo)的相关方法而来。这说明ContextImp是一种轻量级类,而PackageInfo才是真正重量级的类。而一个App里的所有ContextIml实例,都对应同一个packageInfo对象。

    2.Context的获取

    (1)通常我们想要获取Context对象,主要有以下四种方法

    • View.getContext,返回当前View对象的Context对象,通常是当前正在展示的Activity对象
    • Activity.getApplicationContext,获取的context来自允许在应用(进程)application中的所有Activity,当你需要用到的Context超出当前Activity的生命周期时使用
    • Activity.this 返回当前的Activity实例,如果是UI控件需要使用Activity作为Context对象,但是默认的Toast实际上使用ApplicationContext也可以
    • ContextWrapper.getBaseContext()用来获取一个ContextWrapper进行装饰之前的Context,也就是ContextImpl对象,如果想获取另一个可以访问的application里面的Context时可以使用

    (2)再来看看getApplication()和getApplicationContext()
      这两个方法有什么区别呢?看看以下结果:

    通过上面的代码,可以看到它们是同一个对象。其实这个结果也很好理解,因为前面已经说过了,Application本身就是一个Context,所以这里获取getApplicationContext()得到的结果就是Application本身的实例。那么问题来了,既然这两个方法得到的结果都是相同的,那么Android为什么要提供两个功能重复的方法呢?实际上这两个方法在作用域上有比较大的区别。
      getApplication()方法的语义性非常强,一看就知道是用来获取Application实例的,但这个方法只有在Activity和Service中才能调用。如果在一些其它的场景,比如BroadcastReceiver中也想获得Application的实例,这时就需要借助getApplicationContext()方法了。也就是说,getApplicationContext()方法的作用域会更广一些,任何一个Context的实例,只要调用getApplicationContext()方法都可以拿到我们的Application对象。
    (3)getActivity()和getContext()

    • getActivity()返回Activity,getContext()返回Context;
    • 两者是Fragment的方法,但Activity没有,多数情况下两者没有什么区别,但新版Support Library包,Fragment不被Activity持有时,区别见这里
    • 参数是context的,可以使用getActivity() 。因为Activity间接继承了Context,但Context不是Activity;
    • this和getContext() 并不是完全相同。在Activity类中可以使用this,因为Activity继承自Context,但是getContext()方法不在Activity类中。

    Application使用相关问题

    1.什么时候初始化全局变量

    在应用程序中常常会持有一个自己的Application,首先让它继承自系统的Application类,然后在自己的Application类中去封装一些通用的操作。虽然Application的用法很简单,但同时也存在着不少Application误用的场景。Application是Context的其中一种类型,那么是否就意味着,只要是Application的实例,就能随时使用Context的各种方法呢?做个实验试:

    方式1:
    public class MyApplication extends Application {      
        public MyApplication() {  
            String packageName = getPackageName();  
            Log.d("TAG", "package name is " + packageName);  
        }     
    }  
      
    方式2:
    public class MyApplication extends Application {      
        @Override  
        public void onCreate() {  
            super.onCreate();  
            String packageName = getPackageName();  
            Log.d("TAG", "package name is " + packageName);  
        }     
    } 
    

    这是一个非常简单的自定义Application,以上我们分别采用了在MyApplication的构造方法和onCreate()方法中两种方式来获取当前应用程序的包名,并打印出来。获取包名使用了getPackageName()方法,这个方法就是由Context提供的。那哪种方式能得到想要的结果呢?得到的结果是否又是一样?
    结果表明,方式一应用程序一启动就立刻崩溃了,报的是一个空指针异常:


    方式二运行正常:


    这两个方法之间到底发生了什么事情呢?我们重新回顾一下ContextWrapper类的源码,ContextWrapper中有一个attachBaseContext()方法,这个方法会将传入的一个Context参数赋值给mBase对象,之后mBase对象就有值了。而我们又知道,所有Context的方法都是调用这个mBase对象的同名方法,那么也就是说如果在mBase对象还没赋值的情况下就去调用Context中的任何一个方法时,就会出现空指针异常,上面的代码就是这种情况。
    Application中方法的执行顺序为:Application构造方法—>attachBaseContext()—>onCreate()。
    Application中在onCreate()方法里去初始化各种全局变量数据是一种比较推荐的做法,但如果你想把初始化的时间提前到极致,也可以重写attachBaseContext(),如下所示:

    public class MyApplication extends Application {      
        @Override  
        protected void attachBaseContext(Context base) {  
            // 在这里调用Context的方法会崩溃  
            super.attachBaseContext(base);  
            // 在这里可以正常调用Context的方法  
        }       
    } 
    
    2.自定义Application?

    其实Android官方并不太推荐我们使用自定义的Application,基本上只有需要做一些全局初始化的时候可能才需要用到自定义Application。多数项目只是把自定义Application当成了一个通用工具类,而这个功能并不需要借助Application来实现,使用单例可能是一种更加标准的方式。不过自定义Application也并没有什么副作用,它和单例模式二选一都可以实现同样的功能,但把自定义Application和单例模式混合到一起使用,就会出各种问题了。如下:

    
    public class MyApplication extends Application {  
          
        private static MyApplication app;  
          
        public static MyApplication getInstance() {  
            if (app == null) {  
                app = new MyApplication();  
            }  
            return app;  
        }       
    } 
    

    就像单例模式一样,这里提供了一个getInstance()方法,用于获取MyApplication的实例,有了这个实例之后,就可以调用MyApplication中的各种工具方法了,然而事实却非想的那么美好。因为我们知道Application是属于系统组件,系统组件的实例是要由系统来去创建的,如果这里我们自己去new一个MyApplication的实例,它就只是一个普通的Java对象而已,而不具备任何Context的能力,如果想通过该对象来进行Context操作,就会发生空指针错误。那么如果真的想要提供一个获取MyApplication实例的方法,比较标准的写法又是什么样的呢?其实这里我们只需谨记一点,Application全局只有一个,它本身就已经是单例了,无需再用单例模式去为它做多重实例保护了,代码如下所示:

    public class MyApplication extends Application {      
        private static MyApplication app;  
          
        public static MyApplication getInstance() {  
            return app;  
        }  
          
        @Override  
        public void onCreate() {  
            super.onCreate();  
            app = this;  
        }      
    }  
    

    getInstance()方法可以照常提供,但是里面不要做任何逻辑判断,直接返回app对象就可以了,而app对象又是什么呢?在onCreate()方法中我们将app对象赋值成this,this就是当前Application的实例,那么app也就是当前Application的实例了。

    Context引起的内存泄露

    context发生内存泄露的话,就会泄露很多内存。这里泄露的意思是gc没有办法回收activity的内存,在传递Context时会增加对象指针的引用计数,所以基于智能指针技术的GC无法释放相应的内存。
      当屏幕旋转的时候,系统会销毁当前的activity,保存状态信息,再创建一个新的。比如我们写了一个应用程序,它需要加载一个很大的图片,我们不希望每次旋转屏幕的时候都销毁这个图片,重新加载。实现这个要求的简单想法就是定义一个静态的Drawable,这样Activity 类创建销毁它始终保存在内存中。实现类似:

    public class myActivity extends Activity {
        private static Drawable sDrawable;
        protected void onCreate(Bundle state) {
        super.onCreate(state);
     
        TextView textView = new TextView(this);
        textView.setText("Leaks are bad");
        if (sDrawable == null) {
        sDrawable = getDrawable(R.drawable.large_bitmap);
        }
        textView.setBackgroundDrawable(sDrawable);//drawable attached to a view
        setContentView(label);
      }
    }
    

    这段程序看起来很简单,但是却问题很大。当屏幕旋转的时候会有内存泄漏(即gc没法销毁Activity)。屏幕旋转的时系统会销毁当前的activity,但是当drawable和view关联后,drawable保存了view的 reference,即sDrawable保存了textView的引用,而textView保存了Activity的引用。既然Drawable不能销毁,它所引用和间接引用的都不能销毁,这样系统就没有办法销毁当前的Activity,于是造成了内存泄露,gc对这种类型的内存泄露是无能为力的。为了防止内存泄露,我们应该注意以下几点:

    • 不要让生命周期长的对象引用Activity Context,即保证引用activity的对象要与activity本身生命周期是一样的
    • 对于生命周期长的对象,可以使用Application Context
    • 避免非静态的内部类,尽量使用静态类,避免生命周期问题,注意内部类对外部对象引用导致的生命周期变化

    相关文章

      网友评论

        本文标题:Android Context 熟悉还是陌生?

        本文链接:https://www.haomeiwen.com/subject/zjkqattx.html