Android中所谓的视图残留

2017-12-02  本文已影响0人  lostsearover

最近的一个项目,要实现下图所示的界面显示以及功能:


2017-12-02 20-41-06屏幕截图.png

经过搜索,知道可以通过TextView+SpannableString+ClickableSpan来实现:
具体如下:

        mCheckNet = (TextView) mNetErrorPromptContainerBeforeReady.findViewById(R.id.check_net);
        final String checkNetText = getString(R.string.net_error_hint_before_ready);
        final String clickableCheckNetText = getString(R.string.clickable_check_hint);
        final int totalLength = checkNetText.length();
        final int start = checkNetText.indexOf(clickableCheckNetText);
        final int end  = start + clickableCheckNetText.length();
        SpannableStringBuilder ssb = new SpannableStringBuilder(checkNetText);
        final int normalSize = getResources().getDimensionPixelSize(R.dimen.normal_check_net_size);
        final int bigSize = getResources().getDimensionPixelSize(R.dimen.clickable_check_net_size);
        ssb.setSpan(new TextAppearanceSpan(null, 0, normalSize, null, null), 0, start - 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
        ssb.setSpan(new TextAppearanceSpan(null, 0, normalSize, null, null), end + 1, totalLength - 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
        ssb.setSpan(new TextAppearanceSpan(null, Typeface.BOLD, bigSize, null, null), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
        //设置检查点击事件
        ssb.setSpan(new CheckNetClickListener(getContext()), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
        mCheckNet.setText(ssb);
        mCheckNet.setMovementMethod(LinkMovementMethod.getInstance());

    private static class CheckNetClickListener extends ClickableSpan {

        private Context mContext = null;
        public CheckNetClickListener(Context context) {
            mContext = context;
        }
        
        @Override
        public void onClick(View widget) {
            // 启动设置网络 Activity
            Intent intentNet = new Intent(mContext, WifiListActivity.class);
            intentNet.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
            mContext.startActivity(intentNet);
        }

        @Override
        public void updateDrawState(TextPaint ds) {
            // 下划线
            ds.setUnderlineText(true);
        }
    }

适配了所有系统语言,包括中文,阿拉伯语,俄罗斯语,西班牙语,德语,日语,韩语,英语等,一切非常顺利.
我也觉得差不多了,高高兴兴拿去给领导演示,被打脸了!!!
问题是拿去的时候显示的是中文,显示的内容如上图所示,然后我HOME键退出当前界面,切换系统语言到日语.
结果: 依然还是上图所示,而且点击 检查, 应用崩溃了. 赶紧拿回去分析原因.

主要是这个问题也不是必现的,所以一时半会儿也没有头绪.
因为我们这个布局是 Activity + ViewPager + Fragment 实现的.
有一点就是在切换系统语言之后Activity是会被重启的.意思是它的生命周期会重新从onCreate开始.
所以下一意识就怀疑是Fragment没有被及时回,造成老的Fragment依然压在新生成的Fragment之上,一顿调试没有进展.
然后因为这个是新加的功能,修改之处就是以上贴出的代码片段.最后发现去除以下片段:

        //设置检查点击事件
        ssb.setSpan(new CheckNetClickListener(getContext()), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);

问题就不能再被复现了.于是怀疑是不是ClickableSpan导致Fragment没有被回收,然后网上一搜,还真有类似的案例,仔细看下来,跟我这个不是一个问题.

然后搜索视图残留,还真有挺多.

主要是在切换系统语言的时候,Activity会被销毁,然后它的ViewState会被保存起来.但是给出的解决方案说实在不人性,主要是以下:
第一种:

     @Override
    public void onSaveInstanceState(Bundle outState) {
         // 注释掉,就可以
//        super.onSaveInstanceState(outState, outPersistentState);
    }

第二种:
主动调用FragmentMananer去移除Fragment.

这里我说下第一种,的确问题没有了,但是也带来非常不友好的体验: ListView实现界面滑动到中间后,切换语言之后,界面又重新跳到顶端.

不知道咋整了.
已经凌晨了,无奈下班打车回家了.
第二天要出版本了,都考虑摒弃ClickableSpan了.
但这时候有新发现, 大家应该知道Activity有以下方法:

    @Override
    protected void onRestoreInstanceState(Bundle savedInstanceState) {
        super.onRestoreInstanceState(savedInstanceState);
    }

同样Fragment以下方法:

    @Override
    public void onViewStateRestored(@Nullable Bundle savedInstanceState) {
        super.onViewStateRestored(savedInstanceState);
    }

于是我把savedInstanceState打印出来,对比正常和出问题时的log:
正常log:

11-30 04:58:30.672 D/Translator(15708): onViewStateRestored: LostFragment{28bcd0c #0 id=0x7f0800f8 android:switcher:2131230968:1}, 
savedInstanceState: Bundle[{android:view_state={2131230754=android.view.AbsSavedState$1@efa6742, 
2131230755=android.view.AbsSavedState$1@efa6742, 
2131230757=android.view.AbsSavedState$1@efa6742, 
2131230775=android.view.AbsSavedState$1@efa6742, ...
...
2131230922=android.support.v7.widget.RecyclerView$SavedState@1482655, 2131230946=android.view.AbsSavedState$1@efa6742,
 2131230950=android.view.AbsSavedState$1@efa6742, 

出问题时候

11-30 04:59:03.030 D/Translator(15708): onViewStateRestored: LostFragment{759c4b2 #0 id=0x7f0800f8 android:switcher:2131230968:1}, 
savedInstanceState: Bundle[{android:view_state={2131230754=android.view.AbsSavedState$1@efa6742,
 2131230755=android.view.AbsSavedState$1@efa6742, 2131230757=android.view.AbsSavedState$1@efa6742,
// 异样的地方
2131230775=TextView.SavedState{ed32c03 start=30 end=40 text=Netzwerk, nicht bereit, Bitte überprüfen sie die WLAN - konfiguration Oder Sim - karte.}, 

2131230802=android.view.AbsSavedState$1@efa6742,
...
2131230950=android.view.AbsSavedState$1@efa6742, 2131230951=android.view.AbsSavedState$1@efa6742}}]

正常:2131230755=android.view.AbsSavedState$1@efa6742

出问题: 2131230775=TextView.SavedState{ed32c03 start=30 end=40 text=Netzwerk, nicht bereit, Bitte überprüfen sie die WLAN - konfiguration Oder Sim - karte.}

2131230755这个数字你一看应该就大概猜到它是什么了,转成16进制: 0x7f080037. 没错它就是R.id.check_net的值.

我们看下savedInstanceState

savedInstanceState: Bundle[{android:view_state={
2131230754=android.view.AbsSavedState$1@efa6742, 2131230755=android.view.AbsSavedState$1@efa6742, 2131230757=android.view.AbsSavedState$1@efa6742,
2131230775=TextView.SavedState{ed32c03 start=30 end=40 text=Netzwerk, nicht bereit, Bitte überprüfen sie die WLAN - konfiguration Oder Sim - karte.}, 
2131230802=android.view.AbsSavedState$1@efa6742, 
2131230821=android.view.AbsSavedState$1@efa6742,
...
2131230950=android.view.AbsSavedState$1@efa6742, 2131230951=android.view.AbsSavedState$1@efa6742}}]
2017-12-02 21-34-32屏幕截图.png

这个是key值, 它的值是mSavedViewState

public class Fragment implements ComponentCallbacks, OnCreateContextMenuListener, LifecycleOwner {
    ...
    SparseArray<Parcelable> mSavedViewState;

所以问题原因清楚了:
onViewStateRestored 回调中给TextView设置了savedInstanceState中保存的值,因为onViewStateRestored是在onViewCreated之后执行的.

解决方法如下:

    @Override
    public void onViewStateRestored(@Nullable Bundle savedInstanceState) {
        super.onViewStateRestored(savedInstanceState);
        if (savedInstanceState != null) {
            SparseArray sparseArray = savedInstanceState.getSparseParcelableArray("android:view_state");
            if (sparseArray != null) {
                Object savedState = sparseArray.get(R.id.check_net);
                if (savedState != null && savedState instanceof TextView.SavedState) {
                    /////////////////////////////////////////////////
                    // 重新设置TextView的内容//////
                    ////////////////////////////////////////////////
                }
            }
        }
    }

所谓的视图残留其实正是Android的视图内容恢复机制.
这个对于ListView而言非常重要,可以跳到用户之前阅读的位置,当然也会带来类似我遇到的困扰.

上一篇下一篇

猜你喜欢

热点阅读