Environment Switcher is a one-click Environment Switcher using Java annotations, APT, reflection, obfuscation, etc.
It has been a week since Environment Switcher was officially released. This week, with the release of Environment Switcher 1.4, here is the interpretation of the principle of Environment Switcher.
If you are not familiar with EnvironmentSwitcher, it is recommended to read the article “How about EnvironmentSwitcher?”
This paper is based on Environment Switcher 1.4 analysis.
The Environment Switcher review
As anyone who has used Environment Switcher knows, simply configure the Environment according to the modules in the application, and the Environment Switcher automatically generates a series of methods. For example, the following code is the environment for configuring the Music module:
public class EnvironmentConfig {
@Module(alias = "Music")
private class Music {
@Environment(url = "https://www.codexiaomai.top/api/", isRelease = true.alias = "Official")
private String online;
@Environment(url = "http://test.codexiaomai.top/api/".alias = "Test")
private String test; }}Copy the code
After compiling, the Environment Switcher automatically generates less than 100 lines of code, including toggle/get environments, add/remove Environment toggle listening events, get all modules/environments, and so on.
public final class EnvironmentSwitcher {
private static final ArrayList ON_ENVIRONMENT_CHANGE_LISTENERS = new ArrayList<OnEnvironmentChangeListener>();
private static final ArrayList MODULE_LIST = new ArrayList<ModuleBean>();
public static final ModuleBean MODULE_MUSIC = new ModuleBean("Music"."Music");
private static EnvironmentBean sCurrentMusicEnvironment;
public static final EnvironmentBean MUSIC_ONLINE_ENVIRONMENT = new EnvironmentBean("online"."https://www.codexiaomai.top/api/"."Official", MODULE_MUSIC);
public static final EnvironmentBean MUSIC_TEST_ENVIRONMENT = new EnvironmentBean("test"."http://test.codexiaomai.top/api/"."Test", MODULE_MUSIC);
private static final EnvironmentBean DEFAULT_MUSIC_ENVIRONMENT = MUSIC_ONLINE_ENVIRONMENT;
static {
ArrayList<EnvironmentBean> environments;
MODULE_LIST.add(MODULE_MUSIC);
environments = new ArrayList<>();
MODULE_MUSIC.setEnvironments(environments);
environments.add(MUSIC_ONLINE_ENVIRONMENT);
environments.add(MUSIC_TEST_ENVIRONMENT);
}
public static void addOnEnvironmentChangeListener(OnEnvironmentChangeListener onEnvironmentChangeListener) {
ON_ENVIRONMENT_CHANGE_LISTENERS.add(onEnvironmentChangeListener);
}
public static void removeOnEnvironmentChangeListener(OnEnvironmentChangeListener onEnvironmentChangeListener) {
ON_ENVIRONMENT_CHANGE_LISTENERS.remove(onEnvironmentChangeListener);
}
public static void removeAllOnEnvironmentChangeListener() {
ON_ENVIRONMENT_CHANGE_LISTENERS.clear();
}
private static void onEnvironmentChange(ModuleBean module, EnvironmentBean oldEnvironment, EnvironmentBean newEnvironment) {
for (Object onEnvironmentChangeListener : ON_ENVIRONMENT_CHANGE_LISTENERS) {
if (onEnvironmentChangeListener instanceof OnEnvironmentChangeListener) {
((OnEnvironmentChangeListener) onEnvironmentChangeListener).onEnvironmentChange(module, oldEnvironment, newEnvironment);
}
}
}
public static final String getMusicEnvironment(Context context, boolean isDebug) {
return getMusicEnvironmentBean(context, isDebug).getUrl();
}
public static final EnvironmentBean getMusicEnvironmentBean(Context context, boolean isDebug) {
if(! isDebug) {return DEFAULT_MUSIC_ENVIRONMENT;
}
if (sCurrentMusicEnvironment == null) {
android.content.SharedPreferences sharedPreferences = context.getSharedPreferences(context.getPackageName() + ".environmentswitcher", android.content.Context.MODE_PRIVATE);
String url = sharedPreferences.getString("musicEnvironmentUrl", DEFAULT_MUSIC_ENVIRONMENT.getUrl());
String environmentName = sharedPreferences.getString("musicEnvironmentName", DEFAULT_MUSIC_ENVIRONMENT.getName());
String appAlias = sharedPreferences.getString("musicEnvironmentAlias", DEFAULT_MUSIC_ENVIRONMENT.getAlias());
for (EnvironmentBean environmentBean : MODULE_MUSIC.getEnvironments()) {
if (android.text.TextUtils.equals(environmentBean.getUrl(), url)) {
sCurrentMusicEnvironment = environmentBean;
break; }}}return sCurrentMusicEnvironment;
}
public static final void setMusicEnvironment(Context context, EnvironmentBean environment) {
context.getSharedPreferences(context.getPackageName() + ".environmentswitcher", android.content.Context.MODE_PRIVATE).edit()
.putString("musicEnvironmentUrl", environment.getUrl())
.putString("musicEnvironmentName", environment.getName())
.putString("musicEnvironmentAlias", environment.getAlias())
.apply();
if(! environment.equals(sCurrentMusicEnvironment)) { onEnvironmentChange(MODULE_MUSIC, sCurrentMusicEnvironment, environment); } sCurrentMusicEnvironment = environment; } public static ArrayListgetModuleList() {
returnMODULE_LIST; }}Copy the code
In addition to automatically generating the code above, The Environment Switcher also provides an Activity page that displays and switches a list of environments. Why is the Environment Switcher so powerful?
This is because it stands on the shoulders of four giants, Java annotations APT Reflection and obfuscation. I’m sure you’ve all heard of them, as they are used in popular open source libraries such as Retrofit, Butter Knife GreenDao, etc., which I won’t go into here.
Composition and principles of Environment Switcher
Open up the Environment Switcher project directory, We will see that Environment Switcher consists of five modules, base Compiler Compiler-Release EnvironmentSwitcher and SAMPLE.
- Base: contains all annotations
@Moduel
和@Environment
, and the Java Bean class:ModuleBean
,EnvironmentBean
, listening event:OnEnvironmentChangeListener
And a class that stores public static constants:Constants
. Several other modules depend on this module. - Compiler: Contains only one class
EnvironmentSwitcherCompiler
Use APT to handle annotated class and attribute generation when compiling Debug versionsEnvironmentSwitcher.java
File. - The compiler – release: and
compiler
Modules, like modules, contain only one classEnvironmentSwitcherCompiler
Use APT to handle annotated class and attribute generation when compiling the ReleaseEnvironmentSwitcher.java
File. - Environmentswitcher: Obtained by reflection
EnvironmentSwitcher.java
And provides a list display and an Activity page to switch environment functions. - Sample:
Environment Switcher
Sample projects for standard usage methods.
Why do the Debug and Release versions use different annotation tools
Since the test environment is only used during the Debug and test phases, only the official environment is used in the Release, and if the test environment is not hidden in the Release, it is packaged into the APK, which may cause unnecessary trouble or loss if acquired by others.
How do I automatically hide the test environment
Let’s start by comparing the main differences between compiler and the EnvironmentSwitcher.java file generated by Compiler-release. The main difference is the generated EnvironmentBean static constant, which differs as follows:
- The Debug version of EnvironmentSwitcher. Java
public static final EnvironmentBean MUSIC_ONLINE_ENVIRONMENT = new EnvironmentBean("online"."https://www.codexiaomai.top/api/"."Official", MODULE_MUSIC); public static final EnvironmentBean MUSIC_TEST_ENVIRONMENT = new EnvironmentBean("test"."http://test.codexiaomai.top/api/"."Test", MODULE_MUSIC); Copy the code
- The Release version of EnvironmentSwitcher. Java
public static final EnvironmentBean MUSIC_ONLINE_ENVIRONMENT = new EnvironmentBean("online"."https://www.codexiaomai.top/api/"."Official", MODULE_MUSIC); public static final EnvironmentBean MUSIC_TEST_ENVIRONMENT = new EnvironmentBean("test".""."Test", MODULE_MUSIC); Copy the code
Through comparison, it can be found that there is only one difference, that is, the specific address of the informal environment in the Release version is an empty string. In this way, the effect of hiding the specific address of the test environment is achieved, thus solving the problem of leakage of the test environment.
Don’t lie to me, I wrote the address of the test environment in environmentConfig. Java.
@Environment(url = "https://www.codexiaomai.top/api/", isRelease = true.alias = "Official")
private String online;
@Environment(url = "http://test.codexiaomai.top/api/".alias = "Test")
private String test;
Copy the code
Hold your horses. I’ll take my time and explain. Although the test environment address is hidden in the compiler-release generated class, the code in EnvironmentConfig.java does contain the test address alive. So how do you hide the test environment in this place?
This brings us to the obfuscation tool, which has yet to be introduced.
Confusion helps me
Here’s a quick review of how confusion works:
- Shrink: Detects and removes useless classes, fields, methods, and properties.
- Optimize: The bytecode is optimized to remove useless instructions.
- Obfuscate: Rename classes, methods, variables, and attributes.
- Preverify: Prechecks Java code to make sure it executes.
See my keywords in bold, the Environment Switcher hides the test Environment by using compiler-release in conjunction with obturation tool removal.
Is it really that amazing? Whether it’s true or not we use the facts. (Sample Project is taken as an example.)
Gradle generates the Release package first, and then decompiles the generated APK file. The following is the directory structure of the decompiled project:
The above image clearly shows the structure of the project when it is confused, and why all subpackages and classes in the EnvironmentSwitcher package are not confused will be explained later.
Which files do the confused classes in com.xiaomai.demo correspond to? By looking at our EnvironmentSwitcher/sample/build/outputs/mapping/release directory to find the mapping. TXT file, extract the main information is as follows:
com.xiaomai.demo.data.Api -> com.xiaomai.demo.a.a:
com.xiaomai.demo.data.GankResponse -> com.xiaomai.demo.a.b:
com.xiaomai.demo.data.MusicResponse -> com.xiaomai.demo.a.c:
com.xiaomai.demo.fragment.HomeFragment -> com.xiaomai.demo.b.a:
com.xiaomai.demo.fragment.MusicFragment -> com.xiaomai.demo.b.b:
com.xiaomai.demo.fragment.SettingsFragment -> com.xiaomai.demo.b.c:
com.xiaomai.demo.net.AppRetrofit -> com.xiaomai.demo.c.a:
com.xiaomai.demo.MainActivity -> com.xiaomai.demo.MainActivity:
com.xiaomai.environmentswitcher.Constants -> com.xiaomai.environmentswitcher.Constants:
com.xiaomai.environmentswitcher.EnvironmentSwitchActivity -> com.xiaomai.environmentswitcher.EnvironmentSwitchActivity:
com.xiaomai.environmentswitcher.EnvironmentSwitcher -> com.xiaomai.environmentswitcher.EnvironmentSwitcher:
com.xiaomai.environmentswitcher.R -> com.xiaomai.environmentswitcher.R:
com.xiaomai.environmentswitcher.annotation.Environment -> com.xiaomai.environmentswitcher.annotation.Environment:
com.xiaomai.environmentswitcher.annotation.Module -> com.xiaomai.environmentswitcher.annotation.Module:
com.xiaomai.environmentswitcher.bean.EnvironmentBean -> com.xiaomai.environmentswitcher.bean.EnvironmentBean:
com.xiaomai.environmentswitcher.bean.ModuleBean -> com.xiaomai.environmentswitcher.bean.ModuleBean:
com.xiaomai.environmentswitcher.listener.OnEnvironmentChangeListener -> com.xiaomai.environmentswitcher.listener.OnEnvironmentChangeListener:
Copy the code
According to the above mapping, the following results can be obtained:
To prove that I haven’t left out information about the EnvironmentConfig class in mapping.txt, post another image:
Once again, EnvironmentConfig was removed by the obfuscation tool when I searched for the keyword EnvironmentConfig with the help of the search tool and was told that the keyword could not be found.
EnvironmentConfig can be removed by obfuzz tools only if it is not referenced by any other class, which is why it is recommended that all classes or properties annotated by @Module and @Environment be private. This essentially eliminates leaks at the code writing stage because the test environment is referenced and cannot be removed in case of confusion.
Why aren’t the classes in EnvironmentSwitcher confused
Those of you who have used open source libraries or other third party NON-open source SDKS know that some of these libraries or SDKS require me to configure obfuscation rules to cause runtime exceptions due to obfuscation. So why is EnvironmentSwitcer not configured with obfuscation rules and not obfuscated?
That’s because the Environment Switcher has already done this for you. Isn’t that sweet? ! The design goal of Environment Switcher is to “save users even one line of code on the premise of ensuring normal functions”.
So how did the Environment Switcher do it? Gradle configuration.
- build.gradle
android { defaultConfig { ... consumerProguardFiles 'consumer-proguard-rules.pro'}}Copy the code
- consumer-proguard-rules.pro
-dontwarn java.nio.** -dontwarn javax.annotation.** -dontwarn javax.lang.** -dontwarn javax.tools.** -dontwarn com.squareup.javapoet.** -keep class com.xiaomai.environmentswitcher.** { *; } Copy the code
In fact, the Environment Switcher did more than configure obfuscation rules for everyone. For example, add a dependency configuration aspect: In the initial version of Environment Switcher, activities inherit from appcompatActivities and RecyclerView is used to display the list of environments. In this way, support-V7 package and RecyclerView-v7 package need to be added. The dependency mode is as follows:
implementation "com.android.support:appcompat-v7:$version"
implementation "com.android.support:recyclerview-v7:$version"
Copy the code
Why not specify the specific version here instead of version?
Because this version is a “TroubleMaker”. If the dependencies of support-V7 and Recyclerview-V7 in the project are different from those in the Environment Switcher, Android Studio will automatically select the higher version dependencies during compilation, which may cause compatibility errors. The normal project fails to compile due to an error. As a simple example, the LayoutInflater parameter of the Fragment’s onCreateView method is nullable in Api 26, as shown below:
override fun onCreateView(inflater: LayoutInflater? , container: ViewGroup? , savedInstanceState: Bundle?) : View? {return super.onCreateView(inflater, container, savedInstanceState)
}
Copy the code
In Api 27, however, null is mandatory, as shown below:
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup? , savedInstanceState: Bundle?) : View? {return super.onCreateView(inflater, container, savedInstanceState)
}
Copy the code
This causes an error ‘onCreateView’ overrides nothing at compile time.
In fact, there are ways to solve this error, the specific method is as follows:
implementation ("com.xiaomai.environmentswitcher:environmentswitcher:$version"){
exclude group: 'com.android.support'
}
Copy the code
In this way, the support package in Environment Switcher will be removed when the Environment Switcher is introduced. However, I always feel that this method is not elegant and violates the design goal of Environment Switcher.
So I’m replacing AppCampatActivity with Activity, RecyclerView with ListView. Both of these classes are provided by the native Sdk, don’t need to introduce any dependencies, and solve the problem perfectly.
In order to facilitate developers, Environment Switcher has made many efforts and attempts, which are not listed here.
The attached
The Environment Switcher can be used as an Environment switching tool and other configurable switches, such as the log printing switch. (PS: This is not the intended feature of the Environment Switcher, it’s a little Easter egg!)
@Module(alias = "Log")
private class Log {
@Environment(url = "false", isRelease = true.alias = "Log off")
private String closeLog;
@Environment(url = "true".alias = "Log on")
private String openLog;
}
public void loge(Context context, String tag, String msg) {
if(EnvironmentSwitcher.getLogEnvironmentBean(context, BuildConfig.DEBUG) .equals(EnvironmentSwitcher.LOG_OPENLOG_ENVIRONMENT)) { android.util.Log.e(tag, msg); }}Copy the code
Of course, this is just a simple example. Environment Switcher can do much more than this, and you are welcome to try more.
If the Environment Switcher is updated later, this article will be updated simultaneously.
To highlight
If you like Environment Switcher, please feel free to reward or Star.