項目地址:github.com/didi/booste…php
在 Booster 系列之——佈局:XML vs 純代碼 這篇文章中已經介紹過 booster 正在作的 Layout Transpiler —— 將 XML 佈局文件翻譯成 class 的轉譯器,在實現的過程當中發現了 Android 系統在設計上的各類坑,並且是天坑,幾乎是繞不過去了,最近 Android 官方發佈了 JetPack Compose 讓我眼前一亮,這不就是我想要達到的效果麼,只不過是換了一種形式罷了。java
在前面的文章中只是大體提了一下實現 XML 轉 class 的思路,不少人看了文章以後反饋說沒看懂,因此在這裏深刻介紹一下實現的細節。android
由於 View 的構造方法須要它git
Android 在運行時調用 View 的構造方法時,傳遞的 AttributeSet 實際上是 XmlPullParser 的子類,如: XmlBlock.Parser,因此,Android 在運行時是邊解析 XML 邊實例化 View。github
主要有如下幾個步驟:app
<?xml version="1.0" encoding="utf-8"?>
<ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context="com.didiglobal.booster.app.MainActivity">
<TextView android:id="@+id/activity_main_greeting" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Hello World!" android:layout_constraintBottom_toBottomOf="parent" android:layout_constraintLeft_toLeftOf="parent" android:layout_constraintRight_toRightOf="parent" android:layout_constraintTop_toTopOf="parent" />
</ConstraintLayout>
複製代碼
public class Layout7f0b0000 {
public static View inflate(Context context) {
final List<String> nameList1833697623 = new ArrayList<>();
nameList1833697623.add("layout_width");
nameList1833697623.add("layout_height");
nameList1833697623.add("context");
final List<String> valueList1833697623 = new ArrayList<>();
valueList1833697623.add("match_parent");
valueList1833697623.add("match_parent");
valueList1833697623.add("context", "com.didiglobal.booster.app.MainActivity");
final List<Integer> nameResourceList1833697623 = new ArrayList<>();
nameResourceList1833697623.add(16842996);
nameResourceList1833697623.add(16842997);
nameResourceList1833697623.add(0);
final List<String> namespaceList1833697623 = new ArrayList<>();
namespaceList1833697623.add("http://schemas.android.com/apk/res/android");
namespaceList1833697623.add("http://schemas.android.com/apk/res/android");
namespaceList1833697623.add("http://schemas.android.com/tools");
final Map<String, Map<String, String>> namespaceValueMap1833697623 = new HashMap<>();
final Map<String, String> namespaceValueMapItem322693759 = new HashMap<>();
namespaceValueMapItem322693759.put("layout_width", "match_parent");
namespaceValueMapItem322693759.put("layout_height", "match_parent");
namespaceValueMap1833697623.put("http://schemas.android.com/apk/res/android", namespaceValueMapItem322693759);
final Map<String, String> namespaceValueMapItem1238245197 = new HashMap<>();
namespaceValueMapItem1238245197.put("context", "com.didiglobal.booster.app.MainActivity");
namespaceValueMap1833697623.put("http://schemas.android.com/tools", namespaceValueMapItem1238245197);
final Map<String, Object> resourcesValueMap1833697623 = new HashMap<>();
final AttributeSet attributeSet1833697623 = new AttrtibuteSetImpl(3, nameList1833697623, valueList1833697623, nameResourceList1833697623, namespaceList1833697623, namespaceValueMap1833697623, resourcesValueMap1833697623);
final android.support.constraint.ConstraintLayout view1833697623 = new android.support.constraint.ConstraintLayout(context, attributeSet1833697623);
view1833697623.setVisibility(0);
final List<String> nameList777448400 = new ArrayList<>();
nameList777448400.add("layout_width");
nameList777448400.add("layout_height");
nameList777448400.add("text");
nameList777448400.add("layout_constraintBottom_toBottomOf");
nameList777448400.add("layout_constraintLeft_toLeftOf");
nameList777448400.add("layout_constraintRight_toRightOf");
nameList777448400.add("layout_constraintTop_toTopOf");
final List<String> valueList777448400 = new ArrayList<>();
valueList777448400.add("wrap_content");
valueList777448400.add("wrap_content");
valueList777448400.add("Hello World!");
valueList777448400.add("parent");
valueList777448400.add("parent");
valueList777448400.add("parent");
valueList777448400.add("parent");
final List<Integer> nameResourceList777448400 = new ArrayList<>();
nameResourceList777448400.add(16842996);
nameResourceList777448400.add(16842997);
nameResourceList777448400.add(16843087);
nameResourceList777448400.add(0);
nameResourceList777448400.add(0);
nameResourceList777448400.add(0);
nameResourceList777448400.add(0);
final List<String> namespaceList777448400 = new ArrayList<>();
namespaceList777448400.add("http://schemas.android.com/apk/res/android");
namespaceList777448400.add("http://schemas.android.com/apk/res/android");
namespaceList777448400.add("http://schemas.android.com/apk/res/android");
namespaceList777448400.add("http://schemas.android.com/apk/res-auto");
namespaceList777448400.add("http://schemas.android.com/apk/res-auto");
namespaceList777448400.add("http://schemas.android.com/apk/res-auto");
namespaceList777448400.add("http://schemas.android.com/apk/res-auto");
final Map<String, Map<String, String>> namespaceValueMap777448400 = new HashMap<>();
final Map<String, String> namespaceValueMapItem1412384463 = new HashMap<>();
namespaceValueMapItem1412384463.put("layout_width", "wrap_content");
namespaceValueMapItem1412384463.put("layout_height", "wrap_content");
namespaceValueMapItem1412384463.put("text", "Hello World!");
namespaceValueMap777448400.put("http://schemas.android.com/apk/res/android", namespaceValueMapItem1412384463);
final Map<String, String> namespaceValueMapItem1690180792 = new HashMap<>();
namespaceValueMapItem1690180792.put("layout_constraintBottom_toBottomOf", "parent");
namespaceValueMapItem1690180792.put("layout_constraintLeft_toLeftOf", "parent");
namespaceValueMapItem1690180792.put("layout_constraintRight_toRightOf", "parent");
namespaceValueMapItem1690180792.put("layout_constraintTop_toTopOf", "parent");
namespaceValueMap777448400.put("http://schemas.android.com/apk/res-auto", namespaceValueMapItem1690180792);
final Map<String, Object> resourcesValueMap777448400 = new HashMap<>();
final AttributeSet attributeSet777448400 = new AttrtibuteSetImpl(7, nameList777448400, valueList777448400, nameResourceList777448400, namespaceList777448400, namespaceValueMap777448400, resourcesValueMap777448400);
final android.widget.TextView view777448400 = new android.widget.TextView(context, attributeSet777448400);
view777448400.setVisibility(0);
final android.view.ViewGroup.LayoutParams layoutParams944291586 = new android.view.ViewGroup.LayoutParams(-2, -2);
view1833697623.addView(view777448400, layoutParams944291586);
return view1833697623;
}
}
複製代碼
R.layout.${resId}
的指令上面的實現方案几乎是完美的,然而,咱們卻低估了 Android 系統的設計,在 ResourcesImpl 中,竟然有兩處把 AttributeSet 強轉成 XmlBlock.Parser,爲何要把一個接口強制轉換成一個只有包可見並且仍是 final
的類呢?我也不知道啊。。。ide
public class ResourcesImpl {
TypedArray obtainStyledAttributes(@NonNull Resources.Theme wrapper, AttributeSet set, @StyleableRes int[] attrs, @AttrRes int defStyleAttr, @StyleRes int defStyleRes) {
synchronized (mKey) {
final int len = attrs.length;
final TypedArray array = TypedArray.obtain(wrapper.getResources(), len);
// XXX note that for now we only work with compiled XML files.
// To support generic XML files we will need to manually parse
// out the attributes from the XML file (applying type information
// contained in the resources and such).
final XmlBlock.Parser parser = (XmlBlock.Parser) set; // <<<=== 看這裏
mAssets.applyStyle(mTheme, defStyleAttr, defStyleRes, parser, attrs,
array.mDataAddress, array.mIndicesAddress);
array.mTheme = wrapper;
array.mXml = parser;
return array;
}
}
static int getAttributeSetSourceResId(@Nullable AttributeSet set) {
if (set == null || !(set instanceof XmlBlock.Parser)) { // <<<=== 看這裏
return ID_NULL;
}
return ((XmlBlock.Parser) set).getSourceResId(); // <<<=== 看這裏
}
}
複製代碼
可能有人會問了,強制將 AttributeSet 轉換成 XmlBlock.Parser 究竟是個什麼樣的坑?佈局
這得從上面生成的代碼提及,細心的讀者可能發現了 AttributeSetImpl 這個類:post
final AttributeSet attributeSet777448400 = new AttrtibuteSetImpl(
7,
nameList777448400,
valueList777448400,
nameResourceList777448400,
namespaceList777448400,
namespaceValueMap777448400,
resourcesValueMap777448400);
複製代碼
沒錯,AttributeSetImpl 就是由 Booster 定義的,大體代碼以下:性能
public class AttributeSetImpl implements AttributeSet {
private final int mAttributeCount;
private final List<String> mNameList;
private final List<Integer> mValueList;
private final List<Integer> mNameResourceList;
private final List<String> mNamespaceList;
private final Map<String, Map<String, String>> mNamespaceValueMap;
private final Map<String, Object> mResourcesValueMap;
public AttributeSetImpl( int attributeCount, List<String> nameList, List<Integer> valueList, List<Integer> nameResourceList, List<String> namespaceList, Map<String, Map<String, String>> namespaceValueMap, Map<String, Object> resourcesValueMap) {
this.mAttributeCount = attributeCount;
this.mNameList = nameList;
this.mValueList = valueList;
this.mNameResourceList = nameResourceList;
this.mNamespaceList = namespaceList;
this.mNamespaceValueMap = namespaceValueMap;
this.mResourcesValueMap = resourcesValueMap;
}
// ...
}
複製代碼
把 Android Framework 原生構造 View 的過程簡化一下:
XmlBlock.Parser parser = new XmlBlock.Parser();
parser.parse(R.layout.main);
...
TextView txt = new TextView(context, parser); // 注意這裏,parser 做爲 AttributSet 傳遞
複製代碼
那麼通過 Booster 優化後,構造 View 的過程則是這樣的:
...
AttributeSetImpl attrs = new AttributeSetImpl(...);
TextView txt = new TextView(context, attrs); // 注意這裏,跟原生的區別
複製代碼
因此,Android Framework 要強制將 AttributeSetImpl 轉換成 XmlBlock.Parser 是轉不成功的,由於 AttributeSetImpl 自己就不會是 XmlPullParser 的子類,更不可能繼承 Framework 中只有包可見並且仍是 final
的 XmlBlock.Parser。
掉到了 Android Framework 的天坑裏一直沒爬出來,正在我心灰意冷的時候,JetPack Compose 讓我眼前一亮,如下是官方的示例。
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
Greeting("Android")
}
}
}
@Composable
fun Greeting(name: String) {
Text (text = "Hello $name!")
}
複製代碼
JetPack Compose 跟 Anko 以及 iOS SwiftUI 有點相似,之因此 JetPack Compose 的體驗如此之好主要得益於兩點:
至於 Android Studio 是如何作到所見即所得,我大膽推測一下,應該跟 Booster 系列之——佈局:XML vs 純代碼 這篇文章提到的 Layout Lib 原理相似,只不過由原來解析 XML 變成了解析 Kotlin 代碼或者字節碼,至於實際的實現,還得細細研究一下 Android Studio 的源碼。
因此,有了 JetPack Compose,經過 XML 描述佈局的時代即將成爲歷史,而用代碼直接描述佈局纔是將來,這樣,由於 XML 而致使的性能問題也就不復存在了。