一文吃透Android Context:从原理到实战

一、Context 是什么?

在 Android 开发中,你是否曾好奇:为什么启动一个 Activity 需要一个特定的参数?为什么访问应用资源时总有个 "神秘" 的对象参与其中?这个贯穿于 Android 开发各个角落的关键元素,就是 Context,它就像是 Android 应用的幕后英雄,默默支撑着各种功能的实现 ,其重要性不言而喻。

从官方定义来看,Context 是一个连接着有关应用程序全局信息的抽象类。它就像是一个 "信息枢纽",允许开发者访问特定应用程序的资源和类,以及执行应用程序级别的操作,如启动活动、广播和接受意图等。这就好比它是一个城市的信息中心,掌握着城市里各个角落(应用资源和类)的信息,并且能够调度各种城市活动(应用程序级操作)。

作为一个抽象类,Context 本身不能被实例化,它定义了一系列方法和接口,具体的实现则由 Android 系统提供的具体类来完成 。就像一份建筑蓝图,它规划好了房子的结构和功能,但真正建造房子(实现功能)的是具体的施工团队(具体实现类)。在 Android 中,Activity、Service、Application 等类都间接继承自 Context,它们在不同的场景下为我们提供了具体的上下文环境。例如,Activity 提供了与用户界面交互相关的上下文,Service 则为后台任务提供了上下文环境 。

简单来说,Context 为应用程序提供了一个运行环境,是应用程序与系统环境进行交互的桥梁。它就像一个管家,管理着应用程序的各种资源和组件,使得它们能够协同工作。通过 Context,应用程序可以获取资源、启动组件、访问系统服务等,这些功能对于构建一个完整且功能丰富的 Android 应用至关重要。

二、Context 的类型

在 Android 开发中,Context 根据不同的应用场景和生命周期,主要分为以下几种类型:Application Context、Activity Context、Service Context 和 BroadcastReceiver Context 。每种类型的 Context 都有其独特的生命周期和适用场景,了解它们的区别对于正确使用 Context 至关重要。

(一)Application Context

Application Context 的生命周期与整个应用程序的生命周期一致 。当应用程序启动时,它被创建;当应用程序终止时,它才会被销毁。这就好比一个城市的市政中心,只要城市存在(应用程序运行),市政中心就一直运转(Application Context 存在)。

由于其生命周期长的特点,Application Context 适用于那些需要在整个应用程序生命周期内保持的操作和资源获取,比如全局初始化操作、获取应用程序级别的资源等 。例如,在一些应用中,我们可能需要在应用启动时就初始化一些全局的配置信息,这时就可以使用 Application Context 来完成这个操作。因为它在应用的整个生命周期内都存在,所以不用担心在其他组件销毁时导致初始化操作失败。

获取 Application Context 也非常简单,在任何组件(如 Activity、Service 等)中,都可以通过getApplicationContext()方法来获取 。比如在 Activity 中,我们可以这样获取:

java 复制代码
Context appContext = getApplicationContext();

在自定义的 Application 类中,我们还可以通过重写onCreate方法,将applicationContext保存起来,方便在其他地方使用:

java 复制代码
public class MyApplication extends Application {
    private static Context context;
    @Override
    public void onCreate() {
        super.onCreate();
        context = this;
    }
    public static Context getContext() {
        return context;
    }
}

然后在其他地方,就可以通过MyApplication.getContext()来获取 Application Context。

(二)Activity Context

Activity Context 与对应的 Activity 的生命周期一致 。当 Activity 被创建时,Activity Context 随之创建;当 Activity 被销毁时,Activity Context 也会被销毁。这就像一个商店,商店开业(Activity 创建)时,商店的运营环境(Activity Context)开始生效;商店关门(Activity 销毁)时,运营环境也就结束了。

Activity Context 主要用于与当前 Activity 相关的操作,比如启动新的 Activity、弹出 Dialog、访问当前 Activity 特有的资源等 。因为它与 Activity 的生命周期紧密相关,所以在这些场景下使用 Activity Context 能够确保操作的正确性和安全性。例如,当我们在一个 Activity 中点击一个按钮,要启动另一个 Activity 时,就需要使用当前 Activity 的 Context 来启动:

java 复制代码
Intent intent = new Intent(this, AnotherActivity.class);
startActivity(intent);

这里的this就是当前 Activity 的 Context。

在 Activity 内部,我们可以直接使用this关键字来获取 Activity Context 。而在 Fragment 中,由于 Fragment 是依附于 Activity 存在的,所以可以通过getActivity()方法来获取其所属 Activity 的 Context 。不过需要注意的是,在 Fragment 的onDetach()方法被调用后,getActivity()可能会返回null,此时就不能再使用getActivity()来获取 Context 了。例如:

java 复制代码
public class MyFragment extends Fragment {
    @Override
    public void onViewCreated(View view, Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        Context activityContext = getActivity();
        if (activityContext != null) {
            // 使用activityContext进行相关操作
        }
    }
}

(三)Service Context

Service Context 的生命周期与对应的 Service 的生命周期同步 。当 Service 被创建时,Service Context 被创建;当 Service 被销毁时,Service Context 也随之销毁。这类似于一个工厂的运行环境,工厂开工(Service 创建)时,工厂的工作环境(Service Context)建立;工厂停工(Service 销毁)时,工作环境也随之消失。

Service Context 主要用于 Service 内部的操作,如访问 Service 的资源、发送广播、启动其他服务等 。因为它与 Service 的生命周期一致,所以在 Service 中使用 Service Context 能够保证操作与 Service 的状态相匹配。例如,在一个 Service 中,我们可能需要在 Service 启动时发送一个广播,告知其他组件 Service 已经启动:

java 复制代码
public class MyService extends Service {
    @Override
    public void onCreate() {
        super.onCreate();
        Intent intent = new Intent("SERVICE_STARTED_ACTION");
        sendBroadcast(intent);
    }
    // 其他方法...
}

这里的sendBroadcast(intent)就是使用了 Service Context 来发送广播。

在 Service 内部,我们可以直接使用this关键字来获取 Service Context 。比如在上述代码中,sendBroadcast(intent)中的this就是 Service Context。另外,也可以通过getApplicationContext()方法获取 Application Context,但一般情况下,在 Service 内部使用this作为 Context 就可以满足大部分需求。

(四)BroadcastReceiver Context

BroadcastReceiver Context 在 BroadcastReceiver 接收到广播时被创建,当广播处理完成时就会被销毁 。这就像是一个临时搭建的信息处理站,当有广播消息(信息)到来时,处理站(BroadcastReceiver Context)开始工作;消息处理完后,处理站就关闭。

BroadcastReceiver Context 主要用于在 BroadcastReceiver 中接收广播并处理相关逻辑 。由于其生命周期短暂,通常不用于启动 Activity 或 Service 等长期存在的组件,因为在广播处理完成后,BroadcastReceiver Context 就会被销毁,如果此时启动的组件依赖于这个 Context,可能会导致组件无法正常工作。例如,在一个监听网络状态变化的 BroadcastReceiver 中,我们可以这样处理:

java 复制代码
public class NetworkChangeReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        if (ConnectivityManager.CONNECTIVITY_ACTION.equals(intent.getAction())) {
            // 处理网络状态变化的逻辑
        }
    }
}

这里的context就是 BroadcastReceiver Context。

在 BroadcastReceiver 内部,我们可以直接使用this关键字来获取 BroadcastReceiver Context 。比如在上述代码中,onReceive(Context context, Intent intent)方法中的context,如果在方法内部使用this,也可以获取到相同的 Context 对象。

三、Context 的作用

Context 在 Android 应用开发中起着举足轻重的作用,它为应用程序提供了与系统交互的关键接口,涵盖了资源访问、组件启动、系统服务获取等多个重要方面 。下面我们来详细了解 Context 的具体作用。

(一)资源访问

在 Android 应用中,资源是构建丰富用户界面和实现多样化功能的基础,而 Context 则是访问这些资源的关键入口 。通过 Context,我们可以轻松获取应用程序中的各种资源,包括字符串、图像、颜色、尺寸、布局文件等 。

获取字符串资源是开发中常见的操作,比如在界面上显示应用的标题、提示信息等 。使用 Context 获取字符串资源的代码示例如下:

java 复制代码
Context context = getApplicationContext();
String appTitle = context.getString(R.string.app_name);

在上述代码中,R.string.app_name是字符串资源的标识符,通过context.getString(R.string.app_name)方法,我们可以从资源文件中获取对应的字符串值,并将其赋值给appTitle变量。

当我们需要在界面上展示图片时,就可以使用 Context 获取图像资源 。示例代码如下:

java 复制代码
Drawable image = context.getDrawable(R.drawable.ic_launcher);

这里的R.drawable.ic_launcher是图像资源的标识符,context.getDrawable(R.drawable.ic_launcher)方法会返回一个 Drawable 对象,我们可以将其用于设置 ImageView 的背景或者显示在其他需要展示图片的地方。

如果要获取颜色资源,用于设置界面元素的颜色,也可以通过 Context 来实现 。代码示例如下:

java 复制代码
int color = context.getColor(R.color.colorPrimary);

在这个例子中,R.color.colorPrimary是颜色资源的标识符,context.getColor(R.color.colorPrimary)方法会返回对应的颜色值,我们可以将其应用到 TextView 的文本颜色、View 的背景颜色等地方。

获取尺寸资源在适配不同屏幕尺寸和分辨率的设备时非常重要 。以下是获取尺寸资源的代码示例:

java 复制代码
float fontSize = context.getResources().getDimension(R.dimen.text_size);

在上述代码中,R.dimen.text_size是尺寸资源的标识符,context.getResources().getDimension(R.dimen.text_size)方法会返回对应的尺寸值,我们可以将其用于设置 TextView 的字体大小、View 的宽度和高度等。

布局文件是构建用户界面的重要组成部分,通过 Context 可以获取布局文件并将其 Inflate 成 View 对象 。示例代码如下:

java 复制代码
LayoutInflater inflater = LayoutInflater.from(context);
View view = inflater.inflate(R.layout.activity_main, null);

在这段代码中,LayoutInflater.from(context)获取了 LayoutInflater 实例,然后使用inflater.inflate(R.layout.activity_main, null)方法将R.layout.activity_main布局文件 Inflate 成一个 View 对象,这个 View 对象就可以添加到 Activity 的布局中展示给用户。

(二)组件启动

Context 在启动 Activity、Service 以及发送广播等组件相关操作中扮演着核心角色 。它为这些组件之间的交互提供了必要的环境和方法。

启动 Activity 是用户在应用中进行页面切换和交互的常见操作 。使用 Context 启动 Activity 的代码示例如下:

java 复制代码
Intent intent = new Intent(this, AnotherActivity.class);
startActivity(intent);

