Android Componentized Architecture Learning Notes - Static Variables/Resources/Confusion/Multi-Channel Packaging for Componentized Programming

1. Componentized static variables:

  • Generation of R.java:

Each module generates an aar file and is referenced in the Application module, which is merged into an apk file.When each secondary module is decompressed in the Application module, the resource R.java is decompressed into build/generated/source/r/debug(release)/package name/R.java at compile time.

When the aar files in each component are summarized in the App module, that is, during the initial resolving resource phase of compilation, when the R.java of each module is released, all the R.java files are detected, merged, and finally merged into a unique R.java resource.

ButterKnife Is an injection framework that focuses on Android View and can significantly reduce third-party libraries for findViewById and setOnClickListener operations.

Constants can only be used in annotations, and errors such as attribute value must be contant are prompted if they are not constants.Alternatively, you can use a copy of the R.java file named R2.java.Then add the final modifier to the R2.java variable, referencing the R2 resource directly where relevant.

If used in the project ButterKnife The cost of schema adaption using R2.java is the lowest if maintenance iterates for a period of time.

The best solution is to use findViewById instead of the annotation generation mechanism.

Here you can use generics to encapsulate findViewById to reduce the amount of code you write:

 @Override
    protected void onCreate(@androidx.annotation.Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        TextView textView = generateFindViewById(R.id.rl_full_view);
    }
    
    protected <T extends View> T generateFindViewById(int id) {
        //return to view with generic T
        return (T)findViewById(id);
    }

 

2. Resource conflicts:

In componentization, the Base module and function module are based on the Library module, which is compiled by dependent rules in turn. The bottom-level Base module is compiled into an aar file first, and then the last-level compile decompresses the dependent aar file into the module build because of the compile dependency.

AndroidMainfest conflict issues:

The app:name attribute of the application is referenced in AndroidMainfest, and in case of conflict, you need to use tool:replace= "android:name" to declare that the application is replaceable.Some issues where attributes in AndroidMainfest.xml are replaced can be resolved using tool:replace.

Package conflict:

If you want to use low-priority dependencies, you can use exclude to exclude dependencies.

compile('') {
        exclude group:''
    }

Resource name conflict:

In multiple module development, it is not guaranteed that all resources in multiple modules will be named differently.If the same happens, it may cause problems with incorrect resource references.Typically, a post-compiled module overwrites the content in the resource field of a previously compiled module.

Solution: One is to use renaming when resources conflict.This requires that we start naming resources differently between different modules, which is a constraint of the coding specification, and Gradle's naming hint mechanism, which uses fields:

android {
    resourcePrefix "Component Name_"
}

All resource names must be prefixed with the specified string. Otherwise, errors will occur. The value resourcePrefix can only limit resources in xml, not picture resources. All picture resources still need to be manually modified.

3. Componentized confusion:

Confusion Base:

Confusion includes optimization processes such as code compression/code confusion and resource compression.

Android Studio uses ProGuard To confuse, ProGuard is a tool for compressing/optimizing and confusing Java bytecode files. It can delete useless classes/fields/methods and attributes, delete useless comments, and maximize the optimization of bytecode files.It can also use short, meaningless names to rename existing classes/fields/methods and attributes.

The confused process for Android projects helps avoid the 64k method bottleneck by removing unused class/class members/methods/attributes from their main project and dependent libraries, while renaming class/class members/methods to meaningless short names makes reverse engineering more difficult.

Confusion removes unnecessary resources from the project and effectively reduces the size of the apk installation package.

Confusion has four operations: Shrinking/Optimiztion/Obfuscation/Preverification.

  buildTypes {
        release {
            minifyEnabled false     //Whether to turn on confusion
            shrinkResources true    //Whether to turn on resource confusion
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'     
            //Rule traversal for setting proguard
        }
    }

Each module creates a confusion file, proguard-rules.pro, which is basically empty.

#Specify compression level

-optimizationpasses 5

#Do not skip class members of non-public Libraries

-dontskipnonpubliclibraryclassmembers

Algorithms to be used in #confusion

