Optimization of slow cold start caused by wechat tinker

Posted by grandadevans on Tue, 04 Jan 2022 20:41:34 +0100

1. Android S users reported that wechat started slowly

The first thing I think of is the state of dex

  1. Whether oat DEX (such as bg dex or other types of dex) is performed, and whether oat files and art files are normal
  2. Whether the commonly used communication software is not protected. Generally, it is not recommended to recycle the software with strong user perception,
  3. Old question: is there a tinker (wechat hot update, which is prohibited in Google play, and generally appears in domestic downloaded apps)
  4. Other situations

2. Grab wechat systrace and check it

=>You can see that there was tinker
OpenDexFilesFromOat(/data/user/0/com.tencent.mm/tinker/patch-66e50d2a/dex/tinker_classN.apk)

=>Check out / data / user / 0 / com tencent. Mm / tinker / directory. It is found that there are many tinker contents

$ adb shell ls -al /data/user/0/com.tencent.mm/tinker/
drwx------ 3 u0_a211 u0_a211 3452 2021-12-21 16:27 .
drwx------ 46 u0_a211 u0_a211 3452 2021-12-22 08:56 ...
-rw------- 1 u0_a211 u0_a211 0 2021-12-22 08:57 info.lock
drwx------ 6 u0_a211 u0_a211 3452 2021-12-21 15:57 patch-66e50d2a
-rw-rw-rw- 1 u0_a211 u0_a211 359 2021-12-21 16:25 patch.info
-rw------- 1 u0_a211 u0_a211 42 2021-12-22 08:57 safemode_count_rec_com.tencent.mm
-rw------- 1 u0_a211 u0_a211 42 2021-12-21 17:32 safemode_count_rec_com.tencent.mm:appbrand0
-rw------- 1 u0_a211 u0_a211 42 2021-12-22 08:57 safemode_count_rec_com.tencent.mm:appbrand1
-rw------- 1 u0_a211 u0_a211 42 2021-12-21 16:26 safemode_count_rec_com.tencent.mm:cuploader
-rw------- 1 u0_a211 u0_a211 42 2021-12-22 08:56 safemode_count_rec_com.tencent.mm:push
-rw------- 1 u0_a211 u0_a211 42 2021-12-21 16:27 safemode_count_rec_com.tencent.mm:recovery
-rw------- 1 u0_a211 u0_a211 42 2021-12-21 17:32 safemode_count_rec_com.tencent.mm:sandbox

=>It contains tinker_classN.apk also has odex/vdex/art/so and other files. At present, tinker has been optimized by wechat,
However, due to the large size of the file, it is equivalent to loading the dex file twice. It is easy to see the impact of low configuration mobile phones. (equivalent to 2 cold starts)

0 /data/user/0/com.tencent.mm/tinker/info.lock
8.4M /data/user/0/com.tencent.mm/tinker/patch-66e50d2a/patch-66e50d2a.apk
36K /data/user/0/com.tencent.mm/tinker/patch-66e50d2a/dex/oat/tinker_classN.apk.cur.prof
2.6M /data/user/0/com.tencent.mm/tinker/patch-66e50d2a/dex/oat/arm/tinker_classN.vdex
8.1M /data/user/0/com.tencent.mm/tinker/patch-66e50d2a/dex/oat/arm/tinker_classN.odex
2.8M /data/user/0/com.tencent.mm/tinker/patch-66e50d2a/dex/oat/arm/tinker_classN.art
14M /data/user/0/com.tencent.mm/tinker/patch-66e50d2a/dex/oat/arm
64K /data/user/0/com.tencent.mm/tinker/patch-66e50d2a/dex/oat/tinker_classN.apk.prof
14M /data/user/0/com.tencent.mm/tinker/patch-66e50d2a/dex/oat
132M /data/user/0/com.tencent.mm/tinker/patch-66e50d2a/dex/tinker_classN.apk
146M /data/user/0/com.tencent.mm/tinker/patch-66e50d2a/dex
3.5K /data/user/0/com.tencent.mm/tinker/patch-66e50d2a/odex
3.7M /data/user/0/com.tencent.mm/tinker/patch-66e50d2a/lib/lib/armeabi-v7a/libliteavsdk.so
9.4M /data/user/0/com.tencent.mm/tinker/patch-66e50d2a/lib/lib/armeabi-v7a/libapp.so
6.3M /data/user/0/com.tencent.mm/tinker/patch-66e50d2a/lib/lib/armeabi-v7a/libflutter.so
1.1M /data/user/0/com.tencent.mm/tinker/patch-66e50d2a/lib/lib/armeabi-v7a/libwechatlv.so
21M /data/user/0/com.tencent.mm/tinker/patch-66e50d2a/lib/lib/armeabi-v7a
21M /data/user/0/com.tencent.mm/tinker/patch-66e50d2a/lib/lib
21M /data/user/0/com.tencent.mm/tinker/patch-66e50d2a/lib
68M /data/user/0/com.tencent.mm/tinker/patch-66e50d2a/res/resources.apk
68M /data/user/0/com.tencent.mm/tinker/patch-66e50d2a/res
243M /data/user/0/com.tencent.mm/tinker/patch-66e50d2a
4.0K /data/user/0/com.tencent.mm/tinker/patch.info
4.0K /data/user/0/com.tencent.mm/tinker/safemode_count_rec_com.tencent.mm
4.0K /data/user/0/com.tencent.mm/tinker/safemode_count_rec_com.tencent.mm:push
4.0K /data/user/0/com.tencent.mm/tinker/safemode_count_rec_com.tencent.mm:appbrand1
4.0K /data/user/0/com.tencent.mm/tinker/safemode_count_rec_com.tencent.mm:appbrand0
4.0K /data/user/0/com.tencent.mm/tinker/safemode_count_rec_com.tencent.mm:sandbox
4.0K /data/user/0/com.tencent.mm/tinker/safemode_count_rec_com.tencent.mm:cuploader
4.0K /data/user/0/com.tencent.mm/tinker/safemode_count_rec_com.tencent.mm:recovery
243M /data/user/0/com.tencent.mm/tinker/

