summaryThis article introduces the resource confusion problem when plugins use host resources in the Android plugin framework, as well as the causes of the confusion, the industry's general solutions, and the optimization solutions we proposed. This article will explain step by step in the following order:
1. Dynamic resources in Android plug-inAndroid has been developed for so many years, and many plug-in/hot fix frameworks have emerged on the market. Both plug-in and hot fix are to achieve dynamic content outside the main apk, including dex (class), res (resources), so (dynamic library), etc. For each type of content, the industry has many implementation solutions. Although the solutions are different, the underlying principles are similar. There are also many articles and open source projects on the Internet for reference.
When it comes to dynamic Android resources, the ideas are similar:
Although the dynamic technology of Android resources is very mature, there are still many shortcomings in practice. For example, "fixed resources" are often complained by business colleagues. 2. Introduction to resources in AndroidBefore introducing resource pinning, let's first briefly introduce the basic knowledge related to resources in Android. 2.1 Resource IDs in AndroidAfter the Android code is compiled into an apk, each resource corresponds to a unique resource id, which is an 8-bit hexadecimal int value 0xPPTTEEEE:
Notice:
2.2 Resource usage in AndroidThere are usually two ways to use resources in Android:
[<package_name>].R.<resource_type>.<resource_name>
@[<package_name>:]<resource_type>/<resource_name>
So what is the difference between these two methods? From the perspective of code writing, resources are accessed through a resource name (resource_name). Let's decompile the apk and see what it looks like after compilation. Write the following code in the project app module, library module, and xml respectively Let's decompile the apk and see how these three codes are represented in the apk. It can be found that the resources in the appTest method and xml have become numbers (0x7f0e0069), and the resources in the libTest method are still accessed through Lcom/bytedance/lib/R$string;->test in conclusion:
So why is it referenced by field in libTest method but becomes a number in appTest? 2.3 Simple process of resource compilation in AndroidAssume that there is a project with only one app module, which depends on several third-party AARs through the Maven repository. The simplified process of compiling the project is as follows:
Therefore, it is easy to understand why libTest still uses resources through R, while appTest directly references them by value (inlined).
2.4 SummaryWhether the resources in Android are used through Java code or XML, they are ultimately searched through the resource ID value. Drag the apk into the as file and check the resources.arsc file. You can see that it contains the id index of all resources in the apk, as well as the real resource or value corresponding to the resource name. It is easy to imagine that when the App is running, it also searches for the real resource content through the resource id value through this resource table. 3. Plugins use host resources3.1 How plugins use host resourcesImagine that we want to make the live broadcast function of the App into a plug-in and distribute it dynamically. Most of the resources required for the live broadcast function are in the live broadcast plug-in, but there are always some resources from the host, such as the resources contained in some common UI components (support/androidx library), etc. So, suppose there is a picture named icon in the host, and the xml in the live plug-in references this picture through @drawable/icon, and also references it in the code through R.drawable.icon. In fact, there is no icon picture in the live plug-in, but it exists in the host. After the host is compiled, according to the previous knowledge points, the value corresponding to icon in the host is compiled into 0x7f010001. The plugin itself is also an apk. According to the knowledge points introduced above, after the plugin is compiled, @drawable/icon in the xml will be compiled into a value (0x7f010001), and R.drawable.icon in the java code will also be compiled into a value (0x7f010001) directly or indirectly. When the plugin runs on the host, according to the previous introduction, the plugin will search for 0x7f010001 and find that it can be found, so the host resources are used correctly.
3.2 What are the problems with plugins using host resources?As mentioned earlier, adding or deleting a resource may cause the IDs of many other resources to be changed. After our host is compiled, the icon is 0x7f010001. After a plug-in is compiled based on the existing host, the icon referenced in the plug-in is also 0x7f010001, so there is no problem at this time. After the host iteration, a new resource aicon is added. According to the resource ID allocation rules introduced above, the ID value of aicon in the new version of the host is 0x7f010001, and the ID value of icon is assigned to 0x7f010002. When the old version of the plug-in is sent to the new version of the host, it will still use 0x7f010001 to find the icon in the host, so it will naturally find the wrong one. If you are lucky, it may only cause an abnormal display of the image, but if you are unlucky, it may crash directly. 3.3 How to solve this problemTo solve this problem, the industry currently has a universal and stable solution - resource fixation. When the host is compiled, the resources used by the plug-in are fixed through the parameters provided by aapt2, so that the values of these resources will never change each time the host is packaged. Disadvantages of resource fixation solution:
In order to solve the above problems, we have developed a new solution to solve the resource confusion problem. 4. Free resource fixation solutionWhen the same version of a plug-in runs on different versions or even different apps, the plug-in code is fixed, but the resource ID in the host will change. In order to solve the resource confusion problem, the current idea is to ensure that the resource ID remains unchanged every time the host releases a new version. So is there a way to keep the plug-in consistent with the host's resource ID without constraining the host? When the plugin is packaged, the host is unknown, and when a plugin runs on multiple hosts, the hosts are also diverse. So there is no way to specify that the plugin should type the id to satisfy the host. As mentioned above, all places in the plugin that reference the host id are constants. So what can we do? Is it possible to dynamically modify the content of the plug-in when it runs on the host to achieve the effect of matching the plug-in and host ID values? For example, the plugin uses the host's resource icon, and the corresponding id value is 0x7f010001. When the plugin runs on a host with an icon of 0x7f010002, since the runtime resource search is performed by the id value, we can only know that the plugin is looking for a resource with an id of 0x7f010001. If we can map 0x7f010001 to the string icon by some means, and then use the Resources#getIdentifier method provided by the Android system to dynamically obtain the resource id corresponding to the icon in the current host, we can ensure that the plugin loads the correct resource. This work requires some work to be done when the plug-in is compiled and run. 4.1 Plugin compile-time work
As mentioned above, there are two main situations in which a plug-in uses host resources: 1. Through Java code 2. Through XML. 4.1.1 Handling host resources referenced in Java codeAfter the Java code is compiled into a class, some of the code that references the host resource ID will be compiled into a numerical value, while others will still be referenced through R. We can easily find the latter, but it is a bit difficult to find the former, because simply scanning the number starting with 0x7f in the class can easily lead to misjudgment and treat a meaningless number as a resource ID. We have discussed why the resource ID in the class is inlined as a value. So why not just not inline it? We only need to process R.jar during compilation and remove all final fields in the class to ensure that all resource IDs in the plugin that reference the host are referenced through R.
Start practicing
Summarize: The above mentioned problem of free resource fixation when referencing host resources in the plug-in Java code can be solved through some processing during compilation.
4.1.2 Processing resources referenced from the host in XML codeThe problem of referencing host resources in XML cannot be solved only at compile time, because XML cannot execute logic like Java code. As mentioned earlier, after XML is compiled, all resources are compiled into numerical values, and we cannot know which host it will run on in the future and what the value will be at compile time. Therefore, the work of modifying resource IDs in XML can only be moved to runtime. Of course, we also need to do something at compile time to assist the modification operation at runtime. At runtime, we need to modify the resources starting with 0x7f in the apk's xml, and change their values to the correct values corresponding to the current host. However, through xml, we can only get one value, so we can collect the xml files where the host resources used in the plug-in xml are located and their corresponding resource names when the plug-in is compiled. At runtime, we can use the mapRes method mentioned above to get the values that need to be modified. Start practicing As mentioned above, aapt2 generates an ap_ file after compilation/linking. This file contains all the compiled resources (including various XML, resources.arsc, AndroidManifest.xml) that will eventually enter the plug-in. We only need to analyze the resources starting with 0x7f referenced in these files, find the corresponding resource name according to R.txt (a file generated by aapt2), record the resource name, id value, and file into a file, and package them into the plug-in apk. As for how to scan the resources at 0x7f in these files, we used different methods at different stages. You can choose by yourself:
Summarize: The above generates a file that stores the information of the host resources used in the plugin xml. It looks like this:
4.2 Plugin Installation WorkAs mentioned above, when the plug-in is compiled, some logic is inserted into the Java code to achieve the effect that the plug-in dynamically obtains the resource ID according to the host environment. However, after the XML is compiled, the resource ID is directly compiled into a number, and logic cannot be inserted into the XML. Therefore, we can only modify it according to the host environment before running the plug-in. There is a plugin installation process before the plugin runs in the host, which is similar to the installation of apk in the Android system. Therefore, you only need to modify the xml and resources.arsc files in the plugin according to the configuration file generated during compilation and the mapRes method before each plugin installation or after the host upgrade. After determining the timing and content of the modification, the next step is to introduce in detail how to modify these files. 5. Modify the resource files in apk5.1 How to modify xml and arsc filesAfter being compiled into apk, layout, drawable, AndroidManifest and other files in Android are no longer regular XML files, but a new file format Android Binary XML, which we call axml here. So how to modify the axml file? All files have their own file formats. When reading files, the program reads byte arrays and then parses the meaning of each element in the byte array according to the file format. Therefore, we only need to understand the axml file format, parse the file according to the specification, find the location of the resource id in the byte array, map the original resource id to a new value according to the resMap method, and then modify the corresponding part in the byte array. (Fortunately, we only modify an 8-bit hexadecimal number in the axml file. This modification will not change the length, offset and other information of the content in the file, so we can directly replace the corresponding part of the byte array.) resources.arsc is the resource index table of apk, which records all the resources in apk. For resources of value type, the corresponding content of the resource will all enter resources.arsc, so we also need to modify this file (for example, if the parent of a style is the host resource, we need to modify it). The modification method is similar to XML. You only need to parse the byte array according to the specification, find the offset of the content to be modified, and replace it. There are many articles on the Internet introducing the file formats of axml and arsc, so I will not describe them in detail here. Apktool is a powerful, open source reverse tool that can decompile apk into source code. It must also have the code to read axml and arsc in apk, otherwise how can it output an editable xml source code file? So we can directly go to the code to read axml and arsc in apktool. When reading the id belonging to the host in axml, record the offset of the byte array and directly replace the byte subarray at the corresponding position.
The following is the dump of arsc. You can see that the parent of the last style is a host resource starting with 0x7f. The following is the dump of activity_plugin1.xml. You can see that the TextView references a resource in the host as the background. 5.2 Modify the xml/arsc file in apkAbove we know how to modify an axml and arsc file. When installing the plugin, we get an apk file, so how to modify the axml and arsc files in the apk? 5.2.1 Modification of recompression methodApk is actually a zip file. To modify the file content in apk, the simplest way that comes to mind is to read the file in the zipFile, modify it and then re-compress it. Java provides us with a set of APIs for operating zipFile. We can easily read the contents of the zip file into memory, modify it in memory, and then rewrite it into a new zipFile using ZipOutputStream. The code is very simple to implement. After the modification is successful, the test finds that it is feasible, so our first step is successful, which means that the way to dynamically modify the plug-in at runtime is feasible. I was secretly happy to find that the modification process was very time-consuming. Taking the company's live broadcast plug-in as an example (the live broadcast plug-in is about 30 MB, which is a relatively large plug-in), it takes about 8 seconds on devices with version 9.0 and above, about 20 to 40 seconds on devices with version 7 to 8, and about 10 to 20 seconds on devices below 7.x. Although the plug-in installation is done in the background, it is acceptable to increase the time appropriately, but a few tens of seconds of time is obviously unacceptable. Then we can only think of other ways.
5.2.2 Directly modify the byte array of apkApk is actually a zip file. For an introduction to zip files, please refer to the official documentation of Zip. To summarize briefly, a zip file consists of a data area, a central directory record area, and a central directory trailer area (higher versions of zip files add new content).
Start working After understanding the format of the zip file, we only need to find the offset of the file data we need to modify in the apk according to the file format protocol, and then directly modify the corresponding byte array in combination with the previous method of modifying the axml/arsc file. With the help of the RandomAccessFile tool provided by java, we can quickly read/write any position of the file. During the modification process, we found that most of the XML files in the apk are compressed (those in the res/xml directory are generally not compressed). This results in the byte array we get from the apk being compressed axml data. If we want to modify this data, we need to first decompress it using the Deflate algorithm (the Deflate algorithm is generally used in zip files), then modify and recompress it. However, after our modification, the recompressed data may not match the length of the data before the modification. If it is shortened, it is okay to modify the file metadata. If the file length becomes longer, it may cause the offset of subsequent files to change, which will affect the entire system. Fortunately, we can hack into the plugin packaging process. When we introduced "plugin compile-time work" earlier, we got the files that need to be modified during compilation, so we only need to control the apk packaging not to compress these files (in fact, Android Target30 also requires arsc files not to be compressed). This solves the problem very easily, but of course it will increase the size of the plugin package. Finally, in the live streaming plug-in, turning on this function will increase the package size by 20kb. For a live streaming plug-in with a size of nearly 30mb, this increase is acceptable and will not affect the host package size. (This increase depends on how much XML the plug-in uses host resources. Generally, the increase of the plug-in should be smaller than that of the live streaming plug-in.) After the transformation was completed, it was tested that the modification time of the live plug-in on various versions of mobile phones was about 300 to 700ms, and the modification speed increased by 10 to 90 times. Most plug-ins are also smaller than the live plug-in, and the time consumption can be guaranteed to be within 100ms. At the same time, this modification process is only done when the plug-in is installed for the first time or the host is upgraded, and it is completed in the background, so it is completely acceptable. |
<<: Creating a Line Chart with SwiftUI Charts in iOS 16
In terms of underlying logic and final results, t...
What are the common ways to attract new members t...
According to the definition of Baidu Encyclopedia...
The author summarizes the "three-part theory...
When a new product is just starting to operate, I...
If our computer browser has CSS compatibility iss...
Introduction: What content operation strategies d...
Event operations are more explosive and require a...
Isn’t it that product sales have encountered a bo...
The birth of iPhone in 2017 ushered in a new roun...
As a user product, short videos have unified the ...
We all know that the role of creativity is to att...
In 2019, Google publicly released a non-bundled t...
I wonder if advertisers often think about this qu...
Is the title of my article attractive to you? Obv...