背景
最近项目组在开发一个供公司内部其他项目组集成的sdk,该sdk需要以ui的各种形式(Activity
、Dialog
、View
)向外输出具体功能。想到各种展现形式都是基于一个自定义View
(Activity-View
、Dialog-View
、View
),所以应该把逻辑都集成到自定义View
中实现才好(具体实现是采用了MVP模式开发的,业务逻辑放在了Presenter
,展示在自定义View
),此时才能保持逻辑的统一性。在开发过程中碰到这样的场景:自定义View
需要在宿主Activity
的onStart()
中开始加载,在onDestory()
中去释放资源等操作,此处该如何实现呢?
解决方案一:
在自定义View
(下文统一称TestView
)中暴露出接口函数onStart()
、onStop()
、onDestory()
,并在相应函数中做具体业务逻辑操作,然后在宿主Activity
的生命周期函数中调用TestView
相应的接口函数,从而达到TestView
与Activity
的生命周期同步的效果。具体实现如下:
//自定义View
public class TestView extends FrameLayout {
private Presenter mPresenter;
public void onStart() {
if (mPresenter != null) {
mPresenter.onStart();
}
}
public void onStop() {
if (mPresenter != null) {
mPresenter.onStop();
}
}
public void onDestory() {
if (mPresenter != null) {
mPresenter.onDestory();
}
}
}
//模拟集成方的Activity接入
public class MainActivity extends AppCompatActivity {
private TestView mTestView;
@Override
protected void onStart() {
super.onStart();
if (mTestView != null) {
mTestView.onStart();
}
}
@Override
protected void onStop() {
super.onStop();
if (mTestView != null) {
mTestView.onStop();
}
}
@Override
protected void onDestroy() {
super.onDestroy();
if (mTestView != null) {
mTestView.onDestory();
}
}
}
该方案确实可以达到保持TestView
和宿主Activity
的生命周期同步的效果。但弊端是:接入方 必须 在自己的Activity
的生命周期函数中调用TestView
的相应函数,否则会存在泄露的风险,甚至是导致逻辑混乱。这样给sdk内部逻辑的封装带来了风险,有没有办法让TestView
能自主监听宿主Activity
的生命周期呢?于是有了接下来的方案二。
解决方案二:
想到sdk的这个应用场景,跳出来的第一个瞬间是Glide.with(Activity)
就是实现了监听Activity
生命周期的场景,只不过他只是用来管理request
请求。于是顺着with()
函数看了下源码,也在网上查资料验证实现方式,Glide
总的实现方案是在with()
传入的Activity
上添加了一个空白的Fragment
来监听Activity
的生命周期来实现的(具体实现方式可以参考http://blog.csdn.net/u013510838/article/details/52143097此处的分析)。于是照葫芦画瓢,有了我们的第二种方案,具体实现如下:
//生命周期回调接口
public interface LifeListener {
void onCreate(Bundle bundle);
void onStart();
void onResume();
void onPause();
void onStop();
void onDestroy();
}
//sdk输出自定义View
public class TestView extends FrameLayout {
private final String TAG = "TestView";
//省略构造函数之类...
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
Activity activity = getActivity();
if (activity != null) {
addLifeListener(activity);
}
}
//获取宿主Activity,此处是否有问题?
private Activity getActivity() {
final Context context = getContext();
if (context != null && context instanceof Activity) {
return (Activity) context;
}
return null;
}
private void addLifeListener(Activity activity) {
LifeListenerFragment fragment = getLifeListenerFragment(activity);
fragment.addLifeListener(mLifeListener);
}
private LifeListenerFragment getLifeListenerFragment(Activity activity) {
FragmentManager manager = activity.getFragmentManager();
return getLifeListenerFragment(manager);
}
//添加空白fragment
private LifeListenerFragment getLifeListenerFragment(FragmentManager manager) {
LifeListenerFragment fragment = (LifeListenerFragment) manager.findFragmentByTag(TAG);
if (fragment == null) {
fragment = new LifeListenerFragment();
manager.beginTransaction().add(fragment, TAG).commitAllowingStateLoss();
}
return fragment;
}
private LifeListener mLifeListener = new LifeListener() {
@Override
public void onCreate(Bundle bundle) {
Log.d(TAG, "onCreate");
}
@Override
public void onStart() {
Log.d(TAG, "onStart");
}
@Override
public void onResume() {
Log.d(TAG, "onResume");
}
@Override
public void onPause() {
Log.d(TAG, "onPause");
}
Log.d(TAG, "onStop");
}
@Override
public void onDestroy() {
Log.d(TAG, "onDestroy");
}
};
}
//空白Fragment
public class LifeListenerFragment extends Fragment {
private LifeListener mLifeListener;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
public void addLifeListener(LifeListener listener) {
mLifeListener = listener;
}
public void removeLifeListener() {
mLifeListener = null;
}
@Override
public void onStart() {
super.onStart();
if (mLifeListener != null) {
mLifeListener.onStart();
}
}
@Override
public void onStop() {
super.onStop();
if (mLifeListener != null) {
mLifeListener.onStop();
}
}
@Override
public void onResume() {
super.onResume();
if (mLifeListener != null) {
mLifeListener.onResume();
}
}
@Override
public void onDestroy() {
super.onDestroy();
if (mLifeListener != null) {
mLifeListener.onDestroy();
}
}
}
xml文件
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/view_main"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.test.life.TestView
android:layout_width="match_parent"
android:layout_height="200dp"
android:background="#50ff0000" />
</RelativeLayout>
测试成功!输出结果如下:
11-16 10:14:23.762 D/TestView(16409): onStart
11-16 10:14:23.762 D/TestView(16409): onResume
11-16 10:14:26.090 D/TestView(16409): onStop
11-16 10:14:26.091 D/TestView(16409): onDestroy
正当在demo测试成功而沾沾自喜时,表示想重新review了一下代码,表示对View.getContext()
的返回值到底是不是Activity
有质疑,毕竟还是很少看到在View
中的这个强转Activity
的用法,为了程序的健壮性,决定切换各种场景来测试一番:
- 将
TestView
直接写入xml布局文件,然后Activity
通过setContentView()
加载时(上述场景)已验证通过,确实是Activity。 - 将
TestView
写在一个单独的xml文件中,然后使用inflate()
方式加载:
方式一:LayoutInflater.from(MainActivity.this).inflate(R.layout.layout_test, (ViewGroup) getWindow().getDecorView());
在TestView中getContext()
,确实是Activity
。
方式二:LayoutInflater.from(getApplicationContext()).inflate(R.layout.layout_test, (ViewGroup) getWindow().getDecorView());
在TestView
中getContext()
,竟然是ApplicationContext
,导致注册监听失败。
发现TestView
的mContext
的具体类型,取决于LayoutInflater
的from()
参数?!。 此处后续专门花时间梳理一下看看原因,总之,这里是有可能不是Activity
的。 - 使用
new
的方式添加TestView
:
//首先添加一个父布局
FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT,
FrameLayout.LayoutParams.MATCH_PARENT);
FrameLayout parent = new FrameLayout(getApplicationContext());
parent.setBackgroundColor(0x55ff0000);
addContentView(parent, params);
//添加TestView
TestView view = new TestView(getApplicationContext());
view.setBackgroundColor(0x5500ff00);
parent.addView(view, new FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, 200));
TestView
中getContext()
,这种方式仍然是ApplicationContext
。
思考:这个监听我是在TestView
的onAttachedToWindow()
接口中设置的,说明此时TestView
一定是添加到了Activity
上了,如果一直向上查找parent
,应该是可以找到一个rootView
是在创建Activity
时add进去的。于是修改getActivity()
函数,并验证:
private Activity getActivity() {
View parent = this;
Activity activity = null;
do {
final Context context = parent.getContext();
Log.d(TAG, "view: " + parent + ", context: " + context);
if (context != null && context instanceof Activity) {
activity = (Activity) context;
break;
}
} while ((parent = (View) parent.getParent()) != null);
return activity;
}
往上查找确实找到了系统的布局文件FrameLayout
包含Activity
类型的Context
,打印结果如下:
11-16 17:40:48.297 D/TestView(25739): view: com.test.life.TestView, context: android.app.Application@39f059d
11-16 17:40:48.297 D/TestView(25739): view: android.widget.FrameLayout, context: android.app.Application@39f059d
11-16 17:40:48.297 D/TestView(25739): view: android.support.v7.widget.ContentFrameLayout, context: android.support.v7.view.ContextThemeWrapper@c64f72d
11-16 17:40:48.297 D/TestView(25739): view: android.support.v7.widget.ActionBarOverlayLayout, context: android.support.v7.view.ContextThemeWrapper@c64f72d
11-16 17:40:48.297 D/TestView(25739): view: android.widget.FrameLayout, context: com.test.life.MainActivity@270fe67
总结
综上所述,View
中的mContext
变量,不一定是Activity
!!这里后续开发要注意了,比如拿View.getContext().startActivity()
时要注意了flag
标记了、或者在View
中使用Glide.with().load()
时有可能不能监听到Activity
的生命周期。他的取值有可能是Application
、ContextThemeWrapper
,甚至是TintContextWrapper
(AppCompat
系列会存在这个转换)。
附上View
中,getContext()
函数:
//View中的getContext()有这样的注释:意思是通过context可以来获取theme,resource等
/**
* Returns the context the view is running in, through which it can
* access the current theme, resources, etc.
*
* @return The view's Context.
*/
@ViewDebug.CapturedViewProperty
public final Context getContext() {
return mContext;
}