ps:
In the previous version of Tinker (June 2020), there was only one hot update apk in the tinker directory, which would be slower

$ tinker$ du -ah

5.1M ./patch-66490a13/patch-66490a13.apk
4.0K ./patch-66490a13/odex
3.9M ./patch-66490a13/lib/lib/armeabi-v7a/libmagicbrush.so
3.4M ./patch-66490a13/lib/lib/armeabi-v7a/libliteavsdk.so
476K ./patch-66490a13/lib/lib/armeabi-v7a/libwechatsight_v7a.so
11M ./patch-66490a13/lib/lib/armeabi-v7a/libapp.so
19M ./patch-66490a13/lib/lib/armeabi-v7a
19M ./patch-66490a13/lib/lib
19M ./patch-66490a13/lib
83M ./patch-66490a13/dex/tinker_classN.apk
44K ./patch-66490a13/dex/oat/tinker_classN.apk.cur.prof
48K ./patch-66490a13/dex/oat
83M ./patch-66490a13/dex
48M ./patch-66490a13/res/resources.apk
48M ./patch-66490a13/res
154M ./patch-66490a13
0 ./info.lock
4.0K ./patch.info
154M .

3. Effect of Tinker on cold start time

=>With tinker, verify wm_activity_launch_time this time is about 4s

I wm_activity_launch_time: [0,263586177,com.tencent.mm/.app.WeChatSplashActivity,4071]
I wm_activity_launch_time: [0,225687646,com.tencent.mm/.app.WeChatSplashActivity,4032]

=>If you manually delete the entire tinker and the verification time is significantly reduced, wechat on Android S will still slow down the startup time

1317 1408 I wm_activity_launch_time: [0,244922921,com.tencent.mm/.app.WeChatSplashActivity,2158]
1317 1408 I wm_activity_launch_time: [0,216957026,com.tencent.mm/.app.WeChatSplashActivity,2108]

4. Modify the scheme

1. Blocking in the process of loading DEX, such as OpenDexFilesFromOat in systrace, if / data / user / 0 / com. Is not loaded tencent. mm/tinker/patch-66e50d2a/dex/tinker_ classN. apk,
If you modify in art
OatFileManager::OpenDexFilesFromOat or the following ArtDexFileLoader::OpenZip/ArtDexFileLoader::OpenAllDexFilesFromZip can block the opening process

It is recognized that tinker skips directly. The following is the skip in OpenAllDexFilesFromZip (this scheme is only valid before Android S. in the normal android version of Android S, art has been given to mainline, and art in gms is used)

//art/libdexfile/dex/art_dex_file_loader.cc
bool ArtDexFileLoader::OpenAllDexFilesFromZip(
    const ZipArchive& zip_archive,
    const std::string& location,
    bool verify,
    bool verify_checksum,
    std::string* error_msg,
    std::vector<std::unique_ptr<const DexFile>>* dex_files) const {
  ScopedTrace trace("Dex file open from Zip " + std::string(location));
//...
  //Identify whether the location contains tinker
  if (hasTinker) {//Skip if included
    return false;
  }
//...
}

2. So the art modification scheme of Android S is invalid. What should we do?
It's the same sentence: investigate first, and then write

5. When did open DEX trigger? Where does the incoming location come from?

1. Go up from OatFileManager::OpenDexFilesFromOat
//

art/runtime/native/dalvik_system_DexFile.cc

static jobject DexFile_openDexFileNative(JNIEnv* env,
                                         jclass,
                                         jstring javaSourceName,
                                         jstring javaOutputName ATTRIBUTE_UNUSED,
                                         jint flags ATTRIBUTE_UNUSED,
                                         jobject class_loader,
                                         jobjectArray dex_elements) {
  ScopedUtfChars sourceName(env, javaSourceName);
  if (sourceName.c_str() == nullptr) {
    return nullptr;
  }

  std::vector<std::string> error_msgs;
  const OatFile* oat_file = nullptr;
  std::vector<std::unique_ptr<const DexFile>> dex_files =
      Runtime::Current()->GetOatFileManager().OpenDexFilesFromOat(sourceName.c_str(),
                                                                  class_loader,
                                                                  dex_elements,
                                                                  /*out*/ &oat_file,
                                                                  /*out*/ &error_msgs);
  return CreateCookieFromOatFileManagerResult(env, dex_files, oat_file, error_msgs);
}

