The author

Hello everyone, my name is 🐜;

At present, I am mainly responsible for domestic related business development and some daily business.

background

SDK business recently made function embedded video live broadcast of the game, because of the different research and development of the game interface style is different, so we in the SDK UI standards, research and development can be according to the standard UI resource to replace the corresponding skin, do not need to change the contents of the SDK, choose the corresponding change skin SDK version out skin replacement can be realized.

Project research

There are Resource wrapper stream and AssetManager replacement stream on the market.

The Resource wrapper flow works like this:
1, Create a new Resrouce object (Resource of the proxy)
2. Replace the system Resource object
3. Dynamic mapping at runtime (same Type and Name in different resource tables for the same resource)
4. XML layout parsing interception (resources in XML layout cannot be loaded by proxy Resource, LayoutInflater)
Advantage:

Support the String/Layout

Existing problems:

1. The efficiency of resource acquisition is affected

2. The Style and Asset directories are not supported

3. There are many substitutions for Resource and large amount of code for Resource packaging class

Resource redirection:

Support dynamic mapping

The AssetManager replacement stream works as follows:
1. Hook system AssetMananger object (system resource Path and application resource Path are added to the Path of AssetManager)
Static alignment at compile time (the id value of the resource file in the skin package is changed to be the same as that in the application)
Advantage:

Support style, Asset directory, replace AM instances concise

Existing problems:

Strongly dependent compiler resource IDS

Resource redirection:

Dynamic mapping is not supported

Scheme selection

Resource LayoutInflater is adopted for the following reasons:

  1. There is no need to maintain apK for skin packs, reducing SDK maintenance costs and r&d access costs
  2. Dynamic resource mapping is required
  3. The business only needs to support image replacement

So the development access process is:

  1. For each SDK version, set the skin image resources and name and output them to the skin description document (shared by Android and iOS).
  2. The access party generates the slice map according to the peel instruction document, and then places the slice resource in the specified asset directory
  3. Parent package select the corresponding skin SDK package

The principle of analysis

LayoutInflater analysis

  1. How are LayoutInflaters instantiated

We usually call the following code to get a LayoutInflater using the flow

LayoutInflater.from(context)
Copy the code

Context usually passes an instance of an activity

Inflater_service name not found, look at the parent class

The key point is the instance returned by getBaseContext(). By analyzing the context, getBaseContext() returns the member variable of ContextWrapper, which is passed in the attachBaseContext method, as shown in the figure below

Because the Activity is reflected through the instance in the ActivityThread, find the following code in the ActivityThread

The instance of appContext is ContextImpl

Based on the trace above, instances of LayoutInflater are generated as shown below

  1. How are views generated from LayoutInflater, as shown in the figure below

By analyzing the LayoutInflater class, we can set the factory instance in blue to intercept the system View generation. So we can replace the default XML resource by dynamically mapping resources to createView, which corresponds to the factory instance, using the implementation logic as shown in yellow

3. Use the factory

So we can use the setFactory and setFactory2 methods of LayoutInflater to intercept our business in corresponding instances. The functions of these two methods are basically the same. SetFactory2 was introduced after SDK>=11. So we call the above methods depending on the SDK version. The v4 package has a class called LayoutInflaterCompat to help us with compatibility operations, providing methods as follows:

LayoutInflaterCompat
- setFactory(LayoutInflater inflater, 
             LayoutInflaterFactory factory)
Copy the code

AppCompatActivity conflict processing

1. SetFactory2 of LayoutInflater is used internally, and the process is roughly as follows:

Through the above analysis, its internal logic is the same as our skin logic

2. Exception handling

If we’re in AppCompatActivity onCreate () after setting LayoutInflaterCompat. SetFactory2 throws an exception

Through code analysis

A LayoutInfalter call to setFactory or setFactory2 will raise an exception the next time it is called.

So we’re AppCompatActivity onCreate () set before LayoutInflaterCompat. SetFactory2? , through the source code analysis

