View State

什么是ViewState?
对于ViewState,我们有许多的误解。Viewstate不保存控件,而是去保存form中对应ID控件的值,特别是那些由于他们没有和form一起post 而在页面回传时会丢失的控件的值。viewstate一般不要用来保存session或在页面间传输数据。在页面回传后,viewstate不能用来动态地创建页面的控件。他在页面回传之后不回复控件的值。甚至一个控件的viewstate被禁止了,在页面回传后,控件的值仍然不会丢失,比如textbox,dropdownlist控件。那什么是viewstate呢?viewstate保存最后一次在服务器上处理的页面状态。他不能保存那些被动态改变的控件的值。

viewstate是如何工作的?
所有的服务器端控件都有一个ViewState属性。如果他是enable的,这个控件的viewstate就起作用了。那viewstate是在哪里,是如何存储的呢?当一个页面第一次加载,所有的控件被序列化到viewstate,保存在一个叫_ViewState的隐藏form字段里。这个隐藏字段对应服务器端的ViewState对象。页面的ViewState使用System.Web.UI.StateBag对象存储键值对。当一个回传发生,页面反序列化ViewState然后恢复所有的控件。页面中保存控件的ViewState以base 64 编码格式存储成name - value。当一个页面重新加载,会调用两个和ViewState相关的方法, LoadViewState 和SaveViewState。下面是我的一个页面中的_ViewState隐藏字段。

<input type="hidden" name="__VIEWSTATE" value="dNrATo45Tm5QzQ7Oz8AblWpxPjE9MMl0Aq765QnCmP2TQ==" />

启用和禁止ViewState
在默认情况下,所有服务器控件的viewstate开启状态,通过以及几种途径来禁止。
1.页面级别
2.控件级别
3.应用程序级别
4.机器级别
页面级别禁止的方法是在页面的开始写入

<%@ Page EnableViewState ="False" %> 
or 
<%@ Page EnableViewState ="True" %>

控件级别是

<asp:TextBox id="txtCode" runat="server” EnableViewState="false" />
or
<asp:TextBox id="txtCode" runat="server" EnableViewState="true" />

程序级别是在web.config中

<pages enableViewState="false" />
or
<pages enableViewState="true" />

机器级别是在machine.config中

<pages enableViewState="true" enableViewStateMac="true" ... />
or
<pages enableViewState="false" ... />

在viewstate中保存和取出值
viewstate能处理以下的类型
基本类型,基本类型数组,ArrayList 和Hashtable,任何可以序列化的对象。

以下代码是将ArrayList存到viewstate中并取出

