one Abstract

With the increase of our business, our package size is getting bigger and bigger. The smaller the package size is, the higher the conversion rate of our APP will be. This chapter describes the unified processing logic of pictures. The biggest factor taking up package volume

  • So file, using dynamic loading so, do not pack SO in the package, see dynamic loading SO
  • Picture processing, using AOP ideas to process the APP, the third party AAR and the third party JAR in the picture, the content of this section

two Some ways to reduce package size

  • Compress the code to remove unused resources. Use ProGuard for compression
  • Reduce enumeration use
  • Streamlining language Resources
  • Using vector diagrams
  • Only specific densities are supported
  • Resource confusion AndroidResGuard
  • R File inline Bytex
  • pluggable

3. Write a plugin for compressing images and converting PNG and JPG to WebP

So let’s first think about how do we implement this function

  1. How to get a unified picture?
  2. What tools or code do you use to compress?
  3. How to convert compressed images into WebP?
  4. What images cannot be compressed and converted to WebP?

Based on question 1: we can look for the answer in the task packaged apK, and we can take the input file in the Task mergeResources. To get all the images and compress them, For specific packaging tasks, see completing the Android skill tree — from the AGP build process to the APK packaging process, After another from gradle 4 com. Android. Build. Gradle. Internal. AbstractAppTaskManager. In Java createCommonTasks method can also watch out. PNG can be compressed using PngCrush, PngQuant or Zopflipng, JPG can be compressed using packJPG and Guetzli. Some logos, as well as the larger ones after transformation, can be used

Start coding

Create the Plugin project

Create a plugin project, create a buildSrc directory in the main project, create a SRC /main folder, create resources in main to mark our plugin, and create a meta-INF /gradle-plugins folder in Resources. In creating com. Nzy. Plugin. The properties of profile, configuration inside our plugin, implementation – class = com. Nzy. Plugin. CwebpPlugin,

Create the Java plugin CwebpPlugin class

public class CwebpPlugin implements Plugin<Project> {
    int oldSize = 0;
    int newSize = 0;
    public static final String TAG = "CwebpPlugin :";
    /** * User config configuration */
    private WebpConfig mConfig;

    /** * Prints logs */
    private Logger mLogger;

    private ArrayList<String> bigImgList = new ArrayList<String>();

    private Project mProject;
    private AppExtension mAppExtension;

    private ArrayList<String> cacheList = new ArrayList<String>();

    /** * all image files */

    private ArrayList<File> imageFileList = new ArrayList<File>();

    @Override
    public void apply(Project project) {

        if(! project.getPlugins().hasPlugin(AppPlugin.class)) {throw new GradleException("The plugin must be used in the Android Application plugin");
        }
        // Get the Android configuration
        mAppExtension = project.getExtensions().getByType(AppExtension.class);

        // Create an extension to WebpConfig that users can use in build.gradle
        project.getExtensions().create("webpConfig", WebpConfig.class);

        mProject = project;
        mLogger = project.getLogger();
        LoggerUtil.sLogger = mLogger;
        printAllTask();
        convertTask();
    }

    /** * start conversion */

