天天看点

Android 5.0使用android:onClick属性出现崩溃的原因及解决方案问题及表现原因实验证明源码分析问题解决方案

问题及表现

在项目中,对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使用android:onClick属性出现崩溃的原因及解决方案问题及表现原因实验证明源码分析问题解决方案

Android 5.0 Crash信息如下:

Android 5.0使用android:onClick属性出现崩溃的原因及解决方案问题及表现原因实验证明源码分析问题解决方案

原因

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

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

实验证明

在使用getContext()获取view的Context时,如果在布局文件中未设置主题,返回值是当前的Activity实例[email protected]。

Android 5.0使用android:onClick属性出现崩溃的原因及解决方案问题及表现原因实验证明源码分析问题解决方案

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

Android 5.0使用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(, );
            final boolean hasThemeOverride = themeResId != ;
            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, );
            if (layout == ) {
                final String value = attrs.getAttributeValue(null, ATTR_LAYOUT);
                if (value == null || value.length() <= ) {
                    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(), 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(, );
            if (themeResId != ) {
                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", );
            if (layout == ) {
                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(, );
            final boolean hasThemeOverride = themeResId != ;
            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

继续阅读