-optimization !code/simpliffcation/arithetic,!field/*,!class/merging/*

#Confused the method name in the obfuscation class as well

-useuniqueclassmembernames

#Optimize to allow access to and modify modifiers for classes and class members

-allowaccessmodification

#Rename the file source to the SourceFile string

-renamesourefileattribute SoureFile

#Keep line numbers

-keepattributes SoureFile,LineNumberTable

Key process logs are printed when:

-dontpreverify

Whether to log in case of confusion

-verbose

Internal structure of all class es in #apk package

-dump class_files.txt

#Unconfused Classes and Members

-printseeds seed.txt

#Lists the code deleted from the apk

-printusage unused.txt

Mapping before and after confusion

-printmapping mapping.txt

Confusion should not be used in the following situations:

  • The elements used in reflection need to be guaranteed the same class name/method name/attribute name, otherwise they will not be reflected after confusion.
  • It is best not to confuse some bean objects;
  • Confusion is not recommended for the four components. The four components are registered in AndroidManifest and the class name changes after confusion, which does not conform to the registration mechanism for the four components.
-keep public class * extend android.app.Activity
-keep public class * extend android.app.Application
-keep public class * extend android.app.Service
-keep public class * extend android.app.content.BroadcastReceiver
-keep public class * extend android.app.content.ContentProvider
-keep public class * extend android.app.backup.BroadAgentHelper
-keep public class * extend android.app.preference.Preference
-keep public class * extend android.app.view.View
-keep public class * extend android.app.verding.licensing.ILicensingService
  • Annotations cannot be confused, and in many scenarios, annotations are used to reflect elements at run time.
-keepattributes *Annotation
  • value and valueOf methods in enumerations cannot be confused because they are added statically to code to run and are also used by reflection.Applying enumeration adds many methods, increases the number of methods in the package, and increases the size of dex.
-keepclassmembers enum * {
    public static **[] values();
    public static ** vauleOf(java.lang.String);
}
  • JNI calls Java methods, which need to be formed from an address consisting of the class name and the method name;
  • Java uses the Native method, which is written in C/C++, and cannot be confused together;
-keepclasswithmembername class * {
    native <methods>;
}
  • JS calls Java methods;
-keepattributes *JavascriptInterface*

 

  • JavaScript calling methods in WebView cannot be confused;
-keepclassmembers class fqcn.of.javascript.interface.for.Webview {
    public *;
}
-keepclassmembers class * extends android.webkit.WebViewClient {
    public void *(android.webkit.Web,java.lang.String,android.graphics.Bitmap);
    public boolean *(android.webkit.Web,java.lang.String);
}
-keepclassmembers class * extends android.webkit.WebViewClicent {
    public void *(android.webkit.Web,java.lang.String);
}
  • Third-party libraries recommend using their own confusion rules;
  • The subclasses of Parcelable and static member variables of Creator should not be confused, otherwise android.os.Bad-ParcelableExeception will occur.
-keep class * implement android.os.Parcelable {
    public static final android.os.Parcelable$Creator *;
}
-keepclassmembers class * implements java.io.Seriablizable {
    static final long seriablVersonUID;
    private static final java.io.ObjectStreamField[] seriablPersistentFields;
    private void writeObject(java.io.ObjectOutputStream);
    private void readOject(java.io.ObjectInputStream);
    java.lang.Object writeReplace();
    java.lang.Object readResolve();
}

 

  • Gson's sequence number and deserialization are essentially classes parsed using reflection;
-keep class com.google.gson.** {*;}
-keep class sun.misc.Unsafe {*;}
-keep class com.google.gson.stream.** {*;}
-keep class com.google.gson.examples.android.modle.**{*;}
-keep class com.google.** {
    <fields>;
    <methods>;
}
-dontwarn com.google.gson.**
  • Use keep annotation to create annotation classes where you don't want to be confused about "keep";
package com.demo.annotation;
//@Target(ElementType.METHOD)
public @interface Keep {

}

@Target can control its available range to class/method variables.Later declared in proguard-rules.pro;

-dontskipnonpubliclibrayclassmember
-printconfiguration
-keep,allowobfusation @interfaces android.support.annotation.Keep
-keep @andriod.support.annotation.Keep class *
-keepclassmen=mbers class * {
    @android.support.annotation.Keep *;
}

As long as one confusion principle is remembered: confusion changes the Java pathname, it is important to keep the path unambiguous.

Resource Confusion:

ProGuard is a Java obfuscation tool, and it can only obfuscate Java files. In fact, it can continue to be further obfuscated, confusing resource file paths.

Resource confusion, in fact, is also the confusion of resource names.There are three possible approaches:

  • Modification at the source level, replacing R.string.xxx with R.string.a in the code and XML, renaming some picture resources xxx.png to a.png, and then giving them to Android for compilation;
  • All resource ID s are compiled to 32-bit int values. You can see that the R.java file saves the resource values, directly modifies to the binary data of resources.arsc, does not change the packaging process, modifies resources.arsc after it is generated, and renames the resource file.
  • Process the installation package directly, unzip it, modify the resources.arsc file directly, and repackage it.

WeChat AndResGuard Resource confusion mechanisms.

Componentized confusion:

After each module is created, a custom confusion file for proguard-rule.pro comes with it.Each module can also have its own confusing rules.

However, in componentization, if each module is confused with its own, repeated confusion will occur, causing the problem of not querying resource files.

To solve this problem, you need to ensure that the apk is generated at times with only one confusion.

  • The first option is the simplest and most intuitive, with only confusion set in the Application module and all other modules turning it off.Confused rules are then placed in the application module's proguard-rule.pro file.The disadvantage of this confusion method is that when some modules are removed, the confusion rules need to be removed manually.Although adding more confusion in theory will not cause ruin or fail compilation, unnecessary confusion filtering will still affect compilation efficiency.
  • The second option is to start a command to synthesize the proguard-rule.pro files of multiple modules referenced when the Application module is confused, then overwrite the confusion files in the Application module.This decouples confusion conditions into each module, but you need to write a Gradle command to configure the operation, which adds a composite operation to each build and also affects compilation efficiency.
  • The third option is that the Library module itself has settings to package the proguard-rule.pro file into an aar.The consumerProguardFiles flag can be relied on in open source libraries to specify how the library should be confused. The consumerProguardFiles property packages the *.profile into an aar, and this confusion profile will be used automatically when the library is confused.

When the Application module confuses all typed code summaries, the Library module is packaged as release.aar, then referenced summaries, which are confused individually through the proguard.txt rule, guaranteeing only one confusion.

Fixed third-party confusion is placed here in the Base module proguard-rule.pro, and each module's unique reference library confusion is placed in its own proguard-rule.pro.Finally, the proguard-rule.pro file of the App module contains the Android basic attribute confusion statement.

 

4. Multi-channel packaging:

Think of development tools as production factories, use code and resources as raw materials, and use the least code consumption to build different channels and versions of products.

Multi-Channel Base:

When we need to find out which channel users are changing, which channel users are sticky, which channel needs more personalized design, we can get all kinds of information such as application version number/version name/system version/model by Android system method, but only the information of the application store (channel) can not be obtained from the system.Yes, we can only think of adding channel information to the apk.

There are two things we need to focus on in multi-channel packaging:

  • Write channel information to apk file;
  • Transfer channel information from apk to background.

Packaging must go through the process of signing, while Android has two different ways of signing:

  • Prior to Android 7.0, the v1 signature was jar signature, derived from JDK;
  • After Android 7.0, the v2 signature method was introduced as a unique apk signature for Android, which is valid only for Android 7.0 and not for Android 7.0.
 signingConfigs{
        release{
            v2SigningEnabled false
        }
    }

apk is a ZIP format file in our province. The difference between v2 signature and ordinary ZIP format packaging is that ordinary zip file has three blocks, while v2 signature apk has four blocks, the extra blocks are used for v2 signature verification.If the other three blocks are modified, they can not escape v2 verification, which directly leads to the failure of verification, so this is why v2 signatures are more secure than v1.

Batch packing:

Packaging with native Gradle can be time consuming due to large projects and multi-channel packages. If errors are found during the packaging process and problems need to be repaired, the speed will be doubled.As a result, bulk packaging technology has become popular.

1. Package with Python:

  • Download and install the Python environment, recommended AndroidMultiChanneBuildTool .This tool only supports v1 signatures, makes ChannelUtil.Java code ready for use in a project, gets the channel number when the app starts and delivers it to the background (AnalyticsConfig.setChannel(ChannelUtil.getChannel(this));
  • Put the generated APK package (project/build/outputs/release.apk) in the PythonTool folder;
  • Edit the channel list in PythonTool/info/channel.txt, separated by line breaks;
  • There is an Android MultiChannelBuildTool.py file in the PythonTool directory. Double-click to run the file and you will begin packaging.When finished, a folder named output_app-release appears in the PythonTool directory, which contains the packaged channel package.

2. Use officially provided methods to achieve multi-channel packaging:

  • Add channel identification to AndroidManifest.xml and write a meta tag;
<meta-data android:name="channel" android:value="${channel}"/>
  • Configure productFlavors in build.gradle in the app directory:
   productFlavors {
        qihu360{}
        yingyongbao{}

        productFlavors.all {
            flavor -> flavor.manifestPlaceholders = [channel : name]
        }
    }
  • Select the setup channel in Android Studio Build ->Generate signed apk.

This allows you to package packages from different channels. After Android Studio buildd Variants in the lower left corner, you can also choose to compile debug and release versions, typing out all the packages at once, using the Gradle command: *. /gradlew build

3. Add zip Comment after apk file

The apk file is essentially a zip file with signature information and conforms to the zip file format specification.The signed apk file has four blocks. At the end of the signature block is the zip file comment, which contains two fields: Comment Length, which represents the length of the comment, and File Comment, which represents the content of the comment. Correct modification of these two fields will not damage the zip file.Use this field to add data about channel information, recommended packer-ng-pugin Package.

4. Volume packaging tool compatible with v2 signatures walle

The above four packaging methods are speed and compatibility, zip comment and Meituan walle packaging method, do not need to recompile, only decompress/add channel information in the packaging operation and can be compatible with v1 and v2 signature packaging.The best compatibility is native Gradle packaging.

Multi-Channel Module Configuration:

When you need to customize some of your requirements across multiple channels or scenarios, you must build app s using native Gradle.

Here is an example of a demonstration:

productFlavors {
        //User Version
        client {
            manifestPlacehoders = [
                channel:"10086",     //Channel Number
                verNum:"1",          //version number
                app_name:"Gank"      //app name
            ]
        }

        //Service Version
        server {
            manifestPlacehoders = [
                    channel:"10087",     //Channel Number
                    verNum:"1",          //version number
                    app_name:"Gank Service Edition"      //app name
            ]
        }
       
    }

dependencies {
    
    clientCompile project(':settings')  //Introducing a customer-specific module
    clientCompile project(':submit') 
    clientCompile project(':server_settings')  //Introduce service version specific module

}

Multichannel is set here by the productFlavors property, while manifestPlaceholders set different properties in different channels that need to be declared in AndroidMainfest to be used.Set up xxCompiles to configure module files that need to be referenced by different channels.

Next, it is declared in the AndroidMainfest.xml of the app module:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    package="com.example.demo1">

    <application
        android:name=".basemodule.BaseApplication"
        android:allowBackup="true"
        android:extractNativeLibs="true"
        <!--app Name Reference-->
        android:label="${app_name}"
        tools:replace="label"
        android:supportsRtl="true"/>
    <!--Version number declaration-->
    <meta-data android:name="verNum" android:value="${verNum}"/>
    <!--Channel Name Statement-->
    <meta-data android:name="channel" android:value="${channel}"/>
</manifest>

The android:label property is used to change the signature, ${xxx} automatically references the key value corresponding to manifestPlaceholders.Finally, replacing the property name requires adding the tool:replace property, which prompts the compiler to replace the property.

Declare meta-data for some additional custom properties that can be obtained by reading package information from code:

public class AppMetaUtils {
    public static int channelNum = 0;

    /**
     * Get meta-data value
     * @param context
     * @param metaName
     * @return
     */
    public static Object getMetaData(Context context,String metaName) {
        Object obj = null;
        try {
            if (context != null) {
                String pkgName = context.getPackageName();
                ApplicationInfo applicationInfo = context.getPackageManager().getApplicationInfo(pkgName
                        , PackageManager.GET_META_DATA);
            }
        }catch (Exception e){
            Log.e("AppMetaUtils",e.toString());
        }finally {
            return obj;
        }
    }

    /**
     * Get Channel Number
     * @param context
     * @return
     */
    public static int getChannelNum(Context context) {
        if (channelNum <= 0) {
            Object object = AppMetaUtils.getMetaData(context,"channel");
            if (object != null && object instanceof Integer){
                return (int)object;
            }
        }
        return channelNum;
    }
    
    
}