//This is a jni way to get over it
static JNINativeMethod gMethods[] = {
  NATIVE_METHOD(DexFile, openDexFileNative,
                "(Ljava/lang/String;"
                "Ljava/lang/String;"
                "I"
                "Ljava/lang/ClassLoader;"
                "[Ldalvik/system/DexPathList$Element;"
                ")Ljava/lang/Object;"),

2. The upper level directory here is libcore / Dalvik / SRC / main / Java / Dalvik / system / dexfile java,
The dex file is opened when the DexFile object is created

    private static Object openDexFile(String sourceName, String outputName, int flags,
            ClassLoader loader, DexPathList.Element[] elements) throws IOException {
        // Use absolute paths to enable the use of relative paths when testing on host.
        return openDexFileNative(new File(sourceName).getAbsolutePath(),
                                 (outputName == null)
                                     ? null
                                     : new File(outputName).getAbsolutePath(),
                                 flags,
                                 loader,
                                 elements);
    }

    private DexFile(String sourceName, String outputName, int flags, ClassLoader loader,
            DexPathList.Element[] elements) throws IOException {
        //...

        mCookie = openDexFile(sourceName, outputName, flags, loader, elements);
        mInternalCookie = mCookie;
        mFileName = sourceName;
        //System.out.println("DEX FILE cookie is " + mCookie + " sourceName=" + sourceName + " outputName=" + outputName);
    }
	

3. Search for new DexFile, only dexpathlist java,DexFile.java new DexFile object

libcore/dalvik$ grep -rn "new DexFile" .
./src/main/java/dalvik/system/DexPathList.java:268: DexFile dex = new DexFile(dexFiles, definingContext, null_elements);
./src/main/java/dalvik/system/DexPathList.java:347: DexFile dex = new DexFile(new ByteBuffer[] { buf }, /* classLoader */ null,
./src/main/java/dalvik/system/DexPathList.java:442: return new DexFile(file, loader, elements);
./src/main/java/dalvik/system/DexFile.java:216: return new DexFile(sourcePathName, outputPathName, flags, loader, elements);

   DexPathList(ClassLoader definingContext, String dexPath,
            String librarySearchPath, File optimizedDirectory, boolean isTrusted) {
        //...
        // save dexPath for BaseDexClassLoader
        this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
                                           suppressedExceptions, definingContext, isTrusted);
        //...
    }

    private static Element[] makeDexElements(List<File> files, File optimizedDirectory,
            List<IOException> suppressedExceptions, ClassLoader loader, boolean isTrusted) {
      //...
      dex = loadDexFile(file, optimizedDirectory, loader, elements);
      //...
    }
    private static DexFile loadDexFile(File file, File optimizedDirectory, ClassLoader loader,
                                       Element[] elements)
            throws IOException {
        if (optimizedDirectory == null) {//This is null during initialization
            return new DexFile(file, loader, elements);
        } else {
            String optimizedPath = optimizedPathFor(file, optimizedDirectory);
            return DexFile.loadDex(file.getPath(), optimizedPath, 0, loader, elements);//Open wechat * * * apk is going here
        }
    }					   

4. Find the associated process up

Find loadedapk directly here Java, this is where App loads apk, and the process goes from here to point 3

//frameworks/base/core/java/android/app/LoadedApk.java
    public ClassLoader getClassLoader() {
        synchronized (mLock) {
            if (mClassLoader == null) {
                createOrUpdateClassLoaderLocked(null /*addedPaths*/);
            }
            return mClassLoader;
        }
    }

   private void createOrUpdateClassLoaderLocked(List<String> addedPaths) {
        //..
        //mApplicationInfo.sourceDir is / data / APP / * * * / com tencent. mm***/base. apk
        makePaths(mActivityThread, isBundledApp, mApplicationInfo, zipPaths, libPaths);
        //...
        final String zip = (zipPaths.size() == 1) ? zipPaths.get(0) :
                TextUtils.join(File.pathSeparator, zipPaths);//zip is / data / APP / * * * / com tencent. mm***/base. apk
        //...
        if (mDefaultClassLoader == null) {
            //...
            //Create mdefaultcloader
            mDefaultClassLoader = ApplicationLoaders.getDefault().getClassLoaderWithSharedLibraries(
                    zip, mApplicationInfo.targetSdkVersion, isBundledApp, librarySearchPath,
                    libraryPermittedPath, mBaseClassLoader,
                    mApplicationInfo.classLoaderName, sharedLibraries, nativeSharedLibraries);
            //Mappcomponentfactory of wechat = androidx core. app. CoreComponentFactory
            mAppComponentFactory = createAppFactory(mApplicationInfo, mDefaultClassLoader);
            //...
        }
        //...
        if (mClassLoader == null) {
            //Creating an mClassLoader from mAppComponentFactory
            mClassLoader = mAppComponentFactory.instantiateClassLoader(mDefaultClassLoader,
                    new ApplicationInfo(mApplicationInfo));
        }
    }

Continue with application loaders getDefault(). getClassLoaderWithSharedLibraries

//frameworks/base/core/java/android/app/ApplicationLoaders.java
    ClassLoader getClassLoaderWithSharedLibraries(
            String zip, int targetSdkVersion, boolean isBundled,
            String librarySearchPath, String libraryPermittedPath,
            ClassLoader parent, String classLoaderName,
            List<ClassLoader> sharedLibraries, List<String> nativeSharedLibraries) {
        // For normal usage the cache key used is the same as the zip path.
        return getClassLoader(zip, targetSdkVersion, isBundled, librarySearchPath,
                              libraryPermittedPath, parent, zip, classLoaderName, sharedLibraries,
                              nativeSharedLibraries);
    }

    private ClassLoader getClassLoader(String zip, int targetSdkVersion, boolean isBundled,
                                       String librarySearchPath, String libraryPermittedPath,
                                       ClassLoader parent, String cacheKey,
                                       String classLoaderName, List<ClassLoader> sharedLibraries,
                                       List<String> nativeSharedLibraries) {
        //...
                ClassLoader classloader = ClassLoaderFactory.createClassLoader(
                        zip,  librarySearchPath, libraryPermittedPath, parent,
                        targetSdkVersion, isBundled, classLoaderName, sharedLibraries,
                        nativeSharedLibraries);//Note the passed in parameter zip
        //...
    }

//frameworks/base/core/java/com/android/internal/os/ClassLoaderFactory.java	
    public static ClassLoader createClassLoader(String dexPath,
            String librarySearchPath, String libraryPermittedPath, ClassLoader parent,
            int targetSdkVersion, boolean isNamespaceShared, String classLoaderName,
            List<ClassLoader> sharedLibraries, List<String> nativeSharedLibraries) {

        final ClassLoader classLoader = createClassLoader(dexPath, librarySearchPath, parent,
                classLoaderName, sharedLibraries);
        //...
    }

    public static ClassLoader createClassLoader(String dexPath,
            String librarySearchPath, ClassLoader parent, String classloaderName,
            List<ClassLoader> sharedLibraries) {
        ClassLoader[] arrayOfSharedLibraries = (sharedLibraries == null)
                ? null
                : sharedLibraries.toArray(new ClassLoader[sharedLibraries.size()]);

        ClassLoader result = null;

		//Generally due to mapplicationinfo Classloadername is not set, so PathClassLoader is created by default
        if (isPathClassLoaderName(classloaderName)) {
            return new PathClassLoader(dexPath, librarySearchPath, parent, arrayOfSharedLibraries);
        } else if (isDelegateLastClassLoaderName(classloaderName)) {//If classloaderName = "dalvik.system.DelegateLastClassLoader" is set, enter here
            return new DelegateLastClassLoader(dexPath, librarySearchPath, parent, arrayOfSharedLibraries);
        }

        throw new AssertionError("Invalid classLoaderName: " + classloaderName);
    }

5. We go to the other code file directory libcore/dalvik /, and continue to view libcore/dalvik / SRC / main / Java / Dalvik / system / PathClassLoader java

//libcore/dalvik/src/main/java/dalvik/system/PathClassLoader.java
    public PathClassLoader(
            @NonNull String dexPath, @Nullable String librarySearchPath, @Nullable ClassLoader parent,
            @Nullable ClassLoader[] sharedLibraryLoaders) {
        super(dexPath, librarySearchPath, parent, sharedLibraryLoaders);
    }

//libcore/dalvik/src/main/java/dalvik/system/BaseDexClassLoader.java
    public BaseDexClassLoader(String dexPath,
            String librarySearchPath, ClassLoader parent, ClassLoader[] libraries) {
        this(dexPath, librarySearchPath, parent, libraries, false);
    }

    public BaseDexClassLoader(String dexPath,
            String librarySearchPath, ClassLoader parent, ClassLoader[] sharedLibraryLoaders,
            boolean isTrusted) {
        super(parent);
        // Setup shared libraries before creating the path list. ART relies on the class loader
        // hierarchy being finalized before loading dex files.
        this.sharedLibraryLoaders = sharedLibraryLoaders == null
                ? null
                : Arrays.copyOf(sharedLibraryLoaders, sharedLibraryLoaders.length);
        //Notice that you start building the new DexPathList here
        //dexPath is / data / APP / * * * / com tencent. mm***/base. apk
        this.pathList = new DexPathList(this, dexPath, librarySearchPath, null, isTrusted);

        // Run background verification after having set 'pathList'.
        this.pathList.maybeRunBackgroundVerification(this);

        reportClassLoaderChain();
    }

//libcore/dalvik/src/main/java/dalvik/system/DexPathList.java
   //Here we return to point 3 of this chapter
   DexPathList(ClassLoader definingContext, String dexPath,
            String librarySearchPath, File optimizedDirectory, boolean isTrusted) {
        //...
        // save dexPath for BaseDexClassLoader
        this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
                                           suppressedExceptions, definingContext, isTrusted);
        //...
    }

