From c4239ac4a8c145c99daf806b6e1a3ce5109049ba Mon Sep 17 00:00:00 2001 From: Eric Holk Date: Wed, 5 Sep 2018 10:43:31 -0700 Subject: [PATCH] Prototype XML view compiler This is an initial step towards a tool for pre-compiling layout XML files. It accepts an XML file and produces a Java language class called CompiledView with a static method, inflate. Calling CompiledView.inflate should then return a view object that is equivalent to calling LayoutInflater.inflate on the same resource. There are still several important limitations, but this works well enough to do some experimentation. The limitations include: * Currently only one layout can be compiled at a time. * `merge` and `include` nodes are not supported. * View compilation is a manual process that requires code changes in the application. * The tests in this CL do not yet exercise any interesting behavior. Bug: 111895153 Change-Id: I3e6880b08c52087d24ae7486495bd7fa282f4ff7 --- startop/tools/view_compiler/Android.bp | 49 ++++++++++ startop/tools/view_compiler/README.md | 25 +++++ startop/tools/view_compiler/TEST_MAPPING | 7 ++ startop/tools/view_compiler/java_lang_builder.cc | 115 +++++++++++++++++++++++ startop/tools/view_compiler/java_lang_builder.h | 65 +++++++++++++ startop/tools/view_compiler/main.cc | 105 +++++++++++++++++++++ startop/tools/view_compiler/util.cc | 32 +++++++ startop/tools/view_compiler/util.h | 23 +++++ startop/tools/view_compiler/util_test.cc | 28 ++++++ 9 files changed, 449 insertions(+) create mode 100644 startop/tools/view_compiler/Android.bp create mode 100644 startop/tools/view_compiler/README.md create mode 100644 startop/tools/view_compiler/TEST_MAPPING create mode 100644 startop/tools/view_compiler/java_lang_builder.cc create mode 100644 startop/tools/view_compiler/java_lang_builder.h create mode 100644 startop/tools/view_compiler/main.cc create mode 100644 startop/tools/view_compiler/util.cc create mode 100644 startop/tools/view_compiler/util.h create mode 100644 startop/tools/view_compiler/util_test.cc diff --git a/startop/tools/view_compiler/Android.bp b/startop/tools/view_compiler/Android.bp new file mode 100644 index 000000000000..c3e91849e636 --- /dev/null +++ b/startop/tools/view_compiler/Android.bp @@ -0,0 +1,49 @@ +// +// Copyright (C) 2018 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +cc_library_host_static { + name: "libviewcompiler", + srcs: [ + "java_lang_builder.cc", + "util.cc", + ], + static_libs: [ + "libbase" + ] +} + +cc_binary_host { + name: "viewcompiler", + srcs: [ + "main.cc", + ], + static_libs: [ + "libbase", + "libtinyxml2", + "libgflags", + "libviewcompiler", + ], +} + +cc_test_host { + name: "view-compiler-tests", + srcs: [ + "util_test.cc", + ], + static_libs: [ + "libviewcompiler", + ] +} diff --git a/startop/tools/view_compiler/README.md b/startop/tools/view_compiler/README.md new file mode 100644 index 000000000000..56595016cbb9 --- /dev/null +++ b/startop/tools/view_compiler/README.md @@ -0,0 +1,25 @@ +# View Compiler + +This directory contains an experimental compiler for layout files. + +It will take a layout XML file and produce a CompiledLayout.java file with a +specialized layout inflation function. + +To use it, let's assume you had a layout in `my_layout.xml` and your app was in +the Java language package `com.example.myapp`. Run the following command: + + viewcompiler my_layout.xml --package com.example.myapp --out CompiledView.java + +This will produce a `CompiledView.java`, which can then be compiled into your +Android app. Then to use it, in places where you would have inflated +`R.layouts.my_layout`, instead call `CompiledView.inflate`. + +Precompiling views like this generally improves the time needed to inflate them. + +This tool is still in its early stages and has a number of limitations. +* Currently only one layout can be compiled at a time. +* `merge` and `include` nodes are not supported. +* View compilation is a manual process that requires code changes in the + application. +* This only works for apps that do not use a custom layout inflater. +* Other limitations yet to be discovered. diff --git a/startop/tools/view_compiler/TEST_MAPPING b/startop/tools/view_compiler/TEST_MAPPING new file mode 100644 index 000000000000..cc4b17a7a65a --- /dev/null +++ b/startop/tools/view_compiler/TEST_MAPPING @@ -0,0 +1,7 @@ +{ + "presubmit": [ + { + "name": "view-compiler-tests" + } + ] +} diff --git a/startop/tools/view_compiler/java_lang_builder.cc b/startop/tools/view_compiler/java_lang_builder.cc new file mode 100644 index 000000000000..0b8754fc7096 --- /dev/null +++ b/startop/tools/view_compiler/java_lang_builder.cc @@ -0,0 +1,115 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "java_lang_builder.h" + +#include "android-base/stringprintf.h" + +using android::base::StringPrintf; +using std::string; + +void JavaLangViewBuilder::Start() const { + out_ << StringPrintf("package %s;\n", package_.c_str()) + << "import android.content.Context;\n" + "import android.content.res.Resources;\n" + "import android.content.res.XmlResourceParser;\n" + "import android.util.AttributeSet;\n" + "import android.util.Xml;\n" + "import android.view.*;\n" + "import android.widget.*;\n" + "\n" + "public final class CompiledView {\n" + "\n" + "static T createView(Context context, AttributeSet attrs, View parent, " + "String name, LayoutInflater.Factory factory, LayoutInflater.Factory2 factory2) {" + "\n" + " if (factory2 != null) {\n" + " return (T)factory2.onCreateView(parent, name, context, attrs);\n" + " } else if (factory != null) {\n" + " return (T)factory.onCreateView(name, context, attrs);\n" + " }\n" + // TODO: find a way to call the private factory + " return null;\n" + "}\n" + "\n" + " public static View inflate(Context context) {\n" + " try {\n" + " LayoutInflater inflater = LayoutInflater.from(context);\n" + " LayoutInflater.Factory factory = inflater.getFactory();\n" + " LayoutInflater.Factory2 factory2 = inflater.getFactory2();\n" + " Resources res = context.getResources();\n" + << StringPrintf(" XmlResourceParser xml = res.getLayout(%s.R.layout.%s);\n", + package_.c_str(), + layout_name_.c_str()) + << " AttributeSet attrs = Xml.asAttributeSet(xml);\n" + // The Java-language XmlPullParser needs a call to next to find the start document tag. + " xml.next(); // start document\n"; +} + +void JavaLangViewBuilder::Finish() const { + out_ << " } catch (Exception e) {\n" + " return null;\n" + " }\n" // end try + " }\n" // end inflate + "}\n"; // end CompiledView +} + +void JavaLangViewBuilder::StartView(const string& class_name) { + const string view_var = MakeVar("view"); + const string layout_var = MakeVar("layout"); + std::string parent = "null"; + if (!view_stack_.empty()) { + const StackEntry& parent_entry = view_stack_.back(); + parent = parent_entry.view_var; + } + out_ << " xml.next(); // <" << class_name << ">\n" + << StringPrintf(" %s %s = createView(context, attrs, %s, \"%s\", factory, factory2);\n", + class_name.c_str(), + view_var.c_str(), + parent.c_str(), + class_name.c_str()) + << StringPrintf(" if (%s == null) %s = new %s(context, attrs);\n", + view_var.c_str(), + view_var.c_str(), + class_name.c_str()); + if (!view_stack_.empty()) { + out_ << StringPrintf(" ViewGroup.LayoutParams %s = %s.generateLayoutParams(attrs);\n", + layout_var.c_str(), + parent.c_str()); + } + view_stack_.push_back({class_name, view_var, layout_var}); +} + +void JavaLangViewBuilder::FinishView() { + const StackEntry var = view_stack_.back(); + view_stack_.pop_back(); + if (!view_stack_.empty()) { + const string& parent = view_stack_.back().view_var; + out_ << StringPrintf(" xml.next(); // \n", var.class_name.c_str()) + << StringPrintf(" %s.addView(%s, %s);\n", + parent.c_str(), + var.view_var.c_str(), + var.layout_params_var.c_str()); + } else { + out_ << StringPrintf(" return %s;\n", var.view_var.c_str()); + } +} + +const std::string JavaLangViewBuilder::MakeVar(std::string prefix) { + std::stringstream v; + v << prefix << view_id_++; + return v.str(); +} diff --git a/startop/tools/view_compiler/java_lang_builder.h b/startop/tools/view_compiler/java_lang_builder.h new file mode 100644 index 000000000000..c8d20b23cd13 --- /dev/null +++ b/startop/tools/view_compiler/java_lang_builder.h @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#ifndef JAVA_LANG_BUILDER_H_ +#define JAVA_LANG_BUILDER_H_ + +#include +#include +#include + +// Build Java language code to instantiate views. +// +// This has a very small interface to make it easier to generate additional +// backends, such as a direct-to-DEX version. +class JavaLangViewBuilder { + public: + JavaLangViewBuilder(std::string package, std::string layout_name, std::ostream& out = std::cout) + : package_(package), layout_name_(layout_name), out_(out) {} + + // Begin generating a class. Adds the package boilerplate, etc. + void Start() const; + // Finish generating a class, closing off any open curly braces, etc. + void Finish() const; + + // Begin creating a view (i.e. process the opening tag) + void StartView(const std::string& class_name); + // Finish a view, after all of its child nodes have been processed. + void FinishView(); + + private: + const std::string MakeVar(std::string prefix); + + std::string const package_; + std::string const layout_name_; + + std::ostream& out_; + + size_t view_id_ = 0; + + struct StackEntry { + // The class name for this view object + const std::string class_name; + + // The variable name that is holding the view object + const std::string view_var; + + // The variable name that holds the object's layout parameters + const std::string layout_params_var; + }; + std::vector view_stack_; +}; + +#endif // JAVA_LANG_BUILDER_H_ diff --git a/startop/tools/view_compiler/main.cc b/startop/tools/view_compiler/main.cc new file mode 100644 index 000000000000..0ad7e24feb3b --- /dev/null +++ b/startop/tools/view_compiler/main.cc @@ -0,0 +1,105 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "gflags/gflags.h" + +#include "java_lang_builder.h" +#include "util.h" + +#include "tinyxml2.h" + +#include +#include +#include +#include +#include + +using namespace tinyxml2; +using std::string; + +constexpr char kStdoutFilename[]{"stdout"}; + +DEFINE_string(package, "", "The package name for the generated class (required)"); +DEFINE_string(out, kStdoutFilename, "Where to write the generated class"); + +namespace { +class ViewCompilerXmlVisitor : public XMLVisitor { + public: + ViewCompilerXmlVisitor(JavaLangViewBuilder* builder) : builder_(builder) {} + + bool VisitEnter(const XMLDocument& /*doc*/) override { + builder_->Start(); + return true; + } + + bool VisitExit(const XMLDocument& /*doc*/) override { + builder_->Finish(); + return true; + } + + bool VisitEnter(const XMLElement& element, const XMLAttribute* /*firstAttribute*/) override { + builder_->StartView(element.Name()); + return true; + } + + bool VisitExit(const XMLElement& /*element*/) override { + builder_->FinishView(); + return true; + } + + private: + JavaLangViewBuilder* builder_; +}; +} // end namespace + +int main(int argc, char** argv) { + constexpr size_t kProgramName = 0; + constexpr size_t kFileNameParam = 1; + constexpr size_t kNumRequiredArgs = 2; + + gflags::SetUsageMessage( + "Compile XML layout files into equivalent Java language code\n" + "\n" + " example usage: viewcompiler layout.xml --package com.example.androidapp"); + gflags::ParseCommandLineFlags(&argc, &argv, /*remove_flags*/ true); + + gflags::CommandLineFlagInfo cmd = gflags::GetCommandLineFlagInfoOrDie("package"); + if (argc != kNumRequiredArgs || cmd.is_default) { + gflags::ShowUsageWithFlags(argv[kProgramName]); + return 1; + } + + const char* const filename = argv[kFileNameParam]; + const string layout_name = FindLayoutNameFromFilename(filename); + + // We want to generate Java language code to inflate exactly this layout. This means + // generating code to walk the resource XML too. + + XMLDocument xml; + xml.LoadFile(filename); + + std::ofstream outfile; + if (FLAGS_out != kStdoutFilename) { + outfile.open(FLAGS_out); + } + JavaLangViewBuilder builder{ + FLAGS_package, layout_name, FLAGS_out == kStdoutFilename ? std::cout : outfile}; + + ViewCompilerXmlVisitor visitor{&builder}; + xml.Accept(&visitor); + + return 0; +} \ No newline at end of file diff --git a/startop/tools/view_compiler/util.cc b/startop/tools/view_compiler/util.cc new file mode 100644 index 000000000000..69df41dff3d7 --- /dev/null +++ b/startop/tools/view_compiler/util.cc @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "util.h" + +using std::string; + +// TODO: see if we can borrow this from somewhere else, like aapt2. +string FindLayoutNameFromFilename(const string& filename) { + size_t start = filename.rfind("/"); + if (start == string::npos) { + start = 0; + } else { + start++; // advance past '/' character + } + size_t end = filename.find(".", start); + + return filename.substr(start, end - start); +} diff --git a/startop/tools/view_compiler/util.h b/startop/tools/view_compiler/util.h new file mode 100644 index 000000000000..03e093920bfa --- /dev/null +++ b/startop/tools/view_compiler/util.h @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#ifndef UTIL_H_ +#define UTIL_H_ + +#include + +std::string FindLayoutNameFromFilename(const std::string& filename); + +#endif // UTIL_H_ diff --git a/startop/tools/view_compiler/util_test.cc b/startop/tools/view_compiler/util_test.cc new file mode 100644 index 000000000000..d1540d3a6e43 --- /dev/null +++ b/startop/tools/view_compiler/util_test.cc @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "util.h" + +#include "gtest/gtest.h" + +using std::string; + +TEST(UtilTest, FindLayoutNameFromFilename) { + EXPECT_EQ("bar", ::FindLayoutNameFromFilename("foo/bar.xml")); + EXPECT_EQ("bar", ::FindLayoutNameFromFilename("bar.xml")); + EXPECT_EQ("bar", ::FindLayoutNameFromFilename("./foo/bar.xml")); + EXPECT_EQ("bar", ::FindLayoutNameFromFilename("/foo/bar.xml")); +} -- 2.11.0