Android基础之Context

Android基础之Context

Context

对于Android开发者来说Context应该是非常熟悉的。我们在使用Toast,启动Activity,启动Service,加载资源,操作数据库,获取App相关的文件路径,创建View等操作时,都会涉及到一个Context引用。那么Context到底是什么,Activity和Application作为Context到底有什么区别,哪个更好呢?以及我们在项目建立最初自定义的Application提供全局Context该怎么实现呢?

什么是Context

写过一个小Android demo的应该会有一个疑问为什么一个Android应用不需要一个main()方法来启动整个程序,如果是通过一个main()方法来启动程序,那么我们就可以在里面执行例如new一个Activity等操作。Android的应用模型是基于组件的应用设计模式,组件能够运行需要一个完整的工程环境。同时,各个组件拥有自己独立的场景(Context),并不能采用new的方式创建一个组件对象。Context是维持Android程序中各组件能够正常工作的一个核心功能类。

总结:一个应用是一个完整的工程环境,在这个应用中我们有许多功能,例如打电话,发短信,发邮件。这些功能都算是这个环境中的一个场景。比如发短信场景包含界面的显示以及数据的传递等等。所以我们可以把场景(Context)理解为用户与操作系统交互的一个过程。

Context类

首先来看Context类的介绍

/**
 * Interface to global information about an application environment.  This is
 * an abstract class whose implementation is provided by
 * the Android system.  It
 * allows access to application-specific resources and classes, as well as
 * up-calls for application-level operations such as launching activities,
 * broadcasting and receiving intents, etc.
 */
public abstract class Context {
    ......
}

他的注释:Context提供了关于应用环境全局信息的接口。它是一个抽象类,它的执行被Android系统所提供。它允许获取以应用为特征的资源和类型,是一个统领一些资源(应用程序环境变量等)的上下文。也就是说Context描述一个应用程序环境的信息(即上下文);是一个抽象类,Android提供了该抽象类的具体实现类;通过它我们可以获取应用程序的资源和类(包括应用级别操作,如启动Activity,发广播,接受Intent等)。

引用自博客Android应用Context详解及源码解析

那么在平常开发中我们用到哪些这个Context抽象类的实现呢?

盗用大牛郭霖的图片.png

可以发现Application,Service,Activity都是者间接继承自Context。在上面的继承关系中我们看到ContextImpl和ContextWrapper是直接继承自Context。ContextWrapper是Context类的一个封装类,而ContextImpl是Context抽象类的实现类。ContextWrapper的构造函数中有一个真正的Context的引用(ContextImpl对象,mBase)。ContextWrapper的子类有一个ContextThemeWrapper,是带主题封装的类,即通过android:theme属性指定的(例如Activity带UI显示)。

那么我们一个应用中有多少个Context呢?

Android应用程序只有四大组件,而其中两大组件都继承自
Context,另外每个应用程序还有一个全局的Application对象。所以在我们了解了上面继承关系之后我们就可以计算出来Context总数,如下:

appContextCount = Application + ActivityCount + ServiceCount;

Context的实例化

Activity,Application,Service实例都是在ActivityThread中创建的。我参考了博客Android应用Context详解及源码解析(因为自己还没有深入了解到App的启动,只是想了解Context基础知识)

Activity中ContextImpl实例化源码分析

通过startActivity启动一个新的Activity时系统会回调ActivityThread的handleLaunchActivity()方法,该方法内部会调用performLaunchActivity()方法去创建一个Activity实例,然后回调Activity的onCreate()等方法。所以Activity的ContextImpl实例化是在ActivityThread类的performLaunchActivity方法中,如下:

    private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
        ......
            //已经创建好新的activity实例
            if (activity != null) {
                //创建一个Context对象
                Context appContext = createBaseContextForActivity(r, activity);
                ......
                //将上面创建的appContext传入到activity的attach方法
                activity.attach(appContext, this, getInstrumentation(), r.token,
                        r.ident, app, r.intent, r.activityInfo, title, r.parent,
                        r.embeddedID, r.lastNonConfigurationInstances, config,
                        r.referrer, r.voiceInteractor);
                ......
            }
        ......
        return activity;
    }
    

通过createBaseContextForActivity(r, activity);创建appContext,然后通过activity.attach设置值。

