/* * Copyright (C) 2015 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.tencent.tinker.loader; import android.content.Context; import android.content.pm.ApplicationInfo; import android.content.res.AssetManager; import android.content.res.Resources; import android.os.Build; import android.util.ArrayMap; import android.util.Log; import com.tencent.tinker.loader.shareutil.ShareConstants; import com.tencent.tinker.loader.shareutil.ShareReflectUtil; import java.lang.ref.WeakReference; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.util.Collection; import java.util.HashMap; import java.util.Map; import static android.os.Build.VERSION.SDK_INT; import static android.os.Build.VERSION_CODES.KITKAT; /** * Created by zhangshaowen on 16/9/21. * Thanks for Android Fragmentation */ class TinkerResourcePatcher { private static final String TAG = "Tinker.ResourcePatcher"; private static final String TEST_ASSETS_VALUE = "only_use_to_test_tinker_resource.txt"; // private static final String MIUI_RESOURCE_CLASSNAME = "android.content.res.MiuiResources"; // original object private static Collection<WeakReference<Resources>> references = null; private static Object currentActivityThread = null; private static AssetManager newAssetManager = null; // private static ArrayMap<?, WeakReference<?>> resourceImpls = null; // method private static Method addAssetPathMethod = null; private static Method ensureStringBlocksMethod = null; // field private static Field assetsFiled = null; private static Field resourcesImplFiled = null; private static Field resDir = null; private static Field packagesFiled = null; private static Field resourcePackagesFiled = null; private static Field publicSourceDirField = null; // private static boolean isMiuiSystem = false; public static void isResourceCanPatch(Context context) throws Throwable { // - Replace mResDir to point to the external resource file instead of the .apk. This is // used as the asset path for new Resources objects. // - Set Application#mLoadedApk to the found LoadedApk instance // Find the ActivityThread instance for the current thread Class<?> activityThread = Class.forName("android.app.ActivityThread"); currentActivityThread = ShareReflectUtil.getActivityThread(context, activityThread); // API version 8 has PackageInfo, 10 has LoadedApk. 9, I don't know. Class<?> loadedApkClass; try { loadedApkClass = Class.forName("android.app.LoadedApk"); } catch (ClassNotFoundException e) { loadedApkClass = Class.forName("android.app.ActivityThread$PackageInfo"); } resDir = loadedApkClass.getDeclaredField("mResDir"); resDir.setAccessible(true); packagesFiled = activityThread.getDeclaredField("mPackages"); packagesFiled.setAccessible(true); resourcePackagesFiled = activityThread.getDeclaredField("mResourcePackages"); resourcePackagesFiled.setAccessible(true); // Create a new AssetManager instance and point it to the resources AssetManager assets = context.getAssets(); // Baidu os if (assets.getClass().getName().equals("android.content.res.BaiduAssetManager")) { Class baiduAssetManager = Class.forName("android.content.res.BaiduAssetManager"); newAssetManager = (AssetManager) baiduAssetManager.getConstructor().newInstance(); } else { newAssetManager = AssetManager.class.getConstructor().newInstance(); } addAssetPathMethod = AssetManager.class.getDeclaredMethod("addAssetPath", String.class); addAssetPathMethod.setAccessible(true); // Kitkat needs this method call, Lollipop doesn't. However, it doesn't seem to cause any harm // in L, so we do it unconditionally. ensureStringBlocksMethod = AssetManager.class.getDeclaredMethod("ensureStringBlocks"); ensureStringBlocksMethod.setAccessible(true); // Iterate over all known Resources objects if (SDK_INT >= KITKAT) { //pre-N // Find the singleton instance of ResourcesManager Class<?> resourcesManagerClass = Class.forName("android.app.ResourcesManager"); Method mGetInstance = resourcesManagerClass.getDeclaredMethod("getInstance"); mGetInstance.setAccessible(true); Object resourcesManager = mGetInstance.invoke(null); try { Field fMActiveResources = resourcesManagerClass.getDeclaredField("mActiveResources"); fMActiveResources.setAccessible(true); ArrayMap<?, WeakReference<Resources>> activeResources19 = (ArrayMap<?, WeakReference<Resources>>) fMActiveResources.get(resourcesManager); references = activeResources19.values(); } catch (NoSuchFieldException ignore) { // N moved the resources to mResourceReferences Field mResourceReferences = resourcesManagerClass.getDeclaredField("mResourceReferences"); mResourceReferences.setAccessible(true); references = (Collection<WeakReference<Resources>>) mResourceReferences.get(resourcesManager); } } else { Field fMActiveResources = activityThread.getDeclaredField("mActiveResources"); fMActiveResources.setAccessible(true); HashMap<?, WeakReference<Resources>> activeResources7 = (HashMap<?, WeakReference<Resources>>) fMActiveResources.get(currentActivityThread); references = activeResources7.values(); } // check resource if (references == null) { throw new IllegalStateException("resource references is null"); } try { assetsFiled = Resources.class.getDeclaredField("mAssets"); assetsFiled.setAccessible(true); } catch (Throwable ignore) { // N moved the mAssets inside an mResourcesImpl field resourcesImplFiled = Resources.class.getDeclaredField("mResourcesImpl"); resourcesImplFiled.setAccessible(true); } // final Resources resources = context.getResources(); // isMiuiSystem = resources != null && MIUI_RESOURCE_CLASSNAME.equals(resources.getClass().getName()); try { publicSourceDirField = ShareReflectUtil.findField(ApplicationInfo.class, "publicSourceDir"); } catch (NoSuchFieldException ignore) { } } /** * @param context * @param externalResourceFile * @throws Throwable */ public static void monkeyPatchExistingResources(Context context, String externalResourceFile) throws Throwable { if (externalResourceFile == null) { return; } for (Field field : new Field[]{packagesFiled, resourcePackagesFiled}) { Object value = field.get(currentActivityThread); for (Map.Entry<String, WeakReference<?>> entry : ((Map<String, WeakReference<?>>) value).entrySet()) { Object loadedApk = entry.getValue().get(); if (loadedApk == null) { continue; } if (externalResourceFile != null) { resDir.set(loadedApk, externalResourceFile); } } } // Create a new AssetManager instance and point it to the resources installed under if (((Integer) addAssetPathMethod.invoke(newAssetManager, externalResourceFile)) == 0) { throw new IllegalStateException("Could not create new AssetManager"); } // Kitkat needs this method call, Lollipop doesn't. However, it doesn't seem to cause any harm // in L, so we do it unconditionally. ensureStringBlocksMethod.invoke(newAssetManager); for (WeakReference<Resources> wr : references) { Resources resources = wr.get(); //pre-N if (resources != null) { // Set the AssetManager of the Resources instance to our brand new one try { assetsFiled.set(resources, newAssetManager); } catch (Throwable ignore) { // N Object resourceImpl = resourcesImplFiled.get(resources); // for Huawei HwResourcesImpl Field implAssets = ShareReflectUtil.findField(resourceImpl, "mAssets"); implAssets.setAccessible(true); implAssets.set(resourceImpl, newAssetManager); } clearPreloadTypedArrayIssue(resources); resources.updateConfiguration(resources.getConfiguration(), resources.getDisplayMetrics()); } } // Handle issues caused by WebView on Android N. // Issue: On Android N, if an activity contains a webview, when screen rotates // our resource patch may lost effects. // for 5.x/6.x, we found Couldn't expand RemoteView for StatusBarNotification Exception if (Build.VERSION.SDK_INT >= 24) { try { if (publicSourceDirField != null) { publicSourceDirField.set(context.getApplicationInfo(), externalResourceFile); } } catch (Throwable ignore) { } } if (!checkResUpdate(context)) { throw new TinkerRuntimeException(ShareConstants.CHECK_RES_INSTALL_FAIL); } } /** * Why must I do these? * Resource has mTypedArrayPool field, which just like Message Poll to reduce gc * MiuiResource change TypedArray to MiuiTypedArray, but it get string block from offset instead of assetManager */ private static void clearPreloadTypedArrayIssue(Resources resources) { // Perform this trick not only in Miui system since we can't predict if any other // manufacturer would do the same modification to Android. // if (!isMiuiSystem) { // return; // } Log.w(TAG, "try to clear typedArray cache!"); // Clear typedArray cache. try { Field typedArrayPoolField = ShareReflectUtil.findField(Resources.class, "mTypedArrayPool"); final Object origTypedArrayPool = typedArrayPoolField.get(resources); Field poolField = ShareReflectUtil.findField(origTypedArrayPool, "mPool"); final Constructor<?> typedArrayConstructor = origTypedArrayPool.getClass().getConstructor(int.class); typedArrayConstructor.setAccessible(true); final int poolSize = ((Object[]) poolField.get(origTypedArrayPool)).length; final Object newTypedArrayPool = typedArrayConstructor.newInstance(poolSize); typedArrayPoolField.set(resources, newTypedArrayPool); } catch (Throwable ignored) { Log.e(TAG, "clearPreloadTypedArrayIssue failed, ignore error: " + ignored); } } private static boolean checkResUpdate(Context context) { try { Log.e(TAG, "checkResUpdate success, found test resource assets file " + TEST_ASSETS_VALUE); context.getAssets().open(TEST_ASSETS_VALUE); } catch (Throwable e) { Log.e(TAG, "checkResUpdate failed, can't find test resource assets file " + TEST_ASSETS_VALUE + " e:" + e.getMessage()); return false; } return true; } }