android compile tasks中dex过程源码情景分析_incrementaltaskinputs has been deprecated. this is-程序员宅基地

0x00 前言

MultiDex中出现的main dex capacity exceeded解决之道中我们知道main dex的class可以由maindexlist.txt指定,Android MultiDex机制杂谈中我们分析了google MultiDex机制中Secondary dex的install过程,那么,我们的app在android gradle build过程中,.dex文件是怎么创建的呢? 再者,Secondary dex中的class是按什么顺序分配到不同dex中的呢?


0x01 android build system概述

为了解答上面的两个问题,本文将进一步分析android build system源码。
android build system是google提供的一组用来构建、运行、测试和打包我们app的工具集,包含了aaptaidljavacdexapkbuilderJarsignerzipalign等工具。在我们构建app时,build进程会去按一定顺序调用上述工具来生成相应文件,而最终的输出将会是一个完整的可安装的.apk文件,构建流程如下:

构建系统先从product flavors, build types和dependencies中合并资源,如果不同目录下有重名资源,将按以下优先级进行覆盖:

dependencies > build types > product flavors > main source directory
  1. aapt编译应用的资源文件(如AndroidManifest.xml),输出R.java文件
  2. aidl把.aidl文件转换为对应的java interface文件
  3. javac编译所有.java文件,输出.class文件
  4. dex工具把上面生成的.class文件转换为.dex文件
  5. apkbuilder把所有没编译的资源(如图片),编译过的资源和dex文件打包输出为.apk文件
  6. 在release模式下,用zipalign工具对.apk进行对齐处理,以减少运行时内存占用

本文重点对第4步中.class经过dex到.dex过程源码进行分析。


0x02 android compile tasks分析

为了更好地分析.dex的产生过程,本文设定情景如下:

构建工具为gradle,采用android plugin 'com.android.application',method数超过65535,需要进行multidex,并且指定了multiDexEnabled = true

在shell终端cd到project根目录,输入:

gradle assemble

gradle进程会启动,在dex之前,进程控制流将进入VariantManager. createTasksForVariantData。添加完assemble task依赖后,会去调用taskManager.createTasksForVariantData(tasks, variantData)。由于android plugin为’com.android.application’,这里的taskManager是ApplicationTaskManager。

com/android/build/gradle/internal/VariantManager.java

/**
 * Create tasks for the specified variantData.
 */
public void createTasksForVariantData(
        final TaskFactory tasks,
        final BaseVariantData<? extends BaseVariantOutputData> variantData) {
       

    // Add dependency of assemble task on assemble build type task.
    tasks.named("assemble", new Action<Task>() {
       
        @Override
        public void execute(Task task) {
       
            BuildTypeData buildTypeData = buildTypes.get(
                            variantData.getVariantConfiguration().getBuildType().getName());
            task.dependsOn(buildTypeData.getAssembleTask());
        }
    });
    ...
        taskManager.createTasksForVariantData(tasks, variantData);
    }
}

ApplicationTaskManager.createTasksForVariantData()会通过ThreadRecorder.get().record()第二个callback参数的类型为Recorder.Block<Void>,在call回调中调用父类TaskManager.createPostCompilationTasks。ThreadRecorder可以记录该任务的在当前线程的执行时间,并且保证task之间是串行的。

/**
 * TaskManager for creating tasks in an Android application project.
 */
public class ApplicationTaskManager extends TaskManager {
       

    @Override
    public void createTasksForVariantData(
            @NonNull final TaskFactory tasks,
            @NonNull final BaseVariantData<? extends BaseVariantOutputData> variantData) {
       
            ...
        // Add a compile task
        ThreadRecorder.get().record(ExecutionType.APP_TASK_MANAGER_CREATE_COMPILE_TASK,
                new Recorder.Block<Void>() {
       
                    @Override
                    public Void call() {
       
                        AndroidTask<JavaCompile> javacTask = createJavacTask(tasks, variantScope);

                        if (variantData.getVariantConfiguration().getUseJack()) {
       
                            createJackTask(tasks, variantScope);
                        } else {
       
                            setJavaCompilerTask(javacTask, tasks, variantScope);
                            createJarTask(tasks, variantScope);
                            createPostCompilationTasks(tasks, variantScope);
                        }
                        return null;
                    }
                });
                ...
    }
}