createBaseContextForActivity(r, activity)部分源代码:

    private Context createBaseContextForActivity(ActivityClientRecord r,
            final Activity activity) {
        //实质就是new一个ContextImpl对象,调运ContextImpl的有参构造初始化一些参数    
        ContextImpl appContext = ContextImpl.createActivityContext(this, r.packageInfo, r.token);
        //特别特别留意这里!!!
        //ContextImpl中有一个Context的成员叫mOuterContext,通过这条语句就可将当前新Activity对象赋值到创建的ContextImpl的成员mOuterContext(也就是让ContextImpl内部持有Activity)。
        appContext.setOuterContext(activity);
        //创建返回值并且赋值
        Context baseContext = appContext;
        ......
        //返回ContextImpl对象
        return baseContext;
    }

Activity.attach()部分源代码:

    final void attach(Context context, ActivityThread aThread,
            Instrumentation instr, IBinder token, int ident,
            Application application, Intent intent, ActivityInfo info,
            CharSequence title, Activity parent, String id,
            NonConfigurationInstances lastNonConfigurationInstances,
            Configuration config, String referrer, IVoiceInteractor voiceInteractor) {
        //特别特别留意这里!!!
        //与上面createBaseContextForActivity方法中setOuterContext语句类似,不同的在于:
        //通过ContextThemeWrapper类的attachBaseContext方法,将createBaseContextForActivity中实例化的ContextImpl对象传入到ContextWrapper类的mBase变量,这样ContextWrapper(Context子类)类的成员mBase就被实例化为Context的实现类ContextImpl
        attachBaseContext(context);
        ......
    }

总结:Activity通过ContextWrapper的成员mBase来引用了一个ContextImpl对象,这样,Activity组件以后就可以调用这个ContextImpl对象的一系列方法(启动Service等);同时ContextImpl类又通过自己的成员mOuterContext引用了与它关联的Activity,这样ContextImpl类也可以操作Activity。

由此说明一个Activity就有一个Context,而且生命周期和Activity类相同。在使用Activity作为Context引用时要考虑到内存泄漏。

Service中ContextImpl实例化源码分析

通过startService或者bindService方法创建一个新Service时就会回调ActivityThread类的handleCreateService()方法完成相关数据操作。具体handleCreateService方法代码如下:

    private void handleCreateService(CreateServiceData data) {
        ......
        //类似上面Activity的创建,这里创建service对象实例
        Service service = null;
        try {
            java.lang.ClassLoader cl = packageInfo.getClassLoader();
            service = (Service) cl.loadClass(data.info.name).newInstance();
        } catch (Exception e) {
            ......
        }

        try {
            ......
            //不做过多解释,创建一个Context对象
            ContextImpl context = ContextImpl.createAppContext(this, packageInfo);
            //特别特别留意这里!!!
            //ContextImpl中有一个Context的成员叫mOuterContext,通过这条语句就可将当前新Service对象赋值到创建的ContextImpl的成员mOuterContext(也就是让ContextImpl内部持有Service)。
            context.setOuterContext(service);

            Application app = packageInfo.makeApplication(false, mInstrumentation);
            //将上面创建的context传入到service的attach方法
            service.attach(context, this, data.info.name, data.token, app,
                    ActivityManagerNative.getDefault());
            service.onCreate();
            ......
        } catch (Exception e) {
            ......
        }
    }

Service中的attach方法:

    public final void attach(
            Context context,
            ActivityThread thread, String className, IBinder token,
            Application application, Object activityManager) {
            //特别特别留意这里!!!
            //与上面handleCreateService方法中setOuterContext语句类似,不同的在于:
            //通过ContextWrapper类的attachBaseContext方法,将handleCreateService中实例化的ContextImpl对象传入到ContextWrapper类的mBase变量,这样ContextWrapper(Context子类)类的成员mBase就被实例化为Context的实现类ContextImpl
        attachBaseContext(context);
        ......
    }

说明一个Service就有一个Context,而且生命周期和Service类相同,在使用Service作为Context引用时要考虑到内存泄漏

Application中ContextImpl实例化源码分析

