Android 5.0使用android:onClick属性出现崩溃的原因及解决方案

来源:互联网 发布:阐释者淘宝 编辑:程序博客网 时间:2024/05/29 03:46

问题及表现

在项目中,对Button设置点击事件监听时,大多数情况下还是习惯使用setOnClickListener设置监听,但是最近发现当在布局文件中同时使用了android:theme和android:onClick属性时,在响应点击事件时程序会发生crash,发生Crash的设备为Android 5.0及以上(7.0未测试),不限机型。在Android 5.0和Android 6.0上发生crash时Log信息不一致。

Android 6.0 Crash信息如下:
这里写图片描述
Android 5.0 Crash信息如下:
这里写图片描述

原因

如果去掉android:theme属性,则点击事情可以正常响应,并未出现任何崩溃的情况。

查找资料发现,从Android 5.0开始,支持对单独的View设置主题。当在布局文件中设置了主题之后,ContextThemeWrapper 会被指定为View的Context,因此View的Context不再是Activity了,这时候点击事件的回调响应也就不存在了。

实验证明

在使用getContext()获取view的Context时,如果在布局文件中未设置主题,返回值是当前的Activity实例MainActivity@4124。
这里写图片描述
在使用getContext()获取view的Context时,如果在布局文件中设置了主题,返回值是ContextThemeWrapper,它的成员变量mBase才是当前的Activity,因此在ContextThemeWrapper无法找到”android:onClick”中设置的方法。
这里写图片描述

源码分析

Android M中代码如下:

 private void parseInclude(XmlPullParser parser, Context context, View parent,            AttributeSet attrs) throws XmlPullParserException, IOException {        int type;        if (parent instanceof ViewGroup) {            // Apply a theme wrapper, if requested. This is sort of a weird            // edge case, since developers think the <include> overwrites            // values in the AttributeSet of the included View. So, if the            // included View has a theme attribute, we'll need to ignore it.            final TypedArray ta = context.obtainStyledAttributes(attrs, ATTRS_THEME);            final int themeResId = ta.getResourceId(0, 0);            final boolean hasThemeOverride = themeResId != 0;            if (hasThemeOverride) {                context = new ContextThemeWrapper(context, themeResId);            }            ta.recycle();            // If the layout is pointing to a theme attribute, we have to            // massage the value to get a resource identifier out of it.            int layout = attrs.getAttributeResourceValue(null, ATTR_LAYOUT, 0);            if (layout == 0) {                final String value = attrs.getAttributeValue(null, ATTR_LAYOUT);                if (value == null || value.length() <= 0) {                    throw new InflateException("You must specify a layout in the"                            + " include tag: <include layout=\"@layout/layoutID\" />");                }                // Attempt to resolve the "?attr/name" string to an identifier.                layout = context.getResources().getIdentifier(value.substring(1), null, null);            }            ... } ... View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,            boolean ignoreThemeAttr) {        if (name.equals("view")) {            name = attrs.getAttributeValue(null, "class");        }        // Apply a theme wrapper, if allowed and one is specified.        if (!ignoreThemeAttr) {            final TypedArray ta = context.obtainStyledAttributes(attrs, ATTRS_THEME);            final int themeResId = ta.getResourceId(0, 0);            if (themeResId != 0) {                context = new ContextThemeWrapper(context, themeResId);            }            ta.recycle();        }        if (name.equals(TAG_1995)) {            // Let's party like it's 1995!            return new BlinkLayout(context, attrs);        }}

Android K中代码如下:

private void parseInclude(XmlPullParser parser, View parent, AttributeSet attrs)            throws XmlPullParserException, IOException {        int type;        if (parent instanceof ViewGroup) {            final int layout = attrs.getAttributeResourceValue(null, "layout", 0);            if (layout == 0) {                final String value = attrs.getAttributeValue(null, "layout");                if (value == null) {                    throw new InflateException("You must specifiy a layout in the"                            + " include tag: <include layout=\"@layout/layoutID\" />");                } else {                    throw new InflateException("You must specifiy a valid layout "                            + "reference. The layout ID " + value + " is not valid.");                }                ...}...View createViewFromTag(View parent, String name, AttributeSet attrs) {        if (name.equals("view")) {            name = attrs.getAttributeValue(null, "class");        }        ...}

对比源码可以发现,在M的源码中,多出了如下几号代码

final TypedArray ta = context.obtainStyledAttributes(attrs, ATTRS_THEME);            final int themeResId = ta.getResourceId(0, 0);            final boolean hasThemeOverride = themeResId != 0;            if (hasThemeOverride) {                context = new ContextThemeWrapper(context, themeResId);            }            ta.recycle();

如果对布局或者View指定了主题,那么当前的context(activity或fragment或application context)被转换为 ContextThemeWrapper的实例,因此会出现ContextThemeWrapper找不到对应方法的问题。

问题解决方案

如果是对整个Activity设置主题,尽量不要在布局文件中设置,在Manifest配置文件中设置;
如果要求必须在布局文件中设置主题,那么不要使用属性android:onClick,使用setOnClickListener替代。

参考资料:
http://stackoverflow.com/questions/31653126/crash-when-clicking-button-with-custom-theme/31672941#31672941

0 0
原创粉丝点击