使用 Context
英文文档原地址:戳这里
概览
Context 提供了有关于获取、读写程序状态信息的渠道。通常是活动 Activity
、碎片 Fragment 、服务 Service访问它们的资源文件、布局信息、主题、样式、以及 Android 设备的外部目录空间。通过它也可以访问 Android 内部服务,比如布局加载器(LayoutInflater),键盘,和文件共享器(Content Provider)。
在调用某些需要 Context 的方法中,我们通常会直接传入当前 Activity 的实例。例如我们把 Context 对象传入到 Activity 需要(依赖)的对象中去,比如适配器 (Adapter)、碎片 (Fragment);又如我们在无关 Activity 的类中,使用 getApplicationContext() 获取应用对应的 Context。
Context 到底是干什么的?
下面是几个需要 Context 到场合:
明确的启动一个组件
//如果MainActivity是一个内部组件,那么传入 context.
Intent intent = new Intent(context,MainActivity.class);
startActivity(intent);
当我们明确的启动一个组件时,我们需要提供两个信息:
- 详细包名,它指明了包含了该组件的应用。
- 组件 Class 的全名。
如果启动的是一个内部组件,那么可以直接传入Context,因为当前应用的详细包名可以通过调用context.gerPackageName()获取到。
创建一个视图(View)
TextView textview = new TextView(context);
创建一个 View 需要提供以下信息:
- 设备的屏幕分辨率和将 dp/sp 转换为像素的比例
- 样式属性
-
Activity的onClick方法的引用
而Context可以提供这些信息。
加载一个 XML 布局文件
通过传入 Context 获取一个布局加载器(LayoutInflater),之后通过布局加载器加载 XML 布局文件。
LayoutInflater inflater = LayoutInflater.from(context);
inflater.inflate(R.layout.mylayout,parent);
发送一个本地广播(Broadcast)
通过 Context 获取一个广播管理器(LocalBroadcastManager)。然后通过它发送一个本地广播
//The context contains a reference to the main Looper,
//which manages the queue for the application's main thread.
Intent broadcastIntent = new Intent("custom-action");
LocalBroadcastManager.getInstance(context).sendBroadcast(broadcastIntent);
获取系统服务
从应用中发送一个通知信息(Notification),需要传入 Context。
// Context objects are able to fetch or start system services.
NotificationManager notificationManager =
(NotificationManager) getSystemService(NOTIFICATION_SERVICE);
int notificationId = 1;
// Context is required to construct RemoteViews
Notification.Builder builder =
new Notification.Builder(context).setContentTitle("custom title");
notificationManager.notify(notificationId, builder.build());
Application 和 Activity 的区别是什么?
样式和主题通常会统一嵌入在整个应用中,但是也可以为某个 Activity 设定一个不同的主题(例子:在某些特定的 Activity 中关闭 ActionBar 控件)。你可能注意到在 AndroidMainfest.xml 文件中 Application 标签通常有一个名为 android:theme 的属性,我们同样可以给 Activity 标签添加这个属性。
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme" >
<activity
android:name=".MainActivity"
android:label="@string/app_name"
android:theme="@style/MyCustomTheme">
从这个角度考虑,你需要知道一点—— Application 和 Activity 拥有各自的 Context 对象,并且它们对生命周期是不同的。某个 Activity 内的 View 控件应该传入对应的 Context 以获取对应的主题,样式文件。如果一个 Activity 没有明确表明它的主题属性,那么就会使用 Application 的主题资源。
大部分情况下你都应该使用 Activity 的 Context 对象。通常情况下,关键字 this 指向当前的 Class 的实例,可以当作一个 Context 来使用。下面的例子展示了如何通过这种方法显示一个吐司( Toast );
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Toast.makeText(this, "hello", Toast.LENGTH_SHORT).show();
}
}
匿名函数
当你实现一个匿名内部类作为参数传入一个方法时,在类内部,关键字 this 就不再是指向当前 Activity 的引用了。它将指向最近的一个类的实例。在下面的代码段中,必须通过类名来显示传入一个 Context 。
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
TextView tvTest = (TextView) findViewById(R.id.abc);
tvTest.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Toast.makeText(MainActivity.this, "hello", Toast.LENGTH_SHORT).show();
}
});
}
}
适配器( Context )
ArrayAdapter
当创建一个适配器给 ListView 时,通常会在布局加载期间,通过调用 getContext() 方法获得 Context :
if (convertView == null) {
convertView = LayoutInflater.from(getContext())
.inflate(R.layout.item_user, parent, false);
}
如果你在这里使用了错误的 Context 对象,那么将无法正确加载主题样式。如此考虑,你应该正确使用 Context 。
RecyclerView Adapter
public class MyRecyclerAdapter extends RecyclerView.Adapter<MyRecyclerAdapter.ViewHolder> {
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View v =
LayoutInflater
.from(parent.getContext())
.inflate(R.layout.itemLayout, parent, false);
return new ViewHolder(v);
}
@Override
public void onBindViewHolder(ViewHolder viewHolder, int i) {
// If a context is needed, it can be retrieved
// from the ViewHolder's root view.
Context context = viewHolder.itemView.getContext();
// Dynamically add a view using the context provided.
if(i == 0) {
TextView tvMessage = new TextView(context);
tvMessage.setText("Only displayed for the first item.")
viewHolder.customViewGroup.addView(tvMessage);
}
}
public static class ViewHolder extends RecyclerView.ViewHolder {
public FrameLayout customViewGroup;
public ViewHolder(view imageView) {
// Very important to call the parent constructor
// as this ensures that the imageView field is populated.
super(imageView);
// Perform other view lookups.
customViewGroup = (FrameLayout) imageView.findById(R.id.customViewGroup);
}
}
}
ArrayAdapter 的构造方法需要传入一个 Context 对象,而 RecyclerView 则不需要。在加载时,它能够正确的获取到 Context 。关联的 RecyclerView 能够将自己当作参数传送给 RecyclerView.Adapter.onCreateViewHolder()。
避免内存泄漏
在使用单例设计模式时通常传入 Application的 Context 。当你创建了一个管理类,通过持有 Context 来获取系统信息时,如果使用了 Activity 的 Context ,将会获得若干个不同的 Context 。当 Activity 持有了 Context 的引用时,静态引用不会随着 Activity 的销毁而同时释放,如此便会引发内存泄漏问题。
下面是一个例子,如果一个 Activity 的 Context 被一个静态管理类持有引用,那么当 Activity 释放时, Context 无法被释放。因为静态管理类持有对它的引用。
public class CustomManager {
private static CustomManager sInstance;
public static CustomManager getInstance(Context context) {
if (sInstance == null) {
// This class will hold a reference to the context
// until it's unloaded. The context could be an Activity or Service.
sInstance = new CustomManager(context);
}
return sInstance;
}
private Context mContext;
private CustomManager(Context context) {
mContext = context;
}
}
正确使用 Context - 使用 Application Context
避免内存泄漏的方法是,不让生命周期长的对象持有生命周期较短的 Context 引用。检查后台线程,handler,内部类确保不会发生这种情况。
正确使用 Context 的方式是通过 Application 来创建管理类。它被生命周期为整个应用的进程,全局只有一个实例。
public static CustomManager getInstance(Context context) {
if (sInstance == null) {
// When storing a reference to a context, use the application context.
// Never store the context itself, which could be a component.
sInstance = new CustomManager(context.getApplicationContext());
}
return sInstance;
}