欢迎Follow我的GitHub, 关注我的CSDN, 精彩不断!

CSDN: http://blog.csdn.net/caroline_wendy/article/details/68923156

在GitHub上Clone的某开源Android项目, 下载配置, 完毕构建, 在手机上能够安装, 可是无法执行. 项目的编译版本号(compileSdkVersion)是25(7.1), 最低的兼容版本号(minSdkVersion)是19(4.4), 手机的系统版本号是21(5.0), 已经满足应用的最低执行条件. 然而, 在同样系统版本号(25, 7.1)的模拟机上, 应用执行正常.

Why

在我的手机执行应用时, 报错例如以下:

E/AndroidRuntime: FATAL EXCEPTION: main
java.lang.RuntimeException: Unable to start activity ComponentInfo{com.saulmm.cui/com.saulmm.cui.HomeActivity}: 
java.lang.IllegalStateException: You need to use a Theme.AppCompat theme (or descendant) with this activity.
Caused by: java.lang.IllegalStateException: You need to use a Theme.AppCompat theme (or descendant) with this activity.
    at android.support.v7.app.AppCompatDelegateImplV9.createSubDecor(AppCompatDelegateImplV9.java:359)
    at android.support.v7.app.AppCompatDelegateImplV9.ensureSubDecor(AppCompatDelegateImplV9.java:328)
    at android.support.v7.app.AppCompatDelegateImplV9.setContentView(AppCompatDelegateImplV9.java:289)
    at android.support.v7.app.AppCompatActivity.setContentView(AppCompatActivity.java:140)
    at android.databinding.DataBindingUtil.setContentView(DataBindingUtil.java:276)
    at android.databinding.DataBindingUtil.setContentView(DataBindingUtil.java:261)
    at com.saulmm.cui.HomeActivity.onCreate(HomeActivity.java:42)

定位

问题起源于DataBindingUtil#setContentView, DataBindingUtil绑定layout布局.

// HomeActivity.java
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    binding = DataBindingUtil.setContentView(this, R.layout.activity_home);
    // ...
}

调用AppCompatActivity#setContentView, Activity绑定layout布局.

// DataBindingUtil.java
public static <T extends ViewDataBinding> T setContentView(Activity activity, int layoutId,
        DataBindingComponent bindingComponent) {
    activity.setContentView(layoutId);
    // ...
}

终于是Activity代理实现类AppCompatDelegateImplV9实现setContentView的详细逻辑. 通过ensureSubDecor方法创建DecorView, 填充Activity的自己定义布局resId, ensureSubDecor再调用createSubDecor方法创建DecorView.

// AppCompatDelegateImplV9.java
@Override
public void setContentView(int resId) {
    ensureSubDecor(); // 创建并初始化DecorView
    ViewGroup contentParent = (ViewGroup) mSubDecor.findViewById(android.R.id.content);
    contentParent.removeAllViews();
    LayoutInflater.from(mContext).inflate(resId, contentParent);
    mOriginalWindowCallback.onContentChanged();
}

private void ensureSubDecor() {
    if (!mSubDecorInstalled) {
        mSubDecor = createSubDecor();
        // ...
    }
}

createSubDecor方法, 依据应用的样式主题(Theme)设置根布局DecorView的样式, 并执行初始化. 当未含有AppCompatTheme_windowActionBar属性时, 则觉得主题未设置, 并抛出异常IllegalStateException.

// AppCompatDelegateImplV9.java
// 依据布局样式Style设置根布局DecorView的样式
private ViewGroup createSubDecor() {
    TypedArray a = mContext.obtainStyledAttributes(R.styleable.AppCompatTheme);

    // 没有布局属性
    if (!a.hasValue(R.styleable.AppCompatTheme_windowActionBar)) {
        a.recycle();
        // 问题所在!
        throw new IllegalStateException(
                "You need to use a Theme.AppCompat theme (or descendant) with this activity.");
    }
    // ...
}

为什么API 25的模拟器能够启动, 我的手机(API 21)就不能启动呢? 原因非常easy, 就是由于开源项目的主题资源设置有误. 默认主题在AndroidManifesttheme属性中设置.

<application android:theme="@style/AppTheme">

点击IDE的AppTheme跳转至声明, 发现仅仅有一处, 即在values-v23中声明.

values-v23

原因

对于资源属性而言, 系统默认查找与匹配低于当前API等级的属性, 保证高版本号属性不会在低版本号中执行. 由于高版本号会加入很多其它的新接口, 低版本号无法找到, 强制使用可能导致异常甚至崩溃, 所以禁止訪问高版本号的属性.

解决

理解了问题的所在, 解决方式就非常easy. 为了支持最低API以上的所有系统, 在默认的values/themes.xml中, 加入AppTheme属性就可以.

<style name="AppTheme" parent="Base.AppTheme"/>

values

问题虽小, 但不可忽视, 否则就仅仅能在某些手机可用, 在某些手机崩溃, 摸不着头脑. 在开发中, 优先在默认values目录中加入属性, 假设须要额外支持, 在其它高版本号values-vXX中再加入. Do you get it?

That’s all! Enjoy it!