While this does not run an exception, it does not execute the onCreateView method to AppCompatDelegateImpl, which is equivalent to invalidation of the Factory set by AppCompatDelegateImpl. So we need to do factory processing on AppCompatDelegateImpl on our own factory. Because the method below AppCompatDelegateImpl is public

So we can handle the above situation by calling the AppCompatDelegate method on our factory processing logic

View view = delegate.createView(parent, name, context, attrs);
Copy the code

So the processing logic looks something like this

LayoutInflaterCompat.setFactory(LayoutInflater.from(this), new LayoutInflaterFactory() { @Override public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {// Here you can directly new a custom View // here you can replace the system class with a custom View //appcompat create View code AppCompatDelegate delegate = getDelegate(); View view = delegate.createView(parent, name, context, attrs); return view; }});Copy the code

As a bonus, since our business involves cutting packages, we can’t confirm whether the game’s code uses AppCompatActivity, which also involves support and compatible packages on AndroidX, as shown above

Appcompat creates view code AppCompatDelegate delegate = getDelegate(); View view = delegate.createView(parent, name, context, attrs);Copy the code

Reflection is used to deal with this compatibility problem.

So the treatment should be:

Don’t consider compatibility with appactivity:

Just set our custom Factory directly before the Activity setContentView.

Consider compatibility with AppCompatActivity:

Call our custom factory before AppCompatActivity onCreate(), and then call the AppCompatActivity method on our custom factory to make it compatible with the existing processing of AppCompatActivity.

Detailed analysis of XML -> View instances

  1. System views in XML do not need a prefix to be loaded into?

Because PhoneLayoutInflater lives a field

  private static final String[] sClassPrefixList = {
        "android.widget.",
        "android.webkit.",
        "android.app."
    };
Copy the code

Then onCreateView(String name, AttributeSet attrs) is prefixed to load, as shown below

for (String prefix : sClassPrefixList) { try { View view = createView(name, prefix, attrs); if (view ! = null) { return view; } } catch (ClassNotFoundException e) { // In this case we want to let the base class take a crack // at it. } } return super.onCreateView(name, attrs);Copy the code

The other prefix in the parent class is Android.view.

protected View onCreateView(String name, AttributeSet attrs)
            throws ClassNotFoundException {
        return createView(name, "android.view.", attrs);
    }
Copy the code

In LayoutInflater createView(String Name, String prefix, AttributeSet attrs), LayoutInflater’s createView(String Name, String prefix, AttributeSet Attrs) method, LayoutInflater’s createView(String name, String prefix, AttributeSet Attrs) Reflection corresponds to the Constructor argument types that need context. class and attributeset. class, otherwise exceptions will be thrown, critical code


            Object lastContext = mConstructorArgs[0];
            if (mConstructorArgs[0] == null) {
                // Fill in the context if not already within inflation.
                mConstructorArgs[0] = mContext;
            }
            Object[] args = mConstructorArgs;
            args[1] = attrs;

            final View view = constructor.newInstance(args);
Copy the code

3. Look at the source code in the process, the view of parames is generated by the method of parentview, params = root. GenerateLayoutParams (attrs), I thought parames were generated in the view’s own method until I set the value of the corresponding attribute by parsing the content of the XML. Some screen adaptation solutions are handled by overriding generateLayoutParams.

Develop business processes

The services to be handled are as follows

1. Call the SkinManager initialization method in the onCreate method of each activity.

2. The SkinManager initialization method registers the View we need to skin.

3, in our custom Factory implementation class onCreateView method mimicking the system loading mechanism to load our skin View and get the corresponding resource properties and values.

4. Load external skin resources dynamically according to the obtained resource values. The principle is to obtain the resource ID, obtain the corresponding resource string, and then check the path of loading external resources through string.

The last

The above is the research and thinking on the landing of LayoutInflater skin changing scheme. I hope it will be helpful to you and welcome to discuss with you.