Use the getApplicationInfo method to get the application information, then read the different key values in meta-data to get the channel number further.

 /**
     * Jump to Settings Page
     */
    public void navigationSettings() {
        String path = "/gank_setting";
        if (channel == 10086) {
            path +="/1";
        }else if (channel == 10087){
            path += "_server/1";
        }
        ARouter.getInstance().build(path).navigation();
    }

The above is an example of a value call.If a class call is required, the path can be passed directly as a value, then the object can be created using reflection:

productFlavors {
        //User Version
        client {
            manifestPlacehoders = [
                channel:"10086",     //Channel Number
                verNum:"1",          //version number
                app_name:"Gank"      //app name
                setting_info:"material.com.setting.SettingInfo"//Set Data File
            ]
        }

        //Service Version
        server {
            if(!project.ext.isLib) {
                application project.ext.applicationId + '.server' //appId
            }
            manifestPlacehoders = [
                    channel:"10087",     //Channel Number
                    verNum:"1",          //version number
                    app_name:"Gank Service Edition"      //app name
                    setting_info:"material.com.server_setting.ServerSettingInfo"//Set Data File
            
            ]
        }
       
    }

Declare a meta-data for passing class names:

    <meta-data android:name="setting_info" android:value="${setting_info}"/>

Get the classes you need to call from the previously encapsulated getMetaData:

 /**
     * Get Settings Information Path
     * @param context
     * @return
     */
    public static String getSettingInfo(Context context) {
        if (settingInfo == null){
            Object object = AppMetaUtils.getMetaData(context,"setting_info");
            if (object != null && object instanceof Integer) {
                return (String)object;
            }
        }
        return settingInfo;
    }

