All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.tencent.tinker.build.gradle.transform.ImmutableDexTransform.groovy Maven / Gradle / Ivy

Go to download

Tinker is a hot-fix solution library for Android, it supports dex, library and resources update without reinstalling apk.

The newest version!
/*
 * 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.build.gradle.transform

import com.android.annotations.NonNull
import com.android.build.api.transform.*
import com.android.build.gradle.internal.pipeline.TransformManager
import com.android.build.gradle.internal.pipeline.TransformTask
import com.google.common.base.Joiner
import com.google.common.collect.Lists
import com.tencent.tinker.android.dex.ClassDef
import com.tencent.tinker.android.dex.Dex
import com.tencent.tinker.build.gradle.TinkerBuildPath
import com.tencent.tinker.build.immutable.ClassSimDef
import com.tencent.tinker.build.immutable.DexRefData
import com.tencent.tinker.build.util.FileOperation
import com.tencent.tinker.build.util.Utils
import org.gradle.api.Action
import org.gradle.api.GradleException
import org.gradle.api.Project
import org.gradle.api.Task
import org.gradle.api.execution.TaskExecutionGraph
import org.gradle.api.execution.TaskExecutionGraphListener
import org.gradle.api.tasks.JavaExec

import java.lang.reflect.Field
import java.util.zip.ZipEntry
import java.util.zip.ZipFile
import java.util.zip.ZipOutputStream

/**
 * Created by wangzhi on 16/11/24.
 */
public class ImmutableDexTransform extends Transform {

    public static final String TASK_WORK_DIR = "keep_dex"

    private static final Joiner PATH_JOINER = Joiner.on(File.separatorChar)

    Project project

    String oldApkPath

    File classPreDir

    File baseDexDir

    File mainDexListFile

    String varName

    String varDirName

    def variant

    def dexTransform


    ImmutableDexTransform(Project project, def variant, def dexTransform) {
        this.dexTransform = dexTransform
        this.project = project
        this.variant = variant
        this.varName = variant.name.capitalize()
        this.varDirName = variant.getDirName()
        this.oldApkPath = project.tinkerPatch.oldApk
        if (dexTransform.mainDexListFile instanceof File) {
            this.mainDexListFile = dexTransform.mainDexListFile
        } else {
            this.mainDexListFile = dexTransform.mainDexListFile.getSingleFile()
        }
    }

    public void initFileEnv(TransformOutputProvider outputProvider) {
        classPreDir = getDirInWorkDir("class_pre")
        baseDexDir = getDirInWorkDir("base_dex")

        classPreDir.mkdirs()
        baseDexDir.mkdirs()

        FileOperation.cleanDir(classPreDir)
        FileOperation.cleanDir(baseDexDir)
    }

    private File getDirInWorkDir(String name) {
        return new File(PATH_JOINER.join(
                TinkerBuildPath.getTinkerIntermediates(project),
                TASK_WORK_DIR,
                name,
                varDirName)
        )
    }


    @NonNull
    @Override
    public Set getOutputTypes() {
        return dexTransform.getOutputTypes()
    }

    @NonNull
    @Override
    public Collection getSecondaryFileInputs() {
        return dexTransform.getSecondaryFileInputs()
    }

    @NonNull
    @Override
    public Collection getSecondaryDirectoryOutputs() {
        return dexTransform.getSecondaryDirectoryOutputs()
    }

    @NonNull
    @Override
    public Map getParameterInputs() {
        return dexTransform.getParameterInputs()
    }

    @Override
    String getName() {
        return dexTransform.getName()
    }

    @Override
    Set getInputTypes() {
        return dexTransform.getInputTypes()
    }

    @Override
    Set getScopes() {
        return dexTransform.getScopes()
    }

    @Override
    boolean isIncremental() {
        return dexTransform.isIncremental()
    }