一个APP以后每次重新启动时都会首先创建Application对象(每个APP都有一个唯一的全局Application对象,与整个APP的生命周期相同)。创建Application的过程也在ActivityThread类的handleBindApplication()方法完成相关数据操作。而ContextImpl的创建是在该方法中调运LoadedApk类的makeApplication方法中实现,LoadedApk类的makeApplication()方法中源代码如下:

    public Application makeApplication(boolean forceDefaultAppClass,
            Instrumentation instrumentation) {
        //只有新创建的APP才会走if代码块之后的剩余逻辑
        if (mApplication != null) {
            return mApplication;
        }
        //即将创建的Application对象
        Application app = null;

        String appClass = mApplicationInfo.className;
        if (forceDefaultAppClass || (appClass == null)) {
            appClass = "android.app.Application";
        }

        try {
            java.lang.ClassLoader cl = getClassLoader();
            if (!mPackageName.equals("android")) {
                initializeJavaContextClassLoader();
            }
            //不做过多解释,创建一个Context对象
            ContextImpl appContext = ContextImpl.createAppContext(mActivityThread, this);
            //将Context传入Instrumentation类的newApplication方法
            app = mActivityThread.mInstrumentation.newApplication(
                    cl, appClass, appContext);
            //特别特别留意这里!!!
            //ContextImpl中有一个Context的成员叫mOuterContext,通过这条语句就可将当前新Application对象赋值到创建的ContextImpl的成员mOuterContext(也就是让ContextImpl内部持有Application)。
            appContext.setOuterContext(app);
        } catch (Exception e) {
            ......
        }
        ......
        return app;
    }

Instrumentation.newApplication方法部分源码:

    public Application newApplication(ClassLoader cl, String className, Context context)
            throws InstantiationException, IllegalAccessException, 
            ClassNotFoundException {
        return newApplication(cl.loadClass(className), context);
    }

newApplication方法部分源码:

    static public Application newApplication(Class<?> clazz, Context context)
            throws InstantiationException, IllegalAccessException, 
            ClassNotFoundException {
        ......
        //继续传递context
        app.attach(context);
        return app;
    }

Application类的attach方法部分源码:

final void attach(Context context) {
        //特别特别留意这里!!!
        //与上面makeApplication方法中setOuterContext语句类似,不同的在于:
        //通过ContextWrapper类的attachBaseContext方法,将makeApplication中实例化的ContextImpl对象传入到ContextWrapper类的mBase变量,这样ContextWrapper(Context子类)类的成员mBase就被实例化为Application的实现类ContextImpl
        attachBaseContext(context);
        ......
    }

说明一个Application就有一个Context,而且生命周期和Application类相同(然而一个App只有一个Application,而且与应用生命周期相同)。

通过Context引用获取资源

看到这里我有个疑问不同的Context引用,获取到的资源是同一份吗?

class ContextImpl extends Context {
    ......
    private final ResourcesManager mResourcesManager;
    private final Resources mResources;
    ......
    @Override
    public Resources getResources() {
        return mResources;
    }
    ......
}

有了上面分析我们可以很确定平时写的App中context.getResources方法获得的Resources对象就是上面ContextImpl的成员变量mResources。那我们追踪可以发现mResources的赋值操作如下:

    private ContextImpl(ContextImpl container, ActivityThread mainThread,
            LoadedApk packageInfo, IBinder activityToken, UserHandle user, boolean restricted,
            Display display, Configuration overrideConfiguration) {
        ......
        //单例模式获取ResourcesManager对象
        mResourcesManager = ResourcesManager.getInstance();
        ......
        //packageInfo对于一个APP来说只有一个,所以resources 是同一份
        Resources resources = packageInfo.getResources(mainThread);
        if (resources != null) {
            if (activityToken != null
                    || displayId != Display.DEFAULT_DISPLAY
                    || overrideConfiguration != null
                    || (compatInfo != null && compatInfo.applicationScale
                            != resources.getCompatibilityInfo().applicationScale)) {
                //mResourcesManager是单例,所以resources是同一份
                resources = mResourcesManager.getTopLevelResources(packageInfo.getResDir(),
                        packageInfo.getSplitResDirs(), packageInfo.getOverlayDirs(),
                        packageInfo.getApplicationInfo().sharedLibraryFiles, displayId,
                        overrideConfiguration, compatInfo, activityToken);
            }
        }
        //把resources赋值给mResources
        mResources = resources;
        ......
    }

由此可以看出在设备其他因素不变的情况下我们通过不同的Context实例得到的Resources是同一套资源。

以上源码分析完全是引用自别人的博客,自己对上面的分析也只是停留在一个字面意思上,只是通过上面的分析理解了不同Context的实例相对于整个App的生命周期,可以在实际开发中作为判断到底使用哪个Context引用的依据。以及解决了心中对于不同Context引用获取资源的疑问

getApplication和getApplicationContext的区别

getApplication()方法

Activity和Service提供了getApplication,而且返回类型都是Application。