You then need a public method call, which can be declared in the Base module as an interface and extended in the functional module.

public interface SettingImp {
        void setData(String data);
    }

This interface implementation is inherited in the client and server, respectively:

public class SettingInfo implements SettingImp{

        @Override
        public void setData(String data) {
            //Processing data
        }
    }

public class ServerSettingInfo implements SettingImp {

        @Override
        public void setData(String data) {
            //Processing data
        }
    }

Next, you can encapsulate it again in the Base module and get the calling method:

  public static void SettingData(Context context,String data) {
        if (getSettingInfo(context) != null){
            Log.e("AppMetaUtils","setting_info is no found");
        }
        
        try{
            Class<?> clazz = Class.forName(getSettingInfo(context));
            SettingImp imp = (SettingImp)clazz.newInstance();
            imp.setData(data);
        }catch (ClassNotFoundException e) {
            Log.e("AppMetaUtils","getSettingInfo error:"+e.toString());
        }catch (InstantiationException e) {
            Log.e("AppMetaUtils","getSettingInfo error:"+e.toString());
        } catch (IllegalAccessException e) {
            Log.e("AppMetaUtils","getSettingInfo error:"+e.toString());
        }
    }

Initialize the interface using reflection and make it a common call.A deeper level of application needs to be adjusted in real demand.

Tags: Android Java Gradle xml

Posted on Thu, 12 Sep 2019 20:00:22 -0700 by shseraj