    @Override
    void transform(TransformInvocation transformInvocation) throws TransformException, IOException, InterruptedException {
        // because multi dex is enable,we only process jar file.
        List jarInputs = Lists.newArrayList()
        for (TransformInput input : transformInvocation.getInputs()) {
            jarInputs.addAll(input.getJarInputs())
        }
        //because the multi-dex is turned on,so the jarInput.size()==1 in theory.
        if (jarInputs.size() != 1) {
            project.logger.error("jar input size is ${jarInputs.size()}, expected is 1. we will skip immutable dex!")
            dexTransform.transform(transformInvocation)
            return
        }

        //init
        initFileEnv(transformInvocation.getOutputProvider())
        //get all old dex
        ArrayList oldDexList = new ArrayList<>()
        traversal(new ZipFile(oldApkPath), { ZipEntry zipEntry, byte[] bytes ->
            if (zipEntry.name.startsWith("classes") && zipEntry.name.endsWith(".dex")) {
                project.logger.info("find dex: ${zipEntry.name} in old apk. ")
                File classDxFile = new File(baseDexDir, zipEntry.name)
                classDxFile.withDataOutputStream { output ->
                    output.write(bytes, 0, bytes.length)
                    output.close()
                }
                oldDexList.add(classDxFile)
            }
        })

        //hashmap:classPath <==> dexName
        HashMap pathDexMap = new HashMap<>()
        project.logger.info("old dex list is : ${oldDexList}.")

        //hashmap:classPath<=>dexName
        oldDexList.each { dexFile ->
            Dex dex = new Dex(dexFile)
            dex.classDefs().each { ClassDef classDef ->
                String classPath = dex.typeNames().get(classDef.typeIndex)
                if (pathDexMap.get(classPath)) {
                    throw new GradleException("double class: ${classPath} in dex: ${dexFile.name} ")
                }
                pathDexMap.put(classPath, dexFile.name - ".dex")
            }
        }
        //the dex start index for orphan class
        int newDexIndex = oldDexList.size()
        //a hashset for maindexlist
        HashSet mainDexSets = initMainDexSet(mainDexListFile)
        project.logger.info("mainDexSets is ${mainDexSets}.")
        //zip file name <==> ZipOutputStream
        HashMap osMap = new HashMap<>()
        //zip file name <==> mtd count and filed count in the zip
        HashMap methodAndFieldsNum = new HashMap<>()
        //orphan class's entry <==> orphan class's bytes
        HashMap orphanMap = new HashMap()

        //all class  in allClass.jar
        HashSet allClassSet = new HashSet<>()
        //process all-classes.jar
        processJar(jarInputs.get(0).file, allClassSet, pathDexMap, mainDexSets, methodAndFieldsNum, osMap, orphanMap)

        Iterator> iterator = orphanMap.entrySet().iterator()
        Map.Entry leaveEntry = null
        while (iterator.hasNext()) {
            boolean writeResult = true
            while (writeResult && iterator.hasNext()) {
                if (leaveEntry != null) {
                    String newDexName = dexIndexToName(newDexIndex, "")
                    project.logger.info("write level orphan class: ${leaveEntry.key.name} to zip: ${newDexName}")
                    writeResult = writeClassToZip(methodAndFieldsNum, osMap, newDexName, leaveEntry.value.toByteArray(), leaveEntry.key)
                    if (!writeResult) {
                        throw new GradleException("add one class to a new zip failed!\n" +
                                "\t class:" + leaveEntry.key.name + "  zip: " + newDexName)
                    }
                }
                Map.Entry entry = iterator.next()
                leaveEntry = entry
                String newDexName = dexIndexToName(newDexIndex, "")
                project.logger.info("write orphan class: ${entry.key.name} to zip: ${newDexName}")
                writeResult = writeClassToZip(methodAndFieldsNum, osMap, newDexName, entry.value.toByteArray(), entry.key)
                if (writeResult) {
                    leaveEntry = null
                }
            }
            newDexIndex++
        }

        osMap.each { key, value ->
            value.close()
        }

        //a list for all dex's path,use for checkClassConsistence mtd
        ArrayList dexPathList = new ArrayList<>()

        def dxOutDir = transformInvocation.outputProvider.getContentLocation("main",
                getOutputTypes(), TransformManager.SCOPE_FULL_PROJECT, Format.DIRECTORY)
        if (dxOutDir.exists()) {
            FileOperation.cleanDir(dxOutDir)
        } else {
            dxOutDir.mkdirs()
        }

        classPreDir.eachFile { classZip ->
            String classIndexName = classZip.name - ".jar"
            String dexPath = "${dxOutDir.absolutePath}/${classIndexName}.dex"
            dexPathList.add(dexPath)
            doDex(dexPath, classZip, project.android.getDexOptions())
        }

        checkClassConsistence(dexPathList, allClassSet)

    }