6. You can basically sort it out here

  1. When did Open Dex trigger? = > Triggered when creating and initializing ClassLoader
  2. Where does the incoming location come from? = > This is DexPathList, corresponding to zip file, such as * * * apk

6. Take a look at the ClassLoader with and without tinker

  1. Set wechat breakpoint location
//frameworks/base/core/java/android/app/LoadedApk.java
    public ClassLoader getClassLoader() {
        synchronized (mLock) {
            if (mClassLoader == null) {//You can set the breakpoint location here
                createOrUpdateClassLoaderLocked(null /*addedPaths*/);
            }
            return mClassLoader;
        }
    }
  1. ClassLoader without tinker is installed by default
    The open dex file is / data / APP / * * * / com tencent. mm***/base. apk, installed by wechat by default

mClassLoader = {PathClassLoader@33282} "dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/~~IhVsaxjwrzj_c6zzMznrww==/com.tencent.mm-hX9AatQT4nETNnGgbug_GA==/base.apk"],nativeLibraryDirectories=[/data/app/~~IhVsaxjwrzj_c6zzMznrww==/com.tencent.mm-hX9AatQT4nETNnGgbug_GA==/lib/arm, /data/app/~~IhVsaxjwrzj_c6zzMznrww==/com.tencent.mm-hX9AatQT4nETNnGgbug_GA==/base.apk!/lib/armeabi-v7a, /system/lib, /system/system_ext/lib]]]"

  1. ClassLoader with tinker, but the default PathClassLoader is also created, which is equivalent to creating two classloaders (note that two apk s need to be loaded here, which will take longer)
    You can see that the opened DEX file is / data / user / 0 / com tencent. mm/tinker/patch-***/dex/tinker_ classN. Apk, this is the tinker file in wechat hot update, not the file we installed at the beginning

mClassLoader = {DelegateLastClassLoader@15795} "dalvik.system.DelegateLastClassLoader[DexPathList[[zip file "/data/user/0/com.tencent.mm/tinker/patch-66e50d2a/dex/tinker_classN.apk"],nativeLibraryDirectories=[/data/user/0/com.tencent.mm/tinker/patch-66e50d2a/lib/lib/armeabi-v7a, /data/app/~~Gcugv7eeiy5Og6ory1jVrg==/com.tencent.mm-OKmJgE3WNLcG_AaLmxxzw==/lib/arm, /data/app/~~Gldgv7eeiy5Og6ory1jVrg==/com.tencent.mm-OKmJgE3WNLcG_AaLmxxzw==/base.apk!/lib/armeabi-v7a, /system/lib, /system/system_ext/lib]]]"
parent = {PathClassLoader@125091} "dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/~~IhVsaxjwrzj_c6zzMznrww==/com.tencent.mm-hX9AatQT4nETNnGgbug_GA==/base.apk"],nativeLibraryDirectories=[/data/app/~~IhVsaxjwrzj_c6zzMznrww==/com.tencent.mm-hX9AatQT4nETNnGgbug_GA==/lib/arm, /data/app/~~Gldgv7eeiy5Og6ory1jVrg==/com.tencent.mm-hX9AatQT4nETNnGgbug_GA==/base.apk!/lib/armeabi-v7a, /system/lib, /system/system_ext/lib]]]"