TaskManager.createPostCompilationTasks方法,这个方法比较长,我们分段来分析。

首先从config得到isMultiDexEnabled,isMultiDexEnabled,isLegacyMultiDexMode,由于已经假设当前为需要MultiDex的场景,因此isMultiDexEnabled为true。若isMinifyEnabled也为true,则说明输入jar包需要进行混淆,本场景先不考虑。

TaskManager.java


/**
 * Creates the post-compilation tasks for the given Variant.
 *
 * These tasks create the dex file from the .class files, plus optional intermediary steps like
 * proguard and jacoco
 *
 */
public void createPostCompilationTasks(TaskFactory tasks, @NonNull final VariantScope variantScope) {
       
    checkNotNull(variantScope.getJavacTask());

    final ApkVariantData variantData = (ApkVariantData) variantScope.getVariantData();
    final GradleVariantConfiguration config = variantData.getVariantConfiguration();

    TransformManager transformManager = variantScope.getTransformManager();
...
    boolean isMinifyEnabled = config.isMinifyEnabled();
    boolean isMultiDexEnabled = config.isMultiDexEnabled();
    boolean isLegacyMultiDexMode = config.isLegacyMultiDexMode();

    AndroidConfig extension = variantScope.getGlobalScope().getExtension();

在支持MultiDex的场景中,先创建manifestKeepListTask,将依赖设置为ManifestProcessorTask,这些android compile task由AndroidTask<TransformTask>类型来描述。

接着创建multiDexClassListTask,依赖manifestKeepListTask。这两个tasks用来输出maindexlist.txt,其中包含了MainDex中必须的class,可参见MultiDex中出现的main dex capacity exceeded解决之道


// ----- Multi-Dex support

AndroidTask<TransformTask> multiDexClassListTask = null;
// non Library test are running as native multi-dex
if (isMultiDexEnabled && isLegacyMultiDexMode) {
       
    if (AndroidGradleOptions.useNewShrinker(project)) {
       
        throw new IllegalStateException("New shrinker + multidex not supported yet.");
    }

    // ----------
    // create a transform to jar the inputs into a single jar.
    if (!isMinifyEnabled) {
       
        // merge the classes only, no need to package the resources since they are
        // not used during the computation.
        JarMergingTransform jarMergingTransform = new JarMergingTransform(
                TransformManager.SCOPE_FULL_PROJECT);
        transformManager.addTransform(tasks, variantScope, jarMergingTransform);
    }
    
    // ----------
    // Create a task to collect the list of manifest entry points which are
    // needed in the primary dex
    AndroidTask<CreateManifestKeepList> manifestKeepListTask = androidTasks.create(tasks,
            new CreateManifestKeepList.ConfigAction(variantScope));
    manifestKeepListTask.dependsOn(tasks,
            variantData.getOutputs().get(0).getScope().getManifestProcessorTask());

    // ---------
    // create the transform that's going to take the code and the proguard keep list
    // from above and compute the main class list.
    MultiDexTransform multiDexTransform = new MultiDexTransform(
            variantScope.getManifestKeepListFile(),
            variantScope,
            null);
    multiDexClassListTask = transformManager.addTransform(
            tasks, variantScope, multiDexTransform);
    multiDexClassListTask.dependsOn(tasks, manifestKeepListTask);
}

最后创建dexTask,这个用来把.class文件转为.dex的task,它依赖multiDexClassListTask。

    // create dex transform
    DexTransform dexTransform = new DexTransform(
            extension.getDexOptions(),
            config.getBuildType().isDebuggable(),
            isMultiDexEnabled,
            isMultiDexEnabled && isLegacyMultiDexMode ? variantScope.getMainDexListFile() : null,
            variantScope.getPreDexOutputDir(),
            variantScope.getGlobalScope().getAndroidBuilder(),
            getLogger());
    AndroidTask<TransformTask> dexTask = transformManager.addTransform(
            tasks, variantScope, dexTransform);
    // need to manually make dex task depend on MultiDexTransform since there's no stream
    // consumption making this automatic
    dexTask.optionalDependsOn(tasks, multiDexClassListTask);
}