    private void convertTask(a) {
        // Just as with the introduction of the apply plugin: 'com.android.application', android{} can be configured.

        // The gradle implementation parses the build.gradle file. AfterEvaluate means to execute our code after parsing
        mProject.afterEvaluate(new Action<Project>() {
            @Override
            public void execute(@NotNull Project project) { initConfig(); }}); }/** * get the user configuration */
    private void initConfig(a) {
        mConfig = mProject.getExtensions().findByType(WebpConfig.class);
        int quality = mConfig.quality;
        ArrayList<String> whiteList = mConfig.whiteList;
        boolean debugOn = mConfig.debugOn;
        mLogger.log(LogLevel.ERROR, TAG + The quality of config is: + quality + "The whitelist is" + Arrays.toString(whiteList.toArray()) + "Debug mode enabled or not:" + debugOn);

        // Android projects have debug and release by default,
        // getApplicationVariants will be a collection of debug and release variants, and all will run through the collection
        mAppExtension.getApplicationVariants().all(new Action<ApplicationVariant>() {
            @Override
            public void execute(ApplicationVariant applicationVariant) {
                // The current user is in DEBUG mode, and the DEBUG run is not configured to perform hot fixes
                if (applicationVariant.getName().contains("debug") && !debugOn) {
                    return;
                }
                // Start compressing some columnsconvert(applicationVariant); }}); }private void convert(ApplicationVariant variant) {
        // Get: debug/release
        String variantName = variant.getName();
        // Uppercase
        String capitalizeName = Utils.capitalize(variantName);

        // This is the root path of the project
        String rootPath = mProject.getRootDir().getPath();

        // This is the address of the cwebP tool
        FileUtil.TOOLS_DIRPATH = rootPath + "/mctools/";

      

        // The first way to get resources
        getRes1(capitalizeName);


        // The second one gets resources
        //getRes2(variant, capitalizeName);


        // The third way to get resources
        //getRes3(variant);

    }


    /** * The first way to get resources **@param capitalizeName
     */
    private void getRes1(String capitalizeName) {
        / / get the android mergeDebugResources/mergeReleaseResources task
        final Task mergeResTask =
                mProject.getTasks().findByName("merge" + capitalizeName + "Resources");
        if(mergeResTask ! =null) {
            imageFileList.clear();
            cacheList.clear();
            mergeResTask.doFirst(new Action<Task>() {
                @Override
                public void execute(Task task) {
                    // All files output by the resource
                    TaskInputs outputs = mergeResTask.getInputs();
                    Set<File> files = outputs.getFiles().getFiles();
                    for (File file : files) {
                        // Walk through the folder to get all the images
                        traverseDir(file);
                    }
                    // Start compressionstartConvertAndCompress(); }}); }}/** * handle image compression task */
    private void startConvertAndCompress(a) {
        if (imageFileList.isEmpty()) {
            return;
        }
        for (File file : imageFileList) {
            / / compression
            CompressUtil.compressImg(file, mProject);
            / / webpConvertWebpUtil.securityFormatWebp(file, mConfig, mProject, mLogger); }}private void countNewSize(String path) {
        if (new File(path).exists()) {
            newSize += new File(path).length();
        } else {
            int indexOfDot = path.lastIndexOf(".");
            String webpPath = path.substring(0, indexOfDot) + ".webp";
            if (new File(webpPath).exists()) {
                newSize += new File(webpPath).length();
            }
        }
        mLogger.log(LogLevel.ERROR, TAG + "Compressed size: oldSize - newSize =" + (oldSize - newSize));
    }

    /** * Recursively traverse the folder to find the image to transform **@param file
     */
    private void traverseDir(File file) {
        if (cacheList.contains(file.getAbsolutePath())) {
            return;
        } else {
            cacheList.add(file.getAbsolutePath());
        }
        if (file.isDirectory()) {
            for (File listFile : file.listFiles()) {
                if (listFile.isDirectory()) {
                    traverseDir(listFile);
                } else{ filterImage(listFile); }}}else{ filterImage(file); }}/** * Filter invalid image files */
    private void filterImage(File file) {
        // If the image whitelist is added or the file is not an image format, filter it
        if(mConfig.whiteList.contains(file.getName()) || ! ImageUtil.isImage(file)) {return;
        }
// // If the image size is correct and the image is a large image, the large image whitelist has no image
        if((mConfig.isCheckSize && ImageUtil.isBigSizeImage(file, mConfig.maxSize)) && ! mConfig.bigImageWhiteList.contains(file.getName())) {// Add to the list of larger images
            bigImgList.add(file.getName());
            mLogger.log(LogLevel.ERROR, TAG + "Big picture address:" + imageFileList.size() + "-- -- -- -- --" + file.getAbsolutePath());
            throw new GradleException("There's a picture you need to eat and fix.");
        }
        // Add the image to the image directory
        imageFileList.add(file);
        mLogger.log(LogLevel.ERROR, TAG + "Picture address:" + imageFileList.size() + "-- -- -- -- --" + file.getAbsolutePath());

    }

    /** * Prints all executed tasks */
    private void printAllTask(a) {
        mProject.getGradle().getTaskGraph().whenReady(new Action<TaskExecutionGraph>() {
            @Override
            public void execute(TaskExecutionGraph taskGraph) {
                List<Task> allTasks = taskGraph.getAllTasks();
                for (int i = 0; i < allTasks.size(); i++) {
                    Task task = allTasks.get(i);
                    // Print out the names of all tasks
                    mLogger.log(LogLevel.ERROR, TAG + "All tasks:" + i + "-- -- -- -- --"+ task.getName()); }}}); }/** * third way to get resources ** /
    private void getRes3(ApplicationVariant variant) {
        Get the mergeResources task
        MergeResources mergeResources = variant.getMergeResourcesProvider().get();
        mergeResources.doFirst(new Action<Task>() {
            @Override
            public void execute(Task task) {
                TaskInputs outputs = task.getInputs();
                Set<File> files = outputs.getFiles().getFiles();
                // Traverses the resource file directory
                for(File file : files) { traverseDir(file); } startConvertAndCompress(); }}); }/** * The second way to get resources ** /
    private void getRes2(ApplicationVariant variant, String capitalizeName) {
        Get the mergeResources task
        MergeResources mergeResources = variant.getMergeResourcesProvider().get();
        // Create your own task
        Task convertTask = mProject.task("convertTask" + capitalizeName);
        convertTask.doLast(new Action<Task>() {
            @Override
            public void execute(@NotNull Task task) {
                Set<File> files = variant.getAllRawAndroidResources().getFiles();
                // Traverses the resource file directory
                for(File file : files) { traverseDir(file); } startConvertAndCompress(); }}); mergeResources.dependsOn(mProject.getTasks().findByName(convertTask.getName())); }}Copy the code

Copy the required tools to the McTools folder in the root directory

Design to CwebP, Guetzli and PngQuant, then we will directly traverse the resource folder, execute the corresponding command

In the end, both the main app and library were converted to WebP

The project addressgithub

reference

McImage