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

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

推荐阅读更多精彩内容