美文网首页
Android中所谓的视图残留

Android中所谓的视图残留

作者: lostsearover | 来源:发表于2017-12-02 21:46 被阅读0次

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


    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而言非常重要,可以跳到用户之前阅读的位置,当然也会带来类似我遇到的困扰.

    相关文章

      网友评论

          本文标题:Android中所谓的视图残留

          本文链接:https://www.haomeiwen.com/subject/bohobxtx.html