task执行时,gradle引擎会去调用含有@TaskAction注解的方法,TransformTask类拥有Transfrom类型字段,其transform方法被标记为@TaskAction。同样通过ThreadRecorder.get().record中回调call(),执行transform.transform()

TransformTask.java

/**
 * A task running a transform.
 */
@ParallelizableTask
public class TransformTask extends StreamBasedTask implements Context {
       

    private Transform transform;
    ...
    @TaskAction
    void transform(final IncrementalTaskInputs incrementalTaskInputs)
            throws IOException, TransformException, InterruptedException {
       
		 ...
        ThreadRecorder.get().record(ExecutionType.TASK_TRANSFORM,
                new Recorder.Block<Void>() {
       
                    @Override
                    public Void call() throws Exception {
       
                        transform.transform(
                                TransformTask.this,
                                consumedInputs.getValue(),
                                referencedInputs.getValue(),
                                outputStream != null ? outputStream.asOutput() : null,
                                isIncremental.getValue());
                        return null;
                    }
                },
                new Recorder.Property("project", getProject().getName()),
                new Recorder.Property("transform", transform.getName()),
                new Recorder.Property("incremental", Boolean.toString(transform.isIncremental())));
    }

上述android compile tasks关系可以用下图描述:

从gradle task角度上看,这些task都属于TransformTask(继承至DefaultTask),它们区别仅在于transform字段。DexTask是本文主要关心的task,下面分析这个task执行过程中都做了什么。


0x03 DexTask执行过程分析

android build system中dex过程发生在DexTask,DexTask关联的Transform是DexTransform。

当DexTransform.transfrom方法被调用时,会先创建并初始化main目录作为输出dex的目录,然后调用androidBuilder.convertByteCode方法进行.class到.dex的转换,此时jarInputs为classes.jar,directoryInputs长度为空,传递的boolean类型的multiDex参数来自build.gralde文件中在defaultConfigmultiDexEnabled = true的设置。

DexTransform.java

@Override
public void transform(
        @NonNull Context context,
        @NonNull Collection<TransformInput> inputs,
        @NonNull Collection<TransformInput> referencedInputs,
        @Nullable TransformOutputProvider outputProvider,
        boolean isIncremental) throws TransformException, IOException, InterruptedException {
       
        ...
    // Gather a full list of all inputs.
    List<JarInput> jarInputs = Lists.newArrayList();
    List<DirectoryInput> directoryInputs = Lists.newArrayList();
    for (TransformInput input : inputs) {
       
        jarInputs.addAll(input.getJarInputs());
        directoryInputs.addAll(input.getDirectoryInputs());
    }
    
    
    try {
       
        // if only one scope or no per-scope dexing, just do a single pass that
        // runs dx on everything.
        if ((jarInputs.size() + directoryInputs.size()) == 1 || !dexOptions.getPreDexLibraries()) {
       
            File outputDir = outputProvider.getContentLocation("main",
                    getOutputTypes(), getScopes(),
                    Format.DIRECTORY);
            FileUtils.mkdirs(outputDir);

            // first delete the output folder where the final dex file(s) will be.
            FileUtils.emptyFolder(outputDir);

            // gather the inputs. This mode is always non incremental, so just
            // gather the top level folders/jars
            final List<File> inputFiles = Lists.newArrayList();
            for (JarInput jarInput : jarInputs) {
       
                inputFiles.add(jarInput.getFile());
            }

            for (DirectoryInput directoryInput : directoryInputs) {
       
                inputFiles.add(directoryInput.getFile());
            }

            androidBuilder.convertByteCode(
                    inputFiles,
                    outputDir,
                    multiDex,
                    mainDexListFile,
                    dexOptions,
                    null,
                    false,
                    true,
                    new LoggedProcessOutputHandler(logger));
        } else {
       

为了把输入的.class转换为.dex,AndroidBuilder.convertByteCode会另起进程去做dex,实际上是在新进程中exec dex工具,接下来我们进入dex源码,看看到底发生了什么。

 public void convertByteCode(
         @NonNull Collection<File> inputs,
         @NonNull File outDexFolder,
                  boolean multidex,
         @Nullable File mainDexList,
         @NonNull DexOptions dexOptions,
         @Nullable List<String> additionalParameters,
         boolean incremental,
         boolean optimize,
         @NonNull ProcessOutputHandler processOutputHandler)
         throws IOException, InterruptedException, ProcessException {
       
...
     BuildToolInfo buildToolInfo = mTargetInfo.getBuildTools();
     DexProcessBuilder builder = new DexProcessBuilder(outDexFolder);

     builder.setVerbose(mVerboseExec)
             .setIncremental(incremental)
             .setNoOptimize(!optimize)
             .setMultiDex(multidex)
             .setMainDexList(mainDexList)
             .addInputs(verifiedInputs.build());

     if (additionalParameters != null) {
       
         builder.additionalParameters(additionalParameters);
     }

     JavaProcessInfo javaProcessInfo = builder.build(buildToolInfo, dexOptions);

     ProcessResult result = mJavaProcessExecutor.execute(javaProcessInfo, processOutputHandler);
     result.rethrowFailure().assertNormalExitValue();
 }

0x04 dex过程分析

android 5.0中dex工具源码路径是dalvik/dx/src/com/android/dx,入口类是com.android.dx.command.Main,当解析到参数–dex时,转入com.android.dx.command.dexer.Main.main()

 public static void main(String[] args) {
       
...
     try {
       
	...
             if (arg.equals("--dex")) {
       
                 com.android.dx.command.dexer.Main.main(without(args, i));
                 break;
             } else if (arg.equals("--dump")) {
       
                 com.android.dx.command.dump.Main.main(without(args, i));
                 break;
             }
             ...
         }

main会调用com.android.dx.command.dexer.Main.run(),此时args.multiDex为true,直接进入runMultiDex

com.android.dx.command.dexer.Main.java

public static int run(Arguments arguments) throws IOException {
       
 ...
    try {
       
        if (args.multiDex) {
       
            return runMultiDex();
        } else {
       
            return runMonoDex();
        }
    } finally {
       
        closeOutput(humanOutRaw);
    }
}

runMultiDex会调用processAllFiles,第一行代码调用createDexFile()


 private static boolean processAllFiles() {
       
     createDexFile();
...

createDexFile先检查outputDex(: DexFile)字段是否为空,不为空则调用writeDex()把该dex的byte[]添加到dexOutputArrays(: List<byte[]>)。

writeDex()具体是通过outputDex.toDex(humanOutWriter, args.verboseDump)得到dex的byte[]。java中数组的下标是int类型,长度为32bits,因此一个dex文件最大理论是4G,但实际由于method, field数等限制,正常最大也就10M左右。

然后还会为outputDex字段新建一个DexFile对象,表示当前dex文件已经处理完毕,可以开始处理新的dex文件了。这里假设进程第一次执行createDexFile,因此outputDex为null。

private static void createDexFile() {
       
    if (outputDex != null) {
       
        dexOutputArrays.add(writeDex());
    }

    outputDex = new DexFile(args.dexOptions);

    if (args.dumpWidth != 0) {
       
        outputDex.setDumpWidth(args.dumpWidth);
    }
}

随后processAllFiles会根据args中numThreads来决定是否需要创建线程池。

if (args.numThreads > 1) {
       
    threadPool = Executors.newFixedThreadPool(args.numThreads);
    parallelProcessorFutures = new ArrayList<Future<Void>>();
}

接下来判断args.mainDexListFile,不为空说明指定了maindexlist.txt文件,这里假设不为空,filesNames数组是{‘path/way/to/classes.jar’},长度为1。方法在for循环中调用processOne()

...
   anyFilesProcessed = false;
   String[] fileNames = args.fileNames;
   ...
   try {
       
       if (args.mainDexListFile != null) {
       
           // with --main-dex-list
           FileNameFilter mainPassFilter = args.strictNameCheck ? new MainDexListFilter() :
               new BestEffortMainDexListFilter();

           // forced in main dex
           for (int i = 0; i < fileNames.length; i++) {
       
               processOne(fileNames[i], mainPassFilter);
           }

processOne调用ClassPathOpener.process处理输入的classes.jar。ClassPathOpener会遍历classes.jar中的每个ZipEntry,读出byte[],对每个ZipEntry在回调processFileBytes中调用Main.processFileBytes方法。

/**
 * Processes one pathname element.
 *
 * @param pathname {
        @code non-null;} the pathname to process. May
 * be the path of a class file, a jar file, or a directory
 * containing class files.
 * @param filter {
        @code non-null;} A filter for excluding files.
 */
private static void processOne(String pathname, FileNameFilter filter) {
       
    ClassPathOpener opener;

    opener = new ClassPathOpener(pathname, false, filter,
            new ClassPathOpener.Consumer() {
       

        @Override
        public boolean processFileBytes(String name, long lastModified, byte[] bytes) {
       
            return Main.processFileBytes(name, lastModified, bytes);
        }
...
   });

    if (args.numThreads > 1) {
       
        parallelProcessorFutures.add(threadPool.submit(new ParallelProcessor(opener)));
    } else {
       
        if (opener.process()) {
       
            anyFilesProcessed = true;
        }
    }
}

Main.processFileBytes把输入的bytes分为三类:

  • .class文件
  • .dex文件
  • 资源文件

如果输入是.dex或资源文件,则把bytes分别写入libraryDexBuffers字段或outputResources字段,此时输入name(: String)为.class。当发现是class,则进一步调用processClass处理


 /**
  * Processes one file, which may be either a class or a resource.
  *
  * @param name {
        @code non-null;} name of the file
  * @param bytes {
        @code non-null;} contents of the file
  * @return whether processing was successful
  */
 private static boolean processFileBytes(String name, long lastModified, byte[] bytes) {
       
     boolean isClass = name.endsWith(".class");
     boolean isClassesDex = name.equals(DexFormat.DEX_IN_JAR_NAME);
     boolean keepResources = (outputResources != null);
... 
     String fixedName = fixPath(name);

     if (isClass) {
       

         if (keepResources && args.keepClassesInJar) {
       
             synchronized (outputResources) {
       
                 outputResources.put(fixedName, bytes);
             }
         }
         if (lastModified < minimumFileAge) {
       
             return true;
         }
         return processClass(fixedName, bytes);
     } else if (isClassesDex) {
       
         synchronized (libraryDexBuffers) {
       
             libraryDexBuffers.add(bytes);
         }
         return true;
     } else {
       
         synchronized (outputResources) {
       
             outputResources.put(fixedName, bytes);
         }
         return true;
     }
 }

processClass方法主要做了以下几件事:

  1. 为传入的class创建DirectClassFile对象,对应.class字节码文件
  2. 得到已经生成的dex的numMethodIds,numFieldIds
  3. 得到新Class的constantPoolSize,计算maxMethodIdsInDex = numMethodIds + constantPoolSize + 新Class的方法数 + 2个预留method, 计算maxFieldIdsInDex = numFieldIds + constantPoolSize + 新Class的字段数 + 9个预留field
  4. 一旦发现maxMethodIdsInDex > args.maxNumberOfIdxPerDex 或者 maxFieldIdsInDex > args.maxNumber OfIdxPerDex,说明当前dex已经满了,调用createDexFile创建新dex来容纳该Class
  5. 否则,通过CfTranslator.translate方法将输入的DirectClassFile对象,得到ClassDefItem,添加到outputDex(: DexFile)

由此可以看出:

secondray dex中的class是根据classes.jar中ZipEntry的遍历顺序添加的。


/**
  * Processes one classfile.
  *
  * @param name {
        @code non-null;} name of the file, clipped such that it
  * <i>should</i> correspond to the name of the class it contains
  * @param bytes {
        @code non-null;} contents of the file
  * @return whether processing was successful
  */
 private static boolean processClass(String name, byte[] bytes) {
       
     if (! args.coreLibrary) {
       
         checkClassName(name);
     }

     DirectClassFile cf =
         new DirectClassFile(bytes, name, args.cfOptions.strictNameCheck);

     cf.setAttributeFactory(StdAttributeFactory.THE_ONE);
     cf.getMagic();

     int numMethodIds = outputDex.getMethodIds().items().size();
     int numFieldIds = outputDex.getFieldIds().items().size();
     int constantPoolSize = cf.getConstantPool().size();

     int maxMethodIdsInDex = numMethodIds + constantPoolSize + cf.getMethods().size() +
             MAX_METHOD_ADDED_DURING_DEX_CREATION;
     int maxFieldIdsInDex = numFieldIds + constantPoolSize + cf.getFields().size() +
             MAX_FIELD_ADDED_DURING_DEX_CREATION;

     if (args.multiDex
         // Never switch to the next dex if current dex is already empty
         && (outputDex.getClassDefs().items().size() > 0)
         && ((maxMethodIdsInDex > args.maxNumberOfIdxPerDex) ||
             (maxFieldIdsInDex > args.maxNumberOfIdxPerDex))) {
       
         DexFile completeDex = outputDex;
         createDexFile();
         assert  (completeDex.getMethodIds().items().size() <= numMethodIds +
                 MAX_METHOD_ADDED_DURING_DEX_CREATION) &&
                 (completeDex.getFieldIds().items().size() <= numFieldIds +
                 MAX_FIELD_ADDED_DURING_DEX_CREATION);
     }

     try {
       
         ClassDefItem clazz =
             CfTranslator.translate(cf, bytes, args.cfOptions, args.dexOptions, outputDex);
         synchronized (outputDex) {
       
             outputDex.add(clazz);
         }
         return true;

     } catch (ParseException ex) {
       
         DxConsole.err.println("\ntrouble processing:");
         if (args.debug) {
       
             ex.printStackTrace(DxConsole.err);
         } else {
       
             ex.printContext(DxConsole.err);
         }
     }
     errors.incrementAndGet();
     return false;
 }

再回到processAllFiles,前面假设指定了maindexlist,如果minialMainDex也为true的话,会立即创建新的DexFile,保证这个main dex中只包含maindexlist里的类,如何指定可以参考MultiDex中出现的main dex capacity exceeded解决之道 0x05。前面没有过滤掉的class都会放入到secondary dex。


        if (dexOutputArrays.size() > 0) {
       
            throw new DexException("Too many classes in " + Arguments.MAIN_DEX_LIST_OPTION
                    + ", main dex capacity exceeded");
        }

        if (args.minimalMainDex) {
       
            // start second pass directly in a secondary dex file.
            createDexFile();
        }

        // remaining files
        for (int i = 0; i < fileNames.length; i++) {
       
            processOne(fileNames[i], new NotFilter(mainPassFilter));
        }
    } else {
       
        // without --main-dex-list
        for (int i = 0; i < fileNames.length; i++) {
       
            processOne(fileNames[i], ClassPathOpener.acceptAll);
        }
    }
} catch (StopProcessing ex) {
       
    /*
     * Ignore it and just let the error reporting do
     * their things.
     */
}

在runMultiDex的最后,dex文件将以classes(..N).dex的形式输出在由args.outName指定的目录之下。

private static int runMultiDex() throws IOException {
       
		...
        } else if (args.outName != null) {
       
            File outDir = new File(args.outName);
            assert outDir.isDirectory();
            for (int i = 0; i < dexOutputArrays.size(); i++) {
       
                OutputStream out = new FileOutputStream(new File(outDir, getDexFileName(i)));
                try {
       
                    out.write(dexOutputArrays.get(i));
                } finally {
       
                    closeOutput(out);
                }
            }

        }

0x05 结论

通过对android build system中android plugin tasks和dx工具源码的分析,我们可以得出如下结论:

  • .dex文件本质上是.class文件经过com.android.dx.dex.file.DexFile.toDex方法转换得到

  • Secondary dex是在指定了multiDexEnabled = true且MainDex满足65535限制,或者指定multiDexEnabled = true和minimalMainDex = true的情况下,才会创建的dex,其包含的class是根据classes.jar中ZipEntry的遍历顺序添加的。


版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/bd_zengxinxin/article/details/52250512

智能推荐

leetcode 172. 阶乘后的零-程序员宅基地

文章浏览阅读63次。题目给定一个整数 n,返回 n! 结果尾数中零的数量。解题思路每个0都是由2 * 5得来的,相当于要求n!分解成质因子后2 * 5的数目,由于n中2的数目肯定是要大于5的数目,所以我们只需要求出n!中5的数目。C++代码class Solution {public: int trailingZeroes(int n) { ...

Day15-【Java SE进阶】IO流(一):File、IO流概述、File文件对象的创建、字节输入输出流FileInputStream FileoutputStream、释放资源。_outputstream释放-程序员宅基地

文章浏览阅读992次,点赞27次,收藏15次。UTF-8是Unicode字符集的一种编码方案,采取可变长编码方案,共分四个长度区:1个字节,2个字节,3个字节,4个字节。文件字节输入流:每次读取多个字节到字节数组中去,返回读取的字节数量,读取完毕会返回-1。注意1:字符编码时使用的字符集,和解码时使用的字符集必须一致,否则会出现乱码。定义一个与文件一样大的字节数组,一次性读取完文件的全部字节。UTF-8字符集:汉字占3个字节,英文、数字占1个字节。GBK字符集:汉字占2个字节,英文、数字占1个字节。GBK规定:汉字的第一个字节的第一位必须是1。_outputstream释放

jeecgboot重新登录_jeecg 登录自动退出-程序员宅基地

文章浏览阅读1.8k次,点赞3次,收藏3次。解决jeecgboot每次登录进去都会弹出请重新登录问题,在utils文件下找到request.js文件注释这段代码即可_jeecg 登录自动退出

数据中心供配电系统负荷计算实例分析-程序员宅基地

文章浏览阅读3.4k次。我国目前普遍采用需要系数法和二项式系数法确定用电设备的负荷,其中需要系数法是国际上普遍采用的确定计算负荷的方法,最为简便;而二项式系数法在确定设备台数较少且各台设备容量差..._数据中心用电负荷统计变压器

HTML5期末大作业:网页制作代码 网站设计——人电影网站(5页) HTML+CSS+JavaScript 学生DW网页设计作业成品 dreamweaver作业静态HTML网页设计模板_网页设计成品百度网盘-程序员宅基地

文章浏览阅读7k次,点赞4次,收藏46次。HTML5期末大作业:网页制作代码 网站设计——人电影网站(5页) HTML+CSS+JavaScript 学生DW网页设计作业成品 dreamweaver作业静态HTML网页设计模板常见网页设计作业题材有 个人、 美食、 公司、 学校、 旅游、 电商、 宠物、 电器、 茶叶、 家居、 酒店、 舞蹈、 动漫、 明星、 服装、 体育、 化妆品、 物流、 环保、 书籍、 婚纱、 军事、 游戏、 节日、 戒烟、 电影、 摄影、 文化、 家乡、 鲜花、 礼品、 汽车、 其他 等网页设计题目, A+水平作业_网页设计成品百度网盘

【Jailhouse 文章】Look Mum, no VM Exits_jailhouse sr-iov-程序员宅基地

文章浏览阅读392次。jailhouse 文章翻译,Look Mum, no VM Exits!_jailhouse sr-iov

随便推点

chatgpt赋能python:Python怎么删除文件中的某一行_python 删除文件特定几行-程序员宅基地

文章浏览阅读751次。本文由chatgpt生成,文章没有在chatgpt生成的基础上进行任何的修改。以上只是chatgpt能力的冰山一角。作为通用的Aigc大模型,只是展现它原本的实力。对于颠覆工作方式的ChatGPT,应该选择拥抱而不是抗拒,未来属于“会用”AI的人。AI职场汇报智能办公文案写作效率提升教程 专注于AI+职场+办公方向。下图是课程的整体大纲下图是AI职场汇报智能办公文案写作效率提升教程中用到的ai工具。_python 删除文件特定几行

Java过滤特殊字符的正则表达式_java正则表达式过滤特殊字符-程序员宅基地

文章浏览阅读2.1k次。【代码】Java过滤特殊字符的正则表达式。_java正则表达式过滤特殊字符

CSS中设置背景的7个属性及简写background注意点_background设置背景图片-程序员宅基地

文章浏览阅读5.7k次,点赞4次,收藏17次。css中背景的设置至关重要,也是一个难点,因为属性众多,对应的属性值也比较多,这里详细的列举了背景相关的7个属性及对应的属性值,并附上演示代码,后期要用的话,可以随时查看,那我们坐稳开车了······1: background-color 设置背景颜色2:background-image来设置背景图片- 语法:background-image:url(相对路径);-可以同时为一个元素指定背景颜色和背景图片,这样背景颜色将会作为背景图片的底色,一般情况下设置背景..._background设置背景图片

Win10 安装系统跳过创建用户,直接启用 Administrator_windows10msoobe进程-程序员宅基地

文章浏览阅读2.6k次,点赞2次,收藏8次。Win10 安装系统跳过创建用户,直接启用 Administrator_windows10msoobe进程

PyCharm2021安装教程-程序员宅基地

文章浏览阅读10w+次,点赞653次,收藏3k次。Windows安装pycharm教程新的改变功能快捷键合理的创建标题,有助于目录的生成如何改变文本的样式插入链接与图片如何插入一段漂亮的代码片生成一个适合你的列表创建一个表格设定内容居中、居左、居右SmartyPants创建一个自定义列表如何创建一个注脚注释也是必不可少的KaTeX数学公式新的甘特图功能,丰富你的文章UML 图表FLowchart流程图导出与导入导出导入下载安装PyCharm1、进入官网PyCharm的下载地址:http://www.jetbrains.com/pycharm/downl_pycharm2021

《跨境电商——速卖通搜索排名规则解析与SEO技术》一一1.1 初识速卖通的搜索引擎...-程序员宅基地

文章浏览阅读835次。本节书摘来自异步社区出版社《跨境电商——速卖通搜索排名规则解析与SEO技术》一书中的第1章,第1.1节,作者: 冯晓宁,更多章节内容可以访问云栖社区“异步社区”公众号查看。1.1 初识速卖通的搜索引擎1.1.1 初识速卖通搜索作为速卖通卖家都应该知道,速卖通经常被视为“国际版的淘宝”。那么请想一下,普通消费者在淘宝网上购买商品的时候,他的行为应该..._跨境电商 速卖通搜索排名规则解析与seo技术 pdf

推荐文章

热门文章

相关标签