### Verify Image Optimization Tool Installation Source: https://github.com/camnter/androidlife/blob/master/buildSrc/resources-optimize-l2-plugin-README.md This snippet shows how to verify the successful installation of image optimization tools (cwebp, guetzli, pngquant) by checking their executable paths using the 'which' command, ensuring they are accessible for the Gradle plugin. ```shell which cwebp which guetzli which pngquant ``` -------------------------------- ### Install Image Optimization Tools with Homebrew Source: https://github.com/camnter/androidlife/blob/master/buildSrc/resources-optimize-l2-plugin-README.md This snippet demonstrates how to install essential image optimization command-line tools (cwebp, guetzli, pngquant) using Homebrew, a package manager for macOS and Linux, which are prerequisites for the resource optimization plugin. ```shell brew install cwebp brew install guetzli brew install pngquant ``` -------------------------------- ### Example Output of dex-method-counts Source: https://github.com/camnter/androidlife/blob/master/dex-method-counts/README.md An illustrative output showing the hierarchical method counts per package, including overall method count, generated by the dex-method-counts tool. ```Text Read in 65490 method IDs. : 65490 : 3 android: 6837 accessibilityservice: 6 bluetooth: 2 content: 248 pm: 22 res: 45 ... com: 53881 adjust: 283 sdk: 283 codebutler: 65 android_websockets: 65 ... Overall method count: 65490 ``` -------------------------------- ### Dynamically Load Plugin Activity in Android Source: https://github.com/camnter/androidlife/blob/master/plugin-life/single-resources/single-resources-host/README.md This Java code snippet illustrates how to dynamically load and start an activity from a plugin APK. It initializes a UI element (`startText`) and sets an `OnClickListener`. Upon click, it retrieves the `DexClassLoader` from a custom `SmartApplication` instance, uses it to load the plugin activity class by its fully qualified name, and then starts the activity using an `Intent`. Error handling with `ToastUtils` is included. ```java /** * Initialize the view in the layout * * @param savedInstanceState savedInstanceState */ @Override protected void initViews(Bundle savedInstanceState) { this.startText = this.findViewById(R.id.start_text); this.startText.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { startText.setEnabled(false); handler.post(new Runnable() { @Override public void run() { try { // plugin activity final SmartApplication smartApplication = (SmartApplication) SingleResourcesActivity.this.getApplication(); final Intent intent = new Intent(SingleResourcesActivity.this, smartApplication .getDexClassLoader() .loadClass( "com.camnter.single.resources.plugin.SingleResourcesPluginActivity")); startActivity(intent); } catch (Exception e) { ToastUtils.show(SingleResourcesActivity.this, e.getMessage(), Toast.LENGTH_LONG); e.printStackTrace(); } finally { startText.setEnabled(true); } } }); } }); } ``` -------------------------------- ### Execute Method Trace Tasks via Gradle Source: https://github.com/camnter/androidlife/blob/master/buildSrc/method-trace-plugin-README.md Demonstrates how to execute the method tracing tasks provided by the plugin from the command line using Gradle. It includes tasks for whole trace and expected trace, with an example of passing a package name parameter. ```Shell gradle methodWholeTraceTask gradle methodExpectTraceTask // sample gradle methodExpectTraceTask -P packageName=com.camnter ``` -------------------------------- ### Execute life-plugin Tasks Source: https://github.com/camnter/androidlife/blob/master/gradle-plugin-life/life-plugin/README.md These shell commands illustrate how to invoke various tasks provided by the 'life-plugin' from the command line. The examples include 'lifePlugin', 'lifeTask', and 'lifeExtensionTask', demonstrating the execution of different functionalities exposed by the plugin. ```shell gradle lifePlugin gradle lifeTask gradle lifeExtensionTask ``` -------------------------------- ### Dynamically Install Content Providers from Plugin APK in Android Host Source: https://github.com/camnter/androidlife/blob/master/plugin-life/content-provider-plugin/content-provider-plugin-host/README.md This Java code snippet demonstrates how an Android host application can dynamically load and install Content Providers from a plugin APK. It involves extracting the plugin APK from assets, patching the ClassLoader to enable loading of plugin classes, parsing `ProviderInfo` from the plugin's manifest, and using reflection to invoke `ActivityThread#installContentProviders` to register the plugin's Content Providers with the system. This allows for modularity and dynamic feature delivery. ```java /** * @author CaMnter */ public class SmartApplication extends Application { final Map providerInfoMap = new HashMap<>(); /** * Set the base context for this ContextWrapper. All calls will then be * delegated to the base context. Throws * IllegalStateException if a base context has already been set. * * @param base The new base context for this wrapper. */ @Override protected void attachBaseContext(Context base) { super.attachBaseContext(base); try { // assets 的 content-provider-plugin-plugin.apk 拷贝到 /data/data/files/[package name] AssetsUtils.extractAssets(base, "content-provider-plugin-plugin.apk"); final File apkFile = getFileStreamPath("content-provider-plugin-plugin.apk"); final File odexFile = getFileStreamPath("content-provider-plugin-plugin.odex"); // Hook ClassLoader, 让插件中的类能够被成功加载 BaseDexClassLoaderHooker.patchClassLoader(this.getClassLoader(), apkFile, odexFile); // 解析 provider providerInfoMap.putAll(ProviderInfoUtils.getProviderInfos(apkFile, base)); // 该进程安装 ContentProvider this.installProviders(base, providerInfoMap); } catch (Exception e) { e.printStackTrace(); } } /** * 反射 调用 ActivityThread # installContentProviders(Context context, List providers) * 安装 ContentProvider * * @throws NoSuchMethodException NoSuchMethodException * @throws ClassNotFoundException ClassNotFoundException * @throws IllegalAccessException IllegalAccessException * @throws InvocationTargetException InvocationTargetException */ private void installProviders(@NonNull final Context context, @NonNull final Map providerInfoMap) throws NoSuchMethodException, ClassNotFoundException, IllegalAccessException, InvocationTargetException { List providerInfos = new ArrayList<>(); // 修改 ProviderInfo # String packageName for (Map.Entry entry : providerInfoMap.entrySet()) { final ProviderInfo providerInfo = entry.getValue(); providerInfo.applicationInfo.packageName = context.getPackageName(); providerInfos.add(providerInfo); } if (providerInfos.isEmpty()) { return; } final Class activityThreadClass = Class.forName("android.app.ActivityThread"); final Method currentActivityThreadMethod = activityThreadClass.getDeclaredMethod( "currentActivityThread"); final Object currentActivityThread = currentActivityThreadMethod.invoke(null); final Method installProvidersMethod = activityThreadClass.getDeclaredMethod( "installContentProviders", Context.class, List.class); installProvidersMethod.setAccessible(true); installProvidersMethod.invoke(currentActivityThread, context, providerInfos); } } ``` -------------------------------- ### Configure Reduce Dependency Packaging Gradle Plugin Source: https://github.com/camnter/androidlife/blob/master/plugin-life/reduce-dependency-packaging/reduce-dependency-packaging-plugin-three/README.md This Groovy snippet illustrates the application and configuration of the `com.camnter.gradle.plugin.reduce.dependency.packaging.plugin`. It defines the `packageId` for resources (e.g., `0x73`), specifies the relative path to the `targetHost` application module, and allows enabling or disabling `applyHostMapping`. This setup is critical for the plugin to correctly modify and package dependencies within an Android project. ```groovy /** * 必须先编译好宿主项目,在 build/reduceDependencyPackagingHost 下必须存在 * allVersions.txt * Host_R.txt * versions.txt * */ apply plugin: 'com.camnter.gradle.plugin.reduce.dependency.packaging.plugin' reduceDependencyPackagingExtension { // the package id of Resources. packageId = 0x73 // the path of application module in host project. targetHost = '../reduce-dependency-packaging-plugin-host' // optional, default value: true. applyHostMapping = true } ``` -------------------------------- ### Hooking Android Instrumentation and Loading DEX Plugins Source: https://github.com/camnter/androidlife/blob/master/plugin-life/single-resources/single-resources-host/README.md This Java code demonstrates how to dynamically load a plugin APK using `DexClassLoader` and then hook the `mInstrumentation` field within the `ActivityThread` using reflection. The `attachBaseContext` method initiates the DEX loading and instrumentation hooking. The `loadDex` method copies a plugin APK from assets to a cache directory and initializes a `DexClassLoader`. The `hookInstrumentation` method uses reflection to get the `ActivityThread` instance and its `mInstrumentation` field, replacing it with a custom `SmartInstrumentation` to intercept system calls. ```Java /** * Set the base context for this ContextWrapper. All calls will then be * delegated to the base context. Throws * IllegalStateException if a base context has already been set. * * @param base The new base context for this wrapper. */ @Override protected void attachBaseContext(Context base) { super.attachBaseContext(base); this.loadDex(); this.hookInstrumentation(); } private void loadDex() { try { String dir = null; final File cacheDir = this.getExternalCacheDir(); final File filesDir = this.getExternalFilesDir(""); if (cacheDir != null) { dir = cacheDir.getAbsolutePath(); } else { if (filesDir != null) { dir = filesDir.getAbsolutePath(); } } if (TextUtils.isEmpty(dir)) return; // assets 的 single-resources-plugin.apk 拷贝到 /storage/sdcard0/Android/data/[package name]/cache // 或者 /storage/sdcard0/Android/data/[package name]/files final File dexPath = new File(dir + File.separator + "single-resources-plugin.apk"); final String dexAbsolutePath = dexPath.getAbsolutePath(); AssetsUtils.copyAssets(this, "single-resources-plugin.apk", dexAbsolutePath); // /data/data/[package name]/app_single-resources-plugin final File optimizedDirectory = this.getDir("single-resources-plugin", MODE_PRIVATE); this.dexClassLoader = new DexClassLoader( dexPath.getAbsolutePath(), optimizedDirectory.getAbsolutePath(), null, this.getClassLoader() ); this.dexAbsolutePath = dexAbsolutePath; } catch (Exception e) { e.printStackTrace(); } } private void hookInstrumentation() { try { /* * 反射 ActivityThread * 通过 currentActivityThread 方法 * 获取存放的 ActivityThread 实例 */ @SuppressLint("PrivateApi") final Class activityThreadClass = Class.forName( "android.app.ActivityThread"); final Method currentActivityThreadMethod = activityThreadClass.getDeclaredMethod( "currentActivityThread"); currentActivityThreadMethod.setAccessible(true); final Object currentActivityThread = currentActivityThreadMethod.invoke(null); /* * ActivityThread 实例获取 Field mInstrumentation */ final Field mInstrumentationField = activityThreadClass.getDeclaredField( "mInstrumentation"); mInstrumentationField.setAccessible(true); final Instrumentation mInstrumentation = (Instrumentation) mInstrumentationField.get( currentActivityThread); // 是否 hook 过 if (!(mInstrumentation instanceof SmartInstrumentation)) { final SmartInstrumentation pluginInstrumentation = new SmartInstrumentation( dexClassLoader, mInstrumentation); mInstrumentationField.set(currentActivityThread, pluginInstrumentation); } } catch (Exception e) { e.printStackTrace(); } } ``` -------------------------------- ### Initiating Android Clipboard Service Hooking Source: https://github.com/camnter/androidlife/blob/master/plugin-life/hook-binder/README.md This Java class provides a static utility method, 'hookClipboardService', designed to initiate the Binder hooking process for the Android clipboard service. It uses reflection to access and modify the ServiceManager's internal cache (sCache), replacing the original IBinder object for the 'clipboard' service with a dynamic proxy. This setup is crucial for enabling the subsequent interception of service calls by the BinderProxyHookHandler and BinderHookHandler. ```java /** * 替换 ServiceManager # HashMap sCache 内的缓存内容 * 替换为 动态代理对象 * * @author CaMnter */ @SuppressWarnings("DanglingJavadoc") public final class BinderHookHelper { private static final String CLIPBOARD_SERVICE = "clipboard"; @SuppressWarnings("unchecked") public static void hookClipboardService() throws Exception { // 反射获取 clipboard 的 IBinder final Class serviceManagerClass = Class.forName("android.os.ServiceManager"); final Method getServiceMethod = serviceManagerClass.getDeclaredMethod("getService", String.class); ``` -------------------------------- ### Android Activity for Service Plugin Interaction Source: https://github.com/camnter/androidlife/blob/master/plugin-life/hook-ams-for-service-plugin/hook-ams-for-service-plugin-host/README.md This Java code defines an Android `Activity` (`HookAmsForServicePluginActivity`) that demonstrates how to start and stop services from a separate plugin APK. It initializes UI elements, sets click listeners, and, upon user interaction, uses `startService` and `stopService` with `ComponentName` to target specific services (`FirstService`, `SecondService`) located within the `com.camnter.hook.ams.f.service.plugin.plugin` package. This setup implies a mechanism (like AMS hooking) that allows the host application to interact with services not directly declared in its own manifest. ```java /** * @author CaMnter */ public class HookAmsForServicePluginActivity extends BaseAppCompatActivity implements View.OnClickListener { View startFirstText; View startSecondText; View stopFirstText; View stopSecondText; /** * Fill in layout id * @return layout id */ @Override protected int getLayoutId() { return R.layout.activity_main; } /** * Initialize the view in the layout * @param savedInstanceState savedInstanceState */ @Override protected void initViews(Bundle savedInstanceState) { this.startFirstText = this.findViewById(R.id.start_first_text); this.startSecondText = this.findViewById(R.id.start_second_text); this.stopFirstText = this.findViewById(R.id.stop_first_text); this.stopSecondText = this.findViewById(R.id.stop_second_text); } /** * Initialize the View of the listener */ @Override protected void initListeners() { this.startFirstText.setOnClickListener(this); this.startSecondText.setOnClickListener(this); this.stopFirstText.setOnClickListener(this); this.stopSecondText.setOnClickListener(this); } /** * Initialize the Activity data */ @Override protected void initData() { } /** * Called when a view has been clicked. * @param v The view that was clicked. */ @Override public void onClick(View v) { switch (v.getId()) { case R.id.start_first_text: this.startFirstText.setEnabled(false); this.startService(new Intent().setComponent( new ComponentName("com.camnter.hook.ams.f.service.plugin.plugin", "com.camnter.hook.ams.f.service.plugin.plugin.FirstService"))); this.startFirstText.setEnabled(true); break; case R.id.start_second_text: startSecondText.setEnabled(false); this.startService(new Intent().setComponent( new ComponentName("com.camnter.hook.ams.f.service.plugin.plugin", "com.camnter.hook.ams.f.service.plugin.plugin.SecondService"))); startSecondText.setEnabled(true); break; case R.id.stop_first_text: stopFirstText.setEnabled(false); this.stopService(new Intent().setComponent( new ComponentName("com.camnter.hook.ams.f.service.plugin.plugin", "com.camnter.hook.ams.f.service.plugin.plugin.FirstService"))); stopFirstText.setEnabled(true); break; case R.id.stop_second_text: stopSecondText.setEnabled(false); this.stopService(new Intent().setComponent( new ComponentName("com.camnter.hook.ams.f.service.plugin.plugin", "com.camnter.hook.ams.f.service.plugin.plugin.SecondService"))); stopSecondText.setEnabled(true); break; } } } ``` -------------------------------- ### Execute Gradle Tasks Source: https://github.com/camnter/androidlife/blob/master/plugin-life/single-resources/single-resources-gradle-plugin/README.md These commands demonstrate how to execute specific Gradle tasks, likely related to assembling or running the project, using the `gradle` wrapper. ```shell gradle assD gradle assR ``` -------------------------------- ### LinkedHashMap Get Method with Access Order Reordering Source: https://github.com/camnter/androidlife/blob/master/article/LruCache源码解析.md This overridden `get` method in `LinkedHashMap` retrieves the value associated with the specified key. Crucially, if the `accessOrder` flag is set to `true` (as it is by default in `LruCache`'s `LinkedHashMap`), accessing an entry will cause it to be moved to the end of the internal doubly linked list, thereby implementing the 'recently used' aspect of LRU. ```Java /** * Returns the value of the mapping with the specified key. * * @param key * the key. * @return the value of the mapping with the specified key, or {@code null} * if no mapping for the specified key is found. */ @Override public V get(Object key) { /* * This method is overridden to eliminate the need for a polymorphic * invocation in superclass at the expense of code duplication. */ if (key == null) { HashMapEntry e = entryForNullKey; if (e == null) return null; if (accessOrder) makeTail((LinkedEntry) e); return e.value; } int hash = Collections.secondaryHash(key); HashMapEntry[] tab = table; for (HashMapEntry e = tab[hash & (tab.length - 1)]; e != null; e = e.next) { K eKey = e.key; if (eKey == key || (e.hash == hash && key.equals(eKey))) { if (accessOrder) makeTail((LinkedEntry) e); return e.value; } } return null; } ``` -------------------------------- ### Run dex-method-counts with Gradle on Windows Source: https://github.com/camnter/androidlife/blob/master/dex-method-counts/README.md Instructions for setting up and running dex-method-counts on Windows using Gradle, including copying the batch script and adjusting the classpath for the JAR. ```Batch $ mkdir dex-method-counts/scripts $ gradle :dex-method-counts:assemble && cp dex-method-counts/build/scripts/dex-method-counts.bat dex-method-counts/scripts/dex-method-counts.bat $ vi dex-method-counts.bat `CLASSPATH=...` -> `CLASSPATH=$APP_HOME/dex-method-counts/repository/dex-method-counts.jar` $ ./dex-method-counts/dex-method-counts.bat path\to\App.apk ``` -------------------------------- ### Configure Reduce Dependency Packaging Gradle Plugin Source: https://github.com/camnter/androidlife/blob/master/plugin-life/reduce-dependency-packaging/reduce-dependency-packaging-plugin-two/README.md Applies and configures the `com.camnter.gradle.plugin.reduce.dependency.packaging.plugin` Gradle plugin. This configuration block sets the resource `packageId`, specifies the relative path to the `targetHost` module, and optionally enables host mapping. It's essential that the host project is compiled beforehand, with `allVersions.txt`, `Host_R.txt`, and `versions.txt` files present in the `build/reduceDependencyPackagingHost` directory. ```groovy /** * 必须先编译好宿主项目,在 build/reduceDependencyPackagingHost 下必须存在 * allVersions.txt * Host_R.txt * versions.txt * */ apply plugin: 'com.camnter.gradle.plugin.reduce.dependency.packaging.plugin' reduceDependencyPackagingExtension { // the package id of Resources. packageId = 0x72 // the path of application module in host project. targetHost = '../reduce-dependency-packaging-plugin-host' // optional, default value: true. applyHostMapping = true } ``` -------------------------------- ### Start Android Plugin Activities on Click Source: https://github.com/camnter/androidlife/blob/master/plugin-life/hook-loadedapk-classloader/hook-loadedapk-classloader-host/README.md This Java code snippet implements the `onClick` method for an Android `View.OnClickListener`. It handles click events for two different UI elements (`R.id.start_first_text` and `R.id.start_second_text`), each triggering the launch of a specific plugin activity (`FirstPluginActivity` or `SecondPluginActivity`) from the `com.camnter.hook.loadedapk.classloader.plugin` package. It temporarily disables the clicked view, attempts to start the activity using an explicit `Intent` with `ComponentName`, and re-enables the view, including basic exception handling. ```Java /** * Called when a view has been clicked. * * @param v The view that was clicked. */ @Override public void onClick(View v) { switch (v.getId()) { case R.id.start_first_text: this.startFirstText.setEnabled(false); try { this.startActivity(new Intent().setComponent( new ComponentName("com.camnter.hook.loadedapk.classloader.plugin", "com.camnter.hook.loadedapk.classloader.plugin.FirstPluginActivity"))); } catch (Exception e) { e.printStackTrace(); } this.startFirstText.setEnabled(true); break; case R.id.start_second_text: this.startSecondText.setEnabled(false); try { this.startActivity(new Intent().setComponent( new ComponentName("com.camnter.hook.loadedapk.classloader.plugin", "com.camnter.hook.loadedapk.classloader.plugin.SecondPluginActivity"))); } catch (Exception e) { e.printStackTrace(); } this.startSecondText.setEnabled(true); break; } } ``` -------------------------------- ### Build Host Module for Reduce Dependency Packaging Plugin Source: https://github.com/camnter/androidlife/blob/master/plugin-life/reduce-dependency-packaging/reduce-dependency-packaging-plugin-three/README.md This shell command builds the host module (`reduce-dependency-packaging-plugin-host`), which is a prerequisite for the `reduce-dependency-packaging` plugin. Successful execution ensures the generation of essential files like `allVersions.txt`, `Host_R.txt`, and `versions.txt` within the `build/reduceDependencyPackagingHost` directory, necessary for the plugin's operation. ```shell gradle :plugin-life:reduce-dependency-packaging:reduce-dependency-packaging-plugin-host:build ``` -------------------------------- ### dex-method-counts Command-Line Options Source: https://github.com/camnter/androidlife/blob/master/dex-method-counts/README.md Detailed documentation for the command-line options supported by the dex-method-counts tool, allowing customization of output and filtering based on various criteria. ```APIDOC Options: --count-fields: Provide the field count instead of the method count. --include-classes: Treat classes as packages and provide per-class method counts. One use-case is for protocol buffers where all generated code in a package ends up in a single class. --package-filter=...: Only consider methods whose fully qualified name starts with this prefix. --max-depth=...: Limit how far into package paths (or inner classes, with `--include-classes`) counts should be reported for. --filter=[all|defined_only|referenced_only]: Whether to count all methods (the default), just those defined in the input file, or just those that are referenced in it. Note that referenced methods count against the 64K method limit too. --output-style=[flat|tree]: Print the output as a list or as an indented tree. ``` -------------------------------- ### Execute Resources Size Plugin Tasks Source: https://github.com/camnter/androidlife/blob/master/buildSrc/resources-size-plugin-README.md This snippet provides shell commands to execute the `resources-size-plugin` tasks for both debug and release builds, along with a clean task. These commands are typically run from the project's root directory. ```shell gradle clean gradle resourcesSizeDebug gradle resourcesSizeRelease ``` -------------------------------- ### Android Singleton Utility Class Definition Source: https://github.com/camnter/androidlife/blob/master/plugin-life/hook-ams-for-activity-plugin/hook-ams-for-activity-plugin-host/README.md Defines the `Singleton` abstract class, a common helper for lazy initialization of objects in Android. It ensures that only one instance of a class is created and provides a thread-safe `get()` method to retrieve that instance. ```Java public abstract class Singleton { private T mInstance; protected abstract T create(); public final T get() { synchronized (this) { if (mInstance == null) { mInstance = create(); } return mInstance; } } } ``` -------------------------------- ### LinkedHashMap Get Method with Access Order Logic Source: https://github.com/camnter/androidlife/blob/master/article/LruCache源码解析_改.md This method returns the value associated with the specified key. It's overridden to optimize performance by avoiding polymorphic invocation. If 'accessOrder' is true, accessing an entry will cause it to be moved to the tail of the internal linked list, ensuring LRU behavior. ```Java /** * Returns the value of the mapping with the specified key. * * @param key * the key. * @return the value of the mapping with the specified key, or {@code null} * if no mapping for the specified key is found. */ @Override public V get(Object key) { /* * This method is overridden to eliminate the need for a polymorphic * invocation in superclass at the expense of code duplication. */ if (key == null) { HashMapEntry e = entryForNullKey; if (e == null) return null; if (accessOrder) makeTail((LinkedEntry) e); return e.value; } int hash = Collections.secondaryHash(key); HashMapEntry[] tab = table; for (HashMapEntry e = tab[hash & (tab.length - 1)]; e != null; e = e.next) { K eKey = e.key; if (eKey == key || (e.hash == hash && key.equals(eKey))) { if (accessOrder) makeTail((LinkedEntry) e); return e.value; } } return null; } ``` -------------------------------- ### Build Host Module using Gradle Shell Command Source: https://github.com/camnter/androidlife/blob/master/plugin-life/reduce-dependency-packaging/reduce-dependency-packaging-plugin-two/README.md Executes the Gradle build command for the `reduce-dependency-packaging-plugin-host` module, which is part of the `plugin-life` project. This step is crucial before configuring the dependency packaging plugin. ```shell gradle :plugin-life:reduce-dependency-packaging:reduce-dependency-packaging-plugin-host:build ``` -------------------------------- ### APIDOC: LruCache entryRemoved Method Scenarios Source: https://github.com/camnter/androidlife/blob/master/article/LruCache源码解析.md The `entryRemoved` method is a callback invoked by LruCache under various conditions: when a key conflict occurs during `put`, during `trimToSize` for the last evicted entry, upon successful `remove`, or in specific `get` scenarios involving a custom `create` method. It allows for custom logic to handle evicted or replaced entries. ```APIDOC LruCache.entryRemoved(boolean evicted, K key, V oldValue, V newValue) Description: This method is a callback invoked by LruCache when an entry is removed or replaced. It allows for custom logic to handle evicted or replaced entries. The default implementation does nothing. This method is not synchronized and can be executed by other threads accessing the cache. Parameters: - evicted (boolean): - true: If the entry was removed to free up space (e.g., by trimToSize or remove). - false: If the entry was replaced due to a put conflict or successfully created via get's custom create method. - key (K): The key of the entry. - oldValue (V): The old value of the entry. - newValue (V): The new value of the entry (null if removed, non-null if replaced by put/get). Invocation Scenarios: 1. **put (key conflict):** - evicted: false - key: The key of the current put operation. - oldValue: The value being overwritten (the conflicting value). - newValue: The value from the current put operation. 2. **trimToSize:** - Invoked once for the last evicted least-recently-used entry. - evicted: true - key: The key of the last removed entry. - oldValue: The value of the last removed entry. - newValue: null (no conflict, just removal). 3. **remove:** - Invoked if the key exists and is successfully removed. - evicted: false - key: The key of the remove operation. - oldValue: The value of the removed entry. - newValue: null (no conflict, just removal). 4. **get (after custom create, with conflict):** - Occurs if a custom `create(K key)` method is implemented, and the created value conflicts with an existing entry when being put into the cache. - evicted: false - key: The key of the get operation. - oldValue: The value created by the custom `create(key)` method. - newValue: The value that was originally present in the map for that key. Method Signature: protected void entryRemoved(boolean evicted, K key, V oldValue, V newValue) { } ``` -------------------------------- ### Run dex-method-counts with Ant Source: https://github.com/camnter/androidlife/blob/master/dex-method-counts/README.md Instructions to build and run the dex-method-counts tool using Ant, targeting Android APK, ZIP, DEX files, or directories. ```Shell $ ant jar $ ./dex-method-counts path/to/App.apk # or .zip or .dex or directory ``` -------------------------------- ### Implementing LruCache for Bitmap Caching in Android Source: https://github.com/camnter/androidlife/blob/master/article/LruCache源码解析.md This Java code demonstrates how to initialize an LruCache for caching Bitmap objects. It shows the essential overrides for `sizeOf` to calculate the byte count of Bitmaps for capacity management, and `entryRemoved` to handle custom logic when an entry is evicted or removed. The example also highlights the thread-safe nature of LruCache operations. ```Java private static final float ONE_MIB = 1024 * 1024; // 7MB private static final int CACHE_SIZE = (int) (7 * ONE_MIB); private LruCache bitmapCache; this.bitmapCache = new LruCache(CACHE_SIZE) { protected int sizeOf(String key, Bitmap value) { return value.getByteCount(); } /** * 1.当被回收或者删掉时调用。该方法当value被回收释放存储空间时被remove调用 * 或者替换条目值时put调用,默认实现什么都没做。 * 2.该方法没用同步调用,如果其他线程访问缓存时,该方法也会执行。 * 3.evicted=true:如果该条目被删除空间 (表示 进行了trimToSize or remove) evicted=false:put冲突后 或 get里成功create后 导致 * 4.newValue!=null,那么则被put()或get()调用。 */ @Override protected void entryRemoved(boolean evicted, String key, Bitmap oldValue, Bitmap newValue) { mEntryRemovedInfoText.setText( String.format(Locale.getDefault(), LRU_CACHE_ENTRY_REMOVED_INFO_FORMAT, evicted, key, oldValue != null ? oldValue.hashCode() : "null", newValue != null ? newValue.hashCode() : "null")); // 见上述 3. if (evicted) { // 进行了trimToSize or remove (一般是溢出了 或 key-value被删除了 ) if (recentList.contains(key)) { recentList.remove(key); refreshText(mRecentInfoText, LRU_CACHE_RECENT_FORMAT, recentList); } } else { // put冲突后 或 get里成功create 后 recentList.remove(key); refreshText(mRecentInfoText, LRU_CACHE_RECENT_FORMAT, recentList); } if (cacheList.contains(key)) { cacheList.remove(key); refreshText(mCacheDataText, LRU_CACHE_CACHE_DATA_FORMAT, cacheList); } } }; ``` -------------------------------- ### Execute Android Resources Optimization Gradle Tasks Source: https://github.com/camnter/androidlife/blob/master/buildSrc/resources-optimize-l2-plugin-README.md This snippet provides shell commands to clean the project and execute specific Gradle tasks for resource optimization. It demonstrates how to trigger the optimization process for both debug and release build variants. ```shell gradle clean gradle resourcesOptimizeL2Debug gradle resourcesOptimizeL2Release ``` -------------------------------- ### LruCache Get Method Implementation and LRU Integration Source: https://github.com/camnter/androidlife/blob/master/article/LruCache源码解析.md This method retrieves a value from the cache based on a given key. If the value exists or is created by the `create` method, it's moved to the tail of the internal `LinkedHashMap`'s access-ordered list, ensuring LRU behavior. It tracks hit and miss counts and handles potential conflicts if a value is added concurrently during `create` execution. ```Java /** * 根据 key 查询缓存,如果存在于缓存或者被 create 方法创建了。 * 如果值返回了,那么它将被移动到双向循环链表的的尾部。 * 如果如果没有缓存的值,则返回 null。 */ public final V get(K key) { ... V mapValue; synchronized (this) { // LinkHashMap 如果设置按照访问顺序的话,这里每次get都会重整数据顺序 mapValue = map.get(key); // 计算 命中次数 if (mapValue != null) { hitCount++; return mapValue; } // 计算 丢失次数 missCount++; } /* * 官方解释: * 尝试创建一个值,这可能需要很长时间,并且Map可能在create()返回的值时有所不同。如果在create()执行的时 * 候,一个冲突的值被添加到Map,我们在Map中删除这个值,释放被创造的值。 */ V createdValue = create(key); if (createdValue == null) { return null; } /*************************** * 不覆写create方法走不到下面 * ***************************/ /* * 正常情况走不到这里 * 走到这里的话 说明 实现了自定义的 create(K key) 逻辑 * 因为默认的 create(K key) 逻辑为null */ synchronized (this) { // 记录 create 的次数 createCount++; // 将自定义create创建的值,放入LinkedHashMap中,如果key已经存在,会返回 之前相同key 的值 mapValue = map.put(key, createdValue); // 如果之前存在相同key的value,即有冲突。 if (mapValue != null) { /* * 有冲突 * 所以 撤销 刚才的 操作 * 将 之前相同key 的值 重新放回去 */ map.put(key, mapValue); } else { // 拿到键值对,计算出在容量中的相对长度,然后加上 size += safeSizeOf(key, createdValue); } } // 如果上面 判断出了 将要放入的值发生冲突 if (mapValue != null) { /* * 刚才create的值被删除了,原来的 之前相同key 的值被重新添加回去了 * 告诉 自定义 的 entryRemoved 方法 */ entryRemoved(false, key, createdValue, mapValue); return mapValue; } else { // 上面 进行了 size += 操作 所以这里要重整长度 trimToSize(maxSize); return createdValue; } } ``` -------------------------------- ### Android Hooking: SmartApplication for DEX Loading and Instrumentation Injection Source: https://github.com/camnter/androidlife/blob/master/plugin-life/load-plugin-resources/load-plugin-resources-host/README.md The `SmartApplication` class extends `android.app.Application` to facilitate dynamic DEX file loading and inject a custom `Instrumentation` into the `ActivityThread`. It copies a plugin APK from the application's assets to a cache directory, loads it using `DexClassLoader`, and then uses reflection to replace the default `Instrumentation` instance with a custom `SmartInstrumentation`. ```Java /** * @author CaMnter */ public class SmartApplication extends Application { private DexClassLoader dexClassLoader; /** * Set the base context for this ContextWrapper. All calls will then be * delegated to the base context. Throws * IllegalStateException if a base context has already been set. * * @param base The new base context for this wrapper. */ @Override protected void attachBaseContext(Context base) { super.attachBaseContext(base); this.loadDex(); this.hookInstrumentation(); } private void loadDex() { try { String dir = null; final File cacheDir = this.getExternalCacheDir(); final File filesDir = this.getExternalFilesDir(""); if (cacheDir != null) { dir = cacheDir.getAbsolutePath(); } else { if (filesDir != null) { dir = filesDir.getAbsolutePath(); } } if (TextUtils.isEmpty(dir)) return; // assets 的 register-activity-plugin.apk 拷贝到 /storage/sdcard0/Android/data/[package name]/cache // 或者 /storage/sdcard0/Android/data/[package name]/files final File dexPath = new File(dir + File.separator + "register-activity-plugin.apk"); AssetsUtils.copyAssets(this, "register-activity-plugin.apk", dexPath.getAbsolutePath()); // /data/data/[package name]/app_register-activity-plugin final File optimizedDirectory = this.getDir("register-activity-plugin", MODE_PRIVATE); this.dexClassLoader = new DexClassLoader( dexPath.getAbsolutePath(), optimizedDirectory.getAbsolutePath(), null, this.getClassLoader() ); } catch (Exception e) { e.printStackTrace(); } } private void hookInstrumentation() { try { /* * 反射 ActivityThread * 通过 currentActivityThread 方法 * 获取存放的 ActivityThread 实例 */ @SuppressLint("PrivateApi") final Class activityThreadClass = Class.forName( "android.app.ActivityThread"); final Method currentActivityThreadMethod = activityThreadClass.getDeclaredMethod( "currentActivityThread"); currentActivityThreadMethod.setAccessible(true); final Object currentActivityThread = currentActivityThreadMethod.invoke(null); /* * ActivityThread 实例获取 Field mInstrumentation */ final Field mInstrumentationField = activityThreadClass.getDeclaredField( "mInstrumentation"); mInstrumentationField.setAccessible(true); final Instrumentation mInstrumentation = (Instrumentation) mInstrumentationField.get( currentActivityThread); // 是否 hook 过 if (!(mInstrumentation instanceof SmartInstrumentation)) { final SmartInstrumentation pluginInstrumentation = new SmartInstrumentation( dexClassLoader, mInstrumentation); mInstrumentationField.set(currentActivityThread, pluginInstrumentation); } } catch (Exception e) { e.printStackTrace(); } } public DexClassLoader getDexClassLoader() { return this.dexClassLoader; } } ``` -------------------------------- ### Initialize Android Plugin Framework in Application's attachBaseContext Source: https://github.com/camnter/androidlife/blob/master/plugin-life/hook-ams-for-activity-plugin/hook-ams-for-activity-plugin-host/README.md This `SmartApplication` class extends `Application` and overrides `attachBaseContext`. It performs critical initialization for an Android plugin framework, including hooking `ActivityManagerNative` and `ActivityThread.H`, extracting plugin APKs, patching the `ClassLoader` with `BaseDexClassLoaderHooker`, and pre-loading activity information using `ActivityInfoUtils`. This setup enables the application to load and manage plugin components dynamically. ```Java /** * @author CaMnter */ public class SmartApplication extends Application { private static Context BASE; private static Map activityInfoMap; /** * Set the base context for this ContextWrapper. All calls will then be * delegated to the base context. Throws * IllegalStateException if a base context has already been set. * * @param base The new base context for this wrapper. */ @Override protected void attachBaseContext(Context base) { super.attachBaseContext(base); BASE = base; try { // Hook AMS AMSHooker.hookActivityManagerNative(); // Hook H AMSHooker.hookActivityThreadH(); // assets 的 hook-ams-for-activity-plugin-plugin.apk 拷贝到 /data/data/files/[package name] AssetsUtils.extractAssets(base, "hook-ams-for-activity-plugin-plugin.apk"); final File apkFile = getFileStreamPath("hook-ams-for-activity-plugin-plugin.apk"); final File odexFile = getFileStreamPath("hook-ams-for-activity-plugin-plugin.odex"); // // Hook ClassLoader, 让插件中的类能够被成功加载 BaseDexClassLoaderHooker.patchClassLoader(this.getClassLoader(), apkFile, odexFile); activityInfoMap = ActivityInfoUtils.preLoadActivities(apkFile, base); } catch (Exception e) { e.printStackTrace(); } } public static Context getContext() { return BASE; } public static Map getActivityInfoMap() { return activityInfoMap; } } ``` -------------------------------- ### Launch Plugin Activities via ComponentName Source: https://github.com/camnter/androidlife/blob/master/plugin-life/hook-ams-for-activity-plugin/hook-ams-for-activity-plugin-host/README.md This snippet demonstrates how to programmatically start activities from a plugin APK. It uses `Intent.setComponent` with a `ComponentName` specifying the package name and the fully qualified class name of the plugin activity. This method is typically used after the plugin's `ActivityInfo` has been pre-loaded and the `ClassLoader` has been patched to recognize the plugin classes, allowing the host application to launch activities defined within a loaded plugin. ```Java /** * Called when a view has been clicked. * * @param v The view that was clicked. */ @Override public void onClick(View v) { switch (v.getId()) { case R.id.start_first_text: this.startFirstText.setEnabled(false); try { this.startActivity(new Intent().setComponent( new ComponentName("com.camnter.hook.ams.f.activity.plugin.plugin", "com.camnter.hook.ams.f.activity.plugin.plugin.FirstPluginActivity"))); } catch (Exception e) { e.printStackTrace(); } this.startFirstText.setEnabled(true); break; case R.id.start_second_text: this.startSecondText.setEnabled(false); try { this.startActivity(new Intent().setComponent( new ComponentName("com.camnter.hook.ams.f.activity.plugin.plugin", "com.camnter.hook.ams.f.activity.plugin.plugin.SecondPluginActivity"))); } catch (Exception e) { e.printStackTrace(); } this.startSecondText.setEnabled(true); break; } } ``` -------------------------------- ### Build Host Module for Reduce Dependency Packaging Plugin Source: https://github.com/camnter/androidlife/blob/master/plugin-life/reduce-dependency-packaging/reduce-dependency-packaging-plugin-one/README.md Executes the Gradle build command for the `reduce-dependency-packaging-plugin-host` module. This step is crucial as it generates necessary files (`allVersions.txt`, `Host_R.txt`, `versions.txt`) in the `build/reduceDependencyPackagingHost` directory, which are required by the plugin. ```shell gradle :plugin-life:reduce-dependency-packaging:reduce-dependency-packaging-plugin-host:build ``` -------------------------------- ### Android Activity Launch Flow and ActivityThread.H Hook Point Source: https://github.com/camnter/androidlife/blob/master/plugin-life/hook-loadedapk-classloader/hook-loadedapk-classloader-host/README.md This section explains the intricate flow of Android activity launches, from ContextImpl to IActivityManager in the system_server process, and finally back to the app process via ApplicationThread. It highlights ActivityThread.H as a crucial Handler responsible for dispatching messages like LAUNCH_ACTIVITY to start activities. This H handler is identified as a key interception point for modifying activity launch behavior within the application's process. ```APIDOC /** * 启动 Activity 时 * * ContextImpl # execStartActivity -> Instrumentation # execStartActivity -> IActivityManager # * startActivity * * 进入了 AMS 所在的进程 system_server * * 然后,在该进程一直辗转与 ActivityStackSupervisor <-> ActivityStack * * 要回到 App 进程的时候,system_server 通过 ApplicationThread 这个 Binder proxy 对象回到 App 进程 * 然后通过 H( Handler )类分发消息 * * ------------------------------------------------------------------------------------------------- * * public final class ActivityThread { * * - ... * * - private class H extends Handler { * * - ... * * - public static final int LAUNCH_ACTIVITY = 100; * * - ... * * - public void handleMessage(Message msg) { * - switch (msg.what) { * - case LAUNCH_ACTIVITY: { * - Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "activityStart"); * - final ActivityClientRecord r = (ActivityClientRecord) msg.obj; * * - r.packageInfo = getPackageInfoNoCheck(r.activityInfo.applicationInfo, * - r.compatInfo); * - handleLaunchActivity(r, null, "LAUNCH_ACTIVITY"); * - Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER); * - } break; * * - ... * * - } * * - ... * * - } * * - ... * * } * * ------------------------------------------------------------------------------------------------- * * 得在 system_server 的消息回来的时候,拦截这个 ApplicationThread 发出的 LAUNCH_ACTIVITY 消息 * * ------------------------------------------------------------------------------------------------- * ``` -------------------------------- ### Build Android Plugin APK Source: https://github.com/camnter/androidlife/blob/master/plugin-life/multi-classloader-plugin/multi-classloader-plugin-two/README.md Provides instructions on how to build the Android plugin APK using the project's build system. The output APK will be located in the specified directory, and a renaming step is required for proper identification. ```Build System Action: Build Project Output Path: build/output/apk ``` ```Shell Rename ***.apk to multi-classloader-plugin-two.apk ```