/*
* Tencent is pleased to support the open source community by making Tinker available.
*
* Copyright (C) 2016 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the BSD 3-Clause License (the "License"); you may not use this file except in
* compliance with the License. You may obtain a copy of the License at
*
* https://opensource.org/licenses/BSD-3-Clause
*
* 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.annotation.TargetApi;
import android.app.Application;
import android.content.Context;
import android.os.Build;
import android.text.TextUtils;
import android.util.Log;
import com.tencent.tinker.loader.shareutil.ShareReflectUtil;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import dalvik.system.DexFile;
import dalvik.system.PathClassLoader;
/**
* Created by zhangshaowen on 16/7/24.
*/
@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
class AndroidNClassLoader extends PathClassLoader {
private static final String TAG = "Tinker.NClassLoader";
private static final String CHECK_CLASSLOADER_CLASS = "com.tencent.tinker.loader.TinkerTestAndroidNClassLoader";
private static ArrayList<DexFile> oldDexFiles = new ArrayList<>();
private final PathClassLoader originClassLoader;
private String applicationClassName;
private AndroidNClassLoader(String dexPath, PathClassLoader parent, Application application) {
super(dexPath, parent.getParent());
originClassLoader = parent;
String name = application.getClass().getName();
if (name != null && !name.equals("android.app.Application")) {
applicationClassName = name;
}
}
private static AndroidNClassLoader createAndroidNClassLoader(PathClassLoader original, Application application) throws Exception {
//let all element ""
AndroidNClassLoader androidNClassLoader = new AndroidNClassLoader("", original, application);
Field originPathList = ShareReflectUtil.findField(original, "pathList");
Object originPathListObject = originPathList.get(original);
//should reflect definingContext also
Field originClassloader = ShareReflectUtil.findField(originPathListObject, "definingContext");
originClassloader.set(originPathListObject, androidNClassLoader);
//copy pathList
Field pathListField = ShareReflectUtil.findField(androidNClassLoader, "pathList");
//just use PathClassloader's pathList
pathListField.set(androidNClassLoader, originPathListObject);
//we must recreate dexFile due to dexCache
List<File> additionalClassPathEntries = new ArrayList<>();
Field dexElement = ShareReflectUtil.findField(originPathListObject, "dexElements");
Object[] originDexElements = (Object[]) dexElement.get(originPathListObject);
for (Object element : originDexElements) {
DexFile dexFile = (DexFile) ShareReflectUtil.findField(element, "dexFile").get(element);
if (dexFile == null) {
continue;
}
additionalClassPathEntries.add(new File(dexFile.getName()));
//protect for java.lang.AssertionError: Failed to close dex file in finalizer.
oldDexFiles.add(dexFile);
}
Method makePathElements = ShareReflectUtil.findMethod(originPathListObject, "makePathElements", List.class, File.class,
List.class);
ArrayList<IOException> suppressedExceptions = new ArrayList<>();
Object[] newDexElements = (Object[]) makePathElements.invoke(originPathListObject, additionalClassPathEntries, null, suppressedExceptions);
dexElement.set(originPathListObject, newDexElements);
try {
Class.forName(CHECK_CLASSLOADER_CLASS, true, androidNClassLoader);
} catch (Throwable thr) {
Log.e(TAG, "load TinkerTestAndroidNClassLoader fail, try to fixDexElementsForProtectedApp");
fixDexElementsForProtectedApp(application, newDexElements);
}
return androidNClassLoader;
}
private static void reflectPackageInfoClassloader(Application application, ClassLoader reflectClassLoader) throws Exception {
String defBase = "mBase";
String defPackageInfo = "mPackageInfo";
String defClassLoader = "mClassLoader";
Context baseContext = (Context) ShareReflectUtil.findField(application, defBase).get(application);
Object basePackageInfo = ShareReflectUtil.findField(baseContext, defPackageInfo).get(baseContext);
Field classLoaderField = ShareReflectUtil.findField(basePackageInfo, defClassLoader);
Thread.currentThread().setContextClassLoader(reflectClassLoader);
classLoaderField.set(basePackageInfo, reflectClassLoader);
}
public static AndroidNClassLoader inject(PathClassLoader originClassLoader, Application application) throws Exception {
AndroidNClassLoader classLoader = createAndroidNClassLoader(originClassLoader, application);
reflectPackageInfoClassloader(application, classLoader);
return classLoader;
}
// Basically this method would use base.apk to create a dummy DexFile object,
// then set its fileName, cookie, internalCookie field to the value
// comes from original DexFile object so that the encrypted dex would be taking effect.
private static void fixDexElementsForProtectedApp(Application application, Object[] newDexElements) throws Exception {
Field zipField = null;
Field dexFileField = null;
final Field mFileNameField = ShareReflectUtil.findField(DexFile.class, "mFileName");
final Field mCookieField = ShareReflectUtil.findField(DexFile.class, "mCookie");
final Field mInternalCookieField = ShareReflectUtil.findField(DexFile.class, "mInternalCookie");
// Always ignore the last element since it should always be the base.apk.
for (int i = 0; i < newDexElements.length - 1; ++i) {
final Object newElement = newDexElements[i];
if (zipField == null && dexFileField == null) {
zipField = ShareReflectUtil.findField(newElement, "zip");
dexFileField = ShareReflectUtil.findField(newElement, "dexFile");
}
final DexFile origDexFile = oldDexFiles.get(i);
final String origFileName = (String) mFileNameField.get(origDexFile);
final Object origCookie = mCookieField.get(origDexFile);
final Object origInternalCookie = mInternalCookieField.get(origDexFile);
final DexFile dupOrigDexFile = DexFile.loadDex(application.getApplicationInfo().sourceDir, null, 0);
mFileNameField.set(dupOrigDexFile, origFileName);
mCookieField.set(dupOrigDexFile, origCookie);
mInternalCookieField.set(dupOrigDexFile, origInternalCookie);
dexFileField.set(newElement, dupOrigDexFile);
// Just for better looking when dump new classloader.
// Avoid such output like this: DexPathList{zip file: /xx/yy/zz/uu.odex}
final File newZip = (File) zipField.get(newElement);
final String newZipPath = (newZip != null ? newZip.getAbsolutePath() : null);
if (newZipPath != null && !newZipPath.endsWith(".zip") && !newZipPath.endsWith(".jar") && !newZipPath.endsWith(".apk")) {
zipField.set(newElement, null);
}
}
}
// public static String getLdLibraryPath(ClassLoader loader) throws Exception {
// String nativeLibraryPath;
//
// nativeLibraryPath = (String) loader.getClass()
// .getMethod("getLdLibraryPath", new Class[0])
// .invoke(loader, new Object[0]);
//
// return nativeLibraryPath;
// }
public Class<?> findClass(String name) throws ClassNotFoundException {
// loader class use default pathClassloader to load
if ((name != null
&& name.startsWith("com.tencent.tinker.loader.")
&& !name.equals(SystemClassLoaderAdder.CHECK_DEX_CLASS)
&& !name.equals(CHECK_CLASSLOADER_CLASS))
|| (applicationClassName != null && TextUtils.equals(applicationClassName, name))) {
return originClassLoader.loadClass(name);
}
return super.findClass(name);
}
@Override
public String findLibrary(String name) {
return super.findLibrary(name);
}
}