7. Wechat internal tinker loading process

A simple idea is to restore the DelegateLastClassLoader to PathClassLoader when setting the DelegateLastClassLoader on wechat, but if you only modify loadedapk java,
You will find that wechat will crash, that is, the modification is not perfect, and other initialization contents have not been restored

    public ClassLoader getClassLoader() {
        synchronized (mLock) {
            if (mClassLoader == null) {
                createOrUpdateClassLoaderLocked(null /*addedPaths*/);
            }
            // yunhen test start
			if (mResDir != null && mResDir.contains("tinker")) {
				mClassLoader = mDefaultClassLoader;
				if(mSourceDir != null) {
					mResDir = mSourceDir;
				}
			}
            // yunhen test end
            return mClassLoader;
        }
    }

Continue to add logs in all places of the system where mClassLoader =, and find that there is no running system code at all,
It can only be the function injection and reflection called by the app itself to realize the setting function, so the problem is more complex than calling system methods.

1. There are many ways to analyze third-party applications, such as decompilation tools
jadx-gui-***.exe,jd-gui. Exe = > you can get java code directly. It's easy to see, but the disadvantage is part of the code
java -jar apktool_***.jar d + path = > this is decompiled into class and converted into smali. It's not good-looking, but it won't be missed

2. Look at the process after decompilation

The source code starts from handleBindApplication to application After Java's attach, enter the wechat overload process
handleBindApplication(ActivityThread.java)->makeApplication(LoadedApk.java)->newApplication(Instrumentation.java)->attach(Application.java)

