ConstraintLayout 遇到的问题记录

2,989 阅读3分钟

记录某次解决使用ConstraintLayout遇到的问题

问题1:如何快速知道最新版本的ConstraintLayout

访问它

问题2:ConstraintLayout中使用Group ,且项目使用了AndResGuard 资源压缩工具,导致无法更新 Group 引用的相关子视图

分析问题思路

  1. 怀疑可能是混淆导致的? 尝试去掉混淆后,发现问题依旧没有解决,故排除它

  2. 怀疑是AndResGuard 压缩工具导致的?

    去掉压缩任务后,重新编包,可以正常更新视图,通过实验结果,这个问题肯定和AndResGuard 脱离不了关系

  3. 确定了导致问题发生的原因,寻找最优解

    根据官方文档可知:AndResGuard 将原本冗长的资源路径变短,例如将res/drawable/xxx 变为 r/d/a R.string.name 变为 R.string.a ,相关原理请参考这篇文章

    项目中使用情况:

    constraint_layout1

在代码中我们动态去更新这些被Group关联的视图控件:


STATUS_LOADDING -> {
              contentGroup.visibility = ConstraintLayout.INVISIBLE
              restGroup.visibility = ConstraintLayout.INVISIBLE
              loaddingGroup.visibility = ConstraintLayout.VISIBLE
          }
///...
          }
// 调用updatePreLayout 间接去刷新Group所关联的视图,这里表现为隐藏所有关联子视图
contentGroup.updatePreLayout(contraintLayoutRoot)
restGroup.updatePreLayout(contraintLayoutRoot)
loaddingGroup.updatePreLayout(contraintLayoutRoot)

为什么调用updatePreLayout 能够刷新它所关联的子控件,接着看源码Group.java

 public void updatePreLayout(ConstraintLayout container) {
        int visibility = this.getVisibility();
        float elevation = 0.0F;
        if (VERSION.SDK_INT >= 21) {
            elevation = this.getElevation();
        }

        for(int i = 0; i < this.mCount; ++i) {
            int id = this.mIds[i];
            View view = container.getViewById(id);
            if (view != null) {
                view.setVisibility(visibility);
                if (elevation > 0.0F && VERSION.SDK_INT >= 21) {
                    view.setElevation(elevation);
                }
            }
        }
    }

从源码中可以看出,它总共做了两件事:

  • 遍历所有Group所关联的视图id, 并通过 ConstraintLayout 找到视图实例
  • 设置视图可见性 & 阴影

由上述可知,我们需要去研究:

  • Group 所关联视图 的ID值 是通过何种方式获取到的?

    找到mIds 定义处:Group 的父类 ConstraintHelper, 其构造方法:

    TypedArray a =  this.getContext().obtainStyledAttributes(attrs,  styleable.ConstraintLayout_Layout);
            int N = a.getIndexCount();
            for(int i = 0; i < N; ++i) {
                int attr = a.getIndex(i);
                if (attr == styleable.ConstraintLayout_Layout_constraint_referenced_ids) {
                    this.mReferenceIds = a.getString(attr); // app:constraint_referenced_ids配置的字符串
                    // 拆解ids 
                    this.setIds(this.mReferenceIds);
                }
            }
            
    ///....
    
    private void setIds(String idList) {
        if (idList != null) {
            int begin = 0;
            while(true) {
                int end = idList.indexOf(44, begin);
                if (end == -1) {
                    this.addID(idList.substring(begin));
                    return;
                }
                // 获取id值并保存到数组中    
                this.addID(idList.substring(begin, end));
                begin = end + 1;
            }
        }
    }            
    ....
    
    private void addID(String idString) {
       ....
       try {
                    Class res = id.class;
                    Field field = res.getField(idString);
                    tag = field.getInt((Object)null);
                } catch (Exception var5) {
                    ;
                }
    
                if (tag == 0) {
                // 注意这里是通过getIdentifier这种方式获取资源id值
                    tag = this.myContext.getResources().getIdentifier(idString, "id", this.myContext.getPackageName());
                }
                ....
                if (tag != 0) {
                    this.setTag(tag, (Object)null);
                } else {
                // 若获取不到id资源值,将打印以下异常信息
                    Log.w("ConstraintHelper", "Could not find id of \"" + idString + "\"");
                }
        }            
        ... 
        
    

从源码中得出:Group 所关联的视图 是通过getIdentifier 获取到的,由于AndResguard 对原有的id 做了混淆变更处理,getIdentifier 传递的是未混淆之前的id ,导致无法获取到,进而就无法为mIds 数组中赋值

  • ConstraintLayout 是如何通过id找到视图引用的?
public class ConstraintLayout extends ViewGroup {
    SparseArray<View> mChildrenByIds = new SparseArray();
}
/**
* Called when a new child is added to this ViewGroup. Overrides should always
* call super.onViewAdded.
* @param child the added child view
*/
public void onViewAdded(View view) {
    ...
    // 子视图添加到当前是容器中,将id 和 引用进行关联缓存
    this.mChildrenByIds.put(view.getId(), view);
    this.mDirtyHierarchy = true;
}
// 此方法恰好在Group#updatePreLayout 时用到,由于andresguard 导致id 值无法找到,所以也就没法找到对应的视图引用,进而也就无法更新关联视图的可见性
public View getViewById(int id) {
    return (View)this.mChildrenByIds.get(id);
}

综上:由于AndResGuard 混淆了视图id name,而ConstraintHelper 在获取所有关联的视图id时,使用了混淆之前的视图id name,导致无法获取到正确id值,进而在执行Group updatePreLayout 找不到对应关联的视图引用导致界面可见性无法改变

解决问题

  • 方案一:AndResGuard官方文档也申明了若使用了getIdentifier 方法访问的资源,都需要加入白名单,但是笔者不建议这样做,因为后续开发过程中若用到了Group refreshID,都需要手动去维护白名单,成本较高。这块已经有对应的开发者提出了issue
  • 方案二:不在xml 中定义Group, 在代码中初始化Group, 再调用ConstraintHelper#setReferencedIds 绑定关联视图id, 绕开构造方法中getIdentifier 获取Group 所关联的视图的id值 这种方案