Android Context 熟悉还是陌生?

一、什么是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
  • 避免非静态的内部类,尽量使用静态类,避免生命周期问题,注意内部类对外部对象引用导致的生命周期变化
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,560评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,104评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,297评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,869评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,275评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,563评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,833评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,543评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,245评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,512评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,011评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,359评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,006评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,062评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,825评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,590评论 2 273
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,501评论 2 268

推荐阅读更多精彩内容