在上述代码中,this表示当前的 Context(通常是 Activity),Intent对象用于描述要启动的目标 Activity(这里是AnotherActivity.class) 。通过startActivity(intent)方法,就可以启动目标 Activity。如果需要传递数据给目标 Activity,可以使用intent.putExtra(key, value)方法,例如:

java 复制代码
Intent intent = new Intent(this, AnotherActivity.class);
intent.putExtra("message", "Hello, AnotherActivity!");
startActivity(intent);

在目标 Activity 中,可以通过getIntent().getStringExtra("message")获取传递过来的数据。

Service 常用于执行后台任务,如网络请求、数据处理等 。使用 Context 启动 Service 的代码示例如下:

java 复制代码
Intent serviceIntent = new Intent(this, MyService.class);
startService(serviceIntent);

这里的MyService.class是要启动的 Service 类,通过startService(serviceIntent)方法,就可以启动MyService 。如果需要停止 Service,可以使用stopService(intent)方法。

广播是一种广泛应用于 Android 组件间通信的机制 。使用 Context 发送广播的代码示例如下:

java 复制代码
Intent broadcastIntent = new Intent("MY_CUSTOM_ACTION");
sendBroadcast(broadcastIntent);

在上述代码中,创建了一个自定义的广播 Intent,其 Action 为"MY_CUSTOM_ACTION",然后通过sendBroadcast(broadcastIntent)方法将广播发送出去 。其他组件可以通过注册广播接收器来接收这个广播并进行相应的处理。例如:

java 复制代码
public class MyBroadcastReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        if ("MY_CUSTOM_ACTION".equals(intent.getAction())) {
            // 处理接收到的广播
        }
    }
}

在 Activity 或其他组件中,可以通过以下方式注册广播接收器:

java 复制代码
MyBroadcastReceiver receiver = new MyBroadcastReceiver();
IntentFilter filter = new IntentFilter("MY_CUSTOM_ACTION");
registerReceiver(receiver, filter);

这样,当广播发送出去时,MyBroadcastReceiver就会接收到并执行onReceive方法中的逻辑。

(三)系统服务获取

Context 提供了获取各类系统服务的便捷方式,这些系统服务涵盖了窗口管理、通知管理、电源管理、网络管理等多个方面,为应用程序与系统底层功能的交互提供了可能 。

窗口管理器(WindowManager)用于管理窗口的创建、显示、隐藏、布局等操作 。通过 Context 获取窗口管理器的代码示例如下:

java 复制代码
WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);

获取到WindowManager实例后,就可以进行一系列窗口相关的操作,比如创建一个悬浮窗口:

java 复制代码
WindowManager.LayoutParams params = new WindowManager.LayoutParams(
        WindowManager.LayoutParams.WRAP_CONTENT,
        WindowManager.LayoutParams.WRAP_CONTENT,
        WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY,
        WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
        PixelFormat.TRANSLUCENT);
View floatingView = LayoutInflater.from(context).inflate(R.layout.floating_view, null);
windowManager.addView(floatingView, params);

上述代码创建了一个悬浮窗口,其布局为R.layout.floating_view,并设置了相应的参数。

通知管理器(NotificationManager)用于管理应用程序的通知 。使用 Context 获取通知管理器的代码示例如下:

java 复制代码
NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);

获取到NotificationManager后,就可以创建和发送通知。例如:

java 复制代码
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, "CHANNEL_ID")
       .setSmallIcon(R.drawable.ic_notification)
       .setContentTitle("New Message")
       .setContentText("You have a new message!")
       .setPriority(NotificationCompat.PRIORITY_DEFAULT);
Notification notification = builder.build();
notificationManager.notify(1, notification);

上述代码创建了一个简单的通知,设置了图标、标题、内容等信息,并通过notificationManager.notify(1, notification)方法将通知发送出去,其中1是通知的唯一标识。

电源管理器(PowerManager)用于管理设备的电源状态,如屏幕亮度、休眠状态等 。通过 Context 获取电源管理器的代码示例如下:

java 复制代码
PowerManager powerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE);

获取到PowerManager后,可以进行一些电源相关的操作,比如判断屏幕是否处于唤醒状态:

java 复制代码
boolean isScreenOn = powerManager.isInteractive();

上述代码通过powerManager.isInteractive()方法判断屏幕是否处于唤醒状态。

网络管理器(ConnectivityManager)用于管理设备的网络连接状态 。使用 Context 获取网络管理器的代码示例如下:

java 复制代码
ConnectivityManager connectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);

获取到ConnectivityManager后,可以获取当前的网络连接信息,判断网络是否可用等。例如:

java 复制代码
NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo();
if (networkInfo != null && networkInfo.isConnected()) {
    // 网络已连接
} else {
    // 网络未连接
}

上述代码通过connectivityManager.getActiveNetworkInfo()方法获取当前的网络连接信息,并通过networkInfo.isConnected()方法判断网络是否已连接。

(四)其他功能

除了上述主要功能外,Context 在获取包名、包管理器,文件操作,SharedPreferences 操作等方面也发挥着重要作用 。

获取应用程序的包名在一些场景下非常有用,比如获取应用的版本信息、存储路径等 。使用 Context 获取包名的代码示例如下:

javajava 复制代码
String packageName = context.getPackageName();

上述代码通过context.getPackageName()方法获取当前应用的包名。

包管理器(PackageManager)用于管理应用程序的包信息,如获取应用的安装列表、启动应用等 。通过 Context 获取包管理器的代码示例如下:

java 复制代码
PackageManager packageManager = context.getPackageManager();

获取到PackageManager后,可以进行一些包相关的操作,比如获取应用的启动 Activity:

java 复制代码
Intent launchIntent = packageManager.getLaunchIntentForPackage(packageName);
if (launchIntent != null) {
    context.startActivity(launchIntent);
}

上述代码通过packageManager.getLaunchIntentForPackage(packageName)方法获取指定包名应用的启动 Intent,并通过context.startActivity(launchIntent)方法启动该应用。

在 Android 应用中,文件操作是常见的需求,如保存用户数据、读取配置文件等 。Context 提供了便捷的文件操作方法 。例如,保存文件的代码示例如下:

java 复制代码
String data = "Hello, World!";
try {
    FileOutputStream fos = context.openFileOutput("test.txt", Context.MODE_PRIVATE);
    fos.write(data.getBytes());
    fos.close();
} catch (IOException e) {
    e.printStackTrace();
}

上述代码通过context.openFileOutput("test.txt", Context.MODE_PRIVATE)方法创建一个名为test.txt的文件,并以私有模式打开,然后将字符串"Hello, World!"写入文件中。读取文件的代码示例如下:

java 复制代码
try {
    FileInputStream fis = context.openFileInput("test.txt");
    BufferedReader reader = new BufferedReader(new InputStreamReader(fis));
    StringBuilder sb = new StringBuilder();
    String line;
    while ((line = reader.readLine()) != null) {
        sb.append(line);
    }
    reader.close();
    fis.close();
    String content = sb.toString();
} catch (IOException e) {
    e.printStackTrace();
}

上述代码通过context.openFileInput("test.txt")方法打开名为test.txt的文件,并读取其中的内容。

SharedPreferences 是一种轻量级的存储方式,常用于存储应用的配置信息、用户偏好等 。使用 Context 操作 SharedPreferences 的代码示例如下:

java 复制代码
SharedPreferences sharedPreferences = context.getSharedPreferences("config", Context.MODE_PRIVATE);
SharedPreferences.Editor editor = sharedPreferences.edit();
editor.putString("username", "John");
editor.putInt("age", 25);
editor.apply();

上述代码通过context.getSharedPreferences("config", Context.MODE_PRIVATE)方法获取名为config的 SharedPreferences 对象,并通过editor.putString("username", "John")和editor.putInt("age", 25)方法向其中存储了用户名和年龄信息,最后通过editor.apply()方法提交修改。读取 SharedPreferences 中的数据示例如下:

java 复制代码
SharedPreferences sharedPreferences = context.getSharedPreferences("config", Context.MODE_PRIVATE);
String username = sharedPreferences.getString("username", "");
int age = sharedPreferences.getInt("age", 0);

上述代码通过sharedPreferences.getString("username", "")和sharedPreferences.getInt("age", 0)方法分别获取存储的用户名和年龄信息,如果获取失败,则返回默认值。

四、Context 的实现原理

(一)Context 的继承结构

要深入理解 Context 的实现原理,首先需要了解其继承结构。Context 是一个抽象类,它的主要实现类有 ContextImpl 和 ContextWrapper 。ContextImpl 是 Context 的真正实现类,它实现了 Context 中定义的所有抽象方法,提供了上下文环境的具体功能 。而 ContextWrapper 则是一个包装类,它通过持有一个 Context 对象(实际上是 ContextImpl 对象),将所有方法调用委托给被包装的 Context 对象,起到了代理和装饰的作用 。

Activity、Service 和 Application 都与 Context 有着紧密的继承关系 。Activity 继承自 ContextThemeWrapper,而 ContextThemeWrapper 又继承自 ContextWrapper 。Service 和 Application 则直接继承自 ContextWrapper 。这种继承关系使得 Activity、Service 和 Application 都能够拥有 Context 的功能,并且在不同的场景下提供特定的上下文环境 。例如,Activity 提供了与用户界面交互相关的上下文,Service 提供了后台服务运行的上下文,Application 则提供了应用程序全局的上下文 。

用一个简单的比喻来说,Context 就像是一个基础的建筑框架,ContextImpl 是这个框架的具体搭建者,负责实现所有的功能;ContextWrapper 则像是给这个框架添加了一些装饰和扩展功能的组件;而 Activity、Service 和 Application 就像是在这个框架基础上建造的不同功能的房间,它们各自有独特的用途,但都依赖于 Context 这个基础框架 。下面是 Context 的继承关系图:

typescript 复制代码
Context
├── ContextImpl
└── ContextWrapper
    ├── ContextThemeWrapper
    │   └── Activity
    ├── Service
    └── Application

从图中可以清晰地看到 Context 的继承层次结构,这有助于我们更好地理解不同类型的 Context 之间的关系以及它们如何协同工作 。

(二)ContextImpl 类解析

ContextImpl 类是 Context 功能的实际执行者,它包含了许多关键的成员变量,这些变量对于理解其工作原理至关重要 。其中,ActivityThread mMainThread 变量持有应用程序的主线程对象 。ActivityThread 是应用程序的入口点,负责管理应用程序的生命周期、消息循环等重要任务 。mMainThread 在 ContextImpl 中起到了桥梁的作用,它使得 ContextImpl 能够与应用程序的主线程进行交互,例如在主线程中执行一些操作,或者将任务发送到主线程的消息队列中 。

LoadedApk mPackageInfo 变量则包含了与应用程序包相关的信息 。LoadedApk 代表一个已加载的应用程序包,它包含了应用程序的资源、类加载器、组件信息等 。通过 mPackageInfo,ContextImpl 可以访问应用程序的各种资源和组件,例如获取字符串资源、启动 Activity 等操作都依赖于 mPackageInfo 提供的信息 。