ArrayList obj = new ArrayList();
//Some code
ViewState["ViewStateObject"= obj;
  
obj = ViewState["ViewStateObject"];

性能问题
为了更好的页面呈现性能,viewstate应该尽可能的小。要记住Viewstate中的数据会占用很多的网络带宽。因此我们要谨慎的利用viewstate。如果页面和控件不需要回传,那么就要禁止viewstate属性。通常在aspx页面之外保存Viewstate会取得更好的性能表现。为了达到这个目的,我们可以使用SavePageStateToPersistenceMedium 和LoadPageStateFromPersistenceMedium 这两个方法。在web.config或machine.config设置来禁止某个程序的所有页面或全部程序页面的viewstate。

注意只有控件包含在<form runat=server>里才能存储viewstate。然而即使页面所有的viewstate被禁止,页面仍然在viewstate中保存20字节的数据,用来在回传时为相应的控件分配viewstate中的数据。所以当页面完全没有回传,移去runat="server"能减少20字节的数据。如果有很多这样的页面,20字节的节省也能在一定程度上减少带宽。viewstate应该在必要的时候使用。在DataGrid和DataRepeater这样的控件中要避免使用viewstate,因为这些控件的viewstate占用数据相当大。

下面我提供一个简单的用于计算页面viewstate大小的方法。创建一个MasterPageBase 类,然后其他所有的页面都要继承他。

public class MasterPageBase: System.Web.UI.Page
{
  
protected override void OnPreRender(EventArgs e)
  
{
    
object viewStateObject = HttpContext.Current.Request["__VIEWSTATE"];
    
if (viewStateObject == null)
      HttpContext.Current.Trace.Warn(
"The ViewState Size is:""0");
    
else
      HttpContext.Current.Trace.Warn(
"The ViewState Size is:",
        HttpContext.Current.Request[
"__VIEWSTATE"].Length.ToString());
    
base.OnPreRender(e);
  }

}

安全问题
可以采取两个措施来避免viewstate被仿冒
使用EnableViewStateMac属性
 ViewState中的内容加密
EnableViewStateMac会进行一个机器授权验证(MAC),这应在页面级别或程序级别使用。当设置时,这个属性会在viewstate呈现之前附加一个viewstate的hash值。当回传发生时,hash值会被重新计算和核对。如果他们不匹配,页面会拒绝显示,这样就确保了viewstate没有被纂改。

在machine.config中设置对viewstate内容的加密

<machineKey validation="3Des" /> or <machineKey validation="SHA1"/>

viewstate容易出错的地方
当将一个页面的控件传输到另外一个页面(第二个页面)时,通常会出现错误。解决方法是在第二个页面中将viewstate禁用。

Fatal Exception: java.lang.IllegalArgumentException: Wrong state class, expecting View State but received class androidx.appcompat.widget.Toolbar$SavedState instead. This usually happens when two views of different type have the same id in the same hierarchy. This view's id is id/toolbar. Make sure other views do not use the same id. at android.view.View.onRestoreInstanceState(View.java:21043) at android.view.View.dispatchRestoreInstanceState(View.java:21015) at android.view.ViewGroup.dispatchRestoreInstanceState(ViewGroup.java:4063) at android.view.ViewGroup.dispatchRestoreInstanceState(ViewGroup.java:4069) at android.view.ViewGroup.dispatchRestoreInstanceState(ViewGroup.java:4069) at android.view.ViewGroup.dispatchRestoreInstanceState(ViewGroup.java:4069) at android.view.ViewGroup.dispatchRestoreInstanceState(ViewGroup.java:4069) at android.view.View.restoreHierarchyState(View.java:20993) at com.android.internal.policy.PhoneWindow.restoreHierarchyState(PhoneWindow.java:2185) at android.app.Activity.onRestoreInstanceState(Activity.java:1753) at android.app.Activity.performRestoreInstanceState(Activity.java:1706) at android.app.Instrumentation.callActivityOnRestoreInstanceState(Instrumentation.java:1356) at android.app.ActivityThread.handleStartActivity(ActivityThread.java:3618) at android.app.servertransaction.TransactionExecutor.performLifecycleSequence(TransactionExecutor.java:221) at android.app.servertransaction.TransactionExecutor.cycleToPath(TransactionExecutor.java:201) at android.app.servertransaction.TransactionExecutor.executeLifecycleState(TransactionExecutor.java:173) at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:97) at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2187) at android.os.Handler.dispatchMessage(Handler.java:106) at android.os.Looper.loop(Looper.java:236) at android.app.ActivityThread.main(ActivityThread.java:8057) at java.lang.reflect.Method.invoke(Method.java) at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:620) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1011) 线上崩溃分析
最新发布
07-02
<think>好的,我现在需要分析用户提供的这个Android崩溃日志。首先,崩溃类型是IllegalArgumentException,错误信息是“Wrong state class, expecting View Statebut received class androidx.appcompat.widget.Toolbar$SavedState instead.This usually happens whentwo views of differenttype have the sameid in the samehierarchy. This view's id is id/toolbar. Makesure other views donot use the sameid.”。看起来是视图状态恢复时的问题。首先,我需要理解这个错误的原因。根据错误信息,当两个不同类型的视图在同一个视图层级中使用了相同的ID时,就会发生这种情况。系统期望恢复某个视图的状态,但接收到的却是另一个不同类型的视图的状态。例如,可能有一个Toolbar和其他视图(比如LinearLayout)使用了相同的ID,导致在恢复状态时出现类型不匹配。 接下来,我需要检查用户提供的代码和上下文。用户之前讨论的是关于Parcelable异常的问题,但现在这个崩溃是另一个不同的问题,涉及视图ID冲突。用户可能在布局文件中重复使用了相同的ID,尤其是在包含多个不同类型的视图时。 可能的场景:用户可能在某个Activity或Fragment的布局中定义了一个ID为toolbar的视图,比如一个Toolbar,而在另一个布局文件(例如通过include或者动态加载的Fragment)中,可能有另一个不同类型的视图也使用了id/toolbar。当系统尝试恢复状态时,保存的状态属于Toolbar,但当前视图可能是另一个类型,导致类型不匹配。需要验证这一点,用户需要检查所有相关的XML布局文件,确认是否有多个不同类型的视图共享相同的ID。特别是检查主布局文件和被包含的布局文件(如通过<include>标签引入的),以及各个Fragment的布局文件。另外,用户可能在代码中动态修改了视图的类型,比如在运行时替换了某个容器中的视图,但没有更新对应的ID,或者没有处理状态保存和恢复的逻辑。 解决方案方面,首先建议用户检查所有布局文件,确保同一层级内的视图ID唯一。如果确实需要复用ID,应确保同一时间只有一个视图存在,或者在替换视图时处理好状态保存。此外,可以重写onSaveInstanceState和onRestoreInstanceState方法,自定义处理状态保存,避免冲突。 还需要考虑是否使用了ViewPager或Fragment切换导致的视图层级问题,不同Fragment可能使用了相同的ID,尤其是在动态加载的情况下。另外,检查是否有自定义视图组件可能引入了ID冲突。 总结步骤:1.检查所有相关布局文件,确认id/toolbar是否在多个不同类型的视图中使用。 2.检查是否有include标签或合并布局导致的重复ID。 3.确保在动态替换视图时,正确管理ID和状态保存。 4.如果使用Fragment,检查各个Fragment的布局是否与其他部分存在ID冲突。5.考虑在自定义视图中正确实现状态保存和恢复逻辑。最后,提供具体的修复建议和示例代码,帮助用户解决这个问题,并预防类似问题的发生。</think>### 崩溃原因深度分析 #### 异常类型:**`IllegalArgumentException`** ```java Wrong state class, expecting View State but received class androidx.appcompat.widget.Toolbar$SavedState ``` #### 核心问题定位 - **根本原因**:**两个不同类型的视图在同一个视图层级中使用了相同的 `id`**。 - **具体场景**: 系统在恢复视图状态时,发现 `id/toolbar` 对应的视图类型与保存的状态类型不一致(例如:原本是 `Toolbar`,但当前是 `LinearLayout` 或其他类型)。 --- ### 典型触发场景 1. **布局文件中重复 `id` 定义** - 示例: ```xml <!-- activity_main.xml --> <androidx.appcompat.widget.Toolbar android:id="@+id/toolbar" ... /> <!-- included_layout.xml --> <LinearLayout android:id="@+id/toolbar" <!-- ❌ 与 Toolbar 冲突 --> ... /> ``` - **问题**:`<include>` 标签引入的布局与父布局中的 `id` 冲突。 2. **Fragment 动态替换导致的 `id` 复用** - 示例: ```kotlin supportFragmentManager.beginTransaction() .replace(R.id.container, MyFragment()) // container ID 被多个 Fragment 复用 .commit() ``` - **问题**:不同 Fragment 的布局中使用了相同 `id`。 3. **自定义视图与系统组件 `id` 冲突** - 示例: ```xml <!-- 自定义视图 --> <com.example.CustomView android:id="@+id/toolbar" ... /> <!-- 系统 Toolbar --> <androidx.appcompat.widget.Toolbar android:id="@+id/toolbar" <!-- ❌ 冲突 --> ... /> ``` 4. **ViewPager + FragmentPagerAdapter 导致的视图复用** - **问题**:`ViewPager` 预加载机制可能导致不同页面的 `id` 冲突。 --- ### 修复方案 #### 方法 1:确保 `id` 唯一性 - **步骤**: 1. **全局搜索 `@+id/toolbar`**:检查所有布局文件、`<include>` 布局和 Fragment 的 XML。 2. **修改冲突 `id`**: ```xml <!-- 修复后的 included_layout.xml --> <LinearLayout android:id="@+id/linear_layout_toolbar" <!-- ✅ 修改为唯一 ID --> ... /> ``` #### 方法 2:避免动态替换视图时的 `id` 冲突 - **场景**:动态添加/替换视图时。 - **修复代码**: ```kotlin // 使用新布局时移除旧视图 val container = findViewById<ViewGroup>(R.id.container) container.removeAllViews() val newView = layoutInflater.inflate(R.layout.new_layout, container, false) container.addView(newView) ``` #### 方法 3:Fragment 布局 `id` 命名规范化 - **最佳实践**:为每个 Fragment 的根布局分配唯一 `id`。 ```xml <!-- fragment_a.xml --> <FrameLayout android:id="@+id/fragment_a_root" ... /> <!-- fragment_b.xml --> <FrameLayout android:id="@+id/fragment_b_root" ... /> ``` #### 方法 4:禁用视图状态保存(极端情况) - **适用场景**:明确不需要恢复视图状态。 - **修复代码**: ```kotlin override fun onSaveInstanceState(outState: Bundle) { // 不保存视图状态 super.onSaveInstanceState(outState) } ``` --- ### 调试工具推荐 1. **布局检查器(Layout Inspector)** - **路径**:Android Studio → `Tools` → `Layout Inspector` - **用途**:实时查看视图层级和 `id` 分布。 2. **日志打印视图层级** ```kotlin val decorView = window.decorView Log.d("ViewHierarchy", getViewHierarchy(decorView)) fun getViewHierarchy(view: View): String { val sb = StringBuilder() fun traverse(view: View, depth: Int) { sb.append(" ".repeat(depth * 2) + view.javaClass.simpleName + " ID: ${view.id}\n") if (view is ViewGroup) { for (i in 0 until view.childCount) { traverse(view.getChildAt(i), depth + 1) } } } traverse(view, 0) return sb.toString() } ``` --- ### 预防措施 | 场景 | 风险 | 推荐做法 | |------|------|----------| | 多布局文件共用 `id` | 视图状态冲突 | 命名规范(如 `toolbar_main`, `toolbar_sub`) | | Fragment 动态替换 | `id` 复用 | 使用唯一根布局 `id` | | ViewPager 预加载 | 状态混乱 | 在 `Fragment` 中重写 `setUserVisibleHint()` | | 自定义视图 | 与系统组件冲突 | 使用 `@+id/custom_` 前缀 | ---
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值