    private void processJar(File jarFile,
                            HashSet allClassSet, HashMap pathDexMap, HashSet mainDexSets, HashMap methodAndFieldsNum, HashMap osMap, HashMap orphanMap) {


        ZipFile zipFile = new ZipFile(jarFile)
        //process class in maindexlist in first
        traversal(zipFile, { ZipEntry zipEntry, byte[] bytes ->
            if (zipEntry.name.endsWith(".class")) {
                if (mainDexSets.contains(zipEntry.name)) {
                    String classPath = rePathToClassPath(zipEntry.name)
                    allClassSet.add(classPath)
                    project.logger.info("process main dex list's class " + classPath)
                    if (!writeClassToZip(methodAndFieldsNum, osMap, "classes", bytes, zipEntry)) {
                        throw new GradleException("main dex is exceed the limit! reduce the class number on your main dex keep please.")
                    }
                }
            }
        })

        traversal(zipFile, {
            ZipEntry zipEntry,
            byte[] bytes ->
                if (zipEntry.name.endsWith(".class")) {
                    String classPath = rePathToClassPath(zipEntry.name)
                    if (!Utils.isBlank(classPath) && !allClassSet.contains(classPath)) {
                        allClassSet.add(classPath)
                        //get the old dex name which class be located
                        String belongDex = belongTo(pathDexMap, classPath)
                        //the class is new or method|fields exceeds limit
                        if (Utils.isBlank(belongDex) ||
                                !writeClassToZip(methodAndFieldsNum, osMap, belongDex, bytes, zipEntry)) {
                            if (Utils.isBlank(belongDex)) {
                                project.logger.warn("find new class: " + classPath)
                            }
                            saveOrphan(orphanMap, zipEntry, bytes)
                        }
                    } else {
                        if (Utils.isBlank(classPath)) {
                            project.logger.error("illegal zip entry: " + zipEntry.name)
                        }
                    }

                }
        })
    }

    public HashSet initMainDexSet(File mainDexList) {
        HashSet mainDexSets = new HashSet<>()
        BufferedReader reader = mainDexList.newReader()
        List lines = reader.readLines()
        lines.each {
            mainDexSets.add(it)
        }
        return mainDexSets
    }

    private String rePathToClassPath(String rePath) {
        int eIndex = rePath.lastIndexOf(".class")
        if (eIndex >= 0) {
            return "L${rePath.substring(0, eIndex)};"
        } else {
            return ""
        }
    }


    private void doDex(String dexPath, File classZip, def dexOptions) {

        def dexJar = "${project.android.getSdkDirectory()}/build-tools/${project.android.buildToolsVersion}/lib/dx.jar"
        def task = project.tasks.create("dx" + (classZip.name - ".jar") + varName, JavaExec.class, new Action() {
            @Override
            void execute(JavaExec javaExec) {
                ArrayList execArgs = new ArrayList()
                execArgs.add("--dex")
                if (dexOptions.getJumboMode()) {
                    execArgs.add("--force-jumbo")
                }
                if (dexOptions.getIncremental()) {
                    execArgs.add("--incremental")
                    execArgs.add("--no-strict")
                }
                execArgs.add("--output=${dexPath}")
                execArgs.add(classZip.absolutePath)
                project.logger.info(execArgs.toString())
                javaExec.setClasspath(project.files(dexJar))
                javaExec.setMain("com.android.dx.command.Main")
                javaExec.setArgs(execArgs)
            }
        })
        task.execute()
    }

    public static void inject(Project project, def variant) {
        project.logger.info("prepare inject dex transform ")
        if (!variant.mergedFlavor.multiDexEnabled) {
            project.logger.warn("multidex is disabled. we will not replace the dex transform.")
            return
        }
        if (!FileOperation.isLegalFile(project.tinkerPatch.oldApk)) {
            project.logger.warn("oldApk is illegal. we will not replace the dex transform.")
            return
        }
        try {
            Class.forName("com.android.build.gradle.internal.transforms.DexTransform")
        } catch (ClassNotFoundException e) {
            return
        }

        project.getGradle().getTaskGraph().addTaskExecutionGraphListener(new TaskExecutionGraphListener() {
            @Override
            public void graphPopulated(TaskExecutionGraph taskGraph) {
                for (Task task : taskGraph.getAllTasks()) {
                    if (task.project != project) {
                        continue
                    }
                    if (task instanceof TransformTask && task.name.toLowerCase().contains(variant.name.toLowerCase())) {
                        if (((TransformTask) task).getTransform().getClass() == Class.forName("com.android.build.gradle.internal.transforms.DexTransform") && !(((TransformTask) task).getTransform() instanceof ImmutableDexTransform)) {
                            project.logger.warn("find dex transform. transform class: " + task.transform.getClass() + " . task name: " + task.name)

                            def dexTransform = task.transform
                            ImmutableDexTransform hookDexTransform = new ImmutableDexTransform(project,
                                    variant, dexTransform)
                            project.logger.info("variant name: " + variant.name)

                            Field field = TransformTask.class.getDeclaredField("transform")
                            field.setAccessible(true)
                            field.set(task, hookDexTransform)
                            project.logger.warn("transform class after hook: " + task.transform.getClass())
                            break
                        }
                    }
                }
            }
        })

    }