Let's post these processes
Attach (application. Java) - > attachbasecontext / onbasecontextattached / loadtinker (tinkerapplication. Java) - > tryload / trylodpatchfilesinternal (judge whether there are various files of tinker, patch.info(getPatchInfoFile) (tinkerloader. Java) - > loadtinkerjars (tinkerdexloader. Java) - > installdexes (systemclassloaderadder. Java) - > inject (NewClassLoaderInjector.java) ->createNewClassLoader/doInject(NewClassLoaderInjector.java) -> Thread setContextClassLoader/ContextWrapper mBase mClassLoader/ContextImpl mPackageInfo mClassLoader

//frameworks/base/core/java/android/app/Application.java
    /* package */ final void attach(Context context) {
        attachBaseContext(context);
        mLoadedApk = ContextImpl.getImpl(context).mPackageInfo;
    }

// Application.java
package com.tencent.mm.app;

import com.tencent.tinker.loader.app.TinkerApplication;

public class Application extends TinkerApplication {
    private static final String TINKER_LOADER_ENTRY_CLASSNAME = "com.tencent.tinker.loader.TinkerLoader";
    private static final String WECHAT_APPLICATION_LIKE_CLASSNAME = "com.tencent.mm.app.MMApplicationLike";

    public Application() {
        //Tinkerloader of wechat (tinker_loader_entry_classname = com.tencent.tinker.loader.tinkerloader), and 7 represents tinkerFlags
        super(7, WECHAT_APPLICATION_LIKE_CLASSNAME, TINKER_LOADER_ENTRY_CLASSNAME, true, true);
    }
}
	
//TinkerApplication.java
package com.tencent.tinker.loader.app;

    protected TinkerApplication(int i, String str, String str2, boolean z, boolean z2) {
        this.mCurrentClassLoader = null;
        this.mInlineFence = null;
        synchronized (SELF_HOLDER) {
            SELF_HOLDER[0] = this;
        }
        this.tinkerFlags = i;
        this.delegateClassName = str;
        this.loaderClassName = str2;
        this.tinkerLoadVerifyFlag = z;
        this.useDelegateLastClassLoader = z2;
    }

    public void attachBaseContext(Context context) {
        super.attachBaseContext(context);
        long elapsedRealtime = SystemClock.elapsedRealtime();
        long currentTimeMillis = System.currentTimeMillis();
        Thread.setDefaultUncaughtExceptionHandler(new TinkerUncaughtHandler(this));
        onBaseContextAttached(context, elapsedRealtime, currentTimeMillis);//Here is the next step of tinker
    }

    public void onBaseContextAttached(Context context, long j, long j2) {
        try {
            loadTinker();//Load wechat tinker
            this.mCurrentClassLoader = context.getClassLoader();//Here, after loading, the ClassLoader becomes the ClassLoader of tinker
            this.mInlineFence = createInlineFence(this, this.tinkerFlags, this.delegateClassName, this.tinkerLoadVerifyFlag, j, j2, this.tinkerResultIntent);
            TinkerInlineFenceAction.callOnBaseContextAttached(this.mInlineFence, context);
            if (this.useSafeMode) {
                ShareTinkerInternals.setSafeModeCount(this, 0);
            }
        } catch (TinkerRuntimeException e2) {
            throw e2;
        } catch (Throwable th) {
            throw new TinkerRuntimeException(th.getMessage(), th);
        }
    }

    private static final String TINKER_LOADER_METHOD = "tryLoad";
    private void loadTinker() {
        try {
            Class<?> cls = Class.forName(this.loaderClassName, false, TinkerApplication.class.getClassLoader());
            //The method called is the tryLoad method of TinkerLoader (the reflection call should be done only when tinker is a public component)
            this.tinkerResultIntent = (Intent) cls.getMethod(TINKER_LOADER_METHOD, TinkerApplication.class).invoke(cls.getConstructor(new Class[0]).newInstance(new Object[0]), this);
        } catch (Throwable th) {
            this.tinkerResultIntent = new Intent();
            ShareIntentUtil.setIntentReturnCode(this.tinkerResultIntent, -20);
            this.tinkerResultIntent.putExtra("intent_patch_exception", th);
        }
    }

//ShareTinkerInternals.java
    public static boolean isTinkerEnabled(int i) {
        return i != 0;
    }

//TinkerLoader.java
    public Intent tryLoad(TinkerApplication tinkerApplication) {
        ShareTinkerLog.d(TAG, "tryLoad test test", new Object[0]);
        Intent intent = new Intent();
        long elapsedRealtime = SystemClock.elapsedRealtime();
        tryLoadPatchFilesInternal(tinkerApplication, intent);
        ShareIntentUtil.setIntentPatchCostTime(intent, SystemClock.elapsedRealtime() - elapsedRealtime);
        return intent;
    }

    private void tryLoadPatchFilesInternal(com.tencent.tinker.loader.app.TinkerApplication r22, android.content.Intent r23) {
        /*
        // Method dump skipped, instructions count: 1301 Decompilation fails here. Go to apktool to get tinkerloader SmalI can see the virtual machine code inside
        */
        throw new UnsupportedOperationException("Method not decompiled: com.tencent.tinker.loader.TinkerLoader.tryLoadPatchFilesInternal(com.tencent.tinker.loader.app.TinkerApplication, android.content.Intent):void");

      .locals 21

                .prologue
                .line 64
        invoke-virtual/range {p1 .. p1}, Lcom/tencent/tinker/loader/app/TinkerApplication;->getTinkerFlags()I    //Get tinkerFlags

        move-result v6

                .line 66
        invoke-static {v6}, Lcom/tencent/tinker/loader/shareutil/ShareTinkerInternals;->isTinkerEnabled(I)Z //Call isTinkerEnabled to determine whether tinker needs to be supported

        move-result v2

        if-nez v2, :cond_0   //nez(not equal zero). If tinkerFlags is not 0, v2=true enters cond_0, otherwise v2=false, enter the following logic,

                .line 67
    const-string/jumbo v2, "Tinker.TinkerLoader"

    const-string/jumbo v3, "tryLoadPatchFiles: tinker is disable, just return" //The description here is also very clear. Tinker is not supported, and the tinker process is not followed when returning

    const/4 v4, 0x0

        new-array v4, v4, [Ljava/lang/Object;

        invoke-static {v2, v3, v4}, Lcom/tencent/tinker/loader/shareutil/ShareTinkerLog;->w(Ljava/lang/String;Ljava/lang/String;[Ljava/lang/Object;)V

                .line 68
    const/4 v2, -0x1

        move-object/from16 v0, p2

        invoke-static {v0, v2}, Lcom/tencent/tinker/loader/shareutil/ShareIntentUtil;->setIntentReturnCode(Landroid/content/Intent;I)V

                .line 392
    :goto_0
        return-void
//... Let's skip the process we don't pay attention to. This is just to tell you the method and record the analysis process, not to analyze the wechat code
//Later, judge whether there is a tinker directory in the process of Tinker
//The logic of Tinker directory is generally in applicationinfo The tinker directory of dataDir, such as / data / user / 0 / COM of Android S tencent. Mm / tinker, wechat has made special efforts for oppo and put it on wc_tinker_dir
//If the tinker directory does not exist, the tinker process will not be followed, and it will be returned directly here
    public static final String PATCH_DIRECTORY_NAME = "tinker";
    public static final String PATCH_DIRECTORY_NAME_SPEC = "wc_tinker_dir";
    public static File getPatchDirectory(Context context) {
        ApplicationInfo applicationInfo = context.getApplicationInfo();
        if (applicationInfo == null) {
            return null;
        }
        return new File(applicationInfo.dataDir, (!"oppo".equalsIgnoreCase(Build.MANUFACTURER) || Build.VERSION.SDK_INT != 22) ? ShareConstants.PATCH_DIRECTORY_NAME : ShareConstants.PATCH_DIRECTORY_NAME_SPEC);
    }
//...
//Here is judgment / data / user / 0 / com tencent. mm/tinker/patch. Whether info exists. If it does not exist, the tinker process will not be followed
    :cond_3
        invoke-static {v10}, Lcom/tencent/tinker/loader/shareutil/SharePatchFileUtil;->getPatchInfoFile(Ljava/lang/String;)Ljava/io/File;
/*
    public static File getPatchInfoFile(String str) {
        return new File(str + "/patch.info");
    }
*/
        move-result-object v11

                .line 97
        invoke-virtual {v11}, Ljava/io/File;->exists()Z

        move-result v2

        if-nez v2, :cond_4

                .line 98
    const-string/jumbo v2, "Tinker.TinkerLoader"

        new-instance v3, Ljava/lang/StringBuilder;

    const-string/jumbo v4, "tryLoadPatchFiles:patch info not exist:" //patch.info file does not exist
//... 
    goto/16 :goto_0

                .line 104
//...  info.lock is a PatchInfo synchronization lock to prevent multi-threaded logic exceptions during reading

//...  The purpose of reading patchInfo information, such as identification, is to select the contents of the patch-66e50d2a directory

//...  Others such as intent_is_protected_app settings, version identification (returned if an exception is identified, and the old Tinker (if there are multiple tinkers) will be deleted if successful), check whether the tinker ota file is correct, whether it needs to be regenerated, and handle a bunch of exceptions
//...  Whether isTinkerEnabledForResource needs to load / data / user / 0 / com tencent. mm/tinker/patch***/res/resources. apk 
// (loadTinkerResources not only sets resources.apk, but also sets mResDir/publicSourceDir = "/data/user/0/com.tencent.mm/tinker/patch-***/res/resources.apk"),addAssetPath/mAssets and other resources
/* Various contents to be loaded are i=7 by default, that is, isTinkerEnabledForDex, istinkerenabledfornatelib and isTinkerEnabledForResource return true
    public static boolean isTinkerEnabledForDex(int i) {
        return (i & 1) != 0;
    }

    public static boolean isTinkerEnabledForNativeLib(int i) {
        return (i & 2) != 0;
    }

    public static boolean isTinkerEnabledForResource(int i) {
        return (i & 4) != 0;
    }

    public static boolean isTinkerEnabledForArkHot(int i) {
        return (i & 8) != 0;
    }
*/
//...
   :cond_1c
        if-nez v17, :cond_1f  //v17 is an adaptation of isarkhotrunning to huawei, which is generally false

        if-eqz v16, :cond_1f  //v16 is isTinkerEnabledForDex, and the result is not 0

        move-object/from16 v2, p1

        move-object/from16 v5, p2

                .line 325
        //This is the next step of calling loadTinkerJars to load tinker
        invoke-static/range {v2 .. v7}, Lcom/tencent/tinker/loader/TinkerDexLoader;->loadTinkerJars(Lcom/tencent/tinker/loader/app/TinkerApplication;Ljava/lang/String;Ljava/lang/String;Landroid/content/Intent;ZZ)Z

        move-result v4

                .line 327
        if-eqz v6, :cond_29

//TinkerDexLoader.java
    public static boolean loadTinkerJars(TinkerApplication tinkerApplication, String str, String str2, Intent intent, boolean z, boolean z2) {
        if (!LOAD_DEX_LIST.isEmpty() || !classNDexInfo.isEmpty()) {
            ClassLoader classLoader = TinkerDexLoader.class.getClassLoader();
            if (classLoader != null) {
                //...
                if (isVmArt && !classNDexInfo.isEmpty()) {android S Up, isVmArt = true
                    //file2 is data / user / 0 / com tencent. mm/tinker/patch-***/dex/tinker_ classN. apk
                    File file2 = new File(str3 + ShareConstants.CLASS_N_APK_NAME);//CLASS_N_APK_NAME = "tinker_classN.apk";
                    //...
                    arrayList.add(file2);
                }
                //...
                if (z) {
                        //...
                        //Wechat can also manually call dex2oat. At present, it doesn't run, tinker_classN.odex should be downloaded
                        TinkerDexOptimizer.optimizeAll(tinkerApplication, arrayList, file4, true, tinkerApplication.isUseDelegateLastClassLoader(), currentInstructionSet, new TinkerDexOptimizer.ResultCallback() {
                        //...
                }
                try {
                    //installDexes is the next step of tinker process. arrayList contains tinker_classN.apk
                    SystemClassLoaderAdder.installDexes(tinkerApplication, classLoader, file3, arrayList, z2, tinkerApplication.isUseDelegateLastClassLoader());
                    return true;
                //...
    }

//SystemClassLoaderAdder.java
    public static void installDexes(Application application, ClassLoader classLoader, File file, List<File> list, boolean z, boolean z2) {
        ShareTinkerLog.i(TAG, "installDexes dexOptDir: " + file.getAbsolutePath() + ", dex size:" + list.size(), new Object[0]);
        if (!list.isEmpty()) {
            List<File> createSortedAdditionalPathEntries = createSortedAdditionalPathEntries(list);
            if (Build.VERSION.SDK_INT < 24 || z) {
                injectDexesInternal(classLoader, createSortedAdditionalPathEntries, file);
            } else {
                //Android S goes here. Injection is actually reflection calling system code. Createporteddadditionalpathentries contains tinker_classN.apk
                classLoader = NewClassLoaderInjector.inject(application, classLoader, file, z2, createSortedAdditionalPathEntries);
            }
            sPatchDexCount = createSortedAdditionalPathEntries.size();
            ShareTinkerLog.i(TAG, "after loaded classloader: " + classLoader + ", dex size:" + sPatchDexCount, new Object[0]);
            if (!checkDexInstall(classLoader)) {
                uninstallPatchDex(classLoader);
                throw new TinkerRuntimeException(ShareConstants.CHECK_DEX_INSTALL_FAIL);
            }
        }
    }

//NewClassLoaderInjector.java

    public static ClassLoader inject(Application application, ClassLoader classLoader, File file, boolean z, List<File> list) {
        String[] strArr = new String[list.size()];
        for (int i = 0; i < strArr.length; i++) {
            strArr[i] = list.get(i).getAbsolutePath();//strArr contains Tinker in this process_ classN. apk
        }
        //After a lot of searching for him, he finally came. createNewClassLoader created ClassLoader
        ClassLoader createNewClassLoader = createNewClassLoader(classLoader, file, z, true, strArr);
        //Set the new class loader reflection to the system
        doInject(application, createNewClassLoader);
        return createNewClassLoader;
    }

  private static ClassLoader createNewClassLoader(ClassLoader classLoader, File file, boolean z, boolean z2, String... strArr) {
        List<File> list;
        ClassLoader tinkerClassLoader;
        Object obj = findField(Class.forName("dalvik.system.BaseDexClassLoader", false, classLoader), "pathList").get(classLoader);
        StringBuilder sb = new StringBuilder();
        if (strArr != null && strArr.length > 0) {
            for (int i = 0; i < strArr.length; i++) {
                if (i > 0) {
                    sb.append(File.pathSeparator);
                }
                sb.append(strArr[i]);
            }
        }
        String sb2 = sb.toString();//Tinker is sb2 included in this process_ classN. apk
        Field findField = findField(obj.getClass(), "nativeLibraryDirectories");
        if (findField.getType().isArray()) {
            list = Arrays.asList((File[]) findField.get(obj));
        } else {
            list = (List) findField.get(obj);
        }
        StringBuilder sb3 = new StringBuilder();
        boolean z3 = true;
        for (File file2 : list) {
            if (file2 != null) {
                if (z3) {
                    z3 = false;
                } else {
                    sb3.append(File.pathSeparator);
                }
                sb3.append(file2.getAbsolutePath());
            }
        }
        String sb4 = sb3.toString();
        if (!z || Build.VERSION.SDK_INT < 27) {
            tinkerClassLoader = new TinkerClassLoader(sb2, file, sb4, classLoader);
        } else {
            //Currently, the wechat in Android S uses DelegateLastClassLoader//DelegateLastClassLoader(String dexPath, String librarySearchPath, ClassLoader parent)
            //sb2 is a dexPath containing tinker_classN.apk
            tinkerClassLoader = new DelegateLastClassLoader(sb2, sb4, ClassLoader.getSystemClassLoader());
            Field declaredField = ClassLoader.class.getDeclaredField("parent");
            declaredField.setAccessible(true);
            //Set the parent of DelegateLastClassLoader(DexPathList is / data/user / * *) to the original PathClassLoader(DexPathList is / data/app * *)
            declaredField.set(tinkerClassLoader, classLoader);
        }
        if (z2 && Build.VERSION.SDK_INT < 26) {
            findField(obj.getClass(), "definingContext").set(obj, tinkerClassLoader);
        }
        return tinkerClassLoader;
    }

    private static void doInject(Application application, ClassLoader classLoader) {
        Thread.currentThread().setContextClassLoader(classLoader);
        Context context = (Context) findField(application.getClass(), "mBase").get(application);//ContextWrapper mBase
        try {
            findField(context.getClass(), "mClassLoader").set(context, classLoader);//Assign ClassLoader of tinker to ContextImpl mClassLoader
        } catch (Throwable th) {
        }
        Object obj = findField(context.getClass(), "mPackageInfo").get(context);//ContextImpl mPackageInfo
        findField(obj.getClass(), "mClassLoader").set(obj, classLoader);//Assign ClassLoader of tinker to contextimpl mpacketinfo
        if (Build.VERSION.SDK_INT < 27) { //Android S SDK_ Int > 27 don't run here
            Resources resources = application.getResources();//ContextImpl public Resources getResources() {return mResources;}
            try {
                findField(resources.getClass(), "mClassLoader").set(resources, classLoader);//Resources mClassLoader
                Object obj2 = findField(resources.getClass(), "mDrawableInflater").get(resources);//DrawableInflater mDrawableInflater
                if (obj2 != null) {
                    findField(obj2.getClass(), "mClassLoader").set(obj2, classLoader);//DrawableInflater private final ClassLoader mClassLoader;
                }
            } catch (Throwable th2) {
            }
        }
    }


8. Optimize the scheme that causes slow startup of tinker

1. Referring to Google play, tinker is not allowed. In fact, the function can still run normally, so it is still necessary to restrict the use of tinker.

2. Redo the dex optimization for the mobile phone. For example, when adding known secondary dex files, do dexOptSecondaryDexPathLI,
The location of the plug-in can be placed in notifyDexLoadInternal. However, this will involve a series of problems. There are many scenarios that may lead to patch failure. (if there are odex files in the latest version of wechat, the improvement is not great)

Dexopt state:
  [com.tencent.mm]
    path: /data/app/~~Gldgv7eeiy5Og6ory1jVrg==/com.tencent.mm-OKmJgE3WNLcG_AbdLmxxzw==/base.apk
      arm: [status=verify] [reason=install]
      known secondary dex files:
        /data/user/0/com.tencent.mm/app_xwalk_3164/apk/base.apk
          class loader context: PCL[];PCL[]
        /data/user/0/com.tencent.mm/tinker/patch-66e50d2a/dex/tinker_classN.apk
          class loader context: DLC[];PCL[]
        /data/user/0/com.tencent.mm/app_xwalkplugin/XFilesPPTReader_338/extracted/pptreader.apk

3. In fact, there are many ways to limit Tinker. Through the above process analysis, you can block the tinker process in any piece of code.

  1. Before Android S, such as Android o, P, Q, R, etc., you can directly skip in art
  2. After Android S main line art, you can consider some judgments of tinker above, mainly in tryLoadPatchFilesInternal
    For example, Tinker is not allowed to download (com.tencent.mm: the patch process starts. com.tencent.mm/com.tencent.tinker.lib.service.TinkerPatchForeService} service is downloaded), or deleted after downloading, or Tinker's identification patch.info is blocked
  3. You can restore wechat immediately after setting tinker. For example, the attempt in Chapter 7 continues to be improved according to the decompilation process (not only LoadedApk.java, but also Thread and ContextImpl need to be modified, including resource related ones, and even the internal logic of wechat need to be considered)

From this point of view, it seems that it is easier to 2 see each other.

Finally, I will provide an idea. In fact, I will patch Info can be deleted. The deleted location can be placed before handleBindApplication(ActivityThread.java) calls makeApplication(LoadedApk.java)
As for other methods, we will continue to discuss here. You are interested in trying them yourself

   private void handleBindApplication(AppBindData data) {
        // ...
        try {
            File tinker = new File("/data/user/0/com.tencent.mm/tinker/patch.info");
            if (tinker.exists()) {
                tinker.delete();
            }
        } catch (Exception e) {
            Slog.w(TAG, "tinker.delete Exception e = " + e);
        }
        // ...
        try {
            // If the app is being launched for full backup or restore, bring it up in
            // a restricted environment with the base application class.
            app = data.info.makeApplication(data.restrictedBackupMode, null);//Just before you put it here

Topics: wechat