The author
Hello everyone, my name is Xiao Xin, you can also call me crayon xiao Xin 😊;
I graduated from Sun Yat-sen University in 2017 and joined the 37 mobile Game Android team in July 2018. I once worked in Jubang Digital as an Android development engineer.
Currently, I am the overseas head of android team of 37 mobile games, responsible for relevant business development; Also take care of some infrastructure related work.
What is plug-in
A running App serves as the host to load an uninstalled APK file and run it, which is called plug-in
Plug-in usage scenarios:
1. Online new functions (such as Taobao, Alipay, etc.)
2. Hot repair (repair the function by issuing patch plug-in)
3, when the compilation is too slow, you can use plug-in, some unchanged code into plug-ins, speed up the compilation
Two, the three common implementation of plug-in
1, placeholder to achieve plug-in
1, the characteristics of
1. Plug-ins follow the criteria defined by the host and use the host context.
2, advantages: only use a small amount of reflection, no hook, simple implementation
3. Disadvantages: The plug-in can only use the context provided by the host, such as the plug-in Activity, can not use this as the context, that is to say, there is a certain invasive, need to modify the implementation of the plug-in Activity.
2. Implementation steps
1. Define the host criteria. Take the Activity as an example
public interface ActivityInterface {
/** * Give the host (app) environment to the plugin *@param appActivity
*/
void insertAppContext(Activity appActivity);
// Lifecycle methods
void onCreate(Bundle savedInstanceState);
void onStart(a);
void onResume(a);
void onDestroy(a);
// Other declaration cycles are omitted here for demonstration purposes only
}
Copy the code
2. In the plug-in module, implement the plug-in Activity according to the standard
// BaseActivity in the plug-in module implemented according to the standard
public class BaseActivity implements ActivityInterface {
// The context passed by the host
public Activity appActivity; // The host's environment
@Override
public void insertAppContext(Activity appActivity) {
this.appActivity = appActivity;
}
@SuppressLint("MissingSuperCall")
@Override
public void onCreate(Bundle savedInstanceState) {}@SuppressLint("MissingSuperCall")
@Override
public void onStart(a) {}@SuppressLint("MissingSuperCall")
@Override
public void onResume(a) {}@SuppressLint("MissingSuperCall")
@Override
public void onDestroy(a) {}// This is actually the host setContentView method
public void setContentView(int resId) {
appActivity.setContentView(resId);
}
public View findViewById(int layoutId) {
return appActivity.findViewById(layoutId);
}
@Override
public void startActivity(Intent intent) {
Intent intentNew = new Intent();
intentNew.putExtra("className", intent.getComponent().getClassName()); // TestActivity full class nameappActivity.startActivity(intentNew); }}Copy the code
/ / BaseActivity is the key
public class PluginActivity extends BaseActivity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.plugin_main);
// This will report an error because the plug-in is not installed and there is no component environment, so the host environment must be used
Toast.makeText(appActivity, "I'm a plug-in.", Toast.LENGTH_SHORT).show();
findViewById(R.id.bt_start_activity).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// This startActivity follows the startActivity of BaseActivity, which is also propped
startActivity(newIntent(appActivity, TestActivity.class)); }}); }}Copy the code
3. Package the plug-in module apK and load the APK in the host
The loading of plug-in APK is divided into two steps: loading classes and loading resources. The custom DexClassLoader is used to load the classes and the addAssetPath method of the AssetManager is used to load the resources.
The specific code is as follows:
public class PluginManager {
private static final String TAG = PluginManager.class.getSimpleName();
private static PluginManager pluginManager;
private Context context;
public static PluginManager getInstance(Context context) {
if (pluginManager == null) {
synchronized (PluginManager.class) {
if (pluginManager == null) {
pluginManager = newPluginManager(context); }}}return pluginManager;
}
public PluginManager(Context context) {
this.context = context;
}
private DexClassLoader dexClassLoader;
private Resources resources;
/** * 1; /** * 2
public void loadPlugin(a) {
try {
File file = AssetUtils.copyAssetPlugin(context, "p.apk"."plugin");
if(! file.exists()) { Log.d(TAG,"Plug-in pack does not exist...");
return;
}
String pluginPath = file.getAbsolutePath();
File fileDir = context.getDir("pDir", Context.MODE_PRIVATE);
dexClassLoader = new DexClassLoader(pluginPath, fileDir.getAbsolutePath(), null, context.getClassLoader());
// Load resources
AssetManager assetManager = AssetManager.class.newInstance();
Method addAssetPathMethod = assetManager.getClass().getMethod("addAssetPath", String.class);
addAssetPathMethod.invoke(assetManager, pluginPath); // Plugin package path pluginPath
Resources r = context.getResources(); // Host resource configuration information
// Special Resources, Resources that load Resources inside the plugin
resources = new Resources(assetManager, r.getDisplayMetrics(), r.getConfiguration());
} catch(Exception e) { e.printStackTrace(); }}public ClassLoader getClassLoader(a) {
return dexClassLoader;
}
public Resources getResources(a) {
returnresources; }}Copy the code
4. Define placeholder activities in the host
The most important steps here are:
1. Override the getResources and getClassLoader methods to use the plugin’s ClassLoader and the plugin’s Resources
2. Instantiate the plug-in Activity
Inject context into the plug-in Activity
4. Call the onCreate method of the plug-in Activity
The code is as follows:
public class ProxyActivity extends Activity {
// We are using resources from the plug-in
@Override
public Resources getResources(a) {
return PluginManager.getInstance(this).getResources();
}
// The class loader from the plug-in is used here
@Override
public ClassLoader getClassLoader(a) {
return PluginManager.getInstance(this).getClassLoader();
}
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Actually load the Activity inside the plugin
String className = getIntent().getStringExtra("className");
try {
Class mPluginActivityClass = getClassLoader().loadClass(className);
// Instantiate the Activity in the plugin package
Constructor constructor = mPluginActivityClass.getConstructor(new Class[]{});
Object mPluginActivity = constructor.newInstance(new Object[]{});
ActivityInterface activityInterface = (ActivityInterface) mPluginActivity;
// Inject context into the plug-in
activityInterface.insertAppContext(this);
Bundle bundle = new Bundle();
bundle.putString("appName"."I'm a message from the host.");
// Execute the onCreate method in the plugin
activityInterface.onCreate(bundle);
} catch(Exception e) { e.printStackTrace(); }}@Override
public void startActivity(Intent intent) {
String className = intent.getStringExtra("className");
Intent proxyIntent = new Intent(this, ProxyActivity.class);
proxyIntent.putExtra("className", className); / / package name + TestActivity
// To push TestActivity
super.startActivity(proxyIntent); }}Copy the code
3, summary
Now that we are ready to implement a simple placeholder plug-in, let’s summarize the steps:
1. Define a standard between the host and the plug-in, such as IActivityInterface for activities
2. Implement the plug-in module according to the standard, and type it into apK file (the most important thing here is that the context used in the plug-in is passed from the host)
3. Load the plug-in module APK in the host
4. Define a placeholder Activity. In the OnCreate method, the placeholder Activity context is injected into the plug-in Activity based on the Activity information carried by the Intent. It starts by calling the onCreate method of the plug-in Activity instance
The advantages of this implementation are: only a small amount of reflection, no hook system operation, simple adaptation.
The disadvantages are also obvious. In plug-in activities, you need to follow host rules, and if you want to make a framework, the problem of invasions is difficult to solve
2. Hook plug-in
After learning placeholder plug-in, let’s introduce a way to use this in the plug-in, using hook system API way to achieve plug-in
1, the characteristics of
1. Activities in plug-ins can use this, just like normal writing, without conforming to the standard as placeholders
2. There are many hook operations, and there are mainly two links that need hook. One is hook spoofing AMS, which starts activities that are not registered in AndroidManifest. The other is a hook implementation that merges the dex of the plug-in with the dex of the host, replacing the original dexElements
2, the principle of
1. StartActivity process
Have you declared this Activity in your Androidmanifest.xml error: have you declared this Activity in your Androidmanifest.xml This is triggered when AMS checks the Activity to be started after calling startActivity
That is, if we want to cheat AMS, the Activity in the Intent carried by startActivity must be an Activity registered in the AndroidManifest, not the Activity in our plugin.
What about here? Steal dragon to phoenix, steal beam to replace pillar, leopard cat for prince ~
The solution is to temporarily replace the Component in the Intent with a placeholder Activity (as declared in the AndroidManifest) and store the actual plug-in Activity to be launched in the Intent as a parameter.
When AMS sends the LAUNCH_ACTIVITY event to the App, the actual Activity is started
That’s the basic principle, so the key questions to solve are as follows:
1. When invoking an Activity, replace the Intent that starts the plug-in Activity with the Intent that starts the placeholder Activity, and store the Intent that starts the plug-in Activity with parameters in the Intent that starts the placeholder Activity
When AMS sends a LAUNCH_ACTIVITY event, it intercepts the Intent and replaces it with the Intent that started the Activity
3. When you start an Activity, the default ClassLoader is used to load the Activity class and reflect the instantiation, so you need to add the plug-in Activity to the default ClassLoader
So let’s do it one problem at a time
3. Implementation steps
Hook AMS, steal dragon to phoenix
private void hookAmsAction(a) throws Exception {
Class mIActivityManagerClass = Class.forName("android.app.IActivityManager");
// We need the IActivityManager object to invoke the dynamic proxy
Static public IActivityManager getDefault(
Class mActivityManagerNativeClass2 = Class.forName("android.app.ActivityManagerNative");
final Object mIActivityManager = mActivityManagerNativeClass2.getMethod("getDefault").invoke(null);
// Dynamic proxy IActivityManager
Object mIActivityManagerProxy = Proxy.newProxyInstance(
HookApplication.class.getClassLoader(),
new Class[]{mIActivityManagerClass}, // The interface to listen on
new InvocationHandler() { // callback method for the IActivityManager interface
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if ("startActivity".equals(method.getName())) {
// Bypass the AMS check with ProxyActivity
Intent intent = new Intent(HookApplication.this, ProxyActivity.class);
// Store the Intent to start the plug-in Activity as a parameter
intent.putExtra("actionIntent", ((Intent) args[2]));
args[2] = intent;
}
Log.d("hook"."Intercepted methods in IActivityManager" + method.getName());
// Let the system continue to execute normally
returnmethod.invoke(mIActivityManager, args); }});/** * To get gDefault * get the gDefault variable (object) via ActivityManagerNative */
Class mActivityManagerNativeClass = Class.forName("android.app.ActivityManagerNative");
Field gDefaultField = mActivityManagerNativeClass.getDeclaredField("gDefault");
gDefaultField.setAccessible(true); / / authorization
Object gDefault = gDefaultField.get(null);
/ / replace points
Class mSingletonClass = Class.forName("android.util.Singleton");
// Get this field mInstance
Field mInstanceField = mSingletonClass.getDeclaredField("mInstance");
mInstanceField.setAccessible(true);
/ / replace
mInstanceField.set(gDefault, mIActivityManagerProxy);
}
Copy the code
2, hook the LAUNCH_ACTIVITY event to replace the Activity that will be started
/** * Hook LuanchActivity, to instantiate the Activity, to change the ProxyActivity back -- "TestActivity */"
private void hookLuanchActivity(a) throws Exception {
Field mCallbackFiled = Handler.class.getDeclaredField("mCallback");
mCallbackFiled.setAccessible(true); / / authorization
/** ** ** /** ** * Public static ActivityThread CurrenvityThread () * * Find H * */ using ActivityThread
Class mActivityThreadClass = Class.forName("android.app.ActivityThread");
// Get the ActivityThrea object
Object mActivityThread = mActivityThreadClass.getMethod("currentActivityThread").invoke(null);
Field mHField = mActivityThreadClass.getDeclaredField("mH");
mHField.setAccessible(true);
// Get the real object
Handler mH = (Handler) mHField.get(mActivityThread);
mCallbackFiled.set(mH, new MyCallback(mH)); // Replace to add our own implementation code
}
public static final int LAUNCH_ACTIVITY = 100;
class MyCallback implements Handler.Callback {
private Handler mH;
public MyCallback(Handler mH) {
this.mH = mH;
}
@Override
public boolean handleMessage(Message msg) {
switch (msg.what) {
case LAUNCH_ACTIVITY:
// Do our own business logic (replace ProxyActivity with TestActivity)
Object obj = msg.obj;
try {
// We need to get the TestActivity from the previous Hook
Field intentField = obj.getClass().getDeclaredField("intent");
intentField.setAccessible(true);
// Retrieve the intent object to retrieve the actionIntent
Intent intent = (Intent) intentField.get(obj);
// actionIntent == The Intent of the plug-in Activity
Intent actionIntent = intent.getParcelableExtra("actionIntent");
if(actionIntent ! =null) {
// Replace ProxyActivity with a real plug-in ActivityintentField.set(obj, actionIntent); }}catch (Exception e) {
e.printStackTrace();
}
break;
}
// The event executes normally
mH.handleMessage(msg);
return true; // The system does not execute down}}Copy the code
3. Merge the plug-in dex and host dex
private void pluginToAppAction(a) throws Exception {
// Step 1: find the host dexElements and get the object PathClassLoader representing the host
PathClassLoader pathClassLoader = (PathClassLoader) this.getClassLoader(); // The essence is PathClassLoader
Class mBaseDexClassLoaderClass = Class.forName("dalvik.system.BaseDexClassLoader");
// private final DexPathList pathList;
Field pathListField = mBaseDexClassLoaderClass.getDeclaredField("pathList");
pathListField.setAccessible(true);
Object mDexPathList = pathListField.get(pathClassLoader);
Field dexElementsField = mDexPathList.getClass().getDeclaredField("dexElements");
dexElementsField.setAccessible(true);
// The essence is Element[]
Object dexElements = dexElementsField.get(mDexPathList);
/ * * * -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- - * * * /
// Step 2: find the plug-in dexElements to get this object, representing the plug-in DexClassLoader-- representing the plug-in
File pluginDirFile = getDir("plugin", Context.MODE_PRIVATE);
File file = new File(pluginDirFile.getAbsoluteFile() + File.separator + "p.apk");
if(! file.exists()) {throw new FileNotFoundException("No plugins found!! :" + file.getAbsolutePath());
} else {
Log.i("ZXX"."Find plugins:" + file.getAbsolutePath());
}
String pluginPath = file.getAbsolutePath();
File fileDir = this.getDir("pluginDir", Context.MODE_PRIVATE); Data /data/ package name /pluginDir/
DexClassLoader dexClassLoader = new
DexClassLoader(pluginPath, fileDir.getAbsolutePath(), null, getClassLoader());
Class mBaseDexClassLoaderClassPlugin = Class.forName("dalvik.system.BaseDexClassLoader");
// private final DexPathList pathList;
Field pathListFieldPlugin = mBaseDexClassLoaderClassPlugin.getDeclaredField("pathList");
pathListFieldPlugin.setAccessible(true);
Object mDexPathListPlugin = pathListFieldPlugin.get(dexClassLoader);
Field dexElementsFieldPlugin = mDexPathListPlugin.getClass().getDeclaredField("dexElements");
dexElementsFieldPlugin.setAccessible(true);
// The essence is Element[]
Object dexElementsPlugin = dexElementsFieldPlugin.get(mDexPathListPlugin);
Step 3: Create a new dexElements []
int mainDexLeng = Array.getLength(dexElements);
int pluginDexLeng = Array.getLength(dexElementsPlugin);
int sumDexLeng = mainDexLeng + pluginDexLeng;
Int [] String[]... We need Element[]
// Parameter two: the length of the array object
// Element[] newDexElements
Object newDexElements = Array.newInstance(dexElements.getClass().getComponentType(),sumDexLeng); // Create an array object
// Step 4: host dexElements + plugin dexElements =----> merge new newDexElements
for (int i = 0; i < sumDexLeng; i++) {
// Fuse the host first
if (i < mainDexLeng) {
// Parameter one: the new container to be fused -- newDexElements
Array.set(newDexElements, i, Array.get(dexElements, i));
} else { // Remerge pluginsArray.set(newDexElements, i, Array.get(dexElementsPlugin, i - mainDexLeng)); }}// Step 5: Set new newDexElements to the host
/ / host
dexElementsField.set(mDexPathList, newDexElements);
// Handle the layout in the loaded plug-in, which is consistent with the placeholder
doPluginLayoutLoad();
}
Copy the code
4, summary
Hook plug-in consists of three key steps:
1. Cheat AMS to bypass AMS ‘detection of plug-in Activity, mainly by stealing dragon to phoenix
2, Hook AMS LAUNCH_ACTIVITY event to start the Activity
3. Merge the plug-in Dex and host Dex
In this way, the plug-in Activity can be used in this, low invasion. However, due to the use of a lot of hook operation, system adaptation needs to do more work. The key three operations in particular need to do a certain adaptation according to the system source code
3, LoadedApk plug-in implementation
In the plug-in implementation of hook, because all plug-ins are added to dexElements, the host and plug-in use the same ClassLoader. LoadedApk is a plugin that uses multiple ClassLoaders
1, the characteristics of
The host and the plug-in use different Classloaders
2, the principle of
The implementation of cheating AMS and stealthily changing the phoenix is the same as that of hook plug-in. The difference is that hook plug-in is to add the dex of the plug-in into the dexElements in BaseDexClassLoader to successfully load the plug-in class. LoadedApk is not. The following analysis of LoadedApk plug-in implementation principle
Look at the code that starts the Activity in ActivityThread
private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
ActivityInfo aInfo = r.activityInfo;
if (r.packageInfo == null) {
// get LoadedApkr.packageInfo = getPackageInfo(aInfo.applicationInfo, r.compatInfo, Context.CONTEXT_INCLUDE_CODE); }... Omit Activity Activity =null;
try {
// get the ClassLoader from LoadedApk to load the Activity class
java.lang.ClassLoader cl = r.packageInfo.getClassLoader();
// Instantiate the Activityactivity = mInstrumentation.newActivity( cl, component.getClassName(), r.intent); . omitCopy the code
The code for obtaining PackageInfo is as follows:
private LoadedApk getPackageInfo(ApplicationInfo aInfo, CompatibilityInfo compatInfo,
ClassLoader baseLoader, boolean securityViolation, boolean includeCode,
boolean registerPackage) {
final booleandifferentUser = (UserHandle.myUserId() ! = UserHandle.getUserId(aInfo.uid));synchronized (mResourcesManager) {
WeakReference<LoadedApk> ref;
if (differentUser) {
// Caching not supported across users
ref = null;
} else if (includeCode) {
// The LoadedApk object for mPackeges can be implemented as long as the LoadedApk object is constructed and put into mPackeges
ref = mPackages.get(aInfo.packageName);
} else {
ref = mResourcePackages.get(aInfo.packageName);
}
Copy the code
3. Implementation steps
The operation of cheating AMS is the same as that of hook.
How to build LoadedApk objects and add them to mPackages in ActivityThreads
/** * create a LoadedApk.ClassLoader to add to mPackages
private void customLoadedApkAction(a) throws Exception {
File pluginDirFile = getDir("plugin", Context.MODE_PRIVATE);
File file = new File( pluginDirFile.getAbsoluteFile() + File.separator + "p.apk");
if(! file.exists()) {throw new FileNotFoundException("Plug-in pack does not exist..." + file.getAbsolutePath());
}
String pulginPath = file.getAbsolutePath();
// mPackages add custom LoadedApk
// Final ArrayMap
> mPackages Add custom LoadedApk
,>
Class mActivityThreadClass = Class.forName("android.app.ActivityThread");
Public static ActivityThread currenvityThread () Gets the ActivityThread object
Object mActivityThread = mActivityThreadClass.getMethod("currentActivityThread").invoke(null);
Field mPackagesField = mActivityThreadClass.getDeclaredField("mPackages");
mPackagesField.setAccessible(true);
// Get the mPackages object
Object mPackagesObj = mPackagesField.get(mActivityThread);
Map mPackages = (Map) mPackagesObj;
// How to customize a LoadedApk, how the system created LoadedApk, how to create LoadedApk
Public Final LoadedApk getPackageInfoNoCheck(ApplicationInfo ai, CompatibilityInfo compatInfo)
Class mCompatibilityInfoClass = Class.forName("android.content.res.CompatibilityInfo");
Field defaultField = mCompatibilityInfoClass.getDeclaredField("DEFAULT_COMPATIBILITY_INFO");
defaultField.setAccessible(true);
Object defaultObj = defaultField.get(null);
/** * ApplicationInfo */
ApplicationInfo applicationInfo = getApplicationInfoAction();
Method mLoadedApkMethod = mActivityThreadClass.getMethod("getPackageInfoNoCheck", ApplicationInfo.class, mCompatibilityInfoClass); / / class type
// Execute to get LoedApk object
Object mLoadedApk = mLoadedApkMethod.invoke(mActivityThread, applicationInfo, defaultObj);
// Custom loader loads plug-ins
// String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent
File fileDir = getDir("pulginPathDir", Context.MODE_PRIVATE);
// Customize the ClassLoader for loading plug-ins
ClassLoader classLoader = new PluginClassLoader(pulginPath,fileDir.getAbsolutePath(), null, getClassLoader());
Field mClassLoaderField = mLoadedApk.getClass().getDeclaredField("mClassLoader");
mClassLoaderField.setAccessible(true);
mClassLoaderField.set(mLoadedApk, classLoader); // Replace the ClassLoader in LoadedApk
// Add a custom LoadedApk plugin specifically loaded inside the class
// The final target is mpackages.put (plugin package name, plugin LoadedApk);
WeakReference weakReference = new WeakReference(mLoadedApk); // Add the custom LoadedApk -- plugin
mPackages.put(applicationInfo.packageName, weakReference); // Added our own LoadedApk
}
/** * Get ApplicationInfo * for the plug-in@return
* @throws* /
private ApplicationInfo getApplicationInfoAction(a) throws Exception {
// Execute the public static ApplicationInfo generateApplicationInfo method to get ApplicationInfo
Class mPackageParserClass = Class.forName("android.content.pm.PackageParser");
Object mPackageParser = mPackageParserClass.newInstance();
// generateApplicationInfo Method class type
Class $PackageClass = Class.forName("android.content.pm.PackageParser$Package");
Class mPackageUserStateClass = Class.forName("android.content.pm.PackageUserState");
Method mApplicationInfoMethod = mPackageParserClass.getMethod("generateApplicationInfo",$PackageClass,
int.class, mPackageUserStateClass);
File dirFile = getDir("plugin", Context.MODE_PRIVATE);
File file = new File(dirFile.getAbsoluteFile() + File.separator + "p.apk");
String pulginPath = file.getAbsolutePath();
Public Package parsePackage(File packageFile, int flags
// Get the object that executes the method
Method mPackageMethod = mPackageParserClass.getMethod("parsePackage", File.class, int.class);
Object mPackage = mPackageMethod.invoke(mPackageParser, file, PackageManager.GET_ACTIVITIES);
// Parameters Package p, int flags, PackageUserState state
ApplicationInfo applicationInfo = (ApplicationInfo)
mApplicationInfoMethod.invoke(mPackageParser, mPackage, 0, mPackageUserStateClass.newInstance());
// The obtained ApplicationInfo is the plugin's ApplicationInfo
// ApplicationInfo we get here
/ / applicationInfo. PublicSourceDir = plugin path;
/ / applicationInfo. SourceDir = plugin path;
applicationInfo.publicSourceDir = pulginPath;
applicationInfo.sourceDir = pulginPath;
return applicationInfo;
}
Copy the code
2, hook AMS starts the Activity callback
class MyCallback implements Handler.Callback {
private Handler mH;
public MyCallback(Handler mH) {
this.mH = mH;
}
@Override
public boolean handleMessage(Message msg) {
switch (msg.what) {
case LAUNCH_ACTIVITY:
// Do our own business logic (replace ProxyActivity with TestActivity)
Object obj = msg.obj; / / ActivityClientRecord nature
try {
// We need to get the TestActivity from the previous Hook
Field intentField = obj.getClass().getDeclaredField("intent");
intentField.setAccessible(true);
// Retrieve the intent object to retrieve the actionIntent
Intent intent = (Intent) intentField.get(obj);
Intent actionIntent = intent.getParcelableExtra("actionIntent");
if(actionIntent ! =null) {
intentField.set(obj, actionIntent); // Replace ProxyActivity with plug-in Activity
/*** * We distinguish between plug-ins and hosts in the following code */
Field activityInfoField = obj.getClass().getDeclaredField("activityInfo");
activityInfoField.setAccessible(true); / / authorization
ActivityInfo activityInfo = (ActivityInfo) activityInfoField.get(obj);
// The host Intent's getPackage gets the package name, and the plug-in's getPackage is empty to determine if it is a plug-in Intent
if (actionIntent.getPackage() == null) {
// Change the applicationInfo package name to the plugin package name so that the LoadedApk we get is our own
activityInfo.applicationInfo.packageName = actionIntent.getComponent().getPackageName();
// This is the next step, hook PMS, bypass PMS detection
hookGetPackageInfo();
} else { / / hostactivityInfo.applicationInfo.packageName = actionIntent.getPackage(); }}}catch (Exception e) {
e.printStackTrace();
}
break;
}
mH.handleMessage(msg);
// Let the system continue to execute normally
// return false; // The system will execute down
return true; // The system does not execute down
}
Copy the code
3. Hook PMS to bypass detection
Light operation also not line, so the Activity starts, the PMS will detect whether the corresponding Apk package name is installed (LoadedApk initializeJavaContextClassLoader method), is not installed an error.
Calling process: performLaunchActivity – > makeApplication – > initializeJavaContextClassLoader.
InitializeJavaContextClassLoader () method code is as follows:
IPackageManager pm = ActivityThread.getPackageManager();
android.content.pm.PackageInfo pi;
try {
pi = pm.getPackageInfo(mPackageName, PackageManager.MATCH_DEBUG_TRIAGED_MISSING,
UserHandle.myUserId());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
if (pi == null) {
throw new IllegalStateException("Unable to get package info for "
+ mPackageName + "; is package not installed?");
}
Copy the code
Therefore, hook PMS is also needed to bypass detection, and the implementation code is as follows:
// Hook this getPackageInfo to do its own logic
private void hookGetPackageInfo(a) {
try {
// sPackageManager replaces our own dynamic proxy
Class mActivityThreadClass = Class.forName("android.app.ActivityThread");
Field sCurrentActivityThreadField = mActivityThreadClass.getDeclaredField("sCurrentActivityThread");
sCurrentActivityThreadField.setAccessible(true);
Field sPackageManagerField = mActivityThreadClass.getDeclaredField("sPackageManager");
sPackageManagerField.setAccessible(true);
final Object packageManager = sPackageManagerField.get(null);
/** * dynamic proxy */
Class mIPackageManagerClass = Class.forName("android.content.pm.IPackageManager");
Object mIPackageManagerProxy = Proxy.newProxyInstance(getClassLoader(),
new Class[]{mIPackageManagerClass}, // The interface to listen on
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if ("getPackageInfo".equals(method.getName())) {
// How to bypass the PMS and cheat the system
// pi ! = null
return new PackageInfo(); // PMS detection was successfully bypassed
}
// Let the system continue as normal
returnmethod.invoke(packageManager, args); }});// Replace tanuki with prince with our own dynamic proxy
sPackageManagerField.set(null, mIPackageManagerProxy);
} catch(Exception e) { e.printStackTrace(); }}Copy the code
4, summary
LoadedApk is a plug-in, which is different from hook when the Activity is started.
The main steps are:
Imitate the system source code, plug-in APK LoadedApk instance. And placed in the mPackages object in the ActivityThread
In the mH callback of the ActivityThread, stealing the plugin Activity replaces the package name in applicationInfo in activityInfo with the plugin’s package name, allowing subsequent logic to use the plugin’s LoadedApk
Finally, hook PMS to bypass the detection of plug-in installation by PMS
4. Summary of three ways to achieve plug-in
1, placeholder plug-in is relatively stable, good compatibility, because there is no HOOK system API. But writing plug-ins can be uncomfortable because you have to keep an eye on the host context
2. Hook method to implement plug-in, without considering the host environment, but hook the system API, poor compatibility.
3, LoadedApk method to achieve plug-in, and hook method is similar, do not consider the host environment, but the system API hook, poor compatibility
3. Plug-in frameworks on the market
1. Meituan Robust
The implementation of meituan Robust plug-in is different from the three methods mentioned above, but refers to the implementation of Instant Run, which inserts a control logic code for each function in the compilation and packaging stage
Here’s a quick look at the repair process:
In the following method, a bit of control logic is added at compile time to determine whether the patch operation should be implemented or the original operation
public long getIndex(a) {
return 100;
}
Copy the code
The compiled getIndex
public static ChangeQuickRedirect changeQuickRedirect;
public long getIndex(a) {
if(changeQuickRedirect ! =null) {
//PatchProxy encapsulates the logic to get the current className and methodName, and finally calls the corresponding function of changeQuickRedirect within it
if(PatchProxy.isSupport(new Object[0].this, changeQuickRedirect, false)) {
return ((Long)PatchProxy.accessDispatch(new Object[0].this, changeQuickRedirect, false)).longValue(); }}return 100L;
}
Copy the code
When a patch is loaded, it is reflected to the changeQuickRedirect setting instance. If the instance is not empty, the plugin logic is followed
Can perfect realization, of course, is not so simple, specific reference: tech.meituan.com/2016/09/14/…
2. Tencent QZone and Tinker
Tencent’s QZone and Tinker are essentially plugins implemented in hook mode and operate dexElements
Verify; verify; verify;
When apK is installed, the virtual machine optimizes classes.dex into an odex file and then executes it. During this process, the verify operation of the class is performed. If all the classes calling the relationship are on the same dex, the CLASS_ISPREVERIFIED flag is marked and then the odex file is written.
At run time, an error is reported if the marked class references a class in another DEX.
Therefore, it is necessary to solve the problem of labeling.
QZone does this by referring to one of the other Dex classes in the constructor of each class to avoid being marked
Tinker merges the host dex with the plug-in dex, deletes the original dex from dexElements, and adds the merged dex. All codes are in the same dex, so there is no CLASS_ISPREVERIFIED problem
Iv. SDK plug-in
1. It is recommended to choose placeholder implementation
The essence of SDK plug-in is not different from that of APP plug-in, but if there are not many four components of SDK, it is very recommended to use placeholder plug-in, because there are fewer compatibility problems.
2. Customize plug-in Context
However, since the SDK is usually attached to the host Activity call, it is best not to process the host Activity’s getClassLoader and getResources to avoid affecting the host’s logic. You can then implement a Context with the plugin ClassLoader and Resouces for the Sdk to use
public class SQwanCore implements ISQwanCore {
@Override
public void init(Context context) {
// Construct a Context with the plugin classLoader and Resources
SdkContextProxy sdkContext = new SdkContextProxy(context);
try {
ISQwanCore sdkObj = (ISQwanCore) sdkContext.getClassLoader().loadClass("com.sq.plugin.PluginSQwanCore").newInstance();
sdkObj.init(sdkContext);
} catch(Exception e) { e.printStackTrace(); }}}Copy the code
The SdkContextProxy code is as follows:
public class SdkContextProxy extends ContextWrapper {
private Context baseContext;
public SdkContextProxy(Context base) {
super(base);
baseContext = base;
}
@Override
public ClassLoader getClassLoader(a) {
return PluginManager.getInstance(baseContext).getClassLoader();
}
@Override
public Resources getResources(a) {
return PluginManager.getInstance(baseContext).getResources();
}
// When you start an Activity, do a special operation that leads to ProxyActivity, referring to placeholder ProxyActivity
@Override
public void startActivity(Intent intent) {
String className = intent.getComponent().getClassName();
Intent proxyIntent = new Intent(this, ProxyActivity.class);
proxyIntent.putExtra("className", className); // Package name + plug-in Activity
// To push the plug-in Activity into the stack
super.startActivity(proxyIntent); }}Copy the code
Plugin implementation of Sdk:
public class PluginSQwanCore implements ISQwanCore {
@Override
public void init(Context context) {
//PluginActivity is implemented according to the standard IActivityInterface
context.startActivity(newIntent(context, PluginActivity.class)); }}Copy the code
Other logical reference placeholder plug-in can be realized
As for the add-in CLASS_ISPREVERIFIED of SDK, the simplest method is to delete the classes common to the add-in and the host. For example, the ISQwanCore class is deleted in the add-in in this case.
3. Resource processing
FAQ:
Adaptation of AssetManager (different from 19 above and below)
What can and cannot be proxied (resource references in XML files)
Isolate host and plug-in Resources (handled with ContextWrapper)
Use Gradle to modify resource IDS
How to handle a getIdentifier conflict (plug-ins are preferred and ResourceWrapper is used)
Combine host and plug-in Resources into one large resource. Why?
1. Host resources contain system resources, which is needed
2. Some SDK resources are stored in the host for easy package cutting (such as flash screen)
public class SuperHostResources {
private Context mContext;
private Resources mResources;
public SuperHostResources(Context context, String pluginPath) {
mContext = context;
mResources = buildHostResources(pluginPath);
}
private Resources buildHostResources(String pluginPath) {
Resources hostResources = mContext.getResources();
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.KITKAT) {
try {
AssetManager assetManager = mContext.getResources().getAssets();
Method addAssetPathMethod = AssetManager.class.getDeclaredMethod("addAssetPath", String.class);
addAssetPathMethod.setAccessible(true);
addAssetPathMethod.invoke(assetManager, pluginPath);
hostResources = new Resources(assetManager, mContext.getResources().getDisplayMetrics(), mContext.getResources().getConfiguration());
} catch(Exception e) { e.printStackTrace(); hostResources = mContext.getResources(); }}else {
try {
AssetManager assetManager = AssetManager.class.newInstance();
Method addAssetPathMethod = AssetManager.class.getDeclaredMethod("addAssetPath", String.class);
addAssetPathMethod.setAccessible(true);
addAssetPathMethod.invoke(assetManager, pluginPath);
// If you want to change the id, you need to change the id
String baseApkPath = mContext.getApplicationInfo().sourceDir;
addAssetPathMethod.invoke(assetManager, baseApkPath);
hostResources = new Resources(assetManager, mContext.getResources().getDisplayMetrics(), mContext.getResources().getConfiguration());
} catch(Exception e) { e.printStackTrace(); hostResources = mContext.getResources(); }}return hostResources;
}
public Resources get(a) {
returnmResources; }}Copy the code
Use ResourceWrapper. Why?
1. For example, getIdentifier can be used to determine whether plug-ins or hosts are loaded first
public class MixResources extends ResourcesWrapper {
private Resources mPluginResources;
private String mPluginPkgName;
public MixResources(Resources hostResources, Context context, String pluginPath) {
super(hostResources);
PluginResources pluginResourcesBuilder = new PluginResources(context, pluginPath);
mPluginResources = pluginResourcesBuilder.get();
mPluginPkgName = pluginResourcesBuilder.getPkgName();
}
public MixResources(Resources hostResources, Resources pluginResources, String pluginPkgName) {
super(hostResources);
mPluginResources = pluginResources;
mPluginPkgName = pluginPkgName;
}
public String getPluginPkgName(a) {
return mPluginPkgName;
}
@Override
public CharSequence getText(int id) throws Resources.NotFoundException {
try {
return super.getText(id);
} catch (Resources.NotFoundException e) {
returnmPluginResources.getText(id); }}@Override
public String getString(int id) throws Resources.NotFoundException {
try {
return super.getString(id);
} catch (Resources.NotFoundException e) {
returnmPluginResources.getString(id); }}@Override
public String getString(int id, Object... formatArgs) throws Resources.NotFoundException {
try {
return super.getString(id,formatArgs);
} catch (Resources.NotFoundException e) {
returnmPluginResources.getString(id,formatArgs); }}@Override
public float getDimension(int id) throws Resources.NotFoundException {
try {
return super.getDimension(id);
} catch (Resources.NotFoundException e) {
returnmPluginResources.getDimension(id); }}@Override
public int getDimensionPixelOffset(int id) throws Resources.NotFoundException {
try {
return super.getDimensionPixelOffset(id);
} catch (Resources.NotFoundException e) {
returnmPluginResources.getDimensionPixelOffset(id); }}@Override
public int getDimensionPixelSize(int id) throws Resources.NotFoundException {
try {
return super.getDimensionPixelSize(id);
} catch (Resources.NotFoundException e) {
returnmPluginResources.getDimensionPixelSize(id); }}@Override
public Drawable getDrawable(int id) throws Resources.NotFoundException {
try {
return super.getDrawable(id);
} catch (Resources.NotFoundException e) {
returnmPluginResources.getDrawable(id); }}@TargetApi(Build.VERSION_CODES.LOLLIPOP)
@Override
public Drawable getDrawable(int id, Resources.Theme theme) throws Resources.NotFoundException {
try {
return super.getDrawable(id, theme);
} catch (Resources.NotFoundException e) {
returnmPluginResources.getDrawable(id,theme); }}@Override
public Drawable getDrawableForDensity(int id, int density) throws Resources.NotFoundException {
try {
return super.getDrawableForDensity(id, density);
} catch (Resources.NotFoundException e) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) {
return mPluginResources.getDrawableForDensity(id, density);
} else {
return null; }}}@TargetApi(Build.VERSION_CODES.LOLLIPOP)
@Override
public Drawable getDrawableForDensity(int id, int density, Resources.Theme theme) {
try {
return super.getDrawableForDensity(id, density, theme);
} catch (Exception e) {
returnmPluginResources.getDrawableForDensity(id,density,theme); }}@Override
public int getColor(int id) throws Resources.NotFoundException {
try {
return super.getColor(id);
} catch (Resources.NotFoundException e) {
returnmPluginResources.getColor(id); }}@TargetApi(Build.VERSION_CODES.M)
@Override
public int getColor(int id, Resources.Theme theme) throws Resources.NotFoundException {
try {
return super.getColor(id,theme);
} catch (Resources.NotFoundException e) {
returnmPluginResources.getColor(id,theme); }}@Override
public ColorStateList getColorStateList(int id) throws Resources.NotFoundException {
try {
return super.getColorStateList(id);
} catch (Resources.NotFoundException e) {
returnmPluginResources.getColorStateList(id); }}@TargetApi(Build.VERSION_CODES.M)
@Override
public ColorStateList getColorStateList(int id, Resources.Theme theme) throws Resources.NotFoundException {
try {
return super.getColorStateList(id,theme);
} catch (Resources.NotFoundException e) {
returnmPluginResources.getColorStateList(id,theme); }}@Override
public boolean getBoolean(int id) throws Resources.NotFoundException {
try {
return super.getBoolean(id);
} catch (Resources.NotFoundException e) {
returnmPluginResources.getBoolean(id); }}@Override
public XmlResourceParser getLayout(int id) throws Resources.NotFoundException {
try {
return super.getLayout(id);
} catch (Resources.NotFoundException e) {
returnmPluginResources.getLayout(id); }}@Override
public String getResourceName(int resid) throws Resources.NotFoundException {
try {
return super.getResourceName(resid);
} catch (Resources.NotFoundException e) {
returnmPluginResources.getResourceName(resid); }}@Override
public int getInteger(int id) throws Resources.NotFoundException {
try {
return super.getInteger(id);
} catch (Resources.NotFoundException e) {
returnmPluginResources.getInteger(id); }}@Override
public CharSequence getText(int id, CharSequence def) {
try {
return super.getText(id,def);
} catch (Resources.NotFoundException e) {
returnmPluginResources.getText(id,def); }}@Override
public InputStream openRawResource(int id) throws Resources.NotFoundException {
try {
return super.openRawResource(id);
} catch (Resources.NotFoundException e) {
returnmPluginResources.openRawResource(id); }}@Override
public XmlResourceParser getXml(int id) throws Resources.NotFoundException {
try {
return super.getXml(id);
} catch (Resources.NotFoundException e) {
returnmPluginResources.getXml(id); }}@Override
public void getValue(int id, TypedValue outValue, boolean resolveRefs) throws Resources.NotFoundException {
try {
super.getValue(id, outValue, resolveRefs);
} catch(Resources.NotFoundException e) { mPluginResources.getValue(id, outValue, resolveRefs); }}@Override
public Movie getMovie(int id) throws Resources.NotFoundException {
try {
return super.getMovie(id);
} catch (Resources.NotFoundException e) {
returnmPluginResources.getMovie(id); }}@Override
public XmlResourceParser getAnimation(int id) throws Resources.NotFoundException {
try {
return super.getAnimation(id);
} catch (Resources.NotFoundException e) {
returnmPluginResources.getAnimation(id); }}@Override
public InputStream openRawResource(int id, TypedValue value) throws Resources.NotFoundException {
try {
return super.openRawResource(id,value);
} catch (Resources.NotFoundException e) {
returnmPluginResources.openRawResource(id,value); }}@Override
public AssetFileDescriptor openRawResourceFd(int id) throws Resources.NotFoundException {
try {
return super.openRawResourceFd(id);
} catch (Resources.NotFoundException e) {
returnmPluginResources.openRawResourceFd(id); }}@Override
public int getIdentifier(String name, String defType, String defPackage) {
int pluginId = super.getIdentifier(name, defType, defPackage);
if (pluginId <= 0) {
return mPluginResources.getIdentifier(name, defType, mPluginPkgName);
}
return pluginId;
}
public int getIdentifierFromPlugin(String name, String defType) {
returnmPluginResources.getIdentifier(name, defType, mPluginPkgName); }}Copy the code
Five, the summary
1. This paper introduces three common plug-in implementation schemes, including placeholder, hook, LoadedApk and their respective characteristics
2. This paper introduces the scheme of plug-in framework commonly seen in the market, and briefly describes how to avoid CLASS_ISPREVERIFIED problem
3. Introduce the plug-in of SDK and the implementation scheme of SDK Context