    void checkClassConsistence(ArrayList dexPathList, HashSet allClassSet) {
        project.logger.info("start check class's consistence ..")
        if (dexPathList == null || dexPathList.size() == 0) {
            throw new GradleException("immutable dex list is null! ")
        }
        project.logger.info("check dex list: " + dexPathList)
        HashSet dexClassSet = new HashSet<>()
        int classSize = 0
        dexPathList.each { path ->
            File dexFile = new File(path)
            if (dexFile.isFile()) {
                Dex dex = new Dex(dexFile)
                classSize += dex.classDefs().size()
                for (ClassDef item : dex.classDefs()) {
                    int index = item.typeIndex
                    dexClassSet.add(dex.typeNames().get(index))
                }
            } else {
                throw new GradleException("dex: ${dexFile} is illegal!")
            }
        }

        HashSet hashSet1 = new HashSet<>(dexClassSet)
        HashSet hashSet2 = new HashSet<>(allClassSet)

        hashSet1.removeAll(allClassSet)
        hashSet2.removeAll(dexClassSet)

        if (hashSet1.size() != 0 || hashSet2.size() != 0) {
            throw new GradleException("class is inconsistent! " + "\n\t"
                    + "allClassSet size is " + allClassSet.size()
                    + ",dexClassSet size is " + dexClassSet.size() + "\n"
                    + "allClassSet has extra class: " + hashSet2 + ",\n"
                    + "dexClassSet has extra class: " + hashSet1 + ".\n"
            )
        } else {
            project.logger.info("check class consistence successful! ")
        }

    }

    boolean writeClassToZip(HashMap methodAndFieldsNum,
                            HashMap osMap,
                            String belongDex,
                            byte[] bytes,
                            ZipEntry zipEntry) {
        File jarFile = new File(classPreDir, belongDex + ".jar")
        DexRefData mfData = methodAndFieldsNum.get(jarFile.name)
        if (mfData == null) {
            mfData = new DexRefData()
            methodAndFieldsNum.put(jarFile.name, mfData)
        }
        ClassSimDef cf = new ClassSimDef(bytes, mfData.refFields, mfData.refMtds)
        ZipOutputStream zos = osMap.get(belongDex)
        if (zos == null) {
            project.logger.info("jarFile is  ${jarFile}.")
            zos = new ZipOutputStream(new FileOutputStream(jarFile))
            osMap.put(belongDex, zos)
        }
        if (!writeClassToZipNoCheck(mfData, cf, zos, zipEntry, bytes)) {
            project.logger.error("except limit! \n \tfind class ${zipEntry.name} method num: ${mfData.methodNum},field num: ${mfData.fieldNum},belong dex: ${belongDex} ")
            return false
        } else {
            return true
        }
    }

    boolean writeClassToZipNoCheck(DexRefData mfData, ClassSimDef cf, ZipOutputStream zos, ZipEntry zipEntry, byte[] bytes) {
        /**
         * In ClassSimDef, only the fields  which  methods referenced or in the class definition are scanned.
         * But in fact, some fields may be referenced in annotation. So the statistics in ClassSimDef is not complete.
         * The threshold is adjusted lower in order to avoid the troubles to calculate the fields referred by annotations.
         */
        if (mfData.methodNum + cf.methodCount >= 65536 || mfData.fieldNum + cf.fieldCount >= 64536) {
            return false
        } else {
            mfData.methodNum += cf.methodCount
            mfData.fieldNum += cf.fieldCount
            zos.putNextEntry(zipEntry)
            zos.write(bytes)
            zos.closeEntry()
            return true
        }
    }

    void saveOrphan(HashMap orphanMap, ZipEntry zipEntry, byte[] bytes) {
        ByteArrayOutputStream bos = new ByteArrayOutputStream(bytes.length)
        bos.write(bytes, 0, bytes.length)
        bos.flush()
        orphanMap.put(zipEntry, bos)
    }

    public static String getNextClassName(int index) {
        return "classes${index + 1}.dex"
    }

    public String dexIndexToName(int index, String suffix) {
        return "classes" + (index == 1 ? "" : index) + suffix
    }

    public String belongTo(HashMap pathDexMap, String classPath) {
        return pathDexMap.get(classPath)
    }

    public static void traversal(ZipFile zipFile, Closure callback) {
        try {
            Enumeration enumeration = zipFile.entries()
            while (enumeration.hasMoreElements()) {
                ZipEntry entry = enumeration.nextElement()
                callback.call(entry, zipFile.getInputStream(entry).bytes)
            }
        } catch (IOException e) {
            e.printStackTrace()
            Utils.closeQuietly(zipFile)
        }
    }
}







© 2015 - 2024 Weber Informatics LLC | Privacy Policy