在View中监听宿主Activity的生命周期实现

背景

最近项目组在开发一个供公司内部其他项目组集成的sdk,该sdk需要以ui的各种形式(ActivityDialogView)向外输出具体功能。想到各种展现形式都是基于一个自定义ViewActivity-ViewDialog-ViewView),所以应该把逻辑都集成到自定义View中实现才好(具体实现是采用了MVP模式开发的,业务逻辑放在了Presenter,展示在自定义View),此时才能保持逻辑的统一性。在开发过程中碰到这样的场景:自定义View需要在宿主ActivityonStart()中开始加载,在onDestory()中去释放资源等操作,此处该如何实现呢?

解决方案一:

在自定义View(下文统一称TestView)中暴露出接口函数onStart()onStop()onDestory(),并在相应函数中做具体业务逻辑操作,然后在宿主Activity的生命周期函数中调用TestView相应的接口函数,从而达到TestViewActivity的生命周期同步的效果。具体实现如下:

//自定义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文件


    

测试成功!输出结果如下:

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的用法,为了程序的健壮性,决定切换各种场景来测试一番:

  1. TestView直接写入xml布局文件,然后Activity通过setContentView()加载时(上述场景)已验证通过,确实是Activity。
  2. 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());TestViewgetContext(),竟然是ApplicationContext,导致注册监听失败。
    发现TestViewmContext的具体类型,取决于LayoutInflaterfrom()参数?!。 此处后续专门花时间梳理一下看看原因,总之,这里是有可能不是Activity的。
  3. 使用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));

TestViewgetContext(),这种方式仍然是ApplicationContext

思考:这个监听我是在TestViewonAttachedToWindow()接口中设置的,说明此时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的生命周期。他的取值有可能是ApplicationContextThemeWrapper,甚至是TintContextWrapperAppCompat系列会存在这个转换)。

附上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;
    }

你可能感兴趣的:(在View中监听宿主Activity的生命周期实现)