在实现 Context 的抽象方法方面,ContextImpl 可谓是 "亲力亲为" 。例如,当我们调用 Context 的 getResources () 方法获取资源时,实际上是调用了 ContextImpl 中的 getResources () 方法 。在 ContextImpl 中,该方法会通过 mPackageInfo 获取应用程序的资源对象,并返回给调用者 。代码示例如下:

java 复制代码
@Override
public Resources getResources() {
    if (mResources == null) {
        mResources = mResourcesManager.getTopLevelResources(mPackageInfo.getResDir(),
                Display.DEFAULT_DISPLAY, null, mCompatibilityInfo, getActivityToken());
    }
    return mResources;
}

在上述代码中,首先检查 mResources 是否为空,如果为空则通过 mResourcesManager 获取顶级资源对象,并将其赋值给 mResources 。最后返回 mResources,从而实现了获取资源的功能 。

再比如,当我们调用 Context 的 startActivity (Intent intent) 方法启动 Activity 时,ContextImpl 会对 Intent 进行一系列的处理,包括检查权限、查找目标 Activity 等 。然后通过 ActivityThread 将启动 Activity 的请求发送到系统中 。代码示例如下:

javajava 复制代码
@Override
public void startActivity(Intent intent) {
    warnIfCallingFromSystemProcess();
    String resolvedType = intent.resolveTypeIfNeeded(getContentResolver());
    try {
        intent.prepareToLeaveProcess(this);
        mMainThread.getInstrumentation().execStartActivity(
                getOuterContext(), mMainThread.getApplicationThread(), null,
                (Activity) null, intent, -1, resolvedType);
    } catch (RemoteException e) {
        throw new RuntimeException("Failure from system", e);
    }
}

在这段代码中,首先对 Intent 进行一些准备工作,然后通过 mMainThread 获取 Instrumentation 对象,并调用其 execStartActivity () 方法来启动 Activity 。可以看出,ContextImpl 在启动 Activity 的过程中起到了关键的协调和执行作用 。

(三)ContextWrapper 类解析

ContextWrapper 类的核心机制是通过内部的 mBase 对象将方法调用委托给 ContextImpl 来实现 。在 ContextWrapper 的构造函数中,会传入一个 Context 对象,实际上这个对象就是 ContextImpl 的实例 。当我们调用 ContextWrapper 的任何方法时,它都会将该方法调用转发给 mBase 对象 。例如,当我们调用 ContextWrapper 的 getResources () 方法时,其内部实现如下:

java 复制代码
@Override
public Resources getResources() {
    return mBase.getResources();
}

从上述代码可以清晰地看到,ContextWrapper 只是简单地将 getResources () 方法的调用转发给了 mBase 对象,而 mBase 对象实际上是 ContextImpl 的实例,因此最终会调用 ContextImpl 中的 getResources () 方法来获取资源 。

这种委托机制使得 ContextWrapper 在不改变 ContextImpl 核心功能的前提下,能够方便地对 Context 的功能进行拓展 。我们可以通过继承 ContextWrapper 类,并在子类中重写一些方法,来实现对 Context 功能的定制 。例如,我们可以创建一个自定义的 ContextWrapper 子类,在其中添加一些额外的逻辑:

java 复制代码
public class CustomContextWrapper extends ContextWrapper {
    public CustomContextWrapper(Context base) {
        super(base);
    }
    @Override
    public Resources getResources() {
        // 在调用父类方法之前或之后添加自定义逻辑
        Log.d("CustomContextWrapper", "Before getting resources");
        Resources resources = super.getResources();
        Log.d("CustomContextWrapper", "After getting resources");
        return resources;
    }
}

在上述代码中,我们创建了一个 CustomContextWrapper 类,继承自 ContextWrapper 。并重写了 getResources () 方法,在调用父类方法前后添加了一些日志输出,以展示如何在不改变原有功能的基础上进行功能拓展 。在实际使用中,我们可以使用 CustomContextWrapper 来包装 ContextImpl 对象,从而实现对 Context 功能的定制:

java 复制代码
ContextImpl contextImpl = new ContextImpl();
CustomContextWrapper customContextWrapper = new CustomContextWrapper(contextImpl);
Resources resources = customContextWrapper.getResources();

通过这种方式,我们既利用了 ContextImpl 的核心功能,又通过 ContextWrapper 实现了功能的拓展和定制 。

(四)ContextThemeWrapper 类解析

ContextThemeWrapper 类的主要特性与主题相关 。它内部包含了与主题相关的接口和方法,用于管理和应用主题资源 。在 Android 中,主题是一种用于定义应用程序或 Activity 外观和风格的资源集合,包括颜色、字体、样式等 。ContextThemeWrapper 为 Activity 提供了设置和管理主题的能力 。

Activity 继承自 ContextThemeWrapper,这是因为 Activity 具有界面展示的特性,需要有自己的主题来定义其外观和风格 。通过继承 ContextThemeWrapper,Activity 可以方便地使用其中的主题相关方法 。例如,Activity 可以在其生命周期方法中通过 setTheme (int resid) 方法来设置主题 。代码示例如下:

java 复制代码
public class MainActivity extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // 设置主题
        setTheme(R.style.AppTheme);
        setContentView(R.layout.activity_main);
    }
}

在上述代码中,通过 setTheme (R.style.AppTheme) 方法将 Activity 的主题设置为 R.style.AppTheme 。这个主题资源可以在 styles.xml 文件中进行定义,例如:

xml 复制代码
<style name="AppTheme" parent="Theme.MaterialComponents.Light.NoActionBar">
    <item name="colorPrimary">@color/colorPrimary</item>
    <item name="colorPrimaryVariant">@color/colorPrimaryVariant</item>
    <item name="colorOnPrimary">@color/colorOnPrimary</item>
    <!-- 其他主题属性 -->
</style>

通过这种方式,Activity 可以根据不同的主题资源来展示不同的外观和风格,为用户提供更好的视觉体验 。ContextThemeWrapper 在 Activity 的主题管理中起到了关键的桥梁作用,使得 Activity 能够方便地与主题资源进行交互和应用 。

五、Context 的创建过程

(一)Application Context 的创建过程

当一个 Android 应用程序启动时,其入口函数是android.app.ActivityThread类的静态成员函数main 。在这个函数中,主要完成了两件关键的事情:一是创建一个ActivityThread对象,并调用它的成员函数attach向ActivityManagerService(AMS)发送一个进程启动完成的通知;二是调用Looper类的静态成员函数prepareMainLooper创建一个消息循环,并在向 AMS 发送启动完成通知之后,使当前进程进入到这个消息循环中 。

在attach方法中,当system参数为false(表示非系统进程)时,会通过mgr.attachApplication(mAppThread)向 AMS 发送一个进程间通讯消息 。AMS 接收到消息后,会通过远程调用到ApplicationThread的bindApplication方法 。该方法的参数中有一个ApplicationInfo类型的数据,它是由 AMS 创建的,并通过 IPC 传递到ActivityThread中 。在bindApplication方法中,会用这些参数构造一个本地AppBindData数据类,然后调用handleBindApplication方法,并将AppBindData作为参数传入 。

在handleBindApplication方法中,首先会使用data.info = getPackageInfoNoCheck(data.appInfo, data.compatInfo)方法为info变量赋值 。这里的getPackageInfoNoCheck方法会根据AppBindData中的ApplicationInfo中的packageName创建一个LoadedApk对象,并把这个对象设置为弱引用 。接着,会调用LoadedApk类的makeApplication方法来创建一个Application对象 。在LoadedApk类中,会把创建的Application对象保存在mApplication变量中,返回这个变量又会赋值给ActivityThread类中的mInitialApplication变量 。通常情况下,这两个变量的值是一样的 。

在创建Application对象的过程中,Context也随之创建 。具体来说,LoadedApk的makeApplication方法中会创建Application的Context 。它通过ContextImpl.createAppContext(this, getSystemContext().mPackageInfo)来创建ContextImpl对象,这个ContextImpl对象就是Application的Context 。然后,mInitialApplication = context.mPackageInfo.makeApplication(true, null)会创建Application对象,并将之前创建的Context与Application关联起来 。最后,调用mInitialApplication.onCreate()来初始化Application 。

(二)Activity Context 的创建过程

当请求启动一个Activity时,ActivityManagerService会通过 IPC 调用到ActivityThread类的scheduleLaunchActivity方法 。该方法有一个ActivityInfo类型的参数,这是一个实现了Parcelable接口的数据类,由 AMS 创建,并通过 IPC 传递到ActivityThread中 。然后,使用这些传入的参数构建一个ActivityClientRecord对象r,ActivityThread内部会为每个Activity创建一个ActivityClientRecord对象,并使用这些数据对象来管理Activity 。

scheduleLaunchActivity方法会通过H类发送一个LAUNCH_ACTIVITY消息 。H类继承于Handler,是ActivityThread的内部类,主要用来在ActivityThread内部分发消息 。当H类接收到LAUNCH_ACTIVITY消息后,会调用ActivityThread类的handleLaunchActivity方法来继续处理 。

handleLaunchActivity方法首先调用performLaunchActivity方法,这个方法主要用于创建Application Context和Activity Context 。在performLaunchActivity方法中,首先会调用getPackageInfo方法来获取一个LoadedApk对象 。如果r.packageInfo为空,会通过getPackageInfo(aInfo.applicationInfo, r.compatInfo, Context.CONTEXT_INCLUDE_CODE)获取 。接着调用createBaseContextForActivity方法来创建Activity的Context实例 。

createBaseContextForActivity方法通过调用ContextImpl.createActivityContext(this, r.packageInfo, r.activityInfo, r.token, displayId, r.overrideConfig)来创建ContextImpl类实例对象appContext 。这里的createActivityContext方法实际上就是创建一个ContextImpl实例 。创建完Context后,会通过反射创建Activity实例 。具体来说,使用java.lang.ClassLoader cl = appContext.getClassLoader(); activity = mInstrumentation.newActivity(cl, component.getClassName(), r.intent),其中cl是类加载器,component.getClassName()是Activity子类的名字,通过反射创建一个Activity类的实例 。

之后,调用LoadedApk对象的makeApplication方法来创建Application实例 。由于进程启动的时候可能已经创建了Application对象并保存在LoadedApk对象的mApplication变量中,这里会直接返回这个Application对象 。但如果LoadedApk对象是弱引用保存的且被回收了,就需要重新创建Application对象 。

最后,对创建好的Activity进行一系列的初始化操作 。例如,appContext.setOuterContext(activity)将Activity与Context关联起来,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, window, r.configCallback)对Activity进行初始化,包括设置Context、Instrumentation、Intent等相关信息 。

六、Context 的使用场景与最佳实践

(一)常见使用场景举例

  1. 弹出 Toast:Toast 是 Android 中用于在屏幕上显示简短提示信息的组件,在使用 Toast 时需要传入 Context 作为参数 。示例代码如下:
java 复制代码
Context context = getApplicationContext();
Toast.makeText(context, "This is a Toast", Toast.LENGTH_SHORT).show();

在上述代码中,getApplicationContext()获取了应用程序的上下文,Toast.makeText(context, "This is a Toast", Toast.LENGTH_SHORT)创建了一个 Toast 对象,其中第一个参数为 Context,第二个参数是要显示的文本内容,第三个参数是显示时长 。最后通过show()方法将 Toast 显示出来 。需要注意的是,Toast 只能在 UI 线程中显示,如果在非 UI 线程中使用 Toast,会抛出异常 。因此,在多线程环境下使用 Toast 时,需要通过 Handler 将显示 Toast 的操作切换到 UI 线程 。例如:

java 复制代码
Handler handler = new Handler(Looper.getMainLooper());
handler.post(new Runnable() {
    @Override
    public void run() {
        Toast.makeText(context, "This is a Toast in non - UI thread", Toast.LENGTH_SHORT).show();
    }
});
  1. 创建 Dialog:Dialog 是 Android 中用于显示对话框的组件,创建 Dialog 时也需要传入 Context 。示例代码如下:
java 复制代码
Context context = this;
Dialog dialog = new Dialog(context);
dialog.setContentView(R.layout.dialog_layout);
dialog.show();

在上述代码中,this表示当前 Activity 的 Context,使用这个 Context 创建了一个 Dialog 对象 。然后通过setContentView(R.layout.dialog_layout)设置了 Dialog 的布局,最后通过show()方法显示 Dialog 。这里需要注意的是,创建 Dialog 时应尽量使用 Activity Context,而不是 Application Context 。因为 Dialog 是依赖于 Activity 的窗口,如果使用 Application Context 创建 Dialog,可能会出现BadTokenException异常,提示Unable to add window -- token null is not valid 。这是因为 Application Context 没有与具体的 Activity 相关联,无法提供有效的窗口令牌 。只有 Activity Context 才能保证 Dialog 与正确的窗口管理器和 Activity 生命周期进行交互 。

  1. 启动 Activity:启动 Activity 是 Context 的常见使用场景之一 。示例代码如下:
java 复制代码
Context context = this;
Intent intent = new Intent(context, AnotherActivity.class);
startActivity(intent);

在上述代码中,this表示当前 Activity 的 Context,通过这个 Context 创建了一个 Intent 对象,指定要启动的目标 Activity 为AnotherActivity.class 。然后使用startActivity(intent)方法启动目标 Activity 。如果需要传递数据给目标 Activity,可以使用intent.putExtra(key, value)方法 。例如:

java 复制代码
Intent intent = new Intent(context, AnotherActivity.class);
intent.putExtra("message", "Hello, AnotherActivity!");
startActivity(intent);

在目标 Activity 中,可以通过getIntent().getStringExtra("message")获取传递过来的数据 。需要注意的是,当使用非 Activity 类型的 Context(如 Application Context)启动 Activity 时,需要在 Intent 中添加FLAG_ACTIVITY_NEW_TASK标志 。这是因为 Activity 的启动需要在一个任务栈中进行,而 Application Context 没有与之关联的任务栈 。添加FLAG_ACTIVITY_NEW_TASK标志后,系统会为启动的 Activity 创建一个新的任务栈 。例如:

java 复制代码
Context context = getApplicationContext();
Intent intent = new Intent(context, AnotherActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);

(二)避免内存泄漏

在 Android 开发中,不正确使用 Context 是导致内存泄漏的常见原因之一 。例如,当一个静态变量持有 Activity Context 时,由于静态变量的生命周期与应用程序相同,而 Activity 的生命周期是有限的 。如果 Activity 被销毁后,静态变量仍然持有其 Context,那么这个 Activity 及其相关资源将无法被垃圾回收器回收,从而导致内存泄漏 。示例代码如下:

java 复制代码
public class MemoryLeakExample {
    private static Context sContext;
    public MemoryLeakExample(Context context) {
        sContext = context;
    }
}

在上述代码中,MemoryLeakExample类中的静态变量sContext持有了传入的 Context 。如果在 Activity 中创建了MemoryLeakExample对象并传入 Activity Context,那么即使 Activity 被销毁,sContext仍然会持有该 Activity 的引用,导致 Activity 无法被回收 。

为了避免这种内存泄漏,应尽量避免让长生命周期的对象持有短生命周期的 Activity Context 。如果需要在长生命周期的对象中使用 Context,可以考虑使用 Application Context,因为 Application Context 的生命周期与应用程序相同 。例如,修改上述代码如下:

java 复制代码
public class NoMemoryLeakExample {
    private static Context sContext;
    public NoMemoryLeakExample(Context context) {
        sContext = context.getApplicationContext();
    }
}

在上述代码中,使用context.getApplicationContext()获取 Application Context 并赋值给sContext,这样就避免了持有 Activity Context 导致的内存泄漏 。

此外,在使用内部类和匿名类时也需要注意内存泄漏问题 。非静态内部类和匿名类会隐式持有外部类的引用,如果外部类是 Activity,并且内部类的生命周期比 Activity 长,就可能导致 Activity 无法被回收 。例如:

java 复制代码
public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        new Thread(new Runnable() {
            @Override
            public void run() {
                // 模拟长时间运行的任务
                while (true) {
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }).start();
    }
}

在上述代码中,new Thread(new Runnable() {... })创建了一个匿名内部类,它隐式持有了MainActivity的引用 。由于线程会一直运行,导致MainActivity在销毁时无法被回收,从而造成内存泄漏 。