public final Application getApplication() {
        return mApplication;
    }

这个mApplication都是在各自类的attach方法参数出入的,也就是说这个mApplication都是在ActivityThread中各自实例化时获取的makeApplication方法返回值。而且不同的Activity和Service返回的Application均为同一个全局对象。

getApplicationContext()方法

通过ContextImpl对象调用getApplicationContext()方法

class ContextImpl extends Context {
    ......
    @Override
    public Context getApplicationContext() {
        return (mPackageInfo != null) ?
                mPackageInfo.getApplication() : mMainThread.getApplication();
    }
    ......
}

可以看到getApplicationContext方法是Context的方法,而且返回值是Context类型,返回对象和上面通过Service或者Activity的getApplication返回的是一个对象。

区别

虽然这两个方法最终的得到的值都是同一个对象的引用,但是它们的类型不同(一个是Application,一个是Context)且作用域有很大的区别。getApplication()方法的语义性非常强,一看就知道是用来获取Application实例的,但是这个方法只有在Activity和Service中才能调用的到。那么也许在绝大多数情况下我们都是在Activity或者Service中使用Application的,但是如果在一些其它的场景,比如BroadcastReceiver中也想获得Application的实例,这时就可以借助getApplicationContext()方法。

还有一个得到Context的方法,getBaseContext()。getBaseContext()方法得到的是一个ContextImpl对象。也就是调用ContextWrapper的getBaseContext()方法返回存储ContextImpl对象的mBase。

Activity和Application作为Context引用

Context引起的内存泄漏

在实际开发中我们需要用到工具类,一般都会采用单例模式:

public class Singleton {
    private static Singleton instance;
    private Context mContext;
    private Singleton(Context context) {
        this.mContext = context;
    }
    public static synchronized Singleton getInstance(Context context) {
        if (instance == null) {
            instance = new Singleton(context);
        }
        return instance;
    }
   
}

你在某个Activity调用了getInstance()方法,直接传了个this;这样问题就来了,我们的这个类中的sInstance是一个static且强引用的,在其内部引用了一个Activity作为Context,也就是说,我们的这个Activity只要我们的项目活着,就没有办法进行内存回收。而我们的Activity的生命周期肯定没这么长,所以造成了内存泄漏。

Java强引用会导致GC内存无法回收

解决办法:

public class Singleton {
    private static Singleton instance;
    private Context mContext;
    private Singleton(Context context) {
        this.mContext = context;
    }
    public static synchronized Singleton getInstance(Context context) {
        if (instance == null) {
            instance = new Singleton(context.getApplicationContext());
        }
        return instance;
    }
   
}

由于Application对象的生命周期如上面分析的那样,和整个项目一样长。也就是它的生命周期和我们的单例对象一致。

不同的Context适用场景

正如上面所说的Context引用不当会造成内存泄漏,那么我们是否可以都引用Application作为Context引用呢?答案是否定的,不同Context(指的是Activity,Application,Service)的应用场景是不同的,并非所有Activity为Context的场景,Application Context都能搞定。

Application Activity Service
show dialog N Y N
start Activity N Y N
Layout Inflation N Y N
start service Y Y Y
send a BroadCast Y Y Y
register a BroadCast Receiver Y Y Y
load Resource Y Y Y

上述表格中可以看到,在启动一个Activity和Dialog时,不推荐使用Application和Service。Android系统出于安全原因的考虑,是不允许Activity或Dialog凭空出现的,一个Activity的启动必须要建立在另一个Activity的基础之上,也就是以此形成的返回栈。而Dialog则必须在一个Activity上面弹出(除非是System Alert类型的Dialog),因此在这种场景下,我们只能使用Activity类型的Context,否则将会出错。调用除了Activity的,其他Context实例的LayoutInflation,会使用系统默认的主题样式,如果你自定义了某些样式可能不会被使用。

总结:和UI相关的方法基本都不建议或者不可使用Application,并且,前三个操作基本不可能在Application中出现。实际上,只要把握住一点,凡是跟UI相关的,都应该使用Activity做为Context来处理;其他的一些操作,Service,Activity,Application等实例都可以,当然了,注意Context引用的持有,防止内存泄漏。

参考

Context都没弄明白,还怎么做Android开发?

Android应用Context详解及源码解析

Android Context完全解析,你所不知道的Context的各种细节

Android Context 上下文 你必须知道的一切

目标

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

推荐阅读更多精彩内容