http://www.jianshu.com/p/d9143a92ad94
Fragment系列文章:1、Fragment全解析系列(一):那些年踩过的坑2、Fragment全解析系列(二):正确的使用姿势3、Fragment之我的解决方案:Fragmentation
本篇主要介绍一些最常见的Fragment的坑以及官方Fragment库的那些自身的BUG,并给出解决方案;这些BUG在你深度使用时会遇到,比如Fragment嵌套时或者单Activity+多Fragment架构时遇到的坑。
Fragment是可以让你的app纵享丝滑的设计,如果你的app想在现在基础上性能大幅度提高,并且占用内存降低,同样的界面Activity占用内存比Fragment要多,响应速度Fragment比Activty在中低端手机上快了很多,甚至能达到好几倍!如果你的app当前或以后有移植平板等平台时,可以让你节省大量时间和精力。
简陋的目录1、getActivity()空指针2、异常:Can not perform this action after onSaveInstanceState3、Fragment重叠异常-----正确使用hide、show的姿势4、Fragment嵌套的那些坑5、未必靠谱的出栈方法remove()6、多个Fragment同时出栈的深坑BUG7、深坑 Fragment转场动画
最新版知乎,单Activity多Fragment的架构,响应可以说非常“丝滑”,非要说缺点的话,就是没有转场动画,并且转场会有类似闪屏现象。我猜测可能和Fragment转场动画的一些BUG有关。(这系列的最后一篇文章我会给出我的解决方案,可以自定义转场动画,并能在各种特殊情况下正常运行。)
但是!Fragment相比较Activity要难用很多,在多Fragment以及嵌套Fragment的情况下更是如此。更重要的是Fragment的坑真的太多了,看Square公司的这篇文章吧,Square:从今天开始抛弃Fragment吧!
当然,不能说不再用Fragment,Fragment的这些坑都是有解决办法的,官方也在逐步修复一些BUG。下面罗列一些,有常见的,也有极度隐蔽的一些坑,也是我在用单Activity多Fragment时遇到的坑,可能有更多坑可以挖掘...
在这之前为了方便后面文章的介绍,先规定一个“术语”,安卓app有一种特殊情况,就是 app运行在后台的时候,系统资源紧张的时候导致把app的资源全部回收(杀死app的进程),这时把app再从后台返回到前台时,app会重启。这种情况下文简称为:“内存重启”。(屏幕旋转等配置变化也会造成当前Activity重启,本质与“内存重启”类似)
在系统要把app回收之前,系统会把Activity的状态保存下来,Activity的FragmentManager负责把Activity中的Fragment保存起来。在“内存重启”后,Activity的恢复是从栈顶逐步恢复,Fragment会在宿主Activity的onCreate
方法调用后紧接着恢复(从onAttach
生命周期开始)。
可能你遇到过getActivity()返回null,或者平时运行完好的代码,在“内存重启”之后,调用getActivity()的地方却返回null,报了空指针异常。
大多数情况下的原因:你在调用了getActivity()时,当前的Fragment已经onDetach()
了宿主Activity。比如:你在pop了Fragment之后,该Fragment的异步任务仍然在执行,并且在执行完成后调用了getActivity()方法,这样就会空指针。
解决办法:更"安全"的方法:(对于Fragment已经onDetach这种情况,我们应该避免在这之后再去调用宿主Activity对象,比如取消这些异步任务,但我们的团队可能会有粗心大意的情况,所以下面给出的这个方案会保证安全)
在Fragment基类里设置一个Activity mActivity的全局变量,在onAttach(Activity activity)
里赋值,使用mActivity代替getActivity()
,保证Fragment即使在onDetach
后,仍持有Activity的引用(有引起内存泄露的风险,但是异步任务没停止的情况下,本身就可能已内存泄漏,相比Crash,这种做法“安全”些),即:
有很多小伙伴遇到这个异常,这个异常产生的原因是:
在你离开当前Activity等情况下,系统会调用onSaveInstanceState()
帮你保存当前Activity的状态、数据等,直到再回到该Activity之前(onResume()
之前),你使用commit()
提交了Fragment事务,就会抛出该异常!
解决方法2个:1、(不推荐)该事务使用commitAllowingStateLoss()
方法提交,但是有可能导致该次提交无效!(在此次离开时恰巧Activity被强杀时)2、(推荐)在重新回到该Activity的时候(onResumeFragments()
或onPostResume()
),再执行该事务!
如果你add()
了几个Fragment,使用show()、hide()
方法控制,比如微信、QQ的底部tab等情景,如果你什么都不做的话,在“内存重启”后回到前台,app的这几个Fragment界面会重叠。
原因是FragmentManager帮我们管理Fragment,当发生“内存重启”,他会从栈底向栈顶的顺序一次性恢复Fragment;但是因为没有保存Fragment的mHidden属性,默认为false,即show状态,所以所有Fragment都是以show的形式恢复,我们看到了界面重叠。(如果是replace
,恢复形式和Activity一致,只有当你pop之后上一个Fragment才开始重新恢复,所有使用replace
不会造成重叠现象)
还有一种场景,add
和replace
都有可能造成重叠: 在onCreate
中加载Fragment,并且没有判断saveInstanceState==null
,导致重复加载了同一个Fragment导致重叠。(PS:replace
情况下,如果没有加入回退栈,则不判断也不会造成重叠,但建议还是统一判断下)
详细原因:从源码角度分析,为什么会发生Fragment重叠?
这里给出3个解决方案:1、是大家比较熟悉的 findFragmentByTag
:
即在add()
或者replace()
时绑定一个tag,一般我们是用fragment的类名作为tag,然后在发生“内存重启”时,通过findFragmentByTag
找到对应的Fragment,并hide()
需要隐藏的fragment。
下面是个标准恢复写法:
@Overrideprotected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity); TargetFragment targetFragment; HideFragment hideFragment; if (savedInstanceState != null) { // “内存重启”时调用 targetFragment = getSupportFragmentManager().findFragmentByTag(TargetFragment.class.getName); hideFragment = getSupportFragmentManager().findFragmentByTag(HideFragment.class.getName); // 解决重叠问题 getFragmentManager().beginTransaction() .show(targetFragment) .hide(hideFragment) .commit(); }else{ // 正常时 targetFragment = TargetFragment.newInstance(); hideFragment = HideFragment.newInstance(); getFragmentManager().beginTransaction() .add(R.id.container, targetFragment, targetFragment.getClass().getName()) .add(R.id,container,hideFragment,hideFragment.getClass().getName()) .hide(hideFragment) .commit(); }}如果你想恢复到用户离开时的那个Fragment的界面,你还需要在onSaveInstanceState(Bundle outState)
里保存离开时的那个可见的tag或下标,在onCreate
“内存重启”代码块中,取出tag/下标,进行恢复。
2、使用getSupportFragmentManager().getFragments()恢复
通过getFragments()
可以获取到当前FragmentManager管理的栈内所有Fragment。
标准写法如下:
@Overrideprotected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity); TargetFragment targetFragment; HideFragment hideFragment; if (savedInstanceState != null) { // “内存重启”时调用 List<Fragment> fragmentList = getSupportFragmentManager().getFragments(); for (Fragment fragment : fragmentList) { if(fragment instanceof TartgetFragment){ targetFragment = (TargetFragment)fragment; }else if(fragment instanceof HideFragment){ hideFragment = (HideFragment)fragment; } } // 解决重叠问题 getFragmentManager().beginTransaction() .show(targetFragment) .hide(hideFragment) .commit(); }else{ // 正常时 targetFragment = TargetFragment.newInstance(); hideFragment = HideFragment.newInstance(); // 这里add时,tag可传可不传 getFragmentManager().beginTransaction() .add(R.id.container) .add(R.id,container,hideFragment) .hide(hideFragment) .commit(); }}从代码看起来,这种方式比较复杂,但是这种方式在一些场景下比第一种方式更加简便有效。我会在下一篇中介绍在不同场景下如果选择,何时用findFragmentByTag()
,何时用getFragments()
恢复。
顺便一提,有些小伙伴会用一种并不合适的方法恢复Fragment,虽然效果也能达到,但并不恰当。即:
// 保存@Overrideprotected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); getSupportFragmentManager().putFragment(outState, KEY, targetFragment);}// 恢复@Overrideprotected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_scrolling); if (savedInstanceState != null) { Fragment targetFragment = getSupportFragmentManager().getFragment(savedInstanceState, KEY); }}如果仅仅为了找回栈内的Fragment,使用putFragment(bundle, key, fragment)
保存fragment,是完全没有必要的;因为FragmentManager在任何情况都会帮你存储Fragment,你要做的仅仅是在“内存重启”后,找回这些Fragment即可。。
3、我的解决方案,9行代码解决所有情况的Fragment重叠:传送门
其实一些小伙伴遇到的很多嵌套的坑,大部分都是由于对嵌套的栈视图产生混乱,只要理清栈视图关系,做好恢复相关工作以及正确选择是使用getFragmentManager()
还是getChildFragmentManager()
就可以避免这些问题。
这部分内容是我们感觉Fragment非常难用的一个点,我会在下一篇中,详细介绍使用Fragment嵌套的一些技巧,以及如何清晰分析各个层级的栈视图。
附:startActivityForResult接收返回问题在support 23.2.0以下的支持库中,对于在嵌套子Fragment的startActivityForResult ()
,会发现无论如何都不能在onActivityResult()
中接收到返回值,只有最顶层的父Fragment才能接收到,这是一个support v4库的一个BUG,不过在前两天发布的support 23.2.0库中,已经修复了该问题,嵌套的子Fragment也能正常接收到返回数据了!
如果你想让某一个Fragment出栈,使用remove()
在加入回退栈时并不靠谱。
如果你在add的同时将Fragment加入回退栈:addToBackStack(name)的情况下,它并不能真正将Fragment从栈内移除,如果你在2秒后(确保Fragment事务已经完成)打印getSupportFragmentManager().getFragments()
,会发现该Fragment依然存在,并且依然可以返回到被remove的Fragment,而且是空白页面。
如果你没有将Fragment加入回退栈,remove方法可以正常出栈。
如果你加入了回退栈,popBackStack()
系列方法才能真正出栈,这也就引入下一个深坑,popBackStack(String tag,int flags)
等系列方法的BUG。
在Fragment库中如下4个方法是有BUG的:
1、popBackStack(String tag,int flags)2、popBackStack(int id,int flags)3、popBackStackImmediate(String tag,int flags)4、popBackStackImmediate(int id,int flags)
上面4个方法作用是,出栈到tag/id的fragment,即一次多个Fragment被出栈。
1、FragmentManager栈中管理fragment下标位置的数组ArrayList<Integer> mAvailIndeices的BUG
下面的方法FragmentManagerImpl类方法,产生BUG的罪魁祸首是管理Fragment栈下标的mAvailIndeices
属性:
上面代码最终导致了栈内顺序不正确的问题,如下图:
上面的这个情况,会一次异常,一次正常。带来的问题就是“内存重启”后,各种异常甚至Crash。
发现这BUG的时候,我一脸懵比,幸好,stackoverflow上有大神给出了解决方案!hack FragmentManagerImpl
的mAvailIndices
,对其进行一次Collections.reverSEOrder()
降序排序,保证栈内Fragment的index的正确。
使用方法就是通过popBackStackImmediate(tag/id)
多个Fragment后,调用
2、popBackStack的坑popBackStack
和popBackStackImmediate
的区别在于前者是加入到主线队列的末尾,等其它任务完成后才开始出栈,后者是立刻出栈。
如果你popBackStack
多个Fragment后,紧接着beginTransaction()
add新的一个Fragment,接着发生了“内存重启”后,你再执行popBackStack()
,app就会Crash,解决方案是postDelay出栈动画时间再执行其它事务,但是根据我的观察不是很稳定。我的建议是:如果你想出栈多个Fragment,你应尽量使用popBackStackImmediate(tag/id)
,而不是popBackStack(tag/id)
,如果你想在出栈后,立刻beginTransaction()
开始一项事务,你应该把事务的代码post/postDelay到主线程的消息队列里,下一篇有详细描述。
如果你的Fragment没有转场动画,或者使用setCustomAnimations(enter, exit)
的话,那么上面的那些坑解决后,你可以愉快的玩耍了。
(注意:如果你想给下一个Fragment设置进栈动画和出栈动画,.setCustomAnimations(enter, exit)只能设置进栈动画,第二个参数并不是设置出栈动画;请使用.setCustomAnimations(enter, exit, popEnter, popExit),这个方法的第1个参数对应进栈动画,第4个参数对应出栈动画,所以是.setCustomAnimations(进栈动画, exit, popEnter, 出栈动画))
总结起来就是Fragment没有出栈动画的话,可以避免很多坑。如果想让出栈动画运作正常的话,需要使用Fragment的onCreateAnimation
中控制动画。
但是用代价也是有的,你需要解决出栈动画带来的几个坑。
1、pop多个Fragment时转场动画 带来的问题在使用 pop(tag/id)
出栈多个Fragment的这种情况下,将转场动画临时取消或者延迟一个动画的时间再去执行其他事务;
原因在于这种情景下,如果发生“内存重启”后,因为Fragment转场动画没结束时再执行其他方法,会导致Fragment状态不会被FragmentManager正常保存下来。
2、进入新的Fragment并立刻关闭当前Fragment 时的一些问题(1)如果你想从当前Fragment进入一个新的Fragment,并且同时要关闭当前Fragment。由于数据结构是栈,所以正确做法是先pop
,再add
,但是转场动画会有覆盖的不正常现象,你需要特殊处理,不然会闪屏!
(2)Fragment的根布局要设置android:clickable = true
,原因是在pop
后又立刻add
新的Fragment时,在转场动画过程中,如果你的手速太快,在动画结束前你多点击了一下,上一个Fragment的可点击区域可能会在下一个Fragment上依然可用。
Tip:如果你遇到Fragment的mNextAnim空指针的异常(通常是在你的Fragment被重启的情况下),那么你首先需要检查是否操作的Fragment是否为null;其次在你的Fragment转场动画还没结束时,你是否就执行了其他事务等方法;解决思路就是延迟一个动画时间再执行事务,或者临时将该Fragment设为无动画
看了上面的介绍,你可能会觉得Fragment有点可怕。
但是我想说,如果你只是浅度使用,比如一个Activity容器包含列表Fragment+详情Fragment这种简单情景下,不涉及到popBackStack/Immediate(tag/id)
这些的方法,还是比较轻松使用的,出现的问题,网上都可以找到解决方案。
但是如果你的Fragment逻辑比较复杂,有特殊需求,或者你的app架构是仅有一个Activity + 多个Fragment,上面说的这些坑,你都应该全部解决。
在下一篇中,介绍了一些非常实用的使用技巧,包括如何解决Fragment嵌套、各种环境、组件下Fragment的使用等技巧,推荐阅读!
还有一些比较隐蔽的问题,不影响app的正常运行,仅仅是一些显示的BUG,并没有在上面介绍,在本系列的最后一篇,我给出了我的解决方案,一个我封装的Fragmentation库,解决了所有动画问题,非常适合单Activity+多Fragment 或者 多模块Activity+多Fragment的架构。有兴趣的可以看看 :)
Fragment随笔© 著作权归作者所有举报文章如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
赞赏支持 等8人喜欢 419 更多分享120条评论 只看作者按喜欢排序按时间正序按时间倒序 coco猫16楼 · 2016.04.20 15:28你好 ,关于栈内顺序不正确,FragmentTransactionBugFixHack要怎么使用,这部分没看明白
赞 回复YoKey: @coco猫 在pop多个Fragment后,FragmentTransactionBugFixHack.reorderIndices(FragmentManager)即可,也可以在add/replace Fragment之前调用一次 :)
2016.04.22 12:15 回复__Berial___: @YoKey 如何拿到 FragmentManagerImpl 这个只有包权限的类?创建相同的包?话说给的那个stackoverflow 的链接里没有这样的回答额。。
2016.05.19 10:45 回复YoKey: 作为一个类放入你工程下新建的android.support.v4.app这个包里就可以了,可以参考我第三篇里的Github里的源码
2016.05.19 10:47 回复 添加新评论 还有1条评论, 展开查看 T摩天倫17楼 · 2016.04.22 12:25楼主,请教个问题,就是我项目是ViewPager + Fragment +RadioGroup的,但是每个Fragment里面还嵌套有Fragment,这样从Fragment打开子Fragment是空白的,这是什么情况,用的是hide(),add() 。
赞 回复YoKey: @T摩天倫 需求是Fragment嵌套吗? 是的话 ,add子Fragment时,使用父Fragment的getChildFragmentManager来操作,而不是用getFragmentManager :)
2016.04.22 12:48 回复T摩天倫: @YoKey 这个知道,用了getChildFragmentManager的话,app直接闪退,报错:No view found .......
2016.04.22 13:26 回复YoKey: @T摩天倫 从这个错,我猜可能是你使用了Activity的R.id.container作为containerViewId父Fragment.getChildFragmentManager().beginTransaction.add(containerViewId,子Fragment)...你应该使用父Fragment的FrameLayout容器,作为containerViewId
2016.04.22 14:03 回复 添加新评论 还有2条评论, 展开查看 滋滋滋Boom19楼 · 2016.05.03 13:58楼主,想问下,为什么使用onAttach(Activity activity)代替getActivity()没有问题,但是使用onAttach(Context context)应用会空指针异常,java.lang.NullPointerException: Attempt to invoke virtual method 'java.lang.Object android.content.Context.getSystemService(java.lang.String)' on a null object reference
赞 回复YoKey: @滋滋滋Boom 从异常来看是你的Context是null,检查下你的context。应该和onAttach没什么关系。onAttach(Context context)这个方法是新的support包里新加的,是为了更安全的保证有宿主Activity。其源码:public void onAttach(Context context) {mCalled = true;final Activity hostActivity = mHost == null ? null : mHost.getActivity();if (hostActivity != null) {mCalled = false;onAttach(hostActivity);}}
2016.05.03 14:05 回复 添加新评论 devYOUK20楼 · 2016.05.11 09:58很好的文章!
赞 回复 依米621楼 · 2016.05.11 17:15楼主将这些坑讲解的很透彻,学习了。
赞 回复 喜欢而非坚持22楼 · 2016.05.11 17:18可能是我看过的关于Fragment系列文章中最好的.
赞 回复 imknown23楼 · 2016.05.11 23:10太棒了, 自己也走了好多 Fragment 的坑. 楼主的 提到的 那一个 兼容包 23.2 的问题 真的是太棒了. 我去 谷歌官网看了一下, The system now calls onActivityResult() for a nested FragmentActivity., 谢谢 哈哈
赞 回复 纤沫24楼 · 2016.05.13 14:20在csdn 上,看到有推荐这篇文章,看了非常不错。
赞 回复 安哉25楼 · 2016.05.16 01:43学习啦
赞 回复 阿尔法猫26楼 · 2016.05.17 10:46楼主,能提供一下代码吗,我菜鸟,复制上去出现了很多没见过的错
赞 回复YoKey: @阿尔法猫 我的这系列第三篇文章里有github地址包含Demo,不过建议新手避免深入使用Fragment,正常使用即可,可能第二篇更适合你,这一篇大部分内容都是深入使用时才会遇到的坑 :)
2016.05.17 10:55 回复阿尔法猫: @YoKey 恩恩,谢谢
2016.05.17 12:01 回复 添加新评论 Jafir27楼 · 2016.05.19 04:58半夜起来打蚊子,看看干货……看到 内存重启恢复那里,为什么只有俩个 ?一个是targetfragment 一个是hidefragment。 像QQ那样 都是3 4个~ 这里的俩个 中的hide具体指的什么?应该是除了要显示的targetfragment之外的fragment吧~希望楼主指明
赞 回复YoKey: @Jafir 打蚊子... 是的 那里为了阅读方便简写了下 就写了一个hideFragment
2016.05.19 09:22 回复 添加新评论 ClownQiang28楼 · 2016.05.19 17:35博主你好~关于“FragmentManager栈中管理fragment下标位置的数组ArrayList<Integer> mAvailIndeices的BUG”,这里我没太理解问题,还有你给出的这一段源码,我看了一下不太清楚问题在哪里,能否写的更详细一点~~谢谢啦
赞 回复YoKey: @ClownQiang f.setIndex(mAvailIndices.remove(mAvailIndices.size()-1), mParent);mActive.set(f.mIndex, f);这两句代码,pop多个Fragment有时会导致mAvailIndices里的下标顺序出问题,导致Fragment的index错误,附带mActive这个管理Fragment的List下标错误
2016.05.19 17:42 回复 添加新评论 yangyirunning29楼 · 2016.05.20 12:02用心之作,干货十足!博主辛苦了!
赞 回复 阿尔法猫30楼 · 2016.05.20 13:57哥哥,Fragment全解析系列(一)这个的源码有吗?就是Fragment重叠异常问题,我放在底部了,结果只显示第一个的fragment,点击其他的不执行
赞 回复YoKey: @阿尔法猫 文章中的代码都是核心代码,是我特意写的,源码倒是没有的;你这个问题,点击其他没作用的话,检查下是否确定执行了fragmentManager.beginTransaction().show(目标Fragment).hide(当前Fragment).commit();
2016.05.20 14:12 回复阿尔法猫: if (savedInstanceState != null) { // “内存重启”时调用tost.showToast("内存重启");List<Fragment> fragmentList = getSupportFragmentManager().getFragments();for (Fragment fragment : fragmentList) {if (fragment instanceof TruckFragment) {tracke = (TruckFragment) fragment;} else if (fragment instanceof accessoriesFragment) {accessories = (AccessoriesFragment) fragment;} else if (fragment instanceof MineFragment){mine= (MineFragment) fragment;} else if (fragment instanceof MessageFragment){message= (MessageFragment) fragment;}// 解决重叠问题getSupportFragmentManager().beginTransaction().show(tracke).hide(accessories).hide(message).hide(mine).commit();}}else{// 正常时tracke = TruckFragment.newInstance();accessories = AccessoriesFragment.newInstance();message=MessageFragment.newInstance();mine=MineFragment.newInstance();// // 这里add时,tag可传可不传getSupportFragmentManager().beginTransaction().add(R.id.frame_content,tracke).hide(accessories).hide(message).hide(mine).commit();}
2016.05.20 15:47 回复 添加新评论 阿尔法猫31楼 · 2016.05.20 15:47radioGroup.setOnCheckedChangeListener(new RadioGroup.OnCheckedChangeListener() {@Overridepublic void onCheckedChanged(RadioGroup group, int checkedId) {switch (checkedId) {case R.id.chache:getSupportFragmentManager().beginTransaction().add(R.id.frame_content,tracke).hide(accessories).hide(message).hide(mine).commit();break;case R.id.accessories:getSupportFragmentManager().beginTransaction().add(R.id.frame_content,accessories).hide(tracke).hide(message).hide(mine).commit();break;case R.id.message:getSupportFragmentManager().beginTransaction().add(R.id.frame_content,message).hide(tracke).hide(accessories).hide(mine).commit();break;case R.id.mine:getSupportFragmentManager().beginTransaction().add(R.id.frame_content,mine).hide(tracke).hide(message).hide(accessories).commit();break;}}});
赞 回复YoKey: @阿尔法猫 这里给你一种简写方法,第一次就把4个Fragment都add到FM中(如果Fragment资源占用比较大时,再考虑切换目标Fragment时才add这样的方式)else{ // 正常时tracke = TruckFragment.newInstance();accessories = AccessoriesFragment.newInstance();message=MessageFragment.newInstance();mine=MineFragment.newInstance();// // 这里add时,tag可传可不传getSupportFragmentManager().beginTransaction().add(R.id.frame_content,tracke).add(R.id.frame_content,accessories).add(R.id.frame_content,message).add(R.id.frame_content,mine).hide(accessories).hide(message).hide(mine).commit();}切换时:getSupportFragmentManager().beginTransaction().show(mine) // add换成show.hide(tracke).hide(message).hide(accessories).commit();
2016.05.20 16:02 回复阿尔法猫: 真是太感谢您了,谢谢
2016.05.20 16:50 回复阿尔法猫: 已打赏。。谢谢了
2016.05.20 16:52 回复 添加新评论 还有1条评论, 展开查看上一页 1 2 34 下一页被以下专题收入,发现更多相似内容week.ioAndroid...Android...Android...玩转iOS &...安卓必须知道的程序员Android那些事AndroidAndroid角落Android开发android...Android实战Android...andnroid加载更多...原文链接: http://www.jianshu.com/p/d9143a92ad94新闻热点
疑难解答