为了解决这个问题,可以使用静态内部类,并通过弱引用持有外部 Activity 的引用 。例如:

java 复制代码
public class MainActivity extends AppCompatActivity {
    private static class MyRunnable implements Runnable {
        private final WeakReference<MainActivity> mActivityRef;
        public MyRunnable(MainActivity activity) {
            mActivityRef = new WeakReference<>(activity);
        }
        @Override
        public void run() {
            MainActivity activity = mActivityRef.get();
            if (activity != null) {
                // 执行与Activity相关的操作
            }
            // 模拟长时间运行的任务
            while (true) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        new Thread(new MyRunnable(this)).start();
    }
}

在上述代码中,MyRunnable是一个静态内部类,通过WeakReference持有MainActivity的弱引用 。这样,当MainActivity被销毁时,弱引用不会阻止其被回收,从而避免了内存泄漏 。

(三)正确获取和使用 Context

  1. 在 Activity 中:在 Activity 内部,可以直接使用this关键字来获取 Activity Context 。例如:
java 复制代码
Context context = this;

这是最常用的获取 Context 的方式,适用于与当前 Activity 相关的操作,如启动新的 Activity、弹出 Dialog、访问当前 Activity 特有的资源等 。此外,也可以通过getApplicationContext()方法获取 Application Context 。但需要注意的是,Application Context 与 Activity Context 的生命周期和功能有所不同,应根据具体需求选择使用 。

  1. 在 Fragment 中:在 Fragment 中获取 Context 有多种方式 。可以通过getActivity()方法获取其所属 Activity 的 Context 。示例代码如下:
java 复制代码
Context context = getActivity();
if (context != null) {
    // 使用context进行相关操作
}

需要注意的是,在 Fragment 的onDetach()方法被调用后,getActivity()可能会返回null,此时就不能再使用getActivity()来获取 Context 了 。此外,也可以通过requireActivity()方法获取 Activity Context,该方法与getActivity()类似,但如果 Fragment 没有依附于 Activity,会抛出异常 。另外,还可以使用getContext()方法获取 Context,这个方法在 Fragment 依附于 Activity 时返回 Activity Context,在 Fragment 被分离时返回null 。例如:

java 复制代码
Context context = getContext();
if (context != null) {
    // 使用context进行相关操作
}
  1. 在非 UI 类中:在非 UI 类中获取 Context 可以通过构造函数传入 。例如,在一个工具类中:
java 复制代码
public class Utils {
    private Context mContext;
    public Utils(Context context) {
        mContext = context;
    }
    // 使用mContext进行相关操作
}

在使用这个工具类时,可以传入 Activity Context 或 Application Context 。如果工具类中的操作与具体的 Activity 无关,建议传入 Application Context,以避免因持有 Activity Context 导致的内存泄漏问题 。另外,也可以通过自定义 Application 类来获取 Application Context 。首先,创建一个继承自 Application 的类,在其中保存 Application Context 。例如:

java 复制代码
public class MyApplication extends Application {
    private static Context sContext;
    @Override
    public void onCreate() {
        super.onCreate();
        sContext = this;
    }
    public static Context getContext() {
        return sContext;
    }
}

然后在非 UI 类中,可以通过MyApplication.getContext()获取 Application Context 。例如:

java 复制代码
Context context = MyApplication.getContext();

七、总结

Context 作为 Android 开发中的核心概念,贯穿于应用程序的各个方面,其重要性不言而喻。它不仅为应用程序提供了访问资源、启动组件和获取系统服务的能力,还在组件之间的交互和通信中发挥着关键作用 。通过本文的详细介绍,我们深入了解了 Context 的类型、作用、实现原理、创建过程以及使用场景与最佳实践 。

在类型方面,Context 主要包括 Application Context、Activity Context、Service Context 和 BroadcastReceiver Context,每种类型都有其独特的生命周期和适用场景 。在作用上,它涵盖了资源访问、组件启动、系统服务获取以及其他诸如包名获取、文件操作、SharedPreferences 操作等重要功能 。实现原理上,Context 通过继承结构,由 ContextImpl 实现具体功能,ContextWrapper 进行功能扩展,ContextThemeWrapper 负责主题相关的管理 。创建过程中,Application Context 和 Activity Context 的创建都有其特定的流程和机制 。在使用场景中,我们了解了常见的使用场景,如弹出 Toast、创建 Dialog、启动 Activity 等,同时也掌握了避免内存泄漏和正确获取与使用 Context 的方法 。

相关推荐
robotx5 分钟前
安卓线程相关
android
消失的旧时光-194326 分钟前
Android 面试高频:JSON 文件、大数据存储与断电安全(从原理到工程实践)
android·面试·json
dalancon1 小时前
VSYNC 信号流程分析 (Android 14)
android
dalancon1 小时前
VSYNC 信号完整流程2
android
dalancon2 小时前
SurfaceFlinger 上帧后 releaseBuffer 完整流程分析
android
用户69371750013843 小时前
不卷AI速度,我卷自己的从容——北京程序员手记
android·前端·人工智能
程序员Android3 小时前
Android 刷新一帧流程trace拆解
android
墨狂之逸才4 小时前
解决 Android/Gradle 编译报错:Comparison method violates its general contract!
android
阿明的小蝴蝶4 小时前
记一次Gradle环境的编译问题与解决
android·前端·gradle
汪海游龙5 小时前
开源项目 Trending AI 招募 Google Play 内测人员(12 名)
android·github